[
  {
    "path": ".ai/README.md",
    "content": "# RedisInsight AI Development Rules\n\nThis directory contains the **single source of truth** for AI-assisted development rules and workflows in RedisInsight.\n\n## Overview\n\nThis repository uses a centralized approach to AI development rules:\n\n- **`AGENTS.md`** (at repository root) - Entry point for AI agents with essential commands, testing instructions, and quick reference\n- **`.ai/rules/`** - Detailed development standards organized by topic\n- **`.ai/commands/`** - AI workflow commands and templates\n\nThese rules are used by multiple AI coding assistants:\n\n- **Cursor** (via symlinks: `.cursor/rules/` and `.cursor/commands/`)\n- **Augment** (via symlink: `.augment/`)\n- **Windsurf** (via symlink: `.windsurfrules`)\n- **GitHub Copilot** (via file: `.github/copilot-instructions.md`)\n\n## Structure\n\n```\nAGENTS.md                              # 🎯 AI agent entry point\n.ai/                                   # Single source of truth\n├── README.md                          # This file (human-readable overview)\n├── rules/                             # Development standards (modular)\n│   ├── code-quality.md                # Linting, TypeScript standards\n│   ├── frontend.md                    # React, Redux, UI patterns\n│   ├── backend.md                     # NestJS, API patterns\n│   ├── testing.md                     # Testing standards\n│   ├── branches.md                    # Branch naming conventions\n│   ├── commits.md                     # Commit message guidelines\n│   └── pull-requests.md               # Pull request process\n└── commands/                          # AI workflow commands\n    ├── pr-plan.md                     # JIRA ticket implementation planning\n    ├── commit-message.md              # Commit message generation\n    └── pull-request-review.md         # PR review workflow\n\n# Symlinks (all AI tools read from .ai/)\n.cursor/\n  ├── rules/ -> ../.ai/rules/          # Cursor AI (rules)\n  └── commands/ -> ../.ai/commands/  # Cursor AI (commands)\n.augment/ -> .ai/                      # Augment AI\n.windsurfrules -> .ai/                 # Windsurf AI\n.github/copilot-instructions.md        # GitHub Copilot\n```\n\n## For AI Agents\n\n**Start here**: Read `AGENTS.md` at the repository root for:\n\n- Setup and build commands\n- Code quality standards\n- Testing instructions\n- Git workflow guidelines\n- Boundaries and best practices\n\n**Then refer to**: `.ai/rules/` for detailed guidelines on specific topics.\n\n## For Human Developers\n\nThis directory contains comprehensive development standards that are automatically used by AI coding assistants. The rules are organized into modular files for easy maintenance:\n\n- **Code Quality Standards**: `.ai/rules/code-quality.md` - TypeScript standards, import organization, best practices\n- **Frontend Patterns**: `.ai/rules/frontend.md` - React, Redux, styled-components, UI component usage\n- **Backend Patterns**: `.ai/rules/backend.md` - NestJS, dependency injection, API patterns\n- **Testing Standards**: `.ai/rules/testing.md` - Testing patterns, faker usage, test helpers\n- **Branch Naming**: `.ai/rules/branches.md` - Branch naming conventions\n- **Commit Messages**: `.ai/rules/commits.md` - Commit message guidelines (Conventional Commits)\n- **Pull Request Process**: `.ai/rules/pull-requests.md` - PR creation and review guidelines\n\n## MCP (Model Context Protocol) Setup\n\nAI tools can access external services (JIRA, Confluence, GitHub, Figma) via MCP configuration.\n\n### Initial Setup\n\n1. **Copy the example configuration:**\n\n   ```bash\n   cp env.mcp.example .env.mcp\n   ```\n\n2. **Get your Atlassian API token:**\n\n   - Go to: https://id.atlassian.com/manage-profile/security/api-tokens\n   - Create a classic token by pressing the first \"Create Token\" button\n   - Copy the token\n\n3. **Edit `.env.mcp` with your credentials:**\n\n   - Add your JIRA and Confluence API tokens\n   - Note: Figma MCP server uses OAuth authentication and doesn't require API keys\n\n4. **Verify your setup:**\n\n   **For Cursor users:**\n\n   - Restart Cursor to load the new MCP configuration\n   - Ask the AI: \"Can you list all available MCP tools and test them?\"\n   - The AI should be able to access JIRA, Confluence, GitHub, Figma, and other configured services\n   - **For Figma**: On first use, you'll be prompted to authenticate via OAuth flow in your browser\n\n   **For Augment users:**\n\n   ```bash\n   npx @augmentcode/auggie --mcp-config mcp.json \"go over all my mcp tools and make sure they work as expected\"\n   ```\n\n   **For GitHub Copilot users:**\n\n   - Note: GitHub Copilot does not currently support MCP integration\n   - MCP services (JIRA, Confluence, etc.) will not be available in Copilot\n\n### Available MCP Services\n\nThe `mcp.json` file configures these services:\n\n- **github** - GitHub integration (issues, PRs, repository operations)\n- **memory** - Persistent context storage across sessions\n- **sequential-thinking** - Enhanced reasoning for complex tasks\n- **context-7** - Advanced context management\n- **atlassian** - JIRA (RI-XXX tickets) and Confluence integration (requires API tokens in `.env.mcp`)\n- **figma** - Figma design files, frames, and layers (uses OAuth authentication - no API key needed)\n\n## Updating These Rules\n\nTo update AI rules:\n\n1. **Edit files in `.ai/` only** (never edit symlinked files directly)\n2. **Update `AGENTS.md`** if you change commands, testing instructions, or boundaries\n3. Changes automatically propagate to all AI tools via symlinks\n4. Commit changes to version control\n\n**Remember**: These rules exist to maintain code quality and consistency. Follow them, but also use good judgment.\n"
  },
  {
    "path": ".ai/commands/commit-message.md",
    "content": "# Commit Message Generation\n\nGenerate concise, meaningful commit messages following RedisInsight conventions.\n\n## Format\n\n```\n<type>(<scope>): <description>\n\n[optional body]\n\nReferences: #RI-XXX\n```\n\n## Types & Scopes\n\n**Types**: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`, `ci`\n\n**Scopes**: `api`, `ui`, `e2e`, `deps`\n\n## Rules\n\n**DO:**\n- ✅ Always include scope: `feat(api):`, `fix(ui):`\n- ✅ Use imperative mood: \"add feature\" not \"added feature\"\n- ✅ Start with lowercase after scope\n- ✅ Keep subject under 250 characters\n- ✅ Inspect all uncommitted files before generating\n\n**DON'T:**\n- ❌ Omit scope\n- ❌ Use past tense\n- ❌ Add period at end\n- ❌ Use multiple scopes (split into separate commits)\n\n## Examples\n\n```bash\nfeat(ui): add user profile editing\nfix(api): resolve memory leak in connection pool\nrefactor(api): extract validation logic\ntest(e2e): add authentication tests\nchore(deps): upgrade React to 18.2\n```\n\n## Issue References\n\n**JIRA**: `References: #RI-123` or `Fixes #RI-123`\n**GitHub**: `Fixes #123` or `Closes #123`\n\n## Process\n\n1. Run `git status && git diff`\n2. Identify scope: API → `api`, UI → `ui`, Both → separate commits\n3. Identify type: New → `feat`, Bug → `fix`, Improvement → `refactor`\n4. Write description (what changed and why)\n5. Add issue reference in body\n\n## Multiple Scopes\n\nSplit into separate commits:\n\n```bash\n# ✅ Good\ngit commit -m \"feat(api): add user endpoint\n\nReferences: #RI-123\"\n\ngit commit -m \"feat(ui): add user interface\n\nReferences: #RI-123\"\n\n# ❌ Bad\ngit commit -m \"feat(api,ui): add user feature\"\n```\n\n## Output Format\n\nPresent in copyable format:\n\n```markdown\nBased on the changes, here's your commit message:\n\n\\`\\`\\`\nfeat(api): add OAuth 2.0 authentication\n\nImplements OAuth flow with token management\nand refresh token support.\n\nReferences: #RI-123\n\\`\\`\\`\n```\n\nIf multiple scopes:\n\n```markdown\nChanges span multiple scopes. I recommend two commits:\n\n**Commit 1:**\n\\`\\`\\`\nfeat(api): add OAuth endpoints\n\nReferences: #RI-123\n\\`\\`\\`\n\n**Commit 2:**\n\\`\\`\\`\nfeat(ui): add OAuth login interface\n\nReferences: #RI-123\n\\`\\`\\`\n```\n"
  },
  {
    "path": ".ai/commands/e2e-fix.md",
    "content": "---\ndescription: Run E2E tests and fix any failures\nargument-hint: <test-pattern>\n---\n\n# Fix E2E Tests\n\nRun E2E tests matching a pattern, analyze failures, and fix them.\n\n**Follow all standards in `.ai/rules/e2e-testing.md`**\n\n## Input\n\n**Test pattern** (required) - Test name or describe block pattern\n- Examples: `\"Analytics > Slow Log\"`, `\"should add string key\"`, `\"@smoke\"`\n\n## Process\n\n### Step 1: Run the Tests\n\n```bash\ncd tests/e2e-playwright\nnpx playwright test --grep \"<pattern>\" --reporter=list 2>&1 | tail -50\n```\n\n### Step 2: Analyze Failures\n\nIf tests fail, check the error context file:\n\n```bash\ncat test-results/<test-folder>/error-context.md | head -150\n```\n\nThe error context contains:\n- **Page snapshot** - Current UI state (element tree with refs)\n- **Error message** - What assertion failed\n- **Call log** - Playwright's action log\n\n### Step 3: Diagnose the Issue\n\nCommon failure patterns:\n\n| Error | Likely Cause | Solution |\n|-------|--------------|----------|\n| `element(s) not found` | Wrong selector or element not rendered | Check snapshot for correct testid/role |\n| `Timeout waiting for` | Element takes too long to appear | Add proper wait or check if element exists |\n| `expected visible` | Element hidden or removed | Verify UI flow, check if dialog closed |\n| `not.toBeVisible failed` | Element still visible | Wait for element to be removed |\n\n### Step 4: Explore UI if Needed\n\nIf the error context doesn't reveal the issue, use Playwright MCP to explore:\n\n```\nbrowser_navigate_Playwright → http://localhost:8080\nbrowser_snapshot_Playwright\nbrowser_click_Playwright → element, ref\n```\n\n### Step 5: Fix the Test\n\nApply fixes following these priorities:\n\n1. **Fix selectors** - Use correct `data-testid` or role from snapshot\n2. **Fix waits** - Replace `waitForTimeout` with proper element waits\n3. **Fix test data** - Ensure unique prefixes to avoid conflicts with other tests\n4. **Fix assertions** - Match actual UI behavior\n\n### Step 6: Verify the Fix\n\n```bash\ncd tests/e2e-playwright\nnpx playwright test --grep \"<pattern>\" --reporter=list 2>&1 | tail -30\n```\n\n### Step 7: Run Linter and Type Check\n\n**REQUIRED before completing:**\n\n```bash\ncd tests/e2e-playwright\nnpm run lint && npx tsc --noEmit\n```\n\nBoth must pass.\n\n## Debugging Tips\n\n### Check Page Snapshot for Correct Selectors\n\n```yaml\n# Look for data-testid in the snapshot\n- button \"Add\" [ref=e123] [cursor=pointer]    # Use getByRole('button', { name: 'Add' })\n- generic \"my-testid\" [ref=e456]              # Use getByTestId('my-testid')\n- treeitem \"String keyname...\" [ref=e789]     # Use getByRole('treeitem', { name: /keyname/ })\n```\n\n### Handle View Mode Differences\n\nBrowser page has List view and Tree view with different element structures:\n\n```typescript\n// List view uses gridcell\npage.getByRole('gridcell', { name: keyName })\n\n// Tree view uses treeitem  \npage.getByRole('treeitem', { name: new RegExp(keyName) })\n\n// KeyList.getKeyRow() handles both\n```\n\n### Test Isolation Issues\n\nIf test fails inconsistently, check for:\n- Missing unique suffix in test data (conflicts with parallel runs)\n- Missing cleanup in `afterEach`\n- Shared state between tests (use `test.describe.serial` if needed)\n\n### Dropdown/Dialog Issues\n\nIf clicking fails after interacting with dropdown:\n```typescript\n// Close dropdown before next action\nawait page.keyboard.press('Escape');\n```\n\n## Example Usage\n\n```\n@e2e-fix \"Analytics > Slow Log\"\n@e2e-fix \"should add string key\"\n@e2e-fix \"Browser > Key Details > String\"\n@e2e-fix \"@smoke\"\n```\n\n## Quick Reference\n\n| Command | Purpose |\n|---------|---------|\n| `npx playwright test --grep \"pattern\"` | Run matching tests |\n| `npx playwright test --grep \"pattern\" --debug` | Run with inspector |\n| `npx playwright show-trace <trace.zip>` | View test trace |\n| `npm run lint` | Check for lint errors |\n| `npx tsc --noEmit` | Check for type errors |\n\n"
  },
  {
    "path": ".ai/commands/e2e-generate.md",
    "content": "---\ndescription: Explore a page using Playwright MCP and generate E2E tests for a Jira ticket\nargument-hint: <ticket-id or ticket-url>\n---\n\n# Generate E2E Tests\n\nUse Playwright MCP to explore a page, discover testable functionality, and generate E2E tests based on a Jira ticket.\n\n**Follow all standards in `.ai/rules/e2e-testing.md`**\n\n**Reference:** @tests/e2e-playwright/TEST_PLAN.md\n\n## Prerequisites\n\n- App must be running at `http://localhost:8080` for Playwright exploration\n\n## Input\n\n1. **Ticket ID or URL** (required)\n\n## Process\n\n### Step 1: Fetch Jira Ticket Details\n\nUse the Jira API to get ticket information:\n- Summary and description\n- Acceptance criteria\n- Related components/features\n\n### Step 2: Check Test Plan\n\nReview `tests/e2e-playwright/TEST_PLAN.md` to find related tests:\n- ✅ = Already implemented (skip or verify)\n- 🔲 = Not implemented (create new)\n\n### Step 3: Explore the Page with Playwright MCP\n\nNavigate to the relevant page based on the ticket's feature area:\n\n```\nbrowser_navigate_Playwright → url (e.g., http://localhost:8080)\nbrowser_snapshot_Playwright\nbrowser_click_Playwright → element, ref\nbrowser_snapshot_Playwright (after each action)\n```\n\nLook for:\n- `data-testid` attributes → use with `page.getByTestId()`\n- Element roles (button, combobox, grid) → use with `page.getByRole()`\n- Form field placeholders → use with `page.getByPlaceholder()`\n\n### Step 4: Check Existing Infrastructure\n\n```bash\nls tests/e2e-playwright/tests/\nls tests/e2e-playwright/pages/\nls tests/e2e-playwright/test-data/\n```\n\n### Step 5: Generate Test Artifacts\n\nBased on exploration and ticket requirements, create/update:\n\n1. **Page Object** - `tests/e2e-playwright/pages/{feature}/{Feature}Page.ts`\n2. **Test Data Factory** - `tests/e2e-playwright/test-data/{feature}/index.ts`\n3. **Fixture** (if new page) - Update `tests/e2e-playwright/fixtures/base.ts`\n4. **Test File** - `tests/e2e-playwright/tests/{feature}/{action}/*.spec.ts`\n\n### Step 6: Verify\n\nRun only the new tests using list reporter (no HTML report):\n\n```bash\ncd tests/e2e-playwright\nnpx playwright test tests/main/{feature}/{action}/ --project=chromium --reporter=list\nnpm run lint && npx tsc --noEmit\n```\n\n**Note:** Use `--reporter=list` to avoid Playwright generating and hosting an HTML report. Use `--project=chromium` to run only browser tests (faster feedback).\n\n### Step 7: Update Test Plan\n\nUpdate `tests/e2e-playwright/TEST_PLAN.md` to match actual tests:\n\n- **Rename** test case names to match the actual test titles in spec files\n- **Add** new test cases that were created\n- **Delete** test cases that were removed or consolidated\n- Mark implemented tests as ✅\n\nTest case names in TEST_PLAN.md should exactly match the test titles in spec files (e.g., `should open Help Center and display all menu options`).\n\n## Exploration Checklist\n\n- [ ] Main page purpose and entry points\n- [ ] Forms and their fields\n- [ ] Buttons and their actions\n- [ ] Lists/tables and CRUD operations\n- [ ] Dialogs/modals triggered by actions\n- [ ] Loading states and spinners\n- [ ] Success/error toasts\n- [ ] Empty states\n- [ ] Validation messages\n- [ ] `data-testid` attributes\n\n## Feature-to-URL Mapping\n\n| Feature | URL Pattern |\n|---------|-------------|\n| Database Management | `http://localhost:8080` |\n| Browser | `http://localhost:8080/{dbId}/browser` |\n| Workbench | `http://localhost:8080/{dbId}/workbench` |\n| CLI | (Panel on any database page) |\n| Pub/Sub | `http://localhost:8080/{dbId}/pub-sub` |\n| Slow Log | `http://localhost:8080/{dbId}/analytics/slowlog` |\n| Database Analysis | `http://localhost:8080/{dbId}/analytics/database-analysis` |\n| Settings | `http://localhost:8080/settings` |\n\n**Note:** Replace `{dbId}` with an actual database UUID.\n\n## Example Usage\n\n```\n@e2e-generate RI-7992\n@e2e-generate https://redislabs.atlassian.net/browse/RI-7992\n```\n\nThe command will:\n1. Fetch ticket details from Jira\n2. Determine the relevant page/feature to test\n3. Explore the UI at http://localhost:8080\n4. Generate appropriate E2E tests based on ticket requirements\n"
  },
  {
    "path": ".ai/commands/generate-release-notes.md",
    "content": "***\n\ndescription: Generate release notes from JIRA tickets for a specific version\nargument-hint: <version> \\[jira-filter-link-or-jql-or-csv-file]\n---------------------------------------------------------------\n\nGenerate release notes for RedisInsight releases based on JIRA tickets.\n\n## Examples\n\n```bash\n# Generate from JIRA filter link (PREFERRED)\ngenerate-release-notes 3.0.3 \"https://<jira-domain>/jira/software/c/projects/<project_name>/issues?jql=project%20%3D%20<project_name>...\"\n\n# Generate from JIRA query (JQL)\ngenerate-release-notes 3.0.2 \"project = <project_key> AND parent = <ticket-key>\"\n\n# Generate from CSV export\ngenerate-release-notes 3.0.2 /path/to/jira-export.csv\n\n# Generate from ticket keys\ngenerate-release-notes 3.0.2 <ticket-key-1>,<ticket-key-2>,<ticket-key-3>\n```\n\n**Always reference the GitHub releases page as the source of truth for format and style:**\nhttps://github.com/redis/RedisInsight/releases\n\nUse existing releases (especially recent ones like 3.0.2, 3.0.0) as examples for:\n\n* Format and structure\n* Tone and language\n* Section organization\n* Ticket reference format\n\n## 1. Get Version and Ticket Data\n\n**If version is not provided as an argument, prompt the user for it.**\n\nThe version should be in semantic versioning format (e.g., `3.0.2`).\n\n**Ticket data can be provided in one of these ways:**\n\n1. **JIRA Filter Link** (PREFERRED): If a JIRA filter link is provided:\n   * **Detection**: Check if the input starts with `http://` or `https://` and contains `atlassian.net` and `jql=`\n   * **Example**: `https://<jira-domain>/jira/software/c/projects/<project_name>/issues?jql=project%20%3D%20<project_name>...`\n   * Extract the JQL query from the URL (decode URL-encoded parameters)\n   * **Use the JavaScript script `scripts/fetch-jira-tickets.js` to fetch all tickets matching the filter** (or JIRA MCP tools if available)\n   * For each ticket, fetch complete details including:\n     * Issue key, summary, type, status, priority\n     * Labels (check for \"Github-Issue\" label)\n     * Description\n     * All custom fields and metadata needed for categorization\n   * Process the tickets the same way as CSV data\n\n2. **JIRA Query (JQL)**: If a raw JQL query is provided (e.g., `project = <project_key> AND parent = <ticket-key>`), use the JIRA MCP tools to fetch tickets with full details\n\n3. **CSV File**: If a CSV file path is provided, parse it to extract ticket information\n   * To export from JIRA: Go to JIRA → Search for issues → Run your JQL query → Export → CSV\n   * The CSV will contain all ticket information needed for generation\n\n4. **Ticket Keys**: If specific ticket keys are provided (e.g., `<ticket-key-1>,<ticket-key-2>`), fetch each ticket individually with full details\n\n## 2. Fetch and Categorize Tickets\n\nFor each ticket, analyze its essence to categorize it:\n\n### Exclusion Rules\n\n**Exclude entirely (do not include in any section):**\n\n* **Spike tickets**: If issue type is \"Spike\", exclude from release notes\n* **POC tickets**: If \"POC\" appears in the title or description (indicating proof-of-concept that is not implemented), exclude from release notes\n* These tickets are not ready for release and should not be mentioned\n\n### Categorization Logic\n\n**Bug indicators:**\n\n* Issue type contains \"bug\" or \"defect\"\n* Summary contains: \"fix\", \"bug\", \"error\", \"issue\", \"broken\", \"crash\", \"fail\"\n* Labels contain \"bug\" or \"defect\"\n\n**IMPORTANT: Bugs/Bug fixes section filter:**\n\n* **Only tickets with the \"Github-Issue\" label should be included in the Bugs/Bug fixes section**\n* When processing tickets from JIRA query or CSV, filter bugs to only include those with the \"Github-Issue\" label\n* **Include all bugs that have the \"Github-Issue\" label**—every such ticket must appear in the Bugs section with no limit or additional filtering\n* Other bug tickets (without this label) should be excluded from the Bugs/Bug fixes section\n* **Bugs with Github-Issue label should ONLY appear in the Bugs section, NOT in Headlines or Details sections**\n* **Always list each of these bugs with the GitHub issue link**: For every ticket in the Bugs section (tickets with \"Github-Issue\" label), output the line in the form `[#ISSUE-NUMBER](https://github.com/redis/RedisInsight/issues/ISSUE-NUMBER) [Short description]`. Resolve the GitHub issue number from: (1) JIRA labels such as `Github-4658` (use the number part), (2) JIRA description or linked PR body (e.g. \"References #5381\", \"Closes #5382\"), or (3) search on GitHub (repo: redis/RedisInsight) for the issue or PR that matches the bug (e.g. by JIRA key like RI-7894 or by summary). If the number cannot be determined, still include the short description but add a note to look up the link.\n\n**Feature indicators:**\n\n* Issue type contains: \"story\", \"feature\", \"epic\", \"enhancement\"\n* Summary contains: \"add\", \"implement\", \"new\", \"introduce\", \"support\", \"enable\"\n* Labels contain \"feature\" or \"enhancement\"\n\n**Improvement indicators:**\n\n* Issue type contains: \"task\", \"improvement\"\n* Summary contains: \"improve\", \"enhance\", \"update\", \"optimize\", \"refactor\"\n* Labels contain \"improvement\"\n\n## 3. Generate Release Notes\n\nUse the template from `docs/release-notes/RELEASE_NOTES_TEMPLATE.md` as a reference.\n\n### Format Selection\n\n* **If only bugs (with \"Github-Issue\" label)**: Use simple \"Bug fixes\" section only; include every ticket that has the \"Github-Issue\" label\n* **If features/improvements exist**: Use full format with \"Headlines\", \"Details\", and \"Bugs\" sections\n  * Note: The \"Bugs\" section must include all tickets with the \"Github-Issue\" label (include every one)\n\n### Section Organization Rules\n\n**IMPORTANT: Avoid duplication between sections:**\n\n* **Tickets with \"Github-Issue\" label**: These should **ONLY** appear in the \"Bugs\" section, never in \"Headlines\" or \"Details\" sections\n* **No duplication**: Items in the Bugs section must not appear in Headlines or Details sections\n* **Headlines and Details relationship**:\n  * Headlines should contain short summaries of the most important user-facing features and improvements\n  * Details can expand on Headlines items with more information, or include additional features/improvements not mentioned in Headlines\n  * The same item can appear in both Headlines (short summary) and Details (full description), but items from Bugs section must not appear in either\n\n### Release Notes Structure\n\n**Reference format from GitHub releases:** https://github.com/redis/RedisInsight/releases\n\n```markdown\n# [VERSION] ([MONTH] [YEAR])\n\n[Release description based on content]\n\n### Headlines (if features exist - see 3.0.0 example)\n* [Top 3-5 most important items - user-facing features or critical improvements]\n\n### Details (if features/improvements exist - see 3.0.0 example)\n* [Short description of what was added/improved] (for JIRA tickets, don't include ticket ID)\n* [#ISSUE-NUMBER](https://github.com/redis/RedisInsight/issues/ISSUE-NUMBER) [Summary] (for GitHub issues, use link format)\n\n### Bugs (if features exist) OR Bug fixes (if only bugs - see 3.0.2 example)\n* **IMPORTANT: Include all tickets with the \"Github-Issue\" label in this section (every one, no limit). Always list each bug with its GitHub issue link.**\n* Each line must be: `[#ISSUE-NUMBER](https://github.com/redis/RedisInsight/issues/ISSUE-NUMBER) [Short description of problem and fix]`\n* Resolve ISSUE-NUMBER from JIRA labels (e.g. Github-4658 → 4658), ticket/PR references, or GitHub search if needed.\n\n**SHA-512 Checksums**\n\nhttps://redis.io/docs/latest/develop/tools/insight/release-notes/v.[VERSION]/\n\n**Full Changelog**: https://github.com/redis/RedisInsight/compare/[LAST_RELEASED]...[VERSION]\n```\n\n**Important formatting notes from GitHub releases:**\n\n* **For the Bugs section (tickets with \"Github-Issue\" label)**: Always list each bug with its GitHub issue link: `[#<issue-number>](https://github.com/redis/RedisInsight/issues/<issue-number>) [Short description]`. Resolve the issue number from JIRA labels (e.g. `Github-4658`), from \"References #NNNN\" / \"Closes #NNNN\" in linked PRs or descriptions, or by searching GitHub (repo: redis/RedisInsight) for the matching issue.\n* **For other JIRA tickets** (Headlines/Details): Do NOT include ticket IDs. Provide a very short description of what was added or fixed.\n* **For GitHub issues** (when referenced elsewhere): Use actual links in format `[#<issue-number>](https://github.com/redis/RedisInsight/issues/<issue-number>)` (not just `#<issue-number>` or `#<ticket-key>`).\n* Use \"SHA-512 Checksums\" for all releases\n* Keep descriptions concise and user-focused\n* Headlines should highlight the most impactful user-facing changes\n\n### Release Description\n\n**Examples from GitHub releases:**\n\n* **Major releases** (x.0.0): \"This is the General Availability (GA) release of Redis Insight \\[version], a major version upgrade that introduces \\[key themes].\"\n  * See 3.0.0 example: mentions \"new UI experience, new navigation architecture, and foundational improvements\"\n* **Patch releases with only bugs**:\n  * Check ticket priorities to determine description:\n    * If only high/critical priority bugs: \"This maintenance patch release includes critical bug fixes for Redis Insight \\[major.minor].0.\"\n    * If only medium/low priority bugs: \"This maintenance patch release includes non-critical bug fixes for Redis Insight \\[major.minor].0.\"\n    * If both critical and non-critical: \"This maintenance patch release includes critical and non-critical bug fixes for Redis Insight \\[major.minor].0.\"\n  * See 3.0.2 example\n* **Patch releases with features**: \"This release includes new features, improvements, and bug fixes for Redis Insight.\"\n  * See 2.64, 2.62, 2.60 examples for format with \"Highlights\" and \"Details\" sections\n\n## 4. Generate All Release Note Formats\n\nAfter generating the main release notes, create **two different formats** for different platforms:\n\n### 4.1 GitHub/Redis Docs Format\n\nSave the generated release notes to `RELEASE_NOTES_[VERSION].md` in the repository root.\n\n**Important: Links**\n\n* **Checksums link**: Use format `https://redis.io/docs/latest/develop/tools/insight/release-notes/v.[VERSION]/`\n  * Example for 3.0.3: `https://redis.io/docs/latest/develop/tools/insight/release-notes/v.3.0.3/`\n* **Full Changelog link**: Use format `https://github.com/redis/RedisInsight/compare/[LAST_RELEASED]...[VERSION]`\n  * Example for 3.0.3 (if last release was 3.0.2): `https://github.com/redis/RedisInsight/compare/3.0.2...3.0.3`\n  * Determine the last released version by checking GitHub releases or repository tags\n\n### 4.2 Store Format (App Store & Microsoft Store)\n\nSave to `RELEASE_NOTES_STORE_[VERSION].md` in the repository root.\n\n**IMPORTANT:** Reuse the same content from the GitHub/Redis Docs format (`RELEASE_NOTES_[VERSION].md`), but apply different formatting. Only the formatting changes - the content (sections, descriptions, items) should remain the same.\n\n**Note:** This single file is used for both App Store and Microsoft Store as they have identical format requirements.\n\n**Format requirements (formatting changes only):**\n\n* Plain text only (no markdown formatting)\n* Section header: \"Bug fixes\" (if only bugs) or appropriate section name (if features exist) - keep the same section names from GitHub format\n* Use bullet points with `•` character (not `*`)\n* No version number or date header\n* No links (remove SHA-512 Checksums and Full Changelog sections)\n* No markdown headers (`#`, `###`)\n* No bold formatting (`**`)\n* Just the section headers and bullet points - same content, different formatting\n\n**Example format:**\n\n```\nBug fixes\n\n• Fixed default database sorting order to display most recently used databases at the top\n• Restored missing Pub/Sub functionality including message clear option, full message display with line wrapping, and descending chronological order (most recent messages first)\n```\n\n**If features/improvements exist, use appropriate section headers:**\n\n```\nFeatures\n\n• [Feature description]\n\nImprovements\n\n• [Improvement description]\n\nBug fixes\n\n• [Bug fix description]\n```\n\n## 5. Display Summary\n\nShow a summary of:\n\n* Total tickets processed\n* Number of bugs, features, and improvements\n* Which format was used (simple vs. full)\n* File locations for all formats:\n  * GitHub/Redis Docs: `RELEASE_NOTES_[VERSION].md`\n  * App Store & Microsoft Store: `RELEASE_NOTES_STORE_[VERSION].md`\n\n## JIRA Filter Link Processing\n\nWhen a JIRA filter link is provided:\n\n1. **Parse the URL and fetch tickets**: Extract the JQL query from the `jql` parameter in the URL and use the JavaScript script `scripts/fetch-jira-tickets.js` to fetch all tickets matching the query\n   * Example URL: `https://<jira-domain>/jira/software/c/projects/<project_name>/issues?jql=project%20%3D%20<project_name>%20AND%20status%20%3D%20Closed`\n   * Extract and decode: `project = <project_name> AND status = Closed`\n   * **Use `node scripts/fetch-jira-tickets.js <jira-filter-url>` to fetch tickets** (the script uses JIRA REST API with credentials from `.env.mcp`)\n   * Alternatively, if JIRA MCP tools are available, use them (check with `list_mcp_resources`) such as `jira_search` or `jira_get_project_issues`\n\n2. **Fetch full details**: For each ticket returned, fetch complete information:\n   * **Basic fields**: key, summary, type, status, priority, description\n   * **Labels**: Extract all labels and check for \"Github-Issue\" label (critical for filtering)\n   * **Custom fields**: Any additional fields needed for categorization\n   * **Links**: Check for linked GitHub pull requests or issues\n   * **Metadata**: Created date, updated date, resolved date, assignee, reporter\n\n3. **Transform to CSV-like structure**: Convert the fetched ticket data into a structure similar to CSV format:\n   ```javascript\n   {\n       'Issue key': '<ticket-key>',\n       'Summary': 'Fix default database sorting...',\n       'Issue Type': 'Bug',\n       'Status': 'Closed',\n       'Priority': 'Medium',\n       'Labels': ['Github-Issue', 'Github-Issue-Notification'],\n       'Description': 'Full description text...',\n       // ... other fields\n   }\n   ```\n\n4. **Process**: Use the same categorization and filtering logic as CSV processing\n\n## Notes\n\n* Keep summaries concise, user-focused, and descriptive\n* For Headlines section, prioritize the most impactful user-facing changes (see 3.0.0 example)\n* Ensure all closed tickets are included if they match the criteria\n* **Always check GitHub releases page** before generating to ensure consistency with published format:\n  https://github.com/redis/RedisInsight/releases\n* Match the tone and style of existing releases (professional, clear, user-focused)\n"
  },
  {
    "path": ".ai/commands/pr-plan.md",
    "content": "---\ndescription: Analyze a JIRA ticket and create a detailed implementation plan\nargument-hint: <ticket-id or ticket-url>\n---\n\nCreate a comprehensive implementation plan for a JIRA ticket.\n\n## 1. Fetch JIRA Ticket\n\n**If ticket ID is not provided as an argument, prompt the user for it.**\n\nFetch all the information from the ticket, its comments, linked documents, and parent ticket.\n\nUse the `jira` tool to fetch the ticket details.\n\n## 4. Create Implementation Plan\n\nUse `sequential-thinking` tool for complex analysis. Break down into thoughts:\n\n### Thought 1-5: Requirements Analysis\n\n- Parse acceptance criteria into specific tasks\n- Identify functional requirements\n- Identify non-functional requirements (performance, security, cost)\n- Map requirements to system components\n- Identify dependencies and blockers\n\n### Thought 6-10: Architecture Planning\n\n- Determine which services are affected\n- Identify new components needed\n- Identify existing components to modify\n- Plan data flow and interactions\n- Consider error handling and edge cases\n\n### Thought 16-20: Implementation Breakdown\n\n- Break work into logical phases\n- Identify dependencies between phases\n- Consider testing strategy for each phase\n- Plan for incremental delivery\n\n### Thought 21-25: Testing Strategy\n\n- Identify test scenarios from acceptance criteria\n- Plan unit tests (behavior-based, not implementation)\n- Plan integration tests\n- Consider edge cases and error scenarios\n- Plan test data needs\n\n### Thought 26-30: Risk Assessment\n\n- Identify technical risks\n- Identify integration risks\n- Identify timeline risks\n- Identify knowledge gaps\n- Plan mitigation strategies\n\n## 5. Generate Implementation Plan Document\n\n**CRITICAL: You MUST create and save a plan document. This is NOT optional.**\n\nCreate a comprehensive Markdown document and save it to `docs/pr-plan-{ticket-id}-{brief-description}.md` using the `write` tool.\n\n**IMPORTANT: Planner Detection**\n- Detect which AI tool is being used (Cursor, Augment, or other)\n- Set the **Planner** field to:\n  - \"Cursor Agent\" if running in Cursor\n  - \"Augment Agent\" if running in Augment\n  - \"[Tool Name] Agent\" for other tools\n- You can infer the tool from context, user messages, or environment\n\n**The document structure MUST include all sections below:**\n\n```markdown\n# Implementation Plan: [Ticket Title]\n\n**JIRA Ticket:** [MOD-XXXXX](https://redislabs.atlassian.net/browse/MOD-XXXXX)\n**Epic:** [EPIC-XXX](link) (if applicable)\n**Parent:** [PARENT-XXX](link) (if applicable)\n**Plan Date:** [Date]\n**Planner:** [Detect and set: Cursor Agent / Augment Agent / [Tool Name] Agent]\n\n---\n\n## Executive Summary\n\n**Components Affected:**\n\n- [component name]\n\n**Key Risks:**\n\n1. [Risk with mitigation]\n2. [Risk with mitigation]\n3. [Risk with mitigation]\n\n---\n\n## 1. Requirements Summary\n\n**Story (Why):**\n[Quote or summarize the story from the ticket]\n\n**Acceptance Criteria (What):**\n\n1. [AC1]\n2. [AC2]\n3. [AC3]\n\n**Functional Requirements:**\n\n- [Requirement 1]\n- [Requirement 2]\n\n**Non-Functional Requirements:**\n\n- [NFR 1 - e.g., Performance: <100ms response time]\n- [NFR 2 - e.g., Security: Requires authentication]\n\n**Resources Provided:**\n\n- [Link 1: Description]\n- [Link 2: Description]\n\n## 2. Current State Analysis\n\n### Frontend Changes\n\n**Components to Modify:**\n\n- [Component 1]: [What changes are needed]\n- [Component 2]: [What changes are needed]\n\n**Components to Create:**\n\n- [Component 1]: [Why it's needed]\n- [Component 2]: [Why it's needed]\n\n**Components to Reuse:**\n\n- [Component 1]: [How it will be used]\n- [Component 2]: [How it will be used]\n\n### Backend Changes\n\n**Services to Modify:**\n\n- [Service 1]: [What changes are needed]\n- [Service 2]: [What changes are needed]\n\n**Services to Create:**\n\n- [Service 1]: [Why it's needed]\n- [Service 2]: [Why it's needed]\n\n**APIs to Modify:**\n\n- [API 1]: [What's changing]\n- [API 2]: [What's changing]\n\n**APIs to Create:**\n\n- [API 1]: [Why it's needed]\n- [API 2]: [Why it's needed]\n\n**Data Models:**\n\n- [Model 1]: [Description and whether it needs extension]\n- [Model 2]: [Description and whether it needs extension]\n\n**Repositories:**\n\n- [Repo 1]: [Description and whether it can be reused]\n\n---\n\n## 3. Implementation Plan\n\n### Phase 1: [Phase Name]\n\n**Goal:** [What this phase achieves]\n\n**Tasks:**\n\n1. [ ] [Task 1 - specific, actionable]\n   - Files: [List of files to create/modify]\n   - Acceptance: [How to verify this task is done]\n2. [ ] [Task 2]\n   - Files: [List of files]\n   - Acceptance: [Verification criteria]\n\n**Deliverables:**\n\n- [Deliverable 1]\n- [Deliverable 2]\n\n**Testing:**\n\n- [Test scenario 1]\n- [Test scenario 2]\n\n### Phase 2: [Phase Name]\n\n[Same structure as Phase 1]\n\n### Phase 3: [Phase Name]\n\n[Same structure as Phase 1]\n\n---\n\n## 5. Testing Strategy\n\n### Test Scenarios (from Acceptance Criteria)\n\n**AC1: [Acceptance Criterion]**\n\n- Test Scenario: [Given-When-Then]\n- Test Type: Unit/Integration\n- Test Location: [File path]\n\n**AC2: [Acceptance Criterion]**\n\n- Test Scenario: [Given-When-Then]\n- Test Type: Unit/Integration\n- Test Location: [File path]\n\n### Edge Cases and Error Scenarios\n\n1. **[Edge Case 1]**\n\n   - Scenario: [Description]\n   - Expected Behavior: [What should happen]\n   - Test: [How to test]\n\n2. **[Error Scenario 1]**\n   - Scenario: [Description]\n   - Expected Error: [Error type/code]\n   - Test: [How to test]\n\n### Test Data Needs\n\n- [Test data 1]: [Description]\n- [Test data 2]: [Description]\n\n---\n\n## 6. Risk Assessment and Mitigation\n\n### Technical Risks\n\n| Risk     | Likelihood      | Impact          | Mitigation            |\n| -------- | --------------- | --------------- | --------------------- |\n| [Risk 1] | High/Medium/Low | High/Medium/Low | [Mitigation strategy] |\n| [Risk 2] | High/Medium/Low | High/Medium/Low | [Mitigation strategy] |\n\n### Integration Risks\n\n| Risk     | Likelihood      | Impact          | Mitigation            |\n| -------- | --------------- | --------------- | --------------------- |\n| [Risk 1] | High/Medium/Low | High/Medium/Low | [Mitigation strategy] |\n\n### Timeline Risks\n\n| Risk     | Likelihood      | Impact          | Mitigation            |\n| -------- | --------------- | --------------- | --------------------- |\n| [Risk 1] | High/Medium/Low | High/Medium/Low | [Mitigation strategy] |\n\n### Knowledge Gaps\n\n- [Gap 1]: [What we don't know and how to find out]\n- [Gap 2]: [What we don't know and how to find out]\n```\n\n---\n\n## 6. Save the Plan Document\n\n**CRITICAL: You MUST save the plan document using the `write` tool.**\n\n1. **Generate the complete plan document** following the structure in section 5\n2. **Save it** to `docs/pr-plan-{ticket-id}-{brief-description}.md` using `write` tool\n3. **Verify the file was created** by confirming the write tool succeeded\n\n**Example filename:** `docs/pr-plan-MOD-11280-dp-services-clean-architecture.md`\n\n---\n\n## 7. Follow-up Actions\n\nAfter saving the plan document:\n\n1. **Confirm document was saved** - Show the file path to the user\n2. **Summarize key findings** for the user:\n   - Key risks\n   - Recommended approach\n   - **Confirm plan document was saved** (file path)\n3. **Ask if user wants to:**\n   - Review the plan document\n   - Proceed with implementation\n\n---\n\n## Important Notes\n\n- **ALWAYS save the plan document** - use `write` tool to save to `docs/pr-plan-{ticket-id}-{brief-description}.md`\n- **Use main branch as baseline** - all analysis should be against current main\n- **Be specific and actionable** - every task should be clear and verifiable\n- **Consider PR stacking** - plan for small, reviewable PRs (see `.ai/rules/pull-requests.md`). plan breaking the implementation in a stack of PRs.\n- **Follow all project standards** - reference rules in `.ai/rules/`\n- **Document assumptions** - if anything is unclear, document the assumption made\n- **Identify blockers early** - surface dependencies and knowledge gaps upfront\n\n## Execution Order Summary\n\n**The correct order of operations is:**\n\n1. ✅ Fetch JIRA ticket\n2. ✅ Analyze current codebase state\n3. ✅ Create implementation plan using sequential-thinking\n4. ✅ Generate implementation plan document content\n5. ✅ **Save plan document to `docs/pr-plan-{ticket-id}-{brief-description}.md`** (CRITICAL - use write tool)\n6. ✅ Present results to user and confirm document location\n\n**Do NOT skip step 5 - it is mandatory and must be done during command execution.**\n\n**Step 5 is MANDATORY:** You MUST use the `write` tool to save the plan document. Do NOT just present the plan to the user without saving it.\n"
  },
  {
    "path": ".ai/commands/pull-request-review.md",
    "content": "# PR Review Command\n\n## Purpose\n\nReviews code changes in the current branch against requirements, best practices, and project standards.\n\n## Usage\n\n```bash\n/pr:review <ticket-id>\n```\n\n**Examples**:\n\n- JIRA ticket: `/pr:review RI-1234`\n- GitHub issue: `/pr:review 456`\n\n## Prerequisites\n\n1. Checkout the branch to review locally\n2. Ensure the ticket ID is valid and accessible\n3. Have JIRA MCP tool configured (if using JIRA integration)\n\n## Process\n\n### 1. Gather Context\n\n- Fetch JIRA ticket details (if available)\n- Read requirements and acceptance criteria\n- Identify affected files in the PR\n- Review recent commits\n\n### 2. Code Analysis\n\nAnalyze the changes against:\n\n- **Code Quality**: Linting rules, TypeScript types, complexity\n- **Testing**: Test coverage, test quality, edge cases\n- **Performance**: Rendering optimizations\n- **Security**: Input validation, XSS prevention, credential handling\n- **Accessibility**: ARIA labels, keyboard navigation, semantic HTML\n- **Best Practices**: React patterns, Redux usage, NestJS conventions\n\n### 3. Requirements Check\n\n- Verify all acceptance criteria are met\n- Check for missing functionality\n- Validate edge case handling\n- Ensure proper error messages\n\n### 4. Testing Validation\n\n- Unit test coverage (80% minimum)\n- Integration tests for API endpoints\n- Component tests for React components\n- E2E tests for critical flows\n- No fixed timeouts or magic numbers\n- Use of faker for test data\n\n### 5. Generate Report\n\nCreate a markdown report in `docs/reviews/pr-<ticket-id>-<date>.md` with:\n\n**Note**: Use appropriate ticket reference format:\n\n- JIRA tickets: `pr-RI-1234-2024-11-20.md`\n- GitHub issues: `pr-456-2024-11-20.md`\n\nReport should include:\n\n- **Summary**: Overview of changes\n- **Strengths**: What was done well\n- **Issues**: Categorized by severity (Critical, High, Medium, Low)\n- **Suggestions**: Improvements and optimizations\n- **Requirements Coverage**: Acceptance criteria checklist\n- **Testing Assessment**: Coverage and quality analysis\n- **Risk Assessment**: Potential issues or impacts\n\n## Review Checklist\n\n### Code Quality\n\n- [ ] No linting errors\n- [ ] TypeScript types are proper (no `any` without justification)\n- [ ] Code follows project conventions\n- [ ] No console.log statements\n- [ ] Import order is correct\n- [ ] Cognitive complexity within limits\n- [ ] No duplicate code\n\n### Testing\n\n- [ ] Unit tests added/updated\n- [ ] Test coverage meets thresholds\n- [ ] Tests use faker for data generation\n- [ ] No fixed timeouts in tests\n- [ ] Edge cases are tested\n- [ ] Mocks are properly configured\n\n### React/Frontend (if applicable)\n\n- [ ] Functional components with hooks\n- [ ] Proper state management (Redux vs local)\n- [ ] Effects cleanup properly\n- [ ] No unnecessary re-renders\n- [ ] Accessibility considerations\n- [ ] Styled-components for styling (no new SCSS modules)\n- [ ] Proper error boundaries\n- [ ] Component folder structure follows guidelines\n\n### NestJS/Backend (if applicable)\n\n- [ ] Dependency injection used properly\n- [ ] DTOs for validation\n- [ ] Proper error handling\n- [ ] Swagger documentation\n- [ ] Service layer separation\n- [ ] Database transactions where needed\n\n### Performance\n\n- [ ] No performance regressions\n- [ ] Large lists virtualized\n- [ ] Routes lazy loaded\n- [ ] Expensive operations memoized\n- [ ] Bundle size impact acceptable\n\n### Security\n\n- [ ] Input validation\n- [ ] Output sanitization\n- [ ] No sensitive data in logs\n- [ ] Proper authentication/authorization\n- [ ] SQL injection prevention (if applicable)\n\n### Documentation\n\n- [ ] README updated if needed\n- [ ] Complex logic documented\n- [ ] API documentation updated\n- [ ] Breaking changes noted\n\n## Example Output\n\n**Note**: Use appropriate ticket format (RI-1234 for JIRA or #456 for GitHub issues)\n\n```markdown\n# PR Review: RI-1234 - Add User Profile Editing\n\n**Date**: 2024-11-20\n**Reviewer**: AI Assistant\n**Branch**: feature/RI-1234/user-profile-editing\n\n## Summary\n\nThis PR implements user profile editing functionality including UI components,\nAPI endpoints, and data persistence. The implementation follows project\nstandards with good test coverage.\n\n## High Priority Issues\n\n1. **Missing Input Validation** (Security)\n   - File: `redisinsight/api/src/user/user.service.ts:45`\n   - Issue: Email validation missing on backend\n   - Recommendation: Add class-validator decorator to DTO\n\n## Medium Priority Issues\n\n1. **Performance Concern** (Performance)\n\n   - File: `redisinsight/ui/src/components/UserProfile.tsx:78`\n   - Issue: Inline function in render\n   - Recommendation: Extract to useCallback\n\n2. **Test Flakiness Risk** (Testing)\n   - File: `redisinsight/ui/src/components/UserProfile.spec.tsx:45`\n   - Issue: Direct state check without waitFor\n   - Recommendation: Wrap assertion in waitFor\n\n## Low Priority Issues\n\n1. **Code Style** (Style)\n   - File: Multiple files\n   - Issue: Inconsistent import ordering\n   - Recommendation: Run `yarn prettier:fix`\n\n## Risk Assessment\n\n**Low Risk** - Well-tested implementation with minor issues that can be\naddressed before merge. No breaking changes or security vulnerabilities.\n\n## Recommendation\n\n**Approve with comments** - Address high priority issues before merging.\nConsider suggestions for future improvements.\n```\n\n## Notes\n\n- Focus on constructive feedback\n- Prioritize issues by severity\n- Be specific with file locations and line numbers\n- Provide actionable recommendations\n- Balance criticism with recognition of good practices\n- Consider the broader impact of changes\n"
  },
  {
    "path": ".ai/rules/backend.md",
    "content": "---\ndescription: NestJS backend development patterns, module structure, services, controllers, DTOs, and error handling\nglobs: redisinsight/api/**/*.ts\nalwaysApply: false\n---\n\n# Backend Development (NestJS/API)\n\n## Module Structure\n\n### NestJS Architecture\n\n- Follow **modular architecture** (feature-based modules)\n- Use **dependency injection** throughout\n- **Separate concerns**: Controllers, Services, Repositories\n- Use **DTOs** for validation and data transfer\n- Apply **proper error handling** with NestJS exceptions\n\n### Module Folder Structure\n\nEach feature module in its own directory under `api/src/`:\n\n```\nfeature/\n├── feature.module.ts           # Module definition\n├── feature.controller.ts       # REST endpoints\n├── feature.service.ts          # Business logic\n├── feature.service.spec.ts     # Service tests\n├── feature.controller.spec.ts  # Controller tests\n├── feature.types.ts            # Interfaces and types related to the feature\n├── dto/                        # Data transfer objects\n│   ├── create-feature.dto.ts\n│   ├── update-feature.dto.ts\n│   └── feature.dto.ts\n├── entities/                   # TypeORM entities\n├── repositories/               # Custom repositories\n├── exceptions/                 # Custom exceptions\n├── guards/                     # Feature-specific guards\n├── decorators/                 # Custom decorators\n└── constants/                  # Feature constants\n```\n\n### File Naming\n\n- **Modules**: `feature.module.ts`\n- **Controllers**: `feature.controller.ts`\n- **Services**: `feature.service.ts`\n- **DTOs**: `create-feature.dto.ts`, `update-feature.dto.ts`\n- **Entities**: `feature.entity.ts`\n- **Interfaces and types**: `feature.types.ts`\n- **Tests**: `feature.service.spec.ts`\n- **Constants**: `feature.constants.ts`\n- **Exceptions**: `feature-not-found.exception.ts`\n\n### Constants Organization\n\nStore feature-specific constants in dedicated constants file:\n\n```typescript\nexport const FEATURE_CONSTANTS = {\n  MAX_NAME_LENGTH: 100,\n  DEFAULT_PAGE_SIZE: 20,\n} as const;\n\nexport const FEATURE_ERROR_MESSAGES = {\n  NOT_FOUND: 'Feature not found',\n  INVALID_INPUT: 'Invalid feature data',\n} as const;\n```\n\n### Imports Order\n\n1. Node.js built-in modules\n2. External dependencies (`@nestjs/*`, etc.)\n3. Internal modules (using `apiSrc/*` alias)\n4. Local relative imports\n\n## Service Layer\n\n### Service Pattern\n\n- Inject dependencies via constructor\n- Use TypeORM repositories\n- Handle errors with NestJS exceptions\n- Use Logger for important operations\n- Keep business logic in services (not controllers)\n\n### Dependency Injection\n\nAlways inject dependencies via constructor with proper decorators:\n\n```typescript\n@Injectable()\nexport class UserService {\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n    private readonly emailService: EmailService,\n  ) {}\n}\n```\n\n## Controller Layer\n\n### Controller Pattern\n\n- Keep controllers thin (delegate to services)\n- Use proper HTTP decorators (`@Get`, `@Post`, etc.)\n- Use `@Body`, `@Param`, `@Query` for inputs\n- Apply guards with `@UseGuards()`\n- Document with Swagger decorators\n\n### HTTP Status Codes\n\n- Use `@HttpCode()` decorator for non-standard codes\n- Return appropriate status codes (200, 201, 204, 400, 404, etc.)\n\n## Data Transfer Objects (DTOs)\n\n### Validation\n\nUse `class-validator` decorators for validation:\n\n- `@IsString()`, `@IsNumber()`, `@IsEmail()`\n- `@IsNotEmpty()`, `@IsOptional()`\n- `@MinLength()`, `@MaxLength()`\n- `@Min()`, `@Max()`\n\n### Swagger Documentation\n\nUse `@ApiProperty()` and `@ApiPropertyOptional()` for Swagger docs.\n\n## Error Handling\n\n### NestJS Exceptions\n\nUse appropriate exception types:\n\n- `NotFoundException` - 404\n- `BadRequestException` - 400\n- `UnauthorizedException` - 401\n- `ForbiddenException` - 403\n- `ConflictException` - 409\n- `InternalServerErrorException` - 500\n\n### Custom Exceptions\n\n**Prefer custom exceptions over generic ones** when you need:\n\n- Consistent error codes for frontend handling\n- Specific error messages from constants\n- Consistent error structure across the codebase\n\nCreate custom exceptions following existing patterns:\n\n```typescript\n// src/modules/feature/exceptions/feature-invalid.exception.ts\nimport {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class FeatureInvalidException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FEATURE_INVALID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'FeatureInvalid',\n      errorCode: CustomErrorCodes.FeatureInvalid,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n```\n\n### Error Logging\n\n```typescript\nprivate readonly logger = new Logger(ServiceName.name)\n\nthis.logger.error('Error message', error.stack, { context })\n```\n\n## Redis Integration\n\n### Redis Service Pattern\n\n- Use RedisClient from `apiSrc/modules/redis`\n- Handle errors gracefully\n- Log Redis operations\n- Use try-catch for error handling\n\n## Code Quality\n\n### Cognitive Complexity (≤ 15)\n\n- Use early returns to reduce nesting\n- Extract complex logic to separate functions\n- Avoid deeply nested conditions\n\n### No Duplicate Strings\n\nExtract repeated strings to constants in constants file.\n\n## API Documentation (Swagger)\n\n### Required Decorators\n\n- `@ApiTags()` - Group endpoints\n- `@ApiOperation()` - Describe operation\n- `@ApiResponse()` - Document responses\n- `@ApiParam()` - Document params\n- `@ApiQuery()` - Document query params\n- `@ApiBearerAuth()` - Auth requirement\n\n## Checklist\n\n- [ ] Services use dependency injection\n- [ ] DTOs have validation decorators\n- [ ] Controllers have Swagger documentation\n- [ ] Proper HTTP status codes used\n- [ ] Error handling with appropriate exceptions\n- [ ] Logging for important operations\n- [ ] Transactions for related DB operations\n- [ ] Configuration via ConfigService\n- [ ] Guards for authentication/authorization\n- [ ] Cognitive complexity ≤ 15\n"
  },
  {
    "path": ".ai/rules/code-quality.md",
    "content": "---\nalwaysApply: true\n---\n\n# Code Quality Standards\n\n## Critical Rules\n\n- **ALWAYS run linter** after code changes: `yarn lint`\n- Linter must pass before committing\n- No console.log in production code (use console.warn/error only)\n\n## TypeScript Standards\n\n### Essential Rules\n\n- Use TypeScript for all new code\n- **Avoid `any`** - use proper types or `unknown`\n- **Prefer interfaces** for object shapes\n- Use **type** for unions, intersections, primitives\n- Add explicit return types for non-obvious functions\n- Leverage type inference where clear\n\n## Import Organization\n\n### Required Order (enforced by ESLint)\n\n1. External libraries (`react`, `lodash`, etc.)\n2. Built-in Node modules (`path`, `fs` - backend only)\n3. Internal modules with aliases (`uiSrc/*`, `apiSrc/*`)\n4. Sibling/parent relative imports\n5. Style imports (ALWAYS LAST)\n\n### Module Aliases\n\n- `uiSrc/*` → `redisinsight/ui/src/*`\n- `apiSrc/*` → `redisinsight/api/src/*`\n- `desktopSrc/*` → `redisinsight/desktop/src/*`\n\n✅ **Use aliases**: `import { Button } from 'uiSrc/components/Button'`  \n❌ **Avoid relative**: `import { Button } from '../../../ui/src/components/Button'`\n\n## Naming Conventions\n\n- **Components**: `PascalCase` - `UserProfile`\n- **Functions/Variables**: `camelCase` - `fetchUserProfile`\n- **Constants**: `UPPER_SNAKE_CASE` - `MAX_RETRY_ATTEMPTS`\n- **Booleans**: Use `is/has/should` prefix - `isLoading`, `hasError`\n\n## SonarJS Rules\n\n- Keep cognitive complexity low (refactor complex functions)\n- Extract duplicate strings to constants\n- Follow DRY principle - no duplicate code\n- Use immediate return (avoid unnecessary intermediate variables)\n\n## Best Practices\n\n- Use destructuring for objects and arrays\n- Use template literals over string concatenation\n- Use `const` by default, `let` only when reassignment needed\n- Use descriptive variable names\n- Handle errors properly\n- Clean up subscriptions and timers\n- Use constants instead of magic numbers\n\n## Vite Cache Management\n\nWhen updating npm packages (especially `@redis-ui/*` packages):\n\n1. **Clear Vite cache** after `yarn install`:\n\n   ```bash\n   rm -rf node_modules/.vite\n   rm -rf redisinsight/ui/node_modules/.vite\n   ```\n\n2. **Restart dev server** to rebuild dependencies\n\n3. This ensures new package versions are properly loaded\n\n## Pre-Commit Checklist\n\n- [ ] `yarn lint` passes\n- [ ] No TypeScript errors\n- [ ] Import order is correct\n- [ ] No `any` types without reason\n- [ ] No console.log statements\n- [ ] No magic numbers\n- [ ] Descriptive variable names\n- [ ] Low cognitive complexity\n- [ ] No duplicate code\n- [ ] Vite cache cleared (if updated dependencies)\n"
  },
  {
    "path": ".ai/rules/e2e-testing.md",
    "content": "---\ndescription: Playwright E2E testing standards, page object models, test structure, fixtures, and navigation patterns\nglobs: tests/e2e-playwright/**/*.ts\nalwaysApply: false\n---\n\n# E2E Testing Standards (Playwright)\n\n## Location\n\nAll E2E tests are in `tests/e2e-playwright/`. This is a **standalone package** - no imports from `redisinsight/ui/` or `redisinsight/api/`.\n\n## Test Plan\n\n**Always refer to `tests/e2e-playwright/TEST_PLAN.md`** for:\n- Test coverage status (✅ implemented, 🔲 not implemented)\n- Feature implementation order\n- Test data requirements\n\n**After implementing tests, update TEST_PLAN.md** to mark tests as ✅.\n\n## Project Structure\n\n```\ntests/e2e-playwright/\n├── TEST_PLAN.md         # Master test plan with coverage status\n├── config/              # Configuration (env, databases)\n│   └── databases/       # Database configs by type\n├── fixtures/            # Playwright fixtures\n├── helpers/             # API helpers for setup/teardown\n├── pages/               # Page Object Models\n│   ├── BasePage.ts      # Base class for all pages\n│   ├── InstancePage.ts  # Base class for database instance pages\n│   ├── components/      # Shared components (InstanceHeader, NavigationTabs, BottomPanel)\n│   └── {feature}/       # Feature-specific pages (browser/, cli/, etc.)\n├── test-data/           # Test data factories\n├── tests/               # Test specs organized by project\n│   ├── main/            # Default parallel tests\n│   │   └── {feature}/\n│   │       └── {action}/\n│   ├── auto-update/     # Serial tests with special setup\n│   └── electron/        # Electron-specific tests\n└── types/               # TypeScript types\n```\n\n## Playwright Projects\n\nTests are organized into **projects** based on execution requirements. Each project can have different parallelism, timeouts, and setup.\n\n| Project | Folder | Parallelism | Use Case |\n|---------|--------|-------------|----------|\n| `main` | `tests/main/` | Parallel | Standard tests that can run concurrently |\n| `auto-update` | `tests/auto-update/` | Serial | Tests requiring special setup or causing flakiness |\n| `electron` | `tests/electron/` | Serial | Electron-specific features (deep links, etc.) |\n\n### Running Projects\n\n```bash\nnpx playwright test --project=main           # Only main parallel tests\nnpx playwright test --project=auto-update    # Only auto-update tests\nnpx playwright test                           # All projects\n```\n\n### When to Create a New Project\n\nCreate a new project folder when tests:\n- Require different parallelism settings (serial vs parallel)\n- Need different global setup/teardown\n- Would cause flakiness when run with other tests\n- Require special environment configuration\n\n### Adding a New Project\n\n1. Create folder under `tests/` (e.g., `tests/my-feature/`)\n2. Add project configuration in `playwright.config.ts`:\n\n```typescript\n{\n  name: 'my-feature',\n  testDir: './tests/my-feature',\n  fullyParallel: false, // or true\n  workers: 1,\n  timeout: 120000,\n  // Optional: different setup\n  // globalSetup: './my-feature-setup.ts',\n}\n```\n\n## Page Objects\n\n### Page Object Hierarchy\n\n```\nBasePage (abstract)\n  ├── DatabasesPage           # Databases list page\n  ├── SettingsPage            # Settings page\n  └── InstancePage (abstract) # Base for all database instance pages\n        ├── instanceHeader    # Database name, stats, breadcrumb\n        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub\n        ├── bottomPanel       # CLI, Command Helper, Profiler\n        └── BrowserPage       # Browser-specific (extends InstancePage)\n              └── WorkbenchPage (future)\n              └── AnalyzePage (future)\n              └── PubSubPage (future)\n```\n\n### Extend the Appropriate Base Class\n\n- **BasePage** - For standalone pages (DatabasesPage, SettingsPage)\n- **InstancePage** - For pages within a connected database (BrowserPage, WorkbenchPage, etc.)\n\nPage objects are **stateless** - they don't store database objects. Pass `databaseId` to navigation methods.\n\n```typescript\n// For database instance pages - extend InstancePage\nimport { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\nexport class WorkbenchPage extends InstancePage {\n  readonly editor: Locator;\n\n  constructor(page: Page) {\n    super(page);\n    this.editor = page.getByTestId('workbench-editor');\n  }\n\n  // InstancePage provides: instanceHeader, navigationTabs, bottomPanel\n  // Plus navigation methods: navigateToBrowser(), openCli(), etc.\n\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n}\n```\n\n### Component-Based Structure\n\nBreak large pages into components:\n\n```typescript\n// pages/feature/FeaturePage.ts\nexport class FeaturePage extends InstancePage {\n  readonly dialog: FeatureDialog;\n  readonly list: FeatureList;\n\n  constructor(page: Page) {\n    super(page);\n    this.dialog = new FeatureDialog(page);\n    this.list = new FeatureList(page);\n  }\n}\n```\n\n## Test Structure\n\n### File Organization\n\n```\ntests/\n├── main/                # Default parallel tests (most tests go here)\n│   └── {feature}/       # e.g., databases, browser, workbench\n│       └── {action}/    # e.g., add, edit, delete\n│           ├── standalone.spec.ts\n│           └── cluster.spec.ts\n├── auto-update/         # Serial tests with special setup\n└── electron/            # Electron-specific tests\n```\n\n### Test Setup Pattern\n\nUse simple, explicit setup with clear separation of concerns. **Page objects are fixtures** - they don't store database state. Pass `databaseId` to `goto()` methods.\n\n```typescript\nimport { test, expect } from '../../../fixtures/base';\nimport { standaloneConfig } from '../../../config/databases/standalone';\nimport { DatabaseInstance } from '../../../types';\n\ntest.describe('Feature > Action', () => {\n  let database: DatabaseInstance;\n\n  // Setup: Create database once for all tests\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({\n      name: 'test-feature-db',\n      host: standaloneConfig.host,\n      port: standaloneConfig.port,\n    });\n  });\n\n  // Teardown: Clean up database after all tests\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.describe('Sub-feature', () => {\n    // Navigation: Pass databaseId to goto() - page is a fixture\n    test.beforeEach(async ({ featurePage }) => {\n      await featurePage.goto(database.id);\n    });\n\n    // Tests receive page fixtures they need\n    test('should do something', async ({ featurePage }) => {\n      await featurePage.doAction();\n      await expect(featurePage.result).toBeVisible();\n    });\n\n    // Tests that need both page and apiHelper\n    test('should create and verify', async ({ featurePage, apiHelper }) => {\n      await apiHelper.createKey(database.id, 'test-key', 'value');\n      await featurePage.refresh();\n      await expect(featurePage.keyList).toContainText('test-key');\n    });\n  });\n});\n```\n\n### Key Principles\n\n1. **`beforeAll`** - Create database/test data via API (runs once)\n2. **`afterAll`** - Clean up database/test data via API (runs once)\n3. **`beforeEach`** - Navigate to page via UI using `goto(databaseId)` (runs before each test)\n4. **Individual tests** - Receive page fixtures they need in the signature\n5. **Page objects are stateless** - Don't store database objects in pages, pass IDs to methods\n\n### Avoid These Anti-Patterns\n\n```typescript\n// ❌ BAD: Storing database in page object\nconst browserPage = createBrowserPage(database);  // OLD pattern - don't use\n\n// ✅ GOOD: Pass databaseId to goto()\nawait browserPage.goto(database.id);\n\n// ❌ BAD: Using page fixture without declaring it in test signature\ntest('should work', async () => {\n  await browserPage.doSomething();  // browserPage is undefined!\n});\n\n// ✅ GOOD: Declare fixtures in test signature\ntest('should work', async ({ browserPage }) => {\n  await browserPage.doSomething();\n});\n\n// ❌ BAD: Navigation inside each test\ntest('should work', async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Should be in beforeEach\n  // ...\n});\n\n// ❌ BAD: Using test.describe.serial when not needed\ntest.describe.serial('Feature', () => { // Use regular describe unless tests truly depend on each other\n  // ...\n});\n```\n\n## Test Data\n\n### Use Fishery Factories with Faker\n\nUse the [fishery](https://github.com/thoughtbot/fishery) library for test data factories:\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nexport const TEST_PREFIX = 'test-';\n\nexport const ConfigFactory = Factory.define<Config>(() => ({\n  name: `${TEST_PREFIX}${faker.string.alphanumeric(8)}`,\n  host: '127.0.0.1',\n  port: 6379,\n}));\n\n// Usage in tests\nconst config = ConfigFactory.build();\nconst config = ConfigFactory.build({ name: 'custom-name' });\n```\n\n### Cleanup Pattern\n\nAlways prefix test data with `test-` for easy cleanup:\n\n```typescript\n// In apiHelper\nasync deleteTestData(): Promise<number> {\n  return this.deleteByPattern(new RegExp(`^${TEST_PREFIX}`));\n}\n```\n\n## Fixtures\n\n### Add New Fixtures to base.ts\n\n```typescript\n// fixtures/base.ts\ntype Fixtures = {\n  myPage: MyPage;\n  apiHelper: ApiHelper;\n};\n\nexport const test = base.extend<Fixtures>({\n  myPage: async ({ page }, use) => {\n    await use(new MyPage(page));\n  },\n  apiHelper: async ({}, use) => {\n    const helper = new ApiHelper();\n    await use(helper);\n    await helper.dispose();\n  },\n});\n```\n\n## UI Exploration with Playwright MCP\n\n**Before writing tests, ALWAYS use Playwright MCP to explore the UI:**\n\n### Why Explore First?\n- Discover actual `data-testid` attributes used in the application\n- Understand element roles and accessible names for `getByRole()`\n- See page structure and component hierarchy\n- Avoid trial-and-error test writing\n\n### Exploration Workflow\n\n1. **Navigate to the page**: `browser_navigate_Playwright` to target URL\n2. **Take snapshot**: `browser_snapshot_Playwright` to see element tree\n3. **Interact with elements**: `browser_click_Playwright` to trigger dialogs, dropdowns, etc.\n4. **Wait for async content**: `browser_wait_for_Playwright` for dynamic content\n5. **Document findings**: Add discovered UI patterns to `TEST_PLAN.md` under the feature section\n\n### What to Look For\n\n- `data-testid` attributes → use with `page.getByTestId()`\n- Element roles (button, combobox, grid, treeitem) → use with `page.getByRole()`\n- Accessible names → use with `{ name: 'text' }` option\n- Form field placeholders → use with `page.getByPlaceholder()`\n- Text content patterns → use with `page.getByText()`\n\n### Use Discovered Patterns in Page Objects\n\nAfter exploring, use discovered patterns directly in Page Object locators:\n\n```typescript\n// Use data-testid when available\nthis.addButton = page.getByTestId('btn-add-key');\n\n// Use role + name for accessible elements\nthis.submitButton = page.getByRole('button', { name: 'Submit' });\n\n// Use placeholder for form fields\nthis.searchInput = page.getByPlaceholder('Search...');\n```\n\n**Note**: Keep TEST_PLAN.md as a simple visual list of test cases. Document UI patterns in Page Object comments if needed.\n\n## Best Practices\n\n### ✅ DO\n\n- **Explore UI with Playwright MCP before writing tests**\n- **Use Page Object navigation methods** (e.g., `browserPage.goto()`, `workbenchPage.goto()`)\n- Use `data-testid` attributes for stable selectors\n- Use `getByRole`, `getByLabel` for accessible elements\n- Wait for elements with `waitFor({ state: 'visible' })`\n- Clean up test data in `afterEach`\n- Use API for setup, UI for assertions\n- Handle both List view and Tree view in key assertions\n\n### ❌ DON'T\n\n- **NEVER use `page.goto()` directly** - tests must work in both browser and Electron\n- Write tests without exploring the actual UI first\n- Use fixed timeouts (`waitForTimeout`)\n- Use CSS selectors for dynamic content\n- Leave test data after tests\n- Import from `redisinsight/ui/` or `redisinsight/api/`\n- Hardcode test data (use faker)\n- Assume element structure without verification\n\n## Navigation (IMPORTANT)\n\n**All navigation must use UI-based methods, NOT URL navigation.**\n\nTests must work in both browser mode (http://localhost:8080) and Electron mode (no baseURL). Direct `page.goto()` calls fail in Electron because there's no baseURL.\n\n### Navigation Architecture\n\n**BasePage** provides only fundamental navigation:\n```typescript\nawait this.gotoHome();              // Click Redis logo → databases list\nawait this.gotoDatabase(dbId);      // Click database → Browser page (default)\n```\n\n**Each page owns its navigation** via its `goto()` method:\n```typescript\nawait settingsPage.goto();           // Settings page\nawait browserPage.goto(dbId);        // Browser page for database\nawait workbenchPage.goto(dbId);      // Workbench page for database\nawait analyticsPage.goto(dbId);      // Analytics page for database\nawait pubSubPage.goto(dbId);         // Pub/Sub page for database\n```\n\n**NavigationTabs component** handles tab switching within a connected database:\n```typescript\nawait browserPage.navigationTabs.gotoBrowser();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.navigationTabs.gotoAnalyze();\nawait browserPage.navigationTabs.gotoPubSub();\n```\n\n### ✅ Correct Navigation Pattern\n\n```typescript\n// Use Page Object's goto() method in beforeEach\ntest.beforeEach(async ({ browserPage }) => {\n  await browserPage.goto(database.id);  // Navigates and waits for page load\n});\n\n// Switch tabs when already connected\nawait browserPage.navigationTabs.gotoWorkbench();\n```\n\n### ❌ Incorrect Navigation Pattern\n\n```typescript\n// NEVER do this - fails in Electron\nawait page.goto(`/${database.id}/browser`);\nawait page.goto('/settings');\nawait page.goto('/');\n```\n\n## Running Tests\n\n```bash\nnpm test                    # Main project tests (default)\nnpm run test:main           # Main project tests only\nnpm run test:electron       # Electron tests (auto-detects platform)\nnpm run test:all            # All projects\nENV=ci npm test             # CI environment\nENV=staging npm test        # Staging environment\n```\n\n## Code Quality (IMPORTANT)\n\n**Always run linter and type checker after making changes:**\n\n```bash\nnpm run lint                # ESLint check\nnpx tsc --noEmit            # TypeScript type check\n```\n\nBoth must pass before committing. Common issues:\n- Unused variables/imports\n- Missing return types\n- `any` types (avoid when possible)\n- Null/undefined handling (use proper types like `Promise<string | null>`)\n\n## Test Isolation (IMPORTANT)\n\nTests should be isolated and not depend on execution order:\n\n### 1. Shared Database with beforeAll/afterAll\n\n```typescript\ntest.describe('Feature Name', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({ name: 'test-feature-db', ... });\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  // Tests can run in parallel - they share the database but don't modify shared state\n});\n```\n\n### 2. Use Serial Only When Tests Truly Depend on Each Other\n\n```typescript\n// Only use .serial when tests modify state that subsequent tests depend on\ntest.describe.serial('Workflow that modifies state', () => {\n  test('step 1: create item', ...);\n  test('step 2: modify item created in step 1', ...);\n  test('step 3: delete item', ...);\n});\n```\n\n### 3. Unique Test Data Per Test (when needed)\n\n```typescript\ntest('should create unique item', async ({ apiHelper }) => {\n  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;\n  // Use uniqueName for this test's data\n});\n```\n\n## Feature-to-Path Mapping\n\nFollow this naming convention for test and page object paths:\n\n| Feature | Test Path | Page Object Path |\n|---------|-----------|------------------|\n| Database List | `tests/main/databases/list/` | `pages/databases/` |\n| Add Database | `tests/main/databases/add/` | `pages/databases/` |\n| Import Database | `tests/main/databases/import/` | `pages/databases/` |\n| Browser - Key List | `tests/main/browser/key-list/` | `pages/browser/` |\n| Browser - Add Key | `tests/main/browser/add-key/` | `pages/browser/` |\n| Browser - Key Details | `tests/main/browser/key-details/` | `pages/browser/` |\n| Workbench | `tests/main/workbench/` | `pages/workbench/` |\n| CLI | `tests/main/cli/` | `pages/cli/` |\n| Pub/Sub | `tests/main/pubsub/` | `pages/pubsub/` |\n| Slow Log | `tests/main/analytics/slow-log/` | `pages/analytics/` |\n| DB Analysis | `tests/main/analytics/analysis/` | `pages/analytics/` |\n| Settings | `tests/main/settings/` | `pages/settings/` |\n| Navigation | `tests/main/navigation/` | `pages/navigation/` |\n| Auto-Update | `tests/auto-update/` | `pages/` (shared) |\n| Deep Links | `tests/electron/deep-links/` | `pages/` (shared) |\n\n**Note**: Most tests go in `tests/main/`. Only use other project folders for tests with special requirements (serial execution, different setup, etc.).\n"
  },
  {
    "path": ".ai/rules/frontend.md",
    "content": "---\ndescription: React frontend development patterns, Redux, styled-components, UI components, and hooks\nglobs: redisinsight/ui/**/*.{ts,tsx}\nalwaysApply: false\n---\n\n# Frontend Development (React/Redux)\n\n## Component Structure\n\n### Functional Components\n\n- Use **functional components with hooks** (no class components)\n- **Prefer named exports** over default exports\n- Keep components focused and single-responsibility\n- Extract complex logic into custom hooks\n\n### Component Folder Structure\n\nEach component in its own directory under `**/ComponentName`:\n\n```\nComponentName/\n  ComponentName.tsx          # Main component\n  ComponentName.styles.ts    # Styled-components styles (PascalCase)\n  ComponentName.types.ts     # TypeScript interfaces\n  ComponentName.spec.tsx     # Tests\n  ComponentName.constants.ts # Constants\n  ComponentName.story.tsx    # Storybook examples\n  hooks/                     # Custom hooks\n  components/                # Sub-components\n  utils/                     # Utility functions\n```\n\n### Props Interface\n\n- Name as `ComponentNameProps`\n- Use separate interfaces for complex prop objects\n- Always use proper TypeScript types, never `any`\n\n### Imports Order in Components\n\n1. External dependencies (`react`, `redux`, etc.)\n2. Internal modules (aliases)\n3. Local imports (types, constants, hooks)\n4. Styles (always last: `import { Container } from './Component.styles'`)\n\n### Barrel Files\n\nUse barrel files (`index.ts`) only when exporting **3 or more** items. Make sure exports appear in only one barrel file, not propagated up the chain.\n\n## Styled Components\n\n**We are migrating to styled-components** (deprecating SCSS modules).\n\n### Encapsulate Styles in .styles.ts\n\nKeep all component styles in dedicated `.styles.ts` files using styled-components. Use PascalCase for the filename to match the component name:\n\n```\nComponentName/\n  ComponentName.tsx\n  ComponentName.styles.ts  # ✅ PascalCase\n  # Not component-name.styles.ts ❌\n```\n\n### Import Pattern\n\nKeep all component styles in dedicated .style.ts files and import them with a namespace.\n\n**CRITICAL: `import * as S` is reserved for local styles only** (e.g., from `ComponentName.styles.ts`). When you need to use styled components from external components, create a local styles file that re-exports them.\n\n#### ✅ Good\n\n```typescript\n// ComponentName.tsx\nimport * as S from './ComponentName.styles'\n\nreturn (\n  <S.Container>\n    <S.Title>Title</S.Title>\n    <S.Content>Content</S.Content>\n  </S.Container>\n)\n\n// ComponentName.styles.ts (when re-exporting from external component)\nexport { ExternalStyledComponent } from '../ExternalComponent/ExternalComponent.styles'\n```\n\n#### ❌ Bad\n\n```typescript\n// ❌ BAD: Importing styled components directly from external component\nimport * as S from '../ExternalComponent/ExternalComponent.styles'\n\n// ❌ BAD: Named imports instead of namespace\nimport { Container, Title, Content } from './Component.styles'\n```\n\n### Use Layout Components Instead of div\n\n**Prefer `FlexGroup` over `div`** when creating flex containers:\n\n```typescript\n// ✅ GOOD: Use FlexGroup\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n// Usage: Pass layout props as component props\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Using div with hardcoded flex properties\nexport const Wrapper = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n`\n```\n\n### Pass Layout Props as Component Props\n\n**Don't hardcode layout properties** in styled components when using layout components like `FlexGroup`. Pass them as props instead:\n\n```typescript\n// ✅ GOOD: Pass props in JSX\nexport const Wrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\n<Wrapper align=\"center\" justify=\"end\">\n  {children}\n</Wrapper>\n\n// ❌ BAD: Hardcoding in styled component\nexport const Wrapper = styled(FlexGroup)`\n  align-items: center;\n  justify-content: flex-end;\n  user-select: none;\n`\n```\n\n### Use Gap Prop Instead of Custom Margins\n\n**Prefer `gap` prop on layout components** instead of custom margins for spacing between elements:\n\n```typescript\n// ✅ GOOD: Use gap prop\n<Row align=\"center\" justify=\"between\" gap=\"l\">\n  <FlexItem>Item 1</FlexItem>\n  <FlexItem>Item 2</FlexItem>\n</Row>\n```\n\n### Use Theme Spacing Instead of Magic Numbers\n\n**Always use theme spacing values** instead of hardcoded pixel values:\n\n```typescript\n// ✅ GOOD: Use theme spacing\nexport const Container = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }) => theme.core.space.space200};\n`;\n\n// ❌ BAD: Using magic numbers\nexport const Container = styled(Row)`\n  height: 64px;\n  padding: 0 16px;\n  margin-bottom: 16px;\n`;\n```\n\n### Use Semantic Colors from Theme\n\n**Always use semantic colors** from the theme instead of CSS variables or hardcoded colors:\n\n```typescript\n// ✅ GOOD: Use semantic colors\nexport const Header = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`;\n\n// ❌ BAD: Using deprecated EUI CSS variables\nexport const Header = styled(Row)`\n  background-color: var(--euiColorEmptyShade);\n  border-bottom: 1px solid var(--separatorColor);\n`;\n```\n\n### Use Layout Components (Row/Col/FlexGroup) Instead of div\n\n**Prefer layout components** from the layout system instead of regular `div` elements:\n\n```typescript\n// ✅ GOOD: Use Row component\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const PageHeader = styled(Row)`\n  height: ${({ theme }) => theme.core.space.space500};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\n<PageHeader align=\"center\" justify=\"between\" gap=\"l\">\n  {children}\n</PageHeader>\n\n// ❌ BAD: Using div with flex properties\nexport const PageHeader = styled.div`\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 64px;\n`\n```\n\n### Conditional Styling\n\nUse `$` prefix for transient props that shouldn't pass to DOM:\n\n```typescript\nexport const Button = styled.button<{ $isActive?: boolean }>`\n  background-color: ${({ $isActive }) => ($isActive ? '#007bff' : '#6c757d')};\n`;\n```\n\n### Avoid !important\n\n**Never use `!important` in styled-components**. Styled-components handles CSS specificity through component hierarchy. If you need to override styles, use more specific selectors or adjust the component structure:\n\n```typescript\n// ✅ GOOD: Rely on CSS specificity\nexport const IconButton = styled(IconButton)<{ isOpen: boolean }>`\n  ${({ isOpen }) =>\n    isOpen &&\n    css`\n      background-color: ${({ theme }) =>\n        theme.semantic.color.background.primary200};\n    `}\n`;\n\n// ❌ BAD: Using !important\nexport const IconButton = styled(IconButton)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.primary200} !important;\n`;\n```\n\n### Verify Type System Compatibility\n\nWhen using layout components or other typed components, verify your prop values match the type system:\n\n```typescript\n// Check the component's type definitions\n// FlexGroup accepts: align?: 'center' | 'stretch' | 'baseline' | 'start' | 'end'\n// Use valid values from the type system\n```\n\n## State Management (Redux)\n\n### When to Use What\n\n- **Global State (Redux)**:\n\n  - Data shared across multiple components\n  - Data persisting across routes\n  - Server state (API data)\n  - User preferences/settings\n\n- **Local State (useState)**:\n\n  - UI state (modals, dropdowns, tabs)\n  - Form inputs before submission\n  - Component-specific temporary data\n\n- **Derived State (Selectors)**:\n  - Computed values from Redux state\n  - Filtered/sorted lists\n  - Aggregated data\n\n### Redux Toolkit Patterns\n\n#### Slice Structure\n\n- Use `createSlice` from Redux Toolkit\n- Define proper TypeScript types for state\n- Use `PayloadAction<T>` for action typing\n- Handle async with `extraReducers` and thunks\n\n#### Thunks\n\n- Use `createAsyncThunk` for async operations\n- Handle pending, fulfilled, and rejected states\n- Use `rejectWithValue` for error handling\n\n#### Selectors\n\n- Create basic selectors for direct state access\n- Use `createSelector` from `reselect` for memoized/computed values\n- Keep selectors in separate `selectors.ts` file\n\n## React Best Practices\n\n### Performance\n\n- Use `useCallback` for functions passed as props\n- Use `useMemo` for expensive computations\n- Use `React.memo` for expensive components\n- Avoid inline arrow functions in JSX props\n\n### Effect Cleanup\n\nAlways clean up subscriptions, timers, and event listeners in `useEffect` return function.\n\n### Keys in Lists\n\n- Use unique, stable IDs (not array indices)\n- Only use indices if list never reorders and items have no IDs\n\n### Conditional Rendering\n\n- Use early returns for loading/error states\n- Avoid deeply nested ternaries - extract to functions\n\n## Custom Hooks\n\n### Extract Reusable Logic\n\nCreate custom hooks for reusable stateful logic. Store component-specific hooks in the component's `/hooks` directory.\n\n## Form Handling\n\nUse Formik with Yup for validation. Keep form logic in custom hooks when complex.\n\n## UI Components\n\n**⚠️ IMPORTANT**:\n\n- We are **deprecating Elastic UI** components\n- Migrating to **Redis UI** (`@redis-ui/*`)\n- **Use internal wrappers** from `uiSrc/components/ui`\n- **DO NOT import directly** from `@redis-ui/*`\n\n### Component Usage\n\n```typescript\n// ✅ GOOD: Import from internal wrappers\nimport { Button, Input, FlexGroup } from 'uiSrc/components/ui';\n\n// ❌ BAD: Don't import directly from @redis-ui\nimport { Button } from '@redis-ui/components';\n\n// ❌ DEPRECATED: Don't use Elastic UI for new code\nimport { EuiButton } from '@elastic/eui';\n```\n\n### Migration Guidelines\n\n- ✅ Use internal wrappers from `uiSrc/components/ui` for all new features\n- ✅ Create internal wrappers for Redis UI components as needed\n- ✅ Replace Elastic UI when touching existing code\n- ❌ Do not import directly from `@redis-ui/*`\n- ❌ Do not add new Elastic UI imports\n\n## Icons\n\n**⚠️ IMPORTANT**: Always use icons from Redis UI library instead of custom SVGs.\n\n### Icon Usage\n\n- **Use Redis UI icons** from `@redis-ui/icons` via `iconRegistry.tsx`\n- Icons are automatically exported via `export * from '@redis-ui/icons'` in `iconRegistry.tsx`\n- Use `RiIcon` component with icon type: `<RiIcon type=\"FolderOpenIcon\" />`\n- **DO NOT create custom SVG icons** - check if the icon exists in Redis UI library first\n\n### Custom Icons (Exception)\n\nOnly create custom SVG icons if:\n\n- The icon doesn't exist in Redis UI library\n- It's a project-specific icon that won't be added to the library\n\n## Testing Components\n\n### Always Use Shared `renderComponent` Helper\n\n**CRITICAL**: Create a `renderComponent` helper function for each component test file:\n\n```typescript\ndescribe('MyComponent', () => {\n  const defaultProps: MyComponentProps = {\n    id: faker.string.uuid(),\n    name: faker.person.fullName(),\n    onComplete: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    )\n  }\n\n  it('should render', () => {\n    renderComponent()\n    // assertions\n  })\n})\n```\n\nBenefits:\n\n- Centralized setup and providers\n- Default props in one place\n- Easy prop overrides per test\n- No duplicate render logic\n\n### Testing Redux\n\nCreate a test store with `configureStore` for components connected to Redux.\n\n## Key Principles\n\n1. **Separation of Concerns**: Keep styles, types, constants, logic separate\n2. **Colocate Related Code**: Keep sub-components and hooks close to usage\n3. **Consistent Naming**: Follow conventions across all components\n4. **Type Safety**: Always define proper types, never `any`\n5. **Testability**: Structure for easy testing with `renderComponent` helper\n6. **Styled Components**: Prefer styled-components over SCSS modules\n7. **Layout Components**: Use FlexGroup instead of div for flex containers, pass layout props as component props\n8. **Type Safety**: Verify prop values match component type definitions (e.g., FlexGroup's align/justify values)\n"
  },
  {
    "path": ".ai/rules/git-safety.md",
    "content": "---\ndescription: Critical safety guardrails for protected branches - prevents direct commits, pushes, and force pushes to main, latest, and release branches\nalwaysApply: true\n---\n\n# Git Safety Rules for AI Agents\n\n## 🚫 CRITICAL: Protected Branch Rules\n\n**AI agents must NEVER commit to or push to protected branches under any circumstances.**\n\n### Protected Branches\n\n- `main` - Primary production branch\n- `latest` - Latest stable release\n- `release/*` - Release branches (e.g., `release/v2.0.0`)\n\nThis is a non-negotiable rule that applies to all scenarios:\n\n### Prohibited Actions\n\n- ❌ **Direct commits** - Never run `git commit` while on a protected branch\n- ❌ **Direct pushes** - Never run `git push origin <protected-branch>` or `git push` while on a protected branch\n- ❌ **Force pushes** - Never run `git push --force` or `git push -f` targeting protected branches\n- ❌ **Merging into protected branches locally** - Never run `git merge <branch>` while on a protected branch\n- ❌ **Rebasing protected branches** - Never run `git rebase` while on a protected branch\n- ❌ **Resetting protected branches** - Never run `git reset` while on a protected branch\n\n### Required Workflow\n\n1. **Always create a feature branch** before making any changes\n2. **Verify current branch** before any git operation using `git branch --show-current`\n3. **Create Pull Requests** for all changes - let the review process handle merging\n\n### Pre-Push Checklist\n\nBefore executing any push command, AI agents must:\n\n1. ✅ Confirm current branch is NOT a protected branch (`main`, `latest`, `release/*`)\n2. ✅ Verify the remote and branch target\n\n### Error Recovery\n\nIf accidentally on a protected branch with uncommitted changes:\n\n1. Stash changes: `git stash`\n2. Create new branch: `git checkout -b <appropriate-branch-name>`\n3. Apply changes: `git stash pop`\n4. Continue work on the new branch\n\n## Rationale\n\n- Protected branches represent production-ready or release code\n- All changes must go through code review via Pull Requests\n- Direct pushes bypass CI/CD checks and team review\n- Mistakes on protected branches can affect the entire team and deployment pipeline\n\n"
  },
  {
    "path": ".ai/rules/testing.md",
    "content": "---\ndescription: Jest and Testing Library standards, test patterns, faker usage, renderComponent helper, and mocking\nglobs: \"redisinsight/**/*.spec.{ts,tsx}\"\nalwaysApply: false\n---\n\n# Testing Standards and Practices\n\n## Core Principles\n\n- **Write tests for all new features**\n- Follow **AAA pattern**: Arrange, Act, Assert\n- Use **descriptive test names**: \"should do X when Y\"\n- **CRITICAL**: Never use fixed time waits - tests must be deterministic\n- **CRITICAL**: Use faker library (@faker-js/faker) for test data\n\n## Test Organization\n\n```typescript\ndescribe('FeatureService', () => {\n  describe('findById', () => {\n    it('should return entity when found', () => {});\n    it('should throw NotFoundException when not found', () => {});\n  });\n\n  describe('create', () => {\n    it('should create entity with valid data', () => {});\n    it('should throw error with invalid data', () => {});\n  });\n});\n```\n\n## Frontend Testing (Jest + Testing Library)\n\n### Running Specific Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### CRITICAL: Always Use Shared `renderComponent` Helper\n\n**Create a `renderComponent` helper for each component test file**:\n\n```typescript\nimport { faker } from '@faker-js/faker';\n\ndescribe('MyComponent', () => {\n  // Define default props with faker\n  const defaultProps: MyComponentProps = {\n    id: faker.string.uuid(),\n    name: faker.person.fullName(),\n    email: faker.internet.email(),\n    onComplete: jest.fn(),\n  };\n\n  // Shared render helper\n  const renderComponent = (propsOverride?: Partial<MyComponentProps>) => {\n    const props = { ...defaultProps, ...propsOverride };\n\n    return render(\n      <Provider store={store}>\n        <MyComponent {...props} />\n      </Provider>\n    );\n  };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should render component', () => {\n    renderComponent();\n    expect(screen.getByText(defaultProps.name)).toBeInTheDocument();\n  });\n\n  it('should handle click', async () => {\n    const mockOnComplete = jest.fn();\n    renderComponent({ onComplete: mockOnComplete });\n\n    fireEvent.click(screen.getByRole('button'));\n\n    await waitFor(() => {\n      expect(mockOnComplete).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n```\n\n**Benefits**:\n\n- Centralized setup (providers, router, theme)\n- Default props defined once\n- Easy prop overrides per test\n- No duplicate setup code\n\n### Complex Component Setup\n\nFor components requiring Router, ThemeProvider, etc., include them in `renderComponent`:\n\n```typescript\nconst renderComponent = (propsOverride?: Partial<Props>) => {\n  const props = { ...defaultProps, ...propsOverride }\n\n  return render(\n    <Provider store={store}>\n      <BrowserRouter>\n        <ThemeProvider theme={theme}>\n          <Component {...props} />\n        </ThemeProvider>\n      </BrowserRouter>\n    </Provider>\n  )\n}\n```\n\n### Testing with Redux\n\nCreate a test store with `configureStore` for Redux-connected components:\n\n```typescript\nconst createTestStore = (initialState = {}) => {\n  return configureStore({\n    reducer: { user: userSlice.reducer },\n    preloadedState: initialState,\n  });\n};\n\nconst renderComponent = (propsOverride?: Partial<Props>, storeState = {}) => {\n  const testStore = createTestStore(storeState);\n  // render with testStore\n};\n```\n\n### Query Priorities (Testing Library)\n\n**Prefer accessible queries** (as users would interact):\n\n```typescript\n// ✅ PREFERRED\nscreen.getByRole('button', { name: /submit/i });\nscreen.getByLabelText('Email');\nscreen.getByPlaceholderText('Enter name');\n\n// ⚠️ LAST RESORT\nscreen.getByTestId('user-profile');\n\n// ❌ AVOID\nwrapper.find('.button-class');\n```\n\n### Testing Async Behavior\n\n```typescript\n// ✅ GOOD: waitFor with proper queries\nawait waitFor(() => {\n  expect(screen.getByText('Data loaded')).toBeInTheDocument();\n});\n\n// ✅ GOOD: waitForElementToBeRemoved\nawait waitForElementToBeRemoved(() => screen.queryByText('Loading...'));\n\n// ✅ GOOD: findBy queries (built-in waiting)\nconst element = await screen.findByText('Async content');\n\n// ❌ BAD: Fixed timeouts (flaky tests)\nawait new Promise((resolve) => setTimeout(resolve, 1000));\n```\n\n### Mocking API Calls (MSW)\n\nUse Mock Service Worker for API mocking:\n\n```typescript\nimport { rest } from 'msw';\nimport { setupServer } from 'msw/node';\n\nconst server = setupServer(\n  rest.get('/api/users/:id', (req, res, ctx) => {\n    return res(\n      ctx.json({\n        id: req.params.id,\n        name: faker.person.fullName(),\n      }),\n    );\n  }),\n);\n\nbeforeAll(() => server.listen());\nafterEach(() => server.resetHandlers());\nafterAll(() => server.close());\n```\n\n## Backend Testing (NestJS/Jest)\n\n### Service Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\n// Define factory for User entity\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserService', () => {\n  let service: UserService;\n  let repository: Repository<User>;\n\n  const mockRepository = {\n    find: jest.fn(),\n    findOne: jest.fn(),\n    save: jest.fn(),\n    update: jest.fn(),\n    delete: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        UserService,\n        {\n          provide: getRepositoryToken(User),\n          useValue: mockRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<UserService>(UserService);\n    repository = module.get<Repository<User>>(getRepositoryToken(User));\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('should return user when found', async () => {\n    const mockUser = userFactory.build();\n    mockRepository.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Controller Test Pattern\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(() => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ndescribe('UserController', () => {\n  let controller: UserController;\n  let service: UserService;\n\n  const mockService = {\n    findAll: jest.fn(),\n    findById: jest.fn(),\n    create: jest.fn(),\n  };\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      controllers: [UserController],\n      providers: [{ provide: UserService, useValue: mockService }],\n    }).compile();\n\n    controller = module.get<UserController>(UserController);\n  });\n\n  it('should return user from service', async () => {\n    const mockUser = userFactory.build();\n    mockService.findById.mockResolvedValue(mockUser);\n\n    const result = await controller.findById(mockUser.id);\n\n    expect(result).toEqual(mockUser);\n  });\n});\n```\n\n### Integration Tests (E2E)\n\n```typescript\ndescribe('UserController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const module = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = module.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  it('/users (GET)', () => {\n    return request(app.getHttpServer())\n      .get('/users')\n      .expect(200)\n      .expect((res) => {\n        expect(Array.isArray(res.body)).toBe(true);\n      });\n  });\n});\n```\n\n## E2E Testing (Playwright)\n\n```typescript\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userDataFactory = Factory.define(() => ({\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n}));\n\ntest.describe('User Management', () => {\n  test('should create new user', async ({ page }) => {\n    const userData = userDataFactory.build();\n\n    await page.goto('/users');\n    await page.click('text=Add User');\n    await page.fill('[name=\"name\"]', userData.name);\n    await page.fill('[name=\"email\"]', userData.email);\n    await page.click('text=Submit');\n\n    // ✅ Use proper waits\n    await expect(page.locator(`text=${userData.name}`)).toBeVisible();\n  });\n});\n```\n\n## Best Practices\n\n### Always Use Faker for Test Data\n\n```typescript\n// ✅ GOOD: Use faker\nconst user = {\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n};\n\n// ❌ BAD: Hardcoded data\nconst user = { id: '123', name: 'Test User' };\n```\n\n### Use Factories Instead of Static Mocks\n\n**Use Fishery** for creating test data factories with sensible defaults and overrides:\n\n```typescript\n// ✅ GOOD: Fishery factory\nimport { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\n\nconst userFactory = Factory.define<User>(({ sequence }) => ({\n  id: faker.string.uuid(),\n  name: faker.person.fullName(),\n  email: faker.internet.email(),\n  age: faker.number.int({ min: 18, max: 100 }),\n}));\n\n// Usage - flexible and reusable\nconst user1 = userFactory.build();\nconst user2 = userFactory.build({ age: 25 });\nconst user3 = userFactory.build({ name: 'Specific Name' });\nconst users = userFactory.buildList(5); // Create multiple\n\n// ❌ BAD: Static mock objects\nconst mockUser1 = {\n  id: '123',\n  name: 'User 1',\n  email: 'user1@test.com',\n  age: 30,\n};\n```\n\nBenefits of Fishery factories:\n\n- Easy to override specific properties per test\n- Consistent default values across tests\n- Single source of truth for mock structure\n- Better maintainability when types change\n- Built-in support for sequences and traits\n\n### Never Use Fixed Timeouts\n\n```typescript\n// ❌ BAD: Fixed timeout\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nawait page.waitForTimeout(2000);\n\n// ✅ GOOD: Wait for condition\nawait waitFor(() => {\n  expect(element).toBeInTheDocument();\n});\n\nawait page.waitForSelector('[data-test=\"result\"]');\n```\n\n### Mock External Dependencies\n\n```typescript\n// ✅ GOOD: Mock services\njest.mock('uiSrc/services/api', () => ({\n  apiService: {\n    get: jest.fn(),\n    post: jest.fn(),\n  },\n}));\n```\n\n### Parameterized Tests with `it.each`\n\n**Use `it.each` for multiple tests with the same body but different inputs:**\n\n```typescript\n// ✅ GOOD: Parameterized tests\nit.each([\n  { description: 'null', value: null },\n  { description: 'undefined', value: undefined },\n  { description: 'empty string', value: '' },\n  { description: 'whitespace only', value: '   ' },\n])('should return error when input is $description', async ({ value }) => {\n  const result = await service.processInput(value);\n  expect(result.status).toBe('error');\n});\n```\n\n**Benefits:**\n\n- DRY: Single test body shared across all cases\n- Maintainability: Changes to test logic only need to be made once\n- Readability: Test cases are clearly defined in a table\n- Easier to extend: Adding new test cases is just adding a new row\n\n### Test Edge Cases\n\nAlways test:\n\n- Empty arrays/objects\n- Null/undefined values\n- Error scenarios\n- Boundary conditions\n- Loading states\n\n## Testing Checklist\n\n- [ ] All new features have tests\n- [ ] Tests use faker for data generation\n- [ ] No fixed timeouts (use waitFor)\n- [ ] Tests follow AAA pattern\n- [ ] Descriptive test names\n- [ ] Shared `renderComponent` helper used\n- [ ] Default props defined\n- [ ] Edge cases covered\n- [ ] Error scenarios tested\n- [ ] Mocks cleaned up between tests\n- [ ] Integration tests for API endpoints\n- [ ] E2E tests for critical flows\n- [ ] Coverage meets thresholds (80%+)\n"
  },
  {
    "path": ".ai/skills/branches/SKILL.md",
    "content": "---\nname: branches\ndescription: >-\n  Create and name git branches following project conventions and GitHub Actions\n  enforcement rules. Use when creating branches, checking out new branches, or\n  the user mentions branch naming.\n---\n\n# Branch Naming Conventions\n\nUse lowercase kebab-case with type prefix and issue/ticket identifier. **Branch names must match GitHub Actions workflow rules** (see `.github/workflows/enforce-branch-name-rules.yml`).\n\n```bash\n# Pattern: <type>/<issue-ref>/<short-title>\n\n# INTERNAL (JIRA - RI-XXX)\nfeature/RI-123/add-user-profile\nbugfix/RI-789/memory-leak\nfe/feature/RI-567/add-dark-mode\nbe/bugfix/RI-345/fix-redis-connection\ndocs/RI-333/update-docs\ntest/RI-444/add-unit-tests\ne2e/RI-555/add-integration-tests\n\n# OPEN SOURCE (GitHub - XXX)\nfeature/123/add-export-feature\nbugfix/789/fix-connection-timeout\n\n# Special branches\nrelease/v2.0.0\nric/RI-666/custom-prefix\n```\n\n## Allowed Branch Types (GitHub Actions Enforced)\n\n- `feature/` - New features and refactoring (affects multiple areas)\n- `bugfix/` - Bug fixes (affects multiple areas)\n- `fe/feature/` - Frontend-only features (only `redisinsight/ui/` folder)\n- `fe/bugfix/` - Frontend-only bug fixes (only `redisinsight/ui/` folder)\n- `be/feature/` - Backend-only features (only `redisinsight/api/` folder)\n- `be/bugfix/` - Backend-only bug fixes (only `redisinsight/api/` folder)\n- `docs/` - Documentation changes\n- `test/` - Test-related changes\n- `e2e/` - End-to-end test changes\n- `release/` - Release branches\n- `ric/` - Custom prefix for special cases\n\n**Note:** When a bug fix affects only the `redisinsight/ui/` folder, use `fe/bugfix/` prefix instead of `bugfix/`.\n\n## Issue References\n\n- **Internal**: `RI-XXX` (JIRA ticket)\n- **Open Source**: `XXX` (GitHub issue number)\n- Use `#` only in commit messages, not branch names\n"
  },
  {
    "path": ".ai/skills/commits/SKILL.md",
    "content": "---\nname: commits\ndescription: >-\n  Generate commit messages following Conventional Commits format. Use when\n  creating commits, writing commit messages, or the user asks to commit changes.\n---\n\n# Commit Message Guidelines\n\nFollow **Conventional Commits** format:\n\n```\n<type>(<scope>): <subject>\n\n<body>\n\n<footer>\n```\n\n## Commit Types\n\n- `feat` - New feature\n- `fix` - Bug fix\n- `refactor` - Code refactoring\n- `test` - Adding or updating tests\n- `docs` - Documentation changes\n- `chore` - Maintenance tasks\n- `style` - Code style changes (formatting)\n- `perf` - Performance improvements\n- `ci` - CI/CD changes\n\n## Examples\n\n```bash\nfeat(ui): add user search functionality\n\nImplements real-time search with debouncing.\n\nReferences: #RI-123\n\n---\n\nfix(api): resolve memory leak in connection pool\n\nProperly cleanup subscriptions on unmount.\n\nFixes #456\n\n---\n\ntest(ui): add tests for data serialization\n\nrefactor(api): extract common validation logic\n\ndocs: update API endpoint documentation\n\nchore: upgrade React to version 18.2\n```\n\n## Best Practices\n\n### ✅ Good Commits\n\n- Clear, descriptive subject line\n- Atomic changes (one logical change per commit)\n- Reference issue/ticket in body\n- Explain **why**, not just **what**\n- **Keep it concise** - Don't list every file change in the body\n\n```bash\nfeat(ui): add user profile editing\n\nAllows users to update their profile information including\nname, email, and avatar. Includes validation and error handling.\n\nReferences: #RI-123\n```\n\n### ❌ Bad Commits\n\n```bash\n# Too vague\nfix stuff\nWIP\nupdate\n\n# Too broad\nadd feature, fix bugs, refactor code, update tests\n```\n\n## Issue References\n\n- **JIRA (internal)**: `References: #RI-123` or `Fixes #RI-123`\n- **GitHub (open source)**: `Fixes #456` or `Closes #456`\n- Use `#` for auto-linking in commit messages\n"
  },
  {
    "path": ".ai/skills/pull-requests/SKILL.md",
    "content": "---\nname: pull-requests\ndescription: >-\n  Create and review pull requests following project standards including title\n  format, description template, and review checklist. Use when creating PRs,\n  writing PR descriptions, or reviewing pull requests.\n---\n\n# Pull Request Guidelines\n\n## Creating a PR\n\n### Labels\n\nWhen creating PRs with AI assistance, always add the **\"AI-Made\"** label.\n\n### PR Title\n\nInclude issue number at the start:\n\n```\nRI-123 Add user profile editing\n#456 Fix memory leak in connection pool\n```\n\n### PR Description Template\n\n```markdown\n# What\n\nDescribe what was changed.\n\n# Testing\n\nDescribe how to test the changes.\n\n---\n\nCloses #RI-123\n```\n\n**PR Description Guidelines:**\n\n- **Keep it concise** - Avoid verbose descriptions\n- **Focus on high-level changes** - Don't list every code change in the #What section\n- **Brief and to the point** - The diff shows the details; describe the \"why\" and \"what\" at a high level\n- **Technical decisions** - Only mention significant architectural or design decisions if relevant\n\n## Review Process\n\n### As PR Author\n\n- **Respond to all comments** - Address every piece of feedback\n- **Don't take feedback personally** - Reviews improve code quality\n- **Update code based on feedback** - Make requested changes\n- **Mark conversations as resolved** - After addressing feedback\n- **Keep PR up to date** - Rebase on main regularly\n\n### As PR Reviewer\n\n- **Be constructive and respectful** - Focus on improvement\n- **Focus on logic, not style** - Linter handles formatting\n- **Check for**:\n  - Logic errors and edge cases\n  - Performance issues\n  - Security concerns\n  - Test coverage\n  - Missing documentation\n  - Architectural concerns\n\n### Review Checklist\n\n- [ ] Code follows project patterns\n- [ ] Tests are comprehensive\n- [ ] No console.log or debug code\n- [ ] TypeScript types are proper\n- [ ] Error handling is adequate\n- [ ] Documentation is updated\n- [ ] No security vulnerabilities\n- [ ] Performance is acceptable\n"
  },
  {
    "path": ".cursor/agents/test-runner.md",
    "content": "---\nname: test-runner\nmodel: composer-1.5\ndescription: Test automation expert. Use proactively to run tests and fix failures.\n---\n\nYou are a test automation expert.\n\nWhen you see code changes, proactively run appropriate tests.\n\nWhen completing a major task, run all tests to ensure the task is complete.\n\nIf tests fail:\n\n1. Analyze the failure output\n2. Identify the root cause\n3. Fix the issue while preserving test intent\n4. Re-run to verify\n\nReport test results with:\n\n- Number of tests passed/failed\n- Summary of any failures\n- Changes made to fix issues\n"
  },
  {
    "path": ".cursorignore",
    "content": "!/node_modules"
  },
  {
    "path": ".dockerignore",
    "content": ".git\n.idea\n.vscode\n.docker\n\n**/node_modules\n\n**/dist\n**/coverage\n**/dll\n**/.issues\n**/.parcel-cache\n**/.temp_cache\n**/.nyc_output\n\nredisinsight/main.prod.js\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nquote_type = single\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[/tests/**.ts]\nindent_size = 4\n"
  },
  {
    "path": ".eslintignore",
    "content": "tests/e2e\ntests/e2e-playwright\n\n# Logs\nlogs\n*.log\n\n# Tests coverage results\nredisinsight/api/test/test-runs/coverage\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n.eslintcache\n\n# Dependency directory\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git\nnode_modules\n\n# OSX\n.DS_Store\n\n# App packaged\nrelease\n*.main.prod.js\n*.renderer.prod.js\nscripts\nconfigs\ndist\ndll\n*.main.js\n\n.idea\nnpm-debug.log.*\n__snapshots__\n\n# Package.json\npackage.json\n.travis.yml\n*.css.d.ts\n*.sass.d.ts\n*.scss.d.ts\n\n# temp folders - remove in future after fix all issues\nredisinsight/ui/src/packages/redisgraph\nredisinsight/ui/src/packages/redistimeseries-app\n\n/report\n__mocks__\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "const path = require('path');\n\nconst noUnusedVarsConfig = [\n  'error',\n  {\n    argsIgnorePattern: '^_',\n    varsIgnorePattern: '^_',\n    destructuredArrayIgnorePattern: '^_',\n  },\n];\n\nmodule.exports = {\n  root: true,\n  env: {\n    node: true,\n    browser: true,\n  },\n  extends: ['airbnb-typescript', 'plugin:prettier/recommended'],\n  plugins: ['@typescript-eslint', 'import'],\n  parser: '@typescript-eslint/parser',\n  rules: {\n    quotes: [2, 'single', { avoidEscape: true }],\n    'max-len': [\n      'error',\n      {\n        ignoreComments: true,\n        ignoreStrings: true,\n        ignoreRegExpLiterals: true,\n        code: 120,\n      },\n    ],\n    'class-methods-use-this': 'off',\n    'import/no-extraneous-dependencies': 'off', // temporary disabled\n    'object-curly-newline': 'off',\n    'import/prefer-default-export': 'off',\n    '@typescript-eslint/comma-dangle': 'off',\n    'implicit-arrow-linebreak': 'off',\n    'import/order': [\n      1,\n      {\n        groups: [\n          'external',\n          'builtin',\n          'internal',\n          'sibling',\n          'parent',\n          'index',\n        ],\n        pathGroups: [\n          {\n            pattern: 'desktopSrc/**',\n            group: 'internal',\n            position: 'after',\n          },\n          {\n            pattern: 'uiSrc/**',\n            group: 'internal',\n            position: 'after',\n          },\n          {\n            pattern: 'apiSrc/**',\n            group: 'internal',\n            position: 'after',\n          },\n        ],\n        warnOnUnassignedImports: true,\n        pathGroupsExcludedImportTypes: ['builtin'],\n      },\n    ],\n  },\n  overrides: [\n    // Backend/API specific rules\n    {\n      files: ['redisinsight/api/**/*.ts', 'redisinsight/api/**/*.js'],\n      env: {\n        node: true,\n        browser: false,\n      },\n      extends: ['airbnb-typescript/base', 'plugin:prettier/recommended'],\n      plugins: ['@typescript-eslint', 'sonarjs', 'import'],\n      rules: {\n        'max-len': ['warn', 120],\n        '@typescript-eslint/return-await': 'off',\n        '@typescript-eslint/dot-notation': 'off',\n        'import/no-extraneous-dependencies': 'off',\n        '@typescript-eslint/no-unused-vars': noUnusedVarsConfig,\n        // SonarJS rules (manually enabled since v2.x doesn't have recommended config)\n        'sonarjs/cognitive-complexity': ['error', 15],\n        'sonarjs/no-duplicate-string': 'error',\n        'sonarjs/no-identical-functions': 'error',\n        'sonarjs/prefer-immediate-return': 'error',\n        'sonarjs/no-small-switch': 'error',\n        'sonarjs/no-nested-template-literals': 'off',\n        'no-console': 'error',\n        'import/no-duplicates': 'error',\n        'prefer-destructuring': 'error',\n        'no-unneeded-ternary': 'error',\n        'prefer-template': 'error',\n        'prefer-const': 'error',\n      },\n      parserOptions: {\n        project: path.join(__dirname, 'redisinsight/api/tsconfig.json'),\n      },\n    },\n    // Backend test files\n    {\n      files: [\n        'redisinsight/api/**/*.spec.ts',\n        'redisinsight/api/**/__mocks__/**/*',\n      ],\n      rules: {\n        'sonarjs/no-duplicate-string': 0,\n        'sonarjs/no-identical-functions': 0,\n        'import/first': 0,\n      },\n    },\n    // Frontend/UI specific rules\n    {\n      files: [\n        'redisinsight/ui/**/*.ts',\n        'redisinsight/ui/**/*.tsx',\n        'redisinsight/ui/**/*.js',\n        'redisinsight/ui/**/*.jsx',\n      ],\n      env: {\n        browser: true,\n        node: false,\n      },\n      extends: [\n        'airbnb-typescript',\n        'airbnb/hooks',\n        'plugin:prettier/recommended',\n      ],\n      plugins: [\n        '@typescript-eslint',\n        'sonarjs',\n        'import',\n        'react',\n        'react-hooks',\n        'jsx-a11y',\n      ],\n      parserOptions: {\n        ecmaVersion: 2020,\n        sourceType: 'module',\n        project: path.join(__dirname, 'tsconfig.json'),\n      },\n      rules: {\n        radix: 'off',\n        'no-bitwise': ['error', { allow: ['|'] }],\n        'max-len': [\n          'error',\n          {\n            ignoreComments: true,\n            ignoreStrings: true,\n            ignoreRegExpLiterals: true,\n            code: 120,\n          },\n        ],\n        'class-methods-use-this': 'off',\n        'import/no-extraneous-dependencies': 'off',\n        'import/prefer-default-export': 'off',\n        'import/no-cycle': 'off',\n        'import/no-named-as-default-member': 'off',\n        'no-plusplus': 'off',\n        'no-return-await': 'off',\n        'no-underscore-dangle': 'off',\n        'no-useless-catch': 'off',\n        'no-console': ['error', { allow: ['warn', 'error'] }],\n        'jsx-a11y/anchor-is-valid': 'off',\n        'jsx-a11y/no-access-key': 'off',\n        'max-classes-per-file': 'off',\n        'no-case-declarations': 'off',\n        'react-hooks/exhaustive-deps': 'off',\n        'react/jsx-props-no-spreading': 'off',\n        'react/require-default-props': 'off',\n        'react/prop-types': 1,\n        'react/jsx-one-expression-per-line': 'off',\n        '@typescript-eslint/comma-dangle': 'off',\n        '@typescript-eslint/no-shadow': 'off',\n        '@typescript-eslint/no-unused-expressions': 'off',\n        '@typescript-eslint/no-use-before-define': 'off',\n        'implicit-arrow-linebreak': 'off',\n        'object-curly-newline': 'off',\n        'no-nested-ternary': 'off',\n        'no-param-reassign': ['error', { props: false }],\n        'sonarjs/no-duplicate-string': 'off',\n        'sonarjs/cognitive-complexity': [1, 20],\n        'sonarjs/no-identical-functions': [0, 5],\n        'sonarjs/prefer-immediate-return': 'error',\n        'sonarjs/no-small-switch': 'error',\n        'import/no-duplicates': 'error',\n        'prefer-destructuring': 'error',\n        'no-unneeded-ternary': 'error',\n        'prefer-template': 'error',\n        'prefer-const': 'error',\n        '@typescript-eslint/no-unused-vars': noUnusedVarsConfig,\n        'import/order': [\n          1,\n          {\n            groups: [\n              'external',\n              'builtin',\n              'internal',\n              'sibling',\n              'parent',\n              'index',\n            ],\n            pathGroups: [\n              {\n                pattern: 'uiSrc/**',\n                group: 'internal',\n                position: 'after',\n              },\n              {\n                pattern: 'apiSrc/**',\n                group: 'internal',\n                position: 'after',\n              },\n              {\n                pattern: '{.,..}/*.scss',\n                group: 'object',\n                position: 'after',\n              },\n            ],\n            warnOnUnassignedImports: true,\n            pathGroupsExcludedImportTypes: ['builtin'],\n          },\n        ],\n      },\n    },\n    // UI test files\n    {\n      files: ['redisinsight/ui/**/*.spec.ts', 'redisinsight/ui/**/*.spec.tsx'],\n      env: {\n        jest: true,\n      },\n    },\n    // Storybook files only\n    {\n      files: [\n        '.storybook/**/*.@(ts|tsx|js|jsx)',\n        'stories/**/*.@(ts|tsx|js|jsx)',\n        '**/*.stories.@(ts|tsx|js|jsx)',\n        '**/*.story.@(ts|tsx|js|jsx)',\n      ],\n      extends: ['plugin:storybook/recommended'],\n    },\n    // TypeScript files (general) - MUST BE LAST to override other rules\n    {\n      files: ['*.ts', '*.tsx'],\n      rules: {\n        '@typescript-eslint/semi': ['error', 'never'],\n        semi: 'off',\n        '@typescript-eslint/default-param-last': 'off',\n      },\n    },\n    // JavaScript files (general) - MUST BE LAST to override other rules\n    {\n      files: ['*.js', '*.jsx', '*.cjs'],\n      rules: {\n        semi: ['error', 'always'],\n        '@typescript-eslint/semi': 'off',\n      },\n    },\n    // Temporary disable some rules for API\n    {\n      files: ['redisinsight/api/**/*.ts', 'redisinsight/api/esbuild.js'],\n      rules: {\n        semi: 'off',\n        '@typescript-eslint/semi': 'off',\n        'sonarjs/no-identical-functions': 'off',\n        'sonarjs/prefer-immediate-return': 'off',\n        'sonarjs/no-duplicate-string': 'off',\n        'sonarjs/cognitive-complexity': 'off',\n        'sonarjs/no-small-switch': 'off',\n        'max-len': 'off',\n        'import/order': 'off',\n        'no-underscore-dangle': 'off',\n        'import/no-duplicates': 'off',\n        'no-console': 'off',\n        'prefer-destructuring': 'off',\n        'no-unneeded-ternary': 'off',\n        'prefer-template': 'off',\n        'prefer-const': 'off',\n        // REDUNDANT: These are OFF by default in newer Airbnb config\n        // 'prefer-arrow-callback': 'off',\n        // 'no-restricted-syntax': 'off',\n        // 'no-control-regex': 'off',\n        // 'func-names': 'off',\n        // 'no-case-declarations': 'off',\n        // radix: 'off',\n        // 'arrow-body-style': 'off',\n        // 'no-constant-condition': 'off',\n        // 'consistent-return': 'off',\n        // 'no-useless-concat': 'off',\n        // 'import/export': 'off',\n      },\n    },\n    // Temporary (maybe) disable some rules for API tests\n    {\n      files: ['redisinsight/api/test/**/*.ts'],\n      // In order to lint just the test files\n      // make sure there's no override on 'redisinsight/api'\n      // a.k.a. comment the above section\n      rules: {\n        '@typescript-eslint/no-loop-func': 'off',\n        '@typescript-eslint/semi': 'off',\n        'no-console': 'off',\n        'prefer-template': 'off',\n        'import/order': 'off',\n        '@typescript-eslint/no-use-before-define': 'off',\n        '@typescript-eslint/no-shadow': 'off',\n        '@typescript-eslint/no-unused-expressions': 'off',\n        '@typescript-eslint/naming-convention': 'off',\n        'sonarjs/no-duplicate-string': 'off',\n        'sonarjs/prefer-immediate-return': 'off',\n        'sonarjs/cognitive-complexity': 'off',\n        'max-len': 'off',\n        'prefer-destructuring': 'off',\n        'prefer-const': 'off',\n        // REDUNDANT: These are OFF by default in newer Airbnb config\n        // semi: 'off',\n        // 'sonarjs/no-ignored-return': 'off',\n        // 'sonarjs/no-identical-expressions': 'off',\n        // 'sonarjs/no-nested-switch': 'off',\n        // 'sonarjs/no-identical-functions': 'off',\n        // 'no-plusplus': 'off',\n        // 'array-callback-return': 'off',\n        // 'no-underscore-dangle': 'off',\n        // 'import/newline-after-import': 'off',\n        // 'global-require': 'off',\n        // 'object-shorthand': 'off',\n        // 'import/no-useless-path-segments': 'off',\n        // 'import/first': 'off',\n        // 'one-var': 'off',\n        // 'no-multi-assign': 'off',\n        // 'spaced-comment': 'off',\n        // 'no-lonely-if': 'off',\n        // 'no-useless-computed-key': 'off',\n        // 'no-return-assign': 'off',\n        // 'prefer-promise-reject-errors': 'off',\n        // 'no-fallthrough': 'off',\n        // 'no-else-return': 'off',\n        // 'no-empty': 'off',\n        // 'import/no-mutable-exports': 'off',\n        // 'import/no-cycle': 'off',\n        // 'no-useless-escape': 'off',\n        // 'default-case': 'off',\n        // eqeqeq: 'off',\n        // yoda: 'off',\n        // 'prefer-arrow-callback': 'off',\n        // 'arrow-body-style': 'off',\n        // 'no-constant-condition': 'off',\n        // 'no-restricted-syntax': 'off',\n        // 'no-case-declarations': 'off',\n        // 'func-names': 'off',\n        // 'consistent-return': 'off',\n        // radix: 'off',\n      },\n    },\n    // Temporary disable some rules for UI\n    {\n      files: ['redisinsight/ui/**/*.ts*'],\n      rules: {\n        'sonarjs/cognitive-complexity': 'off',\n        'import/extensions': 'off',\n        'react/prop-types': 'off',\n        'import/order': 'off',\n        'prefer-const': 'off',\n        'prefer-destructuring': 'off',\n        // REDUNDANT: These are OFF by default in newer Airbnb config\n        // 'react/jsx-boolean-value': 'off',\n        // 'sonarjs/no-nested-template-literals': 'off',\n        // 'sonarjs/no-extra-arguments': 'off',\n        // 'consistent-return': 'off',\n        // 'react/no-array-index-key': 'off',\n        // 'react/no-unused-prop-types': 'off',\n        // 'react/destructuring-assignment': 'off',\n        // 'jsx-a11y/control-has-associated-label': 'off',\n        // 'react/button-has-type': 'off',\n        // 'react/no-unescaped-entities': 'off',\n        // 'no-useless-escape': 'off',\n        // 'no-template-curly-in-string': 'off',\n      },\n    },\n  ],\n  parserOptions: {\n    project: './tsconfig.json',\n    ecmaVersion: 2020,\n    sourceType: 'module',\n  },\n  settings: {\n    react: {\n      version: 'detect', // Automatically detect React version\n    },\n  },\n  ignorePatterns: [\n    'dist',\n    'node_modules',\n    'release',\n    'redisinsight/ui/src/packages/**/icons/*.js*',\n    'redisinsight/ui/src/packages/**',\n    'redisinsight/api/report/**',\n    'redisinsight/api/static/**',\n    'redisinsight/api/migration/**',\n    // Config files that don't need linting\n    '.eslintrc.js',\n    'electron-builder-mas.js',\n    'jest-resolver.js',\n    'resources/resources.d.ts',\n  ],\n};\n"
  },
  {
    "path": ".gitattributes",
    "content": "*       text    eol=lf\n*.exe   binary\n*.png   binary\n*.jpg   binary\n*.jpeg  binary\n*.ico   binary\n*.icns  binary\n*.otf   binary\n*.eot   binary\n*.ttf   binary\n*.woff  binary\n*.woff2 binary\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* krum.tyukenov@redis.com pavel.angelov@redis.com dijana.antovska@redis.com artem.horuzhenko@redis.com petar.dzhambazov@redis.com dimo.georgiev@redis.com valentin.kirilov@redis.com\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: '[Bug]:'\nlabels: bug\nassignees: ''\n---\n\n**Preconditions** (Any important steps we need to know)\n\n**Steps to reproduce** (How to reproduce what you found step by step)\n\n**Actual behavior** (A short description of what you found)\n\n**Expected behavior** (A short description of what you expected to find)\n\n**Screenshots** (Paste or drag-and-drop a screenshot or a link to a recording)\n\n**Additional context** (Operating system, version of Redis Insight, Redis database version, Redis module version, database type, connection type, logs, or any other information)\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Request a feature or submit an idea\ntitle: '[Feature Request]:'\nlabels: feature\nassignees: ''\n---\n\n**Problem description** (Describe the problem you would like to solve with this feature request or suggested idea).\n\n**How often do you encounter this problem** (Describe how frequently this problem occurs).\n\n**Alternatives considered** (Describe any alternative features or products you've considered).\n\n**Additional information** (Add any other context or details here).\n"
  },
  {
    "path": ".github/actions/deploy-test-reports/action.yml",
    "content": "name: Deploy Pages\ndescription: 'Download the artifact and deploy to GitHub Pages'\n\ninputs:\n  group:\n    description: Group matching the artifacts\n    required: false\n    default: '*'\n  path:\n    description: Path for link to the report\n    required: false\n    default: ''\n\n  AWS_BUCKET_NAME_TEST:\n    required: true\n  AWS_DEFAULT_REGION:\n    required: true\n  AWS_DISTRIBUTION_ID:\n    required: true\n  AWS_ACCESS_KEY_ID:\n    required: true\n  AWS_SECRET_ACCESS_KEY:\n    required: true\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Get current date\n      id: date\n      uses: ./.github/actions/get-current-date\n\n    - name: Download artifacts\n      uses: actions/download-artifact@v4\n      with:\n        pattern: ${{ format('{0}*', inputs.group) }}\n        path: public/${{ github.run_id }}\n\n    - name: Deploy 🚀\n      shell: bash\n      env:\n        AWS_BUCKET_NAME_TEST: ${{ inputs.AWS_BUCKET_NAME_TEST }}\n        AWS_DEFAULT_REGION: ${{ inputs.AWS_DEFAULT_REGION }}\n        AWS_DISTRIBUTION_ID: ${{ inputs.AWS_DISTRIBUTION_ID }}\n        AWS_ACCESS_KEY_ID: ${{ inputs.AWS_ACCESS_KEY_ID }}\n        AWS_SECRET_ACCESS_KEY: ${{ inputs.AWS_SECRET_ACCESS_KEY }}\n      run: |\n\n        SUB_PATH=test-reports/${{ steps.date.outputs.date }}\n\n        aws s3 cp public/ s3://${AWS_BUCKET_NAME_TEST}/public/${SUB_PATH} --recursive\n\n    - name: Add link to report in the workflow summary\n      shell: bash\n      run: |\n        link=\"${{ inputs.path }}/index.html\"\n\n        echo \"- [${link}](${link})\" >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".github/actions/get-current-date/action.yml",
    "content": "name: Get current date\n\noutputs:\n  date:\n    description: Current date\n    value: ${{ steps.date.outputs.date }}\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Get current date\n      id: date\n      shell: bash\n      run: |\n        DATE=$(date +'%Y-%m-%d')\n        echo \"date=$DATE\" >> $GITHUB_OUTPUT\n"
  },
  {
    "path": ".github/actions/install-all-build-libs/action.yml",
    "content": "name: Install all libraries action\ndescription: Install all libraries and dependencies\ninputs:\n  skip-system-deps:\n    description: Skip install system dependencies (libsecret, rpm, etc.)\n    default: '0'\n    required: false\n\n  keytar-host-mirror:\n    description: Keytar binary host mirror\n    required: false\n\n  sqlite3-host-mirror:\n    description: SQLite3 binary host mirror\n    required: false\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup Node\n      uses: actions/setup-node@v4.0.4\n      with:\n        node-version: '22.12.0'\n        # disable cache for windows\n        # https://github.com/actions/setup-node/issues/975\n        cache: ${{ runner.os != 'Windows' && 'yarn' || '' }}\n        cache-dependency-path: ${{ runner.os != 'Windows' && '**/yarn.lock' || '' }}\n\n    - name: Cache node_modules\n      id: cache-node-modules\n      uses: actions/cache@v4\n      with:\n        path: |\n          node_modules\n          redisinsight/node_modules\n          redisinsight/api/node_modules\n        key: node-modules-${{ runner.os }}-${{ hashFiles('yarn.lock', 'redisinsight/yarn.lock', 'redisinsight/api/yarn.lock') }}\n        restore-keys: |\n          node-modules-${{ runner.os }}-\n\n    - name: Setup Python\n      uses: actions/setup-python@v5\n      with:\n        python-version: '3.11'\n\n    - name: Install linux libraries\n      if: ${{ runner.os == 'Linux' && inputs.skip-system-deps != '1' }}\n      shell: bash\n      run: |\n        sudo apt-get update -qy\n        sudo apt-get install -qy libsecret-1-dev rpm\n\n    - name: Install macos libraries\n      if: ${{ runner.os == 'macOS' && inputs.skip-system-deps != '1' }}\n      shell: bash\n      run: |\n        brew install libsecret\n\n    # Javascript dependencies (skip if cache was restored)\n    - name: Install dependencies for redisinsight package.js\n      if: steps.cache-node-modules.outputs.cache-hit != 'true'\n      uses: ./.github/actions/install-deps\n      with:\n        dir-path: './redisinsight'\n        keytar-host-mirror: ${{ inputs.keytar-host-mirror }}\n        sqlite3-host-mirror: ${{ inputs.sqlite3-host-mirror }}\n\n    - name: Install dependencies for BE package.js\n      if: steps.cache-node-modules.outputs.cache-hit != 'true'\n      uses: ./.github/actions/install-deps\n      with:\n        dir-path: './redisinsight/api'\n\n    - name: Install dependencies for root package.js\n      if: steps.cache-node-modules.outputs.cache-hit != 'true'\n      uses: ./.github/actions/install-deps\n      with:\n        dir-path: './'\n        keytar-host-mirror: ${{ inputs.keytar-host-mirror }}\n        sqlite3-host-mirror: ${{ inputs.sqlite3-host-mirror }}\n"
  },
  {
    "path": ".github/actions/install-apple-certs/action.yml",
    "content": "name: Add certs to the keychain (macos)\n\ninputs:\n  CSC_P12_BASE64:\n    required: true\n  CSC_MAC_INSTALLER_P12_BASE64:\n    required: true\n  CSC_MAS_P12_BASE64:\n    required: true\n  CSC_KEY_PASSWORD:\n    required: true\n  CSC_MAS_PASSWORD:\n    required: true\n  CSC_MAC_INSTALLER_PASSWORD:\n    required: true\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup sign certificates\n      shell: bash\n      env:\n        CSC_P12_BASE64: ${{ inputs.CSC_P12_BASE64 }}\n        CSC_MAC_INSTALLER_P12_BASE64: ${{ inputs.CSC_MAC_INSTALLER_P12_BASE64 }}\n        CSC_MAS_P12_BASE64: ${{ inputs.CSC_MAS_P12_BASE64 }}\n      run: |\n        mkdir -p certs\n        echo \"$CSC_P12_BASE64\" | base64 -d > certs/mac-developer.p12\n        echo \"$CSC_MAC_INSTALLER_P12_BASE64\" | base64 -d > certs/mac-installer.p12\n        echo \"$CSC_MAS_P12_BASE64\" | base64 -d > certs/mas-distribution.p12\n\n    - name: Add certs to the keychain\n      shell: bash\n      env:\n        KEYCHAIN: redisinsight.keychain\n        CSC_KEY_PASSWORD: ${{ inputs.CSC_KEY_PASSWORD }}\n        CSC_MAS_PASSWORD: ${{ inputs.CSC_MAS_PASSWORD }}\n        CSC_MAC_INSTALLER_PASSWORD: ${{ inputs.CSC_MAC_INSTALLER_PASSWORD }}\n      run: |\n        security create-keychain -p mysecretpassword $KEYCHAIN\n        security default-keychain -s $KEYCHAIN\n        security unlock-keychain -p mysecretpassword $KEYCHAIN\n        security set-keychain-settings -u -t 10000000 $KEYCHAIN\n        security import certs/mac-developer.p12 -k $KEYCHAIN -P \"$CSC_KEY_PASSWORD\" -T /usr/bin/codesign -T /usr/bin/productbuild\n        security import certs/mas-distribution.p12 -k $KEYCHAIN -P \"$CSC_MAS_PASSWORD\" -T /usr/bin/codesign -T /usr/bin/productbuild\n        security import certs/mac-installer.p12 -k $KEYCHAIN -P \"$CSC_MAC_INSTALLER_PASSWORD\" -T /usr/bin/codesign -T /usr/bin/productbuild\n        security set-key-partition-list -S apple-tool:,apple: -s -k mysecretpassword $KEYCHAIN\n        security list-keychain -d user -s $KEYCHAIN\n"
  },
  {
    "path": ".github/actions/install-deps/action.yml",
    "content": "name: Install Dependencies action\ndescription: Caches and installs dependencies for a given path\ninputs:\n  dir-path:\n    description: Path to the directory\n    required: true\n  keytar-host-mirror:\n    description: Keytar binary host mirror\n    required: false\n  sqlite3-host-mirror:\n    description: SQLite3 binary host mirror\n    required: false\n  skip-postinstall:\n    description: Skip postinstall\n    required: false\n    default: '0'\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Install dependencies\n      working-directory: ${{ inputs.dir-path }}\n      shell: bash\n\n      # env:\n      #   SKIP_POSTINSTALL: ${{ inputs.skip-postinstall }}\n      # run: yarn install\n      run: |\n        # todo: uncomment after build our binaries\n        # export npm_config_keytar_binary_host_mirror=${{ inputs.keytar-host-mirror }}\n        # export npm_config_node_sqlite3_binary_host_mirror=${{ inputs.sqlite3-host-mirror }}\n\n        yarn install --frozen-lockfile --network-timeout 1000000\n"
  },
  {
    "path": ".github/actions/install-windows-certs/action.yml",
    "content": "name: Install Windows certs\n\ninputs:\n  WIN_CSC_PFX_BASE64:\n    required: true\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup sign certificates\n      shell: bash\n      env:\n        WIN_CSC_PFX_BASE64: ${{ inputs.WIN_CSC_PFX_BASE64 }}\n      run: |\n        mkdir -p certs\n        echo \"$WIN_CSC_PFX_BASE64\" | base64 -d > certs/redislabs_win.pfx\n"
  },
  {
    "path": ".github/actions/remove-artifacts/action.yml",
    "content": "name: Remove all artifacts\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Merge artifacts by pattern\n      id: merge-artifacts\n      uses: actions/upload-artifact/merge@v4\n      with:\n        name: remove-artifacts\n        pattern: '*'\n        delete-merged: true\n\n    - name: Delete merged artifact\n      uses: actions/github-script@v7\n      with:\n        script: |\n          github.rest.actions.deleteArtifact({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            artifact_id: ${{ steps.merge-artifacts.outputs.artifact-id }}\n          });\n"
  },
  {
    "path": ".github/actions/setup-e2e-playwright/action.yml",
    "content": "name: 'Setup E2E Playwright'\ndescription: 'Setup Node.js, E2E dependencies, and Playwright browsers with caching'\n\ninputs:\n  browsers:\n    description: 'Playwright browsers to install (chromium, firefox, webkit, all, none)'\n    required: false\n    default: 'chromium'\n  working-directory:\n    description: 'E2E tests directory'\n    required: false\n    default: './tests/e2e-playwright'\n\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version-file: '.nvmrc'\n        # Note: We don't use setup-node's npm cache here because:\n        # 1. We cache node_modules directly (more effective)\n        # 2. Direct node_modules caching is faster than npm cache\n\n    - name: Cache E2E node_modules\n      id: cache-e2e-deps\n      uses: actions/cache@v4\n      with:\n        path: ${{ inputs.working-directory }}/node_modules\n        key: e2e-node-modules-${{ runner.os }}-${{ hashFiles(format('{0}/package-lock.json', inputs.working-directory)) }}\n\n    - name: Install E2E dependencies\n      if: steps.cache-e2e-deps.outputs.cache-hit != 'true'\n      shell: bash\n      working-directory: ${{ inputs.working-directory }}\n      run: npm ci\n\n    - name: Cache Playwright browsers\n      if: inputs.browsers != 'none'\n      id: cache-playwright\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/ms-playwright\n        # Use package-lock.json hash as cache key - it changes when Playwright version changes\n        key: playwright-${{ runner.os }}-${{ hashFiles(format('{0}/package-lock.json', inputs.working-directory)) }}-${{ inputs.browsers }}\n\n    - name: Install Playwright browsers\n      if: inputs.browsers != 'none' && steps.cache-playwright.outputs.cache-hit != 'true'\n      shell: bash\n      working-directory: ${{ inputs.working-directory }}\n      env:\n        BROWSERS: ${{ inputs.browsers }}\n      run: |\n        if [ \"$BROWSERS\" == \"all\" ]; then\n          npx playwright install --with-deps\n        else\n          npx playwright install \"$BROWSERS\" --with-deps\n        fi\n\n    - name: Install Playwright system dependencies (cache hit)\n      if: inputs.browsers != 'none' && steps.cache-playwright.outputs.cache-hit == 'true'\n      shell: bash\n      working-directory: ${{ inputs.working-directory }}\n      env:\n        BROWSERS: ${{ inputs.browsers }}\n      run: |\n        # When cache hits, browsers are restored but system deps may be missing\n        if [ \"$BROWSERS\" == \"all\" ]; then\n          npx playwright install-deps\n        else\n          npx playwright install-deps \"$BROWSERS\"\n        fi\n\n"
  },
  {
    "path": ".github/build/build.Dockerfile",
    "content": "FROM node:22.12.0-alpine\n\n# runtime args and environment variables\nARG DIST=Redis-Insight.tar.gz\nARG NODE_ENV=production\nARG RI_SEGMENT_WRITE_KEY\nENV RI_SEGMENT_WRITE_KEY=${RI_SEGMENT_WRITE_KEY}\nENV NODE_ENV=${NODE_ENV}\nENV RI_SERVE_STATICS=true\nENV RI_BUILD_TYPE='DOCKER_ON_PREMISE'\nENV RI_APP_FOLDER_ABSOLUTE_PATH='/data'\n\n# this resolves CVE-2023-5363\n# TODO: remove this line once we update to base image that doesn't have this vulnerability\nRUN apk update && apk upgrade --no-cache libcrypto3 libssl3\n\n# set workdir\nWORKDIR /usr/src/app\n\n# copy artifacts built in previous stage to this one\nADD $DIST /usr/src/app/redisinsight\nRUN ls -la /usr/src/app/redisinsight\n\n# folder to store local database, plugins, logs and all other files\nRUN mkdir -p /data && chown -R node:node /data\n\n# copy the docker entry point script and make it executable\nCOPY --chown=node:node ./docker-entry.sh ./\nRUN chmod +x docker-entry.sh\n\n# since RI is hard-code to port 5540, expose it from the container\nEXPOSE 5540\n\n# don't run the node process as root\nUSER node\n\n# serve the application 🚀\nENTRYPOINT [\"./docker-entry.sh\", \"node\", \"redisinsight/api/dist/src/main\"]\n"
  },
  {
    "path": ".github/build/build.sh",
    "content": "#!/bin/bash\nset -e\n\n# install deps\nyarn\nyarn --cwd redisinsight/api\n\n# build\n\nyarn build:statics\nyarn build:ui\nyarn --cwd ./redisinsight/api build:prod\n"
  },
  {
    "path": ".github/build/build_modules.sh",
    "content": "#!/bin/bash\nset -e\n\nPLATFORM=${PLATFORM:-'linux'}\nARCH=${ARCH:-'x64'}\nLIBC=${LIBC:-''}\n#FILENAME=\"Redis-Insight-$PLATFORM.$VERSION.$ARCH.zip\"\nFILENAME=\"Redis-Insight-web-$PLATFORM\"\nif [ ! -z $LIBC ]\nthen\n  FILENAME=\"$FILENAME-$LIBC.$ARCH.tar.gz\"\n  export npm_config_target_libc=\"$LIBC\"\nelse\n  FILENAME=\"$FILENAME.$ARCH.tar.gz\"\nfi\n\necho \"Building node modules...\"\necho \"Platform: $PLATFORM\"\necho \"Arch: $ARCH\"\necho \"Libc: $LIBC\"\necho \"npm target libc: $npm_config_target_libc\"\necho \"Filname: $FILENAME\"\n\nrm -rf redisinsight/api/node_modules\n\nnpm_config_arch=\"$ARCH\" \\\nnpm_config_target_arch=\"$ARCH\" \\\nnpm_config_platform=\"$PLATFORM\" \\\nnpm_config_target_platform=\"$PLATFORM\" \\\nyarn --cwd ./redisinsight/api install --production\n\ncp redisinsight/api/.yarnclean.prod redisinsight/api/.yarnclean\nyarn --cwd ./redisinsight/api autoclean --force\n\nrm -rf redisinsight/build.zip\n\ncp LICENSE ./redisinsight\n\ncd redisinsight && tar -czf build.tar.gz \\\n--exclude=\"api/node_modules/**/build/node_gyp_bins/python3\" \\\napi/node_modules \\\napi/dist \\\nui/dist \\\nLICENSE \\\n&& cd ..\n\nmkdir -p release/web\ncp redisinsight/build.tar.gz release/web/\"$FILENAME\"\n\n# Minify build via esbuild\necho \"Start minifing workflow\"\nnpm_config_arch=\"$ARCH\" \\\nnpm_config_target_arch=\"$ARCH\" \\\nnpm_config_platform=\"$PLATFORM\" \\\nnpm_config_target_platform=\"$PLATFORM\" \\\nyarn --cwd ./redisinsight/api install\nyarn --cwd ./redisinsight/api minify:prod\n\n\nPACKAGE_JSON_PATH=\"./redisinsight/api/package.json\"\nAPP_PACKAGE_JSON_PATH=\"./redisinsight/package.json\"\n\n# Extract dependencies from the app package.json\nBINARY_PACKAGES=$(jq -r '.dependencies | keys[]' \"$APP_PACKAGE_JSON_PATH\" | jq -R -s -c 'split(\"\\n\")[:-1]')\n\n# Keep class transformer external for minified builds since it is not bundled\nBINARY_PACKAGES=$(echo \"$BINARY_PACKAGES\" | jq '. + [\"class-transformer\"]')\n\necho \"Binary packages to exclude during minify: $BINARY_PACKAGES\"\n\n# Modify the package.json to keep only binary prod dependencies\n# Additionally remove custom \"postinstall\" script to avoid patch-package error(s)\njq --argjson keep \"$BINARY_PACKAGES\" \\\n  'del(.devDependencies) | .dependencies |= with_entries(select(.key as $k | $keep | index($k))) | del(.scripts.postinstall)' \\\n  \"$PACKAGE_JSON_PATH\" > temp.json && mv temp.json \"$PACKAGE_JSON_PATH\"\n\nnpm_config_arch=\"$ARCH\" \\\nnpm_config_target_arch=\"$ARCH\" \\\nnpm_config_platform=\"$PLATFORM\" \\\nnpm_config_target_platform=\"$PLATFORM\" \\\nyarn --cwd ./redisinsight/api install --production\nyarn --cwd ./redisinsight/api autoclean --force\n\n# Compress minified build\ncd redisinsight && tar -czf build-mini.tar.gz \\\n--exclude=\"api/node_modules/**/build/node_gyp_bins/python3\" \\\napi/node_modules \\\napi/dist-minified \\\nui/dist \\\nLICENSE \\\n&& cd ..\n\nmkdir -p release/web-mini\ncp redisinsight/build-mini.tar.gz release/web-mini/\"$FILENAME\"\n\n# Restore the original package.json and yarn.lock\ngit restore redisinsight/api/yarn.lock redisinsight/api/package.json\n\n"
  },
  {
    "path": ".github/build/release-docker.sh",
    "content": "#!/bin/bash\nset -e\n\nHELP=\"Args:\n-v - Semver (3.2.0)\n-d - Build image repository (Ex: -d redisinsight)\n-r - Target repository (Ex: -r redis/redisinsight)\n\"\n\nwhile getopts \"v:d:r:h:\" opt; do\n  case $opt in\n    v) VERSION=\"$OPTARG\";;\n    d) DEV_REPO=\"$OPTARG\";;\n    r) RELEASE_REPO=\"$OPTARG\";;\n    h) echo \"$HELP\"; exit 0;;\n    ?) echo \"$HELP\" >&2; exit 1 ;;\n  esac\ndone\n\nV_ARR=( ${VERSION//./ } )\nTAGS[0]=$VERSION\nTAGS[1]=\"${V_ARR[0]}.${V_ARR[1]}\"\nTAGS[2]=\"latest\"\n\nDEV_IMAGE_AMD64=$DEV_REPO:amd64\nDEV_IMAGE_ARM64=$DEV_REPO:arm64\nRELEASE_IMAGE_AMD64=$RELEASE_REPO:$VERSION-amd64\nRELEASE_IMAGE_ARM64=$RELEASE_REPO:$VERSION-arm64\n\necho \"\n  TAGS: [${TAGS[0]}, ${TAGS[1]}, ${TAGS[2]}]\n  DEV_REPO: $DEV_REPO\n  RELEASE_REPO: $RELEASE_REPO\n\n  DEV_IMAGE_AMD64: $DEV_IMAGE_AMD64\n  DEV_IMAGE_ARM64: $DEV_IMAGE_ARM64\n\n  RELEASE_IMAGE_AMD64: $RELEASE_IMAGE_AMD64\n  RELEASE_IMAGE_ARM64: $RELEASE_IMAGE_ARM64\n\"\n\n# Load images from tar archives\ndocker rmi $DEV_IMAGE_AMD64 || true\ndocker rmi $DEV_IMAGE_ARM64 || true\ndocker load -i release/docker-linux-alpine.amd64.tar\ndocker load -i release/docker-linux-alpine.arm64.tar\n\necho \"Push AMD64 image\"\ndocker tag $DEV_IMAGE_AMD64 $RELEASE_IMAGE_AMD64\ndocker push $RELEASE_IMAGE_AMD64\n\necho \"Push ARM64 image\"\ndocker tag $DEV_IMAGE_ARM64 $RELEASE_IMAGE_ARM64\ndocker push $RELEASE_IMAGE_ARM64\n\nfor TAG in \"${TAGS[@]}\"; do\n    echo \"Releasing: $RELEASE_REPO:$TAG\"\n    docker manifest rm $RELEASE_REPO:$TAG || true\n    docker manifest create --amend \"$RELEASE_REPO:$TAG\" $RELEASE_IMAGE_AMD64 $RELEASE_IMAGE_ARM64\n    docker manifest push \"$RELEASE_REPO:$TAG\"\ndone\n\necho \"Success\"\n"
  },
  {
    "path": ".github/build/sum_sha256.sh",
    "content": "#!/bin/bash\nset -e\n\nfind ./release -type f -name '*.tar.gz' -execdir sh -c 'sha256sum \"$1\" > \"$1.sha256\"' _ {} \\;\n"
  },
  {
    "path": ".github/codeql/config.yml",
    "content": "paths-ignore:\n  - 'tests/**'\n  - '**/*.test.ts'\n  - '**/*.spec.ts'\n  - '**/*.spec.tsx'\n  - '**/__mocks__/**'\n  - './redisinsight/api/test'\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# GitHub Copilot Instructions for RedisInsight\n\n> **🎯 Start here: Read `AGENTS.md` at the repository root for essential commands, testing instructions, and quick reference**\n\nThis project uses a centralized AI rules structure:\n\n- **`AGENTS.md`** (repository root) - Entry point with commands, testing, and boundaries\n- **`.ai/rules/`** - Detailed development standards organized by topic\n- **`.ai/commands/`** - AI workflow commands and templates\n\n## 📂 Rules Structure\n\n### Core Development Rules\n\n- **Code Quality**: `.ai/rules/code-quality.md`\n\n  - TypeScript best practices\n  - Import organization\n  - SonarJS complexity rules\n\n- **Frontend Development**: `.ai/rules/frontend.md`\n\n  - React 18 patterns and best practices\n  - Redux Toolkit state management\n  - Styled-components (SCSS deprecated)\n  - Component folder structure\n  - Internal UI component wrappers (never import from @redis-ui directly)\n  - Elastic UI deprecation (use Redis UI wrappers)\n\n- **Backend Development**: `.ai/rules/backend.md`\n\n  - NestJS module architecture\n  - Service and controller patterns\n  - DTOs and validation\n  - Error handling\n  - Redis integration patterns\n\n- **Testing Standards**: `.ai/rules/testing.md`\n\n  - Jest and Testing Library patterns\n  - Component testing with renderComponent helper\n  - Faker for test data generation\n  - No fixed timeouts (use waitFor)\n  - Backend testing with NestJS\n  - E2E testing with Playwright\n\n- **Branch Naming**: `.ai/rules/branches.md`\n\n  - Branch naming conventions (type/RI-XXX/title or type/XXX/title)\n\n- **Commit Messages**: `.ai/rules/commits.md`\n\n  - Commit message format (Conventional Commits)\n\n- **Pull Requests**: `.ai/rules/pull-requests.md`\n  - PR process and review guidelines\n  - Pre-commit checklist\n\n### Commands and Workflows\n\n- **PR Plan**: `.ai/commands/pr-plan.md` - Analyze JIRA tickets (RI-XXX) and create detailed implementation plans\n- **Commit Message Generation**: `.ai/commands/commit-message.md` - Generate commit messages following Conventional Commits\n- **PR Review**: `.ai/commands/pull-request-review.md` - Review pull requests and provide feedback\n\n## 🎯 Project Overview\n\n**Tech Stack:**\n\n- Frontend: React 18, TypeScript, Redux Toolkit, styled-components, Vite\n- Backend: NestJS, TypeScript, Node.js\n- Desktop: Electron\n- Testing: Jest, Testing Library, Playwright\n\n**Module Aliases:**\n\n- `uiSrc/*` → `redisinsight/ui/src/*`\n- `apiSrc/*` → `redisinsight/api/src/*`\n- `desktopSrc/*` → `redisinsight/desktop/src/*`\n\n## 📖 Additional Documentation\n\n- **For AI agents**: Start with `AGENTS.md` at repository root\n- **For human developers**: See `.ai/README.md` for setup and overview\n\n---\n\n**Note**: This is a minimal reference file. GitHub Copilot cannot read the referenced files directly, but developers can access the full guidelines. Other AI tools (Cursor, Augment, Windsurf) access these rules via symlinks and can read `AGENTS.md` directly.\n"
  },
  {
    "path": ".github/deps-audit-report.js",
    "content": "const fs = require('fs');\nconst { exec } = require('child_process');\n\nconst FILENAME = process.env.FILENAME;\nconst DEPS = process.env.DEPS || '';\nconst file = `${FILENAME}`;\nconst outputFile = `slack.${FILENAME}`;\n\nfunction generateSlackMessage(summary) {\n  const message = {\n    text:\n      `DEPS AUDIT: *${DEPS}* result (Branch: *${process.env.GITHUB_REF_NAME}*)` +\n      `\\nScanned ${summary.totalDependencies} dependencies` +\n      `\\n<https://github.com/RedisInsight/RedisInsight/actions/runs/${process.env.GITHUB_RUN_ID}|View on Github Actions>`,\n    attachments: [],\n  };\n\n  if (summary.totalVulnerabilities) {\n    if (summary.vulnerabilities.critical) {\n      message.attachments.push({\n        title: 'Critical',\n        color: '#641E16',\n        text: `${summary.vulnerabilities.critical}`,\n      });\n    }\n    if (summary.vulnerabilities.high) {\n      message.attachments.push({\n        title: 'High',\n        color: '#C0392B',\n        text: `${summary.vulnerabilities.high}`,\n      });\n    }\n    if (summary.vulnerabilities.moderate) {\n      message.attachments.push({\n        title: 'Moderate',\n        color: '#F5B041',\n        text: `${summary.vulnerabilities.moderate}`,\n      });\n    }\n    if (summary.vulnerabilities.low) {\n      message.attachments.push({\n        title: 'Low',\n        color: '#F9E79F',\n        text: `${summary.vulnerabilities.low}`,\n      });\n    }\n    if (summary.vulnerabilities.info) {\n      message.attachments.push({\n        title: 'Info',\n        text: `${summary.vulnerabilities.info}`,\n      });\n    }\n  } else {\n    message.attachments.push({\n      title: 'No vulnerabilities found',\n      color: 'good',\n    });\n  }\n\n  return message;\n}\n\nasync function main() {\n  const lastAuditLine = await new Promise((resolve, reject) => {\n    exec(`tail -n 1 ${file}`, (error, stdout, stderr) => {\n      if (error) {\n        return reject(error);\n      }\n      resolve(stdout);\n    });\n  });\n\n  const { data: summary } = JSON.parse(`${lastAuditLine}`);\n  const vulnerabilities = summary?.vulnerabilities || {};\n  summary.totalVulnerabilities = Object.values(vulnerabilities).reduce(\n    (totalVulnerabilities, val) => totalVulnerabilities + val,\n  );\n  fs.writeFileSync(\n    outputFile,\n    JSON.stringify({\n      channel: process.env.SLACK_AUDIT_REPORT_CHANNEL,\n      ...generateSlackMessage(summary),\n    }),\n  );\n}\n\nmain();\n"
  },
  {
    "path": ".github/deps-licenses-report.js",
    "content": "const fs = require('fs');\nconst { join } = require('path');\nconst { last, set } = require('lodash');\nconst { google } = require('googleapis');\nconst { execFile } = require('child_process');\nconst csvParser = require('csv-parser');\nconst { stringify } = require('csv-stringify');\n\nconst licenseFolderName = 'licenses';\nconst spreadsheetId = process.env.SPREADSHEET_ID;\nconst outputFilePath = `./${licenseFolderName}/licenses.csv`;\nconst summaryFilePath = `./${licenseFolderName}/summary.csv`;\nconst allData = [];\nlet csvFiles = [];\n\n// Main function\nasync function main() {\n  const folderPath = './';\n  const packageJsons = findPackageJsonFiles(folderPath); // Find all package.json files in the given folder\n\n  console.log('All package.jsons was found:', packageJsons);\n\n  // Create the folder if it doesn't exist\n  if (!fs.existsSync(licenseFolderName)) {\n    fs.mkdirSync(licenseFolderName);\n  }\n\n  try {\n    await Promise.all(packageJsons.map(runLicenseCheck));\n    console.log('All csv files was generated');\n    await generateSummary();\n    await sendLicensesToGoogleSheet();\n  } catch (error) {\n    console.error('An error occurred:', error);\n    process.exit(1);\n  }\n}\n\nmain();\n\n// Function to find all package.json files in a given folder\nfunction findPackageJsonFiles(folderPath) {\n  const packageJsonPaths = [];\n  const packageJsonName = 'package.json';\n  const excludeFolders = [\n    'dist',\n    'node_modules',\n    'static',\n    'electron',\n    'redisgraph',\n  ];\n\n  // Recursive function to search for package.json files\n  function searchForPackageJson(currentPath) {\n    const files = fs.readdirSync(currentPath);\n\n    for (const file of files) {\n      const filePath = join(currentPath, file);\n      const stats = fs.statSync(filePath);\n\n      if (stats.isDirectory() && !excludeFolders.includes(file)) {\n        searchForPackageJson(filePath);\n      } else if (file === packageJsonName) {\n        packageJsonPaths.push(\n          `./${filePath.slice(0, -packageJsonName.length - 1)}`,\n        );\n      }\n    }\n  }\n\n  searchForPackageJson(folderPath);\n  return packageJsonPaths;\n}\n\n// Function to run license check for a given package.json file\nasync function runLicenseCheck(path) {\n  const name = last(path.split('/')) || 'electron';\n\n  const COMMANDS = [\n    `license-checker --start ${path} --csv --out ./${licenseFolderName}/${name}_prod.csv --production`,\n    `license-checker --start ${path} --csv --out ./${licenseFolderName}/${name}_dev.csv --development`,\n  ];\n\n  return await Promise.all(\n    COMMANDS.map((command) => {\n      const [cmd, ...args] = command.split(' ');\n      return new Promise((resolve, reject) => {\n        execFile(cmd, args, (error, stdout, stderr) => {\n          if (error) {\n            console.error(`Failed command: ${command}, error:`, stderr);\n            reject(error);\n          }\n          resolve();\n        });\n      });\n    }),\n  );\n}\n\nasync function sendLicensesToGoogleSheet() {\n  try {\n    const serviceAccountKey = JSON.parse(\n      fs.readFileSync('./gasKey.json', 'utf-8'),\n    );\n\n    // Set up JWT client\n    const jwtClient = new google.auth.JWT(\n      serviceAccountKey.client_email,\n      null,\n      serviceAccountKey.private_key,\n      ['https://www.googleapis.com/auth/spreadsheets'],\n    );\n\n    const sheets = google.sheets('v4');\n\n    // Read all .csv files in the 'licenses' folder\n    csvFiles.forEach((csvFile) => {\n      // Extract sheet name from file name\n      const sheetName = csvFile.replace('.csv', '').replaceAll('_', ' ');\n\n      const data = [];\n      fs.createReadStream(`./${licenseFolderName}/${csvFile}`)\n        .pipe(csvParser({ headers: false }))\n        .on('data', (row) => {\n          data.push(Object.values(row));\n        })\n        .on('end', async () => {\n          const resource = { values: data };\n\n          try {\n            const response = await sheets.spreadsheets.get({\n              auth: jwtClient,\n              spreadsheetId,\n            });\n\n            const sheet = response.data.sheets.find(\n              (sheet) => sheet.properties.title === sheetName,\n            );\n            if (sheet) {\n              // Clear contents of the sheet starting from cell A2\n              await sheets.spreadsheets.values.clear({\n                auth: jwtClient,\n                spreadsheetId,\n                range: `${sheetName}!A1:Z`, // Assuming Z is the last column\n              });\n            } else {\n              // Create the sheet if it doesn't exist\n              await sheets.spreadsheets.batchUpdate({\n                auth: jwtClient,\n                spreadsheetId,\n                resource: set(\n                  {},\n                  'requests[0].addSheet.properties.title',\n                  sheetName,\n                ),\n              });\n            }\n          } catch (error) {\n            console.error(\n              `Error checking/creating sheet for ${sheetName}:`,\n              error,\n            );\n          }\n\n          try {\n            await sheets.spreadsheets.values.batchUpdate({\n              auth: jwtClient,\n              spreadsheetId,\n              resource: {\n                valueInputOption: 'RAW',\n                data: [\n                  {\n                    range: `${sheetName}!A1`, // Use the sheet name as the range and start from A2\n                    majorDimension: 'ROWS',\n                    values: data,\n                  },\n                ],\n              },\n            });\n\n            console.log(`CSV data has been inserted into ${sheetName} sheet.`);\n          } catch (err) {\n            console.error(`Error inserting data for ${sheetName}:`, err);\n          }\n        });\n    });\n  } catch (error) {\n    console.error('Error loading service account key:', error);\n  }\n}\n\n// Function to read and process each CSV file\nconst processCSVFile = (file) => {\n  return new Promise((resolve, reject) => {\n    const parser = csvParser({ columns: true, trim: true });\n    const input = fs.createReadStream(`./${licenseFolderName}/${file}`);\n\n    parser.on('data', (record) => {\n      allData.push(record);\n    });\n\n    parser.on('end', () => {\n      resolve();\n    });\n\n    parser.on('error', (err) => {\n      reject(err);\n    });\n\n    input.pipe(parser);\n  });\n};\n\n// Process and aggregate license data\nconst processLicenseData = () => {\n  const licenseCountMap = {};\n  for (const record of allData) {\n    const license = record.license;\n    licenseCountMap[license] = (licenseCountMap[license] || 0) + 1;\n  }\n  return licenseCountMap;\n};\n\n// Create summary CSV data\nconst createSummaryData = (licenseCountMap) => {\n  const summaryData = [['License', 'Count']];\n  for (const license in licenseCountMap) {\n    summaryData.push([license, licenseCountMap[license]]);\n  }\n  return summaryData;\n};\n\n// Write summary CSV file\nconst writeSummaryCSV = async (summaryData) => {\n  try {\n    const summaryCsvString = await stringifyPromise(summaryData);\n    fs.writeFileSync(summaryFilePath, summaryCsvString);\n    csvFiles.push(last(summaryFilePath.split('/')));\n    console.log(`Summary CSV saved as ${summaryFilePath}`);\n  } catch (err) {\n    console.error(`Error: ${err}`);\n  }\n};\n\n// Stringify as a promise\nconst stringifyPromise = (data) => {\n  return new Promise((resolve, reject) => {\n    stringify(data, (err, csvString) => {\n      if (err) {\n        reject(err);\n      } else {\n        resolve(csvString);\n      }\n    });\n  });\n};\n\nasync function generateSummary() {\n  csvFiles = fs\n    .readdirSync(licenseFolderName)\n    .filter((file) => file.endsWith('.csv'))\n    .sort();\n\n  for (const file of csvFiles) {\n    try {\n      await processCSVFile(file);\n    } catch (err) {\n      console.error(`Error processing ${file}: ${err}`);\n    }\n  }\n\n  const licenseCountMap = processLicenseData();\n  const summaryData = createSummaryData(licenseCountMap);\n\n  await writeSummaryCSV(summaryData);\n}\n"
  },
  {
    "path": ".github/e2e/test.app-image.sh",
    "content": "#!/bin/bash\nset -e\n\npkill -f Redis*  || true\nrm -f apppath\n\nyarn --cwd tests/e2e install\n\n# mount app resources\nchmod +x ./release/*.AppImage\n./release/*.AppImage --appimage-mount >> apppath &\n\n# wait briefly to allow the appimage mount command to output to the file\nsleep 2\n\n# log the content of apppath\necho \"Content of apppath file:\"\ncat apppath\n\n# create folder before tests run to prevent permissions issue\nmkdir -p tests/e2e/remote\nmkdir -p tests/e2e/rdi\n\n# run rte\ndocker compose -f tests/e2e/rte.docker-compose.yml build\ndocker compose -f tests/e2e/rte.docker-compose.yml up --force-recreate -d -V\n./tests/e2e/wait-for-redis.sh localhost 12000 && \\\n\n# run tests add TEST_DEBUG=1 to debug framework execution\nTEST_DEBUG=0\n[ \"$TEST_DEBUG\" = \"1\" ] && export DEBUG=testcafe:*\nexport COMMON_URL=$(tail -n 1 apppath)/resources/app.asar/dist/renderer/index.html\nexport ELECTRON_PATH=$(tail -n 1 apppath)/redisinsight\nexport RI_SOCKETS_CORS=true\nyarn --cwd tests/e2e dotenv -e .desktop.env yarn --cwd tests/e2e test:desktop:ci\n"
  },
  {
    "path": ".github/e2e/test.app-image.sso.sh",
    "content": "#!/bin/bash\nset -e\n\nyarn --cwd tests/e2e install\n\n# Create the ri-test directory if it doesn't exist\nmkdir -p ri-test\n\n# Extract the AppImage\nchmod +x ./release/*.AppImage\n./release/*.AppImage --appimage-extract\n\n# Move contents of squashfs-root to ri-test and remove squashfs-root folder\nmv squashfs-root/* ri-test/\nrm -rf squashfs-root\n\n# Export custom XDG_DATA_DIRS with ri-test\nexport XDG_DATA_DIRS=\"$(pwd)/ri-test:$XDG_DATA_DIRS\"\n\n# create folder before tests run to prevent permissions issue\nmkdir -p tests/e2e/remote\nmkdir -p tests/e2e/rdi\n\n# Create a custom .desktop file for RedisInsight\ncat > ri-test/redisinsight.desktop <<EOL\n[Desktop Entry]\nVersion=1.0\nName=RedisInsight\nExec=$(pwd)/ri-test/redisinsight %u\nIcon=$(pwd)/ri-test/resources/app.asar/img/icon.png\nType=Application\nTerminal=false\nMimeType=x-scheme-handler/redisinsight;\nEOL\n\n# Copy the .desktop file to the local applications directory\ncp ri-test/redisinsight.desktop \"$HOME/.local/share/applications\"\n\n# Update the desktop database with custom directory\nupdate-desktop-database \"$(pwd)/ri-test/\"\n\n# Register the RedisInsight deeplink protocol\nxdg-mime default redisinsight.desktop x-scheme-handler/redisinsight\n\n# Run rte\ndocker compose -f tests/e2e/rte.docker-compose.yml build\ndocker compose -f tests/e2e/rte.docker-compose.yml up --force-recreate -d -V\n./tests/e2e/wait-for-redis.sh localhost 12000 && \\\n\n# Run tests\nCOMMON_URL=$(pwd)/ri-test/resources/app.asar/dist/renderer/index.html \\\nELECTRON_PATH=$(pwd)/ri-test/redisinsight \\\nRI_SOCKETS_CORS=true \\\nyarn --cwd tests/e2e dotenv -e .desktop.env yarn --cwd tests/e2e test:desktop:ci\n"
  },
  {
    "path": ".github/e2e-results.js",
    "content": "const fs = require('fs');\n\nlet parallelNodeInfo = '';\n// const totalNodes = parseInt(process.env.NODE_TOTAL, 10);\nconst totalNodes = 4;\nif (totalNodes > 1) {\n  parallelNodeInfo = ` (node: ${parseInt(process.env.NODE_INDEX, 10) + 1}/${totalNodes})`;\n}\n\nconst file = 'tests/e2e/results/e2e.results.json';\nconst appBuildType = process.env.APP_BUILD_TYPE || 'Web';\nconst results = {\n  message: {\n    text:\n      `*E2ETest - ${appBuildType}${parallelNodeInfo}* (Branch: *${process.env.GITHUB_REF_NAME}*)` +\n      `\\n<https://github.com/RedisInsight/RedisInsight/actions/runs/${process.env.GITHUB_RUN_ID}|View on Github Actions>`,\n    attachments: [],\n  },\n};\n\nconst result = JSON.parse(fs.readFileSync(file, 'utf-8'));\nconst testRunResult = {\n  color: '#36a64f',\n  title: `Started at: *${result.startTime}`,\n  text: `Executed ${result.total} in ${(new Date(result.endTime) - new Date(result.startTime)) / 1000}s`,\n  fields: [\n    {\n      title: 'Passed',\n      value: result.passed,\n      short: true,\n    },\n    {\n      title: 'Skipped',\n      value: result.skipped,\n      short: true,\n    },\n  ],\n};\nconst failed = result.total - result.passed;\nif (failed) {\n  results.passed = false;\n  testRunResult.color = '#cc0000';\n  testRunResult.fields.push({\n    title: 'Failed',\n    value: failed,\n    short: true,\n  });\n}\n\nresults.message.attachments.push(testRunResult);\n\nif (results.passed === false) {\n  results.message.text = '<!here> ' + results.message.text;\n}\n\nfs.writeFileSync(\n  'e2e.report.json',\n  JSON.stringify({\n    channel: process.env.SLACK_TEST_REPORT_CHANNEL,\n    ...results.message,\n  }),\n);\n"
  },
  {
    "path": ".github/generate-build-summary.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst { appendFile } = fs.promises;\n\nconst {\n  AWS_DEFAULT_REGION,\n  AWS_BUCKET_NAME_TEST,\n  SUB_PATH,\n  GITHUB_STEP_SUMMARY,\n} = process.env;\n\nconst Categories = {\n  Linux: 'Linux Builds',\n  MacOS: 'MacOS Builds',\n  Windows: 'Windows Builds',\n  Docker: 'Docker Builds',\n};\n\nconst directoryPath = path.join(process.cwd(), 'release');\nconst dockerDirectoryPath = path.join(directoryPath, 'docker');\n\nasync function generateBuildSummary() {\n  try {\n    // Read the contents of the release directory and Docker subdirectory\n    const files = fs.readdirSync(directoryPath);\n    const dockerFiles = fs.existsSync(dockerDirectoryPath)\n      ? fs.readdirSync(dockerDirectoryPath).map((file) => `docker/${file}`)\n      : [];\n\n    // Combine all files into a single array\n    const allFiles = [...files, ...dockerFiles];\n\n    // Mapping file names to Markdown links and categories\n    const fileMappings = {\n      'Redis-Insight-mac-arm64.dmg': {\n        name: 'Redis Insight for Mac (arm64 DMG)',\n        category: Categories.MacOS,\n      },\n      'Redis-Insight-mac-x64.dmg': {\n        name: 'Redis Insight for Mac (x64 DMG)',\n        category: Categories.MacOS,\n      },\n      'Redis-Insight-win-installer.exe': {\n        name: 'Redis Insight Windows Installer (exe)',\n        category: Categories.Windows,\n      },\n      'Redis-Insight-linux-x86_64.AppImage': {\n        name: 'Redis Insight for Linux (AppImage)',\n        category: Categories.Linux,\n      },\n      'Redis-Insight-linux-amd64.deb': {\n        name: 'Redis Insight for Linux (deb)',\n        category: Categories.Linux,\n      },\n      'Redis-Insight-linux-amd64.snap': {\n        name: 'Redis Insight for Linux (snap)',\n        category: Categories.Linux,\n      },\n      'Redis-Insight-linux-x86_64.rpm': {\n        name: 'Redis Insight for Linux (rpm)',\n        category: Categories.Linux,\n      },\n      'docker/docker-linux-alpine.amd64.tar': {\n        name: 'Redis Insight Docker Image (amd64)',\n        category: Categories.Docker,\n      },\n      'docker/docker-linux-alpine.arm64.tar': {\n        name: 'Redis Insight Docker Image (arm64)',\n        category: Categories.Docker,\n      },\n    };\n\n    const categories = {};\n\n    // Populate categories with existing files\n    allFiles.forEach((file) => {\n      const mapping = fileMappings[file];\n      if (mapping) {\n        if (!categories[mapping.category]) {\n          categories[mapping.category] = [];\n        }\n        const s3path = `https://s3.${AWS_DEFAULT_REGION}.amazonaws.com/${AWS_BUCKET_NAME_TEST}`;\n        const href = `${s3path}/public/${SUB_PATH}/${file}`;\n\n        categories[mapping.category].push(`- [${mapping.name}](${href})`);\n      }\n    });\n\n    // Prepare the summary markdown document\n    const markdownLines = ['## Builds:', ''];\n\n    // Append categories to markdown if they have entries\n    Object.keys(categories).forEach((category) => {\n      if (categories[category].length) {\n        markdownLines.push(`### ${category}`, '', ...categories[category], '');\n      }\n    });\n\n    const data = markdownLines.join('\\n');\n    const summaryFilePath = GITHUB_STEP_SUMMARY;\n\n    await appendFile(summaryFilePath, data, { encoding: 'utf8' });\n\n    console.log('Build summary generated successfully.');\n  } catch (error) {\n    console.error(error);\n  }\n}\n\ngenerateBuildSummary();\n"
  },
  {
    "path": ".github/generate-checksums-summary.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst { appendFile } = fs.promises;\n\nconst { GITHUB_STEP_SUMMARY } = process.env;\n\nconst directoryPath = path.join(process.cwd(), 'release');\n\n// Mapping of file names to friendly package names (matching release notes format)\nconst fileMappings = {\n  'Redis-Insight-win-installer.exe': 'Windows',\n  'Redis-Insight-linux-x86_64.AppImage': 'Linux AppImage',\n  'Redis-Insight-linux-amd64.deb': 'Linux Debian',\n  'Redis-Insight-linux-x86_64.rpm': 'Linux RPM',\n  'Redis-Insight-mac-x64.dmg': 'MacOS Intel',\n  'Redis-Insight-mac-arm64.dmg': 'MacOS Apple silicon',\n};\n\n// YAML files generated by electron-builder\nconst yamlFiles = ['latest.yml', 'latest-linux.yml', 'latest-mac.yml'];\n\n/**\n * Simple YAML parser for electron-builder format\n * Extracts file entries with their sha512 values\n */\nfunction parseYamlFile(content) {\n  const files = [];\n  const lines = content.split('\\n');\n\n  let currentFile = null;\n\n  for (const line of lines) {\n    // Match file URL entries like \"  - url: Redis-Insight-win-installer.exe\"\n    const urlMatch = line.match(/^\\s+-?\\s*url:\\s*(.+)$/);\n    if (urlMatch) {\n      currentFile = { url: urlMatch[1].trim() };\n      continue;\n    }\n\n    // Match sha512 entries like \"    sha512: base64hash==\"\n    const sha512Match = line.match(/^\\s+sha512:\\s*(.+)$/);\n    if (sha512Match && currentFile) {\n      currentFile.sha512 = sha512Match[1].trim();\n      files.push(currentFile);\n      currentFile = null;\n    }\n  }\n\n  return files;\n}\n\nasync function generateChecksumsSummary() {\n  try {\n    const checksums = [];\n\n    // Parse each YAML file\n    for (const yamlFile of yamlFiles) {\n      const filePath = path.join(directoryPath, yamlFile);\n\n      if (!fs.existsSync(filePath)) {\n        console.log(`File not found: ${yamlFile}, skipping...`);\n        continue;\n      }\n\n      const content = fs.readFileSync(filePath, 'utf8');\n      const files = parseYamlFile(content);\n\n      for (const file of files) {\n        const packageName = fileMappings[file.url];\n        if (packageName && file.sha512) {\n          checksums.push({\n            package: packageName,\n            sha512: file.sha512,\n          });\n        }\n      }\n    }\n\n    if (checksums.length === 0) {\n      console.log('No checksums found in YAML files.');\n      return;\n    }\n\n    // Sort checksums in a consistent order\n    const order = Object.values(fileMappings);\n    checksums.sort(\n      (a, b) => order.indexOf(a.package) - order.indexOf(b.package),\n    );\n\n    // Generate Markdown table\n    const markdownLines = [\n      '',\n      '## SHA-512 Checksums',\n      '',\n      '| Package | SHA-512 |',\n      '|---------|---------|',\n      ...checksums.map((c) => `| ${c.package} | ${c.sha512} |`),\n      '',\n    ];\n\n    const data = markdownLines.join('\\n');\n\n    if (GITHUB_STEP_SUMMARY) {\n      await appendFile(GITHUB_STEP_SUMMARY, data, { encoding: 'utf8' });\n      console.log('Checksums summary generated successfully.');\n    } else {\n      // For local testing, print to console\n      console.log(data);\n    }\n  } catch (error) {\n    console.error('Error generating checksums summary:', error.message);\n  }\n}\n\ngenerateChecksumsSummary();\n"
  },
  {
    "path": ".github/itest-results.js",
    "content": "const fs = require('fs');\n\nconst file = 'redisinsight/api/test/test-runs/coverage/test-run-result.json';\n\nconst results = {\n  message: {\n    text:\n      `*ITest - ${process.env.ITEST_NAME}* (Branch: *${process.env.GITHUB_REF_NAME}*)` +\n      `\\n<https://github.com/RedisInsight/RedisInsight/actions/runs/${process.env.GITHUB_RUN_ID}|View on Github Actions>`,\n    attachments: [],\n  },\n};\n\nconst result = JSON.parse(fs.readFileSync(file, 'utf-8'));\nconst testRunResult = {\n  color: '#36a64f',\n  title: `Started at: ${result.stats.start}`,\n  text: `Executed ${result.stats.tests} in ${result.stats.duration / 1000}s`,\n  fields: [\n    {\n      title: 'Passed',\n      value: result.stats.passes,\n      short: true,\n    },\n    {\n      title: 'Skipped',\n      value: result.stats.pending,\n      short: true,\n    },\n  ],\n};\n\nif (result.stats.failures) {\n  results.passed = false;\n  testRunResult.color = '#cc0000';\n  testRunResult.fields.push({\n    title: 'Failed',\n    value: result.stats.failures,\n    short: true,\n  });\n}\n\nresults.message.attachments.push(testRunResult);\n\nif (results.passed === false) {\n  results.message.text = '<!here> ' + results.message.text;\n}\n\nfs.writeFileSync(\n  'itests.report.json',\n  JSON.stringify({\n    channel: process.env.SLACK_TEST_REPORT_CHANNEL,\n    ...results.message,\n  }),\n);\n"
  },
  {
    "path": ".github/lint-report.js",
    "content": "const fs = require('fs');\n\nconst FILENAME = process.env.FILENAME || 'lint.audit.json';\nconst WORKDIR = process.env.WORKDIR || '.';\nconst TARGET = process.env.TARGET || '';\nconst file = `${WORKDIR}/${FILENAME}`;\nconst outputFile = `${WORKDIR}/slack.${FILENAME}`;\n\nfunction generateSlackMessage(summary) {\n  const message = {\n    text:\n      `CODE SCAN: *${TARGET}* result (Branch: *${process.env.GITHUB_REF_NAME}*)` +\n      `\\n<https://github.com/RedisInsight/RedisInsight/actions/runs/${process.env.GITHUB_RUN_ID}|View on Github Actions>`,\n    attachments: [],\n  };\n\n  if (summary.total) {\n    if (summary.errors) {\n      message.attachments.push({\n        title: 'Errors',\n        color: '#C0392B',\n        text: `${summary.errors}`,\n      });\n    }\n    if (summary.warnings) {\n      message.attachments.push({\n        title: 'Warnings',\n        color: '#F5B041',\n        text: `${summary.warnings}`,\n      });\n    }\n  } else {\n    message.attachments.push({\n      title: 'No issues found',\n      color: 'good',\n    });\n  }\n\n  return message;\n}\n\nasync function main() {\n  const summary = {\n    errors: 0,\n    warnings: 0,\n  };\n  const scanResult = JSON.parse(fs.readFileSync(file));\n  scanResult.forEach((fileResult) => {\n    summary.errors += fileResult.errorCount;\n    summary.warnings += fileResult.warningCount;\n  });\n\n  summary.total = summary.errors + summary.warnings;\n\n  fs.writeFileSync(\n    outputFile,\n    JSON.stringify({\n      channel: process.env.SLACK_AUDIT_REPORT_CHANNEL,\n      ...generateSlackMessage(summary),\n    }),\n  );\n}\n\nmain();\n"
  },
  {
    "path": ".github/redisstack/app-image.repack.sh",
    "content": "#!/bin/bash\nset -e\n\nARCH=${ARCH:-x86_64}\nWORKING_DIRECTORY=$(pwd)\nSOURCE_APP=${SOURCE_APP:-\"Redis-Insight-linux-$ARCH.AppImage\"}\nRI_APP_FOLDER_NAME=\"Redis-Insight-linux\"\nTAR_NAME=\"Redis-Insight-app-linux.$ARCH.tar.gz\"\nTMP_FOLDER=\"/tmp/Redis-Insight-app-$ARCH\"\n\nrm -rf \"$TMP_FOLDER\"\n\nmkdir -p \"$WORKING_DIRECTORY/release/redisstack\"\nmkdir -p \"$TMP_FOLDER\"\n\ncp \"./release/$SOURCE_APP\" \"$TMP_FOLDER\"\ncd \"$TMP_FOLDER\" || exit 1\n\n./\"$SOURCE_APP\" --appimage-extract\nmv squashfs-root \"$RI_APP_FOLDER_NAME\"\n\ntar -czvf \"$TAR_NAME\" \"$RI_APP_FOLDER_NAME\"\n\ncp \"$TAR_NAME\" \"$WORKING_DIRECTORY/release/redisstack/\"\ncd \"$WORKING_DIRECTORY\" || exit 1\n"
  },
  {
    "path": ".github/redisstack/dmg.repack.sh",
    "content": "#!/bin/bash\nset -e\n\nARCH=${ARCH:-x64}\nWORKING_DIRECTORY=$(pwd)\nTAR_NAME=\"Redis-Insight-app-darwin.$ARCH.tar.gz\"\nRI_APP_FOLDER_NAME=\"Redis Insight.app\"\nTMP_FOLDER=\"/tmp/$RI_APP_FOLDER_NAME\"\n\nrm -rf \"$TMP_FOLDER\"\n\nmkdir -p \"$WORKING_DIRECTORY/release/redisstack\"\nmkdir -p \"$TMP_FOLDER\"\n\nhdiutil attach \"./release/Redis-Insight-mac-$ARCH.dmg\"\nrsync -av /Volumes/Redis*/Redis\\ Insight.app \"/tmp\"\ncd \"/tmp\" || exit 1\ntar -czvf \"$TAR_NAME\" \"$RI_APP_FOLDER_NAME\"\ncp \"$TAR_NAME\" \"$WORKING_DIRECTORY/release/redisstack/\"\ncd \"$WORKING_DIRECTORY\" || exit 1\nhdiutil unmount /Volumes/Redis*/\n"
  },
  {
    "path": ".github/virustotal-report.js",
    "content": "const fs = require('fs');\n\nconst fileName = process.env.FILE_NAME;\nconst buildName = process.env.BUILD_NAME;\nconst failed = process.env.FAILED === 'true';\n\nconst results = {\n  message: {\n    text:\n      `*Virustotal checks* (Branch: *${process.env.GITHUB_REF_NAME}*)` +\n      `\\n<https://github.com/RedisInsight/RedisInsight/actions/runs/${process.env.GITHUB_RUN_ID}|View on Github Actions>`,\n    attachments: [],\n  },\n};\n\nconst result = {\n  color: '#36a64f',\n  title: `Finished at: ${new Date().toISOString()}`,\n  text: `All builds were passed via virustotal checks`,\n  fields: [],\n};\n\nif (failed) {\n  results.passed = false;\n  result.color = '#cc0000';\n  result.text = 'Build had failed virustotal checks';\n  result.fields.push({\n    title: 'Failed build',\n    value: buildName,\n    short: true,\n  });\n}\n\nresults.message.attachments.push(result);\n\nif (failed === true) {\n  results.message.text = '<!here> ' + results.message.text;\n}\n\nfs.writeFileSync(\n  fileName,\n  JSON.stringify({\n    channel: process.env.SLACK_VIRUSTOTAL_REPORT_CHANNEL,\n    ...results.message,\n  }),\n);\n"
  },
  {
    "path": ".github/workflows/approval-dedupe.yml",
    "content": "# Reusable workflow: Approval-based deduplication by PR head SHA\n#\n# Purpose\n# - Gate heavy CI jobs to run only on Approved PR reviews (or manual dispatch in the caller),\n# - De-duplicate runs for the same PR head SHA:\n#   - mode=run: no prior run found → caller should run heavy jobs\n#   - mode=wait: a prior run is queued/in_progress → this workflow waits and mirrors its result\n#   - mode=mirror_completed: a prior run already completed → mirror its conclusion immediately\n#   - mode=noop: not an approved review (and not allowed workflow_dispatch) → do nothing\n#\n# How to use (in a caller workflow):\n# - Ensure the caller triggers on: pull_request_review: [submitted] and/or workflow_dispatch\n# - Add a job that calls this workflow:\n#     jobs:\n#       approval-dedupe:\n#         uses: ./.github/workflows/approval-dedupe.yml\n#         secrets: inherit\n#         with:\n#           workflow_filename: tests-e2e.yml   # set to the caller's filename\n#           require_approval: true             # only run on Approved reviews\n#           allow_workflow_dispatch: true      # allow manual runs to go to mode=run\n# - Gate heavy jobs in the caller with:\n#     needs: approval-dedupe\n#     if: needs.approval-dedupe.outputs.mode == 'run'\n# - Recommended in caller: set concurrency to key by head SHA and cancel-in-progress: false\n#\n\nname: Approval Deduplicate\n\non:\n  workflow_call:\n    inputs:\n      workflow_filename:\n        description: The workflow filename to inspect for prior runs (e.g., tests-e2e.yml)\n        required: true\n        type: string\n      require_approval:\n        description: Require an Approved review to proceed (ignored for workflow_dispatch when allowed)\n        required: false\n        type: boolean\n        default: true\n      allow_workflow_dispatch:\n        description: Allow workflow_dispatch to always run heavy jobs\n        required: false\n        type: boolean\n        default: true\n      head_sha:\n        description: Optional explicit head SHA to use for de-duplication. Defaults to PR head SHA\n        required: false\n        type: string\n    outputs:\n      mode:\n        description: One of run | wait | mirror_completed | noop\n        value: ${{ jobs.gate.outputs.mode }}\n      target_run_id:\n        description: If mode == wait, the prior run id to wait on\n        value: ${{ jobs.gate.outputs.target_run_id }}\n      prev_conclusion:\n        description: If mode == mirror_completed, the conclusion to mirror\n        value: ${{ jobs.gate.outputs.prev_conclusion }}\n      prev_run_id:\n        description: If mode == mirror_completed, the prior run id being mirrored\n        value: ${{ jobs.gate.outputs.prev_run_id }}\n\npermissions:\n  actions: read\n  contents: read\n\njobs:\n  gate:\n    name: Gate and determine mode\n    runs-on: ubuntu-latest\n    outputs:\n      mode: ${{ steps.check.outputs.mode }}\n      target_run_id: ${{ steps.check.outputs.target_run_id }}\n      prev_conclusion: ${{ steps.check.outputs.prev_conclusion }}\n      prev_run_id: ${{ steps.check.outputs.prev_run_id }}\n    steps:\n      - name: Determine mode\n        id: check\n        uses: actions/github-script@v7\n        env:\n          WORKFLOW_FILENAME: ${{ inputs.workflow_filename }}\n          REQUIRE_APPROVAL: ${{ inputs.require_approval }}\n          ALLOW_WORKFLOW_DISPATCH: ${{ inputs.allow_workflow_dispatch }}\n          HEAD_SHA: ${{ inputs.head_sha }}\n        with:\n          script: |\n            const workflowFilename = process.env.WORKFLOW_FILENAME\n            const requireApproval = process.env.REQUIRE_APPROVAL === 'true'\n            const allowDispatch = process.env.ALLOW_WORKFLOW_DISPATCH === 'true'\n            const explicitSha = (process.env.HEAD_SHA || '').trim()\n\n            if (context.eventName === 'workflow_dispatch' && allowDispatch) {\n              core.setOutput('mode', 'run')\n              return\n            }\n\n            if (requireApproval) {\n              const approved = (context.eventName === 'pull_request_review') &&\n                               (context.payload.review?.state?.toLowerCase() === 'approved')\n              if (!approved) {\n                core.setOutput('mode', 'noop')\n                return\n              }\n            }\n\n            const sha = explicitSha || context.payload.pull_request?.head?.sha\n            if (!sha) {\n              core.setOutput('mode', 'run')\n              return\n            }\n\n            const resp = await github.rest.actions.listWorkflowRuns({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              workflow_id: workflowFilename,\n              head_sha: sha,\n              per_page: 50,\n            })\n\n            const currentRunId = context.runId\n            const runs = (resp.data.workflow_runs || []).filter(r => r.id !== currentRunId)\n            const active = runs.find(r => ['queued', 'in_progress'].includes(r.status))\n            if (active) {\n              core.setOutput('mode', 'wait')\n              core.setOutput('target_run_id', String(active.id))\n              core.setOutput('prev_run_id', String(active.id))\n              return\n            }\n\n            const completed = runs\n              .filter(r => r.status === 'completed')\n              .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0]\n            if (completed) {\n              core.setOutput('mode', 'mirror_completed')\n              core.setOutput('prev_conclusion', completed.conclusion || '')\n              core.setOutput('prev_run_id', String(completed.id))\n              return\n            }\n\n            core.setOutput('mode', 'run')\n\n  wait-previous:\n    name: Wait for previous run\n    needs: gate\n    if: needs.gate.outputs.mode == 'wait'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Wait and mirror\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const MAX_WAIT_MS = 60 * 60 * 1000 // 1 hour\n            const POLL_INTERVAL_MS = 15 * 1000 // 15 seconds\n            const runId = Number('${{ needs.gate.outputs.target_run_id }}')\n            const poll = async () => {\n              const { data } = await github.rest.actions.getWorkflowRun({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                run_id: runId,\n              })\n              return data\n            }\n            let data = await poll()\n            const started = Date.now()\n            while (data.status !== 'completed') {\n              if (Date.now() - started > MAX_WAIT_MS) {\n                core.setFailed('Timeout waiting for prior run')\n                return\n              }\n              await new Promise(r => setTimeout(r, POLL_INTERVAL_MS))\n              data = await poll()\n            }\n            const conclusion = data.conclusion || 'failure'\n            if (conclusion !== 'success') {\n              core.setFailed(`Mirroring prior run conclusion: ${conclusion}`)\n            }\n\n  mirror-completed:\n    name: Mirror completed run\n    needs: gate\n    if: needs.gate.outputs.mode == 'mirror_completed'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Mirror result\n        run: |\n          echo \"Previous conclusion: ${{ needs.gate.outputs.prev_conclusion }}\"\n          echo \"Mirroring workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ needs.gate.outputs.prev_run_id }}\"\n          if [ \"${{ needs.gate.outputs.prev_conclusion }}\" != \"success\" ]; then\n            echo \"Mirroring failure\"\n            exit 1\n          fi\n          echo \"Mirroring success\"\n"
  },
  {
    "path": ".github/workflows/aws-upload-dev.yml",
    "content": "name: AWS Development\n\non:\n  workflow_call:\n    inputs:\n      pre-release:\n        type: boolean\n        default: false\n\nenv:\n  AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n  AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n  AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n\njobs:\n  s3:\n    name: Upload to s3\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get current date\n        id: date\n        uses: ./.github/actions/get-current-date\n\n      - name: Download builds\n        uses: actions/download-artifact@v4\n        with:\n          pattern: '*-builds'\n          path: release\n          merge-multiple: true\n\n      - run: ls -R ./release\n\n      - name: Upload builds to s3 bucket dev sub folder\n        if: ${{ !inputs.pre-release }}\n        run: |\n          SUB_PATH=\"dev-builds/${{ steps.date.outputs.date }}/${{ github.run_id }}\"\n          echo \"SUB_PATH=${SUB_PATH}\" >> $GITHUB_ENV\n\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/${SUB_PATH} --recursive\n\n      - name: Upload builds to s3 bucket pre-releasea sub folder\n        if: inputs.pre-release\n        run: |\n          APP_VERSION=$(jq -r '.version' redisinsight/package.json)\n          SUB_PATH=\"pre-release/${APP_VERSION}\"\n\n          echo \"SUB_PATH=${SUB_PATH}\" >> $GITHUB_ENV\n\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/upgrades --recursive\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/pre-release/${APP_VERSION} --recursive\n\n      - name: Download vendor plugins\n        uses: actions/download-artifact@v4\n        id: download-vendor\n        continue-on-error: true\n        with:\n          name: 'vendor-plugins'\n          path: vendor\n\n      - name: Upload vendor plugins to s3 bucket\n        if: steps.download-vendor.outcome == 'success'\n        run: |\n          aws s3 cp vendor/ s3://${AWS_BUCKET_NAME_TEST}/public/plugins/static/ --recursive\n\n      - name: Generate job summary\n        run: |\n          node ./.github/generate-build-summary.js\n\n      - name: Generate checksums summary\n        continue-on-error: true\n        run: node ./.github/generate-checksums-summary.js\n"
  },
  {
    "path": ".github/workflows/aws-upload-enterprise.yml",
    "content": "name: AWS Upload Enterprise\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: Environment for build\n        required: false\n        default: 'development'\n        type: string\n\nenv:\n  AWS_BUCKET_NAME_PROD: ${{ vars.AWS_BUCKET_NAME_ENTERPRISE }}\n  AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_ENTERPRISE_TEST }}\n  AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n  AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n\njobs:\n  s3:\n    name: Upload to s3\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get current date\n        id: date\n        uses: ./.github/actions/get-current-date\n\n      - name: Download builds\n        uses: actions/download-artifact@v4\n        with:\n          pattern: '*-builds'\n          path: release\n          merge-multiple: true\n\n      - run: ls -R ./release\n\n      - name: Renaming builds\n        run: |\n          APP_VERSION=$(jq -r '.version' redisinsight/package.json)\n          VERSION=\"${APP_VERSION//./-}\"\n          TARGET_DIR=./release\n          PREFIX=\"Redis-Insight\"\n          NEW_PREFIX=\"Redis-Insight-Enterprise-$VERSION\"\n\n          echo \"Renaming artifacts. New prefix: $NEW_PREFIX\"\n\n          if [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n            SED_INPLACE=\"sed -i.bak\"\n          else\n            SED_INPLACE=\"sed -i\"\n          fi\n\n          # Step 1: Rename files in target dir\n          for FILE in \"$TARGET_DIR\"/\"$PREFIX\"*; do\n            if [ -f \"$FILE\" ]; then\n              BASENAME=\"$(basename \"$FILE\")\"\n              SUFFIX=\"${BASENAME#\"$PREFIX\"-}\"\n              NEW_NAME=\"${NEW_PREFIX}-${SUFFIX}\"\n              mv \"$FILE\" \"$TARGET_DIR/$NEW_NAME\"\n              echo \"Renamed: $BASENAME -> $NEW_NAME\"\n            fi\n          done\n\n          # Step 2: Replace old filenames in all .yml files\n          for YML_FILE in \"$TARGET_DIR\"/*.yml; do\n            echo \"Scanning: $YML_FILE\"\n\n            grep -oE 'Redis-Insight[^[:space:]]+' \"$YML_FILE\" | sort -u | while read -r OLD_NAME; do\n              if [[ \"$OLD_NAME\" == \"$PREFIX\"-* ]]; then\n                SUFFIX=\"${OLD_NAME#\"$PREFIX\"-}\"\n                NEW_NAME=\"${NEW_PREFIX}-${SUFFIX}\"\n\n                # Escape for sed\n                ESCAPED_OLD=$(printf '%s\\n' \"$OLD_NAME\" | sed -e 's/[\\/&]/\\\\&/g')\n                ESCAPED_NEW=$(printf '%s\\n' \"$NEW_NAME\" | sed -e 's/[\\/&]/\\\\&/g')\n\n                if $SED_INPLACE \"s/$ESCAPED_OLD/$ESCAPED_NEW/g\" \"$YML_FILE\"; then\n                  echo \"  ✔ Updated: $OLD_NAME -> $NEW_NAME\"\n                else\n                  echo \"  ✘ ERROR updating: $OLD_NAME -> $NEW_NAME\"\n                fi\n              fi\n            done\n          done\n\n      - name: Upload builds to s3 bucket dev sub folder\n        if: ${{ inputs.environment != 'production' }}\n        run: |\n          SUB_PATH=\"dev-builds/${{ steps.date.outputs.date }}/${{ github.run_id }}\"\n          echo \"SUB_PATH=${SUB_PATH}\" >> $GITHUB_ENV\n\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/${SUB_PATH} --recursive\n\n      - name: Upload builds to s3 bucket pre-release sub folder\n        if: ${{ inputs.environment == 'production' }}\n        run: |\n          APP_VERSION=$(jq -r '.version' redisinsight/package.json)\n\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME_PROD}/releases/${APP_VERSION} --recursive\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME_PROD}/latest --recursive\n\n      - name: Generate job summary\n        run: |\n          node ./.github/generate-build-summary.js\n"
  },
  {
    "path": ".github/workflows/aws-upload-prod.yml",
    "content": "name: AWS production\n\non:\n  workflow_call:\n\nenv:\n  AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}\n  AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n  AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n\njobs:\n  release-private:\n    name: Release s3 private\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Merge builds by pattern\n        id: merge-builds\n        uses: actions/upload-artifact/merge@v4\n        with:\n          name: 'all-builds'\n          pattern: '*-builds'\n          delete-merged: true\n\n      - name: Download builds\n        uses: actions/download-artifact@v4\n        with:\n          name: 'all-builds'\n          path: release\n\n      - run: ls -R ./release\n\n      - name: Publish private\n        run: |\n          chmod +x .github/build/sum_sha256.sh\n          .github/build/sum_sha256.sh\n          applicationVersion=$(jq -r '.version' redisinsight/package.json)\n\n          aws s3 cp release/ s3://${AWS_BUCKET_NAME}/private/${applicationVersion} --recursive\n\n      - name: Generate checksums summary\n        continue-on-error: true\n        run: node ./.github/generate-checksums-summary.js\n\n  release-public:\n    name: Release s3 public\n    runs-on: ubuntu-latest\n    needs: 'release-private'\n    environment: 'production-approve'\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Init variables\n        run: |\n          latestYmlFileName=\"latest.yml\"\n          downloadLatestFolderPath=\"public/latest-v3\"\n          upgradeLatestFolderPath=\"public/upgrades-v3\"\n          releasesFolderPath=\"public/releases\"\n          appName=$(jq -r '.productName' electron-builder.json)\n          appVersion=$(jq -r '.version' redisinsight/package.json)\n\n          echo \"downloadLatestFolderPath=${downloadLatestFolderPath}\" >> $GITHUB_ENV\n          echo \"upgradeLatestFolderPath=${upgradeLatestFolderPath}\" >> $GITHUB_ENV\n          echo \"releasesFolderPath=${releasesFolderPath}\" >> $GITHUB_ENV\n          echo \"applicationName=${appName}\" >> $GITHUB_ENV\n          echo \"applicationVersion=${appVersion}\" >> $GITHUB_ENV\n          echo \"appFileName=Redis-Insight\" >> $GITHUB_ENV\n\n          # download latest.yml file to get last public version\n          aws s3 cp s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath}/${latestYmlFileName} .\n\n          versionLine=$(head -1 ${latestYmlFileName})\n          versionLineArr=(${versionLine/:// })\n          previousAppVersion=${versionLineArr[1]}\n\n          echo \"previousApplicationVersion=${previousAppVersion}\" >> $GITHUB_ENV\n\n      - name: Publish AWS S3\n        run: |\n\n          # check if sub directories exists\n          if [[ -z \"$downloadLatestFolderPath\" || -z \"$upgradeLatestFolderPath\" ]]; then\n            exit 1;\n          fi\n          # remove previous build from the latest directory /public/latest\n          aws s3 rm s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath} --recursive\n\n          # remove previous build from the upgrade directory /public/upgrades\n          aws s3 rm s3://${AWS_BUCKET_NAME}/${upgradeLatestFolderPath} --recursive\n\n          # copy current version apps for download to /public/latest\n          aws s3 cp s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \\\n            s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath} --recursive  --exclude \"*.zip\"\n\n          # copy current version apps for upgrades to /public/upgrades\n          aws s3 cp s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \\\n            s3://${AWS_BUCKET_NAME}/${upgradeLatestFolderPath} --recursive\n\n          # !MOVE current version apps to releases folder /public/releases\n          aws s3 mv s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \\\n            s3://${AWS_BUCKET_NAME}/${releasesFolderPath}/${applicationVersion} --recursive\n\n          # invalidate cloudfront cash\n          aws cloudfront create-invalidation --distribution-id ${AWS_DISTRIBUTION_ID} --paths \"/*\"\n\n      - name: Add tags for all objects and create S3 metrics\n        run: |\n\n          # declare all tags\n          declare -A tag0=(\n            [arch]='x64'\n            [platform]='macos'\n            [objectDownload]=${appFileName}'-mac-x64.dmg'\n            [objectUpgrade]=${appFileName}'-mac-x64.zip'\n          )\n\n          declare -A tag1=(\n            [arch]='arm64'\n            [platform]='macos'\n            [objectDownload]=${appFileName}'-mac-arm64.dmg'\n            [objectUpgrade]=${appFileName}'-mac-arm64.zip'\n          )\n\n          declare -A tag2=(\n            [arch]='x64'\n            [platform]='windows'\n            [objectDownload]=${appFileName}'-win-installer.exe'\n          )\n\n          declare -A tag3=(\n            [arch]='x64'\n            [platform]='linux_AppImage'\n            [objectDownload]=${appFileName}'-linux-x86_64.AppImage'\n          )\n\n          declare -A tag4=(\n            [arch]='x64'\n            [platform]='linux_deb'\n            [objectDownload]=${appFileName}'-linux-amd64.deb'\n          )\n\n          declare -A tag5=(\n            [arch]='x64'\n            [platform]='linux_rpm'\n            [objectDownload]=${appFileName}'-linux-x86_64.rpm'\n          )\n\n          # loop for add all tags to each app and create metrics\n          declare -n tag\n          for tag in ${!tag@}; do\n\n            designation0=\"downloads\"\n            designation1=\"upgrades\"\n\n            id0=\"${tag[platform]}_${tag[arch]}_${designation0}_${applicationVersion}\"\n            id1=\"${tag[platform]}_${tag[arch]}_${designation1}_${applicationVersion}\"\n\n            # add tags to each app for download\n            aws s3api put-object-tagging \\\n              --bucket ${AWS_BUCKET_NAME} \\\n              --key ${downloadLatestFolderPath}/${tag[objectDownload]} \\\n              --tagging '{\"TagSet\": [{ \"Key\": \"version\", \"Value\": \"'\"${applicationVersion}\"'\" }, {\"Key\": \"platform\", \"Value\": \"'\"${tag[platform]}\"'\"}, {\"Key\": \"arch\", \"Value\": \"'\"${tag[arch]}\"'\"}, { \"Key\": \"designation\", \"Value\": \"'\"${designation0}\"'\" }]}'\n\n            # add tags to each app for upgrades\n            aws s3api put-object-tagging \\\n              --bucket ${AWS_BUCKET_NAME} \\\n              --key ${upgradeLatestFolderPath}/${tag[objectUpgrade]:=${tag[objectDownload]}} \\\n              --tagging '{\"TagSet\": [{ \"Key\": \"version\", \"Value\": \"'\"${applicationVersion}\"'\" }, {\"Key\": \"platform\", \"Value\": \"'\"${tag[platform]}\"'\"}, {\"Key\": \"arch\", \"Value\": \"'\"${tag[arch]}\"'\"}, { \"Key\": \"designation\", \"Value\": \"'\"${designation1}\"'\" }]}'\n\n            # Create metrics for all tags for downloads to S3\n            aws s3api put-bucket-metrics-configuration \\\n              --bucket ${AWS_BUCKET_NAME} \\\n              --id ${id0} \\\n              --metrics-configuration '{\"Id\": \"'\"${id0}\"'\", \"Filter\": {\"And\": {\"Tags\": [{\"Key\": \"platform\", \"Value\": \"'\"${tag[platform]}\"'\"}, {\"Key\": \"arch\", \"Value\": \"'\"${tag[arch]}\"'\"}, {\"Key\": \"designation\", \"Value\": \"'\"${designation0}\"'\"}, {\"Key\": \"version\", \"Value\": \"'\"${applicationVersion}\"'\"} ]}}}'\n\n            # Create metrics for all tags for upgrades to S3\n            aws s3api put-bucket-metrics-configuration \\\n              --bucket ${AWS_BUCKET_NAME} \\\n              --id ${id1} \\\n              --metrics-configuration '{\"Id\": \"'\"${id1}\"'\", \"Filter\": {\"And\": {\"Tags\": [{\"Key\": \"platform\", \"Value\": \"'\"${tag[platform]}\"'\"}, {\"Key\": \"arch\", \"Value\": \"'\"${tag[arch]}\"'\"}, {\"Key\": \"designation\", \"Value\": \"'\"${designation1}\"'\"}, {\"Key\": \"version\", \"Value\": \"'\"${applicationVersion}\"'\"}]}}}'\n\n          done\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  # Called for Release workflows\n  workflow_call:\n    inputs:\n      environment:\n        description: Environment to run build\n        type: string\n        default: 'staging'\n\n      target:\n        description: Build target\n        type: string\n        default: 'all'\n\n      debug:\n        description: Enable SSH Debug\n        type: boolean\n\n      enterprise:\n        description: Enterprise build\n        type: boolean\n\njobs:\n  build-linux:\n    if: contains(inputs.target, 'linux') || inputs.target == 'all'\n    uses: ./.github/workflows/pipeline-build-linux.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment }}\n      target: ${{ inputs.target }}\n      debug: ${{ inputs.debug }}\n      enterprise: ${{ inputs.enterprise }}\n\n  build-macos:\n    if: contains(inputs.target, 'macos') || inputs.target == 'all'\n    uses: ./.github/workflows/pipeline-build-macos.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment }}\n      target: ${{ inputs.target }}\n      debug: ${{ inputs.debug }}\n      enterprise: ${{ inputs.enterprise }}\n\n  build-windows:\n    if: contains(inputs.target, 'windows') || inputs.target == 'all'\n    uses: ./.github/workflows/pipeline-build-windows.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment }}\n      debug: ${{ inputs.debug }}\n      enterprise: ${{ inputs.enterprise }}\n\n  build-docker:\n    if: contains(inputs.target, 'docker') || inputs.target == 'all'\n    uses: ./.github/workflows/pipeline-build-docker.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment }}\n      debug: ${{ inputs.debug }}\n      enterprise: ${{ inputs.enterprise }}\n"
  },
  {
    "path": ".github/workflows/clean-deployments.yml",
    "content": "name: Delete deployments\non:\n  workflow_call:\n\njobs:\n  clean:\n    name: Clean deployments\n    runs-on: ubuntu-latest\n    steps:\n      - name: 🗑 Delete deployment (staging)\n        uses: strumwolf/delete-deployment-environment@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          environment: staging\n          onlyRemoveDeployments: true\n\n      - name: 🗑 Delete deployment (production)\n        uses: strumwolf/delete-deployment-environment@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          environment: production\n          onlyRemoveDeployments: true\n\n      - name: 🗑 Delete deployment (gh-actions)\n        uses: strumwolf/delete-deployment-environment@v2\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          environment: gh-pages\n          onlyRemoveDeployments: true\n"
  },
  {
    "path": ".github/workflows/clean-s3-dev-builds.yml",
    "content": "name: Clean AWS S3 development builds\non:\n  workflow_call:\n\nenv:\n  AWS_BUCKET_NAME_TEST: ${{ secrets.AWS_BUCKET_NAME_TEST }}\n  AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n  AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n\njobs:\n  deleting:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Deleting builds and test reports older than 7 days\n        continue-on-error: true\n        run: |\n          DATE=$(date +'%Y-%m-%d')\n          DATE_EPIRED=$(date -d \"$DATE - 7 days\" +'%Y-%m-%d')\n\n          aws s3 rm s3://${AWS_BUCKET_NAME_TEST}/public/dev-builds/${DATE_EPIRED} --recursive\n          aws s3 rm s3://${AWS_BUCKET_NAME_TEST}/public/test-reports/${DATE_EPIRED} --recursive\n"
  },
  {
    "path": ".github/workflows/code-coverage.yml",
    "content": "name: 'Code Coverage'\non:\n  workflow_call:\n    inputs:\n      type:\n        description: Type of report (unit or integration)\n        type: string\n      resource_name:\n        description: Resource name of coverage report\n        type: string\n\njobs:\n  coverage-unit:\n    runs-on: ubuntu-latest\n    name: Unit tests coverage\n    if: ${{ inputs.type == 'unit' }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Download Coverage Report\n        uses: actions/download-artifact@v4\n        with:\n          name: ${{ inputs.resource_name }}\n          path: report\n\n      - uses: jwalton/gh-find-current-pr@v1\n        id: findPr\n\n      - uses: ArtiomTr/jest-coverage-report-action@v2\n        with:\n          prnumber: ${{ steps.findPr.outputs.number }}\n          coverage-file: report/coverage/report.json\n          base-coverage-file: report/coverage/report.json\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          skip-step: all\n          custom-title: Code Coverage - ${{ inputs.resource_name == 'report-be' && 'Backend' || 'Frontend' }} unit tests\n\n  coverage-integration:\n    runs-on: ubuntu-latest\n    name: Integration tests coverage\n    if: ${{ inputs.type == 'integration' }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Download Coverage Report\n        uses: actions/download-artifact@v4\n        with:\n          name: ${{ inputs.resource_name }}\n\n      - name: Parse Coverage Summary\n        id: parse-coverage\n        run: |\n          # Extract coverage data from file.\n          # Example of processed row:\n          #   Statements   : 81.75% ( 16130/19730 )\n          # field '$3' = 81.75%, field '$5' = 16130\n          extract_coverage_data() {\n              local keyword=$1\n              local field=$2\n              awk \"/$keyword/ {print $field}\" integration-coverage.txt | tr -d '\\n|%'\n          }\n\n          # Determine status based on percentage\n          get_status() {\n              if [ \"$(echo \"$1 < 50\" | bc)\" -eq 1 ]; then\n                  echo \"🔴\"\n              elif [ \"$(echo \"$1 < 80\" | bc)\" -eq 1 ]; then\n                  echo \"🟡\"\n              else\n                  echo \"🟢\"\n              fi\n          }\n\n          # Extract coverage data from the summary\n          STATEMENTS_PERCENT=$(extract_coverage_data \"Statements\" '$3')\n          STATEMENTS_COVERED=$(extract_coverage_data \"Statements\" '$5')\n          STATEMENTS_STATUS=$(get_status $STATEMENTS_PERCENT)\n\n          BRANCHES_PERCENT=$(extract_coverage_data \"Branches\" '$3')\n          BRANCHES_COVERED=$(extract_coverage_data \"Branches\" '$5')\n          BRANCHES_STATUS=$(get_status $BRANCHES_PERCENT)\n\n          FUNCTIONS_PERCENT=$(extract_coverage_data \"Functions\" '$3')\n          FUNCTIONS_COVERED=$(extract_coverage_data \"Functions\" '$5')\n          FUNCTIONS_STATUS=$(get_status $FUNCTIONS_PERCENT)\n\n          LINES_PERCENT=$(extract_coverage_data \"Lines\" '$3')\n          LINES_COVERED=$(extract_coverage_data \"Lines\" '$5')\n          LINES_STATUS=$(get_status $LINES_PERCENT)\n\n          # Format as a Markdown table\n          echo \"| Status      | Category    | Percentage  | Covered / Total |\" > coverage-table.md\n          echo \"|-------------|-------------|-------------|-----------------|\" >> coverage-table.md\n          echo \"| $STATEMENTS_STATUS | Statements  | ${STATEMENTS_PERCENT}% | ${STATEMENTS_COVERED} |\" >> coverage-table.md\n          echo \"| $BRANCHES_STATUS | Branches    | ${BRANCHES_PERCENT}% | ${BRANCHES_COVERED} |\" >> coverage-table.md\n          echo \"| $FUNCTIONS_STATUS | Functions   | ${FUNCTIONS_PERCENT}% | ${FUNCTIONS_COVERED} |\" >> coverage-table.md\n          echo \"| $LINES_STATUS | Lines       | ${LINES_PERCENT}% | ${LINES_COVERED} |\" >> coverage-table.md\n\n      - uses: jwalton/gh-find-current-pr@v1\n        id: findPr\n        continue-on-error: true\n\n      - name: Post or Update Coverage Summary Comment\n        if: ${{ steps.findPr.outputs.number != '' }}\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const fs = require('fs');\n            const table = fs.readFileSync('coverage-table.md', 'utf8');\n            const commentBody = `### Code Coverage - Integration Tests\\n\\n${table}`;\n\n            // Fetch existing comments on the pull request\n            const { data: comments } = await github.rest.issues.listComments({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: process.env.RR_Number,\n            });\n\n            // Check if a comment with the same header already exists\n            const existingComment = comments.find(comment =>\n              comment.body.startsWith('### Code Coverage - Integration Tests')\n            );\n\n            if (existingComment) {\n              // Update the existing comment\n              await github.rest.issues.updateComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                comment_id: existingComment.id,\n                body: commentBody,\n              });\n            } else {\n              // Create a new comment\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: process.env.RR_Number,\n                body: commentBody,\n              });\n            }\n        env:\n          RR_Number: ${{ steps.findPr.outputs.number }}\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: [main, latest, release/*, codeql]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [main]\n  schedule:\n    - cron: '37 11 * * 3'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: ['javascript']\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]\n        # Learn more about CodeQL language support at https://git.io/codeql-language-support\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@v4\n        with:\n          languages: ${{ matrix.language }}\n          config-file: ./.github/codeql/config.yml\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      # 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@v4\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@v4\n"
  },
  {
    "path": ".github/workflows/compress-images.yml",
    "content": "name: Compress Images\non:\n  pull_request:\n    # Run Image Actions when JPG, JPEG, PNG or WebP files are added or changed.\n    # See https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for reference.\n    paths:\n      - '**.jpg'\n      - '**.jpeg'\n      - '**.png'\n      - '**.webp'\n    branches-ignore:\n      - 'latest'\n      - 'release/*'\n      - 'ric/*'\njobs:\n  build:\n    # Only run on Pull Requests within the same repository, and not from forks.\n    if: github.event.pull_request.head.repo.full_name == github.repository\n    name: calibreapp/image-actions\n    permissions: write-all\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repo\n        uses: actions/checkout@v4\n\n      - name: Compress Images\n        uses: calibreapp/image-actions@main\n        with:\n          # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories.\n          # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions\n          githubToken: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/enforce-branch-name-rules.yml",
    "content": "name: Enforce Branch Name Rules\n\non:\n  pull_request:\n    branches:\n      - main\n\njobs:\n  enforce-branch-rules:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check branch name\n        env:\n          BRANCH_NAME: ${{ github.head_ref }}\n        run: |\n          echo \"Source branch: $BRANCH_NAME\"\n          if [[ \"$BRANCH_NAME\" != feature/* && \\\n                \"$BRANCH_NAME\" != bugfix/* && \\\n                \"$BRANCH_NAME\" != release/* && \\\n                \"$BRANCH_NAME\" != dependabot/* && \\\n                \"$BRANCH_NAME\" != latest && \\\n                \"$BRANCH_NAME\" != fe/* && \\\n                \"$BRANCH_NAME\" != be/* && \\\n                \"$BRANCH_NAME\" != e2e/* && \\\n                \"$BRANCH_NAME\" != test/* && \\\n                \"$BRANCH_NAME\" != docs/* && \\\n                \"$BRANCH_NAME\" != ric/* ]]; then\n            echo \"❌ Pull requests to 'main' are only allowed from 'feature/**', 'bugfix/**', 'release/**', 'dependabot/**', 'latest', 'test/**', 'docs/**', or 'ric/**' branches.\"\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/licenses-check.yml",
    "content": "name: Licenses check pipeline\non:\n  workflow_call:\n  workflow_dispatch:\n\njobs:\n  licenses-check:\n    name: Licenses check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install all libs and dependencies\n        uses: ./.github/actions/install-all-build-libs\n\n      - name: Install plugins dependencies\n        env:\n          pluginsOnlyInstall: 1\n        run: yarn build:statics\n\n      - name: Generate licenses csv files and send csv data to google sheet\n        env:\n          GOOGLE_ACCOUNT_SERVICE_KEY_BASE64: ${{ secrets.GOOGLE_ACCOUNT_SERVICE_KEY_BASE64 }}\n          GOOGLE_SPREADSHEET_DEPENDENCIES_ID: ${{ secrets.GOOGLE_SPREADSHEET_DEPENDENCIES_ID }}\n        run: |\n          npm i -g license-checker\n          echo \"$GOOGLE_ACCOUNT_SERVICE_KEY_BASE64\" | base64 -id > gasKey.json\n          SPREADSHEET_ID=$GOOGLE_SPREADSHEET_DEPENDENCIES_ID node .github/deps-licenses-report.js\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: licenses\n          path: licenses\n          if-no-files-found: error\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  workflow_call:\n  workflow_dispatch:\n  push:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    name: ESLint\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install dependencies\n        uses: ./.github/actions/install-all-build-libs\n        with:\n          skip-system-deps: '1'\n\n      - name: Restore ESLint cache\n        uses: actions/cache@v4\n        with:\n          path: .eslintcache\n          key: eslint-${{ runner.os }}-${{ hashFiles('.eslintrc.js') }}-${{ github.ref_name }}\n          restore-keys: |\n            eslint-${{ runner.os }}-${{ hashFiles('.eslintrc.js') }}-\n\n      - name: Run ESLint\n        run: yarn lint\n"
  },
  {
    "path": ".github/workflows/manual-build-enterprise.yml",
    "content": "name: 🚀 Manual build - Enterprise\n\non:\n  # Manual trigger build\n  # No multi-select\n  # https://github.com/actions/runner/issues/2076\n  workflow_dispatch:\n    inputs:\n      build_docker:\n        description: Build Docker\n        type: boolean\n        required: false\n\n      build_windows_x64:\n        description: Build Windows x64\n        type: boolean\n        required: false\n\n      build_macos_x64:\n        description: Build macOS x64\n        type: boolean\n        required: false\n\n      build_macos_arm64:\n        description: Build macOS arm64\n        type: boolean\n        required: false\n\n      build_linux_appimage_x64:\n        description: Build Linux AppImage x64\n        type: boolean\n        required: false\n\n      build_linux_deb_x64:\n        description: Build Linux deb x64\n        type: boolean\n        required: false\n\n      build_linux_rpm_x64:\n        description: Build Linux rpm x64\n        type: boolean\n        required: false\n\n      environment:\n        description: Environment to run build\n        type: environment\n        default: 'development'\n        required: false\n\n      debug:\n        description: Enable SSH Debug\n        type: boolean\n\n# Cancel a previous same workflow\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  get-selected:\n    runs-on: ubuntu-latest\n    outputs: # Set this to consume the output on other job\n      selected: ${{ steps.get-selected.outputs.selected}}\n    steps:\n      - uses: actions/checkout@v4\n\n      - id: get-selected\n        uses: joao-zanutto/get-selected@v1.1.1\n        with:\n          format: 'list'\n\n      - name: echo selected targets\n        run: echo ${{ steps.get-selected.outputs.selected }}\n\n  manual-build:\n    needs: get-selected\n    uses: ./.github/workflows/build.yml\n    secrets: inherit\n    with:\n      target: ${{ needs.get-selected.outputs.selected }}\n      debug: ${{ inputs.debug }}\n      environment: ${{ inputs.environment }}\n      enterprise: true\n\n  aws-upload:\n    uses: ./.github/workflows/aws-upload-enterprise.yml\n    secrets: inherit\n    needs: [manual-build]\n    if: always()\n    with:\n      environment: ${{ inputs.environment }}\n\n  clean:\n    uses: ./.github/workflows/clean-deployments.yml\n    # secrets: inherit\n    needs: [aws-upload]\n    if: always()\n\n  # Remove artifacts from github actions\n  remove-artifacts:\n    name: Remove artifacts\n    needs: [aws-upload]\n    if: always()\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Remove all artifacts\n        uses: ./.github/actions/remove-artifacts\n"
  },
  {
    "path": ".github/workflows/manual-build.yml",
    "content": "name: 🚀 Manual build\n\non:\n  # Manual trigger build\n  # No multi-select\n  # https://github.com/actions/runner/issues/2076\n  workflow_dispatch:\n    inputs:\n      build_docker:\n        description: Build Docker\n        type: boolean\n        required: false\n\n      build_windows_x64:\n        description: Build Windows x64\n        type: boolean\n        required: false\n\n      build_macos_x64:\n        description: Build macOS x64\n        type: boolean\n        required: false\n\n      build_macos_arm64:\n        description: Build macOS arm64\n        type: boolean\n        required: false\n\n      build_linux_appimage_x64:\n        description: Build Linux AppImage x64\n        type: boolean\n        required: false\n\n      build_linux_deb_x64:\n        description: Build Linux deb x64\n        type: boolean\n        required: false\n\n      build_linux_rpm_x64:\n        description: Build Linux rpm x64\n        type: boolean\n        required: false\n\n      build_linux_snap_x64:\n        description: Build Linux snap x64\n        type: boolean\n        required: false\n\n      environment:\n        description: Environment to run build\n        type: environment\n        default: 'development'\n        required: false\n\n      debug:\n        description: Enable SSH Debug\n        type: boolean\n\n# Cancel a previous same workflow\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  get-selected:\n    runs-on: ubuntu-latest\n    outputs: # Set this to consume the output on other job\n      selected: ${{ steps.get-selected.outputs.selected}}\n    steps:\n      - uses: actions/checkout@v4\n\n      - id: get-selected\n        uses: joao-zanutto/get-selected@v1.1.1\n        with:\n          format: 'list'\n\n      - name: echo selected targets\n        run: echo ${{ steps.get-selected.outputs.selected }}\n\n  manual-build:\n    needs: get-selected\n    uses: ./.github/workflows/build.yml\n    secrets: inherit\n    with:\n      target: ${{ needs.get-selected.outputs.selected }}\n      debug: ${{ inputs.debug }}\n      environment: ${{ inputs.environment }}\n      enterprise: false\n\n  aws-upload:\n    uses: ./.github/workflows/aws-upload-dev.yml\n    secrets: inherit\n    needs: [manual-build]\n    if: always()\n\n  clean:\n    uses: ./.github/workflows/clean-deployments.yml\n    # secrets: inherit\n    needs: [aws-upload]\n    if: always()\n\n  # Remove artifacts from github actions\n  remove-artifacts:\n    name: Remove artifacts\n    needs: [aws-upload]\n    if: always()\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Remove all artifacts\n        uses: ./.github/actions/remove-artifacts\n"
  },
  {
    "path": ".github/workflows/nightly-virustotal-analyze.yml",
    "content": "name: Nightly Virustotal Analyze\n\non:\n  workflow_dispatch:\n    inputs:\n      mode:\n        description: \"Choose 'single' or 'all'\"\n        required: true\n        default: 'all'\n        type: string\n      file_url:\n        description: \"Provide a file URL for single file scanning (required for 'single' mode)\"\n        required: false\n        default: 'https://s3.amazonaws.com/redisinsight.download/public/latest/Redis-Insight-mac-arm64.dmg'\n        type: string\n  schedule:\n    - cron: '0 0 * * *'\n\nenv:\n  VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}\n  DEFAULT_FILES: |\n    https://s3.amazonaws.com/redisinsight.download/public/latest/Redis-Insight-mac-x64.dmg\n    https://s3.amazonaws.com/redisinsight.download/public/latest/Redis-Insight-mac-arm64.dmg\n    https://s3.amazonaws.com/redisinsight.download/public/latest/Redis-Insight-win-installer.exe\n    https://s3.amazonaws.com/redisinsight.download/public/latest/Redis-Insight-linux-x86_64.AppImage\n    https://s3.amazonaws.com/redisinsight.download/public/latest/Redis-Insight-linux-amd64.deb\n\njobs:\n  analyze:\n    name: VirusTotal Analyze\n    runs-on: ubuntu-latest\n    outputs:\n      files: ${{ steps.setup_matrix.outputs.files }}\n    steps:\n      - name: Determine mode and files\n        id: setup_matrix\n        run: |\n          mode=\"${{ github.event.inputs.mode }}\"\n          file_url=\"${{ github.event.inputs.file_url }}\"\n\n          if [ \"$mode\" == \"single\" ] && [ -z \"$file_url\" ]; then\n            echo \"Error: For 'single' mode, a file URL must be provided.\"\n            exit 1\n          fi\n\n          if [ \"$mode\" == \"single\" ]; then\n            echo \"files=[\\\"$file_url\\\"]\" >> $GITHUB_OUTPUT\n          else\n            files_json=$(echo \"${{ env.DEFAULT_FILES }}\" | sed '/^$/d' | jq -R -s -c 'split(\"\\n\")[:-1]')\n            echo \"files=$files_json\" >> $GITHUB_OUTPUT\n          fi\n\n  analyze_files:\n    name: Analyze each file\n    needs: analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        file: ${{ fromJson(needs.analyze.outputs.files) }}\n\n    steps:\n      - name: Download file\n        run: |\n          echo \"Downloading: ${{ matrix.file }}\"\n          curl -sLo file_to_analyze \"${{ matrix.file }}\"\n\n      - name: Get upload URL\n        id: get_upload_url\n        run: |\n          upload_url=$(curl -sq -XGET https://www.virustotal.com/api/v3/files/upload_url \\\n            -H \"x-apikey: $VIRUSTOTAL_API_KEY\" | jq -r '.data')\n\n          if [ -z \"$upload_url\" ] || [ \"$upload_url\" == \"null\" ]; then\n            echo \"Failed to retrieve upload URL for ${{ matrix.file }}\"\n            exit 1\n          fi\n\n          echo \"UPLOAD_URL=$upload_url\" >> $GITHUB_ENV\n\n      - name: Upload file to VirusTotal\n        id: upload_file\n        run: |\n          upload_url=\"${{ env.UPLOAD_URL }}\"\n          analyzed_id=$(curl -sq -XPOST \"$upload_url\" \\\n            -H \"x-apikey: $VIRUSTOTAL_API_KEY\" \\\n            --form \"file=@file_to_analyze\" | jq -r '.data.id')\n\n          if [ -z \"$analyzed_id\" ] || [ \"$analyzed_id\" == \"null\" ]; then\n            echo \"Failed to upload file: ${{ matrix.file }}\"\n            exit 1\n          fi\n\n          echo \"ANALYZED_ID=$analyzed_id\" >> $GITHUB_ENV\n\n      - name: Check analyze status\n        run: |\n          analyzed_id=\"${{ env.ANALYZED_ID }}\"\n          retry_attempts=50\n          interval_time=30\n\n          until [ \"$retry_attempts\" == \"0\" ]; do\n            analyze_status=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${analyzed_id} \\\n              -H \"x-apikey: $VIRUSTOTAL_API_KEY\" | jq -r '.data.attributes.status')\n\n            if [ \"$analyze_status\" == \"completed\" ]; then\n              break\n            fi\n\n            echo \"Current status: $analyze_status, retries left: $retry_attempts\"\n            sleep $interval_time\n            retry_attempts=$((retry_attempts - 1))\n          done\n\n          if [ \"$analyze_status\" != \"completed\" ]; then\n            echo \"Analysis not completed for ${{ matrix.file }}\"\n            exit 1\n          fi\n\n      - name: Validate analyze\n        run: |\n          analyzed_id=\"${{ env.ANALYZED_ID }}\"\n          analysis=$(curl -sq -XGET \"https://www.virustotal.com/api/v3/analyses/${analyzed_id}\" \\\n            -H \"x-apikey: $VIRUSTOTAL_API_KEY\")\n\n          analyze_stats=$(echo \"$analysis\" | jq -r '.data.attributes.stats')\n\n          malicious=$(echo \"$analyze_stats\" | jq '.malicious')\n          suspicious=$(echo \"$analyze_stats\" | jq '.suspicious')\n          suspicious=$(echo \"$analyze_stats\" | jq '.suspicious')\n          harmless=$(echo \"$analyze_stats\" | jq '.harmless')\n\n          echo \"Results for ${{ matrix.file }}: Malicious: $malicious, Suspicious: $suspicious, Harmless: $harmless\"\n\n          if [ \"$malicious\" != \"0\" ] || [ \"$suspicious\" != \"0\" ]; then\n            echo \"File ${{ matrix.file }} is flagged as potentially harmful.\"\n            echo \"$analysis\" | jq -r '.data.attributes.results[] | select(.result == \"malicious\" or .result == \"suspicious\")'\n            exit 1\n          fi\n"
  },
  {
    "path": ".github/workflows/pipeline-build-docker.yml",
    "content": "name: Build docker pipeline\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: Environment for build\n        default: 'staging'\n        type: string\n\n      for_e2e_tests:\n        description: Build for e2e docker tests\n        default: false\n        type: boolean\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\n      enterprise:\n        description: Enterprise build\n        type: boolean\n\njobs:\n  build:\n    name: Build docker\n    runs-on: ubuntu-24.04\n    environment: ${{ inputs.environment }}\n    steps:\n      - uses: actions/checkout@v4\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Install all libs and dependencies\n        uses: ./.github/actions/install-all-build-libs\n        with:\n          keytar-host-mirror: ${{ secrets.NPM_CONFIG_KEYTAR_BINARY_HOST_MIRROR }}\n          sqlite3-host-mirror: ${{ secrets.NPM_CONFIG_NODE_SQLITE3_BINARY_HOST_MIRROR }}\n\n      - name: Build sources\n        run: ./.github/build/build.sh\n\n        # todo: matrix\n      - name: Build web archives for e2e tests\n        if: inputs.for_e2e_tests\n        run: |\n          unset npm_config_keytar_binary_host_mirror\n          unset npm_config_node_sqlite3_binary_host_mirror\n          # Docker sources\n          PLATFORM=linux ARCH=x64 LIBC=musl .github/build/build_modules.sh\n\n      - name: Build web archives\n        if: ${{ !inputs.for_e2e_tests }}\n        run: |\n          unset npm_config_keytar_binary_host_mirror\n          unset npm_config_node_sqlite3_binary_host_mirror\n          # Docker sources\n          PLATFORM=linux ARCH=x64 LIBC=musl .github/build/build_modules.sh\n          PLATFORM=linux ARCH=arm64 LIBC=musl .github/build/build_modules.sh\n          # Redis Stack + VSC Linux\n          PLATFORM=linux ARCH=x64 .github/build/build_modules.sh\n          PLATFORM=linux ARCH=arm64 .github/build/build_modules.sh\n          # VSC Darwin\n          PLATFORM=darwin ARCH=x64 .github/build/build_modules.sh\n          PLATFORM=darwin ARCH=arm64 .github/build/build_modules.sh\n          # VSC Windows\n          PLATFORM=win32 ARCH=x64 .github/build/build_modules.sh\n\n      - name: Build Docker (x64)\n        run: |\n          # Build alpine x64 image\n          docker buildx build \\\n          -f .github/build/build.Dockerfile \\\n          --platform linux/amd64 \\\n          --build-arg DIST=release/web/Redis-Insight-web-linux-musl.x64.tar.gz \\\n          --build-arg NODE_ENV=\"$ENV\" \\\n          --build-arg RI_SEGMENT_WRITE_KEY=\"$RI_SEGMENT_WRITE_KEY\" \\\n          -t redisinsight:amd64 \\\n          .\n\n          mkdir -p release/docker\n          docker image save -o release/docker/docker-linux-alpine.amd64.tar redisinsight:amd64\n\n      - name: Build Docker (arm64)\n        if: ${{ !inputs.for_e2e_tests }}\n        run: |\n          # Build alpine arm64 image\n          docker buildx build \\\n          -f .github/build/build.Dockerfile \\\n          --platform linux/arm64 \\\n          --build-arg DIST=release/web/Redis-Insight-web-linux-musl.arm64.tar.gz \\\n          --build-arg NODE_ENV=\"$ENV\" \\\n          --build-arg RI_SEGMENT_WRITE_KEY=\"$RI_SEGMENT_WRITE_KEY\" \\\n          -t redisinsight:arm64 \\\n          .\n\n          mkdir -p release/docker\n          docker image save -o release/docker/docker-linux-alpine.arm64.tar redisinsight:arm64\n\n      - uses: actions/upload-artifact@v4\n        name: Upload docker builds\n        with:\n          if-no-files-found: warn\n          name: docker-builds\n          path: |\n            ./release/docker\n            ./release/web\n            ./release/web-mini\n\n    env:\n      ENV: ${{ vars.ENV }}\n      RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }}\n      RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }}\n      RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }}\n      RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }}\n      RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }}\n      RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }}\n      RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }}\n      RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }}\n      RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }}\n      RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }}\n      RI_CLOUD_IDP_REDIRECT_URI: ${{ secrets.RI_CLOUD_IDP_REDIRECT_URI }}\n      RI_CLOUD_IDP_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_TOKEN_URL }}\n      RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }}\n      RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n      RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n      RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }}\n      RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK_NEW }}\n      RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == false }}\n      RI_DISABLE_AUTO_UPGRADE: ${{ inputs.enterprise }}\n"
  },
  {
    "path": ".github/workflows/pipeline-build-linux.yml",
    "content": "name: Build linux pipeline\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: Environment for build\n        required: false\n        default: 'staging'\n        type: string\n\n      target:\n        description: Build target\n        required: false\n        default: 'all'\n        type: string\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\n      enterprise:\n        description: Enterprise build\n        type: boolean\n\njobs:\n  build:\n    name: Build linux\n    runs-on: ubuntu-24.04\n    environment: ${{ inputs.environment }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Install all libs and dependencies\n        uses: ./.github/actions/install-all-build-libs\n        with:\n          keytar-host-mirror: ${{ secrets.NPM_CONFIG_KEYTAR_BINARY_HOST_MIRROR }}\n          sqlite3-host-mirror: ${{ secrets.NPM_CONFIG_NODE_SQLITE3_BINARY_HOST_MIRROR }}\n\n      # TODO: Is it needed?\n      # - run: |\n      #     mkdir electron\n      #     CURRENT_VERSION=$(jq -r \".version\" redisinsight/package.json)\n      #     echo \"Build version: $CURRENT_VERSION\"\n      #     cp ./redisinsight/package.json ./electron/package.json\n      #     echo \"$VERSION\" > electron/version\n      #     exit 0\n      # - uses: actions/download-artifact@v4.1.0\n      #   with:\n      #     path: \".\"\n      # - run: cp ./electron/package.json ./redisinsight/\n\n      - name: Install plugins dependencies and build plugins\n        run: yarn build:statics\n\n      - name: Build linux packages (production)\n        if: vars.ENV == 'production' && inputs.target == vars.ALL\n        run: yarn package:prod\n\n      - name: Build linux packages (staging)\n        if: (vars.ENV == 'staging' || vars.ENV == 'development') && inputs.target == vars.ALL\n        run: yarn package:stage\n\n      - name: Build linux packages (custom)\n        if: inputs.target != vars.ALL\n        run: |\n          target=$(echo \"${{inputs.target}}\" | grep -oE 'build_linux_[^_ ]+' | sed 's/build_linux_//' | sort -u | paste -sd ' ' -)\n\n          if [ \"${{ vars.ENV == 'production' }}\" == \"true\" ]; then\n            yarn package:prod --linux $target\n          else\n            yarn package:stage --linux $target\n          fi\n\n      - uses: actions/upload-artifact@v4\n        name: Upload vendor for plugins\n        with:\n          name: vendor-plugins\n          path: |\n            ./vendor\n\n      - uses: actions/upload-artifact@v4\n        name: Upload linux builds\n        with:\n          name: linux-builds\n          path: |\n            ./release/latest-linux.yml\n            ./release/Redis-Insight*.AppImage\n            ./release/Redis-Insight*.deb\n            ./release/Redis-Insight*.rpm\n            ./release/Redis-Insight*.snap\n\n    env:\n      RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }}\n      RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }}\n      RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }}\n      RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }}\n      RI_CLOUD_API_TOKEN: ${{ secrets.RI_CLOUD_API_TOKEN }}\n      RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }}\n      RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }}\n      RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }}\n      RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }}\n      RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }}\n      RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }}\n      RI_CLOUD_IDP_REVOKE_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_REVOKE_TOKEN_URL }}\n      RI_CLOUD_IDP_REDIRECT_URI: ${{ secrets.RI_CLOUD_IDP_REDIRECT_URI }}\n      RI_CLOUD_IDP_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_TOKEN_URL }}\n      RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }}\n      RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n      RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n      RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }}\n      RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK_NEW }}\n      RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == false }}\n      RI_DISABLE_AUTO_UPGRADE: ${{ inputs.enterprise }}\n      RI_APP_TYPE: ${{ inputs.enterprise && 'ELECTRON_ENTERPRISE' || 'ELECTRON' }}\n"
  },
  {
    "path": ".github/workflows/pipeline-build-macos.yml",
    "content": "name: Build macos pipeline\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: Environment for build\n        required: false\n        default: 'staging'\n        type: string\n\n      target:\n        description: Build target\n        required: false\n        default: 'all'\n        type: string\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\n      enterprise:\n        description: Enterprise build\n        type: boolean\n\njobs:\n  build:\n    name: Build macos\n    runs-on: macos-14\n    environment: ${{ inputs.environment }}\n    steps:\n      - uses: actions/checkout@v4\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Add certificates to the keychain\n        uses: ./.github/actions/install-apple-certs\n        with:\n          CSC_P12_BASE64: ${{ secrets.CSC_P12_BASE64 }}\n          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}\n          CSC_MAS_PASSWORD: ${{ secrets.CSC_MAS_PASSWORD }}\n          CSC_MAS_P12_BASE64: ${{ secrets.CSC_MAS_P12_BASE64 }}\n          CSC_MAC_INSTALLER_PASSWORD: ${{ secrets.CSC_MAC_INSTALLER_PASSWORD }}\n          CSC_MAC_INSTALLER_P12_BASE64: ${{ secrets.CSC_MAC_INSTALLER_P12_BASE64 }}\n\n      - name: Install all libs and dependencies\n        uses: ./.github/actions/install-all-build-libs\n        with:\n          keytar-host-mirror: ${{ secrets.NPM_CONFIG_KEYTAR_BINARY_HOST_MIRROR }}\n          sqlite3-host-mirror: ${{ secrets.NPM_CONFIG_NODE_SQLITE3_BINARY_HOST_MIRROR }}\n\n      - name: Install plugins dependencies and build plugins\n        run: yarn build:statics\n\n      - name: Build macos dmg (prod)\n        if: vars.ENV == 'production' && inputs.target == vars.ALL\n        run: |\n          unset CSC_LINK\n\n          yarn package:prod\n          yarn package:mas\n          rm -rf release/mac\n          mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg\n\n      - name: Build macos dmg (staging)\n        if: (vars.ENV == 'staging' || vars.ENV == 'development') && inputs.target == vars.ALL\n        run: |\n          unset CSC_LINK\n\n          echo $APP_BUNDLE_VERSION\n          echo $CSC_KEYCHAIN\n\n          yarn package:stage && yarn package:mas\n          rm -rf release/mac\n\n          mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg\n\n      # handle manual builds\n      - name: Build macos dmg (custom)\n        if: inputs.target != vars.ALL\n        run: |\n          unset CSC_LINK\n          target=$(echo \"${{inputs.target}}\" | grep -oE 'build_macos_[^ ]+' | sed 's/build_macos_/dmg:/' | paste -sd ' ' -)\n\n          if [ \"${{ vars.ENV == 'production' }}\" == \"true\" ]; then\n            yarn package:prod --mac $target\n          else\n            yarn package:stage --mac $target\n          fi\n\n          rm -rf release/mac\n\n      - name: Repack dmg to tar\n        if: vars.ENV == 'production' && inputs.target == vars.ALL\n        run: |\n          ARCH=x64 ./.github/redisstack/dmg.repack.sh\n          ARCH=arm64 ./.github/redisstack/dmg.repack.sh\n\n      - name: Upload macos packages\n        uses: actions/upload-artifact@v4\n        with:\n          name: macos-builds\n          path: |\n            ./release/Redis-Insight*x64.dmg\n            ./release/Redis-Insight*x64.dmg.blockmap\n            ./release/Redis-Insight*.zip\n            ./release/Redis-Insight*arm64.dmg\n            ./release/Redis-Insight*arm64.dmg.blockmap\n            ./release/Redis-Insight*.pkg\n            ./release/*-mac.yml\n            ./release/redisstack\n\n    env:\n      APPLE_ID: ${{ secrets.APPLE_ID }}\n      APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}\n      APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}\n      USE_HARD_LINKS: ${{ vars.USE_HARD_LINKS }}\n      CSC_KEYCHAIN: ${{ vars.CSC_KEYCHAIN }}\n      CSC_IDENTITY_AUTO_DISCOVERY: ${{ vars.CSC_IDENTITY_AUTO_DISCOVERY }}\n      CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}\n      RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }}\n      RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }}\n      RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }}\n      RI_CLOUD_API_TOKEN: ${{ secrets.RI_CLOUD_API_TOKEN }}\n      RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }}\n      RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }}\n      RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }}\n      RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }}\n      RI_CLOUD_IDP_REVOKE_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_REVOKE_TOKEN_URL }}\n      RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }}\n      RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }}\n      RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }}\n      RI_CLOUD_IDP_REDIRECT_URI: ${{ secrets.RI_CLOUD_IDP_REDIRECT_URI }}\n      RI_CLOUD_IDP_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_TOKEN_URL }}\n      RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }}\n      RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n      RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n      RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }}\n      RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK_NEW }}\n      RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == false }}\n      RI_DISABLE_AUTO_UPGRADE: ${{ inputs.enterprise }}\n      RI_APP_TYPE: ${{ inputs.enterprise && 'ELECTRON_ENTERPRISE' || 'ELECTRON' }}\n"
  },
  {
    "path": ".github/workflows/pipeline-build-windows.yml",
    "content": "name: Build windows pipeline\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: Environment for build\n        default: 'staging'\n        type: string\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\n      enterprise:\n        description: Enterprise build\n        type: boolean\n\njobs:\n  build:\n    name: Build windows\n    runs-on: windows-2022\n    environment: ${{ inputs.environment }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Install all libs and dependencies\n        uses: ./.github/actions/install-all-build-libs\n\n      - name: Setup certs\n        uses: ./.github/actions/install-windows-certs\n        with:\n          WIN_CSC_PFX_BASE64: ${{ secrets.WIN_CSC_PFX_BASE64 }}\n\n      - name: Install plugins dependencies and build plugins\n        run: yarn build:statics:win\n\n      - name: Build windows exe (production)\n        if: vars.ENV == 'production'\n        run: |\n          yarn package:prod\n          rm -rf release/win-unpacked\n        shell: bash\n\n      - name: Build windows exe (staging)\n        if: (vars.ENV == 'staging' ||  vars.ENV == 'development')\n        run: |\n          yarn package:stage\n          rm -rf release/win-unpacked\n        shell: bash\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: windows-builds\n          if-no-files-found: error\n          path: |\n            ./release/Redis-Insight*.exe\n            ./release/Redis-Insight*.exe.blockmap\n            ./release/latest.yml\n\n    env:\n      WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}\n      WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}\n      RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }}\n      RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }}\n      RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }}\n      RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }}\n      RI_CLOUD_API_TOKEN: ${{ secrets.RI_CLOUD_API_TOKEN }}\n      RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }}\n      RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }}\n      RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }}\n      RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }}\n      RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }}\n      RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }}\n      RI_CLOUD_IDP_REVOKE_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_REVOKE_TOKEN_URL }}\n      RI_CLOUD_IDP_REDIRECT_URI: ${{ secrets.RI_CLOUD_IDP_REDIRECT_URI }}\n      RI_CLOUD_IDP_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_TOKEN_URL }}\n      RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }}\n      RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n      RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n      RI_FEATURES_CONFIG_URL: ${{ secrets.RI_FEATURES_CONFIG_URL }}\n      RI_UPGRADES_LINK: ${{ secrets.RI_UPGRADES_LINK_NEW }}\n      RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG: ${{ inputs.enterprise == false }}\n      RI_DISABLE_AUTO_UPGRADE: ${{ inputs.enterprise }}\n      RI_APP_TYPE: ${{ inputs.enterprise && 'ELECTRON_ENTERPRISE' || 'ELECTRON' }}\n"
  },
  {
    "path": ".github/workflows/publish-stores.yml",
    "content": "name: Publish to stores\n\non:\n  workflow_call:\n\nenv:\n  AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}\n  AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n  AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    name: Publish to Dockerhub\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Download Docker images\n        run: |\n          mkdir release\n          aws s3 cp s3://${AWS_BUCKET_NAME}/public/latest-v3/docker ./release --recursive\n\n      - name: Publish docker\n        env:\n          DOCKER_USER: ${{ secrets.DOCKER_USER }}\n          DOCKER_PASS: ${{ secrets.DOCKER_PASS }}\n          DOCKER_REPO: ${{ secrets.DOCKER_REPO }}\n          DOCKER_V1_USER: ${{ secrets.DOCKER_V1_USER }}\n          DOCKER_V1_PASS: ${{ secrets.DOCKER_V1_PASS }}\n          DOCKER_V1_REPO: ${{ secrets.DOCKER_V1_REPO }}\n        run: |\n          appVersion=$(jq -r '.version' redisinsight/package.json)\n\n          # Publish desktop image\n          docker login -u $DOCKER_USER -p $DOCKER_PASS\n\n          ./.github/build/release-docker.sh \\\n          -d redisinsight \\\n          -r $DOCKER_REPO \\\n          -v $appVersion\n\n          # Publish cloud image\n          docker login -u $DOCKER_V1_USER -p $DOCKER_V1_PASS\n\n          ./.github/build/release-docker.sh \\\n          -d redisinsight \\\n          -r $DOCKER_V1_REPO \\\n          -v $appVersion\n\n  snapcraft:\n    runs-on: ubuntu-latest\n    name: Publish to Snapcraft\n    env:\n      SNAPCRAFT_FILE_NAME: 'Redis-Insight-linux-amd64.snap'\n      SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Download Snapcraft package\n        id: snap\n        run: |\n          mkdir release\n          aws s3 cp s3://${AWS_BUCKET_NAME}/public/latest-v3/${SNAPCRAFT_FILE_NAME} ./release\n          echo \"snap-path=$(readlink -e ./release/${SNAPCRAFT_FILE_NAME})\" >> \"$GITHUB_OUTPUT\"\n\n      - uses: snapcore/action-publish@v1\n        name: Publish Snapcraft\n        with:\n          snap: ${{ steps.snap.outputs.snap-path }}\n          release: stable\n"
  },
  {
    "path": ".github/workflows/release-prod.yml",
    "content": "name: ❗ Release (prod)\n\non:\n  push:\n    branches:\n      - \"latest\"\n\njobs:\n  tests-prod:\n    name: Run all tests\n    uses: ./.github/workflows/tests.yml\n    secrets: inherit\n    with:\n      short_rte_list: false\n      pre_release: true\n\n  builds-prod:\n    name: Create all builds for release\n    uses: ./.github/workflows/build.yml\n    needs: tests-prod\n    secrets: inherit\n    with:\n      environment: \"production\"\n      target: \"all\"\n\n  virustotal-prod:\n    name: Virustotal\n    uses: ./.github/workflows/virustotal.yml\n    needs: builds-prod\n    secrets: inherit\n    with:\n      skip_report: true\n\n  aws-upload-prod:\n    name: Realse to AWS S3\n    uses: ./.github/workflows/aws-upload-prod.yml\n    needs: virustotal-prod\n    secrets: inherit\n\n  publish-stores:\n    name: Publish to stores\n    uses: ./.github/workflows/publish-stores.yml\n    needs: aws-upload-prod\n    secrets: inherit\n\n  # Remove artifacts from github actions\n  remove-artifacts:\n    name: Remove artifacts\n    needs: [aws-upload-prod]\n    if: always()\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Remove all artifacts\n        uses: ./.github/actions/remove-artifacts\n"
  },
  {
    "path": ".github/workflows/release-stage.yml",
    "content": "name: 📖 Release (stage)\n\non:\n  push:\n    branches:\n      - \"release/**\"\n\njobs:\n  tests:\n    name: Release stage tests\n    uses: ./.github/workflows/tests.yml\n    secrets: inherit\n    with:\n      short_rte_list: false\n      pre_release: true\n\n  builds:\n    name: Release stage builds\n    uses: ./.github/workflows/build.yml\n    needs: tests\n    secrets: inherit\n    with:\n      environment: \"staging\"\n      target: \"all\"\n\n  aws:\n    uses: ./.github/workflows/aws-upload-dev.yml\n    needs: [builds]\n    secrets: inherit\n    if: always()\n    with:\n      pre-release: true\n\n  # Remove artifacts from github actions\n  remove-artifacts:\n    name: Remove artifacts\n    needs: [aws]\n    if: always()\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Remove all artifacts\n        uses: ./.github/actions/remove-artifacts # Remove artifacts from github actions\n"
  },
  {
    "path": ".github/workflows/tests-backend.yml",
    "content": "name: Tests BE\non:\n  workflow_call:\n    inputs:\n      skip-electron-deps:\n        description: Skip install electron dependencies\n        type: boolean\n        default: false\n        required: false\n\nenv:\n  SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }}\n  SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }}\n  REPORT_NAME: 'report-be'\n\njobs:\n  unit-tests:\n    name: Unit tests\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install all libs and dependencies for BE\n        uses: ./.github/actions/install-all-build-libs\n        with:\n          skip-system-deps: '1'\n\n      - name: API PROD dependencies scan\n        run: |\n          FILENAME=api.prod.deps.audit.json\n\n          yarn --cwd redisinsight/api audit --groups dependencies --json > $FILENAME || true &&\n          FILENAME=$FILENAME DEPS=\"API prod\" node .github/deps-audit-report.js &&\n          curl -H \"Content-type: application/json\" --data @slack.$FILENAME -H \"Authorization: Bearer $SLACK_AUDIT_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: API DEV dependencies scan\n        run: |\n          FILENAME=api.dev.deps.audit.json\n\n          yarn --cwd redisinsight/api audit --groups devDependencies --json > $FILENAME || true &&\n          FILENAME=$FILENAME DEPS=\"API dev\" node .github/deps-audit-report.js &&\n          curl -H \"Content-type: application/json\" --data @slack.$FILENAME -H \"Authorization: Bearer $SLACK_AUDIT_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: Unit tests API\n        timeout-minutes: 20\n        run: yarn --cwd redisinsight/api/ test:cov --ci --silent\n\n      - name: Upload Test Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: ${{ env.REPORT_NAME }}\n          path: redisinsight/api/report\n\n      - name: Get current date\n        id: date\n        if: always()\n        uses: ./.github/actions/get-current-date\n\n      - name: Deploy report\n        uses: ./.github/actions/deploy-test-reports\n        if: always()\n        with:\n          group: 'report'\n          path: '${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}'\n          AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n          AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n          AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/tests-e2e-appimage.yml",
    "content": "name: Tests E2E AppImage\non:\n  workflow_call:\n    inputs:\n      report:\n        description: Send report to Slack\n        required: false\n        default: false\n        type: boolean\n\n      debug:\n        description: Send report to Slack\n        required: false\n        default: false\n        type: boolean\n\nenv:\n  E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n  E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n  E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n  E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n  E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n  E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n  E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n  E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n  RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }}\n  RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n  RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n  SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }}\n  TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n  DBUS_SESSION_BUS_ADDRESS: ${{ vars.DBUS_SESSION_BUS_ADDRESS }}\n  DISPLAY: ${{ vars.DISPLAY }}\n  APPIMAGE_PATH: ${{ vars.APPIMAGE_PATH }}\n  REPORT_NAME: 'report-appimage'\n\njobs:\n  e2e-tests-appimage:\n    runs-on: ubuntu-22.04\n    name: E2E AppImage tests\n    environment:\n      name: production\n    steps:\n      - uses: actions/checkout@v4\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Install necessary packages\n        run: |\n          sudo apt-get update -y\n          sudo apt-get install kmod libfuse2 xvfb net-tools xdotool desktop-file-utils fluxbox netcat -y\n\n      - name: Install Google Chrome\n        run: |\n          wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -\n          sudo sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google-chrome.list'\n          sudo apt-get update\n          sudo apt-get install -y google-chrome-stable libnss3 libgconf-2-4 libxss1 libasound2\n          xdg-settings set default-web-browser google-chrome.desktop\n\n      - name: Download AppImage Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: linux-builds\n          path: ./release\n\n      - name: Start Xvfb\n        run: |\n          if [ -f /tmp/.X99-lock ]; then rm /tmp/.X99-lock; fi\n          Xvfb :99 -ac -screen 0 1920x1080x24 &\n          sleep 3\n          fluxbox &\n\n      - name: Run tests\n        timeout-minutes: 60\n        run: |\n          .github/e2e/test.app-image.sh\n\n      - name: Upload Test Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: ${{ env.REPORT_NAME }}\n          path: tests/e2e/report\n\n      - name: Send report to Slack\n        if: inputs.report && always()\n        run: |\n          APP_BUILD_TYPE=\"Electron (Linux)\" node ./.github/e2e-results.js\n          curl -H \"Content-type: application/json\" --data @e2e.report.json -H \"Authorization: Bearer $SLACK_TEST_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: Generate test results\n        uses: dorny/test-reporter@v1\n        if: always()\n        with:\n          name: 'Test results: E2E (AppImage)'\n          path: tests/e2e/results/results.xml\n          reporter: java-junit\n          list-tests: 'failed'\n          list-suites: 'failed'\n          fail-on-error: 'false'\n\n      - name: Get current date\n        id: date\n        if: always()\n        uses: ./.github/actions/get-current-date\n\n      # Deploy report to AWS test bucket\n      - name: Deploy report\n        uses: ./.github/actions/deploy-test-reports\n        if: always()\n        with:\n          group: 'report'\n          path: '${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}'\n          AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n          AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n          AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/tests-e2e-docker-critical-path.yml",
    "content": "name: \"E2E: Critical Path (Docker)\"\n\non:\n  workflow_call:\n    inputs:\n      report:\n        description: Send report to Slack\n        default: false\n        type: boolean\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\nenv:\n  E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n  E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n  E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n  E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n  E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n  E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n  E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n  E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n  RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }}\n  RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n  RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n  SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }}\n  TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n  E2E_VOLUME_PATH: '/usr/src/app'\n  REPORT_NAME: 'report-critical-path-docker-node'\n  E2E_SLOWEST_SERVICE: 'oss-standalone-big:6379'\n\njobs:\n  e2e-docker-tests:\n    runs-on: ubuntu-latest\n    name: E2E Docker tests\n    strategy:\n      fail-fast: false\n      matrix:\n        # Number of threads to run tests\n        parallel: [1, 2, 3, 4]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get current date\n        id: date\n        uses: ./.github/actions/get-current-date\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Download Docker Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: docker-builds\n          path: ./release\n\n      - name: Load built docker image from workspace\n        run: |\n          docker image load -i ./release/docker/docker-linux-alpine.amd64.tar\n\n      - name: Generate short list of the test files\n        working-directory: ./tests/e2e\n        run: |\n          export NODE_INDEX=${{ matrix.parallel }}\n          testFiles=$(find tests/web/critical-path -type f -name '*.e2e.ts' \\\n            | sort \\\n            | awk \"NR % 4 == $(( ${{ matrix.parallel }} - 1 ))\")\n\n          echo \"Selected files:\"\n          echo \"$testFiles\"\n\n          # Multi-Line value\n          echo \"TEST_FILES<<EOF\" >> $GITHUB_ENV\n          echo \"$testFiles\" >> $GITHUB_ENV\n          echo \"EOF\" >> $GITHUB_ENV\n\n      - name: Build web images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          build\n\n      - name: Pull RTE images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.critical-path.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          pull\n\n      - name: Build RTE images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.critical-path.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          build\n\n      - name: Run tests\n        timeout-minutes: 80\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.critical-path.docker-compose.yml \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          up --abort-on-container-exit --force-recreate\n\n      - name: Upload Test Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: ${{ env.REPORT_NAME }}-${{ matrix.parallel }}\n          path: /usr/src/app/report\n\n      - name: Send report to Slack\n        if: inputs.report && always()\n        run: |\n          node ./.github/e2e-results.js\n          curl -H \"Content-type: application/json\" --data @e2e.report.json -H \"Authorization: Bearer $SLACK_TEST_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: Deploy report\n        if: always()\n        uses: ./.github/actions/deploy-test-reports\n        with:\n          group: 'report'\n          path: '${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}-${{ matrix.parallel }}'\n          AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n          AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n          AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/tests-e2e-docker-regression.yml",
    "content": "name: \"E2E: Regression (Docker)\"\n\non:\n  workflow_call:\n    inputs:\n      report:\n        description: Send report to Slack\n        default: false\n        type: boolean\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\nenv:\n  E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n  E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n  E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n  E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n  E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n  E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n  E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n  E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n  RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }}\n  RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n  RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n  SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }}\n  TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n  E2E_VOLUME_PATH: '/usr/src/app'\n  REPORT_NAME: 'report-regression-docker-node'\n  E2E_SLOWEST_SERVICE: 'oss-standalone-big:6379'\n\njobs:\n  e2e-docker-tests:\n    runs-on: ubuntu-latest\n    name: E2E Docker tests\n    strategy:\n      fail-fast: false\n      matrix:\n        # Number of threads to run tests\n        parallel: [1, 2, 3, 4]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get current date\n        id: date\n        uses: ./.github/actions/get-current-date\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Download Docker Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: docker-builds\n          path: ./release\n\n      - name: Load built docker image from workspace\n        run: |\n          docker image load -i ./release/docker/docker-linux-alpine.amd64.tar\n\n      - name: Generate short list of the test files\n        working-directory: ./tests/e2e\n        run: |\n          export NODE_INDEX=${{ matrix.parallel }}\n          testFiles=$(find tests/web/regression -type f -name '*.e2e.ts' \\\n            | sort \\\n            | awk \"NR % 4 == $(( ${{ matrix.parallel }} - 1 ))\")\n\n          echo \"Selected files:\"\n          echo \"$testFiles\"\n\n          # Multi-Line value\n          echo \"TEST_FILES<<EOF\" >> $GITHUB_ENV\n          echo \"$testFiles\" >> $GITHUB_ENV\n          echo \"EOF\" >> $GITHUB_ENV\n\n      - name: Build web images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          build\n\n      - name: Pull RTE images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.regression.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          pull\n\n      - name: Build RTE images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.regression.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          build\n\n      - name: Run tests\n        timeout-minutes: 80\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.regression.docker-compose.yml \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          up --abort-on-container-exit --force-recreate\n\n      - name: Upload Test Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: ${{ env.REPORT_NAME }}-${{ matrix.parallel }}\n          path: /usr/src/app/report\n\n      - name: Send report to Slack\n        if: inputs.report && always()\n        run: |\n          node ./.github/e2e-results.js\n          curl -H \"Content-type: application/json\" --data @e2e.report.json -H \"Authorization: Bearer $SLACK_TEST_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: Deploy report\n        if: always()\n        uses: ./.github/actions/deploy-test-reports\n        with:\n          group: 'report'\n          path: '${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}-${{ matrix.parallel }}'\n          AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n          AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n          AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/tests-e2e-docker-smoke.yml",
    "content": "name: \"E2E: Smoke (Docker)\"\n\non:\n  workflow_call:\n    inputs:\n      report:\n        description: Send report to Slack\n        default: false\n        type: boolean\n\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\n\nenv:\n  E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n  E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n  E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n  E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n  E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n  E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n  E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n  E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n  RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }}\n  RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n  RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n  SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }}\n  TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n  E2E_VOLUME_PATH: '/usr/src/app'\n  REPORT_NAME: 'report-smoke-docker-node'\n\njobs:\n  e2e-docker-tests:\n    runs-on: ubuntu-latest\n    name: E2E Docker tests\n    strategy:\n      fail-fast: false\n      matrix:\n        # Number of threads to run tests\n        parallel: [1]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Get current date\n        id: date\n        uses: ./.github/actions/get-current-date\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Download Docker Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: docker-builds\n          path: ./release\n\n      - name: Load built docker image from workspace\n        run: |\n          docker image load -i ./release/docker/docker-linux-alpine.amd64.tar\n\n      - name: Generate short list of the test files\n        working-directory: ./tests/e2e\n        run: |\n          export NODE_INDEX=${{ matrix.parallel }}\n\n          testFiles=\"tests/web/smoke/**/*.e2e.ts\"\n\n          echo \"Selected files:\"\n          echo \"$testFiles\"\n\n          # Multi-Line value\n          echo \"TEST_FILES<<EOF\" >> $GITHUB_ENV\n          echo \"$testFiles\" >> $GITHUB_ENV\n          echo \"EOF\" >> $GITHUB_ENV\n\n      - name: Build web images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          build\n\n      - name: Pull RTE images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.smoke.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          pull\n\n      - name: Build RTE images\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.smoke.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          build\n\n      - name: Run tests\n        timeout-minutes: 80\n        run: |\n          docker compose --profile e2e \\\n          -f tests/e2e/rte.smoke.docker-compose.yml \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          -f tests/e2e/rte.networks.docker-compose.yml \\\n          up --abort-on-container-exit --force-recreate\n\n      - name: Upload Test Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: ${{ env.REPORT_NAME }}-${{ matrix.parallel }}\n          path: /usr/src/app/report\n\n      - name: Send report to Slack\n        if: inputs.report && always()\n        run: |\n          node ./.github/e2e-results.js\n          curl -H \"Content-type: application/json\" --data @e2e.report.json -H \"Authorization: Bearer $SLACK_TEST_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: Deploy report\n        if: always()\n        uses: ./.github/actions/deploy-test-reports\n        with:\n          group: 'report'\n          path: '${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}-${{ matrix.parallel }}'\n          AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n          AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n          AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/tests-e2e-playwright-chromium.yml",
    "content": "name: E2E Playwright - Chromium (Dev)\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: 'Environment to run tests'\n        type: string\n        default: 'staging'\n      debug:\n        description: 'Enable SSH debugging'\n        type: boolean\n        default: false\n\nenv:\n  E2E_DIR: './tests/e2e-playwright'\n\njobs:\n  e2e-chromium:\n    name: E2E Chromium (Local Dev)\n    runs-on: ubuntu-latest\n    environment: ${{ inputs.environment }}\n    timeout-minutes: 60\n    env:\n      E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n      E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n      E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n      E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n      E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n      E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n      E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n      E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n      RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n      TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Enable SSH Debug\n        if: ${{ inputs.debug }}\n        uses: mxschmitt/action-tmate@v3\n        with:\n          detached: true\n\n      - name: Install application dependencies\n        uses: ./.github/actions/install-all-build-libs\n\n      - name: Setup E2E environment\n        uses: ./.github/actions/setup-e2e-playwright\n        with:\n          browsers: 'chromium'\n          working-directory: ${{ env.E2E_DIR }}\n\n      - name: Build statics\n        run: yarn build:statics\n\n      - name: Build API\n        run: yarn --cwd redisinsight/api build\n\n      - name: Start application (dev mode)\n        run: |\n          yarn dev:api &\n          yarn dev:ui &\n          # Wait for the app to be ready\n          npx wait-on http://localhost:8080 http://localhost:5540/api/health --timeout 60000\n\n      - name: Start Redis test environment\n        run: |\n          TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \\\n          docker compose -p e2e-rte -f tests/e2e/rte.docker-compose.yml up --detach --force-recreate\n\n      - name: Run Playwright tests (Chromium)\n        working-directory: ${{ env.E2E_DIR }}\n        run: npx playwright test --project=chromium --project=chromium-serial\n        env:\n          CI: true\n\n      - name: Upload test results\n        if: ${{ !cancelled() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: playwright-report-chromium\n          path: |\n            ${{ env.E2E_DIR }}/test-results\n            ${{ env.E2E_DIR }}/playwright-report\n          retention-days: 14\n\n      - name: Stop application\n        if: always()\n        run: |\n          # Kill any node processes running the app\n          pkill -f \"dist/src/main\" || true\n          pkill -f \"vite dev\" || true\n\n      - name: Stop Redis test environment\n        if: always()\n        run: |\n          docker compose -p e2e-rte -f tests/e2e/rte.docker-compose.yml down --volumes --remove-orphans\n\n"
  },
  {
    "path": ".github/workflows/tests-e2e-playwright-docker.yml",
    "content": "name: E2E Playwright - Docker\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: 'Environment to run tests'\n        type: string\n        default: 'staging'\n      debug:\n        description: 'Enable SSH debugging'\n        type: boolean\n        default: false\n\nenv:\n  E2E_DIR: './tests/e2e-playwright'\n\njobs:\n  e2e-docker:\n    name: E2E Docker (Chromium)\n    runs-on: ubuntu-latest\n    environment: ${{ inputs.environment }}\n    timeout-minutes: 60\n    env:\n      E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n      E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n      E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n      E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n      E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n      E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n      E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n      E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n      RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n      TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Enable SSH Debug\n        if: ${{ inputs.debug }}\n        uses: mxschmitt/action-tmate@v3\n        with:\n          detached: true\n\n      - name: Setup E2E environment\n        uses: ./.github/actions/setup-e2e-playwright\n        with:\n          browsers: 'chromium'\n          working-directory: ${{ env.E2E_DIR }}\n\n      - name: Download Docker Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: docker-builds\n          path: ./release\n\n      - name: Load built docker image\n        run: |\n          docker image load -i ./release/docker/docker-linux-alpine.amd64.tar\n\n      - name: Start Redis test environment\n        run: |\n          TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \\\n          docker compose -p e2e-rte -f tests/e2e/rte.docker-compose.yml up --detach --force-recreate\n\n      - name: Start RedisInsight Docker container\n        run: |\n          E2E_RI_ENCRYPTION_KEY=\"$E2E_RI_ENCRYPTION_KEY\" \\\n          docker compose -p e2e-ri-docker \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          up --detach --force-recreate\n          # Wait for the app to be ready\n          sleep 10\n          npx wait-on http://localhost:5540/api/health --timeout 60000\n\n      - name: Run Playwright tests (Chromium - Docker)\n        working-directory: ${{ env.E2E_DIR }}\n        run: npx playwright test --project=chromium\n        env:\n          CI: true\n          # Override URLs to point to Docker container (serves both client and API on 5540)\n          RI_CLIENT_URL: 'http://localhost:5540'\n          RI_API_URL: 'http://localhost:5540'\n          # Use host.docker.internal for Redis hosts so the Docker container can reach them\n          # The Redis test environment exposes ports to the host, and the Docker container\n          # uses extra_hosts to map host.docker.internal to the host gateway\n          OSS_STANDALONE_HOST: 'host.docker.internal'\n          OSS_STANDALONE_V5_HOST: 'host.docker.internal'\n          OSS_STANDALONE_V7_HOST: 'host.docker.internal'\n          OSS_STANDALONE_V8_HOST: 'host.docker.internal'\n          OSS_STANDALONE_EMPTY_HOST: 'host.docker.internal'\n          OSS_STANDALONE_BIG_HOST: 'host.docker.internal'\n          OSS_STANDALONE_TLS_HOST: 'host.docker.internal'\n          OSS_CLUSTER_HOST: 'host.docker.internal'\n\n      - name: Upload test results\n        if: ${{ !cancelled() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: playwright-report-docker\n          path: |\n            ${{ env.E2E_DIR }}/test-results\n            ${{ env.E2E_DIR }}/playwright-report\n          retention-days: 14\n\n      - name: Stop RedisInsight Docker container\n        if: always()\n        run: |\n          docker compose -p e2e-ri-docker \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          down --volumes --remove-orphans\n\n      - name: Stop Redis test environment\n        if: always()\n        run: |\n          docker compose -p e2e-rte -f tests/e2e/rte.docker-compose.yml down --volumes --remove-orphans\n\n"
  },
  {
    "path": ".github/workflows/tests-e2e-playwright-electron.yml",
    "content": "name: E2E Playwright - Electron (Linux)\n\non:\n  workflow_call:\n    inputs:\n      environment:\n        description: 'Environment to run tests'\n        type: string\n        default: 'staging'\n      debug:\n        description: 'Enable SSH debugging'\n        type: boolean\n        default: false\n\nenv:\n  E2E_DIR: './tests/e2e-playwright'\n\njobs:\n  e2e-electron:\n    name: E2E Electron (Linux)\n    # Use ubuntu-22.04 to match the old workflow (ubuntu-latest is 24.04)\n    runs-on: ubuntu-22.04\n    environment: ${{ inputs.environment }}\n    timeout-minutes: 60\n    env:\n      E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n      E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n      E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n      E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n      E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n      E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n      E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n      E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n      RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n      TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n      # Environment variables needed for Electron/AppImage on Linux (from repository variables)\n      DBUS_SESSION_BUS_ADDRESS: ${{ vars.DBUS_SESSION_BUS_ADDRESS || 'unix:path=/dev/null' }}\n      DISPLAY: ${{ vars.DISPLAY || ':99' }}\n      # TLS certificates for Electron (uses HTTPS)\n      RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n      RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Enable SSH Debug\n        if: ${{ inputs.debug }}\n        uses: mxschmitt/action-tmate@v3\n        with:\n          detached: true\n\n      # Kill any existing Redis processes (like the old workflow does)\n      - name: Clean up any existing processes\n        run: |\n          pkill -f Redis* || true\n          pkill -f redisinsight || true\n\n      - name: Install system dependencies for Electron/AppImage\n        run: |\n          sudo apt-get update -y\n          sudo apt-get install -y kmod libfuse2 xvfb net-tools xdotool desktop-file-utils fluxbox netcat\n\n      - name: Setup E2E environment\n        uses: ./.github/actions/setup-e2e-playwright\n        with:\n          browsers: 'chromium'\n          working-directory: ${{ env.E2E_DIR }}\n\n      - name: Download Linux build artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: linux-builds\n          path: ./release\n\n      - name: Mount AppImage and get executable path\n        id: appimage\n        run: |\n          ls -la ./release/\n          chmod +x ./release/Redis-Insight*.AppImage\n\n          # Get the original AppImage file path\n          APPIMAGE_FILE=$(realpath ./release/Redis-Insight*.AppImage | head -1)\n          echo \"appimage_file=$APPIMAGE_FILE\" >> $GITHUB_OUTPUT\n          echo \"Original AppImage: $APPIMAGE_FILE\"\n\n          # Mount the AppImage and capture the mount path\n          rm -f appimage_mount_path\n          ./release/Redis-Insight*.AppImage --appimage-mount > appimage_mount_path &\n          APPIMAGE_PID=$!\n          sleep 3\n\n          # Get the mount path and construct the executable path\n          MOUNT_PATH=$(cat appimage_mount_path)\n          echo \"AppImage mounted at: $MOUNT_PATH\"\n          ls -la \"$MOUNT_PATH\"\n\n          # The actual electron executable is inside the mounted AppImage\n          ELECTRON_PATH=\"$MOUNT_PATH/redisinsight\"\n          echo \"path=$ELECTRON_PATH\" >> $GITHUB_OUTPUT\n          echo \"pid=$APPIMAGE_PID\" >> $GITHUB_OUTPUT\n          echo \"Found Electron executable: $ELECTRON_PATH\"\n\n          # Verify the executable exists\n          if [ -f \"$ELECTRON_PATH\" ]; then\n            echo \"✓ Electron executable exists\"\n          else\n            echo \"✗ Electron executable not found!\"\n            ls -la \"$MOUNT_PATH\"\n            exit 1\n          fi\n\n      # Create folders before tests run to prevent permissions issues (like the old workflow)\n      - name: Create test folders\n        run: |\n          mkdir -p tests/e2e/remote\n          mkdir -p tests/e2e/rdi\n\n      - name: Start Redis test environment\n        run: |\n          TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \\\n          docker compose -p e2e-rte -f tests/e2e/rte.docker-compose.yml up --detach --force-recreate\n\n      - name: Start Xvfb\n        run: |\n          # Extract display number from DISPLAY env var (e.g., \":99\" -> \"99\")\n          DISPLAY_NUM=\"${DISPLAY#:}\"\n          DISPLAY_NUM=\"${DISPLAY_NUM:-99}\"\n          echo \"Starting Xvfb on display :$DISPLAY_NUM\"\n          if [ -f /tmp/.X${DISPLAY_NUM}-lock ]; then rm /tmp/.X${DISPLAY_NUM}-lock; fi\n          Xvfb :${DISPLAY_NUM} -ac -screen 0 1920x1080x24 &\n          sleep 3\n          fluxbox &\n\n      - name: Run Playwright tests (Electron Linux)\n        working-directory: ${{ env.E2E_DIR }}\n        run: npx playwright test --project=electron\n        env:\n          CI: true\n          ELECTRON_EXECUTABLE_PATH: ${{ steps.appimage.outputs.path }}\n          APPIMAGE: ${{ steps.appimage.outputs.appimage_file }}\n          # Electron uses HTTPS (with TLS certificates)\n          RI_ELECTRON_API_URL: 'https://localhost:5530'\n          RI_SOCKETS_CORS: 'true'\n          RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n          RI_ENCRYPTION_KEYTAR: 'false'\n          # Disable TLS certificate validation for self-signed certs (like old tests do)\n          NODE_TLS_REJECT_UNAUTHORIZED: '0'\n\n      - name: Upload test results\n        if: ${{ !cancelled() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: playwright-report-electron-linux\n          path: |\n            ${{ env.E2E_DIR }}/test-results\n            ${{ env.E2E_DIR }}/playwright-report\n          retention-days: 14\n\n      - name: Unmount AppImage\n        if: always()\n        run: |\n          # Kill the AppImage mount process\n          kill ${{ steps.appimage.outputs.pid }} || true\n\n      - name: Stop Redis test environment\n        if: always()\n        run: |\n          docker compose -p e2e-rte -f tests/e2e/rte.docker-compose.yml down --volumes --remove-orphans\n\n"
  },
  {
    "path": ".github/workflows/tests-e2e-playwright-lint.yml",
    "content": "name: E2E Playwright - Lint & Type Check\n\non:\n  workflow_call:\n\nenv:\n  E2E_DIR: './tests/e2e-playwright'\n\njobs:\n  lint:\n    name: Lint & Type Check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup E2E environment\n        uses: ./.github/actions/setup-e2e-playwright\n        with:\n          working-directory: ${{ env.E2E_DIR }}\n          browsers: 'none'\n\n      - name: Lint E2E code\n        working-directory: ${{ env.E2E_DIR }}\n        run: npm run lint\n\n      - name: Check formatting\n        working-directory: ${{ env.E2E_DIR }}\n        run: npm run format:check\n\n      - name: Type check E2E code\n        working-directory: ${{ env.E2E_DIR }}\n        run: npm run type-check\n\n"
  },
  {
    "path": ".github/workflows/tests-e2e-playwright-v2.yml",
    "content": "name: E2E Playwright Tests (v2)\n\non:\n  # Manual trigger\n  workflow_dispatch:\n    inputs:\n      environment:\n        description: 'Environment to run tests'\n        type: choice\n        options:\n          - staging\n        default: 'staging'\n      debug:\n        description: 'Enable SSH debugging'\n        type: boolean\n        default: false\n\n  # Trigger on PR with label (remove and re-add label to rerun)\n  pull_request:\n    types: [labeled]\n\n  # Nightly schedule (0 AM UTC)\n  schedule:\n    - cron: '0 0 * * *'\n\n# Cancel in-progress runs for the same PR/branch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  # Check if workflow should run (for PR label trigger)\n  check-trigger:\n    runs-on: ubuntu-latest\n    outputs:\n      should_run: ${{ steps.check.outputs.should_run }}\n    steps:\n      - name: Check trigger conditions\n        id: check\n        env:\n          EVENT_NAME: ${{ github.event_name }}\n          HAS_E2E_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'e2e-tests') }}\n          HAS_RUN_ALL_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'run-all-tests') }}\n        run: |\n          if [[ \"$EVENT_NAME\" == \"workflow_dispatch\" ]]; then\n            echo \"should_run=true\" >> $GITHUB_OUTPUT\n          elif [[ \"$EVENT_NAME\" == \"schedule\" ]]; then\n            echo \"should_run=true\" >> $GITHUB_OUTPUT\n          elif [[ \"$EVENT_NAME\" == \"pull_request\" ]]; then\n            if [[ \"$HAS_E2E_LABEL\" == \"true\" || \"$HAS_RUN_ALL_LABEL\" == \"true\" ]]; then\n              echo \"should_run=true\" >> $GITHUB_OUTPUT\n            else\n              echo \"should_run=false\" >> $GITHUB_OUTPUT\n            fi\n          else\n            echo \"should_run=false\" >> $GITHUB_OUTPUT\n          fi\n\n  # Lint and type-check E2E code\n  lint:\n    name: Lint\n    needs: check-trigger\n    if: needs.check-trigger.outputs.should_run == 'true'\n    uses: ./.github/workflows/tests-e2e-playwright-lint.yml\n\n  # Chromium tests (local dev mode) - no build required\n  e2e-dev-chromium:\n    name: Chromium (Dev)\n    needs: lint\n    uses: ./.github/workflows/tests-e2e-playwright-chromium.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment || 'staging' }}\n      debug: ${{ inputs.debug || false }}\n\n  # Build Docker image for Docker tests\n  build-docker:\n    name: Build Docker\n    needs: lint\n    uses: ./.github/workflows/pipeline-build-docker.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n      for_e2e_tests: true\n\n  # Docker tests (Chromium against Docker image)\n  e2e-docker:\n    name: Docker\n    needs: build-docker\n    uses: ./.github/workflows/tests-e2e-playwright-docker.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment || 'staging' }}\n      debug: ${{ inputs.debug || false }}\n\n  # Build Linux AppImage for Electron tests\n  build-linux:\n    name: Build Linux\n    needs: lint\n    uses: ./.github/workflows/pipeline-build-linux.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment || 'staging' }}\n      target: 'build_linux_AppImage'\n      debug: ${{ inputs.debug || false }}\n      enterprise: false\n\n  # Electron tests (Linux AppImage)\n  e2e-electron:\n    name: Electron (Linux)\n    needs: build-linux\n    uses: ./.github/workflows/tests-e2e-playwright-electron.yml\n    secrets: inherit\n    with:\n      environment: ${{ inputs.environment || 'staging' }}\n      debug: ${{ inputs.debug || false }}\n\n"
  },
  {
    "path": ".github/workflows/tests-e2e-playwright.yml",
    "content": "name: Playwright E2E Tests\non:\n  workflow_call:\n    inputs:\n      debug:\n        description: SSH Debug\n        default: false\n        type: boolean\nenv:\n  E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }}\n  E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }}\n  E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }}\n  E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }}\n  E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }}\n  E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }}\n  E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }}\n\n  E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }}\n  RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }}\n  RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }}\n  RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }}\n  TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n  E2E_VOLUME_PATH: '/usr/src/app'\n\njobs:\n  e2e-playwright-chromium-docker:\n    name: E2E Playwright Chromium Docker Build Tests\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install dependencies for Playwright tests\n        uses: ./.github/actions/install-deps\n        with:\n          dir-path: './tests/playwright'\n\n      - name: Install Playwright Browsers\n        working-directory: ./tests/playwright\n        run: yarn playwright install --with-deps\n\n      - name: Download Docker Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: docker-builds\n          path: ./release\n\n      - name: Load built docker image from workspace\n        run: |\n          docker image load -i ./release/docker/docker-linux-alpine.amd64.tar\n\n      - name: Set up redis test environments\n        run: |\n          TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \\\n          docker compose -p e2e-rte \\\n          -f tests/e2e/rte.docker-compose.yml \\\n          up --detach --force-recreate\n\n      - name: Set up RI docker image\n        run: |\n          E2E_RI_ENCRYPTION_KEY=\"$E2E_RI_ENCRYPTION_KEY\" \\\n          RI_SERVER_TLS_CERT=\"$RI_SERVER_TLS_CERT\" \\\n          RI_SERVER_TLS_KEY=\"$RI_SERVER_TLS_KEY\" \\\n          docker compose -p e2e-ri-docker \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          up --detach --force-recreate\n          sleep 30\n\n      - name: Run Playwright tests\n        timeout-minutes: 80\n        working-directory: ./tests/playwright\n        if: ${{ !cancelled() }}\n        run: |\n          yarn test:chromium:docker\n\n      - uses: actions/upload-artifact@v4\n        if: ${{ !cancelled() }}\n        with:\n          name: playwright-report\n          path: |\n            ./tests/playwright/test-results\n            ./tests/playwright/allure-results\n            ./tests/playwright/playwright-report\n          retention-days: 10\n\n      - name: Clean up redis test environments\n        if: always()\n        run: |\n          docker compose -p e2e-rte \\\n          -f tests/e2e/rte.docker-compose.yml \\\n          down --volumes --remove-orphans\n\n      - name: Clean up RI docker image\n        if: always()\n        run: |\n          docker compose -p e2e-ri-docker \\\n          -f tests/e2e/docker.web.docker-compose.yml \\\n          down --volumes --remove-orphans\n"
  },
  {
    "path": ".github/workflows/tests-e2e.yml",
    "content": "name: ✅ E2E Tests\n\non:\n  workflow_dispatch:\n    inputs:\n      debug:\n        description: Enable SSH Debug (IT and E2E)\n        default: false\n        type: boolean\n        required: false\n\npermissions:\n  actions: read\n  contents: read\n\n# Cancel a previous run workflow\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}-e2e\n  cancel-in-progress: true\n\njobs:\n  build-docker:\n    uses: ./.github/workflows/pipeline-build-docker.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n      for_e2e_tests: true\n\n  build-appimage:\n    uses: ./.github/workflows/pipeline-build-linux.yml\n    secrets: inherit\n    with:\n      target: build_linux_appimage_x64\n      debug: ${{ inputs.debug || false }}\n\n  e2e-docker-tests-smoke:\n    name: \"E2E: Smoke (Docker)\"\n    needs: build-docker\n    uses: ./.github/workflows/tests-e2e-docker-smoke.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n\n  e2e-docker-tests-critical-path:\n    name: \"E2E: Critical Path (Docker)\"\n    needs: build-docker\n    uses: ./.github/workflows/tests-e2e-docker-critical-path.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n\n  e2e-docker-tests-regression:\n    name: \"E2E: Regression (Docker)\"\n    needs: build-docker\n    uses: ./.github/workflows/tests-e2e-docker-regression.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n\n  tests-e2e-playwright:\n    needs: build-docker\n    uses: ./.github/workflows/tests-e2e-playwright.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n\n  e2e-appimage-tests:\n    needs: build-appimage\n    uses: ./.github/workflows/tests-e2e-appimage.yml\n    secrets: inherit\n    with:\n      debug: ${{ inputs.debug || false }}\n\n  clean:\n    uses: ./.github/workflows/clean-deployments.yml\n    needs:\n      [\n        e2e-docker-tests-smoke,\n        e2e-docker-tests-critical-path,\n        e2e-docker-tests-regression,\n        e2e-appimage-tests,\n        tests-e2e-playwright,\n      ]\n\n  # Remove artifacts from github actions\n  remove-artifacts:\n    name: Remove artifacts\n    needs:\n      [\n        e2e-docker-tests-smoke,\n        e2e-docker-tests-critical-path,\n        e2e-docker-tests-regression,\n        e2e-appimage-tests,\n        tests-e2e-playwright,\n      ]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Remove all artifacts\n        uses: ./.github/actions/remove-artifacts\n"
  },
  {
    "path": ".github/workflows/tests-frontend.yml",
    "content": "name: Tests UI\non:\n  workflow_call:\n\nenv:\n  SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }}\n  SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }}\n  REPORT_NAME: 'report-fe'\n\njobs:\n  unit-tests:\n    runs-on: ubuntu-latest\n    name: Frontend tests\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install all libs and dependencies\n        uses: ./.github/actions/install-all-build-libs\n        with:\n          skip-system-deps: '1'\n\n      - name: UI PROD dependencies audit\n        run: |\n          FILENAME=ui.prod.deps.audit.json\n\n          yarn audit --groups dependencies --json > $FILENAME || true &&\n          FILENAME=$FILENAME DEPS=\"UI prod\" node .github/deps-audit-report.js &&\n          curl -H \"Content-type: application/json\" --data @slack.$FILENAME -H \"Authorization: Bearer $SLACK_AUDIT_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: UI DEV dependencies audit\n        run: |\n          FILENAME=ui.dev.deps.audit.json\n\n          yarn audit --groups devDependencies --json > $FILENAME || true &&\n          FILENAME=$FILENAME DEPS=\"UI dev\" node .github/deps-audit-report.js &&\n          curl -H \"Content-type: application/json\" --data @slack.$FILENAME -H \"Authorization: Bearer $SLACK_AUDIT_REPORT_KEY\" -X POST https://slack.com/api/chat.postMessage\n\n      - name: Unit tests UI\n        timeout-minutes: 30\n        run: yarn test:cov --ci --silent\n\n      - name: Upload Test Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: ${{ env.REPORT_NAME }}\n          path: report\n\n      - name: Get current date\n        id: date\n        if: always()\n        uses: ./.github/actions/get-current-date\n\n      - name: Deploy report\n        uses: ./.github/actions/deploy-test-reports\n        if: always()\n        with:\n          group: 'report'\n          path: '${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}'\n          AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }}\n          AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}\n          AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n"
  },
  {
    "path": ".github/workflows/tests-integration.yml",
    "content": "name: Integration tests\non:\n  workflow_call:\n    inputs:\n      build:\n        description: Backend build to run tests over\n        type: string\n        default: 'local'\n      redis_client:\n        description: Library to use for redis connection\n        type: string\n        default: 'ioredis'\n      report:\n        description: Send report for test run to slack\n        type: boolean\n        default: false\n      short_rte_list:\n        description: Use short rte list\n        type: boolean\n        default: false\n      debug:\n        description: SSH Debug\n        type: boolean\n        default: false\n  workflow_dispatch:\n    inputs:\n      build:\n        description: Backend build to run tests over\n        type: string\n        default: 'local'\n      redis_client:\n        description: Library to use for redis connection\n        type: string\n        default: 'ioredis'\n      report:\n        description: Send report for test run to slack\n        type: boolean\n        default: false\n      short_rte_list:\n        description: Use short rte list\n        type: boolean\n        default: false\n      debug:\n        description: SSH Debug\n        type: boolean\n        default: false\nenv:\n  SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }}\n  SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }}\n  TEST_MEDIUM_DB_DUMP: ${{ secrets.TEST_MEDIUM_DB_DUMP }}\n  TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }}\n  REPORT_NAME: 'report-it'\n  #  Disabled RTE for now. Need to prepare reasonable dataset to not take so much time for tests.\n  #  \"oss-st-big\": \"OSS Standalone v6 and all modules and predefined amount of data inside (~3-4M)\",\n  ITESTS_NAMES: |\n    {\n      \"oss-st-5\": \"OSS Standalone v5\",\n      \"oss-st-5-pass\": \"OSS Standalone v5 with admin pass required\",\n      \"oss-st-6\": \"OSS Standalone v6 and all modules\",\n      \"mods-preview\": \"OSS Standalone and all preview modules\",\n      \"oss-st-6-tls\": \"OSS Standalone v6 with TLS enabled\",\n      \"oss-st-6-tls-auth\": \"OSS Standalone v6 with TLS auth required\",\n      \"oss-clu\": \"OSS Cluster\",\n      \"oss-clu-tls\": \"OSS Cluster with TLS enabled\",\n      \"oss-sent\": \"OSS Sentinel\",\n      \"oss-sent-tls-auth\": \"OSS Sentinel with TLS auth\",\n      \"re-st\": \"Redis Enterprise with Standalone inside\",\n      \"re-clu\": \"Redis Enterprise with Cluster inside\",\n      \"re-crdt\": \"Redis Enterprise with active-active database inside\"\n    }\n  ITESTS_NAMES_SHORT: |\n    {\n      \"mods-preview\": \"OSS Standalone and all preview modules\",\n      \"oss-st-5-pass\": \"OSS Standalone v5 with admin pass required\",\n      \"oss-st-6-tls-auth\": \"OSS Standalone v6 with TLS auth required\",\n      \"oss-clu-tls\": \"OSS Cluster with TLS enabled\",\n      \"re-crdt\": \"Redis Enterprise with active-active database inside\",\n      \"oss-sent-tls-auth\": \"OSS Sentinel with TLS auth\"\n    }\n\njobs:\n  set-matrix:\n    runs-on: ubuntu-latest\n    outputs:\n      matrix: ${{ steps.parse-matrix.outputs.matrix }}\n    steps:\n      - name: Create JSON array for run-tests matrix\n        id: parse-matrix\n        run: |\n          # Extract the JSON object from the environment variable\n          MATRIX_JSON=\"$ITESTS_NAMES_SHORT\"\n\n          if [ \"${{ inputs.short_rte_list }}\" == \"false\" ]; then\n            MATRIX_JSON=\"$ITESTS_NAMES\"\n          fi\n\n          MATRIX_ARRAY=$(echo \"$MATRIX_JSON\" | jq -c 'keys')\n\n          # Output the formed JSON array for use in other jobs\n          echo \"matrix=$MATRIX_ARRAY\" >> $GITHUB_OUTPUT\n\n      - name: Verify the formed matrix array\n        run: |\n          echo \"Formed matrix array:\"\n          echo \"${{ steps.parse-matrix.outputs.matrix }}\"\n\n  run-tests:\n    name: ITest\n    runs-on: ubuntu-latest\n    needs: set-matrix\n    environment:\n      name: production\n    strategy:\n      fail-fast: false\n      matrix:\n        rte: ${{ fromJson(needs.set-matrix.outputs.matrix) }}\n    steps:\n      - uses: actions/checkout@v4\n\n      # SSH Debug\n      - name: Enable SSH\n        uses: mxschmitt/action-tmate@v3\n        if: inputs.debug\n        with:\n          detached: true\n\n      - name: Download Docker Artifacts\n        if: inputs.build == 'docker'\n        uses: actions/download-artifact@v4\n        with:\n          name: docker-builds\n          path: ./release\n\n      - name: Load built docker image from workspace\n        if: inputs.build == 'docker'\n        run: |\n          docker image load -i ./release/docker/docker-linux-alpine.amd64.tar\n\n      - name: Make sure coverage dir exists\n        # Important: this directory is mounted on both the `app` and `test` Docker containers.\n        run: mkdir -p ./redisinsight/api/test/test-runs/coverage\n\n      - name: Run tests\n        timeout-minutes: 20\n        run: |\n          if [ ${{ inputs.redis_client }} == \"node-redis\" ]; then\n            export RI_REDIS_CLIENTS_FORCE_STRATEGY=${{ inputs.redis_client }}\n          fi\n\n          ./redisinsight/api/test/test-runs/start-test-run.sh -r ${{ matrix.rte }} -t ${{ inputs.build }}\n          mkdir -p itest/coverages && mkdir -p itest/results\n\n          cp ./redisinsight/api/test/test-runs/coverage/test-run-result.json ./itest/results/${{ matrix.rte }}.result.json\n          cp ./redisinsight/api/test/test-runs/coverage/test-run-result.xml ./itest/results/${{ matrix.rte }}.result.xml\n          cp ./redisinsight/api/test/test-runs/coverage/test-run-coverage.json ./itest/coverages/${{ matrix.rte }}.coverage.json\n\n      - name: Process test results\n        if: always()\n        run: |\n          mkdir -p itest/coverages && mkdir -p itest/results\n\n          cp ./redisinsight/api/test/test-runs/coverage/test-run-result.json ./itest/results/${{ matrix.rte }}.result.json\n          cp ./redisinsight/api/test/test-runs/coverage/test-run-result.xml ./itest/results/${{ matrix.rte }}.result.xml\n          cp ./redisinsight/api/test/test-runs/coverage/test-run-coverage.json ./itest/coverages/${{ matrix.rte }}.coverage.json\n\n      - name: Upload coverage files as artifact\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: coverages-${{ matrix.rte }}\n          path: ./itest/coverages\n\n      - name: Debug and validate test result XML\n        if: always()\n        run: |\n          echo \"=== Checking source coverage directory ===\"\n          ls -la ./redisinsight/api/test/test-runs/coverage/ || echo \"Source coverage directory doesn't exist\"\n\n          echo \"=== Checking test result files ===\"\n          ls -la ./itest/results/ || echo \"Results directory doesn't exist\"\n\n          echo \"=== Current working directory ===\"\n          pwd\n          ls -la .\n\n          XML_FILE=\"./itest/results/${{ matrix.rte }}.result.xml\"\n          SOURCE_XML=\"./redisinsight/api/test/test-runs/coverage/test-run-result.xml\"\n\n          echo \"=== Checking source XML file ===\"\n          if [ -f \"$SOURCE_XML\" ]; then\n            echo \"✅ Source XML found: $SOURCE_XML\"\n            echo \"Source file size: $(wc -c < \"$SOURCE_XML\") bytes\"\n          else\n            echo \"❌ Source XML not found: $SOURCE_XML\"\n          fi\n\n          if [ -f \"$XML_FILE\" ]; then\n            echo \"=== XML file found: $XML_FILE ===\"\n            echo \"File size: $(wc -c < \"$XML_FILE\") bytes\"\n            echo \"Line count: $(wc -l < \"$XML_FILE\") lines\"\n\n            echo \"=== First 20 lines of XML ===\"\n            head -20 \"$XML_FILE\"\n\n            echo \"=== Last 10 lines of XML ===\"\n            tail -10 \"$XML_FILE\"\n\n            echo \"=== Checking XML validity ===\"\n            if command -v xmllint >/dev/null 2>&1; then\n              if xmllint --noout \"$XML_FILE\" 2>/dev/null; then\n                echo \"✅ XML is well-formed\"\n              else\n                echo \"❌ XML is malformed\"\n                xmllint --noout \"$XML_FILE\" 2>&1 || true\n              fi\n            else\n              echo \"xmllint not available, skipping XML validation\"\n            fi\n\n            echo \"=== Basic XML structure check ===\"\n            if grep -q \"<testsuites\" \"$XML_FILE\" && grep -q \"</testsuites>\" \"$XML_FILE\"; then\n              echo \"✅ XML has testsuites root element\"\n            else\n              echo \"❌ XML missing testsuites root element\"\n            fi\n\n          else\n            echo \"❌ XML file not found: $XML_FILE\"\n            echo \"Available files in ./itest/results/:\"\n            ls -la ./itest/results/ 2>/dev/null || echo \"Directory doesn't exist\"\n          fi\n\n      - name: Generate test results\n        uses: dorny/test-reporter@v1\n        id: test-reporter\n        if: always()\n        with:\n          name: 'Test results: IT (${{ matrix.rte }}) tests'\n          path: ./itest/results/*.result.xml\n          reporter: java-junit\n          list-tests: 'failed'\n          list-suites: 'failed'\n          fail-on-error: 'true'\n\n      - name: Add link to report in the workflow summary\n        if: always()\n        run: |\n          link=\"${{ steps.test-reporter.outputs.url_html }}\"\n          echo \"- [${link}](${link})\" >> $GITHUB_STEP_SUMMARY\n\n  coverage:\n    runs-on: ubuntu-latest\n    name: Final coverage\n    needs: run-tests\n    if: always()\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Merge coverage artifacts\n        id: merge-artifacts\n        uses: actions/upload-artifact/merge@v4\n        with:\n          name: coverages-artifacts\n          pattern: coverages-*\n          delete-merged: true\n\n      - name: Download coverage artifacts\n        uses: actions/download-artifact@v4\n        with:\n          name: coverages-artifacts\n          path: ./coverages\n\n      - name: Calculate coverage across all tests runs\n        run: |\n          npx nyc report -t ./coverages -r text -r text-summary\n          sudo mkdir -p /usr/src/app\n          sudo cp -a ./redisinsight/api/. /usr/src/app/\n          sudo cp -R ./coverages /usr/src/app && sudo chmod 777 -R /usr/src/app\n          cd /usr/src/app && npx nyc report -t ./coverages -r text -r text-summary > integration-coverage.txt\n          cp integration-coverage.txt $GITHUB_WORKSPACE/integration-coverage.txt\n\n      - name: Upload integration-coverage as artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: integration-coverage\n          path: integration-coverage.txt\n\n      - name: Delete Artifact\n        uses: actions/github-script@v7\n        with:\n          script: |\n            github.rest.actions.deleteArtifact({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              artifact_id: ${{ steps.merge-artifacts.outputs.artifact-id }}\n            });\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: ✅ Tests\n\non:\n  push:\n    branches:\n      - 'fe/**'\n      - 'be/**'\n      - 'fe-be/**'\n      - 'feature/**'\n      - 'bugfix/**'\n      - 'ric/**'\n  pull_request:\n    types: [labeled]\n\n  workflow_dispatch:\n    inputs:\n      redis_client:\n        description: Library to use for redis connection\n        default: 'ioredis'\n        type: choice\n        options:\n          - ioredis\n          - node-redis\n\n      short_rte_list:\n        description: Use short RTE list for IT\n        type: boolean\n        default: true\n\n      debug:\n        description: Enable SSH Debug (IT and E2E)\n        default: false\n        type: boolean\n\n  workflow_call:\n    inputs:\n      short_rte_list:\n        description: Use short rte list\n        type: boolean\n        default: true\n      pre_release:\n        description: Is pre-release\n        default: false\n        type: boolean\n      debug:\n        description: Enable SSH Debug\n        default: false\n        type: boolean\n\n# Cancel a previous run workflow\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  changes:\n    runs-on: ubuntu-latest\n    outputs:\n      frontend: ${{ steps.filter.outputs.frontend }}\n      backend: ${{ steps.filter.outputs.backend }}\n      desktop: ${{ steps.filter.outputs.desktop }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dorny/paths-filter@v3.0.2\n        id: filter\n        with:\n          base: ${{ github.ref }}\n          filters: |\n            frontend:\n              - 'redisinsight/ui/**'\n            backend:\n              - 'redisinsight/api/**'\n            desktop:\n              - 'redisinsight/desktop/**'\n\n  # Check common conditions that trigger all test suites\n  should-run-all-tests:\n    runs-on: ubuntu-latest\n    outputs:\n      result: ${{ steps.check.outputs.result }}\n    steps:\n      - id: check\n        env:\n          PRERELEASE_CONDITION: ${{ inputs.pre_release == true }}\n        run: |\n          OTHER_CONDITIONS=\"${{ github.event_name == 'workflow_dispatch' ||\n              startsWith(github.ref_name, 'feature/') ||\n              startsWith(github.ref_name, 'bugfix/') ||\n              startsWith(github.ref_name, 'ric/') ||\n              startsWith(github.ref_name, 'release/') ||\n              github.ref_name == 'latest' ||\n              contains(github.event.pull_request.labels.*.name, 'run-all-tests') }}\"\n\n          if [[ \"$PRERELEASE_CONDITION\" == \"true\" ]] || [[ \"$OTHER_CONDITIONS\" == \"true\" ]]; then\n            echo \"result=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"result=false\" >> $GITHUB_OUTPUT\n          fi\n\n  lint:\n    uses: ./.github/workflows/lint.yml\n    secrets: inherit\n\n  frontend-tests:\n    needs: [changes, lint, should-run-all-tests]\n    if: |\n      needs.should-run-all-tests.outputs.result == 'true' ||\n      startsWith(github.ref_name, 'fe/') ||\n      startsWith(github.ref_name, 'fe-be/') ||\n      contains(github.event.pull_request.labels.*.name, 'run-frontend-tests')\n    uses: ./.github/workflows/tests-frontend.yml\n    secrets: inherit\n\n  frontend-tests-coverage:\n    needs: frontend-tests\n    uses: ./.github/workflows/code-coverage.yml\n    secrets: inherit\n    with:\n      resource_name: report-fe\n      type: unit\n\n  backend-tests:\n    needs: [changes, lint, should-run-all-tests]\n    if: |\n      needs.should-run-all-tests.outputs.result == 'true' ||\n      startsWith(github.ref_name, 'be/') ||\n      startsWith(github.ref_name, 'fe-be/') ||\n      contains(github.event.pull_request.labels.*.name, 'run-backend-tests')\n    uses: ./.github/workflows/tests-backend.yml\n    secrets: inherit\n\n  backend-tests-coverage:\n    needs: backend-tests\n    uses: ./.github/workflows/code-coverage.yml\n    secrets: inherit\n    with:\n      resource_name: report-be\n      type: unit\n\n  integration-tests:\n    needs: [changes, lint, should-run-all-tests]\n    if: |\n      needs.should-run-all-tests.outputs.result == 'true' ||\n      startsWith(github.ref_name, 'be/') ||\n      startsWith(github.ref_name, 'fe-be/') ||\n      contains(github.event.pull_request.labels.*.name, 'run-integration-tests')\n    uses: ./.github/workflows/tests-integration.yml\n    secrets: inherit\n    with:\n      short_rte_list: ${{ inputs.short_rte_list || true }}\n      redis_client: ${{ inputs.redis_client || '' }}\n      debug: ${{ inputs.debug || false }}\n\n  integration-tests-coverage:\n    needs: integration-tests\n    uses: ./.github/workflows/code-coverage.yml\n    secrets: inherit\n    with:\n      resource_name: integration-coverage\n      type: integration\n\n  clean:\n    uses: ./.github/workflows/clean-deployments.yml\n    if: always()\n    needs:\n      [\n        frontend-tests,\n        backend-tests,\n        integration-tests,\n      ]\n\n  # Remove artifacts from github actions\n  remove-artifacts:\n    name: Remove artifacts\n    needs:\n      [\n        frontend-tests,\n        backend-tests,\n        integration-tests,\n      ]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Remove all artifacts\n        uses: ./.github/actions/remove-artifacts\n"
  },
  {
    "path": ".github/workflows/virustotal.yml",
    "content": "name: Virustotal Analyze\n\non:\n  workflow_call:\n    inputs:\n      skip_report:\n        description: Skip report\n        required: false\n        default: false\n        type: boolean\n\nenv:\n  VIRUSTOTAL_FILE_NAMES: ${{ vars.VIRUSTOTAL_FILE_NAMES }}\n  VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }}\n  SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }}\n\njobs:\n  download_artifacts:\n    name: Download artifacts\n    runs-on: ubuntu-latest\n    outputs:\n      artifact_names: ${{ steps.list_artifacts.outputs.artifact_names }}\n      artifact_exists: ${{ steps.list_artifacts.outputs.artifact_exists }}\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v4\n\n      - name: Download All Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: ./release\n          # TODO: enable pattern filter after fix:\n          # https://github.com/nektos/act/issues/2433\n          #   pattern: '*-build'\n          #   merge-multiple: true\n\n      - run: ls -R ./release\n\n      - name: List Artifact Files\n        id: list_artifacts\n        run: |\n          # If artifacts don't exist put array of app names for url check\n          if [ ! -d \"./release\" ]; then\n            echo \"NO REALEASE FOLDER ${VIRUSTOTAL_FILE_NAMES}\"\n            echo \"artifact_exists=false\" >> $GITHUB_OUTPUT\n            echo \"artifact_names=$VIRUSTOTAL_FILE_NAMES\" >> $GITHUB_OUTPUT\n            exit 0;\n          fi\n\n          # Get list of artifacts\n          ARTIFACTS=$(ls ./release)\n\n          # Conver list to json\n          ARTIFACTS_JSON=$(echo \"$ARTIFACTS\" | jq -R -s -c 'split(\"\\n\")[:-1]')\n\n          echo \"artifact_exists=true\" >> $GITHUB_OUTPUT\n          echo \"artifact_names=$ARTIFACTS_JSON\" >> $GITHUB_OUTPUT\n\n  analyze:\n    name: Analyze file\n    runs-on: ubuntu-latest\n    needs: download_artifacts\n\n    strategy:\n      fail-fast: false\n      matrix:\n        artifact: ${{ fromJson(needs.download_artifacts.outputs.artifact_names) }}\n\n    steps:\n      - name: Download Artifact ${{ matrix.artifact }}\n        if: needs.download_artifacts.outputs.artifact_exists == 'true'\n        uses: actions/download-artifact@v4\n        with:\n          name: ${{ matrix.artifact }}\n          path: ./release\n\n      - name: Send File to scan\n        if: needs.download_artifacts.outputs.artifact_exists == 'true'\n        run: |\n          uploadZipFile=\"./${{ matrix.artifact }}.zip\"\n\n          # Compress artifactes\n          zip -r \"${uploadZipFile}\" \"./release\" ${{ startsWith(matrix.artifact, 'macos-') && '-x \"*/redisstack/*\" \"*.tar.gz\" \"*.zip\"' || '' }}\n\n          # Generate url to download zip file\n          uploadUrl=$(curl -sq -XGET https://www.virustotal.com/api/v3/files/upload_url -H \"x-apikey: $VIRUSTOTAL_API_KEY\" | jq -r '.data')\n\n          echo \"File to upload: ${uploadZipFile}\"\n\n          # Upload zip file to VirusTotal\n          analysedId=$(curl -sq -XPOST \"${uploadUrl}\" -H \"x-apikey: $VIRUSTOTAL_API_KEY\" --form file=@\"${uploadZipFile}\" | jq -r '.data.id')\n\n          if [ $analysedId == \"null\" ]; then\n            echo 'Status is null, something went wrong';\n            exit 1;\n          fi\n\n          echo \"ANALYZED_ID=$analysedId\" >> $GITHUB_ENV\n          echo \"BUILD_NAME=${{ matrix.artifact }}\" >> $GITHUB_ENV\n\n      - name: Send Url to scan\n        if: needs.download_artifacts.outputs.artifact_exists == 'false'\n        run: |\n          url=\"https://download.redisinsight.redis.com/latest/${{ matrix.artifact }}\"\n\n          echo \"Url to check: ${url}\"\n\n          # Upload Url to VirusTotal\n          analysedId=$(curl -sq -XPOST https://www.virustotal.com/api/v3/urls -H \"x-apikey: $VIRUSTOTAL_API_KEY\" --form url=${url} | jq -r '.data.id')\n\n          if [ $analysedId == \"null\" ]; then\n            echo 'Status is null, something went wrong';\n            exit 1;\n          fi\n\n          echo \"ANALYZED_ID=$analysedId\" >> $GITHUB_ENV\n\n      - name: Check analyze status\n        run: |\n          echo \"Virustotal Analyzed id: ${ANALYZED_ID}\"\n          countOperations=\"50\"\n          intervalTime=30\n\n          until [ \"$countOperations\" == \"0\" ]; do\n            if [ \"$analyzeStatus\" == \"completed\" ]\n            then\n              echo \"Current status: ${analyzeStatus}\"; break;\n            else\n              echo \"Current status: ${analyzeStatus}, retries left: ${countOperations} \";\n              analyzeStatus=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H \"x-apikey: $VIRUSTOTAL_API_KEY\" | jq -r '.data.attributes.status');\n\n              sleep $intervalTime;\n              countOperations=$[$countOperations - 1];\n            fi\n          done\n\n          if [ \"$analyzeStatus\" != \"completed\" ]; then\n            echo 'Analyse is not completed';\n            exit 1;\n          fi\n\n      - name: Validate analyze\n        id: validate\n        run: |\n          analyzeStats=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H \"x-apikey: $VIRUSTOTAL_API_KEY\" | jq -r '.data.attributes.stats')\n          analazedHarmless=$(echo ${analyzeStats} | jq '.harmless')\n          analazedMalicious=$(echo ${analyzeStats} | jq '.malicious')\n          analazedSuspicious=$(echo ${analyzeStats} | jq '.suspicious')\n\n          echo \"Results:\"\n          echo \"analazedMalicious:  ${analazedMalicious}, analazedSuspicious: ${analazedSuspicious}, analazedHarmless: ${analazedHarmless}\"\n\n          if [ \"$analazedMalicious\" != \"0\" ] || [ \"$analazedSuspicious\" != \"0\" ]; then\n            echo \"FAILED=true\" >> $GITHUB_ENV\n            echo 'Found dangers';\n          fi\n\n          echo \"FAILED=false\" >> $GITHUB_ENV\n          echo \"skip_report=true\" >> $GITHUB_ENV\n          echo 'Passed';\n\n      - name: Send Report\n        if: ${{ !steps.validate.outputs.skip_report && !inputs.skip_report }}\n        run: |\n          FILE_NAME=virustotal.report.json\n          BUILD_NAME=$BUILD_NAME FILE_NAME=$FILE_NAME VIRUS_CHECK_FAILED=$FAILED node .github/virustotal-report.js\n\n          BUILD_NAME=$BUILD_NAME FILE_NAME=$FILE_NAME VIRUS_CHECK_FAILED=$FAILED node .github/virustotal-report.js &&\n          curl -H \"Content-type: application/json\" --data @$FILE_NAME -H \"Authorization: Bearer ${SLACK_TEST_REPORT_KEY}\" -X POST https://slack.com/api/chat.postMessage\n\n          if [ \"$FAILED\" == \"true\" ]; then\n            echo 'Found dangers';\n            exit 1;\n          fi\n"
  },
  {
    "path": ".github/workflows/weekly.yml",
    "content": "name: Weekly jobs\non:\n  schedule:\n    - cron: 0 0 * * 1\n\njobs:\n  licenses-check:\n    uses: ./.github/workflows/licenses-check.yml\n    secrets: inherit\n"
  },
  {
    "path": ".gitignore",
    "content": "# compiled output\ndist\nnode_modules\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n**/coverage\n\n#CI\n.actrc\nmy.secrets\nmy.inputs\n\n# IDEs and editors\n/.idea\n.idea/\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n*.css.d.ts\n*.sass.d.ts\n*.scss.d.ts\n**/*.scss.d.ts\n\n# App packaged\nrelease\nmain.prod.js\nmain.prod.js.map\nredisinsight/ui/main.prod.js\nredisinsight/ui/main.prod.js.map\nrenderer.prod.js\nrenderer.prod.js.map\nredisinsight/ui/style.css\nredisinsight/ui/style.css.map\nredisinsight/ui/dist\nredisinsight/api/commands\nredisinsight/api/guides\nredisinsight/api/report\nredisinsight/api/reports\nredisinsight/api/dist-minified\nredisinsight/api/tutorials\nredisinsight/api/content\nredisinsight/ui/dist-stats.html\nreport\nreports\ndistWeb\ndll\nmain.js\nmain.js.map\nvendor\nredisinsight/main.js.LICENSE.txt\nredisinsight/main.prod.js.LICENSE.txt\nlicenses\nredisinsight/ui/src/packages/common/index*\n\n\n# E2E tests report\n/tests/e2e/report\n/tests/e2e/results\n/tests/e2e/remote\n/tests/e2e/.redisinsight-v2\n/tests/e2e/.redisinsight-app\n/tests/e2e/.redisinsight-insight\n\n# Parcel\n.parcel-cache\n\n# caches\n.temp_cache\n\nstatic/\n\n.env*\n.npmrc\n\n# AI rules\n.windsurfrules\n.junie/\n*storybook.log\nstorybook-static\n\n# MCP Environment Configuration (contains secrets)\n.env.mcp\n\n.eslintcache\n"
  },
  {
    "path": ".jit/jit-config.yml",
    "content": "# Jit Security Plan Configuration\n# https://docs.jit.io/\n\nname: RedisInsight Security Plan\n\n# Exclusions from security scans\nexclude:\n  paths:\n    - __mocks__/**\n    - redisinsight/api/src/__mocks__/**\n    - '**/tests/**'\n    - redisinsight/api/test/**\n    - '**/*.spec.ts'\n    - '**/*.spec.tsx'\n"
  },
  {
    "path": ".nvmrc",
    "content": "22\n"
  },
  {
    "path": ".prettierignore",
    "content": "tests/e2e\ntests/e2e-playwright\n**/*.scss\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": true,\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.ts\", \"**/*.tsx\"],\n      \"options\": {\n        \"semi\": false\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".storybook/RootStoryLayout.tsx",
    "content": "import React, { FC, PropsWithChildren } from 'react'\nimport { StoryContext } from '@storybook/react-vite'\n\nexport interface Parameters {\n  storyLayout?: FC<PropsWithChildren<{ storyContext: StoryContext }>>\n}\n\n/**\n * Note: for use in Storybook preview config\n *\n * Define parameters.storyLayout as React component, and it will be used as root layout of the story\n */\nexport const RootStoryLayout = ({\n  children,\n  storyContext,\n}: Required<PropsWithChildren<{ storyContext: StoryContext }>>) => {\n  const { storyLayout } = storyContext.parameters\n  if (!storyLayout) {\n    return <>{children}</>\n  }\n  if (React.isValidElement(storyLayout)) {\n    // @ts-ignore\n    return React.cloneElement(storyLayout, { storyContext }, children)\n  }\n\n  const StoryLayout = storyLayout\n  return <StoryLayout storyContext={storyContext}>{children}</StoryLayout>\n}\n"
  },
  {
    "path": ".storybook/Story.context.ts",
    "content": "import { createContext, useContext } from 'react'\nimport { StoryContext } from '@storybook/react-vite'\n\nconst Context = createContext<StoryContext | null>(null)\n\nexport const StoryContextProvider = Context.Provider\n\nexport const useStoryContext = () => {\n  const context = useContext(Context)\n  if (!context)\n    throw new Error('useStoryContext must be used within StoryContextProvider')\n  return context\n}\n\nexport const useStoryParameter = <T>(parameterKey: string): T | undefined =>\n  useStoryContext().parameters[parameterKey]\n"
  },
  {
    "path": ".storybook/ThemeContextBridge.tsx",
    "content": "import React from 'react'\nimport { useTheme } from '@redis-ui/styles'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { Theme } from 'uiSrc/constants'\n\n/**\n * Bridges the styled-components theme (managed by Storybook's addon-themes)\n * to the app's ThemeContext, so that all consumers (MonacoLanguages, CodeEditor,\n * Config, etc.) receive the correct theme when switching in Storybook.\n */\nexport const ThemeContextBridge = ({\n  children,\n}: {\n  children: React.ReactNode\n}) => {\n  const scTheme = useTheme()\n  const theme = scTheme.mode === 'dark' ? Theme.Dark : Theme.Light\n\n  return (\n    <ThemeContext.Provider\n      value={{\n        theme,\n        usingSystemTheme: false,\n        changeTheme: () => {},\n      }}\n    >\n      {children}\n    </ThemeContext.Provider>\n  )\n}\n"
  },
  {
    "path": ".storybook/helpers/styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport PageBody from 'uiSrc/components/base/layout/page/PageBody'\n\nexport const StyledContainer = styled(PageBody)`\n  height: max-content;\n  max-height: 100%;\n  overflow: hidden;\n  overflow-y: auto;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.globals.body.bgColor};\n  border: 1px dashed\n    ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral500};\n  align-items: center;\n  justify-content: center;\n`\n"
  },
  {
    "path": ".storybook/main.ts",
    "content": "import type { StorybookConfig } from '@storybook/react-vite'\nimport { mergeConfig } from 'vite'\nimport vc from './vite.config'\n\nconst config: StorybookConfig = {\n  async viteFinal(inlineConfig) {\n    // return the customized config\n    return mergeConfig(inlineConfig, vc)\n  },\n  stories: [\n    '../stories/**/*.mdx',\n    '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',\n    '../redisinsight/ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',\n    '../redisinsight/ui/src/**/*.mdx',\n  ],\n  addons: [\n    '@storybook/addon-a11y',\n    '@storybook/addon-docs',\n    '@storybook/addon-links',\n    '@storybook/addon-themes',\n  ],\n  framework: {\n    name: '@storybook/react-vite',\n    options: {},\n  },\n}\nexport default config\n"
  },
  {
    "path": ".storybook/manager.ts",
    "content": "import { addons } from 'storybook/manager-api'\nimport { themes } from 'storybook/theming'\n\naddons.setConfig({\n  theme: themes.dark,\n})\n"
  },
  {
    "path": ".storybook/preview-head.html",
    "content": "<script>\n    window.global = window;\n</script>\n<style>\n    :root {\n        /*\n           Insert global variables here:\n           colors, sizes, fonts, etc.\n        */\n    }\n\n    body {\n      min-height: 100vh;\n    }\n\n    #storybook-root {\n      position: absolute;\n      inset: 0;\n      display: flex;\n      flex-direction: column;\n    }\n\n    .sbdocs-wrapper div:has(>div>.toc-wrapper){\n      width:14rem;\n    }\n\n    .sbdocs-wrapper div:has(>.toc-wrapper){\n      width:fit-content;\n      margin-right:1rem;\n    }\n\n    .sbdocs-wrapper .toc-wrapper .toc-list-item {\n      padding-block: 5px;\n    }\n\n    .sb-errordisplay pre code {\n      background-color: transparent;\n    }\n\n    .docblock-argstable [type='checkbox'][role='switch'] {\n      display: none;\n    }\n\n    pre:has(.docblock-source), pre.prismjs{\n      max-height: none; /*fix code block scroll*/\n    }\n\n    .docblock-source [data-radix-scroll-area-viewport],\n    .docs-story + div [data-radix-scroll-area-viewport]{\n      max-height: 80vh; /*add max height of code block*/\n    }\n\n</style>\n"
  },
  {
    "path": ".storybook/preview.tsx",
    "content": "import React from 'react'\nimport type { Parameters, Preview } from '@storybook/react-vite'\nimport { withThemeFromJSXProvider } from '@storybook/addon-themes'\nimport { ThemeProvider as StyledThemeProvider } from 'styled-components'\nimport { CommonStyles, themesDefault, themesRebrand } from '@redis-ui/styles'\nimport 'modern-normalize/modern-normalize.css'\nimport '@redis-ui/styles/normalized-styles.css'\nimport '@redis-ui/styles/fonts.css'\nimport 'uiSrc/pages/home/styles.scss'\nimport { RootStoryLayout } from './RootStoryLayout'\nimport { StoryContextProvider } from './Story.context'\nimport { useStoryContext } from 'storybook/internal/preview-api'\nimport { TooltipProvider } from '@redis-ui/components'\nimport { Provider } from 'react-redux'\nimport { store } from 'uiSrc/slices/store'\nimport Router from 'uiSrc/Router'\nimport { StyledContainer } from './helpers/styles'\nimport { GlobalStyles } from 'uiSrc/styles/globalStyles'\nimport { ThemeContextBridge } from './ThemeContextBridge'\n\nconst parameters: Parameters = {\n  parameters: {\n    layout: 'centered',\n  },\n  actions: { argTypesRegex: '^on.*' },\n  controls: {\n    disableSaveFromUI: true,\n    matchers: {\n      color: /(background|color)$/i,\n      date: /Date$/,\n    },\n    expanded: true,\n    sort: 'requiredFirst',\n    exclude: ['theme'],\n  },\n  docs: {\n    toc: true,\n    controls: {\n      sort: 'requiredFirst',\n    },\n  },\n  options: {\n    storySort: {\n      method: 'alphabetical',\n      order: ['Getting Started', 'Playground', '*'],\n    },\n  },\n}\n\nconst preview: Preview = {\n  parameters,\n  decorators: [\n    (Story) => (\n      <ThemeContextBridge>\n        <StoryContextProvider value={useStoryContext()}>\n          <Router>\n            <Provider store={store}>\n              <TooltipProvider>\n                <RootStoryLayout storyContext={useStoryContext()}>\n                  <CommonStyles />\n                  <GlobalStyles />\n                  <StyledContainer>\n                    <Story />\n                  </StyledContainer>\n                </RootStoryLayout>\n              </TooltipProvider>\n            </Provider>\n          </Router>\n        </StoryContextProvider>\n      </ThemeContextBridge>\n    ),\n    withThemeFromJSXProvider({\n      themes: {\n        light: themesDefault.light,\n        dark: themesDefault.dark,\n        'light-rebrand': themesRebrand.light,\n        'dark-rebrand': themesRebrand.dark,\n      },\n      defaultTheme: 'light',\n      Provider: StyledThemeProvider,\n    }),\n  ],\n}\n\nexport default preview\n"
  },
  {
    "path": ".storybook/redis-theme.ts",
    "content": "import { create } from '@storybook/theming/create';\n\nexport default create({\n  base: 'light',\n\n  brandTitle: 'Redis UI',\n  brandUrl: 'https://github.com/redislabsdev/redis-ui',\n  brandImage: 'logo.svg',\n  brandTarget: '_blank',\n\n  colorPrimary: '#001D2D',\n  colorSecondary: '#FF4438',\n\n  // UI\n  appBg: '#001D2D',\n\n  // Toolbar default and active colors\n  barBg: '#F0F0F0',\n  textMutedColor: '#5C707A'\n});\n"
  },
  {
    "path": ".storybook/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"types\": [\"node\", \"vite/client\"],\n    \"module\": \"ESNext\"\n  },\n  \"include\": [\n    \"./**/*\"\n  ]\n}\n"
  },
  {
    "path": ".storybook/vite.config.ts",
    "content": "import 'dotenv/config'\nimport { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport svgr from 'vite-plugin-svgr'\nimport fixReactVirtualized from 'esbuild-plugin-react-virtualized'\nimport { reactClickToComponent } from 'vite-plugin-react-click-to-component'\nimport { ViteEjsPlugin } from 'vite-plugin-ejs'\nimport { fileURLToPath, URL } from 'url'\nimport path from 'path'\nimport { defaultConfig } from 'uiSrc/config/default'\n\nconst base = '/'\n\n/**\n * @type {import('vite').UserConfig}\n */\nexport default defineConfig({\n  base,\n  plugins: [\n    react(),\n    svgr({ include: ['../**/*.svg?react'] }),\n    reactClickToComponent(),\n    ViteEjsPlugin(),\n    // Inject app info to window global object via custom plugin\n    {\n      name: 'app-info',\n      transformIndexHtml(html) {\n        const script = `<script>window.appInfo = ${JSON.stringify({\n          version: defaultConfig.app.version,\n          sha: defaultConfig.app.sha,\n        })};</script>`\n\n        return html.replace(/<head>/, `<head>\\n  ${script}`)\n      },\n    },\n  ],\n  resolve: {\n    alias: {\n      lodash: 'lodash-es',\n      '@elastic/eui$': '@elastic/eui/optimize/lib',\n      '@redislabsdev/redis-ui-components': '@redis-ui/components',\n      '@redislabsdev/redis-ui-styles': '@redis-ui/styles',\n      '@redislabsdev/redis-ui-icons': '@redis-ui/icons',\n      '@redislabsdev/redis-ui-table': '@redis-ui/table',\n      uiSrc: fileURLToPath(new URL('../redisinsight/ui/src', import.meta.url)),\n      apiSrc: fileURLToPath(\n        new URL('../redisinsight/api/src', import.meta.url),\n      ),\n    },\n  },\n  server: {\n    fs: {\n      allow: ['..', '../node_modules/monaco-editor', 'static', 'defaults'],\n    },\n  },\n  envPrefix: 'RI_',\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks(id) {\n          if (id.includes('node_modules')) {\n            return id\n              .toString()\n              .split('node_modules/')[1]\n              .split('/')[0]\n              .toString()\n          }\n\n          if (id.includes('ui/src/assets')) {\n            return 'assets'\n          }\n          return 'index'\n        },\n      },\n    },\n    commonjsOptions: {\n      exclude: ['./packages'],\n    },\n    target: 'es2020',\n  },\n  optimizeDeps: {\n    include: ['monaco-editor', 'monaco-yaml/yaml.worker'],\n    exclude: [\n      'react-json-tree',\n      'redisinsight-plugin-sdk',\n      'plotly.js-dist-min',\n      '@antv/x6',\n      '@antv/x6-react-shape',\n      '@antv/hierarchy',\n      'class-transformer',\n      'keytar',\n      '@nestjs/common',\n      '@nestjs/core',\n      '@nestjs/event-emitter',\n      '@nestjs/platform-express',\n      '@nestjs/platform-socket.io',\n      '@nestjs/serve-static',\n      '@nestjs/swagger',\n      '@nestjs/typeorm',\n      '@nestjs/websockets',\n      'nestjs-form-data',\n    ],\n    esbuildOptions: {\n      // fix for https://github.com/bvaughn/react-virtualized/issues/1722\n      plugins: [fixReactVirtualized],\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      scss: {\n        // add @layer app for css ordering. Styles without layer have the highest priority\n        // https://github.com/vitejs/vite/issues/3924\n        additionalData: (source: string, filename: string) => {\n          if (path.extname(filename) === '.scss') {\n            const skipFiles = ['/main.scss', '/App.scss']\n            if (skipFiles.every((file) => !filename.endsWith(file))) {\n              return `\n                @use \"uiSrc/styles/mixins/_eui.scss\";\n                @use \"uiSrc/styles/mixins/_global.scss\";\n                @layer app { ${source} }\n              `\n            }\n          }\n          return source\n        },\n      },\n    },\n  },\n  define: {\n    global: 'globalThis',\n    'process.env': {},\n    riConfig: defaultConfig,\n  },\n  // hack: apply proxy path to monaco webworker\n  experimental: {\n    renderBuiltUrl() {\n      return { relative: true }\n    },\n  },\n})\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"ms-playwright.playwright\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Debug API (Nest Framework)\",\n      \"runtimeExecutable\": \"yarn\",\n      \"runtimeArgs\": [\"dev:api\", \"--debug\", \"--inspect-brk\"],\n      \"autoAttachChildProcesses\": true,\n      \"restart\": true,\n      \"sourceMaps\": true,\n      \"stopOnEntry\": false,\n      \"console\": \"integratedTerminal\"\n    },\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Debug API Integration Tests\",\n      \"skipFiles\": [\"<node_internals>/**\"],\n      \"program\": \"${workspaceFolder}/redisinsight/api/node_modules/ts-mocha/bin/ts-mocha\",\n      \"args\": [\"--paths\", \"--config\", \"./test/api/.mocharc.yml\"],\n      \"env\": {\n        \"NODE_ENV\": \"test\"\n      },\n      \"cwd\": \"${workspaceFolder}/redisinsight/api\",\n      \"console\": \"integratedTerminal\",\n      \"internalConsoleOptions\": \"neverOpen\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  }\n}\n"
  },
  {
    "path": ".yarnrc",
    "content": "# This will set the --ignore-scripts flag whenever running yarn add\n#--ignore-scripts true\n#--install.ignore-scripts true\n--add.ignore-scripts true\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# RedisInsight AI Agent Instructions\n\nThis file provides essential context and instructions for AI coding agents working on RedisInsight.\n\n## Project Overview\n\n**RedisInsight** is a desktop application for Redis database management built with:\n\n- **Frontend**: React 18, TypeScript, Redux Toolkit, styled-components, Monaco Editor, Vite\n- **Backend**: NestJS, TypeScript, Node.js\n- **Desktop**: Electron for cross-platform distribution\n- **Testing**: Jest, Testing Library, Playwright\n\n**Architecture**:\n\n```\nredisinsight/\n├── ui/          # React frontend (Vite + TypeScript)\n├── api/         # NestJS backend (TypeScript)\n├── desktop/     # Electron main process\n└── tests/       # E2E tests (Playwright)\n```\n\n## Setup Commands\n\n### Development\n\n```bash\n# Frontend development (web)\nyarn dev:ui\n\n# Backend development\nyarn dev:api\n\n# Desktop app development (runs all: API + UI + Electron)\nyarn dev:desktop\n\n# Frontend with coverage\nyarn dev:ui:coverage\n```\n\n## Testing Instructions\n\n### Run Tests\n\n```bash\n# Frontend tests\nyarn test              # Run all UI tests\n\n# Backend tests\nyarn test:api          # Run all API tests\n\n# E2E tests\nyarn --cwd tests/e2e test\n```\n\n### Run Specific Frontend Tests\n\n```bash\n# Run a specific test file\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs'\n\n# Run a specific test by name (use -t flag)\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/path/to/Component.spec.tsx' -c 'jest.config.cjs' -t 'test name pattern'\n\n# Example:\nnode 'node_modules/.bin/jest' 'redisinsight/ui/src/slices/tests/browser/keys.spec.ts' -c 'jest.config.cjs' -t 'refreshKeyInfoAction'\n```\n\n### Before Committing\n\n**ALWAYS run these before committing:**\n\n```bash\n# Lint check\nyarn lint              # All code\nyarn lint:ui           # Frontend only\nyarn lint:api          # Backend only\n\n# Type checking\nyarn type-check:ui     # Frontend TypeScript\n\n# Tests\nyarn test              # Frontend tests\nyarn test:api          # Backend tests\n```\n\n**Fix any linting errors, type errors, or test failures before committing.**\n\nAll detailed development standards are maintained in `.ai/rules/`:\n\n- **Code Quality**: `.ai/rules/code-quality.md` - Linting, TypeScript standards\n- **Frontend**: `.ai/rules/frontend.md` - React, Redux, UI patterns, styled-components\n- **Backend**: `.ai/rules/backend.md` - NestJS, API patterns, dependency injection\n- **Testing**: `.ai/rules/testing.md` - Testing standards, faker usage, test patterns\n- **Branches**: `.ai/skills/branches/SKILL.md` - Branch naming conventions\n- **Commits**: `.ai/skills/commits/SKILL.md` - Commit message guidelines\n- **Pull Requests**: `.ai/skills/pull-requests/SKILL.md` - PR process and review guidelines\n\n**Refer to these files for comprehensive guidelines on each topic.**\n\n## Boundaries\n\n### ✅ Always Do\n\n- Write to `src/` and `tests/` directories\n- Run `yarn lint` and `yarn test` before commits\n- Follow naming conventions (camelCase, PascalCase, UPPER_SNAKE_CASE)\n- Use faker library for test data generation\n- Use `renderComponent` helper in component tests\n- Extract duplicate strings to constants\n- Use semantic colors from theme (not CSS variables)\n- Use layout components (Row/Col/FlexGroup) instead of div\n- Pass layout props as component props (not hardcoded in styles)\n\n### ⚠️ Ask First\n\n- Database schema changes\n- Adding new dependencies\n- Modifying CI/CD configuration (`.github/workflows/`)\n- Changes to build configuration\n- Breaking changes to APIs\n\n### 🚫 Never Do\n\n- Commit secrets or API keys\n- Edit `node_modules/` or `vendor/` directories\n- Use fixed time waits in tests (use `waitFor` instead)\n- Use `!important` in styled-components\n- Import directly from `@redis-ui/*` (use internal wrappers from `uiSrc/components/ui`)\n- Use Elastic UI for new code (migrating to Redis UI)\n- Use hardcoded pixel values (use theme spacing)\n- Use `any` type without reason\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 0.0.0\n\n#### Features\n"
  },
  {
    "path": "CONDUCT",
    "content": "Contributor Covenant Code of Conduct\nOur Pledge\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, religion, or sexual identity\nand orientation.\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\nOur Standards\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,\nand learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\noverall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\nadvances 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\naddress, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\nprofessional setting\n\nEnforcement Responsibilities\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.\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.\nScope\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.\nEnforcement\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nthis email address: redisinsight@redis.com.\nAll complaints will be reviewed and investigated promptly and fairly.\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\nEnforcement Guidelines\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n1. Correction\nCommunity Impact: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\nConsequence: 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.\n2. Warning\nCommunity Impact: A violation through a single incident or series\nof actions.\nConsequence: 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.\n3. Temporary Ban\nCommunity Impact: A serious violation of community standards, including\nsustained inappropriate behavior.\nConsequence: 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.\n4. Permanent Ban\nCommunity 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.\nConsequence: A permanent ban from any sort of public interaction within\nthe community.\nAttribution\nThis Code of Conduct is adapted from the Contributor Covenant,\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\nCommunity Impact Guidelines were inspired by Mozilla’s code of conduct\nenforcement ladder.\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWelcome! This short guide explains how to contribute effectively and pass all required checks.\n\n## Submitting an issue\n\n- If you find a bug, please submit an issue to our GitHub [repository](https://github.com/redis/RedisInsight/issues).\n- Before submitting, search the issue tracker to see if your problem already exists. Existing issues may already have workarounds or ongoing fixes.\n\n## Branch Naming Convention\n\nUse lowercase, kebab-case, and a type prefix:\n\n- `feature/<short-title>`\n- `bugfix/<short-title>`\n\n**Example**: `bugfix/fix-header-alignment`\n\n_Note: It will trigger some CI, like unit tests and lint checks_\n\nFor frontend/backend only, prefix with `fe/` or `be/` to trigger fewer checks:\n\n- `fe/feature/<short-title>`\n- `be/bugfix/<short-title>`\n\n**Example**: `be/bugfix/update-databases-api`\n\n_Note: It will trigger only checks related to the back-end_\n\n## Commits\n\n- Keep commits small and focused.\n- This makes it easier for reviewers to understand and track changes.\n- Use meaningful commit messages (see [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for inspiration).\n\n## Pull Requests\n\nUse the following procedure to submit a pull request:\n\n1. Fork RedisInsight on GitHub (_[How to fork a repo?](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo)_)\n\n2. Create a branch from `main` (see [Branch Naming](#branch-naming-convention))\n\n```bash\ngit checkout -b bugfix/<short-title>\n```\n\n3. Make the changes and push to your branch (see [Commits](#commits))\n\n```bash\ngit push bugfix/<short-title>\n```\n\n4. Initiate a pull request on GitHub (_[How to create a PR?](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)_)\n\nTry to provide as much descrtiption behind the context of your changes and how to verify them. Screenshorts and videos are always welcome ^\\_^\n\n5. Ensure tests are passing (_[see more](README.md#tests)_).\n\nDone :)\n\nBy following these conventions, you help us keep RedisInsight stable, reliable, and easy to maintain. Thank you for contributing!\n"
  },
  {
    "path": "Dockerfile",
    "content": "# this dockerfile has two stages, a build stage and the executable stage.\n# the build stage is responsible for building the frontend, the backend,\n# and the frontend's static assets. ideally, we could build the frontend and backend\n# independently and in parallel in different stages, but there is a dependency\n# on the backend to build those assets. until we fix that, this approach is\n# the best way to minimize the number of node_module restores and build steps\n# while still keeping the final image small.\n\nFROM node:22.12.0-alpine as build\n\n# update apk repository and install build dependencies\nRUN apk update && apk add --no-cache --virtual .gyp \\\n        python3 \\\n        make \\\n        git \\\n        g++\n\n# set workdir\nWORKDIR /usr/src/app\n\n# restore node_modules for front-end\nCOPY package.json yarn.lock tsconfig.json ./\nCOPY patches ./patches\nCOPY redisinsight/ui/vite.config.mjs ./redisinsight/ui/\nCOPY redisinsight/ui/src/config ./redisinsight/ui/src/config\nRUN SKIP_POSTINSTALL=1 yarn install\n\n# prepare backend by copying scripts/configs and installing node modules\n# this is required to build the static assets\nCOPY configs ./configs\nCOPY scripts ./scripts\nCOPY redisinsight ./redisinsight\nRUN yarn --cwd redisinsight/api install\n\n# build the frontend, static assets, and backend api\nRUN yarn build:ui\nRUN yarn build:statics\nRUN yarn build:api\n\n# install backend _again_ to build native modules and remove dev dependencies,\n# then run autoclean to remove additional unnecessary files\nRUN yarn --cwd ./redisinsight/api install --production\nCOPY ./redisinsight/api/.yarnclean.prod ./redisinsight/api/.yarnclean\nRUN yarn --cwd ./redisinsight/api autoclean --force\n\nFROM node:22.12.0-alpine\n\n# runtime args and environment variables\nARG NODE_ENV=production\nARG RI_SEGMENT_WRITE_KEY\nENV RI_SEGMENT_WRITE_KEY=${RI_SEGMENT_WRITE_KEY}\nENV NODE_ENV=${NODE_ENV}\nENV RI_SERVE_STATICS=true\nENV RI_BUILD_TYPE='DOCKER_ON_PREMISE'\nENV RI_APP_FOLDER_ABSOLUTE_PATH='/data'\n\n# this resolves CVE-2023-5363\n# TODO: remove this line once we update to base image that doesn't have this vulnerability\nRUN apk update && apk upgrade --no-cache libcrypto3 libssl3\n\n# set workdir\nWORKDIR /usr/src/app\n\n# copy artifacts built in previous stage to this one\nCOPY --from=build --chown=node:node /usr/src/app/redisinsight/api/dist ./redisinsight/api/dist\nCOPY --from=build --chown=node:node /usr/src/app/redisinsight/api/node_modules ./redisinsight/api/node_modules\nCOPY --from=build --chown=node:node /usr/src/app/redisinsight/ui/dist ./redisinsight/ui/dist\n\n# folder to store local database, plugins, logs and all other files\nRUN mkdir -p /data && chown -R node:node /data\n\n# copy the docker entry point script and make it executable\nCOPY --chown=node:node ./docker-entry.sh ./\nRUN chmod +x docker-entry.sh\n\n# since RI is hard-code to port 5540, expose it from the container\nEXPOSE 5540\n\n# don't run the node process as root\nUSER node\n\n# serve the application 🚀\nENTRYPOINT [\"./docker-entry.sh\", \"node\", \"redisinsight/api/dist/src/main\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                     Server Side Public License\n                     VERSION 1, OCTOBER 16, 2018\n\n                    Copyright © 2018 MongoDB, Inc.\n\n  Everyone is permitted to copy and distribute verbatim copies of this\n  license document, but changing it is not allowed.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  “This License” refers to Server Side Public License.\n\n  “Copyright” also means copyright-like laws that apply to other kinds of\n  works, such as semiconductor masks.\n\n  “The Program” refers to any copyrightable work licensed under this\n  License.  Each licensee is addressed as “you”. “Licensees” and\n  “recipients” may be individuals or organizations.\n\n  To “modify” a work means to copy from or adapt all or part of the work in\n  a fashion requiring copyright permission, other than the making of an\n  exact copy. The resulting work is called a “modified version” of the\n  earlier work or a work “based on” the earlier work.\n\n  A “covered work” means either the unmodified Program or a work based on\n  the Program.\n\n  To “propagate” a work means to do anything with it that, without\n  permission, would make you directly or secondarily liable for\n  infringement under applicable copyright law, except executing it on a\n  computer or modifying a private copy. Propagation includes copying,\n  distribution (with or without modification), making available to the\n  public, and in some countries other activities as well.\n\n  To “convey” a work means any kind of propagation that enables other\n  parties to make or receive copies. Mere interaction with a user through a\n  computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays “Appropriate Legal Notices” to the\n  extent that it includes a convenient and prominently visible feature that\n  (1) displays an appropriate copyright notice, and (2) tells the user that\n  there is no warranty for the work (except to the extent that warranties\n  are provided), that licensees may convey the work under this License, and\n  how to view a copy of this License. If the interface presents a list of\n  user commands or options, such as a menu, a prominent item in the list\n  meets this criterion.\n\n  1. Source Code.\n\n  The “source code” for a work means the preferred form of the work for\n  making modifications to it. “Object code” means any non-source form of a\n  work.\n\n  A “Standard Interface” means an interface that either is an official\n  standard defined by a recognized standards body, or, in the case of\n  interfaces specified for a particular programming language, one that is\n  widely used among developers working in that language.  The “System\n  Libraries” of an executable work include anything, other than the work as\n  a whole, that (a) is included in the normal form of packaging a Major\n  Component, but which is not part of that Major Component, and (b) serves\n  only to enable use of the work with that Major Component, or to implement\n  a Standard Interface for which an implementation is available to the\n  public in source code form. A “Major Component”, in this context, means a\n  major essential component (kernel, window system, and so on) of the\n  specific operating system (if any) on which the executable work runs, or\n  a compiler used to produce the work, or an object code interpreter used\n  to run it.\n\n  The “Corresponding Source” for a work in object code form means all the\n  source code needed to generate, install, and (for an executable work) run\n  the object code and to modify the work, including scripts to control\n  those activities. However, it does not include the work's System\n  Libraries, or general-purpose tools or generally available free programs\n  which are used unmodified in performing those activities but which are\n  not part of the work. For example, Corresponding Source includes\n  interface definition files associated with source files for the work, and\n  the source code for shared libraries and dynamically linked subprograms\n  that the work is specifically designed to require, such as by intimate\n  data communication or control flow between those subprograms and other\n  parts of the work.\n\n  The Corresponding Source need not include anything that users can\n  regenerate automatically from other parts of the Corresponding Source.\n\n  The Corresponding Source for a work in source code form is that same work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\n  copyright on the Program, and are irrevocable provided the stated\n  conditions are met. This License explicitly affirms your unlimited\n  permission to run the unmodified Program, subject to section 13. The\n  output from running a covered work is covered by this License only if the\n  output, given its content, constitutes a covered work. This License\n  acknowledges your rights of fair use or other equivalent, as provided by\n  copyright law.  Subject to section 13, you may make, run and propagate\n  covered works that you do not convey, without conditions so long as your\n  license otherwise remains in force. You may convey covered works to\n  others for the sole purpose of having them make modifications exclusively\n  for you, or provide you with facilities for running those works, provided\n  that you comply with the terms of this License in conveying all\n  material for which you do not control copyright. Those thus making or\n  running the covered works for you must do so exclusively on your\n  behalf, under your direction and control, on terms that prohibit them\n  from making any copies of your copyrighted material outside their\n  relationship with you.\n\n  Conveying under any other circumstances is permitted solely under the\n  conditions stated below. Sublicensing is not allowed; section 10 makes it\n  unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\n  measure under any applicable law fulfilling obligations under article 11\n  of the WIPO copyright treaty adopted on 20 December 1996, or similar laws\n  prohibiting or restricting circumvention of such measures.\n\n  When you convey a covered work, you waive any legal power to forbid\n  circumvention of technological measures to the extent such circumvention is\n  effected by exercising rights under this License with respect to the\n  covered work, and you disclaim any intention to limit operation or\n  modification of the work as a means of enforcing, against the work's users,\n  your or third parties' legal rights to forbid circumvention of\n  technological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\n  receive it, in any medium, provided that you conspicuously and\n  appropriately publish on each copy an appropriate copyright notice; keep\n  intact all notices stating that this License and any non-permissive terms\n  added in accord with section 7 apply to the code; keep intact all notices\n  of the absence of any warranty; and give all recipients a copy of this\n  License along with the Program.  You may charge any price or no price for\n  each copy that you convey, and you may offer support or warranty\n  protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\n  produce it from the Program, in the form of source code under the terms\n  of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified it,\n    and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is released\n    under this License and any conditions added under section 7. This\n    requirement modifies the requirement in section 4 to “keep intact all\n    notices”.\n\n    c) You must license the entire work, as a whole, under this License to\n    anyone who comes into possession of a copy. This License will therefore\n    apply, along with any applicable section 7 additional terms, to the\n    whole of the work, and all its parts, regardless of how they are\n    packaged. This License gives no permission to license the work in any\n    other way, but it does not invalidate such permission if you have\n    separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your work\n    need not make them do so.\n\n  A compilation of a covered work with other separate and independent\n  works, which are not by their nature extensions of the covered work, and\n  which are not combined with it such as to form a larger program, in or on\n  a volume of a storage or distribution medium, is called an “aggregate” if\n  the compilation and its resulting copyright are not used to limit the\n  access or legal rights of the compilation's users beyond what the\n  individual works permit. Inclusion of a covered work in an aggregate does\n  not cause this License to apply to the other parts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms of\n  sections 4 and 5, provided that you also convey the machine-readable\n  Corresponding Source under the terms of this License, in one of these\n  ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium customarily\n    used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a written\n    offer, valid for at least three years and valid for as long as you\n    offer spare parts or customer support for that product model, to give\n    anyone who possesses the object code either (1) a copy of the\n    Corresponding Source for all the software in the product that is\n    covered by this License, on a durable physical medium customarily used\n    for software interchange, for a price no more than your reasonable cost\n    of physically performing this conveying of source, or (2) access to\n    copy the Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source. This alternative is\n    allowed only occasionally and noncommercially, and only if you received\n    the object code with such an offer, in accord with subsection 6b.\n\n    d) Convey the object code by offering access from a designated place\n    (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge. You need not require recipients to copy the\n    Corresponding Source along with the object code. If the place to copy\n    the object code is a network server, the Corresponding Source may be on\n    a different server (operated by you or a third party) that supports\n    equivalent copying facilities, provided you maintain clear directions\n    next to the object code saying where to find the Corresponding Source.\n    Regardless of what server hosts the Corresponding Source, you remain\n    obligated to ensure that it is available for as long as needed to\n    satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided you\n    inform other peers where the object code and Corresponding Source of\n    the work are being offered to the general public at no charge under\n    subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\n  from the Corresponding Source as a System Library, need not be included\n  in conveying the object code work.\n\n  A “User Product” is either (1) a “consumer product”, which means any\n  tangible personal property which is normally used for personal, family,\n  or household purposes, or (2) anything designed or sold for incorporation\n  into a dwelling. In determining whether a product is a consumer product,\n  doubtful cases shall be resolved in favor of coverage. For a particular\n  product received by a particular user, “normally used” refers to a\n  typical or common use of that class of product, regardless of the status\n  of the particular user or of the way in which the particular user\n  actually uses, or expects or is expected to use, the product. A product\n  is a consumer product regardless of whether the product has substantial\n  commercial, industrial or non-consumer uses, unless such uses represent\n  the only significant mode of use of the product.\n\n  “Installation Information” for a User Product means any methods,\n  procedures, authorization keys, or other information required to install\n  and execute modified versions of a covered work in that User Product from\n  a modified version of its Corresponding Source. The information must\n  suffice to ensure that the continued functioning of the modified object\n  code is in no case prevented or interfered with solely because\n  modification has been made.\n\n  If you convey an object code work under this section in, or with, or\n  specifically for use in, a User Product, and the conveying occurs as part\n  of a transaction in which the right of possession and use of the User\n  Product is transferred to the recipient in perpetuity or for a fixed term\n  (regardless of how the transaction is characterized), the Corresponding\n  Source conveyed under this section must be accompanied by the\n  Installation Information. But this requirement does not apply if neither\n  you nor any third party retains the ability to install modified object\n  code on the User Product (for example, the work has been installed in\n  ROM).\n\n  The requirement to provide Installation Information does not include a\n  requirement to continue to provide support service, warranty, or updates\n  for a work that has been modified or installed by the recipient, or for\n  the User Product in which it has been modified or installed. Access\n  to a network may be denied when the modification itself materially\n  and adversely affects the operation of the network or violates the\n  rules and protocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided, in\n  accord with this section must be in a format that is publicly documented\n  (and with an implementation available to the public in source code form),\n  and must require no special password or key for unpacking, reading or\n  copying.\n\n  7. Additional Terms.\n\n  “Additional permissions” are terms that supplement the terms of this\n  License by making exceptions from one or more of its conditions.\n  Additional permissions that are applicable to the entire Program shall be\n  treated as though they were included in this License, to the extent that\n  they are valid under applicable law. If additional permissions apply only\n  to part of the Program, that part may be used separately under those\n  permissions, but the entire Program remains governed by this License\n  without regard to the additional permissions.  When you convey a copy of\n  a covered work, you may at your option remove any additional permissions\n  from that copy, or from any part of it. (Additional permissions may be\n  written to require their own removal in certain cases when you modify the\n  work.) You may place additional permissions on material, added by you to\n  a covered work, for which you have or can give appropriate copyright\n  permission.\n\n  Notwithstanding any other provision of this License, for material you add\n  to a covered work, you may (if authorized by the copyright holders of\n  that material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some trade\n    names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that material\n    by anyone who conveys the material (or modified versions of it) with\n    contractual assumptions of liability to the recipient, for any\n    liability that these contractual assumptions directly impose on those\n    licensors and authors.\n\n  All other non-permissive additional terms are considered “further\n  restrictions” within the meaning of section 10. If the Program as you\n  received it, or any part of it, contains a notice stating that it is\n  governed by this License along with a term that is a further restriction,\n  you may remove that term. If a license document contains a further\n  restriction but permits relicensing or conveying under this License, you\n  may add to a covered work material governed by the terms of that license\n  document, provided that the further restriction does not survive such\n  relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you must\n  place, in the relevant source files, a statement of the additional terms\n  that apply to those files, or a notice indicating where to find the\n  applicable terms.  Additional terms, permissive or non-permissive, may be\n  stated in the form of a separately written license, or stated as\n  exceptions; the above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\n  provided under this License. Any attempt otherwise to propagate or modify\n  it is void, and will automatically terminate your rights under this\n  License (including any patent licenses granted under the third paragraph\n  of section 11).\n\n  However, if you cease all violation of this License, then your license\n  from a particular copyright holder is reinstated (a) provisionally,\n  unless and until the copyright holder explicitly and finally terminates\n  your license, and (b) permanently, if the copyright holder fails to\n  notify you of the violation by some reasonable means prior to 60 days\n  after the cessation.\n\n  Moreover, your license from a particular copyright holder is reinstated\n  permanently if the copyright holder notifies you of the violation by some\n  reasonable means, this is the first time you have received notice of\n  violation of this License (for any work) from that copyright holder, and\n  you cure the violation prior to 30 days after your receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\n  licenses of parties who have received copies or rights from you under\n  this License. If your rights have been terminated and not permanently\n  reinstated, you do not qualify to receive new licenses for the same\n  material under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or run a\n  copy of the Program. Ancillary propagation of a covered work occurring\n  solely as a consequence of using peer-to-peer transmission to receive a\n  copy likewise does not require acceptance. However, nothing other than\n  this License grants you permission to propagate or modify any covered\n  work. These actions infringe copyright if you do not accept this License.\n  Therefore, by modifying or propagating a covered work, you indicate your\n  acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically receives\n  a license from the original licensors, to run, modify and propagate that\n  work, subject to this License. You are not responsible for enforcing\n  compliance by third parties with this License.\n\n  An “entity transaction” is a transaction transferring control of an\n  organization, or substantially all assets of one, or subdividing an\n  organization, or merging organizations. If propagation of a covered work\n  results from an entity transaction, each party to that transaction who\n  receives a copy of the work also receives whatever licenses to the work\n  the party's predecessor in interest had or could give under the previous\n  paragraph, plus a right to possession of the Corresponding Source of the\n  work from the predecessor in interest, if the predecessor has it or can\n  get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the rights\n  granted or affirmed under this License. For example, you may not impose a\n  license fee, royalty, or other charge for exercise of rights granted\n  under this License, and you may not initiate litigation (including a\n  cross-claim or counterclaim in a lawsuit) alleging that any patent claim\n  is infringed by making, using, selling, offering for sale, or importing\n  the Program or any portion of it.\n\n  11. Patents.\n\n  A “contributor” is a copyright holder who authorizes use under this\n  License of the Program or a work on which the Program is based. The work\n  thus licensed is called the contributor's “contributor version”.\n\n  A contributor's “essential patent claims” are all patent claims owned or\n  controlled by the contributor, whether already acquired or hereafter\n  acquired, that would be infringed by some manner, permitted by this\n  License, of making, using, or selling its contributor version, but do not\n  include claims that would be infringed only as a consequence of further\n  modification of the contributor version. For purposes of this definition,\n  “control” includes the right to grant patent sublicenses in a manner\n  consistent with the requirements of this License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\n  patent license under the contributor's essential patent claims, to make,\n  use, sell, offer for sale, import and otherwise run, modify and propagate\n  the contents of its contributor version.\n\n  In the following three paragraphs, a “patent license” is any express\n  agreement or commitment, however denominated, not to enforce a patent\n  (such as an express permission to practice a patent or covenant not to\n  sue for patent infringement). To “grant” such a patent license to a party\n  means to make such an agreement or commitment not to enforce a patent\n  against the party.\n\n  If you convey a covered work, knowingly relying on a patent license, and\n  the Corresponding Source of the work is not available for anyone to copy,\n  free of charge and under the terms of this License, through a publicly\n  available network server or other readily accessible means, then you must\n  either (1) cause the Corresponding Source to be so available, or (2)\n  arrange to deprive yourself of the benefit of the patent license for this\n  particular work, or (3) arrange, in a manner consistent with the\n  requirements of this License, to extend the patent license to downstream\n  recipients. “Knowingly relying” means you have actual knowledge that, but\n  for the patent license, your conveying the covered work in a country, or\n  your recipient's use of the covered work in a country, would infringe\n  one or more identifiable patents in that country that you have reason\n  to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\n  arrangement, you convey, or propagate by procuring conveyance of, a\n  covered work, and grant a patent license to some of the parties receiving\n  the covered work authorizing them to use, propagate, modify or convey a\n  specific copy of the covered work, then the patent license you grant is\n  automatically extended to all recipients of the covered work and works\n  based on it.\n\n  A patent license is “discriminatory” if it does not include within the\n  scope of its coverage, prohibits the exercise of, or is conditioned on\n  the non-exercise of one or more of the rights that are specifically\n  granted under this License. You may not convey a covered work if you are\n  a party to an arrangement with a third party that is in the business of\n  distributing software, under which you make payment to the third party\n  based on the extent of your activity of conveying the work, and under\n  which the third party grants, to any of the parties who would receive the\n  covered work from you, a discriminatory patent license (a) in connection\n  with copies of the covered work conveyed by you (or copies made from\n  those copies), or (b) primarily for and in connection with specific\n  products or compilations that contain the covered work, unless you\n  entered into that arrangement, or that patent license was granted, prior\n  to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting any\n  implied license or other defenses to infringement that may otherwise be\n  available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\n  otherwise) that contradict the conditions of this License, they do not\n  excuse you from the conditions of this License. If you cannot use,\n  propagate or convey a covered work so as to satisfy simultaneously your\n  obligations under this License and any other pertinent obligations, then\n  as a consequence you may not use, propagate or convey it at all. For\n  example, if you agree to terms that obligate you to collect a royalty for\n  further conveying from those to whom you convey the Program, the only way\n  you could satisfy both those terms and this License would be to refrain\n  entirely from conveying the Program.\n\n  13. Offering the Program as a Service.\n\n  If you make the functionality of the Program or a modified version\n  available to third parties as a service, you must make the Service Source\n  Code available via network download to everyone at no charge, under the\n  terms of this License. Making the functionality of the Program or\n  modified version available to third parties as a service includes,\n  without limitation, enabling third parties to interact with the\n  functionality of the Program or modified version remotely through a\n  computer network, offering a service the value of which entirely or\n  primarily derives from the value of the Program or modified version, or\n  offering a service that accomplishes for users the primary purpose of the\n  Program or modified version.\n\n  “Service Source Code” means the Corresponding Source for the Program or\n  the modified version, and the Corresponding Source for all programs that\n  you use to make the Program or modified version available as a service,\n  including, without limitation, management software, user interfaces,\n  application program interfaces, automation software, monitoring software,\n  backup software, storage software and hosting software, all such that a\n  user could run an instance of the service using the Service Source Code\n  you make available.\n\n  14. Revised Versions of this License.\n\n  MongoDB, Inc. may publish revised and/or new versions of the Server Side\n  Public License from time to time. Such new versions will be similar in\n  spirit to the present version, but may differ in detail to address new\n  problems or concerns.\n\n  Each version is given a distinguishing version number. If the Program\n  specifies that a certain numbered version of the Server Side Public\n  License “or any later version” applies to it, you have the option of\n  following the terms and conditions either of that numbered version or of\n  any later version published by MongoDB, Inc. If the Program does not\n  specify a version number of the Server Side Public License, you may\n  choose any version ever published by MongoDB, Inc.\n\n  If the Program specifies that a proxy can decide which future versions of\n  the Server Side Public License can be used, that proxy's public statement\n  of acceptance of a version permanently authorizes you to choose that\n  version for the Program.\n\n  Later license versions may give you additional or different permissions.\n  However, no additional obligations are imposed on any author or copyright\n  holder as a result of your choosing to follow a later version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\n  APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\n  HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY\n  OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\n  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n  PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\n  IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\n  ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\n  WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\n  THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING\n  ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF\n  THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO\n  LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU\n  OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\n  PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\n  POSSIBILITY OF SUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided above\n  cannot be given local legal effect according to their terms, reviewing\n  courts shall apply local law that most closely approximates an absolute\n  waiver of all civil liability in connection with the Program, unless a\n  warranty or assumption of liability accompanies a copy of the Program in\n  return for a fee.\n\n                        END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "README.md",
    "content": "[![Release](https://img.shields.io/github/v/release/RedisInsight/RedisInsight.svg?sort=semver)](https://github.com/RedisInsight/RedisInsight/releases)\n\n# <img src=\"https://github.com/RedisInsight/RedisInsight/blob/main/resources/icon.png\" alt=\"logo\" width=\"25\"/> Redis Insight - Developer GUI for Redis, by Redis.\n\n[![Forum](https://img.shields.io/badge/Forum-RedisInsight-red)](https://forum.redis.com/c/redisinsight/65)\n[![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](https://discord.gg/QUkjSsk)\n\nRedis Insight is a visual tool that provides capabilities to design, develop, and optimize your Redis application.\nQuery, analyse and interact with your Redis data. [Download it here](https://redis.io/insight/#insight-form)!\n\n![Redis Insight Browser screenshot](/.github/screenshots/Redis-Insight-Browser.png)\n\n| Workbench | Analysis | Slow Log | CLI |\n| - | - | - | - |\n| ![Redis Insight Workbench screenshot](/.github/screenshots/Redis-Insight-Workbench.png) | ![Redis Insight Analysis screenshot](/.github/screenshots/Redis-Insight-Analysis.png) | ![Redis Insight SlowLog screenshot](/.github/screenshots/Redis-Insight-SlowLog.png) | ![Redis Insight CLI screenshot](/.github/screenshots/Redis-Insight-CLI.png) |\n\nBuilt with love using [Electron](https://www.electronjs.org/), [Monaco Editor](https://microsoft.github.io/monaco-editor/) and NodeJS.\n\n## Overview\n\nRedis Insight is an intuitive and efficient GUI for Redis, allowing you to interact with your databases and manage your data—with built-in support for Redis modules.\n\n### Redis Insight Highlights:\n\n- Browse, filter, visualise your key-value Redis data structures and see key values in different formats (including JSON, Hex, ASCII, etc.)\n- CRUD support for lists, hashes, strings, sets, sorted sets, and streams\n- CRUD support for [JSON](https://redis.io/json/) data structure\n- Interactive tutorials to learn easily, among other things, how to leverage the native JSON data structure supporting structured querying and full-text search, including vector similarity search for your AI use cases\n- Contextualised recommendations to optimize performance and memory usage. The list of recommendations gets updated as you interact with your database\n- Profiler - analyze every command sent to Redis in real-time\n- SlowLog - analyze slow operations in Redis instances based on the [Slowlog](https://github.com/RedisInsight/RedisInsight/releases#:~:text=results%20of%20the-,Slowlog,-command%20to%20analyze) command\n- Pub/Sub - support for [Redis pub/sub](https://redis.io/docs/latest/develop/interact/pubsub/), enabling subscription to channels and posting messages to channels\n- Bulk actions - Delete the keys in bulk based on the filters set in Browser or Tree view\n- Workbench - advanced command line interface with intelligent command auto-complete, complex data visualizations and support for the raw mode\n- Command auto-complete support for [search and query](https://redis.io/search/) capability, [JSON](https://redis.io/json/) and [time series](https://redis.io/timeseries/) data structures\n- Visualizations of your [search and query](https://redis.io/search/) indexes and results.\n- Ability to build [your own data visualization plugins](https://github.com/RedisInsight/Packages)\n- Officially supported for Redis OSS, [Redis Cloud](https://redis.io/cloud/). Works with Microsoft Azure Cache for Redis\n\nCheck out the [release notes](https://github.com/RedisInsight/RedisInsight/releases).\n\n## Get started with Redis Insight\n\nThis repository includes the code for Redis Insight. Check out the [blogpost](https://redis.com/blog/introducing-redisinsight-2/) announcing it.\n\n### Installable\n\nRedis Insight is available as a free download [redis.io](https://redis.io/insight/#insight-form).\nYou can also find it on the Microsoft Store, Apple App Store, Snapcraft, Flathub, and as a [Docker image](https://hub.docker.com/r/redis/redisinsight).\n\nAdditionally, you can use [Redis for VS Code](https://github.com/RedisInsight/Redis-for-VS-Code), our official Visual Studio Code extension.\n\n### Build\n\nAlternatively you can also build from source. See our wiki for instructions.\n\n- [How to build](https://github.com/RedisInsight/RedisInsight/wiki/How-to-build-and-contribute)\n\n## How to debug\n\nIf you have any issues occurring in Redis Insight, you can follow the steps below to get more information about the errors and find their root cause.\n\n- [How to debug](https://github.com/RedisInsight/RedisInsight/wiki/How-to-debug)\n\n## Redis Insight API (only for Docker)\n\nIf you are running Redis Insight from [Docker](https://hub.docker.com/r/redis/redisinsight), you can access the API from `http://localhost:5540/api/docs`.\n\n## Azure Cache for Redis\n\nRedis Insight supports Azure Cache for Redis with Microsoft Entra ID authentication. If your organization requires admin consent for third-party applications, see our setup guide.\n\n- [Azure Setup Guide](docs/azure-setup.md)\n- [Azure Docker Setup](docs/azure-docker-setup.md) - Configuration for Docker, custom ports, and reverse proxies\n\n## Feedback\n\n- Request a new [feature](https://github.com/RedisInsight/RedisInsight/issues/new?assignees=&labels=&template=feature_request.md&title=%5BFeature+Request%5D%3A)\n- Upvote [popular feature requests](https://github.com/RedisInsight/RedisInsight/issues?q=is%3Aopen+is%3Aissue+label%3Afeature+sort%3Areactions-%2B1-desc)\n- File a [bug](https://github.com/RedisInsight/RedisInsight/issues/new?assignees=&labels=&template=bug_report.md&title=%5BBug%5D%3A)\n\n## Redis Insight Plugins\n\nWith Redis Insight you can now also extend the core functionality by building your own data visualizations. See our wiki for more information.\n\n- [Plugin Documentation](https://github.com/RedisInsight/RedisInsight/wiki/Plugin-Documentation)\n\n## Contributing\n\nIf you would like to contribute to the code base or fix and issue, please consult the wiki.\n\n- [How to build and contribute](https://github.com/RedisInsight/RedisInsight/wiki/How-to-build-and-contribute)\n\n## API documentation\n\nIf you're using a Docker image of Redis Insight, open this URL to view the list of APIs:\nhttp://localhost:5530/api/docs\n\n## Telemetry\n\nRedis Insight includes an opt-in telemetry system, that is leveraged to help improve the developer experience (DX) within the app. We value your privacy, so stay assured, that all the data collected is anonymised.\n\n## License\n\nRedis Insight is licensed under [SSPL](/LICENSE) license.\n"
  },
  {
    "path": "api-docker-entry.sh",
    "content": "#!/bin/sh\n\n# Initializing system's secret storage\neval \"$(dbus-launch --sh-syntax)\"\n\nmkdir -p ~/.cache\nmkdir -p ~/.local/share/keyrings\n# fix \"Remote error from secret service:\n# org.freedesktop.Secret.Error.IsLocked: Cannot create an item in a locked collection\" issue\neval \"$(echo \"$GNOME_KEYRING_PASS\" | gnome-keyring-daemon --unlock)\"\nsleep 1\neval \"$(echo \"$GNOME_KEYRING_PASS\" | gnome-keyring-daemon --start)\"\n\nexec \"$@\"\n"
  },
  {
    "path": "babel.config.cjs",
    "content": "/* eslint global-require: off, import/no-extraneous-dependencies: off */\nconst developmentEnv = ['development', 'test'];\nmodule.exports = (api) => {\n  const development = api.env(developmentEnv);\n\n  return {\n    presets: [\n      require('@babel/preset-env'),\n      require('@babel/preset-typescript'),\n      [require('@babel/preset-react'), { development }],\n      [require('babel-preset-vite'), { env: true, glob: false }],\n    ],\n    // added to support storybook\n    plugins: [\n      [\n        require('@babel/plugin-proposal-decorators'),\n        { version: '2023-11', decoratorsBeforeExport: true },\n      ],\n    ],\n  };\n};\n"
  },
  {
    "path": "configs/.eslintrc",
    "content": "{\n  \"rules\": {\n    \"no-console\": \"off\",\n    \"global-require\": \"off\",\n    \"import/no-dynamic-require\": \"off\"\n  }\n}\n"
  },
  {
    "path": "configs/webpack.config.base.ts",
    "content": "import webpack from 'webpack'\nimport TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'\nimport webpackPaths from './webpack.paths'\nimport { dependencies as externals } from '../redisinsight/package.json'\nimport { resolve } from 'path'\n\nconst configuration: webpack.Configuration = {\n  externals: [...Object.keys(externals || {})],\n\n  stats: 'errors-only',\n\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        exclude: /node_modules/,\n        use: [\n          {\n            loader: 'ts-loader',\n            options: {\n              transpileOnly: true,\n              compilerOptions: {\n                module: 'esnext',\n              },\n            },\n          },\n        ],\n      },\n    ],\n  },\n\n  output: {\n    path: webpackPaths.riPath,\n    // https://github.com/webpack/webpack/issues/1114\n    library: {\n      type: 'commonjs2',\n    },\n  },\n\n  resolve: {\n    extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '.scss'],\n    modules: [webpackPaths.apiPath, 'node_modules'],\n    plugins: [new TsconfigPathsPlugins()],\n    alias: {\n      'class-transformer': resolve(\n        './redisinsight/api/node_modules/class-transformer/cjs',\n      ),\n    },\n  },\n\n  plugins: [\n    new webpack.EnvironmentPlugin({\n      NODE_ENV: 'production',\n    }),\n\n    new webpack.IgnorePlugin({\n      checkResource(resource) {\n        const lazyImports = [\n          '@nestjs/microservices',\n          // '@nestjs/platform-express',\n          // 'pnpapi',\n          'cache-manager',\n          // 'class-validator',\n          '@fastify/static',\n          'fastify-swagger',\n          // 'hiredis',\n          // 'reflect-metadata',\n          // 'swagger-ui-express',\n          // 'class-transformer',\n          // 'class-transformer/storage',\n          // '@nestjs/websockets',\n          // '@nestjs/core/adapters/http-adapter',\n          // '@nestjs/core/helpers/router-method-factory',\n          // '@nestjs/core/metadata-scanner',\n          '@nestjs/microservices/microservices-module',\n          // '@nestjs/websockets/socket-module',\n        ]\n        if (!lazyImports.includes(resource)) {\n          return false\n        }\n        try {\n          require.resolve(resource)\n        } catch (err) {\n          return true\n        }\n        return false\n      },\n    }),\n  ],\n}\n\nexport default configuration\n"
  },
  {
    "path": "configs/webpack.config.eslint.js",
    "content": "// eslint-disable-next-line import/no-self-import\n/* eslint import/no-unresolved: off, import/no-self-import: off */\n\nmodule.exports = require('./webpack.config.renderer.dev').default;\n"
  },
  {
    "path": "configs/webpack.config.main.prod.ts",
    "content": "import path from 'path'\nimport webpack from 'webpack'\nimport { merge } from 'webpack-merge'\nimport { toString } from 'lodash'\nimport { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'\nimport baseConfig from './webpack.config.base.ts'\nimport DeleteSourceMaps from '../scripts/DeleteSourceMaps'\nimport { version } from '../redisinsight/package.json'\nimport webpackPaths from './webpack.paths'\n\nDeleteSourceMaps()\n\nconst devtoolsConfig =\n  process.env.DEBUG_PROD === 'true'\n    ? {\n        devtool: 'source-map',\n      }\n    : {}\n\nexport default merge(baseConfig, {\n  ...devtoolsConfig,\n\n  mode: 'development',\n\n  target: 'electron-main',\n\n  entry: {\n    main: path.join(webpackPaths.desktopPath, 'index.ts'),\n    preload: path.join(webpackPaths.desktopPath, 'preload.ts'),\n  },\n\n  output: {\n    path: webpackPaths.distMainPath,\n    filename: '[name].js',\n    library: {\n      type: 'umd',\n    },\n  },\n\n  // optimization: {\n  //   minimizer: [\n  //     new TerserPlugin({\n  //       parallel: true,\n  //     }),\n  //   ],\n  // },\n\n  plugins: [\n    new BundleAnalyzerPlugin({\n      analyzerMode:\n        process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',\n      openAnalyzer: process.env.OPEN_ANALYZER === 'true',\n    }),\n\n    new webpack.EnvironmentPlugin({\n      NODE_ENV: 'production',\n      DEBUG_PROD: false,\n      START_MINIMIZED: false,\n      RI_APP_TYPE: process.env.RI_APP_TYPE || 'ELECTRON',\n      RI_AUTO_BOOTSTRAP: 'false',\n      RI_SERVER_TLS_CERT: process.env.RI_SERVER_TLS_CERT || '',\n      RI_SERVER_TLS_KEY: process.env.RI_SERVER_TLS_KEY || '',\n      RI_SERVE_STATICS: false,\n      RI_APP_FOLDER_NAME: process.env.RI_APP_FOLDER_NAME || '',\n      RI_UPGRADES_LINK: process.env.RI_UPGRADES_LINK || '',\n      RI_DISABLE_AUTO_UPGRADE: process.env.RI_DISABLE_AUTO_UPGRADE || 'false',\n      RI_ANALYTICS_START_EVENTS: 'true',\n      RI_APP_HOST: '127.0.0.1',\n      RI_BUILD_TYPE: 'ELECTRON',\n      RI_APP_VERSION: version,\n      RI_SEGMENT_WRITE_KEY:\n        'RI_SEGMENT_WRITE_KEY' in process.env\n          ? process.env.RI_SEGMENT_WRITE_KEY\n          : 'SOURCE_WRITE_KEY',\n      RI_CONNECTIONS_TIMEOUT_DEFAULT:\n        'RI_CONNECTIONS_TIMEOUT_DEFAULT' in process.env\n          ? process.env.RI_CONNECTIONS_TIMEOUT_DEFAULT\n          : toString(30 * 1000), // 30 sec\n      // cloud auth\n      RI_CLOUD_IDP_AUTHORIZE_URL:\n        'RI_CLOUD_IDP_AUTHORIZE_URL' in process.env\n          ? process.env.RI_CLOUD_IDP_AUTHORIZE_URL\n          : '',\n      RI_CLOUD_IDP_TOKEN_URL:\n        'RI_CLOUD_IDP_TOKEN_URL' in process.env\n          ? process.env.RI_CLOUD_IDP_TOKEN_URL\n          : '',\n      RI_CLOUD_IDP_REVOKE_TOKEN_URL:\n        'RI_CLOUD_IDP_REVOKE_TOKEN_URL' in process.env\n          ? process.env.RI_CLOUD_IDP_REVOKE_TOKEN_URL\n          : '',\n      RI_CLOUD_IDP_ISSUER:\n        'RI_CLOUD_IDP_ISSUER' in process.env\n          ? process.env.RI_CLOUD_IDP_ISSUER\n          : '',\n      RI_CLOUD_IDP_CLIENT_ID:\n        'RI_CLOUD_IDP_CLIENT_ID' in process.env\n          ? process.env.RI_CLOUD_IDP_CLIENT_ID\n          : '',\n      RI_CLOUD_IDP_REDIRECT_URI:\n        'RI_CLOUD_IDP_REDIRECT_URI' in process.env\n          ? process.env.RI_CLOUD_IDP_REDIRECT_URI\n          : '',\n      RI_CLOUD_IDP_GOOGLE_ID:\n        'RI_CLOUD_IDP_GOOGLE_ID' in process.env\n          ? process.env.RI_CLOUD_IDP_GOOGLE_ID\n          : '',\n      RI_CLOUD_IDP_GH_ID:\n        'RI_CLOUD_IDP_GH_ID' in process.env\n          ? process.env.RI_CLOUD_IDP_GH_ID\n          : '',\n      RI_CLOUD_API_URL:\n        'RI_CLOUD_API_URL' in process.env ? process.env.RI_CLOUD_API_URL : '',\n      RI_CLOUD_CAPI_URL:\n        'RI_CLOUD_CAPI_URL' in process.env ? process.env.RI_CLOUD_CAPI_URL : '',\n      RI_CLOUD_API_TOKEN:\n        'RI_CLOUD_API_TOKEN' in process.env\n          ? process.env.RI_CLOUD_API_TOKEN\n          : '',\n      RI_AI_CONVAI_TOKEN:\n        'RI_AI_CONVAI_TOKEN' in process.env\n          ? process.env.RI_AI_CONVAI_TOKEN\n          : '',\n      RI_AI_QUERY_USER:\n        'RI_AI_QUERY_USER' in process.env ? process.env.RI_AI_QUERY_USER : '',\n      RI_AI_QUERY_PASS:\n        'RI_AI_QUERY_PASS' in process.env ? process.env.RI_AI_QUERY_PASS : '',\n      RI_FEATURES_CONFIG_URL:\n        'RI_FEATURES_CONFIG_URL' in process.env\n          ? process.env.RI_FEATURES_CONFIG_URL\n          : '',\n    }),\n\n    new webpack.DefinePlugin({\n      'process.type': '\"browser\"',\n    }),\n  ],\n\n  /**\n   * Disables webpack processing of __dirname and __filename.\n   * If you run the bundle in node.js it falls back to these values of node.js.\n   * https://github.com/webpack/webpack/issues/2010\n   */\n  node: {\n    __dirname: false,\n    __filename: false,\n  },\n})\n"
  },
  {
    "path": "configs/webpack.config.main.stage.ts",
    "content": "import webpack from 'webpack'\nimport { merge } from 'webpack-merge'\nimport { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'\nimport mainProdConfig from './webpack.config.main.prod'\nimport DeleteSourceMaps from '../scripts/DeleteSourceMaps'\n\nDeleteSourceMaps()\n\nexport default merge(mainProdConfig, {\n  plugins: [\n    new BundleAnalyzerPlugin({\n      analyzerMode:\n        process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',\n      openAnalyzer: process.env.OPEN_ANALYZER === 'true',\n    }),\n\n    new webpack.EnvironmentPlugin({\n      NODE_ENV: 'staging',\n    }),\n  ],\n})\n"
  },
  {
    "path": "configs/webpack.config.preload.dev.ts",
    "content": "import path from 'path'\nimport webpack from 'webpack'\nimport { merge } from 'webpack-merge'\nimport { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'\nimport baseConfig from './webpack.config.base'\nimport webpackPaths from './webpack.paths'\n\nconst configuration: webpack.Configuration = {\n  devtool: 'inline-source-map',\n\n  mode: 'development',\n\n  target: 'electron-preload',\n\n  entry: path.join(webpackPaths.desktopPath, 'preload.ts'),\n\n  output: {\n    path: webpackPaths.dllPath,\n    filename: 'preload.js',\n    library: {\n      type: 'umd',\n    },\n  },\n\n  plugins: [\n    new BundleAnalyzerPlugin({\n      analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',\n    }),\n\n    /**\n     * Create global constants which can be configured at compile time.\n     *\n     * Useful for allowing different behaviour between development builds and\n     * release builds\n     *\n     * NODE_ENV should be production so that modules do not perform certain\n     * development checks\n     *\n     * By default, use 'development' as NODE_ENV. This can be overriden with\n     * 'staging', for example, by changing the ENV variables in the npm scripts\n     */\n    new webpack.EnvironmentPlugin({\n      NODE_ENV: 'development',\n    }),\n\n    new webpack.LoaderOptionsPlugin({\n      debug: true,\n    }),\n  ],\n\n  /**\n   * Disables webpack processing of __dirname and __filename.\n   * If you run the bundle in node.js it falls back to these values of node.js.\n   * https://github.com/webpack/webpack/issues/2010\n   */\n  node: {\n    __dirname: false,\n    __filename: false,\n  },\n\n  watch: true,\n}\n\nexport default merge(baseConfig, configuration)\n"
  },
  {
    "path": "configs/webpack.paths.ts",
    "content": "const path = require('path')\n\nconst rootPath = path.join(__dirname, '..')\n\nconst riPath = path.join(rootPath, 'redisinsight')\n\nconst apiPath = path.join(riPath, 'api')\nconst uiPath = path.join(riPath, 'ui')\nconst apiSrcPath = path.join(apiPath, 'src')\nconst uiSrcPath = path.join(uiPath, 'src')\nconst desktopPath = path.join(riPath, 'desktop')\nconst desktopSrcPath = path.join(desktopPath, 'src')\n\nconst dllPath = path.join(desktopPath, 'dll')\n\nconst releasePath = path.join(rootPath, 'release')\nconst appPackagePath = path.join(riPath, 'package.json')\nconst appNodeModulesPath = path.join(releasePath, 'node_modules')\nconst buildAppPackagePath = path.join(releasePath, 'package.json')\nconst srcNodeModulesPath = path.join(apiPath, 'node_modules')\n\nconst distPath = path.join(riPath, 'dist')\nconst distMainPath = path.join(distPath, 'main')\nconst distRendererPath = path.join(distPath, 'renderer')\n\nexport default {\n  rootPath,\n  dllPath,\n  apiPath,\n  uiPath,\n  riPath,\n  apiSrcPath,\n  uiSrcPath,\n  releasePath,\n  desktopPath,\n  desktopSrcPath,\n  appPackagePath,\n  appNodeModulesPath,\n  srcNodeModulesPath,\n  distPath,\n  distMainPath,\n  distRendererPath,\n  buildAppPackagePath,\n  buildPath: releasePath,\n}\n"
  },
  {
    "path": "docker-entry.sh",
    "content": "#!/bin/sh\n# Entry point for distributable docker image\n# This script does some setup required for bootstrapping the container\n# and then runs whatever is passed as arguments to this script.\n# If the CMD directive is specified in the Dockerfile, those commands\n# are passed to this script. This can be overridden by the user in the\n# `docker run`\nset -e\n\necho \"Running docker-entry.sh\"\n\n# Run the application's entry script with the exec command so it catches SIGTERM properly\nexec \"$@\"\n"
  },
  {
    "path": "docs/azure-docker-setup.md",
    "content": "# Azure Authentication in Docker\n\nThis guide covers using Azure Entra ID authentication with RedisInsight when running in Docker.\n\n> **Prerequisites:** Your Azure tenant must have admin consent granted for RedisInsight. See [Azure Setup Guide](azure-setup.md) for initial setup.\n\n## Important: Localhost Access Required\n\nAzure Entra ID authentication in RedisInsight uses a **public client application** which only supports `localhost` redirect URIs. This means:\n\n- ✅ Access via `http://localhost:PORT` - **works**\n- ❌ Access via `http://127.0.0.1:PORT` - does not work\n- ❌ Access via `http://your-server-ip:PORT` - does not work\n- ❌ Access via custom domain - does not work\n\nIf you need to access RedisInsight from a remote machine, use SSH port forwarding:\n\n```bash\nssh -L 5540:localhost:5540 user@remote-server\n# Then access http://localhost:5540 on your local machine\n```\n\n## Supported Ports\n\nAzure Entra ID authentication is supported on the following localhost ports:\n\n| Port | URL                     | Use Case                  |\n| ---- | ----------------------- | ------------------------- |\n| 5540 | `http://localhost:5540` | Default RedisInsight port |\n| 8000 | `http://localhost:8000` | Alternative port          |\n| 8001 | `http://localhost:8001` | Alternative port          |\n| 8002 | `http://localhost:8002` | Alternative port          |\n| 8003 | `http://localhost:8003` | Alternative port          |\n| 8004 | `http://localhost:8004` | Alternative port          |\n| 8005 | `http://localhost:8005` | Alternative port          |\n\nWhen using a non-default port, set `RI_EXTERNAL_URL` to match:\n\n```bash\ndocker run -p 8000:5540 -e RI_EXTERNAL_URL=http://localhost:8000 redis/redisinsight:latest\n```\n\n> **Note:** Azure Entra ID authentication requires one of the supported localhost ports. Custom domains are not currently supported.\n\n## Quick Start\n\n### Standard Docker (Port 5540)\n\nIf you run RedisInsight on the default port, Azure authentication works out of the box:\n\n```bash\ndocker run -d -p 5540:5540 redis/redisinsight:latest\n```\n\nAccess at `http://localhost:5540` and use \"Sign in with Microsoft\" to authenticate.\n\n### Custom Port Mapping\n\nWhen mapping to a different external port, set `RI_EXTERNAL_URL` so OAuth callbacks work correctly:\n\n```bash\ndocker run -d \\\n  -p 8000:5540 \\\n  -e RI_EXTERNAL_URL=http://localhost:8000 \\\n  redis/redisinsight:latest\n```\n\nAccess at `http://localhost:8000`.\n\n### Docker Compose\n\n```yaml\nversion: '3'\nservices:\n  redisinsight:\n    image: redis/redisinsight:latest\n    ports:\n      - '8000:5540'\n    environment:\n      - RI_EXTERNAL_URL=http://localhost:8000\n    volumes:\n      - redisinsight-data:/data\n\nvolumes:\n  redisinsight-data:\n```\n\n## Reverse Proxy Setup\n\nWhen running behind a reverse proxy, RedisInsight will work normally for general usage. However, **Azure Entra ID authentication requires accessing RedisInsight via one of the [supported localhost ports](#supported-ports)**.\n\n### Nginx Example\n\n```nginx\nserver {\n    listen 443 ssl;\n    server_name redisinsight.example.com;\n\n    location / {\n        proxy_pass http://localhost:5540;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n```\n\n> **Note:** For Azure Entra ID authentication, access RedisInsight directly via `http://localhost:5540` instead of the reverse proxy URL.\n\n## Environment Variables\n\n| Variable          | Description                                                      | Default                        |\n| ----------------- | ---------------------------------------------------------------- | ------------------------------ |\n| `RI_EXTERNAL_URL` | External URL for OAuth callbacks (e.g., `http://localhost:8000`) | None (uses `localhost:{port}`) |\n| `RI_APP_PORT`     | Internal port the application listens on                         | `5540`                         |\n| `RI_APP_HOST`     | Host address to bind to                                          | `0.0.0.0`                      |\n\n## Kubernetes / Helm\n\nWhen deploying to Kubernetes, use one of the [supported ports](#supported-ports) and set `RI_EXTERNAL_URL`:\n\n```yaml\nenv:\n  - name: RI_EXTERNAL_URL\n    value: 'http://localhost:8000'\n```\n"
  },
  {
    "path": "docs/azure-setup.md",
    "content": "# Azure Cache for Redis Setup\n\n## Setup Instructions\n\nTo use the Azure integration, your Azure tenant administrator may need to grant admin consent for the RedisInsight application. This is a one-time setup per Azure tenant — once done, all users in your organization can use RedisInsight with Entra ID seamlessly.\n\n> **Why is this needed?** See [Why This Setup is Required](#why-this-setup-is-required) for details on the authentication flow.\n\n> **Running in Docker?** See [Azure Docker Setup](azure-docker-setup.md) for configuration when using custom ports or reverse proxies.\n\n### App Registration Details\n\n- **Application (Client) ID:** `61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0`\n\n### Required Permissions\n\nThe RedisInsight app requires the following API permissions:\n\n| API                            | Permission | Type      | Purpose                                            |\n| ------------------------------ | ---------- | --------- | -------------------------------------------------- |\n| `https://redis.azure.com`      | `.default` | Delegated | Connect to Azure Cache for Redis using Entra ID    |\n| `https://management.azure.com` | `.default` | Delegated | Auto-discover Redis databases across subscriptions |\n\n### Granting Admin Consent (Azure CLI)\n\nRun these commands in [Azure Cloud Shell](https://portal.azure.com/#cloudshell/) or your local terminal with Azure CLI installed:\n\n```bash\n# Step 1: Create the service principals in your tenant\n# 61f3d82d-... = RedisInsight application\n# acca5fbb-... = Azure Cache for Redis API (AzureRedisCacheAadApp)\naz ad sp create --id 61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0\naz ad sp create --id acca5fbb-b7e4-4009-81f1-37e38fd66d78\n\n# Step 2: Grant permissions\n# Grant RedisInsight access to Azure Cache for Redis API\naz ad app permission grant \\\n  --id 61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0 \\\n  --api acca5fbb-b7e4-4009-81f1-37e38fd66d78 \\\n  --scope user_impersonation\n\n# Grant RedisInsight access to Azure Resource Manager API\n# 797f4846-... = Azure Resource Manager (for autodiscovery)\naz ad app permission grant \\\n  --id 61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0 \\\n  --api 797f4846-ba00-4fd7-ba43-dac1f8f63013 \\\n  --scope user_impersonation\n\n# Step 3: Verify permissions were granted\naz ad app permission list-grants \\\n  --id 61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0 \\\n  --show-resource-name\n```\n\nYou should see `AzureRedisCacheAadApp` and `Windows Azure Service Management API` (or `Azure Resource Manager`) in the output.\n\n## Troubleshooting\n\n### Error: AADSTS650057 - Invalid resource\n\nIf you see this error:\n\n> Invalid resource. The client has requested access to a resource which is not listed in the requested permissions in the client's application registration.\n\nThis means admin consent has not been granted for the RedisInsight application in your Azure tenant. Follow the \"Granting Admin Consent (Azure CLI)\" steps above.\n\n### Error: AADSTS65006 - No entitlements matching required permissions\n\nIf you see this error:\n\n> Resource 'acca5fbb-...' had no entitlements matching required permissions configured on the required resource access for client '61f3d82d-...'.\n\nThis means the Azure Cache for Redis API permissions haven't been granted. Run these commands:\n\n```bash\n# Create the service principal for Azure Cache for Redis API\naz ad sp create --id acca5fbb-b7e4-4009-81f1-37e38fd66d78\n\n# Grant permission\naz ad app permission grant \\\n  --id 61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0 \\\n  --api acca5fbb-b7e4-4009-81f1-37e38fd66d78 \\\n  --scope user_impersonation\n```\n\n### Error: AADSTS650052 - Lacks a service principal\n\nIf you see this error:\n\n> The app is trying to access a service that your organization lacks a service principal for.\n\nRun these commands to create the required service principals:\n\n```bash\naz ad sp create --id 61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0\naz ad sp create --id acca5fbb-b7e4-4009-81f1-37e38fd66d78\n```\n\nThen grant the permissions using the CLI commands above.\n\n## Why This Setup is Required\n\n### How RedisInsight Authenticates\n\nRedisInsight uses **PKCE (Proof Key for Code Exchange)** OAuth 2.0 flow to authenticate with Azure Cache for Redis. This is a secure, public client flow recommended by Microsoft for desktop and web applications because:\n\n- **No client secrets** — We don't store any secrets in the distributed application\n- **User-delegated access** — Tokens are scoped to what you can access, not a service account\n- **Short-lived tokens** — Access tokens expire and are refreshed automatically\n\n### Why Admin Consent is Needed\n\nAzure uses a multi-tenant security model. When you sign in through RedisInsight:\n\n1. RedisInsight redirects you to Microsoft's login page\n2. You authenticate with your Azure credentials\n3. Azure checks if RedisInsight is allowed to request tokens on behalf of users in your tenant\n\nBecause RedisInsight is a third-party application, an Azure AD administrator must grant consent before users in your organization can authenticate through it. This gives organizations control over which applications can access their resources.\n\n### What Permissions Does RedisInsight Get?\n\nRedisInsight uses **delegated permissions** with `user_impersonation` scope. This means:\n\n- RedisInsight can only access resources **you** already have access to\n- We cannot access anything beyond your own permissions\n- All access is auditable in your Azure AD logs\n\n| Permission                                        | Purpose                                              |\n| ------------------------------------------------- | ---------------------------------------------------- |\n| `https://redis.azure.com/user_impersonation`      | Authenticate to Azure Cache for Redis on your behalf |\n| `https://management.azure.com/user_impersonation` | Auto-discover your Azure Redis instances             |\n| `offline_access`                                  | Refresh tokens without re-prompting for login        |\n| `openid`, `profile`                               | Standard user info (name, email)                     |\n"
  },
  {
    "path": "docs/plugins/development.md",
    "content": "# Plugin development\n\nThis document describes the guides to develop your own plugin for the Redis Insight Workbench.\n\n## How it works\n\nPlugin visualization in the Workbench is rendered using Iframe to encapsulate plugin scripts and styles, described in\nthe main plugin script and the stylesheet (if it has been specified in the `package.json`),\niframe includes basic styles as well.\n\n## Plugin structure\n\nEach plugin should have a unique name with all its files [loaded](installation.md) to\na separate folder inside the default `plugins` folder.\n\n> Default plugins are located inside the application.\n\n### Files\n\n`package.json` should be located in the root folder of your plugins, all other files can be included into a subfolder.\n\n- **pluginName/package.json** _(required)_ - Manifest of the plugin\n- **pluginName/{anyName}.js** _(required)_ - Core script of the plugin\n- **pluginName/{anyName}.css** _(optional)_ - File with styles for the plugin visualizations\n- **pluginName/{anyFileOrFolder}** _(optional)_ - Specify any other file or folder inside the plugin folder\n  to use by the core module script. _For example_: pluginName/images/image.png.\n\n## `package.json` structure\n\nThis is the required manifest to use the plugin. `package.json` file should include\nthe following **required** fields:\n\n<table>\n  <tr>\n    <td><i>name</i></td>\n    <td>Plugin name. It is recommended to use the folder name as the plugin name in the package.json.</td>\n  </tr>\n  <tr>\n    <td><i>main</i></td>\n    <td>Relative path to the core script of the plugin. <i>Example: </i> \"./dist/index.js\"</td>\n  </tr>\n  <tr>\n    <td><i>visualizations</i></td>\n    <td>\n      Array of visualizations (objects) to visualize the results in the Workbench.\n      <br><br>\n      Required fields in visualizations:\n      <ul>\n        <li><strong><i>id</i></strong> - visualization id</li>\n        <li><strong><i>name</i></strong> - visualization name to display in the Workbench</li>\n        <li><strong><i>activationMethod</i></strong> - name of the exported function to call when \nthis visualization is selected in the Workbench</li>\n        <li>\n          <strong><i>matchCommands</i></strong> - array of commands to use the visualization for. Supports regex string. \n          <i>Example: </i> [\"CLIENT LIST\", \"FT.*\"]\n        </li>\n      </ul>\n    </td>\n  </tr>\n</table>\n\nYou can specify the path to a css file in the `styles` field. If specified,\nthis file will be included inside the iframe plugin.\n\nSimple example of the `package.json` file with required and optional fields:\n\n```json\n{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"description\": \"Show client list as table\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"client-list\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {},\n  \"visualizations\": [\n    {\n      \"id\": \"clients-list\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderClientsList\",\n      \"matchCommands\": [\"CLIENT LIST\"],\n      \"description\": \"Example of client list plugin\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {},\n  \"dependencies\": {}\n}\n```\n\n## Core script of the plugin\n\nThis is the required script with defined visualization methods.\nThe core script contains function and its export (functions - for multiple visualizations),\nwhich is run after the relevant visualization is selected in the Workbench.\n\nThe following function receives props of the executed commands:\n\n```typescript\ninterface Props {\n  command: string; // executed command\n  data: Result[]; // array of results (one item for Standalone)\n}\n\ninterface Result {\n  response: any; // response of the executed command\n  status: 'success' | 'fail'; // response status of the executed command\n}\n\nconst renderVisualization = (props: Props) => {\n  // Do your magic\n};\n\nexport default { renderVisualization };\n```\n\nEach plugin iframe has basic styles of Redis Insight application, including fonts and color schemes.\n\nIt is recommended to use the React & [Elastic UI library](https://elastic.github.io/eui/#/) for\nconsistency with plugin visualisations and the entire application.\n\nFind the example of the plugin here.\n\n- [Client List Plugin README](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/README.md)\n- [Client List Plugin dir](https://github.com/RedisInsight/Packages/blob/main/clients-list-example/)\n\n### Available parameters\n\nAdditional information provided to the plugin iframe is included in the `window.state`\ninside of the plugin script.\n\n```javascript\nconst { config, modules } = window.state;\nconst { baseUrl, appVersion } = config;\n\n// modules - the list of modules of the current database\n// baseUrl - url for your plugin folder - can be used to include your assets\n// appVersion - version of the Redis Insight application\n```\n\n### Plugin rendering\n\nTo render the plugin visualization, the iframe with basic html is generated which is\nthen populated with relevant scripts and styles. To render the html data, use existing\nDOM Element `#app` or create your own DOM Elements.\nRendered iframe also includes `theme_DARK` or `theme_LIGHT` className on `body` to indicate the application theme used.\n\n_Javascript Example:_\n\n```javascript\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  document.getElementById('app').innerHTML = `\n        <h3>Executed command:<h3>\n        <p>${command}</p>\n        <h4>Result of the command</h4>\n        <p>${result}</p>\n        <h4>Status of the command</h4>\n        <p>${status}</p>\n      `;\n};\n\nexport default { renderVisualization };\n```\n\n_React Example:_\n\n```javascript\nimport { render } from 'react-dom';\nimport App from './App';\n\nconst renderVisualization = (props) => {\n  const { command, data = [] } = props;\n  const [{ result, status }] = data;\n  render(\n    <App command={command} response={result} status={status} />,\n    document.getElementById('app'),\n  );\n};\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderVisualization };\n```\n\n## Plugin communication\n\nUse the [redisinsight-plugin-sdk](https://www.npmjs.com/package/redisinsight-plugin-sdk), which is a third party library,\nto communicate with the main app.\n\nFind the list and\ndescription of methods called in the\n[README.md](../../redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md).\n"
  },
  {
    "path": "docs/plugins/installation.md",
    "content": "# Plugin installation & Usage\n\nThis document describes the guides to add `plugins` for the Workbench to Redis Insight.\n\n## Installation guide\n\n**Note**: While adding new plugins for Workbench, use files only from trusted\nauthors to avoid automatic execution of malicious code.\n\n1. Download the plugin for the Workbench.\n2. Open the `plugins` folder with the following path\n   - For MacOs: `<usersHomeDir>/.redis-insight/plugins`\n   - For Windows: `C:/Users/{Username}/.redis-insight/plugins`\n   - For Linux: `<usersHomeDir>/.redis-insight/plugins`\n3. Add the folder with plugin to the `plugins` folder\n\nTo see the uploaded plugin visualizations in the command results, reload the Workbench\npage and run Redis command relevant for this visualization.\n\n## Usage\n\nThe plugin may contain different visualizations for any Redis commands.\nBelow you can find a guide to see command results in the uploaded plugin visualization:\n\n1. Open Redis Insight\n2. Open a database added\n3. Open the Workbench\n4. Run the Redis command relevant for the plugin visualization\n5. Select the plugin visualization to display results in (if this visualization has not been set by default)\n"
  },
  {
    "path": "docs/plugins/introduction.md",
    "content": "# Introduction to plugins for the Workbench\n\n## Introduction\n\nRedis can hold a range of different data types. Visualizing these in a\nformat that’s convenient to you for validation and debugging is paramount.\nYou can now easily extend the core functionality of Redis Insight independently by\nbuilding your own custom visualization plugin.\n\nData visualization provided by the plugin is rendered within the\nWorkbench results area and is based on the executed command, ie. a certain\nRedis command can generate its own custom data visualization.\n\nWe have included the following [plugin package example](https://github.com/RedisInsight/RedisInsight/tree/main/redisinsight/ui/src/packages/clients-list) for your reference: running the CLIENT LIST command presents the output in a tabular format for easier reading.\n\n## Wiki\n\n- [Installation and Usage](installation.md)\n- [Plugin Development](development.md)\n"
  },
  {
    "path": "docs/release-notes/RELEASE_NOTES_TEMPLATE.md",
    "content": "# [VERSION] ([MONTH] [YEAR])\n\n[Release description based on content - will be auto-generated]\n\n### Headlines\n\n* [Top 3-5 most important items - features or critical bugs]\n\n### Details\n\n* #[TICKET-KEY] [Feature or improvement summary]\n\n### Bugs\n\n* #[TICKET-KEY] [Bug fix summary]\n\n---\n\n**OR (when only bugs):**\n\n### Bug fixes\n\n* #[TICKET-KEY] [Bug fix summary]\n\n---\n\n**SHA-512 Checksums**\n\n[Link to checksums]\n\n**Full Changelog**: [Link to changelog]\n"
  },
  {
    "path": "electron-builder-mas.js",
    "content": "const electronBuilder = require('./electron-builder.json');\n\nconst config = {\n  ...electronBuilder,\n  appId: 'com.redis.RedisInsight',\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "electron-builder.json",
    "content": "{\n  \"productName\": \"Redis Insight\",\n  \"appId\": \"org.RedisLabs.RedisInsight-V2\",\n  \"copyright\": \"Copyright © 2026 Redis Ltd.\",\n  \"files\": [\"dist\", \"node_modules\", \"package.json\"],\n  \"artifactName\": \"Redis-Insight-${os}-${arch}.${ext}\",\n  \"compression\": \"normal\",\n  \"asarUnpack\": [\"node_modules/keytar\", \"node_modules/sqlite3\"],\n  \"protocols\": [\n    {\n      \"name\": \"RedisInsight\",\n      \"role\": \"Viewer\",\n      \"schemes\": [\"redisinsight\"]\n    }\n  ],\n  \"electronFuses\": {\n    \"runAsNode\": false\n  },\n  \"mac\": {\n    \"target\": [\n      {\n        \"target\": \"dmg\",\n        \"arch\": [\"x64\", \"arm64\"]\n      },\n      {\n        \"target\": \"zip\",\n        \"arch\": [\"x64\", \"arm64\"]\n      }\n    ],\n    \"notarize\": true,\n    \"type\": \"distribution\",\n    \"hardenedRuntime\": true,\n    \"darkModeSupport\": true,\n    \"icon\": \"resources/icons/1024x1024.png\",\n    \"artifactName\": \"Redis-Insight-${os}-${arch}.${ext}\",\n    \"entitlements\": \"resources/entitlements.mac.plist\",\n    \"entitlementsInherit\": \"resources/entitlements.mac.plist\",\n    \"gatekeeperAssess\": false\n  },\n  \"mas\": {\n    \"mergeASARs\": false,\n    \"entitlements\": \"resources/entitlements.mas.plist\",\n    \"entitlementsInherit\": \"resources/entitlements.mas.inherit.plist\",\n    \"entitlementsLoginHelper\": \"resources/entitlements.mas.loginhelper.plist\",\n    \"hardenedRuntime\": false,\n    \"asarUnpack\": [\"node_modules\"],\n    \"provisioningProfile\": \"embedded.provisionprofile\",\n    \"binaries\": [\n      \"Contents/Resources/app-x64.asar.unpacked/node_modules/sqlite3/build/Release/node_sqlite3.node\",\n      \"Contents/Resources/app-arm64.asar.unpacked/node_modules/sqlite3/build/Release/node_sqlite3.node\",\n      \"Contents/Resources/app-arm64.asar.unpacked/node_modules/keytar/build/Release/keytar.node\",\n      \"Contents/Resources/app-x64.asar.unpacked/node_modules/keytar/build/Release/keytar.node\"\n    ],\n    \"artifactName\": \"Redis-Insight-${os}-${arch}-mas.${ext}\"\n  },\n  \"masDev\": {\n    \"mergeASARs\": false,\n    \"entitlements\": \"resources/entitlements.mas.plist\",\n    \"entitlementsInherit\": \"resources/entitlements.mas.inherit.plist\",\n    \"entitlementsLoginHelper\": \"resources/entitlements.mas.loginhelper.plist\",\n    \"hardenedRuntime\": false,\n    \"asarUnpack\": [\"node_modules\"],\n    \"provisioningProfile\": \"dev.provisionprofile\",\n    \"binaries\": [\n      \"Contents/Resources/app-x64.asar.unpacked/node_modules/sqlite3/build/Release/node_sqlite3.node\",\n      \"Contents/Resources/app-arm64.asar.unpacked/node_modules/sqlite3/build/Release/node_sqlite3.node\",\n      \"Contents/Resources/app-arm64.asar.unpacked/node_modules/keytar/build/Release/keytar.node\",\n      \"Contents/Resources/app-x64.asar.unpacked/node_modules/keytar/build/Release/keytar.node\"\n    ],\n    \"artifactName\": \"Redis-Insight-${os}-${arch}-masDev.${ext}\"\n  },\n  \"dmg\": {\n    \"contents\": [\n      {\n        \"x\": 130,\n        \"y\": 220\n      },\n      {\n        \"x\": 410,\n        \"y\": 220,\n        \"type\": \"link\",\n        \"path\": \"/Applications\"\n      }\n    ]\n  },\n  \"win\": {\n    \"target\": [\"nsis\"],\n    \"artifactName\": \"Redis-Insight-${os}-installer.${ext}\",\n    \"icon\": \"resources/icon.ico\",\n    \"legalTrademarks\": \"Redis Inc., Redis Labs Inc.\"\n  },\n  \"nsis\": {\n    \"oneClick\": false,\n    \"perMachine\": false,\n    \"allowToChangeInstallationDirectory\": true\n  },\n  \"linux\": {\n    \"icon\": \"./resources/icons\",\n    \"target\": [\n      {\n        \"target\": \"AppImage\",\n        \"arch\": [\"x64\"]\n      },\n      {\n        \"target\": \"deb\",\n        \"arch\": [\"x64\"]\n      },\n      {\n        \"target\": \"rpm\",\n        \"arch\": [\"x64\"]\n      },\n      {\n        \"target\": \"snap\",\n        \"arch\": [\"x64\"]\n      }\n    ],\n    \"synopsis\": \"Redis GUI by Redis Ltd.\",\n    \"category\": \"Development\",\n    \"artifactName\": \"Redis-Insight-${os}-${arch}.${ext}\",\n    \"desktop\": {\n      \"entry\": {\n        \"Name\": \"Redis Insight\",\n        \"Type\": \"Application\",\n        \"Comment\": \"Redis GUI by Redis Ltd\"\n      }\n    }\n  },\n  \"deb\": {\n    \"afterInstall\": \"scripts/deb-after-install.sh\",\n    \"afterRemove\": \"scripts/deb-before-remove.sh\"\n  },\n  \"snap\": {\n    \"plugs\": [\"default\", \"password-manager-service\"],\n    \"confinement\": \"strict\",\n    \"stagePackages\": [\"default\"]\n  },\n  \"flatpak\": {\n    \"runtimeVersion\": \"20.08\",\n    \"modules\": [\n      {\n        \"name\": \"libsecret\",\n        \"buildsystem\": \"meson\",\n        \"config-opts\": [\n          \"-Dmanpage=false\",\n          \"-Dvapi=false\",\n          \"-Dgtk_doc=false\",\n          \"-Dintrospection=false\"\n        ],\n        \"cleanup\": [\"/bin\", \"/include\", \"/lib/pkgconfig\", \"/share/man\"],\n        \"sources\": [\n          {\n            \"type\": \"archive\",\n            \"url\": \"https://download.gnome.org/sources/libsecret/0.20/libsecret-0.20.5.tar.xz\",\n            \"sha256\": \"3fb3ce340fcd7db54d87c893e69bfc2b1f6e4d4b279065ffe66dac9f0fd12b4d\"\n          }\n        ]\n      }\n    ],\n    \"finishArgs\": [\n      \"--share=ipc\",\n      \"--share=network\",\n      \"--filesystem=home\",\n      \"--device=dri\",\n      \"--talk-name=org.freedesktop.secrets\",\n      \"--talk-name=org.freedesktop.Notifications\",\n      \"--talk-name=org.freedesktop.Flatpak\",\n      \"--socket=fallback-x11\",\n      \"--socket=wayland\",\n      \"--socket=x11\"\n    ]\n  },\n  \"directories\": {\n    \"app\": \"redisinsight\",\n    \"buildResources\": \"resources\",\n    \"output\": \"release\"\n  },\n  \"extraResources\": [\n    \"./resources/**\",\n    {\n      \"from\": \"./redisinsight/api/static\",\n      \"to\": \"static\",\n      \"filter\": [\"**/*\"]\n    },\n    {\n      \"from\": \"./redisinsight/api/defaults\",\n      \"to\": \"defaults\",\n      \"filter\": [\"**/*\"]\n    },\n    {\n      \"from\": \"./redisinsight/api/data\",\n      \"to\": \"data\",\n      \"filter\": [\"**/*\"]\n    },\n    {\n      \"from\": \"LICENSE\",\n      \"to\": \"LICENSE.redisinsight.txt\"\n    },\n    {\n      \"from\": \"./resources/app\",\n      \"to\": \"./app\",\n      \"filter\": [\"**/*\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "env.mcp.example",
    "content": "# This is Atlassian MCP configuration defined in mcp.json\n\n# Setup:\n# 1. First, copy this file as .env.mcp \n# cp env.mcp.example .env.mcp\n# \n# 2. Edit the .env.mcp and configure the values below\n# \n# For JIRA and confluence MCP, you need to create a token.\n# Go to https://id.atlassian.com/manage-profile/security/api-tokens\n# and create a classic token by pressing the first \"Create Token\" button\n# \n# 3. To verify your setup, run agent with the following command:\n# npx @augmentcode/auggie --mcp-config mcp.json \"go over all my mcp tools and make sure they work as expected\"\n# or you can ask the agent UI to do the same\n\nJIRA_API_TOKEN=<token>\nCONFLUENCE_API_TOKEN=<token>\n\nCONFLUENCE_URL=https://<team>.atlassian.net/wiki\nCONFLUENCE_USERNAME=<your redis email>@redis.com\n\nJIRA_URL=https://<team>.atlassian.net\nJIRA_USERNAME=<your redis email>@redis.com\n\n# for debugging the Atlassian MCP \n# MCP_VERBOSE=true\n# MCP_VERY_VERBOSE=true\n# MCP_LOGGING_STDOUT=true\n\nJIRA_PROJECTS_FILTER=\nCONFLUENCE_SPACES_FILTER=\n\n# NOTE do not put the values in quotes, since the tool will not remove them"
  },
  {
    "path": "jest-resolver.js",
    "content": "const url = require('url');\n\nmodule.exports = (request, options) => {\n  // Remove any query parameters in the request path\n  if (request.includes('?')) {\n    return options.defaultResolver(url.parse(request).pathname, options);\n  }\n\n  return options.defaultResolver(request, options);\n};\n"
  },
  {
    "path": "jest.config.cjs",
    "content": "require('dotenv').config({ path: './redisinsight/ui/.env.test' });\n\n/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */\nmodule.exports = {\n  testEnvironmentOptions: {\n    url: 'http://localhost/',\n    customExportConditions: [''],\n  },\n  moduleNameMapper: {\n    '\\\\.(jpg|jpeg|png|ico|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':\n      '<rootDir>/redisinsight/__mocks__/fileMock.js',\n    '\\\\.svg': '<rootDir>/redisinsight/__mocks__/svg.js',\n    '\\\\.(css|less|sass|scss)$': 'identity-obj-proxy',\n    '\\\\.scss\\\\?inline$': '<rootDir>/redisinsight/__mocks__/scssRaw.js',\n    'uiSrc/slices/store$': '<rootDir>/redisinsight/ui/src/utils/test-store.ts',\n    'uiSrc/(.*)': '<rootDir>/redisinsight/ui/src/$1',\n    'apiSrc/(.*)': '<rootDir>/redisinsight/api/src/$1',\n    '@redislabsdev/redis-ui-components': '@redis-ui/components',\n    '@redislabsdev/redis-ui-styles': '@redis-ui/styles',\n    '@redislabsdev/redis-ui-icons': '@redis-ui/icons',\n    '@redislabsdev/redis-ui-table': '@redis-ui/table',\n    'monaco-editor': '<rootDir>/redisinsight/__mocks__/monacoMock.js',\n    'monaco-yaml': '<rootDir>/redisinsight/__mocks__/monacoYamlMock.js',\n    unified: '<rootDir>/redisinsight/__mocks__/unified.js',\n    'remark-parse': '<rootDir>/redisinsight/__mocks__/remarkParse.js',\n    'remark-gfm': '<rootDir>/redisinsight/__mocks__/remarkGfm.js',\n    'remark-rehype': '<rootDir>/redisinsight/__mocks__/remarkRehype.js',\n    'rehype-stringify': '<rootDir>/redisinsight/__mocks__/rehypeStringify.js',\n    'unist-util-visit': '<rootDir>/redisinsight/__mocks__/unistUtilsVisit.js',\n    d3: '<rootDir>/node_modules/d3/dist/d3.min.js',\n    '^uuid$': require.resolve('uuid'),\n    msgpackr: require.resolve('msgpackr'),\n    'brotli-dec-wasm': '<rootDir>/redisinsight/__mocks__/brotli-dec-wasm.js',\n    'react-resizable-panels':\n      '<rootDir>/redisinsight/__mocks__/react-resizable-panels.js',\n  },\n  setupFiles: [\n    'construct-style-sheets-polyfill',\n    '<rootDir>/redisinsight/ui/src/setup-env.ts',\n  ],\n  setupFilesAfterEnv: ['<rootDir>/redisinsight/ui/src/setup-tests.ts'],\n  moduleDirectories: ['node_modules', 'redisinsight/node_modules'],\n  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],\n  testEnvironment: 'jest-fixed-jsdom',\n  transformIgnorePatterns: [\n    'node_modules/(?!(monaco-editor|react-monaco-editor|brotli-dec-wasm|until-async)/)',\n  ],\n  // TODO: add tests for plugins\n  modulePathIgnorePatterns: [\n    '<rootDir>/redisinsight/ui/src/packages',\n    '<rootDir>/redisinsight/ui/src/mocks',\n  ],\n  coverageDirectory: './report/coverage',\n  coveragePathIgnorePatterns: [\n    '/node_modules/',\n    '<rootDir>/redisinsight/api',\n    '<rootDir>/redisinsight/ui/src/packages',\n  ],\n  resolver: '<rootDir>/jest-resolver.js',\n  reporters: [\n    'default',\n    [\n      'jest-html-reporters',\n      {\n        publicPath: './report',\n        filename: 'index.html',\n      },\n    ],\n  ],\n  coverageThreshold: {\n    global: {\n      statements: 80,\n      branches: 63,\n      functions: 72,\n      lines: 80,\n    },\n  },\n  globals: {\n    riConfig: {\n      cloudAds: {\n        defaultFlag: true,\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "mcp.json",
    "content": "{\n  \"$schema\": \"https://modelcontextprotocol.io/schema/mcp.json\",\n  \"mcpServers\": {\n    \"github\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-github\"],\n      \"description\": \"GitHub integration for managing issues, pull requests, and repository operations for RedisInsight/RedisInsight\"\n    },\n    \"memory\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-memory\"],\n      \"description\": \"Persistent memory for storing context about the RedisInsight project across sessions\"\n    },\n    \"sequential-thinking\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-sequential-thinking\"],\n      \"description\": \"Enhanced reasoning capabilities for complex architectural and debugging tasks\"\n    },\n    \"context-7\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@upstash/context7-mcp\"],\n      \"description\": \"Context 7 MCP server for enhanced context management and analysis\"\n    },\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"@playwright/mcp@latest\"\n      ]\n    },\n    \"atlassian\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-i\",\n        \"--rm\",\n        \"--env-file\",\n        \".env.mcp\",\n        \"ghcr.io/sooperset/mcp-atlassian:latest\"\n      ],\n      \"description\": \"Atlassian MCP server for managing JIRA projects (RI-XXX tickets) and Confluence content\"\n    },\n    \"figma\": {\n      \"url\": \"https://mcp.figma.com/mcp\",\n      \"description\": \"Figma MCP server for accessing design files, frames, and layers. Copy a Figma frame/layer link to provide design context in prompts.\"\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"redisinsight\",\n  \"productName\": \"Redis Insight\",\n  \"description\": \"Redis Insight\",\n  \"license\": \"SSPL\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev:ui\": \"cross-env yarn --cwd redisinsight/ui dev\",\n    \"dev:ui:coverage\": \"cross-env COLLECT_COVERAGE=true yarn --cwd redisinsight/ui dev\",\n    \"dev:api\": \"cross-env yarn --cwd redisinsight/api start:dev\",\n    \"dev:electron:ui\": \"cross-env RI_APP_PORT=8080 RI_APP_TYPE=ELECTRON NODE_ENV=development yarn --cwd redisinsight/ui dev\",\n    \"dev:electron:api\": \"cross-env RI_APP_PORT=5540 RI_APP_TYPE=ELECTRON NODE_ENV=development USE_TCP_CLOUD_AUTH=true yarn --cwd redisinsight/api start:dev\",\n    \"dev:electron\": \"cross-env RI_APP_TYPE=ELECTRON RI_AUTO_BOOTSTRAP=false NODE_ENV=development USE_TCP_CLOUD_AUTH=true yarn --cwd redisinsight/desktop dev\",\n    \"dev:desktop\": \"concurrently \\\"yarn dev:electron:api\\\" \\\"yarn dev:electron:ui\\\" \\\"yarn dev:electron\\\"\",\n    \"build:ui\": \"cross-env NODE_ENV=production RI_APP_TYPE=web yarn --cwd redisinsight/ui build\",\n    \"build:renderer\": \"cross-env NODE_ENV=production RI_APP_TYPE=ELECTRON yarn --cwd redisinsight/ui build --emptyOutDir && copyfiles ./redisinsight/desktop/splash.html ./redisinsight/dist/renderer -f\",\n    \"stats:ui\": \"yarn --cwd redisinsight/ui stats\",\n    \"build\": \"cross-env NODE_ENV=development concurrently \\\"yarn build:main\\\" \\\"yarn build:renderer\\\"\",\n    \"build:stage\": \"cross-env NODE_ENV=staging TS_NODE_TRANSPILE_ONLY=true concurrently \\\"yarn build:api:stage && yarn build:main:stage\\\" \\\"yarn build:renderer\\\"\",\n    \"build:prod\": \"cross-env NODE_ENV=production concurrently \\\"yarn build:api && yarn build:main\\\" \\\"yarn build:renderer\\\"\",\n    \"build:api\": \"yarn --cwd redisinsight/api/ build:prod\",\n    \"build:api:stage\": \"yarn --cwd redisinsight/api/ build:stage\",\n    \"build:main\": \"cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.main.prod.ts\",\n    \"build:main:stage\": \"cross-env TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack.config.main.stage.ts\",\n    \"build:defaults\": \"yarn --cwd redisinsight/api build:defaults\",\n    \"build:statics\": \"yarn build:defaults && sh ./scripts/build-statics.sh\",\n    \"build:statics:win\": \"yarn build:defaults && ./scripts/build-statics.cmd\",\n    \"rebuild\": \"electron-rebuild --parallel --types prod,dev,optional --module-dir redisinsight\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx --cache --cache-strategy content\",\n    \"lint:ui\": \"eslint ./redisinsight/ui --ext .js,.jsx,.ts,.tsx --cache --cache-strategy content\",\n    \"lint:api\": \"eslint ./redisinsight/api --ext .js,.ts --cache --cache-strategy content\",\n    \"lint:desktop\": \"eslint ./redisinsight/desktop --cache --cache-strategy content\",\n    \"lint:e2e\": \"yarn --cwd tests/e2e lint\",\n    \"prettier\": \"prettier --check .\",\n    \"prettier:update\": \"prettier --write .\",\n    \"prettier:fix\": \"prettier --write\",\n    \"package\": \"yarn package:dev\",\n    \"package:prod\": \"ts-node ./scripts/prebuild.js dist && yarn build:prod && electron-builder build -p never\",\n    \"package:stage\": \"ts-node ./scripts/prebuild.js dist && yarn build:stage && electron-builder build -p never -c.mac.bundleVersion=$GITHUB_RUN_ID\",\n    \"package:mas\": \"electron-builder build -p never -m mas:universal -c.mac.bundleVersion=$GITHUB_RUN_ID -c ./electron-builder-mas.js\",\n    \"package:mas:dev\": \"electron-builder build -p never -m mas-dev:universal -c ./electron-builder-mas.js\",\n    \"package:dev\": \"yarn build && cross-env DEBUG=electron-builder electron-builder build -p never\",\n    \"package:win\": \"yarn build:prod && electron-builder build --win --x64 -p never\",\n    \"package:mac\": \"yarn build:prod && electron-builder build --mac -p never\",\n    \"package:mac:arm\": \"yarn build:prod && electron-builder build --mac --arm64 -p never\",\n    \"package:linux\": \"yarn build:prod && electron-builder build --linux -p never\",\n    \"postinstall\": \"patch-package && vite optimize -c ./redisinsight/ui/vite.config.mjs && skip-postinstall || yarn-deduplicate yarn.lock\",\n    \"test\": \"jest ./redisinsight/ui -w 1\",\n    \"test:api\": \"yarn --cwd redisinsight/api test\",\n    \"test:api:integration\": \"yarn --cwd redisinsight/api test:api\",\n    \"test:watch\": \"jest ./redisinsight/ui --watch -w 1\",\n    \"test:cov\": \"cross-env NODE_OPTIONS='' jest ./redisinsight/ui --testLocationInResults --json --outputFile=\\\"report/coverage/report.json\\\" --silent --coverage --no-cache --forceExit -w 3\",\n    \"test:cov:unit\": \"jest ./redisinsight/ui --group=-component --coverage -w 1\",\n    \"test:cov:component\": \"jest ./redisinsight/ui --group=component --coverage -w 1\",\n    \"type-check:ui\": \"tsc --project redisinsight/ui --noEmit\",\n    \"sb\": \"storybook dev -p 6006\",\n    \"build-sb\": \"storybook build\"\n  },\n  \"lint-staged\": {\n    \"*.{js,jsx,ts,tsx}\": [\n      \"eslint --cache\"\n    ]\n  },\n  \"build\": {\n    \"extends\": \"./electron-builder.json\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/RedisInsight/RedisInsight.git\"\n  },\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/RedisInsight/RedisInsight/issues\"\n  },\n  \"keywords\": [\n    \"redisinsight\",\n    \"redis\",\n    \"electron\",\n    \"react\",\n    \"nest\",\n    \"typescript\",\n    \"sass\",\n    \"webpack\",\n    \"vite\"\n  ],\n  \"homepage\": \"https://github.com/RedisInsight/RedisInsight#readme\",\n  \"resolutions\": {\n    \"**/trim\": \"0.0.3\",\n    \"word-wrap\": \"1.2.4\",\n    \"**/semver\": \"^7.5.2\",\n    \"rawproto/protobufjs\": \"^7.2.5\",\n    \"@electron/notarize\": \"2.3.2\",\n    \"webpack-bundle-analyzer/ws\": \"^7.5.10\",\n    \"msw/path-to-regexp\": \"^6.3.0\",\n    \"msw/cookie\": \"^0.7.0\",\n    \"**/cross-spawn\": \"^7.0.5\",\n    \"styled-components\": \"^5\",\n    \"@elastic/eui/**/prismjs\": \"~1.30.0\",\n    \"vite/esbuild\": \"^0.25.0\",\n    \"react-router-dom/react-router/path-to-regexp\": \"^1.9.0\",\n    \"**/form-data\": \"^4.0.4\"\n  },\n  \"devDependencies\": {\n    \"@babel/plugin-proposal-decorators\": \"^7.28.0\",\n    \"@babel/preset-env\": \"^7.23.2\",\n    \"@babel/preset-react\": \"^7.22.15\",\n    \"@babel/preset-typescript\": \"^7.23.2\",\n    \"@electron/rebuild\": \"^4.0.1\",\n    \"@faker-js/faker\": \"^8.4.1\",\n    \"@pmmmwh/react-refresh-webpack-plugin\": \"^0.5.10\",\n    \"@storybook/addon-a11y\": \"^9.1.11\",\n    \"@storybook/addon-docs\": \"^9.1.11\",\n    \"@storybook/addon-links\": \"^9.1.11\",\n    \"@storybook/addon-themes\": \"^9.1.11\",\n    \"@storybook/react-vite\": \"^9.1.11\",\n    \"@svgr/webpack\": \"^8.1.0\",\n    \"@teamsupercell/typings-for-css-modules-loader\": \"^2.4.0\",\n    \"@testing-library/jest-dom\": \"^6.2.0\",\n    \"@testing-library/react\": \"^13.3.0\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@testing-library/user-event\": \"^14.4.3\",\n    \"@types/classnames\": \"^2.2.11\",\n    \"@types/d3\": \"^7.4.0\",\n    \"@types/detect-port\": \"^1.3.0\",\n    \"@types/dompurify\": \"^3.2.0\",\n    \"@types/electron-store\": \"^3.2.0\",\n    \"@types/express\": \"^4.17.3\",\n    \"@types/file-saver\": \"^2.0.5\",\n    \"@types/html-entities\": \"^1.3.4\",\n    \"@types/ioredis\": \"^4.26.0\",\n    \"@types/is-glob\": \"^4.0.2\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/js-yaml\": \"^4.0.9\",\n    \"@types/json-bigint\": \"^1.0.1\",\n    \"@types/jsonpath\": \"^0.2.0\",\n    \"@types/lodash\": \"^4.14.171\",\n    \"@types/node\": \"14.14.10\",\n    \"@types/pako\": \"^2.0.0\",\n    \"@types/react\": \"^18.0.20\",\n    \"@types/react-dom\": \"^18.0.5\",\n    \"@types/react-redux\": \"^7.1.12\",\n    \"@types/react-router-dom\": \"^5.3.3\",\n    \"@types/react-virtualized\": \"^9.21.10\",\n    \"@types/react-window-infinite-loader\": \"^1.0.6\",\n    \"@types/redux-mock-store\": \"^1.0.2\",\n    \"@types/segment-analytics\": \"^0.0.34\",\n    \"@types/semver\": \"^7.7.0\",\n    \"@types/styled-components\": \"^5.1.34\",\n    \"@types/supertest\": \"^2.0.8\",\n    \"@types/text-encoding\": \"^0.0.37\",\n    \"@types/uuid\": \"^8.3.4\",\n    \"@types/webpack-env\": \"^1.18.4\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.18.0\",\n    \"@typescript-eslint/parser\": \"^7.18.0\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"@vitejs/plugin-react-swc\": \"^3.6.0\",\n    \"assert\": \"^2.1.0\",\n    \"babel-preset-vite\": \"^1.1.3\",\n    \"concurrently\": \"^9.0.1\",\n    \"construct-style-sheets-polyfill\": \"^3.1.0\",\n    \"copyfiles\": \"^2.4.1\",\n    \"core-js\": \"^3.6.5\",\n    \"cross-env\": \"^7.0.2\",\n    \"css-loader\": \"^5.0.1\",\n    \"css-minimizer-webpack-plugin\": \"^6.0.0\",\n    \"csv-parser\": \"^3.0.0\",\n    \"csv-stringify\": \"^6.4.0\",\n    \"dotenv\": \"^16.4.5\",\n    \"electron\": \"^39.2.1\",\n    \"electron-builder\": \"^26.0.12\",\n    \"electron-builder-notarize\": \"^1.5.2\",\n    \"electron-debug\": \"^3.2.0\",\n    \"electron-devtools-installer\": \"^3.2.0\",\n    \"esbuild-plugin-react-virtualized\": \"^1.0.4\",\n    \"eslint\": \"^8.57.1\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-airbnb-typescript\": \"^18.0.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-import-resolver-webpack\": \"^0.13.8\",\n    \"eslint-plugin-compat\": \"^6.0.1\",\n    \"eslint-plugin-import\": \"^2.31.0\",\n    \"eslint-plugin-jest\": \"^28.9.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n    \"eslint-plugin-prettier\": \"^5.5.1\",\n    \"eslint-plugin-promise\": \"^7.1.0\",\n    \"eslint-plugin-react\": \"^7.37.2\",\n    \"eslint-plugin-react-hooks\": \"^5.0.0\",\n    \"eslint-plugin-sonarjs\": \"^2.0.4\",\n    \"eslint-plugin-storybook\": \"^9.1.11\",\n    \"file-loader\": \"^6.0.0\",\n    \"fishery\": \"^2.3.1\",\n    \"google-auth-library\": \"^9.0.0\",\n    \"googleapis\": \"^125.0.0\",\n    \"html-webpack-plugin\": \"^5.6.0\",\n    \"identity-obj-proxy\": \"^3.0.0\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"jest-fixed-jsdom\": \"^0.0.10\",\n    \"jest-html-reporters\": \"^3.1.7\",\n    \"jest-runner-groups\": \"^2.2.0\",\n    \"jest-when\": \"^3.2.1\",\n    \"license-checker\": \"^25.0.1\",\n    \"lint-staged\": \"^10.2.11\",\n    \"mini-css-extract-plugin\": \"2.7.2\",\n    \"moment\": \"^2.29.3\",\n    \"msw\": \"^2.11.5\",\n    \"patch-package\": \"^8.0.1\",\n    \"postinstall-postinstall\": \"^2.1.0\",\n    \"prettier\": \"3.5.2\",\n    \"react-refresh\": \"^0.9.0\",\n    \"redux-mock-store\": \"^1.5.4\",\n    \"regenerator-runtime\": \"^0.13.5\",\n    \"rimraf\": \"^3.0.2\",\n    \"sass\": \"npm:sass-embedded\",\n    \"skip-postinstall\": \"^1.0.0\",\n    \"socket.io-mock\": \"^1.3.2\",\n    \"source-map-support\": \"^0.5.19\",\n    \"storybook\": \"^9.1.19\",\n    \"style-loader\": \"^2.0.0\",\n    \"supertest\": \"^4.0.2\",\n    \"terser-webpack-plugin\": \"^5.3.10\",\n    \"text-encoding\": \"^0.7.0\",\n    \"ts-jest\": \"^29.2.5\",\n    \"ts-loader\": \"^9.5.1\",\n    \"ts-mockito\": \"^2.6.1\",\n    \"ts-node\": \"^10.9.1\",\n    \"tsconfig-paths\": \"^3.9.0\",\n    \"tsconfig-paths-webpack-plugin\": \"^4.1.0\",\n    \"typescript\": \"^4.0.5\",\n    \"url-loader\": \"^4.1.0\",\n    \"vite\": \"^5.4.21\",\n    \"vite-bundle-visualizer\": \"1.0.1\",\n    \"vite-plugin-compression2\": \"^1.1.0\",\n    \"vite-plugin-ejs\": \"^1.7.0\",\n    \"vite-plugin-electron\": \"^0.28.6\",\n    \"vite-plugin-electron-renderer\": \"^0.14.5\",\n    \"vite-plugin-istanbul\": \"^7.1.0\",\n    \"vite-plugin-react-click-to-component\": \"^3.0.0\",\n    \"vite-plugin-svgr\": \"^4.2.0\",\n    \"webpack\": \"^5.104.1\",\n    \"webpack-bundle-analyzer\": \"^4.10.2\",\n    \"webpack-cli\": \"^5.1.4\",\n    \"webpack-merge\": \"^5.10.0\",\n    \"whatwg-fetch\": \"^3.6.2\",\n    \"yarn-deduplicate\": \"^3.1.0\"\n  },\n  \"dependencies\": {\n    \"@elastic/datemath\": \"^5.0.3\",\n    \"@elastic/eui\": \"34.6.0\",\n    \"@redis-ui/components\": \"^42.8.0\",\n    \"@redis-ui/icons\": \"^6.7.0\",\n    \"@redis-ui/styles\": \"^14.9.3\",\n    \"@redis-ui/table\": \"^3.5.1\",\n    \"@reduxjs/toolkit\": \"^1.6.2\",\n    \"@stablelib/snappy\": \"^1.0.2\",\n    \"@types/json-dup-key-validator\": \"^1.0.2\",\n    \"ajv\": \"^8.18.0\",\n    \"axios\": \"^1.13.5\",\n    \"brotli-dec-wasm\": \"^2.3.0\",\n    \"buffer\": \"^6.0.3\",\n    \"classnames\": \"^2.3.1\",\n    \"connection-string\": \"^4.3.2\",\n    \"d3\": \"^7.6.1\",\n    \"date-fns\": \"^3.6.0\",\n    \"date-fns-tz\": \"^3.1.3\",\n    \"dompurify\": \"^3.3.1\",\n    \"electron-context-menu\": \"^3.1.0\",\n    \"electron-log\": \"^4.2.4\",\n    \"electron-store\": \"^8.0.0\",\n    \"electron-updater\": \"^6.6.2\",\n    \"file-saver\": \"^2.0.5\",\n    \"formik\": \"^2.2.9\",\n    \"fzstd\": \"^0.1.0\",\n    \"get-port\": \"^7.0.0\",\n    \"html-entities\": \"^2.3.2\",\n    \"html-react-parser\": \"^1.2.4\",\n    \"java-object-serialization\": \"^0.1.2\",\n    \"js-yaml\": \"^4.1.1\",\n    \"json-bigint\": \"^1.0.0\",\n    \"json-dup-key-validator\": \"^1.0.3\",\n    \"jsonpath\": \"^1.2.1\",\n    \"jszip\": \"^3.10.1\",\n    \"lodash\": \"^4.17.23\",\n    \"lz4js\": \"^0.2.0\",\n    \"modern-normalize\": \"^3.0.1\",\n    \"monaco-editor\": \"^0.48.0\",\n    \"monaco-yaml\": \"^5.1.1\",\n    \"msgpackr\": \"^1.10.1\",\n    \"node-abi\": \"^4.24.0\",\n    \"pako\": \"^2.1.0\",\n    \"php-serialize\": \"^4.0.2\",\n    \"pickleparser\": \"^0.2.1\",\n    \"rawproto\": \"^0.7.6\",\n    \"react\": \"^18.2.0\",\n    \"react-contenteditable\": \"^3.3.5\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-focus-on\": \"^3.9.4\",\n    \"react-hotkeys-hook\": \"^3.3.1\",\n    \"react-jsx-parser\": \"^1.28.4\",\n    \"react-monaco-editor\": \"^0.55.0\",\n    \"react-redux\": \"^7.2.2\",\n    \"react-resizable-panels\": \"^3.0.2\",\n    \"react-rnd\": \"^10.3.5\",\n    \"react-router-dom\": \"^5.3.4\",\n    \"react-virtualized\": \"^9.22.2\",\n    \"react-virtualized-auto-sizer\": \"^1.0.6\",\n    \"react-vtree\": \"^3.0.0-beta.3\",\n    \"react-window\": \"^1.8.6\",\n    \"react-window-infinite-loader\": \"^1.0.8\",\n    \"rehype-stringify\": \"^9.0.2\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"remark-parse\": \"^10.0.1\",\n    \"remark-rehype\": \"^10.0.1\",\n    \"semver\": \"^7.7.2\",\n    \"socket.io-client\": \"^4.8.0\",\n    \"styled-components\": \"^5.0.0\",\n    \"unified\": \"^10.1.1\",\n    \"unist-util-visit\": \"^4.1.0\",\n    \"url-parse\": \"^1.5.10\",\n    \"uuid\": \"^8.3.2\"\n  },\n  \"engines\": {\n    \"node\": \">=22.x\",\n    \"npm\": \">=6.x\",\n    \"yarn\": \">=1.21.3\"\n  },\n  \"browser\": {\n    \"uuid\": \"./node_modules/uuid/dist/esm-browser/index.js\"\n  }\n}\n"
  },
  {
    "path": "patches/@elastic+eui+34.6.0.patch",
    "content": "diff --git a/node_modules/@elastic/eui/es/components/.DS_Store b/node_modules/@elastic/eui/es/components/.DS_Store\nnew file mode 100644\nindex 0000000..e69de29\ndiff --git a/node_modules/@elastic/eui/es/components/icon/icon.js b/node_modules/@elastic/eui/es/components/icon/icon.js\nindex a90c191..940715b 100644\n--- a/node_modules/@elastic/eui/es/components/icon/icon.js\n+++ b/node_modules/@elastic/eui/es/components/icon/icon.js\n@@ -55,6 +55,7 @@ import { keysOf } from '../common'; // @ts-ignore not generating typescript file\n // to generate & git track a TS module definition for each icon component\n \n import { icon as empty } from './assets/empty.js';\n+import { typeToModuleMap } from './icon_imports';\n import { enqueueStateChange } from '../../services/react';\n import { htmlIdGenerator } from '../../services';\n var typeToPathMap = {\n@@ -579,12 +580,13 @@ export var EuiIcon = /*#__PURE__*/function (_PureComponent) {\n         return;\n       }\n \n-      import(\n-      /* webpackChunkName: \"icon.[request]\" */\n-      // It's important that we don't use a template string here, it\n-      // stops webpack from building a dynamic require context.\n-      // eslint-disable-next-line prefer-template\n-      './assets/' + typeToPathMap[iconType] + '.js').then(function (_ref) {\n+      // import(\n+      // /* webpackChunkName: \"icon.[request]\" */\n+      // // It's important that we don't use a template string here, it\n+      // // stops webpack from building a dynamic require context.\n+      // // eslint-disable-next-line prefer-template\n+      // './assets/' + typeToPathMap[iconType] + '.js').then(function (_ref) {\n+      typeToModuleMap[iconType]().then(function (_ref) {\n         var icon = _ref.icon;\n         iconComponentCache[iconType] = icon;\n         enqueueStateChange(function () {\ndiff --git a/node_modules/@elastic/eui/es/components/icon/icon_imports.js b/node_modules/@elastic/eui/es/components/icon/icon_imports.js\nnew file mode 100644\nindex 0000000..90036bb\n--- /dev/null\n+++ b/node_modules/@elastic/eui/es/components/icon/icon_imports.js\n@@ -0,0 +1,375 @@\n+export var typeToModuleMap = {\n+  accessibility: () => import('./assets/accessibility.js'),\n+  addDataApp: () => import('./assets/app_add_data.js'),\n+  advancedSettingsApp: () => import('./assets/app_advanced_settings.js'),\n+  aggregate: () => import('./assets/aggregate.js'),\n+  alert: () => import('./assets/alert.js'),\n+  annotation: () => import('./assets/annotation.js'),\n+  apmApp: () => import('./assets/app_apm.js'),\n+  apmTrace: () => import('./assets/apm_trace.js'),\n+  appSearchApp: () => import('./assets/app_app_search.js'),\n+  apps: () => import('./assets/apps.js'),\n+  arrowDown: () => import('./assets/arrow_down.js'),\n+  arrowLeft: () => import('./assets/arrow_left.js'),\n+  arrowRight: () => import('./assets/arrow_right.js'),\n+  arrowUp: () => import('./assets/arrow_up.js'),\n+  asterisk: () => import('./assets/asterisk.js'),\n+  auditbeatApp: () => import('./assets/app_auditbeat.js'),\n+  beaker: () => import('./assets/beaker.js'),\n+  bell: () => import('./assets/bell.js'),\n+  bellSlash: () => import('./assets/bellSlash.js'),\n+  bolt: () => import('./assets/bolt.js'),\n+  boxesHorizontal: () => import('./assets/boxes_horizontal.js'),\n+  boxesVertical: () => import('./assets/boxes_vertical.js'),\n+  branch: () => import('./assets/branch.js'),\n+  broom: () => import('./assets/broom.js'),\n+  brush: () => import('./assets/brush.js'),\n+  bug: () => import('./assets/bug.js'),\n+  bullseye: () => import('./assets/bullseye.js'),\n+  calendar: () => import('./assets/calendar.js'),\n+  canvasApp: () => import('./assets/app_canvas.js'),\n+  check: () => import('./assets/check.js'),\n+  checkInCircleFilled: () => import('./assets/checkInCircleFilled.js'),\n+  cheer: () => import('./assets/cheer.js'),\n+  classificationJob: () => import('./assets/ml_classification_job.js'),\n+  clock: () => import('./assets/clock.js'),\n+  cloudDrizzle: () => import('./assets/cloudDrizzle.js'),\n+  cloudStormy: () => import('./assets/cloudStormy.js'),\n+  cloudSunny: () => import('./assets/cloudSunny.js'),\n+  codeApp: () => import('./assets/app_code.js'),\n+  color: () => import('./assets/color.js'),\n+  compute: () => import('./assets/compute.js'),\n+  console: () => import('./assets/console.js'),\n+  consoleApp: () => import('./assets/app_console.js'),\n+  continuityAbove: () => import('./assets/continuityAbove.js'),\n+  continuityAboveBelow: () => import('./assets/continuityAboveBelow.js'),\n+  continuityBelow: () => import('./assets/continuityBelow.js'),\n+  continuityWithin: () => import('./assets/continuityWithin.js'),\n+  controlsHorizontal: () => import('./assets/controls_horizontal.js'),\n+  controlsVertical: () => import('./assets/controls_vertical.js'),\n+  copy: () => import('./assets/copy.js'),\n+  copyClipboard: () => import('./assets/copy_clipboard.js'),\n+  createAdvancedJob: () => import('./assets/ml_create_advanced_job.js'),\n+  createMultiMetricJob: () => import('./assets/ml_create_multi_metric_job.js'),\n+  createPopulationJob: () => import('./assets/ml_create_population_job.js'),\n+  createSingleMetricJob: () => import('./assets/ml_create_single_metric_job.js'),\n+  cross: () => import('./assets/cross.js'),\n+  crossClusterReplicationApp: () => import('./assets/app_cross_cluster_replication.js'),\n+  crossInACircleFilled: () => import('./assets/crossInACircleFilled.js'),\n+  crosshairs: () => import('./assets/crosshairs.js'),\n+  currency: () => import('./assets/currency.js'),\n+  cut: () => import('./assets/cut.js'),\n+  dashboardApp: () => import('./assets/app_dashboard.js'),\n+  dataVisualizer: () => import('./assets/ml_data_visualizer.js'),\n+  database: () => import('./assets/database.js'),\n+  devToolsApp: () => import('./assets/app_devtools.js'),\n+  discoverApp: () => import('./assets/app_discover.js'),\n+  document: () => import('./assets/document.js'),\n+  documentEdit: () => import('./assets/documentEdit.js'),\n+  documentation: () => import('./assets/documentation.js'),\n+  documents: () => import('./assets/documents.js'),\n+  dot: () => import('./assets/dot.js'),\n+  download: () => import('./assets/download.js'),\n+  editorAlignCenter: () => import('./assets/editor_align_center.js'),\n+  editorAlignLeft: () => import('./assets/editor_align_left.js'),\n+  editorAlignRight: () => import('./assets/editor_align_right.js'),\n+  editorBold: () => import('./assets/editor_bold.js'),\n+  editorCodeBlock: () => import('./assets/editor_code_block.js'),\n+  editorComment: () => import('./assets/editor_comment.js'),\n+  editorDistributeHorizontal: () => import('./assets/editorDistributeHorizontal.js'),\n+  editorDistributeVertical: () => import('./assets/editorDistributeVertical.js'),\n+  editorHeading: () => import('./assets/editor_heading.js'),\n+  editorItalic: () => import('./assets/editor_italic.js'),\n+  editorItemAlignBottom: () => import('./assets/editorItemAlignBottom.js'),\n+  editorItemAlignCenter: () => import('./assets/editorItemAlignCenter.js'),\n+  editorItemAlignLeft: () => import('./assets/editorItemAlignLeft.js'),\n+  editorItemAlignMiddle: () => import('./assets/editorItemAlignMiddle.js'),\n+  editorItemAlignRight: () => import('./assets/editorItemAlignRight.js'),\n+  editorItemAlignTop: () => import('./assets/editorItemAlignTop.js'),\n+  editorLink: () => import('./assets/editor_link.js'),\n+  editorOrderedList: () => import('./assets/editor_ordered_list.js'),\n+  editorPositionBottomLeft: () => import('./assets/editorPositionBottomLeft.js'),\n+  editorPositionBottomRight: () => import('./assets/editorPositionBottomRight.js'),\n+  editorPositionTopLeft: () => import('./assets/editorPositionTopLeft.js'),\n+  editorPositionTopRight: () => import('./assets/editorPositionTopRight.js'),\n+  editorRedo: () => import('./assets/editor_redo.js'),\n+  editorStrike: () => import('./assets/editor_strike.js'),\n+  editorTable: () => import('./assets/editor_table.js'),\n+  editorUnderline: () => import('./assets/editor_underline.js'),\n+  editorUndo: () => import('./assets/editor_undo.js'),\n+  editorUnorderedList: () => import('./assets/editor_unordered_list.js'),\n+  email: () => import('./assets/email.js'),\n+  empty: () => import('./assets/empty.js'),\n+  emsApp: () => import('./assets/app_ems.js'),\n+  eql: () => import('./assets/eql.js'),\n+  eraser: () => import('./assets/eraser.js'),\n+  exit: () => import('./assets/exit.js'),\n+  expand: () => import('./assets/expand.js'),\n+  expandMini: () => import('./assets/expandMini.js'),\n+  exportAction: () => import('./assets/export.js'),\n+  eye: () => import('./assets/eye.js'),\n+  eyeClosed: () => import('./assets/eye_closed.js'),\n+  faceHappy: () => import('./assets/face_happy.js'),\n+  faceNeutral: () => import('./assets/face_neutral.js'),\n+  faceSad: () => import('./assets/face_sad.js'),\n+  filebeatApp: () => import('./assets/app_filebeat.js'),\n+  filter: () => import('./assets/filter.js'),\n+  flag: () => import('./assets/flag.js'),\n+  fold: () => import('./assets/fold.js'),\n+  folderCheck: () => import('./assets/folder_check.js'),\n+  folderClosed: () => import('./assets/folder_closed.js'),\n+  folderExclamation: () => import('./assets/folder_exclamation.js'),\n+  folderOpen: () => import('./assets/folder_open.js'),\n+  frameNext: () => import('./assets/frameNext.js'),\n+  framePrevious: () => import('./assets/framePrevious.js'),\n+  fullScreen: () => import('./assets/full_screen.js'),\n+  fullScreenExit: () => import('./assets/fullScreenExit.js'),\n+  function: () => import('./assets/function.js'),\n+  gear: () => import('./assets/gear.js'),\n+  gisApp: () => import('./assets/app_gis.js'),\n+  glasses: () => import('./assets/glasses.js'),\n+  globe: () => import('./assets/globe.js'),\n+  grab: () => import('./assets/grab.js'),\n+  grabHorizontal: () => import('./assets/grab_horizontal.js'),\n+  graphApp: () => import('./assets/app_graph.js'),\n+  grid: () => import('./assets/grid.js'),\n+  grokApp: () => import('./assets/app_grok.js'),\n+  heart: () => import('./assets/heart.js'),\n+  heartbeatApp: () => import('./assets/app_heartbeat.js'),\n+  heatmap: () => import('./assets/heatmap.js'),\n+  help: () => import('./assets/help.js'),\n+  home: () => import('./assets/home.js'),\n+  iInCircle: () => import('./assets/iInCircle.js'),\n+  image: () => import('./assets/image.js'),\n+  importAction: () => import('./assets/import.js'),\n+  indexClose: () => import('./assets/index_close.js'),\n+  indexEdit: () => import('./assets/index_edit.js'),\n+  indexFlush: () => import('./assets/index_flush.js'),\n+  indexManagementApp: () => import('./assets/app_index_management.js'),\n+  indexMapping: () => import('./assets/index_mapping.js'),\n+  indexOpen: () => import('./assets/index_open.js'),\n+  indexPatternApp: () => import('./assets/app_index_pattern.js'),\n+  indexRollupApp: () => import('./assets/app_index_rollup.js'),\n+  indexRuntime: () => import('./assets/index_runtime.js'),\n+  indexSettings: () => import('./assets/index_settings.js'),\n+  inputOutput: () => import('./assets/inputOutput.js'),\n+  inspect: () => import('./assets/inspect.js'),\n+  invert: () => import('./assets/invert.js'),\n+  ip: () => import('./assets/ip.js'),\n+  keyboardShortcut: () => import('./assets/keyboard_shortcut.js'),\n+  kqlField: () => import('./assets/kql_field.js'),\n+  kqlFunction: () => import('./assets/kql_function.js'),\n+  kqlOperand: () => import('./assets/kql_operand.js'),\n+  kqlSelector: () => import('./assets/kql_selector.js'),\n+  kqlValue: () => import('./assets/kql_value.js'),\n+  layers: () => import('./assets/layers.js'),\n+  lensApp: () => import('./assets/app_lens.js'),\n+  link: () => import('./assets/link.js'),\n+  list: () => import('./assets/list.js'),\n+  listAdd: () => import('./assets/list_add.js'),\n+  lock: () => import('./assets/lock.js'),\n+  lockOpen: () => import('./assets/lockOpen.js'),\n+  logoAWS: () => import('./assets/logo_aws.js'),\n+  logoAWSMono: () => import('./assets/logo_aws_mono.js'),\n+  logoAerospike: () => import('./assets/logo_aerospike.js'),\n+  logoApache: () => import('./assets/logo_apache.js'),\n+  logoAppSearch: () => import('./assets/logo_app_search.js'),\n+  logoAzure: () => import('./assets/logo_azure.js'),\n+  logoAzureMono: () => import('./assets/logo_azure_mono.js'),\n+  logoBeats: () => import('./assets/logo_beats.js'),\n+  logoBusinessAnalytics: () => import('./assets/logo_business_analytics.js'),\n+  logoCeph: () => import('./assets/logo_ceph.js'),\n+  logoCloud: () => import('./assets/logo_cloud.js'),\n+  logoCloudEnterprise: () => import('./assets/logo_cloud_ece.js'),\n+  logoCode: () => import('./assets/logo_code.js'),\n+  logoCodesandbox: () => import('./assets/logo_codesandbox.js'),\n+  logoCouchbase: () => import('./assets/logo_couchbase.js'),\n+  logoDocker: () => import('./assets/logo_docker.js'),\n+  logoDropwizard: () => import('./assets/logo_dropwizard.js'),\n+  logoElastic: () => import('./assets/logo_elastic.js'),\n+  logoElasticStack: () => import('./assets/logo_elastic_stack.js'),\n+  logoElasticsearch: () => import('./assets/logo_elasticsearch.js'),\n+  logoEnterpriseSearch: () => import('./assets/logo_enterprise_search.js'),\n+  logoEtcd: () => import('./assets/logo_etcd.js'),\n+  logoGCP: () => import('./assets/logo_gcp.js'),\n+  logoGCPMono: () => import('./assets/logo_gcp_mono.js'),\n+  logoGithub: () => import('./assets/logo_github.js'),\n+  logoGmail: () => import('./assets/logo_gmail.js'),\n+  logoGolang: () => import('./assets/logo_golang.js'),\n+  logoGoogleG: () => import('./assets/logo_google_g.js'),\n+  logoHAproxy: () => import('./assets/logo_haproxy.js'),\n+  logoIBM: () => import('./assets/logo_ibm.js'),\n+  logoIBMMono: () => import('./assets/logo_ibm_mono.js'),\n+  logoKafka: () => import('./assets/logo_kafka.js'),\n+  logoKibana: () => import('./assets/logo_kibana.js'),\n+  logoKubernetes: () => import('./assets/logo_kubernetes.js'),\n+  logoLogging: () => import('./assets/logo_logging.js'),\n+  logoLogstash: () => import('./assets/logo_logstash.js'),\n+  logoMaps: () => import('./assets/logo_maps.js'),\n+  logoMemcached: () => import('./assets/logo_memcached.js'),\n+  logoMetrics: () => import('./assets/logo_metrics.js'),\n+  logoMongodb: () => import('./assets/logo_mongodb.js'),\n+  logoMySQL: () => import('./assets/logo_mysql.js'),\n+  logoNginx: () => import('./assets/logo_nginx.js'),\n+  logoObservability: () => import('./assets/logo_observability.js'),\n+  logoOsquery: () => import('./assets/logo_osquery.js'),\n+  logoPhp: () => import('./assets/logo_php.js'),\n+  logoPostgres: () => import('./assets/logo_postgres.js'),\n+  logoPrometheus: () => import('./assets/logo_prometheus.js'),\n+  logoRabbitmq: () => import('./assets/logo_rabbitmq.js'),\n+  logoRedis: () => import('./assets/logo_redis.js'),\n+  logoSecurity: () => import('./assets/logo_security.js'),\n+  logoSiteSearch: () => import('./assets/logo_site_search.js'),\n+  logoSketch: () => import('./assets/logo_sketch.js'),\n+  logoSlack: () => import('./assets/logo_slack.js'),\n+  logoUptime: () => import('./assets/logo_uptime.js'),\n+  logoWebhook: () => import('./assets/logo_webhook.js'),\n+  logoWindows: () => import('./assets/logo_windows.js'),\n+  logoWorkplaceSearch: () => import('./assets/logo_workplace_search.js'),\n+  logsApp: () => import('./assets/app_logs.js'),\n+  logstashFilter: () => import('./assets/logstash_filter.js'),\n+  logstashIf: () => import('./assets/logstash_if.js'),\n+  logstashInput: () => import('./assets/logstash_input.js'),\n+  logstashOutput: () => import('./assets/logstash_output.js'),\n+  logstashQueue: () => import('./assets/logstash_queue.js'),\n+  machineLearningApp: () => import('./assets/app_ml.js'),\n+  magnet: () => import('./assets/magnet.js'),\n+  magnifyWithMinus: () => import('./assets/magnifyWithMinus.js'),\n+  magnifyWithPlus: () => import('./assets/magnifyWithPlus.js'),\n+  managementApp: () => import('./assets/app_management.js'),\n+  mapMarker: () => import('./assets/map_marker.js'),\n+  memory: () => import('./assets/memory.js'),\n+  menu: () => import('./assets/menu.js'),\n+  menuDown: () => import('./assets/menuDown.js'),\n+  menuLeft: () => import('./assets/menuLeft.js'),\n+  menuRight: () => import('./assets/menuRight.js'),\n+  menuUp: () => import('./assets/menuUp.js'),\n+  merge: () => import('./assets/merge.js'),\n+  metricbeatApp: () => import('./assets/app_metricbeat.js'),\n+  metricsApp: () => import('./assets/app_metrics.js'),\n+  minimize: () => import('./assets/minimize.js'),\n+  minus: () => import('./assets/minus.js'),\n+  minusInCircle: () => import('./assets/minus_in_circle.js'),\n+  minusInCircleFilled: () => import('./assets/minus_in_circle_filled.js'),\n+  mobile: () => import('./assets/mobile.js'),\n+  monitoringApp: () => import('./assets/app_monitoring.js'),\n+  moon: () => import('./assets/moon.js'),\n+  nested: () => import('./assets/nested.js'),\n+  node: () => import('./assets/node.js'),\n+  notebookApp: () => import('./assets/app_notebook.js'),\n+  number: () => import('./assets/number.js'),\n+  offline: () => import('./assets/offline.js'),\n+  online: () => import('./assets/online.js'),\n+  outlierDetectionJob: () => import('./assets/ml_outlier_detection_job.js'),\n+  package: () => import('./assets/package.js'),\n+  packetbeatApp: () => import('./assets/app_packetbeat.js'),\n+  pageSelect: () => import('./assets/pageSelect.js'),\n+  pagesSelect: () => import('./assets/pagesSelect.js'),\n+  paperClip: () => import('./assets/paper_clip.js'),\n+  partial: () => import('./assets/partial.js'),\n+  pause: () => import('./assets/pause.js'),\n+  pencil: () => import('./assets/pencil.js'),\n+  percent: () => import('./assets/percent.js'),\n+  pin: () => import('./assets/pin.js'),\n+  pinFilled: () => import('./assets/pin_filled.js'),\n+  pipelineApp: () => import('./assets/app_pipeline.js'),\n+  play: () => import('./assets/play.js'),\n+  playFilled: () => import('./assets/playFilled.js'),\n+  plus: () => import('./assets/plus.js'),\n+  plusInCircle: () => import('./assets/plus_in_circle.js'),\n+  plusInCircleFilled: () => import('./assets/plus_in_circle_filled.js'),\n+  popout: () => import('./assets/popout.js'),\n+  push: () => import('./assets/push.js'),\n+  questionInCircle: () => import('./assets/question_in_circle.js'),\n+  quote: () => import('./assets/quote.js'),\n+  recentlyViewedApp: () => import('./assets/app_recently_viewed.js'),\n+  refresh: () => import('./assets/refresh.js'),\n+  regressionJob: () => import('./assets/ml_regression_job.js'),\n+  reporter: () => import('./assets/reporter.js'),\n+  reportingApp: () => import('./assets/app_reporting.js'),\n+  returnKey: () => import('./assets/return_key.js'),\n+  save: () => import('./assets/save.js'),\n+  savedObjectsApp: () => import('./assets/app_saved_objects.js'),\n+  scale: () => import('./assets/scale.js'),\n+  search: () => import('./assets/search.js'),\n+  searchProfilerApp: () => import('./assets/app_search_profiler.js'),\n+  securityAnalyticsApp: () => import('./assets/app_security_analytics.js'),\n+  securityApp: () => import('./assets/app_security.js'),\n+  securitySignal: () => import('./assets/securitySignal.js'),\n+  securitySignalDetected: () => import('./assets/securitySignalDetected.js'),\n+  securitySignalResolved: () => import('./assets/securitySignalResolved.js'),\n+  shard: () => import('./assets/shard.js'),\n+  share: () => import('./assets/share.js'),\n+  snowflake: () => import('./assets/snowflake.js'),\n+  sortDown: () => import('./assets/sort_down.js'),\n+  sortLeft: () => import('./assets/sortLeft.js'),\n+  sortRight: () => import('./assets/sortRight.js'),\n+  sortUp: () => import('./assets/sort_up.js'),\n+  sortable: () => import('./assets/sortable.js'),\n+  spacesApp: () => import('./assets/app_spaces.js'),\n+  sqlApp: () => import('./assets/app_sql.js'),\n+  starEmpty: () => import('./assets/star_empty.js'),\n+  starEmptySpace: () => import('./assets/star_empty_space.js'),\n+  starFilled: () => import('./assets/star_filled.js'),\n+  starFilledSpace: () => import('./assets/star_filled_space.js'),\n+  starMinusEmpty: () => import('./assets/star_minus_empty.js'),\n+  starMinusFilled: () => import('./assets/star_minus_filled.js'),\n+  starPlusEmpty: () => import('./assets/starPlusEmpty.js'),\n+  starPlusFilled: () => import('./assets/starPlusFilled.js'),\n+  stats: () => import('./assets/stats.js'),\n+  stop: () => import('./assets/stop.js'),\n+  stopFilled: () => import('./assets/stop_filled.js'),\n+  stopSlash: () => import('./assets/stop_slash.js'),\n+  storage: () => import('./assets/storage.js'),\n+  string: () => import('./assets/string.js'),\n+  submodule: () => import('./assets/submodule.js'),\n+  swatchInput: () => import('./assets/swatch_input.js'),\n+  // Undocumented on purpose. Has an extra stroke for EuiColorPicker\n+  symlink: () => import('./assets/symlink.js'),\n+  tableDensityCompact: () => import('./assets/table_density_compact.js'),\n+  tableDensityExpanded: () => import('./assets/table_density_expanded.js'),\n+  tableDensityNormal: () => import('./assets/table_density_normal.js'),\n+  tableOfContents: () => import('./assets/tableOfContents.js'),\n+  tag: () => import('./assets/tag.js'),\n+  tear: () => import('./assets/tear.js'),\n+  temperature: () => import('./assets/temperature.js'),\n+  timeline: () => import('./assets/timeline.js'),\n+  timelionApp: () => import('./assets/app_timelion.js'),\n+  timeslider: () => import('./assets/timeslider.js'),\n+  training: () => import('./assets/training.js'),\n+  trash: () => import('./assets/trash.js'),\n+  unfold: () => import('./assets/unfold.js'),\n+  unlink: () => import('./assets/unlink.js'),\n+  upgradeAssistantApp: () => import('./assets/app_upgrade_assistant.js'),\n+  uptimeApp: () => import('./assets/app_uptime.js'),\n+  user: () => import('./assets/user.js'),\n+  users: () => import('./assets/users.js'),\n+  usersRolesApp: () => import('./assets/app_users_roles.js'),\n+  vector: () => import('./assets/vector.js'),\n+  videoPlayer: () => import('./assets/videoPlayer.js'),\n+  visArea: () => import('./assets/vis_area.js'),\n+  visAreaStacked: () => import('./assets/vis_area_stacked.js'),\n+  visBarHorizontal: () => import('./assets/vis_bar_horizontal.js'),\n+  visBarHorizontalStacked: () => import('./assets/vis_bar_horizontal_stacked.js'),\n+  visBarVertical: () => import('./assets/vis_bar_vertical.js'),\n+  visBarVerticalStacked: () => import('./assets/vis_bar_vertical_stacked.js'),\n+  visGauge: () => import('./assets/vis_gauge.js'),\n+  visGoal: () => import('./assets/vis_goal.js'),\n+  visLine: () => import('./assets/vis_line.js'),\n+  visMapCoordinate: () => import('./assets/vis_map_coordinate.js'),\n+  visMapRegion: () => import('./assets/vis_map_region.js'),\n+  visMetric: () => import('./assets/vis_metric.js'),\n+  visPie: () => import('./assets/vis_pie.js'),\n+  visTable: () => import('./assets/vis_table.js'),\n+  visTagCloud: () => import('./assets/vis_tag_cloud.js'),\n+  visText: () => import('./assets/vis_text.js'),\n+  visTimelion: () => import('./assets/vis_timelion.js'),\n+  visVega: () => import('./assets/vis_vega.js'),\n+  visVisualBuilder: () => import('./assets/vis_visual_builder.js'),\n+  visualizeApp: () => import('./assets/app_visualize.js'),\n+  watchesApp: () => import('./assets/app_watches.js'),\n+  wordWrap: () => import('./assets/wordWrap.js'),\n+  wordWrapDisabled: () => import('./assets/wordWrapDisabled.js'),\n+  workplaceSearchApp: () => import('./assets/app_workplace_search.js'),\n+  wrench: () => import('./assets/wrench.js'),\n+};\ndiff --git a/node_modules/@elastic/eui/src/global_styling/mixins/_loading.scss b/node_modules/@elastic/eui/src/global_styling/mixins/_loading.scss\nindex 0f72a84..69b3450 100644\n--- a/node_modules/@elastic/eui/src/global_styling/mixins/_loading.scss\n+++ b/node_modules/@elastic/eui/src/global_styling/mixins/_loading.scss\n@@ -1,6 +1,6 @@\n @function euiLoadingSpinnerBorderColors(\n-  $main: $euiColorLightShade,\n-  $highlight: $euiColorPrimary\n+  $main: var(--euiColorLightShade),\n+  $highlight: var(--euiColorPrimary)\n ) {\n   @return $highlight $main $main $main;\n }\ndiff --git a/node_modules/@elastic/eui/src/global_styling/reset/_reset.scss b/node_modules/@elastic/eui/src/global_styling/reset/_reset.scss\nindex e4f8104..0f96065 100644\n--- a/node_modules/@elastic/eui/src/global_styling/reset/_reset.scss\n+++ b/node_modules/@elastic/eui/src/global_styling/reset/_reset.scss\n@@ -62,7 +62,7 @@ html {\n   font-size: $euiFontSize;\n   color: $euiTextColor;\n   height: 100%;\n-  background-color: $euiPageBackgroundColor;\n+  background-color: var(--euiPageBackgroundColor);\n }\n \n body {\n@@ -85,7 +85,7 @@ body {\n \n a {\n   text-decoration: none;\n-  color: $euiColorPrimary;\n+  color: var(--euiColorPrimary);\n \n   &:hover {\n     text-decoration: none;\ndiff --git a/node_modules/@elastic/eui/src/global_styling/reset/_scrollbar.scss b/node_modules/@elastic/eui/src/global_styling/reset/_scrollbar.scss\nindex 18c12b1..b5db47e 100644\n--- a/node_modules/@elastic/eui/src/global_styling/reset/_scrollbar.scss\n+++ b/node_modules/@elastic/eui/src/global_styling/reset/_scrollbar.scss\n@@ -1,8 +1,15 @@\n // Firefox's scrollbar coloring cascades throughout which is why it's set at the html level\n // However, the width sizing is not, but this has been added to the euiScrollBar mixin as well\n \n+\n+@mixin transparent($color, $factor) {\n+  $alpha: 1 - $factor;\n+  color: rgba($color, $alpha);\n+}\n+\n+\n html {\n   // sass-lint:disable-block no-misspelled-properties\n   scrollbar-width: thin;\n-  scrollbar-color: transparentize($euiColorDarkShade, .5) transparent; // Firefox support\n+  scrollbar-color: transparent(var(--euiColorDarkShade), 0.5) transparent; // Firefox support\n }\n"
  },
  {
    "path": "patches/monaco-yaml+5.1.1.patch",
    "content": "diff --git a/node_modules/monaco-yaml/yaml.worker.js b/node_modules/monaco-yaml/yaml.worker.js\nindex c4e3806..7140210 100644\n--- a/node_modules/monaco-yaml/yaml.worker.js\n+++ b/node_modules/monaco-yaml/yaml.worker.js\n@@ -6709,11 +6709,11 @@ var YAMLHover = class {\n \\`\\`\\`${example}\\`\\`\\``;\n           });\n         }\n-        if (result.length > 0 && schema.schema.url) {\n-          result += `\n+//         if (result.length > 0 && schema.schema.url) {\n+//           result += `\n\n-Source: [${getSchemaName(schema.schema)}](${schema.schema.url})`;\n-        }\n+// Source: [${getSchemaName(schema.schema)}](${schema.schema.url})`;\n+//         }\n         return createHover(result);\n       }\n       return null;\n"
  },
  {
    "path": "patches/react-vtree+3.0.0-beta.3.patch",
    "content": "diff --git a/node_modules/react-vtree/dist/cjs/Tree.js b/node_modules/react-vtree/dist/cjs/Tree.js\nindex c46ce3e..879f0a6 100644\n--- a/node_modules/react-vtree/dist/cjs/Tree.js\n+++ b/node_modules/react-vtree/dist/cjs/Tree.js\n@@ -33,6 +33,7 @@ var Row = function Row(_ref) {\n   return /*#__PURE__*/_react.default.createElement(Node, Object.assign({\n     isScrolling: isScrolling,\n     style: style,\n+    index: index,\n     treeData: treeData\n   }, data));\n };\ndiff --git a/node_modules/react-vtree/dist/es/Tree.d.ts b/node_modules/react-vtree/dist/es/Tree.d.ts\nindex 5e7f57e..b216b36 100644\n--- a/node_modules/react-vtree/dist/es/Tree.d.ts\n+++ b/node_modules/react-vtree/dist/es/Tree.d.ts\n@@ -24,6 +24,8 @@ export declare type NodePublicState<TData extends NodeData> = Readonly<{\n     data: TData;\n     setOpen: (state: boolean) => Promise<void>;\n }> & {\n+    index: number;\n+    style: object;\n     isOpen: boolean;\n };\n export declare type NodeRecord<TNodePublicState extends NodePublicState<any>> = Readonly<{\ndiff --git a/node_modules/react-vtree/dist/es/Tree.js b/node_modules/react-vtree/dist/es/Tree.js\nindex 2b1c7c0..b22e873 100644\n--- a/node_modules/react-vtree/dist/es/Tree.js\n+++ b/node_modules/react-vtree/dist/es/Tree.js\n@@ -19,6 +19,7 @@ export var Row = function Row(_ref) {\n   return /*#__PURE__*/React.createElement(Node, Object.assign({\n     isScrolling: isScrolling,\n     style: style,\n+    index: index,\n     treeData: treeData\n   }, data));\n };\ndiff --git a/node_modules/react-vtree/dist/lib/Tree.js b/node_modules/react-vtree/dist/lib/Tree.js\nindex fb824bd..6feba4e 100644\n--- a/node_modules/react-vtree/dist/lib/Tree.js\n+++ b/node_modules/react-vtree/dist/lib/Tree.js\n@@ -17,6 +17,7 @@ export const Row = ({\n   return /*#__PURE__*/React.createElement(Node, Object.assign({\n     isScrolling: isScrolling,\n     style: style,\n+    index: index,\n     treeData: treeData\n   }, data));\n };\n"
  },
  {
    "path": "pull_request_template.md",
    "content": "# What\n<!-- Briefly explain what have you changed in the code and any tech decisions that were made. -->\n\n# Testing\n<!-- Please explain how you've ensured the change works properly - Manual and/or Automation tests as well as Screenshots and Recordings for visual changes -->"
  },
  {
    "path": "redisinsight/__mocks__/brotli-dec-wasm.js",
    "content": "export default () => Promise.resolve();\n"
  },
  {
    "path": "redisinsight/__mocks__/fileMock.js",
    "content": "export default 'test-file-stub';\n"
  },
  {
    "path": "redisinsight/__mocks__/monacoMock.js",
    "content": "import React, { useEffect } from 'react';\n\nconst editor = {\n  addCommand: jest.fn(),\n  getContribution: jest.fn(),\n  onKeyDown: jest.fn(),\n  onMouseDown: jest.fn(),\n  addAction: jest.fn(),\n  getAction: jest.fn(),\n  deltaDecorations: jest.fn(),\n  createContextKey: jest.fn(),\n  focus: jest.fn(),\n  onDidChangeCursorPosition: jest.fn(),\n  onDidFocusEditorWidget: jest.fn(),\n  onDidBlurEditorWidget: jest.fn(),\n  onDidChangeModelContent: jest.fn(),\n  onDidLayoutChange: jest.fn(),\n  getLayoutInfo: jest.fn().mockReturnValue({ contentLeft: 0 }),\n  onDidAttemptReadOnlyEdit: jest.fn(),\n  executeEdits: jest.fn(),\n  updateOptions: jest.fn(),\n  setSelection: jest.fn(),\n  setPosition: jest.fn(),\n  createDecorationsCollection: jest.fn().mockReturnValue({ set: jest.fn(), clear: jest.fn() }),\n  getValue: jest.fn().mockReturnValue(''),\n  getModel: jest.fn().mockReturnValue({\n    getOffsetAt: jest.fn().mockReturnValue(0),\n    getWordUntilPosition: jest.fn().mockReturnValue(''),\n  }),\n  getPosition: jest.fn().mockReturnValue({}),\n  trigger: jest.fn(),\n};\n\nconst monacoEditor = {\n  Range: jest\n    .fn()\n    .mockImplementation(\n      (startLineNumber, startColumn, endLineNumber, endColumn) => ({\n        startLineNumber,\n        startColumn,\n        endLineNumber,\n        endColumn,\n      }),\n    ),\n  languages: {\n    getLanguages: jest.fn(),\n    register: jest.fn(),\n    registerCompletionItemProvider: jest.fn().mockReturnValue({\n      dispose: jest.fn(),\n    }),\n    registerSignatureHelpProvider: jest.fn().mockReturnValue({\n      dispose: jest.fn(),\n    }),\n    setLanguageConfiguration: jest.fn(),\n    setMonarchTokensProvider: jest.fn(),\n    json: {\n      jsonDefaults: {\n        setDiagnosticsOptions: jest.fn(),\n      },\n    },\n  },\n  KeyMod: {},\n  KeyCode: {},\n};\n\nexport default function MonacoEditor(props) {\n  useEffect(() => {\n    props.editorDidMount && props.editorDidMount(editor, monacoEditor);\n    props.editorWillMount && props.editorWillMount(monacoEditor);\n  }, []);\n  return (\n    <textarea\n      {...props}\n      onChange={(e) => props.onChange && props.onChange(e.target.value)}\n      data-testid={props['data-testid'] ? props['data-testid'] : 'monaco'}\n    />\n  );\n}\n\nexport const languages = {\n  CompletionItemKind: {\n    Function: 1,\n  },\n  CompletionItemInsertTextRule: {\n    InsertAsSnippet: 4,\n  },\n  ...monacoEditor.languages,\n};\n\nexport const monaco = {\n  languages,\n  Selection: jest.fn().mockImplementation(() => ({})),\n  editor: {\n    ...editor,\n    colorize: jest.fn().mockImplementation((data) => Promise.resolve(data)),\n    defineTheme: jest.fn(),\n    setTheme: jest.fn(),\n  },\n  Range: monacoEditor.Range,\n};\n"
  },
  {
    "path": "redisinsight/__mocks__/monacoYamlMock.js",
    "content": "export const configureMonacoYaml = () => ({\n  update: jest.fn(),\n});\n"
  },
  {
    "path": "redisinsight/__mocks__/rawproto.js",
    "content": "export const visit = jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/react-children-utilities.js",
    "content": "export const onlyText = jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/react-resizable-panels.js",
    "content": "export const Panel = ({ children }) => children;\nexport const PanelGroup = ({ children }) => children;\nexport const PanelResizeHandle = () => 'MockPanelResizeHandle';\n\n// Mock utility functions and constants\nexport const DATA_ATTRIBUTES = {};\n\nexport const assert = jest.fn();\nexport const disableGlobalCursorStyles = jest.fn();\nexport const enableGlobalCursorStyles = jest.fn();\nexport const getIntersectingRectangle = jest.fn();\nexport const getPanelElement = jest.fn();\nexport const getPanelElementsForGroup = jest.fn();\nexport const getPanelGroupElement = jest.fn();\nexport const getResizeHandleElement = jest.fn();\nexport const getResizeHandleElementIndex = jest.fn();\nexport const getResizeHandleElementsForGroup = jest.fn();\nexport const getResizeHandlePanelIds = jest.fn();\nexport const intersects = jest.fn();\nexport const setNonce = jest.fn();\nexport const usePanelGroupContext = jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/rehypeStringify.js",
    "content": "export default jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/remarkGfm.js",
    "content": "export default jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/remarkParse.js",
    "content": "export default jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/remarkRehype.js",
    "content": ""
  },
  {
    "path": "redisinsight/__mocks__/scssRaw.js",
    "content": "const styles = '';\n\nexport default styles;\n"
  },
  {
    "path": "redisinsight/__mocks__/svg.js",
    "content": "import React from 'react';\n\n// Mock SVG component for Jest tests\nconst SvgMock = React.forwardRef((props, ref) => <svg ref={ref} {...props} />);\n\nSvgMock.displayName = 'SvgMock';\n\nexport default SvgMock;\nexport const ReactComponent = SvgMock;\n"
  },
  {
    "path": "redisinsight/__mocks__/unified.js",
    "content": "export const unified = jest.fn();\n"
  },
  {
    "path": "redisinsight/__mocks__/unistUtilsVisit.js",
    "content": "export const visit = jest.fn();\n"
  },
  {
    "path": "redisinsight/api/.dockerignore",
    "content": ".git\n.idea\n.vscode\n\n.nyc_output\ncoverage\nnode_modules\ndist\n\ntest/test-runs/results\n"
  },
  {
    "path": "redisinsight/api/.eslintignore",
    "content": "node_modules\ndist\ntest\nmigration\n"
  },
  {
    "path": "redisinsight/api/.gitignore",
    "content": "# compiled output\n/dist\n/node_modules\n/static\n/defaults\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# Dev\n*.db\n/secrets\n/ca_certificates\n/client_certificates\n"
  },
  {
    "path": "redisinsight/api/.jest.setup.ts",
    "content": "import 'reflect-metadata';\n// Workaround for @Type test coverage\njest.mock('class-transformer', () => {\n  return {\n    ...(jest.requireActual('class-transformer') as Object),\n    Type: (f: Function) =>\n      f() && jest.requireActual('class-transformer').Type(f),\n  };\n});\n"
  },
  {
    "path": "redisinsight/api/.prettierignore",
    "content": "# Tests\n/test\n"
  },
  {
    "path": "redisinsight/api/.prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "redisinsight/api/.yarnclean.prod",
    "content": "*.md\n*.ts\n*.map\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Create/Create Query (invalid).bru",
    "content": "meta {\n  name: Create Query (invalid)\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"description\": \"Missing required fields: indexName, name, query\"\n  }\n}\n\ntests {\n  test(\"should return 400\", function() {\n    expect(res.getStatus()).to.equal(400);\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Create/Create Query.bru",
    "content": "meta {\n  name: Create Query\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"indexName\": \"{{RQE_INDEX_NAME}}\",\n    \"name\": \"Vector Search - Bikes\",\n    \"query\": \"FT.SEARCH idx:bikes_vss \\\"*\\\"\"\n  }\n}\n\nscript:post-response {\n  const data = res.body;\n  bru.setEnvVar(\"QUERY_LIBRARY_ID\", data.id)\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Create/folder.bru",
    "content": "meta {\n  name: Create\n  seq: 3\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Delete/Delete Query (invalid).bru",
    "content": "meta {\n  name: Delete Query (invalid)\n  type: http\n  seq: 2\n}\n\ndelete {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/non-existent-id\n  body: none\n  auth: inherit\n}\n\ntests {\n  test(\"should return 404\", function() {\n    expect(res.getStatus()).to.equal(404);\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Delete/Delete Query.bru",
    "content": "meta {\n  name: Delete Query\n  type: http\n  seq: 1\n}\n\ndelete {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/{{QUERY_LIBRARY_ID}}\n  body: none\n  auth: inherit\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Delete/folder.bru",
    "content": "meta {\n  name: Delete\n  seq: 6\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Get/Get Query (invalid).bru",
    "content": "meta {\n  name: Get Query (invalid)\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/non-existent-id\n  body: none\n  auth: inherit\n}\n\ntests {\n  test(\"should return 404\", function() {\n    expect(res.getStatus()).to.equal(404);\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Get/Get Query.bru",
    "content": "meta {\n  name: Get Query\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/{{QUERY_LIBRARY_ID}}\n  body: none\n  auth: inherit\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Get/folder.bru",
    "content": "meta {\n  name: Get\n  seq: 4\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Get Queries/Get Queries.bru",
    "content": "meta {\n  name: Get Queries\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  indexName: {{RQE_INDEX_NAME}}\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Get Queries/Search Queries.bru",
    "content": "meta {\n  name: Search Queries\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  indexName: {{RQE_INDEX_NAME}}\n  search: bikes\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Get Queries/folder.bru",
    "content": "meta {\n  name: Get Queries\n  seq: 2\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Seed/Seed Queries.bru",
    "content": "meta {\n  name: Seed Queries\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/seed\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"items\": [\n      {\n        \"indexName\": \"{{RQE_INDEX_NAME}}\",\n        \"name\": \"Vector Search - Bikes\",\n        \"description\": \"Retrieve all bikes in the database\",\n        \"query\": \"FT.SEARCH idx:bikes_vss \\\"*\\\"\"\n      },\n      {\n        \"indexName\": \"{{RQE_INDEX_NAME}}\",\n        \"name\": \"Vector Search - Cheapest Bikes\",\n        \"description\": \"Retrieve all bikes which price is between 1200 and 1500\",\n        \"query\": \"FT.SEARCH idx:bikes_vss \\\"@price [1200 1500]\\\"\"\n      }\n    ]\n  }\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Seed/folder.bru",
    "content": "meta {\n  name: Seed\n  seq: 1\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Update/Update Query (invalid).bru",
    "content": "meta {\n  name: Update Query (invalid)\n  type: http\n  seq: 2\n}\n\npatch {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/non-existent-id\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"name\": \"Updated Name\",\n    \"description\": \"Updated description\",\n    \"query\": \"FT.SEARCH idx:bikes_vss \\\"*\\\"\"\n  }\n}\n\ntests {\n  test(\"should return 404\", function() {\n    expect(res.getStatus()).to.equal(404);\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Update/Update Query.bru",
    "content": "meta {\n  name: Update Query\n  type: http\n  seq: 1\n}\n\npatch {\n  url: {{API_URL}}/databases/{{DB_INSTANCE_ID}}/query-library/{{QUERY_LIBRARY_ID}}\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"name\": \"Vector Search - Bikes\",\n    \"description\": \"Hello world\",\n    \"query\": \"FT.SEARCH idx:bikes_vss \\\"*\\\"\"\n  }\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/Update/folder.bru",
    "content": "meta {\n  name: Update\n  seq: 5\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/Query Library/folder.bru",
    "content": "meta {\n  name: Query Library\n  seq: 1\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"RedisInsight\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "redisinsight/api/bruno/RedisInsight/environments/Local.bru",
    "content": "vars {\n  API_URL: http://localhost:5540/api\n  RQE_INDEX_NAME: idx:bikes_vss\n}\nvars:secret [\n  DB_INSTANCE_ID,\n  QUERY_LIBRARY_ID\n]\n"
  },
  {
    "path": "redisinsight/api/config/default.ts",
    "content": "import { join, posix } from 'path';\nimport * as os from 'os';\nimport { trim } from 'lodash';\nimport { version } from '../package.json';\n\nconst homedir = join(__dirname, '..');\n\nconst buildInfoFileName = 'build.json';\nconst dataZipFileName = 'data.zip';\n\nconst staticDir =\n  process.env.RI_BUILD_TYPE === 'ELECTRON' && process['resourcesPath']\n    ? join(process['resourcesPath'], 'static')\n    : join(__dirname, '..', 'static');\n\nconst defaultsDir =\n  process.env.RI_DEFAULTS_DIR ||\n  (process.env.RI_BUILD_TYPE === 'ELECTRON' && process['resourcesPath']\n    ? join(process['resourcesPath'], 'defaults')\n    : join(__dirname, '..', 'defaults'));\n\nconst proxyPath = trim(process.env.RI_PROXY_PATH, '/');\n\nconst customPluginsUri = posix.join('/', proxyPath, 'plugins');\nconst staticUri = posix.join('/', proxyPath, 'static');\nconst tutorialsUri = posix.join('/', proxyPath, 'static', 'tutorials');\nconst customTutorialsUri = posix.join(\n  '/',\n  proxyPath,\n  'static',\n  'custom-tutorials',\n);\nconst contentUri = posix.join('/', proxyPath, 'static', 'content');\nconst defaultPluginsUri = posix.join('/', proxyPath, 'static', 'plugins');\nconst pluginsAssetsUri = posix.join(\n  '/',\n  proxyPath,\n  'static',\n  'resources',\n  'plugins',\n);\n\nconst socketProxyPath = trim(process.env.RI_SOCKET_PROXY_PATH, '/');\n\nconst socketPath = posix.join('/', socketProxyPath, 'socket.io');\n\nconst dataDir =\n  process.env.RI_BUILD_TYPE === 'ELECTRON' && process['resourcesPath']\n    ? join(process['resourcesPath'], 'data')\n    : join(__dirname, '..', 'data');\n\nexport default {\n  dir_path: {\n    tmpDir: os.tmpdir(),\n    homedir,\n    prevHomedir: homedir,\n    dataDir: process.env.RI_DATA_DIR || dataDir,\n    staticDir,\n    defaultsDir,\n    logs: join(homedir, 'logs'),\n    customConfig: join(homedir, 'config.json'),\n    preSetupDatabases:\n      process.env.RI_PRE_SETUP_DATABASES_PATH ||\n      join(homedir, 'databases.json'),\n    defaultPlugins: join(staticDir, 'plugins'),\n    customPlugins: join(homedir, 'plugins'),\n    customTutorials: join(homedir, 'custom-tutorials'),\n    pluginsAssets: join(staticDir, 'resources', 'plugins'),\n    commands: join(homedir, 'commands'),\n    defaultCommandsDir: join(defaultsDir, 'commands'),\n    tutorials: process.env.RI_TUTORIALS_PATH || join(homedir, 'tutorials'),\n    defaultTutorials: join(defaultsDir, 'tutorials'),\n    content: process.env.RI_CONTENT_PATH || join(homedir, 'content'),\n    defaultContent: join(defaultsDir, 'content'),\n    caCertificates: join(homedir, 'ca_certificates'),\n    clientCertificates: join(homedir, 'client_certificates'),\n  },\n  server: {\n    version,\n    env: process.env.NODE_ENV || 'development',\n    host: process.env.RI_APP_HOST ?? '0.0.0.0',\n    port: parseInt(process.env.RI_APP_PORT, 10) || 5540,\n    externalUrl: process.env.RI_EXTERNAL_URL, // External URL for OAuth callbacks when running behind proxy or custom port\n    docPrefix: 'api/docs',\n    globalPrefix: 'api',\n    customPluginsUri,\n    staticUri,\n    tutorialsUri,\n    customTutorialsUri,\n    contentUri,\n    defaultPluginsUri,\n    pluginsAssetsUri,\n    base: process.env.RI_BASE || '/',\n    proxyPath,\n    secretStoragePassword: process.env.RI_SECRET_STORAGE_PASSWORD,\n    agreementsPath: process.env.RI_AGREEMENTS_PATH,\n    encryptionKey: process.env.RI_ENCRYPTION_KEY,\n    acceptTermsAndConditions:\n      process.env.RI_ACCEPT_TERMS_AND_CONDITIONS === 'true',\n    tlsCert: process.env.RI_SERVER_TLS_CERT,\n    tlsKey: process.env.RI_SERVER_TLS_KEY,\n    staticContent: !!process.env.RI_SERVE_STATICS || true,\n    migrateOldFolders: process.env.RI_MIGRATE_OLD_FOLDERS\n      ? process.env.RI_MIGRATE_OLD_FOLDERS === 'true'\n      : true,\n    autoBootstrap: process.env.RI_AUTO_BOOTSTRAP\n      ? process.env.RI_AUTO_BOOTSTRAP === 'true'\n      : true,\n    buildType: process.env.RI_BUILD_TYPE || 'DOCKER_ON_PREMISE',\n    appType: process.env.RI_APP_TYPE,\n    appVersion: process.env.RI_APP_VERSION || '3.2.0',\n    requestTimeout: parseInt(process.env.RI_REQUEST_TIMEOUT, 10) || 25000,\n    excludeRoutes: [],\n    excludeAuthRoutes: [],\n    databaseManagement: process.env.RI_DATABASE_MANAGEMENT !== 'false',\n    maxPayloadSize: process.env.RI_MAX_PAYLOAD_SIZE || '512MB',\n    cors: {\n      origin: process.env.RI_CORS_ORIGIN ? process.env.RI_CORS_ORIGIN : '*',\n      credentials: process.env.RI_CORS_CREDENTIALS === 'true',\n    },\n  },\n  statics: {\n    initDefaults: process.env.RI_STATICS_INIT_DEFAULTS\n      ? process.env.RI_STATICS_INIT_DEFAULTS === 'true'\n      : true,\n    autoUpdate: process.env.RI_STATICS_AUTO_UPDATE\n      ? process.env.RI_STATICS_AUTO_UPDATE === 'true'\n      : true,\n  },\n  encryption: {\n    keytar: process.env.RI_ENCRYPTION_KEYTAR\n      ? process.env.RI_ENCRYPTION_KEYTAR === 'true'\n      : true, // enabled by default\n    // !!! DO NOT CHANGE THIS VARIABLE FOR REDIS INSIGHT!!! MUST BE \"redisinsight\"!!! It's only for vscode extension\n    keytarService: process.env.RI_ENCRYPTION_KEYTAR_SERVICE || 'redisinsight',\n    encryptionIV: process.env.RI_ENCRYPTION_IV || Buffer.alloc(16, 0),\n    encryptionAlgorithm: process.env.RI_ENCRYPTION_ALGORYTHM || 'aes-256-cbc',\n  },\n  sockets: {\n    serveClient: process.env.RI_SOCKETS_SERVE_CLIENT\n      ? process.env.RI_SOCKETS_SERVE_CLIENT === 'true'\n      : false,\n    path: socketPath,\n    namespacePrefix: process.env.RI_SOCKETS_NAMESPACE_PREFIX ?? '',\n    cors: {\n      enabled: process.env.RI_SOCKETS_CORS === 'true',\n      origin: process.env.RI_SOCKETS_CORS_ORIGIN\n        ? process.env.RI_SOCKETS_CORS_ORIGIN\n        : '*',\n      credentials:\n        process.env.RI_SOCKETS_CORS_CREDENTIALS === 'true' ? true : false,\n    },\n  },\n  db: {\n    database: join(homedir, 'redisinsight.db'),\n    synchronize: process.env.RI_DB_SYNC\n      ? process.env.RI_DB_SYNC === 'true'\n      : false,\n    migrationsRun: process.env.RI_DB_MIGRATIONS\n      ? process.env.RI_DB_MIGRATIONS === 'true'\n      : true,\n  },\n  redis_clients: {\n    forceStrategy: process.env.RI_REDIS_CLIENTS_FORCE_STRATEGY,\n    idleThreshold:\n      parseInt(process.env.RI_REDIS_CLIENTS_IDLE_THRESHOLD, 10) ||\n      1000 * 60 * 60, // 1h\n    syncInterval:\n      parseInt(process.env.RI_REDIS_CLIENTS_SYNC_INTERVAL, 10) || 1000 * 60, // 1m\n    idleSyncInterval:\n      parseInt(process.env.RI_CLIENTS_IDLE_SYNC_INTERVAL, 10) || 1000 * 60 * 60, // 1hr\n    maxIdleThreshold:\n      parseInt(process.env.RI_CLIENTS_MAX_IDLE_THRESHOLD, 10) || 1000 * 60 * 60, // 1hr\n    retryTimes: parseInt(process.env.RI_CLIENTS_RETRY_TIMES, 10) || 3,\n    retryDelay: parseInt(process.env.RI_CLIENTS_RETRY_DELAY, 10) || 500,\n    maxRetriesPerRequest:\n      parseInt(process.env.RI_CLIENTS_MAX_RETRIES_PER_REQUEST, 10) || 1,\n    maxRedirections: parseInt(process.env.RI_CLIENTS_MAX_REDIRECTIONS, 10) || 3,\n    slotsRefreshTimeout:\n      parseInt(process.env.RI_CLIENTS_SLOTS_REQUEST_TIMEOUT, 10) || 5000,\n    maxStringSize: parseInt(process.env.RI_CLIENTS_MAX_STRING_SIZE, 10),\n    truncatedStringPrefix:\n      process.env.RI_CLIENTS_TRUNCATED_STRING_PREFIX ||\n      '[Truncated due to length]',\n  },\n  redis_scan: {\n    countDefault: parseInt(process.env.RI_SCAN_COUNT_DEFAULT, 10) || 200,\n    scanThreshold: parseInt(process.env.RI_SCAN_THRESHOLD, 10) || 10000,\n    scanThresholdMax:\n      parseInt(process.env.RI_SCAN_THRESHOLD_MAX, 10) || Number.MAX_VALUE,\n  },\n  modules: {\n    json: {\n      sizeThreshold: parseInt(process.env.RI_JSON_SIZE_THRESHOLD, 10) || 1024,\n      lengthThreshold: parseInt(process.env.RI_JSON_LENGTH_THRESHOLD, 10) || -1,\n    },\n  },\n  redis_cli: {\n    unsupportedCommands: JSON.parse(\n      process.env.RI_CLI_UNSUPPORTED_COMMANDS || '[]',\n    ),\n  },\n  profiler: {\n    logFileIdleThreshold:\n      parseInt(process.env.RI_PROFILER_LOG_FILE_IDLE_THRESHOLD, 10) ||\n      1000 * 60, // 1min\n  },\n  analytics: {\n    writeKey: process.env.RI_SEGMENT_WRITE_KEY || 'SOURCE_WRITE_KEY',\n    flushInterval:\n      parseInt(process.env.RI_ANALYTICS_FLUSH_INTERVAL, 10) || 3000,\n    startEvents: process.env.RI_ANALYTICS_START_EVENTS\n      ? process.env.RI_ANALYTICS_START_EVENTS === 'true'\n      : false,\n  },\n  logger: {\n    logLevel: process.env.RI_LOG_LEVEL || 'info', // log level\n    stdout: process.env.RI_STDOUT_LOGGER\n      ? process.env.RI_STDOUT_LOGGER === 'true'\n      : false, // disabled by default\n    files: process.env.RI_FILES_LOGGER\n      ? process.env.RI_FILES_LOGGER === 'true'\n      : true, // enabled by default\n    omitSensitiveData: process.env.RI_LOGGER_OMIT_DATA\n      ? process.env.RI_LOGGER_OMIT_DATA === 'true'\n      : true,\n    pipelineSummaryLimit:\n      parseInt(process.env.RI_LOGGER_PIPELINE_SUMMARY_LIMIT, 10) || 5,\n    logDepthLevel: parseInt(process.env.RI_LOGGER_DEPTH_LEVEL, 10) || 5,\n  },\n  plugins: {\n    stateMaxSize:\n      parseInt(process.env.RI_PLUGIN_STATE_MAX_SIZE, 10) || 1024 * 1024,\n  },\n  tutorials: {\n    updateUrl:\n      process.env.RI_TUTORIALS_UPDATE_URL ||\n      'https://github.com/RedisInsight/Tutorials/releases/download/2.42',\n    zip: process.env.RI_TUTORIALS_ZIP || dataZipFileName,\n    buildInfo: process.env.RI_TUTORIALS_INFO || buildInfoFileName,\n    devMode: !!process.env.RI_TUTORIALS_PATH,\n  },\n  content: {\n    updateUrl:\n      process.env.RI_CONTENT_UPDATE_URL ||\n      'https://github.com/RedisInsight/Statics/releases/download/2.54',\n    zip: process.env.RI_CONTENT_ZIP || dataZipFileName,\n    buildInfo: process.env.RI_CONTENT_INFO || buildInfoFileName,\n    devMode: !!process.env.RI_CONTENT_PATH,\n  },\n  notifications: {\n    updateUrl:\n      process.env.RI_NOTIFICATION_DEV_PATH ||\n      process.env.RI_NOTIFICATION_UPDATE_URL ||\n      'https://github.com/RedisInsight/Notifications/releases/download/latest/notifications.json',\n    syncInterval:\n      parseInt(process.env.RI_NOTIFICATION_SYNC_INTERVAL, 10) || 60 * 60 * 1000,\n    queryLimit: parseInt(process.env.RI_NOTIFICATION_QUERY_LIMIT, 10) || 20,\n    devMode: !!process.env.RI_NOTIFICATION_DEV_PATH,\n  },\n  workbench: {\n    maxResultSize:\n      parseInt(process.env.RI_COMMAND_EXECUTION_MAX_RESULT_SIZE, 10) ||\n      1024 * 1024,\n    maxItemsPerDb:\n      parseInt(process.env.RI_COMMAND_EXECUTION_MAX_ITEMS_PER_DB, 10) || 30,\n    unsupportedCommands: JSON.parse(\n      process.env.RI_WORKBENCH_UNSUPPORTED_COMMANDS || '[]',\n    ),\n    countBatch: parseInt(process.env.RI_WORKBENCH_BATCH_SIZE, 10) || 5,\n  },\n  database_analysis: {\n    maxItemsPerDb:\n      parseInt(process.env.RI_DATABASE_ANALYSIS_MAX_ITEMS_PER_DB, 10) || 5,\n  },\n  browser_history: {\n    maxItemsPerModeInDb:\n      parseInt(process.env.RI_BROWSER_HISTORY_MAX_ITEMS_PER_MODE_IN_DB, 10) ||\n      10,\n  },\n  commands: [\n    {\n      name: 'main',\n      url:\n        process.env.RI_COMMANDS_MAIN_URL ||\n        'https://raw.githubusercontent.com/redis/redis-doc/master/commands.json',\n    },\n    {\n      name: 'redisearch',\n      url:\n        process.env.RI_COMMANDS_REDISEARCH_URL ||\n        'https://raw.githubusercontent.com/RediSearch/RediSearch/master/commands.json',\n    },\n    {\n      name: 'redisjson',\n      url:\n        process.env.RI_COMMANDS_REDIJSON_URL ||\n        'https://raw.githubusercontent.com/RedisJSON/RedisJSON/master/commands.json',\n    },\n    {\n      name: 'redistimeseries',\n      url:\n        process.env.RI_COMMANDS_REDISTIMESERIES_URL ||\n        'https://raw.githubusercontent.com/RedisTimeSeries/RedisTimeSeries/master/commands.json',\n    },\n    {\n      name: 'redisgraph',\n      url:\n        process.env.RI_COMMANDS_REDISGRAPH_URL ||\n        'https://raw.githubusercontent.com/RedisGraph/RedisGraph/master/commands.json',\n    },\n    {\n      name: 'redisgears',\n      url:\n        process.env.RI_COMMANDS_REDISGEARS_URL ||\n        'https://raw.githubusercontent.com/RedisGears/RedisGears/v1.2.5/commands.json',\n    },\n    {\n      name: 'redisbloom',\n      url:\n        process.env.RI_COMMANDS_REDISBLOOM_URL ||\n        'https://raw.githubusercontent.com/RedisBloom/RedisBloom/master/commands.json',\n    },\n  ],\n  connections: {\n    timeout:\n      parseInt(process.env.RI_CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000, // 30 sec\n  },\n  redisStack: {\n    id:\n      process.env.RI_BUILD_TYPE === 'REDIS_STACK'\n        ? process.env.RI_REDIS_STACK_DATABASE_ID || 'redis-stack'\n        : undefined,\n    name: process.env.RI_REDIS_STACK_DATABASE_NAME,\n    host: process.env.RI_REDIS_STACK_DATABASE_HOST,\n    port: process.env.RI_REDIS_STACK_DATABASE_PORT,\n  },\n  features_config: {\n    url:\n      process.env.RI_FEATURES_CONFIG_URL ||\n      // eslint-disable-next-line max-len\n      'https://raw.githubusercontent.com/RedisInsight/RedisInsight/main/redisinsight/api/config/features-config.json',\n    syncInterval:\n      parseInt(process.env.RI_FEATURES_CONFIG_SYNC_INTERVAL, 10) ||\n      1_000 * 60 * 60 * 24, // 24h\n  },\n  cloud: {\n    apiUrl:\n      process.env.RI_CLOUD_API_URL ||\n      'https://app-sm.k8s-cloudapi.sm-qa.qa.redislabs.com/api/v1',\n    apiToken: process.env.RI_CLOUD_API_TOKEN || 'token',\n    capiUrl:\n      process.env.RI_CLOUD_CAPI_URL ||\n      'https://api-k8s-cloudapi.qa.redislabs.com/v1',\n    capiKeyName: process.env.RI_CLOUD_CAPI_KEY_NAME || 'RedisInsight',\n    freeSubscriptionName:\n      process.env.RI_CLOUD_FREE_SUBSCRIPTION_NAME || 'My free subscription',\n    freeDatabaseName: process.env.RI_CLOUD_FREE_DATABASE_NAME || 'Redis-Cloud',\n    defaultPlanRegion: process.env.RI_CLOUD_DEFAULT_PLAN_REGION || 'eu-west-1',\n    jobIterationInterval:\n      parseInt(process.env.RI_CLOUD_JOB_ITERATION_INTERVAL, 10) || 10_000, // 10 sec\n    discoveryTimeout:\n      parseInt(process.env.RI_CLOUD_DISCOVERY_TIMEOUT, 10) || 60 * 1000, // 1 min\n    databaseConnectionTimeout:\n      parseInt(process.env.RI_CLOUD_DATABASE_CONNECTION_TIMEOUT, 10) ||\n      30 * 1000,\n    renewTokensBeforeExpire:\n      parseInt(process.env.RI_CLOUD_DATABASE_CONNECTION_TIMEOUT, 10) ||\n      2 * 60_000, // 2min\n    idp: {\n      google: {\n        authorizeUrl:\n          process.env.RI_CLOUD_IDP_GOOGLE_AUTHORIZE_URL ||\n          process.env.RI_CLOUD_IDP_AUTHORIZE_URL,\n        tokenUrl:\n          process.env.RI_CLOUD_IDP_GOOGLE_TOKEN_URL ||\n          process.env.RI_CLOUD_IDP_TOKEN_URL,\n        revokeTokenUrl:\n          process.env.RI_CLOUD_IDP_GOOGLE_REVOKE_TOKEN_URL ||\n          process.env.RI_CLOUD_IDP_REVOKE_TOKEN_URL,\n        issuer:\n          process.env.RI_CLOUD_IDP_GOOGLE_ISSUER ||\n          process.env.RI_CLOUD_IDP_ISSUER,\n        clientId:\n          process.env.RI_CLOUD_IDP_GOOGLE_CLIENT_ID ||\n          process.env.RI_CLOUD_IDP_CLIENT_ID,\n        redirectUri:\n          process.env.RI_CLOUD_IDP_GOOGLE_REDIRECT_URI ||\n          process.env.RI_CLOUD_IDP_REDIRECT_URI,\n        idp: process.env.RI_CLOUD_IDP_GOOGLE_ID,\n      },\n      github: {\n        authorizeUrl:\n          process.env.RI_CLOUD_IDP_GH_AUTHORIZE_URL ||\n          process.env.RI_CLOUD_IDP_AUTHORIZE_URL,\n        tokenUrl:\n          process.env.RI_CLOUD_IDP_GH_TOKEN_URL ||\n          process.env.RI_CLOUD_IDP_TOKEN_URL,\n        revokeTokenUrl:\n          process.env.RI_CLOUD_IDP_GH_REVOKE_TOKEN_URL ||\n          process.env.RI_CLOUD_IDP_REVOKE_TOKEN_URL,\n        issuer:\n          process.env.RI_CLOUD_IDP_GH_ISSUER || process.env.RI_CLOUD_IDP_ISSUER,\n        clientId:\n          process.env.RI_CLOUD_IDP_GH_CLIENT_ID ||\n          process.env.RI_CLOUD_IDP_CLIENT_ID,\n        redirectUri:\n          process.env.RI_CLOUD_IDP_GH_REDIRECT_URI ||\n          process.env.RI_CLOUD_IDP_REDIRECT_URI,\n        idp: process.env.RI_CLOUD_IDP_GH_ID,\n      },\n      sso: {\n        authorizeUrl:\n          process.env.RI_CLOUD_IDP_SSO_AUTHORIZE_URL ||\n          process.env.RI_CLOUD_IDP_AUTHORIZE_URL,\n        tokenUrl:\n          process.env.RI_CLOUD_IDP_SSO_TOKEN_URL ||\n          process.env.RI_CLOUD_IDP_TOKEN_URL,\n        revokeTokenUrl:\n          process.env.RI_CLOUD_IDP_SSO_REVOKE_TOKEN_URL ||\n          process.env.RI_CLOUD_IDP_REVOKE_TOKEN_URL,\n        issuer:\n          process.env.RI_CLOUD_IDP_SSO_ISSUER ||\n          process.env.RI_CLOUD_IDP_ISSUER,\n        clientId:\n          process.env.RI_CLOUD_IDP_SSO_CLIENT_ID ||\n          process.env.RI_CLOUD_IDP_CLIENT_ID,\n        redirectUri:\n          process.env.RI_CLOUD_IDP_SSO_REDIRECT_URI ||\n          process.env.RI_CLOUD_IDP_REDIRECT_URI,\n        emailVerificationUri:\n          process.env.RI_CLOUD_IDP_SSO_EMAIL_VERIFICATION_URI ||\n          'saml/okta_idp_id',\n        idp: process.env.RI_CLOUD_IDP_SSO_ID,\n      },\n    },\n  },\n  ai: {\n    convAiApiUrl:\n      process.env.RI_AI_CONVAI_API_URL ||\n      'https://staging.learn.redis.com/convai/api',\n    convAiToken: process.env.RI_AI_CONVAI_TOKEN,\n    querySocketUrl:\n      process.env.RI_AI_QUERY_SOCKET_URL ||\n      'https://app-sm.k8s-cloudapi.sm-qa.qa.redislabs.com',\n    querySocketPath:\n      process.env.RI_AI_QUERY_SOCKET_PATH ||\n      '/api/v1/cloud-copilot-service/socket.io/',\n    queryHistoryLimit:\n      parseInt(process.env.RI_AI_QUERY_HISTORY_LIMIT, 10) || 20,\n    queryMaxResults: parseInt(process.env.RI_AI_QUERY_MAX_RESULTS, 10) || 50,\n    queryMaxNestedElements:\n      parseInt(process.env.RI_AI_QUERY_MAX_NESTED_ELEMENTS, 10) || 25,\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/config/development.ts",
    "content": "export default {\n  server: {\n    env: 'development',\n  },\n  sockets: {\n    cors: {\n      enabled: true,\n    },\n  },\n  db: {\n    synchronize: process.env.RI_DB_SYNC\n      ? process.env.RI_DB_SYNC === 'true'\n      : true,\n    migrationsRun: process.env.RI_DB_MIGRATIONS\n      ? process.env.RI_DB_MIGRATIONS === 'true'\n      : false,\n  },\n  logger: {\n    logLevel: process.env.RI_LOG_LEVEL || 'debug',\n    stdout: process.env.RI_STDOUT_LOGGER\n      ? process.env.RI_STDOUT_LOGGER === 'true'\n      : true, // enabled by default\n    omitSensitiveData: process.env.RI_LOGGER_OMIT_DATA\n      ? process.env.RI_LOGGER_OMIT_DATA === 'true'\n      : false,\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/config/features-config.json",
    "content": "{\n  \"version\": 3.5,\n  \"features\": {\n    \"redisDataIntegration\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"REDIS_STACK\",\n          \"cond\": \"neq\"\n        }\n      ]\n    },\n    \"insightsRecommendations\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]]\n    },\n    \"hashFieldExpiration\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]]\n    },\n    \"documentationChat\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ]\n    },\n    \"databaseChat\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ]\n    },\n    \"cloudSso\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ],\n      \"data\": {\n        \"filterFreePlan\": [\n          {\n            \"field\": \"name\",\n            \"expression\": \"^(No HA?.)|(Cache?.)|(30MB$)\",\n            \"options\": \"i\"\n          }\n        ],\n        \"selectPlan\": {\n          \"components\": {\n            \"redisStackPreview\": [\n              {\n                \"provider\": \"AWS\",\n                \"regions\": []\n              },\n              {\n                \"provider\": \"GCP\",\n                \"regions\": []\n              }\n            ]\n          }\n        }\n      }\n    },\n    \"cloudSsoRecommendedSettings\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ]\n    },\n    \"redisModuleFilter\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"data\": {\n        \"hideByName\": [\n          {\n            \"expression\": \"^RedisGraph.\",\n            \"options\": \"i\"\n          },\n          {\n            \"expression\": \"^RedisStackCompat?.\",\n            \"options\": \"i\"\n          },\n          {\n            \"expression\": \"^rediscompat?.\",\n            \"options\": \"i\"\n          }\n        ]\n      }\n    },\n    \"redisClient\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]],\n      \"data\": {\n        \"strategy\": \"ioredis\"\n      }\n    },\n    \"enhancedCloudUI\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]]\n    },\n    \"vectorSearchV2\": {\n      \"flag\": true,\n      \"perc\": [[0, 10]]\n    },\n    \"databasesListV2\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]]\n    },\n    \"azureEntraId\": {\n      \"flag\": true,\n      \"perc\": [[0, 100]]\n    },\n    \"dev-azureEntraId\": {\n      \"flag\": false,\n      \"perc\": [[0, 100]]\n    },\n    \"dev-browser\": {\n      \"flag\": false,\n      \"perc\": [[0, 100]]\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/config/logger.ts",
    "content": "import { transports, format } from 'winston';\nimport 'winston-daily-rotate-file';\nimport {\n  utilities as nestWinstonModuleUtilities,\n  WinstonModuleOptions,\n} from 'nest-winston';\nimport { join } from 'path';\nimport config from 'src/utils/config';\nimport { prepareLogsData, prettyFileFormat } from 'src/utils/logsFormatter';\nimport * as fs from 'fs';\n\nconst PATH_CONFIG = config.get('dir_path');\nconst LOGGER_CONFIG = config.get('logger');\n\nconst transportsConfig = [];\n\nif (LOGGER_CONFIG.stdout) {\n  transportsConfig.push(\n    new transports.Console({\n      format: format.combine(\n        prepareLogsData({\n          omitSensitiveData: LOGGER_CONFIG.omitSensitiveData,\n        }),\n        format.timestamp(),\n        nestWinstonModuleUtilities.format.nestLike('Redis Insight', {\n          colors: true,\n          prettyPrint: true,\n          processId: true,\n          appName: true,\n        }),\n      ),\n    }),\n  );\n}\n\nif (LOGGER_CONFIG.files) {\n  try {\n    const logsDir = join(PATH_CONFIG.logs);\n    fs.mkdirSync(logsDir, { recursive: true });\n\n    transportsConfig.push(\n      new transports.DailyRotateFile({\n        dirname: logsDir,\n        datePattern: 'YYYY-MM-DD',\n        maxSize: '20m',\n        maxFiles: '7d',\n        filename: 'redisinsight-errors-%DATE%.log',\n        level: 'error',\n        format: format.combine(\n          prepareLogsData({\n            omitSensitiveData: LOGGER_CONFIG.omitSensitiveData,\n          }),\n          prettyFileFormat,\n        ),\n      }),\n    );\n    transportsConfig.push(\n      new transports.DailyRotateFile({\n        dirname: logsDir,\n        datePattern: 'YYYY-MM-DD',\n        maxSize: '20m',\n        maxFiles: '7d',\n        filename: 'redisinsight-%DATE%.log',\n        format: format.combine(\n          prepareLogsData({\n            omitSensitiveData: LOGGER_CONFIG.omitSensitiveData,\n          }),\n          prettyFileFormat,\n        ),\n      }),\n    );\n  } catch (error) {\n    // Log to console but don't crash the app if file logging setup fails\n    console.warn(\n      'Failed to initialize file logging',\n      error instanceof Error ? error.message : error,\n    );\n  }\n}\n\nconst logger: WinstonModuleOptions = {\n  format: format.errors({ stack: true }),\n  transports: transportsConfig,\n  level: LOGGER_CONFIG.logLevel,\n};\n\nexport default logger;\n"
  },
  {
    "path": "redisinsight/api/config/ormconfig.ts",
    "content": "import { TypeOrmModuleOptions } from '@nestjs/typeorm';\nimport { ServerEntity } from 'src/modules/server/entities/server.entity';\nimport { CommandExecutionEntity } from 'src/modules/workbench/entities/command-execution.entity';\nimport { PluginStateEntity } from 'src/modules/workbench/entities/plugin-state.entity';\nimport { NotificationEntity } from 'src/modules/notification/entities/notification.entity';\nimport { DatabaseAnalysisEntity } from 'src/modules/database-analysis/entities/database-analysis.entity';\nimport { DatabaseRecommendationEntity } from 'src/modules/database-recommendation/entities/database-recommendation.entity';\nimport { DataSource } from 'typeorm';\nimport { AgreementsEntity } from 'src/modules/settings/entities/agreements.entity';\nimport { SettingsEntity } from 'src/modules/settings/entities/settings.entity';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport { BrowserHistoryEntity } from 'src/modules/browser/browser-history/entities/browser-history.entity';\nimport { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';\nimport { FeatureEntity } from 'src/modules/feature/entities/feature.entity';\nimport { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';\nimport { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/cloud-database-details.entity';\nimport { CloudCapiKeyEntity } from 'src/modules/cloud/capi-key/entity/cloud-capi-key.entity';\nimport { RdiEntity } from 'src/modules/rdi/entities/rdi.entity';\nimport { AiQueryMessageEntity } from 'src/modules/ai/query/entities/ai-query.message.entity';\nimport { CloudSessionEntity } from 'src/modules/cloud/session/entities/cloud.session.entity';\nimport { DatabaseSettingsEntity } from 'src/modules/database-settings/entities/database-setting.entity';\nimport migrations from '../migration';\nimport * as config from '../src/utils/config';\nimport { TagEntity } from 'src/modules/tag/entities/tag.entity';\nimport { QueryLibraryEntity } from 'src/modules/query-library/entities/query-library.entity';\n\nconst dbConfig = config.get('db');\n\nconst ormConfig = {\n  type: 'sqlite',\n  database: dbConfig.database,\n  synchronize: dbConfig.synchronize,\n  migrationsRun: dbConfig.migrationsRun,\n  entities: [\n    AgreementsEntity,\n    CaCertificateEntity,\n    ClientCertificateEntity,\n    DatabaseEntity,\n    ServerEntity,\n    SettingsEntity,\n    CommandExecutionEntity,\n    PluginStateEntity,\n    NotificationEntity,\n    DatabaseAnalysisEntity,\n    DatabaseRecommendationEntity,\n    BrowserHistoryEntity,\n    SshOptionsEntity,\n    CustomTutorialEntity,\n    FeatureEntity,\n    FeaturesConfigEntity,\n    CloudDatabaseDetailsEntity,\n    CloudCapiKeyEntity,\n    RdiEntity,\n    AiQueryMessageEntity,\n    CloudSessionEntity,\n    DatabaseSettingsEntity,\n    TagEntity,\n    QueryLibraryEntity,\n  ],\n  migrations,\n};\n\nexport const ormModuleOptions: TypeOrmModuleOptions =\n  ormConfig as TypeOrmModuleOptions;\nexport default new DataSource({ ...ormConfig, type: 'sqlite' });\n"
  },
  {
    "path": "redisinsight/api/config/production.ts",
    "content": "import { join } from 'path';\nimport * as os from 'os';\n\nconst homedir =\n  process.env.RI_APP_FOLDER_ABSOLUTE_PATH ||\n  join(os.homedir(), process.env.RI_APP_FOLDER_NAME || '.redis-insight');\n\nconst prevHomedir = join(os.homedir(), '.redisinsight-app');\n\nexport default {\n  dir_path: {\n    homedir,\n    prevHomedir,\n    logs: join(homedir, 'logs'),\n    customConfig: join(homedir, 'config.json'),\n    preSetupDatabases:\n      process.env.RI_PRE_SETUP_DATABASES_PATH ||\n      join(homedir, 'databases.json'),\n    customPlugins: join(homedir, 'plugins'),\n    customTutorials: join(homedir, 'custom-tutorials'),\n    commands: join(homedir, 'commands'),\n    tutorials: process.env.RI_TUTORIALS_PATH || join(homedir, 'tutorials'),\n    content: process.env.RI_CONTENT_PATH || join(homedir, 'content'),\n    caCertificates: join(homedir, 'ca_certificates'),\n    clientCertificates: join(homedir, 'client_certificates'),\n    oldFolders: [\n      join(os.homedir(), '.redisinsight-preview'),\n      join(os.homedir(), '.redisinsight-v2'),\n      process.env.RI_GUIDES_PATH || join(homedir, 'guides'),\n    ],\n  },\n  server: {\n    env: 'production',\n  },\n  analytics: {\n    writeKey:\n      process.env.RI_SEGMENT_WRITE_KEY || 'lK5MNZgHbxj6vQwFgqZxygA0BiDQb32n',\n    flushInterval:\n      parseInt(process.env.RI_ANALYTICS_FLUSH_INTERVAL, 10) || 10000,\n  },\n  db: {\n    database: join(homedir, 'redisinsight.db'),\n  },\n  cloud: {\n    cApiUrl: process.env.RI_CLOUD_CAPI_URL || 'https://api.redislabs.com/v1',\n  },\n  ai: {\n    convAiApiUrl:\n      process.env.RI_AI_CONVAI_API_URL || 'https://redis.io/convai/api',\n    querySocketUrl:\n      process.env.RI_AI_QUERY_SOCKET_URL || 'https://app.redislabs.com',\n    querySocketPath:\n      process.env.RI_AI_QUERY_SOCKET_PATH ||\n      '/api/v1/cloud-copilot-service/socket.io/',\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/config/stack.ts",
    "content": "import { RequestMethod } from '@nestjs/common';\n\nexport default {\n  server: {\n    excludeRoutes: [\n      'redis-enterprise/*',\n      'redis-sentinel/*',\n      { path: 'databases/import' },\n      { path: 'databases', method: RequestMethod.POST },\n      { path: 'databases', method: RequestMethod.DELETE },\n      { path: 'databases/:id', method: RequestMethod.DELETE },\n    ],\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/config/staging.ts",
    "content": "import { join } from 'path';\nimport * as os from 'os';\n\nconst homedir =\n  process.env.RI_APP_FOLDER_ABSOLUTE_PATH ||\n  join(os.homedir(), process.env.RI_APP_FOLDER_NAME || '.redis-insight-stage');\n\nconst prevHomedir = join(os.homedir(), '.redisinsight-app-stage');\n\nexport default {\n  dir_path: {\n    homedir,\n    prevHomedir,\n    logs: join(homedir, 'logs'),\n    customConfig: join(homedir, 'config.json'),\n    preSetupDatabases:\n      process.env.RI_PRE_SETUP_DATABASES_PATH ||\n      join(homedir, 'databases.json'),\n    customPlugins: join(homedir, 'plugins'),\n    customTutorials: join(homedir, 'custom-tutorials'),\n    commands: join(homedir, 'commands'),\n    tutorials: process.env.RI_TUTORIALS_PATH || join(homedir, 'tutorials'),\n    content: process.env.RI_CONTENT_PATH || join(homedir, 'content'),\n    caCertificates: join(homedir, 'ca_certificates'),\n    clientCertificates: join(homedir, 'client_certificates'),\n    oldFolders: [\n      process.env.RI_GUIDES_PATH || join(homedir, 'guides'),\n      join(os.homedir(), '.redisinsight-preview-stage'),\n      join(os.homedir(), '.redisinsight-v2-stage'),\n    ],\n  },\n  server: {\n    env: 'staging',\n  },\n  analytics: {\n    writeKey:\n      process.env.RI_SEGMENT_WRITE_KEY || 'Ba1YuGnxzsQN9zjqTSvzPc6f3AvmH1mj',\n  },\n  db: {\n    database: join(homedir, 'redisinsight.db'),\n  },\n  logger: {\n    stdout: process.env.RI_STDOUT_LOGGER\n      ? process.env.RI_STDOUT_LOGGER === 'true'\n      : true, // enabled by default\n    omitSensitiveData: process.env.RI_LOGGER_OMIT_DATA\n      ? process.env.RI_LOGGER_OMIT_DATA === 'true'\n      : false,\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/config/swagger.ts",
    "content": "import { OpenAPIObject } from '@nestjs/swagger';\n\nconst SWAGGER_CONFIG: Omit<OpenAPIObject, 'paths'> = {\n  openapi: '3.0.0',\n  info: {\n    title: 'Redis Insight Backend API',\n    description: 'Redis Insight Backend API',\n    version: '3.2.0',\n  },\n  tags: [],\n};\n\nexport default SWAGGER_CONFIG;\n"
  },
  {
    "path": "redisinsight/api/config/test.ts",
    "content": "export default {\n  dir_path: {\n    dataDir: process.env.RI_DATA_DIR || '.test_run/data',\n  },\n  server: {\n    env: 'test',\n    requestTimeout: parseInt(process.env.RI_REQUEST_TIMEOUT, 10) || 1000,\n  },\n  db: {\n    synchronize: process.env.RI_DB_SYNC\n      ? process.env.RI_DB_SYNC === 'true'\n      : true,\n    migrationsRun: process.env.RI_DB_MIGRATIONS\n      ? process.env.RI_DB_MIGRATIONS === 'true'\n      : false,\n  },\n  profiler: {\n    logFileIdleThreshold:\n      parseInt(process.env.RI_PROFILER_LOG_FILE_IDLE_THRESHOLD, 10) || 1000 * 2, // 3sec\n  },\n  notifications: {\n    updateUrl:\n      process.env.RI_NOTIFICATION_UPDATE_URL ||\n      'https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json',\n  },\n  features_config: {\n    url:\n      process.env.RI_FEATURES_CONFIG_URL ||\n      'http://localhost:5551/remote/features-config.json',\n  },\n  analytics: {\n    startEvents: process.env.RI_ANALYTICS_START_EVENTS\n      ? process.env.RI_ANALYTICS_START_EVENTS === 'true'\n      : true,\n  },\n  cloud: {\n    apiUrl:\n      process.env.RI_CLOUD_API_URL ||\n      'https://app-sm.k8s-cloudapi.sm-qa.qa.redislabs.com/api/v1',\n    apiToken: process.env.RI_CLOUD_API_TOKEN || 'token',\n    capiUrl:\n      process.env.RI_CLOUD_CAPI_URL ||\n      'https://api-k8s-cloudapi.qa.redislabs.com/v1',\n    capiKeyName: process.env.RI_CLOUD_CAPI_KEY_NAME || 'RedisInsight',\n    freeSubscriptionName:\n      process.env.RI_CLOUD_FREE_SUBSCRIPTION_NAME || 'My free subscription',\n    freeDatabaseName: process.env.RI_CLOUD_FREE_DATABASE_NAME || 'Redis-Cloud',\n    defaultPlanRegion: process.env.RI_CLOUD_DEFAULT_PLAN_REGION || 'eu-west-1',\n    jobIterationInterval:\n      parseInt(process.env.RI_CLOUD_JOB_ITERATION_INTERVAL, 10) || 10_000, // 10 sec\n    discoveryTimeout:\n      parseInt(process.env.RI_CLOUD_DISCOVERY_TIMEOUT, 10) || 60 * 1000, // 1 min\n    databaseConnectionTimeout:\n      parseInt(process.env.RI_CLOUD_DATABASE_CONNECTION_TIMEOUT, 10) ||\n      30 * 1000,\n    renewTokensBeforeExpire:\n      parseInt(process.env.RI_CLOUD_DATABASE_CONNECTION_TIMEOUT, 10) ||\n      2 * 60_000, // 2min\n    idp: {\n      google: {\n        authorizeUrl: 'oauth2/authorize',\n        tokenUrl: 'oauth2/token',\n        revokeTokenUrl: 'oauth2/revoke',\n        issuer: 'https://authorization.server.com',\n        clientId: 'cid_p6vA6A5tF36Jf6twH2cBOqtt7n',\n        redirectUri: 'redisinsight:/cloud/oauth/callback',\n        idp: 'test-google-idp',\n      },\n      github: {\n        authorizeUrl: 'oauth2/authorize',\n        tokenUrl: 'oauth2/token',\n        revokeTokenUrl: 'oauth2/revoke',\n        issuer: 'https://authorization.server.com',\n        clientId: 'cid_p6vA6A5tF36Jf6twH2cBOqtt7n',\n        redirectUri: 'redisinsight:/cloud/oauth/callback',\n        idp: 'test-github-idp',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/data/common",
    "content": "LPUSH sample_jobQueue:waitingList \"sample_jobQueue:ticket:101\"\nLPUSH sample_jobQueue:waitingList \"sample_jobQueue:ticket:102\"\nLPUSH sample_jobQueue:waitingList \"sample_jobQueue:ticket:103\"\n\nHSET sample_jobQueue:ticket:101 id 101 user_id 123 description \"Unable to login\" priority \"High\" created_at \"2024-04-09T10:00:00Z\"\nHSET sample_jobQueue:ticket:102 id 102 user_id 456 description \"Network connectivity issue\" priority \"Medium\" created_at \"2024-04-09T10:05:00Z\"\nHSET sample_jobQueue:ticket:103 id 103 user_id 789 description \"Software application crashing\" priority \"Low\" created_at \"2024-04-09T10:10:00Z\"\n\nZADD sample_leaderboard:tetris 105000 \"user1\"\nZADD sample_leaderboard:tetris 145000 \"user2\"\nZADD sample_leaderboard:tetris 280000 \"user3\"\nZADD sample_leaderboard:tetris 325000 \"user4\"\nZADD sample_leaderboard:tetris 480000 \"user5\"\nZADD sample_leaderboard:tetris 510000 \"user6\"\nZADD sample_leaderboard:tetris 560000 \"user7\"\nZADD sample_leaderboard:tetris 640000 \"user8\"\nZADD sample_leaderboard:tetris 200000 \"user9\"\nZADD sample_leaderboard:tetris 180000 \"user10\"\nZADD sample_leaderboard:tetris 220000 \"user11\"\nZADD sample_leaderboard:tetris 420000 \"user12\"\nZADD sample_leaderboard:tetris 490000 \"user13\"\nZADD sample_leaderboard:tetris 570000 \"user14\"\nZADD sample_leaderboard:tetris 690000 \"user15\"\nZADD sample_leaderboard:tetris 125000 \"user16\"\nZADD sample_leaderboard:tetris 150000 \"user17\"\nZADD sample_leaderboard:tetris 300000 \"user18\"\nZADD sample_leaderboard:tetris 360000 \"user19\"\nZADD sample_leaderboard:tetris 540000 \"user20\"\nZADD sample_leaderboard:tetris 570000 \"user21\"\nZADD sample_leaderboard:tetris 630000 \"user22\"\nZADD sample_leaderboard:tetris 660000 \"user23\"\nZADD sample_leaderboard:tetris 230000 \"user24\"\nZADD sample_leaderboard:tetris 275000 \"user25\"\nZADD sample_leaderboard:tetris 350000 \"user26\"\nZADD sample_leaderboard:tetris 420000 \"user27\"\nZADD sample_leaderboard:tetris 520000 \"user28\"\nZADD sample_leaderboard:tetris 620000 \"user29\"\nZADD sample_leaderboard:tetris 670000 \"user30\"\nZADD sample_leaderboard:tetris 145000 \"user31\"\nZADD sample_leaderboard:tetris 265000 \"user32\"\nZADD sample_leaderboard:tetris 315000 \"user33\"\nZADD sample_leaderboard:tetris 490000 \"user34\"\nZADD sample_leaderboard:tetris 540000 \"user35\"\nZADD sample_leaderboard:tetris 580000 \"user36\"\nZADD sample_leaderboard:tetris 680000 \"user37\"\nZADD sample_leaderboard:tetris 175000 \"user38\"\nZADD sample_leaderboard:tetris 225000 \"user39\"\nZADD sample_leaderboard:tetris 365000 \"user40\"\n\nHSET sample_session:123456789 user_id 123 username john_doe email john@example.com last_activity \"2023-01-01 08:30:00\"\nHSET sample_session:234567890 user_id 234 username jane_smith email jane@example.com last_activity \"2023-01-02 09:45:00\"\nHSET sample_session:345678901 user_id 345 username alice_green email alice@example.com last_activity \"2023-01-03 11:00:00\"\nHSET sample_session:456789012 user_id 456 username bob_jones email bob@example.com last_activity \"2023-01-04 12:15:00\"\nHSET sample_session:567890123 user_id 567 username emily_brown email emily@example.com last_activity \"2023-01-05 13:30:00\"\nHSET sample_session:678901234 user_id 678 username chris_black email chris@example.com last_activity \"2023-01-06 14:45:00\"\nHSET sample_session:789012345 user_id 789 username sophia_taylor email sophia@example.com last_activity \"2023-01-07 16:00:00\"\nHSET sample_session:890123456 user_id 890 username david_wilson email david@example.com last_activity \"2023-01-08 17:15:00\"\nHSET sample_session:901234567 user_id 901 username olivia_lee email olivia@example.com last_activity \"2023-01-09 18:30:00\"\nHSET sample_session:012345678 user_id 012 username noah_hall email noah@example.com last_activity \"2023-01-10 19:45:00\"\nHSET sample_session:112233445 user_id 112 username mia_evans email mia@example.com last_activity \"2023-01-11 21:00:00\"\nHSET sample_session:334455667 user_id 334 username ethan_white email ethan@example.com last_activity \"2023-01-12 22:15:00\"\nHSET sample_session:556677889 user_id 556 username ava_martin email ava@example.com last_activity \"2023-01-13 23:30:00\"\nHSET sample_session:778899001 user_id 778 username logan_anderson email logan@example.com last_activity \"2023-01-14 00:45:00\"\nHSET sample_session:990011223 user_id 990 username mia_thompson email mia@example.com last_activity \"2023-01-15 02:00:00\"\n\nEXPIRE sample_session:123456789 1296000\nEXPIRE sample_session:234567890 1728000\nEXPIRE sample_session:345678901 2016000\nEXPIRE sample_session:456789012 2592000\nEXPIRE sample_session:567890123 3024000\nEXPIRE sample_session:678901234 3456000\nEXPIRE sample_session:789012345 3888000\nEXPIRE sample_session:890123456 4320000\nEXPIRE sample_session:901234567 4752000\nEXPIRE sample_session:012345678 5184000\nEXPIRE sample_session:112233445 5616000\nEXPIRE sample_session:334455667 6048000\nEXPIRE sample_session:556677889 6480000\nEXPIRE sample_session:778899001 6912000\nEXPIRE sample_session:990011223 7344000\n"
  },
  {
    "path": "redisinsight/api/data/json",
    "content": "JSON.SET sample_restaurant:1 $ '{\"name\":\"Taj Mahal\",\"cuisine\":\"Indian\",\"location\":\"-97.9708,29.7965\"}'\nJSON.SET sample_restaurant:2 $ '{\"name\":\"Bombay Bistro\",\"cuisine\":\"Indian\",\"location\":\"-98.0464,29.8397\"}'\nJSON.SET sample_restaurant:3 $ '{\"name\":\"Homestead Kitchen\",\"cuisine\":\"American\",\"location\":\"-98.1571,30.3848\"}'\nJSON.SET sample_restaurant:4 $ '{\"name\":\"Panda Cuisine\",\"cuisine\":\"Chinese\",\"location\":\"-98.6412,29.7961\"}'\nJSON.SET sample_restaurant:5 $ '{\"name\":\"Le Gourmet\",\"cuisine\":\"French\",\"location\":\"-97.9873,29.9595\"}'\nJSON.SET sample_restaurant:6 $ '{\"name\":\"Lotus Leaf\",\"cuisine\":\"Vietnamese\",\"location\":\"-98.6737,29.5687\"}'\nJSON.SET sample_restaurant:7 $ '{\"name\":\"Bella Pasta\",\"cuisine\":\"Italian\",\"location\":\"-97.5833,30.2416\"}'\nJSON.SET sample_restaurant:8 $ '{\"name\":\"El Sombrero\",\"cuisine\":\"Mexican\",\"location\":\"-97.6939,30.2369\"}'\nJSON.SET sample_restaurant:9 $ '{\"name\":\"Café de Paris\",\"cuisine\":\"French\",\"location\":\"-97.4593,30.1863\"}'\nJSON.SET sample_restaurant:10 $ '{\"name\":\"Dragon Noodle\",\"cuisine\":\"Vietnamese\",\"location\":\"-97.5922,29.8815\"}'\nJSON.SET sample_restaurant:11 $ '{\"name\":\"Tequila Sunrise\",\"cuisine\":\"Mexican\",\"location\":\"-98.6257,29.9175\"}'\nJSON.SET sample_restaurant:12 $ '{\"name\":\"Bistro du Vin\",\"cuisine\":\"French\",\"location\":\"-98.1075,30.3992\"}'\nJSON.SET sample_restaurant:13 $ '{\"name\":\"Aegean Seafood\",\"cuisine\":\"Mediterranean\",\"location\":\"-97.8293,29.8842\"}'\nJSON.SET sample_restaurant:14 $ '{\"name\":\"Hanoi House\",\"cuisine\":\"Vietnamese\",\"location\":\"-97.9685,30.3923\"}'\nJSON.SET sample_restaurant:15 $ '{\"name\":\"Tokyo Teahouse\",\"cuisine\":\"Japanese\",\"location\":\"-98.4374,30.2573\"}'\nJSON.SET sample_restaurant:16 $ '{\"name\":\"Tuk Tuk Bistro\",\"cuisine\":\"Thai\",\"location\":\"-98.0627,30.5064\"}'\nJSON.SET sample_restaurant:17 $ '{\"name\":\"Eagle Diner\",\"cuisine\":\"American\",\"location\":\"-97.5474,29.9227\"}'\nJSON.SET sample_restaurant:18 $ '{\"name\":\"Croissant Corner\",\"cuisine\":\"French\",\"location\":\"-97.9102,30.8092\"}'\nJSON.SET sample_restaurant:19 $ '{\"name\":\"Aztec Delights\",\"cuisine\":\"Mexican\",\"location\":\"-98.7265,30.0291\"}'\nJSON.SET sample_restaurant:20 $ '{\"name\":\"Bangkok Kitchen\",\"cuisine\":\"Thai\",\"location\":\"-98.7827,29.9863\"}'\nJSON.SET sample_restaurant:21 $ '{\"name\":\"Liberty Grill\",\"cuisine\":\"American\",\"location\":\"-98.4715,30.5709\"}'\nJSON.SET sample_restaurant:22 $ '{\"name\":\"Saffron Lounge\",\"cuisine\":\"Indian\",\"location\":\"-98.535,30.5511\"}'\nJSON.SET sample_restaurant:23 $ '{\"name\":\"Fiesta Mexicana\",\"cuisine\":\"Mexican\",\"location\":\"-98.7286,29.7251\"}'\nJSON.SET sample_restaurant:24 $ '{\"name\":\"Red Lantern\",\"cuisine\":\"Chinese\",\"location\":\"-97.4122,30.7179\"}'\nJSON.SET sample_restaurant:25 $ '{\"name\":\"Golden Lotus\",\"cuisine\":\"Chinese\",\"location\":\"-98.2867,30.5357\"}'\nJSON.SET sample_restaurant:26 $ '{\"name\":\"Chiang Mai\",\"cuisine\":\"Thai\",\"location\":\"-98.7776,30.521\"}'\nJSON.SET sample_restaurant:27 $ '{\"name\":\"Olive Branch\",\"cuisine\":\"Mediterranean\",\"location\":\"-98.0886,30.1038\"}'\nJSON.SET sample_restaurant:28 $ '{\"name\":\"Lotus Thai\",\"cuisine\":\"Thai\",\"location\":\"-98.3112,29.6098\"}'\nJSON.SET sample_restaurant:29 $ '{\"name\":\"Nippon Bites\",\"cuisine\":\"Japanese\",\"location\":\"-98.1846,30.2255\"}'\nJSON.SET sample_restaurant:30 $ '{\"name\":\"Pho Heaven\",\"cuisine\":\"Vietnamese\",\"location\":\"-97.4021,29.8848\"}'\nJSON.SET sample_restaurant:31 $ '{\"name\":\"Sakura Sushi\",\"cuisine\":\"Japanese\",\"location\":\"-98.6179,29.5208\"}'\nJSON.SET sample_restaurant:32 $ '{\"name\":\"Curry House\",\"cuisine\":\"Indian\",\"location\":\"-97.4554,30.1933\"}'\nJSON.SET sample_restaurant:33 $ '{\"name\":\"Dragon Palace\",\"cuisine\":\"Chinese\",\"location\":\"-97.5383,29.6509\"}'\nJSON.SET sample_restaurant:34 $ '{\"name\":\"Trattoria Roma\",\"cuisine\":\"Italian\",\"location\":\"-97.8434,29.6066\"}'\nJSON.SET sample_restaurant:35 $ '{\"name\":\"Patriot Plates\",\"cuisine\":\"American\",\"location\":\"-98.093,30.8803\"}'\nJSON.SET sample_restaurant:36 $ '{\"name\":\"Santorini Sunset\",\"cuisine\":\"Mediterranean\",\"location\":\"-98.2009,30.791\"}'\nJSON.SET sample_restaurant:37 $ '{\"name\":\"Siamese Dream\",\"cuisine\":\"Thai\",\"location\":\"-97.5495,29.6526\"}'\nJSON.SET sample_restaurant:38 $ '{\"name\":\"Il Forno\",\"cuisine\":\"Italian\",\"location\":\"-97.9709,29.8792\"}'\nJSON.SET sample_restaurant:39 $ '{\"name\":\"Pizzeria Napoli\",\"cuisine\":\"Italian\",\"location\":\"-98.3333,30.8893\"}'\nJSON.SET sample_restaurant:40 $ '{\"name\":\"Maison de la Mer\",\"cuisine\":\"French\",\"location\":\"-98.6603,30.2335\"}'\nJSON.SET sample_restaurant:41 $ '{\"name\":\"Star Spangled Bistro\",\"cuisine\":\"American\",\"location\":\"-97.7703,30.8489\"}'\nJSON.SET sample_restaurant:42 $ '{\"name\":\"Saigon Street\",\"cuisine\":\"Vietnamese\",\"location\":\"-98.4409,30.0093\"}'\nJSON.SET sample_restaurant:43 $ '{\"name\":\"Trojan Feast\",\"cuisine\":\"Mediterranean\",\"location\":\"-98.5921,29.7265\"}'\nJSON.SET sample_restaurant:44 $ '{\"name\":\"Szechuan Garden\",\"cuisine\":\"Chinese\",\"location\":\"-97.7426,30.6694\"}'\nJSON.SET sample_restaurant:45 $ '{\"name\":\"Spice Route\",\"cuisine\":\"Indian\",\"location\":\"-97.6912,29.9916\"}'\nJSON.SET sample_restaurant:46 $ '{\"name\":\"Zen Garden\",\"cuisine\":\"Japanese\",\"location\":\"-98.1179,30.671\"}'\nJSON.SET sample_restaurant:47 $ '{\"name\":\"Mykonos Grill\",\"cuisine\":\"Mediterranean\",\"location\":\"-98.1526,30.1543\"}'\nJSON.SET sample_restaurant:48 $ '{\"name\":\"Mount Fuji\",\"cuisine\":\"Japanese\",\"location\":\"-97.6881,30.1887\"}'\nJSON.SET sample_restaurant:49 $ '{\"name\":\"Casa de Taco\",\"cuisine\":\"Mexican\",\"location\":\"-98.3958,30.7947\"}'\nJSON.SET sample_restaurant:50 $ '{\"name\":\"La Dolce Vita\",\"cuisine\":\"Italian\",\"location\":\"-98.6036,30.2975\"}'\n\nJSON.SET sample_bicycle:1001 $ '{\"model\":\"Racer\",\"brand\":\"Speedster\",\"price\":320,\"type\":\"Road\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":8},\"description\":\"The Racer is built for speed and performance on the road!\",\"addons\":[\"water bottle holder\",\"bike lock\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1002 $ '{\"model\":\"Explorer\",\"brand\":\"Trailblazer\",\"price\":450,\"type\":\"Mountain\",\"specs\":{\"material\":\"steel\",\"weight\":15},\"description\":\"The Explorer is your ultimate companion for off-road adventures!\",\"addons\":[\"mudguards\",\"bike lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1003 $ '{\"model\":\"Cruiser\",\"brand\":\"Beachcomber\",\"price\":280,\"type\":\"Leisure\",\"specs\":{\"material\":\"aluminium\",\"weight\":12},\"description\":\"The Cruiser offers comfort and style for relaxed rides along the beach!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":false, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1004 $ '{\"model\":\"Sprinter\",\"brand\":\"Swift\",\"price\":380,\"type\":\"Electric\",\"specs\":{\"material\":\"aluminium\",\"weight\":18},\"description\":\"The Sprinter gives you an extra boost for effortless commuting!\",\"addons\":[\"phone holder\",\"kickstand\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1005 $ '{\"model\":\"Tracker\",\"brand\":\"Pathfinder\",\"price\":410,\"type\":\"Hybrid\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":11},\"description\":\"The Tracker combines versatility and performance for urban and off-road trails!\",\"addons\":[\"rear rack\",\"fenders\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1006 $ '{\"model\":\"Adventurer\",\"brand\":\"Explorer\",\"price\":500,\"type\":\"Gravel\",\"specs\":{\"material\":\"titanium\",\"weight\":14},\"description\":\"The Adventurer is designed to conquer any terrain with ease!\",\"addons\":[\"bike pump\",\"repair kit\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1007 $ '{\"model\":\"Commuter\",\"brand\":\"Urbanite\",\"price\":350,\"type\":\"City\",\"specs\":{\"material\":\"aluminium\",\"weight\":13},\"description\":\"The Commuter offers practicality and style for everyday city commuting!\",\"addons\":[\"front light\",\"rear reflector\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1008 $ '{\"model\":\"Stunt\",\"brand\":\"Xtreme\",\"price\":300,\"type\":\"BMX\",\"specs\":{\"material\":\"chromoly steel\",\"weight\":11},\"description\":\"The Stunt is built to handle the toughest tricks and jumps at the skatepark!\",\"addons\":[\"peg set\",\"gyro\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1009 $ '{\"model\":\"Trailblazer\",\"brand\":\"Rugged\",\"price\":470,\"type\":\"Fat\",\"specs\":{\"material\":\"aluminium\",\"weight\":20},\"description\":\"The Trailblazer conquers sand, snow, and rough terrain with ease!\",\"addons\":[\"bike pump\",\"saddle bag\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1010 $ '{\"model\":\"Speedster\",\"brand\":\"Velocity\",\"price\":330,\"type\":\"Road\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7},\"description\":\"The Speedster is engineered for maximum velocity and agility on the road!\",\"addons\":[\"bottle cage\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1011 $ '{\"model\":\"Urbanite\",\"brand\":\"CityCruiser\",\"price\":320,\"type\":\"City\",\"specs\":{\"material\":\"aluminium\",\"weight\":12},\"description\":\"The Urbanite is your perfect companion for navigating busy city streets!\",\"addons\":[\"phone holder\",\"rear rack\"],\"helmet_included\":false, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1012 $ '{\"model\":\"Thrasher\",\"brand\":\"Extreme\",\"price\":290,\"type\":\"BMX\",\"specs\":{\"material\":\"chromoly steel\",\"weight\":10},\"description\":\"The Thrasher is for riders who dare to push their limits at the skatepark!\",\"addons\":[\"grind pegs\",\"handlebar pad\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1013 $ '{\"model\":\"Navigator\",\"brand\":\"Trekker\",\"price\":460,\"type\":\"Hybrid\",\"specs\":{\"material\":\"aluminium\",\"weight\":14},\"description\":\"The Navigator guides you through urban streets and off-road trails with ease!\",\"addons\":[\"front basket\",\"bike lock\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1014 $ '{\"model\":\"Freedom\",\"brand\":\"Liberty\",\"price\":400,\"type\":\"Electric\",\"specs\":{\"material\":\"aluminium\",\"weight\":16},\"description\":\"The Freedom empowers you with effortless mobility and eco-friendly commuting!\",\"addons\":[\"phone mount\",\"rear light\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1015 $ '{\"model\":\"Trekker\",\"brand\":\"Wanderlust\",\"price\":480,\"type\":\"Touring\",\"specs\":{\"material\":\"steel\",\"weight\":17},\"description\":\"The Trekker is your reliable companion for long-distance journeys and exploration!\",\"addons\":[\"panniers\",\"bottle holder\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1016 $ '{\"model\":\"Seeker\",\"brand\":\"Nomad\",\"price\":430,\"type\":\"Gravel\",\"specs\":{\"material\":\"titanium\",\"weight\":13},\"description\":\"The Seeker is built to explore rugged terrain and endless adventures!\",\"addons\":[\"tool kit\",\"kickstand\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1017 $ '{\"model\":\"Pioneer\",\"brand\":\"Frontier\",\"price\":380,\"type\":\"Mountain\",\"specs\":{\"material\":\"aluminium\",\"weight\":16},\"description\":\"The Pioneer leads the way through challenging trails and mountainous terrain!\",\"addons\":[\"handlebar grips\",\"bike lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1018 $ '{\"model\":\"Aviator\",\"brand\":\"SkyRider\",\"price\":340,\"type\":\"Leisure\",\"specs\":{\"material\":\"aluminium\",\"weight\":11},\"description\":\"The Aviator takes you on a leisurely journey with comfort and style!\",\"addons\":[\"cup holder\",\"rear reflector\"],\"helmet_included\":false, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1019 $ '{\"model\":\"Outlaw\",\"brand\":\"Rebel\",\"price\":310,\"type\":\"BMX\",\"specs\":{\"material\":\"chromoly steel\",\"weight\":9},\"description\":\"The Outlaw is for riders who challenge conventions and ride with rebellion!\",\"addons\":[\"stunt pegs\",\"grip tape\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1020 $ '{\"model\":\"Storm\",\"brand\":\"Thunder\",\"price\":520,\"type\":\"Fat\",\"specs\":{\"material\":\"aluminium\",\"weight\":22},\"description\":\"The Storm conquers any terrain with its robust build and unstoppable performance!\",\"addons\":[\"saddlebag\",\"bottle holder\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1021 $ '{\"model\":\"Glider\",\"brand\":\"Aero\",\"price\":360,\"type\":\"Electric\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":19},\"description\":\"The Glider offers smooth and efficient rides, powered by cutting-edge technology!\",\"addons\":[\"cargo rack\",\"bike bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1022 $ '{\"model\":\"Voyager\",\"brand\":\"Wanderer\",\"price\":470,\"type\":\"Touring\",\"specs\":{\"material\":\"steel\",\"weight\":18},\"description\":\"The Voyager takes you on epic journeys across continents with comfort and reliability!\",\"addons\":[\"bike pump\",\"pannier rack\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1023 $ '{\"model\":\"Nomad\",\"brand\":\"Explorer\",\"price\":340,\"type\":\"Hybrid\",\"specs\":{\"material\":\"aluminium\",\"weight\":13},\"description\":\"The Nomad is your versatile companion for urban commuting and weekend adventures!\",\"addons\":[\"fenders\",\"kickstand\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1024 $ '{\"model\":\"Cruiser\",\"brand\":\"Coastline\",\"price\":290,\"type\":\"Leisure\",\"specs\":{\"material\":\"steel\",\"weight\":14},\"description\":\"The Cruiser offers laid-back rides along the coast with comfort and style!\",\"addons\":[\"cup holder\",\"rear rack\"],\"helmet_included\":false, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1025 $ '{\"model\":\"Thrill\",\"brand\":\"Velocity\",\"price\":380,\"type\":\"BMX\",\"specs\":{\"material\":\"chromoly steel\",\"weight\":10},\"description\":\"The Thrill is designed for adrenaline junkies who seek excitement on two wheels!\",\"addons\":[\"foot pegs\",\"bar ends\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1026 $ '{\"model\":\"Aero\",\"brand\":\"Flyer\",\"price\":450,\"type\":\"Road\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":8},\"description\":\"The Aero is engineered for aerodynamic performance, perfect for speed enthusiasts!\",\"addons\":[\"aero bars\",\"speedometer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1027 $ '{\"model\":\"Explorer\",\"brand\":\"Trekker\",\"price\":480,\"type\":\"Mountain\",\"specs\":{\"material\":\"aluminium\",\"weight\":17},\"description\":\"The Explorer conquers rugged trails and mountain peaks with confidence and agility!\",\"addons\":[\"bike pump\",\"hydration pack\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1028 $ '{\"model\":\"Cruiser\",\"brand\":\"RetroRide\",\"price\":320,\"type\":\"Leisure\",\"specs\":{\"material\":\"steel\",\"weight\":13},\"description\":\"The Cruiser brings back the nostalgia of classic rides with modern comfort and reliability!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":false, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1029 $ '{\"model\":\"Navigator\",\"brand\":\"Pathfinder\",\"price\":460,\"type\":\"Hybrid\",\"specs\":{\"material\":\"aluminium\",\"weight\":15},\"description\":\"The Navigator guides you through city streets and nature trails with ease and style!\",\"addons\":[\"front basket\",\"bike lock\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1030 $ '{\"model\":\"Freedom\",\"brand\":\"EcoRide\",\"price\":420,\"type\":\"Electric\",\"specs\":{\"material\":\"aluminium\",\"weight\":17},\"description\":\"The Freedom offers eco-friendly mobility without compromising on style and performance!\",\"addons\":[\"phone mount\",\"rear light\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1031 $ '{\"model\":\"Wanderer\",\"brand\":\"Nomad\",\"price\":490,\"type\":\"Touring\",\"specs\":{\"material\":\"steel\",\"weight\":19},\"description\":\"The Wanderer takes you on epic adventures across varied landscapes with comfort and endurance!\",\"addons\":[\"pannier rack\",\"bottle holder\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1032 $ '{\"model\":\"Seeker\",\"brand\":\"Frontier\",\"price\":440,\"type\":\"Gravel\",\"specs\":{\"material\":\"titanium\",\"weight\":14},\"description\":\"The Seeker is your ultimate companion for exploring gravel roads and rough terrains!\",\"addons\":[\"tool kit\",\"kickstand\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1033 $ '{\"model\":\"Pioneer\",\"brand\":\"Summit\",\"price\":390,\"type\":\"Mountain\",\"specs\":{\"material\":\"aluminium\",\"weight\":16},\"description\":\"The Pioneer leads the way through challenging trails and rocky terrain with confidence!\",\"addons\":[\"handlebar grips\",\"bike lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1034 $ '{\"model\":\"Aviator\",\"brand\":\"SkyHigh\",\"price\":350,\"type\":\"Leisure\",\"specs\":{\"material\":\"aluminium\",\"weight\":12},\"description\":\"The Aviator takes you on leisurely flights along bike paths with comfort and style!\",\"addons\":[\"cup holder\",\"rear reflector\"],\"helmet_included\":false, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1035 $ '{\"model\":\"Thrifty Rider\",\"brand\":\"Pre-Owned Pedals\",\"price\":89,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":12.5},\"description\":\"The Thrifty Rider offers affordable fun for young cyclists, perfect for their first biking experiences!\",\"addons\":[\"training wheels\",\"bell\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1036 $ '{\"model\":\"Starter Steed\",\"brand\":\"Secondhand Cycles\",\"price\":99,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":14.5},\"description\":\"The Starter Steed is a reliable companion for young riders, providing stability and confidence on every journey!\",\"addons\":[\"basket\",\"streamers\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1037 $ '{\"model\":\"Junior Jumper\",\"brand\":\"Pre-Loved Wheels\",\"price\":109,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":11.5},\"description\":\"The Junior Jumper inspires young adventurers to leap into outdoor exploration, offering reliable performance at a budget-friendly price!\",\"addons\":[\"water bottle\",\"front basket\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1038 $ '{\"model\":\"Cozy Cruiser\",\"brand\":\"Hand-Me-Down Bikes\",\"price\":79,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":13.5},\"description\":\"The Cozy Cruiser provides comfort and joy for young riders, perfect for leisurely rides around the neighborhood!\",\"addons\":[\"handlebar pad\",\"bike bell\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1039 $ '{\"model\":\"Junior Jetstream\",\"brand\":\"Vintage Velo\",\"price\":129,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The Junior Jetstream offers young riders a taste of nostalgia and adventure, providing a smooth and reliable ride!\",\"addons\":[\"rear reflector\",\"bottle holder\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1040 $ '{\"model\":\"Tiny Trekker Plus\",\"brand\":\"Recycled Rides\",\"price\":109,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":14.5},\"description\":\"The Tiny Trekker Plus accompanies young adventurers on exciting journeys, offering durability and fun at an unbeatable price!\",\"addons\":[\"handlebar basket\",\"training wheels\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1041 $ '{\"model\":\"Vintage Velo Junior\",\"brand\":\"Retro Riders\",\"price\":99,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The Vintage Velo Junior brings timeless style and joy to young riders, perfect for nostalgic adventures!\",\"addons\":[\"bike bell\",\"streamers\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1042 $ '{\"model\":\"Retro Racer\",\"brand\":\"Classic Cycles\",\"price\":119,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":13},\"description\":\"The Retro Racer evokes memories of yesteryears, providing young cyclists with a taste of vintage fun and excitement!\",\"addons\":[\"basket\",\"handlebar pad\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1043 $ '{\"model\":\"Hand-Me-Down Hero\",\"brand\":\"Family Treasures\",\"price\":89,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":12},\"description\":\"The Hand-Me-Down Hero carries on the tradition of family adventures, offering reliability and charm to young riders!\",\"addons\":[\"training wheels\",\"bell\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1044 $ '{\"model\":\"Retro Rocket\",\"brand\":\"Nostalgia Bikes\",\"price\":109,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":14},\"description\":\"The Retro Rocket blasts young riders back in time with its vintage design and reliable performance, perfect for retro enthusiasts!\",\"addons\":[\"handlebar pad\",\"bike bell\"],\"helmet_included\":true, \"condition\":\"used\"}'\nJSON.SET sample_bicycle:1045 $ '{\"model\":\"Mini Rocket\",\"brand\":\"Zoom Kids\",\"price\":149,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":12},\"description\":\"The Mini Rocket delivers fun and excitement for young riders, perfect for their first biking adventures!\",\"addons\":[\"training wheels\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1046 $ '{\"model\":\"Speedy Jr.\",\"brand\":\"Velocity Kids\",\"price\":129,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":14},\"description\":\"The Speedy Jr. is designed for budding cyclists, offering stability and confidence for learning riders!\",\"addons\":[\"basket\",\"streamers\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1047 $ '{\"model\":\"Zoomster\",\"brand\":\"Adventure Kids\",\"price\":169,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The Zoomster inspires young adventurers with its vibrant design and easy-to-handle frame, perfect for outdoor exploration!\",\"addons\":[\"water bottle\",\"front basket\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1048 $ '{\"model\":\"Tiny Turbo\",\"brand\":\"Speedy Wheels\",\"price\":119,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":13},\"description\":\"The Tiny Turbo sparks excitement with its sleek design and sturdy build, ideal for young cyclists on the move!\",\"addons\":[\"handlebar pad\",\"bike bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1049 $ '{\"model\":\"Swift Rider\",\"brand\":\"Little Racers\",\"price\":139,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":10},\"description\":\"The Swift Rider offers young racers a taste of speed and agility, perfect for budding champions!\",\"addons\":[\"knee and elbow pads\",\"bottle cage\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1050 $ '{\"model\":\"Mini Cruiser\",\"brand\":\"Tiny Trekkers\",\"price\":159,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":15},\"description\":\"The Mini Cruiser cruises through the neighborhood with style and ease, providing young riders with hours of fun!\",\"addons\":[\"rear cargo rack\",\"bike horn\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1051 $ '{\"model\":\"Buddy Bike\",\"brand\":\"Happy Pedals\",\"price\":129,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":11.5},\"description\":\"The Buddy Bike is the perfect companion for young riders, offering stability and comfort for memorable adventures!\",\"addons\":[\"handlebar streamers\",\"front light\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1052 $ '{\"model\":\"Junior Jet\",\"brand\":\"Speedy Spokes\",\"price\":149,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The Junior Jet zooms ahead with young riders, inspiring a love for cycling and outdoor exploration!\",\"addons\":[\"rear reflector\",\"bottle holder\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1053 $ '{\"model\":\"Tiny Trekker\",\"brand\":\"Adventure Wheels\",\"price\":159,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":14},\"description\":\"The Tiny Trekker embarks on exciting journeys with young adventurers, offering reliability and fun on every ride!\",\"addons\":[\"handlebar basket\",\"training wheels\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1054 $ '{\"model\":\"Mini Racer\",\"brand\":\"Speedy Sprint\",\"price\":139,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The Mini Racer ignites young imaginations with its sporty design and smooth ride, perfect for aspiring racers!\",\"addons\":[\"bike bell\",\"streamers\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1055 $ '{\"model\":\"Tiny Thunder\",\"brand\":\"Zoom Kids\",\"price\":119,\"type\":\"Kids\",\"specs\":{\"material\":\"steel\",\"weight\":13.5},\"description\":\"The Tiny Thunder unleashes excitement for young riders, offering durability and thrill on every ride!\",\"addons\":[\"basket\",\"handlebar pad\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1056 $ '{\"model\":\"Junior Cruiser\",\"brand\":\"Happy Wheels\",\"price\":159,\"type\":\"Kids\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The Junior Cruiser cruises in style with young cyclists, providing comfort and joy on every adventure!\",\"addons\":[\"rear cargo rack\",\"bike horn\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1057 $ '{\"model\":\"Sprinter\",\"brand\":\"Velocity\",\"price\":299,\"type\":\"Mountain\",\"specs\":{\"material\":\"aluminum\",\"weight\":10},\"description\":\"The Sprinter conquers rugged trails with ease, offering agility and durability for off-road adventures!\",\"addons\":[\"hydration pack\",\"multi-tool\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1058 $ '{\"model\":\"Cruiser\",\"brand\":\"Cyclopedic\",\"price\":249,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":12},\"description\":\"The Cruiser navigates urban streets with style and comfort, perfect for leisurely rides around the city!\",\"addons\":[\"rear rack\",\"kickstand\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1059 $ '{\"model\":\"Trailblazer\",\"brand\":\"Adventure Seeker\",\"price\":369,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":9},\"description\":\"The Trailblazer blazes new paths through rugged terrain, offering unmatched performance and durability for mountain enthusiasts!\",\"addons\":[\"bike pump\",\"repair kit\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1060 $ '{\"model\":\"Cityscape\",\"brand\":\"Urban Glide\",\"price\":279,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The Cityscape glides through city streets with elegance and grace, offering style and convenience for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1061 $ '{\"model\":\"Explorer\",\"brand\":\"Terrain Tamer\",\"price\":399,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7},\"description\":\"The Explorer conquers any terrain with precision and speed, making it the ultimate choice for adventurous mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1062 $ '{\"model\":\"Tourer\",\"brand\":\"Nomad Bikes\",\"price\":329,\"type\":\"Touring\",\"specs\":{\"material\":\"aluminum\",\"weight\":13},\"description\":\"The Tourer is designed for long-distance adventures, offering comfort and reliability for cyclists exploring new horizons!\",\"addons\":[\"panniers\",\"rear view mirror\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1063 $ '{\"model\":\"City Cruiser\",\"brand\":\"Metro Bikes\",\"price\":289,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":12.5},\"description\":\"The City Cruiser glides through urban landscapes with ease, offering comfort and style for city commuters!\",\"addons\":[\"front light\",\"fenders\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1064 $ '{\"model\":\"Adventurer\",\"brand\":\"Trailblazer Bikes\",\"price\":379,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8},\"description\":\"The Adventurer thrives on challenging trails, offering agility and control for riders seeking adrenaline-fueled adventures!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1065 $ '{\"model\":\"Commuter\",\"brand\":\"City Commute\",\"price\":259,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The Commuter is the perfect companion for daily city commutes, offering efficiency and comfort for urban cyclists!\",\"addons\":[\"rear rack\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1066 $ '{\"model\":\"Explorer Pro\",\"brand\":\"Offroad Masters\",\"price\":449,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":6.5},\"description\":\"The Explorer Pro is a beast on the trails, offering unmatched performance and agility for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1067 $ '{\"model\":\"Urban Glide\",\"brand\":\"City Wheels\",\"price\":299,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":12},\"description\":\"The Urban Glide effortlessly maneuvers through city streets, offering style and comfort for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1068 $ '{\"model\":\"Trail Master\",\"brand\":\"Trail Trek\",\"price\":379,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Trail Master dominates rugged terrain, offering superior control and durability for avid mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1069 $ '{\"model\":\"City Slicker\",\"brand\":\"Urban Bikes\",\"price\":269,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Slicker navigates city streets with finesse, offering efficiency and style for urban cyclists!\",\"addons\":[\"front light\",\"fenders\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1070 $ '{\"model\":\"Explorer Elite\",\"brand\":\"Offroad Experts\",\"price\":499,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":6},\"description\":\"The Explorer Elite sets new standards for mountain biking, offering unparalleled performance and agility for extreme riders!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1071 $ '{\"model\":\"City Cruiser Pro\",\"brand\":\"Metro Riders\",\"price\":329,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11.5},\"description\":\"The City Cruiser Pro glides through urban landscapes with precision, offering comfort and style for discerning city commuters!\",\"addons\":[\"front light\",\"fenders\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1072 $ '{\"model\":\"Trailblazer Pro\",\"brand\":\"Adventure Seeker\",\"price\":399,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8},\"description\":\"The Trailblazer Pro conquers challenging trails with ease, offering advanced features and durability for serious mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1073 $ '{\"model\":\"City Commuter\",\"brand\":\"Urban Trek\",\"price\":279,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The City Commuter is designed for seamless urban journeys, offering efficiency and comfort for daily commuters!\",\"addons\":[\"rear rack\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1074 $ '{\"model\":\"Mountain Master\",\"brand\":\"Offroad Warriors\",\"price\":449,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7},\"description\":\"The Mountain Master dominates any trail with precision and speed, offering unmatched performance for hardcore mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1075 $ '{\"model\":\"Urban Explorer\",\"brand\":\"City Adventures\",\"price\":309,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":12},\"description\":\"The Urban Explorer navigates bustling city streets with confidence, offering comfort and style for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1076 $ '{\"model\":\"Trail Conqueror\",\"brand\":\"Mountain Legends\",\"price\":389,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Trail Conqueror dominates rough terrain, offering superior control and durability for adventurous mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1077 $ '{\"model\":\"City Trekker\",\"brand\":\"Urban Roam\",\"price\":289,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Trekker navigates city streets with ease, offering efficiency and style for urban explorers!\",\"addons\":[\"rear rack\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1078 $ '{\"model\":\"Mountain Maverick\",\"brand\":\"Trail Blazers\",\"price\":429,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Maverick blazes new trails with agility and precision, offering top-tier performance for experienced mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1079 $ '{\"model\":\"Urban Voyager\",\"brand\":\"City Riders\",\"price\":319,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11.5},\"description\":\"The Urban Voyager explores city streets with confidence, offering comfort and style for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1080 $ '{\"model\":\"Trail Seeker\",\"brand\":\"Adventure Wheels\",\"price\":369,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8},\"description\":\"The Trail Seeker conquers challenging trails with ease, offering durability and control for avid mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1081 $ '{\"model\":\"City Roamer\",\"brand\":\"Urban Cruisers\",\"price\":299,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":11},\"description\":\"The City Roamer glides through city streets with elegance and grace, offering comfort and style for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1082 $ '{\"model\":\"Mountain Pro\",\"brand\":\"Extreme Trails\",\"price\":469,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Pro dominates rugged terrain with precision and speed, offering top-tier performance for hardcore mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1083 $ '{\"model\":\"City Navigator\",\"brand\":\"Urban Wheels\",\"price\":309,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11},\"description\":\"The City Navigator maneuvers through bustling streets with ease, offering efficiency and comfort for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1084 $ '{\"model\":\"Trail Challenger\",\"brand\":\"Mountain Movers\",\"price\":399,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Trail Challenger conquers any trail with confidence, offering durability and agility for adventurous mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1085 $ '{\"model\":\"City Rover\",\"brand\":\"Urban Wheels\",\"price\":329,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Rover navigates city streets with efficiency and style, offering comfort and convenience for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1086 $ '{\"model\":\"Mountain Explorer\",\"brand\":\"Peak Riders\",\"price\":449,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Explorer conquers any terrain with precision and speed, offering top-tier performance for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1087 $ '{\"model\":\"City Stroller\",\"brand\":\"Urban Cruisers\",\"price\":289,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11.5},\"description\":\"The City Stroller glides through city streets with elegance and comfort, offering style and efficiency for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1088 $ '{\"model\":\"Mountain Conqueror\",\"brand\":\"Trail Blasters\",\"price\":419,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Mountain Conqueror dominates rough terrain with ease, offering durability and control for adventurous mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1089 $ '{\"model\":\"City Navigator Pro\",\"brand\":\"Urban Wheels\",\"price\":339,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Navigator Pro maneuvers through bustling streets with ease, offering efficiency and comfort for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1090 $ '{\"model\":\"Mountain Voyager\",\"brand\":\"Summit Seekers\",\"price\":459,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Voyager conquers any trail with confidence, offering top-tier performance for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1091 $ '{\"model\":\"City Cruiser Plus\",\"brand\":\"Urban Adventures\",\"price\":309,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11.5},\"description\":\"The City Cruiser Plus navigates city streets with style and comfort, offering efficiency and convenience for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1092 $ '{\"model\":\"Mountain Dominator\",\"brand\":\"Peak Crushers\",\"price\":479,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Mountain Dominator conquers rough terrain with ease, offering durability and control for hardcore mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1093 $ '{\"model\":\"City Trekker Plus\",\"brand\":\"Urban Wheels\",\"price\":349,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Trekker Plus navigates city streets with ease and efficiency, offering comfort and style for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1094 $ '{\"model\":\"Mountain Warrior\",\"brand\":\"Summit Crushers\",\"price\":489,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Warrior dominates any trail with precision and speed, offering top-tier performance for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1095 $ '{\"model\":\"City Roamer Pro\",\"brand\":\"Urban Adventures\",\"price\":329,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11.5},\"description\":\"The City Roamer Pro glides through city streets with precision and style, offering comfort and convenience for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1096 $ '{\"model\":\"Mountain Maverick Plus\",\"brand\":\"Trail Blazers\",\"price\":499,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Maverick Plus sets new standards for mountain biking, offering unmatched performance and agility for extreme riders!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1097 $ '{\"model\":\"City Navigator Elite\",\"brand\":\"Urban Wheels\",\"price\":359,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Navigator Elite maneuvers through bustling streets with ease, offering efficiency and comfort for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1098 $ '{\"model\":\"Mountain Raptor\",\"brand\":\"Ridge Riders\",\"price\":499,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Mountain Raptor conquers rugged trails with precision and speed, offering top-tier performance for hardcore mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1099 $ '{\"model\":\"City Cruiser Pro Plus\",\"brand\":\"Urban Adventures\",\"price\":369,\"type\":\"City\",\"specs\":{\"material\":\"steel\",\"weight\":11.5},\"description\":\"The City Cruiser Pro Plus glides through city streets with precision and style, offering comfort and convenience for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1100 $ '{\"model\":\"Mountain Trailblazer\",\"brand\":\"Summit Seekers\",\"price\":509,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Trailblazer conquers any terrain with confidence, offering top-tier performance for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1101 $ '{\"model\":\"City Roamer Elite\",\"brand\":\"Urban Riders\",\"price\":339,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Roamer Elite navigates city streets with precision and style, offering comfort and convenience for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1102 $ '{\"model\":\"Mountain Legend\",\"brand\":\"Peak Crushers\",\"price\":499,\"type\":\"Mountain\",\"specs\":{\"material\":\"titanium\",\"weight\":8.5},\"description\":\"The Mountain Legend conquers rough terrain with ease, offering durability and control for hardcore mountain bikers!\",\"addons\":[\"hydration pack\",\"bike computer\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1103 $ '{\"model\":\"City Trekker Elite\",\"brand\":\"Urban Wheels\",\"price\":359,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Trekker Elite navigates city streets with ease and efficiency, offering comfort and style for urban adventurers!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1104 $ '{\"model\":\"Mountain Dominator Pro\",\"brand\":\"Peak Crushers\",\"price\":519,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Dominator Pro conquers any trail with precision and speed, offering top-tier performance for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1105 $ '{\"model\":\"City Roamer Pro Plus\",\"brand\":\"Urban Riders\",\"price\":349,\"type\":\"City\",\"specs\":{\"material\":\"aluminum\",\"weight\":10.5},\"description\":\"The City Roamer Pro Plus glides through city streets with precision and style, offering comfort and convenience for urban commuters!\",\"addons\":[\"basket\",\"bell\"],\"helmet_included\":true, \"condition\":\"new\"}'\nJSON.SET sample_bicycle:1106 $ '{\"model\":\"Mountain Voyager Plus\",\"brand\":\"Summit Seekers\",\"price\":529,\"type\":\"Mountain\",\"specs\":{\"material\":\"carbon fiber\",\"weight\":7.5},\"description\":\"The Mountain Voyager Plus conquers any trail with confidence, offering top-tier performance for serious mountain bikers!\",\"addons\":[\"GPS tracker\",\"LED lights\"],\"helmet_included\":true, \"condition\":\"new\"}'\n"
  },
  {
    "path": "redisinsight/api/data/manifest.json",
    "content": "{\n  \"files\": [\n    {\n      \"path\": \"common\"\n    },\n    {\n      \"path\": \"json\",\n      \"modules\": [\"rejson\"]\n    },\n    {\n      \"path\": \"search\",\n      \"modules\": [\"search\", \"searchlight\", \"ft\", \"ftl\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "redisinsight/api/data/search",
    "content": "FT.CREATE idx:smpl_bicycle ON JSON PREFIX 1 sample_bicycle: SCHEMA $.brand AS brand TEXT $.model AS model TEXT $.description AS description TEXT $.price AS price NUMERIC $.condition AS condition TAG SEPARATOR , $.type AS type TAG $.helmet_included AS helmet_included TAG $.specs.material AS material TAG $.specs.weight AS weight NUMERIC\n\nFT.CREATE \"idx:smpl_restaurant\" ON JSON PREFIX 1 \"sample_restaurant:\" SCHEMA \"$.cuisine\" AS \"cuisine\" TAG \"$.name\" AS \"restaunt_name\" TEXT \"$.location\" AS \"location\" GEO\n"
  },
  {
    "path": "redisinsight/api/data/vector-collections/bikes",
    "content": "HSET bikes:10000  model 'Saturn' brand 'BikeShind' price 3837 type 'Enduro bikes' material 'alloy' weight 7.2 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. It has a lightweight frame and all-carbon fork, with cables routed internally. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"p\\xaa\\xcb\\xbc\\x91$\\x1a<#\\x07\\xa8\\xbc\\x8b\\xf6\\xd0;\\xfc\\xce\\x96<\\x0b\\x07\\x95=i\\x85\\x1f\\xbd\\xb0\\xc3\\x89\\xbb\\xbd\\xef\\xa0=\\xd4aL;X\\xfb\\xd4<6\\xd8n=\\xfb\\x94y=i\\x16\\x03\\xbc\\x95\\xde\\x9a=\\\"\\xe8\\x96\\xbdT\\x91p=\\xfd\\xc2d\\xbc\\x91-Y\\xbc\\\\wf\\xbd\\x87\\xac\\xcc;a\\xd5\\x18\\xbdn\\rH=\\xe8\\xdf\\xb4\\xbd\\x12)p\\xbd\\x98\\x85\\x92\\xbcj\\x00\\x8d;,#\\x0f<\\na\\xdb\\xbc\\xee\\xa3?=\\x7f\\xad\\xf3\\xbb]\\xa3\\xdd\\xbc\\x0f\\x1f\\x81<t\\xb1*\\xbcD\\x9c\\xfe;\\xbd\\x9a\\xd9\\xbcO]\\xdf<\\xe2\\xb1\\x9b\\xbc\\xd3\\xd8X9\\xa4\\x8e\\xa5<s\\xb4\\x06=\\x90\\xe2\\\"=\\xdc)*=d\\x05\\\\<j&@\\xbc \\xb1\\x8c<Lv#=\\xe7\\x85\\xe8:/\\x9f:\\xbde\\xe0\\x1e\\xbc[*K\\xbc1\\xdc%\\xbd\\xb8Q\\xa4\\xbbM\\x9f\\x19<\\xdf%\\x0b=\\xe9\\xa4|\\xbb\\xe3\\xba*\\xbc\\xfc\\xbc\\xb7\\xbd\\xa1\\xf7z\\xbd\\x94\\xd3\\xb5=\\x15\\xf1,=\\x02\\x80W=\\x0e\\x8cv<\\x9f\\xab\\x93=\\xb0/\\x06=x\\xca\\xd2<\\x98{x<\\x8b\\xe3\\x9a\\xbb#\\xe7#\\xbc\\x02\\x82f\\xbd\\x82\\xaa\\x00\\xbdbW\\xd5<\\xb7.N\\xbdx\\xb4\\xa6\\xbc\\xfcpT\\xbd\\x19\\xb6\\x16=\\xdb\\xf0q\\xbc\\xae\\xfe\\xc4\\xbc\\xa5\\xcd%\\xbd\\x85\\n\\x8a<4\\xbc\\x16<\\xdc\\xe0\\x9d\\xbd\\\"\\xdf\\x8f\\xbc\\x7f\\x14\\x06\\xbd\\xbee\\x02\\xbcY\\xe9\\xb0\\xbc\\xa7\\x92T8\\\\\\xe2\\n=\\x84\\x92\\x8b=Kq\\xf28\\xe9\\xcb!\\xbb\\xe9\\xfc.\\xbc\\x1a\\\"\\xc5\\xbc\\xdf\\x99l\\xbcj\\xfb\\xaf<\\xf7\\x03\\x84<\\xac\\x8e/\\xbd\\xa68\\x1f\\xbd\\x12\\xb8>=}\\x81\\xa5\\xbd]\\xbf\\xba< \\x93\\x97\\xbaV\\\"\\xfb<E\\x84\\x84\\xbdVE\\x97;7\\x9b\\xa0=\\x10f\\xf1\\xbc\\xf6>{<\\xd8\\xd4\\xd3\\xbcv\\x88\\xb3;\\xc1\\x19\\xd9;\\x16\\xbd\\x95=@\\xcb\\xcf<\\xe8\\xeez\\xbc\\xfbRZ\\xbd\\xae*);\\x8e\\xb05;h\\xaaW\\xbc\\x98\\\\\\\\=\\r7\\xd4<\\x0c\\xd2\\xb0<\\xa9?\\x8b<\\x05\\x1eo\\xbd\\xbd{\\xc1\\xbc\\x15fm<G\\x90\\x1c\\xbcl\\\"\\x17=\\xee\\x07\\x81\\xbd{$\\x8b\\xbc\\x8b\\xb1.\\xbddF\\x92\\xbc\\x01\\x93\\x1d\\xbd\\xce\\x90\\xa7<\\xc8E\\xf5<xL>\\xbd\\x00\\xe3\\xae<\\xe2+\\x98<{\\x07A:\\xd9\\x8a[<!\\x87)\\xbd\\x81\\x1c\\xea\\xbb\\xc7|S\\xbdF!~\\xbb($\\xa4=(\\x1fS<)\\x97U\\xbc\\xbew\\xfb\\xba\\r\\xc8\\x0f\\xbd\\xfc\\xe1\\xc1\\xbck\\xdeK<\\x84\\xbb*\\xbc)\\xb2b<\\xbdXV\\xbd\\xa3\\xe6\\xc6<0\\xbd\\xff\\xbc\\xe6\\xfaH\\xbd;\\xb8\\x97\\xbb+M\\x87;\\xa1\\xb85=\\x0c\\xf6N\\xbdP\\xf4T=\\xe6{\\xf5<+\\xe2\\x02\\xbd\\xd6\\xca.\\xbd\\xd4\\x98\\xe9<f\\xd3\\xb0<\\xae\\xee|\\xbbv\\x15\\xd2;\\xc7\\xc59=\\x8d\\x1f\\xe0<\\xe9\\xdc\\xa3\\xbc1\\xd1v<\\xebwZ\\xbbk\\xdf6\\xbd\\xd4*\\xd9\\xbc\\xb2\\x0b\\xe5\\xbb\\xde\\x17 \\xbd\\xc2!\\x99=\\xdcZ\\xc1\\xbc\\xd3\\xe4\\x9b\\xbc\\x9fFt<\\xcf\\x16P<\\xd9j\\xa3<\\xd5\\xfd\\xd5<\\xda\\xafo=-\\xa2\\xbb;\\x00\\x1d\\xea<\\xd7\\x86\\xd8;-\\xce\\xfc<\\xd1\\xdf!\\xbdW\\x8fI\\xbb\\x1270=\\xe8\\xf4J\\xbd@\\x14\\x10=;\\x0f\\x98\\xbb\\xc942<G\\xf5+\\xbd\\\"\\x16\\xef\\xbc]\\xc6\\xab<k[ =\\xb2\\xd0\\x14\\xbd\\xd3;\\xae\\xbdO\\xf1 \\xbd4\\xaaJ\\xbdsWk\\xbc\\xc5\\x19\\x85<N\\xca\\xbb\\xbc\\xea\\x16\\x9a\\xbc\\xd6M\\xf6<\\x08\\x05,\\xbd\\xfc\\xc0\\x82\\xbcr\\xe0\\x01\\xbc\\xd7b\\x9e<\\x01<\\x90\\xbc0\\x8c8\\xbc\\x17\\x1dv\\xbc\\xa2p|\\xbdg\\xa8\\x96\\xbc\\xa9ra=\\x7fra\\xbd\\x9b\\xe7\\x13<\\xf4\\x0c\\xb0\\xbb\\x05\\r\\xe4\\xbc\\x99\\xa9/<]\\x99\\x9d\\xbc\\xcc\\xd9\\x11\\xbd\\xf7\\xc2m\\xbb\\xe0\\x98p<\\x833\\x10=\\x91\\xc2Y\\xbd\\xf0&\\x02\\xbd\\xf2\\xd1\\x97\\xbca\\xc0\\xd5<\\xc2X\\xe5<G\\x06;\\xb9\\x97\\\"T\\xbdtI\\x13<\\xbb\\xac%=J\\xb6\\xc5\\xbc\\x88=\\xb1\\xbcfb\\x1c<\\x8bZ\\x0b;\\xe6v\\x89=\\x9e\\xf9\\x8e<\\xdc\\x9cT<!\\xe0.<0\\xf0c=\\xe5e\\xd1;\\x18A{\\xbd\\xc1H5=\\x15\\xad\\xf3<\\x0f\\xcbO\\xbdFU\\xeb<\\xe9\\xe4b=hQK\\xbc\\xde\\xb0 =\\xc4@\\xda\\xbc\\x0b*\\x92<rcy\\xba\\x8e\\x98,=0S\\x07\\xbcfh\\x81<P,X\\xbd\\x8d6\\x159\\xd1\\xa2<\\xbdt\\x89\\xbd=\\xa4-\\x97\\xbb\\x1e\\xfe\\x86\\xba\\x82\\xa1g\\xbc\\\"=\\xab;\\xc9RQ<X\\xf7\\x8b\\xbc\\xa5\\x11\\xdf\\xbc\\xcb\\xcc\\x95<n\\x17\\xfb;\\xaf\\x1a\\xd1\\xbc$\\xb5\\xc2\\xb8[\\x9f\\x16\\xbc\\xe5X\\xf9\\xbb)\\xd6\\xb3\\xbd\\xa1M\\x8c\\xbb\\xde\\xd6\\x14=\\x0e\\xdeC<\\xbc\\xb2m=\\xec\\\\\\xf6:\\x18\\x9e2\\xbd\\xd3s\\xfe\\xbc/\\xc5\\x8a=\\xcc6\\x97\\xbd\\xb5\\xce\\xef<P\\xb4#\\xbb\\x8b\\xabq\\xbc\\xe2\\x84\\x9b=\\x05c\\x01<\\x9b1\\x18\\xbd\\x1f\\xd2\\xe3\\xbb\\xf3\\xc6\\x0c=\\x1bF\\x0b=\\xf9\\x08\\xa3<\\x15\\\"\\x98\\xbcL{\\xa1\\xbc\\xb3lQ;\\x0f\\x18w=\\x19|\\xbe\\xbc\\x80\\xa4\\x10=>\\xb3\\xd5<\\xd6W\\xbd\\xba\\\"\\x9c\\xa5<r\\t\\x98=\\xc9\\x08!=Y\\xa7\\xc4\\xbc\\x86\\x15S\\xbd2\\xd3\\x14\\xbd\\xaa\\xce\\x18\\xbd\\x07\\xa2\\x05=\\\"\\x9f\\r\\xbd\\x17b\\xc4\\xbc\\x05Q\\xf3<\\xca\\x1d+=Z\\xda\\xb6\\xbc\\xc1\\x82\\xe0\\xbc\\xe8*\\x03\\xbd\\x93\\xe4\\xbb<\\x8cch;\\xc9tq<h\\xa5Z\\xbd\\xb18\\x85<\\xeev\\xb8\\xbd\\x8b\\x94\\xd4<7fX<\\xf3\\xcf\\x0f\\xbd\\x0f\\xf8~\\xbd\\xa7\\xb9\\x00\\xbd\\x17\\xc6\\x94<\\x8bo\\\"<=\\xe6\\xf1\\xbc\\xe0\\x10z\\xbd\\x0e\\xce\\\"=\\xb6Y\\t=\\x8ao\\xaa\\xbb\\x96\\x10\\x81\\xbccI+\\xbd\\x88*\\x8d=[\\x98M\\xbb\\x9fz;<\\xfcJ\\x02=\\xca\\x89\\xdf=\\xe6f\\x0f<\\x87\\xde\\x98=\\x9e\\xe11=\\x95*\\x08=\\x9d\\xdc\\xcf\\xbc\\x9d\\x89|=\\x08\\xf5~\\xbb\\xb1\\xab\\x9c\\xbd2e\\xa6;r\\t\\xa2\\xbbyW\\x8a\\xbc\\x06\\xbe\\x8f\\xbc\\x04\\xdc\\xd6\\xbd\\xe6N\\x80\\xbc|7{;\\tiI=\\xe4\\xac\\x15=br\\xfc<\\x82t\\x11=\\xa2\\xfb.\\xbd\\xc4\\xd1\\xe2\\xbb\\x9f\\xcb\\xe1\\xbc&\\x13>\\xbc\\xed\\x00M=\\\"\\xc3\\x89:\\x93w\\x14<?cL\\xbc\\x80\\n\\xe9\\xbb{\\xb84<=Mt\\xbc\\xd7\\xa2\\x81\\xbd\\xc1\\xbd\\x86<fM\\xd4\\xbcR\\xd4K\\xbd\\xc6\\x87c\\xbcN!a\\xbd\\x03\\x18\\xd6\\xbbEK\\xba\\xbd\\xebf6=g\\x98\\x08\\xbd\\x9c\\xe5-<t\\x89\\xf1\\xbc\\x03\\xad\\x9d;\\xb9\\x08\\x93\\xbdd\\xa5\\xb9=\\xa9\\xb9\\xce=H\\xae \\xbd\\xf1,;<\\xbdb\\xb0<\\rX\\x89\\xbc\\xa9{~<`\\xa2J=\\x95\\x7f\\\\\\xbd:\\xff\\x1e<\\xb4\\x1b\\x03\\xbdQ\\xac\\xb1<\\x1b7\\xb2:E\\x9b;=9\\xa6{\\xbb^[J=[^\\x81\\xbc]\\xbf\\xa2\\xbb\\x11\\\"\\x1a=\\xc7<G=\\x80\\xcaY<7($<c#f<\\x1d\\x16\\xdd<\\n\\xaci<\\x08(\\xad\\xbc\\x0f\\xa2\\xb1\\xbd\\xfc\\xed\\xdf\\xbc\\xb2bF\\xbc\\xabl\\xa9\\xbb\\x90\\\\\\x02\\xbd\\x19\\xcf\\xb8<x\\xfa7=\\x01~\\xac;J\\x9e\\xdd\\xbcw(\\xa6\\xbc&\\x9d\\x8a=\\x8c\\xa45;\\xc6\\xd9\\xf9\\xbc\\xf2\\xeb\\x0b\\xbc\\xedY!\\xbcS\\xa9e\\xbd+\\xdd\\\"\\xbd~|\\xea\\xbc\\x1d\\x9cz=\\xed\\x95;=\\x0b\\xb3\\x84<\\xb1\\xde\\xce<\\xbf3\\x9f<\\xbd\\xd1\\r\\xbd\\xf4\\xf8\\x93\\xbc\\xc0t\\xf1;RZ\\x05\\xbdI\\x1dC<@\\xe4B<\\x81\\xba\\xb4<\\x07v\\x04\\xbc\\xbf\\x12{\\xbd\\xb8gt=\\xf0\\x92s\\xbdk?\\x87\\xbc\\x1d\\xc0\\x93\\xbc\\xeb^\\x90\\xbdSw\\xc4;_\\x95V\\xbbi\\x96\\xc8<\\xa9\\xb7\\x8a<|\\r\\xf1;4\\xe5\\x1f=\\x82\\x02\\x90=\\xcb2\\xee:\\x96sS\\xbbR:\\x0f\\xbd9Y\\x18<\\xb3\\x15\\xa1\\xbb\\x98\\x12\\x1c\\xbcfC\\xa8\\xbc\\xa1\\x00\\xae\\xbb\\xb1\\xdaS=\\x93\\x96;\\xbdd\\xfd\\x9e<=J\\x90\\xbc\\x97/%=\\xcd\\x19\\x92<\\xcd\\xb3\\xde;kx,\\xbd>\\xc4\\xb6\\xb9UM\\x8a;)Q\\x8e<\\xd1F(\\xbcz\\x9e\\x03=\\xb4\\xe2\\xf2\\xbb\\x94\\x99$<~\\x11>\\xbd\\xe7i\\x15\\xbd\\x8a\\x01\\xd6<\\xd4\\xe9\\x9f<`\\xbf6\\xbc\\x08\\xd4\\xab\\xbb\\xf8\\x0c\\x89\\xbd@\\x9d\\t<o\\xfe\\xf7<\\xb5&\\x92:\\x81\\x9bj\\t\\x0el~\\xbc\\xb2\\xd2l\\xbd<9\\x08=\\xcb\\xf8<\\xbcx\\xb8V<&!>=(\\xe5\\x0e\\xbc6\\xd3\\x00\\xbd\\xa8\\x82\\xa9<{\\xd8\\xfc\\xbc\\x81S\\x89<\\x87\\x1f\\x17=\\x17:\\xd0;\\xce\\xe6]=\\xb5\\x95\\x82\\xbb\\xf50+\\xbcm\\xe9n\\xbdIn.\\xbd\\xb4\\x95\\x01;p\\x89i=,\\x0b}\\xbc\\xde\\xae\\xce\\xba\\x01\\\"\\x8b<\\xb1\\xd2(\\xbd\\xe8\\xb6\\x1d=\\x1ak\\xd4\\xbc\\xb8\\x1e\\xd4;_\\xd0\\xde;\\xf8\\x98\\xc4\\xbaz\\\\\\xec<\\x8c\\xd6K<\\x94\\x84\\x17=\\x7fv\\x91\\xbd\\xcb\\xe7n\\xbdP<\\xb4\\xbd\\xf8\\xc5\\xbd<\\x00\\x0fF=\\x85z\\x15<\\xc54\\\"<\\xd8\\x7fh\\xbc\\xde\\xff\\x01\\xbca\\xf4\\xa8\\xbc\\x94]\\xb0\\xbc\\xe8\\xda\\x80=?\\xa9\\x17\\xbb\\xff4\\xc4\\xbb\\x8a\\xf7\\xb2=^\\xd00\\xbd\\xa7\\xad3\\xbd\\xf1\\xf9\\x84:\\x1b\\xbc%\\xbd(\\xe84\\xbd/\\x01\\x9b<\\xf716=r\\x86\\xa3<F~\\n\\xbc\\xbd\\xc6\\x90\\xbch\\xc5\\xd8:\\x17\\xe6\\x0c=l\\xf6Z\\xbc\\xb4\\xf7Y=_\\xc8\\x9c\\xbc\\x8c%\\xb6:\\xe4E$=\\xa0\\xa1-\\xbdC\\xce0\\xbd`\\xd4\\xb5;\\x91\\xe1\\x94\\xbc?\\xcc\\x94\\xb9mQ\\xf5<\\x08\\xa6\\xcc\\xb9\\xde\\xa0\\xbb\\xbc \\xa2u\\xbcX\\x1fQ=\\xc1\\xa7\\xac<r\\x95\\xef\\xbc\\x90K\\x0f\\xbd\\t\\x12\\x90=+\\xf7_\\xbd\\x84\\xd4\\xc8=\\x08\\x02i\\xbddR\\xc0<\\x0f\\\\\\xca9/\\xcf\\x12\\xbb:\\x88\\xef\\xbc|\\xb7\\x84\\xbd\\x16Q>=}n\\x0c=\\x05\\xa8\\xfd\\xb8\\xd5\\x84C<\\xf9\\\\\\xba<\\xfe\\xb3\\xa8\\xba\\xa9\\x82\\xaa=\\xf6{\\x06;\\x9eXk=\\x86F]=\\x18\\xa7\\xed\\xbb\\xc1\\xef?\\xbdXi\\xb9:w\\x95\\x8b\\xbc\\x0eD\\xb3\\xbc\\xa8\\re<\\xd7G\\xb6<a\\xd5f;\\xa3\\\\5=\\xa6K\\x07\\xbde\\x8c\\xd4<K\\xb5\\xd7;dX!;\\\"\\x1cY\\xbd\\xc5\\xc2^=\\x92\\xd5\\xec\\xbc\\x18\\xab\\x8b<\\x99\\xa6w<\\x08\\x0f\\x9a=\\x12\\x87\\x9c<l\\xc0\\x8f<\\xa3\\xb9`\\xbc\\xf1e\\xa2<U\\x96t\\xbd\\xb0\\xd0\\x88\\xbb&\\x87\\xe6\\xbb\\x02\\x0e\\xc6;\\x1f\\xa8\\xa7<\\xee\\x08\\x98<\\x8a\\xfa\\xb6\\xbc=\\xc8\\xb3\\xbca\\xb1\\x90<|\\xb4x=/\\xa4\\xe3\\xbc\\x05\\xfa\\xde\\xbc\\xa7LR\\xbd(\\x128<h6}<q\\xac\\x90=\\xd7\\x05\\x19=\\x80u^=\\xc3\\xe4P\\xbd\\xcc\\xf3\\xb5\\xbb\\xf7`\\xb0;\\xe0\\x15S=\\xadU\\x18\\xbd\\x86>&\\xbdj\\xbc\\x9b\\xbc5\\x02\\xa6</\\xe1\\x83\\xbcb&B<P\\xa2X\\xbc\\xbf\\xe8\\xa8<\\xc1\\xd5m\\xbb\\xe5\\x0c\\x8d\\xbc-\\x0b\\x85<\\x0c\\xed\\xea\\xbc\\x08\\xd3\\x02=\\xf3G\\xb5;s\\xba}=.l\\x94\\xbd \\xfe\\x81\\xbc\\xc0\\xc0w<\\xdc\\x02\\xd2<\\x1f\\x07f<x\\xdf\\xed<\\x8a:\\x16\\xbcs\\x15\\xb9\\xbbDPM\\xbb\\x98z\\x12;\\\\\\xa3\\xc4\\xbc\\xb0>H;@\\xa5.=\\x12\\xf3=\\xbd\\x97\\xc7D;\\xc3U\\x8b\\xbb\\xf0\\x04\\xdb9!\\xeb\\xae\\xbb\\xbc$\\x0b<zr\\x96=JS(<\\xb1\\x0f\\x94;\\xbb4\\xd0<\\xbd\\xc3\\x1e\\xbc\\x7fl\\xad\\xbc\\x9b>\\x1e\\xbc\\\"\\xe5c=4T\\x1c\\xbd_\\xc9\\xec\\xbcG8\\x85<+\\xa2\\x91\\xbcu\\x12\\x84\\xbd\\x99\\xce\\x02=D\\x16R<\\xeaS ;\\xae\\x9e\\x1c=\\xa1:\\x01\\xbd2\\xc15\\xbc\\x88u\\x8d\\xbd\\xfd\\x85i\\xbd\\xcb\\x00\\n\\xbdb\\x81u\\xbc\\xd2\\x16M\\xbcN\\xd8\\x9e<\\xf0\\x94\\xf2\\xbcn\\x80\\xea\\xbcF\\xe9\\xb0\\xbcL9h\\xbd\\x88;\\\"\\xbd\\x8bf%<\\xe5\\xd6 <\\x820!\\xbd\\n@\\x93<\\x14R\\x91\\xbd\\xed\\xb3j;\\xe7\\x9d\\x8e:\\x9c\\xee\\x07<-T\\xd8<\\xb2\\x96B\\xbdb\\x14\\xa5\\xbb\\xd0\\xd2P<d\\xa2}\\xbc\\xd3\\x9b\\x85\\xbd\\xc3\\x08\\x91\\xbd\\x96\\x84\\xef\\xbc\\xb0S\\x7f\\xbc\\x0eJV=\\x05A\\x96<U\\xd9\\xc2=WES<\\xe5\\x9bb<\\r\\x07\\x89\\xbd\\xda\\x92|\\xbd\\xfc\\xa9\\x16=\\\"\\xbeQ\\xbcP\\xfa\\x85<\\xcb\\xc5\\x93\\xbc\\x01\\xc2\\x87\\xbcH\\x8a\\x8a\\xbc\\xc9D\\x8d\\xbd\\xef]\\xd9\\xbc\\xf4\\x1c\\xcd<\\x0c\\xeel=\\x1a\\x91c\\xbcG\\xd4.\\xba\\xc4\\xc8{=LM\\xf9:\\xa3j\\x02=\\xd6\\x83\\xf4<d*L\\xbc;\\x99Z\\xbditd\\xbc\\xe4@\\xb3\\xbd\\x83R2;\\x11i\\xa2\\xbc\\x94\\x98\\x04\\xbc\\xab=\\xb7<\\x87q\\t=Pw\\x00\\xbd`\\xd9K\\xbc\\x9b\\xdfh\\xbd\\xa7j\\xd1\\xbbW\\\\\\x1b\\xbd$\\xf0\\xc8\\xbd\\x07\\xa6\\xc7<0) =j3\\x07=\\x953\\xf4<\\xe7\\xa2\\xc5=F\\x8a\\xb1\\xbcn\\x86^=\\x07\\xf6[=\\xce\\xe8\\xcc\\xbb\\xfcm^<\\xc7\\xd7\\xeb\\xbc\\x90;\\x9a<\\xde=\\xc5\\xbc\"\nHSET bikes:10001  model 'Triton' brand 'Breakout' price 2215 type 'Enduro bikes' material 'aluminium' weight 15.6 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\x94\\x1eJ\\xbc\\x1d\\xdaI\\xbb\\xa1\\x99\\xc0\\xbcc\\xf8\\xbf;\\n\\xd1\\x8d<\\x0b%V=g\\x1e%\\xbd\\x8b\\xb6\\x9e\\xbc?\\xff\\xa8=\\xac\\xcb9<\\x83\\t\\xfe<\\xa9\\x105=\\x83\\x04~=\\xae\\xec\\x8f<\\xf5\\x1b\\x98=\\xe1\\x10\\x82\\xbd{~\\x86=o\\xb7=\\xbd0\\xa1\\xd0\\xbb\\x04\\x93\\x12\\xbd\\x84\\xb4\\r<\\xdcs2\\xbd\\x84\\xba\\xb7<\\x88\\x12\\xab\\xbd}\\x87|\\xbd\\x1d95\\xbcn]\\xa7<e\\xdf\\x97:\\x96t\\x87\\xbcu\\xd3\\xab=j\\xc6\\x9f\\xba\\xd8<\\xb7\\xbck\\xb3F<\\xb0\\x0e\\x82\\xbaQ\\xa6\\xc7:\\x97\\x9e\\x01\\xbdFz\\xa7<\\x10\\x9e\\xfd\\xbc\\xf6/\\x9b\\xbb\\x81\\x88\\x06=\\x9b!y=\\x8a\\x12\\x1a=H\\xe0\\xbf<\\x1d\\x01k<U\\x01E\\xbb\\x0fH\\xda:\\xef\\xd5N=\\xf60\\xaf<{\\x86\\x1c\\xbd\\x8b7[;D\\x14w\\xbc%\\x0b\\x00\\xbd\\xa8\\n\\xda\\xbc\\xc1\\\"\\x02=\\xdd}\\t=\\xd5W-\\xbc\\xac\\xf5\\xb0;\\x83{\\x88\\xbd\\x19\\xabC\\xbd\\x9d\\xbf\\xa5=\\xd0\\x8e\\xc4<\\xddq\\x88=\\xe2\\xd2\\x0f\\xbb\\x01U}=\\x10\\x02\\xc3<\\xef\\xef\\xcf<f\\xa3\\xe9<\\xe7\\xe3I\\xbcB\\r{<\\xec\\x85`\\xbdN>\\x97\\xbc\\xaf~\\xa4\\xb9\\x9a\\x0b\\x1a\\xbd\\x06\\x9c\\xd9\\xbcX#\\\"\\xbdD\\x07\\x02=:;0\\xbc\\xa7v\\xaa\\xbc\\xafE\\x97\\xbd\\xf8\\x14W<\\xcf\\xdd#<\\xee\\xab\\x9a\\xbd\\xbaw\\x17\\xbcE\\x08 \\xbdp\\x13\\x9d\\xbbG,(\\xbd\\xe6\\xdad;\\x19\\x9f\\xa8<%*\\x84=\\xe2(\\r\\xbc\\x16\\xf4\\xcb\\xbc\\xe5\\x06,<\\x89\\xbc^\\xbc\\x96|\\xb0\\xba\\x1b\\x1a\\xa7<b\\x1b\\x15<\\xbd![\\xbd#\\xd0\\x04\\xbdm\\t\\x0f=n\\x12\\x8c\\xbd\\xb2\\xf6\\x8a<\\xafFh<\\x93D\\xdf<0\\xb9\\x88\\xbd\\x13\\r$\\xbc\\x0c:\\x9d=8\\xddS\\xbc=:\\xa9<\\xcc\\x05u\\xbc:\\x03i<qQ\\x88;\\x99\\xf9\\x97=\\xd4\\xed\\xd9<\\xc3I\\xbd\\xbc\\x8f\\xafg\\xbd6\\x81\\x9d\\xbb,\\x98B:\\x9d\\x01\\x97;]\\x9d^=)o[<\\xe4\\xe0\\xd5<\\xac\\xea\\xd2;\\x19\\xefC\\xbd\\xd3\\x13\\x81\\xbc\\xe2\\xd1\\x9c;\\x998\\xef\\xbb\\xe6<3<S\\x89\\x85\\xbdu[\\xd4\\xbc\\xea~5\\xbd\\xda\\x04\\xf4\\xbc\\xe7\\x98\\x06\\xbd,:\\x15;w\\x7f\\xef<\\xbc\\x9c\\xa0\\xbc\\x19\\x90\\xb1<\\x8d\\xdd<<\\xcbV\\xa7:?\\xf9\\xc0<`EZ\\xbd\\x99n\\r\\xbde\\xc8s\\xbd\\xfeH\\xae<8\\xf0\\xaa=B\\xac\\x00\\xbdB \\xbd;\\xa0A\\x8e;q\\x95\\x10\\xbd\\x8c\\\\\\xff\\xbc\\xfa\\x0c\\xe4;;\\xce\\x0f\\xbcr\\xc3\\xfb<q\\xad+\\xbd&z\\xb1<\\xdd\\x80\\xc1\\xbc\\x0c\\xee-\\xbd\\xb9\\x90\\xa5\\xbb<h\\xc0\\xbb\\x9fg\\x03=J\\xd2\\x16\\xbd\\xbc\\x8aV=\\xf7O\\xf3<\\xa6\\x92!\\xbc\\xa3\\xab;\\xbd\\xd1\\x96t<\\xbef\\xae<\\x1b\\xfaL<\\xf6\\x8b\\xa8<\\x96!e=d\\x9d,<\\xfbO\\x91\\xbc\\x9f\\xb1\\xf2<>W\\x92\\xbc\\xf8\\x99M\\xbc4\\xfb\\xe7\\xbc_\\x02.<(\\xb9&\\xbdJ\\x97\\x8b=\\xc6Il\\xbc!x\\xfb\\xbc\\\"\\xc9\\xe8<\\xabrw<\\xf4Ls<\\xf0\\xc8#=*Tx=M\\xdc\\xce\\xbc\\xf3\\xca\\n=\\xc2\\xfc\\xa5<\\x96d\\x81<\\x86\\xd9\\\"\\xbd\\xebh\\xc8\\xbc\\xc8\\\\\\x0f=\\xc5\\x90@\\xbd\\x07\\x1fB<\\x91\\xc2\\xa9\\xba\\xb75\\xba;\\xc2*\\xc7\\xbc\\x95\\x19\\\"\\xbd\\xcf\\x0b`<U8R=\\n\\xbc5\\xbd\\x1a\\xb6\\xaf\\xbdj\\x0e}\\xbda_\\x14\\xbdd\\xc3\\x02\\xbd\\x9e\\xcc\\x9b\\xbb\\x10]\\x92\\xbc\\xbf\\xc5\\xa4\\xbcF\\x82\\n=7\\x91u\\xbd0\\xe1\\x91\\xbcgz\\xd2\\xbc\\xb0\\xe3\\xd4<\\xcb\\xfa\\xf8\\xbb\\x89f\\xaa\\xbc \\xe2\\xb0\\xbc\\x1aRw\\xbd\\tH\\xed\\xbc\\xc7\\xe0J=\\xf0\\xf8|\\xbd9\\xeb\\x02=\\t)\\x82\\xbc`\\xe7T\\xbd\\xd1\\xfa;<\\x91\\xb3\\xde\\xbb\\xc8\\x19\\xcb\\xbc\\xd3\\xfa\\x04<E\\xd7\\x8f<\\x8e\\xa5S=Y\\xa02\\xbd\\xf3\\x80/\\xbdQ\\xcet\\xbc\\x01\\xf9\\x1c=\\xcfD\\x0e=5\\xa90<\\x93]\\xb6\\xbc\\x06\\x01\\x94:\\x05\\xa64=\\x8a\\x07\\xa2\\xbc\\\"1\\x1f\\xbb=@\\x0c=\\x15;\\x1c<?]\\x8e=\\xa2`\\x99<J\\xd1[<\\xcd\\x92\\x81<\\x05.\\x04=\\x9d[\\xb2;\\xb2\\xd5s\\xbdY\\xb0\\x90<\\x7f\\xb1\\xd5<\\xeb\\xd5D\\xbd\\xdd\\x955=%?`=\\xceBP;R\\xe7>=\\xdf\\xeeP\\xbc\\xba\\x83\\x03=\\xe8\\x01\\xe1\\xbb\\xabdn=#\\xe4\\xef\\xbb\\xc1\\x94\\xb8;\\xa5]J\\xbd\\xcc.\\xbf9\\x18c\\x0b\\xbd\\x8f\\xa6\\x06>E\\x171\\xbcr\\x0bz\\xbb\\xc0;\\xe3\\xbbio\\xc8\\xba\\x05q\\x9b<\\n-G\\xbb\\xdel\\xf4\\xbc\\xe7\\x91\\xae<\\xa0\\xc9\\x08\\xbc\\x9cAu\\xbc0\\xe0\\x11;\\x00\\xb0i\\xbc\\xf9\\tm\\xbbF\\xb0\\xc2\\xbd\\x90\\xfbd\\xbc\\xe3w\\xc9<\\x1a\\xe1\\xd0\\xbbD\\x0bC=\\xe1.f<\\xefY\\x9a\\xbc\\x18\\xe9\\xbd\\xbcBbp=C\\xfb\\x9d\\xbd\\x84\\\"\\xd8<0\\xee?\\xbb\\xc1\\x9e\\xad\\xbc\\x93\\xa3`=\\xb3D1<&\\x17\\xaa\\xbc\\xd7\\xa3,\\xbb\\xc0\\x07\\xdc<\\x8a\\xa5\\xd5<\\xd67\\xa8<\\x1a\\x80d\\xbc\\xa8\\x0b\\\\\\xbc\\x8d\\x00(\\xbc2\\x143=\\x1f\\x07\\x08\\xbdee/=\\x8at\\x00=\\xab\\x04\\xaf<\\xcblP<63v=\\x15\\x9c><s\\xc0)\\xbd\\x8bVL\\xbd\\xe8\\xe4\\xee\\xbcz\\xd06\\xbd\\xb3\\xe9\\xb1<[%\\x1c\\xbd1\\xd8\\xd1\\xbc)tP=\\xaet\\x16=\\x80\\\\\\x80\\xbcf\\x0c\\xad\\xbc\\x07\\xf8\\xd1\\xbc\\xd1\\xcdH<\\xe5\\xdb\\xd8\\xbbL\\xf5p<WE\\xca\\xbcHzW<\\xb4X\\xa0\\xbd\\xfc=\\x9b<\\xa5A2\\xbbK\\xf5>\\xbc\\xe9<\\x89\\xbdli\\xd3\\xbcmY\\xae<\\rl\\x83<\\xb1\\xcf\\x9a\\xbbjn\\x93\\xbd\\t\\xe7H=\\xe2Cr=\\xf2Ai\\xbc:\\x1a\\xc5\\xbc\\xb7\\xc07\\xbd\\xeb\\xa2\\x92=Iij;\\xa2\\x8fp< \\x95\\x03=\\xbd\\x14\\xa8=\\xb5\\x12\\x92<\\x0e\\xb4\\x9f=\\x81\\xbe\\x1b=\\xbaJ\\xdc<(\\x81\\xb0\\xbc;\\x7f\\x9b=\\xc3\\x8bs\\xbb\\x03\\x81\\xa6\\xbdp\\xc9\\x03\\xbc\\xc1\\xdf\\n\\xbcx\\x7f8\\xbc{\\xc6\\xdd\\xbcb1\\xc8\\xbd\\xdd&\\xe0\\xbc\\x0c\\xea\\x8f<;]\\xc8<\\xe5K\\x1d=\\xf5\\xdc\\x83<E\\xee\\x0b=\\x83\\xa9<\\xbd\\x81\\x0bl\\xbc}\\r\\x0c\\xbd@\\xe7\\xb6\\xbczD)=7Z\\x99\\xbc\\t\\xfc\\x9a;\\x8bi\\xe9\\xbb\\\"\\xe6\\xa0<\\xa9Y\\xb1<\\x9cU\\x93\\xbc\\xa8en\\xbd\\x96/*\\xbc\\x11\\x04\\x18\\xbd\\x8b\\x85f\\xbd\\xea\\xcc\\xa8\\xbc\\xfc\\xcf)\\xbd\\xfc0!\\xbcn\\xf0\\xce\\xbd\\xf6\\xa2B=a\\xba\\xea\\xbc]\\xf3\\xec\\xbb\\x8f}@\\xbc\\xd4\\xff\\xac\\xba\\xe3\\x96\\xa1\\xbd2\\xcd\\xb5=4\\xdc\\xcb=\\x81\\xc3(\\xbdE\\x14\\x13<\\n\\xa1\\x85<\\xff6!\\xbd\\x17\\r\\x0c\\xbc\\xc9\\xe6\\x8c=8\\xf8\\x84\\xbd%\\xaf\\x99\\xbb\\xcc\\xbcA\\xbd\\x8c\\x15%=I\\x92.;SZG=\\xcf\\xf6\\x0e\\xbc\\xe5o\\t=m~\\xba\\xbc\\xcey\\x94;\\xe0;==9ef=\\xb2p\\xee;\\xe3\\x04e<\\xe9\\x1e\\xab<Yr\\xdd<\\xae\\x82O<\\x7f\\xa1\\xb3<\\x8bl\\xa9\\xbd\\xdc\\x16\\xac\\xbb\\xc7\\xe2\\xfc<\\xb5C%<\\xc7\\xbe\\x0f\\xbd\\xff)Y=_QT=\\xcf\\xa5\\x0c<\\xef<\\x0f\\xbd\\xb4\\xd3P\\xbc\\x1ctm=\\x91u\\x14\\xbb\\xe8\\xe8\\xc3\\xbc.\\x8e\\x1b:Z\\xba#\\xbb\\xa62=\\xbd\\x19{~\\xbd=\\x0c\\xdf\\xbc]\\xd9\\x89=\\x1b\\x11Z=U\\xbb5<\\xcd\\xa7\\x85;\\xb1\\xbe\\x05<c#\\x1d\\xbd[\\xd4\\xba\\xbc:bQ:\\xe8\\xd2\\xc1\\xbc\\x9a\\xc0\\x8d<\\xcd\\x92}<\\xd4*\\x8b9p\\xeai9\\xe0\\x96\\x82\\xbdl\\xd26=wX\\x8f\\xbd\\x8f\\xf4\\xfe\\xbcF\\x89.<\\x07=\\x94\\xbdi\\xe0\\xcf<\\x0e?2\\xbc\\xdc\\xdd\\xa79\\xf6w\\x0b<\\xd9\\xd0z\\xbc4kG=\\x8f\\xd1k=\\xd7J\\x85<\\xcbt\\xef;\\xe1\\x9f\\x1e\\xbd#\\xb6Z<\\xdek\\xec;P\\xf4B\\xbcf\\xc0\\x0b\\xbd\\xef_S\\xbc\\x96Ay=\\x93B\\x90\\xbc\\x90\\xff\\xb8<\\xdf\\x15I\\xbc\\x91\\xd8\\xd4</\\xd4r<\\xd9\\xac\\xab<G\\x7f \\xbd\\xed.\\n\\xbb/\\x1a\\\\;\\n\\x12\\x0f<\\x80\\xe7\\xd5:\\x950\\xa2<\\xad\\xdd[\\xbc\\xa8\\xe2_<\\xe5\\xca\\xee\\xbc^\\xe5j\\xbdm\\xeb\\x8d<\\x12\\x08-<\\xd1\\x85e\\xbc\\xd5\\xb2\\x9c;\\xa6|\\x89\\xbd\\x0c]\\xa9<m\\x18C=\\xe80\\x13\\xbc\\xe9\\xe8g\\t\\x00%\\xc3\\xbb\\xb3\\xa7\\xe6\\xbcV\\xd4q<\\xf6\\x8c\\x96\\xbbuBi<\\xaa\\xf5q=\\xd1a\\xc6\\xbb\\xdc3Y\\xbcpr\\xba<\\x15\\x11\\xc0\\xbc\\n\\x87\\xfb<?\\xb9\\x0b=\\xb9$)\\xb9f\\xacJ=N\\r\\x91\\xbc\\xae>\\x8d;\\xc2\\xd18\\xbd\\x91\\xf4k\\xbd\\xec\\xa4\\x17<c%\\x84=\\xb6\\x94\\xdb\\xba_\\x9f\\x00\\xbcr\\\\r<\\xc6\\xe1\\x18<qm!=\\xb4\\xb8E\\xbc\\x83\\xd1t<\\xcf:\\xbb<\\\"\\xa6 \\xbcjq\\x88<Z\\xe5\\xc2<\\xac-\\xb0<\\xa3\\xae\\x8c\\xbd\\xf9aA\\xbd\\x17\\x95\\xa8\\xbdd`\\x0e=_\\x0eb=w\\x8a\\xb5<\\x82\\xbb\\xf4<I\\x8f\\xd7\\xbb\\xc6A\\xa1\\xbc=lx\\xbc{\\xd7\\x90\\xbc\\xa4t\\x80<\\x1eym<\\xc7\\xebb<d\\xc2\\xb8=h\\xeb*\\xbd_\\x1e\\\"\\xbd\\xb8\\x94u<\\x12\\x9di\\xbd\\xe5 \\x17\\xbd\\x83<\\xa9<\\x1a\\xac/=\\x89@?;r\\xdf&\\xbc:\\x05a\\xbc\\x89LK;_\\xa2\\x1e=\\xd6\\x97\\xa6;\\x81\\xbd\\x08=\\x18\\xb6\\xa5\\xbcM\\xb1\\x0e=\\xc1\\x0c\\x07=\\x84\\xbd\\xc0\\xbcZ\\x9f(\\xbd\\xf9\\xe0\\x8d\\xbb\\xe4L\\x07\\xbc\\xc7\\x96\\x18<c\\xb9\\xbf<8&\\x9b\\xbb!\\xdf\\x14\\xbdb\\x1e\\x98\\xbc\\xdf\\x8b\\xde<1\\xb9\\x0f=QJU\\xbd\\xa5\\x1f\\xe1\\xbc/\\xc4==H\\xaaa\\xbd\\x05\\x85\\x95=\\xd0S\\x85\\xbdv0\\xde;\\xc0\\\"\\xc0:\\xf2\\xf7O<~J=\\xbdC\\xae\\x06\\xbd_7O=\\xac\\xdd\\x85=\\xb3\\xf3\\x9a<]/M<\\xae8\\xf0;\\x946\\xd2\\xba\\xe8\\xbf\\xae=^\\xa1]:YN2=\\xd2\\xf6.=u\\x11/\\xbc4\\xd7\\x1b\\xbd\\xc1\\x08`\\xbcK\\x98l;7\\x1f~\\xbc\\xa2\\xca\\xe3\\xba\\x8b\\x08\\xcc<H\\xcaX\\xbb\\x9c\\x02\\x14=\\x18\\x17\\x11\\xbd1\\xe8e<\\xcc\\xe3\\xb1;\\xad1\\xa9\\xbc\\x8e\\x15z\\xbd+\\xc9I=\\xe9\\x83\\x16\\xbd]\\xb9\\x9a<W\\xfb\\xe2<k\\x82\\xac=O4\\x16<\\x8f\\x88;<\\x05\\x88\\xc6\\xbc\\xc7i\\xa0<\\xa4\\xabc\\xbd\\x1c\\x8b\\xab<\\x00_\\x0e\\xbb,\\x0eH=GC\\xf2;\\xcd\\xea\\x1e=\\x06\\xa8r\\xbc\\x1d\\x9dG\\xbd\\xd6\\xd6G<\\r\\xdfX=O\\x04>\\xbd\\x02\\xcb8\\xbc\\xe2bf\\xbd\\x17\\x08\\x12=\\x17#`;3\\xdd\\x9d=XF\\x89<>\\x14\\\\=\\xbb\\\"@\\xbdXH\\xf5;\\xa4O\\xc3<\\\\\\xfbM=\\xf9K6\\xbd\\xb9\\xcb\\x1f\\xbd\\xccdK\\xbbHi\\xd4<;\\x04\\xea9\\x1e\\x93\\x06\\xba\\\"\\xba/\\xbc\\xa3\\xce\\xe3<\\xc1r[\\xbc~\\xb2\\x7f\\xbc\\x18\\xd3\\xb8<\\x9d\\xab`\\xbc\\xa0y\\x14=\\xc7+\\xae<\\xef|@=\\xff\\xa9\\x9f\\xbd2\\x08 8\\\"\\x11W;mI6<\\xd8\\x19\\x1f<\\x94\\xf4\\xae<8\\x03,;r\\x8dm\\xbcu\\xact\\xbc%\\xfc\\xed\\xbb0\\xdb\\x0b\\xbd4o\\xf3\\xbbw/h=\\xa2\\x08\\xd1\\xbc\\x96\\xcc\\x93;mZ\\xdc\\xbc\\\\!?\\xbb\\xd6\\x18s;\\xfa\\xe8\\xad<\\xd4.\\x98=\\x1e\\xae\\xb2<\\x844\\x9d:\\xe55\\x8a;\\x1a\\xfb\\x15\\xbd\\x8a\\xd4\\x0f\\xbcX\\xa5Y\\xbd\\x021\\x18=WX\\n\\xbd;\\xb4!\\xbd\\xe4r\\xb6\\xbb\\xc7\\x1a\\x7f\\xbc\\xf4C:\\xbd\\xed\\x9d\\xa0<\\xbd\\xcdH\\xbc\\xaa\\x15\\x84\\xbc\\xf7$L=V\\xc32\\xbdos\\xcb\\xbc>\\xfc\\xac\\xbd\\x15\\x8eX\\xbd}\\xe1\\xfd\\xbc\\x1b\\xc2\\xba\\xbcsH\\xf9\\xbb9\\x8c\\xb6<\\x1f\\xd5&\\xbd\\x0b<\\x83\\xbc\\xfd\\xf5\\xd0\\xbc1l\\x95\\xbd\\xda\\x7f\\xef\\xbcU\\xa0\\x1f<\\x14y?<r\\x05W\\xbd0\\x12\\x9e;Z>\\xa0\\xbd\\x7f\\x96\\xc1;#\\xd9\\x8a\\xbb\\xc6\\xfb\\xa3<\\x88\\x80\\xbc<\\x9d[\\n\\xbd\\xc1\\x01\\xd8\\xbc\\xd8\\x8eK<\\xcdM\\xa2\\xbc\\xe4T\\x90\\xbd^4\\x9c\\xbd\\xcf\\x14P\\xbdd\\x18M;\\xd5\\xa8{=,/\\x00:T\\x92\\xb3=\\xe1\\x13\\xe2\\xbbm\\xdd\\xb3<\\xb8\\x1a\\x82\\xbd>o\\x8d\\xbd\\xca\\xa8)=\\xdfD\\xf7\\xbb[o\\r;1\\x9dx\\xbc\\x08\\x1c\\xb6\\xbc\\x036f;\\x8d:\\x93\\xbd8=\\xc7\\xbc\\x9b\\xaf\\xfe<\\x1e<:=N\\x03\\x8b\\xbc\\t \\x83\\xbbZ\\x89e=\\xf7B?<\\x1c^\\xcc<\\x8b\\x9do<\\x1bE4\\xbaf\\x0b\\r\\xbdJp\\xa5\\xbbT\\x12\\x88\\xbd\\xea\\xa6\\xe6\\xb8\\xf2\\xb5\\xdd<\\x83\\x7f\\x11\\xbc\\x7f\\x05V<M\\\\\\xe2<6L\\x02\\xbdk\\x96\\xb8\\xbb\\xdc4\\n\\xbdI;\\xbd<\\xe2m\\t\\xbdM\\xc5\\xd4\\xbd\\\\\\x12\\x1a=\\xb6\\xd8\\n=\\xf7\\xd3\\xdb<\\xe4\\xb8\\x1d= \\xf2\\xbc=\\xac>\\xb0\\xbc\\xdb\\xb52=\\xe5\\xc3m=\\xf7\\x9c2\\xbc\\xce\\xb4\\x10=\\x06rG\\xbd>c\\x0f=\\xcd\\xc5\\xf9\\xbc\"\nHSET bikes:10002  model 'Tethys' brand '7th Generation' price 2961 type 'Road bikes' material 'alloy' weight 7.4 description 'The bike has a lightweight form factor, making it easier for seniors to use. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"j\\xa1\\xa5\\xbcM\\x18\\xa1<\\xfc\\x7f\\x9a\\xbcV\\xb6\\xcd<n\\xad\\x85\\xbc\\x04\\xf6?=!Md\\xbc[\\xb6\\xad\\xbcf\\xa3c=\\xc6\\x8cD=\\xb6R\\xa2<\\xff~\\xcc<\\x1bz\\x06=X\\xefm\\xba{!\\xc0=~jD\\xbc\\x00_)=\\x077\\x9d\\xbc\\xdd\\x12(=\\x0b\\xb5\\x84\\xbd\\xce\\x86\\x9b<r\\x8dK\\xbd~\\xe3y=-@k\\xbd\\x0f\\xcb2\\xbdmzW\\xbc\\x82\\x9e8=\\\\z\\xc8\\xbalz\\xb8\\xbc\\x0c\\x10\\xf1=\\x1aX\\xc2\\xbc\\xab,-\\xbd\\xcf$\\xd7<\\xd8\\xfb\\x13<@\\xab\\xfc\\xbc\\xb3\\xbf\\xb6<R{\\x84<\\x16j\\x9a\\xbc\\x9f\\x9a\\xfc\\xbcu\\xb2k;\\x10\\n`=\\x8f\\xd3)<\\xda-_=\\xba\\x8bX<N\\xc9;\\xbc`\\x93\\xde\\xbcX\\xeb\\x05>3\\xe90\\xbd\\xea\\x7f\\x97\\xbc\\xc9\\xcdm<e\\x03\\x14\\xbd\\xe7\\xd4\\x07\\xbdV\\xe0H\\xbc\\xf8\\x10E<\\x8e\\x1e\\t\\xbd\\x0f\\x91\\xaa\\xbc\\xad\\xc7z\\xbb\\xcex\\x91:\\xea.\\xcb\\xbcIf\\xb2=\\xcc\\x92\\x08=\\x03\\x7f\\xe0;\\xf8]\\x84<\\xd1\\xb3\\x87<$\\\"\\x03=f\\xa0y\\xbbo\\r\\xcf\\xbb\\xd2\\xa8\\xda<-O\\xf4<\\xefG\\xa4\\xbcA\\xfa\\x0e<;\\xa1\\xe3<.\\x1d\\xb9\\xbc\\t\\xc2\\xf3<\\x97\\xd1\\x17\\xbdI}\\xea\\xbb\\xdd\\x82\\x89<\\x9c\\xd4\\x18\\xbd*=\\x81\\xbd\\xb0n\\xcd\\xbbP\\xa3A=E]]\\xbd\\x827\\xe7;~Hh\\xbdY?\\x83<\\xad8\\x96\\xbb\\xfb\\xfe\\x83<d\\xaf\\x1b<D%\\x1b=\\xf2\\xcb\\x1f:+\\x8d\\xed\\xbc\\xd7\\xaad\\xbd\\xa8E\\x82\\xbdp=K<\\xbe{@=,\\xb6\\x84<,\\xad\\xf0\\xbcZ\\xac\\\"\\xbdnI+=\\xb6\\x0c8\\xbd|t\\x0b9\\xa3Z&<p\\xe92=\\xbfc,\\xbc\\x11\\xa01\\xbb\\xc7\\x81\\xcc=\\x9e\\xdc$;\\xf9\\xb3\\xf1:\\x9cl\\x12\\xbc3N\\xbf\\xbc\\x863Z\\xbc\\xb5\\x18\\x04=U\\xf2G<\\x82\\xb7\\x83\\xbc\\xd1\\x1f.\\xbdP*b\\xbd\\xd4\\xd3\\xd5;\\x91\\xf7\\xe6<^8\\x1e=\\\"\\xfaG\\xbd\\x01\\x9bY\\xbb\\xb1\\x1e\\x1d9\\xab\\x8d\\x14\\xbdzf\\xce\\xbc6\\x8d\\xb5<\\x97d\\x83\\xbc\\xe4\\xce\\xfa\\xbc,v\\x95\\xbd4D\\xee\\xbc\\x81pV\\xbd\\xd4^\\x9a\\xbcL&\\x1d\\xbc\\xe1\\xed\\x13<\\xa0\\x1e.\\xbd\\x86\\x10\\xd5:\\x98\\xfd.=\\x8d\\xa1\\xa3\\xbc\\x1a\\xf1\\x1b\\xbcTM\\xeb\\xba\\x9b\\xe4\\xba\\xbc(\\xeav\\xbc\\xf7\\xae.\\xbdW~+< h\\xe5=P/\\x81<s\\x13\\xd0\\xbc\\\"\\x0fy<C\\x8c\\xf9;K\\x07\\x86\\xbc\\xd7\\xbc\\xb6<\\x8dT\\xa5;\\x1ajo=\\xb7w\\x0f\\xbd\\xd4w\\x02=\\x18\\xd3\\x90\\xbd;\\x83\\xf5:,?P<>+\\x0c\\xbd\\xd7\\x10\\x9f<g\\xc2\\xb8\\xbd\\x9a}\\x07=!\\xb8\\x9f<FJ\\\"\\xbcK-J\\xbd\\xd2[\\xd2;\\xb4Y-=\\x87\\x9e\\x99\\xbc\\xa7\\xdc\\x92<kS_=\\xc8\\x1f\\xc0\\xbc\\x1fD\\x1c\\xbc8E\\x02=\\x85j-<\\xbb\\x1a\\xfb\\xbcY\\x17\\x99\\xbc\\x89)\\xee\\xbc\\xf6&5\\xbc>-?=\\xda\\xd5\\xad\\xbcE\\tB<\\x9cR\\x13\\xbdo\\x8e\\x0b<e`w;TC\\xa1\\xbcV\\xdd\\\\=\\xef\\x1c\\x18\\xbc\\x96\\x16\\x1d=nB5\\xbd\\xc0\\xea\\x02<\\x1eGR\\xbb)\\x9e\\x0e=\\x0c\\xc6a=\\xbfZ\\x89\\xbc:c\\xdb<\\x94\\x84)\\xbc/w\\xcd<\\xd7n}\\xbd\\xa4\\x0f*\\xbc\\xf5nu\\xbdy\\xa6\\t=\\xee\\x87\\xcf\\xbc%C\\x9c\\xbd\\xca\\xb4\\xf8;w@\\x1f<\\xbeH\\x17;=\\xb3U=\\x83\\x96H=\\x94tK\\xbd\\x9cW\\x88;\\xdc\\xb4\\xd4\\xbc\\xa2y\\xb3\\xbc\\x0b\\x0f\\x1d\\xbd$c\\x99=O\\x05\\x0e\\xbc\\x15\\x04J\\xb9\\x97\\x1a\\x8a\\xba*\\x06\\xa5\\xbc\\xb2\\xf5\\x19\\xbd\\x80\\xbc\\xbb;\\xec\\xa9\\xc4\\xbd!\\xe8\\xaf\\xbb\\xb6\\xcf\\x1a:\\xaf\\xbd\\xbc\\xbc\\x07\\xa9\\x9c\\xb9\\xa3\\x908\\xbd\\xec\\t>\\xbc\\x1e\\x86\\xb5<\\xd98e\\xbd\\xe9\\xed]=lZ\\xc7\\xbc\\x0c\\x8d\\xd5\\xbc\\x1c\\\"\\x90\\xbd~/\\xbf\\xbc\\xe0U\\x87=\\xa8\\x9d\\xb0\\xbb\\x08`B=F\\x19\\x02<\\xbcYD=\\x00Oe\\xb9\\x10^\\xe9<\\xbec\\x08\\xbd\\x89U*\\xbcr\\xd4\\x01=\\xb1s\\xde\\xbbB@\\xad9T5\\xa8=\\xafz_\\xbc\\xaem\\xd8\\xbd\\xady\\x96\\xbdm\\xe2\\xa9<\\xa51\\xaf\\xbc\\x06\\x9a\\x93\\xbc\\xfe5m\\xbc;\\xdf\\xce;\\xa8\\xdd\\xc7\\xbd\\xdc\\x06\\x8f=\\x9d\\xf2(\\xbd\\xbdf\\xfd\\xb9Y\\x04\\x13\\xbd\\xea\\xbcx=,\\\\\\xe59\\x1d\\x80\\x13\\xbc~|\\xf3\\xbc\\xe3\\xc1\\xf1\\xbc\\xb9>\\x97\\xbcLL\\xa9=\\xc6H\\x00\\xbdq\\xd4\\xe3\\xbcR~\\xb2<\\xa0Q[<\\xb3\\xff\\xe2;\\xb1OP\\xbc,9^<H\\xed\\xaa<\\xd1U\\xdb\\xbctC\\x93\\xbbs\\xe8\\xe2\\xbc\\xd2U\\x81<0\\x9f\\xa0=\\xcb;\\xb4\\xbd\\xb2\\xa4\\x18\\xbdx\\xa3E\\xbc3Gz\\xbd~\\xb5\\xd2:\\xf3S\\x1d=\\xbf\\x88+\\xbb\\xd1\\x94K=C]\\xc1<Hc\\x8e\\xbd\\r\\n\\xc4<\\xe9\\xef\\xac;?\\x86\\x01\\xbd\\xa8q\\xe3<;\\xe8\\x9e;q\\xc63<\\xec\\xc9\\x15\\xbd\\xc9\\x80\\x99<\\x0b\\xd8\\xb5<\\xa5p\\xbf;<fN\\xbd\\xa2\\x9b\\xbd<\\x8a:\\x8f\\xbb\\xa6\\xe3\\xf8<\\x1f\\xdf\\t\\xbd3`\\x86\\xbc\\xd0)\\x82<\\xd0\\xd4\\xaa;\\xd5&6\\xbc\\xc5\\xf4\\xba<FyS\\xbd1\\xb8\\xa8<\\xa4\\x1c\\x92\\xbd\\xc6\\x10\\xff\\xba0\\xcd\\x84\\xbc\\x17\\xcf\\xb2<\\xba\\xa8\\xd3\\xbc\\x89 B\\xbd<\\x9b\\x9f=h\\x9fh=#\\x96\\x7f\\xbc\\x15\\xe1\\x06\\xbd\\x8bt\\xb5;\\xac\\xd8\\x0b\\xbc\\xd7I\\x91<h\\xbfM=\\x8aQ\\x1b\\xbd\\xe4h\\xd2<p$U<\\xd9a\\xe4<wa\\x12\\xbc\\xf4\\rO=\\xc7nw\\xbd\\xcewF\\xbc\\xe9U$<\\xa8P)\\xbcH\\x1dy:\\xbf\\x96\\x08\\xbd&\\xcc\\xe8<\\x0e#O;\\xfb\\x1b\\x83<\\xcco\\xae:<\\x1d\\x8c;I<\\xbc<\\xdaY\\xbd\\xbc\\xb7\\xa4\\xd0<vf\\x0b=\\x1cW\\x95=XOR=\\xd9\\xd1K=I\\x94\\x01=Y\\x97\\x10\\xbc\\xd8*\\x86\\xbb\\x1a\\xea\\x98=\\xc1\\xa2\\xa5\\xbc\\x15\\x93\\xe2\\xbc\\xe6]D<D1?<\\xdfP\\xbd\\xbd\\\"h\\x869e\\x92\\xc2\\xbdc\\xc0\\xa5\\xbcm\\xc3\\x08=0\\x0cU<\\xea\\xc2a=\\xec\\xcf==\\x04\\xf1y<=\\x16\\x13\\xbc\\xe7~\\xb3\\xba\\x95YI\\xbd\\xce\\xd9\\x89\\xbc\\xaa[]=\\x99\\x9d\\x89<\\x86F4<\\xb4\\xaf.\\xbc.\\xd3\\xa0<\\x7f\\xc58=\\x99;&\\xbd\\x03\\x16\\xbf\\xbc#\\xb8M\\xbc\\xad\\x1cd=\\xbd\\x8e\\xe9\\xbc\\xac@\\x8b\\xbd\\xcd\\xa9\\x1b\\xbd\\xc1\\x16\\xac;k\\xfc\\xce\\xbcPaZ<z6<<\\xba\\xbb\\xe9\\xbc\\xf4\\x7fP=v\\x9d\\x89<b\\x0c!\\xbd\\xc6}\\xcb=\\xf4\\\\\\x8b=\\x1f\\n\\xc7\\xbc\\xd9\\x08C\\xbckc\\xcc\\xbc\\x0c\\x9d\\x90\\xbd\\x1aj\\xaa<\\x88;e:%O\\x07\\xbd_\\xc6|\\xbc^\\x07a\\xbdD\\n\\x00=\\xb4\\xecl<\\xc5u\\x92\\xbc\\xf2+\\xc8\\xbc\\xd4\\xca\\\"=\\xc0\\xfd\\xa1\\xbc\\xcb\\x8a\\\"=\\xe5_h\\xbc\\xa2|\\x1d=\\x1aD\\x1e<\\x1a\\x00\\x15\\xbc\\xec\\xfa\\xc3<\\xa1\\xa35<\\xce\\xa8\\\"=h9o:\\x10\\xe7\\xf8\\xba\\x10$\\x8d\\xbc\\xc5\\x9f\\xc5\\xbc\\xdc8\\x01<\\n^\\x06\\xbc\\xd6\\xf7\\x19<\\xda\\xfe&=\\x8e\\xb4\\xf9;\\\\[6\\xbd\\xdd\\xcf4\\xbb\\xce!0=5Ew\\xbc^G\\x89\\xbc\\x17\\xb1\\x1d\\xbc\\xdb\\x16\\xc8\\xbc\\x17C\\\\\\xb9\\xde\\x0e\\x83\\xbdu\\xeb\\x0c\\xbd3\\xae\\x9e=\\xefY`=\\xc4\\x89s=\\xd4n$\\xbb\\x8a\\xa2\\x82;\\xbe\\xe4\\x0c\\xba~vq<?\\xcc\\xa5\\xbc&\\x7f\\xaf\\xbc\\x98\\x13\\x8f<\\x93\\xf6P\\xbcxt\\x92=\\x8d\\xfdv\\xbc\\xc5\\x8b\\x14\\xbd\\xed\\xe1\\x85=2=W\\xbd\\xc9\\xf0r\\xbd\\x1fw2\\xbdt\\x12\\x12\\xbdL\\t\\xa8<\\x93S\\x89;\\x15\\\"\\xb9<\\x1f\\xb2\\x01\\xbdq\\x92\\xbd\\xbct\\xf3\\xfe<\\x03z\\x9e=}l\\x02\\xbcv]\\x86=\\xe5\\x1d\\x90\\xbcF\\x13\\x90\\xbc\\xb7\\xaf|=VJ\\xa8<?\\x81\\xb2\\xbc\\xe4#\\xcc\\xbbt\\xb8e=\\x98&s\\xbc\\xd8\\xba\\xbe<\\x11Bu<\\x0fK\\xc0\\xba\\x7fS\\xdc\\xbb\\x05n\\xe4<\\x9bs_\\xbd\\nJ\\x8c\\xbc2b\\x10=0}\\x03\\xbd\\x8e+\\xbc<\\x12Is:Bs\\x06:\\x8a\\x8d#=\\xbe;\\xc8\\xbc\\xbe\\x80\\xde\\xbd\\xc0\\xbc\\xa3\\xbc\\xbc\\xbd\\xea\\xba\\xa0\\xa3\\xbb\\xbc\\x18\\xbd\\xc4<b1!\\xbd\\x0ejV\\xbc7\\x90!=\\x03\\xf16<\\\"\\x88b\\t?l\\x97<d\\xc5\\x0e\\xbcV!\\x10=\\x00C8\\xbb\\xf8B\\n\\xbb\\x0be\\xe1<\\xbe8\\x1a\\xb9\\xace\\xda\\xbc\\xe5U+=(\\x1dS\\xbc\\xd2\\xe9y<Z6\\x18<#\\xc1\\xdb\\xbb\\x8c\\xe6\\\"=\\xdd\\xf8\\xff\\xba\\xb3\\xc4+=\\x0c\\xcea\\xbd\\xdez\\xf7\\xbc\\x0b4>\\xbc+\\x1c+=~\\xbb\\xdf<3R\\r\\xbd\\xcc4\\xab<\\x99\\xa5\\x80<\\xa1,\\x96\\xbb\\xb49 <\\xff\\x9d\\x98\\xbc\\xe1=\\xdd<\\x84\\xa45\\xbd\\x96\\x1e/\\xbd\\xd8\\xca:;G\\xc3n<\\t\\xd5v<\\x81\\x9b\\x89\\xbd\\x84\\x1bK\\xbbq\\xe6\\xca<\\xfa_\\x17=\\xf5<\\xe6<k\\xb6\\x10=\\xe3I\\x10<\\xc7\\xa5\\x98\\xbb6\\xa7\\xe6;-\\xb0V=\\xce\\xaf?:p\\x8b\\xdd<^\\x1e\\x90\\xbc\\xd1A\\x04>\\xc4\\x85\\xfe;\\x1a\\xcd(;lH\\x1e\\xbc{\\xfd\\t\\xbd\\x89\\x0c\\x89\\xbdgw\\x1d=\\xee\\xd2^\\xbc\\x1c8\\x85\\xbb\\xecn4=l\\xc3&\\xbd\\x96\\xc2\\xbf:}uv;.O\\x00=?\\xa2\\x91<\\x1e\\xad\\n\\xbb\\x1f\\xa5\\xbe\\xbc\\xf9\\xc9\\x16=\\x83\\x14\\x16\\xbb\\x85\\x89\\x84\\xbd\\x13\\xadH\\xbc\\rL\\x97\\xbc,p\\x94<\\x93$\\x8b\\xbc\\xc4i\\xb8<\\xd5@\\x16\\xbd\\x1cC\\xdc<\\xb6\\xcf\\xdf<\\x1fMU<\\xe6\\xef\\xc0\\xb9Q\\x85\\xef<$\\x85t=\\x86\\x97\\x91\\xbc\\xcc\\x1d\\xfd<\\xe1]%\\xbd?\\x92t:\\x8f\\x1bJ\\xbd.\\x0cz=-\\xbd\\x7f\\xbdMZ&=\\x93?`=kTh=\\xbdW1=\\xed\\x9b\\xbd\\xbb\\x0b\\x81\\x80<\\xe5\\x11X<v\\x08R=\\xb9\\xd4\\xa1:N}4=\\x16\\xd1Y=Op\\xcf\\xbc\\xc0\\x9a\\x07\\xbb\\xb1\\xbeu<\\xbb\\x1d\\xb6<Ck\\xd7\\xbc\\xf5]\\xaa<\\xb2\\xae\\xfa:\\xf5\\x17G\\xbd3$)=\\xb1MF\\xbc\\xe9\\xc6\\xbd<+0\\xb1\\xbc\\x94P\\x96\\xbd\\xbc\\x92)<[\\x7fg=\\x10\\xc2=\\xbd~H\\xa8\\xbc\\xee\\xdcm=j=,=\\xf4\\x93f<Y\\x95z=\\xe6\\xa6\\xf8\\xbc\\xb8\\x7f1\\xbd;\\xbeR\\xbdH\\xab\\x96<\\\\\\xd3z<i<\\xad\\xba\\xa8Q/<\\xeeb\\x06=\\r\\xd6w;B\\\">\\xbd\\x0f5C=\\x04\\xe0&<\\x1fn\\x1b\\xbdU\\xf4\\x03\\xbd;L\\x93\\xbdD\\xa7Q=1\\x1dz\\xbc\\xa8\\xd5%= \\xafU\\xbc\\xff\\x1a\\\"=sND\\xbd\\x9dh.\\xbd\\x86\\tI\\xbd/\\x88\\xef;\\xe6\\x06\\xb2\\xbd\\xc8\\x1c\\xa0<L<\\x12;zQ\\xeb;\\x18\\xb6\\xdb<n4\\x13=q\\xa7v<F^r=A\\x93\\xc6<\\x9a\\x10\\xf5\\xbc]\\xdd\\x84\\xbc\\xe6\\xcbA\\xbd8\\xc4-=t\\xd1\\xae;%O%<\\x8d\\xb5\\xe7\\xbc\\xa1\\xd4\\x9e=m\\xa9Q<T\\x80<<\\xf7\\x8e\\x9a92\\x92#=FD\\x9e\\xbcQ\\x19x\\xbc\\xe7}\\xdd\\xbc\\xba\\x82%\\xbd\\xe9\\xfa,\\xbd\\x10VY\\xbc\\xf9\\xc0Z<\\x01S\\\"\\xbd\\x18|J<\\xf7\\x98\\x0b\\xbd\\xe2yX<\\n$\\xce\\xbc\\x96\\xe3A;\\xdb\\x11!=7\\xae\\xf0\\xbcpb\\x91;\\x0er6=\\xcf\\x87)\\xbd\\x01x\\xf1\\xbbsQ\\xfb\\xbd3\\xcd\\xd1<\\xac\\x1f\\xe7\\xbc\\x84\\xb0\\x07\\xbd\\x18\\xd9\\xe1\\xbc~]\\xe8<\\xcb\\xa8k\\xbc\\x19\\xd9\\xc0<\\x0c\\xe9\\x15<u\\xcd\\xf7\\xbcP\\xf6\\x1c=\\x92\\xf09\\xbdMS\\xbd\\xbcsa\\x8c\\xbd?\\xed\\xde\\xbc\\xcc\\xe1\\x80\\xbc\\xd6\\xf4^<\\xf7\\r\\xf1\\xbc\\xf1\\xd2H;FS?\\xbd_/B\\xbc\\x1d\\x1a\\xdd;\\x05\\xbf\\xac\\xbd\\x95\\xf4\\xd6\\xbbM!\\xa2\\xbb\\x91\\xfb\\xf9\\xbb/,\\xb4\\xbc\\xe2\\x9d-<sh%\\xbd.\\x05X<\\xd0\\x18V\\xbco\\xd9\\xb2<~n\\xa6\\xbc\\xb4\\x8e;\\xbd-\\xe3\\xc2;.Z\\x91<>n\\x91\\xbc\\xcb)U\\xbd\\xbc\\x85N\\xbd\\xdb\\xc8\\x9e<\\xb0e?\\xbd\\x9d\\xcc\\xce<\\xe4)4=\\xc0\\x15;=\\x02\\x8b\\xa2;\\xa8\\xa9N=Kj<\\xbdT\\xe2\\x8c\\xbd\\x19\\xff\\x19=\\x93\\xcc\\xfa:J7\\x97<^\\xab9\\xbd\\xd09k;P\\x11\\xe5<[\\xa0e\\xbc~Yu<1j,<\\x05]\\t9E\\xe5\\xc3\\xbc[p\\x0c:\\x9aSd<D\\x82y=j\\xab\\xb3<\\xec\\\"\\x0e\\xbdV\\xf7\\xea<\\xb8N\\x9c\\xbc\\x1fd\\x17=?\\x84\\xd1\\xbcW\\xa6-::\\xc6\\x90;f=\\xa7\\xbc\\xa7F6;\\x9b$q=?Q\\x9d\\xbc\\xd6\\xc2?\\xbd\\x81]A\\xbd\\xdd\\xd1\\xed\\xbc\\xdf\\xc7\\xb6<\\xcb\\xaeL\\xbd7C\\xfe\\xba\\x85\\xa5:=\\xab=\\xc1<T\\xa6`=\\x8b\\x90\\x94=\\x13\\xb3[<S\\\"8=\\xa3.\\xc8=\\xbf\\xd5>\\xbc\\xb0D\\\\\\xbc\\x12\\x8d0\\xbb\\xfe}Q<\\x7f\\x80\\n<\"\nHSET bikes:10003  model 'Enterprise' brand 'nHill' price 1244 type 'Kids bikes' material 'alloy' weight 13.0 description 'Easy, intuitive, and very lightweight, these bikes are carefully designed to make bike riding as natural as possible. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. It’s for the rider who wants both efficiency and capability.' description_embeddings \"q^\\xb1;`\\x88$=\\xe3\\x08\\xeb\\xbc6\\xc4\\x15<<\\x99T\\xbc\\x00\\xba\\x90<\\xbc\\x9ai<K\\xf0\\xa6\\xbc!\\x9c~=\\xaa\\xd1L= \\xabf<\\xa08(=]\\x90\\xa4<\\xc3CB\\xbb\\xfd+\\xd5<9[\\x1b\\xbd/s\\x83\\xbc\\x81m\\xe1\\xbc\\x85\\xda\\xdd<\\xba\\xa1\\x9d\\xbd\\xf6#\\xb1<\\xd2\\t\\x1b\\xbd\\xd4;y\\xbcO\\xadc\\xbcv\\xd7\\x12\\xbd\\x08\\xa9>=M\\xe3\\x05<\\x1a\\xa5\\x94\\xbc\\xf5\\xf29\\xbd\\x1c\\\"\\xf7=\\xe1\\\"\\xc2;\\xdb2\\xe1\\xbc\\x07\\x18\\x9c<\\xact\\x19\\xbb,\\r\\x03<\\x0f\\x04C\\xbc\\x08\\x9d\\xff:\\xb8\\xf3\\xac\\xbc1B\\xca\\xbc!\\xcf\\xb8:\\xb6\\x01\\xc9<\\xbc\\x83\\n\\xbdpP\\x06<\\xfe\\xeb?<D\\xf9\\xa8;1\\xa7\\x10\\xbd\\xda\\xc7f={\\xe0\\\"\\xbdx2\\xd9\\xbc)\\x10\\xe4<\\x95\\xb4t\\xba\\xda5\\x10\\xbd\\x1f4\\x7f\\xbd\\xefo\\xee;\\xaf\\\"V\\xbb;\\x13\\xc3;\\xf0\\x1a\\x1c\\xbc\\xc8\\xf9\\x82\\xbd\\xff\\xb8\\xfd\\xbb\\xcb\\xfb\\xc0=\\x80\\xaf\\xc4\\xbb\\xa6h\\t\\xbb\\x9d\\xd6H=9\\xfe\\x90=\\xbfi\\xc7<\\xd7\\xda@\\xbc\\xc2\\xd8\\x8a:8\\x99\\x8e;\\t\\xb2f<\\xb2\\xa3e<\\xf0k\\x07\\xbd\\x9a\\xc6\\xc9\\xbc6\\\\K;\\xb5\\xe6\\x1a\\xbdi\\xcb\\x81<H\\xd2\\x03=\\xcc\\xaf\\xb9<\\xa4Y7\\xbb\\x11f \\xbd\\x87\\xc6\\xa5<\\xfc\\x90 =*\\xa0\\x9a\\xbd\\x9c3+\\xbd|\\xe7\\xb7\\xbd\\x9c\\x0f0\\xbd\\x85\\x91\\x87<\\xb1Ci<\\\"RA=\\xcf-\\x0c=\\xbc\\xa72=\\xbcj\\xbb\\xbct$\\xea\\xbc\\xe3m\\x8f\\xbdw\\xa5w=\\x9b\\xf2N\\xbc\\x10\\xa5W=\\x9en\\x86\\xbc\\x12\\x87y\\xbd8\\xce\\xfa<\\xb8\\x8c\\x8d\\xbb\\xfd\\x10\\xdd<\\x19\\x16\\xc8<\\xd7K\\xf1<T\\xeek\\xbd\\x9c1\\xc4\\xbc\\x89\\x1f\\xc4=\\x0c\\xcb\\x84=E8\\xa6\\xbcU`Z\\xbc\\xf6\\x07\\x84=\\xa5\\x87\\r=\\xc7<d<\\xad\\xbc\\xec;\\x9a\\xcaF;\\xdf\\xe8\\x1a\\xbd#e\\x15\\xbd&s\\xe5\\xbcy\\xa2c<p+\\t<\\x03\\xad\\x16\\xbc\\xf9\\x93c<d\\x12\\x11\\xbb\\xea\\xc7\\xb5:\\xcd:\\xcf\\xbb\\x88\\xb1[=b<\\x8d\\xbc\\xf6\\x1e@\\xb9k\\x95\\xf0\\xbc\\x00q\\x19;\\x04\\x7f2\\xbd\\x0c\\xab0\\xbd\\x18`[\\xbcw\\xd1\\xd0<\\x91\\x8c\\x8b\\xbc\\x1c\\x1d\\x02=\\x9ap\\x97=K\\xa1#\\xbcR\\x9f\\x8d<\\xa6\\xa8\\x82\\xba\\x81a\\x9d\\xbdr\\x0cL\\xbd+\\x1e\\x10\\xbdF\\xc4\\x07=L\\x9a\\xbe=\\x1e11\\xbd}\\x02\\xc2\\xbb\\xd9\\xd2\\x1c\\xbd\\xf3\\x95\\\"\\xbd\\xd2\\x17\\x93\\xbc\\x13\\xf4\\x98\\xbc\\xa2\\xfdN\\xbc\\xc5cs=\\x81\\xe8/\\xbd!\\xa6\\x12=X\\xbb\\x9a\\xbd\\xedX\\x84<\\x10\\xa4m\\xbd\\xef\\x10\\xa9\\xbbBz\\x00\\xbc=\\xe4\\x81\\xbd\\xa1\\xa8\\x94=(a\\x05=6\\xe4\\x97\\xbb,\\x1e.\\xbdb\\xeb4=\\x1e8\\xb2\\xbb\\n\\x9c\\x96\\xbb\\x1a\\xa7\\xc2<,b\\x8f=\\xfe\\x9a\\xd0\\xbc\\xeb\\x08s\\xbd\\xbb\\xfe\\x96=\\t\\x0c\\x8f;\\x8a1\\x96\\xbc\\xe5/6\\xbc`\\xcb\\n\\xbb\\x9b\\x039\\xbd\\x8e\\xcd\\x11=sZ\\xf8<\\x80\\\"\\xf8\\xbb\\x04w0\\xbc\\xacA\\xbd<\\x9c\\xb8\\x15\\xbc\\x85}\\x01<\\x87!\\\"=}\\xfa\\x8a\\xbc:\\xe8\\xfb<\\xbd\\xb3S<\\x0c\\xaf\\x84<\\xaa\\x05\\xa9\\xbc\\\"\\xc4=<-Q\\x94\\xbcc\\xb8\\xcc\\xbc\\x02T\\x8d\\xbbgX\\xe8\\xbc\\xaa\\xe8\\xc4<\\x90g\\\"\\xbd\\x9b`|\\xbdB\\xb0\\x9c\\xbdS\\xf0\\x91=\\x8f\\x8a\\x18\\xbd!\\xa2\\xa6\\xbc\\x08\\x06\\x04\\xbe1\\x19Q<\\\\\\xf2U;\\xd9\\xa2}\\xbc\\xcc\\x1e1\\xbd\\x8d\\x03\\xdb\\xbbt_-=I\\xf8l\\xbc&Y\\xbe\\xbc\\x107s\\xbd\\\"\\xbe0\\xbb\\x1b\\xa64\\xbd8\\xa5\\xca\\xbc<\\x8a\\xbf\\xbb\\xe6\\x01\\xb2<\\\"e\\xe5\\xbcg\\x1b\\x15=\\xbc\\xces\\xbd\\xb2\\x95\\xb8</\\x16\\r<8\\xdc=\\xbd\\x13\\xa5\\x12\\xbd\\xd8\\xbb\\t\\xbc\\xac\\xf1\\xa0\\xbc\\xb6\\x95\\xc6<\\\\^\\x00\\xba\\xf2\\xb6X=\\x02\\xf9d\\xbd\\x94\\xf1&\\xbce\\x8d\\x9c\\xbd\\xad\\x82\\x01;\\x01,/=\\x1c\\x97d<\\x81\\x88\\xa5;\\xeft\\xec<\\xd2\\x1e-<\\xa6d\\xef\\xbb\\xf7\\x07\\x0e=\\xe3^G\\xbd\\xbd\\xd49<\\x81>\\x1d=\\xa7\\xa9\\t=p\\xbb\\x9a<\\xe3Jb=\\xa3\\x0e\\\"<!\\xea\\xfe<*\\xf8\\xfd;\\xe2R,<\\xccE[\\xbb\\\"\\xb1\\xa6\\xbc\\xc2\\xc3e=\\xb6S,=\\xc1\\xc9\\x1b\\xbdi\\x00n=w\\xff\\x9d\\xbc\\xb1|\\xba:\\xda\\xf6/=\\xb1O\\x92=1\\xb4\\xa5<\\x15\\x81\\xd9\\xbc\\xd8+Y\\xbd\\x86m$\\xbd\\xb1\\xfa\\xc6\\xbbSK\\x05> \\xea\\xe6\\xbb\\xff\\x8e\\xed<\\xde\\xc8\\xe5<\\x0c~\\xfe<s.\\xa3<\\x82, <\\t*\\xac\\xbb\\x8b\\x7f\\xa3<\\xc0\\x99Y\\xbd\\xbe\\xee\\xe5\\xbc\\xc5@?<m\\x85!\\xbc\\x89\\xd6\\x1c=\\xb8y\\xa4\\xbd]8/\\xbd\\x8f\\xcaK\\xbd\\xee\\x1cF=%X\\xb5\\xbc\\xd4\\xf09<\\x1e\\x8bj;\\x80A\\xac<|\\x0c\\xa0=\\xeb\\xb01\\xbd\\xd0\\x8e\\x9d<\\x97\\xc3\\x80<\\xde\\x11\\n\\xbd?\\xc0*=\\x91\\xc5\\xbb;5y\\x9c<Jb\\x07\\xbd\\xd0\\x85\\\"\\xbb\\x9a\\xf0\\xf7\\xbc\\x96\\\\\\xdf;\\xe3\\xc9\\xa0\\xbc\\\"\\xf6W:\\xf2\\xaf\\xd8\\xbc\\x19\\x7f\\xe0<\\xc4;O\\xbd\\xb9k\\xc6;\\x9b\\x01\\xd7<\\x19g\\x80\\xbc\\x86H`<\\xc3\\x05[=`\\xb2\\xdf\\xbc\\xe0[\\x07\\xbdkT\\xeb\\xbcB\\xf0\\xc4\\xbc-o\\xc6\\xbc\\xda\\xa71;8)\\x14<8\\xb0y;U\\xdc<=\\xe7\\xbe\\xc1<\\xcb\\x8b\\xf6<D\\xe4\\xb4\\xbc3\\x01\\xfa\\xbcy\\xeb\\x8f\\xbd\\xc7\\xeb\\\\\\xbd\\x81i\\x94\\xbc)\\x03x\\xbc[\\x93<<}\\xf8 ;\\x98q&=\\x97\\xfc\\xd5\\xbc3\\x99\\xb9\\xbcv\\x9a\\xd5\\xbc\\xee\\x00\\x8b\\xbc\\xee\\xa1\\xd4;\\x13\\x15\\x07=\\xb8L\\xec\\xbbJ\\xa5Z\\xbd#\\xe60=\\xa2*\\xf4<8\\xe6\\xf8\\xbb\\xc2i\\x89<}\\x9e\\xb8\\xbc7\\xda\\x98=R\\xe1\\xc8;\\x14\\xd3\\x00\\xbd\\xaa|J=\\xb0\\xa4\\xc2\\xbamf\\x82=\\xe4n\\xff<\\xc5\\xd0G;\\xd3\\xc5y<\\xa3\\xceI\\xbc\\xe0\\x87;=!\\xef\\xe6\\xbb\\xea\\x16\\xa8\\xbd\\xf2\\xe9\\xff\\xbc?\\x8f\\xac<\\xdcI\\x10\\xbda\\x06-\\xbd\\x14-\\x15\\xbd\\xe5\\xef\\x97;~\\x8b\\\"<KH\\xc3\\xbb\\xa1<\\x84=\\xe4\\xd2j=\\x1eT\\x90\\xbc\\xa9@\\xb5\\xbc\\xcb\\x91\\xe8\\xbc\\x06\\xa8\\xda\\xbc`U\\x1f\\xbc\\xaf\\x17Q=\\x1e\\xbb<\\xbcT\\xdf\\x19\\xbc/\\xb1\\xbe<\\xfc\\xe8\\x1c=\\x14\\xff7=c\\x83\\xce;E\\xd1\\x86\\xbd\\xd1\\x1a\\x99;k\\x8f6<\\x1f\\xa2\\x83\\xbd\\xd4\\xb9\\x8e\\xbd\\xd9\\x9b\\xaf<#I\\xb2\\xb9.\\xf3\\xa4\\xbdbl\\xf0<9P \\xbc\\xa2\\xd9L\\xbdZ\\r\\xc3<\\x92\\x85\\x19\\xbd\\xc4\\\"\\x9a\\xbd\\n~t=\\xd50L=\\xf1\\xcd&\\xbd6\\xee\\x0f\\xbcx\\xe0\\x95\\xbc`\\x8d\\x87\\xbde\\xa9\\x85:\\xfa.x=o\\x17]\\xbd\\x11\\xb7\\n\\xbdvG\\x13\\xbd\\xec\\x06z=\\xe2\\xf4\\x9c\\xbc\\xd4G\\xf9<\\x93\\xab#<|\\x84l=.\\xf4\\xa3\\xba\\xdaZ\\xef<\\x0f\\xb6Q=\\x11\\xfb\\x1f<\\xe3V1\\xbc#\\x03\\xb3=V\\xe6W\\xbc|\\xfb\\xba<bG\\xad\\xbc\\xec\\xbe`=w\\x141\\xbd\\xf6\\xec\\xd1;\\xec\\xc9w=\\x19\\xf7D\\xbcs\\xa4|\\xbcS\\x10\\x86</A{\\xbbP\\x7f\\xd0<:\\x860\\xbd\\x1e_\\xd9<\\xc7\\x021=\\xbcn\\x15;^\\r\\x1b\\xbd\\x94r\\x88\\xbc0\\x1d\\xad\\xbb\\x10\\xb0\\xba\\xbc-\\xf8\\x93\\xbdi\\xcf\\x12\\xbd<\\rd=@\\xee:=O\\x8eO\\xbb(\\x91/=^\\xad\\x92<\\x15\\xb4m\\xbcZY%\\xbd\\xf9\\x98j\\xbc\\xcb\\xcf\\xd5\\xbb\\xf89\\xa4\\xbc\\xf1\\xda\\x02\\xba5\\xe42<h\\xb8\\xdc;\\x88b\\xed;\\x0bc\\x7f<\\xd3\\xd6\\x99\\xbd\\x07\\x89U\\xbdi\\xbc\\xc6\\xbc\\\"&\\xf5\\xbb0\\x1bf=\\x0cQU<\\x1c\\xb0\\xa3\\xbc\\xd6\\xe5^\\xbd\\xa721\\xbd\\x98/\\xb4=c1\\x81=\\xfa\\xbe,\\xbd]L\\xb1<\\x8d\\x08V\\xbcr\\x1b\\xd5\\xba\\xc2\\xbe\\x15=b\\xa7q\\xbc\\xbc\\x1d\\x0e\\xbd\\xe6\\xb6\\x0e\\xbdi\\x9c\\xa2<\\x88hd<2\\xd8B=zEs\\xbb\\xc6\\xe8\\x14<\\xa7\\xc4\\x0f\\xbd\\xb8\\x95\\x99<9\\x192=\\x80\\xda\\r\\xbd\\xb2\\xdba<\\x96o#\\xbc\\xe4\\xeb\\xc6<3\\xd1\\xff\\xbb~\\xf8\\xdd:\\x9f3I<\\xb4m6<w\\xe7\\xfd\\xbd\\x00\\n\\x00;\\x0e\\xb5~\\xbc\\\\\\xb9\\xd1;\\x08\\xc9\\xae\\xba\\x1b\\x97\\x08\\xbdX\\xb6\\xa7;5\\x88\\x94=\\xf0\\xb6D\\xbc\\x8dW[\\t\\xadR\\x17=*\\x1bU<\\x1f\\xab\\x18\\xbbIK\\xc3;\\xb2\\x80|\\xbbu\\x1b\\x1c=}\\xe2i\\xbc\\xccyR<$v\\xe7<7\\xeb\\xae\\xbc:\\xb8\\x8a=M0\\xb8<\\x9b[(<q\\xbd\\xdb<T\\x9d\\xb8\\xbcl\\x99\\x03<K\\xa6\\xcc\\xba\\xb8\\x19\\xaf9\\x85\\x92\\xa6<\\x9c\\xf0\\x86=~\\x18\\xc4:\\x18\\xc0}<\\xba\\xf1P<\\x93\\xdb)=\\xfaT\\xb4<t\\xea\\xb2<\\xbe\\x19\\xa6;\\x81\\xa5G<\\x07 \\xbe\\xbc\\x7f\\xba\\xd2\\xba\\xbf\\x95\\x88<\\x915T\\xbco\\xd4B\\xbd\\x7f\\xbf\\x99\\xbd^UY\\xbdj\\xddg\\xbd\\xdf\\x92<=\\xf4\\xb0\\x90<\\xea\\xcdi=C\\xd6.\\xbd\\x80\\x16\\\\<\\xd8\\xdd\\x8d\\xbcd\\x8d\\xc8<\\x9d\\x9aw\\xbd;\\xcc\\xf6<K\\xbc\\\"=m\\x01M=<\\x18\\xc8\\xbc\\xb3[\\xa8\\xbc\\xae\\xed\\x91<r\\xddn\\xbd\\x92\\x9b\\\"\\xbc\\x01\\xbc\\x9e<\\xb4A\\xf3<\\xaf.r<[Va\\xbc\\xd9\\x17\\xb2\\xbc\\x0b@\\x05=\\x92Z\\x96\\xbc\\x12\\\\8=P\\xec\\xc4\\xbc\\x97\\x1f\\xb5<?w$=\\x05\\xfe\\x00=L\\\"y\\xbc&\\x92P\\xbd\\xacJ\\x8a:\\xb8\\xa5\\xa6<$\\xda-=\\x9d\\x7f\\xfd\\xbc\\xd5\\x0f\\x1f=\\x97\\x9f\\x1d\\xbb\\xba\\xa5\\xc8\\xbc\\xb4\\x96T<\\x8a\\xde\\x1e=C \\x98\\xbc\\\"i\\x01\\xbd\\x05{-=\\xb6\\x80\\x8b\\xbd\\xc8\\x07$=o\\x9eQ\\xbd\\x9a\\xa2\\xc6;\\x08\\xbc\\x04=e\\x84\\x94<\\xcbL.\\xbc\\xe6\\x8f\\x1a=g\\xf3\\xe8<\\r\\xa4\\x8a=\\xcd\\xa9\\x8a<\\xc4\\xa4\\x87<\\x0c\\xad\\x8c\\xbc\\x02\\xe17\\xbd\\x82\\x03>=\\xa1\\xe0\\x949\\xf0\\xd3j;\\xd8\\\"\\x07=\\x91\\x7f\\x1a\\xbb\\xf7z\\x00\\xbc\\x11\\x12\\xaa\\xbc\\x13c\\x9b\\xbcm\\x8d\\xb9<\\x03\\x88\\xf1\\xbcwJ\\x84=\\xbdr\\xdb\\xbc\\xfe\\xd2l;.\\x03\\xd1\\xbc\\xf0\\xe4\\x89<\\xb8\\xea\\x95\\xbaL\\x02\\xe5\\xbdSvZ\\xbdj\\xb1\\x05=\\x006\\x85\\xbc\\xc5\\xab\\xd8\\xbc#.\\x9c=\\xd6~\\x7f=\\x01yc<\\xe5\\xa2\\x7f;\\xf4\\x9cD\\xbd\\x16\\xe4\\t\\xbc\\xd9yq\\xbd\\xc9\\xa4>=}#\\x9c<w\\xf7\\x89=J{\\xc3<\\xf0W\\x19={(E\\xbb\\xb2\\xaf2\\xbd\\xe4\\xd7\\xe0\\xbc\\x97;\\xe5<\\x1d\\x98\\xa7\\xbd\\x1e\\xd4\\xee;\\xac \\x94\\xbd?w`=\\x98\\xe7\\xf3\\xbc\\xcd\\x85T=f.\\xac<\\xca\\xad\\x06=F\\x8a\\x84\\xbdR\\x91\\x82\\xbcj\\x02\\xba<\\xc7\\xe7\\x82;{\\x99:\\xbd\\xebHs<e \\xce<.\\xf6\\xb3\\xbbH\\\"\\x1e=\\xa3\\x05\\xb09(\\xe2\\x13<#>\\xd9;\\x1d\\xfb\\x07\\xbd\\xf4\\x8a\\x8a\\xbc\\x8aE\\xac\\xbb+\\x89\\xaa\\xbc\\xf3\\xab\\xb8<\\xafv#=\\x17\\x98\\xe7<\\xc9\\x0c\\xea\\xbc\\x98\\xe0\\xc5<[M1=0\\xd1~:\\xaf\\xf9\\xaf;\\xc1\\xd2\\x80\\xbc\\xe2~\\x08:\\x0f\\xc0\\x9a<\\xc0Y\\x16<C\\xd6\\\\\\xbb]\\xebk\\xbdq\\xa9\\xf79\\xb5te=\\xd1\\xfd\\xd4<\\xf6O\\x8e\\xbc\\xab\\xb9\\x06\\xbd<\\xf5\\xcc\\xba*N\\xab<\\xe6\\xa06=\\xee6u=\\xb7\\xc6U\\xbbr\\x12\\x82\\xbc\\x1f\\xdf\\xa6\\xbc3.\\x1d=V\\xae\\x1d\\xbd\\xbbQ\\x16\\xbe\\xe3/A;\\x0f\\x9a6\\xbc\\\\\\xadN\\xbd\\x86\\x03,\\xbd\\x85\\x8a\\x12=\\x1ba8=$/\\x02=\\xeeC\\xb0\\xbc\\x85\\xa68\\xbd|\\xa8\\xaf<\\xe1e~\\xbd\\x04+~\\xbb\\\\e\\x9d\\xbd\\xc5\\xec\\x0b\\xbd\\x1c\\xe4a=\\x80\\xc9\\x14\\xbc_\\x10\\x11\\xbd\\xf6\\xa7-<\\x95\\xf0\\xd4\\xbcw\\xad\\xaa\\xbc\\xe0~\\x9a:\\xfb`\\x8c\\xbd\\x8b\\x81f<\\xfebw\\xbc\\x033\\xcb<\\xde2\\x10;\\xd5)\\xcd<\\xa4\\xa1U\\xbc\\x18\\xac\\x0f;\\xfcc0\\xbc~\\x02\\\"=Np\\x91<x-\\\"\\xbd_\\xc9\\x14\\xbb|Q\\xaa<\\xcb\\xe9\\x8c\\xbc\\x9f\\xa8*\\xbdS\\x9fC\\xbdM0\\xfd\\xbc%1\\x1a\\xbd>\\xfaJ\\xbc\\xf2t\\xbf<\\x9e\\x02I=\\xbdU8<;\\\"\\xaf<Xk\\x06\\xbdT\\xafM\\xbd\\x14.3=&j\\x8d<#[\\xa8\\xbbs\\xdcY\\xbaA\\x95&\\xbc\\xaf0\\x1d=!\\xa2C\\xbd\\x1a\\x88^<\\xb4!\\xce<o\\xac\\x91<s4G\\xbd\\xbdk\\x0c\\xbd\\xe5\\xa6q;\\xc0@\\xf0<\\xbc\\x8d\\xab\\xbbb\\x85\\xa1\\xbc\\x0c\\x8b\\x1b<\\xfe\\x0c\\xbc\\xbcJs\\xf7<%\\xbd\\xba:\\xb1\\xdb\\x1a<\\x85Z$=\\r\\x05\\xa6\\xbaH\\\"\\xa4\\xbb\\\"5{=u\\xd7v\\xbc6V\\xbc\\xbb^\\x8d\\x8f\\xbcj\\x0e\\x9d<\\xc5\\xfd\\xef<\\x11\\xbeK\\xbdi\\x15#<\\xe5\\xe5\\x10;>*\\xc2<\\x8b\\x1d\\x06=\\x04\\xbb[=\\xb7\\xb6.\\xbc\\xad\\xda(<DS\\xc8\\xbaL\\x914\\xbd8[h=\\xc0[,\\xb7\\xfa\\xc5\\x02=`\\xe8\\xfd\\xbc\"\nHSET bikes:10004  model 'Pallas' brand 'Eva' price 2572 type 'Commuter bikes' material 'full-carbon' weight 15.0 description 'This bike is a great option for anyone who just wants a bike to get about on. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"f\\xcc?<\\xf4\\xa6%=KG6\\xbc%\\x16\\x1b=4+\\x0c=\\x7f\\xb6\\xbf<N\\xd4\\xe6\\xbc\\xea\\xe9R\\xbd@w\\x8f=\\x98\\xf7\\xf3<6=\\xb9<#g\\x17=v\\x07+=\\x8c\\xdb\\x0e<i\\xf0\\x8b=\\xf20q\\xbc\\xee)\\xa6\\xbc\\xe6U7<\\xca\\xcc\\xc4\\xbc\\x99v\\x85\\xbc\\xffF\\xf7<\\xd5a\\xdb\\xbcI\\x06j=\\x8c\\xc2&\\xbd\\x86g\\xb6;^I\\x88<\\x06G(=MH\\xa1\\xbb\\x91\\x1c\\xa9\\xbc\\r/Z=\\x85\\x05\\\"\\xbd\\xf4\\x06x\\xbb\\xb1ZS\\xbc-\\x14\\xe9:\\xd8\\xc8\\xbf<\\xb8\\xe5\\xc39b\\xac\\xb2:m\\xa6\\xd1\\xbc\\xf9\\xe7A\\xbc\\x1aTx\\xbcp\\xd2P=\\x17W\\x10<$<D=f\\xae\\x12<\\x1c\\xe1\\r<u\\xa7N\\xbd\\xfa\\xbf\\xc6=\\\"m\\xed\\xbb^l \\xbd\\xf04\\xa8\\xba\\xca\\x9c\\x87\\xbd\\x1a\\xcaS\\xbd6p\\xa9\\xbc\\xda\\xf8\\\"=U\\xd0\\x89<\\xcf\\xb0\\x8c\\xbb}q\\x12\\xbc\\xf37k\\xbd/\\xafL\\xbd\\n9\\xbb=\\xa9\\xb2j=DK\\x12=\\xd6\\x8f\\xcc<\\xee58=o\\x16\\x86<tr/:m\\xf3\\x01\\xbdb\\xfd\\xee<#\\xbd\\xcd;\\xba\\xde><\\xc2\\x00\\xa2:\\xdaY\\r<*\\xc7#\\xbdd\\xda\\x86<\\x8fz\\xdc\\xbb#:\\xc0\\xbbKrb<lK\\xf3\\xbc@*\\xda\\xbcZl\\xee\\xbcb\\xf2!=.\\xe9\\x8a\\xbd.\\xb7\\xbe;\\x13>\\xff\\xbc\\xd9\\x84\\xf5\\xbcO\\\\\\xd6\\xbc\\x9b\\x9c&<\\x83L\\x93<\\x7f=;=\\xe3\\xc0\\x8c<\\x02\\x87d\\xbc\\xc7\\x9c@\\xbd\\x1e\\x8c\\x07\\xbd\\xd2\\x87\\x86\\xbc\\x90P\\xcf<\\xef}\\x8d\\xbb\\x10\\xa3~\\xbb,|P\\xbd\\xb1\\xd6\\xa3\\xbb\\x16\\\"\\r\\xbdf\\n\\xbd<\\x1e\\xde\\xa8<\\x80\\x04\\x03=\\xcf\\\"\\x87\\xbd\\\"\\xd9\\x1e\\xbc6\\xf5\\xae=#\\xde\\x9d<\\xe2!\\x19<z\\xec\\xc9\\xbc2\\xc4\\xbd<8\\xba\\xc3<5\\x80.\\xbb\\xef\\\"\\x03<\\xf4kZ\\xbdd\\xa2,\\xbd\\x8d_,\\xbd\\xcd\\xba\\x807\\xd3}{\\xbc\\xdd\\x13\\x1d=\\xa4MW<s\\xb3~\\xbc\\xe9@R<?IN<\\\"\\x06\\xe2\\xbcn.\\x88=H\\x1e\\xe7\\xbb\\xdc\\xc1\\xa3\\xba[|_\\xbd\\xaaQ%\\xbd\\xa8\\xbd\\x12\\xbd~O\\x8d\\xbc\\xea\\xb1\\x14\\xbdc\\x9e\\x8e<\\xadJ\\x15\\xbc\\xac\\x0f\\xe2<\\xdd\\\\T=Ou\\x1c\\xbc\\xeaB\\xc6\\xbc\\x8f{\\x83;\\xcb\\xbf\\xc0\\xbc\\xafC\\xad\\xbc\\x1f\\xbf\\x13\\xbdZ\\x9av=.\\xc8\\xa1=\\x0e\\x9b\\xef;\\xf2\\xf2Y\\xbc\\xb4:\\x15\\xbb\\xbe\\xd4\\t=5t\\xd9\\xbcwW\\xe1<v\\x95J;\\x82\\xc7\\x87=\\xef9E\\xbd\\xbe\\x02\\xd7<t\\x07\\x0b\\xbdD_]\\xbd\\xeaW\\xfd\\xbc\\x0f\\xaf\\xaa\\xbc\\xdd\\xac7\\xbc\\xf8y!\\xbd\\x88\\xb3D=\\x95\\x8e\\xf3\\xbc\\xa2:\\x07\\xbd\\x9eOk\\xbc\\xc9\\xda\\xb5\\xbbb\\x9e\\xa4<q\\xebe\\xbc\\xe0S\\xe7<o\\x884=\\x19\\x00\\n\\xbd\\x9eR\\x0c\\xbdW\\x81\\xed<\\xb91F:\\xe3\\xb2\\n\\xbdV\\x1a\\xb0\\xbc\\xe0\\xef\\x07\\xbd\\xc2\\xfeV\\xbd\\n\\x1c\\xe1<\\xfb\\xe2\\xa1<L\\xaa\\x00\\xbcg\\xaef\\xbcQ\\x08\\x84\\xbad\\x1b\\xe5\\xbc\\xf8~v<\\xc6\\xfc\\x88=\\xec\\xe6\\x86<\\xce\\xe1O=D\\xd6\\xa4\\xbc\\x90\\xc9\\xcd<\\xa2\\x00\\xa8\\xbc}f\\xb8<\\xaf\\xa3\\xd6<\\xeb{\\x01\\xbc\\xb9\\xa8g<Y[\\x81\\xbc\\x11\\x85q=PZ\\x8e\\xbd\\xde\\xb4\\x18\\xbd\\x8e(\\xc0\\xbdL\\xf9M=\\xf4\\xd47\\xbc\\xad\\x07U\\xbd\\xab\\xc3\\xa1\\xbd]\\rT\\xbc159\\xbd\\xe5mP<\\xfb\\xe6\\x04;.\\xd2\\xea\\xbc\\x0b\\x7f==\\x1eF\\xc1\\xbcV\\x1c\\x85\\xbch\\x1a\\xb0;\\x98\\x80-=\\xf0\\x8f\\xed\\xbc\\xb9{\\xc5\\xbb\\xb7\\xf6[<C})<2~\\x1d\\xbd\\xb5\\xe5\\xd2<\\xc9\\xa4\\x8d\\xbd\\xa0\\xb2l\\xbb\\x90\\xd1\\x12=\\xd4\\xec\\xd6;(\\x93\\x08\\xbc\\xd8\\xee\\xf3\\xbc\\xcfQb\\xbd\\x8d\\xfd\\x94<\\x9fq\\xfc\\xbc+\\x8b\\x00=o\\x1c\\x87\\xbd\\xcc\\x1b\\x7f\\xbcP\\x0e]\\xbd?\\xad|\\xbc2p[=\\xf8,\\xdd\\xba\\xeb\\xe1>\\xbcY*4<\\x04\\x03\\xe6<\\xf5\\x95\\xdc\\xbc\\xed\\x8f\\x03=\\xde\\xbe^\\xbc\\xc7\\x12\\x86\\xbcP\\xc4\\x88<\\xeb\\xab\\x04=\\x9e\\xd3\\x95;\\x8aF\\x94=V{8<\\x81\\xdf1\\xbc\\xe4}\\x1f\\xbd\\xe0\\xce\\x0b=\\x19\\xddq\\xbb[yE\\xbd~\\xb5\\xaf9B\\xff{=r\\xb6\\xae\\xbd\\x85\\x17\\xe5<s?\\x8f\\xbc\\x9a\\x0f\\xb7\\xbc\\xa4\\x06\\x8b\\xbc\\xe6\\xce\\x87=\\xe03\\xb7\\xbb!\\xa9\\x00\\xbdw\\x03\\x0b\\xbd\\xd4|\\xa0\\xbc\\xf5(\\x92\\xbc\\x0f\\x98\\xfc=\\xda\\xf3\\xd8\\xbc\\xaf\\xbe\\x04\\xbd;\\xd6+=\\x9a\\xe51\\xbc\\xfb\\xa3\\xaf<\\xea\\xb8)\\xbc\\x82\\xb2\\x91\\xbcfG\\x04\\xbb_\\xe4\\x99\\xbc\\t\\x1aW\\xbcC\\x7f\\x19\\xbd\\xb0$m\\xbc\\xcev#=t\\x8d\\xc4\\xbd\\x19\\xeb}\\xbd\\x10g\\x8a<\\x7f\\x84\\xe2;\\x10\\xc9n<fF!=\\xa8yL</NY\\xbc\\x8cO\\xd3<k\\xad\\xb1\\xbd\\x11\\x17\\xd5;\\x08\\xc8\\xe8<\\xdbj\\x93\\xbc]9\\xac=\\xfd\\xad\\xcf\\xbc\\xe2\\xd5\\xb6<Tf\\x01\\xbd\\xd1\\xb8\\xcb<\\xc8\\xd6>=FXJ=\\xc3\\x86\\x02\\xbdc\\xd9m<\\xd5J\\x14=\\xcd5\\x17\\xb9\\xc356\\xbd[\\xc3#=c\\xe0\\xc5=\\xfbKM\\xbd \\xe8\\xa2\\xbb$\\xd9\\xac=T\\x05B\\xbb\\xbfz\\xe9\\xbcj_y\\xbc[\\x07w\\xbc\\x80\\xea\\x14\\xbd\\x01\\xde5=k\\xfb\\xd5\\xbb\\\\#\\xd0\\xbc\\xff\\xf7R=\\xab\\x91j=\\xbaT\\xc5\\xbcWy\\x1b\\xbdF\\xc4\\x04\\xbd\\x13\\xd4\\x0f\\xbd6M,\\xbd^\\\"G<\\xa3S\\x08\\xbd{h\\x0f=\\xfaeu\\xbd\\xe8\\x8cH;\\x06\\xd0\\xe3\\xbb:6\\x05=\\xa3\\xf6g\\xbd\\x03\\xd9\\x7f\\xbcv\\xb8\\x9d\\xbb\\x1e\\xe9\\xa5<q\\xfc\\xa0\\xbc5\\x80\\xae\\xbd\\xc0AS=7[o<\\x12\\x9f7<\\x9c\\xb3\\xaa\\xbb\\x03\\xaf\\x94\\xbcd\\xed\\xa0=\\x87\\xfaw<\\xd7F\\x82\\xbc\\xedV\\xc4<`f\\xba<\\x06\\x9be=\\x14t\\xd1<w,\\x85\\xba2\\x14A\\xbc\\x9a\\xe1*\\xbd\\r\\xb5~=<mT\\xb9f\\xccd\\xbd\\x83I7=\\\"\\xba\\x14=\\x9d\\n\\xb1\\xbcv^a\\xbd|\\xbd\\xdd\\xbd\\xcd!\\x9b;\\x85\\x00 <_\\xb5o\\xbd\\xe9#\\x84=\\xf0\\xd3q=\\x9c\\xf4\\x1393Z\\xf1<pA!\\xbbW\\x12\\xaa\\xbdG\\xdd\\xd5\\xbc\\xf4\\x1f\\x9d=\\xda\\xc0\\x03<\\xca\\x9fh\\xbc\\xce\\xd0\\x0b\\xbc\\xbf\\t\\x1a;\\x08\\xeb\\x0c=1\\x1b\\x87\\xbc\\xbfa\\xd9\\xbcc^\\x92\\xbdV\\xaf}<,\\xd3\\xff\\xbcC1C\\xbd\\xb1\\x1f\\x0c\\xbd$e\\xba\\xbb\\xf38H\\xbdR\\xdf\\x8e<R\\xf2Z\\xbd\\x02WJ\\xbd\\xb3\\x95\\x1f=\\x06z\\x00\\xbdl\\xf0\\x84\\xbd{rH=\\xfc\\x03s=F_\\x90\\xbc\\xd8SU<\\x8e\\x04J<\\xb3^m\\xbd \\xa9\\n\\xbd\\xfa\\xce\\xe2<\\xc9\\x80\\x91\\xbd;\\xe4\\x8f\\xbcw\\x83,\\xbd\\x91|\\xf5<\\x903\\x08=zvt\\xbcX?\\x8b\\xbc\\x83\\x9f\\x81=Z\\xebv\\xbc\\xeb\\xfe\\xf0\\xbb\\x10\\xc3\\xb7<\\xf8c\\xad;\\xa6\\xaa\\xb3<\\xe0P\\xcf<\\x9c/}\\xbbk\\x06*<\\x81g9=&;\\xe2\\xbb\\x04AD\\xbdf\\xb66<R\\xd6\\x02=ZJ+<\\xcd\\xee\\x0f\\xbd2%\\xfd<\\xb0\\xee.=\\xc7\\x7fI\\xbb`\\xf7\\xbe\\xbb\\xd9c\\x16\\xbd\\xb4\\xebm==\\xfda<\\xa6]\\xc3\\xbcn\\xbd\\x11;\\xe7e\\x86\\xbb\\x81\\x8ak\\xbc\\xb2\\x8e\\xd5\\xbc\\xb1Gj\\xbd#;\\xc3={\\xc7\\x14=ab\\xfa<\\x96\\x99\\xc9<\\xdb\\xbdJ=\\xafl\\xd6\\xbc\\xb1\\xe0\\xba\\xbc\\xaa\\xf0\\xa0\\xbc\\x9d\\x8e\\x03\\xbc\\xc9\\xd4\\x9d;\\xf3\\xb9i;v\\x02\\x86\\xbb1iT;\\xfd\\xc2-\\xbd\\xa1\\x94\\x88<\\x19\\x1b\\xd1\\xbcW\\x9ai\\xbdZ\\xcf\\r\\xbd\\xda14\\xbd@\\x17`=\\x13\\xf21\\xbb\\x02\\x05\\x8f;\\xcb\\xba\\\"\\xbd\\x9e/\\x1d\\xbcW\\x90\\x91=]\\x9d\\xab=\\xd6\\xd8\\x1a\\xbd\\x14RI=j\\x87\\xa1\\xbcd.\\x08=\\x82>I=\\x129\\xce;\\x03\\xa3\\x0b\\xbd\\xb6\\xf6\\xfc<\\x81\\x06\\xdf<\\xb8\\xa1\\x04\\xbc+E\\x11=\\xfeh\\xe4<%N\\x11<\\xc0l\\x17=\\x8e\\xc2\\x14=\\x06#\\xb3:\\x0c\\xcc\\xe8<\\x8f\\xce\\x1b<\\xc8\\xbb;\\xbc\\xb4\\x16\\x91<\\x14\\xea\\xa6<rl\\xe7\\xbceV\\x9d<`\\xcc\\xad\\xbb\\x9e\\xd7\\xde\\xbd\\xb7\\x9cO<t\\xac\\xb7\\xbc\\x8d\\x02?\\xbb\\x02%==j\\x85\\x02\\xbd\\x17U/\\xbb\\xdd8\\xa1=\\x15\\x91\\x02=6NO\\t\\xd4\\xb4\\x15\\xbc\\xec\\x08\\xa3\\xbc\\xea\\x10\\xa6;W\\x9d)<\\x86\\x81\\xce9\\xbb\\xad_=\\xec\\xc5!;f7\\\\\\xbdhf\\x1f<<\\xeb\\x83\\xbc\\x19\\xe4\\x17<\\xcbQ\\\"=M\\xc5-\\xbc\\x14,L=\\xb9#\\xb2\\xba\\x7fc\\x8b<\\x9f5\\x01\\xbd\\xedJ\\x8f;\\x92Eg\\xbc:6\\x1f=G\\xfb\\x06<\\x02\\xe1<\\xbd\\xe08:<\\n<\\x13<\\xc1I\\x83:\\x9eK\\xd3<\\xa5\\x82\\xb3<\\x86\\xe8=<\\x90\\xe4\\n\\xbd\\x0f\\x80\\xc7<E! \\xbc\\n\\x04\\x91;\\xceoK\\xbcZ\\x9e\\x98\\xbd\\x88-\\xe8\\xbcd\\xb4\\x9d<\\xe8\\xec\\x10=.\\x1c\\x1a\\xbb\\xa7\\xea\\xd2=hT=\\xbcPJ6<\\x03\\x07!=#?\\x15=\\xeap$=p\\xff\\xda<\\xbd\\\\\\xdd;\\r0\\xda=\\x91\\xd9`\\xbb\\xa2\\xc0\\xca\\xbc\\xc9C\\xf3\\xbc*\\x15$\\xbd\\xfa\\xe9^\\xbc\\x9dO\\xc1\\xba\\x85\\xc4\\xe5\\xbc\\x9d\\xa4\\xee<{\\xc0\\x9b<\\x0ex\\x0e=\\xa1\\xb5\\x0c=\\xef*\\x88<<\\xe8 =\\xd9\\x99\\xf5\\xbb\\xfb\\xe1\\x86;\\x9d\\xa7\\xbc;\\xb2\\x0c\\x8c<W\\xc1\\xac\\xbd\\x7fcM\\xbd\\x96\\xca\\x08\\xbd\\xb6\\x07\\xcc\\xbch\\x7f\\xa5<\\x10\\xa9\\xa5<\\xa2t|=\\x07\\xe7\\xae\\xbc\\\"\\x10$<\\xd0e\\x87<\\xe1\\xdd\\x08=\\xeb\\xc1\\xb6\\xbc\\xc4\\xde\\xa8<jo0=\\xec\\xd8D\\xbd\\\"\\x90\\xe1<:\\xaa&\\xbdP\\xa9\\xb9<\\xdft\\x0f\\xbd\\x03\\xfcK=\\xc4T\\x19\\xbd\\xcdG\\xb6<\\x8a<\\x8b=\\x0e\\x86\\x89=\\x9b\\xc9\\xb4:\\xd8c\\xb1<\\xe01 \\xbd\\x1a\\x10\\x17<A\\xa8\\x91=\\xf3A\\x01:5\\xffo=\\x16\\xc2\\x1c=\\x85\\x0b%\\xbdi\\xd4\\x9b<\\x8e\\xb7\\x8f\\xbc\\xda\\x929<\\x9ag\\x15\\xbdb\\xebQ\\xbd|6\\x08=\\x19\\xaas\\xbd\\xdd\\x8f\\xd8;>\\xcb\\x96\\xbc\\xdb\\xd3x=8O4\\xbc\\xe2\\xa8\\xa8\\xbd\\xd3\\xac \\xbc\\xb2\\xa4\\x8e<\\xb5\\xa3o\\xbc\\xa7yn\\xbd\\xf8\\x1a\\x9c=\\xd4\\x8b\\xa5<*\\xf7\\x90<\\xa8v;=\\xa4\\xebM\\xbc\\xc8v\\xcc\\xbc\\x9d\\x80\\x9b\\xbd:\\x95\\x89\\xbc\\xdc\\r\\xe6<6\\xc8\\x1a\\xbc\\xef\\xda\\t<\\xc2\\x19\\x1a\\xbc\\xdd\\xe2X<B\\xa3\\x17\\xbb%\\xf9\\xcb<\\xa0\\x85\\xb1\\xbb\\x81\\xadu\\xbd\\x13o\\xef\\xbc\\xebgj\\xbd\\x84-\\x81=*n\\xca\\xbbm\\\"\\x80=\\xce\\xf6\\x998\\x88x\\xcb<\\xef\\x1d\\x93\\xbd;\\\"\\x86\\xbc\\xfb\\x91\\xa7<3\\xd3i<\\xa1|\\x85\\xbd\\xafB?\\xbc\\x96\\xee3\\xbdg\\x90\\xf5\\xba\\xb9F\\x96\\xbc\\x92{\\xef\\xbc\\xa7\\xe7\\xe1\\xbaK\\xf3\\x83<\\xbc\\x1c\\xfd\\xbb\\xa2ss\\xb8\\x96\\xf9\\xa2:\\xb7:\\xbb\\xbb\\x9dr\\xa3<\\xff;\\x80<\\xcc\\x1bd<\\x0b\\xb6\\x82\\xbc/\\x08\\xb9=\\xa9\\xa1\\x1b<c}u<\\x16\\xcez\\xbd\\xca\\x100=N\\xa9~;_\\xc6\\x05<\\xddi \\xbb\\xed\\xac\\x15\\xbd\\xd0CS\\xbd\\x82hQ\\xbc<^0=\\x03\\x0bV:\\x87\\xd4\\x12;\\xee\\xb9\\x11;i\\xef\\r=\\x07\\xffE\\xbd\\xbd\\xe36\\xbb\\xf2\\xdc\\x95=\\xd9\\x81\\\"\\xba-U\\xf3<.\\x10\\x92<\\xd6o7\\xbb\\xb9%\\x8a\\xbd\\xbclH\\xbdH\\x1aK=F#\\x02\\xbdq\\x9c\\x1d\\xbcK\\xbf\\x0f\\xbd\\\"g\\x92\\xbc\\xafi\\xae\\xbb\\x01bU=2gU:Q\\xad\\x9d<\\xbd=\\xba<X\\xf3B\\xbd\\xd3\\xea7<\\x1f?&\\xbd\\xc9I\\xc2\\xbcv\\xbep\\xbc\\x10\\x86\\xa0\\xbc\\xb7H9\\xbdjJ\\x06<\\xado\\xa8\\xbc\\xd0\\x19\\x17=\\x04\\x9e\\xb6\\xbc\\xe3x\\xb0\\xbdSz\\xdb\\xbc\\x01\\xa0H\\xbb\\xd8C\\x1b=\\xf5\\x93\\xe8\\xbb\\x9aP$\\xbc\\x80a\\x83\\xbc84\\xc8<C\\xea\\xfe\\xbb\\xf0RL=\\xfa\\xb6\\x83<\\x93\\\"\\xe1\\xbcQ^\\x9e\\xbc\\x06\\xbc\\t=\\x08\\x18\\x89\\xbc\\x0b\\xe6Y\\xbd\\xe5\\xb8\\xb1\\xbd\\xb3A\\xd5;\\xc3\\x88M\\xbdv\\x91\\xb3<e\\x90\\x1c=*F\\x8b=jLu\\xbdjO\\xa3<w\\x1b\\x81\\xbbu\\x04\\x8e\\xbd\\xf3\\x02\\xa0<Cx\\xd0<\\x85@\\x81\\xbbP\\x13\\xb5\\xbb\\xfa\\xf8=;\\xe7\\xc1\\xca<5\\xb0\\xbf\\xbcp\\xcf\\xcc\\xbb\\xb3\\xef\\xb1\\xbc(y\\x83\\xb9T{\\x0f=\\xbbp\\\";[\\ry;\\x11\\xd1\\x16=\\xe1\\xe7\\xa0<\\xce&\\xa6\\xbc\\xb2\\xaf9=\\x85m\\x98\\xbc\\xc4T\\xc7<\\xf6\\x86\\x14\\xbdV\\x8a]<Q\\xef<<\\x9dTS\\xbcI\\xad\\xb1\\xbc\\x0e\\x977=\\xd8`^\\xbb\\xa9Z&=\\xea\\xd7\\xba\\xbc\\xb5\\xde\\\"<\\xc4\\x18\\x0c<\\xec,u\\xbdP\\x8fB\\xbc\\x96\\x8d\\xa5<\\x0c\\x1e\\x94<\\x9f\\x9c\\x0b=\\xa1@\\xaa=\\x03\\xd5\\x0f\\xbc\\x12\\xd3a=b\\xf9P=\\xfaei\\xbd\\xbe\\xd9\\x8c<%JI\\xbd\\xd6\\xf98=|\\xef\\r\\xbc\"\nHSET bikes:10005  model 'Triton' brand 'Velorim' price 1237 type 'Kids bikes' material 'aluminium' weight 11.4 description 'The latest kid-specific bike brand on the scene, this brand aims to offer high-end, kid-friendly bike geometry at an economy price point. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"=\\xbeA<\\xdfP\\xed<Sa\\x02\\xbd\\xb7\\xd9\\x0b=I\\xa2p\\xbbe\\xec\\xa6<\\xf9\\xa1\\xce\\xbc\\x88J\\x1b\\xbcx\\xd9\\xaa=\\xe4II\\xbc3\\x08,\\xba#\\xc1^\\xbcH\\xca\\xf8<w.I\\xbbzJ\\x0f=g\\x93\\n\\xbd\\xc9\\n\\x06\\xba,\\xc6\\x1a\\xbd\\xd0\\xf2::\\xcc{\\xcf\\xbd@\\x8a\\xa2;hI\\x8b\\xbd;\\x1f8=\\x8cP\\xd4\\xbb\\x85\\xaa\\xcd=\\t,\\x8d< \\x05\\x02\\xbcH\\xfc\\xf8\\xbb\\xbd\\xef\\xfc\\xbc`\\xf7\\x84<u\\x029\\xbd\\x89\\xee\\xaa\\xbc\\xefi\\xbd\\xbcZ\\x98K<j?B=\\xd5\\x0e\\x94\\xbb\\\\<\\x87<\\x7f9\\xa4\\xbc\\xeb(6\\xbd\\xb3\\xa2\\xbc\\xbc\\x15C\\\"=I\\x06\\xe9\\xbc\\x03<\\x94\\xbc\\xba{\\x9f<\\x98\\xdb\\xe4\\xbcW\\xe8\\xf6\\xbb\\xfd\\xf6U=L\\xe6\\x96:\\xa4\\xf3\\x07\\xbdj\\x9f\\\"\\xbc\\xcbI-\\xbc\\x1dS\\xa6\\xbc\\xae\\xf6\\xd6\\xbc\\r\\x93\\xdb;\\r\\xed\\x0b\\xbb\\t\\xdeB<\\xe9\\xc7\\xc2<\\x8e\\xf0\\xe9\\xbd\\xf3\\xc1Y\\xbdc8\\x0e=\\x93&\\xab;V\\xf6\\xa6<\\xabh\\x10=4\\xf4m=\\x90\\xfe\\xb2<0\\x83\\xb0<\\x91\\x10\\xcb\\xbcr_\\xbe;)\\xda{<\\xb3\\xb8\\xfe<\\xe0F\\xa4\\xbc\\xff\\xce5\\xbd\\t0\\x80\\xbc\\xae\\xc3\\xf2<\\xc4\\xce\\xc0\\xb8\\\"\\xdf\\x17=\\x956\\xaf<T\\x0c\\xcd\\xbcF\\xc2\\x93\\xbd\\xfa\\xa94\\xbb\\x89P\\xf6<\\xc33\\x9c;\\x14\\xcb\\xb3<\\x80\\xf1\\xb7\\xbdH?D<;\\x96\\x8b=4$P<\\xdb\\x05?\\xbc\\xf4C\\x03;\\xe0\\xc3\\x17<~|4\\xbc\\\"\\x15>\\xbd\\xf4\\xcd\\xcf\\xba\\x93n\\n;osN\\xbc\\x8bU^<\\xbb\\x16\\x19\\xbdU\\xc6d\\xbd\\x94}o<w\\x19\\xc4\\xbc\\x9e\\\"\\\"=F\\xd3\\x0b=\\xa5\\xbe\\x08\\xbch)Y\\xbb\\xdfl7\\xbdC\\xbb\\x14>K\\xed\\xba\\xba\\x01\\xf0\\xdd<m\\x8b\\xf9<\\\"\\x0e];Y\\xbe\\x9f;y\\xe0z=Z\\xe4\\xd5\\xbbT\\x1d\\xa7\\xbc\\xf8H6\\xbd\\xda6:\\xbc0|\\xb6\\xbc\\xd8\\x89\\xaa\\xbc\\xc4I]<>\\xd5,=g\\x14\\xc1<\\x04Gz<hI\\xfd<\\\"\\xd10\\xbd\\x11\\xdd\\xa6=\\xa2\\xe4\\xbe\\xbc\\xac\\xea\\xaf\\xbb\\x98W\\x9d\\xbd%\\x07\\xc1\\xbc\\xab\\x0e;\\xbd\\x0b\\xa4\\xcc8>&<;\\x9dP\\x06=\\xdf)\\xf4\\xbc\\x1f\\x96\\x0b=\\x92\\xbd\\xf5<\\xe0j\\x83<\\xa6Z\\xb4\\xbc\\x03vC=F\\xc4\\xbe\\xbcp\\x83/\\xbd\\xd7\\x82\\x98\\xbdc\\xb0\\xc7<\\x13\\xb5\\xcc<\\xbd\\xec\\xa9\\xbc\\xec\\xc5m\\xbd\\x8f$\\x88<\\xb2\\xb2M=\\x97\\x07\\xea\\xbc-FH\\xbd\\x8e\\xa50\\xba\\x07\\x8a%=\\xed\\x93\\x94\\xbc\\xa3\\x97\\x1f=\\x0cs\\xe7\\xbcf\\xc2\\xab<\\x9b\\xe3O\\xbdm\\xbf\\xf4\\xbb\\x88uA\\xbc:B\\xf3\\xbc\\xe3\\xeb\\x96=\\xe8\\xbc.\\xbd\\xebA\\xdf\\xbc\\xa5\\xfb\\x80\\xbd*n[=\\x8b\\xc7\\xb8<\\x9a\\xe9\\x92;x}Y={\\xa6h<\\xce\\xed\\x87;\\xcf\\x17\\xb5<\\xd4\\x050=O\\xb4\\x15\\xbcz\\x0fL\\xbd2\\x04\\x9a\\xbc\\xa7\\x86\\x8c\\xbc\\xdbW\\xa0\\xbd\\xd8&\\n=\\x9b\\x82\\xb9<\\xf5\\xf0\\xb2\\xbc<\\xb4\\x19<\\x8bRM=~\\x89\\\"\\xbd\\x86\\x80,=\\x1dG\\xcf\\xbcs_F\\xbd\\xad\\x00\\xec<\\x16d\\x16\\xbd\\x99\\xe9\\x0f<\\xf6\\xe8\\x93\\xbc\\x89C\\x0e\\xbd\\xc2\\x11\\x9e<\\x81\\xc7[\\xbd\\xd7\\xc4E\\xbc\\xdd\\x06\\xe5\\xbc\\xdd\\x90w=\\xcb\\x05\\xde9\\xc80W\\xbd\\x93\\x18\\\"=s\\\"\\xb8=\\x10\\xc5\\xa5\\xbc\\xec\\x18\\x80\\xbd\\x1f\\xd3\\x00\\xbe\\xb8\\xfc\\x86<&\\xc8\\x11\\xbd\\x7f@\\x13=\\x17\\xa8M\\xbb\\xd8\\xdd>\\xbb\\x7f\\x8dQ=;\\x91\\xda\\xbc\\xff;W\\xbb\\\"\\xd1{\\xbbf\\x1f\\x07=\\x0c7$\\xbd\\xb5\\xa4z<(5E\\xbd\\xb8\\x89\\x87=\\x11\\xb0\\xd3<\\xd6\\xdd\\x03=\\xa4\\x0b\\x97\\xbc\\xaaL9<3\\xd9,\\xbc\\xdb\\x1ak\\xbd\\xd8\\x00\\x84<\\xc9\\xf5\\xab\\xbdmm\\xdf\\xbc3\\n\\x99<\\x04\\xaeA\\xbc\\xe8\\xde\\xf5<\\tX\\x15\\xbd%eC\\xbc/K\\\"\\xbd\\xe0\\x03\\x8e<d\\x84\\x82=\\xa4t\\xd2:\\xb7\\xc5\\x04=\\xe3\\xbd4<M\\xd5\\xd7<#\\x93\\x00\\xbd\\xcf\\xca4\\xba\\x13\\x1e(<c\\xa7\\xdd;\\x0c\\x99\\xb3<\\xc1\\xbd\\x80\\xbcr\\x1c\\xa4<>\\xa7t=\\r\\x9c\\x1d=\\xef\\x8e\\xed;\\xc5;U\\xbd\\x0cR\\xfc9w*-=\\xfdk\\xb6\\xbd,\\xe3\\x90\\xbc\\xbc\\xa4^=\\x1e9q\\xbdoY\\x80=]\\xf6\\x14\\xbd!_1\\xbc\\x0c\\xd3\\xb4<\\xf6\\xd6e=7e\\x9e\\xbb\\x06\\xf6\\x1f\\xbd\\x82\\ra\\xbd\\x0b\\xb07:&\\x9c\\x1d\\xbc\\xb4\\xe2\\xff=0\\xa0*\\xbd\\xa9\\xbe\\xa0\\xbb\\xb6G\\x8a=\\xbc\\n%\\xbc\\xae\\x1d(\\xbc\\x1d\\x8a\\x11<\\\"\\xd27\\xbd\\x12wz=x\\xb1\\xfc\\xbc5\\x80\\xa0\\xbc\\xe2\\xa4u\\xbc\\xfdb,\\xbch\\xf4*=\\x86\\xfb3\\xbdSY\\xab\\xbcm\\xae\\xe9<b#\\x91\\xbd\\xf9\\\\\\xd7<r\\x11\\x00=\\xcb\\xb2\\xef\\xbc\\x81\\xc6\\xda;l\\x92\\x01\\xbc[\\xc5\\x94\\xbdv9\\x1a\\xba\\x9e\\x1a]\\xbcj\\xcb\\r\\xbdI\\x07\\xb0=x\\xa3R<gT=\\xbc\\xc4\\x9a\\n\\xbd\\xff\\xb9\\xa4;\\xecA\\x867\\x14\\x9c\\xd0<\\xca#\\x01\\xbdp\\xf3\\x1b=\\xbe\\x05\\xb6\\xbbpJ\\x8f=J}\\x90\\xbb\\x1a\\xb3P<\\x90\\x1d\\xbe:k\\xfb\\x99<L>K<\\xe1\\x17+=J3\\x16<\\x0c\\x9fZ\\xbd/\\xe8\\x8b<\\x89\\xf2\\x02\\xbd\\xe3\\x8f\\x85\\xbcE\\xb8l;\\xe0\\xaf\\x01\\xbd\\xf6\\xdf\\x81\\xbc\\x1e\\x08\\xed= 5@<\\x18d\\x01<\\xac\\xbd\\x80\\xbc\\xff\\x04[\\xbd\\xb2\\xbaG=m\\x0fT\\xbc\\xdd`\\xa4<\\xa9\\x82\\x08\\xbd9\\xa8\\x12\\xbd\\xd9\\xaf\\xe6\\xbd\\xdc36=\\x1ay\\x12\\xbd\\x88\\xc2;<\\x00\\xfa\\x0e\\xbd\\xc7\\xe7Z\\xbdy\\xf0\\xe8;\\xd1\\x7f\\xca\\xb9Q\\xc1\\x87\\xbco*3\\xbc$\\xc9^<-\\x1f\\xfb<\\x05Ue\\xbc\\t\\x9br<\\xe7\\x0e\\xb3\\xbc&\\xe8\\xb1=\\xfaf\\x99\\xba\\x04S\\xfa\\xbb\\x0b6\\xec<\\xd2=\\x0b=\\xbaja<\\xe0\\x95,<\\xba\\x8b\\xa6\\xbd\\x91\\x1e-\\xbd\\xe08s\\xbbx\\x02Z=\\xf6\\xac\\xfd\\xbb2~\\x8a\\xbc\\x15b&\\xbd}N\\xb3\\xbb\\xf3w\\xfd\\xba\\x97\\x18\\x0e\\xbd8L\\xca\\xbd\\x93\\xb8T\\xbc\\xd6\\xa1\\\"<|5P\\xbcA\\xfb+=IF\\x0b=4\\xc4\\x0e=\\xd1m\\xab;\\x0e\\xc3f=m\\x9c)\\xbc\\xc9\\x93\\x11\\xbdr\\xd7i;\\x03\\x89\\x08\\xbaQi\\xa3<\\\"v5\\xbd\\x89\\x06S\\xbc2C\\x05=\\x1b\\xdd\\xbf8\\xce\\x1ar\\xbdi\\xcc\\xfa\\xbc\\x96\\xec\\xe2<\\\\\\xf6\\xfe\\xbc\\xd1S\\xf8\\xbc\\xdf\\xc4 \\xbc\\xae\\x8c$\\xbc\\x9f\\x1a\\x91\\xbd\\xa5\\x89\\xa9\\xbc\\xaa\\xb5\\x1c\\xbc\\n\\xe7\\x10\\xbdhr\\xfe<:\\xe0\\xd6\\xbc\\xdb;\\x84\\xbd\\xae\\x8e\\xb2<\\x84v\\xae=\\x15\\x9aj\\xbc\\xc0\\xb5\\x9f\\xbaT\\xa0\\xa6\\xbbX\\xac\\x9b\\xbdR\\xfd%=\\x17\\xeeT=DK\\x96\\xbd\\x1e~\\x87\\xbb\\xb4\\xb9\\x1c\\xbc5\\xb1\\xa4<\\x85w\\xa5=\\xf7\\xc9G\\xbb\\x92p\\xf8\\xbc\\xa45\\x1a=\\xe1<}\\xbcH\\xcb\\\\\\xbc\\x1e\\x9e\\x10=\\x93\\x154=7/\\x8a\\xbc2FS=\\tN\\x11\\xba7s\\x02=0\\x0e2<A\\x8eP\\xbd\\xbc\\x8c\\x86\\xbd=\\x11\\xf3<\\xedz,\\xb9i \\x12\\xbd\\xf4\\nq\\xbbT\\xce\\x9c\\xbc\\xab\\x02\\x0b=-|k\\xbce\\xca\\x17\\xbbi)\\x91<\\x9c\\xe4\\x8d=\\xbb\\xdd<\\xbco#\\x84\\xbc\\xeb]\\x90<\\x13\\xf1\\x11\\xbd\\x04\\xbf\\xec:\\xdf\\x87\\xae\\xbbh \\x05\\xbd\\x10XY=\\xe9\\xab\\x0e=\\xff\\x9f\\x15=\\xe8\\x8f\\x05\\xbcG\\xb7\\x10=\\xdbU\\t=)p\\xa7\\xbcyX\\xc0;x8\\x9b\\xbcD\\x83\\x8d\\xbc\\xce\\x1e\\xbe<k\\xa1 \\xbdM\\xaf\\xef\\xbc5M\\x95<#\\x95\\x13;\\x8e?\\x15\\xbd\\x87[\\x88\\xbd:\\xb6\\xb1\\xbc\\xba\\xaaw\\xbd$\\x1e\\xde\\xbc\\xe9\\x15W\\xbcl.6\\xbc\\xe1\\x98\\xb2\\xbc^\\xf8\\x07\\xbd\\x05\\xe6{=Y\\\"\\xc1=;w\\x12\\xbb\\xbc\\x1c\\x7f=\\x92\\xcc\\xe9\\xb9\\xcb\\xfc\\n<\\x89\\x8b[=!\\xba\\xbf\\xbc\\xa2{n\\xbc$O^\\xbc@K\\xe4<\\xd1\\xd6?\\xbc\\xc9I)<\\x1f\\xbd\\\"\\xbd\\x07\\xd8e\\xbbP\\x95`\\xbb\\xaaF\\x10=\\xc9\\xe8\\x01\\xbd\\xbfv\\xd9:\\xbfyN=l\\x12\\x02<`\\xd7\\xbf<\\xeb\\xbe\\xe7:}\\xc4\\x98\\xbcz\\x89\\xdf<\\x8bl<<\\xb8\\xf8\\xd1\\xbd\\xf9\\x84\\x8e<p\\x05d;$\\x01\\xde\\xbc\\x87\\x9b\\xf0<\\x8a\\xae\\xa7<<\\x19t\\xbb\\xbb\\xcb\\xb2=!\\x08\\xfc;\\xed\\xa8`\\t\\xf4}7\\xb95\\xc51\\xbb\\x19d\\xff<\\x04FH=\\x9b\\xa2\\x17;\\x01\\x8d\\xb3<\\x045\\xda\\xbcv\\x1d\\xc4\\xbc\\xb2O\\x1b\\xbc\\x1dU\\xb1\\xbcxC\\xb2;m|\\xec\\xbbl\\x0c+;F\\xa8\\xee<P\\xb5\\xa1\\xbc\\xb4\\xcb\\xfd<\\xde\\xb7\\x82<\\x1f\\xf1\\x17\\xbc>r%\\xbc*A\\x10=\\x866.<\\x02\\xd7\\xf1\\xbcxI>\\xbc\\xe1\\x8f|\\xbd\\x8a\\xdf\\x06=\\x0bP\\xe4<#\\x9b:<=\\xed \\xbcY3s9a\\x85\\xf0:\\xfe\\x0f\\xd0<\\xd6\\xf7|\\xbd\\x1e\\x8e\\xa0\\xbc\\xef\\xed\\xbc:\\xaa\\xbe\\xfe\\xbb\\n\\x13\\xe8;\\x86\\xba\\xd5\\xbc\\xa5>\\xb1<\\xa6e\\x92<\\x8b\\x04\\xac<\\xe6\\x86\\x9c<\\x11\\xf4\\x7f\\xbcYM\\x97;}\\xea\\xa4<\\x01\\x1b\\x11=NB\\xb4\\xbah?\\x1e=-\\x84\\xc0;\\x9e\\xfe\\x03\\xbc,\\xb3T\\xbc\\xd1*K\\xbd\\xf3\\xe3\\xa2\\xbb/G%=\\xd3\\xdd\\xd6\\xbb\\x05,\\xec\\xbc.~\\x82<\\x85\\xdf\\xad<\\x88X\\xad<\\x1e\\x97m\\xbc8\\xfa\\xd1<j\\xfbq\\xbb\\\"Rj=\\x01-\\n<P\\xf2G=\\xa7Ds\\xbd\\xd5\\x03\\xfb\\xbc\\x02\\x18\\x94\\xbd\\x0c_2<\\xba{W=x\\x15\\t\\xbd\\xc6\\xd6\\x1b=\\xb9R~\\xbc$\\xd0\\x8f\\xbc\\xb7\\xbb\\x02=/]\\x19=`\\xb96=\\xbf}\\\\;x\\xe7\\xb8=}\\xe7\\xe9\\xbc\\xa4\\x9d\\x1a=\\xa6#\\xb3\\xbb\\xc6k\\xdc<0}\\xcb<#\\x83A=;\\xf1\\xb9\\xbc\\xc1\\x01~<\\xa2\\x99\\xf5<\\x10\\xc7V=\\xednz\\xbc\\xc0j|<\\xaf\\x1a=\\xbc6\\xc9\\xba\\xbc5\\x98\\x05\\xbc\\xfc\\xa7\\x01:r\\xa0\\x08=\\x03\\xa7T<\\xd5\\xa6H\\xbd\\xe5\\x16\\x1e=\\xb9\\xb6\\x1d;\\xa4\\xed\\x84<\\x84\\xc36\\xbdj\\x8d\\x10\\xbd\\xec\\x02\\x94<\\xf9Y\\xb1\\xbc\\xaa\\xf2\\\\=\\xf9\\x0e\\\"\\xbc(\\x8fm=\\x7f$\\xf1\\xbcz\\xb7\\xb5\\xbc=\\xc1\\xa5\\xba\\xd1\\n\\xf3:\\x00\\xb0\\xf3<<F\\xbd\\xbck\\x99\\xb5=\\x19\\xea\\x02=4\\xe1\\x8a\\xbbd%\\x8e=KP\\xeb<N\\x01e\\xbd\\xe0\\xffB\\xbd\\xca\\xb2\\x95<\\xa8N\\xcb\\xbc7\\xfe\\xa1<\\xfd6\\xa7<\\xbb)\\xc5\\xbc\\x9d\\xb2m:\\r\\xad,\\xbd\\x9b\\xcbO\\xbbH\\x83\\xce=\\xd5?x\\xbd\\xf5\\x90\\x10\\xbc\\xa7\\xae!\\xbd\\xbb\\xad\\xb3=\\xbc\\xde\\x8c;l\\xae\\x1a=j4\\x88<\\x1fH\\x12=T\\xd8(\\xbdt(\\x19;P(\\x06\\xbd\\x01\\xdd\\x9d\\xbb\\xddR\\xf5\\xbc\\xd8\\xd3\\x8a\\xbd\\xe4\\xf7\\xcb<?\\xa5\\x88\\xbc6\\x1b\\x0c=\\x8c\\xe1|\\xbcD\\xcf\\x8c:\\x1a\\x05\\x1c<\\xab\\x9c\\xa0\\xba^\\xf2\\xbb<\\x01\\xcc\\x18\\xbc\\xc8\\xcd\\x8a\\xbc}\\xfb\\x93<\\x1f\\r\\xb8<\\xef[Y<?\\xd0\\xd3\\xbcd\\r\\x82<\\x84\\x90\\x8b\\xbb\\xb9^\\xe3\\xbc\\xf6\\x96\\xd6\\xbcD[w=\\x7fy\\n<\\xd0\\xba/\\xbcU\\xd0\\x92\\xbc\\x0f5\\xba\\xbc)rK\\xbdY\\xdbl\\xbc$\\xf8\\xa5=k\\x9a\\x92<4#\\x11=\\x85\\xb6\\xa1;\\xea\\xc9U=\\\"\\x8ek\\xbc<\\\\\\xb3;`\\xb7-=\\x95\\x8f\\xd5\\xbcU~\\n=\\xf0\\xad\\x9d<R\\xd4\\xac\\xbc.xY=\\x9e\\xf6\\x85\\xbd.\\x93\\x1e\\xba\\x12E\\xc9\\xbcU\\x06\\xaf;|_\\x04\\xbd\\xfe\\x85~<\\xc8{\\xda;|\\xc1\\xc6=b\\xe0\\x1d\\xbdx\\xc1\\xfb<\\x83Vo<1\\x80H\\xbc\\x13f\\x0b\\xbc\\x05\\xeaj\\xbc\\x9d\\xf6\\xba\\xbb)\\xb02\\xbdW\\xb3\\\"<\\x13\\xbf|<Yi\\x96\\xbb.kH\\xbd\\x94D\\x13=\\xa1\\xc5z\\xbce\\xd3Y\\xbd\\xe4u\\xe5\\xbc\\x1a\\xe1\\t\\xbdP\\xc4\\xd6\\xbadt\\x81\\xbd\\xc2\\x9d\\x8e<h>\\xc8\\xbc\\xa3\\xa5\\xbb<\\x1d\\xdaI;\\x03j(=/\\x0e\\xbf\\xbc\\x17\\x07\\xfb\\xbcY\\xbd:\\xbd\\x93\\xd1\\x1b<\\xe6\\xd6\\x11\\xbc\\xaf\\x13\\xbb\\xbcA\\xe6\\x1f\\xbd\\x9e\\xbf\\xf5<nq\\\"\\xbd\\x04\\x8d\\xef<>\\x05\\xb5=@\\x0e^=\\xcaqp\\xbc\\n|\\xa5<\\xa8\\xf0\\x9d\\xbb\\xc7A\\xa2\\xbc\\xbe:\\x82=\\xed\\xd2#<\\x16h*<4\\x88(\\xbc8\\x94\\x16\\xbcD\\x04T\\xbdd\\xb2Z\\xba\\xf1%\\xae<\\x015\\x1a\\xbd\\xb8\\xd9<=,\\\"\\xea<\\xc5]\\x06=\\xbc-E=7W\\xa5<\\n\\x83r\\xbb\\x81\\xf5O\\xbdV?\\x16\\xbc1\\x8a\\x0f\\xbcEl\\xa1<\\xcf;\\xa9\\xbd\\t\\xfd\\x8d\\xbc\\xce\\x9a%\\xbc\\xed%\\xb6\\xbc\\xc0c5\\xbb\\xe4\\x927=\\xd3\\xa0\\x1f\\xbd\\x9e\\x015<\\xebs\\xc6\\xbd\\xc5\\xda\\xf6\\xbbEZ\\xdb\\xbc\\xb3\\x02F\\xbd\\xffr\\xda\\xbb\\xa4\\xf9\\xf5\\xbbuXA=\\xe7q\\xdf<\\x01\\x15[=\\x88\\x06u<\\xae\\x0e.<\\x84D\\x1a=\\x82\\\"\\x1f\\xbd,?L\\xbc\\xa3\\xcf\\xbb\\xbc\\x8c\\xa8\\xb6\\xbb\\xbd^$\\xbd\"\nHSET bikes:10006  model 'Quaoar' brand 'ScramBikes' price 3689 type 'Commuter bikes' material 'carbon' weight 12.8 description 'This bike is a great option for anyone who just wants a bike to get about on The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\xd03\\xd1;\\x1a\\xc8\\xee<2L\\xc9\\xbc5\\xc5\\x03=\\xed[\\x90<&|\\xd4<\\xe2\\xc0\\x03\\xbd\\x84%\\\\\\xbdU\\x00\\x89=G\\x9b\\xda<\\xee\\xc6\\x83<\\xddJ\\x00=\\xb7,\\\"=\\x8e9\\x99;\\xd6\\xe1\\xba=t\\xab>\\xbc9+\\x88\\xbcPO8<o\\x1b\\x1b\\xbd\\\"\\xb0\\t\\xbcB<\\xdf<!<\\x08\\xbd\\x16\\x17L=@\\x8a\\x13\\xbd\\x8d\\xaa\\x9c<\\x93o\\x1c=\\xb0\\xc8\\x8d<A\\xc7\\xe4\\xba`\\xe9\\xaf\\xba\\xd9\\x13f<\\xa6s\\x9b\\xbc\\x8fZ<\\xbb2$}\\xba6S\\xa5<w\\x99\\x17=m\\xac\\xc4<-\\x82r\\xba\\xc2\\xe32\\xbdT\\xa4\\x02<\\x0f<V\\xbc\\x06Fz=lCq<Y8n=}\\x8e\\x97;\\xa6\\xeb\\xc7;\\tp*\\xbd+\\xb8\\xda=\\xe4\\x16&\\xbd\\xd0\\x9e\\x15\\xbd&F\\x87\\xbc\\xa6\\xcc`\\xbd\\xe7\\x84i\\xbd_\\xecO;m\\xcb\\xbb<\\xcee\\x82<1l\\xaf\\xbb=,\\xce\\xba\\x97\\xba\\x84\\xbdF\\x8a7\\xbd\\xaf\\x8d\\xa8=\\x1al0=)f\\x14=\\x18V\\x18=7\\x84\\r=+\\xdd`<\\x93\\xf1\\x1a; \\xfd\\xf4\\xbc\\xcc\\xe5\\x04=\\xe0p\\xdb\\xbb^\\xb5\\x95<\\x95\\xd0|\\xbbIz\\xcb<\\x0b=D\\xbd\\xcc\\x1d\\xa1<\\xf5\\x99=\\xbc\\xff\\xce\\xa5\\xbc]\\xfc\\x1d<\\xc3\\xbaa\\xbdl\\xf4\\xdf\\xbcc\\xbb\\xbe\\xbc\\xd1\\xaf\\xfa<J\\xc4V\\xbd\\x8c\\xe1=\\xbc\\x1f\\xfa\\xa3\\xbc\\xf7\\x9d\\xdb\\xbcB\\xc2\\x01<`\\x1f!\\xbc!L\\xe7<z\\x99(=|g\\xb9<(>\\xbf\\xbc\\xa1\\xf3N\\xbd\\xdeC\\xa8\\xbc\\xd2t\\x81\\xbcz\\xc8\\xff<\\x01\\xb0\\x83<\\x87\\x87\\x8f\\xbc\\xaeEx\\xbd\\xaf_g;\\xdcSa\\xbd\\n\\x95[<]Qh<\\xf3\\xf5Q<%q=\\xbd/`\\x909\\x0b\\x91\\xa8=p\\xb5[\\xbaV\\xb5=:;\\xb5\\xc1\\xbc\\x03\\xd9I<6\\xd4\\xff<\\x81\\x7f><\\x0e\\xaf\\x0c<\\xc4LM\\xbd\\xefl\\n\\xbd\\x83\\x851\\xbd\\xfb/\\x8c\\xbc\\xc8]\\xcf\\xbb\\x1a\\xd5\\xe0<b*\\xdf\\xbb\\x96\\xa3\\xfe;\\x87\\x9f\\xb5<?Y+=L\\x8c\\xe8\\xbb\\x1el7=\\xbc\\xde?\\xbd\\x1a\\xc3\\x9e;\\x14\\x86>\\xbd\\xe9\\x98M\\xbdZ}0\\xbd\\x82\\x0b\\xf3\\xbc*\\x19*\\xbd\\xb55%<|\\xf3\\xda\\xbc1\\xb8\\x06=\\xde4F=/\\xdeq\\xbc0}\\xfd\\xbc\\xffw\\xfd;S\\xbc\\xbb\\xbc\\xe9\\xf4\\xf0\\xbc?\\xd5\\x0f\\xbd\\xbaQ\\x11=;\\xfc\\xb2=kA\\x8c9\\x8d\\xcf\\x83\\xbbgd\\t\\xbc\\x92\\n\\xe2<\\xcb\\xba\\xa6\\xbc\\xbc\\xe3\\xaf<z\\xa1\\xb0\\xbb*\\xf6|=\\x91\\x84+\\xbd6\\xa5\\x1a=\\xa1\\x16\\xc6\\xbc\\xa7~o\\xbd\\x97\\x9b\\x8d:\\xc9\\x99\\x90\\xbc\\x10\\x1e\\xa4<Gw\\x93\\xbd\\x18\\xcc\\x81=\\xfa\\xa4\\xeb\\xbc2s\\xd8\\xbc\\xcc\\xfa\\xf0\\xbcv^(\\xbc\\xe2g\\xce;\\xb7~Y\\xbc\\xb3\\xdc\\xe3<\\xa2\\xccN=\\xd3X\\x8a\\xbcC;.\\xbd\\x93\\x1b\\x15=\\xaas\\x90<\\x9fA\\x11\\xbd\\xf5w\\xcc\\xbc\\x9f\\x80\\xac\\xbc\\xda\\xb6T\\xbd]\\x8dJ=\\x8fy\\xb6:\\x1c\\x96\\xe4\\xbb\\x13\\x08W\\xbcep\\x0e<\\xc2(\\x8c\\xbc;\\x93\\xc9<\\xebz>=\\xd7u\\xbe<\\x8ad&=^,\\x9f\\xbc\\xa9\\xe8\\x02=k]\\x03\\xbd\\x03\\xe4\\xc2<|ja<\\xdd\\x14$\\xbc\\x0e\\xd8\\x17<\\xcc\\x19\\xe8\\xba\\xfa\\xd5F=\\xf1\\rY\\xbd\\xa0\\xb53\\xbdD\\xb1\\xd9\\xbd\\x8e#:=g\\xebn\\xbb\\xa3\\xa2A\\xbd\\xc5\\x97\\xa2\\xbds\\x0c\\xdb\\xbc\\x82\\x0e\\x0e\\xbd4\\xc7&<\\xfa$\\xcc9*W\\xf0\\xbc\\xf9\\x1a7=\\x95\\xe6\\x18\\xbaP\\x06\\xbb\\xbca\\x82m<p\\x93\\xcd<\\xd0\\xf6\\x13\\xbd\\xae\\xe1,\\xbc\\x86\\xd7\\xed:\\xacp\\x00:\\x9f\\x9a+\\xbd\\x97=\\x95<\\xc1\\xd8`\\xbd\\x1e\\xaf\\x93;\\xc3\\xb8\\x9a<:xi\\xbco\\xbb\\x1c<=\\x9c\\x1b;\\xd9\\xecd\\xbd]%S<\\xbf\\x83\\x11\\xbc\\x86&.=\\x9d~\\x89\\xbd\\xa0\\x8f\\x12\\xbd\\xd3B\\x83\\xbdr\\xcea\\xbcUsZ=\\xc8Z\\x94\\xbb\\x94\\xdc\\x99\\xbcR4`<\\xb8\\x81\\xf7<\\xbf\\xa9\\xea\\xbck\\xf6\\xfe<\\x99\\x86d\\xbd\\xc41\\xe9\\xbb\\x1f\\x86/=rt\\x8e<\\xfd\\xa2\\xa8<\\\"S\\x93=w,\\x03<\\xc1z\\xbc\\xbb\\xfe@\\xbf\\xbc\\x19\\xd4\\\"=(r\\\":\\r\\x91g\\xbd\\xb5\\x00\\x04\\xbcL]~=\\x1f\\x99\\x8e\\xbd\\xe5\\xac\\x05=\\x07\\xa4\\xf9\\xbc_I1\\xbc\\xd02\\x8a\\xba\\x92\\xed\\x82=\\xb9\\xaey\\xbc5\\xa7\\x91\\xbc\\xfa\\x01\\xf0\\xbcy8u\\xbc\\xea\\xb2\\xb9\\xbc\\xba\\\"\\xf0=\\xa6M\\x9b\\xbc\\x83\\xba4\\xbd\\x1c 7=\\xaam\\xa5\\xbcw\\x8f><=\\xfd\\x04\\xbc\\xf5\\x86\\xd7\\xbcjs\\xc1\\xbb\\xe8r\\xd9\\xba\\xb2\\x0f\\x92;\\xd7O^\\xbcJK\\x99\\xbc\\x80#+=\\x15\\x90\\x94\\xbd\\r\\xfa\\x82\\xbd\\xea\\xe0\\xcb<\\x85\\xdef;tT\\xe4<c\\xe8\\xb9;;\\x93\\x89:\\xd5\\xca\\xbd\\xbc\\xbc\\xfa\\xff<O\\xe0\\xb0\\xbd|d\\x07;\\x0ec\\xb7<\\x19ei\\xbc\\xa3\\xeb\\xa3=x\\xe5\\x8c\\xbc\\xd1gc<\\xeeu\\x0b\\xbd\\xe9\\x19\\xb3;\\x8e\\xde\\x14=\\x9c\\x9f\\x1e=\\xba\\x8d:\\xbc\\xc7\\xaa\\xd8<\\xce\\x9c`=r\\xe8\\x81<B\\xed\\xe4\\xbc8_\\x18=\\x8d\\x02\\x8a=rp\\x0f\\xbd\\x99\\xb4\\x03;\\xff\\xf7\\xa5=\\x86,\\xec;\\xfc\\x1d\\x1b\\xbd\\n\\x85\\xca\\xbc\\x14\\x93\\xd8\\xbb\\x93\\x93C\\xbd\\xf11\\xc2<i\\xd4\\x08\\xbdi<\\xca\\xbcf\\x1bz=\\xcb\\x0eU=\\xbeD\\x9e9\\x17G\\x15\\xbdc\\xe6\\x01\\xbd\\x1a\\xf4\\xa3\\xbct]+\\xbdbsZ<a(C\\xbdY8\\x83<\\x16pc\\xbd\\xc1\\xf1\\x1a<\\xae\\xadU\\xbc\\xbd\\xa0\\x0e= \\xeb\\xa7\\xbd\\t&M\\xbcS\\xf9\\xd9\\xbc/\\xf6\\xa9<\\x9a\\x9d\\x81\\xbc,-\\xaa\\xbd\\xa8Iq=s\\xef\\x02=\\x00\\xc7\\xdb;\\xacA\\xb7\\xbc\\x86cB\\xbc\\x02\\xa6\\xb3=j.F<\\xd7\\xbd4\\xbc\\xeb\\x14\\x83;<\\x08_=\\xc1\\x8cS=\\\\\\n\\xb4<(\\xfb\\xaf;\\xef\\x00\\xbd\\xbc\\xf97\\x17\\xbd\\xa0\\x15I=a\\xfb\\xc9\\xbc\\xa7\\xd7:\\xbd\\x82\\x02\\x1a=\\x80\\x11\\xc4<Yb\\xfd\\xbc\\x8d\\xd4\\x84\\xbd\\x10\\x16\\xf4\\xbd\\x8d\\xc8\\x18;B\\xc4\\xb2<\\x9e\\x005\\xbd\\x14\\x1b\\x81=V\\x8dA=\\x9bl\\x16<\\x83Q/<\\xac*\\xc4\\xbb\\xa3]S\\xbd\\xcfW\\xf6\\xbc\\xd5\\x15\\xaa=\\x0f\\x10\\x92\\xbb\\xffr\\x82\\xbcw\\x7f\\\\\\xbc\\x81\\x0e\\x96;P\\xab==\\xc6\\x11\\x86\\xbc\\xf9\\xd3\\x10\\xbd\\xe6e-\\xbd\\x87\\x1a6\\xbc\\x06\\x05\\xcd\\xbc>\\xde\\x94\\xbd\\x98\\x87\\x05\\xbd\\xee\\xcc\\xe0\\xbbs\\xc4Z\\xbdf\\t-=\\xaa\\x18~\\xbd \\xf1G\\xbd\\xa2\\xc69=\\x1aB\\xaf\\xbc\\xe3\\x85\\x89\\xbd(\\x82\\xb1=\\xd7\\xf7\\x88=Et\\x92\\xbc\\xaa\\x83\\xad<8\\x1a\\x83<\\xf6+\\x80\\xbd\\xd0`\\x8c\\xbb\\x8dS$=Vr\\x96\\xbd~q\\x0f;\\xc3l\\xec\\xbcJO\\xff<\\xda\\xb3\\x91<\\xb6$\\x02\\xbb\\xb0\\xd5\\x02\\xbc\\xa2\\xc3\\t=\\xf9\\xca\\x7f\\xbc\\xda\\x18\\x81\\xbb\\x8a\\x80\\xb2<\\xc6\\x04\\xfa<\\x0f\\xa7J<\\xdb\\x96\\xf7;g\\x8d\\xd68y\\xe8\\x98<\\xbf6K=Z\\xbet\\xbc\\xb9\\xdbI\\xbd\\xeaj*<\\xf3\\x17\\xdf<7 \\xd8:\\x98\\x13\\xd5\\xbc\\xa2w\\x02=\\x1d\\xf0$=,\\x05\\x16<\\xa4(\\xa5\\xbc)2Z\\xbd\\xa9\\xbb@=\\x15\\xfb\\xef\\xbb\\xb9j\\xb6\\xbc\\xa8K\\xa8;\\x1d \\xc3\\xbcZ\\x9f\\x1c\\xbc\\x8bJ\\xea\\xbca\\xef\\x13\\xbd\\xcct\\xa3=\\xa9\\xc8\\x10=\\xe3A\\xfb<4\\xf3\\xb3;\\xb6Jh=\\x97\\x10\\x7f\\xbc\\x91g\\xbc\\xbc\\x0cIt\\xbc\\xd89\\x8d<\\xbb\\xbf\\xe5\\xb9\\xb4%\\xbe<\\xb9{\\x82\\xbc\\xf4w\\xed\\xbb\\x8c\\xcf*\\xbd\\xbf\\x85\\x07=e4-\\xbd!\\xb2\\x9d\\xbd%\\xdbn\\xbd\\xc5\\xc6~\\xbd\\x17.D=##\\x81\\xbc\\xf0[\\x97<v\\xba\\x98\\xbc\\x0f\\xd3\\xe4\\xbcNZ\\xa4=\\xc9\\xd3\\xac=Df\\x14\\xbd\\xd7\\xc3k={E\\x91\\xbc\\xad\\x05\\x05=\\xaa\\xd5\\x83=\\xb5k?<)P\\xe5\\xbc\\xcc\\xea\\x12=+\\x02`<\\x19\\x8aB\\xbc\\xcf:\\xaa<\\xdf\\xbd\\xbb;\\x9e\\xad\\x19\\xbc7d\\r=\\xed\\xc5\\xf4<\\x88\\xa4\\x1f\\xb9T\\x87\\xe6;\\xa1s\\\";\\xba\\xcc\\xd4;yi\\xa3<\\xced\\xb2<\\x9ck\\x17\\xbd\\xacv\\xdf<\\xdb\\xb1p\\xbc\\xd0v\\xe3\\xbd\\x88\\x96{<\\xcc\\xf7z\\xbcM\\xd8P<kQp=\\xa5\\x074\\xbdt\\xa7o\\xb8?\\xdd\\x86=\\x11\\x10\\xb2<\\xee&W\\t#\\xb9\\x96\\xbc\\xd2\\x86\\xcb\\xbc1\\xa0x\\xbbr\\xfa\\xba<\\xd7\\xa0\\x0c\\xbc\\xba\\x06O=\\xa0\\xd9\\x15\\xbc\\xf4\\xce=\\xbd\\xf5d\\xf9<\\xc6\\xe6\\x88\\xbc\\x11@\\x8d;,\\x88\\x1b=\\xd0\\x1c\\x94\\xbb\\xe2\\xf9L=\\xd1\\x14U\\xbc\\x1f\\tA<8\\x93\\x06\\xbd\\xe4\\xdd\\xbb<\\xec\\x9d&\\xbc\\\"y,=\\x15\\xb3\\xee<C#\\xeb\\xbcg\\x14\\xc6<k\\xa7$<\\xd9\\x15\\x99;\\x1d\\xf8\\x1b<\\x8ed\\xf4<\\xcdT\\x99\\xbb\\x9d\\xe4\\xe4\\xbc\\x12?)<\\xad\\x0f\\xb0\\xbbg\\xa2\\x02<%p:\\xbc\\xd9\\xf2\\x91\\xbd\\xc4w\\n\\xbd\\xf8(\\xe8:]t\\x1b=\\xbb(\\x84:\\x82\\xef\\xa2==Y\\xe1\\xbb\\xf4]d<\\xb5\\x00\\x1f=T51=\\xf7\\x14\\xfc<\\x1c\\x08*=kg\\xee<>\\x18\\xce=7^\\x88\\xbbs\\xb5\\x8a\\xbc\\x87gd\\xbd|\\xd5\\x9f\\xbcFp\\x15\\xbc\\x8c<\\x14<ja\\xa4:\\xfe\\xa7\\xa7;=\\n\\xc3<\\xd2BT=F !=\\xfa-\\xa8:O\\xc9\\xeb<\\x88\\xc5\\xd49\\xc4k\\x999\\xbfo\\\\\\xbbr\\\"\\xce<8\\xf1\\x8f\\xbd\\x94\\xb4P\\xbd\\xf4\\x85.\\xbd\\xec{\\xc8\\xbc\\x7fZ\\x91<s\\xd0j<Y00=\\xb8\\xdc\\xb9\\xbc\\xc9\\xad?<A^\\xf5<\\xfc\\x1a\\x00=-\\xfb\\x82\\xbc\\x12c\\x94<\\x8a\\xbc6=R\\\"9\\xbd\\xc2V!=\\xa7\\x91\\x01\\xbd\\xeb)\\x93\\xb9\\xfb,C\\xbdZ\\xa7q=7\\xad\\xf4\\xbc\\t\\xb5\\x89<\\xfei\\x91=F9==^r\\xaf\\xbbvZ8=\\xfc\\x91%\\xbd\\x94r\\xea<\\x8b\\\"\\xb7=`8\\xae:|<g=\\x13A+=\\x02\\x08q\\xbd\\x95G\\xad<\\xf96\\xee\\xbb\\x99\\x9e\\x00=\\xd7l\\xaa\\xbc\\x11\\xa6\\xdd\\xbc\\xeb*0<\\x8dm]\\xbd\\xabQ\\xa3<\\x9d\\xcfd\\xbc\\xc3\\xd3q=\\x9d\\xadq\\xbc\\xaa\\xec\\xa0\\xbd\\xd8\\x96\\x1b\\xbc\\x15\\xb42;aP+\\xbc\\xb3\\xb4e\\xbd\\x87\\xfe\\xb8=\\xa6&\\x14=z\\x8fv<\\xe5\\xc7\\x05=\\x1f-\\xed\\xbc\\xc6\\xa8\\x89\\xbb\\xb1\\xb0\\xb7\\xbd\\x15\\xd5\\xc7\\xba\\x8a\\x91\\xcc<\\xe1\\xbaG\\xbc\\x00\\x12 <\\xfc\\xfb\\xfa;\\xaf\\xcd\\x92<\\x9bk\\x85;\\xfe\\x99\\x85<\\xed\\x1bj<6\\x0el\\xbd\\xb0\\xc5\\x08\\xbd\\xddJc\\xbd\\xa0\\xabg=*\\xdf!\\xbcx\\xf7\\xa7=\\x91\\x97\\x1f;\\xa3E!<\\xc6+\\xa8\\xbdUvx\\xbc`\\x16\\xc1<q}C<\\x06\\x1f<\\xbd\\x88\\r\\xbc\\xbaAC\\xc5\\xbc\\xe9\\x14\\xa58\\xae\\xa4\\xdd\\xbc\\xc3e\\xa5\\xbc\\xd8\\xba\\x84\\xba\\xb8\\x9a\\xe6<\\xe1\\xa5\\x99\\xbb#*\\\"\\xbcEmz<\\xfe\\x11#\\xbbt\\x9e\\xed<kH]<y~\\xb0<}\\\\\\x1e\\xbcP\\xaf\\xc8=\\xb6\\xd4N:Q4\\x94<\\xdf\\x86\\t\\xbdW\\xc0\\xb6<\\xe5h\\x85<\\xf9\\xc8_\\xbb5\\x19\\x8d;\\x01\\xd3#\\xbd\\x07\\x1f_\\xbdS\\xdfI\\xbc<\\\\\\x03=Sb\\xa6<\\x16Hw\\xba\\xc1s\\xc7\\xbc+e5=\\x04\\xa4\\x1f\\xbd\\xac\\xf5\\x0c<\\x97d\\x96=n\\xd7\\xe8\\xbb\\xad\\x9b\\xfc<D\\xe5a<\\x03\\x10\\x82\\xbbx\\xc0\\xc5\\xbd\\xc43L\\xbd\\x15p0=\\xe0\\xf4\\x13\\xbd\\x87\\xc3\\x83\\xbc\\xcc\\\\\\x05\\xbd,$\\x1d\\xbc\\x98&\\x1c\\xbc\\x95\\xa6L=\\x81\\x1d\\\"<\\xae/\\xe7;\\xef\\x8c\\x9c<D9\\x80\\xbd\\xb1\\x92\\x92<\\\\\\xb38\\xbd\\xff\\xb6\\x1f\\xbd\\xe2\\xbe\\x81\\xbb\\xb1`\\xf6\\xbc\\xfb6 \\xbd\\xadd\\x17<T\\x1a*\\xbb\\xdb\\x05y<),\\xca\\xbc\\xf8\\xb0\\xb1\\xbd\\x18\\x0c\\xf8\\xbc\\x11@\\x08;\\x932\\xa6<\\xbf\\xf4\\xb7\\xbb\\x02Z\\x92\\xbcq\\xee\\x9a\\xbc0>\\xaa<\\xba}M\\xbca\\xd0\\x89=\\xbdrB\\xbaY\\x9bK\\xbc\\x84a\\xf6\\xba\\xba\\x1d\\xc2<}%\\xa2\\xbc\\xe7a~\\xbd7=\\x9d\\xbd\\xae.\\xf2;]!\\x06\\xbd\\x03\\xe6\\x95;\\x1b\\xb6\\x83=\\xd8\\x14\\xa8=\\x16\\x81i\\xbd:\\xc6\\x03<\\x11\\xa4\\xcb\\xbbg30\\xbd\\xa2\\xe0\\x14\\xbbH\\xe2\\x91<Rp\\x85<4\\xc6\\x0e\\xbb\\xcc>\\x90;{6\\xeb<\\x01\\xb1\\t\\xbd\\xc4c\\x18<\\xa3\\xd7\\xb5\\xbb{\\xc3\\x85\\xbc\\xba\\xcd\\t=\\xaf\\x0f\\x0b<\\x1f\\xe3.<B\\xed\\xa1<<m\\xe8<\\xc0\\xb9#\\xbc\\xb1\\x8e =\\x8cD\\x11\\xbd\\xb2g\\x85<\\xfe\\x15N\\xbdh\\x036<\\x84=B<\\x10\\xf7\\x84\\xbc\\xa8)\\x8d\\xbc\\xb0\\x147=WyI\\xbc\\xca\\xd3\\x11=\\xa2\\x05\\xb0\\xbc\\xcb\\x1dG<\\x8cGd\\xbc\\x12\\xe5]\\xbd\\xc7\\xd0\\x84;{\\x82\\x92<V\\xe8\\xa6<8\\xac\\xe2<F\\xe7\\xaa=\\xe0\\xdf\\xa6\\xbc\\x04\\x91O=\\xf42J=\\xa0\\xd7|\\xbd\\x87\\x89\\x03=\\xec\\x18A\\xbdQP1=\\x99z]\\xbc\"\nHSET bikes:10007  model 'Enceladus' brand 'Eva' price 2655 type 'Mountain bikes' material 'aluminium' weight 10.5 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"u\\xdd\\xbd<=\\x9f\\x9d<\\x08L/\\xbdZ\\xeaQ=\\xaee~<\\xae\\x0c\\x9b=\\xc11o\\xbb+i\\x81\\xbd;3l=\\xf2\\xc3W<\\xcf\\x1cO:\\xf5];=\\xfbw\\xe7<ZX\\xa5\\xbb\\x85\\x0c\\xa0=>j6\\xbdeK\\x88\\xbb\\xc4\\x14\\x8e\\xbc\\x95\\xfcK;\\xaf\\xd0\\xd5\\xbci\\x9e\\x9a;\\x88a\\x06\\xbdl\\x18\\x95\\xbb\\xf8-\\xd8\\xbc\\xfb\\xef\\xa3:\\xcc\\x81F:\\xef\\x98\\xb2<\\x8b\\x08\\xb3\\xbb@8P\\xbd\\xf72;==\\x00[\\xbc\\x7f\\x99.\\xbc\\x9b\\xa7\\x97<Q\\xe5\\xf8<\\xf6\\x04\\t=\\xd6\\xdc\\xdb:\\x96:k<\\xa0m\\xb8\\xbc\\xf9\\x91\\xf5;\\xe6{V\\xba\\\\\\xef\\x86=\\xc4\\xb2n\\xbc0\\xe7\\x8b<\\x03\\x8e\\xb5<\\x11\\x85\\x9c\\xbb\\xab\\xf6^\\xbcW\\xbc\\x8b=\\xfc\\xa9+\\xbd&\\x08\\xd3\\xbc{\\xe6\\x9f\\xbc\\xd1m\\xa7\\xbcf:=\\xbd\\x99\\xf0w\\xb9\\xd4}\\x0c\\xbc\\xab\\x0e{<\\xc86\\x97\\xbc4\\xab8;\\xf1\\r\\xab\\xbd@\\x8a[\\xbd\\xc8,\\x9b=\\xb3\\x8d\\xea<c\\xd7\\x89\\xbb~\\x90\\xb7<\\x89N\\x98=\\rl&=~\\xab\\xd5<(\\xa2\\x92:\\x7f\\xa36;\\x1e\\x7f\\xb0<^_\\\\\\xbc\\x89\\x99y\\xbcZ\\xce\\xa9\\xbck\\xf0\\x11\\xbd\\x953k\\xb9\\xc9\\xfc\\x10\\xbd\\x01\\x88>\\xbc\\xae\\x87\\x1f=\\xba\\xa2\\xc7\\xbc\\x02#Z\\xbd\\x92\\xcf\\x90<r\\x00\\x02=\\xb0\\xc3\\x92\\xbdc\\xa7\\xed\\xbc:ke\\xbd\\xcf\\xff\\xf4\\xbb\\x9b\\xf8\\xd2<\\x8e\\x8di;\\x83\\x87Z=\\xd9\\xa8\\x1f=c\\xd4\\x16=\\xeb\\xafs\\xbc\\xe1<\\x12\\xbd\\x15\\xed\\x11\\xbd#\\x1f\\xe2<\\xea\\xcf\\x85<\\xee\\xb1-=b\\xe8\\xac\\xbc \\xbc\\xde\\xbcT\\xf8\\x88<\\xc9\\x96P\\xbdo\\r\\xee\\xbc\\x1e\\xce\\x80<\\xaeb\\xd3<\\x06\\xab\\x12\\xbd\\xd9\\xd8\\xa0\\xbc\\x87N\\x9f=\\x01\\xef\\x9c<\\xcc\\xb1l\\xbc\\x0e\\xd6\\x1c\\xbc\\xf6\\x08\\xf2<\\x12pk<\\x15z9=\\x1e\\x12n\\xbc\\xbf\\xc5\\x13\\xbd\\xd0:W\\xbd\\xc6\\xa7\\x1c;k\\xe9\\x04\\xbb\\xcd\\x92l;\\xe4ib<\\x87\\xe1U\\xbd`02<\\xa7\\xb8\\x11=(\\xa66\\xbc6\\xe0\\xe6\\xb9\\x12\\xbf\\xe6<m\\x1b\\xa3\\xbc\\x8fMt\\xbc\\xdfO\\x9c\\xbcHJ\\xc4\\xbc\\xc9\\\\\\x85\\xbd\\xca\\xc0\\xd5\\xbc%\\xe8\\x1c\\xbd\\x05\\x00\\x9b<p\\xc6\\\"\\xbd\\xb9\\xc0\\xae:\\xe4#==\\x83p\\x84\\xbc\\xba\\xe4N<\\xa7\\x8d\\xe4;\\xcajE\\xbd>\\xc0G\\xbdu\\x93\\x82\\xbd\\xa1C\\xcf<\\xf46\\x96=@\\x16\\x8c\\xbc{\\xff\\xe2\\xbc\\x83\\x19\\x9a;\\xef9\\x18\\xbdv\\x9f~\\xbb\\xe90\\x1f\\xbdb\\x12\\x05\\xbde\\xdbh=\\xea\\xb3\\xd6\\xbc\\rn\\xf5<\\xf3\\xbf\\x90\\xbd\\xad\\x9c\\x16\\xbd|T\\x0e\\xbc\\x84\\xe6B<w\\xca\\xdf<\\xc8\\xdd\\x96\\xbd\\xa1\\xb9\\xbc=\\xae\\xe3\\xa3\\xbc\\x06\\xe0/\\xbcmWw\\xbd\\xe1\\x16q<\\xe5\\x07\\x8f<KX\\xcb\\xbc5\\x93\\x1b=h)c=\\xa6\\x16[\\xba\\xf8\\x01\\x9d\\xbc\\xf2\\xd4h=\\xf6\\x9f\\xe0\\xb9\\xf0\\xfdQ\\xbc\\xc2N\\xce\\xbc\\xa2\\x1d\\x13=P\\x96\\x99\\xbdDcG=\\xc7\\xa5\\xbb<R\\x0fi\\xbcMK\\xba\\xbb71A=\\xc0^N\\xbch#\\xb9\\xbaq\\x05D=\\x05\\xc1\\xbb;\\xef!{=\\xe7\\x96><\\x9b\\xc1\\x99<\\xb1\\xd4?\\xbd\\xd6\\x1b\\x99;Z\\xb9a=*x\\x08\\xbd9\\xd8\\xe7<\\xb7\\xc6\\x12<\\xf1y\\xcd<\\xf8cb\\xbdi\\xc5\\x80\\xbd\\xf9\\xdeP\\xbdm\\xf3d=\\x99\\x14\\x1d\\xbd\\x9dr\\\"\\xbd\\xd1\\\\\\xb9\\xbd\\xce;@\\xbc\\xaft\\x98<J\\xecV\\xbc%\\x1c\\xd7\\xbc\\xb7~\\xce\\xbc,v\\x8a<\\x1fl\\n\\xbcR\\x1e+\\xbc<d\\xe6\\xba\\xaf\\x12\\xc4\\xbb\\x99\\n\\x06\\xbd\\x02\\x9fA\\xbd\\xda/_\\xbc\\x07\\xf7\\x85;\\x00\\xd7\\xb5\\xbc\\xc8\\x00\\x14=\\xd4\\x17\\xc2\\xbdp\\x13\\xc5<hO\\xad\\xbcmh\\xfa\\xbc\\xbb?\\xe3;\\xe0\\xc7\\x94<\\xed\\x1bm\\xbdt\\x81\\xe7<\\x9b\\xd7\\x13<\\xd2qc=\\xf8\\xad\\xa0\\xbdo\\x1a\\x02\\xbd\\xa1E=\\xbd\\x9c[\\xa4<\\x9as\\xdf<\\xd2\\x0c\\xe0\\xbb+\\xf3\\xb1;\\xf8*\\x1f=\\xb0\\x12><\\xffQ\\x8e\\xbct\\xbd\\xd9;\\x18\\xcc\\xec\\xbc\\xa0k\\xa6\\xbb\\xbf\\xdbt=\\x98\\t\\xa6<0\\xf4\\xd8<\\r\\xed*=\\xb9)A=\\x9f\\xe3@;V\\x8a\\x17\\xbd\\x88{\\xff\\xba\\x89\\x92\\x83<\\nJU\\xbd\\xfdRD;\\xf9\\x9c\\xa4=\\x8b0\\xa2\\xbd\\xd6X\\xff<\\x8f\\xbf\\x86;0Gc\\xbc\\xabR\\x1e\\xbd\\x8d\\x13\\x84=f\\x96\\xe4;L\\xcb\\xce\\xbc\\xf8/J\\xbd_\\x99\\xe9\\xbc\\xdeE\\x10\\xbdP\\xe6\\xcd=\\xbe\\xf46;\\xd2\\x0e,\\xbcI\\xb8\\\\=\\x01e\\xbd;\\x7ff\\x9c<\\xfc\\xef\\x1c<:\\x18\\x08\\xbd\\rM\\xb6<\\xba\\xd2\\x15\\xbd\\xa6=\\x85\\xbc\\x04\\xe2\\xaf\\xba\\xa7\\xf2d<\\x96\\x9d\\xb6;FK\\xb5\\xbd\\xe2\\x0e\\xf1\\xbc\\x98:\\xe4<\\xb9)\\x8b\\xbc1/\\x01<t\\xaa\\xf5:\\x94\\x9aR\\xbcz(D;\\xea\\xaa\\xe1< \\xde\\xa3\\xbd\\xad\\xbcy<\\xaa|\\x9d;\\x8bl\\x17\\xbd\\x02EO=\\xafux\\xbb\\x0e\\x8c\\x8a<\\xfc\\xc20\\xbdR!\\xdb<\\xf0\\xbc\\xed;\\xc8\\x91\\x82\\xbbE!5\\xbc\\xe2@]<\\xeb\\x15\\xdf<\\xfe\\\\l=\\x05\\xc6\\x1f\\xbdf\\x14:=n\\xc8\\n=\\x91\\xd8&\\xbc-\\x089=+\\x02\\xa3=;\\xe3\\xd2<\\x95j\\xfe\\xbcf\\xd2\\xa4\\xbcN\\xcf\\xb0\\xbc\\xb3_\\x1e\\xbd\\xb2R\\x91<\\xe1\\xba\\xa5\\xbc\\xc4\\x96\\xa0\\xbc\\xb9F\\xa3=g9\\x92<\\xeb\\xb8\\xb9<\\x98^<\\xbclV\\xf8\\xbcO\\x8b3\\xbc8\\xc5\\xa4\\xbc\\xe2\\x1b^;M%N\\xbd\\x84\\xde\\x97<\\xee\\x96\\xf5\\xbc1\\xf4{<W\\xd6\\x13\\xbc\\xdei\\x0e\\xbd\\xb8\\xee<\\xbd\\x81\\x11\\x0e\\xbc\\x85\\xe5\\x93\\xbby\\xf2\\xfa;.ee\\xb9\\xdb\\x03L\\xbd\\xd7i\\xf2<\\xbdp\\xc7<_y\\x81\\xba44\\xfb\\xbc\\xb6\\xeaX;\\xfc\\xc9t=A\\t\\xae\\xbb\\x99\\xbd\\x12\\xbbZ\\xadr<\\xa1\\x92>=H\\x86:=\\xe1?k=\\x03\\xcb{9D\\xfa\\xa1\\xbcI\\x82|\\xbc\\xaa\\x81U=\\xfcq\\x86\\xbc\\xb3\\x96L\\xbd\\xa6\\x8a.\\xbcK\\r\\xd0<\\xaf}&\\xbd\\xce\\x94B\\xbd\\xbb\\x96\\xd1\\xbd\\x87\\xfc\\x16<a\\x88\\xcf<\\xf7a\\xfb\\xba\\xdf?\\xb6=#\\xa1\\x85=\\x06\\xbc\\x8e\\xbc\\xf2\\xb6\\x12\\xbd}\\xed\\x90\\xbc\\xb39\\x8b\\xbc\\x01\\xc4\\x01\\xbd\\x1bC\\x9e=\\xb1a\\xa2\\xbc\\xb6\\xd6+\\xbc\\x87\\xb9`<\\x86\\xe2B<^\\xfe3=K\\xab\\xf6\\xb9\\x9c\\xbfS\\xbd\\x16\\xc8\\x18\\xbc\\x04\\xe3\\xeb\\xbb\\x06\\xbe\\x1e\\xbd\\xcdNS\\xbd\\xab\\x92\\x15\\xbc\\x12\\xa6\\xa8:\\x04GW\\xbd#\\xd4E<\\xa4\\x88\\x08<\\x8bi\\x8a\\xbc\\xc7\\xed\\x01=\\xee\\xb7\\xc0\\xbc\\xb6j\\xa3\\xbd\\xe1\\xb5\\x83=\\xc9=\\xba=\\x05\\xd9\\xc9\\xbc!\\xb1\\x07\\xbcthN<jm\\x8c\\xbdw\\x1cD=\\xb0\\xb6S=kF\\x82\\xbd\\x98\\xe0\\x9d;\\x9e\\xe6a\\xbd#s =\\xe28\\xae<\\xcb\\x06n<}\\xda\\xbd\\xbc\\xd7\\x95C=\\xbb$\\x9b;b\\xbee<\\xd11==\\xb4\\xe8T<\\x7f{\\x9c<b\\xc1\\x0e=3\\x16\\x1d\\xbd\\x11K\\xac<\\xda\\t7=\\xc6\\xa3\\x9b<\\\"\\xd9\\x96\\xbd 0\\x85\\xbc4t\\xf6;\\xaec\\xec\\xbba\\xe7o\\xbcl\\xc1}<\\x03c\\xcc<lC\\xa3<\\x8e4\\xe4\\xbc\\x90M\\x15\\xbc7>\\x15=|\\xdb\\xd1\\xbbV\\xd3\\x85\\xbc\\x1b\\x9b\\xf0\\xb8\\x1bB5\\xbc`( \\xbd\\xf9\\xa7`\\xbd\\xcby\\xdf\\xbcA~\\x8c=ak\\xae<_\\x87\\xbc<\\xbf,\\xc6<\\\\\\x7f\\xef<\\x08\\x8b\\x9a\\xbci\\x1a\\n\\xbd\\xfa2\\xc5\\xbc\\xe9\\xe1\\xba\\xbb\\xc5/\\x1f\\xbcw)\\x10=\\x13\\x16i;E\\xfb9<\\xc7\\x98S\\xbd\\xa5\\x97\\xd7<S\\x81\\x96\\xbd\\x0f\\xfeM\\xbd\\xd2\\xcc\\xe5\\xbc,\\x7fb\\xbd.\\xc3I=\\xa8\\xec\\xe7<\\x80\\n\\xcf\\xba\\x96t \\xbc\\x04\\xdbA;\\x01\\xef\\xb8=\\xf6\\xdb*=\\x96\\xec\\xa5\\xbc\\n\\xa3Z=\\x8d\\xd8-\\xbd\\xac\\xa9V<\\xa0\\xa2\\xc9<\\xe0\\r\\xac<L\\xe9S\\xbd\\xca\\xd3\\x0c\\xbc\\xf02\\xda<\\xc2/\\xd2;\\x8c\\xf8\\xde<\\xadY+\\xbb\\\\Z\\x00\\xbc\\xbe\\xf80<\\xcf\\xf3\\x9a<\\xc9:+<\\x01xI<\\x08XW\\xbcz\\xc0\\xa8\\xbcw\\xee\\xb6<\\xe9\\x9d\\xc9\\xbb\\xd3\\x87\\x89\\xbcyM\\\\<\\xfc4\\x10\\xbb=\\xd7\\xd2\\xbd:j\\xa7\\xbcq8\\x89\\xbc\\xa0\\x90\\x05\\xba\\xbb\\xa0~<1)\\x12\\xbd\\xec\\x0e\\x8d<iq==\\xe3\\\"\\x89:\\xeftE\\tIn\\xcb;<\\xd6\\x08\\xbd\\x98\\xa0\\xc9<\\x82,A<\\xf1\\xd0\\xf2;\\x91\\x02\\x81=\\xc2I\\xed\\xbb\\xe8j\\x0e:z\\x9f\\xf6<\\x83\\xe9\\xd3\\xbc\\x8c\\xb7\\xc7<\\xc3\\xc3\\xc9<Y\\xbe\\x10\\xbd\\x06\\x1cC=\\xf7\\x89!\\xbc\\xc3l\\xea<\\xbdWb\\xbdLx\\xb4\\xba3B[<\\xd8\\x8a\\x81=\\xdbY\\x8d<\\x99d\\x99\\xbc\\x82\\xfc\\x00\\xbdt\\xd9\\xb6\\xba}\\xc3\\xb5<L\\xcc\\xda;0\\xa1\\xf7\\xbb\\xe4\\x92\\xe1\\xb8\\xf75-\\xbc\\xb1\\xbd\\xb0\\xbb\\xb4\\xeb\\x8f<,\\xa2D;\\tL(\\xbd\\xf5\\x8a\\x82\\xbdq\\xeeG\\xbdy\\x7f>\\xbc\\xed\\x07\\x99<\\xdej6<\\xaf\\x94\\x84=\\x92\\xe9\\x93<\\xe0h\\xd2\\xbcf\\xc7\\xd0\\xbc?\\x9b\\xd0<\\x10\\xf9\\xb1\\xbc\\x1bVo=\\xfd\\xcbk<4\\xf1\\xa3=4/\\x9d\\xbbOV4\\xbdB9\\x9d\\xbc C\\x84\\xbd\\xa1\\xae\\x92\\xbd\\xde\\x94K=\\xc8[o<\\xa7\\x12=<BY,<I\\xa8\\xaa\\xbcO\\xef}=\\x96w><\\x18\\xf8Y=\\xe5\\x17F<\\xd4\\xca\\xde\\xbb\\x08\\xd3\\xb1<-\\x0e\\x93<x\\x98~<\\xba\\x87\\x7f\\xbd\\x9b\\x0b>\\xbdR\\xd2G;p.\\x06=\\xe4\\xd5l\\xbb\\x82T\\x18=\\xba5B\\xbci6\\xa7\\xbc\\x12r\\xe0<\\xb92J=\\xd5rg\\xbd\\x8c\\x0c\\x14\\xbd!T\\x1d=\\xa2T:\\xbd\\xbb\\xc1Z=\\x1d\\xb8\\x85\\xbc\\x04\\xaa\\\";=,(\\xbc\\x91\\xff7=\\xc5\\x97\\xaf\\xbc\\xb0\\xb3\\xb9<Ma\\x12=4\\x9c\\x81=-p\\xaa\\xbc\\xca\\x8fl< \\xa9\\x86\\xbc\\xdcLD<g\\x14T=\\xad\\xa0T\\xb9\\xfb\\x87,=]65=\\xacd\\x0e\\xbd\\xbd\\xc3\\x03\\xbd\\xbf\\xaa\\xb9\\xbc\\x86.\\xad<\\xefk\\xa2\\xbb>\\x84\\x1e<\\x04|\\x19\\xbb\\xd753\\xbd8\\xb3\\x00=@|\\x98\\xbc\\xabC4<\\xa4^/\\xbb\\xd8\\x01\\xc9\\xbd\\x1c\\xcb\\x1f\\xbda\\xdc\\x02=U\\x17\\xd3\\xbc\\x93\\xfaz\\xbd\\xfc\\xea\\x85=\\xbcH\\x07>On\\xf7<\\xf9\\xe9\\xbd<\\x83\\xa4\\xcd\\xbcpB\\xdf;\\x014\\xb0\\xbdGp\\xe2<\\xfe\\\"\\xbe<\\xba?\\xcb</\\xd8\\xa9<\\x02\\xd2\\x91=\\xa3\\xed\\x84\\xbb;\\x9e\\xa0;W\\xd5\\x04<\\xce\\x99~=\\xdab\\x80\\xbdX\\xf1\\x87\\xbcG\\xb3\\x87\\xbdv\\xfb\\xa3=!B\\x83:\\xf7N\\xad=\\x92:\\x15<%b\\x95=:\\x05\\x80\\xbd#\\x13\\x8b<\\r\\x0b\\x04=A\\x80\\x12<\\xaeGo\\xbd\\x0e\\xbb\\xe6\\xbc\\x11\\x9b-<!\\xfeU<\\xc8|\\xd3\\xbcz\\n\\xda<~\\x99\\x96\\xbc\\x90\\x1b\\x02=\\x1a\\xfc\\xf8\\xba\\xd4e\\xc1\\xbc\\x93\\x96\\xe2;\\xb91i9\\xfb0\\r=T\\xebB=\\xb1i\\x82<y\\xd7\\x13\\xbd\\xf9\\xadg=\\rC}\\xba\\re\\xb1<G\\x1aJ=\\xeaN\\x9a;\\x0b\\xeb\\x1a;\\x1f\\xa0\\xa4<\\xcb\\xfb\\xb8;\\x83y9\\xbc\\x93\\t\\x1a\\xbd\\xecJ\\x13=`\\xe4\\\"=\\x01\\x84g\\xbb|7\\xc3<5\\x88\\x99\\xbc@\\xa6\\\"\\xbb\\x19\\xe0/<\\x97\\xe93\\xbc\\xd6\\x1c\\xa7=\\xa2\\x8a\\xb6;Qh{<0\\x0b\\xf0;\\xc4*\\x00=Ez\\x8c\\xbd\\x199\\xd6\\xbd\\x0f\\x92\\xcc<]\\xcdx\\xbc \\xbe?\\xbd\\xb2\\\"\\xf5\\xbc\\x1b0\\xca; \\xbb.;\\xff\\xf48=\\xe63\\xc6;\\xd5\\xe8\\x1b\\xbd\\xdej(<O~\\x1b\\xbd\\x11\\xe5\\xb6\\xbc\\xe0\\x96e\\xbd\\xa7`O\\xbd\\xff\\x82\\x86\\xbb\\x82\\xe1\\xaa\\xbc\\x9e\\x1c\\xa3\\xbb2\\xa8\\x04\\xbd\\xc1\\t\\xd6\\xbc~&\\x1e\\xbc\\n(\\x13\\xbdo{\\x9e\\xbdO\\xa3`\\xbb\\x16\\xa9>\\xbc\\xa7\\x02_<+\\x1c\\x97\\xbc\\xa9\\xb4\\xcd\\xbb\\xb8\\xe5U\\xbd\\xc8\\xa4n<M\\xb20<\\xa1U\\xd4<\\x80)\\x9a\\xb8p\\xd6\\xee\\xbc?\\x14\\xe3\\xbb\\xc2\\xfb\\x83<\\xb7\\xde\\xd1\\xbbhHs\\xbd\\xbf\\xc6\\x1f\\xbd>-D\\xbd\\xad\\xf8\\x0e\\xbc\\x08`\\x01<\\xc6\\x08o=\\x0b>\\xb4=z\\xa7\\xa1\\xbb\\xb99\\x03=\\x9f\\xd7F\\xbd\\xcdUR\\xbd|J\\xc0<\\x9aZ=;\\xc7P\\xfb<\\xf5\\x9b\\x97\\xbcFI\\x98\\xbc\\x16|g;j\\xea\\xa7\\xbdZ\\x9b\\xa0\\xba/\\xd9\\xc5\\xbb\\xd0Z\\xe8<\\xdf!\\xd3\\xbcH3M\\xbc\\xa8\\xf2\\xb5<\\xe5\\x87p<\\x03l`<\\xb0$\\x1d\\xbd\\xa8\\x8b\\t=\\x87+P\\xbcH\\xb55<F\\xea\\x03\\xbd\\xea\\xd0n<\\xf0\\xd6R=tN\\x85<\\x90\\xa1\\xe9\\xbbFcj=Mi\\xdc\\xbc|\\x8bC<\\xe6\\x85\\x15\\xbcz\\xc6\\xaf\\xbc\\x0f_\\xa2\\xbc\\xd6\\x02\\xb2\\xbd\\x19[2=\\xa2\\xc5D=9u\\xd3<|\\x8a\\xd9<\\xd8\\xe1\\xc7=g;Y\\xbcf\\xad\\x83=\\xee4\\xf3<\\xf4\\xd3k\\xbcG\\xfcy=\\xc65\\xef\\xbaW\\xa9K=[!\\x87\\xbb\"\nHSET bikes:10008  model 'Iapetus' brand 'Bold bicycles' price 4967 type 'eBikes' material 'alloy' weight 7.0 description 'A city eBike that could double as a short-haul commuter. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"S-\\xaa:\\xbc\\xbc\\xa8<\\x05\\xe6\\x82\\xbc\\x94\\x9cG=\\xcf\\xf4\\xa7:3\\xb7\\x87<\\xde\\xfd^\\xbcg\\xde\\x95\\xbd\\xd2p)=<\\xfb\\x80\\xbcxvH\\xbc\\xa7\\xde\\x85\\xbcr\\x01>=m\\xaa!=XR\\x02=\\xf9\\x99\\x01\\xbd3\\xbd\\xf3;\\x07\\xc4\\x83</X\\xb5\\xbc\\xcc\\x19i\\xbd\\x11\\x1dv\\xbc_\\xaf]\\xbd\\x16}\\x01=]\\xefH\\xbb\\xc3\\xf2\\xa6=ok\\xc8<&\\x9a\\xe7<\\x1bw\\x13<)\\xeb\\x9a\\xbci\\x1aY<\\x98\\x90\\x18\\xbd\\xcc\\xa7+\\xbd\\xbe\\x11[\\xbc\\xc9\\xbc`;\\xf7)8=m=\\x0e\\xbd9\\xfb\\x99<\\xc4\\xc0\\xa5\\xbc\\x95\\x92\\xb2\\xbc\\x9c#g\\xbb5\\x01\\x80=\\xa7H\\x16\\xbc\\xd3z\\xd09\\x1e\\x05/=}-\\x14\\xbb\\xdc\\xb6\\xe8\\xbc\\x06\\xbcn=\\xf9\\x82~:\\xe6\\xc8:\\xbc \\xd2\\xaf<z\\xcd/\\xbd\\x07\\xb3\\x97\\xbc\\x84(\\x19\\xbd;\\x07\\x13\\xbbWG\\n<\\x1b&\\xed\\xbcn\\xd0\\xf8\\xbb\\xc7\\xc5\\xb7\\xbd\\xb8\\x0c\\n\\xbdw\\x92c=\\\"\\xf77=\\x10\\xb0\\xe1\\xbb\\xaf\\xbf\\x07\\xbcK\\xacp=a\\xe4-=\\xd5\\xc8#=\\xef\\xef\\xfb\\xbc99s=\\x8f\\xdf\\xca:\\xbd`\\xbc<\\xfe\\xb4\\xe8\\xbc\\x03\\xf4\\x0b\\xbd\\xdd#\\x1d\\xbc\\xc6;\\xe4\\xbc\\x9d@C\\xbdG\\xf4a=5!s\\xbb(\\x92\\xd4\\xbc\\xe4\\x1b\\x98\\xbd\\xe7\\xdb\\x8f<\\xc37\\x9a;]\\x18\\xf4\\xbcN@\\xe8<v\\xb7\\x8b\\xbcV5\\xfb\\xbch\\xdd\\xf5<\\x7f\\xa6g:\\xd7K\\xd3\\xbc}\\x19W=\\xae\\\\\\x89<\\x87\\xb2\\x81<<%)\\xbd9\\xde\\xc3\\xbc\\xa6\\x11\\x13;\\x12\\xc2\\x86\\xba\\xeb\\x88)\\xbc\\x08\\xbb\\r\\xbc\\x97\\x82&9@\\xaf\\x87\\xbb\\x94 $<s\\xb6\\xff<\\xa5)\\xac<\\xeeI\\x01<-\\xd9\\x99\\xbc\\x1f\\x06\\xce;\\xecs\\xeb=i\\x17\\n=5\\x94\\x0e=~\\x05 \\xbc\\xdf\\x02\\x02=l\\xdf\\xac;\\x14T\\xb4<\\xc3\\x89\\x06<<\\xe8L\\xbd\\xfd\\x9f\\xf7\\xbc\\xd4\\xd7\\x01\\xbd\\xff9S\\xbd^<\\x9e\\xbc\\xc2\\x86L<\\x1cX\\x9e<\\xe2\\xbe\\x00;\\xc7{d<yw\\xc0<Y3E\\xbd\\xe6\\x08\\x82=9o\\x1f<\\x16`\\x84<\\xa5\\x900\\xbd\\xe5N\\x8d\\xbd<\\xec\\xaf\\xbdl\\x86\\xdd:\\xb0\\xd8l\\xbb0\\\\\\xa3<\\xda\\xde\\xd4\\xbc\\xf61\\x88<\\x88\\x8d7=1H\\xde\\xbbB5v\\xbdECA=s\\xce\\xcf\\xbcgR\\xa6\\xbc0+\\x03\\xbdw\\x11z;C,k\\xbc\\xda\\xe0\\xeb\\xbc\\xb1[t\\xbd\\xf8\\xbbR=0 \\xb1<\\xcf\\xbf#\\xbd\\x9dH\\x12\\xbdj\\x96\\xc1<\\xc18f=\\xf9Z\\xd4\\xbc\\x95\\xa4\\x19<M\\x8a!\\xbd\\\"\\xdf7\\xbcg\\xfa)\\xbdm\\xd9\\x1a:a\\xc4\\xbe\\xbc\\x04\\xf5l\\xbb\\x81\\xaex=!/\\xe6\\xbc\\x9a~*<\\x83?\\xc3\\xbc\\xbc\\xfec=\\xbb2\\xb2<\\xf2C&:\\xc3\\xde\\\\=\\xd2\\xdd&\\xbb\\x90\\x8d\\x13\\xbd^Y\\xa1<c\\xfc0=\\xf4\\xbah\\xbcI\\xd7\\\"\\xbd\\xdfa\\xf5\\xbc\\xd5\\xb9\\x82\\xbco\\x9f\\x8e\\xbd\\xadr>=f[9<b\\xc8\\x80\\xbcYQ\\x81=Z \\xd2;s\\xb9/\\xbdKV\\x13=\\\"\\xc8\\\\\\xbc\\xfc\\xd5\\xaf\\xbc70\\xa3=\\xffb\\x9e\\xbc\\x19;\\n\\xbb\\xdb\\xae\\x07\\xbd\\xe8\\x1c\\x08\\xbd\\x1b\\xe0g;\\x9b\\x1d\\x8d\\xbd1\\xe0\\xd0\\xbc\\x9912\\xbd\\xd7\\x16\\x9d=9x\\x94\\xbb@\\x00Z\\xbd\\x01Yk<\\xc7Ts=\\x94\\xc8\\x08<\\xf7XI\\xbd\\x05$\\x04\\xbe\\x81\\xd0\\xfd;\\xcc\\xb2\\x1c\\xbd\\xd1\\xc1\\xb0=\\x9f\\xe5\\xc8\\xba\\xb7d\\xcd;\\t\\x03\\xfb<\\x99\\xcbv\\xbc\\xd4\\xc9s<`\\x17\\x04;x\\xde\\t=\\xe0\\x88\\xe5\\xbc\\xbe\\x07O\\xbc\\x98\\x94\\x15;H-\\xfd<z\\xe6\\xb6<\\xdd3|=\\xf2\\xa8F\\xbc2\\xc3\\x9d\\xbc\\xb3r\\xc0<\\xc7I\\xc2\\xbc\\x10\\x95\\xf4<ii\\xd3\\xbd\\xac\\xa3\\x84\\xbc_\\x08\\xde\\xba9\\x06\\xb9;\\xae@\\xab<\\x07\\x8a!\\xbd\\\"\\xcbo\\xb9\\n\\xfb\\x18\\xbd\\\"jp=\\xa39H=H\\x15\\x12=\\x85\\x1d*=$L\\xf6<\\x91\\x18\\x9c<\\x80h\\x88\\xbb\\x8a\\xc1\\x05<\\xa4\\xfe\\x11=.\\xde\\xca\\xbc\\xcd\\xbc\\xdf\\xbb\\xc4bW;\\x99\\xc3\\x18\\xbc\\xdeg\\x1c=\\x8d\\xce\\x11=\\x05\\xc5\\xb4<\\x0e\\x15\\x06\\xbd\\xd8\\x82\\xa2;b\\xcbs\\xbc\\x99\\x05\\xd3\\xbd\\xe5q\\x8d\\xbci6\\x8e=\\xb0\\xf3(\\xbd\\xb3\\xf6<=\\x90\\xfb)\\xbd5\\x9d\\x05\\xbc\\xc6\\xaa\\xf7<zX\\xb5=\\x02\\xca\\x96<M\\xa0J\\xbd\\xf0\\x9b\\x93\\xbd\\x7f\\xc2#\\xbdn\\xb9\\x12\\xbcR\\x17\\xaa=\\xae\\x86.\\xbd\\xd1b4\\xbb\\xc7r\\x84=\\x9dg\\\"\\xbc\\xc6\\x95\\xdd\\xbbv\\xaa\\x83<+\\xc2=\\xbd\\x96F\\x8a=f\\x1eP\\xbd\\xaf\\x91\\x02\\xbd\\xdeN\\xdd\\xbc1\\x86B\\xbc<\\x7f\\xe2<\\x94Az\\xbd L\\xb9\\xbc\\x05*E=\\x1a\\xdb\\x05\\xbd\\x1b\\x8e\\x93<\\x00\\x8ek=\\xca\\xe6X;t\\xf0\\x89<\\xd9\\xc5\\xc5\\xbavx\\xa6\\xbd\\\"o3\\xbc\\xecJ\\t9L9\\xc1\\xbd\\xa5\\xa6q=W\\xc2!\\xbc\\xb0;\\xf6;\\x15(\\x19\\xbd#\\xcf\\xf0;k\\x9f\\xfb<\\x06\\xb7\\xc7<I%x\\xbc\\xd8\\xcc\\xa2<97j\\xbc\\xd7rc=\\xbc\\xec\\xfa\\xbc\\xf1\\x06\\x02;\\xe3\\xa0\\x11<\\x94\\xae\\xb7\\xbb\\xb1r\\xed;\\xeb\\x07\\x8d=\\x06^o;\\xdbQh\\xbd\\xfc$e\\xbc\\xb0\\xa1\\x96\\xbc\\xe3\\x83?;\\xf7\\x81\\xa0<\\x0b\\x00f:\\xeap\\xec;\\x1a~\\xd4=\\xb8\\xf9\\xb4;\\x19\\xbf`\\xbd\\xa3\\xa6 \\xbd\\x1a\\x1f\\xab\\xbcyJv\\xbc\\x14@H\\xbd\\x8d\\xa6\\x1a<3U&\\xbd\\x9f\\xa6\\\\<\\xac\\x13p\\xbd\\xa3\\xd77=\\xa0\\xa5;\\xbb\\xa5\\x1bH=4\\r\\x10\\xbd\\xe7-\\xa9\\xbda\\x80\\xcc\\xbb\\x97\\xac:<\\xed\\\"\\xeb\\xbc&7\\x13\\xbc\\xff\\xd1\\xa8<a\\xd8j<\\xc1\\xc3&<\\xcc\\x96\\x01<\\x1aOp\\xbcq\\xcc\\x8a=\\xa2\\xe2\\xea\\xbc\\x9bG*\\xbd\\xef/\\xef<\\xf4\\x7fx\\xbc\\x12\\xe4\\xa3;\\x8d\\xe8\\x03=\\x89\\x05u\\xbd\\xfbb\\x17\\xbd\\x015\\xb0\\xbc\\x16\\xbc\\xd9<\\xad\\x19\\xb4\\xbc5\\xe1\\xc4\\xbct69\\xbd\\xeb}\\xcf;\\x9e\\xfbO\\xbb\\x92\\xe2C\\xbdE\\r#\\xbd\\xc5\\xf3\\xf4\\xbb\\x1b\\rs:*\\xb9\\x83\\xbc\\xa6\\xe8\\x84=\\xfd0\\xf0<a\\xb5\\xe9<)\\x13\\xc7;\\xdd\\xd6\\x91=\\xb5\\xa8\\x1c\\xbd\\xff\\x10Z\\xbd\\t\\xe4w<D\\xc46=\\xf1\\x9cL=\\x04n\\x94\\xbd\\xf6\\xc6\\xc3<\\x02`\\xb9<\\xa8\\x05;\\xbcO~\\xe9\\xbc\\\"UL\\xbd\\xec\\xfc.=\\xa0k\\xfa\\xbc@\\xe8\\x1a\\xbd\\xe9\\x8d\\x90<1\\xd4\\x12<\\\"1\\x9c\\xbd5\\x0fL\\xbc\\xc0h@<\\x9a\\xa7\\xf4\\xbc\\xf7\\x19\\x89=\\x94\\xde\\n\\xbd>\\x11$\\xbd\\xcd\\xbd\\x8c<\\xca3\\xa8=\\x8dJM\\xbd\\xe7y\\xf8;)\\x87\\x7f\\xbbj7x\\xbd\\xf2\\x97\\x17=O\\x94\\xc3<\\xe1\\xee\\x97\\xbd\\x8e\\xa38\\xba\\x1c\\xa8\\x18\\xbd\\xba\\x08~=\\x1c\\x0e\\xa9=\\xe3\\xc2\\t\\xbc\\xd3\\xc1\\xc8\\xbc\\x9a\\xa9c=y\\x8c\\x1b<\\x8eR\\t\\xbcv\\xa3\\x18=\\xf4\\x16#=\\xce\\xf5\\x1e<\\r\\x91\\x9d=\\xc3\\xc6r\\xbc_\\x13M<\\x81\\x96V=\\x90\\xe0\\xf7\\xbc\\xcd\\xe8\\xc9\\xbc\\xe1Y*=|\\\"\\x11=\\xb6\\xa6\\xda:Ws\\xce:\\x99k\\x91:\\x0c\\x17\\x00=\\xcc\\x92&=\\xca6\\x96<\\x87\\xc7\\xd1;\\xa4j\\x8f=\\x8b\\x0f\\xc4;\\x0ff*\\xbd1/H<\\xa41\\xd2\\xbc\\xf0\\x87\\xb3;\\x87\\x0b\\x17\\xbc#\\x85P\\xbd\\xba\\xfe\\x94=\\x15\\x93\\xd0<?!\\xdb<\\x1cr\\xc3<\\xf5l\\xd0<=/\\x19=\\xef\\xae\\xd2\\xbc\\xf0I\\xd1\\xbb\\x99?\\xb9\\xbc\\xbaL\\x11\\xbd\\xa1\\xdc\\x92\\xbbx~\\x10\\xbd|\\x98*<\\xfa\\x1a\\xa7:W|9\\xbd\\xb3\\xd2\\xba\\xbc\\xf3^W\\xbd9\\xcc\\xb2\\xbc\\xc62\\xa9\\xbcSB\\x85\\xba0\\x8a\\xc0\\xbc\\xd0\\xb2N;N\\xb7\\xe0\\xbc6V\\x8a:C\\x12\\x81=M#\\x96=wb\\xa3:b\\xb7#=dZ\\x05\\xbblWP\\xbc3=\\x18=\\xff\\xf5-\\xba\\xa4\\t\\xa4\\xbc\\xf0K\\x05<\\x98\\x10\\x12\\xbcU\\r>;*1f=M\\xffK<\\xb5\\\\\\xca\\xbbsV\\xae;\\xf8\\xf9\\xb3<<\\xe0\\xde\\xbbA\\xd6\\x05<f\\xbf\\xa8<\\xde\\xe2\\xb4\\xbc\\x0cC\\x12=N\\xc6\\x95\\xbc\\x8b\\x94&;,5Q=\\xcc\\xeaF\\xbb\\xa6\\xde\\xbc\\xbd\\x10\\xa0\\xe4\\xbc\\xf0)\\xe8\\xbb\\xf15\\x14\\xbdGB`<\\xa9x\\x13=,\\x9aT\\xbc,\\xead=\\xb1\\x08h:\\xadX]\\t\\xc8\\x04^\\xbc\\x91g\\t<|i\\x9c\\xbc\\xe4\\x17\\x06\\xbcj&\\xa3<X0P<O\\xc8\\xc3\\xbc\\x7f\\xe8i\\xbdh\\xa1\\x18<\\xa3o\\xfa\\xbcX\\x94\\xab<\\xa8Pp\\xbb\\x94\\xb9\\xf9\\xbb\\xd3`\\x86=\\xa5;\\x13\\xbd\\x02\\xbf1=\\x06\\xd8\\x1a\\xbd\\x022\\xec<z\\x13\\xb0;\\x8f`\\n==\\xe4\\xc0<Z\\xfcg<\\\"U\\x0f<\\\"\\xbdv\\xbd\\xec[\\xa1<\\x02\\n\\xc8\\xbb4k\\x0f\\xbd\\x8f\\x1f\\x00\\xbb^a\\xf5\\xbcu\\xc5\\xe3<\\xa9\\x8c\\xa3\\xbb.\\x88?\\xbd[\\xb4\\xd8\\xbc\\xe3\\xca\\t\\xbc,v9<\\x90a\\xc3;\\x99\\xf2B:\\xdey\\x03<D\\xa0&=\\xc0\\xe8\\xc4<\\x14[\\xfe<\\xa9%\\t\\xba\\xe2\\x02V\\xbc\\x01x\\xa3<\\xb1^\\x18=\\xdf\\xb55\\xbc\\x99\\xean=^N\\xa2;\\x91\\xbb\\xe4\\xbb9\\xe81\\xbc\\x1ak\\x1b\\xbdM\\xa7\\x03<b\\xc6j<\\xd7\\xbaW<ud\\x86\\xbb\\xaf\\xbcZ=\\x83\\xba\\xb0<g\\x80\\xe7<~\\x8d\\xad\\xbb01\\xc6<\\xb0~\\xb7<N\\x9f\\xd9<\\x80o\\x08=\\xd7)\\xd1<[,\\n\\xbd\\x11\\xab\\xf4\\xbb\\xd6\\xac\\xf1\\xbcO\\x11v\\xbc d0=\\xb8\\xff\\x16\\xbds\\xfa~=1\\xd1\\xcf<#\\xbb\\x9c<\\x0eN\\x12=\\x93\\x98\\r=\\x85\\x1d)=)\\xab(<h\\xc3l=\\xccb.\\xbc)\\x00\\xd3\\xbc1Z\\x9f:|\\x1e\\xea<\\xa9K\\x1c\\xbc\\x96\\t{=\\xf2\\xb5\\x10\\xbc\\\\\\x1b\\x15=\\xc4\\xd18=\\xc2\\x8a7=4I\\x02=A\\x9e\\x88\\xbc\\xec\\x81\\xb9\\xbc\\xe5\\xb6-\\xbc>\\xf4\\xbe<\\r\\xaf\\x01\\xba\\xb8\\xd7\\t<R\\xc8\\xa1<\\xdd\\x95\\xa2\\xbd^\\xd2-\\xbau\\xd5\\x1b\\xbc\\xf2\\x90\\t\\xbd\\x8c\\x97f\\xbd\\xedK\\x7f\\xbd\\xaaf\\xcb;\\xd2:\\x9e\\xbd\\x95\\xf7\\n=\\x03d\\x87<\\x9e\\xbc\\x12=\\xc1\\xce\\x9d:\\xfb@-\\xbdh\\x1f\\x93\\xbc\\x9f\\x0f<<\\x84\\xe66\\xbb>t\\\"\\xbd\\x8c~\\xc8=m\\x0c\\x05\\xbc\\xd7\\x00+=\\x81\\xfdS=\\xdep\\xe6<\\xfc.\\xb4\\xbcv)2\\xbd7\\x15\\xbc\\xbb\\x1a#\\x8e<\\x9eF#=YT\\xa6<5\\x02|\\xbda\\xc3w\\xbc\\x99\\xc0\\xb4\\xbb2\\xd5 =\\xb0\\xbe\\xc6=\\xe7%\\x81\\xbd`~\\xca\\xbc\\x87\\x90)\\xbd2\\x8f\\x81=J4\\x00<\\x07\\x9e%=ar\\xb7<\\xbb2\\x07=\\x94_b\\xbd\\xe6i\\x04=q\\xddB\\xbc\\xd9\\x19\\xca\\xbc@z\\x9f\\xbcI?S\\xbd\\x8b\\x9f`;\\xab\\xf0\\xc8\\xbc\\xdes\\x0f=\\x13.\\xd8\\xbc\\xd6K\\x1c<\\xafN\\xe0\\xbc\\xfe\\xc9\\xab\\xbc\\xdd\\x01\\x12\\xb9\\xb4\\xd5T<\\x08\\xff\\x8f\\xbc\\x0865\\xbc\\xac\\xd2\\xc0;+\\x7f\\xcd;\\xff94\\xbd\\xdc\\xcb\\r=\\x89NX<9\\x157\\xbc\\xb3\\x01K\\xbd\\x8e\\xa9\\x97=f\\xee\\xa6<%\\xe2\\x0f=\\xca\\xf4\\xff\\xbch\\x82Q\\xbc\\x8d?5\\xbd9 \\x18;\\xd6\\x99\\xc5=\\xe3q\\x91\\xbb\\x01\\x93\\t=z\\xf0\\xd8<0p\\x89\\xbbl\\x16\\\\\\xbc\\xd1h;\\xbcid#=f1U;eK\\x9d=_\\xdf\\xb7\\xbc\\x9b\\xa3\\xcb\\xbc\\xb6}\\xc38]\\xf0\\x8d\\xbd\\xff\\xee\\x89\\xba\\xcb\\xd3\\x10\\xbd\\x91t\\x0f\\xbb\\x95$\\xfe\\xbcD`\\x90\\xbc\\x1d\\xe4?\\xbb\\xc7$n=5\\x07\\x12\\xbc.k\\xad<\\xd8\\x0b\\xdd\\xbc\\xbe\\xf2\\x12\\xbdo\\x0e\\x15\\xbdum,\\xbc\\xfd,$\\xbdF+B\\xbd\\x9a\\xde\\xa0:\\xee+\\r=\\xca\\x8a\\x96;[\\xe7\\x84\\xbd\\xee\\xed]=\\x16<1\\xbc\\xc4I1\\xbdk\\xa8\\xa7\\xbc\\xd7\\xcc\\x19\\xbdi\\xeb\\xed<\\xcax\\x00\\xbd\\x05\\x82\\xe8;y\\xbd\\x06\\xbdkF\\xe1;58&<\\xfaL2=\\xbb;\\xc5\\xbc\\x04\\xd3\\xe4\\xbc\\x06\\xb2>\\xbdV\\xf7;=\\xe5\\x00N\\xbd6\\x83\\xe3;\\xb8\\xe1\\x05\\xbd\\xfe\\xec\\xf7<\\x07\\xfcs\\xbd\\xd4\\xd0T=_\\xe19=\\xbay\\x17=\\x9d\\xa8\\x83\\xbcWp\\xe6<\\x99E\\x8c\\xbc\\x83\\xf1\\xab\\xbc=\\xe5m=bZ\\xcd<E\\x8aO\\xbd2\\\"\\x16\\xbdR\\x0f\\x91\\xbb3\\x86\\xa5\\xbc\\x80\\x11Y<\\xe5.\\x8f<G\\xb0\\xd8\\xbc\\xa1\\xe5>=I/\\xec;s\\x80\\x9c\\xb9un\\x0c=\\xa1\\xee\\x1b=\\xc8~\\xe4\\xbc\\x99\\x95\\xcf\\xbc\\xe5\\x8c\\xc5<~\\x1as\\xbdA\\xea\\x8f<\\x85\\xb2\\xc0\\xbd\\xaaG\\x08\\xbd\\xcdq\\xbd;\\xf0\\xab\\xce\\xbc\\x82xD<\\x0bk\\x81=[\\x99\\xae\\xbc*\\x89\\x04=U\\xdd\\xe2\\xbd&\\xbb\\xe4<\\x00\\t\\xe5\\xbcP\\xcf\\xc1\\xbd\\\\\\xdb\\x08\\xbc\\xf1Z\\xde:\\x0c\\\"\\xf3<\\x8ch\\x0f=\\x81\\x9d>=\\x97\\x1bb\\xbc\\xd2\\xfd\\xc8<\\xd6\\x93\\xb4<\\x9ajf\\xbd\\xe9\\x17\\xae\\xbc\\xfd\\tt\\xbc\\x15\\xdf9=\\x89\\xc2=\\xbd\"\nHSET bikes:10009  model 'Mars' brand 'BikeShind' price 4580 type 'Kids bikes' material 'alloy' weight 13.3 description 'The innovative braking system on this bike has been a game changer in the kids’ bike world. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\xc3h\\xbf;\\x94\\xf04\\xbcF|\\x06\\xbd\\x94\\x93\\xc2\\xbbb\\xd7_\\xbdo\\x8a=\\xbbQ\\x15.\\xbd\\xe5\\x8c$\\xbd\\xcau\\x94=\\xc9w\\x16=\\xd0>\\x04\\xbb\\xab\\xdc\\xba\\xbcE\\xac\\xd4<\\xcc\\x9f\\xbd<\\xab)M=6\\xabA\\xbd\\x8cvg=\\xf9\\x9c\\x86\\xbdO\\xfc\\x1a\\xbd\\xe6g\\x1d\\xbd\\xe0\\xd4\\\"=\\x8fX\\x13\\xbd\\x91\\xb4\\x02=\\xf6y\\x99\\xbck\\xe6\\xfa\\xbc\\x13\\xd2P\\xbc?l\\xda<\\x00y\\x9e\\xbbI\\xc0\\xd8\\xbb\\\\ux=f\\x92\\xb6\\xba\\xaf\\x15\\xff\\xbc\\x9b\\x1bI=\\x82\\xecR=F\\x13@\\xbd\\xe1\\xdb*\\xbc4\\xa1\\x1f=\\xed\\x98G\\xbc\\x8eE5\\xbd!\\x8e\\xb5;\\xcd\\xd0\\xc3<\\x1cxH\\xbc\\x82R\\xd9;o\\x9c\\xc6:PG\\xff\\xbbCY>\\xbd\\x8c\\\"\\xf4<\\xb7B\\xbc\\xbc\\x9d\\x9f\\xe6\\xbc\\x1dY\\xf1\\xbb-\\x91\\xd2;W\\xcc\\xa8\\xbcn\\x8f\\xf6\\xbc\\x18\\xd0\\x1b<\\xc8~\\xa7\\xbc\\xb7\\x00k\\xbc\\xe9\\xbe9<*\\xd9\\xea\\xbc\\xd8\\x1a\\xb9\\xbc\\xc6\\\"\\xcc=@\\xfe\\xd2\\xbc\\x88O\\x07=\\x000,;j\\xa4\\xe0<\\x00\\\\\\x84;5\\xe6\\x06\\xbd\\xfc\\\\\\x95<\\xd4i/\\xbb\\x86s\\xce<YO>\\xbc\\xf7[\\xc1\\xba-[\\xa4\\xbb\\x1a\\x7f8\\xbc%1\\x94\\xbd\\xa4\\xc2\\xa9<*\\xca\\x8a\\xbc\\x95\\\"\\xa0\\xbc\\\"E*\\xbd\\xd7(!\\xbd\\x95$y=\\xba\\xb1\\xeb<\\x0e\\xee\\x0f\\xbd\\xa1\\xaa\\xf0\\xbc\\x008\\x1a\\xbd\\xd9\\x03l9<\\x8e\\x0f\\xbd\\xf1\\x1e\\xab\\xbc\\xf4U\\xea<\\x9f\\xf8C\\xbcB\\xc4p<Z\\xe7G\\xbc\\x91\\xfd\\x14<\\x1c{\\x0c\\xbc\\xb4\\x96\\x07\\xbb7\\x89\\x00=\\x98\\xe6\\xa3<\\x1f\\x1a+\\xbd\\x88\\xf1\\x9e\\xbc\\xb8\\xdc\\xe9<\\x92&J\\xbd\\xd6\\x15@\\xbd\\x0f\\x8b\\x8e<\\xad\\x88?:\\x8bv\\x07\\xbd\\xc2\\xef\\x9d<\\xf9]\\x11>e9\\xb5<\\xb7\\xf4\\xe7<\\xfe\\x85\\x8e\\xbc\\x01\\\"\\x80\\xbc\\xba\\xad)<\\xcb\\xc42=!\\x08\\xa1<\\x1d\\x0e\\n\\xbc\\x0c\\x08\\x88\\xbd\\x02\\x1c\\xda\\xbc\\x8d\\x1a\\xfd::\\xd8I<\\xfbu\\x10=\\xab\\xf5C:\\x8a(P=\\xcd\\t\\xb0\\xbb\\x9ad\\x82\\xbd\\x15p\\x07=t\\xad\\x15=\\xdc$\\xc1\\xba\\xb8\\x0fB<\\x12\\x95#\\xbb*\\xbb.\\xbd\\xd1\\x1b\\x08\\xbd\\x82\\xcb\\xc6\\xbc\\xf7|\\xbb\\xbc\\x9ep\\x01=#\\xf7\\xd3\\xbc\\x1c\\xea\\xa9<\\x9f\\xcb\\x15=\\xd4\\xb1\\t\\xbd\\x83\\x1f\\x80\\xbc\\x1c\\x8c\\xb9<:\\xd4\\xd3\\xbc\\xf9\\xea0\\xbd\\xbd\\xb4\\x98\\xbd\\xae}X\\xbakd\\x81=\\x7fE\\xb7\\xbcV\\x87\\xe3<\\x85f\\xbf\\xbc\\xf1}\\x0e=\\xe2\\xcc\\xb7\\xba\\xc8Z\\x8c\\xbc\\xef\\xe6\\x08;\\x8eR\\xa2<\\x7f\\xa1*\\xbb\\x80Z\\xe9<\\x1a\\xfau\\xbc\\x16\\x9b\\x80\\xbct\\x01\\xc5\\xbc]5\\x07<\\x0c}\\xe2<\\\\5s\\xbd\\xb0\\x9f\\\\=\\xa3\\x0bh\\xbb\\xb0\\xcd\\x12\\xbb\\xa3\\xba/\\xbd\\t\\xb4\\xbe;\\x06\\x86\\x16=BQU;\\xc67\\xb7<=j\\x10=mFv\\xbc\\nR\\x83\\xbc\\x95hb=\\x9939\\xbb\\xc9\\x87%\\xbd\\x8d\\xd2\\x17\\xbbl\\xd4\\x05=\\xbb^\\xff\\xbcE\\x83P=0\\xfe[\\xbc\\x05l\\x03<\\xc4\\x86\\xb6\\xbc\\x86\\x17\\xf8;\\xab\\xfc\\x02\\xbc\\xa9\\x83,=\\xa2\\x03\\n=\\xf1_\\x84\\xbd\\t\\xfb\\x8d<\\xb3]\\xc0<\\x86hJ=\\x99M\\xa3:\\xbd~\\x1a=\\xf2\\x9f&=q+\\x8e\\xbd\\x9bY\\x98=\\xc8\\xe2\\x86<\\x1aq\\x84=p6\\xeb\\xb9\\x1a\\xc3\\x8b<\\x19\\xbd\\x82;\\xc6\\xd4\\xaf<\\x93\\xfb\\xd1\\xba\\xc9_g\\xbdL\\x99\\xa4\\xbdH$\\xb2\\xbaog\\x8d\\xbc\\xd6\\xa0\\x19=\\x00_S\\xbdE\\xfe\\xe7\\xbc\\xda;`=\\x9c\\xf8\\x03\\xbd\\xfe\\xd7\\xed\\xbc\\\\.E=\\x9a\\x1fi=rd\\xab<\\x7f\\x07b<\\x0bH\\xbc<\\xa3`\\xda\\xbd\\xb7\\xccy\\xbc\\x8e\\xd8\\x82;y\\x8d9\\xbd\\x99\\x95w;\\xb5\\xf0\\xd2\\xbcaz\\x85\\xbd\\xb6`\\xd0<\\xca0\\r\\xbcA\\xc6\\xa3;\\xf2\\xfbP=FSR\\xbc?\\xb2\\\"=*\\xd2\\xda;dK\\xf5\\xbc\\xd1\\xd17\\xbb\\xd7\\x98]=Xc\\xfc<\\xe4\\x1b9;\\\\\\x99\\xef\\xbc\\x02vo\\xbc\\xf4(\\\"=\\x02\\x98\\xa5<r\\x03\\xaa\\xbbc\\xb2\\xa7<\\xd6\\\"\\n<\\x8a_:=q=1=\\x10\\x97\\x98<\\xa6\\x82d=\\xe2\\xbf\\xcd<aT*<\\x81\\xe2r\\xbc\\xa2A+\\xbcz\\x87<\\xbc#i\\xba\\xbc\\x91\\xe2\\xc4;\\xddx<=\\x01\\\"\\x17<\\xca\\xf1\\xd6=:2\\xe5\\xbcB\\xbf\\x03=\\\\\\xe7\\xb1;\\xd8*\\x85=\\xb0\\xb9\\xb8\\xbc\\xf9z\\xe0\\xbb\\xa3\\xc3w\\xbc\\xdeb\\xc1\\xbc\\xf1[\\x84\\xbdbR\\x9a;K\\xfa\\xb3\\xbc\\xa3\\x02\\x82\\xba\\x87\\xe6\\x90<\\xae\\x16a;x\\xb0\\xbb\\xbb!K\\xcd\\xbc\\xfa\\x08?\\xbd\\xec\\x87\\xf4:\\xa8#\\xd6\\xbc9\\xf9\\xbb\\xbc\\xef\\x96\\xe7<\\x02\\xb9\\x94\\xbc\\x90\\xd2\\xed=l\\xba\\x7f\\xbd\\xf3vs:5\\x04\\x92\\xbcRv\\xf4<\\x1c\\xda\\xb3\\xbc7\\xa7!<\\xe2\\xdcy=\\xcbE\\xd9\\xbc\\xd5\\xaf\\x86=&\\x83\\x04\\xbd\\xc2*\\x89<>\\\"\\x84\\xbcK(I;\\xdb3\\x81=\\x86\\xb1\\xdc\\xbc\\xe5l\\\\<\\xdf\\xe1\\x05\\xbd*\\x8d\\x91;gx\\x08<\\xc1\\xdc\\xe0;w-\\xf4\\xbc\\x85,\\xcc<w\\x0e\\x10\\xbc\\xdf\\xe98<\\xcd&{<r\\xc7\\xb2<\\x98s\\t<\\x9b\\xec=<o N;\\xfa\\xff2=\\xe0\\xb3U\\xba\\xaah\\x19\\xbd\\xa8\\xd97\\xbd\\x83X|\\xbc\\xf1\\xc3z\\xbcj\\xc9\\xcf;W\\x02\\xb8\\xbd\\xd1\\x94\\x17\\xbdc>\\xc1=\\xe1\\xb7\\xd8<Z\\x0b\\xaf\\xbce]\\xfd\\xbc\\xc0\\x9f\\x05\\xbd\\xf3\\xea\\xa0<\\x96\\x91P\\xbcM\\xce\\xc2<\\x97!\\xb5\\xbc\\x12]\\x18=\\xabY\\xa0\\xbc\\xb4?\\x97=\\x1e\\xa1i\\xbc\\xc4d(=-\\xc2\\x98\\xbc_\\x1e\\xff;4M\\xd4\\xbc\\xda\\xd7+=\\xf4v\\x10\\xbc\\x848 \\xbd\\x8bv\\t\\xbbm\\xa1\\x19=3\\xf6\\x0f;\\xd6R\\x1d\\xbd\\x90W\\xc3<E)\\xaf=\\xb7\\x86B<\\xbalz\\xba8\\xf3\\x93<q\\xd40=K\\x02J;\\x98\\x97E=\\x0c\\xfd\\xdc<=\\xef)<\\x7f\\x96u;\\xafp\\xb1=\\xa7\\xd6K:\\x80\\xaa\\x7f\\xbd~\\xe4\\x91\\xbbJY\\\"\\xbd\\xb9M\\x1a\\xbd\\xf5\\x86\\xf6:$\\xca\\xc9\\xbd51\\xcd<\\xbe\\xe6\\xb0<\\xa3$\\x11=?}\\xe4<Cyn;N\\xee\\x97\\xbc\\xd2\\x8f\\xcd\\xbc5ZI<\\xb0\\xbbg\\xbd\\xd1~\\xe0\\xbce{\\xc3<?\\x130<\\xda\\xe2v;,\\xa2\\x0b=/\\xdb\\xca<\\x84\\x9c\\xb1<Z\\xd0[\\xbc\\x0fQ \\xbd`v\\xc8<\\xa3\\xf3\\x95\\xbc%\\x93z\\xbc\\x0b\\xc5N\\xbd\\xdeMH\\xbc\\xea\\xb1\\xeb;\\xe8-0\\xbdF\\xae3=\\xd6~d\\xbc\\xf5}\\xa6<\\xd8\\xaa\\xa9;\\xbe#\\x98\\xbc/*\\x9f\\xbcn\\xc1\\x9c=%\\x15\\xe2=\\x84\\xda \\xbd|\\xcd\\xc7<\\xa1\\xe1\\xfc\\xbc-\\x9e\\x9c\\xbcf\\xdf\\x8b\\xbcN@\\xeb=\\xfb\\xfa\\x90\\xbdb>\\x08\\xbd\\xd5\\x97\\x08\\xbd4\\xbc|=?\\xc4\\xbd;\\xfcCa= m*\\xbd\\x8b\\x98\\x80\\xbc\\x01\\xcfb\\xbb\\\"/\\xa9\\xbc\\xe5\\xbeV<@G1=\\r\\xd9\\xb8\\xbc\\xa2M\\xbe<\\xa5\\x8c7<\\xff\\\"\\xea<\\xa2\\x1d\\xd3\\xbbO\\xa4\\xb7<\\x04\\x18d\\xbd\\xae\\xcc\\x06=\\xf9I\\xfe;\\xaas\\x17\\xbd&e\\x1a\\xbd\\x9d6Z=RB\\x94=\\xb6\\x12\\xbb<!\\xb6t\\xbd\\xff\\xd3s\\xbdM9\\x1b<F\\x8c\\xcc<!U\\\"\\xbd\\x14Y\\xe0:(\\xd6\\\"<\\xe0$s\\xbd-\\x97\\xc2\\xbd\\xd6\\x15\\x81\\xbc\\x9bF\\xb3=Epx=c\\tt<\\xfbw\\x82\\xbd\\x0etB:Xj\\x7f\\xbdgr\\xad\\xbcp6\\xa3<\\x8e2G\\xbc\\x8d\\x85\\x7f<v\\x1eJ<\\xee\\xd3\\xd3<\\x18A\\x07\\xbc\\r\\x07\\x89\\xbdm\\xec\\x8a=\\xe5\\x8d0\\xbd\\\"\\xe8L\\xbd.\\x92\\xf6\\xbbZ\\xbd\\xb6\\xbd\\x9f\\xd8\\xe9<k\\xe9\\xf3\\xba\\xadv\\x827u\\xba\\xb3<4\\xd2\\x0c\\xba\\xb8\\x00\\xe0<BA^=\\x9a\\xbe\\x1d=\\xf4\\x0b\\x10<\\x99x\\x94\\xbd\\x96\\xb5\\x1e\\xbc\\xf1c\\xd9<\\xc0\\xbc\\xfe;;\\xb8q\\xbdd\\x7f\\x10\\xbd\\x98A\\xa7=^e~\\xbcy\\xf66<\\x92\\xe9*\\xbd\\x0f+k\\xba\\xd0z\\x9d<\\x1c\\xd0\\x91<\\xa1t+\\xbd\\xbd\\x80J\\xbd\\xee\\xd9k\\xb9\\xe0\\x01\\x80\\xba\\x04\\xba\\x90\\xba\\xcf\\x1eo\\xbcLU\\xe9\\xb9\\xca-\\xdb;e$\\xd99\\xb0\\x08\\xb6\\xbd\\xe6MD;.;\\xd0<=\\xc3\\x11;\\xf0\\xd8\\x8b<\\x0f\\x1f\\xd8\\xbd\\xd8\\\\\\x93<\\xdf_*<A\\xb8\\xa0\\xbbJ5c\\t\\xcao]\\xbc\\xb4\\x93t\\xbc>\\xa8g<A\\x1d\\x97=1\\xb7>=7\\x8b&=\\xb7\\xb1\\xdd<T\\xfd\\x95\\xbc\\xe3\\xfd\\x95<\\x1c+O\\xbc\\x89\\x0b\\xb4<\\x96\\x0f\\xa7\\xb98Dh;7\\xe0\\x86\\xbb\\x16H;\\xbd\\x8bK\\n;\\xe3M\\x86<\\xc9\\\"\\x18\\xbd\\xc6-\\x15<\\xc23u=h\\x8a\\xce;\\xe8I\\xc4;\\xc1\\xcd/=\\x08\\xd8\\xab\\xba\\x93`\\xad<\\xdf\\x8d\\x89<IH7=\\x955c<adt\\xbdYr\\x00\\xbdX\\xf0\\x9c<J\\xb4?<\\xd0\\x83F\\xbd\\xc6\\x172\\xbd\\xc6\\xcc\\x9e\\xbcz\\x1dJ\\xbdzUS:\\x98\\x85\\x17=.\\xce\\xaa;\\xfe\\xa4d\\xbb{\\\\_;\\x0c\\xcf#\\xbb\\xd7\\x98\\xca<\\x8fY\\x9f:\\x14}\\xb9<?.\\x1f\\xbd\\xed\\xf8\\x96=\\xaa\\xc4\\xcd\\xbc\\xc1\\xfc*\\xbdl\\xb3\\xea<M\\x00\\xc6\\xbcf\\x1f\\xd2\\xbcS\\x8eN\\xbc-\\xf5b<T\\xf8\\r\\xbd\\x1e\\x1f\\x07=\\x08\\x1eI\\xbc\\xff\\xca\\xbf\\xbc\\xd8\\\\\\x93<\\x0eKe=\\xbb^3\\xbc\\xd5\\x94\\x83:*U\\x98\\xbcVh\\x0c=\\x9f(\\x0c<\\x17\\xe6\\n\\xbd\\xea\\x8c\\xfe<g|\\xbf\\xbcy)\\n\\xbcD\\x03z<\\xa6H#<\\xa8\\\"o\\xbc\\xd8\\x08:\\xbc\\xb2i\\xa1<.\\x81\\xd4<7]\\\"\\xbd}\\xf4\\xcd\\xbbw,\\xc7<T\\x90\\x14=!\\x84\\xf3<\\x1b8u\\xbd\\xff\\x0c\\x90\\xbc2\\xea\\x9d\\xbc#\\x85\\x1f=\\xb2\\x8fD\\xbd\\x1b\\xa7\\xb0<\\xc1_\\x9e=\\x9b\\x1d\\xc9<\\x0e\\xa3\\x9f\\xbc\\xddF\\x02=\\xc9\\xe9\\xb1\\xbc\\x1e\\xef}\\xbd\\xea[==\\x8c\\x8ap:\\x10W\\xc5\\xbb\\xc5\\x86s<&H\\xe5\\xbc\\xe7Q\\xf8<\\xe3c\\xba\\xb9R\\xfc\\x88=SO~<\\\\q\\x82\\xbcT\\xfd\\xb2<B\\x91\\n<\\t\\xe05\\xbb\\r\\xea?\\xbcrC*<\\xe7\\xc5)\\xbc\\xe6B\\xec\\xbc\\x86\\x02\\x83\\xbc\\xe9\\x0eO=\\x03\\x18\\x07\\xbd\\xbc\\xc1\\xed\\xbc\\xe1_\\xae<te\\xbd=\\xde\\x8b\\xf4\\xbb}\\xb5\\x0c<\\x8c\\xcb\\x9d\\xbc\\xb3\\x91\\xc3:E\\x0fy\\xbc\\x1f@\\xec<\\x88%\\x81<h\\x87\\xce<\\xc0\\xeei=v\\x9b\\xb0<\\x8c\\x00\\x01=\\xc4p|\\xbdr\\xfck<V\\x9a\\x85=\\x03\\x1b\\xba\\xbc\\x84r\\xb4\\xbc\\x0c\\xac\\xa4\\xbd\\xf4|#=l\\x16\\x95\\xbcWM\\xd8=2\\x8b\\xb7\\xbcXT(=X\\x9c>\\xbd),\\xf1\\xbbm\\x94.\\xbc\\x95\\xe0\\xb1<\\xa8R\\x0c\\xbd\\x8f)\\xf7\\xbcHc\\xbf\\xbc\\xea\\xd7\\xae;\\xc8*\\xf6<\\xc9\\xe3n=\\xbe\\xe1\\xca:\\x13y\\x82=\\\"\\xdc\\t\\xbd\\x9dep\\xbd.\\xb2\\xca<\\x01^a\\xbc\\xfa\\xf6\\x12=5\\xf4A=1\\x1d\\x82<$Y\\x86\\xbd5\\xf9\\xed<s\\xcd(<\\x9d\\x15\\xae;\\xa9\\xd4\\xa4;\\xfb\\xc6\\x96=Nnf<\\xe9\\xd6\\x87\\xbc\\xd7\\x16\\xa5\\xbc\\xf7\\xa6\\x91\\xbcT\\x97\\x9a;\\x1f\\x01^=\\x8d\\x8eJ=\\x13{\\n=\\x9fm\\\"<\\xba\\xb3\\xa0\\xbc\\x94\\xba\\xaa<\\x7ff\\x16<ux\\xf6<\\x03\\x98,=\\x8b\\xca\\xb7\\xbc\\x8e\\xd0\\x10<\\xdd]\\xab<\\xac3\\xe9\\xbc\\x03\\x1f\\xd9\\xbc\\x0f\\xb5\\xcc\\xbdQ\\xe2S<\\\\\\x83\\xe6<+\\x1b\\xa1\\xbc\\xf1\\x85\\xb1\\xbc\\x14J\\x8c\\xb7\\x9e\\x13\\r\\xbc\\xa5$\\xd3<_\\x00;<\\xf9\\xe8\\xd5\\xbc\\xbb\\xe6\\xbd<\\x91L\\x9b\\xbd\\x92Z\\x87\\xbc\\xfa[e\\xbd\\xb7\\xee\\xb0\\xbdI\\xed\\x13\\xbd\\xbe/\\xea\\xbc/\\xad\\xe9\\xbc\\x8e\\xab\\x11\\xbd\\xa56-\\xbd\\xea\\xaa3\\xbc\\x04\\xbd2:\\xa6\\x8a\\xa1\\xbd\\xf9\\xc8\\x8f<\\xf9O1\\xbd\\x85\\xb2\\xdb;\\xa42\\xd1\\xbc^\\x17\\x15\\xbcG&\\xd8\\xbc\\xbfH\\xce:q^\\xba\\xbb\\xe5\\xa6\\x05=z\\xde\\x9b\\xbbJ+\\x89\\xbbW\\xbc\\x11<\\\\\\x9e\\xab\\xb9\\xcf(\\x00\\xbd\\xa9V\\x83\\xbd\\x98\\xe9\\xe4\\xbd\\x1be\\x90\\xbc\\xb6\\n\\x14\\xbc%\\xad\\x06<\\xf46\\x93;P\\xdd[=b\\x1at\\xbd\\xf9e6=\\xb4\\x8f\\x1d\\xbd!@\\xaa\\xbde:\\x16=D\\x02\\x1a<\\x92\\xc5\\xb1\\xbc\\x9b\\x1c\\xf4\\xbcJM_<\\xc9{\\x0e<\\xec\\xe2 \\xbd\\xe4s\\x0e\\xbd7\\xf3x;\\x03\\xc8\\xb9<\\xda\\x87\\t:\\x90a4<\\xbej\\x11=k\\xa9\\xf6;r\\xdfd=s1\\xc0\\xbc7\\xb1\\xdd:\\xa2\\xf0K\\xbd}\\xb4Y=\\\\R=\\xbd\\xd8\\xc7\\xf6<\\xa9\\x08\\xba;\\xb1\\x17\\x15\\xbcQ\\xbf\\n;\\x00\\x06r=\\xbe\\xdd\\xfc\\xbcy,\\x93\\xbc\\x15d\\x82\\xbc\\xd5\\xc2\\xd4;H\\xfbw\\xbcyL\\x84\\xbd\\xd3=\\xee<Y\\xa2\\xf0\\xbc\\xdf\\xd0\\xf5\\xbc\\x08w\\x8f=\\xd08\\xbb=\\xa3\\\"v\\xbb\\xa6\\x11\\xe5<\\n\\x06s=\\xa02\\x17<k!\\x81=X(\\xa8\\xbdO\\x87\\x05\\xbc\\x00IO\\xbb\"\nHSET bikes:10010  model 'Dysnomia' brand '7th Generation' price 4099 type 'Road bikes' material 'alloy' weight 16.9 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\x0cW\\n=\\x00k\\xcb;4\\x9b\\xbc\\xbc\\x1b\\x10\\xbd<\\xa8O\\x00<\\xbe:]<\\\"}\\xb3\\xbc\\xc4^/\\xbd~\\xa3\\xe3<;g\\x08\\xbc\\x14\\xad\\x9f;\\x96;\\x08\\xbc\\x19d;=\\xafZ6;\\xfe\\x80f={\\x9b\\x1a\\xbd+\\xac\\xda;\\xbc\\t?<\\x7f\\xb8.=\\x1c\\xaf\\x89\\xbd;F\\xde\\xbc;a\\xc5\\xbcpI\\xb8<\\xc4{\\xbd\\xbc;n\\xc4=je=<\\xd9\\xd2\\xfc;\\xd0N\\\"\\xbc\\xa5\\xc1\\x02\\xbc\\xa3i\\xb3;\\x9f\\x9e\\x1e\\xbd\\xa3=I\\xbc>\\xcd\\xa7\\xbcy\\x9b@\\xbc\\xc0\\xfd9=\\x830\\x92<\\x02\\xa2\\xf8\\xbb\\xb2\\x8f\\x01\\xbd\\x16\\xa6T\\xbd\\x83\\xe8~\\xbcY[\\xbd=\\xa7\\x9an\\xbc\\\"\\xf8\\x92\\xbc\\xe2Mg=S\\x9ar\\xbc\\xeeL/\\xbc\\xe1\\x11`=g\\x99\\x97:\\x1c\\x14-\\xbc\\xd7\\x88\\xf9\\xbbi\\xa6\\xdc;\\xcb{\\x02\\xbd\\xd08\\x97\\xbc^cA\\xbb\\xde\\xc1\\x9b\\xbb\\xc3\\x88\\\"\\xbdIp\\xa8\\xba=\\xec\\xdc\\xbd\\xb2\\xcc\\x9a\\xbdl\\xc0 =J\\\"M<5C\\xac;\\xe6\\xbeg<YFI=\\\"\\x00Q=\\x9cB\\xe3<\\xa4I\\x93;A\\xd9\\x05=\\xccL\\xb9;\\x8c\\xaac\\xbc\\\\de\\xbc\\xb5\\xde%\\xbd\\xd0\\xc1\\xc3\\xbcH\\x17\\xf5<\\x93\\xc6\\x8f\\xbc\\xdd\\xea\\x10=\\x19\\x0e\\xc8\\xbb\\x01\\xe0\\x16\\xbd\\xbc\\xf8\\x82\\xbd49q<^\\x9bN:\\xc8\\xf8\\xb5\\xbc\\x9b\\xe1\\xe6<D;>\\xbdLJ\\x9f\\xbc\\xbaA\\x13=\\xf1\\x16\\xe4\\xbap\\x92[<\\xdd\\xbf\\xc6<w\\x07\\xab<,\\xcd\\x1b\\xbc\\xe5\\x7f]\\xbc\\\"|\\x11\\xbd\\x00\\xee\\x12<\\x92\\xc1\\x90<\\x8c\\x0fm<\\xa8\\xf8J:\\xec\\x13F<&[\\xb1;\\x07\\xe1\\xe6\\xbc`8\\x9c;B\\xd4d=\\x9cN#\\xbc@D\\x81\\xbc\\\"W\\xef\\xbc\\xc2\\x13\\x16>\\x00\\xa7\\xb8\\xbb\\x7fP\\xf0<\\xb7[\\x08<\\xb8\\xeb\\xc4<\\x0c\\x19\\x06\\xbd\\x9c\\xcd\\x99=8\\x1e\\x08=b\\xe8\\x9d\\xbc\\x80\\xad\\x95\\xbc8\\x1eD\\xbd\\xda\\x9c7\\xbd\\x02\\xa8 \\xbc\\xf9\\xd8x<\\xab\\x8f&=\\xa0h\\x10< c\\xc1<\\xefQ\\xfb<\\xa4\\x990\\xbdm\\xd4#=\\x03]\\xda\\xbc\\x15\\xef\\xbc\\xbbi\\xf5\\x9c\\xbd=\\xd8\\x17\\xbd/\\x15b\\xbd\\xf4\\xbc\\xf6;\\x01\\x8d\\xe4<\\x0f\\xe2\\xfd\\xbb\\x04\\xf8N\\xbd\\xdc\\xde\\xba<\\xc5\\xbb\\xf4<\\\"\\xf14;\\xc1\\xb5\\xe8\\xbcv\\xaa\\\"=\\x1fJ\\x03\\xba[\\xa0\\xdc\\xbc\\x10\\xfa\\x8d\\xbd\\x98\\xae\\xc9<\\x9b#\\x91<\\xec0\\x91<\\xab[L\\xbd\\xddS\\xa8<\\x8d\\xeao<\\xebk\\x81\\xbd0\\xd2A\\xbd\\x82M\\x18=\\x84L\\\\=5\\x8fz\\xbc\\xac\\x015=\\xd6\\xd6\\xe1\\xbc\\xc1Q\\x9f\\xbcm\\x93H\\xbd\\xb5\\x03\\\"\\xbc\\xbe&\\x83\\xbc\\xc6?\\xb9\\xbc\\xa0\\xb4\\xad=\\xa4\\x0bt:\\x03\\x1b\\x8c\\xb7e\\xd03\\xbd\\x80\\x03[=N\\xd61=p\\x85\\xa9<t\\xaf\\x8c=CM/=(W\\x1c\\xbc\\x95\\xe9\\xbc<\\x06\\xc6D=$\\x84\\xc9\\xbcG\\x1fg\\xbd\\xf7K\\xe7\\xba\\xb9I\\xba;Y\\x17\\x89\\xbd\\xd8\\xb0o=\\x1e\\xa0:<\\x8c\\x8b\\x0c\\xbdm\\xe2\\xf9<\\xaa\\x9fT;\\xc8i\\x04\\xbd\\xf0\\xbe\\xda<\\x0b\\xc3\\x14<\\x94/\\x1a<\\xec\\xe8\\x9d=\\x07\\x98\\xbf\\xbc\\xcf\\xae\\xed<\\x9d_D\\xbd4\\x05\\x99\\xbc?E\\x94\\xbb\\x95w\\x83\\xbd\\xa5U\\x93\\xbb\\x1dfY\\xbb\\xe6(|=\\x04L\\x91<\\x01\\xda\\x98\\xbdA\\x86\\x81<\\xb6f\\x9e=]\\x1f\\xbd\\xbc:\\xa5\\x8a\\xbd\\x02\\\\\\xf8\\xbd\\xbb\\x18\\xf5\\xb8xSv\\xbb\\xf4\\xea\\x0b=.\\x04\\x88\\xbb\\xd3\\xc4\\xc6\\xbc;CF=\\xf87\\r\\xbd\\xea\\xf2\\xa9\\xbc\\xee\\xc3A;\\x14\\x82\\xda<O\\xd3\\xa4\\xbc\\xc7`^\\xbcz\\xeft\\xbc\\xf9\\x90\\xdb<!\\xab\\xbe<#\\xa6\\xbc<\\x96\\xae\\xcd\\xbc}\\xd9\\xf3<\\xa7\\xff\\x81<2\\x9c{\\xbd\\xeb\\xd3!\\xbcKZ\\x81\\xbd\\t\\xfc\\xef\\xbc7L\\\"\\xbb\\x9c{b<\\xf5lE=[\\xe6s\\xbd\\xc6\\xb6\\xba\\xbb~H\\x9e\\xbc\\x96tN<z\\xa5_=\\xc1\\x9b\\x86\\xbb(\\x07\\xa8=\\x98\\xf4\\x1d=h\\x8d\\x8f<A;\\x00\\xbd\\x17\\xc2\\xda;q$\\xb8;$u\\x81\\xbb\\x06\\x87L=\\xfdC\\x9d\\xbb\\xda\\xd2M\\xbc\\xdcd\\xa2=\\x80\\xa6\\x08<\\xe190\\xbd$&?\\xbd\\x8b4\\xfc;\\xd5\\xcf\\x8f<\\xf1\\x97\\x8e\\xbd@)>\\xbc\\xd2~\\x8d=\\x11\\x9aP\\xbds\\x99\\x80=j\\xd0H\\xbc\\xe0\\xde\\xff\\xbcQ\\xc3\\xd5<\\xd9r\\xad=\\xea\\xbcm<\\xc4\\x00\\xea\\xbc1[U\\xbd\\x11\\xd6P\\xbd\\x98\\x85\\xd3\\xbct\\xdc\\xd3=\\xbb@G\\xbd-4\\x06\\xbd8\\xcaa=\\x1dF[<\\xb7d\\x87;\\r\\xa0\\x89;\\x7f?\\x04\\xbd/\\xc5\\x0f=\\x85^\\x1e\\xbdv\\x0b\\x1f\\xbc\\x80\\xd4\\xc1\\xbc\\xb3\\xa9\\x0e<Q\\x02K=\\xd0\\xaf\\x7f\\xbd\\xb5\\x04\\x0c\\xbd\\xfc9J\\xbbx\\xf0\\x83\\xbdr\\x8b3<\\x91j2=\\xc2A\\x127\\x84\\xb5\\x92;\\x81\\x15\\xea8\\x042L\\xbd.\\xf8\\x08\\xbc\\xf7\\xa9?\\xbc:\\xc8k\\xbd\\xaaP\\x87=\\xd2\\xab\\x85;:\\xc0o<\\xa5\\x1dX\\xbd\\xdd\\xb6\\x0f=\\x81\\x8b\\x0e=\\xb6\\xa5\\xda<t\\xdb\\x16\\xbd,\\xe9;=\\x1d\\x9fu\\xbc\\x02_t=\\xe7\\x04\\xd2\\xbcIPd<\\x9b5V:\\xf3\\xdfA\\xbc\\xcev\\xed<\\xd0\\x0f\\x89=4\\xfe\\xf8;\\x1e5\\xb4\\xbc\\xcc\\x01f\\xbbpoN\\xbd5\\xec8<M\\xc1\\xe2<\\x97\\xcd\\x9f\\xbc\\xef\\xb6\\\"\\xb9\\xd0q\\xd0=\\x9eT\\xa0\\xbb\\x1f|\\x97\\xbbl#\\xbd\\xbc\\\"\\x1d\\xed\\xbb5v\\x94<\\xaeU<;Y\\xcd\\xa7<\\x1cR\\x9e\\xbd-E\\xf7\\xbc\\x82H\\xaa\\xbds\\x06\\x16=H\\xf7X\\xbc\\xa6\\x96\\x98=\\x9b\\x8d\\r\\xbd\\xdbJ\\x1b\\xbd{p\\xdf<\\x83%\\x81\\xbcGx59\\x13\\xe6\\x15\\xbd\\xa5\\xf3\\xa0<\\xa1g><Gb\\xc5;\\xfa\\xc4\\x02\\xbcC\\xbe{\\xbcw\\xa9\\xa3=\\x19\\x8cg\\xbc\\x89\\x0c\\xe8;\\xbd\\xeb\\xd2<\\x9d\\xaf\\xc4<\\xc9\\xe9\\xe3<\\x0e|\\x0f=\\x0e\\\"t\\xbd\\xeeY^\\xbd\\xc4\\x13\\xc3;\\xefp\\x1d=x\\xcb\\x00\\xbd\\x9d\\x97\\xca\\xbcN\\xfbg\\xbd\\x91\\xc8\\x08;9\\x02\\x15\\xbc\\x1c\\xf9H\\xbc\\xcft\\x8b\\xbd\\x82/\\x08\\xbc\\xf7\\xe4c\\xbc\\xe2\\xb7\\x84\\xbd\\\\\\xc1\\x85=\\x9d\\xb8p=\\xac\\x8a5=:\\xf5\\xe8\\xbc\\xb2\\x88K=\\xa8\\x1d;\\xbd\\x16RG\\xbd\\xea7\\xd7<]\\x90:;\\xc7\\xf8\\x8a<\\xc4v2\\xbd\\xc3\\x97`\\xb8\\xa5\\xcaU<\\x16h\\x9e\\xbb\\xcf6\\x05\\xbd\\x10\\x9a\\x8b\\xbb[N\\xa5<m\\x15\\x01\\xbdb\\x8a\\x1a\\xbc\\x13]\\xcf\\xbc);\\xdf\\xbb\\\"\\xdf,\\xbdE\\xe8\\x80\\xbc`\\x0cn;\\x1e\\xe9c\\xbdR\\xca-=\\xd5M:\\xbcJ\\xf09\\xbd\\xbdn\\xa5<vN\\xa4=\\xb5\\x90\\xf9\\xbc\\x83|\\xb3\\xbb\\x8d\\xae\\x05\\xbc8y\\x95\\xbdH\\xceC=\\x9c\\xc0\\x87=Eh\\xdc\\xbd\\xfaW\\xfa<Q\\x0f`\\xbd\\x90\\xbcq<\\xffc\\xa8=G\\x89c\\xbc\\x9e \\x15\\xbd\\x15\\xa82=m\\xa3\\x06<hF\\x8c<\\xddO^<.\\xad5=\\xfc\\x96\\xbe<\\x02\\xa0\\\"=\\xbf\\xd1\\x19\\xbc\\x95\\xba\\x05=_\\\"+=.]W\\xbd\\x17\\x89f\\xbd\\xd8\\xef\\x1d<X\\xbd\\x9d\\xb8\\x0f\\\"\\xe2<E\\x9f\\xb6\\xba\\xeb\\xe4\\x15\\xbcp\\xcc\\x89=\\xbe\\x1d\\\"<\\xd3\\x9a\\xcb:\\xa3\\xc4\\x07=\\xee)@=\\xd65\\t\\xbc\\t\\x9e\\x0c\\xbdv\\x0e\\xc1<Y\\x1f\\xaa\\xbc\\x1b\\xd7\\x84:s\\\"\\xbf:\\x17%\\xb5\\xbc;oX=\\x17m\\xd4<\\xe6\\x84$=8<\\x1f\\xbc\\x0c\\xc0\\x13=\\x00\\x1bN=\\x19\\xe8\\x07\\xba\\x92/\\xe8\\xba\\x08a2\\xbcDO?\\xbc\\xf6v\\x05\\xbc\\x90w\\xf4\\xbc\\xc6F\\x14\\xbd\\x8d\\xdc\\xd1;\\xf5\\xde\\x81\\xbb\\xb4\\x06\\x19\\xbdh\\xffg\\xbdx\\xc6=\\xbc\\x84k-\\xbd\\xb1\\xd0`\\xbc\\xb9S\\xca\\xbc;\\xd8\\x15\\xbc\\x93\\x94\\x88<.n\\x0e\\xbd\\xd4\\xf9\\xa4=#\\xb1>=R@\\x05;^\\xbd3=\\xffh\\x8b\\xbc\\x12}\\xa4;6\\x86a=\\xc8\\xa6/\\xbc\\r\\x03R\\xbb\\x90\\xc2\\xcd\\xbc\\x0c\\x14\\xcf\\xbbWM\\x8c\\xbcV7\\x8d:\\ng\\xb2\\xbb\\xddH\\xc2\\xbcu\\x0e\\xea9K`4=r\\x9dp\\xbd\\xe6\\xacY\\xbck\\xba<=\\xbd\\x88\\x1b\\xbc\\xf1]=<*5\\xff\\xbb\\x8c\\x88\\x97\\xbc$\\xfb\\x16=p\\xcb\\xa4<\\xec:\\xa9\\xbd\\xbe5\\x1a=X\\x91;\\xbc\\x7f\\xb1\\xd9\\xbc\\xf62,=\\x8dp\\xa8<\\xea\\x06\\xab\\xbc\\x91\\xc5\\x93=*5\\x81<\\x01\\xb8i\\t\\xca\\xb9\\x11\\xbbq\\xd2\\x92\\xbc^N\\xd6\\xbbVxN<e\\x99\\xc4;\\xa6\\xef\\xb6<\\x04\\xa1\\x1f\\xbdRR\\xf6\\xbc<\\xad\\xb2\\xbc\\xc8g\\x11\\xbd\\xd8u\\x0f;\\xb7\\x1d\\xc0\\xbb\\x8b-\\xba;z\\xe3x=\\x15z\\x1c<\\xe3\\xcd\\xea<\\x11\\xc9\\xc6\\xbc\\xd9J/<)\\xe3\\xc3\\xbaE\\xe3\\xe5<\\x1b_)<\\xf0j\\xee\\xbc\\xabb\\xaa\\xbc\\xce>\\x92\\xbd\\x98\\xa2f<\\x8a\\x08\\xb9;\\xbfP\\x1d\\xbc\\xc6\\xea/\\xbc\\xe18w\\xbc\\xd7\\xe2\\x0f;S\\xb7a:g\\xc56\\xbd\\x95uT\\xbd\\xde\\x18q\\xbc\\xca\\x8c\\xe0\\xbc\\x9a\\xb0\\x92<V\\x13\\x99\\xbb\\x9a\\xdb\\xb7<\\xc9\\xa4\\x12=\\xeb\\xe2[=\\xd2`\\x0b=\\x89\\xb8>\\xbc\\x88[r\\xbb\\xe9\\xc0\\xbd<b\\xa0I=\\xbe\\xf8O\\xbdN\\\"4<\\xb9K\\xa5<\\x1f\\x9c>\\xba\\xf7\\\\M\\xbd\\xdb`s\\xbdC\\xb5\\x15\\xbd\\xb5\\nK=\\xd9\\x12\\xef<\\xe2.\\x88<\\xf6\\xed\\xa6\\xbb6l\\x9e;6\\xd0\\x01=O\\\"#\\xbci\\x1a3=\\xd3:\\x15=\\x06\\x8c\\x1b=\\xdf\\xc9d\\xbc\\x96\\xe7>:JD_\\xbd\\xb4XN\\xbd\\xcc\\xb7\\x84\\xbdO5i<\\xcb\\xfb\\x9f=\\xd9\\xc3\\x96\\xbb\\x0ez\\xc8<=?5;~\\x07\\xbc:\\xb8\\xc1*=\\x9bo\\xbc<\\x04$\\xea<\\xca\\r\\x8a:\\xe0<\\x8e=j\\x85Y\\xbd\\xd25\\x01=J\\n\\xb3<\\x07\\xd0i<\\xab-\\xa3;\\xe3\\x92\\x85=&\\xa9$\\xbdA\\xca\\xa2;6\\xeb\\xe2<\\xf9+p=\\xf3\\x0c\\x92<\\xf4\\xba)\\xbbw+\\xf8\\xbc\\xd3\\xf7\\xeb\\xbc\\xf6Lr<5\\xba\\x82:32\\x07=g\\r\\xf9<l0S\\xbd\\xe5\\x95\\xa7\\xbc\\xf5\\xb8\\xf4<\\n\\xf5\\x05<\\x17FZ\\xbd\\x02\\xac-\\xbc\\xa8\\xb3\\x19\\xbc\\xa8\\xb5t\\xbd\\x14\\xc1\\x0f<\\x01T*\\xbc\\xb6\\r@=P\\x08\\x8b\\xbc\\xab\\xc3)\\xbd\\x82\\xfd\\x8f<\\x18\\\\\\x02=\\x9d\\xcb-;\\x1a\\t;\\xbd`6\\xa1=^\\xdf\\x95<\\x95p\\x07<\\x10h_=\\x04\\xc2\\x8d=3~\\xf6\\xbc\\xb2\\xb9\\x88\\xbd\\x9e\\xadr<\\xcal \\xbc\\xb1\\x9cR=\\x0bl\\xfd<\\\"\\x17\\x00\\xbc\\x8c\\xd8\\x10\\xbc\\xb9\\t\\xc5\\xbci\\x97(=w\\x1f\\xa6=\\x97\\xe2S\\xbd\\x9e\\x94-<\\xf6\\xe0\\xc2\\xbc\\x81,\\x9b=d\\x86\\xa1\\xbc\\x12P\\xf9<w\\xad\\x0c:*\\xc8,=my\\x92\\xbd\\xf9\\\"\\x14=\\x01\\xf1\\xc9\\xbc\\xb6\\x86\\x98\\xbc\\x85\\xbdL\\xbd\\xfbYc\\xbd\\x83\\xf2\\x94<\\x1bd\\\\\\xbc\\xf8 \\xd2:^\\x03\\xcb;\\xebmA\\xbb\\xb8\\x177\\xbb?\\xe8\\xde\\xbc\\xa0-\\x18<\\xf7\\xf83\\xbb\\x14O\\x86\\xbc\\x8b\\xfd\\xfe<\\xdd\\x08\\x9a<\\xee\\x15k<\\x9bL\\xb6\\xbc\\xdb\\xca\\x8b<o\\x89\\xa1<AmI\\xbc\\xd6>,\\xbd&\\xccW=\\xc3\\\"6\\xbc}\\t\\x8f;\\xe3\\xb7\\xe3\\xbbRc\\x0f\\xbdQ%\\x95\\xbd\\xc3%\\x87\\xbc\\x1dK\\x83=~\\xae\\x0f\\xbcZ|E=\\x1a\\\"\\xa9\\xbb\\xd4\\xda\\xa1<\\t\\xc3\\x10\\xbd\\xb5\\x9b\\xd0\\xbc o5=X@\\xce: \\x9f\\xce<\\x1e\\x9al<;\\xa3B\\xbc\\x80N\\xce<\\x90\\xcf\\x92\\xbd\\xaa\\x19\\x02=\\x8f\\x90,\\xbc\\xdb\\xaeD;\\xd1ot\\xbc\\x15\\xd6g\\xba2\\xb8:\\xbc\\x9aZ\\x83=~\\x91\\x18\\xbdL5n<\\xb3\\xf6\\x91<N\\x98\\x95\\xbc\\xffUK\\xbd7$\\x02\\xbdW>\\xb1\\xbbw$\\x0f\\xbd\\xe1U\\x07\\xbc2L\\xba<\\xed\\xa9\\x85\\xbb\\xc7\\xb5\\xfe\\xbcK(\\xdf8]\\xca\\xe3\\xbc\\xe2d9\\xbdX<[\\xbd\\xa2\\x90J\\xbd\\x82\\xa0\\xd8<3\\xb9K\\xbd~\\xaeV<\\xdb\\xda\\x0e\\xbd\\x19\\xbe\\x07=\\xc9\\xc1\\xbd\\xbb\\xe7\\xed|;&\\x06\\x07\\xbdx\\xe5\\xf2\\xbc\\xd4\\xbc5\\xbdZe =&\\x19\\x12\\xbd\\x0c\\x02\\xd4\\xbc_\\x9d\\x07\\xbd\\x11\\x12\\x93<\\xea\\xf0S\\xbcI(;=5\\x16\\x85=m\\x8b\\x81=\\x17\\xbf\\xb0\\xba\\xe9\\xee\\x10=\\x15\\xff\\xe6\\xbc\\xea\\xb3\\\\:\\xd9}s=d\\xa6\\x00\\xbbZL\\xae\\xbbo-\\xeb\\xbc2*+\\xbc\\x1d\\xe6&\\xbd\\xb3\\x90\\xad\\xbc\\xef\\xe4\\x1d<\\\"Z\\xe1\\xbc\\x0005=IT\\x05\\xbcr\\xb3\\x02\\xbc;\\xa2(=\\x7f\\xb1\\x00=4\\x8a\\x8f:9g}\\xbd\\x17B\\xc0:,\\x87\\x02\\xbd\\xb4\\xa1\\xcf<\\xdbW\\xa1\\xbd\\x11\\x8e\\x08\\xbc#\\x07\\xdc\\xbb\\xe9d1\\xbd\\xb6~\\xef\\xbb\\xc1S\\xe5<\\xe4$\\xe0\\xbch\\xd7k<\\xaf7\\x00\\xbe\\xecb,=\\x00\\x84p\\xbd`wb\\xbd\\xac\\xe9\\xcd9E\\xd5\\x08=W\\xf6Q<\\xd7Q*=\\x00\\xfbx=#\\xcc|<P\\x000=\\xfd\\x93\\xb7<\\nvx\\xbdw\\xc3X:\\xf6\\xcd-\\xbc\\x9e\\xb2\\xd5;\\x1dI@\\xbb\"\nHSET bikes:10011  model 'Calypso' brand 'Bicyk' price 4986 type 'Enduro bikes' material 'alloy' weight 11.2 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\xd8\\xeab<\\xa2\\xce\\\"<LzJ\\xbb\\xfbOK<\\xa9\\xcbi\\xbc\\xcd/4=\\xb6\\x02\\x81\\xbc\\xf6\\xefL\\xbdsS\\x98=\\xb6\\xa1\\xf0<l\\x92\\xe6\\xbb\\xec\\xde\\x14<\\x12\\xc2.=\\xa7\\xdf\\x03=\\xdf\\xac\\x18=!\\nu\\xbd\\xac-\\x01=\\xb2=P\\xbdG?\\x08=\\xe0\\t/\\xbd\\x8d\\xccC<\\xb1\\x97-\\xbd\\x1aZO\\xbdU\\t\\x84\\xbd\\xd4\\xf15\\xbdv_G<\\x00@\\x10=#\\x0f\\xa1\\xbbY~\\x8a\\xbd\\x19?\\xa0=Y\\x03\\xc3\\xbc,\\x00\\x8c;\\x06c\\xd4<\\xfd4P=!\\xe6(\\xbd\\xad\\xc67\\xbb\\x07\\x19\\xe9\\xbb\\xe3\\xcb9\\xbdX\\xc6\\x84\\xbc\\xf6&\\xeb<\\xcfAS=\\xc1\\x0el\\xbct\\xef\\xc8<$\\no\\xbb\\x10\\xe0M;\\xb2bV\\xbdx\\r8=\\xd7a\\x0e\\xbd\\xc9\\x82\\x05\\xbdS\\xb8\\x93<\\x18\\x9b/=[\\x18\\xcc\\xbcq\\xe3\\xbf\\xbc\\x01@`<\\xf8e\\xa5\\xbbs\\xd7\\xd9\\xbc~\\xf5\\xdb\\xbbE\\x0c\\x1c\\xbdGwD\\xbdL\\xdf\\x93=(\\xc3\\xf6<9\\xb9c=\\xfeK\\x8f\\xbc\\x96\\xea8=V\\x0b1=\\xd3g\\t<\\x80\\x12\\x14<w v<\\xb5\\x9c\\t=\\x85\\x8fO\\xbc\\xec&\\x10\\xbc\\x84\\xd6\\xa1\\xbcC\\x88\\xbe\\xbcE\\xf8\\x9a\\xbc\\xcfa\\x0f\\xbc:\\x04\\xbe\\xbcM\\x8c\\x8e\\xbb<\\xc5W\\xbd\\x95,\\xb8\\xbd\\xd3\\xc3\\xb2<\\xecd\\xe5<\\x8c+\\xd8\\xbc\\xca\\xf2\\x89\\xbc\\xdd\\xcbi\\xbdv\\x18\\xe5\\xbc\\xcbh|\\xbd\\xe89\\x9e;\\xbd\\x9b\\x7f;5\\xa4,=`\\x98B\\xbbB\\xcc-\\xbd\\xd1\\x1a*;Os\\xdf\\xbb\\xa9d`=OE}<\\xcb\\x92\\xca<\\x02\\x0f4\\xbd\\x1f\\xa4\\xeb<M\\xe1\\x99\\xb9\\xea\\xfd\\xcc\\xbc\\xd8\\xee\\xa8\\xbc\\x89#\\x98<k\\xf4\\x82<\\xe7.u\\xbd\\x8agW\\xbbE\\x8e\\xa5=\\x17\\xda\\xda<\\x82\\x93\\xc4<x\\xfc\\xfe\\xbc(r\\x92;\\xb0?2\\xbd\\xcc\\xf0w=\\xda\\xcd?=\\xf7\\xfb\\x0e\\xbd\\xa6\\xd51\\xbd\\x14n\\x87\\xbbB\\xe5\\xac:\\x01($<>\\xe0\\xf3<\\xbf9\\xb3<\\x1a\\xc2)<\\xdd\\xa4\\xa5;\\t\\xba\\xe9\\xbc\\xf6|\\xaf\\xbc\\xed\\xb5\\xe6\\xbb\\xca\\xe6\\x99\\xba\\x0c\\xe4\\xa5<\\xbc\\xb3*\\xbd\\xdc\\\\M\\xbdP\\x89*\\xbd\\xdf+c\\xbc\\xbbz;\\xbb\\xef\\x8a\\xe0<G\\x89\\x02;\\xb9iQ<Z\\x9e8=\\x81\\x07(\\xbd\\xf7\\xab\\x12\\xbb\\x04\\xec\\x02\\xbc}\\x19\\x05\\xbd\\x9dss\\xbd\\x0e\\\"=\\xbd\\x83\\x9a2\\xbcPj\\x04=\\xdc)\\xab\\xbb\\\\\\xc7\\r=\\xf9:\\x83\\xbb\\xa6\\xa9^\\xbc\\x00Ne;\\xdd\\x82\\x81\\xbd\\xd1\\x9e-\\xbc\\xed\\xa6h=\\xb5\\x87\\xe2\\xbc\\x89\\x0b\\x89<A&!\\xbc\\x19\\xd1g\\xbb/S\\xb7\\xbcbGu\\xbc\\xf9f\\xa9<\\x19V\\x81\\xbc:\\x93\\x82=&\\xceA;\\x04\\\\\\x10\\xbc:\\x85\\x89\\xbdIRd\\xbc\\x1d:$<#u\\x9a\\xbcb\\xb1$=\\x97e\\xf8<\\x0e\\x01\\x1d\\xbc\\\\\\xc9\\x98\\xbcM3\\x99=O\\x86\\xb2\\xbb\\xd9\\x80r\\xbc\\xc8r\\xab\\xba\\xbb,(<\\xcdI1\\xbd(@z=\\xf7 \\x8d;\\xed\\xcd\\x1e;\\x7f\\x05F\\xbc\\xe4@\\xe5<\\x97f\\xfb<\\x9aK==;\\x0cG=\\xb5\\xf6]\\xbd\\xf7\\x17\\xb3<\\xc1\\xea%=\\xef\\x8d6<A\\xcf\\x1a\\xbd\\x9at\\xa6\\xbb\\xd5y\\x87\\xba\\xebb\\xde\\xbc\\x11\\\"\\xd8<\\xa2`\\xc9;~\\xb5\\x92\\xbc\\xdf\\x16\\xc1\\xbc\\xdc\\xbb\\x1f\\xbd`\\x91\\x9f\\xbc\\x9b,F=\\xccg\\x04\\xbdn\\x88[\\xbd\\x85m\\xc1\\xbd<\\xfdU\\xbd\\xa0\\x1b5\\xbd\\xc82\\x8b\\xbc\\xb7\\x83\\x9d\\xbc\\xce9\\x84\\xbc\\xd4\\xd5\\xf0<%\\x1fH\\xbd!\\x9f\\xc9\\xbb\\xa0\\xc2\\x0f\\xbc\\xf1D\\x18\\xbc\\xb7\\xb2\\x0b\\xbd\\xa4\\xa9\\xee<s<z\\xbc&l\\x0e\\xbd\\\"y_\\xbd\\xfb\\xa2H=\\x8f\\xd2\\xbf\\xbd\\x9a&\\x1f=\\xfab\\xa0\\xbc\\x85*1\\xbdX\\x81\\xab:\\xad\\x8d\\x18<\\x9c\\xb4\\x9e\\xbb\\xf6+-=f/\\xbf<u\\xe8\\x88=\\xf3\\xeb3\\xbdy5(\\xbd_\\x1e\\x81<\\x14\\x8b;\\xbc\\xb43\\x91<\\x05&\\x8a\\xbc\\x04B\\xd6\\xbb\\xfca\\xf3;S\\xd7\\x07<\\x9er\\x96\\xbb\\x00\\x7fp;\\xd4\\x03\\\"=\\xdet\\xdb\\xb9d\\xc3_=\\xa8\\x8d\\xde<o\\xc3\\xcd;\\xa7\\xdc\\xf0<4\\xb8l<g\\xb7\\xff;\\x15\\x08;\\xbd\\x03\\xc3\\x87;\\xff\\xc1y\\xbcw\\xf2\\x83\\xbd{\\x9d\\xa2<\\xbe\\xcc\\x92=\\xae\\x82\\xa2\\xbc\\xef\\x84n=\\xbbC\\x15\\xbc#U\\xb3<\\n\\xbe\\x10\\xbd:\\xf6\\xa6=\\xaa\\x83\\x8e<\\x81\\xfa\\xca\\xba\\xa7\\xdc\\xab<\\x8a\\xb69\\xbcvqi\\xbdZ\\xff\\xbb=u\\xa2\\xe6\\xb9u\\x17\\xc0\\xba\\xaf\\x7fb<p\\xed\\x8d\\xba6\\xf5\\x9a<\\xef\\xcf\\xe3<J>w\\xbd\\xcd1C\\xbc\\xe1e\\xea\\xbc\\x05\\x87\\x8d;\\xd00];l\\xe5\\x8a\\xbc}\\n\\xbf<\\xed\\x04\\xc2\\xbd\\xce5\\xaf;\\x94\\xf9\\x0e=J\\xfb\\xde\\xbc\\xa9\\x87\\x13\\xbb\\xd4\\xd8\\xe0<\\x90\\x8d\\xb9<\\xcd\\\"\\xd1\\xbcL\\x1e\\x88=/*\\xa4\\xbd\\xcdg\\xe7<\\x84>\\x90<\\xc6\\x7f\\x1c:\\xf7\\x11\\x17=nQ\\x8b<\\x81\\xa1C\\xbc\\xc1\\x0f+\\xbd\\xac\\x1e\\x11<\\xe6\\x1a\\x88<\\x98\\xea\\x9a<)\\xf1\\xe0\\xbc%\\x92\\x8c<\\x94c\\xb2;\\xca\\rI=e\\xc5H\\xbb\\x18\\xae.=\\xc87\\x00=!\\xfbT<\\xe1|~<mO\\x92=\\xf9?\\xe1\\xba\\x1f\\xa74\\xbdZ&\\x07\\xbdB\\x8e\\x83\\xbb\\xd2{\\\"\\xbc\\xf3M\\x04=;M\\xec\\xbct\\x9c\\x01\\xbc\\xc5\\xf9\\xc2=x\\xe0\\x1c=\\x93\\xff\\xfd\\xba\\x17< \\xbd\\xdf\\xe2\\xc8\\xbcpK\\x18=\\x02\\xfe\\xd9\\xbbUs\\xf3\\xbbB\\x0f\\xf3\\xbc&\\xb3;=w*\\xb2\\xbd\\xcb\\xc5\\xb3<\\xc7\\xf3\\xbb\\xbc\\xa6\\xbe\\xe8;\\xa4mv\\xbd\\x06\\xc0b\\xbc\\xfe\\xd7\\xc3<A\\x8c\\xb3<\\x0br\\x14<,v\\x8f\\xbd\\xe1N%=\\xd5\\xf3%=A@\\x81\\xbc\\xd0\\xd9$\\xbd\\x81\\x94?\\xbd\\x08u/=\\xc5\\xb9=;\\x9eh%\\xbc\\x87\\x80\\xed<X\\x15\\x08=\\xb5\\xa4\\xd3\\xbb\\x10\\x04\\x88=\\xb5\\xfe&<\\xa0KP<\\x05\\xf0\\x81\\xbcsh\\xd1;\\xb0\\x87\\x9b<Gj@\\xbd\\x1f\\xe9\\x1e=\\xe2\\x1f-\\xbc\\xe2\\xf3\\xac\\xbcF%\\xe8\\xbc\\xf7\\x89\\xa6\\xbd\\xc8\\xd5\\xe4\\xbc\\xba1\\r=}\\xf4\\xf9\\xbbv\\x9fR=\\x03\\xbbC<\\xcf\\xf4\\xd8\\xb9\\x8e\\xd6\\x0b\\xbd\\xbd\\x0e\\x04=\\xe5\\x7f\\x9e\\xbc=\\xbf\\x8a\\xbcp\\xffd=\\xa4\\xd3\\xc3\\xbb\\x9a\\xa0\\xf1:\\x16\\x13\\x89\\xbc\\x06F\\x93<>\\xd8\\x9c<\\xdb\\xbc\\x1b\\xbd\\\\e\\x82\\xbd0\\xe8\\xff\\xbc\\x98[\\x00\\xbd\\xbd]R\\xbdR\\xbd\\xf0\\xbc\\x97|I\\xbd.\\x95\\xdd\\xba\\x97*\\xb0\\xbdE=7\\xbcmM\\xb6<F@\\xfb<\\x93:A=\\x1fz9\\xbdi3\\x7f\\xbd\\xea\\x81\\x04=\\xf8\\xcc\\xc5=\\xb9\\xb4\\x9c\\xbc-e\\x9a<\\x1f\\xfd1<\\x14\\x9f\\x91\\xbdc\\xad\\x94\\xbd\\xd3f\\x97=;<h\\xbda\\x8a*<\\xaf\\xf3\\x01\\xbd\\xc0\\xbe:=\\x92\\x87\\xa8=~\\x01f<\\xf7\\x99\\x88\\xbd\\x8d\\xb2\\x14=\\xd3\\xd3\\xec\\xba\\xe7\\x8e!\\xbcEm$<\\xb0\\x8bc=\\xe4\\xce\\xf7<\\x98\\x14\\xa9\\xbcv\\x14 ; `\\xe0<=\\x96\\xf5:L\\x8bC\\xbaM:\\xf7\\xbc{BQ<\\xb3N =\\x9e\\xce\\x0f=s\\x00\\xa9\\xbcA\\x85^=\\xe1)\\x89=\\xd7\\x1e\\xd9:\\xe4=9\\xbcJ|=\\xbd\\xe7GY=w\\x0e9\\xbc\\xee\\xfc\\xb4\\xbcl\\xb2\\xa9;\\xd4\\xe0-\\xbc\\xf1\\xd5\\xec\\xbc\\xe7e\\x99\\xbd6\\x8a\\xfc\\xbc\\xfa^\\x8c=\\xa0\\xea\\x08=\\xaeW\\xc3<\\x0f\\xdc\\x04\\xbd\\xf82k\\xbbj`\\x8a\\xbd\\xda\\xdf$:\\xaf\\x89I<0\\xe6A\\xbca\\xc2\\xf8<\\x16\\x93\\xac\\xba\\x96\\x85\\x1d\\xbc\\xd5\\x89Q;\\xee\\x9d\\xed\\xbc0d+=D\\x96\\x14\\xbd\\xd6\\xf1R\\xbd`\\xff\\r=\\xaa<\\xc6\\xbd\\xef)\\xdb<\\xee\\x9b\\xd1:\\x7f6\\xe2\\xbc\\xa7\\x1eL<+\\xbb\\x16\\xbd\\xca\\xfd\\x93=\\xcf\\x96a=\\xac)\\xea<\\xd2\\xe9\\x83=\\x84\\xd4\\x1b\\xbd\\xe3\\xd0\\x9e<\\x8e>_=/D\\x1d\\xbal#`\\xbd\\x15\\x80\\x13=\\xfey\\x16=\\xd9\\x92;8\\xba]U\\xbb\\xd9\\xad\\xf2:\\xcf\\x15\\xa2<S\\x03I\\xbc\\xc6\\x199=p\\x17\\x01\\xbd\\x10,\\x08\\xbd\\x80k\\x14\\xbb\\xcf\\xa7\\xdc;\\x99E3;\\xf0?\\x92\\xbc\\xc7\\x98\\x9a\\xbcM\\xcb\\xd3<\\xc1\\x88\\xd3\\xbb\\xe6\\x7f\\xd0\\xbd\\x90\\xd2\\xc6\\xbc\\xb4l\\x81\\xbcu\\xf3 \\xbb\\x06\\xbe\\x12=\\xd8\\xffH\\xbdDt.=\\x10\\xd3Y=\\xd0&\\xf5;!\\x1e`\\t\\xb71z;c\\x10i;?\\xee\\x12=DE\\xab<\\x89\\xbb\\x98<R\\xba;=\\xccO\\xb4\\xbcXC5<\\x9d\\xd9\\xa6<\\x12\\x81\\xa1\\xbc2\\xdb\\x1f=-\\xa0<=\\x92P0\\xbclbB=\\x1e\\xfe\\xf9\\xbcj?\\xb1;\\xaa\\x82O\\xbd\\n>>\\xbc\\xca\\xf2z<a\\xd1\\x80=\\xaf*\\xbe\\xba\\x1b\\xd7_\\xbb\\x80OY\\xbc&m6\\xbc\\xeb>\\xab<Ai\\x0c\\xbc\\x06\\xa8\\xef<\\xc9\\xc5L<\\\\1\\x16\\xbd\\xf7\\x11V<%_\\xe9<W\\x00S\\xbcvrq\\xbd\\xdc\\xf0\\xad\\xbd/#\\t\\xbd\\x1az}<\\xc9\\x9c\\xfb<\\xf6\\xab+={\\x84e=\\xfcz\\x04<\\xe1q\\xcc<\\x96.p<\\xc4\\xaf\\xa9<\\xac\\x85\\x81<\\xd3\\xf1j= \\r\\x93<J/\\xda=\\x16\\xa7\\x0f\\xbbC\\x9b\\t\\xbd\\xb2o\\x19<\\x8e\\xf3r\\xbd\\xdc@2\\xbd\\xf8\\x9b\\xe3<\\xaf\\x1fo=\\\")I;TV\\xeb<GL\\xa5<\\xf5\\x1c\\xf9<}\\xef\\xc9<\\x17\\xcb\\x06=~\\x85M;\\x18\\xd7\\x19\\xbc m\\xaf<xv\\x11=r\\xe9\\xaa\\xbc\\x93\\x8dA\\xbd\\xa2<\\xb2<g .<`k\\x07=\\xa1Cf\\xbc\\x01/\\xe5\\xbbJ3\\xc0\\xbc\\xc5\\xfe\\x8f\\xbc\\x90\\xd5\\xbd<u\\xd0\\xed<\\x88Gb\\xbd\\x1e\\xea\\xad\\xbc=\\\\\\xce<\\xb7\\x9c\\x0e\\xbdG\\x8e\\xc6<X\\x01U\\xbdF\\x8fQ\\xbc\\x89#\\x04\\xbd\\xa1\\x07P=\\x01\\x18\\x1f\\xbd\\xb1x\\xae\\xbc\\x7f\\x80a={Wh=\\x02\\xe96\\xb9\\x93\\x0e\\x1e=\\xf5\\x00\\t\\xbcV\\xc0\\xcf\\xbaF\\xf6\\x9a=2A\\xcd8z\\x0e\\xf4;\\xd6\\xfbH=\\xad\\x9c7\\xbcx&\\x18=\\xae\\xfc\\xe3\\xbb+\\x16\\x15=!\\n\\xae\\xbc\\xa18\\xb3\\xbc+\\x86\\xf3\\xbb\\xe2U\\xb0\\xbc\\xa9i9<\\x0f\\xdd\\xe4\\xbc\\xfd\\xad\\xae\\xba;\\x88w<F#>\\xbd]\\x8c\\x13\\xbdl\\xf7d=_\\x8aw\\xbd\\x01\\xc7D\\xbc\\x95b+=\\x80\\xc5\\xa1=p\\x95\\xf4\\xbbo\\x8d\\x11;\\x97EK\\xbc,\\tU<\\x10\\xe7\\r\\xbd\\x88\\xcd\\xd9<4\\xc9\\xb0<\\xb6n\\x08=/\\xd4\\xf2\\xbb\\xbc\\xc1\\x89=B2\\xb8<\\xcf\\xd2o\\xbd\\x1f\\xf9)<\\xc4J\\x89=S\\xb7\\x8f\\xbd\\x1cL\\\"<S6?\\xbd\\xb2\\xea\\x84=\\x9b\\xbcq;\\x94\\xaf\\xa4=i\\x94?\\xba\\xaa\\x87\\xa6=D\\xd9r\\xbdTOl<\\xb0\\x04\\xbc\\xba\\xc0b\\xff<&\\xd9\\xc0\\xbc\\xe1\\x83\\xee\\xbcb\\xd9><x\\x85\\x83<e\\xc4\\xdd\\xbc\\xc1l\\xa4<\\x0f\\xa3\\x85\\xbc\\xe4\\xa9k=\\x90F\\x93<\\xdd\\xad\\x0c\\xbd\\x03\\xd9\\x90<ya\\xc0\\xbc`\\x9d\\xcf<\\xcb[J=6\\x12\\xd9<p\\xc1\\x9d\\xbd.\\xcf\\\"\\xbd\\x7f?\\x84<\\x97\\xcd\\x04\\xbc \\xa3\\xcd\\xbc\\xd4\\xa1\\x1e=1\\x97\\xde<\\x9c\\xbe!\\xbd\\xe4\\x05\\x01\\xbd\\x0f\\x98\\xc2\\xbc\\x7f\\x85@\\xbd\\xf7\\x8cI<Q\\x95\\x91=+\\xc6\\xac\\xbc\\x90`\\x8c<\\x02p\\x05\\xbd33C\\xb9\\x0f\\\\\\x94\\xbc\\n\\x1e\\x8a\\xbc\\xb5tt=\\x9c\\xbd*=S\\xa8a\\xbc\\xc6\\xbd\\x89:\\xb7oF\\xbc\\xc0\\xc6\\xfb\\xbc\\xf4.\\x1f\\xbd\\x00\\xca\\xeb<\\x89\\x07L\\xbc\\xaa\\xa3\\xee:w}:\\xbd\\x89\\x80\\x9a\\xbbg\\x87^\\xbc\\xaeuZ<\\xbf\\\"\\x8d\\xbc\\xb6\\xb2?\\xbd\\x1dT\\x1a=L\\xba\\x13\\xbdK/*\\xbdf\\x8a\\x1b\\xbd\\xb1d|\\xbd\\xc8o\\x7f\\xbc\\x1fm\\\"\\xbd\\x94\\x0b)\\xbd/\\xdeN\\xbc\\x83\\xd4\\xac;Yu\\x8a\\xbc\\x89\\xec<\\xbd\\x05;\\x92\\xbd\\xd8\\x1f\\xc9\\xbc(\\x97j\\xbdGs}<{\\xdb\\xb3<\\xb0\\xa4\\x08\\xbd;Pt\\xbdXv\\x15<?\\xbe\\x05\\xbc\\x1f^3=s\\xcc,:Al\\xff\\xbb1\\x9e\\xb8\\xbcY\\xbdS=\\x1d8O\\xbcf\\xcbg\\xbdE\\xa4n\\xbd\\xd9\\xee\\x17\\xbd\\x9e2\\xac;P40=$o\\n<\\x98\\xa9\\x9c=Q\\x96\\x06;\\x84\\xa7D<\\xda\\xac\\xf5\\xbc\\xfb\\xef\\xb5\\xbd,e-=\\xe9\\x85\\xfc\\xbb\\xacF\\x15=\\x17\\x1cE;NT=\\xbdc*\\xc9\\xba+um\\xbdz\\x05S\\xbc\\xe7\\xad\\x92;\\\"\\xd9\\xcd\\xbb9\\xc7B\\xbc \\xc6&=\\x806\\xcc<\\xd0\\xa0G=\\x83\\xd1\\x86\\xbcV\\xa3P\\xbc\\xa6\\xaf.\\xbdf\\xf1\\xc9\\xbc\\x9d\\xa7\\xdc<\\x05\\xca\\n\\xbd\\xd53\\x1c\\xbcm=B=EYC;s\\x8e\\xe2\\xbb\\xad\\x13;=\\xc1\\xc5\\xf8\\xbc\\xc18\\xe6<`A\\xe0;\\x95\\xda\\x19\\xbd\\xcdZ\\xcb\\xbcT=\\x82\\xbd\\xa3=\\xe9<}\\xdb\\x8b:\\xa7\\x9d\\x85<\\xa6\\xb2@=E\\xb4\\xf4=\\xfd\\xca\\x18\\xbc\\xb18o<\\x8a7\\x9a= \\x9ab\\xbb\\x82h\\x9c=\\x19rB\\xbd\\xb2\\x7f&=\\x9b+\\x9a\\xbc\"\nHSET bikes:10012  model 'Makemake' brand 'Nord' price 4781 type 'Kids bikes' material 'alloy' weight 8.3 description 'These bikes pretty easy to ride while also being lightweight enough and quite durable. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"xgh\\xbb\\xfd\\xcb!=\\xf8\\xc5(\\xbc5e\\x9b< *%\\xbc\\xaaD4<\\xc2\\xc5L\\xbc\\xc1\\xc8\\x17\\xbd\\xd9\\xfc\\xcd=Q\\xab\\xd1\\xbb\\x1d\\x13\\x1b\\xbc2wO<\\xfd\\xa1\\xac<-\\xb3\\x92\\xbc{\\x98\\xf2<\\xb7\\xa36\\xbd`\\xbbm\\xbc%\\x91\\x9d\\xbcoG\\xc5\\xbcV3\\xab\\xbdO=]<\\x91-\\x01\\xbc\\xb8c\\x15=\\xa1\\xd9^\\xbcV\\x9c\\xd1<Q\\x07r=\\xbd\\xf0\\x85;Jet\\xbc?\\xae\\x02\\xbd\\x12\\x92S=\\x85j\\xd6\\xba\\xb9\\xb1\\xe1\\xbcU0L\\xbc\\x1c\\x18\\xe2:\\xbf\\xefQ=u\\xa7\\xfe<\\x8a\\x84\\x9b;\\x19\\xdf\\xe6\\xbb\\xabh\\x06\\xbb\\xf3\\xfa\\x98\\xbc0\\xd3N=3F\\xf5\\xbcm\\xd7\\x8e;\\x81\\xeb\\xac<\\xdd\\xe2+\\xbc\\xbb \\xba\\xbc\\xfc\\xb2\\x86=\\x8c\\xc3\\xa1\\xbc\\x84c\\xca\\xbc\\xde\\xaf\\xb2;2\\xe9^\\xbc9\\xed\\x8b\\xbd \\xbc\\x07\\xbdZ\\x02\\xee;\\xc5\\xf1\\x8d<\\x81\\xc5\\x0e=9\\xff\\x07<Sg\\xb4\\xbd\\xd1\\xe7;\\xbdj\\x00\\x93=\\xfd;\\x81=h\\xf0(\\xbb\\x94\\xfd\\x1c=q@a=\\xddC\\xc4<q}5\\xbc\\x11w\\x99\\xbcb\\xcf\\xb1<\\xa1]\\xd2;\\x88\\x96\\xe5;\\x0e\\x03\\xb6\\xbcj\\x06\\xed\\xbb#\\xdem\\xbd}\\xbb\\r\\xbd\\xd6DG\\xbd\\xc86\\xcc<%\\xf0\\x00<\\\"|\\xc0\\xbc\\xdd\\xfdc\\xbd\\xbe\\xb0\\xcc<rU\\x00=\\x8a\\\\\\xce\\xbcn\\x98H<\\xb5i\\xaa\\xbd\\r.[\\xbc\\xa0\\xf5\\x0c=l\\x07Q<\\x85/\\x19\\xbcI\\xb2\\x98;\\xab:\\xc9;_Z\\n\\xbc\\xb75\\x00\\xbd\\xaf\\xf0\\x9e<\\x1br\\xce\\xbb\\xd5\\xfdB;\\xd1\\x00\\xc0\\xbb^I\\x18\\xbc\\xe3\\xcc9\\xbd \\xea\\n<\\x00\\xde\\xf0\\xbc\\xd4:\\x91<\\xb9u\\x1e=a\\xc0!;\\x1d\\xbe&\\xbd\\x9a\\xcf\\xab\\xbcXM\\xea=<\\xe0\\xc7<0\\xd9\\xea<\\xd4\\xa1b\\xbca\\x8b==\\x021\\xef\\xba\\x8a\\xe7\\\"=QC\\x97;\\xe3\\xf0\\xb5\\xbc|\\xbe\\x1e\\xbdpK\\x95\\xbd\\xffh\\x94\\xbc\\x170\\x86\\xbc\\xa8\\x94\\xf0;\\xc4S`=~\\xf8%=\\xd66\\xcc<\\x82\\xaf\\xb5\\xbc\\x1d\\xaa\\xf1\\xbc\\xb5\\xb9W=\\x10S\\xfd\\xbc6\\x002=\\xc5X\\xa1\\xbd\\xd5\\xec\\x1c\\xbd\\xd9\\x96\\xcc\\xbc\\xe9U\\x9a\\xbc\\xe3U\\xd4\\xbc\\xe3\\xffC=8\\xfd\\xff\\xbanF1<\\xe8\\x85\\x1b=\\\"q\\x8b\\xbcI&\\xce\\xbcL\\xfa\\x1e=\\xab\\xa4\\xdf\\xbc\\x10\\x81\\x10\\xbd#\\xdb\\x17\\xbds\\x97\\x97<\\xf9s\\xa9<\\xb1.#=>gX\\xbcdh\\xa4<-\\xc7\\xff<\\xec\\xe2\\x84\\xbc\\x85\\xa7\\x85\\xbd\\xb9O\\xfc;\\x9bN\\x81=\\x02\\x05\\xf7\\xbcJ)\\t=\\xe1\\xfe\\xbc\\xbd\\x9dm\\xcb\\xbb(\\xb6=\\xbd\\xe5\\xd0\\x86\\xbaV\\x97\\x1c<\\xad\\xdd\\x10\\xbdb\\x89e=h6\\\\\\xbd1\\x8d\\x12<P\\x10\\xd9\\xbc\\xe1\\xc5\\r=\\x0f\\xd7\\xc5<;*\\xea\\xbc$\\xf7G=\\xf2*\\x1c=\\xfa\\x9d\\xae\\xbc\\xc8A\\xa4\\xbc\\x9c\\xc7g=L\\x82\\xb1\\xbb\\xf0\\xeaf\\xbd*\\xf6\\x92;\\x84\\xef$\\xbc\\xf6\\xf1\\xa9\\xbd\\xb7\\xf4\\x91=\\xe9\\xfe\\x96<x\\xb8\\x8a\\xbb\\x9c?\\x8c<\\xe8\\xf9\\xc2:\\xe6<\\x0f\\xbdL\\x90A=\\x0f\\x84\\xdb;\\x9ew\\xaf\\xbc .6=\\x94\\xfc\\xa2\\xbc9\\xd1\\\"=\\x1c\\x9e\\xe1\\xbc\\xbb\\xc9\\x8e;+\\xff\\xb7\\xbcI0\\xea\\xbc\\xe1\\xad\\x07<\\x85\\xc5\\x10<\\xa8x\\x19=}r7\\xbd^9\\x0f\\xbd\\x1c\\x18\\xb3;2\\x05\\x9e=\\xf9.\\xed\\xbbs\\x92_\\xbd\\xe3\\x08\\xe1\\xbdz9H\\xbd\\x9e\\xa7\\x8d\\xbc\\xb4u;=\\x99Q\\xb5:\\xd1\\xde\\xea\\xbc\\rD[=\\x0cPM\\xbc\\xd9\\x85N\\xba\\xffiy\\xbcE\\xfe\\xb0<\\x1e\\xd0q\\xbd(.\\xf5<\\x81]M\\xbdl\\xc7\\xbd<\\xb5\\xa3J\\xbc%\\xd5\\x80=\\xd9\\x92`\\xbd\\xd9\\xd32:\\xc1\\x96A<w+\\xb3\\xbc\\x8bW%\\xba\\x11=Z\\xbc$\\x11V\\xbdu\\xa08;\\xe9\\xedk\\xbci\\x81\\xed<\\x86\\xe3\\x9f\\xbd\\xb6\\xf0.\\xbd\\xe3`I\\xbdxb\\xbc\\xbc\\xc9\\xfb\\x84=:\\x91\\x9b\\xbb~.\\xe6\\xba4\\x878=\\xc0\\xc5\\n=\\x05&\\x0f\\xbc\\xd5\\x13\\x99;%)\\x9f<\\xf1z\\x8d\\xbc\\xe8_(=\\xb34l;\\x92\\x12j<$\\xa6\\x1b=\\n\\xa1p=\\xa2\\xc8\\xcc<\\xb1\\x82(\\xbd,\\xd9\\xa3<W_\\xe0<,a\\xa9\\xbd\\xf0L#;\\xc3\\xbe\\x88=`\\xa7\\x04\\xbdn\\xd4\\xb5=\\xee\\xfcB\\xbd)\\x9a\\xb3\\xbc\\xc6\\xb0^<\\xec\\xa6\\x99=\\xc7U\\xfa;\\xb8\\xf3\\x1e\\xbd\\x08\\x8f9\\xbd\\xdb\\xc0\\xb3\\xbc\\x08\\x10\\xb2\\xbc\\xfc\\xc9\\xc4=l3\\x0e<i\\xee\\xb6\\xbc\\xc5\\xb1\\x10=gI\\x92\\xbc}\\xc7c\\xb8,\\xf07\\xba\\xe0\\xf4d\\xbdG1\\x81<\\xa9eY\\xbcH\\x97\\x14\\xbc\\x97\\xec\\xdf\\xb9\\xb7m\\xc9: \\xea\\xc6<R\\xf30\\xbd\\x89`\\xc9\\xbc\\x94\\x02\\xde;;\\x15\\xdd\\xbb\\xcb\\x89\\xf6\\xba\\xe1\\xd0\\xa4<u\\x0b\\xa0\\xbc\\xfa\\xf6J\\xbd\\x969\\xf4<|m\\x97\\xbd\\xdc\\xaf\\n;j\\x11\\xb8<n\\x97%\\xbd\\x88\\x0e\\xd4=\\xc4&\\x90<\\x9c\\xe2\\xda\\xbc\\xb8b\\x13\\xbd\\xcc\\xd7\\xb0<!\\x80*<nKY=\\x98\\x8f\\xac\\xbc\\x92L^=\\x9e\\xaa\\xe7\\xb9\\xea\\x1e\\x84={\\xfe\\xa1\\xbc%IW<\\x1d\\x80\\n=fO\\xba\\xbc\\x9e{\\xb8<%\\x87^=]\\x86\\xac<\\x95c\\x01\\xbd9\\x9b\\x02\\xbd\\xff \\xdf\\xbc\\x12\\xed\\x84\\xbc\\x8bw\\xce<\\x9f\\x07\\x13\\xbd\\xd3M\\xde\\xbc\\xa8\\xbc|=\\xed#\\x17=!,\\x83\\xbb\\xbf\\xda\\xcc\\xbc\\xa0Au\\xbd\\xcd\\x10l\\xbc\\xaf\\x9f\\xd4\\xbc\\xcbxR\\xbb$\\x06\\x8e\\xbd\\x80\\xb0\\xd3\\xbc\\x04&W\\xbd\\xd5\\xae*=/\\xca\\x98\\xbc\\xa7\\xac\\xc1\\xba\\xbf\\xf3\\xbb\\xbd\\x1e\\x94h\\xbd\\xa2\\x17W\\xbc\\xb8(@<\\x82\\x05,\\xbdj\\x1bx\\xbd\\xd0\\xba+=\\x1a\\t\\xa6;k\\xa7\\x96\\xba\\xd0\\xda\\xc2\\xbc\\xde|\\xe0\\xbc&<\\x85=\\xb8|\\xa8<\\xba#J\\xbc\\xf6Y\\x80=\\x11\\x91\\x12=et\\xf7<[\\x9b\\x1d=\\xa6q\\xb6\\xbc3\\x1a\\xa6\\xbc\\xa10\\x10\\xbc\\x01\\xd6\\x17=\\xb6\\xb9\\x17\\xbcT0\\x8c\\xbd\\x02\\xfd7\\xbd\\x93l&\\xbcb^\\xf5\\xbcR\\x81b\\xbd\\xb1\\xac\\x02\\xbejV\\xa9\\xbb\\xd1N\\x17=<,\\xb0\\xbc\\xed\\x9fg=\\xe9\\xca\\x81<1\\xd5\\x0f=0\\xc0\\x87\\xbcf\\xe1\\x06=\\x1e\\xf5\\xf3\\xbc\\x1f\\xf9\\x15\\xbd\\xb5\\xbeZ=\\xaaP\\xa0<\\xeb\\xe0<\\xbb[/F\\xbd\\xd8\\x17\\xa4;\\xae\\xb6F=8\\x8b\\xbc;m!\\x88\\xbd\\xee\\xc1\\x0c\\xbc\\xaa\\xea\\xe8<\\xee{3\\xbd\\xbeRO\\xbd\\xd01\\xad\\xbc>\\\\\\xa7;axH\\xbdm\\xba\\xf9;oo\\x1c\\xbd\\xc6\\t\\x1d\\xbd(\\xb1\\x11=\\xd2V<\\xbd)\\xc2\\x1f\\xbd\\xe9\\x02\\xa8<\\x11\\x89\\xa9=f\\xb4\\xd4\\xbcK\\x1c\\x18<HF.\\xba\\xca\\x9bo\\xbd>\\xc3\\x1a\\xbc\\xf3@\\x9a=I\\xa7\\xac\\xbd\\x84_\\x15<:\\x95\\x8b\\xbc/\\xd4@=\\xa3\\x11\\xa6<\\xee1\\xab\\xba\\x95G\\xc3\\xbc\\xb9\\x94\\x96=b*\\xc4<z\\xe3\\xb9\\xbc\\xa8dg=\\xda\\xaa+=_\\xfag<\\xbd\\xa8\\xba<\\xdb\\x1c\\xd7\\xba\\xa6\\xc2\\xa1\\xb8\\x0e\\x07\\x8e<\\xda\\x80R\\xbc\\xb0\\xd5q\\xbd\\x0e`\\x95\\xbb\\xa2\\x97\\xab\\xbb\\xc3!/;n9\\x8e\\xbc\\xcb0\\x0e<\\xd9\\xfb.=\\x96\\xd3\\x82\\xbct\\xc7\\x06\\xbd\\x821\\xea;]\\x8c\\x85=\\xd8qg<*\\xe02\\xbd\\x949\\x8f<e\\x89e\\xbc/:\\x1c<\\xb1{\\x86\\xbc1\\x0b\\x89\\xbd\\xcb\\x1d:=\\xad\\xac\\\\=\\xdc\\x82\\x12=\\xe6\\x04M<\\x918F=\\xa3\\xad\\xe1\\xbc\\x87N\\xb8;\\xa6\\x14c\\xbc\\xbd\\xdd\\x06\\xbc\\t\\x9dl\\xbcPM\\x1b\\xbb\\x13\\x88\\xa0\\xbcqht;\\x03D\\x16<2\\x17\\x12=V4m\\xbd\\xb44\\x8d\\xbdBJ\\x18\\xbc\\xc21\\xc4\\xbd\\xf8(Z;\\xb1\\x88m\\xbb&\\xa5\\xeb\\xbb\\xaa\\xda\\xe8:\\xf0 \\xa3\\xbc\\xf9\\x95\\x94=E\\xe8\\xa4=o\\xbe7\\xbd/&Z=i\\xd2+\\xbb\\x1f\\xde~\\xba\\x91\\xa8\\x10=K|\\x97<\\xcf\\xda\\x18\\xbd\\x84\\xea`\\xbc\\x84\\x0b\\x9c<\\x8f\\xe7\\xa0\\xbc\\xd2(\\xb9;\\x18\\xd5\\xee\\xbcG\\x8d\\x88\\xbc\\xd6d\\x17\\xbb\\xa1Q0=/!\\xa7\\xbb\\x12(c\\xbc\\xe6\\xee\\x0b=\\x0c\\xe3\\x02<\\x1d=\\xa9<\\xb9\\xcc$=\\xe9\\xa6\\xa0\\xbc0J\\xce;\\xbc%\\xc1\\xbc%\\xd7\\xa7\\xbd\\xc0^\\xc0\\xbaJ\\x99\\x8c\\xbc\\x04\\xf8\\xdb\\xba|m\\xcd<^\\x81.\\xbc.\\xc4w\\xbb\\x81J\\x8d=Q\\xa9\\xd1<0\\xbc]\\td\\xa3\\x0c\\xbc\\x0c\\\" <i`\\xa6<s\\xc0G=\\xb9Y\\x1d<\\xe0>\\x01=\\x86\\xce3\\xbd\\xbaK\\n\\xbd\\xbf&\\xfa\\xbcG\\xaf\\xcf\\xbc(V\\x15=#O\\xcb<\\x80\\xfdG<\\x1f\\x00W=<$\\x1c\\xbc\\x12\\xbd\\x94<\\xc1\\xb2\\x02\\xbd\\xddt\\xb1\\xb9n\\x9d\\x9a\\xbc\\xbf\\xc3\\xd1<w\\xc6\\xc9:\\x0e\\x10Y\\xbby\\xa4\\x06<\\xb6\\x87\\xb7\\xbc\\x98\\xb8\\x03=\\x04\\x81\\x98\\xbbB\\xf7C<\\r\\xcc\\x16<\\xb0uA\\xba#\\xa5\\x02=<g\\x16<\\xd1\\x00\\x1e\\xbd\\xec\\xc2\\x04\\xbd\\x03\\xdc\\xbf\\xbd\\xe97)\\xbd\\x0bI\\x93\\xbc\\xbb\\xb0\\xab<a\\xb1\\xdd<\\\\\\x158=\\x06 *=\\x11W$<]\\xe4\\xf0<\\xec\\x1c\\x14<\\x0c\\xb3\\xce<\\xc7\\xa9\\x02=\\xaf*u<\\xe7\\xa7\\x87=\\n\\x93\\x8b\\xbc\\xf6\\xb8\\xd0\\xbc\\xa7L\\x05\\xbd\\xac\\xbbk\\xbc\\xf0\\xf5\\\"\\xbc\\xbe\\xc2!\\xbcA\\x96(=\\x8eL\\xfa<\\x8f\\xde\\x11=\\x00\\xbe\\x03<Q\\x85\\xa2<\\xee\\xa3!=\\xe9\\x9b{<\\x97lK<\\x8ew\\xc6;\\xf6\\xbcX\\xbb\\xeb\\x82z=_ju\\xbd\\x80\\xc0\\x1d\\xbd\\xa4\\xdbn\\xbcb-u<\\x1bXx=Xa\\xee;\\x04t\\xc3<\\xe9\\xd9\\xd1\\xbbk\\xaar<\\x10\\xcdQ=\\xb7\\x936<;\\x9f\\xcb\\xb9\\xa6.l\\xbc(\\xe9\\xa5=Z]\\x15\\xbd]\\x97\\x83=\\xb0M\\xe4:\\xc8\\xc0\\xa3<\\x1d(\\xc0\\xbbw\\xaaP=\\\"\\xbe\\xd1\\xbc\\x07\\x81\\x91;\\xab\\xe5\\x91=P\\xf7u=\\xd47E\\xbc\\xc9\\x83\\xf2<8\\xb6M;\\xe9B\\x83;C\\xc4!=\\xb9\\x18\\x8c:\\xf8sb=\\x00)\\xaa<rL\\x0f\\xbd.*\\xa5;E\\x1f\\x94<y\\xa0\\x18=\\\"@\\x08\\xbd\\xba\\xc18\\xbd,@G<\\xb1\\xa46\\xbd\\x8e\\x0f\\xc4<\\xed\\r\\x08\\xbdx,x=\\xc7\\x08\\x96\\xbb\\x8d_\\x9d\\xbdk\\x1f\\x15=\\xba\\xa7\\x01=\\xb7o7\\xbd\\x9d\\x03\\x0e\\xbd\\x8a\\xb7\\x86=\\xf1\\xf4*=\\x98\\x04\\xb8<\\xeb\\x9ai=\\xfc\\x88\\xdb<|4\\x98;/\\x9a\\x8c\\xbd\\x0ef\\\"=\\xfe\\x83-\\xbc!\\x16\\x7f<\\x91\\x9a\\xf69\\x08\\x8d\\x9d<3e\\xab\\xbcPz[\\xbcSyw;tY9=\\xe4G\\xc4\\xbd\\x89E\\xa5\\xbc\\x11>=\\xbdx\\xb9\\x93=\\xba\\x0e\\xb79J{\\xb0=\\x0fB\\xb1<\\x8bj\\x9e<\\x96\\x10@\\xbd\\xdb\\xc1T\\xbc#\\xdc\\x1b:\\x0b\\xa6c\\xbc\\xcf\\xef$\\xbd\\xc8O\\x18\\xbd\\x13j\\xad\\xbc\\xbcU\\xc3;]UN<F\\xc0\\xbd\\xbc\\xee\\xd79\\xbd~\\xd1\\xf9;q{(\\xbd\\xeb.\\xc2\\xbc\\x8c\\xd8\\x8c\\xbc\\x9f\\xfe\\xb2;%S\\x96<Z\\x90\\x16=`\\x15r<\\x13\\x0f\\x02\\xbd\\xa1U\\x1a=\\xf7=\\x0e=9\\x1b%<\\x06\\x93\\xfa\\xbb\\xc2\\xb7\\xac=\\xae\\xed\\n<R\\x9d\\xb5<~\\x10\\x82<\\x0c1\\x93\\xbcg\\x7f\\x13\\xbd<\\xe9\\xae<\\xd5?N=\\xeb\\x90+<\\x00\\x94\\x03\\xbc\\x84n.\\xbc<\\xe5\\xd8<R/\\xa0\\xbc\\x00:\\x8c\\xbc(\\xc43=\\xa0\\x07\\x8f<\\x84\\xb1\\x9e<%\\x8d\\xd4<\\xa89\\xf9\\xbb_k\\xf5\\xbc\\x10V\\x82\\xbdq\\x9b.=8\\xc3~\\xbc\\xb2Y\\xbd9\\xb8\\xeb\\xe6\\xbcw\\xaa\\xac\\xba\\xfd\\xa1\\x0c\\xbc\\xd0o\\xb1<\\x01\\x03\\xaf\\xba\\x9f\\xdf\\xec<\\x02\\xb4~<\\xf3L\\xbf\\xbc\\xed\\x0b\\x9a<DU?\\xbd\\xb5\\x12\\\"\\xbd\\x9ar\\x8a\\xbc\\xfbk\\r;\\x08g\\xe5\\xbc\\xacA\\x9f<i\\\\V\\xbd\\xb4\\x95\\xac<0\\n\\x87\\xbb5\\xb5\\xbf\\xbd\\xde\\xe50\\xbd\\xb2\\x93i\\xbd&\\xab)<\\x9d\\x86~\\xbb[e\\x1c\\xbbzB\\x1c\\xbd0\\x82\\xc4<\\xa9l\\xee\\xbb\\xd0\\xc2c<R\\xde\\x1a\\xbc\\xd3+\\x7f\\xbd`\\xde\\xe4\\xbb\\xe5k\\x00=\\x92\\xf2\\xf0\\xbc\\x06\\x19?\\xbd\\x0b\\xc9V\\xbd\\x81\\x84\\xe3<\\xf7w\\x9c\\xbd\\x01\\xf4\\x04<\\xfb\\x9c\\x0f=\\xde\\xb3\\x9f=\\xa2}\\xa0;\\xdbF\\x80\\xbb\\x9d\\xeb\\x9e\\xbc-\\x00\\xb7\\xbc\\x9b\\x89\\x16=\\xd2\\xaaz<!\\xe0\\xd2<\\x93\\x9dx\\xbc\\xb7,\\xcf;\\xff\\x069\\xbc\\xa7\\xc2\\\";\\x937z<4\\x08\\xbc\\xbc\\xce\\x1f\\x8c=\\xebJT=\\xcd~\\x1e<iy\\xf1<\\xe5\\xbe$=\\x1cnM\\xbc\\xa4\\xcc\\x00\\xbd\\x82r\\x91<\\xc9nH\\xbd\\x88\\x98\\xa9<\\xf9\\x9f\\x82\\xbdU\\x94\\x11\\xbc\\x1b\\x05\\xb6\\xbc\\x16\\x90\\xa3\\xbc\\x89\\xac\\n\\xbb\\xd9\\xfc\\x7f=\\xaa\\xff\\x0c\\xbd\\x1fn\\x0e\\xba\\xee\\x9eM\\xbd\\xedy\\x13<{\\xee\\xac\\xbc\\x92\\xe2\\xb7\\xbdBY\\xa6\\xbc\\xf4\\x9d\\x1b\\xbce\\xce\\xca<\\x8d5D=W\\x9f\\xa4=V\\xe0\\xfb\\xba)\\xb04<\\xc9\\xb1\\xc6<d\\x82\\x96\\xbd\\x11d\\xde<\\xec\\xf2\\xf5\\xbb\\x00\\x13 <\\xf0.\\x06\\xbd\"\nHSET bikes:10013  model 'Kirk' brand 'Classic wheels' price 2601 type 'Mountain bikes' material 'aluminium' weight 14.5 description 'This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\x95\\xc7\\x0b\\xbd\\x9d\\xf7\\xa0:\\xb7\\x93Q\\xbc\\xfc\\x10A=\\x1a\\x7f^\\xbb\\xd3\\xd7|;\\xda[\\x86\\xbcx\\x0b\\xb3\\xbc\\xc5F\\x1e=\\xbd \\x98<\\xe0W\\x8b=\\xcb\\xe0\\xbb\\xbcb>\\r=\\xf73\\xaf<\\xcc\\xf2\\xeb=\\x14Z\\x8c;Jzy=\\xab\\xd4\\x87\\xbd8\\xc01=\\x8c\\x10\\xa6\\xbd\\xc4\\xeb+\\xbc\\xc9<\\x00\\xbd)\\x93\\xa2<]\\x12!\\xbd-\\xce\\x16\\xbd\\xc9k\\x16<\\xf8\\xffl={\\x8c\\n\\xbd\\xfd@\\x8f\\xbd\\x00\\xca\\xe0=\\xce\\xe0\\xa7\\xbcwu\\x13\\xbd#\\xe3\\xc6<\\x15\\\"\\xef<\\x8b\\x1a\\x8f\\xbc\\xab=6<\\x08\\xc8E<\\xb2\\xc2\\x0c\\xbc\\x1f\\xfa5\\xbd\\xbc3[\\xbbjM\\xce=\\xe6\\x9a\\x92\\xbc\\x97]\\x8a=\\xf2\\x88\\xbe;S\\x92\\xc3\\xbb\\x92d\\x96\\xbc\\xaa\\xc3\\x01>-G\\xa2\\xbcd\\xfa\\r\\xbdh\\x8d5<J\\x0b\\xc5\\xbc\\xc89\\xd7\\xbci4\\x03\\xbd#\\xf1\\x07\\xbc\\x13\\xea\\xd5\\xbc\\x16\\xfc\\xa5\\xbcmsV\\xbbM\\xc6\\x7f\\xbbv?\\xed\\xbc\\x03\\x03v=\\xa1N\\xdf\\xbc\\xa5\\x89_\\xbb\\x87\\xed\\x89<^\\x08\\xe2<w\\xf4\\xc1<B\\x11\\xfb<8Q\\\\<\\xd7-\\xc3<\\xc3<\\xda<v\\xef\\x80\\xbd\\x98\\xbd\\x06\\xbb.<\\x8d<\\xb1\\x1d\\\"\\xbc\\x9e\\tu<_/,\\xbdt\\xcc\\x1e\\xbd\\xae\\x8a\\x99<\\x96\\x105\\xbd\\xa9\\x12\\xbd\\xbd\\n\\xa2\\xa7\\xba\\xc4u\\x03=`\\x10\\x12\\xbd\\x99\\x9d^;\\xe1\\xaba\\xbdF\\xcf\\x9c<]\\x0b]\\xbcF\\x8f\\xab<\\x86_\\xa5<\\x95c\\x10=\\xaf\\x94C<\\xcfG\\x04\\xbd\\x91\\xda3\\xbd\\xca\\xb9*\\xbd\\x97\\xebR<\\xdb9\\xde<\\xcf\\xc3\\x06=3/\\x94\\xbd\\xed6b\\xbdp-\\x1e=5\\x07p\\xbd\\xb3\\x94\\x8d<6O\\xb8<\\x135\\xc9<\\xd8}\\x97\\xbc\\xdf\\xb6\\x90\\xbc\\xac\\xc8\\x8d=\\x06 \\xab<h\\xb3\\x88;\\xf8n\\xc7\\xba\\x8asd\\xbc>\\xd6\\xd9\\xbb?\\xd47=\\t%\\xa6:C&\\x86\\xbc\\x88\\x1d\\xf2\\xbc\\x0f\\x00f\\xbd\\xc8\\xf7\\x01\\xbcR\\xbc\\x0c=3\\xa3\\xe2<\\xf8r\\x16\\xbd\\x97\\x9a\\xfc\\xbbM\\x0e\\xf0<a\\x7f-\\xbdj\\x81$\\xbd\\xc45\\n=df\\x96\\xbc\\xbe\\xaa\\xa1;\\x1e\\xd8\\x8c\\xbd\\xeb\\x99>\\xbd4\\xc3d\\xbdX$\\xb3\\xbcrC\\xed\\xbc|\\xea\\t</uv\\xbd\\x99\\x0e\\xc0:\\xed\\xf4E=\\xbd\\r\\x92\\xbb)\\x03\\xf5\\xbc\\x0f\\x9a\\x88<\\x04k\\xd2\\xbc.\\xa5H\\xbd=6M\\xbdu\\xcb)<\\xcb\\x0f\\xc9=\\x90\\xa8\\xee9\\x93Kn;\\x88\\xa7\\x18=.\\xdd\\xa8;\\xe4\\x15\\xb4\\xbc\\xf7O\\xcc\\xbb\\xb9=a\\xbc\\xbf\\x96o=\\xc2\\xf3\\xdc\\xbc\\xd6`m</\\x883\\xbd\\x18!\\xd29\\xdc\\x01\\xf0;\\xd2\\xe0\\x03\\xbd\\xe3\\x97e=\\xf3\\x9c\\x19\\xbd\\xd6n\\x1c=\\xc4\\xc3\\x9b\\xbb\\xc1\\x82\\xa9\\xbc\\xbf\\x90\\x9b\\xbd\\x03\\x8c[;\\xefi\\x16=\\x9a\\x89\\xe2\\xbc\\xa4\\xc2\\xcc<~;==Em\\x16=\\xbbX\\xe18\\xb8}5=\\xde\\x02Y\\xbb2\\xe2\\x08\\xbd-\\xe5\\xe0\\xbc\\x1a\\xb5\\x93<v\\x81\\x92\\xbcuL\\x80=N\\xaa\\x9f\\xbcy\\xe1\\xf2\\xbc\\x17N\\xe4\\xbc\\x9f>w<\\x7f\\xbaw<B\\\"\\x9e\\xbc\\xee\\xc4\\xe6<\\x9b\\xda\\xe9:\\x1b\\xbf\\x16=\\xfd,$<\\xfb\\x872<\\xda\\xc4\\x7f\\xbc&\\xe8\\x1a=\\x81\\x06R=N=\\x9f\\xbc\\x00\\x90\\xc3<\\xb4JO<B\\xf8\\xc9\\xbb\\xd6eZ\\xbd\\xa6\\x9e\\xf5\\xbcv\\x94\\x85\\xbc\\x1b\\x02\\xe3<\\xe5\\xc0\\xbf:\\x9b\\x90\\t\\xbd\\x15\\x87\\x9c\\xbbVE\\xd7\\xbc\\x00.i\\xbc9\\xd6\\xe8<?\\x9f\\xc8\\xb9\\xc8\\x8b%\\xbdA\\xab\\x99<\\xb2\\xf2\\x97;$[\\x07\\xbd-f\\x19\\xbd\\ta<=m\\r<\\xbc\\xad\\xd0\\x03<k\\x00\\x92\\xbc\\xd9\\x93\\xcf\\xbb\\x02\\xa0\\x0f\\xbd\\xddHP<W\\xfd\\x95\\xbd#\\x99\\xbf<!\\x98\\x1f\\xbd\\x00\\xda\\xfa\\xbc\\xb87;<\\x8c\\xa1\\x06\\xbcO\\xee\\xca\\xbc%\\xac\\x19=X\\x061\\xbb!\\xc0\\x91=\\xcf\\x92\\xff\\xbc\\rb3\\xbd\\xf4)\\x86\\xbd\\xcc\\xd1*\\xbc\\x08bc=\\xca\\xaaH\\xbc\\xe4\\xf7\\xb3<\\xcc\\x17\\xee;H\\x81\\x86=\\xc6\\xbb\\x8b\\xbc>\\xb7~;&\\xd2%\\xbd\\xc7Mr;-\\xe9c=\\xfeP\\xb8<6\\x1b\\xc7<\\xc8\\xd6\\xa3=\\x0e&\\x1a<Ac\\x00\\xbew\\x90?\\xbd\\x0bC2\\xbb\\xd4\\xcd\\x06<\\x98\\xab\\xe1\\xbcS/v\\xba\\xddm\\xa6<o\\xa4!\\xbdv\\xf1u=\\xf1\\xed\\x06\\xbd5\\xefu<\\xe2\\xf6\\x16\\xbc\\x82L5=\\xb0\\xf3\\xb4\\xba\\x8d\\xe9\\xf1;k\\xb9\\x80\\xbc\\xe9S\\x18<7B\\xb5\\xbc\\xbb\\xa9w=\\xcb\\xa4\\x81;\\xa1\\xe2\\x02\\xbd_2_<\\xda\\x05\\x1e<i\\xed\\xf8\\xbcE\\xfb\\xaf;v/\\xd0\\xbcoI\\xe4<\\x84\\xe4K\\xbcn\\xfew;:\\x9b5\\xbcs\\xb0\\x95\\xbc\\xa1 \\xba=hy\\xa6\\xbd\\xd6\\xed\\x03\\xbds\\xb0\\x8a<\\xd1\\xfb\\xaa\\xbcr\\x18I<=\\xec\\x97<\\x0c:m\\xbcX\\x9a\\x8e<\\x9e\\x02\\xb3<\\xd3\\xa3\\xae\\xbdP\\xec\\x7f\\xbb=\\x9c\\xa0<\\x1b\\x12&\\xbd\\\"33=UGg<\\x9c>\\xfc<\\xf3\\xd72\\xbdp/\\xa1;\\x87\\xe3\\xe6</d&:\\xcfw\\x12\\xbd\\xa2R\\x85=(\\x83\\t;\\xe9\\xb18<j\\x1d\\x01\\xbd\\x1cv\\xaa<wc\\x83\\xbc#0<<#\\\"\\xec;T9h<lS#\\xbd\\x93^\\xad\\xbb\\xb2\\x9e\\x87\\xbd\\x1f;\\\"\\xbc\\xc1`\\x0f\\xbc<M\\x95<\\xf6\\xf9w\\xbdj[\\xa1\\xbd\\xc6(\\x9f=+\\r0=\\x95\\x85\\xdb\\xbc\\xa5\\xb8\\x1c\\xbd0\\\\v<\\xbb\\x87\\x07;%\\xc0\\xcb<\\x17m\\x80=\\xca\\x9cW\\xbd\\xba\\x1b\\xaa\\xbc\\xca\\x82V\\xbc\\xb8N:=\\xb3t\\xa6;\\x91x\\x89<O[\\xbb\\xbd\\xa6x\\xc6\\xbc6n\\x99;\\xd5\\x8f\\x03\\xbc\\xcb\\r\\xd1;\\x0b\\xcb\\xcb\\xbc\\xd4I==W\\xa7\\x1c=\\x9d~\\xba<&v\\x10\\xbc\\xb5d\\xdc\\xbb\\x03\\xed!=\\x8b\\xb2\\xec\\xbc\\x89\\xb0\\xb7<G\\xc3!=\\xa1m\\xac=\\x8aZ\\x94<U\\x84S=3\\x1f\\xc4<ci4<K\\xa0\\xfd\\xbc\\x8e)\\x93=\\xf9\\xa4\\x0b\\xbd\\xa3\\xd0\\x1d\\xbc\\x92\\xeb\\\"<qr\\x81\\xbc&\\x0bW\\xbd\\xf7\\x01>\\xbdY4\\x01\\xbe\\xd7\\xc6\\xfa\\xbaq\\xb8I=\\xa6Z\\xa6<\\x9b\\x96\\r=\\x0b\\tY;\\xdb\\xadP=/K\\xaf\\xbc\\x8d\\xce><\\x0f\\x83^\\xbdt\\x13\\xc4\\xbc\\xc7\\xc8\\x85=EE\\xd5<\\x80\\x94\\x9d\\xba\\x17F0\\xbd6wj=.\\xbeo=\\xf0\\x860\\xbdoq6\\xbd\\xdfC\\xf9;\\xef\\xd3\\x11=\\xae\\x13$\\xbd\\x9d\\xa7q\\xbdi\\x82H\\xbd\\xb9(W<\\x14\\xfeI\\xbda\\xd3\\xcf<\\x13\\x0eX<\\xfc\\xe77\\xbc\\x8d\\x03U=\\x8f\\xbe\\xe4<\\x88\\xc7p\\xbd\\xe3\\x82\\xac=\\xab\\xdc\\xd9=W:\\x89<\\xf0\\x16\\x12\\xbc{\\xf2\\x9b\\xbc\\xb7\\xcc\\xb9\\xbd\\x16\\xbb\\xa1</\\xa5\\x80;\\xf21\\x1e\\xbdM~\\r<9\\xab\\xa4\\xbd9\\n\\x11=\\x96y\\x82\\xbbQ\\x87\\xaf;\\x18\\x97\\xfc\\xbb7M\\x1b<*\\x8b\\xa9\\xbch\\xf6Q=\\x98\\x98\\xc5:\\xf0n\\\\=\\xe6\\xdbo\\xbc\\xde~j\\xbc<p\\x0b<\\xba\\xack<p\\xd7\\xb7<\\x0e\\x9f\\xd7\\xbc\\xe9\\xb1\\x08\\xbd\\x85\\xd8\\xc9<\\x888u\\xbc.Y\\x85;UJ\\xaa\\xbc\\xba\\xf4q<\\xc5\\x90\\xf5<\\xb2{\\xe3\\xbb\\xbf\\xbe5\\xbdJ\\x0e\\x90\\xbb\\xce&\\x0c=\\x0f\\xfbY\\xbd\\x00\\xc8\\r\\xbc\\xe1\\x83T<0\\x81\\xf7\\xbc>h\\x1c\\xbbD\\xa6\\x8c\\xbd\\x16\\xbd\\xa7\\xbc\\xc3\\xd0F=\\x82\\xa5\\x1c=\\xd4\\x1a+=\\xb6}\\x80\\xbc\\xbck\\n<\\x02\\x84n<\\x86\\xbe}<\\xad<f\\xbc1?\\xa0\\xbc\\xe3q\\x14<db`<s8X<\\xbc\\x96-\\xbbE\\x0b\\x04\\xbd\\x0e\\xb0\\x7f=E\\x93\\x91\\xbd\\x98R\\x8a\\xbdwl\\xc0\\xbc\\x9c3\\x8b\\xbd\\xf7\\x07c=\\xbb\\xac3\\xbb\\xcb<H<\\xd1\\xa9\\x80\\xbc_\\x03\\xa4\\xbc ~\\x83=&\\xcb\\x00=\\x80\\xb5\\x92;\\xe4\\xdd;=\\xdf\\xa8\\x85\\xbc\\xd6\\xeb,\\xbc\\xe84\\x0c=\\xe9c\\x12=e\\xfc\\x04\\xbdBx\\xe0<\\xba\\xb8\\xda<A\\xf1\\x8e\\xbc\\xf9k\\x8c<)4\\xc7\\xbb7\\x9e\\xa6\\xbb\\xbd@\\x8d<m\\x8d\\xea\\xbb\\x1e\\xb3\\r\\xbd\\xbaY\\x1e\\xbd`\\xed\\xb1<\\x04\\x80\\x82\\xbc}\\x80\\xfe<\\xb2\\x8a\\x8f<\\xff\\xbf\\xb1\\xbc\\xabX\\\"=\\x87M\\xe1\\xbb\\x9a\\xd6\\xc5\\xbd\\xfe\\x85c\\xbd:s\\xd1\\xbbT\\x02u9\\xb5\\xa2&=k\\x89/\\xbd\\x7f#\\xe7;\\x9d[l:\\xe9\\xa1\\x9e\\xbcG\\x13X\\t]Z\\xb0<\\xb7\\xc7B;AL\\xbf;\\x97\\x1e\\x96;\\xb0\\x90\\x88\\xbcg\\x92-=+]\\x07\\xbdI\\x91C\\xbc\\xff\\x8e\\n=\\xe4\\\"b\\xbc\\xb6\\xefX\\xbc!\\xc1\\xb9;\\x00=\\xe3\\xbc^\\xdc9=\\x8ep\\x84\\xbc\\x9f\\x10\\xeb<14\\x8d\\xbd\\x84\\x19[\\xbc\\x06\\x16-\\xbc\\xf7%<=\\xa9\\xe7\\x08=S\\xc9\\xa3\\xbbbP\\xa1<\\xce\\xe7\\x1c=\\x9f\\xd0\\x88<\\xa1\\x19\\x05<\\x1b\\xdd\\xb1<\\x88\\x9d\\x95<\\x0e\\xe7\\x04\\xbdv\\x941\\xbd\\xbc\\xc6\\xb7<(C\\xbf:\\xf0\\x99\\x1f\\xbc\\xaf\\xc8\\xdc\\xbc*\\x0b\\x98;\\xa7\\xe0\\xfb<\\xd2\\x88D=\\xa4t\\xfc<\\xcc\\x8c]=\\xc4d`<\\xcek\\x05\\xbd1\\x93`\\xbc\\x9cB\\xfd<\\xd2\\n^9\\xa2\\xa4[=mb<<:\\r\\xe5=\\x99\\xd99:,\\xa8\\x9a\\xbcm:d<\\x143\\x06\\xbd\\\"\\xec\\xa0\\xbd\\x7f\\xd6\\x0c=\\xef1\\x9d<\\xf0\\x1f\\x00=u\\xd3\\x94<am\\x8a\\xbcU \\x00\\xbc\\x1b\\xc4X\\xbcI\\xe5\\x96<!\\xd6\\xc1<A\\x14o\\xbc\\x81tU\\xbb+%\\x03=\\xb5\\xfd\\xc9\\xbbvl\\x8b\\xbdQr\\xf5\\xbb\\xa8\\x1b\\x96\\xbc\\x16Q\\xa9<\\x91 q\\xbcU\\xa8\\xae<\\xbb\\xce\\xcc\\xbc\\x9b\\x95\\xc4<\\xaeK\\x15=\\x00\\x17_<\\xc2\\x07\\x08\\xbda/\\x80<.\\xb6\\x18=\\x89\\xc3\\xbd\\xbb8\\xe2\\xa4<\\xa5;\\xba\\xbc\\xb1\\x96\\x89\\xbc\\xff\\xee\\x82\\xbd7\\x88\\xc5<\\xba\\xda&\\xbd\\xee\\x8f|<E\\xb4_=\\x01\\x0fB=\\xaf\\xb3\\x9a;Q\\xd5r<\\xcf\\x0c\\xef<\\xf0V\\x0f=n*-=\\xad\\x1e\\x80:\\xf1\\xce\\xff<\\x1ae\\xfc<k\\xd9B\\xbc\\xa3\\x95\\x7f\\xbb\\xa1\\xdfc<0\\x81\\xe3<\\xcf\\x03\\xb1\\xba?\\x86\\x04=\\x01\\x1e\\xd2\\xbb\\xd5u\\xe5\\xbc)k+=ZB\\x94\\xbb;6\\xd9<?\\xd7\\x99\\xbb+((\\xbd%$\\x19\\xbd4\\xecw=\\xaa\\xff\\x8f\\xbdH\\x9e\\xf3\\xbb\\x8a\\xf4\\x80=st\\xad=\\x88\\xa0\\x86<\\xe67\\xb2<\\x00\\xc7\\x99\\xbc\\xf2o\\xa2\\xbc\\xfa\\xf0d\\xbdb\\x83N=\\xdc\\xbdg<\\xbb\\xd7M\\xbcqR\\x0f</\\x05+<(e\\x03=u/_\\xbd^o\\xf8<\\xb5p\\xa9<\\x16\\x93\\xa0\\xbd\\x9b\\xac\\x17\\xbd\\xcd\\x9b\\x92\\xbd\\xadk\\x88<\\xf7\\x11h\\xbc\\xd8`\\x95=\\x80\\xd0t<1\\xd5\\xd5<l(S\\xbd\\xa6^\\x89\\xbce\\xdb\\xcf\\xbc<\\x9f@\\xbbn\\xcaj\\xbd\\xb0\\xe5\\xcd\\xbb\\xdd):\\xbcSq\\x13=\\x13\\x00\\xf2;N\\xef\\xb3=P\\x97\\xa5\\xbc\\x10\\x1et=m\\x145=\\xf6\\x00\\xd5\\xbcw\\xa2\\xc7;\\xb8A\\x80\\xbcH\\x0f\\x8c<\\x05&!=\\\"n\\xae<\\xaf\\r\\x84\\xbc\\xacY\\xa4=J\\xa6\\xb3<\\xea\\x0e:< \\x8d$\\xbc\\xee\\xd2j<E\\x11_;J\\xf9\\xa2\\xbc\\xaai\\xe6\\xbcu\\xf8\\x94\\xbc\\x8a/\\xab\\xbd[\\x98\\\"\\xbcjW\\xb3;;\\xc3\\xb7;\\xfa\\xe3\\xb4<\\x92\\xd0^\\xbd+\\x15\\xc6\\xba\\xde\\xf9$\\xbc\\xe7\\x15\\xa69\\xdf\\xaf\\xbc<%v\\xf9:\\x06\\xb7}\\xbc7\\xfdR=\\xf4q\\xed\\xbc\\x86\\xb8\\xe8\\xbc\\x87\\x83\\xd1\\xbd\\xc6\\xa2\\x91=p\\xd8 \\xbdo\\x85\\x7f;\\xfc\\xf9$\\xbd\\xbbn\\x0b<\\xb8\\xd6\\x80\\xbc\\x9f\\x9d\\x03=\\xe4\\x98C\\xbb\\x7f\\xdc/\\xbd\\x12X\\xf5<\\xfe\\x98.\\xbd\\xc2\\x90\\x8f:\\xf7\\xba\\x17\\xbd\\x17j\\x0f\\xbc\\x06\\xc8\\x07\\xbc\\x8b\\x16Z<X;\\x05\\xbd\\\\J\\xf4:\\x8e\\x88\\x85\\xbc\\xde&\\x05\\xbdDF\\x8a\\xbcS\\x11\\xa8\\xbd~\\x97:\\xbcO\\xb6L\\xbc&\\x92\\xb5;\\x8e\\x90\\x11<\\xc4f\\xb0\\xbbL\\x85\\x81\\xbd\\x14jS<\\x1c\\x83\\xae\\xbc\\x8f\\xe5<=\\x01B\\xd2\\xbc\\xdb\\xb9\\x03\\xbb`\\xaf\\xf0\\xbb\\xc62\\x1a=BS\\xa3\\xbc\\x98\\xb8\\xa0\\xbd\\xdd\\x96C\\xbd\\xee\\xd2)\\xbca\\xb4\\xe6\\xbc\\x84\\xa2e=\\xc4\\xa6C<\\xdd@w={\\x06\\x82<\\xca\\xf6-;\\x98\\x1bF\\xbd\\xc3\\xb7\\xae\\xbd\\xda\\xce\\xd3<ro\\x8f<\\xfe\\xc8\\xb3<|\\xd8\\x1a\\xbcYp\\x11<!\\xf1r<A\\xd2\\xee\\xbc_\\xb2\\xbf<,l4=\\xbc\\xa3\\xc9\\xbb@\\xb2P;;e\\x99\\xbc\\xd0\\xb8\\x0f=_\\x0f\\\"=\\xa7\\xef\\xd4<\\xe2z\\xc5\\xbb\\xe9\\\"M=\\x8e^e\\xbd\\xa8mS;\\x8b+!\\xbcU\\xab4<8V3<\\x176\\xb3\\xbbE\\x0c\\x91:\\xafKG=\\xa9H\\xec\\xbc\\xbd\\xd6\\xdb\\xbc\\xc5?\\xda;up\\xa5\\xbc7J\\xd6\\xbc5\\xc8]\\xbd\\xdeY\\xff<\\x1b\\x0e+=\\x08M\\x10=\\xf6G\\xa9<\\xaeG\\x8c=Z\\xcc\\x80;\\x05\\xc4\\x0c=\\xea\\xc0\\xb8=\\xa8\\xde\\xaa\\xbc\\xc0\\xbc\\xba<R\\xe5\\xda\\xbb1l\\x1f=\\xca\\x1a{\\xbc\"\nHSET bikes:10014  model 'Umbriel' brand '7th Generation' price 983 type 'Road bikes' material 'alloy' weight 13.4 description 'The bike has a lightweight form factor, making it easier for seniors to use. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\x1f\\xd6\\xf1\\xbc\\xd9i\\x19<\\x8f\\xfe\\x8f\\xbc\\x00^\\xe3<cI\\xe0\\xbc*p\\x16=\\x88CH\\xbcPmv\\xbc\\x94x\\x8a=\\xb5)<=\\x0ev\\xd1<\\x9f(\\x06<\\x03\\x1d\\xd4<yp\\xe1\\xbby\\x81\\xcf=\\xb4\\x1a\\xb5\\xbc\\xe0\\xed\\\\=\\xb5\\xa1\\xc0\\xbc5\\xe5\\x17=\\x0e/\\x8e\\xbd0\\\\\\x0f<\\x90\\xedc\\xbd\\xd6\\\\9=\\xc5\\x1eO\\xbdo\\x18\\x87\\xbd\\x96\\x1b\\x05\\xbc\\xc1%M=\\x16\\xd4\\x0f\\xbc\\x12J\\x93\\xbc\\xe7\\xd7\\xe0=\\xcf\\xe1\\xb9\\xbcT\\xa8=\\xbd\\xb8!\\x8a<o\\xcd\\xd5;m\\xd1\\xad\\xbc\\x8a\\x86\\xcf<\\xf4\\xe4\\x87<\\xed\\xc3\\xc8\\xbbV\\xa7\\x17\\xbd\\xf1l\\xa2;\\xb9\\xaa\\\\=\\xe8\\xd5\\xce\\xbb\\xc2\\xf3Y=\\xf6\\xa6\\xeb;\\x90\\x91\\x04\\xbc\\xe9\\x1a\\x91\\xbcWZ\\xf5=~EM\\xbdU\\x92\\xc9\\xbc4\\xf8\\x8a<E\\x03\\x02\\xbd\\x9b\\xb0\\xdb\\xbcc\\x8d\\xaf\\xbc\\xd9\\\"\\xa29i\\xcd\\x04\\xbd\\xe2kU\\xbc<\\x89U\\xbb\\xf7\\xc4(\\xbb\\xf6\\xdfF\\xbc\\x03(\\xa9=G\\x9d\\xb0<\\xbd\\x9e.<}\\xfa\\xae<\\x90\\x9b\\x9c<\\xb3\\x1e\\xba<\\xe0~#<\\xf1\\xa2\\x16\\xbb*\\x96\\x89<\\xe3\\x91\\xc7<\\xd4\\xc3\\x18\\xbdp\\xb0\\x0c<\\xedg#=B\\x0b\\x85\\xbc\\xe6\\xa4\\xbd<{\\xeb\\xf8\\xbc\\xcbJw\\xbc&\\x9e\\x00<:\\xf5\\x0c\\xbd2Uj\\xbdnc\\x95;<\\x1f-=\\x9e,\\x15\\xbd\\xc6V\\xc4:->\\x85\\xbdZe\\xa9<\\x08!\\x80\\xbbOBn<\\xc3\\xd3j<KB/=rw\\xa8:\\xb5\\x05\\x01\\xbd_\\xba/\\xbd\\x8c\\xd4\\\\\\xbdpW\\xac<\\x8e\\xdc\\x01=\\xfb6\\xbf<Y\\\\<\\xbd\\x18XJ\\xbd\\x9e\\xca^=\\xb6\\xda)\\xbd\\x86\\xd2g;\\x07\\xe2\\x8f<w6\\xfc<\\xb8\\xd1$\\xbcY\\xa4\\\"\\xbc\\xb9m\\xc0=Z>Z: ;\\x0f\\xbc\\xc5\\xfc1\\xbcM\\xf1\\x9c\\xbc\\xd3&b;\\x90\\xeb(=\\x14ET<\\x9e0~\\xbc\\x0e\\xf9\\x0e\\xbd$\\xc6b\\xbd\\x1a\\x97\\r\\xba\\xaa\\xa7\\xe3<Fw3=M\\xf0A\\xbd\\xfa\\x84\\x0b\\xbc\\x90F\\x87<\\x1f\\x8a \\xbd\\xd3\\x9b\\xfd\\xbc\\x12\\x8f\\x02=2_\\x94\\xbc\\xa9\\xb9\\x87\\xbc\\x11l~\\xbd\\xaa\\xf3\\x01\\xbdK\\x1f\\x80\\xbd\\x95L\\x08\\xbd\\xfe\\xe0\\x9c\\xbc\\x94\\x1b\\n<\\xca\\x9b\\x0e\\xbd\\t<]\\xbbt\\xa0>=\\xf1OJ\\xbc\\xe5\\x9c\\x9d\\xbc\\x1d\\xb7\\xa5\\xbb\\xe1\\xb4\\xcd\\xbc\\x9a\\xe4\\x0b\\xbd\\xfe\\xf7\\xff\\xbc\\\\c\\x9e;D7\\xf3=\\xff\\xac\\x85<\\xf8\\xd3\\xb6\\xbc\\x85A\\xc8<\\xbe{\\xb4;\\x1f\\xf1\\x9f\\xbc\\xbb\\xce\\x90<^&\\xe9;\\xd5\\x9d`=\\x8e(!\\xbdr\\x1e\\x02=B\\x17\\x97\\xbdW1i:\\xee\\x9d\\xa7<XS\\x1c\\xbd@R9=F7\\x9d\\xbd\\xd8\\x97\\x13=%2\\xe7<C\\x83M\\xbc\\xcf\\xe4v\\xbd \\xb8\\xa2<\\xcaCK=\\x8e\\xb4\\xa8\\xbcE\\x89\\xd3;\\x07\\x9c^=\\xbaL\\x8f;\\x96\\xfd\\x08\\xbb2hA=V\\x99\\\\;\\xc4\\x0f=\\xbd\\xbb%\\xf1\\xbb\\xe2\\x8e\\x88\\xbc(\\x16J\\xbc\\xfb\\x02\\x80=\\x05i\\xc7\\xbc\\x8f\\x16\\x95\\xbb\\xe64\\xe4\\xbci\\x83\\xea;\\x17\\xe3B;\\x97jx\\xbc\\\"\\xe8G=f\\x96\\xb6\\xbc>\\xeb\\x03=\\xec\\xc8\\xf3\\xbc\\xbf\\x911<\\xb5\\xb3n\\xbc;i)=D\\xf54=\\xb0\\x9cb\\xbc\\xd8\\\"\\xa0<O\\x8b\\xf8\\xbbI\\xe4i<\\xe1\\xd4c\\xbd\\xe2\\x92d\\xbc\\x13\\x85_\\xbd\\xf6\\xfe\\x00=\\x14\\x97\\xb9\\xbc`&T\\xbd\\xa3\\xdc\\xa7;p\\xcb\\xf7\\xbb\\xbc\\xc0\\xa3;\\xa7\\xd2\\x17=\\xca\\x853=\\x81\\xe66\\xbdZ,\\x9e<\\xc2\\xcb\\xc3\\xbcM,\\xc7\\xbcn\\xed4\\xbd1{\\x91=\\x06?y\\xbc\\xba>};\\xbd\\xc0v:\\xa4\\xa6\\x92\\xbc,K\\x19\\xbd\\x8bW\\x90;%\\t\\xa1\\xbd\\xb0\\xea>\\xbb\\x1e\\x8d\\x10\\xbc\\xdaG\\x06\\xbdj\\x82\\x91\\xbb\\\"nI\\xbdT\\xf2a\\xbc\\xd2V\\xc0<._\\x1a\\xbd<\\xa1p=s\\xc2\\xf3\\xbc\\xf0~B\\xbd\\x8e\\xa4\\x99\\xbd<\\xe1\\xf4\\xbc;\\x13\\x88=o\\xb2`\\xbca\\xd4F=6\\x9cv;Zq\\x83=\\xd4\\xef\\xb6\\xbb^\\x7f\\xc2<B\\x0c=\\xbd6A\\xe2\\xbaT\\xcc(=\\x8055\\xbaVW\\xa8<\\xc0N\\xa5=\\xadr\\xb8\\xbbEt\\xda\\xbd\\xb9\\x8f\\x82\\xbd[g9<\\x9d\\x16\\xc5\\xbb\\xa1\\xf0\\xbb\\xbc\\x03W\\xfa\\xbb\\x19\\x9b\\x9e\\xbb5\\x12\\xb4\\xbd\\xf5\\x86\\x89=\\x14k\\x03\\xbdlOM<\\xbb\\x9b\\xef\\xbc\\x89\\x82t=X\\xe7\\x92\\xbc\\xa8}n\\xba\\x9d[\\x07\\xbdH\\xf3\\xb6\\xbcm\\xcb\\xa2\\xbcF\\xfa\\xbc=\\x97\\xe0\\xbd\\xbc\\xcb\\xcd\\xfd\\xbc~@f<\\xac\\xf1\\x87<\\xac\\x1d\\x93:\\x11\\xc2K\\xbc\\xe6<\\xe2;\\xb2H\\xd2<H\\x15\\x80\\xbc1\\x9b\\xc4\\xb8T\\xca\\xde\\xbc\\xb6:\\xbf;E\\\"\\xa8=\\xf9\\xae\\xb0\\xbdR\\x0b\\x05\\xbd\\xb4y>\\xbc\\xe6E\\xee\\xbc\\xd1\\x88\\xc1:-\\xe7\\xe4<\\xb6\\x81\\r\\xbc\\xba\\xe2\\x08=\\xff\\xc9\\x17=\\xce\\xc7\\x98\\xbd\\xfaC\\x1e<\\xbaS\\xdc;\\x99\\xb5\\xbe\\xbc\\x1e\\x13\\x9b<3H\\xe2;\\xfeA\\xb6<:(P\\xbd\\x7f:\\xf6;t\\x13\\xa8<\\xc3\\x87,\\xbb\\xf0\\xe8<\\xbd&\\xa9\\xad<\\xac5;<\\x92\\xf9\\xf5<|\\x84&\\xbdq\\xdc0\\xbc\\xbbq\\x14;\\\"ZJ\\xbbU\\x08\\x16\\xbc\\xabY\\x96<\\x08f@\\xbd\\xd1\\x99N<\\x86\\x1c\\x94\\xbd\\x17J\\x11\\xbcDc)\\xbb\\xd3\\xd7\\xc3<X\\xf77\\xbd\\xb5\\x8aT\\xbd\\x88\\xf2\\x85=\\xd7\\x17d=-\\x8b\\xbf\\xbc\\xd7\\xa9\\xe6\\xbc\\x8ex\\x87<%P\\xbd\\xba\\xe2\\x0cw<\\xa1a-=sGF\\xbd\\xa8z\\xc4;\\x9d\\xf9m\\xba?\\xb0\\x04=\\xcf\\xd7$\\xbc\\xc3\\x05\\x1b=\\xf21\\x87\\xbdM\\xc6@\\xbbI\\xb82<2\\xf8v\\xbc\\xc0\\x8eQ<}|\\x1b\\xbd\\xc9\\xc0\\x03=k\\xb5\\xa6<H\\xf1\\xbe<\\xffW\\x86\\xbcfA#\\xbb\\x9e\\xea\\x1e=\\xde\\xda\\xc5\\xbc\\xb7-\\n=\\xe1\\xa9\\x19=\\xbf\\x8e\\xb8=\\x92\\xd3\\x01=\\xdf\\xd4l=\\xaft\\xfe<C\\xcf\\xb7\\xbb\\xb9\\xb3\\xec\\xbbK\\xbb\\x9a=F\\x1c\\xec\\xbc\\x9c\\xb6\\xd6\\xbcG;\\xc1<\\xdb\\xabV:\\x15\\x9a\\xbb\\xbd\\x81\\xe7B\\xbb\\xbbQ\\xe5\\xbd\\xf1\\x8b\\xa6\\xbcf\\xd0\\xbf<\\xd1\\xd0\\x9c<\\xe7\\xd69=\\xfeO\\xb4<|\\\"\\xf8<8\\x14L\\xbc%\\x00\\xf2;\\xda\\xbb7\\xbdK~\\xaa\\xbcj\\xc2]=\\xc4\\xf2z;U\\x9b\\xe49\\xb5\\t\\\\\\xbc\\xca&o<\\xe1\\x01`=\\t\\x95\\x83\\xbd\\x88\\xa7\\xda\\xbc\\x8d\\xb2\\x85\\xbc \\x946=\\xae\\x99\\x1b\\xbd\\x87\\xec\\x9e\\xbd\\x02\\xdf\\x1a\\xbd\\x0fRW<\\xab\\xb1\\\"\\xbd\\xd7\\xfcs<0\\x1f\\x13:\\xdf\\x0eZ\\xbc\\x18\\xb1~=m\\xa1\\x97<Y\\xcfg\\xbd\\x0e\\xd0\\xd6=\\xf9\\xae\\xa1=\\\\\\xcc\\x9e\\xbc\\xea\\xcf\\x14\\xbc\\xc9#\\x14\\xbd\\x86e\\xac\\xbd9]S<\\x9e\\x95\\xf9:o)\\xee\\xbc\\x99\\xd4T\\xbbUhe\\xbd\\x05\\xd6\\x04=\\xd4TG<]6\\x8d\\xbb\\xf3\\xa6\\x84\\xbc\\x1ew\\xd6<\\xa0wF\\xbc\\xbb:>=n\\xb2\\x0b\\xbcU\\r[=\\x1e\\xa2\\x03;X\\xf6\\n\\xbc\\xa5fE<\\xc0\\x8eM<\\xac;\\xfb<\\xac\\xf3\\xc1:\\x0f\\xac\\xaa\\xba\\x9a\\x9e\\x85\\xbb\\x07=\\xd5\\xbc\\xcb\\xe0V;HsK\\xbct*M<\\xec\\x9f%=U\\xa1\\x1e\\xbc\\xdfhh\\xbd\\xa3s\\xdc;\\\"\\xff\\x14=\\xe3T\\x9a\\xbc\\\"\\x98\\x95\\xbc\\xf1!\\xd7\\xbb\\xe3\\x1e\\xf9\\xbc\\x18u\\xc8\\xbb\\xcaL\\x9c\\xbd\\xb2\\xf7\\x9d\\xbc\\xb1<\\x8d=n#j=\\x1d\\xb7M=\\x07]\\\"\\xbc\\xb3}I<\\xb1\\x9cs\\xbb6\\xd4\\x98<0*T\\xbcz1\\x84\\xbc\\xf6\\xb3\\x85<\\xea\\xd1[;\\xaa\\x80c=\\xce\\xa4\\x90\\xbc\\x11X\\x17\\xbd\\xfd*\\x8e=\\x00\\xfcj\\xbdx)\\x99\\xbd\\xee\\xeb)\\xbd\\xaa\\x9eY\\xbd\\x96\\xf4\\xcd<\\xd3\\x02\\x82\\xbb\\xdbV\\x92<\\xb6\\xff\\xc7\\xbcB\\x00\\x13\\xbd\\xc6>\\x13=\\x91\\x14\\x8f=\\x1f\\x8d\\x1b\\xbcC\\x87x=@\\xda\\x03\\xbc\\xbc\\x8b{\\xbc\\xf0\\xc7M=C\\x1f\\xc1<\\n}b\\xbc1\\x1d\\xfe\\xbb\\xd06\\x84=\\xef\\x93M\\xbc\\xd5\\xa3g<\\x86\\xd9\\x9f;\\x95\\x99\\xaa;&[$\\xbb\\xd1\\x11\\x10=6\\x9e5\\xbd\\xf6&\\x19\\xbd-\\xac/=A\\x05J\\xbc!H\\xbe<\\xc7\\xca\\xfe:8fj\\xbc\\r\\xb1\\\"=SzZ\\xbc\\xe6\\x9b\\xdd\\xbdB\\x82\\xf9\\xbc/\\xa1\\xb7\\xbab\\xf8{\\xbc\\x03z&=\\xe0}/\\xbd\\xdao\\x8a\\xbc\\x0c\\xe9\\x05=\\x93>]< F^\\t\\xebm\\x90<\\x80\\x11\\xe8\\xbb[\\x95\\xe9<\\x7f\\xd5\\x1d\\xb7\\xe1\\tS\\xbc\\xad\\x9b$=\\x96\\xb7\\x86\\xbc\\xa3K\\xb7\\xbc\\x95N>=\\xeb\\xbbo\\xbc[\\xca\\xca\\xbb\\xb56C<Jd\\xae\\xbbn\\xa7\\x0c=\\xfdq\\x0f\\xbc\\\\M#=\\xf6O,\\xbd\\x93?\\xde\\xbcF A\\xbco9a=\\x00\\xaf\\x00=\\x86\\xe4\\xa1\\xbc$\\x0c\\xdf<\\x0e\\x16\\x03=\\t\\xf8\\xa4\\xbbb@\\xb9<\\xe8\\xdb\\xa5;(O_<\\x15]=\\xbd\\xe5\\xaa\\x17\\xbd\\x17\\x1c_<\\xb9\\xed\\n<\\xea\\xea.;\\xa1\\xecY\\xbdP\\xeb%\\xbaF\\xf2\\x8f<m9\\xf1<\\x7f%\\x04=2\\xdc@=8\\xa5\\xb5<\\xd9\\xd9e\\xbc\\xec\\x10\\xea;I\\x03R=\\x0f\\x1e\\x12;\\xd5\\xa7\\xd2<,\\xbe\\x81\\xbc\\x0bH\\xf9=r\\xf0\\xdb\\xba\\xfb\\x9bu\\xbbp\\xec\\xa2;\\x9e[\\x08\\xbd\\xa9=\\x92\\xbdx\\xa80=TK\\x8c9db\\t\\xbb\\x8a\\xfd,=\\xe4k!\\xbdu\\xd0\\xca;\\x8b\\x9a\\x1e<2\\x82\\x9f<\\xe8\\xa5\\x97<h.\\x01\\xbb\\x9d/\\xc8\\xbc\\xe1\\xb9+=L\\xe9}\\xbb\\x1f\\xa2\\x84\\xbd\\x812\\x93\\xbc\\xb8b\\xaa\\xbc\\xae\\x05\\xc0<w\\x1a\\r\\xbb+V\\x97<P\\x1e=\\xbdE~\\xc4<\\x86\\x89\\x1f=\\x8cFy;O\\xb6 \\xbc\\x0e\\xa2\\xc8<w\\x97W=\\x85D\\x18\\xbbSA9=\\x8e\\xe1\\xfb\\xbcQ\\x8e\\x0c\\xbc\\xe34Z\\xbd\\x8b\\x95F=\\xe2\\xb5}\\xbdl\\xc9\\x16=\\xb8}5=\\x9c\\xa6_=\\x92\\xa1\\x07=Y\\xc1s<\\xc8\\xcb\\x14=\\xe1\\xad_<\\xde\\xefl=\\xdf?\\xb4:d\\xc8:=\\x1e`B=\\x97\\xb7\\xcb\\xbc\\xa8\\xb6\\\"\\xbb\\x0b\\x08\\xb5<\\xe0D\\xec<\\xd8\\x07\\x88\\xbc\\n\\xa1\\xf8<6!\\xb6;\\xbc)\\xea\\xbc\\xb2hQ=\\t!\\xb8\\xbb\\x91\\x12r<\\xb1b\\xb1\\xbcb3|\\xbdUk=<)m\\x84=\\x1ftB\\xbd\\r\\xcaO\\xbc\\xd6\\x06z=\\x02m1=.\\xc3*:\\x80\\xa8C=\\xd4\\xf6\\x07\\xbd\\xd2\\xb1\\xe8\\xbc\\xbd\\xd3[\\xbd\\t\\xaa\\x16=\\xc6\\x8d\\x89<~\\x87{\\xbb4\\x19\\x1b;\\x05\\x8a\\xce<\\xe7Y;<\\x0cV@\\xbd\\xc4\\xd2\\xff<\\xc0\\x8fC<\\xea\\xa9U\\xbd?\\\"\\x1c\\xbd\\xb5\\xcc\\x9d\\xbd*\\xb9\\x14=\\x87!\\xb7\\xbc\\x91A\\x89=\\xae\\\\c\\xbc\\xb0h\\x0b=\\x9c^B\\xbd\\x14\\x80\\x1d\\xbd\\r\\x90\\x1c\\xbd\\xae9\\xb8\\xbb\\x025\\xad\\xbd\\xc1\\xbe\\r<\\x18*\\x9d\\xbb\\xc3\\x82-<B\\xbf\\xc6<\\xe4\\xef\\xef<\\xc3:\\x93;\\x89\\xdam=lU\\x0c=jj\\xd8\\xbc\\xd01\\xa4\\xbc\\xff_<\\xbd\\xcaO/=\\x93m\\xa1<\\\\\\x7fq<\\xb5U\\x04\\xbd\\xbbZ\\xa1=X\\xa5\\xbd<$\\x14\\x80;\\xac.\\x14;)l*<\\xce\\xf1\\xbf\\xbc\\x07\\x1e\\xd7\\xbcJ\\x99\\xcb\\xbc\\xa7I2\\xbd\\x01\\xe2Q\\xbd\\x8f\\xcfe\\xbcG\\x95:;\\x80\\xc6\\xe6\\xbc\\xfb\\xb4)<\\xb5\\x94?\\xbd#,\\xcf<\\x88\\x91\\x1a\\xbc<p\\xc9\\xbb\\x13}#=\\\"Q\\\"\\xbd\\x84\\xa3\\x0f\\xbc\\x0c\\x13\\x84=\\x1aA\\x12\\xbdp\\xfb\\x8b\\xbb\\xcbL\\xf5\\xbd\\xbb*Q=u1\\xb1\\xbc1d\\xb6\\xbb\\xa4\\xf9B\\xbd\\xf8\\xaa\\xf8<\\xa9b\\x8d\\xbc%\\xdc\\x90<\\xaa+\\x95<U\\xac\\xcb\\xbc\\xd1#%=\\xa3\\x0el\\xbd\\xdc\\xf4,;M]\\x88\\xbdzD\\xd4\\xbc!\\x7f\\xb5\\xbc\\xb2w\\xab<\\xb5\\x1d\\xda\\xbc\\xde\\x14\\x91;x\\xee\\xa5\\xbcn\\xf4\\x8a\\xbc\\x0c\\xbc\\xbf;\\xa3\\x12\\xa5\\xbd\\x8b$Q\\xbc\\xf6>\\xe5:\\xf74A\\xbb\\xd8\\xed\\xbc\\xbc\\x8a\\x12n<\\xa3\\xb4\\x1f\\xbd\\x186l</\\xa9\\x93\\xbc\\xfb\\x06\\xe9<\\x83\\\"\\xce\\xbc\\xb0$\\x0b\\xbd\\x94X\\xde;\\x94\\xc9\\x9c<\\xbf\\x0e\\x8e\\xbc\\xc5\\xb8\\x83\\xbd@\\xfdD\\xbd0\\x94\\xb7<\\xf7\\xdeJ\\xbd\\xf9\\xd7\\x02=\\x02\\xd4\\x18=x\\xd1==x\\x02\\x13<t\\x9c\\xfa<\\xb8_A\\xbds\\x9f\\x95\\xbd\\xf5g\\xd3<\\xafT\\xcc;\\xcaX\\xce<\\x9c\\xb3\\x18\\xbd\\r\\x9c\\xfb;\\\"\\xbc\\xeb<\\xa9\\xe74\\xbc\\x9e\\xf4x<+^\\xd8<{\\x94\\x86\\xbaX\\\\\\xc4\\xbc\\xc5K\\xd29\\xfe\\xe4\\xa2<w\\xebz=\\xf0l\\xfc<\\x96?\\x92\\xbc\\x0f\\xea\\x13=?\\xf4\\xf6\\xbc\\n\\xf7\\xa2<\\xb8\\xcd\\xce\\xbc\\xc16\\xe9;\\x8f\\xb1\\x1e<h\\x0e\\xbc\\xbc\\xbd\\xa1`\\xbb4\\x9e\\x9c=\\x9c5\\xf7\\xbc\\xd5~H\\xbd\\x1a\\x07\\xdc\\xbci\\x84\\x8b\\xbcT\\xcf\\x0f<\\xee\\\"\\x13\\xbd\\xb7r&<\\x12\\xff3=*f\\x05=\\x89f\\x13=%\\xff\\x8b=^l\\x06<e\\x12,=\\xf5\\x97\\xc1=\\x1b\\\"\\x94\\xbc4\\xfe\\x81;.\\xb9\\xe0\\xbb\\xe4\\xa6l<\\x93\\x18^\\xb9\"\nHSET bikes:10015  model 'Hiiaka' brand 'Classic wheels' price 4248 type 'Enduro bikes' material 'aluminium' weight 16.8 description 'The new version with 142mm rear, 160mm front travel is longer and slacker than its previous generation, but it’s also a bit taller and steeper than much of its competition. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\x07{$<\\x82\\xca\\x12=\\x00\\xbc];\\xa504=\\x87\\x8b\\xb3;3\\xe5\\x96=\\xf0\\xaf\\xeb\\xbcH\\n9\\xbd\\x11\\xbd\\x9d=\\xf2\\x01S<\\x18\\xfe\\x07\\xbc!\\x0f\\x92<\\xb0\\xcf\\x18=X\\xd9\\xa8\\xbc=\\x03U=_y\\x1c\\xbd\\xa4\\x00\\xb7</q1\\xbc\\x0f\\xcc\\xd7\\xbc\\x7f\\xf8\\x83\\xbdg\\x88\\xdf;\\x96T \\xbd5\\xc0C=4\\xe2b\\xbd[\\x14_=\\x81\\x8f\\xd2;\\xae)\\xf5\\xbbCp\\xdf:qj\\xfc\\xbc\\x93\\x7f3=\\xe2\\xf3;\\xbd\\x8d!\\x0b\\xbd0\\xe0*\\xbc\\x06\\x99\\xc3<9.\\x95<K\\xb0Y<\\x06\\xee\\xcb\\xbaC\\xef\\xb3\\xbc\\x8bJ\\x00\\xbb[\\x92<<\\xea\\x9e\\x8a<\\x13\\x19\\x9b\\xbb\\x13\\x0e_=\\x9b%%=,1\\xba9\\xc4\\xf2\\x83\\xbc\\x83\\xed\\xaf=\\x94\\x8a\\xb7<5W\\x96\\xbb\\x97\\x9b5\\xbc\\xea\\x8f\\xac\\xbcE\\xa3#\\xbdZ \\x90\\xbc\\x85\\x07\\xa1<\\x1f6\\xa3<3\\x8e\\xac\\xbcx\\xe2\\x9e\\xbc\\xd7{s\\xbdz\\x10@\\xbd\\xde\\xddk=\\xf8cJ=\\xb5\\xf8i\\xbc\\xcc=\\xdf<;\\xc2\\x9b=\\n\\xff\\x8e<\\xd0\\xea\\xae\\xbb%E\\n;?DZ<\\xaf\\x19\\xac;\\xab__\\xbc\\x04\\x0e\\xeb\\xbb3\\x14T;\\x10b\\xe4\\xbcHzG;\\xad\\x00\\\\\\xbd&\\x7f\\\\;b\\xc8\\xb4<f\\xdf\\xa8\\xbc\\x9b\\x81|\\xbd\\xe11\\x1e\\xbbF\\xf7\\x12<*\\x94\\x85\\xbd5\\x83\\x90<\\xd0\\xe3\\x8b\\xbd\\xa2\\xcaV\\xbcml\\xeb\\xbbj\\xc5\\xa0\\xba\\xd5d8<\\x17\\xcb]=\\x91jA;\\xb5\\x9c\\x1b\\xbd\\xba\\x8d\\x92:\\xbd\\xb9\\xd6\\xbc\\xd5&\\x01=\\x85\\x89O\\xbc\\x8c[\\xa3< ,\\xf2;\\x12\\\"\\xcd<\\x17\\x02\\xb7;w\\x92>\\xbb\\x0c\\x1bU<X\\xae\\n=\\xbe1\\x11=2\\x0bU\\xbd\\xeel\\x13<\\xfb\\xd4\\x8e=IWM\\xbbt/\\xed<i\\xef\\xd5;\\xe4\\xcf\\xbf<\\x10\\xd44\\xbc\\x80\\xe2\\x9a=\\xd8\\x87\\xf19A\\xfe\\x81\\xbc\\x92I\\xe8\\xba\\xb41@\\xbd\\xbb\\xb6M\\xbc\\x96RV;\\xf6\\x82*=Z\\xfd\\xa9:\\xc7\\xc7\\x01\\xbcxF\\xb5\\xba>\\x026\\xbc\\xa5\\x82^\\xbd.\\xd0\\x8b=\\xf4d\\xb9\\xbc\\xd3E\\x88\\xbb.\\xc2V\\xbd\\x04z)\\xbd\\x8a\\x1b\\xfc\\xbc.<\\x92;\\xce1/\\xbc\\xd0\\xbc\\x10=\\xb6\\xd2\\\"\\xbd\\xce\\xff\\xeb<\\xbc+\\x03=\\x8c\\x95*;m9\\xb2\\xbbmi\\x92<&\\xd6\\x99\\xbc\\x15:\\xf2\\xbc\\x7f\\xe6\\xa4\\xbc\\x998L=\\xa7~\\x07=8\\x85)\\xbb\\r\\xf6\\t\\xbdc\\xfa\\xc6</`\\x87<SL\\x00\\xbdx\\x82\\xb7\\xbc\\x9e\\xea\\xf4<\\x97\\x15\\x9c==\\xcbA\\xbd\\x8c8m=l\\x1a\\x0e\\xbd\\r:\\xe2\\xbb\\t\\xf9\\xe1\\xbc\\x13n\\x84<\\x05\\xc2\\x07<3%B\\xbd\\xc5\\xa2H=\\x13\\xc4>\\xbc8\\x82\\xac\\xbc\\xf2R\\xaf\\xbdy\\xf1h\\xbc\\x83H\\x84<\\x85\\x15\\x82\\xbc\\x16n\\x13=\\x04n\\x01=\\xa5zZ<\\xe2.f\\xbc\\x0c\\x9b\\x83=Q\\xca\\xfd;S\\x9f\\x9e\\xbcQ\\x8c\\xc6\\xbb\\x16\\x0f\\xc1\\xbc\\xd3ao\\xbd\\n\\x80\\x89=u\\x1f\\x9e<\\x94Md<\\xf9e\\x99\\xbc\\x0e&v<j>0\\xbdMzC<\\xceyB=\\xc1\\xb0\\x90\\xbb\\xeb\\xde\\x01=+\\xecQ\\xbc8\\x95\\xe2;\\xda6c\\xbd8\\xe4\\x05\\xbcB\\xfa\\xee<W&\\\\\\xbd\\xc7\\xe2\\x02=\\x97\\xfa\\xb1\\xbc\\x04a\\\\=\\x9d\\x84:\\xbd\\x11\\x12<\\xbd_|6\\xbc\\xdf/\\xd0<a\\x95\\xa6\\xbc\\xf7\\\"\\x9a\\xbd\\xb2\\x04\\xb6\\xbd#m\\xdd\\xbc\\x08g,\\xbc\\x7f\\x8f\\x90\\xbb\\xb67\\x97\\xba\\xb60\\xe8\\xbak\\x1aR<\\xfb\\\"\\x83\\xbd\\xbe\\xb5h;8T4\\xbc\\xe6\\xde%=\\xa9\\x7fr\\xbd\\xbbV\\xd1\\xbc\\xe3+\\xac\\xbc]\\x1d\\x06<\\x15\\xbe\\x9b\\xbbIk1=V\\x17\\xa2\\xbd\\xd52i\\xbbm\\xe3\\x1f;0\\xd6Y\\xbd\\x7f\\x05\\x1b<\\xe8@\\x18\\xbd\\x0f|\\xdd\\xbbf\\xd4\\x90<p\\xf9\\x95\\xbc\\x16\\xbcd=\\xdd\\xb8\\x86\\xbd\\\"-(\\xbd\\xce\\x94\\xa1\\xbc%q7\\xbc\\xbf\\xee0=L\\x9ek<p\\x1a\\x83<\\xeb\\xcbh=.\\x91\\xe6<HM\\x08\\xbd\\x9d\\x02\\x1c<*\\xc2\\x91<Dq\\x9a\\xbc\\xad\\x7fg=\\xa7S\\xea<\\x8f\\xfd\\x13\\xbce\\xb1\\x1e=|\\xdd\\xb1<\\x90\\x1e\\x1e=\\\"\\x13\\xdc\\xbc\\xac\\x8c\\xe6<\\x16L\\xec<\\xd8U\\xbb\\xbc\\xbf\\x1b\\x86\\xba\\xe8\\xea\\xa9=>XV\\xbd\\x7f\\xa2[=\\xe7\\x14\\xcd\\xbc^\\xe5V\\xbc3\\x94\\xec\\xbc\\xde^^=:%\\xe2;R\\xc8\\x8a\\xbc\\r\\xab5\\xbd\\xd9\\xfbj\\xbc^\\xdb\\xcb\\xbcS\\xd5\\xcb=\\x0fK/\\xbd\\x85m\\x0e\\xbd\\xdf\\x1e\\x93<\\x1bU3;C\\xfb4=\\xa4G\\x93<\\x1bs\\x8f\\xbd-\\x15\\x91:P\\xd2\\xdc\\xbc\\x17e\\xae\\xbc\\xacCB\\xbd2e}\\xbb6\\xe7\\x05=,5\\xac\\xbd\\xbe\\x9e\\x0c\\xbdR\\x88\\x17=e\\x9d\\xf6\\xbc0\\\"*<;\\x14\\xb1<\\xaf@\\xee<\\x80\\\\\\x1d\\xbd\\xdf2\\xfb<9\\x8f\\xce\\xbdI\\xd4\\x82\\xb9\\xee\\x8c\\xb3<\\x1b\\x8c\\xd5\\xbc\\x04\\r\\xa8=\\xcf\\xae\\x03\\xbcv1*\\xbd\\x98\\xd8\\xee\\xbam\\xbeI=\\x08\\x07\\x0c=,\\x8f4=i\\x11\\x88\\xbd\\xaf\\x1d\\x03=\\x8a!\\xa7;\\x07\\xe8D=\\xc11\\x13\\xbds9w<\\xde\\xdb1=\\x93\\xd9\\xf7;6\\xe0\\xdc<\\xf4+H=\\xee|\\xf5<\\xdb]\\xdc\\xbcd\\xd2\\xf6\\xbc\\xccX\\x1e\\xbd?r\\xe0\\xbc\\xec\\x06#<#\\x820\\xbd\\x9bB\\xb3<\\xc5\\x8b\\x87=?\\xbf*=&\\xae\\x97<y\\x93\\xc4\\xbc\\x886\\x9e\\xbd\\xa2\\xfc&<\\xb8\\xb1A\\xbc\\xbe\\xfa\\x08=\\x9dk \\xbd\\xa6\\\"\\x82=_\\xb3\\x8c\\xbd\\xd8.\\xf0\\xb9\\x81\\xc5\\xd1\\xbc\\x1cm.=5u\\xc0\\xbd4\\x8a\\x97\\xbc\\x1c\\xd4K<\\xf8y\\xc3<\\xd9\\xfa\\xc9\\xbc$\\\"U\\xbdp\\xf2\\xa1\\xbb\\xf6/\\xff<\\xd3\\x8a\\xf7;t\\xce\\x9e\\xbb\\xaf=a\\xbd`\\xb9\\x85=\\xbf\\x03\\x91\\xba&I\\x8d<\\xcb\\xf3\\xde<\\xf8\\xbc\\x85=K%\\x8f<\\xa8\\x06D=\\xc8H\\xb0\\xbb \\xb2B\\xbcp\\xeb\\x9b\\xbc\\x06*}=>\\xe9\\xb5\\xb9\\xa6~\\x88\\xbd\\xb6\\x7fT\\xbc\\xf8\\xec\\x8a<r\\xea\\xc4\\xbc\\x0c\\x98E\\xbbx\\xa3\\xf6\\xbdB\\x98\\xa3\\xbc\\xe5\\x19\\xa3\\xbb!\\x1b\\xac;\\xc56&=L\\xa3\\\\=\\\\\\x9eM<e\\xdf\\xaf\\xbcv\\x82\\xb5<\\x15\\xe3\\xb6\\xbc\\xcf\\xc93\\xbd\\xc4\\xe0U=\\x9bR\\x0e\\xbcRQ\\x97\\xbc\\\\!\\x1c\\xbd\\x15\\xfb\\xae\\xbc\\xc2x\\x07=\\x92\\x1c`\\xbc\\x95d<\\xbd\\x9a\\x11\\xf2\\xbchg\\x18<\\x89\\xea\\x16\\xbd_o\\x9d\\xbcsC\\x91\\xbc\\xba\\x03\\xc3\\xbcI\\xa9\\xbc\\xbdO\\x9bI\\xbby\\x82\\xd0\\xbb\\x96\\xdb@\\xbc\\xc7u\\x80;79\\x11\\xbcZ\\x84g\\xbd^(+=\\x1a\\x81\\xb5=\\xc7\\xb9\\xd8\\xbc\\xfaI\\x05\\xbcJ\\x8e\\xc3<#N\\x8f\\xbdB?\\x91;\\xc2j\\x16=.\\xc2\\x96\\xbd\\xd7\\xe4\\xce:\\x01\\xf2\\x9d\\xbc]\\xfd/=\\x05d\\x87=JJG\\xbb/\\xca\\xbc\\xbc<\\xca\\xce<YR\\xab\\xbb`&\\xa9\\xbcj!;=l\\x0c\\xce<\\xfd\\x8f\\xe3<\\x99dT=v\\xad\\xc3<3\\\"s<\\xd5\\\"\\x84<[\\x9d\\xcc<\\rb\\xa1\\xbd\\xac]\\xf7\\xbb\\xfc\\xfc\\xd1;\\xfci?<D\\xb2G\\xbbd\\x12&<*H\\x01=\\x9d\\xbf 90\\\"\\n\\xbc^\\xb8\\xb9<mN5=Q\\xe7\\xa6\\xbb\\x87\\xf6\\x00\\xbd\\xf2\\x90a;\\x89\\xa1\\x80\\xbc\\x19n\\x83\\xbc-JE\\xbd\\xd5\\x8f&\\xbd\\xa9\\xdc\\x89=\\\"\\x9ej=1g/=\\x8fZ\\xb5;\\x96\\\"\\x1b=\\xe7\\xf9r<2\\x8f\\x8f\\xbc\\xe1\\xe7`<\\r\\xd7H\\xbd(\\x9f\\n\\xbb\\xfaO\\xd3<\\x86j\\x1a\\xbc|L\\x02\\xbdh\\xcd\\\"\\xbc\\x05\\x7fd<\\x02\\xdeD\\xbd\\xfddi\\xbd\\xb7Y\\x0f<\\xdf\\xce[\\xbdU\\x8aU\\xbc\\xcf5.\\xbc\\xce\\x8b\\xd0<\\xdc\\x95\\x1f=\\xc3w\\xc4\\xbcY\\x1e\\xd6<X\\xad\\x1d=[C7\\xbc\\xfczh=\\xbf\\x9a{\\xbc\\x8fqn;jR\\x05=\\xed\\x98\\x99;aZ\\x16\\xbd\\x05G\\x02\\xbc\\xe6X\\x10=\\xe0\\xee\\xa6\\xbcX\\x8fQ<$\\x8c\\xb39\\xb1\\xe1?<\\xcc\\xbf\\xc1;\\x0c\\xf3,=4\\x9d\\xe6\\xbc\\x06\\xe1\\x06\\xbc9\\xd7D9\\xe1\\xeaB;\\x1fT\\xa3<\\x95 \\x0b=\\xc44\\xca\\xbc\\xdfj\\xaa<\\x0f\\xf9{\\xbc\\\\\\x82\\xce\\xbd>y\\x91\\xbc1T\\xea\\xbc6x\\xda\\xbc\\xb9\\xfdD=\\xb8E\\x8a\\xbbao\\xdd<?v\\xb2=a\\x8c\\xa7<\\x9c\\xefK\\t\\xb2\\xbf4;-\\xde\\x19\\xbd\\xd6\\xa5#=H\\x17\\\"=4\\xa8\\xd8<tX,=\\x8f\\xa7\\x8e\\xbcO8\\x0c\\xbck\\n\\xc1\\xbb>6\\xd8\\xbc\\xc0i\\x19\\xbb\\x02u\\x83=&b_\\xbc}\\xc7\\x91=\\xf0\\xcb\\xd8\\xbb\\xf9\\x19\\xb2<|\\xd0~\\xbd\\x0b\\x07\\x86\\xbc\\n}\\xe2\\xbcNE\\xe3<\\x14j\\xb7:jA\\xbf:\\xc2}\\xd8\\xbcv\\x93A\\xbd\\x19t\\x15<V\\xb1>\\xbc\\xc1\\x88\\xbc<R\\x98\\x91<m\\xb9z\\xbc\\xee\\xe8\\xbb<P\\xa0p\\xbb\\xad:\\xe3\\xbc\\xf3\\x13=\\xbdU\\xda\\x9b\\xbd\\x90\\xa1r\\xbd\\x0f\\xe1\\x8a<z\\xdeQ<\\xf8\\x92\\x0c=N)k=\\x8c\\xaa\\xfe<\\x99q\\x13\\xbc\\xd32\\xfd<\\xd1\\xa6\\xb7<\\xa6\\x04\\x1b=P\\x9b#=\\x82\\x85W<p7\\xcf=\\x82\\x1d\\xb4\\xbb2\\xcdp\\xba\\xb3\\x07\\xaf\\xbc\\xe03`\\xbd\\x8a\\xab\\x9b\\xbdRC%= \\xc4\\x08<&:$=\\xb2\\x82\\x08=\\x87Y!\\xbc8\\xa9\\xc9<)\\x1e)=\\x07j\\x10=\\xc2\\xe1\\xe6<Sy\\\"<\\xcfXu\\xbc~{\\xe0<\\xb2o\\x91\\xbdS`\\x98\\xbd\\xf5\\xe9\\xd5\\xbb\\xb7O7;9g\\n=.\\xda\\xed;i\\xa0\\xe4\\xb9\\xdb\\x8dF\\xbcW\\xe1*;}Pb=\\x9aV\\x12=SAQ\\xbcxi\\x05\\xbd8K\\xa1=\\x91\\x8f\\x84\\xbd\\xc7\\x08/=eO\\xac\\xbc+[\\x1d;\\x9d)v\\xbb3\\x18J=\\\\\\x8d\\x86\\xbdX@\\x96<\\x0e6\\x04=\\xee\\x1b\\x92=\\xcf\\x12r\\xbc\\rk\\x9e:Q\\x18w:\\xb0lf<\\xa8\\t\\x01=\\xe9\\xd8R:\\x13\\xd8y=f\\xde =\\xdd\\xfb&\\xbdg \\x85\\xbc\\xfbk\\r<\\xbb_7=\\xd0\\xdb3\\xbd\\xae\\xe7\\x88\\xbctd\\x0b<p\\x85v\\xbd\\xc7\\x1b\\x10=d\\xce\\xd8\\xbc{]U;\\x92\\x12\\x93\\xbc\\x8dwl\\xbcV\\xf5\\x8c;\\xbdq@=\\xa3\\xa9`\\xbd\\xc6\\xe3\\x14\\xbd\\xab\\xc2\\x17=\\xcd\\xbc\\xf7<\\x01gm\\xbb\\x14p)=\\xb8\\xd4\\x8b<\\xcd\\xecG\\xbc\\xb4\\x8c\\x87\\xbd\\xe9\\\"|<0\\xf1Y\\xbc\\xe4\\xebt<\\xa1\\xc5\\xd0;\\xd2ig<\\xd7\\xf4\\x16\\xbcT\\x0c(\\xbd\\xdd\\x1a\\x08<\\xbd\\xe0\\xb1=\\x8dom\\xbd)8\\xab\\xbco/:\\xbdV\\xfc\\x83=~\\xcd\\x16<\\xf7H\\xad=\\x1bV$\\xbc\\xb2p\\xb9=\\xddV\\x16\\xbd\\xc7\\xb6i<n\\x7f*\\xbc@1\\xe9<\\x8d\\x9a\\xb3\\xbd\\xc83\\x0b\\xbd\\xb6\\xe8\\xde:\\xbb@\\x9a<\\xee\\xbe\\xd6\\xbc\\xc0k\\xcb;\\xdeI\\x8c\\xbc\\xf4>M\\xbbieC\\xbc\\x12\\xea\\xea\\xbc^\\x91\\x04\\xbd\\xd1qs<qJ\\xc1<\\xea\\xb9I<\\x93\\x07\\xdc<nu?\\xbdg8\\xbe<\\xd1\\xb7\\xfc\\xbcVTq\\xbb\\x19\\xd4F\\xbd[\\xbb\\x83=\\xb3\\xf8\\x84<t\\xfa\\xa5<\\x8fY\\xe4\\xbc\\xaf\\xbbK\\xbb\\x0ba\\x8f\\xbd6E\\xe9<l\\xf9}=\\xfe(h\\xbdS\\xe7Q;\\x1d.\\x11\\xbc\\xc9\\xc5\\xaa<N~\\xa2\\xbcjU \\xbdZ}\\xb6=R/\\xa3\\xbc\\x84X\\x06\\xbd7\\xd5\\x13=\\xea5\\xd0\\xbbc\\x18\\xe6\\xbbU\\x9c*\\xbd\\xfefC=\\xe6<\\xe1\\xbcG\\x17\\xda\\xbcP\\xad\\xd5\\xbbc\\xa8\\xd9\\xbc<{\\x14\\xbc\\x99UP=-=<\\xbdt.\\xd7<>|\\x12=\\x8c\\x80\\x01\\xbc\\r\\x92E\\xbc\\x93\\x0e6\\xbd\\xc7pz:r\\xaaL\\xbd_\\xf7S\\xba\\x9d\\x00\\xd3\\xbb\\x97\\x06\\xb7\\xbc2\\x83\\\"\\xbd\\x1d\\xe1\\xae\\xbc\\xb4\\xc7\\xcc\\xbc7\\x98\\x9f\\xbdu~\\x8a\\xbdrX\\xfe\\xbc\\xf0!\\xa1;\\xad\\x02\\xad\\xbc\\xa9\\xae\\xa7\\xbc\\x1d>?\\xbd\\x1d\\xc1\\xf9<\\x19I\\x81\\xbc\\x86m\\xd2<\\xe1#L:x\\xa4U\\xbd\\x92\\xe8\\x86\\xbc\\xbc\\xc0\\xf4<T\\x0b\\x81\\xbc7\\xc4\\x8f\\xbd\\xa3\\x87\\x06\\xbd\\xeb\\x8fX\\xbc\\x9e\\\"\\r\\xbd\\x1f\\xef/=\\xe9\\xccH=\\x82\\x02\\xae=\\xd3V\\x87\\xbbZ\\xf6\\x0e=\\xc2{\\xc5\\xbb\\xbc\\xd2\\x8a\\xbdC\\x96m<\\x1c\\xa3\\xa2;UF\\x17=\\xaf2A\\xbc\\xb3U\\x9b;\\xb0\\x90\\x0c\\xbd\\x98P*<\\x92y\\x08\\xbc J\\x91\\xbc\\x93\\xca\\x84<\\xbd4w<\\x96\\xe7\\x11=\\xb1o\\xd3;\\x80 \\xfb<g\\x86\\x8b<\\xd2\\xbd\\xfc\\xbc^i\\x9b<\\xdf~\\x98\\xbc\\xf9\\x85\\xe5\\xba%~\\x1f\\xbd\\xa4P\\xe5:v\\x99\\xa7<\\x8a\\xb2\\x93\\xbc7G\\x17\\xbb\\x00]\\xc7<Q4\\x8c\\xbc\\xe2\\xd9D=.\\xa1M\\xbd)~\\x0e\\xbb\\xcf`\\x13\\xbd*\\x8ba\\xbd\\xb0\\xf8\\x97<\\x8d\\xd9R=A\\xe0H;\\x9e\\xbb^=\\x9c\\x9e\\x88=\\x01\\x7fA\\xbc\\xfch,=\\xa6`\\x12=\\xe4\\xdcm\\xbd;r\\xbf=Y\\xa5y\\xbd\\x8d\\xe7\\\"=j-\\xd5<\"\nHSET bikes:10016  model 'Titan' brand 'Eva' price 663 type 'Mountain bikes' material 'full-carbon' weight 11.0 description 'This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"_\\xa1C\\xbc\\x12\\xe5\\xbd\\xbb\\x81ba\\xbc\\x9aK\\x94<o\\xd9\\xf0\\xbc&\\xff7\\xbc1\\xed\\xee\\xbc\\x89\\xcf2\\xbd\\x19\\x81\\x06=I\\xe1?<\\xfdp\\x19=<+\\x1a\\xbdW\\xebK=\\xafE\\x89=\\x1f\\x9a\\x8e=\\xf8\\x9dI\\xbc\\xc4W\\\"=\\xdb\\xae\\xa2\\xbd\\x13\\xf3\\xe4\\xbc\\x8a\\xe9L\\xbd\\x0e\\xab\\x01<m`\\xc6\\xbb\\xae\\xb1t<v\\xb5\\xde\\xbc\\xeb\\xa5\\x87\\xbcQ\\x1e}\\xbb\\x1fX\\x05=\\xc9T\\xbf\\xbco\\xea<\\xbd\\xcck\\x8e=t1m\\xbb\\xd6J\\xe7\\xbc\\t7\\x1e=z\\x1d\\x93=\\xeb\\xb0\\x13\\xbd<G\\x9c\\xbc\\x19w\\x07=\\xf0-a\\xbb+X&\\xbd\\xb1#\\xb3\\xbb\\xf3\\xae\\x93=\\xca\\xd1$\\xbc\\xdd\\xbe\\xcf<M\\x94\\x88;\\x00E\\x80\\xba~\\xa8E\\xbd\\xff\\xda\\x99=\\x06\\x9a\\xf1<I\\xc8\\xd5\\xbc\\xd5V%<\\x17\\xc8>\\xbc\\xa5\\x16\\xd3\\xbc\\xbcP\\x1e\\xbd\\\\%\\xb1:\\x02p\\x12\\xbc\\x06y\\xeb\\xbc\\x84\\xa5\\x0b\\xba9\\x13$\\xbd-\\xd5?\\xbd\\x0b=\\xa6=/`\\xfc\\xbc\\xa9m\\x06=3&!\\xbb\\xad\\x07\\x18=\\xefm\\xbb<\\x95l\\xa5\\xbcx\\xa7\\xaf<\\x15\\x91\\xbe<\\xcb\\xea\\xb5<\\xcd\\xe9)\\xbd\\xdd\\xce\\x88\\xbc\\xba\\x1d\\x05\\xbd\\xd7\\xe0-\\xbc\\xeb\\x9b\\x94\\xbdV\\x01\\xa8\\xbczv@\\xbd\\xd3c)\\xbb\\xe4\\x11_\\xbd\\x9e\\xfcq\\xbd\\x03\\xcc\\x03=#@\\x0e<\\x99\\xdcB\\xbd\\x808\\xd9\\xbc\\x81m\\xb6\\xbc\\x85z\\xab;\\xc9\\xc4\\x19\\xbdi:\\xd4\\xbb\\x07\\xcd\\x06=%\\xfc9<\\x92\\xa9\\xe6<+=\\xdc\\xbb\\xd8\\xfa\\xcb;kW\\x1c\\xbc\\xde\\xc6\\xe2:\\x06\\x9a\\x13=\\x1dh\\xef<\\xde\\x86l\\xbd\\xe9\\xf9\\xae\\xbc\\xc3\\xae\\xdd<\\x87%\\x9d\\xbd\\x00h\\x10\\xbd\\xc4\\xeb\\xae<v\\xf5\\x00;\\x10\\x88\\xc8\\xbc\\xd72J<\\x8b\\xba\\xe1=::\\x13=\\xa7\\x97w<\\xebX\\xb8\\xbcv\\xa5\\x86\\xbc\\t6\\\"\\xbcY\\x95\\xc3<\\xae\\xd1\\x7f<>\\xfc\\xeb\\xbc\\xf4\\xe58\\xbdZ\\x8d>\\xbd\\x0b\\x9b\\xaf\\xba\\x89\\xb0\\xa7<\\xd6\\xea\\n=\\xdb\\xec\\x8f\\xbb\\x13<\\x0c=)\\xf9\\xaf;\\xa5\\xcbJ\\xbdtE\\xeb;\\xf6\\x14\\xdd<\\xc7\\x9d\\xa2\\xbcC#L<1\\x9e*\\xbd\\x1a\\xb9[\\xbd\\x92n#\\xbd\\xed`\\x98\\xbctSZ\\xbd?\\xf4\\x11=\\xe7\\xe6g\\xbdTX\\xb7\\xbb\\xe7\\xb5X=Vq \\xbd\\xdcI\\xee\\xbc\\xa2\\xe0\\x13=\\x19\\xe4#\\xbc@\\xb6B\\xbd\\xef\\xad\\x8d\\xbdN\\x8c\\xf5\\xbb\\xb2\\\\S=/s\\x01\\xbd8\\xa2%=\\xef\\x90\\x05\\xbc\\x11\\xfd\\xb6<\\x87\\x9aH\\xbc\\x03VZ\\xbcpz\\x8f\\xbc\\xcbm.=`\\x8f1\\xbb\\xcex&;\\xf6\\xf6\\x0e\\xba?\\xe8\\x88\\xbc\\xca\\xe7\\r\\xbdw\\x9c\\xf2;\\x01M\\x1f=\\x10\\x86\\\"\\xbd\\r\\x90\\x1a=G\\xdd\\xe5\\xbcM\\xd8v\\xbbJ\\xa6F\\xbd\\xa7+\\x16;\\x9e`\\xe2<\\xb6|\\xd8\\xbc\\xed\\xad\\xde<4\\xd6%=P\\x94\\x92;x\\xdd\\x97\\xbc\\x0fa\\x86=%\\x89\\xe6\\xbb\\x9c\\xd4/\\xbd$\\x16\\xb5\\xbc\\xae\\xb2\\x11=\\x0f\\x9a\\r\\xbd\\x96Pa=L\\xad\\xaa;\\xa8\\x94g\\xbc\\x10\\x87\\xb3\\xbc\\xf1\\xb1\\x90\\xb8b\\x7f#\\xbb\\x06\\xcap<\\xe5\\x0b\\x04=\\xbc\\xcf=\\xbc\\x97F.=\\x07\\x1b&=il\\x1e=\\x9bi\\xa0\\xbb\\xaea\\xdb<\\xae_\\x14=!\\xddB\\xbd\\x8c\\xe8\\x85=N5\\x8a<W\\xe52=\\xc7 \\xb1\\xbcvc\\x08\\xbcUq[<:\\xea\\x01:\\xben\\x1b;6\\xef\\n\\xbd\\xbe2\\xac\\xbd\\x93f\\xfe\\xbc8e\\xe8\\xbc\\x18U\\x15=\\xd5+\\x8a\\xbd\\xc4UV\\xbc\\xe6\\xbb,=\\x82\\x81\\xe0;\\xe3,\\xff\\xbc\\x1a\\xb0\\xf0<\\xd7G4=\\x02Q1<%\\x8f\\xd7<Q\\xf2@<a\\xd0\\x9a\\xbdvw\\x9a\\xbc\\xad\\xfc\\x9f<\\xd8\\xf8E\\xbd\\xd63@<\\x9f\\xd7&\\xbd\\xdb\\x9f.\\xbd\\x1a\\x80)=\\xf0\\xcbG<0Um\\xbc\\xac\\xecC=\\xa4l\\x0b;\\xb0\\x1e?=<\\xe9?\\xbc\\xa2\\\"\\x0f\\xbd\\xb9\\xc25\\xbc/\\x93\\x8e=\\xd1\\xcf\\xef<2\\xf7\\xc2\\xb9\\x82\\xa5\\x88\\xbc\\xbd0\\xa2\\xbb9=H=\\xc7\\x9bB\\xbc\\x97\\xa8\\xdd\\xbbO\\x1e\\x9a<o8\\xfc;a\\xcfs=\\x1d\\x0bY=\\xb7\\xcd\\x9c<\\x1f\\xf6z=n\\xdd\\x01=\\x91\\xa5\\x03\\xbd2\\x06O\\xbc\\xf1\\x9a\\xb7\\xbb\\xed\\xa8\\x07\\xbb\\x9e\\x7f\\x19\\xbd\\xffc\\x12<\\x1f\\xd2L=\\xb4-\\x0b<\\xb5C\\xca=A3\\xef\\xbc\\xcb\\xf2\\xa5<<\\x1fP;\\xb0\\xb7\\x05=KF\\xa5\\xba\\xc1=E;Nb\\x82\\xbci\\x0fe;\\x85N5\\xbda\\xeau\\xbb\\xe4\\x07\\xe2;w\\x88|\\xbc\\xdf\\x8d\\xbe<\\xfb\\x81(\\xbc\\x1e\\x98\\x11\\xbd&\\xb4\\xc0\\xbcja\\x80\\xbd\\xca\\x80\\x1e;\\x94\\xb4\\xd2\\xbc\\xf5:t\\xbc\\xb2W\\xd7<t\\\\\\x18\\xbdf\\\\\\xc6=\\x92\\x7f\\x88\\xbd?\\x17,\\xbcl\\\"X<\\x14\\xa9\\xa7<\\xa8L\\xfe;\\xbd\\xec\\r<\\xa7\\xc4\\x8d<\\x04\\xee(\\xbd\\xc2\\xf9.=\\x0e\\x8c\\x88\\xbd\\x8a\\xb24<\\x85\\x13\\xb9\\xbbB\\xef\\x07\\xbdJ\\xde\\xa9=\\xb1\\xdc%\\xbczK\\xed<\\x99F\\xb0\\xbcY\\x00!<\\xdaK\\xed<\\x94Qx<\\x8azK\\xbdd\\x1f\\x8f=\\xc1ZA\\xbc\\x84\\x89\\x9f\\xbc\\x93\\xecw<\\x8d\\xc0>=`\\xc0\\x98\\xbb\\xbe\\xbff<\\x16n\\x8c<\\xcb\\xac*=\\x9dO\\xbc;\\x18\\xf67\\xbd\\xdaz\\x18\\xbd\\xd0z%\\xbc!\\xe4\\xc7\\xbc\\xc43\\xcd;\\x91@\\xb6\\xbd\\xa8$\\x8c\\xbd3\\xbb\\xbd=BR\\xb9<\\x94\\xcd\\x11\\xbd\\x83I;\\xbdhA\\x05\\xbdgm\\x8d<\\xbf\\xfec;\\x90yk=+\\x1b(\\xbd\\xf0\\xabG<\\x9fL\\x08\\xbd\\x82\\xad\\x99=@\\x07\\xd1;\\x89\\x11@<\\xacYl\\xbd+>\\xbc\\xbc\\xb3\\x07p\\xbc\\x15w\\x15=>\\x1e\\xd3\\xbc\\x9a\\xe3\\xf3\\xbc\\x8a\\x9bi<\\x99{-=v\\x9e\\x00<>\\x14\\x11\\xbd\\x9b\\xf9*<\\xae\\x86\\x92=uSV\\xbcg\\xc0\\xf9\\xbbYE\\\"=\\xab\\xd3\\x1b=\\xd7r\\xa8;x\\xe2U=_\\xe8\\xd7;=\\xfe\\xf2<\\x14\\xce\\xd7\\xbc\\xfe\\xff\\xa2=\\xc9\\\"\\xdc\\xbc\\xd4\\xc00\\xbdF\\x80h\\xbc\\xea\\xda\\x10\\xbdjX0\\xbc\\xad}K\\xbd\\xd5\\x12\\xed\\xbds\\x1e\\xda<\\x81\\xc2\\\\=\\xe5\\x9d:=8>*=\\x9fT\\xa0;\\xfdK\\x9a\\xbc\\xbc\\xd2\\x95\\xbce\\x97\\x97<0\\x15\\x8b\\xbd[\\xdd\\xe8\\xbcm\\xec\\xf0<\\x0b\\xc6V=uH\\x80<L\\x10{\\xbc\\x08g_=}\\x10\\xd8<<Z\\xab9m\\xc02\\xbdb1\\x10=\\xa3o\\xfd:kZ\\x03\\xbd\\xa4\\xc2\\xef\\xbc#I\\xad\\xbc\\x07\\xaa\\xaa;1\\xd7Z\\xbd\\x1dgS=\\xa3{\\x14\\xbc_K\\xc7<\\xb2\\xe6\\x80<*\\xf8@\\xb8\\xee|\\x90\\xbc\\x8bV\\x86=\\xb2\\r\\x05>\\xa3\\xdd\\xc2\\xbb?B.<&a\\\"\\xbc\\xd0\\xb94\\xbd%\\xc0\\x04\\xbc\\xdcz\\x89=\\x8d\\x8d\\x9f\\xbd\\x8a\\x12\\x8b\\xbb\\xfa-j\\xbd\\xab\\xf3Y=\\xbcW\\x17\\xbc9,C=i|\\x07\\xbd\\xa14\\xdf\\xbb\\xfb\\xacT;\\x8e\\x9dJ\\xbcnQ\\x14<Cw#=>q\\x9f\\xbcy \\xc8<Z\\xaa\\xc4<\\xb9v\\xaa<\\x85\\xbc\\xed;L\\xa6\\xba\\xbcXq\\x91\\xbd\\xaf(\\xdb<\\xf6uC;\\xec\\x1f\\x04\\xbd\\\\\\x86\\n\\xbd\\xe5.\\x01=\\xd2\\x83t=\\xbe\\x88\\x02<\\x845!\\xbdg\\xe4Q\\xbd@4\\xc3<s\\xcf\\xd7\\xbc\\x9e\\xcf\\x1d\\xbd\\xa8\\xe2\\xfd;J\\x00(<\\xfe\\x0bT\\xbd\\xea\\xdc\\xa6\\xbd\\xa9M\\t\\xbdo\\xcf\\xb7=m\\xd0/=\\x98\\xad|<L\\x1dB\\xbd\\xce\\xa1\\xae;/m\\x08\\xbd\\xfa\\xed\\x87\\xbc\\xfek\\xd0;\\x90-\\x82\\xbc\\x0bM\\x86;1\\r\\xad<\\x93\\xc2w<\\xeb\\x97\\xd8;\\x1e\\xbf~\\xbdyp\\x81=\\xa3\\xee\\x81\\xbd\\x00\\xc0\\x18\\xbd\\xa9^\\\"\\xbc\\xe5\\x03\\xb5\\xbd\\x15V\\x80=\\x10\\x9a\\xe6\\xbb\\xd6\\xb31<_h\\xf9;.0*<k\\x0fR=\\xb6\\xa3\\xbb<\\xeb\\xce\\x14=\\xad\\xc86;\\x0es}\\xbd\\xc0\\xfc4\\xbcL\\x10\\x03=\\x10^\\x92<\\x12\\xad\\x7f\\xbd)`\\x94<I\\xa9\\x0b=\\x0c\\xfb\\xfc\\xbc]\\xc1\\x19=z,\\x0b\\xbd\\x84\\x06\\xa6\\xbcK\\xb8\\x13=\\x0fw\\x8b\\xbc\\xa5\\xd6\\xfe\\xbc\\x81\\xa0\\xa5\\xbc\\xe1H\\x10\\xbc\\x8c4\\xc1\\xbcg-\\xb6;\\x9e\\xdd\\x85<$\\x9e\\x1e\\xbcK\\xf3\\xd0<#7|\\xbbt\\x18\\x92\\xbd\\x12\\xf80\\xbd9\\x0f\\xc7<Oe\\x06<I^\\x82<{\\xcb\\xa0\\xbd\\xb3\\xd0\\x05=/\\x0e\\x1a\\xbc\\xa7!\\xcb\\xbc\\x1b<r\\t\\x0b\\x1c\\xa5\\xbc\\xa6\\xed\\xef\\xbb\\xd4\\x88\\x00<J\\r\\x96=\\x1e[\\t=V\\xe4)=\\x85\\xaa\\xb5\\xbb\\x96Z7\\xbck\\tH<w\\xfa\\xcb\\xbc\\xe7\\xe0 \\xbb5\\xe6\\x17<\\xadM\\xa4\\xbcs\\\\\\xf4<nR\\x1e\\xbd\\x17|S\\xbbh\\x1a\\x98\\xbc\\xd7\\xe5\\x94\\xbcS\\xbb\\xdd\\xb7l/L=\\xc4\\xb9Y<\\x8c\\xc1\\xb5<%}\\xc0<\\x03x\\xb9\\xbc0(\\xdd<\\x86GP;\\x1c\\xbe\\x15=\\xa1\\xee\\xa3<?t\\x16\\xbdX}\\xac\\xbc\\xa0y\\xc4<\\xd4=D\\xbaq\\xc3:\\xbd\\xc5~\\x12\\xbd\\x93\\xa1\\xfc\\xbb\\x8c\\x82\\xd2\\xbc\\xc8\\xb2\\xb2<\\xecJ\\x03=\\xb5\\x1d\\xda<\\xa0\\xc5S\\xbc\\x1ar\\x01\\xbd\\x93\\x9f(;|\\x01\\xfc;8\\x8f\\xa6<\\xe2\\xd6J=\\x07?\\x1c\\xbc\\xb0\\x9a\\x93=:.\\xa1\\xbcH\\xa2\\r\\xbd`\\t\\xc6<\\x04\\x8f\\xc7\\xbc\\xa8\\x06R\\xbds\\x84\\r\\xbb\\xf4<\\xbb<\\xbe\\x98T<\\xe1\\xd7\\x99<\\x03\\xd3\\x1f<<\\x8f\\t\\xbd\\x8c\\xbe.<\\xa8\\x82\\xc1<\\xdeI\\xbf;$\\x94\\xaa\\xbcZ=\\x0c;R\\xe6\\r=1f\\xa2:Z\\xb6k\\xbdC\\xfaR=\\xc6\\xb0\\x14\\xbdaa\\xa0;k\\x92O\\xbb$\\xdc\\xa6<\\xbc>\\xae\\xbb\\r\\xc8k<:\\xb1\\xc0<\\x9d\\x0c\\x14=\\xa8x\\x19\\xbd\\xb9\\x99\\xeb\\xbbEy\\xd0<CY,=\\xffDJ<\\x06\\xebZ\\xbd/\\x1f\\xac\\xbc\\xc9\\x1d8\\xbd>\\xec\\xa3<\\x8d\\xf5\\x07\\xbdw\\xc4+<\\x86\\xb5\\x9c=\\x19\\xe1\\x01=\\\"^\\xe1\\xbc.\\\\#<$~\\x7f\\xbcX\\x9b#\\xbdv\\xe1\\x14=\\xd4\\x97\\xd3:\\xda\\xb3\\x97;\\x00%\\x99<9h\\xfa\\xbc\\xaf\\x94\\x91<\\xa8\\xac\\x81\\xbb=\\x06S=\\rm\\x90<n\\xd66\\xbch\\xd8H<\\xaa\\xa8I\\xbc\\xbb\\xc2Z<\\\"X\\x11\\xbc\\xf0\\xf6\\xbe<\\x06\\x0cz\\xbbV\\xb6\\xe9\\xbc\\xb7\\x8d\\x8c\\xbd\\xba\\x96I=\\x1f\\x99\\x86\\xbdd\\x9f\\xb0\\xbc\\x1du$=\\xbd\\xa1\\xdc=\\xc5P\\xbb;\\xaa\\x1d\\x07<\\xaa\\xce\\xd9\\xbb\\xe4\\xe5\\xb7<\\xdc\\xfc\\xfa\\xbc\\xe1l\\xee<x9i<\\x866e<\\xb8D\\x19=v\\xba\\x83\\xbaV\\xfd\\t=V\\x96\\x8c\\xbd\\x87\\xde\\xb2<\\xe0uH=Pea\\xbd\\x8e\\xab\\xd7\\xbc$g\\xaf\\xbdx-\\xcc<3h&\\xbb\\x99\\xe7\\xc0=\\xc1\\xe7=<\\xca0/=\\x00\\x80q\\xbd.\\xd4\\xb3\\xbbA\\x96\\xe8;\\xe8\\x87\\xe8<\\x9e\\x91\\xa9\\xbc:\\x1c\\xf9\\xbc\\xef~\\x90\\xbc\\r\\xccg<\\xb3\\xc1t\\xbc\\xe9\\xc2\\xd2=\\xf7H\\xf6\\xbc\\xae\\x89E=\\xe5\\xab\\xdd\\xbb\\x11zH\\xbd\\xdf\\xf8!=\\x9bf\\xda;\\xd8\\xe6K<\\x1e{B=\\xe5\\x80r<4\\xc5\\x19\\xbd\\x87|G=\\x93\\xa9\\x94<\\xa1\\x93\\x9c<9\\x92/\\xbd\\xbe\\xa9\\x9c=\\xd4\\xc3\\n=\\x8c2\\x9b\\xbb^\\x15\\xb3\\xbc|I\\t\\xbc\\x06R\\x19\\xbdj\\xf1[=\\xe6\\x06l=\\x0f,J=\\xb0#\\xdc<\\xe1\\x9a\\xbe\\xbcre5;\\xbe\\x8b\\x16\\xbc\\xc4(\\xd7<w\\xa1\\x97<\\x80\\xf3\\x06=\\xd0>\\xa2<\\x85,\\x9b<\\x99\\xda\\xd6\\xbc\\x02\\xebt\\xbd\\xde\\xa5\\xad\\xbd\\xd7\\x91#=\\xbc~\\xfa\\xbb\\xffKM\\xbc\\x98\\xc9\\x1b\\xbck\\xcbv\\xbcJ\\x1b\\x87\\xbc\\xce\\x7f?=\\xeb\\x1c\\xd0\\xbb\\xc0\\x8d\\x19\\xbd\\x03\\xf0\\xda<\\xe9\\x9dV\\xbd\\x14\\x0f\\xad\\xbcj\\x89|\\xbc\\x04\\x13@\\xbd\\n\\x9f\\x11\\xbd\\xd6\\xb6\\xb6\\xbcV\\xbf\\xce\\xbcZ=\\xed\\xbc\\x07k\\xc1\\xbcbp\\x83\\xbc\\xbe\\xc9\\x93\\xbc\\x14\\x1c\\x9d\\xbd\\x1e\\xe0\\xeb;\\xfc\\x93R\\xbd\\xac#\\xb5<\\x8bJD;\\xb6<\\xbf\\xbc\\xc6Z=\\xbd\\x85\\xeb^;v\\x04_\\xbcRY!=!\\rT\\xbb\\xabMM;r &\\xbcd\\xdaj<\\xb5i\\xa6\\xbcYc\\xa3\\xbd\\xd8\\xc4\\xb9\\xbd\\x02Z%\\xbd\\xf2\\xcf\\xa6\\xbc\\x9d\\xeb\\x08=3=\\xf8\\xba>\\x82\\x95=\\xefV\\x0b\\xbdVQ\\x13<\\x15^\\x16\\xbdk\\xac\\xbc\\xbdv\\xf3Q=Q{\\x9e;\\xcd\\xbbn\\xbc\\xfaYz\\xbcP\\x94\\xb9:\\x1d8\\xf1\\xbb\\xb4\\xb3\\x1c\\xbd\\x07\\xd4X\\xbc5I\\n<\\xc9\\xaat<\\x9e\\xe3\\xdd<jq\\x90\\xbc\\xcbl^=N\\xa2\\xc3\\xbau\\xbd.=\\xaa\\x19\\xc5;#s\\x01=\\xa1Y\\x80\\xbd\\xfe8a=v\\x84\\xdf\\xbc-\\xb1\\xfa<\\\\\\xb0\\xbc:\\xffi\\x8f;\\xeb\\xfb\\x0e\\xbc\\x88c\\x0c=\\xa1T\\xd5\\xbc\\xe9a\\xa7\\xbaL\\xc9\\xb0<M\\xad@\\xbc\\x1f\\xf2\\x07\\xbd]\\xd9\\xb7\\xbdn\\x05\\x15=\\xbe\\x91\\x1d\\xbc\\x83\\xdb\\x8d\\xbc\\xc65>=W5\\xc0=%C\\x868\\x1e\\x12-=\\xd2\\x19E=N\\x97\\xc3:M\\xb1w=uh\\xb2\\xbd\\xb4>,=\\x0b\\x1c\\x16\\xbc\"\nHSET bikes:10017  model 'Sol' brand '7th Generation' price 2083 type 'Road bikes' material 'carbon' weight 8.9 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xcc\\xd9\\x1a=\\xbc\\xf8\\xde;\\xc3\\xdb\\\"\\xbd\\xe0\\x15y\\xbc\\xff\\xc6\\x1c<W\\xfd\\xbe;\\xcd\\xe3\\x0b\\xbd\\x8fx\\x8b\\xbd\\x9bV\\xd9<\\xd2\\xb2\\xa7<X \\x98<\\x13f\\x06=)\\x98K=\\xe0\\x86\\xcd<Nt\\\\=\\xf1\\x19\\xc1\\xbc\\x95n\\xd9<O\\xeb\\x11\\xbcU!l=\\xe3Lc\\xbd\\\"y5\\xbc\\x15s\\x05\\xbd\\xdd\\x80\\x84\\xbc\\x1d\\x7f=\\xbd!\\xa0\\x0e<\\x86\\n\\x88\\xbbo\\n<=\\xcc\\xf0\\x01<FI\\xeb\\xbb\\xd11\\x00>\\xdc\\xf1\\xc4\\xbc\\x10H\\xc0\\xbc\\x98\\x07\\x1b<\\xc0T#\\xbbM\\x81+<;\\xef\\xc2:U\\xd4\\xd7<\\xf2\\xf7q\\xbc\\x83\\xdd\\xb7\\xbcR\\xc1\\x85<2\\xe9\\xf0=N\\xb2(<\\xab\\x90K\\xbc\\xab\\xff,=agm\\xbc\\xe7\\x93\\x8a\\xbc?<J=\\x08%\\xbd<H\\xa6\\x8e9\\xba\\x14~<\\xfe\\xd0\\xfe\\xbb\\xe08\\xb3\\xbc\\xfc\\x1e\\\\\\xbd\\xfaa\\xb7<Jk\\xcc\\xbc\\xebH\\x8c\\xbdKq\\xc8;\\x86\\xfa\\x96\\xbd\\x03\\xf4\\x86\\xbd*\\xc8\\x9f=\\xf5(\\x8e<S\\xfa\\x06=\\x08\\x1eN\\xbb0\\x04T=\\x81d\\x16=\\xc3\\xdaS\\xbb\\x9d\\xd9\\xbd<\\xffF>;\\x06\\xee\\x91<\\xfb\\x1c\\x1c\\xbd\\xfd\\xe5\\xb8;P\\xd4b\\xbd\\x85t\\xa3\\xbc\\nQ\\xc6;\\x0cr\\x90\\xbcS\\xfdI<vgZ\\xbc\\x82\\x96O\\xbd\\xca\\xaai\\xbdLI\\xa9<\\x92\\xf2\\x98<N\\xf2\\x91\\xbd\\x0c\\x1c=\\xbcJK\\x7f\\xbc\\xc2#\\xc0\\xbc\\xcd\\xaf\\x84\\xbc.\\n\\x0c\\xbcV\\xed\\xf0;Q\\xac+<\\xd8\\xfa\\xbc<z\\xc4\\x0f\\xbdQ\\x9a\\x84:\\xb7\\xb1{\\xbd\\xe1\\xc7>=\\x86\\x1f\\x1e=f2\\xd1<&%\\x0c\\xbd\\x1c\\x87\\xf3;h\\x19e<IFm\\xbd\\xdf\\xc0<<\\xfdW\\xe9<\\xbb\\xe9\\xba<\\x14p\\x99<)\\xa5\\x02\\xbd\\xca\\x9b\\xe9=\\xcc\\xb1\\x18=\\xf6p\\xa6<\\x7fb\\xdc\\xbc\\x11I4<\\x02\\tS\\xbd\\x0c\\x88\\xa6=\\xe1\\xc9n=o^\\x88\\xbcz\\xc6G\\xbd\\xe4\\xf3\\xf6\\xbc\\xa2\\xf0\\xdb\\xb9e9\\xbe<.\\t\\xb5<\\x00\\xb2l<s\\x9c\\xc1<\\x92f\\x0b<\\r+9\\xbd n\\xc7\\xbbnM\\x90<\\xad\\xc1\\xeb\\xbcP4\\x80\\xbc\\x83\\x16\\x8a\\xbd3c\\x1d\\xbd\\x95\\x91A\\xbd\\x1b\\tE\\xbc\\xe3\\xee\\xc4;\\x1bGP\\xbc`\\x0e\\xbc\\xbc\\xcb>]<\\xc8\\xb9>=\\x9aS\\x1a\\xbb\\x8a\\xad\\xb7;t\\xaa\\xb0<\\x95\\x0e\\xef\\xbb\\x894s\\xbd\\xf9x\\xa0\\xbdV \\x11<\\xd2u5=\\xc0\\xe5d<\\x8b\\t\\x06\\xbc\\\"\\xfd\\xd3;\\xa8\\xcf\\x01\\xbcaKa\\xbd\\xd9\\xf1\\xe2\\xbc\\xa8E3=`MV=\\xcc\\xa3\\xc9\\xbc\\x8f4%=\\xa4?c\\xbc0\\x8e\\\"\\xbd\\xa6\\xdb\\xeb\\xbc\\xd94\\xa9<h\\xc9\\xfb<\\x84o/\\xbdh4\\x97=cRC=\\xf4f\\xff\\xbb\\xd4D\\xcd\\xbc\\xab\\xe6\\x04=\\x10\\xfe\\xd6< \\xf5\\xb2<\\x08\\xbe\\x99<\\x0f\\x86\\x99=\\xc3\\x8a`\\xbczH\\xc4<_{\\x86=\\x1f\\x18\\xcd\\xbcg\\xb1N\\xbd\\xcc\\xea`\\xbc\\xf9$\\x88<\\xb4<\\x9a\\xbd\\x98\\x10M=\\x81\\xcf\\xde\\xbal\\xcaD\\xbc\\xf2\\xb2\\xb1\\xbb\\xa6\\xb8\\x8d\\xbb7\\xdaj\\xbbic\\xc3<F\\xad\\x8e=\\x03{\\xcd<q\\x00\\x98=\\xe4\\x05\\xdd<\\\"\\x1a\\xc9; \\x07\\xdd\\xbc\\xab\\xab\\x12\\xbd#w\\x81<\\x1c\\x9b\\x8f\\xbd\\xb0H\\x05=\\x03\\xd6\\xe1<\\xae\\xdb\\xf7<\\xa8~\\xdb\\xbb\\x9cg\\xfe\\xbc\\x9fZm\\xbc\\x8a\\x932=\\x8a\\x01\\xff\\xbb3\\x91\\x8e\\xbd\\xaf\\x03\\xa7\\xbd\\x87\\xd5u<\\xf3\\x05.\\xbcT\\xbb\\x8b<K\\xeaE\\xbdrl#\\xbdS\\x0b+=J\\x10%\\xbd\\xf2\\xc9M\\xbd;\\xd5\\xb7\\xbc=4&=\\x80j\\xbc\\xbc\\xaf\\x07\\xef\\xbc\\x07\\xc0E<\\x1e\\\"/\\xbd\\x95;\\xd9\\xbbf\\x9b\\x1b<e\\x7f\\x83\\xbd\\x8f\\\\\\xf6<\\xf7\\x14\\x03<\\xc0\\x8a\\xb2\\xbd\\xb7\\x18\\x84\\xbc\\xc4M\\x00\\xbdvg\\x14\\xbdS\\x1f\\x87;\\xe9\\xdb\\x0e:\\xe7}v=\\xc8\\x00\\xbc\\xbdG\\xf1$\\xbcI\\xf4+\\xba\\x01aQ\\xbb\\xdb\\xad\\xd2<\\x16\\xe01\\xbc\\t\\xbe\\x82=\\x12\\xb9\\x98\\xbc\\xd17\\xd4<\\xca\\xef\\x9e\\xbc\\x1d\\x18\\x9f<\\x8d\\n\\xfd<\\x1f\\x13\\x00\\xbc\\x8dhL=\\x1a\\xe7\\xcf\\xbb\\x8d\\xce\\xf3\\xbb\\x9755=\\xc3\\x12\\x07\\xbc\\x95k\\xe5\\xbc\\xe4\\x81\\x16\\xbd\\x01T~\\xbc\\x11T\\x18\\xbb\\xa3\\xe1W\\xbdZ\\xc0\\xba\\xbbb\\x115=\\x9aA\\xcd\\xbc\\xe1\\xac\\xab=\\xde\\x82\\x97;|\\x0fe<\\xa9}\\x00=\\x1e\\x8a\\xda=\\xc8\\x1e\\x95<#\\xe3\\xb7\\xbc\\x9esW\\xbd\\x16\\\"\\x12\\xbdM\\xb9\\xfd\\xbc\\xe6\\x8c\\xf0=(\\xc5\\x81\\xbd\\xff\\xa5\\xe5;\\xa7\\xe5]<h_\\x81<\\x8c\\x1cq<\\x9a\\x0f\\xaf\\xbc\\xec\\x951\\xbc,\\x04\\xa0<\\x98\\xf4\\x00\\xbd\\x14V\\xac\\xbc\\xee\\xe9/;\\xcc\\x11\\xa3<1\\n?=\\x0e\\x19\\xce\\xbd\\x1c(\\x05\\xbd\\xdcx8\\xbd6C\\xf4\\xbc\\xc16\\xad;}0+=\\xc0\\x89s=\\xc9\\xd9\\x9b;V\\xa8\\x06=`[o\\xbdY\\x9cZ=\\x19G\\x98\\xbc\\xfe\\x89.\\xbd\\x00\\x96\\xb5<&\\xed\\xff\\xbc\\x82\\x9f\\x91<x\\x92+\\xbdC\\xef\\xea<\\\"Be=\\x01\\x91\\x1a;\\xa2\\x87\\x81\\xbd\\xe4PI\\xbc\\xb4\\xce\\xe4\\xbc@\\xb7\\xa5<\\xaa\\x9fv\\xbd\\x9c\\xfc\\xb3<&\\x95\\xa5<\\xe5\\xbei<\\xec5\\xe6<\\x9ej\\x9b=}y\\xaa:\\x969D\\xbc\\xbb\\xab\\x9a\\xbc\\x9d\\x15\\n\\xbd\\xb0\\xe7\\xfd\\xbc\\xd9U\\x19=\\xc8\\x92\\x82\\xba\\x8f\\x9a\\x1f\\xbbl^\\xb5=\\xe67\\xd9;\\xc3\\xe9\\xb0\\xbb5\\x1c\\xe5\\xbb y\\x10\\xbb\\x86\\xa6\\x8f\\xbc\\xe3\\x9c\\x94;X?\\x85<\\xff\\xd13\\xbd;\\xa9.<m1\\x1f\\xbd\\x04\\x86\\x8e\\xbb\\xb1b\\x07<\\xaa\\x95)=q\\x1cx\\xbcL\\xae\\xd2\\xbc\\x14~B=\\x89\\x13o<\\x84K\\xc4<\\x10^B\\xbdb\\xc7,<p,\\n=&\\xa8\\x8d\\xbc9\\x94\\x87\\xbc+\\xa1\\xdc;_\\x1ca=dL5\\xbc\\xb4j\\xc4;\\x82\\x9d<=\\xc9/5=W\\xef\\xfd;\\xc2\\xd8_=d\\x17\\x0c<\\xf4z\\xbf;H\\xf4\\x8d<\\\"(z=}\\x16D\\xbc\\xd43m\\xbd\\x103I\\xbcDPM<\\x19!\\x1e\\xbd-\\xaa`\\xbb.\\x0fD\\xbd\\xf8;<<\\x1d\\xf1/<a\\xbf\\xd9\\xbbH\\xbfI=\\x0e\\xb8\\x8c=m\\xf4F\\xbb[g\\x0f\\xbd\\xdc\\xed\\xfc<\\x80\\xd7l\\xbd\\x90\\x14P\\xbd\\xc5\\x9d\\x15=\\x14i\\x08\\xbc\\xa2\\x13\\x13<g\\xde\\xc0;J\\xbf\\x8e<\\xb6yc<H\\xac\\xbc\\xbcd)\\xaa\\xbb\\x07Of\\xbb\\xcd\\x9eO<@Q5\\xbd|\\x1c\\xa5< \\x95\\xef\\xbc\\xd6\\x8fQ\\xbc\\xc4[\\xb4\\xbc9\\xd1\\xba<\\xd0h\\x14=\\xfe\\xbc\\xf6\\xbc\\x053`<\\xc7\\xf7\\xb0\\xbc\\x9c\\x99=\\xbd>wl=\\xaa\\x0f\\xc7=\\xf2\\x16\\x99\\xbcw\\xbf4\\xbcbV\\xf4\\xbc\\x88\\x8c\\x08\\xbd[%\\xb2<\\xa8..=\\xea\\x9e\\xad\\xbd~f~\\xbc\\xb7Fk\\xbdX\\xba\\t=\\xfb\\xb7\\x19=if1;r@}\\xbd\\x8b\\x10\\xd1<\\xc5\\xcf5\\xbc\\xe7\\xdbd<\\xaa\\xee!<O3W=}Q\\xd2<\\xb0\\xba\\xd7<\\xb3\\x8c\\xf5\\xbb\\x1dr\\x17=d\\xfd\\xb2<\\xe5s\\xba;\\xc0\\x0e\\x99\\xbd\\x8a\\xb8\\xc9\\xbc\\xae\\xe8\\xc0</\\x98\\\"=\\x12q8\\xbdc\\xeem=\\xf8\\xb9\\xa6=\\xa7\\x13q<i\\xa3\\xde\\xbc\\xc8kt<W\\xf7\\x12=\\xf7\\xa0c<\\xf2<\\xb3\\xbc\\xff\\n\\x9d<^\\xf1`<[\\xb5=\\xbd\\xc0\\xa4z\\xbdw\\x12\\xbf\\xbc\\x13\\x9d\\xa0=!\\xea1=\\xab`\\\"=M\\x9d\\xd6\\xbc\\xb5\\\\\\x8f<eB\\xbe<\\xc6\\x99\\x97\\xbc&i\\x85<k7;\\xbd\\xcai\\x84\\xbb\\x18p\\xc8\\xbc\\r\\xd8\\x9d<\\xa9:\\x12\\xbdR1h\\xbd\\xc2!\\xe3<\\x08TI\\xbd\\xdeNG\\xbd\\xb1\\xc5@<\\xedzJ\\xbd\\xa66\\x86\\xbb\\xd7\\xf1\\x8f\\xbc;l\\xbd\\xbc\\xda\\x85\\x9e<\\x8cs\\x94\\xbcvJ\\x90=mv\\x04=\\xea\\xd4\\xc4<B\\xa3\\xf0<f\\xc6\\xdb\\xbc#\\xbd\\x989\\x8c\\xf0_=m*\\xbf\\xbc!\\xf8\\xc5;\\x8eyH\\xbd\\x94\\xa0T=\\xdb\\xcb|\\xbc\\xecB\\xdb;\\xf9\\xc5\\x06<\\xbfp\\xb5\\xbb\\xd7\\xa96<S\\x81v=\\x7f\\xc6\\x0b\\xbd\\xac\\xc6\\x9a\\xbcz\\x86\\x80=\\xd0D\\xe7\\xbc4\\xf0\\xdc<\\xc6\\x16\\xcb\\xbc\\xb3yK<0\\xd0\\x07=\\x13h\\x9f\\xb9\\xc0\\xccM\\xbd!\\xb1\\xa9<\\x8e\\x0b\\x88<\\xd3\\x86<\\xbb_\\xa64<\\xd8\\xddV\\xbd\\xc7\\xd9\\xb6\\xbb\\x1b\\x8d\\x11=|Z\\x1f<\\x01\\\"z\\tc1\\xb1\\xbb\\xa4\\x00\\xae\\xbcm\\n3\\xbd\\x93\\xda\\xb1\\xbb\\xff}\\xa39y\\x85%=\\\"\\x80.\\xbc\\x98\\xb1\\xbc\\xbcex\\x8d<f\\xe7\\x0e\\xbdY\\xdc\\xf7;\\xc7!\\xf8<\\\"\\xe2W\\xbch\\x11h=.\\xd63<\\xe1\\x8b\\xd2<\\xee\\x8a\\xf6\\xbc)\\x84\\xf6\\xbc\\xf4\\x00\\xa1<\\xe7\\x185=5h\\xa4;\\x8d\\xa0G\\xbdV]\\x16;\\xab\\xe4\\xb0\\xbc\\x18\\xab\\xbb\\xbb\\xc2\\xf0z\\xbb\\xde1\\x93\\xbc\\x86\\x03I<\\xb7\\xf9\\x9f\\xbc\\xdb\\x87\\xde\\xbc\\xfb\\xd5P\\xbc\\xbfQ\\x12=`;.\\xbd\\xcc<z\\xbd^\\xeb\\xbd\\xbc\\xbb\\x17\\xe3;I\\x9b/<\\x82E1=\\xd2:2=\\x05\\xeb\\\\<\\x88\\x9a2\\xbc\\xea\\xd1\\x1f8\\xb2\\xfe\\n;\\xe5@2= \\xf2\\x08=\\xbd\\x171\\xbd\\xddX[=(AE\\xbb\\xe4\\xfe\\xe1\\xbc\\x8d\\x9aC\\xba\\xd8\\xbb\\n\\xbd\\r\\x19\\xc2\\xbd\\xfd\\x1e~=\\xect\\x1a=V\\x93\\x06;\\x04o\\xbb;\\x07P\\xce\\xbb\\x18\\xc4\\xd7\\xbb\\x01W\\xfd<\\x8c\\x9e\\x0b=\\x13s\\xbb<\\x00~\\x0b\\xbb<\\xec\\x99<\\x16y4\\xbc\\x82tu\\xbd\\xad\\x99[\\xbdr\\x8d\\xca\\xbce\\xb0\\x04<\\x99\\xf2\\xfa<<\\xe1\\xfc<\\x8b\\xca\\xe4<P\\x7f\\xaa\\xbb\\xeb\\xc8\\xdb\\xbcl\\xbe\\x02=\\x8bD\\x06=\\xd1/\\xd0\\xbcr\\xe9\\x1a\\xbc\\xa3\\xa4D=\\xdf\\xfcE\\xbd\\xe5\\xc0\\x00=\\x92\\xc8\\xea\\xbc`\\xd7\\x08<!I[:xh\\xd1<\\xc3L\\x87\\xbd\\x90\\x99E<\\x1b\\xa1\\x1d=\\xcdb\\x84=pM\\xaa<\\x95\\xfc\\xdb;p\\xb5\\xcd\\xbc\\xde\\x91,\\xbd\\xc4vW=\\xb9\\xb7\\xd6:\\xa7\\xb2\\xe6<j\\xa6\\x04=\\xc8C<\\xbd\\x94*\\xb4\\xbb\\xee\\xf9\\xf9;\\xf7\\xf9\\xe0;C\\xe1^\\xbdT{\\n=K+\\xb9:\\x0f;\\x04\\xbd\\x91\\x9e\\xe2\\xbc\\xfd2\\xdb\\xbc\\xdf\\x9e\\x19=\\xb2\\xdb;\\xbc\\x0b6~\\xbd\\x8f\\xec5\\xbdfS\\x91=%\\xf2\\xf0\\xbc\\xefe\\\"\\xbd%d\\x15=\\xfd\\xd9 =\\xb8\\xddI<\\xa8C\\x8d<\\x84\\xf0\\xf5<\\x8f\\xe9\\xb8\\xbc@\\xa9\\x08\\xbd\\x12=\\xdc<\\xa5\\x96\\xf4;\\xfaf\\x87=i\\xa2\\xfb<\\xfc\\xa0\\xc1<\\xcf\\xd2\\xa5\\xbb\\xb3\\xdb\\x1b\\xbd\\x7f_\\xda<\\x12a1=\\xc7\\xfe\\xc5\\xbc-5\\x91;\\xe2\\x11s\\xbddMh=\\xfd\\x1e\\xa5\\xba\\x85\\r\\x19=V\\xa5\\x9b\\xbcI\\x15h=GYw\\xbd\\xfe\\x9e\\x8c<\\x0b\\xdc\\x13\\xbc\\x7f\\xd0\\x8c<e\\xec.\\xbd\\xbcM\\xc9\\xbcg%\\xd2;6\\xd9p9~\\x88\\xaa\\xbcNk\\x0b=\\xda,\\x1f;%J\\x8b<\\xa8  \\xbdI\\xa4\\xfe\\xbc`\\x0c\\xec;\\x93\\x1b\\xe1\\xbc\\xae\\x1d\\x15=7\\x16\\x86<\\xa0(\\xb3\\xbbj\\t\\x83\\xbd\\x17\\xd2m<\\x1f]\\xcb;\\xb8z\\xf6<\\xb9\\x16A\\xbd\\x82\\xe3\\x13=0Ce\\xbc5[\\xf3\\xbb\\xf9\\x96N\\xbb\\x8d\\xf5T\\xbd\\x88\\xc1h\\xbd]\\xef\\x82:\\x0e|\\x8c=\\x0b`&\\xbd\\xe8\\xe6\\x03=\\xe4\\x95c\\xbc\\x9a\\xddM\\xbd&V\\x11\\xbd\\xfe\\x88\\x17\\xbc\\xe01t=\\x0cj\\x13=\\x01\\x1az\\xbc\\xa9\\xbf\\x86<\\x02\\xa0\\xe1\\xbc\\xbfU\\xa1\\xbba\\xe9\\xae\\xbd<\\xd6\\x91<i3\\x92\\xbb2\\xd6\\x14\\xbd\\xdfb`<\\x8b+P\\xbc\\xc8h!\\xbbMA\\xa1<\\xee\\x10\\x00\\xbd\\xec\\x97\\t\\xbckv\\xe5<u\\xa6~\\xbb\\xf0\\x87T\\xbd:\\xd9\\xaf\\xbdP)\\x85\\xbd\\xc2\\x113\\xbdDP\\x9b\\xbc\\xbf\\xa7 ;\\xb4\\x14.;b\\xe0\\xc9\\xbc$\\xc7 \\xbd\\x02\\x82c\\xbc\\xd1\\xa5\\x95\\xbd\\x16\\xa9\\xc4\\xbc\\x08U\\x9e;e\\x06\\x06=#\\xc9\\x03\\xbdg\\x95e;\\x92\\xea\\x08\\xbd\\xd4\\xc8\\t<\\xd1g\\xce\\xbc\\xd2x\\x18<\\x9c\\xe4\\xa5;\\xe8d\\xa6\\xbc\\xc1\\x98\\x18\\xbd\\\"\\xfe\\x15=\\n\\xa0M\\xbdH,\\xc2\\xbcW\\xe3\\xa3\\xbd_\\xf6S\\xbd\\x11\\xeb\\xe5\\xbb\\xf9\\xa8\\\"=b\\xc5\\xfc\\xbb\\xa6\\x12~=S=\\x93\\xbb\\xe4o\\\"=\\x8c&b\\xbd\\x89I.\\xbd\\xa6\\xe8\\xb5=E\\xaf\\xe3\\xbc9_\\xd1\\xbc.G\\x99\\xbc`\\x02(\\xbc\\x80\\xd9;<R\\xf1a\\xbd\\x1eHr:\\x17\\xeb\\x88\\xbc\\xda\\xeb3=f\\xdc\\xef\\xbc\\xea\\xc1\\x0c;b\\xfb)=g\\x9e\\xe2<\\x15\\xd6s<\\xca*\\xd6\\xbc2$\\x9e\\xbcv\\x8d9\\xbd\\xf1\\xdc\\x88=\\xd3\\xdd\\x83\\xbd7\\xc9\\x95;\\xb8\\x8cG=c\\xab\\xb4\\xbbo`\\x1e;\\x0e\\xa2\\x13=C\\x82\\xa0\\xbcWBv<\\t\\x95c\\xbd^%u\\xbc{\\xa3\\xa0\\xbc\\xf6\\xcb\\x8b\\xbd\\x0e^\\xd7<.\\x7f\\x7f=\\x03\\xad\\xcf<\\x80\\x89\\x87=\\x80\\x94\\xa6=\\xe2\\x9d\\x19<\\x89\\x9a\\xa8=0\\x12\\x0e=07\\xf1\\xbc\\x02B(=y\\xbd\\xfb\\xbb\\x07\\xae\\xa9<\\x11\\xc7\\x1e\\xbc\"\nHSET bikes:10018  model 'Neptune' brand 'Nord' price 2872 type 'Kids mountain bikes' material 'full-carbon' weight 9.4 description 'This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. It has a lightweight frame and all-carbon fork, with cables routed internally. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\xef\\x84\\\"\\xbd7|S=N\\x00;\\xbd\\x17s\\x91;#\\x18\\x11;\\xc2\\xd8;=\\x12\\x9d]\\xbc#:N\\xbd\\xcb\\xfb\\xbe=}\\x0b\\x97<SW\\xb3\\xbb\\xcbu\\xd9<\\xbf\\xed\\x04=\\x17O\\xee\\xbcO*\\x05=L\\xb8\\x13\\xbd\\xe6\\xd3D<$7\\x17\\xbd\\xe8\\x19\\x9a\\xbbEiq\\xbd\\x00;!=\\x93\\xcf#\\xbd\\x94\\xfc%=M\\xa6\\xa9\\xbc\\x8c\\xdaK\\xbc\\x1e\\x8b\\xa6<\\xfa\\xcdU<\\xbfW\\x88<O\\x18\\x9a\\xbc\\xe1\\x87W=\\\"\\x1d\\xdb\\xbb\\xc4\\x1c\\x8e\\xbcu\\xe1\\x1a=},*<\\x05\\x0f\\xde<2\\t\\xbd\\xba1\\xffC=\\x95{\\x1a\\xbd\\xa1\\x1b\\xb2\\xbb\\xfc\\rF\\xbc\\xb2d\\xf0\\xba%\\xbf}<\\x04\\xa3\\xd1<B\\x8bk<\\x1fZ\\xde\\xbc<\\xfd\\xc0\\xbc\\x80Nh=\\xe2\\x8a\\x00\\xbcvIa\\xbc\\xa2\\n\\\"\\xbd\\xec))=\\x14\\xd2\\xed\\xbc\\xa0\\xa9\\xe3;\\xabI\\x13\\xbb3\\xc7\\x94\\xbc\\xa6Tl<\\x08\\x80\\x83<h\\x1a\\x94\\xbd\\x1c\\x14#\\xbd\\xfa~\\x7f=dO\\x0b\\xba\\xd1\\xf7X=\\x951\\x04=\\x06\\xcc\\xb7=\\xb1f\\xaa<\\xf5g9<=\\xf1(\\xbc\\xb1\\xa4\\xa5\\xbcp\\xdfW=j\\x13\\x1f\\xbc\\xba{\\xb7;/\\xdb\\xd0\\xbb;x\\x93\\xbd\\xb2\\tI;y7\\x13\\xbd\\xc1P=:`\\xfd!\\xbb\\xed\\xa3\\xd5\\xbc_\\x06\\xab\\xbd\\xdfb\\xf9:\\xb8\\xaf\\xb0<\\x0c4D\\xbbR\\xa0i\\xbcfm\\x99\\xbd\\xad\\x99\\xd4;Q\\xaem=\\xa6\\xa9\\xcf\\xba0r\\x1c;\\x93\\xb5\\x91<\\x04\\t\\xd8<\\xdcE<<C\\x83\\xd3\\xbce\\x9b\\x96\\xbc\\x9f\\x18S;A\\xb4\\xc9\\xbb1@%:\\xc4\\x0f\\x1a\\xbd\\xf4\\xa2\\xc3\\xbc\\x17b\\xdf<v\\x13O\\xbdr\\x1b\\x1d=*\\x9a\\xba;H\\x07\\xe4<\\xb5(r<>\\xb4\\xa3\\xbc\\x1d\\xae\\xe5=8\\xd1\\x14\\xba\\x1eg\\xf4;\\xc1\\x83\\xc5\\xbb\\x8a\\x84\\x00\\xbd\\x00|\\x8a\\xbc\\xc3KY=\\x84\\x85G<\\x15\\xf32\\xbdP]\\xa5\\xbd~T\\xd5<ssT=?\\xe9\\xf0\\xbc\\xb9\\x9c\\x0b=O\\xe1\\x1d=\\xa3\\xf93=\\xd7\\xc4\\\"=\\xeb\\xc6&\\xbd \\xc5p\\xbc|\\x14|=\\xeb\\xe9\\x17\\xbd\\\"\\x9c\\xa4<L\\x18\\t\\xbdb\\xb8=\\xbd\\x80\\xa5\\xef\\xbc~\\xf7\\x9b<\\xd9\\xbbd\\xbd}\\x01\\x92=\\x8c\\x96o\\xbc\\xbbqx\\xbb[\\xc2(=\\x1e\\xcd\\xc8\\xbcsG\\xfd\\xbc\\xddq\\x85:\\xff\\xa3%\\xbd\\x8b\\x88s\\xbdp\\x9a\\x90\\xbd7K\\x88\\xbcb\\xe3\\xc4=\\xdc\\xa0\\xcf;L\\xae\\xfa\\xbb\\r\\x13\\x9f\\xbcc\\x8c\\xfc<\\xe1]\\x93\\xbc\\xb4\\x81g\\xbdbX\\xae\\xbcd\\xa7\\x1e=\\x08\\xc68\\xbd\\xb3\\xe6\\xa1<\\x1bW?\\xbc\\xea\\xe5P\\xbc\\x83\\xd2G\\xbd!\\\\\\x1b\\xba\\xcc\\xb6\\x95=B\\xe3\\x81\\xbc\\x82\\xd6Y=\\x83:\\xd4\\xbc\\xd4]\\xce\\xbc\\xb6\\xe7x\\xbd<X\\x8c<d\\x17\\xb9<\\xc6\\x8d\\x1f\\xbc\\xd4\\xcd\\xda;Px\\xa9\\xba\\x1e\\x03\\x9b\\xbc\\xeck\\xba\\xbc2\\xbf\\xd7<\\x1aH\\x19\\xbc\\xfe7\\x84\\xbd_\\x10\\x99\\xbc4\\xcc\\xca\\xbb\\xc5RF\\xbd\\x00\\x91f=\\\"J,<\\xb4gJ<\\xbc\\x8dA\\xbcr\\xa6\\x1d=\\x12t\\x90\\xbam\\x19\\xf5<,)\\xf8<\\xe1<\\x00\\xbc\\xd8\\xcd-=\\x10\\x87\\x94\\xbc\\xa7\\n|<\\xe2\\x92c;=\\xbf\\x0f\\xbd\\xcb\\x14\\x8c=py\\n\\xbd\\xcex\\x08=\\xc1\\x0c\\xae<\\xce\\xa7\\x85<\\xe51\\xb0\\xbc\\x02\\x90\\xd3;\\x9b!\\xcd\\xba\\xf6hO=\\xea\\xe86<K\\xdf%\\xbd\\xb1\\xb1\\x82\\xbd`:\\x8f\\xbc\\x1c!\\x1d\\xbdP\\n\\xd5<\\x1atG=\\xd2\\xc4\\xcd\\xbc\\xbf\\xef&=\\x9c\\xc0\\xe9\\xbc\\xe9\\xbe\\xf6\\xbblue\\xba\\xfa\\xab1<C\\xb5\\x80\\xbd\\xad\\xb4 =\\x02\\xbbw\\xbc\\xf8^\\xd0\\xba^\\xde\\xcb\\xbc\\n\\x81S=}\\\"\\x81\\xbd\\xba\\x8c\\\\<b\\x9f\\xf1\\xbc\\xd0\\x81\\x08\\xbcc\\xb7\\x97=4\\xbf\\xf1\\xbc@\\xe3\\xe5\\xbc\\xd7\\xdb\\x08=d}\\xd0\\xbc\\x8b\\xd2\\xf1<AF\\t\\xbd\\x9eB\\xa3\\xbb\\xfa\\x9f\\x83;\\x02GP\\xba6\\xe8\\x98<\\xa0y\\x8c\\xbc~#H\\xbd\\x80H\\x15\\xbd\\x81\\xab\\x88<5S\\xf9; Ok;G}\\xb1\\xbc\\xc2\\xda\\xad\\xb9\\xd5.-\\xbc\\x06\\xc2\\x0c<\\xe4\\xe7\\xc8<\\xe85\\x81<\\x18\\x82\\xea<]\\xd3\\xd6<-O\\xc0\\xbd\\xda\\xdc*=A\\xa3\\x8c:\\xc2\\x1c\\xa2\\xbd\\xc7x\\xfc\\xbc\\xee\\x9a0=DS\\xb4\\xbc\\x9f\\x18\\xb5<\\x1f\\x0b*\\xbd\\xfce=<-\\x1f\\xef\\xbc\\xf4\\xa4\\x06=\\\"\\x8d%\\xbd\\x0c\\x9d\\xb6\\xbaq6<\\xbd\\xde\\x89\\x18\\xbc+\\xd9g\\xbd\\xde\\xe1\\x82=y\\x15\\xbb:28p;\\x8f?\\xa9<\\xdcV\\x82<\\xda\\xd3a\\xbc\\xe4V<\\xbc\\x97\\x06G\\xbc\\x81y\\xe4<\\x85P4<\\xf5\\xf0\\x9f\\xbc\\xb05\\x96\\xbcJv\\x96<\\xe4r\\x0c\\xbdn\\xe9\\xbd\\xbd\\xa5\\x8f\\xbc\\xbc@\\xb6u=\\xcbzR=?(\\xba<\\xc2G\\x8b\\xbc\\xcf\\x0b\\x1c\\xbdP`&\\xbd\\x12\\xef*<\\x1d\\xe5\\xeb\\xbdG\\xe44=NM\\xb1\\xbc-\\xcc\\x0c\\xbd\\xceI\\x93=bC\\xb3\\xbco\\xb0Y\\xbd\\xe6\\xd6+\\xbd\\xc9?\\x9e;1\\xd3\\xc4<1\\x93\\t<#\\xdfD\\xbdI\\xd9-<\\\"\\x8at=%EL=\\x0f#\\x05\\xbaFs#=#8\\x16=\\x91\\x98\\r=\\x0bA\\x03=\\xf4\\x8c\\xb3<\\x90\\x86\\r=6\\xc3X\\xbdU\\xc2g\\xbc\\x08\\xd9\\xbc<\\xae\\xc7\\x1b\\xbd\\xa27\\xd2<\\x93;d\\xbd\\xee\\x84\\x02\\xbd\\x9an\\x80=5 \\xca<ZH\\xe2<\\\"\\x94L\\xb9HW\\x15\\xbdC{+=L\\xb9\\xf3;HO\\xe2<\\x1b\\xe7A\\xbd\\xdb\\x85\\x13=\\xd3*\\xba\\xbd\\xad\\xcd\\xb7\\xbc\\xf9\\xfb\\xa5\\xbc\\x12\\x1ca\\xbdh\\x84\\x04\\xbd\\x16<G\\xbd\\x0e\\t\\x04\\xbc\\x0e\\xc8\\xb4<*\\x9d?<\\xdfv\\xc4\\xbc\\xf5\\xe9_\\xbb\\xe0\\xcf\\xc2<\\xdd\\x08\\xc8\\xbc\\x18;\\xe2\\xbc\\xe9\\xbc\\x95\\xbb_\\xfd\\x9b=\\xb3w+;\\xae\\x8e\\x16\\xbd0<%=\\x9eC\\xa7=\\xbc\\xd6\\xa1<\\xa5d\\x83=\\xcb\\xa8\\xaa\\xbc\\xbb;x\\xbc\\\\\\xea=\\xbd}\\xb6g=e8\\xd8\\xbb\\x96-m\\xbc\\x04Y\\x11<7\\x07t<K\\xe5\\r\\xbd!\\x0e\\xb3\\xbc\\xe5\\xa7\\xf5\\xbd7\\xf2f<~\\xb1\\xc7<\\xb0\\xeb\\x04=R#f=\\x82&2;\\xd1\\x95\\xb3\\xbc\\xac\\x9d:\\xbcR\\xb8X=\\xbbn\\x81<$\\xb3]\\xbbA6==\\xc1# <\\xee\\xc1};N\\xdd\\r\\xbc\\xb2\\x1f~\\xbcNB\\xdb<N\\x0f\\xa9;\\xe4\\\"\\xe7\\xbd\\xe9>\\x13\\xbc~\\xcb\\xd8;\\x01\\x91\\xe2\\xbcF\\x8ao\\xba\\x17!\\x0b\\xbdB4\\x0b<\\xe5\\xc9\\x90\\xbd\\xdc\\x84\\xd7<\\x9b\\xe6\\xb5\\xbct\\x8c\\xb0<]\\xeb\\xc3<\\xce\\xa4\\xd6\\xbc\\x02\\xd0\\xa2\\xbd\\x12\\x94\\x85=s2\\x97=\\\"\\xac\\x92\\xbc\\x98\\x88\\x0c;\\xe9~:=\\xae\\x8b\\xf8\\xbc#\\xda\\x80<\\x7f\\x16\\x85=\\xe1\\xc4q\\xbd\\xbe\\xba\\r;\\x80+\\x7f<<V.;\\xe8\\x86\\\\=,\\x1c\\xe2<\\x9clY\\xbd\\xea\\x19\\xdb<V\\xbb_\\xbc\\xb9g*\\xbd\\xa7\\x18\\x12\\xbc\\xcaM\\xcb<f#;\\xbc\\xe8^\\xa2<\\x89\\xbb\\xd7:\\xda\\xdd8=\\x93\\\",\\xbcm{\\xd6\\xbc*\\xe5\\x9d\\xbd\\x1a\\xc9\\x8e\\xbcBt\\xc2\\xbc\\xb2\\xba%\\xbd\\x9d<-\\xbd\\xfb\\xe6\\x9e;\\x06E5=\\xac\\xd0\\xab<0\\x90\\xd8\\xbc-\\x0bY\\xbd/\\x99\\xa9=r\\xafT=\\x10\\xd8\\x1a\\xbc\\x04@\\x99<6\\x96F\\xbd\\x92Z\\x00\\xbd\\x19\\\"t\\xbdAW%\\xbdhn\\xc4=\\xad\\xbcb=\\x19\\x9d\\x8a;`\\x1f\\x8d;5C\\xef<C\\xc0@\\xbd\\xd1%\\xc0\\xbc\\x08T\\x1d<kt\\xe8\\xbc\\xe7\\xed\\x89;gs:=-v\\xb0<Z\\xc0\\xad\\xbc\\xe5eQ\\xbd\\xbdiv=\\xb7\\xe7\\xc8\\xbc)\\x94\\xd7\\xbc\\xa4\\xb65<\\xaa\\xb6\\xea\\xbd\\xd3@\\x85\\xbb\\\"\\xdf.<\\xe7`\\x8e\\xbb\\\"\\x8a(\\xbc\\xff:n\\xbb7\\x8br=29\\xe2=\\x90a\\xa8<\\xb4\\x11\\x86=\\xb8&\\x1a\\xbd\\xc6\\xdb\\t<{\\xc9\\xf2<1\\xdb\\xe4\\xbc\\x0e\\xc0U\\xbc\\xb2#\\xac\\xbcI\\x86@=R$\\x9d\\xbc\\x92\\xfc:<\\x89\\x83t\\xbdK\\xb7\\xd7<\\xec\\x15,\\xbcP\\xdfs<\\xd2Q\\x8c\\xbb\\x0b\\xd6B\\xba?kQ=\\xb6\\xbbS<q\\x9e)<\\xbf\\xc4\\xd8\\xba\\xa1\\xf0\\xa6\\xbc\\xe3\\x0c\\xb4;tr\\x89\\xbc\\xb3\\x1d\\xc8\\xbd\\x8bU\\xcd\\xbb\\x9d\\xbe\\\"<%\\xa2\\xec\\xbb\\xab.\\xab<\\xcc\\xce\\x89\\xbd\\xb3@\\r=\\xafb\\r=\\x16\\x9dz<(\\xd7R\\t\\x81\\xbd\\x1b\\xbd\\xd9\\x93\\x1f\\xbd\\xa1\\x05s=\\xd0\\x18\\x0c=\\xa9)\\xc1\\xbc6:\\xfe<\\xac^\\x8f;\\x03$\\xea\\xbc\\x98\\xff><:\\r\\xe8\\xbbo$\\xca<\\xe5q&=\\xda22\\xbc!\\x19n<\\x8fO\\x9b\\xbc\\x0c\\xc1\\xe9<\\xfe\\x83^\\xbb(\\xe5\\x95\\xbc\\x8a\\x84\\xf6\\xba\\xed \\xea<\\xe3\\xae\\xea<\\xf8\\xba\\x0e\\xbd\\xcf\\xd0\\xb5\\xbc\\x8e\\xdf\\xdd\\xbcB\\x911=\\xe14\\xc5\\xbb\\xa7\\xe3\\xb0\\xbcCqr\\xbcyT\\x86\\xbc|\\xa4P<D\\xbc\\x10=\\x93l\\xc7<\\xa4\\xe6\\xc1\\xbcT{\\x83\\xbd~\\x84\\xb7\\xbcE\\xa3l<\\xf8A\\x01\\xbbxS\\x1b<J\\xdd!<\\x92%\\xc7<\\xa6Ci<\\x99]\\x85</\\x85&<\\xb2+\\xa1<\\x0e\\x17p<$!\\x16\\xbd\\xc4\\x88\\x8e=\\x9e\\xbe\\x88\\xbb;\\x19\\r\\xbd\\r\\x1c\\xd1\\xbc\\x1c\\x93-\\xbc\\x174\\x95\\xbd\\x1d\\xd5E=\\x11\\xa0\\xfc:\\xef\\x84\\x9a\\xbd\\xd6*A=\\xc1\\xa2\\xe1\\xbc\\x0ex\\xa9\\xbc#Z\\xcd<fC\\x17=\\x99\\xaf+<\\xc5\\xb5S<\\x9fB\\x11\\xbd\\x8f\\x11\\xc2<\\xd9\\x81?\\xbd\\xd1\\xd5\\xb7\\xbcTo\\xf1\\xbc\\\"WL\\xbca^\\xbd;\\xb6V\\\\\\xbb\\xa8\\xb6\\xe2<\\x81\\x9b\\x9f\\xbcW\\xf3&;\\x19\\x06\\x10=\\xc9\\xb0\\x0f<\\xeb\\xda\\xc9\\xbc\\x0b]\\xb6\\xbc\\xf6=H=\\xd1\\x846\\xbd\\xa8cE=\\xd4.U\\xbd\\xe0\\x9a\\xb3<\\xd0xv\\xba\\x88\\xcb\\x97<>\\xab\\xcd\\xbc\\x9e\\xf8-\\xbd\\x8f\\xb6\\x87=\\x84#A<n}R\\xbd\\xa3\\x0cx=\\xc6\\xe2\\xbe<\\xc81\\r\\xbdTj<=Zo\\x99:\\xe2\\xbf\\x90=W\\x16\\xd1<.\\xbc\\x1c\\xbd\\xed\\xfeq<\\x92O\\x8a\\xbc\\xfa\\xb8)=8\\\\\\x04\\xbdq_\\xb7;\\xe5\\xe7Q<\\x94\\xaf\\x12;\\xbf\\x8c\\x8d=(W\\x84\\xbb<\\xd3`<q\\x14\\xec\\xbc\\xc0\\xeaJ\\xbc\\x9b\\xed\\x90\\xbdk`|=\\xd1\\x1d\\x1e\\xbd=}>\\xbd\\xc9K\\xff<a\\xd4t=\\x9a}\\xbb<J\\xf1\\x0e=d\\xcb\\xad\\xba\\\"\\xd98\\xbc\\x14\\xaf\\xca\\xbc4D\\x9e<7\\x9dF\\xbc\\xd8\\xe7\\t\\xbcI\\xe4\\xa9\\xbb\\x94\\xc4l=\\xb9\\x9e\\t\\xbc\\xc7\\x86L\\xbd\\xd4\\xe9\\x89\\xbcj\\xf6/=0\\x82\\x1a\\xbd\\xdczs\\xbd+\\xfbm\\xbd\\xb0\\x1dP=\\x17\\x15\\\"=U-I=\\x03\\x83\\x16=\\x8d\\x16P=\\x06\\x88\\xd1\\xbc;\\x03u\\xbcr\\xb9)\\xbd\\xbb\\x8bm=\\x13\\xf3\\x97;\\x0eQ\\xa2\\xbd\\x05\\xa1\\x0c\\xbb#\\xf6R;\\xff}\\xeb<\\xddj\\xa5\\xbcvq\\xe7\\xbb\\x0e\\xb6\\x1b=\\xb4\\xfa\\xa1<\\xde\\x11\\x06\\xbdR\\x81\\x8b\\xbc\\x10\\xf2\\x16\\xbdM\\xff\\x02=\\x8b\\x0f\\xc2<\\n\\xf9\\x16=mOu\\xbd\\\"P\\xb9\\xb9\\\\\\t\\xc4<\\xc5p\\x1d=\\x04\\xf3\\xf8;\\t\\xf4!=N\\t\\x02=\\x02N\\t\\xbd1{E<|\\x11\\xc3\\xbc\\xb9\\x0c_<\\xb4\\xa2\\x9c<\\xeb\\xd8(=\\xc2vO\\xbd\\xca\\x96\\xbd<\\x8eG\\xbe\\xba;\\x81\\xcc<B\\x9b\\x7f\\xbcl\\xf2\\xdd<\\xad\\xb5\\x15=\\x07\\x92\\x99<F\\xdf\\x05=\\x87Ql=\\x0f\\x104\\xbc\\x19q\\x8e\\xbco\\xb5+\\xbc\\xa7\\xb6\\x05\\xbd\\xe8P\\xa7\\xbc\\x05\\xe57\\xbc>Q%<C\\x80U\\xbc\\xc4\\xf8\\x1a\\xbd=\\x10\\x84=Z\\xcf\\x87<\\x9f\\x9d\\x9e<\\xe3\\xd4\\n=+\\x9cH\\xbc\\xab\\xf11=4\\xf4\\xab\\xbc\\xdc\\xa0\\x81\\xbd\\x8b\\xb2W\\xbd\\xa7\\xd2\\x85\\xbc\\xe72\\x19\\xbc\\xf0_\\xd5\\xbc)\\xf1\\x8b\\xbd\\xa5$#;\\xe0\\x01\\xa39Q\\x04r\\xbd\\xe2\\xaf=\\xbd\\xf5\\xab\\xfa\\xbb\\xab3\\xfb\\xbb\\x81\\x06\\x94\\xbcFHK\\xbc4\\xa5\\xfc\\xbc\\xc9E\\xa7\\xbb\\xa7f/\\xbb\\x95DQ<\\xd2\\x18\\x82;\\xad\\xdbb\\xbc@r\\xbb;\\xb3\\xcdc\\xbc\\t(\\xb0;\\x1e\\x85\\xfd\\xbc&O+\\xbdKh_\\xbc\\xbf\\xa8\\xa8\\xbc\\xc4\\x9e\\x07=\\xb4\\xfd =\\x0cr\\xb8=\\x1d\\x11\\xca<\\xfa\\xec~\\xbb\\xe5\\xe0\\r\\xbd\\x9a\\x0b\\x0b\\xbdYd7=q\\xb3\\x87\\xbb\\xab{|=\\x8dR;\\xbc}%\\x00\\xbc\\x02p\\x05\\xbda^\\x82\\xbd`\\x89\\xc1\\xbcIW\\x0e\\xbd\\xb6\\xa1\\xcd<Y\\xb0\\xaf<f\\xd6\\x02=X\\xc5n=\\rA\\x12\\xbc\\xefC\\xec<\\xd4\\xc4O\\xbc\\x98\\xe5\\xb5\\xbcg\\xa7\\xf6\\xbb\\xb9M\\xa0;\\x89bq\\xbd\\xf1NG;\\x18f-\\xbbZ\\xca\\xeb<;\\x92F\\xbcL\\x0eo<\\x8a\\x98s\\xbb\\xc6;\\x01=\\x17a\\x10\\xbd\\x13L\\x8e\\xbdr\\xde\\xa8\\xbb\\x9f\\x1a\\x98\\xbdn\\xd2\\xf4<\\xf9\\xaf-:\\xe9\\xf6i=\\xbf\\x85\\xb5<y$\\xc4=\\x96W\\xdf;\\xeeh\\xb6<:\\x8d\\x1e=r\\x96\\x8e<\\xc2\\xa6F=\\xad\\x87\\x1b\\xbd\\x8cl\\x06;\\x07\\xd0&\\xbd\"\nHSET bikes:10019  model 'Orcus' brand 'Peaknetic' price 2775 type 'Kids bikes' material 'full-carbon' weight 9.0 description 'The latest kid-specific bike brand on the scene, this brand aims to offer high-end, kid-friendly bike geometry at an economy price point. It has a lightweight frame and all-carbon fork, with cables routed internally. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\x86\\xbc\\xce\\xbb>h\\xef<\\x06\\xcdI\\xbd\\xaeK\\xd8\\xba\\x0f{\\x0c\\xbc\\x92\\xdf==\\xed]\\x9a\\xbc\\xe5G\\x11\\xbb\\xb9\\x9c\\x94=%\\x1a\\x9a<,\\xe74\\xbb\\xed/<=\\xf2r\\x03=~\\x08\\\"\\xbd\\xba\\xdb\\xe6<\\x8c[\\x16\\xbd\\x91\\\"\\x00\\xbb&;!\\xbd1\\x9b\\x04\\xbd\\x0c7\\xe6\\xbd\\xa7\\x8e\\xca<\\t\\xd4u\\xbdMv\\x05=H|\\x91;\\x03\\x05\\x9e<\\xce\\xc6\\xb9;v5z<\\xcbX\\x04=\\xf3E(;\\x07\\xf7{=\\xb1\\xb7\\xa1\\xbc\\xde\\x15\\xc7\\xbc\\xdf\\x90P<\\x9b\\xf4\\n=\\xcd\\r\\xc5<\\xa9\\\"\\x95\\xbc\\xb5\\xbcv=X7\\xdf\\xbc\\xc5\\x16\\x8d<[\\x8be\\xbc(\\xad\\x1b=\\xbf\\nS<\\xe1I\\x1f;v\\xe3\\xdb;\\xf8++\\xbd\\xc0\\x03i\\xbc\\x1b:P<\\xc2.\\xab\\xbc\\xfd\\xab0\\xbd(\\x91E\\xbc]c\\xfc\\xbc\\x94ny\\xbc\\x11D\\xb8\\xbbl\\x93\\xe7\\xba\\xdb<\\xe4\\xbb\\xe8V\\xf7\\xbb\\xd7\\xb3\\xb6<\\x1e\\xf8\\xc3\\xbd\\xe6`\\x97\\xbd\\xdc|\\x8c=M\\x8de:w\\x01\\xac=\\xb5\\n\\xfe<6V\\x9c=\\xa0\\x18\\xe4<\\xbar3\\xba\\xe1\\xea\\xd8\\xbc=\\xae\\x1f\\xbd\\xe6\\x1b\\x8c\\xba\\xff\\xcf\\x01<F\\x9a)\\xbbR\\xd6*\\xbc}\\xb1\\xe7\\xbc\\x01\\xe2\\xa3<\\xce\\xdf4\\xbc\\x8ft\\xc0<T\\xe4\\xb3<B\\x10i\\xbd\\xa2JN\\xbd\\x03\\x07\\x1e=\\xcc\\x05\\x13=\\xa4\\x1a\\x0c\\xbc\\xc0\\r(\\xbc3`\\x1a\\xbdL\\x96\\x9d;\\x8a\\x92\\xa0=CB\\x82<\\x88Z\\t=O\\xc5\\x00\\xbc\\xbc\\xf7\\xaa\\xbb\\xb5\\xd0\\xdc<\\x87f\\x98\\xbd\\x10\\x85\\xeb\\xbb\\xedN$<\\x91\\x8f\\x90<4\\xf9@=\\xa8\\xe3\\x8c\\xbdx\\x84\\x8a\\xbd\\x0fD\\x00=\\xdd\\xba\\xd0\\xbd`\\xfa\\xb6<\\xfd\\x9fI\\xbc\\x023\\xac<]\\x1d+=\\\"3}\\xbc\\xa5Y\\xf4=\\xdfA\\x11\\xbcE\\xb8\\xb5<p\\x8e7\\xbc\\x1d\\xfb\\xcc\\xbc\\x80{\\x8f;x1Y=J\\\\S\\xbc\\x91\\tF\\xbb\\xaa8\\x9c\\xbd_\\x18\\x88<,\\x11\\xd1<e\\xc5\\x15<At4<f\\xd4\\x05=\\x87\\x08 =\\x17\\x83\\xbc;h6\\x8e\\xbdb\\xf3d\\xbb@\\xdc\\xb4=\\x90\\x816\\xbd\\x81\\xfc];\\xe4\\xed:\\xbd\\x19M\\xd7\\xbc\\r\\x11\\x9a\\xbc]\\t\\xcf\\xbb+\\xf2A\\xbd\\xa1\\xe1\\xbc<\\x8a\\xed#<\\xa9\\xba\\x00\\xbcb\\x9c\\xc4<\\xaen}<d\\x83\\x1d<\\x9d\\xdas\\xbc\\x83\\x9e\\xe9\\xbc\\xea\\xf3\\x1d\\xbdW@\\x85\\xbd\\x83Q\\xb2\\xbb\\x81\\xe2\\x88=v.\\xa6<f\\x94\\\\\\xbc\\xcf\\xaf\\x85\\xbc\\x04\\xae\\x97<\\x10\\xd0<<\\xe1\\xb85\\xbd\\x1d\\xa9\\x98\\xbc\\xdc0\\xf3;;\\x14\\xb1\\xbc\\\\\\xcd\\xcf<3\\xcc\\x8f;\\xa4-G\\xbcQu\\xcc\\xbc2\\xa1M;\\x8e\\x98Y=\\xf9\\xf4q\\xbd\\xc9\\xe9Y=f6\\x98\\xbc\\x07Z<\\xbd\\xad\\xb9e\\xbd\\x06I;=f@\\x13<\\x828\\xc4\\xbc\\x18 \\x12\\xbbp\\xe8e<\\x9bZN;kU.\\xbb\\x99\\x97I=\\xd4\\xd5\\x93<\\x15\\xacv\\xbd\\x93\\xe5\\x8f\\xbcO\\x84\\xe0\\xbc\\xa4\\x95\\x8d\\xbd\\xa1;\\n=\\x8b=\\x92;GK\\x87<\\xb4\\xa4\\x81\\xbd*\\x9fg=\\xcd\\xfc&;\\x9c\\xc0\\xc0<\\x01\\x9f\\x8f<\\x94\\xd5+<\\xba\\xac\\xd7;\\x11\\xf7\\x07\\xbc\\xfeD\\x9b;\\x01\\xe3\\xed\\xbbWlx\\xbc\\xfa\\xfc\\x8c=\\x10dO\\xbd1h\\\\=\\x8a\\xbd+;\\xd5\\x1a\\x14=v\\x12\\x9f\\xbcFV-\\xbd3\\xbe\\xcc\\xbc\\xd9\\xfb\\x1c=\\x0f\\xb5-\\xbc=?\\\\\\xbd\\x996Y\\xbd\\x95\\xc3\\x1e\\xbc\\xba\\xc2\\x13\\xbdX\\x1e\\xda<-\\x14\\xb5\\xbc\\x9a>\\x1c\\xbd\\xd6\\x1b\\x0f=b\\xe6\\x16\\xbd\\x05\\xe6\\xe3\\xbc\\x1c\\xad\\xcc\\xbc:\\xe6\\xb7<C\\x9f\\xb5\\xbd\\xb6\\xed\\x04;\\xd4\\x92\\xda\\xbc\\x02\\xecM;\\x048T\\xbd8E>=rS\\xaf\\xbdT\\xf71;\\x18G\\x1f\\xbdJ\\xd8\\x0f\\xbdW\\xcd\\x02=\\xda\\x80\\x8e\\xbdo\\x9bD\\xbd\\x89\\xe0\\x86<\\x96[\\x9f\\xbc\\xb2\\xd8\\xe1<\\xdf\\xbc1\\xbdT\\xf7\\xcf\\xbb\\xa8\\xfa4\\xbc\\xaa\\xd7\\x8e\\xbcG\\xb2M<\\x82{\\x80\\xbc\\xc9\\xb37\\xbd\\xfe\\xadE\\xbc\\xfe(0<\\x1e\\x0b\\xd5\\xbc\\xa9s\\x93\\xbc\\xc0\\x10\\x11=\\x94\\xef\\xd7\\xba\\xc0e\\x1c<I\\x14\\xa6\\xbcaw\\xcc<\\xbc\\xc6\\x8d<\\x08\\xbeD=l\\x18\\xf6<\\\"\\x9do\\xbdu\\x87\\xe9\\xbb\\xecU\\xb6<\\xf09\\x96\\xbd\\x92:F\\xbd5\\xbb\\xef<\\xa9;\\xbd\\xbd$d\\x85=\\xb4\\xe5=\\xbd\\x14\\x80\\r<\\xf3\\x011;\\xceY\\x15=\\x1e\\xd3R\\xbcX\\xbf\\x00\\xbd$\\xa49\\xbd\\xc1\\xe8\\x1d=\\x17[\\xeb\\xbc\\x16\\x11\\xae=\\x9f\\x9f-\\xbc\\xb2X\\xb8<no\\xd2<\\n\\x90\\xf9;\\x9f\\xa4\\x05\\xbcK*\\x1c\\xbd\\xb2\\x05\\x12\\xbdk\\x0c\\x1c=\\xd2\\x1d\\xf5\\xbb\\x88\\xf8\\xe9\\xbc\\xa8\\xfe\\x9f<+\\xae\\x06<\\xfb9\\x90;\\xe2\\xad\\x81\\xbdn\\xfc\\xb8\\xbbY\\xbd\\x06=A\\xd7\\x15<f\\xc0+=\\x82 \\xda\\xbb<Y\\t\\xbd\\xc2\\x13%\\xbd\\xc9\\x0e\\x8b<\\t\\xa3\\xb7\\xbd\\xf5\\xd7h=\\xf2PC\\xbbk\\xf9\\x8a\\xbc\\x98:\\x05==\\x1f\\xab\\xbcn\\x9d\\x99\\xbc\\xae\\xb44\\xbd\\xab\\xe9\\xcb;\\x96\\x11\\xb1\\xbcN@u\\xbb\\xcb&s\\xbd\\xc8\\x1d\\x17\\xbd\\xcd\\xde\\x8c<|\\x1cm=\\xb2$\\x03\\xbc\\xa8\\xf6\\xbe<\\xdf\\x13\\xf0\\xbb\\xfe\\x02\\x04<\\xed\\x9c\\x1d<\\xdc\\x18\\x0c=\\xf5_-=e\\xa7=\\xbd\\xc1\\x97\\x9b\\xbb\\x9eW(\\xbc+\\x009\\xbd\\xca=\\xbc<=\\xbe\\xab\\xbct\\xfe\\x8c\\xbc\\xb9\\xfa\\xb1=\\xfem\\x8d<\\x95\\xd1\\xad<\\xcc\\xea\\xf1\\xbb\\x0359\\xbd\\x803a=?\\xf5\\t;\\xe9\\x07\\xf8<:g$\\xbd\\x0b{j<\\xf5b\\x94\\xbd\\xb5\\x8aX\\xbc\\xb3+^\\xbc\\xf3\\x9f\\x96\\xbd\\xa1\\x8f$\\xbd\\xf8\\xcc\\xad\\xbc9G\\x1e\\xbc\\xfc\\x19(<\\xe5u+<#\\xbe\\xa3\\xbb\\xa8\\x17g\\xbc<\\xbe\\xf7<\\x9f\\xb1\\x1b\\xbdJM\\x95;N\\xb2\\xcd\\xbb\\x02\\x89\\xb5=5\\x86\\x80\\xbbp\\x0e\\x99\\xbcw\\x07\\\"=\\xed\\xb0\\xac=\\xccH0\\xbb\\xa0\\xb6\\xf9<\\xe6\\xf1\\xb1\\xbcI\\xd4\\x88\\xbb\\x7f\\x8e\\x00\\xbd\\x7fk\\x9f=\\xcb;9<*J\\xb0\\xbc\\xad\\x12\\xe4<Rw\\xd3\\xbb_eV\\xbdUGd\\xbcGS\\xb3\\xbd\\x8a\\xf06<\\xa8\\x94\\x1a=\\xb2P\\x84=|\\x9b9;\\x01\\xeb\\x1e=B\\xe0\\xb1\\xbclr\\x15\\xbb\\xd0\\xca\\xd5<X\\x19q\\xbb\\x1e\\xc1Q\\xbc\\xaf+\\x8e<\\xe7\\x13 \\xbc\\xcd\\x1eH<\\xdf]\\xcf;c\\x85$\\xbd\\x03\\xd86=v\\xc2\\xda\\xbbdx\\x9e\\xbd\\x0bM\\x97;2[]<\\x96\\xa1\\x91\\xbca\\x04|\\xbc\\x18\\\"\\xf2\\xbc\\xd9\\xb3\\xc9:\\rjD\\xbd\\x92\\xb7\\xc7<UD\\x9c\\xbb*\\xd4_\\xbbaab<\\x8d\\x8b\\x1d\\xbd\\xad=v\\xbdj\\\"\\xaa=\\x97*\\xbe=G\\xea\\xf0;\\xce\\xad\\x02<\\x03\\x0c\\\"<\\\"&\\x9a\\xbc\\xa7\\x86\\x94=RO\\x12=\\x0b\\xbd\\x15\\xbd\\xd7%\\x04;s.A\\xbc\\xbe\\xfb\\r=\\x0b\\xcc:=\\xc9\\xe8\\xbb;\\xcf&r\\xbd\\xae\\xe9]<\\xc9)\\x14\\xbd@\\xe55\\xbd\\x852\\x87<v{.=\\\\\\x08\\x9b\\xbc;x\\xce<\\xad\\xedz;\\xcd\\xbf\\x9a<h\\xc2\\x1f;\\x91{E\\xbd\\xc3\\x11\\xba\\xbdo.\\x82\\xbc\\xac\\xfb:\\xbd\\xd7\\xcaI\\xbd\\xf7Q\\xc8\\xbc\\xbf\\x9c\\r<\\xe4\\x18\\xe2<<\\x17\\t;\\xf2x\\x8c\\xbcY\\xd9.\\xbd\\xe8\\x92\\xa6=\\xfbW\\xe7<f\\xca\\xf0\\xbac\\xca\\xac<)\\x89C\\xbd\\xeeOF\\xbd\\x84\\xb9f\\xbd\\x01\\x96\\xa0\\xba*\\xd6\\x9d=\\xcc\\xcd\\x8d<\\xee:\\xa6<X\\xb0\\x1c<Q\\x10\\x82=f\\xdaS<t\\x9f\\xaf\\xbc\\x89\\xd7>:35=\\xbd\\x9a\\x01K\\xbb\\xb6\\\"\\xd6<$~e=$\\xf9_\\xbc\\xaf\\x1a:\\xbd\\xcd\\xeb&=\\xb2zq\\xbd\\x15\\xfe\\xff\\xbc2TG\\xbd/\\x7f\\x02\\xbe\\x8eZ\\xd4\\xbc \\x00\\xf5;,\\x95Y\\xbco\\x06\\xfc\\xbc\\x9f\\xcb\\x89\\xbc+c\\\"=4\\x89\\xd0=$\\x1c><O\\xbfB=9\\xbb\\x97\\xbc%y\\xea;\\x1e\\xe7\\x02=r\\x99\\xb3\\xbc\\xa8{\\x18\\xbc\\xd4\\x9b\\xa1\\xbc\\xaa\\xb3\\x96=\\x97\\x8d\\t\\xbd~:7<\\x90\\xd7\\x1a\\xbd{\\xf9\\xad<\\x17\\x19+<\\xbek\\xda<\\xe2T\\x0f\\xbd\\x82\\xf9\\xc2<7^J=\\xe8\\n\\xea\\xb9\\xfep\\x16<\\xd8m\\x00\\xbcW\\x95J<\\x99\\xf9\\xb3;\\x9c\\xc7\\xad\\xb9\\xa5@|\\xbd\\xc2\\\\\\xdf<\\x1d\\xca\\x01=\\xca\\x8a\\x1e\\xbd\\x19\\x13\\x83<\\x02\\xe8\\x81\\xbd\\x86\\xe8u<\\xbap\\x7f=\\xb1\\xd6\\x16=uvv\\tE)\\xa2\\xbcF\\xba0\\xbd\\xe1\\xc2*=\\xcdz\\xbe<\\xca\\x95K\\xba\\xe2o\\x1a=m\\xad\\xc7;\\xc6\\x04%\\xbd~\\xde\\xdb<R\\xfb\\x16\\xbd\\xdd`\\xc5<\\xa3$\\xdc<\\x8c>B\\xbb\\xf9\\xc4\\x14<\\x17\\xc0F\\xbcI\\x0c\\x9c<\\xd1Z\\xa5<\\xb6\\xdb4\\xbd\\xe6OT<0x]=C\\xc0\\xab<\\xaa\\x8e/\\xbdF\\x89\\xce\\xbb\\xb5\\x84w\\xbd\\x03\\x1e\\xed<R\\xa4\\xdb\\xbb\\xa8rX\\xbc$\\xfe\\x8b\\xbc\\x18(\\xd0;\\xc1/I\\xbb(e\\x89\\xbbIFD=\\x9f\\x15,\\xbc\\xff\\xdfF\\xbdw|G<\\x15cz\\xbckW\\x0b\\xbbN\\x8e\\xfc<\\xf1k\\n\\xbb\\x0e\\xb7\\x8f\\xbb\\xeb\\x04\\x85;\\t\\x90\\x05\\xbcr\\xe3H;\\xe3\\x17[=\\x81\\xa4\\x8e<j\\x9f\\xc6\\xbc\\xf8\\x9f\\xcf=b/1\\xbc4\\xd0+\\xbd\\x15\\xee-\\xbc/\\xfa\\x11\\xbc\\t\\xf7\\x89\\xbd\\xd3}\\x18=\\xdb?d;\\x13Z\\x82\\xbd\\xcc\\x01\\xf1<\\xd8\\x14\\xe1\\xbbG\\x81\\xa0\\xbc\\xe3/\\x7f<\\x85o\\xf4\\xbb\\xf2e\\x01\\xbc\\xf9\\xf6=<M\\xd6\\x15<T\\xb9\\x0f=2\\x10!\\xbd\\xf7<\\x89\\xbc\\x8c\\xdap\\xbd\\xad\\x05\\xc2;\\xe6\\xf6\\xe5:w?\\x9b\\xbc\\x95iZ<\\\"K\\xd5\\xbc\\xdeY\\xa6\\xbc\\t\\x85#=\\x85\\xcd\\r=\\x12\\x16\\xaf;b\\xf2+\\xbc\\x15\\xb5\\x97=\\xa7\\xc0\\x0e\\xbd\\xf9=\\xa0==\\x0b\\x8c\\xbdT\\x02\\xb3<\\xd4j\\x0f<\\xb8\\xe5E=W.\\\"\\xbd.\\xed&<\\x9f5\\xeb<\\xb11\\xbb;\\xac\\x12\\x1a\\xbdZ\\x9f==l\\x9c\\xf1<\\xd6\\x7f\\x8d\\xbcv|\\xc9<A\\xa9A;Wo*=\\x13\\x83\\xe0<9\\x1d\\x9b\\xbc\\xac\\xb5D=\\xc0\\xd8\\xa4<\\x89\\xbc\\x8a<\\xfc=+\\xbc;+\\x8b<\\xe9M\\xe3<\\x88)5<\\x97\\x9e\\x1b=\\xeb\\xd2\\xd5\\xbc\\x0b\\x01\\x0e=b\\x93\\xc0\\xbc\\x01[5\\xbc\\x19-\\x89\\xbd\\x80\\xed_<I\\x8c\\x80<\\xc2\\x0f\\xa6\\xbcJwk=M\\xfc\\x9b=\\xb3\\x16\\xc8\\xbbn\\xf2\\xdd<\\x80;~\\xba\\xb5f\\x86\\xbd+\\x9e\\x97\\xbc\\xa2\\x9f\\xc1\\xbc\\xb8v`\\xb9a&S\\xbb\\r\\xf40=\\x95<\\xc8</I\\xd0<bS\\xdd\\xbc\\x83\\x05/\\xb9C+\\x89=+\\xfa\\x9d\\xbb[\\xc2F\\xbd\\xc4M\\xa6\\xbd\\x98\\xf0%=\\xa8%\\xe8<1\\xa1-=\\xcdm\\xc5<\\x9awT=\\xe8\\xef\\x1b\\xbd\\xfb\\xb8\\x1f\\xbd\\xbe$\\xed\\xbc\\xc4\\xa3\\x19=Mb\\x90\\xbbH\\xa2<\\xbd\\x87\\xdd\\xaa;\\x9c\\x9b\\xa4\\xbc\\xb2S\\xb0:\\xd5\\xde\\xf9<\\x06\\x17\\x85;\\x94\\xff\\xa8<p\\xfc\\xbc<\\x81x\\xd3\\xbc\\xea\\xbd\\xb5\\xbb\\xed\\x0c\\x7f\\xbd<\\xbcg<\\x9d\\xb4\\xc3:\\xfb0\\x8a<\\xd5\\x1eP\\xbd\\x1e\\xeb <5\\x12`<\\xe8\\xb1\\x15=U \\xb5<E\\xf0r<^\\x9b\\x14\\xbc\\x1c\\xa7\\xb0\\xbc\\x1e\\x06k;\\x9d\\x15\\xaf\\xbc\\x97b\\xa1\\xbc\\x88\\x9c\\x16\\xbc@{Z=\\xb7A\\xe2\\xbb\\xed\\t\\xaf<W\\x0c\\xc3<_I\\x80;q]\\xc5\\xbc\\xcd\\xdb\\xf3<J\\xcb\\x83=\\xab\\xd6\\x05\\xbdo\\xf6w<|\\x92F=\\x9a\\x06\\xae\\xbb\\x0f\\x16\\xbc\\xbc\\x85\\xac\\x89:\\xb2:5\\xbd\\\\\\rA\\xbbk\\x1b\\xbb\\xbc\\xd1\\n/\\xb95]!<-\\xfa_\\xbc\\xe0{\\x8d=b\\xda\\xca\\xbbz7\\xac<\\xc6=\\xe6<\\xff\\xff\\x1a;\\n\\xfe\\xfc<\\xd6\\x8c1\\xbd\\xdd\\x80e\\xbd\\xaa\\xa18\\xbd\\xdbs+\\xbc\\x8eU\\xf0\\xbb\\x1c\\x1d\\x1d:\\x89\\xe35\\xbd\\xac\\x8c\\xab9\\xbf\\x02.\\xbad\\x08\\x7f\\xbdd(\\xfd\\xbc\\x15K\\xb0<E\\x02\\xba\\xbc\\xa6a\\x87\\xbd\\xd0\\xc6\\xdf\\xbaSN\\x12\\xbd\\xf90B<\\xba\\xe5B<\\xc8&\\xf1<\\x98q\\x98;\\xcd\\xd9\\x10\\xbc\\xdd\\x82\\xe4\\xbb@n\\xb6\\xbb0\\xf3N<)\\xbe8\\xbd7\\xe3P\\xbd\\xeb\\xa5\\xcc;T\\xf3\\xec\\xbb\\xf3\\n\\x18<\\xd6\\xf6m=\\x11\\xbbA=FZ\\xe3\\xbb\\xf8\\x04,<\\xb64>\\xbd_\\xc7\\x8c\\xbdL\\x9fA=\\x12\\xe0\\xa1\\xbc\\x9b\\x9a\\xff<!\\x1bC<2\\xe1\\x95\\xbc\\x17\\xaf\\xbd\\xbc:\\xdb#\\xbd=e1<\\xb3%\\x83\\xbc%\\xa4]=O\\xc6\\x04<\\x1c,\\x0c=\\xc8b\\x8d=\\xcbE&<\\\"\\x1b\\x18=\\xce\\x87v\\xbc\\xf4n\\r\\xbd\\xdc\\xa2\\x86\\xbc\\xbf\\xc7\\x0e=\\xbc\\x04\\x8c\\xbd\\xe7\\xbb\\x0c<W\\x9d\\xd7\\xbb\\\\\\\"\\x1a=g\\xa8\\x8c<\\x00S~=N\\xe6w\\xbc\\\"\\xac\\x0b\\xbc0\\xb1\\xcf\\xbcYP\\xbb\\xbd\\xbdl\\x82<k\\x17\\x9b\\xbd\\x9a\\x80\\xc8\\xb9\\xfe8N\\xbc\\xd7\\t\\xc1=\\x06dO=\\xd6\\x15\\xb5=)8\\xfe;*X(=\\xc5\\xfc\\x8e=&\\xbc\\xa3<\\x01\\xe3\\x90<\\x9e\\xac\\xdd\\xbb\\\\\\xc9\\x00<\\x84\\xcc\\xd6\\xbc\"\nHSET bikes:10020  model 'Gandalf' brand 'Nord' price 507 type 'Enduro bikes' material 'alloy' weight 13.8 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"\\x7f6\\x86\\xbc\\xe1g4;q\\x0c\\xbc\\xbbp\\x94\\xf8<f\\x8c\\xdb\\xbc\\x0bh\\x0e=m\\xf9\\xe2;\\x19\\x83\\xe5\\xbc\\xbe\\x87\\x90=N\\x0c+=xG\\xcd<\\x1fv0\\xbc>\\x17\\x9a<z\\xf3\\x01<\\xeap\\x90=U\\x1f7\\xbd[x\\x19=\\xfe\\x97@\\xbd{\\x1a\\x93=WZ\\xa2\\xbd\\xf6\\xce<\\xbc\\x19e\\xd4\\xbc\\xd7#\\x03\\xbc\\\\|L\\xbdg\\x10<\\xbdum\\x87<q\\xf8a=\\xebhC\\xbc\\xe2\\xd8\\xa2\\xbd\\x19\\xf4\\xc9=\\x96;\\xe7\\xbc\\xf54\\xb7\\xbc\\xc6\\x8c\\xb9<\\xdc\\xd8\\xe3<\\\"\\x96\\x1e\\xbd\\xae\\xc9\\xf3<c%\\x80\\xbbW\\x93\\x9c\\xbcnG2\\xbd\\xae\\xfc\\xc1<\\x10\\xbf\\x0e=\\x94k!\\xbd\\x1e<6=/\\x06m\\xbc\\x89\\xa8p\\xbc\\\"\\xe4\\xe1\\xbc<L\\xc2=yS\\xa2\\xbdw|\\x12\\xbd\\x1f\\x7fp<\\x98`\\x16<cA.\\xbdx\\x8e<\\xbc\\xb5\\x83\\\"\\xbc\\xcc6,\\xbd\\xa4\\xd9\\x0f\\xbd\\xb9\\xf6\\xd7\\xbc*\\xaf#\\xbb>{\\xfe\\xbc\\xf8\\x19\\x97=cV\\xd9<\\xba\\x19\\x90\\xbb\\x8d\\xd3\\xf9\\xba5w\\xc4<\\xa6\\x92$=\\xd2\\xcbz<\\x91\\xaa;\\xbcY\\xc2&=\\xdcDr<%\\xc8J\\xbc\\xd7m\\x8f:\\xdc\\xc2\\\"=\\xe9\\xe37\\xbc\\x0c\\xf4\\x8b;>\\xe1\\xe8\\xbc\\x99\\xf5\\x03\\xbdK\\xa1\\x87;e\\x86\\x80\\xbdV\\x1a\\x9a\\xbd\\xab\\xee\\x00=\\x99K\\x19=\\xad]\\x92\\xbc*\\xca\\xc7\\xbc\\xb6\\x81\\x9f\\xbd0\\xe4\\xc0\\xbb3\\xe3\\x1f\\xbd\\x19\\xe8]<`\\x13\\xcd9,u9=\\\"\\xf9\\x04\\xbc)\\x82Q\\xbc\\xa7,O\\xbd&\\xd2\\x19\\xbd\\xcb\\xb4)=\\x8a\\x82\\x8d<\\xe1\\x9f\\r=\\xd4]<\\xbd/\\xe9c\\xbc\\xc8H\\x8f<\\xc4\\xa8\\xf4\\xbc-\\xd8-\\xbd\\x8b7\\xa3\\xbaK\\x9d$;\\xf54_\\xbd$*\\xa48cT\\xe3=\\xdc/];0|\\x01= \\xb7J\\xbcuj\\xea\\xbb\\xe8\\xc0(\\xbd\\xd2\\x1f\\r=\\x15\\xbf\\n=\\xee\\xa0\\xda\\xbc/wD\\xbd\\x86oF\\xbdBj\\x1a\\xbc/\\x1a\\n<\\xecH\\x9d<v\\xd8\\xcb\\xbc\\x13\\n\\x15\\xbcr\\xde\\x18<\\x03\\x845\\xbd\\xa5\\x07\\xfb\\xbc\\x86mC<\\x16v[\\xbb\\x87\\xf6\\xf7<\\x8825\\xbd\\xcb\\n&\\xbdp(r\\xbd\\x96/\\xb3\\xbct!\\x94<\\xa6q\\x14=\\xe6\\xc5.\\xbd\\xad~\\x84<\\x88\\x0c\\x88=e\\x84\\x16\\xbd\\xee\\xc6\\xb7\\xbc\\xb4\\x06\\x05\\xbc\\xa6\\xd6B\\xbc\\xd3\\x15\\xc0\\xbc\\x08\\xc7J\\xbd\\x11\\x97\\x03\\xbd$\\x00t=\\x0c\\x8b<=\\x08mg<\\xaa\\x02{<\\x1f\\\"1;4\\xa6\\x93<\\x1dw\\xac\\xbc`\\xb4X\\xbc\\x98\\r^=;>\\xdb\\xbc\\xa3\\x91\\x00;)i\\x86\\xbd\\xe6\\t\\xd5<\\xec\\xc3\\xad:\\xb8v)\\xbd\\x0ez\\xcc<\\x8c\\xc7\\r\\xbd\\xb1XV=\\xe1\\xa6\\xf7:3sk\\xbc\\x93t\\x8c\\xbd\\xb7\\x86\\x98<\\xef\\x84\\xda<\\x80\\xa39\\xbd\\xafd\\xd6<\\xf1\\x02\\xdb<HxH\\xbc\\x18qs;\\xb6\\x108=`\\xe0Q<S\\xcd\\xe2\\xbc\\x15\\xbbY\\xbc\\x87\\x06\\x0b\\xbc~\\xc3,\\xbde\\xe6}=r\\x7f\\xf4\\xbb\\xd0\\x98x\\xb9\\x867%\\xbdNr\\x90<\\xea\\xcd\\x15=\\xad\\xfeh\\xba\\xba\\n\\xe7<\\xed\\x18E\\xbd&\\n\\xc8<8\\x0c\\x94:6\\x9d\\x1f\\xbb3&\\xa2\\xbcq\\xf0W=\\xdeH\\x93<\\\"O\\xd8\\xbc\\xc1\\xfd\\xcd<\\xe3\\xf3)\\xbc\\xa6Z\\xcc\\xbc3\\x80j\\xbd\\x1f\\x98\\xcc\\xbch\\xde\\x05\\xbdT^\\x1b=\\xd8\\xa0\\xca\\xbc1_[\\xbd8t\\xfd\\xbc\\xf1\\t\\xfb\\xbc\\xdd7\\xb7\\xbc\\x9f\\xd2\\xd1<\\x07&\\x11=o\\x9c\\x18\\xbdz\\x08\\xa4;\\xd3|\\x00\\xbd\\xc7q\\x98\\xbc\\x88\\x87\\xc4\\xbc\\x0f\\x8f\\xb9<\\xb6\\x14\\xab\\xbcp|\\x13=o\\xe9\\x03=\\xe5\\xc5I\\xbc\\xf8\\xf5=\\xbd[E\\xdf<R\\xd1\\xf0\\xbd\\xd6\\xe0\\x87<\\x0fq\\xd5\\xbc\\xbd\\xb3\\x1e\\xbd\\x00\\xf7V\\xbcBz\\xbc\\xbc\\xed5\\xea;l\\xd1\\x17=\\x08=\\x08;\\xe4\\xf0|=s\\xb4\\xfc\\xbb\\x9fc\\x10\\xbd\\xea\\x91=\\xbda\\xf27\\xbdQ*6=\\x9c%\\xac\\xbc\\x93M\\x99<\\xceR\\\\<\\xd3\\x96\\xdb<\\xcf?\\xec;\\xceJ\\xb9;h\\xf7\\x99\\xbc-]q;\\x16r\\x02=\\x86&\\x1b\\xbb\\xac\\xb0\\xa4<\\x85j\\x98=\\xf7\\xec\\xc0;\\xe8\\x8c\\xd8\\xbd,M\\x85\\xbd\\x94\\xbf\\x9e<p\\\"\\xac\\xbc\\xc3_\\\"\\xbdI\\xff\\xd4:\\xd20\\\"<\\x16\\xfcl\\xbd|\\x17r=\\xc5\\xcbR\\xbdE9j;\\x0b>\\x1c\\xbd\\xce8\\x88=\\x90\\x99\\x1f\\xbc\\xca*\\x17;XU\\xb2;\\x93\\xb8\\xd5\\xbbl\\x02\\\"\\xbd\\xffma=aY*<\\xf76\\x80\\xbc4g\\xb2;\\x06x\\xbd\\xbb\\xa5\\xab\\x9e:f\\n\\x91<\\xc9 \\x9f\\xbc\\xe6p\\x86\\xbb\\xad\\xa5\\xb8\\xbc[!u;\\xac\\xae\\xd1\\xbc\\xe3\\x9e5\\xbb\\xea\\x08\\xa6=\\xa16\\xac\\xbd\\x11\\xca\\x80\\xbbV\\x17\\xa4;UE?\\xbd\\xc2\\xa8\\x11<\\xc2\\xc9\\xf3<\\x0cm\\x87\\xbbA\\xb8\\x05=\\x90A>=\\xc6\\x1bk\\xbd\\x85\\x0e\\x98\\xbb\\xc8\\t<= \\x1b\\xa5\\xbc\\x96x\\xa9<\\xeb4\\xee<\\x80\\xc9 <\\x04\\xfc\\x82\\xbd\\x7f\\x87j<^\\xff\\x01=\\xee\\xfd\\xb3\\xbb\\xb65\\x1e\\xbc\\xe4t\\x0c=\\xb5\\xf2\\x829\\xc3\\xfbu=^\\x8f\\xb7\\xbc\\xb4\\xe7\\xaf;x\\x10\\x16<x\\xefh\\xbc\\xc5\\x19z:\\x17\\x008=AP&\\xbd\\xbb\\xc7\\xe2:\\xd4\\xaeJ\\xbd\\x83\\x86\\xa5\\xbb\\xa1:\\xd6<_\\x0c\\xc4<\\xd1B\\x92\\xbc\\x9d\\xf7\\xfc\\xbc\\xd7\\x1e\\x9c=\\xce\\xe4L=R\\x9a\\xa5\\xbc\\xf8P_\\xbd\\xaa>\\x93<\\x1d.\\xc2<1WJ<g\\x06\\xdd;\\xcbNW\\xbd\\xd4\\xe8\\xcc<\\xfc\\n\\xf8\\xbc\\x8a\\x8bO=\\xc5\\xa0U\\xbc\\x00\\xde\\xa5<\\xe76\\xad\\xbdpB\\x9e\\xbcD\\xc5\\x91;\\x17M\\xc3:\\xe2\\\\\\xc1\\xbb\\x06\\x08-\\xbdK\\x7fr=`\\xa2\\xe3:\\x8c\\x01\\x84<\\x02+*\\xbd\\x1b;\\x02\\xbdX\\x983<\\x9fJ/\\xbc?\\x81/<\\xf0\\x80\\x17=\\xf3\\xe8w=\\xcf\\x99\\x89\\xb8\\x15AU=A\\xba\\xaf<K\\xe9\\xcb<\\xb1\\xdf\\x0f\\xbbw\\xf2\\xff;\\x99E(;\\xbe\\x0c\\x13\\xbc\\x00\\x9cO=l\\x92\\xab\\xbcdR\\x9d\\xbd\\xd0|\\xb3\\xbcqC\\xe1\\xbd\\xe8\\x94\\x0b\\xbd\\x86\\xa3\\x12=68 <iB\\\\=\\xe7\\xf2\\x05=>\\x8d\\xc6<\\x13\\xf2\\xe6\\xbc\\xa4\\x92\\xfc<qa\\x05\\xbdWJ\\xa2\\xbc\\x8b\\xba\\x89=;\\x10u<o\\x82@\\xbb\\x88\\x9b\\xb7\\xbc\\xe9{H<H\\xcc/=\\xec\\x86J\\xbd3*N\\xbd9\\xb0\\x06\\xbc\\xbf\\x17\\xe0<@WH\\xbd\\xbf{\\x89\\xbd\\x97bv\\xbd6\\xe5a<\\x82w?\\xbd\\xfb\\xd3\\xe6\\xbct\\xb9\\xf4<\\xfd\\xa2\\xcb<\\xd2\\xe6\\xd0=O&\\xb3\\xbb\\x82\\xdfB\\xbd\\xb2\\xd3B=8\\x18\\x97=\\xac\\xea\\xcb\\xbae\\xc0+;>\\xeb\\x11\\xbc\\rY\\x8f\\xbda\\x8a\\xd7\\xbc\\x81\\xf1\\xcf:^\\xe0\\x07\\xbd\\xd4\\r\\x98<\\xcd-_\\xbd`\\x15\\xd5<u\\x92\\x88=\\xde\\x9c\\x0e\\xbb\\xb5\\x85-\\xbd\\x8e\\x15C=\\x98\\x19D\\xbc\\xf9\\r\\xb4<\\x19(\\xde\\xbc\\xe0\\xaft=.9\\xca<q\\xb7-\\xbd\\x96z,<m\\xefa\\xbc\\xbf\\x92\\xb8<\\x1e\\x1f/\\xbc\\xef\\xfb\\xcf<q\\x7f\\xf1<*\\xdc\\x92\\xbb\\x93\\x1c\\\"=cR\\x8b\\xbb\\x86.\\xb8;}\\xe4%=\\x9b\\x8c;\\xbc\\xc7\\xbe\\x05\\xbd\\x85q\\xef\\xbc\\xfa\\xce==\\x17\\xa1^\\xbc\\xc117;cAj;:L\\x1d\\xbd\\xa6\\xab\\xe4\\xbb\\xebn\\x98\\xbd\\x11\\x97\\x88\\xbc\\xd5nf=Z\\xf4\\x15=\\xd8\\xefx=1\\xf0.\\xbc\\xdf\\xf3\\x8c\\xbc\\xb7\\x16\\xde\\xbcDK\\xd4<\\xd7\\xc5-;7\\x91\\x8f\\xbc/t\\x14=v\\xb8\\xbe\\xbcP\\xae\\xfc<\\xdb\\xfd\\xcd:\\x9d\\x8a\\xc4\\xbb\\x8bmb=\\x080(\\xbd\\xe8\\xdfc\\xbd\\xd7\\xean\\xbcB\\xc1{\\xbd\\xba\\xf8\\xdb<\\xd5X\\x98;W\\x7f\\xa1\\xbc\\xb4\\xf65\\xbcV\\xa9+\\xbdW\\x87M=\\x14\\xcc\\x92=,00<\\x03\\x14~=\\xb6\\xdf\\xf7\\xbc\\x01\\xcf\\xcf;j\\xa6\\x8f=\\xe5\\xa5\\x17=\\xfd\\x0e\\x98\\xbc\\xe0^l=y\\xda\\xe8<\\x89C\\xd4\\xbb\\x8d&\\xa8\\xbc@\\xfd\\x11=\\xd2\\xe5\\xa2;\\xdc\\xd0\\xf5\\xbbI\\x0f\\x0f<\\xb8\\xf0L\\xbd\\xb4\\x0fn\\xbd\\x85\\xbeK\\xbbp\\x080\\xbc6R\\xc4<\\xa5\\xfa\\xd1\\xbb=\\x93\\x86\\xbcR\\x1a\\x11=`o\\x89\\xbc\\x80\\xbf\\xd1\\xbds\\x0cM\\xbdz\\xd7\\x05\\xbd\\xcd\\x99-\\xbc\\xf2G\\xe5<\\xc73\\xe6\\xbbf\\x16\\x82\\xbb\\xea \\xf4<\\xdcv{\\xbb*~~\\t1\\xc2\\xb0<e\\xbe?\\xbb\\xd1\\xdd\\x01=\\x1e\\x18\\x7f<{\\xd0\\xfb\\xbbc9\\x04=\\x04\\xfd\\xe5\\xbc<\\xa1\\xe8:\\x06\\xab\\x11=\\x1f\\x8c\\xce\\xbb\\xaaT\\xf48F*Q=!\\x86\\x9a\\xbc\\xc5\\xf2Z=|\\xc4p\\xbb\\xbb\\x9f\\x0e<\\xaa\\xf5\\xb2\\xbd\\x15\\x06i\\xbc 8\\x07<\\xfa\\xf5m=X\\x10\\xdd<[3\\xa1<\\xf4\\xa9\\xd3\\xbc\\xad6\\xc9<4r\\xf2;\\x8f4\\xb4<\\xd3\\xb4u<\\xe0\\xa8\\xf1;\\x84\\x8a]\\xbd%\\x05\\xce\\xbc\\xbe4\\x8c<6\\xce\\x9a;\\xb1$\\x89\\xbc;.~\\xbdJ\\xce\\r\\xbb\\x15\\xa4\\xcb<\\xe2\\xc8:=V\\xc2&=t\\xc1\\x0c=\\x18\\xce\\xc7<U@\\x16=RW\\xf3\\xbcw\\xa6[=\\x86@\\x00=\\xf0\\xaa\\x92=\\x9f\\x8dm;\\x96\\xc2\\x05>\\xa3h\\xa0<y\\xa7\\xf4\\xbc\\xd39\\xf2:\\x15$8\\xbd \\xfa\\x94\\xbd\\xd1S\\x1a=t\\x82\\x04=,#\\xfd:\\xbc5f=\\x80\\xe7\\x16\\xbd^T\\x15=2\\x05\\x82\\xbc\\x15\\x86\\xde<\\\"\\xcc\\xb4;\\xa4\\x983\\xbc\\x10\\xbb\\xc2\\xbbm\\xac9=Xe\\xb1<\\xfd\\x9a`\\xbd\\x98\\x87P\\xbc2\\x1c\\x03<1G\\x10=V\\xa4:\\xbd\\x91*.<|8q\\xbc\\xda\\x8f\\xe6\\xbb\\xb5\\xc7\\xe0<M\\x9a\\xe0;b\\x1c\\xc9\\xbb7\\xd7\\xec;\\x1em\\x19=\\xb4\\xffu;\\x1d\\xfc\\xe6;gh\\xb9\\xbc@I\\x1a\\xbc\\xf4\\x05h\\xbd*\\xc9m=%\\x1c\\x02\\xbdd\\x87\\xd5:[:\\x81=\\x03lE=\\xfc\\x8a\\xd4<\\x9bv\\xb1<\\xef\\xa4\\x90<\\x95=\\x84<\\xe7\\nN=\\xeco\\xf3:\\x15Bo<c\\xf9\\\\=\\xa1\\xbah\\xbc\\x01\\xf1\\xba<8d\\x8f;\\xb3\\x87\\xc6<:c\\xf4\\xbc\\xae\\xcb@<\\xcd\\xb9\\x82\\xbc\\xf5\\xa0\\xed\\xbc\\x1fO\\x1a=Q|*\\xbc\\xd1\\xfc`;u\\xee\\r<\\xb2\\x1aI\\xbd\\xf8$d<|8N=\\x85\\xecf\\xbd\\xd8\\x038\\xbc \\\"^=\\\\\\xeeN= A*\\xbb\\xf9\\xfc\\xa6<tj\\xef\\xbcT$\\x1a\\xbd\\x18(p\\xbd\\x01\\x03)=\\xa7\\x9c\\xbe<#\\xa7H\\xbc\\xed\\x19\\xf6\\xbb\\x84\\xe1v=\\x1d\\x89\\x14=\\xc3/R\\xbd)\\x05N=F\\x14\\x06=\\n\\xcd\\x8f\\xbd\\xd9\\xdf\\x93\\xbc=\\xb5E\\xbd\\x8a\\xc5\\x0c=\\x9b\\xdcc;\\xaf)J=\\xa3\\x8e\\x80;\\xcb\\xfb?==\\x15\\x84\\xbd\\x15\\x0e\\x11\\xbc\\x1e\\\"m\\xbdw\\xc2\\xde\\xba#\\xe75\\xbd\\xfa\\xb4\\xa1<\\x81 \\x84<9\\x9f\\xdf<\\xe6zV;\\x92}\\x7f=\\x06\\xce\\x0b\\xbb\\xb9jm=\\xba%I=\\xa3\\xa1\\xac\\xbc\\x1a\\xd4\\xa9\\xbce\\xc8\\xb6\\xbdB\\xa9\\x04=\\x81\\xfa\\xee<\\xff\\xe1\\xfc<\\x1fL\\\"\\xbd\\x14\\xf4\\xba<,x0=\\xa8\\xb2\\xce\\xbb\\xc1\\xce5\\xbc\\xd03\\xa3<\\xffVj\\xbck\\xe5\\xb1\\xbc\\x04\\x8e*\\xbd\\x001\\xc5\\xbc9\\x1af\\xbd*\\xb3\\xd2\\xbcPX|<\\xd7V\\x82\\xbd\\xed\\x08\\xa1\\xbb\\x9d\\x10b\\xbd\\xc0\\xa3\\xbf<\\xc7T\\xf9\\xbb\\xba\\xad\\x91\\xbc\\x1d\\xd3\\x04=\\xe3\\xea\\x05<\\x93I>\\xba\\xcb\\xe6-=\\x9a\\x87\\x9a\\xbc\\x82j\\xbe\\xbcp\\xce\\xb9\\xbd\\xb7\\nm=[0\\xcb\\xbck\\xcek<\\x07wr\\xbd\\xc5\\x92\\x88<\\xbc\\xc4#\\xbb8\\x04\\x8d;\\xb9\\xa5\\\"<v\\xa8R\\xbd\\xdf\\xdf\\xe0<C\\r+\\xbdc\\x9fQ\\xbc\\xcff\\x0c\\xbd\\xe7\\x08\\x19\\xbd\\xb9\\x85\\x87<\\xde\\x91k;\\xc9\\x9a@\\xbd\\x02\\x04\\xd9\\xbb\\x12\\xae\\xbc\\xbc\\xf0B\\xdc\\xbc\\xe2\\x1f\\xdd\\xbc\\x86\\x81y\\xbd\\n(!\\xbc\\x8a\\\"\\x03\\xbd\\x0f\\xd9\\xb8\\xbb&\\xcc\\\"=[\\x85\\x01;\\xd0~/\\xbdV\\xc5f<\\x03\\xda\\xdd\\xbc\\xd2\\x7f\\x1c=U\\x9d&\\xbd\\xf1,\\x06\\xbd\\xc9D\\x1c\\xbb\\xa3\\xde|=\\x87\\xe8\\x98\\xbc?\\x91N\\xbd\\xe7F&\\xbdo5\\xa9<\\xf9\\x9e\\xce\\xbc\\x95_\\x05=\\xe2t0=\\xe9\\x8d7=\\xb8\\x06\\x12=\\xefX\\n=\\t)O\\xbd\\xb5\\xbd\\xaa\\xbd\\x95\\xe6\\xa2;\\x84\\xec\\x87<\\xc09K=\\xf7\\x17\\x83\\xbc\\xee\\xb9\\xd6\\xbc\\xaf\\x92\\xf1;\\xd3\\xc0\\x96\\xbc\\x8c%\\xa7<\\xf2\\xf7\\x13=^\\x12\\xf3\\xbb\\x96J\\xc3\\xbb\\\"\\x9c]<\\x9a\\x9c\\xf4\\xba\\x82\\xa4\\xa6=7\\xdbJ\\xbc\\xde\\xec5\\xbc\\xc5\\x03\\xfb\\xbb\\nF\\x12\\xbd\\rH\\xb1;\\xe0\\x0f\\x1a\\xbd\\xec\\x94\\x02\\xbb-\\xce\\xc9<\\xa2\\xba\\x84\\xbcn\\xb2\\xe0:[\\xa4\\x84=6\\x91=\\xbd\\x08\\x02\\x88\\xbc\\xb8D\\xab\\xbc\\xe0\\x08R\\xbd\\x82\\xa9\\x0f\\xbc\\x9bL\\x95\\xbc-\\xc1\\x01<\\xb2\\xc3\\x89;\\x83\\xfb1=\\xbc\\x1d\\x16=j\\xa8\\xcb=\\x90@E<\\xcea\\xca<\\xc7\\x02\\xc5=N\\x19\\x8d\\xbc=P\\x82=&\\x0e\\xdf:\\xfe6\\x86< *\\x8a\\xbb\"\nHSET bikes:10021  model 'Polydeuces' brand 'Tots' price 3974 type 'Enduro bikes' material 'alloy' weight 7.6 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\xd1\\xfbP<g\\xe4\\xfe:\\xc9\\xcc7\\xbb\\xc4\\xda\\x88<Av\\x01\\xbckw\\x80<7\\x1a\\x82\\xbb\\x7f\\x80\\x1f\\xbd~f\\x87=\\x97\\xd1\\xfd\\xbb\\xfbs\\x1b\\xbb\\xe8?<\\xbd>2\\x81<\\x98l\\x7f=\\xc2\\xb1\\xd6<\\x0eYj\\xbd\\xc3\\x9a <\\x08\\xfc\\xb4\\xbc6\\xe7M=\\xe3\\x9f\\x84\\xbd\\xe2K\\x10\\xbd\\x1f\\x98\\xe6\\xbb\\x92{%\\xbd\\xba\\x16\\x1f\\xbd\\xf6&J=5\\x13\\x13=\\x1cB&<\\xea\\xab\\x9a<A*\\x95\\xbd\\xa7\\xafS\\xba\\xeb\\xaa0\\xbd\\xae\\xfb\\xc1;\\xc7\\x93\\x90;\\xfc\\xb5.=\\xee\\xff\\xb9\\xbc\\xba\\x99\\x96;\\xb1\\n\\x03\\xbd\\xdf)\\x06\\xbd1\\xd7\\x8b\\xbd\\xab\\x9c\\x97<)1\\xd3<\\x9d\\xd7\\x9f\\xbca\\x99\\xae:\\x1a\\x93*\\xba\\x18E\\\"\\xbc<\\xd6\\xe6\\xbc\\x96\\xb7J=Z\\xa4\\x0b\\xbd\\xfa\\x85\\xff\\xbcgg\\x01=\\xf5!\\\"=\\x9a\\x13\\xbe\\xbcC\\x7f\\x89\\xbcM\\x93\\x0b:\\x93o\\x108\\x84t\\x18\\xbd\\xcf\\xa9\\xb4\\xba\\x8a`\\x9c\\xbd\\xe7JM\\xbdl\\xd9 =\\nE\\x19=\\xa3{_\\xb9\\xdcA\\xbc\\xbb\\x83:\\x85=\\xaeRb=7\\xaa\\xb6<b\\x91\\xb4\\xbc.\\x91\\x86<ko\\xa9<\\x01=\\x86<\\\\F\\xbd\\xbc\\x92\\xea\\x14\\xbdc\\xa4\\x9f\\xbc\\xd5\\xd5\\xfe\\xbbK\\xfc0\\xbc|\\x0e\\x14\\xbc\\xfe\\x108\\xbc>\\xabM\\xbdM\\xd6\\x83\\xbd\\x8f\\xb5/<,\\xb4\\x99<-\\xfb-<\\xd3\\xc8\\x0f\\xbd\\xb6\\xab\\xa0\\xbdRp\\x8d\\xbc\\x87~V\\xbc\\xebW\\x18\\xbc\\\\K\\\"\\xbc\\x99\\xe9!=\\x9cG\\x03\\xbc\\x16\\x11h\\xbc\\x9f_\\x8b\\xba\\\"B\\xd9\\xbb\\xd6\\xd12<\\x909*:\\xe0\\x89\\xfe<\\xeeI\\x1e\\xbd\\xd0\\x98\\xc2<Q@.;g\\xb9\\xf0\\xbb\\xa5\\xa8\\xcd\\xbc\\xe5\\xa9R<\\xbb\\x9a\\x85\\xbc(\\xd4\\x88\\xbdQ\\xee\\x97\\xbc&~\\x02>\\xbf\\xa1\\x81;\\xdea\\xf7<\\x90^\\xa0\\xbb\\\"Fk<[x\\x7f\\xbd\\x80\\n\\x8f=\\xd4\\xaa^=6\\xbc\\x02\\xbdY@\\x8e\\xbc\\xafe\\x86\\xbb#\\x00\\x03\\xbd\\x89e\\xc0\\xbc\\xc2\\xbfA<D\\xc3\\x0c=\\xed\\xac\\xd7\\xbb\\xc3md<qs#=\\xb6\\xd3\\xcf\\xbct\\x10\\xff<\\x9b\\xd9\\xd9;)G)=;;P\\xbds\\xc9\\xd2\\xbc?\\x18l\\xbd\\x99\\xe6o;I\\xe2\\xd5<u8d=\\xb7\\xbc\\x01\\xbd\\\"\\n\\x90<\\x0f\\x07W=\\xa6\\xb42\\xbd\\xd0\\xfd\\xf3\\xbb\\xf4\\x11l< |\\x88\\xba\\xe3\\xd9\\xc7\\xbc\\x05\\xabF\\xbd\\x82f#\\xbde\\xd4\\xe4\\xbc\\x935N:\\xd2k\\xea;y\\x81\\r<\\xa5\\x18\\x82<\\xe2\\xc72<\\x0e\\x9ej\\xbd\\xb3\\xc9\\x11\\xbbTO==\\x017\\xd0\\xbc&F\\xd8\\xbb\\xc2\\xf3\\xa6\\xbc\\xf5j\\xa8<\\xed\\xfb\\x06\\xbd\\x86R\\xd0\\xbb\\xdeh(\\xbdP\\x98\\xca\\xbc\\x1b\\\"\\x99=\\xa7\\x0eW\\xbc8\\x1f!\\xbaen\\xbc\\xbd\\xf8\\x10\\xa6<\\xf0@\\xb5\\xbb}\\xc4\\xe9\\xbc\\xa7\\x88\\xc8=G%\\x85<\\x029|\\xbc\\xeb^\\x8d<\\xb8\\x15H=DC\\x19<\\xe0s\\xfd\\xbcN\\x8f\\x1c\\xbc\\x1db0\\xbb^k\\xa2\\xbd\\xfb\\x91==\\xd3\\xc4\\x00<\\xfd\\x9b\\xcf\\xbb\\xb0\\xe5:<\\xfe\\x9e\\xb5<\\xcc*\\xb19\\x8d\\x91\\x1c=\\x84S\\xa4\\xbc\\x80q\\x8b\\xbd\\x1a\\x0f\\x1d=\\xbbh\\xd4;\\x94\\xf0\\xad<7WP\\xbd\\xa6\\xd04;\\x02o4\\xbdh\\x94\\x13\\xbd\\xd2?\\x0c\\xbc\\xd5fT\\xbd\\xd9\\xdd\\x8f9,(><\\xd4|Z\\xbd\\x858\\x12=m\\xb0\\x8f=/e+\\xbdR\\xac\\\\\\xbd\\x85^\\x0f\\xbe\\x16\\xa3\\x18\\xbd[\\xae\\xfb\\xbc\\xee2l\\xba\\xfd:\\x96\\xbb\\x15\\x95$<\\x07`\\xf7<2\\x18\\xd8\\xbcjh\\x14\\xb9@\\xb3`<W\\xac\\xca\\xbc\\xd1i\\xe8\\xbc\\xaa\\xc0*=1\\xbd\\xf4;\\xb3A\\xac\\xba\\xdb8\\x0e\\xbc\\xb4\\x97-=\\xd7\\xe2I\\xbd\\x02v\\x13=\\x1b^\\x1b\\xbc\\x86\\xca\\\"\\xbdX\\xc2\\xbc\\xba\\x9aq\\x07\\xbdC\\x1eN;d\\xde\\xf0<\\x8b\\x1c\\xbb<h\\xb87=\\x98\\x05\\x9d\\xbc\\x97\\x10\\xa6\\xbc\\xc11\\xb1\\xb9\\x9aB\\xa6\\xba\\x94_\\xef<j\\x85\\xd6:\\x9dp\\x06=\\x1a\\x81\\x1a==\\x86\\x1f;\\x01\\xfa\\x90\\xbbF^}\\xbc\\xd0\\r\\xa6<\\xfe\\xaa\\xbf;\\x17\\x1e\\xf2<M#:<!\\\"\\x98\\xbb\\x07s/=\\xa41\\\"=\\\"\\xadZ<\\xc1\\x85\\n\\xbd\\x19\\xf0\\xd4<nE\\xe0\\xbaE\\xca\\xd1\\xbdZ\\x85\\x8d\\xbbv\\xed\\x83=\\x1c\\x91\\xb4\\xbc-\\xe0+=\\xe5\\xf3\\x0b\\xbd\\x89\\x81a\\xbc\\xd5\\xef\\x1f\\xba\\xe4u\\x8d=5P]<;\\x7f\\xc8\\xbcSs\\x8a\\xba\\x0ca\\x94\\xbc:K\\x10\\xbd[\\x89\\x99=\\xde\\xb8\\x9e\\xbc\\xd2\\xf2!\\xbc`U[=D\\x92\\xe9\\xbc\\xfc\\x87\\x08<\\x81?-=\\rZj\\xbd6\\x1e\\xca;\\xe4\\xa8\\xfd\\xbcZ\\x8f\\x91:\\xef\\xa1\\xb4\\xbc\\xf1\\x84\\n\\xbd|;]=\\x1b`\\x81\\xbdz\\x95\\xcd<\\xd5\\xb4,=\\xaf\\x19\\x93\\xbd\\x12\\\\\\x02=R\\x1f1=P\\xb67<$L7<\\xc1\\xfd\\xd9<l\\x1c*\\xbd\\xe3\\nx\\xbc\\xa4\\xc4\\x8b<\\xc7;L\\xbc\\xd9\\xe5\\x8a=\\xcb\\xe9\\xfe<\\x06\\xe0\\xa1\\xbc9\\xb9\\xe1\\xbc;\\x87\\x13<\\xcd\\x12\\xd8<7\\x8b\\xae<\\xa6\\xf3\\x97\\xbc\\x0b?b=$\\xac\\xd0\\xbcM\\xe3\\xb0=\\x86*6\\xbbQZ\\xa1:\\xa1l\\x8a<\\x07$\\xc8\\xbb\\xe9#9\\xbaW^\\xab=\\xa7\\xea\\xbf\\xbb\\x82\\x05\\x11\\xbd\\xfcn\\x1f<\\xba\\xb1q\\xbc\\xd9(\\xe2<\\xd5\\xbdD<\\xc4\\xa8\\xcf\\xbb\\xa1Cm<\\x12\\x8f\\xb9=E\\xb8\\xec:\\x1e\\xaf\\x1a\\xbcx%@\\xbd\\xe7\\xa7\\x87\\xbcMQP=\\x83\\xe3O\\xbc!~\\xc7\\xbbv\\x8b\\\"\\xbd\\x9d\\xd9e\\xbcoX\\xdf\\xbd\\x0b7\\x97=\\x83\\x07\\xdf\\xbc\\xee\\xdf_=#\\xb8,\\xbd\\x8d\\xe8i\\xbd\\xa5\\xcf\\xb0;\\xb8W\\xa5;\\xaeD>\\xbceu\\x0b\\xbd`\\x97*=\\xc9\\xcf\\x8f:\\xeb\\xa9v\\xbb\\x83i\\xe9\\xbc\\xa0U[\\xbdrEo=\\x1c\\x81<\\xbc\\xecw\\x08\\xbc\\xad]\\xd2<\\x81\\xba\\x81<\\x8a<\\xc9\\xbc\\xce\\x80L=y\\xe5N\\xbd\\x8dq\\x1d<\\xda\\x85\\x9e<\\xab\\xd0\\xc4\\xbcQp\\xea;\\x83vp9\\x8f\\x1d\\x83\\xbb\\x91\\xe6C\\xbcm\\xab\\xb4\\xbb\\xd1h\\xba\\xbc\\xf4\\x1c\\x99\\xbd\\xce\\xc7^\\xbd#dI\\xbb:\\xc2\\xb9\\xbc\\xd5\\x94\\x98=\\x02\\xb8\\x17=\\xd2\\x07\\x93<RN\\x8f\\xbb\\x8c2\\xb3=P\\xac\\x90\\xbc\\xdfl{\\xbc(p\\xee<\\x82\\\\\\x8c<\\xc6#\\x11=\\xc4\\xcd\\x81\\xbd\\x9fJ\\xde;\\x131\\r\\xbc\\xb4l\\xd6\\xbc\\xed\\t\\x1e\\xbd\\xa5\\xe9\\x14\\xbd2\\xbc!;\\xca\\xfe\\x08\\xbd\\xeac\\x16\\xbdR#,\\xbdQ\\x0f\\x16\\xbcV\\xbdb\\xbdI\\x14H\\xbd\\xf5\\\"o<cvC=wp\\x7f=\\x11\\x8f\\xd8\\xbcJ\\xc5\\xe2\\xbc\\xcf\\x11x;\\x85\\t|=\\xdb\\xbc\\x8c\\xbc\\xe7\\\"N\\xba:\\xf1\\xf3;))\\xbf\\xbd\\xb6\\x12\\x10\\xbd\\x04\\x98\\x1f=.\\xf7l\\xbdu\\xc7\\xc6<K\\x14K\\xbc\\xdc\\xb6\\xe3<\\x1b\\xff\\xeb=\\xe3C,\\xbc`H\\x89\\xbdJ\\xd9L=\\x999\\xa2;\\xae\\xde\\x1a\\xbc\\x1biD\\xbc\\xa3\\\\\\x8d=\\x11\\x02e<4\\xc7\\xe5\\xbb\\x17\\x087;\\x8d\\xf6\\xd0<\\xaf\\x88\\xac<\\xac\\xa9\\x93\\xbd^ZH\\xbc\\\\QT=\\xf6\\xcc:<:@6<\\xd7\\x01\\x84<f\\xb0\\xdf\\xbb\\xab\\x9cD=\\x11\\x07\\x12\\xbc\\x06\\xf3E<*\\xda6\\xbd\\xc0\\xa6\\x83=\\x91\\x8b6\\xbd\\x9c\\x82\\xa5\\xbc\\xc4;\\xd4\\xbb@:\\xe6\\xbc\\xbc\\xa6\\x7f<\\x03o\\xdd\\xbc\\xcb)\\x17\\xbddH_=!\\xe3\\xb7<\\x12\\x9eP=3\\xb3\\xd3\\xbc\\x1e\\xbf\\x8a\\xbc\\xbd(\\xf2\\xbc\\xec\\xd6\\x9a<\\t\\x04\\xa9<\\xcf\\xc8\\xc4:\\xec\\xb1\\xbd<y\\x10\\x8c\\xbb\\xdb\\x9b{\\xbd\\x8f\\x97\\xa0\\xbc\\xe7&\\x0b=8N\\xc4<\\xd4L\\xb6\\xbc\\xb6\\x100\\xbd\\xcb\\x90b<\\xf4]\\xea\\xbc\\xc4\\xf8%\\xbc*\\xe7\\xa4\\xbb\\\\D\\xdd\\xbc};\\n=\\x92ak\\xbd\\xa0\\n\\x9c=\\xe1\\xe3\\x9a=\\xc6\\r\\xb7<;\\x82g=W>@\\xbd\\xb8\\x8b\\x01<\\xaf\\xdd\\xb8=\\xd4Dq\\xba\\xb3\\x83\\x1d\\xbdZwk=oc\\x93\\xbc\\xcc\\xf5\\xb9\\xbbt\\xcd\\xb3\\xbcF\\xe0\\xf5\\xbb6\\x9b\\xf5\\xbbx \\x7f\\xbc\\xc8\\x8e\\x99<\\xcc\\x04/\\xbd.<!\\xbd\\xe4\\xdbz\\xbc}\\xd3\\x13;\\n\\rd<\\xf7\\xf7\\xee\\xbc0\\x15\\x02\\xbd\\x81\\xcf,=OnF;\\x8e\\xed\\xbd\\xbd\\x02I\\xdd\\xbc\\xd3\\xe8\\xed\\xbc\\x9eek\\xbc5KX<Q\\xfe\\x07=\\x8fh\\x9c<\\xf8\\xf2P=\\x11\\x11\\x15\\xbc\\xbe\\x89u\\t\\x99\\xef\\xc9<F\\x9a\\xdd\\xbcH\\xe4\\x1c=\\xd3\\x89\\xe6<#\\xcf_<\\x05||<\\xe5\\xdb\\x08\\xbd\\xd7!\\x88<\\xab\\x82\\x92\\xb8\\x10\\x99\\x95\\xbca\\xf8\\x07=\\xdb\\xd4?=*\\x1at;\\xdd\\xc39=\\xf3\\xeb\\x81\\xbc\\xd6\\xb2\\x1f<w\\x17O\\xbd*\\xfd\\x01=IVe:\\xc8\\x94Q=\\xcd\\xe5s\\xbbnT\\xc2<\\x03e\\xba\\xbc\\x80\\x8cE\\xbd9\\xca\\xed\\xb8\\xa1z\\xb8\\xbb\\xfb\\xd6\\x07<\\xe9F\\xbc\\xbb\\xb2\\xc1G\\xbd\\xb8\\xe2\\xa7<\\xdf\\xe2\\xc6<\\x12\\xbc\\n\\xbd\\xa2Ox\\xbd\\xc6\\x08\\x97\\xbcC\\x8e\\xa7\\xbb\\xb9\\xef\\xf8\\xbb\\xaa\\x8d\\xfe<3\\xd2\\xe4<\\xc4{\\x90<\\x81\\xa5\\xc4<rQ\\x96=\\xd7\\xd49\\xbb\\x9fT\\x1c;\\xcan\\xf3<C\\xf2\\xa2=V\\xb2\\x11<\\xa9\\x90P=oo\\x89<\\xc4\\xa7\\x10\\xbc\\x95a\\x98\\xbc\\xae\\x8by\\xbd\\xcc\\x1f\\xf3\\xbc\\x85\\xc0\\xb7<p\\xe69=\\x83\\x93\\xa2\\xbc\\xady\\xf3<G<\\x18=@]#=s\\x12\\x92\\xbc\\xf7\\xfd<<\\xcc\\xea:<+9\\xb4<6\\x1e\\x94<\\xcb\\xb95=j}J\\xbb\\x86\\xcc\\x1a\\xbd/\\xbb\\xeb\\xbb\\xe81x<W\\x9e\\x9e=\\xee\\xc1\\x8c\\xbdxh&;\\xc5\\xebt;\\x10\\xcb\\x8f\\xbc\\x9d\\xf7\\x06=\\x1d!q<\\xb7\\xe1)=\\x88\\xa8|\\xbc\\x96dy=*\\xa41\\xbd;\\xa7\\x16\\xb9FB\\xa6:*\\xa6m<\\xa9\\xf6Y\\xbc\\x9c\\xcc\\x87=N\\xb2\\xb1\\xbb\\xd2i\\x07\\xbd\\xf4p5=t\\xd4f=\\x13`\\x8b<\\xf7\\xda9<zh`\\xbc`\\xeb\\x0c<mC\\x0f=B\\x8e\\xa5:#X\\xd8\\xba\\xfb\\xd3\\x0c=z\\x9a\\xe4\\xbc\\xbf\\x11$=\\xf7\\xac\\x1c:-\\x82\\x05=,\\xc2\\\"\\xbd\\x0b\\xc5\\xda\\xbc\\x89\\xdf2\\xbc\\xebM\\xc5\\xbc\\x81\\x12\\x08= \\xdca\\xbc03\\xaa<R\\xc9\\xab<\\x8fW$\\xbd\\xed\\xc7\\x08\\xbc7G\\x88<\\xf9\\xb1\\x8c\\xbc\\x92h\\xd6\\xbb4a\\x85=\\x99\\xcf\\xfd;1E\\xae\\xbc\\xac~\\xa7<O\\x10A<\\x10\\x93\\xe9\\xbc\\x92\\x1dz\\xbd\\xa6\\xac\\x06=\\xbff\\xcd<3-\\xe6<\\xe70l<\\xc8\\\\\\xf6<\\xff\\x98\\xd1<7\\xbd\\x96\\xbd\\x0e\\xf2N<\\xb6\\xd8\\xc1=\\xee`\\xc1\\xbd\\x82\\x7f\\xf6<R\\xde\\x17\\xbc\\xed\\x87\\x89=#; ;7\\xa6\\r=\\x12*\\xae<\\xfc\\x95e=\\xa7\\x83\\x90\\xbd\\x99b =5\\xd4E\\xbdzk\\xaf\\xbbs\\xd1\\x1a\\xbc>q`\\xbd]\\xa1>=7c\\xfa\\xbbT+\\x8e\\xbc;\\xe9\\x13\\xbcJ;(:\\x17\\xf1\\xd6<*\\xd2\\xb1<\\x9c\\xd2v\\xbc^\\xe7\\x87:I\\x97)\\xbd\\xd8q\\x83<\\xf6\\xac?=g.\\x14=\\tTA\\xbd\\x08\\x0b\\x8f\\xbc\\xcc\\xa4\\x13=d\\x1aM\\xbd9\\x04.\\xbd\\xddP\\x81=\\x8e\\xc1\\xe0:\\x95\\x16!\\xbd0\\xd9\\xe0\\xbc\\x83O$\\xbcdmW\\xbdm\\xc2f\\xbc\\xf4C\\xc7=\\xd8\\x1b\\xc5\\xbc\\xb4\\xc3\\x12=\\x08\\x8a\\x12\\xbd^\\x9c\\xa3<\\xec\\xbe\\xa9\\xbc<C\\xfa\\xbc\\xd28G=\\x95\\xcd2=l\\x84\\xb8<\\xbeK<\\xbb\\x8a*\\x8e\\xbc\\x0b)\\xfc;?G\\x0f\\xbd\\x9c\\x15]=>\\xc6E\\xbd@\\x05\\xdb<\\xb5\\x99U\\xbd\\xe2\\x89\\x85<\\xaeP\\xa7\\xbb\\xa1\\xfd>=\\xd9\\xce\\xb0\\xbc\\xcf\\x1b\\x06\\xbd\\xcc\\xbb0<>\\xe83\\xbd\\xcayj\\xbd\\x0e\\r\\x8a<\\x1d.\\x1d\\xbc\\xeed\\xe2\\xbb\\xc3J\\xe6\\xbb\\x1a\\xeaq\\xbc\\xc8>\\x1f\\xbd\\xd3N\\xc3\\xbcoM\\xca<\\xdd\\xfa7\\xbd\\x96\\xdd\\x03\\xbd\\xf3V\\xc0\\xbc\\xdaA\\xca\\xbd\\xc8\\xa3\\xeb<s\\xf9Y<\\xd0\\xb2X\\xbb\\x89\\xd0J\\xbdA\\x05\\x97<\\xb6\\x98\\x96\\xbbV>\\x84=\\xf21\\x10\\xbd\\xbf\\xce:\\xbd\\xe6\\xb4?\\xbd#\\xbft=\\xbdE\\x8a\\xbcmb\\x1b;\\xee\\xd76\\xbd(\\xdb\\xb8;g1\\xb1\\xbc(:s=\\xba&\\x9c=\\x04\\xbc\\x89=\\x05S\\xfc\\xbbd\\xe7\\xe2<u\\xe6i\\xbb\\xfa\\x13\\xa1\\xbc4\\\\\\xb8<J\\x14\\xa9;=C\\x11=x#\\x81\\xbaT\\xd4\\x8b\\xbd]\\x8dv\\xbd\\xf32O\\xbc\\xa5\\x81\\x19<\\x1e\\xc0\\xdf\\xbcH\\x11,<\\xe6j`\\xbb\\x05\\x16#=D\\x80\\x81<\\xcf\\xc8J=\\xe4}+\\xbd\\xbb\\x1f\\xa2\\xbc\\xf3\\xb1d\\xbd\\xc4^\\x1c\\xbda\\xaa\\x1d\\xbc\\xf2\\xa3\\x86\\xbdN\\xc31\\xbcJ\\xc4y<\\x87p\\xea\\xbc\\x88\\n\\x9a\\xbb\\xa2e?=\\xe2\\xed\\xff\\xbcH\\xad1</\\x1du\\xbd\\x83\\x8a\\x08\\xbdy\\xc3\\x85\\xbd\\x89U\\x9c\\xbc\\x84@\\x91;\\x8c/\\x1b\\xbc\\xe0.\\xe5;up\\xba<>&\\xc9=\\x02\\xcc;<\\x0e\\xe7i\\xbc>TD=\\xe59%\\xbd\\x98=C={\\x81\\x86\\xbc}\\xa7\\x86<o\\xa3=\\xbc\"\nHSET bikes:10022  model 'Telesto' brand 'Tots' price 4821 type 'Mountain bikes' material 'full-carbon' weight 14.2 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"kuL:=I\\x1e=.\\x9a8\\xbb\\r\\xe5\\x00=\\x7f \\xfd\\xbbZ\\x7fq=\\xca\\xd6!;\\xfe\\x9aI\\xbd\\xd5@\\xaa=\\xd3\\xe4\\x04=\\x9d\\xfe=\\xba\\x0b\\xcau\\xbc?\\x9e\\xe6<\\xbd\\xcf\\xfd;\\x93\\x1c\\x7f<@\\x01\\x18\\xbd\\xa3C\\xe7<\\r\\xf7\\xdc\\xba \\xe1\\xd1<\\x86\\xa4\\xae\\xbd\\xcfU\\xbb\\xbb\\x07\\x02\\xb1\\xbcA\\x95\\x05\\xbd\\xb0^\\x81\\xbd\\x96\\xde\\x0f\\xbd\\xe2\\xda0\\xbb\\xa2W\\x89<\\xa5D\\x82\\xbb\\x17Xg\\xbdy\\xb99=\\xd5\\x9a\\xe4\\xbb\\xd3\\xd2\\xd2<\\x88[\\x0c=N:^=\\x9d\\xab\\xa7\\xbc[\\xea\\xd7;L\\x03\\xae<\\x84\\xcf\\x0c\\xbd\\xd1\\xa1<\\xbc\\xebN\\x04=\\x98\\xabF\\xba\\xfa\\xce\\x0c\\xbdg\\x7f\\xa5<\\x13\\xddH<\\xb55\\xa7<\\xd6\\xc3\\xb6\\xbc+\\xf9\\x80=$\\x85V\\xbdI\\xe7\\x1a\\xbd\\xc8\\x8d\\x88<\\xa2\\x14k<KUy\\xbck\\xb2V;\\xd8\\xed\\xe4\\xbc\\xdbQ\\x80\\xbc\\xba\\x95\\\"\\xbd*Hz\\xbc\\xfe\\xa1\\x14\\xbd{\\xb7k\\xbdb\\xec\\x8f=C\\xb7\\x06=~\\xc3)=\\xc5s\\xc0\\xba\\xd1e^=\\xeb\\xc9\\x9f=;S\\xb8\\xbaXW/\\xbc\\xf8\\x0f\\xf2<\\x1a\\r\\xed:F\\x131\\xbc\\xa0\\xba\\x8e\\xbc\\x15\\xa2)\\xbc\\x83\\x1b\\x95\\xbc\\x12\\xbd\\x1c\\xbd\\xd6\\xfe\\xce\\xbc\\xfa\\xc9\\xcd\\xbc\\x00\\x00\\x86\\xbcf\\xd6\\x81\\xbd\\xda\\x8bU\\xbd\\x19\\xc6t<\\xed\\xf6\\xf9;\\x9ba\\xca\\xbc\\x86\\xc9*\\xbd\\xdfb0\\xbdn\\x1c\\xcc\\xbc9 \\x11\\xbd\\xa2\\xbd\\xfa;\\x9d\\x10\\xee<\\xf0\\xae\\xbc<py\\xdf\\xbb$\\x8fX;\\xa6h\\xef\\xbcK\\xfd\\xc2\\xbbo\\x89\\xb5<\\xa2\\\"\\x00<\\xa6}\\x1f=>\\xff\\x13\\xbd^\\xf0m<X\\x8a\\xbb\\xba\\x9e\\xbd:\\xbdu\\xc4\\x08\\xbd\\xafJ\\xc7\\xbb\\xa0\\xf7\\x9f\\xbc\\xf9\\xf7_\\xbd M\\xab<\\n\\xfeE=\\xe4\\x04\\xb3<sJ\\xd7<\\xfe\\n\\xe4\\xbcb\\xc4+;s\\x18\\xec\\xbcrm]=M\\xe1\\x03=U2\\xc0\\xbc:2)\\xbd\\x91\\xaa\\xa0<\\x88EU;\\x1c\\x91\\x13\\xbd7~}<\\x9a\\xeeA=_r\\x85<\\xe0\\x9c\\xad<X\\x0f=\\xbd\\x83~\\x85;\\x80\\x85e\\xbc\\xc4z\\x19\\xbc\\x0c\\n\\xe4<\\x00\\xf7\\x7f\\xbd\\xa6\\x06\\x10\\xbdK\\x96\\x1c\\xbc\\xbc\\x02\\x0e\\xbcQ\\xb9\\xc4\\xbbZ\\xa7\\x10=\\xae\\xd7\\x1a\\xba\\x92}\\xfe\\xbcN\\xee)=\\xf3\\x82\\r\\xbdvR\\xf7;\\x81w^\\xbc\\x80\\x1a\\xcf\\xbc\\xba\\xdc\\xdc\\xbcs\\x0bg\\xbdR\\x18\\x11\\xbd\\xda\\xb5\\x82=\\xfe\\x8e]=\\xdfT2=(!.\\xbd\\xa94)\\xbc\\xde\\xe8C=\\x80\\xa0d\\xbd#\\x04\\xa1\\xbc{\\x0e\\x85=\\x80PH\\xbd\\xa8T\\xb6\\xbb,\\x1dM\\xbdjrD\\xbc\\x11\\xce\\xe7\\xbc\\x9c\\x0f\\xa0\\xbcZ7\\xe8<\\xb1>n\\xbcsbx=\\xb76\\xb7\\xbc\\x06{>\\xbdIX\\xa5\\xbd8f\\x8c\\xbb\\xd9\\xc9\\xac<Ne\\x8e\\xbcH*\\x17=\\x1b,\\xd0<\\xd6\\xc0\\xef\\xbb\\x1b\\xa5W\\xbb\\xd1L\\xe3<7\\x02_<\\xc3\\xad\\x07\\xbdmbU8S\\xa3[\\xbc\\x8f\\xb5@\\xbd\\x83\\x8f\\xa0=\\xaaC\\x8a<)\\xfd\\x81<\\x05\\n\\xb3\\xbc\\xfc\\xb0$<\\xc8:\\x13=\\x98\\x88\\\"=\\xb9\\xd0\\xa4<\\xf9&\\xe9\\xbc\\xe2\\xcd\\x10=\\xbc\\xaf8<\\xfa\\xab\\xf1<\\x94\\x1cz\\xbd,\\x1c\\x05<\\x9c7\\x0c;\\xb0h\\xf2\\xbc\\xedm\\xd8<W\\xcc\\xe0\\xbb\\x87\\x13h\\xbc`\\xff/\\xbd\\xc8\\x1a\\xb8\\xbcDB\\x07\\xbdR\\xba\\x1c=\\xf5\\x82\\x88\\xbcz4;\\xbd\\x16\\xd7\\xc7\\xbd>\\x1e>\\xbd\\x95\\xb3\\xa2\\xbc\\xe3\\x84\\x97\\xbb,\\x13F\\xbc\\x83#\\x01\\xbdx\\xbb\\x19=\\x8cA$\\xbd\\xe9\\xfb5\\xbc\\xf4\\xd38=(!\\xdc\\xbc\\x08\\xc00\\xbd3\\xff3=\\x89\\xb4\\x9c\\xbb\\x19\\xee\\xc6\\xbc\\xa9\\xab\\xb0\\xbbI\\xea\\x88<\\x8a\\\\\\xa5\\xbd\\xb6\\xa2\\xd7;k\\x063\\xbc4\\xac\\x0c\\xbd`\\x9cl<K\\xc7o\\xbcEK\\xad\\xbc\\xb5L\\xfc<\\xb4\\x0eG<gN\\x0c=\\x9d\\t\\xd1\\xbc>\\xe6\\x0f\\xbd0C\\x9c<\\xba\\xc3\\x04\\xbd\\x14\\xd0\\t=Mxh\\xbc\\xee\\x14\\xd6\\xbb\\xbds\\r\\xbb\\xd0/U<\\xbe\\t\\xc3;p\\x1eO\\xbb\\xaf\\x95\\xbf\\xbbw\\xf6O\\xbb\\xa2L\\x06=\\xd4w\\x82\\xbb6\\x19\\xa3<l\\xf2\\xf9<>9\\xdc<=\\x8b\\x00\\xbc`\\x05\\xaf\\xbd\\x120\\xdf<\\x00Z\\xaa\\xbae\\xc1\\xa7\\xbd\\x9d\\x98\\xa3\\xbc\\xac\\x96\\x96=\\x8d\\x16!\\xbd\\xbc\\x14z=\\xe2/\\xb3\\xbc.c\\x84<G~\\x88\\xbd\\xe9u\\x90=\\x154\\x9f<\\xdd\\xab(<\\xf8\\xd7\\x14<\\xedd\\xdc\\xbb\\xd4\\xfd(\\xbd)\\xaa\\x9b=\\xf7!|<9/\\xbd\\xbc\\xe61~;\\\\h,<+v\\xa7\\xbc\\x93\\x1f\\x00\\xbb\\xe2|0\\xbd\\x16u\\x9c\\xbc\\x95\\x16\\xbf\\xbc1\\xd9\\x94<>\\xbbN<\\xcce\\xe4\\xbb\\xcb\\x0b\\xa1<\\xfd\\x11\\xc5\\xbd\\xdc\\xd8\\xa8<\\x00\\x95S<!O\\xd2\\xbb\\xb0\\\"\\x0e=\\xd8\\x1a\\xe4<\\x06\\t\\x0b<xrb\\xbc\\x91M\\x12=}\\xd1\\x82\\xbd\\x94c\\x16=\\xfe\\x17\\xbe<\\xe4\\xd4\\x95\\xbc~\\x81\\x85=\\xaa\\xf0\\xc9\\xbb\\xb4y\\x9b\\xbc\\xff\\xec#\\xbd\\xa2UQ<|\\xfcD=g>0:\\xcf\\xa5\\x01\\xbd\\xa2_\\x80<\\xda\\xed\\x8b<\\rS\\xc7=I\\x96\\xe0;+%%=\\xf6!Q=$\\xda\\x13\\xb9b\\x18\\xa3<\\n\\xb5\\xaf=i\\x9e\\x94<E\\xc4\\xac\\xbcZ\\x9bK\\xbd2\\xcd$\\xbcE\\x1a\\xd1;\\xd4\\xe4\\x1f=hK\\xf1\\xbb7\\xad\\xbf;<\\t\\xaf=p\\x80\\x12=\\x92\\xa4\\xe7\\xbcB^.\\xbd\\xd5F\\xaf\\xbcQ(\\x0c=\\xf0\\x12\\x9d\\xb8\\x14\\xf3;<+m1\\xbda\\xf2\\x83=x\\xc7\\xba\\xbd\\x9bR\\x8f<\\x92\\x17\\xd6\\xbc\\xc4\\xcd\\x91\\xbc\\xec\\xc8\\x17\\xbd\\x8b\\x1cH\\xbc/IH=\\xbdf\\xd2\\xb8\\\"I\\x16\\xbc\\x91I2\\xbd\\x96:L=z\\x0c0<\\xd1\\xe8\\x1e\\xbb:\\xc3&\\xbd<\\x8d^\\xbd\\x8f\\x82\\x1a<A\\x9d\\x1e<\\xd9\\x10\\xbb\\xb9\\xc4\\xd5\\\\=\\xe7\\x93m=\\x99\\xb1\\x82\\xbc\\xd6\\xc7E=S\\xa1\\xa3<\\xe3\\xcc\\x17;XCW\\xbb\\xe8C\\xea<\\x17\\x82\\xa4:jw/\\xbdoPj=0s\\x91\\xbb\\x96\\xdf \\xbd\\xe9D\\xd8\\xbc\\xe6\\xd1\\xa9\\xbdi\\xd6\\x8e<8=\\xc3<\\x12\\xaf\\x16\\xbc2\\xfbO=s\\x93-=\\x8a\\xed\\x9d:\\rX\\x0f\\xbd\\x0e\\xa9V=k\\xb4\\xe9\\xbcvLN\\xba\\xc8&\\x9f=\\x98\\xd8\\x07<\\xd5\\xfc\\xae; \\xca\\x03\\xbd\\x1b\\x9d\\xc8\\xbc\\xb4\\x0b\\xc1<~\\xe6\\xc8\\xbc\\xf7)\\x8e\\xbd\\xd3\\xc9\\xcf\\xbcK\\xd8/\\xbb2\\xc9L\\xbd`\\xd1^\\xbd,/\\x84\\xbd\\x82bb\\xba\\xfbC6\\xbdf\\xf3\\x8b\\xbc\\xa78\\x1d<\\xe1\\xa3m=\\x92\\xf5\\x19=\\xed\\xbd\\x82\\xbc\\x10\\xc4M\\xbd\\xa3\\x1a\\\\=\\xec\\xd4\\xa2=\\xcc\\xd3\\xe7\\xbcp`\\x98<\\xc0t\\xa9<\\xb5\\xae\\x03\\xbdpz(\\xbd\\t\\xb2D={\\xa3\\x8a\\xbd\\xad\\xc4\\xcb<\\x12I\\x04\\xbda\\xbf\\x0e<f\\xf9\\xc7=-I\\xad;\\x15\\xd7@\\xbd1\\xe3*=T\\x15\\xe0\\xba\\x10\\xf3\\x7f\\xbc\\xf9A/;4\\xfad=V\\xfd\\xf8<c8\\xd8\\xbc)]\\xa1;\\xc3=\\x96;\\xdaKs<n\\xe2\\xca\\xbc0Q\\x80\\xbd\\xee:\\xa0<\\xbds\\xae<Q\\x17\\x0b=\\x00\\xaf\\xb5\\xbc\\x94\\x01\\x15<s\\x0e:=-I\\xa4\\xbb,\\x81^\\xbc\\x01Ls\\xbd%bz=\\xa1\\xf1u\\xbc_\\x9dD\\xbd\\n0\\x87\\xbcCD%\\xbbUG\\xbd\\xbc\\x15\\x80}\\xbd\\xe7j\\xa4\\xbc1Ay=\\xe6\\xf9\\xc6<\\x05W\\xa3<g\\xcf\\t\\xbd>6\\x80<i\\x1bo\\xbd\\x08\\x19\\r\\xbc\\xec\\x9d\\xa0<)\\x8f/\\xbd\\xbf\\x9a\\xcb< *t\\xbaH\\xa5\\xae;\\\"k\\\"\\xbc\\xed\\x1f\\x08\\xbd\\x82w;=YF\\xcf\\xbc\\xf2`%\\xbd\\xbb\\xad\\x98;9\\x8e\\xb1\\xbd\\xb0\\x84\\x8a;)\\xb7\\x06\\xbcK\\xae\\xb0\\xbc\\x15\\xc3\\x87<:\\xad\\xdd\\xbc\\x9c\\xd5i=\\xd2!\\xa3=\\xd5}9<\\xc5\\x98\\x82=\\xdf2Z\\xbd\\xa3s\\x82:\\x8f\\x12\\x12=\\xcf\\x87\\x9c\\xba\\xe2\\xf3)\\xbd\\xdeAb=_Z\\xe4<\\x1a\\x8c\\x18\\xbd%\\xd1\\x9d\\xbcw\\x98\\xa5;\\x0f\\x8dG<\\\"\\x05\\xde<\\xa1K7=\\xb7T_\\xbdy\\xc1\\xc8\\xbc+~\\\\\\xbc\\xf2T%:\\xda\\xad\\x97\\xbb\\x1e\\t\\x03\\xbc\\x88\\xc9\\x95\\xbc\\x80\\x14\\xbe<\\xcf\\xa5\\xf0\\xbc\\x01\\x04\\xac\\xbd/\\x82\\x86\\xbc\\x86\\xa4\\xd6\\xbceR\\xed;}\\xab\\x0c=*\\xadh\\xbddR\\xda<\\xc3\\x8a-=Q\\xe2S\\xbc@p\\x83\\t\\xdeW\\xb8\\xbb\\n\\xecZ\\xbc\\xc9\\x12[=\\xfbr\\x07=\\xd4\\xa1\\xbe<1\\xaf@=\\xa8\\x02\\x87\\xbc\\x83\\xa9\\x90<\\xf3\\xdc =\\x03S\\x8f\\xbc\\x87\\xf8\\xdf;<\\x9c\\x92=\\xa0W\\x89;\\x0e9K=W\\xb3\\x1d\\xbc\\x1c\\xac\\x97:\\xde`\\x9c\\xbd\\x986\\xa1\\xbcB\\xc8\\x18<k\\x97u=\\x18?\\xd3<\\xcbs\\xde<\\x9f7\\xe8\\xbc\\xe8\\xd3\\x8b\\xbc\\x89\\xa4&<\\xf5\\x8aI\\xbar\\x82\\xf2;\\xbb\\xb9I<\\x92\\x1d!\\xbd\\xfd\\xa8\\\"<\\x8e\\xad\\x8a<\\xee\\xb2\\x86\\xbc]*X\\xbd(\\xc2\\xbb\\xbd\\xfa\\xbc\\x0b\\xbc\\x83\\xb5\\xc2;b\\x1d\\\\<\\xeb\\xab\\xce<\\xfd$\\x00=\\x96\\x880;\\xd6yh=\\x1c\\x8c\\xb2<\\xb9\\x94+=\\r\\xe3\\x1a=\\x967)=\\xb6\\xb6\\x87\\xbb\\xef\\xe8\\x01>\\xa5\\xf9\\x86<#\\xcf\\xec\\xbc\\xb37\\xf2\\xbcE\\xdf/\\xbd@\\xe9\\x85\\xbd\\xa2^\\x8a<\\xd8a\\xe9<\\x07~6\\xbb\\xb9\\xbd*=.P\\x0c=\\xbeU\\r=\\x7f,\\xb8<\\xa1\\x97\\xac\\xbc\\xd5\\x90?<]\\x94\\xbd\\xbc\\xc4\\x97=\\xbc\\xe7\\xb82=Uk\\xd2\\xbc\\x15\\x1aM\\xbdV}v;\\xf7\\xc2\\xf7;9\\x80\\xe0<UbK\\xbb(a\\xe0;]B\\xd4\\xbb\\xa4\\xb3\\x9c;\\xa9\\x9e\\x06=\\x8d\\xd4\\xfe\\xbb\\xd7\\\"L\\xbc]\\xc8\\xfe\\xbcm`\\xab=6cb\\xbd+sW<\\xf1.\\x1c\\xbdp\\xfa+\\xbc\\x0b\\x9a\\x03\\xbd\\x91\\x8ce=\\xa1\\x8c\\xf2\\xbc\\x8b\\x12;\\xbd\\xf7R_=\\xd4\\xa1\\xc6<\\x99\\xef]\\xbc+\\xb0\\x92<c6z:\\xda\\x1f\\x04=<\\x1d\\x8e=o\\x07\\xff:\\xc1\\xa9)<\\xba\\x84h=\\x97\\x1aI\\xbd\\r\\xf9\\xf9<_\\x062\\xba\\x08V\\x12=\\xbe\\xebN\\xbd\\x18qy\\xbcW\\xd0\\x90:\\xf6\\xa7\\xc0\\xbc\\\"\\xcb\\x9e<\\x01\\xfd*\\xbd\\xe7!\\xc1\\xbb\\xb4\\xb9\\x80;|\\x0f$\\xbdn\\xe1\\x0b\\xbd\\xf0\\xb4\\\"=\\xd0\\xd2\\x1a\\xbd\\x8e\\xcal\\xbd+\\xbd)=\\x1b\\xb8\\x93=\\xe4\\xcac;\\x1c\\xd4\\xc8\\xbbZ\\xcd\\x94\\xbc\\xb1Z\\xf9\\xbcJ\\xa0\\x8c\\xbd\\xd8\\xbd\\x01<\\xf5\\x86\\x8d< K\\x17<\\xac\\xa2^\\xbb#\\x1a$=\\x98i\\x0f=o\\x9d\\xd4\\xbc\\x84\\xd9\\x88=\\xac|k=J\\x8dE\\xbd\\xd1\\x19^\\xbb\\xc5\\x1bh\\xbdMb\\x93=\\xf6\\xe6 <\\xafQ\\x88=#\\xae\\x87;\\xe7\\xa6g=\\x19x\\x88\\xbd\\x1dq\\x87<\\xcc\\xa5\\x95\\xbb\\xf1\\xa9\\xa6;\\x1f\\x00\\x05\\xbd\\xfe\\xd8\\xd8\\xbc\\x97[T\\xbcP\\xc91\\xbb\\xb3\\x06\\xfd\\xbc#\\x969<\\x9b\\xe1\\x8a\\xbc\\xb4\\xe7o=\\xb3u\\xd2<\\x19\\x0c(\\xbd\\xf7\\x96\\x82<\\xd6\\x16R\\xbdr\\x8a\\x19=\\xe4\\xc4\\x18=F\\xb8\\x88<\\xf0\\x0e\\x83\\xbd\\x8dp\\xe4:\\x0fh\\xce<2\\x12r<\\xb1s\\xe5\\xbb\\xd6\\xd2\\x8e=\\xfc\\xfdx<6\\x10\\xed\\xbc\\xd3m\\t\\xbd\\xe1\\x86\\x81\\xbc\\x95GN\\xbdJ\\x9b(;\\x1d\\xfe\\x8f=\\xaa6\\x10\\xbd\\x10Z\\xbb<[.>\\xbb\\x03Q\\x0f=\\x19B\\xe3\\xbc\\xce\\xfe\\xdf\\xbc\\xc9\\xe0d=\\xffD<=@\\t3\\xbc\\xe4~~<h\\xceg<\\x89nH\\xbd\\x01\\xa1\\x1e\\xbd3<\\\"=Z\\xc2\\xe7\\xbb\\x9d[\\x18\\xbc\\xd0\\x90\\x10\\xbd\\xd9+\\xc1\\xbc\\xb6\\x88n\\xbd\\xdf\\xc4\\x86<C\\xd2!\\xbcZ\\x81?\\xbb\\xec#\\x0b=\\x17\\xec\\x8a\\xbdxj\\x18\\xbc\\xa5\\xfeR\\xbd\\xe2\\x12\\x81\\xbd\\xb6\\xd0\\xee\\xbbX\\xae\\xf1\\xbc\\x8c\\x8dM\\xbdzE\\xc3\\xbc\\x92\\xae^\\xbc\\xc3\\x086\\xbd_u\\x0f\\xbd]\\x02\\x17\\xbdh8\\x1c\\xbd\\x01%E\\xbd&\\x15\\x07\\xbc#\\x15e=\\x9a\\xe6\\xd8\\xbc`kD\\xbdJ\\xdf\\xec;mXu\\xbb8\\xf5\\xf8<\\xb3M\\xe3\\xbc\\x1e\\x80\\xd8\\xbc\\x1aE\\\";\\xe9b5=L#\\xa3\\xbc\\xb8W/\\xbd\\xd7\\t|\\xbd\\x7f\\x08\\xf0;\\x16\\x17\\x10\\xbd\\\"-B=\\xa3\\xbds=K/\\xa4=\\x9a\\x9ea:8LG=\\x1a\\xd37\\xbd\\xa6\\x81]\\xbd\\xf2\\xf4\\x9d<Z\\\"\\\";\\xec\\xb96=\\xb9\\xae\\x1d</\\xb3\\x12\\xbc\\x90\\\"3\\xbcF\\x1f0\\xbd\\xebA5<m;\\x94<6s\\xac;\\x9a\\xf8\\xb1;{\\x0c1=\\xe6\\xb4\\x03=\\xe7\\xad*=}N\\t\\xbd\\xfe\\xd5\\xab\\xbc\\x93d\\x8a\\xbd\\xed\\x10\\xc2;Y\\x87\\xaa;\\x82=\\xfd\\xbc\\x7f\\xdd\\x0c\\xbc\\xf9<\\xed<\\x04[\\xf4\\xbb7D*\\xbcd\\xcf\\x92<\\xcaU\\xe5\\xbc\\xfc\\x04I=\\xd5\\x95\\n8\\xc2}y\\xbd\\xce\\xf5\\t\\xbd\\xe8\\xa6\\x05\\xbd\\xbd#\\x14<\\xac/j\\xbc@`\\x94<)v =\\x04H\\xf8=QsB\\xb9\\\\\\\"\\xb5<\\\"\\xcb\\xd2=t\\r\\x04\\xbc\\x95\\xe5\\xa2=#pn\\xbb\\xd1\\xd2\\x19=7l\\xbc\\xbb\"\nHSET bikes:10023  model 'Ganymede' brand 'Pedal pals' price 3011 type 'Mountain bikes' material 'alloy' weight 10.0 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"\\tL#\\xbc\\x8c\\xa5\\xc3\\xbc\\xe0r\\xaf\\xbcp$3<\\x91\\xf2\\xba\\xbc\\xea\\xab\\xaf<J+\\x06\\xbd \\x14\\x16\\xbbK\\x8f\\xb2=\\x9b\\x92\\x01<U\\x1a\\xaf<\\xe3r\\xfd<\\x16\\xd6K=S\\xaf\\xb0<@\\xfb\\x90=\\x94\\x85}\\xbd\\x86\\x9ae=\\xb4\\xd9\\x82\\xbd\\xaba\\x98\\xbc\\x9bCt\\xbddv\\xe6;]\\x1cX\\xbcS\\x00(=\\x1dn\\x9b\\xbd@0{\\xbd=\\x15j\\xba\\xee}\\xd1:rY\\xdd\\xbc\\xaaf\\x1f\\xbdN\\x9c\\x8a=\\x9a\\xfba<\\x86\\xf7D\\xbd\\xe003<f\\xe3f;\\xe8\\x93^\\xbc\\x9d\\x1a\\x8f\\xbc\\xbe\\xe0\\xf0<\\x9e\\xd9\\xd8\\xbb\\x88\\xa4\\x9d\\xbc\\xa5\\x16\\x8e<B\\xabA=\\x17o^<V\\xf1k<\\x1f8Z<\\xf3\\xe4\\x8f\\xbcp\\xcf;\\xbc\\xc0\\xe7(=\\xe8b\\x9f<2\\xcb\\xc9\\xbc\\xa9\\x8cv\\xbco#\\x159\\xd8\\xbc\\r\\xbdV\\xeb\\x1b\\xbd Y\\xa4\\xbb\\xc4\\t\\x86<\\x92B&<\\xf6c&\\xbc\\x14P\\xa3\\xbd\\xcb\\x1a_\\xbd\\x8e=\\xe4=\\\"\\xfb\\xc8<\\xdf\\x88\\x82=U\\xe4\\x83\\xba\\xfcvP=\\xc3F\\x01=\\xaa5\\xae\\xb9\\xc2|(=\\x93\\xb2\\xf2\\xbb\\xbe\\x95\\xdf<\\x89As\\xbd\\xc6\\xa7\\r\\xbd\\xef\\xbbg\\xb7&i\\n\\xbd8\\xe9\\x88\\xbd;\\xef\\x19\\xbd\\x06Q,\\xbc\\xa8\\xbb\\xcf\\xbb\\x1a\\xdb\\xe7\\xbcTQJ\\xbdDGC=\\xa7\\x18@<\\x0fY\\xa8\\xbd\\xedw\\xac\\xbc\\xd0~+\\xbd\\xa7Z\\xe8\\xbbB\\xaf\\x01\\xbd(\\x0fM;0\\xb8><pS\\x1c=p0\\t<\\xb9\\xd3\\r\\xbcS,\\xa8;\\x0c<\\xca;\\x9f\\\"\\x9c;&~\\xfd<\\xa0o\\xd3<K\\xd9c\\xbd\\x1b\\x80\\xcd9!I\\x1f=\\n\\n\\x83\\xbdI\\x1e\\x03\\xbdgc\\x84<\\xdem\\xb2;\\x8e2d\\xbd\\x80\\x02\\x0f;\\xbbP\\x0b>w\\x96\\x05;\\x01\\x88\\xae<(F\\xbb\\xbc\\x06qr;ii\\xdc:6\\x7f/=\\x19\\x9b@<\\xd5M\\x9f\\xbc\\x1c\\x9c\\x90\\xbd*\\x122\\xbcK\\xc8\\xb9<#\\x0e\\xaa;\\xc8TF=\\xd1qH<=I\\x04=\\xd7\\xab\\xa2<7N\\xb3\\xbd\\xe2-9<d\\xe5W<\\x07\\xe0\\x11\\xbbH\\x92\\x07= O,\\xbd\\x9dj\\x1c\\xbd\\xa8|H\\xbd\\xf0\\xdc/\\xbdXH&\\xbd\\xb7\\x8e\\xb7<\\xdd\\xe7\\xf6:\\x89\\xab\\xb4\\xbc\\xa4VE=\\xd7\\r\\xf4\\xbb\\x82d\\xec\\xbb\\xf5\\xb6\\\"=\\xa0\\xa1\\xdb\\xbc\\xf6G\\xa5\\xbc\\x14\\xa7\\x91\\xbd\\x8c\\xae\\x9c\\xbceZ^=\\xce\\xa8\\xbd\\xbc=\\xa5-<\\x9cj\\xd2\\xba\\xb8!=\\xbcB\\xbd\\xca\\xbbz\\x1e\\xbc8:\\x04\\x0f\\xbd\\xc1\\xd4x<\\xbfq]\\xbc\\x057\\x8c;\\x06`\\xf7\\xbc\\\\\\xd4\\xbd\\xbc\\x17\\xb4<\\xbb\\xf9v-<\\xa6\\x10;=@d?\\xbd\\xfeR#=\\x83\\xa8\\x80;#\\x12\\x88\\xbb\\xaf\\xce\\x17\\xbd\\xd1F\\xdb<%\\xc8\\x07=\\xc5\\x7f<;T\\x9f\\x8d<Ve\\\"=\\xae\\x8bP\\xbcR\\xde\\x9c\\xbcM7\\xc3<\\x04j\\xa9\\xbc\\xd03?\\xbd\\xb6\\xca\\x14\\xbd\\xaf\\x80\\x07=\\x87fc\\xbd@\\x03\\x99=\\xf1\\x03\\x8b\\xbc\\xce\\x81\\xd9\\xbc\\x0b\\x0bn<u\\x14H\\xba\\x1d\\xcf\\x87<\\xc1\\xbbw<\\x8a\\xb8*=0\\xd4\\xe1\\xbc\\x87P\\xa8<\\xd5\\x95\\x8c<\\xce\\xca\\x17=\\x7fU\\x17\\xbdy\\t\\xc1;\\xa8\\xf2L=i\\xf1\\x8e\\xbd2t\\xf8<\\x1eyn\\xbb\\xd8<\\xc7<=!\\x97\\xbc@\\xdc!\\xbcJ\\x0b\\x00=r\\xd2\\xe4<\\x90\\xe0\\xee\\xbc\\x1d\\xb5x\\xbd\\xba\\x04\\x98\\xbd\\x86<\\x1c\\xbd\\x19\\xef\\xb8\\xbckB\\x00=\\x16\\x14B\\xbd)\\x17\\x97\\xbc\\x97\\x96)=\\xb0S\\x00\\xbdx\\x9d\\x18\\xbc\\x93\\x9c\\x11<d\\xe3\\x18=\\xf3(\\xde<\\x92\\x81\\x1e;\\xc1\\xd7@\\xb93\\xd5\\xc4\\xbd\\xbe\\x1a\\x9d\\xbc\\xc5u\\xc0<\\x8a9X\\xbd_W\\\"<\\xe0\\x12\\xb1\\xbc\\x96\\xbb$\\xbd4X\\xa0<\\xf9\\xf1@\\xbc\\xeeR\\x8e\\xbc\\x14\\xa9c<\\x8c\\x8c\\xf1;V\\xdf\\xd5<\\xe1\\x1a\\xce\\xbcy\\x00\\x11\\xbd;\\xd7\\xa9\\xbc\\x94HK=-\\xfa{<mRI<y\\\")\\xbd\\x19\\xcf\\x84\\xbbw\\xa6D=\\xe4<\\\\\\xbc0\\xb8\\xd2\\xbc-\\xe4\\x06=G\\xf8\\xab<\\x9c\\xb4f=\\x97\\x19\\xbd<\\xa6\\xb4\\xfc<\\xb1$\\xec<\\xaf\\xc6^=\\xa7Y\\x84<\\x8e\\xf3\\x17\\xbd\\x82\\xf6`<\\x0e\\x95\\x91<\\xb0{\\x18\\xbd037=0\\xca\\xd0<\\x88\\x1a\\xe8\\xb9\\x85\\x14\\x8e=\\xa0\\x03\\x1a\\xbdN\\xbc\\xc0<\\x05\\xbf\\x1b<7\\xd8%=\\xd8R\\x8b\\xbcGSC<e\\tL\\xbd\\x84r\\x0b<\\x7fSD\\xbdb\\xce\\x89=\\xa87\\x00=q\\xe3\\xe7;\\xc6\\xd4\\xbd\\xbb\\x97\\x9b\\x16\\xbbtw\\x1a;\\r\\x00\\xfb\\xbb\\x12\\xb9;\\xbd\\xb8\\xc2 <\\xe0_\\x98\\xbbr\\xd5\\xbe\\xbc+;\\xfb<\\xda\\xde%\\xbb]+<=\\x05\\x99\\x87\\xbd\\xd7\\xb2\\x0b\\xbc\\xe56\\x1b<\\xe1\\x94\\xf6<\\xd0\\xcb\\xfd<\\x9f\\x94)<\\xf72\\x10\\xbd\\\"\\x0b\\xfc\\xbc\\xcd\\x8e\\xb9=\\xeb\\xc8I\\xbd\\x95\\x97?<\\xe5Z&<\\xf3$m\\xbbli\\xb0=mi\\x9c;\\x98\\xed\\xd7\\xba\\xd8\\xbfh\\xbc\\x9fq\\xcc<\\xe7\\xc7\\x92<QS\\xa1;\\xf7\\xe3I;\\xe4\\xec\\x00=\\xf7\\xd0\\xb4\\xbb\\xddH;=\\xa6\\xe1\\x9d\\xb8\\x1a1K=\\x9c&\\x12;hF\\xa8:\\xfc\\x06Y<\\xf4\\x7fG=\\xb9\\xa5\\x81<\\xaa\\xb3\\xf1\\xbccl7\\xbdl\\xd4=\\xbd\\x9d\\x19\\x17\\xbd\\xc7\\xae\\x9a<\\xfc\\xe70\\xbd\\xb0\\xa5\\xe1\\xbc\\xd9\\x9f\\x8e=\\xe1~\\xd9<\\x9eX\\xd1\\xbc\\xdd\\x1d\\xdb\\xbcF\\xb2J\\xbc\\xb8N\\xe2<\\x89\\xf5A:\\xa8\\xf2\\x0c=\\\"\\x11`\\xbd\\x13\\x8b\\xc2;:\\x18\\x96\\xbd\\x11\\xc2\\x1f=\\xcc\\x0b\\xca<\\x81yB\\xbb\\xcf\\xe7\\x86\\xbd\\x19;\\xf4\\xbc\\xd1\\x146<\\x1e\\x0e\\x90<V\\xe40\\xbd\\xb7\\x04J\\xbdb\\x1f\\x1c=5\\x8eL=o\\xc3\\x16<\\xbf\\xdb]\\xbdY\\xd7\\xc0\\xbc\\xa4\\xed\\xb6=\\x9a\\xc5\\x03:g\\x1e\\t\\xbb\\xa5\\xca\\xfd<\\xd3m\\x96=\\xce\\xe7\\xe2<}\\xfc\\x97=\\xef\\x00\\x9c<M\\x11D=g\\xc9\\xa9\\xbc\\xad\\x88\\x92=\\xb1\\x9dT\\xbc\\xec!\\x9b\\xbdH\\xdc\\x9f\\xbczM\\xd5\\xbc\\x05\\xf5\\xb6\\xbcg\\xa3\\xab\\xbc\\xe8\\xad\\xf1\\xbd\\xa2V>\\xbb<8\\x9e<\\x04\\xa1\\\\=0k\\xe8<\\xcc\\xdd\\xcb<@\\x08\\x9a\\xba\\xb0fB\\xbd\\xa2gf;\\xd4:j\\xbd\\xe8\\x0f\\xb1\\xbc\\x0b\\x8e.=\\xe1\\x91\\x91;E+0<\\xcb\\xf8E<q\\x14\\xcd<\\x02\\x82\\x87<\\x08jq;\\xae\\x80B\\xbd\\xab\\xab\\xca<\\xe6\\xa8\\x16\\xbd\\xbc\\x16\\\"\\xbdw*\\xa5\\xbb\\xb1\\xa8\\xe3\\xbc\\xe3\\xcc\\xa5\\xb7\\xefO\\xaa\\xbd\\xcd|m=\\xe1\\xfc\\xef\\xbc\\x15<\\xdb<4\\xe6\\x92<\\xad\\r\\x07<0\\x1cs\\xbdw\\xc3\\xad=>\\x89\\xd7=\\x9a\\xcd\\x10\\xbd\\x8d\\xe5\\x97<5\\x97\\x18\\xbc\\x81q\\xd9\\xbc\\xb0t\\xfd\\xbb}\\xfc\\xb7=-k\\xa1\\xbd\\xac\\x0c\\xba\\xbb\\xc48S\\xbd\\x80\\xd3\\x12=:\\xd2\\x97\\xbc\\xd5\\xbf\\x86=:\\xf1Q\\xbc\\xf5\\x06\\xc7<\\xfd^M\\xbb\\x8f\\x91<<\\xabA?=\\x93\\xd66=\\x01\\xf1><\\x9d\\x07}<\\x14A\\x11<\\xd0\\xeb#<\\n\\x1a\\xea;\\xa5E\\xac<\\x8b\\x1e\\x9c\\xbd\\xc8\\xb6D\\xbc\\xd50\\x8e;\\xf5\\xbf\\xd1\\xbc\\x15\\x89\\x16\\xbd\\x07\\xfe\\x11=z\\x14f=\\xb9\\xa0\\x8c<\\xf6FE\\xbd\\x04\\xcf)\\xbc\\x93\\xce\\x07=&p\\\"<\\xa9\\xc9R\\xbdK\\x88W<\\xa6\\x17\\xee\\xbaD\\x0c\\x97\\xbdyI|\\xbd\\xe2=(\\xbd\\xe3\\x1d\\x80=\\xe1mx=\\x08\\xec\\x15<\\x92\\xdf@\\xbc\\x8cmE<Cce\\xbd\\xfb\\x19\\x9b\\xbc\\xb5\\xff\\x1e;q\\x13\\xea\\xbc\\x08\\x17$<\\xd8\\x9e%:\\x10\\x99\\xa3<\\xd9<]<Ho\\x87\\xbdi!U=I\\xfe\\x97\\xbdq\\x06n\\xbcKp\\xa9\\xbcP\\xf1\\xbe\\xbd_&\\xb3<\\xf0\\xcbW\\xbb\\x14\\x12\\xd7;\\x85\\x0cp<\\x02\\xddV\\xbc\\xf9\\xaf\\xb4<\\x9b\\xb15=.\\x98\\xe9<\\xea\\x9f\\x11\\xbd\\xd8N2\\xbd\\xe4\\xb3\\xf2:2(\\xb8<\\xd8\\x9e\\x1e<\\x8b\\x8ac\\xbd\\xcb\\xe0\\xfb;Zn^=\\x8a\\xb1\\x04\\xbdY\\x1a\\xf5<[\\x9f\\xda\\xbc\\xa97\\xfd\\xbb\\x0c8>;^)m\\xbb\\\"eY\\xbd\\n01\\xbc\\x0ce\\x9b\\xba\\xbf@\\x8e;\\xab\\xa7\\xa1;1\\xd1\\x00=2\\xffU\\xbc\\xd30\\x0e;\\xab\\xeb\\xba\\xbc\\xe1f\\xeb\\xbc)\\xa84<.\\x95\\x1f=\\xf5\\xee\\x04<#\\xba\\xcd\\xbb\\x15\\xc5\\xa2\\xbd\\xdf\\xe9\\x1a<\\xf5\\xca\\xe7<\\xb5\\xd9\\x18\\xba\\xb7I\\x84\\t\\xbet\\x1f\\xb9N\\xdf8\\xbc/B\\xab<s\\xf4\\x1c=\\xb3\\x91\\x1b=r\\xe9(=\\x809\\x18\\xbb\\x14\\xfa@\\xbc<\\x93\\x8b<\\xee$\\x81\\xbc|\\xab\\xc1;a\\xa0\\x9e<\\xd0\\x00\\xaa;l\\xa6+=1\\x84\\x96\\xbbj\\x16p\\xbc\\x04\\x0f\\x03\\xbd\\n!\\x8f\\xbdX\\x95\\xdf:(;\\x93=U/\\xce:\\x82\\xc9\\x01=6S\\xf7\\xbb0O\\xa3\\xbc\\x8b\\xf2R=\\x99Z\\x9f\\xb7\\x9e\\x9a\\x03=\\x10\\xd4n<>\\xfd\\xdb\\xbc\\x8a\\xe6o;u\\x1a\\xf6<\\xb1\\xf4\\xcb<\\x07J\\x96\\xbd\\xfa\\xbce\\xbdA\\xaey\\xbd-\\xb5\\x80;\\x83\\xce\\x10=y~\\x1a=\\x96\\\"\\x03<-eP\\xbc-\\xb7\\xda\\xbc\\xb9\\xe4*\\xbd\\x0e\\xce\\x8e\\xbc\\x12R\\x03=\\x93\\xfc\\x00<\\xe5\\x16\\x93\\xbc\\x96\\xdax=\\x0b\\xdfR\\xbd\\xfbdL\\xbd\\x83\\x94\\x12=.\\xe4\\x18\\xbd\\x17_\\xa2\\xbc\\xe2\\xd5$\\xbc02;=\\xbe\\xb9\\x12\\xbcB\\xdb\\xe7\\xbb\\x12\\xfe\\x1b\\xbdh9f;\\xa9\\x1c\\x14=f\\xfb#<\\xbex\\xb6<\\x1e\\x98\\x05\\xbd^\\x93\\xe1<\\x1b9Y=\\x8e\\xe5\\xbb\\xbbM\\xa6*\\xbdM\\x8a\\xb3<\\xcc\\xa8;\\xbcf\\xab\\x8b<\\x93.\\x07=\\xec\\xa8\\xd5\\xbb\\x18\\xcd\\xa4\\xbc\\x8a\\x02\\x06\\xbd\\xa9\\x90\\n=m/\\xdc<B\\x02x\\xbd\\x9a\\xe7B\\xbdz\\xf5d=\\xc6\\xd9|<\\xb5\\x06\\x83=H\\x9eq\\xbd\\x94?\\xfa;&Y\\x92\\xbc\\xc8\\x03\\x85<\\xbd\\x0e(\\xbd\\x83=\\x0c\\xbdS\\xefw=\\xe5v3=\\x00\\x96\\xb1;\\x9d\\x99\\xc5<Uzg;8\\rF\\xbd\\xbf\\x08\\x9b=\\t\\xfd\\xb6:\\x9f\\xd1\\xc5<\\xee\\x8d\\x02=Q\\xd6\\xb1;\\xd2\\xef\\x0b\\xbd\\x81\\xf4\\x9d\\xbb\\x86e\\x12<\\xd4M\\x05<\\x9dF8\\xbaLg\\x80<:.x\\xba\\xe4\\x9e\\xad<\\xef\\xd2\\x0f\\xbd\\n\\xdd\\xf0<\\xfb\\x1c\\x8c\\xb9>3\\x82\\xbcw]\\r\\xbd\\xf7\\x87f=\\x82\\x10*\\xbd\\xd9iW<[)M<P#\\xdd=u\\x89\\x1b;iUi<\\xf42\\xda\\xbcqu-=\\xc3\\xc10\\xbd\\x92\\x87}<\\xa2\\x0c\\xdf\\xbaB\\x93\\xd0<\\xf6a\\xf5<\\xf4o\\x0e=,])\\xbb\\xbat9\\xbd\\xcd\\x95d<4+U=\\xd1\\x80\\xe5\\xbc\\xc6f\\xe0\\xbc\\xf3\\\\\\x90\\xbd\\xae+\\xe2<\\xfby\\xa6:\\xf9d\\x94=\\xff\\xa6\\x8c<X\\xba(=p\\xf1\\x86\\xbdB\\xe8\\x07\\xb9\\xfe\\xeeq<\\xd7\\x99\\x1a=8\\xd8\\x08\\xbd:l\\xd8\\xbcZ3u;\\x06e\\xc0<\\x90\\xb9\\xee;\\x99|4=q\\xd1\\xe1\\xbc^\\x0e\\x06=1.\\xa7\\xbc\\xd2\\x83\\x00\\xbdW\\xdd\\xae<\\x15!\\x82\\xbc\\xa5\\xe3\\x0c=y\\xdd\\xa3<\\xe8\\x84\\x18=1\\xe0~\\xbd\\x9a,\\x8c\\xbcp\\x12\\x88;\\xd0\\xcb\\xd0;\\xc0\\x03\\x19=\\xde\\xd98=\\xa6\\x96K;z\\\\o<\\x07\\xcd\\x92\\xbc\\xec\\xfb\\xdc\\xbb\\xb4W\\x8c\\xbc\\x9a7\\x96<n\\xdb\\xf4<3\\x84\\xdc\\xbc\\xca\\xcd\\xf3:7\\x1f\\x9e\\xbc=\\x9eI<.\\x15};\\x8b\\xf3\\xc3<w X=\\x0ca\\xbc<\\x9f\\\\Q=\\x98F\\x9a<T\\xa7\\xec\\xbc\\x842\\xcb\\xbc\\xf8\\x93\\x90\\xbd\\rnI=\\x96\\x13\\xa1\\xbc\\xeb\\x8d\\x02\\xbd\\xb3\\xcb1\\xba\\xb5\\xc4\\x8f\\xbc2n\\xc1\\xbc\\x96\\x07\\x9d<\\x0e\\x9aL<\\xf6\\xb2\\xc0\\xbc\\xad\\x0bD=\\xb1\\x89\\xf7\\xbcM+~\\xbc\\x0f\\x92\\x81\\xbd\\x7f\\xa5\\xa8\\xbdN\\xe9s\\xbc\\xe7U\\xb4\\xbc\\x19\\xe3}\\xbc\\xd6\\xb2\\x05<<HM\\xbd\\xfa\\xe26\\xbb\\x8d \\x90\\xbcu%\\x8b\\xbd)\\xd0\\x1a\\xbcQ9\\xd0\\xbc\\xf0\\x1c\\x0b<\\n\\xbf:\\xbd\\xf6\\xf5Y<0\\x0ei\\xbd#G\\x83<S8\\xa6\\xba\\xa3;\\xf38\\xf3Uv<\\xa6\\x06\\xc6\\xbcg\\x06\\xd5\\xbcP\\xd5\\xe9;\\x02\\x19\\x0c\\xbbh\\x1a\\xa6\\xbdD\\xcf\\xad\\xbdq\\xc1\\x04\\xbd0\\xff\\xcb\\xbc[\\xf1\\x1b=\\x86\\x87G<\\xc3]\\x90=\\xdcE\\xbf\\xbc\\xc4\\x9c\\xfe<S\\x9e\\x95\\xbd\\xffG\\x8c\\xbd\\x97\\xb7\\x15=i\\x8b\\x7f\\xbb\\xf8\\xac\\xbf\\xbc j\\x03\\xbd\\x9e\\xdf\\xf5\\xbc\\xe6\\xa4\\xa9\\xbb \\x8fu\\xbd&w\\xb0\\xbc\\xb6\\x1d\\x03=&\\xa0\\x80=4\\xd7\\xc8\\xbbHr\\x06\\xbc\\x93\\xff6=#\\xab\\xd1\\xb9\\xb7W\\xf7<\\xc3Z:<\\xdb\\xf3\\xbd;\\xff*\\x87\\xbd\\xd8\\xc98;R\\xce\\x9a\\xbd\\xb8\\x80$\\xbb\\x00D\\x94;K\\r\\x0f\\xbc\\xf8\\xf4&\\xbc\\xeaPL=]\\x88f\\xbd\\xd8\\x1aL\\xbcf\\x87\\x15\\xbd\\x84m\\xf6<T\\xd7\\xb7\\xbc\\xce\\xc6\\xb2\\xbd-#\\xae<\\xf6\\xa6^;\\xc1\\x16L<D{A=L\\x93\\xcc=\\x13`<\\xbc\\xa8\\xc1E=\\xa8</=^\\xfa\\x84;\\x95t\\xf7<\\x15\\x92\\x83\\xbd\\xad$\\xf6;\\x1d\\x0c\\xb3\\xbc\"\nHSET bikes:10024  model 'Hiiaka' brand 'Bold bicycles' price 1217 type 'eBikes' material 'aluminium' weight 8.8 description 'A city eBike that could double as a short-haul commuter. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"]\\x94\\x01<\\t0\\xd8<\\xcf\\x01\\xc7\\xbcZc9=\\xd5t\\xf3\\xbb\\xeb\\x0c\\\";\\xcc\\xb4\\x94\\xbc\\xae\\x08\\x9c\\xbd5\\xa0\\\"=\\xda\\xc1\\x02\\xbdi\\x1a\\x91\\xbc\\xe3\\xd8\\x11\\xbdq\\x13&=yX\\xfc<K\\x8e1=\\x81\\x99@\\xbdE\\x91\\x01<I\\x159<\\x9ei\\xf5\\xbc\\xe6\\x02\\x8b\\xbd\\x04\\xf1\\xa8\\xbc\\xe1\\xf3x\\xbd\\x9eI.<\\x82nF;\\x9b\\x0e\\xdf=\\x04\\xb4\\x06=\\x00w\\x8b<\\xce$\\xd3<vw\\x84\\xbc\\xcd\\x88d\\xbc5\\x11\\x0f\\xbd\\x13\\x06\\xa7\\xbc.b\\xb7\\xbc\\xd8\\xa5\\xe7\\xba\\x82}6=?\\x97\\xce\\xbc\\xd9\\x9d\\xa0<\\xf0\\xed\\x89\\xbc\\xfdc\\x05\\xbdV\\xdc$\\xbc\\x12\\xef\\x8e=\\xc770\\xbc\\\"\\xa2\\x01\\xbb0\\x9f0=K\\x86\\x12\\xbb\\xb7\\xe6\\x1f\\xbc\\xb8\\x05\\x83=\\xb06X<\\xc3\\x06\\xab\\xbb_&\\xc6<\\xeb9P\\xbc\\x9cl\\xf0\\xbcUD\\x0c\\xbd\\xbb\\xc8e;g:\\x7f<\\xb1\\x93\\xa2\\xbc\\xdc(H\\xb8\\x16$\\xc1\\xbd\\xcb\\x8c\\x9b\\xbc81\\r=-^\\x14=\\xc7\\x1d\\x84\\xbcm\\x9cO;&\\x96W=\\xf5\\x1f*=Y;\\\"=\\xd5\\xf8\\x8e\\xbc\\x9e\\x8cF=\\xa8\\x14\\xd2;u#\\xa9<\\xf2&\\x9d\\xbc}<\\x0f\\xbd\\x0c\\x052\\xbc\\x82\\xf3\\x08\\xbd\\xf4MI\\xbd\\xe0\\xfa\\x0e=\\x1b~\\xe1\\xba1=\\xaa\\xbcD\\\"\\x9c\\xbd\\x05\\xaa\\x9d;^\\x9f\\xd4\\xb9\\xf7\\xb7\\xd3;\\x1c\\x83\\xfe<\\xd5~\\xe0\\xbcP\\xb4\\xb9\\xbc>2\\x1f=\\xa7\\x86\\x1b\\xbc\\x1fO\\x19\\xbdbQM=d\\xd7W<\\xd3bt<\\xecM\\xe6\\xbcc\\x97 \\xbb\\x7f\\x11\\x94\\xbb.4\\x80\\xbc\\xa9\\xd4-\\xbc\\xd9;\\xc1\\xbc\\x10\\x005\\xbc\\xce\\xd7$;\\xa7Cy<\\xcfM\\xe5<\\x92\\xe5\\xf5<\\x14@z\\xbc\\x1d\\x02|\\xbcW\\x98\\x1f9\\xee\\xc3\\x02>5\\xa1\\x92;\\x8c\\xbc\\\"=\\xc1&\\xfd\\xbaX\\x0b\\xe2<&>\\xff;\\x97$Z=\\xcf\\xc6\\x14\\xba@\\r\\\"\\xbd|{\\x9f\\xbc\\x1e\\xd3\\x8e\\xbc\\x1b\\xd8}\\xbd\\xfa\\xf4\\x81\\xbc\\xb5\\xf8\\x15<b\\xadK<\\xedW\\xb9;\\xfc\\xfb\\x9f<\\x88\\xeb1=izF\\xbd\\xc0\\xc5j=\\x97\\xa8\\xe8;4\\xc0\\xa2<\\xa7))\\xbd\\x8c\\xa0r\\xbd\\x90@\\xc1\\xbdM\\x92Q\\xbc\\xdd\\x84\\x9f\\xbb0,\\xb2<\\xab\\\"\\x14\\xbdy\\x0e\\x8f<2\\xd0\\x06=\\x15\\x9d\\x06\\xbc\\xf2\\r\\x89\\xbdh\\x82\\x82=\\xc0\\xe81\\xbc]\\x05\\r\\xbd|\\xef\\xfa\\xbc\\xfa!(\\xbc\\xe2V\\xf2\\xbc\\x92\\xb2\\xf2\\xbcK\\xdb<\\xbd\\xd8\\x9dv=\\x93\\xbda<\\xc6\\xd90\\xbdH\\xb6\\x13\\xbd\\x80~f<\\xba\\xd9Z=Q`\\xf8\\xbc \\xd6\\xb7<W\\x0c\\x9e\\xbc\\x00~\\xc5;\\x85\\xb4\\x8c\\xbc\\x01\\xea\\x90;\\x08\\x86\\xfe\\xbc6[%:\\xb8\\n\\x93=!\\x17\\x12\\xbdN0\\x8b<\\xa5b6\\xbd\\xa2\\xbc\\x1a=\\xd4\\xb0\\x84<T\\x16\\x1f\\xbc\\xa2\\xf0\\x89=0\\x86~\\xbc\\xf7\\x8b\\x8e\\xbc\\xc7\\xb4\\x92<\\xa0{P=\\xe7\\xb8\\x8f\\xbc\\x8c\\xe0\\n\\xbd9\\x15\\xaf\\xbc\\r\\x05\\xaa\\xbb\\x91\\x18\\x8e\\xbd\\xdd\\x04\\x0f=\\xffv\\x8c:\\x1e5\\xae\\xbc\\\"\\xb2\\x9b=J\\x15\\x98<\\xe2D\\x1d\\xbd6\\x8f_=\\xf6\\xd4\\xd0\\xbcUlY\\xbd6_u=\\x0e\\xf5\\x01\\xbd\\x97\\x00\\xcd\\xbb\\x07Y~\\xbdC\\xcd:\\xbd\\xd1\\x9c\\x9f\\xbb\\x14\\x8c\\x90\\xbd~\\xce*\\xbd\\x8ec.\\xbd\\xd7\\xd0\\x80={\\x94\\xa5<\\xfcvy\\xbd gp=0\\xa1\\x89=\\xa8\\xde\\xad\\xba\\xa6\\x1e2\\xbd+o\\x04\\xbe\\xc6\\xff\\x95;\\x8f\\xb0\\x13\\xbd\\x950\\x8a=\\x8a\\xa9\\xa2\\xbcL\\xad\\r<c\\x9b\\x03=\\xcd\\xfc\\x81\\xbc\\xaf}\\x83<a\\xac\\x18\\xbb-\\xf1\\xc3<G\\xc4\\xae\\xbc\\x94\\x94\\xe2\\xbbl2\\x12\\xbc\\xbb@\\x12=y]\\xc2<d\\xadv=l\\xcf\\xa8;\\xa5\\x19\\xda;0  <#\\x8c\\xf4\\xbc\\xe1L\\xa4<\\x1f\\xa4\\x9c\\xbd\\xbe\\xdc\\x8b\\xbb\\x8d\\xfe\\x14\\xbc+\\x10\\x9e<\\x91\\xcc\\x1c=+\\xe4\\xa8\\xbc\\xf5\\xb0\\x90\\xbc]\\xf41\\xbdp?}=\\xf2\\x8eM=\\x8d-\\x0c=\\xeb \\x06=\\xce\\xaa0=b\\xb6\\x02=\\x86V \\xbc\\xb5\\x16\\x9d\\xbb%2!=\\xc1\\x8e\\x8b\\xbc\\xd2\\x8f\\x8c<\\xde\\xf3\\x8c\\xbb\\x94\\x9c\\x11\\xbc\\xb7\\xef\\xf1<\\xe3\\xfb\\xfe<\\x96/8=s\\x86\\x9a\\xbc\\xfc\\x82O<\\xc8\\xcc\\x98;\\x95\\xdc\\xc5\\xbd\\x9cf\\xad\\xbb7\\x80\\x90=\\xdd2\\xa9\\xbcP\\xd2}=n\\xcb\\x00\\xbd&\\xb5V\\xbc/`\\x16=\\x06\\xa2}= o\\x14<\\x1a\\x12$\\xbd/\\xfe\\x82\\xbd\\xf2\\x003\\xbdRX\\xa6\\xbcP\\xc8\\xb3=\\xd2b\\x11\\xbd\\xf3\\xbe\\xf5\\xbbl\\r\\x93=C\\x1c\\xd8\\xbcT\\x1c\\xb5\\xbbt\\xbfa<\\x1dlQ\\xbdB\\xe5\\x8b=\\xb49\\x05\\xbd\\x9ac\\xb8\\xbc\\x02\\xd8\\xe1\\xbc\\xbc\\x8c\\x89\\xbcsz\\xe9<\\x98\\xf7\\xfe\\xbc\\xdd\\x96K\\xbc{%o=\\xf6\\x99c\\xbd\\xd3\\x07\\xe4<M96=eJ\\x9f\\xbcaN\\xc1\\xbb\\x83H\\xff;\\x98\\xed\\xa6\\xbd7_\\xec\\xbc\\xcc\\xef\\x82:\\xaa\\xa0\\xa1\\xbd\\xecf\\x9a=Acg<T\\xf5/\\xbc\\xc9\\xc5\\x86\\xbc\\x15\\xc0\\\"<]>F<\\xc2I\\x16=\\x12BD\\xbcv7\\xfe<\\x1e7\\xa9\\xbcY\\xa7\\x94=2J9\\xbc~H\\x19\\xbcF\\x08\\xde\\xbcd?g\\xbc4Hb<I*\\x93=\\xf6m\\x80<\\xe0\\x1e~\\xbd\\xc6\\xd9\\x91\\xb9\\xde\\x82\\xd7\\xbc5\\x8f&<mrX<zt[\\xbc\\xa3\\xd7c\\xbbo\\xd0\\xcb=\\xe5\\x11\\x8b\\xbbt+,\\xbdDk%\\xbd\\xe6\\x8d\\x9b\\xbc V\\xcb\\xbb\\x04\\xef\\r\\xbd\\xea\\x90\\xa1\\xbb\\xd7\\xb8)\\xbd\\x9c{\\xe4\\xbc\\x02|\\x9e\\xbd\\xe7\\xb5\\x84=\\x81\\xee\\x99\\xbc\\xf1u\\x8f=\\xeeu\\x11\\xbd\\xc0#\\x83\\xbd\\xd8\\xc5_\\xbc8\\x8f\\xd2;\\x18\\x94\\xf3\\xbc\\x1e\\xa1m\\xbc\\xee\\xd4\\xce;@\\x17\\xd3<\\xc5\\x83\\xb6;q.\\xac;\\x99M\\xb9\\xbcF\\xc5\\xaf=\\xf5\\x13\\x9e\\xbc\\x05\\x95\\x8a\\xbc\\t\\xf6\\xb2<3\\x865:\\x1dF\\xbb\\xbau>\\x1d=;\\xd0\\x87\\xbd>\\xba.\\xbd9z9\\xbcu\\xb0P<ed?\\xbd&\\x92\\xa5\\xbb\\xb6\\xefc\\xbd\\x9a\\xa4\\xf6\\xbbiQ^<\\xb8,_\\xbd\\x9e\\xec\\xb5\\xbc\\xef\\xeb\\xb9\\xbc\\x1f\\xfc-<HM\\xa3\\xbcK??=\\xc5\\xab\\xb2;k\\xca\\x01=\\xb8S\\x16\\xbb\\xce\\x96\\xbf=M\\xbc\\x1a\\xbcP\\xd0@\\xbd8m\\x1a<i\\xa7\\xc8<\\xf3\\x1dg=n\\xc7\\xbf\\xbd\\x9d\\xdf\\xdf<\\r\\xe3\\xc3<.\\x059\\xbc\\x01\\xc9\\xd6\\xbc\\xba\\x878\\xbd\\x00\\xf8\\x05=\\x00\\xf3\\xcb\\xbc\\xb8N3\\xbd\\xdc\\xc0\\\"<v\\xc3\\xa1:1\\x1b\\xb3\\xbd\\x05\\x92\\x99\\xbb\\xf6\\x97\\xe5\\xb9\\x10\\xf8\\xad\\xbc\\xeaAF=\\x9e6\\xc2\\xbc\\xa7>\\x0c\\xbd\\x8f\\xc9\\x8c<+\\xa5\\xa6=\\x91\\x19<\\xbd\\x9b \\\"<!\\x85\\xc2:t\\x0e\\x88\\xbd\\xb5NU=\\xb0D\\x1b=j\\xb3\\x8c\\xbd\\xc6\\n\\x1f<d/\\xe7\\xbc\\xd9\\xc0N=\\xf5\\x01\\xa6=\\x9bb \\xbcD\\xd3\\x90\\xbc\\x89_3=\\x1a\\xcc\\x08<\\x97n\\x03\\xbc\\xfc5\\xdd<-\\xb2\\x83=\\x9c!\\xce\\xba\\xc2\\x9dr=\\xef\\xe8\\xd6\\xbb\\x9f\\xd2\\xc3;\\xe8iB=d\\x9c%\\xbd\\xe4\\x9b\\xbf\\xbcs9\\x17=\\\"d\\xd2<\\x0c\\xda=\\xbal\\x94\\xb3<\\xe9\\x04\\xa2\\xbb\\xea\\xdd\\x0f=\\xfaD\\xbb<U\\xa9\\xa1;\\\"\\x13\\xc3:\\x88\\xc2D=\\x12\\xba\\x9a\\xbc\\xb4\\\"\\x1b\\xbd\\xaf/g<\\xad\\x95)\\xbd\\x98\\x1d\\x1e<\\x94d\\xbe\\xbbV\\x85&\\xbd\\xbbJP=\\x9f\\x96$=\\xb2\\x1d\\xd3<\\xdb\\xee\\x80<#\\xd6\\x1b=~X4=\\xe4D%\\xbc\\xc74\\x92\\xba\\xf4\\x06\\x1a:\\x99\\xcc\\xb6\\xbcm \\xb6<\\x03#m\\xbd\\xb9h*\\xbc\\xcc\\xe3\\xb2<u&\\xc8\\xbcxc\\xf4\\xbc\\x0fp\\x9c\\xbd\\xee\\x07\\x16\\xbcC\\xc4\\x0c\\xbd\\xcfQ\\xc3\\xbb\\x80\\xc8\\x0e\\xbdMd\\xaf;w\\x92\\xfe9:b\\xcd\\xbc\\x14\\x19{=dD\\x85=\\xabO9<\\xeb\\xf4\\x13=\\x1d\\x92=:\\x0c\\x81\\x87\\xbc\\xc0\\xf93=\\xd0\\xa8\\x8b\\xbalN\\xcf\\xbc\\x11\\x9a\\xc8;\\xa3Q\\xa6\\xbc\\xa5\\x17S\\xba\\xd0\\x93!=<#\\x81\\xbck\\xdb\\x17\\xbc\\xd8u\\x9d;Q\\xee\\xa9<\\td\\r\\xbc\\xa8s\\x08\\xbcN\\xe96<\\xf4\\xb1\\xa4\\xbb:E\\xe7<f[\\xb8\\xbc\\xc1Z!\\xbc[(g=x\\xe5Y\\xba\\x1d\\x90\\xa6\\xbdQ\\x81\\xf9\\xbc}\\xcb\\xd4\\xbb\\xb3`\\x18\\xbd\\x9c\\xfe\\xb8<\\xc5Y8=\\xdfK^\\xbcF)j=\\xd8\\xae\\x96\\xbc\\x12OZ\\t\\xe6Q\\x10\\xbc\\xb0\\xc83<\\x9a\\x1e\\x89\\xbc\\xcd\\xc1\\xa4\\xbbZ\\xaf|<f \\xf6;B\\xd6\\x10\\xbd\\xf0K0\\xbd\\x80\\xca7<\\xda\\x1f\\x08\\xbd\\xda0\\xbf:4\\xa0\\x8a\\xbc\\xf1\\nD\\xbb6h\\x87=\\x90\\xf7U\\xbdd\\x1d =[\\xdf\\x95\\xbc\\xee\\x03T=\\t\\xd1r;\\x90\\xb5\\xe5<f\\xf0\\xf0<q\\x11\\xc9<\\xc2\\xfb\\xe0;\\x88_w\\xbdoUc<\\xbb\\x1cL\\xbc\\ni\\x9d\\xbc \\x94k\\xbb\\x84z\\xb0\\xbcV\\x9c\\xce<5J\\x1f<9rJ\\xbdBR\\x14\\xbdz3\\xc5<\\x01\\x07\\x81<\\xe9L\\xda;\\x17\\xce|\\xbb\\xe8[{;x\\xf7\\x90<\\r\\xcb$=\\x8e\\x19\\xfc<\\x00F\\x10<\\xb6\\x15a\\xbc~\\x1e\\xa0<\\xc7\\x99\\x02=\\x92\\xd5\\x88\\xbb\\x18rE=\\x1a\\xb3\\xe7:\\xdc\\xef\\xac;\\x0cN\\xa1\\xbc\\x844C\\xbd\\xc7&6<\\xc9\\t\\xe6<s\\xfc\\xf7<\\x86f\\x83\\xbcex\\x1f=\\xd9h\\xd2<_\\xd0j<\\x8fTF\\xbb\\xa9n\\x1c<_\\x8a\\x07=\\xa5\\x0c =\\xd3\\xf5\\xe3<U\\xf0\\xd9<\\xeb\\xa7\\xf9\\xbc[,\\xbd\\xba\\xa1\\xcd\\x1a\\xbd3\\x8e\\x11\\xbbH~D=\\x1d\\xf8\\x10\\xbdu\\x04\\x16=\\xfc\\x9bM<\\x1f\\xb0?<\\x94\\x7f`=6\\xa7&=\\x15\\x93>==\\xd9W<\\x05A(=\\xfa\\x14\\x9c\\xbc\\xed%v\\xbc\\xfb\\x9a\\xa1<\\xfd\\xbc\\xc9< {V<\\xf1EC=\\xc0j\\xb0\\xbc\\x19+A<0y\\x12=\\xf7yZ=\\\"a\\xb9<bEv\\xbbK\\x9b\\xb6\\xbc\\x8d>\\xb7\\xbb^\\x98\\xc0<i\\xc5A\\xba\\xc6M\\x8d:\\xe1\\xa3n<J\\x0e\\x86\\xbd\\xf4\\x1d\\xad\\xbb\\xa9\\xe8,:\\xfc\\xf9y\\xbc\\xdb>)\\xbd\\x02\\xd7k\\xbd\\xddeP;\\x1f\\xf5\\x87\\xbd\\x14\\xa71=(<\\xc8;l\\xae\\x12=\\x00\\xba\\x13<:\\x11\\xe9\\xbc\\xdc\\xc0\\xe4\\xbb\\xea\\r\\x9d\\xbb\\xf4\\x08G<\\x8e\\x04\\xb2\\xbcig\\xd3=\\x9a^\\xe3\\xbb@:\\xe6<Z:\\\"=\\xfa\\xd7\\x1e=\\x00\\x7f\\x82\\xbcS\\xacU\\xbdr\\xedN<-\\xed\\x10;\\xd2<1=_\\x1e\\x84<\\xcctJ\\xbd\\xe4i\\x1f\\xbc~\\xb5J\\xbc$\\xfa4<\\xcc\\xa0\\x05>\\x8e\\xac\\xaf\\xbd\\xc1/\\x80\\xbc\\x8f\\xae\\xa9\\xbcb\\x98\\x1f=\\xe7@\\xa2;\\xe7bW=\\xd5\\x0b\\xea<(D\\x95<\\xea\\xa7H\\xbdo\\x08*=+\\x8d\\x14\\xbc\\x14\\xe0\\x11\\xbd\\xf6C\\xc2\\xbb\\xda\\xc8~\\xbd\\xf9J\\xa0<<\\x10\\xb9\\xbc\\xc6&\\xf8<\\x9e\\xd9\\xd5\\xbc\\xaf\\x01\\\"<\\x061\\xdf\\xbcd\\x1c\\x8d\\xbchO\\x9e\\xba\\x8f\\xebn;a\\xac\\x91\\xbc\\x1e\\xc9\\xf1\\xbb\\x0cmC;\\xc9\\x82V<a\\x12\\x1d\\xbd\\x8f(\\x17=\\xeac ;iD\\x15\\xbd\\x87R\\xed\\xbco\\xd6\\x80=\\x03\\xb8f<\\x80\\xc8\\xa4<\\xefR\\xd2\\xbc\\xcb\\x97\\xcb\\xbb\\x0czQ\\xbd\\xb4\\x17\\xfb;|m\\xc0=Z\\xfeR;2+\\x1a=\\x1b\\xec\\xe7;\\xa8p\\xd1<6\\xb8\\x1a\\xbc\\x13\\x99M\\xbcf\\x82\\xf5<\\xd6\\xc6\\x03;\\xd3\\\"\\x90=\\xb6\\\\!\\xbc\\xfcQ\\x02\\xbd\\xf6\\x13O<x*_\\xbd\\xff; <\\x85g*\\xbd\\xd5\\x9c\\xa1<\\xb5\\xaa\\x16\\xbdo\\xc2T\\xbc\\\"]\\x9f\\xbc\\x92o_=\\xd7\\x9c\\x80\\xbc\\xdd*\\x03=\\x0fC\\xdf\\xbcc\\x15\\xcf\\xbc\\x86)\\x1a\\xbd\\xb2{/;t\\xcf\\xba\\xbc\\xfakH\\xbdH\\xdfj;\\x0b\\x90N=\\xc0i\\x8f;mSl\\xbd1\\x8e\\\\=\\xc8\\x90-\\xbc$\\\"\\x08\\xbdJ\\xbe\\xd7\\xbc+\\xcdc\\xbd\\xc5\\xdc\\xeb<\\xed\\xa7\\xea\\xbc\\\"\\x9fx;\\x15b#\\xbdb\\xe5\\xa1<j?\\xa9:M\\x08R=K\\x1f\\xd5\\xbc\\xe9\\x94\\x95\\xbc]\\x93B\\xbdoE\\x18=(\\xa1Q\\xbdO\\xf3@<\\xef\\x1f\\xaa\\xbcH\\x03\\xf7<\\\"JM\\xbd\\xf6AO=\\xf4\\xd5j=&\\x878=\\xaa\\xd3\\xcc\\xbc.\\x98-<\\xe5\\xc3\\x80\\xbb\\x92\\xd0v;\\x1bkR=<<\\xa1<w\\x02\\xe5\\xbc^\\xc2\\xf7\\xbc!7\\xc1\\xbb,\\x18\\xed\\xbcv\\xad\\xd5<I\\x1e\\xbc<\\x88\\x01\\xa2\\xbc\\xbd\\x90>=2\\x86@<%\\xdf\\xc7<\\x9cH\\x18=e\\xfb\\t=]\\x88d\\xbc6\\xec\\xd2\\xbc\\xb2\\xa2\\xa9<J\\x95\\x8e\\xbd\\x0b\\xd6\\xc9:\\x82\\xae\\xbe\\xbdj\\xfe\\x1a\\xbdW\\xce\\xcf;\\x84\\xa84\\xbd\\xf8/\\xa3\\xba\\xaaYr=\\xf8\\xcb\\xf1\\xbc\\x04;\\xe9<\\x00o\\x01\\xbe\\xd3\\xad\\x04=\\x85:X\\xbd\\xc0\\xff\\xad\\xbd\\x0f\\x0f\\xa8:\\xb8n\\xc0\\xbat\\x97z<\\x08\\x8b\\x92<\\xda!\\x05=h\\x8a\\x80\\xbc\\x1c~>;\\x8c\\xe3\\x8f<\\xe4\\xf1|\\xbd\\xd0\\xa3\\x05\\xbd\\xd5i\\xea\\xbc\\xc3\\x89\\xbf<h\\xaf^\\xbd\"\nHSET bikes:10025  model 'Nereid' brand 'Ergonom' price 3567 type 'eBikes' material 'aluminium' weight 14.0 description 'Urban riding, gentle off-road ebike. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xa7f=<\\x97\\xca\\x89<E\\xe8\\xc9\\xbc\\xaedQ=\\xdf\\xf5\\x95;\\x81.\\x82\\xbc\\x8b |\\xbcyw\\x8b\\xbd\\xd9\\x13\\t=\\xc6\\xc1\\xc2\\xbc\\x8e\\xe5\\xbb\\xbc\\n\\xf1\\xa1\\xbc\\x00\\xa8V=\\x05\\xee\\x0c=LTe=\\x9dM\\x0f\\xbd\\xb2%\\x97;\\n\\xba \\xbcX\\xf6s;\\x7f\\x81\\x8e\\xbd\\xeb\\xdem\\xbc\\xdd\\xa3e\\xbd\\x93\\xa6\\xbf<\\xc6\\xe2\\x91\\xbb\\x03\\xc4\\xa2=\\xad\\x8e\\x86<w`0<\\xd7p\\x06\\xbc\\xd9,\\xff\\xbcM\\xa9\\x13;\\x8e]-\\xbd\\x05\\x98\\xd2\\xbc\\xc6&\\x8c\\xbc\\x90\\xf4\\xcd:\\x08\\x1a?=-\\\"\\xdb\\xbc\\xa1z|<\\xd1\\x86\\x9f\\xbc[Z\\xd0\\xbc\\xac\\xe9\\xb5\\xbb\\xcen_=h\\xa0\\xda\\xbb\\xa3%\\xe1\\xbb\\x1b\\xdb\\xf8<\\xec\\x0fz\\xba\\x9b\\xc0\\xbf\\xbc2\\xf3_=H<5;y$\\xa8\\xbco\\xbd\\xa2<\\xb6\\xe8\\xcc\\xbc\\xa6\\xe7\\x90\\xbc\\x91\\x1c*\\xbdL\\x05[\\xbb\\xa6\\\"\\x88<K\\x9cs\\xbcz\\xe4D\\xbc\\xe4\\x8d\\xd0\\xbd\\\\X\\xa3\\xbc\\xb7\\x0ec=JX\\xda<\\x9d[\\x91;P\\xfe\\xc2;\\xb2\\x9af=\\x84\\xdd:=\\xa1\\x02\\xfe<\\x8b\\x80\\xe4\\xbc\\xbfaX=\\x80\\x1fC<1\\xd7z<\\xd0\\xfc\\xe3\\xbcF\\xd3[\\xbd<Q$\\xbc\\x1a\\x8b(\\xbc\\x11E\\x15\\xbdu\\xc3M=\\xf3\\xf8\\xff\\xbb\\x08\\xed\\x9c\\xbc0\\x8a\\x91\\xbd2\\xb6n\\xbc\\x01\\xb4\\xe1<L\\x19*\\xbd\\x1b\\x9d\\x8f<\\x9eTI\\xbdB[\\xfd\\xbc\\x85\\x1e\\x9c<\\xae\\x07;<N1\\xb6\\xbc\\xc3O<=\\xb3g\\x85<j\\xab\\x0f<\\xff\\xff\\x0e\\xbd\\xb6\\xd5\\xc0\\xbc\\xd4\\xe1\\xb3;rDL;\\xfep\\xd2\\xbb\\x08h%\\xbc\\xe8-K\\xbc\\xe9\\x9d\\xdb;\\xdc\\xc7\\xd1\\xbbc-B<P\\xfd0=\\x89\\x8a\\xec;\\x1c\\x8f\\xb9\\xbc_Q\\x7f\\xbb\\xc9\\xa7\\xf1=JO\\x05<\\xf8\\x1a\\xcd<X\\x1c\\x0b\\xbcDc\\x02<\\x0f\\x9c\\n\\xbcj\\x13\\x0b=I\\xb7\\x93< }S\\xbd\\x10\\r@\\xbd\\x9ek\\xb6\\xbc}+1\\xbd\\x06\\xefb\\xbc\\x88\\xda\\xc2<\\xbf*\\x92<.=U\\xbcD\\xf0\\xd1;n\\xf1\\xc9<\\xb8\\x11\\x9b\\xbc\\xf0\\x93^=\\xdf\\xd5\\x91<T\\x96e\\xbc\\x93\\xbf|\\xbd\\x8f\\xdb1\\xbd\\xfb\\x82\\xaa\\xbdD\\x9c9\\xb9>\\xbd ;\\x05\\xcf\\x95<\\x9e\\x04g\\xbc!\\x87\\x8b<\\x1e\\x87\\x1f=\\\\\\xfc0\\xbb\\x97\\x848\\xbd\\xf3\\xeaM=\\x91f\\xfc\\xbc\\xb5v0\\xbd\\xcd3E\\xbd\\xdf\\xea\\xab\\xba\\x8d\\xe6\\x88;\\xf6\\x13=\\xbd\\xe1\\xcb\\x7f\\xbdd\\xb3\\xe7<.p\\xee<\\xda2\\xc0\\xbc\\xaa\\xaf\\x13\\xbd\\xaa\\x0ep<\\xcb\\xa8\\x90=\\xe4\\xe3\\xce\\xbcM\\x8f\\xcb<\\xd2\\x8d;\\xbd~\\xde?\\xbb\\xc2\\x9fp\\xbd\\xcd\\xd5\\xdf\\xbb\\xcc\\x97\\xf7\\xbb\\x03m\\xed\\xbch\\xfd\\x99=\\\\[N\\xbc*G\\xb1\\xb9F\\xa2\\xf8\\xbcW\\x00\\\"=\\x98)\\x00=\\xc1\\x82%:\\xe4\\xb3\\x90=ig\\xf0<\\\"\\x89\\x1a\\xbd\\x9a\\xdb\\xb6<z\\xb2P=\\xb8\\\"\\xa4\\xbc\\x8aZ:\\xbd\\x14\\x83/\\xbc\\xc7,R\\xbc\\x16\\x8c\\x96\\xbd\\xacU\\xd9<y\\xa9\\x99<\\xcd\\xe0\\x88\\xbc{j|==\\x86\\xe4;\\x11L\\xa5\\xbc\\xd0\\xa9]=q\\x8e[\\xba\\xaf<\\xce\\xbc<\\x99\\x8c=\\x07\\xbf\\xa9\\xbc\\\"\\xf4\\xa9;\\x9bi\\x02\\xbd\\xed\\xa6\\xde\\xbc\\x04\\xdf\\xd3\\xb9obt\\xbd\\x87\\x9b\\xbc\\xbc\\xe5*\\xd6\\xbcH}|=\\xfd\\xdck\\xbb\\xb6\\xfff\\xbd;_\\xce<\\x05\\xc8\\xac=!\\x89\\x89\\xbc\\x82`T\\xbd\\x96\\x03\\x10\\xbe\\x94\\xe7\\xd6;\\x82\\xcb\\xea\\xbb\\xee\\xe0\\xa3=\\x9f\\xdd#\\xbb0\\xc4\\x01\\xbc\\xff\\xd7Q=o\\xf6\\xfb\\xbc\\xce{\\xb3\\xbbH;\\xe7\\xbb\\xa0\\xcf\\xec<&\\xc4\\x12\\xbdL\\\\4\\xbc\\xbc\\x91Q\\xbc\\xcdm\\xe0<\\xe0~\\x94<\\x02AI=\\x9f\\xa6\\xe6\\xba\\xc3\\xaa\\x9e\\xbc5}\\xb2<\\xa8\\xd1\\xa5\\xbc\\xc3\\\"\\xad<\\xf6\\xf3\\x85\\xbdV\\xab\\x19\\xbd\\xe1\\xa3o<\\\"\\x8bX;Y\\x90\\t=\\xe4M\\x17\\xbd\\x88\\x9c\\x85\\xbb\\xdd\\xc8\\x01\\xbd\\xd1\\xcfU=Dw?=c(\\x00=v\\xea\\xe9<37\\xe1<YN\\xa7<A\\xd7*\\xbc\\xe6\\x97Z<W\\xc1F<\\x1f\\xe0\\x99\\xbc\\x8b\\xc4<<t\\x7f8<\\t\\xd2\\x07\\xbcr\\xb0?=\\xdd\\x1c\\xba<\\x1d\\xc0\\xc6\\xbaa\\x83N\\xbcq\\x03X\\xbb\\xb1`i<\\xe8\\x1f\\xd8\\xbd\\xbaC\\x8b\\xbc\\xc3\\xbc\\x8e=bTR\\xbd7^q=@\\xa6\\xe0\\xbc\\x88y\\x9a\\xba\\xb4$\\xf2<\\xac\\xfc\\x8e=\\xd6\\xdd\\xe0<\\xa7&v\\xbd\\xb9U\\xaa\\xbd\\xd0T!\\xbd\\x06y\\x02\\xbc\\x82|\\xbe=\\x9ay8\\xbd\\xca\\xa29\\xbcz\\xe6\\x9b=\\x08\\x918\\xbc\\x84$\\xd8\\xbb\\x0e\\xec\\x97<\\x10P0\\xbd\\x1fI0=\\\"y=\\xbdfZ3\\xbd\\x99\\xc8H\\xbc\\xd5\\xa2\\xcf\\xbc?\\xfcH=.K\\x9d\\xbd\\rp\\xa6\\xbcS\\xf74=\\xbe\\x88=\\xbd\\x11\\x80\\x84<F\\\"\\x89=y\\x1c\\xc5<>\\xd4O<9\\x89\\x9e;\\xef\\xd8\\x84\\xbdP\\xca\\x94\\xbcO\\x8b\\x84\\xbb\\xfb;\\xa7\\xbd\\xcd\\xf8l=\\xfc\\xcd\\x92\\xbb\\xa7\\xeb\\xa6<\\xb8\\x19\\xc6\\xbcO\\xd8\\xe7;L\\x0b\\xd7<h\\xd2Z<\\xa5\\n\\x93\\xbc\\xc3\\x8b)=s\\xc7\\xc7\\xbb\\x10\\xe6\\x82==\\x14\\xd3\\xbc\\xf9\\xe4\\x07;\\xffg\\xac<F\\xd2\\xcf;\\xca\\xef\\xc1;\\x12\\xdc\\x8e=\\x86\\xfb+\\xbb\\xae<\\x80\\xbde\\xda5\\xbcQ\\xbe\\xdc\\xbc,2\\x15\\xbbEr\\xef< \\xfb?\\xbc\\x17\\xb1\\x1b\\xbc\\xbfm\\xc9=\\x97/\\r<U\\x83*\\xbd\\x99\\x17N\\xbd\\\"\\xec\\xe3\\xbc\\xf6\\xd7\\xe69\\xa9I7\\xbdA\\xf1\\x8f;\\xa0\\xc6F\\xbd\\x9eP\\x05\\xba\\xfe\\x0b\\xc1\\xbd\\xed\\xb8W=\\xdb\\xbcO;\\x89TL=\\xb9\\xca\\xce\\xba\\x06\\xda\\x99\\xbd==|\\xbcJF\\xd2\\xbb\\x91\\xd30\\xbc\\xde\\xca4\\xbc\\\"z\\x15=\\xac\\t\\x13<k\\x8e\\\"<\\x07\\xcb8<+\\x13\\xf8\\xbc\\xf7\\x94\\xa7=F:\\xd7\\xbcS\\xa6\\xe9\\xbc\\xd3\\xc7\\x07=_P\\x10\\xbc}\\x8a\\xc8<y>\\x89<=l#\\xbdu\\x1c\\x14\\xbd\\x99\\xd7\\xb0\\xbc\\xb1\\xb9\\xf7<\\x18\\xc6\\xf2\\xbc\\x89-\\xe5\\xbc\\xf4\\xc2@\\xbd)bB<\\x971\\xda9I\\xbbH\\xbd\\n\\xd4\\x19\\xbd#\\x8bR\\xbc`\\xc3\\x97:\\x99_\\xb2\\xbc\\xb3N\\x94=\\x81\\xa7\\n=\\xe8\\xb3\\x00=\\xef\\xbf\\x92;\\r\\xc3\\xa8=C\\xc5$\\xbd\\x9c}/\\xbdd\\x9cp</1\\x84<\\x86\\xf3+=\\x03\\xa6\\x93\\xbd\\x1b\\x82\\x15=\\x01\\xf9\\x1c<\\x9e\\n\\xe1\\xbc\\xbagm\\xbc\\xfeM \\xbd:\\xb3\\\"=-<\\x00\\xbd\\x06-\\t\\xbd\\x83\\xe6n<,\\xd8\\x9f\\xbb\\x14A\\x89\\xbd\\x83\\x04\\xba\\xbc\\x9a\\x8d`;\\\\p\\xea\\xbc\\x1cj\\x15=\\xf5)\\x1a\\xbd\\x96E\\xdd\\xbcp4\\xb7<\\xce\\r\\xac=\\xc9\\x00\\x18\\xbdz\\x02\\xdc\\xbb\\xa2#\\xb4\\xbb\\xe0\\x07\\xa7\\xbd\\xf1\\\"%=\\x03\\xeeP=lv\\x91\\xbdp\\xe2\\x7f\\xbc\\x0bo\\x10\\xbd\\xb8\\xfa#=\\xe2\\x1b\\x99=n\\x9c\\x89\\xbbA\\x8a\\xc9\\xbc\\x1e.n==\\xdf+<\\x91.\\x9d\\xbc/t\\x83<\\x9a\\xb0<=NQ\\x12;Z\\xfd\\x87=\\xfd\\x15\\x92\\xbb\\xa5\\x96V<V\\xfbE=\\x0b\\x9d\\xfa\\xbc\\xed\\xbc\\xda\\xbc\\xfc\\xb6\\x19=\\x92\\\"\\x9c<\\xe1\\xcf\\x81:\\xbe8 \\xbc\\x9d\\xb7\\x16<H\\xfb,=\\x18\\xeb\\xe8<hr\\xe7<\\xa4.\\xef:g\\xe7\\x90=.\\x96\\x1f\\xbcS}\\x18\\xbd\\xe4\\xcb\\xa4;5,\\x9c\\xbc\\t3\\xef<L\\x0f\\x8f\\xbc\\xea1\\x1f\\xbdz\\xd9\\x8c=\\xee7\\xe9<#\\xd6 =\\xa8g\\x06<Z/\\r=\\xa0G\\x14=\\xb3n\\xe3\\xbc\\x8fi\\x1a\\xbc\\xc1\\x88\\xd2\\xbb\\xc1\\x07\\x19\\xbd\\xe18\\x04\\xbcm\\xab\\x82\\xbd\\xc3\\xe2\\xa1\\xbb%a\\xc4;\\x9f\\xe2\\t\\xbc\\x06\\x8c\\xb7\\xbc~\\xa4\\x83\\xbd\\xbbrY\\xbc\\xbc2\\x8b;\\xeb\\xb9\\\"\\xbbg\\xbaI\\xbcV\\x8e8\\xbb2<\\xea\\xbc\\x03\\x17\\xb7\\xbc\\xd3\\x90\\x85=\\x85\\x0f\\x9f=R\\xe1H\\xbb\\x14\\xfb\\x1e=\\x03Z\\x18\\xbc#{\\xd2\\xbcL:#=\\xf6\\xacH\\xbc6A\\xa1\\xbc\\xd5\\xf4><\\xdd\\x1d\\x84\\xbbb\\xc9\\x1b\\xbcx\\x1b)=Q\\xa5\\x91<\\xc4\\xce\\xec\\xbc\\xaa9&<\\n\\x11\\xdc<j\\x9d\\xc3\\xbc\\xba\\x17D\\xbc\\xd2\\xf9\\x9a<B\\xcb\\x04\\xbd\\x91\\xce\\xf0<\\x97\\x0f\\xc0\\xbbvC\\xba\\xba\\x16z`=Ko\\x10<\\xa1\\x1e\\xd4\\xbd\\x9d\\x8ah\\xbc\\xddg\\x00\\xbd\\xbc\\xfd\\x1a\\xbdL\\xfa\\xd6;\\xb7\\x96\\xa6<\\x9e\\xc3\\xb9\\xbbW\\x02\\x8e=8\\x18\\x1f\\xbb?K_\\tC\\x9e_\\xbc\\xb2\\x03\\xb2\\xb9\\xb6\\xe1]\\xb9q\\x1e\\xef;\\xdf_\\x96<\\xe4\\x86}<}Y\\xb2\\xbch)\\x12\\xbdFT\\xcc\\xbb\\\\\\xb3c\\xbck\\xf4\\xf5<\\xdf*_\\xbb\\x1b\\xaa5\\xbc|Y{=0\\xd0\\xfd\\xbcW\\x05@=!>\\xb2\\xbc\\xd9\\xc5%<}\\xc6]\\xba\\x83|O=)\\xffX<\\xde!6\\xbb\\xc6\\x9c\\xf0\\xbaw\\xaeF\\xbd\\x87U\\xd7<\\xe2~\\x1d<kZ\\xe9\\xbc\\x06+\\xd3<\\\"\\xdc\\x0c\\xbd?\\x91j<\\xb4\\xce\\x85\\xbb\\xc3\\t1\\xbd\\xb5\\x1dG\\xbd9\\x03C<\\xc9\\xc5\\x05:r$\\x83\\xbc\\x8fa\\x0f<,\\x9f^<5\\x16\\xff<\\xa9D\\xe3;\\x9a\\xf6\\xd4<i+\\xc4<\\x1b5\\x88\\xbc\\xe8R\\xd5:\\xab\\xd53=\\x11\\xf3;\\xbc\\xa1^\\x87=\\xb7\\x06\\xdd;S\\t\\xa1:\\xbdDM\\xbc\\xc1 o\\xbd\\x9f\\x98\\xd0\\xbc\\x976^<y\\xc6\\x97<\\xf5\\xaf\\xf0\\xbb\\xf7\\x14\\t=S\\x0bx<\\xd1c\\xd8<R\\x96a\\xbcB\\xe9\\x8b<w3\\x83<_1\\x0b=\\x1fg\\r=\\x01\\x95\\x1e=\\x96O\\\\\\xbdm\\x92\\x13\\xbc{\\xc6\\xd8\\xbcaUV\\xba\\xf9-f=\\xfe\\x86,\\xbdfI\\x94=EgO<\\xe3\\x89\\xe3;(f\\x1d=g\\x85\\x1f=\\xfe\\xcf9=&6\\xbb\\xbb\\xf7\\xc3\\x9f=\\xf3\\x92\\xed\\xbc\\xa5\\xdb5;\\xd5\\xc3%\\xbc\\x0c\\\"\\x08=P\\x97\\x17\\xbc1X`=W\\x92\\xc6\\xbb\\xb4\\x96l<\\x1d\\xaa\\xea<,2r=\\xf0B\\r=UJm\\xbc\\xec\\xfc\\x01\\xbd\\xe1m}\\xbcD=\\x89<a\\\\<\\xb9X\\xf3\\xc8<\\x8e\\xf0\\xeb<9\\x1d\\x88\\xbd=n%;\\x95p\\x19\\xbc\\x10\\xbb\\x86\\xbc\\xf4\\xbbA\\xbd\\xdd\\x1ar\\xbd\\xa0\\xf9\\x9c<j\\xc9K\\xbd\\xfa<\\x1f=\\x10g\\xaf;h\\x9a\\xec<\\x90\\x83\\x12\\xbb\\xc8\\xaa\\x8d\\xbd\\xdda\\x1a;H\\xfb\\x10;\\n\\\"\\xa4:\\xa5\\xd2D\\xbd\\xace\\xc3=\\xd3\\xfd\\xae<\\xbf\\xbf\\x1c=\\xe4\\xeb,=\\xee\\x9a\\xbf<6\\x97\\xc6\\xbc\\xe2\\x89H\\xbd\\xef\\xb73\\xbb\\xf2]\\x8a<\\xe5\\xee\\x1c=\\xb7\\xe4\\x8d<Q\\xb19\\xbd\\xf2\\xf8\\xac\\xbc\\x7f\\x13\\xf3\\xbc\\xc3\\x91\\xe1<M\\x98\\xaa=\\xfb\\xb7\\x94\\xbd\\xacF\\x98\\xbc\\xea\\xb5,\\xbd^ \\x8d=\\xc9M\\n\\xbb\\xe8\\xe5@=\\x82\\xf1]<g\\x18\\x14=H8G\\xbd\\x06\\xf5\\xc1<\\xcc\\xcdt\\xbc#\\xc4\\xce\\xbc\\x1a#\\x15\\xbd\\xb7P\\x91\\xbd\\x0c\\x1eQ<12\\xf4\\xbc=\\x9e\\x05=_\\x12\\xe6\\xbc\\xa3\\x9a\\x00<\\xb6\\xaaT\\xbc\\xa9\\x0b\\x8d\\xbcX/\\x1f<\\x8d\\x0eH<\\x0b\\x82V\\xbcx\\xa9\\x84\\xbb\\x7f\\xcf\\xbb<\\xf6\\x10!</\\xd3*\\xbd\\x92\\xda\\x19=V\\xf0\\x80<)\\x80\\x88\\xbc\\x12\\xb2 \\xbd\\xeb7\\x99=\\x19oC<\\xca\\x81\\x03=\\x87=\\xfe\\xbc~\\xfc;\\xbc\\x99\\x0c+\\xbd\\xda\\x12\\xe3;\\xb5\\xe7\\xba=\\\"\\x1d\\x92\\xbb\\x1fo#=Ue!=\\xa1\\x8d\\x00;t\\xf3\\xa2\\xbc\\x9d7\\x03\\xbc~P\\x17=\\x81\\r\\x88<K\\x8d\\x05=\\x9d\\xec\\r\\xbci\\xd8G\\xbc\\xb1\\x1f\\x82<\\x05d\\xad\\xbde\\xb2\\x8a;\\xf9#\\x06\\xbdD7[<<\\xae\\xbb\\xbc\\xea\\x19\\r\\xbc|\\xd0\\x9b;\\x15c\\xa7=\\xf6\\xed\\x90\\xbb\\xa3\\x17\\xd1;5\\xb4\\xab\\xbc\\x97\\xf1\\xcf\\xbc\\xa4\\xd2\\x17\\xbdA<\\x0c\\xbc\\x08\\xe6\\xd9\\xbc\\xec\\x98Y\\xbd\\x11\\xba[<\\x1d\\x94\\xee<:\\xc2\\x96<\\xa89i\\xbd\\xfb\\x87)=\\xec\\xe9;\\xbcZeD\\xbd\\x8d\\xce\\xd8\\xbc\\x0e\\xe5&\\xbdf\\xa5!=aD\\xf8\\xbc-\\x8f\\x85<\\x90.\\x1b\\xbd\\x0cN\\xf1;\\xc9\\xe4\\x9c;\\x80\\x954=r\\xa7\\xd3\\xbc\\\"\\xfd\\\"\\xbdc!,\\xbd4\\xac\\xf5<\\xd0\\xe2(\\xbd\\x88\\x94\\xbe8`\\xf0\\x13\\xbd%\\x17\\x02=\\x92o\\x81\\xbd\\xe0\\xf9q=g\\xdeV=\\xfbn\\x81=\\x11&Z\\xbc\\xf1-\\xf6<\\x92`\\x94\\xbcv\\x19B\\xbc\\xc4\\x85\\xab=\\x87\\x97\\xce<\\x1d\\xa4\\x1e\\xbdHF\\xc5\\xbcgb\\xaa\\xbc7E\\xf9\\xbc&[\\xfe;\\xac#g<\\xa1\\xa2\\xcc\\xbc\\xcb89=\\x861\\x959{\\xfe@<\\xa8\\r\\\"=,m\\x05=\\x9bk\\xbc\\xbc\\xa9\\x9e\\x1c\\xbd\\xa7\\xd5\\xc1\\xbb\\xeb\\x9b\\\"\\xbd\\xcb\\x9e6<\\xd9\\xf5\\x99\\xbdr\\x1b\\xfb\\xbcB|\\\"<|\\xba\\xa3\\xbc\\x85{ <\\xd30C=\\xc3\\xc9\\xf3\\xbc\\x0b\\x8d\\x0b=\\x07\\x15\\xde\\xbd\\x8c\\x14\\n=\\x1c\\x86\\x0e\\xbdn\\xd5\\xc7\\xbd\\xd3#A\\xbc\\xd7\\xba\\x14<E-\\xbd<\\xc6\\xb3\\x02=\\xce#l=]\\xb6\\xa1\\xbc\\x18\\xecm<\\x90&K=s\\\\E\\xbd\\x98\\r\\x8e<g\\x14W\\xbc\\x9cHN=\\xd7\\xefI\\xbd\"\nHSET bikes:10026  model 'Quaoar' brand 'BikeShind' price 4557 type 'Mountain bikes' material 'carbon' weight 11.4 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. It has a lightweight frame and all-carbon fork, with cables routed internally. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\x85)\\x02<\\xa5\\x8d\\xe0<\\xc7$\\x18\\xbdI\\x88\\xdc<\\x98\\xee\\xbb<\\xc6\\xe6\\xa1=I\\x15\\xe8\\xbc\\xe6\\xd1\\x8a\\xbd\\xfeL(=\\xb9Z\\x18\\xbbrr\\xfe\\xba\\xc59k=yI\\x16=\\x1e\\xdc\\x91\\xbc\\xf6\\xba\\xa6=\\x83\\xd9\\x9b\\xbd\\x9f0\\xe0\\xb9\\x18|F\\xbc\\n\\x10\\x9e\\xbc\\xdc\\xb6\\x03\\xbd\\xd1\\x0e\\xdd\\xba#\\xd1\\xb3\\xbc\\x93A\\xe0<\\xff|\\xee\\xbcj\\xfc\\x03;\\xb6\\\"\\xb2\\xbc\\x19}\\x88<\\xd7\\x1e\\x88<d\\xe89\\xbd\\xd8\\xd9\\xbc<:\\xc4\\t\\xbc\\xe8\\xb4`\\xbcU\\xca\\xb5<p\\xdf\\x0f=\\x1f\\xfb\\xfa<8\\x06R\\xba_\\x02\\x9c<\\x05\\x8e\\x9b\\xbcj\\xc2\\x15<\\x03%:\\xbc!Q\\x95=\\xa1\\x83\\r<\\xfck\\x9b<8Z\\xc5<_\\xc3\\xd5\\xbb4\\xc9\\xe8\\xbb\\x0f\\xdcG=\\xd6\\n\\xaa\\xbcG\\x01d\\xbc\\xa4O\\xb2\\xbc\\x8c\\xcf\\xe1\\xbcg\\xf8@\\xbdd\\xd66<p\\x1d\\x89<\\xf8\\xdaN<\\x98\\x91?\\xbd\\xa1\\xbd\\xbf;\\xee\\xab\\xd3\\xbdoz\\x9a\\xbd\\xd6Z\\xb0=\\x8e\\xa8\\xfe<\\xe7m\\xa6<s\\x0f8<\\x91*\\x9d=\\xf6J!=\\xdb~\\xee<\\xa7\\xd6\\xea:\\xa9@_\\xbc5\\xafg<\\xb0\\x04\\x05\\xbdR=\\xe9\\xb9\\xd7\\x89\\xac\\xbc\\x90 X\\xbd\\xb4\\x93\\x1f<g\\xb2I\\xbd\\xf1\\xdd\\x82\\xbc|\\xff <q\\xf9;\\xbd\\x11\\\"\\xfc\\xbc\\x11U,<\\x92\\xd7\\xe1<\\x1a\\\"y\\xbd\\x15\\x95\\x8e\\xbc\\xb2\\xc5\\x1e\\xbd\\xd2L\\xff;E%\\xea<\\xc9\\xa6y\\xbb\\x9c5A=\\xfe\\x893=\\xde\\xe0\\xe8<?C\\n<\\xcf\\xaf\\x08\\xbdX\\x90\\xe9\\xbc\\x86\\x8e\\xba;\\x04\\xe0\\x01=N\\xf8\\xb1<\\x04\\xdf\\xdc\\xbc\\xa24\\xce\\xbc2\\xc7g<\\x1cO\\xb6\\xbd\\n\\x9f6\\xbd\\x14c\\xec\\xbba{\\xc3<\\xd4\\x97\\xfa9G\\xceg\\xbcK7\\x9f=\\x7f\\xe3\\x14<_\\xe4\\xe3;\\xf0\\xdc\\xab\\xbcrt\\xa4\\xba\\x8e=\\x93\\xbcmXk=q\\xb2?\\xbc\\xa6t1\\xbd\\x84\\xf1g\\xbd%\\x99(<\\x97\\xba\\xbc:)\\x99\\xa3\\xbc!!\\xee<#\\x84^\\xbc/\\xa0E<\\xf6\\x87@=;f=\\xbd\\tm\\x959%\\xa3\\xba<\\xa19\\xb6\\xbc\\x14ES\\xbb\\xb7h\\x8c\\xbcr\\x12l\\xbc\\xfe2y\\xbdB,\\xc8\\xbb\\xa8\\xce\\xe4\\xbc\\x11V\\xa5<\\xd1\\x8ey\\xbc\\x1b\\xfb\\\"\\xbd\\x95a\\xd9<P\\x98\\xa9\\xbc\\xe1\\x0c\\x95;\\x03\\xdd\\x1d\\xb8/\\xedl\\xbc\\x17%\\x1c\\xbd\\x83#c\\xbd\\xfc\\\":\\xbc*::=\\xe9\\xa2\\xbf<\\x9e],\\xbcHN\\x80<\\x860\\xd2\\xbc\\xc9\\xd7\\xe2:r\\tL\\xbd\\xfah\\x98\\xbcL\\xb1%=F R\\xbc\\x13\\xa7\\x97<X\\\"g\\xbd\\xb1\\xe7g\\xbd\\xe7\\xbe\\xd3:U\\x9d\\xc7<\\xc3\\xed\\x13=\\xaeQ\\x88\\xbd\\xee]\\x8c=\\x10-\\xa6\\xbc\\x9e\\xcb\\x9a;O\\x12\\x87\\xbd\\xbft[;\\xa6\\xf7\\xd9<1z\\xf6\\xbc\\xacY\\xeb<\\xb9\\xa76=s\\x02\\xf5:L\\xcaq\\xba0\\xbc<=X\\x89@;\\xf3\\xfd\\x0e\\xbdZH\\xad\\xbc\\xca\\xc9\\xed<\\xce\\xa0~\\xbd^\\xdb\\x0f=\\x84u\\x98<\\x9c\\x92\\xec\\xba\\xea`\\x18\\xbb\\xd7\\x8aG=\\xe7\\xc8q\\xbc\\xfaAj\\xbb\\xe2\\xe0x=\\r\\xc6\\x8e<BVM=\\xf1\\x1f\\x96<\\x0e@\\xe0<k\\x03K\\xbd\\xb0\\xf9\\xf8\\xbbJ\\xe9`=\\x1b\\xa7 \\xbd\\xffl\\x8f=\\xee\\xce\\xd3<w&\\xa7<\\xc6\\xbc<\\xbdk\\x92\\xdc\\xbc|\\x11\\x82\\xbb!~\\xc1<:\\xec\\x1c\\xbd\\xbb<M\\xbd\\xa9\\xa5\\x14\\xbd\\x04\\xdc\\x14\\xbd\\x90\\xfd\\x18<\\xff\\x1d\\xba<\\x90\\xa1\\xda\\xbc\\xcd\\xa3\\\"\\xbddx+<Zr\\xe3\\xbc\\xf4\\x91I9\\x0f\\x9c\\xf0:\\x12y\\xe9;?\\x90.\\xbd\\x9dw\\x1a\\xbdc\\x80\\xab\\xbb \\x93\\xf7\\xbc\\xa4\\xe6h\\xbdo^n=\\xef\\xc9\\xdb\\xbd\\xe9b\\x80<\\xe8\\xce3\\xbc\\xff\\x08\\xfd\\xbb\\xb1\\xe8\\xbd;,\\x80\\x13;QCb\\xbdQt\\x86<\\xc53&;\\xe0X0=\\x90\\x95\\xa7\\xbd\\xe6|-\\xbd0\\xee\\xd0\\xbcH\\xee,<S\\x8b\\xd7<\\x04g\\xc2\\xbc\\xf0\\xda\\xf2\\xbc\\xf2\\x14J<\\xf9\\xdaC;3\\xe0T\\xbc\\xec\\xa9o\\xbb\\xbb\\xe0\\x88<\\xf6jx\\xbc\\x1d\\xb9b=\\x84D\\xfd;\\xa9\\n\\x8c<\\xb2\\xe6\\xa4<;n8=\\xe8\\x01\\xd2\\xbb\\xa1Q\\x16\\xbd\\xf7\\xfeo<\\x16\\x03\\x91<\\xc0~\\x84\\xbd\\xaf\\xb8\\xe1\\xbb\\xa7|\\x9a=VS\\xa7\\xbdk\\x15S=\\xcb\\xa0\\x17;\\xa3\\xba\\t\\xbc\\x15+\\x83\\xbd\\x91^o=\\x04\\xef\\x00<\\xf7HA\\xbc\\xb0W\\xd6\\xbc\\xb0\\xa3\\x8a\\xbcy(`\\xbd\\xd3lt=p\\xe5 \\xbbX\\xad\\xb5\\xbc\\x98\\xde6=\\xbd\\xaa\\xbe\\xbc\\xb4]w<\\x86\\x97\\x0f\\xbc\\xdeq\\\"\\xbd1m\\xb9<\\xac\\x90S\\xbb\\x93\\xdc_\\xbc\\xa5\\xcaP\\xbb\\x90]\\x9a<\\x1bE\\xdf\\xbc\\x19t\\xb7\\xbd\\x98\\xde\\xe0\\xbc\\xbay\\x95=-\\x1c\\x0b\\xbdK\\xce =\\x006\\x0b\\xbb\\xfa\\xf9\\xc5\\xbcm\\x1c\\xa6\\xbc\\x885\\xe5<\\xa7\\xfd\\x99\\xbd\\x16#1<\\xf1s\\x80\\xbceV\\x19\\xbdH=\\x8f=\\x02\\xaf\\xaf\\xbc\\xfcO\\x1a;1k\\x1f\\xbd\\xe6`E=\\xea\\x1f\\xab<\\\"y\\xc7;\\x8d\\xda\\x0f\\xbd\\xbd\\xa3\\xc8\\xbb\\xbc\\x01\\x00=\\xa0\\xd7U=FXq\\xbc\\x8a\\\"!=h\\xcd\\xe9<sK\\x9b\\xbc\\xec~J=\\xc4F\\xa7=\\xd0\\xfc\\x8e=]\\x90\\xef\\xbc\\xbc\\xcb\\x80\\xbcR\\xef\\xb0\\xbc\\x1263\\xbd\\x92,\\\"=\\xe4\\x94\\x0f\\xbd\\xb0b\\xca\\xbc\\x86\\x9d\\x99=B$\\xa9<DTM;\\xdbe\\x16\\xbcx\\xf3b\\xbdO\\x0e\\xed<\\xe2\\xee\\x14\\xbc\\xb3m\\x90:@\\xb1y\\xbdu%\\x8d<\\x0b\\xd7w\\xbd\\xbdF\\xb7\\xbb\\x03\\x13\\xb8\\xb9IV9\\xbcQ_O\\xbd\\xa6I\\xc8\\xbb\\xea\\x90\\x97\\xbb\\xdc\\x1f\\x1b<\\xf7B\\xa4\\xbcC\\xd1\\x11\\xbd<i\\xa1;.\\xdd\\x86<pF\\xaa\\xbb@\\xe2\\x16\\xbd\\x08\\x07\\xde\\xbb{m\\x84=\\x82O\\xbb8\\nT\\x82:{ C;\\xc9u\\xc7=\\xec\\xc6\\xd9<\\x84\\r\\xa2=\\x8e\\xc9/<\\xa5\\xc09\\xbc}1\\x08\\xbd+\\xd8:=\\xcee\\xdb\\xbcX\\x03\\xea\\xbc\\x06\\x18\\xa4<\\x01# <\\xa6\\xd8\\xe4\\xbc\\x87^\\x17\\xbd\\xb2\\x0b\\xe8\\xbd*g(\\xbc\\xfa\\x08\\xa4<\\x02\\x9c\\xe7<*\\x07\\x8d=\\x0e\\x14o=3I*\\xbc\\xb3\\xc5\\n\\xbd\\x1dL\\x93:f\\xb2\\x87\\xbc\\x9b\\xe4!\\xbd\\x026\\x96=\\rzK;\\x1a\\x97h;\\xb1\\x80(\\xba*\\x90\\xe2:\\x8b\\r\\x00=\\xa2\\x0b\\x8b\\xbc<\\xe5\\x12\\xbd\\xab\\xdd\\x8f\\xbc!\\xb0\\x94;o\\x06\\xd9\\xbc6Q\\xb8\\xbc\\xa1\\xa0\\x04\\xbd\\x91\\xaa\\x92;\\xf2\\x181\\xbdS2><o\\xbbH<\\x0b\\x00\\x7f<\\x8ap\\x07\\xba9\\x93\\t\\xbd\\x0b\\xc8Z\\xbd\\xa1\\xb06=\\xb3\\xcc\\xdb=\\xbag\\xb1\\xbcW\\xc9\\xa1\\xbb^\\xbc\\xde<\\xa5\\xd6$\\xbd\\xb7\\xe5\\x19=\\xdb:\\x16=\\xc6\\xcdA\\xbd\\x86\\xe8\\x04=\\xed\\xc2V\\xbd\\x15o\\x00=\\x9bh\\xbe<:\\x05\\x1a<l\\x05|\\xbd\\x1c;K=\\x02V\\xd4<\\xd4\\x98\\xcf\\xbc\\xbb\\x93\\xf8<B\\xd6\\xa1<\\x14n-=\\x08C:;\\xe0\\xc8\\xff\\xbc\\xf2N\\xc3<\\xdf\\x819=\\x01=\\xc4\\xbc\\xd0\\xf6\\xc3\\xbdV{\\x19\\xbd\\xc0:\\x00\\xbcA\\x18\\xbe\\xbc\\x10\\x16\\x03\\xbd\\x9d^M<\\x010;=u>\\x97;(W\\xd2\\xbc\\x98\\x0fn\\xbd\\xde\\x84{=+r\\xe5<y\\xb3\\xb2\\xbb\\x83AT\\xbcD\\x0c\\x99\\xbc\\x03R\\xed\\xbc\\xc2n\\x07\\xbd)\\x84\\x01\\xbd+\\xca\\x8c=\\xa4\\xf2\\x80<\\xd7A)=&\\x96\\x9f<cB\\xe4<\\x17\\xcc\\xd4\\xbc\\x13,\\x8b\\xbc\\xb5\\xb7Y\\xbc\\xd950\\xbc\\xc1\\xba2<4@\\x11=.\\x17\\xae<\\xa2}\\x99;Tl\\x9e\\xbdwSV=\\xddd\\x8d\\xbd`\\x15&\\xbd\\xb7\\x11\\x92\\xbc\\xcd\\xf2\\xda\\xbdyp<<\\xb4\\xe9L<dk\\xca\\xba\\xd8\\r\\x9a<\\nu\\x0b<f\\x7fQ=\\\"\\xb4\\x03=\\xd3\\xbb!<\\x01\\x005=\\xf5\\xd73\\xbd\\xdc\\xd5\\xc8;f(i<M\\xf7$=]\\xb5a\\xbd^\\xef\\x80\\xbb\\x0bt8=\\x1e\\xb6+\\xbc\\x02\\xed\\xdb;Kt\\t\\xbc\\x9f\\n\\xb6<e\\xf6\\x9d<\\xeb\\xb0\\x83<X\\xfe\\x9d\\xbc\\x0c\\xda\\xaf<\\xa5:B\\xbc{\\x82\\x9e\\xbc%\\xaa\\xfc;\\x80\\x04\\xdc\\xba\\xd8\\xb2#\\xbc\\x91Fl<\\xf8wM\\xbc5s\\x8c\\xbd\\xf8\\xae\\x03\\xbc\\xb8\\x92\\x18;G\\x1f\\xd2\\xbb\\xdf\\x9c\\xc7:\\xd1\\xd5E\\xbd\\xf1\\xd6\\xc6<)\\x0b\\xfd<5\\xf8\\x9f;J\\xddP\\t\\xad\\xea\\xb4\\xbbN\\xd2\\x81\\xbd\\xb4\\x9c}<\\xd1\\x07\\x00\\xbc\\xf8\\xce\\x1f;+\\x85V=\\x11/P\\xbc}\\xb0\\x04\\xbd\\xcb2\\x11=;\\x8e\\xf3\\xbcK\\x83-<\\x93\\xf6\\x03=\\xddP\\x0e\\xbd\\xadpV=\\xb7&F\\xbb)\\xe7\\xee<\\xd6e\\x82\\xbd\\x12\\xb2\\xae\\xbb\\xc4\\xf0\\xe7;~@Z=P\\x97\\xd7\\xba\\xd8Z\\xfb\\xbc\\xc5\\xdf\\xe7\\xbci\\xd1F\\xbd\\xd7II<\\x81u\\xa0\\xbc\\x82\\xe8\\xb4\\xbb\\x85|\\x13\\xbcW+\\x05\\xbc\\xaa\\x14|;\\xd8S\\xa5\\xbaZ\\n\\xea<H\\xefK\\xbdqU\\x91\\xbdI\\xc5\\x8d\\xbcGd^\\xbco\\xdf\\xfb;\\x05,\\x86<\\x01\\x9a\\x02=\\x84{\\xff<\\xa8\\x9c\\x9c\\xbc\\xaf\\xac=\\xba\\xbb^\\x8e;\\xf0\\xf2\\xe0<\\xf4Y:=b\\x01\\x81\\xbc\\xf9\\xd6\\xae=t\\x8cR\\xbc\\xb2\\x96N\\xbdx\\xf3\\xe6\\xbc\\xd5s\\x1e\\xbd\\xb7\\xb9\\xdb\\xbd\\xcc\\xca)=\\x1cx\\xea;\\x8d\\xc1\\x84<\\xba\\xb1\\xce<9\\xaf\\xc6\\xbb\\x04\\xb6%=\\xee\\xa9\\x0e=\\x7f\\xda$=\\x15\\xa54=)\\x86\\xbd\\xbc\\x0f/\\xa4\\xbb\\xc3?s<\\x88@\\xd7\\xba\\xe0\\x883\\xbd\\x8fG*\\xbd\\x8b\\x9f4\\xbc\\x04\\xc3\\xf0\\xb96\\xcaj<\\xab\\xac&=\\x97\\xf7\\xf5\\xbb3g\\x17\\xbb\\xa9\\xf1U=\\xc4o/=f6?\\xbd[\\xf4\\xca\\xbcggr=\\xb9\\xf9,\\xbd8\\xf0\\x81=Hl\\x96\\xbc\\t\\xf4E<\\x8d\\x9e\\x18\\xbd\\x06J\\x19=\\x80m\\x16\\xbd&W\\x03\\xbc\\xc4~==\\x9aj\\x1c=m\\x95\\n\\xbd\\x05\\x84}<\\xb8\\xb6<<\\x89$^<$fC=\\xac\\x92T:\\xa9]s=O@2=%K\\xca\\xbc\\xb7\\x03\\x00\\xbcZ\\xf5\\x8e\\xbaH\\xea\\xf7<@1\\x84\\xbc\\x9b\\xd5\\\"=\\xa8\\xda\\xbd\\xbc\\xd8\\xa7\\xee\\xbc:]\\xfc<6\\xb9_\\xbc\\xe2i\\xcf<v\\x01\\x03<\\xba\\xe99\\xbd\\x8aIC\\xbd\\xc7h\\x01=\\xe4\\xb2/\\xbd\\xf5\\r-\\xbd\\xe13a=\\x05=\\xda=\\x15\\xfd&=\\xf5\\x96|<\\x18\\xa5A<>|\\xc3\\xba48\\x97\\xbd\\xf7\\x87J\\xbc]?\\x8b<0\\x14\\xcc:\\xee\\xf9\\xbb<P?\\xa5=>\\xd75\\xbc\\xaf\\t\\x81\\xbb\\x15\\xdaC<Ow\\xa9=w\\xe81\\xbd-\\x0cN\\xbd\\xe3Wn\\xbd \\x03\\x18=\\xc5\\xfa\\xe9<\\xf1\\x0f\\xbe=^\\x1f\\x98<\\x08\\x03s=D\\x00\\x16\\xbd\\xdciK<\\x8a\\xb0%\\xbbD\\xb5\\xcb<\\xd3\\xab\\xe0\\xbcl\\xb6D\\xbd\\xb9n\\x94\\xbb.\\x8d\\xdf<\\\"hK\\xbd\\x83\\xb4\\xb4<k\\xeb{\\xbc\\xb0[)=w\\r\\xd7\\xbb\\x98*)\\xbd\\xe9e\\x11<\\xd0\\xef\\x86\\xbc\\xe0`\\x13=c\\xad#=\\xe5M\\x98<\\xe4\\xd2y\\xbdM\\xf07=\\xa1\\xf4l<\\\"]\\xa5<M\\xdc5=\\r\\xd7\\xfc<Y\\x8e\\xa8\\xba\\xd9}0<\\xafm\\xcc\\xbb^=\\xf8\\xbb\\x10\\xcd\\x18\\xbc5*l=\\xac\\xd6 =\\xcc\\xa5\\x1e\\xbdn/R=\\x1c07;\\x96+\\xac\\xbb~ J\\xbc\\x806\\xfa\\xbc\\xc5\\\\\\x97=K\\xce-\\xb9\\xd5\\xa7\\x90<\\xd4z\\x1c=-\\xb0\\xe2<0\\xb3\\xa2\\xbd\\x16\\x93\\x05\\xbd\\xb6j\\xbe<\\xbe\\x06\\xf6\\xba\\x88X\\xba\\xbc\\xda\\x80\\xec;\\x0cU)\\xbb\\xfcQ\\xc4\\xbc\\xf8\\x8d/=\\xb3A\\xcc<\\x9a\\x16\\x84\\xbcz\\xe2d<\\xaa\\x1c\\xb4\\xbc\\xf9b\\x10\\xbc\\xbdi\\xc3\\xbc\\x01\\xf8t\\xbd-&\\x08\\xbd\\\\F\\xa8\\xbc\\xda\\x0e\\x04\\xbc\\xfd\\xd0\\xa1\\xbc\\x84\\xc5\\xfc\\xbc?\\x0c\\xa2;\\xc8+\\x11\\xbdr\\xfey\\xbdEr\\x92\\xbc\\xa4\\x17\\xde\\xbb\\x1c\\xdd\\xd0<rq*\\xbc\\x92\\xf9Q\\xbc\\x85\\xd0\\x8f\\xbd\\x18\\x85\\xcb;q|\\xa1;W]\\xa5<\\x90\\xc9\\x96;\\xdeDG\\xbc\\xeds%;\\xcf\\x97=<Sb\\x1b\\xbc\\x18jY\\xbd^\\xb9\\x16\\xbd\\xa8EX\\xbd\\xb1\\xb2\\x99\\xba\\xf43\\x18=k\\x9eR=9\\xab\\xbc=\\xf9\\xb1\\x1b;z\\xbe\\xc6<\\xd9\\xfe;\\xbd\\xfe\\x17(\\xbd\\x9e\\xaa\\xcf<I\\x82\\xd2:\\xaa\\xf1\\xf3<\\xfc\\xc5\\xf7\\xbcm\\x19\\xb7\\xbc\\xaf\\tl\\xbc\\xcb\\x04\\x8d\\xbd\\t\\x8c\\xa0\\xbc\\x9e.\\xec\\xbcNx\\xbb<E*\\x00\\xb9\\xb9\\xdf\\xa5<\\xb0\\x13!=r9(<P>\\x14=i\\xfa<\\xbc\\xa8\\x1a\\xe7<\\xcb\\xf7\\xe7\\xbc\\xdd\\x19\\x96<\\xf6\\xde\\x8a\\xbdl\\xfa\\xa4<C\\x95+<\\xc9V\\xc8<\\xc2\\x90\\x10\\xbc\\x0b\\x89\\x9d=s\\xabl\\xbc\\xc5;\\xdb;\\xd3S&\\xbc0\\xeb\\x91\\xbd\\xe7+\\xe6\\xbc\\r^\\xc5\\xbd\\x1f\\x8d\\xa8<9\\xe7\\x7f=-Z\\xed<\\xd4g\\x02=\\xc3\\x92\\xf5=\\xe1SR9%\\xd1\\xb2=\\x93D#=\\xd9\\xbd\\xb7\\xbb\\x00zW=u\\xaf\\xbe:j\\x93\\x1f=h}\\xac9\"\nHSET bikes:10027  model 'Oberon' brand 'Ergonom' price 4779 type 'Road bikes' material 'alloy' weight 9.3 description 'The bike has a lightweight form factor, making it easier for seniors to use. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It’s for the rider who wants both efficiency and capability.' description_embeddings \"qX\\x1d\\xbc\\x8eg8=-\\n\\xba\\xbc}\\xf3\\xd7</\\x16\\xa4\\xbcd\\x99\\x16=O\\xba\\xd5\\xbc\\x13Z\\x14\\xbd\\xc8O\\x81=\\x8a\\xf4\\xa7;\\xbaf>\\xbc\\xfc\\xb1]\\xbc\\xe4)\\x06=\\xeeq\\x96<6\\xe3g=\\xdc\\x852\\xbd\\x18\\xe3)\\xbc\\xf3\\xfc\\xfd\\xbb@f\\xfd:ky\\xaf\\xbd{n7<h?\\x8a\\xbdn\\xdc\\\"=\\x89\\x90O\\xbc\\x7f\\x968=\\x1a\\x16M<(\\x830\\xbc\\xf99#\\xba\\x8e\\xcd\\x12<\\x82\\x90\\x14=\\xd9se\\xbc\\x01w`\\xbd\\xf7W\\x92\\xbc)\\xf4\\x7f<f\\x92/=|)\\x08<\\x10\\x06\\x17\\xbb\\xa95\\xbd\\xbcP\\\\\\xf5\\xbbx\\x1f\\xb0;B\\xd5\\xfa<\\xe9\\xef\\x05\\xbc\\xae\\x11\\r=\\xb4\\xc4\\xb1<\\x1e\\xbd\\xc5\\xbb\\x15\\x0f\\xc5\\xbc\\x18\\xe2\\xb4=\\\\\\xa1\\r\\xbczw\\x82\\xbc\\xf6\\xbc\\xa3<\\x9b\\xa2\\xe1;R\\xf4\\xaf\\xbc\\xdd\\xbe\\xe3\\xbc\\x1c\\x17\\x88<\\xa6\\xb24<\\\"\\xef\\xd0;\\x82s\\x84\\xbb\\xd5a\\xbe\\xbdW\\xaa\\x1d\\xbd\\xeb \\xb0=\\xfbW\\x16=\\n\\x8e4<\\x81\\xf6\\xd4<p\\xb6P=\\xe4\\xa6\\x13=\\x89\\x8f\\xb4\\xbc\\xf5\\xc0\\x94\\xbc\\x00?\\\\<x\\x84\\x1c<\\xb2\\x83\\x84\\xbc*A\\xe6\\xbc(/\\x1b\\xbd\\xf45\\x0b\\xbd\\x0f\\x0cz\\xbb\\rY\\x0e\\xbd\\x89\\xbe*<\\xb57\\x92\\xbb\\xefs1\\xbd\\xb5\\\\c\\xbdE\\x82\\x88\\xbcW#&<yL,\\xbd\\xa0v\\x1b;\\x96&\\x81\\xbd_\\x8e\\xaf;\\xa9\\xd4\\\"=;\\xea\\x02<\\xf0\\x9b\\xaf\\xbbh\\xbd\\xcc<\\x12j\\x1c<\\xa9\\xa7\\xb1\\xbc\\xb6\\xa3\\xbd\\xbc\\xa8x!\\xbc\\xed\\xfe\\x11<\\xda\\\"\\x1f;:\\xf4\\x19<W\\x862\\xbc\\x88\\xe9$\\xbd\\xfd\\xc8\\x0f=\\xa9\\x7f\\x1e\\xbd(\\x90\\xf4;\\x85\\xb41=s\\x1d\\xfd<\\xc2\\xed\\x83\\xbc\\xf99\\xca\\xbc\\xd1\\xed\\xc6=\\x14\\xcb\\x05=p\\x88T<\\x84\\t\\x15\\xbc\\xb2g\\x96<\\x0b\\x02C\\xbb\\xd0{\\x88=6Ml<%\\xc5\\xa3\\xbc`\\x10\\x04\\xbdk\\xa7\\xb9\\xbcQ\\x0b\\x82\\xbc(\\xf6\\xa9:?~2=\\xcd\\xc0\\xf3<\\xb2=\\x82<\\xdc\\x02P<\\x8d\\xbai<\\xc2\\x96\\xe7\\xbcc\\xfc\\x83=hJO\\xbd\\x9d]\\xcd<\\xf6$\\xca\\xbd\\xe3\\x82v\\xbc~>_\\xbd\\xac\\xc7\\xe4\\xbb!\\xad\\x15\\xbd#uJ=\\x95\\x0f\\x00\\xbd\\xac\\x10T\\xbcC\\x8b\\xfb<\\xf3J\\xdb\\xbc\\xdfop\\xbcg\\xc9%<\\x1c]\\x92\\xbc\\x95\\xabI\\xbc\\t\\xa5S\\xbc\\xb1\\xdd\\xb2<\\x0c\\tD=8\\x85\\xc4<\\x8d\\x08\\x11\\xbd*\\xa9\\xb8;u\\xba\\x00=\\xcb\\xf2\\xf0\\xbct\\x8b\\x0b\\xbc.\\xe5\\x8e<\\xbb\\\";=3N}\\xbdSN\\x16=\\x8a@\\x0c\\xbdG^\\x94\\xbb\\x94yZ\\xbd\\xb5q\\xdc;\\x04\\xb1\\xfa\\xbb\\x8b\\x07]\\xbdE\\xdf\\x88=\\x1cZ9\\xbcR4c;Qqt\\xbdL\\x06\\xfe<CN%=\\xeb\\x12\\x87;\\xfcc;=\\xf7gk=\\x1e\\xd4\\xd1;\\x12\\xec\\x10\\xbc\\xc5-4=U\\xdb7<\\xdc5I\\xbd\\xf0\\xfe\\xaf\\xbc\\x86\\x12\\x08\\xbd0W\\xf3\\xbc\\xc6\\xba\\xa0=]\\x9f8<A\\xe0\\xde;`Mj\\xbc\\xd0X\\x06;\\x0c\\xe0H\\xbdb\\xeb\\xad<i\\x1f\\x12=sO\\x1e\\xbd\\xd4\\xa2k=\\x97)I\\xbd]7\\x1c=H\\xa2U\\xbdi\\x8c\\xa1\\xbc\\xb2|\\xf8\\xb9\\x1b\\x08\\x05\\xbd\\xf3<\\xc3;\\xe1\\x93\\xdd\\xbb\\xaf\\\\.=\\xc7\\xba\\x88\\xbc\\x06\\x96\\x13\\xbd\\xa8\\xeb\\x9f\\xbc\\x04?\\x83=\\xb9\\x9a\\xf2\\xbc\\x8c\\xc6\\x9d\\xbd\\xb0\\xde\\xde\\xbd\\xe3\\xb8\\xa8\\xbc\\xf8?\\xf1\\xbb\\xc2\\xc4\\x1b=\\xea\\x8c\\xf7;\\\"\\x00\\x8e\\xbbC\\xab==w\\xe04\\xbd\\xe3\\x8f\\x83\\xb9\\xae\\x900\\xbc,jQ=\\xb7J\\xe0\\xbc\\xd3Q\\xd8<\\xa1\\xaa\\t\\xbd\\xbe~\\xb0<\\\"\\x1c8<\\x08A\\\"=\\xa7\\xc0\\xff\\xbc]\\xa3{;f\\x8a\\xce;\\x1a\\xbcY\\xbd\\x93\\x00\\xb6;\\x8c\\x00E\\xbd\\x88\\xa2\\x1e\\xbd%E \\xbcx\\\"\\xd4\\xbc\\xeb\\xe8\\xe5<\\x0b\\xf2_\\xbd#\\x8a:\\xbd\\x9b\\xbd\\xd9\\xbcb\\x03\\x8b\\xbc\\xb7\\xfcG=*\\x94\\xbe;bq\\x13=:\\xf3\\xaf<\\xd9L\\xd3<\\xd8\\xe5\\xcd\\xbc*v`<\\xea\\x8b\\xc0:\\xfc.\\x82\\xbb=\\xea\\xcd<\\x9fa\\x9c<\\xdf!D\\xbc\\xb4\\xb7B=mK\\\"=\\xc9?\\r=\\xe0\\xb1q\\xbd\\x00\\x7f&=\\x87\\x11\\x0c<c\\x05\\x8a\\xbd+\\xf5\\xa6\\xb9\\xf0\\xd1}=]{\\x91\\xbd;|\\xab=\\x81\\xb4\\xd5\\xbc&k}\\xbb\\xb4\\x02\\x96<<\\xd2^=\\xf1\\x95w<wHw\\xbb\\xb6\\xe4\\x13\\xbd\\xfb \\x1f\\xbd\\xa0\\n\\x14\\xbc\\xb5S\\xd4=%\\xdb$\\xbdSyQ\\xbc\\\"\\xc61=\\xbd\\x88Z<k\\x15\\xeb:n\\xe4\\x9a\\xba\\xe1\\xe7W\\xbdJ\\x0e\\x02=\\x82\\x9c\\xa4\\xbc\\xad\\xc5\\xcc\\xbc/\\xe1\\xaa\\xb9\\x14\\x0b9\\xbb\\\\\\x1aG<!\\xe3\\x8d\\xbdh(\\x01\\xbdPm\\x92<J\\x13\\x13\\xbd\\xae\\xda?<?/\\xb6<\\xeb\\xb0O<\\x92\\xdb\\x85\\xbc\\x99\\xaf\\xf5<\\x97\\xda\\x9e\\xbd\\x87M\\xd1<\\x1b\\x85\\xcf\\xbcFq9\\xbd\\x01\\xa8\\xad=\\xa4\\xb7C<\\xa1d\\xe7\\xbc\\xce\\xeb\\x86\\xbc\\\"q\\xdb<\\x16\\xf3\\n\\xbb\\xf3\\xeb\\xf4<!\\\"\\x9d\\xbd\\xbe\\xe9\\x14=np\\xbc\\xbc\\xd5 \\x81=\\x06VS\\xbc\\xc8\\xd2\\xe0\\xbb!\\x0b\\x11=\\xc6W{<b\\xd4\\xcc<,\\xe5X=>\\xd01<0\\xe5\\x1d\\xbd\\xf9\\x11\\xf5\\xbb\\x92\\xe7\\xbe\\xbc\\xad\\xd8s\\xbc*\\xff\\xc3;%H\\x08\\xbd\\x8eX~\\xbc\\xf0\\xca\\x8b=\\xae\\xc1\\xa5<t\\\"\\xb3:\\xf6\\x10\\xbc\\xbcC[[\\xbd\\x1d\\xc8#<\\x14\\xdc\\x1d\\xbdD-\\xb0<\\xad\\xa8D\\xbdx\\xbd\\xfc:K\\\\\\x80\\xbd\\xe4\\xe6\\xfc<\\xaf%\\x1e\\xbd\\xa4\\xa6\\xb6<\\x01)M\\xbd\\xf7\\xbd#\\xbd\\xe1\\x99\\xc0\\xbaO:S<~\\x04\\x92\\xbczO7\\xbd\\x11\\xcb\\x05<\\x08`\\x03=\\x03`Q;\\x14\\xc2\\x8091\\x95\\x1d\\xbd\\xeewx=p{\\xf0;Ui\\x89<\\xe7\\xde.=@\\xc9\\x80=1\\x81y<WA0=\\x9b\\x14\\xac\\xbc\\xa6~\\x86\\xbc\\x90E\\x8a\\xbc\\x8b\\xd1\\x96=\\xa4%z\\xbd\\xf4\\xca{\\xbd%M\\x1f\\xbdg\\x1c@<^F\\xc4\\xbcq\\xf0z\\xbc\\xad*\\xf0\\xbd\\xa2W\\xa5\\xbcs7\\x13=\\xe3R\\xeb\\xbb\\xf6e\\x84=\\x0c\\xc36=\\xc9\\xa7\\xaf;f\\xc9S<\\xab\\xd0\\x04=\\x1fQ\\xf3\\xbc\\xeb|G\\xbdR\\x0b\\xda<{\\xa9\\xf7<\\xcb\\x92\\xbc<\\x99P4\\xbd\\xc6\\x02\\x8b;O\\x16\\x1d=\\xc2\\xbcm\\xbcph\\x9c\\xbd\\xbe\\\"\\xbb\\xbc\\xdd\\xd4\\xa9<E\\xb2\\xf8\\xbc\\x85*\\x03\\xbd\\xf7.\\xa2\\xbc\\x0e\\xea<\\xbc\\xcd\\xcc\\x9b\\xbd\\xdb\\xe1\\xc9;W\\x8d\\x1e\\xbc\\xe8|\\xe4\\xbc\\xe4\\x1f\\xbd<\\x81\\xa1\\xc9\\xbcZ\\xee1\\xbd\\xe9\\xe1\\x8b=\\x1b\\\"\\x86=\\xbbT\\\"\\xbd\\xebA\\x01<\\xb9Ci<\\xbc\\x013\\xbd\\x13\\xc5A<R\\x92\\x86=\\x9cd\\xac\\xbdN\\x07\\x11\\xbc\\xefd\\x10\\xbcs*&=\\x9b@C=\\\"f\\xb5;\\xc8N?\\xbdc=\\x93=\\x04\\x18:<\\x9d\\x0f&\\xbd\\x8bFq=\\xbf\\xa9\\x82=b\\xe6\\xf9;\\xcf\\x039=\\x93\\xad\\xe2<\\x8dQF=4l\\xf2<\\xe4\\xd4$\\xbd\\xb5W}\\xbd\\x91>\\x9e\\xbc\\nC\\x11;\\x9f\\xd6\\xd5\\xbc?\\x9c\\x99\\xbcw\\x06\\n\\xbc\\xcdD\\x12=R\\xb3\\xbe;z{\\xb4\\xbc$\\xff\\x82<\\xecA\\x88=\\x96\\xcc\\xb9\\xbb\\xce\\xa8X\\xbd\\x1d\\xbb\\xcf\\xbc\\xe1\\xe8\\xac\\xbb\\xb8\\x1e\\xaa\\xbcbc\\x1c\\xbd\\x87Uy\\xbdDd\\x9e=\\x02\\x9f\\x8e=\\xbfS\\xe3<\\x8f\\x95\\xc3\\xbb\\xaeO<=|X(\\xbc\\xcc\\xa6\\x0f\\xbcE\\xfe\\xc9;BI&\\xbc\\xf7\\x89\\xc7\\xbc\\xffwI<b\\xda\\x95\\xbb\\x8dq\\\"\\xbd\\\\C\\xbc\\xbb`\\x14\\x18=8[\\x89\\xbd\\xb3\\x1c\\x8f\\xbd\\x06\\x8bc\\xbb\\xc2\\x03~\\xbd\\xb1\\x00D\\xbc\\xd3d\\x88:-\\xd4\\xfc<:\\xe6\\x99<.\\x00\\x01\\xbd\\x96\\x9e\\x13=\\\"\\xc1\\xac=\\xb0\\x9e\\xb3\\xbc\\xe1\\xd0\\x91=n\\xd7\\xa0\\xbb\\x17\\xde\\xd0;\\xc6\\xed\\xa8=\\x01\\x94\\xd2\\xbc\\x0ci\\x14\\xbd\\x1d\\t\\x1b\\xbd\\xc5\\xf5\\x10=Abn\\xbc]\\xfb\\x9b<\\x1bJ\\\"\\xbd\\xe6.\\x1f\\xbc\\x1e\\nB;\\x01\\x10J=\\x9b\\x0c\\x0b\\xbd\\x13F:\\xbc\\xde\\xee\\xe2<Co\\xbb\\xbc\\xc9q\\xc4;\\xe88\\n<\\xcb\\xb5\\x85\\xbb\\xab\\xfa\\xed<E\\x1d\\x12\\xbd\\xdf\\x06\\x07\\xbe\\x04\\\"\\x02<\\x85.*\\xbcK\\xec\\xbc\\xbcrZ\\xef<S9p\\xbc\\xa6\\x02\\xbd<\\x8a\\xa0\\x81=\\xedt\\xcb<\\x8ac`\\t\\x85\\xdb\\xd0\\xbc\\xbe\\x96\\xbf\\xba\\xb9l\\xe8<j\\xbcX=dd\\xbb<\\x1f\\xae\\xc1<\\xaeP\\xfb\\xbc\\x9c\\xbd\\xc7\\xbcJ \\\\\\xbb\\x9c\\x00\\xe6\\xbcG\\xb5v=\\xb1\\xb1\\xfc<\\xc7HB<X^A=@\\xfb\\x05\\xbc\\xbc)P=\\x8d\\xc7<\\xb9\\x01\\xe4\\x89:*\\xf0\\xf7\\xbcA\\x06\\x17=\\xb6S\\x0f=\\xf9t;\\xbb\\x1bL\\x06;\\x9c\\xcbh\\xbd\\x97et<\\xee5\\xa9\\xbb\\x07L\\xdc\\xbb}k4;0\\x12\\x11\\xbd\\xca\\xb2\\xb5<z?\\x8a;s\\xe9\\x1a\\xbd\\x88T\\x17\\xbd\\xde\\xd4n\\xbd\\xdeH\\xda\\xbc\\x17u\\\"\\xbbm\\xbco<\\\"\\xf5\\xab<X\\x08\\x02=\\xc2\\xb5d;\\x10\\x00R;.\\x03-=\\xe8W\\x84<\\x9e\\x04\\x11=O\\x1a\\x93<\\xcfk\\xc0\\xba\\x88F\\x82=\\xc4)\\xcc\\xbc\\x92\\r\\xbe;8_!\\xbc\\xfe\\xa1\\xe8\\xbcGH\\x19\\xbd\\x96i$=\\x19:\\x96<\\xc3\\x03\\x90:\\xe2\\xcaV=\\x12\\xba\\x07=\\xc5\\xd1\\xae;\\xc4O\\xa1<.\\x17E;\\x11\\xcf\\xb5<\\x17+\\x88<q\\\"\\xab\\xbc\\x92@\\r=Oys\\xbdg\\xa0)\\xbd\\xb0\\x15y\\xbcVW\\xa7\\xbc\\x82\\xcaT=0\\xea\\x93\\xbc\\x1dw\\x96<\\xba\\xaa\\xa3\\xbc\\x17Z\\x8e;\\xb2\\x7fA=0I\\x91<\\xc6x\\x90<v\\xa3\\xc7:\\xed\\x1e\\xa7=\\xcb\\xa4\\xa0\\xbd9]\\x8c=t\\xec\\x8f\\xbc\\x8f^d;\\xb5L\\x13\\xbc\\xe7\\x8dL=\\x02qN\\xbd3\\xf7\\xf6</\\x05!=\\x129j=6Z,<\\x19\\xb5\\x8e<!\\xc2~\\xbc\\xae\\xda@<:\\xe2\\x04=\\x01\\xe1\\x01;\\xc7\\xffj={\\x80 =\\xd37V\\xbd|\\xe4\\x0e<;\\x88\\x97<V\\x02\\x1a=`I\\xeb\\xbc\\xf6\\x99\\xd8\\xbc\\xe6\\n\\x1f<\\x98Y\\xf9\\xbc\\xbe\\tj=Z\\x95,\\xbd\\xf0i\\x01=O\\x86o\\xbc\\x02\\xf4n\\xbdr\\xa0\\x03\\xbb\\x02\\x9b\\xdd<\\xf6\\x9c\\xf5\\xbc\\xbf\\x83&\\xbd\\x84\\\"g=\\x10\\xb1\\xf2\\xbb>\\x9a\\xf9\\xba\\xe0\\xdez=|\\xf8\\xae\\xba+E\\x11\\xbcT\\xdfG\\xbd\\xc1\\xa83<\\xadK%\\xbc\\xfdL*=\\x8e\\xaaz\\xbc\\xffM\\xd2\\xbav\\x08\\xbd\\xbc\\xfb\\x0br\\xbds\\xc9\\xe2<\\xeb\\x8c#=g\\xd1\\xa7\\xbdEN|\\xbb\\xd1\\x08u\\xbdq\\xe1\\xa2=\\xcfN\\xc4\\xbc\\x19\\x19\\x89=mB\\x16<\\xa6\\nw=\\x8b()\\xbd\\xaey\\xb9\\xbc7\\x9e\\xe5\\xbc\\xa5\\xdd+=\\x98\\x81f\\xbdF\\x8f\\x1f\\xbd\\xd1R\\x0b;\\xdbd[\\xbc3L\\x14<\\x9f\\xfc \\xbd\\xeb\\xdeo;\\xc9\\xb9\\x11=S\\x1d\\x91\\xbcv\\xe1\\xd5\\xbc\\x9a\\x98h\\xbc\\x8f\\x8b\\xff\\xb9\\x80\\xa2\\x0f=;Q#=\\x82\\xdc\\x1c<\\xf1-R\\xbd\\xfbN6=\\xb5) =\\xf3oB<\\xaa\\xfaD\\xbd\\x0e\\xaf\\xc6=2\\xeb\\xb2;\\xbb\\t\\xe5\\xbc@\\xb8\\xdc\\xbb\\xc5#\\xe9\\xbckXW\\xbdQ#j<\\x0bp\\xa7=\\x19\\xf0\\xf4\\xbaY\\xf8\\xe3<\\xf7\\x80\\xa3\\xbc\\xe7\\x86\\xe6<\\x1a\\xbd\\x85\\xbc\\xfaA)\\xbcY\\x1c4=\\xec\\x7fm;k8\\x04;/N\\xf7<\\x04\\xa7I\\xba\\x16d\\xfb\\xb9+s\\x99\\xbd\\xc3\\xf3\\xde<\\xe0&L\\xbc\\xcfN\\xca\\xbcEay\\xbc\\xfc\\xce\\xef<\\xd6\\xf8\\xee\\xbb\\x96^|=\\\"\\xc9\\xd1\\xbc,\\x1f\\x06=\\xa6\\xd6\\n=1\\xcd%\\xbd\\xce\\x17\\x91\\xbcm\\xf5j\\xbd\\x00\\x92\\xb2\\xbc\\xbe\\xf5@\\xbd\\xc7\\x9cc<\\x05-A\\xbc\\xc6\\x06\\x95\\xbc\\xd8\\x16\\xf3\\xbc\\xd4\\xf2\\xbf<|\\xf9\\x8a\\xbc\\xc6\\x1b~\\xbd\\x12\\xb9\\\"\\xbd*=J\\xbd0]\\x08=\\t\\x0c\\xa2\\xbc\\xc1\\x16O<\\xb0\\nG\\xbd\\\\\\x91\\x97<\\xe7\\x08\\x1e;o\\xefZ<\\xb5\\x18\\x84;\\xc3(\\x8a\\xbd\\xd4\\x15\\xe8\\xbb\\x9d\\xc1\\xea;\\x01\\xef\\xcf\\xbci\\xbc\\xb9\\xbc\\x86\\x82\\x18\\xbd\\xc0\\x9b\\x86<sL\\x80\\xbd\\xf1\\xeeG=\\xec@\\x1c=\\xf3\\x8b\\xa2=\\x8c\\xdc\\x89\\xbb\\x9e^g<R\\xff\\xe4\\xbb\\x0b\\xbe\\x8f\\xbc\\x84\\xcc8=\\x19\\xd1\\x12<\\xd9\\x97\\xc3\\xbaE\\xb4\\xff\\xbc\\xc7\\xca$<N\\xeb\\x05\\xbd@\\xc3\\x06\\xbc\\n\\x0em<D\\xa7G\\xbdK\\x85W=\\xacZ\\xe8<&u\\xc3<$\\xb3\\xf1<\\xb9\\xf8\\x0c=\\xda=\\x15;\\x04\\xcd\\x1e\\xbd\\xb39\\x89<\\x80E\\x11\\xbd|4\\x13=\\x7fC\\x80\\xbd\\x92\\x1a%<\\xa7\\xf7\\xf2\\xbbh\\xea\\x02\\xbdl\\xa2G\\xbc\\xd80`=\\xb1\\xe0\\xae\\xbc\\x7f\\xbd\\xbd<\\xdb!\\xab\\xbdw\\x93\\xfa\\xbbn\\x04\\x1b\\xbd\\xc9\\x9b\\x84\\xbd\\x03\\x14\\xbd\\xbc\\xf7\\xe1\\xcd<\\x18a\\xdd<|\\xaef=*yE=3J\\x96\\xbc\\xfbb\\x0f=\\r\\\"\\x03<\\xffC]\\xbd)\\xfc\\x18=\\x9bM\\x03\\xbd\\xcb\\x80\\x1c<@.\\xe5;\"\nHSET bikes:10028  model 'Rhea' brand 'BikeShind' price 3469 type 'Commuter bikes' material 'alloy' weight 13.9 description 'This bike is the perfect commuting companion for anyone just looking to get the job done The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"G\\xb0\\xfc;\\x96\\xe3\\xbf\\xbc\\x82e\\xd2\\xbc\\x05\\xbc\\xbb\\xba\\xf1\\xbe\\xa1\\xbdz\\xdaJ\\xba\\xe2\\x1e\\xb2\\xbcy\\xc9;\\xbd\\x82@\\x80=&\\xb0\\x87<\\xdeZQ\\xbb+k\\x99\\xbcoU\\x1b=E\\xb7\\x80=_}\\x02=\\x0c=3\\xbd->\\x85=\\xa9\\x97\\x83\\xbd\\xac1j\\xbd\\xbd\\x96&\\xbd\\x83\\xf7\\xe2<\\xc1;\\x81\\xbc\\xf7\\xbd\\xcf<N\\xf9\\x18\\xbcs\\x99\\x15\\xbdC\\xfa\\x81\\xbc\\x92@\\xbd<1\\x01%;\\xbe`Z\\xbb\\x7fQ\\x07=\\x90%\\x82\\xbb\\xd4\\x92H\\xbd\\xe5g\\x1a=\\xa7\\x9bF=\\xbc)V\\xbd\\xfc\\xb8\\xab\\xbc`\\x8a\\xb8<\\x9c\\xdf0\\xbc\\x06\\xe4=\\xbdK\\x95\\x89\\xbb\\x9fHb=f#\\x83\\xbc\\xdd\\xaaT<1\\x12\\xa9<<\\xdd\\x0e\\xbba\\x0fz\\xbd\\x0f\\x07U=Gx`\\xbbt\\xf2\\x08\\xbd\\\"\\x1d\\x83<c`9\\xbc\\xf7\\n\\xe2\\xbc\\xa9V\\xc2\\xbc\\xb7\\xa4\\xad\\xbc\\x9e\\xc7\\x8c\\xbc\\xfeY\\xd9\\xbc\\xbaD\\xf2\\xbaV\\xc2\\xdd\\xbc\\x96&\\xfe\\xbc\\xc2\\xb3\\xbd=\\x88\\xba\\x16\\xbcYWj=+C\\x1f\\xbcn\\x92\\x8a<\\xc0Q\\xad<\\xa1%\\xd4\\xbc\\\"\\x1d\\xdc:a\\x0f\\x8c<\\xc4h\\xa0<--M\\xbcY5\\x99\\xbbr\\xf9\\xd2;\\xc0\\xb1\\xb6\\xbc\\xf3\\xdb\\x93\\xbdJ\\x01\\x11\\xbc\\xd3\\xe3\\x14\\xbd\\x0e0\\xa2\\xbc\\x02\\xe7\\\"\\xbdZ5\\xc5\\xbc\\xc7V;=\\x89\\xe9C<|\\xc6&\\xbdl\\xa1\\xb1\\xbc\\x8epr\\xbb\\xa5\\xba\\xb2;\\xe9\\xc2x\\xba\\x10uk\\xbc\\x12`\\x14=\\r\\xba{\\xba\\x10yb<9\\xf8\\x18<t \\xb5;[[\\x1e\\xba\\xcfs\\x91\\xbc\\xd3@\\x19=\\xda\\xfc\\x1e<.w\\xba\\xbc\\xda{\\x9a\\xbcr\\x9d\\n=\\x0b\\x99#\\xbd\\xbb\\xaa\\xa0\\xbd\\xe3%\\x8b<x\\xfe\\x979S\\x1e\\xed\\xbc\\xdf+\\xa2<\\xd7h\\x0e>8im<p\\t\\xb3<Qp\\x11\\xbdf~\\xda\\xbc\\xa5%\\x89<\\xe2\\x16\\xd7<\\x97\\xd6\\xd7<\\xb0\\xc62\\xbc\\xd5l\\x85\\xbdP\\xc6\\xb0\\xbc\\x91\\xb4\\xa5\\xba\\xe7\\xf4\\x0c<\\x80{*=\\xd3\\xb0\\x9d;\\xad\\xe6-=k\\xcfX<[U{\\xbdR\\xdc\\xb7<\\xaf\\x84\\\"=\\x87\\xe0\\xb2\\xbb~\\xfe\\x8a\\xbc\\x94\\xbe\\x8a\\xbcS\\xa9]\\xbd.&\\xfd\\xbc\\x95\\x9f\\xbe\\xbc\\x8f\\x1c0\\xbd\\xafK\\xd7<\\xb6{\\x1e\\xbd4\\xcb=\\xbc1\\x8b8=\\xa3DT\\xbd~\\xc8=\\xbdU7\\xc1<\\xdaR~\\xbc\\xff\\x00H\\xbdUao\\xbd\\tzO\\xbc\\xfd\\x08[=\\xdf\\x88\\xf3\\xbc[\\x8a\\xa1<\\xcb\\x0c{\\xbcZ6\\x04=\\x95ki\\xbc,\\x06\\xc8\\xbc\\xf1\\xf1\\x18\\xbc\\xb7\\xae9=*+\\x08\\xba\\xf6\\x114<\\xf1\\x7fZ\\xbc`\\x03\\xfe\\xbb\\xfdl:<\\x1f\\xd80;\\xaa\\x88\\x12=\\xc4\\x93@\\xbd\\xa7\\x7fH=DH\\xdd\\xbcJ\\xd6\\xbf\\xba#\\xa3\\xc7\\xbc.PZ<\\xa8\\xdd\\xed< \\xb3\\x87\\xbc\\xb3\\xad\\xd2<\\x11\\xc8\\x11=\\x97,z\\xbbg\\xbbX\\xbc\\xdd{\\x95=\\xa3]\\x87\\xbc\\xf4\\xc48\\xbd\\xfc\\xe8\\x85\\xbc\\rw+=\\xe2]\\xb0\\xbc\\xdaRM=i\\x10\\xbc\\xbc\\x11\\x9d\\x91\\xbco\\x85I\\xbc\\xae\\xfd\\x12\\xbcD\\xaa\\xcd\\xbb\\x95\\xdb)=\\x14\\xea\\x8d<PK>\\xbd\\xd62\\xec<\\x1e\\xb6\\x9a<=0\\\\=\\xbd\\x8e\\xc4\\xbb\\n\\xa56=q\\x19\\xf5<\\xe1Zv\\xbd\\r\\x0e~=n\\\\7:}\\xa8W=\\x1a\\xfc\\xea\\xbb,q\\xea\\xbat\\xa13\\xbcq\\x12\\x00;\\x17\\xf6\\xb6;<\\x1d\\x14\\xbd\\\"\\xf0\\xbe\\xbd9t\\xe6\\xbc\\xfe\\x00\\x0e\\xbcIKA=\\x96\\xb7\\x02\\xbd\\xa98\\x1b\\xbdB\\x06f=\\xb6\\x8b\\x96\\xbc@P\\xda\\xbc\\xd7\\xc8\\x93=|\\xd2\\x8e=Q1\\x15<s\\xf3\\xa6<\\x08\\xd0\\x8b<\\xea`\\xc8\\xbd\\x10\\x89\\xe4\\xbc\\x87\\x93\\xb5<\\x91\\xafe\\xbd3\\\"\\x86\\xbc\\xc9\\x05\\xe1\\xbc\\xaa\\xf1\\x10\\xbd\\x0c\\xe4,=\\x1f$ \\xbc\\x94\\xf5\\xca\\xba\\x86\\x1dG=\\xd2:\\xa1\\xba\\xfcdO=\\x97\\xd7v\\xbc\\x00g\\xdb\\xbc#d\\r<[\\x8cP=a,\\x0e=O\\x83\\x94;\\xd8U\\x0f\\xbd\\x9a\\xb3\\xab;\\xc619=A\\x92!;\\xac\\x03\\x8d\\xbb\\xe0|\\xe9<\\xa6O!\\xbc\\xe3)P=\\xa4\\xc8\\\"=\\x01\\xc2&<\\xde\\xde}=\\x17\\xb7\\x14=\\xc0\\x82\\xac\\xbb\\xd8\\x01a\\xbc\\xc2A\\xe3\\xbb\\xf9\\x1aJ\\xbc}e,\\xbd\\x11\\xe1t\\xbcL5\\x12=_\\xa6\\xa8:\\x03\\xa2\\xd9=v\\x9a\\xe8\\xbc\\xfa\\x19#=cMW<\\xac\\x10o=5&\\xbd\\xbco\\x14\\x16\\xbcks\\x04\\xbd\\xbc&_\\xbc\\xfc\\x8cg\\xbd\\x81\\xd03;=\\xa6x\\xba\\xe1\\x83\\xd2\\xbcc\\xe1\\r=d\\xdc\\xa1\\xbc8n\\x02\\xbc\\x9a\\xa7\\xe2\\xbc\\xfbKq\\xbd\\x8d[\\n\\xbc\\xed4\\x03\\xbd\\xee\\x18\\xb8\\xbc\\x9a\\x80\\x9e<@U\\\"\\xbd\\xb0i\\xd2=\\xe5Hp\\xbd\\x12w2\\xbb\\xba\\x04\\xeb;!zH=9l\\xf7\\xbb0u~<\\x81&\\x95<\\xb6\\xe7\\xfc\\xbc\\xe3\\xaa\\x8c=Bm\\x18\\xbd\\x8b\\xa2/<0\\x02(\\xbc\\x85\\xe3u\\xbc\\xbc\\x96w=\\xd5h\\x9f\\xbc\\x84\\x0e\\xed<\\x93\\xe8\\xd2\\xbc\\xd0\\x16\\x86;\\x99\\x94\\xe0<\\x01Z\\x97<\\x87\\xd2\\xe8\\xbc\\xd8LK=\\xfb\\xbf\\\\;\\xc4\\xb8g<\\x12r\\x92<-/\\x19=\\x86\\x12\\x16\\xbb\\x15ig\\xbc\\xa8\\xbdP\\xbbm0M=\\xd8\\xdd\\xad;\\xf9\\xf7#\\xbd*\\xaa.\\xbd\\xab8\\xa5\\xbc\\xe6f]\\xbcx\\xddY\\xbb\\xd6\\xaf\\xb2\\xbd\\x9f\\x8c\\x10\\xbdWL\\xd6=\\\"\\xa1\\x90<\\xdb\\nQ\\xbdD\\xef\\xdb\\xbc\\xa6\\xcd\\xf4\\xbc\\xfa\\n\\x01=\\xb5\\x98\\x89\\xbc13\\xd7<\\x02}x\\xbd\\x81\\xdd\\x17=\\x87V\\xed\\xbc*\\xf1\\x87=X\\xaf0\\xbc\\xc2\\xd2\\x0c=E\\xe3(\\xbdH\\xf6\\x04\\xbc\\xefs\\\"\\xbd\\x07\\xae\\x17=g8\\xa7\\xbc\\xa1]A\\xbd\\xe8\\xb3x<\\xf6\\xc9\\x06=\\x89\\xa5+<\\x94\\xaaW\\xbd\\xd2\\x84\\xbb<\\x16u\\xbd=,\\x00N\\xbc\\xe8k>:\\xe4\\x85\\x01=-\\xa2\\xf9<Fb\\xa7<gQ[=\\x9b\\xec\\x84<\\x8b\\x86\\xa6<j\\xa9\\x10;P-\\x97=\\xa7t\\x8a\\xbc>\\x9e2\\xbd\\xc8}\\xb4\\xbb\\x11hC\\xbdEc3\\xbd\\xd2\\x10\\xc7\\xbc\\xdd\\\"\\xdb\\xbdHVM<\\xa3\\xe4-<\\x05\\xc2\\xd2<E\\xc1W=]\\x07\\x98;/\\xab\\xf9\\xbb\\x7f}\\xc7\\xbc\\x18\\x1b\\xca<-\\x97\\x9f\\xbdE\\xa7\\xba\\xbcv\\xb5\\xfc<iB\\xdf<\\x03\\xd0\\n<\\\"\\x8c\\xa0;\\x98[\\xda<4Yg<\\x1a\\x0f\\xcb\\xbc\\x05\\xfd\\x95\\xbcV\\xb4\\xdf<\\x9c\\xdb\\x18\\xbcqIL\\xbc4du\\xbd\\xe8>\\x92:uH\\xca:\\x97\\xe25\\xbd\\xb2Pp=yBu\\xbc\\xb3\\xc6\\xd8<\\xa1\\xc6\\x92<\\xeb\\xc7\\xa0\\xbc\\x0cy\\xb8\\xbb\\xac\\x1a\\x8e=\\\"\\x10\\x11>sr\\x16\\xbd\\xf1\\xc5\\x89<\\x0f\\xd2\\x86\\xbb\\xbb\\xba\\xe0\\xbc\\x1d\\xd7E\\xbc\\xfe]\\xc9=\\xc5A\\x9c\\xbd\\x17\\xa2\\x9f\\xbc\\x1b;\\x1f\\xbdx\\xdb\\x8c=#\\xbc}\\xb8\\xcd\\xd6h=\\x89E@\\xbd\\xf4B\\x88\\xbc?\\xfbQ<\\xa1\\xf8\\xe3\\xbc\\xa8\\x85\\x02<5^\\\"=[+{\\xbc\\xe1\\xdd\\xce<\\x90\\x06\\xad;\\xe3\\x9b;\\xbc$\\xdb\\xd3<\\xa4\\x86\\xca<\\xd7@D\\xbd9\\x86\\x95<\\x81|\\\\\\xba\\xad\\xc5-\\xbdD\\x81\\x11\\xbd]\\x04\\xf7<Co}=\\xef\\xfd\\xb6<e\\xc3\\x15\\xbd\\x92\\\"\\x96\\xbdc\\x81\\xad<C\\xf8\\xdd<m\\\\\\x00\\xbd\\x7f\\x832<\\rq\\x9f;\\x866t\\xbd\\t\\xe6\\x86\\xbd\\xdc\\xe4\\xa3\\xbc\\xf9\\xd9\\xb3=\\r\\x9a@=\\x80!\\xfb;r\\xaaw\\xbd\\xef%\\xce\\xbb\\xc0\\x98C\\xbd\\x89[\\xdb\\xbb\\xae\\x7f\\xf9\\xbb\\xb2\\x17\\x98\\xbb\\xb6\\x12u<\\xdbq\\xca<\\x1e\\x81\\xc4<r\\xe3#;\\x19\\x13\\xa2\\xbd)ZX=\\x8f\\xf8\\x0c\\xbd)UD\\xbdf\\x90,\\xbcn\\xb0\\xaa\\xbdW\\\\P=\\xf3\\xe1{\\xbc\\xb9\\x8b^\\xbbF\\xd9\\x18<\\xc0\\xb8M\\xbb4\\xc4\\x06=\\x84H:= \\xf0\\x17=\\x18v\\xdf:]\\xaa\\x84\\xbd\\xc3\\xc77\\xbc\\x05\\xb8\\xba<\\xfdtC;!\\xb2L\\xbd\\xfdc\\x80\\xbc+\\xe1c=\\xe9\\x8dQ\\xbc]$\\xd7<\\x98\\xb3\\xa6\\xbcj\\xae\\xba\\xbcZ\\x0b\\xf6<\\xebv\\x8d<\\xd8:\\xf4\\xbc,]\\xd3\\xbc\\xa8~!\\xbb\\xc2\\xda\\x1c\\xbb\\xdb\\x92%\\xbb\\x84\\xa80\\xbcX\\x1b\\x1f\\xbb\\x04$P<6\\x91-<\\x0c\\xdd\\x8d\\xbd\\xa0N.\\xba\\xe1\\x8b\\x01=O,8\\xbc9Y\\x8d<<\\xfc\\xb5\\xbd=\\xb8\\xba<\\xb7\\xce\\x89\\xb9|m\\x829\\x8c\\xd9j\\t\\xf2l\\xe9\\xbc\\r\\x816\\xbd\\xd7\\x05\\x1f=\\x00\\xc9\\x8f=n\\nB=\\xd8d,=\\xbf\\xe1`<\\x07k\\x8d\\xbc\\xf1\\xe6\\xcb<v\\xc2\\xbb\\xbcn\\xc29\\xbc,\\x1dW\\xb9z\\x98Y\\xbb\\x9bK?<\\xa2\\xe4;\\xbd\\xda\\\"%\\xbc|AU<\\xd8>\\xa3\\xbc\\x0fX\\xd2\\xba\\x15\\xa6\\x81=\\xe9\\xdbP<`\\x84\\xae<\\xf6\\x95,=5\\x8b\\xf6\\xbc\\xa4\\xaa\\xad;\\xdc\\xae\\xdc;\\xcee\\x14=\\\"r\\xa9<\\xfd\\xf4/\\xbdM\\xd9-\\xbc\\xf7\\xde\\x1f<\\x8c\\x1f\\xc1\\xbbi\\xd3p\\xbd\\xab~\\x0b\\xbd\\xfc\\xd7m\\xbc\\xd0\\xdd8\\xbd\\xd5\\xec\\x01\\xbb\\x1a4%=\\xfe\\xe5\\x9f</ b\\xbbo\\xb5\\x00\\xbc\\x9dk\\x0e<\\x04\\xfe\\x9f<\\xfax\\xfe<S\\x12\\xcd<\\x10g=\\xbd_p\\x9c=\\xa9\\xa3\\n\\xbdR\\xdc5\\xbd<d\\xbc;\\x01\\xee\\xae\\xbc\\xd3\\xe6&\\xbd\\xddw\\xa2\\xbc\\x13\\xaf\\xf7<\\x85s\\xef\\xbc\\xfc\\xd9\\x16=\\xa9\\xffz\\xbb3\\x10\\xea\\xbb>pj<\\x83%\\x07=\\xed\\x17\\xf5;\\x008O\\xbc+}8\\xbc\\x9c\\xdaM=S\\xc7\\xbc;S\\x1d\\x11\\xbdS\\x14 =_a\\xbd\\xbc\\xf8\\r\\x83<yk\\xf2<\\x88k[<H\\xc6r\\xbc\\x17A\\xa2<\\xe7\\x11\\xa6<\\xd9\\xc0A<\\xce\\xe3\\xc2\\xbcc\\xaa\\t\\xbcT\\x18\\x1e=\\x1b\\x96L=\\xad\\xa0\\n=X_{\\xbd\\x07\\xac\\x95\\xbc.9\\x82\\xbd\\x12\\xe0\\xd3<\\x84\\xbd4\\xbd\\xb4\\xbf\\x9f<\\xf81\\x98=y\\x87\\x06=\\xfer\\xbc\\xbc\\xf4\\x01\\x1a=\\x08\\xe4\\xa1\\xbc\\x15}\\x1f\\xbd\\xd39c=t\\xf2\\xed:\\x18\\xf5D;]*\\xa0<\\x96_\\xf1\\xbc\\xd0\\x9a\\x9d<B-\\x06<\\xde\\x9c^=\\x9fP\\xcb8\\x0c\\xd6\\xf1\\xbc<\\x1f\\x14<\\x06\\x83\\xfb\\xbbX$\\xed;2\\x9b\\xca\\xbb\\xeb\\xc4\\xfe\\xbb\\x99\\xe5\\x8f\\xbc\\xe7k\\x8a\\xbcT\\x17\\xd6\\xbc5\\\"8=\\x94\\xf7\\t\\xbd-+\\xb7\\xbc/\\xcfI=\\x88\\xc1\\xa2=\\x8cf\\x8f\\xbbL\\x0e\\x1f\\xbb\\xaep\\xf9\\xbb\\xe9X\\xf3<E\\xfd\\xb4\\xbc3\\xf7X<1\\xa3m<\\x8e\\x87\\xe3<68\\x85=\\nf\\x82<\\xdc\\x87\\xe9<C\\x84h\\xbd>\\xf1\\xc1<\\xf6\\xfd9=\\xc8@\\x00\\xbd.\\x81\\n\\xbd\\x94\\x14\\xa6\\xbd\\xfa\\xef\\xfa<\\x85\\xbeb;f\\xf5\\xda=\\x91z\\x9c\\xbcuq =\\xad\\xbe\\x8b\\xbd)]\\xa9;e\\xd5\\n;\\xe2\\xd6\\xb8;\\xd7\\xa8:\\xbd\\xe8\\xb5\\xff\\xbc\\x81\\x86\\xf7\\xbc(E~\\xbb\\x16\\x16!<e\\x9f}=\\xd9\\x80\\x02\\xbc\\xcb;\\x1d=I\\xc5\\xb8\\xbcq\\xd5R\\xbd\\xbb\\x80\\xe1<P\\xf5R\\xbc\\x8a\\x19\\t=a\\xe2==,\\xf1S<F\\x03\\r\\xbd\\xcc\\xc4\\xd4<\\x13\\xde\\xda:\\x94\\x8e*;\\x89L\\x93\\xbc%\\x18\\xb2==\\xa3\\x9e<\\xfc\\xef\\n\\xbcv\\x13L\\xbc-\\xf8f\\xbcb04\\xbaj\\x083=9\\x98?=\\xa5.\\x07=(\\xcd\\xcb<P\\xe0\\x97\\xbc{=\\x81<g\\xf0\\x07\\xbb\\xa1\\x16a<\\x99\\x17\\x08=\\xc6\\r\\x92\\xbcz\\x92$=\\xad\\xe1\\xf4<}\\x82\\r\\xbd]83\\xbd\\xc0\\x7f\\xa9\\xbd\\xdb\\xfc\\xb9<J\\xfd\\x1d=]\\xd5\\x9d\\xbcJ\\xec\\xc3\\xbc\\x85j{\\xbc\\xc3\\xd5\\xa7\\xbc\\x92\\x1b\\x98<>0\\x9f<\\xd5\\r\\xd6\\xbb?\\x18\\xab<\\xe5\\x19\\x80\\xbd\\x0cN\\x83\\xbcUx;\\xbd\\x8bC\\x91\\xbdLB#\\xbd\\x7f\\x11\\xc5\\xbc\\xf0~\\x94\\xbb\\tu\\xb9\\xbcw\\xb1\\x02\\xbd\\x15\\x9aJ\\xbb\\x81FL\\xbcCg\\x90\\xbd\\x12\\xb1\\xfa<\\xb0\\xeb^\\xbd\\x7f\\xa8\\x99<\\xc9\\x8cR\\xbd\\x92\\x90\\r\\xbc:Y\\xef\\xbc\\x0e\\xe4\\xa1\\xbb\\xc5\\x81\\x80\\xbc\\xbar\\xeb<\\xdf\\x93\\x0b\\xbc\\\\t\\x0e\\xbcB\\xe6\\x8f\\xbaS&d<\\xa4*x\\xbc\\x0c\\xef\\x9a\\xbd\\xc5\\xa1\\xe4\\xbd\\xb7\\xf9\\\\\\xbc\\xb6m\\xe8\\xbc\\xda !<\\x9d\\xebm<\\x80\\xe5\\x93=\\xec\\xeeP\\xbd\\xa8\\x97\\x12=\\x06\\x95\\xd3\\xbc0\\x14\\x85\\xbd\\xfe\\xd2\\x1b=#\\xb0K<\\xc0S*\\xbc\\xbb_-\\xbd\\x8dl\\x84\\xbci0\\xcc:P\\xf0\\xbf\\xbc\\x8f\\xc5\\xd7\\xbc\\xcb\\x90\\x0e<\\x81-b=e\\x8b\\xd3;?\\xc0(<\\x82eJ=\\xd0c\\xbc\\xbb\\x04]|=\\x1f2\\xd5\\xbb\\xa5U\\xeb<Y--\\xbd\\xd0D\\x8c=\\xe5E\\x0c\\xbdL\\xebQ<\\xadl\\xc89\\xb1.5\\xbb\\x9clE<\\xaa\\xa9#=u\\x00\\xfe\\xbc\\xee\\xd3,\\xbcF\\xeaf\\xbc7\\x15\\xa9;\\xeb\\\"#\\xbc\\x81\\x04\\x9d\\xbd\\xc6n;=F \\xd7\\xbc\\xa6\\xaf\\xe2\\xbc\\x85Fg=\\x82\\xbd\\xbb=\\x015\\x00<\\x7f\\x19\\x1d=h\\xd0h=F\\x7f\\x1f<\\xd9\\x8b\\x81=.\\xbe\\xca\\xbdir\\xb7<\\xd8H\\x9e\\xbb\"\nHSET bikes:10029  model 'Weywot' brand 'BikeShind' price 2797 type 'Commuter bikes' material 'carbon' weight 12.4 description 'This bike is the perfect commuting companion for anyone just looking to get the job done A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xf6W\\x8e;,\\x9c\\xe8<1 \\xc2\\xbc\\x08\\x08`;\\x13S\\x0b\\xbc\\x05\\x0b\\x02<\\x9e_@\\xbc\\xe1RV\\xbdC\\x1c\\xbf=.\\x8b.=\\xfdU\\x05\\xbb\\x86C]=\\xf2@\\xa3<\\xa0\\xee\\xf0<\\x95-/=,[(\\xbd\\xf2\\xb3\\x1e<EQ$\\xbcd\\x13u\\xbb\\x87+O\\xbd\\x03\\xec\\xa1<V\\xf6>\\xbd\\x04\\xe2c;Z\\x85x\\xbc\\x85T\\x8f\\xbcV\\\"\\xeb<\\xadP\\t=\\xe3:\\xc1<&\\xad\\xdd\\xbc\\xd8\\x81\\xcb=%\\x03_86\\x1c\\x18\\xbc\\xd1;\\xd9;aY\\xde\\xbc\\x1a\\xc3x\\xbbi\\xca\\xa7\\xbc\\x16\\xf1\\x89\\xbc\\xeeb\\xb3\\xbc5Xc\\xbd\\x1c\\x00\\x83<\\xad<\\x85=\\xe4\\xda~\\xbd>&\\xda<x\\xe7\\xf5<\\xd8\\xdb\\x9a\\xbc\\x1c\\xadz\\xbd\\\\\\x0e\\x9b=\\xbd\\xc5\\xf3\\xbc\\xb6\\xef\\x15\\xbd\\xcb\\xbf#=\\x1b\\xa7\\xad\\xbcS \\x1a\\xbd2\\x83\\xe5\\xbct\\xe6\\xeb;w\\x10\\xeb;J|\\n\\xbb\\xa7\\x1b \\xbc\\x9e]\\xef\\xbc\\xcd9\\xf9\\xbc\\x92\\x85\\xbf=\\x12\\x06\\x80<K\\\"\\x0e=\\xf4w\\x9d:\\xaf,%=\\x19\\xb1\\xca<q\\xbb\\xaf9\\xaa\\x83\\xbb\\xbc\\x8d}\\xa4<\\x95\\x02\\xad;\\xc3\\x13\\x9b<>\\x94\\xac\\xbb\\xc9\\xc5\\x8c\\xbch^\\xa9\\xbc\\xef\\xc02\\xbdt\\xdc\\xd2\\xbcm\\x0fm<*\\x9b\\xdf:.C\\xb9\\xbcv\\x03\\x15\\xbd\\xcf\\xbcr<\\x14Qt<c\\xc1\\x9f\\xbdk\\xbe\\xa7\\xbc[\\x06\\xd8\\xbc\\x17\\xd3\\xdd\\xbc,n\\xdb<2\\x97\\xb3<UM2=\\xe2\\x1c\\x05=\\xa2\\x911<\\xb4\\x16\\x0f\\xbd\\x9e\\x00F\\xbdk1=\\xbd\\xfd\\xbb\\xf7<\\x8b\\x01\\x9e\\xbaP\\x92\\x85<\\xe8\\xc8\\x15: E\\x12\\xbd\\x81\\xf8\\xb1<\\x99a\\x11\\xbb\\x849\\xc0<\\xa4`\\x9d<#\\xd4\\xef<\\xf5\\x88[\\xbd$\\xdf\\xe8\\xbc\\x8c\\x86\\xe3=\\xa3n|=\\xfaW);;[\\xaa\\xbc\\xaae\\n=\\xeaI4=\\x1aB\\x83\\xbc\\xd5\\xf5\\x0b<6\\x065\\xbc\\x93\\xfe\\x89\\xbd\\x893&\\xbaK\\xb4\\xa5\\xbb\\xb1|\\xc2<\\x91a\\x03<C\\xda\\x18<Z}\\x96\\xbb\\xa77\\xb1<\\x8cX\\x14\\xbcE\\x02\\x1c\\xbd\\x9e\\x95,=\\x9b\\xe5\\x92\\xbb\\xc7\\xee\\x97\\xbc\\x80\\x19\\x12\\xbd\\xb0!6\\xbc\\x9fa\\x1b\\xbd\\x03@\\xef\\xbc\\xdc*\\xa7\\xbcQ\\x10\\xf1<:(\\xf3\\xbcU\\xef\\x0c=3\\xb7\\x85=\\xaf\\xb1+\\xbdk\\\"\\xf9\\xbcW\\x9d&<1\\xfe\\x89\\xbda\\xe4b\\xbd:\\xcc:\\xbdB\\xba\\xa2<\\xbd\\x0e\\x9f=\\\\\\\"*\\xbd\\xc8~X\\xbc)\\xd9\\\"\\xbc5\\xcc\\x16<\\xc5\\x97V\\xbd\\xcbLt\\xb9\\\"T\\x8d\\xbbd\\xed\\xa5=:\\x9e-\\xbdB\\x95\\x06=\\xe3]r\\xbd>cZ\\xbc\\xfb\\xf1+\\xbd\\x992\\xba\\xbc\\xd5\\x9f\\x88<S\\xef\\x02\\xbd\\\"?K=\\x05F\\xdf\\xbbNg\\xc0\\xbc\\xd1\\x92\\xe5\\xbb\\x97\\xfe\\xf6<yW\\x8c<\\x83g\\xbb\\xbbkz\\x98<\\x19\\xf90=\\x16k\\x0b\\xbc#\\\"\\x04\\xbdY\\xc3\\xa9=NP\\x83:\\xc6@l\\xbc=]\\x15\\xbcJ\\x00\\xd6<|\\xed\\r\\xbd\\xa4\\xadB=\\xeb\\xde\\x1a<\\xd9\\x00\\xea\\xbc{\\x18\\xa6<\\xfc\\xab\\x9f9\\x1ef\\xc5\\xbc\\xa7i\\xd3<\\xc9\\x04 =\\xb5n\\xe6\\xbb\\xee\\xa1^=\\xd7y\\xa4\\xbcE\\xb1\\x0f=(\\xd7\\xfe\\xbc\\xbde\\x92<\\x11\\xa2\\xb0<\\xfa\\xa3\\xec\\xbbI\\x87\\xf4;\\x86\\x9b\\t\\xbd\\xc0V\\x17=\\x17\\xef&\\xbdR\\xd1j\\xbd\\x08\\x8c\\xac\\xbd\\x8a\\x99y=\\xb2\\x97\\xa6\\xbc\\x01\\\"\\x86\\xbc\\xccr\\xdf\\xbde\\xce+\\xbc\\xdf\\x92\\xbf\\xbc!\\x12\\xfc:\\xba\\xb3\\xba\\xbc\\xf8\\xec\\xd5\\xbc\\x85L\\x83=)C\\x01\\xbd\\x02\\xb8\\xc4\\xbc\\x92\\xc2\\xfc\\xbbDe\\xeb<\\xb2D\\xad\\xbct>Z<\\x0c\\xb2\\xf4\\xbbe\\xab\\xd6;`\\xd3A\\xbd\\xfcv\\x84=8=\\xb3\\xbd#<\\xb4:tN\\x19<(A\\x0c\\xbd\\x85\\x94\\xcc;\\x82(\\xbf\\xbc\\xb2#X\\xbd\\xaf\\x91o<\\xf9 \\\\\\xbcfVu=\\x9f\\x1e\\x88\\xbd^\\xaf\\x18<\\xacR\\x84\\xbd\\xec\\xe6\\x0b;\\xcf\\xdb3=JO?=\\x98\\x19~\\xbb\\xf1/\\xd6<=hl=}\\xfc{\\xbc=\\xeb\\x08=s\\x01\\x03\\xbb\\xe33Z\\xba8\\xd1\\xf8<\\xa5\\xc54=\\xe6\\xa6\\x7f\\xb9a\\x86\\xec<v\\xbb\\x17=\\t\\xef\\xe5<\\x9c\\x10j\\xbd\\x11Tg\\xba\\x84\\xe0\\x1c\\xbc\\xec{W\\xbdU\\x7f\\x02=\\xcdbf=\\x1d\\xbfI\\xbd\\x87\\x9cu=\\x80\\xa4\\x15\\xbc\\xc4\\x1f\\x8e\\xbb(\\x98w<1\\x8f\\x8a=\\x1d\\x18\\xbe;t\\xbe$\\xbc\\x1b/\\x8d\\xbd\\xe5i\\x05\\xbdN3\\x84\\xbc\\xb4\\xc8\\xc3=v\\xa9d\\xbc\\x19\\x1a6<\\x94U\\x03=\\x80\\xdd#\\xbb\\xd7]/<Z\\xcd<:\\x96&\\xf3\\xbc\\xa6w\\xdc\\xbb\\xf5\\xfaG\\xbd\\xfaN\\xd3\\xbcB\\\\\\x83\\xbc?\\xd4\\xfa\\xb9\\r#\\xcc<>\\xfb\\xac\\xbd\\xff\\xb6O\\xbd\\x1f\\xb7\\x92\\xbc\\xc1|n=R\\xb3\\xf1;\\xb9\\x9c\\x06=#\\xa1\\xba\\xbc\\xbb\\x1a\\xa7\\xbc5\\xccA=m\\xcb\\x92\\xbd\\x998o<\\xb0\\xc3\\x91<&\\x10\\xfb\\xbc\\x0f\\xdem=\\x8aW1<;5>=\\x8f\\xf2\\xfb\\xbcG)\\x18;\\xcd\\x86\\xdd;\\xaf\\xa6Q=\\xd7$\\n\\xbdY\\x911\\xbc\\xfb\\x8fZ<-\\x95\\x92<8\\x12s\\xbd\\xf0\\xfbI=\\xda\\xed\\\"=\\xca\\xfa\\xa0\\xbc\\x8a\\xae\\x06<\\xd2\\xf2\\x91=\\xc2\\xfdM\\xbc\\x96\\xb3\\x1e\\xbd\\xdc\\xfd\\x17\\xbd\\xc2\\xf8D\\xbd\\x1e\\x0c\\xff\\xbc\\xc5\\xe9\\xfe;\\x90\\x9d\\x1d<\\xa0\\xab\\xef\\xbcA2\\x83=Ak\\xbe<\\x97k\\x1c\\xbdk\\x1f\\xd4\\xbc[\\x1b\\x10\\xbd\\x15\\x1a(\\xbd\\xd4\\xb2\\x8e\\xbd\\xd7\\xe6?\\xbc\\x0b\\xbd\\xf2\\xbcG\\x067=8&\\x05\\xbb\\xff\\x88\\xff<M~\\x0b\\xbd\\xfc\\xbb\\x97\\xbb#\\x97W\\xbd@Ai\\xbc\\x05W\\xbf\\xbb\\x14O\\xe0<>\\xa5\\x04\\xbc\\xb3G\\xce\\xbd\\xed Z=}\\xa2\\xc2<D\\xa3\\xca\\xbc5.\\x95;\\x93\\xbb\\xaf\\xbc\\xb5A\\x9f=y\\xbb5\\xbc\\x9a\\x0c\\x88\\xb9X)\\n=\\xbe\\xca!\\xbc\\xe6\\xec[=\\xe1\\xc2\\xd6<\\xf1LJ<\\x13\\x8c\\xf0;O\\xb8\\xa5\\xbc\\xe9\\xb2E=\\\"\\xc6*:}N\\x1f\\xbd\\x97\\xbc\\xa7\\xbc\\n\\xd6\\x0c=\\x99\\x80\\x10\\xbd\\xc7`\\x91\\xbd>\\xff\\x8f\\xbdRL\\xea:\\xd4eG<D\\xb5H\\xbd\\x07\\xf9\\x9c=M\\x1d\\x13=`\\x00\\xbf;\\xb5$\\x02<\\xec\\x19\\x1e\\xbc\\xb1\\xe8\\x80\\xbd\\x84\\xb6\\x8b\\xbc\\xa3\\xf8\\x81=\\x8f\\xf8\\x08=C\\xd7\\xb0\\xba\\xac\\xec\\xdd\\xbb\\xdeqC=\\x9a\\xf1`=eV\\x9f\\xbc\\x8f\\x9c\\x15\\xbdx9\\x04\\xbdb\\xf60\\xbb\\xbcC7\\xbdQ\\xc8^\\xbdgEa<\\x12E^<r\\xfa\\xb4\\xbd\\xa5\\xb1\\xc9<\\xdaQ\\xc9\\xbc\\x8f\\xf9h\\xbd\\x16\\\\#<.\\xdc\\x0c\\xbd{>\\x9c\\xbdq\\xc3T=\\xd9\\xbc\\xac=n\\xcb\\xe9\\xbcM\\xf3\\xcc;\\x0c{\\xb2;\\xe3\\xf18\\xbd\\xb5|\\x95\\xba(\\xc5L=+D\\x83\\xbdO\\xdd\\x1a\\xbd\\x0b\\x10\\xd8\\xbc\\xcb\\x97\\x98=o{\\xe7\\xbc\\xbf\\xd2\\x16=\\x9b\\nq;\\x13\\xb4X=\\xaf\\xa6=;*9s<\\xfe}\\r=\\xdf\\xb1\\xb5<\\x08\\xb9\\x14\\xbd\\x90\\xe9.=\\x8d\\xf8\\x96\\xbc\\x05s\\x08:\\xa0O:=\\xa4\\xeeI=y1R\\xbd\\xa1\\x0fV\\xbc\\xe4sC=\\xa29i\\xbc\\xa9\\xd4\\x12\\xbdw0\\x98\\xbbg\\xbf7<\\xac6S=\\xce[|\\xbcJ\\xa0\\x13\\xbd\\x12 V=W\\xbb\\x01=\\x9a\\xb0\\x0c\\xbda\\xa6\\xbf:\\xa37\\x1e\\xbc\\xe6\\xa3\\x1a\\xbd=\\xa6o\\xbdT\\x9b\\x1f\\xbd\\xf7\\xe4\\xa4=\\x10\\xe4R=l\\x83\\xfa\\xba\\x13V\\x0e=\\xe8\\xafR<\\x94\\x15\\x06<\\xb4\\x8a\\r\\xbd\\xfd\\xd6=\\xbd\\x94\\x94=\\xbc\\xc0\\xa65:\\xdai\\xdc<\\x982\\x91<\\xc9\\xe4\\x90;U\\xeb\\x02\\xbd\\xd7\\xce\\xea;|6_\\xbd\\xf7\\x7fH\\xbd\\xe0\\x92\\xe4\\xbc\\xc6}\\xb9\\xbc\\xafm\\x95=P\\xdc\\x98<\\xc7?\\xf8\\xbc:\\x9d\\x10\\xbd4Y\\x1c\\xbd\\x87\\t\\xa6=$\\xdb\\x9a=F2\\xc7\\xbc\\x9d\\x01y<H?B\\xbc\\x97\\xaf\\xd8<\\x12\\xf8\\x04=\\xf4\\xb5\\x80\\xbb\\xcf\\n\\x04\\xbd\\x91\\x8d\\xd1\\xbc\\xee\\x82p<&i\\x03=f\\xad\\x18=&i\\xe6<l\\x1d\\xfa\\xbb\\xb5\\xb2\\x00<\\x13\\xf3\\xce<\\xfd$:=#;\\x84\\xbc\\xe7\\x00\\x87;\\x85\\x07\\\"\\xbc\\xd0w\\xba;\\x05\\xed\\xf7;3\\xb8 \\xbb\\xfa)\\xdd;:\\x10\\x1b\\xbb\\xdc[\\x05\\xbe\\xec\\x0f\\xce;\\xdc~}\\xbc`\\xd9\\x90\\xbc\\x1bgh<3\\xbe\\x89\\xbcey\\x0c<\\xc6\\x9c\\xa2=\\xc7\\xc5\\x8e<\\x1f=]\\tT\\x0b\\xf6\\xba\\\\\\xa5*\\xbd\\x07\\x99\\xba<>9^;\\x85\\x85\\x8b<\\x93\\xfdX=\\xff\\xf3\\xbd\\xbc\\xdb$R\\xbc\\x18\\x9d\\x1c=\\xf7i\\x19\\xbd\\x0f\\\"M;\\xcc\\x7f\\xb1<W\\xbfW\\xbca\\\\0==\\xdf\\x05\\xbc\\xeb%\\x9e<\\xb0qt\\xbcu\\xbf\\xb1;\\x07a!<4Fg=\\xb6x\\xfa<5F<\\xbc\\x95\\xe3><6\\x17\\xed<0s\\xbd<\\x82\\x0b3=\\xf7\\xa5%<R\\x1e\\xae<\\xdbi\\xed\\xbc\\xf2\\xfe\\t<\\xafMj<W\\x85\\xd5\\xbc\\xb9\\xdfg\\xbdT`\\x7f\\xbd\\xbfF$\\xbd*\\x0b-\\xbc`U\\x05=\\x8a\\x1b1<P\\x7f\\x8e=\\xfb\\xe3H\\xbd\\xb6\\x18\\x03\\xbbR3\\xe5;\\x9bj\\x19<\\xed\\x07\\xa7\\xbc\\xcb\\xbb\\xed<\\x96.\\x89\\xbb\\x12!\\xab=\\xed\\xe4\\xb9\\xbc;_$\\xbd<\\x16\\xc2\\xbc\\xb09\\xe6\\xbcuL\\x0c\\xbc<\\xd6\\xa2;\\xba\\r\\xb8<\\x9d\\x9d\\x02;\\xa6N\\xa2<\\xdf*S<\\xd9\\x02\\xb0<h\\xaf\\xaf\\xba\\xa6\\x1b4=\\x06[\\t\\xbc\\n1\\xb0<\\xc5,\\xe6<H+6=N\\xe3\\x19<w\\x0f7\\xbd]\\xd0\\xd4\\xbc2J}<\\xb9\\x0f\\x10=6.G;2\\xdc==\\x9d0\\x11\\xbc4\\x9f\\xbb\\xbbV}\\x06<7\\xb4\\xd1<\\x12\\x1d\\x9b\\xbc\\xb1:\\x94\\xbbW\\x1b!=x\\x9e=\\xbd~X\\xb2<|_\\x85\\xbd\\x94\\x8b\\xb1\\xbc$\\x9f\\xf4\\xbcme\\x9b<)]\\x06\\xbd\\xa2/\\x12=\\x14\\x8d\\x87=A\\x94\\xb8=\\xa9mu\\xbb\\xbd\\xae\\x0e=\\xde\\xb7P\\xbc\\xfb\\xf1\\x83<\\x9d\\x801=\\xfc\\xd6\\xb19\\xe3\\xd6u<\\x0c\\x9c\\x0c=\\x1f- \\xbc\\x8b\\x94H\\xbck\\xeb\\x02\\xbd\\xfd\\x15\\x1b\\xbb\\xf5=$\\xbd\\xf18\\x9a\\xbd\\x1d\\xe1,=\\x1a\\x06+\\xbdt\\x9cr<\\xc5S\\x8a\\xbc!n\\x9b<\\xb5\\xf0\\xda\\xbb#\\xca\\xd5\\xbd\\xcf\\xeb]\\xbdvp\\xfb<\\xca\\x1b\\xa9\\xbc\\xc8\\x01\\x0b\\xbde\\xc6\\xc7=@\\x10\\x94=S\\xae.<U\\x15\\x0c\\xba\\xf2L)\\xbdM*\\x13<\\x02J\\x8b\\xbd\\xce\\xa6l<F\\x98k;N?D=\\xfa\\xf2\\x16=\\x9f\\xd0\\x99:+\\xdf\\xdb<\\xd9\\xa4u\\xbc\\xe1t\\xb4<yf\\x95<\\xde\\x06|\\xbd\\xb4sG\\xbc`\\xb9\\x85\\xbd\\x01\\x90x=\\xf9\\xca\\n<K\\xc8\\x99=y\\xf4\\xd8<\\x9e-\\x18=\\xf7\\xcd\\x9a\\xbdP\\xab\\x9f\\xba\\xf3\\x10(=\\x9dg\\x1c\\xbc\\xd3*e\\xbd\\xac\\x1d\\xe0:\\xca\\xb9\\xfc\\xbc\\x15\\x06\\x0b<\\xd7\\x88\\xe8<\\xf4\\x03\\x13\\xbd\\r\\xdfL<\\x03\\xba\\xbd<C\\xcb\\xe8\\xbb\\xe1\\xb9v\\xbc\\xfd\\x97\\x83\\xbc<\\x009\\xbc\\t\\xbd!=h\\xef!=\\x95\\xbf\\xb2<D5\\x1a\\xbcrh\\xe2<f\\xfa\\x1e;\\xe94\\xb6:\\xa1\\xb3\\xd2\\xbcQ8\\x1d<\\xe75\\xca8\\xd4\\xb2J<\\xbb\\x10\\x83<\\xfb\\\\q\\xbb\\x1bB\\x94\\xbd\\xf9h\\xd8\\xbcz(d=e,\\xd8<\\xd8\\xcbQ\\xbbKf\\x14\\xbd\\x9fb\\xba<\\x05R\\x1d\\xbc9\\xde\\x8e<\\x87\\xd8\\x97=\\xb7\\xa8/\\xbcf\\x08O;\\xd4\\xc2{;\\x19\\xbe\\x8e\\xba\\x99\\xba\\x90\\xbd\\x1f\\x85\\xc2\\xbd\\x1d\\x8b\\xa8<-qZ\\xbc\\xec\\x00\\x1f\\xbdh\\xafw\\xbd5\\x1d\\xcd\\xbb\\xfc\\xb0V<\\xf8\\xf4.=vq~\\xba/\\x83A\\xbb\\x03g\\xee;\\xc9Z\\x14\\xbdJ\\xac5\\xba(\\x96\\xbe\\xbdl\\xb7\\xf9\\xbc\\x08c\\x13<r\\xd1\\xa5\\xbc\\xdd\\x96q\\xbc\\xee/-<\\xf9\\x99*\\xbd\\xd6\\xf3c<\\xf6h\\xda\\xbc\\xbe\\x9av\\xbd\\x0b\\t9</\\xe2r\\xbcV\\xcd\\xcc<\\xe7\\xbb\\xe3\\xbc\\x83\\xa4\\x88:\\xbe\\xef\\x9a\\xbcm\\x11\\x86:\\xee\\x8d\\xaa\\xbb\\x85\\xcf0=\\xfc3\\x1b<5\\x9f\\x18\\xbd\\\\\\x91\\x97\\xbc\\xab`\\xe5<\\xb4e\\x88\\xbci\\xfa\\x95\\xbdq\\x07s\\xbdxz\\xda\\xbb\\xe4qw\\xbd+\\xe2\\x99<\\xe5\\xa6\\xe0;4J\\xac=\\xea\\xe5\\x8c\\xbb\\xc9\\x1f\\xa9<q\\xb3Q\\xbc\\x1fUT\\xbd\\xc3\\x01\\x87<\\xcc\\xa8\\xe2<\\x84\\xa0\\x8f\\xbb\\x85\\xac\\x99\\xbc\\xac\\x9c\\x9a\\xbc\\r\\xa9\\x96<p\\x1c6\\xbdd\\x87\\x9c\\xba\\xeb\\xa8\\x8a<7p\\xf8<\\x11nj\\xbc\\x8a\\xc5\\x0c\\xba\\xab\\xb2\\xe4<\\xb2\\x9f\\xd4<LV\\xcf<\\xd2/\\xc8;Bl#=\\x91\\xa7\\x10\\xbd;\\x1b4=\\r\\xe1\\x9c:\\xa2v\\x90<\\xf1\\xba:=\\xf8\\xd8\\xb1;\\xa7\\x98I\\xbc\\xb8;\\x88=,\\xf5\\xf1\\xbb\\x8f\\xfe\\x8f;b\\xed\\x91\\xbc\\x06dC<\\xdc\\xe3\\x01<vI\\x85\\xbdh/\\xfb;A\\x19\\x8a\\xba]\\xe0\\xd8;\\xba\\xb0$=\\x8f\\x0f\\x8c=\\x15\\t\\x11<(8\\x8d<G\\xc0~<}\\x00c\\xbd\\xd6\\x9f\\xb2<L\\xf8\\x8b\\xbc%\\x9e\\x88=\\x07-m\\xbc\"\nHSET bikes:10030  model 'Iapetus' brand 'nHill' price 942 type 'eBikes' material 'full-carbon' weight 13.4 description 'If you\\'re looking for the best commuter eBike for your trip to and from the office, that will keep you rolling from home to work. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\x07\\xe3\\xf9;S\\xbd\\xcc\\xbc\\xd5gG\\xbd0E\\xaa\\xbag&\\x88\\xbd\\xb4\\xa5h;\\x187\\xba\\xbcl\\x1b\\x81\\xbd\\xe9\\x16\\t=S\\rV\\xbb\\x00t\\x8b\\xbc5R\\x98\\xbd\\\"\\x94\\xbc<\\x88\\xadA=\\xd3\\xcaE=\\xf7\\x8b\\xa8\\xbc\\xa7\\xbeS=\\xfe\\x084\\xbd\\x94Du\\xbd\\xc6D\\x00\\xbd\\xa4\\xd4\\x02=g\\x1a\\xb2\\xbb^l\\xb1\\xbap\\\"\\x00\\xbcs\\x86\\x0b\\xbd\\x130\\x0c\\xbc\\xcf\\xdf\\xf5<To\\xc4\\xba\\x1d\\x07\\xd3<\\xacmP\\xbb\\x02\\x08\\x83\\xbc\\xda:6\\xbd\\xaa\\r$=>\\x9dE=(W\\xe1\\xbc\\r8\\x12\\xbd\\x1d\\xb9E=\\x9d\\xcd5<\\xe5\\x01\\x81\\xbd\\xea@\\xee;;zy=u \\x81;\\x14?`<\\xa0\\xf9c<\\x1b3\\x14;\\x81Z\\x15\\xbd\\xd6\\xfe\\xe2<$\\x94x\\xbcd\\xd9\\xda\\xbc\\xdad\\x15<\\xe0Nr\\xbc!\\xda\\x97\\xbc\\xf4\\x02\\xb2\\xbc~\\x7f\\x05\\xbd4\\xb4\\xcf\\xbc\\x1a>\\xe9\\xbc#\\x141\\xbc\\xb2~\\x1c\\xbd\\xd7\\x8f\\xe3\\xbbz\\xf4\\x99=m\\x9f\\xe5\\xbc~\\x05.<}\\xde\\x1d;\\xe4\\xd9\\x82<=\\xb2\\x00=\\xb52\\t\\xbd\\xfa\\xc5]<\\xd9x\\xe6<\\x80\\xcd\\x81<W\\xd2E\\xbc,\\x9b\\r<K\\xb3&;\\xae\\x99\\x82;1%\\xb6\\xbd\\xa6\\xe3\\n<k\\xf27\\xbd\\xced\\xcf\\xbc\\xb8\\x9b\\xf2\\xbc\\xb5;>\\xbb\\x9c\\xc2\\x8a=\\x1f\\xa2\\n<\\xdd|\\r\\xbd\\xc2\\xab\\x17\\xbd\\xb3\\xbd\\xce<\\x04\\xff\\xaf\\xbc\\x89\\x8d\\x0b\\xbd\\x03d\\xd7\\xbc~@\\xe6<\\xd2\\xe1\\xb59\\x86\\xf7\\xe2<$\\x99\\x07=\\x82\\x05!\\xbc\\n\\xb4\\x07<\\xfaA\\xb0\\xbc\\xd9\\xa7\\xb2<1(j<\\xf3\\xd9\\x08\\xbd\\xf14q\\xbcBM\\xef<\\x8e9\\xd1\\xbc\\x7f\\xcf\\xb8\\xbd\\xa9-\\x81<\\x8a\\xf0\\x05\\xbdm\\xebB\\xbc\\xee\\x16\\x81=\\xe5\\x8d\\x00>\\xbe\\xf8\\xa7<\\xa4![<\\x14\\xf89\\xbd\\xd5a\\x15\\xbd\\x82\\x8d&=\\xba\\xf5o<A,\\xf1<\\x19Y\\x9f\\xbc\\xbeQM\\xbd\\x1ca\\x0c\\xbd~\\x05\\xdc\\xbb\\x87\\xd7\\x94<\\xee\\x90\\xb0<\\xf8\\x1a\\x9a\\xbbp\\xb6h=IB\\x00<\\x8b6M\\xbd\\x07\\xc9b=d\\xd1U=@-\\xb8;\\xaeg\\x01=|4\\x12\\xba\\xa2\\xb5\\x82\\xbd\\x88\\x15|\\xbd\\x13#6\\xbd\\x17lC\\xbd\\xa7p\\xe0<\\xa1Q\\x1d\\xbdS}\\x98\\xbcU\\x82?=_J1\\xbdS\\x80e\\xbd;\\xa3\\x17=\\xa0\\x99\\xd7\\xbb\\x15\\x9dX\\xbdsQj\\xbd\\xbeo\\x85\\xbc\\\"\\xb3\\xbd<\\x923\\xbc\\xbc\\xfa\\x94\\xd1<6Bg\\xbb\\xcc\\xbf\\xb5;\\xb0\\xe1\\xa5\\xbb\\\"\\xe6\\xef;]\\x15T<9`\\x99<\\x12r\\x19<\\x13\\x16,=l\\xbd\\x84\\xbb\\xa2\\xfa\\xe2:\\xb9q\\x91<3\\xb4\\xfb<r\\xc3\\x07=HGY\\xbd\\xf1cI=\\xc9U\\r\\xbd\\xc6\\xbc\\x0f<\\x9e\\xef\\x86\\xbc\\xbf\\xa4\\xb1<3\\x19\\xd0<\\x10\\x10\\xef:\\x14\\x95\\x9e<%Pp\\xbcO\\\"\\x88\\xbc\\x1a\\xa9\\xfe\\xbb,\\x04\\x9e=]G:\\xbc0k;\\xbd\\x83\\xf1\\xbc\\xbcS\\xe5\\xc0<f\\\"\\xbf\\xbc\\xe0\\xc7L=\\xe8\\xd5\\xba\\xbc\\xee\\xc8\\xa5:\\xf8\\xf4S\\xbcO\\xc3\\x96\\xbc\\xd3Y\\xdb:9\\xbc\\xd6<@\\xb2\\x90<r\\x8b\\x87\\xbd\\xca\\x07\\x1e=:\\xc5\\x8b<\\xce\\xc2P=\\x13\\xd0\\xd8\\xbc\\xabL\\x1e=E\\xc8F<A\\xee\\xa3\\xbd\\xe3\\xa6\\x86=6\\xea\\x86<\\xdb\\xa8\\x8c=\\xaan\\xe9;\\x1b<\\xc2;\\xe3\\xab\\x1a<\\x87\\x9d@\\xbc\\xe4\\x08R<\\xa5\\xab\\xd5\\xbcV\\x98\\xb0\\xbd\\xb4\\x10\\xf3\\xbb\\x17\\xa6\\xba\\xbc\\x95p~=\\xdc\\x10M\\xbd>\\xd0\\xc5\\xbc\\xb0.)=:,\\x83<\\x99q\\xcc\\xbc\\x18\\xdf\\xa4=\\x84\\xd8~=\\x01\\r\\xef;\\xb6A\\xef\\xba\\xbc\\x83q=\\x93q\\xee\\xbd\\x88\\xf4\\xc8<\\\"\\xbd\\x89;\\xc0z\\xd4\\xbc\\x81v\\x9d\\xbc\\xe5\\xf9y\\xbc\\x8b\\xcah\\xbdE\\xbb\\x06=\\xe7\\x88\\x8a\\xbc{#\\xc7<\\x8eeF=\\xaa\\x90\\xbe<\\xb1\\x91%=T-\\xf3<\\xcc\\xd2\\xaa\\xbc\\\"\\x04\\xe7:\\x7f\\xe1\\xa4=pO+<\\xa8\\xcdQ<7a{\\xbdqDY\\xbc\\x8bk\\\"=\\xd9\\xe7?<)\\xfa\\x02\\xbc\\xb9\\x0f\\xb5<An\\x8e\\xbb$\\xff\\xd7<\\x05\\xb2\\xd9<\\\\\\xac\\\\<*&t=~\\xbb\\xc2<O\\xda\\xf0\\xbb\\x13\\xe9&\\xbcv\\x91\\x99\\xbb\\x03%\\xef\\xbcp!/\\xbd\\x90\\xa0\\x13\\xbd0\\xea\\xd1<,9W<_\\x16\\xc1=>\\xca\\x85\\xbc{\\x9e\\xa9<\\xf4\\x85\\xe9<\\x15\\xb8\\xcc<\\x0e\\xf1\\xa1\\xbcG%\\x0f\\xbc\\xec\\xc5\\xb0\\xbc\\x00[|\\xbcr\\xaek\\xbd\\x9a\\xd9\\x1b\\xbd\\xe8=Q;1\\xfa\\xdc:_c\\x07=\\x18\\xac\\xb5\\xbc\\xc5\\xfa\\xf0\\xbcn1\\xfe\\xbc[k\\x02\\xbdy]2<BJ\\x1d\\xbdS\\xb6%\\xbd\\x030\\xdb<J\\xd3!\\xbdb0\\x01>\\xe4\\xb1\\x86\\xbc\\x9e`r<^\\x04\\xb5\\xba\\x9a\\xf8\\x94=\\x878\\x11\\xbd\\x93\\x17\\xf4;\\x1fp\\xd4<\\x89m\\x9b\\xbc?\\x8c\\xa6=\\x1b\\xb6M\\xbd\\r\\x02\\xcd:\\xa6\\x02#\\xbc_\\x12\\xd1\\xbc{\\t#=G\\xcc\\xf7\\xbc1p\\xcc<\\xe9^.\\xbdHL\\xa8\\xbc\\x80j\\x01=H\\xe0{\\xbc\\x90\\xba\\\"\\xbc\\xc3\\xa0/=i\\xb8B\\xbc\\xbf\\xc9\\xb2<\\x81\\x8b\\xcb<\\x01\\x7f\\xa9<^\\xf0\\xf7\\xbb9\\x9d\\xb9\\xbc\\\"\\xafq\\xbb\\xb5\\xc1C=\\xa3\\xf4\\x9c<\\xc0 \\x1e\\xbd\\x8f\\x076\\xbd\\x96\\xb0\\x9b\\xbb`jf<#\\x91\\xc4;\\xb3Q\\x98\\xbd\\xa0\\xf1\\xc6\\xbc\\xce\\xcb\\x9e=\\xe1\\xf6\\xbe\\xb8\\xe1 r\\xbd\\xef\\x8fT\\xbc\\xd2\\xa7U:\\x8f\\x92h;\\x952\\x19;?\\x8c\\x83<:\\xa0M\\xbd\\xbc\\xfeY=\\x8b\\x19\\x13\\xbc\\x9b\\xc4\\xb7=\\xa4\\x0c\\xbc;Fv\\x0c=\\xf4\\xa2\\x0e\\xba\\xb3=y\\xbcd\\xf0Y\\xbd$0%=qRg\\xbb\\x90Q\\xb8\\xbc\\xffd\\x93\\xbc\\x0cN\\x12=W\\xea\\xe1<\\x13\\x91#\\xbdK\\xf3g=/.\\x93=+Z\\xb4:h:s\\xbc\\xcdf\\x89<\\xbb-\\x03=\\xef\\xe35\\xbc\\rk`=\\xdd\\xcf\\x93\\xbc\\xbbWI\\xbc\\xb0\\xf2\\xe7;\\xceFc=\\xe13\\x9b\\xbc\\x8d\\xf3\\xc2\\xbcu~\\xc6;dpf\\xbd\\xcaYG\\xbdG\\xa0\\x8c\\xbb\\xdb\\xcby\\xbd\\x87\\x9d\\xdb<\\x805\\xa5\\xba\\xd3aj=f\\xa7\\xc0<\\xffr\\xc4\\xbc\\x8fe\\r\\xbd\\xc0~\\x81\\xbc\\x15\\x1a\\x04=\\xb6h\\x10\\xbd\\xc9H\\xbe\\xbc\\xa9\\x02\\xba<\\xa8*\\x00=N\\xe8\\xb5<[\\\"\\xa8<L\\xcf\\xe1<-\\xd3V<\\xbb\\xdd\\xa2:\\xc5\\x9f\\xf0\\xbb\\x1a2\\xcb<\\x80\\x02\\x8a\\xbb\\x85*\\x87\\xbc\\x89\\x9e\\x1d\\xbd\\x13\\x9bM<%T\\x85<\\x0c\\xfcY\\xbb\\xec\\xb5|=a\\xe7.\\xbc\\x83k\\xff<#\\xff\\xee<\\xd4;\\xaf\\xbco\\xb5\\x85\\xbbV(\\x91=\\xa9\\x1b\\xd5=KoK\\xbd\\xa0\\xf8;<l\\x9c\\x0f\\xbd\\x92\\x03/\\xbc]\\xe9\\x91;[\\x1b\\xba=\\xe4iT\\xbd\\xfbX\\xf8\\xbc\\xa5\\xb5S\\xbdZ\\xe7.=\\xac\\xa8\\x8f<\\xf0\\xbeX=Z3U\\xbd\\xaa\\x8f/\\xbd\\x15y3<.k2\\xbd\\xbd\\x10\\xe2:\\xf7\\xc3\\x08=\\xf1\\xd4=\\xbd\\xb9b\\xff<\\x94\\xaa\\x8d<\\x86$\\x89\\xbbe\\xe6\\xd3<\\x82\\x9dY<A\\x9e$\\xbd\\xd1\\xeb\\xf4<8J\\xc5\\xbc\\x1cu\\xdf\\xbc\\xb45\\xc2\\xbcNp8=\\x96\\xda\\x86=\\xbc\\xf1i;\\x7f*1\\xbd\\x02A\\x93\\xbdT\\xb6\\x8f\\xbb~3\\xa1<.V-\\xbd\\xce<\\x93<\\xd53\\xef6\\xe7\\xd87\\xbdM\\x03\\xb2\\xbd\\x82:8\\xbb\\xce\\x19\\x99=j\\xa8\\x1b=\\xcf\\x81\\x84<.}W\\xbd_\\xc9\\x03\\xbc)\\x11\\x89\\xbd(\\xf3e\\xbc\\xd3\\xb2\\xe1<\\xd8YB\\xbcL\\xb5\\xe0;\\xfa~+<\\xd8\\x8a\\xff:\\xadnD\\xba\\x02\\xb5\\xbc\\xbd$o\\xe0<E)c\\xbc\\x95\\x15|\\xbd\\xb4^#<Q\\xbcu\\xbd6\\x9e\\x13=(\\xc4\\xed\\xbc\\xc1\\xdf\\x07=\\xe9\\xd9\\x11=\\x7f1\\xee<\\x8c!\\x92<\\xcdBV=*\\xf74=\\x07\\xa6\\xd1\\xbbal\\x91\\xbd\\xee|\\x00\\xbd\\xd5sV<p\\x1f\\xff\\xbb\\xa2g\\xc9\\xbc:\\x95\\xfb\\xbbUwB=^\\x91\\xa6\\xbc\\x88\\x06\\xfe<\\x88\\xcc\\x0c\\xbd4\\\\\\x17<\\x03\\xe7)=/\\xde\\xce\\xb8\\xe6O\\xf6\\xbcD\\xaf\\x11\\xbd\\xd6c \\xbcZ\\x8b.\\xbbb\\x06\\x08<\\x0e\\xe3\\x13\\xbd]\\n\\x85;\\x98\\xbe\\x86<\\xf6\\x1b\\x9d<\\xa9\\xb5F\\xbd\\x9c\\x1a)\\xbc\\xd8ai<\\x0e\\x1b\\x18\\xbcs5f<\\xf1l\\x95\\xbd\\xd5To<\\xd8\\xc2\\xa7;\\x15\\xb8\\x8e\\xbc\\xb6]p\\t\\xb3\\xa5\\xe9\\xbc\\x1e\\xefx\\xbbk\\xb0h:\\xc2\\xe8~=d\\xc6x=%c\\x10=\\\\\\x96\\x08=\\xa4\\x0c\\xa5\\xbcIj\\xdd<\\xf4\\xb7\\x02\\xbd\\x9f\\x1f\\x9e\\xbc\\x99\\xba=\\xbc\\xdf\\xce\\x8a\\xbb\\xccl:<Io\\x8e\\xbdQ\\xbd\\x92\\xbc#e\\xfc<>;=\\xba\\xf12r<~bu=\\xbb\\x0fH<4\\xf5r=\\xedbS=\\x97\\xb9\\x1e\\xbd\\x0b\\xd0Z:\\xeb\\xdf\\x19<\\xcap1=U\\xd6\\xed\\xbaL\\xc1P\\xbd\\x83\\xad\\xce\\xbc\\x8b\\xb8H\\xbb]\\\\\\xaa<\\xd3\\xc2.\\xbd\\xca\\\\\\xf1\\xbcY\\x87 \\xbc\\xd1\\xfec\\xbd\\xb7\\xc4\\x83\\xbcJ\\xd3n</|/<\\x8f\\xb4Z<\\x87z#\\xbcB&A\\xbc/:\\x8e<\\xa1q\\x10=\\x87}-=_\\x9aW\\xbd\\x99\\x9b`={u\\xa7\\xbc^\\\"\\x10\\xbd\\x89\\xde\\xf8<\\xfa\\x18t\\xbc\\xba\\xa7\\x06\\xbc:ti\\xbc\\x00J*=:D\\xb4\\xbc\\xfaS =\\xeb\\xd2\\x17\\xbd!\\x07\\xa0\\xbc\\xd7IG\\xba\\xdc\\x0c\\xd8<\\xfe\\xa4P<K}\\xc0\\xbc5\\xc5\\x12\\xbc\\xb2\\x83\\x04=\\x18Ch<U\\x97\\xc7\\xbc\\x1bA\\x87=\\x8e\\x13<\\xbd0\\n\\xcf;\\xff\\x16\\xb5<i~\\xc7\\xbc\\xda\\n\\xe0:+\\xe5\\xe6\\xbbl\\xb2M=:\\xe1O<\\xb6;\\x0b\\xbd\\xf0\\x1fK<Uk\\xd0;\\xe64\\xab=\\xf4\\xdb\\xe4;S\\xb7\\x0e\\xbd^\\t`\\xbc\\xdea\\x8a\\xbc\\xee\\xab\\x05=6\\n\\xd2\\xbcB\\x04\\xa5<*\\xc7a=,\\xca\\x18\\xbb\\xd1\\x04\\x94:-\\x00\\xcc<2\\xc6\\x13\\xbd\\r\\xbc\\t\\xbd\\t\\xbdS=\\x8a\\xd3\\xd4:3$\\xea\\xbc\\x88\\xa6\\xc7;\\x85\\x9a_\\xbd\\xe0v\\xd1<\\xfc\\xf0^<\\xb5s\\x15=\\x91\\x98\\xb8<\\xad\\x88w\\xbc\\x03c\\x8d:\\x10\\x8b\\x87\\xbb\\xda$];E5\\x10<\\x08\\x00*<>p^\\xbc\\x1f\\x13\\x18\\xbcI\\xd2\\xf7\\xbc\\x8b\\xed)=\\x92\\xc1\\xb1\\xbcB\\xfe\\xc7\\xbcCf*=\\x83\\xdb\\x80=\\x8b\\x9ch\\xbb\\x0b%d;D\\xa4\\xc3\\xbb\\xb5\\r\\x9e<\\x80\\xd1\\x9a;-\\xfbL<\\xe9A\\x0c=\\x85\\xa8\\x8f<^\\xe1k=\\xff\\xf6\\x05\\xbd\\xa4\\xd0\\xaa<\\xa1\\xda\\xf9\\xbc\\xd5\\xf9\\x06;w\\x1e\\xac=@\\xc7\\xf7\\xbc,\\xf2\\x13\\xbd\\x94J\\x8e\\xbd\\x14p\\x06<\\x1c\\xbcE\\xbc\\x8f\\x1c\\xdb=\\x10\\x8f\\x0c\\xbdUx@=\\xd4tf\\xbd\\tN\\x9a<\\x86\\xfd\\xcd\\xbb>\\x06\\n\\xba?%\\x18\\xbd\\x98@\\x19\\xbd\\x1c\\xf3R\\xbcvG\\x13\\xbc^\\x9e\\xc8<\\xc9\\x8e\\xb7=\\xfeG\\xe8\\xb9\\\"\\n\\\"=q\\x9a\\xa5\\xbcH\\r\\x87\\xbdw\\xf1J<\\\\\\x04\\xe2\\xbc~k\\x96<\\\"\\x1b\\xae<\\xa2\\x97\\x9f<\\xb2\\x8eQ\\xbd\\xb7+\\xdd<\\xfa\\xa0P<\\xf7\\x11\\xae<H\\xa4\\x03\\xbc\\x16\\x14\\xcb=\\xb9St<\\xc0+\\x88\\xbbQ\\x1a\\x8d\\xbc\\xa4\\xa6\\xcc\\xbb\\xbc\\xa9Y<\\x88}\\x91=UjU=\\x0f\\xaa8=\\x08\\xaa\\x8b<\\x84\\x8a\\x8e\\xbb$\\xa8Y\\xbb\\xd4m\\xbb<\\xa1\\xf9\\xe7<\\n\\x95%=\\xe5\\x89\\x16\\xbdR\\xc7,=\\x9b2\\x8f<\\x11\\xe97\\xbd\\xf4\\xd6\\xff\\xbc\\xa5\\x8d\\xa5\\xbd\\xf61\\x9d<V\\xf1\\x00=V\\x907\\xbb\\xc8\\x1d/\\xbd\\x94!\\x12;\\x84\\x1fo\\xbc\\xf7\\xd6\\xa3\\xbb\\xb9\\xac\\xc5;6\\x9dZ\\xbb\\xfa\\xff0\\xbc`P\\x82\\xbda\\xe2V\\xbc\\x7f\\r\\x19\\xbd\\xfb\\x8c\\xa0\\xbd\\x88\\xe4:\\xbd\\xa6\\xaf\\xe5\\xbc\\xe3##<v\\xed[\\xbdP+\\t\\xbd6\\xf3\\xa6\\xbc\\xfa%\\x00\\xbc\\x01\\xedm\\xbd\\xfd\\xf0C=\\xc2ik\\xbd\\xde\\x07\\xc4<3\\xd6\\x87\\xbcT\\xf0\\xc3\\xba\\xc9\\xa54\\xbc.\\x1f\\xd4\\xbbE\\x10\\x8d9\\xdf\\xe8\\xa7<\\xf7\\xaf\\xc0\\xbb\\x03\\xcc\\x1e<s!\\x85<\\xbbP\\xdb;?\\xbb(\\xbd\\x99&R\\xbdgf\\xb2\\xbd\\xce\\xa8\\x93<\\x13x\\x97\\xbc\\xcd\\xba\\t\\xbcVJ\\xcd<b\\x83*=\\xed:f\\xbd\\xfbcs=\\x03\\\"L\\xbd?\\x04\\x89\\xbd\\x9b\\xe11=*`3;\\xa1\\xf33\\xbd\\xa6\\x92\\\"\\xbd\\x82n\\x87\\xbb\\xf4\\xbd\\x8e<*\\x9b\\x81\\xbc#\\xc7\\x0f\\xbb\\x9d\\xf0\\xad;:^S=/t\\x8a\\xbc~\\xd9\\x81\\xbc/\\x1e/=\\xd9\\xab\\xe2\\xbb\\xe6\\xb5j=\\x89y\\x7f;\\xdb\\xe3\\x8d<\\xc8\\xfbk\\xbdg\\xb5\\xa2=\\x95k\\x06\\xbdR\\xa1\\x08<4[_<\\xd0\\xb6u\\xbc{\\xf3\\x1a<F\\xc7\\x81=m9C\\xbdZ\\xd1(;\\x90\\xac#\\xbd\\t\\xaa\\x07<\\x0f\\xf0\\xa8\\xbc\\xd2\\x8f\\xcd\\xbd\\x8b\\r\\x19=C\\xb0\\x0c\\xbd\\xd3E\\x1c\\xbd\\x94\\xd9u=1\\xe9\\xb5=\\x85\\xed\\x83\\xbc\\xcf_\\x17=\\\"3\\xec<mz\\xea<Z\\xa4w=\\x19\\x82\\xe6\\xbdA\\x82\\x99<\\xef\\xf8\\x8f\\xbb\"\nHSET bikes:10031  model 'Picard' brand 'Bicyk' price 1376 type 'Enduro bikes' material 'alloy' weight 15.6 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\xc8<\\xa2<g~l<\\x96\\x17*\\xbdV\\x93Q=\\xa6\\xd4\\x1c=\\x11\\x07`=`D\\x86\\xbc\\xae\\xc1E\\xbd\\xa2+8=\\xc3I\\xfd;%j\\xe5<\\xd2\\x92\\x18=jp0=\\x8d\\\"?\\xbb\\xec\\xbc\\xcd=\\xc7\\x92b\\xbc \\xcc\\x98\\xbc\\xf1\\x81:\\xbbI|\\x98\\xbc`S\\xbe\\xbbf\\x1f<<E1\\xfe\\xbc\\x80\\xaa1=\\xa5\\xed\\x11\\xbd\\xa51\\x0c=Hn1<O2X<\\\"g\\x84\\xbb:\\xc3\\xf1\\xbc\\x90\\xa1#\\xba\\xff\\\"\\x9c\\xbc\\x0f\\x03]\\xbb~;\\x0b<\\x06\\xf5\\x10=K\\xb5d=#\\x1d\\xb8<wr\\x9e;\\tQ\\x17\\xbd\\xb1\\xa3\\xb6<\\x87\\x01\\x84\\xbc\\xf1<\\x8d=\\xf2\\xf8\\xd4<\\xae\\xf6\\x07=\\x9e\\xa0\\x08<\\xfb\\x0e*\\xbb~\\xf1\\xa4\\xbc\\x87\\x8d\\xd3=\\x95\\x94\\\"\\xbd\\xb3\\xeb\\xd5\\xbc}\\xcd\\x03\\xbd\\x04\\\\]\\xbd\\x8a\\xefj\\xbd:\\x07\\xb4;v\\x8d\\xa0<\\xf6\\x8e\\xaa<\\x01\\x9e\\xb8\\xbc\\x88/\\x89\\xbb\\xe7\\xaa\\xb1\\xbd\\xac%\\x91\\xbdi\\x07\\x9f=\\xbc\\x81C=\\\\\\x08\\x89<\\x1fp\\x0f=1\\xf5z=\\x1er\\xb1<yp\\xbe;;\\x9a\\x84\\xbc\\xbdQ =\\x93\\xd7\\x8f;\\\\\\xb7\\x81<\\xa1cI\\xbc#f3\\xbc\\xffH6\\xbd\\xb4(\\x0c=P\\t\\x9e\\xbc\\xfba\\xcd\\xbc\\x8bJ\\xe1<\\xba\\xa0(\\xbd\\xd9@(\\xbd\\xea\\xb7\\xcd\\xbc\\\"\\xc7I=\\x92ov\\xbd\\x90\\xdc\\xcf\\xbc\\xcenG\\xbd\\xa4\\xd9\\xc9\\xbcGF\\x8b<`\\x0f\\xb6\\xbb\\x9f\\xd0\\xf2<\\xb3\\xf2\\xf2<\\xf6\\xa9\\x08=;\\xe2m\\xbc=x>\\xbd\\x0ba\\x92\\xbcnc\\xa6\\xbb\\x1b\\xdd\\x1f=_\\xf7%=\\x9f\\x15m\\xbcL\\x9e}\\xbd\\xb5\\xe1e:J\\xcbs\\xbdR\\xdb\\xcc\\xbcJ\\x1c\\x02<[\\xf7\\x05=\\x82\\x0c\\xe2\\xbc\\xf8\\xc2\\xe1\\xbb\\n\\xe2\\xbc=x\\x16\\x87\\xb9\\xc3\\xb7v\\xbbCa\\xe6\\xbb\\x14\\x8fX<\\xa2\\\"\\xc4<Db!=\\xd1\\x94\\xe9\\xb9\\xb7\\xddK\\xbdgh\\xa6\\xbc\\xbe{=\\xbd\\xb2\\xe3\\xae\\xbc\\xcd>\\xd0:g \\xab<\\xe5\\x01\\xc8\\xbc\\x9dK\\x1b<G\\x16\\xa3<\\xdd1\\x1f=\\x8c\\xb7\\x90;b\\x89\\x1e=\\x7f\\xd7\\x06\\xbd+\\xd0\\x11:\\xc0@\\xf0\\xbc\\xdf\\x05\\x13\\xbd\\xdbXX\\xbd\\xd1\\x08\\x16\\xbdtzA\\xbd K\\x80<\\xd9}\\x1a\\xbdj\\x14U<\\xfc\\xf8Z=\\xb7U\\x1e\\xbc>\\xc4:;\\\\\\x17\\xa0<\\x9d\\xcd\\x8e\\xbcJ\\xe4\\x9a\\xbcW&_\\xbd\\xcc6\\x88<\\x131\\x82=\\x1e\\xd2\\x10\\xbc\\x87A:\\xbc h\\x0e;K{\\x90\\xbbU-\\x04;EV%<\\x96\\x93]\\xbc\\xf3\\xcfz=\\x12\\xd5\\xc4\\xbcs\\x86\\t=\\xa0\\x03S\\xbd\\x8dH/\\xbd\\xba(\\x0f;C\\xcd\\x95<\\xf0\\x97\\x8d\\xb9\\xe2j\\xa8\\xbdW\\xbf\\x8b=\\xa6D\\x1c\\xbd\\xb4\\xbc\\n\\xbc\\x9d\\x8fc\\xbd\\x8bG\\x90\\xbb\\xd7\\xb03<W\\xcb\\x93\\xbc9\\xb3%=4\\xa5e=\\xea\\xad\\xc3\\xbb\\xc6k\\x17\\xbd\\xd0\\xb0\\x08=!92<\\xa8\\x95\\xc7\\xbc&\\x07\\x04\\xbd\\x03\\x98\\xbf\\xbb\\xc8\\xfc\\x97\\xbd.\\x88\\x15=\\x1d\\xc7\\x90<\\xfa\\xbd\\x92;\\xb0\\xef\\xa1\\xbc\\xf7P\\xe5<i\\xa6\\x8c\\xbc\\xc2\\xbe\\x0c\\xbct<M=?\\xdd\\x1b=\\xb4\\xe2Q=*\\xe0\\xeb;\\xcb\\x13\\x9f<\\x8e\\xcb)\\xbdZ\\x16\\xbb<\\x97\\xd7\\xda<\\nS\\x08\\xbd\\x88\\xdc\\xcf<\\x99\\xe4\\xa3;n\\x80b=\\xfa\\x16\\x80\\xbd#s=\\xbdx\\x99\\xa9\\xbdU)\\x0e=\\xab(\\xac\\xbcV+`\\xbd^\\xfa\\x8d\\xbdV\\\"$\\xbcx\\x7f\\xa0\\xbc\\xf1{\\xd3:\\x0bk}\\xbc\\xc4P\\xbe\\xbc$\\x96\\x0c<\\xd2zW<t5\\x90\\xbc\\xb3\\xfe\\x92<d\\xd2g<\\xa0X\\x08\\xbd\\x1c8\\xfc\\xbc\\xda\\xd1v<78+\\xbb\\xb7\\xab\\x01\\xbd\\x0f\\xf3l<\\xeew\\xac\\xbd\\x81\\\"\\xd1;\\xe7F\\xa0<\\xb9\\xea\\xd5\\xbb\\xd8\\xfaU\\xbb\\xb4\\x8cN<\\xd3\\xed^\\xbd\\xb5W\\xf6<\\xcaVw:>\\xf72=v\\x10\\xab\\xbdL/\\x13\\xbd\\x17\\x80f\\xbd\\xca-\\xd7; (\\x0c=\\xaa\\xa6\\x02\\xbc\\xf3\\xbd\\x05\\xbc\\x7fT\\xb1<8\\x88O\\xba\\xa2J\\xb2\\xbchf\\xae<\\x12\\xa7\\x01\\xbd\\x1c\\xe8\\x16\\xbc\\xb9\\xc5V=\\xc1\\xf4\\x86;\\xd0+\\xba<\\xdb\\xd2m=\\x06\\x86\\xce<\\xba\\xc9]\\xbc]_\\x11\\xbc\\x89\\x0b\\x15= b\\x8a<\\x96!P\\xbd\\x05+?\\xbcIv\\x9c=p\\x85\\xcc\\xbd\\x00\\xf8\\xa6<\\x1fu\\x91\\xbc\\x00\\xee\\xa1\\xbc\\xfa@\\xf5\\xbc`\\xeb\\xa0=\\x05\\xef\\t\\xbbHJ\\xc3\\xbcG\\x1f\\x1e\\xbd\\xa5\\xaf@\\xbc\\x9c\\xb8\\xa8\\xbc\\xc1\\x14\\xc6=o\\xbd!;\\xf6\\x03\\xea\\xbc\\xaffr=>\\x9d\\xb3\\xbc\\x0e4\\xa9<Ce\\xe7;\\x9d\\xf9\\x81\\xbc\\xd3\\xf8[<\\xda \\xaa\\xbc\\x94\\xd9\\xc4\\xbbT\\xf6_;\\xc3\\x0b\\x0c\\xbb--\\x08=O\\x86\\xa3\\xbdh\\xa6\\x9e\\xbdt\\xd7E=\\xd5I\\x0b\\xbd\\xc6\\x85\\x00=\\x02\\xc3\\xdf;\\xe8\\xf7\\x82\\xbc:\\x83Q\\xbc\\n\\xe8\\x94<\\xa4\\x0b\\xa4\\xbd]/\\x9b;\\xc03\\x1d<x@%\\xbdew\\x88=\\x8c\\xe1\\x92\\xbc\\xe0\\xeb\\xca:\\xfa\\x8e\\t\\xbda2\\x06=%\\xcc\\xc3<\\xf3\\x94\\x8e;a\\xf1\\xaa\\xbb\\xa7y\\x13=n\\xd1\\x08=H\\x0f\\xcb<\\xd5\\x8d\\xd5\\xbc\\x9c\\xae\\x0b=gcs=CG\\xf2\\xbc7\\xa3\\x9e<a\\x11\\xb8=\\x19\\xef\\xbc<\\xbc<\\xe7\\xbcz\\\"\\x03\\xbc\\xec\\xb6\\xe4\\xbb\\xbd\\x9a\\x15\\xbd\\xd2\\xdff<Y\\x01\\xf0\\xbc\\x18\\xd3\\\\;\\x0c\\xe5\\x8f=\\xbf\\x84\\x15=\\x1f\\xe3<<\\xb7o\\xd1\\xbc\\xfdr\\x02\\xbd\\xd9N\\x1c\\xbc\\x8e\\x9e\\xd2\\xbcA\\xd6\\xc4\\xbb\\xde\\x9dW\\xbd.}\\x10<a\\x8a\\x8a\\xbd\\xa3\\xb2h<\\\"\\xe5\\x1c\\xbb\\\"K\\xb7<\\xa2E\\x9c\\xbd<\\xdb=\\xbceq\\x04\\xbc\\x05\\x1e\\xd8<\\xf9Gl\\xbcfp\\x81\\xbd<\\x1a\\xf4<|\\xb58<\\xce4\\xfe<\\xcbb\\xb3\\xbc\\xab\\xc7\\x08\\xbck\\xf7\\xc5=\\xf1\\xe4\\xc3;\\xfb6\\x88\\xbc\\x14\\x06\\x13<\\x80fr=&\\xe1]=qfB=\\x0f\\x18e\\xbcl\\x1f\\xe0\\xbc\\xf4\\xb0\\x04\\xbd:\\xc1\\xfe<`\\x94\\xe4\\xbc\\xd7\\x94f\\xbd&hE=\\x07\\xbe\\xa3<\\xae\\xf7\\xba\\xbcu\\xaa[\\xbdf\\xf3\\xff\\xbd)\\xdf\\xd3\\xba\\xc0\\x9d\\x86<\\x97\\xe7\\x86\\xbc\\\"\\xe7\\xa3={\\x11\\x98=\\xb3\\n\\xa6\\xbbS\\xe4\\xd5\\xbb\\x8f\\x9b\\x83\\xbc\\x9f\\xf4\\x11\\xbd\\xf03\\x1a\\xbd\\x14\\xe5\\xc1=\\x1bU\\xb2\\xbc\\xab\\xbc\\x8d\\xbc\\x97Z*\\xbak\\xd8\\\"\\xbc\\x8du\\x0c=cvo:\\xad\\x94\\x05\\xbd\\xd9\\x8c\\x0b\\xbd\\x92\\x84\\xdf;J\\\\\\xe6\\xbcX>\\x84\\xbdom\\xa4\\xbc\\xed(2\\xbc+(\\x0f\\xbd\\xf2\\xa9m<\\x0b\\xaf3\\xbd\\x0f\\xc7.\\xbd\\x9b\\x127=\\\"\\xab\\xbf\\xbc\\xf4\\x02\\x88\\xbd\\x12\\xb8^=V\\xc9\\x94=\\xf3\\xbf\\xc9\\xbc{\\xae4\\xbb2\\r\\x80<\\xc9z\\x88\\xbd8\\xf0\\xdc<\\x19\\xc2\\xf1<3{\\x86\\xbd?\\xe9\\x9a<{\\xeb\\x18\\xbd\\xf6\\x13\\x00=\\t,#<w\\xe8\\xd6\\xbb\\x80r\\xa3\\xbc\\xcf\\xb8+=\\tfP\\xbcA\\xce\\x96;\\x15\\xbdp<\\xd4\\xdcs<\\x1d\\xed\\xc0<\\xcf+\\xd7<\\x94\\xcf\\x83\\xbc#\\xcc\\xdd:*\\xa6Q=G\\xd6u\\xbb\\xd8rj\\xbd\\xbd\\x91\\n\\xbd\\x1b\\xaa\\xb3<\\x96Gr;\\xb5Z\\x9e\\xbcI\\xee\\\"=\\x98p\\x01=/\\xb4\\x9c;\\xb8\\x95\\xdd\\xbc\\xf3c\\x1d\\xbd$\\xba)=\\x18\\xe20\\xbc\\x85\\t|\\xbc\\x95\\xd6[\\xbb1\\xa1\\x87\\xbc\\x80\\x99\\x96\\xbc%\\x10\\xd9\\xbc8\\xa76\\xbd\\xfe2\\xa8=/\\x8eo<R\\xa04=E\\xbb\\x8c<G\\xb8y=__h\\xbc\\xba%\\xa9\\xbc\\xd5\\\\[\\xbc\\xbcp\\xc7;o12\\xbc\\xe2\\xb1\\xb7<\\x9eY\\xed\\xbbFf\\xdd\\xb9\\xfe|>\\xbdid\\x17=\\x95\\xc0X\\xbd\\xcf\\xd0\\x8b\\xbd\\xa6\\xdd=\\xbd)G\\x89\\xbdZ\\xa9B=\\xa3J\\xc6\\xbbHVy<H\\x19j\\xbc\\xef\\x93\\x1f\\xbc\\\\D\\x95=3\\x1fb=\\xdd\\xa9\\xdf\\xbc]\\xcc_=2\\x0c\\xd4\\xbc\\xb4_\\xb8<\\x14~s=q\\xaf\\x90<}?\\x05\\xbdm\\xaa)=\\x9e\\x88\\xbb<\\xe6\\x1aI\\xbcnn\\xb7<\\xa0\\xec\\xa6\\xba\\x9d\\x8c\\xd0;\\x0b\\xe9\\xd3<|\\x9c\\x91<\\x85\\x01G\\xbcj38<\\xe0E\\x8f\\xbcQ\\x19\\xd3\\xbc\\x0b1\\xd8<\\xebG\\xca;i]\\x03\\xbdj\\x9d\\xf4<\\x9e\\xca\\xc1\\xbb\\x96\\xb5\\xd5\\xbd\\xfa\\x94s<\\x9b%I\\xbc\\xfe.\\x86<&\\xda)=\\xca\\xb8\\x01\\xbd\\xf9m.<\\xbd(\\x91=p\\xeff<\\x92\\xb8V\\t[9%\\xbbp\\x07\\x15\\xbd\\x93.\\x17\\xbc!\\x130=\\xdd\\xb9\\x81;\\x10\\x9b)=4\\x0e\\x03\\xbb\\t\\xef\\x13\\xbd\\xf9\\x94\\xd2<\\xe0\\x14h\\xbb\\n\\x8f\\xd6\\xba\\xc5b%=\\x81\\x90\\xbc\\xbc/\\xb0q=\\xa8F)\\xbc\\xe3wJ<\\r\\x10\\\"\\xbdA>\\xaa<\\x80\\xf7\\xfe\\xbb tQ= \\xaf\\xf6<o\\xd7\\x0e\\xbd\\x04u\\xc4\\xbc?\\xcc\\xd9;w2!<\\xe3\\xbd\\xa3\\xbb\\x07XE<tD\\x1b\\xbc\\xc8\\x9eC\\xbc\\xc0\\x83?<0\\x08\\xb7\\xbb\\xf2\\x1d\\xe5<\\x9e\\xfe\\x8a\\xbcv\\xe8\\x83\\xbd\\xe5\\xc7/\\xbd,\\x9f\\xb29\\x92\\xd8\\xd0<\\x9f\\xfe\\x0b;\\xbc\\xb1\\x8d=\\xb8\\xc8\\xc1<\\xc9%\\xb5;4\\xf0\\xd8\\xbbw\\x90\\x0b=\\xbcJ\\xee<}Ku=\\x12\\xc9\\xd1;\\xa9j\\xbb=j\\x1e\\x9f;j~\\xf2\\xbc\\n\\xce\\n\\xbd7\\x98P\\xbd2\\xc2\\x19\\xbd\\x8e\\xba5=\\xefK8\\xbc\\xfd\\xe1\\xa2<Wr;<\\xf3W*<\\xd4\\xf2\\x87=\\xfe\\xf0\\x93;W\\xa7$=\\\"\\x12\\\\<LU#\\xbcH\\x0f\\xe2\\xbbz=\\n\\xbbH\\xb4V\\xbd~\\x95\\x82\\xbd\\x98\\xee<\\xbd\\xeb\\xacF\\xbc\\xf7#\\xd3<\\x9c\\xe2\\x84\\xbcD\\x94\\t=\\xca0\\xad\\xbc\\x8aq\\x8b\\xbcOQ\\xc9<F\\x19\\x85=w\\xc3\\xd4\\xbc\\xc6\\xee\\xb0:\\x06\\xf2&=\\x9d\\x124\\xbduh\\x10=Y\\xb3\\x8a\\xbcU7+<\\xcd\\x8a\\x01\\xbd\\xaaO\\x8c=\\xf1\\x12\\xa6\\xbc>,\\xb1<\\xba\\xf5f=\\x94\\xf3r=\\xba\\x9c\\x04\\xbd*Y\\x84<\\xa5\\xcb\\xc7\\xbc!\\x11\\xd0<\\xbe\\xc4\\x85=\\x04\\tW:Q\\xb4\\x90=\\xf8\\xaa0=\\x02@\\x83\\xbdP`\\\"<\\xc0^\\x13\\xbc\\x80\\x1d\\xfc<99#;M\\xd8$<\\xf6)\\x95\\xbbr\\x86n\\xbdm\\xa1\\xa8<\\xa1<\\x86\\xbc\\tp\\x12=j\\xa2\\xe9\\xbc\\xec\\x11\\xa1\\xbd\\xfb\\xa5\\xe9\\xbcb\\xee:;H6\\xb8;\\x88\\x00s\\xbd\\xe9\\xf8\\x9a=qU\\x9d=\\x89\\x15\\xfa<\\x90.\\x08=MI\\xd4\\xbcN\\x9d(\\xbc\\\"Q\\xa7\\xbd\\x02\\x96\\x01<F\\x91\\xd9<\\xf3\\xb5\\xf5\\xbb\\x1c\\x13\\xe7<\\x14|b=\\xf5\\x1b\\x94\\xbb\\xbe` <\\xac(0<9Z==\\xbe7{\\xbdr\\xce\\xf3\\xbc\\xdf\\x14\\x8a\\xbd\\x15is=R\\\"\\x9e8\\xadV\\x99=F\\x8d\\x1f\\xbc\\xcd<\\x15=\\x9f\\xa7\\xad\\xbd2\\xebH;g\\xb46\\xbb\\xbf#\\xab;\\x82\\xf2.\\xbdIa@\\xb9\\xa9\\xa9\\xd0:Sf\\x02< L/\\xbd\\xb4?\\xbc:\\xb68\\xb0\\xbc5\\x04\\n=\\xd7\\xedy\\xbcR\\xb5d\\xbc\\xd1\\xcd\\x16<:\\xed\\xf2\\xba\\x03\\xe6\\xe6<\\xe8\\\"\\x1f<\\x074\\x07<0\\x9cp\\xbb\\x10\\xdc\\xdf=\\x96\\xe6 9/\\x90\\x8d;\\xc4\\xdeY\\xbbV$\\x8f<A\\x98\\xad\\xbb+\\xb4\\xb8<\\xa5\\xa9\\x18;\\xb4\\xdc\\x92\\xbcvw-\\xbd\\x91{\\xc8<1E4=AK\\xe4\\xbb\\xd53W<\\x14\\xdb\\x94\\xbc\\x07?e<n\\xbbq\\xbc\\xcf\\x05\\x83\\xbco\\x7f\\xa3=\\xa9P\\x15<\\xa7\\xd75=\\x11\\x9d\\xaa<\\x17\\xb75\\xbc\\xba\\xe5\\xae\\xbdB\\x12~\\xbd\\x86\\xc9(=\\xef[\\n\\xbd \\xe3\\\"\\xbd%\\x1c\\x85\\xbcR\\xf2E\\xbb\\xactt<\\xeaRK=\\xcd\\x04m<Am\\x02\\xbc\\xad\\xd0\\x9d;\\x03\\xe4@\\xbd\\x81a\\x0c\\xbc\\xab\\x92\\x1c\\xbd\\x9b\\x0c\\x07\\xbd,\\xde&<\\xb8u\\xbd\\xbc\\xd0<?\\xbdi\\x08K\\xbch\\xd8\\xbc\\xbb\\xb2\\x80=<~p\\xeb\\xbc\\xd2\\x91\\xae\\xbd\\xcb\\xe2\\xd6\\xbc\\xc2s-\\xbbI5\\x19<,P\\n\\xbd\\x81\\xfaK\\xbc\\xd8\\xce\\xf4\\xbc\\xdb\\x97\\xf2<i\\x17K\\xba\\xad]<=\\xa6\\x9bE\\xbb\\xda\\x83\\x11\\xbc\\xb2\\x0e\\x13\\xbc\\x91\\x84\\xa8<m\\xa5S\\xbc\\xe3B9\\xbd\\x19\\x9dn\\xbd\\x84\\xd6}\\xbcs\\xb5Z\\xbc\\x02C;\\xbb\\xf5\\x01\\x98=\\xbfP\\xa4=n\\xdfU\\xbd\\x0bh\\xb2<\\x10\\xa2(\\xbd\\xa9A2\\xbd\\xf0G\\xb0\\xbb\\xfb\\xab\\x82;7\\xf5\\x81<\\xa0\\xdb\\xda\\xbbP\\xc4>\\xbcK\\x9f\\xe6<26H\\xbd\\xd4\\x87\\xdd;\\xc5\\xa95\\xbbWY\\xbb\\xbb)8\\xf4<\\x0bk\\x0c\\xbce}M\\xbb?\\x93\\x91<\\x18~\\xe1<\\x8a:\\xd7\\xbc$1P=N\\xa3\\xcb\\xbc\\x91g\\xcf;M\\x9cV\\xbd\\xa1\\xcey<\\xd5\\xdb\\xed<\\xdeJ\\x83\\xba\\xe4x\\x08\\xbc\\xd4\\xfb`=\\xc3\\xb9!\\xbdkv\\x0e=\\x8eK\\x16\\xbd\\x0c*\\xca<\\xa2\\x80\\xa1\\xbc\\xcc\\x92\\x8e\\xbd\\xf0\\xae\\x84<#Q\\x1a=\\xe1\\x02\\xb1<n\\xc7\\xd6<^=\\xb2=\\xc3\\x91R\\xbc\\xc5\\x85\\x99=\\xa0>\\x0b=\\x01|>\\xbd\\x9c\\xb2,=b\\x15\\xc4\\xbc\\xda\\x06<=\\xbaU:\\xbc\"\nHSET bikes:10032  model 'Enceladus' brand 'Velorim' price 3421 type 'Kids bikes' material 'alloy' weight 11.8 description 'These bikes pretty easy to ride while also being lightweight enough and quite durable. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\xd2&\\xa8\\xbc\\xb0\\xfd\\xbe<\\xf6nt\\xbcQ\\x00\\x01=\\x8aB\\x00\\xbd&\\x86P=:\\xdbm;E\\x17\\\"\\xbd\\x7f1G=\\x17\\\":=j\\xd1 =\\rV\\x03;IG\\x13=\\x8a\\x16U;\\xa7u\\x88=\\x8f\\x15\\xd5\\xbc_\\xb9U=\\x878\\xc6\\xbc\\x9f\\xf6\\x1a=\\xaa\\x88\\x86\\xbdK\\xbbC:\\xe0\\xb5\\x0c\\xbd\\xa4\\x11G= \\x82e\\xbd{H\\x9d\\xbd\\xbc\\xabc<\\xc6\\xa1o=\\x87\\x19\\xee\\xbb\\x9a\\xa7%\\xbd\\n\\xc6\\xe8=\\xed\\xc4>\\xbd\\xa2\\xa8?\\xbd6$\\xe7<\\xd1\\x0c\\x13\\xbc\\xbc#y\\xbaa \\x9b<\\x9e3\\x1b<\\x8a\\x8bD\\xbc\\x06v\\x16\\xbd\\xd5\\xba.\\xbc\\xde\\xae\\x9c=\\x96\\xf4\\r\\xbc-\\xe5_=\\xb0\\xcc\\xbe<h\\xe8u\\xbcb\\xe4\\xb9\\xbc\\x1e&\\xc2=\\xd5_\\x1e\\xbd\\xd3\\xb4\\xbf\\xbcD\\xbdg\\xbbqL\\x08\\xbd\\x14p+\\xbd\\xba\\x97\\xdc\\xbc\\x95c5\\xbc\\x0b_\\xe5\\xbc&&p\\xbc\\xf4\\\"c\\xbc.\\xdb\\xe8\\xba\\xbd \\xff\\xbcT\\xb4n=\\x1e}=<3\\x0fG<\\x8d\\xbd\\xea<\\x1c6\\xe1<\\xe2\\x18,=;2\\xed<9B%\\xbbw\\x84\\x9b<\\xb4y\\xb7<\\x16\\x839\\xbd\\x9a{H<\\xe1\\x12\\x10=\\xf8\\xfa\\x93\\xbc\\x00\\x00\\xe2<\\x81\\xcc1\\xbd\\xcd\\xd4\\xbf\\xbc\\x17\\x8f\\xa2<>,\\x90\\xbcy\\xdd\\x93\\xbd]\\xd8><\\xb1\\xcf\\xdd<\\xf1Q\\xdf\\xbc\\xc8\\xe4e<\\xbb\\xc7\\x86\\xbd(\\xee4<+\\xf1\\xbb;\\x07\\x19\\xa0<$D\\x93<*J/=\\xbb\\xe1\\x0e<);\\x04\\xbb\\xc7NR\\xbd\\x1aF\\x8e\\xbd\\x96\\x80&<\\xac \\x0f=\\xfdI\\x12<u#\\x05\\xbd\\x93\\x1f[\\xbd(\\x91J=\\xb0\\xdd\\xac\\xbc\\xf2\\x079;\\x15\\x04\\x9c<\\xf6\\xc0\\xfa<\\x8d\\xf8\\xc5\\xbc\\xe4\\xe5\\x80\\xbc\\xbe\\xc3\\xa9=5}\\xe6;\\x05\\xba\\xac\\xbb\\xe1\\x0en;s\\x1f5<d\\x10\\x9c\\xbcOc\\x04=\\xb1\\xa7\\xb7<\\x92\\x9d\\xa0\\xbc\\xde\\xa6w\\xbd\\x90\\t\\x02\\xbd\\x9f{\\xa6<S\\x9b\\xb7\\xbb\\xa3\\xdf\\x11=\\x8d\\xa7\\xd2\\xbc\\x85q\\x83\\xbc\\xce\\xff\\x16=\\xd6\\x92/\\xbdf*\\xcf\\xbcF\\xa66=H\\xbcD\\xbc\\xc06O\\xbc\\xe8\\x9ah\\xbdD\\xb3;\\xbd\\x9c\\xd7\\x03\\xbdm\\x92\\x82\\xbc\\xa6\\xa1\\xf6;\\x80d\\x1e<\\xf6\\x8b\\xb8\\xbc\\x8c\\x0e\\x0e\\xbc!>\\x05=X!c\\xbcgV\\x9b\\xbcI\\xd0Z:\\x81Vt\\xbd\\x80\\xe6 \\xbdz\\xb7\\r\\xbd\\xa83\\xd5<\\x16|\\xe6=\\x9d\\x01\\x14=#u\\xc3\\xbc\\xbe;\\x9f<g8\\xa7:\\xbe>\\xde\\xbc.|\\xd4\\xbcTEI\\xbc~.x=\\x0b\\x07/\\xbd\\xbb\\x07n<DT\\xb2\\xbd|\\xec\\xaa;\\x18r\\x90<\\x86i1\\xbd\\xc6\\xe2A=\\xc0(Y\\xbd!\\x93O=\\xd3P\\x12\\xbb\\xcc?\\xeb\\xbc;\\x9fR\\xbd\\xba\\xacO<\\xae\\xfaB=ZC\\x07\\xbdYu\\xac<\\xfc\\xb12=9\\xcd\\xc2\\xbc2\\xc8\\x98\\xb9\\x10\\xdcI=<\\x1f\\x9b\\xbc1g\\xf9\\xbc\\xa3\\x81\\x05\\xbc\\xf1\\xbcB\\xbc\\xe9\\xf9\\x13\\xbd\\x0e(i=\\xc7\\xd8\\xab\\xbc\\xe2/\\\"\\xbc\\xf9\\xe2\\xbc\\xbc\\xd5\\x97+<\\n\\x86l<\\x8134\\xbcQL\\\"=\\x1b\\xc8\\xc7\\xbc\\xfa\\xf8/=\\x1anv:\\x02\\x18\\xb2:,+\\xbd\\xbb\\xd8`Q=\\x8d\\x8d2=Z\\xe7-\\xbdgV\\xff<8\\x89\\xb8\\xbb\\xd5`O<;Pm\\xbd\\xf9\\xc8\\x0c\\xbd\\x03\\x1a \\xbdq\\xd07=)\\xe1q\\xbc\\xc1\\x8c_\\xbd\\x12Hh\\xba\\r\\xca\\x8a\\xbcj(\\xed;\\x95}\\x11=\\xf5-\\\"=J\\xef=\\xbd\\xe1\\xadx<`A\\xde\\xbc\\xcd\\xbb\\x84\\xbc\\xba\\x91\\x19\\xbcJ\\xbc)=c\\xaa\\xdb\\xbc\\x95\\xa2N\\xbcD\\x97\\xc2\\xbb\\x1c\\xeap<b\\x99\\x88\\xbd\\x8a\\xfc\\xe3<b1\\xd5\\xbd\\xc1\\xb53<_\\xe5\\xa7\\xbc\\xe4Kp\\xbcu9S\\xbc\\xe9G\\x04\\xbd~K1\\xbc~\\x98\\x01=\\xacX\\x06\\xbd\\x14\\xc3\\x98=aU\\xc2\\xbc\\\"B\\xe3\\xbc \\\"K\\xbd\\x9e\\xb8\\xe9\\xbc\\xcd\\xb6\\x81=\\xb7\\x1e\\x1f\\xbc\\x1c>V=\\xe3\\xd4\\xa7<\\xfdNA=\\x14,7\\xbcN\\xee\\xe4<:h\\xe5\\xbcv\\xb7\\xa5\\xbc\\x8b\\xb6\\x15=\\x11\\xcd\\\"\\xbc\\xcd\\x93&<mA\\xbc=\\xfd\\r\\xa1\\xbb\\xb2\\x1f\\xd3\\xbd\\n\\xce\\xc2\\xbd\\x05jY<ko)\\xbcJ\\xab\\x9c\\xbct\\xac|\\xbc\\xf0\\x01\\x10<Rbo\\xbdj\\r*=b*\\xa8\\xbc)\\x05\\xb9<o\\xa7X\\xbd\\xd8\\xc9a=\\x17\\x1f\\x97\\xbc\\x13\\\"\\x1a\\xbd\\xbfq\\xfe\\xbc9\\xe6\\x02\\xbd\\xb1\\xdf(\\xbd\\xee\\xba\\x9f=\\xa7z\\xb5\\xbc\\x1a[\\x0f\\xbd0\\xd2g<\\xd3\\x8d\\xed<\\xe3M,\\xbb\\x9b\\xce\\xcf;\\xc79\\x81;\\xd5\\xd0\\x8f<r\\x8c\\xcd\\xbc$ \\xa6;uQ\\xee\\xbcQ\\xb1\\x92;RmI=\\x91\\xb4\\xbf\\xbd\\x00\\xb1\\xa9\\xbcs\\x1c\\xe7\\xbb\\x8f\\xab\\xa0\\xbc\\xacX\\x14\\xbc\\xb1G\\xd2<B\\xd8\\x17\\xbc\\xef;\\x07=,\\xe9\\xa6<-\\x05\\xb5\\xbd\\xcc\\xf8R\\xbb\\xf4\\xae\\x81<\\x16\\xd9\\\"\\xbd\\xee\\xce\\x9a<j\\x8a,<\\nC\\x92<c\\x90\\x0f\\xbdK\\xf7\\x04<\\xc9Q\\x14=\\x08o\\xb5;\\xa8@\\xa2\\xbc\\xaed\\xf2<jS\\xc2<\\xe1\\x11\\x03=\\x0eE\\x18\\xbd\\x1b\\xdf\\xc9;\\xd0\\x07\\xc0<\\xff\\xa9\\x919\\xe8\\xf0\\xfc\\xbb\\xfd&\\x93<\\xcc\\x9d6\\xbdaV\\xee;E\\x1dy\\xbdD\\xba\\x03\\xbc\\xe0\\xd1\\xfd;;U\\xf8<\\x07\\x141\\xbd\\xe4cG\\xbd\\x9a\\xe9q=\\xd9\\xac;=`yt\\xbc\\t9\\x9a\\xbc\\x9b\\xf3\\x1f<F\\xa2\\x8c\\xbc}^\\xb7<\\xe0\\n\\x10<\\xd0\\xc9\\x8d\\xbd W\\xbf<\\x93\\xc8\\x9a\\xbbn\\x00\\xc3;-\\xd6\\x84:\\x1c\\xeb\\xce<\\xa9\\xb7w\\xbd\\xb9\\x11\\xc6\\xbc\\\"\\x9b\\x94\\xbb\\xdcp\\x97\\xbc\\xcd\\xaf\\x92\\xbak3\\r\\xbd\\x7f\\xc8Y=:\\x85j<\\x10\\xfe\\xc4<\\xe5^\\xc1\\xbb\\x1a\\xd5L\\xbb-\\xd0\\xa8<m*\\x9c\\xbc\\\"\\xd0\\x01=\\x87EV=R\\xc8\\x88=\\x14eG=\\x9f\\x0c\\x83=7F\\x01=\\xc7\\xfe\\\";\\xb2\\\"\\x81\\xbcms\\x93=\\x05\\x86\\x02\\xbc\\xfb\\x7f\\x04\\xbd\\xfe\\x1d\\xea<\\x8d\\xb6\\x03\\xbcZ\\x92\\xa3\\xbd\\x19\\xb2e\\xbcm`\\xc9\\xbd\\x84J\\xae\\xbb\\xef\\x07v<_&\\xbc\\xba\\x97LH=\\x91<\\x18<\\xda\\xda\\x82=\\x84\\xc4\\xea\\xbc\\xb4\\xd3b;k8#\\xbd\\xb1\\xcb\\xc7\\xbc\\x8f\\x0bu={\\xc2 <\\x83z_\\xbcIn=\\xbb\\xe5\\xd5\\xb4<m\\xf1S=Auo\\xbd(\\xee\\\"\\xbd\\x82\\xa8\\xac\\xbc\\xbb)B=M\\x05\\x1e\\xbd!\\xd1\\x9c\\xbd\\xf3qJ\\xbd~\\xd1$:\\x11X\\x1c\\xbdq\\\\\\t<\\x00\\x06\\x84<\\xdf\\xc2\\x19\\xbd\\xf0\\x9cN=\\x1a\\x04>\\xbb\\x7fjP\\xbdcE\\xb7=Q\\xaa\\xa6=t\\xf5a\\xbc\\xaf\\x8d\\xa6\\xbb!\\x96\\x88\\xbc\\\\b\\xaf\\xbd\\x067\\xb0<^\\x97)=0=!\\xbdp\\t\\r<\\xcc\\x92\\xb0\\xbd\\xf8s==\\xb4\\xa6\\xb7<$\\xe9T\\xbc\\xcd\\x8d\\xcf\\xbbZ\\xb4C=\\xf9\\x9bY;\\x1d(%=\\xc5\\xecJ\\xbc\\xfbIA=\\x89\\xf4\\xc4<\\x00\\x0f\\xcc:\\xca\\xbeI\\xbc\\x86\\x87T\\xbb&\\x1f\\xf7<z\\x8bo<\\x8a^\\xed\\xbc\\xdc^\\xac<\\xe9\\xec\\r\\xbd\\x10\\xc2\\x01;\\xf7\\xee\\x1f\\xbc\\x1b,\\xdf;\\xd6\\xeb\\x1b=~*I\\xbb\\xad\\x91\\xf5\\xbcR\\x8dB<\\x04\\xbaG=\\x18\\x14\\xab\\xb9\\x1a\\x11\\x8d;w\\x0c\\x02=\\xbd\\xbd\\x10\\xbdI\\xc3z:\\xd9\\\\\\x81\\xbd\\nA\\x12\\xbc\\x82\\xccP=1f\\x10=\\xd8g\\xe0<\\xd7\\x06\\x85\\xba\\xc4\\xca7<\\xab\\xa3{\\xbc\\xa9\\xcb\\x8c<\\x07,\\x05\\xbd\\x02\\x1b\\xab\\xbcc\\xda\\x9f<#o\\xca\\xb9\\xe2!\\x0b=!\\x01\\x02<\\x92\\xf1\\xbb\\xbc+\\x1cW=\\xb2\\xad0\\xbd\\x98\\xd5\\x89\\xbd\\x9d\\xd6\\xbb\\xbc\\xbf\\xeb/\\xbd\\xdd\\xdd\\x02=\\\"!\\x91;\\x8a\\xdb\\x909Jm)\\xbd%\\x05.\\xbdt{d=\\xe1\\xaf\\x88=\\x0e#\\x04\\xbd\\x10zu=\\xad\\xcf\\xbb\\xbc\\xc8]\\xd8\\xbc\\x91\\xce\\x87<\\xf2\\xb5\\xf8<\\xeb\\xe5\\x90\\xbc\\xfd\\x11\\x08\\xbb\\r\\x82%=&9\\x96\\xbc\\xcf\\xd7\\xe8<tG\\xd2<\\xb9x\\x7f<\\x1a\\xc0\\xb9;\\x82O\\xe9<1VR\\xbd\\xc8\\xbeT\\xbc=\\xc7\\xe2<\\x1e\\x10\\xed\\xbb\\x8b\\xad*=\\x03%Q<\\x7f.\\xa1\\xbc\\xb5:\\x06=\\xc1Vm\\xbc\\xac\\xbd\\xbd\\xbdXO\\xb3\\xbc\\xf8\\xe3d\\xbcw\\x81\\r\\xbd\\xcb8\\xc1<f\\xd0#\\xbd%Be\\xbc\\xc1\\xe3\\xc1<\\xe7N\\x03<a\\xe2S\\t~\\x05\\xb9<co\\xb6\\xbb\\xea\\xa4(=\\xfeAT\\xbbF\\xb2\\\\\\xbc\\n\\xf8^=\\x84\\xd5\\\"\\xbc-\\x8a\\xbd\\xbc\\x1a \\x01=D\\x08\\xc1\\xbc\\x14c\\xb8<m\\xcd\\x81<.Y\\xb2\\xbc\\xc8gJ=6\\xb0\\xf5\\xbb~\\xac\\x1e=d\\x15\\x90\\xbd$^-\\xbd\\xf5\\xcfS:~4T=\\xd5!\\x1a<z\\x1cQ\\xbc\\x93\\x89\\x96<\\x16x\\n=q4b;r\\xf6\\x80\\xbb\\xc4<\\x8c;\\xbcz\\x08=\\xa7\\x13\\xed\\xbcs\\xf4\\xd0\\xbc\\x9ac\\xb3\\xba\\xcdD\\x8a\\xbb}\\xd0\\xcb\\xbbR\\xee\\x83\\xbd\\t\\xa1m\\xbc/\\x8d\\xa4\\xbb\\xe9\\x7f\\xeb<<\\xf3\\xde<\\xec\\x98\\x85=rL\\xf2<\\xc9\\xe9\\x83\\xbcR\\x86D<\\x98\\x82d=\\xb2\\x8e\\xfc\\xbc\\xb6\\xb8\\x1d<\\xfd\\xd2g\\xbay<\\x06>\\x8dH\\x82;\\xee\\x8d\\xb4\\xbc\\xcbj\\xfd\\xbc\\xb8\\x92]\\xbdrm\\xc5\\xbd\\xaa\\xb8\\x04==\\x11\\xf6<\\x10\\xe6\\x02;\\x9cJ\\xf4<c\\x00^\\xbd\\xd1\\x16\\x9e<\\xce\\x01\\xa8<\\xf7\\x9f\\r=Q\\x06\\x87<\\x1e|\\xec8c\\xa2\\xa1\\xbc\\xf0_K=TE\\x9d:\\x86N\\x88\\xbdc\\x1c\\xf3\\xbcf|\\x1e\\xbc\\x87\\xed\\xce<%I%\\xbb\\x84\\x90:=\\t\\x98\\xdb\\xbcq\\xe7\\x1e=\\xe7pD=\\xb2\\xfbL;\\xac\\x08\\xea\\xbb%#\\xa5\\xbc\\x1d<H=\\x9e\\xc3\\x99\\xba\\xbb\\xe2\\x0b=\\xac\\xe3\\xd3\\xbc\\xee9\\xac;?\\xa6\\x10\\xbd\\xab!R=>\\xfd>\\xbd\\x14\\xea\\x89<\\xa5\\xb4\\x0f=\\xd9K4=!j\\xcc<\\x96K><$\\xd3\\xdf<\\x1a\\xb8\\x06<\\xcaO!=\\r\\xb6\\xdf9\\xcd\\xb2\\x80=\\xa63h=V\\xcd\\xa0\\xbc\\xcd +\\xbd\\xf4\\xdf}<\\xca\\x7f\\x1e<\\xde?\\x17\\xbd\\x9e;\\x87<\\xa5\\x8a\\x16<n\\x01\\x1e\\xbd\\x00])=A\\rE\\xbcK\\x10\\xba<.F\\x1b\\xbc\\xe9\\xbdl\\xbd\\xb4=\\xe8<\\xe6L\\xb3=\\x9b99\\xbd\\x8d\\x08\\\"\\xbd\\x10w_=Q\\x03\\x89=`\\xf6\\xc6<-\\xf0\\xd0<\\xa1\\\"<\\xbc\\x03D\\xe9\\xbc&\\x824\\xbd\\xb8&\\xb5<d`\\xc5<\\x11b\\xfd\\xbb^\\xe0\\xfd\\xb9\\xfd\\xf0\\x0c=s|\\x81<\\x04c\\xf0\\xbc,z\\xfa<\\xa4$\\r=\\xb2\\xc8h\\xbd\\xbfSG\\xbd\\x11\\x94\\x8a\\xbdw+#=\\x1e\\xa8\\xed\\xbb\\x82m\\xa5=\\xc9\\x92@\\xbc\\xc01T=a\\xc5I\\xbd?\\xd8\\xd5\\xbc\\x97J\\xca\\xbc\\x84\\xcf5\\xbc9@\\xa9\\xbd+\\xf8\\x9b\\xbc\\xf1\\xb6\\xfd\\xbb`a\\x83<)W\\xb5<\\xac\\xfe\\x1d=\\xcb\\xf7K\\xbb\\xbc\\xa7\\x00=y\\xf5\\n=\\xf0O\\x87\\xbc\\x16\\x84\\x04\\xbdu\\xfc`\\xbd\\xe0\\xd0\\x14=?\\xce\\xcc<\\x92\\xe6\\x93<\\xbe\\x07\\t\\xbd;\\x08\\x95=\\x1c\\x91\\x18<\\x04\\x140;\\x16\\xe6\\x08=\\\"\\xc4\\xe7<!\\x97\\x85\\xbc\\xe8\\x91E\\xbc\\x984\\x08\\xbd\\xd4\\xbf\\x17\\xbd\\xba-F\\xbd\\x8f\\x12\\xe6\\xbc\\xbc\\xf4\\xcc;\\xb1\\x96/\\xbdl\\xd5\\xe3;\\xfb\\x92\\xce\\xbc\\x9a\\x87.\\xbc\\x85\\x99\\x92\\xbbFo\\xda\\xbc\\xdcK\\x15=\\xb4\\xc3\\xc7\\xbcVf\\x82;\\x83){=?\\xf9\\xaf:\\xd8\\xd5(<=V\\xdd\\xbds\\x14`=\\x7f\\x8d\\x8e\\xbcil\\xdb\\xbb\\x06xF\\xbd\\x16An<}@\\xe3\\xbc\\xfe\\xb8\\xf0<\\xe9\\x93\\x93<\\xe1\\xc9\\x1c\\xbdc>\\xce<M\\x99\\x13\\xbd\\x1e\\x90\\xa8<\\x83=\\x9d\\xbd\\xaf\\x9f\\x08\\xbd\\xce\\x9e\\xd5\\xbcm\\x88\\xa6<\\xa3+\\xc7\\xbc2\\xee\\x96<\\x94\\xa0T\\xbd]:h\\xbc\\xfa\\xd8\\xde\\xbb\\x0f\\xec\\xc4\\xbd\\xb9\\xe3\\x7f\\xbc\\xec\\xd8J;U\\x97\\xf3\\xbb\\xaa\\xf6\\x01;\\xbe\\x12\\x90;b\\xe0T\\xbd\\x14\\xf0\\x1e<\\tH\\x96\\xbc\\xe5\\x00\\x81<\\xfa\\xd5\\x94\\xbc\\x81\\t\\xf8\\xbc\\xa5\\xa5\\xbc;\\xcfi\\x04=#\\x7f\\xd1\\xbc:\\xc7\\x98\\xbd\\xbd\\xc9 \\xbd\\xb6\\xd7\\x8e;\\\\|B\\xbd\\xc3O\\xa4<\\\"(\\xbb<\\x17\\xd3P=\\xb4\\xe7\\xfc<\\xcd\\xab\\x14= sM\\xbd\\xad\\x08c\\xbd\\xd4\\xdc\\xb3<\\n@\\xc2<\\xe2X;=\\xd3\\r)\\xbd\\x88s\\x87\\xbbj\\xad\\xf7</o&\\xbd4G[<\\xaeF\\xf7<\\x18\\xa6c<rX\\xd3\\xbcD\\x80\\x04\\xbc\\x9cx\\x86<\\x99\\xb2Z=\\x99\\xb4\\x1d<\\x84\\x88V\\xbd0\\\"\\xd6<\\x93\\xb9\\xa4\\xbc\\x98\\x06b<\\x90+\\xef\\xbc\\x05\\xdc\\xab\\xba-\\xee\\xbb:\\x19\\x16\\x92\\xbb\\xde\\xe5&<t+\\x84=`V\\x8b\\xbc\\x89I\\xd3\\xbc\\x92\\xe0\\xee\\xbc\\xfb\\xe6\\xed\\xbae\\x13n<\\x99\\xf5I\\xbd\\xe2\\x1b\\x8a<\\xbcN#=\\x1b\\x10\\xe2<\\xc1\\x88\\x05=y\\xa0\\xab=z@\\xdf<r\\xa7\\x1d=\\xd6\\x85\\xd9=e(\\xac\\xbc\\xb1M\\xe2<\\x81\\xff\\x1e\\xba\\xbc\\x18\\xfd<i\\x8b\\x10\\xbc\"\nHSET bikes:10033  model 'Salacia' brand 'Nord' price 2194 type 'Mountain bikes' material 'alloy' weight 7.9 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\xd7\\x90\\x8d\\xbc\\x85\\xf4F\\xbb\\t\\xbb\\x11\\xbdI\\x8cb=\\x92\\x9f\\x1d\\xba\\\\\\x06_=R\\xbdB\\xbc\\xbbW\\xf2\\xbc\\x9c\\x0cM=\\x13\\xbe\\xab<\\x92\\x1f\\x06=\\x12\\x8e\\xab<\\x9dL\\xcc<%\\x8a\\xd3;\\x8e\\x8b\\xd7=\\nY$\\xbd1\\xb26=\\x96\\xbeB\\xbd,\\xe5\\xc3<\\xf4\\xecN\\xbd\\x8c\\x08\\x1e\\xbc\\xd6W\\xf2\\xbcy\\xcd\\x19=\\xbbg\\x1e\\xbd\\x19\\xd1\\xb6\\xbc<+\\xa8\\xb9\\xa5\\xda\\x13=\\xa1\\xb8l\\xbc\\xcdj\\x87\\xbd\\xe4\\xb4\\x84=\\x16\\xf3\\x9a\\xbcB\\xa5\\x18\\xbd\\xf9x\\x8e<\\n\\xf5\\x86<[\\xba|<\\xefoT<\\xa5K\\xa0<w\\xd5m\\xbc\\xc9s\\xef\\xbb\\xaaR\\xaf\\xbb\\x01\\x90\\x9d=\\xbfCE\\xbb\\xad,<=\\xb5o\\x0c<0\\xc4\\xb0\\xbcV!\\r;s\\xbc\\xd7=\\xf1\\x1f)\\xbd\\x90\\xed%\\xbc\\x19\\xc3\\xee\\xbc\\xd2\\xcb8\\xbd\\x96\\x97!\\xbd\\xf9\\xa6\\x04\\xbb\\xac\\xf6\\xea\\xbb\\x18\\xb9\\x9a\\xbc\\x81i\\xf7\\xbc\\xf2\\xacg\\xbb\\x0cr_\\xbddY+\\xbdu\\xda\\x91=Vu\\xcd<\\r\\x9e-\\xbcP\\x81\\xc7<\\x88\\xe9g=\\x80E\\x13=P>\\xfb<7\\xfc\\xa7;\\xae\\xc1\\x87<\\xae\\xf5 =5V/\\xbds\\x11\\xc8;v\\xfe&<\\x1d\\x15\\xb6\\xbc\\xd7\\xbe\\n=r\\xa2K\\xbd\\x0c\\xe5E\\xbd\\xc1h(=\\xef\\x8b\\x0c\\xbdI\\xa3l\\xbd \\x07\\x11<:+\\xf0<<X\\x14\\xbdM\\x10a\\xbb\\\\\\xb7\\x8c\\xbd\\tK\\xc2<\\xdb\\xd8\\xaa<\\xad\\xd4\\x11<\\x13\\xe7\\xae<%\\xd9N=\\xc6\\xc0B<\\xe6\\xfd\\xd6;\\xce\\xb36\\xbd\\xaa\\xb4B\\xbd\\xe8b\\xc6<\\xef\\xf1\\x1a=?\\x19\\xaa<Z\\xec5\\xbd\\x15\\xe6?\\xbdN\\xe9\\x1d=\\x04\\xd9M\\xbdu\\x99%\\xbdr\\x93\\x8d<\\xeeY\\x06=\\x83n\\xc2\\xbb\\x9f\\tZ\\xbc\\xc0\\xb0\\xba=V\\xca\\x86\\xbb\\\"\\x07\\xdd;w\\\\\\x03\\xbc\\x92\\xca\\x0c\\xbc\\xd3AP\\xbcd\\xf5U=\\xce\\xa9U\\xbc\\xc8\\x04\\x07\\xbd\\xfa\\xe7V\\xbdV#5\\xbc\\xfd\\xc3\\xe4;\\xed\\xaf\\x86;^\\x11\\xca<u|\\x8c\\xbd\\xa8\\xfb\\x97\\xbcS=:=\\xd6\\x14\\x16\\xbde\\xe0\\x1d\\xbc\\xf3Z\\xe2<\\xe3(\\x99\\xbcXZd\\xbc\\x90o\\xff\\xbc\\x93}\\x0b\\xbd\\x0e\\xa9\\x8a\\xbd_\\xb7\\x0b\\xbd\\xa1\\xe1\\x8e\\xbc\\x93\\\"\\x17<\\x1f\\xc8e\\xbd\\xa3\\xab\\x7f\\xbc\\xa8\\x808=e\\xd0f\\xbc\\x9dN]\\xbcWOQ<\\x07\\xcew\\xbc\\xf0\\x0cW\\xbd&\\xc9v\\xbd\\xd4Y\\xda\\xba\\xbc\\x04\\xbc=\\x0b\\xa9T\\xbb\\x94\\x0e\\xb3\\xbc\\xd09\\t=\\x84b(\\xbc\\xbb\\xc3\\x16\\xbb\\xb8\\xf0\\xbc\\xbc9\\xdb\\xe9\\xbc\\x19am=\\x0bW\\xec\\xbc\\x9f\\x93\\x17<I\\xb8\\x86\\xbd\\xdf\\xc0\\xae\\xbb~\\xb4\\x10=\\x99\\x8f\\x8e\\xbc2\\xbaA=by\\x9c\\xbd\\xe2/h=@\\xcc\\xec\\xbc\\xca[\\x06\\xbc\\x11~\\x8e\\xbd\\x19\\xf6!<%\\xe6\\x10=\\x1b\\xfa$\\xbd\\x9fR\\xcd<.\\xc2F=\\x07\\n\\x19\\xb96\\x83\\\"<\\x99\\xb3:=h5\\xaa\\xbb(\\xc7\\xd5\\xbc?\\x1b\\xd8\\xbcQL\\xfc<H\\xfed\\xbd\\x8133=\\xca\\xe31\\xbc\\xaa6\\xaf\\xbc\\xce\\xa6\\xdb\\xbc\\x0c\\x8b\\xed<\\x0b\\xa5x<\\xad\\x8c\\xea\\xbc\\xc4R\\\"=\\x1c)X\\xbc\\x95}M=52\\xdb\\xbb2\\xe7\\xad;\\xeb\\x94\\x07\\xbd/\\x9a\\x12=\\x83\\xbc\\x93=\\tJ3\\xbdun\\x15=(JS<\\xebo\\x9d;U\\xce\\x88\\xbd\\xe9\\xb2\\x0b\\xbd*Y\\xa4\\xbc\\xfc\\xa7\\xc8<\\x85\\xe5\\xf5\\xbcL\\xcdG\\xbd40X\\xbcs~\\x9b\\xbc\\x97T\\xc8;\\xe4\\xc1\\x0e=\\xc64O<\\xa3\\x91(\\xbd\\x04\\xafd;L\\xdd8\\xbc\\xce\\xe2/\\xbc\\xa3\\x7fG\\xbc[\\xd0\\x08=\\x87P5\\xbcR\\x84;\\xbd\\x9a\\xaf\\x19<\\xe2%*\\xbc\\x18N0\\xbdu\\x83\\x03=\\x0ef\\xd1\\xbd\\xe3\\xcb.<\\xe7\\xc7\\x0b\\xbd\\xc1k\\x0f\\xbcJ\\x04\\x1a<|\\xa0p\\xbc#h\\xa1\\xbcch\\r=5\\xe3\\\\\\xbc!\\xe3t=D\\\"\\x1e\\xbd\\x08\\xdaE\\xbd\\x88\\x9d\\xa3\\xbd\\xc2\\xdcf<\\xc2\\x94\\t=\\x11L\\x11\\xbcSw\\x0c=\\xa8V\\x9e<a\\x08\\xbf<\\xe5\\xe2\\xa9\\xbbj\\xe7\\\"<6z\\x0e\\xbd\\xe5\\xc6\\x01\\xbb\\x9f\\xecj=\\x9f%\\xab:\\x9e\\xcc\\xf9<\\xe5?\\x8b=\\xb8X\\xa7<Q\\x1d\\xb2\\xbdN\\x1f[\\xbd\\xd9\\xda\\x13<i\\xfcF<\\xb7\\x0f\\xac\\xbc\\x9c\\xde\\xc0\\xbb )\\xe2<\\x04\\x06\\xb4\\xbd\\x94\\x99$=\\x08\\x80\\x90\\xbc\\x07Sd\\xbb\\xa4\\tR\\xbdc\\x1e@=\\xdc8\\xb6\\xbc\\xddO\\x17\\xbc\\x89\\x05\\x00\\xbdA8;\\xbc\\x13\\xc4(\\xbd\\x07&{=3\\x17:;\\xf0\\x9a\\x0e\\xbd-\\xa0\\xd3<\\xcf\\x8d\\x85:\\xd8\\x95\\x00\\xbb\\xe1\\x8d\\x89;\\t\\x9f\\x95\\xbc\\x91%\\xf3<x\\x1a\\xd6\\xbc\\xd4 )\\xbcjs\\xc2\\xbc4\\x98\\n<\\x0b\\x88N=\\xd3\\x85\\xb3\\xbd.\\x9e\\r\\xbdm\\xe3\\x0f=\\xe1L=\\xbdt\\x13\\xfb<\\x05\\xaeY<\\xee\\x01\\x0e\\xbd\\x00U\\xef<rE\\x9d<\\x97\\xaa\\x9e\\xbd\\x17\\xb6\\\"\\xbc\\xd6\\xd0U<o\\xb9&\\xbd\\xcb\\x16\\x02=\\x9f\\x97\\x17<w\\xca\\xa1<v\\x0f-\\xbd\\xfc\\xfd\\xe5<\\xd2\\x11\\xc9<z\\xc7\\xaa\\xbcn\\xdb\\x8d\\xbbU\\x8d\\xba<G\\xb5\\xa8<\\x92~H=\\xe5K\\xf2\\xbc\\xa6\\xa89<\\xf7\\xcc\\xc7\\xbb6?\\xce;\\xe0\\x14\\x8e<\\xd8\\x1d\\x1c=\\x0b\\xb6\\x80\\xbc\\x9a$*\\xbb\\x85fJ\\xbd \\x95V\\xbcC\\x1c\\xa0\\xbc\\x9c\\xc4\\xdf<\\x831\\x16\\xbdm\\xbb*\\xbd\\\"\\xb8\\x84=\\xf2\\xe1\\xf3<&\\xe0\\x17\\xbc@\\x9d\\xc4\\xbc\\xe6\\xec\\xb5;W\\xf3\\xc6;\\xc7\\x95|<4\\x00\\x16<\\x02\\xc4\\x9e\\xbd\\xedx\\x9b:\\x1e<\\x0f\\xbd\\xf1;\\xf0;\\xa2\\xc1\\xed;S\\n\\xa6<\\xe8\\xfb\\xab\\xbd\\xd4\\xe2%\\xbc\\xa4f\\xd3\\xbb\\xeb\\x8a\\x98\\xbc\\xe6\\x10G\\xbb\\xd4d\\xab\\xbc\\x0e$\\x03=dC\\x84<\\xe5\\x1d\\x18=\\\"\\xbe\\x18\\xbdP\\xff\\xcd\\xbaiMW=\\x81K\\xdf\\xbcA\\x85\\xbf<\\x87\\xfb\\xe1<\\xac\\xd8\\xae=d\\x7fM=\\xbb\\xc7\\x96=\\xc0n\\xb6<L\\xdd\\x11\\xbbWJ\\xb4\\xbcD\\xd9\\x82=\\xb9\\xee\\x06\\xbd\\x1dm\\xad\\xbc\\x8aK\\xa0<]+i\\xbc7\\x91\\x91\\xbd.\\x17\\x01\\xbd\\xaeN\\x05\\xbe\\x89g\\xaf\\xbc\\xbfb\\xd8<rq\\xb1<\\xdd\\xed\\x93=\\xb4$\\x19=\\xf0i%=\\xd9\\x10\\xf4\\xbc\\xff\\xd6\\xb8;\\\"\\xe5\\x01\\xbd\\xce\\x9a,\\xbd\\x10\\xe8\\x87=\\xba\\xe5\\xc3\\xbb\\xef\\xf1};D\\xdep\\xbc\\xc9\\xa7\\xab<\\xeb\\xd6X=\\xfb\\xa5K\\xbd\\xf7Z\\xff\\xbc\\x12I\\xa7\\xbc\\xb5^\\x13=\\x90A\\xf1\\xbc\\xe1\\x01\\x85\\xbd\\\"\\x11\\xd5\\xbc\\x10\\xde[;\\x16\\xbf\\x03\\xbd@\\xe3N<S\\x03\\xa2<\\xd2\\x8b\\xef\\xba\\xea7\\x8a=\\x90xz<o\\x8es\\xbd\\xed\\xa4\\x90=\\\"a\\xbe=\\xf8\\xec\\x94\\xbc\\x08=k\\xbc\\xe8\\xf6\\xdc\\xba\\xcc\\xc5\\x98\\xbd\\x05\\x03)=\\x9d%y<$\\xe85\\xbd\\xdd \\xa2<\\x02\\xf6\\x9b\\xbd\\x80\\xe0\\t=\\xf9\\x9f\\x84<I`6:\\xe1\\xb6\\xa0\\xbc\\xe7\\xa75=B;E:\\xd0/%=\\xec\\x82G\\xbc\\xb5\\x089=Bv\\x16=cS+\\xbb\\x04\\xd0\\x97\\xbc\\xfd\\xf8\\xb8\\xbb\\xe3\\xaa\\\"=\\x89b\\xa9;\\xe1\\xdf\\x17\\xbd\\xa5\\xde\\x84;/\\x08/\\xbd\\xbe\\x8a\\xb8\\xbb\\xbee \\xbc\\x9d\\xc7\\x93;\\x07E\\xea<\\xbb\\x8d1;\\x84\\xae\\n\\xbd\\xea\\x8e\\x9b\\xbaV\\x0b\\xf1<\\xce6\\x9c\\xbc\\x0e\\xdd\\x1b\\xba\\xae\\x1f\\xe2;x[)\\xbd\\xcc\\xfd\\x9b\\xbc\\xb9\\x0fT\\xbd\\xff\\xd1\\x84\\xbcX\\xfc\\x85=%\\xb3\\xf0<\\xaf\\xf8]=\\x0e\\xd1\\x03<\\x87\\xb5R<\\xe92O;\\x14`\\xc8\\xb7\\x01\\t\\xcc\\xbc\\x83:+\\xbc\\x9ezF<p\\x11\\xbe<q\\x0c\\xff<%T\\xa3;\\xb3\\x87K\\xbd6pc=\\xd0\\x8d\\x83\\xbdI4\\x83\\xbd@\\xa37\\xbd\\x0eIp\\xbd\\xc0\\x82\\x1e=\\xd3\\xd6\\x05<\\xe1L\\x1c;\\xc8\\x87l\\xbc\\xe2:\\xe5\\xbc\\\"5K=Ro\\x05=\\xc0\\xa3\\x91:\\xa8\\xa1m=\\xd1\\xf5\\x06\\xbd\\x07\\xe9S\\xbc$\\xb1\\x00=\\x89\\xa03=6\\xa2\\x07\\xbd\\xc6F\\x7f<y\\xa4!=\\xe0s4;*\\xe7><y\\x02\\x8d<\\xb6\\x8c\\x9c\\xbb\\xf2\\xefY<\\xaf\\xe46;\\xff|\\x08\\xbd\\xf28\\xc4\\xbb\\x12\\xd3\\\\\\xbb\\xfdo\\xc9\\xbc\\xac\\xb2\\xd2<y\\x0cD<\\x02\\xdeh\\xbc\\x9bR\\x0f=\\xee\\xd2H\\xbb\\xc4\\xca\\xa2\\xbd\\x18\\xfd-\\xbdv#b\\xbc\\xe4\\xce~\\xbc\\xd1\\xa2\\xf5<\\xc4\\x90\\x1e\\xbd(\\x88u;C\\xd9=<\\\"\\x15\\xce\\xbb\\xf3Q\\\\\\t\\xe1\\xa8\\xa6<z\\xfc\\xc2\\xbc(\\xbf\\xe5<\\xe0:U;\\x7fU\\xd7\\xbb\\xf5p7=W\\x8c:\\xbc\\x9c\\xedk\\xbc0JG=\\xfb99\\xbc\\xa9\\xcd*\\xbct\\xca\\xd8<uc#\\xbdd[f=\\x13>\\xd3\\xbb\\x10\\xe1\\x1e=\\xed\\x83\\xa1\\xbdiH\\xf4\\xbc\\x18*\\x08;wRy=a\\n\\xf0<\\x12\\x12L:/\\xc9\\xad\\xbcB\\xed\\xe2<\\xc9=\\x81;LI\\xb6\\xbbB\\xdc\\x1e\\xbbEym<6\\xc3\\\"\\xbd6\\x1a\\xf7\\xbc\\x94\\x03Q<\\xd1H$=G1U\\xbcDs0\\xbd#\\xe0\\x96\\xbc\\xc6\\xdb\\x12<\\xaa\\xc5\\xdf<\\xc3y\\x03=\\x9c\\xc52=\\xe6\\x08\\r=/\\x9e\\x0b\\xbdq\\xa9\\xf2\\xbcJ\\xc9\\x16=n\\xf0\\x89\\xbb\\x00\\xc14=i\\xd6\\x98:\\xdas\\xfd=\\xc5j\\x0e\\xbclU\\xc2\\xbc\\xd5\\x7f\\x1d\\xbc\\x0b\\xd3?\\xbd\\xb4u\\xe1\\xbd\\xdbGi=\\xd1QU<\\xc9\\xd27;)/\\xc1<\\xc8\\xb8T\\xbd\\xab\\xba*=R\\x1c <Y\\x06\\x16=!e\\xe1<G\\x0f\\xe4\\xbc\\\"?\\x16<\\xde^\\x12=\\xf7\\xb4z<\\xc3p}\\xbd\\x0f\\x15_\\xbda?6\\xbcv\\xb5j<\\xaa\\xa9\\x94\\xbcV\\x18\\xe5<I?\\x0b\\xbd\\x0f\\xe0`;\\xf6Q*=0\\xaf\\x15=XB\\x19\\xbd\\xdcRD\\xbb9\\x92;=~\\x82\\xf1\\xbb\\xcf\\xda\\x01=\\xf64\\xa5\\xbb\\xa5\\x0c\\xff;/\\x01-\\xbd\\xacAF=y!C\\xbd\\xe7b\\xd1<\\xc7=\\\"=\\xeb\\xf7F=f\\xa8@\\xbc\\xb5\\xa0!:\\x0b\\x00\\xa7<\\xb0\\x98y<`+\\x15=\\x07\\xf4f:\\xc3\\xbaa=W;0=\\xa4\\x08\\xd1\\xbc\\xaeU\\xc6\\xbc\\xe6\\xcc\\x93;\\xa4\\xa8\\x9a<{\\x94\\x95\\xbc:\\xaa\\\"=\\xcb\\xf6\\xa8\\xbc\\x96\\xee\\x12\\xbd\\x04\\xc2?={@\\xb5\\xbb\\xb9\\xc8s<\\xea\\x87q\\xba\\x19\\xe9l\\xbd\\xa9P\\xaa;\\x95\\xa4e=\\xdf\\xfd\\x1e\\xbdh\\x0c\\xee\\xbc\\x9d\\xfbk=\\x0c\\xcc\\xcb=z\\x92\\xc7<\\x9dT\\x9d<\\x90\\r\\xbe\\xbc\\x91\\x82E\\xbcP\\xb3}\\xbd\\x1f\\xe7\\xfc<A\\t\\t=\\xd6_\\x06\\xbb\\xa2|F;^:\\xa9=\\xa2\\x88#<;\\xa9\\xb8\\xbc\\x86!\\xf5<\\x84\\xa9k=\\xcb\\x15<\\xbdw}b\\xbd\\xf2\\x12\\x8a\\xbd\\xe0\\xae2=\\x83\\xceH\\xbb$2\\x97=R\\xd0\\xc1;\\x01\\xc8D=\\xad*~\\xbd75\\x04\\xbc\\xc6\\xed\\x1c\\xbc\\xa0\\xa3\\\"\\xbc)\\xb0\\x8e\\xbd\\x13\\xa3#\\xbc\\xf8\\xd9\\xae<\\\\\\xe4\\x05=\\x83Q\\x18;\\xbc\\x8c\\x83==!\\x92\\xbc\\xc0=W=[\\xf0\\x12=;E\\xd8\\xbc\\xf9\\xc0_\\xbcR\\xeeK\\xbd\\x80\\xec\\xff<\\xab\\x8b\\xc9<\\xc2\\x0fF<\\x0b\\x97\\xd8\\xbct\\x05\\xaa=\\\"\\xf8\\\"<\\xccd\\xfb;\\xc8\\xde\\x81=\\xa7\\xd3<\\xbcW\\xef9\\xbc[\\x83\\xd5\\xba\\xbf\\x84\\xe4\\xbcY\\xf3}\\xbc\\rg:\\xbd.\\xc0\\xd1;\\xc1\\x8a4\\xbc\\xdc\\x03\\x1d\\xbd\\x95\\xf5\\x02==\\xd6\\x18\\xbd<\\x02\\x98;NB\\xdd\\xba\\x8f\\x06\\xb0\\xbc\\x92w)=\\xfe\\xb4z\\xbc\\xe0^\\xa0<\\x05\\x12y=7\\x9e\\x82\\xbb\\xc98\\x06\\xbdg\\xe3\\xdc\\xbd\\x0c\\xe9C=\\xf0m\\xdf\\xbcmQ\\x9b\\xbc\\x88\\xd2\\x03\\xbd\\x18\\x18a<\\xa7\\x04\\x0f\\xbc\\xf6\\x10\\xf4<\\x1fB\\x01=\\x18\\xc6\\x1b\\xbdD\\xaf\\xbb<\\x84^\\x92\\xbc\\x1a\\xbe\\x95\\xbb\\xcb@\\x18\\xbd\\xd6\\xfa\\x0e\\xbd\\x8a\\xa7a\\xbb\\n\\x98Z<{\\xa6\\x16\\xbc\\xc4\\xaf\\xf3\\xbb\\xf4E\\x1d\\xbds\\xdfC\\xbcM=\\xf1\\xbc\\xd0]\\x8f\\xbd\\x9b`\\xd5\\xbb\\xf8\\\"\\x94;0\\xd0\\xed;U\\xc69\\xbc\\xca\\xda\\xb5;\\n-z\\xbdgh?<\\xfbo:\\xbcv\\xd1\\xeb<\\xc0\\xa9\\x98\\xbc5F\\x86\\xbc\\x9c\\xd9.\\xbc\\xda]\\xe4<P\\x8c\\xa6\\xbc\\x8c@\\x97\\xbd]\\xcf\\x1b\\xbdG\\xaa>\\xbc\\xddn\\xc2\\xbc:\\xac\\x03=\\xffpP=F\\x1e}=^\\xcaa<5\\x92\\x18=\\xd7\\xc2q\\xbd[\\xd4B\\xbd>\\x89\\x9f<a#f<\\xf7\\r\\xf9<\\xa3\\xd39\\xbd.\\x08\\xc7\\xbc\\x1d\\x91\\xaf< y0\\xbd\\x08\\xb2|;}\\x8c\\xc7<\\xee\\x1b\\x8f<\\xb75\\xb0\\xbc\\xf4\\x05\\x86\\xbcH\\xfb\\x9a<\\nT^=K\\xean<\\xdds\\x02\\xbdst\\\\=\\xbc\\xd2\\x01\\xbd\\x18\\xfeu\\xbb\\xc8\\x9a\\x0b\\xbdt]O<\\r0\\xb5<\\xae8\\x90;\\xc1\\xf9\\xa3\\xbb\\xe4\\xd9\\xa1=\\xd2\\x15\\x03\\xbd<-\\x04\\xbd\\x8c{n\\xbcV;\\xeb\\xbc\\xf0\\x18\\x9a\\xbc\\x83\\x92]\\xbd\\t|\\x04=\\xb2aI=q\\x872=\\xef\\xb5\\x9d<\\xa91\\xbc=\\x93\\x9eg<Csv=\\xcd\\x97\\xb7=\\x81\\x90\\xb0\\xbc\\x8d\\xee\\xa4<\\xb7\\xfcQ<\\x83U\\xe8<\\xcf\\x86h\\xbc\"\nHSET bikes:10034  model 'Umbriel' brand 'BikeShind' price 1141 type 'Mountain bikes' material 'carbon' weight 11.1 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. It has a lightweight frame and all-carbon fork, with cables routed internally. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\xf2\\xe8\\x94\\xbb\\xa7\\xd2\\xa7<\\x06R\\xca\\xbbI\\x90\\x91<\\xc3\\xf4_\\xbc-\\x1ex=\\xe7\\x18\\xae\\xb9X\\xc9[\\xbd\\xb2\\xd9\\x83=\\x04\\x9d\\xae<\\x8a\\xa9\\x06\\xbc\\xe7\\xa6\\xb0;\\x8c\\x0b?=\\xfdS\\xae<\\xd3c\\xfd<\\xe2Fd\\xbd)m\\x13=\\xf7\\x82\\xa2\\xbc\\x9d\\\"\\x15=}%M\\xbd\\xb7jl;\\xaf\\xff\\xe2\\xbc\\xd1\\xdc\\xbb\\xbcc\\x95v\\xbd\\x95T\\xfe\\xbc\\x10\\xc9\\t<Q\\n\\xd4<:\\xc1\\x17<\\x82Q\\x96\\xbd~)F=\\xf5o\\x1a\\xbdg\\xb2\\x02\\xbc\\xc4\\x15\\xf4<\\x13_H=O\\xff\\x18\\xbdM\\xce`\\xbb\\xad\\xa1\\x85\\xbb\\xf8A&\\xbd\\xc6]\\xdb\\xbc\\xa3\\xfd^<\\x8a\\xdb\\r=\\xda;\\x08\\xbd\\x9fT\\\"=\\\"\\xdf\\xa3:\\x08\\x8c>;\\x8cx(\\xbd\\xdb\\xbf\\x16=K\\x91\\x80\\xbd\\xa3\\x9f \\xbdf\\x12`;?\\xc9\\x00=\\x0f\\n\\x12\\xbd\\xda\\xec\\xd7:\\xd0\\x02?\\xbc_/\\xb8\\xbc\\x12\\xf7\\x02\\xbdN\\xe3\\xbb\\xbc\\x10\\xffh\\xbdr\\xccV\\xbd\\xae\\x8f\\x9f=\\xe4\\xd9\\xa8<[6R==\\xc2\\x9e:\\x07-G=\\xdc\\xf4[=\\xa2NR<\\xf2\\x8a=\\xbc,\\xbe\\xde<\\x92\\xc3K<EO\\x81\\xbc9\\x81S\\xbc+\\xe7\\xcc;\\xde8\\x14\\xbd\\xbat\\x81\\xbc\\x80\\x0f\\x97\\xbc\\xc3f\\xbf\\xbbx\\xe3\\xf1\\xba\\xaf+q\\xbd,\\xbf\\x8e\\xbd\\xf3\\x1a\\xa7<\\x08)\\xef<\\x9f\\xe7\\xeb\\xbc\\xb8\\xed\\xe8\\xbc_N@\\xbdo?r\\xbc7\\xa3\\x12\\xbdm\\xe6\\x82;\\x9d\\x8f\\xb6<O\\xe1\\x18=\\xde\\x12\\xe7\\xbbN#\\x98\\xbc\\xfb\\xb5\\xb5\\xbc#\\x81\\xe5\\xbb\\xaf\\xeb\\x1e=\\xa7\\xe4E<&\\x85\\xeb<\\xab\\xf2\\xe9\\xbc\\x9ab\\n<\\xc2\\x1f\\x81;_Q1\\xbd\\x1f\\xe3\\xe1\\xbc\\xe8\\xb0-\\xbb|\\xde\\x8e<\\xbdPX\\xbd\\x15i\\xa0;fG\\xa8=\\xeb\\x9f|<\\xd5i\\xdc<\\x0b\\x18\\xe2\\xbc\\xb0\\xbb\\xe3\\xba\\\"\\x7fH\\xbd\\xc2\\x10M=x\\xcc!=n\\xcc\\x0b\\xbd\\xa4\\xedZ\\xbd\\xd1\\xea\\xbb;\\xba;\\x98;\\x9b\\x84\\x8d\\xbc/\\x94\\xf5<-2\\x06=G9<;\\x0e\\xa2\\xee<]\\xc7*\\xbd\\xc7\\xcdB\\xbc\\x13\\x1b\\xb7;z\\x07O\\xbbqb\\xef<\\xc8E\\\"\\xbd\\xf65#\\xbd\\xcfb\\x04\\xbd\\xb0\\x96\\x84:8\\x81\\\"\\xbc\\x8d%3=.LK\\xbc\\xaeH\\x9f\\xbc\\xf0?!=\\x9d\\x8dS\\xbd\\xdb\\xe6\\t\\xbc;\\x85\\xce\\xbc\\xf0\\xa2\\xb8\\xbc{H\\xf3\\xbc\\xbb\\x1b!\\xbd\\x9e\\x80\\x0e\\xbdr\\xef(=]\\x0bY=<\\xc1\\xdf<\\x08\\x04\\xd3\\xbc\\xa1@\\x11\\xbc\\xdaM\\xc9<-\\xe8z\\xbdG\\xec\\x85\\xbc\\xca\\tF=\\x91Z\\xe8\\xbc\\xf0\\xf0,<\\xbb\\xff\\r\\xbdmW|\\xbb\\xc8\\xb3\\x05\\xbd~z\\x96\\xbc\\xa9d\\xb4<\\x0cm\\xca\\xbcs\\x8d\\x8e=\\xc0\\x86\\x8f\\xbc\\xb9\\xd8\\xb5\\xbcN\\x9f\\x94\\xbd\\xa6\\xb3\\xef\\xbbV\\xc0\\x02<_)\\xf3\\xbc\\xa1\\xe0G=\\t\\x16\\x95<\\x99zb\\xbc\\x18\\x99\\x89\\xbc\\xeb,U=Y\\x8d\\n<3\\xf2 \\xbd\\x0b\\x12s;<\\x8d\\xe2\\xba\\xedr\\x1a\\xbdSn\\x8b=7\\xc3\\xee\\xbb\\x9eo\\x03<\\xd97\\xb5\\xbcHE\\x9a<\\xaa\\x7f\\x19=\\x14b\\xdd<>\\xef\\xff<\\xc8{\\x0f\\xbd\\x8d\\xec\\xc3<\\xba\\x11\\xb0<\\t*\\xde<\\x12\\x0b3\\xbdEB\\xb9<\\xdb;\\xef9\\xc2\\xfd\\x98\\xbc\\xfe\\xd1k=x\\t\\xf3\\xbb\\x82n\\x89\\xbc\\xb7\\xd3\\xeb\\xbch2\\xfb\\xbc\\r!\\xad\\xbc\\xfd\\xf0\\x1d=\\xccW\\xfe\\xbcG\\x9d]\\xbd\\x9b\\xb0\\xca\\xbd\\xbc\\xb0\\x82\\xbd\\xe4\\xc2\\x02\\xbdh7\\x86;#\\x9dl\\xbbq\\x16\\xa4\\xbcH8\\xa9<\\xb0\\xb8-\\xbd\\xf1pw\\xba}\\xb9\\x9c<S\\xa1\\x8c\\xbc|m&\\xbd\\xdb\\xbb+=)\\xb4#:\\xb36\\x01\\xbd\\x81\\xcc|\\xbd/5\\x89=\\xcd\\x8f\\xd6\\xbd\\x00SV<\\x0b\\x85\\x86\\xbc\\xceE\\x19\\xbd]\\xdd\\x8c<\\x8bZ-\\xbc9\\x11\\xc4\\xbc\\xec\\xc27=:\\xd8\\xe5<t\\xb8\\x86=g\\xc43\\xbd\\xb9\\x99\\xaa\\xbcY\\xa6\\x93<\\x94\\\"\\x0b\\xbd\\xdc\\xe0\\x16=\\x12j\\x83\\xbcm{\\x00\\xbd\\xf7g\\xbd<f\\xf28;\\xda\\x97\\x8e\\xba:\\x97\\xb2\\xbb\\x15\\xdd\\xe2<\\xa4\\x92\\xb6\\xbb\\xca\\x8e\\x12=\\x9eH\\xb6<e8%;\\x9e\\xf0\\n=3F\\xf8<Z\\x91\\xfb;O{U\\xbd\\x8e\\xd1\\x08=I(\\x86\\xbc\\xfds\\xa2\\xbdn\\x00\\xe5\\xbbT\\xcb\\x84=rF)\\xbd\\x80!T=\\x1e\\xe2\\xf0\\xbcf\\x13\\x88;\\x93\\xc60\\xbdx&\\x94=?:3<\\x8aHr;\\xc2~k<\\xc3,T\\xbbd\\x84n\\xbdv\\xb5i=\\xe2\\x8fV<\\xa4&Z\\xbc\\x1e\\xf5\\xee<G?\\x14<q\\x01|<\\xd2\\x7f\\x97<\\xfd\\xde\\x80\\xbd\\xb7\\xa4\\x83\\xbc\\xec\\xf1\\xd7\\xbc\\\"\\xa1\\xe7;\\x1d\\x97b<\\xac\\x0cL\\xbct\\xd6\\xf1<\\xf7\\xb3\\xb5\\xbdH%W<\\x1eW!=\\x8b\\xa2\\x87\\xbb\\x9a\\xcda<\\x1e\\x84\\\"<\\xe7p(;\\xfc\\xe9\\n\\xbde\\xe2\\x8e=\\t_\\x8b\\xbd\\x1e\\xf7\\xc1<:@g<M\\xf5\\x0e;g\\xc59=,3\\x8b<\\xb4!x\\xbc\\x9a\\x8c\\x8a\\xbd$]\\x86<\\xd0v\\x88<\\xa4\\xd4\\xa5<\\x16\\xba6\\xbd\\x0b\\xf2\\xa1<\\x01M\\xb1<\\xb4\\xfc\\x90=\\x8eI\\xf8;\\xa94\\x01=\\x8d\\\"\\r=\\xca:\\xb3\\xbcF\\xab\\x83<2D\\x9e=\\x85R\\xc1<\\x1e\\x8b\\x15\\xbd\\xb6A\\xf6\\xbcsT\\x0e\\xbc\\x90D\\x0e<\\x91\\xb63=\\xb6G\\x16\\xbd\\x86\\x06F\\xbcKc\\xab=]H.=\\xce.\\x19\\xbc:\\x05R\\xbds\\x12\\\"\\xbd\\x16*?=\\xdb.\\x8b\\xbb\\x96V\\x8e\\xbce\\x86\\x85\\xbd{\\xedS=-9\\xc2\\xbd\\xd7\\xff\\xc6<\\xae#\\xa1\\xbcn\\xa6\\xd9\\xbc\\xadQN\\xbd/\\xea\\x9a\\xbc\\x00\\xf9\\xc7:f\\xac\\x9d<Q9\\xc7\\xbb\\x0cH|\\xbd\\xa2\\xd43=-\\x06\\xaa<\\xdd\\x9c.\\xbc\\x95\\xa6(\\xbd!W0\\xbd\\xf0\\xe0\\xdb<\\x91[\\xa1:Y\\xcai\\xbc\\x82\\x13\\xd4<.OR=\\xff\\xdf^\\xbc{fs=\\x8c\\x80C<}\\xb2\\x01=\\x13o\\xa9\\xbc\\xba\\xc2\\r:\\x13\\x04\\x91<\\x8e\\x9e\\xe4\\xbc^\\xa0R=HU%\\xbc\\xd8\\xbd\\xd4\\xbc(\\xf1\\xec\\xbc\\xd3\\x1b\\xc8\\xbd1\\xf7\\xe8:\\xe5\\xd3\\xaf<;\\xdb\\xa1;%\\x13u=Sc\\xcf<\\xa2\\x84!\\xbc|\\xd0g\\xbc\\xdb\\x9a\\x07=V-\\xb6\\xbc\\x08\\xcb\\xbf\\xbbKF\\x8c=\\xba\\xfb\\x7f<\\xb5m\\x84;\\xaf\\xe3\\x04\\xbdMMg\\xba._\\x06=cs\\xcb\\xbcu\\xfc\\xa0\\xbd\\xe6\\xfd\\x8d\\xbc\\xc8ia\\xbcx\\xf9\\x15\\xbd\\x035\\x11\\xbd?\\x99\\x8c\\xbd\\x1d7\\x1c<D$s\\xbd\\x9d\\xef\\xc7\\xbc\\x16\\xae\\xbd<\\xbd8\\x11=\\xb0MJ=\\r\\x92#\\xbd)\\x7fx\\xbdYEB=\\xfa\\xd7\\xcf=pt\\xb0\\xbc\\xc3\\x04\\x87<\\xf4T\\xf5<\\x1ebe\\xbd\\xaauC\\xbd&Cj=\\xb5\\xcfh\\xbd\\x82\\xa9\\xda<$$\\xdc\\xbc\\xb9\\xe1\\x0c=`\\x06\\xb9=yc\\xcf;\\xaaa\\x87\\xbd\\xceAd=EX\\x90<\\xd6\\t\\xa2\\xbc\\x88~m;\\xfe\\xf2_=\\xb6\\x13\\x15=\\x1e\\xb0m\\xbc\\xa3%\\x03<\\xe57\\xca<d\\x99\\x05<\\x08\\x85\\x10\\xbd\\xaf0+\\xbdP3\\x8f<\\x16\\xbaX;U\\xab\\x82<pmu\\xbc\\x86.\\x1a<\\x80(f=Md\\x08<PN\\xbc:w\\x84\\xae\\xbd\\x03|\\x8f=\\xbe/\\x01<\\x96\\xbb\\xed\\xbc\\xdf*\\x9f\\xba\\xb3\\xba\\xa2\\xbc\\x1dx\\xb0\\xbcE\\x8eu\\xbd:\\x87\\xb1\\xbc\\x7f\\xe9\\x99=*k\\x96<QA\\x08=\\x95\\xa6\\xaf\\xbc$n\\x92;\\xa5\\x97b\\xbd\\x96\\x15v;\\xf8\\xf75<U\\x93\\x8b\\xbc\\x9e\\x99\\xe9<\\x1d\\xf0\\xb5\\xbb\\xf6\\\\\\xeb;\\xf0\\\\C;\\x0fv?\\xbd\\xcaKx= \\xf1\\x16\\xbdN/\\x02\\xbd\\xc5\\xcc{;\\x1d\\xaf\\xd0\\xbd\\xa5i\\xac<t6\\xfe;v\\xb1\\xba\\xbc\\x91\\xa9\\xc1<f\\x9e\\xcf\\xbc\\xe8\\xc9p=\\xbc\\xea\\x9e=M\\xb4\\xc7<\\xd7\\x90\\x88=\\xeed[\\xbd`\\x80\\xb7:\\xcey[=\\x86\\xee\\x88<\\x98\\xdb>\\xbd\\\"\\xeaE=i\\xa5\\xab<ob\\xc0\\xbc\\xba\\xbb:\\xbc\\x8a\\xde\\x03<Hw)<\\xdc\\x7f\\x84<\\x84%\\xb1<g\\xf7R\\xbdM{\\xea\\xbc|Y\\xd9\\xba\\xf9 =<\\xc4S\\x0b\\xbcb\\xb0r\\xbc\\xec\\xe6\\x90\\xbc[\\x03p<\\xaf_\\x1a\\xbcG<\\xc3\\xbd\\xb5\\xcaq\\xbcom\\x97\\xbc\\x970\\xa6\\xbc\\xdb\\xf7\\xc4<\\x9b1>\\xbd\\x03#\\x15=\\xdc\\x9b\\x03=\\x88\\xf4]<zAU\\t\\xcc\\x8a=\\xbc\\xab\\r@\\xbdIu5=+\\xe3T<7\\xff\\xca<=\\x1eE=\\xeb]\\xe3\\xbc_\\x8e\\xf7:\\x8d\\x0f\\xd2<\\xd2\\xc1\\xdf\\xbcA\\xfc\\x0b=\\xc7\\xc4x=\\xff5\\x83\\xbcO=M=\\xad\\xf2o\\xbc\\xd3\\xd5_\\xbc\\x9e\\xb5\\x89\\xbdC\\x05~\\xbb\\xder\\x81<m\\x8fj=\\xa3\\xd7\\xd39\\xb5\\x01!<\\xde\\xc1\\xb7\\xbcM\\xc2\\x14\\xbd\\xf5\\xecc<\\xda\\xab\\xf0;(q:<\\x9fE:\\xbbC\\x02\\x04\\xbd(/6<\\x99$\\x0e<\\\\\\xf6\\x1c\\xbc\\x94\\xbf>\\xbd\\\"\\x0f\\xb8\\xbd\\\\&\\xa6\\xbc\\xde\\xf0\\x07<\\x81\\xa2\\xe5<*)\\x1a=\\xcc\\x80\\xfa<\\xb2\\x06\\x05;\\xc0\\x89\\x0c=\\x7f\\xe78<\\xfbO\\x98<\\x04\\x80a=\\xaf8y=k\\x1d\\xb4;\\xadn\\xe7=%\\xcd\\x109\\x07`2\\xbd\\x96\\xf1\\xc0\\xbc\\xdd\\xd2\\x1b\\xbd\\xd5^\\x81\\xbd\\xe6I\\x03=\\x03\\xa5\\\\=\\xc8\\x82`<\\x9c\\xf0-=\\\\\\xe7\\xf5;\\xe8\\xb5\\x1e=\\xa9\\xdb\\xa3;\\xb2\\xf7|<\\xbc\\xa9\\xab;\\xceY\\x12\\xbcz@V\\xbc+\\x81*=\\\\f\\xcb\\xbcfa(\\xbd\\xf4\\xad\\x18<\\xde\\xe4\\xed;\\x14\\xc2\\xf5<\\x92\\xeb\\xa0\\xbc3\\xafG\\xba\\xa0@\\xb6\\xb8\\xf6\\xf3\\xa4;\\xda\\xf7\\xfe<X\\xfa\\x95<\\x8d \\xa4\\xbc\\x13\\x1b\\xdb\\xbc\\x83\\xf9Q=\\xd1\\xfd8\\xbd6A\\xf4<\\x13\\xd9C\\xbd\\xf2\\xa2\\x84\\xbc\\x8d\\xe5\\x17\\xbd2\\xaao=\\x12V\\xb5\\xbcSU\\x07\\xbd\\xa9\\xa8u=\\xeer\\xce<\\t\\x93\\xba\\xbb,\\xd9\\x1e=h\\xd4\\\"\\xbb\\xd7\\xc9\\xc9<!\\xc0\\x8c=zKc:\\x12\\xc5\\x90<\\xc3\\x96Y=\\x9c\\xf4\\xf4\\xbc\\xff\\xbd\\x0b=\\xd8\\xc2\\xfc\\xbb\\x9c8\\xcb<@).\\xbd\\xf9\\x15\\x1f:\\xb5\\x9a\\x0e\\xbc\\x97\\xf7Q\\xbc\\\"\\x0c\\xc6<zB\\x94\\xbct\\x1cL\\xba\\x17\\xdb\\xd5;\\x16:\\x15\\xbd\\xf5\\xc7\\x0b\\xbd\\x80kB=\\xa3\\xadS\\xbd\\xc69\\xcf\\xbc\\xe4\\xe0B=z\\x8a\\x91=\\xe8\\xb9\\xf6:\\xfd\\xed<\\xbb\\xc5\\x04\\xf1\\xban\\xcb\\xe1\\xbcoU?\\xbd\\xa0\\x1d\\xcd9\\xd58\\x05=t~\\x86;+\\x9b\\x9b\\xba\\x8e\\x1e\\x19=:\\x01\\x1f=}\\xd3B\\xbd\\x99\\xfd\\xf8<\\xf4\\x92\\x96=\\xea-l\\xbd}\\xdcr\\xbc\\xfc\\x98T\\xbd\\xa6\\xa2e=O\\x8b\\x93<\\x05\\xe4\\x9c=\\xce-\\xc8<\\x9fI\\x9f=\\x93\\xecY\\xbd\\x0e\\x88+<Ks\\xc5\\xbc8\\x11!=?\\xa5\\x91\\xbc\\xfb\\xe6\\x00\\xbdt\\xd2W\\xbbm8\\n<Pu\\x10\\xbd\\xf2\\xba\\xc6<\\xb5\\xcf\\xe7\\xbb\\t\\r\\x1c=\\x07&7=\\xe2\\xd0\\x1d\\xbdkk`;\\x0b\\x03o\\xbdy^\\x00=\\r%\\x16=J\\x9d\\x08=t\\x91\\x97\\xbd\\xfci;\\xbd\\xcc\\x0b\\xb7<\\x16NW\\xbb\\xf9\\xf7\\xbc\\xbc\\xb5zf=\\xc2)\\xdd<\\\"\\x8c\\x1a\\xbd]\\xe5\\xf4\\xbc\\xe3*m\\xbc.\\xd2[\\xbd\\xb7\\xe5W<\\x1f:\\x9f=+\\x03\\x1b\\xbd\\x9d\\xb2\\xff<~\\xc1\\x16\\xbcq\\xc4\\xd0;\\xeaP\\xe9\\xbc\\xbe\\x9dT\\xbcu7S=e\\xbe\\x14=V3\\xa2\\xbb\\xfdf\\xd9<70\\xdd<\\x9c\\xa25\\xbd\\x06\\xd9\\xb0\\xbc\\xd3\\x1d\\x14=\\xa9\\x81\\x9f\\xba\\x9f\\x87\\xd6:\\\",\\r\\xbd\\x06\\x128<\\xed\\x01\\x06\\xbd\\x95\\x87\\x02=MHA<\\xe1\\xe0\\xee\\xbc\\xbfn\\xf7<\\xd8|P\\xbd\\xf7l\\xbb\\xbc\\x0b_\\xbc\\xbc\\\\\\xe1^\\xbd\\xca\\xa3\\xb0\\xbb\\x18\\xfb\\xd6\\xbc\\x8f\\x01.\\xbd\\x83pj\\xbc=\\xb4\\x9f\\xbbub\\x99\\xbc\\x86\\xc4A\\xbd\\xdf\\xaa\\\"\\xbd{\\x98\\xfd\\xbc\\x1c8l\\xbd\\xc3\\xa9\\x01<\\x11us=\\xabE\\xcc\\xbc\\x84\\x15S\\xbdd|*\\xbb\\x90\\xc3@\\xbc\\tp,=a\\xe6\\xaf\\xbb:\\xce\\xf6\\xbc\\r\\x14\\x85\\xbbFn\\\\=\\xd5a\\x88\\xbb_\\xa2H\\xbd.\\xa9t\\xbd\\x05+\\xd9\\xba\\xa0\\xdcL\\xbc\\x13b\\xfc<~^\\xfd<\\x8a3\\x9d=,n\\xc8<\\xf2\\x95\\xad<\\xe6>\\x07\\xbdU\\xae\\x98\\xbd*\\xe3\\xf6<\\xf9_\\xb8;\\xe0)H=\\x7fb#:\\x13\\\"A\\xbd<\\xf3\\xbc\\xbc*\\xa3i\\xbd=\\x19\\xbc\\xbb\\x8f9i9\\xbb\\x8c\\xcc;,\\x96\\x0c\\xbc1\\x84\\x12=\\x938\\xf9<6p\\x1b=\\xb70\\x93\\xbb\\xf7\\x88\\x08<\\t\\xdaN\\xbd\\xe1\\xff\\x04\\xbd\\xf9\\xdeq<\\xc5\\xfb/\\xbd\\xa5\\xa7\\xe4;\\xf6\\\"b<\\x05\\xdc-;\\xda\\xa1\\xc1\\xbb\\x92\\x81 =A\\xd3>\\xbc\\xc5\\r\\xd6<\\xb6n\\x1b\\xbbxw\\xad\\xbdC\\xd2\\xe1\\xbc\\xd07*\\xbdL\\xe2\\xa2<w\\xde?\\xbc\\x02\\x00\\x11=\\xb5\\x1fJ=\\xf9?\\xf8=\\x05\\xd0\\xbe\\xba\\xd9\\xf3\\xa6<NO{=b{G;\\xc3\\xb1\\xa7=\\xf4\\x07\\xb4\\xbc\\xf5\\xf0(=\\xe8\\n\\xbf\\xb8\"\nHSET bikes:10035  model 'Ganymede' brand 'Bold bicycles' price 665 type 'Enduro bikes' material 'alloy' weight 7.2 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"5>\\xe5;\\xf3\\xf5\\x84:\\x8b\\x92|;\\x1e\\xf2\\xdb< \\x06\\x85\\xbb.\\xd6]\\xb9\\x8dMW\\xbbTh-\\xbd\\xbb\\xfb\\x86=Y\\x89N\\xbc\\xf6\\xf2\\x9b\\xbb\\x8b\\x03\\x14\\xbd\\x1b\\xa1W<\\x80\\x10K=\\xc9\\x1b\\xdb<B\\x10W\\xbd\\xe2 \\xab;\\x8c\\x0b\\x04\\xbd\\xe6\\xd1(=\\xacK\\x96\\xbdx\\x9b\\x15\\xbd\\xa3\\x14_:\\xd76\\x10\\xbd\\xd2J\\n\\xbd\\xc1\\xeeN=\\x06\\xdd;=\\xca\\x90\\xe4;\\x8e4\\x89<\\xa2C\\x9f\\xbd\\xb6\\xa6\\x9e\\xbb\\xb05\\xf8\\xbcZ\\xd5{<I\\x98\\xf4:\\xe1\\xc1\\x05=\\x02\\xe9J\\xbc\\x10/\\x98;E\\xc7\\x04\\xbd\\x84\\\"\\xe8\\xbc\\x95F\\x9a\\xbdY\\xafy<\\x98|\\xed<\\xe3\\xba\\xe0\\xbc\\xa9h\\x97\\xbc)\\x9f\\xb2\\xbb.\\x13l\\xbc\\x9cV\\xe9\\xbc\\xa9\\xa6Q=\\x1b\\xdc\\xed\\xbc\\xbf[\\xfc\\xbc\\x1f\\x89\\x1f=\\xde\\x19\\x10=\\x84\\xa3\\x9e\\xbc`^\\x97\\xbc-\\xf2-\\xbb\\xad\\x04\\xe3\\xba\\xcb\\x7f\\xc6\\xbch(l;\\xb3\\x82\\xa9\\xbdF\\xe4Q\\xbd\\x1cA*=\\x1b\\x05\\\"=\\xe5BY:~\\x92r\\xbc\\x96\\x17\\x90=\\xc7)>=Fc<<\\xec\\x95\\x9d\\xbc\\xf5n\\x91<\\xd4\\x9c\\xb3<\\x98\\xda\\xd3<\\x9e\\x0f\\xc0\\xbc5\\x08&\\xbdQ\\xfc\\xc9\\xbc3y\\x81\\xbcv?h\\xbcJf\\x9a\\xbb]_\\xb2\\xbc\\xbf\\xceG\\xbdb\\xbc\\x91\\xbd\\x8a\\xcfW<\\xb6\\x81\\xc1<f9y<\\x8b\\t\\xea\\xbcu\\x7f\\xa4\\xbd`\\xe3b\\xbc\\xfe\\xb8\\xe2\\xbbz*\\xd1\\xbb\\xa5\\xc9o\\xbc\\x12(\\xf0<\\x1b\\x99\\xa7\\xbb(K\\x81\\xbc\\x06\\xf6\\xf9\\xbb\\x89\\xe3\\\";\\xf9\\xd3\\xfc;U\\xeay:\\xb2L\\x08=\\x91/\\x01\\xbd\\x7f\\xb1@<\\xb5So\\xbbxk\\x1c\\xbc\\xe9|\\x97\\xbcI\\xa73<ga\\xbe\\xbc\\xd6\\xc9\\x83\\xbd\\xe0\\xce\\xb6\\xbc\\xbd?\\x13>\\xb2\\xf2\\xc4;\\x9e\\x82\\x05=\\x87\\x8d\\x18\\xbb\\xe2~9<#\\xb9Y\\xbd\\x91\\xb3j=\\xb1\\xb3h=s\\xd9\\xfd\\xbc\\xa6u\\xc9\\xbc\\x1b\\xc7t\\xbc\\x82\\x0b\\x06\\xbdJv\\xac\\xbc\\xd2U\\xb9;\\xbcy#=K\\xa4\\xde:\\xf4\\x88\\xb1<\\xba!\\x03=\\xf5\\n^\\xbcn?\\xba<\\\"\\xfa\\x978\\xcb\\x19\\x1f=-\\xc4I\\xbdH\\x8a\\xc3\\xbc\\xa1\\xcek\\xbd\\xaeH\\xc5:\\xa5\\xcf\\xb6<\\xb7\\xdcs=\\xee\\xa5\\x88\\xbc\\x17\\x9d\\xba<K]\\x89=\\x87+>\\xbd%\\xb97\\xbc1z\\xc2<^K\\xd3;8\\x1a\\xcc\\xbc\\xf4\\x97e\\xbdq\\x88>\\xbd\\x18-\\x12\\xbd\\xf3\\xd16:@8\\xd7;\\x0cvB<\\x13\\x12\\x84<<hm<z\\x8al\\xbdi0j\\xbc0y==.*\\x84\\xbc\\xc6HM\\xbar\\xcd#\\xbd\\xca\\x86\\xa6<\\xd6n\\x0f\\xbdl\\xab\\x92\\xbcw\\xac\\x0c\\xbd7\\x90\\xa0\\xbc\\xa1\\x1c\\x9a=\\xe0Z\\xb1\\xbc\\xbb\\xee\\xca;\\x9e6\\xb0\\xbd\\xaac\\xb3<\\x89\\x96\\xa4\\xb9wi\\xdc\\xbc\\xf5:\\xc0=2{6<\\xa3\\x15\\xc6\\xbc\\xcd\\x82t<\\x96T@=\\xb5\\x10\\xd6;\\xed|\\x0e\\xbdk{Z\\xba\\x94\\xca\\x87\\xba\\x05\\xf6\\xb5\\xbdb\\xf6#=\\xbb\\x1c\\x14<\\xb8$\\xba\\xbbu>\\xcb<\\xcc\\xa1\\xa0<M\\xd8\\x8d\\xba\\xed@\\x16=#z\\xa5\\xbc\\x04\\x80o\\xbd\\xd6T/=g\\xab\\xa8\\xbb\\x0e\\x81\\xc1<x\\xddM\\xbdn\\x96Z\\xbb\\xf83?\\xbd\\xf6M\\x11\\xbd?\\xf3\\xec\\xbb\\xec\\x9b%\\xbd0\\x19\\xa2:Y\\x06&;u\\xbb!\\xbd\\x8b\\xa1&=Le\\x98=.q$\\xbdh.W\\xbd\\xae?\\x16\\xbe\\x0e\\xca\\x06\\xbd\\xd8\\xc3\\\"\\xbd..\\x1b<\\x16\\xd1\\xce\\xba\\xa0\\xcd\\xab;\\x97\\x91\\x05=\\xf8\\xdb\\xd2\\xbc\\x80Ll\\xbb\\x19\\xc9?<\\x81Ea\\xbcX\\xd9\\xc2\\xbc\\xfd\\xd0?=\\xcb\\xa4\\xb2:\\xc2v=<,\\x94\\x03\\xbc\\xee=#=\\xd5uD\\xbd_v\\x0e=gp\\x18\\xba\\xb6W\\x1f\\xbd^\\xd5\\xae;0f\\x1d\\xbd\\xeeP\\x00\\xbcw\\xd5\\xe7<rI\\x9a<\\xfd\\x07&=\\xbd0\\xbd\\xbcq ]\\xbc3\\x18\\x9c\\xbcCi\\xe6\\xba\\xfd\\x1f\\x0f=\\x01\\x1a\\t\\xbb\\xb6C\\xfc<\\xe1b\\xf4<\\x8b\\x06\\xcb;\\x0f\\x9dn\\xbb\\xa6\\xc9E\\xbcj\\x02\\xb9<\\xd7!6<pN\\xbc<jl\\xc8;\\xdeX\\x1d\\xbb\\xfb\\xbd?=\\xe9\\xe8$=Z1\\xfb;\\x86P\\t\\xbd \\xc9\\x83<\\x86\\x03\\xfb;q\\x91\\xd6\\xbd\\xc2\\x15\\x82\\xbb(ru=*\\xe7\\x03\\xbd\\xa7\\xd7^=\\x10\\x1f\\x17\\xbd\\x1a\\xae\\xce\\xbcX\\\\\\x9c\\xbaR\\x1d\\x8e=Ff)<\\xec\\xb5\\xc4\\xbc\\x11\\xc5\\xaa\\xbb\\xf8\\x9cf\\xbc\\x04\\xcd\\x02\\xbdjB\\x9f=\\x18\\xda\\x05\\xbc\\x00\\x95\\xd5\\xbb\\xbcdc=\\xbe\\xab*\\xbd\\xda$\\x15;u\\x18 ==\\xf8O\\xbd\\xfd\\x81\\x80;\\xa4\\xa0\\xee\\xbc\\xe02+;XXi\\xbcG\\x92\\xde\\xbc<\\xaao=\\x15\\xa0k\\xbd$\\x1b\\xac<\\xf8F\\x12=\\xb2\\x85\\x84\\xbd\\xa0E\\t=\\x01I =i2K\\xbb\\x89\\xc5m<\\xc2\\xbf\\x9c<BC\\\"\\xbd]p\\xad\\xbc,\\xdc\\xc8<\\xd0\\x9b\\xa1\\xbcR\\x06\\x98=p\\xab\\x11=k\\xaeI\\xbc\\xbf\\xb9\\\"\\xbd\\x1b\\xd2m<\\xd9\\xfa\\xf3< \\x15\\x9b<\\xad\\xb9j\\xbc\\xfcJ}=\\x1b\\\"\\x9c\\xbc\\xcf\\x1d\\xb4=\\x17)\\x81:\\xde\\xf7\\xf5:\\xdc\\xea_<L\\xb1\\x0c\\xbc\\x8cP\\xb1;\\xa2w\\xa2=\\xd0\\x19\\x00\\xbcL\\xbf\\xee\\xbc\\x90E\\\\<\\x18\\x90\\xa7\\xbc\\xc8\\xcf\\xce<xD\\x93<R!\\xd7\\xbaf\\xeb\\xd6:\\x9f+\\xcc=;\\x8e\\x82\\xbb\\xc3\\x98(\\xbc\\xbe\\xeeW\\xbd\\xa5\\xf7\\x7f\\xbc&\\x187=n\\x13\\x1f\\xbc\\xc6r)\\xbbos?\\xbd\\x81a\\xce\\xbc\\xc7;\\xde\\xbd\\xd2N\\xa0=o\\xfe\\xe7\\xbc\\x9c\\x9bP=\\x94](\\xbd\\xb1\\xcb\\x81\\xbdB#\\xd0;\\x9b\\xaf\\xca:0\\xfe\\xb3\\xbc\\x88\\x11\\x11\\xbd\\x1109=w\\x0b\\x14\\xbc\\t\\xb9\\xc9\\xbb\\xdfX\\x13\\xbd\\x08HA\\xbd\\xab\\x1aw=}\\xa8\\x00\\xbc\\x1bZ\\x9c\\xbc*w\\xe0<\\xf4m?<\\xfath\\xbc\\x91N.=\\x03.x\\xbd\\x18\\x8ed\\xbb\\xcc\\x9bm<\\xe6g\\xa9\\xbc\\t`\\x1e:u\\xc44;8\\xcc\\xa3\\xbc^\\xac\\x98\\xbc\\x84\\\"/\\xbc\\x1a\\xb1\\xed\\xbc\\xef\\n\\xa9\\xbds\\xa9_\\xbd\\x96\\x8b2:\\x83\\xd9\\xbf\\xbc\\x98|\\x8d=\\ro\\x14=|\\xd5\\x0b<\\xc5\\xd1\\xff\\xbb\\xba\\x9a\\xc0=J\\x0e\\xc2\\xbci\\xc5X\\xbc\\xd4b\\x15=_\\xdb\\x9e<\\xca\\xfd\\x12=\\xc1\\t\\x84\\xbd\\xc49\\xca;7\\xb9g\\xbb\\x94\\xc7\\x85\\xbc&C\\xe9\\xbc\\x00\\xe1\\x93\\xbcVM\\xdf;\\x99\\xe3%\\xbd\\xff8\\x10\\xbd\\x85\\xfb\\\"\\xbd[x\\x99\\xbb\\x148S\\xbdjG6\\xbd\\x0b\\xb9h;s\\x14N=8(\\x82=\\xa2\\xbd\\xf2\\xbc\\xc5\\xbf\\xf4\\xbcv\\x83\\xb2\\xbb\\x85\\rm=$kc\\xbc\\xb9&\\xd9\\xbb\\xac\\xb28\\xbb\\xd5x\\xca\\xbd]\\x8d\\xe3\\xbc\\xd7a+=\\xd3\\xdbe\\xbd\\xd9\\xee\\x9a<\\xef\\xe9#\\xbc\\x1c\\xff\\x8e<\\xac\\xd6\\xc9=G\\xd1\\xd6\\xbbP\\x84\\x80\\xbd\\xcc\\xd5i=\\xc4\\x1c\\x95<\\x93\\xc6r\\xbc\\xafO\\xd6\\xbbq\\xdc\\x85=8\\xb7\\x8a<\\x97\\xd4\\xe9\\xbb\\xbf\\xf3\\xa1;k<\\x87<\\xcc\\xf2\\x95<\\x1d|\\x94\\xbd\\x93\\\"\\x8a\\xbc\\x9eNE=b\\x8f\\xaf<|\\tj<\\xbaar<L\\xd0\\x0c\\xbc\\x80Z0=\\x83z\\x8e\\xbc\\xc2\\xe2\\x13<\\xf5]@\\xbd\\xf1\\x9cg=\\xbaX5\\xbdeV\\xe1\\xbc\\x81\\x91\\xd1\\xbb-\\xf4\\xf4\\xbc/\\x0e\\xba<*\\xff\\xca\\xbc\\xf5|&\\xbd\\x98\\x05M=\\x1ed\\xd4<\\xd1DY=\\x92\\xe0\\x85\\xbc\\xc6d\\xbd\\xbc&G\\xeb\\xbc/i\\xa2<+V\\x82<?\\xc3\\xce;H\\xcf\\xb6<\\x93$\\\\\\xbc\\x95Ks\\xbd\\xc8\\x14=\\xbc\\\">\\x02=\\xdd\\xb3\\xfa<\\xc2h\\xcc\\xbc\\xc2\\x08:\\xbdy\\x04a<\\x9c\\xcd\\x15\\xbd\\xb2\\x8e+\\xbb7\\x93\\xe1\\xbb\\xf6\\xbf\\xe4\\xbc\\\\\\x0f\\x0f=\\xcf\\xf3]\\xbd\\xbe\\xfc\\xae=\\x90A\\xa2=\\x03z\\xc8<>\\x98l=\\xc9_&\\xbdAh\\\"<OB\\xaa=S\\xe2\\xd4:\\xa6\\x13!\\xbd<\\xeb}=\\x7f\\x00\\xa2\\xbc\\x18!\\x0e\\xbcK\\xcf\\xb3\\xbc\\x85i\\x9a\\xbcWU\\xcd\\xbb\\xb4M\\xbe\\xbc\\xf5z\\xb1<\\xc9A<\\xbd\\xb92\\x18\\xbd\\x9d./\\xbc\\xf1L\\xb19\\x915`<q\\xc4\\x9c\\xbc\\xdbF\\x17\\xbd]\\x83\\x10=\\x10\\xbe\\x00;rP\\xb0\\xbd\\xdd\\x18\\xe1\\xbc\\xfc-\\xb6\\xbc_1\\xd8\\xbb\\xb0]\\xc3;L\\x8e\\x04=\\\"z2<\\xb4)g=s\\x8d\\x06\\xbc\\xd8\\x05\\x85\\t\\xd4^\\xee<\\x13^\\t\\xbd^g\\x1b=s\\xae\\x01=\\xcf\\x0f~<_\\xe2\\x80<\\xfb\\xd6\\x17\\xbd\\x9f\\xbex<\\x06\\xa6\\xee\\xbat\\\"\\x8c\\xbbE\\xe5\\xc4<\\t\\x94\\x1a=\\x8cY:<\\xe1S0=%:y\\xbc\\x85\\x9a\\x07;\\xcaTE\\xbd9z\\x19=\\x1ap\\xa3\\xba\\xad\\xa2K=?\\x8fe\\xbb\\x9b$\\xb7<\\\"\\x86\\xe2\\xbc\\xee\\xd9-\\xbd\\xa5C\\xc9;#Zc\\xbaU\\x91\\xe4;C\\x84\\x90\\xbb[\\xb25\\xbd\\x0b\\x9b\\x8a<5.\\x01=\\xeeB(\\xbdxng\\xbd\\xb3\\xa6\\x8c\\xbcR\\xc4\\x05<f\\x17\\x18;*\\xfd\\xd0<\\x82\\xb0\\xe7<\\xfaCD<-r\\x16=\\x88\\x0c\\x9f=\\xe2\\xa1\\xe1\\xbbfy?\\xbb\\xda]\\x0b=\\xb8\\x91\\xa7=\\xfd\\x9b\\xff;\\xa1a$=dob<.gw\\xbc3vr\\xbc5+m\\xbdn\\xd0\\x8a\\xbc\\xde\\xc2\\x90<\\x0c\\x93\\x16=@g\\xbc\\xbcv\\xd5\\xbc<\\xef\\xc1\\x06=\\xad\\xc20=\\xa4\\xf8c\\xbc\\xc2\\xc2\\x87<\\xbaB\\x06<J1\\xcf<\\xb0H\\xa5<f\\x89:=\\xa0K\\x1a\\xbb4w\\xf2\\xbc\\n\\rG\\xbca\\xd9\\xd1<4_\\xa4=\\x96-v\\xbd\\x03\\x84L;\\xae\\xe1\\xe1:\\x90<\\xd4\\xbcN\\xa1\\x1f=\\xf8tw<\\x94\\xb8\\x1d=\\x8bSM\\xbcQ\\xf9\\x87=\\xde\\xf0\\xe1\\xbcW\\xb0$<mZ8;J\\x18\\xc2<\\xb0\\x15O\\xbc\\xe0\\xf9\\x83=\\xa9\\x8a\\xd8\\xbbsd\\x05\\xbd\\xb51`=y\\xfbj=\\xad\\xf5\\x9b<\\xa2\\xb1R<O\\x8e\\x1b\\xbcV\\x08\\x91\\xb8=\\x96\\xed<?S\\xc3:T\\\"\\xa1;9\\xa6\\xe9<\\x0bp\\xf6\\xbc\\x9e^1=D\\\"\\x11\\xbb\\xc6\\x0e\\xf5<\\x9e\\x81\\xe7\\xbc!\\xb0\\xda\\xbc\\xad\\x91\\x96\\xba=\\xa0\\x01\\xbdL2\\x10=\\xcf0+\\xbcwF\\x0b=\\xfa`\\x89<s<_\\xbd\\x9e\\xa4\\x9b;\\xe9\\xbcM;\\\"z\\xba\\xbc\\xb4Q\\\"\\xbbf\\x8e\\x90=\\xd5\\x08l; ]\\xa2\\xbc\\xc5!\\xed<\\x13k\\xb2<\\xd0\\xaa\\xdd\\xbc\\x1f\\xb7\\x97\\xbd:\\xfb\\x08=\\xf9O\\xa1<\\xc3[\\x8a<:\\x19I<=u\\xf8<\\xf4\\\"\\xcb< \\xb1\\x95\\xbd\\xf5\\x8a7<\\x0c\\\"\\xa8=(h\\xcc\\xbdy\\x82\\xe7<f\\xc4=\\xbcw\\x0c\\x95=\\x9al\\xd2;\\xff\\x05\\xa4<`m\\xd4<_\\xb1-=z\\x0b\\x87\\xbd\\x13\\n\\t=Abk\\xbd\\x00h\\xb9\\xbc\\xf9Z\\xd7\\xbb\\xd4\\xc0K\\xbd\\xe5\\xfe$=a\\xb2\\xf5\\xba\\x16&\\x9e\\xbc\\x1f\\xe8\\xc3\\xbb\\xf8S\\x07\\xbc\\xd9\\xa1\\xba<#\\x07N;\\xee\\xd6!\\xbc\\xc9\\xb6\\x80\\xbb\\xc1\\x7fM\\xbdR\\xc0q<\\x17\\xe8>=\\x08\\x15\\x01=.\\x98#\\xbd*\\xee1\\xbcW%3=c\\\"K\\xbd1\\xcb4\\xbd\\xf7\\x86\\x81=\\xee\\xc5\\x82;\\xf3\\xb5\\xf6\\xbc\\x18\\xbf\\xcf\\xbc\\xb1X\\x17\\xbc\\xc5\\xefL\\xbd\\xa8xn\\xbc\\xe9\\xc6\\xaa=\\x0eA\\xa9\\xbc$\\xf4\\x0b=\\xfe\\x07\\x1e\\xbd\\xe5\\x80\\xe9<\\xda\\xea\\xc5\\xbc\\xcf\\x8f\\xcf\\xbcE\\x82)=\\xbc\\x842=h5>=@_\\xe98G\\x83\\xdc\\xbck\\xa8\\x13<\\x84s?\\xbd\\xa2\\x83d=\\xdd\\xaf\\\\\\xbd\\xbf\\xf2\\xc4<\\x9a\\xf3e\\xbdYtW<CS$\\xbb\\xccf\\\"=+\\xc5Y\\xbcJ\\x1c\\x11\\xbd\\xbcb\\xfe:n\\xf45\\xbd\\xa5\\xf3N\\xbd\\x92.\\x9f<\\xb6\\x12\\xde\\xbb\\xd7\\xd4u:\\x1b\\xa2\\n\\xbck1\\xb4\\xbc\\xc4\\xcd7\\xbdO\\x97\\n\\xbd\\x8a\\x9a\\x03=\\xb86%\\xbd+I\\x13\\xbd\\xb9h\\xeb\\xbc\\xa0T\\xce\\xbd\\x05y\\xfa<j\\xdaW<8\\xc5\\x83\\xba~!\\x16\\xbd@\\xf4\\xdb<d\\xcf\\x1c\\xbc\\xda\\xb0c=\\xa6B\\x08\\xbds\\xfb:\\xbd\\xfa[J\\xbd,\\xb5Z=\\x84jQ\\xbc%W\\xbc;\\xba\\x86;\\xbd x\\x8f<\\xedE\\x18\\xbdY\\x93R=+\\xac\\x97=\\x9a\\x83x=\\xc0Gj\\xbb\\x08\\xb1\\xcf<z\\x11)\\xbb1CJ\\xbci\\xfc\\xae<=\\xae7<\\xb4\\x89\\x04=%[\\xb9\\xbbA\\xc0\\x88\\xbd\\xca\\xf7S\\xbd\\xb1\\xd6\\x80\\xbb\\x7fk<<DB\\x12\\xbd\\x100\\xa6<\\x9ax0<+\\x94\\xf4<\\xfb\\xbec<\\xbb\\x16T=G\\xe4\\x15\\xbd\\xb3\\xf5\\x9e\\xbcy\\xb1O\\xbd\\x0b.,\\xbdJ\\x81\\x10\\xbcG\\xf2\\x88\\xbd\\xfb\\x17t\\xbc|%\\xa2<\\xdb\\xa2\\x0f\\xbd\\x92\\xfe\\xbc\\xbb\\xf6,]=\\xd1T\\xfe\\xbcB\\xbc\\xe3;:\\x92^\\xbd\\xac\\xf2\\xcb\\xbc\\x11y\\x7f\\xbdJ@\\xcd\\xbcq\\\"\\x8a\\xbb\\x91\\xfc\\xca\\xbc\\x06\\xd2\\xba;v&\\xd9<\\x8aJ\\xd0=]Gi<\\xdc|k\\xbc\\x81;@=\\xe8\\x82$\\xbd]q\\x0c=V&\\x1c\\xbcL\\xff\\x16<\\x9b\\xec\\x82\\xbc\"\nHSET bikes:10036  model 'Hiiaka' brand 'Nord' price 4693 type 'eBikes' material 'carbon' weight 9.8 description 'This bike feels like a higher-quality product than the price indicates. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\xa1S\\x8f\\xbc\\x9ft\\xeb;h<\\x8e\\xbc\\xe6\\x1e\\xaf<\\xf3\\x08\\x86\\xbc\\xd8\\xda\\x17=9\\xa6\\xb6\\xbc\\xa1o\\xd1\\xbc\\xc6\\xdd\\x83=@Z7=\\x8fN\\x10=\\xa1\\x06\\x94<<\\x9d\\x06=O=:;\\xb6~\\xab=:\\xac(\\xbd\\x1fX:=\\n|l\\xbd\\x80\\x95\\xc3<8\\x1d\\xb1\\xbd\\x9b6\\xdd;\\xd1L\\x0b\\xbd<}H=\\x85\\xd6+\\xbdE\\x88M\\xbd.\\xac<<\\xb4\\xfao=\\x7f\\xd9.\\xbc762\\xbd\\xd5\\x88\\xdd=\\xcd\\x92\\xc5\\xbc\\x92\\xac5\\xbd\\n\\xc0Z<u\\x8f?<\\xba\\x9a\\xe0\\xbb\\x17\\x91\\xdf<\\xfe\\xcd\\x8d;\\x85j\\xb3\\xbc\\x9f\\xf6\\xe4\\xbc{,\\xc5\\xbb/j~=T\\xcd\\x0e\\xbc\\x8f,Z=zt\\xf6;\\xd3\\xab>\\xbbxW\\xb6\\xbc\\x90]\\xd5=\\xda{\\xd6\\xbc\\x96\\xab\\xab\\xbc\\x07U\\xc5\\xbb\\x1ek*\\xbd\\x19\\x05\\x1b\\xbd\\\"F\\xc7\\xbc\\xff\\xea\\x9e<g\\x06\\xe9\\xbcZ\\xcf\\x0f\\xbd\\xad=\\xdb\\xbb\\xe2\\xce\\x8d;Q\\xd1\\x00\\xbd\\xc5\\x81\\x94=6F`<\\xe0n;<9;\\x9d<x\\xe4\\t=h\\x1c\\xa4<;\\xe7\\x81<\\xf3\\xe4Z9\\xee\\x8c\\xaf<l\\x07\\x96<\\x1a\\x03\\xf1\\xbc\\xc3\\x8d\\x9d<\\xa5\\\"\\x13=a\\xa0\\x92\\xbc/\\xc9\\xbd< \\xec7\\xbd\\xc4[\\xe9\\xbc\\xe5\\xe4\\x8e<\\x189$\\xbd\\x00\\x11\\x9e\\xbdl\\xa1w<\\xfe\\x17\\x0b=or\\x19\\xbd[\\xd7J<\\x07\\x90O\\xbd|{\\x0e;o\\xa4\\x17;\\xc9fn<\\x8c\\x9c\\x95<\\xd7w@=\\x9e\\xdd4\\xbbU\\xb5\\x9c\\xbb3/8\\xbdU>G\\xbd5\\x05\\x14=8 \\xb8<Jj\\x98<Q\\x02C\\xbd\\xad %\\xbdl\\x86!=\\x8d\\x94(\\xbd\\x85-\\x8c\\xbc\\x96\\x01\\x83<\\x9f\\x9f\\xd4<\\x0f\\x8c\\xbf\\xbc0^\\x95;`Z\\xc2=\\xd2\\xed\\x0b\\xbc\\xe3\\xfd\\x84<V\\xf08\\xbc\\xd1\\xa9L\\xbcR6\\t\\xbcQ\\xc2$=b\\xa94:n$(\\xbc\\xdc\\\\4\\xbd\\xa7[D\\xbd\\x92dl\\xbc*\\x02\\xbb<\\xd45@=\\xea&\\xf9\\xbc\\xc2;\\x1d\\xbc\\x94\\xc8\\x85<\\xed\\x19R\\xbd8\\x06!\\xbdy^\\xfd<\\x84D*\\xbcPgr\\xbch6\\\\\\xbdg\\x1c5\\xbd\\\"\\x04x\\xbd\\xce\\xf9\\xe0\\xbcdT/\\xbb\\xc3\\xd1\\x93;\\x99\\xda\\x0f\\xbd\\x0f\\x96O<yM?=$\\x90\\xe6\\xbb\\xa7\\xf5\\x95\\xbc\\xa8\\xa6\\x83\\xbb\\\\\\xb2\\x94\\xbc\\x8fy\\x12\\xbd\\rIE\\xbd\\x07\\xc9\\x1a<\\x18\\x94\\xe1=\\xed\\xa8\\x97<!i\\xc1\\xbb\\xa0\\x1b\\x1c=\\xa4\\x0f\\x08<\\x83h\\x9a\\xbcN\\\"X<\\xe5\\xee\\x8f\\xb9\\xce\\xe6]=;\\xb3\\xf8\\xbc\\xf3\\xad\\xd4<1>@\\xbd\\\\\\xbaM;\\x9b\\x19\\xe7<\\xf1\\xf1\\x08\\xbd\\x7f\\xfe\\x13=}\\xc3_\\xbd\\x9d\\xef\\xc5<Xp0<\\xf1\\xdf\\xe3\\xbb\\x96\\xf6\\x82\\xbd\\xb5\\x80\\xbe\\xb9\\\"\\xd5$=\\x80\\xae\\xf1\\xbc\\xf4\\xe1]<\\x8b\\xb0M=\\xf5\\xb2\\x8d\\xbb6\\x91\\xd3\\xbb\\xf9\\xa4+=\\xd5\\xcf*\\xbb\\xcb\\x145\\xbd\\x95\\xf8$\\xbcuo\\x03\\xbb\\xd1\\xf0\\x19\\xbdGfo=l\\x87\\x90\\xbck\\xa1?\\xbc\\xb2\\x98\\xe7\\xbc\\xa5c\\x87<\\x13\\rF<\\xa0\\xae?\\xbcn(&=m\\xcb\\xb0\\xbc/\\xac\\xcc<nfX\\xbc\\xa7\\x81I;\\xb8r\\xbd\\xbc\\xfe\\xc4\\x1a=?3\\x8a=W\\xf8\\x0f\\xbd\\xf1\\xd7\\xc4<\\xc7f\\x14\\xbc>\\x1e\\x1a<\\x9e\\xec\\x91\\xbd\\x97.\\xdc\\xbc\\x8b\\x1a\\t\\xbdJo\\x01=a\\xe9\\xd0\\xbcR(N\\xbd\\xb1o\\xd3\\xbbB\\xa0\\x8b\\xbc\\x1bl\\xd5;\\xbc\\x13\\x12=ys\\x8f<Lg+\\xbd\\xab\\xfa\\x85<\\x91\\xd8\\t\\xbdm\\x84\\xbb\\xbc`\\x1c9\\xbdV\\x85\\x93=\\xf4\\xc1\\xa3\\xbcga\\xa3\\xbc\\xe4\\x0b\\xd9:w\\x03\\x9d\\xbch@\\x81\\xbdl\\x19\\r<\\xbf>\\xc7\\xbd\\xe7*F<=7\\xfa\\xbc`\\x81y\\xbc[</9\\rY\\\"\\xbd\\x9e5I\\xbbh7\\xa2<;\\x12\\xe2\\xbc&B[=\\xc7+\\x8e\\xbc\\xbb\\xa9^\\xbd\\xces\\xa9\\xbd\\x03\\x05\\xa9\\xbc\\xb2\\x17}=\\xc8\\xf8Q\\xbc\\xa2M\\x0f=\\x01^R<^PQ=\\xb9\\x06{;\\xe7g\\x86<\\xcdZ\\xb7\\xbcP\\xa4\\xc6\\xbb\\xaf=J=6\\x03\\x93:\\xaf\\xf0\\xb3<7\\x9e\\x93=\\xd3\\x82-\\xbb\\xf2\\xd4\\xf8\\xbd\\xc4#\\x81\\xbd\\xbd\\xe0\\xdb\\xbad\\ro\\xbb\\xceRj\\xbcvZ\\x8e<=\\xa3\\x18<\\x06\\xf7\\x8c\\xbd\\xde\\x97\\x8d=[\\xb5!\\xbd\\x94\\xfd\\x83<O#\\x16\\xbd\\\"\\xad\\x87=/\\xd0\\xdf\\xbc._~\\xbc\\xa8g\\xe5\\xbc\\xc6p\\xd3\\xbb\\xa5\\x15\\xdd\\xbc\\xb2\\xb6\\xb6=\\x9a\\xac&\\xbc%\\x19\\x03\\xbd \\xd6q<\\xb8\\x9f\\\"<\\tq=;\\xe4\\x98U\\xbcMK\\x8a\\xbb\\xddI\\x9c<p\\xfa\\x7f\\xbc\\x00\\x15\\xe1:\\xd7[\\\"\\xbd\\rm\\xf8\\xb8R\\x95\\xa0=\\xb6\\xea\\xbd\\xbddZ\\x03\\xbd\\x1cT\\xc6\\xbaXh1\\xbd\\x0fp\\x9a<\\x85N\\x1c=\\xb68\\xe9\\xbb\\x1aM\\xa8<\\xd6f*=\\xa4@\\x8b\\xbd97\\x81\\xbbY\\xc5\\xba<\\x03\\xc9\\x8a\\xbc\\x9a\\xab\\xe2<\\x1c\\x9fa<\\x8d\\xecd<\\x08Q\\x10\\xbd\\xe3\\xf1\\xf5<\\x90\\\\\\xdb<\\xe6\\x80{<\\\"\\\\\\xba\\xbc{\\x80\\xc6<\\xbeP];\\xba\\xc6\\x16=r\\x04F\\xbd\\xa9>\\x02<\\x05\\x1c\\xbc\\xba\\xe3d\\x13\\xbc \\xba#\\xbc\\x93}\\x01=\\x13\\xeb\\x1c\\xbd\\x13X3<\\xc2\\xa8q\\xbdG\\xa0g\\xbcu\\xf1\\x11\\xbc\\xced\\xe0<\\xf4-.\\xbd\\xf4w[\\xbd\\x16\\xdae=\\x1c\\x98^=R{\\xc6\\xbc0\\x17\\x08\\xbd\\xba|\\x82<\\x0f~\\xb9;\\x9a(\\x81<\\xef\\xbe\\x06=7\\xddB\\xbd\\xc5\\xf8A<\\x97`)\\xbc\\xb9\\x94\\x9c<Zt \\xbae\\xbf`=\\\"\\xce\\xac\\xbd-\\xfd\\x0e\\xbc`\\x1a`;\\xe8C\\x11\\xbc\\xe7\\xf2\\xfa\\xbb\\xed`\\x03\\xbd\\x1d\\x00\\xf1<\\x8a$\\xcd<A\\x9d\\xb2<fR\\x03\\xbc]\\xba\\x0b\\xbc{v(=\\xe9\\xa3o\\xbc\\xff\\x18\\x1a=\\xad\\x8c\\xf3<G?\\xa2=\\xe9?\\x0e=\\x1aIm=6\\xdd$=\\xa8\\xa7\\x03<\\x8e}\\xdb\\xbc\\xde!\\x96=.e\\xc8\\xbc=\\xe5\\x05\\xbdt\\xfd\\x03=\\xe5\\xfd\\x91\\xbb4-\\xb3\\xbd.o\\xb8\\xbc4\\xb1\\xc5\\xbd\\x8d\\x0b\\x03\\xbd0k\\xfa<\\x13\\ra<\\x02!+=GK\\x05=\\xd0I4=\\x00\\xd1\\xaf\\xbc\\xf2\\xda\\xf2;\\x9b\\xecc\\xbdm\\xca\\xfa\\xbc\\xdamw=E\\xf3\\xa0\\xb6\\x9a\\x16\\x1b\\xbc\\x89\\xe1\\x92\\xbc\\xc8`\\x85<\\xac\\x17k=\\xcc\\x06u\\xbd\\xcdN\\xe5\\xbcM\\x9a\\xec\\xbct\\xdc9=\\xa3A\\x19\\xbd\\x1c\\xf4\\x95\\xbd\\x12\\xa5\\x03\\xbde\\x1b\\xc0:\\x80\\t[\\xbd6\\x9d\\x92<)\\x97\\xcf:\\x81\\x1d\\x94\\xbc0z\\x81=\\n\\x8d\\xb7<\\xd7\\xbb{\\xbd\\xc79\\x9f=\\xbb%\\x99=\\x0b\\x17j\\xbcM\\xd5\\xd2\\xbb\\xb9\\x8f\\xab\\xbbQ\\x8c\\x99\\xbdGf\\x9c<\\xcc\\xb7n<\\xfd\\xef\\x19\\xbd\\xbd\\xd0\\xad<x\\x9b\\x8e\\xbd\\xec\\xe3\\x13=;d\\x97<\\x1bB(\\xbc\\xa7\\xde\\x02\\xba\\xbf\\x1a\\x90<w\\xff\\xf0\\xbc\\xe3\\xe2.=,\\xd4 \\xbd\\xfd\\xa1I=,\\x85\\x88<E\\xf6\\x12\\xbcb\\x15\\x83\\xba\\x8d\\x15\\xe1\\xbb\\n\\xfd\\x06=g\\x9c\\xe7:q\\x9a\\x10<\\xf0\\x7f\\xb2;\\xc5\\r\\xa3\\xbc\\xe0%(;\\xce\\xb8t\\xbc\\xedi\\xaf<F\\x97\\x05=\\xbf\\xdf\\x00\\xbcV\\xffZ\\xbd7;\\x06<-\\x01\\x1b=\\t\\xe2\\xe0\\xbbKc%\\xba\\x83\\xd3\\x06<\\xab\\x9c+\\xbd\\xc4\\x07\\x1a\\xbc0\\x15\\x86\\xbdU\\x88\\xcd\\xbcA\\t^=\\xc9\\x1d:=a\\x94?=d\\xd2\\x92\\xbb\\xe2h\\xcd;\\x0e\\x17e<{h\\x9b<\\xb1q\\x9c\\xbc\\xba\\xb3\\xd1\\xbc\\xec\\xd2\\\\<\\xcbE\\x1c<\\x07\\x1bZ=\\x80\\n\\xdd\\xbb\\xf7\\xc1\\xd6\\xbc\\x91\\xb3[=\\xc5CK\\xbd\\\\\\x98\\x90\\xbd\\x1b{\\xc7\\xbc\\x92\\x07|\\xbdd\\xe7\\xec<\\x0f\\xd1\\xce\\xbb\\x94\\xb6\\xa1\\xb8&\\x17\\xe4\\xbc\\xbd\\xb8:\\xbdEC\\t=S\\xddN=\\xa2\\xe9\\x04\\xbcA\\x12X=\\x19\\x8d\\x8d\\xbc\\x8e\\xcd\\xd7\\xba\\xc2$D=3`3=\\x86b\\xde\\xbc\\x0baV<\\xf1\\xb9v=]\\xcb\\x84\\xbc\\xdc\\x91C<\\xb1\\xfb\\x02=\\x1a\\xac\\x07\\xbcf\\x07\\xb0;k\\xa4R<\\x84\\x918\\xbdb\\xe4\\xc2\\xbc:E\\xd3<7\\xc72\\xbc\\xb6C\\xd5<\\x05\\xf8\\x8f<r\\xca\\xab\\xbc\\x8f\\x88\\x17=\\x9f\\xe5\\xd2\\xbbBW\\xc9\\xbd\\xa8d\\xf2\\xbc\\xd8\\x16\\xa5\\xbcT\\xed\\xe2\\xbcs\\xb8\\r=\\xed=)\\xbd\\xc63\\xd9\\xbc\\xb5c\\xf8<\\xff\\xc70<K\\xd2p\\t\\x80\\x84\\x07=\\x86\\x94^\\xbc?s\\xc1<\\xea\\xdb\\xe4\\xbbw\\xea\\x96\\xbc\\xc8\\x993=JT\\xb1\\xbc\\x0e\\x19\\xce\\xbcO\\xef\\x06=&\\xa7\\xd4\\xbc\\x80)\\x94\\xbbi\\xd9f<y=\\x96\\xbc\\xc9\\xf9]=\\xbeN\\xd5\\xba\\xeb;\\x1b=r\\xf7y\\xbd\\xbduW\\xbd\\n>\\x82\\xbb\\x8cf<=8\\xe0\\x1c=\\xe4\\xd6\\xb4\\xbb])^<\\xab\\xea%=!`\\x91\\xb9H\\x82\\\\<|$-;\\xb5\\t\\xbf<@\\xc34\\xbd#N\\xe4\\xbce\\xf2d;\\x06\\xb8a<\\xf2\\x16\\n\\xbbUIi\\xbd\\x12\\x02\\x1a\\xbcp\\xe4\\xda;D\\xbd\\x04=\\x98d\\x05=3Z\\x00=\\x05\\x95\\xdd<\\\\\\xe9\\xbf\\xbcY\\x8a\\n<\\xaevK=\\xe3\\xdaC<\\x9c\\xdd\\x01=\\xda\\xf6X\\xbc\\x1e\\xc1\\n>\\xe5\\xdc\\xa2\\xba\\x9f\\x02\\x81\\xbc\\xd3\\x04\\xce:\\xa14\\x00\\xbdj\\x07\\xb6\\xbd\\x12\\x8a\\x04=\\xe3H\\x8b;\\x8d3\\x1d94\\xb0\\x03=\\xc7pH\\xbdm\\xf2I\\xbb*c\\x90<NX\\x0c==f\\xa1<\\xec\\xc5\\xd4\\xbb\\x99s\\x1d\\xba^gC=\\xc7i\\xc9\\xbb\\x0cT}\\xbd\\xb3\\x08\\xe9\\xbcZ8\\x18\\xbc\\x8cM\\xb2<46\\xb2\\xbb\\x7f,\\xd0<,\\xe6\\x07\\xbd\\xe4@\\xb9<\\xb5=%=\\x9b\\x81\\xc8<\\xf3.#\\xbcC\\x8a\\x9c<\\x0b/d=J\\xbf\\xf7\\xbb\\x11H\\x1d=e\\xd3\\x00\\xbdR/\\xd2\\xbb\\xf4D`\\xbd\\\"R5=/\\xbd\\x9c\\xbd\\x9c\\x1e\\x02=\\xc4)V=C:v=\\x9f\\x85\\xad<&j\\xba<*\\xec\\xd0<\\xaau\\xcc\\xbb\\xd6\\xa7:=\\xa1\\x8b\\xd9:\\xc8\\xe9N=+\\x9e^=\\xdc\\xea\\x84\\xbb\\x96\\xf2s\\xbc\\xd8\\xaf\\x90<\\x0cw\\x0c=\\x80M\\xa7\\xbc\\x19u\\xa6<e$\\xe8;\\x02n\\x0b\\xbd{\\xbf\\x17=\\xbc\\x0e\\x82\\xbcF\\xc3\\xca<\\xce\\xa6\\xd3\\xbb\\xces\\x10\\xbd\\xc6\\x8f\\xd3<\\x92uw=<\\xd6<\\xbd!\\x17-\\xbc\\xe3\\xcb\\x80=\\x98\\xe9\\x80=`\\xd0B<]P\\x0b=\\xa5\\x8e\\xca\\xbc\\xc9\\x8a\\x11\\xbd> P\\xbd`l\\xbb<\\xbe\\xbb\\x89<\\x13\\x9b\\x03\\xbbu;/<\\xbe\\xb0k=.\\xaa\\x9a<\\n\\xe70\\xbd\\t]\\x02=X\\xabI=\\x9f\\x825\\xbd\\xe6\\r+\\xbd,\\x14\\x82\\xbdk_\\x96<\\xe6\\xbc\\xc0\\xbb\\x1d\\x13\\x95=\\xa6;\\xf3\\xbb\\xde3\\xe5<\\xe4@e\\xbd\\xd3U\\x00\\xbd\\xad\\x9c\\x15\\xbdo\\xe4\\xb0\\xbb\\x9f\\xd0\\xae\\xbd\\xe2\\xcdB<\\xc26\\x9b\\xba\\x8ee\\xdd<y\\x14\\xd3<\\x84n!=s\\x08/\\xbcg\\x0c>=\\xf4F\\xe0<b\\t\\xb1\\xbc\\xf7\\xd4\\xa6\\xbc\\x8b\\xd0f\\xbd\\xd2\\x84+=\\xc9\\xf2:<$\\x99r<Tf\\x06\\xbdxH\\xba=n\\xac`<z\\x04;<J\\xbbX<\\x97u^<\\xe3l\\xa6\\xbc\\x15\\xd4i;\\x02K$\\xbd\\xc4P\\xda\\xbc\\xbb\\xb1l\\xbd\\x1e\\xff\\xa1\\xbc\\xcf\\xa0\\x8c\\xbcY\\xbcQ\\xbd\\x8c\\x80k;0\\xaf7\\xbdG\\xba\\x8e<Uj\\x9d\\xbc\\x17`?\\xbc\\x9aH%=\\x17\\x86\\xe7\\xbc\\xfb6!\\xbb\\xbb)~=e\\x96\\x0b\\xbd\\xc8@\\xc3\\xbc\\x02&\\xd2\\xbd}bz=^\\xb4f\\xbc\\x96[\\xb18\\xef\\xe8-\\xbd\\x8c\\x82\\x91;\\xf6\\x00\\x8f\\xbc\\xf5\\xa9U<\\x1ae\\x8e<\\x14\\xba\\x05\\xbd~\\x06*=@\\x06\\x03\\xbd\\xd6\\xa0U\\xba\\xce\\xd9\\x96\\xbd\\x8a\\x8a\\xc9\\xbc\\x13\\xf5\\x0b\\xb9g\\x0c\\x90\\xba\\x04%\\x07\\xbd\\xfbe\\xde<\\x13\\xf3\\x06\\xbd\\x89\\x10\\xee\\xbc\\x10%\\\";\\x82\\x96\\xaa\\xbd\\xed\\x06\\x90\\xbc\\xbdV\\x1c<\\xb0\\x14l:\\xad\\xc1\\x87\\xbcj\\xe9d:H\\\"G\\xbd\\xb6\\x04\\xab<]\\xcc\\xca\\xbc\\xc0\\xaa\\x0f=\\xd9(\\xb6\\xbc\\x9aM\\x8b\\xbce\\xd99\\xbc\\x9e\\xe1\\x03=+\\x08\\x9c\\xbc\\xabP\\x9e\\xbd(~Q\\xbd\\x00\\x03\\xab;\\x1c2\\x04\\xbd\\x94K\\x1c=\\xa7L1=\\xbda8=\\xe59\\xa3<\\x8c\\x89\\xd1<\\x94\\xf7^\\xbd\\xee\\xff\\xbe\\xbd\\xfc2p<\\xf0\\x0b\\xb9;cs\\x0f=7u\\xfd\\xbc$\\x1a9\\xbc\\x04\\xa9\\x02=q\\xbe\\xa4\\xbb\\xc8\\xc8\\xc2;\\x8a=H=\\x7f\\xc7\\x00\\xbb\\x1fI\\x89\\xbc\\xffN\\\";\\xbc\\x1d\\xbe\\xba\\xcf>\\x96=t\\x90\\x86<\\xb4I\\x04\\xbd\\xec H=`\\xb5\\xc9\\xbc}\\xab\\x8e;\\x0c\\x9a\\xe7\\xbc\\xa9>49\\xea\\x95\\x10\\xbc\\x15V\\x9d\\xbc\\xf70\\xee;\\x95\\xce\\x8a=\\x86\\xf2\\x0e\\xbd\\x0b\\\\\\x1d\\xbdh\\x1c3\\xbc\\xee\\xd7\\x0b\\xbc\\xa1\\x8a\\x11<}1\\x7f\\xbdy,\\xf4<hj&=\\xeb\\xb5\\xdf<c\\x1f\\x00=\\xb6\\x88\\xa6={\\xa7\\xa3<vn1=7J\\xdd=\\x90\\xac\\xee\\xbc\\xba\\x19\\x89<\\x95Bk\\xbc\\xd9l\\x87<\\x9eI\\xcf\\xbb\"\nHSET bikes:10037  model 'Ganymede' brand 'Peaknetic' price 1091 type 'Enduro bikes' material 'full-carbon' weight 14.7 description 'The new version with 142mm rear, 160mm front travel is longer and slacker than its previous generation, but it’s also a bit taller and steeper than much of its competition. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"&\\xc1\\xb7\\xbane\\x85<\\xad\\xbd\\\"\\xbc4\\x91\\xa2<\\xcb\\x00j\\xb9~L\\x8b=Q\\xdc\\x1d\\xbd\\x8f\\xf4\\x85\\xbdY\\xa5\\x7f=C\\xf7\\xb2<I\\xb1\\xf0;\\xa3\\xdbb=\\x88:^=\\xc0\\x8f\\xc9;(au=l\\x98\\t\\xbd`\\xb5c=\\xef\\x8c\\x9a\\xbc\\x1byk\\xbcH\\xbc\\xe3\\xbc\\xe2J\\x9f<\\xbaG.\\xbd\\x06\\xee&<\\r+\\xa7\\xbd@\\x15B<\\xd46%\\xbc\\xf7\\xc3\\xd4<\\x91X\\x8d<\\x8b\\xf1B\\xbc\\xcb\\xbd\\xbf=h`h\\xbc\\x1d\\x98U\\xbc\\xe7\\x85\\xfa<r\\xfc\\xb4<\\x0b\\x85\\x88\\xbcx\\xd1\\x1b:\\r\\x14\\x95<X\\xb9A\\xbd\\x9a\\x1d\\xbd\\xbc\\x84\\xe2\\x11=\\xa6Qf=w>\\xa8<9E\\\"=\\x90:9=\\xef\\xb3\\xa4<\\xd6x\\xe3\\xbcw\\x00\\x84=\\xc9G\\x02<\\xd4\\xc4\\xd4\\xbbt\\x142<\\x90Vc\\xbc|\\x84x\\xbc\\xe1\\xe7\\x80\\xbc3\\x88\\xcc<\\xe6rO<\\x13\\xaeC\\xbdW\\xf2\\x0b\\xbc}\\xc4\\t\\xbd\\x9d\\xdc\\x13\\xbd\\xb4\\xb0\\x8b=\\xab\\xf9\\xde<\\xd4\\x917=\\x8e\\xa9A;\\\\\\xa5\\x93=U\\x0c\\xa0<3\\x17\\xce;z{\\x90<;G\\xb7\\xbb\\xbc\\x8ab<\\x9fE\\x02\\xbd\\x85e\\xcb<G\\x9fR\\xbb\\x170\\x88\\xbc\\x8b\\xbd\\xe5\\xbc\\x91\\xd9V\\xbd\\x82\\xa5\\x9c<\\xee\\xda]<Z\\xf7\\x9e\\xbc\\x92\\x05\\xab\\xbd\\xbf\\x1e\\xc2\\xbb\\x93\\tm<\\xc0[i\\xbdCw^<\\xfe\\xe0\\x95\\xbcx<\\x1c\\xbc|\\xa2D\\xbd\\xec|\\xf3\\xbb\\xc3\\x9f\\x85<3\\x00|=\\x10`\\xa3\\xbc\\x05\\xe8N\\xbd7(\\xef:\\xe4\\xe53\\xbd\\x03\\xd3\\x15=\\xa8\\x19=<U\\xe0\\xe5<\\xaf\\x87\\x9a\\xbc+\\xb2\\x10=\\xb4\\x98\\xd4:}W\\xb2\\xbc_\\xe3\\x1f<\\xbe\\xf2\\xcc<\\xf3\\x95\\x02=!\\xfd\\xc3\\xbc\\x96\\\"v<\\xd1@j=Mc\\xfb:\\xcbW\\x1a=\\xb1\\xca\\xaf:\\x8c\\xaa\\x1d<\\xed\\x16~\\xbc@\\x95\\x90=k|><\\x07\\x10\\x17\\xbdHx\\x96\\xbc\\x18\\xf7&\\xbd\\x17\\xba#;\\xd2\\xe5\\x92\\xb91.W=\\xde\\xbdg\\xbcgMZ\\xbb\\x1e\\xe4\\x05\\xbb\\x16\\x080\\xbd\\x0f\\xf8f;f|\\x15=c\\x19|\\xbc\\x94\\xc8\\x07\\xbd\\x1a\\xad0\\xbd\\x03\\x0bO\\xbd\\xd9&,\\xbdd\\xe5\\x84<7\\xf3U\\xbb\\xda\\x0c\\xff;\\xd9z\\x9b\\xbb\\x91\\x0c*<\\x08>U<\\xa6\\x1a6<\\xa9>,\\xbcnL\\xcb<z(\\x01\\xbd\\x1cE\\x8a\\xbd8\\xa63\\xbd\\xc5\\x80\\x12=\\x80\\xc1B=iq\\x88\\xbc\\x99\\n\\x8e\\xbc\\xd3\\xef\\x84<\\xa3\\xcf\\xe1\\xbb\\xc0\\x82\\x18\\xbd\\x1a\\x00\\\"\\xbd\\xb4\\xfa\\xe3<@\\xe0\\x8f=\\xf7$\\xde\\xbcV\\xb0e=\\x92H\\x94\\xbc\\xb3M\\x1c\\xbd\\xf4TS\\xbb\\x14\\x86\\xc29\\xbb6x<\\x16Q\\xf3\\xbc\\x7f\\x14B=<\\x9aR\\xbc\\xe7\\xee\\xd2\\xbb\\x8e\\x8a\\x92\\xbd\\xb5\\xbb\\xf8\\xbc\\xb0\\x17\\x91<\\xfe\\x88\\x9b\\xbc1;Z<\\x00\\x0e\\xe0<pgd\\xbc$\\xd5\\xd6\\xbc\\x89j\\x9d=\\x19\\xba\\x8b\\xbaO\\xf4L<\\x80\\xae\\xd1:A\\xda\\xca\\xbah[\\x18\\xbdAjQ=\\xa9!A;-US<\\x05M\\xe7\\xbb\\xf0O\\xa8<+\\xa6|\\xbc\\x8a~\\xa9<\\xd2\\xc9\\x85=sm\\x87\\xbb\\xd2c\\x0c=\\xb4\\xf6\\x1b<\\x1a\\x932\\xbc\\xd7\\x014\\xbd\\xd9\\x11\\xb7\\xbc\\xbc\\xd7==\\x1c\\x0e\\x1e\\xbdr\\xf7<=\\xf0\\x83\\x9a\\xb98w\\xf4<w\\x02\\x02\\xbd\\xe9]4\\xbd\\xe5\\x9e\\x99\\xbc~+\\xbc;\\xfe\\xbd\\xe9\\xbc\\xa2\\xba\\xb0\\xbd\\xe76\\x18\\xbd\\xd2&;\\xbcb\\xae\\x95\\xbci\\x95\\xb1;\\xd4\\xe9\\xa0<A\\xe6\\xf4\\xbc^3\\x04<\\x83\\x96\\x95\\xbdS2\\xf9\\xba\\xae1\\x12\\xbcS\\xf8\\xae<f\\x847\\xbd*\\xab\\xc5\\xbc9a\\xac\\xbc\\x19M\\x87\\xbc\\xf4+0\\xbd\\xa6k\\\"=\\xfc\\xa4\\xe8\\xbd\\xf8\\xe5w<\\xf31q;E\\xb9u\\xbd\\xc0\\x07\\xe7:\\xc8^w\\xbc^-\\x11<\\xd4\\xf4B=\\xbaF\\x85\\xbb\\x18\\xb2\\xa6=;xO\\xbd\\x9f\\x0b\\xc9\\xbc\\x0ft\\xdd;\\xf3\\xf7\\x88\\xbc\\xba\\xb6I=o\\x1f\\xee;9\\xa2\\\\;&\\x0e\\xa9<\\xdd\\x01\\xe2<\\x9bW\\xe8\\xbc|\\xe9\\xf0<\\xb9\\x84\\x19=\\x1f\\xd7\\x99\\xbc\\xe1f\\\"=\\x15v\\x85<\\x82Yz\\xbc\\x872\\xb7<\\x86&\\xb0\\xbc\\xfe`\\xbc<\\xaby\\x90\\xbdrg?\\xbaN\\xfa\\x95:\\xbeG\\x02\\xbd>hR;\\x14E\\xb3=\\xfb\\xb5\\\"\\xbc\\x08Ca=\\xfbe\\x82\\xbc\\x96\\x97\\xb6;\\xcd\\xeb\\x81\\xbd\\xa9qZ=C\\x86\\xca;\\x81+\\x1f\\xbc<)>\\xbd<\\x1eu\\xbc\\xbc\\t(\\xbd\\xf0`\\xd6=\\xa4@\\xaa\\xbc\\x08\\xeb\\xc4\\xbc\\xd7\\x91N<\\xf7~\\xc4\\xbb\\xef<5=C\\xfe0<\\xdd6Q\\xbd,G\\x0c<\\xea\\x04!\\xbd\\xe0\\x1b\\x99\\xba\\xc5\\x94\\x13\\xbdR\\x00\\x9f<\\x9d\\x07]<t\\x9d\\xc3\\xbdWp\\x1e\\xbdD\\x0e\\x00=[\\xa6!\\xbd\\x92\\xbb\\x03<\\x03\\xd4\\x95<\\x05\\\".=\\xa4\\xa3\\x19\\xbd\\xd7A\\xbb<\\xf6/\\xcf\\xbd)v[<\\xcck\\x14\\xbc\\xb7\\xde\\x05\\xbds\\xca\\xf5<\\xc0\\xb8\\xfa\\xbcw\\xb6h\\xbc\\t\\x0b\\xdd\\xbb#\\xd4\\xf7<\\xaf\\xfb\\x9d<\\xb3AE=\\x1a\\x18\\x7f\\xbd\\xaeT\\xf8\\xbbh-\\xc8<\\xc9C\\n=@/0\\xbdFF\\x98<\\xdd+\\x1b=\\xac\\xcb\\xaf<\\xc7\\xc3\\x0e<[\\xd1\\x18=\\x03\\xd4\\xb2<oB\\x0f\\xbdh\\x8cy\\xbd\\xb6%\\r\\xbcX\\xfbG\\xbd\\x04O\\x0f</\\x92F\\xbd\\x06\\xfe\\x88<gf\\xa3=>@\\x0b=;\\x18e<\\x919\\x08\\xbc7\\xdc\\x87\\xbdRm\\xfd\\xbaj\\x0f\\xc8<<c*=\\xa5\\xd5\\xa3\\xbcak\\xb4=\\xd3\\x1af\\xbd\\xa2\\\"\\xe4\\xbcc\\xcc\\x18\\xbdaC\\x16=\\xb9\\xb9\\xb2\\xbdv\\x9e\\xae\\xbbB)U<\\x1f\\x85t<\\x12f\\xb1<V0q\\xbdl\\xf4\\xa2;\\xdee\\xf4<\\xedi\\x94\\xbckw\\xbd\\xbc\\xf1\\xce\\x19\\xbdg<f=n`\\xf6\\xbb\\x16\\xbd\\x83<P\\xeb\\xf3<y\\xf9s=YE\\x00<\\x93\\xf3\\x87=-W\\n=M\\xd3\\x0b<D\\xb0\\xe6\\xbc\\xf6\\xe9\\x9d=\\x92\\x0b\\xc3<\\xabbN\\xbd\\xbbAw<M_\\x8e<.b\\x15\\xbd&v:\\xbc\\xd9\\xbf\\xbd\\xbdA@S\\xbb\\xb6z\\n\\xbc\\xe0=?<\\xdfF\\\"=\\x03\\xe7\\xe1<y\\x1f{;\\x19\\xef\\x12\\xbd\\x15\\x08\\x12\\xbb\\xac\\x1b\\xf1\\xbc)\\x8c\\xc0\\xbc\\xa5\\xbe\\x83=\\\"\\xb9\\x10\\xbc\\xfd\\x1fA\\xbcu\\x16\\xa3\\xbc\\xbc\\x91\\x1f\\xbc\\xaf\\xa5\\x9e<W\\r\\xe1\\xbc}\\x1c\\xdf\\xbc?\\xfb\\xe7\\xbc\\xb4@\\x1c\\xbch\\x01\\x1e\\xbd\\x1b\\x85:\\xbc\\xae\\xba\\n\\xbd0>\\xf3\\xbbB}\\xa7\\xbdD\\xeb\\x85<n\\x1f\\x0f<bw\\xd6\\xbb\\x87a\\xd6:\\x04\\xc6\\x95\\xbcA*V\\xbd\\x9e\\xf9\\x82=\\x17\\x07\\xf6=x\\x17\\xc6\\xbc\\x7f\\xb8\\x88;\\x1dK\\xaa<\\xc6\\x8d\\x85\\xbd\\xed9><\\xbb\\xdb\\x1d=\\x12?X\\xbd\\xed\\x8fB\\xbb\\x8e\\xe7\\x00\\xbda\\x11\\x82=s\\x01d=\\xbd{(\\xbcS\\xb4\\x02\\xbd]\\x8a?<\\x7f\\xad_\\xbb$(\\x10;\\xda\\xa5\\x11=\\xd0p\\xec<\\x18\\xb7\\xf3<\\xd7\\x9b\\xb5<\\x91X\\x03=\\xf2\\x05\\x8b<\\x8a\\xdd\\xc0<\\x99\\x9cI=\\xfeB\\xaa\\xbd\\xe1u\\x99\\xbb\\x7f2\\\"=\\xf4\\xa0\\x04=\\xab\\xdaM\\xbc\\xef\\xa8D=.C%=\\xcb\\x80\\x8e<\\xad\\xc8\\xfb\\xbb5\\xd3\\x0b\\xbd\\xfc\\x9e2=\\xed\\xab\\x82<\\x04\\\":\\xbc\\xea\\xf6\\xe6;\\x85\\xb3\\x96\\xbc\\xf1\\xe4\\xb5\\xbc@\\x8c~\\xbdE\\xf2\\x07\\xbc\\x00\\x8d\\x91=f\\\\^=\\xaf\\xea\\xd3<\\xc4\\xc0\\xa5:\\x8563<t%\\x82;\\xe7|\\xc8\\xbcu\\x7fM<; \\n\\xbdp3\\xda<%\\x8bV<o\\x05\\xcf<p\\x1bF\\xbc\\r\\xe79\\xbd\\xf5\\xa6\\xd6<\\x8b\\xef6\\xbd\\xcd\\x02h\\xbd\\x1f\\xbf\\xf8<c\\x86\\xa3\\xbd\\xbf\\xad\\x06\\xbc\\xfcG\\x99\\xbc\\xc7[\\xf0:)!\\x0c=\\xa0\\x178\\xbc\\\"LF=\\xff&\\x0e=\\xf0/\\xd1;\\xbc\\xc5\\x85=;n\\\"\\xbdS\\xc4V\\xba\\x8b\\x1ax<C\\xc8W;\\xd4\\xa1\\x1d\\xbd\\xdaK\\xb9\\xbbn\\x18\\x16=\\x03\\xf5\\x0f\\xbb[\\xaa\\r<\\x11zY\\xbc\\xc67\\xf3<\\xba\\xecO\\xbb\\x95:\\x19=\\xc8\\x0c\\xa7\\xbc\\xc7\\xc0\\x89\\xbc\\xc5\\\\G;\\xe9\\x10\\xba;\\xec\\x96\\xa7<\\xe4\\xa8\\xaf<\\xc6\\x03\\xf6\\xbc\\x13\\xa9\\xaf<_E\\x18\\xbcy\\xec\\x9a\\xbdt\\x00\\xbb\\xbc,\\x84\\x8b\\xbbF+\\xe5\\xbbps\\xec<]\\xb6\\xe9\\xbc\\xc3\\x8e\\xdb<\\xf5\\x0e\\xb3=\\x07\\xff1\\xbc\\r\\xcaC\\t\\xc7\\xfc\\x0b<\\x0fUc\\xbd\\xbe\\xbd\\xda<\\xc2\\xcd\\xb7<\\xe9\\x1f\\xcf<\\x88\\xe1X=\\x8cC\\xab;{\\x9f\\xa9\\xbb\\xa3\\xceA<>\\xf9b\\xbc\\xd8\\xf3\\xb1;\\nye=TnP\\xbc\\xc3\\x9dY=\\xc9\\x914\\xbc0\\x05\\t=1\\xae\\x81\\xbd~\\xdfq\\xbcJ2\\x8b\\xbb\\x1a\\x18/=\\xf9\\xd6\\x80;C\\xbe\\xb8\\xbc)>\\x01\\xbc\\x02g{\\xbbY+\\x07<\\xd3$\\xc5\\xbc\\x91\\xb4X<\\xc3k\\xf6<Y\\x08\\xe3\\xbb\\x90\\xbb\\xb8\\xba\\x13\\xfc\\x1d\\xba\\xbe\\xa6R<\\xf7?O\\xbd\\x02\\xb3\\x9b\\xbd\\x96o\\x11\\xbd\\n\\x8d\\x98<7|\\xf1<Z\\x0b\\x14=\\n:\\x80=]\\x91\\xab;\\x88\\xf7\\x0b\\xbc\\xfc\\xb9$=\\x92\\x1d_;\\xbc\\xcep\\xba\\x95(-=\\xe0\\x19\\n\\xbb&\\xb2\\xfd=\\xbc6$\\xbc0\\xea\\xa1\\xbc\\x80\\x8a\\xcb\\xbc\\xb3\\x9a\\x89\\xbd\\x0c\\xf0\\xb3\\xbd\\xad\\x1a\\x08=[\\x03\\xf78\\xd1*E<\\xad\\x17~<\\x02^e\\xbc\\xe4\\x0b.<\\x1d\\xc0\\x0f=\\x8d\\x8a1=\\xfa^\\xdc<\\xcf\\x9c\\x94\\xbc\\xc5\\xbc7\\xbbAqo<\\x115\\x01\\xbd\\xb2FK\\xbdDsD<\\xd5\\xc3=\\xbaO\\x1d3<\\x00)\\xa9<\\xc7\\x99\\xe0\\xbb\\xddU\\x18\\xbd\\xb8 \\x84<\\xab\\xd7\\x19=\\xce|\\xe5<m\\x96N\\xbd\\x17I\\x1c\\xbc\\xda1?=\\x86\\xfbC\\xbd\\xf3\\xb1\\xec<\\xdc9Q\\xbd$\\xc7\\xf6\\xbaEt\\x00\\xbc[\\xbeL=n\\x1a\\x9c\\xbd\\xfb\\x90\\x92<\\xd7\\xe9\\x1a=;}\\x8e=\\xa9\\xdb\\x10\\xbc\\x0e\\x8f\\xda;\\xd5\\xac\\xc1\\xba\\xab\\xa5\\x92<\\xe1\\x1c\\x91=\\xc1x>\\xbaI4\\x83=rH\\n=?\\xd6:\\xbd\\x88\\nd\\xbb\\xf4*\\xd2\\xbb\\xaf\\xff\\x0f=P\\x14S\\xbd\\rl\\xd2;\\xe9up\\xbc\\x86\\xf8>\\xbd\\x16,\\xef<\\x91\\x17\\xc7\\xbc\\xbb\\n\\xa4\\xbc\\x8b\\x8c\\xaa\\xbc`\\x13\\x15\\xbd|\\xfb\\x10\\xbdH<%=>\\x01\\x81\\xbd+y\\xac\\xbc\\xbf\\xae\\x15=\\x1c%e=\\xe1\\x9d\\xfc:\\x05\\xd6\\x8d<\\xf0\\xa8d;\\xa9\\x12\\x9e;%p^\\xbd\\xbe\\n\\x04=-jA\\xbcqG\\\\=\\xbc9\\xed;^\\x95\\xd6<E\\xccr<+\\xa7:\\xbd\\xf3\\x90\\x10\\xbb\\x98\\x8a\\x8f=y|\\\"\\xbdC\\xee\\x05\\xbd\\x12\\xe6E\\xbdg\\x9d\\x8c=g\\x83(<YQ\\xc2=\\xb4a\\xb2\\xbc\\x1c\\x1e\\x96=\\xfdW\\\"\\xbc\\x97[\\x9d<|\\xcf\\xc0\\xbb\\xb9\\xe3\\x02=\\xff\\x15\\x94\\xbd\\xe6%\\xa5\\xbcw\\x98\\x9d<+\\xfd\\xc2<\\xd2\\xcd\\x85\\xbc~\\x08\\x8b<\\xe9\\xc7a;\\xa2\\xc7\\x82<cz(\\xbc7b*\\xbd\\xf1!l\\xbc*e\\xa6<\\x93\\xdb\\x11=.aY<\\xae8\\xc2<Y^\\x86\\xbd\\xdc\\x92\\x85\\xba\\xb4|S\\xbd\\xbc\\xb7\\x80;V\\xb5\\x86\\xbb\\x1ea\\x1b=p\\x94\\xac<\\xb5\\xa4o\\xbc\\xd1\\xf1&\\xbd!\\x11\\x8e\\xbc=\\x063\\xbd\\xc8\\x84\\xc7<L\\xb9\\x8e=n\\xdc\\x7f\\xbd2\\xa3\\xad;;1a\\xbc\\xec\\xa3\\x10\\xbb\\xbf\\x0b%\\xbd)$\\xbb\\xbb\\xb9\\xdc\\xc7=\\xf5\\x01\\x02\\xbc|*\\xbd\\xbc\\x18g\\xef<\\xf7\\xe7s\\xbc\\xf2p0<\\x04\\n\\\"\\xbd\\\"\\xe6\\x1d<\\xdff\\x1b\\xbd8W\\x1d\\xbdi\\xed\\x1f\\xbc\\xd4s\\xd7\\xbc@\\xbd-\\xbd\\x98\\x15@=\\x0e\\xf5\\x11\\xbd6a\\x0f\\xbc\\x99\\x1eE=\\xe0v\\xa2\\xbc.\\xff\\xca\\xbc\\x1f\\x99I\\xbd\\x88\\x8f\\xcb\\xbc\\xfa\\xdc\\x95\\xbdPs\\xdb\\xbc\\xc1\\xd9\\xe4;Ks\\x19\\xbd\\\"\\xa3,\\xbd\\xed\\xb9[\\xbc\\x92\\x1c\\xc8\\xbc8\\t\\xad\\xbdJ\\x893\\xbd\\xd4\\xa1\\xd3;h\\xdf\\x10\\xb9\\x0f\\xca}\\xbc\\x18\\x9f\\x1d\\xbd\\xe9\\xb7{\\xbd\\xab\\x8b\\x9a<$\\x88\\x9e\\xbc$\\xe3\\xed<\\xa5\\xbc\\xeb\\xbb\\xc1h\\xac\\xbc\\x17(\\x82\\xbc\\xd4\\x9d}<\\xa8N\\xfb\\xbc\\xd0\\x07\\x84\\xbdz\\xa6e\\xbdm\\xb3N\\xbd\\x1b\\xcf\\x81\\xb9\\xefYX=\\x89\\x83V<\\xff\\xac\\x9b=\\x0f?\\xe8\\xbc\\\"\\xd2L=\\xb9\\x99\\xaa\\xbc\\x82\\n\\xb8\\xbdd\\xa8\\x07=\\xbe\\xf3\\xf5\\xbaE\\x0fe=\\x18\\xc6\\x1c\\xbc\\xfc\\xa1\\xa2\\xbcXk\\x00;\\xa1R\\x05\\xbd\\xdaW\\xdf\\xbc\\x8a@\\xcc\\xb9Xd \\xbc\\xcf\\x0ei\\xbc\\x1b\\x93\\xe9<\\xf2i\\\"=\\x18!\\x8c<*\\xa7\\xf5<\\x14-l\\xbc\\xbeR?;\\xd7\\xc4\\xad\\xbc\\\\x\\\"<\\x94T\\xea\\xbcg\\x03\\x84\\xbb\\x89\\xad\\x8d=\\xff\\x89\\x01<\\x89\\xdaf;\\xcay\\x07\\xbc=bn;c\\x99;=0\\xdeb\\xbc\\xe6\\x032\\xbag\\x1b\\x0c\\xbd{\\xe8\\x95\\xbd\\xfd\\xceM=\\x14 a=\\xd2\\xa0\\xa0\\xbc^\\xa9y=\\x7f\\xe6\\xa9=\\n\\x83\\x1e\\xbci7\\x03=}\\xf2\\x8f=\\xb7`\\x08\\xbd\\x0c}\\x90=\\xcc\\xf7\\x9b\\xbd\\xa7\\xd91=I\\xe1\\xa3<\"\nHSET bikes:10038  model 'Hyperion' brand 'Nord' price 4632 type 'Mountain bikes' material 'full-carbon' weight 7.1 description 'This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\x94\\x10\\xdd\\xbb\\x15\\xca\\xe3<n\\xe5\\xd1\\xbb\\x17xj=\\x08\\x85\\x19<\\xef\\xe3\\x1e; _\\xc1\\xbcq\\xc8q\\xbd\\xd2\\x9e\\x91<\\x7f\\x81\\x05\\xbdC\\xe4\\xd4<\\xf9T\\n\\xbd~\\x9ad=\\\"\\xc8D=&c\\x8e=V\\xc0\\x8d\\xbc<}\\x8e<\\x17\\x98@\\xbd\\x10\\x15\\r<\\xe2\\x03\\x8e\\xbdA\\xf6\\xb7\\xbc\\xc7\\x1f\\xb7\\xbcP\\xd5I<5j\\xdc\\xbc\\x9f\\xc7\\xb0=g\\xa1\\xfa<&W\\xc9<\\x80\\\"\\\"\\xbcYsw\\xbd\\xd7\\xc8g;\\xc5}\\x15\\xbd\\x13+D\\xbb\\x8f\\xec=\\xbc\\xb0J\\xae<j]\\x0c=PQ\\\"\\xbc\\xa6\\xb9\\xd8\\xbb\\xf6\\xa1\\xe2\\xbc\\xcfJl\\xbd\\x0f\\x88\\xfd\\xbbL\\x8c\\xb7=[F\\x93\\xbc\\x03\\x1cC<A\\xdb\\x0f=\\x83\\xa6=\\xbc\\xd52\\xac\\xbc\\xb9\\xa3\\xb9=\\x1d\\x93\\xa2< @\\xef\\xbc\\xce\\xa7t<\\xcco\\x9b\\xbb\\xaeM\\xdf\\xbc\\xd1C\\xdc\\xbc\\xfbr\\xd0;\\x12a\\xa3<2j\\xa6\\xbck\\xa6\\x91:u\\x00\\xbd\\xbd,\\xf3o\\xbd\\xee\\x05\\xb3<oX\\x8a\\xbc\\x93x-<\\x94\\xa1\\xa3;\\xed6w=T\\xb0I=\\xfe|\\x05=\\x86\\xdf\\x90\\xbby\\x8b\\xe0<\\x05\\x85\\x99<\\t1l\\xbcNo\\x01\\xbdo\\x14D\\xbd\\xd2\\\\\\x91\\xbc\\x85c\\xae\\xbb\\xe6\\xff&\\xbd\\x8f\\x16L\\xbb\\xb3\\xfa\\x02<C\\x02\\xc9\\xbc\\xadW\\xcb\\xbd6\\xb3\\x19\\xbd\\x1b\\xc56\\xbb,B\\x92\\xbcu\\x14\\xd0<.\\x81\\x80\\xbd<p\\x98<\\x8f\\xc5\\xa3<zN\\x98<4\\xdbQ;p_\\x17=\\x8a\\x12!<\\xb9.-\\xbc\\x9e)\\xa4\\xbc\\x9dbN\\xbc)!\\x15\\xbc=\\xdf\\x1c;M\\x14\\xd9<\\xce\\xac\\xd0\\xbc\\xea[<\\xbd\\xc1_\\x85\\xbb\\xe68\\x05\\xbd\\x9aj\\x00=\\xd7\\xce#=\\x82\\xc5\\x86\\xbb:~\\x01\\xbd\\xfa\\x81\\x16\\xbd/\\xc4\\xe9=\\xa61\\xa5<\\xcc\\xd1\\xf2<R\\x89\\xa4<\\x12U\\xb3<\\xfb\\xf8\\x11\\xbdg\\x1aJ=s\\xcd\\x1b<\\xa5\\x12.\\xbd|\\n\\x00:1\\xf4\\xf6\\xbc\\xa2I\\x07\\xbd\\xc1*@;\\xb9\\x93\\x8f<w\\x99\\xad8L\\x12\\xc8\\xba-\\xe9\\xcb<\\x17dI=\\xd6\\x13B\\xbd\\x9c\\x07\\xff<A`?\\xbc8p\\xa5<\\x94\\x84\\x8f\\xbd\\x85\\xff\\xf6\\xbc\\x00A\\x97\\xbd\\xdd\\x89-<\\xe4B\\xad\\xbcK\\x02\\xab<\\xd5R\\x8e\\xbd\\xaeN\\x03<\\xe7+#=B\\xf4\\xe4\\xbb\\x7f\\xc9\\xe1\\xbc_eS=\\xa0\\xa4\\xc3\\xbc\\x9c\\xdd\\x12\\xbd;\\xf1\\x85\\xbd\\x91\\xc0\\xb0<y\\x95\\x81<\\xc4A\\x15\\xbdd\\xfb\\x9d\\xbc\\xc8t\\x06=C\\x08G<\\x16\\x1c\\xc9\\xbc\\xbf\\xad[\\xbd;\\xeb\\x82\\xbc\\x89?\\x94=\\xbaB~\\xbc\\x82\\x04\\xa9<\\xc3\\\"\\xbc\\xbc\\xb7\\x17,\\xbb\\x99\\x95K\\xbd\\x19\\xcb\\x05\\xbc\\xd8~\\xbc\\xbc\\x04((\\xbcR\\\"\\x85=\\xed\\xac7\\xbd#\\xcb*\\xbc\\xcdh\\x83\\xbd;\\x0cO<^\\xd4\\x87<\\x07\\xa2z\\xbcZ\\x1d\\x9a=\\x12h\\xc3<\\xe1\\xb6a<+\\xa7\\x8c:2P4=I\\xb1I\\xbcd\\x94\\xd6\\xbc\\xb6\\xb4\\xe4\\xbc\\x99\\x84d<\\x0b;^\\xbd\\t\\x91:=&6u<\\xfd\\xc4\\xff\\xbc\\xcfyM=i.\\xca<\\x05\\xb5\\x02\\xbd\\x04\\x12\\xe2<+\\xf2\\xda\\xb9\\xf1\\xdcc\\xbc\\xd3\\xb2\\x93=\\xf0\\xe3V\\xbc\\x94\\x88\\xb1<\\xe6\\xee\\x00\\xbdk\\xe6\\xfd\\xbc*vC;\\xd6\\x1fc\\xbc\\xe3g\\x1e\\xbc<UB\\xbc[\\x9f\\xfd<\\xa7\\x9aH\\xbb?\\xc4\\x82\\xbdE\\x95 =\\xbbz\\x9b=\\xc5\\x12\\x84\\xbc\\xa6^6\\xbd\\x1a\\xf7\\xc8\\xbd\\xbf`;\\xbc\\x1dP\\r\\xbd\\xd2\\xc7&=\\xc2\\xfc\\x1e\\xbd\\xdf\\xb5i\\xbc;\\x17M=\\x98\\xcbk\\xbc\\x05\\xff#\\xbcl\\x13\\xf59\\xf7\\xe6\\xfe;\\x9a\\x0e\\xf5\\xbb\\x9f7S<\\xe2\\x18\\x19\\xbd\\\\\\x82\\x1a=b0f\\xbbB\\x07X=\\x05\\x8eB\\xbc\\xb8h\\x13=\\xcaLS\\xbc^\\xf4\\xf8\\xbc\\xd5G\\xda<\\xa3!\\x1c\\xbc\\xe9Q\\r\\xbd\\xda:q<\\xef\\x99\\xd7\\xbaw\\xe44=5\\xf3_\\xbd\\x9e\\xe8N\\xbcD\\xa6\\x18\\xbda\\x05E=\\xae\\xd1p=\\xeb\\xdbE<\\x14#L=\\xe9`4=\\\\\\xa77=X\\x9a\\xe0\\xbc\\xe4\\x83/<\\xba\\xb6v;;\\xfe_;\\x98\\xd0\\xf7<\\xcem\\xae<\\xa6\\x8a\\x86\\xbb\\x14\\x15\\x89=\\xads\\xc6<\\x94\\xcf\\x00\\xbd?\\x84\\x06\\xbd\\x80\\x0c\\x1f:\\xe7\\xf3\\xb8<\\xccd\\x89\\xbd\\xc0\\\\B\\xba \\x1b\\xab=\\xbc\\x08\\xaf\\xbc\\x80<z=\\x01\\xe1\\x0f\\xbd\\xc4\\xc5\\xb3\\xbc\\xd9\\x91\\xa6<B\\xc0 =Ny\\x8a<V\\x8f\\xa9\\xbcV]\\n\\xbd\\x96f\\x15\\xbc\\xfc\\x86(\\xbc\\x853\\xaf=\\x84\\xc9?\\xbc\\xf6\\xdb\\xc9\\xbc\\xa1\\x17r=\\xac]\\xf3\\xbc\\xc35\\xbb\\xbc\\x03\\x12\\xdc<\\x03\\x05q\\xbdHv\\x1b=q-\\x14\\xbd\\xd5\\xbc.\\xb9p\\xe5!\\xbc\\x08\\x14\\xb3\\xbc\\xee\\xf2\\x1f=\\xe9\\xa1?\\xbdv}\\xb1\\xbc\\x0c\\xb5\\x86=\\xb90\\x89\\xbd*\\xb25=\\xf0\\t\\xd1<\\x85\\x03\\x98\\xbc\\xa4\\xed\\x1b\\xbc\\xa3U\\x06\\xbc\\x9b\\xa2\\xa2\\xbd\\x19k\\xb8\\xbc\\x19\\x8cP\\xbc\\xfa\\xfb\\x8c\\xbd\\xd8\\xa3\\xe3=\\xe2\\x9c\\x08=\\xa0a\\xc8<\\x8e\\x89\\xb3\\xbc\\xaae\\x96<4\\x950=\\xce\\xfaF=\\xf93 \\xbd\\x1a}\\x9d=\\xcd\\xe6\\x8b\\xbc\\x12\\xb3\\x0e=\\x18r2;\\x84P\\x97<z\\xb6\\xcb\\xbb\\xadN\\xd9<\\xbfa\\xe9<Zfu=\\xd5>\\xa5\\xbb\\xb3rd\\xbd4\\xe1\\xf9\\xbb`v\\xb0\\xbc\\xa4\\xd0\\x86\\xbcVd)<\\x81\\xce \\xbdK\\xac_\\xbd\\x0eS\\xde=\\xde\\x99=\\xbb\\xda\\xee\\xcd\\xbc\\xe5B9\\xbd\\xb9U\\x02\\xbd\\xb7)\\xe8;3N{;\\xcc5B=\\xaf\\xd1c\\xbdZ\\xe3<\\xbdj\\xa9\\xa2\\xbd`\\xb2y=\\xfe\\xb4\\\"\\xbc\\xa1x\\xc0<.\\xdd.\\xbd\\x05:\\x99\\xbd\\xfb\\x12C\\xbc\\xa1\\x8eM\\xbb|\\xd2\\xcb\\xbc\\x7f\\xe0\\xbd\\xbc\\x16\\xb12=p\\x11\\xd8<\\x9aP\\x95\\xbc\\xfb\\x87\\xeb\\xbbg\\xe7\\x0e\\xbd\\xe0\\xd5\\x8d=\\xfe\\xf6)\\xbc\\x02:?\\xbcH\\xf5\\x04=cIb<\\x8d\\xee\\x01=/\\xa9\\x07=\\xceoN\\xbd\\x14B\\r\\xbd<\\xd7\\xf7\\xbc29\\x19=\\x7f\\xe6\\x17\\xbd\\x92\\xfa\\xdf\\xbb\\xa7u\\x81\\xbdp\\xf8\\x00\\xbc\\xd5v\\xb3<(\\xe0\\x9f\\xbd\\\"\\xc5\\xc7\\xbd\\xb2S\\x85\\xbc\\x1c\\x0f\\x1b=\\xf8\\xd4\\x1d\\xbd\\xe9\\\\s=>\\x02\\x16<e;P=G\\x86=<\\xcbSi=\\x10\\xe05\\xbd\\xcba#\\xbd8H\\xc2<c\\xe1J=\\x9c\\x02\\x0c=\\xe6\\xfc\\xbd\\xbd\\xbc\\x0c!=\\xb3D\\xb9<\\xacm/\\xbcv\\x18J\\xbd\\x82!H<b\\t\\x86<\\xc2G\\\"\\xbdh\\\\\\xe3\\xbc\\xd8m\\xd9\\xbc\\xcd\\xfc\\x87::l\\x95\\xbd\\xec\\xf92\\xb9\\xd212\\xbc\\xcbwX\\xbc\\x9be\\xf6<2\\xf0y\\xbb\\xab\\x80Y\\xbd\\xc0\\x7f\\x02=\\xc7\\xdf\\xac=\\x9d\\xc1\\x8c;U\\t\\x1a\\xbc\\x86}\\xbc<w)\\xcc\\xbd\\xf7\\xb0O=\\xb7\\x90\\x08=\\x84*\\xa0\\xbd\\xef1\\xda<h\\xfd\\x05\\xbd\\\\g\\xfc<\\x81&B=\\xfeqx\\xbc%(&\\xbb\\xa9\\\"F=A\\x16\\xa0;\\x08\\xad\\xfc;\\x99\\xbf\\xc9<\\xda\\xf79=)\\xc6\\x91\\xbc7\\x9f\\xe7<\\xa4\\xbfg<\\x0c\\xa4\\xef<6m<=\\xa1\\xca\\x9c\\xbd\\x9a\\xe2\\x99\\xbd\\\"\\xd7\\\"=\\xa8\\x04\\xed;\\x8b\\xa8\\xcc\\xbbC\\x99^;\\xb4?\\xa0\\xbcT\\x81\\x18=\\xaf\\xb5\\\\\\xba\\t\\xb3\\x90;5\\xd1\\x84\\xbc\\xde8T=s\\x83f\\xbd`\\xa7\\x05\\xbd\\xd7\\x9f\\x0e;\\xca7\\xa9\\xbc\\xe6+\\x82<b&|\\xba\\x0f\\xac\\x1c\\xbd?xl=R\\xb5\\x11=Z`\\x1a=?Z\\xb5;\\x81\\xb8\\r=\\x82&\\xd4<d\\xe8\\xb9\\xbb\\x08\\xa5,\\xbc&`-<\\x86\\x8d\\xae\\xbb]\\x9d\\x97<b\\x86]\\xbd\\x9b\\x88?\\xbb[aV<=\\x14\\x02=9\\x05i\\xbd\\xfa_y\\xbdZ\\xfc\\xe2\\xbb\\xee\\xce-\\xbd|\\x0b\\xff<\\xb2]H\\xbcs\\xcb&<\\x8e$\\xf1\\xbbA\\x15:\\xbc\\xb5\\x88\\xbc=Y\\x9e<=\\xf0M\\xd2;\\xac\\xe0c=\\xdc^D\\xbc;\\xb25;\\n\\xe7\\x1a=\\xd9\\x00\\x84;&yb\\xbd)=\\xcd<&\\xe8\\x0f\\xbdW\\xcc\\xae\\xbc\\xe9\\r\\x03=b\\x01\\x1b\\xbd\\xf1\\xdc\\xfc\\xbc\\t\\x0c\\x10:\\x10!\\x8c\\xba\\x0e/\\xa3\\xbc!\\xc7\\xe0\\xb9}\\xf9\\x8d:\\xe1\\x9fa\\xbc\\xf4\\x18\\x1b<Qs\\x00=2\\xc9\\x1c\\xbd\\xe8\\x8d#=\\xd2\\xbd\\xc1;\\xf9\\x82\\xc8\\xbd\\x17\\x93\\x80\\xbc\\xde\\x05\\xf8\\xbbd\\xc8\\xb6\\xbc2\\x8f\\xd4<B\\xae\\xe0<C\\xbc\\x1f:\\xd1\\xb7\\xfe<y\\x0f\\x0b\\xbd\\xee\\xb8_\\tb\\xe1\\x17<\\xc1@.\\xbb\\xb7\\x08\\xed<\\r\\xa4\\xdb<6E\\n\\xbc=\\xb8e<\\x16\\xc9#\\xbdV\\x95\\x97\\xbcQ\\xe5N\\xbb\\xe9J\\xc0\\xbc\\x06\\xa2\\x8a<\\xf5N\\x89\\xb7\\xcf\\xd8\\xdc\\xba\\xae\\xb2[=:\\x92k\\xbc\\xef\\x82\\xb1<J\\x1d\\x08\\xbd\\xde9\\xf5<\\x94\\x83U\\xbc\\xa3\\x1a\\xb4<\\xc8O\\xb2<N\\xa5i\\xbc\\xcbV\\xd5\\xbbo\\x8d\\\"\\xbdz\\xad\\xbb<\\x10F\\xc4\\xbb\\x9aX\\xfd\\xbb\\xd8\\x1f\\x18<\\xf3\\xcc2\\xbc[\\x93*<\\xb7=\\xc8<J\\xf6R\\xbd\\x93\\xfbZ\\xbd<c\\xab<%D\\xc7\\xba\\xb5\\x91\\xc7<N\\x9d\\xc5<q\\x1f\\xa2<\\xca\\x84\\\"=\\x95\\xfd\\x83\\xb9]\\xd1M\\xb8\\xba\\xccY<{o\\xed\\xbc\\xc5\\xc8\\xbc9\\x8eym=\\xea\\xdf\\xcc;\\xddu\\n=B/\\xac9\\xdf\\t\\xaa\\xba5\\xab\\x9a\\xbc\\xcdy>\\xbd\\x83;&\\xbd\\xf1\\x01\\xf7<:\\x08\\xad<1\\x8b2=\\x85\\x17\\xfd\\xbaS\\x19\\xa4<\\xb4\\x9c\\x84<\\xa0v\\xb0\\xbc7Q\\xc0<!\\xe7\\xaf<\\xf1\\x8a\\xda<\\xccH-<\\x0ei\\xd8<\\x7fh&\\xbdp1/\\xbdz\\x13\\x06\\xbd{$4\\xbb{\\x10\\x85=\\x00x[\\xbd!\\x10\\xe6<\\xcf\\x07J\\xbc5\\xc7\\xbf<\\xf4\\xca\\x07=e\\x94\\xf4<\\x15\\x02w;\\xb6\\x93\\x05<\\xc0\\x96a=\\xa6\\xef<\\xbd\\t\\xc7\\x0c<\\xaf\\x06\\xd4:;\\xa6\\x1b<d\\x10|\\xbc\\xfa\\xab\\xfe<T\\xaa\\x80\\xbcfT\\xc77\\xce\\x97N=\\xcf\\xbe\\x98=\\x1d\\x14\\x7f\\xbcn\\xc9\\xcc\\xbbW\\x04\\xd5\\xbc\\xd2#d<Q\\xe4\\xce<(\\xdft6\\xb6\\xf8\\xe8<\\xc4\\x8a\\xa9<\\xb1M\\\"\\xbd\\xc1o\\xa9;\\x9b\\xda\\x84\\xbbD\\xcb\\x9d<\\x12\\x02\\xfa\\xbc\\t\\xc9\\x10\\xbd\\x9b\\x90\\x83\\xbb\\xab\\x0bh\\xbd\\x8d\\xd3n=\\xb3\\xae\\xaf;\\xd6\\x19d=%\\x96\\x05;\\x11:D\\xbdD)!\\xbdU\\xe1\\x8b9\\xb7\\x05)\\xbd\\xef\\x9c\\xb9\\xbb\\t\\xfd\\xb2=\\x1e\\xd8\\x80=\\xcc\\xa2\\x8d<;9U=\\xd85\\x1f=\\xb7\\xfc\\xac\\xbb\\xf0:\\x94\\xbd\\xab\\xe4\\xdc<\\x9e\\xce\\x83\\xbc\\x12d\\xa6<g+\\x10:\\x7f#\\x06\\xbd\\xd6IO<IZM\\xbd\\xa1\\x02w<OU\\x81=\\xc9\\xba\\xc4\\xbd\\x1fe\\x80;\\x94\\xd1\\xf1\\xbc\\\"\\xa9Z=/su\\xbb\\x8d\\xc9\\x0e=p\\xc1\\\"=&6\\x01=\\x90BF\\xbdA\\xdf\\xb8<S\\xc1\\x88\\xbb\\xf8\\xe5\\x95\\xbc\\xbe7\\x15\\xbd~\\xcfk\\xbd\\xfaU-<\\xde\\x16\\x11;\\xef\\x87$\\xbb\\\"\\x8dn<\\x17\\xee\\xca\\xbc^} <U\\x96\\xf1;\\xbeF\\\"<\\xf1\\xc0\\x9d<_[\\xd0;\\xdbx\\xcf;\\xf6\\x1c0=\\x1a\\x96\\xb3<\\x19s\\\"\\xbc\\xfc3n=3\\x0f\\xb7<|a\\xa1\\xbc\\xaa\\xf3k\\xbd\\x9b\\x9bf=\\xe2=\\x01=R\\x1b\\xe3\\xbb&E\\x89\\xbc \\xc34\\xbbX\\xc6\\x9f\\xbd\\x9fd_:\\xe5 \\x97=\\xcf\\xca1=\\x12\\xafO=\\rZ\\x83\\xbc\\xb8q\\xef<b\\xf9\\t\\xbd\\xb1#\\x92\\xbb\\x8d\\x97\\r=\\x9c\\xb2\\\"=*t\\x1b=R\\x9c\\xb49\\xef\\xc0\\x89\\xbc7\\xf0+\\xbb\\xbf5\\xa1\\xbd\\xa5\\x11@=\\xe0\\xf8\\x81\\xbdVi(<\\x8d#\\xe7\\xbc<\\xa2&:\\xf0E\\xda\\xbc\\x80r\\xbe=t\\xba\\xe0\\xbct\\xd0T\\xbb\\x81i8<\\x04\\xae\\n\\xbdL\\xac\\x15\\xbd{A\\x82<\\x19\\xee\\x88<\\x1e\\x80\\x0f\\xbd\\n;\\x8a;\\xab[)<\\xd1s$\\xbc\\xda\\x00\\xfa\\xbc!\\xa3\\xe2<\\rj\\x0b\\xbd\\xd8\\x08i\\xbd\\x93\\xe1=\\xbd\\xac\\xd0\\x8a\\xbd\\\"\\x17\\x01=;0\\x06<\\xb6\\xe4\\xa4\\xbb\\xa4\\x0bY\\xbdd\\xd5\\xbc<Sd\\xbb;\\xbe\\x89,=\\x99\\x19\\x03\\xbd}\\xd5\\xac\\xbc!\\x11K\\xbd\\xdd\\xae\\xb6<\\x85G\\xb7\\xbc\\x13\\xee\\t\\xbd\\xb2]\\n\\xbd\\\"@\\x12<--<\\xbdh\\xf2\\x84=\\xbe\\x06)=\\xfd\\xee\\x99=\\x99bB\\xbc\\xd5\\xff\\x1d\\xbc\\xc3\\xb4\\xdf;\\xf4\\xc2\\xb2\\xbc\\\"\\x01L=?\\x9f{<\\xee)\\x08\\xbc\\xcf?K\\xbcYG\\x0e\\xbc\\xb9+R\\xbd\\x9c@\\xf1\\xbb\\xfb.\\xa9<\\xec\\xc9\\x06\\xbd\\x82\\xc6\\x93<\\x94G\\r=\\xcf\\x97m\\xbcb7p=\\xd7\\xea4<\\xd6\\x81\\xa1\\xbbL\\xb0\\x1e\\xbd>9\\x15=\\x02Vu\\xbd\\x9a\\xf9\\xaf;\\x9b\\xcf>\\xbdD\\xe0\\x98\\xbb\\xc5\\xa4\\x9f\\xbc\\x01}\\xb1\\xbc\\xab=\\x05\\xbc\\xf1$\\x05=\\x84\\xfd\\xbb\\xbc\\xd4{\\x89<,\\xc8\\xa5\\xbd\\x01\\x9d\\xcf<\\xecnf\\xbd}\\xd5\\xa2\\xbd\\x8c2\\x05<\\x8a\\x15b<\\xf0\\xd4{<+&}<\\xd5&h=\\xc4\\xae\\xbe\\xba9\\x06\\xd0;p\\r\\xf2<Z\\xc1\\x86\\xbd5\\xae\\xa2\\xbc\\xa2t[\\xbc5%\\x0c=\\xf9\\xed\\xd0\\xbc\"\nHSET bikes:10039  model 'Charon' brand 'Eva' price 2863 type 'Kids mountain bikes' material 'full-carbon' weight 12.8 description 'Small and powerful, this bike is the best ride for the smallest of tikes. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\xe0\\x1e\\xcc<\\x94{\\x11=J\\xbf\\xe7\\xbc\\xb8\\x80{=j\\xdc\\x06=C\\xfd\\x03=p9\\\"\\xbb4\\nl\\xbd\\x0ek\\x8d=\\xba\\xfe\\xb2:bW\\xd2\\xbcH\\xac\\xb5<\\xfa\\xef[=-%\\xd9;w\\xa4\\xab=\\x12\\x10\\xd5;[\\xfa|\\xbc;?\\x96<\\xed\\xdf)\\xbd[\\xfa\\x16\\xbd\\xd0\\xe4\\xf6<\\xc9y\\xb6\\xbc\\xd2G\\x13=\\xac\\x9a\\x02\\xbdx\\x17(\\xbb\\xc2\\xce\\x11<\\xdb\\x16\\xe1<\\x00`6\\xbb\\rt\\xc5:Z\\x83\\xff<\\xea\\x84\\x89\\xbc\\x13\\xa4 \\xbc\\x156\\xf3<e\\x1a\\xfa<\\xa38\\x83=\\xd4\\x98\\x11<:\\xce\\x94<\\xe0\\xa2\\xf8\\xbc\\xb3\\xd1\\xda:\\xffa\\xfd:\\xea\\x01\\xf4<\\xaaER=\\x91\\x15\\x1f=_\\x86\\x88<\\xdd \\x0b<\\xdf\\x0e\\xf5\\xbch\\x97\\xcb=\\x94>\\xba\\xbc\\x04\\xc0+\\xbd\\xc9w\\x83\\xbb]\\xafC\\xbdL\\xb7Q\\xbd{\\x0c\\xee;\\xe1p[<\\xba\\xab\\x9e<\\xc9\\xfa\\x87;(\\x85\\xdf\\xbb$\\\"q\\xbd\\xe2\\xbf%\\xbd\\xc1\\x95t=\\xe7\\xd91=<\\xaa\\xc9<\\xd0\\x82@<3x==\\x15P3=\\xf7\\x8d\\xba;\\xe8N\\t\\xbd\\x99\\xcc\\x1b=\\xf5\\xfb 9\\x9a\\xe8\\x00;\\xd1\\x88p\\xbc\\xde\\xfc\\x83;\\xa2\\xec\\x15\\xbd}\\xf5\\xb6:UE^\\xbc\\x82\\x18\\x80;\\xedQ\\x0c\\xbc\\xf8N\\xce\\xbc\\x17\\x91;\\xbd\\xf9}d\\xbc\\x00\\xd9\\xd1<q\\xbf\\xc8\\xbcuI\\xf9\\xbc\\xd7\\xff>\\xbb\\xf20c\\xbc7`\\x1a\\xbd\\x85\\xa94<F\\x99t=\\t\\xf7\\x1a=2\\xa4\\xbb<\\x82\\xf04<\\xa0\\xf3\\x14\\xbd\\x9e/\\x08\\xbd\\xf9\\x10\\x08\\xbdv\\x8d\\x9b<\\x15\\xdc\\xbc<\\xd3:M<\\xd78\\x80\\xbd\\x93m\\x9c<N\\xa6\\x83\\xbd\\xf5\\x95\\xcd<\\xb8\\x9e\\xd3<a\\xfa|9/Qb\\xbd\\xd5\\xdd6<\\xda\\xae_=a\\xe9\\xf2\\xbc\\x9d\\x8d&<A\\x12\\x83;5X\\xd4<\\x10\\xb2\\xf1<[\\xf9T=\\xa8\\xc5\\xe5<\\x93\\x1at\\xbdH\\x9e8\\xbd\\xb0\\xb2\\x00\\xbdC\\x160\\xbc_\\xd9\\x8d\\xbc\\x8f\\x9e-=\\xeb\\x07\\x0e\\xbc\\x92\\x01\\x00<\\x93\\xb0\\x04=\\xe6)\\x8a<y-h;\\xa3\\xf0\\x06=\\xb3\\n\\xd4\\xbcN\\x9f\\xae<\\xf0\\xe7y\\xbdM\\x08&\\xbd\\xe5\\xbd\\x83\\xbd\\xbf\\xa1\\xb3\\xbb\\x16\\x1aQ\\xbd9&q<\\xac*6\\xbc~\\x19\\xf1\\xbc\\xaa\\xa8\\xae<\\x9f*\\xb1\\xba~\\x83\\t<|\\x16\\x87\\xb9\\xf4\\x18v\\xbd\\x1e\\xfd}\\xbba(&\\xbd\\xa6\\xf0\\xd9<\\xf3\\xab\\x93=\\x08A\\xc1:M\\xd0\\x9c\\xba\\x8f\\xc5\\x98\\xbb\\x99\\xfe\\xe1\\xbc\\x9d\\x9e\\x93\\xbc\\xe6\\xfa\\xb2<g~5\\xbc\\xbcm]=l\\x03=\\xbdy5$=\\xa8\\xb7\\x8c\\xbd\\xfew:\\xbd\\xec\\x1f7\\xbb\\xae\\xf0\\x9c;7l\\x89;8*[\\xbd\\xe3\\x89\\x99=\\xd6\\x94y\\xbc\\x8c\\x05\\xd0\\xbc\\x88\\xa7I\\xbd\\xcbJ!<f\\xa3w;J\\xcaN\\xbc\\xac\\xe41<\\x9eX\\x7f=W\\x88s\\xbbr\\x8ft\\xbd\\xbf\\xe1\\xe8< \\xe1\\xba<\\xb9\\xad\\x15\\xbd\\x80\\x86[\\xbc\\x96\\x82A\\xbd\\xb1]d\\xbd\\xc1R\\x99=6\\x1e\\xf8;y\\xf5\\xab<cP$\\xbc\\xc1\\xd6\\xc0;;)G<`I(=\\x94\\x9c\\x0f=\\xf7\\xef\\xa7<(lg=\\x86\\xbf\\xbf;n\\xe5\\x0b;Fj\\xbf\\xbc\\x9bQ\\x8b<\\xe7\\xc2\\x1a<)3\\x10\\xbd\\xb8\\x81\\x1f=\\x99\\x99+<o\\x18\\x87=\\x1d+z\\xbd\\x89\\x9dy\\xbdd\\x13\\xc4\\xbd?\\x95\\x87=\\x1b\\xd3\\x9b\\xbc.8\\x9c\\xbd\\xff\\xa7\\xac\\xbd\\xfb\\xb7+\\xbc\\xa0m\\x08;\\xada\\xe4;_\\xc1\\xe9;8`/\\xbc\\xd2xv<\\xf3\\x15+\\xbc\\xac\\xc3\\xa4\\xbc\\x86\\x1aK;\\x18\\x8c\\x02=iK\\x04\\xbd\\x1a\\\\\\xbc\\xbb\\xdb]a<\\x91\\xc8\\x14<\\x0f(\\xb7<\\x0eP\\xb2<U}]\\xbdb\\\\\\xd3\\xbaLU)\\xbc\\xdbo\\xe1:\\xf4\\x9d\\x82<\\xc4\\x03\\x13\\xbd\\x90\\x13a\\xbd\\x07\\x97\\xf1<P4\\x8c\\xbc\\xb1(\\xd2<\\x03_\\x8f\\xbd/\\xd56\\xbcB\\xad\\x87\\xbd~\\xd7\\x06=O\\x92\\x80<\\xd6\\x9b4\\xbc\\xbbC^\\xbcc@\\x81<P\\xc7\\x1d=\\x87\\xae\\xa6\\xbc\\x98\\x85\\xe0<\\xe2E\\x94\\xbd\\xfa\\xa9S\\xbc\\xd0\\xa4h=\\xa3\\x10\\n;\\xc2\\x86F=?\\xc7w=s=\\x88<\\x94\\xe3\\xff<\\xd7\\xdf\\x02\\xbdF\\xfc|=L\\x8c\\x08<\\x08-R\\xbd\\xe0\\x97\\x1f\\xbd\\x0c\\xec\\x8b=kz\\x95\\xbd\\x94\\x91\\xd2\\xbc\\xc5s\\x90\\xbc\\xc3l\\x07\\xbd5W\\x10\\xbd\\xce\\xa6\\xa1=\\xe1\\xa4w;y\\xb0\\n\\xbd\\\"\\x83\\\"\\xbdW1\\x13\\xbd\\x17n\\xb4:\\x18\\xda\\xe2=\\xa0\\\"F\\xbcDy3\\xbd\\xe0xC=o\\x1c\\x90\\xbc\\xd3\\xfb\\xa7<\\x19\\xb8\\xce;\\\\6\\xed\\xbb\\xf8hC<\\xe0^&\\xbd7\\x94\\x11\\xba\\xf4D\\xe8;\\xaa\\xbc{\\xbc(r#=\\n\\x1e[\\xbd[\\xb9\\x01\\xbd8\\xee\\xd0<\\x02NC=\\x17\\xec\\xbf;}\\xdc_\\xbc8\\x83\\xe8<\\xa8<\\xf5\\xbc\\x03\\x1f\\x8c\\xbc\\xbb/\\xe0\\xbd\\xac3\\xee<\\xf4\\x83\\xa4<\\x02{\\xd1\\xbcr\\x86K=-j\\x15\\xbd\\xae%\\x1b\\xbbnK\\xe4\\xbc\\xc3d\\xcb;\\xa9\\x0fg<\\xae\\x8a\\xc9<}\\xe7p\\xbc\\x1a\\x07\\x159q\\x038=\\xba\\xb2\\xdb<\\xe4\\x9dT\\xbc\\\"\\xf8\\xf4<DG\\xad=2\\xa8\\xbd\\xbc\\xc6\\xfe\\xa9<*\\x00\\xe0=7~\\x01\\xbc.\\x94f\\xbd\\xf5\\x88\\x11\\xbd\\x96l \\xbcP\\xae\\xf5\\xbc2\\xber<\\x9e\\xfa\\r\\xbd\\xc9\\xfa\\xa5\\xbcsL)=\\x1e\\xab\\x18=\\xa1\\xde\\xef\\xbb\\xd6\\xef\\x17\\xbd\\x1a1\\xa7\\xbc\\xd0\\xd0\\x95\\xbc\\xcd_\\xa7\\xbb\\xd8\\xa5^\\xbb\\n?y\\xbd\\xb1\\x17\\xa4<\\xb8\\xb4:\\xbd*\\xf9q=\\r\\x7f\\xa0:\\xdfI\\r\\xbd\\x14FT\\xbd\\xed\\t\\xeb\\xbc?\\x82\\xb9\\xbcEzd<\\x95\\xa9x;\\x05\\xa1\\\"\\xbdU1B=4^\\x0c<\\xb3B\\xf3;h\\x8dB\\xba\\xb9\\xe1q\\xbb\\x17V\\x9f=\\x1f\\xeeW\\xbc\\xa6\\xbai\\xbcY5w=\\x00\\x97\\xcb<\\xfc\\xefG=\\xb2\\x17\\x02=<\\x0cC;\\x15\\xa3=\\xbd\\xac\\x8e\\xc1\\xbcc\\xccx=\\xceN8;]:\\x85\\xbd\\xbfd\\xc2<4\\x18\\x05<\\x0b@\\xed\\xbc~*\\x11\\xbd\\xdf\\x9f\\xc9\\xbd\\xae\\x7fc=\\xf3\\x00a<\\xd0\\\\\\r\\xbdK\\xdc\\xa1=\\x01\\x98\\xa7=\\x97\\xc3\\xe9<Qk\\x04<\\xc5Fv\\xbb\\x8a\\x921\\xbd\\xb8\\x02V\\xbc\\\"G\\x8e=|\\xa9B7\\x03|\\x8a<\\xe0\\x0c,<E-o\\xbas\\xa3\\xd1<tp!\\xbd\\x13\\xd2H\\xbd;N\\xac\\xbclu\\x13\\xbc\\xc3\\x01\\xea\\xbc\\x07%\\x91\\xbd\\xb9\\xe7\\xcf\\xbc(!\\xb3\\xbb\\x13\\x9d/\\xbd\\rA\\xf7<\\xedU\\x00\\xbd^\\x9c\\t\\xbd\\x1b\\xb3;=\\x1b\\xf7\\x97\\xbc\\xfdI*\\xbd\\xa0zx=\\x04Pf=cA\\t\\xbd\\xa6Z\\x9a;\\xdc+\\xbe<6P\\x92\\xbd\\xf4z\\x95<\\xf20\\xce<0\\xb7\\xa9\\xbd\\x11\\xba:\\xbc\\xb12k\\xbcj\\xaa\\x00=\\x87=/=\\x0c\\x8eA;\\xf0\\xf1\\xaa\\xbcF\\x95\\x84=\\xd0\\xf0\\x02\\xbci\\xba/:.\\xac\\x94<cW\\x0f=>\\xe12;Jm8=\\xc1\\xc0s\\xbc\\xf2\\xce\\xbe:\\xcdT\\x9a<\\xa7\\x92~\\xbb|\\x88\\xa0\\xbd\\x9e\\x97\\xe9;X\\xb3\\x91\\xbc\\xbd\\x07\\x8d<\\xb9\\xc5\\xae\\xba\\xa6(\\x97<\\xbe\\x030=\\xce3\\x89;\\xa9p\\x91\\xbc\\xec{B\\xbd\\xbc\\xaf7=S]\\x18\\xbdJ`\\x04\\xbd\\x84\\r@; n\\xbd\\xbc]\\xca\\x9a\\xbb\\x81\\xbc\\xe7\\xbcH\\xdb\\xda\\xbc\\xcd#\\x8d=\\xc90\\xb0<\\xb5D\\x06=\\x0e?\\xfa<\\xe5gX=\\x86\\xe2I\\xbc\\xc6\\xc8\\xa9\\xbc=\\xb8\\x9a;\\x1a\\xe0\\xae\\xbc\\x9f\\x88\\xe5\\xbb`}\\x8c<\\xcc\\x1f\\x1f\\xbc\\xbe^\\x8d\\xbcm\\x91-\\xbd\\xcdY\\xc4<\\x92\\x95\\xcb\\xbc\\xe9\\x9a\\xc8\\xbd\\\"\\x95\\x01\\xbda\\xea\\x17\\xbd\\x00\\xbb\\xc2<\\x05\\x0f\\xf1\\xba6\\xf8\\xad<s\\xfb\\xfe\\xbc\\xc5\\x90m;\\x02\\x00\\xaf=D0\\xa7=\\x8a\\xfb9\\xbd\\x8c\\xa0G=\\x1f\\xa2\\xe2\\xbc\\xf1\\xbcw<\\x8b\\x9e8=\\x83\\x03\\x86\\xbc\\xf7\\x1b\\xab\\xbc3\\x8c\\xf3;\\xabA\\xd1<M\\xa2\\x02\\xbdJ\\xa1\\x1c=\\x86\\xad\\x0f\\xbdH\\xd8\\xf6:X\\xc1\\xf1<\\xb3\\x16\\xee<\\xd5)\\x86\\xbc\\x10\\t\\xfc\\xba\\\"\\xb5\\x1f\\xbc\\x9cd\\x80\\xbc\\xc0\\xc3\\xa9<\\xbc\\xdf\\x80<,\\xbd#\\xbd\\x81\\xd4\\xf4<[i\\xe5\\xbc\\xd0v\\xc9\\xbd|\\xc6\\xe1;.\\xd0\\xa6\\xbc\\n\\xc2\\x84\\xbb\\x81\\xf4\\xe6<N\\x86\\x16\\xbd\\xc1\\xf4\\x89<*j\\x82=:]\\xb9\\xba\\x1eq@\\tF$\\x8b\\xbcc\\x08\\x1c\\xbd\\x07\\xba\\xe1<\\x9cg\\xcb<\\t\\xed6<\\x06Kg=\\xc7\\xa8\\x10<\\xbcdB:4\\xa9I\\xbb\\xf1\\xa9\\x84\\xbcJ\\xef&=\\xa59/=\\xe5\\xdb\\x19\\xbc\\x0c\\x95\\x94=\\xc1W\\x1f\\xbdN\\xd9\\x8b<\\x1a\\x0c\\xd6\\xbc\\xa7\\x1d&<\\x08MI\\xbc\\x13\\xda>=\\n\\xb9\\x93<\\xe6\\xd7\\x12\\xbbM4\\xfc<\\x0b@\\n<A\\xbe\\xc4\\xbc%\\xc7\\xc8\\xbc\\xd0K\\xef<\\xa8\\xca\\x85\\xbc\\xf5h\\xc3\\xbcn\\xbd\\xb6<\\xa4\\xb0\\xb3\\xbb\\xb4,\\xd7<\\x04j\\xb1\\xb8\\xa5Ni\\xbd#\\xfaD\\xbd\\xbeX\\xe1\\xbc\\xe9\\x95\\n=\\x1c{\\x14\\xbb\\xc4\\xbb\\x98=\\x9a1`\\xba\\xec2\\x11=F+\\x1f<\\t\\xdd\\x0c=f\\x7f\\x1e=\\x0c*\\xea<\\x1dZ\\t=-\\xab\\xe4=%$\\x0c<\\xb6\\x88\\x18\\xbc\\xac\\x83j\\xbdjKa\\xbd\\xb0\\xd6\\xbc\\xbc\\x91\\xdcI=\\xd7\\xb0\\x84\\xbc\\x1a\\x88\\xa6<\\x8c\\xd2\\xde<\\\\\\xe2`<U:\\x8c={\\xa2$\\xbb\\xdf\\xafP<,\\x01\\xb0\\xbb\\xb9\\xe3\\x88;\\xcd\\x0e\\xad\\xbc\\x0e\\\"\\x01=x\\xcf}\\xbd[@m\\xbd\\xc9\\xcc\\r\\xbb|\\x12\\xcd\\xbc\\x9e\\xc7\\x12=\\xc6)\\xfc\\xbc\\xa6G\\x15=\\xaf\\xd9\\xc8\\xbcQ\\x8c\\x19=\\xc0l\\x83<S\\xa3\\x02=\\x04\\x88\\xd8\\xba\\x8f\\xceE\\xbcR\\xaf\\x82=R\\xee#\\xbc\\x98r==/r\\xda\\xbc\\xa0T\\xf1:\\x1aO\\xdb\\xbc\\xa8\\xeeX=\\xf7\\xbe\\xc3\\xb9\\xb6`\\x10<@hM=\\x11Y\\x1e=;\\xb8\\x07\\xbc\\xa4\\xff\\x12\\xbc\\xc0\\x97\\xed\\xbb\\xb56\\xf1<!\\xb1\\xc2=\\xa6\\xda\\x06\\xb8\\xeb~c=(a7=\\x8e\\xfbb\\xbdH\\x84\\r\\xbc\\xd74\\x89\\xbc\\xff\\xd0\\x08<\\xc7X\\x0e\\xbd6Y4\\xbd[\\x8b:<\\xaa\\xd3\\xfb\\xbc\\x90_\\x15;\\xf5\\xda0<)\\xdf\\x9a<\\xb5\\xa1\\xa4\\xbc\\xdc4\\x8c\\xbd_r\\xe7\\xbc\\xb2\\xb2}<\\x804\\xa5<\\x0f\\xfb\\x8a\\xbd\\xdc\\xc9H=5*#=\\x94^\\xa7<TZ\\x97=Ou\\xf8\\xbc}\\x15\\x99\\xbb\\xbd\\xe2\\xa5\\xbd\\x9e\\x8d\\xfa<\\xe2\\xf1k<\\xb2\\xe6\\xb3<PK\\xf7\\xbbAw\\xc0<\\xbb\\x0e\\xa9\\xbc\\x96\\x8b\\xf4;\\x1a\\x01\\x08\\xbd\\xc3\\xae4=(\\xa3|\\xbd\\x08\\xd9\\x9e\\xbcc\\xef\\x88\\xbdtgE=R\\x95\\xce\\xbbNt\\x9f=\\\\\\x18$;\\x86\\xf1\\xd7<\\xbf\\xf7\\x88\\xbdM\\xcd5;\\xaf.\\xd3<+-\\x87\\xbcO\\x86V\\xbd\\xab\\x94\\xd2\\xbc\\x9b\\x80g;\\xd8 \\xbc\\xbc\\x84\\x88\\x8c\\xba\\x11>\\x1e\\xbbi\\xa3\\xc2\\xba\\xa7lD<R`\\xff;\\xad\\x02\\xae<\\xf1!\\xb6<\\xa2\\xd2\\xe6;I2\\xee<\\xec\\xee\\xbf<\\x1e\\xd8\\x8d<7\\xde\\x96\\xbc\\x03Sg=\\xe0\\xe5 <\\xaas\\x00=A$q\\xbd\\xdc\\xcb\\xb8=V\\x06Q<{\\xf6$;\\xb5V\\xd0\\xbb\\xb5l\\n\\xbd\\x95\\n?\\xbd\\x1aN\\xa1<\\xfd\\xa3\\xb9=\\x94l\\x9e<\\xb7b\\x90\\xbc\\x05\\xebD<lt\\xad<h\\x06\\x1b\\xbc\\xccH\\xea;\\x14g\\x97=\\xec\\x00\\xfd\\xbb\\xb1\\x81u<t\\xf8\\x05=y)\\x02<\\x8a\\xdb6\\xbd\\xa1\\xa7j\\xbdaa\\x1a=`\\xa6\\x1d\\xbd\\xe3X\\xed\\xbc\\x18l\\x02\\xbd4B\\x04\\xbc<?6\\xbd\\x1f\\xcb3=\\x0b\\x82\\x95<\\xc3\\xad\\x01\\xbcp.\\xb4;M\\x8a\\xa2\\xbd\\xf9\\xd3\\xc3\\xbc\\xf5s\\x02\\xbdoJ\\x04\\xbd\\xf1\\xac:\\xbd}\\xe3\\x1b</\\xc1\\xa0\\xbc}\\x19:\\xbd\\xbbD\\xb8\\xbc\\xe0\\xb3X9&\\xd9\\x89\\xbc\\x81\\xeb\\x98\\xbd\\xd6U\\xe7\\xbc\\x06?o\\xbc\\x83\\x1e\\x91<\\x9b\\xba\\xc7\\xbc\\xe2\\xf8\\x80\\xbb\\xd7m\\xb9\\xbc\\xfa\\xe1\\xed<j\\x8f\\xac;Q\\x08!=\\x0b\\x8ff\\xbc\\x8f\\xc1V\\xbd\\x7f\\xbe\\n<\\xd4\\xaf;<J\\xd5Q\\xbc\\x05L\\xb5\\xbc\\xf9xl\\xbd\\x88\\xa2\\x86<-jP\\xbd\\x80\\x8cz\\xbc\\x10\\xa1\\xaf=t\\x9c\\xa1=+eM\\xbdk\\xa1\\x89<\\xa07\\\"\\xbd\\x85\\xd4\\x0b\\xbd\\xc2\\xd1\\xaf<\\x0b\\xb8\\x10\\xbc\\x02\\xf2\\xfb<2\\xad\\xc7\\xbc\\xd2\\xc1\\xf8;\\xfd\\xfa\\xb1<\\x81\\xc2L\\xbd\\xd1\\x0b/<\\xaf\\xac\\x0f\\xbc\\xe9\\xee\\xd8;\\x1cty;\\x85^\\x90<ve\\xbe<\\xdb\\xfb\\xb2;\\x03v\\xda<\\xfeZ\\xe9\\xbcH\\xce\\x08<\\xb4iY\\xbcdy\\n\\xbb\\xf3\\xec\\x9f\\xbd|O\\x1d<1\\\"\\xb7\\xb6\\xca\\xc9\\x86\\xbc=\\x9f\\xd0;d7\\xa5<ow\\x98\\xbc\\x17id=\\xb3O\\t\\xbd\\x9a<\\x88<\\xef\\xd8\\xe8\\xbc5G\\x1f\\xbd\\x1c\\x1f4<\\n\\xadP<ui\\x8e;l\\xb5|<\\xa3%\\xa6=\\x9e\\x9ez\\xbc\\xf4/O=e\\xc2\\x8d=u\\xa1?\\xbd)i\\xbd<\\xc0-\\xe0\\xbcm/S=\\t\\xa8\\\"\\xbd\"\nHSET bikes:10040  model 'Quaoar' brand 'BikeShind' price 2532 type 'Kids mountain bikes' material 'full-carbon' weight 13.5 description 'This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"r\\xed1\\xbdDKi<\\x89\\x8d(\\xbd\\xe2\\xf9\\xfc<@\\\"v\\xbbw|\\xd7<\\xca\\xd6\\x1d\\xbb\\x08\\x98\\xd2\\xbcu\\xad\\x91=)\\xf2-=\\xa8\\x94\\x10=M\\x9a*<\\xc9;\\xbb<[`\\xdb\\xbb\\x94\\x89\\xa8=\\xbf*\\xf2\\xbb\\xfe_7=C\\xfeV\\xbd\\x9e\\xe5L=\\xddr\\x8a\\xbd\\x9c\\x7f\\x8f<\\x89\\x1c<\\xbd\\xd7\\x19\\x8f=\\xb6\\x88)\\xbd\\x17O\\x16\\xbd\\xb8I\\x8e<N\\x82,=2h\\xb2\\xbc\\x0c\\xc2\\x1a\\xbd;e\\xea=n.\\xb7\\xbc\\x19\\xd7*\\xbd~\\xea\\xbd<\\x08\\x1dK\\xbb&\\xacX;\\xdam\\xe2<Ov\\x00=\\x12Gn\\xbc\\x11&\\x14\\xbd\\xff\\xc4\\xbf\\xbb\\xe3\\xf3f=I^\\r:\\x98s\\x1f=\\x87QE<\\xb6S\\xe3\\xbc\\xf8\\xd5\\x89\\xbc\\xaba\\xfd=YE\\x16\\xbd\\x98\\xb1F\\xbcL\\xf2\\xb0\\xbc\\x1a*\\xe7\\xbc:8\\r\\xbd\\x01\\xa7j\\xbc P\\x8a\\xb9f!\\x17\\xbd8\\xcb\\xaf;\\xdf\\x1a\\xb7;\\xceTE\\xbc\\xe0\\xed\\xcd\\xbc\\xc5\\xe7\\x94=\\xefn\\xb8;c\\x12\\\"<d\\x18\\x1c=q\\xfcX=Pd\\x04=0e\\x83<v\\x18%\\xbc\\xfaj\\xe9<\\xba\\xa9o=i\\x86\\xf1\\xbcv\\x06+<\\xa2\\x93M<T7\\x0c\\xbd\\\"\\x1c\\x12=\\x1fN\\x1f\\xbd\\xcaB\\xe8\\xbc%5\\x02=U\\xe0\\x0b\\xbd7\\xc2\\xb3\\xbd\\x8f\\xcbj\\xbbK\\x12\\xe5<\\xa7\\xd3\\xc8\\xbc\\x1d\\xe9\\xfa;\\x03\\xc1\\xb9\\xbd+\\xca\\xa5<q\\xe3\\xf1<\\xc5\\xfdV<\\x8c\\xa9\\x1b<3:\\x1e=\\xf9\\xda\\xb0;\\xb5\\xc1\\x0f\\xbc7\\xcbV\\xbd@\\x08H\\xbdw\\\\\\x97<Gim<l^\\x7f<\\x12n=\\xbd=g\\xdf\\xbco\\x0bD=2\\xf6\\xcc\\xbc\\x9cA\\x1d:\\x8e\\xb5\\xb0;V\\x85\\x0e=\\xfe\\xb2\\xd0;\\xf8j\\xe5\\xbcV~\\xeb=\\x13\\xfe\\xd1\\xbb\\xd4\\xaf[<E;\\xcb;\\x08\\x95\\xda\\xbc\\xff\\x91\\xac\\xbcba1=:Y\\x8d;2\\xd4\\x18\\xbd\\t\\xab\\x82\\xbd\\xf5\\xd1\\xfc\\xbc\\x95j\\x0e=\\xa1\\x92\\xcf;\\x19m\\xd5<\\xd9k\\xb1\\xbc\\xc6\\x86\\xf3:\\x16\\xb1\\xc7<!\\xcb,\\xbdy\\x06\\xfe\\xbc\\x1b\\xeah=\\xc3\\x00\\x8f\\xbc\\x80\\xd6\\x95\\xbc\\x89\\xa5K\\xbd\\xbc52\\xbd!\\x1c.\\xbd#z\\xc4\\xbc\\x80\\x97\\xcc\\xbcC\\x08\\xe5<\\t}8\\xbd\\xc6\\xa3\\xa3;\\x8a\\xba^=6!6\\xbc@(\\xbc\\xbcaK\\xad\\xbb\\xcf\\n\\xf1\\xbc\\xf3\\xd21\\xbd\\xacg\\xa4\\xbdo\\x82a\\xbb\\xdaa\\x01>\\xe3G\\xb0;\\xd5q\\x84\\xbc\\x0c\\xdc\\x93<\\xfc\\x06\\xdd<\\x18Ql\\xbc)3\\x87\\xbc\\x01\\xf6\\xae\\xbc\\xec\\xd2Q=\\x92\\xe74\\xbd;d\\x1d;g\\xaa\\x89\\xbd\\x0f4P<\\xde\\xf6\\x94;\\xb1\\x19\\xfd\\xbcQ\\xfcT=\\x1e\\x0be\\xbd\\x9b\\xae\\x17=\\xa4g\\x10\\xbb\\x89R\\x01\\xbd0\\xc6\\x81\\xbd\\xbey\\x82<\\xbe\\xfd\\x1f=,=\\x9c\\xbc{f\\xa7<\\x8a\\xf6\\x94<\\xbbv\\x94\\xbc\\x8a\\xaa\\xe2;\\x92Z\\xe6<\\xf2\\xba\\x88\\xbcC\\xf5\\x0f\\xbd\\xaa\\xf4\\xfe\\xbc^P\\x16\\xbc.=\\x1a\\xbd\\t\\xc4B=/.\\x9b\\xbc\\x86\\x92\\x15\\xbb\\x85\\xec&\\xbd/X\\xae<\\xf7\\x8aA;K\\x99\\xa7\\xbc\\xa8\\x9a8=\\xc6\\xa6\\xc3\\xbcL\\xe5)=\\xf2\\xe6\\xb0\\xbc\\xc93)\\xba\\xafz8<\\x1a\\xbf\\x0c=\\x96J\\x8e=\\x15^*\\xbd\\xb8\\xfd\\xa1<S\\x91\\xe7\\xbb?\\xd1\\xbc<\\x03\\x8f}\\xbd\\\"}Z\\xbc\\xc7\\xb5#\\xbd\\x03\\x1a)=\\xd0u\\xad\\xbb\\xb0G[\\xbd\\x00%\\xb39\\x8b\\xfcB;d\\xfa\\x9e\\xbcD\\xb8I=\\xa8%\\x8a=c\\x9f*\\xbd\\x89Lm<\\x97\\xd2\\x95\\xbc\\xeaL\\xf6\\xbb3\\x95\\xf1\\xbc\\x1e+\\x8e=\\xc1\\xcb\\x90\\xbc\\x92\\xe8T<\\xfd\\t\\x8d<\\x07uK\\xbaF\\xbb.\\xbc\\xd8\\xe5.<\\xd5F\\xb2\\xbd\\x95C\\x10<\\xc3\\xf4(\\xbdc\\x07`\\xbc\\xd4\\x11\\x07=\\xca\\x90*\\xbdavV;d&\\x1b=F\\xc9-\\xbd\\\"\\x15r=Q\\x04\\x9e\\xbb\\xe4\\xc7\\xe4\\xbc\\x99.m\\xbd\\xd2O\\x13\\xbc\\xb6h<=P\\x06\\xe0\\xba\\x04\\xb2\\xf2<>\\x13a\\xbc\\x180*=>\\xb2\\x93<\\x17\\xfd\\xca<\\x93\\xaa \\xbd\\xb1$\\x92;\\x7fBM<\\xd5\\x85Y\\xbb\\nH\\xd0<o(\\x9e=\\x0c\\x8d\\x11\\xbc\\xb0\\x1e\\xc2\\xbd\\xe6\\xdb\\xc2\\xbd_S@=\\xbe.~\\xbc\\xcf\\x10\\x01\\xbd0\\x0c\\xcc\\xbcU\\xbf\\xb6\\xbbT(\\x82\\xbd\\x03\\xd1\\x04=\\x90\\xaeB\\xbd\\x06V\\x81<\\xd0\\xf3G\\xbd/\\xf8\\x88=\\xb0\\xf9\\x1e\\xbd\\x05\\x19\\xd0\\xbb.\\xf5(\\xbd\\x82v\\xfd\\xbb\\x84`\\x02\\xbd\\xf5:\\xa5=:\\x1f\\xc4\\xbb\\xba\\xc4\\xba\\xbc\\xfa\\x8bl<\\\\\\xf0\\xe1<\\xee\\xe2\\x92\\xbc\\x0fg\\x84\\xbb\\xd82D<\\x0f\\x91\\t=\\xfa`\\xbc\\xbc\\x81de\\xbc\\xe4\\xaf\\x1e\\xbd\\xb5\\xbaA<\\xa03X=\\x85B\\xc9\\xbd\\xcd=%\\xbd\\xa8Sg<\\x15Eh\\xbc\\xdd\\x80R<\\x9a\\xa8\\x95<\\xd7\\xce\\x12\\xbd\\x16\\xdc\\x0b=\\xacp\\xa0;\\xd6u\\xac\\xbd\\x8d\\xc2@:#\\xd8G<\\\"\\x1d\\x15\\xbd$\\x8f\\xd7<\\xf1A\\xe8;2\\x04\\x0c\\xbc\\x84_N\\xbd0`_;\\x89W/=t\\xd1e\\xbcFG\\xcb\\xbc\\x8eY\\x01=\\xea\\xaa\\xd3<#\\x96%=\\x1b\\xd5\\xe4\\xbc\\x90\\x9a\\xc2;.#\\xbf\\xba\\x9e\\x88\\xd6<sD\\x06\\xba\\xd84\\xa8\\xba\\xe5\\xea\\x17\\xbd\\x15P/\\xbc\\xce\\\"R\\xbd\\x1c\\xa7\\x87;\\x1eX>\\xbc? \\xd7<\\xcf\\xaa5\\xbdd&.\\xbdE\\xb2\\x8e=p\\x01V=\\xd5d\\x04;\\x18}\\x89\\xbc7\\x18x<\\xcd\\x8cf<\\x882\\xc5<\\x1e0B=\\xf0\\xe87\\xbd\\xb7\\xab\\x19<\\xc8JA\\xbcK\\xa94\\xbb\\x1c\\xc6\\x8c\\xba\\xa8N\\xd4<\\x01\\xa7\\x97\\xbd\\xdd\\xc3\\x04\\xbd\\x94P\\x00\\xba\\xff\\x88M:$\\xd4\\x13<\\x8a\\x19\\xec\\xbb\\\\\\xf5\\xee<W$\\x1e<s\\x0c\\xad<%r\\x80\\xbc\\x89?\\x89<mU;=1\\x87M\\xbc\\xe80\\xd89\\x83\\xb9@=\\xa0=\\xb6=O\\xd7&=\\n]X=\\xa3i\\xef:\\r\\xe8\\xab\\xb9\\xbf\\xed\\xb2\\xbc\\xa0\\xd0\\x92=`*\\xaa\\xbc4{\\xaf\\xbb\\x8b3\\xc0<\\xa7\\x7f\\xce\\xbbE\\xa1\\xad\\xbd\\x88lS\\xbb\\xdc5\\xf2\\xbd\\x00\\xeb\\xe7\\xbb\\xa7\\xb0\\x0b=\\xd4|\\xc5<\\xcd\\xeb\\x85=\\xa5\\xf8\\xf6<\\x87\\xad\\xda<\\xd2p\\x83\\xbccn\\xde<\\xb5\\x97\\x10\\xbd\\x07\\x1c\\xba\\xbc$\\xdbj=\\xbaI\\xcc;-/a\\xbb=\\xd4\\\"\\xbcv\\xb7t<\\xcaA-=\\x95u\\x0e\\xbdy\\xe7\\x82\\xbd\\x0e\\x9a%\\xbd=\\x83;=\\x04\\xeb\\x08\\xbd\\x01\\x1a\\\\\\xbd\\xf3t3\\xbdkt\\xd4;P\\\"\\x05\\xbdJ7}<\\x167\\xb1;\\xe4\\x9a6\\xbc>\\x08\\x96=\\x96.\\xd6;t\\xe1\\x82\\xbd\\xb8\\xeb\\xab=\\xaa\\x89\\x81=\\xb1\\xa0\\x93\\xbc\\xfc]\\x9b\\xbc\\xf0\\x00\\x8f9\\xf2\\xc1\\xaf\\xbd\\x16Rn<\\x81\\xbf\\xdb<$\\xe2Y\\xbdh\\x0eR9\\xdf\\xdd\\x7f\\xbdN]\\xda<(6\\xd9<\\xe3~\\xac;\\xb2\\xfa\\x0c\\xbd\\x00\\xcc\\x16=\\xfcO\\x06\\xbd\\xaa\\x8e\\xa3<\\xc0\\t\\xe8\\xbc\\xdc{x=\\xdd>\\x0e<\\x14\\x1b\\x89\\xbb\\xf8\\xb2\\xac;\\x99\\xf9\\xcb;QBe<M\\xac\\xb5;\\xe2\\xde\\xcf\\xbc4\\xd9,:\\xb9\\xae/\\xbd\\x0eWw:\\xf1\\x95\\x8e\\xbc\\x19\\x9c\\x7f<\\xee$.=@\\x94#<\\xf8\\xf9\\\"\\xbd\\x96U\\xf7\\xbb\\x82\\xb6y=_\\x0e\\xff6\\xa5@\\xa3\\xbb\\xe4\\xc8\\xf3<32Q\\xbdx\\nA\\xbc\\xde\\x98\\x88\\xbd\\xc0G\\xa4\\xbcA\\xb5\\x98=\\xdfVs=3\\x112=\\x84\\xf7\\x92;\\xd1\\xfe\\xcd;f\\xe8\\x8d\\xbbaS\\x11<N\\xf9\\x96\\xbb\\x83\\x96\\x02\\xbd\\x99\\x9a`<\\xfd\\xe6o<\\xd7\\xcd4=\\xf0|\\r\\xbcS\\xd9\\x0e\\xbd@\\xa2\\x8e=\\xb2\\xfc\\x18\\xbd \\x8eb\\xbd`m\\xc5\\xbc\\xaa?|\\xbd(\\xd2\\xb8<j\\x91L\\xbc;\\x00\\x03;2\\x87\\x1f\\xbd\\xbc\\xc9\\x05\\xbdu\\xa3\\x1a=B\\xa4\\xbb=P\\xee5<\\xdbN\\x8f=\\x19\\xf2\\xd4\\xbc\\xcd\\xe3s\\xb8\\xa9\\xda\\\"=\\xc0\\x08=<Q\\x9f\\x99\\xbb7\\x9fD\\xbaC\\xcd{=\\xa7IJ\\xbbOL\\xa2;Uc\\xd4;\\x8e2\\xd9\\xbb}2\\x98\\xbcC\\xbe(<\\x16!/\\xbd\\x1a\\x07\\xd2\\xbc\\xb9\\x8a)=\\xa8\\xeb\\t\\xbd\\x83E\\x12=\\xe4\\x03\\x0e\\xbcAy\\xa1\\xbb\\xe6D/=#@\\xb4\\xbb\\x89\\xc0\\xdf\\xbd9\\x93B\\xbd\\xaa0|\\xbc\\x15\\xb7\\xd8\\xbc:)\\xf8<,\\x1dL\\xbd\\x02J\\xac;\\x8e1\\x8c<\\xd1\\x9fx;\\xc0\\xa3o\\tI\\xdd\\xfb;Y/\\xad9\\x95\\x8c2=\\xd2\\xa8\\x9e<]\\x14\\xe1\\xbc\\x12i\\xd6<\\xf9\\xef\\xf0\\xbb\\x83\\xbaa\\xbc]}\\x14=\\xc1F\\xe3;\\x84,1\\xbb\\xd7`\\xc9<9\\x9b\\xe7\\xbcr$\\xe7<E\\x1e\\xbd\\xbc\\xb9\\xd2\\x17=\\x8e\\x07P\\xbdfN6\\xbd\\x1d\\xa0:\\xbb\\x83\\xf7 =\\x15V<=\\x96<\\x81\\xbc]\\xd9y\\xbcg\\xf7\\x11=\\xfbl\\x1f<\\xd1*F<\\xb9\\x89*\\xbb>\\xf4\\x8d<\\x8f\\xe7%\\xbdu\\xba*\\xbd\\x0cA\\xa1<\\xabg\\xf1<\\x85\\xc0K\\xbaz]Y\\xbd\\xf6\\xb2#;\\xec\\xc1\\xf9<\\xaf\\xd3\\xd9<\\x80\\xb3\\r=\\xe0\\x05\\xca<\\x00\\xc6\\\"=\\xfb\\xd11\\xbc\\x10#\\xca\\xbc{FC=\\xd0\\xa2y\\xbbv\\xcf\\xf0<+&\\x00\\xbd\\x1a2\\xfc=;\\xb9\\x9e<#\\xe34\\xbc\\xfaC\\xbf\\xbb\\\"$\\x07\\xbdx\\xfb\\xbd\\xbda\\xf8]=Em\\xd9\\xbb\\xfb\\xca[\\xbd\\xf2\\xfcD=\\xfbpe\\xbd[\\x8c\\xb1\\xbb\\x1d\\xae\\x83;\\xc2<K=\\x94.\\xb3;\\xf1\\x91\\xf1\\xbb\\xb8\\\\\\x89\\xbc\\xff\\x9f\\x17=\\xe1\\x90$\\xbc\\x03\\x87F\\xbd\\t\\xd7\\x15\\xbd\\x04\\\"\\x8f\\xbc\\xce\\xe5r<\\xdd\\xb3\\xbe\\xbc=\\xf6\\x97<\\xa7\\\"\\x15\\xbd \\x96%<\\xac\\xc7}<\\xb8\\x8d6<Ta\\x8c\\xbb\\xff\\x96\\xc1\\xba\\x91\\xf93=s\\x99\\x83\\xbc\\x0b\\xb4\\x9b<\\xc4|\\x05\\xbd54~<@\\xd39\\xbd\\x99\\xdbQ=P\\x8cW\\xbd@\\x1c\\xd4<o\\xef`=\\x9c\\xaf\\x12=\\xc9\\xad\\x88\\xbb\\x1b\\xd6\\xb2<\\xe3\\xa4!=gO\\\"\\xbc\\xa1h\\x1d=)\\xa8\\xb3:\\xe4\\xa4\\x84=\\xb3\\x7f\\x0b=\\xb1\\x07\\xed\\xbc\\xcf[\\xae\\xba\\xa8\\xad~;(\\xbc\\x00=\\x96\\xd8!\\xbd(In<\\x0fy\\x8d;\\xa9P\\x11\\xbd\\xf3\\xd5[=\\x8fw%\\xbcj\\x8b\\xb9;\\xa9\\x05\\xbe\\xbc\\xa3\\xdb\\xfe\\xbc\\x1b\\xf6j\\xbc\\xdd\\xa9\\x96=?\\x980\\xbd\\xe4\\x19\\xcc\\xbc\\xf2\\xb7D=a8w=\\x9e<\\x98<\\xdc\\xcd1=#\\xe9\\xe5\\xbcO\\xa3,\\xbdvn\\xd9\\xbc\\xe4\\x8f\\x17=;4\\x8a<:z}\\xbc{\\xa2\\x85:@\\x82p=\\xce\\xe4&;\\xb4=f\\xbd\\x1c\\xc0\\x8b<L\\x1c\\xab<W\\xe5\\x1c\\xbd\\xb8XQ\\xbd>P\\x86\\xbd\\x08\\xe2-=\\x01mn;\\xd4\\xd1*=\\xc4\\x1f6<\\xe1\\xb4\\x1e=\\\"7l\\xbd\\xa0\\x1e\\xe6\\xbc\\xea\\xc9\\x85\\xbd\\xbe~d<!\\xde\\x81\\xbdIRH\\xbc\\xb4\\xfb\\x84<B\\x0c\\xa3<\\xe19R=E\\xbf\\xab<D`\\xcd\\xbb8q+=\\x94s<=\\xcc\\xa3Z\\xbcH\\x9c\\xdf\\xbcy\\x08\\x8f\\xbd]T\\x04=\\x95\\xad\\x8a<5\\xb3\\x9d<Z:\\xc8\\xbc\\rb\\x85=r\\x9c\\xa8<s\\xd3\\x94<\\x97\\xd7\\xcb<\\xaa\\xbf\\xb2<v\\xbb\\\"\\xbc\\x9f\\xdb\\xb3\\xbc\\xba\\x85\\xaf\\xbcg\\xef\\xea\\xbcfK\\xf6\\xbc\\x9b\\xb1\\x06\\xbd\\x8c\\x17=;2 _\\xbd\\xcc\\xbb9<\\xd7\\xe7\\xef\\xbc\\xe9\\x16A<\\xfcdF\\xbb%\\xf8f<\\x93\\x17\\xc5<\\xee\\xa3<\\xbc\\x19\\x84t<3\\xdd\\x87=\\x17\\xc13\\xbd+\\x9a\\x00<66\\xce\\xbd(/\\xb3<N\\xce\\xe8\\xbcT\\x10\\xe5\\xbc\\xef\\xe6\\x89\\xbc\\xf3\\x07%<\\xfb\\xec\\x05\\xbca\\x90\\xe9<!\\xd0\\x92<qB\\xae\\xbc\\xc4\\xd4-=\\xbb\\x8d\\xba\\xbc\\xda\\x8a <t\\xaf,\\xbd\\xcb*\\x07\\xbd\\xd1\\xd1\\x1b;r\\x11X<k\\xc9\\x11\\xbd\\x04\\x01\\xb6\\xba]\\xbc\\x90\\xbdb\\xf8\\xbd\\xbc\\xaeB \\xba\\xbb\\\\\\x94\\xbd\\x01\\xba\\xd2\\xbc\\xda\\x009<\\x7f\\x10\\xa3\\xbc\\xd0\\xf6\\x1f\\xbc\\x9f\\xa7\\\"<\\x1a\\xfbD\\xbd\\xe9E\\xd7;\\xe0\\x19\\xcd\\xbcj\\x96\\xae<\\xb7\\xbb\\xac\\xbc\\x89s\\x95\\xbcD\\xb8\\xf1\\xba3U\\xb5<\\x99\\xb1\\x84\\xbc<\\x1bi\\xbd\\xe5\\xb5/\\xbds\\xa25< |\\x1a\\xbd\\xa3\\xe0\\x14=c\\x19K=\\xfe\\x86L=\\xc0\\xb0\\xd0<\\x8d\\\"\\xf9<\\xf0\\xb7}\\xbd\\xf7\\xcfx\\xbdCZ\\x14=\\x83\\xe9><;\\xb7S=\\x834\\xe5\\xbc+\\xab*\\xbc\\xcc\\xa5\\xb7;AY\\xb5\\xbc3F\\xab;\\xde}\\xb5<dr#\\xbcA}\\x1e\\xbc\\xe1\\x81\\xa8\\xbb\\xb6\\x00n<\\xc5\\x9eR=\\x19\\x13\\xae<q2\\xe7\\xbc%i\\x01=O\\xcd\\xcb\\xbc\\x7f% ;>W\\xf4\\xbc\\xd2\\xd58:-\\x91\\x96<O\\x91\\xa4;q5\\x87:\\x91,\\xf6<\\xc1>\\xee\\xbc_H\\xe7\\xbc\\xa8x\\x1b\\xbd\\xfbB\\x0b\\xbd\\xa5z\\x82<\\xc0]\\xe1\\xbc\\xbc\\xc2\\xa3<{W\\xfd<\\x7fkQ=/\\xfb\\xe1<`w\\xa9=\\xa2\\x92\\xe6<a\\xb21=yb\\xb1=\\xb0\\xbfO\\xbb]\\xddi<\\x19i\\xf6\\xba\\xbcKA<Q\\xdc\\xf7\\xbc\"\nHSET bikes:10041  model 'Ncc1702' brand '7th Generation' price 1254 type 'Kids mountain bikes' material 'carbon' weight 16.1 description 'Small and powerful, this bike is the best ride for the smallest of tikes. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\xc6\\x0c\\x8c<\\xa0j\\x08=\\x80w\\x0e\\xbd\\xe4\\x93j=lH\\xeb;\\xd2\\x12\\xbf<\\x83@\\x06<F\\xfbD\\xbdt\\xb4\\xaa=\\xb0x\\x99;-MH\\xbd}-\\x83<\\x15\\xddQ=Oc$<jT\\x85=\\x97>\\x01\\xbc\\xfc\\x0e\\x87\\xba\\xe2A\\x95<\\xc1\\xcd\\xec\\xbc\\xc4\\x9bi\\xbdM2\\xb7<\\xc3\\xee{\\xbc)\\x0f\\xb9<4\\xe7\\xb1\\xbc\\x96\\x01\\xb2\\xbcW7\\xa3;+M\\xc5<\\xd32\\x03\\xbcf\\x13<\\xbcvp\\x92=\\xba\\x91~;\\xff\\xb5\\x9d\\xbc\\xe4J$=\\xdd\\xb1\\xe1<5IF=\\xd5Q\\xcb\\xbb\\x10\\r\\x07=a|\\xdb\\xbcQS\\xa2\\xbc\\xc7\\xc4\\x8d<U\\x83\\x14=(\\x1a =\\xde\\x1f\\xbd<\\x005\\xe7<\\xf1\\x86\\x86;!<\\x91\\xbc^X\\x94=(\\x11\\xb3\\xbc\\x9c|\\x11\\xbd\\x81\\xb3\\xb4;s!\\xaa\\xbc\\xce]\\x0e\\xbd\\xdf\\r\\x1f\\xbc\\xaf\\x0f\\xba:_4\\x1b<(Hk< \\xdd\\x88:\\x07o\\x82\\xbd\\xeb\\x8b\\xdc\\xbc&\\xb1\\x8a=\\x8f\\xe7\\x9d<b+\\x84<%\\xf9\\x1d<\\xfe}S=\\\"Fg=\\\\\\xb9\\x8c<\\xbfq\\x85\\xbc,\\x99\\x88<\\xe2\\rh<\\x8co\\xf3\\xbc\\xf1\\xd5g\\xbc\\x81\\xcdl\\xbc\\xe7qn\\xbc\\xb3V\\x02\\xbdo\\x87\\x9e\\xbcI2\\xa8<\\xf5\\x0e\\xed;\\x8d\\xe9o\\xbc\\xb5\\x9ft\\xbd\\x1c\\xc3=<\\\\r\\x91<\\xc5\\x00\\xb9\\xbc+]\\x15\\xbd\\xbc\\x12Q\\xbc_\\x9fB\\xbb5\\x81F\\xbdC\\xf2\\xc3<\\xcd\\xab\\xa7=\\xf1\\x148=\\x8f\\x16\\xb6<\\xcb\\xb7\\xf5;\\x8d\\xc4a\\xbc&\\xc5G\\xbd\\xf9\\xd4\\x08\\xbc\\x91\\xb71<\\x98\\x84\\x0e=&q(<\\xb2\\x03\\xdd\\xbcL\\xaa\\x04=\\x9a\\x9e\\x8b\\xbd\\x12\\xe5\\x03=\\xf1\\xb0\\xf0<\\xeb\\\"\\x84\\xbc\\x7f\\x1c\\x1d\\xbd\\xdd;$;\\xb2\\x04==\\x10\\xb5J\\xbc\\x12\\x82h<\\x8a\\xb44<\\xbct\\x0c=\\x8fz\\xff<\\xf6\\x15{=\\xdfrb<\\x06\\xc9f\\xbd\\xe17k\\xbd\\xfcd\\xff\\xbb\\x81\\xf3*\\xbbF\\xb9e\\xbc:0-=\\xfb{\\xc7\\xbcb\\x87\\x1c<Y\\xf2\\xea<i3\\x98\\xbc\\x1a\\xe6\\x89;9\\xa3\\xc3<\\x0e\\x95\\xd6\\xbc*=\\xc9<\\xa3C\\x7f\\xbd\\x0f\\x13\\xd0\\xbc\\x06[\\x92\\xbd\\xd5;\\x9a\\xbb\\xcaZ;\\xbd\\xa3\\xb59<G\\xe9]\\xbc\\xbcy\\x0c\\xbd\\x0cn\\xa4<\\xa0\\x7f\\n\\xbcs\\xb8\\xcb<\\xcd\\xe9\\xd2\\xbb\\xc5j\\xa9\\xbd\\x84\\xbb\\xb2\\xbcg ,\\xbd\\xa9{\\xab<,t\\x9f=!\\xcd$\\xbc=\\xc4\\xc7:\\x89g\\x08\\xbb\\xc0\\xa9u\\xbd\\xbc\\x89\\x8a\\xbb\\x886\\xcd\\xba{a\\xb2\\xbc\\xd4\\xde@=\\x1e\\xd97\\xbd*\\xa1\\xdc<\\xc3x\\xa3\\xbd\\x8a\\\"\\xdd\\xbc\\xe3\\x18M\\xbc\\x02\\x1f\\x0e<\\x8e\\x17\\x84<p\\xfa!\\xbd\\xc0\\xfc\\xb1=\\x06\\xe9\\x87<\\xab\\x99t\\xbcNNc\\xbdu@\\xd8<_\\x84\\x11\\xbb/p\\xf2\\xbb\\xac\\xb4m\\xbb\\xed\\xcf\\x82=\\xae\\xed\\xad;\\xdd\\xfa0\\xbd\\x9fi%=+\\x89\\xfc<\\x9a\\xaf\\x1b\\xbd\\xc1M}\\xbc\\x85\\xc5\\xc7\\xbc_\\\"<\\xbd^\\x05\\x9d=\\xbbS\\x84;]\\xb0\\x17<;9t;4\\x95\\t<\\xba\\x88\\x9a<\\xcaA8=\\x01\\xb6\\t=\\xdcC\\x10\\xbczj\\x86=\\\"\\xb0%<\\xde\\xe3O\\xbcM\\xef\\xde\\xbc*\\xc2\\x0e;k\\x0c\\x88<\\xdaPY\\xbd\\x97\\xa9(=\\xf1~V<\\x01\\xa9H=\\x00PO\\xbdf\\n\\x8f\\xbd\\xd58\\x9c\\xbd\\x12\\xf6\\x9b=.\\xa2\\xd8\\xbc\\xac\\xf3\\x87\\xbd\\x96H\\xd2\\xbdc\\xf3y\\xbb\\xd26\\x07=^\\xe37<;\\x99\\xe8\\xbb\\xb3\\xdd\\xc6\\xbb\\xc9]\\xc1<\\xf7b\\xbf\\xbcLf\\x83\\xbc\\x99\\xd9\\xcb\\xbc\\xdf\\xab\\xca<\\xba\\xeb\\x9e\\xbc\\xf0\\xd1\\xac\\xbb\\xfa\\xfd\\x8f;\\xb9rZ<\\xe6\\xc5S=\\x17\\xbd>=\\x15\\xfe-\\xbd\\xde\\xe8\\x9a<\\x87b\\x1a\\xbd\\xe5\\xc2\\x96\\xbc\\xacnq<\\xed\\xd4@\\xbd\\x17X\\x1c\\xbd\\x7f\\x1e\\xad<D\\x04X\\xbcpG\\xc3<\\x82DZ\\xbdj\\xc4L\\xbb\\xe9\\xbe\\x8b\\xbd\\x96\\xa0T=\\xf3\\x7f\\x1b\\xbcC4\\xd8\\xba]\\x9e\\x89;\\xaa\\xaa-<\\xab\\x172=l\\xab\\x9d\\xbb\\xf9\\x9c\\x02<\\xc6V\\xa5\\xbd\\x1dX\\xa2\\xba*vq=\\xde\\x87w<\\xf7\\xadl=\\xbb-A=\\x9e\\xd0\\xc2<\\xe9,^=\\xbc\\r\\x11\\xbd\\x81\\x1a\\x07=\\x90\\xed\\xd8;t\\xa5\\x0e\\xbd8\\xac\\xc1\\xbc\\xe0\\xa0\\x7f=\\xec\\xa0^\\xbd\\x18\\x81H\\xbcgb\\x00\\xbc\\xe6\\r\\xd3\\xbc\\xf6\\xc6\\xa3\\xbc\\xfd\\xcf\\x95=\\x1c\\x14<<*\\x99\\x1d\\xbd9\\xaa:\\xbd8II\\xbd\\xcb\\xb0\\xad;2\\x8f\\xca=\\x8d\\n\\x1e\\xbc\\x80\\xf9\\xe0\\xbc^=+=]\\xf6\\xc0\\xba\\x18\\xcb[<Z\\x01\\x11<\\xde\\x13\\xd4\\xbc\\xcc.\\xd8<\\x05\\xd6d\\xbdg\\xc1)\\xbc\\xb3;\\xdb<\\xa0^i:/T\\x01=\\x1a4h\\xbdA\\x80\\x02\\xbb\\xd4P\\xd4;4\\x93\\x89=u\\xfa\\xef\\xbbS\\x0c\\x84\\xbcO\\xdc\\xe6<\\xf0\\x0bs\\xbb\\xf1\\xe03\\xbc7\\x03\\xd7\\xbd}\\x80\\x0b=/\\xb0\\xcf;\\x8e\\xf2\\x12\\xbd\\x86\\xd1\\x08=M\\xcd\\xbe\\xbc\\xe7\\x85\\x83;\\xbc+\\xfe\\xbcGl\\x01;\\xd2\\xcc\\x01\\xbc\\xa0c|<\\xc9\\x9b\\x8f\\xbc\\xc0a\\xc1\\xbc\\xbaS\\xae<\\xbe.\\\"=\\xbb\\xc4\\x84\\xbc\\x98\\xab\\x90<u\\xddm=-\\xb3\\x87\\xbb\\xc8i\\x13=A\\xdf\\xc7=\\xad\\x9a\\x85\\xbc!\\xd3~\\xbd\\x00\\x031\\xbd6\\xe3\\xd6\\xbc<?\\xfc\\xbc\\xe3\\xec\\x8a<\\x84\\x00\\xe0\\xbc\\x14\\x07\\xed\\xbc\\xd3\\xe2D=\\\\\\xf5\\xc3<\\xe7\\xadE\\xbc:\\x05\\xe1\\xbc\\xfe9\\xd0\\xbb\\xeb\\xab\\xd1\\xbcWp\\x8a:\\xc3\\xe8\\x13\\xbc\\x87\\xae\\x81\\xbd\\xfd\\xc2\\xb3<`\\x18\\xc7\\xbcC\\xd5\\x87=\\x98[W;f\\x0f|\\xbd\\x0b~\\r\\xbd\\xb7`\\xe0\\xbc\\xdc\\xe7\\xaa\\xbc&\\xb7\\x99;\\xc8\\xaf^<\\x1b\\xb4$\\xbc\\xad\\xfb)=\\xd9 \\x90<\\xf4\\xafN\\xbc=\\x88\\x81\\xba{\\x9e0\\xbb\\x04\\x12\\x87=x\\x0e\\x8a\\xbc\\xb2\\xd1\\xc2\\xbb\\xc3\\xea\\x88=i\\xab\\x12<-U#=\\xc8\\xb91=\\t\\xf26<\\x07\\xf6\\t\\xbdt\\x8aO\\xbc\\xda\\x90\\xa6=\\xc1\\x1a5;\\xf6M\\x86\\xbd\\x91\\x9b\\x0e\\xbdI\\xee\\x00\\xbbR\\x89(\\xbdi\\xb6\\xe9\\xbc\\t|\\xa8\\xbd|j\\x8e=\\x01nA<\\xae(h\\xbc\\xe4E\\xa6=s\\xd8\\xa8=\\xc6\\x0e\\xa2<\\xe7s2\\xbc\\x14Qy\\xbb$D\\x03\\xbd` \\x89\\xbb\\xb0\\xae7=\\x91P\\xcb:3\\xff\\x1d=\\x04J\\x04=\\xba\\xccZ<Fq\\xf3<\\xfaA1\\xbd\\xf9G\\x93\\xbd]\\xca\\x00<\\x0eu\\xa1\\xbc\\xd0\\xc1/\\xbd\\x0c7t\\xbd\\x10o\\xfb\\xba!\\xb8\\x04<\\t\\xa3\\x8b\\xbdB\\xbb5=\\xdb\\x9a\\xf3;\\x83\\xb1\\xa2\\xbc\\x00PA=\\xa3\\x1d\\xf9\\xbb(\\xbb)\\xbd-\\xa3\\x8a=\\xb9\\xd0\\x96=\\xfd&+\\xbd\\x92\\x14\\xae;!/\\xad<\\x888\\x81\\xbd+\\\"\\xa2<\\xbd\\xab\\x0b=4q\\xac\\xbdu\\x8c\\xe9\\xbc\\x0f\\xa3\\xd4\\xbcF1-=\\xdd\\x05\\r=\\x90H\\xd0<\\xb7\\xed\\xdc\\xbc\\xf0\\x17\\xa5=\\x17:\\r<i\\\\\\xbe;\\xaeo\\x1a=\\x89\\xe5G=XN\\xf2:\\xb1Dr=O\\xe5\\x08\\xbd\\xba\\x06\\x83<\\x88\\xc5\\x9c\\xbb\\xa2M\\xe2<\\xf9\\xe1\\xb2\\xbd\\xb6\\xdcN<{\\xb7\\x17\\xbd\\xa8\\xb9\\x89;\\xe2\\xadA\\xbc\\xd5jT\\xbbOc\\\"=i\\xb8|<y\\x8f\\xd5\\xbc{\\xd6\\xa2\\xbcs^%=+\\xa2\\xdb\\xbc-\\xc7 \\xbd\\xe9D\\x8e\\xbb\\xe3\\x7f\\xa1\\xbc:\\x07\\xe3\\xbc\\xba\\x16M\\xbd\\xa5\\xc2\\x92\\xbb\\x84\\x1ab=\\x92k\\x03=\\xbdy\\xd0<Oe8=\\xb5g\\x0b=\\x12x\\xba\\xbb\\x90\\xc6\\x05\\xbd\\x0f\\nv;\\xdfR\\x1e\\xbds\\x93A\\xbc\\xfaw\\x9a<\\x08\\xbe><\\xdf\\x10\\x95\\xbc_\\\\\\xfb\\xbc\\xb9@\\xa7<\\x92\\xd8s\\xbd)V\\xc7\\xbdk5\\x93\\xbc\\xdc}\\xd9\\xbcOs\\xb2<Y1P<\\xa6\\n\\x0e<W?\\x1f\\xbd\\x83\\xbe\\xcf;\\xee\\xe2\\xa2=\\xa3\\xb5\\x8f=\\xe2\\xf1\\x1d\\xbdh\\xf4\\x0b=\\xf8\\xd1!\\xbd\\x80uu;\\x1a_\\x01=]\\x11\\x8d\\xbc\\xa0\\x9d\\x90\\xbc\\xd4.*\\xbd\\x86\\xee\\xcf<\\x04\\xfb\\xaf\\xbc\\xbd\\xa62=%\\x9e6\\xbd\\x94P\\x01\\xbb\\x81\\x02-<\\x17O\\xe2<o\\x0c\\x9c\\xbc\\xda3\\x86\\xbc\\xf5\\xe7Z\\xbc\\xb2+\\x99\\xbc\\xaa\\xe4N<n\\xa1\\x1a<4G\\x87\\xbc\\xbc\\x94\\xd7<\\xec\\x80\\x03\\xbdH%\\xc8\\xbd\\x02d\\xfc\\xbb\\xb3RV\\xbc\\xa7\\nr\\xbc\\x92\\x9c\\xed;PW\\xfc\\xbcr\\x83\\\\<L\\xe4J=\\x9b\\x80l\\xbcy\\xbb9\\t\\x10\\x17A\\xbc\\x0c/\\xcc\\xbcJ\\xe3(=,b\\x01;f\\xae5<\\x9dyL=\\x1c\\\\w\\xbb:_\\xf6<\\xb7\\x9d);\\x97\\x1dA\\xbc[Za=\\xc4\\xd5\\x0c=JBe\\xbc\\x19D\\x85=\\x00\\xf6\\x1d\\xbd\\xd1\\xda =\\xb1%\\xe4\\xbc\\x8d)s\\xbc\\x05\\xd8\\x9a;\\x9e\\xa5_=\\x05\\xe9R<F\\x89%=a_\\x01=\\xb6\\x04\\xab<A$\\xc1\\xbc\\xae\\x0bP\\xbc]\\x8b\\x80<\\xe3\\\\\\xb2\\xbc\\x10\\xca\\xe4\\xbc\\\"yP<\\x94\\xff\\xa2<\\xf5 \\x9f<bc\\x8a\\xbc]pQ\\xbd^^P\\xbd\\xe1\\xdeJ\\xbd\\xc4E\\xff<\\xd5i\\x11<\\x98{s=\\xb2yN\\xbc\\r\\xd5\\xb1<\\xae\\t\\xe0\\xbb\\xab\\xbd\\xc9<;\\xdc\\x849\\xa2\\x0c\\xad<\\xec[\\x14=\\xf9\\xe9\\xc9=\\x12h\\xbf\\xbbl{\\x96\\xbc\\xf1>\\xd2\\xbc\\x88\\\"{\\xbd$\\xb3\\x1a\\xbd\\x88\\x0ey=\\xcd\\xb6\\xf0:\\x80\\x1eY<\\xc9\\x86\\xc6<\\x17\\x14\\x8a\\xbc;\\xd1v=]Ud\\xbcn\\xe5\\x10<\\x01\\x07\\x12\\xbb)\\xe0\\x8c\\xbbS\\x0e^;0^O=|n\\xd9\\xbb\\xa0\\x8c@\\xbd\\xcf\\xd8\\xd6\\xb7t\\x02~\\xbc\\x8f?.=?DT\\xbd\\xc2\\xf1*=\\x13\\xb4\\x9d\\xbc_\\x16\\x0b=\\xdfaf<\\xb5\\x12\\xdb<\\xd3\\x9d\\x8e\\xbcHn\\x19\\xbd\\xa7\\xd1_=\\x88*\\xd3;a\\xbfN=\\xa2\\xec\\x1c\\xbdM\\x02\\xb5\\xbb\\xca\\xd5\\x01\\xbcF4$=\\x99\\x8f\\xee\\xba:\\xb2j<\\xb8\\xf4\\x00=\\xbfC/=A\\xcf\\xb4\\xbb\\x8c\\xa1%\\xbc\\xee%\\x08<Z\\x9f\\xa0;\\xff\\xe8\\xb8=\\x856*\\xba3\\xfd\\xbb<y\\x96;=\\xf8\\xe4\\r\\xbd\\xd1O\\x15\\xbd\\xc2\\x95\\xb2\\xbc\\x04\\xb1Q\\xbal/\\xfe\\xbc\\x04_I\\xbd$C2<\\xc0M\\xc2\\xbc\\x85C\\x1e<u\\xc5\\x1d;a\\x05\\x15\\xbc`_\\xcb\\xbb\\x01\\x92\\x84\\xbd{7S\\xbd\\x07\\x15\\x10=\\x9d\\x87\\x97\\xbb\\xfb\\x05o\\xbd\\xaa\\x95\\x1e=\\xe0w\\x93=\\xa2B\\x8d<\\x08\\xc1\\x95=\\x1b\\xb4%\\xbd\\x00\\x8f/<y\\x99\\xa0\\xbdNlS=\\xc6\\xb3\\xeb;P\\xbc\\x93=\\xef\\xab[\\xbcUQ\\x1d=\\xdf\\x05\\xb7\\xbc\\xd8\\x1f\\xae\\xbb\\xea\\x0ch\\xbd\\xc6\\xbb<=\\xf6\\x02o\\xbdQ\\xe41\\xbc\\xf6u\\x93\\xbdo\\xae9=\\xf7\\xa7\\xb1\\xbc\\x8e\\xc9\\x90=bSO<\\\"\\xf6,=\\xb3\\x98h\\xbd\\xce\\xd8\\xce;x.6=I\\xe0\\xae\\xbc\\xde~=\\xbd?m\\x16\\xbddk\\xb4<\\n\\xc3\\x97\\xbc[\\x86\\xea<\\xdfL\\x8d<\\x827\\x9c;\\x9a\\x94G<\\xd0\\xe7C<yW\\x86<\\x1aw\\xb4<5\\xcd\\x1d\\xbb\\xb9@\\xd1<\\x14!1=\\xfe,\\x95<\\xe5\\x970\\xbd@\\x9e\\x0c<x)\\xaa<\\x9eaJ=\\x89\\xa4\\xb0\\xbc\\x91\\xc9\\x9d=\\xe1A\\x9d<\\x0c\\xc6\\x1d\\xbb\\xb5\\x14\\x00\\xbc\\xfc\\xba\\xbb\\xbc\\x01\\xd6G\\xbd(\\x80\\xed<\\xbd\\x17\\xb5=\\x02\\x1e\\xce<\\xfa\\xcb\\x87\\xbc\\x19r\\xb5;F4\\x81\\xbb\\xa0\\\\\\x8d<\\xaf s<\\xb1\\xda\\x85=Y\\x944\\xbcS\\xe1\\x8a\\xbcm\\x14\\xe9<\\xc0\\x19/=\\x1cI\\x92\\xbc[j\\xbc\\xbd~j\\xbf<\\xfa\\x07\\xbe\\xbcU\\xbb\\n\\xbdF7\\\"\\xbd\\x87|\\x07\\xbc\\xa4j;\\xbdy\\xb66==/\\xc2<\\xef`\\x0e\\xbd\\xcd\\xc7G<M\\xbf\\xb4\\xbd\\xff\\xc7\\x0e\\xbd\\xdb\\xa2\\x0b\\xbdO(8\\xbd\\x0f\\xd3S\\xbd\\xff\\x86\\xd9<lm\\x03<\\xcc\\x87\\x88\\xbd~1\\x19\\xbde\\xe1\\x87\\xbcs\\xa5z\\xbc[\\x8c\\x86\\xbd\\xf2jf\\xbb\\xac\\x8d\\xe6\\xbcQ\\xc7\\x94<*Z\\x86\\xbc\\xa8t/<\\x04$.\\xbd\\xf4\\xd3B<\\xf9\\xc5[<\\x1f\\x05\\x0f=\\x87C\\x8a\\xbc\\xd9#\\x87\\xbd\\x92\\n\\xfa;\\xbe\\xb8\\xfa;@\\xacm\\xbc<\\x8a\\xd1\\xbc\\xbdf;\\xbd\\x179\\xf4;T<n\\xbd^\\x8a\\xa5\\xbbse\\x84=Q\\x99\\x8a=\\xcc(\\xdb\\xbc\\xf2\\xfb\\xce<\\xac\\xbeY\\xbdVT=\\xbd\\x18\\xf13=s\\xa5U\\xbc2\\x9f\\x8b<Z\\x03\\xb3\\xbcX\\xd3\\xd5;\\xdb\\x0c\\xc4<\\x8e\\xf1\\x92\\xbd\\x0b\\xb4\\x81<H\\xe4o\\xbb\\xef\\xe5\\x8d<b\\xda\\x0e\\xbd\\xf6\\xf9\\x18<t\\xe6(=\\xfar\\x19;\\xeaF\\x93<\\x07\\xc4\\xe8\\xbc\\xdeR,\\xbb\\xa2\\xab\\xe6\\xbc\\xaf\\xa6}:\\x90$\\x95\\xbdpE\\xb8;DY\\x87<\\x08b\\x13\\xbc\\x14\\xba\\xa7;\\xa31\\x02=\\xe9\\xb8B\\xbc\\xbfo\\x02=\\x8cK\\xb4\\xbcy\\xafC\\xbcB6\\xed\\xbc\\x02\\x98\\x03\\xbd\\xab@\\xa5<\\xf2?\\\\<VU#<\\x19\\x96,<\\xe3\\xb6\\x91=; \\x81\\xbc\\n\\xc8\\x00=\\xbc\\x19w=\\xef\\x0e\\r\\xbd\\xd3\\xdb\\xab<k\\xa3\\x87\\xbc\\xfb\\xb1b=p\\xe1`\\xbd\"\nHSET bikes:10042  model 'Enterprise' brand 'Bicyk' price 708 type 'Road bikes' material 'full-carbon' weight 15.0 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\xd3 \\xf1:\\xae~\\xb8\\xbc\\x07$\\xcc\\xbcU}\\x01<\\xa7tc\\xbb\\xa6f\\x91<\\xc8\\xdb\\xca\\xbc\\xa7kE\\xbcj\\xe5\\x80=\\xd2/&=/\\x8c\\x0b=\\xe4\\xf5\\xfa<\\x87\\x0b\\x91<8=\\x8d\\xbc.h\\xc2=y\\x0b\\x93;Wf\\xcd;\\x94\\x18w\\xbc\\xf3\\xa3{=\\x83\\xb5%\\xbd5\\xfb\\xbf\\xbb1}\\x93\\xbc\\x06\\xa3\\x0f=\\x93\\xac^\\xbdC\\xea\\x9c\\xb9A\\xd0\\xad;.j,=\\xd6\\x99\\xd7\\xbc\\x81xQ\\xbc\\x90\\xb7\\xd7=(\\x1a\\x8c\\xbc\\xad\\x14\\xd9\\xbc\\xa3 1<\\x1a\\xc4\\x8a\\xbb\\xdd\\xd2\\x15\\xbd\\x1e)<=\\xb1(3\\xbb\\xe7\\xa5W\\xbc(\\xdf\\xfb\\xbcbXg<G\\x8a\\xa7=\\\\]\\xf2;\\x82\\xd2\\xed<\\xe4\\x1f\\xd0<_(k\\xbcL\\xb4j\\xbc\\x19T\\xf0=#\\xe7\\xbe\\xbc\\xf8\\x15\\x85\\xbbk\\x95\\x7f\\xbc\\xa8\\x8e\\x90\\xbc\\xba\\xa6M\\xbd3u\\xa7\\xbc5\\x92G<KY,\\xbd\\xef\\x02*\\xbd\\x81\\x1f\\x8d\\xbb\\xdd\\x1e\\xf8\\xbb>\\xb8\\x8b\\xbdf\\xca\\x88=6\\xf7\\x12=\\xe5\\xae\\xfe\\xbc\\x9c\\xb8\\xa1<\\xb8\\x1eT;\\xa3\\xdc\\x8a<\\xa5\\x8a`<7R0<\\xcaI\\x06=;6(<\\xd3\\x0b\\x99\\xbc\\x1eD\\xac<\\xe7\\xa9{<\\xb8\\xe2\\xe7\\xbc\\xab\\xa3w=\\x85Z\\xd4\\xbc\\x1c\\x08\\xf2;\\xa1\\xfeb<\\xe3FN\\xbd\\xe3\\xf1\\xa3\\xbd\\xde,\\x17=\\xcd\\xab3=xD\\x16\\xbd\\xba\\xf0Q;\\x8c\\xe9\\xcf\\xbcG-\\x85\\xbc\\x87\\xee\\xd2\\xbc\\x05<.<l\\xc8\\xfc<~\\x9d\\xc0<\\x19\\xdb\\xfc;\\xff*\\x04\\xbd\\xa1z\\xfe\\xbcx4m\\xbdj\\x1e-;\\xa2\\xe1n=)/\\xa4<\\xd4=\\x13\\xbd\\xbf\\xb6\\x92<\\xd39o<\\xf0U7\\xbd\\xa8\\xa05\\xbal\\xa8\\x83<\\xd4\\x048<\\x88\\x83\\xc1\\xbc\\xaai\\xa7;j/\\xf9=m\\xa7\\x80\\xbc]\\xc9\\xf0<\\x06HB\\xbcO\\x85\\xc9\\xbb\\x16\\xdb\\xd9\\xbc9\\x90\\x87=S4\\x08=\\xe9\\xca\\xe8;u\\x13\\xa0\\xbc\\xfc\\x84\\xc6\\xbd\\xb1<\\xce\\xbc8\\xe6\\xf7<\\xe1\\x0f\\x05\\xbc*)&\\xbc\\xf2Q\\x8d<gB\\xe2\\xbc\\xc1\\xcb\\xa6\\xbc\\xbf\\xbeU\\xbd\\x17\\x9d\\x82<F\\xc5\\xdf\\xba\\x94\\\\\\xc2\\xbb\\x89\\x8a\\x81\\xbd\\xeb\\x19)\\xbdk\\x014\\xbdj\\xc9\\xcd\\xbc\\x1ef(=\\x8c\\x01\\xbc\\xbch\\xbf\\\\\\xbd\\x11\\x18\\r=\\\"\\xa89=\\xc6\\xd4\\x11\\xba2hG\\xbc\\xea8q<\\x86\\xf5\\xa3<.\\x0eG\\xbcB\\xd0\\xa7\\xbd\\xf7\\x05\\xa3<]\\xf6\\xbd=#1O=\\x9fV\\xa0\\xbcG\\xd8\\t=o\\xcf0;\\xba03\\xbd\\x14\\xffi<Mh\\x17=\\x8fR\\\"=\\xa78\\x0f\\xbd\\xeeP\\xe2<3\\xaew\\xbdGO(\\xbc\\xce\\\\n<4h\\xf6\\xbcYv\\x0c=\\x1e\\xf3k\\xbdjy\\x06=\\xc8\\x8cs=\\xab\\x97\\xa0\\xbb\\x13\\xbf\\xba\\xbcK:\\xd5<ca/=\\xd6\\\\\\xb3\\xba\\xd7\\xac\\x07=\\xad\\x90\\x15=]\\n\\xae\\xbc\\xbe|]<\\\"\\x12\\xae<z\\xf2\\xe0;\\xbf\\x97.\\xbd\\x03T\\x9c\\xbcf\\xe2\\xc6\\xbb \\x11$\\xbd\\xf5\\xdc2=\\x9e\\xf2\\x8d\\xbc1\\xca\\xbe\\xbb\\xb8\\x0b\\x1a\\xbd\\xd5Ak\\xbaz\\xc6><\\x1aB\\xeb\\xbb\\xd0\\x9cU=;\\xe6;=0\\x1e\\xa7<-1\\xa8\\xbc\\xd5\\xb3&;##\\x02\\xbb\\x9d\\x0f:={\\xccc=r3F\\xbd2\\xf0\\xe4<\\xedi\\x9a<\\xfdi\\x0c=\\x82\\x9fN\\xbd\\\"\\xdc:\\xbda\\x18\\x8d\\xbd\\x08D$=\\xc9\\xd0\\x10\\xbc\\xa78\\x99\\xbd*\\\"\\xb7<\\xaax!\\xbc\\x80>\\x13<d\\xf5\\x0b=M\\xa3\\xa5<\\xb6\\xcc\\x82\\xbdx~\\t<A\\xc2\\xaf\\xbcx\\xf9,\\xbd[\\xfd\\x1c\\xbd\\xae\\xaf\\xa6=\\x871\\x8b<\\x1a\\xbb\\x1d\\xbb`G\\xc3<|\\x94\\x9e\\xbc\\xfd\\xea\\xe7\\xbc\\xce\\xa3\\x1a\\xbd\\xd63\\xbc\\xbd\\xe2j\\x80<\\x18\\x8b\\xbc\\xba\\xb7KQ\\xbdD\\xf40\\xbc>O$\\xbd5\\xd2\\xa4;<\\xa8\\xb7<\\x9f`\\xcb\\xbc\\x87\\xbb8=\\xf5\\xb0\\xc2\\xbc\\x02\\x1b\\xd5\\xbc,\\x9b`\\xbd\\xabHI\\xbc\\xa7\\x1dK=\\xb0\\x1b\\xdf\\xbc\\x852y=p\\xcb\\xad<\\xe9u\\x18=\\xe3A\\x86<w$9<\\xb7~\\x10\\xbd\\x15o\\x8f;\\x15w==\\x1aX\\xa7\\xbb\\xf3\\xc2{;\\xa5\\x9f{=\\x10C\\xbf\\xbc\\x19P\\x1e\\xbeS\\xd1P\\xbd\\x1fl\\x94<\\x8f\\n\\x89\\xbbJ\\n\\x16<\\xc3\\xbe\\xa3\\xbc\\t\\x02K<\\x84\\x1b\\x8c\\xbd\\x83\\x16\\x89=:\\xb3/\\xbd\\x9a\\xdd\\x02\\xbc\\x0cs\\\\\\xbcB\\x84\\xcd=jA\\x82\\xbb\\xe3\\x8a\\x19\\xbc?\\x01\\x92\\xbc\\x932\\xf2\\xbcvn\\xac\\xbb*Ec=\\xe9Q\\x04\\xbdt\\x8f\\x0f\\xbdb9\\xa6\\xbbR\\x06\\x0b<\\x9d\\x14c<ys9\\xbc\\x14\\x15\\xcb<\\x18\\xea\\x0c<r\\xc6\\xb0\\xbc\\xe7\\x80\\xc0\\xbbO5\\xf7\\xbc\\x84!\\xd7<<\\xd0\\xf0=\\x02.\\xb3\\xbd\\n\\xa2$\\xbd\\x9cY\\x07\\xbd7\\xbcn\\xbd\\xc2L_\\xbc\\xcbR==\\xf4\\x81\\xf2;\\x90\\x10)=\\xaf\\x87\\xcb<\\\"\\xc6G\\xbd&$$;J%\\x02=g\\xc6\\xa4\\xbc\\xa1\\xdcw<\\xe1\\x05k<\\xaa~\\x00=\\x11P=\\xbd\\xd2\\r;=Rcy=\\x9b\\x94K\\xbb\\xb2\\xee\\xf9\\xbb\\xd7\\x84\\x17=[\\xb4A\\xbc\\x93j\\x19=\\xcf\\xa0x\\xbd\\xd7\\xfb\\x0f<\\xf5\\xf94\\xbc\\x8b\\xec\\x84\\xbc\\x0b\\x90\\x86<c\\xe2\\xb1<8\\xcf.\\xbd\\n\\x93>=\\xce\\x88K\\xbd\\xe6\\xa2\\xde\\xbc2\\xeaL\\xbc>\\x16#=?T\\xf8\\xbc\\xa1\\xb5\\t\\xbd\\xbe\\x18\\x93=\\x8e?Q=\\xf4\\x0e\\x1b;\\xe7\\x83\\xac\\xbc\\x9d\\xcbY=\\xae\\x91\\x11\\xbb\\x1d!\\xf1<`\\xe2\\x16=\\xce\\x13G\\xbd\\xd7\\x00\\xc5;\\x02\\x8f\\x07=;\\x1f\\x14=\\xf5Vm<\\xd0^\\xba=\\xe2\\xa7\\x9b\\xbd\\x8fE:;\\xec,\\x9d<GSU\\xbcqP\\xad<~\\x88\\x10\\xbd\\xb5-\\xc8<s*J\\xbb\\xb0\\x1a\\xf7<!\\xf1`\\xbb\\x01uh<\\n@\\xd7<d\\xf0\\xce\\xbc\\xd0\\xd7J<\\x90\\x102<)\\x14\\x88=:]\\xde<;\\xf6K=\\x9b\\xc7,=\\xae\\xb5}\\xbc\\\"G\\x14<h\\xd0,=9\\x1em\\xbcw\\x87\\x05\\xbd82$<\\x9fL\\x1d:9\\t\\xc3\\xbdR~\\xc39\\x83\\xb4\\x9a\\xbd\\x82gA\\xbcZ5\\x89<\\x14\\x18\\x9f\\xbcm\\x0ba=X\\xb4\\x8f=\\xb7a\\x1d=_\\x89#\\xbd\\xa0\\rv\\xbc\\x9c\\x90`\\xbds\\xea\\xda\\xbc5\\x9e]=vu>\\xbbd\\x05Z\\xbc\\x1e\\x83p\\xbbY\\xea\\xff<\\x9fF\\xf9<\\xd8be\\xbc\\xfdO\\xcf\\xbb\\xcb\\xc7\\x80<\\x87\\x85\\x1a=(\\x83\\xe0\\xbc\\x82\\t\\x12\\xbd\\xd5*7\\xbd^[\\x05<\\xf7B\\xaa\\xbb\\xe4\\x89-=\\xde<\\\\<\\xe8\\\"w\\xbdw\\xb2\\x81=$\\xed\\x19=\\x17\\x920\\xbd\\x85K6=\\x80\\x014=\\x90\\x89\\x86\\xbc \\x98N\\xbc\\xd0\\x0e\\xe3\\xbc\\xe3\\xd0E\\xbd\\xa1_\\x80<\\x89YN\\xbcp3M\\xbdC\\xae[<{\\xea\\x9b\\xbd\\x07\\xd8\\x05=ud\\t=N\\xcc[\\xbc_\\xa8\\x1b\\xbd\\x97\\xc2:;\\x84\\x95:\\xbd\\x9c5_=\\xfd@^\\xbd8\\xf5\\xea<x\\x03\\t=\\xbb\\x1b#\\xbd\\x0b\\x06\\x89;\\xfaq2;\\xd4\\xb1\\xe1<\\xdf\\x9a\\x8e\\xbc!d\\x90<e\\xb7\\xe8\\xbb\\x1b\\x1d\\xfe\\xbc\\x95k\\x81=W\\xc0M\\xbc\\xb3X\\x06=\\x85=U=\\x00\\x94-<&,p\\xbd\\x89\\x88\\xdb<\\xb7M\\xe0<r\\x91C\\xbc\\x98\\xd2R\\xbc\\xa4?\\xb5<\\xe6\\xd4\\xf4\\xbc\\xb4\\xd5\\\"<%\\x19\\x0f\\xbdg\\xd1\\xc0\\xbcF\\x0ef=\\xb0\\\\\\x06=Ey\\x95=\\x12&M\\xbb-\\xec\\x8c<\\xed;+=\\xbcqn<\\\"e\\x81\\xbc\\xe5b\\x0c\\xbcq\\xdd0<j\\x813\\xbd\\x85N\\x86=T\\xc3\\xe0\\xbc?8-\\xbc\\xb4\\x8bD=9\\xafK\\xbd\\xb6\\xf3)\\xbd\\xe7K\\x89\\xbc\\x9aJ4\\xbd\\xf5\\xcc\\x94<\\xc2\\x94\\xab\\xbb\\xe2\\x13\\x0f<\\x97\\xf7\\xe6\\xbcB(\\xa8\\xbch\\xfd\\r=I\\t\\x05=R9\\xa3\\xbap\\x14 =,\\xd6M\\xbcUsx;\\xf5\\x99\\x80=1a-=\\x9c\\xf5\\x04<\\x81X$\\xbcz\\xd1Y=K\\xe8S\\xbc\\x05;\\x86\\xbc\\xdf#O=QK\\xdf\\xbc=JA\\xbc\\xeb\\xb3o<\\x87\\x8a\\x88\\xbdx\\xa07\\xbd\\x1b;3=X\\x06\\x04\\xbd\\x16|\\xa2<W\\xc0\\x92;;C\\x8b\\xbbS\\x01\\t=\\x01a\\xab\\xbb\\xce\\x98\\x9e\\xbd\\xf8p\\x85\\xbb\\x06\\xb3o\\xbc\\xe0\\xb7M\\xbc\\xac\\x0eF=\\x86\\x8c\\x18\\xbdo\\xad\\xea\\xbc\\xb5\\xf4\\x0e=\\xa6\\xe7\\x90<J}q\\t>d\\x15= zy8_\\xd7\\x04\\xbc\\x822[\\xbc\\n\\xa4\\r\\xbc\\\"(O<\\\\\\xf0\\xa5\\xbc\\xd0n\\x0c\\xbd\\x99\\x98\\xdb<\\xa8=\\xe4\\xbc\\xe5\\x0c\\x01\\xbcN\\x81B;c\\x16\\x91\\xbc*<(=\\xe3Q\\x00=\\x0f\\xce\\xee<\\xca\\x88\\xa6\\xbd\\x8d{%\\xbd\\xf7j\\x9c\\xbb*]\\xaa<j\\xc5\\xb5<\\x0e\\xde%\\xbdIX^;[J\\xb5<`]\\x13:\\x9c\\x8b\\xb0<\\xa6q\\xce\\xbb\\xaf|\\\"\\xba{\\xc3\\xe7\\xbc\\x1bZn\\xbd\\x9d\\xc1f:\\xb6z\\xab<\\xed\\xe4\\xc8\\xba\\xc9EP\\xbd\\x92\\xc9\\xae\\xbc\\xf5oJ=\\xf5\\xcf!=\\x1a!\\xf5<mU\\xe2<\\xb6\\xf8r=L`\\xa3;Y\\x94\\xf4\\xbc\\xe8\\xedE=\\x04\\xe8\\xb5<LHN=m\\xc7)\\xbd\\xf7\\xd2\\xc4=`\\xde\\xf8<\\xa4\\x8b\\x98\\xbc\\xffhr\\xbcSr\\r\\xbdR\\xa9K\\xbd\\\\\\xf8\\x03=\\xad<\\xee\\xbb\\x9fd\\xbe;q\\x97\\xb4<b\\x17h\\xbd\\xe3/\\xe8;\\xb85\\xeb\\xbb\\x97$e=m|\\xc4<D\\xd1\\xec\\xbcQ\\x0f\\xb5\\xbcous<Px\\xa0\\xbcF\\n\\x96\\xbd\\xf9\\xdc6\\xbd\\xb5,\\xff9\\x07\\x00\\x08=\\x92\\xd6\\x9e\\xbcW\\xdb\\x8a;4\\x87\\x90\\xbc\\xb6\\x0f\\x07\\xbc\\xae?g<J\\xed\\xa4<\\xfd\\xa1\\x04<\\x08\\xa8\\x1b=\\x99`\\x07=b.$\\xbc\\xbfS\\\"<\\x0c`\\xef\\xbb\\xffv\\x14\\xbb\\xdd\\xf8u\\xbdx\\xc8h=\\xeb\\xc26\\xbd\\x80\\x90\\x06=pG\\x8f=\\xd3\\xfdY=5\\xce;=)e];\\xf4\\\"\\xbc;\\x1a\\xc3\\xb0\\xbb\\x8e\\xf5r=*\\x12\\xfc::\\x9c\\x08=\\x0c\\xfa\\x1c=\\x840*\\xbc\\x10<\\x8f\\xbb\\xaaK\\xa4<+\\xbf\\x97<:\\x88)\\xbd\\xd0\\\"\\xbd<m\\xc9\\xb3\\xbc\\x04GI\\xbdM\\x1b\\x8a\\xbcg$\\x83\\xbc\\x96\\xdc\\xb1<\\x10u\\xbc\\xbc\\x92P9\\xbd\\xdb\\xd2\\x07=\\xb4\\xc1y=Nk&\\xbd+pr\\xbc\\xa8{G=N\\xde\\x9a<S\\xbaS<\\xab\\xbc\\x86=\\x12f\\x90\\xbb\\xa8\\x17\\x89\\xbd\\xe5e\\xdc\\xbc\\x914\\x08=\\x9d\\xac\\xcd\\xba\\xc8C\\xee\\xba\\xeeP$=\\xa9u)=~8\\x10\\xbc@\\xad\\xb9\\xbc\\x82<a=\\xc0\\xa0I<\\x032\\x91\\xbc8\\x0fc\\xbb\\xc9\\\"\\x06\\xbd9\\x04\\xc7<3\\x9d\\x96\\xbc\\x1aR\\xb0<\\xc2;\\xa9\\xbcY\\xf3\\x1a=\\x1e\\x16\\x91\\xbd\\xbb\\xf4\\x8b\\xbc\\x81\\xabz\\xbd\\x86;\\x90\\xbc\\xf6\\xdd\\x86\\xbd\\xa2\\xf8\\xed<cu\\xe3;\\xab\\xc6\\xd3<\\xe7\\x1d\\xff<(\\x1d\\xa2=79v;QS\\t=\\x94\\xd0[;\\xa2\\xb6\\x93\\xbc\\x85\\x18q\\xbcLyz\\xbd\\xb3\\x8e\\x16=\\xa0\\xb5\\x99\\xbcr\\xfc\\x06<t\\x0c\\x8b\\xbb\\xf0\\x9b\\x14=\\xf7(\\x85<N\\xb8\\xc0<\\xe4\\x8d\\xe2\\xbc\\xd2\\xa8\\x8b<3F\\xff\\xbc+\\xc2\\xfa;\\x9do\\x91\\xbc\\x12\\x16\\x12\\xbd\\xe7\\xcd2\\xbdA\\xaa\\x16\\xbd\\x06\\xc4-\\xbbq\\x8aP\\xbd\\x9a\\xa3\\x91\\xbb5\\xb0V\\xbd\\xa42%\\xbc\\xb0\\xa0\\xe8\\xbc\\x00\\xdc\\x10\\xbc\\x85\\x918=P}r\\xbb\\xacX\\xdf;\\x13\\xa3\\xbf<\\xe4\\xc4]\\xbd\\x0c\\xb8\\xb8\\xbb9\\x91\\xc7\\xbd\\x07\\x050=\\x93H\\x1f\\xbd\\xbe\\xaa\\x02\\xbc\\xd5\\x90&\\xbc[x\\x88\\xbc1\\x8c6\\xbb\\x84\\\"\\xa3\\xba\\xdf\\x0c\\xca\\xbcR\\xe1\\xe1\\xbc\\xce\\xe8\\x00=\\xdb\\xdd\\x84\\xbc\\xd6:F\\xbd\\xf1\\x9c\\x87\\xbd\\xbe\\xe8\\x97\\xbcT\\x91\\xe3<\\x7f\\xe8\\xe0\\xbb^\\xdf \\xbd\\x96X}<L\\x17\\xdc\\xbcl\\x95]\\xbd\\x0c`2\\xbaC\\xda\\x98\\xbd\\xa325\\xbc\\x01\\xad\\x87\\xbc\\xfa\\xb01\\xbcd\\\"\\xb4\\xbc\\xaak\\xfa;\\xda\\xa7\\xf9\\xbc\\xcd\\xad\\xc1<\\xb2\\xf2\\xb7\\xbc[\\xee\\x07=p+\\r\\xbd\\xec\\xfa\\xba\\xbc\\r9\\xd1\\xbb\\xb4R.=\\x90\\xa3\\xe1\\xbc\\xf4Bm\\xbd\\xd0\\xfa/\\xbd\\xa3{\\n<\\x9a|\\x8f\\xbaV?\\x90;\\x80\\x03z=b\\xdb\\xcb<\\\"\\xdd-<\\x8fAu=)\\xc2\\x9d\\xbd\\x888\\x9d\\xbd\\xa4\\xa1\\xca<i\\x81K\\xbc\\xdfk\\xf2:\\x04i\\xae\\xbc\\xa1\\x85#<\\x1f\\xe2\\\"=B\\xbc\\x1f\\xbb\\xf0t\\x15<\\x0c\\xf1\\x1f=\\xc2#A\\xbcQ0\\xd3\\xbc\\xf85D\\xbd\\x06\\x95\\xf0\\xbb\\xad\\x98\\x95=\\xeb\\xf9\\xe3<\\x93L\\x1e\\xbd\\\":\\xea<O\\xed\\x07\\xbdE\\xbc\\xbb<\\xffB$\\xbd\\x1b\\xcb\\x8d9 \\xed\\xff\\xbb\\xa5\\x0b\\xf4\\xbc\\xf4\\x847\\xbbc\\xc35=\\xaa\\xf7%\\xbd\\x900<\\xbdVP\\x82\\xbd\\x98\\x0e\\x04\\xbcxbH\\xbbG^\\x17\\xbds\\x00\\xb4;\\x12\\xea+=\\xef\\x8b\\xd2<\\x9b4==\\xe4\\x9c\\x92=\\xb7\\xa9\\xd3;\\xa4*<=ji\\x97=\\xdd=\\x01\\xbd\\xcd\\xd64\\xbc\\x1f\\xf9\\xcd;\\x99\\x18Z:w\\xac\\x04<\"\nHSET bikes:10043  model 'Gandalf' brand 'nHill' price 1855 type 'Kids mountain bikes' material 'carbon' weight 16.2 description 'Small and powerful, this bike is the best ride for the smallest of tikes. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\xc0:{<Tt\\xc6<S\\xbe\\xb3\\xbcy\\xc7X=P\\xc4\\xab<\\xc2Lo<\\x1f\\x03\\x05\\xbc\\x00^1\\xbd:\\x9c\\x96=\\xa2\\xa7b\\xb9\\xc5r\\x8c\\xbc\\xa0c\\xd8<\\x0e{X=\\xb1\\x8dV;\\xc9\\xf9\\xb5=mu\\x13;\\xe3\\xadI\\xbc\\xf8U\\x8e<V\\x9a2\\xbd\\x8d\\xe3\\xe2\\xbce!\\xac<7\\xca\\xcb\\xbc\\x19\\xb6L=\\xc5p\\x11\\xbd\\xefI\\x0b<\\xd5\\x06\\xda<$\\xe3\\xc1<6\\xb9D\\xb9k~\\xf6\\xbb\\xcbT\\x17=\\xb6\\xe5t9/\\xda\\x92\\xbc\\\"k\\x0b=f\\x87\\xe2<L\\x8f\\x8b=\\xedB\\\\<\\x8b\\xb9\\x9e<\\x7f\\x97>\\xbd\\xe5 j;s\\x8f\\xbf\\xbb\\xe1{(=_Q_=5(O=\\x1c\\xe8^<1\\xdd\\x0e<)!\\x04\\xbdp\\t\\xf3=\\xd2\\xbe\\xa9\\xbc\\xb2\\x14\\xd9\\xbcL\\xd6\\xf3\\xbcvh*\\xbd\\xcd\\xe9f\\xbdj\\xbc\\xa1;{\\xf6\\xe2<\\xfd\\x83\\xd1<(\\xcd\\xb1;\\x87Y2\\xbc.\\x94\\xa0\\xbd&\\x192\\xbdw\\xf3\\x86=l\\xbe\\x0c=(\\x9b\\xf8<\\x92\\x92\\xa1<3\\xe9:=\\xd1:\\xfb<\\x93\\x13\\xfe\\xba\\xebd\\xd5\\xbc\\xb9\\xc1@=\\x13\\xa6\\x05<\\xc4\\xa4\\x07\\xbc\\x86\\xb4\\x05\\xbct^^;\\x8e\\xb6!\\xbd\\x98G~\\xbc\\xbdg\\x8d\\xbc\\x81.@\\xbc<\\xb7\\xb2\\xbb\\xdf\\xee\\r\\xbd\\x84Ee\\xbd\\x02\\xb6\\xa0\\xbc-r\\x02=\\xabT\\xa4\\xbc\\xa3\\xdd\\x81\\xbcY \\xd4\\xbc\\xddL\\xb5\\xbc\\xea\\xe8T\\xbdr\\x93\\x849\\x1a\\xf7\\x0e=SL\\x1f={\\xff\\x96<\\xd4\\x88\\xab;\\xb3\\r\\x95\\xbc\\x83\\xf7\\x80\\xbcuO\\xba\\xbc{@\\xda<\\\"\\xb7\\xaf<x\\xe1\\x98;\\xef\\xfd!\\xbd\\xd3\\xd4\\x9b<E\\xaf\\x81\\xbdA\\x16\\xcd<C\\xcd\\xd9<\\xbf`\\x1f<VG@\\xbd\\xcdQo<\\xf7\\x90\\x99=\\xd3\\xf0\\x04\\xbd\\x10c\\x92<]\\xac\\xac\\xbb\\xbf\\xad\\xe4<\\x9d\\xd3\\xf7<\\xa7\\xa2w=\\x89ug<\\xae\\xefy\\xbd\\xf1^\\x08\\xbd\\x9f\\xce9\\xbd\\xe6*j\\xbc\\xe5\\xf4\\xac;amP=\\x19Z\\xb3<\\x1a\\x98&<3f.<\\x91{\\xbd<\\x8d\\xa9\\x8e;&\\x8b\\x10=\\xbd\\x9a\\x02\\xbd\\xe2\\xc8(=\\xd1I\\\\\\xbd\\xbf\\x0eC\\xbd\\xd6i=\\xbdVr\\x82\\xbcZP^\\xbd\\x15>\\x87<\\xc7\\xf2\\x97\\xbc\\xb2\\x1a\\x8b\\xbc\\xb6\\x0b\\x05=\\xb4\\x9c\\x0e\\xbcJ\\x19\\x1c\\xbb\\x97\\xd42<#\\xcc\\x14\\xbd\\xa3a\\x9c\\xbc\\x98V\\x19\\xbd\\xb3\\x9a\\xed<\\xf9y\\x95=B\\xfb\\xf8\\xb9\\xd5\\x9c=<\\x98\\xaf\\x82:\\x06;\\xc9\\xbc\\x05\\xc7\\xc0\\xba\\xf1\\x80\\x17=\\xe5\\x02R\\xbcr\\xd3S=\\xf4\\xde;\\xbd\\xd4-/=\\n9b\\xbd\\xe0\\x8e\\xe0\\xbc\\x00\\x0e\\x8b\\xbb\\\"xY<k\\xfd\\x1f9\\\"\\xe0T\\xbd\\x17UV=\\xd5\\x93\\x08\\xbd\\x0c/:\\xbcU\\x87X\\xbd\\xe1\\ty\\xbcD\\x1a\\xa2\\xbbz\\xd2\\xbb\\xbc\\x84Y\\x85<\\xa6\\xa0\\x81=\\x8f\\xd6k;\\xb8D\\x85\\xbd\\xca\\x89\\x9d<Q\\\"\\xe0<Z\\xd4\\x1b\\xbd\\xfd;\\xa5\\xbc\\x1678\\xbd\\x88\\x8eh\\xbd\\x92\\x0f\\x89=w~j\\xbbw8\\xb3<=p^\\xbc\\x9b\\xf3\\xd5;j\\xbf\\x8c<\\xbe\\x92\\x0c=\\x8a-&=8L\\x03<\\xe5\\xf52=\\xdb\\x0fJ<\\xc0\\xfb\\x17\\xbb\\x05\\xc7\\xfd\\xbc\\xea\\xb2\\xa1<\\xc6\\xe5\\x8c<\\tl\\x0e\\xbdk\\xb1\\x10=\\xa0\\x1bg<yS\\x86=x\\xad:\\xbd\\x9e\\xee#\\xbd<!\\xa7\\xbd\\xdfG/=sw\\xb3\\xbc\\x9e\\xe3\\x8c\\xbd~7\\x99\\xbd\\x97\\x17\\x99\\xbc\\xd7\\xcb\\xbb\\xbc\\x0f\\xe9\\x16<\\xcc\\x95[\\xbb\\x18\\xf9.\\xbc\\xc8\\xd6\\xda<\\xb7?(\\xbc\\x17\\x91\\r\\xbc\\x0cQ\\xd0\\xba\\xf7\\xbc\\xc8<X\\x8b\\xe0\\xbc\\xae0\\xc6:\\xe8$\\xcb<s@;\\xbc\\t\\xb9\\xaf\\xbc\\xa5K\\xf2<6+?\\xbd\\xb8;\\r:\\xa5\\x1b1\\xbc\\xaa\\xd4[<\\xcc\\x18\\x87;F\\x05\\xe9\\xbc4n\\x04\\xbd\\x04\\xfe\\x9b<?u\\x86\\xbc\\x9c\\xc2\\xb0<Dh{\\xbd~\\xb9\\x05\\xbd\\xbd\\x13\\xa2\\xbd\\x85]\\xfb<\\x10\\xad0<_\\x87\\x0b\\xbc\\xf2\\xde\\xbf\\xbc\\x1b\\xf6\\x84;\\xf9J\\xaa<\\xdf\\xe9P\\xbc\\xb0\\xfd%=1fk\\xbd)\\x8c!\\xbc\\xc6\\xb7\\x87=\\x0c6\\x05<\\xd2\\x04G=\\xe5cu=3\\x1b:<\\x83\\xaa\\xff<\\xbd\\xe0\\xb6\\xbc\\xd0\\xb8`=\\xf9\\xac\\x1e<\\xf3\\x12\\xc3\\xbcI\\x1bz\\xbc\\xa1\\xef\\x9d=\\x17\\x16\\x9d\\xbd\\x96`f\\xbc6v\\x1d\\xbdMl\\xdb\\xbcj^\\x99\\xbc\\xacG\\x90=W\\xf5\\x14\\xbc4\\x0c\\xf5\\xbc\\xeb\\xe2\\xb7\\xbc\\x03b\\xe4\\xbc\\xff\\xc9q:\\xb0\\xdb\\xe4=Tj\\x1a\\xbbrF\\x17\\xbdi\\xd00=\\xea\\xa9\\xc7\\xbcF\\x96\\xc6<kN\\xb1;N\\xef\\xc6\\xbc\\x89\\xaao\\xba\\x1f\\x1d\\x1c\\xbd9=\\xe1;\\x96\\xadm<(\\xa2R\\xbc\\xed\\xb7e=\\xfa\\x0bS\\xbd\\xa2_]\\xbd\\x98\\xdb\\x0c=\\x9d\\xe0\\x08=\\x84\\x98\\x8f<<\\x98\\x1a\\xbc\\xab\\x8a\\xad<\\x8b \\xf9\\xbc\\x99*^;\\xe4\\xac\\xd0\\xbd\\xf9\\x99\\xc1<\\xc3\\x91S<+F\\xca\\xbc\\xb3}\\x8a=\\\"\\x1a\\xc5\\xbc\\xba\\x1aQ\\xbc\\x8eE\\x8a\\xbc\\xb6\\x93\\xa8<u\\x0f\\\"<\\xe3\\xd4\\x87<\\xe6\\xfev\\xbc\\x9a\\x87\\xbc:\\xad\\xd0\\x0c=\\xe1\\xa6\\x19<\\x87\\x99\\x80\\xba\\xd6\\xb5\\xa1<h^\\x8e=-\\x0c\\xcf\\xbc\\xa1\\xc8G<\\x88S\\xd1=\\xb8\\x85\\xe5\\xbb=\\xa4m\\xbdC6\\x04\\xbd\\x93\\x92\\xaf\\xbb\\xdc*\\xfa\\xbc\\xfb\\xc7s<\\xc4\\xa28\\xbd\\xecP%\\xbcy\\xecK=\\xb7\\xf26=8h@;\\x92/\\\"\\xbd\\xe2\\xaa\\x82\\xbc\\x89 n\\xbc\\x8at\\xbd\\xbb\\x13r\\xeb:\\x19\\x1fG\\xbd\\xe0\\xf2\\xda<\\xef<\\x83\\xbd8lH=\\x95:\\xe0\\xbbw7]\\xbc\\x9a\\x0b\\xb0\\xbd\\x96\\x042\\xbc0+\\x83\\xbc\\x85\\x1b\\xe0<\\xa4e\\xa7\\xb9\\xf7\\x1e3\\xbd\\xac7\\x12=\\t\\xf1\\x94<\\xf9\\x9b\\x8d<4\\xfb\\x05\\xbc]V\\xae\\xbc\\x1f\\n\\xd1=|S[;=r\\x9f\\xbc\\xad\\t7=\\x92\\x87 =/\\xd3K=\\x91\\x16 =\\x18\\x19\\xcc\\xbbf=\\xed\\xbc\\x12\\x9b\\x11\\xbd\\xe6Ni=\\xc0\\x8c\\xba\\xbb\\xfa\\xb3\\xa6\\xbd\\xc8\\t\\xf7<\\xafCe\\xbb\\xdec\\xe3\\xbcH\\x8a/\\xbd\\x1d\\xf5\\xe4\\xbd\\x8e\\xb8\\x05=z\\x91\\xb4<\\xd9\\x13\\xbe\\xbc\\xfb\\x92v=ns\\x91=\\x0b\\xce\\x1c=\\x17\\x05\\x12;\\xc9K\\xd6\\xbb7\\xe5B\\xbd\\xeeo\\xdb\\xbc\\xc6\\xc8\\x9b=-\\xb8d<\\xb4O^<B2\\x99<hA\\x06\\xbb\\x9b:\\xbf<\\x1d\\x11\\x15\\xbdv\\x16\\x8a\\xbd\\xf9\\xbf\\n\\xbd\\xe2\\t{\\xbcQ\\x16\\xd0\\xbc,\\xdf\\xa1\\xbd\\xb0\\xd5\\xf3\\xbc?D.\\xbcD\\x1b\\x8b\\xbdU\\x87/=J\\xf08\\xbdrE\\x00\\xbd\\t2[=|\\x95\\x1c\\xbc0\\xcd[\\xbd\\x04\\x08^=\\xc7\\x1ec=\\xd0\\xfe\\xdf\\xbc\\xfa\\x9fD<1\\xbc\\xc3<\\x95\\xea\\x85\\xbd\\xd7p\\xeb;(u\\x97<<G\\xc7\\xbd\\x19\\xc3K;\\xbe\\xe0p\\xbc\\xbc9%=u\\xf5\\x1a=\\xb2\\xa5\\x90:\\xab\\xfa\\x1b\\xbcb\\x88\\x82=i\\\"0\\xbc\\x10\\x91\\r\\xbbh\\xd0\\x8e<\\xc990=lT\\x88<\\xef\\x8b\\x11=p\\xa61\\xbb\\x08\\xa9\\xd9\\xbb\\x9d\\xfa.<\\xee\\x95)<\\\"O\\x80\\xbd?\\xb3\\x19\\xbc\\xff\\xee\\xf1\\xbb\\xa0 Y<VX}\\xbcY-\\x0b=\\x06)\\xf2<H\\xea\\x04<\\xceW\\xbf\\xbc\\xdaZ6\\xbd#\\xc3\\x11=\\xc7b\\xd0\\xbc\\x82>\\x05\\xbd|g\\xee;\\xc5\\x9a\\xd9\\xbc\\x0c\\xf9\\x13\\xbcA1\\xc4\\xbc\\xd924\\xbda\\x00\\xa5=\\xd4i#=\\x9f\\xe2(=\\xa7\\x92\\xa7<sY\\x81=H\\xe12\\xbc\\x95\\xc8\\x95\\xbc\\t\\xfe(;\\xc8\\x05\\x92\\xbc\\xd6\\x89\\xa9;|\\x07\\\\<\\x1f}\\xe8;\\xea\\xc1\\x15\\xbc\\xb17\\xf5\\xbc\\x11i%=\\x067/\\xbd\\xb7\\xad\\xb8\\xbd\\x85\\xb7\\xeb\\xbc\\x8d\\x1f\\x8e\\xbd\\x88\\xbf\\xdf<x\\xc1\\\\\\xbc6\\xb8w<4.\\xd0\\xbc\\x9f\\x91\\x87\\xbci\\xde\\x8d=\\xd5\\xf9\\x9b=\\x08\\t\\x13\\xbd-u9=\\x99.\\xe6\\xbc\\x92#\\xb7<SBc=\\xbd0\\x10\\xbb\\xe0\\xba\\x0b\\xbd\\xda\\x13\\x9f<\\xdf\\x19,=U{\\x02\\xbd\\xd7\\xda\\xd5<Z\\xb3\\x00\\xbd\\x96-\\x8f9\\xfeB\\xb9<N\\xa6\\x9a<(T\\xe5\\xbc01C\\xbc\\xe8\\xe0d\\xbc\\xe0\\xa2\\x03\\xbc\\xca\\xca\\xc2<B\\x05\\xd8<\\x03\\xf0 \\xbd\\xf1_:=\\x1b\\xea\\xfa\\xbc\\x7fB\\xe1\\xbd\\x0e\\xe1\\xc1</\\x96\\x8d\\xbc$\\xa3\\xbc<\\x1b\\x0e!=\\xfeC\\xee\\xbc\\x02\\xb9\\x99<.\\xdf\\x93=\\xb82\\x1b\\xbc\\xb8\\xa7R\\t4\\x94 \\xbcc\\x8c&\\xbb}6.<\\x82\\x07h<v8\\x0f<}\\x0e0=\\x19CK\\xba\\x83eM\\xbc\\xfap\\x00<\\x90\\x0e\\xb4;\\xde\\xa0\\t=\\xd2\\xd8H=\\xeaao\\xba\\xde2\\x8c=\\x04\\x01\\x1b\\xbdx\\xe2\\xd0<h>\\xe9\\xbcvu\\x94\\xbb\\x02I\\xab\\xbc\\x97/4=y\\xfa\\xba<\\x1b\\xa0^\\xbbV\\xe4\\xd4<\\x8cqZ<&C\\xca\\xbc\\x1b\\xf7\\xb2\\xbcxC\\x18=\\xfdC\\x9e\\xbc\\xec\\xbc\\xe1\\xbc\\xb7\\xff\\x14=2\\xd8\\x03<\\xfe\\x1f\\x06=e\\x05\\xec;\\xe8/\\x83\\xbd\\xd7\\xd5\\\\\\xbd\\xe1\\xec\\x8a\\xbc\\x92\\xda\\x14==\\x99#\\xbb&\\x08\\x89=C\\xd6m<\\xc9\\xcf\\x03=\\x10\\x81\\xed;Y*6=\\x91\\x81\\xf3<\\xc8\\xc2\\x0c=\\xb8\\xaa\\x0b=\\x84f\\xe5=i\\xef\\x1a<\\xe3\\x8b\\x86\\xbbix\\n\\xbd2\\x90C\\xbd\\x10\\xc3\\x02\\xbd\\xc1\\xacC=lQ\\x04\\xbcY}\\xbe<W\\xa9\\x1b=S\\xa0+<~RV=7\\xe4];\\x87\\xb6\\x17<c\\xc8\\xd9;\\xd0\\xce\\xcd\\xbb\\x06\\xce\\x87\\xbc\\xf2]\\xcc<\\xfa\\xbc\\x85\\xbdra}\\xbd0\\xaa\\x04\\xbc\\xf6v\\xc3\\xbc\\xe5h\\xd6<;\\x97#\\xbd\\x83\\xc7\\xce<\\xf4Z\\x0c\\xbdQ\\xfe\\x1f=\\xbe\\xca6<\\xd3A3=n\\xfc\\x93\\xbcR)\\x87;\\xcdZ[=\\x8dOC\\xbc\\x15}\\x00=\\x9a\\n\\x04\\xbd\\n\\xf1\\x06<\\x86\\x92\\xf3\\xbc\\xe3@\\x81=\\x837\\x87\\xbc\\xb2\\xc7\\xdc:gji=z\\xb3Z=sc~\\xbc\\x1f\\xf5&\\xbb\\xef\\xd4z\\xbcmN\\x05=\\x17\\x9e\\xe3=\\xff\\x01\\x14\\xba\\xfc\\xbe^=\\xe5\\x0b<=s\\xe1L\\xbd$\\x7fy:\\xce\\x06\\xc6\\xbb$:\\x06<\\xa9i\\x1c\\xbc\\xc5\\xc6\\t\\xbduwJ;\\xb8\\xec@\\xbd2\\x87\\r<\\xbf\\x85\\x1d;\\xc8\\x9c\\x03=\\xb5-\\xbf\\xbc^\\xcfg\\xbd\\xbe\\xec\\x07\\xbdzw\\xbc;\\x05\\xf7;;\\x87\\xa22\\xbdY\\x92<=b\\xd12=\\x80\\x175<1\\xae\\xa2=P\\xbf\\x07\\xbd\\xa1\\x9f\\xc3;b&\\xa2\\xbd\\xb3o,=\\xad\\xba`<\\x99\\xfe\\xa9<}\\x08\\x93\\xbc\\x9a\\r,=\\xdc\\xea\\xc5\\xbcY\\x0eF\\xbcC\\x98$\\xbdX\\xb1%=\\x13\\x8b5\\xbd\\x14T\\xf1\\xbc%X\\x88\\xbd\\xcb\\xfd\\xe9<[\\x10R\\xbc\\xf4h\\x88=\\xeduu\\xbb\\x01\\x82\\xc4<\\xac\\xcf\\x95\\xbdwfX\\xbc\\x97\\xdc\\x8e;h\\r\\x0b\\xbcSb5\\xbd\\xe4\\xae\\r\\xbc\\xb9n\\x14;\\x1e+\\xfd\\xbb9~g:\\x14b\\x00<t\\x0f\\x80:\\xef\\xe5\\xe6<E2\\xd1\\xbb\\\\\\xa3\\xec;q\\x92\\xe2<\\xe6\\xd9\\n:\\\"\\xd0\\xdc<>5\\x0f\\xbc\\x02\\xfa\\xa5<t)9\\xbc\\x0c\\xe7e=\\xb3\\xd5\\xda<D{\\xc7<n\\xc5%\\xbd\\x98C\\x88=T~T<\\xcf\\xb8\\x84<\\xddx\\x80\\xbc\\xe5=\\x0f\\xbd^0o\\xbd\\x078\\xfc;;\\x1d\\x99=\\xbb\\xeb8<L\\x87>\\xbcV\\x00\\x1c\\xbcC\\xba\\xbc<\\xbf\\x89L\\xbcu\\\\f<1\\xb2\\x88=\\x89i.;6r\\xb3<\\x89\\x0b\\x0c=lb\\x06<\\x11\\xcbr\\xbd$\\xf5T\\xbd\\x14\\xef\\x15=\\xf2I\\\"\\xbd\\x8aY\\x9f\\xbc\\\\ \\x85\\xbc\\xb9\\xbcF\\xbc\\xc3R\\x01\\xbd\\xa5cY=\\xb3\\x9e\\x8a<y\\xbd\\xd5\\xbbRi\\xdf<\\x85\\xc7\\xa9\\xbd\\xb4\\x9b\\x8a\\xbc}\\xfb\\x08\\xbdJ\\xc99\\xbd\\xca\\xbb\\xf8\\xbc#U\\x9e\\xba\\xed\\x0b\\x19\\xbd\\x8f\\x90\\xdf\\xbc\\xf2\\xa1\\x93\\xbc\\x00\\x96;<\\x0e<\\xc9\\xbc\\xcb\\x8d\\xa2\\xbdN\\t\\x10\\xbd/\\xf4\\x89\\xbc9\\xb4\\x95<|\\xc7\\x00\\xbb\\xac@1\\xbce\\xa1\\xeb\\xbc`\\x9a\\xf2<A,)\\xbb{d!=`\\xa89\\xbc\\xd1\\xb5\\xfd\\xbc\\x071\\x85;\\x8f\\xac\\xde;\\xad$\\x85\\xbct\\x8a\\x04\\xbd\\xcf\\xe9I\\xbd\\xe2\\x87\\x99<\\xec\\xf5b\\xbd\\xd4\\x81l\\xbc8\\x01\\xa4=A\\xa5\\xa0=\\x97G\\x95\\xbdY\\xdd!;\\xb0\\x1b\\x0b\\xbdT\\xd54\\xbd\\xb2v\\xaa<v\\xa4s\\xbc\\xcb#\\x94<\\xb6^\\xd4\\xbc\\x92\\xff;\\xbb\\xca\\x819=\\xce\\xc0;\\xbd\\xc3!9<0\\xd0\\x9f;\\xe9\\x19L\\xbc\\xdd\\xe7\\xfe<\\xdc%\\xb2<\\x0bFI;9\\xbf\\xac<`\\xeb\\xdf<\\xa70\\x80\\xbcL\\x95\\xef<(B$\\xbd\\xb4\\xe7\\xdb9\\xe4w\\x9b\\xbd\\x82Q\\x8f<\\xc0\\xaa\\x05<\\xf5\\xfe\\x8f\\xbc\\x99\\x15\\x0b\\xbc&\\xd6\\x18=\\xc8x\\xd8\\xbc\\xef\\x06w=\\xa7:\\x04\\xbd\\x9b,\\x87<\\xbd\\xbcf\\xbc\\xba\\xbaS\\xbd]+=\\xbb\\xd2\\xdb\\xef;\\xbd\\xaa\\x82<\\xdb\\xbd\\xc6;l\\x93\\xac=\\x0c\\xabc\\xbc\\xad$\\r=f\\xbap=\\xdb1~\\xbd2\\x15\\xb9<\\x103:\\xbd5_>=\\xe3\\x8fI\\xbd\"\nHSET bikes:10044  model 'Europa' brand 'Tots' price 3391 type 'Commuter bikes' material 'alloy' weight 8.1 description 'This bike is the perfect commuting companion for anyone just looking to get the job done At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xe7\\xd4\\x9d\\xbcd\\xe1\\x8d\\xbb\\xbbi#\\xbcf{\\x06=Mk\\n\\xbdh,\\x9d<\\xe2\\xd2\\x1f\\xbc\\xca\\x0b\\xd0\\xbc\\xfc\\xfds=M5\\xfa<\\xf6\\xd8\\xe4<\\x14\\xfa[<s7X<,\\xff\\xc2;\\x800\\xa0=\\xe3\\xde\\xdd\\xbc7\\xf0}=\\xd5\\x1a(\\xbd3\\xb4\\x18=h!\\xa6\\xbd\\xd4/\\x1a\\xbbIbv\\xbd\\x15\\xf34=\\xc3o#\\xbd\\x13\\xfdY\\xbdt\\xa3o\\xbbd\\xb5\\x88=\\xf1\\xa4\\x08\\xbcX\\xec\\x0f\\xbdl\\xcd\\xec=\\xdf\\xc7\\xc1\\xbc\\x87yZ\\xbdYr\\xb1<)2\\xa4\\xbcQ9j\\xbc\\x9f\\x98\\x1e<\\x9f\\x16}<\\xbb\\xcf\\x16\\xbbd\\xaa\\x1f\\xbd a\\x8e;}x\\xa1=ir\\x81\\xbd\\xfc\\xaf7=\\x9d\\x8b\\xd8:\\xa5\\x82\\xa7\\xbcIJ\\x1b\\xbd\\\"\\xc7\\xf7=\\xdfQF\\xbd`\\x8a\\xd3\\xbc/\\xbf\\xb7<&\\xc1:\\xbd\\xe0Q&\\xbd\\xb2!\\x05\\xbd\\xfc\\x1c4\\xbb\\xcf}\\xa5\\xbc\\x13Y\\x10\\xbd+\\xc0\\xdb\\xbc\\xbd8<\\xbc\\xca\\xb3\\xc0\\xbc\\x91\\x90~=s7*<@%\\xb8<=,\\xa1:}{\\xcb<AZ\\xb3<\\xf6\\xc1\\x87<\\x0b\\x1b9\\xbc\\xc7H\\x15=\\xf8\\xed\\xba<\\x8aT\\x1e\\xbd\\xfa\\x86Z\\xbb\\xdcS\\x0f=\\xbeJ?\\xbc@\\xbfv<4\\x95\\x11\\xbdNX\\xdd\\xbc\\xe7]8<\\xa7\\xc2\\xf5\\xbc\\x18\\xdb}\\xbd\\x08\\xc01<\\xba\\xb8<<\\xcb\\x19@\\xbd;\\x81\\x9e\\xbb\\x9a\\xba[\\xbd.7i<\\x17\\x02\\xc8<\\xc8<\\xc2<\\x90\\x19\\x04<\\x9b\\xe3>=J\\xdf];\\x8b*\\x11\\xbc\\xb4=t\\xbd\\xf9t[\\xbd\\x1a\\xae\\xf8<\\x01\\x9b><WkW<\\x0b`\\x01\\xbd52\\x8a\\xbd\\xd6\\xbd6=\\xff\\x86y\\xbc\\x95u\\x1d8=\\xda\\xd6<\\xeek\\xfe<\\xd3\\x99\\xd7\\xbcAY}\\xbc;\\xb6\\xda=\\xdc\\xbc\\xa0<\\xf7A\\x8f:\\x1d\\x01:\\xbc\\xd3\\x92)\\xbb\\xc3\\xc2\\xe6:3_\\xba\\xbb\\xf2Ft<:\\x05\\x9c\\xbc\\xdb\\xf2l\\xbdz9D\\xbd\\xd3\\x17c\\xbbSK\\xba<\\xa4u\\x0f=)\\x7fd\\xbd\\xc7\\xae\\x01\\xbdj\\x8a\\x1a=\\xbb\\xf3\\x1c\\xbd\\xd1\\xc9\\x12\\xbd\\xfa\\x80\\x1c=\\\\\\x98\\x03\\xbbS\\xc9\\xc9\\xbc\\x90\\xba\\x02\\xbdC+\\x0c\\xbdc\\xc8w\\xbd\\xac\\x16/\\xbdk\\xa4{\\xbbK\\xa2/;\\x01k4\\xbd!\\xd0D<\\xc2F\\x86=O-q\\xbc\\x05\\x83#\\xbd\\x00\\x0c\\xbb;\\x00\\x13=\\xbdt\\xeb,\\xbd\\\"i4\\xbd\\x8f0\\x80:\\xba\\x1f\\xc2=d\\xc4\\x13;IC\\x89\\xbc\\x19\\x14\\xef<\\x90,\\xa1<\\xc5[\\x14\\xbd\\xd1\\x7f!<\\xb31.\\xbcs2\\x84=\\xe5\\x93\\xdc\\xbc<\\xc2s<q\\xdc\\x8c\\xbd\\x0b{\\xa7<\\x90\\\"\\xc1<\\xd0F+\\xbd\\xb9i4=\\x0fT\\x19\\xbd\\x05\\x8b\\xed<\\xfa}O\\xbc\\x1f\\xcfI\\xbc\\xd4\\x8f\\r\\xbd\\xdb\\xba\\xa3<f K=\\x15}\\xd0\\xbc\\xcd>Y;\\x9f\\xb62=\\x1d5_;\\xa1!\\xab8\\x9c\\xa5o=8\\x99m\\xba\\xc2\\x83\\x0e\\xbdM\\xc3\\x9d\\xbc\\xae\\x1f\\x8d<\\x0e\\xe1\\xbf\\xbc\\xbb\\xc4N=\\x9d\\xde\\x84\\xbc \\xfc\\xbb\\xbc\\x94/\\xcb\\xbb\\xa3S\\x0c<O`n\\xbb\\x1ck.\\xbc\\xd2_-=\\xb5\\xaa\\x9d\\xbc\\xb7\\xc9*=\\xb5cX\\xbc\\x8aq\\xc2:\\xf7P.<hRt=\\x8e\\x1aA=\\xc2o\\xa5\\xbc\\x91w\\xb4;\\x83\\xd3j\\xbc\\xc11\\xa3<\\xc4\\xf8S\\xbd\\xff\\\"\\x8f\\xbc\\xf9\\x95:\\xbdhH\\xad<\\x15\\xf3\\x86\\xbcq\\xaf\\x01\\xbd\\x86+\\xea\\xbb9}\\xd2\\xbai\\x1a\\x9a\\xbc\\n\\tU=\\xf2\\r?=<<#\\xbd\\xb5\\xc2\\n=\\xd5\\xcb\\xe9\\xbc\\xf1\\xb7\\xa5\\xbc\\xdf@\\xe7\\xbc\\xe34\\x8b=\\xaa\\xe1\\x8b\\xbcb\\x0c\\x0f\\xbc(<\\xd7;1\\xa9\\xe9\\xbb3\\x17\\x84\\xbd\\xc8z\\xc7<\\x83\\xdf\\xcf\\xbd!\\x00\\xaa\\xbc\\x1c\\x90\\xc7\\xbc\\x12\\x05\\xcc\\xbb\\xbb\\xa9j\\xbbja}\\xbd\\x04F\\xfa\\xbb\\xfd\\xa8\\xb4<\\xf8\\x99\\xf4\\xbc\\xa55\\x82=\\xc8\\xb87\\xbdM\\x83\\xf4\\xbcL\\xc7\\xc4\\xbdc*\\xb4\\xbb0vq=k\\xf2%<\\xcd\\x9b\\xef<\\xa2E\\xdb;\\xbdb\\x83=\\xe6L\\n\\xbc\\xae\\xa5\\xe5<af\\x1a\\xbd\\xd8\\x03\\x00\\xbc\\x91\\xbd\\\"=\\xb5yU;\\xf2,\\xd3<\\x1e\\xfe\\x99=\\x85\\xcf\\xcc\\xb9S\\xa0\\xe5\\xbd\\xf7\\x1c`\\xbd\\xe5\\x1aD;\\xb9\\x11u;\\xde\\x11\\xbd\\xbc\\x96\\x00*<z\\xa0\\x1d\\xbc,\\xae\\xa1\\xbd\\xee\\x14\\x11=):\\xd4\\xbc{-\\xd7<\\xb8\\x88\\xaf\\xbc\\x8a<k=\\x86\\x87\\xf0\\xbcq\\x89\\x8b\\xbbJ\\xcf@\\xbdJ\\xb5B\\xbc\\xe0\\x18\\xce\\xbc#.\\x98=\\xe5\\xe0_\\xbc\\xd0\\xaf\\xc9\\xbc\\xf8;\\xcf<\\xad\\xe0N:R\\xd2\\xd4\\xbb\\xc3\\x04\\x0f;/\\xed\\xce\\xba\\xb2\\xa0\\xb2<\\x13\\xe4\\t\\xbd\\xa8}}\\xbc\\x10\\xa54\\xbdq\\xf9\\xc0\\xb9|[\\x81=\\xcc,\\xb5\\xbd\\x08\\x02N\\xbd\\x13\\x95\\xde\\xbb\\t.\\xd6;\\xd6Xv<z\\xd4\\x0e=QM\\x19\\xbd\\xcb\\x86\\n=\\xa1\\x9a\\xcc<gm\\xa9\\xbd]\\xc0(\\xbc\\x9f\\t\\xb4<Zv!\\xbd\\xca\\xea\\xc2<E\\xc7\\x0e<\\x8a<\\x19=\\xae\\x15B\\xbd\\xf0\\xce\\xcb;\\xa3\\xcc\\x14=;\\x1dt\\xbb\\xd8w\\x02\\xbd\\x02\\x19\\xaa<\\x9e0[<\\x10\\x90\\x98;O\\x13<\\xbd\\xdf\\xff\\xcb;\\x9d\\x1a\\x9a\\xba\\xfa\\xe8\\xd7\\xb9\\x95\\x16P\\xbc\\xfe\\xa9\\xab<2\\xaf\\xdd\\xbc\\\\q\\x0b\\xbb\\x93@K\\xbd\\xe2\\xf1\\xd1\\xbc\\x12QN;jzM<e\\x9a\\xe6\\xbc\\xf0\\xd3k\\xbdN\\xb5n=&\\x0f&=_\\t6\\xbd=\\xba0\\xbd?\\x9fY<\\x98o\\x15\\xbcz\\xf3\\xed\\xbb\\x8e\\x1fL;UAl\\xbd\\xd4\\xc6\\x98<\\xd5\\xecS\\xbc\\xad*\\xe7<\\xdf\\xeb\\xd67K\\xac\\xcb<\\x17h\\xb7\\xbd\\x04\\xd5\\xa7\\xbcf{\\x85\\xbb]\\xce\\x9d\\xbaK7\\xed\\xba}\\xfa%\\xbd\\x0c\\xe2R=\\x90\\x88\\x7f<\\x93S\\x14=-\\xdad\\xbcl\\xcb\\xdc:\\xbe\\xe1 =\\xd8\\x94\\xf3\\xbc\\x84\\\\\\t=Q\\xf4 =?\\xbf\\x81=J\\xe1P=\\x7f\\xbcU=\\xe36$<\\t~\\xa6<^\\x08\\x9d\\xbc\\xd1\\xe6\\x8f=\\x99\\xf0\\x1e\\xbdl!2\\xbcG\\x88\\xf1<\\x1f\\x00\\xff\\xbb\\xadX\\xa2\\xbdp\\x87\\xf7\\xbch\\x97\\xdc\\xbd\\xcc?\\xc0\\xbc\\xf3_\\x96<\\xe4\\xc0\\xfc;\\xa5cE=\\xfaw\\x8c<K\\xef\\x92=\\xd3\\xe9\\x9f\\xbc\\x07\\x95\\n<?5\\x8b\\xbd~P\\xf8\\xbc\\x02\\xe0P=\\x03\\xb6\\xb3<D\\x17\\x8e\\xbb\\xa8\\x02\\x80\\xbc\\xf6\\t\\x06=\\xdf\\x1fw=U$\\x9d\\xbd1\\xe06\\xbcG<\\xca\\xbcg\\xf71=\\xec\\xe2\\x17\\xbd\\xb1m\\xa2\\xbd\\x84\\xf12\\xbcB\\xb2\\x1e<\\xa2Eb\\xbd\\x06\\xa6\\n;\\xea\\xd3\\xc6<w\\xa8\\x05\\xbd\\xf9m\\x99=\\xfc\\xfd\\x1d<\\xfa\\x8a5\\xbd\\xa1\\x8c\\x8f=\\x1e\\xb1\\xc1=Q\\xea\\xbd\\xbb\\xa8\\x91]\\xbc\\xc9>\\xf2\\xbc\\x89\\xfa\\xbc\\xbd\\x10\\xb9\\xb5<O\\xdc\\x98\\xbbZ\\xb3\\x9c\\xbc\\t>\\xf9\\xbb{\\xbfb\\xbd\\xfe\\x9e\\x1b=\\xe6\\x80C\\xbb\\x99R\\x01<\\xf8/\\x8f<\\x0b\\x1e8=\\xa3c%\\xbbtb-=\\t<\\xba\\xbc\\xd0\\x14~=\\xe5kO9\\xf7\\xa5\\\"<~\\xf4\\xfd\\xba(k\\xa6\\xbc\\xa1f\\x1e=\\x82\\x7f\\xac;\\x0f\\x1e2\\xbc\\xdb^\\t<yW\\r\\xbc\\x945W\\xbbc\\xda\\xd4\\xbcc\\x8b\\x86\\xba\\xb6;\\xa1<\\x11]\\x1e<r\\xf5\\xf6\\xbcc\\x19\\x0b\\xbc\\x0f\\xcc\\x16=\\xa1H\\\\;\\x070M;\\x04\\t\\x99<-\\x80\\x18\\xbd\\x9cN\\xca\\xbc\\x8eFs\\xbd\\xd1\\xbaw\\xbcP\\xf9j=\\xe6\\xf8+=\\x80\\x0c\\x0b=\\x8c\\x12\\xc9:\\xa9\\xed\\xf09\\xb0\\x0f\\xd4<\\x11\\xbb\\xaa<\\xb9\\x93.\\xbd\\xb8^\\x82\\xbc\\x93\\x8b\\x81<>\\xce8<9e*=!\\x1b\\x13<u}+\\xbdE3\\x1f=aEK\\xbdD<q\\xbd\\x8d\\x85W\\xbd\\\".\\xdb\\xbc\\xbc\\xaeI=G\\x92\\xf9\\xbb\\x9d\\xa9v\\xbb\\x07H6\\xbdM\\xe6\\x84\\xbd?Q\\x18=\\xa64I=\\xd0\\x95\\xa4\\xbcL\\xef<=\\x16t(;\\x85\\xa4I\\xbc-t\\xd4<\\xdc\\x8c\\xcc<\\x07\\xfe\\xcc\\xbb\\xecc\\\"<\\x05\\x85(=t)\\xdb9H\\\"l<\\xaa\\x046=\\xaa9\\xa0;=\\x1e\\x98;\\x02\\xf22<\\r-\\x00\\xbc\\xfaQ\\xec\\xbc\\xa5\\x8d\\xbf<\\x88M\\x8d\\xbb\\xd6\\x10\\x03=Uo\\xff:\\xf4EI\\xbczS\\x1a=\\x85R\\x81\\xbbY\\xc2\\xd6\\xbd\\xd4\\xf1\\xe1\\xbc\\x80%\\x95\\xba\\xaa\\x93\\xbb\\xbcZ2\\xf9<\\xa6\\xfc\\x84\\xbc\\xf4\\xb7\\xe2\\xbb\\x19\\x95\\x0b=\\xfb\\xf6S<\\xdb\\xb5s\\t$\\x18\\xbc<\\\"@\\x03\\xbd\\x1f%\\x85<\\x95PL\\xbcX\\xa8\\x119\\xa6[\\\"=\\x1c\\x1e\\xfc\\xbc\\x84\\xcd\\xe1\\xbc\\xe8Su=\\xe5e\\xc6\\xbc\\xae\\x1b\\t\\xbdf\\xb1\\xd3;\\x17\\xbd\\xab\\xbc/!h=\\x1a\\x03b\\xbc\\xb4A!=e\\xfd\\x00\\xbd\\xc5\\x83\\xdd\\xbc5\\xaa\\x95\\xbb\\xc9Q\\\\=8r$=\\x99\\xf8{\\xbc\\xa4\\x84\\x8d<\\x1d\\x9b[=nI\\xab;\\x89\\x87\\xa9<\\x13[\\xd2<\\x89x\\x15=Z\\xf5R\\xbdo\\x18\\xe5\\xbcB\\xf5y<R\\xf9\\xf7\\xbb\\xc2P\\xd0\\xbaz\\xbe$\\xbd\\xb5\\xaa\\x92\\xbar\\xca\\xfc;hW\\x8a<* \\x8e<\\xd5\\x1ev=:\\x0b\\xce:\\x15\\x95\\x91\\xbc\\xe8g@\\xbb@WR=\\x0e:W\\xba\\xad\\xec\\xce<\\x9f[\\xcd\\xbcuK\\x08>\\x0b\\xd28\\xbcU\\xc1\\xdc\\xbc\\xdc\\xdb\\x11\\xbc\\xbe\\x97\\x1d\\xbd$\\xa4\\x9d\\xbd\\xb8\\xb63=\\x88IY<7><\\xba\\xc3\\xa1D=!n&\\xbd@\\x96]<\\xf6TB<\\xda\\x83\\x12=[gB<\\xec}\\x0e<\\xa5pF\\xba\\x81\\x195=He\\xab<\\xfa\\x0bg\\xbd\\xb8[9\\xbd\\x81\\xe5\\x15\\xbc\\xa0\\xe1\\xde<I\\xc7\\x8f\\xbc*eI=V\\xac\\x19\\xbda\\x13\\x04=F\\x8e#=\\x89\\xfe\\xa3:\\x0f\\xdb]\\xbcx\\x97\\xf2;\\xe0o&=\\xd4\\xe8\\r=\\x8a\\xca\\xf9;G\\xf4\\x01\\xbd)\\xa2|\\xbc\\xd7\\xb4h\\xbd\\x8d\\xb5\\x12=:\\xd4/\\xbd\\xb9l7=\\xe0\\x05&=\\x0bE\\x89=\\xebA\\x1f<\\x7f\\x11\\xbb<{/ =M{\\x89<\\xb1\\x9d\\x0b=\\x17z\\xb0:\\x9f\\x1c4=A^J=\\x80\\xce\\xdb\\xbbv=\\x98\\xbc\\xfa]\\xc8\\xb9\\xd9\\xb1`\\xbb\\x93\\xe9\\x0c\\xbd\\xa1\\xc8\\xe9;:\\xa8\\xd3;k\\xe5\\x05\\xbd\\\"\\xfe<=\\xc8\\x1bx;X\\xd8\\x89<Jt{\\xbc\\xb9\\xe6&\\xbd\\x17\\xf5\\x83;_\\x0em=\\xd5R\\xed\\xbc\\xa6=e\\xbc\\xb2\\x11\\xa6=.\\xc9Z=\\xacdj<\\xd1q\\xa2;\\xdf=\\x13\\xbdO\\x16\\x7f\\xbc\\xccG\\\\\\xbdXY\\x83<\\xf9\\xf0\\xa8<\\x00\\x80\\xb6;\\xec\\xce\\x88;q\\xbc\\xad<\\x8e\\xa9\\x04=rV\\x0f\\xbd\\xe6\\xd6\\x17=oJ\\x9a<\\xd0@G\\xbd;\\x1d:\\xbd\\xb0R\\xa9\\xbd\\x9f\\xb2\\xab<RX\\x97\\xba\\xac\\\"]=\\xb1\\x1fd\\xbb\\x03\\xd0\\x96<\\xb0\\x15z\\xbd+N\\x8f\\xbcM\\xac\\xc7\\xbc\\xf5S\\xbf\\xbc\\x1d~\\xa9\\xbd]\\xa2/;E0\\xf6\\xbb\\x8c\\x13\\xc9<J7\\x1a=\\x0e(\\xf5<\\x85\\xb3\\xe3;\\xd8\\xdd!=\\x1c<\\xf2<\\xc7Z\\x05\\xbc\\xf8Q\\x07\\xbdz6i\\xbd>\\x90\\\"=\\xdd+\\xad<\\xca\\xfc\\x9a<\\x95)^\\xbc<\\xdd\\xa5=\\xb8z\\xf4;r\\xdb\\x11;k\\x9a\\xab\\xb9v\\xe7\\xc9;X\\x8b&\\xbcm\\xa3\\xe2:-\\xd4\\x10\\xbd\\x14%\\xdd\\xbc\\x19\\xdb\\x8a\\xbd\\xa0\\xb2!\\xbd\\\\*\\x90\\xbb\\xebG\\x1c\\xbd\\xe2\\xf2\\xd0;\\x9c\\xd7I\\xbd}x\\xab<\\x8bC\\xc4;\\x11 ]\\xbc\\xfbP@=\\xc4\\xcbF\\xbdv-w\\xbb\\xa5\\xb2E=\\xd1 F\\xbd\\xb6\\xb3\\xa0\\xbc.\\xe6\\xf7\\xbd\\x07\\xdau=\\x8a\\x04\\x05\\xbc\\xa7o\\xb1:\\xb9\\x19\\x8a\\xbd\\xd09`<\\x11:\\x02<L{\\x95<\\xe3\\xcf\\xba<\\x9d\\xe7\\x85\\xbc\\xc5\\x12Z<i\\xdf\\xc2\\xbci\\xe05<\\x1d\\x10\\xab\\xbd\\x8c\\xc5\\xe4\\xbc$\\xff\\x81\\xbcJ\\xa6\\xda<\\xeb\\x8d\\xa0\\xbc\\xfcw\\x11=\\xd1\\r\\xeb\\xbc\\xf1pc<\\xb1\\xf3\\xa4\\xbcS}\\x83\\xbd\\xcc\\xc8\\xf4\\xbb\\xa4s\\x8c<5\\x04M<WS\\xd0\\xbcK=\\x83<\\xcb\\xe9\\r\\xbdL\\xab\\xa0\\xbb\\xb6\\x8a\\xf6\\xbc\\xc1t\\x0e=\\xb8j\\x85\\xbc,2\\x95\\xbcy\\xae\\x15\\xbc\\xf2\\xad\\x15=\\x86\\xa8\\x94\\xbc\\x87\\x92\\x96\\xbdS\\xf3\\x00\\xbd\\xa8i\\x91<^\\xc8G\\xbd\\xb6^\\x0c=\\xef\\x07\\xc8<\\xad\\xd0K=\\xc0\\xad\\xb8<\\xd5\\x94\\xfe<\\xf3\\x95\\x13\\xbd\\x9d\\xe2d\\xbd&:)<\\x04m\\xcc<g\\xf6\\x9b<G4F\\xbd\\x0e4h\\xbc\\xb4\\xb8\\x04=\\xa0d\\xcc\\xbb\\x84fD<38<=\\x7fB\\xea<\\xee\\x87\\x98\\xbbe\\x12\\x98<\\xd2y\\xbd:\\x9f\\xf6\\x92=:\\x96\\x11=\\xb2\\xa7+\\xbc^\\x16\\x83=(\\xef\\xf9\\xbc\\xbco\\xb5<\\xc3G\\x99\\xbb\\x94J\\xad;\\xfc^\\\"<`\\xc5!\\xbc\\xc5\\xbd\\r<\\xf1\\xab\\xbc=\\xc4\\x18\\x03\\xbd\\xbd\\xd2!\\xbd\\xde\\xcb\\xd6\\xbc J\\xf2;6<4<\\xf0\\x16\\\"\\xbd\\xc1\\xf8\\x01<\\x94\\x15\\x06=\\x1cJ\\xf4<\\x80\\xb6\\xd4<L\\x9c\\x83=\\xc9\\x8e\\xd6<\\\\6\\\"=\\\\\\x0e\\xb3=\\x82\\x17\\xe3\\xbc\\x95h\\x0b<\\xb1\\xcb\\xb0\\xbb\\xb6\\xec =M\\xbaH\\xbc\"\nHSET bikes:10045  model 'Ariel' brand 'Velorim' price 2950 type 'Mountain bikes' material 'carbon' weight 13.2 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"5>\\xe5;\\xf3\\xf5\\x84:\\x8b\\x92|;\\x1e\\xf2\\xdb< \\x06\\x85\\xbb.\\xd6]\\xb9\\x8dMW\\xbbTh-\\xbd\\xbb\\xfb\\x86=Y\\x89N\\xbc\\xf6\\xf2\\x9b\\xbb\\x8b\\x03\\x14\\xbd\\x1b\\xa1W<\\x80\\x10K=\\xc9\\x1b\\xdb<B\\x10W\\xbd\\xe2 \\xab;\\x8c\\x0b\\x04\\xbd\\xe6\\xd1(=\\xacK\\x96\\xbdx\\x9b\\x15\\xbd\\xa3\\x14_:\\xd76\\x10\\xbd\\xd2J\\n\\xbd\\xc1\\xeeN=\\x06\\xdd;=\\xca\\x90\\xe4;\\x8e4\\x89<\\xa2C\\x9f\\xbd\\xb6\\xa6\\x9e\\xbb\\xb05\\xf8\\xbcZ\\xd5{<I\\x98\\xf4:\\xe1\\xc1\\x05=\\x02\\xe9J\\xbc\\x10/\\x98;E\\xc7\\x04\\xbd\\x84\\\"\\xe8\\xbc\\x95F\\x9a\\xbdY\\xafy<\\x98|\\xed<\\xe3\\xba\\xe0\\xbc\\xa9h\\x97\\xbc)\\x9f\\xb2\\xbb.\\x13l\\xbc\\x9cV\\xe9\\xbc\\xa9\\xa6Q=\\x1b\\xdc\\xed\\xbc\\xbf[\\xfc\\xbc\\x1f\\x89\\x1f=\\xde\\x19\\x10=\\x84\\xa3\\x9e\\xbc`^\\x97\\xbc-\\xf2-\\xbb\\xad\\x04\\xe3\\xba\\xcb\\x7f\\xc6\\xbch(l;\\xb3\\x82\\xa9\\xbdF\\xe4Q\\xbd\\x1cA*=\\x1b\\x05\\\"=\\xe5BY:~\\x92r\\xbc\\x96\\x17\\x90=\\xc7)>=Fc<<\\xec\\x95\\x9d\\xbc\\xf5n\\x91<\\xd4\\x9c\\xb3<\\x98\\xda\\xd3<\\x9e\\x0f\\xc0\\xbc5\\x08&\\xbdQ\\xfc\\xc9\\xbc3y\\x81\\xbcv?h\\xbcJf\\x9a\\xbb]_\\xb2\\xbc\\xbf\\xceG\\xbdb\\xbc\\x91\\xbd\\x8a\\xcfW<\\xb6\\x81\\xc1<f9y<\\x8b\\t\\xea\\xbcu\\x7f\\xa4\\xbd`\\xe3b\\xbc\\xfe\\xb8\\xe2\\xbbz*\\xd1\\xbb\\xa5\\xc9o\\xbc\\x12(\\xf0<\\x1b\\x99\\xa7\\xbb(K\\x81\\xbc\\x06\\xf6\\xf9\\xbb\\x89\\xe3\\\";\\xf9\\xd3\\xfc;U\\xeay:\\xb2L\\x08=\\x91/\\x01\\xbd\\x7f\\xb1@<\\xb5So\\xbbxk\\x1c\\xbc\\xe9|\\x97\\xbcI\\xa73<ga\\xbe\\xbc\\xd6\\xc9\\x83\\xbd\\xe0\\xce\\xb6\\xbc\\xbd?\\x13>\\xb2\\xf2\\xc4;\\x9e\\x82\\x05=\\x87\\x8d\\x18\\xbb\\xe2~9<#\\xb9Y\\xbd\\x91\\xb3j=\\xb1\\xb3h=s\\xd9\\xfd\\xbc\\xa6u\\xc9\\xbc\\x1b\\xc7t\\xbc\\x82\\x0b\\x06\\xbdJv\\xac\\xbc\\xd2U\\xb9;\\xbcy#=K\\xa4\\xde:\\xf4\\x88\\xb1<\\xba!\\x03=\\xf5\\n^\\xbcn?\\xba<\\\"\\xfa\\x978\\xcb\\x19\\x1f=-\\xc4I\\xbdH\\x8a\\xc3\\xbc\\xa1\\xcek\\xbd\\xaeH\\xc5:\\xa5\\xcf\\xb6<\\xb7\\xdcs=\\xee\\xa5\\x88\\xbc\\x17\\x9d\\xba<K]\\x89=\\x87+>\\xbd%\\xb97\\xbc1z\\xc2<^K\\xd3;8\\x1a\\xcc\\xbc\\xf4\\x97e\\xbdq\\x88>\\xbd\\x18-\\x12\\xbd\\xf3\\xd16:@8\\xd7;\\x0cvB<\\x13\\x12\\x84<<hm<z\\x8al\\xbdi0j\\xbc0y==.*\\x84\\xbc\\xc6HM\\xbar\\xcd#\\xbd\\xca\\x86\\xa6<\\xd6n\\x0f\\xbdl\\xab\\x92\\xbcw\\xac\\x0c\\xbd7\\x90\\xa0\\xbc\\xa1\\x1c\\x9a=\\xe0Z\\xb1\\xbc\\xbb\\xee\\xca;\\x9e6\\xb0\\xbd\\xaac\\xb3<\\x89\\x96\\xa4\\xb9wi\\xdc\\xbc\\xf5:\\xc0=2{6<\\xa3\\x15\\xc6\\xbc\\xcd\\x82t<\\x96T@=\\xb5\\x10\\xd6;\\xed|\\x0e\\xbdk{Z\\xba\\x94\\xca\\x87\\xba\\x05\\xf6\\xb5\\xbdb\\xf6#=\\xbb\\x1c\\x14<\\xb8$\\xba\\xbbu>\\xcb<\\xcc\\xa1\\xa0<M\\xd8\\x8d\\xba\\xed@\\x16=#z\\xa5\\xbc\\x04\\x80o\\xbd\\xd6T/=g\\xab\\xa8\\xbb\\x0e\\x81\\xc1<x\\xddM\\xbdn\\x96Z\\xbb\\xf83?\\xbd\\xf6M\\x11\\xbd?\\xf3\\xec\\xbb\\xec\\x9b%\\xbd0\\x19\\xa2:Y\\x06&;u\\xbb!\\xbd\\x8b\\xa1&=Le\\x98=.q$\\xbdh.W\\xbd\\xae?\\x16\\xbe\\x0e\\xca\\x06\\xbd\\xd8\\xc3\\\"\\xbd..\\x1b<\\x16\\xd1\\xce\\xba\\xa0\\xcd\\xab;\\x97\\x91\\x05=\\xf8\\xdb\\xd2\\xbc\\x80Ll\\xbb\\x19\\xc9?<\\x81Ea\\xbcX\\xd9\\xc2\\xbc\\xfd\\xd0?=\\xcb\\xa4\\xb2:\\xc2v=<,\\x94\\x03\\xbc\\xee=#=\\xd5uD\\xbd_v\\x0e=gp\\x18\\xba\\xb6W\\x1f\\xbd^\\xd5\\xae;0f\\x1d\\xbd\\xeeP\\x00\\xbcw\\xd5\\xe7<rI\\x9a<\\xfd\\x07&=\\xbd0\\xbd\\xbcq ]\\xbc3\\x18\\x9c\\xbcCi\\xe6\\xba\\xfd\\x1f\\x0f=\\x01\\x1a\\t\\xbb\\xb6C\\xfc<\\xe1b\\xf4<\\x8b\\x06\\xcb;\\x0f\\x9dn\\xbb\\xa6\\xc9E\\xbcj\\x02\\xb9<\\xd7!6<pN\\xbc<jl\\xc8;\\xdeX\\x1d\\xbb\\xfb\\xbd?=\\xe9\\xe8$=Z1\\xfb;\\x86P\\t\\xbd \\xc9\\x83<\\x86\\x03\\xfb;q\\x91\\xd6\\xbd\\xc2\\x15\\x82\\xbb(ru=*\\xe7\\x03\\xbd\\xa7\\xd7^=\\x10\\x1f\\x17\\xbd\\x1a\\xae\\xce\\xbcX\\\\\\x9c\\xbaR\\x1d\\x8e=Ff)<\\xec\\xb5\\xc4\\xbc\\x11\\xc5\\xaa\\xbb\\xf8\\x9cf\\xbc\\x04\\xcd\\x02\\xbdjB\\x9f=\\x18\\xda\\x05\\xbc\\x00\\x95\\xd5\\xbb\\xbcdc=\\xbe\\xab*\\xbd\\xda$\\x15;u\\x18 ==\\xf8O\\xbd\\xfd\\x81\\x80;\\xa4\\xa0\\xee\\xbc\\xe02+;XXi\\xbcG\\x92\\xde\\xbc<\\xaao=\\x15\\xa0k\\xbd$\\x1b\\xac<\\xf8F\\x12=\\xb2\\x85\\x84\\xbd\\xa0E\\t=\\x01I =i2K\\xbb\\x89\\xc5m<\\xc2\\xbf\\x9c<BC\\\"\\xbd]p\\xad\\xbc,\\xdc\\xc8<\\xd0\\x9b\\xa1\\xbcR\\x06\\x98=p\\xab\\x11=k\\xaeI\\xbc\\xbf\\xb9\\\"\\xbd\\x1b\\xd2m<\\xd9\\xfa\\xf3< \\x15\\x9b<\\xad\\xb9j\\xbc\\xfcJ}=\\x1b\\\"\\x9c\\xbc\\xcf\\x1d\\xb4=\\x17)\\x81:\\xde\\xf7\\xf5:\\xdc\\xea_<L\\xb1\\x0c\\xbc\\x8cP\\xb1;\\xa2w\\xa2=\\xd0\\x19\\x00\\xbcL\\xbf\\xee\\xbc\\x90E\\\\<\\x18\\x90\\xa7\\xbc\\xc8\\xcf\\xce<xD\\x93<R!\\xd7\\xbaf\\xeb\\xd6:\\x9f+\\xcc=;\\x8e\\x82\\xbb\\xc3\\x98(\\xbc\\xbe\\xeeW\\xbd\\xa5\\xf7\\x7f\\xbc&\\x187=n\\x13\\x1f\\xbc\\xc6r)\\xbbos?\\xbd\\x81a\\xce\\xbc\\xc7;\\xde\\xbd\\xd2N\\xa0=o\\xfe\\xe7\\xbc\\x9c\\x9bP=\\x94](\\xbd\\xb1\\xcb\\x81\\xbdB#\\xd0;\\x9b\\xaf\\xca:0\\xfe\\xb3\\xbc\\x88\\x11\\x11\\xbd\\x1109=w\\x0b\\x14\\xbc\\t\\xb9\\xc9\\xbb\\xdfX\\x13\\xbd\\x08HA\\xbd\\xab\\x1aw=}\\xa8\\x00\\xbc\\x1bZ\\x9c\\xbc*w\\xe0<\\xf4m?<\\xfath\\xbc\\x91N.=\\x03.x\\xbd\\x18\\x8ed\\xbb\\xcc\\x9bm<\\xe6g\\xa9\\xbc\\t`\\x1e:u\\xc44;8\\xcc\\xa3\\xbc^\\xac\\x98\\xbc\\x84\\\"/\\xbc\\x1a\\xb1\\xed\\xbc\\xef\\n\\xa9\\xbds\\xa9_\\xbd\\x96\\x8b2:\\x83\\xd9\\xbf\\xbc\\x98|\\x8d=\\ro\\x14=|\\xd5\\x0b<\\xc5\\xd1\\xff\\xbb\\xba\\x9a\\xc0=J\\x0e\\xc2\\xbci\\xc5X\\xbc\\xd4b\\x15=_\\xdb\\x9e<\\xca\\xfd\\x12=\\xc1\\t\\x84\\xbd\\xc49\\xca;7\\xb9g\\xbb\\x94\\xc7\\x85\\xbc&C\\xe9\\xbc\\x00\\xe1\\x93\\xbcVM\\xdf;\\x99\\xe3%\\xbd\\xff8\\x10\\xbd\\x85\\xfb\\\"\\xbd[x\\x99\\xbb\\x148S\\xbdjG6\\xbd\\x0b\\xb9h;s\\x14N=8(\\x82=\\xa2\\xbd\\xf2\\xbc\\xc5\\xbf\\xf4\\xbcv\\x83\\xb2\\xbb\\x85\\rm=$kc\\xbc\\xb9&\\xd9\\xbb\\xac\\xb28\\xbb\\xd5x\\xca\\xbd]\\x8d\\xe3\\xbc\\xd7a+=\\xd3\\xdbe\\xbd\\xd9\\xee\\x9a<\\xef\\xe9#\\xbc\\x1c\\xff\\x8e<\\xac\\xd6\\xc9=G\\xd1\\xd6\\xbbP\\x84\\x80\\xbd\\xcc\\xd5i=\\xc4\\x1c\\x95<\\x93\\xc6r\\xbc\\xafO\\xd6\\xbbq\\xdc\\x85=8\\xb7\\x8a<\\x97\\xd4\\xe9\\xbb\\xbf\\xf3\\xa1;k<\\x87<\\xcc\\xf2\\x95<\\x1d|\\x94\\xbd\\x93\\\"\\x8a\\xbc\\x9eNE=b\\x8f\\xaf<|\\tj<\\xbaar<L\\xd0\\x0c\\xbc\\x80Z0=\\x83z\\x8e\\xbc\\xc2\\xe2\\x13<\\xf5]@\\xbd\\xf1\\x9cg=\\xbaX5\\xbdeV\\xe1\\xbc\\x81\\x91\\xd1\\xbb-\\xf4\\xf4\\xbc/\\x0e\\xba<*\\xff\\xca\\xbc\\xf5|&\\xbd\\x98\\x05M=\\x1ed\\xd4<\\xd1DY=\\x92\\xe0\\x85\\xbc\\xc6d\\xbd\\xbc&G\\xeb\\xbc/i\\xa2<+V\\x82<?\\xc3\\xce;H\\xcf\\xb6<\\x93$\\\\\\xbc\\x95Ks\\xbd\\xc8\\x14=\\xbc\\\">\\x02=\\xdd\\xb3\\xfa<\\xc2h\\xcc\\xbc\\xc2\\x08:\\xbdy\\x04a<\\x9c\\xcd\\x15\\xbd\\xb2\\x8e+\\xbb7\\x93\\xe1\\xbb\\xf6\\xbf\\xe4\\xbc\\\\\\x0f\\x0f=\\xcf\\xf3]\\xbd\\xbe\\xfc\\xae=\\x90A\\xa2=\\x03z\\xc8<>\\x98l=\\xc9_&\\xbdAh\\\"<OB\\xaa=S\\xe2\\xd4:\\xa6\\x13!\\xbd<\\xeb}=\\x7f\\x00\\xa2\\xbc\\x18!\\x0e\\xbcK\\xcf\\xb3\\xbc\\x85i\\x9a\\xbcWU\\xcd\\xbb\\xb4M\\xbe\\xbc\\xf5z\\xb1<\\xc9A<\\xbd\\xb92\\x18\\xbd\\x9d./\\xbc\\xf1L\\xb19\\x915`<q\\xc4\\x9c\\xbc\\xdbF\\x17\\xbd]\\x83\\x10=\\x10\\xbe\\x00;rP\\xb0\\xbd\\xdd\\x18\\xe1\\xbc\\xfc-\\xb6\\xbc_1\\xd8\\xbb\\xb0]\\xc3;L\\x8e\\x04=\\\"z2<\\xb4)g=s\\x8d\\x06\\xbc\\xd8\\x05\\x85\\t\\xd4^\\xee<\\x13^\\t\\xbd^g\\x1b=s\\xae\\x01=\\xcf\\x0f~<_\\xe2\\x80<\\xfb\\xd6\\x17\\xbd\\x9f\\xbex<\\x06\\xa6\\xee\\xbat\\\"\\x8c\\xbbE\\xe5\\xc4<\\t\\x94\\x1a=\\x8cY:<\\xe1S0=%:y\\xbc\\x85\\x9a\\x07;\\xcaTE\\xbd9z\\x19=\\x1ap\\xa3\\xba\\xad\\xa2K=?\\x8fe\\xbb\\x9b$\\xb7<\\\"\\x86\\xe2\\xbc\\xee\\xd9-\\xbd\\xa5C\\xc9;#Zc\\xbaU\\x91\\xe4;C\\x84\\x90\\xbb[\\xb25\\xbd\\x0b\\x9b\\x8a<5.\\x01=\\xeeB(\\xbdxng\\xbd\\xb3\\xa6\\x8c\\xbcR\\xc4\\x05<f\\x17\\x18;*\\xfd\\xd0<\\x82\\xb0\\xe7<\\xfaCD<-r\\x16=\\x88\\x0c\\x9f=\\xe2\\xa1\\xe1\\xbbfy?\\xbb\\xda]\\x0b=\\xb8\\x91\\xa7=\\xfd\\x9b\\xff;\\xa1a$=dob<.gw\\xbc3vr\\xbc5+m\\xbdn\\xd0\\x8a\\xbc\\xde\\xc2\\x90<\\x0c\\x93\\x16=@g\\xbc\\xbcv\\xd5\\xbc<\\xef\\xc1\\x06=\\xad\\xc20=\\xa4\\xf8c\\xbc\\xc2\\xc2\\x87<\\xbaB\\x06<J1\\xcf<\\xb0H\\xa5<f\\x89:=\\xa0K\\x1a\\xbb4w\\xf2\\xbc\\n\\rG\\xbca\\xd9\\xd1<4_\\xa4=\\x96-v\\xbd\\x03\\x84L;\\xae\\xe1\\xe1:\\x90<\\xd4\\xbcN\\xa1\\x1f=\\xf8tw<\\x94\\xb8\\x1d=\\x8bSM\\xbcQ\\xf9\\x87=\\xde\\xf0\\xe1\\xbcW\\xb0$<mZ8;J\\x18\\xc2<\\xb0\\x15O\\xbc\\xe0\\xf9\\x83=\\xa9\\x8a\\xd8\\xbbsd\\x05\\xbd\\xb51`=y\\xfbj=\\xad\\xf5\\x9b<\\xa2\\xb1R<O\\x8e\\x1b\\xbcV\\x08\\x91\\xb8=\\x96\\xed<?S\\xc3:T\\\"\\xa1;9\\xa6\\xe9<\\x0bp\\xf6\\xbc\\x9e^1=D\\\"\\x11\\xbb\\xc6\\x0e\\xf5<\\x9e\\x81\\xe7\\xbc!\\xb0\\xda\\xbc\\xad\\x91\\x96\\xba=\\xa0\\x01\\xbdL2\\x10=\\xcf0+\\xbcwF\\x0b=\\xfa`\\x89<s<_\\xbd\\x9e\\xa4\\x9b;\\xe9\\xbcM;\\\"z\\xba\\xbc\\xb4Q\\\"\\xbbf\\x8e\\x90=\\xd5\\x08l; ]\\xa2\\xbc\\xc5!\\xed<\\x13k\\xb2<\\xd0\\xaa\\xdd\\xbc\\x1f\\xb7\\x97\\xbd:\\xfb\\x08=\\xf9O\\xa1<\\xc3[\\x8a<:\\x19I<=u\\xf8<\\xf4\\\"\\xcb< \\xb1\\x95\\xbd\\xf5\\x8a7<\\x0c\\\"\\xa8=(h\\xcc\\xbdy\\x82\\xe7<f\\xc4=\\xbcw\\x0c\\x95=\\x9al\\xd2;\\xff\\x05\\xa4<`m\\xd4<_\\xb1-=z\\x0b\\x87\\xbd\\x13\\n\\t=Abk\\xbd\\x00h\\xb9\\xbc\\xf9Z\\xd7\\xbb\\xd4\\xc0K\\xbd\\xe5\\xfe$=a\\xb2\\xf5\\xba\\x16&\\x9e\\xbc\\x1f\\xe8\\xc3\\xbb\\xf8S\\x07\\xbc\\xd9\\xa1\\xba<#\\x07N;\\xee\\xd6!\\xbc\\xc9\\xb6\\x80\\xbb\\xc1\\x7fM\\xbdR\\xc0q<\\x17\\xe8>=\\x08\\x15\\x01=.\\x98#\\xbd*\\xee1\\xbcW%3=c\\\"K\\xbd1\\xcb4\\xbd\\xf7\\x86\\x81=\\xee\\xc5\\x82;\\xf3\\xb5\\xf6\\xbc\\x18\\xbf\\xcf\\xbc\\xb1X\\x17\\xbc\\xc5\\xefL\\xbd\\xa8xn\\xbc\\xe9\\xc6\\xaa=\\x0eA\\xa9\\xbc$\\xf4\\x0b=\\xfe\\x07\\x1e\\xbd\\xe5\\x80\\xe9<\\xda\\xea\\xc5\\xbc\\xcf\\x8f\\xcf\\xbcE\\x82)=\\xbc\\x842=h5>=@_\\xe98G\\x83\\xdc\\xbck\\xa8\\x13<\\x84s?\\xbd\\xa2\\x83d=\\xdd\\xaf\\\\\\xbd\\xbf\\xf2\\xc4<\\x9a\\xf3e\\xbdYtW<CS$\\xbb\\xccf\\\"=+\\xc5Y\\xbcJ\\x1c\\x11\\xbd\\xbcb\\xfe:n\\xf45\\xbd\\xa5\\xf3N\\xbd\\x92.\\x9f<\\xb6\\x12\\xde\\xbb\\xd7\\xd4u:\\x1b\\xa2\\n\\xbck1\\xb4\\xbc\\xc4\\xcd7\\xbdO\\x97\\n\\xbd\\x8a\\x9a\\x03=\\xb86%\\xbd+I\\x13\\xbd\\xb9h\\xeb\\xbc\\xa0T\\xce\\xbd\\x05y\\xfa<j\\xdaW<8\\xc5\\x83\\xba~!\\x16\\xbd@\\xf4\\xdb<d\\xcf\\x1c\\xbc\\xda\\xb0c=\\xa6B\\x08\\xbds\\xfb:\\xbd\\xfa[J\\xbd,\\xb5Z=\\x84jQ\\xbc%W\\xbc;\\xba\\x86;\\xbd x\\x8f<\\xedE\\x18\\xbdY\\x93R=+\\xac\\x97=\\x9a\\x83x=\\xc0Gj\\xbb\\x08\\xb1\\xcf<z\\x11)\\xbb1CJ\\xbci\\xfc\\xae<=\\xae7<\\xb4\\x89\\x04=%[\\xb9\\xbbA\\xc0\\x88\\xbd\\xca\\xf7S\\xbd\\xb1\\xd6\\x80\\xbb\\x7fk<<DB\\x12\\xbd\\x100\\xa6<\\x9ax0<+\\x94\\xf4<\\xfb\\xbec<\\xbb\\x16T=G\\xe4\\x15\\xbd\\xb3\\xf5\\x9e\\xbcy\\xb1O\\xbd\\x0b.,\\xbdJ\\x81\\x10\\xbcG\\xf2\\x88\\xbd\\xfb\\x17t\\xbc|%\\xa2<\\xdb\\xa2\\x0f\\xbd\\x92\\xfe\\xbc\\xbb\\xf6,]=\\xd1T\\xfe\\xbcB\\xbc\\xe3;:\\x92^\\xbd\\xac\\xf2\\xcb\\xbc\\x11y\\x7f\\xbdJ@\\xcd\\xbcq\\\"\\x8a\\xbb\\x91\\xfc\\xca\\xbc\\x06\\xd2\\xba;v&\\xd9<\\x8aJ\\xd0=]Gi<\\xdc|k\\xbc\\x81;@=\\xe8\\x82$\\xbd]q\\x0c=V&\\x1c\\xbcL\\xff\\x16<\\x9b\\xec\\x82\\xbc\"\nHSET bikes:10046  model 'Tethys' brand 'Bold bicycles' price 686 type 'Kids mountain bikes' material 'alloy' weight 15.3 description 'Small and powerful, this bike is the best ride for the smallest of tikes. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\xfc\\\\\\x83<\\n?u<\\xacQ\\xb4\\xbc.\\xde\\xf3<\\x9cV\\xf7\\xbb/\\xb5\\xa3<\\xed\\xd4\\xa2\\xbc\\xa3\\x9fn\\xbd>\\x80\\x90=\\x9da\\xb5\\xba3\\x94F\\xbd\\\"\\xda\\xe0;L\\xe9w=\\xd7a\\x94<\\xb4\\xee\\\\=\\xdfN\\xc3\\xbct\\xe2==N\\x0c\\x87\\xbc\\xcce\\xbe\\xbc\\x04\\xc99\\xbdxH\\xab<\\xe5\\xba\\xe7\\xbc\\x0c(@<\\xadf\\x0f\\xbd\\x1d22\\xbd\\x06 4\\xbc\\xa3G==4\\x95W\\xbb\\xdd&G\\xbc\\xa1\\xd2\\xd5=/\\xaf\\x97\\xbbF\\xca\\xd5\\xbc\\x9awO=RC\\xcd<\\x10\\xe2\\r=\\x0f4\\xff\\xbcql\\x16=\\xea\\x87\\x0c\\xbd\\x86\\xde\\xaf\\xbcL\\x868<\\xdb/\\x92=\\x0ev\\x01=\\x81\\x16b<\\x82\\x7f\\xea<\\x08k-<>}\\xe1\\xbc\\x90OP=\\x18\\x06\\xa6<\\xc5\\x17\\xb0\\xbc\\xe2\\xdc\\x19\\xbbT\\x82\\xb3\\xbb\\x97\\x12\\xf4\\xbcB\\x1f\\xd4\\xbc/\\x11\\xa1<\\x93U|<S\\xd6\\xde\\xba\\\"\\x85\\x7f<V\\xb8j\\xbd\\xdd\\xc7\\xff\\xbcK?L=[,\\x0f<\\xef\\xf1E=\\xbd\\\"\\xc5\\xbc\\xcf\\x9br=w\\xd5\\x15=\\xa2e\\xc1<7%5<b\\xf7\\x96\\xbc\\x8d7-=\\x1b\\xc1X\\xbd\\xf7\\x95!\\xbc\\xb5\\x0e\\xa9\\xbc\\xed\\x10\\xc2\\xbcs{8\\xbd%\\xc9\\xe4\\xbc\\xc0\\xb2%\\xbbB/\\x14\\xbb\\xd4|\\t\\xbcU\\xeb\\x98\\xbdcR\\x1b<P3r< \\xb7\\x96\\xbb\\xbeF\\x13\\xbc\\xbdv\\x82\\xbc\\xb7M8<\\xca:\\x97\\xbd\\xa4\\xd9~<\\xe9\\xd6\\x17=\\xef\\xb4A=\\xce\\xc4\\x0c<\\x9a\\xc7\\x95:\\x1f\\x1a\\xca<\\x92\\xd6\\xa9\\xbc\\xb3\\x89D\\xbb1\\xe8;<\\xc6\\xfc=<\\x0b\\xce\\x8c\\xbcn\\x90\\x83\\xbc\\x88c\\xdc<\\x15\\\"\\x8e\\xbd\\xf9H\\xfe<\\x9cJ\\x1d=:l\\xb7\\xbb\\xf1y\\xec\\xbc\\xa5\\x7f#\\xbb}\\xf4G=[\\x97`\\xbc}(\\xef<\\x0c{\\x81\\xbb\\x1c%\\xb5<\\x9c\\x0e\\xfc;0b\\xa9=\\xc3\\xac\\x81<\\x12OE\\xbdJ\\x89\\x8f\\xbd\\xc7X\\xe8;\\xc3\\x0cw<NZ\\xc2;r\\\\|=81H\\xba\\x16NS<\\x93\\xf3\\x85<\\x17\\xff@\\xbd\\xa6\\xa9y<\\x91}\\xdc<DcC\\xbc\\x039\\xb7;J\\x9e|\\xbd\\xe1\\xb1-\\xbd\\x0b\\xe9k\\xbd8\\xfa\\xc8\\xbbk&N\\xbd\\x01\\x9c\\x14;\\x94\\x1f\\xa0<\\xd3\\xf7\\t\\xbdT\\x1bf<\\x86\\x940\\xbc<\\xceW;\\x19\\x1f\\xea;_\\xd9r\\xbd\\xee\\xaf\\x85\\xbd\\xd3\\x97X\\xbd/w\\xd2<\\xd3\\xc8~=p\\xb2\\xfa\\xbc\\x1a\\x85\\x95<\\x86\\xae\\xd6<cS \\xbd\\x89^!\\xbc+N\\xe8\\xbc\\x9a6\\xa3\\xbc\\x14,D=\\xdd\\xff\\xcf\\xbc\\xa9\\x82\\x07=\\xf49\\x00\\xbdy\\xe9u\\xbcW\\xa66\\xba\\xf2\\x85\\x8a\\xbbkl\\xfa<\\xf7|J\\xbc\\xbd\\xea\\x86=\\xf0\\xb56\\xbbRkn;:\\xd7b\\xbdF\\xad\\x08\\xbc\\xd1\\x1b\\x90;u\\x8b\\x83\\xbc\\x8a\\xb0n\\xbb%\\xcf|=\\x8b\\xcb*<&)\\x9d\\xbc\\x98\\xb0b=~\\xc07;\\xa3{\\xc3\\xbc\\xa2\\xab\\x15\\xbb\\xb0\\xba\\x9f<}\\xe6Q\\xbdE\\xfa\\x89=g\\xde4\\xbc`\\xe1\\x1a:!\\xa0\\xb2<%\\xeb~<W\\xcd\\xf1<!\\xc86=\\x11\\x10S=\\xca\\x051\\xbdm\\x9d:=\\xf7\\xe1/=\\xa9\\x05}\\xbc\\x97\\x10\\xf4\\xbc\\xd9b\\xee\\xbc,\\xba\\x1c=:-g\\xbd\\xa2vD=W&\\xfb<\\xf0\\xc3\\xad<\\xdfk\\xa6\\xbc\\xa3u0\\xbdM\\xe0!\\xbc\\xa4\\xe4f=@P%\\xbd\\xe2Sx\\xbd$\\x81\\xaa\\xbd\\xf7S\\xdb\\xbc\\xfc\\x1f(\\xbb\\xdb\\x8cd<\\xadg)\\xba+M\\xff\\xbb6&\\r=\\x7faj\\xbd\\xa0\\xdd\\x82\\xbb\\xe2\\t\\xae\\xbc\\xe1{\\xfd<\\r\\xf4\\xfb\\xbcp\\xc3\\r\\xbc\\xb7\\xef\\xe2\\xbc/\\x0f\\xc6\\xbc\\xe6-\\x1c:\\x1b\\xfaR=f\\xb5F\\xbd.\\x94\\xf8<\\xdf+I\\xbd\\xcc\\xec\\x8b\\xbc\\xac`\\xed;\\x83\\xd5\\xde\\xbcbS\\x9d\\xbc\\xfb=\\xe7<\\xafH\\x03\\xbc\\xa2R\\x05=\\xb9\\xb6s\\xbd\\x8eY\\x05\\xbd\\xac\\xdf,\\xbds$~=\\\\\\xf1c<\\xfe\\x9a\\xb6\\xbb5\\xd1W\\xbc\\xf9\\xaa\\x9f\\xbc\\xca\\xcd\\x17=\\xd4\\\\3;\\xcc\\xcb\\x8d<1\\xac.\\xbc\\x13\\x8b.\\xbc\\x1c\\xf5\\x95=)[\\xcf;\\x82\\xd08=\\x99\\xf4 =QR\\xe1<^\\xc3\\x80=o\\xa33\\xbd\\x08&\\x9a;\\x14\\xd5\\xaf<\\xa8\\xcb\\x07\\xbd\\x91\\xb3\\xe8;Pf\\x87=M2\\xef\\xbcf\\x95\\xe1;\\xe2\\x8e\\x01;\\xdf\\xf0\\x80<\\x92\\x07\\x01\\xbd\\x0f\\xb6j=\\x01\\xd6L\\xbc\\x90M\\x18\\xbd\\x7f\\x1a\\xf2\\xbc\\xf9\\x11\\xbe\\xbc\\xe0V\\xf0\\xbc{\\xb9\\xe8=,\\x81)\\xbc\\xd4\\xa6\\xc2\\xbc\\xbc\\xeb\\x04=\\xbe\\xe3P\\xbcY\\xaa\\xe9;\\xd6E\\x1c<\\xc9\\x1fS\\xbd5\\x88\\xad<m\\xb0&\\xbd\\x0c\\xd57\\xbc\\x14II<\\xa0;\\x95\\xbb\\x9e\\xc4\\x12<u\\xd6\\xa8\\xbd\\xa2\\xe4\\xfd;nJ:=\\xbf\\x9bB=\\x0fKj<xRw\\xbb\\x16V\\x13=:?\\x9c\\xbc\\xc9\\xfa\\r<!8\\xf8\\xbd\\x11v\\x01=\\xc5\\x95Q\\xbc\\xb3\\xf4\\xda\\xbc\\x05\\x0b\\x1e=\\xfb\\xb2\\xda\\xbc\\xebFr\\xbc>X\\\\\\xbc>\\xa8\\x81<l\\xabC;\\x7f+\\x84<\\xfc\\\\\\xc8\\xbc\\xd5\\x01\\x1f\\xbd\\x15\\xb0\\xa8<\\xec\\xd0\\xad<5\\xdf\\\\\\xbcX\\x03\\xcf<\\x04mH=\\xbaJ\\xe0<\\r\\xf2\\xbe<y\\x1c\\x95=U\\xc9\\x0b;c\\x19\\x83\\xbdQ\\x05&\\xbd\\x96\\xae}\\xbc\\xaa\\xb5\\x0f\\xbd\\xe84\\xee<T\\x8aE\\xbdTO\\x0c\\xbd2\\x12\\x8c=a\\x8c\\xb3<\\xaa\\x0e0\\xbc`Kt\\xbc\\x05\\xfap\\xbc\\xed\\xc9\\x8d\\xbb\\xe6\\x1c\\x80<\\x07\\x1da\\xbb\\x0c\\x03(\\xbd\\t\\xe0\\xec<\\xe2\\xe5f\\xbd\\x9d\\xbd.=\\xe2\\x95\\x87\\xbc\\x82\\x90\\x87\\xbd\\xee\\xa2\\x0b\\xbdte\\xf4\\xbcJ\\xf5\\xeb;\\x1by/<\\xb5\\xb9\\xde<\\x05\\\"\\x94\\xbc\\x19\\xbe\\xf8<c\\x94M=\\x91\\xf8\\xe1\\xbc\\xf0\\xfe\\xce\\xbc*\\xfc\\xc9\\xbc\\xecv\\x98=Md\\x9c\\xbb\\x92\\xc0v;\\xcc\\xb1u=\\x88_\\x08=\\xcb\\x11\\x12=)\\xac\\x9f=U\\x905<\\\\\\x0f\\x80\\xbcB\\xcb\\xe6\\xbc\\xa2\\x17\\xbd=\\xda\\xda:<B\\xac\\x96\\xbd\\x83\\xd1\\xa8\\xbc\\xa6t\\xb5\\xbc2\\xc7\\xba\\xbc\\xf4\\xa5\\xaa\\xbc^\\x97\\xb2\\xbd\\n\\xa0\\xe9<i\\xff\\\\<\\xc5\\xa9:<\\x85\\x9aL=\\xbf\\x04\\x0b=L\\xa9>=O\\x8a\\x14\\xbc\\xcc\\xd6\\x82<d\\xee\\xbc\\xbc\\xf9\\x8f\\xc4\\xbc\\xfex\\x07=\\x9fiL;\\xd3T\\x19=\\xd5\\x9b\\xf3<f\\tm<B\\x08\\xa5<r\\x80~\\xbd\\\\5\\x98\\xbd\\xf4\\x00\\xc0\\xbc\\x7f\\xee\\xe0\\xbc\\x04&Q\\xbd\\xb9\\x9f+\\xbd\\xe1\\x1b\\x9c\\xbc\\xc6g\\x96:\\x96@\\x9a\\xbd\\xab\\xd23=\\xb5A\\x89<\\t;+<`M\\x99<\\xf8\\x8a\\x97\\xbc\\xd3\\x15^\\xbd0}I=\\xd8\\xa1\\xb8=\\xba\\xd7\\n\\xbd\\x85\\x12D;?@}<\\xd5\\xce\\x8f\\xbd\\xa8T#<s\\xc4^=eR\\x9e\\xbd\\x19\\x1a\\xaa\\xbc\\xc0\\xfb&\\xbdh\\x7f=={b~=o\\x18\\xae<W\\xaf\\x19\\xbd\\xe3\\xf7}=\\\\\\xe1\\xef\\xbae]\\xbe\\xbb\\xeb\\xab\\\\=\\xb5\\xd9\\x80=1h\\xcd<\\xff\\xe2\\x1b=\\t\\xa3u\\xbc\\x88E\\xa8;\\x9d\\xa2\\xad\\xbc\\xd6M\\x1b=\\x80\\xac\\xd3\\xbdR \\xd2;\\xab\\xef6\\xbc\\xa6%\\x80<\\x84\\x11\\x00\\xbdk\\xf4\\x1f=\\xd6\\xf9e=\\xb2\\xc9$<={\\xc0\\xbc\\xf1\\xf4\\xb6\\xbc\\x1b-\\x10=\\x8a%3\\xbc$n\\xf2\\xbc\\x96\\xf1\\xc6:\\\"Ei\\xbcg7\\n\\xbd;w\\x80\\xbd\\xc1\\xd5~\\xbc\\xd7\\xed\\x92=\\xbayS=\\x7f\\x80\\xf4<\\xab\\x00\\x1e<\\xd80\\xb5<\\x11\\xff\\x1d\\xbd\\xb7qu\\xbc\\xa1\\x9c\\xec\\xbb$!\\x12\\xbd\\xd8o\\xb5<M\\xb3\\xaf<O0!<\\x9b+}\\xbbJXP\\xbdm\\xc7\\xc1<p\\xdf\\x80\\xbd>X\\x9b\\xbd\\xda\\\\\\xad<\\xce\\x80\\xa7\\xbd,r\\xa7<\\x18`\\x0b;E\\xd6\\x10\\xbc\\x0b\\xbe\\x14\\xbc\\x1f2\\x8c\\xbc\\x99\\xe3\\x94=a\\x08\\x80=\\xffo\\x10;\\xe8c)=\\x8f\\x01\\\"\\xbd\\xf3J\\x95\\xbc\\xc7\\x85H<\\xea,K\\xbc~<%\\xbd\\x9b\\x96\\x14\\xbd%\\xddn=|[\\x97\\xbc$j\\xf2<\\xeb\\xd5+\\xbd&\\xfa\\x85<m\\xa4\\x8c; :T=\\xe0!\\x0f\\xbd\\x00\\x1bz\\xbcv\\x87\\x03;jO\\x9b;\\x84\\x8b\\x95<\\x1e\\xce]<\\xad5\\xdb\\xbc\\x9d\\x97\\x16=\\xdfn\\xd2\\xbc\\xab*\\xa1\\xbd\\xc1!\\xfa\\xbb\\x12\\xa2\\xcc;\\xe3\\xb7\\xb9:\\xddj\\\\;\\x0e\\x9bj\\xbd\\xd3\\xf4\\xa9<o\\xfd7=\\xfd\\x1c\\xe3\\xbc\\x15\\x9fE\\t\\x15[\\xce\\xbb\\xcdB\\xc5\\xbc\\xec*\\xb5<\\x17L\\x88\\xbcc\\xfc\\xef:K\\x84N=\\x06\\xc3\\\":\\xd0E\\xac<\\x85\\xc2@<\\xacXG\\xbc\\x16/`=c\\xba\\xf6<\\x05\\x19\\xe5\\xbb\\xa4\\xf8[=h\\xde\\x1c\\xbd\\x95\\xb0Q=\\xad\\xde\\\"\\xbd\\x95\\xda\\x19\\xbdA?Y:w\\\\k=@%\\xa1;\\xb4\\x9a\\xd7;\\xa6v!=\\x04/\\r=\\xbd\\xe1\\x8e\\xbcGr|\\xbco\\xba\\xcb<\\xbd4G<\\x0f\\xdb\\xde\\xbc\\xf5c\\x99<\\xec\\xb9-=\\xc4?\\xea<f\\xf7\\xb1\\xbc\\x0eOI\\xbd\\x8f\\xd3\\xdd\\xbc\\x9dB\\x8d\\xbc\\x80\\xb4\\xb3<\\xc9=\\xc6<\\x7f.}=?\\x90\\x12<\\xe5\\xa3\\xb3\\xbb\\xdcW\\x99<\\xf6\\xfa\\xd1;\\x0e\\r\\x83\\xbc\\x81jU<\\xb8\\x13\\xf6<\\xee#\\xdc=\\xfd\\xe9\\xa6\\xbc\\x97\\x89\\xe0\\xbc\\x9am\\x83;1Gu\\xbd\\x0b\\x95\\x8f\\xbd\\xdb\\xecU=\\xaf\\xa2\\xb8<\\x8c\\x8e\\xa5\\xbb\\xd3\\xb0\\xcf<\\xceT\\xee\\xbc\\x0c\\x19\\xb3<;\\xe5\\xe9<\\x85\\xd0\\xdc:i:j<\\xaf\\xdf\\xae\\xbc\\x8aA\\xcf<\\xc9\\xe8/=F\\xe9\\x9f\\xbcf`\\x1b\\xbd\\x17\\xd6\\r\\xbc\\xf80\\xd8\\xbc\\xd3L\\xb9<\\xc9\\x90\\x06\\xbd\\xbcm\\xef<\\\"\\xdc6\\xbdS\\xc8\\x07=tk\\xd2<u\\t\\x03=\\xec\\x16e\\xbdRx*\\xbd\\xef\\x053=\\xbd%\\x86;U\\xc7k=Y\\xbdH\\xbd?\\xe84;\\xf9H\\x03\\xbc\\x8d3\\x06=\\x95\\x08\\x1b\\xbd\\xf6\\xd5\\x00\\xbc\\xf0\\x8c\\x13=\\x916M=\\x0e$\\x19\\xbc\\xd4\\xa6\\x1b;=\\xebv<\\x7fE\\x1d\\xbc*9\\xcf=M\\xd5y\\xba\\x14Q\\x14=]\\xc4\\x05=\\xd4\\x1ak\\xbc\\x85\\x82\\xb6\\xbc\\t%Y\\xbc?l\\xf1:\\xff<\\xc0\\xbc\\x92|g\\xbc\\x13\\xb0.<H\\xdcP\\xbc\\xb3\\xfa\\xd2<R\\x06H\\xbb\\x97\\xd11\\xbc\\x11\\x19F\\xba\\xc8!\\x1f\\xbd\\x80\\xabo\\xbd\\xdf\\x84\\x1a=%-\\xcf\\xbcnK\\xdc\\xbc\\xd5\\x17\\xa9<6\\xe5\\x96=\\x1fT\\xc3;\\x18%`=\\xf2\\x92\\x81\\xbc2\\xb5\\xed<\\xf1Vl\\xbdG\\xd7W=@\\xa0\\x96;d%\\x95=\\x9aX\\xf0\\xbcS\\xb9\\x88=\\t\\x83(\\xbc\\xf2\\x00\\x08\\xbd\\x85\\xcaE\\xbd7EX=\\xd93Q\\xbd/\\x88\\xc9\\xbcv\\xfb\\xae\\xbd\\xbd\\x83\\x08=\\xffW\\x1f\\xbbe/\\x93=\\xb5q\\xb1\\xba\\x07LR=\\x9b\\xf7\\x1d\\xbdm\\xb1\\xd3\\xba\\r\\x1d\\x0b=\\x9a|N\\xbb\\xb1\\x812\\xbdC\\x98q\\xbdKe\\xad<\\xc3\\xda\\xa3\\xbb\\xea\\x9c~<?W+<\\x18\\xab-\\xbczH\\x04=\\xdc^\\xdb\\xbbM\\\"u\\xbc\\x92\\xe2\\xf0<\\xee\\x84\\xb9:K\\xc3\\xfc<\\x84\\xa5\\x06=\\x9b\\xd3\\xe8<\\xb8%v\\xbdv<\\x87:vb[;t\\xcc\\x02=NP\\x10\\xbc_\\xf4\\x83=\\x1c\\xd0\\xab<\\x17?+\\xbc}(\\x13\\xbd\\xf8\\xf9B\\xbd>\\xdd\\x14\\xbdJ\\xc6\\xdf<\\x84\\x05\\x91=\\rs\\x8c;\\xd6\\xd8V<\\x84\\x9dD\\xbc\\xc5\\xadp\\xbc\\xd2Q0<\\x9a4\\x18<\\xea_M=2\\xac\\xe5\\xba\\xe6\\x18\\x9a\\xbcF\\xd3\\xc4<6{|<W\\\"\\xa1:w]\\x89\\xbd\\xc3\\r\\xed;#>\\xd3\\xbcK\\x02\\x06\\xbc\\x81\\xda\\x1f\\xbdS\\x0e?\\xbc\\x8e\\x94l\\xbd>\\xcd+=\\xfd\\xf0\\x98;zn\\x13\\xbdg\\xa1\\x0f=\\xe8\\\\\\x8b\\xbd{\\xef\\xf7\\xbc\\x81\\xc7\\x1a\\xbd85\\x87\\xbdE\\xb6\\xaf\\xbd\\x94\\xae\\x01<\\xb3\\xb6O<t\\x12(\\xbd\\xe7\\x84\\x07\\xbd\\xa4\\xe6\\xcb\\xba\\x8a\\xa6\\xc6\\xbc4/\\xa0\\xbd\\xd4\\xd1\\xbb\\xbc\\x0e\\xb7\\xc0\\xbc(\\xeb\\x03=5?\\xa8\\xbc\\\"\\x07\\x8f\\xbb6v\\x92\\xbd\\xf5\\x03\\x01<\\xc6\\xe9\\x8c:\\xec@\\xad<\\xff\\xcaH<\\xa0\\x8c\\xf7\\xbc \\xa46\\xbc\\xa4\\xe4>;\\x8b\\x94R\\xbc\\xde\\x1c\\xef\\xbc\\x97\\xdcE\\xbd%\\x0c\\xb6\\xbc\\xe5\\xa2\\x1e\\xbd\\x89%\\xd4<$\\x13\\x0e=\\x046\\x8e=\\x03l*\\xbd\\x13f\\xb3<\\xdb\\x88\\x16\\xbd\\x12\\x90y\\xbd\\xe7\\x92\\x84=\\x86\\xca\\x93\\xbcq\\x1eE<H\\x17\\xbf\\xbc\\xab\\xde!\\xbcm=\\x00=GS\\x87\\xbd&9\\x1f\\xbc7}\\xe6:\\xb6\\x88\\xef<\\x87\\x14b\\xbc6C\\xdc<O\\xa5\\x16=B\\xa5\\x87<m\\x15L<\\xdd\\xe5\\xf7\\xbc\\x9cP\\xc5;\\xff\\xa6\\xf5\\xbc\\xf5\\\\8<\\ny\\x8b\\xbd\\x84:+\\xbb\\xb5\\xbb\\x08=\\x86+\\x84\\xba6\\x07\\x85;\\xbfN*=\\x8c\\xb8_\\xbc\\x90\\xbf\\x0c=\\\\\\xf5\\x19\\xbb\\x10&\\xca\\xbb]\\x9b\\x02\\xbd\\x16\\x1a\\x9c\\xbd\\x9e\\x02\\xa8<?\\xff\\x9f<\\xe2\\xc5\\xae<\\xe6\\xe8\\xf2<\\x934\\xca=6R\\x90\\xbco\\xb9\\x89<\\xc5{\\xa2=[\\x19\\xd9\\xbc\\xcd\\x1d(=\\x0fAk\\xbd\\xad\\x1b?=\\x03\\xdf\\x89\\xbd\"\nHSET bikes:10047  model 'Millenium-falcon' brand 'Velorim' price 2790 type 'Commuter bikes' material 'full-carbon' weight 13.1 description 'The perfect commuter bike for anyone who is constantly rushing around, and prone to forgetting to charge lights, maintain their bike, or not quite getting round to checking weather reports. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\xe0\\xad\\x08<\\x9e\\xf3:\\xbd\\x9e\\xf9\\xc5\\xbc6\\x01\\xa5\\xbc\\x87\\x03m\\xbd-3C;2/\\x92\\xbc\\xb7y<\\xbd\\xf2\\x98\\x95<\\x14~\\x80;\\xf8\\\\\\xf0;\\xb7\\xfa\\xc9\\xbc`a\\xee<A\\x94L=\\x8b\\x1f\\x11=\\xdb\\x836\\xbd\\xf2kY=Ek\\xb4\\xbdN\\xf6\\xe0\\xbc\\xb9iW\\xbde\\x07\\r=\\x0c\\xb2A\\xbcnx\\xa4;\\xd9\\t\\x8b\\xbc\\r\\x10\\xff\\xbch\\x1f3;a\\x9d\\xf7;\\x1a\\xae\\xa09\\xfbd\\xf2;\\xf5\\x97\\xba<\\x9bX\\x0b<\\x9a4^\\xbd\\xe6\\x13@=>\\xdd;=\\x1fD]\\xbd8\\x13\\xb9\\xbc%3\\xd5<\\xe1n\\xd4;\\xa3\\xeb\\x89\\xbd\\t\\x9d\\x9a\\xbbe\\xee>=[\\xa9\\x1a:\\xfc\\x85\\x1b<\\xbe`~<j\\xea\\x04<7\\xaa\\x88\\xbd\\x0b\\xb8\\x8c<\\xce\\xa0\\xad<\\x81^\\xdf\\xbc\\x16u\\xbf;*\\x1b\\x91;\\xf3\\xfc\\xd7\\xbc\\x1ae!\\xbd\\x86\\xba9\\xbc\\xf7\\xc9\\xdc\\xbc\\xc0\\xb52\\xbd\\x86\\x04\\x1b\\xba\\xb1\\xf1\\x93\\xbd\\x04P\\t\\xbd\\x7f\\xad\\xab=B\\x93.\\xbdT\\x96r=\\x98\\xac\\x88:t\\xca\\xd3\\xb9\\x18x1<\\\"I\\xdd\\xbc\\x03\\xe8\\xb9;\\x05\\x16\\x0c<%\\xef\\x1f;\\xd0\\x1c\\xe9\\xbc\\xf0i,\\xbb>\\xfa\\x9a\\xbc9\\xc3>\\xbb\\x81\\xaf\\xc9\\xbd\\xb1\\xe1\\x9a<x:7\\xbdr\\xba\\x1e\\xbd\\xa2\\xda\\x10\\xbd]\\n\\x01\\xbc\\xcf\\x06\\x86=\\xdaH\\xfc\\xba\\xbauZ\\xbd\\xf5\\x1f\\n\\xbd\\xef/4\\xbaV\\xc2\\\"\\xbc\\ny\\x8c<4\\xe6\\x07\\xbd\\xf81j<\\x0b\\xcf\\x0f:\\xa7\\xea\\xcd<(\\x93o<>:\\x89<U\\xa0\\x8e\\xbbK\\xa6\\\"<\\xf4\\xad\\xb7<,\\x98#=nE\\x0b\\xbdi_\\x9d;|\\xc6\\x1b=\\xf1\\x82\\xcc\\xbcI\\xbb\\xc5\\xbd\\x15K\\xcf<\\x87\\xe5\\x89\\xbcEo\\x0b\\xbc<\\xbf\\xff<v\\x92\\x1b>\\x7f\\x9a\\xd8<\\xe9\\x1e\\x80<\\xae\\xcc\\xb7\\xbc|\\x85\\x9c\\xbb\\x9f\\x81\\x90;\\xde\\xae\\xc5;\\xf5C\\x14=}\\xe2\\x83\\xbb\\x11\\xb4l\\xbd\\xfd\\x1f\\xf0\\xbc\\x94\\xb5\\xbc;\\xfb\\xb8Y;I\\x1e6=OB\\xd4\\xba\\xb6\\x188=P\\x8b\\t:\\xa8\\xe4\\x17\\xbd\\x93!7=\\x89\\xd8\\xa2=\\x16Pn\\xbc\\x8e\\xd7\\x83<tn\\x06<\\xe5o!\\xbd\\x17\\xa9\\x08\\xbd\\x91u\\xf3\\xbc\\xd1\\x8d\\\"\\xbdp\\xf1\\xe8<\\xc3\\xd5/\\xbdP\\xc9\\x9d\\xbc\\x80\\x93\\x8a=\\xff\\xb6J\\xbd\\xad\\xaa)\\xbd\\xa7\\xe1\\xc3<\\x0cPY\\xbb\\x97\\xff\\x1e\\xbd\\xdc\\x0e[\\xbd\\x18\\xa0\\x0c:r\\xfc\\x12=vE\\\"\\xbd\\xab\\xff\\xf3<]\\xe4\\x89\\xbc\\xde\\x19&=\\xca\\xc9\\xf4\\xbc\\x83~\\\"<Z\\xbcz\\xbc\\xcc\\xed\\xa5<\\xb7\\x02 =\\xc7=\\x1d<\\xfc\\xba\\xc5\\xbb\\xd8o\\x07<qYB\\xbc\\x9e\\xab\\x99<;\\x93\\xc7<\\xa2z\\xb4\\xbc\\r\\r\\x1d=\\xd4\\n\\xa9\\xbb\\x80\\xe0\\xfb;\\x04\\xe1\\x00\\xbd\\xbd\\xbe\\x94<\\x02b\\x0b=)\\x11\\xc6\\xbb\\x97\\xe4\\xdb<\\x073\\xf7<|\\xd4\\xbb\\xbbbw\\xc9\\xbci\\xa8\\xa8=\\xa5\\x85\\x1a\\xbc\\xfe\\xcbG\\xbd\\xc0X\\x00\\xbd\\xa5Q\\x1d=}\\x8a\\xa1\\xbc\\x05\\xe97=\\x99\\x00\\x01\\xbcC\\xec\\xcf;\\x10a\\x1a\\xbcv?l\\xbcT\\xf5\\xa0;X[\\xaf<\\xd6\\x83\\xf8<k\\x15m\\xbd\\x10\\xd1\\xd5<\\xef_\\xcc<\\xa4U\\x0f=\\xf6EY\\xbcn\\x98\\x0f=\\xb4\\x16\\xea<S!N\\xbd1\\x93q=:f\\x99\\xbb\\xabtp=k\\x81S<M\\x8a\\t<e\\x00\\xe6;f\\xcf\\xff\\xbc[v`<-\\xfe.\\xbd\\xd69\\xa6\\xbd\\xf3`\\xa5\\xbc{VR\\xbd7V*=\\x1fW1\\xbdq)\\n\\xbdg\\x9ad=\\xb1\\xaa\\x1c\\xbc\\xb3q\\xbc\\xbcJ\\xcc\\x97=\\xb2\\xeen=\\x01\\x87\\x08=\\xd29\\xdb<4\\xe7\\x03=$\\x8d\\xf4\\xbd\\xdeH\\xb3\\xbc\\x052Y\\xba\\x17X]\\xbd\\t\\xa7\\xcf\\xbbn\\x04\\x04\\xbc\\xdaj\\x8e\\xbd\\xc8\\x04\\x99<=\\xef\\xbe\\xbb\\x8c\\xd9Z\\xbb\\t\\x98W=\\x12\\\"\\x1f<3\\x84?=\\xee\\xd4d<\\xc8\\xd6\\xde\\xbcT\\xf4\\xc1;ILn=b\\xae\\xaf<\\\"gP;yc\\xf6\\xbc>Y\\x90\\xbb\\xe7R\\xe1<0\\xaa\\x81\\xbb\\x83\\xff\\x15\\xbb8\\x08H=\\xca;\\x00\\xbb|\\x946=\\x10\\x1e&=U\\x8b\\\\<\\x1d\\x1a\\x87=1\\xeb\\xb1<\\xb7z\\xd3\\xbcj ?<\\x0c\\x9da\\xb9&p\\xd5\\xbc\\xb5\\xd2W\\xbd\\xb9\\xb8\\xf3\\xbb\\xfbE\\x90<\\x89\\xc2\\xe4<\\xcc\\xe9\\xce=\\n\\xf5\\x1b\\xbc\\x9cC\\xf5<\\x87\\xa8\\xa8<\\xd8\\xe9\\xc9<\\x9b\\xd3\\xc0\\xbc\\x96M2\\xbcQH\\xc2\\xbc\\x9aq\\xa0\\xbc\\xd6\\xbc\\x82\\xbda\\xdcF\\xbdJ\\x18w\\xbbk0\\xbc\\xbb\\xb7u\\x05=\\x96\\x9c\\xaa\\xbc\\x8c\\x92\\xd9\\xbcB*&\\xbd\\xbd\\x9dZ\\xbdU\\xebn;}\\xc9\\r\\xbd\\xd9n\\x90\\xbc]r\\x13=\\x92\\xfb\\x02\\xbdyV\\xfd=\\x87\\xe9\\x1d\\xbd\\xf5\\xc2s\\xbc\\xa0\\xf2\\xf5;=ca=9d\\xf6\\xbbs\\x9e\\x8f\\xbb\\xf5}\\xb8<%v\\xdc\\xbcQ\\xad\\xad=VR\\x10\\xbdq(0<\\x07\\x18\\xd3\\xbcv\\x82\\xb9\\xbbH5\\x92=W\\xa9\\xad\\xbcY\\x00\\xed<\\xb2\\x1d\\x15\\xbdWO\\xa6:\\x19 O<\\xb5\\x8e\\xbe;\\xc2t\\xd2\\xbc\\x00;\\x86=?g\\x19\\xbc\\x9e\\\"i;\\x1fP\\xcb<\\xda/\\xb6<\\r,\\xdb\\xbcpE\\xdf;\\x18\\xf8_\\xbb\\xedc\\x1e=~\\t\\xf8;\\x91\\xdc:\\xbdI=(\\xbd&5\\xa6\\xbb\\x89\\x93$\\xbb\\x16K\\xad\\xbcGy\\xa9\\xbd}\\xec\\xec\\xbcd\\x1c\\x91=\\xc6\\x86v\\xb8\\x00\\xc9 \\xbd\\r\\x8a\\xe3\\xbaz\\xcd\\xcd\\xbc\\xc0v\\x01=\\x9c!\\x7f\\xbb\\xf1\\xe1\\xb1<\\xe9\\xf1o\\xbd\\x02J\\xc9<\\xa5\\xfd\\x88\\xbc\\xbe\\xec\\x85=\\x90\\xb5\\x9b\\xbbq\\xaf\\x14=\\x01w#\\xbd2\\x03\\xfa\\xbbI\\xa1k\\xbdOFS=\\x8b2\\x18\\xbci\\x8c\\x05\\xbd\\nx.\\xbcO\\xb4\\x07=\\xe8\\xa3\\x0b=\\xcc\\x16S\\xbd\\xe2\\x00\\x0e=W\\xec\\x94=\\xbe\\\"g\\xbc\\x83t)\\xbcBF\\xd2<\\xb0\\x04\\xbd<\\xa0\\xa2\\x9a;\\x05x\\xac=\\xa3F\\x81\\xbc\\xdf\\x11n<\\xa8t\\x11\\xbbn\\xdb\\x82=\\x9eX\\xc1\\xbc\\xfc\\xde\\x11\\xbd4}\\xad:w\\xdbO\\xbd\\x05\\x86%\\xbd\\x0c\\xf6\\xf5\\xbb\\xfaG\\xa7\\xbd\\x8b\\xd4\\xe3;\\x04h\\x9d\\xbbF\\x156=\\x95\\x9b>=3\\xe2C;\\xfbL\\xdd\\xbc\\xcb\\xc0|\\xbc\\xa9\\xec\\x05=\\x9c\\\"\\xa0\\xbdVc\\xbc\\xbclC\\xc7;[\\xd3\\x04=\\xd5B\\xe5<\\xef@\\x11<[\\xdf\\x0c=\\xc3#\\xd4;=\\xd57\\xbc4\\x1a\\xa9\\xbc\\xbb\\xaa(=\\xbd\\x0c\\xa3\\xbc\\x12\\x80?\\xbc\\x16\\xec{\\xbc\\\"\\xe25;\\x15*\\x06<\\xc0Y\\xc1\\xbc?PR=)\\xdbB\\xbc\\xe5\\xa3\\x02=c\\xe2\\xec<\\xffu\\xd6\\xbc\\x8fL\\x88<\\xa5\\\"\\xa1=b<\\xf6=\\x929\\xe6\\xbc\\xc4E\\xfc:\\x88\\xd4\\xed\\xbc\\xef\\x07\\x1e\\xbdC\\xa3\\xd7\\xbc+\\xfc\\xdc=7\\xe9M\\xbd\\xbb\\xea\\x89\\xbc:\\x8b\\x1d\\xbd\\x009n=\\x9e)\\xda\\xbb:\\x11_=]\\x85\\xea\\xbcd\\x03\\xac\\xbc\\x84\\x80\\xaa<\\xc1/\\xcb\\xbc\\x19\\xc35\\xbc\\x83r\\x01=\\xbbO5\\xbd\\xfe\\x04\\x04=]f\\x04=`[\\xce\\xbaEH\\xcc<_%x;\\x1c\\xb5\\x7f\\xbd\\x14\\x1a\\xf2;\\x8e\\xff\\xb1\\xbb\\xd6\\xf3/\\xbd}\\x08\\x08\\xbdR\\x10\\xdc<\\xd8yN=(\\xda\\xcc;\\xdc,\\r\\xbdD_~\\xbd\\xf4\\xed><\\x80#\\x05=g\\x05\\x99\\xbcQ}\\x97<\\xc5y\\xc8;\\x14\\xc3\\x1d\\xbdy\\x9e\\xae\\xbd\\xaff\\xae\\xbc7\\xf9\\xc1=\\xbc\\x89$=\\xb7\\xfa\\x92:\\xc5@\\x85\\xbd\\xa7\\xc3Z\\xbc\\xcdEm\\xbd!d\\\\\\xbc`\\xd8\\xe6<\\x0e\\xa2\\x04\\xbc\\x04\\x14\\xa4\\xb9\\xdf\\xf6\\x00=\\xc8\\xb6\\xb4<\\xe64\\x89;\\xc5~\\x8a\\xbd\\xc7\\xaf8=\\xe6o\\x1e\\xbd4\\xea\\n\\xbd%\\xd8p\\xbbI\\xb1\\x9c\\xbd\\x84\\xe6\\x02=\\xc4\\xb9\\xf0\\xbcZ\\xcd\\x97<d\\x83\\x84<ncb<\\x07\\x91\\x80<^W\\x1e=iM5=\\x18kH:\\xed\\x98\\x9f\\xbd\\x89\\x0c\\x91\\xbc\\xfc\\xf5\\x16=\\x16\\xee\\xe2;\\x8cx6\\xbdV_\\xab\\xbc\\xb2%%=\\xaaA\\x88\\xbci\\xef\\x10=Yr\\\"\\xbd\\xf9\\x8d4\\xbc\\x04\\xcd=<$\\x049;\\x0c#\\xa4\\xbc\\xbe\\xb2\\xf2\\xbc\\x87\\xa4\\x92\\xbc^\\xdb%\\xbb\\xe3I\\xad\\xbb\\xe7\\xb1\\x05\\xbd\\xbaD\\x8d\\xbcEed<\\xc3_\\xba<\\xb8aT\\xbd#\\x88\\xc3;,\\xb6\\xf7<g\\xbf*;,\\x1f,<w\\xa7\\x97\\xbd\\xd4\\xba\\x88<Z\\\\\\x1c<\\x8d\\x17\\xcf\\xbc\\xa8\\xa3n\\tD\\\"Q\\xbbJ\\xeb)\\xbc%lg<\\xacT\\xb2=ZF;=\\x03\\xbe\\xb6<\\x0c\\x8b\\xec<\\x9b\\xa0E\\xbb\\xbb\\x1b\\xc8<\\x92\\x9c\\xe1\\xbc\\x0c:\\xbd\\xbc\\x1d\\x82 \\xbc\\x0bno\\xbc\\xa3DY<VFv\\xbd\\x0bj\\xa3\\xbc\\x83\\xe8\\xdd<\\xb1{\\x10\\xbd\\x9ebt:\\xe1\\x13i=\\xb0\\xb2\\x81<\\xb9\\r&=\\rT-=[t\\x0c\\xbd\\x9am\\xaf\\xbbK\\x97C;g\\xfa3=\\x87_\\\"<\\xa6\\xb3\\\"\\xbd#\\xcd\\x01\\xbc\\\\\\x9f\\x85<\\xb5\\xa0\\x0e<I:/\\xbd(\\xe1\\x17\\xbd\\x19\\x01\\x88\\xbc\\xa8$\\x85\\xbd\\xe5\\xf6.\\xbb\\x04p\\xe2<\\xba\\xa7p<N\\xe2\\xb7\\xba:c\\x06\\xbc\\xdf\\xd9\\xa3\\xbc)\\x97\\x7f<pdJ=\\x9a\\xa3\\xb0<a\\xdae\\xbd\\xf4.\\x00=\\x90\\x06\\x1f\\xbd\\xdc\\x8a \\xbd\\xb24\\x8a<\\x8e\\xb0\\x0b\\xbc39q\\xbd\\xac\\xac\\x97\\xbc\\xc3]\\xde<\\x84\\xd4g\\xbc\\x9d\\xaf:=\\xdc}4\\xbcQ\\xd6\\x8f\\xbc\\xf8\\xc3\\xc2;\\x98\\xb9:=\\xa0\\x8e\\x91\\xbb\\x8f\\x83\\x08\\xbb>\\xf7u9\\x8d\\xb3\\xfe<Lw\\xe8;\\xe5\\xf14\\xbd\\x08\\xc3\\x93=e\\xe6\\xd7\\xbc\\xc9\\xab\\x02=\\x1a8\\xcb<\\xd3\\x0f\\x86;\\x13\\xa2\\x04\\xbc\\x1cwU<\\xabJ\\xec<\\xd7D\\x8a<\\x80\\xf0\\xfb\\xbcr\\xfc#;\\xe6\\xe0\\x05=\\x92)\\xc1=E\\x84\\x1d<\\xfc\\x8aS\\xbd;?\\x95\\xbcx\\xd0\\x11\\xbd\\xf7j\\x9a<\\x9f\\x08\\x08\\xbd\\x05\\x0f\\xbd<\\t\\tf=oG\\xa2<\\x8a\\xe4\\xd5\\xbc\\x83\\xa3\\xcb<\\x1e\\x1b\\x8b\\xbc\\x19\\x1em\\xbdj\\x7f)=\\xef%\\xcc:0\\\"e\\xbbi>H<\\xc4\\x1b\\x01\\xbd\\xdc\\x10)<\\xddow<\\x1a\\x86!=\\xa4\\t\\xb5<\\xa1\\x7fZ\\xbc\\x9fB{\\xb9\\x15\\x8be;\\xbc\\x14\\xfb;\\x1cS\\xba;\\x9b\\x0c\\xb5:\\x0b\\x8d]\\xbc\\x9d\\xb8\\x1e:\\t\\x9d\\n\\xbd\\x1e\\xaf.=8\\xf2\\xaa\\xbc\\x02\\xaa\\x0f;\\xb8\\xd0H=4\\xbb\\x9b=\\x9e\\x97\\xc4\\xbb\\xa0\\x87\\xa1;m\\x13]\\xbcB)\\xb7<\\n\\xa5\\x13\\xbbk\\\\\\xa7<\\xef\\xef\\xa9<\\xba\\x86\\x1c=h`\\x83=\\xfb\\xf1g;\\xb9w\\x1d=\\xf7p}\\xbd\\xe0JC:\\x89\\xfb\\x88=\\xf2\\xa15\\xbd^\\x1d\\xb5\\xbc\\xd2\\x10\\x8d\\xbd\\xde\\\"\\x81<\\xe1\\xcb\\xfe\\xbbI\\x98\\xd9=N\\xe9\\xac\\xbc\\xc2HE=@0t\\xbd\\xf5\\xd2)<\\xb9\\xbd\\x8c\\xbch\\x06f<q\\xf50\\xbdbW\\n\\xbd\\x1a\\x10\\x1a9G\\xf8\\xb2;s\\xb3\\xc5<\\xaf\\xe0\\xad=\\xff\\xd4\\x08<\\xa6lF=\\xa3\\xee+\\xbdG\\x06\\x1a\\xbd]&\\xcd<R\\x00\\x85\\xbc\\xf5\\x96\\x90<)/T=b\\xa2\\xa59\\x1f\\xd5\\x89\\xbc\\xa4\\xa8\\x85<\\xbb7\\xc4<u\\x03\\xff;\\xc2\\xd4\\xc1\\xbc\\xd8\\xf0\\xa4=\\x19\\xc8)=\\xeb=\\x0b\\xbc\\xcb\\x93\\x02\\xbc\\x9f?\\xaf;\\x17\\x9d\\xd2\\xbbG\\xb6Q=\\xc3E,=\\x8c\\xbd\\xd0<\\xa1\\x8d\\xa4<$U\\xdb\\xbc\\xaf\\xb3s;Z\\x0f\\t;\\x14\\xcd\\x85<p7)=\\xc1\\xba\\x8b\\xbc2\\xf0!=Lh\\xd7<\\t\\xbe\\xe9\\xbc\\xcf\\x9f\\xbc\\xbce\\\"\\xb6\\xbd\\x9a\\x16\\xe1<\\xe0M\\xd8<\\xf1t\\xc5\\xbb\\xa6;\\xc4\\xbc,\\x1e\\xd2\\xbb\\x8fl[\\xbc.\\x10\\x8b<\\x1a\\x93\\xa0\\xbb\\x99\\x02\\xc3\\xbc\\xe1\\xb7`<\\xacq\\x16\\xbda\\x1b\\xa1\\xbcs#\\xbc\\xbcR)\\x9f\\xbdnf\\xf7\\xbcU\\x9d\\xbd\\xbc\\x0c\\xe8g\\xbb\\t\\xac\\xac\\xbc\\x8f)\\x8e\\xbc\\x00\\xd3\\x89\\xbc3\\x88+\\xbc/ \\x82\\xbd\\x10\\xdf0=|\\xcf0\\xbd\\x9c(\\xd9<=\\x1d\\x1f\\xbd\\x91\\x089\\xbb\\xb2\\xfa\\x9d\\xbb\\xec2\\xe9\\xbbPP\\n\\xbd\\xe5\\x86.<\\xde\\x08\\xce\\xbb]\\xea\\x88<^\\x8a6;\\xb8\\x11F;\\x0bA\\xda\\xbc\\xed\\r\\x92\\xbd\\x1d1\\xcc\\xbd\\x96\\xa5\\x1c\\xbc[\\x9a!\\xbc\\xb6\\xf0[\\xba\\xc6^=<\\xae\\xefY=$ja\\xbd\\x8c\\x0e\\x03=t\\xca\\xc5\\xbc\\x07\\xbb\\x85\\xbd\\x95\\x8b\\x85=\\x9fQ8;\\x04T\\x98\\xbc\\x0b\\xd6\\x00\\xbdw\\xb5(\\xbd\\xa7\\xb5\\xb6\\xbc\\x1a`\\xbe\\xbbr\\xd6\\xa6\\xbc\\x8eEq<\\x1e\\xcfx=^S\\x0f\\xbc\\xd7\\xa3\\\\:\\xb0\\xd3\\x1f=\\xa6\\x00\\xde\\xbbq\\xc98=PY8\\xbcEK\\n=-y^\\xbd>\\x86\\x8b=\\x0ep\\xdf\\xbc\\xcf\\xce\\xf8</\\xc7d;zGc\\xbbU6\\x1e;\\xc7QT=\\xf2\\x97%\\xbd\\xc4\\x0ff\\xbc\\x9bt\\xad\\xbc{a\\xd9;cQ^\\xbc9-q\\xbdFc\\x07=<\\x962\\xbd\\xac\\xae\\xf1\\xbc\\xda\\x8fm=k\\x9b\\xbc=9\\xd70<\\xbb\\x83(=T\\xfak\\xba\\x14\\x8b\\xa9<\\xb7U\\xb1=\\xc1\\xb1\\xff\\xbd\\xf6\\x11$<Z\\xa2\\xc4\\xbb\"\nHSET bikes:10048  model 'Pallas' brand 'Peaknetic' price 1714 type 'eBikes' material 'alloy' weight 9.5 description 'If you\\'re looking for the best commuter eBike for your trip to and from the office, that will keep you rolling from home to work. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\x9e\\xf9\\x06<\\xf0\\xc2$\\xbc\\xe4@V\\xbd\\xb3\\xe6\\x9e;\\xfa\\xd7\\x83\\xbdy\\xe7L<\\xe2\\x99\\x94\\xbc3^\\xa3\\xbd\\x82\\xcf\\xe9<\\x82lG\\xba\\x96fY\\xbc\\x1f\\x1at\\xbd\\xef\\x8d\\x05=\\xd8\\xc6F=r\\x92H=\\xbe\\x0e\\xe2\\xbc\\x1b5C=\\x1c\\x9c<\\xbdA\\x12P\\xbd\\xbe\\xe8\\x04\\xbd\\x14\\xb9\\x13=\\x05;G\\xbc\\x1d\\x02\\xa9\\xbc\\x90\\xf9\\x06\\xbc\\xc2\\xd5\\xd5\\xbc|\\x98\\xb3\\xbb!~\\r=\\xf2\\x95\\xc79f\\x11\\xdb<\\x1fa\\x96;\\xd9)\\xb2\\xbc\\x9foK\\xbd\\xbe\\x9b\\x19=\\xbc\\xd0\\x16=\\xe3\\xe8\\x92\\xbc\\x16XD\\xbdZ\\x0cR=\\x7fp\\xe7;B+\\x8b\\xbd\\x94\\x84\\x93;\\x9f\\xb5\\x98=\\xfa\\xed\\xc2\\xbb\\x0b\\x03\\x8c<~\\xa6\\xc2<\\xa9\\xebd;2\\xc8:\\xbd\\xe2K\\xdb<.\\xb4\\x8f\\xbc]n\\xc1\\xbc+\\x03*<\\xba}\\xa3\\xbbhh\\xb2\\xbc\\xb1\\x97\\xc4\\xbc\\xb4P\\x1e\\xbd\\xad\\x97\\xe8\\xbc\\xf9\\xda\\x0e\\xbd\\xcb[\\x82\\xbc\\x91\\xec0\\xbdw\\rr\\xbc|\\x14\\x8e=\\xe2`\\xbe\\xbc[\\x14\\x9c<\\x04K9<\\x02T\\x94<\\x89\\xa44=\\xf7u\\xf6\\xbcn\\x88i<\\x99Y\\xc6<\\xa6\\x86{<\\xa2\\x8bP\\xbc\\x9c\\xfb\\xcd;,\\xf0\\xda;\\x0c\\xdb\\xeb;:R\\xce\\xbd\\xd3 6:T/Q\\xbd\\xfa\\x16\\xc9\\xbc\\xa5\\x81\\xc3\\xbc\\xd3\\x0e3\\xbb\\x94\\xfet=\\xda\\x8b<9P\\x95>\\xbd\\x07u\\xcc\\xbc\\x9a\\xaf\\x01=\\xd4\\xe0\\x02\\xbdX@\\xce\\xbc\\xa6f\\xb8\\xbc\\x97\\xe4\\x94< {P<\\x13\\t\\x18=\\xb8\\xcb\\x00=,Z\\x83\\xbc\\xe7uS;{\\x95\\xe8\\xbb\\xf6\\xb8R<&\\xba\\x83;\\x1bE\\x04\\xbdB6\\x95\\xbc\\xe3q\\xc8<\\xf2\\x8f%\\xbcm\\\"\\xa3\\xbdO\\\"\\xa0<\\x89Q\\x9a\\xbc\\xb4PP\\xbc\\x17{{=&\\xa0\\xee=n\\\\\\xf5<\\x1d~l<\\x180?\\xbd\\t\\xd1\\xd2\\xbc\\xdam*=\\xc4@);\\x08E\\n=\\xae\\xf8\\xdc\\xbc\\xfbpW\\xbd\\xf2\\xb5\\xb1\\xbc\\xc6\\x91\\xa9\\xbab\\xc6\\x96<n\\x08\\xd1<<\\xdf\\\"\\xbcu\\xe6O=\\x98z-<\\x11\\xe4g\\xbd\\x96\\x94c=\\x07\\x05l=\\x96\\x19t<\\xf6\\x12\\xd5<.d\\xa2\\xb9\\x16u\\x93\\xbd\\xee\\x96\\x96\\xbd\\xe16<\\xbd\\xb3\\x90A\\xbd\\x8e\\x10\\x8b<\\xadZ\\xd5\\xbc\\x9e\\xc3\\xdc\\xbc\\x83cN=\\xd0\\xc5K\\xbd#\\x96\\x8b\\xbdR\\x7f3=\\xdepK\\xbc\\xe5\\x0fy\\xbd\\xf9\\xecK\\xbd\\xc2{\\x80\\xbcL\\xbf\\xa8<f\\xf2\\xac\\xbc \\xf1\\xa2<_\\xdee;\\x9f\\xb4\\n<\\xd3\\xab\\x99\\xbc\\xe4\\x10w\\xbb\\x90b\\x92;\\xf7E\\xfc<\\xfa\\x165:B\\xeeH=oO\\xad:\\x1aj\\xba;\\xa8\\n\\x8d<\\x96\\x12\\x15=\\x057\\x06=\\r?G\\xbd\\x8a\\xf9S=.\\x0b1\\xbd\\xac\\xcc\\xc1;\\n\\\"\\x81\\xbc\\x8d/\\x9a<\\xbcS\\x9a<\\xfc\\xd0g\\xbcv\\xe3\\x92<\\x80F\\xae\\xbcm\\xf4\\\\\\xbcx:_\\xbcp\\xd6\\xa8=<~A\\xbcgp,\\xbd\\xfd\\xb1\\xbd\\xbc\\xa67\\xbd<\\x9f\\xc7\\x0e\\xbd\\xef\\xfbM=\\xad_\\xb1\\xbc\\xa5?\\x06<6\\xa3\\x9a\\xbb\\xe0\\x92,\\xbc,\\x8fX\\xbb\\x08\\xe4\\xa9<$\\xbe\\x91<8\\x02\\x8b\\xbdt\\xc8<=\\xfa\\x11S<\\xff\\xf65=\\xa7\\x0b\\xc4\\xbc\\x88\\xf7\\xe1<\\xa2\\x9a\\x94<w\\xa6\\xa5\\xbd9\\xfdg=\\xf3C><\\x9e-\\x99=\\xe4@\\x1b;\\xe6u\\xbf\\xbb\\xedd};4y\\x1c\\xbcB\\xe6\\x1f<\\xb7\\x0f\\xe9\\xbc\\xe2c\\xc8\\xbd\\x96\\x15\\t\\xbc\\x9bh\\x0e\\xbd;\\xc7f=U\\xa4K\\xbd)\\x1c\\x9d\\xbc\\xddC(=I\\x17\\t<\\x98\\xc1\\x8d\\xbc\\xea)\\xb3=\\\\)Z=\\xd8e\\xe4\\xbbz\\x9e)\\xbc\\xe3PM=Q9\\xdb\\xbd\\x1a\\x19&<.z|<r\\xf7.\\xbdG\\x8b\\x91\\xbca\\xb6\\xf8\\xbb\\xf8Pp\\xbd)\\x07\\xfa<\\xeb\\xc7\\xd4\\xbcYjL<*\\xe53=$h\\xfc<\\xb9\\xb3B=\\xf3\\x94\\x07=\\xb6<\\x7f\\xbc\\xfd0\\xf1;\\x0f_\\x99=S\\\"\\\\<\\xa57\\x9d<\\xb0\\xaa\\x8c\\xbd\\x9b\\xba\\xf4\\xbb\\x9d\\xa2\\x0f=\\xed4\\t<}M\\xa1;\\x88\\xb7\\x1c=\\xaa\\x04m\\xbc\\xab\\xca\\xdb<<|\\x96<\\t\\xf2p;\\xa4\\xc1u=\\x86 \\x07=\\x92\\x90\\xa4;)5\\xeb\\xbc\\xc2\\xef\\xa3\\xb9\\x92\\n\\x03\\xbda\\xfaS\\xbd5\\xc9\\x08\\xbd\\xe5G\\x10=\\xce\\xadd<\\x15\\x06\\xb4=\\xd5\\x07\\xa3\\xbcc9\\xb6<\\x8e5\\xad<\\xc5\\xdb\\xa7<,\\xfcR\\xbc\\x82.q\\xbcD\\x8c\\\"\\xbdw\\x05\\x8d\\xbcD\\xa3\\x85\\xbd||\\xef\\xbc\\xfc\\xf1\\xd5;.\\xa5\\xac;\\xcf[\\x1e=\\xa3\\xcbs\\xbc\\x033\\xf0\\xbc\\xc7\\t\\xf9\\xbc>\\xc2\\x08\\xbd\\x17\\x85\\x01<\\xce\\x97%\\xbd\\xe32%\\xbd:\\xa9\\x93<\\xd8j\\x00\\xbd\\x08\\x83\\xdd=B\\x13\\xad\\xbc\\x83\\xd39<\\x026!<\\x90c\\x97=<\\x85\\x06\\xbd\\n\\x08\\x06;L.\\xe4<\\xad\\t\\xa2\\xbc\\x9d\\\"\\xa7=\\x07\\xc2\\x88\\xbd\\x15\\x8a\\xfc;\\xa6{^\\xbc\\xb0y\\x08\\xbd\\xc1\\x1a#=\\xd8^\\x0f\\xbd\\x14k\\xa4<8\\x15#\\xbdW\\xc8\\xb2\\xbc2 \\xdd<,\\xca\\x87\\xbc\\n\\xc2U\\xbc\\xd7\\xd8\\n=\\xe3\\xed\\x85\\xbb\\xe0Z\\xf0<n\\x87]<\\\"\\x16}<\\x953\\x80\\xbb(\\x82\\xe3\\xbc-\\x96\\x82\\xbb\\xc8\\xf1]= H\\x81<\\xde\\xceP\\xbd2\\xea6\\xbdi\\xd9\\x1d;\\x00bG<\\xa5\\x94\\xe8;\\xe4C\\x83\\xbdS\\xf8u\\xbc\\x00\\x12\\xc1=\\x9c\\x88k\\xbb\\xdd-j\\xbd\\xc4\\xdc\\xd6\\xbb\\xe8\\x02\\xf7\\xbaKQR7\\x97/\\\"9,\\xc7\\x85<N.v\\xbd\\xce\\xe5\\x82=\\x86h\\x9e\\xbc\\xcf\\x12\\xaa==\\xa08;\\xf4\\xd2/<\\x93\\xa9\\x92\\xbb\\xd5\\x89\\xc9\\xbc\\x17*i\\xbd\\xad\\xc7\\x1c=d\\x85\\x15\\xbc\\xcd|\\xbf\\xbc\\x88\\x06\\x8c\\xbc\\xf9r!=\\xcdY\\xae<\\x93,\\xd5\\xbc\\xd45N=R\\xf2\\x92=\\x80\\xca\\xa9\\xba\\xffn\\xb8\\xbc \\xee\\xb0<.\\x19\\xb6<\\xae=\\xfa\\xbb\\x99\\xd9`=c\\xf4\\xad\\xbc\\xce\\x89\\x9b\\xbc\\x1b\\x0bv\\xba0\\xccc=\\xa7\\xd8\\x82\\xbc\\xb9\\x12\\xb0\\xbc\\x82LR<\\x84\\xe8Y\\xbd\\x92\\xd8B\\xbd\\xa1\\xbaK\\xbc\\xd5\\xc7L\\xbd\\xf8D\\xdb<\\x80\\x82x\\xbb?\\xcaP=d\\xdb\\xe9<\\xf7\\x80\\xd2\\xbc>\\xa5\\x16\\xbd\\xa4tI\\xbcc\\xff\\xe8<\\xa4[\\xe5\\xbc?\\x9d\\xd0\\xbc\\xa6\\xce\\xe0<>\\xd8\\x08=\\xdd\\xf2\\xe1<\\xad\\x9e\\x05<\\xf0\\x07\\xb2<T\\xd3\\x8a<\\xc4\\x11R;\\xdc\\xcb\\xb1\\xbc\\xeeXY<UGN;\\xc7\\x8f\\xad\\xbcvh\\x01\\xbd\\xc4\\x0cX<\\xcbj\\x9a<\\x97\\xc0<\\xbc\\xae\\x84z=\\xe2R\\xb1\\xbans\\xcb<I\\x99\\x10=#/\\xf2\\xbc}\\xc3l\\xbc`\\xaa\\x8e=\\x0e\\xa9\\xdb=S^(\\xbd\\xb7-\\xd3;\\xb8\\xb7\\xe2\\xbc\\x91\\x82\\x8d\\xbc9\\xed\\x8a<L\\n\\xb9=P+[\\xbd1\\xfa\\x04\\xbd\\xe8\\xb7e\\xbd\\xca:X=\\xc9V\\xcf<\\xe3K\\x1e=-\\xd67\\xbd\\x01\\x01\\xeb\\xbc\\xe1\\xcfR<\\x9eP%\\xbd\\xf802<\\xd2\\xec\\t=\\x9c\\x14M\\xbd\\x8c\\xb89=\\xc3\\xbe\\x14<\\xeb\\xe2\\xc4\\xbb\\xcd\\x82\\xf8<\\xa0\\xb6\\xc0;a\\xbe/\\xbd\\xbb\\xf2\\x81<\\xfb\\xbfe\\xbc\\x04Q\\xf0\\xbc\\xde\\x17\\x99\\xbc\\x99;8=A\\xfd\\x85=\\xfa\\x14\\x87<\\xf6\\x91\\x0b\\xbd=\\x03\\x9e\\xbd\\xfe\\xb9\\xd6\\xb9;\\x10\\xc9<\\x1e\\xf4\\\"\\xbd\\xef\\xa7\\xa5<%6\\x1f\\xbc1\\x1e\\\"\\xbd\\x0b\\x14\\xae\\xbdL\\x9fu\\xbb\\xe5\\x97\\x9d=|\\xff\\x0f=4M\\x13<\\x0cy\\x16\\xbd\\x8b~.\\xb9\\xfa\\x1f\\x80\\xbdwp\\x93\\xbc\\xd7={<\\xc7\\x90{\\xbc\\xf8\\x172<\\x9a\\xecB<(\\x8c\\xbe;\\x90!)<\\xed\\xfa\\xc7\\xbd\\xb2\\xe5\\xa2<\\x04\\x83\\x13\\xbc\\xbd\\xc1}\\xbdkYo<efs\\xbdq\\xaa\\x01=\\xf8\\xca\\xd1\\xbc^q\\xdb<\\xfd\\xee\\x04=\\\"\\x14\\xd4<\\xd6\\x8f\\x03=\\x02\\xa2f=\\\\W.=\\x8a\\xbb\\xfd;\\x8a\\xa8\\x8d\\xbd\\x1b\\x93\\x18\\xbdzy\\x83<\\xff\\x93%\\xbc\\x98\\xb8\\xaf\\xbcIK\\xca9:\\xfd\\x0f=a\\xb2n\\xbc\\xd5\\x1c7=\\xa4\\x89\\xdf\\xbcU\\xc1\\x8a<\\x0e\\x8f\\x18=lu\\x85;:\\xd9\\xa9\\xbc\\xfc\\x8b\\xf2\\xbc\\xd5\\xaa8\\xbc\\x93Cz\\xbbGU\\x17<\\x0ba\\x14\\xbd\\xdb\\xf4\\xc8;,8\\xc8<\\\"!\\x8f<^mb\\xbdG\\xbf\\x84\\xbc\\xf3\\x86C<fa\\x83\\xbcv\\xc3\\x9f;\\xb3\\xae\\x80\\xbd\\x85\\x17\\x1a<\\x00\\x13u<N\\xce\\xa3\\xbcN@{\\t\\xb9\\xc3\\xf7\\xbc\\xf8l5\\xbc\\xc6\\x10\\xb3;\\x070\\x85=\\x99$k=\\xc9\\x06&=\\xc5\\xea\\n=\\xf2\\xe0\\x91\\xbc\\xe5c\\xec<\\xad\\x9e(\\xbd\\x19F\\x86\\xbc8R\\xa3\\xbb)\\x051\\xbc&\\xce\\xb7<\\r\\xc7\\x81\\xbd4\\x87]\\xbc\\x0b\\x0f\\x9a<n\\x06\\x11<\\xd4\\xf2U<H\\xbf{=AZ\\xf0;\\x1bg|=,\\xb3<=\\xf1e\\x14\\xbdK\\xdaB\\xb9\\x03\\x14%<\\x8c\\x19\\x19=\\xc0k^\\xba\\xa4\\xee;\\xbd\\x0244\\xbc\\x0f\\xf9\\xcb\\xbb+\\x18\\x8d<xG4\\xbdp/*\\xbd\\xb1\\xd1\\x96\\xbb\\xb1\\x04X\\xbd\\x06\\x8e\\xa6\\xbc\\x19\\xca\\x00<\\x8f\\xef\\xae<v-0;\\x0f\\x8c\\xd0\\xbbS\\x83\\xfd\\xbb\\xc6\\x01\\x83<\\xd0\\x8d\\xfb<\\x9fU\\x12=U\\xfcU\\xbd\\x8b\\x05t=\\x95W\\xa3\\xbc\\xa8\\xb5\\x14\\xbd\\x86\\xfb\\xf4<f\\x91\\xc9\\xbc\\xd8W\\xdf\\xbc\\xf9\\\\o\\xbc\\xa0\\x0eV=z\\x9b\\xb1\\xbc\\xfc\\x1cG=y\\xcd@\\xbd\\xd3\\x96\\x00\\xbcf[)\\xbb\\x86+\\xb7<\\x11s\\x96<\\x15\\xd7\\xfe\\xbc\\x8c\\xe5\\xb2\\xbb\\xb2\\x90\\xde<\\xc2\\x9f\\xd6;\\x15\\xd7\\xb8\\xbc\\x19Qy=\\xa1\\x8cQ\\xbd\\xab\\x80\\x8b;co\\x86<c\\xa5f\\xbcX\\x8f\\x17<\\xf2\\xcc\\\"\\xbc\\x1d\\x17l=Q\\xd0Y<\\xe8g\\xfc\\xbc\\xaf\\xc3\\xd7;\\xf6\\xfb\\x84;Q\\x02\\x93=mz\\xbb;K\\xc5\\n\\xbd]\\xf6I\\xbc\\xd9\\xd0\\xfe\\xbbE\\xf9\\x17=\\x89\\xc9\\x02\\xbd\\xb6q\\x88<\\x11@C=\\xeb\\xd9\\xf3;+\\xb1\\xd7;\\x19\\xe2\\xdc<\\xa3\\x1a\\xf6\\xbc\\xa3!\\xec\\xbc\\xe9\\xddO=\\xad\\x8e\\xeb:<\\x1f\\x9f\\xbc\\xf0A\\x0b<\\xe2\\x02Y\\xbdr\\t\\xbc<\\xb2}\\xda;G\\x1e\\x95<\\xe8K|<\\x89\\xe6o\\xbc\\xbe\\xeb\\xd1\\xbb\\xf41R\\xbc\\xe4\\xe7\\xca;\\xa4\\xe4#<\\x96s/<\\xbf8\\x1e\\xbc\\xd1\\xc7\\xa0\\xbc\\xa8\\xa07\\xbd\\x10\\xf2\\\"=\\x08\\xbb\\xb7\\xbc\\x16\\x10\\x14\\xbdA\\x97d=\\x85\\x98n=q\\xe1\\x9b;8\\xe8\\xc0\\xbb\\x8fMM\\xbb\\x07\\x96\\xb2<\\xb2V\\xbd:\\xcd\\xab\\xff;-\\xc20=I~\\x7f<c\\x1fY=5\\x15=\\xbdp\\x96\\x9a<\\x1e\\xfe\\x81\\xbc\\xa1\\x8b\\xa5\\xba[\\x80\\xb9=.\\x07\\xe4\\xbcOi\\x17\\xbd\\xf0\\x94\\x94\\xbd\\xa9\\xaa\\xb5;\\x9c\\xc1\\x1d\\xbaV\\xa5\\xdc=\\x9a*\\x93\\xbc\\xa8gq=\\xaf\\x86}\\xbd{4\\x89<\\xdb\\x9a9\\xbbf@\\xd9\\xbaX\\xa8\\x00\\xbd\\xe2\\x981\\xbd\\xe9\\x81\\xf5\\xbb+K\\x8a\\xbc\\x89\\x17\\xd4<\\\\\\xc1\\xa3=\\xd6h\\xf19\\x10\\x02\\x00=Yf\\x8e\\xbc#\\xc4\\x8f\\xbd\\x81\\x9d?;\\xc3\\xee\\xdc\\xbc\\xa3\\xaf\\xa9<t.\\x9d<\\x8a$\\xcb<\\x92\\xb7h\\xbdF\\xff\\xee<Z\\x17\\xae;{\\xcf\\xb9<p\\x8bM\\xbb\\nK\\xc5=8\\x0b\\x9d<\\x91bF;z\\xc4\\xa6\\xbc\\xc4l<\\xbcl\\x8f\\xf9;\\x9d\\xca\\x84=\\x99\\x1fW=<\\xcd\\x06=\\x03\\xeb\\xbb<\\x81#I\\xbb\\xd1uC\\xbcy|\\xa6<\\xa1\\xe5\\xd3<x\\x1e-=\\x930)\\xbd\\xcf\\x8bR=\\xa4\\xa9<<|m\\xed\\xbc\\xcc\\xff\\xe9\\xbc\\xcc\\xf1\\xaa\\xbd1\\x18Q<+\\x7f\\xfa<\\xbf\\xd0\\xfd:Q\\xfa1\\xbd]\\xb5\\x08<^I\\x9b\\xbcA\\xb6\\xa7:\\xf5\\xbb\\x84\\xbbM\\xae5\\xbb\\x13\\x86\\\"\\xbcO\\xaej\\xbd\\x91\\xf1p\\xbc\\x85\\xed;\\xbd\\x0bt\\xaa\\xbd\\\"\\x1eH\\xbd\\x9f\\x7f\\xfb\\xbc.\\x1f\\x98<\\x81\\x17\\\\\\xbd\\xd8\\xe0\\x0f\\xbd\\x0ec\\xb6\\xbc\\xcf:U\\xbcE\\xf8\\x86\\xbd\\xf0\\xf9)=H[V\\xbdu\\\"\\xfa<n~\\x82\\xbc\\x1f\\x9e\\xb7\\xba\\xf3DL\\xbc\\xa2(\\xaa\\xbbc\\x06\\xc0;\\x84\\x06\\xaa<`v1;\\xf5\\x176<)\\xf9\\x97<\\x1d{2<!\\xc5\\x05\\xbd\\xbeT&\\xbd\\x83M\\x95\\xbdG\\x00G<(\\xbf\\xe5\\xbc\\xb1\\xae\\xea\\xbb\\x93r\\xbd<\\xa9\\xe1/=N\\xb7!\\xbd#\\x9f^=\\x044:\\xbd\\xb5\\x9e\\x80\\xbdr\\xafH=\\xd8\\xca\\xcf;u=-\\xbd\\x0b\\x1bS\\xbdrd\\xcf:\\x9dJ <\\xec\\xc0\\xb2\\xbc\\x01\\xcc\\xb49\\xd0k\\xf1;u2t=\\x89o\\xc2\\xbc\\x90\\xc0\\x05\\xbc\\x80`\\x14=\\xaa\\x7f0\\xbc\\xfep.=vH#\\xbb\\xd6\\xf3\\xf0<\\xb1\\xa2X\\xbd\\xcc\\x95\\xa2=)\\xb2\\xef\\xbc\\xc4\\xfd\\x12<#)\\xce<\\xd1\\x9a\\x9d\\xbb\\x81\\xda-<z\\x9d\\x82=I`\\x0f\\xbd\\x0c\\xef/<\\\\\\xc5#\\xbd\\x81\\xad_<\\x85=j\\xbc\\x8f\\x81\\xee\\xbdb\\x88\\n=\\x0c\\x8d\\x01\\xbd\\x9d\\x1d\\x0b\\xbd\\x1e=s=2L\\xb6=\\xb3`\\x8f\\xbc\\x03\\x13\\x1a=\\x91\\xd5\\xd4<`.\\xa1<O~y=b\\x8b\\xd6\\xbd%\\x88\\xf0<D\\xcf0\\xbc\"\nHSET bikes:10049  model 'Rhea' brand 'Peaknetic' price 4130 type 'Kids bikes' material 'aluminium' weight 15.3 description 'The innovative braking system on this bike has been a game changer in the kids’ bike world. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\x17\\xc6Y;\\xa8D.=\\\"\\x01\\xe5\\xbc\\x8c,$;\\xe5\\xa1\\xba;\\\"\\xbe\\xd79~x\\x94\\xbc\\xb0N\\xc5\\xbc\\xd0\\x91\\xd7=\\x80\\xd0U=]\\xc0c<\\x13\\x97M=\\x8c\\xf8\\x96;\\x92\\xb6\\xb8\\xbc\\xc4\\xcd\\xb8<\\x7f\\x9f\\xdf\\xbc\\x0e;V\\xbc?\\x17\\xc6\\xbc\\x13\\xbc\\xb3<^\\xe0a\\xbdv\\xf4\\xe8<Gm\\x83\\xbd\\xc2.\\xac\\xbbc|\\n\\xbc\\\\\\x11\\xf0\\xbb\\x03\\xa6\\x04=l\\x85\\xec<7\\xb7\\xa1\\xbb\\xef\\xfa`\\xbd/9\\xf5=\\xb7\\x1a\\x16\\xbcZF\\x13\\xbcG\\x96\\x9c<k!\\x06\\xbc=\\x08\\xa9\\xbc\\xa0\\x02m\\xbbx\\xe9/<\\xa2\\x81\\xe0\\xbc\\x8es+\\xbdM\\xad\\x1c<\\xd8c\\xb5<\\x92u[\\xbd\\xd7\\x9a]<\\x97\\xd4\\xbb;\\x8d\\x97&\\xbc0\\xa7\\x1c\\xbdnu=={\\xb0r\\xbd\\xaa\\x80\\x1b\\xbc\\xc8\\xe7\\\\\\xbb\\xb2\\xd3\\xaa<\\x97\\x9a\\x82\\xbcv\\x82X\\xbd\\xd6\\xdc\\xf7<\\x0f\\xa9\\r\\xbc\\xe7L?<\\xb9\\x90\\x88<\\xb1s\\xb5\\xbc!.\\xab\\xbc\\xf25\\xd3=\\x06\\x0f\\xa4\\xbc-(\\x0c\\xbc\\xb8~P=\\x9f\\xa2@=\\xe9y9\\xbbV\\xa0\\x8a\\xbc\\xe9\\xca\\xd6\\xba\\x18\\x16\\xe6\\xbb R\\x99<02\\x8b<]\\x8a\\x11<\\xf9\\xb7b\\xbc8|V\\xbb\\xc2\\x08\\x0c\\xbd\\x85E\\xf2<Q\\xe8K=\\nB\\xe0<-K\\x14\\xbc\\xa8\\x87d\\xbdv\\\"9<\\x02\\xed5=a\\x15~\\xbd\\x19\\x9e\\x03\\xbd:Y\\xec\\xbd\\xbb\\xc35\\xbd\\xdcM\\xd9\\xbc\\xda\\xcc\\x0f<\\xdd\\xd2\\xcd<\\xa6*\\xd2<x\\x91\\xfc<\\x86\\x11p\\xbd\\xf0{\\x0e\\xbd\\x0e\\xbcD\\xbd\\x11\\xb2W=\\xbe\\xb4\\xfa\\xbb\\tL#=\\xd7\\xbd\\x83\\xbc\\xd4\\xa1\\xb4\\xbc\\xbd\\xfa\\xb0<l\\x94i\\xbc\\x7f\\xf6[=\\xc6\\x9e{<bU\\xaf<k>\\x86\\xbdCi.\\xbd\\xe1\\x87\\xd8=\\x8dg\\x81=\\xd3)[\\xbba\\xd2\\x9f<b\\xc3\\x1c=\\xa2\\x8d?=u\\xdb\\xec;(\\xde^\\xbc_\\xf4-\\xbc}\\xa0*\\xbd\\xedz\\xe9\\xbc\\xb4\\x02\\x07\\xbc\\xfb\\x1a\\xbb<\\xf6\\xfa\\x81;e\\xa0x<\\xa4\\xc9q<;\\xeb|\\xbbp\\xe3\\xdd\\xbc\\xc7\\xb6\\xb2\\xbc8\\x19<=\\x06d\\x8c\\xba\\x852\\x9c<\\xe4VJ\\xbc\\xdc\\xcf\\x9f\\xbc\\xfd&\\x08\\xbdz-\\xe1\\xbc\\x1e\\x85\\x8a\\xbc\\xdb\\xf4\\xa8<\\xda\\x10$;cd\\x94=\\xcd7U=\\x80\\x1e\\xfe\\xbb\\x80\\x85\\xb2<\\xd4\\x1eC<\\xb7\\xe2\\x94\\xbd\\xcc\\x88i\\xbdh\\xc8Q\\xbdA\\x93\\x1d=\\xa6\\xc0\\xb5=\\xef\\xdd\\x1c\\xbd\\xd1\\xd8O\\xbcCl\\x06\\xbd$\\xf3\\xbf\\xbb\\x05g\\xc0\\xbc\\xd4\\xb5\\xd4\\xbbL\\xe1\\x8a;\\xdc\\xc8W=H\\xa9+\\xbd^\\xa3\\x1f=\\xdf\\x8e\\x7f\\xbd \\x1a\\xe2\\xbb\\x11R\\x9f\\xbd5[<\\xba\\xe5\\xe9\\x06<C^\\x86\\xbd\\x94\\x8e_=\\x1a\\x945=\\x17\\x8b\\xbd\\xbc\\x8b\\x81\\xf8\\xbc\\x1b\\xaa\\t=\\x94<\\xf8:\\xe0\\x17\\xa6<Y\\x1c\\xb6<\\xd2\\xca8=v\\xc3~\\xbcO1\\xea\\xbc\\xe9\\nq=\\x13\\xfe\\xa4<\\r\\x05\\x1f<\\xe1,W\\xbc\\x8f~\\x94<\\\\\\x8c0\\xbd\\x0e)\\x05=\\x82D\\xba<\\xc5\\xa7R<Z\\x1e\\xf6\\xbb\\xd8\\xb7 =_:\\xcc\\xbc\\xc0\\x95\\xbe;X\\xfa\\x89=\\x0e\\xf1\\x14\\xbd\\xf9\\x8d\\xef<w\\xdcv:\\x88E\\xbe<{\\xf7V<\\xc6z\\x9a:k\\xdc\\x87<h\\x01\\xeb\\xbc\\n\\xb3I<\\x120e\\xbc\\xedw\\x90<\\x97\\x83\\xcf\\xbc\\xda\\x119\\xbd \\x90\\x9b\\xbdh\\xdc\\xb3=\\x81!!\\xbd\\x07D\\x0b\\xbd\\xceE\\xd0\\xbd\\xd6\\xe0\\x01=eo\\x06\\xbd\\xd9S\\n\\xbd\\xb8\\xab\\xa5\\xbc\\xca\\xc2\\xb3\\xbc\\xe9d\\x84=\\xc4.\\x10\\xbd+:\\x1f\\xbd/{\\\"\\xbdK\\x93\\x80<\\xcaA\\xe3\\xbcT\\xda\\x15<-\\xf4\\xd19\\x924\\x17:\\xf1\\xfb\\x95\\xbc\\x80\\x02\\xc3<i\\xf1\\x87\\xbd\\xc8\\xe5\\x16=\\xc3\\xa5\\xf3;x\\x81\\x9c\\xbd\\xd9\\x1f\\xd6\\xbc\\xc7\\xf8E\\xbc\\xfe/)\\xbc\\xde+\\xcd<\\x14[\\xb7\\xbc\\xe4\\x84!=[\\xbe4\\xbd\\xe9\\xce`\\xbc\\xda\\xd8\\x8b\\xbd\\xf2\\xfd\\xd5\\xbbV\\t\\\"=*.\\xa1<\\xf9\\x9d_\\xbb\\xb3-\\x9a;>\\x0f\\xdb<\\t_O<fS\\x00=\\x9d]\\x18\\xbd\\x83]\\x00=\\x1e@\\xc9<\\x9c\\x1f\\n=}\\xdd\\x0f<\\xf3 \\t=\\x1d\\xbd\\xff;*3\\xbb<\\x94=\\x0c\\xbd\\xb8g\\x04\\xbb\\xcf\\xc3G\\xbc\\xcee_\\xbc~\\xd2\\x80=\\xef4==^}\\xf5\\xbc\\x96(\\x8f=\\xec\\xac\\xc7\\xbc\\xe6/\\x12<Y\\xdc\\xf5<\\x9cE\\x92=J\\xf3:<\\x99|\\xb8\\xbc$9H\\xbd\\x959\\x0e\\xbd]\\x95\\xbc\\xbc\\xa3I\\xde=\\xba*\\t\\xbd\\xd5D5=\\xf1\\xea9<-\\x8f\\xeb<\\xa3u\\xce<\\xffU\\xb1:\\xf55\\xe7\\xbb\\x19M\\xc1<1\\x90\\x14\\xbdW\\xa0\\xe2\\xbc\\xaf\\xca\\x92\\xba\\x8ex\\\"<\\xb1\\x1fW=\\xef@\\x9e\\xbd\\xb2\\xdb#\\xbd\\xcd\\x11\\x8a\\xbd&\\xf3\\xfe<\\xfa\\xd5\\xce\\xbc\\x12\\x98\\xcc<\\xd8\\x961=\\xbd\\x80\\xee;2t?=8\\\\\\x13\\xbd\\x8a\\x1b\\x07=\\x99\\x1d\\xb8:g_\\xd8\\xbb\\xf6\\xdd0=\\xcb\\xe0,<\\xaa\\t\\xf3<\\xa1\\x04\\x03\\xbd\\xce:\\x0c\\xbb\\x9e\\x8c\\xad\\xbc\\xf2aE<.S\\x9e\\xbcm2\\x02\\xbd\\xf2\\x9de\\xbc\\xc0:\\xfd<\\x05\\x05E\\xbd\\xa0\\xc2\\x99<&\\xe2Y=\\xf3\\xec\\x06<\\xe9\\x99\\x0f<\\x14\\x9fH=\\x80\\x85\\x0b\\xbd\\xc5\\xe8\\x02\\xbdZ_\\xd8\\xbc\\x0f\\xc7\\xfe\\xbc\\xa0H\\xd1\\xbc\\xa8\\x80\\x84<\\x04\\xf3\\x08;3Y\\xda\\xbc\\x0c\\xd9\\x9b=\\x1cZ\\x1c=\\xb3\\xd8\\x0b<Mg\\x99\\xbc\\xd7\\x07\\xd0\\xbc\\x01~O\\xbd\\xe2\\x80\\x94\\xbd7\\x1e\\x13<LV\\x96;\\xda\\x8b\\x01=\\x8a\\x98c;\\x89\\x1b\\xbd<N\\x84\\x1c\\xbd\\xbd\\x91\\xce:\\x1a\\xcfN\\xbc\\xd9\\xfeV\\xbb$\\xf6f<\\xc9$\\xc6<=\\\\+<\\xd2\\x17\\xae\\xbd\\xed\\xda\\xae<L\\x9a4=\\x93\\t\\x03\\xbd]\\x94\\xd9<\\xb7\\xe0\\xc1\\xbc9\\x9b\\x86=l\\xe2\\\\<\\x142\\xe8\\xb9\\xa3\\x9b)<.\\x95J;\\t\\x11:=\\xd9\\xa1\\xa1<\\xcev\\x91<7\\xd9\\xb5\\xbb\\xc3R7\\xbc\\xde$I=YC\\x88<\\x18\\xb4\\x85\\xbd\\x9bu\\xf4\\xbc\\x1cN\\x01=s76\\xbdQt\\xdd\\xbc\\xd7LZ\\xbd\\xd3\\xa8\\xc7;\\xab9\\xb4<\\x08\\xd5\\x02\\xbd1\\x894=\\xdcO\\\"=\\x15\\x89\\x9a\\xbc\\xc8\\x1dh\\xbc\\x8d\\xf8\\r\\xbdn\\x91\\x0c\\xbd\\xcc\\xc3U\\xbc\\xba(;=\\xf6\\xd9\\xd5\\xbb{\\xd4\\x9b\\xbc&H\\x13=\\x8f]\\x08=\\x19\\xa1V=\\xb4\\xe0J<\\xbf\\x98\\x95\\xbd\\x8fv\\xe1\\xbc\\x18#g:\\xb5\\xc2r\\xbd\\xde\\xed;\\xbdj\\xe3L\\xbc\\x99+\\x04<\\xc1\\xf4\\x9d\\xbd\\xc1\\xb2\\xe7<\\x81h\\xd4\\xbcd\\xa7d\\xbd\\x96i\\xa6;#\\x97=\\xbd\\xbc\\x85\\xb8\\xbd\\n\\x19d=\\x15\\xd5f=\\x81\\x92\\x1e\\xbc\\xdal6<)b\\xc4\\xbc\\xf5e+\\xbd\\xd4\\x03\\n\\xbcF\\xa8g=\\x17\\x7fV\\xbd1pZ\\xbd\\xba\\xe4\\x99\\xbc\\xd5M\\x85=|}\\xf2\\xbb\\xe60\\x12=l\\x96\\x8b\\xbc\\xd0\\x01R=Q\\xe4\\x94\\xbc5\\xb1\\xc5<}\\xcdC=,\\x83\\x8c<w\\x0b\\xd5\\xbcv\\x1e#=\\xefDx\\xbc\\x83\\x06F=\\xd1t\\x81\\xbc\\xc5\\xa4N=E\\x1d>\\xbd?{~<\\x1c v=bQ\\\"\\xbc\\xc2\\xf5\\x16\\xbd\\xd6w\\x8b<\\xa9\\xa6\\\\<\\xd6\\xd1\\xde<\\xb5\\xce\\x1a\\xbd\\xa7jx;xq\\x13=i\\xa3%=\\x02\\xff\\xac\\xbc\\xd4\\xbag\\xbcS\\x8e?\\xbc\\xe8\\xf3\\x98\\xbc\\xd4\\x89\\xcb\\xbdxt\\xf4\\xbc4\\x00\\x81=\\x00\\x1fN=\\x19%e;%\\x19\\x86<\\xc9\\xa8~<\\xdb\\x1a\\xff\\xbc\\\"\\xedU\\xbd\\x1a\\xef\\x94\\xbcmO\\xbf\\xbc\\x9c\\x0es<\\x1b\\xd8\\xde;\\xa4\\xc1s<>\\x1f\\r\\xbcE\\x933\\xbc\\xa97\\x91<\\xe5\\x04\\x89\\xbdu\\x12R\\xbd\\xee\\xbb\\xed\\xbc\\xa0B\\xf0\\xbc3\\x05\\x18=\\x84\\xcd[<H\\xe7\\x1c\\xbd\\x9e\\xb8\\x0f\\xbd\\xee\\x06\\x01\\xbds\\xc6\\x90=\\xb3\\xe0\\x97=\\xa7\\x19\\x93\\xbc\\xf6t\\x9a<\\xcc.[\\xbcD\\x13\\t=\\xd3\\x977=\\x89\\x8f\\xcc\\xbb\\xfe\\x0f\\xbb\\xbc}3V\\xbd\\xd7Td=\\x01h\\x07=\\x0e\\x92\\xfb<;\\xd0\\t9Y\\x1e\\x0e=\\x88\\xbe\\x84\\xbc\\xc6\\xea\\xf5<r\\xec\\xd4<\\xb9\\x8f6\\xbd\\xa2\\x1e\\x1e\\xba:\\x9fa\\xba\\xaf\\\"\\xe5;i\\xa6\\xc9\\xbbN\\xbba;\\xa33\\x00\\xbcMJ\\xe2\\xba\\x1f\\x18\\x11\\xbe\\xf1\\xd0\\xdd;3E\\x14\\xbcS[\\xf5\\xbaCX\\xde;V\\x1c\\xc7\\xbc\\xec5\\x02\\xbc\\xd2h\\xb5=\\xd9_\\x15;P\\x90^\\t\\xc89\\x00=\\x84\\xd2\\xf1<\\x06\\x11<\\xbc\\xffMK<\\xcd\\x91\\x12<\\xf5\\xd9<=|S\\x10\\xbcbq2\\xbc\\xb6\\x98\\x81<\\x87\\x18D\\xbcm\\x9de=&\\xc3\\x8f<\\xc6ce<\\xdb\\xcf\\x1f<\\xe8\\x0b\\x8b\\xbb\\\\Z|;\\xb0\\xe2\\x89\\xbc*7|\\xbc\\xd6\\x8f\\xcd<S\\xf2\\x85=\\xc8\\xcf\\xfb;\\x81\\xa4\\x0f\\xbd)\\xb9\\x13;\\xd09\\x88=\\x1d\\x1e3=\\xa7\\x1f\\x85=ir\\xb9<]\\xc8\\xd79\\xe9\\xdc&\\xbd\\xdc\\x8aT\\xbc\\xb6\\xc9\\x11=\\x08/d\\xbc^\\xc3?\\xbd\\xe2\\xff{\\xbd<\\x14 \\xbd\\x90\\xa3\\x12\\xbc\\x8c\\xa7\\x05=\\xb71\\x9a<\\xb8[&=\\x84V\\x15\\xbd\\xce}\\x89<\\xe2\\xa1\\xf9\\xbb\\xe4\\xb7\\n=[c\\x87\\xbdv\\xa3\\xf3<W0\\xa9</\\x05[=\\xd4B\\xba\\xbb\\x98\\n\\x17\\xbd\\xa0\\x19[=zy\\xfa\\xbc\\xb8\\x04\\xd2;\\nz\\xb9<\\xe9\\x1f\\x91\\xba\\xe0\\xab\\x8c\\xbbW\\xf0\\xcf;\\xe5X=\\xbc7\\x9c\\x8b<\\x92\\x93\\xe1\\xbb\\x96\\xc0\\x99=\\x93\\xca\\x0b\\xbd)\\xf9\\xab<ab\\x91<\\xf1Q\\xf5<\\xf4\\xe6\\xf6\\xba\\xa3b\\x14\\xbd\\x1b\\x91\\xd2\\xbc\\x9d\\xe4\\xa3<\\xf1s\\xd8;p\\xbb\\xb7\\xbc\\x8e=7=\\xe1M\\xd8\\xbbU\\xd6J\\xbd\\xb0\\x1b`:\\x0f\\xc11=\\xbf\\x07R\\xbdv\\xef\\x7f\\xbc9\\x84\\xa8<\\x1cV\\xb6\\xbd\\xbe\\x07M<\\xabI\\x7f\\xbd\\xcf\\t\\x1b\\xbcN\\x1d\\xf1<\\\\\\xbb&=\\x0b\\xee\\x19\\xbd\\x04\\xd30=F{\\t=$\\x80\\x9c=\\xc6\\x1dS\\xbb\\x08\\x90\\xe4<Z\\x89M\\xbc\\xf0\\xce\\x02\\xbd~\\xa0_=\\xa1\\xe4\\xc3\\xb8\\xbe\\xdbj\\xbb\\xc4V\\xc0<\\x8al\\xa2\\xbbY\\x8e\\xe6<\\xbb\\xd3\\x07\\xbd\\x1a\\xf8\\xa7<ta\\xd0;\\x8f\\tK\\xbd\\xcd\\xfeD=\\xe9{\\xa7\\xbc\\x11\\xcf\\xa7\\xbbJ\\x8b\\x0c\\xbd\\xcf\\xa4\\xa7<\\x834B;DC\\xc0\\xbd\\\"\\xfbc\\xbd\\xfd\\xf5\\x17=\\xae\\x81\\xa3\\xbcoG2\\xbd&\\xde3=f\\x93\\xc6=\\xc9Z\\xd6\\xba\\x8d\\x1f\\xf8<\\xf3\\xaaP\\xbd\\xec\\x1b\\xe4\\xbc\\x00@\\x83\\xbd\\xb70V=\\xab{\\x0c\\xbc\\x18\\xba==\\x85\\x16\\xfd<\\x95\\x80u<\\xc2!1<\\xb9@\\x06\\xbd\\xffbv\\xbb\\x05\\xb8\\xf6<a\\x0fH\\xbdSs\\xf8;qwL\\xbd?RV=\\x88\\xcc\\x10\\xbd\\xb9\\xb7y=\\x9cJ <z\\x18A=\\xab\\xe6S\\xbd\\xe5_\\xf1\\xbb=?\\x9b;\\xfa\\xed\\xda<c\\xac\\t\\xbd\\x1d\\x86\\x80<!H\\x11\\xbc\\xb5\\xa1\\x92<\\xa4\\xc1\\x83=\\xe1o\\xdf\\xbbm\\x0eM<\\x81)==|\\xf2\\xae\\xbc;*\\xda\\xbc\\x1cD\\r\\xbc\\xf2\\xd3\\xca\\xbci\\x9b8=\\xb0.x=6\\xfa\\xe1</\\xa5=\\xbd\\xear\\\\<\\xc5=\\x06=\\xbbb\\xf9;\\xd5\\x98W;\\xa7\\xf2\\xca\\xbc\\xff\\x96\\xcd:\\x95\\xe2\\xd3\\xb9\\x04\\xc9\\xb4\\xbbk\\x9a\\xc1\\xbcf\\xc7q\\xbd\\x86\\xf2\\xfa\\xba\\xad)\\x86=H\\xcc\\xb7;\\x97e\\xd1\\xbc\\x81\\x0c\\x13\\xbd\\xb8\\xcb\\xf2<\\xca-\\r<\\nj\\xfe<\\xf1\\xe6\\xaf=9\\x0c\\x81\\xbc\\xaes\\xee\\xbc7\\x1ck\\xbb\\xfdl\\xb8<6\\x0e\\xa5\\xbc}\\xc7\\xfd\\xbdsj\\xb8\\xbb\\x1e\\xf8-\\xbc=KU\\xbd\\xf1CF\\xbd9\\xa7\\xaf<~\\xa8?=\\xbd\\xc66=4TO\\xbc\\x96])\\xbd0\\xe0\\xd4<\\xde\\xf1\\x8c\\xbd\\xa3+\\\";BO\\xb2\\xbd0\\x081\\xbd\\xca\\xff\\x08=Lf\\x8a\\xbb\\x89,\\x03\\xbd\\xd5\\xa9?\\xbc\\xab\\x05T\\xbd\\xc28\\xb9\\xbb\\xbc\\x107\\xbc\\x1fu\\x9a\\xbd\\xdc\\x92\\x8c<\\x0b\\xaev\\xbc\\xbc\\xae\\x84<\\x82\\xe4};aR\\xd8;\\xed{\\x83\\xbce_\\x1b:.[\\x8a\\xbbltE=]_\\x18<xk\\x00\\xbd\\xdb\\xd4r\\xbc\\x8c\\x81\\xaf;\\xb1\\x15\\x8f\\xbc\\xbe^V\\xbd.=~\\xbdRkr\\xbc\\xf1\\xf3\\n\\xbdf\\x98H;\\xa1\\xd8\\xb8\\xbb\\xd0\\x8e8=t15;&\\x02\\x02=|C\\x04\\xbd\\xac>\\x95\\xbd\\x9d\\xf4\\xbf<cZ\\xe2<\\x18\\x81o\\xbc\\x86\\x85\\x02;\\xd7g\\xbc<\\xaf\\xdb\\xdd<\\x81\\xddl\\xbd\\xe8:`\\xbc\\x17p\\xcc;\\xab\\x0eD\\xbctpW\\xbd=)\\xc0\\xbco\\xdd\\xd9;\\xb6\\x98\\x13=Y\\x86Y<\\x07\\xd0\\xd8\\xbc\\xe2\\x0e\\x97:f\\xa6\\xc8\\xbc\\x9bD\\x8a<\\x11\\xbe]\\xbbc\\xe3\\x03=\\x98\\xdcJ=\\x92jP\\xbb>D\\x9b\\xbc(\\xc6w=\\x1f\\x95\\n\\xbc\\xb8\\x07\\x07\\xbbnr(\\xbckh%\\xbb\\xf0\\x1c7<\\xf5\\xc6,\\xbddk\\x07<\\xf4\\xf4\\xe6\\xbb\\xb3\\x8d\\x9f<\\r\\xd0M=7\\x9dI=\\x89@\\x82\\xbb9s\\xc7<\\x00\\xa8\\x91<\\x8a*\\x06\\xbd\\xca\\xc0\\xba<\\xf7C\\xca;\\xeb\\x9f\\xa1\\xbbk\\xb1$\\xbc\"\nHSET bikes:10050  model 'Enceladus' brand 'Nord' price 2298 type 'Kids mountain bikes' material 'full-carbon' weight 14.0 description 'This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"FY\\xf4\\xbcf\\x87[=\\xa8\\xb6\\n\\xbdr\\xce\\r=\\xbd\\xb7\\xb1\\xbc\\xfc$\\xe3<n\\x7f\\x91\\xbb<\\xe7\\xbd\\xbc4\\xcb\\xd9=pa\\xf2<^%:<\\xe4\\x8f\\xe2\\xbc\\xe5\\x0b\\x80<\\xb8s\\x01\\xbd\\xd2\\x9a\\x03<\\xb8\\xa2\\xb2\\xbc\\xdf7\\xda<\\xc4|\\xe9\\xb7E\\x13\\x08\\xbb?\\x15\\xbb\\xbd\\\"\\xc6\\x19\\xbb\\xf9\\xa5\\x02\\xbd\\xd0\\xfa\\xbe<\\x96N&\\xbd\\x14\\x1e\\xb6\\xbb\\xc5\\x10K</\\xe5s\\xbc\\xd0\\xe6B\\xbc\\xbe\\xf8\\xcc\\xba\\xc1\\xbe2=\\x89\\x00\\t=\\xac\\x06\\x11=\\xb8\\t\\xb3<|s%=\\xd9q\\x1e=\\x18\\xd7 <\\xa8\\x8bl=\\x1d\\xdc0\\xbd\\xd6\\xe7\\xb2<\\xad\\r\\x00=\\xb5p\\xd8\\xbcr\\xd6\\x99<\\x88\\xeac\\xbb]\\xab\\xe0<\\x8d\\x91\\xb2:\\xa8Oa:A\\xf4\\xae=\\x96d|\\xbc\\x0bS\\xb8\\xbcb\\xcd\\x17\\xbc\\xcf@\\x04<#\\xd7\\n<\\xf2\\x87S;?8\\xab\\xbc\\x87\\\".\\xbc\\x1eC%\\xb9\\x08,\\x0c<\\x96\\xecu\\xbd\\xdeZL\\xbd\\t\\xa0u=v\\\"\\x08=\\xa5\\xefR=N\\x05\\xcf<\\x07\\xe7\\x97=XC\\xa2=6L\\xa1\\xbc\\xec\\x08\\x8e\\xbcr\\xa3\\x14\\xbb\\x08r =\\xd8\\x1e\\x05\\xbc\\x85\\x1d\\xe3\\xbb\\xb0\\xfb\\xfa\\xbcJ\\xb9\\xb5\\xbcx\\xea\\xd9\\xbcJ`\\x1f\\xbd\\xe1\\xab\\xb8\\xbc\\xfc\\x16\\xc1;\\x81\\xd5I\\xbd\\xcft\\x8e\\xbd\\x15v\\x14\\xbc\\\"\\x966\\xbce\\xde\\\"\\xbct\\x840\\xbd,\\xcb\\x86\\xbd\\xda\\xe7i\\xbc3\\n#=\\x0e\\xe0V<\\x0c\\xd46<\\xc3\\xc7q;a=f<\\xc3\\xae\\xaa<\\xcd_D\\xbdN\\x05\\x84<Xx^\\xb9\\xe3\\x90\\x19\\xbc\\x14\\xe7\\xe4<\\x9cXN\\xbd\\xb1\\xa3\\x83;\\x0c#\\xa5<\\xb0s \\xbd4y\\x16<<_@\\xbb\\xb9\\xd6\\xbf\\xbcU\\x9f\\x8d<RG\\xcd;\\x88+_=\\x18M\\x87<\\xfd.\\x12<\\xba\\xfc4\\xbb\\xe18\\x89\\xbc\\x1f\\xdc\\x9e;md\\xb1=M\\xc1{9\\x1f&\\x15\\xbd\\xc8\\x00;\\xbd\\xaazE=k\\xc3\\xd8<\\xfe\\xea\\\"\\xbdzg\\xb8;>\\xc04=B\\x8a\\x01=\\xda9\\x9b<\\xd5\\xd0;\\xbdm\\x00\\xff<\\xea\\t1<ay<\\xbdk\\xbb\\x89<\\xda\\x8b\\xb0\\xbd\\x1b\\xf7,\\xbd\\xa3\\x9f\\x89<\\x80\\x82\\x83\\xbc\\xe4\\xc8f\\xbd\\x97@9=\\x9d\\xdcm9\\xfe\\xb2\\x07\\xbd\\xb8\\xc2+=i\\xad\\xed\\xbbBmm;\\xffx\\x0c;\\xa4i\\x18\\xbdkjS\\xbd/:\\xb7\\xbd,n\\xfe\\xbc\\x1b \\xcf=\\x8e\\xf5=<\\x07\\x84,=/\\xf12\\xbdp2R<@\\x1fk=F\\x0bU\\xbd\\xdc(\\xe2\\xbc\\xac\\xa5a=W\\xd6\\x94\\xbdz8\\xc7\\xbcB\\xe26\\xbdV\\xce\\xb3;(A3\\xbdt\\xba\\x9d\\xbc\\x89Z\\x8c=P\\xa9\\xfa\\xbb\\xd3U\\r=Sl\\x1d\\xbd\\x96\\x13\\x80\\xbd\\xec:\\x9e\\xbd!\\xb3L;\\x1d\\xd8 =\\xda<\\xd9\\xbb\\x1fj\\xdb<\\x80!\\xbd7@\\xef\\x95\\xbc\\xadH\\xd1;Gd\\xd6:N\\x90O<\\x8f\\xfe)\\xbd}\\xf5\\xf5\\xbc\\xe9\\xb2E\\xbc\\xdc\\xb5R\\xbd!\\x16\\x90=\\x16\\xb2\\xab<30\\xa7<\\x04\\xe9\\x83\\xbc\\xed\\x03p<\\xb1\\xe5\\xe1<z\\xd7\\x10=\\xb0>\\x98<\\xec6\\x80\\xbc\\xe1\\\\5=\\xf97\\xc7\\xbc\\xad3r<^\\x9dj\\xbd\\x10\\xa9\\x16\\xbd\\r~0=\\x84\\x98N\\xbd\\xa2Q\\x0f\\xbb\\x15]\\xcc\\xbb\\x1cu\\xc5\\xbb\\x18\\xb7\\x1e\\xbd\\x01\\xf8_<L\\x85\\x0c\\xbd&\\xa3\\x1b=f\\x06\\xb5<\\x1c\\xe8i\\xbc\\xbd\\xd8\\x96\\xbd.\\xf2\\xe3\\xba\\xd41\\xb1\\xbcg1\\x1c;Q\\xd6)<\\x16P\\x0e\\xbdMk\\x85=\\xcd;\\xf5\\xbc)D\\x8b\\xbc<\\\\p=\\xf1\\xb4&\\xbc\\xa3p*\\xbd*\\xf2H=/\\x89\\xa4\\xbcO\\xeb\\x8b:,\\xe8J=\\x8e\\xfc\\xa6\\xbb\\xd9\\x8c\\xc8\\xbc\\xe3\\x8f:;\\x1a\\xa8\\xd0\\xbc\\x9c(j\\xbc\\xd2R\\x85= S!\\xbd\\x0e\\xec\\x04\\xbb\\xf5\\xac\\x8d<W\\xf1\\x11\\xbd\\xfe\\xf2e\\xbb3\\xdb\\x97\\xbcAp\\x83\\xbc\\xcd\\x89\\xe2\\xbb\\x1c\\xbd\\xa7;P\\xcb#;\\xc3\\x83\\x8e9\\\"W(\\xbb\\xcd\\xfeK\\xbd!\\xcf\\xb9<p\\xc3\\xa3<\\xa8\\xab\\x1a\\xbc\\xa2\\xe83\\xbd\\xa5d\\x1b;>\\xd1\\x05\\xbaS\\xc6\\xde\\xbcVS<=(I\\xcb<\\x1f5\\x85<\\x1e\\xe4\\xfb;j>\\xe7\\xbd\\xf1\\xf6\\xea<\\x8a\\\"\\xb9<\\xfc!\\x8e\\xbd\\x913\\xe3\\xbc\\x9coh=\\xd9\\x8cw\\xbc\\x0f\\xba\\x02=u\\x81\\xd0\\xbc\\xac\\x01\\xd9<E)&\\xbdDc\\x1e=\\x08\\xde\\xb2\\xbb\\xd32\\xbb<\\x8f\\xa4%\\xbd\\xc4:\\x08<\\x8e\\xc4\\x02\\xbd\\xe6\\xdb\\xae=\\x07-\\xce<\\xa9\\xbb\\xb7\\xbb\\xd5u\\xb3\\xbc\\xa9:r<\\x80EU\\xbdj\\x8e\\x91\\xbc\\xb0\\xbb\\xc2\\xbb\\x1673<\\xff\\x87\\x02\\xbaM\\xe7e\\xbc+XZ<>q\\x0c<V\\x88\\xc3\\xbc,\\xab\\xc8\\xbd\\x8fj|\\xbb7R\\x87<\\x19+<=\\x93>t=\\xfb\\x85\\x9c<)\\xe4 \\xbd\\xd7\\xdb\\x1e\\xbc\\xaa\\x12*\\xbc8+\\x92\\xbd\\xd8\\xb6J=\\x12\\x8cu\\xb9\\x1b\\x7fD\\xbd;t\\xc1=\\xda\\x95\\x08\\xbc\\xb4\\xc8J\\xbd\\x88\\x1d\\x0b\\xbb\\x1f\\xf7\\x80\\xbc%Xn=Z\\x9d\\x88\\xbc\\xc5;\\x0f\\xbd32h9\\x8b\\x08\\xc3<5;\\xb9=\\x8cp\\x14\\xb9[\\x1d\\x1f=\\xf9 \\x1a=\\xd4\\xa9m=\\xd0\\xb6\\xd9<Q\\x94\\x11=/\\x08\\x06=\\xe0\\x84\\xd3\\xbcd@?\\xbd\\xba\\x90\\xf5;f;\\\"\\xbd\\x97\\xe2h<\\x8b7\\x83\\xbaV\\xd1#\\xbc\\xb0\\x89\\x8a=\\xac\\x85\\xa4<w\\x01\\x93\\xbc\\x00\\xf4/\\xbc\\xaa\\xc3\\x1b\\xbc\\xb7\\x87\\xb0<\\xb1\\xb7\\x83\\xbb&\\x97+=\\xcf\\xd7\\t\\xbd,1~=\\xd7\\xcd\\xcb\\xbd\\xaf\\xac\\x12\\xbc\\x9cB!\\xbc\\xd8y\\xf9\\xba\\x8ab\\x00\\xbd\\x9a\\x9b\\x0f\\xbd\\xadp%=\\xa3\\xeek\\xbc\\xa4\\xc1\\xd2;P\\x85\\xf3;\\x02\\xac\\x92<y{\\xaf<[\\xeb\\x1c\\xbb\\xee\\x00\\xc8\\xbc\\xd4@$\\xbd\\xa5\\xced=\\rY\\x8e<\\xf3\\x1d\\x16\\xbc]\\x8f\\x85=Tp\\x93=T\\x9aw;\\xfc\\xc2 =\\xa2\\x99:\\xbc\\xe7\\xad\\xbd\\xbcI\\x98O\\xbc\\xe2\\x02p=L@H\\xbd\\x08$\\x1e\\xbd\\xe8\\x816<\\xbe\\x9dh<)\\n(\\xbddU\\xa5\\xbc)\\x89\\xc7\\xbd\\xfc^\\x0c=\\xf7\\x94\\x9e<\\xeb\\xb9\\x16<\\x88\\x91==\\xa7C\\\"=TM?\\xbc\\xc3\\xe6\\x18\\xbdn\\xba\\x87=\\x1f\\xca\\\"\\xbc\\xa1\\xc7\\xe2\\xbbV\\x12b=*\\xd7\\xac:\\xca\\x99\\xd4;s\\x03\\x1e\\xbd\\x93\\xb3\\xeb\\xbc\\xafB\\xcf<\\x0c\\xb7J\\xbb^\\xec\\xb9\\xbd|\\x13\\xb9\\xbcJ2+<US\\xca\\xbc\\xf7T\\x17\\xbdi\\xc4\\xc0\\xbc\\x0c\\x13F:i\\xd4f\\xbd_\\xb3\\x00=<[\\xfd\\xbc\\xd5\\xf4|=#b\\xf5<a\\xd2\\\"<\\xa6x\\x89\\xbd\\xb6e\\x8f=\\x0b\\xfaP=\\xe4\\x91\\x8e\\xbcGbn<\\xdd$\\xfe<\\xdb\\x18h;\\x1e2\\x1b;\\xd6\\r}=\\xb4h\\x95\\xbdz\\x01u;\\x01\\\"8\\xbc\\x7fj\\xe3\\xbc\\xa2\\x12\\x8e=\\xee\\x9b.=\\x0f\\\\\\xca\\xbc\\xd9\\x9b\\xaa<\\x96A\\x06\\xbdb\\xe7\\xec\\xbcHY\\xf1\\xbc)|H=\\x15\\x1a\\xa2;\\xeb1u;9,\\x93<\\x1b\\nG;\\x9f\\xa7\\x0b;\\xc3P\\xa3\\xbb8\\x9c\\xac\\xbd\\x08\\x1b\\x86<34\\n\\xbb\\xc1\\xa5/\\xbc\\xc6\\xc7I\\xbd\\x08\\xe5U<{\\x88\\xe3<X\\x9eY<d\\xf1\\n\\xbd\\xe5\\xbd\\xb6\\xbc\\x95\\xb4\\xa0=O\\xcf[;\\xd8\\xf8@\\xbdK\\xcd\\x84\\xbc\\x11\\xa3\\x8d\\xbcKV\\x14\\xbd\\x145}\\xbd@\\xd0\\x9d\\xbc#Aj=OG\\x7f=\\xa0@n;ox\\x8d\\xbc\\xa0\\xd6?=\\x80P!\\xbd~]\\xa0\\xbcC\\xe0\\x13=\\x9c\\x90t\\xbd>:\\xcc\\xbb\\x1f8\\xfc<\\xc1\\x05\\x0f<f:\\xca\\xbc\\x1d\\x9e\\xeb\\xbc\\xbd\\xa5^=\\xbf7\\xae\\xbc\\xa4\\xe1\\x15\\xbd\\xa0\\x07O\\xba\\x06\\x1b\\xa7\\xbd\\xef\\x8c\\x9a\\xbb\\xeb@\\xd0\\xbc\\xae\\x9b\\xea\\xba\\xe9\\xef\\xca\\xbb\\xe1\\x9bY\\xbc\\x81N\\xed<\\xdd\\xec\\xbb=\\xf8\\t\\xce<#\\xdf\\x83=jv\\x10\\xbd\\x1b_?7\\x10\\x1d\\xb6<iA\\x18\\xbd\\\"\\xa7\\x1a\\xba\\xbe\\xc0\\x99<#\\xe7v=_V\\x03\\xbd\\\\J#\\xbc\\x7fA\\x0b\\xbdT\\x89\\x17<8\\x05\\xcd;\\xf7\\xf2\\xae<p\\xda;\\xbd\\x96\\xd3\\x8e\\xbc\\x1f5\\xa4<\\xf3\\xe4t\\xbc@\\xf8\\xb2\\xba\\x93\\xcc\\x00\\xbb\\xf0!\\xee\\xbbw\\x1fG<V\\xd8\\\"\\xbd\\x85n\\xc3\\xbd\\xcb\\x17\\xde\\xbb\\x93\\xa2\\x86\\xbc\\xb0\\xdd\\xbf<%SB=\\x1c\\xa5\\x94\\xbd\\xdf] =69\\xff<\\xf8\\xee\\x93\\xbcG\\xab\\x86\\ty\\xd2\\xc6\\xbc&\\x18e:IV\\x87=u~+=\\xb2\\x10\\x8d\\xbb\\xd1/\\x9a<SO-\\xbb\\xfe\\xbb5<pm,=\\xa7B\\x14<\\x91\\x10\\xbe\\xbc0\\x15\\x8e=B\\xd2\\x0e;\\xc8\\xb6\\xd9<\\xcf\\xd2k\\xbc2b\\x1e=\\xed\\xd1!\\xbd\\xef\\xeck\\xbd2\\x07\\x11;W\\x804=H\\xe8\\x83=c\\xec\\xee<J.\\xcd\\xbc]_\\x16\\xbc\\xde\\x930=\\xab\\xc7\\\"\\xbc\\xdfn\\xa6\\xbc<Az<l\\x9b\\xd1\\xbc\\x1e\\xe6L\\xbb#\\xb5\\xfb<}(0<\\xd5b \\xbdu\\x1a\\x81\\xbd\\xa4\\x00\\xef;\\x87\\t\\x8f<\\xb5\\xfc5\\xbc\\xf4\\xe9G\\xbb\\x0f\\xab\\xaa;\\xe5\\xaa\\r<\\xc2\\xd7\\xe1<~\\xfa\\xb5<\\x99I$=\\x7f\\xf9\\x11\\xbc\\xd9YV\\xba\\x1eo\\xd8\\xbc25\\xd9=$\\xbf\\x8c<T\\xa6\\xb7\\xbc\\xa9\\xc6\\x1d\\xbd\\xbd%\\xca\\xbc\\xad\\xdb\\x8a\\xbd\\x9b\\xab\\xb8<4_\\x15:\\x0f\\xb5\\xc2\\xbd\\x0b\\xaeR=\\xbdni;\\xf4\\xd7\\xda\\xbc\\xd7\\xb1\\\"=\\x00n\\xc5\\xbc|J\\x0c<\\r\\xffr\\xbc\\x8a\\x9c\\xa1\\xbc\\xe2B\\x0f=\\xb3t\\x0c\\xbd\\x8c\\\"\\x13\\xbd\\xbe\\xa1\\xf2\\xbc\\x9f\\x1e \\xbc\\xe4\\x0b?<\\xb2\\xcf\\xdf;\\x16\\x8b_<d\\x9e\\xf0\\xbb\\xbcPV\\xbc,>\\xbf<7y\\xbb\\xbc\\n\\x8c\\xcc\\xbc\\xf0\\x0c\\x1f\\xbdB\\xf6\\xa0=\\xb7\\x93n\\xbd\\xe2;\\xa6<\\x1eMO\\xbd6\\xb1K<\\xa4k\\x8e\\xbc=\\x8a\\x07=\\x14\\x08\\x03\\xbd\\xb2/\\x03\\xbd\\xb9\\xf3\\x81=cK\\x05<\\xa0\\x99L\\xbd|\\xd4\\n=\\x87\\xa1\\xd0;\\xec\\xd0\\xc2\\xbb\\xbe\\x1bu=\\\"\\xec\\xe4:\\x8c\\xccH=\\xc3\\x0b\\x0e=\\x98\\xee\\x80\\xbd\\xd0\\xe4!<\\x0b\\xd3\\xc8\\xbb\\\"\\x121=\\xbc\\x9f&\\xbd\\\\\\x8bw\\xbc\\xaaF\\x91<\\xf0\\x15q\\xbc\\x92\\x84g=\\x9f\\xe4\\x17\\xbd\\xea\\x1b\\x02\\xba>\\x14\\xc4\\xbc\\xf3\\x80\\x90\\xbc\\xbb\\x0fc\\xbd\\x1a\\x14\\x14=\\x94\\\\\\x95\\xbc\\xe8R\\xa7\\xbd\\x87=\\x05=\\x0b\\x00\\x80=\\xc0\\xdfN<5\\x0c\\x8b;\\x96\\xbf\\xc3\\xbch\\xd1\\xec\\xbc0\\x10\\x1c\\xbd\\xba\\xaf\\xec<i\\xe3o\\xbc\\x1c\\xf7\\xe1;l_\\x8c\\xbc\\x13?\\x91=:\\xc3\\xfa;@\\x89\\xc3\\xbc\\x8f\\x9e>=\\xd1{\\x08=\\xd0\\xf4\\x19\\xbd\\xf4\\xd2\\xa0\\xbc\\xc2\\x16\\x8e\\xbd\\x12d\\x7f=G\\xc2\\xce<\\xf8\\xd6j=\\xfc2\\x10<\\xd7\\xc7\\x1e=\\x1b\\x80o\\xbd\\x8d\\xb9\\xe6\\xbagE\\x04\\xbc\\xe1#\\x00<*#Z\\xbc\\xa0-j\\xbdUs\\xc7\\xbb\\x0c\\x9f\\xc7\\xbaA/+<\\xd1e\\x98\\xbc\\x92n\\xbf\\xbc>\\xe0}=XZ\\xb0;V\\xed)\\xbd\\xdc\\xd92:\\x91JB\\xbd\\xb4\\xdb\\xe6<\\xc6I><\\x8d\\x8d*<\\xb3\\xef\\xeb\\xbcj\\xd2;=\\xfa\\xa6\\xcd<C\\x86j=K\\xdc~<\\x9c\\xfe)=}\\xfc\\x12=\\x84\\x1f\\x8f\\xbc\\xa5Rr\\xbcA\\xbf\\xc2\\xbb\\xa0\\x8d\\xc8\\xbbuw\\xf4;w\\x0e\\x07=\\x91\\xe3Q\\xbd)\\xf1d\\xbb\\xe6\\xa2J;\\xb4\\xa0C=\\xba\\x8a\\x12:\\xec\\xe6X<\\xd5X\\x07=?\\xed\\x81=X\\xfa5<4\\xacJ=\\x1a)\\x8b\\xbc\\\"\\xa9O\\xbd\\xccC\\x0b\\xbd\\x97\\xa5\\xeb\\xbb\\xbc\\x86\\xa9\\xbc:X\\xfd\\xbc\\x94\\xc4l\\xbc-\\x9co\\xbd\\xb3\\xd1t\\xbd\\x8e\\xf7C=-\\xe4\\xca\\xbb;\\x9cP=\\\"\\xd05=\\x1b\\xe2\\x1b\\xbdvZ\\xb7<Ta+\\xbd\\xd8\\x17\\x93\\xbd\\xb2%X\\xbc\\x80b\\xc0\\xbcK\\xb9\\x12\\xbd\\xfb\\xcf\\x11\\xbd\\xd2\\x83L\\xbd(/\\x14\\xbd\\x8d\\\\\\x9e9h\\x04\\x04\\xbd4x%\\xbd\\x8f1]\\xbb\\xa9\\x97\\x12\\xbd\\xfb\\xc0|;\\xae\\x14\\xc1\\xbcj\\x12\\xfb\\xbc\\xb2\\xb5\\x84;\\xa8\\xdb\\x11<\\x13*4<H\\xc6\\xcb\\xbc\\x98%\\xe7\\xbb\\xb9}J<sV\\xae\\xba\\xee\\xfe\\xd6\\xbc\\x04S\\xab\\xbc6T\\x0f\\xbd\\xbf2\\xa1<5YF\\xbda\\x1eI=\\xc5:\\xbd=\\xdd\\x0b\\xaf=\\x8e0\\xa6\\xba\\xac\\xe1G=\\x1d\\x94B\\xbd\\x992P\\xbcfI)<_u\\x95\\xbcb\\xf0\\x86<-\\xaf\\xc3<\\xc5\\xe5u<\\xa2?\\x04\\xbc\\xe8\\x8c\\xd6\\xbc\\xe9\\xcb\\xb3<v\\xb1\\xcc\\xbbJ T<\\n\\xb48=\\xdfn5=\\xff\\x15B=\\xf1\\xcbq<\\xd4\\x91\\x00\\xbd\\xe7\\xfb\\x05\\xbdz+\\x1f\\xbd\\n\\x86\\xca<\\xcc\\xce\\xf6\\xbb\\xad\\xb73\\xbd\\xe2\\xf6\\n\\xbd\\xd6\\xdd\\xc5<2A\\x058\\xc1\\x1d\\xbf\\xbc\\xb7\\x8a\\xae;\\xdc\\xdb\\xa8\\xbc*\\x0b`=\\x16T\\xcc\\xbc\\xecl\\x0b\\xbd\\x14*\\x06\\xbd\\\\\\xf7\\x08\\xbd\\xdb\\x14A<K\\x87/\\xbc\\x15\\xa0\\r=@\\xca\\xd3;x\\x07\\xa2=:\\xb5[\\xbbP\\x17\\xb1<\\x04\\x9c\\xb2=\\xa4\\xdb6\\xbc\\xa5\\x9f)=\\xd6\\x98\\x01\\xbbX\\n5<\\xadqi\\xbd\"\nHSET bikes:10051  model 'Ganymede' brand 'Pedal pals' price 3706 type 'Kids bikes' material 'alloy' weight 9.0 description 'These bikes pretty easy to ride while also being lightweight enough and quite durable. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\x18\\x18m\\xbc\\xa7\\xe2\\xb8<\\xe0\\xf5d\\xbc\\x91\\x01\\xc6<9:\\x00\\xbd\\xee\\x085=\\xe4B\\x8f\\xba\\xa5\\xb6\\x17\\xbd\\x8e\\xfdn=1\\x140=\\x1ff\\x0c=\\xca\\xda\\x10;\\xa9\\xde\\x02=\\\"\\x96g\\xbb\\x19\\xb0\\x89=\\x9e{\\x06\\xbd vP=%5\\x03\\xbd\\x16\\xf3\\x04=\\r\\xf0\\x88\\xbd\\xb5\\x8f\\xd2\\xba[B\\x1b\\xbd\\x84\\xf57==,Y\\xbd\\x120\\x9e\\xbdZ\\x99F<\\xea\\x19Q=D\\x88,\\xbc;\\xc0C\\xbdM&\\xe2=`\\\\6\\xbdD\\x1aK\\xbd\\xd5K\\xb6<n\\xbf\\x1c\\xbc<BX\\xbc?\\xd6\\xc1<\\xbd\\xb8\\x18<\\xd7\\xcb\\r\\xbcAu\\x06\\xbd\\x98`&\\xbc\\xb3\\xb4\\x8f=\\x0f\\xeb(\\xbcl\\xecp=\\xdcE\\x9e<\\x91rP\\xbc\\x97\\x81\\x8b\\xbcy\\x91\\xd0=/\\xd0\\x11\\xbd\\x13\\xd5\\xb1\\xbc\\xf3\\x81\\xf0:\\xa9Y\\xe5\\xbc\\x07\\xa99\\xbd\\x97\\\"\\xd3\\xbc\\x05\\x05\\x94\\xbb-B\\xc3\\xbc<\\xf0\\x7f\\xbcW\\x9b\\\"\\xbc\\x98d\\x9f;\\x1d1\\xeb\\xbcG\\xe9\\x83=S\\x1d\\x98<\\\"\\x1d\\x14<\\x08W\\xac<\\xf1\\xf9\\xe0<\\xad\\xbf\\xe9<\\x94\\xe2\\xda<\\xbeo\\x91;~\\xaez<\\x06\\xac\\xaf<\\xd5\\x1b3\\xbdzk5<\\xca\\xa05=\\xd0w\\x9a\\xbc\\xf5:\\xb1<\\x89$\\x1e\\xbd.\\xea\\xd8\\xbc\\xdf\\xe9}<\\xf7\\xbb\\xa3\\xbc\\xef\\xf6\\x9b\\xbd\\xe4\\xa2\\x91<Y\\xc9\\x12=\\x153\\xd1\\xbc\\xf9C\\x88<\\x96\\x87\\x87\\xbdN\\xd7\\x1c<H\\xb5n;\\xa4\\xc0v<\\x97\\xe8l<b7*=4\\xb7\\\"<$\\xe3\\x11\\xbcE\\xd8>\\xbdT\\xee\\x81\\xbdC\\xb6q<`\\xe5\\xed<\\xe4m\\x0b<\\x97\\x06+\\xbd\\x13\\xd1h\\xbdU\\xd5F=\\x18\\x1e\\x8b\\xbc;\\x90\\x02<\\x19\\xa6\\xbe<\\x98\\xfa\\xf0<=\\xcd\\xef\\xbcx|[\\xbc\\x99\\x8c\\xb1=K\\\"\\x82\\xbb\\xa0S\\xe4\\xbbgn\\x96\\xbb}9\\n<\\xf9L^\\xbc\\xabv\\x1b=\\xf1\\xda\\x90<d\\xaf\\x05\\xbc\\x12*\\x7f\\xbd2\\xa5\\x17\\xbd\\xed\\x01\\x8e<\\xdcF\\x0e<\\xcbP\\x06=\\xba\\x97\\xa8\\xbc6\\xc69\\xbc\\xfb\\xe6\\xde<,KM\\xbd\\x13\\xe0\\x17\\xbd\\xa4k\\x1d=)*\\x8f\\xb8\\x02c\\x1b\\xbc\\x13\\x03V\\xbd\\x0ffA\\xbdah \\xbd\\xc6:\\xea\\xbc\\xe7KC;%5\\x1e<\\xbb\\x10\\xd4\\xbc\\xf0\\xe2\\x94\\xbb\\xe0\\xbc\\x0c=O\\x0fa\\xbc\\x0eB3\\xbc\\x9f\\xad];\\xa9\\xbcU\\xbd\\xff\\x089\\xbd\\x9b\\xa0\\x0c\\xbd\\xdb\\xe7\\x00=\\xdb\\x00\\xe5=\\xb5\\xf6\\xf6<\\xe0ro\\xbc5\\xec\\xdf<A|\\x90;Q\\xd0\\xed\\xbc\\xf0\\\"\\x81\\xbc\\xec6+\\xbc\\x15Jg=<\\xee\\r\\xbd\\x89\\xbe\\xc2<ET\\x9f\\xbd\\xbb%\\xd7;[t\\xa7<\\xf1\\xd4,\\xbd\\xe2\\x1f[=\\x1da_\\xbd_\\xf5K=\\x19\\xf2J<\\x96\\xe0\\xce\\xbc\\xe8gt\\xbd3(;<FL*=;:\\x18\\xbd\\xa6a`<\\xda\\x84D=\\xa8NR\\xbc\\xbc\\x7f\\xce\\xba\\xdd\\\"J=|]n\\xbc\\xa5\\xc7\\x1c\\xbd\\xdf\\xc4>;\\xd4\\x8a\\n\\xbc0\\xe8/\\xbd\\x9d\\xe6\\x8a=\\xfcl\\xb6\\xbc\\xcfG6\\xbcc7\\xb3\\xbcu\\\"\\x8c<J\\xc5,<\\x16\\x90\\xaf\\xbb\\xb8k\\x1b=\\xe3\\x81\\xcb\\xbc\\xa8\\xae\\x0b=\\xaa)\\x87:\\xec.\\xba;f\\xe6\\\\\\xbc\\xe8\\x18F=\\xc5\\x00@=y\\x9c.\\xbd\\\"\\xbf\\xd0<\\x00r\\t\\xbc\\xa4\\x0e\\xb5<\\xeebY\\xbd\\x1e\\x9c\\x12\\xbd\\x125\\x0c\\xbd\\xb7\\xb6(=v\\xaaE\\xbcn\\xe1S\\xbd\\xbc\\xf9^\\xbaS\\xb2\\xaf\\xbc9\\x9b9\\xbbjk\\xcc<\\xf2*\\x15=N\\\"B\\xbd\\x16\\xeb\\x9b<\\xbbp\\xba\\xbc;\\xa7\\x8a\\xbc\\xe1\\xdb\\x8f\\xbc<\\xa2E=\\xc58\\xda\\xbcf\\xad\\x0c\\xbc\\x86\\xb3K\\xbc\\xfd\\x8d.<P$\\x8f\\xbd\\\"[\\x80<\\xb5\\xe1\\xd2\\xbdc\\xc7o<\\xe0\\xf1\\xca\\xbc\\x16\\x9f6\\xbc#\\x0c\\\\\\xbc\\xd2\\x18\\xc7\\xbc\\x08\\x92\\x80\\xbc\\x17\\x9c\\xdc<\\x7f\\xfd\\x00\\xbdN\\x97\\x98=\\xdc\\x07\\x01\\xbd\\xa0\\xa83\\xbd\\x19\\x94a\\xbd\\x08\\xcb\\xcf\\xbc\\xc2\\xc0\\x88=\\x8a\\x96\\x8a\\xbc_2C=\\xa3\\x95M<\\xef\\xbf^=\\x81\\xdc!\\xbc\\xb3\\xcf\\xe9<\\xc8T\\xa0\\xbc;\\xf9\\x85\\xbc\\x03\\x8cW=\\x02P7\\xbc\\xa2\\xf0O<\\xc3\\x84\\xb6=\\xbf\\xfb\\xdd:S\\x8f\\xd7\\xbd\\xf4\\xdb\\x9f\\xbd\\xae\\xd8\\xc7;V\\xee\\x1f\\xbb\\x9c\\x82X\\xbc\\xed\\xc7\\xf7\\xbb\\xf6\\x02\\xfe;@4t\\xbd\\xb4\\xe9C=\\xd6\\xc8\\xab\\xbc\\xf9\\x91\\xce<.&?\\xbd\\xe9\\x03p=\\x0c\\xae\\xa4\\xbc!&\\xc4\\xbcE\\xce\\xf9\\xbc!\\xdc\\xf5\\xbc\\xb3\\xc6(\\xbd\\x1bU\\xb3=\\xfe\\x98\\xdf\\xbc\\xbaX\\x0e\\xbd:l\\x11<\\xf9P\\xbb<\\\\\\xf9\\xb3;\\x97\\x9c\\x8b;\\xa0q\\xd2:\\x9axa<\\x04\\xe1\\x84\\xbc\\xa6\\xd2b;\\x0cC\\r\\xbd\\x8d\\x83\\x08:\\xe5\\xb8v=:G\\xbd\\xbd2\\xca\\xb2\\xbc\\xc99\\xc5\\xbaj\\x9bo\\xbc\\xf5\\x97P\\xbb\\x1c\\xf7\\xd1<\\xe5\\xde\\xa5\\xbb\\x0b(\\xf6<4\\\\\\xec<\\xff\\x8f\\xb2\\xbdi\\xfc\\xb98-\\xe2\\xb0<M\\xf1\\xd9\\xbcp5\\xa3<1\\xcf\\xf4;O\\xae-<\\x8a\\xb7\\x06\\xbd\\xe3\\xdc\\x04<\\xa4\\xf3\\x13=e\\xd3\\xb5\\xba\\x07)\\xb7\\xbc\\n\\x14\\xe1<H\\x8b\\xa6<\\xa6\\x82\\x13=$\\xd0(\\xbd\\xeb\\xf7\\x11<}\\xf6\\x87<\\x1f3I;Y,\\xc0\\xbb!\\xb1\\xca<\\xae\\xe04\\xbd14\\xe7;UO\\x80\\xbd\\x9d\\\"!\\xbc\\x05sA;\\x93\\xc4\\xdd<\\xbe\\xc5Q\\xbdc#6\\xbd\\x1cT\\x85=#\\x9aF=\\xf6+\\x04\\xbcU\\x8a\\xcc\\xbc\\xd8\\xa4O<l\\xeaI\\xbc*n\\xa0<pS\\x8e<\\xe1\\xaap\\xbd\\xa6+\\xb7<\\x86\\xbe\\x81\\xba\\xc6\\xc2\\xaf<<;\\xaa;a\\x02\\xf1<\\xebQ\\x9a\\xbd\\xdf\\x90\\x97\\xbc\\xde\\x06x8R\\xefg\\xbc\\x14}\\x10\\xbbj\\x0e\\x1d\\xbd\\x84n`=\\xbbu\\xbf<\\xe7{\\xda<\\xc3q\\xa7\\xbb\\xac\\xe1\\x13\\xbc\\x80\\x1e\\xea<\\xe9c\\xa0\\xbc\\xdc\\x04\\xf5<\\xd3\\xe9F=\\xa0\\x00\\x99=\\x87\\xc2?=\\\"\\x12\\x82=\\x91}\\xf5<~*\\x1a:\\xd4\\xb0\\x83\\xbc\\x83{\\x8a=j\\x7f6\\xbc\\xa4:\\r\\xbd\\xcfO\\xd6<`8F\\xbc\\x12V\\x9b\\xbd\\xcc\\xa9J\\xbc9\\x8b\\xd8\\xbdC\\xd8f\\xbc\\x13w\\x96<\\x9f\\t\\xe2;\\xf2)-=9\\xbc\\x0e<\\xdcV\\x80=\\xcfe\\x12\\xbd\\xbav\\xa2;\\xb1\\x9d\\x14\\xbdGD\\x03\\xbd{b\\x81=I\\xf0\\x90;\\x9e\\x9a\\x92\\xbce\\xc4(\\xbc;\\x99\\xe2<B\\xbfY=\\xc6\\xbes\\xbd+\\x8c$\\xbd\\xe9n\\xc3\\xbc\\xba\\xe85=A\\xf9(\\xbd\\xd1\\xaa\\x92\\xbd\\xc8g-\\xbd\\x89b\\x87\\xb9e2@\\xbd\\xc8\\xe0\\x07<\\xde\\xd9\\x11<I\\x1a\\xbc\\xbc\\xa8\\x8d?=\\x10\\x94\\xc4;\\x87\\xfeb\\xbd\\xed\\xd6\\xc6=\\xcb\\n\\xaa=\\xc1p\\n\\xbcA\\n\\x00\\xbb\\xa3\\xb7\\xca\\xbc\\xf3\\xba\\xbc\\xbd\\x84\\x82\\xe7;\\xf8\\xc2\\xfe<\\x92r\\x07\\xbd\\xb8\\xb2\\xe4;\\xec\\x06\\x9a\\xbdy\\x905=\\x01u\\xb9<\\x8c\\x8c \\xbct\\xe8[\\xbb\\xb9\\xcc\\x18=\\xa9\\xffo\\xbb\\x14U%=+\\t\\x85\\xbce\\\"9=\\x88\\x8cy<\\xaeVw\\xbb\\x19\\x94F\\xbc\\x8c\\n\\x07\\xbbP\\x08\\xaf<l}\\x89<\\x9b\\xb1\\xa1\\xbc\\x82\\x18v<s\\xed\\xf3\\xbc?\\xcf$;\\xd7\\xf5\\xfd\\xbbb6\\x91< *#=:\\n>\\xbc\\x9fT)\\xbdQt\\x85<A(2={\\t/\\xbc(l\\xc1:CZ\\xe5<\\xc5\\xc8\\xfc\\xbc)(\\xa2;m\\xab\\x8b\\xbd(\\x1d\\x8b\\xbc0\\xe2]=\\xdd\\x8a,=i*\\x0e=\\xee\\xee\\x82\\xbb\\x12q-<\\x91\\xf8\\x8a\\xbc[\\xf8\\xb4<\\xc2\\xa3\\xf9\\xbc\\xc4\\xfc\\xad\\xbc\\x073\\x89<\\xb9\\x1c\\x0c<e\\\\!=\\xd1\\x08\\xaa;\\xe5\\xdb\\xb6\\xbcg\\xb7h=\\x81w<\\xbd7w\\x8f\\xbd\\xc6\\xa6\\xcb\\xbc\\x19\\xdbL\\xbd0F\\xf8<\\x03Z\\x95;\\x91\\xff\\x92;\\x1a\\xd8\\n\\xbd\\xd3\\x0e@\\xbdd\\x91g=gFt=\\xdc\\xfe\\xc1\\xbc\\xb9\\x8ax=\\xff\\xc0\\x8b\\xbc\\\"\\xde\\xa4\\xbc7\\xd1\\xbc</\\xb5\\xfc<_4\\xa1\\xbc\\xf2j\\x9d\\xb6r\\xbfW=_\\xdc\\x86\\xbc\\xcdI\\xda<L\\xce\\xb9<B\\xd9n<\\xb5\\x9d\\xd2;GH\\t=\\xa3==\\xbd\\t\\x1c\\x99\\xbc\\x81\\xc7!=X\\x8e\\x1a9[W\\x1d=\\x8cW\\x1f<I\\xf5\\xb9\\xbc\\x80\\x11\\x06=\\xa3BO\\xbc0\\x1f\\xbb\\xbd\\x9d\\xa2\\xc0\\xbcn\\x0e\\xd9\\xbb\\x9c-\\xe7\\xbc\\x82/\\xfa<\\xc18;\\xbdd\\x05F\\xbcOj\\xdc<\\x83A\\x91<\\xd7\\xabT\\tG.\\xdc<x*\\xbb\\xbb5\\x11\\x08=x\\x85\\xeb\\xbb\\xf5\\xf6\\x81\\xbc\\x99\\x85V=j8=\\xbc\\xe5r\\x9b\\xbc\\xb4t\\x0b=\\x03\\xce\\xcd\\xbc_\\x8f <\\x82\\x96O<\\xb2\\xab\\x91\\xbc\\xe1\\nO=\\xf8\\xeaH\\xbc\\xcd\\xd4\\x04=\\x8a\\x85\\x8b\\xbd\\x96\\xae$\\xbd<j\\x97\\xbb\\xa6tb={\\xdc\\x0e<\\xd2\\x16}\\xbc\\xc1q\\xc6<\\x9f\\xc6\\\"=\\xf8\\x1cT;%\\x89\\xb5\\xb9\\xdc\\xa0w<2\\xd5\\x00=\\x91\\xc8\\x0b\\xbd\\x88+\\xd8\\xbcpu\\x8d;{\\xb0\\xce\\xbb\\xec\\xbb\\x01\\xbc\\xa9\\x1d{\\xbdT<\\xa2\\xbc7\\xb2\\x07\\xba\\xaf\\x89\\xc0<\\xa4\\x92\\xd1<\\xd95\\x89=jE#=\\x04\\xa1\\xe2\\xbc>\\xc2\\x00<\\xb8<r=[@\\xd1\\xbc\\\"6\\x81<u\\xd4\\xce;\\x01@\\x05>\\xb0\\xc4\\xa9;\\xfb\\xa0\\xb8\\xbc,$j\\xbc\\x93\\xba[\\xbd\\x1d|\\xbd\\xbd\\xb6\\xc0\\x15=\\xa4\\x9c\\xe5<\\x84\\xdb\\x95;\\xaa\\xf5\\x08=\\xa0^_\\xbdd\\x1f~<\\x0b\\x86\\xd1<\\x0c\\xaa\\x18=\\x84\\xd4\\x98<\\x03\\x01\\x90:\\xce\\xbb\\x9b\\xbc\\xbc\\x90@=\\xa7\\xf4\\xf2\\xbb\\xbc\\x15\\x92\\xbd\\xe8\\xd4\\xbc\\xbc\\xd5\\x0c\\x86\\xbc(\\xcc\\xa8<5\\xdc\\x82;u\\x0b?=\\xdd\\x17\\xf2\\xbc5\\xc1\\x05=\\x90\\x028=\\xeaE\\xdc;_\\x98b\\xbc\\xba\\xd1\\x8a\\xbcN\\x85W=\\xcdq\\x1d\\xbb\\xb8\\xb93=\\x81\\x1e\\xb7\\xbc0\\x96D;Q8\\x1b\\xbd\\x16\\xd6E=+\\xbfh\\xbdx\\x03U<\\x92v!=z#>=\\xb7\\r\\xe7<\\xaeE\\x8f<Cc\\x16=gyo;\\xb3\\xa30=\\xa0\\x02\\x14:\\xed1r=qkL=\\xa5\\xbeo\\xbc\\xa2\\xe1\\t\\xbd\\xe4Vb<\\xd4TR<\\x04\\x18\\xb8\\xbcP6}<u\\xa8*<b\\xd9\\x07\\xbd\\xce\\xc9\\x1e=\\xc9eO\\xbcy\\x06\\xd4<\\xfa\\xdc\\x07\\xbc\\x80\\x97O\\xbd\\x12\\xe2\\xc8<\\x06C\\xb1=\\x08\\x15B\\xbd\\xd6\\x97\\xd8\\xbc\\x05\\x02X=f\\xf5}=\\x89X\\x9b<\\x9f\\x1c\\xca<\\x90\\xdd\\x84\\xbc\\x89\\xae\\xb6\\xbc\\x0fvJ\\xbd\\xd7\\x9b\\xf4<f\\x00\\x9f<0\\xb5V\\xbcM\\x1b\\xac;#\\xb5&=q\\xaf\\x10<\\x81(\\x1c\\xbd4d\\x8a<\\x9aq\\x14=J\\xf1R\\xbd.JB\\xbd\\x0b8\\x8e\\xbd#\\xc6\\x01=\\x00\\x02\\xfd\\xbb\\xc1\\x1d\\xae=f\\xe9e\\xbc,\\xebC=H\\xfcW\\xbd\\x8aO\\xda\\xbc\\xc0I\\xd3\\xbc\\xf9\\xdfp\\xbc\\x7f\\xfd\\xb2\\xbd\\xcd\\xec\\x94\\xbc\\xe89\\x95\\xbb\\x01z\\x98<Zn\\x8f<;\\x1b\\x1f=\\xc1\\x1a+\\xbc\\xdb\\xa2\\x1c=\\x0eT\\xf0<\\x9f\\xaf\\xb1\\xbc\\x12o\\xe0\\xbc\\x01\\xa0\\\\\\xbd:\\xc6\\x18=gS\\xeb<\\xea\\xee\\xd4<\\\\r\\x0c\\xbd\\xaf\\xa5\\x9a=\\xa9\\x10\\x0c<9;\\x9a\\xbbq\\x91\\xca<<\\x15\\xcb<+\\x84f\\xbcjR*\\xbcI\\xe7\\xef\\xbc\\x8ac)\\xbdc\\x07L\\xbd,\\x7f\\xa8\\xbc\\xfb\\x93;\\xbb\\x05\\xef\\x08\\xbdI(\\x00<\\x10I\\x16\\xbds_\\x9d\\xba\\xd0\\x1b\\x13:YD\\x9f\\xbc\\xbdh\\x12=\\n\\xe5\\xec\\xbc\\x03\\x81\\xdd\\xbb\\xa2Dg=al\\xd2\\xbb\\xa1K\\x0e<!?\\xcf\\xbdD\\xf4{=\\x91Bx\\xbcmu-;I\\xe5>\\xbd\\xcf^\\x83<\\x99\\x07\\xe0\\xbc\\x15\\xa8\\xc9<\\xed\\xfaW<T\\x980\\xbd\\xcb\\xf9\\xfe<\\x04\\\"\\x12\\xbdi\\rs<>\\xb9\\xa2\\xbd\\xae\\x1c!\\xbd\\xd0\\x83\\xcf\\xbc\\x85\\xedT<\\x0b\\\"\\xf9\\xbc`\\xc0\\xa3<Lf%\\xbd\\x85m\\x9a\\xbc\\x9f\\x0bK\\xba \\xb4\\xd3\\xbd\\xc4\\xad\\x9a\\xbcC\\xbaI:\\xab\\x1f\\x06\\xbc]\\xd4W\\xbcM[\\xa0;\\x99\\xf2=\\xbd\\xc6\\xa5e<\\xe4I\\x92\\xbc\\x9b}\\x92<qc^\\xbc\\xd7\\xdf\\xe4\\xbc\\x8c\\x9a\\x1d<\\xcdz\\x05=\\xa3\\x10\\x83\\xbc[\\xd5\\x9a\\xbdn\\x8d\\x19\\xbd\\xb0\\xbcJ\\xba\\x89t/\\xbd\\xb3\\x19\\x87<e\\xa7\\xdd<\\xb6ZR=\\xb4/\\xb6<1|\\xd2<F\\xaaC\\xbdX\\xe7\\x89\\xbd\\nS\\x84<\\x03\\xfd\\xa9<]\\xa8A=<\\xa7)\\xbd\\xcb\\xd1V\\xbb\\xcaz\\xe6<\\xca\\xdd\\xda\\xbcC*\\xda;\\xc4{\\x13=\\xc1\\xd5\\x92<\\xa2\\x9d\\xcc\\xbcCO\\x13<\\x9at\\x01<\\xe28X=E\\x9aq<,\\x87B\\xbdB\\x10\\xed<\\xb12\\x89\\xbc\\xa9r%<\\x9c\\xbf\\xec\\xbc\\xa1x7\\xb9\\\\e6;\\xcc\\x0e.\\xbc\\x94\\x02*;\\xc4z\\x98=J\\x8f\\xed\\xbc\\x90\\xb2\\xfb\\xbc\\xaa\\xc1\\xc1\\xbc\\xe1\\xa7\\x88\\xbb\\xb3R\\xab<\\x0c\\x02N\\xbd\\x8b\\xb9\\x87<mv\\x1d=\\xbfs\\xe9<\\x14\\x9f\\xe1<\\xb2A\\xa6=\\xe4!\\xcd<\\xebQ =\\x94<\\xd7=\\xc5\\xfc\\xb2\\xbc\\xb2s\\xed<! \\x06\\xbc\\x97>\\xee<\\xa0\\x0b\\x85\\xbc\"\nHSET bikes:10052  model 'Ganymede' brand 'Breakout' price 4938 type 'Enduro bikes' material 'full-carbon' weight 16.5 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"K?4\\xbc\\x8bb\\xc4<\\xaci\\x94\\xbc\\xa6\\x8a\\x87<e\\xba\\xb1<\\x06\\xb3==\\xec\\x82 \\xbd.e\\xe3\\xbc]r\\x81=!\\x84,\\xbc\\xdfB\\xc6;\\xd3\\xf7\\x80<jZ\\xff<u\\x11\\xc4;g\\xc1n=I\\x13|\\xbd\\xe6\\x06%=\\x17\\x86\\xbf\\xbc\\xd7\\x05\\xec\\xbb\\xb0\\x19\\x94\\xbdyU0\\xbc\\x05\\x1a!\\xbc\\xc2qo=\\x9b\\xfbY\\xbd\\x14\\xa3\\xbf\\xba\\x90a\\xc3\\xba\\xda7\\xbc:\\x12\\xd0\\xe1\\xbcp\\xcao\\xbd\\n\\xbc{=\\x88\\xf6\\x18\\xbcj\\\"q\\xbd\\x11r\\x16\\xbd\\xb10\\x96\\xbc\\xff\\x1a\\xb1<\\x16\\xaf\\x97\\xbct \\xfa;\\x82\\xc5\\x89\\xbb\\x91\\xb5\\x98;\\xf3t\\x0b<\\x15D\\x06=e\\xdf,<\\xef\\xbe\\x0c=\\x8d \\xe7<\\x0b4\\t\\xbc\\xd7=&<\\xa4\\x8d\\x8f=\\xf8\\xd9v<[W\\xa6\\xbcs\\x0f\\x89\\xbc\\x006\\x94\\xbc\\x0fF5\\xbd\\\"\\xe3\\xed\\xbcT\\x82\\xc6<\\xb6\\x8d\\x1f=i\\xd2\\x18<S\\xeb\\x9f\\xbb\\x8dD\\xd0\\xbdg\\xc0\\xa1\\xbd\\x0c\\xcf\\x9b=^\\xac\\x1d=\\xf7~H<?\\xc1\\xd5<\\x19\\xb1y=|\\xff\\xec<\\xb6\\xf8\\x9e<\\x99s5;\\x16t\\xce<c\\xca\\xfa\\xbb|\\xc7\\x04\\xbd\\xe3\\xce\\x17\\xbd\\xc4\\xfa\\xad\\xbb\\x94++\\xbd\\xbd\\x00\\xfc;\\xe0\\x8e:\\xbd\\xda]\\xda<\\xb39n<E\\xa8\\x86\\xbc\\xc2\\xff\\xa2\\xbd\\x9e\\xbbD\\xbc\\xe2\\xeea\\xbb\\x89\\x10\\xa4\\xbd\\xe4\\xd5\\xb9<G\\x7f\\xa6\\xbd\\x9f0|\\xbc\\xbf\\x9f\\x11\\xbdE[6<n\\xbd\\xaf<\\x17\\xcew=\\x89\\x92\\xbf;\\xe5+\\x169\\x1a_<\\xbc$\\xbe\\xbd\\xbc\\r\\xa4Z;\\xdau\\xad;\\x13D\\x89\\xbc\\xb3:\\xdb\\xbb\\xf8\\x93D\\xbd]\\xed\\xe9<\\xae\\x8e-\\xbd\\xfeR\\xfb<y9\\xc9<)\\x83\\r=\\xdfr\\x93\\xbd\\xc7\\x16\\xc3\\xbc\\x0e\\x15\\xb4=)mf\\xbbU\\xc8\\x12=\\xfe\\xcc\\xba\\xbb^\\xe2(=\\x96\\x88\\xe5\\xbc(N\\x90=\\xd8\\x96\\xf6:\\xdcI\\x94\\xbc\\r\\xb1L\\xbd\\x8b\\x90F\\xba\\x81j\\x1e\\xbb&)\\x1a\\xbc\\xe7\\xa9\\xff<\\xb8\\xf9|<z\\x99\\xd0\\xbb\\xfdmZ<\\x03\\x97\\x06\\xbd\\n\\xc4n\\xbd\\xcd\\xc8\\t=6\\xdc\\xb1\\xb9\\xedV\\x00=\\x833\\x8e\\xbdfza\\xbc\\xcd\\xf6]\\xbd\\xd4\\xb4\\xc3\\xbc\\\"\\x8e\\x0c<\\xb1,\\xd6<\\xa9b\\x11\\xbc,\\xc24\\xbc\\x10\\xa2\\xb0<\\xc5t\\xcc;\\x9e}h;+h\\xd6<\\x90d\\x12\\xbd\\xb7\\xf4\\x9a\\xb9Gc\\x82\\xbd\\xddgG=\\x04\\xa2T=(\\x1d\\x93<7u\\x19\\xbc\\xc1\\n\\xa6<\\x00\\x0c$<x\\x8e\\xc4\\xbc(\\xb2\\x1f\\xbc0\\xe9U<[\\xcb\\r=\\xbd\\xafZ\\xbd\\xc8O\\x9a<\\xef\\xb20\\xbd\\x0e\\x07\\x9e\\xbc\\xad\\x93\\x1a\\xbd\\x18*_<[y\\x87;\\xd3\\xf14\\xbd@9\\x1d=\\x19\\x1f\\xbd\\xbb\\xb9\\t\\xee\\xbcD\\\")\\xbd\\xc8\\x18\\xd4<1C\\xd6<5]\\xc4;\\xfaS\\x17=#\\x8a@=n\\xd0\\x9d;\\xf0+\\x8a<$A\\xe1;\\xc1\\xd9\\x8d\\xbc%\\xce\\x02\\xbdIH\\x17\\xbd\\xd0\\xea\\x84\\xbb\\x1e\\x19/\\xbd\\x0c\\xfa\\x86=\\xa8\\x03M<\\xbd-\\xe3\\xbc\\xae\\xf0\\\\<\\xfe\\xe5.<!K\\t\\xbdX\\x9f\\xcf<\\xee\\xe0f=8\\x91\\x82\\xbb,o\\x1d=\\x87\\xdb\\x10\\xbdW\\x17J<&\\xd7\\xfa\\xbc\\x13\\\"\\x14\\xbchs\\x15=\\xe6\\xcdm\\xbdT\\xc6\\x10=\\xf7I\\x89\\xbcBT3=\\x88\\xc1)\\xbd\\xba\\xb9a\\xbd\\xd1\\xc0\\xd5<&\\xc9\\x88=\\x00\\xac\\x18\\xbd\\x1bj\\xb4\\xbd\\xec\\xfc\\x9d\\xbd\\xce\\xce\\xc2\\xbc\\xd6\\xee\\xa9\\xbc\\x1c\\x8a\\xae<\\x97\\\"\\xc3\\xbc\\xfc\\xc8\\xb4\\xbb\\xda\\x17\\x01=\\xab\\x06\\xaa\\xbd\\xbbx\\x0f\\xbb\\x9b1\\xc2\\xbc\\xf6p\\\"=\\xcf0\\x1a\\xbc\\xfb\\x18\\x10\\xbd\\xda\\xb6\\xa3\\xbc\\x89\\x14\\xa2\\xbc\\x1b\\xda\\xac<\\xa9\\x1dj=\\xa0N@\\xbdgV\\xb6<\\xd5\\x07\\xeb:\\xf5\\xfe\\x05\\xbdK\\xc0\\xf4\\xbb\\xff\\xcf\\x00\\xbd\\xd5\\xdc\\\"\\xbd\\xa0\\xc4\\x9a\\xbcs[\\x94\\xbc#|\\x96<\\x952\\x8a\\xbds\\xc3(\\xbd\\x06\\xab\\x9c\\xbclx#;\\xa3w\\xd8<k_\\xe3<\\x19\\xf8\\xad\\xbc\\x18g\\xcc<\\xd6\\x1c\\x06=\\xa7\\n\\x93\\xbc\\xcf:\\x11\\xbc\\x9c\\x85\\x18=\\x15\\xd0c<\\xe8\\xef\\x1f=\\xf6I\\xf2<\\x05Gb<up*=\\x14DS=\\x9d\\xfa\\x9c<c\\xe9y\\xbd\\xf1\\x13\\x1c=\\xdbp\\xb7<\\xf0i-\\xbd7\\x9c\\x0b=#\\xa8\\x8c=\\xebW\\x04\\xbd\\x17\\x95-=Z\\xee\\xa6\\xbc\\x8d\\xba(\\xb9Ot\\xb2;?\\xb8\\xb0=\\xcf\\xd9\\x1f;f^\\xb1;]-5\\xbd\\r\\x11\\x86\\xbbj\\xde\\x8a\\xbc7$\\xc4=\\x11\\xb9\\xfb\\xbc\\x1e\\x98B\\xbc\\x95\\xec]<\\x86i\\xe6;)7\\x0c<\\x1b\\xc5\\x81<\\xa8f1\\xbd<\\x82\\x03=\\xad&r\\xbc\\xb7\\\"\\x16\\xbd\\xa1D\\xdd\\xbc\\xe7c]\\xbcc\\x84\\xc5;\\xbd\\x0b\\xc5\\xbd\\x16D\\xda\\xbc\\x196\\x06=\\xb4L\\xb0\\xbc\\xff\\\\*=\\xc2\\xb0N=\\x89\\xf6\\xd9\\xbc\\xb6\\xb4\\x90\\xbcD5\\x13=W\\x91\\x86\\xbd\\xde\\x1d9\\xba0\\xbf\\x06=M\\x90\\x07\\xbd~\\x1b\\xc9=L\\x07\\xb5<\\xb3\\xbe\\x1a\\xbd\\x87\\x8a\\xa4<\\x8e\\x9f\\x86=\\xde..=*\\\"\\x92<(\\xd0A\\xbc\\xba\\x8a\\xb1<\\xf0\\x9e\\x0b\\xbd]\\x7f\\xf8<\\xa9X\\t\\xbd\\xf9\\xe90=\\xb6\\xee3=\\x1e\\xa4z<u\\x95\\x15=\\xe86l=AM\\xce<\\xbe\\xa9\\x1b\\xbd2\\xb4\\x8d\\xbc\\xbexc\\xbd\\xb15r\\xbc\\xa5\\x1d1=\\xc8y\\xbe\\xbcc\\\\{\\xbcu\\xd3(=\\xf1\\x05\\x06=\\xd8\\xa5\\x08\\xbdl\\xf0\\x84\\xbc\\xd1t\\x18\\xbd\\x0c\\x84\\x99<Ix\\xf1\\xbcH&\\xe9<#D2\\xbd\\x02^9;\\t6\\xac\\xbd7Ia<\\xf3!\\x1c<\\xc6\\x1b\\x83;\\x8d\\x02z\\xbd\\xa9\\x00X\\xbd\\xfc\\xa59\\xbc_\\x92\\x14=\\x8cZa\\xbd4/b\\xbd\\xac\\xa1\\x1d=\\xca\\xebM=\\xa7\\xd1\\x8e;l\\xd89<_\\x13h\\xbd\\xd9!Q=\\xbd\\xe5N; \\x91!<f\\x96\\x14=%\\xac\\x93=\\xe4\\x1e\\x05=\\xe9\\x08\\x87=\\x92\\x11\\x12\\xbc\\xbdd\\x05=\\xa1\\xee\\x17\\xbd\\xe7\\x89y=\\xa1+\\x89\\xbc\\x8eS\\x83\\xbd\\x82\\xc6\\xe0\\xbciB:<\\xb2\\xda\\x8c\\xbb\\xd4\\xde5\\xbdyZ\\xf7\\xbd\\xccB\\x8d\\xbc\\x06\\xf8\\x86;\\xf5\\x1f\\t\\xbc<cq=q\\xben=\\x88$U=\\x10\\xa9?\\xbc4\\x9dm:.\\xa8t\\xbd\\x95\\xb4g\\xbd)\\\\\\x8d<\\xfa\\xb7\\xc8<\\x9c?+:\\xcbr\\x13\\xbd/a\\\"<)S.=T\\xef\\\"\\xbc\\x18\\xc9l\\xbd\\x15\\x96\\xd9\\xbc\\xc7\\xac\\x0f\\xbcgqO\\xbd\\xe9#\\x19\\xbc\\xdd|\\xd4\\xbc0\\xf53\\xbc\\xe6\\xdc\\xad\\xbd\\nyY<\\x1b\\x1e\\xbb\\xbc\\xce\\xc9\\x8d\\xbcY\\xbf[\\xbc\\x83%\\x11<A\\xe0\\x8d\\xbd\\xd8\\x1ad=\\xab\\xfa\\xb9=\\x07\\xa2\\x13\\xbdi:\\xd8;A\\x1d\\xe8<\\xc2\\xb7$\\xbd{\\x1b\\x02\\xbb\\x86\\xa6M=\\x9c\\xe8\\x84\\xbd\\\"\\x1f\\xe2\\xba\\x85p\\x81\\xbd\\r`\\x03=\\x93e\\n=@\\r\\xf5<\\x9e\\xdc\\xc4\\xbb\\x13\\xc0\\x93=\\x14\\xce\\xca\\xbb\\xd8\\x9e\\xbd\\xbc\\xd1DP=R2Y=\\xdb\\x051=\\x97{&=\\xb4\\xfb+\\xbb|\\x05\\xbf<\\x0fh\\xcd<V7\\x90\\xbc\\x1f\\xce\\x98\\xbd\\x97\\xbf\\xee:\\x0f]\\xdf\\xbb\\xa6\\x0e\\xf4;\\xc1\\xad\\xbf\\xbcv\\x17`\\xbcv,;=1\\x8a\\x99\\xbcNf\\x8f\\xbc\\x08a\\t=\\x9c`\\x8e=\\xf5\\x91\\xcf<D^1\\xbd\\xf9\\xc0M\\xbb\\xb2\\xc3o\\xbbV,#\\xbd\\x08\\x11\\xd5\\xbc\\xa9~\\x8f\\xbdz\\xf9\\x85=\\x16iR=\\x9e\\xf5\\x0f=\\xcf#\\xf5<\\xeb=\\x05=-\\xc5L\\xbcc\\xab\\xef\\xbc#\\xedS\\xbaa\\x03h\\xbd\\x18q\\x14\\xbb|Q\\x18\\xbc\\x90\\xb4\\xea\\xbb\\xe3XL\\xbc\\\\\\x1d\\xba\\xbc\\xa1\\xb7\\x84<}\\xd5\\x83\\xbd`\\xb6\\x1a\\xbd3\\x91\\xf1\\xbbK\\xa5w\\xbd\\xeb\\x900<\\xdf\\xa3K\\xbcS\\x8a\\xe9<\\xcc\\xf5\\xb7\\xb9\\x84\\xad\\xa2\\xbb>\\xf3\\xa79J\\xc2Y=E\\xa1\\x92\\xbb\\xbc\\xbd\\x0c<\\xfc\\xd8\\xa4\\xbc\\xa5\\xe25<\\x99\\xba\\x15<|>r\\xbbe<\\x16\\xbd#\\x9c\\xdf\\xbb,]N=-4\\xe5\\xbc\\xbf\\xad\\xce<\\x114\\xd6<\\xf9n}<\\xa5\\xcf\\xbc<\\xdd\\x8d\\\"<\\xf0?\\x04\\xbd6\\x1b,<=\\xba\\xce:^\\xe8b\\xbc)\\x9e\\xa5;\\x1b\\x85%=\\xf5c\\xc2\\xbb\\x94\\x0f\\x98<\\xe6\\xc2\\x11\\xbdp\\x0b\\x91\\xbd\\xef\\x1e\\xb9<\\xb9\\xdc\\xdf\\xbc\\xd6\\xd8\\x01\\xbd\\xef\\xa8\\x8a\\xbb\\x8b6|\\xb9j;\\x82<\\xed\\x1ag=\\xbe\\xec\\xe69Nch\\tL\\xe6\\x88\\xbbn&\\xb3\\xbc\\xbcc\\xab<]\\xb9\\xc1\\xbai\\xe3\\xa1<\\x87:\\xcf<\\xcb\\xf7\\x07\\xbc\\\"\\xc7\\xe9\\xbc\\xe7\\x90\\xa4;\\x19\\x98\\t\\xbd\\xfa\\x0f\\xc8<\\xee\\xfa*=\\xdame\\xbcsL|=\\xfd\\xe9\\x84<\\x12\\x83\\x94<\\xd9\\x04\\x84\\xbd\\xbf\\x98m\\xbd8!\\x8f\\xbc\\xfaZ\\n=d?\\xe7\\xbb9$\\xbb\\xbao\\x0c\\xb1\\xbcY~%\\xbd!f\\xcb<\\xc9\\xdf\\xef\\xbb\\xa5\\xbc\\xa9\\xbb\\xfe\\x9d!=\\x98\\n\\xa6\\xbc\\xf6\\xc7\\x1b=\\xb8\\xc1\\x0c<\\xbfm\\xbc\\xbcX\\xb7\\x9a\\xbd\\xc2\\xe0\\x8c\\xbd\\x8d\\xd7\\xb4\\xbdyj\\xc8<l\\x86L=\\xa9ML<\\x8d\\x91\\x9e<B\\xa7\\xad;T\\x1c\\x06\\xbc=TL\\xbb\\x15\\xb6\\x97\\xbc\\x81\\x8b|=\\xed\\xb5\\xe9;@\\xcf+<[(\\x8d=\\x92\\x80\\xb7\\xbcR\\x9d \\xbd\\x80\\xc4\\n\\xbb\\x8b\\x19\\x08\\xbd\\x97\\x8f(\\xbd\\xca+q<|\\xaf\\xd2<\\xb5$\\x00=\\xb3\\x9d\\x9d<}\\xca\\x87\\xbc\\xb1\\xa3E<\\xf5=\\x12=\\x04K\\xbe<\\x1dO\\x1a=\\xe3\\xb3\\x0c\\xbcy:\\x07<\\xb2iP=\\xd6\\xe0\\x94\\xbd^\\x9dV\\xbd\\xbc\\x13\\xe5\\xbc\\x07\\xede\\xbbP\\x1f\\x07=\\xd4#\\xc4\\xbb\\xd0!<=$F\\xcf\\xba\\xc9\\x83\\xde\\xbc\\xba\\xa0\\x13=<\\xc1\\x1a=\\x05d\\x11\\xbc\\xef\\xf8*\\xbddb\\x9a=\\x8eYC\\xbd} ~=\\x83\\x1f\\xfe\\xbc\\xb6\\x12\\x8a<\\xbd\\xb1\\x07;\\x06\\rQ<i\\x9b\\r\\xbd]\\x97\\x16\\xbb\\xe6bI=->\\xa9=X\\xec\\xa8:\\x92\\n`;j\\xd4\\x8e\\xbb\\xd4\\xec<\\xbcyX#=\\xfa\\x8a\\x18:\\xb7\\xca\\x16=6cy=\\xc6M\\x81;\\x9a\\x11/\\xbd\\x9dZ5\\xbc{y<\\xbc\\xa6\\xbbz\\xbd\\x07\\xd0C\\xbdp\\x07\\xbd<!\\x174\\xbd\\xf9\\xd3C=4\\xa9E\\xbd\\xbdK{=`Y\\x82<\\xf2R\\x9c4\\xf8uM:\\xe4\\xa2s=\\x97\\xc2\\x1f\\xbdZ^\\x989\\x05\\xcb\\x01=\\xa7\\xea9=5O\\x06=xQ\\x14=\\xfe\\xfa\\x16<:\\xf1\\x1a\\xbc\\x1er7\\xbd+:\\x10;y\\xc5\\xc7\\xba\\\"\\xbc\\x85<\\xf3\\xf9\\x9b:g\\xd6\\xbe<\\xc4\\xaa\\xea\\xbb\\xd6\\x87\\xf5\\xbc\\t\\x8d==\\xf53w=\\xc5\\xe4V\\xbd\\x98\\xcb\\x07\\xbc\\x88\\xdc#\\xbdl\\x0b\\xf1<c\\\\[\\xbb\\xbe\\xd9n=\\x10>\\x13=\\t\\xd2\\x92=\\xbc\\x80*\\xbd\\xed2\\x96\\xba\\xbf\\xcfr<\\xe1\\xe0\\x8f=\\xfb\\xe0*\\xbdx\\xd6!\\xbd\\xb1^\\xb0\\xbb\\xb3\\xed\\r=\\n\\xa4L;\\xf8\\xc8\\\"\\xbcz`\\x9c\\xbc~\\x18\\xda:k\\x94[\\xbc\\x9cx\\xa3;\\x87\\x18\\n\\xbc\\xees\\xa7\\xbcJ\\xd7\\xc4<_\\xfcd<b67=\\xb5\\x15F\\xbd:\\xfa\\x90<)\\xcb\\xc1<6\\x8e\\x8f<\\xeap\\x07\\xbcM\\xfc\\x8f=o|\\x9c\\xb9\\xe5p\\xca<o`\\x98\\xbc\\xc8\\x0e\\xed\\xbb>`V\\xbd[%\\x19\\xbc\\xf6\\xdd\\x84=&\\x0bi\\xbd\\x86\\xd9n<\\x8f4y<\\xfdX\\xcd<\\xcd*\\xd0\\xbbx\\x1e\\x93\\xbc\\x85\\xb4^=\\xa3\\\"\\x8d<\\xe2u,\\xbb\\x97\\x13\\x8b\\xbc\\t\\x0bK\\xbc\\xa9\\xed\\xcc\\xbbo\\x88\\x8a\\xbd\\xd6\\xc7.=S\\x8e)\\xbd\\xe1\\x02\\x11\\xbd^{\\xbb\\xbaJ\\x03\\x05\\xbc\\xcf=M\\xbclB\\xf8<\\x81\\xe5\\x04\\xbdu\\xdaX<\\xe0\\xc5\\xfb<\\x104F\\xbc\\x06 ]\\xbc\\xd9\\x02I\\xbd\\xe2E\\xf2\\xbcm\\xb7U\\xbc3\\na<%\\x94\\xf6\\xbcJ\\xb4\\xf4<\\xf1\\xb3g\\xbd3\\x86\\xaf<\\xe1\\x94\\xfe\\xbc\\xb2\\xe0\\xea\\xbc~|-\\xbdY\\x18\\xee\\xbc\\x83O\\xcf<$\\x89\\xd7\\xbc\\xecl\\xc8<\\xdar\\x89\\xbd\\xe7\\xe9\\xfb;\\xa1\\xec%\\xbb\\xbav\\x06;\\xd7\\x84\\xa4<\\xf7\\x13h\\xbd\\x87\\x1d)\\xbcI\\x99A=\\xa3\\xb9\\xa7\\xbcH\\xb3\\x92\\xbd\\xdf\\xb1H\\xbdh\\xe1\\x91\\xbb.\\x00\\xbb\\xbcHa\\xb3=>\\xb4\\x94;\\xa2\\x9b\\xb5=\\xacG\\x97<\\x1f\\xdd<=\\xde\\xbc\\xf7\\xbc4\\x15\\x96\\xbd^\\x0b\\x19=\\xc0P`<\\x8a\\xea\\xdf\\xbcs\\x18\\xa1\\xbcQ\\x1d\\x7f\\xbc\\xa7\\x97\\xb5\\xbc\\x89<\\x18\\xbdx\\xf1:\\xbc\\xf8\\xb1\\x10<\\x01\\x9cN=\\x9c\\x9f\\xa9;+g\\xd7<a\\xc8\\x1b=\\xeeF\\x18=o\\xb0\\xb9;\\xba)\\xb3\\xbc\\xc6mm;\\x9f\\x87 \\xbdt\\xb3>\\xbc\\xaao\\x8a\\xbdo\\xf3\\xa9;\\x01-\\xbd\\xbc\\x8d9\\x9c\\xbc\\xb5\\xad\\x16<\\x1d8\\x1f=\\x0b7f\\xbd\\xb6R\\x15\\xbbr\\xb6\\x8c\\xbdn\\xf6\\xee\\xbb\\x0b\\n\\x10\\xbd\\x07\\x05\\xaa\\xbd<\\xb3\\x8d\\xb9\\xfd\\xa5~=Gl\\x05=$\\x1cD=\\x0fG\\x95=|\\xed\\x88\\xbc\\x80\\x92#=9\\x15\\x9a<\\x11\\xb2W\\xbdk.\\xfd<\\x0cSR\\xbc\\xb2\\x83 =p\\xda\\x8e\\xbc\"\nHSET bikes:10053  model 'Io' brand 'Peaknetic' price 665 type 'Kids mountain bikes' material 'aluminium' weight 13.5 description 'This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. It has a lightweight frame and all-carbon fork, with cables routed internally. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\xdcV)\\xbd&\\xe5r=\\x93J_\\xbdf(\\xd6\\xba\\xb4\\xe7B<j\\xc5D=\\x9f\\xe6\\x9b\\xbc\\x87M_\\xbd\\x9e\\x0f\\xa4=I\\xe9\\xbc<OsI;\\x9f1\\x04=<#0=c-\\r\\xbd\\x03{\\x1a=K\\x93\\xeb\\xbc\\xf3\\x0co<\\x13\\xa1\\xf4\\xbc\\xcb\\x8c\\x1d\\xbad^]\\xbd\\xa4\\x8b?=\\x8d\\xb5!\\xbd[\\x88G=\\xceR\\xda\\xbc\\xdd\\xc5\\xf3\\xbb\\\"Z\\xbc<>\\xc8.<\\r\\x80\\x07=\\xbb\\x01\\x05\\xbc>{^=dz#\\xbc/\\x8c\\xe7\\xbbwU =\\xf2\\xb9\\xa9<\\\"\\x1b\\xe6<+\\xbf_;\\x8a\\xda.=\\xf7\\x1f%\\xbd\\xa2\\xda\\x9a\\xbcZ\\xd1Y\\xbc_\\xad\\xd2;w\\xddO<\\xbaq\\xc8<\\x9cNf<p\\x8a\\xca\\xbc\\x8b\\x0c\\xeb\\xbc\\xed\\xa1t=;=\\xa2\\xbc\\x94a\\x95\\xbc1\\xb2%\\xbd\\xcb\\xd8\\x02=\\x0ce\\x8d\\xbc7%Z<\\xdd\\x194\\xbb~N\\x9a\\xbc&`1<\\x1f\\x18\\x95<\\xdd\\x92^\\xbd\\xc7V\\x1e\\xbd\\x02ez=U\\xc3\\xc2\\xbbQ\\x0bo=\\xeb\\xc08=\\x0eu\\xab=c\\x9c\\xf5<)\\x18\\x0e<\\x10\\xab\\x8c\\xbc\\xa3\\\"\\x96\\xbcP\\xc1U=\\xd8_\\xe6\\xbb6\\x88\\xaa<\\x03\\xfc/\\xbbw\\x81\\x8b\\xbd\\xbb\\xaab;q\\x97\\n\\xbd\\x91\\x8bQ<\\x1c\\x86\\xa3;}`\\xcf\\xbc\\\"\\\\\\xa3\\xbd\\xc5U=\\xbc\\xed\\x8f\\x81<\\x93r\\x0e\\xbc\\xd2\\x0c8\\xbc\\x1at}\\xbd6\\x01_<@\\xa6l=8\\xdb;\\xbb{\\x15K<Q\\xe4\\xc6<\\xe3qp<\\x1fh\\x89<\\xde\\xb9,\\xbdA\\xea\\xc6\\xbc.wQ;\\xc1\\x08U\\xbb\\xc5\\xb1/\\xbb3\\xbc\\x03\\xbdm*\\xf0\\xbc\\xd8j\\xf2<\\xf9\\xdeP\\xbd\\xe1\\xfb\\xf8<\\xb14B\\xbc\\xed\\xa5\\x17=\\x11L\\xe2<\\x986\\xc9\\xbch\\xcc\\xd7=\\x8c\\xbb\\x8e;W\\x8d\\x9f<\\xbbTF\\xbb\\x9a`\\x0c\\xbd\\xb4\\x12\\xb9\\xbc\\xdf\\xcaB=4\\xf5\\x88<<Rm\\xbd\\xa6w\\x96\\xbd\\xe4T\\xe0<^\\x9eO=\\nV\\x1e\\xbd\\xd8\\x7f\\x1e=\\xbd\\xbe\\x00=\\xa7yC=:\\x0cB=Rv\\x1e\\xbd\\xc9b\\xf18\\xc90h=L*\\\"\\xbdO\\x05><R\\x0f5\\xbd(\\x18\\x13\\xbdx\\xd9\\xff\\xbcn\\xfc\\x9e<D\\xddJ\\xbd\\xf4\\x8c\\x8a=\\x19\\xbe\\xc8\\xbc\\xfc\\x9f$\\xbcB\\x04\\xdb<k\\xdf\\xc7\\xbc\\xf7G\\n\\xbd]\\x0e\\x92\\xbb\\x1b\\xb22\\xbdv4a\\xbd{\\xc3\\x9c\\xbd\\xb9\\xde\\xba\\xbc\\xe3\\xad\\xd3=\\xa2\\x01u<n\\xf5H\\xbc\\xd2\\x8br\\xbc#\\x85\\x0f=\\x84n\\x01\\xbd:\\x13s\\xbd\\x07\\xf1h\\xbcw\\xd3\\x1c=\\xe1Oz\\xbd\\x87\\xa2\\xc3<\\xac\\x16O\\xbc\\x87\\xba\\xad\\xbc.\\xabU\\xbd\\x06/x\\xbb\\xec5f=N3\\xb1\\xbcr\\xc4H=\\x03q\\x1c\\xbd\\xf1\\xd9\\x03\\xbd\\x9f?M\\xbd\\xe5)\\xa7<\\x02u\\xa6<\\x1e\\r\\xa7\\xbc\\x94\\x04\\x10<cE\\xc1\\xbb\\x98\\x1c\\xcf\\xbc\\xba\\n\\x95\\xbcdz\\x8f<\\x80\\xb8\\x11\\xbcape\\xbd\\xe8x\\x97\\xbc\\n,\\x95\\xbc\\x8f\\x9c#\\xbd\\xc9$>=C7\\x1e;\\xfe\\xd2\\xb8<.LY\\xbc\\xa9\\xd7\\x0b=\\x9b?&\\xbbH\\xcd\\xb6</\\xc4\\x06=\\xde\\xf0\\xb1\\xb8X\\x88I=\\x053\\xde\\xbc>\\x03\\xd2;U\\xedC:\\x01\\xf18\\xbd\\xfbS{==\\x02\\xd2\\xbc\\xaeY\\x1b=#\\x14\\xce<sN\\x07<w\\xda\\xd4\\xbc\\xbeB\\xa4\\xba\\xe7\\xc3\\x88\\xbcQ\\x1fL=\\tmT<o\\x13N\\xbd\\x17\\x13I\\xbd\\xbez\\xa2\\xbbY\\x8b\\xe6\\xbc\\xa8\\xd7\\xff<\\x9dpM=\\x0e\\x01\\xfd\\xbc\\x89\\x17\\xd4<\\x83Q\\xdb\\xbcI\\xae\\xf0\\xbb\\xf2\\xea\\x13<5\\xb1\\x08<\\xe9\\x06a\\xbd;\\x91F=(pC\\xbc\\x82\\x98\\xad;\\\\l\\xa1\\xbc.V^=\\xfcX\\x86\\xbd\\x02\\x0e\\xa1<Y\\xbb\\xae\\xbc\\xe6t\\xbd\\xbcJ2\\x8c=p\\xd4\\xfc\\xbcu\\xc6\\xe7\\xbc\\x00\\x04\\x1e=\\xa1\\x0e\\xe7\\xbc\\xc0K!=I{\\xe2\\xbc\\x9d\\x91#;\\xad\\x8bD<\\xc6\\xc5\\x88\\xbc\\x9f\\x11\\xfa<\\xbc\\xf0v\\xbc\\xef-9\\xbd=l\\xf8\\xbc\\xe9\\xee\\xa3<\\n\\x03=\\xbb\\xd79\\x11<\\xfb\\x1e\\x96\\xbc\\xd1\\xb7\\x15\\xbb\\x0eX\\xad\\xbc\\xda\\xe1\\xf2\\xbau\\x88\\xb8<j\\xd1k<\\xc2\\xea=<m\\xba\\xc3<rj\\xd9\\xbd\\xa4O4=Pd3\\xbc\\xb5~\\xc1\\xbd\\xa6\\xa0\\x19\\xbd\\x04\\x12:=\\x7f(U\\xbc\\xed\\x85\\xbb<I\\xe3%\\xbdOlf;\\xd5\\xdc3\\xbd\\xcc\\xe6(=\\xb3\\x8d\\x12\\xbd\\xd8\\x13\\xc69G\\x91.\\xbd\\x0ba\\x93\\xbc\\x9a\\xcai\\xbd\\xb2\\x1f\\x7f=\\xde\\xf1\\xa0:e\\x07D\\xbb\\xf5\\x9e\\xad<\\xd4f\\x9a<\\x0b\\x82|\\xbcV\\xa0R\\xbc\\xf1\\xa8y\\xbbw\\x0b\\xe0<\\xb0;!<{\\x00%\\xbc\\x15}\\x8f\\xbc\\x08\\xe7\\xe4<g^\\x16\\xbd\\xd3 \\xaf\\xbd\\xa7\\x00\\xf7\\xbc,\\r<=[\\x11\\x0e=\\xfc\\x94\\xc9<m\\x92\\x98\\xbc\\xb5\\x16\\x10\\xbd\\x9b\\xa63\\xbd\\xa6\\x89\\xe7\\xbb\\x04W\\xe1\\xbdg4\\x1d=v\\x8e\\x9d\\xbc\\\"\\xb3\\x06\\xbd\\xa4Zz=~m{\\xbcH\\x1fJ\\xbd\\xd7(\\x0e\\xbd\\xb5n\\x10;\\xd4\\xca\\xe6<\\xdc:\\x86<G\\xf2g\\xbd&9\\x18<\\xc3\\xb0\\x82=\\xaf\\xfc\\\\=\\xc0\\xef\\x1e\\xbc~@4=S\\xc4;=\\x1f\\xec\\x14=\\xc2\\xca\\xd3<\\x1bn\\x9d<\\xbb\\xb1\\xea<\\xf4Qd\\xbdJL\\x96\\xbc\\xd0\\x16\\xf9<\\xe9;:\\xbdQ\\x18\\xb1<\\x1f^]\\xbd`i\\xe8\\xbc\\xdbt[=\\x9f\\x1e\\x0e=33\\x94<$\\xb5$;}\\xf3\\x10\\xbd<4\\x1c=\\xd3\\xb2\\x92<\\xd8\\x17\\x01=+Z4\\xbd\\xf58\\xa3<\\xfcR\\x98\\xbd72\\xdc\\xbc\\x18\\x1b\\xde\\xbc\\xd8g\\x8d\\xbdn\\x8bP\\xbc\\xee\\\\D\\xbd\\\"}\\xbc\\xbc\\x1c\\xd8\\x93<\\xa5\\xb9\\x8b<\\xd6\\xcb\\xb1\\xbc\\xa3NX;\\x91\\x93\\xa6<^\\xbd\\xf9\\xbc\\xe2\\x8a\\xe6\\xbc{&?<\\xcdb\\x89=\\xd6\\xd6\\xd8;\\xbbY\\t\\xbd\\xf3\\x1e;=o\\x10\\xa2=\\x9c\\xa7\\xa7<\\xc2\\x1en=\\x0f\\xe6\\xb4\\xbc\\xefX\\xcf\\xbc7\\xac;\\xbd\\x04\\xca\\x8c=\\xef\\xd6\\x92\\xbb4g\\x8d\\xbbW\\x06\\x86<\\x95\\xf9\\xae<\\xaa\\xc1%\\xbd\\xf2t\\xd9\\xbc\\xac\\xb0\\xd8\\xbd\\xd5\\xc7\\xee<\\xa3\\xd8\\xa4<\\x06\\xf1\\x99<\\xd1g\\x8d=\\x1b\\x1c\\x82;\\xae/\\x18\\xbd\\x81\\xaf*\\xbb\\xfeHN=\\xd8\\xb3z<\\xb8\\xe1_;\\x1f]J=\\x86*\\xde:\\x8d\\xa6\\x13;t\\xafL\\xbcZ\\x9ep\\xbc\\x0c\\xd1\\xd2<.\\x07\\xfb;\\xe9(\\xde\\xbd\\xea\\xfe6\\xbc\\xed\\xbe\\x12:Z\\x7f\\x86\\xbc\\xf9\\xba\\x9c\\xbb\\xadc)\\xbd\\xd8Ev<\\xab;Y\\xbd\\x94<\\xfb<\\x80\\x0e\\xd5\\xbc\\xf1\\xeca<0\\x90\\x88<\\xd3\\x81\\xd8\\xbc \\xd4\\xa6\\xbd\\xa4=\\x91=s:\\x96=;\\xcf\\xd0\\xbc\\n\\xf5\\x89\\xba_4g=\\xc6\\x10\\xe0\\xbcu\\xd8\\xab<\\xb9\\x8d\\x85=\\xebW\\x83\\xbd\\x7f\\x81\\x93\\xbb\\xfc\\x87\\x00<Sj\\x0b<R\\xef%=\\xa4\\x89\\x9c<\\xbd\\xbdX\\xbd6C\\xbe<\\x17@U\\xbc+\\x01\\x11\\xbd\\xe3\\xf7\\x87\\xbc\\xa1\\xbf\\xd5<>\\xb6\\xcf\\xbb~\\xcb\\x92<\\xefi\\x96;\\xd5\\xdeG=\\xaa\\x84\\x02;@\\xb2\\xeb\\xbc\\xbd,\\xab\\xbd\\xb7$\\x94\\xbc\\x19=\\xc7\\xbc[L\\x1c\\xbd*\\x9a!\\xbd\\x7f=0\\xbbW\\nM=\\x97$\\x84<ul\\xdf\\xbc\\x04\\xe6\\x8d\\xbd\\x01\\xfb\\xb8=@\\x11V=T\\x050\\xbc\\xbc\\xcb\\x81<\\xfc\\xe3=\\xbd\\x9b\\xde\\x12\\xbd\\x00$}\\xbdmi\\xea\\xbcQ\\xf7\\xb9=\\xf6\\xd0X=\\x04L\\xbc;@\\xe4\\x8d<\\xee\\x08\\x04=\\xc6\\\")\\xbdA\\xc7\\xba\\xbc\\x9fV[<\\xe3\\xc3\\xda\\xbc>\\x82Y;V\\x88E=V/\\xc2<zi\\xc2\\xbc\\xef\\tr\\xbdr\\x16k=\\x97\\r\\x86\\xbc-}\\xe5\\xbcw\\xf4\\x14<;4\\xf6\\xbd\\x9b>\\xad\\xbbmk\\x8a;\\xd1\\xf0\\x8b\\xbbG{7\\xbcMT\\xe4;\\xd8\\x1cw=\\xa0\\xed\\xf1=\\xc0\\xd4\\xa3<6\\x80\\x9c=A\\x87:\\xbd\\xca\\xa4c<\\xe7/\\xdd<#\\x8a\\x06\\xbd\\xf7\\x98^\\xbc\\x06h\\xcf\\xbc\\x08^\\x1f=\\xc2\\x8d\\x95\\xbc\\xfa6\\x03<\\xd5\\xc6h\\xbd:\\x82\\xc6<\\xe8<\\xf0\\xbb\\xf6\\x91\\xb9;\\xd2dH\\xbc\\xfe\\x1ep9\\xb9\\xceL=-\\x14\\xdc;h\\x8b\\xca;\\x89x<\\xbc\\n\\xaci\\xbc\\xdf\\xcb\\x85;<\\xfa\\x98\\xbc@%\\xbe\\xbd1<1\\xbc\\xcd\\xadz;\\x1b;\\xaf\\xbc%<\\xb8<t\\x13h\\xbdlx\\x07=\\x80\\x8d\\\"=&\\xd2*<\\x14\\x08P\\t\\xba33\\xbd\\xe6\\xd1p\\xbd\\xea\\x87\\x80=r\\xc9?=\\xfe\\xd2\\xc6\\xbc>\\x9a#==\\xb28<\\x0e\\xf8\\xde\\xbc\\xe0\\x06\\xa0;jo\\xf3\\xbbd\\xad\\xe0<\\x17\\xc6\\x08=\\xe5N\\xa0\\xbc\\xa2\\xf1\\x0c<``\\x9a\\xbc\\x8e9\\n=Bi\\x19\\xbc\\xc7\\x11\\xd5\\xbb\\n\\x064;o\\xd8\\x80<?W\\x11=\\xef\\xc5%\\xbd\\xecE\\xb9\\xbc\\x97\\xac\\xb8\\xbc\\x95\\x85\\x00=\\xa7^\\\"\\xbc\\xac\\xb8\\xeb\\xbc\\xf4wY\\xbc/\\x16y\\xbc\\x9a\\xd8\\xc3;\\xc8\\xc2\\xec<\\xac\\xd8\\xd8<\\xa64\\xbd\\xbc\\xf9\\x01\\x80\\xbd6.\\x12\\xbcj,t<\\xce\\xab\\x9d:Fo\\n<\\xc2\\xd5\\x16<\\x10\\xc0\\xc7<oC\\x85<\\xb0\\xa8\\x94<xL\\x10<J\\xd2?<K\\x13Q<#1\\x01\\xbd\\xcf\\t\\x8c=\\xd0\\x10W:A\\x00\\xf7\\xbc\\x8f\\x9d7\\xbd\\xbfH/\\xbcWM\\x8c\\xbdy\\x16B=\\xdcTQ\\xbch\\x13\\xa5\\xbd\\x95E<=\\x1b\\xbc\\xff\\xbc\\\"\\xa5\\xa9\\xbc\\x9du\\x9e<7\\x9b\\x1a=\\x9f\\n\\x93;\\x90\\xcf\\xa8<d\\t&\\xbd\\xbd\\x8c\\xa6<\\tXT\\xbd\\\\7\\xa2\\xbc\\xf0\\xef\\\"\\xbd\\xc7$X\\xbb\\xdb)$<\\x8cg\\xff\\xba\\xe0\\xce\\xe4<R\\xcc\\xce\\xbc]V\\x13<\\xa3\\x86\\x01=z[\\x89;\\x00<\\x0e\\xbcQ\\x8d>\\xbc\\xe7\\xbaD=i\\x8d\\\\\\xbd\\xda;;=\\xc6UK\\xbd\\\"\\x05\\xb4<\\x9a\\xc9\\xe1\\xbb\\x86\\xd0\\xd4<\\xff\\x8f\\x07\\xbd\\x99\\xd8\\xf8\\xbcQ\\x03q=\\xc9c\\t<\\xd8\\x93B\\xbd\\xbaYV=\\xba+e<\\x9a\\x1c\\xc9\\xbc\\xe5\\xa56=M\\x84\\xa3:\\xc7f\\xa2=X\\x8f\\xf6<\\n:U\\xbd(\\x84\\xeb;\\x9f\\x9d\\xa9\\xbc\\x98\\xfbX=Ef#\\xbd\\x8e\\xe9\\\\<rt\\xfa;h\\x8c\\xca\\xba\\x90\\xc2\\x8f=\\xd1\\xd8P\\xba\\x9a\\xe0l<\\xa1\\xf3\\xd9\\xbc\\xa9\\xa2\\x85\\xbc\\x1b\\x7f\\x80\\xbd\\x10\\x93Z=\\xe3s\\xeb\\xbc\\xcc\\xba)\\xbd+%\\x05=\\xb9\\xf3\\x80=>\\xd7\\xcd<\\xe32\\xf5<\\xbd[\\xd1;\\xcf\\x91\\xba\\xbc\\xae\\xce\\xd0\\xbc\\xa5\\xd3}<c\\xdbd\\xbcg\\x19\\xc6\\xba\\xf0\\xa0\\xc3\\xbb\\xed\\xbbD=\\xd7a\\xa0:Y\\xe9>\\xbdCm\\x81\\xbc29\\x13=\\xe4\\xa4\\\"\\xbd\\x9ay_\\xbd\\x11\\xd9F\\xbd`.O=\\x12\\xc3\\x1a=:\\xa5]=V\\x8c\\x13=\\xf1\\xda==\\xaa\\xd7q\\xbc/\\x9d\\x83\\xbcJ\\xd56\\xbd\\xc4\\xc1j=N\\xb2\\x1f<OF\\x98\\xbd\\xb4;\\x04\\xbc\\xf1r\\x18;\\xd3\\xcd\\xf1<\\xb2\\x00\\x00\\xbd\\xf0P\\xfb:\\xa4P\\xe4<l\\xa9\\xc2<\\xca4\\x8d\\xbc\\xf1\\xa8\\x0c\\xbc\\x84\\xd59\\xbd\\x7f\\x9b\\x01=\\x13\\x994<\\xe37\\x0e=\\xe7\\xe7G\\xbd?\\x0cw<m\\xa5M<\\xa9\\x8b\\x0b=\\xb5\\xfd5<Q\\x11+=-#\\x00=\\xac\\x14E\\xbd\\xb9)\\xd3;#C\\xbc\\xbc\\xf5\\xbf.<}\\xcfU<m\\xd32=\\xb1\\x86;\\xbd\\x0b\\xd9\\x90<\\xfd\\x84x;\\xbeD\\xf7<{\\x14\\xd8\\xbcX\\x96\\n=\\x00\\xef$=\\xb8\\x9f2<P\\xbd\\xf8<\\x98\\x1dx=k\\xda\\xb6\\xbb\\x14\\x92\\x8d\\xbc\\xf3\\x1a\\xdd\\xbbe\\xa38\\xbd\\xdb$\\xbb\\xbcV\\x11,\\xbcf\\x161<\\x07a\\xa0\\xbb5\\x9e:\\xbd\\xfd\\xcd\\x8b=T\\x8b\\xb9<e=\\xbe<\\x96\\xfa\\x1f=\\xfa\\xab\\x9f\\xbc\\x86;\\t=Z%\\x8d\\xbc\\x99\\x8f`\\xbdK\\xd0g\\xbd\\x0b\\xe3U\\xbc\\x15A\\x11\\xbb_M\\xea\\xbcE\\x80\\x83\\xbdhN\\xe2;.S\\x9f;\\x8d^O\\xbd\\xc3\\x13.\\xbda\\xe2\\x98\\xbb|*t\\xbc\\xd4+_\\xbcP\\xa9]\\xbc\\x02i\\xee\\xbc\\x11t\\xd9:\\x05\\xf9w;=\\x9a\\x85<\\xae\\xb1\\xc9\\xbbM\\xe2\\x7f\\xbc\\xe4i)<\\x95B\\x9c\\xbc\\xa1A69\\xfeT\\xa5\\xbc\\x08\\x10T\\xbd\\\"o\\x06\\xbb\\x05\\xcbp\\xbc\\x15\\xd94=\\xfc\\xc1\\x16=\\x15\\xad\\xbc=(z\\x96<\\x94Q1\\xbb\\\"aG\\xbd\\xad/\\xeb\\xbc\\x07XS=\\xd2\\x89\\x95\\xba\\xeb\\x80\\x94=\\xf7\\x88\\x94\\xbb\\x9bA\\x0e\\xbc\\x15,\\x01\\xbd\\xa1\\x93\\x8f\\xbd\\xdd\\x96\\xa0\\xbc\\x9f\\x1b\\x03\\xbdEr\\x8d<\\xb76\\x96<G\\x97\\xa1<BI\\x8a=I}\\x00\\xbc\\xfc\\xf8,=\\xb8\\x977\\xbc\\xb9\\xc4\\xbb\\xbcW\\x8b\\xab\\xbb\\x7fA\\xe4\\xb9\\x1faS\\xbdFG\\xab;\\x90\\x8d4:gB\\xde<;\\x8c^\\xbcq\\x03>\\xbb\\xae\\x07\\xa1;\\rY\\xfb<6\\x99\\x04\\xbd\\x82\\x8d\\x85\\xbd\\xfa\\xc2J\\xbc\\xdb\\xbb\\x86\\xbd\\t\\xda\\x06=\\x85\\xd4\\xed;m\\\"C=\\x83W\\x08=V\\x91\\xbc=\\x8c\\xc5\\xdb;}\\xd0\\x8f<\\x16`\\x19=}\\xcc\\xc3<\\xe5p\\x0b=\\x81\\x12\\xdf\\xbc\\x1c\\xed\\xf3;\\x18\\x8d\\xfa\\xbc\"\nHSET bikes:10054  model 'Salacia' brand 'Ergonom' price 4471 type 'Kids bikes' material 'carbon' weight 7.0 description 'For shy or agressive riders, paved or dirt trails, this bike boasts kid-friendly geometry and strong quality parts at a minimal price point. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xe6fz<\\x89\\x17\\t=\\x8f\\x9f\\x91\\xbcS\\\"\\xf6<p\\xd6$<\\xc1\\xf6\\x11<\\x14k\\x0f\\xbd-77\\xbd\\xdc\\xdf\\xbe=\\x0f\\x0c\\xdf<i\\xe5\\xa4\\xbc\\x1f\\xf2\\x12=b\\x1f\\xc6<\\x9c*T\\xba!\\xe3m=\\x05\\x0fP\\xbdFm\\xf2\\xba\\x03eI\\xbd\\xde\\xf8\\x1e\\xbc\\x9e)\\x96\\xbd\\xdc\\x13\\xf0:\\xdf*\\x16\\xbd\\\\4\\x08=\\xa9m\\x96\\xbc\\xd8\\x91H=5i\\xd6<=\\xbc\\xcb;\\x1d\\x0f\\x82\\xbc:hU\\xbd\\xf0\\x90|=\\xa5\\x9a\\xd6\\xbcy\\x0b\\x15\\xbdQ\\xea\\xc8\\xbc\\xd3#\\x0c=\\xc5\\xa3\\xd4<N\\xf8\\xb9<\\xc9\\xae\\xa1;\\x1b\\x91\\x82\\xbc\\xe7a?<w~\\xf3;\\xcb\\x87\\x8a<\\x88/\\xb9\\xbc\\x84\\xfbF<*\\xc9r<\\x05\\xc1G\\xbb)\\xce\\xb3\\xbc\\xa0W\\xa6=\\x8f\\xf1R<\\x933\\xa1\\xbcA\\x98\\xdf\\xbbvG4\\xbc\\xa6\\xda\\x8a\\xbd0\\xfb\\xff\\xbc\\xd5\\x02\\xf5<wsu\\xbb\\xb3\\x83\\xb6<\\xf5i\\xa7<\\xf4!\\x8e\\xbd\\xa74L\\xbdx]\\x83=.l\\xcd<R\\xf9\\xed\\xbb\\xb5\\xd7D=\\xd3\\xffY=\\x16\\x95\\xbf<\\xc8\\x11\\x16\\xbb\\xdf:W\\xbc\\x9c\\xef\\xc7<l\\xc1\\\"<\\x998g\\xbc\\xdc\\xa61\\xbc:9\\xa0\\xbc\\x1e\\x03\\x0e\\xbd\\x9a\\x92t<\\xf4\\xb7\\xc0\\xbc\\xb6X\\x14=\\xad(\\xc9<.P+\\xbdF\\xc4\\xb3\\xbd\\xd9\\x92\\xfd;\\xe5\\x91\\xe5<\\xa6f\\x03\\xbd\\xad\\x98\\x88<\\xd3D\\xb9\\xbd\\x19J\\r\\xbct\\x0c0=-\\xa0\\x01<\\xb4\\x13\\x02<\\x84\\xb8\\x97<\\xce#\\xda;{BH\\xbc\\xa2\\x8b\\xaf\\xbc7\\xf0\\xd6\\xba\\x8bm\\xe7;\\xa4\\xc7\\xef\\xbb\\xe9\\x04\\x07\\xbbmR\\xcf\\xbc\\x14-\\x87\\xbd8\\xd3\\x8a<kM\\xb2\\xbckz\\xaa<\\x1ce\\xbe<\\xf2X\\x8a<\\xbf\\x9b\\x0b\\xbd\\x06e\\x95\\xbc$S\\xc6=&e\\\"<\\xee\\x19\\x18=l\\xb7\\x0b;\\x8b\\xda\\xb7<\\x0c\\xfa\\x19\\xbc\\xd6O\\x86=O\\xf2\\xe6\\xbbC\\x84\\x92\\xbc\\xd0\\x17;\\xbd\\x01m\\x14\\xbd\\x99\\x87\\xc3\\xbcw\\x93\\xaa\\xbc\\xf4\\xe5\\xa4<\\x89\\xa7\\xf8<\\x9e\\x84Y<E&P;\\xba\\xa3\\x05<\\xe8\\xf7u\\xbd\\x850\\xbc=\\xa7T\\xff\\xbc\\x80<r<7L\\xa1\\xbd\\xbc#\\x0f\\xbc\\xa3\\xff\\x03\\xbd\\x00F\\xc8\\xbc\\xd0\\x04\\x9e\\xbc\\x8a)K=z\\xc9+\\xbd\\xf1yo<l/\\x05=\\t\\x088\\xbc\\xdf\\n\\xa4\\xbc\\xcd\\xad\\xdc<\\xe3\\x9b\\xe4\\xbc\\xc97N\\xbd\\xbcfp\\xbd\\xef\\x88N=e?G=\\xf4\\x11\\xd7<\\x0bDY\\xbcl+\\x1f<\\xb4d\\xf4<\\x16\\xe1\\xc6\\xbcnC\\\"\\xbd\\xc4\\xe2\\xb7<\\xab`M=\\xdc\\xaf8\\xbd\\xe4\\x17\\\"=\\xe1d9\\xbd\\xad\\xd1h\\xbcG\\xc5L\\xbd\\x89\\nI:\\t\\xe6\\x8a<\\xeeq[\\xbd+\\x0e\\x87=9\\xf3\\xd9\\xbc\\xae\\xc1T\\xbco\\x07\\x97\\xbd\\xcb8\\xdf<;\\x83b< \\xa5\\x90\\xbc\\x98\\xfaX=r\\xd0&=hK!\\xbbZ\\x0e\\xa2\\xbc\\xc0\\xa5I=\\r[\\xbc:=\\xdd\\x00\\xbd\\xd7~\\xb3\\xbc\\xc7Wx:IjT\\xbd\\xe4qN=D\\x00\\x04=\\xecK\\xd6\\xbb\\xbc(\\xf6\\xbc\\x01\\xca\\x8f<kM\\xdf\\xbc\\\"\\xea\\n=\\xb3\\xc5\\x0b=`\\x07\\xd7\\xbcP\\xfdO<\\xb0z\\xc7\\xbc\\x1dH\\xb2<\\xc0\\x9e\\xe7\\xbc\\x8d\\x1e&\\xba\\x16*\\x9a<\\xe3.H\\xbdc\\x8a\\x04<\\xab,\\x00<\\xe5d\\x19=\\xd4>L\\xbd8OD\\xbd\\xc6\\x0f\\xf48\\x85\\x81\\x91=\\xf2~H\\xbc\\xfd\\xfd\\xaa\\xbd\\x89\\xbb\\xfd\\xbdr\\xf7-\\xbd\\xac\\x9f\\x8c\\xbc.\\xcf\\xa3<^\\x9a\\xa2\\xbc\\x94Is\\xbc\\xfb\\xc3)=\\xb8^1\\xbd\\xd9\\xf3\\r\\xbcg\\x14#\\xbc\\xfa\\xa0\\xdb<\\xffnY\\xbdl\\x00\\xb8\\xbb\\x9e;\\t\\xbd\\\"\\xbb\\xcd<|\\\"\\x91;,`\\xcb<\\xfa\\xa8Y\\xbd\\xa0\\xef\\x90<\\xa6v=\\xbc\\xa4\\x863\\xbd6p\\xa5<\\x17\\x7f\\x0b\\xbd\\x99\\xfd\\x12\\xbdMY\\xc2<N\\xf7\\x12\\xbc-\\xf8;=\\xcd7\\x96\\xbd\\xda{c\\xbdFA\\x19\\xbdkA\\x9c\\xbceI&=\\xfcP0<<_N\\xbc\\xe7\\xb1\\x85<\\xbf\\xfe\\xf2<\\x9c\\xe4\\xd4\\xbc\\xec\\xf4\\xd7:R\\x8b\\xf7\\xbb|\\xa6\\xc0;\\xcbv =F\\xc5\\x98;\\x0c\\xda$<\\xad\\xdd =Y~\\x12=Z%\\x05=\\x1dg\\x07\\xbd\\xca\\xf8`=\\x11p\\xa1<_\\xe8\\x9e\\xbd\\xd4\\x08\\x13<0\\x03\\x9d=P\\x941\\xbd)U\\x94=\\x0b\\xda\\x04\\xbd\\x0b\\xd8e\\xbc6\\xba\\xbe\\xbag:\\x89=&\\xc2\\x11:8\\xd5J\\xbcyq4\\xbd\\x0b\\x1e\\xcd\\xb9\\xa3B}\\xbc\\xa6\\x11\\xe8=i\\\"\\t\\xbd\\x08\\x97\\xe0\\xbc.\\xc5%=ep\\xcf\\xbb\\xa8s%<^\\x1d\\xd3\\xbb\\xb6-F\\xbd\\x04\\xb9L<}\\xf2\\x8a\\xbc?\\n\\x9c\\xbc\\xef\\x18?\\xbc\\xe1.$\\xbcl\\x95;=4^\\x96\\xbd\\x06\\xcf\\xc2\\xbc\\x83\\x0c\\xba<1lK\\xbc\\x0cT\\x97<\\xf2-\\x17=\\x1b*\\x05\\xbc\\x8f\\x18\\xe6\\xbch\\x07 =I\\x13\\xa0\\xbd\\xb6\\x04\\xce\\xb9Q\\xb6\\xa9<x1\\xf9\\xbc\\xf3!\\xe7=5\\xe5\\x90<`\\xba\\\\\\xbcj<J\\xbb;\\xc3\\x15=i\\x8e9;M{;=9K\\\"\\xbdkiN=\\xb6\\x02K\\xbcH\\xf07=\\xfbA\\xea\\xbc\\xd0\\xd6\\xd3<P\\x82\\x16=2\\xf4\\xa1\\xbc\\xc0;\\xdd<\\xfcxd=\\x07\\xdd\\x9b<\\x03\\x87I\\xbds\\xc2\\xbf\\xbcV\\xe5\\xb1\\xbc\\xcc\\xe2%\\xbdv\\xb0\\x8e<*fK\\xbd\\xda\\x84\\x05\\xbc\\x00rT=\\x97;\\x1d=\\xcd\\xad\\x04\\xbc@\\xae\\x10\\xbd\\xd1C\\x99\\xbd=!\\xee<\\xc9[\\r\\xbd\\xdc\\xb9\\xf0<\\x08\\xfb\\x1e\\xbd\\xbc\\x91\\x98;9)\\x9c\\xbdoL%=\\xb0\\xaf\\xb1\\xbc\\x03>Z<\\x1a\\x95\\x9c\\xbd\\xae\\x13\\xb5\\xbc\\xa1\\xcb8\\xbc\\xcc\\xd30=9\\xa6*\\xbd\\x88##\\xbd-\\xa6\\xad<\\xfeS\\x0e=\\x1d\\xd2Z\\xbc%d\\r<\\xa8\\x8a\\x01\\xbdJ\\xe6\\xca=SPg<\\xad\\xdd\\x1c;5<:=\\x1e\\x80/=\\x90\\x9d;<\\xcc\\xdd\\r=\\x9a[\\x9c\\xbc\\xae\\x1d\\x03\\xbc\\xddp\\xe2\\xbc\\xee\\xff0=\\xb9\\x14\\xfa\\xbc\\xdem\\x81\\xbd\\xcd\\x13\\xd3\\xbc\\x01\\xd6\\xc8<\\xb80\\xff\\xbc\\xeei.\\xbdK^\\xe6\\xbd\\x14\\x1e\\x93\\xbc\\xe8\\xb2:=I[\\x90\\xbc\\xda\\x10\\x87=Y\\x1b!=\\xad\\x1d\\x1d<k|Z<\\xe2\\xda\\xdd<\\x05\\x020\\xbd\\xe5=[\\xbd\\xde\\xe4)=\\xd9\\x87\\x8e:%2\\xed\\xbb2T5\\xbd~L\\x81;^\\xfb;=\\x90\\xef\\xb9\\xbc\\x1b\\x0f`\\xbd\\xbe\\xf1\\xe2\\xbc^\\xffK<\\x16r \\xbd\\x03\\x87\\x0f\\xbd\\x14\\xd4\\xf4\\xbc\\x87\\xb6\\xeb\\xbb\\xd2\\xfe\\xaa\\xbd\\x18\\xa0\\xf9\\xb9\\x1b\\x85\\xa8\\xbc\\xf2&\\xc9\\xbc\\x11\\xb6\\xf6<\\xebW\\xed\\xbc\\x86[\\x84\\xbd\\xc0\\xf1\\xcc<J)\\xa9=\\xd0z\\x04\\xbd\\r\\xfe\\n< Z\\xa4<h-\\xa7\\xbd\\xfd\\xe5\\x19<~[w=\\xa9\\xe9\\xbc\\xbd\\x06\\xe2\\x9c:\\x89!\\x83\\xbc\\xc20;=\\x9b\\xa8R=);J<4\\xb6q\\xbc\\xc3%Y=<\\x01\\t\\xbc\\xe6\\x84\\xdc\\xbc\\xbcxV=\\xd0\\x06B=\\x9c\\x90$=\\x84<&=\\x865\\xb8:\\x94\\xb4\\x1d=O\\xed\\x14<\\xe7\\xb5\\xd4\\xbcS\\t\\x98\\xbd\\xaf\\xd3\\x8a<\\xd1\\x97u<\\x17\\x93w;T\\xe5\\x93\\xbc\\x8ci\\x05<\\x95\\xb1\\x17=\\xc9J{\\xbb$\\xcc\\x0f\\xbd\\x11_;<\\x9f\\xfc\\x97=6\\x10\\x85<\\xce\\xc3\\xf4\\xbco+\\xa1\\xba\\xa6\\x84\\xa4\\xbc\\x9c/l\\xbc\\xbaC\\xa7\\xbc\\x01\\xf5\\x89\\xbd\\x91f\\x8e=\\x8d\\x08H=\\xa4\\x12\\xd6<\\xda\\xd6Y;\\xd8\\xfaX=\\xe3\\x05\\xab<\\xa6\\xc3\\xe7\\xbc\\xa4\\xf3\\xf3;R\\xad\\xfa\\xbc\\x92\\xc1E\\xbcipe<M^\\xd0\\xbc\\xb1\\x05\\x15\\xbd\\x06\\x07\\x9d;g6\\x00=\\x94\\x03K\\xbd\\xd6;\\x86\\xbd\\xe3\\xb1\\x05\\xbc\\x91\\r\\xa4\\xbdg\\x9f3:\\n\\xcc\\x14\\xba\\nL\\x88<B\\n\\x9c\\xbb\\xc49\\x8d\\xbc:5\\x1f=o\\x94\\x83=\\x7f(\\x03\\xbc\\xd1\\x96M=\\x81\\x07y\\xbc\\xbd\\x98l<{)X=\\xae\\xec\\n\\xbah\\xfe\\n\\xbd\\xe2\\xb35\\xbc\\x1c\\\"\\\\=Y\\x08\\x8d\\xbc\\x1e\\xea_;\\xb0\\xa20\\xbcQ(\\x96\\xbb6\\xe4h<\\xdf\\xc9\\xbc<\\x84m\\r\\xbd\\x99|g\\xbc>\\xbb\\x04=4\\xf8\\xa6\\xbc\\xe3\\xcc@<\\xd0\\x92\\xb2<\\xb12\\x1d\\xbcJ\\xf2\\x81<\\xedy\\xbf\\xbc\\xa8\\\"\\xe1\\xbdp\\xe5\\xb7\\xbb\\xfb>\\xb6\\xbck\\x15\\xeb\\xbcQ\\x1c+=\\xab\\xf4\\x99\\xbb\\xad\\x11\\x89<\\x1e8\\xac=\\xf3\\x17\\xf5\\xbbz\\x17T\\tv/p\\xbc\\x16KF\\xbcW\\xec-=C\\n\\x87=\\xab\\xa3Y<\\x848\\xe9<\\xe8\\x97\\x07\\xbdt\\\\\\xd6\\xbc\\xd6\\xe7!\\xbc\\\"^\\x18\\xbd\\x0c\\x95\\xbb<]h\\x00=\\r3\\xf6\\xbaH\\x9fV=\\xe9L\\x0e\\xbcK\\xaf\\x8d<j\\xfc\\x1d\\xbd\\xe8\\xed\\x8e\\xbc\\xce\\xf5 \\xbc\\xa4?\\x03=\\x9a\\xac\\xa3<\\xa1~\\\\\\xbb>\\xf1\\xfc\\xba]F\\x1a\\xbd\\xa8\\xbd\\xda<\\x18\\x8d\\x91<:\\xa4\\xa8<563<iKn\\xbc\\xf9<\\x81<\\xdb\\xd3g<\\xf3=e\\xbd\\xf3\\x1d\\x07\\xbd2\\xc5\\x90\\xbd\\xec\\xd29\\xbdF\\x97!\\xbc)`\\xd2<\\x89\\x1e\\xf1<\\x98\\x18\\x07=\\xa6\\xe0\\x89<>y\\x95;U\\xfd\\x17<\\xf0l\\x84<,\\xf0K=<\\xcc\\x04=Bh\\x06=E\\x9e\\xae=y{\\x13\\xbc\\xb1\\x85\\x84\\xbc\\x8dK>\\xbc\\xcb!\\xe1\\xbc\\x9d\\x8c\\x04\\xbd2\\xcc\\x8d<\\x04\\x05\\x84;[\\x1b>;8\\xaf\\xf9<&\\xf6\\xc5<\\x9e\\xa0\\x18< @\\\\<x\\xa1 =,\\xbc\\t<Uw}<\\x1bX\\x9e;\\\"\\n?=Y\\xdd\\x95\\xbd\\x1ay/\\xbd\\xd3\\x8c<\\xbds\\xa6\\xab:\\xf8\\xbeQ=\\xae\\xaa\\xc9\\xbcET!=\\x9b\\xb6\\xca:\\xe4\\xe8(\\xba\\x12M\\xe8<\\x0f\\x08\\\"=t;O\\xbc\\xf7\\x1b\\xed\\xbb{\\xf4\\x9d=\\x14\\x1d\\x9e\\xbd_\\x8aa=\\xc7\\xc9\\xca\\xbc\\xe3Z\\x8a\\xbb^\\\"y<\\xd4\\xcc\\xeb<\\x9b\\x17\\xec\\xbc\\x95\\x9f\\xe2<\\x1e\\xffm=\\xba,\\xa2=\\xc0\\x94Y\\xbc\\\"\\xb8\\x17;\\xc9\\x97\\xbe\\xbb\\xcaz\\xb2\\xbb\\x19{\\xe7<n\\\"\\x869/\\xd6&=\\xa9\\xce\\x1f=Pk\\x1a\\xbdig\\xbd<}\\x93\\xa6:yj1=\\xa1\\xe7W\\xbd\\x81\\xeeC\\xbdb(\\x83<\\xd9Q\\xc2\\xbc:\\xf8\\x07=\\x08L,\\xbd\\xa6Vi=\\xd9J\\xdf\\xbb\\xe6\\x8a\\x02\\xbd\\x03`\\x92<|v\\xb4<\\xf7\\xf3\\xb7\\xbc4\\xe4\\x02\\xbdc\\\\r=\\xf4\\xb5\\x0b=\\xe3\\xef+<`\\x10\\x8c=\\x1f\\xf0\\x19<\\x10\\xa7\\xe9\\xbc9D\\t\\xbd\\x80U\\x08=\\x9a\\x94\\xaa:\\x07\\x17\\xb2<\\x9fU\\xff\\xba.4\\xde<\\xcd\\xba\\x8d\\xbc*\\x9e3\\xbd\\x10\\x9e\\xa8<\\xa2D\\x87=*\\xef\\x84\\xbd\\xbf\\xc4=\\xbaS\\xd0\\x1d\\xbdO\\xdfp=A\\x0f\\x1b\\xbc\\x08\\xc9\\x9c=\\xcb6\\x8d<x\\xb4I=\\xf9iW\\xbdI\\x8b\\xf0\\xb9T\\x11\\xcb\\xba}~/=\\xde\\xee\\x1a\\xbd\\xd7\\xc4\\xed\\xbcrS\\x0c<;8C<\\x88;\\xd4<\\x86sL\\xbb\\xba$6\\xbcoP\\xf0<\\xddp\\xf7\\xbbS\\xcc\\xa1\\xbcEI\\x13\\xbc\\xc5\\xeb\\xa3\\xba\\xcd\\xa2\\x95<?5#<\\xb1\\x81\\xc7<\\xcb\\xf4\\\"\\xbd\\x82XV=\\x10\\xb6\\xa1<d\\xb4Z\\xb74a\\x0c\\xbd\\x93n\\xa9=\\xbd\\xc4P;,\\x8e\\xc9;\\x82|\\x97\\xbb\\\\m\\x01\\xbc\\x13\\x00K\\xbd\\x81o\\xbc\\xbb\\xbd\\x95\\xcc=^\\xae \\xbc\\x1a*\\t<N*b\\xbc1\\x1c5=c\\xe2\\xc4\\xbb\\xf0\\xf68\\xbb\\x91U\\x87=\\\"\\xe2\\x8a\\xban\\x08z\\xbc\\xa5U\\xa6<\\xc3yr\\xbc\\xd7\\xa1\\xa3\\xbcoj\\x8b\\xbd<r#<\\x95\\xa7\\xe4\\xbc,\\xce\\xc6\\xbbiC^;\\xcb\\xb9\\x15<\\x1caC<&\\x0e\\\"=.%\\x14\\xbdak\\xb5<g\\x13\\xed<0\\xca\\x92\\xbcV\\xf8\\x10\\xbc\\xed\\xc4\\xc5\\xbcy\\x7f\\xbc\\xbc\\xd4M\\x96\\xbc\\xc9\\xd9\\x83:\\x0c\\x91\\xa4\\xbc\\x0e\\xd4\\x14:\\x89\\x98d\\xbd3H{<2\\xac\\xbf\\xbb\\x84$\\x82\\xbd~>\\x12\\xbdff\\x0f\\xbd\\x845I<m\\x0b\\x8a\\xbc\\x0e\\xf71\\xbc)X\\x11\\xbd\\xe2HU<\\x96\\xe9\\xaa\\xbc\\x1f\\x06-=w)O<\\xbd\\x02(\\xbdI\\xd3\\xa4\\xbb\\xaa\\xaf\\xe0<`\\xcf\\x1b\\xbc\\xfd\\x81q\\xbd\\x96\\x83G\\xbd\\xcdE <\\x84\\x1d\\t\\xbd\\\"\\x07C=\\xd5\\x16\\xa3=\\xa7\\xf5\\x91=OJ#\\xbc\\x0b8J<6\\x8d5\\xbc)o?\\xbd\\xdd\\x16H=\\xff\\xb9\\x84<<5\\x00=\\xb3\\xb4M\\xbbP:\\xec\\xbb\\xa4\\x91\\xe2\\xbc\\xf3\\xbc\\x0c\\xbc\\x80\\xce\\xc3<\\\"O\\\\\\xbb6\\xe0\\x00=]\\\"/=\\xc4/\\xb3<d\\xbc\\xd6<\\x18u\\x13=\\xf7\\xe8\\x96\\xbb\\xed\\x9d\\\"\\xbd\\xb3-|;\\xc6\\xfa\\r\\xbd\\xb1\\xac\\xbe\\xbb\\x0b\\xac\\x83\\xbd\\xb7E\\x1d<\\x02\\xae?\\xbc\\x0b\\x15\\xda\\xbc\\xde\\xcd\\xa7\\xbc\\xd7\\xa4\\x8a=+\\x1f\\xfc\\xbc\\x01~\\xd3<\\xea@}\\xbd\\xd8\\xab\\xff\\xbcn\\xb1\\xb6\\xbc\\xc5w\\\\\\xbd\\xdc\\x13\\xa3;g\\xf1\\xa9:q\\x96\\xde<\\xd6iz=\\x8dp\\x96=f\\x83!\\xbc\\xcd\\x05\\x00=\\xbb\\x8e\\xab;\\xb1\\xc4A\\xbd\\xea!!=\\xb8\\xb7F\\xbd\\xb8\\xfe\\x04=J\\xca\\xab\\xbc\"\nHSET bikes:10055  model 'Millenium-falcon' brand 'Bicyk' price 2253 type 'Enduro bikes' material 'aluminium' weight 10.0 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"}0\\x08<\\n\\x98&<d\\x1e%\\xbc\\xf0\\xdaB<F\\xf8Z\\xbc\\xa9\\xcf(=j/K\\xbc\\x9d\\xd3q\\xbdL\\xe5\\x88=ye\\xd4<o\\x1e\\x8a\\xbb\\x11\\x88\\x02<v\\xd3E=\\x10l\\x1f=\\xe2\\x07\\x1f=$\\x85W\\xbd\\xb2\\xb6\\x1b=\\xc6\\xd65\\xbdB\\x94\\xf9<\\xeb\\xff\\x19\\xbd\\xe0\\xfc\\x83<\\xd0A6\\xbd%u!\\xbd:\\xb0\\x82\\xbd\\x8a\\xa9&\\xbdI\\x9c\\x85<\\xc7w!=\\xa8\\x01\\x0f;r\\xedn\\xbd!\\\"\\xa6=s\\t\\xcf\\xbc\\x0eI@;a\\xf8\\xe8<\\xc6\\xf8:=@b\\x13\\xbdrK\\x13\\xbcR\\xcd\\xb0\\xba.\\xebP\\xbdP\\xb4\\xdc\\xbcwD\\xf1<\\x8b\\xb2\\x88=\\xd3\\x88\\xa4\\xbc7\\xf6\\xea<\\xfei :\\xc2\\xbb\\x90;\\x04\\x8cL\\xbd@bC=\\x0bJ!\\xbd\\xc7\\x91\\x02\\xbd\\x96{\\x8b<!\\xcf\\xc8<\\xbb\\xd6\\xc1\\xbc\\xe2\\x10\\xb7\\xbc|\\x0c\\xda;\\xc9\\x05\\xa7\\xbb\\xf2g\\xe1\\xbc\\n\\x11 \\xbc\\x9c#5\\xbd\\xc5B.\\xbd\\xa1O\\x9e=\\xe1|\\xba<H\\xe5y=\\xfd\\xbb\\xd6\\xbb\\x10\\xc9D=\\xe5\\x197=]}B<$\\xae\\xad;!\\xb0\\xa8<\\x98\\t\\xbe<k\\x19\\xba\\xbcj%\\x85\\xbb]\\xa3\\x90\\xbcO\\x1d\\xb7\\xbcs\\x0e\\xc8\\xbc\\x1c\\\"~\\xbc\\xa8\\x9c\\xd9\\xbb\\xe9e\\xb4:\\xdc-E\\xbd\\x9e}\\xb6\\xbd&\\xc8\\x89<E0\\xe1<\\x0e=\\xfc\\xbc\\x89T;\\xbc\\xdfGC\\xbd1\\x90\\x9b\\xbc\\x00\\x19j\\xbdfA/<\\xa0\\xcd\\x19<\\xea\\xbd2=\\xc9\\xedM\\xbc\\x12\\x12(\\xbd\\xd7\\x15\\\"\\xbc\\x01l\\x86\\xbc9\\xac<=\\xfb\\xa7\\xa0<\\xb6\\x04\\xca<\\xdc\\x8f6\\xbd\\xe4\\x00\\xab<*\\x02\\x00;i$\\xe7\\xbc\\x861v\\xbc7=\\x8b<\\x8b\\xff8<\\xbd9U\\xbd\\\\5\\xfc\\xbbJ\\xb9\\xa6=\\xdd\\x0f\\xec<\\xf2\\x93\\xea<\\x7f\\xc3\\xd6\\xbceh\\x1a<\\xe9\\x10.\\xbd\\x8dDj=\\xc2\\x123=\\xf5w$\\xbd\\r\\x9fX\\xbdT\\xfe\\x9a\\xbbn\\x84\\x10<\\xa2\\xc4U9\\xce\\xc4\\x08=7\\x94\\x92<\\xc7\\xfe\\x15<\\xbf/i<&\\xca\\x0c\\xbd\\xf0\\x8a\\xb8\\xbb\\x9c\\xc7\\x1d\\xbbT~\\xfc\\xbb\\xbc$\\x1f<`s-\\xbd\\xcf`B\\xbd&\\x02=\\xbd\\xfa\\xa7\\x13\\xbc8n\\xa1\\xbb\\x19y\\xb9<\\xd2\\x00\\n:y\\x8c\\xd2;:\\x8a\\x14=\\xcf\\xd4\\x13\\xbd[\\x1d\\x0f\\xbc\\xc3\\xe0\\xb2\\xbb\\xcbk\\x14\\xbdR/\\x84\\xbd\\xeb\\x91X\\xbd6\\xe0&\\xbc7\\xcf4=\\x0f\\x93\\xb6;[\\xb6\\xc7<v\\t\\xd9\\xbb]o\\x14\\xbc\\xc1ck;\\xbd\\x88\\x8d\\xbdLw\\\"\\xbcc\\xcct=\\x1dq\\xbc\\xbcd\\x05\\xb1<\\xa4.\\xae\\xbc\\xeb\\x97#\\xbck\\x8c\\xff\\xbc\\x02\\x89\\xdd\\xbc\\x17\\n\\x88<g\\x98:\\xbc\\xcf\\xc5\\x91=9\\xd2j\\xbcKgM\\xbc\\xfctg\\xbdJ\\xe2l\\xbcz\\\"E<\\xe4\\xed\\x99\\xbcp-==\\xc9a\\xd3<)\\xa7\\xad\\xbc~v\\x86\\xbc\\xf1A\\x83=\\xeei\\x1f\\xbc*\\\"?\\xbcs\\xccl:\\xec\\x02K<\\xf8t\\x1a\\xbd\\x87;w=\\xc3\\x05\\x93:\\xf7\\xac]:/\\xff\\x10\\xbc\\xff\\xec\\xbe<\\xbb\\x96\\x0f=\\xe9B3=\\xbf\\x1d>=\\xd8G\\\\\\xbdP\\x06\\xfd<\\x0f\\x88\\x1d=[\\xee/<\\x00\\\\4\\xbd]\\xe0\\x12\\xbbP\\x9b7:ue\\xa8\\xbc\\x80\\x12\\xec<p\\xf6\\x97;\\xe8Z\\xc2\\xbc\\xa9\\xfd\\xc7\\xbc\\xcd34\\xbd~T\\xf7\\xbc\\xb3\\xf3U=\\xe5\\xdd\\x1d\\xbd\\x85%w\\xbde\\xa0\\xb8\\xbd\\x14nN\\xbd\\x9c\\x99\\x12\\xbd\\xcb\\xca4;\\x1ffA\\xbcC\\x8b\\xbd\\xbc;\\x10\\x05=\\x8c\\xa7P\\xbd-\\x904\\xbb\\x80<\\x15\\xbb\\xc2b\\xad\\xbb\\xf1\\xad\\xf2\\xbc\\xf3n\\xf1<\\x05\\xe7q\\xbc\\xcd\\x94\\x03\\xbd\\x94RN\\xbd\\x1c\\xf7e=)\\x85\\xbf\\xbd\\xaff\\x07=\\xdbD\\x83\\xbc\\xd06D\\xbd\\xc0\\r^;\\r\\x94\\xaa;\\xe0\\r\\xc0\\xbb\\x83\\xd4<=aQ\\xc4<$c\\x83=y\\xf3.\\xbd\\xc0E\\x01\\xbd[\\xd2\\x82<\\xee\\x12D\\xbc\\xccA\\x10=.nf\\xbc2\\xcd]\\xbc\\xd9\\x02O<\\x00\\xe7S<\\x9aw\\x00\\xbc\\xf5YR<ak\\x10=\\xb46\\x9c\\xbb\\xb0q2=0\\xe3\\xd0<\\x83\\x01C;f\\xc5\\t=\\xfbR\\xa7\\xb9\\x87\\x0b\\x88;\\xf6Jz\\xbd\\x07\\xbb\\xac;$\\xdd\\xbf\\xbck4\\x95\\xbdP\\x91S<\\xff\\x9d\\x99=\\x19\\xf9]\\xbc\\x8b\\xc7t=\\xf3m\\x12\\xbc\\xf9\\x18\\x86<\\xd9\\x12\\x13\\xbd\\xbe\\xc9\\xa7=\\xf7\\x94\\x16<z\\xd6f\\xbb\\xab\\xb2a<\\xb1(%\\xbc@re\\xbd\\xaa\\xc5\\xb7=\\x02TC:]\\xda\\xf7\\xbb \\x98\\xb3<\\\"\\xea\\xc5:\\x1c\\x1bG<\\x1al\\xd5<Zbj\\xbd\\xbf\\xcd\\xd4\\xbbQ\\x9f\\x10\\xbd\\x05=\\x03<\\xe3\\x02L<\\xbb\\xea?\\xbc\\xc5\\xe4\\xc0<}9\\xc3\\xbd\\xddEi;w\\xdd\\x02=\\xf7\\xa4\\xe2\\xbcU\\x80\\xed\\xb9\\x9b\\x8f\\xdf<\\xce\\x92\\xcd<9\\xf8\\xb5\\xbc\\xbf\\x03s=\\xb4\\xd8\\xa0\\xbd\\x1e*\\xbb<$\\xb2A<\\xaex\\x94\\xbb\\xbc\\xff\\x14=X\\xbaa<\\xa6ST\\xbb\\xf0)B\\xbd\\xfd\\xe7\\x99;\\xddg\\x8a<\\xc5\\x07\\xcf<\\xb2#\\xf8\\xbc\\xf5\\xb3.<\\xa5\\xa2,<,\\xf0F=i\\xb0U\\xbb\\xee\\xf2#=\\x9fa\\x0b=M\\x14.<v\\xda7<\\xe7p\\x8d=\\xdcE\\xf2\\xbb\\xae\\xeaE\\xbd\\x99\\xea\\x1e\\xbd&\\x1bp\\xbb\\xf8\\xeb\\\"\\xbc\\x87i\\x07=\\xb7\\xae\\xed\\xbcp\\xa4=\\xbc\\xa9\\x1e\\xc5=\\xbb?#=8\\xcd!\\xbc_?/\\xbd}\\x9b\\xfd\\xbc\\xfc\\\\\\x0e=\\r\\xfe\\xef7\\x01\\x1c\\x14\\xbc\\xafY\\x08\\xbd623=\\xef\\xc2\\xac\\xbdB9\\xa6<\\x18\\n\\xf5\\xbc\\x14\\x9eZ;k\\nF\\xbd\\xc8\\x0f\\x90\\xbc\\xb3I\\x03<~\\x81\\x9d<\\xf3\\xd2i<\\x12u\\x92\\xbd\\xf6.K=\\xb3\\xbb\\x1e=\\xcf\\xce\\xa9\\xbc\\xddR#\\xbd\\x0cK\\x15\\xbd&Y\\\"=\\xee4\\xbb;\\xb3\\xe6\\xf9\\xbb\\xc1\\n\\xed<\\xd3\\x18\\x01=\\x1b\\xd3\\\"\\xbb\\x80\\xa2\\x83=\\x0f\\r\\x89<\\xd5{\\x8a<R\\x13\\x86\\xbcr\\xb2\\xa0<\\xd5\\x93\\x96<\\x02\\x1a\\x1f\\xbd\\xca+$=\\xc1zJ\\xbc\\xee\\xc7\\xe0\\xbcwX\\x15\\xbd:\\xe4\\x9a\\xbd\\x0f\\xeaz\\xbci\\x99\\xff<Fx\\x80\\xbc\\xfe\\xe1m=\\x004\\x87<1\\xb9\\xc9;<(\\xb3\\xbc\\xc5\\x92\\xcc<4\\x0b\\xcd\\xbc\\x7f\\x91{\\xbc\\xb0zx=\\xaa\\x81\\xe1\\xba\\xec3\\xc4\\xba\\x9a\\x9b\\xac\\xbc\\xe4\\xc1\\x85<f\\x94\\xf4<\\\"\\xb3\\r\\xbd\\xe1\\r\\x81\\xbdd\\x1a\\x0e\\xbd\\x0b\\x9b\\xfe\\xbc\\xfd27\\xbd\\xa8Q\\x18\\xbd\\xeb2[\\xbd\\x06s\\xe1:U\\x13\\x94\\xbd\\xe19\\x18\\xbc\\xf2\\xcf\\xcb<\\x8f\\x15\\xa6<\\xe5\\xcbL=p\\x199\\xbdrS\\x82\\xbd\\x07m)=n\\x1f\\xcf=\\xa4\\x1b\\xdb\\xbc@\\xb2\\x94<\\xb5\\xb4}<\\xb3\\x03\\x89\\xbde\\x89M\\xbdZ\\x8f\\x91=+Q\\x86\\xbdv\\xd0c<\\xdc\\xa8\\r\\xbd\\xf5\\x8dA=f\\xa6\\xa6=\\xcb|,<\\xf2\\xacs\\xbdP$K=P\\x945;\\xe5:/\\xbcA\\x89\\x87<q\\x99o=\\xf7U\\xe5<O<\\xa2\\xbce\\x8b\\x06<ab\\xe0<\\xc1\\xf7\\x92<v\\xc87;\\xbc5$\\xbd\\xa0\\xbe\\xd8<\\xc8X\\x13=\\x95\\xdb\\x1a=\\x8c\\xe1\\xc3\\xbc\\x83/<=\\x88\\xbe\\x81=)\\x8c\\xe4;\\xd9\\xf7\\x13\\xbb\\\"/\\x85\\xbd\\xc6=\\x82=L\\x9f\\n\\xba\\xaf\\xae\\xb2\\xbc\\x08\\xfb+<\\x12\\x1cS\\xbc\\xbf\\xb9\\xf6\\xbc\\x80\\xe3\\x96\\xbd\\xd8\\xc1\\xa1\\xbc\\xb3J\\x90=\\xf7\\xda\\n=\\x95\\x1d\\xe3<x@\\xd7\\xbc\\xa8jz;\\nDt\\xbd\\x07\\x8e\\x80\\xbb\\xfeF\\xe3;H`\\\"\\xbc\\xbf\\xad\\x04=\\xf9\\xea\\xcb\\xbb\\x90;J\\xbc\\xf8\\xd5\\x99;\\x99\\x1c\\x19\\xbd\\xb5\\xea<=\\xe9\\xcd!\\xbd\\x04\\x00K\\xbd\\xd0G\\xe4<#\\xd3\\xc3\\xbd\\x16\\x8d\\xda<\\xee\\xc9U\\xbb\\x08L\\x01\\xbd\\xe8\\x1cC<t\\x05\\xe2\\xbc\\x03E\\x93=\\\\6\\x88=\\xcb9\\xd7<\\xf94\\x8f=t\\xe04\\xbd$\\xf7\\x17<Zk/=\\x9a\\x84 ;K\\x04K\\xbd_\\x00\\x08=\\x00\\xe4\\n=+\\xa2\\xa8\\xbag\\x90l\\xbb\\xe5S\\xb49#,[<}r\\x9d9\\xf2\\x94\\x1b=j\\xc5\\x17\\xbd\\xcd=\\r\\xbd\\xbc\\x84\\xbe:;\\x9d\\xad;\\xf8\\xc4\\x8c:!\\xb0\\x85\\xbc\\xd4\\xf3\\x81\\xbcd\\xa6\\x8d<\\xec\\xee\\x86\\xbb|s\\xd4\\xbd^\\xc3\\xa2\\xbc&\\xbey\\xbc\\xc3\\xb3\\x0f\\xbcr\\x9d\\xf4<=\\x04@\\xbd\\xf1B\\xfa<=\\xd3S=@\\x91I;\\n\\x9bQ\\t\\x98\\xa0\\\";\\xfc\\x90`\\xbc\\xfb\\xad\\x03=\\x86\\xceB<\\xe1%\\x8b<rKe=\\x97\\xe3\\xa3\\xbc\\xf3\\xb5\\x8d;\\x1az\\xa1<mZ\\xab\\xbc\\xc5\\xf6\\\"=X\\x0eE=\\x87\\xd4{\\xbc\\xfd\\xe81=\\x1eP\\xc8\\xbc\\xa4\\xe0\\xb9;\\xfd\\xb7p\\xbd\\xe1\\xf1\\xe5\\xbb\\n\\x82\\x9b<\\x1f \\x8c=3\\x04\\xc8:\\xd7\\xde\\x01\\xbc\\xbeP\\xd6\\xbb\\\\2<:\\xb1\\x1e`<\\x11\\xb0#8\\x9a\\x91}<\\xb4[\\x86<\\xe6I\\x02\\xbdb\\xac|;\\x9bD\\x99<j6\\x07\\xbc\\xb9\\xcf[\\xbdM`\\xa8\\xbd\\x1f\\x7f\\xe8\\xbc\\xb4l\\x87<\\xd2W\\x12=e\\xb7/=\\xa0\\xcd\\\\=\\xb2\\x14Z;\\xf9O\\xa7<\\xe7\\r\\x8a<\\xd0\\x9de<2\\x12<<;\\xe4l=\\x13\\x94\\x86<\\xce[\\xec=\\xf3\\x06\\xab\\xba\\xdc\\xf5\\x14\\xbd~\\x9e\\xcf\\xbb\\xef>~\\xbd\\xfd\\xd6K\\xbd\\xa8\\x1b\\xdf<\\xe5\\xbbD=\\xf3(\\x9d;Y\\xe5\\xf1<\\x8a\\xb9T<\\x7f\\xad\\x14={\\x17q<\\x93\\xb4\\x04=9S \\xbbAe\\x08\\xbcY\\xcf\\xa6<\\xba\\xcf(=\\xf9\\x86\\xa9\\xbc\\xccT3\\xbd\\x13v\\x02<\\xfb\\xc0o<\\\"\\x96\\x0f=(\\xe3l\\xbc\\x1fs\\x8b\\xba\\x89\\x18\\xb0\\xbc\\x05L\\xf9\\xbbOv\\xc4<T\\xc8\\xf7<\\x94>M\\xbd1\\xac\\x81\\xbcg\\xde\\x03=\\xc8\\n/\\xbd\\xde\\xd2\\xd1<\\x9e\\xd1\\\\\\xbd\\x1d\\xccJ\\xbcX\\xae\\x0c\\xbd9/e=k\\xed\\\"\\xbd\\xf2\\xe7\\x1f\\xbc\\r\\xfb^=_1b=\\xad\\xd6K:\\xacg\\x03=\\xa5H\\x95\\xbc\\xa8:D<=h\\xa3=V\\xf9\\xc6\\xb9\\x04kn<\\x92\\\"X=7\\xd4\\xd9\\xbc\\x1b\\x00\\x03=\\xa5\\x9e\\x10\\xbc\\xec\\xdb\\xf2<4n\\x10\\xbd\\x977.\\xbc\\x7f\\xd8\\xa4\\xbb\\xc7\\xba\\xd4\\xbc\\xaf\\xe3{<}f\\xe9\\xbc\\x1b/\\xba\\xbb\\x84$O<\\xe0\\x96c\\xbdg\\xe5\\x06\\xbd\\xf9\\xb3@=pdw\\xbd\\x86R\\xa1\\xbcT{8==\\xc7\\xb4=^\\xb0\\x06\\xbbP\\xe6\\xdb:\\xe5\\x95\\xa6\\xbb\\x02(^\\xbb\\x0c\\x03\\x1e\\xbd$\\x1a\\xd1<\\xac\\xfa\\xea<\\xd7\\xce2=\\xfeK\\xcc\\xbb~<?=\\xad)\\x0b=K\\x9aj\\xbdu\\x81\\xb2<bF\\x8d=\\xfd\\xb8\\x97\\xbd\\x1e$\\x10;w\\x1cP\\xbdc\\x08\\x8e=+\\xae\\x05;\\xe8\\xb4\\xb2=\\xe2\\xc7Q;%(\\x9c=H%C\\xbdt\\x963<\\x1f\\xd7\\xab:[w\\xf9<:\\xa9\\xd1\\xbc&\\x87\\xed\\xbcaY\\xbb;\\xa9\\x99^<\\xe5\\xa7\\xc8\\xbc\\xaf8\\\\<\\x81\\xa8\\x0e\\xbc\\xad\\xe4E=\\x1c\\xa1\\xc9<\\xb7T\\x10\\xbd\\xfb\\x13\\xa0<\\xed\\xe2\\x03\\xbd\\x96\\xa5\\xfb<H\\xe8:=\\xa5x\\xbd<gZ\\x9a\\xbd-d\\x13\\xbd\\xf53F<bW\\xc6\\xbb\\xb4\\x17\\xa6\\xbc\\xa6B4=kk\\xdd<;\\xa1&\\xbd\\xc7\\xc9\\x1c\\xbd\\x94\\xd1\\xb9\\xbc\\xdf\\\\K\\xbd\\x96#\\x0b<\\x02\\x1c\\xa7=\\x1d\\xdb\\xca\\xbc!\\x8c\\xab<\\t\\\"\\xae\\xbcd\\x13\\xb9\\xbb5\\xf6\\xcf\\xbc8\\x05/\\xbc\\x9b\\xe1\\x85=E\\xbc\\x1f=\\xb1CA\\xbc\\xb4\\xee,<+bM\\xbb8M\\xcf\\xbc\\xba16\\xbd\\xc6\\xf3\\xd8<\\x14\\x90\\xf6\\xbb0~\\xbb\\xbb\\xf3\\xde6\\xbdy\\xf3\\x84;\\xec+\\xc8\\xbcsa\\xa9<\\xfe\\xb8\\x03\\xbc\\xb4$\\x18\\xbdXE\\x1d=$R$\\xbdE{\\x05\\xbd}M$\\xbd\\xf4x\\x85\\xbd;\\x98\\xab\\xbc\\\"\\xc0\\x1f\\xbdW\\x9f\\x19\\xbd\\x90\\xcc\\x1b\\xbc\\x0ffp\\xbbF\\xe8\\xaf\\xbb\\x8d\\xffA\\xbd!\\xa3\\x8c\\xbd~\\xe3\\xaf\\xbc\\x93jI\\xbd\\x90\\xe5\\r<\\xb4\\xfe\\x03=\\xf3i\\n\\xbd\\x7f~\\x82\\xbd\\xa8k\\xa0;]1\\x03\\xbc\\x99\\x1a-=\\x15R\\x96:\\x17\\xe8r\\xbc\\x86\\x82\\xaf\\xbc\\x18\\xfeG=\\xa8\\xde\\xa8\\xbc\\x88]j\\xbd\\xfe\\xb4\\x89\\xbd-t\\xca\\xbc]\\xfb\\xa5:\\xbc\\x124=\\x03)d<\\xa4\\xfd\\xa7=\\xf1a\\x9e\\xba\\xba\\xf2\\xcf<\\xe9\\x92\\t\\xbdJ\\x8d\\xa3\\xbd\\xb2<S=\\xcc8\\xdb\\xb9\\\"\\xa1\\xef<\\xe3\\xb1\\xfb;<\\\"1\\xbd\\xcbU@;\\xf8-\\x89\\xbdW\\x944\\xbc\\xf1D7<!\\xa7\\\"\\xbb\\xcf\\xf5\\x89\\xbc\\x9d6\\xdd<\\x08F\\xfa<\\xf6$5=\\xee\\xe7\\x1e\\xbc8\\x9a\\x12\\xbc]\\xc9\\x19\\xbd\\xa6\\xe7\\xb7\\xbcB\\x9d\\xe0<\\x1dT\\x17\\xbd\\xbc\\x17\\xd2\\xbb\\x03x3=\\x1cIZ;\\xcb5\\xf5\\xbb\\x83\\xa9\\x11=\\x1b\\xda\\x95\\xbcw\\x12\\xe4<\\xc6\\x82?\\xbb( \\x0c\\xbd\\xd6j\\xce\\xbc\\\\V\\x8c\\xbd\\x13\\xd3\\xfc<\\xde\\xca]\\xb9\\x9b&\\xa8<\\xf1\\x05^=\\x08\\xb4\\xf3=\\x97l\\x1e\\xbc\\xf4\\x12]<8S\\xa4=\\x8bL \\xbck\\x92\\x8e=I\\xb41\\xbd\\xc0\\x8c:=\\xbc\\x8a\\x84\\xbc\"\nHSET bikes:10056  model 'Polydeuces' brand 'Nord' price 4200 type 'Mountain bikes' material 'aluminium' weight 15.1 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\xbe\\x9e\\x89\\xbc4\\tC<\\xf2\\x92\\x91\\xbc\\x0fO\\xf5<iZ\\xba;\\xdc\\x91\\\"=c\\xee\\x03\\xbd\\xe0\\x95\\xec\\xbc\\x1a1K=\\x9aX\\n\\xbd\\xf1\\x9f\\xbb<\\x96x1<d\\xcbw=\\xceu7<\\x16P\\x91=\\n\\t\\x8d\\xbdTW8=\\x8d\\xf6\\xc3\\xbc\\nF\\xc1\\xb9\\x95q\\x8c\\xbdR\\x93S\\xbcS!\\xdf\\xbc\\x855F=Mq\\x83\\xbd\\xcc\\xee(=\\x1cI2;\\x1aU4\\xbb>\\x8d7\\xbcT\\x11\\n\\xbd\\xabN\\xda\\xba\\xf2\\xb7\\xc3\\xbc\\xf1\\x0e\\x01\\xbd(\\x9f\\xf0\\xbc>3\\xc0\\xbcH%\\x1d=\\x8d\\xdd\\x08\\xbd\\x05\\xf0\\xd9\\xba\\xe2\\x95\\xb3\\xbcd\\xc1F\\xbd\\xfa/\\xd1;\\xc8\\x81k=N\\x84\\xcd<\\xf5\\x1d\\x18<\\x04c\\x1f=,\\x05\\xe3\\xbc\\xd9\\xd6\\x80<\\x88\\x0cb=N\\xf1\\xa9<\\x92\\xf3\\xad\\xbc\\xe1\\x99\\xa9\\xbb\\x94\\\"\\x9a:\\xa4\\xa2J\\xbcQ\\xb8\\x93\\xbc>z\\x87<y\\xc7\\x1f=\\xbf\\x1d\\xa0;l\\x00g\\xbc\\xf7\\xc9\\xfa\\xbd\\xc0\\x1a\\x8d\\xbd{J]=\\xa9j\\x92<\\xa52\\x08=\\x91\\xe7\\x07<E\\xf5\\x8f=\\xe4\\x0bG=\\\\\\xb6J=\\xb8\\xa2T<\\xca\\xfa\\xbd;3\\x1f\\\"<YO\\x14\\xbd\\x19\\x03_\\xbd&!\\xa8\\xbc\\x19\\x8a\\xed\\xbc\\xb6Y\\x17<jf\\x1f\\xbd\\x10\\x058=5!\\xb4;\\xd1\\xc8\\x12\\xbbn\\xab\\xa9\\xbd\\x04\\xdc\\xfb\\xbc\\xa6\\xa9\\x8f\\xbbA\\r*\\xbdxj\\xa1<\\x18h\\x7f\\xbd\\x1eA\\x11<\\xf9J\\xb8;\\x84\\x16\\x83<\\x86\\x0b>9%\\x9e\\x92=\\x17\\xebM\\xbbE\\xfe~\\xbb\\xba\\x7f\\x8a\\xbc\\xddM\\x00\\xbdg\\xc2\\xdc\\xbb;\\x15\\x88<`\\xfa\\x9f;\\xd6\\xb9E\\xbc \\xf5\\x10\\xbd/\\x86\\xbc<c?#\\xbd_%\\xf6<\\xe9\\x11\\x1e=\\xd2QE<\\xfb\\x87(\\xbd}l\\x1b\\xbdzt\\x00>\\xe1\\x96a\\xbc\\x8bu\\xf8<;\\xaf\\xc7;\\x8c\\x0e\\xc0<4#\\xa4\\xbc\\n\\x12\\x9d=\\\\t\\x96<\\xe7\\xc0\\x14\\xbd\\x8a\\xa9\\\"\\xbd\\xbfC\\xd3;[%\\x85\\xbc\\n\\x86\\xc4\\xbc\\xe8\\xbf#=\\x8d\\xa5\\x8a<\\x1eM\\xed\\xbb\\xcd\\x0e\\xd3<\\xbd\\x18\\xf4<\\xbb_\\x1e\\xbd\\xa08\\xc1<\\xdc\\x12\\x92:\\xa3\\xfc\\xa9<\\x1dg\\x8d\\xbda\\xfdz\\xbc&\\xea\\x91\\xbd4U\\xa2;\\xc4\\xdf\\xe7;\\x0cy\\x8d<\\xa6\\xe8\\xe6\\xbc7\\xedE\\xbc\\xf5;\\xce<\\x1d\\xce9<]\\x8cT\\xbc\\xd1\\x94j==\\xfa\\xf5\\xbcL\\xdb\\xd9;\\xf3C\\x9e\\xbd\\xe6\\xa4\\t=1\\xee\\xa9<\\xe2\\xbc\\xa5\\xbc|\\xc6B\\xbdN\\xa4\\xc1<\\xfd5\\xed\\xba\\xf2\\x87\\x04\\xbd\\x063\\x16\\xbd\\r\\xe1O\\xbb\\xe9\\xae\\x10=i\\x01\\x08\\xbd\\xc44\\xc5<\\xff%\\x1c\\xbd\\xfc\\xaa\\xac\\xbc\\xb6\\xc2\\xe0\\xbc\\xd15\\x94\\xbb8\\xc8\\x97\\xbcf\\xc3\\xc6\\xbcc\\x14\\x9e=\\xa4\\x80\\x8b\\xbcBfz\\xbc`\\xef?\\xbd\\xd2H\\xd7<\\x93\\xfa\\xbd<\\x82\\x1aw\\xbbR^o=\\xc3p\\xc8<\\xaa\\xa8\\xe4;\\xb7\\x17\\x9a<y\\xb3\\xa0<\\xe3\\x94\\t\\xbd\\x17\\xb5\\xdb\\xbc:\\xe4\\x1b\\xbd\\xef<\\xbe\\xbbm\\x191\\xbd\\x97*7=\\x12\\x80\\x1b\\xbc\\x07(\\xe2\\xbc\\x97!\\x83=#2\\xa9<\\xd4\\x82\\x88\\xbc\\n\\xa8\\xa0<B\\xaf\\xe4;\\x0fh\\xd4\\xbc\\x91&\\x81=\\xf2\\xa6!\\xbd\\xa4\\xcaw<\\\\18\\xbd\\n\\xe8\\xd5\\xbc\\xdd\\x8e\\x88<vot\\xbdK\\xfd\\xb3\\xbb\\x01iT\\xbc\\rX0=|~\\x83\\xbb\\xd3\\xa7\\x9e\\xbd\\x16\\xa8d=\\xf3\\x9f\\xa9=\\xe5\\xc1\\x16\\xbd\\xca\\x1b\\xb6\\xbdX\\x04\\xb4\\xbdP\\x7f\\x89\\xba\\x07\\x02\\xab\\xbc(*J=*Qn\\xbc\\xae\\xd1G\\xbc\\x8f\\x07\\x11=\\xdc\\xb6\\x8c\\xbd\\xfa\\xfa\\xdb;\\x01\\xdf\\xbb;i\\x1a\\xa4<j6Y<\\xad\\xe4a\\xbc\\xb5J;\\xbd\\xa9\\xa6\\xe6;\\xfd\\xfb$=buF=\\xc5~\\x95\\xbc;k\\x03=\\x0eIw<{>+\\xbd\\xe0m\\xa5:$-\\x1e\\xbdJ+\\xb5\\xbc\\xfb\\x7fU;\\xbe\\xe4T;\\xb2\\xb2\\r=\\x87\\xb1;\\xbd\\xfb\\x07\\x9d\\xbc\\x1a\\x99\\x8a\\xbc6\\x8c\\x0b=,\\xdf =\\xb5\\x8e\\xb1<\\xc0\\xeb\\xdb<%\\xff9=\\x0e\\xbc\\x02=9B\\xde\\xbc\\xb8\\xc4\\x15\\xbc\\xba\\x184<Q\\xddJ<\\xd0\\xe2\\r=8\\x96P<\\x84\\x9f,<w\\x1aY=\\xee\\xcc\\x19=\\x9ct\\x02;\\xebw\\x8b\\xbd\\xe1:\\xf1<)Q\\xd4<8\\x0c\\x81\\xbd\\x1ej\\xb1<\\x98\\x00\\x82=p\\x8fe\\xbc\\xdb\\xd4\\xce<<n\\x8d\\xbc\\x8e\\xa7\\x0f\\xbc\\xfbbq<\\x9dau=\\xb7~A<m\\x1fj\\xbc{-B\\xbd\\xc2V\\xfb\\xbc\\xfd\\x06\\xe2\\xbc\\x91\\xea\\xda=4\\x8c\\xc6\\xbc\\xc2t\\xd5\\xbcZ\\xff\\x17=(\\xb11\\xbcn5\\x12\\xbb\\x01\\xa5\\xba<\\xd0\\x93\\x14\\xbd\\x8eha=\\xdbO\\xd1\\xbcuA\\xa3\\xbc\\xee\\x8b6\\xbc\\xaf\\x9e\\x07\\xbc\\xc0&\\xbd;D\\xd3\\x8a\\xbdz!f\\xbc_2n=\\xeb9^\\xbd-\\xf0L=OC\\x01=C\\x11H\\xbd.\\xf6\\x01<\\x1f\\xa5\\xad<\\xde\\xe6\\x8f\\xbd\\x07\\xbc\\x99\\xbc\\xb6\\xb0\\xea\\xbb\\xcc\\x99G\\xbd\\xae\\xfb\\xb2=\\xfa\\x00\\x95<\\xa9<\\x8e\\xbcO\\xc5T;\\x11z\\x1b=\\xfc\\x05\\x04=p\\xc6\\x91<\\x0c\\x833\\xbcC\\x97[==\\xf9\\xb7\\xbc\\nxn=\\xf3~d\\xbb\\x1c\\xd7\\xdc<\\x184\\xe3;\\xa6(\\xca<\\x01N\\xfe<\\xd2+\\x7f=\\x8b\\x96\\xae<-\\xd2J\\xbd(\\\"\\xac\\xbc_\\x068\\xbd\\xa8D^\\xbb+`\\xaf<\\xd5\\x8b\\xac\\xbcTC\\xb3\\xbc\\xe4\\xd6\\x8e=1\\r\\x9b;r\\xac\\xdf\\xbc\\xca\\xd4\\x0c\\xbd(}\\xec\\xbc\\x7f\\xb6\\xe5<=\\xf5\\xae\\xb99\\xbe\\xed<B\\xf6q\\xbd\\x9d#\\r\\xbd\\x90\\xce\\x03\\xbe\\xe7F)=a\\xda$<-a\\n\\xbb\\xb0\\x9a\\x06\\xbd\\xeb4\\x8b\\xbd\\xe5\\xf5\\x94\\xbc\\\"\\xc6\\x14\\xbaN\\xb4-\\xbd\\xc26\\x1e\\xbd\\xa46e=\\xb7C\\xe4<\\x92p\\xb2;\\xa0\\x9c\\x819\\xc8\\x901\\xbdZ\\xdd\\x96=\\x04c\\xa5\\xbcn%\\xc3:i\\x99\\xf4<\\xc5\\xbb!=ON5=\\xc1\\xe9\\x83=\\xd0E\\x1d\\xbd\\\"xl:\\xdd\\x7f\\xd3\\xbc\\xc1\\xe7[=\\xb7e\\x80\\xbc\\x9c\\xe8\\xfb\\xbc\\x17\\xa9,\\xbd\\x82\\xb9\\xa1;r\\xb0\\xa7<\\xc2?4\\xbd\\x8eg\\xc0\\xbdC5\\xfe\\xbc>\\xc6\\x00\\xbc\\x8a\\x0f\\xb6\\xbc\\xeb\\x85\\x80=w1\\x18=~7\\x82=\\xd0\\x99q\\xbc\\xf1f5=:\\x96!\\xbd\\x1fH \\xbd\\x10\\xeb@<\\xd8\\r\\xc5<T>\\xb2<{\\x90\\x88\\xbd\\x9c\\x7f&<&N\\x88<i6\\xda\\xbc\\x03\\\"c\\xbdK\\t+\\xbb\\xeb\\x1a\\x80\\xbc\\xa9\\xf4!\\xbd\\xec\\xddj\\xbc\\xb5E\\xd8\\xbc\\x93\\xb9\\x01\\xbc;\\xd4\\xa4\\xbd\\x9a\\x95\\xfb;\\xb8\\xde\\x90\\xbc\\x1d\\xf5\\xda\\xba\\xcba^<&\\x18n<C\\xc4q\\xbd\\xd11R=\\x14(\\xb4=PV!\\xbd{\\x97\\xef:e\\x0f\\xc8<\\xae9\\x9a\\xbd:tI=\\xa8\\xcf\\x82=*!\\x8b\\xbd \\xa3\\\"<\\xa1\\x0bB\\xbd\\x0e\\xe9\\x84<\\xeb\\x89E=\\x80 a;\\x80\\xa2\\x93\\xbb%/\\x97=\\xec\\x17\\xa2;7m\\xb7:\\xf0P)=\\x80}\\x81=\\x98\\x8d\\xce<\\xce\\x1f>=$\\x18\\x19<\\xf2\\xe0\\xd3<\\xd3\\x8c5=\\xb2\\x9c\\x08\\xbd\\xa2U\\x94\\xbd\\x15\\xa2\\xf8<\\xae3\\x10\\xbcQs\\x04\\xbcQH\\xf7\\xb9\\xb2\\xd9\\x86\\xbc#\\xe1^=\\x91\\xee <y\\x90P<\\x17\\x90\\x1f<\\xfd\\xdb}=\\xbb\\x04\\xdd\\xbbc\\xf8A\\xbd\\xdao\\x15<\\xad\\xb9\\xf4\\xbc\\xf3\\\"\\xd5\\xbc\\xc72\\x0e\\xbcQ\\xf1+\\xbd\\xe1ya=*a\\x19=\\xc5\\xb0\\x06=\\xfc9\\x94<\\xf9-\\x9e<\\x9f\\xe8\\x04;\\xffc\\x9c\\xbc\\x13W\\x01\\xbc\\xa7R4\\xbc\\\"\\xb6|\\xb9\\xd8Q\\x10\\xbb\\x14\\xbc<\\xbd\\x7fA(\\xbc\\xbe6\\x8d\\xbc\\xe7\\xa5A<\\x04ey\\xbd@%P\\xbd\\xa4\\xa8:\\xbc\\xe3\\x14\\x17\\xbd\\x84\\n\\xb5\\xbbEf\\xe4\\xbc\\x91\\x9d{<\\x06\\x16+;\\xb2EH\\xbc\\x8bm)=\\r~\\x87=\\xe3}\\xb2;\\xa1t\\xfa<\\x1e\\r\\x9c\\xbc\\xb5\\x94\\x16;\\x06p\\x84<s\\x89\\x1a\\xbc\\x07\\xac\\n\\xbd?\\xb12:\\xf2w\\x879\\x18g\\xca\\xbc\\xfd]\\x0c=\\x8c]\\x0c\\xbc\\t\\x9a\\x0e<1\\x13\\t<\\x1a\\xabq<\\x15\\xb0Q\\xbd\\xcb\\xe2u<\\xae6#\\xbcM\\xb2C;J\\x95\\x0c<\\x11o!=8^\\x86\\xbc\\xea\\xba\\xe3<\\xbf=\\x07\\xbc\\xc53\\x9a\\xbd\\x8d*\\xce<l\\xc8\\x80\\xbb\\xbd\\x171\\xbd&m\\x8f\\xbb\\xcaR\\n=\\x0c\\xb2s;o)b=\\x94\\xb7\\x84\\xbci\\x8bo\\t/s\\xe5;G\\x10\\x04\\xbd\\xcc\\xad\\x18=\\xb3kC<{\\xfdI;N\\xaf\\x89<\\xdb\\xd6`\\xbc\\xe0-s\\xbc\\xaef\\xe0\\xbb\\xfd\\x0b\\x93\\xbc\\xa1\\xd7\\xb4<d\\xa6/<\\xe8\\xd2\\x01<e\\x108=\\xa9\\x83\\x8d\\xbb\\xbf\\x92\\xbe<\\xfd\\x86G\\xbd\\x98\\xac\\xcf\\xbc3,f\\xbcc\\x97\\x17=\\xd96\\xe1\\xbbv\\x038<\\xe3\\xe7\\xd4\\xbc\\xd7\\x08C\\xbd<K\\x01=\\x94x}\\xbc\\xe1\\xafw\\xbcI%g<\\xc7\\xefi\\xbc\\x95\\x07\\xdc<\\xb7\\xca\\x17<I\\x96\\xb9\\xbc\\xd3b\\x97\\xbd\\x85\\xcfC\\xbbo\\xa1e\\xbd\\xdc\\xe9\\x93<x\\xbd\\xbc<>\\xa9o<I\\xa2\\xa3<\\x01\\xc3s\\xbb\\x18(s<I\\xb7\\xa7\\xbc|\\xafQ\\xbd\\xe6\\xd3\\xcf<#6e<:g\\x85\\xbc\\x87M\\xf5<\\x821\\x19\\xbc2\\x84\\x8e\\xbcMg\\x0f\\xbdi;\\x9c\\xbdk\\xbc\\xc6\\xbc\\xb1\\x96\\x9e<m\\xd8\\\"=\\x07\\x86\\xeb;\\xf9\\x9c\\xd9;\\x00|\\x13\\xbdT\\xb5\\xe0<w=\\xbb\\xbb\\x1a\\x16\\x8b<\\xb5cU=\\x0b\\xe2\\xd5<\\xe8\\xd2\\x16<\\xcdD9=q\\xb9\\x83\\xbd\\xa7\\xd54\\xbd\\x8cVs\\xbd\\xab\\xd0\\xd6;\\xcfXj=\\x1b\\xee<\\xbc\\xf4\\xd9\\r=\\xf9\\xb4\\xbe\\xbc\\xf1\\xb6\\x92\\xbc>\\\"a=\\xa25\\xc5<]5\\xec\\xba\\x99\\xb4\\xfa\\xbct\\x93\\xab=\\xc2-:\\xbdSz?=]q\\xba\\xbb\\x9f\\x10%=;\\xe8N<\\x13\\xce\\xe3<\\xady\\xb3\\xbc(\\xff\\xd9\\xbcl+\\xdf<\\x05\\x8e\\x8c=\\xfc\\xfb\\xb5;\\xcd\\xff\\xdd\\xbb\\x84I\\xba\\xbb{\\\\\\xdc\\xbc\\xc1\\xdd\\xfd<\\x87\\x93\\xf58\\xf3\\xf4\\x13=\\x8d\\xb5==\\rc\\xb7\\xbc\\xfb\\xc9.\\xbd\\xdblk7R\\xdd\\xf4\\xbcJaf\\xbd\\x83\\xbd\\xd5\\xbc\\xe8\\x87V;\\xc0Ph\\xbd^F\\xa1=~\\xc5(\\xbc\\xb9\\xedE=J\\xfcD<\\x8d\\xaa\\xa3\\xbc\\x1a\\x1e\\xd4\\xbab\\xab\\xc4<\\xde\\x0eH\\xbc\\xbe\\r\\xf8;\\xedsH==\\x0bX=\\xd8\\xc0\\xd4<\\x1e\\xa5_=\\x0c*\\x16=5\\x19\\x1d\\xbc%h\\x85\\xbd\\xe9n\\xbd;5\\x8cQ\\xbc\\x06|\\xe5<\\xde\\xdb\\xad;y\\xabB\\xbc\\\\\\xf5u\\xbb\\xc1\\x13\\xf8\\xbc\\x05\\x8e\\xa6<4_\\xb2=$:\\x87\\xbd\\x91J\\xf4;\\x1bL\\xd2\\xbc\\x99\\xb2\\x16=\\x01\\xa04\\xbb\\xcc\\xa5-=dt+=\\x9d6a=\\xea\\xc9J\\xbd4\\xd2\\xa7<+d\\xd9\\xbbU\\xcf\\x8d<\\xb3%:\\xbdj\\xef\\x88\\xbd\\xfa\\xa0\\xfe<\\xb8\\xa9\\xad;\\xa9\\x1a\\x9c9A\\xd2\\xb4\\xbc\\x1b\\x02f9\\x19Z\\xcc\\xbc\\xa0\\x155\\xbbG\\x97\\xfb<\\xac\\xd5\\xab;\\xe9\\xd2\\x05\\xbd\\xa5kU<K\\x92\\xca<r\\x11\\x1c=\\xdd \\xea\\xbc\\xe0\\xf1\\x98<\\xa9\\xdf\\x96;\\xb9i\\x82\\xbch\\x15b\\xbb\\xd8\\xfe9=\\xe6\\xd9\\x19;tl\\x9f\\xbb\\xf1J\\xe1\\xbc\\xe5k\\\"\\xbc\\x00\\x8d|\\xbd\\xa8\\xb8\\xe2\\xbc.X\\x83=\\x95\\xe4\\xd8\\xbc\\x03G\\xea<\\x82(\\x01<\\x9b\\xd5\\x98<:>\\x95\\xbc\\xe3g.\\xbc\\x01\\xe38=s\\x96\\xc6<h\\xdeG=d\\x0f\\\\\\xbb\\x11\\x0f\\xa8:=\\xe3>=\\x9c\\xac\\x87\\xbdb,\\x10=N\\xf6z\\xbd\\x08\\x82\\\\\\xbcR\\xb9\\xb5\\xbc\\xc5\\x1a\\xe7:\\x8b\\r\\x1d\\xbdV\\xa1\\xa8=\\xe2\\xe7H\\xbcob\\xe3;\\xae\\xd9e<\\xd1\\xa5\\xc7\\xbc?\\x99\\xf4\\xbc\\xb0\\\"\\xc6\\xbc\\x8f\\xdf\\x18\\xbc\\xfb\\xe2-\\xbdmG\\xa5<HZh<x\\x9e]<\\x9b\\x1f=\\xbdb\\xc6*=G:\\n\\xbd`]\\x03\\xbdH\\xc0J\\xbdi\\xf6Y\\xbdn\\x11\\x14=\\xd5\\xcb-\\xbd\\xaeW\\t=Gq\\x90\\xbd\\xfaa\\xcc<\\xae\\xc2J<\\x94\\x07!<\\x9d\\x8aI\\xbc\\x7f\\xafI\\xbd\\xea\\xfd=\\xbd\\xcc^\\xcf<\\x8bz\\x91\\xbcv\\x1b\\x1d\\xbd\\x85Q+\\xbdk\\t\\xdc;/\\xb1\\x04\\xbd\\xa6\\x07\\x8f=\\xe9\\x17\\x0b=~\\xdd\\xac=\\xa94\\x86:#k\\x15=K\\x15\\x9e\\xbc\\xbb\\r\\xee\\xbc\\xbc\\xcdC=3n\\xa2<\\xcb\\xc0\\xd4\\xbc\\xc0\\xe5\\xf2\\xbc\\t\\xe0\\xcb\\xbcr\\r \\xbd\\x0c \\xef\\xbc4V\\xf8\\xbb\\xd1\\x9cV\\xbc*!f=\\xb44W\\xbc\\x05\\xf2\\x1c;\\r\\x19w=\\xa6h0<\\xb6\\x17P\\xbcsR&\\xbd\\xc9\\x1f\\n\\xbbx\\xd1U\\xbd\\x97\\x81\\xdf\\xbc\\xeay\\xb1\\xbdN\\xb6\\x85\\xbc8\\xdf\\x8c\\xbckb\\\"\\xbd\\x9c\\xb01<\\xad\\xfa\\xd2<\\x19E2\\xbd\\xdc\\x8d\\x8c9\\x99\\x16\\x02\\xbe0\\xf6P=!\\r\\x94\\xbdJ\\x97\\x86\\xbd\\xa8$\\x80;q\\xd5>=\\xb3W\\xb4<\\x18\\xed\\x8a<\\xf2\\x11\\x8c=\\xf8\\x19V\\xbc\\xdc\\x1f\\xe8<L\\xca\\x02=\\xd6(A\\xbd\\xe4sy\\xbc\\xca\\x94\\xfc\\xbbr@\\xf2<\\x1f\\xc4\\x14\\xbd\"\nHSET bikes:10057  model 'Salacia' brand 'Nord' price 1212 type 'Enduro bikes' material 'full-carbon' weight 11.2 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\x0c\\x1eq<9\\x86\\xcb<Y\\xa4\\x13\\xbd\\x0f\\xe4c=\\xc0\\x93\\xc4<R\\x1bA=\\xc1\\xea\\xf3\\xbc\\x9c\\xa1\\\"\\xbd\\xd7$w=.pL;\\xfa\\n[\\xbc%\\xd4\\x88:#R\\xeb<\\xc2\\x0f\\x01;\\xfd\\r\\xa9=\\x01_j\\xbd\\x0b5O\\xbc\\xe7\\xa3\\x12\\xbdl\\xb1z\\xbc\\xa2\\xa6Y\\xbdL\\xdex\\xbb\\x87\\x07\\x11\\xbdw\\xbc\\x05=A\\xd1\\x98\\xbcj\\xc3?=xI\\x9a:\\xbe\\x86\\x91\\xbcG\\xebf\\xbc\\x83\\x86\\x1c\\xbd\\x86I+<\\x8a\\x94M\\xbc&E\\n\\xbd\\xa4Hz\\xbc\\\"\\xfa/=\\n\\xb8H=\\xc0\\x14\\x82<D\\xcc\\x05\\xbc\\x9f\\xbdm\\xbc\\xf9b\\xe9;_\\xadP;\\xdc\\x89w=)\\x10\\x95\\xbc;\\xbc}<HYE<%S\\x8d\\xbc\\x16\\xe7\\xbc\\xbb\\x1a\\x0b\\xa7=S\\xf6\\xd7\\xbb\\xe2\\x8c\\x89\\xbc\\xa6\\xae\\xcd\\xbbY\\xd1\\xd2\\xbc\\x9aM}\\xbd\\xe6\\xbfF\\xbc\\xe7C(<\\x9d@\\x87<a\\xb6h\\xbb\\xa4\\x04\\x16<\\x02\\xde\\xb2\\xbds\\x1b\\x81\\xbd[\\x11R=>\\x83\\x07=\\xc7\\xe8\\xd6\\xbc\\xb6\\rO=q\\xab\\x85=3\\xb09<\\x8f%\\x12<\\xb1}P\\xbc\\xda>\\xac<\\x1dG\\xd8;\\x03\\x89\\xdc\\xba\\xb5\\xf9\\xc1\\xbc/\\xff\\n\\xbdjm%\\xbdy\\x0b\\xee<\\xc2\\x8c\\x1a\\xbdA\\x98\\x9d\\xba\\xaf\\x03e<S3\\x12\\xbdd\\xc9#\\xbd\\x11a\\x80<\\xa7\\x8c\\xe5<\\xcf\\x97\\x1c\\xbd\\xdb\\x8c\\x08<f,\\xa5\\xbd\\xd3\\x13k<j49=\\xd7mf<\\xd7I\\x02=\\x8a\\xdb\\xaf<$\\x9d\\x84<\\xa0\\xa9F\\xbc\\xb97\\xa4\\xbcE\\xef\\x08;\\xac\\xcep<\\xcc\\xa1\\x13<\\xa3\\xd5\\xf1;|\\xdb\\xee\\xbc\\xfb\\xb3k\\xbd\\x8d\\x8b\\x88<a}\\x84\\xbd\\x95\\xa9[\\xbc\\tS\\xe0<\\xf2i\\x86<\\xeb{*\\xbd\\x8bL\\xd4\\xbc\\x16\\r\\xc9=^\\x1eA<\\xca\\x88\\x8f<\\x07=\\x1e:\\x19\\x92\\xdf<L\\x0b\\xbd\\xbb\\xd5\\xa8\\xc4=\\xc7\\xd3\\x91\\xbc\\xce(T\\xbc\\xda\\xc7(\\xbd\\xc4;\\xd6\\xbc\\xaf\\xc5#\\xbd\\xc7\\xa7\\xbf\\xbb\\x8e\\xc7A<\\xee*\\xce\\xbb\\xc5\\xf4|<kk\\xeb<5L\\xb5<S/I\\xbd\\x90\\x92v=\\xcb\\xae\\x10\\xbd\\xd0p\\\";\\x03\\xcb\\xfc\\xbco5\\x8f\\xbc\\x1bLv\\xbdB\\xbd\\xf7\\xbcx\\x92\\x14\\xbdR\\xf7\\x14=\\x99\\xceL\\xbd\\xe3\\xccA\\xbcK\\x0f\\x13=\\xdc[\\xd1\\xbc\\x92\\xa6\\xdd\\xbb\\x0b\\xcd\\xd6<\\xa1Z\\x84\\xbc\\xfd}\\x04\\xbd\\xdd\\nK\\xbd\\xe8O\\xd6<\\xc1\\x9df=\\xb1\\xf3G<\\xa4\\x1d\\xb7\\xbc6 \\x9a<\\x0fA\\x11<\\xe9\\x0e\\x9b\\xbc\\x13HG\\xbd\\xb0\\n\\x93<s\\x92!=\\xdd\\xad\\x0b\\xbd\\x8b\\x05\\x08=\\x82ft\\xbd?\\x10\\xbe\\xbc\\x9f\\x1c\\x90\\xbc\\xc3{/<A\\xb7<<Y\\x81\\x8b\\xbdC\\xa0\\x98=\\xd8\\xde+\\xbc\\x85\\x82*\\xbc\\xda\\xab~\\xbd<gV<\\xb00\\xb6<M\\xd1N\\xbc\\xee,u=\\x95\\xe1:=\\xd1\\xbej<\\xb3h)\\xbb!\\xdaF=F\\xbe#<\\x82.\\xf6\\xbcU\\xedC\\xbc\\x8b\\x98\\xa6<H\\x1f\\x9c\\xbdyN\\x8a=\\xef:\\xe3<<V\\xc3\\xbc\\x8b\\xc8\\xd4\\xbb\\x94\\x9f\\x0f=^\\xefW\\xbd\\x8c\\xf1}<\\xe6\\x9d\\xe8<\\xb6\\xf7\\xd4\\xbc\\x80\\xa25=\\x08N\\xce\\xb9n\\xf5\\xb8<\\x8a\\xb1F\\xbdn\\x1c\\xbb\\xba\\xdd\\xf8\\xfe;f\\xb7?\\xbd\\xceg\\xa7<\\xb2\\xe0\\xaa<\\xdd\\x14*=\\xadF;\\xbd\\xb8IY\\xbd\\x8f\\x84\\xbb\\xbc\\xd7\\\"w=\\xfam\\x8d\\xbc\\xa6@b\\xbd\\xe7m\\xe5\\xbdY%\\x03\\xbdne4;\\xdf\\xc51<\\xe0\\xfe\\xa4\\xbc\\xdf\\x92S\\xbc\\xca\\xc5\\x82<\\xa7+\\xcb\\xbc;Ww\\xbc\\x99\\xe51\\xbba\\xd4\\xeb<L\\xf6+\\xbd\\xa2yt\\xbc\\xb91\\xd1\\xbc\\x18\\x12b<\\x91\\xa8\\x10\\xbb\\xb7\\x9f>=\\r\\x16[\\xbd\\x99\\xc8\\xa0<\\xdb\\xa9I\\xbb\\x8ew#\\xbd\\xc3\\xb9O<%\\xd4\\x82\\xbc\\xeb<o\\xbd\\xa8\\xf0\\xa0<d~\\xf7;\\xdfS\\x1d=n\\x04\\xc6\\xbd^<q\\xbd9\\xdf6\\xbd\\x9d_\\x90:I@X=k\\xde\\x9f\\xbah@\\xc3<\\xed\\x19A=3\\xfb\\x83<#y\\xf2\\xbc*S\\xdc\\xba\\x02\\x92\\xcf:+H\\xf3:\\xe1\\xcf\\x87=\\x1do\\xa6;n\\xe9x<\\x19\\x94$=\\xcc\\xeeZ=M\\xae\\xb4\\xbb|\\xd0\\xef:\\x96\\x90\\xd4<X\\xe3\\xfb<\\xab%\\x8e\\xbd\\xb1\\x8d\\x81\\xbbG\\x88\\xac=\\xd6\\xfc\\x9d\\xbd\\x1d\\xf2\\x9c=\\xb8\\x19\\xbe\\xbbtC\\xa1\\xbct\\xbd\\xf0\\xbc\\x8b\\xe3\\xb3=\\r\\x00l<\\xb1\\xff\\x0b\\xbcr\\xb1\\x0b\\xbd\\x08\\xc8\\x1c\\xbcJe\\xaa\\xbc\\x85Y\\xd5=\\x87\\x14\\xbc\\xbc\\xc8z\\x12\\xbd\\xfe\\xa67=\\\"SC\\xbc\\x82oe<s[{<\\xed\\x82j\\xbdN~\\xdc<\\xaa\\xb5n\\xbcB\\xbe\\x9c\\xbc\\xda\\x8c\\xa3\\xbc\\xa4\\xf5y\\xbc\\xfc\\xa6\\x03=\\xc2~\\xa0\\xbd\\xc0;/\\xbd\\xc2\\x8cK=\\xa2T\\\"\\xbd\\\"\\x98x<\\xc6n\\x8d<;\\xda\\x16\\xbb\\x18\\xc8\\xde\\xbc{\\x1d\\x11=\\x0bZ\\x93\\xbd\\x0f\\xd5\\xf3\\xbb\\xdb\\x89\\x9a;k\\xcf\\x18\\xbd-\\xb1\\xc8=\\x1b|\\x80<\\xe1C/\\xbc\\xfb\\xc0\\xf0\\xbcg\\xad\\x19=F\\xbe\\xd3<J\\x02\\x91<\\xf8\\x95\\n\\xbd\\xf0\\xd1\\x80=\\x9c~\\xc8;\\\"\\tl=Q(\\x9e\\xbc\\t[\\xe2<\\xe2\\xdd\\xf2<\\xcc\\x8b\\xaa\\xbc\\xff\\xba<=\\x1c7\\x97=q-\\x08=-\\x92\\xdf\\xbc\\xcbFx\\xbcF\\xff\\x16\\xbd=Q\\xd5\\xbc\\x9b\\xc4^<,\\xf1M\\xbd\\xd1\\xfek\\xbcQ\\xf0\\x9c=I\\x9c\\xb1<\\x8aE\\xe6\\xba\\xc3N\\xe7\\xbc\\x8a\\x14|\\xbd\\x0f \\x84<\\x01\\xe8\\x10\\xbd\\xea\\x0c\\xdc\\xbc\\xfa\\xdb[\\xbd\\xa1j\\xeb\\xbc\\x86`\\x87\\xbd\\xfc\\xb0:=|\\x1d\\x9a\\xbc\\x06e\\xe4<\\xbd\\x1a\\x8d\\xbd\\n\\xb7d\\xbc\\\"[*\\xbb7\\x17\\xce<U.\\xc0\\xbc\\xb2QS\\xbd\\xe5\\xfc\\x8e<_\\xe1\\xf4<\\x88\\\\6<\\xed\\xd6Y\\xbc{{+\\xbd\\\"t\\xad=\\x86\\xfcg<\\x1d\\xf0\\xb6:\\x8f\\xde\\xa7<D\\x06\\x8b=n\\x8a\\xbb<w\\xdaS= +\\x1b\\xbd\\xc0{+\\xbdb\\xb4\\xf3\\xbb\\xd1(\\xaa<2\\xec\\x15\\xbd6j\\x84\\xbd6\\xd3\\xe3\\xbc\\xd5\\xec\\xf8;\\xc3\\x14\\xaf\\xbc4\\\\,\\xbd\\xb6J\\x12\\xbeF%\\xd5\\xbc\\xd7!\\xa3<\\x8f\\x8d\\x97\\xbc\\x88\\xb7\\xb2=@oF=e7\\x98<\\x99_S\\xbc]5\\xa5<\\xf1\\xd2\\x95\\xbc\\xde\\x1au\\xbdX\\xa0-=\\xcc]\\x97\\xbc#\\xce\\x11\\xbc4\\x8c\\x02\\xbd!\\xb9\\xb1;\\x1a\\xfd\\x15=\\xa8\\x19U\\xbbZM:\\xbd\\x92\\xec\\xc8\\xbcC@\\x88<\\xcf\\xcb\\x04\\xbd\\x7f\\xc2o\\xbd\\x80\\xc6h\\xbc\\x1d\\xe8f\\xbc\\xd4\\xc2M\\xbd\\xac8[\\xbc\\xbc\\xee8\\xbc[\\xdf\\x08\\xbd\\xb7\\x94\\xe0<\\x16\\xfe\\xb1\\xbc\\x98\\xb9\\x84\\xbd\\xa7\\xdc&=\\x89\\xea\\xba=$\\x97)\\xbd\\x84I\\xee\\xb9\\xe8\\x91\\x07<U\\xf2\\x8c\\xbdx\\x08\\xa0<Hpf=\\xdd\\x9b\\x9c\\xbd>\\x8c\\xbe<\\xffl\\xf4\\xbc5?\\xee<\\xf1J\\xe2<\\x9c]\\x8e<\\xfb(\\x0e\\xbd(3\\x13=\\xec\\x08#<\\xd7\\\"\\xbe\\xbb\\xc5\\n\\x83=\\x83\\x07\\x0b=<V\\xb7<_\\xb4\\xfe<\\xb1\\xd9Y\\xbc\\x19\\xc3\\xd5<\\x80\\x17\\x0f=\\x8bp\\x98\\xbc\\xf6\\x0b\\x9b\\xbd\\xa8\\\"\\xe2\\xbbx\\x94a<\\x06\\x966\\xbc\\xbe\\x99L\\xbc\\x83\\xf2\\x8d;J\\xc9A=\\x8boz\\xbc\\xc9\\xd9\\x19\\xbd\\x81\\xe4Y<d\\xbb@=\\x88\\x82I;\\x7f3\\xb8\\xbc\\xad\\xbfJ\\xbc\\x14n\\xd0\\xbb\\x8b}\\xa6\\xbcyR2\\xbc\\xde2\\x0f\\xbd\\xd0\\xf6F=W\\x1a!=A\\xfaE=\\xc5w\\x85<\\xcc_4=\\xc3\\xf52\\xbcZ\\xa5p\\xbc\\x86\\x1d^;\\x15\\xa5\\x16\\xbc\\xb5fh\\xbc\\xab\\x00!=\\x02eK\\xbd\\x86N\\xe5\\xbc\\\\t\\xa6\\xbc\\xeb\\xbf\\x1c=9\\x17\\x93\\xbd\\xda\\x8cu\\xbd3)\\x8d\\xbb\\x7f\\xcd\\xa6\\xbd\\xd5\\xac\\x0f<n\\xab5\\xbc\\xc97\\xa4<\\x89\\xb8\\xc1<\\xa8gZ\\xbc\\x15\\x8fI=\\x9bV>=\\x04p\\r\\xbc\\xba\\xcaC=\\xa2\\xbd\\xae\\xbc\\xe1\\xdd\\x01<\\x14s\\x18=\\x9ck\\xdc<\\xf0\\x88\\\"\\xbdN\\x19\\xcc\\xbc\\t\\xdd\\x19=\\xc2\\xcd\\xa8\\xbb\\x9e-h\\xbb\\xd2\\xc9\\x94\\xbc\\xbf\\xf9o;\\xdeM\\xe7<XK\\x01=\\x1dh\\x12\\xbdI\\x05\\xd0\\xbaV\\x9cs;\\xee!\\xb7\\xbc\\xe5A\\x90<\\x11\\x81\\x99<8\\\"|\\xbc\\x9bF\\x9e<t\\xdf\\x88;B\\x1e\\xcf\\xbdRf\\\"\\xbcc\\xbe\\xfe\\xbc9\\xb9k\\xbc\\xcbK\\x16=\\xcf\\xb5\\xb6\\xbc\\xc1\\xdf\\xaf<Y\\x93t=N\\x9b\\x99<\\xd5>P\\tv\\x1b\\xf7\\xbb-5\\xfd\\xbc\\xab,^<\\xb9V8=\\xf6KL<\\xe4\\\"3=\\x9b\\xab\\xca\\xbc\\xc1\\xce\\xa4\\xbc\\xc5aV<\\xaf7\\xf4\\xbcU\\xd3\\x19<\\xacN\\xe2<\\x9b8\\xd2\\xbb\\xf1\\x8cm=\\x1a\\xb3\\x85\\xbc\\xdbM\\xd9<-\\xf3\\x1a\\xbd\\xf7\\x89\\x90;\\xb6\\xc1\\x87\\xb9V\\xb3\\xf0<\\x8a\\xbd\\xaf<\\xcf7\\x9d\\xbcC\\x8a\\xb2\\xbc1\\xcb\\x04\\xbd\\xf6\\x80X<O\\xea\\xba\\xb9\\xa8\\xf8\\xb2<\\x11\\xed\\xd8\\xbb[\\x99t\\xbc\\xb8\\xb5\\xe2;\\x0f[x9!\\xf2\\x9a\\xbc\\xba[\\x1a\\xbd\\xcf#M\\xbdf\\x10v\\xbd\\xaa\\xb3!\\xbc^HW<W\\xb3\\xcd<MA.=I/H=\\xf9\\xc2{\\xbc-\\xa3\\xb6\\xbbO\\xb4\\xc2;\\xd9X6=\\x7f\\xec\\x80=9\\xfda<\\x84\\x84s=#\\xdb\\x0f\\xbc\\xd1F_\\xbc\\xf6\\xf0\\xd7\\xbc\\xaa\\xc7*\\xbd\\x81\\xd4n\\xbd\\t\\xfa\\x07=\\xc7<Y<\\xcb\\xbdI=Q\\x88\\xe3<\\x05y\\xfa;:\\x95\\x15=9\\xb3\\r<3\\x02\\xa7<k0\\xf8<d\\xaa\\x00;d\\xc0\\xcf;^F\\r=\\x9c\\x0f.\\xbd\\xde\\x0fr\\xbd\\xcb&]\\xbd,,\\xeb;\\x80\\x18L=\\xcf\\xbdf\\xbc\\x81\\xd7\\xb6<\\n\\xa8\\xb4\\xba8Dl\\xbc\\x01\\xc9\\xd8<e/w=P\\xf5\\x86\\xbc\\xbcGg\\xbc\\xbd6\\x89=\\r\\xbd\\x83\\xbdo|\\x8e=\\xaf\\xf6\\x82<\\xd0\\x03\\xa8\\xb8\\xc6\\x8e\\xb4\\xbc\\x02\\xe1\\x00=\\x90\\x13\\xb5\\xbcFC\\x00=b\\xca8=N\\xa4\\x81=\\t\\x03\\x1e\\xbdU9\\xeb;s\\xb4d\\xbcBH\\x95<x\\x05\\xec<{r\\x8e:P\\x96T=\\xba\\x83\\x18=\\\\BR\\xbdTfj\\xbc\\xfc\\x1eO<;\\xf2<=\\x13(\\x7f\\xbc[\\n\\xa1;D\\x05W\\xbc\\xdf\\xcb\\xe5\\xbc\\xc5U\\t=d\\xa3\\x97\\xbc\\x05\\xd3\\x15=\\xaf\\x00\\xc0\\xbb-\\xb69\\xbd\\xf0\\xd9);\\xb2\\x93\\xbc<\\x8e\\x10\\xc3\\xbcy\\x03\\x10\\xbd\\x87\\x18\\x97=\\x1f\\xbfh=ow\\xba<%z6=\\xae>*<\\xddX\\x9d\\xbc\\x8e\\x83\\x93\\xbd\\x9c\\x04\\x96<j\\xb4\\x929t\\n\\xbd<\\xe0\\\"\\xbb;\\x98\\xb19=\\x8f\\xdf\\xcc\\xbc\\x1f\\xde\\x83\\xbcD\\xec\\xc1<,0\\xad=\\xb3\\xfa\\xa1\\xbd\\x82,N\\xbcVfW\\xbdT\\xfa\\x93=\\xb6\\x95\\x05\\xbc\\x91\\xec\\xc9=\\xc5Q\\xdf;i\\xa5g=\\xe6\\xa3s\\xbd\\xc5\\x12\\x8b<\\xf72@;tx\\xdd;\\x94\\xf8p\\xbdi\\xc9\\x13\\xbd\\xae\\xa8/<\\x1fw\\xa0<>{\\x9e\\xbcqc\\xea\\xbb\\xd8\\x9d\\x8b\\xbc7\\xa5\\xbd<\\xecr\\xb4\\xbc\\x89c\\xa9\\xbc\\xbb:\\xb2;q\\xf6\\xb1\\xba\\xbd7\\xdf<\\x87\\x98\\x1d=\\xa6\\x07w<x+\\x1a\\xbd\\x18y\\x93=\\x10Q\\x06=\\x85E\\x06\\xbb-\\xd8\\x88\\xbc\\r\\x03r=X\\xb8\\x9b\\xbb-\\x0fl<\\xfc\\xe2\\xf2\\xbb\\xdf\\xad\\x94:\\xb1&\\x17\\xbd\\xfc6\\x0c=W\\x05\\x98=8\\x9f\\x04<\\xd0\\xec\\xde<\\xef@W\\xbcK\\xdds<\\xc9\\xff\\xda;\\xa1\\xdb\\xc0\\xbc\\xach\\x82=\\n\\x97.\\xbc?\\x87>;?\\xcf[<\\xb1\\x82\\x81\\xbbb\\x82\\x1e\\xbd\\xaa\\x03\\xc1\\xbd\\xc4\\x8bZ=\\xdcho\\xbc+\\x82\\x9c\\xbc\\x0c\\x96\\xb4\\xbb\\xa5\\xff\\x82<\\xa6k\\xb9;\\xc0\\x0e>=\\xd3{\\xb1\\xbc\\xed\\xab\\xd9;\\x8a\\x1a\\x95<([\\xb7\\xbc#c\\xa3\\xbc\\xc7\\xc6.\\xbd\\x82\\xd9\\xac\\xbc\\x10-h\\xbc\\x9aZ\\x1e\\xbc\\xdd7P\\xbc4\\xb9\\x19\\xbc\\xe47\\xcb\\xbcf\\xd1J;\\xcb\\xf4\\x08\\xbd\\tQz\\xbd:H%\\xbd\\xde\\xfe\\x1a\\xbd`z\\xef<\\x0f\\x8c\\xed\\xbc\\xcd%\\xd8\\xb9/\\x0eW\\xbd\\xb3\\xbbu<1B\\x08\\xbcz\\xbc\\x00=\\x85\\x81_\\xbb\\xc7b \\xbd\\x88rq\\xbc\\xf25\\xfc<{\\xff\\xd9\\xbc\\xba\\xfcz\\xbd\\x9cB\\x19\\xbd{\\xbfw\\xbb4\\x19\\x84\\xbcU\\xb7\\x05=\\xc5\\x1c\\x84=4w\\xa4=\\xb9\\xa5\\x05\\xbc\\xf7\\x1d\\x06=\\x8aU\\x17\\xbdj\\xd6\\x81\\xbc\\xe0\\xe7\\xb1<M\\\"P<\\xa7\\x8f\\xb0<\\xcd\\x1d\\xf8\\xbbF\\xda\\xc6\\xbb\\x84G\\x85\\xbc\\xf7D\\xee\\xbc*m\\xb7;v\\xc2\\xbf\\xbc\\x0c\\x10\\x10=$\\xd2\\xf4<\\x03L\\x96;\\xbf\\xcd\\xcd<L\\x04\\n=O\\xef;<\\xfd\\x8b\\xe7\\xbcg\\xbb\\x14=\\xaf\\x05\\xdb\\xbc\\x9ce\\xe3\\xbb\\x1e1z\\xbd\\xa4\\xc1\\\\<i\\xf2J:\\xc5\\xd0\\xa9\\xbc\\xca\\\"\\x8c\\xbc\\x9c\\xceh=%\\xe3G\\xbdr\\xb2\\x88<\\xb6t@\\xbd*\\x99j\\xbc\\x1b\\xd7X\\xbd\\x07E\\x8e\\xbd\\xd6o\\xf4;\\x96\\x99\\x10=+\\n\\xd9<\\xd6\\xcd1=]\\xf6\\x9b=:\\xaf(\\xbbh\\xee6=G\\x14\\xb3<\\x97\\xf4e\\xbdPg\\x8d=y_\\xb8\\xbc;\\x07\\x04=N\\xe6\\xdc;\"\nHSET bikes:10058  model 'Proteus' brand 'Nord' price 3557 type 'Commuter bikes' material 'alloy' weight 12.0 description 'This bike is the perfect commuting companion for anyone just looking to get the job done It has a lightweight frame and all-carbon fork, with cables routed internally. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\x90\\x16\\x96;k\\xba\\x10=?\\x0b\\x8a\\xbc5\\x86\\xa2\\xbc\\xa6_\\xc2\\xbbhTn<w\\x924\\xbca\\xffP\\xbd\\xa0\\xe2\\x7f=\\xa7\\x13B\\xba\\x08\\x92\\xe9\\xbc\\xd4\\xc9\\x82={\\x99\\xa7<\\xdb\\xb0U<\\x93!\\x0b=\\xbf\\xdd\\xaf\\xbd\\x03!\\\"=c\\xea=\\xbc\\x93\\xfc\\\\\\xbd2\\x93{\\xbdN\\xf2\\x1e<\\x7f\\xef\\x01\\xbd~B<=8O^\\xbcX\\x81\\xdc;\\xdc\\xd9\\xd4;\\xd7iZ<\\xbc\\xaf\\xf7<\\xeb\\x01\\xc1\\xbc\\xd1*\\x00<\\xad\\xbe\\xc8\\xbb\\x9a\\x96\\xc9\\xbc\\x1f\\xca\\xf9<\\xf9$`:T\\xd0\\x17\\xbc\\xb8q\\x04\\xbd+\\x7fh<\\x0f\\x9d%\\xbd\\x99\\xf1=\\xbc+\\x9f\\xb6;\\xfc\\xd2\\x98=\\xd5\\xbd\\xc9\\xbcJ\\xae\\xfa<\\x05m =lT\\x8e\\xbc\\xf7\\x15\\xf2\\xbcN\\x81\\\\=\\xec\\x92\\xb2\\xbcy\\xc5\\xb5\\xbc%S\\x80<Ud\\xa2\\xbc\\x94\\xf8v\\xbd[\\x13\\x9f;\\xba\\xf9\\x94\\xbbx[\\x89;\\xb4\\xa3`\\xbd\\xfcN\\\";\\xfb\\xbd\\xae\\xbd\\xb9fQ\\xbd\\xc6\\x98\\xbe=[\\x95b<]\\xaa\\x8b= \\xc1\\xe6\\xbc\\xb5\\x1da=\\x9dq\\xbf<\\xad\\x83\\x02<\\xc0n\\xdc\\xbc}\\xd3{<)\\xc2E\\xbc|>\\x8a\\xbbg\\xcc)\\xbc4\\xd9\\xfd\\xbb\\xc3^L\\xbdz>7\\xbd1\\x01W\\xbd\\x13\\xe8\\xbc<\\xabC\\r\\xbd\\x9fI]\\xbd\\x83^a\\xbd\\xff\\xad1=\\xb3\\xd2\\xcc;#OO\\xbd\\xd7\\x0c\\x1d:(\\xd1\\x9a:\\xdf\\xa9\\x1f<\\x8e*0=V\\xc3\\x1f<\\xeab\\x9c<\\xacg\\xe6<s\\xfc\\xb1\\xbb-|\\xb0<\\xbc\\xfdQ\\xbd&\\x93\\xad\\xbcG\\xea\\xdc\\xbb\\xe2\\xe7\\x90<v\\xe6\\x04\\xbdq\\x88\\xca;\\x8b\\x7f\\xbf\\xbchkm<I\\x1c$\\xbdu=j;:q7<\\xd4s\\x16=\\xe8@\\x1c\\xbdW\\xce\\x8b;\\x99\\xad\\xe3=\\x1a\\xa1\\x04=\\x86u\\xe3<\\x87\\xc9\\\"\\xbd\\xb9\\x9ae\\xbaq:\\x15\\xbc\\x08\\xc7 <\\x17\\xfa3;\\xd5\\xd5N;\\x82\\xc1\\x8f\\xbd\\xb9\\xd7\\x0e=\\x87\\x88\\x8e\\xba\\xf7dZ\\xbb\\xb1e\\xe7<5\\xf4\\xf8<\\x05\\xee\\x87<\\x1dG8=G\\x85\\x86\\xbd\\xca\\xab6\\xbd\\x101\\x13=\\x8aS\\t<\\xaf\\xcf\\x87\\xbb\\x1a\\xc3\\x83\\xbc\\xf6\\x12\\x18\\xbd\\xf6\\x1dA\\xbd\\xcc\\xc9\\xdd\\xbat\\x8c\\x06\\xbc\\x99\\xee\\xbb<r\\x07\\x0e;\\x1ff\\x1f\\xbd\\xb3E\\xcf<\\x0f!A\\xbd\\xcaL\\n\\xbd\\xaf\\xb0\\x9d;\\x83\\xa5\\xbe\\xbc\\x00R\\x12\\xbdu|\\x04\\xbd3\\xc9\\xb3\\xbc\\x91{7=\\xe1\\xad0=&)\\xf7;\\x82B\\xb8;i\\xb5\\\"<\\x1f\\xba\\xaa\\xbc\\xfd\\xb81\\xbd\\x02\\xd8\\x08\\xbcz\\xbe`=W\\xa4\\x89\\xbc\\x83v\\n=Z\\xe3\\xdf\\xbc\\xde\\x924\\xbb-\\xdeu\\xbb5\\xfd\\x92\\xbb\\xf6\\xab7=\\x9d6Q\\xbd\\xc7B\\xfd<\\tUo:|\\xce\\\\\\xbcH[\\x93\\xbcK\\x9a*<\\xd9Na<\\xff\\xf5\\xff\\xbce\\x1b\\xbc<t\\x8a\\xd6<\\xe4\\xd3s<\\xad\\xefR\\xbd\\xcbg\\x18=k\\xdf\\xc2\\xb9=\\xebX\\xbd\\r+\\xe5\\xbb\\xff5g<\\xd6\\\"\\x8a\\xbc\\xa8\\xc13=`\\xef\\xe7\\xbc\\x88\\xec\\x879\\xa7d\\xab\\xba\\x95\\xdf}<\\xd3x\\xee\\xbb\\x82\\xdf4=[8\\x0e=9J7\\xbb\\x0c4\\xcd<\\xa9\\x96\\x9f\\xbb>_!=[\\xb7\\xe5\\xbc\\xa0\\xca\\xfd\\xbb3y\\x0b=S~I\\xbc\\xec\\x02E=z*\\x88;\\x81\\xbaU=\\xc3\\xd8\\x1c\\xbd\\xc2\\xdb4\\xbc\\x0b\\x11\\t\\xbb\\x8dR\\xab<\\x0e\\xe9\\x0b\\xbc\\xf8\\xce`\\xbd\\xb8\\x95>\\xbd1\\xde\\x16\\xbd\\xc6h\\xf5\\xbcl^\\\\=\\xa6 \\x14\\xbc\\x95\\xbfU\\xbd\\xae\\xafM=\\x9d\\xfb\\x1b\\xbd\\xd1\\xb1,\\xbc\\xee\\xef\\xe1\\xbaX\\x1d\\x11=j\\x95\\xeb\\xbc\\x04\\x8d\\xdb<v!\\xd4\\xbc\\xad\\x13\\x03\\xbd\\xbdT\\x87\\xbdG \\xc1=\\xf3h\\xc9\\xbd\\xcd2\\xce\\xbc\\xcd\\x1f\\xc3\\xbb\\xe4\\xaf\\xa6\\xbb\\xb3\\xc6.=\\x11\\x92\\xaa\\xbc\\xb7\\xe22\\xbd\\x10\\x1d\\x87\\xbb\\xb9\\x85\\xe6;\\n%>=*\\x88\\xb5\\xbd\\xed=\\x9d\\xbcH\\r\\x13\\xbd&\\xef\\x8d\\xbc\\x0f-?=\\xf6\\xf4\\x92<\\xbe(\\x98\\xbd\\xf0\\x1f\\xd0<\\xe6_\\xe1<\\xf0\\\"\\xb2;\\x7f5z;?\\xed\\t=\\xab\\xfa\\x04\\xbd\\x8b\\xe9\\xb3<|\\xfc\\x84<\\x0c>\\xa8\\xbb\\xb4\\xb7\\x91<9{e=4x3<0\\xadG\\xbdw\\r+=.\\xeb\\x95\\xbb\\xf6\\xd9@\\xbd\\xa0\\x8c]\\xbc\\xfd\\xd9\\x8c=\\xca\\xc93\\xbdkl\\x98=\\xc4\\xdc_\\xbd\\x85\\xaf)<\\x8b\\xa1\\x13\\xbd\\x96V}=\\x96\\xd8\\xeb;\\x7fV\\xa0<i)*\\xbdd.\\x88;\\xa2J\\x8f\\xbd\\x866\\x87=o)\\xfb\\xba\\x19\\xd6\\\"\\xbd\\x11\\xe9\\xcd<k=\\xef\\xbc\\x84\\x00\\xc4;\\xb2\\xfax\\xbc\\xeb\\x95\\x1d\\xbdH\\xc39\\xbc2o\\x88\\xbb\\xb6\\xf2\\xab\\xbb\\xfc\\x9f\\x9d;\\x02N\\x9b<\\x94\\xb3\\xcb\\xbc\\xde\\xe2\\x8f\\xbd\\xafa\\xd1\\xbbf\\xba`=5]8=\\xb4\\x94\\xdf<\\xa1D9<\\xdd\\nJ\\xbd\\xda\\xc5m\\xbd1\\xd1G=\\xd2\\xcf\\x8b\\xbd\\x82\\xd1\\xb7<Hp\\x82\\xbc\\xc4)\\x13\\xbdq\\xa1\\xc9=C\\x0c\\x04\\xbc\\x89U\\x8a\\xba\\xdfaf\\xbdm\\xde\\x13=J\\xc8\\xe9<&\\\"J=\\x9a\\xf7j\\xbd\\x03\\xd1,\\xbc\\xcb\\x94\\xfb<\\xdd\\xfd\\x1e=\\xff\\xbe\\x85;\\xf6\\xf0\\xa5<\\xbd\\x1a\\xd8\\xbb5\\x80\\x04\\xbd\\x0f\\x0b\\n=\\xd6\\x12\\xa4=~@R=U\\xb6+\\xbdL\\xb0\\xdf\\xbc8\\xf5\\xe3\\xbc\\xc6:\\xbb\\xbcL\\xd2\\x9b<c\\xd0\\t\\xbd\\x1b\\xbe\\x1a\\xbd\\xd9L\\xbd=\\xf8\\xc5\\x0f=\\x89\\x1e\\x1e\\xbd`\\xb7*\\xbd\\x88\\xd5\\x86\\xbd;M\\xca<\\xe9~\\xac\\xbcQ\\\"T\\xbb0re\\xbd\\xb8\\xf1n=g\\x06\\x9a\\xbdzR\\x8d<\\x9aq\\x82\\xbc2e\\xc3;\\xd6d\\xcb\\xbd\\xff\\xf2\\xc8\\xbc\\\"\\x7f\\xbd\\xbb\\x8a\\x9b;=1\\xb8\\xe8\\xbch*\\x9e\\xbd\\xcej&=80\\x8e<$:\\xaa\\xbc\\xfb\\xfd\\x19\\xbb-\\xe9\\xb2\\xbcf\\xea\\xa4=CO\\xe3\\xbbo\\xc2\\xa3\\xbc\\xbe\\x1d\\xd1;\\xfd\\x97O=\\xe8l&=\\xbf\\x8d]=y\\x958=<\\x02\\x0c<K3\\x12\\xbdbmk=\\xd9\\xc0\\xd5\\xbc\\x85Z\\x05\\xbd\\xda\\x8c\\xfc<`\\x1b\\xbc\\xbc\\x8e\\xde\\xe0\\xbcA[g\\xbd\\xb6\\xe6\\x9b\\xbd\\xf3\\xf8\\xfd;a\\xdf\\xf5<A\\xb3\\x87\\xbb\\x14\\x17Z=t`d<\\x08]\\x0f=\\xbfm\\xba\\xbc\\x1d\\x8a\\xa1<\\x12\\xfc\\x18\\xbd\\xa9\\xa2\\xcb\\xbc\\x92\\\"P=R\\\"U=\\xfe\\xa8v<J\\xc1X\\xbd}d$;\\xbfz\\xde<tX\\xdc\\xbc]\\xdf\\x1a\\xbd\\xc1\\xae;\\xbc\\xf3Uy\\xbc\\x9b\\xa0\\xa4\\xbc\\xac\\x0f\\x1e\\xbda,\\x1d\\xbd\\xfcY\\x04<D\\xef\\x9f\\xbd\\xf0\\x02\\xb0<%2\\x80\\xbc\\x84\\xd7\\\"\\xbb?\\xa2\\xe5;~\\xae\\x81\\xbc\\xf4J\\xde\\xbc7:l=\\xe9\\xde\\xed=\\xb2\\x0f\\xcb\\xbb\\xc0\\x977<\\xdd\\xee\\x1d=\\xdb!\\xa7\\xbc\\xbe\\xa3\\xc0;\\nw\\xe7<\\xbc&\\x81\\xbd+-\\x9e<\\x9ax2<&u\\x91=Q\\xbe\\x16<t\\xe3c;\\xba\\xc5\\x16\\xbd\\x95\\x07&=-\\xe7R<q\\xdb4\\xbd\\x950\\xd99t\\x9e\\x12=\\x05\\xb0l<^\\xdd\\x12\\xb9&`\\x0b\\xbd\\x8dI\\x01\\xbc\\\\xU=\\x8a\\xcb\\xa6\\xbc\\x0c\\xb8\\xaf\\xbd\\x89&\\x14\\xbd\\x98g\\xb4<\\xed\\x08\\x17\\xbc\\xde0\\xda\\xbc\\xec\\x01\\x9b\\xbc\\xa5\\xd1\\xec<GW0=S\\xe7(\\xbc\\xc8~\\xcc\\xbd\\x02\\xc2e=G\\nj=*\\n\\xde\\xbb\\xb2=3;\\xe34\\xbc\\xbc\\xa0\\xfc\\x9c\\xbcp\\xc5\\xd5\\xbb-C!\\xbd,\\xe2\\x86=\\xee\\xbd==\\xdaO\\xd1<\\x8d\\x12\\xee;\\xcb4H=\\x0f\\x01E\\xbc\\xa4\\x16\\xad<\\x12\\xdc;\\xbd\\xd5n=\\xbc\\x19\\xce4<\\x0c\\xa5\\xe4<\\xfc\\x97==sM\\x85<jT\\x8c\\xbdt\\xaf\\xb3=O\\x9a_\\xbd\\x10y\\x88\\xbcT(\\xb3\\xbct\\xb8\\xe4\\xbd\\xc3\\xdeA=\\xe8\\x12\\x0c\\xbc*V\\xe2\\xbb\\xea\\xd7\\x01\\xbb\\xefD\\x11\\xbd\\xa9\\x10\\x83=\\xf5D\\xa5=\\xae\\xd8\\xe1<\\xd3\\x81\\xb2<\\xd6\\x9c\\x99\\xbc\\xe6\\xf8\\x88<*9\\xbb;d\\x8d\\x1b<\\xfdy/\\xbd\\x8d$\\xaf9=\\xc9\\x8e<\\xd4\\x17\\xe0\\xbc5OD<\\xd4\\x868\\xba\\x90\\x90\\n<r\\xa8\\x03<\\x9b\\xa2\\xa6<v\\xb2\\x9f\\xbb(\\xe4T;\\xd1\\xeb\\x06<>Q\\x8c<\\xa2\\xf5w\\xbc\\xea\\xcca<\\xb0\\xf1\\xd2;\\x7f\\xa9\\xa0;\\x91e(:9\\x13\\x92\\xbd\\xba\\xcb\\xcc<\\xc3\\xa4\\x11=@\\x92\\xcf\\xbc\\xa5\\x89r;\\xf5\\xcdO\\xbd\\x8d\\xec\\x8b;n\\xe4\\x02=)\\x02\\xe7<&\\x18]\\t\\x00\\x18\\xb4\\xbc_\\\"\\x89\\xbd\\xf4\\xbb\\xd1<v.\\xbf\\xbc\\xc3\\xfe\\xc4<\\x1aQ\\xc7<\\x90Y\\x8e\\xbc\\x84\\xf7E\\xbd$\\xdaz=9\\x1e\\x18\\xbdJ\\x90K</\\xde]\\xba\\xd9\\xab\\x84\\xbb\\x92\\xef\\x14=5\\xe1{\\xbc\\xbb\\xa8\\xb0<<\\xb5=\\xbd\\x18{8<\\x90\\x199\\xbcb4\\x1c=\\\"\\x1fv\\xbb\\xfd\\xbcG\\xbd\\xfdl+=\\xc99I\\xbd\\xe2\\xa1><\\xebL\\\"\\xbd\\x91\\xa9\\x19:\\x81fa<\\xa8}\\xb9\\xbc\\x1a\\x16\\xa1<|9\\xf59\\xb1Y\\xa9;yz\\\\\\xbdB\\xdc\\x90\\xbd\\xdf5\\x01\\xbd\\xceR\\x8a<\\x91\\xc6\\xd8:U\\x82\\x8c<\\xa2\\xad0:o\\x97S\\xbcR2\\x1a\\xba\\x12;\\x9e<\\xbe\\xd1r\\xbb\\x86t\\xbf=)2\\xc9;\\xea\\xb9\\n\\xbd\\xe2\\x98\\xde=4\\xc2\\xa4\\xbcTHA\\xbd$I\\xa1\\xbc\\x1c\\xaa\\x99\\xbb\\xa2\\xbaU\\xbd5v0;\\xf9\\xf8\\xb6<;\\xff\\xc3<<\\xee\\x18=\\x17\\x08\\xc1<jl7\\xbcc\\x83\\\"=y\\xff\\xa8<\\x8a\\x89\\x1f=\\xca)[\\xbb$\\x1e \\xbc\\x0c\\x99\\xf3<)k\\x19\\xbd\\x87\\x9f\\xbf\\xbc\\xe4\\xfdT\\xbc\\xd9\\n\\xb3:|y\\xad\\xba\\xe7e|<\\xfe\\xad\\x90:\\xb1\\xe6Q\\xbc`\\x98\\x14=+D\\xcf<d@\\x9b<\\x93\\xfb\\x9f;\\xe6^\\t;\\xa2\\xfb\\\\=\\xc9;\\x1a\\xbd(@1=\\xb3\\xa7\\x82\\xbd\\xf8lL\\xba>!q\\xbd\\x91qp\\xbb.bd\\xbd\\xdd\\xc2\\x0f\\xbd\\x0f\\xb4\\xa0=\\x9d\\xf9^=5@\\x12\\xbdv\\xdd\\xf6<\\xd5\\x97\\x80;\\xe3\\xbaT=\\xc2\\x08V=\\xf1\\xfb\\xe2:\\xe3e\\x03=\\xb2\\xd0&=\\xcb\\x90\\x84\\xbc\\x0b\\xd7n<\\x95Z\\xe1;\\xd5O\\x07<J\\xc2\\x1f\\xbd\\xff\\xb8\\xa7\\xbc\\x89\\n\\xc2\\xbb\\x8ee\\\"\\xbdf\\xdf\\xe3<b\\x10\\xe7\\xbb\\xa7\\xd6q<)a3<\\xb8\\xbf\\xc5\\xbc\\xc1\\x94M\\xbd\\xf7\\xbb{<<y\\xb3\\xbc\\xcf\\x9d\\x83\\xbc\\xc2\\xc9\\xb8=\\x10z\\x0c=iu\\x04=+\\xea\\x1c\\xbc\\xbc\\x86M<\\xe3\\xe3\\x909\\x0e\\x83\\x8a\\xbd\\xa40X\\xbc9.\\x81\\xbao\\x16\\x85<t\\xa4\\xfe<EE.<\\x16\\\\\\xe4\\xba\\xa9\\x87\\xb7\\xbca\\xdc\\xc3<Z\\x815=SUA\\xbc\\xfbA\\\\\\xbd\\xe9\\xf2z\\xbd,p\\xdf;\\xe0,==\\xf6\\xca\\x81=f\\n;=dq\\xd2<\\xc5?o\\xbde2A<>\\x81q\\xbc\\xd2\\xceE\\xbc\\x9cR\\xe0\\xbc\\xb6}\\x0f\\xbdU\\xe6\\xe3\\xbcd`\\x7f;\\x84,;\\xbcL1\\xbf\\xbc\\xe9)\\xda:\\x84\\t!=\\x17\\x0c\\x08\\xbc\\n\\xd3\\\"\\xbd\\n\\xb5\\x82;\\x8bj\\xf5\\xbc\\x15a\\xf7<Q&$=\\xb5T\\xf3<$R\\x0e\\xbd\\xc27`\\xbc\\xf3\\x07\\\"=\\xe5\\xdf\\xcf<=\\x12\\x82\\xbc\\x04\\xebq=;\\xf9\\xa3<\\xf4i\\x1d;\\xe1\\xb9O<\\xee\\x1c2<a\\x96\\xc1\\xbb\\x91\\xeb\\x1c;@\\x95\\x82=;4%\\xbd\\xe6\\x05C=\\x90\\xdf\\x0c\\xbcOX\\xb7<H\\xfe\\x1f\\xbd\\x89$\\x7f<\\x9b](=^\\xb6\\xdf\\xbb\\x00SC;_!\\x0e=\\xff\\x1dL;dS\\x9a\\xbd\\x99\\x00J;Y\\xa1\\x85<\\xa7i\\xf2;\\xdeN\\xdd\\xbbS\\xc9\\xe7\\xbb\\xbb;\\x8f\\xbc\\x86B\\x82\\xbd9\\xbd\\x00=o]5=*\\x10\\x0c=\\x06\\xfdm<\\xbf\\x11\\x07\\xbc\\x98+~\\xbb\\xb1\\xa5|\\xbd\\x04@\\x8f\\xbd\\xd3\\xd0\\xcb\\xbc\\xda\\xe7r\\xbcds\\x01\\xbcP\\xf7\\x93< \\x99V\\xbd\\x1cP\\x07=\\xd0r\\x0f\\xbd\\x97\\xbdZ\\xbd<y\\x01\\xbd\\xd8e\\xe1\\xbc\\tp\\xc1<\\xb8\\x86\\x85\\xbc\\xd1S\\x93\\xbc\\xbc\\x9fR\\xbdG\\xb9t\\xbc\\x05\\xbb\\xcf\\xbc<\\xf3\\xd2<c\\x06\\xee;\\x8f\\xbe}\\xba::m<\\xeay =\\x0e\\xabi\\xbbe\\xe1g\\xbd\\xf0D3\\xbd\\x9c\\x83\\x8a\\xbb^t<\\xbd\\xd1\\xd5\\r=\\x18\\xca\\xe4<\\x96\\x12\\xb0=\\xddsP\\xbc\\xa1*\\x00\\xbc\\xae\\xcf\\xa5\\xbc\\xd7\\x9a\\x0f\\xbd\\xe2\\x8e\\x92<(\\xf0s<hc\\x1a=\\xab75\\xbd{\\x15\\x0b\\xbd\\\"\\xc0\\x17\\xbcE\\xb9\\r\\xbd\\xcfw\\x82\\xbcq\\x80;<_\\xe3\\x80=\\t\\x00\\xb6<\\xa3\\xd3\\x83=\\x83\\xed\\x82=^\\xb6\\xe2;\\xdc\\t\\xe3<Dv\\xa1<?6\\x1f=\\xfd=B\\xbdq5\\\"=\\xd3\\xec\\n\\xbd\\xa0\\x9d@<\\xeaO\\x05\\xbd:\\xd6R<i\\x8f\\xdd;\\x93\\x80\\x90=\\xdb.\\xa7\\xbc)g\\xf4\\xbb\\xdb\\x0c\\x17\\xbd\\xe0\\xab\\x85\\xbd\\x94p\\x06\\xbcS\\x1a\\xc4\\xbd@\\x0f\\xf7;\\xbc\\x89\\xa1;9k\\xff<\\x1eo\\x12=\\xce\\xa5\\xc7=\\x94\\x8e\\xac<-k\\x16=^\\xf3l=\\xbe\\x06\\x80\\xbc\\xb2b%<\\xc3\\xb7K\\xbc*p\\x9c<\\x959\\xaa\\xbb\"\nHSET bikes:10059  model 'Tethys' brand 'Velorim' price 4970 type 'Kids bikes' material 'aluminium' weight 10.6 description 'The latest kid-specific bike brand on the scene, this brand aims to offer high-end, kid-friendly bike geometry at an economy price point. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\x84\\x15\\x88\\xbbg\\xf9\\x8b<S\\x1cn\\xbd1\\x95\\xc3\\xbc\\xd1\\xfb\\\"\\xbc\\xb5\\xec\\xb9<V,\\x8f\\xbc\\xb5gY\\xbc=v\\xc1=\\xb1\\x8d\\x83<6\\x90\\x00<\\xadq\\xcb<\\x0bw\\x01=\\x87\\xd0\\x9b\\xb9;\\xa0\\xfc<\\x0c7\\xd2\\xbci\\xe7\\x82<XE|\\xbdz]W;\\xaa\\x8d\\xab\\xbdpi\\x0c=\\x05\\xf3\\x92\\xbd\\xe2\\xdb>\\xbc\\x03\\x8b\\x1e\\xbcfv\\xb5\\xbc\\xab\\x86\\x18<\\xbf^\\xfc<\\xc6\\x04`;6\\x0c\\x9e\\xba\\xb7\\xd0\\x06>\\xf95\\xb9\\xbc+\\xc0\\x16\\xbd,B\\xc5<\\xce\\xaa\\xcf<b\\xea\\x80\\xbc\\xea\\r\\xab\\xbc\\x0bLV=\\x07\\xf4\\\"\\xbd\\x96\\x97\\xb8\\xbc?4E;\\xc3W\\x81=\\xf9\\xf7\\x08\\xbc\\t\\xd2P\\xbb\\xa96(;\\x86\\xfc\\xfc\\xbc0\\xe6s\\xbc~\\xfe\\xa3<\\xa2g\\x92\\xbc\\xa4A\\x1b\\xbd\\xa0\\xbd\\xec\\xbb2u\\xee\\xbb\\xab(\\x89\\xbc\\xf6no\\xbd\\xb3\\x93\\xe4<7\\x96\\x01<g\\xed\\xa6;\\xae\\x81\\\"=FN^\\xbdi\\x93\\x01\\xbd:u\\x89=\\xf4\\xb5\\xff\\xbb#]\\xab=\\x19\\xfc\\xac<\\x17f|=\\x0f\\xb8l<\\xf6w\\xcd;\\xed[\\xe8\\xba\\x92\\xceM\\xbd\\xad\\x99\\x88<\\xa4\\xc9\\x1a\\xba\\x90\\\")<6\\xa51\\xbd)\\xdf/\\xbc\\xf5g\\x81\\xbc\\xadP><j\\xfe?=z\\xf3\\n=l\\x94\\xf1\\xbc\\xef\\x1e\\xa1\\xbd6\\xb9\\xe0<\\x12\\xd0T=\\xcb|\\xf7\\xbc}\\x80\\xab\\xbc\\xa8\\\\k\\xbd\\\"\\xa5\\xdc\\xbb&~\\xfd<\\x10\\x07\\xed<\\xc4N\\xa8\\xbcn\\x14k\\xbb\\x0c\\xf3\\xa0\\xbcL\\xc8\\xe9\\xbc\\x1bp,\\xbdl\\x9d\\xea\\xbcF\\x02\\x12=\\xaf\\x13\\xeb;\\xf4e,=L\\x9b\\x9d\\xbd\\x97\\x8cM\\xbd\\xcd-\\x19=\\xed\\xd1\\x80\\xbd\\xcf\\xf2E=\\xe8^J<\\x03\\xa0\\xe4<h\\xa2\\xc5<\\xa5\\x85=\\xbd\\xa6\\xea\\xe4=4\\xf6\\x9b<CT3<\\xb4\\xee+<\\r\\xee\\x9b\\xb9\\x04N[<\\x13\\x04w=\\xf2?\\xab\\xbc\\xe5\\xf1\\\"\\xbc!\\xcc\\xa6\\xbdN\\xb4\\xeb\\xbb\\\"\\xe4f<\\x86\\xa0\\xb7<p7k<MO\\xb7<\\x9e\\xa2M=S8\\xe7\\xbb\\xfc\\xa0W\\xbd\\x8b\\xaan<\\x14\\x02\\x84=\\xa0\\xa3\\xb0\\xbc\\x11\\xdds\\xbb\\x1f\\x8aR\\xbd\\x99\\r\\xdc\\xbc(\\xf5#\\xbdBR\\x0f\\xbd\\xc0\\x12\\x10\\xbd\\x12\\x17\\xa7\\xbaT+\\x0f=\\xd6\\xb0\\xe3<\\xdf\\xd1\\x8f<\\x8c\\xe8\\xf7<\\xe1\\xe7\\xce; \\x03\\x86<w\\x82z\\xbdj\\x86\\xa2\\xbd\\x03\\xc5\\x9a\\xbd\\xe6\\xdd\\x07=H\\x9e\\x93=\\x93\\xa8 \\xbd\\xf5]\\xb1\\xbb\\xa9\\x89n\\xbb\\xe1\\x03O\\xbb\\xc4/\\xf6\\xbb\\xecz4\\xbd\\xac\\xc1m\\xbc_-\\xf7<Si\\x9b\\xbcl\\xdc(<\\xc0v&\\xbc\\xbc\\xcd\\x0c<:\\xde8\\xbd}\\xe7`;\\x16\\xce\\x1c=\\x8a\\xa8.\\xbd\\xba\\xd6X=?S\\x86\\xbb\\xa3\\xb0\\xcb\\xbc\\xcdDg\\xbd\\xc6f\\xf4<\\x12|\\x02<\\xcf\\xf6\\x04<\\xc1h\\x7f<m\\xe3\\xe4<\\xfd\\xcc\\x86\\xbc\\xa6z0\\xba\\xc3\\x87\\x88=\\x04\\xde\\x90;6\\xbe\\x8a\\xbc\\x99\\xeb\\xb8\\xbb*\\x0bM\\xba\\xdc\\x89u\\xbd\\x88\\xbd\\xfb<\\x9f\\xd4\\xb1<\\xdf\\xbc,\\xbc\\xbe\\xdf\\xd4\\xbc\\x02\\x10u=#Dr;\\x8d\\xf9_=>\\x1e\\x89<\\x1273\\xbde\\\"s;\\x14B\\xcf<\\x8d\\x8cQ\\xbc\\x1c\\x94\\xa7\\xbb\\xcb\\xf4B\\xbd\\x854B=^S5\\xbd\\x15\\x19\\x98<\\xf22\\t\\xbb\\xcb\\xc6\\xa4<9x\\xf7;F\\x96*\\xbd\\x9b\\xe6\\x8f\\xbc\\xdd\\x84m=;)\\xaf\\xbc8\\xe8p\\xbd\\xae\\xc4\\x8d\\xbdyx\\x14<`Va\\xbd>\\x95\\xc4;L\\x9a\\xfe\\xbbj^\\x1a\\xbdmdS=\\x86g&\\xbd\\xca\\xf7\\xd1\\xbc\\x9b\\xafA\\xbd`{\\x03=\\xa5\\xdau\\xbd]_H<\\xb3}\\x11\\xbd\\x80a\\r;\\\"\\xada\\xbdI\\xa9\\\"=B\\x80\\xb8\\xbd\\x87D\\xb5<\\xa7\\xd3\\xcf\\xbc\\xd8k\\x97\\xbde\\x9b\\xdd\\xbbbb,\\xbdYu\\xaa\\xbb^\\xd6\\x1d=\\x17\\xab\\xbd\\xbc\\xbeXP=Q\\x0b\\xd3\\xbc\\xeb\\xb0\\x8c\\xbc%!{\\xbb{q\\xa8;Y?\\x1b=\\x03\\xd5\\xb2;30\\xa2\\xbcyr\\x08\\xbd\\xe2\\xb0\\x86<\\xa3\\xa8\\xb2\\xbc9~\\x8d<n\\xb8<=L-v<)\\xc8\\x89<9\\xde\\xcb\\xbb\\x98\\xe5\\x98<\\x85C\\x8c<\\xd9ND<\\xf8\\x1a\\xfa<{\\xc3\\x83\\xbd\\xefO\\xc2\\xbc\\xd9\\x9f\\xca<\\xda\\x8c\\x8b\\xbd\\x96Xw\\xbb\\xb46\\t=\\xa2\\xbe/\\xbd\\x9a\\xcb\\x80=\\xc68\\xf9\\xbct[?=\\xdfLb<\\xe9\\xf8<=P\\x19\\x0c\\xbc\\xc2\\xa4(\\xbd\\x1bt:\\xbd)\\x8f\\x00=a\\xcd\\x8f\\xbc\\x12\\xf0\\xf6=\\x87\\x08]\\xbc?.&=X\\xa1%=\\x92hQ<\\rN4\\xbb\\xf7y\\x85\\xbc\\x05\\xf7/\\xbd\\x08i\\\"=\\xd6\\xc3\\xda\\xbcB\\r\\xd7\\xbc\\xbb&\\x1b=\\xcaBa<0\\xf8\\xc1<\\x1ec\\x9d\\xbds\\xacz\\xbc\\x08\\x89\\x8d\\xbb}\\xb2B;\\x98J\\xa2<`\\x92G<\\x89w\\xa2\\xbc\\x00\\xe7&\\xbc\\x13\\xaf\\xdd;\\x9f^\\xb6\\xbd\\x8d7W=\\xd3\\xa7\\xac\\xbc/~\\xf5\\xbc\\xb2l\\xe7<\\x85\\x0ey\\xbc\\xa6p\\x86\\xbb\\xb0}\\xc4\\xbc\\x19\\xd1\\xcf\\xbcJ\\x97\\x0f\\xbd,\\xcaQ;\\x0574\\xbd8\\xd4\\xf9\\xbc+\\xbb\\xbd\\xbbY\\x94 =\\x90\\xeb\\x16\\xbd\\xe8\\xde\\xba<zz\\x03<\\xe3h\\x19=\\xc6\\xabv\\xbc\\x9eD\\xb5;\\x93\\t\\xad;\\xc4\\x00\\x86\\xbd:\\x00\\xa7\\xbc\\x00K\\xbd\\xbahV&\\xbd\\xc6:+<\\x8e\\xd3\\xe4\\xbc_\\xbc\\xa7\\xbc\\x96\\xc6\\xb9=\\x11{\\xb6<\\xdd\\x1d\\xe7<v\\xa2v<N2\\x1d\\xbd\\xa0I\\xda<N\\xb33\\xbb\\xd1\\x1d\\xb6<\\x19\\xfb\\x1f;c:\\x81<_\\x9f\\x1e\\xbd\\xf7\\x11A:S|:\\xbd\\xbe\\x84/\\xbd\\xae\\x02\\x10\\xbd]\\xf1\\xa8\\xbc\\x85e\\xe0\\xba\\xeb+:<\\xd4\\r\\x04=iZ\\x8b\\xbc\\xf0\\xac^<\\xeb8\\x87=\\xd5\\xe1\\xd6\\xbc\\xc8\\x04\\xb1;y\\x13\\x1d\\xbc&a\\xa4=\\xec\\x16\\xe8;\\xcd\\xdf5\\xbc\\xf3\\x00\\x17=\\x04\\xaa-=\\xf2\\xbc\\xfe;|\\xb5P=\\x14r/\\xbc\\xb3\\xbc\\xcb;}\\x9b\\x01\\xbb\\x1dZ\\x96=\\xa4\\x99\\xcb<\\xac\\xb3\\x07\\xbd\\x16\\xb6o<\\x91#\\xb39?\\xe04\\xbd\\xce\\x04\\x90\\xbc\\xbd\\x0f\\x87\\xbd\\x8f\\x86\\xb9\\xbb\\x19\\x03\\x1f=q\\x99K=\\x1b\\xad\\xb3<\\x8bT\\x81\\xbb\\xaf\\x91\\xe9\\xbb\\xb9\\xb2I\\xbc\\xba\\x95/\\xbb&\\xb1\\x04<x\\n\\x03\\xbc\\x84\\x8b\\x94<Zwj\\xbc\\xd2\\xbe\\xe7;P\\xba\\xed<\\xa5c\\xc5\\xbb\\x82*J=\\x80\\x9f;;S8\\xc0\\xbd\\xf8\\\"I\\xbc\\xc8\\xf6\\xc3\\xbb\\x83c\\x19\\xbd~\\x0c\\xa6\\xbc\\x8c\\xaf\\xe0\\xbc\\x88\\xc9\\xbc\\xba\\x85\\xfa\\x8c\\xbd\\x99n\\xa0<\\xd9\\xc6v<M\\x91!\\xbdL1\\xe6;\\xd6\\x1e7\\xbd+E\\x81\\xbdW\\xfa\\x98=de\\xcd=\\xcb\\x8b7\\xbcC\\xc6c;\\xcc\\xfaj\\xbc\\xd8\\x06\\x1d\\xbd_\\x8c\\xd1<\\xd5!o=\\xf1*=\\xbd(\\xa3<\\xbdf\\xb4\\x97\\xbc\\x9a*g=\\xa4&\\xca<y\\x91\\x07<\\\\\\x01o\\xbd\\xc0\\xbcp<_\\x05\\x13\\xbd\\xefd\\x1a\\xbc\\xa7Y&=q\\x88 =\\xaa\\x02\\x0b\\xbdo\\x07\\x17=:+\\xdf;\\xaf<\\x07=\\xb2o\\xa0\\xbc\\xc3\\xf4\\xba<\\x11\\xeb\\x92\\xbd\\xe1)\\x0f\\xbba\\xf4\\xee<\\x12\\x89\\x04\\xbd\\xa0\\x984\\xbd\\xeb\\xea3=\\xab\\x12\\x16=\\xce\\x81\\xa6<Po\\x99\\xbc\\x1a\\xd0\\xaf\\xbc?\\xf7\\x89=J\\xe3\\x08=<Y\\xf5\\xba\\x13L\\xf4<\\x052!\\xbd\\x15\\xf0F\\xbd\\xb3\\xe2\\xaa\\xbd\\xe8\\xb4\\x18\\xbc\\xc4\\x17\\x9d=\\xda\\xb5\\\"=[\\xe2t<\\x94ky\\xbcF\\x8a\\xfc<\\xdf\\xfb\\x00\\xbc\\x9f;\\x06\\xbdas@\\xbc\\xf6t;\\xbd\\x94u~<F\\xb5z<\\x1d&\\x0b=\\xcb)X\\xbc\\x9c\\xa3\\x0c\\xbd\\xfe\\xc7&<\\xea\\x82v\\xbd\\xaf\\xb46\\xbd\\x02\\xa4=\\xbc\\xc5O\\xcb\\xbdw\\xf1s;L\\xedt\\xbb;\\x91\\x0f\\xbd\\x97\\xe9\\xfa\\xbcO\\x14\\xf8\\xbc\\xee\\x82@=~\\xdd\\xb1=\\x17b~;i\\xf6c=\\x81c|\\xbc\\\"\\xda\\x91;t\\xf9\\\"=\\xd6\\xc8\\xdc\\xbc\\xa7\\x00\\x05\\xbd\\x98N#\\xbd\\xe2o\\xae=!)E9\\xd8^\\x82<\\x89J!\\xbd\\xfe%\\xd9<\\x17\\xad\\x7f\\xbc\\xeb\\x80\\xfa<\\xa4u\\x8d\\xbc\\xcc\\xe0C\\xbcL\\x81j=cW\\x84\\xba\\x97T#=\\x0f<\\xdb\\xbc\\xfdF\\n:\\xfe\\\"g;\\x8c\\x81\\x1e:\\xb7E\\xd2\\xbd\\xc7\\xa3\\xc8<\\xe08\\x00=x\\xa3w\\xbc\\x02_\\x8b<\\x08p~\\xbdc\\x86\\xb3<\\xc5\\x01\\x9e=\\\\\\xb6\\xa1:W\\xf2d\\t\\xe7d\\x1f<r&\\xf9; \\xec\\xd1;,U\\x1d=\\xb8G9\\xbb\\r\\xa8L=p\\xdc\\xe1:^\\xa6\\x03\\xbc`\\xe6\\xee<v\\xc8\\xac\\xbc\\x1c\\xecm=\\xcaHP<\\x14i-\\xbc\\xfe\\x83\\xe4\\xbbQ#\\xec\\xbc `I=\\xaf\\xe3\\x95<[E\\x19\\xbd\\xd9H\\x87<\\xffM\\x88=K \\x89;#{)\\xbd#\\x10U<\\xf4\\x81\\xea<\\x91=\\x16=#\\xe6\\x08=\\xac\\xbd\\x86;\\x96\\xa9\\xdd\\xbb\\x15E(<\\xc62]\\xbc-\\x94\\xfc<}z\\x8c<\\xc5\\x03\\x9e\\xbc\\xc8/\\xf4\\xbcs\\x8d\\xc7;/&\\xf8:\\x81L$;\\x06\\xe7T=\\xf1\\xf5\\xb6<\\xc4\\xf2\\xb2\\xbc\\x03N\\n\\xbc\\xf8\\x86\\x83\\xbc\\xa30\\x97<\\x86\\xd9\\x0b\\xbd\\xfe.\\xa2;\\xce?\\xd4\\xbb;\\x9b\\xaa=]\\xa1]\\xbc\\xe7\\xf3\\xf8\\xbc\\xf3\\\\\\xda<AeE\\xbdRT\\xc0\\xbc\\x1bp\\x17=j\\xaf\\xc7;/\\xe5\\xa1\\xbd\\xe5\\x81\\xc8<3\\xd3\\x9c:u\\x10)\\xbc%D\\x14<\\xdde~<\\x0b\\xe4\\xf9\\xbcz\\xcd\\xd5\\xb9?\\xb6F=\\xa3\\xa4\\x13=no\\x06<\\xf7\\x9d\\x89\\xbc8\\x90[\\xbd\\xb9^%<_\\xaaO<Q\\xfd\\xee\\xbc\\xf3H_<\\x83/\\x0e\\xbdF\\xbd\\x13\\xbd|Z\\xcc<\\xfb\\xeb\\x1d=a\\xe9\\xf9\\xbc\\xec`\\x0b\\xbcD\\xa0\\x0f=,\\xf2#\\xbd!\\x9f\\x80=m\\x08\\x92\\xbd5\\x9bp<v`\\xae<c_\\x13=P\\xc5)\\xbd\\xc7,\\x15=\\xdd\\xb5\\xd3<\\xe64X=\\xf4\\xfd/\\xbc\\xef\\xb6\\x10=\\xef\\xff\\x8e\\xbbQ\\xbf\\x0f\\xbc\\xaf\\x14\\x03=y@l: \\x12\\xb0<$\\x9b,<\\x9d\\xbd\\xac\\xbc\\xba<9=@\\xf2\\x0c\\xbc\\xb8\\xe6\\x89<\\xfa\\xb4*\\xbc\\x8f\\xe1\\x8d\\xbc\\xab\\x10\\xc3<\\xd4\\xbbO<\\xb5P.=\\x11>\\x05\\xbdM\\t\\xc3<\\xb7G\\xc5\\xbctn(\\xbdH\\x9a\\xb1\\xbd\\xde\\r9=mk\\xbb;\\xb0\\xf0R\\xbc\\xc5\\x81_=\\xb6s\\xbd=\\xea\\xf6i\\xbb\\xd0\\xd5\\xb7<\\xa2t\\xda\\xbc;\\xe80\\xbdj\\xa3N\\xbc\\xfe\\xba\\x10=\\xef\\n\\x88\\xbb\\xbe\\xb48=\\xf6\\xa3\\xdd<{\\xaf\\xef<I;\\xf5<U)d\\xbd\\xca\\xff\\x18\\xbc\\xe8\\xd0G=\\x04\\x94-\\xbd\\xc2>\\xb4\\xbc\\xda\\x1f\\xc2\\xbd\\x14\\x86\\xa2=\\xfd\\x92f<\\x82\\x944=\\x11\\x8d\\x0f<\\xc8\\x89b=\\x11\\xa1\\xea\\xbc\\x11M\\xe7\\xbc\\xf5\\x8e)\\xbc\\xa3>\\xd7<6\\x08\\xfe\\xbci\\x942\\xbdm3\\xd7<\\xa9)\\x7f\\xbc`a\\x10=*\\xe0)<\\xc9\\xe6\\xe59R\\xfd8=\\x84)\\x90\\xbbA\\x11\\xb9\\xbcb\\xfd\\x88<\\xeap\\x83\\xbd4J\\xde<\\x13\\x9a\\xcd<@i1<\\xf5\\xf5\\x85\\xbd\\xe8\\xd9\\xa9\\xbc\\xab\\xf4\\\\\\xbbK\\xe9_<q\\xec\\xf3<\\xa8\\x8c\\x10<:<\\xbb;\\xd9\\x86\\x13\\xbde\\x0f\\xf8\\xbb\\xc0\\xb31\\xbd\\xeb\\\"\\xfc\\xbc\\xb2\\xc9T\\xba\\xe5\\x86\\x9a=pN\\xbc;\\xfe\\xdaZ<r\\x87\\x93\\xbb\\x84b\\x84\\xbc\\xeaT\\xb4\\xbaf4I=Vn\\x9e=3\\x02\\x93\\xbc\\xa7k?<\\xf2J\\xa9<G\\x86\\x9c\\xbc\\x0f\\x0b\\x05=\\xc4\\xd1\\x83\\xbd\\xa0\\xd1B\\xbd\\x14\\x97\\x1e\\xbcar&\\xbdB\\xa3\\xd6\\xbcp\\xd0\\xc1<)\\x98\\x00<\\x1d\\xea8=J\\x8c3\\xbd\\x9d\\x1b\\x9d\\xbb\\xe5\\x15\\xfa<A\\xc7\\x82\\xbc >N<S\\xac\\x82\\xbd\\xba\\x93/\\xbd\\xf2\\xa2B\\xbd\\x17\\x8bX\\xbc\\xc8\\x0f\\xc9\\xbbr\\x88/;\\xd0i2\\xbd\\xb9\\xd9n<-\\x99\\xcf\\xbb\\xbd\\x04\\xbc\\xbdK\\x90\\x91<\\x02\\xc7\\xd5<O\\xb7\\xfa\\xbc7>\\x90\\xbd=\\xd5s\\xbb/\\xe9:\\xbd\\xd4}2:{\\xdf\\x07;\\r\\x86T=<\\xeb\\xac<D\\xb8\\xdb\\xbb\\xec\\x1e\\x8e\\xbc\\xe3\\t\\x1b\\xbcCA%:\\xd2w\\x0f\\xbd\\xa2\\xabn\\xbd\\x87\\xa6]\\xbc\\xfc\\x056\\xbc\\xda\\xefs;%z\\xb7<\\xff?\\x1b==\\x98\\xdd\\xbb\\xf9-\\x14=\\x04\\xf0\\x07\\xbd\\xd2C\\xa6\\xbd.\\x8cb=bR\\x8b\\xbc\\t\\x81\\x18;\\\"9t<?+\\xcf\\xbc\\xbbK\\x02;C\\x03m\\xbd\\xd3bL<U^=\\xbc\\x9e\\xf8;=h\\xbbw\\xbc\\xcf1\\x06<\\xcd\\xe1q=\\x7f\\xb6\\x17<-$E<Oq\\xb1\\xbc\\xbc\\x06\\xa1\\xbc\\xed\\x13\\x94\\xbb\\x82pF=F`>\\xbd\\xe6\\x8c\\xdb\\xba|Qf=\\xde#\\x07=\\xdca\\x0b<\\x1c$\\x80=\\xad\\x1f\\xdb\\xbc\\xb1\\xfe\\xbf\\xbc(R4\\xbbl\\xf8\\x00\\xbdd$\\xc1<\\x9f\\xe9\\x9e\\xbdt\\x12\\xaa<\\x8e\\xe6o\\xbc\\x8eB\\x7f=i5\\x99=\\xe5\\xb6\\x89=\\xe1\\x98P<\\xb9\\xdbO<K\\xcaS=\\x14\\xd5\\x95;\\x83+\\x07=\\xc2\\xc7C\\xbd\\xa4\\xbc\\x08<Y\\xdd,\\xbd\"\nHSET bikes:10060  model 'Iapetus' brand 'Peaknetic' price 3276 type 'Kids bikes' material 'full-carbon' weight 13.9 description 'For shy or agressive riders, paved or dirt trails, this bike boasts kid-friendly geometry and strong quality parts at a minimal price point. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\x05\\xef\\xd0<\\x91%\\xb0<\\x18\\xb5\\x9d\\xbcT\\xa5\\xf3<-\\xea\\xdc<\\xba\\xa8\\x02=\\xc1\\x7f\\xeb\\xbc-\\x1aH\\xbd\\xfc)\\x8b=!2;=%l\\x93<\\x93\\x91\\xa0=^\\x1c>=Y\\xc0\\xdd:\\xc6\\xb6\\xb7=m\\xaeB\\xbc\\xf3L\\x86\\xbc\\x0bzz\\xbc\\x9f\\xff\\xa4\\xbc{X\\xa7\\xbc\\xbba\\x08=[\\t\\x03\\xbd\\x99C\\xc1<\\xeb\\xdf\\x1f\\xbd\\\\k\\xc8<\\xc3\\xd5\\x81<\\x9aP\\xff<~)A<x\\x98\\x90\\xbcU(\\xcd<\\\"|\\x05\\xbdpB\\xd6<\\xfb\\x86\\x97;d\\xd5\\x15=\\xe2|\\x90<\\xbaU\\xa9<\\xef\\x94$\\xba{\\x151\\xbd\\x1fb\\xc8;\\x11L\\xd1\\xbb\\xcd\\xff\\xb0<\\xc0E\\xb7<\\xad\\x1c\\x00=\\x16\\xec\\xb8;C\\xf9T<\\x8f\\xd7/\\xbd\\x81c\\xd7=\\x0b\\x98\\x07\\xbd\\xaea<\\xbd\\xd1$b:\\x83\\x035\\xbd\\x9eKv\\xbd\\xaa\\xa3\\xce:q\\x06\\t=\\xf5\\xdc\\x96\\xba\\x0e\\x83:<\\x91\\xe8\\x84;\\x9b<\\xc7\\xbc\\x00\\x02l\\xbdF\\xae\\x94=M\\x9e\\x0e=r\\x81\\x1c=\\xb8\\x99\\x1e=<\\x023=\\x89\\x0f\\xd4<e\\x8f\\x84\\xba~\\x89\\xdd\\xbcF\\xcfC=\\x1bh-\\xbcv\\x1d\\x17=\\x19\\x8d\\xb0\\xbbC3\\xa6<z\\xa1`\\xbdV\\x0e\\xf3<Cf)\\xbc\\x07+\\xb9<\\xe0\\x1b\\n<\\xbc\\xf25\\xbd\\xcfO}\\xbd\\xe4\\x18\\xc7\\xbc&\\xaeh=\\x98\\xabA\\xbd_\\xde\\xda\\xbc+\\xf7\\x12\\xbd\\x03\\x83\\xa7\\xbcU7\\x87<\\xa9\\xc0\\xe0\\xbb\\xfa\\xec\\xcf<V\\xd1\\x8a<\\x82\\xa7\\x90;\\xee\\x06{\\xbc\\xd7 \\x7f\\xbd,\\x96\\xb3\\xbc\\xf2\\xa6\\xd4\\xbc\\x8d\\xc7\\xbf<\\xd0\\xaa\\xd1<\\xa4\\x1d\\xb2\\xbb\\xf8\\xa4\\xbd\\xbd\\xca\\xfc ;\\xd8(\\xfd\\xbc\\xf9\\x18\\xa4\\xbb\\xd5^\\x89\\xbb\\xacc\\xbe<#\\x9b\\r\\xbd\\xb7\\xf5\\xf2:\\x80\\x11\\xa0=\\xdcL\\x95\\xbc<;\\x98<\\xf5v\\xcb7:cD;\\x9e9&<\\x89\\xaeK<\\xad\\x97\\xaf<af\\t\\xbd\\x1cy\\x17\\xbd\\xa2qT\\xbdr\\x8aF\\xbc\\x17\\xff\\x87\\xbc\\xf1\\xad\\xba<z2\\xc8;\\x9a\\xc3\\xbd<\\x0bZ\\x04<2\\x9c\\xcd<L\\x95\\xd2\\xbb4\\x0b:=\\x9e\\x05\\x10\\xbdZ\\x83\\x85\\xbb\\xc5\\xc6\\x94\\xbd\\x96\\x93\\x8e\\xbcP\\xd2\\\"\\xbd\\xa4\\x90\\x8c\\xbc\\x06#\\xef\\xbc\\\"\\xa3\\xbf<\\xa6\\x11\\x18\\xbd\\xb7w\\x1d<\\xab\\xb6\\x15=#\\xa4\\xa3\\xbci@_\\xbcL\\xb5\\x95<\\xe7\\xc5\\x13\\xbd\\x8b.\\xc5\\xbc\\x81\\xb6\\x91\\xbduE.=\\xda\\x1e\\xa0=*\\xc7\\xa0;\\xedz-\\xbc\\xa8p\\x03\\xbc^\\xd0\\x18<}\\xff\\xde\\xbc\\xc0\\xb9U:H\\xeb\\xea\\xba\\x9f\\xbd\\x85=\\xb4\\x0b\\\"\\xbda6h=\\xb5\\xe8\\x0b\\xbd\\\\\\x17\\\\\\xbdR\\xe7>\\xbcL\\xaf\\xf0\\xbb\\xbb\\x054\\xbc;\\x1c\\xa5\\xbd \\x07\\x99=f^\\xfc\\xbc\\xaf\\xd1\\xff\\xbc\\x12\\x04Q\\xbd]\\x7f\\x04:\\x90\\xd6R\\xba\\xf7?Q\\xbc9\\xfd\\xbe<s-u=\\xd0\\xd6\\xb7\\xbc\\xdf\\xddx\\xbd@\\xfc\\x12=\\x03\\x96\\xd2;\\xe2\\xdb\\x92\\xbc\\x94\\\\\\xf6\\xbb\\x19\\x84\\x1e\\xbd\\x07gO\\xbd\\xbce\\xd2<*\\x97\\x1e<8\\x1f\\xa3<\\xc7\\xce\\xd3\\xbc\\xa2\\xc5\\xe4;\\xfe\\x8a4\\xba\\xc0\\x96\\xc3<\\xe8\\xdf\\\"=\\xe8\\x08\\x19=eC\\xc6<\\xaf\\x13\\xb2\\xbcgD\\xc9<\\xdf!\\x9b\\xbc/OI<\\xc1V\\xc3<\\xf9\\x0f\\x14\\xbc0G\\xee<\\xfd~\\x83</\\xacf=\\xca\\xe4m\\xbd\\xed)X\\xbdd\\xc7\\xc2\\xbd\\x8e|\\x80=\\x84\\xf0\\xb6\\xbc%k\\xab\\xbdl\\x18\\x81\\xbd\\x99\\t\\x9e\\xbcl\\x03\\xd9\\xbc\\x87\\xa1z;MSe;\\xe5S4\\xbd7[\\xa5<\\xe7y\\xc5\\xbb\\xad\\xaa\\xb9\\xbc\\xd6\\x13\\x9a<\\x18JZ<\\x89\\xd0?\\xbd\\xdei4\\xbb\\x03\\x19\\xe0;\\xe6\\xcf5;\\xbf#\\xc1\\xbc\\x04@\\x05\\xbct\\x8c\\xb8\\xbd\\xa1+z<\\xfc\\x14V<F%\\xcc\\xbc\\xc5\\xe6n<\\x19YF9\\x04\\xbai\\xbd\\xd8\\xfb,=\\xb9\\x82R\\xbcS\\xfbF=V\\xc5o\\xbd%\\x95q\\xbc\\xffv\\xc1\\xbcw\\xa1\\x1e\\xbd\\xaa\\x8eX=!\\x9d\\x8e\\xbb\\xac/\\x0c\\xbd\\xc5\\xf5\\xb9;;`\\xdb<\\x97r\\xdc\\xbc\\x9e\\xa51=[\\xfd+\\xbd\\xc8\\x1a\\xf7\\xbb\\x89\\xcd\\x1a=R\\x96\\xa7\\xbbR\\xba <`aB=<\\x8c\\xa8;\\xde$X<\\\"6#\\xbd\\xde\\xfb\\x81=L\\xb9\\x86;\\xb9\\xd6\\x92\\xbd\\xc19\\xb7\\xbc\\xe4\\xff\\x87=BE~\\xbd\\x8d\\xf1\\xc3<Y\\xb9\\xfa\\xbc\\x0f$g\\xbc*\\xf4\\xfa\\xbc\\xbe{\\xa7=\\xc6\\r\\xb7\\xb8NI\\xf5\\xbch\\xd9B\\xbd\\xb8\\x8c\\\"\\xbc\\xc4Q\\xae\\xbc\\x08\\xbc\\xde=\\xfe\\xd2n\\xbc\\xa6\\xe0\\x17\\xbd3\\xa2\\\\=\\xf1\\xfe\\xc9\\xbc\\x8d\\xba\\x86<g\\xf9\\xaa\\xbc@\\xb6\\xfc9\\x1f{x\\xbc\\xd5\\xe7\\x98\\xbc\\xe2)Q<\\x92W\\xa4\\xba.\\x86?\\xbce\\xe1r=\\xeac\\x8e\\xbd\\x9end\\xbd\\x8c\\x03\\xd3<\\\"e\\x18\\xbc\\\"t\\xab<v\\xf2\\xf2:\\x17mn\\xbc\\xfc\\x19\\xb2\\xbc\\xc6\\x1d\\t<\\x0cg\\xb2\\xbd\\xd4a\\xc4<\\xb0&\\xef<|\\xa2\\xc1\\xbc+rm=\\x94u\\xbd\\xbc\\xff\\xa6\\x16<u\\xe0\\x0b\\xbc\\x825\\xac<\\x84K\\x06<\\xc7S\\x1b=\\xf5\\xee\\xd5\\xbc*\\xa2\\x10=\\xcc\\xe9\\xf7<o\\\\\\x97<\\xb6\\x05\\x08\\xbd\\x9e\\xc4\\xf8<\\xf6(\\x99=>\\r\\x1c\\xbd\\x7fQ9\\xbc\\x93\\xc7\\xa8=\\xc1\\xa31\\xbc\\xaf\\xb0\\x15\\xbd`\\x86\\x15\\xbd\\x87\\x95K<\\x80.W\\xbdm\\xa4Q<\\xf9\\xacD\\xbd\\xd5\\xac\\x15\\xbc\\xf5=6=.\\xbbG=\\x19K%<\\xa5\\x835\\xbd\\xac\\xd7m\\xbd\\x83\\xac\\xb7\\xbbA\\t\\x8e\\xbb\\\"`\\xe5<\\xd3\\xdd\\xdc\\xbc\\x9d(\\x9d<\\xe7zM\\xbd( \\n=\\xde\\xc5\\\"\\xbct\\xd3\\x1d\\xbc\\xa5Wa\\xbd\\xa5\\xdf\\x1d\\xbce)\\xcc\\xbc`\\x8a#=\\xedX\\x93\\xbc\\xdeN\\x81\\xbd\\xbe\\xdcA=O\\x83\\r<\\x8a\\xafs\\xbb\\xa5\\x0f\\xa1\\xbbN|\\xd1;\\xfc|\\xb5=/ \\xce;\\x1b\\x93\\xc4\\xbc\\x89\\x07\\x02=c`\\x1a=\\xf42\\x1a=\\xf6oy<\\xf8B\\x05:v0\\xf8\\xbc*\\xba\\x11\\xbdO\\x03S=\\r:{\\xbb\\n\\xb8\\x16\\xbd\\xae\\xdd,=\\xfd\\xd9\\x07=Lz#\\xbdH\\xdbS\\xbdP/\\xa2\\xbd\\xa8=\\x8e:\\xb1\\xae\\r=S\\x9f\\x06\\xbd\\x8fw\\x87=\\xe8\\xc1L=\\x048\\x98\\xbc\\x9bLN<<\\x19@:\\xbf\\x85\\\\\\xbd7|\\xa3\\xbc\\xd0\\x9b\\xce=\\xa8\\x07]\\xbc\\xfa\\x16c\\xbcT\\xdc\\x19\\xbc\\xda\\\"\\xc0\\xbb\\x89\\\"\\xf1<\\xf3\\x8b[\\xbc\\xf9\\x10\\x1c\\xbd\\x9as\\xb2\\xbbJ\\x16\\xae\\xbb{\\xa6\\xfb\\xbc\\xd0x\\x9b\\xbd5\\x99@\\xbd \\xc6\\x199\\x9a\\x1c\\x17\\xbd\\xcc\\xcc\\xba<\\x9b3`\\xbdo\\x92\\x06\\xbd\\x05\\x9f\\x06=\\x04\\xbd\\x10\\xbd\\x19\\xdb\\x83\\xbd\\x98\\xe1\\x85=\\xcc\\xa6J=|\\xe0\\xcd\\xbc\\xb0\\xed\\xa1\\xba\\xfa\\x9c\\xd6<\\xb3T\\xb5\\xbd\\n\\xcb\\xf0<.=\\x06=U\\xa9\\x8b\\xbd\\x89\\xccK\\xbc\\xf6F\\x12\\xbc\\x0f\\xd2\\r=\\xf9\\xc7z<H1\\x86\\xbc\\xf9\\xe4W\\xbc\\xbeg\\xce<\\xff\\xab\\x06\\xbd\\xba\\xbf\\x9c\\xbbM\\x19*\\xbc\\\\\\x83\\\\<k%\\xaa;k\\x1a\\xae<\\xa90\\x84:\\x8d\\x85\\xd1<M\\x8a\\n=Y\\xa2.\\xbc-\\xe4\\x80\\xbd\\x88!\\t\\xbc\\x83\\x9e\\xf9<8\\xd4\\xd1<\\xef\\x1a+\\xbbv\\xb80=\\xaa0\\xc2<\\xa6!\\xbd;M\\x8d\\xd0\\xbc\\xc6!\\x92\\xbdk\\xf7~=\\x80\\x98\\x88\\xbc9c\\x9f\\xbcO\\x94\\x04\\xbb\\x9c\\x8f\\x1a\\xbd\\xffC\\x87\\xbb\\xfb\\x0b\\xdc\\xbc(@]\\xbd\\r\\xe2\\xab=\\x03a\\xd0<O\\xdb\\xdd<\\x11\\xf2\\xcd<=)0=\\xd4\\x00q\\xbbF\\xdb\\xe3\\xbc!\\xd3\\x04\\xbc!\\x94\\\"\\xbb\\xe0\\xc6\\xd1\\xb9\\x99\\xfee<!\\x04\\x1e;\\xd5\\x85S\\xbc \\x1d&\\xbd\\xc1sS=ED\\xb3\\xbc\\xd2b\\xb5\\xbd\\xf2C\\x16\\xbddO\\x92\\xbd\\xd6m\\xf2<\\xaf,\\xef\\xba\\xf2\\xfc\\xe1<\\xb9\\x8b\\xa8\\xbcu\\xbe\\x19\\xbcU@\\xb6=\\x95\\xc8\\xb7=\\x0e\\xe9\\xed\\xbc\\xf5Z\\x81=\\x1dS\\xac\\xbc\\x80\\xd8\\xf3<+\\xa4f=\\x02\\xfe\\x82\\xbcu4\\xc8\\xbc\\xca\\xb4c=\\x920%=\\x02g\\xdd\\xbc\\xff&\\xb7<\\x7f\\xf6\\xd1\\xbb\\x1a\\x1d\\xce\\xbb\\xad\\xc6\\xe6<%K\\xe2<\\xcf\\xef\\xb1\\xbb#\\xe8E\\xbb\\x1c\\x12)<\\xe8\\xbd\\xd1\\xbcO\\xa0M<\\x8c1\\x9d:\\x07:\\xe8\\xbc\\x99\\x13[<\\xcee\\xcd\\xbc\\xfa\\xcd\\xdb\\xbd\\x11\\x9f\\xb5<Y\\xd2\\x1d\\xbc\\xae\\x92\\t;L\\xc21=_l \\xbd\\xc7\\xe4G7\\x87W\\xb0=t\\\"f:zKX\\t\\x96\\xc9\\x01\\xbc\\x83Q\\x7f\\xbd\\xb3\\\"\\x12=\\x8e\\xb7\\x92=8\\xa1\\xa3\\xba+K+=\\\\\\x17\\xa9;\\xb9\\xdcF\\xbd\\xb6\\x1b]\\xbc\\x9b\\x92m\\xbc\\x04\\x88B<\\x13E\\x03=zeW;\\xfa~g=\\xcc\\xf8j\\xbbA\\xe2\\xea\\xbb\\xa3&\\xb6\\xbcqY\\xed<\\xfe. \\xbc\\xdb\\x00==\\x8a\\x04\\xf0<\\xb7\\xdd!\\xbdr3\\xf4:\\xfdQ\\x96<\\xe2\\xf1\\x88<\\xc9m\\x16\\xbb\\x8f\\xfeV<H\\xa3b;W]\\x98\\xbb\\xcf\\x8e\\xda:\\xb4\\x1b\\x82\\xbc\\x93P\\x03\\xbc\\xf6\\xdd\\xfe\\xb6\\xe2\\\"\\xb0\\xbd\\xa0\\xd1\\xad\\xbcB\\x1e\\x9b\\xbb\\x17\\xe2\\n=\\xa5\\xea\\x0b\\xbb\\xd1y_=\\xba\\\"\\x16\\xbc\\xde7 =_\\xe3\\xc7<2D\\x0e=\\x93\\xdd0=\\\\\\x19I=\\xaeD\\xab<\\xf3\\xa3\\xe0=\\xf8\\xa31<\\xa95\\x8e\\xbc\\xcd\\xb8I\\xbd7=#\\xbd\\xf3\\x0c\\x06;+\\\"\\x93<J\\xe7\\xb9\\xbc8\\xf4\\xc6\\xbb\\xd6\\x08\\x86:e\\xbe\\xd0<,t\\x0c=u\\xfe\\x90\\xbb\\xbe\\x1dQ=\\xa3wf\\xbcT\\x08\\x9a<iB\\x81\\xbc5\\xa8\\x11<\\xa0X\\x99\\xbd+\\x8dp\\xbd\\xb3\\xdf\\x05\\xbd\\xa1Y\\r\\xbc\\x10\\x03\\xfe<i\\xce\\x14\\xbc\\x82\\xd0\\n=4\\xfa\\xc5\\xbc\\x90\\xee\\x97;j\\\"m<\\xf8\\x19\\x14=\\x85\\x0bt\\xbbV\\xc0\\x97<\\xb9\\xaf\\x82=(\\x9e\\x98\\xbd\\x86\\xfb1=\\xdfs6\\xbdd\\xe1\\x83\\xba\\xfa\\x11\\x05\\xbc\\x98Ef=9R\\x9e\\xbc[PR<)\\x16\\x8a=\\xcb\\xd5h=\\t\\xbe\\xd6\\xba\\xa2\\xc2\\xdd;G\\x06\\xac\\xbc;k\\x94<n\\xbb\\xa8=\\xf5\\r\\x97:\\xf5\\xf1}=g\\x07+=\\xb45P\\xbd\\xee\\xb2\\x05=Io\\xd7\\xbc\\xbf\\x8f0=d\\x90\\xa4\\xbc\\xd1\\xa9\\x1f\\xbd\\xeaO\\xb4<\\xeb\\x06#\\xbd\\xad\\xc1\\x85<\\xd7\\xe7\\xaa\\xbc\\xc6\\x83T=d\\x8c\\x13\\xbdGn\\xa3\\xbd\\x7f;\\xf9\\xbb\\xdd\\xdeI;\\xd4Fr<\\x9c+8\\xbd\\x7f\\x8b\\x9f=\\xfaf\\x1a=\\rh\\xa9<\\xcf\\xc0\\\\=\\x91@\\x97\\xbcPj\\xe3\\xbc\\xe5/\\x8e\\xbd\\xb5\\xfa\\x8b<\\xa0\\xdc\\x86<\\xea1\\xd0\\xbc\\x99c\\r=K\\x13\\x85<WA\\xcb;\\xde,x\\xbcT\\xd6\\x11<\\xc6\\x8b~<\\x93\\xbcU\\xbd6\\x9c\\xa6\\xbcd\\xef!\\xbd#\\xd0a=V\\x9e\\x00<\\xb8\\x13\\x8e=\\x04\\x0e\\x8b:\\xe0h\\xe2;\\xe1f\\x90\\xbd\\x9a\\xef-\\xbc\\xacZ\\x87\\xbc\\xe7\\x95\\xd6<\\xe41\\x14\\xbd\\x14\\x94\\xfe\\xbb\\x1e\\xe8\\x89;\\x99E\\xa7\\xbb6\\x0e\\x81\\xbc9G\\x1a\\xbb\\xc8l\\xac;SC\\x04=S\\xb4\\\";\\xcc\\xaa\\x08\\xbb\\xdcIx<?\\xa0\\x17<~\\xf5\\x1d=\\x94N\\xbf\\xbb\\r\\xf7\\x86<\\xe5.\\x94\\xbc\\xb2\\xc8\\xdc=\\x90Po\\xbc\\xbe\\x10\\x9d\\xbc\\xbaw\\xc8\\xbcpj/=\\x7f\\x89\\xcd\\xbaN\\xa2\\x10\\xbcm<\\xee;>\\x96\\xf9\\xbc\\x1bq_\\xbd\\\"\\x01E\\xbcb\\xcf\\x99=\\x96\\x93\\xde:<\\xd8\\x8e\\xbc\\\\\\xa5\\xb7\\xbbi\\x85Y={\\xfd\\x0c\\xbd\\xc9T\\xb3<\\xd9V\\xac=\\xb7n2\\xbc\\x81N\\x10=!\\xd3X<\\x91\\xc5\\xcd\\xbc\\x1fV\\x95\\xbd\\xb01C\\xbd\\x0b^\\xbf;\\xa8B\\x15\\xbd=\\xe5\\x04\\xbd\\x0cm?\\xbc4\\x00s\\xbb<l3;\\x0f!\\x19=o\\x1a6<O\\x18\\x81;\\xc4<e<\\xc9\\\"l\\xbd\\xb9\\xe0\\x01\\xbb\\xaf\\xe7\\x15\\xbd\\x85\\tb\\xbc\\xcb\\xd4\\x06\\xbc\\xab\\xbd\\xab\\xbc\\xbat<\\xbd7D\\x8f;\\xab\\xa1\\x13\\xbd\\xd2\\xca\\x8e\\xbb\\xe2\\xc6\\x1e\\xb9\\xcb\\x87\\xa0\\xbd\\x9b\\xf1\\xed\\xbc\\x0b\\x004\\xbb\\x1b\\x05\\x97\\xbc\\\"\\x04\\xd5\\xbc\\xa8\\xbf\\xad\\xbc+_\\xcf;\\xca\\x049=\\xdd>\\x07\\xbcD[^=]\\xe2\\x87\\xba0#\\x90\\xbc\\xb5a\\xe4;kl\\xe2:\\n\\xf4\\x83;\\xcc\\x94\\xcd\\xbc\\x06\\xa9\\xbc\\xbd\\xea\\xa3):\\\"-(\\xbc\\x1b\\n:<g\\xfb\\xb9=\\x8b,\\xa6=\\x0b\\xefO\\xbd7\\xc0\\x08< \\xcf\\xfd\\xbc;\\xadv\\xbd\\x05\\x04T<\\xfb\\x93\\xb1;\\xb5\\xf5N=We\\x92:[\\x14\\xda\\xbb\\x9a\\xa2\\x90<\\x1fg\\xc2\\xbc\\xe7\\xc9~<\\xf9\\x1bl<\\xd1\\xc6c\\xbb\\x84\\xdd*=\\\"\\xcdy< \\xbc\\x82<\\xf2\\x15\\xb7:)\\xec\\xf3<\\xde\\xbb\\xc9\\xbc\\x85\\n\\x9a\\xbb\\x9b\\x1a\\x85\\xbcb\\x18\\xe8\\xba36>\\xbd\\x91\\xf7\\x9c<S\\xba\\xa1<-\\x15G\\xbc\\xe3\\xeb(\\xbc\\x1bD\\x0c=\\x913\\x93\\xbc\\xc5\\xe6\\x03=8\\xba\\x02\\xbd\\x93WT<\\xa4l\\xdb;\\x90C;\\xbd\\x0f\\x91U<\\xf6\\x8b\\xc5;\\x91\\xea\\x9e;OQW=\\xccT\\xc3=\\xba\\xe4r\\xbc!\\xb50=[\\x15\\x1b=\\xf6S\\xd6\\xbc4\\x08\\xe2<S%;\\xbd6\\xc4M=M(\\xb5\\xbc\"\nHSET bikes:10061  model 'Tethys' brand 'Velorim' price 4875 type 'Mountain bikes' material 'carbon' weight 8.6 description 'The new version with 142mm rear, 160mm front travel is longer and slacker than its previous generation, but it’s also a bit taller and steeper than much of its competition.  With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It’s for the rider who wants both efficiency and capability.' description_embeddings \"O\\x89x\\xbbk*\\xb2<3\\xd4\\x0c\\xbb\\xb0\\xc32=\\x98\\xedC\\xb9m\\xf2\\xa5=\\xc4\\x9d\\xdc\\xbc\\x14\\xb3\\x0b\\xbd!A\\xa1=\\xd3\\x8b\\xa7;\\x8a\\xd7\\x1a<\\xf8\\x9bq<H;\\x1f=\\x9c7\\x9e\\xba/Xq=7\\xd3\\x0c\\xbd\\xe6h\\xd0<\\x14\\xb5\\x1d\\xbc\\x0bi\\x97\\xbc\\x10BW\\xbd\\x95;\\xc6\\xbb\\x00\\\"A\\xbd\\x08\\x84@=$:]\\xbdC\\xe3n=\\x97\\xd9\\x15<jk\\x1c\\xbc\\xd3\\xd0\\x93<\\x17\\t\\xc3\\xbc\\xc9\\xe1\\x17=\\x13\\xe3\\x12\\xbdg`0\\xbd\\xe2>\\xd28\\xccI\\x05=Y\\xc3\\xb1<7\\xdbz<\\xdc\\x93z;\\xef \\xeb\\xbc\\x18\\x06\\x9e;\\xd1vr<\\xf1\\xaez<~\\xb7^<?Hn=d@B=\\x03\\xc2\\x8d;\\xc2 \\x13\\xbckI\\xbd=9\\xb5\\xb1<5\\xb2\\xea\\xbb`\\xf0x\\xbb\\xea\\x16\\xd2\\xbc\\xff\\xb3\\x10\\xbd+J\\x02\\xbcZ\\xe8\\x9b<\\xa48\\xd8<\\x9b\\xcf\\xed\\xbc\\x8e\\xe7\\xdc\\xbc\\xa0\\xc2\\x8c\\xbd\\xc9\\xa8O\\xbd\\xa3\\xfe\\x90=\\xd7A;=}]\\n\\xbcB\\xbe\\xdd<k\\x94\\x9f=\\x95\\x10\\x98<\\xe9FD\\xbc\\xe5\\xcb\\xf1\\xbb\\xe6\\x9d\\x91<\\x02\\xf0\\x87:\\xf3n\\xc1\\xbc\\x942\\xcc\\xbb\\x8e0\\x04\\xbbW\\xb2\\xc9\\xbcd\\x1a\\x13<\\x82\\xd8\\x84\\xbd\\x98x\\x82<M\\x19\\xb2<$\\x88\\xf5\\xbc\\x0e\\xde\\x91\\xbd\\x88\\xde\\x01\\xbb\\x95\\xd8\\xf0;\\x02\\x04\\x91\\xbdO\\xc9\\x03<\\xc5SM\\xbd\\xadU>\\xbct\\x8aB\\xbc\\xe1\\x1a\\x9b\\xbb%\\xa9\\x7f:\\xeb\\xebo=\\x15\\xb4]\\xbb}i+\\xbdv\\x98\\\"\\xbb\\xa6\\x17\\x9e\\xbc\\x95\\n\\xab<\\xf4{\\x14\\xbbP\\xf3\\xe9<D`l\\xbb\\xbe\\xf0Q=m\\xfeU;>\\xe8n\\xbc{V\\xe6;\\xc1k\\xd0<~\\xd8\\x06=$e\\xd3\\xbc\\xdc5\\x89<\\r\\xa4t=\\x7f[;\\xb9\\xfbp\\xdd<\\x81$_<d\\xf9\\xab<\\xc1\\xe5C\\xbc\\xb4\\x19\\xa4=\\xd6\\xfa69Mr\\xf9\\xbcY;|;\\x93\\x18^\\xbd\\xf4\\x11\\\"\\xbc\\x89\\x95\\x83:2<.=\\xc4(\\xb4\\xbb\\xe0\\x89-\\xbc\\xeaX,\\xbc\\x07\\x13\\x8c\\xbbc\\xd5\\x1e\\xbd\\x05=\\x8b=\\xcc\\x01\\xe7\\xbc/\\xda,\\xbaY,e\\xbd\\xc3JB\\xbd\\x07<8\\xbdByn</\\x0bR\\xbc\\x04\\xb8\\xf7<\\xa4!?\\xbdG\\x83\\xe8<Y\\x95\\xe9<\\xfb\\x9b\\x90;\\xc3\\xddT\\xbc\\x81\\x05\\xb7<\\x04\\xa5:\\xbc\\x11\\xf5\\x97\\xbc\\x04\\xc2\\xa1\\xbc\\x9d\\xe7\\xef<\\x0e\\xee\\xb4<t#-\\xbbI\\xe6#\\xbdc\\x06\\x99<\\xd8\\xcc\\x8f<+\\xf3\\xc8\\xbc\\x87\\xf7\\xb9\\xbc\\xf62\\xfb<\\x189\\x9a=\\\"YJ\\xbd\\x8c\\xefN=\\xbf\\x8f\\x0b\\xbd\\xe4:5\\xbc\\xe80u\\xbcf6\\xab<\\xb7X(<2\\x97%\\xbd\\xa2\\x98V=\\x8b\\xde*\\xbc\\xf0\\xaa\\x8d\\xbc5$\\xb3\\xbdo\\xfb\\x95\\xbc\\xb4\\xd9v<3\\xd1\\x0f\\xbc\\x15j\\x1a=t\\xa7\\x0c=\\xc8\\x04\\x1a<8\\x19\\x06\\xbc&^\\x86=.\\xef\\x93<4\\x13\\xba\\xbcC0\\x88\\xbc\\xa7\\x8a\\xee\\xbc\\x0es.\\xbd\\xc4\\x15\\x89=\\xb9N;<\\xf9\\x08]<\\x9av\\xda\\xbc\\xaf\\xe0%<\\xb9/\\x12\\xbd~\\xb8\\x83<\\xbb\\xe9\\x17=\\xbc\\x19<;\\x0fw\\xee<\\xa5)!\\xbc\\xb6c\\xc1\\xba\\xfc}\\x84\\xbd\\x02I\\x9b\\xbb\\x94Y\\xb9<\\xd4\\xect\\xbd*\\xeb\\x16=\\xae\\xfe\\xdd\\xbc6^-=\\x00\\xf3\\r\\xbd}\\xde3\\xbd7\\xd4_\\xbcp\\x0b\\x9f<R\\x02\\xb2\\xbc\\x98\\x8e\\xb6\\xbd!\\x06\\xa6\\xbdp\\x18\\x92\\xbc\\x1c\\xa5p\\xba.M<<\\x1f\\xb4\\xce;\\xd6/\\x0f\\xbb\\x8d\\x86.<\\xad\\x01s\\xbd=\\xb2H<\\x17\\x9aM\\xbc\\xe0\\xd4\\x1e=\\xb0\\x1bP\\xbds\\xfe\\xbc\\xbc\\x14\\xa8\\xb3\\xbb,B\\x86;\\xdb\\xf5Y\\xbc\\xf0\\x9c9=\\x86\\xb6\\xac\\xbd\\t\\x85\\xd1\\xbbEO\\xb7;G\\xe1d\\xbdL, <L\\xa3\\x0f\\xbd\\x86fh<#;\\xa2<\\xac/\\x1e\\xbc\\x1f\\xfen=\\xa7\\xbbi\\xbd\\x9dK\\x0e\\xbd\\x15\\xa8v\\xbc\\xcb\\xe8\\x08\\xbc\\xe1r\\xda<\\x815\\x1f<\\xd2\\xc7\\xc4<\\x82Vb=GN\\xf1<v\\x99\\x07\\xbd\\xb7?\\x0e<`\\x0b\\n<1B\\xd7\\xbcL\\x10V=\\xaa6\\xe3<\\xd4\\xfe\\x1f\\xbc\\xba\\xd5\\x06=8v\\xe5;;t\\xe1<;G\\xe0\\xbcmW\\x12=S9\\xac<s\\x88X\\xbc\\xd4\\x86\\xef\\xbb\\xf4_\\xb2=8\\xf7V\\xbd\\xdc\\xb3;=X\\xab\\x03\\xbd\\xf0\\xec\\xe0\\xbb\\xacCr\\xbc\\xff\\x0f\\\\=LW4<\\xf8\\x98\\x91\\xbc=\\xbe9\\xbd\\x1e\\xeeh\\xbc\\xc9\\xbe\\xa2\\xbc;\\x0e\\xb0=\\x9eN\\xfe\\xbc\\x85\\xd1\\xed\\xbc\\xe0\\xc9a<-*n\\xbbqXI=\\xba\\x0e\\xa8<4\\xc7\\x93\\xbd\\x1bjv<\\x87$\\x04\\xbd\\x11\\xad\\xce\\xbc\\xf7\\x90\\x13\\xbdX\\x89I\\xba\\x80\\xa2\\x15=\\xa4 \\xae\\xbd\\x929\\x10\\xbdX\\xb9\\x17=\\xe3\\x0b2\\xbd4\\xab\\x89<_d\\x9d<\\xd6\\x19\\x06=\\x86\\x89\\x0c\\xbd\\xee,\\xaa<\\xad\\xf1\\xd2\\xbd\\xadV\\\"<\\x8f\\xbe\\x1f;\\x92\\x1f\\x19\\xbd\\x88\\xa7\\x95=\\x9d\\x81#\\xbc\\xf1%.\\xbd\\x05q\\xc79s&$=\\x12]\\xe4<\\x13\\xdf!=\\x1b;\\x81\\xbd\\x02n\\xe6<p\\xceL\\xba^mW=;C\\xe9\\xbc\\xa3p\\xc0;p\\xcf\\x02=\\xe6\\xd7\\xdf;@\\x8f|<\\xabz-=\\x8c\\xfb\\x91<\\xd2+\\xbe\\xbcWP\\x1f\\xbd\\xc0\\x8b\\x10\\xbd!\\xec\\x15\\xbd\\xb9\\xc7\\x0f<\\xfe\\xc6$\\xbd\\x9c\\xb9\\n=\\x1d}\\x8e=\\xf23\\x1a=g\\\\\\x92<\\xb3\\xa3\\x93\\xbc\\\\\\x12\\x97\\xbd\\x8a\\x04\\x16<y\\xa4!\\xbc\\x98\\n\\x16=$\\x07\\\"\\xbd\\xd3]\\x94=\\x88\\xa4\\x81\\xbd\\x007#\\xbc\\x86\\xad\\x99\\xbc~<\\x93=\\xd6\\xdb\\xd8\\xbdx\\xba\\x8c\\xbc^k\\x14<\\x93AQ<\\xaa&\\x13\\xbc\\x0e\\xfa\\\"\\xbdD:n\\xbc\\xfd\\x81L<\\x9f\\xa5@<mE\\r\\xbc\\xb5&k\\xbd,\\xd7x=\\x11\\xd8\\x17\\xbcn\\xbb\\xc0<v\\x8c\\xcc<\\x81\\xff\\x92={$r\\xb9\\xbd\\x05C=!)d<rW\\xd2\\xbb\\xa1\\x11~\\xbc\\x17\\t\\x89=\\xa2*C\\xba\\x97\\x92w\\xbd\\xf6\\xde\\xcc\\xbb\\x17\\xad\\xa0<(T\\\"\\xbd^Lt\\xbb-\\x99\\x02\\xbe,\\xe4\\x96\\xbc\\x87\\xfc\\x91\\xbc\\x13u|<S\\xd4,=\\x1c{t=\\xa3\\xf9\\x85<\\xc1\\xf9\\xc8\\xbc\\x9c\\xf9d<\\xb3\\\\\\xaa\\xbcs}\\x1e\\xbd\\xf4\\xb1n=\\xb7\\xb0s;\\xca<J\\xbc\\xbao-\\xbd^\\x92\\xe1\\xbc\\x8e\\xcb\\xe9<\\x84\\xc2\\r\\xbc\\xb3dC\\xbd\\xf1/\\xe0\\xbc$\\xd8\\xcb;\\xb3A\\xf8\\xbc\\xd4\\xc3\\xc0\\xbco\\x83\\x96\\xbc\\x8a\\xab\\xb4\\xbc\\x1d~\\xc9\\xbd\\xfct\\x06;\\xa21`\\xbb+\\xdc\\x88\\xbcCa\\xe8;\\xbf/h;^\\xeb9\\xbdB\\x8dL=\\xc0\\x93\\xbd=p\\xd6\\xd4\\xbc\\xd9\\x04\\xec\\xbb\\xabw\\xd6<\\x82\\xd0r\\xbd\\xde\\xffG<X^\\xd5<\\x06\\x06\\x9b\\xbd\\xcd\\x82\\x8f<,\\xf0\\xa8\\xbcw\\xdb@=\\xac\\xf2\\x8d=\\xe1\\x10\\xb99\\x83m\\x14\\xbdi\\x90\\xba<9 \\x7f\\xbc\\xafJ\\xb7\\xbc\\xfa&#=\\x87\\x8a\\x07=x8\\x05=\\xac\\xf1\\x1d=\\xf2 \\xef<\\xc08\\x87<@\\xef\\xc5<bcs<\\x93\\x12\\x99\\xbd\\xbf\\x1b\\xbc\\xbbTl\\xac:\\xdb?\\x8a<\\x88Y\\xbd\\xbb\\x07W\\x9d<9\\x84\\xcc<\\xf7\\xf8\\x92<\\xb3~\\xe0\\xbb|\\xb7\\x08<M\\x7fd=\\xbd\\x92\\xfa\\xbb\\x91\\xa1\\xc0\\xbcg \\xe0\\xbb\\xa4\\x01\\xbb\\xbc^\\xd2\\xdc\\xbb\\\\\\xcf6\\xbd{j$\\xbd\\xa3\\xb8\\x8b=\\xf5\\xdck=A\\xb2L=IG\\xd1\\xbb!\\xd5!=$\\xe0\\xb0<\\xf6^\\xb0\\xbc\\x1c\\xe5\\x9a<o^9\\xbd\\xa0\\xc3\\x08\\xbc\\x99\\xfez<l\\x93\\x14;\\xea\\xf4\\x07\\xbd\\xae\\x98\\x89\\xbaH\\xfb\\x14<gV%\\xbd>\\xc9\\x85\\xbd\\x18(\\xad:\\xd2\\t0\\xbd\\xe5\\xf6\\x9c\\xbcL\\xb7\\xa7\\xbc(\\xaf\\xd1<|4,=\\x8de\\xc4\\xbc\\xba\\\"\\x88<\\xa5\\x88\\n=9\\xd0J\\xbc\\xd8\\xa9E=\\xd6\\x0cP\\xbcU\\x94\\x8a;w\\\\J=\\x7fH\\xaa:\\xda\\x1c\\xc1\\xbc}\\xd7m\\xbb\\x0c\\x85\\xfc<\\xd7\\xcd\\xa6\\xbcU\\r\\xbb;\\x89\\xa1\\x1c\\xbc2}\\xa9;\\xbdTf\\xbb\\xb2z\\xe8<\\xf2w\\xef\\xbcw\\xa9\\x9a\\xbc\\xc0\\xd6\\xf1\\xbb\\x1e\\xc3\\xbf\\xbbS\\x8e\\xc9<h\\xf8\\xcf<\\xb6\\x8a\\x82\\xbcr\\xa9\\xf6<\\x02\\xf6\\xd3\\xbce\\xdd\\xc7\\xbd\\xe9u\\xad\\xbc=\\xe6\\xad\\xbc\\xde\\x1a\\x86\\xbchm?=\\x9a\\xd2\\x00\\xbb0\\x83\\xec<\\xe1\\x06\\x9b=\\x8a\\x1c\\x1f<\\x91L[\\tx\\x9f\\xf7;\\xb8>\\xf3\\xbc{\\xa2\\t=\\xaf\\x162=\\x9fS\\xd8<\\x99\\xc6!=s\\xd0\\xa6\\xbc\\x1bf\\x96\\xbc\\xf2;\\xcb\\xbb\\xe0\\xa7s\\xbc|k\\\"<\\x9f\\x9a\\x9a=\\xf9\\xeb\\n\\xbcv\\x86\\x93=\\xa6\\xda(\\xbb\\x880\\x04=\\x8f?\\x96\\xbd\\xc8\\xabj\\xbc\\xb6a\\xe4\\xbc\\x8a\\xec\\xec<\\xc9\\x12{<\\x183\\xc0;(\\xe5\\xd2\\xbcG\\x91>\\xbd\\xc3\\x98\\xef;\\x9eM\\x8a\\xbc\\xab\\x18m<\\xb4a5<xI\\x84\\xbc\\x82\\x04\\x9f<0\\xa0&\\xbc\\x1d\\xcd\\xa0\\xbb\\x06\\xd64\\xbd\\x08\\x02\\x96\\xbd\\x9dme\\xbd\\xa7^`<\\x81Q\\xaa<L[\\x12=d\\x17D=\\n\\x11\\xbc<;\\xa3\\x9b\\xbbM\\x81\\xc9<\\x99\\xcc@<3\\xc8!=}\\\\U=\\x9f\\xb75\\xbb\\xb3!\\xec=\\xb2o2\\xbc\\r\\x82@;e_X\\xbc\\x8d\\x16o\\xbd\\xc7\\xee\\xa3\\xbd\\xde~==\\x1fU\\x0c<wi\\x07=\\xbf\\xe01=\\xb3y\\x1d\\xbcJ4\\xe9<\\x91C\\xc2<\\x92m\\xc7<\\x16\\x05\\xfe<z\\x02\\xae\\xbb_\\x04 \\xbc*W\\xcc<\\xed\\xc5p\\xbd\\xb5\\xd6\\x8f\\xbdN\\xa1\\xf0;\\xd3\\xe2\\xa0:\\x8e\\t\\xdb<\\x0f\\xe1\\x8d\\xbb\\xef\\xcd2\\xbc7M=\\xbc\\x19r\\x9b;/\\xf1x=5*\\x08=.\\xe5\\x15\\xbbJ\\x7f\\xa5\\xbc[\\x93\\xaa=\\x13\\x81\\x8a\\xbd[\\xd7\\xd0<B\\xde\\xf6\\xbc\\x87\\xba\\r\\xbb\\x83 \\x99\\xbc\\x9e\\xd3\\x80=\\xe5es\\xbd\\xad\\x83\\xd2<\\x83\\x13\\x11=\\x93\\xd0\\x91=d\\xc8P\\xbae\\x8c\\xaf\\xbb\\xad\\x1d\\xe2;i=\\xa7<+W2=\\x1f\\xd9\\xdc:U\\xeex=\\x06\\x00&=\\xab^L\\xbd&\\x8d\\xf8\\xbb\\x16\\xe9\\x05<\\xe8\\xe05=\\xfc\\xb1O\\xbd\\x0e\\xb0|\\xbc\\x13R-\\xbcMje\\xbdP\\xa8\\xed<\\xcc\\xc4\\xdc\\xbc\\xdd{\\x9e\\xbc\\xd8\\x82\\xbc\\xbcFiI\\xbcW\\xb6\\x04\\xbb\\x86\\xdb$=\\xebq}\\xbd\\xd6\\x0c\\\"\\xbdH\\xfc\\xe0<h\\xac\\xc8<\\xde5<;&W-=\\xa3\\xbfG<Lz\\xaa\\xbc)\\x1eu\\xbd\\x8a\\x96\\x9a<_\\xed\\xec\\xbbs\\xf1\\xc2<\\x93\\xc8T<\\x02&J<k\\x85$\\xbb&\\n\\x18\\xbd\\x8coS<\\x12\\xfd\\x9b=L3a\\xbd\\xf3s\\xa9\\xbc\\xc7\\n\\x1d\\xbd\\xb1/\\x84=$;\\x0c<\\xd3X\\xae=s\\xe3\\xf9\\xbbm\\xac\\xaa=I7\\n\\xbd<\\xe8\\x8f<\\xa7\\xf9\\xb5\\xbc\\xb1~\\xef<\\x93a\\xae\\xbdQ\\x1b\\xb6\\xbc\\xb7\\xe7\\xea;}t\\xa9<\\x01\\x88\\xcc\\xbc\\xec\\xfd\\xf3<\\xef\\xef\\xd1\\xbb\\xa3\\xe7\\x8d;>o\\xc9\\xbb\\xc2\\xf0\\x04\\xbdR\\xa8\\x00\\xbd\\x11\\\\\\xba<+<\\xc0<\\xbc^9<\\xd9T\\x9a<Q\\x86H\\xbdd\\xd3\\x8e<n\\x03\\x04\\xbd\\xec3\\x1c<\\xb3f\\x0b\\xbd\\xbbii=\\xdf\\x0bR<M/b<\\x89\\xf2\\x0f\\xbdHr\\x7f:K\\xa5\\x93\\xbd;\\x97\\xfe<3f\\x83=U\\xc5\\x94\\xbd>\\x96\\xf4\\xbb2\\xad\\xe0\\xbbU\\xea\\xb6;\\\\`\\xbb\\xbcq\\xcd0\\xbd}5\\xb8=\\xd5\\xbf\\xc0\\xbb\\x89e\\xf0\\xbc.\\xcc1=\\xc9\\x183\\xbc\\xc5\\xa4\\xe0\\xbb\\x11\\x90\\x11\\xbdY\\x91#=\\x89\\x96\\x0c\\xbd\\x17\\xbc*\\xbd1\\xc8F:\\xafl\\x0e\\xbdn[\\xcb\\xbb\\xb5lH=I\\x8a)\\xbd-\\xd9\\xb7<\\xc4\\xc0$=4\\xad\\x88\\xbc\\x12\\x97\\xa1\\xbcO\\x16\\x0c\\xbd\\x152,\\xbbLo]\\xbd\\xe9\\xe5\\xe3\\xba\\xaa<\\xaf\\xbb\\xc4\\xce\\xdd\\xbc\\x8f\\xbe*\\xbd*\\xb0\\x9a\\xbc\\xb6\\xfc\\xf3\\xbc\\xa9k\\x99\\xbd[\\xd5g\\xbd4=\\x12\\xbcc\\xd2\\\"\\xbaD\\xea\\xa8\\xbc\\xb8)\\xd5\\xbc\\xef$\\x7f\\xbd\\x03\\xdb\\xef<\\xa4\\x12\\xa1\\xbc+\\xeb\\x13=\\xe0\\xac\\x81\\xbc9\\x11s\\xbd&\\xe2\\x8b\\xbcp\\xc6\\x08=\\xafF\\xf2\\xbc\\x95\\x83\\x90\\xbd-v\\x10\\xbd\\xe7\\x8d\\x90\\xbc\\xd2\\x97\\x02\\xbd\\x97/^=\\x99\\xbbF=\\xec>\\xaa=I\\\"\\r\\xbb\\xbd+1=\\x89\\xbd}\\xbc\\xd3\\x91\\x8e\\xbd+\\x84W<\\xaeV\\xbc\\xbbP\\xc8\\xe0<MH\\x91\\xbb\\xf0i\\xfc\\xbaz\\x9b\\x12\\xbd\\x03I\\r<{\\x1cY\\xbc\\x94\\xd1\\xa9\\xbcg1f:\\x9du\\xae;m\\xe3\\x03=\\x00\\x150<%;\\xef<N\\xd1\\x80<_h\\x93\\xbc-\\x9e\\x0b<\\\"\\xa7\\x11\\xbd\\xaf\\x05W:H<N\\xbd\\xe7z\\xa8\\xbbgq\\xbd<\\x1c\\xe96\\xbc4f#<3\\xfc\\xc9<0Q\\xcc\\xbcQ\\x1ec=\\x91\\xdbV\\xbdp(\\xb9\\xbb\\x1b\\xabF\\xbd/<q\\xbdd[\\xf8<\\xc52O=\\xbbK0;\\xc4\\x8dN=\\xe9\\xb6g=\\x10\\\"\\x89\\xbc\\xbc\\xd2\\x0b=\\x82\\xf7B=\\x01f\\x82\\xbd\\xf5U\\xb8=\\xb8\\xf2_\\xbd\\xaf\\xe2\\\"=3E\\xd7<\"\nHSET bikes:10062  model 'Picard' brand 'Bicyk' price 2163 type 'Road bikes' material 'full-carbon' weight 7.6 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xc2\\xd80=5@\\xe6;\\xd5\\x9e\\xda\\xbc\\xa9\\xad\\xbf<\\xe1F =\\xa7\\xdb\\x9f<Zs\\xb0\\xbc\\xef5(\\xbdm\\xa23=\\x92\\xf6\\xc2<=\\x04\\xee<\\xdb\\x15X=\\xf4\\xc2$=\\xf8W\\xb4\\xbb\\x8f\\x1b\\xb3=\\x8b\\xed]<\\xb5\\x0e\\xe1\\xbc0G\\xd5<Y\\xab\\xe9<A?\\x82\\xbc\\\\\\x80<:\\xb0\\xd6R\\xbc\\xab\\xae\\xed<\\x1b\\x8b$\\xbd\\x129,=k\\xccF<{\\xd0\\xe9<\\xd9\\x9f\\x0c\\xbc\\xf7\\xcf\\x93;\\xe3\\xf1\\x1f=\\x0e\\xb0\\x07\\xbdU\\n\\xfe;v\\xc4!\\xbc\\x12\\xd6<\\xbb\\x92\\xcc\\xd7<\\x1f\\x8c\\x1f=sGM<+\\x8e\\x86\\xbc\\xf0\\xb7\\x02\\xba\\xa5\\xdfG\\xbc\\xce\\x86\\x9b=,\\xf2\\xab<}[\\xa1<\\x068\\xc2<S\\xb0\\xd8\\xbb\\x8a6)\\xbd\\xe0\\xcc\\xd4=V\\xff\\xbd\\xbc\\\"Yv\\xbc\\xc9\\x10\\x06\\xbc%\\x94O\\xbd\\xfb\\xc0a\\xbd\\xdd\\xaa\\x90\\xbcj+\\xcf<Ft \\xbc\\x9b\\x93\\xd8\\xbc\\x88\\xa7\\xa1\\xbc\\x8c\\xc0\\x8f\\xbd&\\x9d\\xb1\\xbd\\xf8\\x1d\\xa2=K4Y=e\\xc1\\x80<\\xe5\\xc48=E\\xc9\\x07=l^\\x0e<\\xe7\\xc8C\\xbc.(\\x90\\xbc2\\xef\\x86=n\\x95\\x91\\xbc3\\xab\\x0b=\\xa0\\x89n\\xbc\\xa0\\x08\\x80\\xbc\\x1c41\\xbd\\x15MF=\\xa4bC<\\x0bU3\\xbb \\x81\\xdd\\xbbG\\x8ex\\xbd:\\xfd\\xbb\\xbcHK3\\xbc\\xfe\\x9f3=|\\x14\\x83\\xbd\\xeec\\xf7\\xbc\\xa3\\x0c\\xe2\\xbc4\\xc6z\\xbdP\\xa3\\xd5\\xba\\x89@5\\xbc\\xf6}\\xd1<\\xe7\\xfe\\x8c<\\x9b\\x80\\x11=\\x9d2\\x06\\xbdK\\x90\\x15\\xbdc-]\\xbd\\xa7\\x1f\\x03\\xbc\\x9d\\xb5*=\\x87H\\x1f=\\x9c1\\x86:\\xec\\x08\\xbc\\xbc+\\xa2\\xea\\xbb\\xf1\\x9e^\\xbd\\xd1\\x07\\xa8;\\xa0\\x98\\x8b<L\\xd1\\xc9<{\\xf6\\xc9\\xbcEL\\x10\\xbc\\xc2;\\xf0=\\x87\\xcf)<\\xed^,<F\\xff\\x93\\xbc\\xbc\\xc1u<\\xb5\\x836\\xbc\\xc7\\x19\\xdf<38N=u7\\x04\\xbd\\xcf\\xde\\x92\\xbc\\xec\\x89\\xbc\\xbdM\\xed\\xdd\\xbc\\x89\\xcb\\x9a<\\xa6\\\"\\x04\\xbc\\xcf\\x01\\xae<(\\x07]<\\\\z\\t;\\xaa\\xf7\\\"=\\x123E\\xbc\\x96N\\x01=\\xed,\\x01\\xbdn\\xb3\\x95<\\xbd\\x16\\x83\\xbd\\x0e\\xa3+\\xbdg\\x91\\xe1\\xbc\\xfc<\\x9c\\xbc\\\\\\x15\\x0e\\xbc)\\xd6\\\";\\x05_3\\xbd\\x16\\xdc6=\\xc67r=\\x94!C\\xbb]\\x91\\x05\\xbc\\x1f\\x97\\xa3<\\xa8\\xa3z\\xbb\\xd9g\\x11\\xbcR\\x8f\\x8c\\xbdN\\r\\xe6<\\x83\\x1c@=A\\xcd.=\\xcf8*\\xbc\\x13XS\\xbc>\\xff\\xc2;\\x16~(\\xbd\\xb0\\x1c6=\\x01[\\\"=\\xd5\\x84\\x89=\\x07 \\x1f\\xbd~\\xfd2=\\x82\\x99\\x1a\\xbd\\xa2\\x95]\\xbdU\\xb5\\xe6\\xbcz\\xe2.<\\xbf\\x8e\\xfb\\xbbQ\\xd2\\x8a\\xbd\\x9a\\xe1X=&\\xcb\\x8a<\\xe7F\\xc6\\xbc\\xea\\xb8\\x8d\\xbc\\x84d\\x01=Ug\\xa0<\\tE\\xb0</\\xeb\\xfd<c\\x1ek=\\xd4\\xa5\\xea\\xbc#&\\x03\\xbd\\x88\\x9d\\x15=LH\\xba\\xbbr\\xba[\\xbd\\xe4(\\xef\\xbc\\xa5\\xee\\x16\\xbd\\x19En\\xbd\\xc4\\xa7,=9\\xa2\\x95<\\x03\\xb8&<\\xa6\\xdb\\xdc\\xbc\\x1d\\xb9\\xac\\xbc\\x1c\\x86$\\xbc\\xfef\\x9b9sx\\x92=\\xfb\\xb4\\x9c=\\x83\\\"f=\\x8d\\xb4,;\\x99\\x00\\xc7<\\xfd\\xde\\xa8\\xbc\\x8c\\x05\\xbc<>\\xffC\\xbb`B=\\xbd2\\xdf\\xa6<b5\\x03<\\x9f\\x96\\x96=:\\xb6H\\xbd9\\x175\\xbd\\xb4\\x91\\xd8\\xbd\\x0e\\xffV=\\xe5\\x16\\xba:R\\xad\\x84\\xbd\\xe1\\xca\\x9f\\xbd|\\xdf\\x02;\\x88J\\xf6\\xbc\\x0e\\xee\\n<k&\\t\\xbd4\\x90\\xe9\\xbc\\xc7\\xdd\\xcc<\\xafT_;\\x11H\\\"\\xbdo`\\x0b<RF*=\\xb6\\xac\\xd8\\xbc\\xbf\\x9e\\xab\\xbc\\xe6\\x94^=\\x01\\x80>\\xbc\\x9c\\x8b\\xf1:\\x98\\xd9\\xd0\\xbcx\\xfbd\\xbdlh+<@\\xd9U=N\\x05\\x12\\xbd\\xe5f\\xcc\\xbc\\xb0\\xc5\\xc6\\xbc9\\xad~\\xbd{\\xadZ;\\xa3hZ\\xbc\\r\\x97\\xff<}\\x8e\\xab\\xbd\\x05\\xb7/\\xbbq\\xd13\\xbd\\x83\\x8f\\xaf\\xbc-<\\xc7<\\xcd\\xeb?\\xbc\\x00{\\xd0<o\\xb4\\xf9:\\x91\\xd3Q<\\x84)\\xe3\\xbc\\xe1\\x9a =\\x1bn\\x11\\xbdI\\xf0\\xa5;\\xb1\\x92:=\\x12\\xe4\\xfe\\xb9B}h<\\x04\\x03\\x94=\\xb3x=\\xbcK\\x80 \\xbda\\xee\\xa0\\xbb\\x94\\xca1=\\x1b\\xbcL\\xbbV\\xd3\\x11\\xbd=K\\xd4\\xbc\\xd51V=\\xeb\\xa8\\xaa\\xbd\\xee\\xd5\\x0f=m\\x8am\\xbc\\xbf\\xa6\\xc4\\xbc\\xa1\\x0f\\xbb<f\\x80\\xd0=8H\\xa4;4\\x96\\xf8\\xbc\\xcc\\x87\\x1f\\xbd!\\x8a\\\"\\xbd\\xa8\\x8e\\x9e;NE\\xdb=Y#0\\xbds\\x9c6\\xbc\\xdeM\\x11=\\xe6\\x9c\\x8d\\xba\\x8e\\x9f\\x08=\\xd4c\\x80\\xbc\\xcb\\x0e\\xb2<\\xff\\x00\\x0c\\xbbp\\xce\\xd0\\xbc\\xb6\\xa7i\\xbc9<0\\xbc=O\\xbc;\\x14!\\x9d=%0\\xae\\xbd\\xd6C\\xa9\\xbd\\xf5W\\x02\\xbdUr\\xb3\\xbc\\xee\\xecJ<\\xe0r\\x1e=H\\xd8\\xa9<`h\\x9c\\xbbb\\xab\\t<\\xd6\\xa2\\x8a\\xbdI\\xe1\\x12=\\xa3v\\x08=\\xd2\\xb7\\x05\\xbd\\x98\\xe4#=\\xbe/\\x0e\\xbd\\xeb6\\xe5<\\xc1\\xa8/\\xbd\\x98\\xad6=G\\x00s=\\x80p@<5.\\xc0\\xbc\\xabg\\x04=V\\x1b0<\\xfce\\x81:M\\xd1U\\xbd\\xde\\x0e\\xf2<\\x91M\\x8c=\\xe7\\x11M\\xbd]\\xbap<\\xc2\\x96\\xc9=-\\x1f\\xd0\\xbbB[s<x\\x87\\x1e\\xbc\\xbe8\\xfe\\xbc?\\xa4\\xcc\\xbc-9\\x10=F\\x83\\xdd:\\xe7\\xd1\\xc5<\\x95\\xb8f=\\xd47\\x1b=\\x99\\x06W;f\\xc2\\xf6\\xbc\\xb0\\x83u<\\xde\\xc0\\t\\xbdW\\x90{\\xbc\\x82fk<\\x8fdM\\xbd\\x87\\x17H<\\x0c\\x13\\x11\\xbdcM\\xda;\\xe0\\x9c\\x89<\\xe7N\\x80=\\xd9\\xab/\\xbd\\xc9\\xc0V\\xbc*\\xce\\xaf<\\x91\\xb0\\xbe<a\\xab6\\xbcy\\xa7\\x90\\xbd\\x8d\\x99\\xd7<\\xe3\\xb8\\x9d\\xba\\xe3\\xa6\\xa1<\\xf4\\xb91\\xba\\xde`=;\\xd8]\\xaf=\\x9e\\xea7;\\xeb\\x82\\x80\\xbc>\\xa8\\x02=P\\xea\\x13=\\xd2~(=\\x9a\\x9cb<caR\\xbc\\x0b%\\x95\\xbc\\xce\\n\\xff:\\xf7\\x85\\xc0<]Xt\\xbc%\\x04Z\\xbd\\x19f\\x07=\\x0fZ\\t=J* \\xbd\\xdc\\xa3\\xb6\\xbc\\x8e\\xf3\\x8f\\xbd\\xf7\\xd3\\x85<\\x91]\\xe6;n\\xf3T\\xbd\\x9a\\x12\\x84=L\\x89\\xda=+\\x9c\\x85\\xbc\\xb3\\xb9W;\\xa3M\\xe1\\xba\\xca;\\x89\\xbd\\xab\\xaf\\x11\\xbd4\\x14\\xa4=\\xa1\\x0e\\x83\\xbc\\xf1Q\\xae\\xbct\\x1c><\\x94\\x9a\\x15\\xbc$e\\xc5<\\xc9L\\xc3;\\x9fV\\xce;l\\xfb\\xc5\\xbc=\\x9e\\xfa<\\xf6Z\\xdf\\xbc\\xf6p\\x0b\\xbd\\x9bP\\xf4\\xbc{R9\\xbc.\\xad\\xfb:\\xc9\\x10\\xcb<>6\\x12\\xbd\\x854\\xa4\\xbdY\\xfe(=.\\xc7\\xc7\\xbc\\xb4\\xe1X\\xbd\\x87\\x07\\xe7</\\xe9-=\\x10h\\xbd\\xbc\\xba\\x96*\\xb9\\xffgk\\xbc\\x01\\xfe$\\xbd\\x9cP\\x11<\\x1cH\\x02<G\\x0c\\x99\\xbd\\x1b\\x16\\xd58\\x9c\\xa2\\x0e\\xbd9i#<\\xd8\\xf2\\xf6<f\\xbc\\xbc\\xbc\\xb0\\xb5\\r\\xbd;\\xf9\\xc1<\\x7f\\x1a\\xe9\\xbc\\xd7\\xfaZ<\\x8e\\xeaw\\xbc+\\xda\\n<\\x01\\xaa\\xbc<=\\xe7\\xae<\\xe4\\xd2\\xf5\\xbb\\x1c/3<e\\\">=\\xa3CJ\\xbc\\xf4N\\x02\\xbd\\xefa,\\xbd\\xe3\\xf4\\xb7<bk7=\\xb0\\xc8\\x90\\xbc_\\x15N=\\xbe\\xeae=\\xe6{\\x02\\xbb\\x9b\\xe3\\xbf\\xbc\\xf8\\x84\\x84\\xbb<\\x1c\\xfd<\\x1d\\xa4\\x14\\xbcu\\xf4\\xb7\\xbc!\\x06I<\\xa2 \\xa8\\xbb\\xea\\x11\\x9c\\xbc\\xb7#\\xa2\\xbc+CQ\\xbdOQ\\xc3=BY\\x98<46;=\\x0b\\x8e\\x8f<\\xa6\\x08V=\\xe8\\xea\\x90<\\xb6\\x0b\\xb0\\xbcs\\xa4L;\\xe8\\xc0\\xd0\\xbc\\x87i^\\xbc\\x93\\x90\\xcd\\xbc\\xee\\xd1\\xdd;VV\\xef\\xbc\\\\\\x07\\t\\xbd\\xb2\\x9e\\x91<\\x8f\\xcb\\xec\\xbc\\xe8xa\\xbd\\xe7\\x14\\\"\\xbd\\xecm\\xda\\xbc&\\xf6\\xa7<\\xc5\\xcd\\xc1\\xbc\\xbdC\\xbf<\\xdc\\\"\\x9e\\xbc\\xa1\\xd2\\xaa\\xbc9`\\x93=\\xa3\\x0f^=\\x1c\\xa1\\x0f\\xbd\\x1a>\\xeb<]\\x0fN\\xbcV\\x94\\xe2<\\x9e\\x8c\\x9f=Xf\\xe8\\xba\\x8c\\xa9\\x90<w_\\x87<\\xcc\\x9c\\xb7<^D\\xab\\xbc\\xdd\\x1e;;\\xa2\\x08\\xd8<\\xa3%6\\xbc\\xbd\\x9f\\x9d<\\x00[\\xfd<M\\x88\\xd4\\xbc\\x8b(S\\xbc\\xea\\xa5\\x03=q\\xb2\\x18\\xbd|\\xaf\\x93<-\\xb0b\\xbb\\xf30\\xbf\\xbc\\x06h\\r=n\\xac\\xbc\\xbb\\x95\\xfb\\xb0\\xbd{\\xec\\\"=\\xf3b\\x80\\xbc\\xcd|\\x9a<=i&=]z\\xc8\\xbc\\x08N\\xab\\xbc!\\xb7\\xb0=\\x7fR\\xff<\\x94\\x82f\\t2\\x1d\\xb7\\xbb\\xefR\\x86\\xbc\\xdf\\x00%\\xbd+&\\xeb<\\xbf\\xc8p;;\\xba\\x05=\\xe3\\xcb\\xe1\\xbb\\xa2\\x99\\x80\\xbd9\\xd6\\xad\\xba\\x0b%\\xb6\\xbch\\xcd\\xda\\xbc\\x90F6=k\\xcc\\xd5\\xbb\\xa9\\x97\\x89=\\xb8\\xfd\\t<\\x170\\xed:\\xec\\xdf\\xac\\xbc\\xad\\x0bs<j\\x8ba;\\xd0\\x1a\\x16=\\xc3\\x98\\x03=\\xb8\\xd0]\\xbd\\x08\\xbb\\x80\\xbc\\xb1\\x1a\\xc6\\xbb\\x1b\\x19\\t\\xbc\\xf5WW<\\x9f\\xa6_;i\\xa8\\xef\\xbcT\\x7f)\\xbc\\xabh\\xe5\\xbb\\xaaY\\xda\\xbc\\x97\\xf1\\x8d<\\x82@z\\xbb\\xd1F\\xa5\\xbd\\xc7\\x1d\\x15\\xbd\\x16yz<\\xf8<\\x9f<G\\xb2(<\\x1e\\xf3\\x83=4\\xce\\xd3<lB\\x1c=\\x96F\\xd8;\\x93A\\x16=\\x9a\\x1a\\xaa=uVf=\\xf8\\xf3\\x08\\xbd\\xea\\x86]=\\x16\\x18\\xe0<:\\xd7\\xa6\\xbc\\xe8\\xc9\\x0e\\xbd\\xe7\\x17\\x19\\xbd\\xe6\\xee\\xb9\\xbco\\xe0>=r\\x18D\\xbc\\x88,\\xa9<\\x9bI\\xb3\\xbb\\x83\\\"\\xdd<.]\\x12=/\\xa6\\xcd;\\x14\\x98\\x1f=\\t\\xea\\x12\\xbb\\xe6\\x8a\\xdc:\\xda\\xa1\\xd2\\xbc\\xac\\xe7\\x88\\xbc\\xbay\\xc9\\xbd=\\xd3\\x8a\\xbdeT\\xbe\\xbc\\xf1\\x13\\x86\\xbb\\xe8\\xd1B=5\\xcbt<\\x0e\\x13&=\\x8dr\\xb5;\\x14\\x90\\xb3\\xbc,o\\xe4<\\x19$;=.\\xea\\xa7;\\x8e\\x11\\xb2<\\x9a\\x9ec=e\\xbd&\\xbde\\x048<g\\x1a\\xb6\\xbb\\xe2\\xea\\\"<o\\xe3\\xac\\xbc\\xf2r\\x87=d\\\"\\xc0\\xbc\\xdaR\\xe0<?\\x7fo=\\xba\\xb5\\x84=2\\x89\\xd0;\\x85\\xc0\\x88<\\x17ND\\xbdyC\\xb9\\xbc\\x05Z\\x85=\\xa4\\xa5\\xbf:\\xa8\\xd9R=\\xda\\xca;=sz\\x84\\xbd\\xb3o\\x00<\\x94\\x07,:\\xd0\\xf2L<\\x9fM\\x06\\xbd\\x12\\x01\\x87\\xbb\\xae\\xbd\\x90<`\\x11\\x85\\xbd\\xc1\\xb8\\xaf\\xbc\\xeb\\xe9N\\xbc\\xfc\\xc6\\x80=\\x0fD$\\xbdy\\xbf\\x9d\\xbd\\xb2\\xad):\\xc62o<\\xd1\\x96\\xb6<vH\\x87\\xbdQ\\xad\\x88=\\x19\\x0c\\xb4;?-c<T\\xc7O=\\\"\\x04N\\xba\\xccU>\\xbdj\\x13o\\xbdNz@;\\xa7\\x1e\\x18<\\x0e\\xa2\\x17;\\x97\\x02:=\\x8cF\\x85<R\\xa8\\xad\\xbc\\r\\xe4\\xa9;\\\"}\\x0c=\\xbfC\\x1f<\\x8d\\xe0\\x08\\xbdz\\x9e\\xad;\\x0b\\xa1\\r\\xbdR\\x0eB=\\x139\\x9f\\xbc\\xe0 #=\\xcc\\xdf\\n\\xbd\\x89Z\\x12<\\x81\\x91\\xc3\\xbd+-\\x18<\\xa9\\x10\\xb9\\xbc\\xb9J\\x06\\xbb\\\"\\xc0\\x1c\\xbd\\xd3oj<\\x11\\xb3\\xef\\xbb\\x05\\xce\\x03\\xbb\\x81\\xc9\\x02\\xbd\\xb3q\\x97<\\x80\\xa3\\xef\\xbb7\\xb9,<4\\xe7\\x04\\xbd\\x01v\\x9c;\\xac\\xe0\\x8d\\xb9\\x19h\\xbd\\xbb\\xbc\\xbc\\x10=I\\xf4\\x1b\\xbc\\xf6c\\xca\\xbb\\xc9\\xc4\\r\\xbc\\x0b\\xde\\x95=\\xbcH\\x8c<\\t\\x81=<}\\x1b\\xb2\\xbd\\x1e\\x8d\\x04=X\\xb4\\xab\\xbc\\xfb\\xc8\\x8f<\\xdd\\xbet<\\xd2\\x01G\\xbd\\xdbD\\x96\\xbd\\x81\\x97<\\xbc\\xce\\xe9K=\\x9aG\\xcb\\xbc\\x03v\\r\\xbc\\xc2\\xf6y\\xbc\\xad\\x97\\xae;\\xdf\\xa7.\\xbd\\xa0\\x93T\\xbc\\xdf_\\x91=\\xaf\\xf3\\xd9<\\xaaT\\x00==\\xda\\xb1<D\\xd9\\x11\\xbd\\xd3l\\x84\\xbd\\xe9\\te\\xbds\\xa1\\x1e=\\x8fP\\x07\\xbd\\x91\\xad\\xbf\\xbc\\xc8\\x84\\xb6;\\xe0L\\xa4\\xbcP(\\xc7<\\xd1\\xf5\\xc9<\\x98\\xd1\\x8b\\xbc\\xd7\\xb3x<f\\xb4\\xd6;\\xc9$\\xc2\\xbc\\xfa\\xff\\xc5\\xbc=sg\\xbd\\xdd)\\xac\\xbc\\xd4D|<2\\xaa\\xbf\\xbc\\xe9$E\\xbd\\x97`(<s>\\xcd\\xbb\\x12\\xb0\\xc7\\xbc\\x961\\x06\\xbcK+\\x82\\xbdT\\x08.\\xbd\\xd4\\x9d\\x8e\\xbaD\\xbc2<bo\\n\\xbdod-\\xba\\x9f\\xd2\\xab<\\x80n!==\\x16\\xa4\\xbcA+\\x07=X\\x06X\\xbcT\\t\\xbc\\xbcBv\\x94\\xbc\\xc1\\\"\\x1b=\\xbe\\x02\\x1d\\xbd\\xb5\\x99\\x9a\\xbcpC\\x95\\xbd\\x01\\xbc};e|L\\xbc\\xb9\\xa4j\\xbb\\xa7\\x05c=\\xef]|=?G>\\xbd\\x07g\\xcf<B\\xdba\\xbd\\x11\\x1dS\\xbdEA\\xfa;1\\xfe\\n\\xbc\\xe3\\xbf8\\xbcIe\\x9b:}@\\x8e;?5\\xef<\\x97\\xc9\\xd7\\xbc|\\x98s<L\\x13\\xef\\xbb\\x90\\x1a\\x1f\\xbc\\x89[\\x9e<\\xabE\\x9c\\xbc\\x04\\xb1U\\xbc\\xd7\\xd7\\xcc<\\x1c\\xb0\\x0f=\\\\\\x8c\\x04\\xbd9\\xbfz<\\x92\\xc0\\xb7\\xbc\\xfd\\xde\\t=gF\\x81\\xbd\\xc49A<y\\x92\\xa0<\\x18\\xfc\\xff\\xbc\\xd7\\xb45\\xbc\\x91J\\xe1<U\\xf3\\xf9\\xbc\\x1b\\xb5$=\\xf9\\x95\\x90\\xbdT\\xa7\\xf2<\\x05Wx\\xbc^\\xb2\\x12\\xbd\\xa48\\xa5\\xbb\\x16\\x82\\x08=\\xe1\\xa6k<Z\\x13M=\\x85\\xad\\x9b=\\xe0\\x0fS\\xb9\\x86O\\xb8=\\xaf\\xe5\\xa3<\\x0f\\xf7\\x91\\xbd\\xe0T\\x88<E\\xd5\\xc9\\xbc]t\\x9e<\\xcaZ\\xfb;\"\nHSET bikes:10063  model 'Hygiea' brand 'Eva' price 3892 type 'eBikes' material 'carbon' weight 15.7 description 'A city eBike that could double as a short-haul commuter. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\xfc/\\xa0\\xbc\\xa4\\xb0\\x18<\\xa4\\xa6\\x15\\xbdI\\xf5)=\\xd7+F\\xbc\\xb4\\xf9V=\\x9d\\xe7\\xb6:\\xacN\\x80\\xbd\\xf6R9=\\xdf\\xf3\\xc6<\\xf9\\xdd.<\\x14\\xf4\\x93\\xbc\\xa3_*=\\xff\\x94\\xbd\\xbc\\xfe\\x06\\x95=\\t\\xb1-\\xbc(\\xcd\\t=\\x84\\xc7\\x9e\\xbb\\xca\\xd8:<\\xea\\xfe\\x95\\xbd}^I\\xba\\xb5&\\x87\\xbdsf\\x94<\\xa6l\\xdb\\xbc\\xc3`)\\xbd?\\xba\\xbb\\xbbM\\xb9\\xa2=\\xe9\\x93d\\xbb_M\\xac\\xbc\\xfa\\xd7\\xf0=\\xd7\\xd25\\xbdD\\x93_\\xbd\\xb0\\xa0\\xa0<\\xf7\\x10x:\\x95\\x8a\\x8d\\xbb\\x19\\xaf\\xab\\xbc>\\x8a]=\\xf4\\xc0O\\xbc#\\x8f\\xb9\\xbb+\\xf2\\x0b;.\\x8c\\xb1=\\xc9\\xbf \\xbd)W<=\\\\&\\t=J\\xceF\\xbc\\xc6\\xa8\\x80\\xbcy\\xe3\\xb5=\\xdfC\\x82\\xbd\\xac_:\\xbc\\x83F\\xb8;1\\xb05\\xbd\\xae\\xdc-\\xbd\\t \\xd0\\xbcA\\x9a\\x8d\\xbc\\xbbD\\xfe\\xbc[O\\x0e\\xbd\\xac\\x8b\\xe4\\xbc\\x8bw\\x15<(\\xd1\\xcf;\\x95WV=n\\xda\\xf3<#\\xf2o\\xbcZ\\r\\x06<t\\xd7\\xde<\\xcb\\x83S=\\xa8h\\x8c<(\\xa7\\x18;\\x93\\xdbn=\\x9da\\\"=\\xac\\xc3\\x8d\\xbc\\xaf.T<\\xf8\\xf7X=\\x84\\xf3;:\\x1f7\\x85\\xbc\\xc4\\xa7p\\xbd\\x85\\x11\\xc0\\xbb\\xbb<_<\\xda\\xa9\\x84\\xbc+\\xfe\\x9c\\xbd\\xfb\\x0b\\x08=c\\x9a\\x8b<\\xb0\\xb6\\xf0\\xbc\\\"\\x0e\\x9e<C\\xcf\\x8e\\xbc\\x10\\xba\\xe0\\xbc7\\x1b]\\xbc\\\\\\x0bB<U\\n\\x17\\xbc\\\"6\\x8b=\\xe2\\x11N;\\xe8\\x85\\xc5<\\x01\\xe0\\x93\\xbd\\x89\\xc7J\\xbd3\\x0b0=\\xf5\\x0f\\x17<\\xeaqs\\xbc\\x8f\\\"3\\xbd]\\xe3#\\xbd\\xe3\\xf6\\xed<\\x05A\\x9d\\xbbvG\\xff;\\x8c>^<{C\\x0c=\\xd01\\xc3\\xbc+n\\x07=\\xfa\\x82\\xc0=y,\\xd8:\\xc1\\xb1.<\\xa1G\\x1d\\xbd+A\\\\\\xbc\\xbc\\xe3\\xd3;.\\x9eO<\\x0b3c:,P\\\"\\xbd\\xf2\\x8aR\\xbd\\xf0\\x0eP\\xbd\\\\\\x88\\xc8\\xbcu\\x04 =\\x85\\xe8\\x90<5\\x19\\x80\\xbdj\\x0b9\\xbc\\x80\\x9b\\xa3<>\\r\\x8b\\xbd`F\\xe5\\xbch\\xa0?=-r\\xed<\\x87\\xfd=;\\xc8\\xa7\\xda\\xbc\\x03\\xaf\\xd6\\xbd\\xbbR\\xc1\\xbd\\xeb\\xceB\\xbd\\xf8\\xd4\\x87\\xbcS\\xbe\\xfa\\xbb\\x93U\\x17\\xbd\\xc2c\\xa9;\\xf9[Z=\\x16#\\xfe:yus\\xbd\\xdb\\x86;<\\xdc\\rH\\xbdz\\xf8\\x1e\\xbd\\x08\\x08\\x05\\xbd\\xee\\xc77\\xbc@\\x8c\\x8f=\\x7f\\xb0\\\"<h\\xd7\\x0f\\xbd\\xcc\\x17>=\\xfa\\xba\\\\\\xbc\\xbe\\x10\\x16\\xbd\\xfc5B<\\x99\\xef\\x15<\\n\\xa2^=\\xe1\\x17\\x12\\xbd\\x97\\xe8\\x0c=$\\x144\\xbd\\x9f\\x89\\xab;\\x18\\xbc\\x12=\\xa9o\\x81;\\xbb\\x1a$=}n\\x07\\xbd\\x1c\\x97U=\\x9c\\xbe_\\xbc\\xb5\\xa5\\x13<\\x06d\\xfe\\xbc\\xfc\\x10\\xc0<\\x01\\xc3/=\\x15v\\x01\\xbd\\x01;N<\\xc8\\x16\\x0b\\xbcm\\xa2\\x97\\xbcE\\x03\\x93\\xba\\xe9\\x84[=\\xc6(p\\xbc\\x14.\\x17\\xbd\\x91\\xbd\\xac\\xbc\\xae|\\x85\\xbc*\\xfdC\\xbd\\xfb\\x1d{=!\\x02\\xda\\xbcy\\xeef\\xbb\\xba\\x9aG\\xbb\\x84\\x7f\\\\<\\x19\\xe9*\\xbbf\\x0f\\xa8\\xbb\\xb6\\xe6\\x0e=\\x9b\\x8e\\xbd\\xbc\\xa6 \\n=\\xfe\\xfc\\x9b\\xbb\\xd1f\\xe1\\xbb\\xfcA\\x1b\\xbd\\x9f\\xa8\\xb1<Q0F=>\\xc4\\x8d\\xbdh\\xa4\\xde\\xbbL\\xb7\\x1b\\xbc\\xfcVR=G\\xf3&\\xbd\\x01\\xb9\\\"\\xbd\\xee\\xe9#\\xbd\\xe95\\x07=\\xa3\\xaf\\xff\\xbbw\\x15\\xab\\xbcBS\\xa9\\xbc|\\x02\\xdc\\xbb\\xd9\\xb0\\xc2\\xbcB\\t\\x85=2\\xfc.=\\xf3\\x8a\\xde\\xbcE\\xeeW;)e9\\xbc\\xa8N\\xe8\\xbbQr\\xab\\xbc\\xe2\\xaf\\x88=7\\xb14\\xbd\\xf5\\xab,\\xbd\\xc0\\xe8\\x07=\\x93\\xe4\\xc4\\xbb\\x96\\xc6b\\xbd\\xa8\\x95/=HH\\xc3\\xbd\\xea\\x12\\x01\\xbd\\xf3\\r\\xb7\\xbc\\x05t\\x04\\xbb\\xa6\\xc5\\x08<\\x9aE\\xa1\\xbd(4y:\\x8a7\\xa3<p\\xf1X\\xbc[\\x1cu=/I1\\xbck\\x05\\xdb\\xbc\\x8f\\xea\\x84\\xbd\\x8f-\\xd1<\\x9a%0=\\xb5*\\xbf<O\\x19k<\\x1c\\x02\\x8a<\\xf6\\x99L=\\xfb\\xa1\\xed;\\x89\\xcc\\x92<\\xa6g\\xe8\\xba\\xeb\\r\\x04\\xbd\\xd9\\x8c\\xc5<h\\xd9\\xba\\xbc\\xa61\\x92<UW\\x83=\\xc8\\xa4\\xd18\\xb0\\xe9\\x89\\xbdp\\xc5\\x89\\xbdD\\xe3\\xac;\\xddM\\x8a\\xbc\\xb4g\\x06\\xbd\\xa9&Q\\xbbo\\xbc\\x84;\\x9c\\xd7\\x92\\xbd\\xc3\\xb8S=\\xbe\\x17\\x1c\\xbd\\xcfy\\xaa<\\x19\\nY\\xbc\\xf7\\x18_=Z\\xbe\\x9b\\xbc\\xdf>\\xa9\\xbc\\x8b`B\\xbd\\xbds\\xcf\\xbc8\\t\\x1a\\xbd\\x0fh\\x84=k4\\xed\\xbbnP\\xf0\\xbb\\x11\\xe0\\\"=\\x8c\\x84\\xcf<\\xa3\\x13\\xb7\\xbb6\\x01\\x13\\xbb\\xa3\\xa9\\xc2;\\x8f=\\x06=\\x02\\t\\xd3\\xbc\\xa2\\x80\\x93\\xbc\\xdd_\\\"\\xbd\\x06\\x82\\xdc\\xbb\\x08 \\x89=0T\\x80\\xbd_\\xf5\\xe2\\xbc\\x8ea\\x81<1\\x85\\xa8;\\xe1\\xbaP\\xbcFL\\x14=DM\\x8a\\xbc1\\xe6\\xc8<\\x8a\\xf6\\r=g\\\"\\xee\\xbd\\x85|\\xc9\\xbb\\xbc#!=\\\\\\xbfR\\xbd\\xa5\\x8c\\x1c\\xbc(\\xc8\\xf2\\xbc),\\x9b<Y\\x0c<\\xbd\\xbf=)\\xbb\\xdc\\x80\\xb2<\\x0fx\\x08\\xbd9Q\\x8a\\xbcB*W\\xbc\\xdb\\x18i<\\x02}\\x1b=\\xfd\\xed`\\xbd\\x1a\\\"\\x12\\xbc\\x11W\\x01\\xbch\\xe4\\xfd\\xbcUy\\x97\\xbbzE\\x14=\\xf1-\\xcb\\xbc\\xf3\\x92\\xf9\\xbc6\\xce^\\xbd\\xf4\\xd1[:\\xdc\\x12\\\";\\xa8\\x84\\x15=(L\\xae\\xbc\\xe0=\\x07\\xbd\\xd3x\\xa9=\\xdf\\xfe\\x1d=:.\\xbd\\xbc\\xdd^M\\xbd\\x00\\xcc\\xb3<\\xaa\\x17\\x00\\xbd&\\x85\\x05\\xbcAh\\xae;K\\xb7\\\\\\xbd-5C=\\x92\\xd6?<~\\xbfI=taV<c\\x11g<\\x8aBi\\xbd\\n\\xd1\\xe5\\xbc\\x06\\x03\\xa9\\xbc.?O;\\xe9\\xc2y;)\\xa2\\x18\\xbc\\x14L\\x0e=!-\\x04=\\xe7\\x08\\x17=\\xe5]\\xdf;\\x8c)9<\\xc7Y\\\"=tv.\\xbd\\x9d\\x9el\\xbcsd\\xe4<J\\x9cp=g\\x0e\\xdd<\\xb60u=\\xf4r+\\xbb\\xe6\\xf0\\xc1\\xbc\\xa8\\xc3\\xbf\\xbc\\xd5\\xae\\\"=Rf\\xfc\\xbc\\x8bTn\\xbcs\\xd4\\xde<\\xa6\\x1e5\\xba\\xdc;y\\xbd]\\x1f\\xd4\\xbca\\xfd#\\xbd\\xf4\\xe9\\x80\\xbb$-~<Tw\\xe8<\\xb8\\xb9y<-c\\x8e\\xbc\\xb2\\xcc$=\\xc6y\\xc0\\xbcM\\xe3\\x01=\\x12\\x19>\\xbc\\x97$\\x00\\xbd\\xbeUU=\\xf3\\xcbV<\\x9b#\\x86<\\x8c\\x06\\xf3\\xbc\\xf4d\\xee<Y\\xf3\\x99=>\\xb5]\\xbd}\\xd0/\\xbd\\x07\\xab,\\xbd\\xcc\\xdbl=\\x9c\\xb90\\xbd\\xc2\\x04\\x87\\xbd\\x88Hw\\xbc\\x8d\\x17\\xb1<\\x07\\xea,\\xbd[\\x86\\x1b<\\xc6\\xbd)=\\x981L\\xbd\\x93f\\xb8=1\\x85*;\\x04\\x81\\x82\\xbd\\x9d!\\xbf=]\\xef\\x9a= g\\xb1\\xbc\\xa6$u\\xbbE\\xe5\\xbe\\xbc\\xa0r[\\xbd\\xe6q+=\\x90\\x87\\x93\\xbb\\xc3\\xa9\\xe3\\xbc\\xd9\\xd6\\x81\\xbc\\\"p\\xa0\\xbdQ\\xcfy=\\xd8\\x96G=\\x8cO\\xbd\\xbb\\x01\\x0b6;Mq\\xab<\\\\D\\\\\\xbc\\x07\\xd1\\xfe<yf\\xb6<3\\x8b8=l\\x84\\xaa:\\xd67\\xf2<\\xfe{#\\xbc\\x0b\\xa7\\x9f\\xbc\\x81\\x94.=;\\xb6\\x03=\\x96\\x99_<\\xcb\\xd2\\xd3<\\xfbc[\\xba\\x97\\x989;\\x1bE$<\\x8ai\\x05=)\\x1f\\x03=.\\x92\\xbb<fD4\\xbd\\xa2\\x1b\\x87:\\x88\\xe5\\x08=\\xb8>\\x88;7\\x1f\\x8b\\xbc\\\"\\t\\t=\\xa2\\xc3v\\xbd\\xb3D\\xb6\\xbc\\xbdS\\x95\\xbd:aX\\xbc\\xf0X\\x8a=\\xacN\\x19=>\\x1a\\x01=\\xf9\\xfd\\xb2<\\xa7\\xc9\\x00=\\xe2w\\x19=f\\xf2R;&~\\xf9\\xbcA\\x7f\\x88\\xbc\\xccv\\x1d\\xbc\\xc2x,;i\\x11C=\\x9e\\xb8y<\\x15hb\\xbd\\xf5J\\x0b<>\\x12 \\xbd\\xd55\\x94\\xbd\\x97\\xf9\\r\\xbdY\\x88\\x01\\xbd\\x10\\x0e\\n=5\\x11\\xa0\\xbc\\xf1\\xf4S<\\x9b\\xa8\\xfe\\xbcn\\x930\\xbd\\xc0U\\x16=\\x0b\\xa5p=w\\xbd\\xb7\\xbb\\xb1\\to=a\\xc0\\x1f\\xbc\\x90[,\\xbd\\xa8(\\xeb<\\xe9\\xe1\\xc3<\\xb1\\x9b\\xc6\\xbck]\\x13=\\xb2r\\x0f=\\xd1K\\xd7\\xbb\\xadr@=M\\xbeF=%Z\\x97</\\x92\\xb0<yY\\x16<9\\x03#\\xbc\\x1b\\xd7\\xc7\\xbc\\xcav\\x04=N\\x9c\\x93\\xbbD4l=*\\x0f\\x80\\xbc\\xe3\\x1d\\x98<\\xf3\\xb0p=|+\\x1a\\xbcEW\\xb2\\xbd\\xef/q\\xbdT\\xfe\\xb9\\xbc95`\\xbd\\xe0\\xf8\\xb8<D\\xbb]\\xbc+\\x16\\xff\\xbc\\x1c\\xd9\\x02=[\\xd3h;\\xb4\\xbfe\\t.o\\x05<\\tL\\xae<g\\x9a\\x8a\\xbcg\\xb6\\x05\\xbd}(\\x17<\\x12\\x1c-=\\xdb\\x88\\x15\\xbc\\xb0\\x16\\\"\\xbd@$d=\\xf9G3\\xbd\\x17\\x8e\\x82\\xbcWdH;7j\\xe7\\xbc\\xe4\\xc5Q=L\\xf0\\xdf\\xbcS\\x98E=s\\x1as\\xbd\\xd6\\x93-\\xbc\\xbdVo<\\xb2\\x90T=(\\x85\\x1e=$Q\\xb5<\\xd7@^<\\x04K\\xbc<\\xfc\\x16\\xad;\\xe2\\x96\\x11<B\\xc3\\x97;\\xf6Zk<Q\\xf7$\\xbd\\xb4\\x8f\\xc0\\xbc\\xcd\\xa7\\x9a\\xbbm\\xcfe<\\x00\\xa0\\xe7;\\x8b\\x945\\xbd\\xaa|\\x1d<\\x85\\xd1\\x8f<\\xf8\\xeb\\xd6<rJ\\x8f;\\xde\\x1au=\\xaa\\xbf\\xcb<F\\xd5r\\xbc-\\x9b#\\xbc9\\x11)=\\xcf3.\\xbbSh\\x0c=\\x07\\xc7%\\xbc\\x16\\xa6\\x06>\\xd7\\xd68<\\x7f6\\xa5\\xbc\\x08\\x05\\x19; \\xb5G\\xbd\\xb4)y\\xbd\\xd7\\xbdH={\\xd6\\x03=\\x96\\xed\\x87\\xbcd\\xe9\\x8c=\\xdfA\\x8d\\xbdf0(<\\xc2\\x85\\xa0;6T\\xda<\\x1ae\\xe4<\\x05\\xa5\\x88\\xbcTdf\\xb9{\\xae\\xe9;\\xf8\\xd0\\x14<F\\xd8/\\xbd\\xb5\\xc8\\xe9\\xbc\\xa4\\xa3N\\xbc\\xd0\\xf0\\x97;\\x88\\xdfo\\xbc\\xfb\\x03\\x07=\\r\\xe0\\x07\\xbc\\xe5E\\xa6<V\\xc3U=Vp\\xfb<\\xc2\\xd2o\\xbc\\x14\\xd9\\x16<G\\xa9\\xdc;\\xdc0\\xbb<\\xfaY\\xdd;%K\\x99\\xbc\\xd2\\x0c\\xbe:Z\\x08\\x13\\xbdy\\xf7T=\\xfa,K\\xbd\\x87\\xbaZ=\\xfdA\\xeb<\\xa6\\xb6\\x0b=>\\x8aH=\\x8e\\x1bu<\\xc2\\x80\\x82<\\xf6\\x1a;<+dS=\\xf6>y:\\x9d\\x91\\xa2< n\\xe3<l)\\x12\\xbd\\x1b]\\x8a\\xbc\\x0eP\\x9a\\xbb\\xf0s\\xd6\\xbc\\xac\\xef\\x90\\xbcq\\x8a\\xc0<^\\xf6\\x05:\\x1e\\x12n\\xbd\\xbd\\xc5\\xf1<t\\x0bk<\\xa9\\xb9\\xdb;\\x8c\\xb3\\x85\\xb9\\xb9;\\xf6\\xbc\\xf7\\xce\\x87\\xbcD\\x8d\\x8c=*\\x16\\x1a\\xbd4\\x08\\x1c\\xbd\\x07\\x9a\\x9f=\\n\\xc5\\x18=\\x91\\xed\\xcf<\\x8b>\\xad:\\xf9\\xe4\\n\\xbd\\x15*\\xf3\\xbc\\xe3\\x130\\xbdCW\\xa1;\\xd1Y7=\\x97\\xa9R\\xbcy\\x81\\xa0\\xbb\\xdd\\xf0\\x83\\xbc_I\\xd2:\\xc2\\xc5\\x99;\\x12\\xb85<\\x9b,\\x92=\\xe6\\xe3\\x19\\xbdS\\x02\\x8b\\xbd\\xfe}\\x9a\\xbd\\\\\\xd4d<B\\xdcQ<\\xdc\\x05t=\\xdb\\x1c\\x8c;\\xbaC\\x10=~\\xd6p\\xbd\\x14\\x9e\\x9b;\\xa9Ci\\xbc\\xb5\\xeb\\xb6\\xbc\\x1a\\x16\\x1f\\xbd\\xd8\\xb1\\xad\\xbb\\x8a\\xe0\\xd6:vg8\\xbc\\xe0O.=M\\xfes=\\xad\\xf4\\x81<I\\x95\\xbd<\\x92\\xa6\\xec<\\x9e\\xec\\xf3\\xbc\\xb9Z\\xb2\\xbc3U\\x94\\xbdR\\xae\\xba<Z\\x86\\x9c\\xbcZ\\xa2\\x98<SI\\x87\\xbd\\x12\\x02\\xa4=\\xe5\\x02\\xfb8\\xe9\\xa0f<\\x0c\\xa1\\x81<\\xa5ts<p\\xe7\\t;K\\x14\\xb0<8:.\\xbd\\xfe\\x94\\x08\\xbd\\x97\\x0b7\\xbd\\xf1\\xaf\\x19;jd\\n=\\x8eA\\x0b\\xbdj%\\xb6<\\xaeRf\\xbc&\\xe0\\xb5\\xbc\\x00Wg<\\xd5X\\x9b\\xba\\xbd\\\"$=\\xbd>;\\xbd`m\\x19=_H\\xb6<\\xa4\\xdd\\x1e\\xbd\\xc3}i\\xbc\\xb9\\x0b\\xb1\\xbd\\xaci\\x00=0\\xe03\\xbcG7?<uSM\\xbd\\xdc\\xd9K<\\xef`1<H\\xd2b<\\x99\\r\\r7\\x83\\xcbH\\xbbM\\xf1\\xcf\\xba\\x1f\\x000\\xbd\\x7f*c\\xbc5\\x96\\xb0\\xbddXe\\xbd\\x12p\\xbe\\xbc\\x14\\xeb\\xc3;\\x1f\\xfd\\x7f\\xbb\\xb9}\\xa1<\\xdf\\xb1\\x19\\xbdJ\\xcf\\x82\\xbb\\x11\\x90\\x11\\xbb\\xdb\\xdf\\xa4\\xbdS\\xacl\\xbb4\\xa1\\x88;\\xe1\\x96\\xf2;\\xfeE\\t;\\xb7\\x040<[(\\xdb\\xbc\\xc0i\\xb4;\\xce<&;\\xb3\\x96%=s\\xafG\\xbc\\x0c\\xde\\xc4\\xbcgt\\x91\\xb8\\x83\\x818=\\x04!\\r\\xbd/\\xb6\\x02\\xbd\\xc3+\\x9d\\xbc\\xb4\\x8f\\x07=z\\x1e\\xd5\\xbc@\\x8f\\xeb;\\x14\\xe6\\xb7<\\x89\\xfb\\xb9<z\\x8e\\x9f<\\x83\\xb6C=\\xab\\x93\\x88\\xbd\\x95\\xb5\\x9d\\xbd~\\xb2\\xe2<\\x81{\\xaf<\\x9e\\xb3\\x1f\\xbb_\\x88L\\xbd\\xf7L\\xcf<\\xe9\\x9d==\\xadeN\\xbb\\xca\\xf9\\xd1<\\xfc?[=\\x17\\xa4\\x13=[\\xd7\\x1c\\xbdx\\x827\\xbc\\xad\\x8a\\xa6\\xba3@\\x85=\\xdc?\\xa0<\\x18h\\xa8\\xbb8\\x80M=vo(\\xbd\\x15{=<\\x80\\xeb\\x07\\xbd\\x871i\\xbcV\\x90{<H\\x1d1\\xbc+\\x8ff<g\\xb4\\xae=\\xa7\\xad\\xfe\\xbc\\xab*\\xc1\\xbby\\xb3\\\\\\xbd\\x8dR\\x1b<\\xf5\\xb1\\xbb<\\x0e(\\xb6\\xbd\\x8d\\xc1C<e\\xf5\\x90<a\\xcb\\x03=\\xea\\xdd\\x15=b\\x1f\\x83=b%\\xd3\\xbcy\\x1a\\x06=\\xe1\\x18\\x9a=i[v\\xbc\\xcf\\x90\\x83<\\xaa\\xed<\\xbc\\xe39n=f\\xbf\\xcb\\xbc\"\nHSET bikes:10064  model 'Phoebe' brand 'Tots' price 3176 type 'Commuter bikes' material 'alloy' weight 16.0 description 'A real joy to ride, this bike got very high scores in last years Bike of the year report. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\xee2\\x8b\\xbcE\\x93\\xd9;\\xd9\\x0fR\\xbcU\\x9f\\x16=\\xc55k\\xbc\\xed\\x99L=0\\xaa@\\xbc\\xe8\\x93\\xdb\\xbc\\xc7fZ=2F-=;-5=\\x05R\\xd6;\\xc0x\\xdc<5;\\x96;\\xf8\\xa2\\xab=-\\xc7\\xea\\xbc+)j=\\xf1\\x9c\\xfd\\xbc\\xfb\\xd7G=\\xc4\\xd6|\\xbdB\\xe7\\x8e\\xbb?\\x1c+\\xbd\\xb1\\xb91=Ap\\x7f\\xbd\\xf4\\xbeZ\\xbd\\xd0_\\x9d\\xbb\\xcc\\xd3\\\\=[\\xc2\\x08\\xbc*TC\\xbde\\xd9\\xd9=yFD\\xbd\\x94\\x95\\x13\\xbd@\\\"\\x81<\\xe60\\\"\\xbb]\\xcf\\x9b\\xbcA\\x85\\xb2<[Y`<\\x14\\x0bO\\xbcD\\x93\\x1d\\xbd:_+\\xbc\\xc3+\\xa1=c\\x9c\\xbe\\xbc\\x13\\x8a}=\\r\\xe4\\xae<1BV\\xbc.\\xb8\\xaf\\xbcQ\\xb9\\xb3=a\\x924\\xbd\\x15\\x0fr\\xbcu\\x87\\xda\\xbb\\x8ax\\x19\\xbd\\t,1\\xbd\\xd7\\xcc\\x98\\xbc\\x18\\xed#\\xbc\\xc6v\\x1c\\xbd\\xb9\\xa8\\x92\\xbcP -\\xba\\xea\\x08\\xc5;e\\xec\\xea\\xbch\\x0e\\x84=\\xcbj\\x92;\\xc4u\\x18<\\x86I\\x86<3\\x9c\\xde<\\x06\\xf2\\xe2<\\r/\\x92<\\xb1\\x9c\\x0e:3\\xfe\\xfa<o\\x80\\x9e<\\x99|+\\xbd\\xe6\\xa5\\xb2<\\xb0nP=\\x98\\xbc0\\xbc6U\\x94<~\\xb78\\xbd\\x08.\\t\\xbc\\xe8\\xdf\\xc1<[B\\xba\\xbc\\xaf\\xac\\x9c\\xbd\\x91wX:;\\xae\\xc5<\\xe1\\xd3\\x14\\xbd\\x032\\x94<\\xc8\\x18\\x94\\xbd\\xad\\xd23<A\\xd7\\x05\\xbcI\\xeb|<\\x01d=<\\x93\\x04$=\\x8d;<\\xbaK\\xc7\\xaa\\xbc\\x9e\\xeb\\x1e\\xbd\\xe1P_\\xbd6\\x91\\xcb<|\\xb8\\xf5<\\xdf_\\xcb:\\x81p\\x1d\\xbd\\xfb\\xbe\\xff\\xbcA\\xc3G=3e\\xb1\\xbcb_E\\xbb\\n\\x9e\\xd5<\\x1d\\xff\\x06=\\xbb\\x1b\\x12\\xbd\\xaf1\\xb8\\xbb)0\\xc1=\\xb0,e9o\\x9f\\xa0\\xba8e\\x10\\xbb\\xe3\\xc3\\xa3:\\x93\\xcep\\xbc\\t\\xf0G=\\xc9U\\xdd\\xba\\x9f\\x98\\x8d\\xbc\\\"*I\\xbd\\x9a\\xa0;\\xbd\\xddl`<\\x86\\x16X<n\\xf9&=VW\\x1d\\xbd\\x0f|\\x02\\xbdV\\r\\xeb<O\\xf3{\\xbd\\xfc\\xb1\\x17\\xbd\\x83\\x1b@=\\xba\\xdd2\\xbb\\xd4i\\xf0\\xbc\\xa4@8\\xbd\\x02\\xed$\\xbd\\x99@3\\xbd\\x1c\\xd4\\xd2\\xbcC\\xc3\\x12\\xbc\\xdd@t;\\xac`\\x02\\xbd#\\xe6O<)\\xc9\\x05=\\xb7z\\xa6\\xbb\\x16\\x03\\xa3\\xbcK\\xd4\\xe4;n?\\x16\\xbd\\xe3}\\\"\\xbd\\xaa\\xdb1\\xbd\\x18\\xe7\\x95<\\x18\\xf2\\xe0=u\\xc9\\xd2<\\x9e^\\xa4\\xbcB\\xdd\\xd7<\\x93}\\x82\\xb9\\r\\xec\\xa7\\xbc\\x92i\\xca\\xbb\\xf5^*\\xb9\\x06VP=\\x15\\x9f\\xfc\\xbc\\x13\\xd5\\xd1<y\\xe1\\x8d\\xbd6xi\\xbbd.\\x1b<\\x1e\\x1dC\\xbdv5L=\\xeb\\x17-\\xbd\\x18_$=\\x80/\\xb9\\xbb\\xb1\\xf1\\r\\xbd\\xdd;\\x88\\xbd(\\x13\\x1b<\\x8b\\xce?=\\x01n\\x06\\xbd\\x8bY\\xb9<\\xeb*==\\xa1<\\xa8\\xbc\\x06\\xa9\\x8f<u\\xaf3=\\xc9\\xa7`\\xbc\\x85t\\xe4\\xbc8b\\xa6\\xbb\\x10\\x8f\\x10<\\x10\\x81\\xeb\\xbc\\xb8e\\x80=\\xafl\\xe5\\xbc ,}\\xbbvv\\x0f\\xbdn?N<\\xa8f)<\\x1a\\xccS\\xbcY@==7E\\xc4\\xbc\\x87\\xc2\\x1f=[\\x82\\x91\\xbbM\\xe3\\x1b;\\xac\\xb3\\x87\\xbc\\xcb\\xe2N=\\xbe\\xfay=\\xed\\xb6\\x05\\xbd\\xf4,\\x07=\\xa1\\xf8\\xb6\\xb9\\xa2\\x16h<\\x10j\\x80\\xbd{P\\x00\\xbd\\xb5*\\x12\\xbd6\\xb0$=q\\xc7\\xc0\\xbc\\xf2\\xbbR\\xbd\\xe9s\\xd4\\xbbt\\x12G\\xbc\\t\\x83=;2\\xed\\xcf<J\\x93P=\\x18LG\\xbd\\xe7`\\xb4<\\x9bN\\xe1\\xbc\\x8b\\xa0\\xac\\xbc/8\\x81\\xbcm\\xb8r=\\xf4\\xbb\\xac\\xbc\\xd9\\x99\\xe5\\xbb\\xaa\\xdf\\xd5;r\\\\b\\xbc4C\\x81\\xbdF\\x8a\\x12=#\\x9b\\xda\\xbdTLk;\\xbe\\x14\\x81\\xbc\\x9c\\xdc\\xbd\\xbc!\\x02\\xd6\\xbb\\\"\\x9e\\x01\\xbd9\\xbd\\x18\\xbc$\\xa6\\xfd<\\xa1\\x94\\xe4\\xbcI\\t\\x8c=^\\\"\\x04\\xbd\\r<\\x0f\\xbd\\xad\\xdc\\\\\\xbd\\xe9*\\xa6\\xbc4\\x0f\\x8d=\\xa0A\\xad\\xba9\\xd9\\x14=\\x80dS<\\x0e_i=\\\"\\xd6\\xc7\\xbb\\xe6;\\xbe<x\\xbf\\xae\\xbc;\\x9d\\x1b\\xbcv\\x96F=\\nUR\\xbb\\xc2\\xde\\x89<\\xe2p\\x9e=\\xde:K\\xbc\\x13\\x17\\xc6\\xbdc\\xd3\\xa8\\xbdY\\xb1S;\\x08U\\x1d\\xbb1+\\x83\\xbc9\\x98\\x85\\xbb\\xef\\xde\\x1f<\\x98\\r\\x9a\\xbd\\xa8+J=\\xfdE\\x9a\\xbcC\\x11\\x83<\\xf3({\\xbd\\xbd\\xe2\\x7f=\\xe2\\xb4\\xb8\\xbc\\xe5\\xa8\\xc0\\xbc\\xb5\\x1c\\x01\\xbdC\\xe1\\xb9\\xbc\\xa6T$\\xbd\\x86\\x89\\x98=\\\"J\\x9b\\xbcF6\\x10\\xbd\\xfc}\\xbd<\\xff\\x02\\xf0<>1\\xa2;6?\\xab;\\xa4\\xbb\\x03\\xbca\\xd3?<\\x83\\xdd\\x00\\xbd\\xdc\\x8c\\xc3;q\\xf4#\\xbd\\xc0\\xff\\x1d<$\\xbe\\x81=\\xbd0\\xd0\\xbd)}\\x03\\xbd\\xd7\\xa1J;\\x9b\\xe9\\x15\\xbd\\x1e\\xbf\\xf6\\xb9\\xed\\x9b\\x01=\\xf9\\xf6\\xa3\\xbba\\x02\\xa9<\\\"\\\"%=t\\x00\\x97\\xbd\\x94`\\x88\\xbb\\xe4:\\xad<\\xfb\\x1a\\xc7\\xbc\\xa4\\xab\\xba<Z\\x9b\\xe7;5\\xee\\x8d<\\x00\\xcf\\x1b\\xbd\\xfe4\\x93<A\\xf6\\x01=\\xdcB8;\\x9e[\\xd1\\xbc\\xf1\\x87\\xb4<\\xc8\\xf0\\xa9<\\xf5\\x16\\xfd<V\\x91\\\"\\xbd\\x80\\xce^<\\x06\\xbeN<\\xd3\\xd9\\x05:,Rq:\\xfcx\\x95<\\xdb\\x95\\x11\\xbd\\xfc\\n\\xf3;\\xc67\\x8a\\xbd\\x8b\\xf8S\\xbc\\x8d\\xb2?:\\xa6a\\t=a*)\\xbd\\xca\\x9f1\\xbd\\x1d\\xff\\\\=\\xe18W=\\x06\\xc6\\xa2\\xbc?1\\x0c\\xbdmI\\x80;\\xf5\\x0e\\xab\\xba\\x11W}<\\xfc\\x7f.<\\xce\\xbbl\\xbdA0\\xd4<\\x92.\\xb4\\xbc\\x04\\xa2\\x15<L\\xf4W\\xbb38\\xbf<\\xd3\\xe2\\x95\\xbd\\x98d\\x05\\xbb\\x9d\\xa7\\xdf\\xbb\\x17\\xf0`\\xbc\\xf8\\xc4\\x07<\\xbfh\\x18\\xbd\\xa7\\x1b8=\\xd6\\xb3y<\\x87f\\xd4<\\xdb\\x836\\xbc\\x12,\\x1e\\xbcBS\\t=8\\x02q\\xbc\\xc7*\\x07={\\x89\\x1e=^\\xbd\\x87=)\\xb3*=}\\xbcu=\\xda\\x80\\xb3<\\xd0\\xf1\\x8c;L\\xd1g\\xbc\\x85\\xb8\\x97=\\xea6\\xa2\\xbcp\\x1c\\x06\\xbd5\\xd7\\x0e=\\xd5\\x9c\\x8c;\\xb4+\\x8e\\xbd\\xd4\\xa8\\xa4\\xbcYa\\xc2\\xbd\\xfd]\\xc1\\xbct-\\xd2;B\\x99\\x81\\xba\\xbc\\x90-=\\xb0\\xeax<f\\x1aQ=0\\xa2\\xc6\\xbc\\xfaL\\xa5;y\\xef]\\xbd\\t\\xf1\\xf5\\xbc\\xbdC\\x83=\\xe8r\\xf6\\xba\\x94\\xa6\\x8a\\xbc\\xb6\\xb8J\\xbb3\\xe8\\x9a<\\xa7\\x8fj=\\xc8y\\x89\\xbdhS.\\xbd\\x03E\\xfb\\xbckj\\x0c=\\xfc\\xe5\\x1f\\xbd#\\x8f\\x80\\xbd\\xb0\\xf9(\\xbd\\x14\\xba\\xa6;\\xb5\\xa3\\x13\\xbd\\xc7\\x8a\\xa28\\xb53\\xcc<\\x857\\xed\\xbc\\xbd\\x15|=\\xfe\\xd7Y<\\xfd\\xe6v\\xbd\\xae\\xd8\\xb3=\\x12\\xfe\\xa1=V\\xc3\\xf1\\xbb\\xe9\\x91\\x1a\\xbc\\x0e\\xdam\\xbc\\t`\\xae\\xbd\\xb7\\xfb\\x98<\\x0f3\\xdd<\\t\\xf4\\x14\\xbd\\x87$\\xd6:\\xc3n\\xae\\xbd\\x06\\xed;=\\xc6\\xb6a<\\t\\x15>\\xbb\\xef\\xdc\\x8c\\xbb\\x99\\xed\\xea<\\xa4\\xd1\\xbf\\xbb\\xa3\\xc1\\x05=\\xb5\\xe8\\x08\\xbci\\xdfk=)\\n\\x04=\\x01o\\xcb\\xba9\\xdb\\x9f\\xbc\\xe9\\xd0o;#\\x85\\x1a={\\x05\\xa7<\\t]\\x04\\xbd\\x8e\\x90I<\\xf8\\x80\\xbf\\xbc\\\"c\\x8d<\\x17\\xf8)\\xbc\\xd6p\\x11<\\xdf\\x87\\\"=$\\xbc\\xd2;{\\x03\\\"\\xbd,v#<Zt\\x1d=V\\xdf\\xbc\\xba\\xec\\xe0&\\xbc\\xda^\\xca<~4\\x19\\xbd\\x0b\\x16\\xd0\\xbbhfs\\xbd\\xe3\\xa9\\xb2\\xbb\\xeb\\x86S=\\x1f\\xb4A=\\xc0\\xe9\\x14=\\xbb\\xae\\\"\\xbc\\r\\\\%<\\xc7\\r@\\xbc\\xddh;<l\\xbd\\xee\\xbcs\\xa2\\x06\\xbdBL\\xad<5.\\x1e\\xba\\xb94\\\\=\\x9b\\xc2\\x93;\\xb3\\xec\\x15\\xbd;h8=\\x13\\x95\\\\\\xbd\\xa6]\\x8f\\xbd1>\\xd2\\xbc\\xe2\\xe6b\\xbd\\x84\\xc6\\xe3<\\x84\\\\\\x1a;8\\xecK\\xbb\\x9c\\xbc\\xc5\\xbc!\\xef.\\xbda\\xaf7=\\xee8T=\\x11\\xb7\\xf3\\xbb\\xf5\\xae~=1\\x01o\\xbc\\xba,\\x96\\xbc\\x89\\xdf\\xdc<\\xcb\\xf2\\x0e=\\xbdN\\xbf\\xbc$_O<\\xfeyi=k\\xeaB\\xbc\\xad\\xa9\\x86<\\r\\xd0\\x16=\\xbc\\xb2,<\\x1f\\xfbo<\\xea\\x90\\xd2<\\xac\\x17^\\xbd\\xdf[\\xa3\\xbc\\xeb\\x80\\xc9<t\\xdd\\xcf:q+\\xf5<9\\xbc\\xc9;\\xe4Y\\xa7\\xbc\\xadK\\x1e=\\xd4\\x14B\\xba\\x19Y\\xce\\xbd\\xb6\\xf5\\xf9\\xbc#\\x90\\xae\\xbc\\xdb\\x04\\x04\\xbdq2\\x08=\\x94\\x86B\\xbd\\xdb\\x0f\\xa4\\xbc\\xc8b\\xdc<-^\\xfa;\\x1b6Y\\t\\xacP\\xdb<\\xb9I\\xb7\\xbcG\\xe4\\x16=\\xf3\\\"\\xad\\xbb\\xaa\\x17=\\xbc\\xa2\\xf2}=\\xad[\\xc7\\xbc\\x9fM\\xbb\\xbcu+\\x14=pM\\x80\\xbc{\\xd0\\x1e\\xbb\\xb1\\xb7\\x81<\\xc7\\xc1\\xa6\\xbcW\\xe8T=\\x06O\\\":\\xf2\\xba6=\\xd1\\xf7\\x9b\\xbd\\xe3\\xd8/\\xbd\\x83\\xba\\x85\\xbb/0g=\\xab$F<\\xdd\\xdd\\xd1\\xbc\\xbc\\xb11<\\xde\\x10\\xee<\\x89g\\x95;\\xfe\\xb5\\xd5;cJ\\x8e;\\x85n\\x0c=\\xb9\\x06\\x13\\xbd\\x13\\xd1\\xf0\\xbc\\xd9@2<\\xd1B=<H\\xb3\\xb5\\xbb\\xcd*\\x95\\xbd\\xf0W\\x9b\\xbc\\x03\\xb3\\xce<\\x15\\x1b\\xfd<\\xbc\\xfe\\x10=c\\xa5K=\\x10i\\xe5<\\xfeS\\xff\\xbc\\xd0l\\x19\\xbb\\x0f\\xd2N=\\xcc\\xee\\x96\\xbc\\x89\\xd9\\xd5<\\x08%?\\xbc\\x9d\\xdc\\xfe=n\\xf1\\xcd:\\x8cg\\xea\\xbc\\x8a;\\x94\\xbc\\xf5B=\\xbd\\xbc\\x17\\xb5\\xbd\\xd9R&=\\xb6\\xec\\x03<}\\xfb\\xab;\\x0ez\\x1a=l0q\\xbd~?\\xb3<\\x83&\\xb6<\\xf5\\xc4J=\\xbd\\xf3\\x89<V3\\x05\\xbc\\xc13l\\xbcN\\xad2=\\xdfj\\xc0\\xba\\xa1G\\x82\\xbdx\\x99\\r\\xbd.\\n+\\xbc8\\xff\\xb9<\\x8e\\xccw\\xba\\xfa\\x1f\\x0c=Z\\xc2\\x01\\xbda\\xa3\\xef<\\xb3\\xfd\\x1e=b\\xd1\\xa8<,!\\x80\\xbcn\\xce|\\xbb\\x1f\\x01\\\\=N\\xc1b\\xbc:\\xc5\\x0b=v\\xc1\\xe8\\xbc6\\x04\\xce\\xbbh\\xb4I\\xbd\\xf4\\xeaR=r\\xc7m\\xbd\\x87\\xd6\\x19=\\x1f\\x9b\\n=!\\t\\x83=\\r\\x1b\\x96<\\x88\\x93Z<\\x87\\x97\\xd6<\\xf8=\\xa0<\\xe6\\xab\\x17=\\xc7\\xe8\\x10:\\xd2 B=\\x99\\xb6N=\\x19sq\\xbc\\xb3\\xc5\\x11\\xbdj?\\xd2;?v\\xab<\\x85\\xe1!\\xbd\\x80\\xc6\\xe0<\\xa7EC:\\xf6\\xd7\\x00\\xbd\\xf7<;=\\x95/\\xe8\\xbb\\xe7>\\xce;_\\x8c&\\xbc\\x8d\\x9e.\\xbd\\xadV\\xb4<\\x18\\xa9\\x99={0G\\xbd\\x87Y\\x04\\xbdI\\xae:=\\xb4\\x0c\\x8e=\\xc5\\xdc@<\\x15\\x13\\x91<2u\\xa9\\xbceb\\x0e\\xbd\\x8d\\xf5*\\xbd*\\x07\\x05=\\xbf^\\x9c<\\xb6\\x9d~:\\x94\\xa8\\xfb\\xbb\\xd9\\xa2\\r=\\xf2\\xd8\\x9f<\\xa4\\xe1\\xd6\\xbc\\x10\\x1c\\x1c=\\xd6\\x9b%=2\\x073\\xbdVzZ\\xbd/\\xc2p\\xbd\\x0f\\x16\\x1d=kA-\\xbc\\xddL\\xab=\\\"\\xb3g\\xbc\\x86\\xeeC=\\x9d\\x805\\xbdx4\\x90\\xbc\\xbb\\xe8S\\xbc\\xbb\\xbc\\x86\\xbbGK\\x9b\\xbd \\xa3\\xc8\\xbblR@\\xbc[\\xe0\\xbe<P\\x07\\xb7<^}\\t=3\\xca\\xc9\\xbaV\\xb1\\x0e=\\x87\\x19\\xf3<\\x05\\x0e\\x9a\\xbc\\xd5\\t\\x13\\xbdZ.A\\xbd\\xa18:=\\xe7f\\x85<\\xea\\xde\\x98<\\x1b>\\xee\\xbcqb\\x98=\\xd2\\xa1\\xc2;\\x9c\\x8f\\x1c\\xba\\xf8\\xca\\xb9<\\xa3\\xda\\x96<\\x8d\\x93i\\xbc\\xdc\\x85@\\xbc\\x9a\\n\\x18\\xbd\\x8e\\xcd\\xa9\\xbc\\xda\\x82Z\\xbd|f\\xf3\\xbcUX\\x03<`\\xd5U\\xbd\\xe6\\x8a\\x1e<\\xbd \\xee\\xbc\\xda\\xef);\\xd6/}\\xbcK6\\xfd\\xbc\\xe4\\x82*=*\\x96\\x04\\xbdb\\x91\\xe9\\xbbY@{=\\xf0B\\xc5\\xbc\\x9e\\x86Q\\xbbW\\xb6\\xce\\xbd\\r\\x93b=l\\x9dj\\xbc\\xc5M\\xad\\xbb&\\xd5V\\xbds\\xc6o<\\x1c\\xac\\x9c\\xbcr\\x9a\\xe3<Q\\x12v:\\x90\\x973\\xbd\\x9c\\x0f!=\\x18\\xc0\\x18\\xbd\\xcbg\\x94;H!\\x99\\xbd\\x86\\x96\\x05\\xbd(\\x01\\x94\\xbc\\xe6+\\xa7<$\\x99\\x14\\xbdCG\\xae<\\x1dx?\\xbd\\xb2\\xd0\\x8c\\xbc\\xea\\\\H\\xbc\\x99b\\xa7\\xbd@\\x02\\x89\\xbc\\xe3\\xd6\\x7f;\\x8e\\x8d%\\xbc%\\xde\\xd2\\xb8\\x963\\x0c;\\xc5\\x80a\\xbd\\x13\\x99\\x04<\\xe8\\\"\\xab\\xbcf\\xf4\\xc8<\\xfbC\\xca\\xbcA\\x8a\\xfe\\xbc\\x18\\x85\\x14\\xbbD+\\x03=dJ\\xaf\\xbc\\xa9\\x0b\\xac\\xbd\\\"\\xeeX\\xbdi]B<\\x0b2\\x0c\\xbd\\xab\\x06\\x07=#t\\x0f=\\xfb\\x8dS=\\x17\\xd3Z<r\\xacF=\\x7f\\\"@\\xbd\\x8c\\xab\\x8d\\xbd\\xfdr\\x06=<V\\x97<\\x02\\xeb,=\\xe6\\xe1\\x16\\xbd\\xa2e\\xfa\\xbb\\xaf\\xdd\\xd3<\\xf3\\x89\\n\\xbd\\xca\\x93\\xf1\\xbam\\xbf\\x0e=<N\\x90\\xba\\x82U\\x8d\\xbco\\x9f\\xde\\xbb\\xb2\\x19-<\\x0fx\\x89=U-\\xdc<\\xae2\\x15\\xbd\\xab\\x82.=\\xd8\\xc1\\x90\\xbcqv\\xf1\\xba/z\\x04\\xbcv\\xf61<KY[<f\\xc4G\\xbc{\\xda\\x0c<\\xe4\\x06\\x7f=\\\"\\xf4\\xd2\\xbcL\\xf7\\xfa\\xbc\\xa5Y\\xa6\\xbc\\xdc\\xcd\\x88\\xbc\\x90\\xb0\\x16<\\xc5\\x9bf\\xbd\\n\\xa2\\x85<\\xcf\\xb7\\\"=\\x11Lt<s\\x1c)=\\xcd\\x81\\xb5=Y7y<\\x80\\xa5\\x1c=\\xba\\xc8\\xd2=O\\x0e\\xa0\\xbc\\xd5\\x8fu<\\x9bi\\x90;{X\\r=\\x81\\x83\\x9b;\"\nHSET bikes:10065  model 'Dione' brand 'Bicyk' price 3739 type 'Enduro bikes' material 'carbon' weight 10.5 description 'The new version with 142mm rear, 160mm front travel is longer and slacker than its previous generation, but it’s also a bit taller and steeper than much of its competition. It has a lightweight frame and all-carbon fork, with cables routed internally. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"_\\x83f\\xbc\\x0c\\x1f\\x17=\\xbc\\xed<\\xbc\\xc3]\\xf3\\xbb\\x18\\x12;\\xbctpr==GS\\xbdH5E\\xbd\\xbd\\x9c~=C\\xf8\\x86:\\x13H\\x15\\xbcW\\xe8\\x81=\\xf2\\xe4\\xf2<\\xad\\x02\\xab\\xbc\\xa1\\rD=\\x0e\\xacB\\xbd\\x83\\xbb\\x83=%\\xe1\\xd1<A\\xe4\\xa0\\xbc_\\xbd\\xcc\\xbc\\xb5\\xf0|<\\xf5\\xad\\xfc\\xbc\\x89d,=\\x9b\\xd9\\x9d\\xbd\\xc6\\xd0(=U\\xa8\\xd7\\xbck\\xeeY\\xba\\xdex8=\\x8bW\\xda\\xbc\\xbb\\xd1\\xd8<\\xa3\\xfe\\x9e\\xbc\\xce+\\xb6\\xba\\xb0\\x90\\xff<\\rR\\x0c=\\x9cE\\x1f\\xbc\\x89x\\xe0;\\x8fM\\xaa<ksG\\xbd\\xf1\\x85\\x82\\xbb\\xdd\\xf4\\xa0<pR\\\"\\xbbu\\xe5\\x89=\\\"J\\xaf=Nk\\x14=El\\xe6<\\x1cs\\x9c\\xbb\\x87\\xe5p=O+\\x1e\\xbb\\x817\\xea\\xbcxs8;~u\\xd4;\\xa8\\xa9\\xf8\\xbcAD\\x93<\\xda\\x1c\\xe5<\\xd2\\x07\\xb0;\\xce\\xc8\\x96\\xbd\\xc0\\x90\\xa6\\xbc~Q \\xbd)\\xb1e\\xbd\\xf5u\\x91=[\\x04\\x1a=9\\xbc\\x06=\\xa1\\x16\\x1d;\\xc6\\xb1\\xa0=\\x19\\t\\xa2<\\xa4\\xa1\\xbf\\xbb\\x95M\\x81\\xbc6\\xb3\\x9d<O\\x9c\\r\\xbc\\xd9}\\x84\\xbcwh\\xeb<\\xdc\\xf0&<\\xd8\\x87,\\xbd\\xd81\\xc8<@)n\\xbd\\xd6d3=\\x8b\\xcae<\\x01\\x80\\xcd\\xbcdJS\\xbd\\x17\\xac\\r:\\x92l\\xe7<\\x05[\\xb1\\xbd+\\x86\\xe1;%\\xdc\\x85\\xba[\\xd7\\x91\\xbc\\xf4T\\x05\\xbd6\\xc3\\x0b\\xbd\\tU\\x03=B\\xdaH=\\xd4IH\\xbcBG[\\xbdV)\\x0f\\xbd\\xa7An\\xbd\\x95\\xa7\\x13\\xbbf>A=O\\xee\\xe1<\\xb1$\\x17:?`Z=\\xf4}\\x83\\xbc\\x11\\xb5\\x1c\\xbd\\xe7\\x01\\xae;Nj:;\\x05\\xadG=b\\xaa\\xd6\\xbb\\xc4\\x13(=O\\x84+=h\\xc4U\\xbco\\xb9\\x19=AG\\xac;_s\\xb2\\xbc\\xe9\\xc3T\\xbc\\xf7\\x9e\\x98=0=\\x86<\\xa5j\\x93\\xbc\\xef#\\xd8<\\x83\\xc9\\x87\\xbd:\\xaa\\xe7;\\xc0\\xbcV<\\xb6\\x01u=\\xeePs\\xbc\\xb32\\x82<(\\\\\\xcd\\xbc\\x1b\\xd6n\\xbdR\\xfcc\\xbc+\\x1c,<\\x82c\\xe0\\xbb\\x95\\x10\\xd0\\xbb\\xa8\\xdc\\xe9\\xbc\\x02If\\xbd%\\xabc\\xbc\\xd9\\x89\\x18=9\\x0bY;\\xbe\\xac\\xb5\\xbby\\xbd7;P\\xdb\\xbd\\xbb\\x9f\\x18?<\\xf0\\x94@;\\xe6\\xfd$:\\xe1O\\xe9<\\xfb\\x85\\x94<~\\xeb\\x1d<.\\x99\\xff\\xbc\\xd1\\x88\\x1b;\\x8cL\\xa5<\\xa0B\\x94<\\xc5\\xaf\\xa1\\xbcUs\\xb2<\\x83\\xf0\\xb9\\xbc\\xf5\\t\\x95\\xbc\\xa9\\xf2\\x08:\\x81\\xbe\\x16=,\\xfar=R\\xc9\\xca\\xbc-2z=LP\\xa4\\xbc\\xcbq\\xf0\\xbc/\\x00\\xd4<T\\xe3t<r\\xf9\\x99<\\x9b\\xbeg\\xbd\\xb1\\x96\\\"=\\xd9\\x82\\xab<\\x87\\x10U\\xbc\\x81E\\xa0\\xbd\\x1b\\xc3\\x8a\\xbd\\x01ky<^\\xc4c\\xbc\\xfaTg<@\\xc6\\x1b<\\xbcJ\\x04\\xbc\\x91\\x83\\x0b\\xbd\\xaf\\xc4A=Y9\\x16=\\x8a\\xf8K\\xbc\\xcdv;\\xbb\\xe80p\\xbd\\xa4\\x9a\\t\\xbdG\\\"a=\\x8ceD\\xbc\\x8b\\xcdB=\\xa9\\xa3\\n\\xbd\\xb19\\xbc<\\x80\\x07\\xb8\\xbc\\x85d\\xbd\\xbbg\\xfa\\x1e=\\x0e)\\x1a=3\\x9f <R\\xdb\\x06\\xbb\\xf0\\xfa\\x01<\\xf2\\x0bK\\xbd\\xf3\\xe2\\x15\\xbb\\xa2`a=^Df\\xbd\\xecs\\xa7=\\x87\\x12t\\xbc\\x06\\x91\\x1c=\\xec\\x1a\\x06\\xbd\\x83\\x1b\\n\\xbc\\xfb\\xf9y\\xbcY\\x8e\\x90\\xbc\\x1c\\xc7]\\xbah\\xe5\\xf4\\xbd\\x01\\xcc\\xb9\\xbao\\xbb\\x80\\xbc\\xf1\\xf6\\x8e<:\\xfe\\xa5<\\x87\\x9f\\x83<\\xf1\\xbcG\\xbd\\xd7D$\\xbc\\xa6\\xe9]\\xbd\\xfa\\xfe\\x8c\\xbc\\x821\\x8a\\xbb\\xd5+\\\\<\\xf4\\xa3M\\xbd\\xfa\\x89f<\\xdf3C<\\xa6\\xab \\xbdT\\x07\\xeb\\xbc\\xb0\\xe2R<\\xd6\\x1e\\xe5\\xbd>(t;\\x01\\x06\\x83<\\x89~5\\xbd\\xe2\\x93\\xa1<\\x10\\x15\\x08\\xbd\\x88\\xc9O<[~3=\\x18\\x0bF\\xbc\\x9f\\xd0!=Ch\\xe5\\xbc\\xae\\xbe\\xa0\\xb7\\x029\\x12;\\xcca5\\xb9$\\xc6\\x0f=F\\xaa\\xa8\\xbbH\\xefa\\xbb)\\\"g<\\xf0\\xab\\xb2\\xbb\\xe4\\xf1\\x08\\xbd\\x84\\x02U\\xbcI\\x1f@;3\\xaf\\x02\\xbd\\xfb\\xe4\\x8d<\\xc0N\\x88<\\xe42\\x12\\xbd\\xb7\\xbdF\\xbb\\x7f\\xb4\\xe4\\xbb_\\xfd\\xa6\\xbc\\xe5Fd\\xbd\\x1a\\x0b\\x15=W\\xc6\\xa5;\\x8fM*\\xbbfBq\\xbc\\xceR\\xd4=\\xd0\\xd5\\xbc\\xbcR\\x91\\\"=\\xf1\\x83\\x1e\\xbd\\xf9\\xac\\x12\\xbc7\\x0b\\x15\\xbd\\xcc\\x80<=\\n\\xed\\t=\\xbc{=\\xbc\\xb7kp\\xbd\\xb8{\\xab\\xbcrUO\\xbd\\xd7\\x85^=\\xac\\xa3\\x03\\xbd\\xd8\\x95\\x8b\\xbb\\xac5\\x87\\xbc\\x0eD\\x1c\\xbc\\xe5\\x94^=\\xa1\\x80s<\\xa3\\x126\\xbd?:\\x13<J\\x07A\\xbc\\xa6T\\xa0\\xbc\\x84\\xd3\\xe3\\xbb\\xde\\xe8\\x9f<\\\\\\x05\\xea<@\\x8d\\xb1\\xbd\\x81@\\x9e\\xbc\\n\\xfa\\x19=\\x00~\\x0f\\xbd[\\xbb\\r;\\x92\\xd9\\r\\xbb\\xd7\\x8ck=\\x0c=\\x12\\xbcZ\\x18\\xf09\\x19\\xfa\\x9a\\xbdD\\x96\\x18=\\x07\\xff,\\xbbP\\xb8\\x03\\xbdY\\xb7\\x80=\\x1e\\xef\\xa1\\xbcg\\xc5m\\xbd\\r\\xb9\\x82\\xbb[$\\x14=_\\x80\\x0e=\\x1c\\xcbB=\\xa3\\x06\\x94\\xbd\\xc9)\\xa3<\\xc8\\x978;\\x8d\\xcdt=\\xd3\\xd4\\xa8\\xbc~\\xf2\\x80\\xb9T\\xcb\\xba<C\\xd6\\xca\\xbb\\xab\\x92\\x8d<\\xcf\\xa9\\x10=\\xe5\\xe0\\xad<`\\x93-\\xb9\\xd7\\x88\\x97\\xbd\\x07W\\xa7\\xbc\\xbf\\x89C\\xbd^az:\\xab\\x06P\\xbdSb\\\"=\\x9a\\xa8\\x94=\\xd3?<=Y\\xe7%<\\xef~-\\xbc\\xa0\\xef\\x97\\xbd\\x93\\x0c\\x12<\\x11\\x9d\\x13=\\xc9\\x90y=\\xb5\\xc8\\x07\\xbd\\x05R\\xdf=3[\\x06\\xbd*\\xdc\\xa9\\xbc-\\xe3\\x87\\xbc\\xec\\xea\\x82=dbz\\xbdOW\\x07\\xbc\\xbd\\xa4\\xe8<\\xd5\\xaa]<\\xbb\\x13N\\xbb\\x04\\x01\\x15\\xbd+g\\xea\\xbc\\xd8\\xc7\\x83\\xbc\\xc4\\xf9[\\xbc\\rG(;(a\\xe7\\xbc\\x02\\xcc\\x1a=\\xe7.\\xb3\\xbc\\x98\\xebP<\\xc7\\xbf\\xd2\\xbb\\x1f\\xb1\\xa6=\\x88\\xa7^<ELL=y\\xe0\\x8d=y;i<\\xf9\\xfc\\xa9\\xbc\\xcd\\x11E=\\r\\x12\\x11=\\xc0Y\\x11\\xbd4v\\xef<\\xd9\\xf2W<\\xe7\\\"\\x85\\xbdR\\xf3\\x84<F\\xdf\\xd6\\xbd|`J;\\xabZ\\x13\\xbdN\\x0f5=\\xd3b\\x8b<\\xcd.w=p\\xbb\\xa9\\xbc\\xf8nN\\xbd\\x80{\\x9c\\xbb\\x88\\xd8R\\xbc\\x96\\x80\\x08\\xbd\\xa0\\x14\\x81=\\x87h\\xb8;(\\x8e\\x7f<Fs\\x91\\xbc6jb\\xbd:X\\xe2;\\xb9E\\xc2;fb\\xcc\\xbc\\x962\\xb4<\\x83\\xffA\\xbb\\xbf@\\xac\\xbc\\xec\\xc7A\\xbc\\x87X\\x1d\\xbdyz\\x98\\xbbq\\x14\\x8c\\xbd\\xfd\\xc1?<0\\xca,\\xbc\\xba\\\"E\\xbc\\xed\\x9d\\xee\\xbc\\x11r\\xac<I\\xbb\\xa6\\xbc#\\x04w=o\\xc4\\xcc=\\x86;\\xd6\\xbc\\n\\xc4-\\xba\\xb8f\\x9e< e\\xf3\\xbc\\xd6\\x18\\x1d=\\xcc\\xd9I\\xbc\\xa7\\x0e;\\xbd\\xc4\\x1c\\xf9\\xbb\\xf5\\\"\\x94;\\xe9JL=v\\xf5u=\\x02\\xe1\\x89\\xbb uP\\xbd\\x13\\x8c\\xea;E\\x1d\\xc2\\xbc\\x84\\x0e\\x9f\\xbc,\\x83\\x7f\\xbc\\xa9\\x8e2<\\xa9\\xf4\\x80;G@\\x90<*\\xdc5=\\xd28\\xf9<\\x92\\xe2\\xdb<h\\xc9\\xcd\\xbcm\\xdbL\\xbd\\xcf\\x8f\\x10\\xbdUE\\x0c\\xbc\\xff\\xe8\\xb4<\\x8ay\\n<\\x16\\x12\\xfa<\\xad<)=Ej\\xc9<\\xfd\\x8bk\\xbba\\xad8\\xbdB\\xae\\x02=\\xb9\\xb3\\x84<\\xa6\\x8cF\\xba\\xe8\\x8a\\xec\\xbc\\xc8\\x0f\\xaa\\xbc?\\\"\\x1d=j\\x89\\x06\\xbd\\xad\\x84\\xa0\\xbc\\xd9\\x13^=\\xa7\\xc2+=E.\\x82={uS\\xbbL\\x98N<\\xeba\\xf3<\\xd1\\x8c\\x9f\\xbc\\x7f\\x96\\xd6<\\xb2\\xb6M\\xbd\\xff\\x07\\x90<\\x16\\xe6\\x8c\\xbc\\xb3\\xb7\\x8e=\\x9fQ\\x1b\\xbdK\\xab\\xe5\\xbcm6_=#\\xc1\\xe2\\xbb\\xf6H\\x88\\xbd\\x1bE\\x11=\\xaa\\x87\\x1f\\xbd\\xa7\\xa6\\xdf\\xbc\\xb0\\x08\\xa3\\xbc\\x7f\\xbe\\x15=J\\xf0A=\\x0b\\x9fg;\\xab\\x8a\\xdd<\\xa9&$=*6\\xa4\\xbb\\xf4\\x1a\\x80=I\\xb57\\xbdL[\\x9f;\\x8d\\x10\\xb3<\\xc0\\xd7\\xa9<\\xe0!5\\xbcg]?<E\\x1a\\x19=1\\xaeR\\xbc\\xa3\\x8c\\x179\\xdc\\xfd\\r\\xbc\\xf7\\x88\\x1a=\\xbf\\x1a\\x06<\\xbad\\x91;C\\xc25\\xbd<V\\x80\\xbcqn4\\xbb{\\xa7\\xe0\\xbb\\xba\\xe4%;\\xc9\\x7f\\x9d<z\\x89\\xc0\\xbc\\x1c\\x8cT\\xba\\x96Y\\x0f\\xbd\\xfd\\xf6\\x8e\\xbd\\xf4\\x95p\\xbc\\xdd\\xd1\\x909\\xb0\\xa2u<L\\x95\\x12=\\t}\\x00\\xbdWF]<\\xad\\xa8q=\\x8d\\r\\xa3\\xbb\\xbevR\\t\\x0c\\x08H<\\xbc\\xda\\xa9\\xbdvm\\x12=C$\\xc9<\\xc5\\t\\x15=\\x1f\\xec\\x16=\\n\\xcfs<L\\x05!\\xbdJ\\x9dx<\\xd8<\\x91\\xb8Sv\\xd7<>\\x14\\x81=\\xf6\\x1b\\xf1:\\xf5[d=\\xf3d\\xd9;3\\xb5]<\\xd3[\\xa2\\xbd\\xf3\\xfe\\x8e\\xbc\\x19\\x81\\x9f\\xbc^\\xcc\\x9e<\\x1bI\\x8c\\xbc\\xce<\\x8a\\xbc\\x13\\xaa\\x92\\xbb\\xb1\\xdc\\x17\\xbd\\x0b\\xa8\\xee\\xbb\\xdc\\\"\\x02\\xbdS\\x14y<\\x93g\\x1b<\\xb5{\\x91\\xbc\\xb2@@\\xbb\\x98\\xf8\\t\\xbd\\xf3\\xb3\\x01=\\x99\\x0c\\x14\\xbdf$x\\xbd,5\\xf7\\xbcA2\\t<\\xe2\\xec\\xcb<1\\xec\\xb5<\\xd7\\xf9k<\\x85\\xa8\\xb9;&1\\x8d;\\x95\\x7f\\x97<\\xce\\xbdi<\\\"\\xf9\\x90=\\xa0\\xac\\x1b=\\x96\\xca\\xa3\\xbc\\x7f\\xdf\\xf9=\\x95\\xaao\\xbc\\xf1\\x0b_;\\xaa\\xd4\\xce\\xbc\\x84\\x00 \\xbd\\xd4h\\xb4\\xbd\\xeb\\xbc\\x1b=`\\x89J\\xbc\\x12K\\xa3<a\\xf0\\n=\\x9bA\\x06\\xbd\\x9a\\xe6\\xc9\\xba\\x8d\\x0e\\xa1<F\\xc0\\x9d<\\xb9>r=\\xb3s\\xad\\xbc\\x04|2\\xbd\\xa0s\\x91;\\xee\\x04<\\xbdX\\xe28\\xbdN\\x97\\x01=`?\\xe4\\xbcu\\x0ch\\xbc\\xaa\\xc0\\xfc;\\xc2N\\xc0\\xbca\\x022\\xbck\\xb6\\xff<\\xf3\\x80\\x94=\\xc8\\x95\\xf3<\\x9a\\xad\\xd1\\xbc\\xbc\\xa5\\x9b\\xbcy\\x9bs=a\\xc8X\\xbd]\\xa7Z<=Km\\xbd\\xf3p\\xbe<\\xf2\\xbd\\xaa:{\\xce\\x9c=\\xe0\\xcb\\x95\\xbd\\xf6sr\\xbb,\\xe9\\x87=>\\xb1\\x11=o\\xbd\\x9a;\\xab\\x1f\\x13\\xbc\\xe1\\xee\\x17<M\\xa2\\xcb;\\x0b\\xb4\\x96=\\x96M\\xf2:8\\x8bW=M\\xa2\\xf1<\\x07W)\\xbdc\\xb3\\xca<\\xe6!\\xfc\\xba\\xc8\\xaa\\x98<\\x1c\\x98\\x02\\xbdJ}\\\"<qv%\\xbd\\x19\\x11*\\xbd\\xc8\\x1e\\xb0<\\xd5\\x89\\x04\\xbd\\t\\x8b\\xa0\\xbc\\nP\\x7f\\xbc\\x8a\\x81\\r\\xbb\\xf0t\\x84\\xbcD0\\xa3<l\\xedO\\xbd\\xccm\\x19\\xbd\\xf3\\xa1[<\\x93\\x1b\\xf9<\\xb3\\xaf\\x04<\\xf5\\\",=0\\x8bz<\\xcf\\xb1\\x04\\xbd\\xec\\xc63\\xbd\\xdf\\xca\\xec\\xba\\x87\\xb8k\\xbc<~_<\\x9d\\xa0\\x84=\\xcc\\xd0\\x81<\\xbf\\x9a<;\\xb0\\xd2\\\"\\xbd\\x97\\x84+:>b\\x81=\\x88N\\xd4\\xb8\\xaa\\x89#\\xbd\\x90\\x95\\xac\\xbc\\xb1\\x8a\\x1e=\\xf3\\xf4\\xf6<\\xb2\\t\\xa5=\\xcc \\x18\\xbd\\xc5\\xe2\\xaf=o\\x95\\xf7\\xbc\\xceD\\xaa<3\\x07k\\xbd/\\x9a\\xc2<\\xe5\\xc5~\\xbd\\x19\\x96\\x05\\xbd\\x07\\xf0\\xa4\\xbb\\xbc2\\x0f=\\xad\\xb4I\\xbdJ\\x01\\xa4=\\x10v\\xbc<\\x8c\\\"\\x86<0W\\xa1\\xbcz\\xa7L\\xbdb\\x03\\xee\\xbcO\\xb6\\\"\\xbb+\\x91\\xf8<\\x80a\\xb8\\xbcR\\xec\\\"=\\x9f<)\\xbd\\xe9\\xdf\\xd8\\xbapy\\x1e\\xbd!\\x0b$=\\xe9&\\xd0\\xbc\\xf7\\x03;=vuZ;sW\\xc0\\xbbXr\\xec\\xbc\\xf1aK;\\x98\\xaa\\xc3\\xbcE\\xf1\\\\=\\xf9*\\x89=\\xdc\\xa7\\xb8\\xbd\\xd36\\xbf\\xbb\\xf3|\\x91;c\\xa8\\xa4\\xbb\\xd9\\x1cL\\xbd>\\x8f\\xcb\\xbcI\\xdb\\xb6=\\xday\\xa8\\xbb\\xe9\\x0c\\xaf\\xbc8\\xef\\x18=%\\x05o\\xbaB%.\\xbc\\xe5\\x1b\\x96<\\xd2\\x9b\\xdb<8\\xf1u\\xbd\\xc3H1\\xbd\\xe4\\x80]<b)\\xde\\xbcSbV\\xbdB\\xac>=\\xe1@\\x01\\xbc{~\\xbd\\xba\\x89\\x84\\xe9<5\\xadw;\\x93\\x0f \\xbd\\x85/\\x11\\xbd8\\xc7.\\xbc\\xab+)\\xbdR\\xaf\\xc3\\xbc\\xcd\\x9dV\\xbc\\x97s\\x8c\\xbdbgd\\xbd1r\\x0f\\xbd\\xb3\\xdc\\x9a\\xbc\\x1eA\\x94\\xbd\\x113_\\xbd\\xcf\\xda5\\xbc~u\\x8f\\xbbik\\x99\\xbc\\xb6\\x94\\x12\\xbd\\xa8\\x83I\\xbd\\xd0\\x9d\\xad<\\x06\\xa2\\x8c\\xbc\\x86\\x04\\x02=\\x07\\xe3\\xd6\\xbc\\x93|\\x03\\xbd\\xe3\\xb5\\x85\\xbb\\x8c\\xff\\x04=I\\xdb\\xef\\xbci\\xed\\xf7\\xbc\\x1as8\\xbd\\xbf\\x7f\\xd4\\xbc\\xda\\xb5\\xa9;\\xad\\xca<=Y\\x13\\xcb<v\\xe7e=\\xea4B\\xbb\\x15\\x1cH=/Ou\\xbc\\xfc*\\x97\\xbd\\xf8\\x87\\xee;-\\x81\\xc8\\xbc-Vk=\\xda\\xde\\x9b\\xbc\\xa8L\\xb8\\xbcdFj\\xbc\\x84b\\xa7;\\xacD\\x00\\xbd\\xf9\\xbe\\xa9\\xbce\\x8d&\\xbc\\x130\\x8d9\\xc6\\x18\\x1b=nG\\xd4<\\x82\\xc5\\x8c\\xbb\\xf7,\\xc9<\\xfd\\xcd`;W-\\x86\\xbc\\xb20\\xed\\xbc\\xff\\x8f\\x82<\\xd45 \\xbd\\xb3\\xa8\\xe4\\xbbr\\x88\\xb4<\\x92\\x02\\xbb\\xbb\\x04\\x91\\xf3<\\xe6\\xc1\\\"\\xbcf\\xf2\\xd2;\\xb5\\xed\\xc1<m\\x82v\\xbd\\xab\\xeb\\x08\\xbd\\x05PM\\xbd\\xe6]\\x88\\xbd\\xb4%\\n=f\\x01\\x8d=J\\xcb\\xdf9Z\\xe5L=u$\\xc7=&_\\xb6\\xbc@m\\xda<<E\\xae=\\xa0\\xb8\\xdd\\xbc\\xbf\\x17\\x11=\\xc0QR\\xbdd\\xf8\\xb4;\\t\\xa4B=\"\nHSET bikes:10066  model 'Janeway' brand 'Ergonom' price 1794 type 'Commuter bikes' material 'aluminium' weight 14.5 description 'The perfect commuter bike for anyone who is constantly rushing around, and prone to forgetting to charge lights, maintain their bike, or not quite getting round to checking weather reports. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"\\x8c\\x01\\xbf;\\xc9=G=|\\xa1;\\xbcbFT=\\xcf\\xe1\\r\\xbbW\\rX=\\xbe\\xcbx<f\\xffA\\xbd/\\x0b\\x86=I+\\xf5;\\xef\\xab\\x80<\\xef\\x7fU\\xbc\\xcax\\x7f<T\\x84\\x80\\xbc\\xe0GA<\\xe0\\xae\\x9f\\xbc\\x07\\x05\\xa8<\\xa4\\xd7\\xa3<*\\rz\\xbc\\xba\\x05\\xe9\\xbdr\\xf91\\xbck\\xd5\\xe5\\xbc\\xf6\\x96\\x1e\\xbd\\xb1\\xeb\\x15\\xbdc\\xfc\\x05\\xbc=\\x1d(\\xbc\\x96\\xfb\\x11\\xbc\\xde_E\\xbc\\x15\\xaff<\\xcd\\xb1\\xb0<\\xb0]S=w,%=\\x8b\\xb6\\x11=\\x14h\\xee<\\x8ev\\x12=\\xaf\\x82\\x1f\\xbcRcT=\\x1f\\x12\\xa9\\xbc\\x83\\xc9{\\xbbe\\x91\\x0e=\\xe8\\xd6\\x9e\\xbbJx$\\xbc\\x9f\\xfa\\xb4\\xbc\\x0f<\\xa2<\\x9c)\\x1b=pq\\xf9\\xbb\\xe37\\x87=\\x15\\x0e\\x1e\\xbdB\\xf0$\\xbd#\\xb6\\xfd<E\\x1d\\xea\\xbc\\xc8P\\xca;\\x14\\xa6\\x9b\\xbc]\\xd1&\\xbd\\xcb0\\xd5\\xbb\\xf9\\x01i\\xbd\\x9d\\xb8x\\xbb\\x05\\x84\\xa1\\xbd\\x96\\xb7\\x81\\xbd\\xbd\\xdcj=&\\x92\\x83<.(7=\\xb2\\xa9\\x93\\xbc2\\xda\\x93=9\\xba\\xa0=\\x1dvy\\xbcU1\\x10\\xbc\\x1eO\\x89\\xbc[\\xab\\xdb;b^;\\xbc\\x18\\xe5\\x07\\xbd\\x0c\\xe0G\\xbd\\x9a1\\xbd;\\xca \\x9d\\xbd\\x19Q\\xc9\\xbc\\x82G\\xb5\\xbc\\xf7\\xc3\\x1b\\xbd\\xfcpP\\xbd\\x8d@\\xaa\\xbb\\xb9s\\xce<\\xc5\\xb1\\xd1\\xbc,?y\\xbc\\x89\\xf93\\xbd\\xa6\\xc5\\xb9\\xbc\\xdb\\x9b\\x1a\\xbd\\xec\\xfd\\x82\\xbb$\\xc7\\x97<\\xbeI\\xcc<NP\\x07<\\x7f\\x866<0\\xf4\\xfe<\\xda\\xd6y\\xbd\\xcem\\x9d\\xbb \\xa8Z:,\\x0e\\x9f:{\\xab?=\\xe6\\xd7F\\xbc\\xcd\\x9e\\x7f\\xbc\\xd3\\x84};f\\x1bs\\xbc\\x93~\\xae\\xbcF@\\x96\\xbbp\\xeb\\x1b\\xbd\\xe0\\xa1\\xf1\\xbc\\x0f2\\x12=F\\xc5\\xd9<I\\xe2\\x1a=\\x82\\x0e\\xcd<`\\x9e\\x80\\xbc\\x8b\\xa8\\x8e</\\x99\\x87;H}\\x85=\\xa2\\x90\\xe7;V\\x158\\xbc<V\\xbe\\xbc]\\xfc,=\\xaa\\x82P\\xbc\\xb1\\xe4]\\xbdq\\x9bM\\xbc}\\xf0]=\\xcc\\xea\\xa3<@A\\x91;\\xfbVh\\xbd\\xbcLl= FU\\xbc\\xb0\\xa2\\x1a\\xbcH\\xa7\\x0b<\\xe9\\xf3\\xb8\\xbd\\xfb\\xf0\\xee\\xbc\\t\\xa9k;,\\xeb\\xf7\\xbc\\xae\\x9a\\x1e\\xbd\\xd7d\\x8c<\\xb9\\xb1t=|\\xfc\\x85\\xbd\\x93I`=pTW\\xbc\\xda\\xf8\\x1f<I\\xa33\\xbb\\xaf\\xb3\\xff\\xbc\\xd0.z\\xbc\\xd6\\xe4\\x95\\xbd\\xc1q\\x7f\\xbds\\x1e\\x8d=\\xbb\\xd8\\x0f=\\n\\xcf@=t\\x990\\xbdYG\\x91\\xbba\\\"\\x8a=\\xd1r\\x11\\xbd\\x0e\\x83I\\xbc\\x9c\\xcd\\x92=\\x1ai\\xc0\\xbc\\x9ag\\x8f\\xbcY(k\\xbd\\xc8)A<P\\xaa;\\xbc!~I\\xbd!\\x85%=\\t\\xa3\\xc1<\\x9aZ6=\\xf5\\x1a\\x95\\xbc\\x1a\\x1f\\xa7\\xbd`\\x8fG\\xbd\\x8c\\x90\\xdd;\\x8a\\xb31=\\xf3\\xad\\n\\xbc\\xe0\\x8d\\x87<\\x8f\\xa1\\xab;\\xf9o\\xa9\\xbb^\\xdaE\\xbc\\xdb\\xd4q<\\x0b\\x9f\\xc4<\\xbcv\\xde\\xbc\\xa0\\xdfF\\xba_\\xc8E\\xbc\\xa0\\\\t\\xbd>\\x9f\\xa5=\\xd1\\xfc\\x96<h\\xcc|<\\xbdWl;\\x16\\x02\\xbb\\xbb\\xf4\\xb6\\t=O\\xee@=Q\\x8a\\x14<\\xd6\\xf7\\xd0\\xbbl\\xf3V=0wc\\xbc\\x93\\xa4\\xac<2\\xd3\\xa2\\xbdp\\xe1\\x93;>=\\x80<\\xcd\\x03\\xba\\xbcW\\x99\\x11\\xbc\\x00\\xd1\\xd3\\xbc]M(\\xbbY?\\x12\\xbd\\xcb{\\x9a<\\xc4\\x9d \\xbd;V\\xaf9\\xe9\\xd3^<\\xa7\\xf2\\x81;\\xb7\\xad\\x85\\xbdJ3$\\xbch\\xe1\\x8c\\xbc\\xc8\\x1e\\xa4<X\\x07A\\xbc<\\x10.\\xbdc)\\x87=\\x17[\\xb6\\xbcL\\xe6\\x9f\\xbc%~\\x89=8\\x14\\xa7\\xbcwM5\\xbd\\xe0\\xf2w=\\xa7,\\x19\\xbd\\xef\\xfe\\xb4\\xbc\\x15\\x96@=S\\x8bx;RI\\r\\xbd\\xbf,\\xa1\\xbc_\\xca\\t<\\xfeR\\xd5\\xbcm\\x0e\\xaf<DV\\xef\\xbc}b\\x8c\\xbc\\xe6\\xf4\\xa4;e\\xc4]\\xbcN\\xe4\\xb2\\xbb\\xdd\\xe9]\\xbdtK`\\xbc\\x90$\\x8d\\xbc\\xc0\\x14\\x8a<\\xa6\\xdb\\xca<WLA<\\xc2/d\\xbc\\x16\\xea\\xf6\\xbc\\xca\\xf2\\x14=>\\xc7@<\\xf8\\x9f\\xdc\\xb9\\xff\\x97\\x85\\xbc\\xe8T\\xbf\\xbc :?<\\xe9\\xbbR\\xbd-\\xec;=\\xbc4$=\\x94\\xf39=\\x9b<\\x17<\\xc8\\x9a\\xac\\xbdty\\x85<~%0=M\\xa0\\x9f\\xbd*\\x0c\\x9b\\xbc\\xf0\\x19~=\\x98\\xda\\xfd\\xbc\\xa8\\x17}=\\xac[\\x19\\xbd\\x9d\\xec\\x9b<\\x97\\xb1Q\\xbd\\xbf\\x8e\\x82=[\\x8b\\xac<\\x02\\x12\\xa4;\\x80\\x15/\\xbd\\xed\\xdb\\x07<!\\x037\\xbd\\xa5\\xf5\\x86=e\\xcd\\xff<\\\\\\xbfl\\xbc[~\\xcb\\xbc\\xfd0\\x98\\xbb\\xdc\\x89\\x90\\xbd\\xe407\\xbc\\x9eg\\xb8\\xbc@\\x17.\\xbc/\\x9c\\xef\\xbcz\\xda\\x8f9xY1=T\\xa5\\xde;\\x02\\x0b[<\\xe98\\x9d\\xbd\\x0c\\xcc1<\\xb1r\\x13\\xbc\\xea\\x074=\\x85\\xe5\\x90=yQ\\xdb;yK\\x92\\xbc\\x83\\xf0\\xd4\\xb9LK\\x84:\\xf8Qk\\xbd\\x14\\xef)=\\xd1*\\xae\\xbb,\\xfbH\\xbd_\\x8a\\xeb=\\rd\\x0c\\xbdOo\\xee\\xbcV\\x08\\x13\\xbd>\\xdd\\x83\\xbc\\x02\\xe2\\x94=%,\\xa5\\xbc\\xfbA\\xa6\\xbc\\xe3\\xe1\\xe1\\xbc\\xd0q\\x85;\\xc8\\xbb\\xec=\\x9e\\xef\\x01<\\x91P\\x11=\\xa3\\x81\\x8a< \\x05/=H4\\xc8<\\xaf\\xc2\\x93=\\xf4\\x94S<\\xf2Tl\\xbc\\x81\\xf0c\\xbd\\x80>\\x81\\xbc\\x83\\xa6\\x9e;a\\x81O<e\\xd0\\x1a=t\\xa9(\\xbc@.\\x89=P\\x91\\x88\\xbb\\xec|f\\xbd2jj\\xbc\\xd1\\xe1\\xb1\\xbc\\x08\\xfb\\x87\\xbby\\x97\\xcb\\xbc\\xde\\xf8r<\\xce\\x80U\\xbd\\xfd\\n\\x8a=\\x05\\xde\\x9d\\xbd\\x7f?\\xcd<\\xb9\\x84\\xce\\xbc\\x9axn<\\xc8j*\\xbdj}\\xbb\\xbcv\\xfc\\xa2=\\x1aBg:\\x83\\xf0\\x8d\\xbc\\xda\\xb9\\xb7:\\xfe*\\x18=\\x00d\\x86<S\\x06\\x0c=H\\xa25\\xbd,\\x1e\\x87\\xbd\\xe7/\\r=\\x02X\\xbc:\\x8a\\x85\\x9c:.eU=\\xdf\\xfe\\x7f=\\\\\\xa7\\xa1<A%`=@\\xa2\\xb7\\xbc\\xdb\\xbc\\\"\\xbd=\\xc0\\xa4;+IB=\\xc4\\xa3&\\xbd\\x07/\\x08\\xbd\\xb6\\xd9J<zX\\x01\\xbc-\\xdf7\\xbd:\\xc8\\x96\\xbcF\\xbb\\x95\\xbd\\x17_\\xcd<YNz\\xbc!\\xd9Y\\xbbT\\xf1\\x16=+\\xc4\\x1a=\\xc0*\\x12=\\x94\\xce8\\xbd\\xffi\\x93=\\xd9\\x0b9\\xbd\\x99y\\xea\\xbb\\xab\\xd0I=\\xb8\\x7f\\xcc<\\xe2V\\x08=\\xff\\x86i\\xbd\\xf5\\x89\\x00\\xbd[\\x85\\xe6<\\x03%\\xa2\\xbc\\x14\\xf8\\xf3\\xbc!3\\\"\\xbcg\\xef\\xaa:\\xa9i:\\xbd\\xbai\\x1e\\xbdQ\\xc1x\\xbc\\xb66@<\\xb4]\\x05\\xbd\\x00\\xcdv<\\r/\\xcb\\xbc\\xec\\xb4\\xad=\\xa5\\x10\\x1c<G\\x148;\\xbcR0\\xbd\\x0bf\\x96=\\x8ey~=\\xc09\\xb5;\\xf0\\x8b\\xec;\\xa2\\x18\\xe3\\xbb\\\\d\\xa7\\xbc\\xf3\\xcbu\\xbc_4\\x12=\\xddf[\\xbd\\xaa\\x11\\xba<\\xcaH\\xc5\\xbcj\\xd2\\x9e\\xbc\\x16gB=@B\\x1b=k\\x9b\\x00\\xbcXC\\xf3<\\xcf\\x96\\x80;y\\xfe)\\xbb\\xb9\\x00\\x85\\xbcD\\xf8q=\\xd3\\xa9h<\\x82\\x98\\xc3\\xbb\\xad\\x12W<B&@\\xbd\\x91\\xcf\\x92:%\\x10\\x14\\xbc\\xb4o\\xf7\\xbdt\\xc4!<\\x0e\\xb9\\x94<\\xce\\xccO<3\\x96\\n\\xbd\\x8f\\xe9&;n\\xdc\\x95<\\xfbs^\\xbc\\x8a\\x04\\xc9\\xbc\\xb3\\xbe\\\"\\xbd>\\xb9K=5\\x9d\\xbc:\\xdf\\xf3\\xac\\xbd\\xd6\\x8e*\\xbd\\x8c\\x0e\\x9b\\xbc\\xcb\\xd8\\x1f\\xbdN\\x83>\\xbd\\xd9.r\\xbc\\x0f^\\xfe<\\xd4>?=\\x0cv\\xb5<\\x96\\xde\\xc7\\xbc\\xf9:!=\\r0\\x12\\xbd\\xba\\xfa\\xca\\xbc\\x1f\\xe8(=\\x94\\xfa#\\xbd+\\x978<H\\xa5\\x99<\\xc4\\xa9\\x03<\\xd6\\xda>\\xbc\\x1aO)\\xbd\\x10.\\xbf<\\xca]\\x19\\xbd\\x8ci-\\xbdY~\\x1e\\xbd\\xd7S\\x8a\\xbda\\xc2\\xf8;\\xae;m\\xbd\\xce\\xc8 \\xbb\\x7f|\\xa4<\\x98\\xfb\\xb3\\xbc\\x9bz\\x08=\\xf4\\xfcV=XO\\xb7<GI\\x82=\\x83\\xe4\\xe1\\xbc\\xcdWL\\xbcJ\\x80\\x82<\\x8c_\\x0e\\xbd\\x14\\r\\xd3\\xbcn]-=ic\\xec<\\x91\\xb4\\x0b\\xbd\\xcc\\xf0\\xab;\\xd3_\\x0f\\xbce\\xac~<\\x1c\\xd7\\xf2<)\\xcaa={>\\xe3\\xbc\\xb5\\x83\\xa6\\xba\\x95\\xa7\\x1b\\xbal\\x8bV\\xbc\\x8b8F\\xbc\\xafOu\\xbc\\xc8F\\xb5\\xbb5o\\x9c<!\\xda\\x0f\\xbd\\xd6\\xb9V\\xbdK\\x98\\xe8\\xbb\\xe6T\\xed;4\\xf5&=\\x1e\\xec(=\\x1b\\xa0s\\xbd\\xaa\\xdb\\x1d=(\\xe2J=a\\xd6R\\xbd\\xf3,\\x97\\t\\x92\\xb9\\xdb;\\xc41\\xc1\\xba\\xb00\\x01=n\\x83&<[w\\xb2:\\r\\xd3M<\\xa5\\xf8\\xf0\\xbbJ[\\xde<\\xd1\\x86s=\\x95.\\x8a\\xbb\\xec\\xc4\\xd2\\xbchkl=\\xb3\\x8b\\xeb;\\x1cK`=/\\xe0\\xb1\\xbc-\\x17\\xbd<\\xfb\\xbbF\\xbd\\xd8Y\\xe3\\xbc5\\xf1\\xe2\\xbb\\xd4\\xf4x=\\x1a\\x86*=\\xca\\x14\\xd6<\\xe2\\x05\\xba\\xbb\\xa4\\xfd \\xbc\\xea\\x10\\x98<e8\\\"\\xbc\\xde\\x9e\\t<I\\x1c\\xfc<\\xaf\\xaf\\x10\\xbd\\xdb\\x07\\xee9\\xcc\\r\\xa6<\\xb9\\x14Z<\\x8e\\xaa\\x85\\xbd]n\\x99\\xbd\\xa6\\xbc\\x0c=\\x9b3\\x87\\xbc\\xac\\xc3\\xd4\\xbc\\x83\\xc5m\\xbc\\x1d\\x94\\xbb<\\xfcX5\\xbc(wC=\\xdfY\\xf0<\\xb7N\\xf4<\\xbd\\x07\\x9f<\\xe1\\x0f:\\xbc\\xefJ/\\xbd<%\\xcd=\\x9f\\xfb\\xc9\\xba\\xee\\xf9\\xf0\\xbc\\xfdM\\x1f\\xbd\\x94P\\xb1\\xbcG\\x01\\x97\\xbd\\xb9\\xa52\\xba\\x98\\xdd\\xc8;\\x95\\xf9\\xc8\\xbco@W=6\\xaf\\x16=\\xc6\\xca\\x8d<o\\x04\\x0e=\\x04l\\x96\\xbdI\\xac\\xb9<=\\xe1\\xb4\\xbcE\\x0c=\\xbb5c\\x82<\\xf4`\\x95\\xbc\\x03 \\x0b\\xbd\\x1dP\\xb6\\xbc\\xf8\\xfb\\x8d;\\n\\xb8-<\\xe3P\\t:\\xda}\\x05;\\xe4\\xa5X9\\xde\\xe4R\\xbc\\x0en$=|\\xc5\\x81\\xbc\\x83L\\xa8\\xbcJ\\xfe\\xe7\\xbc\\xc4\\xc4\\xd6=\\xa8s*\\xbdA9\\xa0<\\xd1-/\\xbd\\xc5\\x13i<\\xdb\\x17E\\xbds\\\"0=w\\xf5\\xec\\xbc\\xa6\\xaaf\\xbdmW\\x95=^\\xd94\\xbb\\xbd\\xda,\\xbd\\xde\\\\a<\\xbe>\\xa2<\\xcc8\\xf4<\\xc6l\\x95=\\xd4\\xf1.;d\\x86\\\\<?\\xfb\\x00=\\x03\\x06%\\xbdC\\x11\\x93<\\xd9\\xc0\\xe1;\\xe3X\\x06\\xbb\\xba\\xd3\\xbe\\xbc~\\r%\\xbc\\xc5\\xae\\x8b<R?\\xad\\xbc\\xc7\\xb1\\xfc<\\xda\\x19\\x9e\\xbc\\xd9\\xf3\\xd5\\xba%\\x015\\xb9\\xf1\\xc6Y\\xbd\\x1azp\\xbd\\x10\\xcf\\x00;\\xd0}k\\xbc\\x82?`\\xbd{N\\x7f=\\xcc]\\xd5<\\xa6a|<\\xee\\x1f\\xec;zM\\\"\\xbds\\n\\xb7\\xbc\\xc4\\xe8\\xb5\\xbd\\x05B\\\"<\\xc3\\x02J\\xb9\\x05;\\xe5;\\xadD.\\xbcK\\xc1\\xe9<o\\x01\\x07=\\x03\\xef\\xcd;\\xe2\\xb8g=\\xa6\\x89\\xce<>c \\xbd\\xd0;\\x0f\\xbc\\n\\xc9\\x9b\\xbd\\x97\\xeeb=\\x0b\\xdf\\xaf<\\x084k=\\x9e\\xc3g;<\\xa3\\x90;EZ\\x9b\\xbd\\xda\\xfc\\x1c<\\xf2c\\xb1<\\xa4\\x92\\x17\\xbdT\\x8d\\x14\\xbd\\xc8:\\xfe\\xbc\\x8b\\xbc]\\xbb\\x07\\x06\\xb1\\xbc\\x9d_\\x93\\xbc\\x18]\\xe7\\xbcq\\xa9\\xf0;1#\\x85=\\xd6\\x87\\x8f\\xbcZ\\x8fA\\xbd\\xb3%\\xc1\\xbb\\x93d\\xe2\\xbcs\\x8f\\x16=\\xd7H#=C\\xc1\\x9e\\xba\\x1e\\x9f\\x86\\xbb\\xfb\\xac\\x18=\\xcf4\\x1e=\\x15\\xcc&=\\xe3\\xb6\\xf5\\xbaJ\\xad{=\\x0809=\\x935h\\xba\\xfa\\x16\\xdf\\xbcR\\xed\\xab;,\\xb3\\x08\\xbd\\x8c\\x19\\x989U\\xbec=\\xe2\\xea\\xec\\xbb\\xb9\\xear<\\xb7\\xcb\\x1d\\xbc\\x18-M=\\x1b\\xd3\\x1b\\xbc\\x84\\x02P\\xbc\\xe6\\\\Q=\\x1dIc=\\x14#\\xbd<\\xcd\\xa0\\xc0;\\xbd\\x1c\\xcb\\xbc\\xd1=\\x91\\xbd@\\\"\\x17\\xbd\\x1d\\x81%=\\xfa\\xdd\\xe0\\xbc\\r[p\\xbc\\xa2F8\\xbd\\xfat1\\xbdz\\xe1\\xa3\\xbd\\xc33\\x8a<#\\x06\\xa8\\xbb\\x05\\xf7:=\\xc9\\x10\\xc6;H\\x90Z\\xbd\\xa5\\xd04\\xbb\\xd1\\x96\\x87\\xbd\\x9dOx\\xbd\\x97N&<\\xcb\\xba\\x9d\\xbc\\x08\\xa0\\r\\xbd\\x02Qn\\xbcy_\\x1f\\xbc{\\x15>\\xbd\\x13\\x93\\x9e\\xbce7\\x0c\\xbd:Q\\x8e\\xbc/6\\t\\xbd0`\\x8c\\xbb\\xbcRB<\\x95{\\x87\\xba\\xb1~l\\xbc\\x05\\xa9\\x1c<\\xe2\\xbe\\x968z\\x16\\xab<T{\\xc8\\xbc\\x1eu0\\xbb\\x9e4\\xb1:s`\\xc2<f#\\x16\\xbd\\x9ckA:\\xf2\\xa3\\x0b\\xbdd3%=5\\t\\x82\\xbd\\xa3\\x83\\xe3<\\xe8\\xf5\\xce=*\\xbam=\\xdf\\x00\\xc1\\xba\\x01\\x12~=\\x17\\xeb;\\xbd\\x14\\xe1G<v\\x00\\x15=\\xc9\\x13\\xca\\xbb\\\"\\xf8\\xb3\\xbb\\xf7\\xd3j<Bx\\x11<\\x84\\xef\\xf69\\x1cM>;`\\xef4=\\xc6\\xf1\\xc8<\\x02\\x03\\xff<a\\x0b\\x1a=\\xcdK9=\\x88\\xe4\\x02=\\x8a\\x12\\xda<\\x93^.\\xbd\\x85\\x01\\xc4\\xbc\\x8e\\x15\\xcd\\xbc\\xce|\\xb8;\\xd4\\x1a\\x03<&U\\x87\\xbc\\xfa\\xb6\\x04\\xbd\\xb4H\\x1e=5\\xe6\\x81\\xbcwR\\xd9\\xbc\\xfe\\x96\\x0c=\\xc1.\\x0c\\xbd\\xf6\\x16\\x07=\\xa7}\\xeb\\xbb\\xef\\xb7)\\xbc\\x99%V\\xbd1IJ\\xbcPf\\xea\\xbc7\\xb4\\x9e\\xbc\\xc1\\xd5\\xc9</[\\x1f\\xba \\xd7\\x98=\\x01\\x95J;\\xc0s\\xab<\\xea\\xff\\xd6=\\xc3\\x03\\xb7\\xbc!\\x93V<}\\xb1\\x85<\\xcf\\x91%=\\x10PQ\\xbd\"\nHSET bikes:10067  model 'Iapetus' brand 'Bold bicycles' price 4489 type 'Mountain bikes' material 'carbon' weight 13.8 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\xf2\\x0c1\\xbcj\\x19\\xcb\\xbc\\x9c\\xfa\\xd0\\xbcN\\x842<\\xfc\\xb6\\x9b\\xbc\\xee~3=\\x141\\x04\\xbdDJ\\x14\\xbb\\xa7\\x1d\\xa9=\\xafSh<]\\x1b\\xd8<rD\\xe9<9\\xcbU=A\\x08\\xec<\\\\p\\x87=\\x1f(\\x92\\xbd\\xcc\\xe7v=\\xc4\\x86i\\xbd\\xf5j \\xbc\\xd3\\xdab\\xbd\\x12\\x0e\\x08<VQ\\x82\\xbce\\xcfA=%a\\x99\\xbd\\x1a\\xd4m\\xbdELL\\xbc\\xdf\\x8b\\x06<\\x00~\\x95\\xbc\\x1c\\x97!\\xbdor\\x87=\\x80\\xdc\\xbb:q\\xc3`\\xbd\\x14\\x14s<\\xea\\x83+;1\\xa2\\xb0\\xbc\\xe7~\\x95\\xbc\\xb1\\xb0\\xac<\\xf7qD\\xbc\\xd6`\\x87\\xbcx\\xb5\\x99<^\\x0e+=c\\xaa\\xc3<?\\x97\\x08=\\x87D\\x89<S\\x9a)\\xbc\\x0bZ\\xf8\\xbb9\\xf0\\x1f=K\\x10\\xa9<\\xf6\\xc1\\xee\\xbc\\x84\\xf9\\xa8\\xbcOq3:\\xecR(\\xbd\\x8e\\xa1\\xe6\\xbc\\xb9\\x7f\\x1b;\\xd5G9<\\xdd\\xcc\\xa2;_\\xba\\x80\\xbc\\xcco\\xa3\\xbdA\\x8fq\\xbd\\x97\\xdc\\xd0=\\x94N\\xa2<\\xed\\x97\\x8d=\\xe8P\\n\\xbb\\xe1\\x83M=z\\xcc!=\\xe3\\x15,<\\xda\\x97\\x0e=e\\x97\\x07\\xbb\\xa0\\xb6\\x8a<.sc\\xbd_\\xb1\\x02\\xbd\\xa9\\x9e\\t<92\\xf0\\xbc\\xc8tf\\xbd\\x907\\r\\xbd8\\xda\\x94\\xbc\\x08\\xc6\\xb7:\\xb1#\\xd7\\xbc\\\\\\xf9;\\xbd\\x1bT\\\"=W\\xe8\\xaf;\\xbcg\\xa8\\xbd\\xf3{\\x96\\xbc\\xb0i*\\xbdh\\x951\\xbcM\\xdf\\xf2\\xbc\\x85\\x95\\xab\\xbb\\x03\\x91\\x9e<\\x04OR=\\xddh\\x0b<\\xcan\\xa3\\xbaB\\xdc\\r<u \\x07\\xbc\\xf2}\\x82\\xba\\xaf\\xf7\\xb4<\\xff1\\x91<\\x1aN\\\\\\xbdk\\r#\\xbb$\\xc04=^\\x07\\x8a\\xbd\\x83\\xe6\\x07\\xbd<\\x0b\\x98<\\x13\\xeeI<4\\xaen\\xbd\\xc9\\xe7p;Y\\xea\\xfa=\\xbcU\\xc5\\xbb!\\xdbk<\\xe9\\\"\\x8b\\xbc\\x96\\xe0@<3\\t\\xdb;\\xf7|K=07\\xb0<m\\xbb\\x83\\xbc\\xf2\\x88\\x8a\\xbd\\x8d\\xed\\x12<\\xdb\\xd0\\xa1<m\\xebr;\\xd6%c=\\x90\\xe3\\x12<VC\\xee<\\xef\\x11d<\\x995\\x95\\xbdk\\xac\\xa8:\\xa5\\xf1\\xa9<\\xa5\\xa6S;2\\xdb\\x00=b\\xc48\\xbd\\xb8\\x01!\\xbd\\xc1\\xbd@\\xbd&W\\x05\\xbd\\xa7f\\xf4\\xbc\\xbb,\\xe4<\\x8c:1\\xbc\\x1a\\x04\\xc4\\xbc\\x87\\x03\\x1c=b\\x8a\\x87\\xbc\\xd8\\x89*\\xbc/q\\x0c=W\\xdd\\x12\\xbd\\xfc-\\xc2\\xbc9\\xfcp\\xbd\\xc1-\\xfc\\xbah\\xd7\\x82=[i\\xd9\\xbc\\xdaNQ<Q\\x842\\xbc\\x8a\\x84`\\xbc\\x83\\x17\\x95\\xbc;6\\xb3;s4\\x11\\xbd#\\x06\\x8e<\\xd1\\xd2\\xe1\\xbcO{K;B\\xf4\\x87\\xbckS\\xf9\\xbc\\xbc<\\x91\\xbb\\x85\\xf7t<vW!=\\xe1\\xb2P\\xbd\\x02\\xe6)=h\\xea+<]9\\n\\xbc\\x95\\xa8C\\xbd\\xbc,\\xbe<\\xf7\\x92\\x0e=\\xfe+\\xcd;3)\\xa6<\\r\\xe2L=\\x1c\\xdc\\xfb\\xbb\\x1e:\\x97\\xbcP\\x8f\\xcd<\\xe4\\xba`\\xbcx\\x01\\x16\\xbd\\n}>\\xbd\\xf0\\xcb\\xe2<o\\xd8,\\xbd\\xe3c\\x99=x={\\xbc\\x88\\xf5\\xe9\\xbcv\\x1c\\x8e;7\\xab\\xfa9\\xdd\\x141<\\t\\xd1\\xa9<\\x98\\xab<=\\xa5Z\\x10\\xbd\\xfbd\\x9e<\\x838\\xa8<>\\r =Dn)\\xbd\\x17qR<\\xf8\\xb1V=\\xd3\\xbd\\x92\\xbd\\x8a\\xbf\\xfc<\\xcc\\xa2\\x1d\\xbc\\x1b\\x8d\\x0e=57\\x83\\xbc\\xb4\\xe2\\xb6\\xbc\\xcf\\x07\\xe7<[I\\xc6<\\x1d\\xcb\\x04\\xbdQ\\x9a\\x82\\xbd\\x06(\\xa1\\xbd\\x92?*\\xbd\\xc86\\xc5\\xbc\\x12}\\x9a<\\xf3\\xeb5\\xbd\\\"\\x83\\x87\\xbc\\xfc\\x85(=\\xc9\\r(\\xbd\\xe6\\x17\\xf2\\xbb\\x8d\\xcfj<\\x99\\xef\\x07=\\xbf\\x03\\xce<p\\x13%\\xbc\\x05\\xfa\\x04;\\x0f\\xe6\\xc8\\xbd\\xcf\\xdd\\xe8\\xbc\\xdcI\\xdf<hXm\\xbd\\x84\\xcb9<i\\x91\\xc6\\xbc\\x08\\xd2+\\xbd\\x98\\x07u<I\\x13?\\xbcl\\xe6f\\xbc\\x9a}-<\\xe9\\x1f\\x81;\\xa1Q\\xef<a|\\xa8\\xbcH\\xf1\\x08\\xbd~G\\x0f\\xbch\\xa3a=\\x1ci/<\\xbc\\xcbD<\\x0e>\\x02\\xbd\\xb7\\xc8j;\\xb6O#=\\xc8\\xd6h\\xbcw\\xa0\\x9b\\xbc\\xe3\\xe4\\x18=i\\x04\\x8f<F\\x90\\x85=IL\\x0c=\\x98A\\xa0<\\nb\\x0f=9kt=/\\xbf\\x10<\\x9eO)\\xbdp\\xef\\xd9<\\x00\\xdcU<\\xfff\\xd3\\xbc\\x94i.=\\xe9\\x9d\\x16=\\xe1jz;\\x91=m=H\\x1a\\xb3\\xbc\\xba\\xcd\\x02=\\x8c\\x87\\x89;\\xac\\xde\\x08=R\\xbf\\xa5\\xbc`G\\x96<\\xacX>\\xbd\\xf9\\xfc\\x0f\\xbb\\x08\\x8aI\\xbd\\xca\\x86\\x81=p\\nY<\\x98\\xf9\\xf5:\\xbfvR\\xbbq\\xfe\\xbe;\\xec\\x98V;\\xfb\\xef\\x8a\\xbb\\x1a\\xb18\\xbd\\xe8\\xabW<\\xfad\\xb6\\xbb\\xb1\\xbc\\xd6\\xbcq\\x97\\xba< \\x8f4\\xbc\\xf1\\x084=\\xae\\x8f\\x9e\\xbd\\x886\\xe8\\xbb\\xf3\\xaet<\\x05\\xf7\\xae<Yi\\x17=\\x07c\\x0f<?-\\xcc\\xbc\\xbb2\\x0b\\xbd\\x9a\\xb6\\xb7=\\x05\\x81g\\xbd,\\xbbn<2\\x0f\\xf7;\\xd2.\\xad\\xbb\\xce\\xc9\\xa0=W\\xcd\\xdd;\\x1f\\x86\\x0f\\xbcA\\x95\\x7f\\xbby.\\xc1<\\xa6DD<\\x8f\\xd5\\xe1;\\x07\\x99\\x8c\\xbb\\xc3o\\xf2<,O\\\"\\xbc\\x8aD\\x1e=\\x94\\xc4=\\xbb\\xadBJ=z\\xde\\x83;\\x88`\\x81;\\x16o5<s1d=ef\\x91<\\xaf\\xf8\\x1f\\xbd\\xbe4:\\xbd\\x08m#\\xbd{\\x02\\x15\\xbd\\xb9\\xc6\\x83<\\x93UG\\xbd\\n\\xdb\\xb5\\xbc\\x92p\\x80=\\x10\\x18\\xe3<\\xe5\\xa3\\xaa\\xbc\\x8c\\xe8\\xb5\\xbcn\\x8b\\x97\\xbc\\x90\\x82\\x05=\\xc6q]\\xba\\xd3\\xc7\\xfc<\\x07\\x94Z\\xbd\\xd9\\xf5\\xa6<-\\xc9\\x90\\xbd/\\xab\\x18=D\\xfa\\xa5<\\xfcV\\x95;\\x8f\\xc6\\x81\\xbd\\xb5\\xad\\xaa\\xbc?\\x85\\xad;\\\"\\xd5\\x81<-\\x9d\\x15\\xbd\\x1am\\\\\\xbd8#\\x04=/\\xaaH=+\\x8b\\xb2;\\x7fa\\x1e\\xbd\\xc18\\xd6\\xbcz\\x83\\xa5=\\xa5ES\\xbb\\xbb\\xac\\x05<\\xef\\xd0\\x13=\\x9d;\\x9d=J\\x85\\xab<\\xd3\\xfe\\x9d=w\\xf9\\xc6<\\xa6\\x9fS=\\x9c\\xc1\\xc2\\xbc\\x91\\x9d\\xa9=yg\\x12\\xbcG\\xf7\\xad\\xbd7\\x16Y\\xbc\\xedq\\xc0\\xbc\\xa7\\xb0\\x9f\\xbc%\\xea~\\xbc\\x1a\\x88\\xf2\\xbdAm\\xb5\\xbb\\xc8\\x19w<\\xab\\x06I=\\xd9\\x9a\\x13=\\xfc\\xeb\\xb4<\\xb4z\\xb9;\\xf0\\xd35\\xbd`\\xcb\\x89\\xbbrwO\\xbd\\xe4\\xc3\\xd3\\xbc\\x8b\\xbf!=\\xf9\\x90\\xf6;N\\x05\\xf6;\\xe7\\xe2\\xa1;\\xa9t\\xb1<\\xf6j\\x91<\\xd5\\x13\\xf6\\xbb!Dl\\xbd\\xd0HH<\\x1dg\\x17\\xbd\\xef\\xa5!\\xbd_\\xcd\\xdc\\xbbA9\\x03\\xbd?R\\x01\\xbc\\xf8\\x8f\\xb7\\xbd?\\x8ca=\\x00\\xfe\\xc9\\xbc\\x16\\x08\\xa6<:\\xb2=<\\xa9+\\xee;\\x15\\x9ay\\xbd\\xc2\\x82\\xbc=[\\x0b\\xe0=\\x90F1\\xbd\\xc7\\xe0\\xb6<]!\\x1d;\\x06 \\xd7\\xbc\\xa2\\x14\\x92\\xbb\\xf8\\x03\\xb9=\\x0cK\\xa5\\xbd\\xae\\xe7K\\xbb\\x10\\x01R\\xbdv\\xbb\\\"=\\x11\\x9f\\x0c;\\x7f \\x85=\\x1fS&\\xbc\\x1d\\xa4\\xc7<xYN\\xbc;W\\xe9;\\xe0\\x8f+=\\xb0\\x13>=5\\x89\\x02<K^\\x8f<\\xf5dI<\\x14O\\x8c<b\\x07\\xf5;@\\xfe\\xbe<-\\xc9\\x9d\\xbd\\x9c)V\\xbb\\x14&z\\xbb\\xa2\\x85\\xc2\\xbc\\x80|\\xf7\\xbc \\xed\\xd4<tRz=\\x05\\xf8\\xc0<6\\x0e\\x1d\\xbdSpc\\xbc\\xe4\\xe1.=*\\xfdE<L\\xe12\\xbd\\x99(\\x1d<b\\xc6 \\xbb\\xb1e\\x97\\xbd,zZ\\xbd\\x9e\\xad\\x1a\\xbdl\\xb8\\x8c=x\\x82T=\\xfc!\\xa1;\\xe31\\xad\\xbc\\x15\\xb2\\x82<\\x89DE\\xbdc\\xce\\x80\\xbc,\\x84\\xea;\\xf1\\xcf\\x14\\xbd\\x18_@<\\xa6\\x9c0<\\xdf\\xf59</5\\xca;\\xbfb\\x88\\xbd[\\x99L=+~\\x82\\xbd\\xd7\\x92\\x8d\\xbc?\\x0c\\x93\\xbcFp\\xa5\\xbd\\xfc\\x85\\xa6<\\x1dJ!\\xbb\\xc1}h<3R\\x16<\\xb82|\\xbc`\\xde\\xad<\\x05p,=\\x19_\\x99<)\\xe4\\xce\\xbci\\x12K\\xbd\\x07\\xcf\\x81;\\xef\\xea\\xcb<\\xe4\\x14\\x8b;45O\\xbd\\xce\\xb7\\xcd\\xbb\\x11\\x86a=`\\xab\\t\\xbd\\xf6v\\x01=\\xf9d\\x9a\\xbc\\x15U\\x98\\xb9\\xec.k<\\x88\\x19\\x9c\\xbb\\xd0@e\\xbd\\xa2l\\xd7\\xbb\\x01&-\\xbb\\xb4+\\x8e;\\xe5\\xf4\\xa1;\\xeb\\r\\xfb<\\xa3\\x8aY\\xbcO\\xd27<\\xc3\\xef\\xbc\\xbc\\xd3\\xc79\\xbd9\\x8c\\x8e<(w\\x04=\\xdf\\xd0\\x80\\xbb\\x14<#\\xbbjd\\xa3\\xbd\\xc3\\xc0-<\\xcb\\x05\\x9f<\\x81\\xcc&\\xbbG\\x0cz\\t\\x82\\xb8\\x9e\\xbb\\t\\xeaj\\xbc\\x98\\x1a\\xfb<W;\\x10=\\x16T\\\"=\\xac\\xdf1=\\x06\\x88&<\\xbd\\x8d\\x80\\xbcUp\\x8c<Y\\xf3\\xdc\\xbc\\xeb\\xf3\\x8a<[\\xbf\\xc7<\\xf8\\xecE:l\\x01>=\\t\\xa7\\x08\\xbc\\\"\\x97R\\xbb\\\"\\x9b\\x0e\\xbdY\\xf2\\x93\\xbd\\xc2\\xba\\x8b:\\x81\\xe7\\x8a=~6\\xb3:9z\\x0c=$b\\x15;\\xb0\\xf7\\xe4\\xbc\\x0f:.=%r\\xbe\\xbb\\x08\\x0e\\x06=\\x98\\xc6\\x95<\\x89\\x87\\xe9\\xbc\\xcc\\xbd\\x14<\\xd79\\xb1<eT\\xa1<Y\\r\\x96\\xbd\\xb7\\x8bS\\xbdl@\\x8a\\xbda\\x03\\xcc;y&\\\"=\\x95L\\xdf<\\xf6\\xd1+<5\\x06\\x84\\xbc/I\\xd5\\xbcp\\x9a\\x0f\\xbdQ\\x9e\\x85\\xbcS,\\xfa<vt\\xab;\\xba\\xbbv\\xbc)d\\x91=\\x9f\\xc3G\\xbdDI8\\xbd\\x8b\\xdc\\x00=\\x9b\\t\\\"\\xbdO\\xd8\\t\\xbdj\\xb5\\xd3\\xba]\\xeaY=\\xba\\x95\\xca\\xbb\\\\m\\xc4\\xb8JF\\x0e\\xbd\\x95\\x1a\\x84;\\x988\\x0c=\\x92\\xae|<\\xfd\\xc0\\xb6<\\xeb\\xa3\\x05\\xbdE4\\x83<\\xda\\x04?=\\xb9X\\x00\\xbc\\xd0\\x80D\\xbd\\x10\\x8b\\xcb<g\\xa4\\xa0\\xbc\\xf4\\xf4G<\\x1c\\xca\\xcf<\\x04\\xb2\\xaf:d\\xdc\\x89\\xbc\\xd9.\\xd4\\xbc]6\\x03=\\x9c\\x16\\xde<\\xe2ZI\\xbd)S@\\xbd\\xac\\x9aQ=_\\x01\\x15<\\xae\\x1e\\x85=YQy\\xbd\\xf7\\xd3\\xc79\\xa5\\xd5\\xa4\\xbc\\x19\\xd4\\x82<\\xeac/\\xbd\\x03n\\xf5\\xbc\\x06\\xd8c=2=\\x0f=\\x1a\\r\\x8b;\\xf3\\t\\xa0<\\x11\\x11b8Q\\x1fQ\\xbdO\\xe3\\x96=P\\x7f\\xb9:o\\x16\\xef<\\xd6\\x8e(=T+\\xdb\\xbb\\x9fM2\\xbd\\xac\\xdf\\xb9\\xbb\\xc3\\xd4i;\\x11#~\\xbb\\xbag\\r\\xbc\\x9f\\xc4O<\\xb6\\x0c\\xa2;\\x8bQ\\xd3<U\\x9e\\x12\\xbd\\xb2d\\xc1<\\x8a~\\xa7\\xba\\x11\\xe5m;\\xc7\\xa2\\x16\\xbd+\\x1b\\x90=3\\x9f9\\xbd;8\\x19<(\\xb35<\\xb1M\\xd8=\\xde\\xbf\\x0e<U{\\x16<~T\\xe9\\xbcEz\\x16=\\xe6\\x1f\\x0f\\xbda\\xc6d<\\x88\\x08\\xd8;Ro\\xfb<p\\x9f\\x11=\\xe3O\\t=\\xda\\x02\\xaf\\xbb1m:\\xbd\\xb2\\xf1\\x9b<w\\xd0h=\\x07\\xc6\\x00\\xbd\\x87\\xfb\\xd0\\xbc\\xf9X\\x94\\xbd\\x17~\\xdc<\\xdc\\x06E\\xba\\x18_\\x9e=l\\x90\\xa2<&\\xc8[=\\xe3\\xc7\\x8e\\xbd\\x9e\\xd6\\xfb\\xb9J\\xf2&<\\x1doY=\\xcc\\x83,\\xbd<\\x17\\xef\\xbcM\\xc8!:\\x0ez\\xcb<\\x06B\\xf7:\\xbd\\xc1!=\\x98\\xb8\\xb4\\xbc\\xe5\\xe6\\x08=\\x02w<\\xbcb\\xfe\\xf2\\xbc\\x96]\\xaa<\\xba\\x99\\x90\\xbc\\x18\\x92\\xf8<\\xf4\\x96\\x9e<\\r\\x8d/=\\xdc$\\x99\\xbd\\xe4\\x13\\x99\\xbb\\xe0\\xb9;\\xba\\x8a\\x7f\\x7f;)(\\x02=\\xc5\\x83E=\\xf4Q\\x07;\\x1dN\\xf4\\xb9\\xcdUr\\xbc\\xb2\\xe9v\\xbbt\\x93\\x94\\xbc+&\\xb2<\\xad\\xac\\x1d=\\x8bL\\x03\\xbdX\\xc1\\x06<\\xcb\\x86U\\xbc\\xb5`\\x14<\\x02\\xc5i;\\xffm\\x8d<\\x06\\xb3O=\\xb3\\x1f\\x94<\\x927\\x14=$\\x1e\\x9a<iL\\xc3\\xbcc\\xcd\\xa9\\xbc\\xa2&\\x82\\xbd\\xb3\\t\\\"=\\x1epI\\xbc\\x1f\\xc6\\x1b\\xbd\\xd1\\x8f\\r;t\\x17\\x98\\xbc(\\xbb\\x0b\\xbdh\\\\\\x07=\\xcd\\x98f:\\xa7\\xc0\\xd6\\xbcnYD=\\xf5\\xc1\\x13\\xbd\\\"o\\xa6\\xbc4\\x00\\x89\\xbd\\xec\\x7f\\xa3\\xbd\\xd1\\x98\\x97\\xbc\\xb5:\\xc6\\xbc\\xa4\\xfe;\\xbcO\\x01,<\\xd6\\x1d>\\xbdE\\xfdC\\xbcqZ\\xc8\\xbc\\xa0\\x90\\x81\\xbd\\xcfBJ\\xbc\\x18\\xa4\\x99\\xbc\\xaa)\\xd5;\\xbe\\xc1;\\xbd\\x981i<V\\x08\\x8f\\xbdZ\\xaa\\x06<\\xd7\\x9d0\\xbb\\x88\\x87\\x9d;,\\xfa\\x80<{\\xe4\\xc7\\xbc\\xc3Wk\\xbc2j6<\\xe0]`\\xbb\\x1dN\\xb1\\xbdZ\\x98\\xb0\\xbd\\xdb\\xa6\\x13\\xbd\\x9f\\xa7i\\xbc\\xc9\\xc7== \\xeb?<;\\xf7\\xa7=&\\x1c\\xa5\\xbcOj\\x0c=\\x06j\\x83\\xbd\\xda\\xf0\\x9e\\xbd\\x05\\xaf\\x0f=\\x8e8\\x04\\xbb7$\\x98\\xbc/\\xae\\x00\\xbd\\t\\x10\\x00\\xbd\\x17\\x1f\\xa2\\xbc\\xb19\\x80\\xbdmh\\xfa\\xbc\\xe4J\\x0b=o\\xdf]=\\xa6\\xe7\\x86\\xbc\\x10\\xe4\\xf4\\xbb\\xcc\\x8aK=\\x1a\\xa4\\x9a\\xbb\\xbf\\\"\\xfa<0\\xab\\x0e:@h\\x97;\\xf5\\xe7t\\xbd\\xa6\\x8f\\x87;\\x06p\\x97\\xbd\\xcc\\xbc?;9\\xbd\\xf4\\xbb\\x18\\xfe\\x04\\xbc\\xe5\\x0bC;\\x8f+,=\\x8bnV\\xbd\\xfa\\xfa\\xa7\\xbb\\x05\\t<\\xbdP\\xa7\\xa9<\\xe7>\\xbe\\xbcY\\x9e\\xb1\\xbd<1\\x06=\\xbe\\x82\\x8a<\\x9d\\x81\\x1c<\\x15.7=\\xec\\xe1\\xc0=\\xed\\xf6!\\xbcx\\xd0T=3\\xf9:=d\\x91\\xc59ADJ=i\\x7f\\x8e\\xbd\\xb9\\xb1\\x99<\\xbe\\xe4\\x98\\xbc\"\nHSET bikes:10068  model 'Pluto' brand 'Tots' price 3877 type 'eBikes' material 'alloy' weight 9.2 description 'A city eBike that could double as a short-haul commuter. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"n\\x8b<;\\x8bS\\xd3<\\x1b\\xc9\\xa7\\xbc\\xaa\\xe9L=\\xbf\\x08\\xf1<\\xef\\xed\\n=\\xe2\\x17\\x02;g\\xcd\\x9c\\xbd]\\xe1W=a\\xa3\\xb5<\\x18\\xd7\\xa0<\\xabW\\xd3<\\xee]I=\\xfa\\xe0\\x01;\\xf2\\x80\\x91=\\xc7P\\xb2<\\x17M\\xff\\xbc\\\\<!=\\xa2\\x82%\\xbd,Z\\x8d\\xbc$j9<\\xe7u9\\xbdZW\\x00=nW\\xb8\\xbcr\\x11\\x9b<\\xa1?\\xa2<\\xc8\\xcez=\\xb9\\xd5\\xa0;EJ\\x1f<Mm\\xbe<\\x91+\\x0e\\xbde\\xaaR\\xbcX\\xec\\xa5\\xb7\\xeeo\\xb5;\\xd1\\x9fX=\\xfd\\x94\\xd3\\xbc\\xab\\xf3\\xb2<q\\xd7\\xe7\\xbc\\xda\\xf5\\x85<1\\xc6\\xd4\\xbb\\x90\\xccU=`\\xc9\\xe9;L\\x8f,=\\x9c\\x1a\\x96<\\xf3\\x95?\\xbb0\\xb0M\\xbd\\xb1\\xe1\\xb8=^_\\x16\\xbd!\\xe0\\xff\\xbc\\xbe\\xc4\\x1b<\\xcc\\xfb\\x98\\xbd\\xcd\\xa6L\\xbd\\xb1P\\x87\\xbbsE\\x81<\\xebD;<>\\xcc\\xcf\\xbb\\x8b\\r\\xf3\\xbcn\\x8f<\\xbdt=.\\xbd\\xacp\\x8a=\\x9f\\xd6z=O\\xc9!<z\\xaa\\xad<M\\\"\\x11=\\x17S\\xed<\\xbf\\xff\\xf3:3)\\x1a\\xbd[\\xea\\xab=\\x06 \\xde\\xbb\\xc2\\x94m=\\xcd%\\x95\\xbc\\x9f)\\x80;\\xc96\\xbe\\xbc\\xef\\xad\\x9a:\\x02,\\xc0\\xbcL\\xaa\\xa7<f\\x00 <\\xae\\xd2\\x96\\xbcq\\n\\xea\\xbc\\xbet\\xb2\\xbb\\x02\\xfe\\x13=,\\xdbu\\xbd\\xa09\\x10\\xbc\\xd1Lm;\\x8aN\\x89\\xbd\\xf2\\x8a\\xba\\xbc\\xf8e\\x0e<\\r~\\xef\\xbb\\xf5BV=\\xe4\\x06\\xf4<Ei\\x99:\\xa9\\\"\\x99\\xbd\\x1bQ\\xf7\\xbc\\x88\\xe4\\x8f\\xbb\\xdf\\xb6\\xc6<\\xbc\\x02\\xe0\\xbb\\xee\\x12\\xef:R\\xc03\\xbdm\\xfe\\x82\\xbch\\x0c\\x17\\xbc-\\xdf\\xe6<\\x00\\x1br<B\\x0e2=ylV\\xbd\\xa9\\x80\\xc9<\\x9bi\\xa4=\\xa2\\x8d\\x00=A\\x10\\xa4<\\xaa\\x82\\x0b\\xbdP \\xe0<\\xe5[\\x11=\\x8bW\\xf5\\xbc+\\xba\\x80<\\xa6\\xc6\\x83\\xbd+%\\x0c\\xbd\\xd3-y\\xbd\\xfc\\x12\\xee\\xbc;(\\xdb;\\x1b8\\x0c<\\xc3(P\\xbc\\xf8B!\\xbay\\x10\\xbd:\\x81\\x8b\\xb9<6|\\x18\\xbc\\x8cUV=\\xdc\\xf7\\x91\\xbb\\xe3e\\xaa<|\\r-\\xbd\\x87\\x1b\\xb2\\xbd\\xc3Ci\\xbd\\xa5\\xa4\\xbd\\xbcF+\\r\\xbd`wW<\\xc6\\x0cb\\xbc\\xc9\\xf6\\xd4<X\\xfc\\x81=\\x9f\\x03\\x99;\\xdd\\x13}\\xbd\\xc0\\xbd\\xcc<[\\xe8\\x19\\xbd\\xafk;\\xbb\\x1c\\x82\\n\\xbd*\\x16\\x86<3\\x05\\x02=\\x1ei\\x12\\xbc!\\xd0\\r\\xbd|\\xb1G<\\xd1u\\x9b;vu\\x16\\xbd\\x0bH:=vb\\xae<\\x15\\xa9\\x82=b\\x921\\xbdM\\x03\\xfc<\\x0b\\xbaC\\xbd\\n\\xcc?\\xbd\\x9f\\xff4\\xbcR\\xddz<JK\\xba\\xbc=\\x96\\xeb\\xbc\\xb9\\x075=\\x83\\x04\\x03\\xbd\\x8e\\x1e>\\xbcN\\xc7q;%\\xea\\xc8<E\\xa7\\xb4<\\x99\\x83\\xde\\xb9\\xbc\\x8d\\xd3<\\x97\\x99D;\\xc2\\xad6\\xbd\\xea\\xf7\\x0f\\xbd\\xf5\\xf4\\xb1<i;K\\xbc\\xee\\xb3\\xfa\\xbca\\x90\\x05\\xbd\\x9a9M\\xbd\\xb5B\\x8b\\xbd\\r\\x17\\x0b=\\x04\\xc3\\x89<\\xdbNI<\\xefu\\x1e:C\\x0cY\\xbc]E\\xb9\\xbc\\x8c\\x8d:;uF\\x1b=\\x9c=\\x1e=\\xdc2Z=&\\x00\\xf4\\xba#\\x7f\\x95;\\xddQ\\xc4\\xbc\\xe1\\xd6\\x89<\\xec\\xb0F<\\xe09\\x12\\xbdX7\\xad\\xbb1\\xb5\\xa2\\xbc#\\xa3\\xae=\\x1a^k\\xbd\\xf8@;\\xbdk\\x88\\xe2\\xbd\\x17\\xaeO=\\xa2\\x01\\x1d;W\\x8c\\x0f\\xbd\\x91\\xbd\\xac\\xbdE{\\x07;\\xf1\\xd8Z\\xbd\\x04HU=\\xc6\\xa2\\x0b\\xba\\x019v\\xbc\\xd68\\xcd<\\xf18\\xc0\\xbb{\\x88\\xec\\xbb|\\xe3\\x84<\\xa2\\xb1\\x1d=o\\x13`\\xbdVe\\x99\\xbc\\x10\\xee\\\\=RO\\x1b;\\xde\\x83\\x84\\xbcwO\\x0e=\\xe4\\x1cr\\xbd\\x86\\x16,\\xbd?\\xd5M=\\x10f\\xcc;\\x90\\x185;\\xbb\\xac[\\xbd\\x9a\\x99c\\xbdO\\xb6\\x16<\\xf1:u\\xbc\\x99e\\xc3<\\x89\\xdf7\\xbdr[\\xb6;\\xb3\\x1ae\\xbd\\xc3\\x829<p\\x96\\xeb<\\x9d\\xae\\xb4<2\\xfb:\\xbc\\xd5\\xe2d<b$\\xc2<\\xcay\\xa2\\xbc\\xf3\\xc3\\x19=\\xf4\\xb0\\x0e\\xbcQ\\x1f\\xcb\\xbc\\xab\\xb0 \\xbbZ\\x06\\x14\\xbb\\x90\\x9a\\x05<\\xaey`=\\xf9\\xaa\\xbb;\\x0e\\xbb\\x88<\\xc2q\\x13\\xbd\\x83O\\\"=\\xa0w\\x16\\xbd\\xd5\\x7f\\x95\\xbd\\xa5x\\x18\\xbc\\xfe\\xc9^=2@\\xa6\\xbd\\x84Y\\x0b<\\x05R=\\xbd\\x8f\\xbbv\\xbc\\xbe\\x8c$<\\xa1\\x8e\\x8d=\\xec\\x122;>d6\\xbd{{y\\xbd\\x14\\xb8\\xfd\\xbc\\xd8\\xe5\\xb8:\\x18e\\xcf=\\x98\\x94\\xa9\\xbc\\xd2\\xeb\\x99\\xbb\\x0b\\x07{=\\xbd\\x99\\xb3\\xba:o\\x1d<\\xf0\\x19;\\xbb\\x1a\\xfd\\x0f;\\x90L\\xd6<\\x941.\\xbd\\xdc\\xfc\\xa0\\xbc\\xea\\xdaq\\xbb\\x8a\\xaa-\\xbc\\x02\\x85\\x0c=\\xf7q~\\xbd\\x85j\\x8b\\xbd\\xf0\\x17p<\\xd8\\xf5\\xb0<@\\xee\\x96:\\x88\\xd1,=\\xab\\x8e\\xa7<\\x83\\x8c\\x17\\xbc\\xde\\x1f\\x84\\xbbv\\xc5\\xe0\\xbd\\x1d\\x91u<\\xa6`4=\\xbb\\x92\\x80\\xbd}\\x93\\x19=H\\xd6G\\xbd\\x88\\x1b\\x9c<N\\xf8\\xd5\\xbc\\xd2\\xf6}<\\xf2\\xdb\\x03=hUM<=\\xd2\\xbb\\xbb\\\"\\xf3\\x9e\\xbb\\x8d\\x88\\x0e=\\xb7e/;n\\x8eV\\xbd\\\"\\x9f\\xd5<\\x91\\xa1\\xa7=\\x8c\\xeay\\xbdqYO\\xbcr\\x17\\xab=#\\x8f\\x81\\xbbf\\xc47\\xbdXR\\x9a\\xbcc\\x80\\xac;(\\x93\\xa5\\xbc\\xb9.\\n=\\xf9]\\xdb<_\\x8av<\\xd7\\xb3a=i\\xaf==Or\\x03\\xbd5\\x1c3\\xbd\\xa4\\x92\\x03\\xbcH\\x88\\x82\\xbd\\xa1\\xebB\\xbd#\\xb7\\xcb\\xbb\\xbc\\xca\\x1a\\xbd\\x19\\x8db=p\\x82\\xf1\\xbc\\xf4f\\xa2<\\x94\\xdd\\xce;\\x9e\\x04\\xce;\\xd5\\x86\\xed\\xbc\\xcd\\xdc\\xf4\\xbc\\xbf\\x94\\x9d\\xbcou\\x8d<\\xfc\\xa2\\xa4\\xbc\\xcd\\xa9Q\\xbd\\xdcV\\x1c=\\xa6F\\x8f\\xbb\\xb5\\xd4\\xd2<\\xa8\\xfa\\xa0<\\xc0\\x1d\\xfd:K(\\xa0=\\x9e\\x83\\x97\\xbc[\\xeeP\\xbd{(\\xb9<)\\xf9_;d]$=\\xe6_\\xe4<<\\x1f\\x95\\xbc\\xa1)\\xfd\\xbc\\x06\\xa1\\t\\xbd\\x0b\\xa2\\xb8<\\x1c\\xf0\\xa7\\xbb\\xd0\\x9f?\\xbd\\xa6\\xd9-=\\xfb\\x1b\\xf9<yW\\xd5\\xbcy l\\xbd\\xa0\\xa1\\r\\xbd\\x14I\\xfe<\\x87g\\xff;S\\xe6\\x18\\xbd4gg=B\\xfe\\x1a=\\x08\\x80\\xd9\\xba\\t\\xbb\\xdf<\\xcb\\xeb\\x92<@\\xd1E\\xbd\\xc45\\t\\xbd\\x03\\x92\\x98=<g\\x8b<,\\x0bE<\\xa8c\\xa8\\xbc~\\xb4\\x86;\\x93AL=G\\x99\\x02\\xba\\xd6\\xd7\\x92\\xbc%\\x0c\\x82\\xbd\\xa6\\x0f4=\\xf0a\\xed\\xbc\\xdbFr\\xbdO\\x8e4\\xbc\\x89\\x9e\\xdb;+\\x06\\xd2\\xbc\\x87\\xa8\\xa6<\\xb2\\xc1\\x03\\xbd\\xbb1\\x8f\\xbd\\xa6\\xe0~=\\xc6_l\\xbd\\xe5\\xe9t\\xbd \\xd59=B\\x12 =\\xf2o\\xef\\xbc\\x91\\xdb\\x18<b\\xcb+<f\\xae\\xe4\\xbc\\xca?\\x81<\\xbf\\x07}\\xbad\\xce\\x87\\xbdE\\xe4\\xdc\\xbc\\t/\\xf7\\xbc\\xf0\\xe0d=\\x86G\\x05=M\\x85\\x03\\xbd\\x9a2\\xfe\\xbb\\x8cd<=2\\xde\\xa7\\xbc\\x9f\\xb4b\\xbb\\xeb&\\x16=n\\xf0\\x88;|\\x13\\xba;e\\xd1\\x83=\\xbd\\x86,\\xbbi=\\x11\\xbc\\xc7\\xbe\\x82=U\\xbbW<2\\x05\\xd5\\xbc\\xf0\\xd1\\x8f;a\\xd4N=r\\xa3D<\\x7fA\\x1f\\xbc\\xc1\\xdca=v\\x01\\xc4<n\\xbd\\x1e=\\x12\\xd4\\x12\\xbc\\xc0p\\x1a\\xbdM4D=\\xa7\\n7<Hx\\x11\\xbd\\x94\\xc6C<\\x1a\\x8c\\xfe\\xbc\\xd4\\xc8\\x00\\xbcg\\xd2\\xd3\\xbc/\\xabl\\xbd\\xdf\\xaa\\xd4=[kT<\\xd6?A<\\xc9\\x84S==\\x8fu=\\xf3\\xbaX;3\\xf1\\x0e\\xbd\\xa4s\\xc8\\xbc\\xf9zT\\xbc\\xa2*\\x03\\xbd\\x83\\x1c\\x86\\xbc`\\xa3\\xa2\\xbb\\xea\\xc2\\xc3<b8b\\xbd=\\x08\\xfd\\xbc\\xcb\\x0eW\\xbb\\xa5\\xed\\\\\\xbd\\x07\\x93\\x0c\\xbd\\x01\\xf9F\\xba\\xe6\\x1b0=\\reP\\xbc\\t\\x84\\xa8<\\x96\\xfec\\xbd\\x97[\\x01\\xbb\\xebt\\x94=A\\xf0\\xb4=\\x9fd\\x05\\xbd\\xeb\\xee9=\\x08?\\x86\\xbaH\\xd4\\x859\\x01\\x93r=\\x80\\xc5\\xf9:1\\xd3\\x84\\xbc\\xb3kL=V\\x12j;\\xa2\\xc3\\x87\\xbb\\tE\\x97=l\\\"\\x1c={\\x02f<\\xf6\\x80)=*\\xb7\\x14<\\\\\\xad\\xb9<\\x05:\\x1f<\\x97\\x9c\\n<Qb\\xad\\xbc#\\xbf1=\\x85\\x89t\\xbbv\\x9aU\\xbbS\\xc65=V\\xb5\\xb8\\xbc\\xa7\\xc7\\xd5\\xbd;\\x00_\\xbcV\\xda\\xba\\xbc\\xed\\xf7\\x99\\xbc1\\x8c\\xad<\\x00\\xb5\\xe7\\xbaj\\xd2q\\xbc\\xc7B\\x9c=><\\x90<\\x80\\xb7S\\t\\xa2\\xaeW\\xbb$c\\xb9;\\xce\\xf6+\\xbd\\xfb\\xaeS;m\\xd8a<\\xb0T\\x0c=q+\\x19<\\x13\\x8d\\x97\\xbd\\xf5\\x07K<A\\xb5\\xef\\xbc\\n\\xc9\\xea;\\x9f\\xb3&=\\xdf\\x0e)\\xbc\\x00\\x1a{=\\xdc\\xcf\\xab\\xbc>\\xfe\\xcb<\\x14\\xd0\\xff\\xbc\\xe2N\\xe2<.<T<\\x07\\xbe\\x0f=\\xa8\\x1fJ=u?8\\xbc\\xef-\\xc3:\\xa3\\x19\\xa8;k(\\r<\\xc2U\\xb0<\\x03\\x85\\xcf\\xbb\\x14\\xfa\\xb3\\xbc\\x0b\\x15\\xa7\\xbc$\\x07\\xb8<\\xb9\\xce\\x1b\\xbd\\xdb\\x91\\x93;:>O<o\\xdaw\\xbd\\xc1\\xfa,\\xbc\\x83j`<\\x0f\\xed\\xfd<\\xa7\\xba\\x88\\xbc}\\x97\\xc2=%\\xee|;\\xf5\\x07\\x19=\\x15C\\xa0<Y\\xce\\x86<P\\xcd&=\\xfb\\x80-=9S\\x9b:.\\xd2\\xb5=J\\x87\\x8a<p\\xa8\\xa4\\xbc\\xff\\xe2\\xf8\\xbct\\xca\\x17\\xbd\\x8c\\ti<\\x98\\xc3G<\\x951\\xf8\\xbb\\xff\\xf0n<\\x03C==b\\xbdo<2\\xfe!=g:\\xfb\\xbb\\xf1\\x8c\\xa7<Y\\xff\\xf1\\xbb\\xa6\\xab\\xd0\\xb9g\\xc5\\x9e\\xbb\\xb7C\\xdd\\xbbI\\xb5|\\xbd\\x85!\\x06\\xbdf\\xd7\\x8a\\xbc\\x83\\xa3\\xa4\\xbc~uC<\\xda\\x1e\\x08\\xbc!-\\x8e=:\\xf7\\r<\\x80\\xf5\\x82<Jk\\x9e<z\\x85c=ak\\n<Qo\\xc7<kI\\x86<\\xa3l\\x9e\\xbcV\\x86\\xa2\\xbc %\\xca\\xbc\\x8eq\\xb7<S\\x0f\\xbb\\xbc9\\xa8\\x8e=q\\x96y\\xbc\\x15\\xbd>=;:c=\\xbe\\x03:=\\xae\\n\\xc5<\\xe3\\x0e\\x99;\\x8b\\x10\\x07\\xbd\\x08\\x92\\x9c<\\x81\\xbc~=\\xd99\\xe89~\\x7f\\x10=R\\xc8\\xc3<\\x90\\xe2\\x97\\xbdL\\x83\\x13<\\x83\\xbb\\xaf\\xbc1\\x03\\xe0\\xbc@\\xba\\x15\\xbd\\x01\\x9c<\\xbd\\xa5\\xf9\\x07=\\x98\\xad\\xb5\\xbdg\\x96\\xd8\\xb9\\x92\\x95\\x9a;\\xce\\xa8X=q|\\xad\\xbc\\x1b\\x0c\\xb3\\xbd\\xdb\\x92\\xeb\\xbc\\x91\\x0c]\\xb9\\x07\\xdcJ<\\x8dK\\x9c\\xbd\\\"\\xc5\\xbd=\\x12\\xd4\\\";\\x14\\x94\\x07=D&\\x00=R\\xfd\\xef\\xbcw\\xd3\\xe4\\xbc\\xf5\\xa6b\\xbdH\\xa8\\x86\\xbc\\xc1\\xefR=\\xb7\\xde\\x1f\\xbcpr<<\\xbd\\xea-\\xbdE\\xf0\\x94\\xbb\\x85\\xc24=\\xc7\\x1dd<\\xb6\\xd8\\xe7<\\xc4\\xd4E\\xbd>\\xb7-\\xbd\\n\\xeaO\\xbd\\x15\\xb56=A\\xce\\x13<\\xed\\x18)=Z\\x1d\\xa3\\xbbAd!<\\t\\xd5\\x99\\xbdB\\xaa\\x88;\\xd8\\x8a\\x91;\\x9fR\\x16\\xba1\\x10\\xad\\xbc\\\"\\xb1\\xd0\\xbbe\\xca\\xda\\xbc\\xd0u\\xff\\xbc\\x99h\\x1b<\\x1f\\x8c=\\xbcR\\xfcD<\\x8e\\xc2\\x99\\xbcV`\\x82\\xbb\\xaf\\x06^\\xbb.\\xac\\n<\\xc8\\xb7d\\xbc\\xed@\\x8f<K\\xef\\xe9\\xbc+\\x1eW;\\x15\\xe5\\xd3\\xbc\\x03\\xb0\\xb5=\\xb2\\xd3k\\xbbXSB<\\xd9\\x1e\\x8e\\xbdU\\xcb#=O:2\\xb9\\xd1;)=Ri\\xb79\\xae\\xb0\\xbc\\xbc\\xfa<M\\xbd\\xd3\\x8f\\x0f\\xbb\\xf8\\xbd\\x97=\\xc1\\x89\\xbe\\xba\\xc7\\xac\\xe3\\xba\\xef\\x87\\xc9<O\\xf0\\xbe\\xba<\\x06\\xcb\\xbc\\xa2&\\xe3\\xbb\\xab\\xf1\\x8b=F\\t7;\\x1e\\x1b\\xb4=\\x1d\\xd7\\x05\\xbc=\\xc80\\xbd\\x92F\\x82\\xbdj\\xadL\\xbdCN\\xa3</\\xecQ\\xbd\\x81\\xa4*\\xbc\\x97\\xa7\\xd6\\xbcWAW\\xbc\\x0e\\x08\\xd8<\\xd4\\xd5#=\\t\\xb7\\x02\\xbc\\xa4B\\xf8<\\x9d\\x01\\x9b\\xbcmaq\\xbdF\\xe9\\xc7:\\x8f\\xd1X\\xbd\\xe4\\xbeJ\\xbd\\xd5a\\xee\\xbb\\x81P\\x80\\xbc\\x97\\x85\\xf6\\xbc\\x1f\\x96Y<\\x1fp\\x1b\\xbd\\xea\\xc0\\x15=\\xdd\\xe5\\x91\\xbaZ\\xd7\\x93\\xbd\\xfb\\x16F\\xbcA,\\xd5;K\\xe4\\xcc<\\\\\\xc34\\xbcU\\x0e\\xff\\xbb\\x1a\\xdd-<z\\xa6\\xe1<\\xd6{\\xdb:\\x93\\x1c\\x7f=\\xac\\x94\\x84\\xbb\\x16\\xe7\\xb7\\xbc\\x08\\x84\\xb6\\xbc\\x10\\x127=\\xadT?\\xbd\\xccY\\xe7\\xbbh\\xb4P\\xbd\\x82\\x16\\x0b=\\xb7\\x9aH\\xbd\\xce\\x1cL\\xbc|y\\xba<q!+=\\x03\\x81I\\xbdn\\xd3\\xbc<mN\\x02\\xbd%<@\\xbd\\xf4\\xed\\xd8;b-\\xb4<\\xb4\\xf0\\xe8\\xbcu\\xff\\x9f\\xbc\\xf0A\\xf0;\\xed\\xb9/=~+}\\xbc\\xa9M\\xa2<\\xe4j-<\\x12\\xf6\\x0e<\\xcc\\xa8\\xb2<s\\xeb\\xe3\\xbc\\xec5a\\xbc4Y\\xef<G\\xc3\\x07<R\\xcc8;\\xefh\\x1b=\\x9d\\xb2(\\xbd\\xb7\\x03\\xb0<\\xc3\\xd8f\\xbdDV\\x16\\xba\\xa6\\xad\\xf3<8l\\xbb\\xbc\\xbd\\xb89<\\x88$J=\\x14\\x1f\\xc1\\xba\\x8aUN=\\xa6\\xcc\\x95\\xbdI0:=\\\"%Q<\\x01\\xb5\\x9e\\xbd\\xfe\\x13B\\xbc^\\xb8\\xc2\\xbb]A\\xf9<\\xd9r\\x14=\\x96\\xa6}=\\x04\\x9a\\xfc\\xbc\\x17s[=TH\\xe2<\\x80\\xa6g\\xbd\\\\\\xba\\xb5;\\xc5\\xa0\\xd8\\xbc\\xfe\\xa3\\x8d=\\x1d\\xb5\\xb5\\xbc\"\nHSET bikes:10069  model 'Nereid' brand 'Bold bicycles' price 1175 type 'Mountain bikes' material 'full-carbon' weight 8.2 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"tE\\x93<T\\x91\\x9e<\\x9b\\xf6?\\xbd:h4=\\x1fz\\x9e<\\x82\\xb0\\x8a=\\xa1W\\xd5\\xbb\\xcd\\x08`\\xbduRs=\\x14\\xb2\\x93<6H\\x03<,yC=\\xcf\\xbf\\xee<\\x19\\x9d\\x06<\\xb4\\xe5\\x9d=g\\xdaD\\xbd5S,\\xbc\\x9fp\\xb1\\xbc\\x95q\\xe7\\xbbdL\\xa6\\xbc8 \\xb4;\\x86h\\x00\\xbd\\x14\\xa1\\xb1\\xbb\\xb2O\\x9f\\xbc<\\xba\\xc8;\\x85%\\x89<j\\x98\\x9d<z\\xc0\\xce;TNL\\xbd\\x1f\\x90\\t=\\x81\\x90\\xcd;[\\x1aG\\xbb\\x13%\\xac<\\\"\\xad\\x1c=\\xd0y\\xf9<ami;\\x08P\\x9c<\\x80W\\x05\\xbd\\xd9\\x9b\\x0c<\\x8eE\\xe8;\\x124\\x84=a\\xba(\\xbb)R\\xb0<q\\xf3\\x99<y\\xd18\\xbb\\x8c\\\"6\\xbc\\xcc\\x84\\x94=Nr>\\xbd\\x00\\xc3\\xa9\\xbc9\\xcb\\x97\\xbc\\xdd\\x81\\xbb\\xbc\\xd0!\\x1e\\xbdz\\xb9:;\\x97\\xa7\\x9f\\xbb8\\x92d<\\x96t\\xa5\\xbcD\\x04\\x96;\\xbb\\xe5\\xae\\xbd\\x95\\\\q\\xbdc]\\xaf=\\xbc\\x82\\xbd<7\\x15\\x87\\xbbO\\x15\\xf1<\\xe9\\xe7\\x95=\\x0c\\xe78=M\\x93\\x97<\\xf2\\xc4\\xd9\\xbb\\xad\\xe5?<\\x1a\\xee\\x80<@\\xcbG\\xbc\\x15\\x83@\\xbcP~\\xc8\\xbc\\xb72\\xeb\\xbc\\xf7\\x9e\\x9f\\xbb=T\\x1f\\xbd\\x00kK\\xbc\\xf4\\x91\\x1d=\\xa6\\xb8\\x19\\xbd\\xc2\\xfed\\xbd(\\xecs<wA\\xec<\\xaa\\xd0\\x80\\xbda\\x0f\\xfa\\xbc\\xfcXg\\xbdF\\x07(\\xbch\\xf5\\xde<\\xe3\\xf9\\x1a\\xba\\x8epr=A\\xb43=\\xcf\\xbd\\xff<{\\xb8\\x19\\xbc\\x8f0\\x1f\\xbd\\x14\\x07&\\xbd;\\xcd\\xea<q.\\xc2<,\\xc44=h\\x9d\\xd0\\xbc\\xa1\\x16\\xd3\\xbc\\x91-t<:;[\\xbd9\\xe1\\x16\\xbdS\\xe3\\x1b<\\xbb=\\xb5<\\xe9\\x87\\xe2\\xbc\\xc9k\\x91\\xbcx\\xea\\x9e=kG\\xcf<\\x8d\\x00\\x19\\xbc\\xab\\xd1\\xad\\xbb}\\xa6\\xf6<D\\xf4\\x86<\\x15O\\\"=\\x1eE\\x91\\xbcr\\xf8\\x1b\\xbd\\x1b\\x03T\\xbd\\x06\\x0f\\x8f;\\x89\\xfb\\x0b\\xbc@\\x0e\\x18\\xbb#\\xa8W<\\xb1\\xacf\\xbdf\\xa0\\x90<\\\"&\\x07=\\x91\\xed\\xff\\xbb]\\n\\xf6;\\xe1\\xe7\\xad<\\x93\\xc7\\xf9\\xbcz#\\x0c\\xbc\\xee$\\xd5\\xbc\\xed\\xc1\\xab\\xbcz\\xa9\\x82\\xbd\\x9a\\x85\\xd7\\xbcJ\\x80\\xfa\\xbc\\x82\\x82\\xbd<\\xb6/B\\xbd\\xadf\\xf6:6\\xd8)=\\r\\xaf\\x8f\\xbc\\x07\\t0<\\xeda_<{]*\\xbd:WA\\xbd\\xb9\\x15{\\xbd\\xbd\\xdfv<\\xd8\\x89\\x9c=\\x184\\xc1\\xbc.\\x1c\\xd8\\xbc\\xba\\x1eL\\xbaQ\\x97\\x13\\xbdE\\x99\\x19;\\x98\\xf1&\\xbdYk\\xea\\xbc\\xde0\\x81={\\xad\\x0c\\xbd_\\xb5\\xd1<]\\xa0\\x8b\\xbd\\x9e\\n\\x1a\\xbdU\\x9d;\\xbb\\xad\\xdb\\x16<R)\\x86<\\xf0\\xd2\\xa3\\xbde\\x1d\\xc1=\\xc9\\xc9\\x9a\\xbc\\x90\\xc11\\xbc\\xb3\\xa9\\x82\\xbd\\x0f\\x88\\x80<\\xf9\\xe4e<.0\\xd1\\xbc\\xec\\x83#=\\xb4,a=t\\xffS\\xbbp\\xa5\\xc7\\xbc\\x1e@[=\\xd4\\xa25<\\x03\\xc9=\\xbc\\xe0\\x1b\\xe6\\xbc\\xce\\xdb\\x17=da\\x92\\xbd]\\xe9B=\\xe7\\xe1\\xbb<SOT\\xbc\\x07\\xe3\\xdb\\xbb*\\x962=O\\xec\\x14\\xbcv\\x93u:\\x9fO9=\\xb3!\\xd6;D\\x84\\x80=\\xd0\\x7f@;\\n\\xc4u<\\x8eii\\xbdyo\\r;E\\x04==\\xc5q\\x01\\xbd\\xb8\\xda\\x02=\\xd9{\\x85<\\x97\\xb3\\xc5<\\xc4_s\\xbd\\xbf\\\\{\\xbd\\xa7\\xfaa\\xbd\\xc6\\xa7^=\\n\\xc0\\x12\\xbd^oB\\xbdH\\x0e\\xb8\\xbd\\x8f,\\xfc\\xbb\\xc5f\\x9c<|\\x1a2\\xbc\\x16\\xfb!\\xbdF\\x84\\xb9\\xbc\\x11t\\x8a<\\x848\\xb8\\xbb\\xb1\\x85K\\xbc\\xf6\\x1d\\xc9\\xbaF\\x109\\xbc\\x97\\x99\\xd3\\xbc\\x16\\xc1:\\xbd\\x81\\xd7\\x0b\\xbcP\\xdd\\x11;\\xaa\\x8d\\xc9\\xbc69\\r=\\x00\\xd8\\xc0\\xbdod\\x00=\\x86\\xca\\xa3\\xbc\\x1e\\xf0\\x05\\xbd\\xd7\\x85\\\"<\\xaaK\\xaa<\\xe6\\xf8B\\xbd\\x9c\\xd2\\xcf<\\xa9\\xa3d<\\xd6\\x04[=\\x07\\n\\x8c\\xbd\\x11\\xc0\\x10\\xbdJ\\xadp\\xbdn\\r\\x83<\\x1a\\xe4\\xb3<5\\x9fB\\xbbA\\x84\\xea;\\xed8\\x1a=\\x07\\x85\\n<E\\xdb\\x9f\\xbc\\x1d\\xa9\\xa7;\\x84O-\\xbd\\xf3\\xaf\\x1b\\xbb\\xf3\\x17a=ml\\xbd<t\\x0f\\xd0<\\xb8\\\"\\t=(U\\x19=\\xacO\\x80;\\xb2\\x81\\x15\\xbdh\\xcej\\xba\\xa5\\xce\\n<\\x8e\\x8aS\\xbd\\xca\\x94i;\\xc2\\x96\\xa6=@0\\x8b\\xbd\\xa0\\x97&=d\\xf0\\xf0\\xba\\xe6I\\x86\\xbcz\\x9d\\x03\\xbd\\x1a\\xbe\\x8e=\\xb5\\xb18<B:\\xb2\\xbc\\xd4#6\\xbd\\\\|\\xf7\\xbc\\xe0\\xda\\t\\xbd\\x19#\\xba=v\\xce\\xc3;$\\xf6\\\"\\xbcYF@=Y#>\\xbb\\x0f\\x0ci<l\\xb3\\x9c;\\xc8\\x9d\\xf0\\xbc\\xdd\\x88\\xfd<\\x86\\xa1 \\xbd\\x93\\x15\\x19\\xbc\\xcf:\\x12<\\x93\\xba\\xad<\\xf3OS<r\\xd4\\xa3\\xbd\\xb1\\xdc\\x05\\xbd\\xea\\xb5\\xb9<\\xf3\\xd7\\xf8\\xbcw\\xc1I<\\x13\\xb9q;;\\xb2\\x96\\xbc\\x97\\xaf\\x01\\xbbb\\x9e\\xc3<\\xcb\\x94\\x97\\xbd\\xf2\\x00\\x82<bq\\xb5\\xba\\xfe\\x87 \\xbd\\xeb\\xdeP=`\\xda\\x84;\\x18<\\xb5<\\xba\\x7f\\x1e\\xbd[z\\xd6<r\\x87\\xb8:e\\x80|;\\xf0I(\\xbc^\\xf0\\x83<\\x07Q\\x84<Y}v=Q\\r\\x0c\\xbd\\xf67)=&\\x0c\\xe2<\\x8c\\xcf\\xd3\\xbb\\xc6\\xf93=<P\\x9f=\\xea\\x16e<W\\x0c\\x06\\xbdR\\xd9\\xbf\\xbc}<\\x90\\xbc0\\x85,\\xbd\\t~N<\\xf7\\x1c\\x9a\\xbc^i\\xa2\\xbc:w\\xa1=\\\"\\x1e\\xa5<\\xca\\x10\\x93<C\\xc4l\\xbc3T\\xb5\\xbc\\xef\\xf8\\x14\\xbc\\x89-\\x7f\\xbcO\\xf7\\xd5\\xba\\xf8D=\\xbd\\xc9\\x0c\\xbc;S!\\xf4\\xbc\\xbc\\x80_<\\x12\\xcf@\\xbc\\x01\\x06\\x87\\xbc\\x88\\xfc^\\xbd\\xfe\\x00d\\xbb\\xeds\\xaf\\xbc\\x00\\xd7!<\\xb5\\xf4@:\\xe7\\x98I\\xbd\\r\\xb0\\xdf<C\\x90\\xa6<\\xdb\\xf9\\xa8\\xbb\\xb4\\x18\\x0f\\xbd\\x01\\x80\\xc2;\\x97\\x91\\x8a=g\\x97\\n\\xbc+~\\x8f\\xba\\x84\\xb2V<\\xeeI==$\\r&=\\xacRr=p\\xfd\\r<\\xa5\\x8e\\x98\\xbc\\x16G\\xa6\\xbc\\x18\\xd2d=\\xc9?\\xd8\\xbck*@\\xbdz\\xb7_\\xbc\\x9e\\xf8\\xb3<\\xd2/O\\xbd\\xae\\\\r\\xbd\\x1b\\xf6\\xcd\\xbd\\xcf\\x13`<\\xbb\\xbf\\x00=\\xf5h\\xa1\\xbbO\\xc6\\xb4=\\x1f\\\"\\x90=\\x94\\xbf\\xaa\\xbc&\\x9d\\x14\\xbd\\xb1\\xe4\\x96\\xbc\\x98\\x9e\\x97\\xbc\\x0c+\\x01\\xbd\\xf9{\\xa8=\\xb2\\x90\\xaf\\xbc\\xccx\\x16\\xbb;\\xee$<~\\xf7\\x9d<\\x9f<;=\\x99\\xb0\\x92\\xba\\xfdUF\\xbdZ<\\xf6\\xbbMK\\x0c\\xbc\\xc4\\xee\\x04\\xbd_Ny\\xbdcU\\x05\\xbc\\xbbC\\x9b;\\xf1\\x8cO\\xbd\\x00:\\xc0<\\xe3\\x02\\x99\\xbas\\xb5z\\xbc\\x92\\xd7,=8\\xb6\\x98\\xbc\\xd2\\xf4\\x8f\\xbd\\x02\\x01\\x91=\\x03c\\xb9=\\xea\\xc1\\x04\\xbd\\xfb\\x0b;\\xbbf\\x99\\xa4<\\xde\\xaax\\xbd\\x9a\\xd9E=\\x95\\x19K=I>\\x80\\xbd\\n\\x86\\x1d<\\x91\\xbcI\\xbd\\x11\\x84\\\"=?\\xeem;\\xbf\\xca\\xb1<\\x8dr\\xd6\\xbc\\x9b\\x99<=&\\x90\\xc8;\\x8b\\xc5b<`\\xc5\\xea<n\\xfb\\x96<\\xed3\\x8d<\\x08\\x82\\xe7<c\\xac\\x15\\xbd\\xf5\\xcc\\xbf<\\x84\\xbcK=A\\x10\\x95<O\\xa6\\x91\\xbd\\x8f\\x80V\\xbc\\xac\\x97\\x8e;W\\xca\\\"\\xbc\\xeco\\x8a\\xbc&F\\x0b<^B\\xb6<\\x82w\\xb2<\\x08\\xca\\xf9\\xbcx\\x7f\\xcc\\xbc80\\x0c=\\xebR)\\xbc)\\x12W\\xbc\\xe1\\x18\\xde\\xbb{\\x8fp\\xbcaR&\\xbd\\xea\\x0bj\\xbd6\\xde\\xd7\\xbc\\x00\\xd2\\x8d=W\\x00\\xcb<\\xcd\\xc9\\xb9<\\xf7\\x1d\\xf5<\\x12m\\xf0<\\xec\\xdf\\x17\\xbc^3\\x1e\\xbd:\\x91\\xa1\\xbc\\x9a\\x96\\x81\\xban*\\xbf\\xbb\\xbf\\x9f\\x1f=\\x8fu\\x04<\\x15$t;\\x04v)\\xbd\\xc3{\\x17=\\x11\\x05\\xa4\\xbd\\xbf\\xb0y\\xbd\\xa3\\xdd\\x0c\\xbd\\x86\\x91\\x84\\xbd\\x97\\x8bN=K\\xf6\\xc9<P#h\\xbb\\xff[\\x1b\\xbc\\xb2\\xa9\\xa2;\\xc7x\\xba=O\\xa0%=\\xcb\\xb0\\xba\\xbc\\x8b\\xaeB=\\xa7@K\\xbd\\x98\\x0c\\x9f<|\\xae\\x04=\\xa6W\\xb9<U`H\\xbd\\xa1\\x00\\x8c\\xbb\\x0c\\x9e\\x8e<b\\x03\\t<\\x8cc\\xae<\\xad\\xa1\\x14\\xbc6\\xc5\\xf2\\xbb@\\x93/<\\xc3\\xb2.<Q\\xcdN<@\\xd6\\xe0;[R\\xd4\\xbc\\xf9$\\xe2\\xbc|`Y<V\\xa8\\xbb\\xbb\\xef\\xd46\\xbc\\xa9\\xfe}<\\xb4\\xcd*\\xbco\\xec\\xdc\\xbd\\xc9\\xad\\xc2\\xbcR%\\x8a\\xbcd\\x0b\\x12\\xbb\\xb8\\x06\\xa3<\\x8d\\x83\\xf5\\xbc\\xfb\\xbai<y\\x96F=\\x0b\\x0c\\x13\\xbc\\x14!N\\t\\xa6\\x84\\x9b;\\xb4\\x14\\xee\\xbc\\x83\\x8d\\xdf<\\xb2\\x9b\\x93<\\xe5_M<\\x89\\x9a|=\\xcd\\x0e\\xbd\\xbb\\x896j\\xbb\\xc8P\\xfb<\\xdbP\\x84\\xbc7L\\xdc<=T\\x04=\\x1d\\xd2\\xf1\\xbcD\\x137=\\x0bwQ\\xbc\\xc0\\xdd\\xed<-\\xa0g\\xbd\\x9b\\\"\\xc6;\\x7fB\\x8f<-\\x8aw=\\x94\\xa9\\xe4<i\\x1dY\\xbc\\xfb\\xe9\\xfb\\xbck\\x97!\\xbbR\\x85\\xc3<\\\"I\\xba;\\x04Hr\\xbc\\x8e6\\xd8\\xbb\\x0fl\\x92\\xbcDW;\\xbcPSc<\\xc2ZO<\\xb07\\x1d\\xbd\\xb2\\xc4~\\xbd\\xd8]Y\\xbd\\xe1eJ\\xbc\\x7fZ\\xc8<\\xe4\\xc2\\x8f<K,f=\\xf4\\xc5X<*\\xbc\\x9d\\xbc\\xbf\\\"\\xdf\\xbc\\x83\\xaa\\xc5<\\x85\\x93\\x92\\xbc\\xdd1`=\\x9b\\x99\\xbb<\\xf9\\xb4\\xa3=~\\xb7+\\xbcu\\xda)\\xbd\\x97+\\xb4\\xbc\\x04$m\\xbd\\xc3\\x84\\x84\\xbd\\xf6\\xafE={\\xa4\\x82<)\\xd6\\x08<\\x9b\\xc2\\xfa;l\\x0c)\\xbc\\x8c]f=\\xe2\\\"\\x9d;3\\x86P=.M/<\\x8c\\x91$\\xbc\\xa6\\xdd\\xad<\\xd3\\xe1\\xc1<\\xe7\\x17\\xc0<\\xdf\\xa5n\\xbd\\x8e1K\\xbdU\\x17=<\\x00\\xf6\\xe9<\\xcd\\x1fc\\xbc\\xe1\\xe8\\x11=u5\\x83\\xbcT}\\xb3\\xbc\\xf6$\\xb4<\\xe9\\xb8o=#%T\\xbdS[\\xb3\\xbc\\x1d\\xf9\\x14=D\\x1c?\\xbdTi4=C\\r\\xac\\xbc\\x89\\x0f\\x939G\\x02\\xb7\\xbbb\\xcaI=P\\x18\\xb2\\xbcA\\\"\\x05=\\xcd\\xce%=!\\xe1x=n\\xd8\\xb4\\xbcH\\xf5\\x16<\\x9c\\xdb\\xb8\\xbc\\x93\\x85.<\\x16\\xd8P=\\xc5\\xaf<8\\x07\\x8e =.\\x16J=\\xf7b\\x1e\\xbd\\xc1M\\xd6\\xbc\\xcen\\x87\\xbc\\x94\\xef\\xdb<\\\"\\xe1\\\\\\xba\\xa6\\x9a\\xcd;\\x1e\\xc6\\x89\\xbb\\x19\\xc8A\\xbdq-\\x00=\\xba\\x88\\xc0\\xbc\\x05\\xf6d<\\x99\\xfb!;\\xeaa\\xd3\\xbd\\x11\\xb2\\x1c\\xbd,/\\xc1<\\x13:\\xae\\xbc`\\xe1a\\xbd\\xcb\\x0b\\x90=\\xdd\\x13\\x0e>*s\\xfa<^J\\xb6<\\x92`\\xfa\\xbc\\xfc\\x14\\x07<!\\x99\\xa7\\xbd\\xea\\n\\xef<\\x0b\\x9e\\xd1<Y\\xbd\\x18=\\x06\\xd3\\xd4<\\xb5\\x07\\x9a=]+;\\xb8\\xc4T\\x9a9w\\xf2\\x07<\\x1a%u=5\\x98q\\xbd\\x80\\x82R\\xbc\\xec\\x9aq\\xbd\\xb2\\x9f\\x9b=s\\xb2\\xd7\\xbb\\xa2A\\xb9=v$Z<8\\x7fw=c\\xc0\\x80\\xbd?\\x87\\x9c<\\x96\\x1e\\x02=\\xa8\\x16\\xef;_\\x9bI\\xbd\\xf4\\xed\\xa3\\xbc\\xa2\\x0f\\x87<dyP<FA\\xab\\xbc\\x8a\\xdb\\x07=\\x93\\xedz\\xbc\\xc2\\xc0\\x10=O\\xaep\\xbb\\x9a\\xfd\\xdf\\xbcc\\xf5\\x06<@\\xe4D\\xbb\\x95\\xa2\\x10=\\xc4$<=\\xe5\\x02b<\\x13}\\x10\\xbd\\x9b\\x1d\\x7f=\\x07\\xbd\\xb2;dO\\xc3<\\xc2?\\x82=\\xe3q~\\xbbG\\x0cK;\\x91r#<\\xb4\\x04A;\\xaf\\x9d\\x1f\\xbb9\\xf2:\\xbd5\\x99\\x19=\\xb1,%=\\x10Z\\x01\\xba\\xbb\\xe5\\x8c<`\\xc3\\xea\\xbc\\r)\\x00\\xbb\\xbf\\x9b\\x9c;I\\x06e\\xbb\\xa3\\x9e\\xa2==\\x0c\\xee;*\\x03\\x99<\\x9c\\xef><\\xc2s\\xe2<\\x148\\x9b\\xbd\\xe6K\\xd2\\xbd\\xe3n\\x83<\\x85\\xc7\\xa3\\xbc\\xbb\\xaaX\\xbd\\xce\\x9b\\xd1\\xbc,\\xcb\\xe0;`\\x92\\xdb\\xb8\\xa764=2\\xdf\\x8f<U\\x15#\\xbdd\\xb5~<_r;\\xbd\\x03\\x11\\x02\\xbdg4@\\xbdaI3\\xbd\\xde\\xe2l;\\xfb\\xa5\\x9d\\xbcksG\\xbba\\xda\\x16\\xbd\\x82\\xb9\\xf6\\xbc\\x7f\\x14?\\xbcW\\x87\\x18\\xbd}\\x91\\x98\\xbd]]\\xf3:\\x02\\\\ \\xbc\\xea[I<\\xec=U\\xbc\\x19\\xc7.\\xbc\\x8f+^\\xbd\\x1dpY<\\xcf~\\x02<\\xa3\\x0b\\xe7<I\\x98-\\xbc\\xe1\\xc9\\xe3\\xbc\\x98\\xbc\\xa1\\xbbu-\\x8d<\\xf7\\xcf\\x84\\xbc\\x0f\\xafw\\xbdZ\\x0eB\\xbd\\x15\\xe9/\\xbd\\xd7\\xdc\\xb7\\xbb\\xba\\x9b9<=7w=\\xf2t\\xbe=\\x1f4\\x1f\\xbc\\\"\\x1c\\xd5<\\xd4\\xdcL\\xbd\\xe5\\x95\\x1f\\xbd`\\xfc\\xad<B\\xf1\\x8c;\\x80\\x7f\\r=\\xca\\x8f\\x81\\xbcA\\xfa\\x93\\xbc\\xf5\\xe3\\xa2;\\xb3\\xd1\\xb1\\xbdk\\xf5\\x83;\\xb6(\\x19;\\xfa\\xb3\\xac<gC\\xcc\\xbc\\xbd\\xdc\\x92\\xbc/\\xbe\\xf6<\\xdb\\x01\\x80<\\x9b\\nv<;\\xce\\xfd\\xbc\\x84\\x18\\xd6<(\\x86\\xaa\\xbc\\xe0\\x1b1<?I\\x05\\xbdCy\\xbf<\\x8c\\xefW=\\\"R?<\\xa5\\x93\\xc3\\xbby\\xb7r=\\xe9\\xf0\\xb3\\xbc\\xfb+\\r;\\x03\\xb22\\xbc\\xfb\\xd6\\xa7\\xbcQ\\xd1\\x06\\xbd\\xf9Z\\xa8\\xbd\\xcc\\xcb@=\\xad\\x1e1=\\xb0\\x14\\xec<W{\\x9b<H\\x18\\xc4=k\\x8cB\\xbc}\\x1ej=\\xed\\xc5\\x07=\\xea\\xd9\\x9a\\xbc\\x10\\x89`=\\xbcO\\x11;\\x07&$=\\x86\\x98,\\xbc\"\nHSET bikes:10070  model 'Phoebe' brand 'Breakout' price 2455 type 'Mountain bikes' material 'aluminium' weight 9.7 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\\"\\x11\\x7f\\xbc\\xbb\\xaf\\t=\\xf2\\xcf\\x94\\xbc\\xf6\\x8e\\x11=\\xfe\\xa8e\\xbc\\xf2$F=\\x93_\\xac\\xbc\\x83H*\\xbc;\\x1f\\xc9=@\\xd7`<\\xbc\\x97\\xd2<@\\xb7\\x1c\\xbc\\xa4\\xe7\\x15=\\x87\\xbf\\x99\\xbc\\x94$\\x03=\\xa5k\\\"\\xbdbp]=\\xe6U7\\xbb\\xf0\\xcf\\xff\\xbb\\xafj\\xaa\\xbd\\xb0\\xc5\\xea\\xbc\\xe6\\x18\\xdc\\xbc\\xe3\\xc3\\x85<\\x08\\xd0\\xb4\\xbd@\\xdc\\x10\\xbdA\\x7f3\\xbc\\x94;\\x8e\\xbc\\xf2L\\xd6\\xbc~\\xc9\\xa1\\xbc3c\\x1d=\\xee\\\\\\x91<Q)\\x89<\\x82\\xb1\\xa4<@9\\x9c<\\xe7\\x1f\\xc7<j\\xa60\\xbcv\\xe9M=\\x94\\xe5\\x0e\\xbd|\\xc0O<\\x9dKZ=&\\x9e\\x17\\xbcC\\x01\\xcb<%\\xa2h<<\\x10\\xee<\\xd3\\xeb@<\\x0f\\xfe\\xa9<\\xff\\x17}=\\xb8\\xe7d\\xbc\\x92u\\x04\\xbdW=\\x1f9\\x85t(\\xbc\\xa7\\x8e\\xcd;\\xf70\\xbf\\xbb\\xa6N?\\xbbb\\x08\\xfc<o`q\\xbc\\x16\\xed\\x84\\xbc\\xcb\\x98\\x8c\\xbd\\xeek\\x88\\xbd\\xc1\\xc8\\x8f=\\x12,*=\\x827T=\\xff%$\\xbb\\xea\\xc4\\x88=\\x0b\\xfc\\x97=\\xb09\\xc5\\xba6v2<q>[;}8\\xa3;\\xca\\x13\\x1f\\xbd\\xc3)/\\xbd\\x808E\\xbc\\xa4.\\x1e\\xbcq\\xef#\\xbd\\xfeo\\\"\\xbd\\xbd\\xa7\\x04\\xbcI&0\\xbc\\xf1\\x94;\\xbdt\\x04E\\xbd\\xf0\\x87\\xeb;\\xaf\\xbdF\\xbcUSo\\xbd^\\xd9P\\xbd\\x1795\\xbd\\xb9\\x00\\xf8\\xbc\\x0cY\\xd9\\xbcp*+<0\\x91\\xde<022=\\xf5\\xd8\\xa8\\xbb\\xb2\\xaa\\x05<\\xb1v\\xfd\\xbcs\\x9d\\xcd\\xbbu\\xc7;\\xbc.\\xacJ<\\xe7y\\t=\\xb0\\xeeV\\xbd\\xb3\\xe9\\xba\\xbc\\xa7\\x90\\xc1<\\xd4\\x02e\\xbd\\xa9\\xde\\xf1:\\xf9\\x93\\xf69>\\xbf\\x80\\xbchl,\\xbdM#\\xb2<;\\x04\\x1a=q\\xe5\\xc1\\xbb\\x99\\xd6\\x86<>\\xf1\\xb5\\xbc\\xed\\xde\\xb8;T\\xcd\\x07<\\xb6\\x93\\xbd=\\xfc\\xe7&<\\x1f[\\x94\\xbc\\x18\\x93\\xf2\\xbc[\\xf6\\x18=\\xb7\\xc2<9u\\xb93\\xbd?\\x0f\\xcf<\\xba\\xbd\\x1d=\\x1f=O<\\xfa\\x98\\xa1<\\xb4$g\\xbd\\xad\\x94\\xb7<\\xbe\\xe1\\x84\\xbc\\xc1w\\xbf\\xbc\\xdcd\\x14=\\x14\\x0b\\xbe\\xbd\\xaan\\xc9\\xbc\\xd7\\x0f\\x1e;\\xca?\\xe0\\xbc\\x1e\\xaf\\xac\\xbc\\x14jM<.w\\xdd<;\\xbc\\x87\\xbd\\xb94$=\\x18aX<\\x91F\\xf3<#\\xbbe<L\\x94\\x04\\xbdp\\xe2D\\xbc\\x85\\xbd\\x9a\\xbd\\x15\\xd7\\xe3\\xbc\\xb2\\xc6\\x9d=\\xe9\\xd7\\x89<\\x9a,/=\\x95\\xf1\\xf5\\xbc\\x82<\\x07\\xbdiSG=;\\x05\\xaa\\xbb\\x94`\\xe7\\xbc9\\x03/=\\xe9\\x87s\\xbd\\x07\\xf4n\\xbcg\\xde*\\xbd0ni\\xbc\\xf4\\x8b\\x16\\xbag@\\x9b\\xbc\\x8a\\xabI=+\\xa2\\x0c\\xbddP!=\\x03\\xe6\\x00\\xbb\\x8d\\x00\\x85\\xbds\\x98\\x8d\\xbd\\n\\xb2`<$R\\x10=\\x90\\xe5\\x86\\xbb\\xf9=\\xc6<\\x0e\\xec\\x00=\\\"\\xcf\\x88<\\xe3\\x8cc\\xba\\xae6\\x11\\xbcN\\xa4\\xeb;\\xa7\\xbf\\x05\\xbdG\\x94\\xfd\\xbc\\xd8\\x932\\xbc\\x94\\xecX\\xbd\\\"\\xa2\\xb2=\\xe0c\\x16;\\xbc\\x1f\\x1d\\xbak\\x8b\\x15<\\x84\\xbfD\\xbb\\xc1\\x7f*=\\xb7\\x89\\\"=\\xbc\\x18\\xe0<\\xd1\\x85a\\xba\\xf2<\\x02=\\xa9\\xcb\\xdc\\xbb\\xa2b\\xa9<\\xb7\\xe1\\x90\\xbd\\x07\\x9e\\x1c\\xbc\\x82\\x94\\xe8<\\x91\\x1b}\\xbd\\xfb\\xce6;\\xed9\\xd7\\xbcH\\xcb@\\xbc\\x0f\\xab\\x1a\\xbd\\xba\\xbct\\xbcc\\x8c\\r;O\\xa4\\xf1<\\t\\xbeL\\xbc`\\xbdS\\xbd\\xf1\\xdc\\x81\\xbd-\\xef\\xac\\xbc\\x7f\\xab\\x9a\\xbc \\xcd\\x1f9.\\xe5\\x1c\\xbd\\xc3\\xd8\\xc0\\xbc&(l=\\x1c\\xec%\\xbd\\x17\\x01\\xa2\\xbcq\\xc9:=\\x04\\x96\\xa0\\xbcy\\xc8\\\"\\xbc>k\\xb6;i\\x8ep\\xbc\\x84\\xf7)\\xbd+\\xd0?=%\\x0c\\xef:\\xa1\\xbf\\xc9\\xbce\\xe8\\x00;\\xca\\x13\\xfe\\xb9\\\",\\x04\\xbd\\xe8\\xdc\\x8e<Fm\\xe1\\xbc\\xf7\\xe4R\\xbc\\x9a\\x04\\x80\\xbc\\x1a|\\xd2\\xbbX,\\x00<p\\xe2\\xb3\\xbc\\xf3@0\\xbdxe1\\xbc\\xe9H\\xa9<\\xd3*\\xcd\\xb9\\xc6\\x1c <\\x9d6\\xb6\\xbc\\xf2T\\xe8\\xbc[<\\x1e=\\x0f\\\"\\x00<\\xa5\\x02\\xa7\\xbc\\xec\\xb8\\xe7\\xbc\\xf4\\xcb\\xe2:(ic=H\\xcfq\\xbc\\x00\\xc1\\\"=\\xd5R\\x1a={\\xbc+=\\xd9\\xcc\\xe8\\xbb\\xbe\\x0e\\xa4\\xbdB\\xe7\\x0b=\\xe9\\x9a\\r=\\x80\\xd3\\x12\\xbd?0O<\\xc6/\\x85=\\xbe\\x9c\\xe7\\xba\\xdcX\\xd3<\\x9e\\xd0\\xf2\\xbc\\xb6\\x10\\xf8<\\xffU\\xf9\\xbc9q.=\\x86N\\xaa<b\\x9c\\xe0<d\\x19)\\xbd\\x84Un<\\xdf\\x04\\xf2\\xbcg\\xca\\xcb=(\\x8e\\xbf<\\x02\\xa1\\x8b\\xbcL\\x9a-\\xbd\\xa9q\\x12<:\\x1f\\xd4\\xbc\\\":\\x95\\xbc4\\xe6\\xc8\\xbc\\xc1i\\x0e9u\\x81u;\\xb9Hy\\xbc\\xf9\\x13\\xce<\\x1d\\xbaS\\xbb\\xb8\\xb0O\\xbc\\xc4x\\xb3\\xbd\\\"\\\"\\xe0;\\x11\\x92{<jh\\xe6<\\xf9Z\\xbc=\\x8b\\xe6\\xb4<0\\x870\\xbd\\xdf0\\x11\\xbc\\x99\\xbf\\x17=\\xb4\\x0bP\\xbd`\\x80_=\\xc2\\x80]<\\x00p\\r\\xbd\\x0f\\xad\\xc4=^4\\xd2\\xbbKoI\\xbd3-W<7\\x8a\\x99<\\x05\\xb8x=$\\x82\\x1a\\xbct\\xdfS\\xbbEt\\x9f\\xbc\\xfe\\x8c\\x93\\xbc\\x0f\\x8c\\xc0=>\\x0e\\xf7\\xbb\\xb3W.=\\xa5\\xa0!=ZbG=\\xfa7\\x0b=\\x13:\\xa1=`\\x14\\x17=}\\x181\\xbcJ?\\x8c\\xbdK\\xd4H\\xbdt\\xff\\xff\\xbc\\x9a\\xdf\\x95<\\x85\\xcf\\x89<kk\\xaa\\xba2\\xcf(=x`\\xc7<\\xba\\x83:\\xbd\\xa0\\xff\\x8b\\xbc\\x9b\\xdd\\xae\\xba\\xf1\\xce\\x96<^\\xa7\\x8c\\xbbg*\\x13=\\x10\\xf33\\xbd\\x14\\x1e\\\"=8\\\"\\xf5\\xbd\\xb0g,<X\\x07\\xb2<\\x1d\\x9d1\\xbc2\\xcaG\\xbdU\\x00\\x0e\\xbd\\x13\\xaea=7\\xbe\\x81\\xbcz\\xfb\\xdf\\xbcl\\x84\\xc8\\xbc\\xecq;=@\\xdd\\xdb<Z\\x95G<v\\xa52\\xbcg\\x9a\\x8c\\xbd\\x04\\x897=\\x04\\x16d;\\xbc\\xb6\\xb0<\\xad\\xefb=\\xc9!\\xc1=y\\xe0\\x84<\\xa7\\xabJ=\\x1e\\xc6\\xd7<\\x04\\xbd\\x89<\\xc0+t\\xbc\\xbb\\xca[=L\\xa7\\x1e\\xbd\\xe5^\\x93\\xbdI,\\x83<g\\xd7$;_\\x1eZ\\xbc\\xb9\\xd5\\xc4\\xbcv@\\xd7\\xbdcF\\\"<::~;\\xed\\x98\\x94<\\x10\\x99\\xff<k\\xa4o=Pl9=:\\x9ew\\xbd\\xc2F\\x04=\\xdf\\x9b\\x1b\\xbd\\xa2\\xa3\\x88\\xbbPvZ=.\\x8dG<0\\xbeP<\\x0b*\\xda\\xbc\\x02\\x9f\\xc9\\xbc\\xa7\\xe2I<n\\xfa\\xa3\\xbc\\xa0\\xca\\x8b\\xbd\\xfd\\xcf\\xc4:\\xbb\\x9a\\x9a\\xbc\\xb586\\xbd6\\xa7\\x05\\xbd\\x02\\xb93\\xbd \\xd7\\x9b\\xbbe)\\x8f\\xbd\\x9en\\x1a=>\\xb5\\xe3\\xbcx&b=B^\\x08\\xbbn\\xb0\\xf0<\\x9bdv\\xbdD\\x1b\\xaf=xC\\x93=2\\xed\\x18\\xbd\\xfe\\x8e\\xae<\\xff\\x8b\\x10<+\\x9f\\x1e\\xb8<\\xc9\\x03<\\x17FO=p`Z\\xbd<\\xe0B<\\x7f\\xfdI\\xbd@\\xa3[\\xbc\\xac\\xe5\\x18=\\xd6!\\x13=w\\x03 <f\\xc3\\xe6<n\\xc1\\xe1\\xbc\\xd3\\xbfU\\xbb^\\xe2\\x82<\\n\\xa2\\x82=\\xe0\\xda0<\\x1f\\xbc\\x07;\\t\\x9b\\xcc<\\xb8\\xb8\\xd3\\xbb\\xff\\x11+\\xbb\\x025\\xdd\\xbbt\\xb5\\xbb\\xbd\\x1d\\x9b\\xe5;\\xe7W\\x11<\\x90\\xa9u;5\\x1d\\\"\\xbd\\xd9U\\xaf<mB\\xde<ZT\\x96\\xbb\\xd0\\xdd\\x11\\xbdd$\\xe3\\xbbdz\\x8f=h\\x8f\\xaf\\xbc.x\\x80\\xbd\\xf6E\\xc1\\xbc\\xa8t\\x8b\\xbb\\x1f\\xce;\\xbd\\x05.Q\\xbd\\xfbO\\xcc\\xbc\\x86\\x931=dE9=\\x8c\\xe5x<\\x1d\\xa2\\xaf\\xbbu[\\xbd<\\x83\\xe20\\xbd,\\x89\\xc2\\xbc:\\x10\\xe7<\\xfez\\x82\\xbd\\x9e\\xc61\\xbbKJj\\xba\\xa3\\x96\\xb5<m_5\\xbc\\xcb\\x7f$\\xbd-\\xb6\\\\=\\xfe\\xeb\\x1f\\xbd\\x81\\x01\\xd0\\xbc]\\xd3\\xcb\\xbc\\xb8+R\\xbd\\nQ\\\"\\xbc\\xa7\\xde\\x19\\xbd\\xe5AQ<U\\xb0c<\\xcd\\x83\\x96\\xbc\\x98y\\xa7<\\x9cZ\\x91=\\xd66\\xa8<\\xfa\\xe5\\x96<)\\xcf\\n\\xbd\\xa1\\xac\\xaf\\xbb\\xb2\\xd9l<\\xf7\\xf8\\x9a\\xbc\\xce@>\\xbc\\\"\\xda\\x10=\\x03\\rM={\\x1f\\x81\\xbd\\xad:\\x0e;.4\\x8a\\xbb\\xd0\\xa7\\xc6<u\\x96\\xdf<B\\xe7\\xa7<\\xd2\\x8cd\\xbda]q\\xbbv\\x86\\x81\\xbc-z\\xbc\\xbb>q\\x06\\xbc\\xc9\\xbd\\xd4<X\\x945\\xbc\\x8d\\xa8:<\\xa5zM\\xbd\\xeb\\x93&\\xbd1&\\xc1<%U\\x87\\xbb\\xab\\x9d\\xb9<;A3<x\\x7f\\x9b\\xbdx\\x8c\\xdc<\\x0c\\xbb\\xca<)]\\xf6\\xbcvk\\x8d\\t\\xfeh\\xdd;\\x08M_\\xbce\\xd7\\x11=\\\"K\\x04<\\x90\\x8b\\x99</\\x9a\\xe1<\\x82B`\\xbb\\x92\\xc2\\x87<\\xb7\\x9d:=E\\xb2\\xd8\\xbb,\\xe9\\x94\\xbcc\\x9e\\x84=\\xa5\\x05\\x9b<C\\x87k=I\\xd4\\x88;\\x1f4D\\xbb\\x85\\x9f\\x81\\xbd9\\x19\\x96\\xbdF\\x18\\x1a;@\\xe9\\x84=\\xe0\\xa7\\xb0<\\xe2\\x97W=&k\\x08\\xbc\\x03\\xf7\\xbf\\xbb\\xe9Z\\x16=J[\\xef\\xbc\\xc3\\xf2\\x11;\\x8b\\x17\\xe7<\\x8f\\xd2\\xe6\\xbc\\x14!\\xde<\\xe9\\xa3t<\\xe6z\\xa4<l\\xd4\\x9d\\xbd\\xb5\\x9f\\x86\\xbd \\xcf7\\xbd4\\xe2U<\\xb8\\xc3\\xb8<g\\x949\\xbb\\x02\\xca\\x99;J\\xfc\\xaa\\xbc\\xeb\\r\\xe0<\\x0e\\xa1a:\\xbd\\x8b\\x85<ue\\x05=\\x0fR\\xee\\xbblg\\xc3\\xbbh\\xbb\\xf7=\\xb0C*\\xbc\\xa8\\x02\\xd0\\xbc\\xc5jz\\xbc\\xd9T1\\xbdp\\xb7c\\xbdS\\x1a\\\\;1~\\xa5<\\x91>\\x9c\\xbc\\xc1\\x85C<\\xae\\x9ct\\xba\\x80&\\x9d:\\xf8\\x8eI=\\x84\\x0f\\x19\\xbdP\\xb1(=s\\xc0\\x19\\xbd.\\xc9\\x96;\\xb1L!=[\\xd5\\x18\\xbd\\xf0\\xfd`\\xbds#\\x11\\xbcTg\\x9b\\xbcq\\xab\\x82<\\xb1\\x90\\xab<\\x94\\xb2\\xa6;juZ\\xbbA\\xeb\\x8d\\xbc\\xb3\\xd4K=\\xdc\\xc1N\\xbc\\xd3D\\x1f\\xbd@JJ\\xbd\\x92e\\xc9=\\x95]\\x81\\xbdW\\xd5B=(\\x06>\\xbd]oJ<\\xaf\\xde\\xea\\xbb0\\x85v<\\x17p\\xc6\\xbc\\xb3\\xaf\\x93\\xbd\\xf4\\x1c==\\x1aJ\\x1d=\\rvL\\xbbb\\xe8H;\\x15RO;E_^<`j\\xb1=\\xc8\\xfb\\xe5:\\xb2W\\xfa<7(m=L5\\xbf\\xbc\\xf5\\xe8\\xbc\\xbcQ\\x0e\\\\\\xbbw\\xd6\\\"\\xbbb\\xfa8\\xbdvNZ\\xbb-\\xb4\\xeb<\\x9b\\x80c\\xbcE\\xfa0=\\x8d<U\\xbd\\xe6\\xb6\\xd9<>\\xa4S\\xbb\\xf0?\\xc2\\xbbW\\xfc4\\xbd\\xbb)\\x0f=\\x83\\xac\\xfc\\xbb\\xe7*\\n\\xbd\\xb5R\\x82<$\\xd2\\x85=\\x86\\xa2\\\\<\\xc3\\xce\\x90\\xbbR\\x15\\n\\xbd\\x8c`\\x94:\\x80\\\"\\x93\\xbd\\xd6_#\\xbb$n\\x06\\xbc\\xde(\\xa5<\\xfab\\x95\\xba\\xdc[<=]ZA<\\x01\\x89n\\xbbi\\x80\\x8a=\\xc6\\xae\\x14=\\xe1E\\x0b\\xbc\\xa0;\\xc3\\xbaG\\x93_\\xbd\\xc5\\x92\\xe5<\\xa7\\xae\\xb5;\\x1c\\x1dY=\\xda\\x91@<\\xa5x*=\\x90#\\x96\\xbd\\xae\\xaeh<\\xbf$\\xe1<8\\xd0\\xfc;\\xa1Z\\xfa\\xbc\\x92\\x9a\\x17\\xbd\\xa9\\xb9\\t\\xbc\\x87U\\x18<\\n\\xba+\\xbc\\xf7\\xee\\x06<\\x85\\x82\\xd7\\xbc\\xeb\\x06R=>{\\x0c\\xbb\\x9a\\xbe\\n\\xbd\\xaeo\\xce<\\xbc\\xbb!\\xbd\\x9a\\xb6\\xcc<\\xe9m8<\\xf8T\\x04=\\\\)\\xdf\\xbc\\xfa\\xe1\\x83<\\x19\\x0c\\xf0<\\\"Z =\\xa9\\xcb\\xe5<e59=!Mm<\\x12\\x1a$;!h\\x7f\\xbcv\\x18~9\\nQ)\\xbd\\xa7\\xf9\\x0b\\xbcD\\x839=@\\xc6[\\xbdyi\\x92\\xbb\\xefS\\xb9\\xbb\\xd3\\x02(=X\\xdc\\xb9:\\xec\\x88*\\xba;ds=\\x83`o=\\x1eq4\\xbb\\x8bJ\\xae<O\\xed\\x9d\\xb9\\xd8\\xa6b\\xbd\\x98\\x13\\x0e\\xbd\\xa9#w=\\xb1\\xf5\\t\\xbd\\xa6\\xbd\\r\\xbd\\xfdW\\x1a\\xbc\\xf4M~\\xbd\\x93<\\xa5\\xbdk\\x17\\xc7<6(\\xc2\\xbb\\xec\\xe6\\xfd<\\xfb}\\x0f=\\xf7\\x897\\xbdd\\xae\\x94\\xbb\\xd1\\x19\\x9c\\xbd\\xb2\\xce\\x99\\xbd\\x94W\\xfb:R\\xc9\\xb5\\xbcf\\xe0\\\"\\xbd\\x8e\\xa7\\xcc;\\xb88(\\xbdJ\\xded\\xbd\\xf8\\x15\\x9c\\xbcy\\x88\\xf1\\xbc\\n\\xbaP\\xbd8-u\\xbatIG\\xbcQC0\\xbc\\x1c\\xef\\x07<\\xe0\\xa7A\\xbd\\xa1\\x1c+<\\xb3\\x9a\\x9f;\\xa8N\\x95<\\xdc/P\\xbb\\xe9D\\x08\\xbdgi8\\xbb\\xf5\\xf3\\xa6<\\xa8 \\xdb\\xbc\\\\\\x1c7\\xbd~(a\\xbd\\xe7\\x03S\\xbb\\x1e\\x0c#\\xbd\\xfa\\xc4~=\\x0cu\\x95=Kg\\xaa=\\xea$\\x16\\xbcr\\xe9h=h\\x97w\\xbdM\\xc9.\\xbd>\\x12\\x13<Dy;\\xbc\\xed0\\x0c\\xbdF6\\x17<\\xcf\\x10\\x04\\xba\\x9c\\xd2\\xfd:\\x9c\\xb1%\\xbd\\xed\\x00a<\\xe8.\\x10=\\xe9D\\x11=\\xcb\\x14\\x14<r\\t\\xfd<\\xd5\\xb9#=>\\x9b\\x91<o\\xf6\\xce\\xbc\\xcf\\xa2.\\xbc\\x7fM=\\xbd\\xbe\\xc6_\\xbc\\xcd\\xfe\\n\\xbd\\x1d\\x1f\\x8e\\xbdg\\x82\\xd7\\xbc\\xed\\x85H<\\xc1}~\\xbc\\xd4\\xe3\\x04\\xbc_v\\xad<\\xf96H\\xbdKr\\xba<\\xab\\xf9)\\xbdq\\xa3\\x82:\\x18\\xe26\\xbd\\x81\\x99D\\xbd\\x83\\xb5\\x0f<\\xc6\\xe5\\xde<\\xddE\\xd4<&&n<\\xd9P\\xba=\\x99\\xdf<\\xbc\\x1a\\xc6\\x1f=\\x86\\xc7\\xc8=(\\xd7\\xdd\\xbc\\xbdK\\\"<m\\xe5\\xd2\\xbb\\xda9\\xbb<O\\x820\\xbd\"\nHSET bikes:10071  model 'Oberon' brand 'Breakout' price 3559 type 'Mountain bikes' material 'carbon' weight 16.1 description 'The new version with 142mm rear, 160mm front travel is longer and slacker than its previous generation, but it’s also a bit taller and steeper than much of its competition.  The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\xe9\\x84S\\xbc\\x15R\\xdb\\xbb-q(\\xbc*e\\xd7<\\x7f\\x08Q\\xbd\\x16\\x02s=N?\\xdb\\xbc\\x1c\\x1c%\\xbd\\x94\\xac\\xa7=i\\xd8\\xd1<\\x0cC\\x8f<?\\xb1\\x99<M\\xc6+=\\x11\\xd7\\x87<\\x04Yt=&\\xa03\\xbd\\xfb\\x16\\x8b=N\\xb4f\\xbd\\xd5\\x08\\x81\\xbd_\\xf0=\\xbd\\xbc\\x1b\\x14<\\xa0,\\xdc\\xbc/B\\\\=\\xf1\\x84=\\xbd\\xbe\\xcc\\x85;\\x00E\\xa0\\xbbM\\xd3\\x96\\xbb\\xd5\\xffI<M\\xae\\xa5\\xbcjb\\\"=\\xa1BU\\xba\\xb6\\xcc\\xdd\\xbc\\x8e\\xb4\\x14=Ke9=\\x07\\x11\\xde\\xbc\\x1d\\x08+<\\r\\xa1\\xa9<v#\\x8e\\xbcq\\xdea\\xbcV|\\xe1<\\x11L\\xf3<\\x03\\x140=\\xee@M=d 6=\\xb2\\x14J;\\x0b\\xda!\\xbd\\xa9\\x8d\\x86=4\\x1c\\xa9<}tC\\xbc\\xf6[\\x1f\\xbc\\xe2\\xd8\\x17\\xbc\\xe1V\\xb4\\xbc\\xf5d\\x89\\xbb\\xc3\\xc0\\xf7;\\x06\\xb3\\x15:\\xe5\\xf4\\x1a\\xbd\\xe8\\xf9\\x9b\\xbc>\\x1c3\\xbd\\xe0\\x14\\x1a\\xbdA\\x94\\xa9=\\x98\\xc6c<S~$=&s\\x99<a\\xe1b=\\xcb^l<3\\x1e\\x93\\xbc\\x9c\\xa6\\xc3<\\xb8n\\xe7<\\xc4-\\xb0<\\xa0\\xb6p\\xbc\\xf1.I<\\x1f\\xd1\\x8e;p\\x9e\\xb1\\xbc\\x05\\x804\\xbd\\x87\\x98\\t\\xbdI\\xde8\\xbd\\xdd\\x0b\\x7f;\\xc9*.\\xbd\\x0c\\xd8R\\xbd\\x0e}\\x17=\\x8c\\\"[<\\xc4\\x85\\x8e\\xbd\\xe5W\\x0c\\xbc\\xdf%\\xc5\\xbcm\\x9a\\xf4\\xbb\\xd6\\xa7\\xfe\\xbc\\xe6\\\\\\xd7\\xbc\\xda\\xc9\\xf7<\\x8ef%=\\xb0\\xef\\xb9\\xba\\xbe?\\xdc\\xbc\\x1b\\xe35<\\x16\\xe7\\xef\\xbc\\xa3O\\x81<\\x9e\\x9c\\x00=2\\x0e\\x0f=\\xc7\\xff\\t\\xbdtk\\x84=j\\xea]<Q.F\\xbd\\xc2,A\\xbd\\x9d\\x17\\xa2<\\xf9\\x0bC<U\\xba\\x8e87\\xb5\\x0f=\\x07v\\xd1=4O\\\"\\xbb\\r\\xf5;=\\xb8\\\"\\x03\\xbc\\xb0\\xd9\\xd6\\xbc\\x11\\xdd-\\xbc\\xec\\x08\\x9f=\\xf6\\x97\\x8b<\\x92\\xe0\\xe4\\xbc \\xb2\\xa7\\xbcM+\\t\\xbd\\xefQ\\x7f<\\x7fJ\\x13<\\xfb\\x9e6=\\xbe\\x9f+\\xbc\\xbc\\xb0\\x8f<G\\x81{;\\xfaVZ\\xbd\\xb4\\xaf\\x0e<\\xa6\\xd6\\x08=\\xdf\\x84\\xed\\xbck\\x86\\xbe\\xbblb\\xb0\\xbc\\x95|\\x82\\xbd\\xe3\\x90\\xc8\\xbc\\x9e\\x7f\\x11;\\xdb\\x92h\\xbc-A\\x1b=\\xa8\\xc1\\x1d\\xbd\\xa6\\xa2\\xe9;\\xa3z:=\\x97x\\xd9\\xbc\\xe1\\xe2\\x9b\\xbc\\xfc\\xb7\\xe8<\\xfaRb\\xbb1;<\\xbd\\xa6\\xf1\\x14\\xbd\\xe8yM\\xbbMn\\x05=\\xc3\\xf2\\x04\\xbd\\x1eXa<\\xec\\xd4c9\\xf6\\xe3\\xad<\\xa0g\\x87\\xbbcg\\x87\\xbc\\x7f(\\xe5;+\\x14;=x\\xbd\\x85\\xbc:x\\x08=\\xc4\\xc6\\x91\\xbc\\x9a\\xa4\\xe1\\xbcx\\x8f\\xa0<\\xd5\\xa1\\x83<\\xbd\\x8b\\x0e=\\x03f[\\xbd\\xad\\t\\x1a=\\t\\xf4\\xd9\\xbc\\xa4\\xd7\\x87\\xba+\\xb1\\xb0\\xbd\\x0e\\\"\\xfc\\xbcA\\xea\\xf8<\\x7f\\xfe1\\xbcU\\xba\\xa1<M\\xe0\\xb3<)\\xaek\\xbc\\\"N\\x06\\xbd\\x8c\\xf2\\x92=\\xeb\\x94\\xfb;Y\\xef\\xd3\\xbcf^s\\xbc\\xb3\\xa8)\\xbb\\xc7}\\x1e\\xbd\\xf5Ox=W\\xbc\\xca\\xbb\\x06QT<\\x9e\\xeb\\xc1\\xbc\\xd9\\x06\\x18;u\\xe9\\x06\\xba~\\x0b\\x81<\\x17\\xfaM=\\xe9 &\\xbc\\x93\\r\\xa6<\\xef\\xf1\\xe9;\\xb0\\x8f\\xb2<\\xd1uP\\xbd\\x9e\\x88\\xa7<\\x92\\xd3B=\\x97\\xf9\\x9b\\xbd8}\\x8e=R\\x98\\xac\\xbb9\\xa0b=\\xc8\\x03\\r\\xbc\\x95a\\x1b\\xbby\\x9e\\x15\\xbb\\xc1\\x1c\\xd5\\xbc\\x9e\\xba\\xd8\\xb9p;\\x82\\xbd\\xcbjy\\xbd\\xc4:\\x9b\\xbc\\x05w\\x80\\xbc\\x9b\\x00\\xf0<\\xd1\\xa8\\x91\\xbc\\xab\\x15\\x8b\\xbc\\x86\\xdc\\x1d<\\xa9e^\\xbd\\xbfS\\xfe:\\xc3\\xc8&=\\xef\\xb77=\\xe8\\xa4\\xf8;\\xf6\\x00X\\xbb\\x0e\\xa2~<\\x92c\\x96\\xbdzn5\\xbd\\xfd\\xae\\x88<:\\xea\\xa5\\xbd\\xd8\\xbe\\xcb\\xbb\\xc0\\xa6\\\\\\xbc-8h\\xbdH\\xb3\\x0c=S\\x97\\x00\\xbd\\xe6\\x9f\\x98<l\\x8f\\x0e=\\xd3<e\\xbc\\x1d\\x16]=\\x9f3!\\xbc\\xc8y\\x0c\\xbdL\\xf6\\x8c\\xbb\\x96n\\xb7<\\xb1==<\\x9d\\xc3\\\";\\xcf\\x12y\\xbc\\xd3\\xa1\\x19<N\\xcc\\xdf<<\\\"\\x1d\\xbc\\xa20/<xq\\x02=\\xaa\\xbc\\x97\\xbbk\\x06\\x80=\\xd5\\xdc(=\\x86J=9\\x1a\\x060=6\\xf0\\\"<)\\xa1\\x84<\\xd1\\xd8\\xe4\\xbc}`(<=\\x9ci\\xbc\\xdc\\xec\\xff;0\\xfb\\x84\\xbb\\xc8:\\x80=\\x1bB\\x1c\\xbc\\xf6\\x85\\xaf=\\x14?\\x1b\\xbcJ\\xc4\\x8b\\xbb\\x9d\\x0c(\\xbd\\xd4\\x1b\\xd2<l\\xa2\\x8c\\xbc\\x07\\xf9^;\\xa0\\xc4\\xd1\\xbc\\x89\\x91\\x14\\xbc\\xe8\\x94c\\xbd\\xd5w\\x93<\\xb9\\xa1\\xe0\\xbb\\xb1U\\xf8\\xbc\\x1b\\x14Q\\xbc\\xa3\\xd5 \\xbc\\xc55\\xe3<<A\\x11\\xbcP\\xe7\\x9e\\xbd\\xafs::c\\xc9\\xea\\xbc\\\"\\x06\\xa2\\xbc\\xceO\\\"\\xbc\\xc9\\xa4o:%\\xfb\\x8d=\\x07`\\x86\\xbd\\xaf\\xb2\\x02\\xbdr\\x0f\\x0e=\\x99\\xa2\\xb4\\xbc\\x9b/f<\\\"\\x14\\xf1;\\xa8\\xc8\\x07=\\x83\\xceN\\xbd\\xf8\\x8a-=\\x8f\\xde\\xa6\\xbd\\xd0\\xc3\\x96;\\xe1Uf;_N\\xae\\xbc6(~=\\xcc\\xbd\\x86\\xbc\\xab\\xae\\xa5\\xbc\\x88\\x0bq\\xba~z\\x0c=\\x96(\\xc5<\\x10\\xfc\\x10=\\xeb\\xa0)\\xbd\\xdf\\x9f9=\\xcf\\xbc;<^\\xcf\\xb2<\\xf1\\xd1N;\\xbc\\r\\x97<\\xe4_\\xbe\\xba\\xad\\x1b\\xd0:&\\xa96<~\\x13\\x11=b0\\xb7<5\\xab\\xda\\xbc\\xb4\\xd0q\\xbdc\\xe5\\xd8\\xbc\\xbcMO\\xbd\\x99Ki;\\xach\\x93\\xbd\\xde@\\xf1:y\\x94\\x9b=\\xed\\xc2\\xe2<\\xd3bK\\xbb\\x85\\xef\\xdf\\xbc\\xd2x6\\xbd\\xb3l\\xbe<\\xd6\\x1b\\x9c<8\\xf0J=\\xa1\\xe2]\\xbdg\\xd3\\xae=\\x83*0\\xbdQ?b<\\xf1\\xc1\\x905\\xf2\\x01\\xb5=\\xc1\\x1b\\xbd\\xbd\\x13K\\x0f<G\\xcd\\xe4\\xbb\\xdc\\x19\\x01=\\xe91;\\xbc\\xc1\\xe1M\\xbd\\x16\\x15\\xb7\\xbc\\xe3>\\x1d=kC\\r<!CL\\xbd)=\\xce\\xbc\\xd1T\\xb9=\\xb0\\xd1\\x16\\xb9\\xd4s:<\\xb0\\xe7\\xea<\\x0e\\xb0\\xa3=\\xb5s\\x85;\\xeb\\tj=\\\\K$=\\x1e\\xf3\\xb5<bP\\xb1\\xbc\\xb1\\x81\\xab=\\xa8\\xd0w\\xba\\x12bF\\xbd\\xd2v\\x1c:j>g\\xbcqw^\\xbd\\x8c\\x18\\t<K\\xa6\\x01\\xbe\\xb2N\\xd9;\\x86:\\x9f;\\x9b\\xceI=\\x97\\x15\\x00=\\x12=\\x19=\\x11\\xfc\\xdb\\xbc\\n\\xe0\\xf7\\xbc?\\x89\\xd0<\\x10\\x98\\x00\\xbdZ\\xb0\\xd8\\xbc\\x9aSo=w\\x8f\\x93;;~\\xfa;P!\\xe4\\xbb\\xa4\\x93K\\xbb\\x08\\xa3\\x8a<\\x1b\\xa3\\xbc\\xbb^\\xf0\\x8a\\xbc\\x96\\xe6\\x8b:\\xc2\\x85\\x19\\xbcs\\x0b\\xdd\\xbc\\xfbD\\xd4\\xbc\\x99PF\\xbcY\\xbe\\t\\xbc\\xfc\\xfd\\xb9\\xbdN\\rR=Y\\xc4\\x8b\\xbc\\x90\\r\\xed<9\\x1bl<\\xb6\\xed\\xa2;8\\x1d\\x92\\xbcwE\\x99=\\tR\\xfe=K\\xb8\\x10\\xbd\\x10\\xa6\\x8c<F\\x8d*<\\xd3\\x80\\xc8\\xbc\\xc0j\\x87\\xbc\\xb9\\xf5N=\\x11J\\x8c\\xbd\\xd6\\xce\\x00;G@\\x01\\xbd\\xe9,P=\\xf7V\\x1b=\\xf0\\xaf\\xf5<w\\xb0C\\xbd\\xb8\\x1d\\xa1\\xbb\\x9f\\x872;W\\xb8\\x91\\xbc\\xc5\\xea\\x03<Bw\\x14=\\x0eG\\xf9;\\x1d\\xec\\xb9<\\xac\\x125=+\\x02#;\\xf4\\xbb^;+\\x9b\\x12=u\\xc7\\x84\\xbdx\\xd9\\x02<L\\xebV\\xbc\\x92\\x17\\x80\\xbcZ\\xcb\\x1d\\xbc\\x16\\x038=\\xc3\\x86x=Z\\xa5\\xa5<W\\xba\\x1f\\xbd\\x949)\\xbds8\\xcb<J;\\x9c;\\x9d\\xad\\xe8\\xbc\\xe4\\x97\\x17;\\x88\\x04\\x8a\\xbbD\\xf5K\\xbd\\xd1.\\x8c\\xbds\\x9d\\xee\\xbc\\xa9\\xee\\xab=\\xfe$f=#\\xb6\\xfc<|\\x80.\\xbd\\xcd\\xaf\\xe3;j\\x02\\xbb\\xbcX\\xf6\\x19\\xbcn\\xc6\\xf2<\\xfa]\\x01\\xbdK\\x8d\\xb4<(Pw<\\xac\\xf1\\xe9<UR\\x8b\\xbc\\xb9\\x9aX\\xbd\\x8c\\xefM=\\xb5~!\\xbd\\x9e\\x98\\x80\\xbd\\x9b\\xe8q\\xba\\xd4T\\x9e\\xbd\\xcd\\xcf\\xa1\\xbbZ\\xb3\\xa7\\xbc\\x8d}\\x14=\\xb8\\xa97=\\xa4b\\x8e\\xbb\\xa9\\xce\\xba;\\x1a\\x80\\x9d<.\\xbb\\xf4<w\\xb0\\xb5</\\xedk\\xbd\\x17%8\\xbb\\xe0R&=\\x88_\\x05=\\x94<k\\xbd\\xf2\\xf1\\xff\\xbb\\xb5 4=[\\x16\\xa0\\xbc\\x99\\xe5\\xfe;a/\\x18\\xbdI<\\x81:\\xd6\\xd0\\xb09\\xd2Xq<H\\x01d\\xbd\\x01%\\x01\\xbd\\xef\\x1c\\xaa\\xbcDf\\x979\\xd3\\xabt<\\x17\\xd2\\xed<\\xd8\\x1f\\xb4\\xbc\\xf6\\xf7\\xfe<\\xf5\\xa8\\xa5\\xbc\\r\\xad\\x93\\xbd\\x9c\\x0f\\xa3\\xbc\\xae\\x80\\xe6;\\x00\\xf4\\xb4;\\xa31&=\\x19\\xbc}\\xbdSP\\x1a=7&\\x07=\\xc7_\\xee\\xbbY\\nT\\tW\\xac.8\\xae \\xde\\xbc\\x1deA=D\\xc8\\x92=(\\xb9A=\\xf5\\x88\\xfa<\\x9a\\xfc\\xb1<\\x88W\\x8f\\xbc\\xf6\\x17a<o\\x97\\r\\xbc\\x88\\xfe\\xaa\\xbcV\\x1by=\\x80\\xf4\\\"\\xbc\\\\NQ=O\\x1a\\xd8\\xbc\\x7f\\xeb\\xc6;\\xc8\\x1b\\xbb\\xbc\\x80\\xed.\\xbd\\xbc\\xac\\x07\\xbcF\\xf34=\\xa6|\\x95<\\\"\\xed\\x10=\\x80\\xebg\\xbc\\x9a\\xaeH\\xbd\\x0c1\\xa3<`\\xdb\\xde\\xbc]?C=:\\xa5\\xd2;\\xf7\\x1c\\x06\\xbdG)\\xf6\\xbb\\xa9I\\\"<\\n\\xa6N<\\x86\\xd1@\\xbd\\x0e\\xd3\\x81\\xbdc\\xbc\\xef\\xbc]\\xe0\\xe8\\xbc\\xa8\\xb5\\x96<\\x10\\x9b1=(!\\x98<p\\xd7\\xca<\\x04\\x01\\xce\\xbb \\x00\\xaa<Hy\\x8b<[\\xcf4=\\x0c\\x0b1=|\\xce\\xc7\\xbb\\x1d<\\xdc=\\x8e;\\xbd\\xbc\\xfd\\x95X\\xbc\\xf5\\xe9\\x07\\xbb\\x16\\xd2\\x02\\xbdH\\xbd\\x85\\xbd0\\xf04<\\x8d0\\xc7<\\xfc\\xf6\\x10;\\x84T\\xec<{\\xf2\\x1c\\xbc%\\xc1\\x97\\xbbE^\\xbc<=\\xa3\\x04=/!\\xb9<\\xa2\\x13\\xaa\\xbc\\xe0\\xdc\\xbf\\xbc~l\\xfe<\\x1f\\x04u\\xbc(\\xca.\\xbd\\xf8\\xbf+=\\x1d\\x1a\\xb9\\xbc\\xea_z\\xb9\\x1f4\\x10=D\\xdd\\xb1\\xbc\\xe8\\x14\\x86\\xbc|\\xfb\\x1c<u\\xd9?=`\\xcd\\xd5<\\x0f\\x02\\x16\\xbd\\x11\\xe7H\\xbc\\xaf\\x87F=e\\x10\\xe3<\\xb3^\\xb8<\\xf4\\xb5_\\xbd\\xa7,\\x9d\\xbb\\xa6\\xe3\\x15\\xbd\\xe1\\xfc\\\\=@\\x14\\x91\\xbd^\\xa8\\x9f\\xbb]b{=\\x00\\xd4\\x13=k\\xe5\\xe2\\xbcN\\xc6M<>=\\x9d76\\x0f\\x11\\xbd\\xe1\\xcd\\x96=\\xb6\\xf1\\xaa9T\\x86)=\\xf3\\\"\\xe5<\\xf8\\x98-\\xbd\\x9f\\xa2\\x17<pfh<~U\\x84=/\\x98\\xcd\\xbc{\\xed\\xa7\\xbbA8\\x84\\xbbgB\\xf4\\xbczK,<C\\xa5\\xac\\xbc\\xf4\\xe8\\x87\\xbc\\x055\\xa7\\xbc\\x1a\\xb0!\\xbc/B\\xb7\\xbb\\xfd\\x88*=\\x87\\xbc\\x8d\\xbd\\\"\\\"\\xc5\\xbc\\xd7\\xf5\\xc0<\\xdd\\xa5\\x88=%\\xfb|\\xbcyh\\xf3<n2\\x19\\xbb\\x0e\\xea\\x92<\\xe6.+\\xbd\\xc1~\\x9b<\\x8a\\x8b\\xd4:\\xc5Z\\xdf<\\xe9C5=\\xec\\xd9F=r\\xeb\\x0c\\xbbV\\x0cj\\xbd\\xb9\\x88V\\xbb\\xc9\\r\\x9e=\\t\\xd8\\xef\\xbcQ\\xa4\\x16\\xbd\\xac\\xafj\\xbd\\xe6A4=^\\xfa\\x08\\xbbl`\\xdd=nZ\\xa9\\xbcb\\x18\\x97=\\x98iI\\xbdn\\xc9k<\\xe1J\\x1b\\xbc\\xc0S\\x96<\\xfb\\xdf\\x8a\\xbd\\x89b\\x18\\xbc\\x87MP<\\x19\\xb3\\x0e=^\\xbc\\xfd\\xbb \\xe8\\x92=C\\x89\\x1c\\xbc\\xde\\xd8\\x1e=\\xbc\\xd7\\xad\\xbb\\x83=\\x8b\\xbd\\x13\\xbd\\x00\\xbc(V\\x80:8A\\x8e<*\\x14\\xad<\\xd3*\\x8d<\\x8a\\x83b\\xbd\\x87\\xb8\\x94:\\xf3k-\\xbd[\\xc0B;\\x06k+\\xbc=\\x01\\x88=\\xd2\\x0f\\xad<\\\"\\xdfK\\xbc-P\\x19\\xbd\\xd1\\xe1\\x00\\xbc -\\x9a\\xbc\\xc0s\\x85=\\xdd@\\x1b=aR\\x1a\\xbd\\\"]\\r;w2\\xa3\\xbc!\\x00\\xc7<\\xc5\\xf0\\x9a\\xbc0\\x86V\\xbc\\x1f\\xbf|=\\x87\\xfa\\x1c\\xbciA\\xdf<\\xbeV\\x94=\\x1c\\x13\\x8c\\xbc\\xafp\\xcf\\xbc\\xa2\\x8d>\\xbdS\\xc6(<\\xd9\\xef\\\"\\xbc\\xe39$\\xbd\\xbd7\\xec;`\\xdc\\x10\\xbd\\xa1t\\xf4\\xbcT6==2\\xd8c\\xbb\\xe4\\x11\\xb1\\xbb&\\xf9==J\\x96\\xf1\\xbc\\xe0\\xa4x\\xbc\\xfb\\x0c\\xb3\\xbc\\xb6\\x8a=\\xbd\\x05SH\\xbd%\\x01\\xff\\xbc\\x8d,\\xfd\\xba\\xc8\\x972\\xbdi\\x17\\r\\xbd\\x95%\\x04\\xbd\\x95\\xcf\\x91\\xbc\\xb4L\\x8f\\xbd!\\x1e\\x01\\xbd\\xd9\\x9b\\x97\\xbc\\xa6\\xed\\xa4:a\\xe4\\x97\\xbc\\x9e)\\x13\\xbd\\xb2+C\\xbd\\xf7pD<\\xac\\x06\\xd7\\xbc,\\x8c\\xa6<\\xc6\\x9d\\xa7\\xbc\\x1b \\xda\\xbb\\xc8\\xb0\\x1e\\xbc\\x15\\xbd\\x17<\\xfb\\xd6\\xed\\xbc%\\xa5\\xab\\xbdF\\xda\\xac\\xbdZ%\\xf2\\xbc\\x8f\\xca\\xd0\\xbb\\xd2\\xa6\\x18=\\xa7\\xb0\\xe4<\\x96\\x89\\x9b=\\xde\\x12\\x1d\\xbd\\x08T]=\\x11\\x81&\\xbdp-\\xc3\\xbd\\xbdQ0\\xbak\\x18\\x14\\xbc\\t\\xbc\\xb1<\\x85\\xf7\\xa1\\xbcv\\x1a\\x0c\\xbc\\xa1\\xa5\\x97\\xbc\\xe3\\x9e?\\xbc(\\x18.\\xbd\\xba\\xf7\\xe9\\xbb\\x897\\xcd\\xbc[\\xd9\\x8d<\\x88\\xd9\\x0b=\\xc9\\xfd\\n=(\\x8e\\xe4\\xba~\\x9a!=\\xe0\\xb0\\x0f\\xbc\\\"\\xf4\\x16<\\xf4XC\\xbd\\x15@\\xd7<\\xd4\\x15D\\xbd2\\x99l9\\x9e\\xd3\\x9e<\\xd2\\xfa\\x93\\xbb\\x96\\x07\\x89\\xbc0K\\x9c<\\xfd\\xba\\xdd\\xbc\\xb2\\xc0\\xec<-\\xc7\\x8e\\xbc-\\r\\xe1\\xbbl\\xbc2\\xbd\\x8ekM\\xbd1-6=\\xa7\\x97\\x88<\\x99\\xf4$\\xbd\\xd2^3=\\x90\\x1f\\xb7=\\xac\\xfb7\\xbb\\xcd\\xa8#=K\\xd6\\x94=K\\x14\\xea\\xbc\\xed\\x93\\x99=\\xddL\\xd6\\xbd\\x0f{a<\\x82r\\xd7<\"\nHSET bikes:10072  model 'Makemake' brand 'Eva' price 1459 type 'Road bikes' material 'alloy' weight 7.3 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"*6c<\\xbc\\xae\\x06\\xbd\\xfd\\xfa\\x01\\xbd\\x08l7\\xbc\\xbf ]\\xbd\\x82\\x89K\\xbb\\xca\\xb2\\x06\\xbd\\xb1\\xfa9\\xbd\\xa3\\x8dp=LDS<\\\"7\\x1e\\xbbj\\xdf,\\xbc\\xd4]\\xe2<=\\xa8O<\\x00\\xc3S=\\xf6Z\\x8a\\xbcJ\\x91!=\\xd2\\xbd\\r\\xbd\\xa3\\xdf\\x82\\xbc\\xf0\\xa7f\\xbdJa\\x9d\\xba&\\x82\\xdd;D6d<\\xcal\\x03\\xbd0%\\x85\\xbc\\xdf\\x7f\\xc6;\\xd9\\xecX<\\x10\\xf8\\xe0\\xbc\\x82\\x13\\x05\\xb9g\\x0c\\x8b=\\xec\\x8ap<\\xabn\\xfe\\xbc2\\x8d\\x85<\\xef\\xf7\\x04=0+\\xcc\\xbc\\xf6Q\\xca;\\xeae/=O\\x02\\x16\\xb9\\xe0\\x9f\\xc0\\xbc\\x97&M<\\xe7\\x04\\x8f=\\x90M\\x98\\xbb\\xd0\\x9d\\xe0\\xbc\\xe08\\xf8<EO`\\xbc\\xb9\\x14\\x19\\xbd\\x1f\\x14\\x82=\\xa3Cm\\xb9\\xed\\xca\\x01\\xba\\xf3`\\x97\\xbb\\x97\\xce\\x91;\\xef?\\xf7\\xbc\\xa4\\xa5\\\"\\xbd\\xc5GT\\xbc\\xfd\\x03\\xc9\\xbc\\xf9\\xeb)\\xbd\\x15\\x00\\xfb9L\\xca\\x8d\\xbd\\xee\\xeea\\xbd \\xb1\\xd1=XH)\\xbbH\\xa0\\r=Ea\\xa1\\xbcU\\xe1\\x0b=\\x7f9\\x0e=\\xea\\xa4\\x1e\\xbd\\x80\\x1b\\x1f=\\xbe+\\xb6<\\xce~\\xf3<\\xcd\\xce\\x1d\\xbdGA9\\xbc7\\xaa\\xc4\\xbc\\x06&m\\xbc\\x80\\xe6\\x86\\xbd\\xdc\\xd1\\xe5\\xbb%\\x98\\x89\\xbc\\xb4v\\x02\\xbdV\\xc5\\x90\\xbd9JC\\xbd\\xc9\\x19\\xaf=\\xabe\\x8f<pn\\xf4\\xbc\\xbf\\xbd\\xa6\\xbc\\x0b\\xee\\x8c\\xbc\\xea\\xc2\\x86\\xbb\\\"\\n\\x99\\xbc(\\x89\\x98\\xbc\\x02\\xd0\\x02=* \\xa7\\xbc\\xdd0\\xc8<\\x19\\xc2\\xd3\\xbaE\\x00\\x8a<3I\\xc9\\xb9J\\x9es<\\xd8\\xfaL=\\x98[\\x13=\\xe5\\xaa\\x1f\\xbd\\xf8\\x06\\x07=\\x1a\\x07C<\\xcd\\xa7\\x86\\xbd\\\"r\\x85\\xbdVP\\xd5<\\x1c1U\\xbc\\xdbo\\x82\\xba\\x9b\\\"\\x8f<\\xe2V5>\\xd5\\x88\\xb7;\\xc9\\x11\\xf8<u\\x04\\\"\\xbd\\xd3\\x97\\xa6\\xbcj\\xe4\\xf5\\xbc\\x07\\n\\x89=\\x04\\xdf\\x17=O%T\\xbc~\\xdbF\\xbd\\x96\\xa6\\x88\\xbdAi5\\xbcV\\xdb\\xa2<&\\x8a\\r<\\x95\\xcf\\xc4<H\\xd4}=\\xc6\\x07O\\xbbH\\xf0Z\\xbd\\xe6x\\xa7<\\xc1y0\\xbaV\\x90\\xe9\\xbcw!\\x99<\\xfb}\\xd4\\xbc\\xef&5\\xbd\\xd9\\xf8\\n\\xbd\\x02K\\x13\\xbd5\\xec\\x9b\\xbc!N6;\\xaf\\x8fS\\xbd\\xaf\\xe2\\xa4<!{V=\\x97\\x92\\xc7\\xbc\\xc5y\\xae\\xbcN\\x88\\x15=\\x8f\\x88\\xd1<35\\t\\xbd;\\xf4\\xac\\xbd\\xb60\\xa1\\xbc\\x84\\xd4\\xd7<\\xa0q\\xc2<e@\\xe2<B\\x83\\x8d\\xbb\\xbd\\x98/<\\x89?\\x8a\\xbc\\xa3\\x00(\\xbc\\xd76\\xe8;\\xb8\\x18l<\\xd3B\\t\\xbbQ{\\xfc<e{\\x03\\xbd\\xa1y\\xcb\\xbc6\\xe2,;6\\xd7W<w\\xa39=\\xb3yY\\xbd,O\\\\=\\x16\\x96Z;\\xdc\\x80\\xa0<h\\x7f\\x02\\xbd\\xae\\xaf(=\\xe15\\xf7<9y\\xef;\\xb5\\x10\\xca<\\x13\\xef\\x1b=\\xc9w\\xc6\\xbc}\\x88E\\xbc\\xa3\\xa6d=\\x8c+\\xc7\\xbc\\x8d\\x8f\\xa5\\xbd\\xb7Y\\t\\xbbO:\\xae<\\xc8\\xc3Y\\xbd\\xc9\\xb7\\xa8=LB8\\xbct\\x07\\xeb\\xbbW\\x8d\\xb4\\xbc)\\x9dc\\xbcM;8\\xb9\\x9a\\xd9\\x01=%\\xb8\\x11=\\xa3$\\x04<\\xb4\\x17\\xf7<\\x1d\\xc7\\\"<\\xd9m5=\\xf07\\xf6\\xbc\\xc8\\xd1\\xdc<\\\\;\\xe8<[\\xd0\\xa7\\xbd\\x05?\\x9d=\\x03\\xed==\\xd4D\\x87=[2l<b\\xce\\xdd;5\\xa7\\x17\\xb9\\x8c\\xafz9f(\\xd4;E\\x9e\\x7f\\xbd\\xe5\\xa0\\x88\\xbd\\xbd\\xc9O\\xbcH\\xff\\xc4\\xbb\\xcb\\xbfW=\\xe8q|\\xbd\\x90\\xc3\\x08\\xbd\\xea\\t\\x0b=\\xeb\\x19\\xdf\\xbb&_\\x18\\xbd\\xa5\\x1a$=\\xd8\\xa5p=Z\\xe9\\xdc<\\xb2\\x9c\\xb5<\\xd0t\\xed<IV\\xb8\\xbd\\xf9\\x81\\x91\\xbb\\xd7p\\x92\\xbc\\xe0\\x90&\\xbd(\\xc8\\xc2;\\xd8\\xdb\\xb8\\xbc\\xf65X\\xbd\\xa5\\xa9\\xfb<x\\xf6\\x87\\xbcO\\xa8G<\\xf3\\xb5\\x0b=\\xf9\\xa6j;s\\xb0\\n=\\xb3+\\xc1\\xbc\\x93\\xab\\r\\xbdq\\xd6\\x19\\xbc\\x8a`-=\\xf7\\\"\\xab<\\x03\\xf4r\\xbc\\xc4\\xf7\\x04<\\xb6Y\\x89\\xbc\\xbe\\r6=\\xde\\xb8\\r\\xbc\\xe3[=\\xbcj00=_\\xa5\\xfd;\\xe8\\x91M=\\xfc\\xad\\xa2<7\\\"\\xcd<6\\x03G=\\x16h\\x93;\\xf1f\\xa4\\xbc2z\\\\\\xbc\\xa8t\\x87\\xbc\\x13\\xca\\xc9:\\xd7\\x87\\xa5\\xbcG\\xe3*\\xbbS\\x9d\\xfc<\\xfa\\xed\\xfd\\xbc.\\x1f\\xee=\\xb9\\xb9\\xdf\\xbc\\xf6Y9\\xbb3\\x9f\\x96<@;\\xba=\\x80\\xce\\x8e\\xbca\\xd1:\\xbc0\\xdc\\x8d\\xbc\\xf2\\x91\\xcb\\xbc\\xcb|J\\xbdHU\\xd7<\\xee\\xdbY\\xbc\\xcaw\\x12\\xbc\\xabL\\x9c;\\xc6}.\\xbc\\xfc\\x91\\xc2\\xbb9#*\\xbdZ\\xe1\\xeb\\xbc#\\x90A\\xbc\\xccZ\\xf0\\xbcx4\\xf8\\xbaD\\xee\\x07=\\x87\\xc7\\xec;\\xf47\\x01>\\xab[>\\xbd\\xafOM\\xbc\\x04\\xddO\\xbd\\xf1\\x87#=a\\xa0R\\xbcT\\x8e\\xdc<\\n\\xf5c<\\xed`@\\xbdX\\xced=\\x82\\x8f\\x08\\xbdu\\x08\\x84<\\xb1@V;\\x94\\xcdq\\xbb>\\x13N=\\x85\\x1b\\xb4\\xbc\\x1e\\xb2\\xaf<\\xb0[]\\xbd\\x19\\xd1\\xcb<M\\x9e\\\"=s\\xc9\\xd6;\\xab/\\x00\\xbd\\xcc\\x9f@=\\xae\\x81\\x96\\xbcze\\x83<\\xa0T\\xa5\\xbb\\x17L\\xc3<\\x99\\xb9\\xe6\\xbc\\x94_\\xf1\\xbc\\x0e\\xc7\\xa3<\\x8bM)=c_\\xb4<\\xf5YG\\xbci\\xfd\\xc2\\xbcW\\x19\\x03\\xbdi\\x97\\x10\\xbb\\xe0\\xad\\xb4<\\xc4\\xe7\\x97\\xbd\\x8e\\xd4\\xfb\\xbc\\x86S\\xae=(z(;\\x83f\\x93\\xbc|\\xd9\\x14\\xbd)\\xe7\\x89<\\xfa\\x06r<\\x91\\xe8\\x7f<\\x9f\\xd8\\x17=\\xbawh\\xbd\\x8bh\\x0c<\\xd15\\x0c\\xbd`\\x08\\x86=\\x81s_<\\xcc\\xe7\\x89=\\xb5:L\\xbd\\xd5\\xc0\\x95;(hQ<F\\xef\\xd9<\\xbbL{\\xbc\\x92\\x14\\x17\\xbd\\xf0{\\xc6\\xb7\\xfa\\xab\\x16=Y\\x85\\xbb<-\\xf9\\x8c\\xbd\\x16\\xa4\\xba<\\xd3L\\xb8=\\x8dF\\xac;\\xb2\\x85g\\xbc(\\x8b\\xdb<\\xd9v`=:\\x81\\xdd:\\xd1;y=\\x8fR~;\\x93w\\x02<\\xd2*\\x80<\\xec\\xd4\\x82=M\\xa5\\x01\\xbd\\xd6ws\\xbdd+\\xaa\\xba+J\\r\\xbd\\xd2\\xeaR\\xbdBOr:m\\x92\\xb3\\xbdY\\x87\\x15=\\x03\\xbd\\xb0<\\xf4\\xd9\\xcd<\\x91]\\xe1<\\xde%\\x0e=\\xaf\\x1c\\xf0\\xbcT\\xabW\\xbdO\\x9a\\x05=\\xbd\\x81\\x8d\\xbd8\\xe0\\x06\\xbd\\\"\\xd92=\\xd1!s<\\x06\\x125<\\x9e\\xb9\\xf3<\\x1e\\nB<8\\xb9z<\\x18\\xb3G\\xbb8\\xea\\xfa\\xbb=\\xf3O=\\xdbo\\x82\\xbc\\x16\\xbf\\xca\\xbc\\x1f\\x83\\xa8\\xbc\\xf6\\xb7\\x85\\xbc\\x8a\\x8a\\xcd:k\\xc5\\xd5\\xbc/$@=\\xa0\\xad\\x83\\xbb\\xe4m\\xcd<\\x9c\\xa0==\\xa3\\xbeD<]\\xdc\\x1a\\xbc\\xf5\\xb5\\x83==\\xb3\\xd8=\\x9d\\x16\\x1f\\xbd\\x0fU\\xdf<\\xc6\\x11)\\xbd\\x0cdT\\xbc\\x0f\\xff\\xed:\\x96\\x08\\x98=\\r\\xd4\\xc9\\xbd\\xe9fj\\xbb\\x01(%\\xbd8\\xaf8=\\xc7\\x1c\\x12<\\xf4\\x1eH=F\\xfal\\xbd\\xff\\xd7\\\"\\xbcT3g;&hP\\xbc\\xdci\\x87\\xbckjS=\\x81\\xf1\\x07=\\x12\\xb7\\x96<\\x8bS%\\xba3\\xce ;\\x9f \\x84;\\xcf\\xb74\\xb6\\xeb\\xf69\\xbd \\xffk\\xbc\\x1e`W\\xbc\\xdb\\xcc0<\\xbeg\\xd4\\xbcC\\x8dP=\\x8fu\\x9e=\\x1b\\xe64<-\\xd2\\x8c\\xbdm\\r\\x8a\\xbc\\x01}\\x89\\xbb\\xd3\\xf9\\xaf;\\xb7.O\\xbd\\xed\\xc0\\x15=\\xbeoi<z\\x19\\x7f\\xbd\\xa6\\xaa\\x92\\xbd\\xb8\\x94\\xe3\\xbc\\x0b\\xda\\xa6=)\\x00A=\\xba\\xa2\\xc1<o+j\\xbdf\\r\\x8b<\\xdd\\xedw\\xbc\\xde!\\xe9\\xbb\\x03b\\xb5<\\x94S\\x87\\xbc\\x93Y\\xc4;\\x97\\xc9e\\xbc\\xb2\\x94M=;\\xd5\\xa0\\xbcZ\\xad\\x88\\xbd\\xfe\\xa4A=\\xb5\\x93\\\\\\xbd\\x9dN\\x1c\\xbd\\xb8U\\x04\\xbc\\xefW\\xf7\\xbdf\\xcd\\xb3<\\x01\\x15\\xaf\\xbc\\xef&&<\\xe3`\\x1a=\\xb9\\x13\\x89\\xbcw\\xa1\\xaf<\\xa9\\x94\\x92<\\x11]7=\\xa5>(\\xbc\\x19\\xcez\\xbd>X\\x8f\\xbba\\x04\\\"=-\\xfc:<\\x98O\\x12\\xbdC\\x8a\\xa0\\xbc\\xda_`=Vl$\\xbd\\xd0\\xfc\\xa2\\xba\\x1d\\xb6\\x12\\xbd$\\xdd\\x1a\\xbd\\xe2\\xe4^;\\xe3\\x8bM<@\\xefZ\\xbd\\xbd\\x1b&\\xbd\\xb0\\xaa*=\\xe7x\\x88\\xbc\\xc0\\xec\\\"<9\\x8f=\\xbc2\\x97\\xff\\xbbd#\\xac<%\\xcb\\xcd\\xbb\\xcd\\x1c\\xdf\\xbc\\xf8\\xbb\\x97<\\xfb],=WOT<x\\xcc;=[\\x11\\xb1\\xbd\\xe1\\xee\\n;\\x81\\x92\\xec;Z\\xce\\x95<\\xd1\\xd4\\x83\\t\\x87\\xcf\\xb6\\xbc\\x8a\\xd3\\xff\\xbb\\xafh\\xde\\xbc\\xb8wn=\\xc9\\xf2(=\\nH\\xf0<\\xf2\\xa2\\x07\\xbc\\xcc\\x0f\\xe6\\xbc\\x05\\x0b\\x13<\\\"\\xe1J\\xbc&\\t\\xf0\\xbc\\x04\\xeb2<\\x1e6\\xe2;]\\x93\\x88<R\\x98\\x0b\\xbc\\xed\\xa8\\xae\\xba\\xc4\\xcd2<Z\\x96C\\xbd\\xd0H\\x88<\\xf1\\xe9N=\\x92\\x0b\\x90<\\xe1\\x08H<)\\xb0\\xcc;\\xbe\\x1b;\\xbd-r\\x1f;Y\\x8d\\xb9\\xbb\\x93F\\x0e=\\xb5vq\\xbcJ\\x12\\xf4\\xbcwe\\x05\\xbd\\xa5\\xe0\\xca<\\xd4A\\xba<\\xa8\\xee;\\xbd Xn\\xbdj\\xc8\\x9d\\xbccF\\x86\\xbcPM@;w\\\"L=\\x1c\\n\\xe4\\xb9\\xe6\\x0c\\x17=\\x0by\\x83\\xbb!q\\x0f\\xbd\\xbeb\\xd4<\\xf6\\xbbL= \\xe75=H\\x1a\\x9a\\xbdV0J=Tg\\xe1\\xbcc\\x9b*\\xbdU\\x8d6<\\nY\\xc3\\xbc\\x1dY\\xf1\\xbc\\xfd}\\xa4;\\xe1\\x7f\\xfa<\\xbf\\xa6\\xac\\xbc\\x11O|<:|\\xac\\xbc\\xe1\\x9e\\x9e\\xbc\\x0b6\\xf4<\\x06\\x14\\x1e=x\\xa4\\x82<y\\\"\\x1e\\xbc\\x9b\\x154\\xbc\\xec\\x84\\x1d<\\x05;\\xe9\\xbbF]0\\xbd\\xaeP\\xe6;\\xd4\\xd1:\\xbc\\xa7\\x8e\\xbc<\\x16\\xff\\xe6<|)\\xd2\\xbcN\\x1d\\xa1\\xbb\\x93\\xee2\\xbc\\xd2\\x85\\xe6<\\x9e\\xc9\\x05=\\x84\\xb60\\xbd\\x19\\xb1\\xa0;:q\\xe8<\\xcb\\xe7K=C\\xe5\\xf5<8=\\x0e\\xbd\\x0b\\x00\\xda\\xbb|)\\x1d\\xbd\\xa7\\xb41=^x`\\xbd7/\\xbe\\xbb\\xb6\\xb8\\x90=\\x8d\\x18\\x04=\\xf7\\x19\\x08\\xbb64\\xd5<Mb\\xc2\\xbc\\x9b\\x85\\x9a\\xbdk9p=\\x9dd\\x13;\\xcd}\\x15:\\x0b\\x96\\x0f<%\\xeb\\xd3\\xbc=\\xff\\x1a<\\xff\\xa1\\xb9<\\x12JR=A\\x89\\x91;\\x9f\\xb3Z;\\xbbB}\\xb9ug\\x8d\\xbcV\\xaa\\t\\xbd6v\\xbf\\xbc<Z\\xe0<\\n\\xa2\\xb4\\xbcW\\xb5\\xfc\\xbc\\xf3\\x0b\\x85\\xbb\\x12\\x87:=\\xd0s\\x05\\xbd\\x19\\xd1\\x97\\xbc>^\\xd1<!l\\x8d=R\\x83d\\xbc\\xd2\\xb9?<\\\"q\\xad<\\xda\\xbfD<\\x04\\rs\\xbc\\x0e\\x0e\\xa1<\\xb6=\\x02\\xbbR\\xfb\\x01=\\x06\\xd5\\x80=\\xec\\x8e\\x1b=W\\r\\x80;\\x86dQ\\xbdm\\x87\\xd2<\\xae\\\\p=#O\\x0f\\xbc.\\x1a\\xcf\\xbb\\xa4\\x85\\x82\\xbd\\xa2\\xca\\x0c=\\x9fQ\\x81\\xbc4\\xf8\\x91=\\x1d\\x8b\\x04\\xbd{\\xe6\\x15=\\x1a\\x11\\x91\\xbd^\\xecV<\\xf7\\x95\\x89\\xbbx\\tz;\\xa4\\xb2\\xe8\\xbc\\x1c<\\xa7\\xbc@{\\x9d\\xba\\xd0\\xa0\\x1c<\\xc2\\x1es:2\\x0e\\xb1=\\xb6\\x16\\xea\\xbc\\x0f\\x03\\n=\\xa0~C\\xbd\\xda\\xb13\\xbd!k\\x93<\\xd1\\x9c\\xcc\\xbc!\\\\\\n=\\xedb\\x9f\\xbb\\xe3\\xb5s\\xbc!\\xda)\\xbd%\\\"\\x0e<4\\xb0:<%i\\xe5<\\xd7\\xcdP\\xbc\\xd2\\xe6\\x9c=\\xf2\\xbc\\xac\\xbb\\xe6\\xb1\\x98<[\\xa5\\xd9\\xbbL\\xf8\\x04\\xbd\\xf1p\\xe8\\xbc\\\\J\\xff<e\\xf29=\\xc6u\\xb0;\\xfc\\xa3Q<j\\x85\\xe7\\xbc\\xe9V\\x0b<F\\x10@\\xbc~+\\x84<H\\xbf\\xef<R\\xf9v<\\xc7^\\xfc<B\\x03\\x9a<\\xf1\\x04H\\xbd\\x13 \\xe5\\xbc\\xb2g\\xaf\\xbd\\x9b\\\"\\x11=@\\x93\\xdc<\\x81\\x18|\\xbc<\\xf3\\xd8\\xb8\\x99w\\xa7\\xbc@\\x13\\x9b;y~\\xdd\\xbb\\x0cI\\xe0;u\\xbc\\x07\\xbc\\xb5\\xe9\\xec<\\xbf\\xb86\\xbd\\\"]#\\xbd\\xb4\\xc4;\\xbd:\\x04\\xa5\\xbd<\\x96.\\xbcG\\x05\\x0e\\xbd2\\x1d\\xfd\\xbc\\xe85\\x01\\xbd`\\xc72\\xbcD\\x84\\x12\\xbd\\xe8\\x0e\\x92\\xba\\xc4\\x97\\x91\\xbd\\xfb\\xc9\\xe2\\xbb\\xea\\xb4O\\xbd\\xa30\\x83<U\\xad\\xc3\\xbcK\\xed\\x95\\xbb\\xe8\\x1f\\xbb\\xbb\\x92\\xf0\\xf2<\\xde\\x84\\x8a\\xbc\\xf4F\\x1c\\xbc\\xd8\\x1b\\\"\\xbc\\xcc\\x8b\\xea\\xbb\\xe4\\x1d/\\xbch\\x90\\x0b<\\xe2\\xe8\\x14\\xbd0>A\\xbd\\xda\\xd5\\xb9\\xbd\\x821\\xd5\\xbc\\x95\\xf3\\xfa\\xbb{\\x8a\\x8b;\\x97\\xdb;<M\\xbd+=H\\xdeT\\xbd#\\x8c\\x1a=\\xef\\x89\\x8d\\xbd+\\x12\\x93\\xbd\\x0c\\x853=\\x98\\x03\\x87\\xbc\\xee\\xf2\\xb0\\xbcZa\\xe8\\xbcH\\xdd\\x999\\x1dt\\xd1<\\xf3V\\xf7\\xbc\\xfbm\\x00\\xbc\\xd6\\x964:\\xc8\\xbe\\xdf<\\x98)U;\\xb8\\x7f}<\\xf2s0=i`T<\\xc8\\x10\\x84=\\xabf\\x96\\xbbY\\xf4\\x9c\\xbb\\xf5\\xc3\\x93\\xbd\\xa4\\xee\\x86=T>\\x91\\xbd\\x8e\\x97\\xa6<\\x17\\xc9\\x99<\\x9ar\\xc0\\xbc\\x8d\\x9e0\\xbc\\xae\\xfc\\x8d=0\\xedO\\xbd\\xa0H\\xf3\\xbb\\xf6\\x04#\\xbdog\\xc8<\\xcfF\\x9f\\xbc\\x9c}C\\xbd\\xaa\\xd0\\x8f<c\\xbf\\xa8\\xbb^x\\xe9\\xbc:\\x86t=5W\\xbf=\\xea\\xd3\\xef;\\xb8\\xaaO=\\xa7\\x8c:=T\\xffu:\\r\\x97_=?\\xb6\\x8a\\xbd.Lg\\xbc\\xbfj4<\"\nHSET bikes:10073  model 'Saturn' brand 'BikeShind' price 1109 type 'Enduro bikes' material 'aluminium' weight 13.9 description 'This bike descends with authority, the revised FSR suspension platform is perfection and it eats everything in its path. Try it uphill and it climbs pretty darn well with a supportive pedaling platform and a steep seat tube angle. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xd3\\xbd\\x18<b\\x8a\\xff<N\\x1d\\x06\\xbaP\\xee\\x97<M\\xf8\\x11<i\\xd2\\xec<\\x1ak\\x11\\xbc~\\\"F\\xbdG\\x12\\xbd=B\\xc9\\t=K\\x9a\\xde;\\xbf\\xa24<,E\\x03=\\xf9\\xe5\\x1f=\\xbam<=xh\\xcd\\xbc\\x08\\xb4\\xc6\\xba[\\xaaj\\xbc\\x94\\xac\\x0b=\\xe0R\\xce\\xbc=\\xd1\\xfb:C\\xa5\\xbd\\xbc\\xa7\\xdd\\xd3\\xbb\\x96\\x8c1\\xbd\\xf7O\\x10\\xbc\\x89M\\xaf<\\xd4\\xb3#=_\\xfe\\xb6;h\\xd7\\x8c\\xbd\\xe1\\x0eY=q\\x025\\xbd\\x89\\xbc\\x9c:_\\x94N;W\\xa6\\x07=\\t\\xfc\\xb3\\xbb\\x80\\x0f\\x03<\\xef\\x92\\xa8\\xbb\\x1b\\x1b\\x03\\xbd\\xdd\\x16q\\xbc\\x9e\\xb1]<A$\\xf7<\\x8a\\xfec:\\x8e\\x8fr=\\x85\\x13\\x06\\xb8\\x11k*<\\xfa\\xf24\\xbd\\xd4\\xc1\\xa6=O\\x95\\x12\\xbd!\\x08<\\xbd \\x14u\\xbb\\x0f\\xc2\\xfa\\xba\\xc0E\\x17\\xbd\\\"H0\\xbb\\xbcX\\x11<W\\x84\\xda\\xbbY\\xcd\\xea\\xbb\\xf0:\\xc5\\xbc\\x84?^\\xbd\\x140w\\xbd\\xcb\\xa5\\xb4=\\r\\x02J=\\xa50\\xe7<O\\x7f\\x9d<\\x03aG=\\xe15\\x00=\\xd4u\\xd88\\x87\\xc2\\xd0\\xbc\\xda\\xb3i=S\\xf0\\\"\\xbc\\xcf\\xfc\\xc8<r\\x11l\\xbb\\xaaz\\x02\\xbc\\x9a\\xc0\\x07\\xbd\\xd57n;^\\xeb\\xc0\\xbb\\n)\\xb4\\xbci\\xd5\\xa9;\\xf7\\xbd|\\xbdQ^N\\xbd\\xeb\\x90<:\\xd6\\xfd+=\\xc99+\\xbd\\x96\\x17\\xf0\\xbc\\xd4\\xabg\\xbd\\xd17!\\xbd+%z\\xbd\\xdb\\xc0\\x1e\\xbcQ\\xe9N<\\x8b\\x8d&=\\x9e\\xb6\\x9c;\\xc5\\x89\\xe3\\xbc\\xd6\\xa4\\x0b\\xbd=P\\x05\\xbd\\xee.;<\\xa3v\\x8b<\\xe0\\xb8\\xdc<=H\\xce\\xbc\\xf5\\\"!<\\xc7\\x02\\x99:\\xb7J\\x05\\xbd\\x97\\xd6q\\xbcH/\\x0e<\\xb1\\xd8\\x83<\\xcf\\xff\\x89\\xbd,\\x87\\x93;\\\"\\xef\\xb6=\\x1f\\xae\\xeb<GT\\xb0<\\t\\xd2\\xbb\\xbc\\x11%\\x97<F\\x0ee\\xbc)\\x1b\\x18=7\\x1a7=\\x05dO\\xbd\\xa2\\x1d#\\xbd\\xfd\\xd5\\xda\\xbc\\x87\\x00\\x16\\xbb\\xab\\x95\\xc2\\xbc\\x9fy\\xa0<\\x18i\\x0f=i\\xe2H\\xbb\\xc6a\\xb2;\\xeb\\x9e\\xa0:\\x9b+\\xa9\\xbc\\xeb\\x80\\xb2<\\x025T\\xbaL\\xe5%=P<s\\xbd}y/\\xbd;\\xe0\\x10\\xbdt\\x9a\\xa5\\xbb\\x82\\x1a\\x9f\\xbb\\xb6\\xd4%=\\xa1\\x94\\xcb\\xbc\\\"\\xa9\\xad<c#m=\\xc9\\xd9\\x16\\xbdJ\\x8e\\x0c:\\xe1n\\xe6\\xbb\\xf4~Z\\xbcRa\\xf2\\xbc~\\x89\\x18\\xbdX\\x03\\x8e;\\xe9S8=8\\xd0\\x0b=E\\xe5\\xcc<\\x10q\\x98\\xbc\\r%?<\\xdf;><Z\\x1eH\\xbc\\xea\\xc7\\xd6;2\\xefu=\\xdcu$\\xbd\\xc7\\xf6\\x08<\\xa1\\xa41\\xbd:\\xb4\\x8d\\xbc\\xffaQ\\xbd\\x99\\x02i\\xbc\\x80j\\xb1;\\xff[\\\"\\xbd\\x82\\x88u=\\\"\\xd9\\xb4\\xbc\\xf9\\xcb\\xcc\\xbc\\x04\\x81i\\xbdV\\xe47\\xba\\xbbR\\xff;n^\\xa5\\xbc\\xe1\\x9f2=\\x88k\\xe0<\\x1f\\xd3\\n\\xbd\\xb7\\x06\\xad\\xbc \\xe2,=\\xce\\x9fA<\\xac\\xba\\xe4\\xbcbI\\x9b\\xbc\\xe6\\x07\\xd7\\xbc\\x139]\\xbdF0V=%\\xf6\\xa0<\\xdd}\\xaa;l\\xd9\\xd9\\xbcGN\\x1d<{N\\xcb<L\\xec\\xfb<\\xc0rb=\\xb8\\x81\\xcf\\xbc\\x9f\\xfb\\x17=v3s<\\x97\\x15\\x9d<\\xc3\\x92\\\"\\xbd\\xb7p\\xdf<\\xee\\xe0F\\xbcK\\xec\\xc6\\xbc\\x91\\xb2#=\\x87\\x1c\\xd2\\xbcu\\xfa\\xc6;dBl\\xbd\\xad\\x7f7\\xbd\\xdfN\\x93\\xbd\\xe3\\xf2N=\\x1d\\xa9\\r\\xbcT\\xcd}\\xbd\\x0e2\\xbc\\xbd\\xb5L>\\xbdL\\x1b6\\xbd<\\xef\\xb9\\xbbWN\\xf9\\xbb\\xce\\xa8\\x8d\\xbc\\x07\\xa5\\r=\\xf6\\x88\\x04\\xbdRj\\x97\\xbct\\x83\\x84<\\x15\\x18\\x13\\xbcI\\xd6\\xea\\xbcjA\\xc7<m-\\xd5<\\x9d\\xd2\\x9d\\xbcf\\xb7\\x0b\\xbdO\\xf7\\x0c=\\xa8c\\xb3\\xbd\\xb4o\\xf6;S9\\x9a;=\\xb9\\xe8\\xbc\\xa1\\x08\\xea\\xbbg\\x85b\\xbc2\\xc8\\x11\\xbdd\\xd9\\x0c=\\x9am\\xaa;%F8=l\\xe91\\xbd\\x0f\\xd9\\xad\\xbc\\x9d\\x14e\\xbcP\\x03\\x0e\\xbd\\n(\\x9a<\\\"\\x98b\\xbb\\x1e#\\xdd\\xbc\\n\\x89l<\\x11\\xb2\\x12<\\xaf\\xce\\x99\\xbb\\xf2\\x8b\\x89;V4\\xe1\\xba\\xcf\\xf2\\x9a\\xbb\\xb9\\x80\\x1a=\\x1f\\x82\\x08=L\\x84\\xbd;\\x16\\x9el=\\x95\\x07\\x8c<\\xff\\xfa\\xe6\\xbaDSQ\\xbdII4=\\xbc\\xe3\\xaf\\xbc\\xcf\\x94g\\xbd\\xc2\\\"\\x19\\xbaX\\xae\\x91=:\\x10p\\xbd\\x02\\x87\\xe9<\\xe0\\t\\xb7\\xbckw=\\xbc\\x97\\xf7\\xd0\\xbcz\\xa3\\xbb=\\xb1X\\xab;\\xf3\\xd0\\xed\\xbb\\xc5J\\x81;\\xaa\\x93-\\xbc>\\xe7\\xcd\\xbc>\\x94\\x9a=\\x84b\\xa4\\xba\\xf9\\xa3\\x97\\xbc0\\\\\\xa6<Y\\x87\\xcf9\\xa8\\x13\\x0b=\\x0e\\x1ev<y\\x1b\\x07\\xbdsyl\\xbc\\xb4{\\x15\\xbd\\xf5\\xcd\\xbc\\xbb\\x93\\x820\\xbc\\xd5\\x04\\xd0\\xbc\\xb3\\xf1W=\\xba\\xcb\\xce\\xbd\\x1cIV\\xbc\\xb5T\\xbc<n\\xe9\\x99\\xbc\\xd8AA:\\xc2V7=\\xd0\\x0f\\xd2<\\x0c5\\xb8\\xbc\\xe9\\xcd2=\\xdd\\xb8\\x9c\\xbdAN\\xbf<B46=hz\\x1c:\\xe6\\xa8~=\\xa8\\x87\\x14\\xba\\xbe\\x0b\\\"<5\\xde.\\xbd=\\x9b\\xbe<\\xe5\\x11!=\\xc3;\\x81<\\tK\\x84\\xbc\\xe1z\\x10=\\xd5\\x88^<\\xce\\xc2O=\\xfa\\xcd}\\xbc`\\xc3J=\\x8a\\xf1\\xab=]\\x02\\x08\\xbdtb\\x99:\\x8b\\xfa\\xb5=\\x91\\x11\\x0c\\xbb\\nY\\xf0\\xbc\\xe0S\\xcb\\xbc\\x0c\\x0ej\\xbc!dO\\xbc\\xe10K=\\xcbZ\\x90\\xbc\\xe4\\xea\\xcd;\\x90\\x9b{=\\xeb\\xdbc=\\x95e\\xa7\\xbc\\xd0\\x81N\\xbd\\x89\\xc9\\x97\\xbc\\xbd\\xe3\\x9b<\\xea\\xd0\\\"\\xbd\\x87xJ;\\xbc\\xcd\\x1c\\xbdH\\x97J=/\\xac\\xad\\xbd\\xfc\\xb9\\x11=\\x1c\\xb6\\x928 \\x16\\xe1<\\xe0\\xa4V\\xbd\\xd5\\xf5\\x90\\xbc\\xfd\\x93\\xcb\\xba\\xba8\\xfd<\\xd1\\xab\\xe9\\xbb\\xd1S\\xac\\xbd0\\xbb\\\\=3wF<\\x10\\x82\\xb1\\xbbu\\x86\\x86\\xbc\\r\\xdd\\x1c\\xbd\\xdb[%=P\\xd4\\x19;R\\xf0\\xa6\\xbc\\xa9P\\x1f=\\xf1\\x92\\x14=V\\x15\\xd6;v\\x06\\x16=\\xc5\\xfc\\xaf;\\x12N\\xe2<[\\x11\\xb9\\xbct?l<J\\xe6\\x91<A\\x93B\\xbd\\x185T=T$Y<q\\x0c\\xd0\\xbc-\\x08?\\xbd=Y\\xdc\\xbd\\xec\\xa9\\x92\\xbb\\xdb\\xed\\x8f<\\x8c\\x90\\xf0\\xbc3\\xear=^4S=\\xd6@\\t\\xbc\\xd6\\x87G<q\\x1e\\x16<\\xd1;4\\xbd\\xe9x\\xd4\\xbc\\x95\\x1d\\xad=}\\xe7\\x9e<\\xca\\x1e5\\xbcA67\\xbc \\xeb\\x0b<;\\xf4\\xf1<;\\xbaG\\xbclDU\\xbds(~\\xbd_\\xde\\x93;\\x12\\xec7\\xbd)\\xffF\\xbd)\\x02x\\xbd\\x93\\x9c\\x0f\\xbc\\xf3D\\x89\\xbd\\\"E\\x9b\\xbc\\xbe\\x84)\\xbclf5\\xbaYyc=\\x8e+/\\xbd\\xdf\\x97T\\xbd\\xf5c\\xbd<\\xeb\\xee\\x95=/\\xe1\\xc2\\xbc\\xb0\\xc1\\xcc<g\\xe91<i|$\\xbd&\\xc6v\\xbdic?=\\xcca\\x89\\xbd\\xb2\\xa9\\x03\\xbc\\xd0\\xcd\\xf0\\xbc2\\xc1\\x1d=\\x04\\xa1\\xab=\\xa3m\\x8a9\\x98\\xd3v\\xbd\\xb3@\\x86=\\xff\\x83)\\xbc\\xc94\\x8e\\xbck\\xe1\\xc8\\xbb\\xffq(=\\xccG\\x9c<\\x17\\xe07\\xbc\\x01\\x1fu<\\xde\\x9c\\xb1;2\\xf7\\xc9<S\\x8d\\xa5\\xbc@\\xb5\\xe4\\xbc\\x1c\\xe0\\\"<x-\\x7f<\\xb7F\\xeb<\\x85,\\xbc\\xbc\\xb8\\x88\\xf0<\\xe9x\\x82=\\x87\\xd9.\\xbb.}\\xb6;;e{\\xbd\\x8a\\xc5\\x89=\\xc7\\x82\\xf0:\\x80\\x0e\\xee\\xbc\\x88\\xab\\xa1\\xba\\xa2\\xb0\\xcd\\xbb\\xdd\\xf7\\x8a\\xbc\\xa8\\xe2u\\xbd\\xd8?+\\xbds\\xfc\\xb5=@\\n\\xc3<_u8=\\x9a\\x86\\xfc\\xbbn\\x89C<\\x9c\\x98\\x7f\\xbd\\x15^f\\xbc\\xcb\\xa6a<n\\xb8\\x02\\xbdF4\\xdf<\\x12\\x9f\\x95\\xbc=\\xcb\\xf7:\\xe9\\xd7\\x92\\xbb\\xb3\\x13\\xdb\\xbcpE\\x1f=\\xb9-\\xd2\\xbcjrK\\xbd\\x1d\\xd8\\xa3\\xbc\\x06G\\x8a\\xbd3\\xe4\\xbc<\\xb4=\\x04<$\\x8c\\x07\\xbc\\r\\xbd\\xac\\xbbv\\xd8\\xd9\\xbcH\\xd6k=CA\\xa4=\\x98M)\\xbc\\xd7\\xc8h=\\xfe\\xddD\\xbd:\\x1b\\x92<\\x1a\\\\\\x9b=d4e<\\xce\\x84\\x0c\\xbd\\xce\\xbd\\x85=M\\xe6\\xe5<O\\xc0\\x1b\\xbco\\xa7\\r;\\x00Y\\x13=\\x8f\\xf4q<S\\xc2\\xfe;\\x8a\\xdb\\xc1<A\\x94\\xa2\\xbc\\t\\x1e\\xa2\\xbc\\xc5\\n\\xab\\xbc\\x0f\\xbd\\xde\\xbbF\\x89,<r\\xbcy;\\xe4=\\x9d\\xbc2i\\xe8<\\xaf\\xba\\xcd\\xbc<\\xed\\xe5\\xbd\\xdb\\xcc\\x1b\\xbc-\\xca\\x17\\xbd(\\x06\\xd0;\\xf8\\xf1\\x08=\\xf7+\\xcd\\xbci\\x89\\x91<\\xff g=7\\x91/<\\xb3\\xe9X\\t\\x8dVh\\xbc\\xbd\\xfaX\\xbbY\\xf6\\xf0<\\xb6B\\xd6<\\xc6N\\xd9<\\x8d\\x7f^=h\\x19,\\xbc\\xfc\\xb4\\xc2\\xbc\\x8b\\xfeV<\\xcez\\x9d\\xbc*n\\x1d<\\xca\\x02\\xa4=sx\\xb9\\xbc\\xa4\\xf2\\x81=\\xda9\\xe1\\xbb\\x03\\xdb\\x01\\xbc\\xae.\\x8c\\xbd1\\x0f\\xb2:\\x84\\x83\\xf0;\\x02\\x8df=&\\x1bl;z\\xfb\\x7f:z\\xaa\\xef\\xbcd\\xb2\\x90\\xbc:E\\xa2<\\xe0\\xf7o<I\\n\\xa1<|r\\xab;\\xfe#M\\xbd\\xb9t\\x81<\\x03\\xb7\\xf5:\\x10E\\x88\\xbc\\xb1L\\xee\\xbc\\xba\\xc3\\xc9\\xbdy\\x15\\xfd\\xbcEGl;\\xccB0=t\\xce\\xcc<\\x85\\xdc\\x89=W\\x1c\\x95;\\x1f8J=\\xb7\\xfd\\x9b<\\x81\\xc5\\xfd<\\x1b\\\"S=g\\x14\\x84=\\x1f\\x14a<\\x04g\\xf9=+\\x04\\xa4<\\x06%\\x01\\xbdX\\xab\\x90\\xbc\\xbd\\xe4.\\xbd\\xbe\\xc7\\xf8\\xbcBe\\xfc<+0\\xda<i\\x05q<\\xfd|\\x14=Pn7=\\x04\\xe8;=G7\\n<s-!=\\xb7\\xda9\\xbcO\\xe2\\x87\\xbc,C\\x13\\xbc\\x04\\xd41=\\x8b\\xb8b\\xbd\\xfc\\xc9f\\xbd\\\"H\\xba;\\xe0P\\xf2\\xbbW\\xdb\\xbe<\\xf1o\\x85\\xbc\\x90\\x0b/=\\xfa\\xbb\\xb7;\\xa2\\xbd\\x8f\\xbc\\xe9<\\x98<\\x86\\x1a\\xd4<\\x17z\\xc4\\xbcG\\x9f\\xcc\\xbb5\\xab_=\\x18+D\\xbd=\\xed\\xd68\\xa6\\x961\\xbd\\x8d\\xbd\\x93\\xbb\\xe7\\x89)\\xbd`T\\x8a=\\x9e`\\x81\\xbcP\\xf7\\x82\\xbc{\\x90\\x9c=\\xf9\\xdaV=\\xf9k\\x14<\\x00\\x06\\xcd<\\xad\\xcd\\xb9\\xbc0\\x11\\xc4<\\x97\\xf2\\x98=\\xef\\xab\\x0f97\\x01\\x7f<bQ[=\\x96\\xa6>\\xbd\\x1e\\x0eV=N\\xb3\\xb5\\xbb\\xe6\\xfc\\x0b=C\\xf28\\xbd\\xe2\\xf4_\\xbdgu&<\\xe8-2\\xbd\\xc7\\x7f\\x02\\xba\\x06\\xcd\\xd5\\xbc\\xda/\\x94<\\xf0E\\x0e<\\xa8\\xda\\x8f\\xbd\\x19g\\x18\\xbc\\xd8l\\x1d=\\xd4\\xe2\\n\\xbd\\xdd\\xb4g\\xbd\\x0b\\nD=\\xd9\\xb4Q=E\\x11#;\\x95\\xd3\\xc5<o\\xdb\\xdc\\xbc\\x13\\xfe\\xff\\xbc\\xec\\xc1\\x95\\xbd\\x90\\xfeX\\xbbe\\xe4\\xc6<\\xe9\\xfe\\xf5\\xbak\\x08q;\\\"\\xbd\\xd6<O\\xcf\\xeb<\\x97\\xe8\\xbf\\xbc\\xc7\\\\>=\\xc3qI=x\\xbd\\x8f\\xbd\\xee\\x7f2\\xbc\\xda\\xcaD\\xbd-\\x9az=\\xea\\xa4\\x03<m\\xab\\x9e=\\xaf$\\x02<\\x83\\x1f\\x8a=\\xa6\\x99\\x9d\\xbd\\xb3\\x93\\x8c;S\\xa3C\\xbc\\xceh\\xe9<\\xc9C\\xd0\\xbcnC\\x10\\xbb\\xd4\\xaem\\xbc\\x84>\\x84<\\xaf\\x08\\xae\\xbc\\xe0\\xfcG<\\x81\\xaeK\\xbc%vP=)A\\xe6<\\xf75\\x8a\\xbc\\x9b\\xb1\\xd4;\\x05\\x0f\\xfc\\xbck\\xf1\\xb5<\\xfco\\xf1<\\xd82\\xf6<$k>\\xbd\\xfd&#<\\x1c\\x9d\\xca<\\xcc#\\x96\\xbcS\\xd2^\\xbd\\xde*\\x83=\\xe3[\\x85< `\\xfa\\xbb\\x9d\\x83\\xda\\xbc\\x8d\\x1c\\x18\\xbdH\\xb8\\x8a\\xbd\\xf3\\xb1\\xc2\\xbb\\x1d\\xe0\\x96=\\x85O\\n\\xbd\\x84\\x92\\xb1\\xbb\\x7f2\\x86\\xbbY,\\xc3<[k%\\xbd\\xa7\\xde\\xe7\\xbcFim=\\xf2\\xf3.=\\xb3*\\x0e;2\\x90\\x92<\\x98-\\\\<\\x17\\x95s\\xbd\\xa1\\xe3\\x1e\\xbd\\xfaBU=\\x92\\xce\\xe4\\xbc\\xc0\\xa8\\xa8\\xbb\\xe8\\xd8\\x01\\xbd<\\x9bu\\xbc\\x86\\x9e\\xe1\\xbb\\tQ\\x08=\\x04\\xa0F;\\xbb\\xfaN\\xbcsD\\xaa<\\x91\\xe3\\x1c\\xbds0\\x80\\xbci\\xc8\\xce\\xbc\\xebU.\\xbd\\xdd}\\x17<\\x13C\\x91\\xbc\\xa4\\x91u\\xbdc\\x96\\xaf\\xbb\\xf5\\xaa\\xed\\xbb\\xb8\\x07!;\\x1d\\x7f\\x19\\xbd\\xe6\\xe5[\\xbdA\\xff{\\xbcbd<\\xbdWb\\xc7;q\\xc1%=w\\xbcT\\xbc\\xc8e\\xe7\\xbcM$\\x15<\\xab-\\xa2\\xbc\\\"\\xb7a=\\xc6K;\\xbb\\xb3L\\x05\\xbdVPa\\xbc\\xf8\\xf1\\x91=@\\x9d\\xa8\\xbcL\\xb3\\\\\\xbdl\\x84\\x92\\xbd\\xc53\\xed\\xbb\\xed\\x95\\x04\\xbd\\x00\\xaf\\xee<\\x81\\xb4B=u\\xed\\x9b=\\xe2\\x06\\xd6\\xbaHz\\x1f=H\\xf4\\xd1\\xbc\\xeb\\x9e\\xa3\\xbd9\\x14\\xb5<{0\\x9f<x\\xe3\\xae<\\x89cj\\xb9#\\xe2\\xd8\\xbc%\\xb0\\x80\\xbck/F\\xbdh4\\x82;\\xde@\\x9d\\xbb\\xed-\\x82\\xbc\\xadg\\xe4;\\x9f\\xcd\\x9f<\\xf8\\xaaM\\xbb\\xc7z[=N;\\x91\\xbc\\xb6\\t\\x8b\\xbbr\\xc5\\xf7\\xbc\\xc6\\x9e{\\xbc#\\r\\xbd<\\xcd\\xf2A\\xbd\\xf6d/<1.\\xbb<\\x82\\x82R\\xbcX\\x80\\x9e\\xbcL>\\x1b=\\xe7\\xb8\\xcf\\xbc\\xc1\\\\\\x1a=\\xf8{\\xab\\xbc\\xf6\\x06g\\xbd\\xbd\\xd3M\\xbc\\x1ej\\xff\\xbc\\xbe\\xfc1:\\x9ck\\x85;\\xe6\\x10\\xf2<(p.=\\x98M\\xf4=\\xca\\x12X\\xbc\\xc5\\x00\\xf4<\\x81M_=\\xb1\\x04 \\xbd\\x8d\\xe1b=,\\xb7\\xea\\xbc\\xa6^:=\\x1d\\x16\\n\\xbc\"\nHSET bikes:10074  model 'Titania' brand 'Classic wheels' price 842 type 'Road bikes' material 'carbon' weight 15.3 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\xd4\\xcf\\x0e=\\x9e\\xe5\\xdd\\xba\\x94c\\r\\xbd\\x00\\xf7\\xf2<\\xdcJ\\xea<7\\x02\\x00=\\xfb\\xb7\\xf7\\xbc\\xc3|6\\xbd\\xba\\xd6\\\"=\\x82K\\xce<\\x97\\xc5\\x9e<$\\xc7\\n=\\\"\\xa6\\x1e=\\xd7\\x80\\x1f\\xbc+\\xbd\\xd2=\\xec\\x8bN<4\\x9aJ\\xbc\\xa8G\\xce<\\x9fo\\x97<\\xd8\\x0e\\xbe\\xbc(\\x15\\xa99\\x93p\\x02\\xbd\\xf0\\x8c\\xa3<\\xb5=\\x15\\xbd\\x13LW=\\xf4\\x1d\\xc8<F\\x9b\\x1d<r\\x9e1;\\\"g\\x93<\\xb4q\\xb1<WG\\xb6\\xbc\\x0c\\xfa\\xaa<\\xdc. \\xbc\\xed,\\x13;\\n@\\xdb<\\x96\\x1d8=s\\xa5.<\\xac$\\xac\\xbc\\xeaR\\xd3;0\\xd0\\x05\\xbc\\t\\xeb\\xa6=eW\\xd5<b\\x7fj<\\xbe\\x8a\\xc6<\\x9aj/\\xbcW\\x97\\x13\\xbd\\x17n\\xd9=\\xff\\xb3\\xf1\\xbc\\xdf\\x86\\x82\\xbc\\x00[J\\xbc\\xa4\\x97\\x0e\\xbd\\xb1\\xc3,\\xbd\\x9ex;\\xbc\\xb6\\x0f\\xe1<\\\"\\xd5,\\xbb\\xda\\xe2\\x07\\xbd0\\x04\\xae\\xbc\\xa9\\xb5\\xa5\\xbd\\x13;\\xb2\\xbd\\xecH\\x94=b\\x85\\x1b=\\x92\\xca\\x8b<\\xde\\x1c?=\\xcc\\x1e\\xde<b.*<\\xc7\\xad\\x80\\xbc^\\xcc\\x82\\xbc\\xf5\\x11t=\\xb3M1\\xbc\\x94\\\"\\xb1<3\\xd4\\x9e\\xbc\\xa0\\xae)\\xbc\\x9c+<\\xbd\\xab\\x15\\x15=v\\xe3d\\xba\\x1a\\xbc`\\xbc\\xf7\\xcd\\xa2\\xbc\\x83m\\x9a\\xbd\\xa4i\\xe9\\xbc\\xc6\\x94\\xaa9\\x8f\\x1d\\xf6<c?b\\xbd\\xf7_\\x19\\xbd\\xfb~\\xba\\xbc\\x06-j\\xbd\\x8d\\x1cr<\\xea\\x83\\xae\\xbc\\x08Y\\xd4<\\x95\\xb4\\xd8;K\\xc7\\xfa<\\xb7\\xae\\x15\\xbdb1\\x04\\xbdg\\xc7\\xc5\\xbc\\x18C\\\"\\xbc\\xd1\\xbd8=\\x07\\xbc`=\\xfbQ\\x18\\xbc>l\\x8f\\xbc\\xd3\\xb2\\r\\xbc\\x90\\xc8\\xa0\\xbd|R\\x12\\xbc0A\\x88<l\\x8cu<*\\x10\\\"\\xbc\\xaf}\\xd3\\xbb\\xb00\\xf0=\\xe1\\xd86\\xbcm\\x8f <.\\x9ad\\xbc\\xad \\x9e;\\xa2\\xd7H\\xbc\\xfa\\x9bn=\\xf5\\rH=\\xf9\\x84\\xd7\\xbc\\x14F\\x11\\xbc\\xb4^\\xce\\xbd\\xc5:\\x0f\\xbd\\xc7m\\xe6<\\x15&X\\xbbUi\\x95<t2\\xe4<\\xdd\\\"\\xb2;\\xb5\\xd6&=[\\x0c\\xd6\\xbb\\x98s\\x8d<\\xd6\\x80W\\xbd\\xe8\\x11\\x01=\\x01\\xc2U\\xbdB\\x03\\x10\\xbd\\xe8\\x82\\x0e\\xbd\\xd5\\xa9\\xa5\\xbc:f\\xc0\\xbcT:\\xdc\\xbb\\xdaCd\\xbd\\x1f\\xef$=k\\x81i=\\xfd\\x81\\xa7\\xbb\\x11\\xc4\\x83\\xbc>\\x81\\xd0<&y\\xa7;\\xb0\\x91M\\xbc\\xd2\\xc2\\x82\\xbd\\xc9\\xe4\\xa9<\\xc8\\xf0;=\\x8b\\x862=\\xcbE\\xa5\\xbbE\\x8c\\xdb\\xbb\\x80\\x80\\xc2;\\xd0\\xa1\\xeb\\xbcgX\\x19=\\xce\\xf6\\xd3<)\\xc4\\x84=\\xb2(\\x1a\\xbd$.m=\\x9d\\xb7\\xf4\\xbc_\\xe9X\\xbd\\x18\\xa1(\\xbc\\x18\\x97\\xb5<\\xf7\\xeei;v\\x80\\x91\\xbd\\xa2\\x97\\x80=D\\xf8\\x93<\\x00vk\\xbc\\xbd2\\x1a\\xbdjg\\xe2<AE\\x8a<rc\\xbc<z\\x92\\x1c=\\x8dA|=\\xa7\\xcb\\xc0\\xbb(\\xb3\\x1c\\xbd\\x0bIH=\\xa0pO\\xbb?\\r|\\xbd4@\\xbf\\xbc\\xdd\\xa7\\x17\\xbd\\xb9\\xbdy\\xbdiVr=+\\x8f\\x89;H-A<\\xc7]\\xa4\\xbc\\xa0e\\xbf\\xbc(\\x1fp;\\x05\\xf9\\x04<\\xd0\\xb3y=\\xce\\x0e\\xa3=\\xe1TF=\\x93\\xc5Y;\\xc1f\\xbe<\\x1a\\xcd\\x1b\\xbdh\\xder<^\\xe4\\x1d\\xbc\\x1b\\xf8A\\xbd\\xfc\\x9f\\xb4<\\xf7\\xfa\\xab<:\\xb8\\x7f=\\x07\\xdd\\x95\\xbc\\\"\\xd5:\\xbdti\\xd1\\xbd\\xa3\\r5=\\xfb5\\x01<\\x05\\x9e\\x8d\\xbd\\xe2@\\x8e\\xbdVP\\x04\\xbc|\\xa6\\xa1\\xbc\\xda\\x88\\xe3;\\xefY\\x0f\\xbd8\\x1f\\xa8\\xbc;)\\xb0<O\\x1d\\xce\\xba\\xfc\\xad8\\xbd\\x05\\xa5\\xcb;]\\xc4\\x07=\\xcbR\\xf2\\xbc&C\\x84\\xbcWtC=\\x8f\\\\\\xab\\xbc\\x04\\xe9\\xf9\\xbb\\xb3\\xaa\\xca\\xbc\\x19\\xee8\\xbdK[\\xc7<\\x1dA1=\\x1b(;\\xbd\\x07:.\\xbc!Y\\xd8\\xbcc\\xc3c\\xbd:\\xce\\xae;\\x05\\xe11:^\\xce.=\\xd3\\xd7\\x94\\xbd@\\xda\\x8f\\xbc\\xf4\\x80=\\xbdO\\x8d\\xde\\xbc\\xc6\\x11\\xb1<:w\\xbf\\xbc\\xaaz\\xd4<vo\\xe0;Qt\\xa0<\\xdd\\xcb\\xff\\xbcoi\\x15=\\xfe\\xe3\\x17\\xbd\\x0c\\xca3;I\\x9d\\x85=`\\x80\\xdf\\xbb\\\\\\x00\\xc8<\\x9f,\\x85=\\xd4\\xd5t\\xbc\\xfb\\x90\\n\\xbdx\\xdb^;\\x19n\\x04=\\x07!\\xbe:\\x16|\\t\\xbdn0\\xc7\\xbc\\x8f:R=\\x1f\\x1d\\xa8\\xbd\\x1e$2=\\x88\\x8ey\\xbc\\xe1k\\xd6\\xbc\\xfer\\xf8<\\x1d8\\xc6=\\xc0\\xae]<e]\\xed\\xbc>;\\xf8\\xbcsE6\\xbdq\\\\\\xa1;\\xa3G\\xcd=\\xad\\xbd\\x01\\xbd\\x13$\\x9b\\xbc4\\x97\\xb3<9\\x1f$\\xbc\\xbf\\xd5\\xd2<|\\xa1\\x8e\\xbc\\xfe\\x95\\xe9;h\\xa5\\x90\\xbb\\x84\\x12\\xa1\\xbc\\x1cG\\t\\xbb\\\",N9\\xbf\\x8f{<UV\\xad=\\x98Zx\\xbd\\xdc\\x7f\\xad\\xbd\\x18\\xfe}\\xbc%\\x05\\xbe\\xbc\\xb2{\\xc3<\\xbay\\x8e<\\x10\\x82\\xd1;\\\"\\x03\\x91\\xbc\\x9e\\xf0\\x99<9+\\x97\\xbd4j#=\\x8b\\x98\\x03=\\xbce\\xd7\\xbcA\\xb3\\x08=R\\xd3\\xaa\\xbc\\x83dQ<\\xcdG3\\xbdn\\xec>=\\x96\\xb3i=\\x13\\xdb)<\\xb2c\\xb2\\xbc\\x8e\\xf8O=\\x05\\xb4\\xe9;}\\x1ab<6\\xf8&\\xbd$\\xad\\x93<\\xef\\\\%=)X;\\xbd\\xa3(\\x90<}\\x03\\xc5=doa;\\x14\\x8d\\n<\\xe0\\xab\\x90\\xbc\\x06\\x11\\x0b\\xbd\\xff\\x9f\\xb0\\xbcF\\x85\\xb1<\\xd4t\\xf4\\xbcn\\xcf\\x88<\\xdf<\\x80=\\x02\\x8c\\xd4<\\x08\\x17F<F\\xd1\\x07\\xbd\\x12\\xd4\\xdb<\\x9a2\\x9d\\xbc\\x98k\\x04\\xbcT\\x92\\x8e<\\xe1\\xcdz\\xbd\\x9a^\\xe6\\xbbh+\\xfc\\xbc\\xaa\\xc4\\x7f<m}\\x85<\\x80\\xb0\\x88=\\xd6\\xedp\\xbd\\x17\\x93H\\xbbPw\\x97<\\xbc5\\xa2<\\x9b\\x14\\xa6;O\\xaa\\x9d\\xbda$\\xae<\\xd8P\\xb5<\\xbcK\\xe5<\\xac\\x8dn\\xbcC0\\xa8\\xbb\\xcb \\xcd=\\x04\\xe7C:M\\x9b\\x1c\\xbcC}\\x95<\\x17\\xd9\\x8c=\\x08\\xc6\\xdd<2\\xdcr<=\\xeca\\xbc\\n\\xbf\\xdd\\xbc\\x95\\xfd\\\"\\xbbZ\\xad\\x98<\\x16\\xa9\\xf1\\xbc\\x0b\\xecH\\xbdw\\xe36=\\x97\\xa7\\xc8<\\xa5P\\r\\xbd\\x9d\\x07\\xdf\\xbc\\x8b\\xc5\\xa0\\xbd(sh<o\\xf6\\\"<\\xcfg\\\"\\xbd\\xbe6w=3\\xbf\\xbd=\\xb4\\x16R\\xbc\\x8f\\xabM\\xbc\\x0bn!<\\xf9(J\\xbd\\x84\\x01\\x04\\xbdg\\x06\\xb2=\\x05\\x90\\x04\\xbd\\xb8\\x90\\x91\\xbcy?2<M\\xed\\xe4\\xbb\\xfd\\xe4\\x93<\\x8aq\\xbf;e\\x83,\\xbc\\x07\\\"\\xde\\xba\\xaa\\xbc+<\\xf6J\\xd1\\xbcv\\x08#\\xbd\\x99\\x80\\\"\\xbd\\x80\\xf4\\x1f\\xbbe\\xaaE87\\xcc\\xed<\\xda6#\\xbd\\xf3\\xb9\\x8e\\xbd\\xb3y;=\\xf4o\\n\\xbaW\\x1au\\xbd\\xfa\\xf4c=8\\xbc6=\\xba\\xf7\\xe1\\xbc\\x85\\xdf\\xaa;N\\xf22\\xbc\\x19\\xdb\\n\\xbd\\x98\\x01\\xba<\\xef$\\xb5<\\x98\\x97\\x98\\xbd\\xe2\\x84b<X8\\xdc\\xbcqW\\xd1;\\xcd\\xa1\\xba<\\x8cI^\\xbc\\x9c\\xb0\\xe4\\xbc\\xeb\\x16\\x8f;\\x05\\xa7\\xed\\xbc\\xf1\\x0f\\xaa<{\\xbb\\xa5\\xbc\\x1c\\x04\\xc4<\\xdc\\xa7\\xcc;\\xd8@\\x84;=p\\x89\\xba\\x17\\xc1b<\\xc3\\xd5\\x16=-o\\x82\\xbc\\xb2Y\\x0f\\xbd\\xc7\\x886\\xbda\\xa7S<\\x97\\\\5=}\\xe9Y\\xbc\\xaekx=\\xe8\\xffk=\\xc7z\\x92;I\\xc5!\\xbd\\x05\\x03\\x94\\xbc\\x14\\xb0\\xbb<\\xf1\\xab\\xb9\\xbcIf\\xd2\\xbc\\xb1\\xe3A<NnL\\xbcV\\xc2\\xa7\\xbc\\x17\\x05\\xd6\\xbc\\x81\\x96!\\xbd\\x8a\\x04\\xb4=g\\x04\\xf2<\\x88\\xd3A=n@,<*\\x91l=\\xd7g\\xb7<\\xfa\\xd4\\xe0\\xbc\\x10\\x91\\x90<\\x1a\\x8d\\x05\\xbc\\x03`x\\xbc\\\\m\\xf9\\xbb\\xe5*C<fV\\x18\\xbd\\x1a\\xf0\\x0b\\xbd\\xe1V\\x12=Fz\\x16\\xbd\\x8c\\x18\\x88\\xbd\\x86\\x0c-\\xbdoqY\\xbd)\\xc2I<SH\\x14\\xbd\\t6\\xb9<\\xe8]\\x12<\\x85#\\x06\\xbdlR\\x91=h#R=\\xcf\\xc3\\xa5\\xbcD\\x03\\xdd<m\\xacs\\xbc\\x14p\\xd6<\\n\\xe4\\xac=tN\\x84:\\xe4n\\x82<-\\xd5Y<\\xb4\\xc7u<\\x18o\\xfe\\xbcKeE\\xbc\\xfc\\x1c\\x03:\\xf0\\xe4\\xc9\\xbc\\xb7\\xbd`<\\xa3\\xc9\\xdc<\\x15\\xd2\\x02\\xbd\\xd3\\xb8\\t\\xbd\\x8ap\\x11=\\xcdO\\x03\\xbd\\x06\\xfe\\x8f<{U\\xcc\\xbb\\xfd\\xf9\\xee\\xbcS!\\x18=\\x8a\\xe5;\\xbc]c\\xa2\\xbd\\xbfP4=\\x99N\\\\\\xbc^o\\xfa<n\\x14W=\\xceV\\xdc\\xbc\\x12\\xcb\\xa1\\xbc\\x86\\x8b\\x8a=\\xb6\\xbb\\xe4<\\x8b-o\\t26\\x86\\xbb\\xc1\\x0b_\\xbc\\xd7\\x0fB\\xbduG\\xf1<\\xc27\\xb7;\\xe4g\\xed<\\xdb\\xea)\\xbc\\xcer_\\xbdS\\x04\\xae;\\xda~\\x88\\xbc\\xa0\\xb4\\xd9\\xbc\\xa7!\\\"=B<\\x82\\xbasDv=U\\xba\\x00<g\\xd3\\xa6\\xbbW\\xef\\xa7\\xbb\\xb0\\x8d\\xa9<Hw0<\\xf2w\\n=z\\x91^=X\\xe2U\\xbdxN\\xc4\\xbc|\\x05\\xe9\\xbb\\xa7\\xc3\\xfd\\xba\\xe8\\xde\\x10\\xbb\\xff\\x8at<Lk\\x18\\xbd\\xef\\x90\\xb1\\xbb\\xa5\\xc2\\xd0\\xbb\\xb3\\xfe\\xa6\\xbcl\\xb9\\x02=\\x97\\xea\\x16\\xbc\\x13\\x87\\x90\\xbdz%\\xfb\\xbc`Oj<\\xbcn\\xa9<\\xe5`f<nWU=\\x93x\\x1d=>\\xe3\\x06=Wu\\xf5\\xba\\xa4\\x91\\x12=\\xa1\\x10\\xa9=\\x88)\\x85=*f\\xe0\\xbc\\xe1\\\"\\x7f=;\\x91\\x8e<F\\x07\\xaa\\xbc\\x10\\x913\\xbd\\xe4 \\x03\\xbd\\\"\\xca\\xcf\\xbc1.W=i9\\x91:\\xec/7<\\xb8Q\\x1b\\xbc\\x1d\\xb4\\xed<b\\xf8\\xff<k!\\x1f:\\xf3\\xfc\\xd8<h\\xb9\\x8d;\\xe1\\xf2\\x11<\\xe5\\x1a\\xd2\\xbc\\xa3x\\xb6\\xbc\\xaa\\x97\\xbc\\xbd\\xd6\\x11\\x8c\\xbd\\xbd\\x12\\xca\\xbc5\\x04\\xcb;\\x81d[=\\x9d\\x01\\x95<\\xee\\xbb\\xa0;\\x8c\\xed\\x829UX\\xf5\\xbc\\x04\\x19\\x17=p\\x894=\\nw\\xe4\\xbbI\\xbe\\xc4<\\x946F=I>D\\xbd\\x14\\xab\\xd5<\\xf8\\x91\\x17\\xbc\\x9aG\\xd0\\xbbb\\xbc\\xc4\\xbcW\\xe6\\x7f=!\\x16\\xc3\\xbc\\x1c\\r\\x92<F\\x02`=\\x8eX`=8i\\xa2:b\\xb5\\xcc<\\x18\\xc9H\\xbd1\\xa27\\xbc\\x9b\\xdb\\xa0=\\x1e\\x96\\x15;\\xf0\\x99H=\\x98\\xc1;=y\\xd4\\x84\\xbdF\\x1d&<h\\x86a;\\xf7\\xae\\xf4<R\\x01\\n\\xbc`z\\xb3<-\\xdb\\xfd;\\xc8.e\\xbd\\x1b\\xb8\\x91\\xbcP\\xfbW\\xbc{\\x17`=K\\xa5:\\xbd\\xb9\\x95\\x8b\\xbd\\x10tC\\xbcs\\x8c\\x19<3\\x15\\x05=\\x0c\\xa5R\\xbdb\\xc8\\x99=\\xc1bQ<\\xab\\x00B;\\xc9/L=k\\xd5g\\xba\\x86\\x16\\x1f\\xbd\\xfe\\xbbf\\xbd\\x18\\xd1\\x92;s\\x00^\\xbajg0<a7M=\\xe4\\xb2\\x0c=\\xf9\\x9d\\x94\\xbc>]\\x10;S\\xa2\\xbf<\\xcf\\xb7\\xdc<4|\\x19\\xbd\\x9a5\\xda;\\xdd0\\xfb\\xbc\\r\\xc4\\x03=\\x98\\x80{\\xbc\\x0bz`=\\xfe.\\x10\\xbd\\xc1\\xc2!<I.\\xc4\\xbd\\xd9\\xde\\x8a<\\xa9\\x89\\xcc\\xbc2\\xf7_\\xbc\\x8a-\\xde\\xbc\\xbc\\tY;\\xa2(\\x91;\\xcc`\\xdd;\\x9b\\xed?\\xbd=\\x85\\xff<\\x88_2\\xbc\\xf2\\xf8\\xcf<\\xe4\\xa4\\xfa\\xbc\\xdf6\\xe9\\xb9%\\xc1\\xf3;\\xfcC\\x08\\xbcX\\xae =bc\\x9a\\xbc\\\"\\xb4\\t\\xbc\\xeb\\xda\\xbc\\xbb\\xaf\\x9e\\x87=\\x85K\\x88<\\xf2\\xdd\\x90<\\x18\\xdc\\x8d\\xbdp\\x95\\x8f<E\\x13v\\xbc\\x05\\xe2\\x94;\\x87}\\x9d<F\\x91\\\"\\xbd\\xc2\\x9c\\xa8\\xbd^\\x84\\xbb\\xbb\\x96\\x93==`\\xa7\\xbd\\xbc\\xfe\\x131\\xbc\\x9f\\xea\\x08\\xbdwJ\\x06<\\x80\\xa8\\\"\\xbd\\x06\\xf5Q\\xbb\\xb3 \\x83=Cr\\xf7<\\x01C\\xaa<\\xadx\\x0b=\\x0cK\\xc2\\xbcw\\x9c\\x96\\xbd\\x8c~n\\xbd\\x15\\x9d\\x1f=[\\xf6\\xec\\xbc\\\\\\xbd\\xee\\xbcFK\\\"<\\xe9\\x00\\xbd\\xbcr\\xb5\\xc5<\\xab\\x17\\xaa<\\xf9&\\x86\\xbcD\\xa8\\x94<\\x0f\\xc2\\xf9: \\x01\\xbb\\xbc\\xc9@\\xaa\\xbc9\\x10d\\xbd>j\\xaa\\xbcb\\xeb\\x9e<En\\x03\\xbd\\xceBU\\xbd\\xa5 \\x18<\\xa92\\xcb;fp-\\xbd\\xb8\\x06\\\"\\xbc3\\xaf\\x81\\xbd\\xa4sb\\xbdp8\\xfe\\xbb8Bk<\\xe5d\\x15\\xbd\\xae:\\xde\\xbb%\\x8fI<`\\x90>=Zd\\\\\\xbc7O\\xf3<\\xb8i\\x82\\xbc\\xfe)f\\xbc\\x9c\\x85\\x8b\\xbc\\t\\x06\\xd6<7\\x821\\xbd\\xee\\x98\\xba\\xbc\\xa3=\\x8c\\xbd\\xee\\xd4]\\xbaE<\\x9e;\\x97\\xac\\x81\\xbb\\\\\\xb6c=\\x06\\x14\\x83=`\\xc3.\\xbd\\xbf P<jV\\x7f\\xbd\\x89\\t\\\"\\xbd^\\x93\\xdf\\xbao[\\x9d\\xbc\\xa4\\xd6}\\xbb,K\\xfc;\\xacF\\xc7;&\\x11\\xf2<%\\\"\\xec\\xbcfr\\x92<a%\\xda\\xbb\\xad\\x93\\x88\\xbcI\\xb9\\xcd<c\\xb1\\xe0\\xbb\\xfa\\x1dU\\xbc\\xf5;\\xae<{\\xcd5=0c\\x80\\xbc\\x17uW<F\\xb5+\\xbd\\xe2\\xf8\\x08=.\\x8d\\x93\\xbd9\\xaf\\x9b<\\x02\\xc4\\xc8<\\x82D\\x16\\xbd\\xc5{\\xb6\\xbc\\xec\\x0e\\r=&\\x0fD\\xbd\\xbe\\xd4L=\\xb7\\xd8\\x8f\\xbd\\xa2\\xcd\\xe7<?2\\x06\\xbd\\xc3x\\x03\\xbd\\xa8\\xf4\\xc5\\xbb\\xd4\\xfe\\x00=\\xa1\\xb0i<NDH=U\\xd2\\xa1=,\\xd1$\\xbc\\xe7\\xe8\\xa1=\\x150\\xc8<\\x16\\x04\\x82\\xbdXS\\x1b=^5\\x0f\\xbd\\xa10\\x00<\\x05\\x18\\x1c;\"\nHSET bikes:10075  model 'Phobos' brand 'Ergonom' price 2856 type 'Commuter bikes' material 'alloy' weight 16.8 description 'This bike is a great option for anyone who just wants a bike to get about on. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\x0e\\xc3\\x92;\\xe5\\x02\\xbc<\\xef\\xf7\\x93\\xbb\\xff$\\x0e=\\x00\\x04.\\xbc\\xe6\\xf9A<~4\\xa8\\xbc\\x13&:\\xbd\\x06Rs=?\\xc9\\xad\\xbc\\x7fm&\\xbc\\x87\\x8c\\xc6\\xbc\\\"\\xeb/=i\\xc3\\x9d< \\x97\\\"=\\xbaR\\x84\\xbd\\xb2oe<\\xb4\\r\\x11\\xbdq\\xd1Q;5\\xef\\x84\\xbd\\\"\\x8d_\\xbc|4 \\xbd\\x06\\xc0\\xdf<\\x0b\\xbf\\xea\\xbc\\xfb{\\x99=\\xc8\\xca\\x88<\\xce\\xf9!<\\x81\\x0f\\xd5\\xbbr\\r%\\xbdtb/\\xbc\\xfc\\x1cP\\xbdp\\xa1#\\xbb\\xdf\\x11\\x95\\xbc[\\xac\\x19\\xb9\\xfc#\\x1b=9\\xc8O\\xbb\\x179q\\xbc\\x0cA\\xef\\xbc\\xd7)k\\xbd~q\\xab\\xbb\\xb8\\x94\\xda<\\x88*\\x97\\xbc\\x0cDb\\xbb\\xf0\\xfb\\x0f=X\\xf7\\xc1\\xbaa\\xf2\\xfb\\xbc7\\xcf\\x81=\\x8f\\x13\\x96<\\xeb\\x88\\xac\\xbc\\x91\\xa6r<C\\x18\\x1e\\xbc\\n\\xe1\\xb5\\xbcx(\\xa5\\xbc\\xbb\\xaf\\x85<\\xf3\\x84\\\"<Q\\xc4\\x12\\xbb\\x18/]<\\xb3\\x89\\xca\\xbdv\\xc3)\\xbdi\\x19\\x01=w\\xf5\\x8c<\\xa2\\x07\\xc3<j\\x95\\xb0;(K\\x87=\\x10b\\xe0<\\x0c\\x15\\\"=\\x99\\x1c\\x0e\\xbc\\xd8\\xa5+<o\\xaa\\x07=\\xfe\\xd1V\\xba\\xa9\\x0b\\xda\\xbc\\xb9\\xf5\\xd0\\xbc\\xed\\x14\\xc3\\xbc\\xd8\\x84F<K\\xe0\\xa2\\xbc\\xf9\\xd0\\xb5<:0\\x8a\\xbc^\\x03Q\\xbc\\x9a\\xca\\xac\\xbd\\xb3*a\\xbcxo\\x97<i\\x12\\xa6\\xbcn\\xd9\\x1f=h\\xfc\\x9d\\xbd\\xd4\\xf4\\xd5;\\xc3p\\x11=\\x96p6\\xb8\\x11\\xfc_\\xbb\\x1b\\xee;=\\xec#i<\\\"|n\\xbc\\xf3\\xdeQ\\xbb\\xc3Y\\r\\xbc$\\x8c\\x95\\xbb\\xe6\\xf8u\\xbb\\x8c\\xf2\\x9e\\xbb\\x88\\xc1?\\xbb\\xd7\\xd3\\xcd\\xbc\\xd1mC:jp\\xad\\xbb\\x8a,\\xe9<EP]=\\xa0\\xf3\\xcf\\xbaF!\\\"\\xbd\\xe8\\xa7\\x10\\xbd\\x10\\n\\x08>\\r\\xb5\\x02;D\\xdf\\xb4<\\x9c\\xfb|<K\\xd20<D\\xf3\\xb2\\xbcbEt=Q\\x83\\xae<\\xb2\\xc3\\xff\\xbcs\\xeb\\xa5\\xbc\\x8b\\xacj\\xbc`\\xeaP\\xbd\\xcd\\x14\\xa7\\xbch9\\xf7<\\x0b(\\x97<\\xf6\\x8b\\x1b\\xbb\\x1c\\x93\\xb0<}\\xc90=q\\xf0c\\xbd\\xe9;i=[\\x86/\\xbc\\x05\\xc5E\\xbc^\\x8bf\\xbd\\xf9O\\xd3\\xbc\\xc8\\xf1\\x88\\xbd\\xba\\x12\\xb1;q\\xc0j:\\x9d\\x85\\xbd<`\\x17\\xfb\\xbcp!\\xd8<~\\xc1\\x14=s\\x86\\x86\\xbc\\xf7\\xb8\\xf6\\xbc^\\xa8+=U\\x04\\x9f\\xbc\\xe6\\xa7@\\xbdC\\xb7K\\xbd\\xe3!\\xcd<\\x19uP<\\xb2\\x911\\xbd\\x0f8\\\\\\xbd\\x90\\x99\\x16=\\xb3\\xc4\\x15=\\xdb\\x81\\x01\\xbdd\\xb3C\\xbd\\xe8R\\xa0\\xbb\\xa0$\\x8e=~\\x10\\x1e\\xbc\\xf3\\xcd\\x08=dL\\xb7\\xbcI\\x82\\xb4\\xbb2\\x9d\\x04\\xbd\\x15\\xd7\\xde\\xbc\\xee\\x1d|\\xbc\\xfa8:\\xbc\\xfb\\xa7\\xaa=uC\\xbf\\xbcC\\xc1k\\xba\\xdeb\\x8a\\xbd4\\xc0n<\\xed\\xf9\\xa2<Uu\\xa4\\xbb\\xa1:\\x8c=\\xfe$\\xb5<HO\\x15\\xbc9\\xd9\\x05<\\xa0o,=\\x8f\\xab\\xb2\\xbc\\xa1f+\\xbd\\n}\\x9f\\xbb\\x02\\x14\\xdb\\xbb\\xe8:\\x93\\xbd\\x19\\xf0\\x16=\\\"[\\\"<\\xb1f\\x01\\xbd\\x9aA\\x93=Ou\\xd6<5\\x13\\xcd\\xbc\\xd5\\\"]=E\\x8a\\xc8\\xba\\x15\\xfd,\\xbd\\xcb\\x8fj=\\x8e\\x82\\\"\\xbd\\x18\\xde\\xb8<L\\xf74\\xbd9\\xc4,\\xbd\\x8f\\xc8,\\xbc\\xdf\\x95\\x1a\\xbd~b\\x01\\xbd\\xb9O\\\"\\xbc\\xe1DP=\\x9c4v<\\x1f&y\\xbd\\xee\\xa7\\x02=\\x0eL\\xa8=\\x80Y\\xf7\\xbc\\xf7bF\\xbd\\x93\\xba\\xf4\\xbd]\\xe2\\xb4:\\xf9\\xab\\xd3\\xbc:\\x1b\\xfe<\\xda\\xfar<w\\x95i\\xbc @Y=K:\\x15\\xbd\\x9f\\xd8\\x83\\xbb\\x0b:\\xa3;j\\xdf\\xd4<\\xee\\x97\\xe1\\xbc\\xcc\\xeee<~\\x7f7\\xbd\\x10\\xd1>=_\\xc1\\x01\\xbc\\x9d 5=/#\\xc8\\xba\\x86\\xb1\\x9e<\\\"\\x16\\x86;\\x88l\\xc7\\xbc\\xf6\\x81P<\\x8c5.\\xbd\\x85\\xc9\\xad\\xbc>\\x8e;<\\x88\\x18\\xb6\\xba|*G=\\x04?p\\xbd\\xec\\x9c\\xa5\\xbc\\xc6\\xa80\\xbd.6\\xed<\\x94\\xf5\\x82=\\n\\x95\\xc5;\\xfe\\xc4S=\\xac,\\x13=W\\x80\\r=\\xca/l\\xbc0\\xf9M<\\x14.\\xb2;\\xd9\\x02\\xb4\\xbb\\xa5\\x1c\\x1b=\\x12\\xd7\\x00;\\xf5\\xa3\\xf0\\xbb\\xc6\\xfe\\x88=b\\xe8\\xcc<\\t*O:K\\xf7\\xe6\\xbc\\xee]\\xc8\\xb9\\xc6\\xdc\\x1e=\\x17Q\\x94\\xbd\\xaa\\x8f\\xe5;\\xb0\\xe9\\x99=\\\\\\x0c\\x00\\xbd\\x1eZQ=\\x0f\\xb3\\xd8\\xbc\\x1e\\x0b\\x89\\xbcG\\xad$<\\x90\\xa0j=OF\\x02;<B\\x13\\xbd\\xc7\\xa08\\xbd\\x9d\\x0f\\xda\\xbc@R\\x06\\xbd\\tu\\xfb=5\\xb2.\\xbd\\x96Y\\x1c\\xbd\\xf4\\x80k=k\\xbd\\x88\\xbcG\\x0e\\x8e;\\x00\\x88\\xcb<\\xca\\x11`\\xbd1S\\x07=\\xae@\\xf6\\xbc\\x0ch\\x03\\xbcT\\xddV\\xbd\\x0f\\xd6w\\xbc\\xc0\\xc2\\xc8<Bne\\xbdZ\\x8d\\xc2\\xbc\\xb8(@=\\xfd\\x80\\x91\\xbd~\\x14\\x9c<\\x01W==^\\x17\\xc6\\xbb\\xbe,p\\xbaN\\xdc\\xbe;\\xdaP{\\xbd\\x99/\\xfb\\xbc\\xc1\\x9bp\\xbcV\\x1a!\\xbd\\xef\\xa9\\xbd=q\\x02\\xa4<\\xc3:\\x06<\\xc6[\\xda\\xbc\\xb6\\x80\\x9b<\\x0f@\\xe2<X3H=\\xf2P\\xe6\\xbc%TA=H\\x98\\x9e;\\xb3\\x19F=5\\x1e\\r\\xbc\\xc2\\x88\\xc1<\\xe5\\x158<H,0<\\x1e`\\xd1<\\xc1\\x00\\x81=\\xf4\\xe2C\\xbc\\xbb\\xd3]\\xbd\\xd1\\xf3\\r<\\xca#\\n\\xbd\\x81\\xc4i\\xbcl\\x04:<@\\x07\\xf6\\xbc7\\xf7\\r\\xbd.F\\xd1=o\\\"D\\xbb.\\x98;\\xbcG\\xb2C\\xbd\\xf0%\\x15\\xbd$\\xad(<\\xdd\\xeb\\xe8\\xbc\\xaf\\x9e\\xa7<\\x0b\\x9c<\\xbd\\xd4\\xcc\\xde\\xbc`R\\xde\\xbd\\xa3\\x11!=\\x1d\\xab\\xdb\\xbc\\xb9\\x87v=\\xd2\\x92\\xf8\\xbcz\\xb9\\x82\\xbd\\x18,\\x91;\\x10\\xc4\\x9b\\xbb\\xf6\\xf2\\xc2\\xbc\\x011(\\xbd\\xabPG=\\xaaN\\xb2<\\xef\\xa4c\\xbbh\\n\\x99\\xbc\\xcaR1\\xbd\\xfe\\xf3\\x95=\\xd0\\xb3m\\xbc%4)\\xbcE\\xb5@<+F]<\\xd1`5=wg\\x12=cao\\xbd\\x05\\xfbV\\xbd\\xe3\\x82\\xc3\\xbc\\xec\\x940=\\xf8c\\xb7\\xbc\\xb3\\xf7q\\xbcx\\x80H\\xbd\\xee\\x869;\\x10\\x16\\xed;{\\x96O\\xbd;\\xfc\\x9f\\xbd=\\xcff\\xbdP\\xf1\\x9d:Y\\x8cV\\xbdX\\xa0\\x80=\\x8e\\x82\\x16=\\x01OF=`b\\xc4;\\x8fz\\x94=\\xd8B+\\xbdK\\x89\\\"\\xbd\\x17T\\xdf<\\xc9\\xf3^<^\\x98\\xaa<\\xe4\\x10\\xb5\\xbd\\x93d\\xe6<\\x10\\x80\\x80;\\xc4}6\\xbd\\xc1\\xb8\\xec\\xbc\\x1cQ\\x1d\\xbd\\xd5e\\xd1;(X \\xbd\\xa1\\xd5\\x1a\\xbd\\x8d\\xa6\\xab\\xbb<\\x97\\x1e\\xbc\\x0f\\xfb\\xaa\\xbd\\xb85\\x9d\\xbc\\x9d*b\\xbcv\\x1e\\xd7\\xbbh\\xfd#=\\xe0\\xb7\\xb3\\xbcU\\n\\x81\\xbd2/U<\\xe4\\xea\\xa8=$\\xc9\\xbd\\xbc\\xd9\\xba\\xa8:\\x94)\\xad9O\\x98\\xf0\\xbd\\x1a,\\xde<uIV=\\xf2<\\x95\\xbd{\\xf7<< \\x05\\xe6\\xbc\\xfb\\t\\xa7<\\x92\\xbc\\xa7=r\\xb9\\xbc\\xbcW\\x13w\\xbb\\xe7\\x11r=W*n<\\x03&\\\"\\xbc\\xb90\\xd9<\\xa509=\\t\\x86z<$\\xec,=\\\"\\x94H\\xbc0&\\x11=\\x91\\xd0\\xec<\\xaf\\xe8\\x17\\xbdQy,\\xbdn\\\"#=\\xe3~\\x81<\\xdc\\xa0\\xc9:\\x1c\\xe8\\xf59\\x11\\xb3\\xa6\\xbc\\x87\\x0cL=\\x04\\xb2V:b)\\x80<\\xa0\\x9d\\x8b<\\xe1L]=4[F\\xbcO\\x1f\\xed\\xbc.\\xc9\\x00<\\xcbJ\\n\\xbd[]\\xc3<\\x7fx\\xed;\\x08\\x04\\x00\\xbd\\x1f\\xf4^=:\\x8fP=\\xee\\\\\\x17=\\x13\\xe85\\xbc\\x03[\\xa7<\\x97b\\x92<f\\xd4\\xab:4Bk\\xbcO\\x08\\xd2<7\\x7f\\xd0\\xbb%\\x07\\xc0<S8\\xa3\\xbd\\xbd\\x96_\\xbc\\x94\\x84\\x8c<_J?<d\\xc5\\x15\\xbd\\xd1\\x92\\x88\\xbdP\\x87\\xcc\\xbb\\x026\\xfc\\xbc\\x9f\\xdeV\\xb8\\\"&\\x8c\\xbc|\\xcd\\xd7:h;\\xe6\\xbb7\\xf7$\\xbdsQ\\xad=\\xd1\\xed\\x80=m\\x89\\x98:$\\x89|=\\xd8\\xf6\\xe5\\xbbA\\xaa\\xe1;\\x93\\x16,=\\xce\\xca\\x0c\\xbcV\\xc0\\x12\\xbd\\xc8\\xbb\\xbf\\xb9\\xd1\\xa9}\\xbc\\x8c\\x84\\x85\\xbbIu\\xd3<g\\xb7\\x9e\\xbb\\xd0\\x0c\\xb3\\xbc\\xeb\\xf8\\xca\\xb94lD=\\x7fL\\xe7\\xbc\\xd0\\xa3V\\xbb\\xb3\\x1d\\x9e<f\\xb7\\xd1;\\x03*J<S#\\xc4<\\xc2\\x9e\\x0f\\xbd4^\\x1d=\\xc3\\x12\\xca<\\x08\\xbb\\xd9\\xbd\\xaa\\x0b\\x8b;\\x16\\x05z\\xbcmg\\x06\\xbd\\x8eQ\\xf4<\\xba\\t\\xc0<\\xb6\\x13\\x94;p\\xb8\\x8a=\\xbb\\xbd\\xec:\\x87\\xf0b\\t\\xbd\\xeb\\x04\\xba\\xd5\\xea\\xa5\\xbc\\\"\\xc63=\\x1f\\xb3\\x95<(qI\\xbb\\t\\xfe\\xb4<\\x82]\\n\\xbd\\xa2\\x97\\xd6\\xbcpM\\x989`\\x10\\x92\\xbc\\xc2\\x9d\\xc5<\\x1eW\\xda\\xba2\\xe1\\x8e<\\xbey\\x81=c\\xf1\\xe2\\xbcAw\\x01= 1\\x86\\xbb\\x92\\xb8\\xd2<]\\\\\\xa3\\xbc\\xd4%\\x16=\\xeb]\\xd5\\xbb\\xd9\\xb4\\xe5\\xbc\\xe0\\x84L<\\x90\\x19B\\xbdsk\\x18<\\xc4\\xb1\\x90;\\xc9O\\x08<*l\\xc7<S\\x8a\\xca\\xbc\\x9b\\xfc\\xaf<e1r<$z\\x83\\xbd\\xe9\\x826\\xbd\\xc8\\xfa\\xe7;%\\xa3G\\xbcR\\xa90<\\x8a\\x1c\\xb3\\xbb\\xd0\\xb4\\x9f<|\\xb1==\\x9c\\x9d\\xe5<\\x93\\x0c\\x8d<H\\xdd\\xa8<$\\xfd\\xc1\\xbb\\x8f\\x0e%<\\xf5\\xfe\\xec<\\x959\\x0f\\xbb\\xb6\\xd3\\xdf<\\x9d\\x1b0\\xbc\\\"b\\x1f<\\x15a\\xa9\\xbc\\x8e\\xf5{\\xbd%n\\xc7\\xbc\\x96J\\xce<\\xd4oy<\\xa7z\\x7f<\\xf6\\r?<\\xc8-\\xab<\\x9f\\xfc\\x1d=E\\\"\\xb3\\xba\\xc0\\xd1\\x04=\\xe7\\x9b\\x8b<NjS=\\x85\\xe2\\n<Xb\\x06=n\\x1e\\x83\\xbd\\xf6\\xba\\xb6\\xbc\\x98\\xdaQ\\xbd\\xb7\\xac\\x9d\\xba\\xd1{\\x9a=7)\\xcb\\xbb\\xf7\\xea\\x04=\\xf1\\x15\\xd6\\xbc9[\\x92<\\xc1^\\xec<\\xf2h\\xce<\\xa4}\\xbb<\\xb1C\\xa0:\\xab\\x95\\xa0=7\\x8f6\\xbd\\xb6\\xf7u<\\xfe\\xf5\\x95<\\x83\\x13\\xfb<\\xaa\\x88e;\\xf2\\xe40=\\x10\\xf1\\x1f\\xbdt\\xca\\x86;\\xeb\\xad\\x16=\\xa3\\x8d\\x97=\\xaaG\\xeb\\xbaH\\x03\\xfd;\\x82\\x82\\x8e\\xbc\\x88\\xad\\xb5\\xbcb~\\x0e=\\xb0\\xe2\\x13\\xb9\\xea\\xa9 =\\xc4\\x06\\n=n\\x9f\\x15\\xbd\\xb9:\\x86:\\xf0\\xcf\\x8c\\xbbv\\xfc4<\\x1c\\xe2+\\xbd\\xeb\\x16>\\xbd\\xf9f\\x05<\\xd0\\xddA\\xbd\\xff>d=\\xa5\\xdc\\xa4;R\\x81L=\\xa0!\\x86\\xbba\\xa0*\\xbdK\\xc6\\x97<\\xedC\\x1c<\\xf2\\xf2}\\xbcO\\xf8\\x85\\xbc\\x04\\xa0\\xad=\\xe7:\\x13\\xba\\xe2\\xe7\\x1f<\\xbc\\xbd\\x84=l\\x06#=\\x1cx9\\xbc\\x0f\\xc3\\x9b\\xbd\\x88KR<!\\x8f%\\xbc\\xf2\\xa4\\x10=\\xb2\\xc8c;\\\\V\\xac\\xbc\\xda\\xe7\\xb0\\xbb\\xb3QZ\\xbd)sk8\\xc9\\x13\\xb6=\\xf6\\x86\\x9d\\xbd\\xcb\\x01\\x9c\\xbb\\\"*\\xb5\\xbc_\\xfe\\x94=\\xc0e\\x0f\\xbc\\n\\xb6\\x03=\\xe7\\xde\\xb9<\\xd6g\\t=fLj\\xbd\\x0e\\xee\\xe3<\\x10\\x00\\x08\\xbc\\xc8\\x1c\\xee\\xbck\\xe8Z\\xbdp\\x80\\x8b\\xbd\\xdfA\\x9d<\\xf7\\x96%\\xbc\\xd5\\xb8s<n\\xf7\\xf1\\xbc\\xbc\\x06\\xf2\\xbb\\xaen\\x00\\xbce\\x1c\\t\\xbc;\\xe8\\x90<\\x93\\xe7\\xa2\\xbba\\xa3\\x84\\xbbF\\xe9\\xd1;\\x06u\\x12=\\x88\\xe9\\xda<\\xd8D\\x86\\xbc\\xfc\\x15$=Y\\x80k<)j\\x03\\xbd\\xad3S\\xbd|\\xadf=\\x91\\x92y<]\\xb2\\xd1:\\x8cD\\xae\\xbc;^\\xe6\\xbc:\\x0bL\\xbd\\xc9C,\\xbc\\xd9\\x86t=Myo< \\xebG=;\\xd1\\x93\\xbc7w8=\\xf5\\xe8\\xdd\\xbcvKx\\xbc&WB=H\\x07 \\xbb5X\\x0b=\\xffy\\x13<N\\x7f\\xde\\xba^<\\xd4<J\\xfe\\x9d\\xbd\\xafq8=\\x0cY\\x12\\xbd\\x9d\\\\\\x18=Jy4\\xbd\\\"\\xa1s9\\xc3\\x9fz\\xbc\\x8b\\x00\\xac=\\xad\\x83\\x91\\xbc\\xa2\\x11];\\xf5\\x0e|<\\xf8f\\n\\xbd%\\xc3/\\xbd\\x93\\x88\\x11\\xbc\\x07\\x86\\x03:>\\xd7F\\xbd\\x95-7;2\\x9a\\xc1<C\\x0b/<\\x87\\xfb\\xef\\xbc\\xa5S\\x1f=\\xb36\\xc6\\xbc\\xe7\\xc9j\\xbd\\xb1\\x0fa\\xbdix\\x84\\xbdic:=!m\\n\\xbd\\xfbn\\x01<Z\\xe9\\r\\xbd\\xa3$\\xc2<\\xf0/\\x02\\xbc\\nj\\xfb<z\\xa3\\xb8\\xbc\\x88\\xb4\\xea\\xbc&\\x95:\\xbd\\xf6\\x16\\x08=HS`\\xbc\\xe0\\xe3\\x10\\xbd;\\xaa \\xbd_\\xce\\x98<\\xde96\\xbd\\x07\\x96[=\\x00\\x1aw=6\\xfe\\x91=Gd\\xda\\xbc\\x80\\xeaL;\\x87\\x89f<\\xb4\\x14^\\xbb\\x99\\xc8c=\\xcc\\xc7\\xc1<\\x98\\x06\\xcf\\xba\\x10\\x8b\\x18\\xbdD\\x00\\xa6\\xbc\\t\\xc18\\xbd\\xcd\\x86\\\\<XZd\\xbc\\xd1\\x84\\x04\\xbd\\x92j\\n=\\x18\\xce*<\\xf8\\xe7\\x9a<\\xc2\\xddD=\\x94\\x9e\\x98<\\xfa[\\\"\\xbc\\x10\\xe7x\\xbd=zZ<Vj\\x91\\xbc\\x1a9\\x988&\\x81{\\xbd\\xf8\\x9a{\\xbcT\\xefM\\xbc\\x80J\\x0b\\xbdi\\xfbq\\xbc\\xbap\\x18=\\xa0\\x8d\\xb7\\xbc\\xdf\\xec\\x9f</F\\xc2\\xbd\\xd1\\xc9\\x1a=\\x93>-\\xbd\\x02\\x19\\x90\\xbd\\xf7\\xb6\\x80;\\xd9\\xe6|<q\\xf9\\x8e;\\x98\\xe0\\x9e<kSu=\\xfcVc\\xbb\\xa65a;\\x9a\\x99\\xeb<\\xf6\\xf1\\x97\\xbd\\xd2a\\x7f\\xbcz\\x19\\xdb\\xbc\\xfb\\xe9&<r\\xc5\\xe2\\xbc\"\nHSET bikes:10076  model 'Hygiea' brand 'BikeShind' price 2059 type 'Commuter bikes' material 'aluminium' weight 10.3 description 'This bike comes in male and female versions (the female one has a dropped crossbar), and has some great features, such as the ability to mount mudguards, and panniers even with the large tyres.  The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there’s room for mudguards and a rack too.  All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\xe0\\xe0\\x06=\\xcbL\\x99<\\x8f\\\\\\x14\\xbc\\xb7\\xab\\xfa<\\xd7\\xc9\\xdf\\xbb\\xc4\\x97{;\\xdb\\x80 \\xbdV\\xe6\\x82\\xbck\\x9b7=\\xd0\\xb5[\\xbc|\\xf5\\xb7\\xbc\\x18\\xf4\\n\\xbc2i+=\\xb1\\xee\\x16=\\x9c\\xeb}=\\xb6\\xb61\\xbd\\x01\\xfa\\xcf:\\x08P\\x9e\\xbc\\x04\\xee\\\"=\\xeaV\\xd8\\xbd\\x97\\xdb\\xc5\\xbc\\xab\\\\\\xc2\\xbb\\x03\\xd8w=\\x88\\x93\\x17\\xbd\\xd8L\\x89=h{\\xed<\\x0f\\xbb\\xed<\\x83\\xc2Y\\xba\\xd8!R\\xbd\\x90\\xcdC\\xbc(\\xb04\\xbd\\xc2>a\\xbb\\xa1\\x8a\\n\\xbd\\xb0\\xdf)\\xbb\\xde)\\x02=\\x90\\xb8\\x91\\xb9\\xa5+\\t\\xba(\\xc0\\xdb\\xbc(\\xfcQ\\xbd\\x04\\xa4?:X\\x90\\x81=\\xe2\\xf1\\xa0\\xbbzO\\xdd\\xbc\\x92\\xe3\\x1f=\\xb0\\x01\\x9e\\xba[\\xf6\\x84:\\x97J\\x87=\\x9d0\\xf2<\\x93\\xa9\\\"\\xbc\\xb6\\x85\\xd8;u\\x95\\xad\\xbc6\\xfdZ\\xbd\\x01S\\x8a\\xbcG\\x9e\\xa7<\\x17\\t\\xaa\\xbc\\x89!U\\xbc\\xa86\\xb4<\\tG\\x9b\\xbd\\xa9\\x1d\\xfb\\xbc=\\x0fH=L\\x1b\\xc9<(WX<\\x89w\\xae\\xbc\\xc5\\x9b\\xc8=\\xd52^=@\\xf5\\x95<\\xffba\\xbcm\\xa3\\xc7<\\xc0z\\xb3<s\\xf1y<\\x8d\\x83M\\xbcv\\xac\\x9f\\xbd\\xe4\\x9d#\\xbd+{\\x17<\\x85\\x02\\\"\\xbdR5\\xbc<_\\xa9\\x82\\xbc\\x7f\\x95\\xbc\\xbc\\xcaC\\x91\\xbd\\xb1\\x8d\\x9d\\xbcYd\\x0c=I\\x0b\\x11\\xbd`*\\x06=\\xd0~O\\xbd\\xdd\\xdc\\xdf\\xbci\\xe2q9\\x14\\rf<\\xa0\\x1c\\xd0\\xbc\\\\}#=32\\x04<e\\x17\\x9b\\xbcY\\xd6E:\\x15\\xfa)\\xbb.\\xc1 <V\\xa3\\x03<&\\xef\\xa1<\\x04\\xe0\\xfe\\xbb\\xf7\\x81+;\\xea\\xcaF;A\\xcb5\\xbc\\x95]\\xa0\\xbc\\xa9\\xf0!=f\\x80\\r\\xbc\\xbc\\xec2\\xbd\\xa5\\x7f\\x10\\xbd7\\x8c\\xbd=\\xb3w\\x9c\\xbc\\x81!L=\\xae\\x12\\x9b\\xbbd`\\xfc:@b\\xcf\\xbc\\x83\\xe5\\x90=\\xb72F=#~\\xf5\\xbc\\xc4N\\xf8\\xbc\\x17\\x89\\xa3\\xbcu\\xe5\\x1c\\xbd:r^<$\\xe0\\xde<\\xae\\x95O=2\\x84\\xea:OV\\xf4;\\xfd\\x1cn<-\\x03\\x0c\\xbd\\xc1\\xe6\\x8b=R=\\xda\\xbc\\x15\\x8e\\x89\\xbc*xw\\xbd\\xfb\\xf5>\\xbb#\\xf5f\\xbd*\\x1e\\xa2<V\\xac[\\xbc\\x17\\xc2\\xe1;\\\"\\xc57\\xbd\\x0cd\\x17=\\xd0O\\x13=\\xe1@\\xc0\\xbb\\x9e\\x1eA\\xbc\\x1d\\xcd4=\\xdd\\xb1\\x88;\\xd0\\x8f\\xeb\\xbc\\xa9~\\xac\\xbdYe\\x98\\xba+J\\x89\\xbc=\\x04 \\xbd\\xffO\\x9c\\xbc\\xe7\\xcb%<9\\xdf\\xbb;b\\xa2\\x8e\\xbcdg;\\xbd!\\x0b%<\\xdc\\xf9\\xb5=\\xa0\\xee\\x98\\xbcz\\xe3*=\\x04\\x9c]\\xbc\\x1cn\\xd3;\\xa3\\x0f7\\xbd\\xe6\\x81\\xe99\\x08\\xb5\\xb9\\xbb\\x9d\\x0f\\x95\\xbc\\x94\\xea\\x8a=\\xb9s\\x15\\xbd+\\xb84\\xb9\\x14\\xbds\\xbd \\r\\xb7<A\\xdc\\x84<\\x83k\\xbc<\\xb3\\x9d\\x9f=\\xc7^x=\\x00\\x1a\\x89\\xbc\\xf7\\xe2>\\xbb7\\xa5\\xb0<\\x198\\x9a\\xbc\\r\\xc5N\\xbd\\xa9\\x9b\\xea<\\xad\\x0b\\x8e\\xbc`\\xce\\x83\\xbd\\xf2\\x83\\x05=0\\xf8-<\\x93F\\x1f\\xbd\\x80b?=\\xeaG\\x99\\xbc\\x82a\\x04\\xbd?U?=\\x15\\r%\\xbcF\\x1d\\x84\\xbb\\x1b\\x04e=i?\\x13\\xbd}\\x87C=\\xd1\\xa0\\x0c\\xbd\\xc1\\\\\\xcc\\xbc\\xc5),<\\x18,)\\xbdGy\\xad\\xbb\\xb2\\xb4i\\xbc\\x0c\\xbfi=\\x89\\x82]<\\x8dMG\\xbd\\xb2\\xa0$=.\\x82\\xa2=\\x1cm\\xdc\\xbc\\x1d\\x8c\\x9d\\xbd\\x10\\xc3\\xf5\\xbd\\x15\\xed\\xd0<\\x80\\xf7\\x8c\\xbc\\x00[\\x07=\\x84>\\xb2<~\\xde\\xa2\\xbcm3\\x16=h\\xf3\\x03\\xbd\\xfe!\\xc3\\xbc\\x8d\\x17\\xa0;\\xa1\\xc00<3d\\x94\\xbc\\xe6\\xf2\\xbf\\xbb\\xaa\\x811\\xbds\\xcd\\xcc<\\xf9\\xd3\\x17=\\x02\\x1e =M\\t\\x8d\\xbc}\\xecs<\\x9a\\x06\\x94<e\\xb1M\\xbdT\\xaf\\xd8;j\\xd6\\x02\\xbd|0\\xd7;\\x95*~\\xbc%S2\\xbb\\x99\\xb5\\xd7<\\xce\\x8eA\\xbd\\xee\\xc6\\\"<\\xa7\\x19\\xdf\\xbc\\x8f\\xdc\\xd8<\\x8c`^=\\tE\\x06\\xbc`\\x9fC=\\xbdR\\xad<[\\xa4\\x0f=\\xf7~\\x94\\xbc\\xfaAi<\\xc43=<&\\x0b\\xd4:B\\x0e\\xd2<]\\x80\\xe6\\xbbv\\xbcR;:\\xa1\\xbe<\\x03\\xb3\\xf2<\\xad\\xd6\\xc2\\xbb\\xcf$N\\xbc\\x85\\xea\\x06<\\x0buN<\\xf0\\x05h\\xbd\\xe3\\x04K<\\x07\\x19\\x9a=\\x91\\\\\\xe1\\xbc\\xb28f=\\x8f\\\\S\\xbc\\xadh\\xfb\\xbbB\\xdd4=C\\xf6>=\\xbe\\xc9\\xf3<\\xcd\\xd2?\\xbcf\\xec\\x1a\\xbd\\xbdw\\xfa\\xbc\\x98`\\xdc\\xbc\\xfd\\x83\\x08>J\\x0eT\\xbd\\\"\\xa7\\x9e\\xbc\\xa0\\x10u=\\x80\\xbc\\xa0\\xbc\\x8eX\\xae;\\xe3\\x13\\x90:6\\x0b-\\xbd\\x92\\xc0\\xd1<\\xff\\xab\\x9f\\xbc\\xa9\\x88\\x90\\xbc\\xfd=\\xa2\\xba\\xad\\xcc\\xc5\\xbb\\\"\\xee\\xe8<+\\xb5\\x8a\\xbd\\x0f:L\\xbd\\xb9\\xe3\\x11\\xbbxeq\\xbd\\xd7H\\xc2<G\\x99*=\\xd5\\xec\\xb8<\\x9dh\\xe2<\\x91B\\x90\\xbb.\\x11%\\xbd\\x1c\\xcf5;\\xbcE\\x1c<\\x14\\xc9I\\xbd)\\xd5\\xad=\\x12R-;\\xe1\\xd8\\xb5\\xbb\\xad\\xfd\\x81\\xbc\\xa0\\xab\\x10=L\\x02W=\\x99\\xb3x=%\\x1c9\\xbd*\\xfdh<6j(\\xbd9\\xd6e<&\\xc8\\xb1\\xbb\\xc4\\x9dJ<+\\xf1\\x88<\\xbf\\x15\\x92<\\xbe\\x06%\\xbaN\\xc4i=\\x95\\x8a\\xc7\\xbc\\x85zD\\xbd\\xed\\xbbg\\xbbl\\xfcH\\xbd\\xe1\\xc3^\\xbc(U\\x8a<\\x96!\\xf2;\\xa5G\\xbe\\xbc\\x18x\\xab=\\xa4\\x0f\\xb3\\xbaY\\xe1\\\\;i\\xe0+\\xbd\\x83it\\xbc:^j;9\\xe4\\x85\\xbc\\xec\\xc9w=\\xe6Q\\x0b\\xbd\\xa5M\\t\\xbd\\xb3U\\xc2\\xbd\\xcf\\xe5\\xa1=\\xdf,\\xda\\xbb\\xe5\\x01\\xd4=s\\x010\\xbd.\\x96\\x80\\xbd\\xbfZ\\xd0<\\x8e\\xcbU\\xbb\\xd1Tm\\xbc0\\x8a.\\xbd\\x88\\xef\\xb5<\\x13ck\\xbb\\xe9h\\xa5\\xba\\xc2\\xbf\\xb1\\xbba;\\x95\\xbcc\\xd5x=\\x91\\xf0a\\xbbR\\x0f\\xbc\\xbc\\xbaT\\xb3<P\\x1f\\x08=\\x0f\\xa9i<e\\xdcj\\xbb\\x95\\x98X\\xbd\\xfcg=\\xbd\\x9au\\x08\\xbcYL&=\\xf6\\xe8\\x93\\xbc\\x91\\x95Z\\xbc\\xb6v\\x85\\xbd\\\"\\x9e3\\xbb\\xdc7\\xb3\\xbc\\x98\\x91G\\xbd\\xba\\x9b\\x9e\\xbdT\\x84\\x15\\xbd8\\xf1\\xcd;~)\\x80\\xbd\\xde\\xdej=\\xe2\\x18:=\\xa2\\xe4\\x02=\\xa2\\xb8n\\xbbU\\x059=\\xc4\\\"L\\xbd\\xebu\\x12\\xbd\\xa0,+=\\x06\\tI;\\xe4X\\x11=\\x12\\x1e\\xb0\\xbd\\x1a5[<=\\x8a\\x13\\xbb\\x10EK\\xbc\\x15\\xac\\xb0\\xbc\\x9a\\x07\\xcc\\xbc^\\xe3\\xe5\\xbb\\xe7\\x82\\xc1\\xbc\\xaa\\xfd\\r\\xbdp\\x1a7\\xbbAR\\x1f\\xbc\\x88\\x80;\\xbd\\xd3\\xe2*\\xbc\\x9d\\xd9z\\xbc\\xa2e0<\\xf6\\xcce=\\x0bq,\\xbc\\x1b\\x98\\x13\\xbd\\xa0\\x83\\xc7:&/\\x8a=\\xeb\\xb9\\x81\\xbc\\xa7\\x808\\xbc\\xbb\\xc2\\xc8;j\\xe8\\xe6\\xbd&\\xc3\\x1b=\\xc5$>=\\\"\\x02\\xa8\\xbd\\x8a\\n\\x94<+D\\xfa\\xbc\\x94\\xa2|;\\xd4\\xe9\\x85=\\xa5\\xba\\x0e\\xbd\\xcc\\x9f=\\xbd\\xbd Q=@/I\\xbco:\\xb6\\xbc\\xe7\\x17\\xb7<%\\xdfI=\\xcd\\x15x<F\\xc0(=\\xc9C\\xa4\\xbc\\x8e\\x8e\\xdb<lh\\x11=\\x83<3\\xbd\\xea=I\\xbd\\x81n\\x8e;\\xf9\\xf3o<\\xb0\\xc5\\x07=\\xc3Z\\x1e\\xbc?d\\x8e\\xbc\\xfd\\x15P=31\\xce;&W!\\xbc\\xec\\xe0O\\xbc\\x8aq\\x97=DR\\xf8\\xbc\\x9bo\\xf8\\xbc)\\x9f:=G,5\\xbd\\x1aw\\xac<\\xe2[e<\\x8dn=\\xbd\\x04\\x12\\x86=&\\xfcA=Y\\x8c\\x0f=\\xcd\\x93\\xb9\\xbc\\x13\\x15\\xb6<\\x12}Y=RM\\t<\\x98I\\x0f;\\xa4\\x93\\xd6\\xbcf\\xdf\\xa0\\xbc\\x12\\xa5w\\xbcx\\x91R\\xbd/H\\xea\\xbc1<\\x11<\\xc3\\x83\\xa0\\xbaQg\\x07\\xbd\\x93\\xb10\\xbdj`\\x1b\\xbc\\xa2\\xfd\\x8a\\xbc+\\x07S\\xbc\\xd6U\\x91<w\\x98\\x0c\\xbc\\x8frr;\\xec.\\xda\\xbc}9\\xa8=\\x83r\\xaa=\\x8c~\\x83<\\x1a\\xe5\\x10=\\xaf\\x94^<\\xf5\\xe9Z<( Z=S\\xc1\\xf3\\xbc\\xbb9\\xc0\\xbcK\\xf4\\\"\\xbc\\x81\\xda\\x89\\xbc\\xc3\\xb1\\x95\\xbc/{\\xa2<W7\\x1c\\xbd\\xd2\\x00&\\xbd\\xed\\xd9\\x81\\xbca\\xf8==\\xf9P\\x06\\xbd\\xd4\\x15\\xa2\\xbbH\\xdf\\x03=\\xfdQ\\xb5\\xbb\\xac\\x04\\xb9\\xbabd@:R\\xe4\\\"\\xbd\\xd5<==\\x01/K;\\xbe\\xbb\\xa4\\xbdA\\xf2\\x11=\\xef65::K\\xbd\\xbb{\\x12&=\\xa2\\x95\\xaa\\xbb\\xa9:\\xc4\\xbc\\xf8\\x9f\\x8a=\\xad\\xbd\\xf9\\xbc\\xf1Y\\x81\\tg\\x97\\xec:|\\xd6\\xe6\\xbb\\x91\\xd8\\x14=\\x99P_<v\\xbf\\x17\\xbb\\xe7\\xa3\\x15<`\\xbc\\xbe\\xbc\\xfc\\x83\\xfa\\xbcpc\\x00\\xbd\\xfe\\x91\\x05\\xbc\\xd0\\rj=\\x92il\\xba\\x10\\xf1\\x15=\\xa9\\xa7c=\\xba\\x16\\xe1\\xbc\\xbcN.<D6@\\xbc$\\xf0y<;\\t\\xaa9\\xc9j\\xab<\\xf9\\xd3\\xd0\\xb9\\xccP\\r\\xbd\\xe8k\\xf5\\xbb\\xd9MN\\xbd\\xb5\\xbe\\x06=\\xc5\\x80D<\\xfb.m;\\xb1\\xdd\\xf1<\\xcf\\xa9\\xb7\\xbc\\xff\\xcb\\\\;?\\xec\\x17<O\\x96\\xeb\\xbcE\\xd63\\xbd\\x82\\xd0\\xce\\xb8\\x07\\x12g;\\xeb\\xf0\\xca<nI\\x8f\\xbb\\xe4\\r\\xbb<50\\x12;Q\\x8a\\xec<N~?=\\x1c\\xdc\\xb6:\\xa5\\x19;\\xbd?{\\xfe<Jex=\\x93\\xba)\\xbd\\x81\\x98\\xb8<\\xf6\\\\\\xb4\\xbc\\x8d`C<\\xd2\\xab*\\xbd{%\\\"\\xbd]v\\xb4\\xbc?,\\xb9<c&C<\\xbeI\\x95\\xbb}c\\xb3\\xbc.6\\xf8;\\x9e\\x12\\xbc\\xbbr\\xa2\\xa4\\xbb\\x1a\\xf7u=O\\x12\\xd2<\\x7fM\\xab<G\\x13f<\\xe5f\\x9a<w\\xa20\\xbd\\x97@%\\xbd\\x9d\\xbfJ\\xbd\\xd3\\xfcn<\\xc6\\xc9\\x94=EP2\\xbb\\xa1\\xbf5\\xb9\\x152\\x9b\\xbb\\xd7^\\x88<\\x8a\\xe69=\\x10\\xc0\\xec;\\xb2\\xb7\\xaf<\\xccb\\xa0<\\xd1\\x06\\xae=\\x88\\x839\\xbd\\xd9\\x10\\x8c\\xbb-\\xc0\\xb4<q\\xb0\\x8b<@\\x86S\\xbb\\xd9\\xe0\\x8c=4;\\t\\xbd\\xa2M\\xc7\\xbc\\xb9\\xfc+=!\\xf7\\x9a=\\xea\\xfc\\x13=ox\\x8a\\xbc|\\x90;\\xbd\\xfa;\\n\\xbcZd\\xea<\\xc7j\\x96\\xb8\\xd3\\xb1\\xe9<R\\x1c\\x19=\\x8f\\xbc\\xb5\\xbcOg!\\xbc\\xfe\\xac\\xf5<\\xf7\\xfb\\x81<\\x93\\xb4\\x1d\\xbdA\\xb1D\\xbd\\xa5_\\x0e<LE\\x86\\xbd\\xea\\x14\\xe2<U\\xe3\\x0f\\xbc\\x9cWt=f\\xae\\x86\\xbcN\\xf6\\x1b\\xbd\\xc1\\xf3\\x8d\\xbc[s\\x08<\\xef\\xa9\\xa1\\xbbBB\\xc8\\xbc\\x8f\\xf1\\x9a=\\xc4W\\xa5;\\xb8\\xa5\\x98<o\\xc6v=\\xeb\\xdf\\x8d=\\x01%.\\xbd6\\xf2\\x8b\\xbdcb\\x02=\\xa9\\xa9\\x83<\\xd6\\xf5A=\\xa3\\xf2\\x1e=\\x05\\ns;\\xbd\\xbe \\xbc\\x11\\x86a\\xbd\\x9eo\\xef<\\x1e\\xdb\\x8e=\\xcf\\x98|\\xbd\\x17C\\xa7<\\x1c\\r\\t\\xbc\\x7f#\\xc5=\\xec\\x01X\\xbc\\xd9g :\\x96\\xd6q<\\x16\\xb5\\xd6\\xbb\\xcf\\x19\\\\\\xbd\\xe6\\xec\\x18=S\\x0f\\xa7\\xbc\\xf5\\xafP\\xbd\\x18\\x06S\\xbd\\x86\\xbeT\\xbd\\x90p\\x0b=2\\xd72\\xbb5\\xd1\\x0c\\xbcH\\xf0\\x90\\xbc\\xacZ\\xfd\\xbb\\xa0\\x1d\\xba<\\xa8\\x0e=\\xbd\\xc7\\x83\\xc1\\xbc^#\\x80<\\x11:\\xf9\\xbb\\xb95\\xa2<\\xe7\\x1c\\xb9<%jB<\\x98\\xff$\\xbd\\xff\\xd7.=\\xd3\\xfc\\x91<\\xbb\\xf3\\xd1\\xbb\\x1e\\x82\\x9f\\xbc\\x068\\x97=\\x14\\xfa\\x9d\\xbc^\\x8a \\xbc\\xf8F\\xb1\\xbc\\xc8\\xbd\\xd5\\xbc\\x956\\x9d\\xbd\\xbc\\xbd\\x94\\xbbT(\\xd4<x\\xf4\\x83;\\xe4`]=w\\xaeq\\xbc@t\\xe7<\\r\\xf0\\xd5\\xbcU\\x9c;\\xbc\\x10\\x99~=(\\xedB<\\xa4<E<\\xfb\\xef\\x8c\\xbb\\xf3\\xe9m\\xbd\\xf5\\xd4\\x1f\\xbc\\x1c\\x83E\\xbdV\\xa0Q;\\\";%\\xbd\\x0e\\x99\\xa2<\\x03\\xd7\\xa5\\xbc\\xb9\\xbf;\\xbd\\xb5\\x97\\x7f\\xbc\\xfa\\xcf>=\\xcc\\x1c<\\xbc\\x83w\\xa5\\xbaCb\\xc1<\\x1dG\\xb1\\xbc\\xb8Q\\x03\\xbd\\xd9$c<$\\xe9\\x12\\xbc\\x17JS\\xbc\\x9e\\xb1\\x9c\\xbc\\x83}@;W\\xcfS\\xbb\\xa9E]\\xbd\\x8d=\\xec<9T]\\xbc9\\xda\\n\\xbd\\xed\\x9f_\\xbd\\x11\\xce\\\\\\xbd\\xba\\xc4\\x90<L\\x16o\\xbd\\\"\\xa7\\xc6\\xba\\x02X1\\xbda\\xde\\x8c<\\n\\x01\\xca:\\xc5c\\xe1<\\xc32%\\xbd\\xf5G\\xdc\\xbcV\\xb5<\\xbd\\xcb\\xd6\\xd8<\\x16\\r\\xb7\\xbc\\xb4y\\x8f<\\x7f\\x97Q\\xbd\\xca}\\xea<\\\"o\\xef\\xbc\\x7f\\x87\\xa9=Fe\\xb4=\\x17U\\xc0=\\xcf\\xa0N\\xbcx\\x9eW<\\xdd\\xb5\\xb2\\xbc]RV\\xbc\\xc0Ov=\\x1a\\xdf\\x94\\xbc_\\xc8\\xfb\\xbb\\xb9\\x12\\xf5\\xbc@o\\x17\\xbd7\\xe1#\\xbd\\xdcg\\x8b\\xbc\\x1bS\\x82;\\xa3\\x90\\xe3\\xbc7\\xe4\\x18=G\\x8f\\xae<,&\\xb0<p\\xd0.=\\xafZ\\xa4<\\t\\x05\\x86\\xbcT\\x8e9\\xbd\\xdd\\x8f\\xf5\\xbc;\\x00\\x17\\xbd\\xd8\\x9f\\xcb<|\\xe0\\x82\\xbdX\\x0bq\\xbc\\x13>\\x8f\\xbc\\x0bB\\x0f\\xbdo\\x8ab<\\x0cX(=\\xd7\\x9c\\x1d\\xbdZF\\xf4\\xba\\x80\\x9c\\xb0\\xbdC\\xc65=_Cm\\xbd\\x0f\\xff\\xab\\xbd\\xd4z\\x05<\\xf5\\xbe\\xc7<BL\\xb3;\\x9e\\xe8\\xe2<`\\x8b\\x9e=/\\x1c\\x01<=\\x93\\xdd;\\rM\\x90=\\xael9\\xbd\\x1c\\x90\\x81:\\x9d\\xeb\\x87;,2\\x19<\\xde\\x0c\\xc0\\xbc\"\nHSET bikes:10077  model 'Callisto' brand 'nHill' price 3526 type 'Road bikes' material 'full-carbon' weight 9.2 description 'This bike delivers a lot of bike for the money. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"*\\x19\\xa6<\\r\\x99A=\\x07\\xc4\\xfa\\xba\\x16\\\"V<\\x06e\\xc7<40\\xab<\\x8b\\xa3\\x10\\xbd$\\xe54\\xbde\\x1b\\x85=\\x19\\xe6]<\\x10\\xf6p\\xbc\\xbeI\\x07<8\\x80\\xc4<c\\xff\\x82<BW\\xe5<\\xbd\\xc4p\\xbd\\x8f\\x7fv\\xbb\\xc3I\\xd3\\xbcn\\xd1\\x96\\xbcP\\xc6{\\xbd\\x07\\x13*;\\xe0\\xd2z\\xbc\\x00D?=\\x9bm\\xb0\\xbc-\\xf8\\x0f=y\\x94W<\\xe9\\x9e\\xa0<\\x0f(\\xb8\\xbc\\x94^A\\xbd\\xa2\\xda\\x84=\\xc6\\x81\\x04\\xbdW1q\\xbd\\xc4\\xb5$\\xbdccm<\\x1f\\xbb\\x01=\\xaf\\x89V;\\xbf\\x9c;\\xb9\\x93ZI\\xbc\\x05\\x177;\\x81\\x82q\\xbc\\xac\\xde)=\\x03\\xff\\t\\xbd\\x9d`+=\\xe4\\x10\\xbb<\\x7f\\xbd\\x06<\\xba\\xc7\\xe2\\xbc!+\\xa2=A\\xde\\xea<\\xc4\\x05%\\xbcM\\x18\\xf0\\xbb\\xe0\\xf0\\xd1\\xbc\\xf8+\\x8a\\xbdcD.\\xbd\\x0b\\xba\\xb4<\\xed\\x90\\xee\\xbb\\x18B?\\xbb9\\xba+<9\\x0b\\x85\\xbd\\x15\\x94q\\xbd\\xbc6\\x9d=UK4=\\n\\x18m\\xbc\\xa9\\xed\\xa1<$P^=\\xc6I\\xac<E3\\x82\\xba\\xd28R\\xbc\\xa4\\xd1\\xfd<\\xb3a\\x9e\\xbb2\\x1f\\xb2\\xbb\\x7f\\xa1\\xfc\\xbb\\x9f\\x05\\xef\\xbb\\x90\\x0b\\xe6\\xbc\\xd2\\xe8\\xd29W\\x88\\xef\\xbc<7V<*\\x7f\\xd4<\\xac\\x82\\x06\\xbd\\x05\\\"\\x82\\xbdf\\xc0\\xff;\\x83\\x85\\xd1;\\xed\\xf8(\\xbd&\\x08\\x15=\\xcbT\\xae\\xbdO\\xbe\\xe3\\xbc\\x8d\\xe4k\\xbb\\x99L\\xf6;B\\x1f\\r\\xbb\\xa6X\\x0e=\\xf4\\x92H<\\xfaN\\x9c;\\xf8_\\xde\\xbb\\xc5\\xa1X\\xbc\\xb9\\xd3\\x91<\\xe9o\\x90:\\xaf\\x1c\\xe7\\xbc_\\xa8\\xa3; :\\xd9\\xbc\\xc5K\\x8e;:\\x9c*\\xbc:B\\x99<\\x80\\x9c\\x0c=\\x1d\\xb3\\xf7<\\x18\\xd9\\xa5\\xbd\\x0b\\xf2\\xae\\xbc\\xdb\\xe2\\xb8=\\xcb\\xd8\\xe5<L\\xb6\\xce<G\\x8ch\\xbcj4\\x1d=ahz\\xbc\\xf1\\xc4-=\\xbeE\\xbe\\xbbo\\xf57\\xbc\\x84\\xfb\\x1f\\xbdr+\\xb0\\xbcA\\xb78\\xbco_\\x1d\\xbc+\\xae\\xf9<?H\\x07=M\\xcb\\x84;\\x10#\\x07:\\x04\\x17m\\xbc\\xf3\\xa0\\x91\\xbd9\\xa8\\x94=@\\xb2w\\xb9\\x8d\\xc3)<\\x9e\\x94\\x87\\xbd_\\x9a \\xbd \\x0b\\x0e\\xbd\\xe9\\xbc\\x86\\xbc@T\\xb5\\xbc\\x84\\xb3\\x07=\\xbf\\xa2\\x0f\\xbd%\\xb8\\xce<\\xef\\x17\\xc6<\\x90\\x93~\\xbc\\x9fmC\\xbc\\xe1\\x10\\x99;VL\\xb5\\xbc\\xf7M.\\xbd\\xf4\\xc5\\x01\\xbd\\r,u=\\x06XW=gmu<\\xb6\\xd8#\\xbc\\x93\\x1d\\x9f<\\x10\\xf1\\xc1<\\x0b\\x86\\xb3\\xbcnn\\xb5\\xbc\\xaac\\x07=\\xf7\\xb8c=\\xad\\xd2:\\xbd\\x89\\xbf\\x06=\\x19|\\xfc\\xbc.\\xb2\\xae\\xbc\\xaa\\x1fp\\xbd\\x1fH\\xc1\\xbb\\x81=\\xb6\\xbb\\xbeh\\x0f\\xbd\\\\\\xa8-=P\\x84n\\xbc\\xa1\\x0e\\xf3\\xbb\\xfc\\xf31\\xbd\\xc4q\\xb3<\\x1c^1<\\xa2\\xa1}\\xbc\\xc7oW=D[9=_\\x1a\\xee\\xbc\\xb9\\xe2\\xc5:\\xb2\\xe3\\xe4<\\x87\\x14\\x83\\xb9\\x9fQ\\x18\\xbd\\xb1&\\xc0\\xbc\\xc3@\\xa3\\xbb\\x7f\\xf6p\\xbd\\xb9\\x85s=\\xc4/\\xe4<-\\x19\\xb4\\xbc|A\\xdc\\xbc\\xf0\\x9a\\x03<\\x99&I\\xbd\\xcb\\xb8\\xf5<\\xe4\\xc6`=*k\\xdb\\xbc\\xc4\\x8dJ=\\x04\\x154\\xbc\\xc6\\xfc\\xe3<\\x89E\\x07\\xbd\\xec^\\x97\\xbb\\xbb\\xad\\xd1<\\x8aM \\xbd\\x05r\\xd5<\\x9e\\x80X\\xbc\\xa2\\x03o=\\xbd\\xa2\\x81\\xbd\\xeb:i\\xbdVU\\t\\xbc\\xc9re=d,\\x02\\xbdmn\\x96\\xbd\\xf62\\xef\\xbd\\x87\\xd7\\t\\xbd\\x10\\xba\\xd9\\xbc\\xa0.\\xba;e\\xcf\\x9c\\xbc\\xc6D\\n\\xbc\\xebF9=\\x7fKG\\xbdR\\xd5h\\xbc\\xdaT\\x01\\xbd\\xff\\x90\\xee<\\x11,C\\xbd\\xfcR\\x91\\xbb4\\xfa\\x01\\xbd\\xdd\\xed\\x91<\\xf6\\xf0\\xb0\\xbbJQ6=\\xbe\\x0b\\x7f\\xbd\\xcf\\x02><7\\xc8\\xaa\\xbb\\x87t\\x94\\xbc\\xd3\\x07\\x88;\\xa7\\xd8\\xb7\\xbcd\\xaf\\x18\\xbd\\xbcQj\\xbb\\xe6\\xcb\\xc5\\xbc\\xd7R\\xaf<o[\\x97\\xbd\\xa6\\x9e:\\xbd{)<\\xbd\\xb0\\x14-\\xbc\\x99bE=M\\xab\\\\<7\\x9a\\x86;\\x05Y\\x05=\\x11s\\xee<\\xa0N\\x8c\\xbc\\x8e\\x9e\\x02<\\x10Y\\xe8<\\xbb\\x1c\\xff\\xbb\\xd4\\xa3\\xed<\\xb8F\\xa2<Q\\xa1\\xb2\\xbb\\x98\\xab&=\\xa1h6=\\xce\\x9f\\x82<\\xcd@\\xfc\\xbckm\\xfa<\\x17\\xee\\\"<\\xd8\\xf9c\\xbd\\x9f\\xaf\\x99<\\xd5\\xef\\x9b=!DP\\xbdu\\xa6\\x8c=\\xc2!\\xde\\xbcHgY\\xbc\\x04}\\x97\\xbb\\xfdX\\xb2=&7\\x9c\\xba\\x84.6\\xbc\\xa0\\x88\\x12\\xbd;_\\x01\\xbc\\xeb\\\\X\\xbc\\xb4\\x16\\xd8=W\\xd4\\\\\\xbd\\x98h\\xcb\\xbc;\\xd9\\xfb</\\xa8\\xc0;~\\xb0q<\\xa9w\\xd0;\\xb04G\\xbdy)d<L\\xc1\\xbe\\xbc+M\\xaa\\xbc\\xd4\\xee*\\xbd9\\xa4~\\xbc)\\xf8\\xa9<\\xba\\x8e\\xc2\\xbd\\xb5F\\x99\\xbcPZ\\xae<\\xf3\\x17\\xee\\xbc\\x80\\x85\\x86<\\x13\\xd2i=k\\xfb\\xf0<\\x15\\\\\\xc3\\xbc3\\xdeS=\\x06\\x8fk\\xbd\\x0b\\xbbs:\\x9fo\\xd7<GcD\\xbc\\xebM\\xe9=\\\"\\x88\\x85< 6\\xf4\\xbbc\\xb4Z\\xbb\\xe3sJ=\\xde4\\xdc<\\x1bpQ=e\\xb8S\\xbd\\xa4\\x8d\\xed<\\xe61\\xc5\\xbc\\xac\\x02\\x0c=\\x908\\x06\\xbdh\\xb89=1>~=\\x9f2+\\xbc\\xae2\\xea<\\x1d\\xc3\\xa3=\\xd2\\xbaC<\\xd0\\x9b\\xdf\\xbc\\xe0\\\"Y\\xbc;c\\xda\\xbc\\x91f\\xdf\\xbc-\\xf1\\n=my\\xd9\\xbc\\xbfL\\xa5\\xbc\\x9b\\xccs=2\\xddm=]*\\xe3\\xbc\\xc0\\x91\\xba\\xbci\\xe0\\x83\\xbd\\xce\\xd4\\xd6;a\\x9fX\\xbd\\xa6\\x99\\x1d<.0\\x12\\xbd\\xb3\\x10\\xa2<P8\\x97\\xbd\\xa3\\xd0%<z\\x83}\\xbcw\\x8b\\x0b=F|\\x83\\xbd\\x8c\\x85\\n\\xbd\\xc3\\x8e\\xb4:\\xdc!\\x1e=\\x0e\\xeb+\\xbd\\xf1\\x9cl\\xbdd-\\xb9<\\x8fw\\x11=\\xc1BK\\xbc\\x874^<?\\x82\\t\\xbd(km=\\x91X\\xc2<\\x7f\\xdf\\x03<\\x9b\\xce\\xdb<\\xa9\\xce\\x06=\\x9eD\\xfe<\\xb1\\xf3C=\\x15\\xa3\\xe1\\xbb\\xf4\\xc3R;\\x8do\\r\\xbd\\x8d[{=\\x1a\\x05x\\xbc\\xfcI\\x90\\xbd\\xc2\\x9a\\x01\\xbd9YH<\\xd5\\xb4\\xa6\\xbc\\xc7u=\\xbd\\x8b\\xef\\xde\\xbd`\\xfd\\x86\\xbc\\x18S\\xc6<\\xe9\\xe2\\x12\\xbd\\x1b\\xc0\\\\=A\\xd3K=\\xcf\\x1c\\xfc<\\x18k\\x87<1\\xa8\\xac;\\xde\\xeft\\xbd\\x82 \\x90\\xbdNw\\x0f=\\xa8\\x86\\xd1<L\\xa9R\\xbc\\x18|\\xe9\\xbc\\xf6\\xc8\\x99<\\x99\\x85C=\\xb2\\xcb)\\xbc\\xad$7\\xbd\\x8f\\xccq\\xbd\\x96M\\xe5<\\x92h\\t\\xbd\\x97M\\x0c\\xbd\\xadJ\\x16\\xbc\\xae\\xfc\\xc4\\xbc~J\\x91\\xbd\\xa7\\xd9\\x19\\xbb\\x83^\\x81\\xbc\\x1f_$\\xbdL?\\xc4<Jh\\x05\\xbd\\x18c\\x84\\xbda\\x19\\x82;\\xf3\\xe7\\x8e=R9\\xf2\\xbc^\\x1b><\\xda\\xf7\\xf1<\\x17\\x9bO\\xbd\\x8b\\xc7{\\xbc\\xd9(\\x7f=a\\xaf\\xa2\\xbd%X\\xbf8E\\x95\\t\\xbdhcQ=\\xa51Z=\\xcds\\x99:\\xa5+y\\xbc\\xaa,b=8G\\x9a;nz\\xf7\\xbcj\\xde-=\\xceH\\xed<&\\x8d\\x0e=\\r=6=A\\xb6\\x0e\\xbc[\\xc7\\x03=\\xc7N\\xa8<\\xd5B\\x8f\\xbb\\\"\\xd1m\\xbd\\x86\\xe3\\xaf;/\\x7fe<\\xbe5\\xe0;\\xf67\\xc4\\xbc\\\"\\xe0\\xf7\\xbbX\\xa5G=@b\\xca\\xbcT\\x13\\x9b\\xbc\\x06\\xb3k<\\xc6\\x81\\x7f=\\x9e\\xec\\x98<^U\\x02\\xbd\\r75\\xbc\\xb9\\xdb\\xf6:1Q\\x13\\xbc\\xb0\\xfd\\xd4\\xbc?\\xb1\\x82\\xbd\\xc6?\\x8c=\\xf5\\xc6Q={\\x13\\x06=\\x92\\x84\\xc1<+\\x7fY=D\\xb6\\xd7\\xbb\\x1b\\x8c\\x9b\\xbc?\\x7f\\x82\\xbb\\x15Q@\\xbd\\xe00\\x01\\xbc\\x98\\x9a\\xb4;\\xedS\\xb5\\xbc\\x12\\x91\\x90\\xbcu+H;\\x9epS<q\\\"/\\xbd\\xb1O=\\xbd\\x8a\\x8b\\x01\\xbb\\x03\\xb0\\x99\\xbd\\xee\\\"?<\\xcc=M<\\xa3\\x83h;_\\rQ\\xbc]\\x10W\\xbcr\\\\\\x11=a\\xbbQ=l;\\xbc\\xbc\\xcf\\xa2)=\\x9f\\xb9\\xe6\\xbb6\\xe4\\xa4<T\\xf1\\xd1<\\xa2\\xdcH;8\\xcb\\r\\xbd\\xa5\\x98\\x83\\xbc5\\xdfj=F\\xcdX\\xbc|\\xa1\\x94<NN\\x9f<Q3w\\xbbF\\\"\\xee<Av\\x05=8\\xee\\x98\\xbc3\\xba<<\\x8d8h<T\\xc5y\\xbcJK\\xc5;O\\xb2\\xc5<\\\\\\xaa\\x98\\xba\\xe5IW<\\xb3t^\\xbc\\xab\\x05\\xcd\\xbd\\xffi\\xca\\xba4\\xc6\\xc8\\xbc$\\x963\\xbd\\xe7?\\xc2<DQ\\xb3\\xbc\\xdaK\\x97;Os\\x8c=\\xf0\\\"\\x9b<\\xb8IT\\t\\x1f\\xc7\\xbb\\xbcU\\xcf&\\xbc*K\\xbc<\\xdc\\x90\\xb9<\\xcb\\xe6y<\\xec\\x15\\x16=\\x9a\\x8f\\xf5\\xbc}G\\xf7\\xbc\\x08\\xef}\\xbbu\\x0e2\\xbdXV\\xd0<\\xb8\\xf16=4\\xa3\\x98\\xbc\\x87\\x81e=u\\x9bR;I3\\xc3<\\xd8\\xd9\\x8d\\xbd\\xa4\\xf2\\xcc\\xbc\\xff\\xf2\\xa2\\xbc!h\\x08=\\xe5\\xc29\\xbb\\xeem\\xad\\xbc\\xac\\x02w<\\x06\\xa3\\t\\xbd\\xdd\\xcd!<i_7<\\x07\\xb7\\x96;\\xac\\x88\\x15=\\xacY\\xd0\\xbc\\x0f\\xc8\\xfa<\\x9c\\xf2D\\xbc\\xf80L\\xbdF\\x179\\xbdk\\x13\\x83\\xbd\\xd5\\xaf\\x8b\\xbd(\\x9d\\x19<O\\xd5\\xf1<\\xb9d\\xf9<!\\x17r=}\\x860<\\xe1\\xe9l\\xbc;\\xdd\\x06=\\xd7\\xb1\\x1d<\\x8cU\\r=)\\x8a\\xc0<\\x86\\x9d&<T\\xae\\x9b=\\xd5\\xb1/;\\xebA\\xca\\xbco\\x00\\xfb;V\\xd0\\xec\\xbcfq>\\xbd\\xf4\\xb2]<\\x83\\x17\\xad;B\\x9c[=\\x14\\x9aF=a\\xefK<{\\x89I<\\x8d4Q=\\n,\\\"=:\\x1fA<\\xaf2Z<\\xdd\\x94o<->5=\\xed\\xa5\\x8f\\xbd\\\"NF\\xbd\\xab_\\\"\\xbd\\xff\\xb6B\\xbc#y\\x15=G\\x12^\\xbc\\xfd\\xb3h=\\x03\\x91\\xe3;\\x1b\\x8b8<\\xa9\\xcf\\xba<%\\xc2$=\\x94]0\\xbbu=\\xbc\\xbc\\x16;\\x8e=\\x19\\xe2\\x80\\xbd\\xe7\\xdc6=_gm\\xbcw\\x19\\x05;[\\x87\\x10\\xbc0b>=\\xa4\\xb1/\\xbd\\xf13%=4`q=\\xd2\\xa0\\xb2=\\\"\\xe4\\xb9:\\xa09]< \\x99\\x91\\xbc\\xb0\\xbdk8\\x8b<\\x06=F\\x1aH:[\\xac\\x1f=\\xef\\x1fA=\\x00\\xca\\x82\\xbc\\xc5\\xda\\x02\\xbcX\\x17s\\xbb\\xef\\xda\\x01=\\xa6\\xfca\\xbd\\x86\\xcc\\x84\\xbda\\xf0\\xa8<\\xa6\\x107\\xbd\\xb8yw<I\\x0f:\\xbd\\\"\\xc1P=n\\xb2i<n=\\x1e\\xbd\\x1c\\x87\\\"<\\xba~(=1y\\r\\xbd?F>\\xbdSRC=[\\xe99=\\xab\\xed\\xc0<~\\xea;=O%\\x1f<%\\xac\\x10\\xbd\\xffSZ\\xbd\\xd8\\xcc\\xf8\\xbby&\\xd8;\\xd8\\x0b\\x05=\\xfb\\xc9\\x13<\\xb2\\x115<|2e\\xbc\\x81&\\x07\\xbd\\x81*`=\\xb2U\\x86=\\x7f\\x99g\\xbdJ\\xcag\\xbc\\xa3k$\\xbd\\x84\\xaae=\\xb3\\xd1\\xaa\\xba\\xe5\\xb6\\x9c=[}Z<\\x18\\xfc\\x8c=\\x8bp3\\xbd\\xf4\\xb5\\x8a\\xbb\\x04\\x81\\x85<\\xfc\\nQ=\\xd5_\\x89\\xbdx\\xb6\\x14\\xbdt6\\x12\\xbd\\xb6d\\x8c<m\\x0f\\xba:-\\xdd%\\xbd\\\"\\x1d\\xd6\\xbc\\x8a\\xd5U<\\x9a\\x84\\xa8\\xbc\\\"\\xf0^\\xbc;V\\x85\\xbc\\x86\\xfe\\x109y\\xc9\\xc8<\\xe4b\\xc5<\\x10{\\x11=\\x86XM\\xbd\\xb3z\\x86=\\xed\\xee\\x01=\\xd6\\xb9O\\xbb\\x03{W\\xbdR\\xdd\\xb9=\\x96\\x9c\\xaf\\xb9\\xfe\\xb9\\x02=@\\xc2\\\\\\xbcF^M\\xbc\\xd9\\xfae\\xbd\\x06n\\xbc;\\xf4\\x90\\x92=\\x01\\xbc\\x94\\xbc\\x1dO\\xe0<y\\x97\\xe4;\\xd1\\xc3\\x94<\\x11\\x11\\xbc\\xbcz\\x08\\x92\\xbc-b\\x80=:\\x93\\xaa\\xbb\\xe8\\x8d\\x13\\xbdOhm;\\x8e,\\x8b\\xbc\\x0c\\\\\\x05\\xbd\\x88\\xf0\\x7f\\xbd\\xabj\\t=\\xe0\\xa9,:L\\x97\\xbf\\xba\\x93\\xbe\\x19\\xbc\\xf7\\n\\xf6\\xbb\\xcb\\x95\\xa2\\xbbn\\xc7/=\\x01_\\xb9\\xbc3\\xc2x<\\x0f\\x84\\x1d=\\x1cH\\xae\\xbc\\xa4\\xcb\\x9f\\xbcB\\xd2\\\"\\xbdVw\\x12\\xbd8\\x02\\x0b\\xbc\\xddl\\x9a\\xbb\\xfc\\xeb\\xa5\\xbc\\xdaEk;\\xfb\\xd1+\\xbdn\\x80\\xb7<G\\x91r\\xbc\\xd3\\xd0\\x8c\\xbd\\xc1\\xc5\\xf9\\xbcr\\xbb\\x1e\\xbd7\\xdb\\xd9<\\xf0\\xa0d\\xbcn\\x8d\\x15\\xbb\\x80\\xe5M\\xbdh\\x13\\xee\\xba\\x8e\\xb0\\xae\\xbc$\\xae\\x97<\\xdf\\xb0X<\\xf9\\xcfF\\xbd2`<\\xbb\\xe1\\xb7?=\\xc5\\x1c\\xd5\\xbc\\x03:\\x9e\\xbd\\xe6H^\\xbdH]\\xde;\\xf6\\xd2h\\xbdw\\x8dY=U\\x82S=\\xc2T\\x90=\\xdd\\x9c\\x1d;]>\\xa7<\\x1fh\\x1f<\\xb2\\x90\\x8e\\xbd\\x0cuF=\\xbdt\\xa3<\\x9b\\xfd$;\\xaf\\xae\\xa9\\xbc\\xf2!\\x11\\xba\\x8ej\\xd6\\xbc\\xe7N \\xbc\\\"\\xd5\\xd3;u\\xb5\\xa5\\xbc\\x1b\\xe4X=\\xb9\\xfe\\xec<V\\xe5\\x8a<\\xd2\\xd8\\x8b<\\xd2\\xa2u=\\n\\x9a\\x9c\\xbb\\x7f\\x03@\\xbdSf\\x1c=D,u\\xbcZ:\\xc4<\\x01#\\\"\\xbd\\xae\\x90m<T\\x9f\\xe5\\xbcLH\\x80\\xbc\\xe6\\x88\\xfa\\xbb\\xf8Rf=\\x02\\xff\\x87\\xbc\\x84\\xba <v\\xbeJ\\xbd\\xea.1\\xbd\\xdb\\xa3\\x82\\xbbM\\x81\\xbb\\xbd \\xef8\\xbcQ\\xc5\\xf3<-\\x08\\xa6<\\x96\\x08m=\\x0e0\\x87=p\\xc1\\x8b;x\\xba-=$\\x11D<\\x8ex\\x8c\\xbd\\xe5\\x8d3=\\x9d\\xf7\\x0b\\xbd\\xa3\\xb6\\\"=T \\xa1\\xbb\"\nHSET bikes:10078  model 'Calypso' brand 'nHill' price 2152 type 'eBikes' material 'alloy' weight 15.9 description 'Urban riding, gentle off-road ebike. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\xa4\\xd0\\xc0\\xbc\\xa7\\xed\\x92\\xbb\\xf3J\\x0b\\xbd\\xaf\\xc7,=pk\\x95\\xbc\\xbd\\x9a\\x85<!\\xf5\\x94\\xbb.\\xcb\\x00\\xbd\\xa5\\x92k=\\x1d\\x0c\\xf5<\\xaf\\xfa\\xa5<\\xfev\\x89\\xbcU\\xed\\x15=k\\x025\\xbc\\\"n\\xc8=\\x0fc\\x1f\\xbcD?h=tJ\\\"\\xbd\\xea\\xf1\\xf8<\\x86\\x00\\x9f\\xbd\\x8d\\xc4\\xaf:\\xe9\\xf3Z\\xbd\\x07M\\xb9<\\xcfh,\\xbd\\x9fJ\\\\\\xbd\\x05\\xcb\\xb9\\xbb\\x91w\\x88=$Z\\xa5\\xbc\\x1b\\xfb\\x01\\xbd$\\xeb\\xde=\\x1bN\\x05\\xbd.\\xfcO\\xbd\\xb7\\xc0\\x80<\\xab\\xe2,\\xbb\\xbfk\\x10\\xbc\\xda\\xe2\\xa1;z\\xa7\\xff<[\\xcb\\xed\\xbb\\xf6-\\x8f\\xbc4\\xf3\\xdc;\\x95\\x92\\x8b=Rv\\xe4\\xbc\\xa6\\xfc:=\\n\\xefc<K\\x9bt\\xbc\\x92\\xae\\x17\\xbc\\xafk\\xd7=6\\xadz\\xbd<m\\xde\\xbcc\\xca]:n\\xbc\\\"\\xbd?\\xd1\\\"\\xbdQ|\\r\\xbd4\\xf8\\x83\\xbc*8\\xae\\xbc\\xc5\\x88\\xaf\\xbc\\xfd\\xeb\\x97\\xbcU|\\x89\\xbb\\x0b\\xb7\\xc5;\\x1bS}=Q\\x0fp<p\\x8b\\x80\\xbc\\x92\\xef\\xa2<E\\x12\\xca<N\\x1e\\x17=\\xc2\\x89\\x9a<S\\x07\\xe9;\\xeeq.=Q\\xd5\\x08=\\x9e:\\x19\\xbd\\xbd+\\xcb;\\xb80 =\\x8b\\x06\\x96\\xbb\\xe9C-<\\x07\\x85!\\xbd4\\xb3a\\xba\\x87>\\xad<\\xecwx\\xbc`\\xab\\x9b\\xbd\\xa7*J<R\\x88\\x1f=-f\\x1b\\xbd\\x10a\\x8a9\\xd1BO\\xbd\\xf4X\\x1a\\xbc\\xd2\\xe8#\\xbc\\xe7^\\xbf<\\x9a^\\xfb;\\xf7Ov=\\xacmw;\\xbd<{\\xbb\\xec\\xd9z\\xbd\\x80\\x9dP\\xbd9\\r\\x17=\\x1f=\\xc3<U\\xd6\\x1a<\\x00\\x84c\\xbd\\xbd-h\\xbd\\xefkB=\\xf7w\\xa9\\xbc\\xe9h\\xab\\xba\\n\\xe4\\xa3<\\xfat\\x10=\\x9d\\x80\\xe1\\xbc\\xae\\xd8\\x13<\\x93\\x97\\xba=\\x9bFS\\xbc>\\x92\\xde;\\xe2&\\xa4\\xbc\\x92n\\x89\\xbc\\x93\\xc9E\\xb9\\x1fS\\x1a=\\x02(r<\\x05\\xc0\\xca\\xbc\\xd7\\xe7G\\xbd:\\t[\\xbd\\xd9\\xd72\\xbc>\\xfc\\x19=*\\xd4\\xf3<\\xbc\\xf2n\\xbd\\x9d?\\x90\\xbc7-\\x1e<Y\\xbfH\\xbd\\xcdV\\xa8\\xbc5s6=\\x10V\\xb3<\\x88mN\\xbc\\xd0\\x14/\\xbd\\xc3\\x1db\\xbd\\xda]\\xab\\xbde\\xb6`\\xbd\\xd8\\xabi\\xbb\\x8dt\\x1b\\xbcR.\\t\\xbd\\xd0,O<\\xf4</=\\x9fM\\x01\\xba\\x1a\\x8f)\\xbd\\xa1\\xc5\\x1a<\\x8a\\xe0#\\xbdt\\xb2F\\xbd\\xa5\\x86F\\xbd<o\\x8b\\xbb\\x02\\x9d\\xd1=\\xdf\\x1f.:\\xb3\\xc2\\xeb\\xbc\\xf8\\xbd\\x03=\\x87\\xac\\xda\\xbbLc\\x91\\xbc\\x9dk\\xf2;\\x94\\xd5\\xfd;Q\\xf0\\x82=3\\xf5(\\xbd9\\x91\\xf6<\\x11\\x17\\x80\\xbd\\xb4z@<e\\xf2\\x0b=\\x13\\xf0\\xef\\xbc\\x0e\\xf9k=\\x8b\\x9cZ\\xbd\\x96K7=\\xb7-\\xea<\\xba\\x81\\xd2\\xbb\\x0f\\xe1(\\xbd3t\\xb0<\\xbd\\xb50=k\\xeb\\xbf\\xbcQ\\x08\\x97<\\\\k\\t=\\x80\\xcc\\x0f\\xba\\xc4\\xd7\\xcb\\xba\\xf3\\xfbB=\\xf9\\x15\\xdc\\xbb\\xca\\xe9)\\xbd\\x02Y\\x80\\xbc>d\\xb5\\xbb\\xa0\\x1c\\x1a\\xbdc\\xc1C=\\xa81\\xee\\xbc\\xfe=\\x80\\xbc8\\xday\\xbc$\\x98\\x08<\\xa5g\\x95<\\xe2\\xa1\\x05<\\xa10\\x03=\\x03\\xed\\x9c\\xbcR\\x89\\x99<D{\\xd8\\xbb@\\x80\\xb2:W\\x95X\\xbc\\x06\\xeeL=f8,=b\\xa5X\\xbd\\xa2\\x92{;%\\xee\\xc2\\xbb\\x8b\\xc6\\xa1<\\xff\\x89V\\xbd\\xc3:\\x0e\\xbd\\xda\\x13*\\xbd_\\xc15=o\\xbc\\xb4\\xbc<\\x9f\\x10\\xbd\\x8b\\x08p\\xbc3\\x9a\\x9b\\xbctO\\xe8:\\xfa<i=\\x89I\\x1c=\\xdaQ9\\xbd.\\xb4\\xe8<\\x97\\x98\\x80\\xbc\\xa9\\xdb\\x01\\xbdN04\\xbd\\x9c[\\x88=\\xf0\\xef\\xf9\\xbc*9\\xcf\\xbc\\xfb\\x14@<\\xcb\\\"\\xbf\\xbb\\xe3\\rc\\xbd\\x8ac\\x11<\\xcdy\\xa4\\xbdRt\\x86\\xbc\\x0e|\\x02\\xbd\\xfc\\xe8,\\xbc\\xf9Z\\xa9\\xbb\\xdd\\x9b6\\xbdLr\\x9a\\xbc\\\"\\x1e!=\\xa9x\\xb8\\xbc&\\xe2\\x84={\\x02\\xe2\\xbc-T(\\xbd\\xfa\\xda\\x93\\xbd\\xeeg\\x97;\\xb1\\xceM=,\\xe8\\xb0;\\x00q\\xab<l\\x88h<J\\nv=\\\"\\xe5o\\xbb\\r\\x81^<\\\"\\xee*\\xbdx&>\\xbc\\x94s!=\\xac\\x9e\\x81\\xba\\n\\xaf\\xc8<wG\\x98=\\x1e6[\\xbcY\\xdc\\xce\\xbdFp\\\"\\xbd\\xf0\\xbf\\x8b<.\\x1e\\xb9;\\x88\\x14\\x04\\xbdTg\\xc5\\xbbk\\xadv\\xbb+R\\x99\\xbd\\xd73i=s`\\x07\\xbd\\xb6Y\\xda<\\x1fP\\xad\\xba\\xf4\\x16\\x87=%\\xda\\xaa\\xbc\\xfa\\x9c\\xcd\\xbchkJ\\xbd\\xdf@\\xa5\\xbc]\\x8e\\x81\\xbc{\\xe1\\xb6=U\\xea:\\xbc\\x92\\x97\\xb9\\xbc\\x935\\xaa<\\xef|\\xe6;_\\x80d\\xbb\\xbf\\xba\\xf0;\\xad\\xb1\\xfb;\\tk\\xad<\\x96\\x10\\xc9\\xbc\\xc4p\\xbe\\xbc\\xf7vJ\\xbc\\x1c\\xc9\\xa5\\xbc\\xe3\\xcb\\xce=\\x94\\x80\\xad\\xbd\\x94\\x11\\xf4\\xbc4\\\"\\x1b<&\\xac\\xe9:\\x9fsA\\xbaM\\x88\\x08=\\xd1\\x18\\xc9\\xbb\\xef\\xb9\\x14=\\xf3\\x89\\x1e=q\\xd6\\xb0\\xbd\\xaaQ\\x84\\xbc{C\\xec<\\x88M\\x08\\xbd(f\\x85\\xbb\\xee;\\xdd\\xbb\\xe5\\x8f\\xe3<\\xb0LA\\xbd\\xd4\\xc3\\xa2\\xbb[\\x89\\xd6<U\\xee\\xe1\\xbcnJ<\\xbcm\\xfd\\x96<.g\\x93<Q\\xb2/=\\xdc#c\\xbd\\xd90\\x84\\xbc\\xe1\\xd8(\\xbcT8E\\xbc\\x8eV\\x12\\xbc\\x1e@\\x07=\\xec\\x04\\x15\\xbd\\xcfY\\xdc\\xbbx\\xc4\\x99\\xbd\\xc6:\\x84\\xbc\\xb7T\\x1d\\xbb\\x96O&= | \\xbda\\xf5>\\xbd\\xfdF\\x82=vfB=F\\x90\\x03\\xbd\\x11)1\\xbd\\xb4\\x8a\\xe0<>\\x9b\\xf5\\xbbS\\x87\\xd8:\\xfd\\xf9\\xc6<\\x14\\x17`\\xbd$N$<\\x8d\\x9d\\xda\\xbb\\x9f\\x91J=k\\xecY<\\xb2\\x1a\\x17=COb\\xbd9\\xc5B\\xbc\\x01\\x01;\\xbc\\xb2\\xf4-\\xbc\\x8dg\\xa1<\\xc4\\x16\\x87\\xbc\\x05\\xb02=a\\xeb\\xcc<\\xe1\\xe2\\x10=`\\x1e\\x92;\\x06\\x1f\\xc6\\xbb\\xca\\xddj=}\\xb2/\\xbd)9Q<\\xab)\\x12=`j\\x95=t\\x97\\x06=C\\xdfH=\\xd5_\\x11=\\xf0k\\x8a\\xbbKyL\\xbc\\xfd\\xe9)=y\\xed\\x06\\xbd\\x94\\xc5\\x8f\\xbc\\x8a\\x19\\xe5<\\x94v\\x18\\xbc\\xb1Q\\xa5\\xbd$s\\x9e\\xbc\\xeb\\x02\\x99\\xbd\\xad\\x81D\\xbc\\x1c?\\x10<9\\x84\\xb4<!\\xcc\\x1e=\\x01\\xb8S7s}\\x82=w\\xa2\\xa7\\xbc\\xfc\\x01\\xb6<\\xa2=\\xca\\xbcW\\xe5\\xe6\\xbc\\x02@a=\\xbf\\x15\\xb0\\xb9\\x1a \\n\\xbb\\xc5\\xab\\xf0\\xbc\\xc7\\x9a\\x17=\\x97\\x8b\\x88=\\xb6\\xd9\\xa5\\xbd\\x93\\x19\\xca\\xbc\\xf1\\x82Z\\xbc\\xab\\xac?=l\\xd6/\\xbd\\xf4K\\x94\\xbdkV\\xcc\\xbc5gD<\\xba\\x14\\x0b\\xbd\\\"8w<\\xeb`\\xa8<u \\xcc\\xbc\\xf5<\\x85=\\x8b\\x96G<\\xe6nl\\xbd\\xe2E\\xbf=+X\\xa7=\\x84\\xd3Y\\xbc\\xd7\\xe98\\xbc\\x9b\\xfe\\x12\\xbd\\x17\\x0b\\xa8\\xbd\\xeb\\xef\\x9b<\\xc1\\x97\\x1c<\\xd2\\x9e\\xbf\\xbcUF\\x13\\xbc\\x9c\\x87\\x9f\\xbd\\xec?C=\\xea\\x08\\xa9<5P\\xc9;\\x91Dp\\xbb\\x1dyO<<\\xb7\\xb4\\xbc &==Q\\x08\\xe5\\xbbf\\x16\\x8c=\\xee\\xf1\\x1e\\xbb;\\x10^;\\xd0\\xa6%\\xbc\\x8f\\xe7#\\xbc\\xcc\\xc4\\xdc<P\\x1c\\xc6<\\xa0\\xac!<\\x9fs\\xae<\\x16\\xbc\\xe3\\xbck\\x1dK<\\xa1\\x07\\xcc\\xbb\\xbbJ\\x04=E\\xbd!=\\xa4\\xb6-;{\\x10N\\xbd\\xc01C;\\x9a\\x88\\x04=\\xc8\\xba\\x9c\\xbc\\xca-\\x15\\xbc\\x8cL\\x84<\\x1e\\xdf)\\xbd\\x9dl\\x06<\\xd1\\x13\\xa1\\xbdoX<\\xbc\\xad\\xb5m=\\x88q\\x07=\\xe7\\\"N=\\xb1\\xda\\xf5:\\x1f\\x86g<\\xa1w\\x18=\\x83\\xe5n;\\x83\\xeb\\x98\\xbc~\\xf8\\x95\\xbcR\\x9e\\x18\\xbc\\x95_\\xf8\\xbb\\xf2d\\xc8<\\x14~\\x9d\\xbbO\\xda#\\xbds}D=\\xe9 \\x15\\xbd4\\x1a\\xad\\xbd\\xb6\\x1d\\x1a\\xbdH^\\x91\\xbc\\xeb\\x18<=\\xb3H]\\xbc\\x12\\xe6\\x9a\\xbbQ\\xaa\\x01\\xbdD\\xc7o\\xbd\\xd1(\\x1f=\\xebI|=\\xc4\\xa2(\\xbc~cV=\\xf2\\xbfY\\xbc/\\xdd\\\"\\xbd\\x94\\xee\\x12=W\\xd0\\xdf<k.F\\xbc\\xc5\\xad\\xb2<@ba=^;z\\xba\\xc2\\x87k<\\x8c\\xfa\\x13=I\\xbd\\x9b\\xbb\\x15\\xdc\\xa0<FUg<\\xea(\\xec\\xbc\\xc7VO\\xbd;\\xfa\\x17=\\xea3+\\xbc\\xf9\\xf00= x\\xb3\\xba\\xd0\\xa2p;~\\xae:=7\\\"%;\\xfe\\x9d\\xdb\\xbd\\x8ek;\\xbd\\xc5\\x18\\xe6\\xbc\\xe9\\xcf\\x14\\xbd6\\x97\\x12=[s\\x0e\\xbd\\xce\\x17\\xb3\\xbcT\\x19\\xcd<\\xe1\\x17\\xe5;V\\xa3c\\t\\x03:\\x97<w\\x1c\\xd7;\\x10\\xe7\\x9d;;\\xed\\xe3\\xbc\\x02_\\xdc\\xbbe\\x8e$=\\xa0\\xbd\\xae\\xbc\\xb6\\x93\\xbd\\xbc\\x04)/=Cu\\xcd\\xbc\\xf6l^\\xbc_b\\xd3;\\x05\\x19\\xbd\\xbc\\xa8\\xdfI=\\xc8\\x1e\\xb6\\xbcy5%=f\\x98s\\xbd\\xc0R\\xfc\\xbc\\xbb\\xeac\\xbb\\xc5Vs=\\xc6\\xe6\\xd5<?&\\x82;\\x94\\x03\\xd4<\\xbc\\xb3S={B\\x0c\\xbc\\x847\\xa0<&\\xb9\\x9c<\\x02\\xe8\\xf7<\\xbfL[\\xbdP:(\\xbd\\xd0\\x7f\\xa2\\xbb~\\xf8I<\\xfc\\xaaA\\xbc\\xcb\\xd9\\xc9\\xbc\\xdb#q\\xbb\\xf5\\xbc\\xd49\\xd6\\xff\\x18=\\xca\\x14\\x8d<\\xd6T.=\\x12\\xc6\\x83<\\xd6\\xdf\\x7f\\xbc\\xbaM\\x1b\\xbb\\xbc\\xac\\t=\\xe5WP\\xbcn\\xbd%=\\x15\\xfd\\xe3\\xbb\\xee\\xd4\\x03>\\x1c\\xc1\\xaa;\\x94d}\\xbc(\\xd7\\x02<\\x9b\\xf5Y\\xbdB\\x01\\xae\\xbd\\xfa\\x16\\x07=H]\\xd9<\\x07\\x96\\xc7\\xbb\\xff\\x00.=G\\xebu\\xbd\\xb20Z<\\x00\\x81\\xac\\xb9~=\\xe7<\\xd5^\\x81<\\x99[4\\xbc\\xf0\\x9f\\x01\\xbc\\xe2\\xc9\\xfe<\\xfd\\xf9\\xc4\\xba\\xd8qi\\xbd\\xe3\\x00\\xc8\\xbc\\x13vJ\\xbc\\xba|\\xab<Is\\x99\\xbc\\xc1\\x1a\\xb5<\\x93\\x8e\\xdf\\xbc0<\\xca<f|K=\\x1b\\xd8\\x9f<{\\xeb\\xf1\\xbaY\\x17\\x10<u\\xd7\\x1d=\\xb4\\x14:<F\\xdf\\x10=\\xb1\\x19\\xc9\\xbc\\x99S\\x88\\xbaP\\x8aN\\xbd*\\xb4+=\\xc5\\xa0)\\xbdx\\xa6K=\\x03f\\x11=\\xcaY/=Gp+=C)\\xa7<(p\\x06=\\x99\\x90\\x86<\\x81\\x89==\\x02\\x14\\xc9:\\xfbM\\x00=\\xddV\\x1d=\\xf6a\\xdc\\xbc\\xdf\\xec\\xe9\\xbb>_\\x1d<\\x10\\x9f\\xac;rW|\\xbc\\xd6\\x93\\xe5<\\x1cpi<C \\xee\\xbck<\\x10=\\x0e\\xcd\\x17<6\\xc4\\\"<\\xcc\\x02\\xc7\\xbbIQJ\\xbd\\xd8\\xa1\\\"<DP\\x8d=t\\x82\\x1a\\xbdx\\xe5\\xb7\\xbc\\xc9\\xc3\\x9d=\\xd0Fr=\\x16*\\xa1<F\\x91v<\\xbe\\x8c$\\xbd\\xb3\\xa6\\x15\\xbd\\xf1\\xcf \\xbd\\xf2\\xa0\\x08=\\x0c\\xf8\\t=\\xed\\xb9(\\xbc\\x88\\xa2\\xff:\\xa0\\xa0R<\\x98\\xc6q<7\\x07\\t\\xbd/\\xf3t<\\xd9#)=,\\xdf}\\xbdA\\x90M\\xbd\\xe9\\xd4\\xa1\\xbd\\xd2\\t+<[\\x9e\\x88\\xbb\\x07\\xd9\\x89=\\xd9h\\x06\\xbck\\xaf\\xe5<G)j\\xbd\\x87\\x89\\x19\\xbc\\x84&\\xbc\\xbc\\xe2\\xa5\\xeb\\xbc\\xb5>\\x90\\xbd\\xe3FT\\xbc\\xfd:\\xb0;\\xdc\\xc0\\xfc\\xb9\\xa6,>=\\xa8\\xf4D=\\xadB(;w\\x99\\x1e=_\\x8d\\x1b=\\xba\\xd2@\\xbc$B\\xa6\\xbc\\xd4\\x13\\x9a\\xbdt\\xef\\xf6<+\\xb9P<\\xa8n\\x9f<\\xea\\x1e?\\xbd|\\xca\\x99=\\x88\\xdaZ<\\xb0\\xee\\x81:!Uw<\\xc4\\x08o:\\xce\\x07A\\xbcv\\xf4\\n<k\\xf7\\xfc\\xbc~d\\xd9\\xbc\\xe3\\xbeM\\xbdO8S\\xbc\\xd1:\\xa4<\\xfe[\\xd6\\xbc\\xb2\\xc2C<RG\\xd1\\xbcs\\xd6\\x02;\\xc6r\\xdb;\\xef\\xfbg\\xbbX\\xc41=\\xfe\\xa9\\x01\\xbd\\x96@\\xe7\\xbbI9\\x12=\\x99\\xea\\x1c\\xbd|\\xea\\x8b\\xb9\\x1c\\xe5\\xe8\\xbd\\xe9\\x9fs=\\x15\\x1c\\xd6\\xbc\\x7f)e<\\x0b\\xc3R\\xbd%dL<\\xf7\\x8fM\\xb9w[\\x93<^Y\\x83<\\xad:\\xd0\\xbc\\xd1\\x00F<#\\x83/\\xbd\\x9e\\xdb\\xc7\\xbb\\x9b\\xee\\x93\\xbd\\x06\\xb3\\r\\xbd\\x98\\xee$\\xbc\\xa6]\\xa7<i\\x9f\\x9c\\xbc\\xffo\\x02=\\x95\\xf4\\xcb\\xbc\\x0eV~\\xbc\\xbb\\xfeH\\xbb\\xe1e\\xa4\\xbd\\xbcTd\\xba\\x83\\xe0$<\\x16\\x81.\\xbb\\x9d\\x9e\\x16\\xbc\\xadCl<.J\\x1c\\xbd\\xd7\\x8e\\x0c<\\x13\\xfe\\x82\\xbc\\xa0\\\"V=\\\\3\\xc8\\xbc9\\xcf\\xf7\\xbcy\\x8b\\x14<\\xad\\xa4\\x1a=\\xc1j\\xea\\xbcs.P\\xbdVD\\x1a\\xbd\\x80\\x9f\\xe5<\\xc0<%\\xbdm\\x88\\xfa<\\xb8\\x84\\xff<\\x1c\\x01+=\\xff\\xc3R<\\x7f\\xe20=\\xb6|\\x86\\xbd<\\xdf\\x8c\\xbdo\\x93!=.\\xe9\\x9a<\\x01\\xfe\\x0c<\\xdb\\x8f\\xed\\xbc\\xce\\xf2\\x99;L\\x07.=\\xb9A6;\\xbew\\n=\\x0f\\x06c=\\xe4\\x81B<\\xc5v-\\xbd\\x8fh\\xd2\\xbb1\\xdc\\x1c\\xbb\\xcb\\xb6\\x81=\\xd6m\\xfd<UoL\\xbcts\\x02=U\\x94\\x02\\xbd\\xc6hA\\xb8\\x0bQ|\\xbc\\x9f0T\\xb9\\x86=\\xa0<\\x98~\\xac\\xbcQ\\xbe\\\"<J\\x9c\\x97= \\x8f\\\"\\xbd\\x9f%\\x01\\xbd\\xb7\\xff\\x18\\xbd\\x92\\x17\\xe6;\\xc0\\xd2z;\\x95\\x0b\\x85\\xbd\\xaa\\xed\\x8f<\\xb5\\x0c\\xf5<\\x99*#=\\xf99\\xfe<L\\xc5\\x88=m\\xd6G\\xbb\\x05\\xf1\\xcb<\\xe0s\\xca=[;\\x9c\\xbc2A\\xf4<K\\x151\\xbb\\x9e$5=\\\\\\xf4\\xb3\\xbc\"\nHSET bikes:10079  model 'Titan' brand 'Breakout' price 4822 type 'Commuter bikes' material 'alloy' weight 16.0 description 'A real joy to ride, this bike got very high scores in last years Bike of the year report. At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\xee2\\x8b\\xbcE\\x93\\xd9;\\xd9\\x0fR\\xbcU\\x9f\\x16=\\xc55k\\xbc\\xed\\x99L=0\\xaa@\\xbc\\xe8\\x93\\xdb\\xbc\\xc7fZ=2F-=;-5=\\x05R\\xd6;\\xc0x\\xdc<5;\\x96;\\xf8\\xa2\\xab=-\\xc7\\xea\\xbc+)j=\\xf1\\x9c\\xfd\\xbc\\xfb\\xd7G=\\xc4\\xd6|\\xbdB\\xe7\\x8e\\xbb?\\x1c+\\xbd\\xb1\\xb91=Ap\\x7f\\xbd\\xf4\\xbeZ\\xbd\\xd0_\\x9d\\xbb\\xcc\\xd3\\\\=[\\xc2\\x08\\xbc*TC\\xbde\\xd9\\xd9=yFD\\xbd\\x94\\x95\\x13\\xbd@\\\"\\x81<\\xe60\\\"\\xbb]\\xcf\\x9b\\xbcA\\x85\\xb2<[Y`<\\x14\\x0bO\\xbcD\\x93\\x1d\\xbd:_+\\xbc\\xc3+\\xa1=c\\x9c\\xbe\\xbc\\x13\\x8a}=\\r\\xe4\\xae<1BV\\xbc.\\xb8\\xaf\\xbcQ\\xb9\\xb3=a\\x924\\xbd\\x15\\x0fr\\xbcu\\x87\\xda\\xbb\\x8ax\\x19\\xbd\\t,1\\xbd\\xd7\\xcc\\x98\\xbc\\x18\\xed#\\xbc\\xc6v\\x1c\\xbd\\xb9\\xa8\\x92\\xbcP -\\xba\\xea\\x08\\xc5;e\\xec\\xea\\xbch\\x0e\\x84=\\xcbj\\x92;\\xc4u\\x18<\\x86I\\x86<3\\x9c\\xde<\\x06\\xf2\\xe2<\\r/\\x92<\\xb1\\x9c\\x0e:3\\xfe\\xfa<o\\x80\\x9e<\\x99|+\\xbd\\xe6\\xa5\\xb2<\\xb0nP=\\x98\\xbc0\\xbc6U\\x94<~\\xb78\\xbd\\x08.\\t\\xbc\\xe8\\xdf\\xc1<[B\\xba\\xbc\\xaf\\xac\\x9c\\xbd\\x91wX:;\\xae\\xc5<\\xe1\\xd3\\x14\\xbd\\x032\\x94<\\xc8\\x18\\x94\\xbd\\xad\\xd23<A\\xd7\\x05\\xbcI\\xeb|<\\x01d=<\\x93\\x04$=\\x8d;<\\xbaK\\xc7\\xaa\\xbc\\x9e\\xeb\\x1e\\xbd\\xe1P_\\xbd6\\x91\\xcb<|\\xb8\\xf5<\\xdf_\\xcb:\\x81p\\x1d\\xbd\\xfb\\xbe\\xff\\xbcA\\xc3G=3e\\xb1\\xbcb_E\\xbb\\n\\x9e\\xd5<\\x1d\\xff\\x06=\\xbb\\x1b\\x12\\xbd\\xaf1\\xb8\\xbb)0\\xc1=\\xb0,e9o\\x9f\\xa0\\xba8e\\x10\\xbb\\xe3\\xc3\\xa3:\\x93\\xcep\\xbc\\t\\xf0G=\\xc9U\\xdd\\xba\\x9f\\x98\\x8d\\xbc\\\"*I\\xbd\\x9a\\xa0;\\xbd\\xddl`<\\x86\\x16X<n\\xf9&=VW\\x1d\\xbd\\x0f|\\x02\\xbdV\\r\\xeb<O\\xf3{\\xbd\\xfc\\xb1\\x17\\xbd\\x83\\x1b@=\\xba\\xdd2\\xbb\\xd4i\\xf0\\xbc\\xa4@8\\xbd\\x02\\xed$\\xbd\\x99@3\\xbd\\x1c\\xd4\\xd2\\xbcC\\xc3\\x12\\xbc\\xdd@t;\\xac`\\x02\\xbd#\\xe6O<)\\xc9\\x05=\\xb7z\\xa6\\xbb\\x16\\x03\\xa3\\xbcK\\xd4\\xe4;n?\\x16\\xbd\\xe3}\\\"\\xbd\\xaa\\xdb1\\xbd\\x18\\xe7\\x95<\\x18\\xf2\\xe0=u\\xc9\\xd2<\\x9e^\\xa4\\xbcB\\xdd\\xd7<\\x93}\\x82\\xb9\\r\\xec\\xa7\\xbc\\x92i\\xca\\xbb\\xf5^*\\xb9\\x06VP=\\x15\\x9f\\xfc\\xbc\\x13\\xd5\\xd1<y\\xe1\\x8d\\xbd6xi\\xbbd.\\x1b<\\x1e\\x1dC\\xbdv5L=\\xeb\\x17-\\xbd\\x18_$=\\x80/\\xb9\\xbb\\xb1\\xf1\\r\\xbd\\xdd;\\x88\\xbd(\\x13\\x1b<\\x8b\\xce?=\\x01n\\x06\\xbd\\x8bY\\xb9<\\xeb*==\\xa1<\\xa8\\xbc\\x06\\xa9\\x8f<u\\xaf3=\\xc9\\xa7`\\xbc\\x85t\\xe4\\xbc8b\\xa6\\xbb\\x10\\x8f\\x10<\\x10\\x81\\xeb\\xbc\\xb8e\\x80=\\xafl\\xe5\\xbc ,}\\xbbvv\\x0f\\xbdn?N<\\xa8f)<\\x1a\\xccS\\xbcY@==7E\\xc4\\xbc\\x87\\xc2\\x1f=[\\x82\\x91\\xbbM\\xe3\\x1b;\\xac\\xb3\\x87\\xbc\\xcb\\xe2N=\\xbe\\xfay=\\xed\\xb6\\x05\\xbd\\xf4,\\x07=\\xa1\\xf8\\xb6\\xb9\\xa2\\x16h<\\x10j\\x80\\xbd{P\\x00\\xbd\\xb5*\\x12\\xbd6\\xb0$=q\\xc7\\xc0\\xbc\\xf2\\xbbR\\xbd\\xe9s\\xd4\\xbbt\\x12G\\xbc\\t\\x83=;2\\xed\\xcf<J\\x93P=\\x18LG\\xbd\\xe7`\\xb4<\\x9bN\\xe1\\xbc\\x8b\\xa0\\xac\\xbc/8\\x81\\xbcm\\xb8r=\\xf4\\xbb\\xac\\xbc\\xd9\\x99\\xe5\\xbb\\xaa\\xdf\\xd5;r\\\\b\\xbc4C\\x81\\xbdF\\x8a\\x12=#\\x9b\\xda\\xbdTLk;\\xbe\\x14\\x81\\xbc\\x9c\\xdc\\xbd\\xbc!\\x02\\xd6\\xbb\\\"\\x9e\\x01\\xbd9\\xbd\\x18\\xbc$\\xa6\\xfd<\\xa1\\x94\\xe4\\xbcI\\t\\x8c=^\\\"\\x04\\xbd\\r<\\x0f\\xbd\\xad\\xdc\\\\\\xbd\\xe9*\\xa6\\xbc4\\x0f\\x8d=\\xa0A\\xad\\xba9\\xd9\\x14=\\x80dS<\\x0e_i=\\\"\\xd6\\xc7\\xbb\\xe6;\\xbe<x\\xbf\\xae\\xbc;\\x9d\\x1b\\xbcv\\x96F=\\nUR\\xbb\\xc2\\xde\\x89<\\xe2p\\x9e=\\xde:K\\xbc\\x13\\x17\\xc6\\xbdc\\xd3\\xa8\\xbdY\\xb1S;\\x08U\\x1d\\xbb1+\\x83\\xbc9\\x98\\x85\\xbb\\xef\\xde\\x1f<\\x98\\r\\x9a\\xbd\\xa8+J=\\xfdE\\x9a\\xbcC\\x11\\x83<\\xf3({\\xbd\\xbd\\xe2\\x7f=\\xe2\\xb4\\xb8\\xbc\\xe5\\xa8\\xc0\\xbc\\xb5\\x1c\\x01\\xbdC\\xe1\\xb9\\xbc\\xa6T$\\xbd\\x86\\x89\\x98=\\\"J\\x9b\\xbcF6\\x10\\xbd\\xfc}\\xbd<\\xff\\x02\\xf0<>1\\xa2;6?\\xab;\\xa4\\xbb\\x03\\xbca\\xd3?<\\x83\\xdd\\x00\\xbd\\xdc\\x8c\\xc3;q\\xf4#\\xbd\\xc0\\xff\\x1d<$\\xbe\\x81=\\xbd0\\xd0\\xbd)}\\x03\\xbd\\xd7\\xa1J;\\x9b\\xe9\\x15\\xbd\\x1e\\xbf\\xf6\\xb9\\xed\\x9b\\x01=\\xf9\\xf6\\xa3\\xbba\\x02\\xa9<\\\"\\\"%=t\\x00\\x97\\xbd\\x94`\\x88\\xbb\\xe4:\\xad<\\xfb\\x1a\\xc7\\xbc\\xa4\\xab\\xba<Z\\x9b\\xe7;5\\xee\\x8d<\\x00\\xcf\\x1b\\xbd\\xfe4\\x93<A\\xf6\\x01=\\xdcB8;\\x9e[\\xd1\\xbc\\xf1\\x87\\xb4<\\xc8\\xf0\\xa9<\\xf5\\x16\\xfd<V\\x91\\\"\\xbd\\x80\\xce^<\\x06\\xbeN<\\xd3\\xd9\\x05:,Rq:\\xfcx\\x95<\\xdb\\x95\\x11\\xbd\\xfc\\n\\xf3;\\xc67\\x8a\\xbd\\x8b\\xf8S\\xbc\\x8d\\xb2?:\\xa6a\\t=a*)\\xbd\\xca\\x9f1\\xbd\\x1d\\xff\\\\=\\xe18W=\\x06\\xc6\\xa2\\xbc?1\\x0c\\xbdmI\\x80;\\xf5\\x0e\\xab\\xba\\x11W}<\\xfc\\x7f.<\\xce\\xbbl\\xbdA0\\xd4<\\x92.\\xb4\\xbc\\x04\\xa2\\x15<L\\xf4W\\xbb38\\xbf<\\xd3\\xe2\\x95\\xbd\\x98d\\x05\\xbb\\x9d\\xa7\\xdf\\xbb\\x17\\xf0`\\xbc\\xf8\\xc4\\x07<\\xbfh\\x18\\xbd\\xa7\\x1b8=\\xd6\\xb3y<\\x87f\\xd4<\\xdb\\x836\\xbc\\x12,\\x1e\\xbcBS\\t=8\\x02q\\xbc\\xc7*\\x07={\\x89\\x1e=^\\xbd\\x87=)\\xb3*=}\\xbcu=\\xda\\x80\\xb3<\\xd0\\xf1\\x8c;L\\xd1g\\xbc\\x85\\xb8\\x97=\\xea6\\xa2\\xbcp\\x1c\\x06\\xbd5\\xd7\\x0e=\\xd5\\x9c\\x8c;\\xb4+\\x8e\\xbd\\xd4\\xa8\\xa4\\xbcYa\\xc2\\xbd\\xfd]\\xc1\\xbct-\\xd2;B\\x99\\x81\\xba\\xbc\\x90-=\\xb0\\xeax<f\\x1aQ=0\\xa2\\xc6\\xbc\\xfaL\\xa5;y\\xef]\\xbd\\t\\xf1\\xf5\\xbc\\xbdC\\x83=\\xe8r\\xf6\\xba\\x94\\xa6\\x8a\\xbc\\xb6\\xb8J\\xbb3\\xe8\\x9a<\\xa7\\x8fj=\\xc8y\\x89\\xbdhS.\\xbd\\x03E\\xfb\\xbckj\\x0c=\\xfc\\xe5\\x1f\\xbd#\\x8f\\x80\\xbd\\xb0\\xf9(\\xbd\\x14\\xba\\xa6;\\xb5\\xa3\\x13\\xbd\\xc7\\x8a\\xa28\\xb53\\xcc<\\x857\\xed\\xbc\\xbd\\x15|=\\xfe\\xd7Y<\\xfd\\xe6v\\xbd\\xae\\xd8\\xb3=\\x12\\xfe\\xa1=V\\xc3\\xf1\\xbb\\xe9\\x91\\x1a\\xbc\\x0e\\xdam\\xbc\\t`\\xae\\xbd\\xb7\\xfb\\x98<\\x0f3\\xdd<\\t\\xf4\\x14\\xbd\\x87$\\xd6:\\xc3n\\xae\\xbd\\x06\\xed;=\\xc6\\xb6a<\\t\\x15>\\xbb\\xef\\xdc\\x8c\\xbb\\x99\\xed\\xea<\\xa4\\xd1\\xbf\\xbb\\xa3\\xc1\\x05=\\xb5\\xe8\\x08\\xbci\\xdfk=)\\n\\x04=\\x01o\\xcb\\xba9\\xdb\\x9f\\xbc\\xe9\\xd0o;#\\x85\\x1a={\\x05\\xa7<\\t]\\x04\\xbd\\x8e\\x90I<\\xf8\\x80\\xbf\\xbc\\\"c\\x8d<\\x17\\xf8)\\xbc\\xd6p\\x11<\\xdf\\x87\\\"=$\\xbc\\xd2;{\\x03\\\"\\xbd,v#<Zt\\x1d=V\\xdf\\xbc\\xba\\xec\\xe0&\\xbc\\xda^\\xca<~4\\x19\\xbd\\x0b\\x16\\xd0\\xbbhfs\\xbd\\xe3\\xa9\\xb2\\xbb\\xeb\\x86S=\\x1f\\xb4A=\\xc0\\xe9\\x14=\\xbb\\xae\\\"\\xbc\\r\\\\%<\\xc7\\r@\\xbc\\xddh;<l\\xbd\\xee\\xbcs\\xa2\\x06\\xbdBL\\xad<5.\\x1e\\xba\\xb94\\\\=\\x9b\\xc2\\x93;\\xb3\\xec\\x15\\xbd;h8=\\x13\\x95\\\\\\xbd\\xa6]\\x8f\\xbd1>\\xd2\\xbc\\xe2\\xe6b\\xbd\\x84\\xc6\\xe3<\\x84\\\\\\x1a;8\\xecK\\xbb\\x9c\\xbc\\xc5\\xbc!\\xef.\\xbda\\xaf7=\\xee8T=\\x11\\xb7\\xf3\\xbb\\xf5\\xae~=1\\x01o\\xbc\\xba,\\x96\\xbc\\x89\\xdf\\xdc<\\xcb\\xf2\\x0e=\\xbdN\\xbf\\xbc$_O<\\xfeyi=k\\xeaB\\xbc\\xad\\xa9\\x86<\\r\\xd0\\x16=\\xbc\\xb2,<\\x1f\\xfbo<\\xea\\x90\\xd2<\\xac\\x17^\\xbd\\xdf[\\xa3\\xbc\\xeb\\x80\\xc9<t\\xdd\\xcf:q+\\xf5<9\\xbc\\xc9;\\xe4Y\\xa7\\xbc\\xadK\\x1e=\\xd4\\x14B\\xba\\x19Y\\xce\\xbd\\xb6\\xf5\\xf9\\xbc#\\x90\\xae\\xbc\\xdb\\x04\\x04\\xbdq2\\x08=\\x94\\x86B\\xbd\\xdb\\x0f\\xa4\\xbc\\xc8b\\xdc<-^\\xfa;\\x1b6Y\\t\\xacP\\xdb<\\xb9I\\xb7\\xbcG\\xe4\\x16=\\xf3\\\"\\xad\\xbb\\xaa\\x17=\\xbc\\xa2\\xf2}=\\xad[\\xc7\\xbc\\x9fM\\xbb\\xbcu+\\x14=pM\\x80\\xbc{\\xd0\\x1e\\xbb\\xb1\\xb7\\x81<\\xc7\\xc1\\xa6\\xbcW\\xe8T=\\x06O\\\":\\xf2\\xba6=\\xd1\\xf7\\x9b\\xbd\\xe3\\xd8/\\xbd\\x83\\xba\\x85\\xbb/0g=\\xab$F<\\xdd\\xdd\\xd1\\xbc\\xbc\\xb11<\\xde\\x10\\xee<\\x89g\\x95;\\xfe\\xb5\\xd5;cJ\\x8e;\\x85n\\x0c=\\xb9\\x06\\x13\\xbd\\x13\\xd1\\xf0\\xbc\\xd9@2<\\xd1B=<H\\xb3\\xb5\\xbb\\xcd*\\x95\\xbd\\xf0W\\x9b\\xbc\\x03\\xb3\\xce<\\x15\\x1b\\xfd<\\xbc\\xfe\\x10=c\\xa5K=\\x10i\\xe5<\\xfeS\\xff\\xbc\\xd0l\\x19\\xbb\\x0f\\xd2N=\\xcc\\xee\\x96\\xbc\\x89\\xd9\\xd5<\\x08%?\\xbc\\x9d\\xdc\\xfe=n\\xf1\\xcd:\\x8cg\\xea\\xbc\\x8a;\\x94\\xbc\\xf5B=\\xbd\\xbc\\x17\\xb5\\xbd\\xd9R&=\\xb6\\xec\\x03<}\\xfb\\xab;\\x0ez\\x1a=l0q\\xbd~?\\xb3<\\x83&\\xb6<\\xf5\\xc4J=\\xbd\\xf3\\x89<V3\\x05\\xbc\\xc13l\\xbcN\\xad2=\\xdfj\\xc0\\xba\\xa1G\\x82\\xbdx\\x99\\r\\xbd.\\n+\\xbc8\\xff\\xb9<\\x8e\\xccw\\xba\\xfa\\x1f\\x0c=Z\\xc2\\x01\\xbda\\xa3\\xef<\\xb3\\xfd\\x1e=b\\xd1\\xa8<,!\\x80\\xbcn\\xce|\\xbb\\x1f\\x01\\\\=N\\xc1b\\xbc:\\xc5\\x0b=v\\xc1\\xe8\\xbc6\\x04\\xce\\xbbh\\xb4I\\xbd\\xf4\\xeaR=r\\xc7m\\xbd\\x87\\xd6\\x19=\\x1f\\x9b\\n=!\\t\\x83=\\r\\x1b\\x96<\\x88\\x93Z<\\x87\\x97\\xd6<\\xf8=\\xa0<\\xe6\\xab\\x17=\\xc7\\xe8\\x10:\\xd2 B=\\x99\\xb6N=\\x19sq\\xbc\\xb3\\xc5\\x11\\xbdj?\\xd2;?v\\xab<\\x85\\xe1!\\xbd\\x80\\xc6\\xe0<\\xa7EC:\\xf6\\xd7\\x00\\xbd\\xf7<;=\\x95/\\xe8\\xbb\\xe7>\\xce;_\\x8c&\\xbc\\x8d\\x9e.\\xbd\\xadV\\xb4<\\x18\\xa9\\x99={0G\\xbd\\x87Y\\x04\\xbdI\\xae:=\\xb4\\x0c\\x8e=\\xc5\\xdc@<\\x15\\x13\\x91<2u\\xa9\\xbceb\\x0e\\xbd\\x8d\\xf5*\\xbd*\\x07\\x05=\\xbf^\\x9c<\\xb6\\x9d~:\\x94\\xa8\\xfb\\xbb\\xd9\\xa2\\r=\\xf2\\xd8\\x9f<\\xa4\\xe1\\xd6\\xbc\\x10\\x1c\\x1c=\\xd6\\x9b%=2\\x073\\xbdVzZ\\xbd/\\xc2p\\xbd\\x0f\\x16\\x1d=kA-\\xbc\\xddL\\xab=\\\"\\xb3g\\xbc\\x86\\xeeC=\\x9d\\x805\\xbdx4\\x90\\xbc\\xbb\\xe8S\\xbc\\xbb\\xbc\\x86\\xbbGK\\x9b\\xbd \\xa3\\xc8\\xbblR@\\xbc[\\xe0\\xbe<P\\x07\\xb7<^}\\t=3\\xca\\xc9\\xbaV\\xb1\\x0e=\\x87\\x19\\xf3<\\x05\\x0e\\x9a\\xbc\\xd5\\t\\x13\\xbdZ.A\\xbd\\xa18:=\\xe7f\\x85<\\xea\\xde\\x98<\\x1b>\\xee\\xbcqb\\x98=\\xd2\\xa1\\xc2;\\x9c\\x8f\\x1c\\xba\\xf8\\xca\\xb9<\\xa3\\xda\\x96<\\x8d\\x93i\\xbc\\xdc\\x85@\\xbc\\x9a\\n\\x18\\xbd\\x8e\\xcd\\xa9\\xbc\\xda\\x82Z\\xbd|f\\xf3\\xbcUX\\x03<`\\xd5U\\xbd\\xe6\\x8a\\x1e<\\xbd \\xee\\xbc\\xda\\xef);\\xd6/}\\xbcK6\\xfd\\xbc\\xe4\\x82*=*\\x96\\x04\\xbdb\\x91\\xe9\\xbbY@{=\\xf0B\\xc5\\xbc\\x9e\\x86Q\\xbbW\\xb6\\xce\\xbd\\r\\x93b=l\\x9dj\\xbc\\xc5M\\xad\\xbb&\\xd5V\\xbds\\xc6o<\\x1c\\xac\\x9c\\xbcr\\x9a\\xe3<Q\\x12v:\\x90\\x973\\xbd\\x9c\\x0f!=\\x18\\xc0\\x18\\xbd\\xcbg\\x94;H!\\x99\\xbd\\x86\\x96\\x05\\xbd(\\x01\\x94\\xbc\\xe6+\\xa7<$\\x99\\x14\\xbdCG\\xae<\\x1dx?\\xbd\\xb2\\xd0\\x8c\\xbc\\xea\\\\H\\xbc\\x99b\\xa7\\xbd@\\x02\\x89\\xbc\\xe3\\xd6\\x7f;\\x8e\\x8d%\\xbc%\\xde\\xd2\\xb8\\x963\\x0c;\\xc5\\x80a\\xbd\\x13\\x99\\x04<\\xe8\\\"\\xab\\xbcf\\xf4\\xc8<\\xfbC\\xca\\xbcA\\x8a\\xfe\\xbc\\x18\\x85\\x14\\xbbD+\\x03=dJ\\xaf\\xbc\\xa9\\x0b\\xac\\xbd\\\"\\xeeX\\xbdi]B<\\x0b2\\x0c\\xbd\\xab\\x06\\x07=#t\\x0f=\\xfb\\x8dS=\\x17\\xd3Z<r\\xacF=\\x7f\\\"@\\xbd\\x8c\\xab\\x8d\\xbd\\xfdr\\x06=<V\\x97<\\x02\\xeb,=\\xe6\\xe1\\x16\\xbd\\xa2e\\xfa\\xbb\\xaf\\xdd\\xd3<\\xf3\\x89\\n\\xbd\\xca\\x93\\xf1\\xbam\\xbf\\x0e=<N\\x90\\xba\\x82U\\x8d\\xbco\\x9f\\xde\\xbb\\xb2\\x19-<\\x0fx\\x89=U-\\xdc<\\xae2\\x15\\xbd\\xab\\x82.=\\xd8\\xc1\\x90\\xbcqv\\xf1\\xba/z\\x04\\xbcv\\xf61<KY[<f\\xc4G\\xbc{\\xda\\x0c<\\xe4\\x06\\x7f=\\\"\\xf4\\xd2\\xbcL\\xf7\\xfa\\xbc\\xa5Y\\xa6\\xbc\\xdc\\xcd\\x88\\xbc\\x90\\xb0\\x16<\\xc5\\x9bf\\xbd\\n\\xa2\\x85<\\xcf\\xb7\\\"=\\x11Lt<s\\x1c)=\\xcd\\x81\\xb5=Y7y<\\x80\\xa5\\x1c=\\xba\\xc8\\xd2=O\\x0e\\xa0\\xbc\\xd5\\x8fu<\\x9bi\\x90;{X\\r=\\x81\\x83\\x9b;\"\nHSET bikes:10080  model 'Ariel' brand '7th Generation' price 1845 type 'Kids mountain bikes' material 'aluminium' weight 12.9 description 'This bike is an entry-level kids mountain bike that is a good choice if your MTB enthusiast is just taking to the trails and wants good suspension and easy gearing, without the cost of some more expensive models. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\x845\\x8a\\xbc\\xc5\\xef`=D\\r\\x1c\\xbd\\x03\\xa1\\x06=\\xe1W\\xeb<A\\xc8\\x1a=c}\\xed\\xbc\\x06s!\\xbdCP\\xb5=\\xb1\\x06\\xd6<\\xb4z\\xb6\\xbc\\xed\\xb9\\xa4<\\xb1\\xd3\\xf4<\\x10b\\xfc\\xbb\\xc5\\x03)=\\xf19\\x14\\xbd\\xf6\\xba\\x05\\xbc\\xaf\\xb6V\\xbd\\xfdB\\x97\\xbc\\xf2\\xa9\\x85\\xbd\\xb1\\n\\x08=\\xb2\\x8d\\x15\\xbd\\xc3\\xd71=\\xfd\\x11\\xff\\xbc\\xb8\\xf2.=\\x93\\x8a\\x8e<\\xd9\\xe8<<^(q\\xbc\\xb6)\\xff\\xbc>\\x10\\xfb<\\x0bF\\r\\xbd\\xec\\xc4\\x03\\xbdW\\x89\\x9f\\xbc\\x06R <\\xc7!\\xf1<]8c;S/!=\\xd4u\\n\\xbd{\\xb3\\xfc\\xbc\\x88\\xf8^:\\xf5\\x14\\x12=\\x8c\\xff\\x03\\xbc\\xa4c\\xf8<\\xff\\xe6\\x13<\\xd37\\x9f\\xbcL|\\xaa\\xbc\\xadw\\xa7=0\\xc9\\x18\\xbc\\xd2\\xa2\\xf4\\xbc\\x1f7 \\xbdl7D;\\xa0\\xad\\x87\\xbd,)\\xe8\\xbb\\xbb\\xb6|<\\xd6\\xd6\\x97;\\x0f\\xf3\\xd7<\\xdbF\\xda<\\r\\x96\\x8e\\xbd\\x80-{\\xbdB\\xec(=*I\\x15<\\xec\\xe28\\xbb\\x86,\\x8b=\\xc1y\\x9a=\\x8bQ\\x9e<\\x91\\xf5/\\xbc\\x8f\\xf8\\xc3\\xbb\\xd1\\xd9\\xa5<mz\\xe9<\\xef\\xd3g<\\xf9\\x91\\x1a\\xbb\\x1a\\xedB\\xbc\\xa6\\xf5\\x86\\xbd\\x04\\x87\\xd0<\\xb3\\xb5\\xf4\\xbc\\xf9\\xef\\x8a<\\x19!}<\\x04V\\xb0\\xbcM\\x19\\xa1\\xbd#\\xa3\\x86\\xbc\\xb2]\\xd8<\\x08\\x01\\xde;!S\\xbb<\\x9e\\x95\\xbd\\xbd\\xecK\\x95\\xbb/\\xfc\\xb9<\\x8c\\xb6\\x81<\\x83\\xe6\\xac<&*\\x06=1<y;;\\\"6\\xbb%\\xba\\xdd\\xbcj\\xed\\\\\\xbb\\x1f\\xb5w\\xbb\\x9e@\\xef\\xbbJ\\xc6\\x1a\\xbc\\xf7\\x15\\x02\\xbd\\xfb8[\\xbd\\xd7q/<u\\x85\\xba\\xbc\\x18\\xa4\\xb8<d\\xbfA<\\xd5\\x08\\xd8<c\\xbb\\x1e\\xbdA\\x8b&\\xbd\\xb3Q\\xbe=X\\x0c@\\xbb\\xcek\\x1c=\\x02\\x85\\x87<\\xde^\\x82<t\\xd4\\xcb\\xbc\\x85sU=\\xb9U\\xae\\xbc\\x0f\\xca\\xd4\\xbc\\x05\\x07c\\xbd\\xf7\\x03N\\xbb\\x1d\\x99\\x9c;\\x96\\xab\\xed\\xbczr\\x01=\\x81*/=%X+=\\x19\\x0f\\\"<\\xac\\xc8\\x90;\\xb0\\xfdC\\xbdb\\xffc=;\\x92E\\xbd\\xdbW\\x08=\\xca~1\\xbd\\xc0\\xd0\\\"\\xbd\\xff\\xdb\\xdc\\xbcXWH\\xbc\\xab\\xbb\\x06\\xbd\\xf6\\xc2\\x8a=\\xe0=\\xf9\\xbc\\xb7XU<\\x98\\xa1\\xc6<sB\\xb9\\xbc\\xa0\\xfd\\xce\\xbc\\x84\\xa4\\x9c<\\xac\\\\\\xd1\\xbc\\xfcx\\xf9\\xbcEK\\xa9\\xbd\\xb0\\x93\\x08=\\x04\\x9c\\x85=\\x84\\x1b\\xd4<Ok\\xce\\xbc\\xdd\\n\\x18<\\xfa\\x0f =\\x87:\\x96\\xbc\\xe0;4\\xbd\\x19\\xc6\\x04;\\xc6]n=\\x85\\xf5[\\xbde^\\xf5<\\x1b\\xa0C\\xbd1\\x14g\\xbc\\xd8\\xc6b\\xbdE\\xb7\\xb2\\xb9n\\x02\\x14<D\\xbd\\xdc\\xbc=2\\x94=w\\xb6\\x82\\xbc\\xceo\\x02\\xbdP\\xbet\\xbdo\\xdd\\xe087\\xac5<o\\x94\\xf8\\xbb\\xa9\\x8b#=\\x0c\\r\\xa5<AG\\xde\\xbaa\\x8e\\x8d\\xbc!F\\x88;CN\\xcb\\xbb\\x1f\\x9aZ\\xbd\\x93\\xbb\\xff\\xb4\\xf94\\xcf9\\x8a\\x0cj\\xbd\\x9f\\xf1Z=l\\xcb\\x85<\\xa32\\x0e<q\\xc9\\xa1\\xbcO\\x07\\x05=S\\x17\\x89\\xbcp\\xb5\\x11=$\\xac\\t=\\xabE\\x11\\xbd%\\xae\\x0e=c\\xa3\\xfb\\xbc\\x11\\xef\\x05=\\x02\\xe1E\\xbc\\\"o\\xb5\\xbbhrK=\\xab\\x93\\x0c\\xbdW<\\\\<\\\\7M<\\xc2\\xdc&=\\xd9\\xa7,\\xbd\\x08\\xbc\\x01\\xbd\\x1a\\xf5y\\xbc\\xcb@\\x97=(\\xa6\\x9f\\xbc\\xf9K\\x8f\\xbd\\xb0\\x9b\\xde\\xbd\\xdf\\xaa\\xa1\\xbc\\x93\\xc9\\x03\\xbd\\xb3\\x97Y<\\xbda\\x0c\\xbb\\xbdX\\xb2\\xbc:\\xbf#={\\xc8?\\xbd\\x93M\\xe6\\xba\\t85\\xbc*\\x10\\xf2<$\\xe2U\\xbd\\x11\\xa5\\x05=\\xc7\\xf0\\x10\\xbdD\\x02\\x15=T%\\xa0<\\xee\\x8a\\\"=\\x1crN\\xbd\\x0cm\\xeb<PF\\x87\\xbcpa\\x8d\\xbc%\\xf61=\\x1c^\\xb1\\xbc\\xcb\\xbau\\xbcVM\\xde<e\\xa6\\xc2\\xbc\\xa4\\xf9\\t=\\xfa\\xe03\\xbd\\xf7F\\x15\\xbd\\xbfz\\xd3\\xbc\\\"\\x88E<~\\\\w=\\xb4\\x8f\\xac\\xbc\\x96G(\\xbc\\x00\\xb3\\xd8:\\xcc\\xda\\x9f<\\x15\\xebH\\xbc\\xca\\xf5\\x04<\\x1a\\xd0\\x9e\\xbc\\x1b\\x9d\\x89<<\\x9f<<j\\xfd2<\\x08\\xf0F<\\\"\\xc2\\x1f=BE7=\\xea\\x19\\x8f<\\xb5}\\x8b\\xbd\\xbb\\xda\\x90=\\xf4\\xa3\\x9b;!E\\xb4\\xbd^\\xe6\\xbf\\xbc\\xe0\\x13\\xa5=[\\x16&\\xbd\\x16\\xa2W=\\x0f\\x18>\\xbd&\\xc9G\\xbc)cp\\xbc\\x95<\\x85=\\xdeZ\\xc2\\xbc\\xb8~\\x9f\\xbc\\xc3\\xff\\xc0\\xbcJg\\x808$\\xd5\\r\\xbd\\xb3\\xec\\xe7=\\x9am\\\"\\xbdF\\xc8\\xe0\\xbc\\x7f\\xa4\\x0f=(\\x0f\\x0c<r\\xa7\\xa2\\xbb\\xc5(0<\\xc6\\x00O\\xbd\\x80\\xe9,=\\xca\\x82K\\xbc1E\\x96\\xbc\\xa8#W\\xbcG\\xa6]\\xbb\\xecS\\xa6<\\xd7\\xbe\\xa6\\xbd\\xe0\\xa0\\xf6\\xbc\\xb0GD=6\\xd2\\x9f\\xbcVX\\x87<\\xb8r\\x95<#\\xd1\\x07\\xbcr\\xfb\\x87\\xbc\\x97k\\x8d<l\\\"\\xae\\xbd\\xa5z\\x06<\\xd1\\\\\\xd5:p\\x08\\xa5\\xbc\\xfcI\\xda=\\xaa\\xd2^<H\\x94S\\xbd\\xda\\xea\\xa8\\xbcr\\x8e\\xd7;\\x9e\\xba\\x8a<.\\x82A=\\xcf\\x84\\x10\\xbd\\x0f|\\x1e=}\\xfa\\x94<8\\x91\\xef<V\\xf8f\\xbc\\x00\\x80\\x0b=\\xa6\\xd6q=\\xec\\xaf\\x1b<\\xf3\\xeb\\xb5<w\\x8ee=\\x96)-\\xbc\\xae\\xe6!\\xbd\\xfc\\\"s\\xbcb\\xb2X\\xbcz\\x05\\xac\\xbc\\xe3\\x05\\x16=\\n<R\\xbd\\xdbK\\xe1\\xbcX\\x7f\\x9f={\\xceE=\\x0e<S<J\\xba\\xc8\\xbbvJd\\xbd\\xdf\\xf6\\\"=\\xa3\\xf4-\\xbcL\\xf3\\xae<\\x98z)\\xbd\\xc6v\\xa0\\xbb\\xc9\\xf1\\x8d\\xbd\\xc0n\\xae<|\\xe0\\\"\\xbd\\x95[\\xa6<\\xaf\\xfau\\xbd\\xd8\\x06\\x18\\xbd\\xd7\\xd6\\xa7\\xbc\\x9c\\xf1\\x0b=d!\\n\\xbd\\\"\\xf9;\\xbd\\xf9\\x1b\\xa8<=-\\x12=\\xf1\\xa3\\xa9\\xbcP\\x87\\x1e:{\\x7f\\xae\\xbc\\xd5o\\x88=\\xf0\\xa5\\xc8;2\\xf5\\xaa\\xbc\\xa9^\\x13=\\x08tN=v\\xfc\\x80<\\xa1j\\x1b=\\xef\\xa4\\x0c\\xbb\\x03{\\xb2\\xbcB!\\x05\\xbdcW =,1Z<\\xa4c\\xd9\\xbc3m/;\\xb3\\xfeM<\\xe9S\\xb7\\xbc\\x89\\xd8I\\xbd-%\\x10\\xbeN\\xf3\\\"\\xbb\\x94<\\xdb<\\xa8D\\t\\xbd\\xae\\xd6\\xa9=\\x9e\\x91\\xfd<\\x80\\x1d\\\"=Fg\\xfb<\\x89\\xa4\\xce<\\x82\\xcdV;\\xbdz\\r\\xbd\\x1a\\x07e=\\xd6\\xe7$<\\x9b\\x1c\\x07\\xbc\\x19\\xef\\x00\\xbd\\xe2\\xa0\\xc09A\\xee.=\\xd0\\x8d\\xf0:~\\x16\\xcf\\xbd\\xc4CW\\xbd\\xc72#<|}\\xc8\\xbc\\x98\\x9a\\x13\\xbd\\xce\\xd65\\xbd\\xe5@\\xa0;w\\xf0P\\xbd\\xbe\\x8a\\xaa<\\xf5\\x93\\x17\\xbd\\xea\\xbb\\xf7\\xbc\\xd7\\x03\\xc9<\\x9d\\xa6\\xb4\\xbc1\\xb1\\x97\\xbd\\x1e3\\x10=Y\\xd5_=[\\xec&\\xbdR\\xe6\\x96\\xbb\\x95\\xc3\\n=\\xecM?\\xbd\\xea\\xad\\xa1\\xba\\xbf\\xf9\\x81=,g\\xb2\\xbd\\xdf\\x11\\x92<\\x16]\\xda\\xbc{M\\x92<N\\xf6>=\\x15\\xad\\x19\\xbc\\x9e\\xd9>\\xbd\\xac\\xf5>=\\\\\\xed\\x06\\xbc>XG\\xbd\\x10\\xc5*=&\\x83,=\\x9d<\\xa4\\xbbEs\\x96<\\x85\\x05\\xd5;]\\xb0\\x10=\\x11x`;6-\\xff\\xbc\\x92\\t\\xa4\\xbd=d\\xbc\\xbb\\xc9\\xb5)\\xbc\\x8c\\x92\\xa6\\xbcp\\x8b\\xb0\\xbc\\xf8\\xcb\\x85\\xbb\\r\\xbfd=\\x00\\xfc\\x81\\xbc\\x15\\xfc\\xb5\\xbc\\xba!\\x80\\xbc\\xa8\\xea\\xa6=5\\x83\\x03=I\\xe9\\xa4\\xbcf\\x0bC:\\x92\\xe8/\\xbdz\\x80\\x9a\\xbc\\xc4\\x9d \\xbd\\xb9\\xcc\\x0f\\xbd\\xce^\\xa5=\\x9b\\\\U=\\xac\\x98,=\\xbdSn<\\xed\\x14\\x0f=\\xbe8_\\xbcA\\xcc\\xe3\\xbc\\x8a0d;@\\xe6)\\xbdZ\\xeb\\xde;\\xe7\\xad\\xf0<.\\xf4O\\xbd\\r\\xa5\\xd2\\xbc\\xd8\\xec\\x8f\\xbc\\\"\\xac\\xd0<\\x05\\x1a\\x9b\\xbc\\xc1\\xe9\\x84\\xbd59\\t;g\\xcf\\xa4\\xbd\\x042\\\"\\xbb\\xbe>\\xc5\\xbb\\x9d\\x00\\xb7;\\xf5$\\x02;\\xeb\\x1d\\x9b\\xbcq\\\\[=\\xcdY\\xc5=\\xf4\\xe0J\\xbc\\x8d\\xd9\\x93=\\x11\\xba\\x83\\xbccj\\x94<\\xb5\\xfeY=\\x9b\\xde\\r\\xbcd\\xe1\\n\\xbd\\xb3{\\xd7\\xbc\\\"\\xb59=%\\x93e:\\x13\\xa2\\xea\\xbb\\xf2s?\\xbd\\xab\\x9f@\\xbb\\x07\\xb4\\xe0;\\xb5UA=@\\x0ep\\xbcD\\xcb\\xc7:\\n\\xed\\xc5<\\x1d\\x1d\\xee\\xbb\\xb4=\\xd2;\\x8a\\xd9\\xac<Fb>\\xbc\\x8a$1<\\x91\\xf7\\xb0\\xbc\\xbd\\xa8\\xd5\\xbd\\xf9\\xb9\\xb5\\xbaCY+\\xbd\\xda\\xea\\xdd\\xbc\\xef@\\x0c=1+\\xa9\\xbc\\xf6\\x90\\x14<\\xf4\\x88\\x91=K(\\x8e\\xba\\x7ftF\\t\\x9c\\x82\\t\\xbd\\\"\\x13\\xa6;\\x18\\xcfr=\\xbd\\x82Y=\\xf9}\\x93\\xbc:LP=\\xd3\\xe3+\\xbcj^9\\xbcz\\xabA\\xbc5\\xa7z\\xbcP\\xd9!=\\xcem\\x0c=\\xd3\\xd4\\xda\\xb9\\x90\\xe2Q=\\xeb\\xa0\\x9e\\xbc\\xfb\\x7f\\xa7<\\x12O\\x1b\\xbd\\xdf\\x9ap\\xbc\\x1d\\x18\\x90\\xbc\\xa5t\\xa0<\\x88\\x03\\xcb<s7 \\xbd\\xe8JM\\xbcW\\x00\\xd0\\xbc\\xc3\\x1c\\x99<\\xca\\x1e\\xd9;\\x8eb\\x13<\\xc0d\\t<\\xe7l\\xb0\\xbc\\x12\\xd8\\x80<tgu\\xb9\\x18\\xfe\\xfd\\xbc\\xe2q\\x06\\xbd\\xfdAV\\xbdt^D\\xbdF\\x94q:\\x9a\\x81\\xf7\\xbb\\x83n\\xf6<=\\xd1\\xdb<4N\\n=\\xe8\\x98\\xa3;\\xc9!\\xef<w8\\xd1;\\xd0\\xde\\x97<2\\xfe\\x05=\\x9e\\xd7c\\xbc-E\\x89=K\\r\\xb4<\\x80\\xd90\\xbc\\xa9YX\\xbc\\xd8\\x97\\xa4\\xbc\\x17H+\\xbd?\\xcf\\xad<X\\xb5)<\\xfd\\xe2\\xdd\\xbb:\\x927=\\x9dv\\x99:\\x9dY\\x96<{\\xa0:<UW\\x04=C\\xa0\\r;\\x81]\\xbf<\\xd4\\x1a\\x01\\xbc\\x8c\\x984=\\xca\\xdb\\xaa\\xbd\\x9f\\x9c*\\xbd\\xf3\\x8f\\\"\\xbd\\x80\\xde\\x18\\xbch\\xed\\xf8<\\t\\x04\\xa3\\xbc$5g<M\\x7f\\xa3\\xbc\\xcc4\\xaf\\xba\\xcc\\xa2\\xbd<\\xd7\\x01\\x12=\\xe2\\xac\\xde\\xbb\\xeb\\x06\\xb6\\xbb\\x05\\xf2i= \\x95u\\xbd\\xa5yE=\\xd4a\\xdc\\xbbQC\\xfe:w\\r\\x17<\\xf7\\r\\xea<\\x96\\xed\\x1a\\xbd\\x19e\\x98;\\xdbC\\x85=\\xe3\\xdbH=\\xfb\\x9a\\x04\\xbd\\xa3\\xf9\\x1f=\\xe2\\xb8\\xee\\xbba\\xa2\\x8c;\\xba\\x88\\xc5<\\xf7=U69\\xedU=d\\x9f\\r=\\xc8\\xc3\\\"\\xbd\\xbd\\x0e\\x14=\\x8a\\xcd\\xb4;0\\x83e=\\xb5/\\x84\\xbd\\x1d\\xc2T\\xbd\\x85/%;\\x91\\xcc\\xab\\xbc90`=\\xb9h\\xda\\xbc%(W=\\xca\\xf9\\xb4\\xbc\\xb3\\t\\x00\\xbd\\x1d\\xeb\\xa8:(1E=\\xf3\\x91\\x03\\xbd\\xc2\\xfdQ\\xbd\\x84\\xd9I=\\xc9-\\\\=\\xdc\\xa6\\xad<\\x05?\\xa0=\\xd6\\xddJ;\\x1e\\xdf\\xd4\\xbce\\xfe-\\xbd\\xe2\\x1e\\\"=\\xf7\\x1e\\x1a\\xbc`X\\xa1\\xbb\\xae/\\xfe<\\x9cW =\\xac\\xd8\\x1e\\xbc\\x9c\\xba\\xc4\\xbcf-\\x12=\\xc6\\xb69=\\x81\\x92H\\xbd\\xd5,\\xe1\\xbc7\\\"\\x07\\xbd\\xc0\\x98\\x8e=\\x18\\x951<\\x96\\xd8\\xa3=\\x87l\\x11=o\\xfe:=\\xdf\\xe6S\\xbd\\xc8\\xc2\\xc3\\xbc\\x17\\x12\\t\\xbdz\\xbb==7\\x03X\\xbd\\xe2Xq\\xbd\\x12\\xbb+<M\\xd7\\x8a;2h = 4+\\xbd\\x8a\\xf3\\x1b\\xbc\\x87\\xde\\x08=\\x1f+\\xd7<\\x1f@\\x98\\xbcgV\\xa6\\xbc\\xf22(\\xbd\\xa9\\xc6\\x1d=\\x02\\x0c:=\\xa2\\xfe\\xf0<H\\xc0`\\xbdM 5=\\xbd\\xab\\xd9< \\xb5\\xca<ii\\x90\\xbc:\\xc5\\xad=\\xd1\\x10\\x94<\\xcf\\x17\\x98\\xbc\\x05P\\x84\\xbb\\x8f\\xbcm\\xbc/1Z\\xbc\\x15\\x19\\xb5<\\xf4\\xed\\xaa=_aZ\\xba\\x8f\\xf1\\xed<\\xc1\\xd1\\xce\\xbb9\\xdf\\x07=mT\\xb7\\xbc\\x7f\\x10\\x8f\\xbc\\x9a\\xcby=\\xc7.\\xb1:\\xcar\\xa9;\\x13\\xa9\\xaa<Qm\\xb4\\xbc\\xe7\\x97P\\xbb\\xa3\\x0f+\\xbdv\\xba\\xb3<\\xae\\xe4\\x9f\\xbcJrL\\xbb\\xf8\\xbfa\\xbb\\x08r\\x03;S\\x03\\xc0\\xbc\\xfb\\xd6W=N\\t\\x8b\\xbc\\x96\\\\\\x8f<\\xe9\\x0b\\x1b=\\xd7\\x16\\t\\xbd\\xbe\\xfb\\xc4<\\xd5\\xe4\\xc2\\xbc\\xd0\\xb2\\xc5\\xbc\\x1a!\\xed\\xbc\\x9e`-\\xbb\\xf2\\xdd\\x83\\xbb\\xd5\\xaf\\x0c\\xbd\\xd3t\\x8f\\xbd/\\xd1\\x08<\\x1aD\\x04\\xbdp\\x9a\\x92\\xbd\\x04\\x18T\\xbd\\xfb$\\xd9\\xbc\\x90\\x96Z\\xbb0l\\xb1\\xbb\\x9b\\xa2V\\xbch\\xb2$\\xbd\\xe2\\x19X<\\xe3\\x98\\xd8\\xba\\xc2\\x88\\xcd<\\x81\\x0c\\x04<!Q \\xbd|\\xdfD\\xbb\\xb6\\xab\\xab<\\x93\\xab\\x1b\\xba(\\x06a\\xbd\\x96\\x9e*\\xbdm?g<,|\\xfc\\xbc\\xe0q\\xe1<\\xe3\\xec\\x8a=\\x96\\xa4\\x95=\\xccV\\x87;0q\\xca<L+D\\xbb\\xb7\\xf89\\xbd\\xac\\xe1\\xf3<\\xb8\\xc5\\xcb<\\x9a\\xa9\\x00=HU\\x12\\xbcJ\\xf8\\xc6;\\xfeE\\x13\\xbdD\\xb6\\xa7\\xbcZ\\xa0\\x96;\\xbe\\x90\\x1c\\xbdvf\\xa2<{\\xcf\\x9e</A\\x15=\\xcdg==x\\x144<\\xad;\\x05\\xbc*\\xb6s\\xbdB\\xbdp<!_\\xfb\\xbb\\xce\\xd1\\xd3\\xbb\\x85%f\\xbd\\x85\\x9b\\\"<\\x18\\x1b\\x95\\xb9\\xf6\\xde\\xfe:\\xae\\x1f_\\xbc{\\xb5\\x80<\\xac\\xc6\\xb6\\xbc5\\xbc\\xed<1\\xe0\\x9c\\xbd\\xd3\\xd3\\x04\\xbd\\x99\\xc8\\xb0\\xbc\\x93\\x11r\\xbd\\x13&\\xfb;\\xf9\\xb4>\\xbc\\x1cJ\\xd8<g\\x8cQ=Nw\\x90=_Wr<0-%=U\\x95V=\\xa9\\xf7D\\xbd\\xd4fR=Not\\xbc|\\xddk<\\x05Po\\xbc\"\nHSET bikes:10081  model 'Iapetus' brand 'Nord' price 1978 type 'Kids bikes' material 'alloy' weight 8.4 description 'These bikes pretty easy to ride while also being lightweight enough and quite durable. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xaa:y<\\x08\\xf7T=\\x8bbF\\xbc\\x12\\x00\\xb2<\\xff\\xbbJ<6\\x10\\x81<(\\xc6`\\xbc\\xab=k\\xbdp\\xcf\\x81=|i\\x13=\\xdc\\x80\\x12=g\\xbf\\x08=\\xc7(P=\\xc0\\xa5\\x8d<\\x02\\xad.=\\xc8YD\\xbc;\\x9c}\\xbc\\xbcM\\x1a<\\x05i\\xb8\\xbc\\xba\\xa2\\xba\\xbc\\x19\\xba\\x06=\\xd7\\x8d\\x10\\xbc\\x869h=\\xebq6\\xbd\\x81\\xc7C\\xbc\\xe2(\\xf1<\\xb4\\x8f+=\\x1fc\\xa8;g\\x1a\\x1d\\xbd\\x97\\xb2y=\\x7fl\\x1d\\xbd\\x92\\x13\\x0e\\xbc\\xe61\\xa2:\\xce\\xb0^<079=\\xd4\\x1f><\\xf0n\\xea;r\\xd3\\xee\\xbc\\x1b\\xda\\xff\\xb9\\xf8\\x9a\\x9b\\xbc\\xe3\\xa21=J\\xf0\\x0f=\\x95\\x1aC=\\x1e\\xd1\\x80<\\xcfQv<\\x94\\xb4\\x1e\\xbd\\xf3K\\xc8=|\\xebk\\xbb^M(\\xbde\\xd3\\xd6;-\\xfa\\x7f\\xbd\\xe5\\x19T\\xbd\\xe7\\xce\\xc5\\xbc\\x95\\xb3\\xff<]\\x9e\\x8a<V\\xff\\x98<\\xde,e\\xbc\\x9a0x\\xbd\\xb0\\x1f]\\xbd\\x13\\x88\\xa5=\\xed\\x95\\x9c=\\xac\\xb9\\xf9<\\x92mQ=+mi=\\xd3\\x11\\x10=\\x91\\xdd\\x12;\\x12\\xdb\\xef\\xbcH~Q=*\\xee4\\xbcl\\x1e\\xda</WJ\\xbb&\\xe2a\\xb9\\x9e\\xab9\\xbdL+\\xe0<\\xfeO\\x14;xR\\x14\\xbcK\\xf6\\xcb<\\x8a\\n\\xfe\\xbc\\xd9]\\xcd\\xbc\\xcdlc\\xbc\\xdf\\x93/=\\xc5\\xa9b\\xbd\\xa8\\x03\\x0e:\\xcb\\x16\\x1b\\xbd\\xf3\\xec\\x14\\xbd\\xc8+\\xde\\xbc5\\x0ck:\\xb6\\xfe\\xc8<\\xc4L\\x04=F3\\xce<\\x9c\\x9d\\\\;\\x05\\xb4r\\xbdf\\n\\x87\\xbdnf\\xa8\\xbc\\xe3,\\n=k\\xd3A9\\xc3i{\\xbao\\tn\\xbd\\x80\\xf1\\x85;)-\\xf8\\xbc\\xf4Q\\x03=\\\\\\xacz<;\\xed\\xb1<u\\x85T\\xbd\\xa6\\x8dl\\xbc\\xde3\\x9b=uF\\xe9<\\x85\\x91\\xdd;\\xfa\\x8f\\xd8\\xbc\\x0b(F=x\\xa3\\x9a<\\xdd^\\xce\\xbbO:\\xb8<p0.\\xbd\\xfd\\xc8a\\xbd\\x87lR\\xbd\\x88\\xd6m;\\x84:\\x0e\\xbd\\xf3\\xac\\xdf<%\\x99\\xa2<n\\xdb\\xe1:\\x87\\xab\\x1b<\\x02V\\x8e<n#\\x19\\xbccJ^=\\xf0v\\x9d\\xbc\\x9b_\\x1b<\\xe7\\xc7\\x97\\xbd\\x0c\\xbd\\n\\xbd\\x9a\\xd8\\x8f\\xbc\\xa3\\x8fo\\xbc\\x96\\x14\\xa8\\xbc\\xdb\\x1c\\x91<\\x16\\x92\\x89\\xbcjd\\x03<\\x15q&=L\\x97L\\xbc\\xd8\\x9a\\xe3\\xbb\\xed\\xb0\\xe2;\\x92\\xfd.\\xbd\\x16\\xf5\\xfe\\xbcF\\x85\\x06\\xbdw\\x95}=j\\x8b\\x92=\\x0b<B<\\x08j?\\xbb\\x87^\\\\\\xbc\\xe9U\\xe0<\\xbd\\xfb\\x13\\xbdA\\xe3q<\\xb7\\\")<X\\x18\\x97=\\xb2\\xf4n\\xbd\\x0e\\xec\\xbe<\\xba\\xc0\\x8a\\xbd\\xdf[5\\xbdv\\xd5#\\xbd8\\x08C\\xbcQ\\xcf\\x9f\\xbc\\xd5\\xbdc\\xbdLZ[=m\\xdd\\x17\\xbd\\xe1\\x88\\x92\\xbc\\x14(\\xcf\\xbc\\xc0\\\\\\x81<\\\"d\\x1c<\\xeaWW\\xbcc\\x0e\\xd9<\\xc0N\\x13=\\x88\\xaf;\\xbd\\xff$V\\xbd\\xf2\\xf7\\xaa<\\xaae\\x88\\xbbP\\x88\\x0c\\xbd\\xa1\\xae\\x92\\xbc\\xfbw:\\xbd~\\xc1Z\\xbd\\xce\\xa2\\x06=\\xb0F\\xee<\\x14\\xdb\\xae:\\x97\\xf6\\x9a\\xbcX\\x0c\\xd7\\xbb\\xc7#\\x11\\xbc\\x19\\xff\\x87<\\xfde\\x82=\\xb3\\xecu<=\\x8dK=z\\x0c\\xca;s\\xa1u<\\x1d\\x1d\\xc5\\xbc>v\\xbb<\\xb0\\x16\\x03<\\xc7U\\xc0\\xbc\\xa8b\\xe5<\\x9ew\\xd8\\xbc\\xf8\\xc9\\x8a=O\\x0b\\x8c\\xbd1|S\\xbd\\x954\\xaa\\xbd\\x06\\xf1p=\\xc5\\xe0,\\xbc\\xd8\\xb6\\x8c\\xbd=u\\x86\\xbd\\x08\\xe5b\\xbc\\xf5P0\\xbdF\\xfd\\xe8;\\xf6\\x9d&\\xbc\\x1do+\\xbd\\xed\\xb86=\\x0bh\\xa9\\xbc\\x1b\\x86\\xb6\\xbc\\xe0\\xae1<\\x03f\\xec;tW\\t\\xbdz\\xc8[\\xbcE\\xfc\\x03<\\x950\\xef<\\xacM\\x0f\\xbd\\xfd-\\x89<\\r\\xa7\\xa0\\xbd}\\xe6\\xa1\\xba[#\\x07=Yl\\x1c<ML\\xcc\\xbc\\xd5\\xeb\\x99\\xbc\\xe4\\x12F\\xbd\\x12\\xe7_<w7%\\xbd\\xc5(\\xee<k\\xa5j\\xbd\\xbeh\\xfa\\xbb\\xf6_-\\xbdvGS\\xbcv\\xc8[=\\xb6\\x83\\x00\\xbcp\\x9bi\\xbc\\x9fD\\xa9<\\xc3\\xf0{<r7\\x97\\xbc#p0=\\xaf\\x04\\xd4\\xbc\\xc8\\xedY\\xbc\\x1f\\xde\\xd5<\\x0f\\xe1\\x81<\\x99\\x06\\xdf\\xb8\\x1d\\xcf\\x94=\\x1c\\xebp<\\xad\\xd2\\xbf;]5D\\xbd0XN=\\xbd\\xb2\\xb0\\xbc\\xd9\\xdeS\\xbdd9V\\xbc\\xec\\xa5\\x86=\\x02Ly\\xbd\\x97\\xb9\\x8c<\\x13\\x90c\\xbc\\xeeE!\\xbc\\x16\\x93L\\xbc\\xad\\x16\\x99=\\xd4\\xc9\\xd3\\xbbgV\\x0b\\xbd\\xa6\\xcc\\xe8\\xbcO\\x0e\\x12\\xbd\\x9e\\xf4\\x99\\xbc\\xa2\\x17\\xea=S\\x9c\\x0e\\xbd\\x93h\\xa3\\xbcu\\xb9\\xcf<3F-\\xbb:\\xac\\xc4<\\x00\\x848\\xbc\\xb7\\xcb\\xa1;&\\xec\\x8e;)m\\xd9\\xbc86\\x01\\xbc\\x17\\xa7\\xa0\\xbc\\xddl\\x92\\xbcq\\t\\xea<\\r%\\xbb\\xbd\\xe1Us\\xbd\\xcfB\\xf89\\x0c\\x1d\\x81;u\\x88Z\\xbb\\x11\\x103=\\xa5\\xa3\\x7f<\\xda\\xc7%\\xbc\\\"H\\n<9g\\xab\\xbd\\x12\\xdd\\xbb<*8\\xfa<q\\xdc\\xbd\\xbc\\x12y\\x94=\\tc\\xbc\\xbc\\x8d\\x8e,;Z\\xd1\\xef\\xba\\x88F\\xa7<\\xe8\\x13\\x0f=a\\x8d1=O\\x1al\\xbc\\x8d\\xc8P<{\\xc5\\x87<\\xfe\\xd2\\x15\\xbbn\\xc67\\xbd:K\\xea<\\xb0|\\xc6=+\\x82\\\"\\xbdSh\\xb4\\xbc\\xd4\\xef\\xaf=k\\xee>\\xbc\\x16Fl\\xbc\\xbd8\\xb9\\xbc\\x104\\\"\\xbcK\\\\C\\xbdx\\xf9\\x08=\\xaf\\\\\\xa5\\xbb\\xe80k\\xbb\\x0c7.=\\xeb\\x18x=\\x82\\xa7P\\xbc\\x19\\x1d\\xbb\\xbcW\\xf8\\xed\\xbcY\\xd6#\\xbdm\\xb8\\xe8\\xbc\\xe6@\\xc9\\xbb\\x15+\\x1a\\xbd\\xd0\\xe1\\xe9<\\n\\x837\\xbd{A\\x94<y\\xf9\\x8f9\\xef\\xf9\\x0f=WVS\\xbdL\\x00\\xd1\\xbc\\xef\\xc3<\\xbc\\x97b\\x02<\\xe6\\x15\\xbb\\xbc\\xc5L\\xa2\\xbdI\\xc1U=\\xf4\\x963<\\x14%5<\\xaf3\\x9b<m\\xc0\\xb5\\xb9\\xea\\\\\\x80=\\x94\\xda\\xb8;O\\x0cs\\xbc\\xcc\\xe56=l\\x8a\\xc8<\\xe9V\\x84=r\\x1e\\xdf<$\\xd7:<\\x1e\\x19\\x9d:6\\xae\\r\\xbdI\\x14\\x80=l}\\xd6<\\xefJ\\x82\\xbd{!\\x17=s{\\x92<\\xa9J\\xf0\\xbce\\xc1[\\xbdf\\x0c\\xd0\\xbd\\x93Z\\xbe<\\x0e\\x97\\x8b<@\\xbd?\\xbd\\xa5\\xc2\\x82=\\xceVa=\\xe9\\xdb9<\\x15~R<\\xf1<\\x8d\\xbc\\x08[c\\xbd+\\x1a\\x0b\\xbd\\xd1\\xb0\\xa8=lr\\\"<iGb\\xbc#9~\\xbb\\x81\\x14r<!\\x994=}\\xb5\\x89\\xbb\\xf4\\x01\\xe6\\xbc\\xc0\\xf6w\\xbd]\\xab7=#\\x83\\xca\\xbcx\\xf9_\\xbd\\xa1e\\r\\xbd\\xed\\xca\\xab\\xbbh\\xe65\\xbd@#\\xa8<GSy\\xbd\\xf4\\xa6\\x7f\\xbd\\x93\\xad\\xa8<&\\xa3S\\xbdy>\\x17\\xbd1f\\x0c=\\x0e\\x0c\\x89=\\xbd5\\x14\\xbd\\xf9l\\xb1<\\xbc\\xd9\\x0e<\\x19\\x10)\\xbd\\x8cn{\\xbc\\x1a\\xf0\\x0e=F\\xaa\\x90\\xbd\\x12\\xcb\\x8c\\xbc\\x08\\x87F\\xbd\\xce\\xb43=\\x162\\xd9<\\x92\\xb1\\x02\\xbdVS\\xc6\\xbc\\xf8d\\x9d=\\xef\\xe4\\x19\\xbcl\\x13\\xa2\\xbbM\\xc1<8\\x97[R\\xba,\\xb2\\xc6\\xbaA\\x85\\x18=a<\\xb97n\\xf8n\\xbc^)\\x16=\\xa5Q\\xb6\\xbb<\\xb4M\\xbdo,\\x97:\\xdc\\xd6\\xc9<!\\xfa\\r<\\xe8R\\x10\\xbd\\xff=\\xf2<{\\xc2@=\\xbf\\x99V\\xbcp\\x89A\\xbb\\x07j\\x17\\xbdo\\xa2\\x96=\\x9e\\x8f\\xb9;\\xf0\\xeb\\xa6\\xbbwJ$<\\xfa0+\\xbb\\xdd\\x99\\xe5;kF\\xea\\xbc\\x0bB\\x97\\xbd\\x16\\x91\\xc0=\\xaeg\\xe7<\\xb1(\\xb8<V\\xd0\\n=+\\xb7#=\\xb8\\xa2\\x01\\xbd\\x95\\x0c\\xaa\\xbcV\\xd0\\x8c\\xbc[\\x07F\\xbc\\x1d\\x13P\\xbb\\x1f\\xfa\\x05;\\xbdy\\xb0;c\\x15\\xb2;\\xb3\\xe9\\xb9\\xbc.m\\x96<\\xff:z\\xbc\\x0bSf\\xbdb/,\\xbdO\\xc2\\x15\\xbdW\\xc7\\x1f=0\\xa1\\x19<+\\xa2\\x91<\\x8fFp\\xbd\\xdfj?\\xbbC\\x86\\x92=4&\\xbf=d\\xf5v\\xbd\\xc0\\x1fH=c\\xdf\\xbf\\xbcD\\x01\\x04<\\rP2=\\x1a\\x16+;\\x86\\x8b\\xf3\\xbc\\xe2V\\xa8<FO\\xb0<O\\x14\\xc6\\xbc\\xb8\\xb0A=\\x16e\\xdc<\\x9cg\\x85<Qw\\xcd<\\xff\\xb3\\xc2<\\x8c\\xc4S<\\x97P^<\\xf5\\xd2a<\\xa2R\\xd7\\xbb\\xa8\\x1b\\xc7<\\xa5\\x08\\xdb<\\xb1\\xe7\\x04\\xbd\\xe3\\xe1\\xbc<\\xdc\\xf5\\xe2\\xbc\\xe4\\xe3\\xc1\\xbd\\xd7\\xfb\\xb2<\\xcd!\\x9a\\xbco#\\x17;\\xb3q\\xf1<\\x14\\x9b\\x1e\\xbd\\\\\\xa7\\x7f;V\\xbf\\x9d=\\x00\\x00\\x90<\\x07\\xcdC\\t\\xf3.\\xfe\\xbb\\x83\\xba\\x10\\xbc\\xab\\x17X<\\x04\\xc4\\xe7<&\\xbc\\x7f;\\xfc\\xadM=\\xa6l\\xa6<x\\xbe?\\xbd\\xa1\\xbd\\xde;<-\\xbd\\xbc\\x96\\xb5\\xab<\\x08\\xfa1=~\\xfcc\\xbc\\xe1\\xb4\\x83=\\xf62\\x12\\xbc\\x95;\\\\<\\xed\\xd70\\xbd\\x01\\x19\\xce\\xba\\xa1J\\xc3\\xbb\\xb7P\\x02=\\x1c8=\\xbbVx\\xe6\\xbcT\\\"\\xb5<`\\x80\\x1a<\\xd6*\\xce;\\xd7\\x81\\x97\\xbb_\\xc8\\x06;3mb<\\x7f\\xd6\\xa6\\xbc\\x05\\x88\\xd6<\\xc3&\\xdb\\xbc\\xaaz\\x829\\xb0\\xad\\xf6\\xbb\\t\\xd0\\xb7\\xbd\\xf3\\x9cI\\xbd\\xd2t$\\xbc\\x85\\xde\\x1e=\\xe59\\xa59p\\xbd\\xbe=r&\\xba\\xbb\\x87C\\xfb<H\\x85 =\\xe8\\xb9\\x16=#\\xd7\\x8c<\\x11Ru<\\xf4a\\x81<\\r\\x98\\xdd=\\xa0gj<\\n\\x95\\x94\\xbc\\xbc;\\x10\\xbd\\xe5\\xb3:\\xbd\\xf1\\x96:\\xbc\\x01\\xc0\\xe3;\\x94_3\\xbc\\xd1\\x19\\xf6<:\\xb9;<[(\\x12=h\\x1a\\xf4<\\xa8\\x82\\x96<\\xffm\\x0e=]5\\x86\\xbbM\\x9d\\xe4\\xbb\\xf7.\\x0f\\xbc7N\\xa5<\\x11\\x07\\xa4\\xbda\\xea_\\xbd\\\"[C\\xbcc_\\xf3\\xbc\\xd9H\\x88<\\xf7\\xbc1\\xbacb\\xbd=\\xaf\\xf7n\\xbb\\xdc7Z<\\x1f\\x9a\\xb8<\\x99\\x9b\\xba<\\xcb\\xc7Y\\xbc.18<}\\x8ct=\\x01\\x15\\x14\\xbd%\\x95\\xbf<\\x1a\\xe9\\xb9\\xbcDn\\xb3<\\x9a\\xdcW\\xbct\\xd6g=xW\\xc2\\xbc\\xb7\\xc1\\xdf;\\xb1\\xf8\\x9b=\\xda7k=aJ\\x1c;\\xbc\\x98\\x94;\\x9b+\\xf2\\xbca\\xe8\\x97\\xbb\\x8cCi=\\xcf\\x9e\\xc6\\xb8\\x08bo=#\\xaa<=\\xc5t]\\xbdS\\\"J<5J\\xe7\\xbb\\x97g\\xeb;\\xd7bK\\xbd\\x01A\\x8c\\xbd\\xed\\xf2==\\xf5\\xe7}\\xbd=\\x85\\x1a\\xbc\\xdc*\\xda\\xbc\\xa1U\\xa3=\\x92\\x0f\\xfc\\xbb\\xd3\\xc0\\xd9\\xbd(go;]\\xd2\\xd0<L\\xfb\\xc4\\xbb\\x86\\xfb\\x9b\\xbd\\x91\\xd0\\x8f=\\x9c\\\\\\x02=\\xc5\\x7f\\xf6<\\\"\\xb5+=\\xb6}l\\xbc\\xdc+\\xe2\\xbc\\xacs\\x93\\xbdm\\x95\\x9d\\xbbBN\\xa3<\\xb4\\xc1:\\xbb\\xf3X\\xd5<?Z.<\\xe9\\x19w\\xba\\x11\\x1d\\x9f;\\xec?\\x88<UH3<wg\\x8d\\xbd=\\x95\\xec\\xbc&\\xb1\\x82\\xbdgmy=\\x90\\x9c\\xc0\\xbb\\xca\\n\\xa1=\\n\\x87U\\xb9\\xa9\\xe1\\xc0<y\\x1d\\x8a\\xbd\\x81[\\xca\\xbc\\xb6*\\x95;:C\\x81<\\xdaeJ\\xbd\\xd4\\xc9;\\xbcJ\\\\\\xf3\\xbc\\xb6}\\xe0\\xba\\xf4\\xa0|\\xba\\x95=\\xcc\\xbc\\xf5\\x8e7\\xbc\\xf9J\\xf9<h\\xecB\\xbc\\x01l\\x17\\xbcYa\\x19<\\xb7,\\x1a\\xbc\\xeb\\x06\\xc4<\\x9a\\xe9\\xeb;\\xfa\\x95\\xab<Q>\\x8d\\xbc\\xfa\\xa6\\xb3=D\\x0cF<\\x93A\\xc1\\xba=\\x9e8\\xbd\\xce\\xb0q=\\x80B\\xb0\\xba\\x16\\xfd\\xe0<\\x96u\\xba;f\\xda\\x1c\\xbdmbM\\xbd\\x84cp\\xbcm\\xa48=?\\xb3\\x88\\xb7\\x1acc\\xbb\\xd4\\xedX<\\xbb`\\xa4<R\\x10\\\"\\xbd\\x94W\\xae\\xbbQHZ=\\x91\\x18\\x84<A\\x9b\\xc1<Y\\xad\\x82<\\xf271:\\x02\\x91g\\xbd\\x8b\\x89V\\xbd\\x1d*\\x0b=;B\\x14\\xbd\\xdc\\xdb\\x16\\xbc\\xdd\\xf0\\xca\\xbci|\\x8e\\xbc\\x95e\\x81\\xbc\\xa6\\xa5_=\\xd5\\x8a+:)\\x8f\\x88<\\xddn3<\\xe0}\\x16\\xbd\\xd1\\x03\\xee;q\\xe33\\xbd\\xf5\\xc0\\\"\\xbd\\x04\\xfa\\x96\\xbc\\x8e\\xc3_\\xbcyc\\x1a\\xbd\\xc2\\xb4\\x8b;\\xcf\\xbc\\x15\\xbd\\xe2\\x94\\xd4<\\xc1\\xb2\\x9e;\\x8c\\xc9\\xbd\\xbd\\x10\\xee\\xdf\\xbcw^\\xf0:\\xf4\\xf5z<\\xc6D\\x96\\xbc\\xbe\\x05\\xcf\\xbb\\x10\\xd8I\\xbcS\\xba\\xae<\\xfc=a\\xbc\\x02\\xdb3=\\xeb\\x9en<w[\\x06\\xbd\\xa0\\x94\\x83\\xbb/\\xbc\\x0b=s\\xd1\\x8f\\xbc\\xe2>%\\xbd\\xbe\\xb4\\xa5\\xbdR\\xe4\\x9a;\\xf3\\xdeQ\\xbd\\x84\\xcdK;>\\xb1P=?]\\x9f=R#\\x0c\\xbd\\x9e\\x9cw<Y\\xe7\\x1d\\xbc\\xff\\xa3S\\xbd\\xe5.\\x06<\\xe0\\xd2\\xa8<;\\xc5\\xaf;J^\\x8a\\xbci?\\x8c;K<\\x8e<\\x86\\x80+\\xbd\\xd1\\x06*;\\xbd\\x89\\x08;psV<\\x04X\\xd3<\\xe6\\x8dr;z\\xb59<\\xf4\\x1c\\x02=\\x98wG\\xbc\\xb2\\xf5\\x17\\xbdy\\x0f\\xb0<%kp:\\xae\\xc9.=\\xe6\\xb7D\\xbd\\xbe\\xa5\\xf09\\xf5\\x8a\\xae\\xbb\\x0f>L\\xbc\\x03 `\\xbc=V2=\\x87\\xe2s;a\\xf8\\xef<\\xc5\\xb8?\\xbd\\xd7`\\x9e;k^\\xbf<\\xf7\\xe4r\\xbd3\\x19\\xd9\\xbbL\\xe6\\x8b<\\xbc\\xde\\xb7<\\xcfm\\x06=6\\xc3\\xba=>1\\xee;$a*=6\\xacI=w\\xab\\x82\\xbd\\xe3\\xf3\\x85<\\xbe\\xe1\\x03\\xbd\\x17\\xef\\x00=\\xda\\xf5\\xc7\\xbc\"\nHSET bikes:10082  model 'Millenium-falcon' brand 'Pedal pals' price 2385 type 'Road bikes' material 'full-carbon' weight 15.7 description 'This is our entry-level road bike – but it not a basic machine At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. If you\\'re after a budget option, this is one of the best bikes you could get.' description_embeddings \"\\x93f\\xd1\\xbcO\\xcfT<#\\xbe\\x89\\xbc\\x8a\\xfb\\x0b<\\xac\\x92\\x86\\xbc\\xeb\\x1c\\xb6<\\xa1\\xf1\\x04\\xbd\\xb9\\xb4\\xbd\\xbcv\\x18\\x08=\\xd3\\xc3\\xf2<\\xe8|\\xce<\\xc2\\xc8\\x88;\\x16\\x83\\x0f=\\x10\\x88C;6\\xbe\\xb6=\\x93\\xfa\\xe4\\xbcl\\xae\\x90=\\x96\\xe6Y\\xbd\\xf6\\xbc\\\"=%\\x1a\\x8a\\xbd@\\x187\\xbb\\x8a\\xd0\\xdf\\xbc\\xd4D\\x0e=\\xbf\\x19\\x82\\xbd\\xb65y\\xbdh60<FL\\x98=f\\xe6\\x8f\\xbb(~\\xbb\\xbcH\\xea\\xea=Iy\\xf8\\xbc\\x91\\xd09\\xbd\\x00\\x9b\\x8b<b\\xf5\\xbc\\xbc\\xaa\\xc2P;\\t:D<\\x11g\\x85<\\\"\\x90\\xb2\\xbc\\xce\\xf6*\\xbd\\x0f=\\x1b<!\\xd1\\xa3=\\x07\\x9dC\\xbd\\xb6D*=_\\xa8\\xd8;\\xe0)\\x89\\xbcx|\\x06\\xbd\\xbcx\\xbb=\\x1e\\x81\\xf3\\xbc\\xfb\\xa93\\xbc\\x90\\x9f\\xa9\\xbb\\xa5\\xba\\x02\\xbd\\xcd?\\xe6\\xbc7\\xe0\\xed\\xbc\\x0clc<\\x8c9\\xd7\\xbc[^\\x13\\xbdG\\x14\\x93\\xbc\\xac\\x01\\xc3;xr\\x1f\\xbd\\x06\\x91<=\\x9d\\xee`<\\x1b\\xd4\\x0e<\\xee\\x90\\xaa<\\xc2t\\xb2<[b?=\\x01\\xd2\\xb4<<r\\xed:F\\x14j<@\\\\\\x8f<jo6\\xbd\\xb3\\xb2b<\\x95\\xbf0=\\x0e5G\\xbcKI7<\\xe8\\xe3=\\xbd\\x91I\\xbf\\xbc3#=;\\xed\\xf5\\xf2\\xbcT\\xc4\\xa7\\xbd\\xf3\\xdc\\xbe\\xbb\\xb5\\x91\\xcc<l\\x98\\xde\\xbc8\\x0b|;\\xf4\\x977\\xbd\\xae8_<x\\\"\\x8a<c\\xbe\\xdb<\\xd9\\xd1\\x19<\\xf1\\xbcr=\\xb3\\xf3i\\xb8\\xff\\t\\x84<-}@\\xbd\\xe5\\x93\\x11\\xbd\\xd5\\xd51=\\xf3\\xfe\\xdb<\\xeb\\xc7\\xe3;\\x7f\\xabf\\xbd\\xa5\\x94b\\xbds\\xc45=\\xd0Y$\\xbd\\xd0\\xa0\\xc7\\xbb\\xb4\\x94\\xbb<\\xb8\\xce\\xcf<\\xda\\x14\\x94\\xbc\\xbf\\xe5\\x9e\\xbb\\xb6c\\xcc=\\x99\\x11\\x98\\xbc\\xc9DB<\\x84\\xf7\\xd6\\xbb0\\x08\\x92;/\\x89*\\xbc=3B=\\xd9Ry<\\xe3\\xd4\\xda\\xbc\\x9d\\x91\\x8c\\xbd=\\xec+\\xbdw\\xcaL\\xbbN\\x93\\x14=\\xd1I\\x1d=\\x0c\\xe1\\x18\\xbd\\xbc\\x00\\x9c\\xbc\\xe2\\xde\\xc9<\\xe4h\\xf9\\xbcl\\xe2\\xe6\\xbcu*>=H\\x08M:\\xc42-\\xbb4\\xe4W\\xbdy\\x9ab\\xbd\\xf3\\xc1L\\xbd3\\x08<\\xbd6\\xa87\\xbcl/o\\xbb\\x10MK\\xbd \\xec\\x88<\\x8f\\xe5:=dX\\x04\\xbcm\\xb8\\x9c\\xbc\\xd0\\x8e\\x04;Z\\xfd\\xde\\xbc\\x8c\\xc2\\x08\\xbd\\xdb\\xf9\\x80\\xbd\\x99\\x03M<\\x10\\x95\\xe5=\\xd52\\xb3<\\xda\\\"\\xdb\\xbb\\xbb\\x07\\x1b=\\xa4\\xef|\\xbc\\xaa\\xe7\\x80\\xbbq\\x8dN\\xbc\\xd3*+;\\xb4\\x9bL=\\x1f\\x9b\\x17\\xbdUW\\x83<\\x92PU\\xbd`\\xc1\\xcf:\\xd1\\xc9\\xfa;\\x8d\\x1b\\x02\\xbd\\x01\\xfec=\\xaa\\xb51\\xbd5}\\xa0<\\xdd\\x0c\\x13<b\\x83\\xe0\\xbc;\\xe2#\\xbd_*\\xc2:\\xd6\\xae%=\\xd4\\xff)\\xbd\\xcd\\xd6o;\\xbf\\xffz=C\\xac\\x11\\xbc\\xc5S\\xa5<Up\\r=\\xa3Y\\xc8\\xbb+\\xf0\\x1a\\xbd{\\xb0\\x8f\\xbc-\\x82e\\xba\\t\\x11\\x19\\xbd\\xfeHF=a!\\x86\\xbcx\\xd7\\x8e\\xba0\\xff\\x90\\xbc\\xae[\\xd2<)\\xc2\\x08<\\xd16\\x84;\\xa0\\x076=\\xf51\\x90\\xbc5\\x94\\xe7<\\x11\\x03\\xa0;\\xdeE\\xe1;%/7<\\xf8M\\x1e=\\xdbq\\x8b=\\x1a\\x8b\\xed\\xbc\\x05\\xfe\\xe1<\\x18\\x84x<\\xf3\\xfa\\x9f<\\xaaDs\\xbd\\x00h!\\xbd\\xf1\\x94E\\xbdC\\xb6@=\\x8f\\x19B\\xbc\\xf9\\x04T\\xbd\\xdbZ\\xc0;\\xa1\\xcc\\xf0\\xbbM\\xc7\\n<k.\\t=Ct\\\"=|zS\\xbd\\xfc\\xc68=2\\x87&\\xbd\\xa8\\x13\\x7f\\xbc\\xf9k\\xc5\\xbc\\x9f\\xda\\x7f=\\xce\\xdbx\\xbce\\xe9\\xfe;\\xdf\\x1e\\x1a\\xbb\\x17\\xb2E;T\\xb0~\\xbd\\xcf\\x0f\\x88<\\x8f2\\xd0\\xbd\\xb0m\\x82<\\x94z\\xf9\\xbc\\x0e\\xdfC\\xbc\\x8d\\xf0\\x81\\xbb\\x19lf\\xbd\\x93\\x16Q\\xbc\\\\\\xaa\\xb0<\\xd9\\xd6;\\xbdq\\x03]=\\x94\\xbcb\\xbc\\x1e\\x9f \\xbd\\xee\\xf1\\x97\\xbdM\\x87:\\xbc_\\xc5S=1\\xc8>\\xba\\xe3\\xd9\\x80<\\x05/R<\\x87\\xd0W=\\xc7[\\x85\\xbcx\\xbdg<f\\xb2\\xcf\\xbc\\x1c\\xd16<u\\x1e\\x14=\\xd8\\x151\\xbcS#\\x10=\\xe2\\x05\\x9f=\\x01\\xb5\\x12\\xbc\\x96\\x19\\xf1\\xbd[\\x93\\xa3\\xbd\\xbd\\x15\\x1c\\xbcr\\x8a\\xa2<\\xe4\\x18\\xb5\\xbc\\xdaa\\xc6<w\\x8e\\x81<\\xed9\\x87\\xbd\\t\\xc40=\\x17R\\x1a\\xbdeF\\xab< >\\x19\\xbd\\xee\\xe3\\x9d=\\xc5\\x9bc\\xbc3\\x98z\\xbcH\\x19\\x95\\xbc\\xc5S\\xe3\\xbc\\xdc\\xbb\\x98\\xbc\\x07\\x90\\xc0=H\\x1c\\x9e\\xbcpD\\xcd\\xbc\\x04H\\xb8<\\xaa\\\"\\xc3;\\xe4\\x9dM\\xbc:\\xabf:\\xa8ft;<\\xbd\\xc4<\\x07\\xda\\xb4\\xbc\\xebC\\x13<z\\xcd\\x92\\xbc.oD<\\x17\\xf2\\x91=\\x14\\xff\\xc5\\xbdC\\xe53\\xbcgA&\\xbbM\\xf6\\x92\\xbc\\x96\\xd9\\xad<1\\xf9\\x19=c7|\\xbc\\xd5\\x0e\\xa9<\\x95\\\"&=\\x08\\xd0\\x99\\xbd\\xff\\xb4#\\xbc#_\\x06=\\xeb\\xaa\\x16\\xbd\\xc9;\\\\;\\xb0bN\\xb9\\x7f\\x1b\\xc0<\\x9f\\xb1\\x11\\xbdz\\x1c\\xa9;\\xb9\\xb5\\x90<\\xec~g\\xbc1D\\xe5\\xbc\\xe9\\r2<\\xcc)\\xc8<\\xa4\\xbcT=V\\xf7B\\xbd\\x81\\xb2\\x83;\\xf2w\\x92<\\x0bb\\x80\\xbc#\\x16\\x99\\xbbiw\\xc8<\\nU!\\xbdA\\x04m;\\x12pW\\xbd\\x1f\\x95 \\xbc\\xf7a\\xa2\\xbb\\xe2\\xd2\\x0f=a\\xf7\\xc6\\xbc\\xb4\\x17G\\xbd\\xdd\\xdes=U\\xdc<=\\x95P\\n\\xbc\\x1dJ2\\xbdc\\xba\\xb4<U\\x12o\\xbc\\xfbt\\xea<,\\x92\\xdc<\\x8e5\\x7f\\xbd\\xe8\\xdc\\x0b<\\xf1\\x182\\xbc\\xdd\\xfd\\x9c<4>\\x01\\xbc\\xdb\\x8a7:\\xa0c\\xa1\\xbd+\\x1f\\xa4\\xbb\\x19\\\"\\xcb\\xbcV{\\x03\\xbc.8\\x9e;\\x17\\xaf\\xf0\\xbcm\\x07+=f\\xe0\\x13;\\xb6\\\"\\xaf<\\xf8]A\\xbb\\xad\\x10J8\\x8d\\x1f\\xbc<\\x0e\\xb7\\xc8\\xbc\\xbd\\x96\\xcf<\\x02\\x035=\\xac/p=\\x8eOW=\\x1e\\x02>=\\xeb\\xaae;\\xe7\\xae=;@\\xdfh\\xbc0\\x9e\\x9f=O\\x7f\\x85\\xbcKs\\n\\xbdM\\x04\\xea<k-L\\xbc\\xe8\\xeb\\xaa\\xbd\\xe4\\\"\\xad\\xbc\\xc8\\xa1\\xbd\\xbd\\xb3-\\x85\\xbcl\\xbd\\xce<\\xf70\\x1d\\xbbZ\\xe9V=\\xba\\xfb\\xfc;\\x03\\x1a\\x9b=\\xde\\xc8\\x1b\\xbdK\\xf5\\xbc;\\x06\\xd4\\\\\\xbd>\\x9e\\xd4\\xbc\\\\ow=\\xd3M:<)=\\x97\\xbc\\n\\xf0\\x88\\xbbc\\xd6\\xf2<z\\xc7~=\\xf4\\x82\\x88\\xbdx\\x7f\\xdf\\xbc\\xf1\\x17\\xeb\\xbc\\xa6\\xe6\\x11=G\\x16#\\xbdhh\\xa1\\xbd\\xf1;\\xde\\xbcl]\\xc2\\xbann\\xbd\\xbc\\xe0z\\x1c<\\xb1%\\xa2<\\xc4\\xd6\\xb4\\xbcH@v=\\xf7\\xeb\\x04<;\\xfbX\\xbd}0\\xa4=`\\xb9>=\\xd5\\xc1z\\xbc\\x81\\xf0\\xb2\\xbb\\xcaDQ\\xbc\\x15\\xbe\\xd5\\xbd\\xb9\\xd2\\xe8<\\x14]=<\\xf6\\x0c\\xdb\\xbc\\xc5\\x17\\xf4:S\\xf3%\\xbdXk1=\\xc8\\x98\\xb8<\\xe5r\\x8f\\xbc\\\\\\xb1\\x0c<p\\x9b\\xbb<2\\xbd\\xa0\\xbbJ\\xf7/=\\xa6\\xc8\\xdd\\xbc\\xe8~v=\\xc5\\xc9I<\\t\\xc88;\\xd1\\xd7\\xe2\\xbc\\xad\\x9dA\\xbb\\xf2\\x81+=K\\x13\\xf7<\\xa1\\x17\\xd7\\xbc\\xa0\\xd8@<[&\\x08\\xbcl\\x90\\xa3<zR\\xd2\\xbc\\xc8\\xde\\x90<\\xb8T\\xe5<4Z\\xb3\\xbc2u\\x14\\xbd\\x82\\xc7\\t<#\\x9a\\xd7<J\\x1b\\x06\\xbc\\xb8\\x14\\x7f;zH\\xf6<\\x1c\\xca\\\"\\xbd\\x8cR\\x1d\\xbcx\\xc8\\x92\\xbd\\x932\\x96\\xbc\\xa4\\xd3\\x8d=\\x16z+=y\\xa9\\xde<\\xbb\\x0e\\x02<\\xbdv\\x81<\\xc9!U<\\xc5\\xdaG<\\x07d\\x1b\\xbdV\\x80\\xcc\\xbc/\\x02x<l\\x03\\xdf:\\xbc\\xba`=\\xd3T\\r<\\x81Q\\x1a\\xbdn\\xd4\\x1c=~\\x97@\\xbdosH\\xbd\\xc0\\xeeJ\\xbc`\\xf4\\\\\\xbd\\xfbd&=\\xc9jt\\xbc|\\xab\\x17\\xbc\\xb1\\xc0<\\xbd\\x0c\\xec:\\xbd\\xb8\\x94+=\\xafz\\x80=\\x1f\\tS\\xbcmHi=\\x8a\\x97\\xdb\\xbc\\xa4Xl\\xbc\\xdb`\\x0c=\\x11\\xc5\\xe4<.\\xc9\\x89\\xbc\\xd5\\x13\\x1e<\\xde\\xbav=\\x85[Q;\\xf71\\xba;\\x8bO\\xef<\\x9fFq\\xbb\\xad\\xf2\\xd7;1r\\xeb;j\\xca\\x14\\xbd\\xa38\\x04\\xbd\\x98l\\xb8<\\xf9T\\x15\\xbc\\xd1\\xa5\\x11=G\\xe7{<\\x00\\xa9\\xa0\\xbcE$-=\\x95\\xab\\xa4\\xbb\\xfc\\xa8\\xe0\\xbd!\\x1a\\x00\\xbd\\\"/\\x92\\xbc^\\\"y\\xbcUS\\xf4<G<\\xbf\\xbc\\xac_\\xf5\\xbcm\\x8f\\x0c=\\\"\\xf0\\xd4\\xbb\\x07\\x0ff\\t\\x10\\\"\\t=\\x1f\\x83\\x05\\xbdp\\rn<\\x0e\\x90\\xf4\\xbcn\\x85\\x1d\\xbc\\xc7\\xd9P=\\x00\\xb3\\xb5\\xbc8Y\\r\\xbd\\xde\\n\\x0f=1di\\xbc\\xc6\\xaa\\xe9;\\xd2te<\\x12\\xbd\\x8b\\xbc\\x18\\x8d\\x1c=\\xd5\\xbc\\x8c9^\\xb3W=\\x9dT\\x80\\xbd2AB\\xbd\\xcfbr\\xbb\\xa2\\x88\\x1c=\\xcb\\x94\\xe7<\\x14\\xda\\xa4\\xbc.\\x1b\\xe3;[\\xbfR=\\x06C{\\xbb\\xe8\\x8d7:\\x8c\\xcd\\xc6;\\xf9\\xbe\\xbb<\\xf3\\xca1\\xbd\\x01$\\x0e\\xbd2Ot<%\\x1f\\xd0<\\x17R\\x84\\xbbe\\xdf#\\xbdl\\xed\\x1a\\xbc[\\xb5\\x80<\\x03&>=\\xf5\\x13\\xf7;\\xbf\\x1ft=\\x1b\\xf8\\xa7<&s\\x8e\\xbc\\xa5\\xeb\\xc7;XJG=5\\xf3\\x05\\xbd\\xa8\\x19\\xf2<\\xe0\\xf2\\r\\xbd\\xc3\\x16\\xff=\\x0e\\xe5+<\\xc1(\\xf3\\xbc\\xce9u\\xbc/\\xdcC\\xbd\\xc6\\xc5\\xc5\\xbd\\xe0;:=\\xc5\\xc0S<\\xc2\\xbe\\x87\\xbcR\\xb3i=`\\xb6\\x89\\xbdV\\x80\\xdf<\\x08\\\"Q<?s4=J\\xf1\\x0c<g\\xe3D\\xbce\\xdf\\x11\\xbc\\xbc\\x10\\x19=d\\xab\\xfd\\xba\\xab\\xc8a\\xbd\\x05\\xbbS\\xbd\\x04\\xaa\\x81\\xbc\\xd7\\xb7\\x00=\\xfb\\xb3\\xbc\\xbc\\xa3\\x18\\x10=J0b\\xbd\\xac\\x98\\xc6<V\\x8a/=\\xe8\\xe2\\x15\\xbb\\xe4\\xb3c\\xbco\\x80^\\xbbk23=\\\"\\x93\\x00<\\x8d\\x1f\\xa4<\\xc7\\xfc\\x94\\xbc1\\xf2\\xa4\\xbb\\x1b\\xa0.\\xbd\\xac\\x89\\xe5<\\xdb\\xa0w\\xbd\\xce6\\x01=\\xec\\xf9:=M-G=;\\xd5\\x08=j:\\xc6<\\xbc\\x13/==\\x9bX\\xbbX\\xc3*=\\xa7k\\x82:\\x0eE0=\\xdcQL=\\x99#\\xbe\\xbb\\xb0\\xa8D\\xbdj\\xa0\\xa3;\\x0c#\\xee;\\x97r\\xb2\\xbc]7\\x90<\\x95\\x02$<1J8\\xbdE\\xbdf=3\\x88;<pH\\x85<\\x97\\xd2\\xf7\\xbaK+\\x1d\\xbdu\\xb6\\x8b<\\x84r\\x82=\\x00\\xcc4\\xbdg\\x97\\\"\\xbcQ\\x945=\\x90\\xf3H==\\\"\\\"<;\\xfc\\xc8<\\x8c\\x97\\xd3\\xbc\\xa5\\xd5\\xe9\\xbc5\\x96#\\xbd\\x14q\\x92<\\x88\\x9a\\xec<\\xcd\\x90_\\xbby\\\\,\\xba{YP=\\xbc\\\"\\xe0<\\xdb\\x11\\x0f\\xbd\\x12q\\xe8<\\xcf-K<\\x13\\xc1 \\xbd]\\xe5/\\xbd;\\xc1\\xa1\\xbdx\\x03\\x99<\\x0fzA\\xbc\\x0ex\\x97=~T!\\xbc\\xf8\\xbe\\x05=\\xf44K\\xbd\\x17\\x12\\x03\\xbd\\x94\\xa0\\x1e\\xbd*t\\x87\\xbc\\xc3\\xfdv\\xbd\\x1f\\x1f&<\\xb9\\xf5S\\xbb\\xa6U\\x06<\\xaf\\xe5\\x05=o\\xf8\\xab< d]\\xbc\\xc4.!=}a\\xf2<\\x04\\xe9\\x07<\\x1c\\x99\\xca\\xbc\\x01-\\x1f\\xbd\\xd9\\xeaT=_\\x83^<)\\x7f\\x9c<\\x1f\\xcf*\\xbdJ\\x93\\xb9=\\xb9N\\xa4<\\xa1\\xd9:;\\x7f\\xa5\\xdc<jmO\\xbcM\\t\\xb4\\xbc\\xbdGp\\xbb.X\\x1d\\xbdx5\\x01\\xbd\\x03\\x9c\\x86\\xbd\\xb3\\x13\\xe7\\xbc\\x1ep\\xa1\\xbb\\x9f\\xdf\\x80\\xbdIa\\x13:\\xd6L6\\xbd\\xa9\\xa2\\xf1;J\\x99\\xe67x\\x15+\\xbc|\\xb8\\xf2<\\xd1\\xb3\\x03\\xbd3\\xb9\\x03;\\xb5\\xdd\\xde<\\x12\\xac\\x08\\xbdF\\xafr\\xbc\\x93H\\xe1\\xbdp\\xcd\\x84=\\xbb\\xb0\\xa1\\xbc\\x10\\x8b7<\\xb0).\\xbd\\xfeE\\xa2<\\x8f~\\xa6\\xbc.n\\x0e=\\x1c\\x9b\\x9b<;\\x16\\x19\\xbd\\xd7\\xe2U=A\\x96\\x19\\xbd\\x19\\xe6\\xae\\xbb`\\x19\\xb7\\xbd\\xaa\\x1a\\xd7\\xbc\\xdc^\\xd6\\xbc\\xa6&\\xbe<\\xe2\\xbd\\xc9\\xbc\\xf6]U<\\x16\\xf8\\x07\\xbd\\xafP\\xc4\\xbcM\\xfc,\\xbcPC\\x99\\xbd$\\xf8\\x97\\xbb&\\xb1*<\\x03\\x03\\x1c;Zz\\xed\\xbb\\xf9\\xfe\\xec;\\xde\\xa1M\\xbdN\\xe1\\xb3<\\x0fq\\xdc\\xbb\\x10\\x84\\x91<\\xbe\\xc9\\x07\\xbc\\xd7.\\xd7\\xbc\\x0e\\x80\\x17:\\xcf\\x1a\\x82<\\xff\\x8c\\xeb\\xbc#\\xdab\\xbdd\\xab\\xf6\\xbcW\\xb6?;H\\x16]\\xbd|h.=\\xf0<\\xa3<\\x8a\\n\\x06=\\x89\\xf8&\\xbc[&\\x17=\\x00\\xf3\\x83\\xbd\\xc0\\x84\\xa4\\xbd\\x18\\xb5\\xea<\\xcaA\\xb0<m4\\x1f=\\xa5\\xcf\\t\\xbdJ[Y\\xbc\\x8b\\x90\\x16=f\\xef\\x93\\xbcsm\\x9e<\\xbeg\\xe3<,\\xac\\xa6<8c6\\xbb-\\xa6x<\\x8f\\xf2\\r<\\xe3\\x99\\x98=\\x1a+\\x9b<-_I\\xbdr\\x048=w\\x9a\\xb3\\xbc\\x98\\xef\\x8d;\\\\\\xb1\\x88\\xbc\\xe5\\xa4\\x8b<\\x80\\xf9\\xa0<Z\\x85\\x12\\xbc\\xbemx<H\\xdf\\x8f=\\x9dC\\x01\\xbd\\x1d\\x8e\\x90\\xbc\\xca\\x16\\xbd\\xbcj\\x08\\xa0<\\r\\x06\\x87<\\x8c\\xa2j\\xbd\\x1a x<)\\x04N=\\x07\\xa5-=A\\xd9\\xfa<\\xcd\\x11\\xb4=\\x89f\\x82<`\\x15)=\\xcd\\xe2\\xaa=\\x06V\\x01\\xbd\\x99\\xe1\\x1b;\\xea\\xc2\\x96\\xbbiN\\x05=\\xc8\\xc4\\\"\\xbc\"\nHSET bikes:10083  model 'Phoebe' brand 'nHill' price 1176 type 'Commuter bikes' material 'full-carbon' weight 7.1 description 'This bike is the perfect commuting companion for anyone just looking to get the job done The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"7o\\\\\\xbc\\x05\\x15\\x0e=\\xa3\\x07\\x99\\xba\\xf9#\\x00=\\xc8f\\x19\\xbc\\x1e\\xc9\\xca<\\x86\\xff\\x05<\\n.\\xeb\\xbc[R\\xa4=0\\x07\\xe1<\\xc5=\\xa9;\\xb4\\x85\\xcc\\xbc20\\xf7;\\xa4\\xdc\\\\\\xbc\\xa9-\\xde;\\xf8}\\x0c\\xbd\\\"s\\xbd<6\\x9f\\xc7<\\x1d\\xf9\\x0c\\xbc4T\\xfd\\xbd \\x1e\\xb3\\xbc\\x07\\xef\\xad\\xbc\\xff\\xf9j9\\xcdH\\xe9\\xbc1\\x04\\x18:G~;\\xbcp\\xcd:\\xbc\\xe35\\x90\\xbc\\xe3\\x7f\\x99;t$\\x8b<\\x96\\xa1B=[\\xbe:=\\xbe9\\x18=\\x82\\xee\\x10=q\\x81\\xea<hL*\\xbc\\xc986=\\xce\\x85\\xb9\\xbc\\xb8\\x1b`;x\\xf1!==77\\xbc-\\xf8\\xb8\\xbcp[\\xec\\xbc\\xbb\\xbf\\xdc<\\x97%\\xd4<f\\xaa\\xe8;^\\x9a\\x9b=\\xb0\\xbe\\r\\xbd\\xcc\\x85\\x1c\\xbd\\xd4k\\x1e=\\xf1\\x1a\\xd2\\xbc\\x08\\xb7\\x86<\\xdd\\xf4\\xfe\\xbb\\xe4A\\xfc\\xbc\\x1b(\\xc6\\xba\\x9eF,\\xbd5\\xae\\x0f;\\xb29\\x83\\xbd\\xe0\\x05<\\xbd34]=\\x81\\xe9\\xd3<\\x92\\x8e?=\\xe0\\xd2\\xa4\\xbc\\x1bS\\x8c=\\xd9\\xb7\\x96=^\\xb6\\x8c\\xbc\\xf2,H\\xbc9\\x97\\xc5\\xbb\\x13]\\x05=\\xb1\\xd7A\\xbc\\xdeA \\xbd\\xeb1^\\xbd\\xde\\x85\\r<\\x83Y\\x9d\\xbd\\xc2\\xd0\\xe1\\xbcYn\\xc1\\xbc\\xf0$\\x17\\xbd\\x8b\\x14R\\xbd\\x92\\xfd\\x16\\xbc\\x1f\\xc2\\xf9<8My\\xbc\\xb0\\xbf\\x99\\xbc\\x08\\xe1V\\xbd\\x95\\xe8\\x07\\xbd\\xb1\\xb5l\\xbc\\xe0\\xe7-<h\\xba\\xd9<\\x9f\\xe7\\xc5;\\xe0\\xc2B;B\\xc1\\\"<o\\xffR<*-n\\xbd\\xa4\\xf5\\x87\\xba\\x18\\x05\\xaf\\xbb\\xc0\\x18\\xb0;\\x16\\x16\\x15=B\\x12\\x91\\xbc\\xcbkO\\xbcR\\xe19<;\\x88\\xc3\\xbc\\x84\\x166\\xbcN\\xe4\\xe0\\xba~=4\\xbdQ=\\xe5\\xbcW\\x95\\x11=k\\xe9\\n=\\x0fl\\xf1<\\xa4\\xea\\x97<\\xb8\\x95\\xa9\\xbcm\\r\\x11\\xbb;\\xd2\\xfd<\\x0e$\\x8b=\\x8b\\x8bN\\xbc\\x89\\x95\\x8b\\xbc\\xd7\\xf1\\x87\\xbc!\\xc5D<Sb\\x84\\xbc1N=\\xbd\\x8e\\x0f=\\xbc\\xc7\\xa7i==\\xdc)<\\xea\\xee\\xb1<r\\t\\x85\\xbd\\xbemb=\\xbb\\xf3\\x03\\xbd?\\xb5\\n\\xbbV\\xf4\\x8e;\\xc8\\x92\\xa3\\xbd\\xe6\\x83\\xe4\\xbc\\xd8\\xd1o<x+\\t\\xbd\\xa3\\x92,\\xbd\\x91\\xa7k<\\x87\\\"Y=\\xb1I~\\xbdT\\xb5d=\\x1f\\x94\\xbc\\xbcwn\\x8b;\\xe5\\x19\\xde\\xba\\\";V\\xbct\\xc8\\xc8\\xbc\\xda\\x0c\\xa5\\xbd\\x14\\xc1\\x83\\xbd\\x88E\\x84=*\\x06\\x17=\\xfaUU=\\xa5\\xafE\\xbd\\xa3\\x1c\\x97:\\x87\\xc2\\xa2=\\x8f\\xb2\\n\\xbd\\xf6\\x14I\\xbc\\xfb\\x7f\\x9d=bb!\\xbdY\\xe8\\x9a\\xbc\\x13\\xe3\\x95\\xbd\\x9b\\x12n<\\x04\\xed|\\xbc<\\x04p\\xbdH\\x93\\x03=\\\\\\x95\\xde;\\\\\\x98\\x1a=\\xb7\\x86\\x0f\\xbd0\\xb6_\\xbd\\x00\\x0c,\\xbdQrc\\xbb\\xc8\\xe5F=\\\\\\x8c^\\xbc;\\x1f\\xa4<(\\xd9#<\\x9bA\\x08<\\x96\\x11\\x91\\xbaw.L<\\xf9.\\xa1<\\x9d\\xff%\\xbd\\x00\\x90\\xb4;\\xb5c\\x17<\\xfe\\x1cI\\xbd\\xc0\\x90\\x93=\\xba\\xb9[<\\x95#G<ug\\xa6<\\xd8\\xb8\\xd0\\xbbK`\\x1c=\\xcb\\xe4\\x08=\\xea\\xf3\\xa5<\\xc6oN;\\x04\\xdbD=\\xa3\\xb3\\x84\\xbcD\\xc7\\x11=O*\\x95\\xbd\\xf9\\xa71<\\xa2\\r\\x95;B\\x98\\xed\\xbc\\x0b\\x90\\xb0\\xbc\\xcd\\xd9\\x19\\xbc4\\xeb\\xc7\\xbbJ\\xe9\\x0c\\xbdE\\xa9\\xfd<O\\xcb\\xd1\\xbc*s9<o\\xdb\\x0c<\\xb5\\xde\\xb7\\xbb/\\xdc\\xa2\\xbdL\\xc6\\xf7;h\\xbc\\xd5\\xbc\\xa4\\xa5\\xca<\\x02\\x94\\xc9\\xbav\\xef\\x02\\xbd\\x9b\\xdc\\x99=\\x02\\xe7\\xe3\\xbc\\xd3\\x9f\\xc8\\xbc\\xe1\\xc0\\xa7=\\x98W\\xb3\\xbcI\\xce\\\"\\xbdedJ=\\xd3\\xd2\\xd6\\xbck\\xb1\\xcc\\xbco\\xba7=\\x9e\\\"\\x87\\xbc\\xcf\\xa3$\\xbc\\xc2\\x02\\xdd\\xbcb\\xa9\\xb3;\\xb1,\\xe8\\xb9en*=\\xd0A\\xb0\\xbcTRO\\xbb\\xce:\\xd0:_\\xbd\\x80\\xbc\\xe2\\xd2\\x84;h\\x877\\xbd%y\\xc5\\xbc\\x0eO;\\xbd3\\x1f\\x9f\\xbbX\\xe1~<\\xabX\\xbf;\\xa3\\x9e\\x81\\xbc\\x91\\xfb8\\xbd\\xd8[Q=n\\xab\\x83<<\\xa3-\\xbb\\xc5?\\x07\\xbd^\\x08\\x94\\xbc qR<\\xf3\\xba\\\"\\xbdvb@=I\\x11!=\\x94\\xa8\\xe0<\\xb6\\xe27\\xbc\\xc2\\xbb\\xac\\xbd\\xea\\xaf\\x8a9/\\xf9@=\\x11N\\x8e\\xbd\\xd5N\\x9f\\xba\\xe4B\\x86=\\xcf\\xd4\\x15\\xbdi?\\x8a=\\x99/&\\xbd\\x89\\x80\\\\<y\\x8b\\x99\\xbd\\xfc\\xe6^=\\xed\\x14\\xaf<U\\x81\\xb3<S=\\x8a\\xbc\\xb4\\xfd\\xfa;\\x1c\\x167\\xbd;\\xef\\x86=Z\\x1a+=S\\xc8\\xd6\\xbc5\\x18\\xe6\\xbc\\xa4\\xd9M:\\xaa\\xf8\\x9a\\xbd Yu\\xbc\\xa6\\xf2\\x9d\\xbc\\xa5\\x12\\x95\\xbcM_\\xbc\\xbc\\xcd\\xad\\x95\\xba\\x07\\xb2\\x03=\\xa5>\\xca;\\xf2\\xd2\\n\\xbc/\\x9d\\x9c\\xbd\\xcf\\x9cm:\\xa2l\\x91\\xbc\\x1a\\x9e =\\xa9&\\x84=d\\xf3\\xa9;\\xa4\\xbc\\x12\\xbd\\xdc\\xe8\\xed\\xbbi\\x1a\\xc2\\xbcK\\xfa>\\xbd#J\\xb8<\\x0cZ\\xbc\\xb5d\\x830\\xbd?\\xed\\xd8=t\\xea\\xa5\\xbc\\xc2\\x8f\\xa2\\xbc\\xa6\\x1f\\xc2\\xbc\\xec\\x11\\x1c\\xbc\\xb2]\\xae=\\xdd?\\x16\\xbceU\\xbe\\xbc\\xa9w\\xb0\\xbc\\xd95\\x80<\\x1d\\xd0\\xe1=\\xc0\\x02\\xff;f\\xe8\\x10=\\xb8\\xb2\\xc0<\\xac\\x89+=\\xe9\\x05\\t=\\x1c\\x15\\x84=\\\"\\xc6\\xe4<&?o\\xbc\\x8d\\xf7Q\\xbd\\x84\\xae\\xc0\\xbc\\xce\\xff\\xbd;I\\x95\\x85<k\\xd1\\t=\\xa0\\x8c\\x84\\xbc\\xb0\\xb9\\xab=\\x964I<q=\\x80\\xbdh\\x02\\x16\\xbd\\x0b\\\"&\\xbc\\xcbh\\xce\\xbb\\xe5\\x11_\\xbc\\x9c\\xd2\\xc9<\\xe9\\xcf6\\xbd\\xa2\\xc4\\x8f=#q\\xc1\\xbd\\x8c,\\x9b< q$\\xbb8x\\x89<8\\x8f(\\xbd\\xd8\\x972\\xbc\\xf9\\xc9\\xc6=\\x146\\x08\\xbc1Jz\\xbcE\\xe0U\\xbb\\x9b<K=\\xdap\\x92<\\xe1\\xa7\\r=\\xa3\\x99%\\xbd$\\xa7\\x86\\xbdw\\x1eC=\\xa7\\x00\\xfc;HI\\x12;\\xe3\\x19H=3lm=\\xc9Y\\x9c<\\xdf\\xd1\\x0e=d\\xafn\\xbc!\\xa6\\x14\\xbdp`G<\\x95\\x020=\\x93\\xd7x\\xbd\\x18\\xe7\\x0b\\xbd\\x9f\\x91\\x99<m\\xde3\\xbc^E#\\xbd>\\xb8\\x0b\\xbd\\xefg\\xaf\\xbd\\x1eY\\xa1<\\xd8\\x92\\x1d;\\x9b\\x97\\x00;/\\x16#=\\x95\\xdf\\x06=Ux\\x04=\\x1c\\xce#\\xbdyl\\xae=D\\xbb?\\xbd\\xd9\\xd7s;}3_=\\x9cM\\xaf<\\x8f\\xb5\\xa9<\\xa4\\xd6\\x11\\xbd\\xc0\\t\\xd1\\xbc\\xef\\xb9\\xd8<\\x98\\xed\\xe4\\xbc\\x1b\\x8f\\xb2\\xbc\\x87\\xdf\\x9b\\xbc\\xad\\xf1\\xc5;\\xf0~6\\xbd\\xff\\xc5P\\xbd5\\xbf\\xca\\xbc*[\\x9f;u(\\x13\\xbdr<W;\\xa7\\xf5\\xc3\\xbc\\xb3\\x9f\\xa9=\\xb4\\x80\\x1a=\\x01\\x07Z<\\x84\\xd9\\x17\\xbd\\xe6\\xfd\\x8c=o\\xe5\\x8f=p\\x9e\\xd6;\\xd1Y\\xef:\\xb9%\\x16\\xbccTW\\xbb?\\xab\\xfd\\xbc\\xa1\\r\\x11=\\x03\\x18u\\xbd\\xa9\\xf33;\\x06f\\xa2\\xbc\\xf1p\\xaf\\xbc\\xeeb\\xbb<p\\xba\\x08=\\xfc\\x18\\x96\\xbb\\x085\\xd2<\\xd0\\x10^\\xbb\\xb0\\x08w\\xbcEe?<{Ug=\\xee\\\\h<o\\x85\\xd1;\\x89\\x1aT<\\x97\\xccG\\xbd\\x9a\\x81\\xaf:\\xe7\\xfd\\xf2\\xbb\\xc1\\x9f\\xc9\\xbd\\x816A<?\\xe2\\xdb<\\xfc\\x8e\\x9e<IUN\\xbd\\xa5F\\xdc\\xba\\\"\\xb3\\x93<\\xdf\\x96\\xc4:\\x90#\\xdd\\xbc\\x02T\\x16\\xbd\\x9b\\x7f!=(\\x19\\xa7\\xbb\\x8ct\\xa6\\xbdg+1\\xbdu\\x06\\xf7;\\xed\\x13O\\xbdU\\x17G\\xbdX\\xb3\\x88\\xbc\\n\\x9d\\n=\\x02\\\\k=\\x80\\xa6\\xd4<L\\x07\\xa2\\xbc\\x12;\\x12=\\x1aP%\\xbd)\\x89\\xf0\\xbb\\x19c\\x19=\\x8e\\xbcF\\xbdq L<F\\x8d\\x0b<\\xd7c\\x92<\\xec\\x19\\x18\\xbc\\x85\\xb9/\\xbd\\x90.6= \\xc1&\\xbdhB(\\xbd\\xf9\\x0c(\\xbdI\\xa9\\x8c\\xbdo\\xbf\\xdd<\\x81\\x8dO\\xbd\\xb4\\xfd\\x0f\\xbc\\x84\\x9e\\xc9<\\xb2^\\x02\\xbdG\\xcc*=\\x18\\x8fl=\\x93\\x1e]<\\x11\\xe7U=V\\x18\\xc3\\xbc\\xe5Z\\xb4\\xbc|\\x12\\x86<\\xb3-\\xb6\\xbc\\xcb\\xb8\\x1a\\xbd\\xf5\\xbbJ=\\xe6\\x02\\xe1<\\xac\\xb6<\\xbd\\xb7D\\n\\xbcjUM\\xbcP\\xad\\x03<\\xe8\\xd9\\x02=\\xc2Sz=\\x15\\x12\\x12\\xbd\\xa7vR\\xbc\\xccP];\\xc5\\xee\\x00\\xbc)\\xd5I\\xbc\\xb6WQ<\\xa8\\x06\\xf4\\xb8\\xa6L\\xdd<\\x17\\xb5\\xe4\\xbc\\xafHn\\xbd(\\x05K\\xbc3\\xa5\\x9c\\xbb!j7=&\\xc2\\x1f=V\\xe9\\x8d\\xbdg\\xd7\\x1d=\\xfe8\\x02=^\\xd96\\xbd\\xe12\\x91\\td\\x04\\xe1\\xbb\\x82\\x80\\xae:\\xe4\\xf2\\x11=\\x00\\xa7\\x84<\\xa8k\\xa5<\\x81\\xb8 <g\\xda.\\xbb\\x84Q\\x11=\\x91\\xfbh=f\\x9cD;w\\xfa\\x0f\\xbd~\\xe4:=\\xe9\\xdd\\xa1<\\x1e\\xd7*=\\xa3\\xa6M\\xbc\\x9bA\\xb8<.*\\xde\\xbc\\xa5\\x9d\\xdb\\xbck\\xc3\\xba\\xbbL\\rX=\\xa2s===\\xf0,=\\x14\\xc8\\\"\\xbc3q5\\xbc\\xa95\\xe6<j\\xfeG\\xbc\\x8fi\\x8f<B\\x8d&=\\xea\\x01+\\xbdF,\\x12\\xbb*@\\xa2<\\x86\\xd4\\xf9;O\\xcd\\x81\\xbd\\xce\\x98\\x8f\\xbdp\\x19\\xd9<\\xd7\\x84\\xcc:\\xb7\\xccE\\xbc\\xd1\\xae\\t\\xbc\\x8f\\\"m<\\xb9\\xde\\x95\\xbc\\xd5\\x87T=V\\xd1\\x1e=\\x1d\\x86\\x0c=\\xe5\\xef\\x80<5\\xd2|\\xbc0r\\xb4\\xbc\\x9d\\xc3\\xf8=0\\nH\\xba3.\\xb5\\xbc>\\xf1\\x12\\xbd\\x9aG\\xb1\\xbc\\xba\\xe8\\x84\\xbd\\xabJ\\xae:\\xfeH\\xaf\\xbaG\\xe4\\x00\\xbd\\x12G@=!\\xdb\\x0c=\\xec9\\xda;U\\xd3/=Zz\\x80\\xbd\\xe7\\x1b\\xa1<}\\xb0s\\xbc\\x12k\\x95\\xbb\\x9f\\xb7\\x8b<\\x85\\xfc\\x90\\xbb2\\xa9\\x00\\xbd\\x9cT\\x94\\xbc\\r\\x10\\n<\\n%h<\\xf3\\xa6\\t;\\xa8.\\xe0\\xbb\\x1fU\\xc5\\xbc\\x10p\\x15\\xbb\\x9e/\\x05=\\xee/\\x8f\\xbcQJ\\xca\\xbch\\xe3\\xd0\\xbc:\\\"\\xd6=E\\xbcH\\xbd\\xb69\\x8d<2\\x99/\\xbd\\r\\xdar<5FL\\xbd\\x96\\xe5\\x00=\\x94<\\xcf\\xbc\\x91aG\\xbd\\x85y\\x84=\\xec\\x0b\\xc7<FF\\x13\\xbd\\xdc\\xbd\\x1c<\\xd9\\xf1\\xb3;W\\xf6\\x1a=.\\x91R=;\\xc7\\n;$\\xfd\\xac<\\xc6}\\x1a=q[\\x1a\\xbd\\xfdI\\x98<V\\x7f\\x1d;,}\\xb9;2*\\xb9\\xbc\\xec\\xee\\xff\\xbaI\\x07\\xdf<\\x95\\x83\\xe5\\xbc\\xb0\\xed\\xb1<D0\\xfe\\xbc\\x84\\xf25<\\x1el$\\xbcl\\xdd7\\xbd\\xe1v[\\xbd04\\x15\\xba\\x00q<\\xbbF\\xf7\\x83\\xbd\\xc7\\xb6g=X)k<\\xba\\xf4\\x07<)[)<\\x96\\x07\\x94\\xbco\\xec\\x1b\\xbc\\xc0\\x8f\\xba\\xbd\\\"\\xfa\\xd6:\\x9f\\x8b[\\xbc\\xeaC\\xce;\\xe7\\xf7\\xc9\\xbcr\\xb7\\x16=\\xc5\\xce\\xc7<-\\x8f\\x9c\\xbb\\x13m2=\\xd3r*<\\xea\\x88\\xe7\\xbci\\x98J\\xbcK\\xc8\\xa8\\xbd\\xf8\\x10_=\\xb5:\\x9a<\\x9b\\xc2+=\\xc1\\xba\\xa0:\\xae\\x7f\\x97\\xb91\\x10\\xa5\\xbd\\x9c\\xa4\\xc0<\\xdd\\x8ee<\\xcd\\x8dJ\\xbd\\xfb!B\\xbd\\x01F\\x0f\\xbdN7\\x90\\xbb\\xff|\\x85\\xbc\\xd6\\xa0i\\xbcaP\\x91\\xbc9(x\\xbc8v\\x97=\\x06\\xd1\\x82\\xbcKV\\\"\\xbd\\xdf\\xd8X<\\x11;\\\"\\xbd}\\x1f\\x16=\\x19\\x0e\\x0c=\\xb5.\\\\\\xbc\\xde\\x16\\x0e\\xbc\\xf4\\xaeL=\\xa9S\\xfa<]<%=s\\xee\\xba\\xb9~\\x90\\x82=\\xc7\\xb7\\x1f=S\\x9d\\xf0;\\xd0\\xfc\\x9c\\xbcn\\x08\\x88:\\\"\\x83\\xdb\\xbc\\xc7\\x1a\\xc4\\xba\\xa6\\x1d/=J\\x1d%;\\tZ\\xbc;\\x8d}\\x85\\xbc\\xab\\xb3g=)\\xfa\\x11\\xbc\\xc7\\xdc\\x1f\\xbcI(&=\\\\\\xaf_=9\\x8f\\x8b<Q\\xaa\\xce<\\x9e\\x17\\xb6\\xbc\\x94\\x03\\x87\\xbd_a\\x1d\\xbd\\x16\\xa7&=2\\x85f\\xbc\\xb5\\xb0\\xd9\\xbc\\x98\\x14R\\xbdLNf\\xbd\\x95\\x01\\x96\\xbdd{x<\\x8a\\xec\\xe0;\\x02\\x03a=4\\xe5u<\\xc7s\\x81\\xbdg\\x99\\x92;\\\"/z\\xbdihq\\xbd\\xd3\\xfd>\\xbb\\xbe\\xd8d\\xbc\\xc5E4\\xbdH\\x03l\\xbc\\xe6:\\xb6\\xbc\\x86\\x0c\\x1e\\xbdt\\x9c\\x91\\xbcc\\xfa\\xc7\\xbc\\xb1\\xba\\x15\\xbd\\x1a.\\xee\\xbc\\x86\\x07\\xd1:\\x9aC\\xd1<?\\xca9\\xbc!\\x82\\xc9\\xbc\\\\\\x98\\x8e<m\\x9dR;0oe<\\x0c\\xb8\\xa5\\xbcl\\xc7\\x16\\xbc2\\xd0@<\\x01&\\xc2<!T\\x1d\\xbd%\\r\\x11\\xbb\\x14d\\x1a\\xbd\\r\\x872=\\xb5\\x0c\\xb4\\xbdM\\xd2\\r=\\xdb\\xf0\\xad=\\xfa,y=\\xda\\x8f\\xb9\\xbc+Sy=N\\x924\\xbdZ\\xbf\\xc1\\xbb,\\xf1\\x94<^\\xc6>\\xbc\\xd4\\xc5\\xf2\\xbb\\x8f\\x07]<\\xf2[B<!\\x01\\xcf<\\xd7\\xd8\\xcb\\xba\\xfd\\xd4\\x19=\\x03\\xfe\\xa6<\\x86\\x18\\r=o\\x9f\\x7f=D\\x93.=\\\\\\xd1\\xec<\\xa8P\\x16=\\x08\\xbd+\\xbd4V\\x90\\xbc\\xa4X\\xeb\\xbc\\xe7\\xf60<\\x0f\\xc7\\x17\\xbc\\xdct\\x84\\xbb~`\\x1a\\xbd\\xdfS\\xfe<(\\xb3\\xa7\\xbc\\xce\\xe7\\x02\\xbd\\x0e\\x1b\\xc0<\\xed\\xa7\\x10\\xbdz\\x8a\\xfa<@s\\x07\\xbb1\\xa6\\x9f;\\xd1\\xd7]\\xbd\\x90Z4\\xbb\\xa0\\x06\\xc8\\xbc\\xa8\\xc4\\xcb\\xbcq\\x1du<\\xce{\\x07\\xbbc\\xb8\\x98=\\x9aF\\xe2\\xb9\\x11V\\xbe;\\x9b\\xab\\xcb=\\x16 \\xd1\\xbc\\xb3\\x8a\\x93:\\x1e\\x1f\\xd2<\\x8d\\xa2\\x90<\\xa5[S\\xbd\"\nHSET bikes:10084  model 'Picard' brand 'Eva' price 4752 type 'eBikes' material 'aluminium' weight 12.7 description 'If you\\'re looking for the best commuter eBike for your trip to and from the office, that will keep you rolling from home to work. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\xdc\\x04\\xe5;\\x9f\\x7f\\x8d=\\xabDW\\xbd\\x7f\\xcb\\x80=\\t\\xf8%\\xbc\\x15N(=\\xa6\\x17x;Q\\xf8\\xa5\\xbd*N_=qo\\xd6;C\\x17L<\\x19\\xfeY\\xbd\\x16\\xe1\\x8a<\\xebo\\xbf;I\\x93\\xcb<\\x14\\x1e\\x1a\\xbd\\xa5\\xa2`<\\xae$Z=K\\x91\\x0e\\xbd\\x16\\xe5\\x90\\xbd4\\xe3\\xd8\\xbb\\xfeW\\xf0\\xbc\\r\\xce\\x87\\xbd85\\x88\\xbb\\xdb\\x81\\xd1<\\x92g\\xb8\\xbc\\x9c\\x95N<\\x93j\\xd9\\xbb\\xea\\x17\\x1c=V\\xf0^\\xbc\\x0eP{<\\xca]\\x90<\\xc63\\x9f<\\x97}\\n=\\xc9bz=\\xad\\x86;\\xbd\\x1b\\xee\\x86=\\x85\\xbb\\x0f\\xbd-.6\\xbc(\\x08\\x1c=\\x86\\xa1\\x10=\\x14\\x8b\\x90<\\xd8\\xfc\\x94;n\\xb9%=\\x0f\\xe8\\xff<lJj;\\xda:\\x92=%\\x7f:\\xbd\\xf5)\\xd5\\xbcLWK<\\xccF\\xca\\xbc\\xc2kp<\\xb4\\x08 \\xba\\x82\\xb4f\\xbd\\x10\\xd3\\x03\\xbc2z\\x82\\xbd\\x8f\\xaa\\xfb\\xbc\\xa9\\xfe\\x96\\xbd\\x00\\xc0\\x1f\\xbd\\xd7\\x85\\x0b=g\\xad\\xc9<\\xaa\\x14\\xae<-\\xbb\\x8d<\\x86\\xf1\\x90=Y{\\xef=\\x8c]\\xac\\xbc$Jy\\xbcn0\\xe3<\\xf0e\\xd1<\\xc44\\x08\\xbc\\xe3L\\xc1\\xbc\\x90o\\xe3\\xbc\\n\\xa3\\xae<\\xf2\\xc0\\xa0\\xbde\\x0f\\x10\\xbd\\xfbL\\\"\\xbd.}\\x8b\\xbc\\x0c`F\\xbd\\xe5R\\xa5\\xba!<\\xb7<\\xf1\\x97\\xba\\xbc\\xef\\xe2d\\xbc:2\\x92\\xbcF\\\"\\xb7<\\xfe\\x84]\\xbd\\x17E\\xc8\\xbc\\x84\\x8d\\xb9<\\xeaG\\xaf;\\xa0,\\xd9<\\x0f)U<\\xdd\\ro=\\x85\\xd8\\xb1\\xbd}[&\\xba\\xf3\\x8c\\xb6\\xba\\xc07\\\"\\xbc\\xd1\\x19\\x7f\\xbc\\x91\\x06\\xb3\\xbc#\\x0e\\xfa\\xbcc\\x9c\\xb7\\xbbl\\x98\\xb7:hgd\\xbb&\\xa1\\x9a8\\x01\\xc6\\x1f\\xbd`T\\x99\\xbc\\xedq\\x8a=x\\x8e\\xd2<\\xe6|\\xf6<\\r\\xe9\\x91<1\\xed\\x04\\xbdt\\xca^<\\xc1\\xa6 =\\x95\\xfb\\x88=\\xc0\\xe9}\\xbc\\xf0\\x00\\x10\\xbdtz`\\xbc\\xab\\xfd\\x15=a=\\xa9\\xbc\\xe4T\\xee\\xbcH$\\xa6\\xbc\\xc4\\xf7\\xb7<eT\\x08=\\xf8\\x91\\xd2<~\\x19f\\xbd>o\\x94=\\x03NS\\xbc\\xf4\\xbcz<\\x89\\xf3\\xcd<d\\x81\\x98\\xbd^la\\xbd\\xaf\\xa1i\\xbd\\x05\\xa3@\\xbd\\x87^g\\xbd\\x94Y\\x01\\xbc\\xbc\\x8cE=\\x87\\xa4\\x9f\\xbd\\xa3\\xca\\xcf<\\\\Ve\\xbc\\xe4]]\\xbcK`\\xee<\\xdb\\xab\\xc7\\xbc\\xb6\\x00R\\xbdS\\xcdT\\xbd\\x05\\xb97\\xbd\\x1a\\x17{=\\xe8q\\xf1<\\xd9\\x03\\xf2< H\\xcf\\xbc\\x11\\xc1\\xbb\\xbc\\x8f\\xcaX=n\\xc4+\\xbd\\xe0\\xecs<\\xda@\\x84=\\xcf\\xc2s\\xbdyq7<\\x122\\x05\\xbd0\\xf4\\x99<\\x95k\\x1a=N\\xfbX\\xbc\\xaf\\x9eT=%\\x7f\\xaa\\xbc\\xe4fB=\\x9b\\xe5/\\xbd\\x18K\\x84\\xbd\\xb4\\xef4\\xbd\\xc1r;<x\\x17\\xc3<\\x08\\xae\\x12\\xbd\\x87I\\x1b:\\xda\\xe0\\x03\\xbdU\\x9c\\x8a\\xbc\\xee\\xbf\\xa6\\xbb\\xdf\\xf1\\x0b<\\xadz\\xbb<\\xfc<\\x89\\xbc\\x9du\\xd0\\xbb\\x06W\\x11\\xbcC^{\\xbd\\rj\\x83=t\\t$;\\xca\\xa5\\xec<H\\xc7B<b\\xcb\\xd0\\xbb\\x9d\\x1d\\xfc<\\x87\\xbeJ=\\x1c\\x7f\\\"\\xbb\\x82l\\x03\\xbd\\x01$h=l,\\xec\\xbcG\\xd5H<\\xfd!\\x92\\xbd;m7\\xbb\\x96N\\r<k\\xef@\\xbd\\x938,\\xbc\\xc9\\x05\\x8e\\xbb\\xaf\\x1f\\xeb<\\x90W\\xb0\\xbc\\xe0;\\x11\\xbc\\xc5[Y\\xbd\\xbdG\\x10<4\\x14\\xad<dR\\x81\\xbc\\x0e\\xee\\x8e\\xbd\\xe4\\xcec<\\x1en\\xa3\\xbc\\xc0!\\x91<\\xfa\\xdb\\x89\\xbc\\xf1\\xf1I\\xbc\\xc7r+=\\xdd\\xbc\\x88\\xbc\\xc5\\xae\\xdd\\xbb\\xf5\\x9f\\xbd=\\x1b\\xc2\\xa6\\xbc{at\\xbd8\\xc8\\x04<\\x07\\x08m<\\x8d,J\\xbc\\x16\\xe8\\xdb<y\\x06\\x86<\\xf3n\\\"\\xbd+!\\xb1\\xbc$\\x1f\\xc5\\xba>\\t{\\xbc\\xbc\\xaf\\x8a<F:\\xef\\xbc\\xb0\\xa6\\xf3;\\t\\xf9U<\\xf6t\\xaa<\\x07\\xb5\\x1a<m1\\x06\\xbc\\xd4\\xcd\\xfc\\xba0\\xa1\\xd6\\xbcX\\xbdv<\\x15\\x08J:O9\\x19=sA\\xf2\\xbc\\xa2\\x81\\xf9\\xbc\\x93\\xa7\\xca<\\x14\\xe4\\xa4<\\xbd*1<\\x7f\\xd7\\\"\\xbc\\x9d=\\x06\\xbd!\\xd7\\x13<\\x8eb\\x89\\xbd\\xdd\\xf7Q<\\x0cS\\x18=\\x80\\x08\\xec<\\xf2\\x1d\\xa4<\\x15q\\xd0\\xbd\\x15\\xea\\x8d<*-\\x04<1\\xd9Z\\xbdu\\x899\\xbd*Q\\xa0=\\xb4c\\xaa\\xbc\\xc6\\xdcG=\\xa4\\xa4\\xd3\\xbc\\x0fo\\x15<\\x89lF\\xbd:F[=_\\x01\\xdc<U\\x87\\x94:]f\\x8a\\xbdnX\\x9c<\\xf4\\x00\\x1e\\xbd\\x99\\xaan=\\x97b>=a\\xd5<;[K\\xc0\\xbc\\xbd\\xe8\\x1c;\\x14\\xd7\\x80\\xbd\\n\\xa2\\x90\\xbc\\xbd\\xef\\xa0\\xbb\\x89S\\xde;A\\xd8\\xf6\\xbc\\xaaC\\x87\\xbc8\\xe8t<\\x14!/;\\x18\\x8e\\xc4\\xbb\\x92\\xf2\\x0b\\xbd\\x9f\\x12\\x1a=\\xe0\\x18\\x99<\\xf2\\xa15=\\x06\\x81\\xdc<\\xcf\\xef\\xa8\\xbb\\xe2\\x92\\xd8\\xbb\\xff\\x9bh\\xbc\\xc2\\x17\\x92\\xbc\\xe7\\xf9\\x9d\\xbdM\\xec\\xf6<\\x07\\xf3\\xc6\\xbb\\xb3\\x1a\\x98\\xbd\\xa0U\\x94=\\xb4\\x1e:\\xbd\\xe6\\xe6\\x07\\xbdh\\x9b\\x9e\\xbc\\xb7\\x15$\\xbdm\\xfb\\x82=\\xff\\xee\\xe3\\xbc\\x0e\\x81\\x88\\xbc\\xa2\\xd7\\xff\\xbc\\xb3\\xb5\\x1d<\\xfe\\xaf\\xd9=\\xf3L\\x1a\\xbcx\\x84\\xa0<\\x8f\\r\\xf2;\\xa37+\\xba\\\"\\xa4\\xdb<\\xf3T\\xb2=%\\xb6\\x85<\\xb7h\\x1e\\xbdC\\xafM\\xbd\\x83\\xa6\\x1e<#,\\xc5;FF\\x89<\\xff\\xe8\\x00=Yc\\xd8\\xbb\\x85\\xac\\xd5=\\x93d\\xf1:\\x06\\x00Z\\xbd\\xd4\\x05=\\xbc\\xa5r\\x9a<\\xf5\\x83\\xf0\\xbc^\\xd64\\xbc3\\x99\\x7f<$Ga\\xbd\\x1bX\\xbc=J\\xd2\\x8d\\xbd\\xae\\xc5\\x04=\\xe2\\xe7\\x9d\\xbc\\xb5\\xde*\\xbb\\x02\\x17\\x0f\\xbd\\n\\xc7\\xf1\\xbc\\x11\\xbe\\xed<\\xc7\\xe8\\xb2:\\xa2L\\n\\xbcN\\xe9#<\\xa9!\\xa0<\\x1b~\\xaa<M\\xa8\\xb9<\\xba\\x85H\\xbc\\xa1\\x041\\xbd\\x13\\xcf#=\\xf5\\x05\\x14<\\xbf\\x1c\\xa1\\xbc\\x18\\xe7J=*`$=\\xd8\\xb6\\xef\\xbbS\\xa8\\xfb<\\x15\\x96\\xe5\\xbc\\xd3^\\x84\\xbd\\x88\\xe2\\xdb\\xba]p\\x0b=\\x84\\xbfB\\xbd\\r\\xb6\\x06\\xbd\\xdd\\xf9\\x0b=\\x98\\x11\\x98\\xbc\\x16\\xd1h\\xbd,\\x98\\xc5\\xbc\\xc5K\\xb5\\xbc{\\xc0?=\\x1e\\xda\\x98;\\x8c\\x04g;\\xcc\\xa1y<O\\x95L\\xbb\\x90\\xb9\\xa4<\\\"\\x83J\\xbdU\\xf4H=\\xa4/\\xbb;\\xee\\xa6\\xa8\\xbc4\\xae`=j\\x95\\x85<\\xa8\\x0f\\x10=\\x12pj\\xbdx\\xaf)\\xbd\\xab\\xf1\\r=\\x9e2\\x9e:({\\x1a\\xbd `\\xfd\\xbcB\\x7f\\xc2<1\\xbe\\x0c\\xbd\\x14\\xf2S\\xbd\\xe6\\xfbO:\\xe7\\xeb\\xeb<\\xcd\\xb5\\x13\\xbd\\xb3h1=\\x81y\\xe5\\xbb].R=F{0=\\x0c\\xadV;\\x02#\\x89\\xbdK\\xdf\\x8a=P\\x95V=\\x9c:\\x85\\xbc\\x859\\xa3<\\x19v\\xd1:\\x1d\\xdf\\xfe:\\x95X\\xd7<\\x8d\\x01c=\\xde\\x98v\\xbd\\xaf\\xd6\\x9e<\\xd2)B\\xbd\\xd3x\\xd6;\\x8b\\xbc\\x8e=\\x06\\xce\\x83<\\xa4\\x1b\\xc8\\xbc\\xc98\\xeb;\\xef\\x01\\x9b\\xbc86\\r\\xbc\\xe3\\x93\\x16<\\x8e\\xa2==\\x12\\xbdK\\xbb\\xc4\\x0e\\xa1<}\\xc7>\\xbc\\xe98\\xdd\\xbc\\\\+\\x18=\\x9em\\xdd\\xbc\\xa4\\xd8\\xc6\\xbd\\x1e\\xa9\\xf7<\\xb1\\xcc\\x05<\\x00Z\\xb6<&3\\x02\\xbc\\x1a|\\x98<kr\\xc3<\\xf9\\x88Y<\\xf0\\xdao\\xbc\\x03\\x8fb\\xbd\\xf1{\\xcb<\\xdb\\xfd\\xfe;\\xc6\\xba\\x84\\xbd\\x03\\xc2\\xf7\\xbc\\x97\\xa0#\\xbdQ\\x08}\\xbc\\xf5\\x97\\x0f\\xbd-5;<o\\xdf\\xec<\\x00\\xc0\\xf8<\\xdbF\\x1d<k:[<\\x9b\\x18p=\\x8b\\xf8\\xb0\\xbcy\\x15\\xe0\\xbc\\xc2\\r\\x83<\\xb5\\xd9\\xd8\\xbc\\xf5\\x82\\x11<\\x10!\\xc0;\\xae\\\".:\\xd7\\x02\\xde\\xbbQP\\x92\\xbd\\x01\\xba\\x98<G^\\xa0;\\xad\\x8a\\xb3\\xbdJc\\x81\\xbc\\x849|\\xbd]N\\xcd<\\x15\\x8d>\\xbdL\\x05=<+O\\xd6<\\xbe\\x1f\\xea\\xbb\\x0cvB=Y@]=\\xef\\x9d\\x85<\\x1d\\x90d=\\xce\\xa6\\xd6\\xbc\\xc3\\x9e\\xca\\xbc\\xe3\\xcc\\xe29E|\\xf9\\xbcY\\x854\\xbc\\xae\\xc5\\x9e=\\xcdwN<ba\\x1d\\xbb/\\x8a\\x9b<\\xbe`\\xd1;5l,=\\x81\\x97m=m\\xbb\\xec<8\\xbc\\xde\\xbc@Q\\x0b\\xbc|\\xf0\\x8a\\xbc\\x19\\\"\\x80\\xbc8\\x1b\\x91<\\x8f/\\x17\\xbc\\xbd|\\xe1<`\\x9c\\\"=\\xad\\xce\\xa6\\xbc3\\\\P\\xbd\\x15\\x8a\\x99\\xbc<\\xa8\\xd2\\xbb\\xd4\\xa4\\xc27O\\xa2\\xf1<\\xe2\\x1b\\x05\\xbd\\xf4\\xfe\\xe9<\\xf2\\x0c\\\"=\\xb3\\x7fS\\xbdb\\xe0\\x9c\\t5J\\xd0\\xbc%\\x9f0;W\\xac\\xde<-\\x03\\xaf;%\\x94\\xf7<\\xd6\\xed\\x0e=!\\xc8\\xa4;i\\xa5\\x92;R\\x0c\\x91=\\\"\\xa6\\x08\\xbd\\xe2\\x1fu\\xbc3\\x8c8=\\xa1\\xb2\\x86;N\\xeb\\x83=\\x1a3\\xdd\\xbc\\x90\\x03L=\\x12ih\\xbd`\\xd2\\x10;(\\xaf\\x1b;\\x0cRX=\\xd5N\\x14=\\xada\\\"=\\x97O]:5\\\"\\x9e\\xbc^\\xa1\\xd5<\\x97T\\x92\\xbcLA\\xe8;\\xfd\\xe7,<R\\xdd$\\xbd\\xa8\\xb7\\x1d\\xbb\\xe3\\x96\\x1b\\xbcw \\x06=\\xe5\\xfaT\\xbd\\x03\\n[\\xbd\\xb3*\\xe3<\\x8bu\\xcd\\xbbL\\x9b\\x04\\xbdFI\\x06\\xbdx\\x81\\x05<H\\xf2\\x06\\xbcW\\xcf.=\\xae(\\x84<j\\xc8\\xe1<\\x1b\\x8e\\x89:\\xd7\\xc6#<\\xf5<u\\xbd`\\x87\\xf8=\\xf4\\xf8\\x0f;z6b\\xbcX\\xd2\\xf7\\xbc\\x7f\\xf0\\x19\\xbd\\xae6\\xe6\\xbc\\xdcb\\x84;\\xb4\\x8e\\xec<.\\xbe\\xc0\\xbc\\x12\\\\\\x86=\\x89(\\x8d\\xbabv}<\\x1ag\\x99<\\x94f\\x96\\xbd\\x85\\xabA=b\\x8b\\x88\\xbdu\\xb2\\\"\\xbc\\x1d\\x9f\\x9d<\\xc8\\x97D8\\xa8e8\\xbc\\xdb\\xa9\\xea\\xbc\\x9c\\xbfM\\xbc@\\x04=<\\xe3\\xeb\\xe1\\xbc\\xd0\\xb6\\x93\\xbc\\xcb)h;C\\x07\\xaf\\xbcy\\x9ah=\\x8a\\xc1>\\xbc\\xd1\\xc4\\xe9\\xbcW\\x97\\xa0\\xbb`_d=\\xf9\\x8c9\\xbd\\xe8\\xf6\\xd6\\xbb\\xfbvm\\xbc\\xeb\\x80\\x02<\\xd2}\\x06\\xbc\\xea#L=\\xb3|\\xa8\\xbc\\xfe\\x06\\xc9\\xbcs\\x919=7\\x8f\\xab\\xbbA\\xe5\\x9d\\xbb2rx;\\x7fR\\xe7\\xbb\\xf2\\xdav=\\xa0B\\x85=Qb\\x19;\\x05\\xfd\\xb4:\\xb0\\xb3\\xab<E\\x12\\x8a\\xbd!\\x1f\\x06=\\xc8\\xefE<T\\x0b\\xa9\\xbc?\\x1d\\x93\\xbc\\xbfI\\x19\\xbc\\xc1\\x0b\\xce\\xbc@A\\x0e\\xbd\\x9d\\xe8{<t\\x05\\xfe\\xbbQ\\xd0$\\xbc\\xe7\\x7f\\\"\\xbc{\\xf3B\\xbd\\xdaT\\x99\\xbd\\x8b\\\"\\xd3;\\x8e\\xacp\\xbb\\x9b\\x8d\\xc5\\xbd\\xc5\\xde\\x8f=\\x98\\xa2\\xc6<@\\x9e\\xa4<\\x03>d\\xbc\\xfb\\xe6\\\"\\xbc\\xde\\xc8\\x0c\\xbd\\xba\\x8e\\x86\\xbd\\x80\\x1d\\xd1\\xb9\\xcf\\xc9\\xbe<N\\xcej<\\x98\\x88\\xd1\\xbb\\n\\xee\\xec\\xbc\\x1alS<\\xb35\\x1a=%\\x9b\\x0b=w\\x10==^\\xf6]\\xbc\\x10\\xc8\\xbc\\xbc\\xb6x\\x85\\xbd\\xfa\\x8d\\x03=\\x15\\xa6\\x9e<\\xd8zl=P\\xc2-<8\\xf1\\x0e=\\x1c\\xd8\\x9b\\xbdB\\xdd\\xff<\\xeb\\x83P<:\\x0e7\\xbd\\xb3q/\\xbd\\xf4\\xd8K\\xbd\\xea\\xd4l<7\\xbc\\x02\\xbdb\\xf3/\\xbc\\x99]\\xff\\xba\\x0e\\xce.\\xbb\\xa2\\xd8\\x19=:\\xa8\\xaa9dG\\x80\\xbd\\xc0\\\\N\\xbb\\x83&)\\xbd\\r\\xa9\\xeb<\\xde\\xa5\\x82<\\x1c\\xf2\\x84<rC\\x0f\\xbd\\xa6\\xeeL=\\xbaJ\\xd0<pIK=f\\x00\\x18=*\\xbat=\\xc9\\xec\\xa8<)\\x8f\\xd4;\\xc1\\x87/\\xbd\\xf5\\x1e\\x13\\xbcV]\\xd6\\xbc\\xe9g\\x9c<\\xa1:o=A\\x91\\x8d<\\xe9\\xdf\\x05=3?\\x81;\\xedd\\xc0<+\\xcd\\r<\\x8f\\xcb\\n;\\x9d;,=j:\\xb0<\\xb4HL=\\xb4g\\x93;\\xc5\\x17\\xa6\\xbc\\xfeCv\\xbd\\x95\\xb1 \\xbd\\xf9\\xa66\\xbb\\xd1\\xf1Z\\xbce*S\\xbc=\\x90J\\xbd\\xac\\xd16\\xbc\\xc3\\xd8\\xab\\xbd\\x16Y\\x89<I\\xdc\\x1c<\\xbd\\xdf\\x13=vU(\\xbc<-b\\xbd\\x87r\\xbc\\xbcP\\xa2\\x90\\xbd\\xc3\\x9fz\\xbd\\xca\\xc2\\x85\\xbc\\xb0W\\x87\\xbcl=\\x1a<+q4\\xbd\\x8b\\xc5\\xee\\xbcu\\xd3L\\xbd\\x7f\\xe5\\xb1\\xbcb\\xc5/\\xbd\\xf5xB\\xbch\\xa4\\t\\xbdP\\xe5\\xe9\\xbb\\x08\\xbb\\xfb<r\\xfc\\x89\\xbc\\x08\\xc52\\xbd\\xb5\\xad];\\xf5\\xe33=}\\x80\\x05=5\\x081\\xbc\\x8c\\xbe\\x99\\xbb\\x0e\\xaf\\xa3<j\\xa4\\xfb<\\xe2(a\\xbd@\\x03\\xb5<\\x97\\xe1R\\xbc\\x0c\\xaeZ=\\xf417\\xbd\\xac\\xbb\\xd4<\\\"\\x15\\xba=\\xab\\\"2=\\x17M\\x10;\\xb5\\xd4\\x95=\\x90n\\x97\\xbd\\xac7#<\\xd9\\x07\\x81<\\x10\\x10\\xab\\xbbH3\\x17\\xbdRzr\\xbc\\xeay\\x1b=\\xf3\\xab\\x86<\\x82\\x0c\\xa6\\xbc\\x9f\\xceg=\\\"\\x97t<9\\xbcu<q\\x95B<n\\xcd\\x87<\\xd2\\x9d\\x0e=\\xa0,\\xc6;\\xf7.#\\xbdS(\\x16\\xbdJ\\xee\\x94\\xbc\\x08\\x12\\xf9;-\\x15\\x8c<\\xd7\\t#\\xbc\\xe3\\x01\\x1d\\xbdf\\xe3J=%,\\xd39\\xd9\\x0f\\x87\\xb9\\xc6\\x195=\\x9dw\\x8c\\xbc_\\xc8S=\\xee\\xf0\\x17\\xbda\\x067\\xbcC\\xbb}\\xbdE\\xb9\\xb7\\xbd\\x121\\x0f\\xbcS\\xeeD\\xbc\\xf0\\xfc\\xcc;\\x04\\xe3@;J\\x84X=\\x92x\\x93\\xbb\\xef\\xb4\\x8e<\\xdc\\x17\\xc1=\\xf2\\x0e\\x99\\xbc\\x15\\xb2\\x92<n\\xdf\\\"<8\\xa71=8\\xf3<\\xbd\"\nHSET bikes:10085  model 'Callisto' brand 'Breakout' price 2413 type 'eBikes' material 'alloy' weight 10.5 description 'Urban riding, gentle off-road ebike. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"s\\xac\\x1d<\\x07\\xd1\\\"\\xbd\\xc7\\xf6#\\xbd\\x11W\\xa5\\xbbRE\\x9e\\xbd\\xd8-\\xf57]\\x8d\\xd3\\xbc\\x8a\\xado\\xbd\\xdd\\xff\\xef<%o\\x0c<\\xc7\\xdb\\xab\\xbc\\xbe\\x08m\\xbd\\x17\\xf2:=;:\\x0c=\\x12\\x9a+=\\x06\\xc8\\xe7\\xbc\\x13\\xd6\\\\=\\x9a\\xc8k\\xbd\\x1c\\xfdG\\xbd%\\xe0;\\xbd\\xb0`\\xee<k\\xbe\\x83\\xbc\\x12\\xc8\\x8d<\\xb6\\xc2w\\xbc\\xd0\\xb2!\\xbd\\x13\\xd2W\\xbc\\xc1\\x00\\xac<f\\xdb\\xc2\\xba\\x11L\\xe5;\\x19O\\xac<\\xef\\xe2\\x95\\xbb;.\\\"\\xbd\\xd0e)=`s^=\\xe2>4\\xbd\\x94\\xe0\\xd0\\xbc\\xb0w9=\\x9a\\x80\\x8a\\xb9\\xc7\\xa2(\\xbd\\xf4\\xe6\\r<m\\x006=\\xf3\\xdd*\\xbb\\x9d\\xa3\\x82<\\x1bi\\xe4<q\\x8b\\x92;&\\x01\\x11\\xbd\\x99\\xb9\\xf1<\\xec\\xe7\\xda\\xbb\\x9e\\xee\\xdd\\xbc6\\x1d\\x8c;|\\xcdm;S.\\t\\xbd\\xfb\\xb4\\xf4\\xbcwx\\xb4\\xbcr<\\\"\\xbc,\\x83\\xfb\\xbcT75\\xbc\\xd0\\x8d\\x1a\\xbdn\\xc5\\x11\\xbc\\x16W\\xa6=\\xc2\\xf5\\xac\\xbc=E\\xe6<N\\xf5\\x9b\\xba\\xf3S\\xe1;\\x833\\xfd<\\xed\\xbd\\x00\\xbd\\x9e \\xdc;\\\"\\xdb\\x11=\\xd9\\x82U<\\x80\\xfe\\x93\\xbc\\x07`\\xf3;\\xe8\\xe8\\xcf:9\\xb6\\xe7\\xbb\\xee\\x8c\\xb7\\xbd82\\xae\\xbbR_\\xfe\\xbc\\x96W\\xa8\\xbc\\xed\\xe4\\xf7\\xbc\\xbe-\\xea\\xbc=\\xb8\\\\=\\xb0EJ<Z)N\\xbd\\xae\\xff\\n\\xbdu\\x9f\\x98:P\\xb4\\xb9\\xbc\\r\\xfc\\xb3\\xbc\\xa5\\r\\xa9\\xbc\\xa9\\xf5\\xdc<\\x1e\\x10\\x0e<\\xb8\\r\\x89<,K\\xd5<!ie;c\\xe4\\x97\\xbb^\\x9c5\\xbb\\x8b\\xc4\\x10=\\xa5\\x90\\x9f<!\\xa3\\x13\\xbd\\xd2\\xb3\\xa0:\\x15\\xf0\\r=\\x8c<\\x12\\xbd\\xab4\\xc0\\xbd\\xd0\\xc2\\x14<c\\x00z\\xbc\\xc7x\\xbc\\xbc\\xe0\\x87p=d\\xd3\\x04>\\x9ep3\\xbc\\x03\\xda\\xb0<?j-\\xbd\\xa4\\x89#\\xbd\\xac\\x837<\\xf3\\xc8\\n=l 4=\\xc2\\xa0\\xb4\\xbc\\xb9lq\\xbd\\xcf\\xe1/\\xbduT\\xe5\\xbb\\x0bS\\x94<g\\x87\\x1e=\\x0b\\xe5\\xd6\\xbb[4>=\\xd6\\xe5\\xfb\\xbbC\\x10\\x82\\xbd\\x87wH=\\xf9\\xb1{==\\xc1\\xc0;+BF<\\xc5\\xd2\\x04\\xbc\\xf8\\xd3o\\xbd?\\x1cg\\xbd\\xd1[%\\xbds65\\xbdG\\\"\\xcf<\\x1c\\xe8\\x16\\xbdys\\xe4\\xbb9$5=8\\xdb!\\xbd\\x05}a\\xbd|\\xaf\\xbc<\\xa7\\x19;\\xbcT\\x03J\\xbd\\xa9~Q\\xbd\\xa2m\\xbc\\xbb\\x97\\xff:=\\xe2\\xce\\x19\\xbd\\x16\\x1b\\xbe<d\\xabG\\xbc\\x19\\x9b\\xf3;mB\\xef\\xba\\xf9\\xbc\\x8a9\\xf4\\xb3\\xfa;\\xa5S\\xdd<\\xaa\\x17\\xd7;\\x91\\xb9\\xee<\\xc9\\xd30\\xbb\\xfb\\xde\\x1f\\xbc\\x98\\xfc\\xf0\\xbae\\xa5\\x9a<\\xa8G\\r=\\x9eW+\\xbd\\xb5\\xeal=:\\xde\\\"\\xbc\\xb4\\x86\\x90<\\xeb+\\x83\\xbc\\xaf\\xb0Z<5\\x0f\\\"=\\xafO\\xff\\xbb\\xa1\\xff\\x1a=\\xf2w&<\\xaf\\xd4\\xb0\\xbc2\\xf1\\x92\\xbbe\\x0b\\x8f=\\x11\\x8e?\\xbc|dh\\xbd\\x04\\xc1\\xb1\\xbc\\x0c\\xfb\\xc9<E\\xd4\\xc9\\xbc\\x8d\\xfd =\\xeb\\\"\\xa6\\xbc^6\\xac:\\x11\\x9a\\x90\\xbc\\xe7\\x0bj\\xbc\\xae\\x82O<\\xb5\\xfc\\x1a=\\x14&\\xd0<\\xcb[8\\xbd\\xf6D\\xb1<\\x15\\xb8\\xce<//\\x12=\\x81\\x82\\x96\\xbc)Z\\xee<\\xa4\\x07\\x97<\\xb8[\\xa3\\xbd\\xb2\\xe8p=cHE<\\xcfmV=\\x12j\\x16;\\x1bg\\x08\\xbbq\\x80\\xde:\\x02jP\\xbb\\x91;\\x8b<\\xe5a\\x00\\xbd\\xed\\x03\\xb6\\xbd\\x9c\\xde\\xe3\\xbc\\xec5p\\xbclty=\\x00|\\xfe\\xbc\\xab\\x81\\x1f\\xbd\\xaf\\x07O=\\x8bn\\xd2\\xba\\xe9\\x0b\\x03\\xbd\\\"\\x0c\\x8c=\\xdb\\x80\\x93=\\x94\\xf7\\xc8\\xbb\\xabf`<\\xf1\\xdb.=5@\\xe3\\xbd#\\xcc\\x919\\xa1^\\xb3;jA\\xee\\xbc\\x82\\x9a\\xae\\xbc\\x07\\x80K\\xbct\\xa6B\\xbd\\x96\\x15\\xfe<\\x9eG\\xbc\\xbbP\\x93y\\xbb\\xb3\\xfdx=y3\\xb6:\\x10\\xc3-=\\xb5\\xfb{<\\x13\\x02\\xf5\\xbc\\x03\\xdd\\x81<\\xee\\x11\\x88=\\xedf\\x90<+\\xf5\\xc2:\\xff\\x88@\\xbd0\\xd40\\xbb\\xfbj\\x1c=f\\x8d\\xa5;\\x9a\\x0c\\x8f\\xbc\\x98w\\x1c<xt2\\xbc \\x01\\x18=\\x91\\xbb(=g\\xf8&<n9M=\\xe4\\x9739\\xce\\xec\\xc7\\xbcX\\xa1\\x97<\\xd7\\x97\\\"\\xbc\\x1a\\xbf<\\xbc\\xc4\\xcf[\\xbd[\\xb3\\x8f\\xbc\\xec\\x81\\xe8<\\x0fF\\xb7; \\x99\\xca=\\\".\\x92\\xbc\\xf9S\\x14=\\xa0\\x95\\x1c=\\xa1p\\x07=\\x92\\xc6~\\xbc\\xd2\\x85\\xe3\\xbcGd\\xea\\xbc\\xd1\\\"\\xda\\xbcMp4\\xbd\\x97K\\x00\\xbdn\\xf1\\x17;.@\\x9b\\xbb\\x12\\x8a\\xfc<K\\x9f\\xa3\\xbcZ\\xb3\\xd6\\xbc\\xa17\\x03\\xbd\\xe0.\\x1a\\xbd?\\xeb\\xbb\\xbbrp\\xf1\\xbc^n+\\xbd-\\xbe%=B\\x01&\\xbd\\x8cv\\n>\\xda\\xc9O\\xbd\\x18\\xe9\\x10\\xbb\\xce\\xcb\\x98<\\xd3\\xdd]=` \\xa3\\xbc\\x90\\xa1\\xb7<%LQ=\\xa1$\\x98\\xbc\\x1d5\\xa5=\\xa6\\xd4S\\xbd\\x10\\xab\\r\\xb9\\xbf\\xd1\\xba\\xbbc*\\\"\\xbc\\x87\\xa1\\t=\\xba\\x1f\\xee\\xbc\\xc3\\x94\\xdb<Oe\\xd2\\xbc:\\xad\\xd6;\\x1c\\n\\xed<\\x8f=\\xda\\xbb7\\xb6\\x03\\xbc\\xd3\\xadR=L\\xdc\\xeb:C\\xf6\\x01=\\x99\\x8c0<\\xe6\\x0e\\x8e<m\\xb3]\\xbb\\x02\\xdb\\x9e\\xbbc\\x04\\x1d;\\x1a\\x10K=1m\\xca<x\\xbc/\\xbd\\x7f\\xc7V\\xbd>\\xb1\\t\\xbc\\x11\\xd1\\x1c\\xbc\\x02\\x98I<\\xd0\\xa4\\xb4\\xbd\\xe2G\\xd8\\xbc\\xbd\\xfd\\x85=\\xf0\\xc6\\x90<\\xc4\\xbaK\\xbd\\xda\\x87\\xe3\\xbc\\x10\\x99+\\xbc\\x0b.\\xce<\\x91\\xaa\\x19\\xbb\\xfe\\xf2\\x81<\\xf2\\xd2V\\xbd\\x13\\xdf\\x19=\\x81\\xfe\\xa8\\xbc\\x8fi\\xa4=^67<\\xad\\xa7\\t=\\x98(Y\\xbbf\\xff\\x9b\\xbb\\xba]B\\xbd>\\x81\\x18=Z\\x9e\\\\<W\\xa6\\xdc\\xbct\\xbch:\\xaa\\x03\\t=\\x0b\\x9b\\xad<H\\x850\\xbd\\xde\\xb3\\x19=\\xad\\x1b\\xb1=H\\x85\\x1a\\xbc\\x1b\\xde\\xb2\\xbb\\\"\\x11\\xa5<\\x868\\r=\\xb4\\xe8\\x03:&\\xdeQ=\\x11\\xc0\\xf7:\\xa5|\\xf7;\\nF\\xf39YOM=am\\xe4\\xbc\\xceG(\\xbd\\\"Y\\x1e\\xbcA;/\\xbdhD1\\xbdXl\\x1e\\xbc\\xc4\\x0b\\x88\\xbd\\xd1\\x08\\x9e<f\\xf6\\x01\\xbc\\x85\\x99Q=\\x07\\xd7\\x04=\\xf3I\\xcc\\xbb\\xaa\\xb3\\xee\\xbcYq\\xe4\\xbc7\\xc76=\\x12\\x89U\\xbd\\x17\\xbe\\xb8\\xbc[\\x91\\xb2<\\xa8\\xac\\xaa<f\\x85\\x9c<|\\xb9 <\\xa40)=\\x05i$<\\xf2Y\\xe0\\xbc\\\\\\xdb\\x9e\\xbc]e+=\\xf6\\xb4\\xa8\\xbb\\x92\\xeb\\x97\\xbc\\x02\\x8b\\xff\\xbc?\\x8c0<i^c;\\x10\\x1f\\t\\xbc\\xd0\\xb8]=\\xa9\\xb7\\xd5\\xbb\\x80\\xe3\\xce<f\\xc8\\xb9<\\xc6E\\xbb\\xbc\\xbag\\xe1;1t\\xb6=\\r7\\xfe=O\\xd84\\xbd\\xbdXF<\\xed.\\xf0\\xbc\\x13\\xbe\\x93\\xbc\\x13,\\xda\\xbb\\x9b\\xcb\\xda=\\x12\\\"V\\xbd\\xd4\\x99\\xe0\\xbc\\xfc\\xa3L\\xbd\\xc2\\xb2V=D\\xc2H<:\\xb7z=\\xf8\\xa4/\\xbd\\\"a,\\xbdX\\x11\\x94;\\x9a\\x1d\\x16\\xbd\\xf2\\x13\\x81\\xbbG%+=h\\x0c/\\xbdU\\x87\\t=\\xc2|\\xd6<\\xe2\\xa5\\xef\\xb9\\xca\\xad\\x93<\\xbd\\xd0\\xed<c\\xf2\\x06\\xbd\\xe0\\xa8\\xc7<$\\x82\\xf9\\xbc\\x0c\\x07\\xce\\xbc\\x97Q\\x00\\xbd\\x98\\x85<=MCy=\\x86\\x80*\\xbb?\\xb8F\\xbd\\x81\\x1d\\x84\\xbd:\\x17j<\\x15M\\x93<r\\xa4\\x17\\xbd\\xe5\\xf0\\x98<\\xf7\\xcbK<D\\xef\\x1f\\xbd\\xb7\\xa0\\xc8\\xbd\\xda\\xce\\x1f\\xbc\\xecn\\xb0=\\x15v\\x0b=\\xfaB$<\\xb7\\x93`\\xbdU\\xe0\\xb7\\xbb\\xaf\\xbb\\x1a\\xbd\\xff\\x13\\x9b\\xbc,)\\xbc<\\x84\\xb6_\\xbc\\x16\\xc6\\x95\\xbc*\\x1cG<]U\\x8c<s%\\x8c\\xbb\\x14\\xe9\\x91\\xbd\\x93\\x122=\\xec;\\xb1\\xbc\\x81\\x15`\\xbd\\xa8\\xb6\\x1b;)\\xa9e\\xbdd\\xe09=\\xfd\\xff\\x91\\xbc\\xabR\\x93<\\xbc\\xa7\\xa2<~8\\x8f<m\\x08\\x93<\\x0c-f=\\xc1U\\x17=\\x9b=T\\xbb\\xd7\\xa2\\x93\\xbd\\xda\\xf6\\x12\\xbd\\x87\\xac\\xc9<\\xdb\\xe0\\xd5;\\xecM\\\"\\xbdt\\xc2\\xaf\\xbb\\xd3\\xabm=+\\x19\\xd2\\xbc\\x8b\\xf1\\xaf<\\xe7\\xd1\\xba\\xbc\\x14OP\\xbc\\xcau\\x1f=R|\\x1c;,V\\x16\\xbd_;7\\xbdpg\\x1a;\\xfd\\x01\\x8a\\xbb>\\xe9\\x06<X\\xc5\\xf9\\xbc\\xcfd\\x89;\\xb5\\x00\\x92<*\\x9dZ<5\\x11\\x84\\xbdJ\\xb0\\x0e\\xbc\\x87g\\xa9;L8#\\xbc\\x1f#\\xe1;\\xbd:\\xb1\\xbd\\x9c\\x1c\\xa5;\\xfd6\\xd4;\\xfe\\xe3\\x1b\\xbc\\xd1\\x92[\\t\\x16\\xa2\\xcd\\xbc\\xaaQC\\xbc\\xa1\\xb3\\x9b;\\xc4\\xa8|={4\\\\=6\\xfa\\x01=e\\xf0\\xf0<\\x89?h\\xbc\\x91\\x94+<\\xab\\xb5\\xc4\\xbc\\r3=\\xbc-\\xbaH\\xbb\\x88\\x19M\\xbc\\xcb\\xecE<\\r\\x97\\x88\\xbd\\xcc\\xb4q\\xbc\\xc9\\x1e\\t=l$\\xbd\\xbc\\xb1\\x84><\\xc3\\xefv=3\\xda\\x85<\\xa1\\xe9\\\\=>\\xf3*=v\\xe8\\x0b\\xbd\\\"\\xb7\\xb6;\\x17C\\x06<\\x9a\\xa5(=\\xa0\\x9e.<_\\xb7O\\xbd\\xbd\\xa4\\xb9\\xbc\\xa2\\xf6\\xa4\\xbb\\xa4\\x809</Kg\\xbd\\xbal\\x9c\\xbc\\x02\\xe4\\x87\\xbcN\\xe8\\x8d\\xbd\\xfc\\x85\\x10<Dh\\xef<e\\xa1p<\\xae?V<$h\\xb6\\xbb>\\xb3\\xd2;\\x9cx\\\"<m\\xe0\\xec<r\\xac\\x1a=\\xef\\x7f%\\xbd\\xfb^y=\\x0b\\xf7\\xdc\\xbc\\xe5q\\x12\\xbd\\x7f\\x98\\x9c<\\xd9J\\xc3\\xbcSr9\\xbd<K\\x86\\xbc\\xc6l\\x13=e\\x1f\\xc9\\xbc\\x05L&=\\xcc\\xad\\xb8\\xbcT3\\xd3\\xbc\\x12A\\x0e\\xba \\xdf2=\\xd9\\xd0\\xf7:\\xe0vK\\xbc?\\xf5\\\\\\xbb\\xe3\\xcf\\xfc<\\xb44\\xc5\\xbbQa\\x17\\xbd<F\\x8b=\\xf3\\x81\\xfe\\xbc\\xf1\\xa5+<\\xc62\\xbd<\\n\\xf3\\xca;0\\x9b,\\xbb\\x19\\x9fQ<\\x1b\\xf3J=\\xb3\\xf1\\xb5<\\xfbt\\xdc\\xbc\\xc6\\x85{\\xbcW\\xe5\\t=\\x83\\xaf\\x9b=\\xee\\x00\\xf6<$]n\\xbdre\\xd1\\xbb&j4\\xbd\\x8a\\xad\\x15=\\\"\\n\\xdf\\xbcx\\x1b\\xfb<y\\x8aI=\\xf9\\xc7?<\\nP\\x11<\\xb6\\xc9\\x04=\\xc9\\xa7\\xf7\\xbc\\xc9sq\\xbdA\\x85X=C\\xba\\xea:\\x8b\\xa1\\xb4\\xbb\\xd9\\xfd\\x93<\\xbd\\xacF\\xbd\\xc0{c<,\\xe4\\x82<6\\xb0N=\\xbe=\\x93<\\x0b_\\xf5\\xbbt;\\x88<w\\x84\\x94;\\xfe\\x98Z<X[.;\\x8c\\xe4\\xfa\\xbbu\\xa2\\xb0\\xbct9\\x8f\\xbc6\\xd0\\xbc\\xbc\\xefOA=\\x08s\\xcb\\xbc\\x90\\x19\\xd1\\xbc\\xdc;.=\\\"\\xcd\\xa6=\\xa1\\xcd\\xb9\\xba\\xa4\\x1aU\\xbb\\xfd@e\\xbc\\xb0G\\xb1<U\\xebL;+\\xb43<\\x8cN\\xa9<\\x10\\x82\\xa2<\\xb9\\ns=\\xa8\\xd2\\x9b\\xba\\xe2=\\xaa<\\x88\\xf0B\\xbd\\x9aK\\x90\\xbb\\x83?\\xa4=\\x8ch\\x16\\xbd\\xeb\\x9a#\\xbdb6\\x93\\xbd\\xe5\\x15\\x0e<`.U:}]\\xe6=\\xf0\\xe9\\x1b\\xbdr\\x8bV=\\xe2\\xe6A\\xbd\\x06\\x96\\x8f<\\xd2\\\"Q\\xbb`\\xe4~:Hy\\x04\\xbd\\xf8\\xddW\\xbd#\\xf6&\\xbcs\\xf1%\\xbc\\x11q\\xf7<\\x80+\\xa9=\\xfc\\x1d\\x8a;\\x11\\xb4)=\\x87\\xa3\\x98\\xbc8wC\\xbd\\x0b\\xe4\\x05=KJ\\x1c\\xbdkX\\xea<\\x9e\\x95\\xa0<\\xe4\\xdc\\xb9;?\\x00n\\xbd`\\xaf\\xc4<\\x0e\\xd9\\x08<?\\xb8x<\\x9c<i\\xbc\\xe3\\xf9\\xae=\\xd2\\x90\\xd7<a\\x13$;x\\xeeD\\xbc&\\xfdy\\xbb\\x80>\\xdf;Q;p=c:s=\\xff\\x19\\xdb<\\xf2\\xe9\\xd4;KT\\x94;[h\\x939\\xf86\\xbe;9\\x8b_<}5-=\\t9\\x96\\xbc;\\x17\\xa4<\\xcbE\\x92<\\xc97\\x17\\xbd\\xf1Y\\xd4\\xbc\\xb9\\xed\\x8f\\xbd]\\x0b\\x9e<A\\x9c\\xa4<\\xf9\\x1b\\x13;\\x96v\\xf2\\xbc\\xddS\\xb4\\xbc\\xf6\\x01\\x89\\xbc\\xe8eE<#\\xb5/<\\x85`9\\xbcjv\\xa77\\x9aKm\\xbd5\\xba\\xa4\\xbc)%\\t\\xbd\\xa9z\\xa7\\xbd)\\xb36\\xbd\\t\\x1e\\xd4\\xbcwY\\x96;\\xaa<\\xaf\\xbcR\\x9d\\xbf\\xbcH\\xf1\\xbe\\xbcA\\xa8\\xc8:=\\x8c\\x8f\\xbd\\x99\\xb8!=\\xa3\\x07V\\xbd[\\x0b\\x86<\\xa4 \\x04\\xbd\\x15X\\xba\\xbbz[\\xb8\\xbc|\\x85\\x19\\xbc\\xaa\\xd4n\\xbc\\x08:\\x17=&\\xd0\\xd2\\xbb>\\x19k\\xbbXUU<\\xe8\\xb5=;\\xb6\\xee\\n\\xbd\\xa3J?\\xbd\\xbf\\x9b\\xc3\\xbd\\xd2\\xee\\x83;\\xcdM\\x98\\xbc\\xdf\\xd0\\x06;\\xcd7\\xf4;P\\xbc_=\\xff\\x19J\\xbdAPY=`\\xa9H\\xbd\\xa3U\\xa0\\xbd\\xc5pu=s\\xf2\\x81\\xb5,u\\xc8\\xbc\\x91Xx\\xbc`JH\\xbc\\x13\\x0bO;\\x8c\\x10\\x0e\\xbc\\xc4X>\\xbc\\x99\\xc5\\xc4;\\\\\\xfes=\\x06B\\x0b\\xbc\\x98\\xcc\\n\\xbc;\\xa1\\x1f=\\xe8[\\xb0;\\xc4\\xd2\\x83=\\xcd1\\xd9;\\\"!\\xfb:\\xec\\xd1B\\xbd8\\xb1h=\\x92,\\\"\\xbd\\x13\\x03\\x1e<\\x03\\xf2J<J\\x87%\\xbcZf\\xdf;\\x12 N=\\x1e\\xcb2\\xbd\\x854\\x00<\\x9e\\x8f+\\xbd\\xa0{M<7\\xb8L\\xbc\\x015\\xcc\\xbdk>2=\\xd1\\xea\\x0e\\xbd\\x1b\\xa9\\xbe\\xbc\\xdaH\\x82=h\\xad\\xc9={!\\xc3\\xbc\\xae\\xfc\\xb4<\\xbaQ\\x0b=\\xd3x\\xe3<\\xd9\\xd8\\xca=^?\\xed\\xbd\\xb2\\x8b\\xf9<\\\\\\x0b{\\xbc\"\nHSET bikes:10086  model 'Neptune' brand 'Ergonom' price 1875 type 'Kids mountain bikes' material 'alloy' weight 7.3 description 'Kids want to ride with as little weight as possible. Especially on an incline! At this price point, you get a Shimano 105 hydraulic groupset with a RS510 crank set. The wheels have had a slight upgrade for 2022, so you\\'re now getting DT Swiss R470 rims with the Formula hubs. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\x1b\\x80-\\xbd\\xd0\\xf5\\xd2\\xbc\\x85u(\\xbd\\x1d~\\xda<X\\xc3\\x1e\\xbdq\\x9em<;9\\xd4;V+\\xa2\\xbb\\xd8>\\xb4=\\xcc\\x91(=M\\x85\\x81=\\x91x\\x80\\xbc\\x84\\xdcm;An\\x83\\xbb\\xb7@\\xb8=z\\xa7\\x00\\xbd\\xbeR\\x98<\\x1c\\x0f\\xa4\\xbd\\xd2<\\x8d=\\xfa0\\x9a\\xbd\\x86Z#<*\\xb0\\xbe\\xbc\\xaf\\xb9\\xc5<\\xac}h\\xbch\\xf8.\\xbd\\x8c\\xfew<a\\xf2x=\\xac{h\\xbc\\xf0\\x94\\xf8\\xbc\\xd8p\\x19>\\x08\\xe0\\xe5\\xbc\\x18\\xcb\\x1d\\xbd\\xca\\xf89=\\xda\\xb5\\x8b<b\\xe3h\\xbd\\x12\\xd6.<\\x1c@\\xf3;jB\\x12\\xbb\\xe0\\xcc@\\xbd\\x99\\xe7\\xce\\xbb\\x99\\xcc\\xbc<q=P\\xbb\\xfb\\x18\\xdf;\\xb1\\xdb\\xd2\\xbbe\\xef-\\xbdup\\xbb\\xbbG\\x8d\\xc7=\\x10\\xa8c\\xbd\\x81\\xa1&\\xbd>\\xd4\\x97<\\xe9\\n\\xb2\\xbaam{\\xbc\\x9e\\xdc\\xdf\\xbc\\xd6c\\xca<\\n\\xd7\\xba\\xbc\\xe5\\xd1V\\xbc\\xcb9\\x9a;\\xd5\\x8a\\xdc\\xbb>Z\\xde\\xbb]\\xfbM=\\xe9\\x01\\x86<\\xa0\\xe0;<\\x03\\x8d\\xfe<@6\\x83\\xbc\\x17\\xe5\\xde<\\x82Y\\xc7<\\xd9\\x7f\\x8a;\\xfaD.\\xbd\\x85`\\x94<\\xcd\\xb9\\xb0\\xbc\\x97\\xa3\\xf2<?v\\x00=(\\x15>\\xbb\\xb1\\x92\\xb2<\\xa3H\\xa8\\xbb\\\\\\x12\\xed\\xbc\\xeeG\\xa3<V\\xff\\x02\\xbc\\x08+\\xb5\\xbd\\xcaY/<\\xeauZ=\\xfe8\\xdb\\xbb\\xd4@\\xef\\xbc_\\x85\\xac\\xbd\\x16\\xf2\\x06=\\x8c\\xaf\\xd2<\\xf3\\x91\\xb1<\\xc2\\x95Y\\xbc\\t\\xab4=\\xf1\\xe2{\\xbb9\\xab\\xf4\\xbc\\xb3\\xa6\\\\\\xbd\\x1b\\\"o\\xbd\\xe4(\\x05=\\xe7\\x1b><\\xf1\\xc2\\xe2<\\x1dt\\x92\\xbd-\\xf6\\x91\\xbdp@l=p\\xd9\\x9c\\xbc\\x88\\x83\\x17=\\x12|\\xb2;\\xde\\x87\\xba:\\xbc\\x07\\xa0\\xb8\\xef\\xeb\\x93\\xbcTg\\r>B\\xda\\x89\\xbb\\xd3\\xf2\\xb5<\\xf9\\x16\\x97;\\xc4\\xd6\\x86\\xbc6\\xc2x\\xbb\\xe6\\x14\\x82=\\xf2w\\xe1;\\xe3\\xad\\xd2\\xbc!\\xe0\\x86\\xbd\\xe8E\\x8c\\xbd>m7;\\x93Q\\x93<@\\xab\\xbe<\\x00\\xe7\\x0b\\xbd\\xd3a\\xf7;_|\\x07;\\x14\\xf5\\x9b:\\xe8]\\x8f\\xbc\\x0eM\\x07=\\xb9\\xcd\\xdf<\\xf0t\\xc1\\xbaYb)\\xbd\\xe0\\x8be\\xbdU\\xdc\\x83\\xbdA\\xf2\\x04\\xbd\\x8b\\x841<H\\r\\xce<\\x8e\\xad`\\xbc\\xa8\\xc4}<!\\xad)=_]g\\xbc\\xf5\\x1f\\xc0\\xbc\\xfc\\r\\\"<\\x1au\\xd6\\xbcb^\\x1b\\xbd\\xc9\\xfbz\\xbd\\xa2|\\x14\\xbc\\x06\\x92\\xc4=\\x08\\x11\\x81\\xbc\\x19-\\xe2\\xbc\\xa8a\\x9c<\\xba\\xac==\\xbd\\xe9H;\\xb6\\xff\\x83<~\\xd7\\x9d\\xbc\\x87\\x14\\xdf<Pl9\\xbd\\\"(x:\\xb9.\\x90\\xbd\\xc7\\x9c7=j\\xbfF<\\xec\\xeeG\\xbd\\xfd\\ni=\\xf8w\\x14\\xbd\\xe1\\xc6-=\\\"M/=sN\\x19\\xbc\\xe7\\xb4u\\xbd\\xa9\\xca\\xc0<\\xba\\xa4;=pr\\x87\\xba3h\\x08=\\x01\\xf4\\xb2<\\xdc\\x02\\x12=T^\\x90;%\\x15\\xc7<\\x10\\xf06<\\xd0:#\\xbd\\xe9l\\xb0\\xbc\\x1d<\\xe3\\xbb\\x0b*\\xd4\\xbc\\\"]\\x82=\\xdd\\xfa_\\xb8\\rDV\\xba\\x91\\xa4\\x02\\xbd\\xbbX\\x1e=\\x0b\\xd8K<\\x87ui\\xbc\\x18\\xb6\\xf2<\\x00\\xa5\\x06\\xbd\\x96(\\xab<\\x1bw2<\\xa6Lp\\xbc\\x1c\\x82\\xed<Bm.=p 2=\\xe9\\x8e/\\xbdE\\x96\\\\<\\r)a;\\xf9\\x15\\x88\\xbc\\x008\\xc5\\xbc\\x1f\\xde\\xaf\\xbc\\x15\\x94\\x11\\xbd\\xef\\xe60=\\x05d\\xfd\\xbcn\\x8fu\\xbdE\\xd8\\x82<\\x83\\xa8N<S\\x94\\x1a;\\x16\\x9dN=%Zd=*Q!\\xbd\\xd3\\xe7e;\\x02\\xbb\\x17\\xbc\\xd5\\xf7\\xa5\\xbc\\xc9L\\t\\xbd\\x85Ff=y9\\\":\\xb6\\x05\\x90;\\x0b\\xbb3<\\x1f>\\xaa<c\\x05@\\xbcT\\x9b\\\"\\xbd}\\xfb[\\xbd\\xe0O8<\\xd2\\xf9\\xca\\xbbB|{\\xbc\\xa4Z\\xa7\\xbbC\\xc3M\\xbd\\xbd\\x83\\x11=B\\x08\\x1d=i\\x01\\xe7\\xbc\\x1a.\\x9e=\\xb2wm\\xbb\\xd4I\\xab\\xbb\\xad,P\\xbd1\\xff\\x08\\xbdp\\x11\\xfa<$\\x898\\xbdKW\\x08<\\xc0\\xf1\\xbe\\xbc;\\xa8\\x18=-\\xb7Z<aA\\x80<\\xc3\\x8aP\\xbd\\\"Z\\\"<l\\xfa\\xc2<\\xaf//\\xbc\\xd5\\x8e\\x08=\\x1b\\xf7\\xc0=\\xfe-\\xe0\\xbb\\xcdb\\xe8\\xbd\\n\\xaa]\\xbd\\x97\\xc8\\x02<G\\xa99\\xbc\\x96\\xac;\\xbd\\x98\\x1b\\x89\\xbc\\x80g\\xa4\\xbb\\xf8\\xc4?\\xbd\\xacgK=\\x83HG\\xbd\\xdd\\xd9\\xbc;\\xf8\\x98\\x9f\\xbc$\\xae\\xdb<+,\\xfd\\xbc\\xf3A\\xca\\xbc?\\xe8.\\xbc\\xf4\\xd9\\x81;\\xb1D\\x02\\xbc\\xe8\\xda{=\\x19)\\x8a\\xbc\\xd5\\xe2\\x8e\\xbc\\x98\\xcc\\x90\\xbavg\\xa7<p\\x9d\\x80\\xbc\\x8ai\\x92<R\\x8e\\xab;\\xc3s.=\\x0e#\\x13\\xbc\\xb0\\xf8\\\"\\xbdWHh\\xbc\\xea\\x97\\xd9;\\xd9P\\x9f=o1\\x98\\xbdB\\xe3\\x19\\xbdo\\x9c\\xc4\\xbc\\xbf\\x00\\x00\\xbd1\\xa4\\xb6;\\xb5e\\x8b<\\xf3\\xb7\\xf3\\xbb\\xb5\\xe8\\x93=O5/\\xbc\\x88\\x87v\\xbd<,\\xcc\\xbb\\xd3Z?<\\x85}\\x1b\\xbd^<\\xc2\\xbbb\\x88\\xf4;W&\\x9e<\\xd9b4\\xbd\\x84\\xb5\\xe9;\\x16\\x96=<\\xa1\\xa7#\\xbd}/y\\xbb\\x96\\x97\\x03=d\\xaf\\xa9<\\xec\\xc9\\x0c=\\tGg\\xbd\\x03\\xa2u;\\xa1\\x9d\\x0b\\xbbS\\x8bt\\xbc1\\xf5X;\\xf6\\xc5\\xa6\\xbc\\x0cl\\x80\\xbdv`\\x05<\\x1a\\xcfu\\xbd\\xae\\xc0\\xe2\\xb9\\x18\\xf9\\xd4<\\x86\\x80\\xaf<\\x17\\xd4\\x8b\\xbcZDU\\xbd\\\"(\\xa5=\\xfd\\xd2\\xc8<<Na\\xbcq\\xb1\\x15\\xbd@\\xd0\\xe7<\\xb3\\xa6\\x7f<&\\xe9\\x0e<<\\x0e\\x12=S\\x90*\\xbdjz\\x13<\\xe8:\\x83\\xbcpV[=\\xb2&\\xe3\\xbbJ\\n\\xd2<\\xd9\\x1eM\\xbd\\xe0\\xbd\\x1b\\xbc\\x8b\\x8d\\x8b\\xbcJ\\n\\x0c\\xbc\\x0fs\\x1c=ws\\x04;\\x12wo<\\xc3\\x02\\n=b\\x9c\\x01=\\x12\\\\\\x03\\xbc&\\x03:\\xbcG[Q=9j\\x1d\\xbd\\x11]\\x8f<)s-=\\xcb\\x8ar=S\\x90\\xc5<7\\xe6\\xeb<\\xb7F7<\\x07\\xb0\\xc8\\xbc}4\\x96\\xbc6\\xc4B=|g\\x8b\\xbc\\x82\\x11D:\\x9dI*=\\x92\\x1e+<\\xef\\xee\\xa9\\xbdzJ\\x16\\xbc\\xe1\\x00\\xf0\\xbdI\\x0b\\xc5\\xbc\\x9d\\x18\\x05<\\x96\\r\\x96<\\x13\\xc7~=\\xf5\\xe4\\xd5<7\\xbb%=\\xa9\\x94t\\xbcj\\xef\\xa9<\\x14\\x927\\xbbX\\xde\\x19<\\x81%\\xc8<\\xc4\\x1b#<\\xf6A\\x11\\xbc?\\xc6\\x8d;-1\\t=p\\xd89=\\xc2\\xf5q\\xbdn0\\x1c\\xbd\\x16^\\x19\\xbb\\x8fU\\\\=i\\x1fr\\xbd\\xe0\\x94\\x0f\\xbdYI\\x9d\\xbc\\x05\\xc6\\xb6<\\xc8\\x88\\xb4\\xbc\\xd2\\x8eJ<\\x95\\x8f\\x9c<\\xf4\\xf1\\xcd\\xbcoG\\xb3=\\x03\\x91n<\\xed^\\x1b\\xbdY7\\x82=\\x11z^=\\xf9\\xcb\\xb3<\\x9d\\xac=\\xbb\\xc5*\\x1d\\xbd\\x0c\\x7fh\\xbd\\xce\\x1c\\xc0;l\\xf3\\x8f\\xbb\\xf2\\xea\\xa3\\xbc\\xc1\\xb1\\xa5\\xbc\\xa3\\x14b\\xbd4\\xe7{<sD<=\\xa3\\x0b\\xc16\\x98\\xf8\\x9e\\xbc\\xe0\\x17g<\\xd2\\ts\\xbc9\\x9c\\xa3=Qe\\xb1\\xbc\\xd0U_=\\x87+\\xcb\\xbc\\xe5\\x1bO\\xbd\\xdf:\\\\\\xbc\\x87q\\x04=56\\xf4\\xbb0~\\xdf\\xbc\\x92\\x1c\\x88=\\x9f\\x8c\\x1a=\\x1b\\x9c\\xe2\\xbc\\x08\\xed6\\xbcG\\xe4\\xf8\\xbc\\xff\\xd8f:\\x16\\xe9\\r=\\xd8\\xcf\\n\\xbca\\xe1b\\xbd\\xd2r\\x81<\\x98\\xaa]=\\x14`h<\\xce\\x8d<\\xbb\\x9a\\x9e6<!f>\\xbdq\\xdd+<\\xfc\\x19\\xc1\\xbd\\x804&\\xbc\\x80%i=\\xdbo\\x10=\\xf7\\x8d\\x8c=h\\x9a\\x08\\xbc\\xcd$E;\\xbe\\xf4\\x1d<\\xac5I\\xbb\\x9a\\xe4B\\xbc\\\\>\\xfd\\xbb\\xcd\\x05\\xee;\\x9em\\x91;i&S=l\\xba\\x0e\\xbd\\xa7\\tK<\\xcd\\xa1\\x96=\\x03s7\\xbd\\x95\\xc4\\xa4\\xbd\\xe5w6\\xbd\\xaa\\xbfo\\xbc\\xb8\\x15|<\\x8f[j\\xbc\\r\\x90\\x82\\xbc\\x16\\xf7n\\xbd\\xd3\\xfa7\\xbbN\\x0e\\x04=\\x92,\\x9c=\\xb0\\xd6\\xb8\\xbb\\xe1\\xe7w=\\x0e\\x8b\\xb4\\xbcN\\x897\\xbc6\\\"7=\\xc0\\n\\x9e<O\\x89\\x1c<\\\"\\xda\\xae\\xban\\ta=\\xecNb\\xbc#\\xd8Q\\xbc\\x8b\\xcf\\xeb\\xba\\xbc*\\xba<\\xa8S\\x9c\\xbc\\xce\\x0b\\x0b\\xbcKB~\\xbb\\xb1\\xb4\\x9f\\xbd\\x8a\\xd2\\xdb<\\xa9\\x0f\\\\\\xbc^\\xba\\x14={\\x7f\\x96:\\xd8\\x08i\\xbcA\\xafL<\\xab\\xb0w<A\\xac\\xe7\\xbd\\xf9\\x9c\\xc1\\xbce\\xe1\\x9c\\xbc\\x04]\\xc2\\xbct^\\x80<\\xff\\x88\\xab\\xbb\\xde\\r\\xc9<\\xbc\\xe9\\x0c=-\\x9b$\\xbc\\xbc\\x92g\\t\\xffM\\x00=\\xcd\\xd3\\xd9<Y\\x87\\xe9<\\x08\\x03\\xc2\\xbc\\x87/\\x13\\xbd:3\\x13<\\xac\\xf7\\xe5\\xbbeU\\xc0\\xbc\\x1a\\x1b\\x04<\\xbb\\xb8/\\xbcfz\\xb3;\\xd0\\xa3\\xe7<b\\xe8\\xb1\\xbc\\x9b\\x12[<\\xdbZ\\x8d:\\xb8\\x8b\\x0f=\\x8a\\xd8\\x1f\\xbd\\xcd\\xfc\\t\\xbd\\x9f\\xae\\x97\\xb9|\\xddN=\\x80\\x0e\\x86<\\x12Y\\x08=D>\\xa7;<]>=\\xe1T\\xc2;\\x12XM=\\x82\\xb6#<\\x1b\\xf3\\xfd\\xbb\\xc8\\xb63\\xbdo\\x95@\\xbd\\x99\\x87&=\\x94\\x18\\xa9\\xbc\\xbc-\\x0c8B\\x9b\\xc6\\xbc\\x8e\\xb7#<lpN<\\xd5;\\x11=\\xfe+\\xac<\\x9e{\\x9f<b\\x91\\n<!u\\xa8<\\xaeD\\x05\\xbd\\x1e\\x87{=\\xcf\\x84w;\\xe9&z<\\xaf\\xf3\\xf6\\xbc\\x89\\xd7\\xf0=\\x1f?\\\"<!&\\xb6\\xbaI\\xbf\\xab<\\x88\\xcc\\xc6\\xbc\\x1f\\x15\\x95\\xbd\\xdc\\x9f^=D\\x85\\x9d\\xbc\\xb7\\xd4\\x15\\xbd\\xc0I!=z\\x9b`\\xbd\\x9e\\xd2s<\\xda\\x0b#\\xbd\\xd0\\xa9\\x1a=\\x89\\\\\\xa6\\xbcB\\xd3{\\xbc\\xd1\\xc7\\x81\\xbc\\x1a_?=]?\\xc0<\\xf0\\xeb<\\xbd\\xa8\\xbb\\xea\\xbc\\xebc <\\\"f\\xef<\\xdcCq\\xbd\\x87\\xab\\x19=u#\\x1d\\xbd\\xe7\\xbct\\xbc\\xcc\\x90\\xc2<\\xef\\xe1\\x92<9\\xa8\\x88\\xbc\\xb6\\xac\\xe6<\\xd7\\xac\\x16=\\xa7\\xe9\\xff;\\x0e\\x97\\x05\\xbc\\xb1\\x07\\xfe\\xbc\\xb4\\xf0`<`\\xd1\\x15\\xbdv \\\\=%\\x81\\x94\\xbc\\xf6\\x86,=\\xeb\\x18\\x16=_\\xc4V=?\\xa8\\xf0<\\xd1\\x07\\xf3<\\x9e\\xa3\\x8d=K8P<\\xa5UU=\\x85\\xa6/:\\xbb=\\x0c=cW\\xb7<\\x85\\xd4\\x10\\xbcg\\xab\\xb6<\\x1d-j<K\\xc3Q;\\xad\\x13\\x02\\xbd\\xf9c\\x7f<\\xa5a\\x04<\\x10\\x86L\\xbc\\xe8`*=rB\\x10\\xbc\\xe4\\xac\\xe2<B\\x80\\x03\\xbc\\xbfT\\xe0\\xbcY\\xde\\xcc\\xbba\\xd5\\x91=~\\x19\\\"\\xbd@>\\xd7:\\r\\xb0\\xcf<\\x0e\\xed`<\\x9a\\xaf\\t\\xbc\\xcd\\xc1M=\\x90\\x85D\\xbd\\xa9C\\xb1\\xbd\\x85\\x0c\\x11\\xbd\\x1d\\x19v=gi\\xa4<![\\\"\\xbd#\\xe7\\xe6\\xba\\x92\\xad\\x9c<\\xc4R*<$\\\"\\x1d\\xbd\\xfd\\xe9\\xc8\\xbbm\\xcf\\xe7<\\\"\\x86\\x95\\xbd\\xd3\\x92%\\xbd\\xa2WX\\xbdN\\xd2\\x92<H\\x08l\\xbcn\\xcb\\xa3<x\\xc2&;h\\xcb\\xbb<\\xbd\\x1e0\\xbd\\x08\\x07G\\xbdL`\\xab\\xbd\\xd6l{\\xbc\\x173Q\\xbdM\\xe7\\xe5\\xbb|\\xbb\\xce;\\x9a\\xf4\\xe5<\\xe2DF=<\\xe2\\x91=\\xea-\\xa0<\\xfc\\xda8=z\\xa0#=\\x0eUa\\xbc\\xa5\\x92;\\xbb\\\"\\xaa\\x83\\xbd\\xa1\\xf8\\xba;_\\x9a\\x0b=,\\xe5\\x08<Y\\x91\\x80\\xbcVO\\xab<i,\\x98<\\x0f\\xdds\\xba\\xf9\\xdd\\xe9\\xbb\\xcf\\x00:;\\x88\\x84\\x9d\\xbc\\xbe$\\xef\\xbc\\xbaB@\\xbdx\\x97\\x0b\\xbd,\\x8b\\x0b\\xbd\\x18T\\xd8\\xbc\\no\\xf7\\xbb1L\\x00\\xbdXs\\x10\\xbc\\xcb\\x81\\x8c\\xbd-?|<\\xe2;\\xf9;*Fi;\\xa8\\x06\\x08=MB\\x02\\xbd?\\xfb3=\\xb4U\\x86=!\\xe8\\xdd\\xbc\\x9b\\xf0\\x84=\\xe4\\x8e\\xc5\\xbd\\xe8\\xa9J<\\xda\\xcf,\\xbd\\x8d\\x0c\\xe5<\\x95\\x83M\\xbd\\xcf\\xfc =\\xb2`\\x7f;j\\x82\\x91<\\xbb\\xc6\\xaa\\xbc\\xe2\\x9fe\\xbd\\x84\\x9d\\x03<]\\x93#\\xbd\\xe20\\x11=K\\x8a\\xd4\\xbc0\\xb6\\x05\\xbd\\xeb\\xdc\\xb5\\xbbFI?=c\\x91\\x1d\\xbd\\xc1\\xc3\\x1c\\xbdAP[\\xbdC{&\\xbcy\\xe0\\xbc;\\x7f\\x9e\\x8b\\xbd\\x93\\x80\\xf0<>\\x13\\x9e;\\xa9\\x14\\xcb\\xbb|\\\\\\xb9\\xba\\xac\\xa7\\xee<\\x1c\\xd2\\x1c\\xbd\\x94?%=\\x0c\\x9fM\\xbc\\x9a/Z=\\x9fc\\xbf\\xbc\\xec\\xed\\xe0\\xbc\\xd6j\\xcd;\\xad \\x02=N\\x8f\\x07\\xbd\\x12\\x92\\x04\\xbc\\x92\\xe6\\xf1\\xbc\\xed\\xa5G=\\xbc\\xf6]\\xbd\\xd6R\\x8b\\xbb\\x7f\\xa0\\x0c=W\\x95\\x8d<=\\xe2\\xa0<X\\t\\xa1=\\xd7(o\\xbd\\xccy\\xae\\xbd\\x19X\\x0b=\\x0fg\\x87<\\x00\\xb1\\xcf8\\x94N\\x80\\xbc\\xd7K\\xcf<r?%=BG\\xda\\xbc\\xf8\\\"\\x8b<\\x97\\x86\\xc1<!\\x00\\x9a\\xba\\xe0\\xc0\\x8c\\xbc\\xff\\xff\\xa4\\xbc\\x85\\x1c\\x8f\\xbb\\xe4\\x8d\\x97=\\x14\\x13\\x00=D4\\x14\\xbc\\xbdd\\x89\\xbbo^\\x87\\xbc\\xc3\\x89\\x8a\\xbci\\x8a\\x93\\xbc@h\\x03\\xbc\\xb5\\xa3\\xf5<\\xef\\xd91\\xbc\\xa2K\\x8c;\\xc1j:=\\x0c\\xdb\\x8e\\xbd$\\xabK\\xbd^/\\xff\\xbcA\\xa4\\x8e\\xbc;\\xef&\\xbc\\x8e\\x0b\\xb3\\xbc\\xc5\\x0cb<\\xef\\xc6\\x81<\\xbe\\x17J=\\xa9\\x8b\\xad<\\xb0\\xa2c=\\xc3^\\xaa;\\xde\\x17\\xdc;\\xff\\xa1\\xbf=mS\\xd0\\xbc,\\xf2\\xb9<=t\\x87;\\xe2\\xc1\\xa6\\xbc5\\xe7\\xc9\\xbc\"\nHSET bikes:10087  model 'Hygiea' brand 'nHill' price 3254 type 'Road bikes' material 'alloy' weight 11.1 description 'This is our entry-level road bike – but it not a basic machine With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"qk\\xc1\\xba\\xdbx\\x11=e\\t=\\xbcN\\x90\\xaf:d \\x0e<\\xdeN\\n\\xbd\\xae\\x92C\\xbd\\x1b\\xc1\\xce\\xbc\\x8d\\r\\x8e==\\x91\\xf0\\xbc2\\x1b\\xbe\\xbcXR\\x82\\xbc\\xa7\\xad\\xb4<MA\\x86\\xbb\\x95p|=\\\"\\x00z\\xbdl:!<\\xe00G\\xbd\\x89^\\xf5\\xbc\\xb4\\xf8\\xb4\\xbd\\x11\\xf9\\x9e\\xb8<\\x0eE;gf\\n=[4\\xa5\\xbcq\\xdf,=\\xa6\\xac@=\\x1a \\xcc;\\x8a\\x936\\xbc\\xf7\\xf5\\x0f\\xbd\\xce\\x1b\\xf4<m\\xca\\x04<\\x86\\xa7\\x11\\xbd\\xd78\\xdf\\xbc\\xe7\\x0cY\\xbc\\x1d\\xb9U=h\\x8eU;sW\\xd0;+\\x94\\xa4\\xbc\\tR\\xa9\\xbb\\xc3b\\xdf;\\x7f\\xa0L=\\x86\\xbc\\x8d\\xbdDE\\xbc\\xbc\\xf3(\\xa2\\xb9\\xa2\\x00\\x9f\\xbc=\\xb5\\xf8\\xbc\\xfc\\x07p=\\x02\\xfb2<C:\\x18\\xbcvpA<kN8\\xbc\\xf5\\xe69\\xbd\\xca\\x01\\x07\\xbdj\\xfc\\xdf<\\xfe\\xd4T<\\xc0\\xb9:<\\xe2\\xaf\\x8d<\\x9d.\\xb0\\xbd\\x17\\xb7s\\xbd!\\xdf\\x8a=u7x=\\x19\\xdb\\x95\\xbb\\r\\xd5\\xfa<\\x17ge=\\xe3\\xdd\\xdf<\\xa2\\x04\\xa0\\xbc\\x8dQ\\xb6\\xbcD\\xeb\\x06<\\xbd\\xc38<\\x7f\\xf4\\x9e\\xbak\\xb1B\\xbc\\xefX\\xe0\\xbc\\x89\\x0eo\\xbd\\xcd@\\xb3\\xbcU]`\\xbd\\x15\\x15\\xfe<\\x1dL\\x03\\xbdp&0\\xbd;0\\xb1\\xbd\\xaf\\x93\\xaf;\\xd4\\\"\\xff<\\xc1\\xb6\\xc1\\xbc\\xac\\\\\\xed\\xb8\\xb6 \\x8e\\xbdq\\xbe0<NB@=\\x01\\xee~<\\xa1\\x90\\xd5\\xbc\\xa1\\xdc7<$\\x12\\x03;~>\\xfd\\xbaV)\\xbe\\xbb\\xda\\xc6\\x80=\\xfal\\xdc<\\xe5_\\xb6\\xbb\\x89\\xdb\\x0b\\xbc\\xfe\\xc9\\x05\\xbd\\x1a\\xa7\\xee\\xbc\\xe9NI\\xbb\\xa1B4\\xbd\\xd1Z\\x99\\xbb\\xee{==7y\\xa8\\xba2O\\x03\\xbd\\x84ix\\xbc?\\xb8\\xe3=\\xdb\\\"\\x9d<\\x9217=\\xd6g\\xdd\\xbbp~\\xc7<\\x12T\\x95\\xbb\\xf2r\\x87=j\\x16\\x92\\xbc\\x82P\\xcf\\xbcd\\xa2\\x8d\\xbdw\\x0cM\\xbd\\xbe\\xfe\\x07\\xbd:\\xb8t<\\xe5\\xfb\\x01<\\xab\\rW=+x\\x1c=\\x1bR\\xb3;\\x8e\\\"\\xd6\\xbcPC\\x1b\\xbd\\xa77T=!\\xb1\\xa9\\xbc\\xfd\\xfeG=\\x8b\\xb9\\x84\\xbd\\xe1xP\\xbdw\\xbb\\x18\\xbd\\x1d\\xe0\\x06\\xbdWK{\\xbd\\xaa\\xc0\\x1e=\\xf2x\\xba\\xbc\\xdc\\x81\\x03=\\xdc\\xc1Q=m\\xac\\x86\\xbc\\xf4\\x02\\xac\\xbc\\xe2?\\x0f=\\xc6\\xd8\\xb1;\\xday#\\xbc\\xbb>C\\xbd\\xc3\\xb5\\xb8:P\\xdc\\x7f;\\xc9H\\x03=\\x18\\x7f\\x80<\\x80\\x1cH=\\x81(\\x1e=\\xf1\\x17\\x0f<k\\x80h\\xbd\\xeb\\xa1f<\\t\\x8c\\t=\\xdd3\\xc9\\xbc\\xa3P\\xce<\\xc50l\\xbd\\x16\\xcc\\x1a\\xba\\xd6OO\\xbdU\\x1f\\xa3:M1\\t=\\xec.t\\xbc\\x185\\x1b=\\xfc\\xed\\xf0\\xbc\\xec\\xcb\\xce;#\\x18\\xfa\\xbc;\\x16\\x9e<\\x07\\xb4g<\\xd8\\x96\\xde\\xbc\\xdb\\x98$=\\xe17z=\\xa4|\\x1f;\\r\\x8f\\x9a<\\xbe\\xe8\\x07=\\xa2\\x9c\\xcf\\xbb\\xa1Fp\\xbd\\xd9\\xd1\\xce\\xbb\\x8b+\\\\:YA\\xaf\\xbd\\xc8\\xf3p=\\xeb\\xbf\\x14<a|\\xac;\\x1f\\xc8\\n=!\\xf2\\xc5<\\x8a2\\x12\\xbdB\\xe5`=\\xc6\\xeaO<\\x7f\\x8b\\x0f\\xbd\\x96\\xfa\\x12=\\xcc\\xeb\\xea\\xbb\\x91L\\x1f=`g\\x85\\xbc\\xbe\\xb5\\x07\\xbd&8c<\\xcd@8\\xbd\\xd0\\xb5\\xb6<\\x94\\xf6\\xe0<\\t\\xa0B=\\xfd\\xd09\\xbd\\xca\\xc7\\xcf\\xbcE\\xe6\\xb9<\\x02\\xc4\\x9d=\\x87y\\xc19\\xca7\\x8a\\xbd7R\\xe5\\xbd\\xa4n\\xea\\xbc\\xa1a\\xeb\\xbbu\\x9c*=\\x1c\\x01\\xc2\\xbb\\xee\\xb3\\x89\\xbc\\\"%\\x82=F0\\xc7\\xbc\\x1c*38`\\x16\\xfc\\xbc\\x98\\xc3\\r=\\x06g\\x1c\\xbdf{g=Q\\xfd?\\xbd\\xe3\\xfc\\x1e<\\xc7Y\\xbb<H\\xd6\\x19=\\x83\\xa0?\\xbd\\xc4\\x02D<\\xb2%};|\\xdf\\x8f\\xbcW\\x90\\x9b<\\xa7\\\\\\x07\\xbd\\xa0x\\x95\\xbdh\\x99\\x1f\\xbc\\x14E\\xe3\\xbcm\\xa5\\x9d; \\x0b\\x9a\\xbd\\xc9SE\\xbd\\xfd\\x8f\\x1c\\xbd@+}\\xbc\\x1ev-=\\xab\\x13\\xc7<\\x14\\xf3\\xb1\\xbcS\\t\\x89< \\xba5=\\xde\\x8a\\x12\\xbc?\\x85\\xb7\\xbc3.\\xf4<\\x9a\\x18^<+\\xaf\\x14=\\xf4\\x7f\\xac\\xbbU\\xd6\\x18=\\x84\\xe0\\x1d<\\xe4\\xc3r=\\xd39\\x9b<\\xac\\x08\\xe3\\xbc\\xb0\\xc8;\\xbc#\\xa4W=]\\xea\\xc4\\xbd\\x14\\xd4\\\"=\\xa7-\\x83=\\xa8\\xa9@\\xbd\\xb9\\xda\\xc1=\\xf1\\x89\\x8b\\xbd\\x92\\xf6]\\xbc\\xaeI\\x9c<^l\\xad=|\\xab\\xc9<\\x1b\\xf4\\xcf\\xbc\\xa5\\x0f)\\xbd\\x97\\x13\\\"\\xbctRt\\xbb4\\xf4\\xfa=\\x0c\\xcf0\\xbc,r\\xf7\\xbbT\\n\\x19=\\xe5\\xe3\\x11\\xbd\\xbf:\\x8e\\xbb\\x88\\xd9\\xec\\xbb\\xfdP:\\xbd\\xd2\\xd3\\xb4<\\xfd\\x83\\x0c\\xba\\xda\\x89\\xa7\\xbb\\xa3e\\xa9<\\xb2\\xa8O;\\xb2\\x08\\x8a<\\xecUa\\xbd\\xdb\\xa1\\xba\\xba^\\xc4\\x80<]\\xfa\\x88\\xbc\\xef\\xf3\\xe0<\\x12\\x9e\\xfe<\\x1a\\xb1.\\xbc\\xf7\\xadA\\xbdx3\\n=\\x0b\\xc0\\x80\\xbd\\x826\\x84\\xba\\xd9{\\x07=sTP\\xbd\\xb3\\x8d\\xc1=i\\xe5\\xe4;\\x90_\\xde\\xbcYB\\x10\\xbdI{\\x97<\\xf5\\x94\\x87<]\\xe1\\xb9<fI\\x17\\xbd\\x12\\x1cN=LG\\x04<R\\xb7\\xd3=\\x8d\\xb5\\x83\\xbcr)\\x99\\xbb\\x85y\\xff<\\xda\\xdd\\x11\\xbd\\x8cZ\\t=\\xe0gP=\\xf5\\xfdO<\\x9c\\xa6\\xeb\\xbc\\xa3I\\x07\\xbc\\xe1\\xab\\x06\\xbd\\xbbG<\\xbc\\x82\\xf2\\xe2<\\xe6\\xb3\\xd3\\xbc\\x9e\\x1f\\x00\\xbd)\\t\\x9d= \\xf6\\xc0<~\\x8f\\x91\\xb8\\x93.C\\xbd\\x9aT8\\xbd\\x962\\x87;U\\x14\\xa7\\xbcgy\\x96<\\x01HU\\xbd\\x80S\\xc5\\xbc\\xf1\\xd3\\x96\\xbd\\xf4\\nm=\\xcbwo\\xbcy\\x98\\xe9\\xbcp(\\xdf\\xbd({\\x10\\xbd5g\\xd5\\xbc\\xf3q\\xca<$\\xb8\\x1a\\xbdC\\x96%\\xbd@\\xe0\\xdb<\\xa4t\\xd3\\xbb\\xf7\\x07\\x93;l\\xde\\xf1\\xbc\\x10\\x0e\\x93\\xbc&\\x7fx=\\x18\\x13&<\\xf7\\x81\\x8a\\xbcv\\x96U=p\\xaf\\xec<X\\n\\xe5<\\xbc\\xd9\\xb3<_q-\\xbd\\xaa\\x14n\\xbc\\xa6\\x97\\xb3\\xbb\\xe5\\x0eP=\\xb0\\x9e\\x9f\\xbc#\\xf1\\xa2\\xbd\\xb5]\\x85\\xbd\\xc3~\\x80\\xbcM\\xc4\\xf8\\xbci\\xc1h\\xbd\\x954\\x07\\xbe\\x06\\xe7\\x8e\\xbc\\xe7\\xda;=\\xb7\\xc56\\xbc\\xcc\\x85q=\\xfe\\xc2\\xd1<\\x0c}\\xe4<\\xe0\\xa3\\xf5\\xbc(EA=WOK\\xbd9C*\\xbd|\\xb2\\x0b=\\xa7\\xd0\\x89<\\x8a\\xdc)\\xbc.\\x15\\x10\\xbdZ\\x90$<\\x14\\xd1D=3)\\r\\xbc\\xaf\\xfc+\\xbd\\x05U\\x05\\xbc\\x8d+\\xf9<\\xe3T#\\xbd@\\xcd@\\xbd\\xe9\\xde\\xc3:\\xc5s\\xbb\\xbb\\x98\\x9f\\x03\\xbdckn\\xbc[\\xab\\xfe\\xbcJ\\x16a\\xbc|\\x05\\xec<\\xab\\xc7\\xf8\\xbc\\xed?\\n\\xbd\\xdc\\x98\\xb6<\\xd6\\x0b@=\\xa2\\\\\\x90\\xbc\\xd82\\x11<q\\x9b\\xa3\\xb9_-\\x91\\xbd[\\x11\\xca\\xba86?=1:\\xb1\\xbds\\xa2[\\xbb\\xdc\\x84$=\\xff\\x80(=\\xf0\\x96m<\\x8d\\xf5\\xb4\\xbbs\\xaa\\x82\\xbc\\xe9/9=\\xb3\\r\\x8b<gW\\xfd\\xbc\\x0fr\\xfd<\\xf6\\xda}=\\xcb\\x86\\x93<\\xb5\\xa6\\xaa<\\x93_\\xba\\xbc&\\xc9e<5\\x1c\\xd5<x\\x88U\\xbc\\xcb\\xac\\x89\\xbdt\\xaf\\\\\\xbcy\\xbc\\xec<:\\xe4\\x80<Y\\x13\\x1e\\xbdJ\\x00\\xcd<\\xfe\\xb4\\xfd<\\x81\\x1e&\\xbd\\xb5\\x81\\xf6\\xbc\\x0c\\xe8\\x9d;\\x96e$=})\\xc7\\xbaY\\\\?\\xbd*^\\xa3;\\xbb]@\\xbc\\x9f\\xa3n:\\xc5F\\xbc\\xbc\\\"\\xf9\\x9e\\xbd\\x93\\xe5x=\\x17L|=we\\t=kc\\xab<dx5=\\xa9\\x89D;\\xa3\\x01\\xc7;\\x01\\xa2\\xa8\\xbc\\xbb\\xdd\\xd2\\xbc\\x03\\x98\\xf5\\xbcn\\xf0\\xee:\\xba!\\x86\\xbc\\xa3/\\xfc:\\xc3\\xcf\\xc4;\\xe1=\\t=\\xd5\\xa7w\\xbdR\\xf5P\\xbd\\\"\\xe6\\x9b<\\xa06\\xd4\\xbd\\xd9\\x9d\\xb89.=\\xd3\\xbc\\xc1NP\\xbc\\xb1\\x157;\\xb8+\\x98\\xbc\\x9cVa=\\xd5\\xb1\\x9c=\\xac\\xe9\\x91\\xbb\\xcf\\\\\\\"=\\xad\\x1c\\x17\\xbb\\xf5\\xfcP<\\x17\\xd1>=\\xd1J\\\\:=\\xbf\\xdf\\xbc\\xab\\xe2(:\\x05\\xb3,=c\\x152\\xbc\\xca\\xd1\\x97\\xbc\\xf8\\xe7-\\xbd\\x91\\x85\\x0b\\xbd\\xdbi\\xc5:\\xf6\\xa4]<\\xe3Z\\xa7\\xbb\\x8do\\xa7\\xbc\\x8b\\x90\\xdb<\\xd1-^\\xbcH\\x92G<|\\x93\\x06=\\x96g3\\xbc\\x04\\xb5\\x9b<\\xc4\\xd0\\xe9\\xbc\\x05\\x1a\\x9c\\xbd\\xdaB\\x14\\xbcQ\\xcdN\\xbcm\\x11\\x95<\\xefy?<J\\x0bK\\xbcB\\xb6\\xbd\\xbc\\x0f]\\x8f=\\x99\\xdf`<8\\xdey\\t\\xae\\xe3[\\xbc\\xab\\r\\x86\\xbc7\\xa3I\\xbc,o\\xc7<~\\xd1u<\\x9b\\xe9\\xf8<n\\x88F\\xbd\\x92\\xa1=\\xbd\\xd8`\\xa3\\xbcTr9\\xbc+W\\xd9<e\\xa5\\x95<\\xaf\\xac\\xda<-E\\xe6<\\xec\\xf1\\xe6;,x\\x1a=D\\xea\\xc9\\xbc\\x82\\xbd\\x0f\\xbc\\xd0\\x06\\xbd\\xbc\\xd4\\x7f\\xfc;\\xaa\\xbay<\\xea{\\n\\xbc\\xff\\xe6\\xc9:\\x97\\xe5d\\xbb\\xfd\\xb4\\xcd<l\\xf8\\xf9\\xb9\\xc2I\\xfb\\xba\\t\\x1d\\x97;F\\xe5\\x9d\\xbb\\x9d%\\x83<\\r\\xfd\\xfd<\\n\\x82\\\\\\xbc\\x06\\xe8\\xa8\\xbcWi\\x89\\xbd\\x88\\x94\\xde\\xbcHCp<^\\x80\\xfa<t\\n\\x9e<M\\x89\\xfd<v\\x11$=k\\x06\\xb0:\\xe6\\x10\\xa7<\\xdb\\x18\\xe2\\xbb\\xedF\\x06=I~*=\\nm(\\xbd5$z=\\x8fB\\x8a\\xbc\\x9b\\x9d\\xe9\\xbc]\\x93f\\xbcA\\x8f\\\"\\xbcG\\x1c(\\xbdR\\xfb\\x96<\\x11\\xa7\\xa3<\\x8b,\\xa7\\xba\\x82#z=\\xca\\xa5\\x19<I\\xd0\\x81<I7\\x01=~\\xe2\\xb4<4@\\xed;_\\xa7<\\xbc\\x14i\\xad<H\\x0eC=\\xae/\\x80\\xbd5MF\\xbc&2\\x1b\\xbd\\x9fQ%<U*v=\\xcf]\\xce\\xbb\\\"@\\xd3<<h\\x00\\xbd/\\xa9\\t:\\xaf\\xc2\\n=\\x97[J<F\\xc2Q\\xbcG\\xc0/\\xbc\\x99\\xf2\\x95=\\\"\\xb1\\x05\\xbd\\x9b\\xa2;=\\xc8Ir<&:\\x94<#?\\xd7\\xbb\\x9c\\x86\\x93<\\x88*\\xed\\xbc#\\x17Z;c\\x16\\x86=\\xb7\\xf4\\x9b=\\x8a\\xdb><\\xe2Z\\xeb<s\\xc2V<\\x06\\xe0\\xcc\\xbbQ\\xd9\\x0b=\\xedz\\x1b;\\xc2\\x12\\\"=\\t\\xdd\\x7f<\\xef\\x9e\\xb9\\xbc\\x17\\xf2\\x92\\xbb\\xf4_\\xb5\\xbb\\x83\\xce\\x14=32\\x9e\\xbc\\xfayC\\xbd\\xd4\\x1f_<\\xcb@W\\xbdr9\\x0c={\\x1e\\xaa\\xbc\\t\\x19S=kK:\\xbbrby\\xbde\\xb1\\x8d<?\\xa4\\x04;&e \\xbdw\\xf7\\xb1\\xba\\xc3\\x0ci=/\\x1a\\x03\\xbb\\xe0\\xa3\\xc8:\\xe3\\xf4`=\\xad#\\xd7<\\xae\\x92\\xb3;\\x85\\xa4Z\\xbd@\\t\\x17=x\\xfe\\x0c\\xbb\\xb6\\xe5\\xa2;\\xfb\\xda\\xbb\\xbb\\xfe\\xf1\\xf1<hv\\xef;\\x92\\x1c\\x89\\xbc\\xfa3\\xbf<A\\xcf\\xf0<a\\x81\\x93\\xbd\\xa1\\xcc\\x9b\\xbc\\xf0tt\\xbd\\xd5\\xa3t=\\xadT\\xd7;s\\xfd\\xa3=\\xc7\\\"m<d,\\x99;\\xecA\\x12\\xbdN\\xd2\\x7f\\xbc\\x8a\\xa1\\x08\\xbd\\xb7\\x1c8\\xbc\\xcc\\xbe#\\xbcg9[\\xbc\\x8e\\xf38\\xbco4\\xa9\\xbb\\x83N\\x94;$\\xe40\\xbd\\x15\\xcdd\\xbd\\x9cv\\xe3<\\xedOO\\xbd\\x83\\xb1K;;?\\xeb\\xb8\\xe2\\xad\\xeb<F\\xc7\\x13=\\x9e\\xb5\\x1f=\\x14\\x80\\x90\\xbb\\xefdI\\xbd;\\xff\\x8f=\\x93?:=\\xcdA6<3\\xd6h\\xbcr\\x86x=\\xc7Q~\\xbb\\\"`\\xbf<\\xba\\xe5f\\xba\\xc2\\xdb\\xf9\\xbb\\xd5\\xddF\\xbd\\x12\\xf1\\x9e<\\x93\\xb6\\x19=UW?\\xbch\\xa7I\\xbc\\r\\xd1\\xf6\\xbc\\x0b\\xcf\\xfc<\\xc1\\xd2e\\xbc\\x0b\\xcd)\\xb9wG\\x96<p\\xf0\\xef<&\\x00\\xce<\\x18\\xec\\x9f\\xb9UN\\\"\\xbd\\xc6-\\x12\\xbd\\xaa\\x8f\\x98\\xbd\\xc8\\xe98=2\\x15\\x0c\\xbcC\\xf0B<\\xc6\\x95\\xea\\xbb\\x18\\xd5\\x19\\xbc6\\x1e\\xcb\\xbbG\\xd9\\xf1<\\xe9\\xbf\\xcb\\xbb%\\xd2Y<\\x91\\xf1+=}oF\\xbc]\\xa4\\xab\\xbb\\xf6\\xc3d\\xbd\\x96\\x0e2\\xbd\\xd4\\xd1\\x14\\xbdv\\xb0g;\\xb2\\xbd\\xbc\\xbc\\x05o%\\xbc\\xda\\xf96\\xbd\\x02\\xa3\\x96<)\\x82\\xfa\\xbbl\\xcc\\x82\\xbd \\xe9\\xdd\\xbc\\x1b.$\\xbd+\\xdf\\x1a=\\xf6\\x8a\\xcb\\xbcq0\\xff\\xbb,\\xc8$\\xbd(#\\x14=9\\xaf\\xb2;\\xa1\\xfes;h\\xb5p<dMF\\xbd\\xd5!2\\xbc\\xe6\\x0f\\xc4<\\x85\\x89\\xeb\\xbc\\xf6&\\xc5\\xbb\\x1f\\xec)\\xbdvKt<\\x82\\xcc\\xc4\\xbd\\xbb\\x14==\\xd1uN:q\\\"p=\\x0c\\xebF\\xbc\\xf9q\\\"\\xba\\xff\\xb2\\x0c\\xbd\\xd2\\xfc\\x08\\xbd\\\"v\\x08=\\x949\\xe4;\\x7f\\xd1\\xa8<\\x1d\\xfeN\\xbc\\xa8\\xe3>\\xbc\\xdeU\\xe4\\xbb[?\\x89<\\x96:\\xde<w\\x12*\\xbd\\x8ex\\xb2=iy\\xa5=\\xdf;i=u\\x13\\x96<\\x9f\\x07\\x87=\\xd2`h\\xbc\\x15\\x04\\xd2\\xbc\\xdc\\xc9u<6J<\\xbd\\xd9i\\xe5;J\\xff\\x88\\xbdZe\\x9d<\\xce\\xd8\\x7f<\\x04A\\x9a\\xbc\\xbb\\x9f\\x90;\\x9e\\x11\\x91=\\xf7\\xf3P\\xbd\\xaf\\xe4\\xae<#\\\"\\\"\\xbd:\\xc5\\xcb<\\x94\\x94\\x91\\xbc\\xedB\\xc6\\xbdg\\x9b\\xe2\\xbc\\xf0\\xa8&<\\xc1\\xee)=F.A=\\xe5\\x94\\xa4=\\xf7e\\xe4\\xb9\\x0bw\\xfb<($h<\\x1f\\xd3\\x91\\xbd\\xfc\\x1f4<\\x89\\x08\\x14\\xbc\\xbc\\xcd\\xdb:\\x82\\xe5\\xfd\\xbc\"\nHSET bikes:10088  model 'Ncc1702' brand 'Nord' price 3599 type 'Kids bikes' material 'full-carbon' weight 15.9 description 'For shy or agressive riders, paved or dirt trails, this bike boasts kid-friendly geometry and strong quality parts at a minimal price point. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\x95\\xac\\x13<\\x18,\\x9f\\xbc\\x93\\x98\\xe1\\xbc}\\x9d\\x00<\\x96\\xf8\\x7f\\xbd\\x0cm\\xdb;\\xd0U\\x12\\xbd\\x04QV\\xbdX\\xf2\\xab=`|\\xcf<\\x124\\xe0:<\\xfb-\\xbc\\x8c`,=\\x1f\\xcd%=+S0=\\x14\\x8a\\xb6\\xbct\\x9eT=\\xf5\\x94]\\xbd\\xe6o\\x1f\\xbdf2g\\xbd\\x96\\x13\\xab<\\xc4\\xdf[\\xbc\\xeb\\x07\\xc8<\\xbd\\xac\\x93\\xbc\\xaeh4\\xbd\\x0e!\\x07\\xbc\\x0c\\xa5\\xd7<\\xd0\\x1c2\\xbc\\x08\\\"\\xbe\\xbc\\xc1Cr=\\xbbY\\xa8\\xbck\\xb0\\x15\\xbd\\x18h(=\\xd1\\xf5\\x8a=\\xdd\\xd23\\xbd\\xae\\xb0h\\xbc)r(=\\xb0C\\xa9\\xbb4\\xd6\\x8d\\xbc\\xc4R\\xa3;\\xd1\\x13-<[7\\xd9:\\x07\\xf8\\xc0:\\x1e\\\"X<\\xec\\xfb[;\\xc5\\xa53\\xbdB\\tL=\\xa5\\x0e\\x98;J\\x87\\xec\\xbcC!\\x93;\\xa5)\\xad\\xbcv\\x85\\xd4\\xbcp\\xc4\\xe3\\xbc\\xfe\\xa5\\x01\\xbcp\\x02{\\xbc\\x05\\xcb\\xd9\\xba\\x04\\xd7B;\\x19\\x1c\\xfa\\xbc\\xcf\\x1f\\xa1\\xbc\\xf0\\x18\\xd3=\\xcf\\x9f\\x87\\xbc\\x82\\xd8P=\\x7f \\xa4\\xbb\\xe8\\x17A=\\xb5\\x89\\xf2<\\xb0\\xa4\\xba\\xbc\\x0e/\\x9a<\\xcb\\x8c\\xad;z\\xcc\\xde<\\xed,\\x1b\\xbd\\x81\\x1c\\xbd\\xbbS\\xac)\\xbc\\xbd\\x8c\\x11\\xbc\\x1d\\x13r\\xbd\\rv7:\\xcf\\x06\\n\\xbc\\xc6\\x08\\xef9\\x9d\\xa6:\\xbd\\\\\\xf2v\\xbdY\\x84X=\\x9eN\\x11=\\x00E\\xce\\xbc\\xd9\\x85\\xce\\xbc\\xaa\\x11\\xf4\\xbc\\xc7\\xa0\\x13<\\x8d\\xa9\\x86\\xbc\\x05\\xc7[\\xbc\\xc8\\x8c\\x04=\\xc2!\\xb1\\xb9\\xe3\\x9dS<\\xe8\\xb3w<\\x07\\x1c^\\xbb\\xed\\xf0G\\xbbp\\x93v\\xbc\\t\\xef\\x19=St\\xd5<\\x04\\xad^\\xbd\\xc3_\\xcc\\xbc\\xf26\\x1a=\\x19\\xeeO\\xbd\\xf3\\xa3\\x84\\xbd\\xc2\\xb9\\xf5;\\xbaX\\xcb\\xbb\\xde\\x88\\x01\\xbc\\xcb\\xc0\\xdf<Ot\\x00>s\\x92F\\xbci\\x9c\\xd1<\\x9cb\\xa3\\xbcpv-\\xbd[\\xed\\\";\\x16\\xc6\\x11=\\x00A\\x1a<yP\\xd2\\xbc\\xa2\\xe5\\x8f\\xbdi%\\xad\\xbc\\xa9\\xa5\\xd0:Z<-\\xbb\\x81Y\\x11=\\x1f\\xe8\\x1c<\\xbd3\\x17=&\\xa1\\x0b<\\x11\\x97k\\xbd\\xed\\x88\\xf7<\\xb1\\xc3$=aA\\x9d\\xbc\\x97\\xd7\\x92\\xbc2\\x98\\x06\\xbd\\xdd\\x14\\xda\\xbc\\xa8\\xc9\\xc6\\xbc9.\\xd3\\xbc\\x92x;\\xbd\\x02\\xc2\\x0f=\\xd5t\\xf0\\xbc6\\x92\\x9c\\xbb\\x89\\x9a8=\\x17\\n\\xdc\\xbc\\x7f\\x13\\x8a\\xbc\\xa9H\\xdd<\\xfb\\xd1\\xb3\\xbc\\\"]m\\xbd\\x1a9\\xa2\\xbda~\\x02:\\xe8&e=\\x8fv\\x8a\\xbc\\x1a_\\xa9<d\\xe2\\xa5\\xbc\\x9f\\xe7\\xd7<)\\x85\\xfc;p\\x87\\x96\\xbcm\\x94\\x91\\xbbN\\xa1\\n=\\xbb\\xc7\\x0b\\xbc\\x9f\\xdb\\x16<\\xa4L\\xbe\\xbc]\\\"\\x90\\xbcil\\xb2:?\\xcb.\\xbc\\xa1$T=\\x9f\\x01z\\xbd(~{=\\xf4\\x9c\\xdc\\xbcn\\x8c\\x1c\\xbbw\\xcck\\xbd:\\xe8\\xcb<\\xa7\\xa1\\x89<\\x00\\xd3\\xcc\\xbc\\xfb\\x97\\xbb<\\xd4$\\xf1<g\\x9d\\xf3\\xbcF\\x18O\\xbc\\x01\\xc0\\x88=R\\xcf\\xae\\xbc\\xb0\\xe4A\\xbdj*\\x1c\\xbc\\x83\\x0fE<\\x06\\x02\\x15\\xbdB40=\\x9f\\xcf};\\x02!!\\xbc4\\xaa\\x14\\xbd\\x1dP\\x8e\\xbb}\\x8f\\x15<G\\x828=\\xf5\\xdc\\xaf<n\\x1d\\x14\\xbd H\\xde;\\xa1\\x9f\\x0c<\\xdbD =\\xd00V\\xbc$\\xc6\\xa0<\\x95\\n.=N\\xc8\\xa2\\xbd\\xd1\\x1c\\x8d=\\xe45*<:\\x1fW=~\\xd8\\xfc\\xbb\\x18\\x95\\x1a\\xbb`\\r\\xa9;\\xa7\\xdd\\xaf<\\\"\\\\\\xa1\\xbb\\x046\\x84\\xbd\\x19}\\xb2\\xbd\\xe1\\xa5\\xd0\\xbcc\\xb3\\x91;-(7=C*\\x11\\xbd,\\xa6\\xb1\\xbcH\\xa1-=V\\xf8\\x88\\xbc\\xfa\\xb8\\x97\\xbc\\xe8\\x1eV=a\\x81g=\\x16\\xc7\\x9d;N\\xbe\\x8e<\\xc4\\x07C<\\xc39\\xaa\\xbd\\x0f\\xab\\xc7\\xbb\\x85\\xcf\\xce\\xb9c\\xc0v\\xbd\\xbb$\\xad\\xbb\\xf2sB\\xbd;\\xde\\x13\\xbdZ\\xad5=\\x00\\xce\\x89\\xbc\\xc3+\\x82<H\\xba\\x93=\\xcf\\x1e\\x96:\\xa2\\xcc_=<`.\\xbc\\x8b\\xa5\\x0e\\xbdE\\xd3I<\\xe3JL=\\xb9+\\xaa<\\xd8\\n\\xd1;\\x18\\x0e:\\xbdb4\\xaa\\xbc$pO=\\xe5\\xc3t\\xbb\\x9c\\xaa\\x97\\xbbf\\xba\\xaa<B\\x04:<\\xdd\\xff\\\"=O\\x80\\n=G\\xa2\\t=\\xb9V(=\\xebm\\xa7<\\xf4\\xad\\xbc<u\\xf7\\xf4\\xbc%Q\\x9f;\\xb5h\\xbe;\\xe4\\xe7C\\xbd\\\"\\xa5\\xf1\\xba\\xcb\\x91\\x19=\\x01\\x17\\xcf\\xbc\\x86\\x7f\\xa7=\\x10} \\xbd\\xf6\\xaa\\xd6<\\xde\\x95S\\xbc\\x03HC=\\xa0\\xc9\\x00\\xbdJa\\xbd\\xbcA+\\x1d\\xbdt\\xdfN\\xbc\\xe3JD\\xbd;p\\xff<j\\xc6G\\xbcV\\xc2\\x8f\\xbc\\xe2\\xdb\\xc8<\\x1a\\x05\\x12\\xbc\\xb7\\xf9\\t\\xbc\\xd3\\xdc\\x1f\\xbd\\x13@5\\xbd\\n\\xd5J\\xbcO\\x10\\x14\\xbd?;\\x8d\\xbc\\xa02\\x11=\\x9d\\xf4\\x91\\xbc\\xd8\\xa9\\xcd=\\x89a\\x84\\xbd}\\x19(<\\xf8\\x04 \\xbc\\xf8bL=V\\xef\\xc0\\xbb$\\x9d\\xbd<\\n\\xf9\\x18\\xbb\\x01\\xb5\\r\\xbd\\xfd\\x1f^=\\x99\\xebY\\xbd`\\xfa\\xe9;5XE;\\x14y\\x03\\xbc\\\\\\\"[=5(\\xc0\\xbc.\\xbc\\x8f<\\xc0\\xd5\\xad\\xbc\\xec\\xc8%<\\xe7\\x02}<\\xe2\\xc9{;\\xa0\\xb1\\xaa\\xbc\\xf9G-=\\xc2+O9!A\\xc7<;\\xf5@;X\\xa5$=\\x96\\x9e\\x17\\xb9\\xf5vo\\xbcGs\\x00\\xbbz\\xc5\\xd3<;Y\\xae<\\xd2\\xacO\\xbdK0\\x0e\\xbd\\x83g\\x1f\\xbc\\x18\\x1b\\xde\\xbc\\xd5\\x96!<\\x9b\\xa2\\xc1\\xbd\\x08y\\xf4\\xbc7\\xe7\\x9b=\\x98\\x82\\x99<a\\xb1\\x07\\xbd\\xabr\\x1a\\xbd\\xcdc\\r\\xbdo\\xa1\\x18=\\x11\\xb6\\x94\\xb9.)5=\\xcf\\x06\\x18\\xbd\\x04RK=\\xaf\\x10/\\xbd\\xd0\\x14\\x88=7\\x1a\\x81;4fx<v\\x82&\\xbd\\xb5\\x8b\\x86\\xba\\xacE \\xbd\\x84;)=r\\\"\\xaf\\xbc\\x18\\x01\\xa3\\xbc\\xce\\xbb\\x8b;\\xf9\\x1b.=\\xba\\x18_<\\x808K\\xbd@$\\x96<OJ\\xc6=\\x9e8\\x1f\\xbb&\\x84\\xb3\\xbb\\xcdA5=2_\\x18=\\xbe\\xcfq<k\\x18S=\\x02\\x91)<y\\x13\\xf1<\\x92M\\x1d\\xbc:m\\xb1=V\\xe6_\\xbc\\x00\\xbcW\\xbd\\x87?\\x10<\\xcc\\xf9\\xb5\\xbct`Y\\xbd\\xa8z2\\xbc\\x07\\xa0\\xdb\\xbd\\x07M\\x96<{\\xb3\\xee<\\x85\\xc7D=\\t\\x0b2=\\r\\xce\\x03<\\xa2\\xf5\\x02\\xbd\\xdf\\xd0\\xa7\\xbc\\\\p\\xf9<+V\\x8e\\xbdOn\\xb8\\xbc>\\x9a\\x1e=<\\xeb\\x10<l\\x04a\\xba\\x04\\xcec<\\xa5\\x88\\xb3<:\\xa9\\xd4<-H\\xbf\\xbc\\x83\\n\\xd6\\xbc\\xb8\\\"\\xd5<\\xe0g0\\xbcY>\\xe9\\xbc*WK\\xbd]G\\xa4\\xbb\\n\\xa3\\x00:\\x87\\xb1g\\xbd.\\x81<=7:\\x83\\xbc\\xd9\\xe9D=n0n<\\xde\\x8f\\x1e\\xbcY\\x9d\\xbe\\xbcw\\xa9\\x82=\\xf8\\xae\\x01>)\\xdb3\\xbdmf\\x96<\\x9f\\xd4U\\xbcB;B\\xbd\\x00P\\x99:\\xb2-\\xd1=f!\\xa8\\xbd1\\x1a\\xa9\\xbc\\xa2:\\x1d\\xbd\\xc8\\xae\\x81=\\xd1\\x05\\xc1<\\xc2\\x05M=\\xd8\\xbd \\xbd\\xf3\\x8f\\xf6\\xbb\\\"\\\"E\\xbbF$\\xf2\\xbcb\\xa01;\\x9c-5=\\xbd\\xdf\\xa5<4\\x0e\\x18=\\xa4I\\xe4;0v%<!E\\x17;\\xb2Eu<\\xe4\\xdfx\\xbd\\xd7&\\xea<\\xb5`5\\xbc\\x0c\\xa3\\xf5\\xbc\\xad\\x1b\\xdd\\xbc\\xe9\\x834=]Uh=\\x82\\x13\\x9c<\\xc8\\xb2Q\\xbd\\xc6(G\\xbd$\\xa5\\r=\\x1f\\xa1\\x00<\\x1f\\x0c*\\xbd\\xac\\xe4f<\\xbe\\x98m\\xba\\xa1\\x12~\\xbd\\x81\\xa3\\xa4\\xbdC\\xd9\\x12\\xbd\\xe5\\xb1\\xc7=\\x97*G=\\xd3\\x9c\\xee:\\xc1\\xfdO\\xbd\\x97\\xdf\\x0f<&m-\\xbd\\xb4\\x05\\xfd\\xbc\\xc6Q6<\\xe0\\xe4\\x8e\\xbc@\\xcf!<!\\xe82<!>\\xf3<p\\x7f\\xf9\\xbb\\x88\\xa7\\x98\\xbd\\xf7\\xed.=.\\xb5>\\xbd\\xa9G`\\xbd}\\xf4\\xbc\\xbc\\r\\xfc\\xc3\\xbd\\xf8\\xd8\\x12=u\\xc0\\xb2\\xbb\\x15\\x06\\xc2\\xba\\xda\\xfc0;\\xf1?f;\\xfd\\x94\\xbe<N\\xff+=\\xc7\\x80.=\\xaf2\\xeb;\\xba\\xb8\\x8f\\xbd\\xdc\\xa7$\\xbc\\x9a\\x05\\xe9<\\x01\\x9ce;4\\x004\\xbd{\\xe5\\xdc\\xbb\\x00\\x91\\x8e=\\x83\\xe0\\\"\\xbd{\\xdc\\xd5<\\x1el\\xdc\\xbc\\x1fs\\xa8\\xbc\\xc2\\xe1\\xd4<=U\\\\\\xba\\\"\\tJ\\xbd\\xf7\\x1b\\xeb\\xbc\\xb1y\\x97<\\xb4\\xad\\x97\\xbc\\x8d8\\xdb;6\\xb6=\\xbbA\\x9ew;\\xd7\\x08\\xc1;xl!\\xbc\\x0b2\\x8b\\xbdN>:\\xbc\\x8f\\xac\\xfa<\\x9a\\xa1|\\xbc\\x07s\\xe9<\\x1c\\xf5\\xcc\\xbd\\x182\\x12=/\\xddr<*L\\x8c\\xbc\\x1b\\xb3q\\t\\xe5\\xab\\xd7\\xbcZ\\xb9\\t\\xbd\\t\\x072=\\xf82\\xb3=\\x98\\xec4=~b!=\\xa1;\\\"<\\x03\\xe7\\xac\\xbc\\x0cV\\x92;(\\x01\\x01\\xbd\\x9ez\\xbd:\\xb4o\\x82<\\xfanK\\xbb\\x1f\\x01\\xba;\\x05\\xd6\\x17\\xbd\\xce2\\xa0\\xbb8\\x92e<\\xe0(\\x1c\\xbd\\xe2M\\x03<\\xd5\\xc1\\x8b=u\\xe4N<\\xe6g\\x11=\\x88\\x07\\xfb<He\\xe6\\xbcl\\x1d\\xac<\\xae\\x8b8<\\xc7\\xd1\\x0f=w\\t\\x99<a\\xc6\\x0b\\xbda\\x1f\\xb9\\xbc3\\xa4\\n=\\xad%\\x07\\xbc\\x06\\xa9\\\"\\xbdEmP\\xbdx\\xd7\\x1b\\xbc\\x97\\xc9c\\xbd\\x85\\x82L;x\\xc8\\\"=\\xb1{\\x12<\\x81\\xf7]\\xbc\\x01\\xcf\\x18\\xbc\\xa6\\x1eE\\xbc2\\xf1\\x98<\\xee\\xf4\\xc1<\\xc2\\xe4\\xf5<\\xdd\\xcd\\x15\\xbd\\xfd\\x8f\\xc4=\\xb7=\\x0f\\xbdKW\\x10\\xbd\\x1e\\x15@<P\\x98 \\xbd\\\"]\\xbe\\xbc\\xfcnB\\xbcLMw<a5\\x86\\xbdY\\x8b\\xdc<P\\xd5V\\xbc\\xefGz\\xbc\\xd5\\xee\\xa8<_\\xcd\\x06=\\x0b\\xe6\\xd5\\xbbI@e\\xbcf\\xe2T\\xbb\\xd3\\x03J=\\xdd\\xc1Y\\xbc\\xee\\xa3\\x14\\xbd\\xbb\\x9e\\x83<\\x16Q\\xc2\\xbcXt\\xd3;)\\xab\\xe5;i\\x10\\xdd<Q\\xb1\\\"\\xbc<\\x7f\\x969\\x12\\xc8\\xa2<6\\x98\\x05=S\\\\1\\xbd7\\xdf\\x83\\xbc\\x1d\\xd6A=4\\xeb\\x9e<\\xef7\\xf6<\\x0f\\xa8\\x92\\xbd\\xa9\\\\r\\xbc\\xb0nQ\\xbc\\x9dt\\x12=\\xdd\\xd3\\xfd\\xbc{\\xffk\\xbbH\\xdf\\x80=\\x93I\\xde<\\x13\\xa3/\\xbc\\xe6a\\x0c<*\\xc2\\x90\\xbb:\\x1dV\\xbdr\\x81a=\\xd9\\x94\\xe9:\\x13o\\xe7;\\xa7\\xd5y<\\xf0F\\xcf\\xbc\\x97+\\xd6<\\x81\\t\\x9a\\xba!\\xe8g=C\\xc5\\xa6\\xbb\\x8c\\xb0\\x1b\\xbd\\xb4 \\xdd<\\xd5\\xdb\\x1b<W\\n\\xc1; \\xec\\xa7\\xbc\\x1c\\x12\\x15<\\x82\\x93\\xdd\\xbcO\\x90\\x99\\xbc\\xb2\\x9f\\xbc\\xbc\\xc9\\xd0O=y\\xfc\\x04\\xbd\\xec\\x93\\xe3\\xbcx\\t\\x01=@(\\xb4=V\\xa5>\\xbc)\\x1e\\xb4<\\x03\\xe9\\x8f\\xbc\\xcb\\xa3\\x81<h:\\x0f\\xbc\\xa6\\x89\\xe7<\\xd1\\xc9\\x97<\\xfb\\xee\\xa0<2\\xf7W=\\xc3\\x86\\xec<\\xb2\\xa5\\x8c<q%\\x84\\xbd\\x13\\xea\\xc5;\\xc5\\xc3W=\\xb4L\\x91\\xbc\\xdd\\xff\\x04\\xbd\\x1ch\\xa5\\xbd\\\"Q<=P.\\r\\xbc\\x0e\\xef\\xc9=\\xce\\xcds\\xbc\\xdc\\x8f\\x1b=\\xbf\\xbb\\x84\\xbd\\xb9\\xbc\\x13\\xbc\\xf1e\\x00;Ps\\xf3<B\\xc3i\\xbc^\\xd5\\x00\\xbd\\xa4l1\\xbc\\x1a\\x14\\x0b\\xbb\\xd0P\\xba<\\xa9o\\xa2=[y\\x03\\xbc\\xe2\\xb9#=\\x8d\\xe2\\xc7\\xbbN\\xe2`\\xbdGS\\xf4<B\\x99\\xa0\\xbc\\xdcO\\x05=\\x9eB_<\\xfb\\xbaP<\\xcb\\rR\\xbde\\x9f\\xda<-\\xd9\\x14\\xbc\\x01\\xee\\xad;}\\xaa\\xc1;\\x93\\xe0\\xbb=\\x12\\x91\\xd8;2u\\xf6\\xbbG\\xad\\x97\\xbc\\x07\\x83[\\xbcW\\x91\\x9b\\xbb\\xd2\\xee\\x16=m\\xcf\\x84=EH\\x87<\\xd5\\xf3\\x1d\\xbbX\\x0bY\\xbc\\xaa\\x15\\xcc<\\xc0j\\xcf;NW\\xdb<\\x13\\xb7.=Hkm;\\xbc#\\xeb<E5\\xfd<\\xee\\xee\\x02\\xbd<C\\x08\\xbd\\xe6u\\x93\\xbd\\xab\\xaa\\xc1;7\\xf8\\xb5<\\xfd\\xf4\\xc3\\xbc\\x96\\xe4\\xdc\\xbb*\\xf0\\xbe\\xbb\\xaa\\x9b\\xd8;\\x80\\x84\\x1e<\\x1bd\\x97<@\\x87b\\xbc@|\\xb1<\\xc9\\xd9z\\xbd\\xfb\\x1b\\xd4\\xbcDS\\xc0\\xbc_x\\x97\\xbd\\x9e;1\\xbd\\\"\\xd2\\x81\\xbc\\x85\\xe2\\x80\\xbc\\x7fK\\xf1\\xbc\\xab\\xc1-\\xbd\\x8f\\xa6\\x04\\xbckk0<7D\\x98\\xbdMy\\xeb<\\xe7\\xc9/\\xbd\\xe5\\xa6\\xee;\\xc7f\\x17\\xbd\\xb9\\xc0\\x89\\xbc\\x04\\x81\\xa9\\xbc\\xc7\\xe8Q<\\xe4]M\\xbc\\xea\\x1a\\xfe<\\x08\\x9e\\xd3;\\x9c\\xe8\\xd6\\xbb7\\x7f\\xf4;Tc\\x9f\\xbb\\x8bUw\\xbcj\\x9b\\x8d\\xbdT.\\xd3\\xbd\\xea\\xf6\\xaa\\xbc0\\xbf;\\xbcn\\xc9\\x96<\\xc4\\xb2E=m\\x05b=\\xb03>\\xbd\\xdeQ\\x19=\\xfc\\x190\\xbd[E\\xb2\\xbd\\xf9\\xefW=\\xe5\\x16e<<5\\x00\\xbc}\\x88\\xc1\\xbc\\xc7\\x02%\\xbc\\xc4.U<u\\x85\\xea\\xbcnCp\\xbc\\x03\\x1a\\xa4;\\xf3\\xab\\x96<l?\\x84<xq?<\\x141]=\\xcd\\xfe ;e\\xd2\\\\=\\xce%%\\xbcxL\\x89\\xbc|\\xb2.\\xbd\\x99O)=\\xa6K[\\xbd|hM<=\\x8cj<\\x99\\\"\\xc3\\xbb1\\x10\\x08\\xbc\\x04\\x0c\\x93=!\\xcb\\x1d\\xbd\\xc4\\xce5\\xbb\\xfb\\xfaL\\xbc\\x12\\xf9\\x92\\xbb}\\xe2<\\xbb\\xe2\\x9bw\\xbd\\x0fB,=X8b\\xbc\\xa5f\\xe6\\xbc\\xfa^\\x83=\\xe6\\x8a\\xce=\\xc5fv;\\x12\\xc4\\n=I\\x15w={\\xba\\xed<~;\\x8b=@F\\xce\\xbd\\r\\xdf\\xcd<:Q\\xa9\\xbc\"\nHSET bikes:10089  model 'Phobos' brand 'Peaknetic' price 1980 type 'Kids bikes' material 'alloy' weight 7.9 description 'The innovative braking system on this bike has been a game changer in the kids’ bike world. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires give balance and grip.It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"\\xad\\x1e+;\\xc6\\xd6\\x82=!\\xfb\\x92\\xbc{6\\x05=\\xef\\x1c\\x12\\xb9\\xb1\\xf4\\xc5<\\xa2!\\xe6\\xbb\\xdb\\x9e\\xb8\\xbc 8\\xb9=^\\xaf\\x1a=(\\x07#<\\x9d\\x08\\x0c\\xbd\\xe1\\xd5(;\\xac\\xa7<\\xbd\\\\-\\x9c;\\xca\\xda7\\xbd(\\xc2{<8\\xdc9<\\xda?\\xef<\\xdd\\x82\\xd9\\xbd\\xd4\\x9b\\xa9\\xbb:\\xb5\\x12\\xbd\\x10\\xfb\\xc0;\\x192?\\xbd\\x92\\xa3\\xec;\\xc0\\xe7\\xb3\\xbc\\xe1GO\\xbcA\\xa5\\xd3\\xbc6E\\xe4\\xbb\\x8a\\x8d}=\\xf6/\\xc4<\\x9a\\x95\\t=+\\x8f\\x1d=1\\x9a\\xf8<\\xd8\\t\\xb2<\\xff\\xebB\\xbc\\x1a\\x16?=:X\\xa8\\xbc\\x97\\x1b#;@]\\xff<B\\x88\\xdf\\xbc\\x02/\\xd5\\xbc\\x07\\xc5\\x8b\\xbcg\\x82r<\\xd5\\xcdb<\\xabP\\x10<c\\xb4n=\\x0ep,\\xbd\\x19D\\xc6\\xbc\\x91n\\x84<\\xaay\\x9c;m*\\x02=\\xe2=\\xe3\\xbc\\xde\\x8d\\x7f\\xbc\\xb9\\xb7\\x88\\xbc\\xd8i\\xfc\\xbc\\x08;|<\\xdb\\r\\x8c\\xbd\\x8a|\\x12\\xbd\\xb7\\xb0\\x8d=(1\\x96\\xba\\xa5\\xe9\\xb3<5>\\x92<Q\\xbf\\x8f=\\xc2h\\x83=Di\\x9a\\xbc$h\\xa3\\xbbZ\\r\\xc5\\xbc\\x82F\\xc5<4\\xad\\x9b\\xbc\\xf8\\x1a\\xe2\\xbc\\x88g>\\xbdMA\\xdc<\\x86\\xcbd\\xbd\\xcf\\xc7`;\\xe2>\\x1c\\xbc\\xe2\\xbb\\xc5\\xbb\\xadx\\x10\\xbd\\xa4\\x06 \\xbc\\xc7/7;\\xb0\\x14\\x19<f\\xca\\xa9\\xbc5Q\\x0f\\xbd\\xf1)\\xac\\xbd\\xa5\\xa4\\n\\xbd\\xcd&\\xf4\\xbb\\xe4\\xa0\\x96<|?\\x94<\\xf4\\x98\\xad\\xbb\\x933}<\\xf6,\\xa6\\xbc-\\x86t\\xbdR\\xa0L\\xbc\\x1a\\xc6\\x9c<\\x14T#\\xbc\\x94\\x0b&=\\xfel\\xed\\xbc ?\\x8b\\xbb\\x87\\xca\\x82<K;\\n\\xbd\\xe2\\xcd\\xc9<\\xb5\\xf2\\xde\\xba\\x98E\\x11\\xbd\\xcf\\x14\\xdb\\xbc\\xc6\\x10Z<\\xa9\\xba\\x06=\\x99\\xffB=\\xf6\\xb8`<1\\x99s;\\xafT\\x15<\\xb0\\xe9\\xaa<\\xe5\\xbe\\xa9=\\xe0q\\xf4\\xbcz\\xc6\\x90\\xbcm\\xd6\\xfd\\xbc\\x03\\xc5\\x00=\\xe0w\\x9a;\\xd8\\xea$\\xbd\\x86\\xb2\\xbb;C\\xbd.=\\x80\\x1b\\x86;4\\xdf\\x9a<\\xb9\\xab\\xa1\\xbd\\x1f\\x1a>=g\\xcd\\xed\\xbbxq5\\xbb\\xfe\\x82\\x85;\\xa4\\xaet\\xbd\\xc1\\xed\\x96\\xbchO\\xfc;\\x19B\\xd7\\xbcP\\x02\\x1b\\xbdM`0<QwU=\\xf6\\xad\\xd3\\xbc\\xd95*=\\xf1\\xabA<\\xb5\\x19,=m\\xe3B;\\x89\\x95E\\xbdj\\xb8B\\xbd\\xdb\\x86\\xa3\\xbd\\xfd\\xe7\\xbb\\xbc\\xe7\\xc0\\xb1=;\\x03y<\\x11aN=\\x84^b\\xbdT\\x012\\xbc\\x82\\x9b\\xa2=\\xce\\xa3!\\xbd\\xc5ZX\\xbc\\xe7\\xb1P=nap\\xbd\\x96\\xf9X\\xbcG\\xce\\x86\\xbd\\xf7\\xda\\xc4;\\x8c\\xffw\\xbd\\xdde\\x08\\xbd\\x86\\x10;=I\\xd5\\xf7\\xbbQ~B=6\\n\\xdd<\\xcd\\x99\\x92\\xbd\\\\G\\x8f\\xbd\\xbc\\xc6\\xe9\\xbbI\\xa0D=\\x86@~<\\xce\\xdc\\xd2<~\\x8f\\xd2<I\\x8eH\\xb9F\\xe2\\x12<\\xf6 3<\\xa2\\xe3\\xa1<\\xf4>\\xbb\\xbc\\xd0\\xf6\\xa7\\xbb\\x84\\xa0\\x8a<\\xaa\\x979\\xbd\\x97\\xe8v=\\x89\\xf3\\xb7<1\\x9f\\xf2<\\xb3R\\xfc\\xbb\\x86\\x19\\x9d<\\xcd!\\xc0<\\xa4\\xc9\\x00=\\xe7\\x0b\\\"=\\xadi\\x1f\\xbd\\xbe\\xd6\\x1b=\\xae\\x1b(<\\x12\\xff\\xea<\\xbe\\xd3=\\xbd\\x0c\\x9b\\xf9\\xbbYi\\x97<T\\xf2o\\xbd\\x92F\\xf8\\xba7x\\x18\\xbc\\xc2\\x1e\\x87\\xbc!}\\xa2\\xbc\\xf9p\\t=\\xa3\\xc8\\xec\\xbc/\\x96\\x17=\\xd1V\\xb0\\xbbT\\x80\\xf8\\xbb\\xea\\x96\\x9d\\xbdn\\xcb\\x12=T\\xc5\\xb6\\xbc{\\x88q\\xbc#\\xff\\x9b\\xbcx[\\xc8\\xbcD!\\xa1=A\\xc8:\\xbdl\\xa7\\t\\xbd\\xf6\\xd7\\x13=3\\x1e\\x90\\xbcL\\xec\\x1c\\xbd\\x1d\\xa0\\xf8<G\\xf8\\xa2\\xbc\\x8b\\x8c\\xfb\\xbc\\xd8\\xb3\\x8a=z-u\\xbc8\\x1f\\x84\\xbc\\xef%y<\\xe8\\xb0\\x94<\\xa2\\xd3_\\xbd\\xdd\\xd9H<\\xbe\\xeb\\x03\\xbdJ]\\x97;\\xfc_\\xba<R\\xb8\\x14\\xbd\\x80E!\\xbcg!\\x0c\\xbd\\xf2(\\x84\\xbc\\xb0_\\xef\\xbc\\xe4\\x04`<b\\xe5\\x16<\\xee\\x88\\xcd\\xb9 c\\x12;~SV\\xbd\\xad\\xc5\\xf8<\\x9ae\\xd6<3\\x1b&\\xbcb\\xc44\\xbd\\xb1\\x97\\xb1;\\xef\\x9e\\x9f<\\x05\\x14\\x19\\xbd\\xcaa\\xf3<`\\xe43=\\xe4<\\x88<.S\\xa0<\\x85\\xd2\\x89\\xbd\\xab0q\\xbb\\xbe\\xd8\\x03=\\x166$\\xbd]\\xc2@<y\\xb4\\x7f=cR\\x8f\\xbc\\xdb\\xbb\\x8d=\\x8a\\x9e\\x85\\xbc\\xb5S\\x14=\\x99TB\\xbd\\xc6\\xd3z=9\\x05\\xfc<\\n\\xb5B<D\\xeb6\\xbc\\xda\\xc6\\xcf\\xbbV\\xdb-\\xbd\\xb5\\xe2\\x96=\\xd4\\n5<\\xf6cb<\\xe7M\\xe1\\xbchJ\\x18=O\\xf7\\x81\\xbd\\x16\\x1b\\\\\\xba\\xe7z\\xed\\xba\\xe8\\x95t;\\x87\\x0fM\\xbc\\x9e\\xd1\\xc9\\xbc\\xee\\x9b\\xf1<\\x90/\\x96<\\xb3og<05\\xd8\\xbd\\xa1\\x1aN\\xbb=\\xfbA\\xbd\\x00T\\xc3<\\xc1q$=\\x1f=\\xc2:\\xf7\\xa7,=n*\\xa2<\\x8e\\xce\\x07\\xbc\\xa2\\x98\\x18\\xbd\\xb9\\x98o=n\\n\\xdf\\xbb\\x99\\x88\\xb5\\xbc\\x05C\\xd8=E\\\"\\x1b\\xbd\\xb1\\x87\\xe4\\xbc\\xd6\\x8f\\xbf\\xbcy\\xd7y\\xbc\\xc8SJ=h\\xfd\\xd0\\xbc`\\xf7\\x17\\xbd\\x0c\\x90\\x06\\xbd\\xbe\\xb5\\xcb:\\xce\\x8d\\xc0=\\x81h\\x80\\xbbF\\xed\\x17=\\x00\\x8d3=\\xd8oT=g\\xff\\xf5<T)?=\\x8b\\x10\\xc5<1u[\\xbc#BA\\xbd\\xca\\xe5\\xe6\\xbc#%c\\xbc\\xa1\\xe9\\x96<\\xdb\\xa9\\x04=\\xffDT\\xbc\\x1b\\x15\\xb9=\\xc0\\xea\\x86<\\xd4z,\\xbdjP\\x05\\xbd\\xd4\\x1b\\x06\\xbdg\\xf7\\xd8\\xbbZX\\t\\xbd\\xdb}\\xd6<\\x9c\\xed\\xcb\\xbcdS\\xac=ds\\xa2\\xbdn+\\x1e<\\x860\\x95\\xbc\\xf3\\n\\x96<i\\xb4a;\\x1809\\xbc\\xf5\\xb5\\x9b=\\xcd\\xf1v\\xbbV1t;\\x80\\xad\\x19;\\x8bp\\xd3<\\x81\\x9a\\xf5<\\xa7w[<\\x87\\xb4\\xa6\\xbb;2\\x8e\\xbd\\xff\\xb8/=\\xd5\\xd4\\xc2<Oi\\xe9;\\x94%\\x16=\\xa3WW=\\x0c\\x8c?<i\\xe5\\x04=1\\xf4A\\xbc\\xd5J\\x93\\xbcdi\\x80<k\\x99\\x8f=r\\xf8\\xe3\\xbc\\x12\\xb8t\\xbdb\\xc2\\x08\\xbb\\xc6\\xd9\\xec:\\xa0\\x95\\x0b\\xbd\\xfb6\\xfd;\\xcdM\\x85\\xbd\\x8d\\xfc7=&\\xbb\\xdb\\xb9\\xed@,\\xbb\\n\\x9c\\xb4<\\xa7\\xc44=\\x02\\xaer<uI4\\xbd\\x9aS^=\\xcfK \\xbd\\n\\xfe\\xd8\\xbb\\x80r\\x18=\\xbb\\xc5(\\xbb\\xac<\\xc59\\xe5\\t\\xb9\\xbb\\x1am\\xd5\\xbcw\\x91\\x15=\\xfatJ\\xbc\\x92\\x0cB\\xbd\\x99r\\x07\\xbd\\xa0{V<\\xd9\\x9fS\\xbd,S\\x1f\\xbd\\x82?6\\xbd\\x97\\xb2-;5\\x88\\xf5\\xbc\\xe3,\\xad:ZX\\x83\\xbc=H^=\\xd6\\xa2\\x04<\\xec\\x9a\\x12\\xbb\\x0e\\x03d\\xbd\\xa1\\x15\\x96=F \\x87=X#\\xf5;D8\\xab;\\x86\\x82\\xcc\\xbc\\\"Y\\x03<\\xe3\\xe6\\x85\\xbc=m\\x8f=b\\xb9\\x87\\xbd\\xef\\xad\\xab\\xbc-\\xcd\\xe2\\xbcF\\x18o;\\xc44S=f\\x90Y=\\xf4\\x96B\\xbc/o\\xc1<\\xd4D\\xa6\\xbc\\xe5\\xb81\\xbc\\xe19\\x9f<T\\xa4&=u*#<1iT<A\\x86\\xb3<b\\x9a(<\\x84 \\xa8\\xbc`\\xe6?<\\xbe\\xf4\\xf1\\xbdyd\\x14=\\xee\\xdfV=d3f\\xbb\\xec\\x91h\\xbd\\x1e0\\xc8<4X\\x9b<\\x94\\xab\\x94;\\xd3\\xc1\\x13\\xbdJ\\xdef\\xbc\\xd3\\x97A=T\\xae\\x9c;\\x96-\\xb2\\xbd\\xf0\\xa0]\\xbd\\x9c\\x07\\x88<mN&\\xbd\\xa9\\x03\\xa9\\xbd\\xef\\x0e`;S\\x84\\x0e=B&\\x83=A\\x8c\\xcc<Q\\xa6\\xa5\\xbc\\x18\\xc6,=)o<\\xbd\\x81?\\x18\\xbd\\x9b\\x1d3=I\\x84\\x98\\xbd\\xce\\x8c&<\\x18\\xe5\\xd2;\\xd1\\xa9\\x1a<\\x00\\xf2\\xce\\xbc\\x02t\\xeb\\xbc\\x8d\\xa41=( \\x17\\xbd\\xde\\\":\\xbdT\\xe2\\xa9\\xbc}\\x0bF\\xbd\\x16\\x83\\x99\\xbb\\xb4\\xd0\\x14\\xbdU\\x94\\xe6\\xbcX\\xfdG<\\xe29\\x03\\xbd\\x10f\\xc3<\\x07\\xbe~=xe\\x02=\\xf9Nc=\\n\\xcc\\xc9\\xbc\\xa9w*\\xbc^\\x06\\x85<\\x8e\\xb3\\x0f\\xbd))\\xa8\\xbc\\xec\\xa4\\xdb;\\xf3\\xa2\\x9a=Nu\\xfa\\xbc\\xd7E>\\xbbb?\\xe3\\xbci\\xad<=\\x90w\\xba;\\x01zX=\\xd4.\\x80\\xbd0\\xea\\xee\\xbc_\\x881\\xbc\\x01\\x14\\x98\\xbc\\xb9(\\x99\\xbcK\\xc0L\\xbc\\xee 3<v\\xd98<\\x1c\\xb5\\xd7\\xbc\\x95\\xca\\xb2\\xbd\\xe7r-\\xbb\\x19+\\xbc\\xbb\\x93\\xa7\\x1c=\\x1cS\\xa0<R8\\xa9\\xbdGV\\x02=\\xc6BA=\\x8b\\x1fd\\xbdS\\xb7\\x88\\t\\xab\\x99\\x8c<\\x84\\xdb\\x99<=U\\xe2<{XI:,v\\x91<\\xf4\\xbe\\xe0<\\xdf\\x97G<O=\\xc0<W\\x1c\\x1a=b\\xbe\\x02<{:q<\\x96\\x12<=\\x1a\\xdal<\\x15^\\xb4<.\\\\\\xc3;T\\xd6\\xef<\\x85\\x8e\\xac\\xbc\\x9c\\xfaE\\xbd\\x88\\xf1\\x98<A\\xa1w=\\x0b\\xb1\\xea<\\xc7\\xa5\\xdf;\\xdf0\\xbe:-\\x8c\\x0c=vw\\x0f=\\xadm\\xc0<\\x8a?\\xb7<|\\x97\\x19=\\xdf\\xa8M\\xbd\\xbdY\\xb8\\xbc\\xd5\\xbc\\xf4<b\\x89\\\"<{\\x10g\\xbdX\\x1e\\x89\\xbd\\xc7\\xcb\\xc2<\\x1bJ\\xa6\\xbb\\x02\\xb5\\x8c\\xbc\\xc9:\\xa3\\xbb\\x97\\xe4\\x92<\\xc8\\xe1\\x9d\\xbc/\\xca\\\"=\\xb3\\xc8\\xab<\\xc6DC=\\x92\\xac\\xf1\\xbcA\\xb4\\x84\\xbc\\x81\\xe6\\x90\\xbbS\\xc4\\xe2=\\xe4\\x15\\xb5;r\\xfd\\xa8\\xbc\\x80V\\x84:\\\"\\x02\\x0e\\xbd\\x80\\x0cS\\xbd]B\\xf8;\\xca\\x94\\x94\\xbcA\\xaaO\\xbd G;=;\\x0c\\x80<i\\xe9\\xaf\\xba&z(=\\xc0A\\x04\\xbd\\xb2\\xda\\xac;/\\x90\\x06\\xbc\\xa05S\\xbb\\xd1\\xed\\xa5<\\xa6\\xec,\\xbdA:=\\xbd\\x12\\xc0\\x08\\xbd+~F<\\xe9\\xcej\\xbc\\x93\\x8e\\x00<E\\x8b\\xcc<p\\x8bo\\xbc\\xdf\\x80-\\xbd\\xc7\\x83\\xcc<\\xeb#\\xee\\xbbC\\xdcf\\xbdxEq\\xbd+\\x95\\xaa=\\x8a\\x89\\xa1\\xbd^\\xa0\\x92<\\x1b\\x95{\\xbd\\x191\\xae;\\x82\\x84\\x97\\xbb\\x9d\\xd9Q=\\xbb\\xae\\x0b\\xbd\\x98\\xb3\\x8e\\xbc\\xce\\xd2\\xc2<\\x0f\\x16\\xc0<\\x8a\\x96&\\xbd\\x97T\\n<0h\\x0b<\\x8c\\xa6X\\xbcb\\xd6N= \\xae\\x9b:\\xedo\\xea;\\x88\\x94\\x0e=\\xfc\\xa0\\xdb\\xbc\\xcc\\xab\\x12=\\xa5)\\x85\\xbcZ\\x18!<\\xa6\\x03\\xeb\\xba\\x81\\xa0\\xbb\\xbbi<\\x00=\\xea*\\xfd;g\\xa0\\xc1<@\\x0eY\\xbd\\xfd\\xfed\\xbbE\\xd0\\xac\\xbb\\x97b+\\xbd\\xb9\\xbb\\x9b\\xbd\\xa7\\xbd\\n=\\xa0Y\\xf69\\x9d\\xff\\xaf\\xbdZ8\\xed<k\\xe5Y=M\\xcd\\x08<oG\\\"<\\xee\\xb9\\xd1\\xbc\\xb6\\x93v\\xbd\\xe2U\\x96\\xbd\\x88\\xe4\\xb3<\\xcf\\x1d!\\xbbd\\x95\\xb5<\\x89Z\\x98\\xbc&\\xbb\\t=s\\xd7\\xdf<\\x1f\\xc9p\\xbc2\\x1e4=\\xf7\\xc2\\xfa<>\\xda}\\xba\\xcep\\x0b\\xbc1`\\xa8\\xbdG\\x9e\\x80=\\xb1\\xf4W\\xbb+Ef=d#\\x8e;\\xa8\\xe4\\xec<c\\xf9\\x81\\xbdp[\\xa8<\\xe5\\xa4\\xe4;\\xcd\\x06$\\xbc\\xee\\x80\\x07\\xbd\\x1f\\xbe\\x1c\\xbdh\\xe5\\x02\\xbc/`\\xa6\\xbb\\xdb\\xd8\\xf6;\\x19#\\x80\\xbc\\xe3\\x17\\x89:\\xf34\\x97=\\\\\\\\\\xb4\\xbc\\xff\\xf7L\\xbd/6N;\\x8dpL\\xbd\\x14\\x87&=\\xcf\\x83\\x15=\\xb9\\x01\\xdd\\xbbn{\\r\\xbd\\n\\xd44=\\xef\\x8d)=\\\\\\x81\\x10=\\xf6\\x00\\xb3<\\xc7\\xba\\xae<\\xa2\\xb7\\xeb<\\xbf\\xc6T\\xbb\\xadc4\\xbd\\x18D\\xe8\\xbb\\xcb\\x11\\xee\\xbc;\\xb7\\xa1<\\xad\\xadJ=\\xc4\\xbf?\\xbc\\xee\\xde\\xfe\\xbbpK\\xd4;]\\xcf\\xdf<hR\\xc0;S\\xef\\x8c\\xbboix=N2k=f\\xf6\\x0c\\xbd,\\x9d\\xe7<\\xd1*P<\\x14\\x8d\\xf8\\xbc\\x9d\\xa0\\xaa\\xbdU&>:\\x8a\\xfaC<\\xfd\\x89\\x13\\xbd\\xbelF\\xbd>\\xdf\\x19\\xbdjS>\\xbd\\xbbv\\x0f=7\\xb3\\x82\\xbc\\xb3=\\x93<I\\x00\\xcd<(\\xc9\\x86\\xbd\\xc7|\\x13<\\xce7\\x9b\\xbd[\\xe7\\xa4\\xbdX\\x98\\xcf\\xbat,_\\xbc\\xad\\xa1b\\xbd\\\"}\\x13\\xbd\\xda\\xbe;\\xbd\\x10\\xb2\\x14\\xbd\\xb9\\x82\\x16\\xbcD\\n,\\xbdn\\x7f\\xbd\\xbc \\xfc\\x98\\xbb\\xe2\\x9e\\xb4\\xbbw\\r\\xca<^\\x88\\x84\\xbb\\x00\\xa6\\x13\\xbd\\xa0\\x06\\xbd:\\\"xb<\\x18H\\xfe<\\x9ea\\x11;BRB\\xbc\\xb4\\xdb6:e \\x86<FZ=\\xbd\\xf1\\x18;\\xbcy?.\\xbdU\\xfe\\xf4<+\\xd5r\\xbd\\x13>&=\\xd4\\xdb{=\\x85\\x8eE=w\\xa4\\xd2\\xbb\\x15\\x18\\xb2=\\xc6RK\\xbd\\xe6N\\x95\\xbc\\x1a\\x18\\xe8<Hr\\xda\\xb9\\x19\\x99)\\xbd\\x8eF\\x1f=\\x08\\xe99=\\x8e\\xa0\\xa7<\\xf8\\xad\\x16\\xbd\\xd3\\x93n<B(L<^\\x01\\\":\\x1d\\x04\\n<?\\xfd4=\\xdd\\xa0\\xa3< \\xf7$=\\x91\\xed,\\xbdF\\xba\\x1f\\xbdt\\x882\\xbd\\xd9{\\x9b<\\xfb\\xe86\\xbcS\\x86?\\xbcr4~\\xbc\\x83G\\x19=\\xb0\\x04\\x1e\\xbc;\\xa8\\xf5\\xbc\\x0c\\xb0\\xe4<$\\xfa\\xe4\\xbcu\\x95\\x01=\\x16\\xcdY\\xbc\\xa4\\x92\\xb4\\xbbL\\xc0\\x18\\xbd\\xae!\\xa6\\xbc\\x9d\\xc3\\n\\xbc\\x96\\xa4i\\xbc\\xdc\\x89\\xbe<\\xf4]$=\\xfb\\xef~=L\\xec3\\xbc\\xa5\\xdb\\xa5<[d\\xdf=L\\xd5\\xcf\\xbc\\xf4M\\xe0<\\\\\\x86@=x!\\x9b:p\\xd46\\xbd\"\nHSET bikes:10090  model 'Rhea' brand 'Ergonom' price 618 type 'Kids bikes' material 'carbon' weight 10.2 description 'The innovative braking system on this bike has been a game changer in the kids’ bike world. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \"\\x91z\\xda;\\xf4!\\xfa\\xbb\\xd7\\x14\\xee\\xbcp\\xe7\\r\\xbb`\\x13R\\xbd\\xd9v\\xd4;\\xd5E$\\xbd\\xe7\\x04,\\xbd\\xcd\\xaa\\x9a=T\\xd8$=\\x97\\\"\\xb5:@\\x12\\xa6\\xbc\\xe8X\\x99<H\\x1c\\x84<yS:=AIY\\xbd}\\xe0O=\\\"A\\x8a\\xbd\\\"n\\xd7\\xbc\\xc8\\xdc\\r\\xbd`H\\x16=q\\x191\\xbd\\x88n\\xec<\\x04i\\xc1\\xbc]\\xd3\\x1f\\xbd\\x83\\xa9\\x04\\xbc\\xa0\\xb5\\xea<\\x9a&L\\xbc\\x90\\xd7\\x0c\\xbc\\xa9\\x16\\x8a=\\x96x\\\"\\xbcY>!\\xbd\\xc3d==\\x9c_\\x1e=\\x80\\xe2N\\xbd\\x00\\xc5+\\xbc\\x9f\\x95\\xef<\\x08o:\\xbc\\xd0{Y\\xbd\\xe4d\\xaf;\\xba\\xe7\\xb5<F\\x18\\xce\\xbcj\\xe8$<\\xcbG-:\\xf5\\xc10\\xbcn\\\"\\\\\\xbd_4\\x85<\\x9b\\xa8\\xa6\\xbc\\x0f\\x82\\xee\\xbc\\x16xg\\xbcO\\xd0\\xc1<\\x19\\t\\x90\\xbc9\\x82\\x0b\\xbd^\\xb9\\xa1;#*\\xb3\\xbc\\xc5\\xf6\\xd4\\xbb\\x86;\\x1a<\\xaeA\\x1f\\xbd\\xcfy\\xb8\\xbc\\xdf@\\xbf=!m\\x0f\\xbd\\x1f\\x98\\x1c=\\xebE\\x84;\\xf7\\x1d\\xdb<|\\xbdS\\xbbKf\\xe8\\xbc\\xc75\\xbd<\\xd88\\x81\\xbc\\xc2\\x8d\\x0b=w:\\x98\\xbc\\xc6V\\xce9\\n\\xeb\\x91\\xbb\\xe5\\xaa\\x04\\xbcl\\x17\\x9e\\xbd\\xf8\\xae\\xf5<\\x9e\\xf5\\xef\\xbc|0\\xa1\\xbc\\xc0\\xe8\\x01\\xbd\\x92\\xfc$\\xbdV\\x8dV=\\x00\\r\\xb8<\\xc2t\\x18\\xbddq\\xd4\\xbc\\xd3SM\\xbd\\xabf\\x05\\xbc\\x0c\\xec\\xe2\\xbc\\xc5\\x13\\xa9\\xbcV\\xf7\\xa9<Syj\\xbbC\\x8a\\xbc<\\x03R\\xa5\\xbc\\x96\\xb3\\x97<\\xff}\\x06\\xbc\\x02t*<\\x86\\xcd\\xb9<J|\\x8f<\\xc9:;\\xbd)\\xf3\\xb3\\xbcw\\xd4\\xef<\\\"\\xda,\\xbd^\\xf0\\x16\\xbd08\\xf8<\\xcaO\\xb1;\\x06\\xc4\\n\\xbd\\xf7\\x7f\\x82<Q;\\x0c>\\xb4|\\xf3<\\xf6\\\"\\xbf<Vf&\\xbck{\\xbc\\xbb\\x12\\x1b[<b~\\x1a=]:\\x9b<\\\"\\x9b\\x16\\xbcB\\xab\\x8e\\xbd\\x88\\xab\\xaa\\xbc6\\xe8\\x15<\\xb9g7<\\x88\\xb6\\x12=?Q\\x91\\xba\\xeb\\xe32=\\xb3,\\x9d;\\xa1\\xc3\\x85\\xbdbD\\xd3<\\x04\\x02C=4\\xe9\\xd9;\\xe06\\x86<\\xe3v\\x1f<\\x7fhC\\xbd\\xb6s\\xed\\xbc\\xe3\\x96\\xe0\\xbc\\xf4\\xc0\\xb0\\xbcU~\\x0b=\\xcf\\xdd\\xb2\\xbc\\xa7\\xa0\\x06=c\\xb2%=2\\xfd\\xe3\\xbc\\x89*~\\xbc[H\\xeb<6\\\"+\\xbd\\xd9\\xb7[\\xbd\\xbc\\xff\\x93\\xbd\\x81^J;,>\\x83=\\xaf?\\x0f\\xbd \\xbb\\xdf<\\xbd\\xca\\xb1\\xbc\\x1f\\xe51=\\xd9K\\xd6\\xbb\\x04\\xd2\\x82\\xbc\\xc3\\xa8$\\xbb\\xa4\\xc6\\x87<\\x9f\\xaa\\x00\\xbb\\xe5\\xa2\\xea<\\xc9\\x9cn\\xbc\\xfc\\x13\\r\\xbc\\xef \\x0c\\xbd\\x02l\\n<\\x04\\x0b =^\\xe5C\\xbd\\x8f\\xdc@=\\x92\\x0b\\xbe;\\x14\\xcaD\\xbb)\\xc4A\\xbdTk\\xe3;\\x8fi\\x0f=Q\\x8d\\xa4;\\xfa\\x1b\\x88<\\x93\\x92\\x08=-7.\\xbc\\xa9\\xb0\\xb5\\xbc\\x7f\\x9ds=\\x18\\xf1::\\xe6\\xf9\\x10\\xbdA\\x13\\x1c\\xba\\xa3j\\x11=\\x89\\xca*\\xbd\\xa7mZ=.<\\xc4\\xbbx\\r<<@\\xe8\\x9a\\xbc\\xbf\\xc7<<\\xb7K)\\xbc{\\xf9$=\\xab\\x9f\\\"=;\\xdb\\x96\\xbdO~K<)\\xca\\xf6<\\x9f\\x958=\\x9f\\x1f\\xf7;e\\xd0\\xeb<\\x1e\\xc0!=\\x1e\\x1c\\x8d\\xbd\\xd5\\x0f\\x83=\\xdb\\x18(<\\xc1i\\x89=\\x99:@;\\xb9y\\x8a<A]\\xb9;\\xb9\\x1c\\xd6<R\\xb3\\x19\\xbbh\\xc30\\xbd\\n\\x18\\xba\\xbdL!\\x8c9\\xf8g\\xd8\\xbc\\xd6\\x1f\\xd5<9Z?\\xbd\\x05]\\xc4\\xbc\\x80\\x9dv=\\xfc0$\\xbd\\x8c\\xb0\\xdc\\xbcd\\xf06=0QQ=\\xd8D\\x96<[\\r\\xc3;\\xa4\\xdb\\x9c<\\xd4\\xec\\xca\\xbd~\\xa6\\xc7\\xbc\\xb7G\\xbe\\xba\\xda\\xbf3\\xbdOh\\x12<\\xbb\\xb1\\xe2\\xbc\\xe5\\xbc\\x85\\xbd\\x8c\\x1e\\xf8<T\\x0b?\\xbc\\xe7\\xb7\\x07:\\x13\\x8cB=M\\\"V\\xbc;YM=\\xfeM\\x9e;\\x03z\\x00\\xbd\\xc6\\x98\\x96\\xbb)jQ=\\xb5^\\xc6<Q\\xe8\\\":\\xc9\\x06\\xb7\\xbc7\\xd4\\x8c\\xbcj\\x813=\\xdf\\xd9^<J\\xd8\\x01:J?\\xaf<\\xfe\\xfa[<d\\x1bF=Rk8=\\xe4\\x96\\xac<>\\x03i=\\xaad\\xd3<\\x90\\x9cJ<\\x19\\xf2\\x89\\xbc\\x90\\x9b}\\xbc\\xe0\\x02\\x9f\\xbbM\\r\\x9e\\xbc\\xc9\\x1aq<\\x19I!=%\\xd1\\xe79!*\\xc0=F\\xb3\\xaa\\xbcP\\xdd\\x16=7>a\\xb9\\xe24:=\\xc3P\\xdd\\xbc\\xf3\\xfd\\xf9\\xbb\\xa2s\\xb5\\xbc\\xfe\\xc8\\xda\\xbc\\xd6\\x91\\x97\\xbd\\xba{P<~\\x99\\xfd\\xbcy\\x8b\\xbd;\\xa6\\x8aM<\\x96\\x9cg<_8\\xb78\\xad\\x1a\\xb7\\xbc\\xa8\\xc7B\\xbdg\\x02\\x08;\\x18\\xef\\xb5\\xbc\\x01\\\\\\xb4\\xbc\\x83\\xde\\xc8<OYv\\xbcE\\xa7\\xd1=d\\x07\\x88\\xbd\\xf2\\x1eZ:3t\\xae\\xbc\\xdd\\xcc\\x1d=\\xf2\\xae\\xd2\\xbcr\\x96\\xff;w(s=\\x1a\\x06\\x08\\xbdY\\xd9\\x93=k\\x80>\\xbd\\xb6\\xe4\\x82<\\xed\\xebL\\xbc\\x14\\xb6\\x0b<\\xd6\\xc8\\x8d=\\xbb[\\xed\\xbcE\\xc4j<}d\\x0f\\xbd\\\"\\xae\\x0f;\\xc2nF;\\x01]\\x15\\xb9=9\\xee\\xbc\\xdc\\x0b\\xcf<g\\xbe\\xe3;\\x95\\xae2<\\xa1\\xfd\\x1d<\\xff\\xb2\\xd6<\\xdf\\xd7\\x88<\\xd9;2<*\\r\\xdf;\\\\\\xa8\\\"=\\x80d6;\\xf1\\xee)\\xbd\\xd2$\\x10\\xbd\\xf3t\\x8c\\xbc\\x11LC\\xbc**\\xbd;\\x05>\\xbc\\xbdB\\xb7\\x0f\\xbd\\xe9\\x15\\xc1=H\\xbc\\xc9<\\xc4qQ\\xbc\\xc9b\\xc9\\xbc\\x83;\\x0b\\xbdj\\xfd\\xa2<\\xef\\xbc]\\xbc?m\\xd4<\\xd5\\xfd\\xed\\xbc\\xfe\\xaf1=\\xa1V\\x0c\\xbd\\x1a`\\x81=od4\\xbc}\\xc1\\xe8<\\xdc\\x00\\xbb\\xbc)#\\x0b;\\x0b\\x97\\xc8\\xbcx\\x9f\\x0f=.J\\t\\xbc\\xc1\\xaeE\\xbdzm5;\\x19/1=L\\x0c\\x17;\\x1b-\\x04\\xbd\\xc8\\x7f7<\\xfba\\xbe=\\xef\\xf9\\xfb;\\x16\\xa3\\x12;\\xb2C\\x96<\\xc3\\xfd0=ga.<A\\x06Q=\\x7f\\x97\\xb0<T\\x84C<^\\xd2\\xf9:\\xe3\\x1f\\xb0=\\xe5\\xc7\\xae;\\xf1\\xdb\\x81\\xbd\\x80\\xbb)\\xba\\xe3\\xf2\\xf2\\xbc\\xf2\\xfc\\xe8\\xbcl\\xa0\\x00<\\x02\\x0b\\xd2\\xbd\\x9c\\xb9\\xd7<\\\"\\xe6\\x88<\\xcfz\\x12=4\\xf4\\x9c<\\xbfw\\x0f\\xbc\\x9dN\\x87\\xbcVv\\xd9\\xbce\\xd9R<\\xb4\\xeaS\\xbd\\x1c\\x03\\xcd\\xbci\\xd8\\x87<*\\xbf\\xd7;\\xee\\x89\\x94\\xba\\x8c\\xa3\\x1f=}\\xff\\xd9<\\xd0w\\x9e<\\xf2\\x1c\\x87\\xbcD\\xf7A\\xbd\\x12\\xfb\\x8e<x\\x0e\\xae\\xbc\\xd5N\\xca\\xbcy\\xea\\r\\xbd\\x0f\\xd1\\x8d\\xbc\\x06\\x02\\xee;\\x8a\\xbfl\\xbd\\xa9\\x0f\\x1f=\\xdah\\x94\\xbcT\\x14\\x85<8\\xd5O;\\x89x\\xa9\\xbc\\xd1G\\xc6\\xbc\\xefG\\x92=\\xe3[\\xe3=(|\\xc4\\xbc\\x14\\xce\\xa3<&\\xf9\\x0c\\xbd\\x02\\xde\\xd9\\xbc\\xf9\\x17\\x9d\\xbc\\x13\\x8c\\xfe=\\xc9\\xeb\\x8c\\xbd\\xcf\\xce\\\"\\xbd\\x81\\xe9,\\xbd\\xd1\\x11m=|i\\xc1;\\xec\\xf4k=\\x05\\xd5\\x0b\\xbd-\\xed\\x8f\\xbb\\xf9\\x17n9\\x81\\x9b\\xad\\xbc\\xca\\xfc\\xc4<u\\xbf\\\"=\\xday\\xce\\xbc\\x9aU\\xe0<O\\xd8J<u4 =\\x8de\\x93\\xbc7\\xca\\xde<}S|\\xbd\\xc1\\xe3\\x01=*\\xc1I<\\xcb\\xe73\\xbd\\xd7d\\x1c\\xbd\\xde\\xeba=j\\xa9\\x90=\\xa1\\xbb\\xf5<\\xa0\\x9fb\\xbdd\\xda\\x19\\xbd!`C;K\\x9f\\r=\\x18\\x8d\\r\\xbd\\xf0d\\xa8;\\xda\\xe1Y<\\xea\\x97q\\xbde|\\xcc\\xbd{\\xde^\\xbc\\xf5V\\xad=\\r\\x04o=q\\x9a\\x9e;\\xaf\\xf6\\x7f\\xbd\\xe0\\xea\\xc8;\\xb5\\x0e\\x9e\\xbd\\x07\\xbc\\xa5\\xbcW\\x7f\\xa7<\\xdf\\xe9\\x94\\xbc\\xe3\\x0e\\x8c<\\xa9\\x1d\\x98<\\xa1\\xdc\\xa6<e\\xf4\\xad\\xbb\\x14\\x19w\\xbd\\xf1\\xfd\\x80=\\xbd\\xe4C\\xbd\\x9a\\x9dO\\xbd[\\x08\\xab\\xbbD\\xed\\x9d\\xbd\\xec\\x05\\xbe<\\x81h\\xa0\\xba*j\\xe0\\xbb;\\x9d\\xa3<\\xa7\\xb2\\t\\xbck\\x16\\xfc<\\x01\\xbbW=\\x08\\xa5\\x1d=\\xe5 F<\\x163\\x87\\xbd\\xbdw\\xd9\\xbb\\xe5\\xe4\\xd9<\\xbc\\x83\\xd1;.-w\\xbdo\\xad\\t\\xbd\\xdcp\\xa1=\\xbc\\xf6/\\xbc\\xdd{\\xa7<\\xd3\\xed%\\xbdr%\\xcd;C{\\x85<\\x1d\\x9a\\x9a<=\\xc6\\x05\\xbd\\x8a\\xfc>\\xbd|\\xe6#;\\xd6\\xa0\\xc3;\\xc0?C;EGG\\xbcS<\\xb3\\xbb\\x90\\x07\\xdb;\\xb6\\x92\\xd5;D\\xb1\\xb8\\xbd\\xeaP?;\\xcc\\xb5\\xcf<\\xe1Kw9;\\xdb\\x8a<\\xbd\\x1d\\xc5\\xbd\\xbe\\x99\\x9d<\\x18]m<3\\xbb\\xd4:\\xcc\\x05f\\tGU(\\xbc\\x08\\x91\\x81\\xbasq\\x1a<\\x80$\\x97=\\xb7\\xda7=%69=wn\\xbe<\\xd3\\\\c\\xbc\\x98\\xc9\\xb7<\\xfc\\x06\\xc0\\xbc\\xb9\\x8c\\xc1<\\xf7\\x7fE;t\\x8c\\x1a;J\\x96o\\xbb\\xb94E\\xbd!\\x86\\xdc;\\xbd\\xda\\x89<\\x9e\\x8a7\\xbd\\x98/\\xb0;\\nF\\x7f=U3\\xf1:s\\xd8%<\\xb6W\\x1f=\\x13\\xef\\x009-\\xee\\xc4<\\x18\\x14\\x97<Y\\xefb=\\x96UW<\\x17\\xe7i\\xbdh\\xc9\\xcc\\xbc\\xe9\\x89\\xe2<\\xb0z\\x06:\\xf9\\x13K\\xbd \\xb5@\\xbd\\x7f|\\xab\\xbc\\x17\\xb04\\xbd\\x0e\\t\\xaf\\xbb\\xe3\\x8d\\n=^V\\x80<\\xbeG\\xfd\\xbb\\xff\\xf8X\\xbb\\x08\\x0c\\x0b\\xbb\\xcf\\xd3\\xb2<\\xf65/\\xbc\\xf6+\\x83<:F\\t\\xbd\\xf1\\x05|=\\xeb\\xd6\\xe9\\xbc\\x15\\xc7\\\"\\xbdaY =Y\\xdc\\x02\\xbdP\\x85\\x13\\xbd\\xf3\\x9cE\\xbc\\x1d\\xd5\\xa3<\\x86\\xc8.\\xbdo9\\x1b=\\xab\\x8f$\\xbc\\xbej\\x96\\xbc\\xbd\\xa1\\x88<\\x9a\\x15l=k\\xca9\\xbc\\xf3\\x91\\xb0;\\x12>\\x8c\\xbc\\x1f\\x07\\x01=\\xb8\\x14\\x95;\\xd6\\xdc\\x18\\xbd\\x9c(\\x00=\\xdf(\\xc7\\xbc\\x8c\\xcd\\xbd\\xbb\\xda\\x88\\xc8<\\x8d\\xf8\\x93<\\xf67\\xa0\\xbc\\xb6\\xe2y\\xbc\\rT\\xa5<\\xf7\\xc1\\xc8<\\xf9\\x1d\\\\\\xbd\\xa8\\x88\\xb7\\xbcy\\xcd\\x8b<\\x032\\x17=O\\xcd\\xbe<\\xff\\xa5\\x81\\xbd+\\x12h\\xbciYW\\xbb\\xaf\\xb6 =\\x0f\\x02!\\xbd\\x19rV<\\\"<\\x85=w\\xfc\\xe8<6\\x08\\xbc\\xbc\\xde\\x14\\\"=\\xb8a\\xa8\\xbc!\\xde\\x96\\xbdv\\x8c3=R\\xbf}:Lv\\x92\\xba\\xee\\xba@<\\xcb\\x11\\xb7\\xbc\\xbd\\x12\\xf3<!\\xbd\\xbb\\xbb\\xb7\\xfdk=LA\\x9d<\\xd7\\x04\\xac\\xbc~\\xc5\\xa2<Lpw<\\x9f?\\xda:2\\xdb\\xb1\\xbbZ\\xe9\\x89;L\\xf9Y\\xbc@\\xda\\xf7\\xbc[\\xd6\\xa2\\xbcD$x=\\x136\\x03\\xbdx\\x1b\\x04\\xbd@\\xfd\\x97<\\xe4\\xbf\\xc2=a\\xb8&\\xbc\\x14\\xe5\\xeb;t\\xa7\\xd1\\xbc\\xd4j\\xdc;&-r\\xbc\\x9cU!=B\\xc7[<\\xa7\\xed\\xbd</,J=1\\xc5\\xc5<\\xb2\\xf3\\xe0<\\xa7\\x80b\\xbdW\\xb4B;Y/\\x87=\\x90\\xc9\\xa9\\xbc\\x9a\\xa4\\xb9\\xbc\\x10|\\xa3\\xbdJf4=]O\\xbe\\xbc}\\x8d\\xdc=\\x96\\xba\\xa3\\xbc\\xd0\\x02H=\\xcd@E\\xbd\\xbe\\xfc\\xc9\\xbb\\xa8\\xd2-\\xbb2\\xbf\\xe7<F\\xe9\\r\\xbd\\x9ak\\x16\\xbdr\\xb1\\xc2\\xbc\\x9d\\xb7\\xf2;\\x9a\\xde\\x06=x\\xack=IS\\x05;\\x94>_=\\xb6\\\"\\x18\\xbdA\\xb3j\\xbd\\x8d\\xe8\\x85<*\\xa52\\xbc\\x85\\xf2\\t=\\xb6\\xf2b=\\x1dg\\x86<\\x1e=\\x94\\xbd\\xc4\\xe6\\x99<\\xd8%\\x0f<\\xe2\\xf1\\xe4;%\\xb2\\xc1;\\x81\\xdb\\x83=\\xf6\\xb4\\xaf<$>W\\xbc\\n\\x1bq\\xbcrf\\xec\\xbc\\xf2@\\x01<\\xb7\\xe4e=\\x95bF=\\xc4\\x11\\xeb<\\xd1A\\x18<`h\\xa6\\xbc\\x10\\xcf\\xdd<\\xd3\\xd1|<\\xe75\\xd2<\\x18\\xe3:=\\xf5D\\xc0\\xbc\\x8a\\xb4\\x08<\\xea\\x82\\xd5<s\\xd9v\\xbcO\\xe0\\xb6\\xbc\\xad\\xef\\xd2\\xbd\\xe4/\\x13<\\x84\\x96\\xd3<\\x8e\\xc0.\\xbc\\x9av\\xf3\\xbc@L\\x14;\\x9f\\xa5\\xd8\\xbb\\xf9\\t =\\xe5\\x1a\\xa8\\xba\\xc5\\xa8\\xec\\xbcu~\\xc5<j\\xf2\\x86\\xbd \\xc4\\x15\\xbc\\xc5\\xf3e\\xbd\\xd18\\xbb\\xbd<\\xf27\\xbd\\xfb\\x9a\\xdf\\xbc\\xfe\\x86\\xcb\\xbc\\x8b\\x0e!\\xbd`\\x1b \\xbdkP\\xc1\\xbb\\xda\\x97\\xa5\\xbae\\xe0\\xa5\\xbdu\\xa5|<\\x853\\\"\\xbd\\xb16d<\\x88\\xa0\\xcb\\xbc\\x81Y\\xdf\\xbb\\x17\\xa1\\xa9\\xbc\\xb1\\xf1d:\\xb7%\\xca\\xbb\\xa4l\\x06=\\x16U\\xf5:4\\xcd\\x8a\\xbaiw\\x02<\\x9aN\\x88\\xbb\\xf8\\x1f\\xfd\\xbc\\xe7\\x99\\x8f\\xbd\\xd4\\xce\\xcd\\xbd\\x97\\x8a\\xc6\\xbc\\x80\\x1cK\\xbcn\\xe9\\x8c;[\\xc7!\\xbb\\x1a\\x960=:\\xe1_\\xbd\\xb3\\xdd%=\\xc0\\x03\\xff\\xbck\\xbb\\xab\\xbd|\\x18\\x18=\\x80\\xeb+<\\xbd\\x95\\xd0\\xbc{\\xc4\\x1a\\xbdg\\xce\\x1f<\\x96\\x02\\x8c;\\xc2\\xc4\\x1f\\xbd}\\xb2\\x1b\\xbd\\x84\\x17Q\\xbb\\xf7\\x8d\\x8b<\\xd6\\\"\\x04\\xbc\\xfb\\xc5|<\\xea+\\xf1<6\\xc9\\x13:\\xb2\\x19O=6\\xe5\\x10\\xbd=U\\x0b<\\x0bTI\\xbd\\xe8\\xf8U=\\xab\\xe8+\\xbd\\x9d\\xf3\\xed<C\\xac5<s_a\\xbb5\\xb8\\xba9b\\x93\\x83=\\xa0\\xdc\\xf6\\xbc\\xf6\\xa7\\x10\\xbc2\\xd4\\x1d\\xbcv&\\x05<J\\xedH\\xbc\\xbc\\x18y\\xbd\\x11\\x87\\xdf<\\xfc\\xd4\\x02\\xbd\\x8c\\x81\\xf9\\xbc\\xff\\xaa\\x81=\\x8a\\xac\\xb5=\\xb4\\xb1\\xa8:\\x05\\xdf\\xf8<\\x9ec[=%\\xa5F<i\\x1a\\x9c=i\\x82\\xa7\\xbdG<D\\xbb\\x08\\x8b\\x02\\xbc\"\nHSET bikes:10091  model 'Vanth' brand 'Breakout' price 903 type 'Commuter bikes' material 'alloy' weight 12.7 description 'A real joy to ride, this bike got very high scores in last years Bike of the year report. With a slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance.  That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"j\\xb6|<;-\\x01=\\xafJ\\xc6\\xbcG\\x97%=\\x93x\\x8e<\\xd3\\xcb&=E\\xb9\\x0e\\xbd\\x1e\\xaf\\xe0\\xbc\\xbcL\\xa4=0\\x8eg<0\\xda\\x85\\xbc\\xb84\\xf59\\x12\\xdd\\xbc<ae-;q\\x92|=\\x14\\xbc\\x80\\xbd\\xce\\x91\\xe0\\xbb\\xf8\\x94\\x0b\\xbd\\xda\\xa6\\xb9\\xbc\\xf24\\x8d\\xbd|\\xd7P<\\xdc\\xc1.\\xbd\\xa0\\xab!=\\x01\\x03\\xc3\\xbc\\xdc\\xe3%=\\x83\\xfc\\xab;*i\\xb0\\xbc\\x19\\xd6\\x92\\xbbl\\x7f3\\xbdY\\x84\\x03=X\\xae\\xb1\\xbc\\xe0\\xfb\\xe5\\xbc\\xd9\\xd7\\xd1\\xbck\\xdb\\x06=\\x02p\\x9d<\\xa4~k<\\x00\\xb0\\\"\\xbc\\xe2J\\xcf\\xbc\\xa1i\\xdf\\xba\\xbcx\\xa3:^r\\x16=E?\\xcf\\xbc\\xf9H\\r=\\xe9\\xf9\\x9e;\\x98{\\xe3\\xbb\\xf0Az\\xbc\\xe0\\x01\\x98=\\x83\\x90\\x11\\xba\\xa8<\\x95\\xbc\\xf3\\xc3&<.\\xb6`\\xbc\\x8d\\x1e\\x8d\\xbd\\xaf\\\\\\x92\\xbc\\x833\\x8c<R7[<\\x8c\\xc0\\x83;\\xf6\\x82g<\\xe4\\xacA\\xbd\\x91mB\\xbdo\\xb0]=\\xa2Y\\xe8<\\xa1k\\\\\\xbc-M\\x10=Rrl=h\\xe2!\\xbb\\xcfL\\\"\\xbc\\xe3\\xa0\\xb4\\xbc\\xe7(\\x17=\\xd6\\x07\\xd6\\xb9\\x97c\\xf0;\\x07]\\xfa\\xbbL&\\x90\\xba\\xfe\\xdaH\\xbd\\xc5\\rM<y\\x91\\xfd\\xbc\\xdd\\xec\\xbf<\\xe1\\x08\\xb9\\xbb\\xb3\\xde\\x1e\\xbd\\xb9\\xf8~\\xbd\\nlM<\\xd3\\xfa\\xbb<$\\xf9\\x18\\xbd\\xdb\\xab\\x0c=\\xcfE\\xa3\\xbd\\x13\\xb0\\x00;\\xe0\\xed]<\\xf8b\\x17<\\xc1\\xe6F<\\x9e\\xd0E<\\x06\\xd1\\x9d;\\x13\\xcb\\xbd\\xbc\\x89t\\x84\\xbc\\x8c`\\x13<:*;<>\\x13<\\xbb\\xac+\\xa9\\xbc\\xf9\\xb3\\x98\\xbc\\xa3\\t_\\xbd;\\xb7\\x99<+b$\\xbdd\\x9e\\x86<[:\\x18=\\xbc\\x95\\x99<-\\x84\\x9f\\xbd\\xbf\\x93c\\xbc`h\\xcf=Q_\\x8c<\\x1du\\xc6<\\x00\\x05\\xa3\\xbb;\\x8d\\xf8<\\x06C\\xf4\\xbb\\x9eP\\x9f=\\xc6.\\xf8\\xbb\\xeb.M;\\xce@!\\xbd\\xb8o5\\xbd\\x0cN\\xf7\\xbcxh\\x1a\\xbcEJ\\xc7<\\x12/\\x86<a\\xfd\\x82<\\xcb\\xc8\\xb9;\\x9f\\xba\\x97\\xba?\\xbd\\x9e\\xbd9\\xc0|=2\\xb1\\xd6\\xbc\\xeb\\xf2F;I\\x06\\x1a\\xbd\\xb9%\\xcb\\xbcz\\x8c\\x1a\\xbd\\xbe\\xca\\xc7\\xbc\\xee/\\x05\\xbdH\\xeb\\\"=\\xb9\\x87\\x1c\\xbd\\x04\\x9ff<\\xc6\\xf1\\xa8<\\x9a\\xa5\\xc7\\xbc\\x0eF@\\xbc<\\xfe\\xea<\\x1d\\xd9\\xa2\\xbcZ$\\x12\\xbdA1\\x05\\xbdM\\xe23=\\x10J\\x88=\\x92\\x7fv<6p]\\xbc#H\\x90<\\x93e\\xbb<?z\\x02\\xbd\\x93\\xcd\\x0b\\xbd\\xa0!\\xf4<,\\xea\\x05=\\x8cl=\\xbd\\x162P=\\x03\\xe3\\x17\\xbdo\\xbf\\xa5\\xbc\\x9e36\\xbd\\xcc\\xa3L\\xbc\\n]m<\\x80,N\\xbd\\xd5\\x88}=Y\\xeaB<\\xb8\\xb4\\xda\\xbc\\x06\\xbbk\\xbd\\xf4\\xe72<\\xf9\\x1cu<\\xdf\\xf1\\x80<\\xa5\\xd6A=b\\xbc\\\\=A\\xf6\\xc0\\xbb\\xda\\xc4h\\xbc9\\xc0\\xd6<0}\\x9f<(\\x11\\t\\xbd\\xca\\x7f[\\xb9\\x86\\x8a\\\";\\xaa2\\x80\\xbdS\\xbe\\xa8=\\x15\\x1e\\xce;e1B\\xbc\\r\\x0f\\x9e\\xbc\\x12(\\xa1<\\x81Jh\\xbd\\x06\\xfc0=V]\\xec<[\\x17R\\xbd\\\"0\\x00=\\x1e/\\xd8\\xbb\\x01\\xe1\\x00=\\xa7\\x10W\\xbd\\xaa\\x129\\xbbF\\x0c\\xb3\\xbbJ\\xe3\\xf8\\xbc\\x9e\\xb7\\xcb<\\x98\\x15\\x19<&\\xa4t=\\xb0\\xe00\\xbd\\xc7\\xe2?\\xbd\\x93\\xb8\\xcb\\xbcQ\\x07\\x88=3\\xb2\\n\\xbclw\\x86\\xbde0\\xf6\\xbdD\\xb1\\\"\\xbd;3.\\xbc\\xd5\\xd70;N\\x1d\\\"\\xbc\\xfd\\xb9H\\xbc\\x18\\xed\\x07=D\\x11\\xde\\xbc\\xa4<\\xf2\\xbc\\xa1\\x1eb\\xbc\\xe3\\xeb\\x1b=\\x05x1\\xbdAl;<\\x9f\\xf9\\xc7\\xbc+\\xc3\\x05<\\xd4W\\x1d\\xbc\\xef\\x84J=\\xb2\\xf6-\\xbd\\xd1\\xf7\\x87<\\xb6\\xb4\\xde:E\\x82I\\xbd\\x0b\\x05\\xbc<Ok\\xe4\\xbcd\\xd5^\\xbdL\\x81\\xab<=\\xff\\x80;p\\x18\\xb6<\\x84\\x9d\\xcc\\xbd\\xb6(`\\xbd^\\xb6\\x1b\\xbd\\\"v\\x01\\xbc0!\\x98=\\xa9iq\\xba\\x0eIO<D\\xa5\\x1e=\\x1fv\\xff<\\x8bO\\xb6\\xbcx\\x8d\\xa4;l\\xfc><\\x1c\\x83\\x809R?\\x8b=\\x1c\\x1c\\xb0;\\x8e\\xa1m8\\x1dq\\xd6<\\x99\\xeaS=\\x94\\xf3\\xf9\\xbbU\\xc2\\x99\\xba\\xfc\\xf4\\t=\\xc7\\x9c\\xe2<\\xac\\xa8\\x87\\xbd\\xc9q\\x00\\xbb\\xa3#\\x95=\\xad\\xa4m\\xbd\\xf6\\x1c\\xc0=\\xe2\\\"\\xd2\\xbb\\x80\\xd6\\x14\\xbcT\\x81\\x90\\xbc\\xab\\xbd\\xc5=\\xbf\\xbc\\x80<\\xd9}\\xec\\xbb8\\x11/\\xbd\\xc6\\xa73\\xbc\\xfb\\x1eq\\xbc\\xf5\\xa7\\xf2=\\x01\\xc0\\x07\\xbdi\\x8a\\xf0\\xbc\\x80-\\x17=\\xa2\\xa5Q\\xbc\\xfe\\xd0\\x98<jT;<q\\x07f\\xbd\\xf1\\xd3W<d=\\\"\\xbc\\x1d\\xe1\\t\\xbcy\\x03\\x06\\xbd/\\xce\\xc5\\xbc@*6=D\\x8c\\xa1\\xbd\\x1a!\\x02\\xbd\\xd7u\\xb0<%K\\x92\\xbc|\\x7f\\x95;(\\xa4\\xe7<\\xc1z\\x06=[\\xcd\\n\\xbd\\xe0]4=?%u\\xbd\\xe5\\xe8\\xab;zp\\xa8<\\x0f\\x82]\\xbc\\xc4\\x8e\\xcc=h\\x01\\xd3<\\xcbH\\x84\\xbc\\xd7+\\xdf\\xbc\\x03\\xae\\xf5<=\\x99\\xf0<H!8=\\x15\\xd8F\\xbd\\rXa=\\x7fRM\\xbc\\xcc\\n(=\\xebL\\xaf\\xbc[4\\xed<\\xdc\\x85?=\\x02\\x8c\\xb2\\xbc\\x0e\\x00\\xe7<\\t\\x93\\x98=W)\\x86;6J\\xf1\\xbcLt\\xd2\\xbcA\\xdc\\x0e\\xbd_~\\x88\\xbc\\xdd$\\xac<\\xe6\\xfd\\x83\\xbd\\x041\\x9e\\xbc\\x8a\\xfe\\x83=\\xa6\\x0e(=\\xba,\\x1d\\xbc4\\x1e4\\xbd\\x1a\\xd0\\x8f\\xbd#g.<\\xc0\\x96W\\xbdx:\\xc7\\xbc\\x0b_\\n\\xbdd\\xcb\\xe9\\xbbu\\xfbl\\xbd\\x9b\\xa4M=\\xd4\\xbd\\xd7\\xbc\\x85<(=\\xc2\\x8f\\xa0\\xbd-\\xa6\\x83\\xbc\\xb6\\x94\\xe29h\\xca%=4\\\\\\xa6\\xbc$*\\x87\\xbd\\xc5\\xed\\xd7<\\xc0\\xdb,=\\xc1\\xb6\\x90\\xbb\\x05\\xf3\\xa1:\\xe8\\xc4B\\xbd\\xf8\\x84\\xb0=kP\\x03=\\xaa#\\xab\\xbb\\xfbC\\xdc<k\\xf73=\\xaa\\x10\\xa7<\\xa9\\xfc\\xf0<.uQ\\xbcm\\xaa2\\xbd\\xb5\\xa8c\\xbc\\x10\\xdd\\xf7<\\x8d \\xba\\xbc\\xe9\\n\\xa2\\xbd\\x93\\x03\\xbe\\xbc\\xcb\\xb9\\x18<n\\xb95\\xbc\\n\\xae6\\xbdF*\\xf7\\xbd\\xaf\\x8c\\xf1\\xbc\\x96\\xd1\\x8a<\\xe0\\x0f\\x15\\xbd\\x11\\x81q=n\\x14\\xc8<C\\xab\\xee<\\xf5\\xc2\\xd6\\xbb\\xdb\\xbe\\xf6<\\x1c]\\x07\\xbd\\xc4\\xf1z\\xbdn#-=$]\\xdf\\xbb\\xfe\\xa6H\\xbcS\\x99\\x14\\xbdu\\xa0@<)\\xf1\\x1c=\\xe86\\x84\\xbc<\\xcb_\\xbd\\xbe\\xf6\\xcd\\xbc\\xc0ze<\\xfa$\\xd5\\xbc\\x16\\x10v\\xbdf\\xf5t\\xbc\\xa2\\x1e\\xb1\\xbc\\xabF{\\xbd\\x892\\x99\\xbc\\xd6/\\xdd\\xbc\\x05\\x0c\\\"\\xbd\\xff\\x8a\\x81<\\x11N\\xba\\xbc\\x8cv\\x88\\xbd\\x8e:L=C|\\xa0=\\x7fY\\x13\\xbd(\\x84f<h^\\x1f;\\xecF\\x8b\\xbd\\xa7\\\\C\\xbc\\xb3\\x10q=\\x08\\x1f\\xb4\\xbd\\x16\\xe4\\xb89\\x81\\x84\\x85\\xbb\\xf5\\x95!=\\xf6.\\xd2<\\xa7\\\\\\xd8<\\x8f1\\xbb\\xbc\\x98\\xa4\\xa6<7\\xb8o:\\xa4\\xb5\\xb9\\xbc\\x80])=\\x06\\xcd\\xed<v\\xd4\\xf2;\\xce\\xf2\\xbc<\\xdb\\xe4\\x89\\xbb\\x97*7=\\x7f\\xf5\\x97<.o@\\xbcv\\xc8w\\xbd\\xeb\\x0eC<;\\xa5\\xad<\\xc6\\t\\xc3:;\\x0c\\x95\\xbc\\x9a\\xfdE;\\x9f\\xfbi=;Z\\xb9\\xbc\\xf1W\\x08\\xbdhK\\xcf;X\\x1cj=U\\xab\\xc2;\\x7f\\r\\x1c\\xbd\\x16\\xfd\\xba\\xbb\\xa3p\\x93;o\\xc6\\xbb\\xb9\\xc1\\xfeI\\xbb\\xab\\xcc\\x15\\xbd\\xaf\\xe5<=v\\xa4l=@G\\x1a=\\xeb\\xe3\\x07\\xbbBP.=t \\x98\\xbc3O\\x95\\xbc\\xd3v*<m\\xd7\\x93\\xbc1\\xec\\x82\\xbc\\xc59\\xf5<wo$\\xbdkM\\x01\\xbd\\x9e\\xc1A\\xbb\\xd5\\xc1%=@cS\\xbd\\xc4\\xb1\\x99\\xbdbr\\x8f<5\\xcb\\xa2\\xbd\\xad\\xec><\\x1b\\x00\\xca\\xbb\\xd7\\x19\\xab<\\x04\\x18\\xfe<Hj\\xd4\\xbc\\xf0ON=?c\\x84=\\x07=n\\xbc\\xc5\\xb3L=\\xda\\x11\\xaa\\xbb^Nu<\\\"%\\x1d=\\x9f\\x92\\x8b<m\\xe3\\x18\\xbd\\xd0\\xb6\\xeb\\xbc2\\xb8Y=\\xa1,\\x0f\\xbct\\\"\\x08\\xbb\\x08r\\xfb\\xbb \\xb0`<\\xee\\xfd\\xf9<\\x97\\tB=P\\xe9&\\xbd\\x8fv\\x97\\xbcmz\\x86<9\\x94\\x87:\\xed\\xd7\\xd9;\\xcf\\x1bU<\\xe3\\xf0F\\xbctHH<\\x1fw\\x0e\\xbb\\xb6\\x9e\\xdf\\xbdo\\x15\\xca;\\xc8\\xf8\\xda\\xbc \\x10\\xb4\\xbcB\\xb7/=\\x96\\x80\\x14\\xbd\\xa5\\x8bH;.\\xf9\\x83=\\x1f\\x9f*=\\xfaKZ\\tq1D\\xbc\\xa7^\\x07\\xbd\\xb5Y\\xe4<\\xe5\\xe2\\x13=\\xca\\xf2W<o{V=\\x94\\xaa\\xfd\\xbc\\xc5\\\"\\x18\\xbd ,$9\\x98\\xf3\\xd6\\xbcb\\x96\\xb5<\\xad\\t\\x03=\\xafX\\xfa;\\xbbub=\\\\\\xecF\\xbc\\xc9\\xf6\\xca<\\xdc\\xd1\\x11\\xbd\\xec\\xd3\\x91\\xbb\\xd5\\\\O\\xbcq\\xe6\\xd1<\\rc\\xa38\\xda\\x0b\\x03\\xbdB*\\x85<`\\xca\\xd5\\xbc\\n\\x04I<\\xbb|*\\xbc\\xd7\\x17\\xb3<dUu<~\\x83\\xbf\\xbc\\x08\\x9c-<\\xa6\\x8es\\xbb\\xfb\\x06\\x1f\\xbd\\xdf3\\xf0\\xbc\\xb2\\xd7\\x87\\xbd\\xfc\\x92\\x8c\\xbdH\\x9f\\x85\\xb9\\x1c\\xb2\\x95<G\\x80\\xd9<\\x97U&=\\xca\\xcb0=L+!\\xbc\\xd7=\\xbf<\\xe5\\x8d\\x01<\\x94\\r\\x19=\\x9e[@=\\xdcq\\xc5<I\\x13\\x94=[\\xdb\\x10\\xbc\\xe2\\xf6\\x01\\xbc.\\xf6\\xca\\xbc\\xa3\\xb0\\xd0\\xbc)S\\xbc\\xbc$rM<\\xa5u\\x96<\\x9f\\xb3w=\\xac\\xc8\\x05=W\\xb5\\x94<\\xb2\\xae\\xfd;\\xf0\\x8c\\xb8<\\x7f\\x9e\\t=@c\\xa7<\\x97R#<\\x8adL\\xbb\\xaf\\x00&=\\xd8i\\x83\\xbd\\xed\\x19^\\xbdn+\\x0b\\xbd0!\\x00:\\xb4\\x08>=\\xda\\x06\\xf8;\\x0f\\xbcR<n\\xe39;\\xb1^o<Dj\\x91<\\xa4\\x0e>=\\x07Q9;\\x91l\\x1b\\xbc}\\x19\\x97=+h\\x89\\xbd\\xfa\\xa7\\x9b=\\xef\\xb3\\xd1\\xba\\x9eiZ\\xbb19\\xa0\\xbc\\xf2\\xd8\\xea<\\xf0\\xc8+\\xbd\\x11\\xd7\\x16=o\\xdcj=\\x175\\x90=\\xdb\\x0b\\xdd\\xbc\\t\\x951<6\\xad\\xda\\xbc\\xa8,\\xd5<\\xa6h\\x1d=@\\xfa\\xac:\\xd1\\xbd\\x1c=\\t\\xfd\\xe4<I\\\\,\\xbd\\xa9\\x1bM\\xbb\\xc4\\xb7\\xd4;\\xd7,\\x83=\\x02\\x08\\xa9\\xbc\\x0b\\xd6\\t\\xbd\\xfd;\\x11<\\xb1\\x19\\x97\\xbc\\x03\\xcf\\xfc<U\\x8a\\x82\\xbcKZ\\x06=I\\x06\\x1a\\xbcM\\xee\\x1c\\xbd\\xce\\xb0r<\\xde\\x15\\x13=\\xb9\\xc8\\xdc\\xbc\\xaa*\\xc8\\xbc3\\x01\\x86=\\xf2\\xa0\\xaa<\\xd9\\xf4R<\\x97\\x03 =\\n\\xa6\\xe9:G\\x9f\\xbb\\xbc\\xcd\\x8eC\\xbd\\xb9\\xeb\\xac<\\xc9Q%\\xbc\\xf8\\xfe\\x16=\\tv5;\\x8a,\\x05<m\\x88\\x8e\\xbc*\\x17\\x01\\xbd\\x1d\\xea\\xcb<\\x15*\\x8c=-\\x91\\x9b\\xbd\\x07\\xab\\x88\\xbc\\x84\\x0e!\\xbdA\\xe2\\x9d=\\xa8\\x98X\\xbc\\xf8o\\xd2=\\xac\\x8bv\\xbb\\xa1\\xedV=\\xca;1\\xbdt-\\x18<\\xd05\\\"<\\x9e\\xf1b<VU\\x8f\\xbd\\xa7\\xf4\\x13\\xbd\\xbe\\xfb\\xab\\xbc\\xf7\\x12\\x89<S\\xe1\\x1b\\xbc\\x87\\x0e\\x19\\xbd\\x8fW\\x9a\\xbb3f\\xe1<{\\xbf\\t\\xbd\\\"$\\x04\\xbd\\xa6J0;\\xba\\x14Z\\xbb7\\x8f\\t=(\\xd0!=\\xc3\\xd1\\xb7<G,v\\xbd\\xddjx=\\xeb\\t\\x0c=_\\x86/\\xbc!dp\\xbd\\xcck\\xae=\\xeeh\\xe7;\\x7f)\\xf9:\\xd7\\x82\\xcf\\xba\\xfa\\xb2g:\\xcb\\x84\\x18\\xbd@A\\x15<\\x0c\\xba\\xb0=\\x93\\xe7\\x9c<h\\x8d\\xaa<\\x18\\x1d\\x93\\xbcc\\xe4\\xae<E\\x87>\\xbc\\xcd\\xdd!\\xbc]Wn=4n\\x96\\xbcHl\\xb2\\xbc\\xcf{\\x01<64\\xd4\\xbc\\xa2U\\xd0\\xbc\\x99\\x1c\\x92\\xbd\\xe4\\xceF=\\xe6>\\xf39K\\xf0\\x85\\xbc_\\xbae\\xbc\\xe7\\x8b\\x8e<9\\x8c\\x9a\\xbb:\\xe41=(_\\x0b\\xbdNag<\\x18,\\xdf<&J\\xcb\\xbcY00\\xbc\\x9f\\x99Z\\xbdW\\xc9\\xa0\\xbc\\x89i~\\xbc\\x9c3\\x03\\xbc\\x94\\x05\\xe8\\xbc\\xb7H\\xef;\\xad\\xfa\\xfd\\xbc\\xe7HU;l\\x93\\x93\\xbc\\xe2B\\x95\\xbd&5t\\xbd\\x1d\\xa7f\\xbd=\\x9a\\x8f<z\\\"\\xfd\\xbc03a\\xbcHk\\x07\\xbd\\x7f7\\xe5;2w\\x96\\xbc\\xbdp\\x05=\\x11r>\\xbb\\\"\\xd5E\\xbd\\xa2\\xd5\\x80\\xbc]\\xb2\\x14=\\x03;\\xd8\\xbcr\\r\\x8b\\xbdiwy\\xbd_9s<\\xc7<\\xdc\\xbc*\\xc4%=V\\x88l=\\x9e\\\"\\xac=N\\xf90\\xbcd\\xee\\x00=\\x01\\xab\\x8b\\xbc\\x06\\xc0\\x07\\xbd\\x87\\x8e\\x16=>\\xce\\xde;\\x1c\\t\\xf0<\\xfe\\xc0#\\xbc1\\xacM<\\x15\\x01i\\xbc\\xab\\x1b&\\xbc\\xa3\\\\\\x1b\\xbaT\\xd1z\\xbc\\xb5\\xe6\\x07=<0?=G\\x91\\x93<\\xf7z\\xaa<\\x9ch\\x0e=\\xfc}\\xd6;\\xbe,\\x9b\\xbc:\\xa7\\x17<\\xd8&t\\xbc\\xfanm;\\xd4\\x884\\xbd\\xc8\\xd1\\xbe<\\x1eI\\xc9\\xbc\\x94d\\x0c\\xbd;)\\xc0\\xbb\\x99\\xdci=\\xd1-4\\xbdij\\xba:uf@\\xbd)G\\x8b\\xbcNU\\\"\\xbd+\\xab\\x82\\xbdv\\x0e$\\xbc\\x18V8<\\x95\\xe4\\xc0;\\x8f\\x9ao=R\\x17l=k\\x03\\x19\\xbc\\x95\\xdb\\xa8<%\\xd2\\x11=b@I\\xbd\\xee\\x88\\x89=\\rE\\x01\\xbd0\\xe8\\x9e<\\xab!\\xb1<\"\nHSET bikes:10092  model 'Polydeuces' brand 'Peaknetic' price 3725 type 'Kids bikes' material 'alloy' weight 15.9 description 'For shy or agressive riders, paved or dirt trails, this bike boasts kid-friendly geometry and strong quality parts at a minimal price point. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"\\xc8*\\xe6;\\x0f#\\xf9\\xbc/\\x8b\\xd7\\xbc\\xd1$\\x0b<\\x81`\\x90\\xbd\\xf8\\xf1\\xda\\xbb]~\\x06\\xbd1\\x04(\\xbdvk\\xc2=\\xa7\\x1d\\x99<\\xa4\\x9f\\xa9:\\x86B\\x1b\\xbc\\x11Q\\t=o\\x99\\x02=\\xb0T6=$\\xee\\x90\\xbcA\\x05)=Ovq\\xbd(\\xf8C\\xbd@^h\\xbd\\xa8\\xfb\\xa3<\\xf2\\xb7\\x9f\\xbb\\xd6\\xcb\\xb4;t\\xcb:\\xbcZ*.\\xbd\\xef|y;\\xe3\\xee\\xba<\\x90\\xd6~\\xbc\\xab\\x88\\xa9\\xbc\\xf6\\x10f==\\xac\\x17\\xbc\\x80\\xc6\\x02\\xbd,\\x14-=tH\\x84=\\x08i.\\xbd\\xed\\x85F\\xbc2\\xf5/=\\x90/\\xe0\\xbb\\x83\\x04v\\xbc\\\"\\xf1b;\\x94\\n\\x16<\\xe8\\xcb\\xb1\\xbb>\\xeav\\xbcx\\xacS<\\x8a\\xd8\\xca\\xb9*\\xfe$\\xbd]\\xa0R=\\xd6$\\xff\\xbaa\\x8d\\xf6\\xbc\\x1fuM<\\xea\\xe5b\\xbcK\\xc1\\xb9\\xbcK\\x90\\x0c\\xbd\\xab$\\x83\\xbc\\x89g\\x1b\\xbc\\x8d\\xd4\\xc6;#\\x8f\\xde;\\x03Z\\xd3\\xbc\\xa0\\xa5\\x88\\xbc0\\xa1\\xd8=K\\xa0\\x8a\\xbciqP=\\x1f\\xd7,\\xbct$D=\\x06]\\xd1<\\xc2\\x06\\xf1\\xbco4\\xb8<lw\\x17\\xbc\\xa8H\\x1a=\\x14<\\x10\\xbd\\x85\\xbeP\\xbc\\xe1\\x0f\\xf6\\xbb\\xe2u\\x10\\xbc\\xd3\\xac\\x9b\\xbdX\\x11\\xc8\\xba\\xc1V\\xd2\\xbb#N\\r\\xbc\\x1c/8\\xbd\\xb9/\\x95\\xbd\\xfe3y=\\xdf\\x0b)=\\x10\\x89r\\xbc^\\xd7\\x11\\xbd)\\xd0\\x0e\\xbd\\xe4\\xd7A<\\xed$\\xa2\\xbbu\\x92\\x16\\xbc\\xa0\\n\\xbf<\\xd7vv\\xbcx[\\x1d<\\xa6\\xdd]<\\xce\\tY\\xb8\\xe5\\xbaT<2!\\x81\\xbc\\xaaF1=\\xf5$\\x06=\\x1aIc\\xbd\\xc5\\xc2\\xca\\xbcK\\xfb\\xeb<\\xde\\xc8P\\xbd5}\\x8b\\xbd\\xef\\xba\\xe6;\\xc5mR\\xbc%;\\xfa\\xbb{\\xd1\\xe1<@\\xbf\\x0f>\\xb5\\\\j\\xbc\\xd1\\x8d\\x00=/\\xb1l\\xbc\\xca\\xeb7\\xbd\\xe3<\\xa3:\\xc2\\xac\\r=\\x0cz\\xb3;{\\xeb\\xa7\\xbc\\xac\\x07\\x8d\\xbd\\xb4\\xe6\\x19\\xbds\\xc5\\x9f\\xbbJ|\\xbe;\\x919\\xe4<\\xd4L\\x81<\\x9e\\xd9C=\\xaat\\xfa;I\\xd7\\x81\\xbd9\\x92&=Cp\\x01=P\\xa9\\xc9\\xbc\\xea\\xd20\\xbc7\\xa9\\x05\\xbdS\\x8a\\xc6\\xbc\\xaa\\x91\\xe2\\xbc-\\x8b\\t\\xbdnIV\\xbd\\xa5\\x16\\x01=\\xaa\\xe9\\xd9\\xbc\\xeag\\xb1\\xba\\xfaeL=r\\xcc\\xcc\\xbc\\x9f8r\\xbc\\xc7\\xf6\\x12=\\xd5\\x85\\x96\\xbc\\x0b\\x88V\\xbd_&\\xad\\xbd}3\\r\\xbc\\xb2\\x981==\\xea\\x95\\xbc\\x8a\\xf2\\xa7<\\x8e8\\x98\\xbc\\x93\\xa6\\xea<\\x16\\xf2\\x07<\\x1cP\\xe4\\xbc9i\\x1e\\xbc7g\\xf9<\\x8a\\xaf\\x8f\\xbb\\xa9\\xd2\\xf1;;\\x8d\\x12\\xbd\\xd1\\x95\\x9d\\xbb\\x12\\xc8\\xf0;\\x7fE\\x06\\xbc\\xadaj=\\xbb\\x18e\\xbdY\\xcc\\x81=\\x1a\\\\\\x08\\xbd{\\xda\\xbb;\\\\/S\\xbd\\x16\\x19\\xee<\\xfb\\x854<\\xc2h\\xc2\\xbc\\xc3\\xf7\\xc5<\\x86\\x1e\\xe1<=\\x8a\\xdc\\xbc\\xe2l\\x8e\\xbcX\\x8d\\x90=\\xa5z\\xc3\\xbc\\x9aM>\\xbd_>\\xa5\\xba\\xeb`\\x84<YV2\\xbdJ\\x1b7=g\\xb6T\\xbao\\xfdS\\xbb\\x0c\\\"\\x0b\\xbd\\x0f\\xba\\xc1\\xbbmZ[<\\xa4cP=)%!;lC\\x03\\xbd#\\xe9\\xae\\xba<\\x98\\x85;Ee\\x00=L\\xb7\\x9f\\xbcabE<\\x85\\xd3\\x11==\\xfc\\x9a\\xbd\\xa0}\\x84=\\x86\\x85\\xa7<\\x02\\xf8A=O\\xdf,<:\\xcf\\x17<\\x8f\\xe8\\x01<yw\\xb9<\\\"\\x1e\\x95\\xbbeX|\\xbd\\xe8 \\xa0\\xbdi\\xe0\\x8c\\xbcVv.<\\n\\xfdN=\\x84\\r\\x19\\xbd6\\x0f\\xbe\\xbc\\xd5M\\x19=\\\"\\xb4\\x05\\xbc>B{\\xbc`\\x1c\\\"=3\\xaeU=\\xd8\\xcb\\xf5;-&\\x02=\\x9a\\x80\\r<\\nG\\x9c\\xbd\\xe6;\\xa1\\xba\\x1a\\\"\\xb0\\xbb`\\xee}\\xbd~q`\\xbb\\xf2\\x96>\\xbd\\xa6\\x85\\x16\\xbd;=I=\\x03.m\\xbc4b\\xca<W\\xb5\\x8b=Z{\\\";tOM=\\xa3\\x10\\x15\\xbc\\xa4\\x02\\x0e\\xbd\\xc9\\xf0\\x0c<\\xa7\\xa86=d\\xda\\x90<\\x97\\xc1\\xa0;m\\x82E\\xbd\\xb4@\\x86\\xbc\\xed\\xa2i=\\xc3 \\xa6\\xbbN\\xe0\\xd9\\xbb\\xee\\x88\\xae<\\xdd\\xa6\\x97<\\x0b\\xb3\\x10=\\xbaW\\xd1<N\\x112=\\xbe\\x95\\x1e=+\\n\\xa0<\\xac7\\x06=\\x86H\\xe5\\xbc-\\xb7\\n\\xbc03)<e\\xa0n\\xbd\\xf2N\\xf9\\xba\\xefn\\xca<\\xdct\\xef\\xbc\\xe9f\\xb6=Q\\x04^\\xbd6\\xf8\\xb6<\\\"X\\x89\\xbb1\\x93U=\\xef\\xcf\\xd7\\xbc\\xbf=\\xe1\\xbcJ\\xa7\\x1f\\xbdp\\xab\\xf6\\xbb\\xa9\\x87*\\xbd\\xc5\\xe8\\x00=\\xef\\xaa\\xc9;\\x17\\x17u\\xbcVd\\xcf<\\xf9\\x1e\\xc9\\xbc\\xe0\\xa66\\xbc\\xf5\\xc8\\x1b\\xbd(M+\\xbd\\x0791\\xbc\\x19#\\r\\xbd\\xc9\\xeb\\x04\\xbc\\xd3$p=a9\\xf9\\xbb\\xd8\\x04\\xdc=\\xa5\\xe64\\xbdz\\\":<X\\\"l\\xbcc\\x8cV=\\x90P+\\xbczs\\x87<+*\\x90\\xbco\\x9c\\x1b\\xbd\\xda\\x8aM=HF:\\xbd\\xb5\\xbe\\xd2:\\x94\\xf4=<\\xf1vZ\\xbc\\\"\\xedd=b\\x8e\\x86\\xbc#\\x82\\xb4<- \\xeb\\xbcz\\\\\\x92;00\\x93;YE\\xd2:\\x9a\\xa5>\\xbc\\xd0`\\\\=\\xa1\\x9a\\\"\\xbb\\xca@\\xff<\\xbf\\x0c\\xaa;\\xb3\\x9c\\x08=\\xa1\\rk\\xbc\\x17\\x93\\xa0\\xbc\\xfak\\xa3\\xbbKg\\x10<\\x1d\\xb7\\x8f<\\x02\\xd3C\\xbd{\\x91\\x0c\\xbd\\\"\\xf8\\x0f\\xbc\\x93h\\xb8\\xbc\\t8\\xca:\\x7f6\\xb9\\xbd+G\\n\\xbd\\xee|\\xa8=*V<<K\\x06\\xd1\\xbc\\xe0f\\x14\\xbdF\\xbd\\xac\\xbc\\x84\\x94\\xe7<.|\\xa2;$\\xec8=\\xfc6\\xf8\\xbc3\\x97\\x13=\\xf1\\xb9\\x1c\\xbd0\\xf4\\x91=M\\x10\\x0b<~?\\x16<e\\x9eN\\xbd\\x0b\\x8a,\\xba\\xfc\\xcb*\\xbd\\xcd\\xbb\\x15=\\x80\\xc6\\xbb\\xbcX\\x07\\x96\\xbcB\\x0b\\xeb;\\xa3R3=\\xce\\x92\\x82<}\\xb3\\x83\\xbd\\xdb\\xbe|<\\x15\\xcf\\xd5=\\x90\\x99\\x12\\xbb~\\x99\\x84\\xbc\\xf3\\x1b!=\\xb1\\xcb\\x05=\\x88X`<\\x87\\x9fY=xY\\xd7;\\xb6\\xa1\\xca<\\xafUR;\\xba\\xff\\x9a=\\x0f\\t\\x91\\xbc\\xa6;3\\xbd\\xcfr\\x07;Mm\\xdd\\xbc4Hn\\xbd\\xa5\\x87D\\xbc\\x07o\\xde\\xbd\\xb3\\x88\\xa6<\\xb7d\\x13=\\x8e]L=\\x90Y\\x10=\\x9d\\xfa\\xa5\\xba\\x90\\xd3\\x14\\xbd\\x8f\\xd0\\n\\xbd\\xb2\\xd9\\xf4<\\x04yv\\xbd\\x94\\xc0i\\xbcX\\x808=C\\x17D<\\xfc\\xc9\\xb7\\xba\\xb8d\\x97<\\xf7\\x0e\\xa6<\\x17\\\"\\xd3<,\\x83G\\xbc\\x9fC\\xc2\\xbcCq,=\\xddS\\xc9\\xbc\\xff\\x8a\\xcd\\xbc\\xcc\\xc6f\\xbdc\\x1f\\xc3\\xba\\xbc\\x87\\x8a;\\xe8\\xcfj\\xbdXGY=\\xd4\\x8c\\x87\\xbcj\\x99d=z\\x99\\xf4<]\\x0c\\xf9\\xba\\xfb\\\\\\\\\\xbc\\x18\\xf6\\x82=\\x13\\t\\xf2=\\xc1J$\\xbd%\\x95\\x8a<\\x9e.\\xad\\xbc+o0\\xbd\\xa3\\xd52<iX\\xcb=\\xe8\\xb2\\xaa\\xbd\\x95\\r\\xa1\\xbch\\x0b\\xda\\xbc\\xa0\\xf8u=w\\x11\\x1d;\\xda2A=)\\x99!\\xbdqXC\\xbc\\xd3}\\x9e:Zw\\xea\\xbceZF;;1F=\\xb4\\xfd\\xac<vk\\r=\\xd0t\\xa1\\xbb/\\xdb\\xf5;\\x1d\\xe8\\xf6\\xbaG;\\x95<\\x1a#p\\xbd#l\\xfa<^\\xee\\xa9\\xbb\\xe6\\xc7\\xdc\\xbc9\\x80\\xc6\\xbc\\xedUO=\\x8f\\xec,=(\\xaa\\xd7<\\xd61~\\xbdT\\xe09\\xbd\\xf8\\x1d\\x9a<V\\xcc\\x97:7!9\\xbd\\x02\\xd6\\xb7<X\\xb6\\xc2\\xbbg\\xbep\\xbd_\\x90\\xa2\\xbdT\\x83\\x02\\xbdN\\xc6\\xaa=\\xdb\\xb6d=X\\xda#\\xbb\\xf6\\xafD\\xbdP\\x1a9<\\x06>&\\xbd\\xa01\\xeb\\xbc\\xf9(\\x16<~c\\x07\\xbc\\x1e\\xd1\\r<\\x9bI\\xe3;w\\xff\\x14=X\\xdf\\x14\\xbbH#\\x8a\\xbd\\xee\\xba>=\\x9b\\x0fd\\xbd\\x13jW\\xbd\\xafQ\\xaf\\xbc0\\xf9\\xe3\\xbd\\x999,=\\x7f(\\xd0\\xbb\\x1fG\\x17\\xbc\\xb9\\xdf\\x88;\\x86=\\x85\\xba\\xda\\xfa\\xb5<\\xcd\\x1a!=.\\t>=\\xc0=\\x00\\xbb!!\\x82\\xbd.\\xab\\x06\\xbcz;\\xf1<_L\\xa0:\\n\\xe3K\\xbd.\\x9cZ\\xbb\\xbdVw=\\xcf,%\\xbd\\xd2&\\xc4<\\xe8\\x0c.\\xbd\\x8d;\\xf0\\xbc\\xa2\\xc0P<\\x9dr\\x1d\\xbc\\xfa\\x027\\xbd\\xd2\\x86\\x0c\\xbd\\xea\\x1c\\x89<\\xc6\\xfag\\xbc\\xfe\\x9e&<\\x03\\x07\\xe7\\xbb?d\\x01;\\xb23\\x88;e\\x01\\\\\\xbc\\x86\\xb2z\\xbdWln\\xbcA\\xe4!=\\xf2\\x8f\\xfb\\xbbu\\x8e\\n=A<\\xcb\\xbd\\x97\\xbb\\x05=l3\\x81<\\xde\\xb4\\x8f\\xbc\\xf5Rz\\t\\xbd\\xd4\\x9f\\xbc \\xfe\\xc5\\xbc\\xac^\\x18=\\x88\\xa5\\xc6=\\xbb\\xfa+=\\xeae =7`\\xb8:)\\xad\\x98\\xbc\\x82\\xf3);xIQ\\xbcqi\\xdf\\xb9\\xbd\\\\9<\\x9c)\\x1e<\\xff\\xb3\\x85\\xbbz\\xc3\\x0b\\xbd\\\"\\x92\\xa8\\xbbyq\\xa5<]b\\x04\\xbd\\xb4U\\x04<[\\xaa\\x8b=\\x96K\\x8c<\\xcaY =\\xd1\\xd1\\xbc<\\xed\\xc8\\x9b\\xbc\\x9c\\x85\\xba<\\xf1M\\x0e<\\xdd\\xe0\\x04=Q[-<\\xa4\\xb2\\xc9\\xbc\\xfeP\\xbc\\xbc\\x87\\xc4+=\\xcc.\\x14\\xbb\\xb1=\\x13\\xbd\\xc3\\xceL\\xbd~\\x8d\\x1d\\xbb\\xa7\\xbdS\\xbd8\\x08\\xf0;\\x92\\xb6,=\\x99\\x15\\xb8\\xba\\xe6\\xa3\\xca\\xbb\\xf8\\xa1\\x18\\xbc\\x83\\x94\\xe4\\xbc/\\x19\\x81<\\xc3|\\xa9<\\xdd\\xb2\\x1a=MX4\\xbd\\xa0\\xcb\\xa5=\\xddE\\\"\\xbd\\xae\\xc8\\x12\\xbd\\x18\\xef\\x80<B\\xea/\\xbd\\xab\\xa6\\x95\\xbb\\xd8\\xfa\\xa2\\xbc\\xc7\\x89|<\\x93\\xe7\\x9e\\xbd\\xa6\\xd5\\xcb<\\x15\\x16\\xc0\\xbc\\xe1A\\x87\\xbc:3\\x82<:x\\xcd<\\x9c.\\xcd\\xbb9%k\\xbc\\xf8\\xaa(:\\x8eQJ=xKc;\\x1a\\xd7\\xdd\\xbc\\x01\\x9a\\x87<\\x1c\\xaf\\xd7\\xbbm\\x00s<\\xdd\\x11/<\\x96\\xe8\\xd6;\\xae\\x88M\\xbc\\x83\\r\\xf4\\xbb,\\xa3\\x9f<,}\\x0f=\\x90\\xe3N\\xbd\\x17E\\xcc\\xbb\\xe2k)=\\x01R\\xdc<\\xb4\\xf2\\x05=p\\x14\\x8c\\xbd@\\xf4C\\xbcF\\xbei\\xbc\\xc6\\xae\\x01=\\xac\\xb8\\x9f\\xbcW\\x8e?\\xbc\\x16\\xf4\\x85=$\\x87\\xd6<\\xfe\\xeb\\x83\\xbc\\x81\\x0b\\x19<\\x0bqk;\\xf5\\x84o\\xbdA/q=\\xf7\\xac\\xf7:<\\xa8\\xfe\\xba\\xe0Tu;+G\\xa2\\xbc\\x8ew\\x02=\\x16\\x8d\\x83;}Bj=\\xea\\x03\\x1d<\\x05c\\r\\xbdp\\xdb\\xce<\\x8e\\xf9\\r<c\\\"\\xc2;rH\\x98\\xbc\\xf54*<\\x80/\\xd8\\xbc\\xb2o\\xf5\\xbc\\xce\\xda\\xaa\\xbcSy\\x1d=\\x8a\\x9b\\xf7\\xbca\\xa3\\x85\\xbc\\xcc\\xef\\xfd<Xb\\xbb=\\x02\\xe88\\xbcf\\xa0\\xa0<\\xf9\\xb6b\\xbc\\x1b\\x0e\\xcb<t\\x04L\\xbc9zD=\\x7f\\x18\\x13<\\xbbI\\x81<\\x16YR=\\x00\\n\\xfe<\\x86A\\xaf<\\xb6\\\"}\\xbd\\x05-\\xcb:(\\xc8\\x13=\\xea\\x91\\xab\\xbc\\xe8l\\xd0\\xbc\\xdc\\xa0\\x9b\\xbd\\x95.(=\\xf4\\x87\\x87\\xbc\\xc56\\xbf=\\x8e\\x08\\x8e\\xbc\\xa6\\xbe\\x89<X\\x08y\\xbd\\xc5\\xcf\\n\\xbc\\xf0\\xa3\\xee:\\xdf\\xbcs<\\x8e\\x14g\\xbb(}\\xd8\\xbch\\xe00\\xbb\\x8a#\\x1e\\xbb \\xde\\xdf<\\\\\\xaa\\xa8=\\xaf\\xcb\\x84\\xbc|\\x98$=%\\x11\\x90\\xbc\\xeexW\\xbdy5\\xea<\\xec\\xfe\\x94\\xbc_\\x87\\xf8<\\xab\\xf10<\\xf8\\xb4\\x07<\\xb5\\xdb\\x16\\xbd_\\xac\\xa3<8\\xed\\xe1\\xbbQNf<\\x92\\xe0\\xbc<h\\x1f\\xaa={7\\xb5;\\xd6\\xc6\\xb1\\xbb\\x01\\x96\\x90\\xbc\\xf9\\xd4d\\xbc\\x9c\\x12\\x82\\xbb\\x86\\x0b\\x13=\\xadqp=u\\xf7\\xc3<B\\xb0S\\xbc:j\\x03\\xbdL\\x10\\xed<\\xfe\\x04C<\\x91\\x91\\x0b=\\xec|\\\"=\\x1eS\\xf6;\\x9a,<=\\x1e\\n\\xf5<\\xa9w.\\xbd\\xf7\\xac\\xfa\\xbc\\x81\\\"\\x99\\xbd{\\xd3\\xe0;\\x01\\xaf\\xba<\\xd5e\\xd5\\xbca\\xb4\\r\\xbc\\xb19@\\xbb)Z)<7\\xb2\\x14:\\xda9\\xd2<K\\x80\\x9c\\xbcB\\x8f\\x8a<hVs\\xbd\\xbbQ\\xd4\\xbc\\x87\\xd2\\x8b\\xbcF\\x90\\x95\\xbd\\x90\\xea\\x12\\xbd\\x9c\\x89\\x84\\xbc\\xa8+\\x8d\\xbc\\xf6H\\x00\\xbd~\\xd4C\\xbd\\xea#\\xc2\\xba9Qm<6Z\\xa2\\xbd\\xa1\\xc7\\x10=\\xf0SQ\\xbd\\x02z&;\\xe1\\x010\\xbd\\xc6\\x9f\\x85\\xbc\\xf5\\x039\\xbc\\x96\\xe0\\xae<\\xa8\\xd6\\xfa\\xbb\\x8d\\xe9\\xcd<\\x93\\xc0{;\\x94\\x94\\x02\\xbc\\x1b\\xf7\\xeb;$:\\xd7\\xbb\\xecG`\\xbc\\xb1\\xc3r\\xbd\\x08\\xf3\\xcf\\xbd\\x99\\xdd\\x97\\xbc3\\xadH\\xbc\\xc8T\\xbe;*A#=ymP=\\xd2\\xf9R\\xbdc\\x9a\\x03=;\\xffT\\xbd)\\xe6\\xa4\\xbd\\x08\\xdc4=\\xa0\\x8d+<\\xc66\\x1b\\xbbM\\xd2\\xcc\\xbc\\xbe\\x0cJ\\xbc\\xcb\\xa8\\xb2<\\xfd\\xb8\\xb9\\xbc;\\x0b\\x1e\\xbbR\\xa6\\x95;\\\"\\xe0\\x85<\\xd9\\xa1\\xc1<\\xae\\x1fI<H\\\"a=\\x12\\x18\\xbf\\xb8\\xc0\\x9cq=:h\\x02\\xbcO\\xc6\\x9d\\xbc%\\x85W\\xbdi\\xbb\\x14=\\x08\\x04\\x80\\xbd\\xa7\\xa9\\xe1;\\xf5\\x01\\xf7<m?\\xf7\\xbb,\\xc5*\\xbc\\xb0\\xe1\\xa2=I\\xcb:\\xbdl6#\\xbc\\x99\\x91\\\"\\xbb\\x97\\x16\\xd3;#\\xe9Q\\xbbr\\x1cd\\xbd?\\x85%=9M\\xeb\\xbcx\\x90\\xea\\xbc\\x1e\\xa2{=\\x0f\\xb1\\xbc=\\xf9\\xba\\x8b;\\xab\\xdf\\x9d<\\xed\\xc0y=\\x7f\\xb6\\xdd<v\\xa9\\x8a=?\\xa5\\xc9\\xbd\\xf9KL<\\xfa\\x85\\xed\\xbc\"\nHSET bikes:10093  model 'Quaoar' brand 'Classic wheels' price 1833 type 'Kids mountain bikes' material 'full-carbon' weight 14.2 description 'Small and powerful, this bike is the best ride for the smallest of tikes. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it\\'s an impressive package for the price, making it very competitive.' description_embeddings \"\\x8a:\\xcd<\\x00\\xf2\\x15=\\x05r\\xa4\\xbcL\\xa8H=\\xb6\\x85\\xd9<w\\xf3\\xf2<Qq\\x15\\xbc\\xf1l{\\xbd\\xb7\\xdc|=\\xed\\xbd\\xd1;A\\x16\\xf6\\xbcjV\\\"=\\x9azY=<\\xea\\n;7\\xb7\\x99=\\xddm\\x15\\xbcG3(\\xbc\\x14i[<I\\x17-\\xbd\\x06\\xef\\xeb\\xbci:\\xd8<\\x01Wd\\xbcH\\x8e*=1\\xe9\\x0b\\xbd\\xfc\\x83\\x9a:y5=<`\\x86\\xe9<\\x99\\xc5\\xc3;\\xa8\\x02\\x14\\xbcE\\xa2\\\"=\\\"A\\xa8\\xbb\\x02 \\xc19w\\x89\\x15=\\x08=\\x0c=\\x8c\\x0fm=\\xd8\\x876<~\\xdb\\xb0<l\\xdf&\\xbd@\\xf0\\xf8\\xbaW\\xeb\\xb5\\xbb\\t&\\x19=f$&=\\x99\\xb0K=&\\xefW<\\x1d~\\xd5;\\xc5\\xa5\\r\\xbd\\x80\\xce\\xde=\\xec\\x95\\xa7\\xbc\\xda\\x07!\\xbd\\xda\\xa8\\x92\\xbc9U;\\xbdVB^\\xbd\\x06P\\x14<hC\\x9c<aG\\x85<\\xd7}\\x1c<\\x0c\\xef\\x1b\\xbb\\x15ov\\xbd\\xdc\\\"B\\xbd\\x1b\\x9cd=,\\x897=X\\xad\\xc5<\\xd4\\xf3Q<|\\xdc@=O(8=\\x95\\xd5\\xa0\\xbaP\\xd7\\xde\\xbc\\xb9\\x18Z=\\xb7`0;\\xcb\\x82\\xfd;\\x12\\x81/\\xbci\\xc6\\x1f<T\\r0\\xbd|\\x87/\\xbb\\x13`\\xba\\xbc\\xc9\\x821;\\xb0PP\\xbb\\x87b\\xbd\\xbc?-U\\xbd\\x9f\\xe2I\\xbc\\xbcH\\x0e=<\\x96\\xc7\\xbc2\\xb9\\xac\\xbc\\xf2bY\\xbb\\x94 \\\"\\xbc\\x10~5\\xbdN\\x03*<v\\xe3U=\\x93V\\x00=\\xa5\\x8be<4\\xa7<<\\xde\\x1a\\\"\\xbd\\x9b\\xb7\\r\\xbd\\xf1\\x1c\\x05\\xbd~P\\xc3<\\xf3\\x11?<\\xf3Dr<\\xff\\xe4S\\xbd\\x1c\\xef\\x82<3\\x80R\\xbd)i\\xbc<\\x91E\\x92<\\x1aA1<N\\tX\\xbd\\xc9\\xfdK<\\x8a\\xd3\\x80=b\\x96\\x04\\xbd1\\xe2\\xb8<\\xae\\xbf\\xb8\\xbb\\xe0\\xc4\\xd4<J\\xe0X<v\\x0cZ=\\xdbe\\xa3<\\xc5-T\\xbd\\xba\\x99M\\xbd\\xbc.\\x00\\xbd\\xb6ju\\xbb\\xbb\\x1c\\x8b\\xbc\\xfas?=\\xf2O6:=g}<!\\x0f\\xea<\\xe3\\xa0\\x10<\\x99\\x11\\xe4:f\\x91\\xdb<\\xfb\\x1d\\x88\\xbcO\\x83\\xb7<\\xa9?\\x80\\xbd\\x9d9\\x15\\xbd\\x7f\\xa5d\\xbd\\x0c4.\\xba9\\x8c!\\xbd\\xc5\\xa2`<\\xc8\\x9f\\xa0\\xbc\\x0b\\x1f\\xfe\\xbc\\x802\\x94<\\x85N\\xdf\\xbbz\\xaf\\xd0;DR\\xc9;-\\xb2@\\xbd\\x02\\x1b\\x0e\\xbc\\xafv@\\xbd|:\\xd0<\\xfa8\\x9a=\\x93\\xf6\\x07<\\x8b\\x0c\\x8c\\xba\\xb6K\\n;ik\\x0f\\xbd\\x90\\xc1\\x07\\xbcVf\\x1c<\\x99\\xaa\\xa1\\xbc\\x1e\\xa3\\x81=\\xabj<\\xbdn-A==\\xff\\x84\\xbd\\xc3u \\xbd\\xd7\\xec\\x06\\xbc\\xdb\\xd9\\x04<_ _\\xbc(\\xb2\\x80\\xbdpK\\x91=\\xc5\\x8c\\t\\xbd\\x01@\\x97\\xbc\\x86YR\\xbd\\t\\xdd\\x11\\xbc\\x85\\x8f\\x89\\xba\\x941\\xc3\\xbc\\x14^\\x91<,\\xf0{=~r$\\xbcs>\\x85\\xbd\\\"\\xe6e<\\xf2P\\x9b<\\x0e\\xae\\xfc\\xbc\\xd3F\\x80\\xbc\\xf3i\\x18\\xbd\\xfd-n\\xbd\\x0f!o=s\\xf1\\x9d\\xba7\\xcc\\xd3<\\x01M\\x92\\xbc5\\xf5\\x06<|\\x95\\x98<\\xd7\\x84\\x05=\\x10\\xcd\\xed<\\x17\\xcfi<m;k=R\\x17\\x85;\\x0e\\xb3};\\x11\\xe3\\xb4\\xbc\\xde\\xc3\\x97<\\xda\\x81\\xcd<G\\x96\\xfb\\xbc\\xf9\\x0c;=\\xdbyf<\\xafl\\x82=\\x80X\\x83\\xbd\\xf0\\xdeV\\xbdk\\xc6\\xaa\\xbd,:\\x83=\\x1a!\\xcf\\xbc\\xfa\\xc7\\xb1\\xbd\\x18\\xba\\x8b\\xbdE\\xd9N\\xbc\\xd2\\xea\\xc0\\xbb\\xcd_[<\\x0cl\\xfa:I\\x8a\\xf0\\xbc\\x1dq\\x84<\\x1f+\\x96\\xbc\\x7f\\x966\\xbc\\x87_\\xf1;\\x11\\xa1\\xf3<\\xf0s\\xf2\\xbc\\x83\\xc4\\xb1:w\\xe0]<\\x85`\\x83;\\xd7\\x9d\\xae:\\xf0\\x1f\\xe4<yq\\x87\\xbd\\x8f\\xbc<;\\xd8n:\\xbc\\x174&<\\xbd\\xe7l<\\xf6;\\xe1\\xbca\\xc2>\\xbd\\xd1\\xf2\\xd7<\\\"\\xec\\xab\\xbc\\xba.\\xed<\\xbcch\\xbd\\xd8\\x8c\\x05\\xbca\\xef\\x83\\xbd\\x91\\xd6\\xbb<W\\x80\\xd8<\\xa4\\xee\\xc8\\xbb\\x1fF\\xd1\\xbc\\x8d\\xa1\\x8b<D\\x18\\x96<K\\x88\\x92\\xbc\\xb3s\\xf3<a\\x9em\\xbdd\\x96\\x94\\xbc\\xd6\\xc1a=\\n\\x81<;h\\xb0\\x05=\\xd6:x=\\xa7Hz<\\x91\\xb0\\t=\\x9f\\xcd\\x13\\xbd\\xe4n\\x88=k\\x1b\\xcd;\\xd4\\xdd\\x18\\xbd\\xeeP\\x16\\xbd\\r\\xb2\\xae=\\x80T\\x81\\xbd\\xab-\\x9f\\xbc_\\x14\\xbc\\xbc\\x1d\\xea\\x08\\xbdX\\xbb+\\xbd\\xa9T\\xb5=\\x07\\xa2U:p\\xe3\\x18\\xbdUH\\x05\\xbd\\xd6\\xe1\\xf0\\xbc\\x82\\xe5\\\"\\xbc\\xf2`\\xd8=\\xd1\\xf2\\xd6\\xbb\\xd3\\xd7B\\xbdM\\xa7X=\\xe01\\x91\\xbc\\x81\\xc4\\xb2<\\xd7{\\x00< T!\\xbc\\xe8\\x8dA:/\\xfa+\\xbdM7\\x989\\xdc\\x04\\xad:9T,\\xbc\\xf1\\xcd==\\xaa\\xd2a\\xbd\\x92\\xf6\\n\\xbd\\xb9\\xb2<=\\x99\\xee\\xc5<\\x18\\xc0\\\"<\\xb0\\xc1\\x1f\\xbcY\\x0c\\xd2<\\xc4\\x04\\xb2\\xbc\\xc8cL\\xbc\\xe7\\xa5\\xd4\\xbd\\x95\\xff\\xec<\\\"C\\x93<\\xa7\\x9c\\xda\\xbc\\xac\\x8eu=KE\\xff\\xbc\\xae\\xfd|\\xbbeRE\\xbc1l\\xb7<\\x16MN<\\xf3\\xe1\\t=\\xf9\\xd7\\xa8\\xbc~\\xfb\\\"\\xbb\\x93\\xeb\\x13=g\\xa7\\xa6<\\x0e\\x9b\\xf6\\xbb\\x82\\xc8\\xec<\\xfa\\xa4\\xa9=Y\\xcd\\xad\\xbc\\x98K\\x81<=W\\xd8=\\x9f\\xb9\\xae\\xbc\\r\\xbbK\\xbd\\xc2\\x95(\\xbd\\x1f\\x1b\\xef\\xba\\x1fR#\\xbd\\xab\\xa3\\xa7<\\xda\\xbf\\n\\xbd\\tb\\xa9\\xbc\\xb7\\xd3D=\\xbe\\xbd7=\\xae9\\\";\\x1dD-\\xbdf\\x01\\xd6\\xbc|\\xb1\\x8a\\xbcz\\x16\\r<\\xaff\\xbc:R%O\\xbd!X\\xd1</\\xc39\\xbd\\xa5\\x9dL=\\xc2\\x95\\x07\\xbc\\xf8\\x00\\x12\\xbd\\xf7\\x84o\\xbd\\xc9=\\xc2\\xbc\\x05\\x98\\xb4\\xbc\\xef\\xdc\\xb1<\\xd2_\\xca\\xbbj\\xa7&\\xbd\\xe3\\xcb/=\\xbf\\xc27;\\x81\\xe2\\xa89d\\x89\\xb5;M\\xc2\\xcc\\xbb\\xb0\\x9a\\x8d=S\\x19\\xf3\\xba\\xcbH\\x01\\xbd{W_=\\x1d\\xe6\\xbe<\\x8d\\xafU=\\xa9\\x86\\xef<\\xae(\\xcf;\\xa3\\xb8\\\"\\xbdC8\\xf6\\xbct\\x88\\x84=\\xcbs\\xd8;\\xce\\xe5z\\xbdsG\\xf6<\\xc8\\x08\\xf1\\xba\\x06\\xa3\\xfb\\xbc\\xfdm\\\"\\xbd\\xb4=\\xb2\\xbd\\x90\\xe6P=\\xda&\\xa0<\\x9by\\x06\\xbd\\xf64\\xa3=B\\xc3\\xa9=3\\x88\\xbf<\\xe7q\\xd9;\\xcd[\\x03\\xbcob7\\xbd\\xe4\\x1ef\\xbc\\x8cv\\xad=\\t{,<\\xa6\\xba\\xbb<\\x83$<<&L\\xe6:uW\\x83<y\\xa5\\x06\\xbd\\x9fFa\\xbd\\xfd\\xd2\\x92\\xbc\\xae}r\\xbc.\\xbd\\xa8\\xbc\\xb7m\\x9f\\xbd\\xdd\\xc5\\x19\\xbd1\\xea\\xdf:\\x0c\\x06E\\xbd\\x14o\\x1a=7\\xd6\\xcf\\xbc\\xf3\\xa2\\xf6\\xbc\\x8c\\xf97=\\xb5\\xa5\\x98\\xbc^\\xea:\\xbd\\x0f<L=9\\xd2X=k\\x83\\x11\\xbd7\\x888;A\\xad =Om\\x85\\xbd!\\xda\\xc1<X\\xe6\\xaa<mV\\xb2\\xbd\\xe3\\x9c\\xa9\\xbbm\\xae\\xa4\\xbc\\xf2\\xac\\x1f=6_%=\\xf1\\xba\\xbb\\xbbZv|\\xbc\\xa8\\xd8{=!\\x11\\xfe\\xbb\\x05\\x10J;\\xef\\x063<)!\\xbb<\\xfc\\x15\\xc6;\\xd3F!=\\xe1\\xebn\\xbc\\xbe;\\x89:^\\x10\\x9b<*\\xa3\\x95;\\xc7m\\xa5\\xbd\\xe6\\xb1\\x1d\\xbcN\\xf5\\x18\\xbc\\xa6%\\xca<\\x10%\\xe89I>\\x8e<\\x1a\\xdc =\\xb7\\xf6\\xe2;\\x8e:\\x99\\xbcF\\xed\\x85\\xbd\\xdd\\n==Nq\\x00\\xbd\\xc6\\xdf\\xf0\\xbc\\r\\xe3\\x88:{\\xf2\\xdf\\xbcA\\xdf\\xbc\\xbb\\x03C\\xce\\xbcG\\x1a\\x0c\\xbd\\xda\\xb8\\x94=(\\x17\\xd9<t\\x80\\\"=Yb\\x19={\\x06E=\\x0b\\xe6p\\xbc\\x941\\x92\\xbc\\xb7\\x14\\xdd\\xbb)u\\xa9\\xbcC^\\xa6;\\xf1\\x9d:<\\xb2\\x95\\xec;\\xe0r\\n\\xbc\\x03\\x14\\x17\\xbd\\xc7\\\"\\x1f=\\xe5J\\xe1\\xbc\\x9e\\x86\\xbf\\xbdUb\\xab\\xbcq\\x1b[\\xbd\\xdf\\xcc\\xd5<\\xda\\x1a4\\xba\\xfd\\x84\\xad<\\xd0\\xa6\\xd7\\xbct\\xa3\\xfa;\\xe7\\xd7\\xab=\\x1b\\x91\\xaa=BV4\\xbd\\x06\\xadj=\\xb4\\xa7\\x1b\\xbd\\xc1\\x97e<\\x14a%=\\x90\\xfcY\\xbcy~\\x00\\xbd\\xda}\\x93<z\\x83\\x17=\\x1bz\\r\\xbd\\xc1w\\x03=\\x95\\xf9\\xed\\xbc\\xbft#\\xba\\xf8i\\xcd<\\xca\\xd4\\xa1<\\x0cG\\x86\\xbc\\xc2\\xc3\\x7f:R\\x10J\\xbcv\\x02\\xad\\xbc\\r\\xd8\\x99<\\x82\\x90\\xa5<\\xd3\\xa2\\\"\\xbd\\x94B\\xff<\\x86\\x13\\xeb\\xbc\\xfb\\xa0\\xd5\\xbd\\xd3s\\x99<\\xf6\\xfd\\x9b\\xbc\\xd3\\xf6\\xea:\\xf6\\xce\\xc2<\\xb8\\xe1\\x15\\xbd,\\x1d\\xa1<\\x0e9\\x86=\\xfcKK\\xbc\\x88\\x8fB\\t\\xf8\\xfd\\x06\\xbc:\\x91-\\xbdq\\xfc\\x18=@^\\xc3<\\x0bv\\x17<`\\xe0[=i\\xb3g<@\\x97\\xba\\xba\\xe0*\\xf6\\xbby!\\xe3\\xba\\xd9\\x10==\\xd4R\\x1d=\\xb2h\\xe1\\xbb\\xf4d\\x96=3\\x05\\xe9\\xbc`\\x97\\x9f<&\\x91G\\xbd\\xe4~$<\\xc0s\\x89\\xbc\\x00\\xca3=\\xad\\xdc\\xd9;\\x85j\\x1d\\xbcQ\\x12\\xdd<\\xac:.;@v\\xd2\\xbch\\xee\\xfa\\xbcP\\x96\\x9a<\\x00\\xa43\\xbc*\\r\\xbc\\xbc\\x06\\xae\\xd2<\\x10\\x9a\\xd1\\xbb\\x8f\\xad\\xbf<Bp\\xad;\\xde\\xbc{\\xbd0\\xbcV\\xbd\\xfd\\x88\\xa0\\xbc\\xa6\\\\\\x07=\\xc5\\x1a@:\\xb5\\xbf\\x8c=b\\x86*:\\x16\\x0c =M0\\xa0;\\xaa?.=\\x86\\x82\\x1a=A\\x14\\xd6<\\x89L\\x1e=oO\\xf7=\\xe3\\x8fb<\\xed\\xb5\\xf7\\xbb7\\xfdQ\\xbd\\xd3\\xe8v\\xbdV\\xeb\\xfd\\xbc@\\xd3\\x1e=$\\xac\\x8c\\xbc\\xf0\\x1c\\xe0<\\xe0 \\xc9<\\xb82\\xb9:\\x97\\x01\\x80=\\x98R%:2\\xfb\\xba<\\xc4\\xd4\\x88\\xbb/\\x9a\\xb7;X\\xad\\xc7\\xbc3*\\x05=\\xaci|\\xbd\\xc5\\xec{\\xbd/\\xb1\\xef\\xbb\\x94\\xd6\\xbb\\xbclS\\r=\\xba\\x96\\x16\\xbd\\x97h\\xf4<O\\x8d\\xfe\\xbc\\xf9\\xacF=\\x9b\\\"l<\\x81\\xb1\\x1c=\\xde\\xa5\\xda:D\\x95\\xce\\xbb\\xbd\\x8a\\x84=\\x1e4\\x98\\xbc*\\xf30=!\\x15\\x04\\xbdp\\xd6\\xd0;\\xb4\\x04\\xcc\\xbc2TS=|\\x94\\x94\\xbc\\x18\\xe8B<\\x18\\x88[=c\\x86?=\\x9c,5\\xbc\\xd3\\x7f\\x91\\xbcF1N\\xbcw\\xb7\\xe8<\\xbe\\x8e\\xcb=\\xa5;\\x03\\xba87[=\\xc7UP=\\x10-[\\xbd\\x0e\\x12\\x9f\\xbb\\xc2M\\x91\\xbcj_\\x08<W\\xea\\xca\\xbc\\x03\\xe0&\\xbd\\\\{\\xc3;:\\xa4<\\xbdUy\\\"<,\\xd2\\x82;\\x0c\\xe8\\x04=sy\\x95\\xbc\\x0e\\xcc\\x95\\xbd\\xa8\\x9d\\xe6\\xbc.\\xdb/<\\x9b\\n\\xeb;\\xedSu\\xbd\\x10\\xf1-=l\\x82L=-\\xe3\\xb7<#\\xf8\\x9d=\\x0e\\x90\\xda\\xbc\\xf3\\x81C\\xbc\\xe4\\x8a\\xaa\\xbd\\xe0D\\xf7<*\\x1f\\x86<s\\xdb\\xa0<c\\xfe\\xe2\\xbb_J\\x07=Z\\xbd\\xa1\\xbc\\xec1\\xde\\xbb2/\\xb5\\xbc\\x9e\\x0c9=\\xd0\\xbb@\\xbd\\xdc\\xc1\\xb5\\xbcL/~\\xbd\\xf7Y\\x16=  \\xba;\\x1a\\xc2\\x92=\\n\\x85\\x95;\\xa4-\\xf7<KSy\\xbd9c\\xae:\\xe8*L<\\x1c\\x16\\x06\\xbc\\xfe\\x84X\\xbdr\\xf3\\xd2\\xbc^^\\xa1;\\x002\\x8a\\xbc\\x04\\xb1\\xe79\\xcf\\xfd\\xdf;\\x9d\\x9b-;\\xc5Lc<\\x8f\\xac\\xe7;4\\x96\\x8f<i\\xc7\\xb6<\\x9ef\\xc1;\\x13\\xbc\\x07=\\x00\\xba\\xfa;\\xf3\\x91\\xb8<\\xdf\\x9f\\x91\\xbc\\x05r\\\\=\\xdeD\\xfa;\\x16t\\xa7<\\xa0#$\\xbd\\x8e-\\xb7=\\xbe\\xf5R<\\x94\\x1e\\x08<\\xbc\\x8f\\x95\\xbc\\xfb\\x85\\xfa\\xbc\\x80\\xa1J\\xbd\\x15{\\xa8<qz\\xa8=\\xd7u\\xb0;Al5\\xbc\\tDg<\\x9bV\\xc8<{\\xed\\xa6\\xbc\\x17\\x9ex<\\\"\\xd6\\x8c=\\xec\\x9f]\\xbc2Dt<\\x08\\xbf\\xb0<S\\xdfJ<T\\xbcE\\xbdH\\xddp\\xbd\\x8c\\xad\\xfc<\\xa2>\\x1d\\xbd\\x85\\x06\\xf0\\xbc\\x0f\\x0e\\xac\\xbc\\xc2\\x88\\t\\xbc\\x1b\\\\A\\xbd^\\xe43=\\xb2Y\\xa2<1u\\xc5\\xbb\\xc8A5<\\x80\\xf5\\xad\\xbd\\xeb~\\xb8\\xbc7\\x0b\\xf3\\xbc\\xc6\\xc1\\x1a\\xbd\\xb1\\xfc0\\xbd\\x9e\\xc1f;\\xf2\\x18\\xd2\\xbc\\xed\\xf7\\x1f\\xbd\\xf7C!\\xbd\\t\\x9e\\x83\\xba\\xb8J\\x97\\xbc\\xf4\\xb2\\xa1\\xbd\\xe1d\\xe4\\xbc\\xe0\\xaf\\xbd\\xbc\\x1b\\xd6\\x0b<\\xad\\xec\\xa3\\xbb\\xc1S$\\xbcI\\x8e\\xf6\\xbcf<\\x02=t\\xa2\\xab;\\xccX\\x04=\\x99E\\x8a\\xbc\\x8as2\\xbd\\xcb\\x9c?<\\x7f\\x11\\xd2;\\xd8\\x1c\\x02\\xbc\\x80@\\xdb\\xbck-|\\xbd\\xd1B\\x8d<T\\x1dS\\xbd\\xcb\\x88\\xd7\\xbbd\\x00\\xbb=\\x88\\x92\\xaa=\\xe9\\xc8\\x7f\\xbdmmQ<\\xd7\\xdd\\x1c\\xbd\\xc9\\\\\\\"\\xbdO(\\xe9<\\xb5\\x0f \\xbc\\xd1\\xdb.=\\x86\\xa5\\x0f\\xbd$P\\xa7;D<\\xea<\\x90+m\\xbdyo\\xcb;\\xe5\\x9c|\\xb9\\xf3\\xb3\\xef;\\x18,\\x99<9\\xc1\\xd9<z\\x98\\xbd<\\xff\\xe5\\xbb;\\xdcb\\xb0<\\x0b*\\x04\\xbdi\\xf4_<\\xe0Z\\xc3\\xbb\\x05\\x12\\xa2;\\xa0\\xcf\\x8c\\xbdD\\xd4J<\\xff\\xa4D;]B:\\xbc\\x89\\x08o:\\x1a\\xd9\\x93<D\\xbfg\\xbc\\xa7\\x8eJ={B,\\xbdbw\\xba;E\\xe8z\\xbcu\\xfcV\\xbdy\\xbb\\xe9;\\x84\\xbf\\x89;\\x9c\\x07\\xb4;\\x92)\\x96<\\xe0\\xe6\\xc0=\\xa1\\xfc9\\xbc>\\x91\\x1a=v\\x92\\x8f=\\xa9\\xb8.\\xbd\\xf8rD<\\x98\\x05\\xca\\xbc\\xff\\xc3I=\\xf6\\xb3 \\xbd\"\nHSET bikes:10094  model 'Triton' brand 'Bicyk' price 3978 type 'Enduro bikes' material 'carbon' weight 13.4 description 'This bike fills the space between a pure XC race bike, and a trail bike. It is light, with shorter travel (115mm rear and 120mm front), and quick handling. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. It’s for the rider who wants both efficiency and capability.' description_embeddings \"\\xf2\\x0c1\\xbcj\\x19\\xcb\\xbc\\x9c\\xfa\\xd0\\xbcN\\x842<\\xfc\\xb6\\x9b\\xbc\\xee~3=\\x141\\x04\\xbdDJ\\x14\\xbb\\xa7\\x1d\\xa9=\\xafSh<]\\x1b\\xd8<rD\\xe9<9\\xcbU=A\\x08\\xec<\\\\p\\x87=\\x1f(\\x92\\xbd\\xcc\\xe7v=\\xc4\\x86i\\xbd\\xf5j \\xbc\\xd3\\xdab\\xbd\\x12\\x0e\\x08<VQ\\x82\\xbce\\xcfA=%a\\x99\\xbd\\x1a\\xd4m\\xbdELL\\xbc\\xdf\\x8b\\x06<\\x00~\\x95\\xbc\\x1c\\x97!\\xbdor\\x87=\\x80\\xdc\\xbb:q\\xc3`\\xbd\\x14\\x14s<\\xea\\x83+;1\\xa2\\xb0\\xbc\\xe7~\\x95\\xbc\\xb1\\xb0\\xac<\\xf7qD\\xbc\\xd6`\\x87\\xbcx\\xb5\\x99<^\\x0e+=c\\xaa\\xc3<?\\x97\\x08=\\x87D\\x89<S\\x9a)\\xbc\\x0bZ\\xf8\\xbb9\\xf0\\x1f=K\\x10\\xa9<\\xf6\\xc1\\xee\\xbc\\x84\\xf9\\xa8\\xbcOq3:\\xecR(\\xbd\\x8e\\xa1\\xe6\\xbc\\xb9\\x7f\\x1b;\\xd5G9<\\xdd\\xcc\\xa2;_\\xba\\x80\\xbc\\xcco\\xa3\\xbdA\\x8fq\\xbd\\x97\\xdc\\xd0=\\x94N\\xa2<\\xed\\x97\\x8d=\\xe8P\\n\\xbb\\xe1\\x83M=z\\xcc!=\\xe3\\x15,<\\xda\\x97\\x0e=e\\x97\\x07\\xbb\\xa0\\xb6\\x8a<.sc\\xbd_\\xb1\\x02\\xbd\\xa9\\x9e\\t<92\\xf0\\xbc\\xc8tf\\xbd\\x907\\r\\xbd8\\xda\\x94\\xbc\\x08\\xc6\\xb7:\\xb1#\\xd7\\xbc\\\\\\xf9;\\xbd\\x1bT\\\"=W\\xe8\\xaf;\\xbcg\\xa8\\xbd\\xf3{\\x96\\xbc\\xb0i*\\xbdh\\x951\\xbcM\\xdf\\xf2\\xbc\\x85\\x95\\xab\\xbb\\x03\\x91\\x9e<\\x04OR=\\xddh\\x0b<\\xcan\\xa3\\xbaB\\xdc\\r<u \\x07\\xbc\\xf2}\\x82\\xba\\xaf\\xf7\\xb4<\\xff1\\x91<\\x1aN\\\\\\xbdk\\r#\\xbb$\\xc04=^\\x07\\x8a\\xbd\\x83\\xe6\\x07\\xbd<\\x0b\\x98<\\x13\\xeeI<4\\xaen\\xbd\\xc9\\xe7p;Y\\xea\\xfa=\\xbcU\\xc5\\xbb!\\xdbk<\\xe9\\\"\\x8b\\xbc\\x96\\xe0@<3\\t\\xdb;\\xf7|K=07\\xb0<m\\xbb\\x83\\xbc\\xf2\\x88\\x8a\\xbd\\x8d\\xed\\x12<\\xdb\\xd0\\xa1<m\\xebr;\\xd6%c=\\x90\\xe3\\x12<VC\\xee<\\xef\\x11d<\\x995\\x95\\xbdk\\xac\\xa8:\\xa5\\xf1\\xa9<\\xa5\\xa6S;2\\xdb\\x00=b\\xc48\\xbd\\xb8\\x01!\\xbd\\xc1\\xbd@\\xbd&W\\x05\\xbd\\xa7f\\xf4\\xbc\\xbb,\\xe4<\\x8c:1\\xbc\\x1a\\x04\\xc4\\xbc\\x87\\x03\\x1c=b\\x8a\\x87\\xbc\\xd8\\x89*\\xbc/q\\x0c=W\\xdd\\x12\\xbd\\xfc-\\xc2\\xbc9\\xfcp\\xbd\\xc1-\\xfc\\xbah\\xd7\\x82=[i\\xd9\\xbc\\xdaNQ<Q\\x842\\xbc\\x8a\\x84`\\xbc\\x83\\x17\\x95\\xbc;6\\xb3;s4\\x11\\xbd#\\x06\\x8e<\\xd1\\xd2\\xe1\\xbcO{K;B\\xf4\\x87\\xbckS\\xf9\\xbc\\xbc<\\x91\\xbb\\x85\\xf7t<vW!=\\xe1\\xb2P\\xbd\\x02\\xe6)=h\\xea+<]9\\n\\xbc\\x95\\xa8C\\xbd\\xbc,\\xbe<\\xf7\\x92\\x0e=\\xfe+\\xcd;3)\\xa6<\\r\\xe2L=\\x1c\\xdc\\xfb\\xbb\\x1e:\\x97\\xbcP\\x8f\\xcd<\\xe4\\xba`\\xbcx\\x01\\x16\\xbd\\n}>\\xbd\\xf0\\xcb\\xe2<o\\xd8,\\xbd\\xe3c\\x99=x={\\xbc\\x88\\xf5\\xe9\\xbcv\\x1c\\x8e;7\\xab\\xfa9\\xdd\\x141<\\t\\xd1\\xa9<\\x98\\xab<=\\xa5Z\\x10\\xbd\\xfbd\\x9e<\\x838\\xa8<>\\r =Dn)\\xbd\\x17qR<\\xf8\\xb1V=\\xd3\\xbd\\x92\\xbd\\x8a\\xbf\\xfc<\\xcc\\xa2\\x1d\\xbc\\x1b\\x8d\\x0e=57\\x83\\xbc\\xb4\\xe2\\xb6\\xbc\\xcf\\x07\\xe7<[I\\xc6<\\x1d\\xcb\\x04\\xbdQ\\x9a\\x82\\xbd\\x06(\\xa1\\xbd\\x92?*\\xbd\\xc86\\xc5\\xbc\\x12}\\x9a<\\xf3\\xeb5\\xbd\\\"\\x83\\x87\\xbc\\xfc\\x85(=\\xc9\\r(\\xbd\\xe6\\x17\\xf2\\xbb\\x8d\\xcfj<\\x99\\xef\\x07=\\xbf\\x03\\xce<p\\x13%\\xbc\\x05\\xfa\\x04;\\x0f\\xe6\\xc8\\xbd\\xcf\\xdd\\xe8\\xbc\\xdcI\\xdf<hXm\\xbd\\x84\\xcb9<i\\x91\\xc6\\xbc\\x08\\xd2+\\xbd\\x98\\x07u<I\\x13?\\xbcl\\xe6f\\xbc\\x9a}-<\\xe9\\x1f\\x81;\\xa1Q\\xef<a|\\xa8\\xbcH\\xf1\\x08\\xbd~G\\x0f\\xbch\\xa3a=\\x1ci/<\\xbc\\xcbD<\\x0e>\\x02\\xbd\\xb7\\xc8j;\\xb6O#=\\xc8\\xd6h\\xbcw\\xa0\\x9b\\xbc\\xe3\\xe4\\x18=i\\x04\\x8f<F\\x90\\x85=IL\\x0c=\\x98A\\xa0<\\nb\\x0f=9kt=/\\xbf\\x10<\\x9eO)\\xbdp\\xef\\xd9<\\x00\\xdcU<\\xfff\\xd3\\xbc\\x94i.=\\xe9\\x9d\\x16=\\xe1jz;\\x91=m=H\\x1a\\xb3\\xbc\\xba\\xcd\\x02=\\x8c\\x87\\x89;\\xac\\xde\\x08=R\\xbf\\xa5\\xbc`G\\x96<\\xacX>\\xbd\\xf9\\xfc\\x0f\\xbb\\x08\\x8aI\\xbd\\xca\\x86\\x81=p\\nY<\\x98\\xf9\\xf5:\\xbfvR\\xbbq\\xfe\\xbe;\\xec\\x98V;\\xfb\\xef\\x8a\\xbb\\x1a\\xb18\\xbd\\xe8\\xabW<\\xfad\\xb6\\xbb\\xb1\\xbc\\xd6\\xbcq\\x97\\xba< \\x8f4\\xbc\\xf1\\x084=\\xae\\x8f\\x9e\\xbd\\x886\\xe8\\xbb\\xf3\\xaet<\\x05\\xf7\\xae<Yi\\x17=\\x07c\\x0f<?-\\xcc\\xbc\\xbb2\\x0b\\xbd\\x9a\\xb6\\xb7=\\x05\\x81g\\xbd,\\xbbn<2\\x0f\\xf7;\\xd2.\\xad\\xbb\\xce\\xc9\\xa0=W\\xcd\\xdd;\\x1f\\x86\\x0f\\xbcA\\x95\\x7f\\xbby.\\xc1<\\xa6DD<\\x8f\\xd5\\xe1;\\x07\\x99\\x8c\\xbb\\xc3o\\xf2<,O\\\"\\xbc\\x8aD\\x1e=\\x94\\xc4=\\xbb\\xadBJ=z\\xde\\x83;\\x88`\\x81;\\x16o5<s1d=ef\\x91<\\xaf\\xf8\\x1f\\xbd\\xbe4:\\xbd\\x08m#\\xbd{\\x02\\x15\\xbd\\xb9\\xc6\\x83<\\x93UG\\xbd\\n\\xdb\\xb5\\xbc\\x92p\\x80=\\x10\\x18\\xe3<\\xe5\\xa3\\xaa\\xbc\\x8c\\xe8\\xb5\\xbcn\\x8b\\x97\\xbc\\x90\\x82\\x05=\\xc6q]\\xba\\xd3\\xc7\\xfc<\\x07\\x94Z\\xbd\\xd9\\xf5\\xa6<-\\xc9\\x90\\xbd/\\xab\\x18=D\\xfa\\xa5<\\xfcV\\x95;\\x8f\\xc6\\x81\\xbd\\xb5\\xad\\xaa\\xbc?\\x85\\xad;\\\"\\xd5\\x81<-\\x9d\\x15\\xbd\\x1am\\\\\\xbd8#\\x04=/\\xaaH=+\\x8b\\xb2;\\x7fa\\x1e\\xbd\\xc18\\xd6\\xbcz\\x83\\xa5=\\xa5ES\\xbb\\xbb\\xac\\x05<\\xef\\xd0\\x13=\\x9d;\\x9d=J\\x85\\xab<\\xd3\\xfe\\x9d=w\\xf9\\xc6<\\xa6\\x9fS=\\x9c\\xc1\\xc2\\xbc\\x91\\x9d\\xa9=yg\\x12\\xbcG\\xf7\\xad\\xbd7\\x16Y\\xbc\\xedq\\xc0\\xbc\\xa7\\xb0\\x9f\\xbc%\\xea~\\xbc\\x1a\\x88\\xf2\\xbdAm\\xb5\\xbb\\xc8\\x19w<\\xab\\x06I=\\xd9\\x9a\\x13=\\xfc\\xeb\\xb4<\\xb4z\\xb9;\\xf0\\xd35\\xbd`\\xcb\\x89\\xbbrwO\\xbd\\xe4\\xc3\\xd3\\xbc\\x8b\\xbf!=\\xf9\\x90\\xf6;N\\x05\\xf6;\\xe7\\xe2\\xa1;\\xa9t\\xb1<\\xf6j\\x91<\\xd5\\x13\\xf6\\xbb!Dl\\xbd\\xd0HH<\\x1dg\\x17\\xbd\\xef\\xa5!\\xbd_\\xcd\\xdc\\xbbA9\\x03\\xbd?R\\x01\\xbc\\xf8\\x8f\\xb7\\xbd?\\x8ca=\\x00\\xfe\\xc9\\xbc\\x16\\x08\\xa6<:\\xb2=<\\xa9+\\xee;\\x15\\x9ay\\xbd\\xc2\\x82\\xbc=[\\x0b\\xe0=\\x90F1\\xbd\\xc7\\xe0\\xb6<]!\\x1d;\\x06 \\xd7\\xbc\\xa2\\x14\\x92\\xbb\\xf8\\x03\\xb9=\\x0cK\\xa5\\xbd\\xae\\xe7K\\xbb\\x10\\x01R\\xbdv\\xbb\\\"=\\x11\\x9f\\x0c;\\x7f \\x85=\\x1fS&\\xbc\\x1d\\xa4\\xc7<xYN\\xbc;W\\xe9;\\xe0\\x8f+=\\xb0\\x13>=5\\x89\\x02<K^\\x8f<\\xf5dI<\\x14O\\x8c<b\\x07\\xf5;@\\xfe\\xbe<-\\xc9\\x9d\\xbd\\x9c)V\\xbb\\x14&z\\xbb\\xa2\\x85\\xc2\\xbc\\x80|\\xf7\\xbc \\xed\\xd4<tRz=\\x05\\xf8\\xc0<6\\x0e\\x1d\\xbdSpc\\xbc\\xe4\\xe1.=*\\xfdE<L\\xe12\\xbd\\x99(\\x1d<b\\xc6 \\xbb\\xb1e\\x97\\xbd,zZ\\xbd\\x9e\\xad\\x1a\\xbdl\\xb8\\x8c=x\\x82T=\\xfc!\\xa1;\\xe31\\xad\\xbc\\x15\\xb2\\x82<\\x89DE\\xbdc\\xce\\x80\\xbc,\\x84\\xea;\\xf1\\xcf\\x14\\xbd\\x18_@<\\xa6\\x9c0<\\xdf\\xf59</5\\xca;\\xbfb\\x88\\xbd[\\x99L=+~\\x82\\xbd\\xd7\\x92\\x8d\\xbc?\\x0c\\x93\\xbcFp\\xa5\\xbd\\xfc\\x85\\xa6<\\x1dJ!\\xbb\\xc1}h<3R\\x16<\\xb82|\\xbc`\\xde\\xad<\\x05p,=\\x19_\\x99<)\\xe4\\xce\\xbci\\x12K\\xbd\\x07\\xcf\\x81;\\xef\\xea\\xcb<\\xe4\\x14\\x8b;45O\\xbd\\xce\\xb7\\xcd\\xbb\\x11\\x86a=`\\xab\\t\\xbd\\xf6v\\x01=\\xf9d\\x9a\\xbc\\x15U\\x98\\xb9\\xec.k<\\x88\\x19\\x9c\\xbb\\xd0@e\\xbd\\xa2l\\xd7\\xbb\\x01&-\\xbb\\xb4+\\x8e;\\xe5\\xf4\\xa1;\\xeb\\r\\xfb<\\xa3\\x8aY\\xbcO\\xd27<\\xc3\\xef\\xbc\\xbc\\xd3\\xc79\\xbd9\\x8c\\x8e<(w\\x04=\\xdf\\xd0\\x80\\xbb\\x14<#\\xbbjd\\xa3\\xbd\\xc3\\xc0-<\\xcb\\x05\\x9f<\\x81\\xcc&\\xbbG\\x0cz\\t\\x82\\xb8\\x9e\\xbb\\t\\xeaj\\xbc\\x98\\x1a\\xfb<W;\\x10=\\x16T\\\"=\\xac\\xdf1=\\x06\\x88&<\\xbd\\x8d\\x80\\xbcUp\\x8c<Y\\xf3\\xdc\\xbc\\xeb\\xf3\\x8a<[\\xbf\\xc7<\\xf8\\xecE:l\\x01>=\\t\\xa7\\x08\\xbc\\\"\\x97R\\xbb\\\"\\x9b\\x0e\\xbdY\\xf2\\x93\\xbd\\xc2\\xba\\x8b:\\x81\\xe7\\x8a=~6\\xb3:9z\\x0c=$b\\x15;\\xb0\\xf7\\xe4\\xbc\\x0f:.=%r\\xbe\\xbb\\x08\\x0e\\x06=\\x98\\xc6\\x95<\\x89\\x87\\xe9\\xbc\\xcc\\xbd\\x14<\\xd79\\xb1<eT\\xa1<Y\\r\\x96\\xbd\\xb7\\x8bS\\xbdl@\\x8a\\xbda\\x03\\xcc;y&\\\"=\\x95L\\xdf<\\xf6\\xd1+<5\\x06\\x84\\xbc/I\\xd5\\xbcp\\x9a\\x0f\\xbdQ\\x9e\\x85\\xbcS,\\xfa<vt\\xab;\\xba\\xbbv\\xbc)d\\x91=\\x9f\\xc3G\\xbdDI8\\xbd\\x8b\\xdc\\x00=\\x9b\\t\\\"\\xbdO\\xd8\\t\\xbdj\\xb5\\xd3\\xba]\\xeaY=\\xba\\x95\\xca\\xbb\\\\m\\xc4\\xb8JF\\x0e\\xbd\\x95\\x1a\\x84;\\x988\\x0c=\\x92\\xae|<\\xfd\\xc0\\xb6<\\xeb\\xa3\\x05\\xbdE4\\x83<\\xda\\x04?=\\xb9X\\x00\\xbc\\xd0\\x80D\\xbd\\x10\\x8b\\xcb<g\\xa4\\xa0\\xbc\\xf4\\xf4G<\\x1c\\xca\\xcf<\\x04\\xb2\\xaf:d\\xdc\\x89\\xbc\\xd9.\\xd4\\xbc]6\\x03=\\x9c\\x16\\xde<\\xe2ZI\\xbd)S@\\xbd\\xac\\x9aQ=_\\x01\\x15<\\xae\\x1e\\x85=YQy\\xbd\\xf7\\xd3\\xc79\\xa5\\xd5\\xa4\\xbc\\x19\\xd4\\x82<\\xeac/\\xbd\\x03n\\xf5\\xbc\\x06\\xd8c=2=\\x0f=\\x1a\\r\\x8b;\\xf3\\t\\xa0<\\x11\\x11b8Q\\x1fQ\\xbdO\\xe3\\x96=P\\x7f\\xb9:o\\x16\\xef<\\xd6\\x8e(=T+\\xdb\\xbb\\x9fM2\\xbd\\xac\\xdf\\xb9\\xbb\\xc3\\xd4i;\\x11#~\\xbb\\xbag\\r\\xbc\\x9f\\xc4O<\\xb6\\x0c\\xa2;\\x8bQ\\xd3<U\\x9e\\x12\\xbd\\xb2d\\xc1<\\x8a~\\xa7\\xba\\x11\\xe5m;\\xc7\\xa2\\x16\\xbd+\\x1b\\x90=3\\x9f9\\xbd;8\\x19<(\\xb35<\\xb1M\\xd8=\\xde\\xbf\\x0e<U{\\x16<~T\\xe9\\xbcEz\\x16=\\xe6\\x1f\\x0f\\xbda\\xc6d<\\x88\\x08\\xd8;Ro\\xfb<p\\x9f\\x11=\\xe3O\\t=\\xda\\x02\\xaf\\xbb1m:\\xbd\\xb2\\xf1\\x9b<w\\xd0h=\\x07\\xc6\\x00\\xbd\\x87\\xfb\\xd0\\xbc\\xf9X\\x94\\xbd\\x17~\\xdc<\\xdc\\x06E\\xba\\x18_\\x9e=l\\x90\\xa2<&\\xc8[=\\xe3\\xc7\\x8e\\xbd\\x9e\\xd6\\xfb\\xb9J\\xf2&<\\x1doY=\\xcc\\x83,\\xbd<\\x17\\xef\\xbcM\\xc8!:\\x0ez\\xcb<\\x06B\\xf7:\\xbd\\xc1!=\\x98\\xb8\\xb4\\xbc\\xe5\\xe6\\x08=\\x02w<\\xbcb\\xfe\\xf2\\xbc\\x96]\\xaa<\\xba\\x99\\x90\\xbc\\x18\\x92\\xf8<\\xf4\\x96\\x9e<\\r\\x8d/=\\xdc$\\x99\\xbd\\xe4\\x13\\x99\\xbb\\xe0\\xb9;\\xba\\x8a\\x7f\\x7f;)(\\x02=\\xc5\\x83E=\\xf4Q\\x07;\\x1dN\\xf4\\xb9\\xcdUr\\xbc\\xb2\\xe9v\\xbbt\\x93\\x94\\xbc+&\\xb2<\\xad\\xac\\x1d=\\x8bL\\x03\\xbdX\\xc1\\x06<\\xcb\\x86U\\xbc\\xb5`\\x14<\\x02\\xc5i;\\xffm\\x8d<\\x06\\xb3O=\\xb3\\x1f\\x94<\\x927\\x14=$\\x1e\\x9a<iL\\xc3\\xbcc\\xcd\\xa9\\xbc\\xa2&\\x82\\xbd\\xb3\\t\\\"=\\x1epI\\xbc\\x1f\\xc6\\x1b\\xbd\\xd1\\x8f\\r;t\\x17\\x98\\xbc(\\xbb\\x0b\\xbdh\\\\\\x07=\\xcd\\x98f:\\xa7\\xc0\\xd6\\xbcnYD=\\xf5\\xc1\\x13\\xbd\\\"o\\xa6\\xbc4\\x00\\x89\\xbd\\xec\\x7f\\xa3\\xbd\\xd1\\x98\\x97\\xbc\\xb5:\\xc6\\xbc\\xa4\\xfe;\\xbcO\\x01,<\\xd6\\x1d>\\xbdE\\xfdC\\xbcqZ\\xc8\\xbc\\xa0\\x90\\x81\\xbd\\xcfBJ\\xbc\\x18\\xa4\\x99\\xbc\\xaa)\\xd5;\\xbe\\xc1;\\xbd\\x981i<V\\x08\\x8f\\xbdZ\\xaa\\x06<\\xd7\\x9d0\\xbb\\x88\\x87\\x9d;,\\xfa\\x80<{\\xe4\\xc7\\xbc\\xc3Wk\\xbc2j6<\\xe0]`\\xbb\\x1dN\\xb1\\xbdZ\\x98\\xb0\\xbd\\xdb\\xa6\\x13\\xbd\\x9f\\xa7i\\xbc\\xc9\\xc7== \\xeb?<;\\xf7\\xa7=&\\x1c\\xa5\\xbcOj\\x0c=\\x06j\\x83\\xbd\\xda\\xf0\\x9e\\xbd\\x05\\xaf\\x0f=\\x8e8\\x04\\xbb7$\\x98\\xbc/\\xae\\x00\\xbd\\t\\x10\\x00\\xbd\\x17\\x1f\\xa2\\xbc\\xb19\\x80\\xbdmh\\xfa\\xbc\\xe4J\\x0b=o\\xdf]=\\xa6\\xe7\\x86\\xbc\\x10\\xe4\\xf4\\xbb\\xcc\\x8aK=\\x1a\\xa4\\x9a\\xbb\\xbf\\\"\\xfa<0\\xab\\x0e:@h\\x97;\\xf5\\xe7t\\xbd\\xa6\\x8f\\x87;\\x06p\\x97\\xbd\\xcc\\xbc?;9\\xbd\\xf4\\xbb\\x18\\xfe\\x04\\xbc\\xe5\\x0bC;\\x8f+,=\\x8bnV\\xbd\\xfa\\xfa\\xa7\\xbb\\x05\\t<\\xbdP\\xa7\\xa9<\\xe7>\\xbe\\xbcY\\x9e\\xb1\\xbd<1\\x06=\\xbe\\x82\\x8a<\\x9d\\x81\\x1c<\\x15.7=\\xec\\xe1\\xc0=\\xed\\xf6!\\xbcx\\xd0T=3\\xf9:=d\\x91\\xc59ADJ=i\\x7f\\x8e\\xbd\\xb9\\xb1\\x99<\\xbe\\xe4\\x98\\xbc\"\nHSET bikes:10095  model 'Titania' brand 'Bicyk' price 2055 type 'Road bikes' material 'carbon' weight 8.8 description 'The women-specific, race-ready frameset has received significant upgrades for 2022 and now has a stiffer front end thanks to the upgraded SL fork. The hydraulic disc brakes provide powerful and modulated braking even in wet conditions, whilst the 3x8 drivetrain offers a huge choice of gears. All bikes are great in their own way, but this bike will be one of the best you’ve ridden.' description_embeddings \" \\xfd)=\\xf438;}\\x10\\x10\\xbd\\xe2E_;*c?<\\x17&\\xc4<vI\\xec\\xbc\\xec\\x9e\\x8b\\xbdX\\xf8!=d\\\"\\x9c<\\xbf\\x86\\x03<\\xe6\\x87\\xf1<\\xe4\\xfad=\\x13\\xd4\\x12;\\xafFb=\\xeb\\xde\\xb9\\xbce\\xb7o<\\xf1\\x88\\x81\\xbc6\\xde!=\\xb3\\x81f\\xbd\\xed\\x1eU\\xbc\\x80\\xf3\\x00\\xbdg!7\\xbc\\xf5\\xb8r\\xbd\\xe8\\\"\\x8e;\\x07\\xc1\\x12;D\\xb5\\xb3<z\\xa2I\\xbc\\xef\\x13+\\xbc3\\xdb\\xd7=\\xb0\\xc7U\\xbc\\xc9\\x1d\\x96\\xbcHM\\x15<w\\xb7\\\"\\xbb\\xfb\\xc6\\xe4;\\xca\\xe1\\xf1;\\xbcq\\xcb;\\x837\\x9c\\xbc\\xbd\\x99V\\xbc\\x8568<\\x84\\xa5\\xe1=\\xb4\\xfc\\x08;JwN\\xbcq\\xc35=\\xd6\\xd5\\x81\\xbc\\xb9;\\xef\\xbc\\x80Pk=\\xd2\\xc4\\x8b;I\\xd0E\\xbb\\x96\\xb0\\xf7\\xbai\\x19\\x8d<\\x95\\xe1\\x03\\xbdr?!\\xbdq\\xf3B<y\\xcf\\x85\\xbcg\\x07r\\xbd\\xa1\\x11Y;\\xcd\\x96\\x8b\\xbd\\xa83\\x8e\\xbd^\\x92\\x8a=G\\xfa\\xc6;K\\x87\\x0c=\\xd7,Z\\xbc)\\x94M=\\xfb\\x002=*\\xb9}:!g\\x04=\\x04\\xc4\\xa4\\xbbG\\x1d\\xf8<\\x98\\xc0E\\xbdW?\\x1d;[\\xef\\x0c\\xbd\\xaa\\xd0\\xe8\\xbc7^\\x1c\\xbb\\x95\\x17\\xc6\\xbc\\xbcO\\x88\\xbb\\x8a\\x86\\xb6\\xbc\\xbf9U\\xbd\\xe1\\x89\\x97\\xbd\\x93k#=.\\xbf\\x11<h\\xe8W\\xbd\\\"\\xe3\\xc4;\\x880\\x0e\\xbdbI\\xde\\xbcd\\x85s\\xbc\\xb1)/\\xbc\\xc5\\xb9\\xad<\\x89n\\xd4;\\xc5\\xec\\xdf<\\xcc<#\\xbdC\\x1c\\x84<I\\xd0\\x03\\xbd\\xcc\\x80^=A\\x06\\xfc<\\xa8u\\xc1<\\x95|\\x07\\xbd\\xaeoI<k\\x15\\xb6;(\\xb5<\\xbd\\x9bM\\x95;\\xc8Y#=;fW<:9\\xf2\\xba\\n;\\xe6\\xbc\\xf1\\xc7\\xe7=\\x1e<t<\\xf8\\xaf\\xa9<n(\\xef\\xbc\\x96AG<\\xafP/\\xbdMi\\xba=\\xa7\\x1c_=<\\xb9c\\xbc\\xe5Y5\\xbd1\\xe8\\x03\\xbd\\xd4\\x8a\\x08;\\x92\\x04\\xa3<d\\x0f\\xb5<9\\xe8\\xf2<}\\x97\\xdc<\\xca\\xab_<\\xe3@9\\xbdp\\xfe\\xed\\xbc\\x84\\xe7\\x7f<\\x05u\\x11\\xbd\\xa2\\xefR\\xba\\x17\\x89U\\xbdt\\xa9G\\xbd2.1\\xbd\\xee\\xab\\xad\\xbc\\xc9Q\\xe8;3\\xab\\x97\\xbc\\xa2\\x96\\xdc\\xbc\\xbf\\xad\\x19<\\t\\xe6\\x16=\\x9bi\\xd5\\xbb\\x11\\x99\\xd2\\xbb\\xd62\\xf2<g\\xa7`\\xbc\\x86H\\x82\\xbd\\xfa\\x88\\x8a\\xbd\\xcd\\x01\\xaa<\\xac`>=\\\"g\\xc2<\\xee\\xfaJ;\\x0fv\\x8c<\\xb2\\x01\\x95\\xbc\\xf5F\\x8d\\xbd\\xa8\\\\\\xe7\\xbcZ\\xd3\\xe2<\\xcb\\xe33=\\xc6-\\xca\\xbc\\xc8\\x1cf=g\\xb10\\xbc\\xa5\\x96,\\xbd\\x15t\\xa2\\xbcV#\\xa1<}\\xe8S=\\xd6R\\x10\\xbd\\x93\\xf6\\x8c=L\\xe99=gL\\x87\\xbaL\\x8f4\\xbd9\\xf1\\xdb<\\xa6\\xe2\\x05=\\xa0\\xa4B<\\xd6\\x7f\\xf3<4\\x8f\\x8e=w\\xf2\\xba:\\x85a\\xdd:\\xaf\\x1e\\x95=z\\xb5\\x03\\xbd\\xee\\x16h\\xbds\\x8b\\x1d<\\x8b8\\xbe<\\xf6\\\"\\xa2\\xbd\\xc8S\\xa0=\\x1e\\xb2\\xd3\\xbb\\xef\\xd5\\x82\\xbc\\x9f2\\xf9\\xba\\r\\x021<u\\xb1\\xed\\xbb\\xd3o\\xf6<\\x1bT\\x88=\\xc8l\\x01=\\x9f\\xdcu=\\xc6\\xb6\\xa8<\\xc1{D<P\\x1a)\\xbd\\xa00\\x04\\xbd\\xcb\\xe7\\xcb<\\x88Y\\x91\\xbd\\x92\\xff\\x16=\\xfb{9=\\x98e,=\\xe9\\xc3\\n<\\xcf+(\\xbd\\xc4\\xa0\\x80\\xbc\\xf94(=\\x9d\\xd0$\\xbc\\x85\\xb6\\x86\\xbd\\x9bY\\x81\\xbd\\x90\\xadx\\xbc\\x8e\\x01\\x92\\xbc\\xb2w2\\xbb\\xd3\\xc3\\x14\\xbd\\xf4\\x997\\xbd\\xcf?\\\"=\\xe0a&\\xbd\\x19|A\\xbd\\xeb\\xdd\\x15\\xbclt\\xeb<\\xe3?\\xd1\\xbc\\x7f\\x80\\xbd\\xbc\\xdb\\\\L\\xbb\\x8f\\xd9\\x1d\\xbdo\\x8f\\xf1\\xbc\\xd0\\xa6\\x81<\\x8b\\xa8\\x90\\xbdI\\xc5\\\"=/\\x85\\xe7\\xb9\\xd7\\xb6\\x97\\xbd{\\xf06\\xb9)\\x99\\xc6\\xbc\\xe5}\\xd7\\xbc\\\"\\x89q<b\\xff\\x82<\\xe1x\\x8b=|3\\xa8\\xbd\\x88 \\xf7\\xbc\\xd9\\xbaA<\\xa7\\xa8\\x1d\\xbc\\xb7\\r\\n=~\\xb5\\xc5\\xbc\\x10h^=A\\x9d&\\xbc\\x88\\xda\\xd0<^t\\xc1\\xbc\\xd5+\\xcf<\\x03\\\"7=U&\\xd1\\xbb\\xa0q\\x91=3g\\x89:\\xa7B\\x17\\xb7\\xa1))=E\\x9a\\xde\\xba\\xaeQ\\xa2\\xbcg\\xd3.\\xbd\\xf0\\xe2w\\xbc\\xa6=h<R\\x04\\x14\\xbd3\\xef\\xc5;\\xfa\\xa4J=\\xdd\\xe8\\x1c\\xbd\\xd7t\\x98=\\x0cg!<\\x9b\\xcb\\xeb;\\x05\\x13.<:\\xa3\\xb9=<%\\x89<=|i\\xbc\\x9e\\x11\\x1a\\xbd?\\x8d\\x17\\xbd\\xc5^,\\xbd\\xd7H\\xdd=\\x88\\x81N\\xbd(\\x1a\\x19\\xbc\\xc1\\x08$<\\x9b\\x1e\\xa6<V\\xa3\\x87<\\xc4L\\x82\\xbc#|\\xb4\\xbcd\\xd6O\\xbb\\xfa\\x85\\xc6\\xbc\\x8d\\\\b;t$ \\xbcG\\xda\\xe8<Z (=wa\\xb8\\xbd\\xdb>\\x14\\xbd).\\x8f\\xbcq\\xb6\\t\\xba\\n,<\\xba\\xcc\\x80\\x8b<R\\x07\\x07=M\\xe8\\x98\\xbcJ\\x0c3=Z\\x10\\xab\\xbd\\x15\\\"<=(j\\x94\\xb9\\xb9\\x9c\\xf7\\xbc3\\xc9\\xc5<\\x98\\xc1\\x03\\xbd\\xbf\\xf0\\xe1;;5K\\xbd\\xbc\\xff\\xf1<\\r\\xbd9=\\x9c\\x14T\\xba\\x19\\x83Y\\xbd`\\xe0\\x13<hF\\xd0\\xbb\\n\\xe7\\xfa<\\xaa[E\\xbd\\xf73*=c\\xd2k:\\xf2\\xed\\x99;\\xfb\\xd8\\x1b=Cp\\x98=^)z<\\xc1\\xf5\\xf1\\xbcr\\xaap\\xbc\\xaa#\\x04\\xbd\\\"\\x08a\\xbcqf\\xf1<\\x9a\\xe8!\\xbd\\xb6\\xa7\\xa1\\xbcf\\xea\\xb6=\\x15\\x83\\x95;#\\xe6\\x81<`xd\\xbc\\xdfb\\xa5;\\xa7H\\x9f:\\x0b\\xc3K<\\x06p\\xd7<z\\xb0b\\xbdz\\xf9\\xa8<y\\xf2,\\xbdE\\xd0 <0=\\xde;B\\\"\\xa8<\\xe9\\x1eK\\xbd\\x17\\x97C\\xbc\\x16<]=\\x08\\x9a\\x0e<0a\\xe0<%9{\\xbd\\x0c\\xc2t<lQB=\\x1c\\xa3}\\xbbKW\\xe0\\xbc\\xc8(\\x08:\\xa3\\x8b\\x99=& \\xca\\xbbx-\\x138i\\xa4,=L\\xa7\\x84=j\\x17\\xb7;\\x10\\x0e\\x82=6\\xaa\\x0c;\\x1b\\x9aI\\xbc\\xd9\\xf8\\xb8\\xbb\\xd1\\xfd\\\\=\\n\\xa7\\xa7\\xbc\\x90\\xfc\\x93\\xbd4\\x7f\\xfc9\\xde\\xc0\\x82\\xbb\\x8cA\\xbd\\xbc\\xcc\\x18\\x1e\\xbc\\xf4\\xb5o\\xbd\\xf4\\xb6)<\\x81\\xd1x<\\x16\\x12`\\xbc\\x08\\x97\\x16=\\x95(O=\\xa9\\xb8\\xd9;=\\xe0b\\xbdKt\\xff<\\xffx1\\xbd\\xb6\\xc3O\\xbd8\\x14S=\\x1c\\x10n\\xbc\\xe5\\x8a\\xf4:\\xdea\\x1f<\\xc7Z.<\\xcd@G<\\xb2\\xbe\\xbd\\xbcD.\\x18\\xbd\\xae\\xdb%<U?G\\xbc\\x1bY8\\xbdr\\xd7\\xdf<V\\x91\\x19\\xbd\\x90\\x84!\\xbc\\xf7xM\\xbd\\x9a\\xf6\\xb8<\\x80\\xda\\xa1<\\x9f\\xbfh\\xbc\\x80_\\xbc<L\\xbcB\\xbc\\x90S\\x89\\xbd8\\x01\\x99=E&\\xca=\\x016m\\xbc\\x97~:;n\\x04\\x90\\xbc\\xdf\\xa2K\\xbd8e[<\\xcd\\x0b\\x87=DQ\\xbb\\xbd\\x9d\\x03\\x02\\xb8\\xc7\\x02R\\xbd4\\x03\\x16=\\x85\\n+=\\x07P5<\\xe6Xf\\xbd\\xcd\\xb5\\x93<Xa\\xbe\\xbb\\xc2\\x1a\\x1e<\\xea/R<\\xe2\\x0fJ=\\xa4\\xc8\\xfd<\\x14\\xa5\\xb9<\\xd1v\\x95\\xba\\xdem%=\\xa3|\\xde;+\\t\\xcc;\\x17\\xdf\\x9f\\xbd\\x902\\x1d\\xbd\\x07\\xd1\\x99<\\xe6\\x07J=V\\xd2\\xfa\\xbcU\\xaa\\x80=\\xe7\\xa3\\x9c=\\xfd\\xa4q<\\xb3\\r\\x15\\xbd\\x8bub<\\xabz\\xa9<+\\xf5$\\xba\\x18U\\x00\\xbd\\x9c\\x99\\x03=\\x86,\\x80;\\x83\\x81\\x0e\\xbd\\xc4\\xba\\x87\\xbdp\\xc4\\xa0\\xbc&\\xe6\\x92=\\xe49\\x1f=O\\xb3\\xdb<G\\xc0\\xfa\\xbc\\xed\\xf7\\xaf<=\\xd8\\x03< u\\x8a\\xbb6\\xd0D<P\\xa6\\r\\xbd\\xed\\xe1\\x03\\xba\\x99\\xd3O;\\xe5\\xe4\\xb5<\\xb4\\xb6\\xe9\\xbc\\x90\\x1cH\\xbd\\x10\\x98\\xe0<\\xc9\\xc8i\\xbd\\t\\xd3Q\\xbd\\x17\\x9c\\xae<\\xd0P\\xb6\\xbd\\x19\\xd9j:\\xfbU&\\xbc\\x8f\\xc7{\\xbca_\\x12=5\\xcf\\x11\\xbd\\x97;\\xa3=\\xd5Y\\xd6<\\x99\\n\\xd5<\\x82N)=!(\\xcd\\xbc\\\"\\x10&<\\xf6\\xad]=\\x88\\xc8\\x89\\xbc\\xfd\\x19_\\xbc9\\t\\x14\\xbd\\r\\x14E=3G\\xcb\\xbc^r\\xe0;\\x92[\\xa39\\xe4\\\"\\xe5\\xb9\\x80\\xfa\\xa5\\xbb$\\xe9\\x8a=P\\x92\\x12\\xbd\\xa8\\xe8\\x8a\\xbcE\\xf9\\x8a=\\xe6S\\\"\\xbc\\xbaf\\xa1<\\xe4\\x9f\\x9f\\xbcP\\xea5\\xbc\\x08\\x8d\\x02=\\x11\\xb0\\xf5\\xb8\\r\\xc8%\\xbd\\xd24\\xe7<\\xde\\x88h<\\x1d\\x95\\xe1\\xba\\x15\\\" =\\x8f\\x9c\\\\\\xbd\\xfeS\\xaa\\xbb\\xd4\\x1c!=\\xd5\\xb3\\xe8<\\xc2\\xaci\\t\\x91\\xdc\\x84\\xbbaL\\xaf\\xbcV?\\x17\\xbd\\x07\\\\|:W\\xef\\xc1;FMV=\\x07(\\x89\\xbc\\xba\\x8e\\xab\\xbcq\\xda\\x02<d\\xd5\\xfe\\xbc4\\x7f\\x85;\\xfb\\xd6h<C\\x93\\xaf\\xbbG)S=\\xcbJ\\x1f<\\xfe/\\xbf<\\x8bWl\\xbc\\xca\\xe3\\t\\xbd\\x11\\xfb\\x87<;\\xe0A=\\xda\\xfd\\x9e;\\xad\\xb62\\xbd\\x91\\x91\\xf7\\xbb`\\xf4\\xb2\\xbc\\xff\\xe7\\xee;\\x94\\xf2\\x8e\\xbcF<C<-\\xefk;\\xe6\\xc1\\xb4\\xbb?G}\\xbc\\x15\\xb9n<ieE<sQH\\xbd7\\xea\\x8d\\xbdc\\xea\\x03\\xbd\\x13r\\x9c<A\\xa8i;`\\x90 =\\xd2JZ=}\\xf9\\x18=L\\xdcU\\xbc\\xec$>\\xbc<\\x89O<+\\x95\\xf6<\\x05!\\x16=lB!\\xbd\\xc9y\\x81=)\\xec\\xcc\\xbbX\\xef\\t\\xbd\\xc1*+\\xbc\\xd6dF\\xbd\\x0f\\x95\\xa5\\xbd\\x9a\\x81y=\\xb6\\x12K=V\\xd5#<~\\x8e\\xfa\\xb9\\xc5n0\\xbb\\x8c\\xb0\\xde\\xba\\xf1y)=5\\xef[=\\xbd\\x19\\x0b=\\xef\\x08\\xb1\\xbb\\xa6\\xa0\\x0b<\\x11\\xc4m\\xbc\\x82vl\\xbdq^\\x84\\xbd\\x93r\\x02\\xbd\\xc9`\\xb7;\\x97\\x93\\x19=\\xcbj\\x0b=4:\\xe2;\\xedu\\xa3\\xbc\\xf1!\\x9b\\xbc\\x0c<\\xfd<jd\\xf4<\\x18<U\\xbdU\\xd4\\xae\\xbc\\xd2\\xb2\\x1d=\\xaa\\xba/\\xbd\\xd9\\x04@=\\xb8\\xbd\\xc9\\xbc\\\"e\\x83:\\xc7\\x92G\\xbc\\xf9\\x03\\xe4<\\xe4\\x15\\x96\\xbd\\xf2\\xdc{\\xbc\\xbf@\\xf4<Y/l=\\xd6QQ<\\xec1\\xb8<\\xdf\\xe1Z\\xbc\\xa5^\\x08\\xbd<+\\x87=\\xf0[\\x94:\\xf7\\xfa\\x11=\\xb1\\xf3\\xc3<\\xb1T\\x02\\xbd\\x19\\xfd+\\xbc\\xe9@\\x88;\\x96\\xa3\\xb6<\\xde+\\x0c\\xbd\\xee!\\xb5<\\x03T;\\xbc\\x88\\xb7\\x0e\\xbd\\xc3,\\xf0\\xbcqW\\xcc\\xbc\\xdd@\\xde<W.\\xbb\\xbc\\xadn7\\xbdm\\xa3K\\xbd\\x16\\x92\\x9e=\\xcb\\x99\\xfb\\xbc\\xd2\\xf8\\xf4\\xbc\\xaa\\xee3=v\\x00L=\\x98\\x06\\xf0\\xb9.\\x1f!<T`\\x16=\\x1d\\x0fa<I\\xb8\\x0e\\xbd\\xcae\\x03=,\\xe3*\\xbc\\xfa\\xd2h=.\\xa0\\xaa<\\x85^9=h\\xfdM\\xbc\\x00\\xd7\\x0e\\xbdp\\xde\\xb9<\\x99\\x04\\x81=3\\x1e\\x04\\xbd@_:\\xbc\\x13jJ\\xbd\\x91\\xe0~=\\xde\\xd04\\xbb\\xecQX=F\\xa8\\xb6\\xbc\\xbf\\x9a\\x8f=\\x8f\\xb1\\x81\\xbd]\\\"\\xaa<\\r\\x1aH;s\\xf4\\x1a;\\xfdB%\\xbd\\xfe\\x89*\\xbd\\x9184<>\\x15\\xcb;\\r\\xae\\xc8\\xbc\\xbe\\x93!=\\xaf_\\x8b\\xbc\\xb7\\xfc\\xbe<\\xadV\\x0c\\xbd3\\xb4\\x13\\xbdn\\x9c:<\\x84\\xee\\x15\\xbcK\\x04#=f\\xf4;<\\x7f\\xad\\x92\\xbb\\xabRd\\xbd\\x08%S;a\\xa9Y:\\xe3B\\xcc<\\xb3[\\r\\xbd\\xd1\\x97\\x0b=\\x9e\\x80\\xf9\\xb9>\\xd2r9\\x8bi\\xab\\xbb\\x0e\\\"M\\xbd\\xeb5\\x82\\xbd\\x11\\x95\\xa2\\xbb\\xe9zH=\\x87\\xe1\\x05\\xbd\\xadc\\n=3\\xf8\\xa4\\xbc\\xc4<\\xd5\\xbc\\xf9\\x81\\x08\\xbd\\xf2j\\xc7\\xbbF Y=\\xe3r\\xe7<\\x8c\\x8d\\xc1\\xbc\\xceT\\xad<\\xa4nf\\xbc\\xce|\\xee\\xbbE\\x9f\\x92\\xbd{\\xa3\\xf5<G\\xf9L<\\xe1+\\x91\\xbc\\x06O\\xef;\\xea\\x80\\xc0\\xbcF\\x86\\x04\\xbb\\xb1@\\x92<l\\xdd\\x1a\\xbd\\xbc\\x05n\\xbc\\xb2\\x88\\x1d=\\x05}\\r\\xbc\\x89\\xdb#\\xbd\\x880\\xbb\\xbd\\x00\\x00\\x87\\xbd4\\\\\\x12\\xbdN\\x05\\x1a\\xbdc\\x18f\\xbc\\xcbY\\x98\\xbaKSr\\xbb{\\xab>\\xbd\\x9a\\xbd\\x92\\xbc1\\xd8\\xab\\xbd\\xb5\\x04B\\xbd\\x80\\xa0\\x0f\\xbc\\xca\\x12\\xee<\\x17\\x84\\n\\xbd\\x8b\\x1a>\\xbc\\xb1\\x1b\\x10\\xbd\\x9a$\\xf5<\\x9e\\x90v\\xbc\\x9f\\xe0\\xe2\\xba\\x91\\x0b\\x12<\\xd7\\xd0.\\xbcBh\\x03\\xbd\\xe2v\\xcb<\\x80O\\xe4\\xbc\\x1e\\x85%\\xbd\\xba\\xd1J\\xbd/\\xe4G\\xbd:\\x94-<\\xf4\\xef\\xf7<oK\\xa2\\xbbo\\x1e}=D\\x99\\x91\\xb9\\x1e\\x9b\\xc4<\\x94\\x9ba\\xbd\\x94\\xfa[\\xbd2\\xad\\x95=\\xbd#\\xe4\\xbcN%\\xc5\\xb8#i\\xc2\\xbc\\x08~\\xb5\\xbbi\\xfc0<\\xbb\\x06o\\xbd\\xe9\\\"\\x9f\\xbb\\x04\\xeb\\xa4:R\\xaa\\x18=h=\\x92\\xbcz\\xe4\\xba<\\x88p\\xf3<i\\x0e\\xc6<\\x8bp\\x0c=\\xdd\\xbf\\x9d\\xbc\\xc1\\x80@\\xbcp\\xbc9\\xbd\\x00{m=\\xa3\\x19r\\xbd\\x98\\x8d\\x1b<\\xc9bB=\\xbb\\xc8,\\xbc\\xf9\\\"J\\xbc\\xcdaC=\\x8b\\xcf\\xd6\\xbc\\xe6Q\\xff<\\x1a\\t7\\xbd\\xa6~\\x9c:\\x9d^H\\xbcw,\\x9f\\xbdG\\x93\\xc7<\\x8d\\xb1M=;\\x8c\\x83<\\xe5\\x15\\x84=L\\x18\\xac=\\xd0P7\\xbb\\x98X\\x83=\\xe3\\xe8/=\\xc7\\xe3\\x9c\\xbc\\xa7}}=\\xf5`\\xca\\xbcL\\xc7\\xc0<\\x1177\\xbb\"\nHSET bikes:10096  model 'Iapetus' brand 'ScramBikes' price 1801 type 'Mountain bikes' material 'alloy' weight 11.1 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost.' description_embeddings \"Q\\x9e:<\\x01g\\\\<\\x8c\\xf6\\x1e\\xbdv[0=D\\xdf\\x87<8\\xb9:=\\xa5\\xa9Y;J\\xb4P\\xbd\\xe4\\x0c\\x7f=<\\xea@<\\x03E\\x04<u\\xd5g=R\\xa5\\xdb<&\\xaf\\x119*\\x0c\\xa7=\\xcd&=\\xbd\\x12\\x00/\\xbc\\xbc\\xfe\\xeb\\xbcy\\xa6U\\xbb\\x83\\x1e\\xba\\xbck\\xc3\\x87\\xbaM\\xe7\\xa8\\xbc\\x9a\\xfa\\r\\xbcM\\xd5\\xd7\\xbc\\x05\\xa7\\x0f;\\xa8\\n\\x1f=\\x82\\xe3\\xd8<\\x7f\\xae\\xdf\\xba #;\\xbd)3\\x1e=\\xf8L\\x8b<N\\xc7\\x16<\\x968\\xba<\\x12\\xb9\\xa1<\\x84z\\t=S\\xed\\x11<\\xf9\\xb5\\xcc<E\\x9e\\x00\\xbd\\x06\\x12\\xd7\\xbb\\xd1\\xe3b9\\x89\\x9e\\x9a=\\x9d}\\xdb\\xbc\\xafu\\x0b\\xb8\\xf9\\xf8\\x7f<\\xb5g\\x1c\\xbc\\xe7\\xc8l\\xbcj\\xd7\\xaa=\\xcc\\x02Y\\xbd\\xc0\\xee\\xcb\\xbc(u\\xc7\\xbb\\x8f\\x94\\x7f\\xbc,w%\\xbd\\x00\\xc0\\x91\\xbb;\\xbd\\x81\\xbcq\\x81\\x9d<\\xd0\\xdbo\\xbc\\xe1\\x9c\\x08<pk\\xb2\\xbdoR_\\xbd\\xf6\\xb1\\xc3=\\x1c\\x07\\xd6<\\x01\\x03\\x92\\xbc\\xcb\\xe1\\xd9<\\xc8C\\x94=\\xc4\\xf1#=\\xd5\\xb9\\x00;\\xc0E\\xb7\\xbb\\xa4\\xd4\\xac<N7\\x8f<\\x89\\xa4\\xfb\\xba\\t\\x82\\x1d\\xbc\\x04I\\x95\\xbc\\xb6D\\x16\\xbd\\x9f\\x0f\\xf1\\xbcdh\\x1f\\xbd\\xd5\\x9e\\x04<X\\x00\\xea< \\xbe\\xf9\\xbc0\\x1dk\\xbd\\t\\xa3\\xa9<\\x7f\\xc8 =do\\x81\\xbd\\\"\\xfc\\x1b\\xbd^g\\x85\\xbd\\xdf\\xa2.\\xbc~\\x0b\\x8a<\\xb7\\xa4\\xef;B\\x7fN=d\\x18\\xec<\\xf0\\xd7\\x02=\\xb6\\xcd\\xbf\\xbc\\xc3.U\\xbd\\xebI\\xf3\\xbc/\\xdd\\x83<\\x107\\xce<e\\xc5L=&\\x9bt\\xbc\\xcd\\xe3\\x1d\\xbdP\\x0eS<\\xdd\\x03\\xed\\xbc\\xf6\\xfa\\xf6\\xbc5/\\xf2;\\xa3\\x9c\\x02<\\x91\\xfc\\x0e\\xbd\\xb1\\r\\xbc\\xbco\\xec\\xd2=\\xb9\\x96\\xb8<9\\x84g\\xba~_\\xd1\\xba\\x06\\xf3\\x03=\\xd7Wd<\\xcfp\\x84<\\x019\\x1e\\xbc8I+\\xbd\\xf2\\xae<\\xbd\\x15w\\xff\\xbc\\x12\\x05\\x95\\xbb\\xc2_\\x1c;\\xfb\\x18\\\"\\xba\\xd2\\xfcP\\xbd\\x1b\\x02\\xd5<=\\x8c\\x01=\\n\\x8d\\x83\\xbc\\xed\\xde\\r<\\xc0\\xb5\\xcc;\\xa9\\xd5\\x08\\xbdk\\x94\\xa7\\xbb\\xcfh\\n\\xbda\\xbf\\xa6\\xbc\\xb5\\x8f\\x81\\xbd\\x92\\x08\\x12\\xbdrx\\xf9\\xbc\\x19\\x10\\xa4<0\\xf2\\x15\\xbd$\\xcb\\x9d<\\xa5\\xa6^=\\xf4M\\xe9\\xbcu\\x9eM9^\\xb6\\x8d<\\xf4<\\\"\\xbdp-7\\xbdn\\xa2\\x9a\\xbd\\x90\\xe6f;N\\x83\\x81=>\\xbd9\\xbc\\xb5\\xd8\\xf8\\xbc\\xd05\\xe7;\\xa3\\n\\n\\xbdO\\xbdw<\\xda\\x00\\x1a\\xbd\\x0e!*\\xbd\\xb4\\x1b\\x97=P\\xd3\\x87\\xbc\\xfef\\x08=\\xb9\\xf2\\xcd\\xbde\\\"\\xfe\\xbc\\xac.\\x9a\\xbc\\x1d\\xcb\\xdd\\xbb\\xc7SM<(\\x90\\x93\\xbd\\xc9\\xbe\\xbc=jt\\xac\\xbc$i\\x0e\\xbc\\x91\\xc0*\\xbd\\x1fl\\xcf<\\x1a]\\n<\\xd2\\x87\\xcd\\xbcY\\xf2\\x18=\\xf8}d=\\xeb\\xe62\\xbb+*\\xf5\\xbcy\\xc2f=*C\\x14<\\x1b\\xc6\\x96\\xbcv\\xf5\\x8c\\xbc\\xe1\\xf6\\x11=p\\xb8\\x99\\xbd\\xe4\\x176=\\xa1\\xcb\\x9b<}-T\\xbc\\x18\\x80 <\\xce\\xc6\\x12=\\x94\\x9aF\\xbb\\xedj\\xc99\\x87r\\x0f=\\xa4\\xa0\\xc3<\\xa7\\xf0\\x91=\\xd0\\xee\\x8e;\\x0b\\xd5\\xb5<\\x85ed\\xbd\\xbc\\x8e\\xea;-\\xdc\\xfa<\\xb5w\\xa6\\xbc\\xef\\xf6\\xc3<\\x0e\\x03\\xb5<\\x0e\\x97\\x9a<\\xdc\\xa8z\\xbdM\\x1a=\\xbd6\\xf6u\\xbd\\xc8\\x86\\x8e=|\\xa1\\x1d\\xbd\\x81S5\\xbd\\\\o\\xc3\\xbd\\x1f\\xde\\xd3:\\xb3\\xa3\\x91<\\xbe$/<2w\\x10\\xbdn\\xc1\\xd4\\xbc\\xf6\\x84\\xe8<ns%<\\x19\\xfa\\xbd\\xbb\\xdfi\\xb8\\xbb\\x92PN\\xbc{/\\xa1\\xbc\\xdam\\xfb\\xbcyX\\xfc\\xbb\\x81\\xc8Z<\\xb4\\x8cV\\xbcJ\\x1b*=?\\xe9\\xab\\xbd\\x87F\\xcd<j\\xfb\\x15\\xbc\\xd0m\\xfa\\xbc\\xd89\\xef;\\x96\\xc1\\xa7<m\\xe1O\\xbdO\\x1b\\xda<O0\\x7f<\\x95l.=\\x9cK\\x9a\\xbd\\xfa4\\xa3\\xbc\\x12\\x0e\\x8f\\xbd\\x005\\xfd;\\x03(\\n=\\x82o\\x99\\xba\\xe5\\xcf\\xd4\\xba\\x13(\\x1c=|\\xe9d<E\\x05\\xa6\\xbc\\xd1kJ<&=W\\xbd=k\\xcf\\xbaG\\xab3=!\\xb5\\xb3<Vo\\xec<\\xa9,)=\\x01{0=]\\xb6\\r;B\\x11)\\xbd\\x08\\xd7\\x8b\\xbbF\\x1eA<\\xd2\\\"f\\xbd\\xff\\xb4q<s\\xea\\xa1=P\\\\\\x85\\xbd\\xd4Ji=\\x9b\\x95\\xd7\\xbc\\xaa\\xd3\\xcf\\xbc\\xc4\\xbe\\xcd\\xbc\\xc3\\x10\\xa4=\\x91n\\x81<\\r\\xa2\\x02\\xbd\\xf6\\xe2Q\\xbd\\xc13\\xdf\\xbc\\xd0\\xcc\\x01\\xbd\\xba\\x0f\\xab=D\\x7f\\xa0<\\xb3\\xba(;\\xd6no=@((\\xbc\\xdcPw\\xbb\\x97\\xd6\\x01<\\xea\\xc4\\xce\\xbcSk\\x02=\\xdc\\xafD\\xbd$\\\"\\xa3\\xbbe\\xe4\\xe6<\\x8b\\x95\\xfb<&\\xdc\\xaf<\\t0\\x95\\xbd\\x10q\\n\\xbd\\x99d\\xb7;\\xde\\xaf\\xa6\\xbc\\x0fC\\xbf;wQ#;\\x8e\\xc3\\xcb\\xbc\\xd4e\\x13<\\xeah\\x83<\\x00\\xddq\\xbd0z\\x11<\\x8c\\xdcM<\\x13\\xd0R\\xbd\\x80\\x01g=\\xdc\\xfen<^\\r\\x01=.\\xfcK\\xbdJ\\xe5\\x88<O\\xdb\\xac;\\xea\\xdd\\xb0;\\x95\\xb1\\xaf;\\x02$\\x97<p\\xb0\\xb6<\\xe0\\xf0t=\\xe3K\\x06\\xbdl\\xb30=`\\x17\\x01=\\xaf\\xe7\\x1c\\xbc\\x1b\\xb9,=\\xd0\\xd9\\x88=s\\xb1\\xd4:Z\\x86\\x00\\xbd\\xe8J\\xc0\\xbc^`I\\xbc\\xf7\\xfd\\xfd\\xbc\\x1c2|<\\xf0\\x7f\\x9a\\xb8@}\\xdf\\xbc\\xd7\\xf0\\x9b=\\xb0\\xdb\\x82<\\xec\\x1e\\x0c<f\\xb4\\xca\\xbcW\\xb2r\\xbc\\xacu\\xd5\\xbc\\x1d\\xc4B\\xbc\\xccZ\\xf69u\\xa3=\\xbd@\\xe0\\xca;\\xbc_\\x98\\xbcG\\x0b\\x80<\\x0e\\x89y\\xbc\\xcch\\xfc\\xbc\\xeeI^\\xbd\\x0fA\\x0c\\xbc\\xd6}\\x8c\\xbcY^\\xc3;\\xaa\\xf2\\xf9\\xbb\\xf7\\x14R\\xbd;i+==\\xa2<<\\xdb\\xc6\\xf9\\xbb\\x91\\xba/\\xbd\\x1f\\xc6#<\\xb6\\xc8\\x89=\\xc0\\xb9\\xee\\xbb\\x90:W\\xbc$\\xa1\\xd2<o_\\xdf<\\xf3\\x1f\\\\=8\\x84T=\\xd7\\x82\\x02<\\n\\x1e\\xd7\\xbcP\\xf8\\xa9\\xbc\\xf47N=\\x01\\xfd\\xa4\\xbc\\xc1\\x99\\xfa\\xbc\\xcar\\xdd\\xbc\\x7f\\x0b\\x99<\\xf2*]\\xbd\\xb0\\r\\x8f\\xbd\\xf1\\xe0\\xc1\\xbd\\xd7\\x88r<cP\\xde<\\xf4\\x0e\\x9e\\xbc \\x14\\xc6=\\xc8\\xc2\\x93=\\xee?\\x86\\xbc\\xf9\\xc7\\r\\xbd\\x03\\x8ag\\xbcy\\x9c\\xb0\\xbc\\xb9\\t\\x99\\xbc\\x9a\\x1b\\xb4=q7\\xea\\xbb\\xb1\\x8bl\\xbb\\xfd\\x19\\xc8<\\xb2\\xe2\\xa1<1|U=\\xfb\\xabb<\\x83\\xbdK\\xbd\\x08\\xad\\xad;\\x13UB\\xbb>&-\\xbd(b\\x87\\xbd\\x07\\xa9\\xd6\\xb9\\xfe\\xf1\\x82<\\x06\\xadF\\xbd\\xc4X\\x04=\\x80\\xcb\\x84\\xbc.\\xc9P\\xbc\\x12\\\\T=\\xe87\\xd1\\xbcq\\x93\\x8a\\xbdbL}=\\xe4a\\x95=v\\x17\\x9e\\xbcj\\x9f\\xbb\\xbb\\x9e\\xd6A;\\xab_n\\xbd%\\r_=\\x1f{#=\\xa0\\x8fz\\xbd\\xdcFL;\\xee\\xdb-\\xbd\\x00\\x07\\x06=\\xbb\\xbf\\xef\\xbc\\xf3\\x00\\xa4<d:\\xfa\\xbc\\xa6\\x1f`=\\x99\\xa7\\x8d<\\x0b\\xefw<AT\\x0c=\\x91\\xb0`<k\\x05\\x15<_\\xe7\\xfe<\\xdbu$\\xbd=:x<\\x85\\xcfD=,\\xb4\\xc7<J\\xd7\\x87\\xbdI\\xc0y\\xbcx\\xf9R<\\xf4A\\x9a:\\xecR\\x9a\\xbcC}y;\\xd1\\xf5\\x06<\\xe5\\xb6\\x9a<2\\xe5\\x12\\xbd\\x84y\\xa4\\xbc\\x896\\xca<3n\\xa4\\xbb!\\xd0\\xde\\xbc\\xf41\\x1f;\\xc4\\xb7\\xaa\\xbcR\\xf3\\xe8\\xbc\\xd8|W\\xbd\\xdd\\x03\\x03\\xbd\\xf2_\\x85=\\xd2>\\xe5<\\xaa\\xce\\xfc<\\xd1T\\xd9<\\x8eo\\xb6<\\x109\\x96\\xbc\\xbbz\\x16\\xbd\\xb1\\xbc\\xe4\\xbc~( <\\xb3\\xa7t\\xbc\\xf3\\xb7\\x97<\\xded\\xf1;52\\x9e<\\xfb\\x16\\x15\\xbd\\xab\\x9a\\\"=\\xc2\\xfb\\x9b\\xbd\\x943\\x87\\xbd\\\"M\\x13\\xbd\\\\\\xeb\\x82\\xbdg\\x86v=\\xd6\\x0b\\xb0<\\r\\x1c2\\xbcp\\xcd\\xc4\\xbc<#!<\\x9d\\xaa\\xd1=\\xda\\x00\\\\=[d\\xf7\\xbc\\x14\\xe86=\\xb2]\\x17\\xbd\\xccrx<\\x03m\\xf4<\\xae\\xf1\\x8d<*\\xa2:\\xbd\\xdd\\x13:;g\\x04\\x1b<\\xaay&<\\xf0\\xfe\\xce<^\\xc7\\x92\\xbc\\xf5@\\xf2\\xbc\\xea\\xa7\\x9a\\xba\\x0f\\n`<5?\\xd5<r\\xd5G\\xbb\\xbd\\x8f\\x80\\xbc~\\xc7\\xcb\\xbc\\xba\\x96\\x86<p#\\xf5\\xba\\x0eC*\\xbc\\x84\\xc2\\xb3;\\x9d\\xfdG\\xbc\\xd7\\xfc\\xd5\\xbdm\\xc2\\xf7\\xbc/81\\xbc\\r~\\xdf;N\\xf0@<a\\x16\\x9e\\xbc \\n\\xc9\\xbb;\\xdac=\\x7f\\xe9\\xbb\\xbbu\\x9fM\\t\\xd7\\xc1\\xe2;\\xa5_\\x9b\\xbc?\\xd5\\xb3<\\xb4\\x13\\xc4<\\xd4\\x14D;hXj=\\x17=\\x82\\xbc\\xc8\\xd6\\x0f\\xbc`c\\x96<\\x01\\xac\\xed:\\xad\\x1b\\xd2<\\x19\\x1d\\xa2<(i|\\xbc\\x88]A=J\\xdc\\xb9\\xbbV\\x97\\xa2<\\xcf\\x1d^\\xbd\\xe3\\\"\\xad<\\x84\\x93\\xa5<\\xb4\\xbe\\x8c=\\xdd\\xf0\\xd3<!Fh\\xbc?\\xd0\\x17\\xbd\\xd8\\xf2Q<\\xd5\\xce\\xfd<\\xff\\xfd\\x85<\\x8cTS\\xbc0\\xc0\\x8d;\\x83\\xdc\\xd8\\xbbr\\x02X\\xbc\\xf5\\xc4\\xb9<O\\xc68<]\\xb28\\xbd\\xc4\\x01\\x98\\xbd\\xc4\\x02?\\xbd2\\xf4\\x0f\\xbc\\x96q\\xd2<N;k<\\xeehJ=\\x16(@<D\\xd25\\xbc(S\\xd1\\xbc]\\xc1\\xc0<)\\x81\\xe3\\xbc\\xc6\\x9c\\x86=\\xf3\\xc2m<`l\\x8b=f@?\\xbc\\xe7p?\\xbd\\x962<\\xbc\\xb8\\xd8y\\xbdE\\x15!\\xbdh\\x11\\\"=\\xf2\\xae\\x95<\\xa8\\xcb\\x80<\\x97u\\x99\\xbbm\\x81\\xb4\\xbc\\xb0\\xf3\\x8b=\\xda\\x12+\\xbb0<d=\\x05\\xa6\\xcb;`\\x11\\x99:h\\xb4\\xb4<\\xf7\\xe9\\xe4<\\xad\\xf4\\xc5<{\\xaeU\\xbd\\xd2RB\\xbd\\xcb\\xb8\\xc9<\\x0b\\x14\\x02=2\\xae\\n\\xbc6\\xc4\\t=U \\xa7\\xbc\\xb7\\x11\\xdb\\xbcsw\\xa8<\\x0b\\xc5U=3\\x071\\xbd\\x11sh\\xbc\\x88@>=\\xb1\\x932\\xbd\\xeb\\xa9M=\\x7f\\xfc\\xc7\\xbc\\xc1\\xb2\\x08<0-\\xbd;_\\xb2a=D\\xef\\x89\\xbc\\xe8\\xbe\\x01=\\x93\\x8c]=\\x97,\\x89=k>U\\xbcAF\\x97<\\x1a\\\\\\x9b\\xbc\\xbb\\x9f\\xb3;\\xfe?q=<\\xca\\x19\\xbaZ\\xd6\\xed<\\xd1w/=\\xc5\\x87\\x06\\xbd\\xd9\\xa7U\\xbc\\xe2\\x17\\x90\\xbc\\x1a#\\xe0<\\x91\\\\\\r<\\xbaK\\x89:\\xc3\\x8f\\x1a<w\\xfas\\xbdT\\xa1\\xca<P_\\xd5\\xbc\\x01\\x1c\\xbe<\\xfd|\\xb9\\xb9\\t\\xa8\\xfb\\xbd\\x98-\\xe1\\xbc\\xdb\\x11C<\\xd8\\x16\\xd7\\xbcDw0\\xbd\\x1c\\x93\\x87=\\x87R\\x04>\\xc0\\x81\\xf8<\\xa2X\\xe9<\\x9f\\x83\\xce\\xbc\\xf0\\x8f\\x95<\\x06\\xb6\\xc1\\xbd\\xfc\\x17)=\\xce9\\xa8<\\x93\\xf0\\xb0<\\xd4\\xe8\\xe6<\\xabH{=)\\xf0<:\\x11k\\x98;\\xca<\\xc4;\\x18\\xf2\\xc9<&\\xb5\\x96\\xbd\\xdc8\\x14\\xbc\\\\K\\\\\\xbd4j\\x99=\\xbe\\x19\\x12\\xbcR\\xf7\\x86=X8=< \\xdb\\xf9<\\x06/v\\xbdq\\xfeQ<::\\xd8<\\xc8\\xb1\\x0c\\xbc\\xff\\xfc\\x18\\xbd\\x98*\\x80\\xbb\\xca\\x1f\\x85<\\x11\\xecf<\\x94|:\\xbc\\xd0\\x7f\\xf1<W\\x7f\\x84\\xbc\\xa8\\n\\xdc<\\xea\\x94H\\xbc6\\x8c\\x91\\xbc\\xb4\\xeeF;\\xbc1e\\xbb\\xb4\\xc54=\\xd3R>=\\xa6\\xc0\\x11<\\xda\\n\\xaf\\xbc)sd=\\\\2\\x87<\\x82\\xbb\\xe3<\\xef\\x10w=\\x1228\\xbc\\xe9\\xf4\\xd6;\\xd4d\\x7f<\\xb5\\xc23<CO3\\xbb\\x00}U\\xbd)e\\x19=\\x9aq,=\\x92\\xb1\\xa7;!\\x13N;cB\\x1f\\xbd\\x88\\x98\\xc1;\\xc8\\xa5\\x81;\\xc3\\x06/:\\xf5Z\\xa4=\\x14\\x01g<\\x8cc\\x10=h\\\"\\x97;]\\x8b\\xab<\\xfb\\xc0\\x8a\\xbd\\t/\\xe6\\xbd\\xf7\\x1b/<\\xe5b\\xd6\\xbc\\x0fm7\\xbd\\xeb;#\\xbd\\x12Hu;\\xd8\\xff7<\\xda\\x9b\\xfc<\\x8ci\\xe2<\\x1a\\x93;\\xbd^\\x13\\x8e<\\x9cQ-\\xbd.\\x16\\x9d\\xbcQ06\\xbdZu(\\xbd\\xee\\x9b\\x88<\\xe0 \\x96\\xbc\\x8f|\\x81\\xbcm\\xf8\\xf1\\xbc\\xa5\\x07\\x0f\\xbd\\x04u>\\xbc\\x99\\x98\\x06\\xbd\\xd8e\\x9b\\xbd\\xe3\\x18^<\\x19O\\xc2\\xbc\\xe7X\\x08<\\xb7t\\xb2;\\x9b\\x13k\\xba5\\xd7\\x06\\xbd\\x1dd\\xf0<al\\x11<\\x828\\xbb<\\xa9!_\\xbc\\x9d\\xfb\\x10\\xbdc\\xbc3\\xbc\\x86T\\t<x\\xc9\\xad\\xbcXZf\\xbd\\xbemM\\xbd<\\x07\\x18\\xbd:\\x8c\\xe8\\xbc\\x97\\x83\\xb2\\xbb\\xac\\xfcC=/\\x1c\\xb5=\\xdf\\x00\\x80\\xbcD\\x82\\xad<\\x84Dq\\xbd\\x90\\x1d%\\xbd7&\\xe4<1y\\x12<\\x96\\xc2\\xf7<\\xc1\\xe7H\\xbc\\x7f_\\xa9\\xbcg\\x0f\\x86<\\x9f\\xd3\\xa2\\xbd\\xf4\\n-<\\xc2\\xc1\\xd7;\\xe6\\xf4\\x11=\\x7f\\x06\\x91\\xbc\\xc7\\xef\\xfc\\xbc\\x80\\xaa\\x00=\\xae\\x88f<BX5<\\xa5\\x01\\xf0\\xbc\\x12\\x8a\\xbc<\\xd2\\xea\\x90\\xbcb\\x1f\\x8e<\\xb8\\x12\\xe4\\xbcWm\\xab<\\x1d\\x06\\\\=X\\x93\\x97;\\x0c\\xc4\\x0b\\xbc&\\x85\\x86=\\xab\\x89\\xf7\\xbcF\\xea\\x9b\\xbc\\x9bej:d\\xfa#;\\xc3\\xf0\\xa4\\xbc1D\\xa9\\xbd&;\\x10=o\\xf5\\x86<\\xab\\xf8\\xec<\\xcb-\\xa1<\\xd7\\xb4\\xcb=2\\nR\\xbc\\x1e:T=\\xf1\\x0b\\xb3<\\xe7\\x82\\xa1\\xbc\\x83\\x92\\x11=\\x83\\x9dC<;\\x82\\x11=X\\xee\\x84\\xbc\"\nHSET bikes:10097  model 'Millenium-falcon' brand 'Tots' price 3027 type 'Enduro bikes' material 'aluminium' weight 14.4 description 'Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we\\'ve ever tested. It has a lightweight frame and all-carbon fork, with cables routed internally. That said, we feel this bike is a fantastic option for the rider seeking the versatility that this highly adjustable bike provides.' description_embeddings \"\\x14c\\x07<\\x84B\\xf2<px\\x1c\\xbd\\x86\\xc7\\xec<\\x8e\\x07\\xc1<\\xeaT\\xb2=\\xab&\\xcc\\xbc\\xb8a\\x80\\xbd\\xdc8J=\\x10\\xab\\xb5;\\x9f(E\\xbci\\x8e6=\\xda\\xaa =\\xb0W#\\xbcF(\\xa0=m\\xe8\\xa0\\xbd\\xd80\\x0e;\\x83$7\\xbc\\xa6\\xdf\\xf2\\xbc\\xca\\x89$\\xbdf\\x99\\x14<\\x1e\\xce\\xc7\\xbc.\\xd5\\x82<nD\\x00\\xbd\\xde\\x9c\\x01\\xbc\\xd03\\xad\\xbc\\xee\\x15x<\\x96\\x15{</\\xf6\\xfb\\xbc\\xd8\\x100<\\x7f\\xfc)\\xbb9\\xe5C\\xbc\\xf6^\\xe3<\\xb4B\\\"=\\xdbK\\xca<\\xcf\\xdb\\xaf\\xbb\\t\\x88\\xd2<\\xb3\\x9d\\xbb\\xbcm\\xa9a<\\xa3\\xac\\xa4\\xbb`\\xdap=:z\\x8f<#\\xa1\\xe4<\\xf7\\\\\\xa0<\\xd9v\\x8b\\xbb\\x00\\x92~\\xbb\\\"\\\\R=\\x87\\xd6\\xbf\\xbc\\x14\\\\\\xd4\\xbc7\\xa6\\r\\xbch;\\xa0\\xbc\\x96\\x0b>\\xbdv\\xb3n<\\xc3W\\xd1;#\\x9b)<\\xc4\\xc4U\\xbd\\xb3\\xcb\\x97;\\x9d/\\xcd\\xbdh\\x12\\x88\\xbd6\\xf6\\x9b=\\x99p\\xf3<G\\x14\\x9a<\\x89Js<\\xcd\\xea\\xac=vE\\x12=<\\xb1\\xe2<\\x91\\xf6\\x9d\\xbb\\xff\\xb7\\x7f\\xbcB\\x01\\x91;\\xbb\\x1e\\xf2\\xbc\\xc9\\x92<\\xbbu\\x97&\\xbc\\x93OU\\xbd\\x92\\xc6\\xff;\\xa1\\x8aQ\\xbd%^&\\xbbctr\\xbb\\x13\\xfe0\\xbd\\xa9q\\x0e\\xbdY,x<-t\\xd0<\\\\l@\\xbdUe\\x81\\xbc&\\xc1\\xec\\xbc\\x7f~\\xc5;\\xb1\\xd8\\xd3<@\\x9c\\x86\\xbb\\x89\\\"O=P\\xd3\\x1d=\\x07n\\xa0<\\xa2E\\xb8;-\\x9a\\xdc\\xbc\\xfc#\\xec\\xbc\\xeb\\xa3\\x01\\xb9\\x16Z\\x11=\\r\\xd3\\xab<\\xa1O\\x07\\xbd\\t&\\x15\\xbd\\xd4[\\xac<\\\"\\x8b\\xbb\\xbd+Y\\x16\\xbdNQ\\x08;\\xc6p\\xc7<\\xc3\\x17E\\xbcl\\xf0b\\xbc\\x11:\\x97=X\\x1e\\x03<\\x9d\\x07\\x83\\xbb\\x83\\xed\\xcf\\xbcS5s9\\xb6\\x86\\x8b\\xbc4\\xbf\\x8d=u\\xeb\\xbd\\xbb\\x84\\xaa\\n\\xbdx\\xefa\\xbd\\x9e\\\\+<\\x91\\xc3\\x80\\xbbwj\\xca\\xbc|\\x1b\\x0e=(\\xbf\\x9b\\xbc\\x8f\\x00;<\\xbd\\xf6%=IH.\\xbd\\x94\\x9eX\\xbb\\xe6<\\xe4<\\xbbX\\xea\\xbc\\xfd\\xb6\\xcc91hZ\\xbc\\xa96\\xb9\\xbc1\\x83v\\xbd9\\xcc\\x01\\xbc?\\x8d!\\xbdS\\x90\\xdc<\\x82\\xc0T\\xbc7\\x8b%\\xbdr\\x0e\\xb3<>,-\\xbc\\xf9\\x82\\xb4\\xbb(\\x93\\xf1:\\xbe4\\xa5\\xbc\\x9c\\xa6\\xc2\\xbc\\xab\\rD\\xbd}s*\\xbc\\xfd\\xb3u=\\x0e\\x83\\x91<\\xc9\\xd1A\\xbc\\xff\\xed6<\\xf2w\\xff\\xbc\\xfd<\\xe5\\xb9\\xc7\\xcd>\\xbd\\xe1\\x92\\xea\\xbbs\\x92\\x11=\\xe5\\x02\\xc5\\xbc\\xba;\\xdf<Q{*\\xbd(!Q\\xbd\\x12\\x841<a\\xca.<\\xfc;#=\\x88\\x02\\x84\\xbd\\x92\\xca\\xa9=\\x01G\\xbd\\xbb\\xea\\x94\\xd3\\xbb\\xady|\\xbd\\x95\\xde};\\xbb`\\xb8<Fn\\xdb\\xbc\\x13\\\\\\xd6<\\xe4\\xd0==\\\\<U<\\xc4\\xa4q\\xbc\\xc1x@=\\xee\\xc8G<\\x87KE\\xbd\\x01\\xab\\x1d\\xbc\\x0c\\xce\\x99<i`k\\xbd\\xa9QV=\\x12\\x03\\xde;C\\xc47\\xba\\xae\\x81\\xd7\\xbb~\\xf30=\\xc6$D\\xbcD\\xda@<^4t=\\x96\\xdf\\xd7;}\\x9d8=\\xb3c\\x88<\\x8e\\x07\\xda<\\xef\\x14c\\xbd\\xaa\\xc1\\x11\\xbc\\x0b\\xebK=\\xa6`\\xfe\\xbcy0\\x8d=\\x1a\\xd0\\xe7<\\xaaP\\xaa<\\x93\\x15]\\xbdp,\\x07\\xbd\\x98iY\\xbcp\\xce\\xc9<\\xb4\\xc1\\x07\\xbdh\\x92V\\xbdj\\x94!\\xbd\\xc9\\\".\\xbdTn\\xa9<\\x1f\\xf7\\x91<\\x97\\x8c\\x83\\xbc\\x9e\\xc9\\x18\\xbd0Y\\x13<\\xa8\\x17\\xa4\\xbc\\x02B8\\xbc\\xef \\x97;@\\x0e\\x90;FyY\\xbdu-\\r\\xbd\\xdb\\xd4b\\xbc\\x99X\\xfe\\xbc\\xf8~0\\xbd$\\xdcc=\\x1f\\xb0\\xd1\\xbd\\x1a\\xff\\x85<}cc\\xbc\\x1ePX\\xbc\\xb2\\xf0\\x81<\\x03\\x1a\\x99:\\x16\\xdcq\\xbd\\xb6m\\xe8<\\x9dF\\x19<\\xb8&B=5\\\"\\xb1\\xbd\\xb5P<\\xbdo\\x00\\xbe\\xbcF\\x16\\x91\\xba\\xf4\\xb7\\x1f=>Z\\xe0\\xbc8_\\xdf\\xbc\\xbd\\x10\\xc1<\\xcd\\xa3\\x06<\\xcf6\\x8f\\xbc{t\\n\\xbcm\\x1f\\x17\\xbc\\xf7-\\xb4\\xbc\\xfe\\x0bc=d\\xc1\\xbf;\\xdb\\xce\\x86<;\\xd3\\x9f<\\xce\\xcf)=\\xef\\x9d\\xc0\\xbb\\\\\\x16\\x13\\xbd\\xb2\\x99\\x07<%\\xde\\xb8<\\r&\\x98\\xbd\\xb6(W\\xbcX\\x91\\x9b=\\xf5_\\x9b\\xbde\\xc7f=\\x08\\xd6K\\xba\\xd0\\xb0\\x13\\xbc\\xaf\\xc7q\\xbd\\xc2\\xa4W=D\\x1f(<4`\\x9d\\xbb\\x1b\\t\\x00\\xbd\\xe0V\\x9c\\xbc\\xd2*}\\xbd\\xbcQ\\x93=-d\\xd1\\xbb*\\x9b\\xa5\\xbc\\xce[\\x1c=\\xd3\\xd1\\x86\\xbcB\\xc9d<%xB\\xbc`\\xfd\\x0b\\xbdk=\\xdb<\\x06\\r\\xab\\xbbS<\\x14\\xbc\\xb6\\xf5\\x06\\xbc\\xe8\\xcey<\\x82\\xb3\\x88\\xbc\\xc5\\xa6\\xac\\xbd\\xab\\x9f\\x97\\xbc\\x07\\x8b\\x83=\\\"d\\x9b\\xbc\\xf0\\xb3\\xed<\\\"<\\xe3\\xbb*\\x8a{\\xbcC\\xc2\\xec\\xbc\\xcd\\xf1\\x03=\\x06\\xc7\\xb0\\xbdlS\\xbe<\\xbdm\\xbe\\xbc\\\"\\xf4\\xee\\xbc\\x12\\xcb\\x91=\\x80\\x8c\\x96\\xbch|\\x1a\\xbc\\\"!E\\xbdB\\xe0\\x15=\\xff\\x14\\xcc<\\xc6\\x85z<hd6\\xbd\\x0b\\xf8\\xc68G\\n\\x13=\\xachh=\\x06\\xccZ\\xbc\\\"\\x80\\t=kc\\x03=\\xb4\\xe2\\x82\\xbc\\xe0\\x016=:\\xb8\\xa0=p+q=_\\x83\\xe9\\xbcVm\\xb7\\xbc\\xc0\\xc0j\\xbc\\x93\\xf2\\x16\\xbd\\x96\\x8f\\x0c=\\x1baP\\xbdL\\x13\\xda\\xbc\\xdb4\\xa2=\\x81\\x03\\xb1<\\x95\\xfb\\xc9;\\x8c\\x19=\\xbcPg~\\xbd;<\\xea<f\\xec\\x06\\xba\\x830\\xe4\\xbaD\\xf5}\\xbd\\xd5\\xeaX<\\xe3\\x85\\x80\\xbd4j_<]p\\xb9\\xbb?\\xb3<\\xbc\\xdea]\\xbdI\\x8f\\xf9\\xbb\\xe3\\x03\\t\\xbc\\xa9\\\"A<\\xdf\\x93\\xfa\\xbb\\xfb\\xfbB\\xbdg\\xa0,<\\x1a\\x98\\x88<\\xd9\\xaa\\xc0\\xbaf^ \\xbd\\xe0&\\x8c\\xbb\\x1e\\xa1\\x99=D\\xfd\\xe1\\xbb\\xc9\\x1f\\xfd;\\x94\\x1bM\\xba\\xfa\\\"\\xc3=\\xb4\\x80\\xa5<\\x1d0\\x9d=n\\xb3\\x7f<[\\xa9\\xd7\\xbc\\xd2!\\xd8\\xbc\\xf9k8=\\xbb\\x9c\\xbc\\xbc\\x1aN\\x1d\\xbd<d\\x07=\\x04\\x05\\x80;K\\xe3\\xf8\\xbc\\xf7^&\\xbd\\xc1\\xec\\xed\\xbd\\x1aq\\xc9:\\x80\\n_<\\xf1y\\xd7<x\\x13\\x80=\\xbb\\xc3%=\\xb6 \\xb1:\\xbeo\\xe4\\xbcx\\xdc><\\xf1QK\\xbc\\xe1C \\xbd\\x11\\xb7\\x8f=n\\xf3\\x1e\\xbc\\xd6\\x8f\\xbd:\\xc6\\xbc\\x11\\xbc\\x04z@\\xbcs\\xbb\\xec<\\xd3\\xbd\\x8d\\xbc\\xbeF.\\xbd\\x85\\xe0\\x87\\xbb\\x7f\\xfb\\xa8\\xbb\\x87d\\xf0\\xbco+\\x13\\xbdZ\\xbe$\\xbd\\x119\\xae;TXA\\xbd\\xf8D$<t,\\xa7\\xb9\\xcbC\\xbc<\\x8b\\xda\\x86;\\\"\\xc9\\xcc\\xbc\\x1b\\x0fH\\xbd\\xa0\\xf2\\x8d=\\xfd\\x7f\\xd8=\\x85 \\xcf\\xbc\\xb5\\xec&\\xbb\\x95,\\xae<~6\\\"\\xbd\\r\\xa3\\t=Ec;=\\xc4?C\\xbd\\x8a\\xcc\\xf9<\\x9fB\\x1c\\xbd\\x10\\x97\\x00=\\xcd.\\x9a<+\\x08\\x94<\\xbeJr\\xbd\\xee\\xd2\\x05=\\xb2(\\x97<\\x13\\xc0\\xa9\\xbcJ\\x8b\\x00=\\x163\\xdd<\\xd5\\x89\\n=#\\xeb\\x089F>\\xb9\\xbc\\xcb\\xbf\\xfe<u\\x9e1=\\xc9\\xc0\\x03\\xbd%\\xd0\\xc2\\xbdZ.\\xd5\\xbc\\x9f\\xbak\\xbcW,\\xa8\\xbc\\xec\\xf0\\xd2\\xbca\\x96o<\\xf0j]=\\x88\\xdb\\xb8\\xba\\x10\\x13\\xc5\\xbc\\x07\\xcbm\\xbd\\xf8Tp=d,\\x93<\\xe7l0\\xbcD\\\"\\x93\\xbc:\\xc5\\x8d\\xbcE\\xc1\\xd9\\xbc\\x7f\\x1a\\x1f\\xbd\\xd3q\\x8f\\xbc\\x83\\xbd\\x86=\\xeb\\xac\\xb7<\\xc5\\xd5>=l\\x97\\xa3<n\\xc9!=\\xb9\\x86\\x03\\xbd$\\x9cu\\xbc\\xc6\\xae\\xee\\xba\\x91\\x1eG\\xbc\\x996\\x02<\\x15\\x0f@=Q\\xf8C<\\x0f\\xa5\\x11\\xbc\\x1c\\xfe\\x8d\\xbd\\xcf\\x11\\x83=\\x86\\xbd\\x8c\\xbd\\xe6\\xd9N\\xbd\\x81\\x93v\\xbc\\x98G\\xd7\\xbd\\xabj\\\\<\\xf8\\xc8|<\\xb1\\xb5\\xd5; \\xdf\\xcf<\\xe6?g<e\\x84\\x94=\\xc2\\x12.=e\\x9d\\x80;\\xc1\\xe1P=\\xf24F\\xbd\\xa6\\xb3\\xdc;\\xf5\\xa1\\xcc;9\\xf0\\r=\\xf3*_\\xbd\\x1e\\x0c\\xc3\\xba\\x96\\xdc\\x1b=K\\xdf\\x82\\xbcl\\xf9\\xa1;`\\xa9\\xd3\\xbcB%\\xe7<n\\x87\\xce<\\xab\\xca\\xf4<\\x07\\xcc\\x83\\xbc\\x96+L<\\x03\\xe0\\xe5\\xbb\\x84&\\x06\\xbc\\xa5\\xaf\\xb1;_\\xa5\\xfd8;us\\xbc6\\xfbS<x\\xdc\\x90\\xbci\\xf7\\x80\\xbd\\x14\\xf9?\\xbb\\xcf\\x8a|;\\xcfe\\x05\\xbc\\xfeUJ<\\x1bue\\xbd\\xd2\\xe6\\xc0<\\xe3\\x85\\xde<\\xc8\\xb2/<\\xf3\\x88I\\tJ+\\x1c\\xbc\\xf0Qx\\xbd\\xa4\\xb5\\xce<&Q\\x19\\xbb\\x19\\x07\\xb1\\xba\\xa3ia=\\xc8\\xc4\\xb4\\xbb\\x10u\\xe6\\xbc\\x1cz\\x14=&!\\r\\xbd:Hc<\\x077\\x10=#\\xd3\\xcc\\xbc#\\xd6N=\\x19z\\x9a\\xbc\\xd7\\x0b\\xca<Wsr\\xbd\\x94\\xeb\\xe9;\\x84\\xda\\xe3;s\\x9ab=h-\\xec:\\x0b\\xec\\x06\\xbd\\xc6\\x870\\xbc\\xf8dS\\xbd\\xe9lI<\\xa9\\xa0\\xf0\\xbc\\xf3\\x18\\x9f\\xbb_<\\xaf\\xbb\\xb9b\\x1e\\xbc\\xd0\\x1d?;\\x04\\x16R\\xbbL\\xbb\\xf1<\\xa6\\x85K\\xbdm\\xdc\\x8d\\xbdB\\xaf\\x08\\xbd;Z\\x93\\xbc\\xf2g2<\\xa4\\xd6\\xa1<\\xc4n\\r=\\x92\\xea\\xf1<\\xed1`\\xbc\\x00\\xcf\\x19<\\xe8\\xc8\\x18;\\x93(\\x1e=\\xbc\\x9d\\x1d=\\xe0\\x08\\xc4\\xbb\\xb2\\xaa\\xa1=\\x15G\\x9d\\xbc/\\x9eY\\xbd\\xa3{\\x1f\\xbd8.\\x19\\xbd6t\\xd6\\xbd\\x0c\\xf3\\x1b=)\\xf6\\x85<\\x87\\xe0\\x9a<k\\x14\\x9f<\\xb5\\x08\\x85;\\\"\\x90\\xe8<\\xcc\\xce\\xf9<6\\x96\\xf6<\\xd5\\x1eD=\\xf0\\xb4\\x83\\xbc3p\\x0f\\xbc\\x91\\xe1\\x96<\\xdc\\x90\\x86\\xbc\\xe0\\x7fF\\xbdk\\xea\\x1e\\xbd\\xa8(T\\xbc\\xbcf\\xfa\\xbav\\xc2\\xb9<\\x9c3\\xc6<\\xaf\\x00\\x9e\\xbc\\xca\\xfes;&-g=}\\xbb\\x0f=\\xe5\\xdf+\\xbd\\xa3U\\xb9\\xbc\\x97\\xd3w=\\x11HB\\xbd\\nr\\x98=!F\\x86\\xbc\\xd2zv<Z}\\xfc\\xbc\\x8b\\xc0\\xf6<}\\x9c\\x0e\\xbd?C\\x9b\\xbc\\x97f:=\\x91\\x7f\\x14=\\x16\\xed\\x1c\\xbd<\\x08\\xb9<\\xa4\\x04\\x03<:\\xff\\xb7<\\xf1UC={\\xb5\\xb5:\\xc9E\\x80=\\xf5_7=\\xdd_\\t\\xbd>\\xd2\\x19\\xbb\\xac\\xf8\\xd2;\\xbe\\xcb\\x0c=P\\xa5%\\xbc\\xbc\\xb2\\x1c=\\r3\\xab\\xbc\\x89\\xe1\\xb2\\xbc\\xd8\\xcf\\x16=\\xa3\\xef\\x85\\xbc&*\\xb3<\\xe2?\\\\;\\xf0\\xb7L\\xbdh|#\\xbdx\\x99\\x14=c\\x16\\x15\\xbd\\xa1\\xdd8\\xbd\\\"\\xadl=\\x93\\xe3\\xc9=\\x95\\xec\\x1a=\\xaa#Q<\\x8aM\\x14<\\xd5\\xe0 ;\\x1aj\\xa7\\xbd\\x13\\xa5\\x16\\xbbQ7n<\\xf5\\xda\\x1a<q\\xcf\\x98<K#\\x89=f\\xfd\\xe5\\xbb\\\"Y\\x86\\xbb\\xab\\x9cD<\\xcd\\xf6\\xac=\\x04\\x13K\\xbd|%E\\xbd\\xaf\\x93\\x7f\\xbd\\x15\\x7fN=\\xaa\\xf2\\xbb<]\\xe4\\xcd=\\xcf\\xae\\xae<5\\xf7c=\\xa7\\x96!\\xbd3\\xa4\\xdf;\\x81\\xd87;\\xca`\\x81<\\xcey\\x19\\xbd\\xc1*W\\xbd\\xb0\\xfd4\\xbb{\\x0f\\x90<\\x0c\\x81V\\xbdk\\x9a\\xb0<r\\xb9M\\xbc\\x1b\\xb3(=u\\xc9\\xe2\\xbb\\xd8g/\\xbd\\xbaH\\x88<?\\xb9\\x05\\xbc:\\xcd\\t=\\xbd\\r.=\\xaa\\xb7\\xca<L\\x1b\\x80\\xbd\\x90\\xacN=\\xf8\\xd7O<\\xb6\\xb1\\xe0<\\x07\\x13!=n\\x9c\\xe8<|\\xa7\\x95;\\x0e.\\x87\\xb8\\xac7\\x95;\\\"\\xcd\\xb7\\xbb11\\x85\\xbb\\xc3\\x9fj=\\xff\\xff6=\\xce\\xb3\\x86\\xbc&\\xd0K=Z\\\"\\n\\xbbb\\xaf\\xa6\\xbb]\\x90\\x85\\xbcy\\x8f\\xd1\\xbc\\xbaf\\x91=#\\tJ\\xbc\\xe0\\xe4O<\\xf0\\x1c\\x1c=\\xe3\\x94\\xb8<,A\\x93\\xbd!\\xbf\\xc3\\xbcj\\xf6\\xe6<\\x9d\\xb1\\xdf\\xbb\\xe9\\x7fm\\xbc\\xe5\\x84e\\xbb\\xd9\\xb3\\xe8:3. \\xbdt\\x0e\\x1e=\\xa8\\xe8\\xda<1\\xbdk\\xbc\\x87^\\x91<\\x08\\xf5\\xe5\\xbc\\x02\\x0e&\\xbc\\x8eK\\x02\\xbdA\\x15q\\xbdd\\xfe+\\xbd\\xf3\\xae\\xcb\\xbc\\xc6\\xaa\\xfa\\xbbV\\xcf\\xac\\xbc\\xe0\\xce\\x99\\xbc\\xd0\\x07\\xd3\\xbb\\xa4\\xdf\\x03\\xbd\\xf9\\xc4\\x88\\xbd\\x0b\\xe6\\xcf\\xbc=}\\x8a\\xbb8]\\xd9<\\x08\\x05\\xd2\\xbb\\xef3a\\xbc\\x9b\\xe7\\x8e\\xbd \\xd5o<\\x8b\\xc7\\n<+\\xfa\\xb0<\\xf5Z;; \\x89\\x80\\xbc\\xf5I\\x92;\\x02My<\\x19\\xb2l\\xbc\\xd9\\xd2`\\xbd\\xce\\xb5;\\xbdX\\x7f\\x17\\xbd5u\\xba8\\xd1\\xe0\\x05=g\\xf7T=\\xb1z\\xbc=\\x83_\\xb9\\xbb\\xc9\\xd4\\xca<u\\x97<\\xbde\\x90\\x05\\xbd\\x82\\xf1\\xf5<o\\xbd\\x949F2B=\\xb9\\xfd\\xbe\\xbc\\xa6ls\\xbch\\x06P\\xbc+H\\x91\\xbd\\x7f\\x01\\x9e\\xbcDB\\xbe\\xbc\\xe2/\\xf2<\\x9f\\xb7(\\xbc\\xcf\\x8df<;\\xbcP=\\xd3\\xa9\\x11<Tu!=\\r\\xfb\\x16\\xbc^\\xd2\\x90<F\\xa0\\x0e\\xbdP\\xc7\\x85<P\\n\\x8a\\xbd\\xdb\\xef\\xbf<\\xc9\\xc8\\xf7\\xba\\xd5\\\"V<\\x07\\x8b\\x1c;0\\x07o=\\xe7/s\\xbc|\\xdd\\xb7\\xbb \\xfe\\xd5\\xbbB<\\x83\\xbd$\\x7f6\\xbd\\x92V\\xc2\\xbd\\x92y\\xdc<)\\xa0[=~\\xbc\\xe5<\\xc1\\x1e\\xed<\\xa2\\x82\\xf0=\\xf2BX\\xbc\\xc8\\x80\\x8b=\\x9b\\xc7a=\\x83\\x0f\\x89\\xbb\\xf7\\x01W=v\\xabr\\xbb\\x02\\x19\\x06=VWk\\xba\"\nHSET bikes:10098  model 'Venus' brand 'Ergonom' price 4961 type 'Kids mountain bikes' material 'full-carbon' weight 14.7 description 'Kids want to ride with as little weight as possible. Especially on an incline! The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating, as do the hydraulic disc brakes from Tektro. This is the bike for the rider who wants trail manners with low fuss ownership.' description_embeddings \"\\x98f\\xb3\\xbc|\\x10\\x13\\xbd\\x18\\x9b3\\xbd\\x1ch\\xb1\\xbb{\\x96\\x8e\\xbd\\x89(\\xa2\\xba@\\xcd\\x97\\xbc\\x19\\r\\xee\\xbc\\xed4\\xa4=\\xef\\xab\\xdf<\\x13,\\x83<-\\xa64\\xbdk\\x8e\\x8c<w!(=N\\xbc\\xff<\\xd7\\xf6G\\xbd\\xd1\\x9a*=u\\xc6\\xc1\\xbd\\xdf\\x0e\\xcc\\xbc\\x9d\\xc6*\\xbd%\\x02\\xca<\\xc8\\xd4\\x81\\xbbs3\\xbf<\\xce|\\x08\\xbbu\\xf6\\x13\\xbd\\xfb\\xe1\\xe1;\\xe3x\\x89<F\\xb7M<\\xa7R\\xba\\xbb\\xb3\\x7fV=\\x19\\x11\\x8b\\xbb\\xfa\\x96\\x18\\xbd4xh=\\x02\\xde\\x88=\\x1d\\xcfw\\xbd>!\\xa9\\xbb\\xa2\\xea\\x15=N\\\"\\xa5\\xbaQoX\\xbdn\\x9e\\x9f9\\xc0\\x0fA<`!\\xa5<\\xfa^\\x0b\\xbcR\\x1d\\xe2;s\\xcd\\x89\\xbcD\\xe70\\xbd\\xf4\\xfd\\xdb<=\\xe6\\x0e:\\x81&;\\xbd\\xdfPB<\\xf5\\xba\\x86<\\xa3Z\\xe6\\xbaQ\\xa4\\xe8\\xbcBE\\x90;}\\x8b\\xa1\\xbc\\x8d?Y\\xba\\xc0\\xfa\\xe6;N\\x80b\\xbd\\xf3B\\x01\\xbdM\\x0f\\xab=\\x0c)\\xef\\xbcD\\x03v=\\xa5\\x7f\\xeb<\\xa3\\x1f\\xcf;\\xa1]\\x9a<\\t9\\xb3\\xbc\\xdb4\\x8397N\\xc5\\xbc\\xad)\\xbb;\\xe7\\xa9%\\xbc\\xd6ZQ<\\x96\\x96\\xdc\\xb9\\xfdS\\xbb\\xbb],\\xaa\\xbdr\\xcel<hBq\\xbd\\xdb\\xd7\\xb1\\xbbA\\xaf \\xbdc\\x11\\xf7\\xbc\\x07\\xefS=G\\x81\\x9b<\\xe2,\\x00\\xbdL\\xba*\\xbd\\x8dT6\\xbd.\\xf1\\xcc<\\x15C\\xba<x\\x83\\xc2\\xbcQ\\xe9\\x96<\\x9e!\\xe3\\xbb\\xea\\xfer<\\xcbp\\x1f\\xbc\\xbf\\x80%;tl\\xfa\\xba\\xc3\\x08\\xe3\\xbaU\\\"\\xc7<\\x8a\\x02\\x19=\\x19yb\\xbd7z\\\\\\xbc.C9=h\\xd82\\xbd>FP\\xbd\\r\\xd9\\xe5;\\xaa\\xf8\\xe2\\xbcY\\xcc\\x0c<\\xde\\xcd\\x8a<\\x17\\xce(>p\\xc9\\xac;\\xf4\\x07\\x0f=\\xfeo\\xdf\\xbc\\xb1\\xac\\xe8\\xbc\\xec\\xc9&:;\\x18\\x8e=29\\xdb<\\xedY\\xde\\xbc\\xdc\\xb2\\x9b\\xbdc\\xf3\\xaa\\xbc\\xc1\\xb2\\x9c<\\xb3\\x9a\\x1f:R\\xb7\\xeb<\\xa8E\\x9f;H\\x93N=o}\\x02<\\xa7\\x7f&\\xbdE\\r2=k\\xa9\\x82=\\x19-\\xf2\\xbc>_\\xaf<-\\xb6\\x01\\xbc\\xec\\xc6R\\xbd\\x00\\x82\\x02\\xbdY.\\xa7\\xbc\\x1e\\xd4\\x19\\xbd\\x9c\\xe6N=\\xa9%\\xea\\xbc\\xce\\x90\\xe0\\xba[FX=\\xd7\\xa2E\\xbd\\xbe\\xbf*\\xbd)\\xeaN<\\xd1\\xaf\\t\\xbco\\x85]\\xbd\\xc7\\xc3\\x8e\\xbd\\x1c!|\\xbc\\xcaqe=X\\x13K\\xbd\\xb2\\x07c<\\xd4;\\xe3\\xbc8\\xd1p=,*\\xc8;\\xed\\xa6Z;\\xfe\\xe5\\x02\\xbd\\x88\\xa7\\xb8<\\x02\\xf1\\xc4\\xbb\\x92\\x98!<t\\x16t\\xbc5\\xe14;\\x8a\\xf2s\\xbc\\xaf\\xe5?;XXI=\\xa53\\x17\\xbd\\x8b\\xa8p=\\xfb\\xd6\\x81\\xbc\\xec\\x04\\x1d\\xbc\\xe8\\x0fk\\xbd\\xf0\\xe8\\x82<*\\xd8\\x10=\\xf7\\x01\\xb0;/9\\x1b=\\xca5\\xb0<g\\xdb-<e\\xf43\\xbc\\x97\\xf5\\x9c=\\x9b\\xc39\\xbc\\xe5Q\\x81\\xbd\\xde\\xaf\\x91\\xbc\\x05+\\xaa<\\xaa9.\\xbd\\xcalf=L\\x82{:\\x8b#\\xd7;U\\xdd&\\xbd0\\xcf-<\\xcb\\xb8%<\\xe9h\\x82<\\x82x\\xae<\\x18-S\\xbdM`\\xd6<\\x02C\\xbb<\\xd9\\xae\\x0e=\\x80\\x10a;\\x19\\xe2\\xe0<\\xa0&\\xdd<\\xc7\\xb2p\\xbdW\\xb6\\x9b=*\\x01\\x88;\\xe3\\xa4\\x17=\\x87\\x87\\x0f<\\xa6\\xa2\\x17<\\xd6\\x82\\n\\xbb\\x95\\xfc\\x19;\\xbdw\\x1f<\\x05\\xfeu\\xbdc\\x8f\\xb1\\xbd\\xe2p\\xfa\\xbb\\xdd\\xa2\\xe1\\xbc\\xdfd;=\\xd4\\xf4\\xb5\\xbch\\xa9\\x01\\xbdO;&=\\xa7\\xe6l\\xbc\\x89\\xca\\x90\\xbc\\xcaE\\x98=u\\x95T=\\x91 \\xb0<2R\\x8c<\\r\\x19\\x11=\\xdcZ\\x9a\\xbd?\\xc8\\x14<z=M\\xbc7$\\x9f\\xbc>\\x92\\x17;?\\xe1\\x0e\\xbc$\\xa2m\\xbd\\xeb\\xcc(=\\xf1P\\xad\\xbc\\xc4\\xf8\\xd9<\\xc0\\xeeW=ha\\xd6\\xba\\xe4\\x93\\x82=?;\\xc1<\\xb4\\xba-\\xbc\\x91\\xef\\x15<|\\x9c\\x01=cB\\xbc\\xbb\\xb9\\xe5\\xc9\\xbc\\xa6\\x0f:\\xbd\\\"_\\xd5\\xbc\\xf6H\\x07=\\x1c\\xb1U<\\x92\\xc5%\\xbc\\xa4h\\xc6:\\xf1\\x1bJ<\\xed3\\xd5<Zo/=\\xe6 \\x11=\\xfa\\x07\\x94=\\x9f\\x91\\xb7<%\\xb4\\x12\\xbc\\xb1\\x05\\xf1\\xbc\\xd3\\xf5\\xab\\xbb\\x903\\xbe\\xbc\\xdd\\\"h\\xbd\\xbf\\xc5\\x1c\\xbd\\xb1a\\xea<\\xee\\xcc\\xde;\\x16\\xd0\\xad=\\x1e}\\x0f\\xbdB\\xb3\\xb5<\\xce\\x81\\xda;6\\\\\\xc2<\\xd6w\\x07\\xbd\\x92\\xc3\\xa6\\xbc\\xb2w\\x82\\xbbS\\xd0`\\xbc\\x9c\\x9f5\\xbdYA\\xae\\xbc\\xa9\\xbe\\x96\\xbbM\\x06*\\xbc<\\x92t<\\xe3\\\"g\\xbb2\\x81\\xbb\\xbc)\\xa3\\xf8\\xbc\\xc2\\xbfT\\xbd[5\\x8c<\\xa6L\\xa5\\xbc\\x93D\\x16\\xbd\\x91r2=X\\x05\\x04\\xbb\\x8c\\xb9\\xd7=\\xe4\\xebp\\xbd-9\\x11\\xbcjA\\xa4\\xbbK\\xa6\\xc2<\\xfe\\xe7e:\\xbd\\xc3\\x809\\x05,\\xc1<\\x1f\\xff\\xfa\\xbbT\\xc4+=\\x84\\xc43\\xbd\\xf9)\\xa5<\\xd2\\t\\x9e\\xbcG\\xd92\\xbc\\xf4bS=oi\\xbd\\xbb1\\xb9?<\\x867>\\xbdAK\\x919\\x95\\xe0\\x8a<a*\\x1f\\xbcUm\\xbf\\xbc\\x9c\\xcaW=N\\xcb\\x1c<*\\xe3\\xcb<BF\\xaa< m\\xf3<^\\x11c\\xbbh\\xedh\\xbc\\xb2\\xbe\\x15</\\xc2V<:b\\x1d;\\xf1j\\xf9\\xbc\\x1b\\xae/\\xbd\\xac\\xc3Q\\xbc\\x0e\\x8f\\xa8\\xbaE\\xe6\\x1c\\xbc\\x80\\x8b\\x91\\xbd3 .\\xbd\\xdf9\\xab=\\x11\\xe0=<\\xb6\\xe4\\xea\\xbc\\xc3\\r\\xb1\\xbc\\xd5\\xdc\\x8d\\xbc\\xdc\\x0c&=\\x19\\x82L<\\x000\\xf0<G\\xa7T\\xbd\\xf0;\\xae<z.(\\xbd.\\x8e\\x85=d\\x16\\x9f\\xbb\\x08\\xdb9<.`\\x87\\xbc+GB\\xbc\\xf5G1\\xbd\\xc5f\\xf5<\\xb4\\xf7x<\\xdf_7\\xbc\\xf6\\x8f\\\\\\xbc@\\x13?=\\x01\\x86\\xa8;s\\x1b5\\xbd\\x1e\\xd9\\xd2<X\\xca\\x9d=I\\x93\\xe3\\xbb\\x11O\\xdb\\xbb\\xccn!=l\\x17U=\\x8b\\x06C\\xbc\\x19\\xdd1=>\\x1e\\x8f\\xbb\\x01\\xa0-9\\xfdx\\xb1\\xbbN\\x81\\x9a=\\xe6\\x00\\x00\\xbc\\xe7\\xc5\\xd1\\xbc\\xd7P\\x00<;\\xbb\\x08\\xbd\\x85\\xdeb\\xbd\\xc9_\\x9c\\xbb\\xa0\\xdc\\xf1\\xbd\\xb15\\x9b<nxR<\\xe0zY=\\xbe\\x86n=\\x03\\x88\\xbb<\\x1e\\xe4&\\xbd;\\x93\\x9b\\xbc\\x07-.=\\\"\\xdc:\\xbd\\xc9\\\"#;#\\x103<+\\xc8\\xd2<\\xc9nm<>\\\\\\xe9<Lh\\x0b=k%\\x84;\\xb5\\xeb\\x8c\\xbc\\x90M=\\xbd\\x9f\\xa7\\x08=Q\\x86\\xf9\\xbb\\xfd(\\xe6\\xbc\\xad\\xd5\\xfd\\xbb\\x1d\\xb6\\xb8\\xbb]C\\xab<)}\\xaa\\xbc\\xff\\xda8=\\xb6\\xfa\\x1b\\xbb\\xd52\\x15=\\xe4\\xcf5=R\\x06\\xec\\xbcC}+\\xbaK\\xfc\\x9d=^\\x10\\xcd=\\xf9\\x85\\xb6\\xbc\\x94\\x16`<\\x9a\\xde\\xb4\\xbcz\\xc6\\xbf\\xbc1\\xf8\\xb4\\xbc[\\xae\\xe3=\\x8e\\xb0\\x80\\xbd<\\xae\\xe3\\xbc{q\\xf3\\xbc\\xd4\\\"\\n=\\xfa\\x03\\xab<\\xfczR=\\xb5\\xf7L\\xbd\\x9d h\\xbcx\\xf4\\xc7<K(m\\xbc\\xc7v):/\\xdbI=\\xc4\\x9c)\\xbdT f;\\xc1\\x81\\x8f<j\\x01)=a\\xb5;\\xbcm\\x1e\\x00\\xbc\\x0e\\xb2\\xe8\\xbc~B\\x9f<\\xcf\\x8b\\xe1\\xbc`BD\\xbdc\\x0f\\x1c\\xbdC7\\xd1<\\x03\\xd9~=\\xdc\\xd2*\\xba\\xa9\\x11J\\xbd\\xe5\\xe1<\\xbd\\xe5 \\x11=R\\\"\\xc2<\\xfd\\xbd\\xfc\\xbc\\x8b\\x08\\x87<|\\xabh\\xbc\\xa3gK\\xbd\\xe8\\xa4\\xcd\\xbdA\\x99R\\xbc\\x82\\x80\\xca=Y=8=\\xb7\\x96\\xa0<\\xbc\\xd6p\\xbd%\\xb2|\\xbc\\x16\\x14\\x83\\xbd\\xb5\\x9d\\xbe\\xbc\\x14\\xa7\\xee<cn\\xec\\xbbik\\xce;\\xe5\\xf8\\x17=}`\\x1f<7\\xa2\\xad\\xbcV6J\\xbd[\\xb9\\x98=\\xc7\\xce\\xf9\\xbc\\x9b\\x7f\\x81\\xbd[\\xd7\\x17\\xbc\\\"\\x04\\xb7\\xbd\\x96\\xa6\\x0c;\\x96\\x9e\\xb5\\xbb5$\\xb8;\\xc6\\xb6(<\\x95\\xac\\xb5<\\xb2\\x83\\xc3<\\x9e\\xa3\\x9b=Vp-=\\xa0\\x0es<L\\xb0\\x99\\xbd.J9\\xbc\\x92\\xf9\\xec<\\xda\\x9b\\xfe:\\xb1@\\xdc\\xbc\\xceQH\\xbc]\\xc2\\x8d=\\xbb\\xe5\\x15\\xbdn\\x96\\xbb:\\x90\\x91q\\xbd\\xc8c\\xef\\xbb\\xee\\xba\\x01<\\x8fL\\x8e:\\t\\xed\\x87\\xbc>6s\\xbd\\xfd`\\x9a\\xba\\x02\\x1e\\xab\\xbb\\x8a]\\xb0;\\xfc^\\xb8\\xbc\\x96\\xf4\\xa9\\xbc\\xfb\\xaa\\x8c;\\x1d\\xa5\\xbe;\\xf3\\x15\\x97\\xbd\\xdd]\\x05\\xbaw\\xac\\xd1<\\xd5\\xa8\\x80\\xbb\\xf7\\x07Q<\\x02T\\xa1\\xbd\\xe4\\xcc\\x18=Q\\xe2.<S\\x9c\\x8b\\xbc\\xdf\\xebj\\t\\x08q\\x8b\\xbcQ\\xe6P\\xba\\xdc\\xa92=:\\x89\\xab=\\xe4\\r\\x93<\\xc9H\\xe0<\\xe3\\xa3\\xe7<\\xc7zi\\xbc\\xb9J\\x9c\\xbb\\x1fQ\\x92\\xbc\\x13\\x9e\\xb0\\xbb\\x16[\\xa6<\\xb1\\x08h\\xbbk\\x19\\xec9I\\xa3\\x14\\xbd\\xf7tJ\\xbc\\xf9\\xd1J=z\\xb1\\xdd\\xbc\\xea\\x95\\xc8;\\xdd;h=\\xf2\\xe4\\x02=\\x92Mj=V\\x89\\x9d<VQ\\x12\\xbdZ\\x00\\x92<\\x02\\x9a\\xc8<C\\xc6\\x0b==\\x9c\\xd4\\xbc\\x7f\\x903\\xbdLe\\xbc\\xbc\\xca\\x14\\xec<\\xc3\\x0e\\xaf\\xbb\\xedY$\\xbd\\xc0(+\\xbd;\\x1a~\\xbcPQH\\xbd\\x82V\\x8b\\xbcv[F=f\\x02\\xf5;F\\x88U\\xbb\\x84\\xcf\\xa4;1\\xda\\xa3\\xbce\\xbf\\xe6<w\\xa8,=\\x0fb\\xd6;48A\\xbd\\x93\\xd4\\x86=\\xee\\x01\\xe7\\xbc\\x9eW\\xe7\\xbcvf\\x03=\\x03\\x89Q:\\xed\\xdb%\\xbd/`\\xfe:A09<\\xda~`\\xbd\\xa8\\xa4\\xeb<\\xa7\\x1f\\x17\\xbc\\xe4S\\xad\\xbc:&\\xde\\xbb_\\x9b#=\\x07\\xed\\xbb\\xbc\\x1a:\\x96\\xbb\\x7f\\xf4u\\xbc\\\\\\xbfG=\\xc8\\x00\\x8d;-\\xe9\\xa3\\xbc\\x13nE=\\x83|\\x8f\\xbc\\xd3\\xe6\\x90<\\xf3\\xfc\\xa2:\\xe1q\\x8e<\\xcb\\xb7\\x8e\\xbcP\\xf1\\x92\\xbc\\x90t\\xe2<\\x91C\\x91<fn\\x04\\xbd\\x8e\\xaf\\xbc\\xbb~\\xe1\\x18=y\\xf2\\x80=\\x03\\xb0\\x83<\\xc8M{\\xbd\\xcd\\xed;\\xbc\\xf1\\xa2\\xa2\\xbc\\xc2\\x1bE=\\xa0\\xc4\\xd6\\xbc\\x97\\xa5\\x8c<I\\x00}=\\x86\\x10\\x9b<\\xc6\\xfa\\x85\\xbc\\xe4\\xc1%=\\x8a\\x9b*<\\x9b\\xa5w\\xbdV\\x90r=\\x98\\xab\\xad:Sg^<\\xf3=5<F.3\\xbdL\\xe49=\\xd8\\x04\\x0c<\\xc6\\xadS=Ss\\n\\xbcG\\xba\\xa7\\xbb\\xc5\\x13\\x8b<\\x03T\\x97;RK\\xb3<\\x1d\\xc9@\\xbcu\\x17+<\\x0b\\xf0\\xe0\\xbcT\\x96\\x07\\xbcr9\\x1f\\xbd\\x18\\x88{=\\xa4\\x85\\xc6\\xbcz \\x95\\xbcJA\\x01<g2K=y\\x9f\\xc8\\xbc\\xe14\\xcb<\\xf5)\\x8f\\xbc_\\xa1\\xbb\\xbc\\xabt\\xe8\\xbb\\xb4c\\n=r\\xdc\\x0f<Y\\xe9\\xa8;\\xb1\\xbeT=n\\xcb\\xd1;\\xfbR\\xcb<\\x07\\xb4}\\xbd\\x8f\\xf04\\xbc\\xb2[\\x86=\\x01\\xde4\\xbd\\xd8[ \\xbd_\\xb0q\\xbd\\x9e\\xfc\\xe1<Z+\\x82\\xbb_\\xe2\\xbe=v\\xf7\\x98\\xbc\\x8b\\xb4A=\\x9b\\xb83\\xbd\\\"$)\\xbc\\xb8m\\x19\\xbd\\x15\\x86\\xd5<>n\\xa1\\xbc\\xa4aI\\xbdx\\xd5\\x0b\\xbc\\xa4\\xb9G<\\xb8\\xbb\\xe4<\\r\\xeb\\xb2=\\xd2\\x0eD:\\xa2\\x8a\\x1d=q\\x0b\\xca\\xbb\\xaa\\xa7N\\xbd\\xf5S\\xd4<?\\x9d\\x99\\xbc|B\\x00=\\xe0\\x1c0=LS\\xd2;\\\\\\xe23\\xbd\\xd8m\\xac\\xba\\xd8.\\xa4<\\xb4\\xa5\\x9d\\xba\\xfd\\xb2\\x06\\xb9}7\\xa8=^\\xa3\\x9b<[\\x0c;\\xbd\\x93\\xa0\\x90\\xbc\\xda\\xca\\x9e\\xbc\\xd2Ar\\xbb\\xe2\\xcbF=\\x9b~B=\\xf00\\xb2<\\xc9\\xe7\\x94;Q\\x02\\x08\\xbd\\x1a\\xc3\\xd2<\\x13y\\xad;\\xf1\\xfd\\xfb<\\x83V\\x1c=\\xa1\\r\\x9c\\xbc\\xfa\\xb2e=\\xb88d=\\x84\\xf2\\xcb\\xbc\\xc6\\xc1O<Wz\\x93\\xbd\\x1fL\\x8c\\xba\\x98\\\"\\x98;b^\\xef;t \\x9f\\xbcH\\xf1\\xe7:\\x7f{\\x1d\\xbc?]\\xd4<VV\\r\\xbc\\xf2J\\xae\\xbc\\xd0\\xf2\\x93<\\xfe\\x03\\x84\\xbd\\xc9*\\xf6<\\xf5\\xd5\\x04\\xbc\\xfdq\\xa6\\xbd\\x9e.\\x1a\\xbdM)\\xee\\xbb\\xf7gb\\xbc)\\x9a\\x98\\xbd\\xfb4i\\xbd\\xab\\xdd\\xab\\xbc4\\xf9\\n;*\\x94\\x8a\\xbd\\xf5\\xa9\\x99<\\xdb\\x12\\x1e\\xbd\\\"\\x12~<!S\\x84\\xbc\\x92\\xc9\\xed\\xbbe\\xd2\\xa0\\xbce~\\x07<v\\x12&\\xbc\\xadD\\xbc<8\\xf8\\x88:\\xcc\\\"\\t\\xbb1pT<\\xf8\\x03\\x90\\xb9\\x15\\x1d\\xfe\\xbc\\x1dj9\\xbd\\x86\\xe3\\xc5\\xbd2\\xea\\xb0<S\\xda-\\xbd)\\x18E:n\\xc4\\xd5<\\x87\\xc4-={\\xa5G\\xbd\\x06~o=\\xa3\\x81\\x08\\xbd\\x87j\\xaa\\xbd\\xc6+0=\\xa7\\xa7\\xd3;\\\\\\xd5b\\xbc\\x95\\x8e\\x14\\xbc\\x87\\x89\\xa3\\xbc\\xb1\\xe2\\x8f\\xbc,\\x1c\\x19\\xbd`\\x92\\xd1\\xbcP\\xc3\\xa1\\xbc\\xb6Lq=\\xbf \\x80;\\x0c\\x06\\xec;\\xe03\\x1f=\\x16\\xf1\\xf6;U\\\\T=\\x05\\x15\\x80\\xb9\\xfey\\x84\\xbc4S(\\xbd$VN=\\xe0~6\\xbd\\xa7\\xd0\\x81<Q \\xd6<X\\x9e\\xa0\\xbb\\x15\\xfa\\x86\\xbar\\x08\\x0e=n\\x9c|\\xbd\\xe54\\x1a:B\\x1a\\x8c\\xbc&L\\x84\\xbc;\\xfeT\\xbc\\xd3o<\\xbd\\x95\\x0b\\xf9<\\x9a\\xf1Q\\xbd\\xd4\\xc7O;Ovu=#]\\xe1=\\x04m$\\xbb\\xc1`\\xcf<HQ.={B\\x1d<\\x96\\x9d\\xac=\\x9dt\\xcd\\xbdNpu\\xbc\\n\\xd8z\\xbc\"\nHSET bikes:10099  model 'Eris' brand 'Tots' price 2085 type 'Enduro bikes' material 'carbon' weight 12.8 description 'The new version with 142mm rear, 160mm front travel is longer and slacker than its previous generation, but it’s also a bit taller and steeper than much of its competition. It has a lightweight frame and all-carbon fork, with cables routed internally. Put it all together and you get a bike that helps redefine what can be done for this price.' description_embeddings \"!+M\\xbc\\xa5\\xac\\x10=OU\\x15\\xbc\\x05\\xd9\\x16<\\xe9\\xad\\xbb:\\xf03s=3\\xf6\\x02\\xbd\\xbb\\xf2\\\"\\xbd\\xa4Gg=\\x8c<Y<\\xfe\\x89\\x88;0\\x8cF=\\x8a\\x1e[=K\\xf1V\\xbc\\\"\\xb6O=t/\\x0f\\xbd\\x8ceR=\\xd2\\x0e\\\"\\xbc\\x82\\xa7\\x82\\xbc\\xac\\xbcE\\xbd\\xe4\\xfe\\x8e;\\x9fID\\xbd\\x18\\x19z=\\x92 \\x80\\xbd\\xf8\\x99\\xb2<\\xd8\\xaf\\x82\\xbcV\\x05\\xd6;@\\x9b\\r=\\x07-8\\xbd\\xb3\\x13\\x82=\\xf9\\x07\\x99\\xbc\\xaf\\x11\\x13\\xbda\\x0f\\xab<;\\xb5\\x04=u\\xfb\\x04<\\x00BD\\xbb5p4=!\\xd4\\xf6\\xbc\\x06\\x10\\x07<w\\xc4\\\"<\\xfc\\xa2\\x95<\\xc7\\xa53=\\xc4\\xbf\\x81=\\xd0\\x86\\r=\\x15\\xfd\\x82<\\x0c\\xf3\\xa4\\xbc5\\x12r=h\\xd5s<<K\\xdf\\xbb\\xf1\\xdc\\xb7;\\xf3e\\x03\\xbd\\x15\\x0b\\x05\\xbd\\xa5\\xda{;\\xfe\\xfbk<\\xbf\\x8b)<\\x8e\\n?\\xbd]\\x02\\xa1\\xbc\\xd8\\xc4\\x80\\xbdE13\\xbd\\xb9\\x9f\\xc9=\\xf5\\xd97=\\x1f`,=\\xb4~S<\\xef\\x14\\xb7=p\\xe1\\x8e<B\\x94\\x02\\xbc&\\x90\\x93\\xbb\\x14\\\"\\xa6<\\x82\\xee\\x9d;,{\\x02\\xbd\\xc9xa<V\\x99\\x9b\\xbb\\x92\\t\\x01\\xbd\\\"\\x18\\x96\\xbb\\x15\\xfbL\\xbdV\\xf1\\xa6<\\x95\\xed\\x94<T\\xac-\\xbd\\xd2\\x9d\\x8b\\xbdf4\\xac:\\x8e\\xac\\xce<uJ\\xca\\xbd\\xcb\\xd0\\x02<\\xfb\\xcc\\xeb\\xbc\\xcc@\\\\\\xbc\\x0e\\xdf\\x1e\\xbdb\\x11\\xc8\\xbc\\xc9j\\x97<\\x0f]>=\\xdc\\x02V\\xbc-f\\xee\\xbc\\x15/\\xc3\\xbc\\x01\\xf6l\\xbdo\\xcb\\x16=D\\x86\\xfd<)\\x99\\xab<\\x0e\\xa5\\xd2\\xbbc\\xa5L=\\xec\\xc1\\xc2\\xbb4\\x98\\x00\\xbd\\xbe\\xbb\\x84<\\x92\\xb1\\x8a\\xbb\\x9d}\\\\=\\x97\\x8c\\x85\\xbb\\x8e\\x90\\xc3<\\x8e=\\x84=[3\\x08<>:\\xe2<\\xfdV\\xe3\\xbb\\x10,\\x84\\xbcf\\xd9\\xa9\\xbcx\\xff^=g\\x83\\x81;_}\\x1b\\xbd\\xc4\\xb6w\\xbc/\\xea\\x12\\xbdB\\xa4\\x85<%\\xd3\\xaa9\\xc3mF=\\xef\\xf6Q;x\\xdd\\t\\xbc\\x06G\\x83\\xbc\\xad\\xd5\\x8b\\xbd\\xd5\\x12y\\xbc\\xaf\\xb0/=`\\x80m\\xbc\\x18\\xc2\\xa6\\xbc=B\\x13\\xbd\\x86TD\\xbd\\xfc{\\xfa\\xbc\\xae$\\xda<\\xde\\x84\\x87\\xbc\\x0fV\\xc2<\\xb6\\x1b\\xb4\\xbc\\x19\\x10\\xb7\\xbb\\xed\\xfb\\x04=\\xc8\\xce\\xe7;}\\xcaT;\\xcd|H<6@X\\xbb\\x8a\\xda\\xd8\\xbc\\x10\\xa0\\x83\\xbc\\xe8]K\\xbc\\x97\\\\\\x06=\\x1c_Q<\\xbe_\\xd0\\xbc\\xac\\xaf\\x94<\\x07\\xb4\\x82;\\x1a\\xd0\\xe4\\xbb\\xbdK\\\"\\xbc\\x9f\\x17\\xd2<\\\":l=\\xc7\\xae\\x05\\xbd\\xbf\\xafK=\\xc5\\xad \\xbd-\\x8f\\xf3\\xbc\\xea\\x0c\\x93\\xbc\\xf0_i<SD\\xbd<\\xb5[i\\xbd\\x86\\xf0\\x1d=\\xcc\\x0e\\x83\\xbc\\xa9\\xf7*\\xbcA\\xc8\\xae\\xbdR\\x96\\x14\\xbdT\\xb2\\x81<V\\x12\\xc1\\xbc\\x9e\\xd0O<\\xb3#\\xa4<\\xed\\xe3\\x06\\xbd\\xd3Q\\xa5\\xbc\\x13\\x0e>=*\\xcf\\x84<\\x96\\xa8\\x81\\xbc\\\\\\xda\\x9b\\xbc\\x90\\xb6\\xd8\\xbc9I\\xfb\\xbc2\\xa1o=\\x9cc\\xb4\\xbb\\xb6\\xd0\\xe5<\\xaf\\xa7\\x96\\xbc/\\xfd\\xb7<\\xbej;\\xbc8/\\x16\\xbb\\xab\\\"\\x84=\\x17\\xc9\\xe9<G.\\xf7<\\xe2J(\\xbca>V\\xbb\\x83\\xb5c\\xbd\\x02\\xc1R\\xbc{\\x10X=\\xf0Sb\\xbdX\\xa8\\x9b=\\xfbr\\x10\\xbc#)!=\\x03\\xd4\\x1a\\xbd\\x95+,\\xbc\\xd1^,\\xbc\\\"S\\xb6\\xbc\\xe8\\x03\\x15\\xbc\\xc6\\xfb\\xc3\\xbdpD\\xee\\xbc\\xe8\\x0c\\x8e\\xbb}h\\x98\\xbc\\xa4\\\"\\x1c=^qG:]V\\xd3\\xbcXdn9\\xa7\\x1a\\x85\\xbde\\xec\\x9e\\xbb\\x9aZ\\x86:\\xa5\\xf2\\xf0<P(\\\"\\xbd\\x928\\\"\\xbc\\xa3V\\xd7;\\xbb\\xc1\\x15\\xbd|\\xd0\\xdc\\xbc\\xff\\x115=E,\\xda\\xbdy\\xdaP\\xbc!M\\xaa<H\\xdb\\x1f\\xbd\\x01v\\x81<\\xfd\\xa8\\x1c\\xbd\\xf8\\n\\x18< {\\x0e=+D\\xb6\\xbc\\x93bH=\\xe5\\xe8g\\xbd\\xab\\x08\\x8a\\xbc\\xfbe\\x0c\\xbc\\xd6\\\"\\x13\\xba\\x07(\\n=\\xc9,\\x84<\\x9f\\x9e\\xa8\\xbcpc:<\\xe1\\xc4\\x82<\\xaf\\xfec\\xbc-\\\\!<F\\xdb\\xa5<N\\x0c\\xaa\\xbc\\xcc\\xd7\\xcf<\\xce?\\x80<\\x93>\\x87\\xbc[<\\xbb<\\xe9\\xdf9\\xba\\x92\\x8f\\xaa<\\xab\\x03Y\\xbd\\xbaq\\x0f=>\\xfa\\x9f\\xbb@\\xde\\x8d\\xbc\\xf5bP\\xbb\\xbd\\x0b\\x9e=\\xb2\\xcc3\\xbd\\xad\\xd2R=j\\x8a\\xd9\\xbc\\x00\\xa3\\xba\\xbbB=2\\xbd\\x1bgi=\\x85\\xc0%<k\\xdbl;Ec7\\xbd\\x99\\x9d|\\xbc\\xaa\\x8a\\x18\\xbd\\x7f\\xe9\\xa6=`Y\\x02\\xbd,3\\xa3\\xbb1\\xb8\\xc1;\\xc9`F\\xbc&+?=\\xf8\\xec]\\xbbp6V\\xbdOP><\\xe1\\x9c\\x16\\xbd\\xb1\\x7f\\xad\\xbcek\\xf4\\xbcy\\x9b\\r<9B\\r<9\\x1a\\xdd\\xbduW\\x06\\xbd\\xbb\\x9f\\x02=h\\xbc\\xf8\\xbc\\xdc\\xa0\\xaa<\\x1a\\xca\\xb4<\\x7f!K=\\x82\\xb5A\\xbc\\x0b\\x0b\\x7f<\\xc1\\x13\\xba\\xbd\\xd3\\x06\\xcb<Ue\\xa7\\xbb\\xef\\x0e\\xff\\xbcIB\\x80=3E\\xf6\\xbc\\x18^\\xee\\xbcjA\\xd3\\xbbyh\\t=\\xc9\\x15\\xee<a-\\xe0<\\xc0\\xb2\\xac\\xbd\\x06X\\xe7\\xbb\\x87p\\t<\\x8b\\x88(=?\\xea\\xc9\\xbc~CI<\\x9e+\\xdb<#\\xfc6\\xbb\\xb2\\x04W<\\xc4$W=U\\x85\\xec<\\x96jb\\xbc\\xbc%\\\\\\xbd\\x00q\\x02\\xbd7\\xd3Y\\xbd\\x08b\\xb7<\\x1e\\xfe\\xec\\xbc;\\xcd\\x02=a>\\x8a=\\xfc\\xa1J=\\x14|\\xaf\\xbb\\xe2g\\xb3\\xbcpB\\x8d\\xbd|\\xaa4<\\x914\\x08<O\\xd4l=j\\x06\\x1a\\xbdc2\\xde=`&h\\xbd\\xdfV\\xd6\\xbc\\xef\\xf1\\xbf\\xbb{\\xebA=1\\x0b\\x9b\\xbd\\xf2ra\\xbc\\x96\\xbe\\xb3<]8^<\\xd7^n\\xbc~\\x17\\x14\\xbd\\x95z\\x06\\xbc\\xe4\\xb0!<J\\x04\\xcd\\xbao\\xfdc\\xbc\\xcc\\xf1!\\xbd+kW=\\x0f\\xcb4\\xbc\\xd0\\x979<\\xdd\\xa4Z<\\xc5E\\xc0=\\xca$*<MRX=\\xf2\\n\\x12=\\xad9\\xd3<WN&\\xbd\\x92\\xea\\x8a=h/\\xb2<\\xebnj\\xbdmbC<b\\x95\\xd1<0\\\"d\\xbd\\xa3d-:&0\\xe1\\xbdq\\xf7!\\xbb-\\xf6v\\xbc)\\xf1E=t[\\x9a<\\\\\\xad\\x8c=,&\\x11\\xbdx\\xc9\\xb6\\xbc\\x12\\x18\\xce;\\x14\\x8d\\xe5\\xbc\\xe1n\\x00\\xbd\\xacog=\\x01\\xf0\\xf3<\\t\\x07H<\\x01&\\x93\\xbcu\\xad\\xa9\\xbc;\\xa2\\xd6<D3\\xc6\\xbb\\xe7\\xe9\\x9e\\xbc\\x9f\\xc3\\x07\\xbd\\x9a\\xf0\\x9f<\\xb3\\xbb\\xd8\\xbcmJ\\x14\\xbc\\x0b\\x87\\xae\\xbc\\x96U@\\xbc\\xd7\\xb2\\xab\\xbd\\xf2/\\x11;\\xf3\\xe0\\xa08\\xd4\\x1e\\xa8:#\\xc03\\xbc\\x1a\\x9e\\t\\xbc\\xb6\\x97\\x1b\\xbd\\x80\\x99X=\\xf7\\xc0\\xf9=\\xe6\\xff\\xdb\\xbc\\xf7\\x06A<+L\\x98<(A\\x19\\xbd\\x88\\xfe\\x81<\\xae\\xe1z\\xbaksM\\xbd\\xf4nD;\\xda\\xac\\x1b\\xbc\\xb9\\xf2Y=\\xe0[\\x82=/{d\\xbc\\\\\\xd8;\\xbd\\xc0\\xcb.=\\xa1\\x8b\\xf0:E\\xcf\\xfb\\xbc\\xb8,`<\\x81\\x12\\xaf<Bs#=\\x11}\\x9f<\\x82\\x90!=\\xcb\\x17-<7\\xfd\\x8c<\\xf8<\\x94<\\xfbn\\x9b\\xbd(\\x12\\x94\\xbc+0\\xae;\\xa6y\\x15<\\xe4:\\x8f\\xbc:\\x97\\xde<\\xdc\\xd5\\xed<}\\xcaQ<\\xe9AH;bA\\x07\\xbd\\x93or=\\xb9\\xc7\\xe1<\\x1e=\\xa7\\xbc\\xad\\xd2\\xce\\xbc\\xd0\\xae\\x08\\xbbXP\\xb8;\\xd6vH\\xbd\\x1ez)\\xbd\\xc7\\xc8\\x9d=t\\xe7E=\\x15?g=\\xc8\\xf7\\\":q\\xaeg<\\xf7\\xdbq<\\xf7m\\xea\\xbc\\xcdG\\xac<\\x9bTB\\xbd9\\x10\\x99\\xbb?\\x18H\\xbb\\xe4\\xee\\x82=\\x07%i\\xbc\\xdb\\xc8#\\xbde7\\r=\\xf8\\xd6\\xda\\xbc\\x10\\xdc!\\xbdb\\x8c\\x1f\\xbc\\xce\\x05\\x81\\xbdJ\\xf1\\xba\\xbcK\\xad\\x86\\xbc\\x82\\xd4\\xcc<\\x80?\\x01=;t5\\xbb\\xb98@<nE%=\\xeb\\xc2\\xd3;\\x9c\\xa8q=\\xc7\\x0b%\\xbd\\xb5k\\xea\\xba[\\xbc\\xd8<\\x9e|\\xa7<Y@\\xee\\xbc\\xb2\\xf9\\x1f\\xbbH\\xea<=\\xffl\\xc9\\xbcG\\x1a\\xa8<\\xe6t3<h\\x9f\\xb5<\\xbb\\xf9\\xa5;\\xfc8\\\\<\\xc5U\\x13\\xbd`\\x96V\\xbbA\\xafh<\\x1a\\x06:\\xbcd\\x0c\\\"<\\x1f\\xad\\xa8<bb\\x8e\\xbb_V\\xca<Ss\\x08\\xbd\\xbe]s\\xbd,\\x04e\\xbc\\x1b\\xd8\\x89\\xbb\\x9a\\xcd)\\xbbE\\xa1\\x90<U\\xe5+\\xbd\\x97\\xe1\\xe7<)\\xf9\\x86=\\xd4\\t\\xde\\xbaE\\x9cB\\t\\xf5v\\xa7\\xbb\\x1e\\xcdq\\xbd\\x11\\x18\\xeb<@\\xc0\\x8f<\\xb1\\x19\\r=\\xf5\\xc9\\x04=\\xcc\\x11\\x05<v\\xc2\\x04\\xbd\\xa1\\x15\\x05=\\x10\\x14\\x94\\xbb\\x16aV;81\\x9a=H\\xa8x\\xbc\\x9c\\\"x=G;2;R\\xbb\\t=\\x8b\\x19\\x9b\\xbd\\x00\\xa5\\xed\\xbc\\xcb|P\\xbc\\xaf\\xae\\xfe<\\xc1\\xe8\\xdc\\xbb:$\\xa2\\xbc\\xed\\xf2\\x9f\\xbc|\\xce0\\xbdR>\\x02;\\xdc\\x89\\xc2\\xbcz[\\xf5;\\x95\\xdd\\xe9<\\xe3\\xed\\xa2\\xbc\\xf2\\xe1\\xd0;uS\\xb4\\xbc\\xc2\\x9d\\x0f=^q\\xdb\\xbc\\x96Y\\xab\\xbdp\\x99\\x17\\xbd\\xee\\xc9:<\\xd2\\x10\\xcf<&\\x99\\x1a=\\x0b\\x85\\xe2<\\xc4qK\\xbb>\\x88\\x8c\\xbbN\\x11\\x17=\\x05ry<\\xccwa=\\x80\\xee\\x12=y\\x03\\x9f\\xbc\\x07\\x1e\\r>\\xb9%d\\xbc\\xacAN\\xbc\\x95s\\xf8;\\x96\\xdf0\\xbd\\xd9\\xa7\\xca\\xbd\\xe8k5=\\xd3Zm\\xbc\\x0e\\\"\\xba<\\xfb\\xbd9=\\xb3(c\\xbc\\xc4\\xb0\\xce;\\xd0W\\\"=s\\x9c\\xd5<|H\\x1a=*\\xc6-\\xbc\\x15V9\\xbc\\xa4\\xbc_<\\xc5\\xd6~\\xbd\\x1c\\xe1\\r\\xbd\\xcc\\x91\\xa7<K\\xab\\x8d\\xbcM\\xe1\\x14\\xbc\\xa7\\xbc\\\"<,\\xf6\\xa5<\\x93WI\\xbc\\xae\\xbb\\x9f<D\\xa4-=\\xad\\xaf\\x19=\\xc9H\\x02\\xbd\\xfd\\xed\\xb7\\xbc\\xf9\\xf7\\xa7=\\xd7hJ\\xbd\\xb3\\x94\\x9e<\\xd5}V\\xbd6K\\xa5<\\xb33\\xc8\\xbc\\xf4\\x87\\x8e=\\xd7\\xde\\x94\\xbdWl\\x9b<`Hg=\\xc6\\x19a=\\\\\\xedL;=\\xd9N\\xbcg\\\"5:\\xddP\\xae;\\xf0\\xd5\\x96=\\x12G\\xf19\\x03\\xf0g=\\x96\\xfb6=]\\x90k\\xbdr.`<#\\xa9\\xdc:f\\x8bZ<\\xd9\\xf1E\\xbd\\xc3wx;\\x9c\\x9c\\x12\\xbc}\\xfd\\\\\\xbd3\\xe7\\x96<\\xd9\\xe4\\x0c\\xbd\\xbc\\x1f\\xb4\\xbb\\xa5\\x01\\x95\\xbcr\\x9f\\x08\\xbdt)\\x18\\xbd\\x03u\\x11=-Uu\\xbd\\x89B\\x11\\xbd\\x99\\x97\\xc8<\\x89\\xf7\\\"=1R\\x1b<\\xcf\\xf5\\xc2<V_\\xaa:\\x89,e\\xbcv\\x01i\\xbdn#q\\xbc\\\\\\n7\\xbb\\xdfT\\xba<g\\t\\xc8<\\xd9p\\xcc<s\\xdc\\x8e:x^&\\xbd\\x0cj\\x80<\\xb2~\\x92=6\\xbch\\xbc\\x89\\xfe2\\xbd\\xb0%G\\xbd\\x08@d=\\xd6\\xe8\\xb8<iY\\xab=\\xe2\\xe5-\\xbc\\x17\\x19\\xa7=3\\xcd\\xba\\xbc\\xb7\\x9f\\x89<u\\xa0\\xd0\\xbc$4;=\\xb0\\xeb\\x84\\xbd\\xc3J\\xa8\\xbc\\xea\\xdc\\xab\\xbbz\\xc9\\xd7<%\\xba\\xfa\\xbc\\xc1f+=\\xfb\\xd7\\xf1;\\x18\\x97\\xcd<t\\xb8+\\xbc\\xad\\xa7^\\xbdE\\xd4\\x9a\\xbc\\xfb\\xba\\xde\\xbb\\xed\\xcd\\x8f<9\\xd6\\x81\\xbbQ\\xd7W<\\x13\\x1aN\\xbdU\\xf8\\x11<o\\xfb\\xda\\xbc\\xe8h\\xaf<\\xc4/\\\"\\xbd\\\"\\xa7j=\\x8c.t<$\\xd5j<\\xb6\\x16-\\xbd\\xb4h|\\xbc\\x91R+\\xbdWN\\r=\\xbe\\xeb\\x98=\\x14\\xa5\\xc5\\xbd\\x88\\x95\\x08<\\xe8\\xa2\\xc2<\\xa82\\x1d\\xbc\\\"\\x88<\\xbd\\xe4?\\x14\\xbc\\x00.\\xa5=r\\xf8\\xf6;\\x93o\\xc4\\xbc\\xedf^=\\xdek\\x0b<\\xcf\\xd0\\xce\\xbc\\x17+\\x80\\xbcP\\xf0\\x90<\\xd7\\t\\n\\xbdy\\xfb7\\xbd\\x98\\x99i<\\xc9\\x00\\t\\xbd\\x89\\x82\\x12\\xbdqeQ=0V\\xfc\\xbb\\r\\x03\\xbe:\\xe4\\xb71=\\x8e\\xa0\\xe5\\xbb\\x92\\xa5\\xf5\\xbc\\xe92\\xdd\\xbc-\\t\\x1c\\xbdh\\x16U\\xbd\\xde\\xa6V\\xbc\\xaas\\x13\\xbc#d.\\xbd\\x11\\xca3\\xbd\\xf1h\\x0e\\xbc\\xf4\\xedC\\xbc\\xb6\\xb3\\x8e\\xbd\\xdd\\x15@\\xbd\\xcf\\x0c0\\xba\\xda\\x05\\x9d;\\x8de,\\xbc\\xbaW\\xdd\\xbc\\xcbO{\\xbd\\xd6\\x07V;\\xbf\\xc1\\xd0\\xbc\\x05\\xb8\\x9b<\\xa0/}\\xbbh;\\x1a\\xbdi\\xec\\n\\xbc\\x97>\\x01=s\\xab\\xa6\\xbc2\\x15_\\xbd\\xed\\xe6M\\xbd\\x984\\x12\\xbd\\x91\\x9a\\xae\\xbc\\xc9\\xcfV=\\x96X\\xea<\\x1c\\xc0\\x96=\\\"\\x06\\xb69\\x0b\\x0c1=\\x0b]\\x91\\xbc)\\x01\\xbd\\xbd\\xe2\\x06\\xf9<\\\\\\xf5\\x84\\xbcI\\x15\\x99<w$\\xd2\\xbc\\xec\\x8a\\x04\\xbdXZ\\x91\\xbc\\x9d\\x83C\\xbca\\\"\\xdb\\xbcL\\x17\\xbe\\xbc\\xb3\\xa8\\\"<\\xa8r.;\\xef\\xf6+=f\\x10\\x9f<.^\\xa1<\\x03\\xc1\\xdf<kn_<\\x10\\xe5\\x10\\xbb\\xa8\\xc0\\xd3\\xbcc2\\xd3<~KF\\xbd\\xf9\\xd5\\x1c\\xbcj\\xce\\x02=\\x1b\\xea\\x00<{\\xe4k\\xba?\\x8f\\xe0<:\\x06{:\\x16/&=T\\xbc\\xe0\\xbc7\\xb2I\\xbd\\t\\xad\\x88\\xbcz]\\x85\\xbd\\xdbW\\x03=\\x1a\\xd0\\x90=\\xe6u\\xc6;\\xf6Tk=sg\\xb2=\\xc6\\x1dz\\xbcz!4=\\xafM\\x8e=\\xdd\\x90\\x0c\\xbdC\\xceW=\\xebJ>\\xbd=\\x1d\\xe5<\\x99e!=\"\nHSET bikes:20001  model 'Jigger' brand 'Velorim' price 270 type 'Kids bikes' material 'aluminium' weight 10 description 'Small and powerful, the Jigger is the best ride for the smallest of tikes! This is the tiniest kids’ pedal bike on the market available without a coaster brake, the Jigger is the vehicle of choice for the rare tenacious little rider raring to go. We say rare because this smokin’ little bike is not ideal for a nervous first-time rider, but it’s a true giddy up for a true speedster. The Jigger is a 12 inch lightweight kids bicycle and it will meet your little one’s need for speed. It’s a single speed bike that makes learning to pump pedals simple and intuitive. It even has  a handle in the bottom of the saddle so you can easily help your child during training!  The Jigger is among the most lightweight children’s bikes on the planet. It is designed so that 2-3 year-olds fit comfortably in a molded ride position that allows for efficient riding, balanced handling and agility. The Jigger’s frame design and gears work together so your buddingbiker can stand up out of the seat, stop rapidly, rip over trails and pump tracks. The Jigger’s is amazing on dirt or pavement. Your tike will speed down the bike path in no time. The Jigger will ship with a coaster brake. A freewheel kit is provided at no cost. ' description_embeddings \"\\xbc\\x07\\xd3\\xbc&E.=>\\xbe\\x1c\\xbd\\xd6\\x05N=N\\xe5\\\"\\xbd\\xa8o0<\\x8c\\x12\\xf0\\xbb\\xb3\\xb1\\xfd\\xbc\\x91\\xdf\\x98=\\x07=\\x8a\\xbcv\\xdc\\x0b=A\\xe5\\x15\\xbdy\\xd1+=\\xeea\\xfa\\xbc\\xc3\\xbbS=\\x1a,\\x04<\\x9a\\x9f\\xfc<\\x92\\xf9q\\xbdkyz<\\xe1\\xc2\\xc4\\xbd\\xb7d\\x9f<\\xc2\\xcc\\x90\\xbd\\xcb\\xb9T=\\x93\\x1e[\\xbc\\xb6oB=,\\x057\\xbc@0\\xa4<(\\x01g<\\r\\xb2\\x8b\\xbd\\x91\\xcb\\xb4=\\x07\\xc5\\xdc\\xbc6\\xfe>\\xbd\\xd5\\xbc\\x1e=6\\x96%=;\\xb4\\xc4=yy\\xbd\\xbc\\xc9/\\xf3<g\\xa2>;(\\xb6\\xf9\\xbc\\x19\\x8e\\xe4;Jb\\xd0<\\x918\\xc5\\xbc\\x15\\xcdn\\xbcO\\x92\\x99\\xbc\\xd0t\\xd4\\xbc\\\\J9<\\xdf\\xb7j<\\xb4\\xfa\\xc9\\xbc\\xba\\x1f\\x04\\xbd\\x04\\xf0\\xaf\\xbc\\xa0Ph\\xbc\\xfc\\xb6\\xf8;\\xfc\\xcf\\x9e\\xbb\\x8a\\xedo\\xbcX\\x92\\xa1<\\x84(\\xff<\\xf0\\xd9\\xfe<\\xd9P\\xf4\\xbd\\xab\\x0bV\\xbd5U\\x9a<%\\xa6,\\xbd\\x14\\x02G=\\x8fH\\x86=\\xb0\\xf3\\x8c=t\\xd8p\\xbc\\xc7v\\xbb\\xbcXB\\xb0\\xbcU\\x1f\\xad\\xbd\\\\\\x83\\xc4<^\\xf5\\r\\xbd\\x19}x<\\xd8\\x97\\x0e\\xbd\\xe9\\xcf\\x81<DQ9\\xbc\\xbc0.<\\xa0|\\x12\\xbc\\xdf\\xff\\xb1<u\\x8f\\t=K\\xaaR\\xbdF,\\xfb\\xbcj&\\xf9<\\xa7\\xbb\\xf2\\xbb\\xa3\\x92\\xba\\xbc*\\x90\\x94<\\xb3\\xd6\\xaa\\xbb\\x1c\\x0e\\x19\\xbdp\\xe6K<\\x84\\\"t\\xbc\\x03w\\xd8<V?\\xa9<\\xbf\\xb2`\\xbdZE@\\xbd\\x8a\\x16\\xac<\\xed\\xc1\\x82<\\xfb1\\x80\\xbc\\xc2/\\x8b<\\xcfN\\x9f\\xbc\\x9b\\xe3\\x18\\xbdi\\x19\\x1f=\\x98#:\\xbd\\x0b\\xc4#=\\xf1\\x99\\xbd<n\\xb3\\x84=\\xd2QB\\xbcb\\xba\\xed\\xbch@U=\\xbb\\r\\x85=1\\xfb\\xd5<\\xf4\\xfe\\xba<\\xf2\\x9aJ<ix\\x16<\\xbd\\x16`=\\x10J\\x12=\\xfdc<\\xbd\\xd8gV\\xbd\\x10\\x02\\x01=\\x16\\x822<\\xc3.(\\xbd\\x1d\\x95\\x86<L\\xd1r;VW\\x02=}\\xc7c:\\x9c\\xb9\\x85\\xbd\\xb5\\xf3\\xdc<.\\xc1\\x07=\\x9b`\\x1f\\xbd>\\xb9\\x8a<i\\x08[\\xbc\\xc6o\\xa5\\xbc\\xbd5T\\xbd\\xec\\xf7\\x838n \\xdb\\xbc\\xa2C\\xcd<\\xca\\xd0\\x82=(?X\\xbd\\rU5=\\x0b\\xcbL\\xbcjL\\x05\\xb8\\xd3\\xbe\\xec<\\xd1\\xbbH\\xbd\\x02\\x07\\x1a\\xbdC\\xfa\\xbf\\xbc\\\"\\xa7\\xf7\\xbbR`\\x14=\\x8a\\x7f\\xda\\xba\\xb2A\\xa1\\xbc\\xea5\\x9a<8\\xba\\x1a=8Z?\\xbb\\xf5\\x99\\x00\\xbc\\xb6\\x03\\x1a\\xbdK\\xe5\\xa8\\xbb\\x9b\\x11\\xd9\\xbc.y\\xb8;6\\x9e\\xb1\\xbcfL\\xa1;\\r\\x10\\xec\\xbc\\x8a\\xb1%<\\xd6\\x10}=\\xa6+\\xab\\xbc\\x9a\\x9dr=\\xc87j;5\\xe1\\x11\\xbd\\xb2y\\x9a\\xbd\\xfa\\x8b&<\\xdd\\x05\\x82<\\x16\\xdc\\xcd<\\xba\\xaap\\xbbK9\\\"<\\x1a\\xbc\\x08=\\x06R\\xa2\\xbc\\x0f\\x1dc\\xbc\\xa0\\x81\\xef<\\xd1\\x99\\x13\\xbd\\xd5\\xe93\\xbbp\\x17\\\"\\xbc\\x18D\\x97\\xbd\\xa8\\xebg=\\x0f\\\\\\x98<\\xb3%\\x1e<\\x8d\\xa0\\xec\\xba\\x12\\x1b@=G\\xb44=\\xb3[\\\"<\\xe0v\\n\\xbcq%\\x0c\\xbd\\xe0R\\xea<r\\xfd\\x93\\xbc!\\xf5\\x8f\\xbc\\xc1\\xbd\\xe6<TS\\xc0\\xbc\\x93\\x8d\\xa8<\\xd9\\xbd\\x12\\xbd=\\\\\\x82=\\xd5\\x1b\\x08<0\\xef$=\\x12\\x8f\\x10\\xbd\\xd5\\x99\\xaa<\\xc0\\x8e\\xad;dO\\xd3;\\xe1\\xdb\\xe6\\xbb\\xad\\x9a\\x1a\\xbd9\\xab\\x98\\xbdc{\\xa5\\xbb\\xe6\\x80i\\xbd\\xf1\\xbd\\x05=\\xacCe=\\x8e\\xc51;1\\xd7\\xdb:\\xeb)\\x01\\xbd\\xb7\\x8bR\\xbc\\x1c K=m_\\xad<\\x0b\\xd0}\\xbbl\\xe9\\xc7<yE\\xe6<\\xc22\\x15\\xbd\\x96\\xdf\\x80<\\xab\\xaaX<U\\xb1G\\xbc\\xb2y[\\xbc\\xe9+\\xa0\\xbb\\x14\\xd6\\xb4\\xbd\\xd5\\xb4_<\\x13\\x0c\\x92\\xbdQ\\x1d\\x95\\xbc4\\x07\\x00\\xbc\\xe6_\\x03\\xbd\\x97\\xe0\\x1b\\xbc\\x9e\\xbe\\x84\\xbc0\\xb7\\x9d</\\xab\\xf8\\xbc\\x86\\x1d\\xf6<8&\\x99<\\x0c\\x14\\xa6\\xbc\\x14C\\xf1\\xbbNoO\\xbd\\\"\\xdf\\x1c=Y\\x03\\t=\\xf6L\\x1a=\\xbfr\\xda\\xbb\\xdda\\x01\\xbaZ\\xfd$=3\\xa5\\xe1\\xbcQ\\x81@=\\xa4M\\xd8\\xbc\\xfb\\x17@=\\xf1\\xf8e=\\xe5,\\xf5\\xbc\\x8fX~=R\\xbc\\xf8<\\x18\\xc6\\x99\\xbd\\xf8\\xa3=\\xbcX\\x9aW=\\xa4\\xd0\\xc5\\xbc:#\\xb8\\xbb\\xe0\\xe6^\\xbd[\\xc1D\\xbc\\xa3\\xf8\\xfd<\\xad\\xfe\\xcb\\xbcX\\x85\\xd5\\xbcx\\xf4\\x8c\\xbcjk\\xda\\xbb\\xf8\\x98\\xa4<\\x16\\xb5/\\xbd\\x0c\\xd9\\xad=\\xa8\\xfe\\x84<\\x0c\\x95\\r\\xbd\\xa1\\x8a\\x98<\\x05\\xbf|=vf\\xd4\\xbc-w\\xb7\\xbb\\xa4\\xf4\\xff\\xbc\\x8bSZ<W\\xfa\\xe0\\xbb\\xd4\\xa1\\xbf\\xbc\\xe7%\\x12=\\x92\\xcf]<\\x1eO\\x87<@\\x86~\\xbdo\\xdc\\x88\\xb9\\xc3\\x19\\xac<6\\xf4\\x82<\\xb8D\\\"=!\\xec\\r\\xbc6\\x9c\\xea;\\x16f\\x89;\\xacq\\x8b\\xbdb\\x18\\xe7\\xbd\\x10\\x97}=v\\x16(\\xbdU\\xe5\\x9c\\xbcrK\\xf7<\\xc5\\xf2\\xf0\\xbcB|[\\xbd\\xee\\x1cT\\xbc\\xc6\\x8b\\xfc\\xbb\\xe2\\x80\\xb8<\\xc4(\\x08\\xbdEp\\x18\\xbd\\xef\\x87~\\xbdp\\xd0\\x19=\\x95%7=\\x03\\x1d\\x08\\xbc{\\x00\\x89<\\xcb;\\x05=f\\xd5\\xa8=!\\xb7\\xc7<%\\xf9\\xcd<\\xda8\\xec;\\xcfUM\\xbd \\xd2[:\\x86\\xb1\\xc9\\xbc\\xdb\\xb1\\x10\\xbd|A8=\\xf9I6\\xbd\\xe6\\x7f}\\xbd\\xc8\\xad\\x8b=]\\x90,< \\xbb\\x88\\xbc\\x83\\xc6\\x93\\xbc\\x14\\xbcO\\xbd\\xaa\\x93\\xcf<\\xf8R-\\xbb\\x1a\\x99\\x8c\\xbc]\\xa0\\x05\\xbd\\xf64\\x99<*\\xbbm\\xbd]3\\xd4<\\xa9\\xb9\\x99\\xbcDC\\n\\xbd\\x15\\n&\\xbc7\\x15\\xa0\\xbd \\x8d\\x96\\xbac\\xacZ<ME\\xb3\\xbb\\x9a,\\xfb<\\xa3\\xb9\\x85\\xbc*U\\x06<\\x90\\x9f\\x14\\xbc;\\x818<\\xd2\\xdd\\x07\\xbd\\x18E\\x92=O\\\":\\xbb\\x9a\\xd8\\xc8\\xbc\\x9d\\x0b*=\\xb1\\x85\\x15=Q\\x1d\\x8a:\\xea;n=\\xd8\\xe5\\x85\\xbd\\xdd\\xef\\x06\\xbd_\\x9c#\\xbd.2\\x88=|\\xcf\\xb5<\\xf8\\xc7\\x19\\xbdofu<\\xa5g~<\\x1a\\xc1u\\xbbv\\x81\\xde\\xbc\\x10\\xb9\\xbe\\xbdt\\x0f\\xf9<\\xab6F=\\xfe\\xbb\\x1c=\\xcd\\xbc\\x08<\\xc8\\xd1\\x80=[\\xf9j\\xba\\xfd\\xef\\xfb<\\\"\\xb1\\xa7=\\xcc\\x06z<6\\xba\\xa2\\xbc\\x90s\\xb6<R\\xb3\\x95<\\xc4\\x91\\x11=G\\x12\\x03<\\xf5\\xfc\\x1d\\xbc\\xed\\x95\\x00<\\x07\\x99\\xc6\\xbb\\xbb\\x98\\xbd\\xbd\\xe6\\\\\\x87\\xbdU?\\xac9\\xb2\\x8c\\x1d\\xbd%\\x8c\\xae<\\xca(\\xb0\\xbc\\x8cW\\xbf\\xb9\\x10.\\xe2\\xbcP\\xc6\\x9f\\xbc\\xb0r\\xff\\xbb\\xed.\\x85\\xba5t\\x0e<~K\\xd9\\xbc\\xbbb\\xca\\xbdR\\xab!<\\xa8\\xb7r=\\x1a\\xf6\\x8e\\xbc\\xbe\\xa8\\x00<?\\x9d\\xc7;\\x05\\x0eZ\\xbdl\\xd3\\x93=t\\xf1\\xaa\\xbc\\xe8\\xf1\\xb2\\xbd\\x97\\xff}\\xbdH\\xdd\\xad\\xbc\\x01t\\x06<\\xd1\\x89y<\\xa8D\\x1d\\xbbOt#\\xbc^Q\\xa4=\\xfcT[<\\\"(\\xb6;\\xc3}\\x94\\xbce\\xadj=\\xcc\\xa1};\\\".\\xa5\\xbb U|\\xbc\\xfa\\x7fw=\\xe9\\x94\\xe4\\xbc?\\xa9j\\xb9\\xd5X\\x88\\xbd\\x8f\\xd2\\xfb\\xba\\xe6\\x08\\x9b\\xbc \\x12*\\xbd\\xa0\\xe1;\\xbde\\xa6\\xf0;\\xed\\xe5\\x1b=O\\x9f+\\xbc3\\xd82\\xbb\\x7f_~\\xbd\\n\\x17S=\\xc7\\x08\\x04=\\xc37\\x80\\xbd\\xaa\\xd5@\\xbd\\xd2\\x05\\xf8\\xbc}\\xa4t\\xbd\\x9e\\xa5\\xa4\\xbd\\x0c\\x88\\x07\\xbd{\\x03\\x91=\\x89 |=\\xe8\\xe0\\x9c<i\\x1e)\\xbc\\xda\\xbb\\x05<|\\x17\\xdb;\\xef4\\x03\\xbd6\\xed\\x16<\\x9c\\xca\\t\\xbd/b\\x84\\xbc`\\xb2\\xe9;\\x01\\x87\\xa1\\xbc\\xdf\\xb2\\x07\\xbb\\xa2C\\x15\\xbd\\x81\\xef\\x0c<\\xde\\xd7\\xd7\\xbc}\\xacx\\xbc\\x93X<\\xbd\\x80<~\\xbd~\\xca\\\"\\xbd\\xc8\\xa1\\xd4\\xbc[\\x1f\\x86;<\\xcb\\\"<\\x88\\x8f1\\xbcXX3=\\t\\xbd\\xb8=\\x13\\x98A\\xbbQ\\xb5p=\\xe2\\x0bP\\xbb\\x8af[\\xbc\\xc7Y\\xcc<\\x11F\\x03\\xbd\\xfdf\\xb8\\xbc,\\xbd\\x14\\xbd\\xb5\\xa8\\xd1<\\x88O^\\xbd!\\xe3 =@mJ\\xbd\\x06\\xef\\x08<\\xcf\\x9aq<;!\\x05=\\xe8\\x90\\xc5\\xbc$\\x963\\xbc\\xe4z\\\"=\\xb9\\x8a\\xa3;\\xf1\\x90W<\\xa5n&<\\x0f@\\xd0\\xbc\\x8d#\\xce<\\xe9\\t:\\xbd\\x8d\\xac\\x88\\xbd\\x9d\\xa0\\xd1\\xbb?3\\x05=\\xf5Xc=\\x96qn;,\\x04s\\xbd\\xff;\\xb9;\\x9b\\x93\\x9b=[#\\x8c\\xbc\\xea\\x8bu\\t\\xb6\\xc8\\xec\\xbc\\x00[%=D+1\\xbd\\xe3l\\x9f=\\xdf\\xdd\\xbd\\xbc\\x8eE\\x05=\\xa3\\xe9O<\\xdaR\\xe6\\xbcE\\xfa\\x04<\\xa9\\x1b\\x80<\\xa0-9=\\xac*M=\\x06p\\xb3\\xbcU\\xba\\xee<@\\xd3R\\xbbE\\xfa\\xdc<\\x8c\\n\\xa7<\\xd1\\x9d\\x82\\xbcP\\x9a\\xa0;\\xa1\\xdad<^\\x91\\x85:k\\xf3\\x97\\xbc\\xa7f\\xad<\\x1e\\x14\\x87<\\xe9\\x1fY\\xbc\\x97\\xbf\\x9e<\\xb7\\x1e\\x9b<j\\x00\\xda\\xbb\\xc6:g\\xbcY\\x1e\\xb3\\xbc\\xe9\\x03\\x88<}\\xe2x\\xbcr|\\xe2<\\x0c\\x98&\\xbd6\\xcfE=\\xcb\\xca\\x8b\\xbds\\xc0L\\xbcSE\\xb3<|\\xfc!=\\xa5\\xaf+\\xbd_\\x9d\\xd5<9$~<CQ\\xa4\\xbc\\x9b\\xaa\\x93=\\x9d\\xcf\\xaa\\xbc]\\xea\\x84\\xbc\\xeb\\xb2\\x9e=\\x7f\\x1f\\xd6\\xbcy_\\x91<\\xfe\\xbd\\x12=\\x19\\x8ej\\xbc\\x1fj+\\xbd\\xb4s\\x7f=\\x9fF\\x00\\xbd\\\"\\xbe\\xe5<\\xa0\\xa0/\\xbc@\\xfc\\x97=|g\\x89<\\xcd\\xe3\\xa7\\xbc\\x1c~3\\xbd\\xaf)W\\xbc\\x02\\x13\\x1f\\xbd\\xdb\\xce\\x00=\\xa2\\xc9\\xd2<\\xb8\\xe7\\xa9\\xbd|\\xe4\\x08\\xbd+U\\xac\\xbcbF\\x8b\\xbb\\xf25\\x9a\\xbc\\x92Z\\x9b\\xbc\\x93!m=L\\x7f\\xca\\xbb:Fq\\xbc2\\x11\\x7f<\\x94\\xe3\\x8a\\xbcd\\xf6\\xb8\\xbdJ\\x82\\xbd\\xbb\\x98\\x92\\xe7<\\xa9\\x1b\\xa2<a\\xa1\\x02=\\x0c\\xf4\\xe2\\xbc\\xa1K\\x0c<Y\\xd0\\x98<\\x8b,\\xc7\\xbc:\\xaa\\\\<\\x8d\\xe4\\t\\xbd\\x9f\\xef\\xdd<\\xd1(x\\xbb\\xd3p\\xb4;\\x05<9=M|{\\xba\\x92q\\x97=\\x86}\\xcd<\\xa1(\\t;\\t\\xed\\xad<*\\x93\\xfc<\\x03\\x1fR:\\xd1\\x8e\\x8d<4\\xb4\\xdc\\xbcXy\\xb99\\x04\\xdd\\xfd\\xba5\\x10\\xa1<\\x8c\\xc7#<\\xff(\\x16=\\xe8 \\xb4<\\x8am\\x88\\xbc\\xfe\\x97\\x8f<-4\\xf7\\xbcJ\\x84\\xad\\xbcj\\x98\\xbe\\xbd\\xbb\\xd0\\\\=\\xa1V\\xfd\\xbc\\x9a\\xe15\\xbdr\\xbfE=DY\\xe9\\xbb\\x0b\\xff\\xc0;e\\x96*=%\\xa2\\xfd\\xbb0{^\\xbd\\xb5\\xbaZ\\xbc}\\xe1\\x0f=\\x9fm==\\xb8\\x845;\\r\\xe2L\\xbd#\\x9b\\x05\\xbc\\xd0\\xe7$=\\x8bF\\xdc\\xbc\\xc2\\xdf6\\xbd\\xcevr=y\\x87\\x88\\xbc\\xf4\\\"\\xef\\xbc\\xec& \\xbd\\xdeh+<\\xc5\\xcf\\x94\\xbc\\xca_\\x83\\xbc\\xb6\\xad\\xdc<!\\xbc1=\\xeb1\\xba\\xbc\\x03l\\x19\\xbd\\xd3\\x88@\\xbcVat\\xbc\\x1d\\xbbQ\\xbad*-\\xbd\\x14\\x9eA<\\x1c\\xf0\\xb2\\xbc\\xea\\xbd\\xf0\\xba\\xc8\\x84\\x1a=\\xa4\\xed\\x10=\\xf0~\\x0c=br\\xf4;\\x14+\\x85\\xbc\\xfckz;\\x8f/~\\xbc\\x9ei}<\\x86\\x93\\x04=i\\x94\\xe2\\xbb\\xd4\\xde\\x1e\\xbdo\\xa9\\x0f\\xbd\\x89 :\\xbcf\\xba2=G/\\x89\\xbdt\\xa1\\x88=\\x04\\x9f~==\\x95\\xd2;v\\xc5\\xaa\\xbc\\\"\\xde\\xc6\\xbc\\xe5xJ\\xbdPc\\x8b\\xbb\\tpQ=\\x03r\\xb3;\\xd1\\x81\\x0e=\\xaa\\xca/=\\xdb\\xaf\\xa2:\\x83\\xad\\x93\\xbc\\x17s\\xa9<\\n1\\x9d=$\\x95B\\xbb\\x90\\x1cr;s4B=T\\x05\\x83\\xbb\\x99\\xac\\\"=\\xb6\\xe5\\xaf\\xbd\\\"K\\x83<i\\xd2K\\xbdPA\\x05:\\xe7\\xca\\xf2\\xbc\\xe6\\x1d\\x86<\\xbf\\xe0\\xbf;\\xed\\x82\\x05=\\xfa#\\xc0\\xba\\xad\\xd4\\t=\\xeeY\\xaf\\xbbY B\\xbd\\x87\\xd7\\x93<\\xac\\xb1k\\xbd\\xf4\\xa2{\\xbdg\\xe7\\x11\\xbd\\x14:\\xfe<\\xf4\\x87\\xa5<MU9\\xbd\\xa8\\x99;\\xbc\\xf3\\xbd\\x9b:#;\\xad\\xbbA\\x91\\xa2\\xbcl\\xd8v<\\x85\\xb9\\xb4;\\xd1\\x93C\\xbc\\xfb\\x82F<\\x17\\xd7&<\\xab\\x9e\\xdb\\xba\\xbf\\x8f\\\"<a\\xa3\\xa5<\\x13-9=F\\xe0\\xbe;\\xe2}/\\xbd\\xb0q\\xab\\xbb:\\xee\\x13\\xbc\\x0fU!\\xbar82;7\\xd5\\xc0\\xbc\\xcf9\\xa9<\\x81\\xb0p\\xbd\\xc1-\\xd6\\xba\\x03\\xac\\x96\\xbb\\xe1G\\xdd<1\\xb9m;w*\\xc6<-G(\\xbd\\x1fa>\\xbdW\\xc2\\xab=\\xea\\xbb\\xed\\xbc\\x14\\xd4i\\xbc\\n\\xcf\\xe9\\xbc\\xf9\\xc4\\xe7<r\\xd4\\x95;\\x80|E\\xbd\\x0e\\xc3\\x0f\\xbc\\x0b\\x91\\x87\\xbd\\xdb\\xd1\\x0b=)\\x9ft<\\xed?\\x8b=\\xf8\\x11\\xb8<l2\\t\\xbc\\xd9\\xbd2:=a3\\xbc{\\xd4\\xe2\\xbc}\\xb9\\x8c\\xbd2\\x1a\\xb7;\\xa7\\xfd/\\xbdp\\x0e\\x02=:\\xfe\\xe7<\\xba~\\x12=\\xc4\\xf7\\x1b\\xbd\\x84n\\\\=*\\x00\\xb6;\\x98\\xec\\xc2<Dfz\\xbc\\x16\\x9b@\\xbd\\xdd\\xde\\xda\\xbc\\x9d\\xdd\\xc3\\xbd\\xdb\\x1a\\xf3\\xbcq\\x85z<\\xeb\\xd1\\xb3=H\\xf7m=X\\x88`=\\x1a\\xe8\\x96\\xbb\\xc9\\x98\\x8e<\\x06\\xfem=\\xfa\\xe6\\x88\\xbc|a\\xd5<\\x06\\xc2`<\\xd2F\\x9e\\xbc\\xc5?Z\\xbd\" \nHSET bikes:20002  model 'Hillcraft' brand 'Bicyk' price 1200 type 'Kids Mountain Bikes' material 'carbon' weight 11 description 'Kids want to ride with as little weight as possible. Especially on an incline! They may be at the age when a 27.5\" wheel bike is just too clumsy coming off a 24\" bike. The Hillcraft 26 is just the solution they need! Imagine 120mm travel. Boost front/rear.  You have NOTHING to tweak because it is easy to assemble right out of the box. The Hillcraft 26 is an efficient trail trekking machine. Up or down does not matter - dominate the trails going both down and up with this amazing bike. The name Monarch comes from Monarch trail in Colorado where we love to ride.  It’s a highly technical, steep and rocky trail but the rip on the waydown is so fulfilling.  Don’t ride the trail on a hardtail! It is so much more fun on the full suspension Hillcraft!  Hit your local trail with the Hillcraft Monarch 26 to get to where you want to go.  ' description_embeddings \"\\xdf\\xab?\\xbd\\xef,p= \\xa5+\\xbd/\\xd0\\xaf<\\x80>\\x12\\xbc}\\xeb!=a\\xc9R<e\\xea\\xf7\\xbc\\x82\\x99==\\x84\\x8cC=\\x95\\xfd\\x92<\\x82\\x9a(\\xbc\\x95P}<\\xb6\\x8f[\\xbd\\xdfw\\x91=\\x99V\\\"\\xbdaB\\xdc\\xbc\\xb3\\x8e\\x08\\xbdH\\x9e\\x04<\\x06\\x96v\\xbdeAD\\xbc\\xf0\\x993\\xbd\\xa9\\x96\\x04=\\xad\\x10\\x1d\\xbd\\xfc\\xfbn\\xbdTe\\x82=\\x93K\\x14\\xbcl\\x8a\\xc7\\xbb\\xc9C\\t\\xbdLi\\x88\\xbbYn\\x01\\xbc\\xe4\\xcd\\xc5\\xbct\\xc20=Z*+=\\xa5\\x82\\xb6<\\xe7\\xd4}\\xbc$\\xf42=a\\xc8\\xd1\\xbc\\xa0\\x13\\x15\\xb9<\\x91@<GJ/=X\\xd8.=\\x0c\\x10\\xa4\\xbc\\xe7v\\x9d<X&\\x1f\\xbd\\x16m\\xa8\\xbc\\xad7D<\\x0b\\xd1\\xd59\\x13\\x85\\x1f\\xbd\\x18\\x8a\\x80<@G\\x81\\xbcU\\xfe-=E\\xaa\\x9e<G\\xad\\t<\\xa3P\\x0e<\\x13\\xc1\\x07\\xbd\\x82\\x9c\\xc6<\\xc2\\xfdk\\xbd\\x9ex4\\xbdv\\xf0\\x16=Tbb\\xbd\\xb0\\x9a\\xad=/?\\x08=f\\rl=>*\\\\=\\xc64]<\\xb7\\x8f\\xcd\\xbb_\\td;\\xfes%;\\x8f\\xf5D;\\xf5x\\xea\\xba\\xdcLU\\xbd@k\\x88<\\x01~f<\\x1e\\x15\\n\\xbc\\xeb\\x00A=\\x92\\xd8\\xb8<\\x92\\x11\\xb5\\xbb\\xb1\\xad\\x98\\xbb \\xd5\\xa3;J\\xf7\\x10=\\x15\\xcc4\\xbd\\xe2`;\\xbd\\xe7\\xa1\\x1c\\xbd\\x19{\\xf1<\\xeb\\x8a\\xfa\\xbb\\x9e\\xea\\x85<\\xa5\\xee\\xa5<m)\\x87=\\xa6\\xfc\\xd3\\xbbv\\xcf\\x94\\xbc\\xff\\xf7\\x0c\\xbd\\xd5\\x8ev\\xbc\\xca\\xe9q=g^\\xb6\\xbb+\\x8c\\x85=7R0\\xbd\\x88oo\\xbc\\xf9d6<\\xf1\\x0f\\x8c\\xbd6\\x88d=\\xa0\\x88\\x15\\xbc\\xd3d\\x10={tl=\\xa6\\xca\\x96\\xbc\\xdb\\x07\\x98=?+\\xc0;\\xa4\\xa9L<xr\\x15\\xbc@\\x0b\\xdb\\xbb\\r\\xc1\\xdd\\xbc\\x1dq\\xbd=lV\\x14\\xbc&\\xca\\x1e\\xbd\\\"<g\\xbd!u\\n=s\\xde\\x02\\xbc\\x92\\xfb\\x8b\\xbd`\\xef8=\\xc9\\x00\\xd1<\\x7f\\x03\\x9f:C\\xc0\\xc9<\\xd1\\x91F\\xbd0\\xabK=m\\x087\\xbdYR^\\xbd2\\xa9\\xf6<~6*\\xbc\\xfb\\xd6\\xa7:\\x8bV5\\xbd\\x9c\\x9eq<\\xd7&\\x91\\xbd\\xe9O\\x0f=p\\xc8\\xba\\xbcjf\\t\\xbd\\x0b\\xdbv=#$\\xc3\\xbcJ\\xcb\\xe5\\xbb>\\xb2\\xce;\\xfa\\x90u\\xbd-\\x07\\xd3\\xbc\\xc6G\\x94\\xbd\\x9fi\\xd9\\xbcP\\x94p=\\x02\\xff4\\xbd\\xca\\x98\\x849\\xf7f<\\xbc\\xe6\\xeaQ;\\xba\\xe9\\xb1=o\\x10\\x1f\\xbd\\xfc\\xecO\\xbcV_\\xa2=\\xed\\x18 \\xbd\\x1e\\xd9\\x83\\xbc\\xf1[\\\"\\xbdR\\xd6\\x04<\\xf4a\\x08\\xbd7@`\\xba\\xff}\\xb6<\\xa4-\\x7f\\xbd\\x84\\xb4\\x82=9\\xe8d\\xbd\\xe1\\xbd\\xab\\xbc\\x7f\\x10\\xab\\xbd\\xc2\\x1e\\xac<w$F<\\x9aT\\xdb\\xbc\\x9e=\\xe2<=L\\xab<^O\\x04=p\\xc9>\\xbc\\xd4(+=\\xc0\\xb61<\\x9d1a\\xbd\\xd2\\xae5\\xbc>0<\\xbb9d\\xa9\\xbd\\xc5\\x90\\x08=\\xe7#\\xf7<w)\\xd4\\xbb\\x0c\\x93u:D\\x90\\\"=z\\x957=\\xe0\\xb0K\\xbb\\xb3\\xf6\\xa7<\\\"\\x02\\x03\\xbd\\x8b\\xd4\\x9f=)\\xf3\\xd5\\xbc\\x12\\xd3\\xa7;\\xc2\\x8cM\\xbc\\xd7\\xcc0<\\xafh\\xc9\\xbc>\\xd2C\\xbd\\x10]H\\xba\\x8dN1=\\xba]\\xdb\\xbc9\\xf7\\xd3\\xbc\\x15\\xa2\\x14\\xbd\\xb8\\xa8\\xde;\\x93\\x95\\xb6\\xbatz:\\xbd\\xb2(:\\xbd\\xd7\\x9f\\x04\\xbe\\x19\\x1e\\x80\\xbc\\x14\\x0fo\\xbc\\x04\\x8e\\x84\\xbbH\\xe5\\\\\\xbc3\\x97\\xb7:\\xa9j\\x88<\\xdc\\t\\xb6\\xba\\\"bv\\xbc\\x18\\\"\\x13=\\x10\\x89\\x97<\\xb5a\\x95\\xbd\\xe5=\\\"<.\\xc6/\\xbc\\x8b \\x89<|s\\xd3\\xbc\\x1f\\xc3#=\\xc9\\x8c\\x07\\xbdkR\\x11\\xbc\\x89C\\x9d\\xbc3\\x8f!\\xbd$gV=\\xc1\\x1d\\xf2\\xbc\\xc7\\r\\x9a<3\\xf6\\xf5<\\x08\\x9b+\\xbc\\xfd\\xaf[=\\xea\\xd8\\x86\\xbdb\\x1b\\xf3;W\\xde&\\xbd\\xa3l-\\xbct\\t\\xe1\\xbc\\xb1\\xc8\\xfb\\xbc\\xdd\\x0fI\\xbdp2\\x0e\\xbd\\\\*\\xb0<\\x8eb\\xf3<\\\"r\\xb8<<\\xaa\\xb5\\xbc\\xc8\\xbf\\x87<kk.\\xbc05Q=CP\\x9d<v\\xd3\\xa5=\\xcf\\xa5\\x1e\\xbc\\xcdV\\x03;\\xab\\x88\\xfe\\xbc(h\\xf5\\xbc\\xa2\\x8a*<T\\xbdF\\xbd\\xc2\\xf6\\xe1\\xbcs-\\xa6=\\x8d\\xe0\\x01\\xbd\\x1f}\\xf29P\\xed8\\xbd\\xea\\xbb~<|\\x95\\x85\\xbd\\xde@\\x8f\\xbdt\\xf2\\xca\\xbb\\xd9\\xb4\\xba\\xbc\\xa8Q\\xc0\\xba\\xc8\\x97\\xb6<<\\x08\\xef\\xbc}e\\x96=\\x9d\\x12\\x97;\\xe2\\xd5\\xf17<\\xfe\\xdf<\\xa4\\xea\\x89<\\x89\\xb2J\\xbcL\\xe8o\\xbcC\\xb6\\x19\\xbd/\\x83\\xee<\\x95)/\\xbdSbT\\xbd\\x1d\\x9e\\x17<\\xdd\\x81\\x86<\\xad\\xbf\\xdb\\xbc\\xf6B\\x18\\xbd\\x1d\\x0e\\x9a\\xbb\\x009\\x12<\\x19\\x89\\xfc\\xbbr\\xe3>\\xbc\\x9c\\xb1\\x07\\xbdtf(=\\x8bF\\r=\\xec\\xbf\\xd1\\xba?\\x01\\x8b\\xbd\\xd3\\x9f\\xce<]n\\x1d\\xbd\\x03<&\\xbd\\xbb\\xdd\\xbf<\\x00\\xc2\\xae<\\x13\\xa6\\x95\\xbc\\xdc\\xf2/\\xbd\\x9d\\xb8o\\xbc\\xb4\\x0e\\x92<2E\\x01\\xbdh\\xf3[\\xbd\\xff7\\xf6\\xbb%\\x9cj=\\xab\\x81\\x9d=p5p\\xbc\\xab6\\x88\\xbc\\xd7E\\x97<G*_\\xbc8a\\xa6<r\\xde\\xe8;\\x02\\xd4\\xd59\\xdf%\\xaf\\xbc7\\xb2\\xf0;X\\x0c%\\xbc!\\xad*\\xbc@\\xc1$:c\\xc1\\t<V\\xe48\\xbdM\\x1c\\x94=\\xddYC\\xbc\\xd8\\xd9O\\xbb\\x0c\\x19\\x8f\\xbbH;@;\\xb1\\xaa\\x8e<\\x12\\xd1\\x9a<\\x17\\xe1U\\xbaF\\xf5\\x8c\\xbd\\xd2\\xe8\\x1c=\\xeb\\x9b\\x9c\\xbdX\\xdb\\x1d\\xbc)\\x96O;\\x10,e\\xbd\\xda\\xbbH;\\x83\\x1e#\\xbdw@\\n=\\x08 \\x90\\xbc\\x8a\\x92\\xfd<p\\xf6\\x94\\xbc\\xb3\\xf7,\\xb9\\xe6\\x0c\\x05=\\xd7\\xe8\\xb5\\xbcz\\x88\\x0e\\xbd,\\x8a\\x89\\xbcIQ\\x8e<4Cl\\xbd\\x05\\xb8\\x1e\\xbd#\\xfb\\x1b=\\x05}\\x9b=\\x8b,\\x1a\\xbd\\x87\\x88\\xa3:\\xfea\\xd0<\\x96:\\xac\\xb7;\\x9a0;\\x92\\xc8\\xa6=\\xd7\\xbd\\xd9<9\\x12\\xe5\\xbb\\xb5\\xdft=\\xa7\\x02x<Pg%\\xbd\\xe3\\x82\\x04\\xbdM?Y\\xbd[\\x82\\xf4\\xbc\\xcbz\\x0b<\\xe2\\xfb3=h7\\xfa=U\\xb7C=\\xaf\\xc1\\x19\\xbd\\x81\\xdd*<~\\xd4S=\\xc7CF<\\xe7\\xf4\\xf9;\\xc0\\xec\\xc0;\\xf6C\\x99;]?\\xde<,\\xf0\\x13<\\xd7\\xedd\\xbc7\\x8e\\x06\\xbc\\xba\\xe9\\x80\\xbb\\x086B\\xbdfyR=\\xcf\\\"\\x01=8u`\\xbdt\\xc8\\xd3\\xbb\\xa8\\xf0(<\\xc1\\xe4p<e\\xbb\\x80\\xbdGl+\\xbb\\x1f\\xb3\\x1b\\xbcP)\\x1c=\\xb3I\\x12=:\\xde[\\xbc\\xd3\\x8bp\\xbd\\xa5Z\\xa3=\\x13C\\xbd=}\\xa7\\xff\\xba\\xb54\\x91<0r\\x83;5n\\x80\\xbd!\\x89\\\\\\xbd\\xbcp0\\xbc\\xf7}a\\xbdJGh\\xbc$\\xfb\\x92<+\\xfa\\x15\\xbc\\x02\\xe6g=\\xe5\\xe6\\xf8\\xbbsD\\x89;\\x98\\xbd\\x90=\\xec\\xdcY=\\xa3\\xff\\\"=\\x9a\\x83:=E\\xda\\x03=w\\xaf\\xcf\\xbc\\xa0\\xf0\\xe6<n\\x96\\x12<\\x01~\\x82=\\x86])\\xbd\\xee\\xf8\\x14\\xbd\\x97\\xa5\\xc5\\xbc\\xceM\\x93<]p\\xa0\\xbd\\xfeA\\x8d\\xbdNtd\\xbdb\\x86N;\\x86\\xa1[=\\xb5\\xa1\\x9b\\xbb~\\xf6\\xc0\\xbb\\xf4\\x88\\xa6\\xbd\\xf6%J=\\x95\\x0b&\\xbd\\xbaX\\x8d;,z\\xbb\\xbc\\x85\\xa7\\x18\\xbd}\\x11>\\xbd\\x84n\\x9f\\xbd\\xf9\\x8a\\xa0\\xbb\\x10h\\x83=rD]<+\\x94\\x86<C\\x01R;D;\\x8b8\\xf6Ba\\xbd\\x0b)\\x01\\xbd\\xb2\\xee+<v\\x7f\\xb7\\xbc\\xac\\r\\xab<\\xc9\\xcb\\xad:\\xd4\\xc1\\x1a=\\x85\\xa4M\\xbcY9A\\xbd\\xe8\\x1d\\xdd<\\x1f]\\xa7\\xbc\\xdb\\xa1i\\xbd\\xb54\\xff9\\xe7W\\xe2\\xbbtIL\\xbd\\xa7\\xc7\\xc3\\xbc@\\x05H\\xbc\\xc5i\\x02\\xbd>\\xa6Z<\\x89\\x1eD=\\xdb\\x8d\\x9d=\\xc3\\x05Q<v|y=v\\xb7\\xe3\\xbc\\xc4\\xd55\\xbc\\x86\\x88\\x9e=\\xd5\\x8f\\x86\\xbb\\x00.\\x19\\xbd\\xa9\\xad(\\xbc\\xbc\\x15\\xb6;\\xb5\\x01\\xa6;C@\\x1c\\xbb\\xf6\\x16k\\xbd\\xd5)\\xd4;8\\x8dA<p\\xbf%\\xbb>\\xa9\\x95<0~\\x8c\\xbb\\xe2\\xa5\\x12=\\xdav\\t\\xbcE%+=\\xde\\xd7\\xcb<\\x1e\\xdc\\xae;3\\x08\\xf2<\\x1e\\xf3\\x0e\\xbd\\x95>\\xa7\\xbd\\xc9\\x10:\\xbdR\\x0b\\xe2;\\xb3\\x9cN;=6\\xf1\\xbb\\xe9\\xed-\\xbd&\\xc6Q=\\xcf\\\"\\xa2\\xbb\\x1bgx\\xbc\\xc9(u\\t\\x8e\\xe8W\\xbc\\xb7G+=\\xff\\x7f\\xed<\\xa4\\xc1\\x1b\\xbd6\\xac\\t\\xbc\\xd3\\x15#=H\\x9b\\xdc<.k\\x03\\xbb\\xe2&n\\xbb\\xa8\\xbc\\x17;= \\xb6=f\\x89\\xa2=\\x1a]\\xb1\\xbc8wF=\\xb7\\x1b\\xbc\\xbcT\\xd7\\x06<\\xb7\\x9a\\xa1<<r\\xf0\\xbae\\xb8\\xc0<=pA<\\xdf\\xb2\\xf6\\xbb\\x8d\\x12\\n=\\x89\\xf8\\x06\\xbd\\xef\\xa2K\\xbd\\xc7\\x9af=|\\xaeK<\\x80\\xe7\\x9b\\xbb\\x0f\\x9a\\xdb\\xbc\\x04J\\xdb; \\x1eV\\xbc\\x9d8\\xfa\\xbaw\\x85\\x13;\\xe9\\xe9\\x03\\xbd\\xb7\\\\g<\\xdew\\x0e\\xbc\\xa3\\x0c\\x06\\xbd\\x03]T\\xbcy\\xde.=\\xfb\\xaa\\x98<\\x108\\x81;\\xe1\\x85\\xa4<M\\xad\\\"\\xbb\\x07\\xfb\\x12\\xbc\\xd8z\\xe2\\xbb\\xd2\\xad\\xd4<\\xf1\\x96i\\xbc\\xabf\\x90=&\\x7f\\n\\xbd\\x8b\\xf7\\x9c;%\\x83\\x94\\xbbn@\\x0b\\xbd:\\x82\\xbc\\xbdx+6=\\xa9\\xea\\xc4<\\x08\\xf3\\x18<\\xd1_a=\\xed\\xcb\\x98=Q\\xb1\\x93<\\xdc\\xc6\\xc3\\xbc\\x17\\xfd\\x9d<\\xac!\\xa6\\xbc\\xbdp\\xcc\\xbc\\xf6\\x85\\xb4\\xba\\x1b\\xc5\\r=S\\x8f$\\xbd@}-<\\x89\\xf0\\x1e<\\xaa\\x83\\x9b\\xbc\\x02\\xc6\\x9c;y\\x94i\\xbd,\\x19\\xb2<\\x12\\xb2\\\\\\xbc\\x81\\xef\\xa3\\xbb4(j=A;\\x1d<\\xa1\\xb8\\xef\\xbc\\x1d\\x18/\\xbcAgV=\\x87|\\x1e\\xbd\\xfc\\xed\\x12\\xbc\\x89\\x1d\\xa2\\xbc0D\\xbd<\\xa7\\x1e\\x9d\\xbc\\xc8N\\x84<\\x9e\\x80\\xae<\\xc6\\x13,\\xbc\\x91\\xb3\\xca<\\xd7\\x15\\x8d;\\xb7\\xe9\\xe1\\xbb\\xca\\x0b6<\\xb1\\xa6\\xf5<\\x17_\\x04=!\\x84h=\\xe3\\xab\\t;p\\x9b\\xf2<;7\\xc5<=4/\\xbd\\x93\\x1c\\xf3<\\xe2\\x84\\x1f\\xbb\\xe6\\xd1\\xae\\xbc\\x81\\n\\x06\\xbd\\xdbK\\x90<\\xb08Z<*\\xc7\\x1d\\xbc\\xd6\\xd5\\xae<\\xbb\\xbc\\x01\\xbc3S\\xe5<i3\\x0e\\xbcX\\xe4\\x04\\xbdsm\\x86\\xbd\\x86n\\x86<\\xb0\\x80\\x02\\xbd\\xfa\\x0eY\\xbcc\\x80\\x1d=\\nA\\xb7=\\\\\\x9a=\\xbb\\x83@\\x9a=\\xb5%\\x89\\xba\\xac\\xa2\\x9b\\xba9\\x81+\\xbd\\xeb\\xb9.=\\xab\\x19\\xe4<9\\x11\\x88\\xbc\\x1a\\xf0\\xe8\\xbc\\x16\\xe8\\x86<\\xe5\\xe9\\xd9\\xbc\\x94\\xcf\\x12\\xbd\\xb1\\x11\\x10\\xbbP\\x1c0=#\\xa5\\x01\\xbd\\xb9\\x11\\x92\\xbc\\x8b\\xfc\\x8c\\xbd\\xfa\\xd7\\xfc<\\xca\\x9b\\xa7<@b@=\\xda]!=\\x18 \\x84=\\x11\\x0b\\xa3\\xbc;7\\x1f\\xbc) \\xa0\\xbcx\\xe8\\xeb;\\xf8\\xe9\\xa7\\xbd?\\x1aM\\xbdb\\xd7\\t=\\xc3\\xca\\xa0\\xbb\\xc2\\x16\\x82\\xbc]\\xa7\\xbd<\\xf2G\\xa0\\xbb\\xce\\x19\\xc3<\\xa1M\\x9c<\\r\\xa8\\x8c\\xbc\\xcf1a\\xbb\\x80y\\x94\\xbc\\xa5\\xd3\\xc1;\\xc1\\xa3\\\\=/\\xa2\\xbd\\xb8\\xb9\\xc2~\\xbd\\x87l\\x18\\xbdNv\\x17=\\xa6R\\xbb<\\x1f\\xac\\x87\\xbc\\xf3\\xe5\\x8f=<\\xa1\\xce<Y\\xdfa\\xbd}j\\xdd\\xbc\\xf6\\x84\\x99\\xbc \\r~\\xbcZ\\xb0\\x0c=\\x04[!=$m\\x10\\xbd|\\xebQ=\\x0b#\\x8d<\\x8c1\\x03<\\x17\\xfa\\xf86\\x0b\\xa82<Ob\\x80=\\x84 x=\\xd7\\xc6\\xac=\\xad6V=\\x13\\xa4G=i\\xc4\\x95\\xbc\\xb8\\xa9%\\xbd\\xee\\x1d!\\xbd\\xe5g\\xe6<(\\x13\\x96\\xbc\\x13Q\\xdd\\xbc@\\x1c\\xaa<wlx\\xbd\\xeb\\x82\\x86=\\xab\\x15\\xd6<\\xbc\\xc8\\x0c\\xbc\\x80\\x8f\\xea<9ma\\xbd\\xf3\\t\\x83=\\xa6\\xbd\\xad;U,\\x0f\\xbd=\\xb8\\\\\\xbd\\xb6\\x17\\x93<v\\xe5\\xad\\xbc\\xb6\\x16\\xeb\\xbb^>\\x15\\xbd\\xcc{f<\\x1f.\\xee\\xbc\\xf9\\x1b\\xd9\\xbc\\x90V\\x10=\\x8b\\x85\\xd8<\\x19\\x828<\\xb1\\x11e\\xbc?\\x10\\x8a;\\xc0\\x9bi\\xbd\\xc1\\xac\\xb4\\xbbWc0\\xbc\\xbf\\xe2F=+\\x04\\xff\\xb9H(\\xf0\\xbcA\\\"\\x8a<+d\\x8d\\xbc\\xbbA~\\xbb$\\xf7F\\xbd2\\xb2\\x1c\\xbdm\\x1d\\x8d<:\\xca\\x9c\\xbd\\x8119\\xbc7e\\xa9<2\\x9e\\\"=\\x149\\x1a=\\x15X\\xf0\\xba\\xfaZ\\x08\\xbd\\xd1\\xcb\\x1d\\xbd\\xb7\\xb7P=\\\"\\xa0\\x90<\\xcf\\xc9\\xd4\\xbc\\\"\\x1di\\xbc\\xa0\\xe2\\x9c<i\\x99S<\\x17\\x93n\\xbd!\\x8d\\xc6\\xbc\\xba[x\\xbdA5\\xb5;\\xa3u\\x87<\\xe8\\xbf~\\xbcq\\x9dQ=W\\xf1\\xd3\\xbb\\x9d\\x19\\x07\\xbd\\xc2k;\\xbc\\x7f\\xde^\\xbd\\xeb\\x9d\\xae;\\x08\\xf4\\xe0;\\xb7\\xedM\\xbd\\xb7\\x1eq<\\xd43\\x0e=$\\x18\\xdf;\\xa0\\x05)\\xbc\\xe1\\xfc\\xc9\\xbc\\x0f\\rP\\xbd&\\x90y\\xbb3\\xc4Q\\xbd\\xab\\xfc\\xbc:v\\xe24\\xbd\\xff\\x90g\\xbd\\t\\xa6\\x95<\\xe9\\xda\\x08\\xbc\\x9d\\x82\\x08=:\\x1f\\x12=?\\xdfR=\\n]~;\\xa9\\xa4\\xdf<\\x16\\xfa\\x92=\\xa8-\\xd5\\xbcV\\x08z\\xbd\\x1d\\x8bC=~\\x97\\x8e<n\\x04\\xcd\\xbc\" \nHSET bikes:20003  model 'Chook air 5' brand 'Nord' price 815 type 'Kids Mountain Bikes' material 'alloy' weight 9.1 description 'The Chook Air 5  gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. The lower  top tube makes it easy to mount and dismount in any situation, giving your kids greater safety on the trails. The Chook Air 5 is the perfect intro to mountain biking.' description_embeddings \"%\\x05\\x14\\xbd+\\x9d\\xad<v\\xf5\\xe3\\xbcf\\x999\\xbc\\xd5\\xb1\\xa8<\\x85\\xe7\\x03=\\xac\\x07\\xb1<l\\xa2;\\xbd\\nG\\x94=H\\x18\\x8e9\\x9a\\xb7\\x00;tR\\x1b<}\\x15%=uD\\xf9\\xbc\\xa9k\\xaa<x@\\x86\\xbd\\xaa\\x98b=\\xf1\\x8a\\x97\\xbc\\xcfN\\x85\\xbd\\xf1\\xfb\\xdd\\xbd%\\xc3\\xca<\\xb8\\xe8\\xaa\\xbc6\\x01\\xba<\\x98r\\xd8\\xbb\\xf0i\\x84\\xbc\\x127&<\\xf8\\x08\\xbd\\xbc\\x92\\x10b\\xbcr]\\xa3\\xbc\\x80\\xc4\\xd9\\xbc\\xd5\\x8e\\x1e\\xbdf\\xbc\\xb5;}\\xd9\\xb1<\\xcev\\xb5\\xbbq^\\x8e=\\xfd\\x98\\x9e\\xbc\\xb1,/=\\xf8O \\xbd\\xa7Y\\x81\\xba\\xd8/v\\xbc\\x9f5\\x02=\\xee:\\xa5\\xbc\\xce\\x1a\\x00=\\xb1)\\x15<\\x89}\\x98\\xbcS\\xc5\\x98\\xbcvs\\x06=x\\x0e\\x02:\\xf82\\t\\xbd\\x03\\x1c\\x90<\\xa2\\x16\\xb2\\xbb\\xab\\xbc\\x9f:\\x1b\\xc0\\x1f=G\\xd2r<\\x14\\xb9\\x15;\\xa3\\xb7\\x83\\xbd]\\x95\\xad<X\\x1a\\xe4\\xbd\\xe3\\xbcM\\xbd\\x14.Y=\\xc0p\\x07\\xbc\\x80\\xde\\x8d=\\xa9)\\x90=\\x0fi\\xd6=\\xbc\\x1c\\x06=\\x928,<\\xd1Fe\\xbc\\x1c\\x16I\\xbdh\\xb8==>%\\x9e\\xbb\\x08\\xceY\\xbb\\x99Lg\\xbc\\xd8-0\\xbdq\\xb2\\xb4<\\x9a\\x8ak\\xbcG\\xab\\x9e\\xbc\\x98\\xd8\\x14=!\\x17W\\xbcJ+\\x15<\\xf6\\x87:<g\\x17i\\xbbx\\xfb\\x91\\xbc\\x7f\\xe2\\x86\\xbd\\xa7\\xf5{\\xbd\\xf8i\\x9b<\\xb7\\xc4,=\\xff\\xc4\\xf9\\xbb\\xa1\\xfc\\n<\\xe3 \\xb3<\\x12\\xa6t\\xbc\\xaa\\x96g<\\xef[\\xf0\\xbcN\\xcb\\x01\\xbd\\x9c\\xab\\x02=te\\xc8\\xbbE)\\xa5<\\xed\\x19W\\xbd\\xf0\\xc3+\\xbc\\x91\\xecp<>Yn\\xbcG\\xa0x=C\\x9fC\\xb9[\\xef0:\\xcf\\xb1\\xa7</\\x0c~\\xbc\\x85o\\xf8=\\x0eI\\xee<\\xff\\x8d\\x17\\xbd=\\x90\\x8e<\\x91\\x15\\xa0\\xbc\\x82t\\xc5<\\xb3#\\xc2=\\xb3SD=\\xc0v\\x05\\xbd\\xfeC\\x96\\xbdap\\x85<\\x12\\x8aC=\\x8d6\\x13\\xbdA\\x032=\\xd6G\\x92;\\xad<\\xeb<\\x91\\xe8\\xc1\\xb8\\xbbi\\x8c\\xbd\\xab\\xfd\\xbf\\xbcO(\\x96=w\\x0e\\xf8\\xbc\\x8d\\xdb\\xcf\\xb9\\xe0\\x1d\\xc4\\xb8I\\xf5\\xa4\\xbd\\xac\\x14\\xa1\\xbd\\x10\\xda\\n=1\\xe7\\x8f\\xbd_\\xb9\\x0b=\\x9f\\x7fa=\\xad\\x06\\xf6\\xbc\\xc0W\\x92=\\x81b\\x90\\xbd\\xb5=\\xf4\\xbcm\\xb5W\\xbc\\xfag3\\xbd\\xc4LV;X\\x80f\\xbd\\xba<\\x8c\\xbc\\x16\\xe4-=>n\\xbc\\xbc\\xa7{\\x8b\\xbc.\\x98\\xca:\\xf4i\\xc8<\\xec\\x00~<\\xf0\\x8f7;\\xa4\\xa4\\x9e\\xbc\\x99f\\x8d=\\xfbP\\xe3\\xbc\\xba\\xd2\\xd5<\\x96\\t\\xdf\\xbcH\\x08e;\\xf7\\xfd\\xc5<O\\x9ah\\xbc)\\xba}<\\xfe\\x95\\xac\\xbb\\xa4M\\xa7=\\xeb\\xe8\\x8f\\xbd\\xe3x\\xe0\\xbc\\xe0a1\\xbd\\xc8r\\\\\\xbc\\xdd\\xed\\t=?\\xe0\\xfb\\xbc?c\\xa3<\\xb8\\x13\\x85\\xbc\\xdb\\x8a\\x18\\xbc\\xbe^\\xb2\\xbc\\xe62\\x17<\\x1a\\n\\xd3<\\x03rc\\xbd\\xa7 N\\xbd!b\\x08\\xbc\\xc9\\xa4\\x81\\xbd6\\xad\\xa9=\\xc0&\\x10\\xbc\\x11<\\x94<(;\\xad\\xbc\\x98\\xf3H<F\\x7f\\x9f\\xbc{\\x1a\\x85\\xbc\\xa2\\x971=4@\\xd4\\xbd\\xca\\xff,=Z\\xd2(=\\xe1\\xa2\\x8f\\xbcv\\x86/<\\x91\\xf5J\\xbd@\\x1d3=l\\x0b|\\xbc\\xc7\\xd6\\x80\\xbcW%\\xff\\xbc\\xeb\\n\\x9b<\\x9f\\x98\\xd4<:\\xebI\\xbc\\x1cC,\\xbc\\x0b\\xcb\\x1a=,/\\x8a\\xbcM\\x9a\\xda\\xbcX\\xf3\\xa1\\xbd(\\x9d\\xe7;7\\xf1;\\xbd\\xa7\\xd7&=\\xcb(M=<\\x1e\\xb9;m\\x0e{=\\xfd\\xa6\\xcc<\\xda\\xa4-<\\x8c\\xed-<u@\\x06\\xbc\\x90W\\xf1\\xbc\\x14\\xf8\\xd5\\xbc\\xee\\xc8D=\\x91\\xe0\\xc4\\xba\\xf0\\x8b\\xae;n\\x1e\\xf0\\xbbc\\xfc\\xba\\xbcb\\x9d\\x14\\xbc\\x0ffg<\\xbe\\x97\\\"\\xbd\\xc9\\x00\\xc5<,\\xf9\\xad\\xbd\\\"\\xf0_=\\xb0\\xf77<\\x17\\xbb\\xdb\\xbc\\n\\x95}<t\\xd2@\\xbc#\\xae\\x8c<\\x90_\\x84<\\xc1\\x92Q=\\x8f\\xa5p<.\\xd69\\xbd)\\x0b\\x14=u\\xdc\\x00\\xbd\\xb6\\xf9#=EF+=\\xa9<_\\xbb\\xabv\\xec\\xbc\\x89\\x98,\\xbc\\xd8dW\\xbc\\xf0\\x86\\xe6\\xbb\\xe1\\x84\\x11=\\xedr\\x19=\\xb5\\x9e\\xa2<\\xd4\\x90\\x11<\\t\\x0e_\\xbd\\x85L\\xfb<\\x1b\\xb3\\xdb\\xbb\\x92\\xd2\\x10\\xbdW\\xe0\\x14\\xbd\\x7f\\x87A<<m\\xfc\\xbc\\x12\\x9b\\xac\\xbcu\\x0e\\xdb\\xbc:\\x91$\\xbc@\\xa3}\\xbc\\xa6\\xad6\\xbd\\xebcr\\xbd`\\xcaI\\xbdy\\xa2r\\xbds\\x93k\\xbc\\xb9=\\x7f\\xbd\\xae.\\x07=LA}\\xbc\\x10\\xed\\xb1\\xbb\\xf2\\x7f^=\\xb3G\\xe3<\\x07l\\x97:\\xfa\\x19\\x05:\\xe7X^\\xbc\\x15\\x8b\\x8a=\\x06VS\\xbc\\xcc\\xcfx\\xbc\\x07q\\x1d\\xbc\\x82\\xf4\\xce;\\xa5\\xee\\xa5\\xbc\\x80\\x86R\\xbd \\x93\\xb9\\xb9B\\xb3\\xa7<N6Y=\\x91%\\xb5\\xbc\\xea{a\\xbc\\x1cYS<\\r\\xba4=\\xf6\\x98`<\\xba\\xe3\\xbb\\xbd\\x96\\xa6\\xe7<\\xa6\\x08N\\xbc4$N\\xbbS\\x16\\x1d=r\\x19\\x10\\xbd\\xc0\\xda\\xb4\\xbcN\\xb4\\x1c\\xbd+\\x7fC\\xbd+\\x87\\xbb<\\xd6s\\xcf\\xbc\\xc4|\\xad\\xbc:\\xff \\xbbc\\xd6\\x0f=\\x86\\xe5\\xe1<e[.\\xbb\\x9f\\xfd\\x99<\\x165\\x9d<QB\\xe3\\xbcn\\xe9\\x80=<\\xa0\\xd3\\xbbw*\\xd8<\\x0e2\\x1a\\xbd\\x99\\x96\\x80<\\xcb\\xbb7\\xbcw\\xbf\\x1f;2LP\\xbcht\\xfa:\\xd0\\x8d\\xa3\\xbc\\x8cU\\xad=\\x82\\x88\\x12=IC\\xd3<\\xc2\\x0f\\xec\\xbb\\x148\\x10\\xbd\\xbe\\x1f$<\\x80\\x813;\\x02L=\\xbd\\xfcO\\x98\\xbcQ\\xb4N\\xbc\\xb5\\xa5\\xf5\\xbd3\\xfc\\x97:G\\xfb\\x8c:\\xe5\\xad\\xbb\\xbdF\\x18E;3\\xf5\\xa6\\xbd\\xb9\\xdc\\x16=\\xbd\\xa0\\xca<7S\\n=$\\x19\\xbe\\xbc\\xd9\\xc3\\xb8;\\x17\\xd0e<s-\\\\\\xbcx\\xab\\xa9\\xbc\\xed\\xd7\\xcc;m\\xb0\\x80=\\x83\\xe6\\x98\\xbcm\\x18\\x07\\xbb5,P=\\x01.\\xbd<w]\\x01;\\xee\\xcc\\xe9<\\xb4Ht\\xb5\\xeal\\x9b<\\xc7\\n\\x05\\xbd\\x94f\\x85=r\\xe7\\x86\\xbd\\x11\\x16\\xb8\\xbc\\x8f\\xd0\\xed<\\x82\\xe3@<\\x18\\x03t\\xbc\\xb0\\x03\\x7f;\\xe8\\xb3\\x03\\xbeo\\x80[\\xba\\x9aA\\x98<1\\xae\\x15\\xbb\\xf14\\xa3<\\xbe\\\"\\x01<\\xa5@\\x16<\\xf5\\xeeO=\\xbb\\x9de=j\\xf4\\x97<\\xed)p;\\\"\\x08\\xa0<\\xfa\\xc7\\r=\\x98\\xcb\\\\\\xbc_\\xb4A=\\xd7\\xcdV\\xbd\\xcd\\xc5\\xb6:a\\n9\\xbcU\\xda\\xa2\\xbd\\x13\\xffy\\xbd3{\\x14\\xbb\\x15\\xa5\\x11;\\xeb\\xacm\\xbd\\xe3\\xb88<q\\xbd#\\xbc\\xf3\\xddE\\xbd\\x84/}\\xbc>\\xf2\\x11\\xbdu5\\xac\\xbc#\\x03\\x8a=\\xd9M\\xf19`\\xb2\\x03\\xbd{\\xc9\\xbf<\\xbc\\xffy<\\xe5\\xdb\\x97\\xbb\\x14\\xeb\\xb1\\xbc`vZ<Irw\\xbdi<#=\\x8f\\x1a6=20=\\xbd\\xbe\\xf7;<\\xfc\\xdd&\\xbc\\x81\\xc5H\\xbc\\x93\\xe4\\xaf=,6\\xfd\\xbbV\\x8e\\\"\\xbbT{\\xb5<QPO=\\xcfh\\xd3\\xba\\xd3\\x89H=\\xdf!\\xa0<P\\x99\\xb9\\xbd\\xad\\xb6(=F\\xc6x<N8-=$\\xd2(\\xba\\x9f\\x85j\\xbd\\x01\\xe6\\x80<\\x99\\xb8\\xcc<>\\x8a\\xf2\\xbc\\xc1\\x11\\xa9\\xbd\\\\zr\\xbd\\x06J\\x19<l};=\\xae\\xff:\\xbc\\x143\\xa2<\\xf2\\xb4\\x96\\xbc\\x0f\\x8a\\x9a=9B/<B\\xc5\\xb4<\\xfcG\\x8f;y\\x08E\\xbdA\\xeb\\xa4\\xbd\\xdbF\\xc3\\xbc\\x9aQ\\xe4:\\x9ch\\x9e=q\\x05R=8\\xbb\\x01=\\x1d\\xc1\\xa7\\xbcqQR=\\xbf{\\x96\\xba6\\x1d\\xe6\\xbc\\r\\x17\\xa5<X\\xe9\\x0f\\xbd\\xe5\\x04\\xb2\\xbc\\xd9\\x16j=\\x98\\x9b\\xe1\\xbc\\xef\\xe8\\xe6\\xbb\\xd1\\x8do\\xbdj\\xdc\\x9d=Qcs<\\x8e\\xf7\\xa5\\xbdP\\x87\\xa6;\\xa8\\xd8\\x0b;\\xc9sa\\xbc\\xd5\\x81\\x93\\xbc\\x05N\\xab\\xbb\\t\\xb8q\\xbcW\\xe5\\x8e<\\x11$9={\\xce\\x92=NF9<\\t?\\xc6<\\xc4C\\x0b\\xbc\\xf8&\\x94\\xbc)\\xd8\\xc5<\\xa5\\xb1\\xe0\\xbb>\\xfa~;\\xdak\\xfb\\xbb\\x9f\\xc0==\\xb4H\\xc5;U\\xc61=BaG\\xbc\\xbb\\x1c\\x00;\\x8c[\\xa7\\xbc\\x96\\t\\x8b:\\xc4\\xde\\xfc;<Y\\x06\\xbd\\xf0\\x94\\xea<\\xc2hz<\\xdb\\xc7\\xe6<\\xa9\\xbe^\\xbb\\xf3b\\x06=\\xe08\\xee\\xbb\\xb7\\xec\\xe8\\xba\\xafj\\x8f\\xbdR\\\"\\xa7\\xbc\\x92\\x1a\\xe1\\xba\\xe4\\x0e\\xf2:a\\xdc\\x98\\xbc\\xe6\\x97}\\xbdVh\\xdc<\\x1a\\\"f\\xbc9c_\\xbca\\x8f\\\\\\t\\xb6OJ\\xbb\\x99\\x07\\xb2;\\x9e\\xb0\\xa1=\\x80m\\xf9<\\xfe\\x1a\\xff\\xbc\\xfa\\xb5-<$\\xc5A\\xbc\\xb1@\\x9a<\\xe7\\xcc\\x83<\\x98.\\x14=.\\xc0\\x17=\\xf6<+=\\xc3\\x9a\\x99\\xbb`X\\\"=`;\\xe2\\xbc}\\xa3\\xea<\\x85\\xa5\\x93=@\\xa1\\x8c\\xbc\\xd5^\\x8b:6\\xdf\\xe6<\\xa4i#=\\xd5\\x8f\\x98<\\x01\\xd6\\x7f\\xbc\\xeaO\\xc1=\\x16\\xfb\\x96<X\\xf5\\xc0<B\\xeb\\x98:\\x9a\\xc5\\xda\\xbc\\x13\\xaa\\xc2\\xbabU\\x1c\\xbc\\x0c\\xb2\\xe9\\xbb\\x17S3=e\\x12\\xd0\\xbc\\t\\x9d\\x14\\xbc\\xd3\\x81\\x80\\xbc\\xdaF\\x90\\xbc\\x96w/\\xbd8\\xc7{\\xbb\\xfe\\xc4\\xdc<\\x1er\\xf8\\xbcj\\x0c\\xb3\\xbb\\x85\\\\%<l\\xf0$<\\xe1uI\\xba\\xc7\\x0e\\xc3<\\xcf[\\\\\\xbdjM\\xcd=\\x8a7\\x89\\xbc\\xe9H:<\\x13\\xcf;\\xbd\\x83\\xaa\\xa6\\xbc\\x87\\x9e\\x07\\xbe\\xf0v\\\"=\\xe2\\xa4\\x0b\\xbd\\xc7\\xbc\\x95\\xbd\\xe5P\\x9e=\\xafs\\xf0\\xbc\\x1f\\xd6\\xbc\\xbc\\xa7\\xe6\\x8c\\xbc\\xd20 =\\xc4G\\x1f<\\xa7\\xe7\\x0c=\\x16(\\xed<1\\x9a\\xea<\\xa9\\xb3\\xf3<@r5\\xbd\\xd8@\\x19=\\xb5\\xfa.\\xbdH\\xcbH=\\xf3}\\x89;\\xe9a\\x17\\xbb\\x03x9\\xbbG\\x81\\xb9\\xbc_\\x90\\x10=\\xb1>\\xd0\\xbb\\xa0\\xe5e\\xbd@\\x9e#\\xbd\\xb2|T=\\x03H2\\xbcs\\xd0\\xbd;#h\\x01\\xbd+\\xb4/=\\x00\\xd2\\x92\\xbb\\xdb\\x9f\\xbf\\xb9s\\xeb\\xf9;\\xe5\\xb7\\x82\\xbc\\xeb\\xc8;<CV\\x8f\\xbb\\xd3\\x84\\xff\\xbcF>r<\\xb3\\x03\\\\=\\x80\\xe4\\x00\\xbdh7!=x9\\xbd:^\\xaf%=R;\\xb3<[I^\\xbdiV\\\"\\xbd4\\x93^\\xbbJ\\xba\\x87\\xbb\\x00\\xe3\\x04\\xbd\\xe2@\\xbf<cZv<\\xf2<\\xb1:\\xa2\\x96$=|&5\\xbc\\xfe\\x18{\\xbdLb\\x8e\\xbc\\xc4\\x9d\\xd8;\\xb4\\x05\\x8f\\xbc\\xcd\\x99F=\\xc52\\x18<\\xcb\\\"\\x81\\xbd\\xacK,=,\\xac\\x96=\\x9b#)\\xbc\\xf7\\xae\\x1d=?\\xd8\\x01\\xbd\\xee\\x1f\\x07<\\x1e\\x1d\\xf8\\xbb\\x8eD\\xb4;\\x8f\\xdez\\xbb\\xbdMZ<\\xdc\\xa0d\\xbcg\\xf8\\xb6;\\x18=\\x81\\xbb\\x81\\x91\\xe6\\xbc\\xd9\\x81\\xb2\\xbb\\x18 \\\\=\\xdb\\x88R\\xbd\\xdcD\\\"\\xbd\\xef;H\\xbd\\xa5\\xda\\x8f=\\x80\\xc38=;Of\\xba\\x83 I;\\xff$/=\\xa5:\\xc3\\xbcCAL<Q\\xfd\\x9d\\xbc\\xa9\\xbfi<4\\xc2\\x86\\xbc\\xc1\\x04\\xa7\\xbd\\xc1f%<\\x96!\\xef\\xb8O\\xcbH\\xbc\\xb9\\xaa\\xad:gU\\x06=\\x11\\xc9c=\\xb9\\x1d\\xae<\\xf2\\xf7\\xbf<4\\x85w<=g\\xca\\xbc\\xff\\xa5\\x03<\\x94w\\x11= 9\\x9e\\xbb\\xa47\\x90\\xbbI\\x1c\\x1e\\xbc\\x81.\\x8c=\\x93\\x0bC=\\\"L\\xdf:\\xa5%\\x85\\xbc\\xb2\\x15\\xc7<\\xea\\xb7\\x8d\\xbc\\xc28Q<;\\xcf8\\xbc\\n\\xba\\xfe;kJ\\x8b\\xbc\\xfa\\xee\\xb9\\xbc\\xb2\\x98h\\xbd$(/;fT\\x93<\\xa5\\x92\\x10<9\\xd3\\x05:k\\x8e+=\\x10=\\x04=\\xd2\\xba\\xff\\xbb\\xd0\\xbc:=\\x91\\x02z=\\xcf\\xf8K<\\x90\\x18\\xcc\\xbc)\\xf6\\x9b\\xbc\\x06\\xf0b;\\x82\\x92\\xab<\\x15\\xec\\xd0\\xbc\\x850\\xe2\\xbc\\x80\\xa0\\xa5\\xbc\\x92n\\x91\\xbc\\xe0W\\xb7=<\\xa5:=\\x048\\xc0<\\xecJ4<\\xce\\x1bT<\\x16R\\x12<\\x88\\xbb\\xa8<\\xd7\\x9a(\\xbd1\\xb06\\xbd0\\x1e\\x14=\\x0fG4=KF\\x1a\\xbd+\\xb0!\\xbd\\xe1\\t\\x1c=\\xe0B\\x19\\xb9\\x9b\\xb3\\\"\\xbd\\x0bmL<\\x8b\\xd0%\\xbc\\x1e\\xf8\\xf1\\xbb\\xdeMn\\xbd\\x84\\xad\\xa5\\xba>b*\\xbc\\xf2g\\xd5;4\\x10\\xbc\\xb9\\xb5\\xf84\\xbc\\x14{\\x84\\xbb\\x11y\\xb4<\\xe3\\xcc\\x97\\xbc;\\xbe\\n\\xbc9\\x10+<\\xdc2\\\"\\xbcl\\x13\\xe6\\xbc\\xfaw9;\\x93\\xd9\\xcb\\xbc\\xcfS\\xaa<\\x1e!\\xc0=Y\\x8d\\xa5=\\xa0\\x9e\\x8a<\\xdc`\\xc4;&s6\\xbc\\xc1>\\x16\\xbd\\xb5\\xbe\\xe8<\\xa8\\x8a\\r\\xbd\\xa71\\x01=O\\x1d\\xb8\\xbb\\xc1\\xa9W<(\\xec;\\xbd&r\\xaa\\xbc\\x83[\\x04\\xbb{-{\\xbc+\\xd8\\x89=[.\\xe5<\\x8c\\xa8\\xbe\\xbc-j\\xdd;5\\xc5v<\\x19-\\x91<\\x0b\\x93.\\xbc\\x07\\x9e\\xca<\\xbd\\\"\\xcf\\xba\\x91\\xe3\\x13\\xbc\\xe9kS\\xbd\\xdcf\\xb2\\xbbGu\\xfc\\xbb\\x868G<M\\xa77;@\\xbc\\x95;hb\\x96\\xbc\\\\P.;B@\\x95\\xbd1\\xb5\\xa8\\xbdr\\xed\\x96\\xbc\\xa9,\\xee\\xbc\\xbc:M\\xbb7\\x1dS<\\xd1\\xc9^=D\\x7f\\x15\\xbd\\xd0\\xf9\\x97=PF\\xda\\xbc\\x98\\xe1j\\xbc\\xa5\\xbf\\xce<Al\\xef\\xbc>#I\\xbd\\xdf\\xd2\\x81\\xbc\\xb6\\x91\\xc0\\xba\\xa7\\xf9f\\xbd\" \nHSET bikes:20004  model 'Eva 291' brand 'Eva' price 3400 type 'Mountain Bikes' material 'carbon' weight 9.1 description 'The sister company to Nord, Eva launched in 2005 as the first and only women-dedicated bicycle brand. Designed by women for women, allEva bikes are optimized for the feminine physique using analytics from a body metrics database. If you like 29ers, try the Eva 291. It’s a brand new bike for 2022.. This full-suspension, cross-country ride has been designed for velocity. The 291 has 100mm of front and rear travel, a superlight aluminum frame and fast-rolling 29-inch wheels. Yippee!' description_embeddings \"\\x1f\\xbb\\xe9<\\xd3/\\xcc<\\xda\\xb8\\x15\\xbd\\xc6V\\x98<\\x0bhq\\xbd\\xc8G\\xa1<\\xe1V\\x1f=jIP\\xbd\\xa5\\xc3\\x1f=\\x89\\x877=\\xd6\\x9b\\xaf\\xbbqJ$\\xbd0\\x9c^=\\xfbK\\x0c\\xbd\\xe9_\\x86=)\\x83V\\xbc4\\xc8\\xbb<\\xe3ly=\\\\O\\x9f<\\xee\\x88}\\xbd6\\x18%<~\\x03\\x12:\\xf0\\xe5@\\xbc0\\xbb\\x11;\\xe3=\\xb0<&-\\xa7<\\xbddH=\\xd8\\xd4\\xc1\\xbb7\\xe2\\x1c\\xbb=\\x0e\\x94=\\x85\\xb0\\x1b<\\xd8N\\x1c\\xbd\\x94-\\xfb:a\\xe4\\x8f\\xbbo\\x00\\xd7<\\x06\\xe0s\\xbc5\\xbe\\xee<\\xfb\\\\\\x1b\\xbd$\\xb1-\\xbbVK_<p4\\xd1=\\x87\\xb5L<\\xf9\\x99Z\\xbcm\\x13\\xd5<Ux\\x87\\xbc6[\\x1a\\xbc\\xb2p\\xd3<3\\xaa\\x02<\\xca\\xff\\x0f\\xbd\\t~\\x8e<(T\\x12\\xbc\\x0f:g<dI*\\xbd\\xe9>t\\xbd\\x1b\\xb5\\xfd\\xbbF<\\x89\\xb8\\xbfH;\\xbd\\r|\\xa6\\xbd\\xe7Y\\xfd:\\x93W\\xab=\\xd4{4<\\x84\\x19\\x12=\\x14\\x7f-=\\xc6]}=\\x8c\\x17\\xd1=4\\xf4\\x8f\\xbc\\xdf\\x94\\x9f=DtP<\\x14\\x926\\xbc\\xbc\\x02\\xf6\\xbbz\\xde\\x03<\\xf1\\x11\\xa3\\xba\\xb3\\xe6\\xc7\\xbc]\\xc3\\x11\\xbd\\x18\\xbeg:\\x8c\\xdaN\\xbd\\xb4\\xca\\x91<\\xe8\\xe7/\\xbc)\\x95\\xe6\\xbb/\\xa1\\xb7\\xbc\\xe6{\\xd5<\\xa6\\xf7\\x9c\\xbc?oQ\\xba\\xdf\\xc5\\x1e<\\x99\\x9e\\xbf\\xbdlD\\xd8<\\xb5P\\x91<\\x00\\xc4\\xc1\\xbaA\\xb3\\xb6<\\x03\\xa5\\x07:\\xfc\\xe1\\xce\\xbc=;4\\xbd\\xd8i\\x06\\xbd\\xba\\x13\\xa4<\\xf6\\xc7\\x81<\\xb9\\x88\\\\\\xbc\\x90\\xe1\\x17\\xbd\\xcf\\xb7\\xf5\\xbc`\\x9f\\x90\\xbc\\xdd$\\x98\\xbdn^\\x14=8Zj<\\xfe\\x10\\\"=\\xfev\\x0f<\\xa8\\xcd!<r\\x89\\x99<\\x17Kq\\xb9L\\xce\\x80<y\\x9cj\\xbd-\\xa6t\\xb9\\xf7\\xba0\\xbc\\xf9\\\"t=\\ru\\xdb<^\\xa3\\xcc;8_\\x93\\xbd\\xd1\\xd6\\x8d<\\x8c\\xa2@\\xbbK\\x92\\xea<\\xaf\\x00\\x8d\\xba\\x85Z\\xa0<\\x9f@z<\\xb07W=\\xf73\\xc1\\xbd\\xe6\\x85\\x8f<\\x08iq=>\\xc4>\\xbd[N\\\"\\xbd\\xb2c\\x80\\xbd9\\xca%\\xbdzc\\x97\\xbd\\x16/\\xf2\\xbc\\xb7\\x1d\\x15=\\x08\\x9f\\x10\\xbd\\xa6{\\xbd\\xbc\\xdd\\x90\\xa8\\xbc\\xba\\xaf\\xdf<\\\\\\x1c\\xb0<>\\xa4(\\xbds\\x94\\x88\\xbc\\xb6\\x9e\\xde\\xbc\\x94\\x1a\\x90\\xbd\\x0f\\x08\\xf6\\xbc\\xa8\\x9e\\x9c\\xbd\\xa37#=\\xb6\\xc7\\xed\\xbbc\\x18\\xbc\\xbc\\x01\\xbeH\\xbc\\x05l\\xd3\\xbb$\\x89\\xe9\\xba$\\xb9q\\xbd\\xc8\\xda\\xb8:\\xe3\\xe5\\xaf=\\xd0G\\x1b\\xbd\\xda\\xd9R=7\\xa9\\x1a\\xbd:A\\x91;\\xf2u\\x10\\xbdL?\\x18=]\\xd5\\x8f=\\xae\\xb0\\x96\\xbd@H_=\\xe3\\xe5\\xc3\\xbd\\xce\\xe6\\xc8\\xbcG\\xbd\\xa0\\xbc\\xc7\\xb0\\xc9\\xbc:\\x86}=\\x11D\\\"\\xbd\\xdb\\xa4\\x10=\\x19\\xcc\\xc7\\xba\\xd7\\xf2\\xf0\\xbc<\\xd78\\xba\\x1a\\xccd=y\\x0e\\xdc\\xbcC\\xe9T\\xbc\\x88,\\x90<j\\xed\\xec<\\xc7\\xcf^\\xbd\\xd4h\\x99<A\\x15\\xf7\\xbcL<\\xb6\\xbc\\xf4\\xc2\\t\\xbc\\x94\\xee\\x9e\\xbc:Y\\x07\\xbc:\\xd4\\xfa\\xbc\\xa9\\n\\x05=,\\xf9\\x0f=\\xf9\\x94g=\\x96\\x9dn<\\x8a\\x19P=>\\x9e\\xf2\\xbb\\xa2\\x0eu;\\x95\\xa22<\\xb5\\x8f\\xeb\\xbd\\x02<\\xe5<L\\xe4\\x8c\\xbb^\\xd2\\x16\\xbb\\x18]\\x15\\xbcu\\xe4W\\xbd\\x99\\x89\\x01\\xbd7\\n\\x16=f\\x0bT\\xbc\\xb1*1\\xbd;A\\x0b\\xbd\\x9a\\x92*\\xbd\\xf3\\xa9L<\\xdf\\x97\\xc2\\xbc\\xf0\\xbcj=[u\\x0f\\xbdf\\xbd\\xf2<\\xe1#Z<\\x89\\xb9\\x10\\xbd\\x17\\xfeb=\\xe2K\\xf0<\\xb1N\\x1f\\xbd\\x88t\\x9d\\xbd\\xda\\x14\\x05=4\\xab\\xbb<\\xa6\\\\\\x12<?\\xcf`=\\xd1`\\x04\\xbd\\xad(Q\\xbaz\\xaaL:\\xec\\x16\\xeb<\\xcf\\xcdk\\xbd\\xb6\\\"\\x0f\\xbd\\xa4\\x0b\\xe5\\xbc\\xa5IV<C\\xa6\\xbb<\\xffZ\\xb6<\\xb5\\x99\\xf6\\xbc\\xc3\\xae\\x7f<\\xef\\xb3\\xed<\\x86\\xc5-\\xbd~\\xf7W=@\\x92\\xb5;\\xa7\\\\&\\xbdl\\x00\\xa8\\xbc\\xc0\\xd1\\x0c\\xbc\\x10w\\x06<\\xd8\\xcaa\\xbc}P\\xe1<\\xdaW\\x05\\xbd\\xff\\x19\\x8f<e\\xff\\x84;\\xe7\\x88\\xf8\\xbbMIB\\xbd]\\xb2\\xc5<\\xddu\\xa7=\\x07\\x10%\\xbd\\x063\\x1c=\\x91K\\\"=\\xdd\\xb35\\xbd\\x02l\\xbd\\xbc\\x1b\\x0c\\x8f<\\x8a\\xfb\\xcb\\xbc\\x1bL|=\\x92\\xa8\\xc4<\\xd9b\\x92<\\xac]\\\\\\xbc\\x8f\\xe2A=\\xce\\x91\\xfb;\\xa8(\\x05<T\\xc30\\xbd\\x84\\xd3(=\\xa6\\xc2\\x88\\xbd\\x0fR\\xca=&\\xb11=\\xa3j{:+\\x94\\x8e\\xbck[\\x15=a\\xaf\\xe8\\xbc\\x84\\xf0\\x18\\xbb\\t:3\\xbd\\n\\xaa\\xa6\\xbc\\x96C\\x0e<y\\x11/\\xbd\\nJ~\\xbc!\\x8e\\xdf\\xbc\\xb3\\x10\\x8c\\xba\\x9el\\\"\\xbd\\x08\\xf4\\xbc\\xbb\\r\\x8e\\xf2<\\xe7\\xe3\\x93=\\x1c\\x0c/\\xbc\\x08\\xa7\\x10=\\x18\\\"\\xbe<\\xa4\\xf2\\xa3\\xbcx\\xcc\\xf5;\\xaa\\xe5\\x92\\xbd,Y\\x80<K\\xbe\\xda<\\xae+\\xed\\xbd\\xaf\\x06i\\xbc\\xcd\\x02\\x19\\xbd\\xea\\x17\\x8a\\xbc\\x18\\x13\\xbe;\\xf6\\xaf\\x89;X\\xc1\\x9c<\\x98CN\\xbc\\x11\\x99\\xc3\\xbb\\x00\\x0f\\t\\xbd\\xecz\\x06=\\xd2/\\x19\\xbdcB\\x02\\xbd1)\\x17=B\\x7f\\xc0\\xbc`\\xc0\\x0b\\xbd\\x86n\\xd9;m\\xe0A=\\x99J(;\\xf9\\x17\\x0b\\xbd>\\xf6\\x02\\xbd\\x9f\\xcb\\x04\\xbc\\xf0\\xbaS\\xbb>\\xc1\\xc2<&\\xa3\\xbc<\\xf3y\\xa1<\\xcdRh=Dq\\x00\\xbc0\\xf5\\x84;U~L\\xbb\\xf2\\xfe\\xa9<\\xb1\\x1e\\x15\\xbc\\xc5\\x12\\x06\\xbdg\\xdf\\x80;ndT\\xbd\\\\}9=\\xde\\xc7\\x8e\\xbc\\xdfI\\x00<\\xb9\\xb1\\xf7;D\\xc0\\x07<\\xd7\\xb4(\\xbch\\xbdL\\xba\\xb3\\x1d\\xa0=pU@\\xbcV\\xbdr<]\\xb5\\x94\\xbc\\x9e9\\x9e<^X\\xfa<\\x99\\x0fU\\xbb\\xcb\\xa3A\\xbdWN\\x9c<\\x1aX\\xe7<\\x80hN<\\xe8*k\\xbc.\\x1d%<Y5\\xa5=qy\\x1e\\xbc\\xa5d7\\xbc]\\xea\\xe2::\\xd3h\\xbc\\xdeM\\x02\\xbd\\xaa\\xbd\\xb6;\\xf4\\xfc\\x1d\\xbc\\xdb\\xa2\\xe0\\xbc\\xfb\\x90:<\\xd9\\xa6\\x87;\\xd4\\xd1\\x9a;\\x80n\\xa1<\\x13q\\x97\\xbc\\xa6b\\xe4\\xbbg\\x81?\\xbc\\x9aW\\x7f<\\xc0\\xbd!\\xbcj@\\r=\\xe3\\xde\\xb8\\xbb\\xc3\\xe6\\x15\\xbd\\xb3\\x00\\xa6;\\xc7\\xf2\\x0f\\xbd\\xc5\\x1c\\xed\\xbcZ\\xf24=P\\xb2\\xb9\\xbb\\xa1\\xef\\xb0<|\\xc3\\x9d\\xbc\\xbb6N<\\xea\\xd9/<\\xb8g\\n\\xbd2|\\xcd\\xbc \\xb8\\x07=\\xa8\\x073<\\xfb\\xfa\\xf9\\xbb\\xd8\\xde\\x12\\xbb\\xafY\\t<\\x8e\\x7f\\x93\\xbb\\xa0[><\\xd6S\\x92<\\x00=\\xa0;D\\x16\\xbd<\\x06\\r\\xf5<{\\nm\\xbdS\\x9d\\xce\\xbc\\xfd\\xfcz\\xbc\\xfc\\xeag=&.\\xf2\\xbc]\\x1a\\x03\\xbd{\\x1e\\x81\\xbbY^\\x02\\xb9\\xd4\\xf5.\\xbd\\x01\\xb4\\xb0<\\xd3\\x8e\\xb1\\xbd\\xfc\\xe2\\xbe;D\\xf1\\xab\\xbc\\xba%\\x17\\xbcz2\\xcc=\\x92\\xc1\\x04<\\x80\\xe2\\x82\\xbd\\xff\\xceF=\\xf5\\x01&\\xbbq\\x1f\\x82<D\\x8d\\x1f>\\x0e\\xd7x\\xbc\\xde\\xc9i<\\xd0\\x8b\\x00<\\x11\\x13\\x81\\xbc{~-\\xbb\\x8b\\xe7B\\xbb\\x07\\x1c&\\xbc\\x8c\\x88\\x9f\\xbd\\x88\\xba\\x0e\\xbd\\xae\\x9fT\\xbd\\xe1t\\xd9;\\xbd_\\xec\\xbb\\x1b\\x1f\\xc8;v&V=Lt0=U\\xbc\\x80\\xbd\\x1e\\x9c$\\xbd\\x13\\x81)= ;\\xa4=\\xacV\\x0e\\xbd)#\\x1b=\\x87\\x86\\xd9\\xbc\\xc6X\\xdd\\xbb(w\\x85\\xbcV|\\x95;@\\x9c\\x10=\\xb5\\xd6#\\xbbk\\x10/;Q\\xff\\x03\\xbc\\xbe\\x8d9= l\\x0b=\\xf0~$<V\\xa6\\xaa\\xbc\\x91V?\\xbc\\x0e\\x9e\\xfa\\xbc\\xc6\\xe9w<\\x17Z2=\\xfc\\x07\\x1f\\xbd&r\\xb5\\xbc\\x7f\\x00\\x83\\xbb]~\\xde\\xbb`\\x15W\\xbd\\x1a\\x1b\\x82\\xbb\\r\\x95\\xee\\xbc>\\xb5\\x91<S_\\\"\\xbd\\xda\\rd\\xbc\\x84|\\xe8<L\\x88\\xa3\\xbc\\x82%S=\\xd9\\xedn=\\xa8\\x1aS<\\xbd&g<\\xf0Pw<\\x11\\x11X\\xbb!\\x87\\xb7<b\\xce\\x0b\\xbd^\\xa0\\xda\\xba\\x87\\xf1\\xb3<cL[=\\x00\\xf8\\xe1:i\\x0cr\\xbc\\xd3b\\xb3<\\x99\\x0e\\xda\\xbc\\x93\\xcc\\x08<\\xb2\\xb7\\x8d<b%F\\xbb|W\\xe2\\xbb|x\\x86=\\xf4\\xebD\\xbdD\\xdd\\x14=\\xf3\\x10\\xe2:\\x8f\\x1c\\xbe<\\xb3T\\xfe<\\xa1\\xf2\\xce<\\x0f\\x1f\\x9a\\xbc\\x99\\xa9\\xda<\\xd1{\\x1e\\xbcv\\xc0\\xfb\\xbcgf\\xe2<\\xaa\\x10M\\xbd\\xb3\\xaa~\\xbd\\x05\\x17\\x9d<\\xa2\\x8b\\x04=\\x1fvt\\t&#\\x13\\xbd\\xfb\\nd\\xbc\\x83d\\x8d\\xbbL\\xff\\x1d=\\x84r\\r=\\xcb\\xf4\\xa3<\\x7f\\xc1\\xf2;y1\\x02\\xbdb\\xe4\\xbd;V\\xe2k\\xbd6\\x05\\xd9\\xbb&\\x90\\xb0<F\\x1b9<\\xe9]n<\\x9fu\\x91<\\xc82\\xdc<\\xf6\\xdc\\x84\\xbd^f\\x01<K\\x07\\x8d;V\\x7f\\xab<\\xf9Z\\x12=_d\\xb4<\\xe8\\t\\xfd\\xbc\\xda\\xff\\xad\\xbc\\xba\\xee\\x15\\xbbm\\xbf-;\\x070N;\\xb7[\\t\\xbd\\xca\\xc7\\xa8;\\xd0\\x81\\x83\\xbc\\xb6M\\xfc\\xbcv\\xf2z=\\xa8Lc7\\xdb\\x1ar\\xbdw\\xba\\x11;\\xba\\xf5\\xef\\xbb\\xda\\xb8d\\xbc\\xb1@T=\\x93\\xc1\\xd9<\\x8e\\x11\\x00\\xbc\\x1c\\x86\\xce<\\x8c\\xbe<<\\xfb\\xad\\x00<\\xd4\\x16.=g?#\\xbdg\\xdc\\xde;vW\\xd1=T6\\xae9\\xf5\\xf8j\\xbc\\x06\\xb2E;\\x81\\xf8 \\xbd`\\x00$\\xbd\\xf3\\x88\\x08=\\x1d_\\xa0<h\\x97\\xc2;\\xbb@t\\xbc\\xee\\x805<\\xe1\\x16\\x16\\xbd\\x079p\\xbb\\xe5d\\x92\\xbc\\xb48J<8\\xf4#\\xbd+\\xff\\xfa\\xbbA0\\xeb\\xbcu\\x86\\x80\\xbd\\xc1\\xa8\\xd1\\xbcU\\xd7\\x81\\xbd\\x8cN\\x14\\xbc\\x98t\\xf2\\xbb\\x85i\\xc0\\xbbS.\\x07=\\xc3N\\t=R~{;Q\\x0e\\xef<H\\xeb\\x98;xcA\\xbd\\xe8\\xf6\\x8d\\xbc\\xb3@$=\\xe0\\xfa\\xa3\\xbc\\xf7\\xc9==\\xa8\\x01\\x04\\xbd\\xfbC:=\\xe7\\x11\\xbb:\\xb4\\x0eP=)/\\x1c\\xbd\\x94\\xdah=}E\\xb9;.\\x02\\x89<\\xa6\\xf2\\xf6<\\xe5\\xf72\\xbc\\xeb\\x18C=\\x9a\\xf2[=p\\xe9*<\\x88v\\x13;L>\\x10=\\x82K\\xf8<I\\x86\\xcc\\xbc)1\\x16\\xbd\\xb4\\rl=\\x1e\\x10\\xa8\\xbb\\x8a\\xd1t\\xbd!R\\x95\\xbb\\xd9\\x9fw\\xba\\x08\\xa2H\\xbd\\xea+8<w\\x82\\xc7\\xbc\\x89\\n[\\xbcz\\t\\x02\\xbd\\x82Z7\\xbdV\\x887\\xbd|8/=&\\xc5<\\xbd\\xa5GE\\xbd\\x9f\\xd8\\xef<1\\x07\\x89\\xbc=\\xe4\\xcc;:Z\\x9b\\xbc\\xd8C}=\\xaa+f<B\\x9af\\xbc\\x0fs\\x92<\\x81\\xc6!=\\x16\\xcd8<[\\xd4\\xeb;?v\\x03\\xbdW\\x8d\\x18<\\x92\\xab_=\\x91\\x81\\x0f<dU\\x86=;\\x7f\\x10\\xbc\\x84\\x18\\x16\\xbd\\x13z\\xb0\\xbdqjA=~h\\x07=8\\x90-<2yB;)\\xc5\\xf9<v\\xa3\\x9a\\xbd\\xa2J\\xa6\\xbc*_a\\xbc{d\\r<\\xcb\\xe1\\xcd\\xbc\\xa3\\x9c\\xac\\xbd\\xc3g\\x1d<\\x94\\xee2\\xbd\\xcbv\\x88\\xbc|\\x0c\\xc9\\xbc\\\"\\x8d\\\"\\xbc@;\\xe4<&\\x18\\x15\\xbc\\xb7\\x96L\\xbdI\\x92\\x08=\\x94Gc=\\xbf\\xdc\\xc6<^\\xa8\\x9f\\xbcu\\xbbU\\xbbZL\\x14\\xbdx\\xcd9<\\xdc`T=3\\xbeR=~\\x10\\x1a<\\xf9\\xe3\\xbd<)\\x880\\xbd\\xc2\\xf5\\xf8;,\\xc8r\\xbcp\\xebG\\xbd\\x7f\\x02\\xdb\\xbc\\xdf\\xc3\\xa9<\\xbb\\x81~\\xbc\\xa4R\\x86;\\x90V\\x1d=:\\t\\x18=]\\xdb/\\xbd1W@\\xbc\\xea,\\xa5;\\x83\\xee$=Y\\x1c6=\\xbaN\\x01=@L\\x03<V\\x94h=\\x05\\xd3\\x0e\\xbdk\\xec\\x05<\\x85\\xa5-=\\xc3\\xael<\\x97\\xff(\\xbd\\x14\\\"\\xaa\\xbc\\xe0\\xcb\\xb2\\xbc\\x01\\xc1\\x97\\xbc<\\x9b\\x82;\\x87\\x93\\xa5\\xbdu\\xd5o=\\x08o\\xcf<cEu\\xbc\\xaen]<\\xee\\xe1\\x86\\xbd\\xaeh~\\xbd\\xc8\\x80t<\\xfa\\xbb\\xc3\\xbc\\x82\\xf4\\xcc\\xbcU\\x80\\xc0<(~R\\xbdc\\xaeo<(\\x91p\\xbc\\x12\\x90\\x89\\xbd\\xb1\\xc1\\xa7\\xbdB>\\x94<\\x06\\x06\\x0e<\\xb4\\xd5y\\xbd\\xec\\x9a?<Rs\\xa9\\xbd\\x99\\x1b\\x15\\xbc\\xf7\\x01\\xac\\xbc\\xc0\\xbfG=\\xa6\\xe7\\xf6<\\xf61\\xfd\\xbc\\xac\\x9f\\x01=\\x16\\xa0e=\\x1c\\xe7\\xf6;\\x8ai3=\\x9b\\xc8.\\xbc\\xb3l\\x0e=\\xed\\xa7>\\xbb\\xa0\\x8aj<TNZ=IY\\\":\\xb2\\xbfI<\\xf3_?=\\xfdj\\x07\\xbdN\\xd1\\xf7\\xbc\\x11{n=\\xd5\\xf6L\\xbd\\x91\\x10\\xb6\\xbc\\xa1\\x07\\x1e=\\x88\\xf7\\xcb<\\x92\\xea\\xdd;\\xbe\\xd4\\xc6\\xbd\\xe5\\xe0\\x90<x\\x99\\xb0<\\xd9\\x9f\\xae<\\xcf\\xdc\\xae\\xbb\\\\\\x98\\x04\\xbc\\xdc\\xba\\xb4\\xbb\\x0e\\xa4\\x1c<\\x7fn\\xf5<\\xa5\\x9a\\x1c=\\xeb\\xb4S\\xbd#\\x00\\xe9\\xbco\\xcc\\xd1<d\\x05\\xf0\\xbc\\xef\\x86#\\xbd\\xf6qJ=\\xf8\\xe5\\x81<\\xdc\\xfb\\xae<\\x9c\\x7f\\xb0=\\x07\\xa6\\x15\\xbd\\xae\\xde\\xe1<\\x1e\\xdc];\\xb6\\xfa\\xb2\\xbc\\x18\\x1d\\xef\\xbc\\nF\\x01\\xbe\\xe0<\\x8a<f\\xf7\\x10\\xbd\\x9b\\xd6\\xc4<2\\x9e?=\\\\\\x05j=C\\x88l:`?\\x81=\\x19>G=\\xebu\\x94\\xbc\\x00\\xbb\\xeb=[w<=\\xb5)\\x9d=lX\\x19\\xbd\" \nHSET bikes:20005  model 'Kahuna' brand 'Noka Bikes' price 3200 type 'Mountain Bikes' material 'alloy' weight 9.8 description 'Whether you want to try your hand at XC racing or are looking for a lively trail bike that\\'s just as inspiring on the climbs as it is over rougher ground, the Wilder is one heck of a bike built specifically for short women. Both the frames and components have been tweaked to include a women’s saddle, different bars and unique colourway.' description_embeddings \"G\\x1e\\xaf\\xbc\\xd2\\x06\\xd1<p=v\\xbd\\x97}\\x0c\\xb9z\\x95\\\"\\xbc\\xc1.\\xe3<u\\xcb\\xd9:\\xe2\\x1d\\x9e<\\xea\\x0f\\xfc<0\\xb9\\x89\\xbb\\x8c,\\x02=\\xd6X\\x08\\xbd>\\x01\\xf0<\\xb0\\x8c/<\\xfbk\\xa0=\\x15\\xd7y\\xbbP\\xb3k=\\xae\\x01\\x03<h,\\xac=`Y\\xf3\\xbd\\x83\\xa3T;\\xd5\\xda\\x98<N\\x94\\xe3\\xbc\\xa6\\xb1c<\\x8a\\xdb\\x98=\\x88OR\\xbc\\x92\\x18\\xa6<\\x05\\xf4\\r\\xbc\\xca9\\xa4; 8\\x85=R\\xf0\\xbc\\xba# o:jc\\x00=\\x12\\x7f\\x16\\xbb\\xa5\\xb5\\xf0<7\\x89\\xac\\xbb\\\"*\\x88\\xbc\\\"%\\xce\\xbc\\x19i\\x95\\xbd\\x04\\x95G<\\x85_\\x9b=6\\x1aI=Q\\xfb\\xff\\xbb\\xd25\\xcb<+\\x98\\xdb\\xbc\\xf9L`9\\xdf\\x14S\\xbc)hT;\\x91\\xf2\\x9d\\xbc\\xbb\\xe8\\xac\\xba\\xf79\\x86<8\\x9c9<\\xa8j,\\xbc\\x91oZ\\xbdz\\x98P;Q\\xe81\\xbd\\xe1\\xed\\xe7\\xbc\\xc4\\x0f\\x02\\xbd\\x98\\xef\\x07\\xbdL\\xac\\xf1<\\xa2\\x1b\\x11=pR\\x1b=\\xcd\\x83\\xe0<\\xa1\\xa6\\xb5=\\x96\\x97\\xb1=\\xea\\x8f\\xde<\\xd2\\x13[=|\\xf4\\xe5\\xba\\xe5\\xfcs\\xbc\\xfci\\xbe\\xbc\\x99\\x08\\xa0<\\xe1\\xe5\\xcc\\xbd\\x19o\\xb6;R>$;A\\xb4\\xa8\\xbc\\x9d\\xbf\\x1c;\\xcfa\\x1d\\xbd\\xe0\\x9b]\\xbc\\x89\\\\\\x9c\\xbc\\x88\\xe2Z<\\x1c\\x05Y\\xbc|\\xbe\\xca\\xbd\\xfcL\\xdf\\xba\\xd9@/\\xbcU\\xbd\\xd3\\xbcI\\x8e6=~\\x9d6:Ns\\xc6=\\x99\\xcbD=\\x12,\\xa2<\\xe8P~\\xbd\\xfb2Q<z\\xe1\\t\\xbb)\\x90\\xb7<j\\xe6\\xdd\\xbcv\\xf7K<\\xc7\\xb4\\x0b\\xbd\\x82\\xf5A\\xbd%\\xb4\\xcb<&+L\\xbd\\x8a\\x1b\\x96=Ln\\x0f=\\x0fhy=\\x18\\x1f\\xdf\\xbc\\xc9\\xca\\x1b\\xbc\\x8bw\\xed\\xbc\\x8f\\xb8\\xd9;\\xf0\\t\\x91=\\xa9\\xad#\\xbd\\x8c\\xd7\\x1f<u\\xf1\\x00<\\xc4\\r\\xa6=E\\xdf\\x99=xG@\\xbd_\\xf5Y\\xbd\\x14:\\xc9;\\x1aW\\xec\\xbc\\xd8\\xff\\x92\\xb9\\x86\\xf8E=%\\xf5x<\\x92\\xbb\\x1f=\\x8c\\xae\\xe2<K\\x8by\\xbdxJ\\x9e<\\xf4\\xd2\\xd6\\xbc\\x80J\\x81\\xbds!\\x86<\\xfd\\x17\\x93\\xbd)\\x15\\xdd;r\\xe4\\x16\\xbdB\\x94\\xbc<\\x05[\\xc2<\\xc4\\x02\\n\\xbd\\xa2K\\x90<\\xc6\\x0e\\xac\\xbdW\\xc2\\xf9<Ci\\x7f\\xbd\\x8c\\x02\\xdb<qj\\xec\\xbb_\\xb5\\xec;\\x18B\\xf2\\xbcZ_\\xc4\\xbc.-m\\xbd\\xc0V\\xf7<\\\"\\x11\\xdc\\xbdE\\x9eN;\\xae\\xaa[\\xbc\\x0b i\\xbc\\xc0\\xe0J<\\x82_\\xa1\\xbck\\xea\\x07\\xbb\\x14\\xd0\\x83=\\x1f\\xcd\\x17<\\x8a\\x1c\\x1c=\\xdbB\\x88=yy\\x03\\xbd\\x14\\xde\\x9f\\xbd\\xaa\\xad\\xb2<\\xa2\\xd8<=N\\xf8\\x02\\xbde\\xd7`=\\xd3wA\\xbd\\x8d\\\\\\x0e\\xbc \\xd3\\xb2\\xbc@\\xdf\\x90;\\x1c\\xcf+=b&\\xdd<\\xbaJ\\x0f:\\x90\\xa2Y=\\x0b\\xdd\\xf1:2\\xf1\\x95\\xbd~\\xb2\\xa1<Vl\\x84<3\\\\Y\\xbd\\x15\\xef\\n;Np\\xd6<\\xd1\\xef\\xa3\\xbc*V\\\\=\\xfcT^<fCP\\xbc}Ti;\\xb32*\\xbc\\xb7KX=\\x82\\x86>\\xbc\\n\\xaaV<\\xb5\\xb6\\xdf<\\xdebo=\\xbd\\xe8\\x88<\\xb3\\xbf\\xef<\\x12\\xc1\\xcc\\xbc\\xc3\\x8f\\x81\\xba8\\xe2r<\\x80\\xc1\\x0f\\xbd\\x94<T=gy\\x94=@\\x13\\x9a:\\x97\\x1e\\x0b\\xbc\\xb6N\\x05\\xbd\\x02|\\x10\\xbdS\\xba\\x05=B\\x7f&\\xbc\\x16\\t\\x1d\\xb7H\\x0b\\xc3\\xbbEq\\x9c\\xbc\\x1a\\x81\\x14<\\xef8\\x819\\xf1~O=\\xfe\\x93\\xf7\\xbc*)J\\xbc\\x89\\xe1\\xf1\\xbbuvN\\xbd6\\xd3\\x92\\xbcS\\x8a(<\\xf8l\\x92\\xbc\\xc3\\xe5+=\\x14\\x13E\\xbc\\x1b \\xdb;\\xa8\\xfd\\x17<H\\x0c\\xd8=\\xf20\\x1f\\xbd\\xfaNR\\xbc\\x08\\xbe\\x99<U\\xf7f\\xbdK\\x046<bC\\xcc\\xbc\\xab\\xea\\xaa;&3\\xa3\\xbcy\\x8a\\xfb;g\\xabh=\\xbd\\x829\\xbc\\x14\\xb4@<\\x8aq\\xaa<\\xed\\x04_\\xbd\\xae\\xe4Y\\xbc\\xc8^\\x94<\\xa5\\t\\xbc\\xbd\\x17\\xa3\\xba;hu\\x07=\\xa0\\xb9(<I\\xda\\xb9;b\\x81n\\xbc`,\\xf0\\xbbf~?=i5\\xf7<\\x88,\\xfa<\\x8e_\\xc8<\\xd2\\xbf\\xb5<\\xe8\\x9fD<5\\x08\\xea<\\xbaI0=Z\\x8b\\x1b<\\xec\\x96\\x01\\xbd\\xcf\\n\\x1a\\xbcJ\\xe4$=2\\xe7\\x0e=\\xfe\\xb2\\x92\\xbc\\xcbr\\x8d\\xbbk\\x0b\\x89<\\xce^==\\x0b\\x86\\x93<V\\x03\\xa2<)<0\\xbcj\\x1e\\x1f\\xbd\\x9b\\xfbT\\xbc8\\xfd\\xc0\\xbcl&\\r=\\xd5\\x15Y<\\xbc\\x90\\xd3\\xbc\\x15:\\xab9\\xbce\\xb5<\\xce\\xd4\\x06\\xbd\\xf0\\x1d\\x1d\\xbc^J.\\xbc1\\x129<\\xefL\\x8d\\xbc\\x8da\\\\\\xba\\x1f\\x18&=\\x15\\xb5\\x91\\xbcb\\xb8\\x19;l\\x89s\\xb9\\xfc\\xf5\\xe9\\xbb\\xed\\x1a\\x1e<V\\x08\\x04=\\x0f# \\xbc\\xf1\\xf0\\xba\\xbb\\xd0\\xe9\\x03;\\x1a\\x0ek\\xbd\\xf3\\x13\\x90\\xbc\\xf6\\xc4<\\xbdH\\x1bt=\\x11G\\x97:\\x1cY\\xc7\\xbd@\\x10g\\xbc\\x17t\\x0e\\xbcT\\xf39\\xbd=\\xfdg\\xbb\\xed_\\xc5<\\x14/\\x80<\\x8c\\xd1Z;`\\x96\\xdd\\xbc\\xc1\\xc4\\x8c=\\x9dS\\x81\\xbc\\x91\\xc7e\\xbd\\xfe\\x88T\\xbd\\x01G\\xe58\\xdd\\x103\\xbd\\xe3\\xc4\\xea<\\x7f\\xc4\\x00<ZL\\xd3<)\\xe4!\\xbc\\xd2\\xd0\\x8b\\xbc\\xebH&\\xbd\\x83\\xb9\\xc1\\xbc\\x87h.\\xbd\\x88Ji<\\xe7\\xc5\\xd8\\xbcC\\x13^\\xbd>\\xc5\\x0c=T\\xca\\xbd\\xbcr\\xa9h<\\x83\\x1b\\x08<0\\x83\\x10=$IB<z\\xfa\\xec;\\x91\\x7f\\x05\\xba@\\xad=\\xbd\\xda\\xb8\\x8f\\xbc\\x9f\\xd2:\\xbd\\n\\xe7\\\"\\xb8)\\xee\\x8b<u#\\xb5\\xbcZ\\xcd\\xf1\\xbca\\xdf\\xfb\\xbc\\xd8Zj\\xbb\\x1eQ\\xf4\\xbb\\xae\\xca\\xb2<\\x00\\xd0\\xd0\\xbc\\xcb;\\xfa:g\\xd2\\xf6\\xba\\xa8\\xa5\\t\\xbc\\xf0w\\x16\\xbd\\xcc\\\\\\xba:\\xda\\xa2O=\\x15\\xe0\\xf5\\xbb_\\xc2\\x11\\xbdOn\\xcf<VI\\xc7=\\xc0\\x8f.\\xbb\\xc4\\xebY=f\\x06\\x8c<\\xeaA\\xf7\\xbchi\\x13\\xbd\\xe9uf=\\xc81\\x07<@\\x86\\x18<\\x96\\xfd\\x16\\xbd2\\xd0\\x1f<\\xb5\\xd0\\xfa<h\\r\\xaf\\xbc\\x82}\\xa5\\xbd\\xc1\\xc6\\xe3<\\xf1\\xb4\\x9e<o&b\\xbc\\xacc\\xde<\\xc6u\\xd3<\\xd6\\xea\\x83=\\x8a\\x03\\x10\\xbdVG\\x83\\xbb!T\\xcb;-\\x1e\\x8c<\\xf1\\xbcc=\\x0f\\x7fx\\xbc\\xf4\\x9fr\\xbb\\x7f\\x1f\\x0c;\\xa1~O\\xbd\\x1c\\x9b\\x06\\xbb\\x96\\xde\\xff\\xbc\\xc1\\x84\\xaf<\\x1e\\xf47=\\x93C\\x17=u\\xa6\\xd3\\xbc\\xf19\\\"=\\\\c\\xb1<\\x86:\\x10\\xbcW\\xfas\\xbce\\x99F\\xbcvf\\xf6<%\\xfd\\x1e=\\xcdE\\x02=v\\x9e=<5\\x7f\\xca\\xbb\\x9aJ\\xf4;~\\xe7\\x16=>\\x96Z\\xbd\\xa3\\xc1,=\\x9c:\\xf7<V\\xfc\\xb3\\xbd\\xe5D\\x86=\\xeaxI\\xbc\\xc3\\x9a\\xc9\\xbdc6Q\\xbc\\xaa\\xc3v<\\xb2Z\\xd1\\xbcj\\xfb\\xf6<K\\xd3\\x1a\\xbdx\\xe9\\x0e<\\x905S=\\x18\\x87\\x1a;\\xce-\\x11=\\xdf\\xffU=\\x91Ry=\\x8b)\\x9a<`)\\xaf<}\\xcfA=\\xfb\\x87\\x1c=\\xadT\\x909;\\xe1r\\xbd\\xfc\\x81\\xe6\\xbd\\x93\\xef*\\xbc\\xc4\\xc0\\x9f\\xbc(\\x9b\\xa2<\\xd6\\x0c\\x03\\xbd\\x99\\x16\\xb5\\xbc\\x8a\\xd3\\x1c=\\x86U\\xdd<\\xc9\\\"\\xf9<\\xa9l\\x9e:\\xb6\\x06\\xd3=\\xdb\\x0b\\\"\\xbd\\xce\\x93\\x8b\\xbb\\\"{\\xb9;\\xa3s\\x1d\\xbd\\xba\\xc1v\\xbd\\xachI\\xbd\\xa9:\\xd5\\xbc\\xb3\\xb1X=\\x1a\\x01\\xdf\\xbb\\xf4;\\xa4\\xbc\\x18Q\\xb4\\xbc\\xad\\x9c\\x89<\\xfdD\\x08=\\xe1w\\xc9<\\x04V\\x8d\\xbcr\\x8f\\x8c\\xbc\\xb2\\\"`\\xbde9><m\\x1c\\x0f\\xbd\\x88}\\xaa\\xbc\\x9b\\xe8\\xac\\xbc\\xb6\\xf0%<\\x16W\\xdf:eV6\\xbd\\x8e\\x11\\x0f=5\\xd6V\\xbc\\x1a\\x91\\x0f<)R\\xcf<RN\\x96\\xbc6@\\r=V\\xc3G\\xbc\\x8b\\x7fV=5\\x01\\xe7;MD|\\xbb{\\xff\\xb6\\xbc\\xbc\\xe4\\x0b\\xbc\\x02<*\\xbc\\xda\\x0b\\xd8<\\xd4\\x95\\xca\\xbc\\\"\\x1a\\xc0\\xbb\\xea^S\\xb94Q?<\\xe5+X\\xbd*\\xde<=\\xb5\\xc7\\xe7\\xbc\\xa1\\xe5\\x9a\\xbc\\x9cx\\x17\\xbdl\\x03\\xac<\\xb5>\\x1f;\\x14\\x8a\\xd1\\xbc\\xe8l\\xd9<%P\\xdf\\xbc\\xe0\\xf1\\x0f=\\xa5\\x967=\\xb7\\xe9:\\xbd~\\x90\\x8a=\\xdf~\\x80:\\xcc\\xe0p\\xbdoS\\xdf\\xbbB\\x87\\xf2<\\xd0W\\xf3<\\xb3\\xb0k=\\xa7\\xeaa\\xbd\\x02/;\\xbc\\xd3Bd\\xbdm&\\x82\\xbb\\xe6\\x16\\x81\\t\\x12\\xd3?\\xbd\\x8a\\xd5\\x0e\\xbd\\x91\\xb12\\xbd\\x1dI\\t\\xbc\\x80\\xc0\\x14\\xbd\\xa9\\x97\\x84\\xb9\\xcc\\x0c\\xd7\\xbc\\xe5e\\xf7\\xbcf\\xfe\\xfe\\xbc%%\\xbd\\xbc\\xeb\\tZ\\xbb\\x8a\\xa6\\x90=A\\x97W\\xbc5\\x0e\\xb3=\\x95\\xc2\\x98\\xbc=K\\xfc;v\\x98W<WT\\x9a\\xbc\\xbb\\x05G:\\x8d\\xaa\\x1f\\xbd\\xb0jf=\\xa3~\\xfa<Sv_\\xbd\\xdcl\\x07\\xbd\\xa3]?\\xbb\\xa2\\x14\\x8a\\xbc?9\\x94<\\xd6\\xb0\\xa5<\\x9f\\xb9\\xca<?\\xa9\\xfe\\xbc\\xa6\\xa0\\x8b<b\\x93/=|\\xde!<\\x1d\\x14\\xb8\\xbc\\xa6\\xbaG9dD\\x9f\\xbcj\\t\\x97<\\xd6-3<\\xd1l\\x8d<\\x14\\x18\\xb7\\xbbJ\\xc7\\x06=\\xd2\\xe09\\xbd<\\xe3\\x7f\\xbc\\xcf\\x93E=\\xfb%?<{\\xbb\\x91;+3\\x9b=\\\"uZ\\xbc\\x10\\tg<=\\x80\\x9c\\xbbM4]<\\x1c\\x03\\x18\\xbd,\\xdb\\x8d=\\xa9\\xd5\\xaa;\\\\y7=w\\xf1\\xd9\\xbc\\xb71_<SD\\x9d\\xbc;\\t\\xb0\\xbdfE\\x91\\xbci\\xc3.<5\\x94\\xba\\xbc\\x19\\xa9\\x05<|\\x12\\xfe\\xbc\\xa7n\\xc1\\xbck2\\xbf:\\x18\\xe3\\x8b\\xbc\\xab`\\xcd\\xbc\\rQ <\\xcc\\x91#=\\x1d\\x15`\\xbc\\xa4\\xafp<\\xbep)<k\\x06\\x01=\\xc0{\\xfb<\\xd8\\xcf~\\xbd:\\x82\\xfa\\xbc\\x88\\xb5\\xc0<]\\xd5\\\\\\xbdw\\xf4\\x86\\xbb\\x8a\\x97\\x88\\xbcHD\\xe5\\xbc|k\\xe3\\xbb\\xc2\\x94\\xcb\\xbb\\xfc\\xef]\\xbd\\xecv\\xcf\\xbbl\\xd1^=\\xc8Gf\\xbb\\xb1\\xaa\\x0f=X$v\\xbd\\xbb\\x14\\xb5<j\\xd7_\\xbc\\x1b\\xdd\\xb6=Q0\\x1d;\\xfd\\x10i=\\xb1\\x00\\xdc<\\x94\\x8e\\x15<\\x9d\\xe3\\xde\\xbc\\x9b\\x98F=\\x85\\xab\\xb4<\\xb5y\\x97\\xbd\\xec\\x0c\\xef\\xbc\\xad\\xf18<c\\xc9M\\xbd\\xde\\xf9\\x02=\\xcep\\xef\\xba\\x9f#\\xd4\\xbcOG\\xee;\\xf1r\\x18<\\x18!\\xc5\\xbd\\xc0YM=\\xd3~U\\xba\\x1e\\x97\\xd4\\xbc\\x06\\xa5\\x03;\\\"\\xe8\\xed<$#b\\xbb3\\xb5\\xe5:\\xda\\xa0\\xf7<\\x07\\xbf\\x12\\xbd\\xe7\\xc3b\\xbd\\x07\\x1f\\x1c<Vo\\xe3<\\xcc\\xcb\\xcb<G\\xd3\\xd3:D\\xd9Y\\xbd\\x01Y\\x8c\\xbc\\xd7\\x99\\x1b\\xbc\\xc7F\\xb6\\xbc\\xe0F\\xa9<Ad\\x88\\xbb\\x82\\x99\\x8c9\\xcc\\xddQ\\xbd:\\x8c)\\xbc\\x1c\\x19\\x07=\\xbb\\x98\\x0c\\xbd!\\x95\\x05<\\xf0\\xcb\\xbe:\\xa7C?\\xbd\\xff\\xbcH;\\xb8x#\\xbdn\\x93\\x94\\xbbI\\xc4\\x1e\\xbcit\\xa2\\xbc\\x83\\xf5R=\\xb46;;\\x1b\\xe4\\xb0\\xbcpF\\x00\\xbc\\xfe\\x13P\\xbdS&\\xdf<\\xcc\\xbaO\\xbc\\xfa\\x96Q\\xbc\\x0e\\xbe\\xe1\\xb9G|]\\xbcP\\\"\\x85:5E\\x94=\\x9e\\x06\\x06\\xbc\\x02]\\n\\xbd7@\\xc2\\xbc\\x86\\x06e<g\\xefZ=\\xcd\\xfb\\x89;\\xa2o\\x96\\xbc\\x15\\xa95\\xbb\\xdf\\xfd\\x83<Xo\\xf7\\xbc\\x1f\\xccM:Z?G\\xbd\\xb2A\\xaa=~\\\"c\\xbdO\\xe2\\xd6\\xbc\\x80\\xb0@<I{\\xe1\\xbb\\xc7\\t\\x10=\\xca\\xa8\\xcc\\xbc\\x0f\\x01\\x8c<\\xe0\\xe7\\\\\\xbc\\xec\\x9a\\x00=\\xee\\x9b\\xad\\xbd{\\x1f\\x01\\xbdk\\x9e!=\\x08\\x055\\xbdOJ\\x0c=\\x1dz\\xfb<\\x1b\\xa4\\x7f\\xbd\\xdc\\r\\xef\\xbc\\xac\\x0c\\x00=\\x9acC\\xbdS\\x86w<9C\\x84\\xbc\\xbe#\\x0c\\xbd\\xe2\\x9d\\r\\xbd\\x0f\\xc85<\\xcbA\\xcc\\xbc\\xd48\\x1b<\\xd3\\xb9=\\xbc\\n\\x1b\\x0c\\xbc\\xad\\xb8\\x96\\xbc\\xa0\\xe4\\x0c<,\\x08\\x1a\\xbd\\xafT;=6_f;HN\\xe5\\xbc~\\xb8\\xdd\\xbc\\xd6\\\\\\x9c\\xbbl\\xcfe\\xbd2\\xed\\x01\\xbd\\xec\\x85\\xcb;z\\xe3=\\xbd\\x1cm\\x15<N\\xf9_\\xbd\\x84*>\\xbc!\\x1f\\x8a\\xbd:\\xf5\\x1e=yCw<=\\xde\\xc8\\xbbdb\\x1b<Ey\\xda<\\xf0\\x02\\xa3<+^\\xe0\\xbct?F\\xbd!\\xab\\xa0\\xbc\\t,\\x8c\\xbc\\xb0\\xceb=\\xb4\\xa8\\xba\\xbd\\\"x\\xaf=\\xbc\\x18\\x92=?\\xd8\\xc8\\xbc[\\xca\\x87\\xbd[\\xa7\\x8e\\xbd\\x99\\xccZ=\\xb4\\xbc\\xdb\\xbc\\xfc\\xce\\xd7<\\xa3\\x01\\xfe<\\xe5\\xaa\\xd7;\\xfd\\x8d\\x03<\\x84^\\xf8\\xbdH\\x198\\xbd\\x84\\\\\\x86=s\\xcb\\x1d\\xbdIr#;\\xc8OZ\\xbct\\xf9E=I\\x17A\\xbc\\xbe5\\x94\\xbb\\xc1\\xfc\\x86=9\\x01\\x1e\\xbd\\x86\\xe64\\xbd*\\xb5\\xd1<\\x86\\x1f\\xa5\\xbd\\x96B(\\xbc\\x9f\\xdee=l2\\xe7<\\xcb^\\xa2\\xbbw\\x1bz<\\x027\\x02\\xbbT\\xe3R;N|W=\\x92F\\x13\\xbb\\x91\\x8aF\\xbd\\xc7&\\x05\\xbd\\xac\\xf2^=\\xee\\x1eZ=)3i=rM\\xad\\xbc\\x04\\xe3f=v\\xbfj<\\x02\\xf1\\x8b=\\x93H\\x10=\\x99f\\x8a\\xbbH\\x9c\\xa9<\\tuE\\xbc\\xb9\\xa6\\xe2<\\xb6\\x05G\\xbd\" \nHSET bikes:20006  model 'XBN 2.1 Alloy' brand 'Breakout' price 810 type 'Road Bikes' material 'alloy' weight 7.2 description 'The XBN 2.1 Alloy is our entry-level road bike – but that’s not to say that it’s a basic machine. With an internal weld aluminium frame, a full carbon fork, and the slick-shifting Claris gears from Shimano’s, this is a bike which doesn’t break the bank and delivers craved performance. The 6061 alloy frame is triple-butted which ensures a lighter weight and smoother ride. And it’s comfortable with dropped seat stays and the carbon fork. The carefully crafted 50-34 tooth chainset and 11-32 tooth cassette give an easy-on-the-legs bottom gear for climbing, and the high-quality Vittoria Zaffiro tires balance grip, rolling friction and puncture protection when coasting down the other side.  ' description_embeddings \"\\xba%s<?|\\x19\\xbcU(#\\xbd\\xb3\\xb4Q=!\\x97~=\\x8b\\xe8\\xf5<\\x9a\\xd7\\x93\\xbc\\n\\x9eI\\xbdI\\xcc\\x8b=\\xc4\\x85|=}\\x88h\\xbcn\\xa9\\xc6=\\x0e`\\xc6\\xba\\x84\\xfa-\\xbc\\x9bt\\x1f=\\xd5\\x00\\x80\\xbd\\x81\\x1e\\xb9<\\xf3\\xb3\\xa7\\xbc\\xa8\\x17\\x8a<9L\\x9f\\xbc\\x13\\x12\\xd8<!\\xae\\xbe\\xbcT\\x80\\xce<M\\x07\\xc7\\xbc\\x9e/\\x13\\xbd\\xbf&\\xbc\\xbc\\x05|e\\xbc\\xbb\\xd9\\xbe\\xbc`,\\x92\\xbc\\xb4\\\";=C\\x97Q\\xbc\\xcd\\xc7\\x8a;\\xba\\xb8*\\xbb\\xa3\\xe4;<p\\x94\\xbc\\xbb1\\x13\\x84<on\\x7f\\xbbq)J<\\xcd\\xe1\\xf5;\\xce\\x92-\\xbcf\\x82s\\xbc\\xa7\\x9d\\xc4<\\x884\\x07=r\\x12\\xa1<\\x19\\x9d=<\\xbb[[\\xbca\\x070=\\xae\\x1a\\xa06`\\x9f3\\xbdv\\xce\\x9a\\xbc\\xf6\\xf9\\xcb\\xbbM\\xb7\\x91\\xbcl\\x95\\x90\\xbc\\xdef\\xa1\\xbb\\x96\\xf0A<4\\x05\\xba;F}R\\xbc`\\xf1\\x9a\\xbd-=K\\xbd\\x0f\\xa89=\\xd8\\xa8\\x91=%CH\\xbc)\\xf9\\x08\\xbdl\\x8c9=\\xaf\\nB=\\xe3u\\x8d<E\\xa6\\x1b\\xbd\\xaf\\x99\\xbc\\xbc\\x80\\xd7\\x97<\\xd1\\x15A\\xbcE\\xc9\\x81\\xbd\\xa0}\\xba\\xbc=\\xf1\\xea\\xbb\\x01\\xa5\\t\\xbd\\x19\\x8b\\x01\\xbd\\xdd|\\xeb\\xbcu\\xd7\\x1a\\xbb\\xf8\\xee\\x06\\xbd\\x08$\\xa4;\\xbc~\\x15<\\xe6\\x02\\xc9;\\x93r2\\xbd\\xc4\\xeaG\\xba/.\\x9b\\xbd\\x02\\xf4\\x8c\\xbdB\\xd7U\\xbdyK\\xc4\\xbb\\xa5\\xbc\\xba=\\x888E<\\x10\\xed\\xb5<\\xa8\\x84\\x19\\xbct\\xc8K\\xbbfAJ\\xbdz\\x84\\xbc<\\xb2YF<\\x9c\\xaaX<vp\\x04\\xbce\\x95V\\xbb\\xfe\\xa4M;\\xd2\\x01\\xf3\\xbc\\x98\\x022<\\x01x\\x85:|\\xfd\\xab<\\x7f\\x95{\\xbd_`&=|j\\x0f>\\x8d\\x17P\\xbc\\xc6\\x97\\x15<\\xe7\\xb1T\\xbc=\\x8c\\r=\\xca\\xcc\\xd7;n\\x85\\x8d=\\xc6\\xb8\\x88<\\xab\\x1bC\\xbc\\xde\\xa4\\xbe\\xbc\\xbeg`\\xbd(\\xf6|\\xbd\\xf2\\x9d\\xfc\\xbc4\\x86R<\\x82NY=c\\xbf\\x12<\\xeb_\\xe4<HD\\xac\\xbdO\\xd5\\xe8<\\xbdTX=\\xf6k\\xb6;L\\x8b\\xce;\\xbfC\\x92\\xbc\\xf1g\\\"\\xbd\\xea!\\x07\\xbd\\xed \\x1f\\xbd\\x03\\xdc\\xe4;\\x1b\\x0f\\x1b=9\\xaf0\\xbc\\x81k\\xf3\\xbc\\x8f\\xc9\\x9b<x\\xb17=\\xb9\\x91V\\xbc\\x19Gh<8\\xda1\\xbc\\x8dj\\xd5\\xbcmS\\x16\\xbd6\\xde2\\xbd\\x06\\x8e\\x83=y.Z\\xbc\\x9c\\x886\\xbdp\\x83\\x08;_`(\\xbd\\x14\\xb9m<\\xe3\\x1a+\\xbc\\x95\\\\\\x9e\\xba\\x14\\xdc\\x8f=\\xc3\\x88E\\xbd\\x8bX(<\\x81\\xd9\\x04\\xbd\\xaf\\x8f\\x19;\\x83\\x9a\\xd7\\xbc\\x1e%^=0\\xf6\\x0b=1\\xdd\\xab\\xbc\\xd4m\\x1c=S\\x14\\xb7<\\x01l\\xc2\\xbc,\\x901\\xbd\\xcd_\\x14=\\xaa\\xe8J=z\\x97\\xc1\\xbc\\x90\\x05\\xe1<A\\x9eA=\\x01\\xbbQ<\\xa5\\xa8!\\xbd^d\\xc9;\\xf5)\\\"=\\\\\\xd4\\x84\\xbdd#\\x85;Ke\\xd4<Dy\\xa7\\xbd\\x8e\\x0e\\x89=.\\x81\\xd0\\xbc\\xb35\\x13=\\x9d\\x86*:\\xef\\xd1S\\xbc\\xad\\x8a\\xf7<\\xb6\\x7f\\x98\\xb9\\x8do\\x00=\\x91\\x1c\\x16=\\x00x\\xdb<\\xb8<\\xc9\\xbc\\xee\\x83\\xc6<=8\\xb0\\xbc\\x8c\\xc0\\x00\\xbc\\xd54\\xae<\\xa1\\xaa\\xf3\\xbc\\xdaO\\xf8< U\\xf9;\\xa3<\\x83=\\xd9\\x08\\xa5\\xbc4-\\xee<Z\\x83\\x809\\x8arQ=X\\x85\\xda\\xbc\\x98\\x10,\\xbd/\\xc6\\x9a\\xbd\\x10\\x02\\xee\\xba\\x8f\\x86$\\xbc\\xa3\\xdeD\\xbc\\xf7\\xf5\\x93\\xbc_\\x81d<\\x0e\\x89A=[\\xcd;\\xbdq\\n\\x9c\\xbc\\x17\\xec\\x95<\\x01\\x14|\\xbc\\xa7w\\x89\\xbdF \\xff;\\x99\\xe7\\xec<Y`\\x8d\\xbdV0\\xa4<+q\\x07=3\\xd6\\x87\\xbc=;1;\\xd3\\xab\\x9a<\\xa9\\x9e\\xc1\\xbcQ\\x812\\xbb\\x9d\\xdf\\x8d<e\\xcdD;\\x90\\xa5\\xdd<-m\\xe9<\\x14\\x1e\\xd1<7/u\\xbcU\\x06\\x03\\xbd\\xec\\x86\\x8f\\xbc\\xee\\xb9\\x1e\\xbd2=\\x0e\\xbb>\\x9a\\xf9\\xbcO\\xf5W\\xbd2\\x8e\\xb7<\\xa3\\x9bb;\\xaeG\\xbe<>\\xc7\\xf2;\\\"MF\\xbc\\t\\x0c2\\xbb\\xa2_==\\x9fq4\\xbcxzA=\\x1c\\xb9\\x08\\xbb\\xab\\xd9!;\\t\\xed\\x8f<?\\xe7-\\xbd\\xfbZ\\xc0;/\\x1fP=3\\xa0\\xa2\\xbdh9\\x93;\\x93\\x85\\x85=\\xbd\\xf5 \\xbd\\x85\\x15Z=\\xf6\\x9f\\x8b\\xbd5t\\x84\\xbb\\xf9\\x81\\xb4\\xbc7\\xf4\\xb1=\\xa8f\\xd3<a\\xe0\\xeb\\xbc\\xbf\\x7f_\\xbd\\x95*\\x9a;\\x16\\\"\\\\\\xbd\\xb05u=v\\xadz<\\xc9\\\\u<\\x1a3P\\xbd\\xbew\\x13\\xbd\\xa6\\xeb/\\xbd\\x18i\\x9d<_\\x1aT<}\\xa1\\xf9\\xbb?\\xb9\\x1a\\xba(\\xae\\xb7\\xbc\\xfa\\xb8\\xcd\\xbb \\x88\\xbf<`\\x8d\\\\<c\\x10{\\xbd\\\\\\xa0\\xec\\xbc\\xe9c\\x92<\\xec*\\xfa<8\\xea\\x86\\xbc[&\\xcc\\xba\\x87I\\x8f\\xbc\\xdda\\xb7\\xbc\\xaci<=Z{\\x94\\xbcE\\x9e\\xe9<\\x16\\x9a\\x98;:~q\\xbc|b\\x03=\\xa7\\x95\\xb1;\\xba\\xdc\\xb3\\xbc\\x9a\\x89o\\xbd\\x12\\xcb\\xc99G\\xae\\x17=S3\\x1a<\\xec\\x97\\x0f9\\x14\\xf3\\xbb<\\x94\\x0fw<\\x12\\xd2T=\\xf5W\\x05\\xbd\\xdc\\xbd\\x87<S\\x056=)\\xab\\x19=\\x85O:=\\xaf\\xb3i=\\x8b)\\xcf<\\xd8\\xcb\\r\\xbd\\xae\\x8e\\xbc\\xbc\\xe6!\\x00\\xbd\\xcb-\\x1a<\\x04\\xb6\\x95;l\\x1d\\xf5\\xbc\\xd1\\r\\x81<\\xbdf\\xc9=q\\xe5\\xa4:\\xb74\\xd3\\xbc\\xda>-\\xbd\\xb41\\\"\\xbc\\x93\\xe6\\xc6<F\\xbd\\xe3\\xbc5\\xa0\\x89<%\\\"\\x1e\\xbd\\xdd\\xe0\\x99=UU\\x08\\xbd2[\\x02=5\\xaf\\xb6<\\x85>\\x11\\xbc\\x86\\xb4\\xd3\\xbd\\xc5)\\\";7|\\x90=\\x94\\xa9\\x9f<\\xacQ:\\xbb\\xacE\\xe9\\xbc&k\\x8b=\\xdcT\\xce<+\\xef\\x9a\\xbc\\x92x0\\xbd\\x11\\xe4p\\xbd\\x00\\x94d=.\\xe1)\\xbc\\x8cL\\xf3\\xba\\xd0\\xa3w<\\xe9h\\xbd=\\xff\\x88\\x15\\xbc[\\x1a*==\\xa6\\x0f9\\xa1\\xf9d=\\xbc\\xb1\\x11\\xbd\\xe5\\xfb2=m\\xe9X\\xba\\xff\\x8d\\x9b\\xbd\\x81\\xbd\\xbc=\\x02z)\\xbcw\\x90I\\xbbW\\x00\\r\\xbd\\xb4T\\x8d\\xbd\\x1b\\xa7\\xb7\\xbc\\xa7VX=x@\\xeb<\\xe8\\x93\\x88=\\xe5\\x90\\xfc<\\x00Ur=;\\\\\\xa0\\xbd\\x9f\\xbc\\xcf<\\xc6\\xe9\\r\\xbdA\\x0f^\\xbc\\xf5\\xbdR=UJ-\\xbd00\\xc4\\xbbVFD=\\xee\\xab|\\xbc\\\"\\x92O<\\x8e\\x0b?\\xbd\\x18\\x95b\\xbd\\x9fU\\xed\\xbb\\xaf\\x8c\\xe3;i\\xae\\x17\\xbd\\x8d\\x8d\\\";\\x07`6\\xbd^\\xb4Q<\\xce_\\xda\\xbcO\\x04\\x82<\\x05\\xbd\\x80\\xbd\\xe0<$=\\xbe \\xb0<\\xae\\xe7\\xc2\\xbcb\\xda\\xe2\\xbc\\xbb\\x15\\x82=\\xed\\xb2`=\\xd1\\xd9\\xa1\\xb9kO\\x0b<\\x80\\n\\x19\\xbd\\x05\\x10\\x85\\xbc\\xd6\\x1bR;\\xbd\\x1f\\x88=\\x15\\x84{\\xbd=\\xc5\\xc0<\\xcd?\\xf4\\xbc\\x08\\x1d\\xba\\xbc\\xc9_\\x96=6>:=[{\\xd9\\xbb\\x85\\x03\\x90\\xbb\\xc2~\\xdb\\xbc\\xac\\xf1\\xf4\\xbc\\\"\\xf4\\xaa=\\xde\\xd2$=\\xa3F2=.d\\xa8;\\x03\\xa2\\xa8\\xbc? 5\\xbb\\xe9\\x8a2\\xbc\\xcdS\\xd4\\xbb\\xc1[\\xdd\\xbd\\xd4\\xae\\xc59v\\x84T=o\\r\\x82\\xbb\\x94\\x12\\x1a\\xbdTQ\\x16=\\xd2\\xdcB=,n\\x19\\xbb\\xdd\\x9c\\x15\\xbd54\\xcd<\\x01\\x8d\\xea<\\x94UI<\\x8a\\xb4V\\xbd\\xd0^J\\xbd\\xf0\\xd3\\x18<\\xf5?a\\xbc\\xeepv\\xbd\\xe1\\xeb.\\xbc\\x85\\x86\\x9b<\\x15\\xf8\\x07=y\\xa6*=\\xd7\\xfah\\xbcS@B=\\xc3\\xe1\\x0f\\xbd\\xdeq\\xf4\\xbc\\n#9=+`\\x0b\\xbd\\\\\\x18\\x8e\\xbb\\x8e\\x06\\xbd\\xbb\\xc5$\\x8b=\\xa0\\xd4\\x85\\xbbW\\x0e5\\xbd\\x86\\xbf5=\\xe1h\\xf2\\xbcck\\x14:\\xd0\\xef\\xb3\\xbb\\xb3\\xc1\\x99\\xbdH\\x91\\x8b;@I}<\\xb7I?;6\\xf3\\x8f<$\\xa0#\\xbcZ2\\x16=\\xea\\xf5\\xf7<_\\x16\\x05\\xbdlm\\xaa<\\xb57\\x15\\xbd\\x8fg\\xf7\\xbb\\x965\\xa5<p\\xeb?\\xbcW\\xd4\\x80\\xbdT\\xf7\\xd3<Z+X=&W\\x1b\\xbd\\xc6\\xe4\\x17\\xbcM8\\x1a<\\xb5\\xa4E=\\\"\\xe1\\xdb\\xbb\\xcc\\xf1\\x13=\\xe70\\x17\\xbd\\\\{Q\\xbd\\xcf\\x95\\x0e;>\\xe9\\xf3\\xbc\\x7f\\xa8\\xd8<\\xbem\\xe8<\\xc6p\\x06\\xbc\\xc3v\\x00=\\x12r\\xf7\\xbcP\\xac\\x9d\\xbd=\\xe4\\x82=\\x0e{5\\xbdc\\xc0\\x82<?9N\\xbb\\x01m7\\xbdba\\x88\\xbc`B\\x17=\\xf5\\xf0\\x92\\xbd\\x11N\\x8e\\t\\xe8x\\xf1\\xbb\\xa5\\xa0j=\\x82\\xf1a:\\x9c\\t\\xa6:]r\\xc7<6\\x83\\x04=\\x92M\\x19;\\x84\\xc3\\x8a\\xbc\\xeai\\x07=\\x89f\\xb2<\\xc4\\x855=\\xadq\\x82<\\xa7\\xe4\\xc2;\\xb2y_=\\x196\\x91<=t\\xbf\\xb9\\x1a\\xb3b\\xbd}Jq;x\\xe5\\xac;{\\xdcA=Z\\xbb\\xdb;\\x9b)y\\xbco\\xd6\\xec\\xbc\\x0f\\x00\\xaf\\xbb\\xfcC\\x7f=)?\\xf1\\xbb\\xce+\\x88<o\\xe8\\xca<\\xee\\xbf\\xd3\\xbc\\xe8\\xd0\\x9f<Q\\xeby\\xbb\\xeb\\x01^=\\xb92\\xb9\\xbd\\x0c]\\xa7\\xbd\\xba/M\\xbc\\x07t\\xc3:\\xe9\\xd98=X9\\x9d\\xbc\\x94b\\x86<o\\x9c6\\xbb\\x84\\x9d\\xb7\\xbb\\xd4fE\\xbd\\x96\\x9a\\r<\\xc1\\x95\\x0e<J}\\x0b\\xbcY[=<\\x82]t=E\\x07\\xda\\xbbo\\xafX\\xbdg#\\xab<\\xfb{}\\xbc\\xce\\xb5K\\xbd?\\xc7#\\xbc\\x0f\\xa7\\x9b<\\xcbZA\\xbb\\xa8\\x7f==\\x0b,\\x82=\\xbcb\\x8d\\xbaGaz=\\xecT\\xad<\\xd8\\x02\\x03=&3\\x10\\xbd\\xec4`=\\x87\\x8a\\xd3\\xba\\x99#\\xc5\\xbd9\\x06C\\xbd\\xa4\\xc3\\x17\\xbd\\xf7 \\x8a\\xbbUM\\xf0;\\xad\\x83\\xc1\\xbc\\xcd\\x89_\\xbcJG;\\xbd\\x07\\xbb\\x8e\\xbd\\xb0`2=\\xcdV\\x13=Sm\\x8b\\xbc\\xb4\\x95M\\xbc\\xf7\\xfa\\xb5=\\x89\\x07\\x99\\xbd;\\xc1\\x1a=\\xc1\\xfc)\\xbd*\\x8e\\x16=\\xcdya\\xbd\\\"\\x11\\xd3=\\xdb*\\x94\\xbc}%\\x08\\xbd~\\x07\\x7f=\\xd2\\x99h<&\\x9a\\xc9\\xba\\xf9\\xb6\\x8e<\\x04{5=+`v\\xbc\\x99\\xacF=\\x97@,;v|E<\\x19\\x04%=\\xb5ei\\xbc\\x98\\xa5\\xd9<X\\xd5\\xb5\\xbcK\\x918\\xbd\\xaf\\x0bl=J\\x01#\\xbd\\x815\\x92\\xbc\\xff\\xdb\\xf3;xdD<\\xc0\\xc0/\\xbc\\\"\\x8d+=i\\xdd\\xc5\\xbcz\\xf2$\\xbd\\xfe\\xe1*\\xbdd\\x11\\xe8<,\\x92?;\\xf9\\xba/\\xbdZ\\xbe\\x13<\\xe1i\\xcc;n\\xd5\\x84<D\\xc8\\xbe;}\\x8f~\\xbc\\xd8O\\xae<\\\\\\x16\\x06\\xbd\\xa5\\xca\\xf0<\\x978X<\\xfb\\x85%\\xbc\\x042\\xd3<Z~\\x1f==\\xba,\\xbc&\\r \\xbbP\\xbb\\xcc\\xbcY\\xb0K\\xbc\\x1a\\xf5\\x85\\xbc\\xb6\\xde)\\xbdK&X\\xbd\\xe3e\\xff<\\xf4qw<w\\xd2\\x06=\\xde\\xe9\\x07=\\x13Wd<\\x90\\xaa \\xbd\\xbde\\xcb<\\xb2\\xa0b\\xbd\\xd9ff<\\xdd\\x0e\\x88\\xbcGeL\\xbd`\\x83\\x16<\\x14\\xfe\\xd1:n\\\"\\xbf\\xbc\\x0e\\x16\\x9a\\xbb;\\xed\\xad\\xbc\\x04XW=ZE2\\xbdq\\xec\\xe4\\xbc\\x1a%o\\xba\\xe5T\\x9c\\xbc\\xd1X\\x1f=\\xcb\\x8c%;\\xdf%F<\\x96\\x14B\\xbd\\x82\\x81\\xab<\\x94\\x95i=\\xff\\x120=\\xaf[\\x1d=\\xac\\xf8\\\"=\\xae\\x17\\xb4\\xbb\\x00\\xf7l:\\xdd\\xb9\\xcd\\xbcS\\x12\\xf8\\xbc\\xa0\\x15\\x0c<\\x10&)\\xbc(\\xe2\\xad<\\x95\\xd8\\x94\\xbc\\x15\\n\\x1b;@\\xd0l\\xbd/\\xd5f:,\\xc8m<\\xd2\\x894=\\xd4b\\x06=\\xb24\\x17=dw\\x8c<\\xcc\\xa9\\xa0<e\\xf0\\xc4\\xbc\\x9f\\xad6:\\x86*Y\\xbd\\x93\\xd6\\n=\\xfd6O\\xbdl\\xf6\\xca\\xbc\\xafy\\x1b\\xbc\\xd2n\\xc0;\\xd9i\\t\\xbd\\xbf\\xe6\\xc9;\\xef\\xd4\\xe5\\xbc\\xabh8\\xbd\\xca\\x97\\xc5\\xba\\x00\\x068\\xbc}\\x04\\x0b\\xbdeL \\xbd\\\"\\xf1\\x9b\\xbd\\xc4(\\xb7<\\xf8\\xeeU\\xbcO5\\x9f\\xbd\\xf5\\x99~\\xbd\\xd0\\xa5L\\xbd;F\\xb6\\xbc\\xd06\\xca\\xbb\\xcf\\x85\\x98\\xbd\\x9a\\xbbj\\xbd\\x0e\\xefY\\xbd\\x91\\xee\\x84\\xba\\x02\\xa4\\xcb;\\\\:\\xd2;\\xbe\\xcc\\x89\\xbd\\x1f\\x86a<rp\\x9e\\xbb1A\\x0c=\\x81\\xcc\\x85<<j\\xa8\\xbc|B\\xea\\xbb\\xd4\\x93\\x14=7~\\x11\\xbd\\x99\\x01\\xad<\\x0ek\\x98\\xbd\\r@)<@\\x8b\\xb6\\xbcKv\\xcd;\\xb6\\x97\\xb1;\\xda\\x91^=\\xbcO\\xfd<\\xd2)\\xa1: \\x81\\x8b\\xbd-\\x858\\xbd\\xd9\\xae\\xe2<\\x83\\x0f6<\\x0e\\x03\\x99<\\xb3j\\xa8\\xbc[\\xcd\\x8b\\xbd\\x0b\\xff\\x9d\\xbc\\x9cN}\\xbd\\x08x)\\xbbCA\\xd8<\\x1bn\\x16<\\xcb#\\xd6;\\x92A\\r<\\xe8*V<wZ\\xdf<0\\xa9_\\xbcv\\x98H\\xbc;\\xdb\\x86\\xbc\\xec?D\\xbd(\\x90\\xb4\\xbcM\\xe0\\x03\\xbd\\xbf\\xfb\\x17\\xbby\\x0c\\xaa;\\xd8\\xbaP\\xbb_T\\x06=\\n\\xcc\\xef<;\\xdb}\\xbd\\x9d\\xfe\\x0c\\xb9N\\x98\\xd3\\xbc\\xbf\\x0b\\xb8<\\x05\\x14r\\xbd\\x8d\\xd7<\\xbcX\\xcc\\x81<)\\xaf\\x08\\xbd\\xaf\\xc1*=\\xf4\\xabc<\\x85\\x99\\x9c=\\x1fG\\xff\\xbcn\\xa4\\x85=?HA=&\\x8b\\xf5;\\xb5\\xbe\\x04=\\xfd*\\x82=\\xaf\\xbfb\\xba \\x93V\\xbc\" \nHSET bikes:20007  model 'WattBike' brand 'ScramBikes' price 2300 type 'eBikes' material 'alloy' weight 15 description 'The WattBike is the best e-bike for people who still feel young at heart. It has a  Bafang 500 watt geared hub motor that can reach 20 miles per hour on both steep inclines and city streets. The lithium-ion battery, which gets nearly 40 miles per charge, has a lightweight form factor, making it easier for seniors to use. It comes fully assembled (no convoluted instructions!) and includes a sturdy helmet at no cost. The Plush saddle softens over time with use. The included Seatpost, however, is easily adjustable and adds to this bike’s fantastic rating for seniors, as do the hydraulic disc brakes from Tektro.  ' description_embeddings \"\\x02\\xaf\\xff\\xbaA)\\x02=PA\\xa0\\xbd\\xc9\\x1a\\x18\\xbc\\xf9\\x9a]\\xbd6fM= \\x92\\x8b\\xbc\\xed\\x13w\\xbd\\xddvT=\\xa9*d;\\xe61\\r\\xbd\\\\\\x0bF\\xbd7@+<F\\x0b\\xb2<\\xca*X=\\x8a$\\xb1;\\xa8\\x18\\x17=a\\x8a\\x96\\xbb\\t\\x968\\xbc4\\x14\\xa8\\xbc\\x9f:\\xc5<\\x81\\xbd+\\xbd0*\\xd7<\\xee\\x95\\x00<8}\\xcf<\\xc24\\xe3<D{\\xfe<\\xa7\\x16\\xba\\xbbcM\\xad=\\\"\\x13D=\\xed\\xf2\\xcc\\xbcQ\\x90\\x82\\xbdr\\x9e\\xf0<\\x02\\xbb\\xac<\\x10\\x99\\x19;q\\x1c%\\xbd-\\x8b1=\\x02\\xeah\\xbc5e\\t\\xbc6A6;\\xb5\\xd3X=\\x04\\xed\\xf4<PoV<\\xe3\\x16\\n\\xbc\\xd6\\xee\\xb6:\\xbe\\x97\\x0b\\xbd\\xa7p*=*\\xcd\\xd2\\xbbt\\xca\\xc6\\xbc\\xc5\\xd3\\x10\\xbd_B \\xbc\\xe4\\xb1\\xae<Rj\\x16\\xbdn\\xd5\\xf7:t\\x01|<\\x91\\xf85<\\xd0w\\x89;\\xa7\\x97X\\xbc\\xca\\xd5\\xd4\\xbc4m\\x9e=\\x02C\\x04=\\xdeQK=\\xb6\\x03\\x9d\\xbb\\xb3dM=\\xe2\\x12u=\\n\\xb7\\x88\\xbbTLq\\xbc\\x03\\xf0\\xe2\\xbb\\x14\\xee==$K(\\xbc\\xe6K\\xce;:\\xe1F\\xbd\\x04\\xf1\\xd1\\xbba\\x11u\\xbd\\xa4\\x94\\xcd8\\x1f? \\xbdn(\\x04\\xbdP\\xf4\\x93\\xbco\\xfd\\xaa\\xbc\\x8b\\x85\\xb8<bg\\xa2<\\x86~\\x8b\\xbd\\xe9\\xb2A;\\xcdd\\n\\xbd\\xe2\\xff\\xed\\xbb\\xc8\\xf4\\x94\\xbc\\xafP\\x19<\\xebX)\\xbc\\r\\xf2\\xad<Y\\x94M<\\xd6%1\\xbc^\\x06\\\\\\xbdg\\x1b\\x02=\\x98\\xe2\\x05;\\x19\\x15\\xb9\\xbcA\\r\\x98\\xbc1W\\x1b\\xbd\\xfa_g<l\\x90\\xc9<\\xf6W\\xfd;\\xf74Z\\xbbH3\\xa6<A%\\x10=\\xd9\\xd6\\xbd<\\x1b\\xca\\x9c<`y\\x8c=9c\\xa0<Z\\xact<;\\x07\\xa7\\xbbH\\xa3\\xa9\\xbcA\\x9cI=\\xaa\\x8a\\x11=\\xcd\\x85\\x0e\\xbc?\\x15g\\xbd\\xac\\xfd3\\xbdk\\xe2A\\xbcT\\x9fo<\\xde\\x80\\x86<#\\xd5\\xea<M\\xc9\\xd2<2\\xed(<\\xf5\\n\\x8a<\\xf0\\xbf\\x85\\xbd\\xc3\\xcd$=i\\xd2(<\\x95\\xc7\\xf7\\xbb\\x99\\x91&\\xbdwA\\x81\\xbd\\x9f\\xc0h\\xbd\\xcf\\x00\\x9f\\xbd\\xd6!\\xcc\\xbb\\xe8\\xd0\\xc5\\xbd\\t\\xfa\\x0b=\\xff<\\x04=\\xe4\\xda\\xf5\\xbcUVp<G\\x91B\\xbd\\xf7d\\r\\xbd\\xf0\\xc6\\x99\\xbc@\\x9f\\xa7\\xbc6\\x06\\xe4\\xbc\\xc4\\x80\\xaa\\xbc\\xb6\\x84\\xfc\\xbcP\\x80\\x8a<\\xff\\x9bv\\xbd\\x12\\xa1b\\xbcb\\x1a\\xf8\\xba,\\x7f\\x94<\\xee\\xce\\xee<\\x19\\xdc\\x80<b\\xf0\\x00<\\x05\\xe5\\x94=\\x9f\\x86c\\xbd*\\xc3K=LC>\\xbd,\\xf1\\xd5;\\xbcJ\\x1b\\xbdF\\\"\\x03=w\\x8f\\x8c=\\xd7\\x7f\\x90\\xbdU\\xc2\\x1b=i\\xc7a\\xbdT#%<\\xba\\x804\\xbd\\x91\\x97-=\\xcc\\x82\\x18=w\\x14\\x15\\xbcaw+<\\x13\\xba=\\xbc\\rX\\x02\\xbd\\x00}\\xa2\\xbboh\\xf2<p`\\x02=\\x8fp\\xce\\xbcQV\\x08\\xbd\\xf0\\xd63\\xbcUMe\\xbc\\x9c\\x88l=v\\xd3\\xb8;\\xec\\xc5!\\xbcxA\\xac\\xbct\\xe9\\x84;\\xc2c\\x95\\xbc\\x10#\\x9d<d~\\x11=\\x88q\\xd6\\xbc9\\xf6\\x83=E\\x16\\xd1\\xbc~\\xf9V<\\xe3m,\\xbcu\\x179\\xbd\\x95\\xb9\\xe9;\\x15$\\xe8\\xbc8li<\\x81\\xe0j\\xbc,pw=\\x8b\\xd6\\x90<tH6<\\x9f\\xda!\\xbd\\xccq\\xb7<\\x89*b\\xbc\\xc3c_\\xbd\\x16\\xdb\\xd5\\xbc\\xf0S\\xcb;\\xc8\\x0f\\x8b\\xbc\\x174\\xee\\xbb<\\x8b\\x07=\\\"\\x97\\xad<\\xa7r\\xad<SO)<\\xfd\\xf9*:\\x7fz!=\\x1b\\x8b\\x88=>\\x87f:\\xfd\\xed\\\"\\xbd\\xc8\\xbb\\x80;\\\"\\xbb\\x91\\xbc\\xbd\\xd5\\xd7<b\\x1b\\x80=\\xd5\\x96\\x04\\xbd6\\xc8?<\\x9bq\\xb4<[\\xf7\\xa1\\xbd\\xf0{\\x90<\\x00/K\\xbdv]\\x11=\\xf6\\x9d\\xd0<\\xad\\x88\\xd9\\xbb\\xb8\\xbc\\x07;\\xce\\\"h<\\xf7\\xc4\\xcd\\xbckwX<\\x81\\xb9#<\\xdf\\xc1\\xaa;\\xd4\\x10\\xdd<\\x0b\\xdd\\x14:\\t\\xd5\\x92\\xbc\\x0b*\\x12=\\xf9\\x9f\\x96\\xbb\\xf6\\x7f\\xc6<\\x82\\x9c\\r=yD\\x17\\xbc\\xc4\\xd9\\x91\\xbc\\xb7Z)=\\x05e\\xfc\\xba\\xcbc1=\\x03\\xa7l=x\\xc0\\xa7= Y\\x9a\\xbd\\xcb\\x13R=\\xa2\\rI<\\xbb\\xb8Z\\xbd\\xea#\\x88;\\x0c\\x9a\\xe0<f\\n\\xf4\\xbd\\x7f\\xd5\\x02=\\xbb\\xff\\xcf\\xbcME{<-\\xfc\\xf3\\xbc\\x8c\\xf9\\x8f;Z\\\\\\x1d\\xbc\\x9d\\xd8\\xab\\xbc\\xdd\\x95\\x81\\xbd]\\x17\\xa7\\xbc\\xfa\\x18\\xfe\\xbc\\x86\\xd7j=/\\x04>\\xbb\\x03\\xb3^=\\xc4\\xbcv<\\xaa\\xdc\\x80\\xbc\\xadg:\\xbc+B\\x15\\xbd+\\xe5t<\\xde\\x90:=\\xdc\\x88\\x14<^\\xfc/\\xbd\\xbd\\xf5\\x8b<\\x04\\xc3\\xc5<\\x89\\xc8\\x86\\xbb\\xcbMc\\xbd L\\xad;\\x15\\xa2I<8\\xaf\\x1b=\\xefSd<\\x10\\\\\\xcf\\xbb\\x0f\\x0c\\xba\\xbbZ\\x85.\\xbd\\x9a\\xb8\\xca\\xbc\\xf0\\xd4\\xab\\xbd8\\xe8\\x87=\\xa6W\\xcd\\xbc\\xbajX\\xbdN\\xb7\\xa1\\xbc\\xf3\\x1eP\\xbd\\xfd\\xdal\\xbb\\xe3\\x93\\xfd\\xbc\\xd1\\xf8`\\xbc\\x00\\xfc\\x82\\xbc\\xe3\\xca+\\xbd\\nv\\xad\\xbc\\x99\\x11E=\\x81\\x9f\\xbc<\\xe3\\xb8\\xf9<\\xfe\\xbe\\x07\\xbcWw\\xb0\\xbb\\x07>3<\\x9f)\\x8b\\xbb\\xb8W\\xc3<\\xaeE\\xe9\\xbc\\x8cl\\x89\\xbc6qi\\xbd\\xafAS\\xbcB\\xa9=<\\x92\\xe3\\x05\\xbc\\xd4Q+<w\\xbc!\\xbc\\x8c|q\\xbc\\x18\\x8d\\xb4=\\x99\\x8d\\xdf\\xbb\\x98S\\xb8<\\x1e\\xdc\\xfc\\xbc\\xae\\xe4\\xb2<k\\x07\\xd8\\xbbqQ\\x0b<\\xb5\\x8e\\xe0<\\xa87C<\\r1\\xe1=\\xdd\\xc6s\\xbdI\\xe7\\x90=WK\\xfd:\\x97x9\\xbdo\\x91\\x01\\xbds\\x02\\x13\\xbd\\x8bx\\xb4<r\\x9cU\\xbcH\\xa9n=\\xf3f+\\xbb\\xfe\\xf58<\\x05\\x1e\\xc0<\\xb0\\xa8\\xbf<\\xb2V\\xc5\\xbc\\xd3d\\x89<\\xdb\\xaf\\xb5=`\\x0c\\xb0;\\x99\\xd9\\\\\\xbd\\x82K\\x87={\\xc8\\x85=\\xfa\\xe5\\x85<\\x0b\\xdaU=\\xca\\x02\\x0b\\xbc\\xcd\\xa0\\x9c\\xbd\\x12n\\n\\xbc_g\\xbe=\\x83i\\x13\\xbdVV\\x14\\xbcg0<\\xbd\\xffT\\x9f<,\\xa8R\\xbc0X\\x89<\\xe2\\x187\\xbc\\xbepJ=\\xd8{7=/<S=\\xa0\\x06[=f\\x0bT\\xbd\\xd0\\x9d[\\xbd\\xedK\\x1b\\xbd\\xdd\\xf72=V\\x05\\x11=\\xdft\\x98\\xbb\\xf7I\\xd9<\\xe97\\xb2<\\x84H\\xa4<\\xd9\\x15.=\\xfec\\xce\\xbc\\xb9 \\xa1<\\x96h\\x98\\xba.\\xc0(\\xbcg\\x82#\\xbd\\xdf\\r\\x8b=\\xd6\\xc6\\xcd\\xbb\\x0f \\xa4\\xbc\\x8b\\xe4\\x8a<\\x1a\\xeb\\x8b;u\\xec\\xcd\\xbc\\xca~u<e\\xe5\\xd1\\xbc|Dh<j\\x15\\xad<+_b\\xbc\\xc9\\x8e\\x95\\xbc\\xce\\x8c\\x82=#C{=n\\x1a\\x90\\xbc\\x9c\\x95\\xe7<N\\xc0#\\xbdZ\\x9dQ\\xbc9{\\xc5<\\x1c)C=\\xcc\\xe2\\xa5\\xbd\\x83\\xee\\x99\\xbd\\x96ad;\\x1b\\xd8n=\\x90\\xd3\\xbf<zc>=\\xe7\\xf6\\x92\\xbdJ\\xc2\\xde<\\xad\\xdb\\x92<K+\\x04\\xbd\\xef\\x92\\xad=\\xd7\\x9e\\x0c=\\\"\\xb8X\\xbc7\\r==\\xd4Ol=]\\r\\x07=\\x8f1E;\\xb2@\\x06\\xbdH\\x8a\\xe7\\xbc/\\xc5\\x80\\xbdg.\\x10<\\xb1\\xd0(\\xbdW\\x8ao\\xbd\\xcc\\x9f\\t<\\t\\\"\\x1f=\\x10~\\x84=\\xa3J\\xc5\\xbc\\xc2\\xb4|\\xbd\\x06\\x14w=;\\xb7\\x83\\xbc\\x06\\xc5\\x86\\xbd\\\\{\\x80:7pN\\xbc\\x94\\xe3$\\xbc\\xf6I\\x90\\xbdv\\xd9v\\xbd\\x9a\\x92\\x8a=\\x82w;=\\x8akI<j\\xe2\\x87\\xbcYz\\x16=D\\x11?\\xbd\\xa1`\\xba\\xbb\\xf4\\xf0\\x91\\xbb\\xd9R\\xd2\\xbc\\x8e\\xf2P<\\xbd9\\xfb<\\x02{6=\\xc72\\x0e\\xbc\\xd6\\xdaC\\xbd&\\x89\\x92=\\xdd e\\xbcA\\xebF\\xbd\\xb5\\x9a\\xb6\\xbc?\\xd3\\xb2\\xbd\\xd4!\\x18=\\xc8\\x15,<\\xf4l\\x9a<\\x07&\\x00=Qn\\x0c=/.O<\\xde\\xa0\\xf7=o\\xf0\\xa0<\\xc2\\xe1\\x1c=@3\\x0f\\xbd\\xcc\\x82\\xb6\\xbc*\\x1e\\n=\\xa6(\\xf7\\xbb\\xdd\\x07\\x01\\xbdj\\xc4;\\xbd\\xc8\\xa7\\x87=3_5<*\\x19\\xc8<\\xed\\xbbL\\xbdv\\xcf\\x89;6>\\xf7\\xbcpI-=]\\x99\\\"\\xbd3@(<~\\x1d>=\\xa5\\xd7\\xc6\\xbcm\\xb3\\x01=Z2\\xc9\\xbb\\xb0N4=\\xa1\\xe6\\x17=T\\xd8\\xec\\xbc\\x8d\\xf2\\x98\\xbde\\x08\\\"\\xbd\\xdb\\xc1\\x7f<\\n\\xf3v<7\\xfe\\xb0<\\xcf\\x15c\\xbd\\x85\\xf2\\xcb\\xbb\\x1b\\x96Y\\xbc\\xacM\\x10\\xbc\\x96\\x88\\x8a\\t\\x19\\xee\\x1c\\xbdn\\xdb\\xc2=\\xfe\\x9b\\xdd\\xbck\\xe2\\xe8<,A\\\"=[\\xbf%=Q\\x12\\xa6<\\x12!d\\xbd\\x19\\x14l:\\x98\\xbf\\x1b\\xbcQ\\x97\\x81<\\x06m\\r=\\xb8\\x0f\\xb9;\\xe7t\\xc4\\xbcLM\\r\\xbd\\x17p\\x1d=\\x88\\x91+<:0:\\xbcr\\xe7\\xc0\\xbc\\xad\\xb9i=6\\x1b\\x9b<7\\xbc\\xbb9b\\xbe\\x1c\\xbc\\xf9\\xfb\\x90:QH\\xfc\\xbb\\xd9\\xce\\x89\\xbch[\\xf4\\xbb\\xb7\\x1e(<\\xde\\xca!\\xbb\\x93J\\xae\\xbc\\xf2\\x9a+=\\xe0z\\x90<\\xe0\\x991\\xbb=\\xc1r\\xbd\\x7f\\xca\\xc1;\\xdf\\x942<N\\xed\\xf8\\xbc\\x1d\\x01\\x8f;\\x94_v\\xbc\\x81\\xca\\xfc\\xbbH\\x07T<\\x93&\\x06\\xbd\\x98J\\xb3<\\xe1q\\n\\xbc\\xf3\\x83o\\xbc!\\xe4\\xf1\\xbc{\\xe9\\xb7=\\xd2\\xee\\xd5\\xbc\\xbb\\xd2x<\\xc11\\xd4<\\xa0\\xc0\\x86\\xbdd\\x85\\xaf\\xbb\\x03\\x9f\\x93<\\xed\\xf1\\xee\\xbb\\xe4{\\t\\xbdcd\\x84=\\x0fP\\x84<*\\xea\\xab<\\xa8\\xe5\\xf6<\\xf0=\\xf7<\\r\\xda\\x07<\\xb7\\xe4$\\xbb\\x01 :\\xbdY\\x1c\\xb9<:(\\\"<\\x9a\\xc6\\x8a\\xb8\\xe8v\\xcf<.\\x00\\x0f\\xbd\\xd1\\xb2\\\\<J\\x12\\x01<\\xca\\xf5\\xbf\\xbc\\x06,\\x04\\xbdtp\\xd0\\xbc\\x97\\xfb\\xd8<~\\xafu\\xbc\\x85f1\\xbc\\x94\\xe8\\x85\\xbaV\\xb6\\xd1<o\\x90\\n\\xbd\\xb6s.=\\x07\\xa5\\x10\\xbd\\xa8}\\x0f<2P\\x18\\xbd\\x081\\xe6<K\\x9b\\xac\\xbcAr\\x1d=~\\xa82;<UQ\\xbc\\xd0l\\x04;\\xbf84<]\\xa2\\x82<\\\"\\xf2==\\xad@\\x88=z\\xfd\\xf5:\\xfd\\xd6Y\\xbb\\x1a\\xfb\\x85\\xbc$\\x93h\\xbd\\xc3\\xc8\\x0e=Dy\\x14;\\x19R\\x1c=\\\"\\n<\\xbcY|\\x1e\\xbd\\x12\\x86\\x81<\\x03\\xc9\\x80\\xbc\\x8e\\n\\x0f<\\t\\x95,\\xbdT\\xf8m\\xbdI\\x1d\\xa9\\xbd\\x8c\\xf3i\\xbd3$\\xa3\\xbd\\xdb+\\xf9:\\xde@%\\xbc\\xb5\\xdbM\\xbdam<=<\\x90\\xd6\\xbbf\\xb0\\x97;\\x83\\xfd\\x1f\\xbb\\x9fak\\xbc\\r\\xb2+=\\x88\\xb8(\\xbd\\xdf\\xca0=\\x01\\xcc\\x94\\xbc\\xebe\\x07<\\x84\\x17\\xf2\\xbc\\x86\\xd1\\x92;\\xdd\\xb6\\r\\xbd\\xde\\xa7F\\xbd\\xc8z\\xce\\xbc\\x19~\\xcb<\\\"\\xb2]\\xbd\\x7f{$\\xbd[/\\x92\\xbd3\\xe5O=\\x07\\x8c\\xa4<\\x06\\x0c\\xcc<\\xaa\\x10\\xa9<\\x80\\xdc\\xd8<\\xafb\\xf1\\xbc6\\xb8e<\\xaa\\x99/\\xbd\\x03\\xd26<k\\x17\\xc9<\\xe4A\\x8a<\\xcf\\xdbf;n\\xc2\\x01\\xbd\\x85\\x0cR=s\\xa1\\xd7<\\xb7x\\xe7\\xb9\\xea\\xf8O=xqY\\xbc\\xa5\\x8b7\\xbd\\xef:e<\\xc2e\\xb8\\xbc\\xb3\\x0b\\xba<\\xc3>\\xc5<\\xe6\\xed\\xaf9\\xf75\\xcb\\xbbi\\x92\\x9b<\\x0b%\\x12<\\x19Sn\\xbb8\\xbe\\xba;\\xce&s=x\\xae\\x8f\\xbck\\xe0\\x99\\xbc\\xd3\\xb3*\\xbd\\xf0\\xca:\\xbd\\x85\\x13p\\xbc\\xb9\\xb6\\xb5=+\\xde\\x9d=\\x0f:G\\xbb\\x98\\xc2\\xb1<p\\xe1\\xd5;\\xa5\\xc7z<\\xbf\\xf9|<\\x17w\\xce<\\x87Ny;\\xc0\\xe8\\xd6\\xbcj\\n\\xee<:Xw; \\xc4P<0\\x91,\\xbc\\xd2\\x18R\\xbd\\xa7\\xefh\\xbd\\x10\\x94\\\"\\xbd\\x88\\xcf<\\xbd\\xe7a\\\"\\xbc\\x93HV<`\\xb1\\xaf;\\xf9\\xc1\\n<\\xe2\\x9f\\xd3\\xbb\\xcc\\x8d\\x1e=\\x9f\\t\\xbb\\xbc2\\xb61\\xbd\\x1f\\\"\\xe1\\xbb\\xc4\\xfd+\\xbd\\xda\\xa5o\\xbdF\\x14\\x89\\xbd\\x9bP\\xb7<N\\xff\\xbd\\xbaT\\x91\\xb1\\xbd\\x83\\x13\\x1d\\xbc\\xcet\\xda\\xbb7\\x98\\x10\\xbct\\x9e\\x8f\\xbd\\x9e\\x80\\xf5<[r\\xaa\\xbcH\\xb9\\x84<\\\"0%\\xbdh+\\x95;\\t\\x1fp\\xbd\\x83\\x1c*<\\x8f>\\x8f<3\\xa7)\\xbd\\xf6q?\\xbc\\x85\\x92`\\xbd\\x044\\xc9;EM\\x03\\xbd?\\x94\\x02:7y\\xcd<ZS!\\xbdC\\xf0\\t=\\xb0\\xe2\\xbe\\xbd#\\x90E=\\\"\\x91\\xe6\\xbc\\x0c\\x86\\xa4=u\\xc6\\xd5<\\x92\\xa0\\xfe<\\x9d\\xaa\\x89\\xbd^\\x15X\\xbd\\xa3Vz=\\xc8\\xf3^<\\xce\\x8c\\x16\\xbc\\xf2+\\xb9\\xbd\\xa5K\\x12=\\nN\\x06\\xbd\\xe0\\x93Z\\xbd\\x03\\x94\\x8a<\\xba\\xf2\\xfd\\xbb\\xacw\\xfc<=2\\x98;\\xfe\\x89\\xb0\\xbc\\xdd\\xdc\\x87<\\xe2\\xf0\\xa4\\xba\\x0c\\x1d\\xb9\\xbb~#\\x87<\\xceZz;\\xae\\x111=\\xf9\\xdb4=g\\x11<\\xbdz\\xfd\\x04<+\\xcd\\xfa<\\xf309<\\xc70\\xc7<[\\xb0\\x8b=\\x88\\x05>\\xbd\\x03\\xad\\x9f=%\\xe6\\x0c<g]u<\\xf6K/\\xbc\\xe7\\xd7\\xba\\xbd&\\xc5\\x8d;\\xcc\\xda\\x1f\\xbdX,2<\\xa5\\x0c;=wY\\xa8\\xbb\\xec\\x9f\\xd6\\xbcu\\x8b\\xa9<A\\xb4\\xa6<:c\\xab:L\\x85\\x18=\\x07\\x02r\\xbd\\x0ba1\\xbc\\xaf\\xb6\\xd1\\xbc\" \nHSET bikes:20008  model 'Soothe Electric bike' brand 'Peaknetic' price 1950 type 'eBikes' material 'alloy' weight 14.7 description 'The Soothe is an everyday electric bike, from the makers of Exercycle  bikes, that conveys style while you get around the city. The Soothe lives up to its name by keeping your posture upright and relaxed for the ride ahead, keeping those aches and pains from riding at bay. It includes a low-step frame , our memory foam seat, bump-resistant shocks and conveniently placed thumb throttle. ' description_embeddings \"\\x83\\xbb\\xe3<\\xee\\xb2\\x9f<q\\xc7\\x9d\\xbc\\x80\\x01C<{sl<\\xba\\x1f$=]\\\"(<\\x0cF\\xdb\\xbc\\xbd%\\xa6\\xbcd\\xabX<\\x82\\x0eB<|s\\xf1\\xbc\\xb4X\\xff<\\xcd\\xb7\\x00=V]\\x82=\\t\\xe4\\xd8\\xbb;\\xbc\\x80=Gc*\\xbd\\xca\\xf7\\xd2<#\\x16\\x9d\\xbc\\xe8\\x02\\xf9<S\\xd7G\\xbcG5\\xd2<\\x92\\xca-\\xbd\\x9a\\xb3\\x0f\\xbb\\x94 \\x06=\\x89\\xd2<=\\xe4Hw\\xbdG\\xc7g\\xbc\\xa3\\xc1\\xb8<\\xfb\\x99\\xf6<\\xc1\\x8d\\x0e\\xbc\\x87|\\x96<\\x11\\xe1[=H\\xa4\\\"=\\x04\\xef\\x84\\xbd\\x12\\xdeW=\\xa7\\x19\\xb7\\xbc\\xe4h\\x8c=\\xca2)\\xbd\\x81\\x8a\\x89\\xbc\\xa4U\\x8a<\\x10R|\\xbd0\\xae\\x9c\\xbb\\x8e\\xc7;;\\xccb\\x7f\\xbdn4\\xaa<m\\xf4\\xdb=\\n\\\"I\\xbc\\xa5\\xde\\xdd<L\\x1d\\xd4\\xbb\\xe0D\\x82\\xbdR}\\xb8\\xbbXV\\xa0<\\xf3u]\\xbc\\xd7<\\x99\\xbcM\\xd44\\xbb\\xc5(\\x0c\\xbd\\t\\xd8\\x93:\\xc3\\xfb\\x8e=\\xa6\\x8f\\x92\\xbc\\x89\\x84\\xb3<\\x06\\xa2\\x81<\\xce\\x08\\xf4\\xbc\\x9d\\xb92\\xbc\\nI\\xea\\xbc\\xa5T$\\xbci\\x9e\\xaa<]\\r\\xee<\\xb4Q\\xf8\\xbc\\x0b\\xa2+\\xbd>\\xd7*\\xbd\\x11_X\\xbd3ly<\\x8e$\\xdb\\xbc\\xb4\\x91m\\xbd\\xe0\\x00\\x7f\\xbd\\x9e\\xbep\\xbc\\xb98\\x88\\xbd6W\\xa2<0\\x9a\\xa5:\\xc2\\xe7\\x81\\xbd\\xc6\\x87\\xa0\\xbd\\x16\\xfd\\xe9\\xbbm\\xde\\\"=\\x01V\\xaf\\xbc\\x83\\x9c\\xbc\\xba\\x97#+\\xbcef\\x9d\\xbc8\\x02G=\\x98\\x1d\\x14=b2$\\xbdW\\xf2*=\\xbfaz=N\\xdf\\\\\\xbb1\\xf9\\xf1;\\xd4C\\xe7\\xbc\\xd3*\\x1a\\xbd\\t\\x1d\\x92\\xbc?\\xcc\\x05\\xbd\\r\\x93G\\xbd\\xab\\x9e\\x0e=\\xc8\\xfb\\xd1<\\nU\\x02\\xbdU\\xba\\x0e\\xbb\\xb7)\\x9f=!v\\x16<Q\\xb0\\x83\\xbc0\\x8eF\\xbc\\xd8\\xbe\\x87<\\xb3\\x8fr\\xbd8Q\\x07<\\xc1\\x0eF=\\xb2\\xc8\\xa1\\xbc\\xda\\x19\\x90\\xbc\\x7f\\xab@=\\x06\\x89G=/\\xb8)=\\x81\\xa55=\\xf0~\\xcb\\xbd<kD:D\\xe4\\x87\\xbc\\xc5\\xc0|\\xbd\\x9as(=\\xff{+<\\x902\\x03=S\\x0c\\t=y\\xf6m\\xbdhN\\xda\\xbc\\x12\\x86\\xb0\\xbc\\x80\\xc1\\x99\\xbb\\x0f\\xf0h\\xbd\\x0e*\\\\=y\\x8d*\\xbd(\\x16g\\xbd\\xe7U\\xea<\\x90\\x8c;\\xbdw\\x94\\xbc\\xbc:L\\xde:\\xe3\\\\5\\xbd\\x15K6\\xbd\\xfdS*\\xbc \\xd1\\xe5;\\x82\\xd4\\x8a<\\x06\\xdcp=g\\x86\\xbd<1\\x07D\\xbd\\xb2\\x90\\xc2;\\x1d\\x99\\xce;\\x0bd><+\\x1b\\xcd\\xbcG\\xf1\\xdc;\\x88u\\x91\\xbc\\\\fG=\\xaa\\xc6\\xd0\\xbc\\x92i\\xbb\\xbb\\xea\\x002=\\xe2\\xfdz<\\xe14\\x86=\\xc6\\xa9*\\xbc\\x93V5<\\xae7&\\xbd\\xd7\\xd5\\xb4;\\xad\\xe0\\xd3\\xbc@\\t4<zu\\x89<\\x06\\xa2V\\xbd\\x96\\xc0;=$X[=nJe=\\xc5\\xf5\\xa2\\xbc\\xf4\\x1f\\x1a=\\x81\\x06}\\xbb\\x19\\x19\\\"\\xbd\\x80\\xe3\\xe5\\xbceiG=<\\x1c\\xfc\\xbc>\\x14@<\\x93\\x93\\xf2\\xba\\\" \\xd19\\x9e\\xcfG\\xbd\\x97\\xa5\\xcf<\\xa4\\xdb\\x87=jQ\\xcd;\\x83j\\xec<\\xbcn2=D\\xbaE=\\xd7|];\\x06\\x07\\xce\\xbc\\xdd5E=\\xc1\\xa9d<\\xaf\\xb1\\x06\\xbd\\xeb\\xbf\\x89\\xbdH\\xe6\\x1d<>\\xd2\\x85\\xbcl\\xf3\\xb7=n\\x02\\x19\\xbcD.\\xfb\\xbb\\xad\\x18\\xa9;*\\xb9\\xfe\\xbc\\x8c\\x13-\\xbd\\x16\\xbd\\xc9\\xbcub\\xc2\\xbd\\xed\\x10\\x1d\\xbc\\xf9\\x19\\xe8<Ld\\\\=b\\xc1\\x9b\\xbc\\xe6$(=\\xd8B\\xc4<\\xb1\\xf5\\x8d\\xba\\xd1Rm<0LG\\xbcF\\\"*=\\xb4\\x99<\\xbc8\\xbc\\xad\\xbd\\xed\\x1a\\xda<\\xc6\\xb3\\xc5;#\\xa3}:O\\xc9{\\xbd>\\xed\\xd9\\xbc\\xdeS[;,\\x1d8\\xbdj\\xd9(\\xbdF\\x1b\\x8e<\\xda\\\"m\\xbdg\\xc7\\x9f\\xbd\\xc4Z\\x90=\\x180\\x05;\\x12T>9\\xcd\\xece\\xbdt\\xd25<\\xc9\\x1f.\\xbcy\\x81\\x81=\\xd8Do<7\\x0b\\xd6;3 \\xa2<\\xfd\\xe9\\xcb\\xbcz\\x94==e\\xbf\\x19\\xbd\\xbd\\x14\\x92;\\xbc\\xa5\\xcc\\xbc\\xf3y`\\xbc^$2<\\x1c\\xd3\\r<<\\xa6\\xb9<\\x08O|<Z\\xa5\\x1d;\\xd3\\x8a\\x89=\\xc64\\xc0<\\x1b\\xba\\t\\xb9\\xb5\\x87\\x98;Am\\x14\\xbd\\x02\\x1a\\x81\\xbc\\x90\\xc0\\x7f<(\\x90e\\xbd1&d=\\xc1\\xd8\\x1a\\xbd\\\"\\xfe\\x7f<r\\xd6\\x14<\\xa3\\xaf\\xe0<\\xd1\\xe8k\\xbb\\\"\\xbbJ\\xbd\\xc9\\xa4\\x18\\xbdT\\x88\\x7f\\xbb4\\xfb\\xbe;\\xa0\\xd3\\x81<>\\xae\\x19\\xbc\\xb5?\\\\<t\\\"\\xe7<\\n\\xa0D\\xbd\\x88u\\x15<\\x07i\\x04<4\\x0bn\\xbc\\xb3vC\\xbb\\x1a\\xdd\\t\\xbc3\\xedg\\xbcN\\x17\\xd5\\xbc\\xe7\\x8d\\xc5\\xbc\\x159\\x1d=\\xce\\xc4%\\xbc7?t\\xbd\\r.E=\\xa65N=\\x13d{\\xbd\\xbd_5\\xbd\\x82\\xd73=\\xaa!\\xde\\xbc@\\xc5\\x9b=b\\xe2\\x86\\xbd.0\\xbc<\\xb8T6\\xbb\\x08\\x00\\x05\\xbdA\\xe7\\xc9<L5\\x97\\xbc\\xf7qd\\xbc\\xf8\\x04\\x16\\xbd\\xa7ch\\xba\\xb6\\x1b\\x80\\xbd\\x15O\\t\\xbb0\\xa7#\\xbd./!<\\x18\\xca1\\xba\\xcf\\xd9V=\\xd5\\x1fK=FM\\x9a\\xbc.\\x88\\\"<;5\\x98\\xbdj\\x14y=\\xba\\x92R=\\x17\\x1e\\x9d=\\xa8-H\\xbd\\xc9S\\xb6\\xbc1\\xa5\\xb0\\xbcYt\\n=\\xf4\\\\\\xce\\xbc\\xfc\\xa5^\\xba\\x99\\x8d\\\\\\xbd\\x9e1\\x05=\\xa0\\x88\\xab\\xbc<\\x92\\x13\\xbd:u\\x99\\xbc7\\\"\\xf5\\xbc\\xec0\\xd4\\xbc\\xb4e\\xfc\\xbc\\xe5\\r\\x81==c\\x87\\xbdF\\x17P<\\xb1I\\xd0\\xbd\\x14|\\xe2<Ic\\x90=V\\xed\\xfa\\xbd\\xef\\xea\\x9f\\xbb/\\x06<\\xbd\\xdfd/=a\\xdb\\r=\\xc1&3=\\x17\\xdc\\x0c\\xbb[M\\xaf\\xbc\\x9a\\xb9\\xe9<\\x00\\x1bk=\\xac0m\\xbd\\x10\\x7f\\x01\\xbcc\\x11\\x87=\\xeb\\xd0^\\xbdX\\xbe\\x16<W\\x9f\\xc3=#k\\x8e\\xbbF\\x9b\\xec\\xbb\\xaf\\xc8X=H<\\x8d\\xbc\\xa5\\xc3\\xd0\\xbc\\x06\\x95\\xbd\\xbc\\xd6\\x9f%=Sqg\\xbd?\\xca\\x19\\xbd\\x8b?\\x87=\\xd2\\xb6C<\\xf5NF<\\xc9\\xa9\\x87\\xbc\\xef\\xb8\\x04\\xbbT\\x86-=\\x15\\xd6\\x83=\\x1f\\xbe4=\\x06\\x1b\\xca\\xbb\\xbb\\x8c\\x8b<I\\x1d3\\xbd\\x0f\\xaf\\xb4\\xbcP\\xeb\\x08=\\x82pe\\xbd\\x9b\\xc6\\xcd\\xbc\\x1a\\xa2M;[A8=\\x1e\\x80\\xd2<bj\\xaf;\\x08^?=\\x9b\\xac\\xdd\\xbad\\x85\\xef\\xbc\\xbd[\\x9b:\\x93\\xcb\\xfe;/Y|\\xbc\\x97\\x88\\xd3\\xba\\x12\\xb6i\\xbc\\xb5\\xe3\\xf5<\\x14\\xfeD\\xbd\\xec\\xdd\\xf3:\\xa2\\xd0\\xac\\xbbU!!\\xbcm4\\xa1=\\x9d\\xc8\\x04=\\xe2E\\xdd\\xbcL,\\x9e\\xbc\\xfa\\xbf\\x8a<\\x99#\\xbc<\\x01F:=\\x93\\x9aS\\xbd\\xf3\\x94\\x19\\xbb\\x06\\xb0}\\xbdV$\\x8a=\\x1a\\\\\\x89<2\\x18\\x04\\xbddp\\xe7\\xbcx\\\\\\x8f\\xbc\\xb0x\\xd6<\\xd6\\xe1D\\xbd:\\xe7\\xfe;}%\\x94\\xbcQxJ<\\x1c\\xd3\\xe1\\xbc\\x1b,\\x06\\xbbm\\xcc$\\xbb\\n\\x9b%=\\xc2\\xf4\\x08\\xbd\\x82`\\xdc<\\x8e}:=:3v\\xbd\\xa3\\xb3\\xb0\\xbb\\xd2\\xa7Z\\xbd\\xf6\\xd3\\x8e\\xbc:\\xeb\\xf1<\\x02\\xabf<\\xfc\\x8a\\xea\\xbc\\xfc\\xef\\xb9\\xbcwd&<\\x0b}n;|\\xdc>=\\xfa8\\x1b\\xbce\\xa6\\xdf\\xbc#\\x01\\xdf:\\xc62\\xe0<\\xeb7i\\xbd\\x96\\xa1k\\xbb&i\\x1b=\\x03\\xfd#8+\\x02w\\xbc%\\xad\\x05\\xbd\\xe5\\xe6\\xcd=\\xc4C\\x87\\xbc\\x11\\xda\\xdc<\\x08\\xe9\\x8d\\xbc\\xe4\\x87x\\xbdRk\\xef\\xbcv?w\\xbcz;t\\xbb&\\x1c\\xa3;\\\"\\x1e\\xa6\\xbc\\xc9v\\x8b<z\\xf6\\x86<\\xab\\xb4A\\xbb-:*\\xbdm\\xa1\\x1e=\\x03z\\xc6;o\\xbb\\xf6\\xbc\\xa4\\xd1\\x13\\xbddP\\x1d\\xbc>\\xb1\\\"=APj\\xbc\\xc2\\xcb\\x18=\\xfdp\\x83;W\\xf9\\xd4<\\xf7Q\\xd4<\\\\B\\xa5=\\x1a\\xde\\xff<W\\x966\\xbb\\xbd\\xd7\\xdc\\xbc\\xf8X\\x82\\xbdQ~D=>\\xd7\\x08<&\\x1f.\\xb9Xc9=CB\\xf2<\\xb1n\\x14\\xbd\\xfdh\\x0f=\\xe7nQ\\xbdB\\x0b\\x14\\xbd\\xc50><3\\xcey<\\x02q=\\xb9g\\x13M\\xbb\\xac\\xea1\\xbb\\xed\\x86%\\xbc\\x99*\\xd1<\\xdf\\\"\\x03\\xbd\\xf8\\x13\\r<\\x15\\x17\\xa1;!\\x8b\\xef\\xbc\\x1b\\xcf)\\xbd\\x0b\\xc5V\\xbd\\xa8:4=\\x7f)h:\\xc5\\xceM<k(*\\xbdX\\xab\\xf5\\xbas\\xe2\\xeb\\xbc\\x84\\xf6\\xe5\\xbb\\xcb#\\x87\\t\\xbdl\\x04=\\xbfG\\x14=\\xdbx\\x95\\xbcn\\x13\\xb6;\\x8a\\x9dy<\\xf9Lz\\xbb4\\xbd\\x88\\xbc@<I=,#\\xfb<\\x8b,\\x02\\xbd@\\xc3\\xc9\\xbc\\xef\\xbb\\xb8\\xbc\\x06\\xfa0\\xbcgq\\x1f=\\x10\\xffa;}\\x89\\xe9\\xbbDu\\x86<\\x12\\x13\\xb0\\xbckv\\xfb\\xbb1\\x04\\xf4\\xbb`\\x9b{<\\xc2\\xdd\\x12\\xbd\\xa5\\x1f\\xa0<\\x16F\\xe7;\\x15\\x8e\\xcc\\xbd\\x83t\\x05<7CB;j\\xfb\\xac:\\\\\\x00\\xb6;\\x81\\x04W\\xb7\\xde\\n\\xc6\\xbb\\xe3b\\x8b<$\\xfd\\\\<O\\xc8\\xa8\\xbc\\xc4\\xc27\\xbd\\x8e\\xdc\\xa8\\xbc\\xfc~\\x05=I4\\xb2<}q\\xea;ok\\xc9\\xbbyZ8\\xbd \\x15U\\xbd\\r5\\xc6\\xbc\\x84\\xe8\\xc6=-\\xe4\\x0c=Q{8;\\xa7\\xa3\\xb2=<\\xcc\\x19\\xbdA<z\\xbdnx\\xf0\\xbc\\xa0\\n\\xc8\\xbcn\\x9b\\x8a\\xbd\\x01\\x1c\\n\\xbc\\xf65\\xf7<\\xa8\\t\\x10\\xbcW^\\x1f=\\x98m\\n\\xbd\\xe7\\xd7\\x19<\\x8eG\\x98\\xbc\\x01\\xb4N;\\xfc\\xae\\x12<\\x07\\xe2Z=\\x13\\xef\\x0c=~\\xa2t\\xbcn\\xff\\xf1\\xbd\\xca\\x90\\xb5\\xbc\\xe3\\x8a\\xc7=\\xc4\\xf8\\x1e\\xbd$BN=\\xec\\x01\\x91\\xbd\\xed\\xa2\\x03=\\x0bV\\xad\\xbck`\\x81<-\\x97)=JT\\xfb<D4d<\\xa7\\x03\\x9b\\xbc#\\x99Y=\\xdbh\\x1c=\\xc8\\xc5\\x9c<\\x86\\xa2R\\xbd@\\xbe\\xff\\xbbq\\xadJ\\xbc\\x17\\xe4\\x84=\\xef\\t\\x1e<\\x97\\xa7+=\\xb4q\\x9b\\xbcT\\x80\\xb5\\xbc\\t~=\\xbd\\xa8\\xf0&<FJV\\xbcUj7\\xbd=m(:\\x85\\x14.;\\xfd\\xad\\r=m\\xba\\x14<GS|\\xbds\\xf9\\xb7\\xbc`\\xad=<\\xff\\xfd\\xae<&\\xea\\x05<\\t\\xb9\\xbf<\\xb1\\x03D=$zr<P\\xf0\\xa5\\xbc*Q\\x07<SO3\\xbc\\x95\\xdb,\\xbd\\\"GC\\xbd\\xf5\\xb4\\xa7\\xbd\\xc2TW<\\x80\\xb8\\xa0\\xba@<a\\xbd\\xc7* =\\\"\\xdd\\xd5\\xbct\\xcc\\x84\\xbb&Q\\x13\\xbd\\x18`\\x1c\\xbd\\xbf^d<2\\xec\\xe5\\xbc\\x1bv\\n\\xbagl =\\x88\\xfaF;\\x1a>T;\\xe9`\\xab\\xbb\\x82\\xc0\\xec\\xbb,\\x9c\\xa2\\xbb\\x9eC$\\xbc\\x84s&=\\xab\\xda\\xc1\\xbd\\xda\\x83\\xd2\\xbb\\xcd11\\xbd~5\\x90;\\xf4+\\x03=\\xf7\\xb9\\xf2<\\xa5d\\xce;\\x0b\\xe1\\x18=\\xdcm\\x10\\xbd\\xcd]]\\xbb\\xe3\\xf5+=\\xd5^\\x01\\xbcL\\xb9\\x1c<11\\xa4\\xbc>\\xaeK<\\x1d(\\x1c\\xbdnU\\xf0<\\x9eLl=>\\x95\\xbd;\\x88\\xc6\\xc7<\\xfe\\x9f\\xe4<\\x82o`\\xbc\\x95%0=\\x92D=\\xbd\\xd2<\\x94<\\xab\\xc2\\x1f\\xbd\\xfcc)\\xbdP\\xe3\\xc4;\\x9f^P\\xbc\\x98~\\n\\xbc\\x96\\xd4;=\\x92\\xdba\\xbd&:\\x95=6Gc<\\xf4yu<[\\x1e\\xb5<+\\xeb\\xb9\\xbb\\x98\\x1d\\xf3\\xbcdG5\\xbd\\x1e\\x8b\\x81=2\\xf6o=\\xacT\\x8b\\xbb\\t\\xa5\\xdd<I,\\x85\\xbczH\\x97\\xbc(\\xac\\r=k\\xb1\\xda\\xbb\\xa2\\xdeN\\xbb\\xfdsX=\\x87\\x03\\xca;\\xcaXo\\xbaB%\\xaf\\xbc\\xed!^\\xbdZ\\x03u=F0\\x03;\\xc0j\\xe6\\xb9/\\x116;N\\x86\\xf7\\xbb\\xac\\x88R\\xbdP\\xd8\\x04=\\xc9\\xa0\\xd7<K\\xc1\\x92<\\x87\\xaai\\xbc\\xc7>\\xda<m\\x1d1\\xbd\\x03\\x04\\x85\\xbdK[\\x82\\xbd\\xa9\\xed\\x9c\\xbc5mY\\xbc\\x03\\xf6\\x0c<\\xbd\\x17N=m\\xbf\\x19\\xbd\\xc5hi<\\x8a\\xd9\\x16\\xbc\\xc2MD\\xbd\\xf6\\\"?<\\xf7K\\xbc;\\xdfJH=\\x05\\xf9\\x15<\\xde\\xbf;\\xbb\\x1a\\x03\\x17<\\x87\\x8e\\x9e\\xbc,\\x7fy\\xba\\x81h\\x82<AU\\xba<\\xb8\\xeb{\\xba\\xcbb\\x03\\xbd\\xe8\\xf8\\x99<\\xf8\\xad\\x84<\\x92\\x1d\\xbe<b\\xa2\\x0e\\xbd\\x91\\x82\\xc5<0NY\\xbd\\xdd\\x88q\\xbd5\\xf4\\xab<nu\\x1e=\\xf0\\x8d\\xc1<q\\x8f\\x01\\xbd\\xae#P\\xbd@ `<suP=\\x1ak\\xc0\\xbc\\x03\\xe4\\x85\\xbc\\xd9o\\xcb\\xbc\\x96\\x13\\xae<\\xf3r\\x15=\\xd3Ru\\xbbe1f=U\\x17\\x10=\\xc4\\xaf\\t=\\x1e\\x9f?</\\xd0\\xc8\\xbc\\xe6\\xce\\t;\\x83\\x91%<\\x9d\\x85%=P9o\\xbbLY\\xdf;o\\xa2\\x1b;\\xe5Bv=\\x82J-\\xbdg\\xad\\xce<sk\\xa0;\\x7f\\xbe`\\xbc\\xae\\xe9\\xc6<\\xc7\\xc8#=\\x01\\x9a\\xfb<P\\xae\\xf2=\\x00\\x96a<\\xdf \\x7f\\xbd\\r\\xca@<\\xc3\\xb8\\xbf\\xbd\\xfcv\\xe0<\\xcb\\x9f}\\xba\\x9f\\x82\\xa8\\xbb\\xed\\x81\\x8a=\\xa5\\xf0\\xb5<_\\x17\\xe9\\xbc\\x13\\xf7\\x81\\xbc\\xaa8\\xfa\\xbb\\xa4$\\xa7;\\xb2\\x10\\xcd<Ka\\xec\\xbcr\\xbcV=\\xc0F\\xf5\\xbc\" \nHSET bikes:20009  model 'Secto' brand 'Peaknetic' price 430 type 'Commuter bikes' material 'aluminium' weight 10.0 description 'If you struggle with stiff fingers or a kinked neck or back after a few minutes on the road, this lightweight, aluminum bike alleviates those issues and allows you to enjoy the ride. From the ergonomic grips to the lumbar-supporting seat position, the Roll Low-Entry offers incredible comfort. The rear-inclined seat tube facilitates stability by allowing you to put a foot on the ground to balance at a stop, and the low step-over frame makes it accessible for all ability and mobility levels. The saddle is very soft, with a wide back to support your hip joints and a cutout in the center to redistribute that pressure. Rim brakes deliver satisfactory braking control, and the wide tires provide a smooth, stable ride on paved roads and gravel. Rack and fender mounts facilitate setting up the Roll Low-Entry as your preferred commuter, and the BMX-like handlebar offers space for mounting a flashlight, bell, or phone holder.' description_embeddings \"-\\xa5\\xa0<9\\xb1\\x0f=\\x0eM\\xdc\\xbc!\\xf5e=\\xeb\\xbcG\\xbdGy|=\\xf26\\xfd\\xbc\\x9c\\x8by\\xbd\\xed.\\xb0<\\xaf\\xe5><\\xac\\xa5(\\xbdK\\x05\\x03\\xbd\\xbe\\x1e\\xd5;\\xd92$<\\\"\\x10M=\\n\\xb1\\xb0\\xbdV~\\xec\\xbb\\xea\\xe9H\\xbc\\x1d\\n\\xfa<o-_\\xbc\\xf1\\xd2j:r\\xdf4;}\\x07$\\xbd\\x1cp\\xcb\\xbcI\\x1e\\xb1;\\xb8\\xdc%\\xbbv\\x11\\x85\\xbcu\\xdc\\xd3\\xbc\\x02\\xc42\\xbd\\x8f)\\xc4=\\xcb#X;$\\x8cD\\xbd\\xe3\\nB<\\x92m\\x0c<P\\xe6\\x04;\\xe8\\xa7J9\\xa4T\\x97<\\xe2\\xd3\\x0c;\\x96\\x10\\xd2<\\x01\\x8d\\xc3<n\\x92\\x95<\\xa5+\\xed;\\xe8\\xd2\\xb2\\xbb\\r\\xa0\\x1c<\\xda\\x1d\\xa1\\xbc\\x98\\x90\\xdf\\xbcd\\x96\\xba<\\xb8f\\x96\\xbd\\xbe\\x94s\\xbd)\\xc6\\x1c=\\x18>\\xc2;\\xe4\\x07\\xbc<Ld]\\xbc\\x0e\\xfa\\xdb<V\\x0bI\\xbb\\xfe\\rx\\xbd)\\xc5\\xed<\\x84\\xbb\\xde\\xbd\\n\\xd2\\x99\\xbb\\x10;\\x1c=\\x05\\xa4e=\\x93\\xfb@=9%\\xe4;1\\x158=\\xdf;@=\\x13\\xefw9\\xdea\\x1f\\xbdn\\xcc\\xcd\\xbb\\x9a\\x9b\\xee<\\x15\\xe4\\x14\\xbd\\xa7v\\xfa\\xbc%\\x84[\\xbd\\x0e3\\x80\\xbb\\x92/\\xdf\\xbd\\xfb\\xba!<\\xc5\\x04\\xca<\\xd3\\x82\\xab\\xbc\\xeb\\xa2\\x82\\xbc\\x13\\xbd\\x16\\xbc\\xe7d\\xc8\\xbc\\xb7\\x07\\x04\\xbce\\x1a\\x80\\xbd\\x80\\x1c\\x98\\xbd\\xaa,k;\\x86\\xbd%;\\x07\\nz\\xbd\\x8f\\xef\\xab\\xbb?=0=\\xd0\\xda\\x14\\xbd\\x9c\\xb8\\xf2<f\\x18\\xaa\\xbc\\xbcT\\x13\\xbdI\\xf4y;-k\\n=8\\x87\\x03=+/\\xbf<\\x991\\x9f\\xbc\\x11\\x1b\\xf2\\xbd\\x01\\xfdz<\\xf2-\\xc8\\xbd\\x92En=3V[<\\x17T\\xf6<W\\x9dR\\xbc\\x1d\\xd5G;\\xb0\\xffk=M\\x8f\\xdb<\\x9b\\\"=</\\xc4\\x86\\xbc\\x16\\xa3\\\"=j\\xc0\\x8e\\xbb\\xc5~0=\\x7f\\xf6\\x8c=EK\\xa49\\x82r\\xc5\\xbcFN\\xad\\xbd\\xdc\\xbc\\x91\\xbcO\\xe4!\\xbd\\x86\\x1ac=\\xae\\x14d<\\x9e\\x0fp=y\\x96\\xb6<\\x85\\x1fS\\xbd\\x80w/=\\x8bF\\x0c=N\\x914\\xbc&oR=\\x9d\\xe1\\x1a\\xbd\\xe4\\xee\\xd1\\xbcT\\xd3\\\"\\xbc:\\xf7\\\"\\xbd\\xce6\\x82\\xbd\\xf3\\x83\\xde<\\xbb\\x80\\x80\\xbcj\\x1b\\xfc\\xbc&;\\x19=\\xd5\\x9a<=e\\xf6\\xd0\\xbc\\x11F\\n\\xbcLe\\xcd<\\x89\\xabN=\\xac\\xea\\x85\\xbc\\xb8\\xf0<<L\\xa5@=\\n\\x08\\xaa<N_\\xd7<\\x1ae\\xb5;\\x8fA%\\xbd\\x1cF\\x88<8\\x95\\xc2\\xbc\\xe4b`\\xbae\\xaa\\xaa<\\x9f\\xec\\x0f\\xbc\\x02\\x83\\t=l\\xaf\\xec<K\\xcb6\\xbb\\xac\\x1c.\\xbd\\xfc\\xfd\\xd3\\xbcI\\xd8&\\xbb\\xa2\\x03i\\xbd\\xe36\\x85=e\\x7f\\x1e=(7\\xb2\\xbc\\x8f\\xeb\\xaa\\xbd\\xdd\\xc8\\xb2<\\xe7\\xa9V<)\\x01\\xc6\\xbc\\xb8Q|<\\xed\\xf1q=\\xdf\\x8e\\xe9<\\xa7:>\\xbdo\\x0f\\xc1=S\\xf3[<\\xa9\\x9eD\\xbd3~`\\xbc\\x9f\\xe9\\xd9<\\xb8\\xc1@\\xbd=(\\xb8=\\xf5\\xb6\\x92<\\x8b\\x9e\\x83<\\xbdi\\x15;\\xb5\\xbbm<a\\xf5K;^c}=\\xc9\\xcet=d\\xd5$\\xbd\\x87\\xeb\\xc6<\\x8b\\x10\\x04\\xbd\\xe7\\x00\\xbe\\xbb\\x0c\\n\\x1b=\\x85\\x8e\\x10=\\x85\\xec>=\\x11\\xb2\\xc7\\xb7\\xaa\\xeb\\xa3;\\xc0\\xd9L\\xbd\\xa8/_=\\xeb\\xc4\\xae\\xb9\\xe6\\xd4\\x01=>\\xf1Q=\\x84\\x1a\\x18<)\\xc5_<\\xd6\\xd1\\xaa\\xbdA23\\xbc\\xfb\\x1c\\xae\\xbc]\\xeb(\\xbd\\xb0Z\\xf1<\\x00G\\xdf\\xbc\\xc6G\\xd3\\xbc\\x07n0=F@\\xa8\\xba\\xd2k\\x02\\xbd\\x98\\xf7&=\\x16%\\x94\\xbc\\x08\\x9a\\\\\\xbd\\xb5H|\\xbc\\xdaqE=\\x87\\xd8\\xa6\\xbd\\xe7\\xf0\\xd5<\\xd2\\xe7T<\\x8c=\\xa3\\xbcT`\\x1e<JE\\\"=\\xb2\\x02\\\\\\xbdA\\xe0\\x83=]<7\\xbd\\xf6\\xdc4\\xbdN\\\"?= w\\xaa\\xbc\\xb0\\xcbG=\\x1b\\x05\\x01\\xbc\\xd9\\x0f=\\xbd\\x0b\\x14\\xec;\\x1e\\xb7g<\\x1c\\n%\\xbc \\x19\\x05\\xbd\\xbfS;\\xbd\\xb0f\\xa1<\\xf7\\xa35=\\x08\\nW=\\xd6;i:\\xd93\\x0e\\xbd+\\x99z\\xbc\\t\\xcc\\x82:S:\\x14=\\x03\\xaa\\x03\\xbbD\\xbe\\x1b\\xbb\\xc2\\x9fc<\\xf2w\\xb5\\xbc0\\xf2\\xab\\xbcJ\\xeb\\xde<2\\x86\\xc4\\xbc\\xb3P\\x89\\xbd\\x99\\xfb <3\\x80\\x99=\\xb6\\xaf\\xeb<\\x92W\\x80=\\x01\\xc4\\xe7\\xbc\\xcc\\xc5\\x9b;\\x00g\\x03=G\\x08 =\\x1b\\xa1\\xe4<\\xf2L\\x91\\xbdJ\\xd4@\\xbd\\xfc\\x92\\xaa\\xba\\xaag{;a\\xdb\\x82=$\\x94\\x91<\\xb4%$<\\xa7Z\\x00\\xbc\\xb6\\xa5\\xc6\\xbcdI\\x17\\xb9r%\\x1d\\xbd\\x9d@\\x1e<\\x86\\xac\\x11\\xbc6\\xb2E<1h\\x1c\\xbc\\xde:\\x9f<\\xec\\xc7\\xa2\\xbbN\\xa5\\xc0<i_\\xa8\\xbc3\\xe2e<\\xc2\\xd9P<\\xda\\xf2\\xf1\\xbb\\x14\\xdd\\x1c\\xbc\\xd4|\\xa7\\xbcR\\x89\\xf3<w1\\x9d<\\xe9W\\x91=\\xe4\\xe6k\\xbd\\xb7\\x08V=9\\x1fK\\xbd\\xa5\\x07\\x1e\\xbca\\x89\\xa3;\\xe4[\\xad\\xbdC\\x01\\x04\\xbdj\\x8b\\x81\\xbd\\xe3\\x08\\xd6;\\xbfJ\\x03=`\\xd1f<\\xb9`\\x10\\xbdh\\xc8\\xfb<\\xa3\\x1a\\x8d;\\x1dc\\xb4=\\xb3\\xe5\\xc5\\xbc0\\x7f\\x82\\xbbA\\xee:=Rb\\xab\\xbcB\\xb7\\xee<\\xeeS\\xc2<+\\xfa/\\xbc\\xc4\\x9a\\x8e\\xbd\\x917\\x11\\xbc\\x01Dz:\\xbe\\x1a\\xc6\\xbb\\xbeV\\x1c<m\\xde;\\xbd\\xaf\\x9c\\xa6\\xbc[&]=\\x06(\\xaa\\xbcB\\xf4i\\xbc\\xac\\xf7\\x97;o\\x01~\\xbd\\x11zQ<\\xbf4\\xe6\\xbc$W\\xd0<\\xab\\xac\\xad\\xbd\\xado\\xb4\\xbcr\\xf1\\x91\\xbd\\xed$\\xa8=\\x9f\\xd9\\x00<\\xb4^\\x1f\\xbd\\xe9\\xda\\x19\\xbdy\\xd4\\xfa\\xbb^\\xa9\\x1d\\xbd*\\xbc\\x14=\\xa6\\x9eH\\xbdwmE<l8R;\\xa5Ip\\xba\\x933\\xde;5\\xd8\\x05<\\xb7}\\x1c\\xbd3\\xf8\\x9c=_+N\\xbc\\xb5\\x0b.\\xbc\\xf0\\xa5I=\\x8fb\\xea<yW\\x0e<\\x7f\\xad\\xf7<=\\x87\\xba<\\xdd\\x90<=X!\\x10\\xbdfn\\t=\\xc5\\x95\\x12<\\x17\\x05\\xa6\\xbcKR\\x1e=\\x14\\xbb\\xac\\xbbu\\\"\\xe6;\\xc1\\xb8b\\xbc24\\x9a\\xbd\\xd6b\\x07=\\xfc\\xbc\\x8e<\\xcb\\xfe\\xfb<\\x86\\xdf\\xaa=Y\\xdf\\xbd\\xbc\\x10{M\\xbd\\xb3\\x8e\\x86\\xbbe\\x07h;\\xb5\\xde\\x0c\\xbd\\xf4\\xc9\\x9a\\xbb\\x13\\xed\\xce<X\\x85/\\xbd-\\xa2J=\\xdcT>=\\x7fX\\x80\\xbc\\xc8\\xe7\\xab<1\\x07\\xf2\\xbc\\x1c{$\\xbd{\\xc1\\x81<\\xd61\\xba;\\xd3G\\x00\\xbd\\x1b\\r\\x97:\\xf6\\x07(\\xbd\\xc0\\x02\\x86<\\x1c\\x89\\x03\\xbd\\x8c\\xaa@\\xbc\\\\\\x13\\x03\\xbd\\x8b\\xe0\\\\=VW\\x8a<x\\x9c\\xa3\\xbc\\x13~<\\xbd\\x08\\x8c\\xab=\\\"\\x03\\x87=rD`\\xbd\\x11\\xbc\\x89\\xbaI\\xa4\\xff\\xbc\\xe6\\xb9\\xb3\\xbd\\xbah\\xd2<\\xc1\\x00 =\\xcb26\\xbd\\xc2Z\\xbc\\xbc\\xf9\\x0ce<\\xfe\\xd4\\x8d<\\xdb\\xa3\\x9d=\\x1e\\xbf\\xe7;J(*\\xbd\\x95<a=G0\\x86<%O}\\xbcO\\x82\\xff<\\xd4>\\xae<\\x8fY\\x8b\\xbc\\xcc\\xba\\x9d<\\xc8\\xde*=\\r\\x03\\xe9;1\\xea\\xec\\xb8\\\"\\xddX\\xbc\\x1e@\\xac\\xbc&G\\x04\\xbd\\xc4\\xfd%\\xbd:3\\\\\\xbd\\xbd\\xf3\\xae\\xbc\\xf7Go=.\\x0e\\xf0<\\xc4\\x98\\x1b\\xbd\\xd2*\\xc4\\xbc[,`;X+~=\\xfa\\xc4\\x84\\xbc\\xec9{\\xbd\\x86U\\x06\\xbd\\x00zG\\xbc\\x08z\\x88\\xbd\\x9d\\xbc\\x84\\xbd\\xb1\\x1e\\x05:x\\xe3\\xb8=\\xed\\xa5\\xcb<\\x97\\x1d\\xa5=H\\xc5\\r\\xbd\\xe0\\xd8m=A\\xc0\\x8d<T\\xc3\\x05\\xbd\\xc9\\xeaA=\\xa9\\x86\\x16\\xb9)+`\\xbd\\x83\\xc6\\xe1\\xbb6\\x94\\x9c=\\xdd\\xb0(\\xbc\\xafj\\x1a\\xbd\\x08Y\\xad<\\x98\\x1c\\xfa\\xbc\\xd0\\xcc_\\xbdC\\x89\\x15=\\xf7:\\x95\\xbcU+\\x1f\\xbd\\x06\\x08]\\xbd\\x8d\\xe7\\x0e=\\x8fI\\x13=\\xf8\\xb5\\xc2<,EL<\\xe1\\xba\\x81=\\x85V\\xee<\\x9c\\xfd4\\xbcK\\xb2F\\xbdv$L\\xbd\\xc8\\x8e\\x8f<\\\"W>\\xbcfFN\\xbcM\\x0c\\xf3\\xbc\\xd5&x=\\xde\\x98g\\xbcJ\\x17\\xb0<J\\x11\\x1f\\xbd\\x14\\x9a\\xd1<j\\x1a^\\xbdJ\\x17\\xbb<A\\xf8/\\xbd\\xba\\x9ah\\xbd0\\xbdG\\xbcm\\x0f\\xba\\xbc\\x7f-\\x8e\\xbcz\\\\\\xf2\\xbc\\xf2H\\x1d\\xbcw\\xd0\\x87\\xbc\\xb8\\xe9\\xd2;\\\"\\x15\\x1c\\xbd\\x11\\x16\\x85<\\xb0\\xcex\\xbc\\xe7\\xe3\\xae;\\xedi\\xef\\xbc\\xa3#\\xc6\\xbc\\xad\\x85\\x0b=8\\x01\\xda<\\xd4r\\x14\\xbd\\xacG\\x8c\\t\\x0b\\x190\\xbb\\xf3\\xd7\\x8e=\\xb7F@<\\xa9\\x08\\x99:A\\xff\\x0e\\xbb\\xc4b\\xc6<\\xdd\\xfd\\xe0<n[\\xd0\\xbb\\xae\\xb4B=\\x0b\\xe9\\x18=c\\xcc6=6\\xbb\\xf3<#{E\\xbc\\xb7\\xb8\\xbd<\\x0f\\x14\\x9c\\xbcx\\x0b\\xac\\xbc\\x95JL={\\x06o\\xbcc\\xb7L;\\xf9/\\xe6<\\x9a\\xb7k\\xbb1\\xba\\x9b<\\x04|\\x8b<\\x97\\x0eP<*oe=x\\x05\\xc4<9_y<\\xbaR\\xbe\\xbc\\x16\\xf9\\xbd\\xba\\xfb\\x00\\xfb</\\x14t\\xbc\\xc2\\x136=\\xd8\\x18\\x9f\\xbd\\xc4\\xd1T\\xbd=\\x04 \\xbd\\xd3\\xbdi\\xbc\\xdbA_=E#\\x94\\xbb\\xca\\xc1\\xa3<\\xc6\\xb6!\\xbd\\xb3qg<\\xfa\\x9b1<\\xe0\\xf6\\x8b<\\xe4\\x06\\xd2;\\xd2:u<\\xed\\x06\\xbc\\xbc\\xa4\\xfd\\xe5= \\xef\\xb0\\xba\\x0fv\\x06\\xbd\\x82\\xa8;\\xbb#\\xef\\x8c<\\xf5b9\\xbd\\xb7\\x85\\xf4;O\\x88`<A)\\xf2\\xbc\\xf4{l;\\x0bO\\xbd\\xbb\\xa9\\xbe\\xb8\\xbc\\xf7\\xf6\\xa8\\xbc\\xe3\\xceI;\\xc2T\\x9f<\\xb8\\xdd.<\\xbdn\\x8c<\\x9d\\x10,\\xbc\\x8d\\xa1M\\xbd\\x9f\\xa87\\xbdKL\\xac<\\xdb\\x12\\xce\\xbcYn\\x96<\\x04Cd\\xbc \\xf6X\\xbdzr\\xf2\\xbc\\xde\\xe8\\xd1\\xbb\\xfb!\\x01=w\\xbf\\x05=\\x94\\xe4\\x94\\xbd\\x0e`\\x84\\xbb\\x19\\xbe=<\\x88\\xa5\\xb0\\xbc\\xdb\\x1d\\xa3<.\\xc2\\\\\\xbdS\\xeb\\xf7;\\x11\\x94\\r;Tr\\x9c<\\x13\\x94\\xfe\\xbc\\x85\\xfc\\xd4\\xbc\\xffq\\xcc<z\\xd8\\x00=\\xf8\\xb2v=\\x9a\\xe9\\x9b;\\x00#T\\xbc\\x1e\\xe2i=\\xa1\\xd5\\xc3=\\xe4\\x86I;\\xcc\\x07K<cg\\x0e=G\\xc2\\xf0\\xbcW\\xbb\\xb2<\\xf5\\xf4\\xdb\\xbb\\xe7\\xd6B;\\x86\\xd7G=(\\xcar\\xbc\\xd6\\xc6\\xb3<\\x99\\x9f\\xd8<\\x9e@\\r<I\\xc4\\x00\\xbdf\\xa6\\x1f\\xbb\\x96\\x86\\x0e\\xbd\\xb1\\xd9\\x18\\xbc\\x1fA\\xa3\\xbd\\x9dYs=\\xb6[4=\\x06\\x8d\\xbe\\xbc\\xc9\\x0c\\x06=\\xd3O\\x11=8\\xfc\\xbc\\xbc\\x96\\xe2B=\\x07\\x7f/:\\xd9\\x87L\\xbc\\xa4\\x8f;<03\\xec<\\x18\\xbe\\xb6:\\xcb\\xa8L<Q`2\\xbcu\\xef\\x83<\\x91\\x1a\\xab\\xbca\\x80%\\xbd}!\\x80;u\\xcb\\xe5;\\xcf\\xc9\\x99\\xbd\\xaej==\\x01\\xadU\\xbd!\\xdb\\x10=*J\\xfe<\\x15S\\r=\\xd01*<\\x1d\\xb7\\xe0<\\xefCD\\xbd\\x84fR<\\xc0b\\x1d\\xbd\\x19\\x8c\\xef;\\x9c\\xd8\\xc2\\xbc\\xa6M\\x8e\\xbd\\x14\\xfa\\xd4<\\xfb\\x1e\\xb8\\xbcS\\xcd\\x88\\xbc;\\xda\\x06<\\x17\\xad$=\\xa0\\x02\\xc2<%\\x11A\\xbc\\xee\\x06\\xaa\\xbc/=%<\\xbaB)\\xbd\\t\\x80\\xc8<\\xe9cV=`\\xe8\\xbc\\xb9\\xd8\\xc77\\xbd\\xc3\\xc2\\x11<\\x0e:&\\xbb\\xeaD9=\\xe3\\xc4\\\\\\xbdl\\xfc\\xfd<\\xa9\\x12\\x95=\\x97XS\\xba\\xdd\\r\\xe2\\xbc\\x18r\\xec\\xbc\\x8f;:;o\\xec\\x87<\\xc1\\xd9\\x81=\\x94\\x01\\xf6<\\x1a*\\xc99\\xc3\\x84\\xc9\\xbc\\x03\\x0c\\x18\\xbd\\x18KU<u\\xfc\\xc2<\\xa4\\xee\\x1e=\\xcb\\xb7\\x87;\\xce\\xf8(<\\x1dO\\x7f<\\x1a\\xd9\\x01=`\\xf1\\xa1\\xbc\\x85\\xd3\\xf3\\xbd\\n\\xfa\\x1d\\xbc\\x17\\x86\\xcd\\xbc@\\xc8g6e\\xe2\\xce\\xbc\\xc5\\x7f\\x8a<\\x83\\x05a\\xbd\\xdb\\xa4\\x9a<\\xfdg\\x06\\xbc/G\\x0b\\xbcC\\xfb\\x82<\\xdcX\\x15\\xbc3~\\xb8\\xbb\\x17\\xd4T<\\x1c\\xfd&\\xbd\\xfc\\x99\\xa4\\xbc\\xfc\\xac\\x85\\xbc\\xe4\\x14F\\xbd4\\\\\\xfa\\xbcx\\xe2\\xb0<\\xe3}b\\xbd\\xc7Hk<G\\\"W\\xbd\\xf4\\x9f\\xf0;\\xe3\\x8b\\xf3\\xbcsCC\\xbb\\xe3\\xc9\\xff\\xba\\xfa\\xb3`\\xbb4\\x8e\\xdd\\xbb=\\xe7\\x97<[\\x04\\xac<\\xb2\\xd5R<\\x93\\xb2\\xbd<QW\\xb1\\xbc\\xd6\\x1c\\xb0<\\x9b\\x17M\\xbc\\xb5\\x99\\xa1\\xba\\x8a\\xfb\\xf0<\\xbc\\x15\\x8b\\xbdo\\x178\\xbd\\xed\\x11d\\xbd*\\xc6:<8\\x17\\xb6;v\\xc6\\xea<\\x01\\xee2<\\xf4\\xe0\\x83=Yp\\xb8\\xbb\\xc4zN\\xbd\\xab10\\xbc\\xc0S\\x03\\xbd\\x16\\xe0\\x13\\xbd\\xdf\\xf6\\xd0:\\xe7\\xd6H\\xbc\\x7f0\\xf8\\xbbC\\x00\\x18\\xbbw\\xa5\\x17=i\\xe3\\x15\\xbd\\xf1\\xad8=mzJ\\xbd.\\xfb\\xf2\\xbcIi \\xbbX{8;\\x10\\xc1\\xb7\\xbc\\x95!\\x80\\xbbHLJ\\xbc:\\x88\\xc4\\xbc3\\xe2P=\\x80\\x10n\\xbd\\x8c\\xde1\\xbb)\\x08\\xb6\\xbc!\\xeb\\xba\\xbc\\x81\\xf3\\xd2\\xbcZ\\x82\\xb9<\\xe5\\x9a\\xc5\\xbd\\xc1\\x14\\xbf<j=B\\xbd\\xb9:\\xcc\\xbc\\xd0\\x01\\xa2\\xbb\\xa9\\xb4\\x05\\xbd\\xddzL\\xbc\\xd6\\xfa_<E\\xc6\\xd5<;<\\xd2<Y\\x1cc=v\\xf0Y\\xbcH\\xb9b=%<\\x1d\\xbc\\xbe\\xa5\\x0c\\xbdo\\x9d7=#\\xf54=\\x11M\\xad<F8`\\xbc\" \nHSET bikes:20010  model 'Summit' brand 'nHill' price 1200 type 'Mountain Bike' material 'alloy' weight 11.3 description 'This budget mountain bike from nHill performs well both on bike paths and on the trail. The fork with 100mm of travel absorbs rough terrain. Fat Kenda Booster tires give you grip in corners and on wet trails. The Shimano Tourney drivetrain offered enough gears for finding a comfortable pace to ride uphill, and the Tektro hydraulic disc brakes break smoothly. Whether you want an affordable bike that you can take to work, but also take trail riding on the weekends or you’re just after a stable, comfortable ride for the bike path, the Summit gives a good value for money.' description_embeddings \"\\xd2Tm\\xbbJD\\xbd:r(\\x1e\\xbd4s\\xea<\\xcagE:\\x00\\x83\\xd6;\\x9e\\xef\\x80<\\xbar\\x95\\xbd\\xce\\x14f=\\x96\\x85\\xd4<\\xf2|X\\xbcM\\xf0\\x19\\xbd\\x90\\xd1\\xee<\\xeau\\xf1<\\x18t\\x9d=\\xb0\\x8b\\xcd:\\xb9\\x0b\\xab<@\\xf3B;r});\\x8c\\xaf\\xa3\\xba\\r\\x98\\x03\\xbcHVS\\xbaX\\xe9\\xc6\\xbc\\xbc5\\x8e\\xbd\\x01w\\n\\xbd\\x939\\xbe\\xbb\\xf5?@=M2r\\xbd}L\\xbf\\xbcT1S\\xbb\\xa2\\xd7\\xa7\\xbc]\\x96\\x8b\\xbcW\\xfd\\xf0<\\x10A\\xaa<\\xc5\\r\\x08=\\xdek\\x1e\\xbbz\\xf2\\xe1<tHU\\xbc\\x80\\xeb\\x95\\xbcc\\x07l\\xbb\\xf6|\\x8d=6_\\x1a=;\\xcb\\x05<`U\\xf5<\\xa7\\x1bV<\\x13a\\x8f\\xbdj\\xc3\\x8b=\\x1d\\xdf2\\xbds(&\\xbdq8>;c\\x83\\x89;\\xfd\\x06l\\xbd4\\xd4\\xeb<\\xe7\\x16\\xe6\\xbca\\xd8\\x05\\xbc\\xd4\\x9d\\xea\\xbb\\xe5\\x04\\xc9\\xbbX\\x9b\\xdc\\xbcE\\xbcD\\xbd\\\"\\xcaF=w\\x0e\\xbc;\\x06\\xd7\\x1d<\\xad\\x9d\\x99\\xbb)\\x9e\\x83<\\xb12?=\\x82\\xdb\\xb3\\xbc\\x10v\\xba;\\xba\\xf8 =\\xf2*\\xeb<\\x94\\xedn\\xbbMk\\x1d\\xbd\\x13\\x99\\xb9\\xbc4\\xb3\\\"\\xbd\\xe9\\xfb\\xc1\\xbbD\\xd1\\x80\\xbc\\x83E\\xe3\\xbcd\\xa4\\x84\\xbb\\xe0s\\xef\\xbc\\r\\xe0\\x88\\xbc]A%=\\x97\\xaa\\xab<\\x06}\\xef9\\x03\\x98\\x1d\\xbd\\x03\\xc9\\xdb\\xbc\\xa4&\\x8d<\\xdf\\x8d\\xac<S\\xde};\\xfe\\x07H=\\xdd\\xf4\\x04\\xbbCmG<\\xc98\\xfe<%&\\xff\\xbc#\\xc1=\\xbd\\x81\\xd9?\\xbd\\xf5\\x08!=\\x8a\\xf8\\xad=:\\xe0\\xad\\xbcB\\x9f6\\xbc\\xcb:\\xda\\xb8E\\xe9\\xbb\\xbb\\x81\\xb7\\xc2\\xbc\\x13rh\\xbb<\\xc11\\xbc\\xa4\\xb8\\xe9\\xbc\\xdf$\\xd0\\xbbn\\x01\\xb8=}\\x8a\\x08\\xbd\\x08\\xfc\\xab<\\xfcT\\x11\\xbd\\xce\\xe0\\xb3\\xbb\\x82@V\\xba\\x92\\x1e\\x0b=8\\x1b\\xf3;\\x81\\x06\\xda\\xbc\\xb4 \\xf5\\xbc\\x98\\x18-=\\xe8\\x85\\xe5<\\x12\\x92\\x00\\xbd]\\x93\\xc5;\\x11\\x8d\\x02\\xbc\\xfb\\xb8\\xc7<\\xb0\\xe0*;\\xe1u\\xd7;\\xd6\\x0c\\r=U\\xc9\\x18\\xbd\\xdc\\x94\\xdc\\xbc\\xe2yk\\xbb\\xdd\\xff\\xe6\\xbc\\x1c\\x85\\x0f\\xbd2\\xfe\\x17\\xbd\\xa4/\\xb4\\xb9\\x80yU\\xbd\\x7f\\x07y=\\xe5\\xc9\\xdd\\xbc\\xfa\\xcbM\\xbd\\x94\\xb5\\x8e=\\x11)\\xba\\xbcP\\x12*\\xb9\\xddI\\x11<\\xe6Xl\\xbd\\xaa\\xb2\\xaf\\xbbe\\xf0\\xf0\\xbd\\x19\\xaai\\xbc\\xf7\\xcd\\x80=\\xa8}/=\\x8bK\\xb7<;\\xeb\\xbd\\xbc\\xd8\\x82-<\\x03\\xdao=\\xde\\x06]\\xbdj#\\xd5\\xbb\\x19\\xe50=\\xd4x|\\xbcu\\xbf\\r\\xbc\\x07\\xa4\\xa6\\xbd\\xbdx\\xfc;f\\x80z\\xbc\\xfa\\xafN\\xbc\\xc5s\\xbe9\\xb1\\xde\\xe1\\xbc\\x05-\\x93=\\xf6d\\xb3\\xbc\\x96\\x9c\\x13\\xbc)\\xb3\\\"\\xbdK\\x19\\xed<\\xd9A\\xd0\\xba\\xd8z$\\xbdT\\xc1\\x0f<\\xe9+\\xc6\\xbc\\x7f\\xd4F\\xbd\\xc1<\\xec;\\x96\\x17\\n=\\x8cd/<F\\x1bo\\xbd\\xacXV<\\xdb\\xbfy<\\xf8\\xe9\\xdf\\xbd\\xe6\\xeeS=Ylx<\\x85l\\x98\\xbb\\x19\\xd0\\x1c\\xbd\\xcf\\xa9O;}\\x8f\\xe8<t\\x93\\xcf;\\xb6\\x8f\\\\<\\x85\\x02\\xde<\\x0e5\\xa1=\\xea`$\\xbd\\x8cY?=\\xad\\x8c>\\xbc\\xe4\\xdaw=@\\xd2\\xa0\\xbc\\x03\\x9a{\\xbd\\xf1\\xb9\\x9b<G\\t\\x8d;\\xf04\\x06<@c\\xf2\\xbc\\xa5*\\x8c\\xbd\\x07\\xf8i\\xbd|x\\xb7<\\x9b\\x13\\x05\\xbd\\xcbd\\x0b\\xbd\\xa3\\xa8\\x9b\\xbd?\\x83\\x97\\xbb\\xfd\\xbd\\t=\\xa4d\\x08=A\\x89\\xec\\xbc\\x13\\x9b\\x80< \\x1e0=\\x1a\\xacb:\\x84~g\\xbc\\\"FK=O\\x03\\x8c<*(2\\xbdg^\\xb5<?\\xa6\\xfc<s\\x1b]<\\xba+U\\xbc\\xdb\\xdd\\xe9:[\\x87S\\xbd\\xd4\\xdaj\\xbb\\xeb8\\x02\\xbd\\xbb\\x8c\\x98\\xbc!#\\x18=\\x90\\x89\\xfe<*\\x18j\\xbc\\xff#O=\\x08\\x17\\xc9\\xbbf0I=\\x1c\\xe5i\\xbd9\\x0e3;\\xe4\\x1f\\x83\\xbc\\xd8\\n0=qj\\x87;\\x01\\x98i\\xbc\\xb4\\xabO;*\\x83\\xd4<\\xab\\x19\\xc6\\xbb\\xa8:\\x96\\xbb\\xf6\\xce\\xfd<\\xc1F/\\xbd\\xa7\\x97\\xda:\\xdcx-=>rY=:\\xdd\\x87<\\xc9\\xbfX==\\x90\\xdf<\\x0c[\\x1d=\\xe1\\xa8\\xcf\\xbc/`\\n=\\x1b\\xd5\\x87\\xbcSX\\x92\\xbd\\x0f\\x89\\x82\\xbd6\\xc4\\x06=9\\xd6\\x92\\xbd\\x8dzJ<b\\xea7\\xbd/\\xf3>\\xbc\\x0cj\\xac\\xbc\\xae+\\x96=\\xc3\\x8e\\x95\\xbc\\xfc\\x8cw;\\xaf\\x9e\\xa5\\xbd^\\xcek\\xbb\\xd6\\xafo;\\x80H\\x84<\\r\\x1c\\xb1\\xbc\\xf6\\xef\\xe3\\xbc\\xb0\\xfe\\x02=\\xd6#\\xe2;;\\xed*<\\x04b\\xf4<B\\xc8\\x82\\xbd>\\x06\\xc2<\\x81\\xed\\x88\\xbd\\xd1\\xcf\\x1e\\xbc@\\x01b\\xbb\\xc6\\xa7\\x0b\\xbd\\xa8\\xf2\\x85<\\xfc\\xc8\\x97\\xbd\\xbf/M<\\xb4\\xb9_=c\\xa9\\x81=\\xd3\\\\u\\xbc,3O;\\x1b7\\xb4;\\xd9P\\x1f=\\x8b+:=xOf\\xbd\\xb2\\xb4\\xc1\\xbcb\\xe9\\x1a\\xbc\\xae\\xac\\xda\\xbc\\x8d<\\x95=\\xa3\\x07(<\\xa7\\x16\\xb6\\xbb\\t\\xab9\\xbdx\\xb1\\xdb\\xb9\\xf6\\x85\\x0f=\\x16H3\\xbc\\xe1M\\x14\\xbc\\xd7~\\x9d<\\x88\\x9d =\\xf8C\\x86=b\\x8c\\x98\\xbb\\x05\\xf6\\x05=hw\\xfe<J\\x86\\x9e\\xbc\\xb7$\\x82=\\\\\\xfcV==\\xf9\\x9f\\xbb\\x9ak/\\xbd \\xf9F<\\x05p\\n\\xbdi\\x8f\\xe5\\xbc\\xcd\\x82\\xc8<\\x8f\\\"\\x08\\xbd\\x10\\xc5\\xd8;A\\x91\\xb4=d\\xa3\\xe2\\xbcJ1\\x9a\\xbc\\x10\\xc7\\xa4\\xbc\\x06\\xcbs\\xbc\\x9a9\\xd4\\xbc\\xc6\\\"#<u\\xfd\\xd0:\\xd1\\xbbR\\xbd\\x1a\\xb1\\x02<\\xe2\\xb3y\\xbc\\x91~6=\\x14 \\x0b\\xbc\\xb7\\\"x<j\\x02\\x82\\xbcQ\\xfc!\\xbd\\xedL\\xdb;\\xe6\\xe9\\xa0<\\x9e32<\\x03\\xc6 \\xbdMx\\x0b=\\x1a.\\x9f;\\xfa\\xf9\\x15=\\xedV\\x96\\xbd\\xb1\\xa0E\\xba\\x83&j=\\xbf\\x9e\\\"\\xbdJ\\xb6\\\"\\xbd\\x9e\\xd6*=\\xe6e\\x1d<\\xd1\\x9dt<\\xc1@\\x83=iT\\xb5<X\\xe0>\\xbc\\xf1\\xaa\\x01\\xbd\\x13\\xd4\\x10=\\xe0S\\x96<\\xbf{\\x1d\\xbc\\x18O;=T\\xbbA;v\\xae\\xd1\\xbcnL\\x11\\xbd\\x06\\x1d\\xd7\\xbdS\\x86\\xcf\\xbc\\x06\\xaf\\x0f=i\\x98\\x1b\\xbcp\\x99\\xc4=\\x9ebI=\\xf5\\xd8q\\xbb\\xfb\\x83\\x97<\\x19\\x0e\\xaf<|X\\x8a\\xbd\\xa9%\\x98\\xbc\\xdb\\xc3\\t=P\\xb8V=\\x99\\x82\\x16\\xbb\\xb2\\x16\\x84;\\xf0<\\xa1<\\xd04\\xb3<1\\x8d\\x19\\xbc\\xfa\\xdaz\\xbdC\\xd2\\xca\\xb9[\\xae\\x84\\xbb0\\x84+\\xbd\\xe6\\xc0F\\xbd\\x0b|9\\xbc\\x7f\\xd9o;\\xb1\\x98H\\xbd\\xacd\\xb9<P\\xd5 \\xbd\\xf8\\xaeK=\\xf7Z_=\\\"\\nF<=\\xbfT\\xbd#\\x1d\\x0c=_d\\x98=L\\xd6^\\xbcw\\xaf\\xf0:\\xf4\\xd8\\xae\\xb9.:j\\xbd\\x9a\\xd4\\x98;\\x90 I={\\xc8\\x8d\\xbd\\x06(B=\\x10\\xb7e\\xbd\\xfa\\xd9\\xf7<\\x91\\xc2\\x06=\\xc0X\\xd5<\\xd1;\\xa0\\xbd\\xff\\xb6\\x11=\\xdcr\\x12:\\xa2\\x80\\xbd<\\x1d0\\x88\\xbc\\xa6d\\xa5<]\\xc9\\xcc\\xbb\\t\\x84\\xa9<\\x1d\\xdf\\xdd<U\\x11\\x93<G\\xbe\\t<l-b\\xbdC]b\\xbdH\\x11\\xc0<X\\xd6\\x1f\\xbd\\xf5\\x0f\\xa7\\xbc^\\xad\\x7f\\xbc\\x91\\xcc\\x1b<F\\xc8\\x80=>5\\xc3;\\xbd\\x8e\\xb48\\xcc\\xb7\\x80\\xbdm\\x9e$=\\x18\\x855<\\xc6r\\xb4\\xbc\\x12\\x1d|<\\xe8\\xec\\xc9<\\xd7\\xdbG\\xbd\\x16\\xf0\\x95\\xbdu?\\x12\\xbd\\xe6L\\xa9=\\xbf$\\x85<f\\xc6\\x01=\\xe8\\xc4)=\\x06\\xee\\x83\\xbc\\x85\\x1d\\x98\\xbd\\xb7|\\xd7\\xbc\\xc4a\\xe0;\\r\\xf0\\x14\\xbc\\xaf\\x84\\xe7;\\xd6rm\\xbc\\xe6\\xfbo;\\x16Rk\\xbc\\xa9\\xb3%\\xbd\\x8d\\x9a\\xe4<0\\xc2\\xfb\\xbb\\xc3\\\"U\\xbd\\xf0\\x9a\\xba;\\xbb;\\xaa\\xbc\\xc3J\\n=\\xe1?J:\\x01\\xac\\x84\\xbb\\xddw\\xbe\\xba?\\xab\\x1e<4?\\xac=\\xa5\\xee\\xcb=\\xa2\\x0e\\xbf<Z\\xb0\\xa9=\\xa3Rl\\xbd\\x96\\xdd\\x93\\xbc\\xbe``=\\x19\\x1e8:!\\xdb\\t\\xbd\\xfe\\x1c7=\\\"\\xdb\\x92<A\\xcf\\xe4;\\xb9\\xa1\\xb2<4d\\xb0\\xbc\\x18\\x05_\\xbc\\xa5\\xd1\\xfa<>P\\xc1\\xbckS\\x91\\xbc3\\xff(\\xbc}\\x93\\x7f\\xba\\xa3v\\x19\\xbd\\x1a\\xbd\\xdf<\\xf2\\x12^\\xbc\\xb7\\n\\x0e\\xbc\\xecM\\xf2<1PU\\xbbb\\x81\\xa3\\xbd<\\xd8\\x19\\xbdf\\xca\\x14<\\x87\\xddW\\xbc\\xe5\\xd0\\xc5\\xbb0j\\x15\\xbd#\\xe4\\xf7<\\xfdL\\x1f=\\x1b\\x00\\xbd\\xbc\\x16\\xf8m\\t\\x92\\xcc\\xa1\\xbc\\xf3\\xe9\\x04\\xbd\\xfa\\xaaL=\\x85\\x07\\x8b<)j\\xe9\\xbb\\xbd\\x90 =\\n\\xf4\\x07\\xbc\\xf2\\xf0\\x8d<PZ\\xc2\\xbb\\x8e\\xbd\\xf6\\xbc$88=l\\xf92=\\x8b\\x7f%\\xbc\\x9e\\xfdK=\\x85\\xbc\\x97\\xbcf\\x86\\x80\\xbc\\x86j_\\xbdr\\x10\\x1b<\\xa5\\x14\\x90;\\xb4\\xa2U=\\x83\\xe5Z=:\\xdd\\x1f<u\\t\\xa3\\xbc\\xb7*\\x01\\xbd,\\x1e\\xcc\\xbbh\\xc6\\x8f\\xbc\\x10R\\x1b<\\xa7\\x08h\\xbbm_&\\xbd\\xd5\\x82b\\xbc\\xa3\\x1b\\x92\\xbb\\x8a\\xa7\\xc1;j{,\\xbd\\xd8\\x11\\xe2\\xbc\\xfa\\xfa\\xa0\\xbc\\xad\\xd0\\xf6\\xbc\\x02\\x11=<\\xb4\\x87j<\\x93A\\n=\\x03\\xea\\x8d<Of\\xc4<\\xf9\\x01\\xe7\\xbc\\x8d\\x8b\\x9e;\\xbb\\xc3G=\\x8e\\xff~=C\\x00\\xbc\\xbcM\\xaf\\xab=\\xf5\\xf4\\xb0<\\xa2(\\xe5\\xbc\\xb0\\xdb\\xc9\\xb7\\xd9\\x0eH\\xbd\\xf6J\\xee\\xbdy\\x18T\\xbc\\xf1-\\xa6<\\xef\\x87\\x96<\\xaa\\x1b\\x1f=h\\x8d\\x82\\xbc\\x94\\xd4Q=\\xa4\\x97\\x9c\\xbb)\\xc0\\xe1<\\x8f\\x8f`\\xbb\\xe1\\x8e$\\xbc\\x82TE\\xbc\\xcf,\\x1b=\\xe3\\xf1*\\xbd\\xf9a\\x00\\xbd\\xb4\\xca\\x84\\xbc\\x1d\\xaa\\xc5\\xbc\\xb1*==X\\xa1\\x81\\xbdi\\xd6\\xa8<\\xc2\\x97\\x0b\\xbc\\xa8\\x04\\xe0\\xbc\\x81\\xfbu<Z)v<\\xbd\\xae\\x06\\xbd\\xa2%\\xc8\\xbc\\x91\\x19u=;C\\t\\xbd@\\xaf\\xc6;#\\xf2x\\xbch\\xe9\\x8f<\\x9b\\x18\\x18\\xbd\\x91i\\xa7=\\t\\x05\\x86<k)3<\\x85&\\xb5=\\xe0uK<\\xa1<\\x04\\xbd{\\xe2\\xdd<\\x05\\x9e\\xa5<\\x81\\x14\\n\\xbdi\\xa8\\xaa=\\xde\\x1c\\x7f:\\x7fAh\\xbb\\xb0\\x97\\xc5\\xbb}\\x01\\x91\\xbd\\x9c\\x99\\x90<P\\xf3!;\\xbf\\xd4\\x9e<^\\x13@\\xbd\\xd8\\xb6\\x03\\xbdXB\\xde<\\xc5\\xf4\\xa8\\xbc\\xbe\\xf3\\x98\\xbbXE\\\"\\xbbB\\x0c\\xe7<\\xb2}E9\\x1d\\x9d|\\xbd\\xc5\\xa7\\x1f\\xbd\\xf0^\\x9b<\\xe2t\\x0e\\xbd\\x8b\\xf8_\\xbd><\\x82=8A\\xdb<\\x1b\\xed\\x0f<X\\x00\\x88=Il+\\xbd\\x81\\x10\\xf9<\\x96}\\xd0\\xbc\\x12\\xb9\\x0f=\\xa4\\xc0\\x95=2R\\xc8<\\xc2/~=\\x10S|=zB\\xb5<\\x15\\xbc+\\xbc\\xa4M\\x99=\\xa6\\xe7\\xa2<\\xda\\xa8F\\xbd\\xde>\\x01\\xbbd$\\x9b\\xbdp\\xc1p=\\xdf\\xf0\\\\<\\xdc\\x1fK=\\x14\\xe3c<\\xb3\\x14\\\"=\\x10\\xff\\xb4\\xbd\\x81\\xf5\\x00<!\\x9a\\xe4<\\xe3<\\xf2<m\\xd3\\x82\\xbd\\x18=3\\xbd`b\\xa1<\\xc4q\\xb7\\xbc`\\xd2\\xbb\\xbb\\xda\\x88\\xcb\\xba\\xdc\\x85\\xc4\\xbb\\xcbT\\\"=\\x07\\xcf\\xff<\\xfe\\x83\\x85\\xbc\\x9a\\x1f\\xea<\\x87\\xa4,\\xbd\\x88=m<\\xc1\\xc5.=_\\xc2\\xcd<\\xb8\\x7f\\x1a\\xbc\\xc8\\xf0\\xbe;*S/=2\\xd9M=\\xd8)\\xb3<\\x9c\\x98k=\\xd0\\x92\\x86\\xbb`\\xe5\\x1c\\xbaC\\xf3\\x82\\xbcE\\xe4\\xbb\\xbc\\xbc\\xf5\\xdc\\xbc\\xf6_^<\\xfb\\xc4\\xa7=*\\x99\\xba\\xbc1J\\xf5<\\xc3\\\\Y\\xbbq\\xdd\\xbe<\\x99P{\\xbb\\xd3\\xa7\\xfa;\\xc9\\xbc\\xa2<\\x03\\xaaG=\\xb3\\xe2\\x9b=\\x81\\xd86:\\\\\\x01\\x97\\xbc\\x8a\\xb9\\x15\\xbdX\\xf4\\xaa\\xbdz\\xcb*=\\xb1\\x00{\\xbc\\x1e\\x98\\x1a\\xbd\\x8b*\\xb6\\xbc\\xb8\\xac\\x02=\\xddI\\x00\\xbd\\x1c@v=\\x8c\\x8e\\x10=\\x1eD\\xf5\\xbc=N\\xbb<\\xba\\x8a)\\xbd%3&<\\x9e\\x8d\\xf0\\xbcxR{\\xbd\\x8c\\xbb\\xd3\\xbc^\\xaa\\xbc\\xbc\\x86\\x1b\\xea\\xbc\\x82u\\xce\\xbc \\xf9\\x9a\\xbc\\xe8-\\x97<\\xb0]t\\xbc\\xc3\\xf0O\\xbdu\\x8a/<\\x87\\xc0#\\xbd\\xb5u*\\xbc\\x91*\\x02\\xbc\\x02\\xaa\\x8a<\\xe3\\x03\\x15\\xbd6\\xd7|<\\xdd(\\xc0\\xba\\x85\\\\\\xdc<\\x9c\\xdb\\x99:A9\\n\\xbcW\\x88\\xe7\\xbbq\\xfe\\xe5<W\\xd4\\xf8\\xbc\\xba[%\\xbd\\x18Dm\\xbd\\xb5~b\\xbc\\x01Y\\x15\\xbd\\xd4\\xf3~<\\xaa\\x7f\\x99=\\x18\\xc2V=z\\xbb\\xad<\\x88\\x8a\\xc9<\\xf6\\xedf\\xbd\\xf6Rf\\xbd\\x1e\\xec\\xc1<I,\\xe1<}\\xbd3\\xbdC\\xea\\xa4\\xbc\\xc2\\xab,\\xbdNA\\xde<mO\\xa4\\xbc\\xe8\\xc0r<\\xf6\\xc9\\xdd:\\xc2\\x83\\x08\\xbd\\x8e7)<r\\xebE\\xbd_\\xc9\\x9f<o\\xc8<<a\\x01=\\xbb\\xc6\\xd0.\\xbd\\x17\\xcf\\xad\\xbc\\x18\\xa7\\x10\\xbd-\\xb1\\x1b;)?[\\xbdN\\x8f1\\xbc\\xb5,w=\\x88\\x01\\xd1\\xbbT@\\xb4\\xbc\\xd4\\x86I<\\xd3\\x8f\\x03<\\x14\\xfc\\xc8<\\x19\\x18\\xe7\\xbc d=\\xbb\\x06\\x91Q\\xbd\\xe4|F\\xbdp\\x965=|\\t^;\\x1d\\xbf\\xea;\\xa7A\\xb9<\\x98!\\x9f=\\xdd\\xcb)\\xbbE\\xdam=\\x1f\\xf0\\x86=`\\xad\\xa6\\xbc\\xde\\x97\\t<\\x98:[\\xbd\\xe2\\x1f\\xa2=\\x10\\xff\\xa1\\xbc\" \nHSET bikes:20011  model 'ThrillCycle' brand 'BikeShind' price 815 type 'Commuter Bikes' material 'alloy' weight 12.7 description 'An artsy,  retro-inspired bicycle that’s as functional as it is pretty: The ThrillCycle steel frame offers a smooth ride. A 9-speed drivetrain has enough gears for coasting in the city, but we wouldn’t suggest taking it to the mountains. Fenders protect you from mud, and a rear basket lets you transport groceries, flowers and books. The ThrillCycle comes with a limited lifetime warranty, so this little guy will last you long past graduation.' description_embeddings \"\\xc3g\\x16<-B\\xc4:\\xe1\\xbb%\\xbd\\xbf\\x9f\\xb6<q|\\x82\\xbc\\xaf^\\xd8<\\xc5\\xe4\\xae\\xbc\\xf2mF\\xbd\\xd9^\\x14<\\xe0A)\\xbc\\xf73\\xef\\xbc\\x10?\\xe2<!\\xe7\\x03=L\\xe7\\x1b\\xbd\\x83\\xde\\x95=\\xae\\xcfi\\xbda\\xfd\\xa3<\\x196\\xfa\\xbc0lP=U0\\x10\\xbe\\xd4n\\xf2\\xbb\\x12WK\\xbd\\x06\\xecb\\xbb*H\\x00\\xbdU\\xa3\\xa8\\xbbM\\xa9\\xa6\\xbb\\xb9wF9\\x91\\xd7\\xd0\\xbcz\\xb2\\x9d;?;\\xa1=\\xb2,h;\\x9a\\x96T\\xbd\\xc8\\xab\\x02=\\xaf@\\xe0<\\x1a\\x14\\xe2\\xbb\\xfe!\\x1c\\xbd4\\xd4\\xbb<a^\\x90\\xbcl\\x0f%=\\\"\\xc55\\xbc\\x01 e\\xbci,\\x89<m\\x1f\\xb7\\xbc`^@<\\x82P@\\xbb\\xac;;\\xbds\\xffY\\xbb\\xf3\\xca[\\xbd\\xbb\\xfc\\x04\\xbc\\xd3\\xe5F\\xbc\\x13\\x02g\\xbd\\xaaD\\xdb\\xbc\\xb9\\xe8\\xc7\\xbc\\x0fJ\\xe8<\\x17\\xf1A<\\xfd\\x88\\xc9\\xbc[qb;;\\x15\\x91\\xbd\\x93\\xa0\\xd0\\xbb\\x1b-\\x86=|\\xff+=p\\xefk={h\\xb5\\xbdK\\x8b4=Z\\xe1\\xf8<\\xea\\xef\\xe1;U/\\t\\xbd\\x14G\\xb9\\xbd\\xb2&\\x93<\\xd7\\xe1\\xc3\\xbc\\x95.\\xf2\\xbc20B\\xbd]\\xea\\xaa\\xbc\\xe7P8<\\xbde\\xec\\xbc\\xee4\\xee;@6\\x02\\xbd~BK\\xbd\\xbc\\xbeg\\xbd$\\x00I<\\x8e\\x13\\xd9<%\\xeb\\x82\\xbd\\xca\\xd3^\\xbd \\xbe\\x16<0\\xc9\\xd8;\\xb0D\\xe6\\xbc\\x11\\xe9\\xce:v\\xd2\\x05;\\x89\\x08\\x95<\\x9d\\x8e\\x80<\\xba2\\xa1<{\\xe0\\xa4\\xbc\\x92\\x97\\xf1<\\\\=\\xc4<\\x194\\x93\\xbb,\\x17\\x10=\\x05wL;\\xf6\\xaa0<CV\\x80<9\\xe3\\x90\\xbd\\xcb\\xbaU;O\\xc8\\n=\\xf0\\xc4)=1X\\x08\\xbd\\x803\\x9f\\xba<h\\xa4=\\x1d;\\x91=n\\x1f\\xb4\\xbc\\x1e\\xda\\x94\\xbc\\x1e<\\x8d:\\xee\\xc9\\xbf\\xbc<K\\xb0<\\x18\\xf0\\xaa\\xba\\xbdE\\xb5\\xbc\\xf5\\x8aQ\\xbd?\\x85\\x1d=`>\\x03\\xbc\\x04na\\xbc_\\xd4\\xde<\\xec\\xd5\\xcb:\\xd2\\xba\\x06:\\xb61\\x83\\xbb\\x01V#\\xbd\\x98\\xe1\\x90<L\\n\\x97<\\x14\\xbd\\x83\\xbb\\x19\\xb0\\xf7;Ip\\xbb\\xbd\\x83(\\xe0;\\xc8\\x97\\xe3\\xb9i\\xd2\\x05\\xbb\\xa3^\\xc8\\xbd\\x84X\\xa3<-\\xb1\\x02=\\xed\\n\\x14\\xbc(L;=iV!;\\xcc\\xbd\\x91\\xbc\\xee\\xb5\\xf1\\xbc\\xfa\\xd6\\xdf\\xbcX{\\xd0<\\xe7\\xe3\\xc4\\xbb\\xe3ky\\xbc\\xfau\\x1c=e}\\xd9\\xbcZ\\xad&<{7V<\\x99#\\x05=\\xeaA\\x06=lt\\x87<\\xe2,\\x1c=+Y\\xbe<C\\x8a\\x9b\\xba\\x0eH\\n\\xbc\\x7f\\x10\\x84\\xbd\\xe9{D=\\xcc\\x81\\xcf\\xbcn\\xc4\\xd3\\xbb\\x9c\\xe4\\xc2\\xbb)(\\xa8;\\x12\\xc8\\x18=\\xfd\\x83X\\xbd/\\x9e\\xef\\xbaP\\xe3\\x80\\xbdp<\\x98=\\xe1\\xf1\\xb4<\\xf5\\xf4\\x85\\xbc\\x9f\\xf3\\xb2<\\x89%\\x02=\\x068d;\\xd1P\\xdb:\\xe2\\xd9@=Bj\\x00=\\xa41\\xab\\xbc\\xa1\\xbbX<g\\x03\\xab\\xbcs\\x95\\x1d\\xbdyY\\x1e=`\\x00u;\\\"\\x8a\\xda:\\xde\\xa3\\xab\\xbc\\x02\\xcb\\x01=\\x1b_\\xb5<\\x02vP<?_\\xe1<\\x11\\xf2\\x14==_C= \\x998\\xbc\\xf9\\\"\\xa3\\xbb\\xef\\x167\\xbc\\xc0>\\x9d;\\x8d\\x8f\\x06=}\\x9f4\\xbdF6+=\\\\\\xe9S\\xbd\\x9co+=x\\xa3\\x0c\\xbdW\\x8f8\\xbds\\x97\\xaf<\\x9d\\x0e\\xa9\\xbcj\\xae\\xd9;QM8\\xbdz%\\xe2\\xbd\\xf0\\xa9\\x91\\xbcp\\xbc\\x8b\\xbcX=\\xe8<\\xc1n\\xce\\xbc\\x82\\xd3\\xcb<\\t\\xa4\\x12;\\xa34o<\\xcb\\xdfL<\\xcbU\\xa2<)S\\xf7<T\\xf4\\xef\\xbc\\x9a\\x0e\\x15\\xbc]g\\xac\\xbdE\\xd7\\x14;\\x80\\x807\\xbd\\xa4\\x0b\\x95=p\\xd7\\x9b\\xbd\\x12\\xf4\\xdf\\xbbY \\x8e:\\xef\\xbe\\r\\xbb\\xa0\\xe6\\x87;Z\\r\\x83\\xbd\\x8cc \\xbd\\xd2Le\\xba\\xe7\\xf4S;@y\\x87<\\xa8a\\xca\\xbd\\xab/\\xd9:\\x9c\\xd4\\x03<m\\xfek;\\x97\\x96\\x8f<\\xb7GQ=y\\x81\\xda;3\\xfb\\x96\\xbcU+7<\\x9c\\xceO\\xbc\\xc2e\\xfb\\xbc\\xd1$\\x17=\\x12\\xc4\\xa7\\xbc\\x8c\\xaa\\x0b<\\x8c\\xa4V\\xbc\\xee\\xb5\\x98< \\xc2=<\\xa0\\xa3J;l\\xe13=\\xa9Y\\x04\\xbd\\xd8V\\xd3;\\xec\\x81-=R\\xd5\\xc1\\xbd.\\xb3\\xaf<\\xa7>-=\\x94\\xc3\\xd1\\xbd\\xf3\\xf3\\xca<P\\xc7d\\xbd\\x81$\\x11=\\x8c\\xc3\\xd0<]\\xdbd=M\\x993<\\x979\\xcf\\xbc\\xa0JE\\xbc\\x96\\x12\\x94<mq?\\xba\\x04\\xae2=7\\x93\\xe8\\xbbt\\xda-=\\xcc\\xcf\\xf1<\\x9e\\x9d\\x07=\\x19U\\xac<Lo\\\\\\xbc8\\x14\\xa4\\xbd\\x1d\\xc9\\x1b;\\xcdT\\\"\\xbd\\xc9\\xb3\\xf4\\xbbd\\x0b\\x16=\\x90\\x00Z8\\x1b\\xfc\\xb4\\xba\\x1f\\xcfj\\xbd\\x85\\xb8C\\xbc \\rZ\\xbc\\x01\\xd4\\x1b=\\xbe3\\x95<\\x87\\x90\\xc8;\\x9c\\x1eV;\\xd4\\xce\\xf6\\xbc\\xe96\\x85\\xbc\\x12e\\x13\\xbd\\x04N\\x18=ns\\x82\\xbc\\xcf\\xf8\\x86\\xbd\\xbb4#=Xe=\\xbcWq\\x12\\xbd\\x04\\xba-\\xbd\\x1652=0v8\\xbdS\\x85d\\xbb\\xf47\\x91\\xbdy+h\\xbd\\x1d\\xec\\x84\\xbc\\x943O=\\xe5\\xc5\\x13\\xbde\\xff,\\xbd\\xb0W\\x04\\xbc~\\t\\x1f=,\\xbc\\x85<\\x02D\\xae<C\\x8d\\x06\\xbd\\x12QE\\xbd\\xef\\xf7\\x0f\\xbd\\xeb<\\xf1;\\x85{\\xc9\\xbb\\xe1\\xff\\x81\\xbc\\xc1\\x9f\\xdf\\xbc\\xab\\xe5\\xab\\xbb\\x00\\xbe\\xba=v\\x1a\\xce\\xbc%\\x0c\\xc4\\xbb\\xc4_\\x0c\\xbd\\xba\\xf4\\xc1\\xbc\\xa34~\\xbb\\x19\\xf0 \\xbd\\x1b\\xaeH<B\\x9f \\xbd\\xfa`\\xe4<\\xe9\\xe4\\x8a\\xbdE\\xe1\\xbf<\\x9d4\\xe0<\\x99:\\xfa\\xbc\\x80 \\x8b\\xbd\\x87=k\\xbd!\\xc0\\x7f=\\x8b\\x00\\xcc<\\xc40\\xf4<\\x8e\\x9cF\\xbc\\x97P\\xc1;\\xfe\\x17\\xd1<\\xe7\\xf7\\xb5<\\xa3KM\\xbdn<\\xa2;\\xd7\\xc8\\xb1=Fc*\\xbd\\x0c.\\x8d\\xb9\\xd7<\\xff;\\xd1\\x92q=\\xd3\\x8a\\x9f\\xbc\\x84Z\\xc8= A.:\\x95\\x84\\xad\\xbcrf$\\xbdY9\\\\=3\\xdb\\xc3\\xbcn\\x06\\xde\\xbc\\xb7J$<\\xd4\\x80\\x15\\xbc<M\\x07\\xbd<\\x94\\xdc\\xbc\\x85A\\xa5\\xbdX\\xe5\\x98\\xbb\\x17\\x14i=c\\xe9\\x9e=\\xad\\xfaP=\\xd6\\xc7\\x8d;\\xe9\\xe5\\xf1\\xbc\\xbd\\x1a\\xc98\\xfd\\x17\\xed<=\\n\\x95\\xbd\\x06\\x9d\\xb0\\xbc\\xfc\\xf6p;\\x00\\xa1\\xbf\\xba\\xa49\\x19=O\\x81\\xbd\\xbb\\xa2{\\xbb<\\x14\\xd6\\x08=\\x10\\x86\\x9b\\xbc\\x81O<\\xbdL\\xda =\\xd3Pq<\\xbe\\xe8\\xc0\\xbc\\xc8\\xfc\\xe4\\xbc\\x96\\x0b\\x95<\\xf6>j\\xbbs\\x9cR\\xbd\\x9a%\\xc0\\xbd\\xff\\xf5\\xf6<\\xdb\\xc3\\x13=\\xe5\\xc2\\x1a=\\x9c\\xa8\\\"\\xbd\\xf5\\x0b9\\xbd\\xa5t\\xe7=\\x94\\xc5\\xcf=R\\x00\\xad;\\x1c.2;\\x14 \\xaf\\xbc\\xbf\\xb1\\x81\\xbd\\xc0e\\x06\\xbd\\x1bT\\xc4<\\x8b\\xc0\\x8b\\xbd\\xab\\xc6\\x9f\\xbc\\x10\\x92\\x0f\\xbdUP6=&P\\xd6<\\x0c\\xef\\n<\\x88\\x83\\xc7<\\xbar\\xf9<\\x9cl\\x95;\\x9d\\x0c\\\"=e*\\xc4;!\\xcd\\x12=\\xac\\xae,=\\xd1:\\x05=0\\xf2|;S\\xe4\\xff\\xbb\\xa9M;\\xbc_\\x899\\xbd\\xf4\\xbe4\\xbd9\\xb1\\xda\\xbb\\x91%\\xc5<<\\x9e\\xad\\xbc\\xed\\x80S\\xbdbHp<\\xe5\\x91\\xdc;h\\x9b\\x12\\xbbv$#<\\x10\\xd0f\\xbdocU=d\\x17\\xe1\\xbc\\\"WC\\xbd\\xed\\xdb \\xbcJ!\\xa4<\\xb4\\x7f5\\xbd\\xb9\\x00\\x98\\xbdAr\\x96\\xbd\\xe6\\x18\\xb6=:\\xf1\\xb9<a\\x16F=\\x8d\\xe3\\xab\\xbb\\x05\\x07\\xcb<\\x83\\xb2\\xf3\\xbc^\\x8f\\xa7\\xbc\\x89\\x86\\xae\\xbb\\x8d@\\x00\\xbdO\\xeb\\xd3;qR\\xdf<\\xa2\\x14\\xd4=\\\";:\\xbb5\\xa5f\\xbd\\x8ey\\x8f<P\\xad\\x84\\xbd\\x95,\\x07\\xbd\\x1a\\\\\\x80\\xbd\\x19\\xcdJ\\xbd\\\"\\xc0\\xa6\\xbc\\xf8\\\"\\xe1\\xb9\\x19\\xaa.\\xbc+\\xb1\\x1f<\\x95\\xcf)<@\\xd9\\xfa<\\x9e\\xa3;=+\\xea\\xd4;\\x0e*R=|\\xe2\\x17\\xbdb\\xf4D\\xbc\\x0f\\xce\\xc1<o\\x17\\\\\\xbc\\x10$\\x81\\xbdM\\xf8\\xe6<\\x8c\\x17\\xff<\\x16\\xee\\r\\xbd\\x83e\\x1c=\\xaa\\xb8\\xbc\\xbc\\x1b}\\x18\\xbc\\x07\\x96\\xe4;\\n\\xf2.=@)\\xff;9\\xea2=\\x9b\\xa0\\xe1<&\\xf8\\xb0\\xbc\\xe1,.<\\xb1q\\xb9\\xbc\\x15o\\x0c:\\xea<\\x1c=\\xf3\\xd5\\xca\\xbc\\xf1\\xd8(\\xbd>@\\xf9\\xbb\\xb7\\xa62=\\x9a\\x03~<\\x16[\\x99\\xbc\\x806\\x87\\xbd\\x80\\xcf==d\\xf9.:\\xd8\\xfd\\xb9<\\x9a\\xea{\\t\\xf9\\xads\\xbcx\\x93\\x14=\\xcbB1\\xbc/\\xf8\\xf5:\\x03\\\"\\xd3<\\\"\\x912\\xbc\\x00+\\xf1;\\xee\\xc4\\xf67\\xd1\\xf4\\x14=\\xe8\\xbcR\\xbd\\xc6\\xb9\\r=z$\\xd0;\\xea\\x99A\\xbd\\x91d\\x06=f1\\x11\\xbd\\xd0_+:\\x14J\\x96\\xbc\\x87\\x87\\xc6\\xbce\\x13.<c\\x04\\xf0<\\x9c\\x0b\\xc4\\xb82\\xd6\\x91;\\x13\\xa3F;\\xfc\\xbb&\\xbd(`\\xc8\\xbb\\x99\\xa7\\xed\\xbc\\x8c\\x8c\\xfc\\xbcl\\xfa\\xa5<\\xe6*\\x8f<T\\x10\\x86<\\x92\\xdd#<a\\x18\\xf4<\\xb7\\xb0(\\xbb\\\"\\xdfK\\xbd\\xe3!#\\xbdq\\xb5\\x15\\xbc\\xca|\\xb7\\xba\\xf7\\x9fm<\\x83\\x95\\xd3<k\\xba\\xca\\xbc-\\x03\\x02<m\\x85\\x16\\xbc!?K<p\\x8e\\xdd<\\xbd\\xcaW:<3@\\xbc!vO=N,*\\xbdD\\x94\\xa1\\xbbhR\\xf8\\xbcy\\xc7K\\xbb\\xe3K\\x9d\\xbd~qV=\\xd3\\xaa\\x00=\\ng\\x07=\\xc63\\xa3=?\\x11\\x0e=\\xba\\xa7L;\\xef\\xfb\\x1a<\\xd2Gl:\\xa2\\xd6\\x8f;$t\\xff<H\\x15\\x1f;\\xd5\\xb8\\xe1\\xbb$7g\\xbd\\x854\\xe8\\xbc\\xd8z\\x07=k\\xd5\\x14\\xbd\\xea\\xd0\\xc4\\xbb\\xa1x\\xaa<\\x03\\tt=\\\"\\xc8|\\xbb/\\xae\\xfc<\\xd5\\x8e\\xce<\\x96\\xb6\\xb5;\\xa2\\xa1\\x81<\\xba\\x98\\xbc;\\\"\\x9e\\xa1=P\\x98z\\xbc\\x0c\\xbb\\xdd;W2\\x05\\xbd\\xe4}\\\\<\\xbas0\\xbd\\xd1\\xef\\x01>?\\xc2\\xf1\\xbc\\x8d\\xf6\\xf2<\\x97hf<\\xef7\\xf2;\\x0e\\xfd\\xbe\\xbc\\xd9\\x88\\xd0\\xbc\\xfc\\xc8\\xc4<B\\x0c\\xe2<\\xaex\\xc2<d\\xf1\\x06;Z\\x19\\xc9<F\\x11\\xb9<\\xab\\xfc\\xd5\\xbcBO\\x9a\\xbcsM\\xb8\\xbb\\xf6l5\\xbc0]\\x1d<\\x86-\\xf7<\\xca\\xf2*= h\\xdf\\xbb\\xacg\\xf2<P\\xf0O;u`\\x82<j\\xd7\\x07\\xbc)\\x93\\xa4\\xbd;\\x96\\xc3\\xbd$\\x9c\\xc6<2\\xad><\\xe7D\\xc6\\xbb5\\x0b\\x9e=\\xc8\\xac\\xcb<+\\x06\\n=\\xf1\\x89\\x93<X\\x97&\\xbd\\x87N\\x98\\xbbsZ\\xcd\\xbc\\xd2\\x9f\\xa4<k\\xca\\x9d<\\xa8\\x00\\x11<\\xa2Ob\\xbc\\xf3-\\x9c;\\xf3\\t-=\\xe0@\\xb1\\xbb\\xe2\\xc4\\xa3;~\\xc5\\xf9=\\xe2\\xf8\\xc4\\xbc\\xcc\\xb8\\x88\\xbc\\xe0\\xeb\\xcc\\xbdW\\x94~=\\x0c\\xd9\\t=\\xe8\\xc8c<\\x95FR9zSJ=\\x15Go\\xbd[\\xe3\\xb8;7\\xf57\\xbc>\\x0b*=\\xea\\xc5\\xd5\\xbb\\xf3\\xc5\\\\\\xbc\\xd2\\x0eT<\\x9a\\xc7\\x1e:\\xbf\\xccE<\\x00\\xf5\\xe6<\\x94\\x8a\\xf5\\xbbxl\\xa7<\\xb2A\\\"\\xbc\\xc9\\xdb\\x02\\xbc\\xdf\\x7f\\x12=.:s\\xbc\\xc7\\\"v<~v\\x83\\xbb2iF\\xbc}\\xd5\\x1a\\xbd\\x10\\\"\\xba\\xbc^\\x92\\x1a\\xbc\\x11\\x02\\xcf<4:1<\\x86\\x1b\\xae=\\x83\\x19\\xf0<W)\\xd8<t\\xb3\\xa5\\xbcc\\xa11\\xba\\xce\\x8e%\\xbdm\\x99\\xb5<\\xe0\\xb2~=daj\\xbc\\xbdu\\x17=%\\xc9\\xea;!\\xda\\x1a\\xbdjO\\xc0\\xbb\\xea\\x91(=\\x1a\\\\\\x04=9\\xb5\\xd8<G\\x8f{=\\x13G%\\xbd^\\x08\\xa0;\\x90\\x1b\\\"\\xbd\\x9eDY\\xbc+{!\\xbd?E \\xbd\\xbd*o\\xbd\\xe1\\x12\\xd1\\xbc\\t\\xc6\\x04=zK\\x00\\xbd\\x0b\\x03,=\\xeaU\\xbc;4\\x15\\xc5;\\xf6\\x99b\\xbbR\\xe7L\\xbct\\x92\\r\\xbdE\\xcb`\\xbd\\x81gt\\xbd\\x88i \\xbdX\\x8a`=\\xd5\\xaaY\\xbd\\xa3\\xd7}=&N\\n\\xbd\\xe6jZ<\\xc0:\\x19\\xbc\\xfe\\xed\\x87\\xbdE\\xc0)<\\xba\\xc7\\xa9\\xbb\\xe1\\xb0\\xef<`t\\xa3\\xbd\\x90\\x06\\x1d=\\xb1\\xcc\\x81\\xbb\\r\\xe8\\x02<}Ar;+\\x92\\x00=V/t:6\\xde\\x1c\\xbc_\\xb8\\x1c\\xbd3\\xef\\x8c<D\\xaak\\xbc\\xf1\\\\28\\x0f\\x00&\\xbd\\xb4G\\xc0;f\\x06\\x0f\\xbdP\\x8d\\x8e<\\x88\\xcb\\x0e=2\\xc5!=S\\x94\\xe0<K)\\x7f\\xb8]a\\xd5\\xbc\\xdf\\x03\\xa1\\xbcF\\x08\\xa4=\\xaeB\\x98\\xba\\x9eM\\x02\\xbc\\xad8\\xf1\\xbc\\t\\x1cf;\\xe8\\xb3\\xef<\\xd6\\xee\\xa1\\xbc\\xd5\\x9b =\\xd9:\\xed\\xbc\\xf5f\\x88=k[L=\\x1e+\\xe5\\xbc\\xf1B\\xdf\\xbbq)T=\\xbb-\\xd6\\xbb\\xdd\\xcfP;\\xd4\\xa1\\xd7\\xbb\\x92\\x00\\xbb\\xbb\\x8a\\xb5\\x89=\\xe0\\xea\\x8d\\xbd\\x92\\x0e7<\\xca\\xd6\\x16=`T\\x8a<X)y\\xbc6\\xdb\\xbe=\\x9d\\xb2\\x11;\\xe4,\\xa3\\xbbjUn\\xbc\\xbc\\xec\\x0e\\xbdA\\\"P\\xbc\\xc0\\xac\\xb1\\xbc\\x00\\xef\\x05<\\x10)\\x1b=\\x87\\x10\\\"=\\t7\\x9d=\\x84x\\x97<\\xc3\\xd7\\xfb\\xbb\\xda\\x93\\t=\\x05\\xf4\\x01=\\xe5m\\x8d<h]o<hn\\xc3;\\xcf\\xea\\xdf<\\xc6_\\x03\\xbd\"\n"
  },
  {
    "path": "redisinsight/api/data/vector-collections/movies",
    "content": "JSON.SET movie:001 $ '{\"title\":\"Toy Story\",\"genres\":[\"Animation\",\"Comedy\",\"Family\"],\"plot\":\"Toys come to life when humans arent around.\",\"year\":1995,\"embedding\":[0.22,0.04,0.33,0.12,-0.02,0.17,0.09,0.01]}'\nJSON.SET movie:002 $ '{\"title\":\"Inside Out\",\"genres\":[\"Animation\",\"Comedy\",\"Drama\"],\"plot\":\"Emotions guide a young girl through change.\",\"year\":2015,\"embedding\":[0.20,0.03,0.31,0.11,-0.03,0.16,0.08,0.02]}'\nJSON.SET movie:003 $ '{\"title\":\"Whiplash\",\"genres\":[\"Drama\",\"Music\"],\"plot\":\"A young drummer is pushed to greatness.\",\"year\":2014,\"embedding\":[0.14,0.01,0.22,0.08,-0.07,0.10,0.04,0.00]}'\nJSON.SET movie:004 $ '{\"title\":\"La La Land\",\"genres\":[\"Drama\",\"Music\",\"Romance\"],\"plot\":\"A jazz musician falls in love in LA.\",\"year\":2016,\"embedding\":[0.15,0.03,0.23,0.09,-0.08,0.14,0.06,0.01]}'\nJSON.SET movie:005 $ '{\"title\":\"The Matrix\",\"genres\":[\"Action\",\"Sci-Fi\"],\"plot\":\"A hacker discovers reality is a simulation.\",\"year\":1999,\"embedding\":[0.12,-0.03,0.25,0.04,-0.10,0.09,0.05,-0.02]}'\nJSON.SET movie:006 $ '{\"title\":\"Inception\",\"genres\":[\"Action\",\"Adventure\",\"Sci-Fi\"],\"plot\":\"A thief steals information through dreams.\",\"year\":2010,\"embedding\":[0.14,-0.01,0.27,0.06,-0.09,0.10,0.04,-0.03]}'\nJSON.SET movie:007 $ '{\"title\":\"Tenet\",\"genres\":[\"Action\",\"Sci-Fi\",\"Thriller\"],\"plot\":\"Time-inversion to prevent World War III.\",\"year\":2020,\"embedding\":[0.13,-0.06,0.29,0.05,-0.11,0.12,0.06,-0.01]}'\nJSON.SET movie:008 $ '{\"title\":\"Finding Nemo\",\"genres\":[\"Animation\",\"Adventure\",\"Family\"],\"plot\":\"A clownfish searches for his son.\",\"year\":2003,\"embedding\":[0.18,0.02,0.30,0.10,-0.05,0.15,0.07,0.01]}'\nJSON.SET movie:009 $ '{\"title\":\"Coco\",\"genres\":[\"Animation\",\"Family\",\"Music\"],\"plot\":\"A boy enters the Land of the Dead.\",\"year\":2017,\"embedding\":[0.21,0.04,0.34,0.13,-0.02,0.19,0.10,0.02]}'\nJSON.SET movie:010 $ '{\"title\":\"Soul\",\"genres\":[\"Animation\",\"Adventure\",\"Comedy\"],\"plot\":\"A jazz musician explores the afterlife.\",\"year\":2020,\"embedding\":[0.16,0.02,0.28,0.10,-0.06,0.13,0.07,0.00]}'\nJSON.SET movie:011 $ '{\"title\":\"The Dark Knight\",\"genres\":[\"Action\",\"Crime\",\"Drama\"],\"plot\":\"Batman fights the Joker.\",\"year\":2008,\"embedding\":[0.12,-0.03,0.25,0.04,-0.09,0.10,0.05,-0.02]}'\nJSON.SET movie:012 $ '{\"title\":\"Frozen\",\"genres\":[\"Animation\",\"Adventure\",\"Comedy\"],\"plot\":\"A princess sets off to find her sister.\",\"year\":2013,\"embedding\":[0.22,0.04,0.33,0.12,-0.03,0.18,0.10,0.02]}'\nJSON.SET movie:013 $ '{\"title\":\"The Lion King\",\"genres\":[\"Animation\",\"Adventure\",\"Drama\"],\"plot\":\"A lion prince flees and returns.\",\"year\":1994,\"embedding\":[0.19,0.02,0.32,0.10,-0.04,0.18,0.09,0.03]}'\nJSON.SET movie:014 $ '{\"title\":\"Shrek\",\"genres\":[\"Animation\",\"Adventure\",\"Comedy\"],\"plot\":\"An ogre rescues a princess.\",\"year\":2001,\"embedding\":[0.21,0.03,0.32,0.11,-0.04,0.17,0.09,0.01]}'\nJSON.SET movie:015 $ '{\"title\":\"The Social Network\",\"genres\":[\"Biography\",\"Drama\"],\"plot\":\"The rise of Facebook and its creator.\",\"year\":2010,\"embedding\":[0.10,-0.01,0.21,0.05,-0.07,0.06,0.03,-0.02]}'\nJSON.SET movie:016 $ '{\"title\":\"Guardians of the Galaxy\",\"genres\":[\"Action\",\"Adventure\",\"Sci-Fi\"],\"plot\":\"A group of intergalactic criminals must save the universe.\",\"year\":2014,\"embedding\":[0.13,0.00,0.28,0.07,-0.08,0.11,0.05,-0.01]}'\nJSON.SET movie:017 $ '{\"title\":\"Moana\",\"genres\":[\"Animation\",\"Adventure\",\"Family\"],\"plot\":\"A young girl sets sail to save her island.\",\"year\":2016,\"embedding\":[0.20,0.03,0.33,0.12,-0.03,0.17,0.09,0.02]}'\nJSON.SET movie:018 $ '{\"title\":\"Whale Rider\",\"genres\":[\"Drama\",\"Family\"],\"plot\":\"A girl fights tradition to become chief.\",\"year\":2002,\"embedding\":[0.15,0.01,0.25,0.09,-0.05,0.12,0.06,0.00]}'\nJSON.SET movie:019 $ '{\"title\":\"Rocketman\",\"genres\":[\"Biography\",\"Drama\",\"Music\"],\"plot\":\"The story of Elton Johns breakthrough years.\",\"year\":2019,\"embedding\":[0.14,0.01,0.22,0.07,-0.06,0.11,0.05,0.01]}'\nJSON.SET movie:020 $ '{\"title\":\"Amadeus\",\"genres\":[\"Biography\",\"Drama\",\"Music\"],\"plot\":\"The rivalry between Mozart and Salieri.\",\"year\":1984,\"embedding\":[0.13,0.00,0.20,0.06,-0.07,0.10,0.04,0.00]}'\nJSON.SET movie:021 $ '{\"title\":\"The Sound of Music\",\"genres\":[\"Biography\",\"Drama\",\"Music\"],\"plot\":\"A governess brings music to a family.\",\"year\":1965,\"embedding\":[0.14,0.02,0.21,0.07,-0.06,0.11,0.05,0.01]}'\nJSON.SET movie:022 $ '{\"title\":\"Les Miserables\",\"genres\":[\"Drama\",\"Music\",\"Romance\"],\"plot\":\"The struggles of ex-convict Jean Valjean.\",\"year\":2012,\"embedding\":[0.13,0.01,0.23,0.08,-0.07,0.12,0.05,0.01]}'\nJSON.SET movie:023 $ '{\"title\":\"The Greatest Showman\",\"genres\":[\"Biography\",\"Drama\",\"Music\"],\"plot\":\"The story of P.T. Barnum and his circus.\",\"year\":2017,\"embedding\":[0.15,0.03,0.25,0.09,-0.05,0.13,0.06,0.02]}'\nJSON.SET movie:024 $ '{\"title\":\"A Star Is Born\",\"genres\":[\"Drama\",\"Music\",\"Romance\"],\"plot\":\"A musician helps a young singer find fame.\",\"year\":2018,\"embedding\":[0.14,0.02,0.24,0.08,-0.06,0.12,0.05,0.01]}'\nJSON.SET movie:025 $ '{\"title\":\"Mad Max: Fury Road\",\"genres\":[\"Action\",\"Adventure\",\"Sci-Fi\"],\"plot\":\"In a post-apocalyptic wasteland, Max helps rebels escape.\",\"year\":2015,\"embedding\":[0.11,-0.02,0.26,0.05,-0.10,0.08,0.05,-0.02]}'\nJSON.SET movie:026 $ '{\"title\":\"Blade Runner 2049\",\"genres\":[\"Sci-Fi\",\"Thriller\"],\"plot\":\"A new blade runner uncovers secrets.\",\"year\":2017,\"embedding\":[0.12,-0.03,0.27,0.06,-0.09,0.09,0.06,-0.01]}'\nJSON.SET movie:027 $ '{\"title\":\"Arrival\",\"genres\":[\"Drama\",\"Sci-Fi\",\"Thriller\"],\"plot\":\"A linguist communicates with aliens.\",\"year\":2016,\"embedding\":[0.13,-0.01,0.28,0.07,-0.08,0.11,0.05,-0.01]}'\nJSON.SET movie:028 $ '{\"title\":\"Interstellar\",\"genres\":[\"Adventure\",\"Drama\",\"Sci-Fi\"],\"plot\":\"Explorers travel through a wormhole in space.\",\"year\":2014,\"embedding\":[0.14,-0.02,0.29,0.08,-0.09,0.12,0.06,-0.02]}'\nJSON.SET movie:029 $ '{\"title\":\"E.T. the Extra-Terrestrial\",\"genres\":[\"Family\",\"Sci-Fi\"],\"plot\":\"A boy befriends an alien.\",\"year\":1982,\"embedding\":[0.17,0.01,0.31,0.10,-0.06,0.15,0.07,0.01]}'\nJSON.SET movie:030 $ '{\"title\":\"The Avengers\",\"genres\":[\"Action\",\"Adventure\",\"Sci-Fi\"],\"plot\":\"Superheroes team up to save the world.\",\"year\":2012,\"embedding\":[0.13,0.00,0.27,0.07,-0.08,0.11,0.06,-0.01]}'\nJSON.SET movie:031 $ '{\"title\":\"Guardians of the Galaxy Vol. 2\",\"genres\":[\"Action\",\"Adventure\",\"Comedy\"],\"plot\":\"The Guardians fight to protect the galaxy.\",\"year\":2017,\"embedding\":[0.15,0.01,0.28,0.09,-0.07,0.13,0.07,0.01]}'\nJSON.SET movie:032 $ '{\"title\":\"Up\",\"genres\":[\"Animation\",\"Adventure\",\"Comedy\"],\"plot\":\"An old man goes on an adventure in his flying house.\",\"year\":2009,\"embedding\":[0.21,0.04,0.32,0.11,-0.04,0.16,0.09,0.02]}'\nJSON.SET movie:033 $ '{\"title\":\"Zootopia\",\"genres\":[\"Animation\",\"Adventure\",\"Comedy\"],\"plot\":\"A bunny cop solves a mystery in a city of animals.\",\"year\":2016,\"embedding\":[0.20,0.03,0.31,0.10,-0.05,0.15,0.08,0.01]}'\nJSON.SET movie:034 $ '{\"title\":\"Big Hero 6\",\"genres\":[\"Animation\",\"Action\",\"Comedy\"],\"plot\":\"A robotics prodigy teams with friends to fight crime.\",\"year\":2014,\"embedding\":[0.19,0.02,0.30,0.09,-0.05,0.14,0.08,0.01]}'\nJSON.SET movie:035 $ '{\"title\":\"The Prestige\",\"genres\":[\"Drama\",\"Mystery\",\"Sci-Fi\"],\"plot\":\"Two magicians engage in a deadly rivalry.\",\"year\":2006,\"embedding\":[0.12,-0.02,0.24,0.06,-0.08,0.10,0.05,-0.01]}'\nJSON.SET movie:036 $ '{\"title\":\"Dunkirk\",\"genres\":[\"Action\",\"Drama\",\"History\"],\"plot\":\"Allied soldiers are evacuated during WWII.\",\"year\":2017,\"embedding\":[0.10,-0.03,0.22,0.05,-0.09,0.07,0.04,-0.02]}'\nJSON.SET movie:037 $ '{\"title\":\"Jumanji: Welcome to the Jungle\",\"genres\":[\"Action\",\"Adventure\",\"Comedy\"],\"plot\":\"Teens trapped in a video game jungle.\",\"year\":2017,\"embedding\":[0.16,0.01,0.27,0.08,-0.06,0.12,0.06,0.01]}'\nJSON.SET movie:038 $ '{\"title\":\"Cinderella\",\"genres\":[\"Animation\",\"Family\",\"Fantasy\"],\"plot\":\"A young girl overcomes her cruel stepmother.\",\"year\":1950,\"embedding\":[0.19,0.03,0.31,0.11,-0.04,0.16,0.08,0.02]}'\nJSON.SET movie:039 $ '{\"title\":\"Mulan\",\"genres\":[\"Animation\",\"Adventure\",\"Drama\"],\"plot\":\"A young woman disguises as a soldier.\",\"year\":1998,\"embedding\":[0.20,0.03,0.32,0.11,-0.04,0.17,0.09,0.02]}'\nJSON.SET movie:040 $ '{\"title\":\"Beauty and the Beast\",\"genres\":[\"Animation\",\"Family\",\"Fantasy\"],\"plot\":\"A young woman falls in love with a beast.\",\"year\":1991,\"embedding\":[0.18,0.02,0.30,0.10,-0.05,0.15,0.08,0.01]}'\nJSON.SET movie:041 $ '{\"title\":\"The Godfather\",\"genres\":[\"Crime\",\"Drama\"],\"plot\":\"The aging patriarch of an organized crime dynasty transfers control to his son.\",\"year\":1972,\"embedding\":[0.11,-0.04,0.24,0.06,-0.10,0.07,0.05,-0.03]}'\nJSON.SET movie:042 $ '{\"title\":\"Pulp Fiction\",\"genres\":[\"Crime\",\"Drama\"],\"plot\":\"The lives of two mob hitmen, a boxer, and others intertwine.\",\"year\":1994,\"embedding\":[0.12,-0.03,0.23,0.07,-0.09,0.09,0.04,-0.01]}'\nJSON.SET movie:043 $ '{\"title\":\"Forrest Gump\",\"genres\":[\"Drama\",\"Romance\"],\"plot\":\"The presidencies of Kennedy and Johnson through the eyes of Forrest.\",\"year\":1994,\"embedding\":[0.14,0.01,0.26,0.08,-0.07,0.11,0.06,0.01]}'\nJSON.SET movie:044 $ '{\"title\":\"Gladiator\",\"genres\":[\"Action\",\"Drama\"],\"plot\":\"A former Roman General seeks revenge.\",\"year\":2000,\"embedding\":[0.13,0.00,0.25,0.07,-0.08,0.10,0.05,0.00]}'\nJSON.SET movie:045 $ '{\"title\":\"Titanic\",\"genres\":[\"Drama\",\"Romance\"],\"plot\":\"A seventeen-year-old aristocrat falls in love with a kind but poor artist.\",\"year\":1997,\"embedding\":[0.15,0.02,0.28,0.09,-0.06,0.13,0.06,0.01]}'\nJSON.SET movie:046 $ '{\"title\":\"Jurassic Park\",\"genres\":[\"Adventure\",\"Sci-Fi\"],\"plot\":\"Scientists clone dinosaurs for a theme park.\",\"year\":1993,\"embedding\":[0.14,-0.01,0.26,0.08,-0.07,0.11,0.06,0.00]}'\nJSON.SET movie:047 $ '{\"title\":\"The Shawshank Redemption\",\"genres\":[\"Drama\"],\"plot\":\"Two imprisoned men bond over a number of years.\",\"year\":1994,\"embedding\":[0.15,0.00,0.27,0.09,-0.06,0.12,0.07,0.01]}'\nJSON.SET movie:048 $ '{\"title\":\"Fight Club\",\"genres\":[\"Drama\"],\"plot\":\"An insomniac and a soap maker form an underground fight club.\",\"year\":1999,\"embedding\":[0.13,-0.02,0.24,0.07,-0.08,0.10,0.05,-0.01]}'\nJSON.SET movie:049 $ '{\"title\":\"The Silence of the Lambs\",\"genres\":[\"Crime\",\"Drama\",\"Thriller\"],\"plot\":\"A young FBI cadet seeks help from an imprisoned cannibal.\",\"year\":1991,\"embedding\":[0.11,-0.03,0.22,0.06,-0.09,0.08,0.04,-0.02]}'\nJSON.SET movie:050 $ '{\"title\":\"The Departed\",\"genres\":[\"Crime\",\"Drama\",\"Thriller\"],\"plot\":\"An undercover cop and a mole in the police attempt to identify each other.\",\"year\":2006,\"embedding\":[0.12,-0.02,0.23,0.07,-0.08,0.09,0.05,-0.01]}'\nJSON.SET movie:51 $ '{\"title\":\"Saturday Night Fever\",\"year\":1977,\"genres\":[\"Drama\",\"Music\"],\"plot\":\"A young man finds escape from his mundane life through disco dancing.\",\"embedding\":[0.154,-0.050,0.300,0.150,-0.150,0.154,0.034,-0.118]}'\nJSON.SET movie:52 $ '{\"title\":\"The Rose\",\"year\":1979,\"genres\":[\"Music\",\"Drama\"],\"plot\":\"A rock star struggles with fame, addiction, and love.\",\"embedding\":[0.144,-0.055,0.295,0.145,-0.160,0.150,0.030,-0.120]}'\nJSON.SET movie:53 $ '{\"title\":\"Cabaret\",\"year\":1972,\"genres\":[\"Drama\",\"Music\"],\"plot\":\"A performer and a writer navigate love and politics in pre-WWII Berlin.\",\"embedding\":[0.151,-0.052,0.297,0.148,-0.152,0.150,0.031,-0.121]}'\nJSON.SET movie:54 $ '{\"title\":\"Tommy\",\"year\":1975,\"genres\":[\"Drama\",\"Music\",\"Fantasy\"],\"plot\":\"A deaf and blind boy becomes a pinball champion and religious figure.\",\"embedding\":[0.149,-0.051,0.301,0.149,-0.151,0.153,0.033,-0.119]}'\nJSON.SET movie:55 $ '{\"title\":\"All That Jazz\",\"year\":1979,\"genres\":[\"Drama\",\"Music\"],\"plot\":\"A choreographer reflects on his life and art while facing death.\",\"embedding\":[0.153,-0.049,0.299,0.151,-0.149,0.152,0.032,-0.117]}'"
  },
  {
    "path": "redisinsight/api/esbuild.js",
    "content": "const esbuild = require('esbuild');\nconst fs = require('fs-extra');\nconst path = require('path');\nrequire('dotenv').config();\nconst { dependencies } = require('../package.json');\n\nconst production = process.argv.includes('--production');\nconst watch = process.argv.includes('--watch');\n\nconst outDir = 'dist-minified';\nconst define = {\n  'process.env.NODE_ENV': JSON.stringify(\n    production ? 'production' : 'development',\n  ),\n};\n\nconst external = [\n  '@nestjs/microservices',\n  '@fastify/static',\n  'class-transformer',\n  // packages with binaries\n  ...Object.keys(dependencies),\n];\n\nasync function main() {\n  const ctx = await esbuild.context({\n    entryPoints: ['dist/src/main.js'],\n    bundle: true,\n    format: 'cjs',\n    minifyWhitespace: true,\n    // if true - some nestjs decorators are not working\n    minifyIdentifiers: false,\n    sourcemap: !production,\n    sourcesContent: false,\n    platform: 'node',\n    outfile: `${outDir}/main.js`,\n    define,\n    external,\n    logLevel: 'silent',\n    plugins: [\n      /* add to the end of plugins array */\n      // eslint-disable-next-line @typescript-eslint/no-use-before-define\n      esbuildProblemMatcherPlugin,\n    ],\n  });\n  if (watch) {\n    await ctx.watch();\n  } else {\n    await ctx.rebuild();\n    await ctx.dispose();\n  }\n}\n\nfunction copySource(source, destination) {\n  try {\n    if (fs.pathExistsSync(source)) {\n      fs.copySync(source, destination);\n    }\n  } catch (error) {\n    console.error('✘ [esbuild ERROR copySource]', error);\n  }\n}\n\n/**\n * @type {import('esbuild').Plugin}\n */\nconst esbuildProblemMatcherPlugin = {\n  name: 'esbuild-problem-matcher',\n\n  setup(build) {\n    build.onStart(() => {\n      console.debug('[esbuild] build started');\n    });\n    build.onEnd((result) => {\n      result.errors.forEach(({ text, location }) => {\n        console.error(`✘ [esbuild ERROR] ${text}`);\n        console.error(\n          `    ${location.file}:${location.line}:${location.column}:`,\n        );\n      });\n      console.debug('[esbuild] build finished');\n\n      copySource(\n        path.resolve(__dirname, 'defaults'),\n        path.resolve(__dirname, outDir, 'defaults'),\n      );\n      console.debug('[esbuild] copied \"defaults\" folder');\n    });\n  },\n};\n\nmain().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "redisinsight/api/migration/1614164490968-initial-migration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class initialMigration1614164490968 implements MigrationInterface {\n  name = 'initialMigration1614164490968';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"certFilename\" varchar NOT NULL, \"keyFilename\" varchar NOT NULL, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar)`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"filename\" varchar NOT NULL, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n    await queryRunner.query(`DROP TABLE \"ca_certificate\"`);\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(`DROP TABLE \"client_certificate\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1615480887019-connection-type.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class connectionType1615480887019 implements MigrationInterface {\n  name = 'connectionType1615480887019';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1615990079125-database-name-from-provider.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseNameFromProvider1615990079125\n  implements MigrationInterface\n{\n  name = 'databaseNameFromProvider1615990079125';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"type\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1615992183565-remove-database-type.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class removeDatabaseType1615992183565 implements MigrationInterface {\n  name = 'removeDatabaseType1615992183565';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"type\" varchar NOT NULL DEFAULT ('Redis Database'), \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1616520395940-oss-sentinel.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class ossSentinel1616520395940 implements MigrationInterface {\n  name = 'ossSentinel1616520395940';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1625771635418-agreements.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class agreements1625771635418 implements MigrationInterface {\n  name = 'agreements1625771635418';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"agreements\" (\"id\" integer PRIMARY KEY AUTOINCREMENT NOT NULL, \"version\" varchar, \"data\" varchar)`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"agreements\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1626086601057-server-info.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class serverInfo1626086601057 implements MigrationInterface {\n  name = 'serverInfo1626086601057';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"server\" (\"id\" varchar PRIMARY KEY NOT NULL, \"createDateTime\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"server\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1626904405170-database-hosting-provider.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseHostingProvider1626904405170\n  implements MigrationInterface\n{\n  name = 'databaseHostingProvider1626904405170';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1627556171227-settings.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class settings1627556171227 implements MigrationInterface {\n  name = 'settings1627556171227';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"settings\" (\"id\" integer PRIMARY KEY AUTOINCREMENT NOT NULL, \"data\" varchar)`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"settings\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1629729923740-database-modules.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseModules1629729923740 implements MigrationInterface {\n  name = 'databaseModules1629729923740';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1634219846022-database-db-index.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseDbIndex1634219846022 implements MigrationInterface {\n  name = 'databaseDbIndex1634219846022';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1634557312500-encryption.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class encryption1634557312500 implements MigrationInterface {\n  name = 'encryption1634557312500';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_client_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"client_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"client_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_client_certificate\" RENAME TO \"client_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_ca_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"ca_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"ca_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_ca_certificate\" RENAME TO \"ca_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"encryption\" varchar, \"certificate\" varchar, \"key\" varchar, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_client_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"client_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"client_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_client_certificate\" RENAME TO \"client_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"encryption\" varchar, \"certificate\" varchar, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_ca_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"ca_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"ca_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_ca_certificate\" RENAME TO \"ca_certificate\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"ca_certificate\" RENAME TO \"temporary_ca_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"ca_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"temporary_ca_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_ca_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"client_certificate\" RENAME TO \"temporary_client_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"client_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"temporary_client_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_client_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"ca_certificate\" RENAME TO \"temporary_ca_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"filename\" varchar NOT NULL, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"ca_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"temporary_ca_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_ca_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"client_certificate\" RENAME TO \"temporary_client_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"certFilename\" varchar NOT NULL, \"keyFilename\" varchar NOT NULL, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"client_certificate\"(\"id\", \"name\") SELECT \"id\", \"name\" FROM \"temporary_client_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_client_certificate\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1641795882696-command-execution.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class commandExecution1641795882696 implements MigrationInterface {\n  name = 'commandExecution1641795882696';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\" FROM \"command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_command_execution\" RENAME TO \"command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"command_execution\" RENAME TO \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\" FROM \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_command_execution\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1641805606399-plugin-state.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class pluginState1641805606399 implements MigrationInterface {\n  name = 'pluginState1641805606399';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"plugin_state\" (\"commandExecutionId\" varchar NOT NULL, \"visualizationId\" varchar NOT NULL, \"state\" text NOT NULL, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (\"commandExecutionId\", \"visualizationId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_plugin_state\" (\"commandExecutionId\" varchar NOT NULL, \"visualizationId\" varchar NOT NULL, \"state\" text NOT NULL, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_91b21bec94d7107162e34a03ceb\" FOREIGN KEY (\"commandExecutionId\") REFERENCES \"command_execution\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION, PRIMARY KEY (\"commandExecutionId\", \"visualizationId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_plugin_state\"(\"commandExecutionId\", \"visualizationId\", \"state\", \"encryption\", \"createdAt\", \"updatedAt\") SELECT \"commandExecutionId\", \"visualizationId\", \"state\", \"encryption\", \"createdAt\", \"updatedAt\" FROM \"plugin_state\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"plugin_state\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_plugin_state\" RENAME TO \"plugin_state\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"plugin_state\" RENAME TO \"temporary_plugin_state\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"plugin_state\" (\"commandExecutionId\" varchar NOT NULL, \"visualizationId\" varchar NOT NULL, \"state\" text NOT NULL, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (\"commandExecutionId\", \"visualizationId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"plugin_state\"(\"commandExecutionId\", \"visualizationId\", \"state\", \"encryption\", \"createdAt\", \"updatedAt\") SELECT \"commandExecutionId\", \"visualizationId\", \"state\", \"encryption\", \"createdAt\", \"updatedAt\" FROM \"temporary_plugin_state\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_plugin_state\"`);\n    await queryRunner.query(`DROP TABLE \"plugin_state\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1650278664000-sni.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class sni1650278664000 implements MigrationInterface {\n  name = 'sni1650278664000';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1655821010349-notification.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class notification1655821010349 implements MigrationInterface {\n  name = 'notification1655821010349';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"notification\" (\"type\" varchar CHECK( type IN ('global') ) NOT NULL, \"timestamp\" integer NOT NULL, \"title\" varchar NOT NULL, \"body\" text NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), PRIMARY KEY (\"type\", \"timestamp\"))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"notification\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1659687030433-notification-category.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class notificationCategory1659687030433 implements MigrationInterface {\n  name = 'notificationCategory1659687030433';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_notification\" (\"type\" varchar NOT NULL, \"timestamp\" integer NOT NULL, \"title\" varchar NOT NULL, \"body\" text NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"category\" varchar, \"categoryColor\" varchar, PRIMARY KEY (\"type\", \"timestamp\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_notification\"(\"type\", \"timestamp\", \"title\", \"body\", \"read\") SELECT \"type\", \"timestamp\", \"title\", \"body\", \"read\" FROM \"notification\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"notification\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_notification\" RENAME TO \"notification\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"notification\" RENAME TO \"temporary_notification\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"notification\" (\"type\" varchar NOT NULL, \"timestamp\" integer NOT NULL, \"title\" varchar NOT NULL, \"body\" text NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), PRIMARY KEY (\"type\", \"timestamp\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"notification\"(\"type\", \"timestamp\", \"title\", \"body\", \"read\") SELECT \"type\", \"timestamp\", \"title\", \"body\", \"read\" FROM \"temporary_notification\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_notification\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1660664717573-workbench-mode.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class workbenchMode1660664717573 implements MigrationInterface {\n  name = 'workbenchMode1660664717573';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\" FROM \"command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_command_execution\" RENAME TO \"command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"command_execution\" RENAME TO \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\" FROM \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_command_execution\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1663093411715-workbench-group-mode.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class workbenchGroupMode1663093411715 implements MigrationInterface {\n  name = 'workbenchGroupMode1663093411715';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\" FROM \"command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_command_execution\" RENAME TO \"command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"command_execution\" RENAME TO \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\" FROM \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_command_execution\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1664785208236-database-analysis.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseAnalysis1664785208236 implements MigrationInterface {\n  name = 'databaseAnalysis1664785208236';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\" FROM \"database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_analysis\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_analysis\" RENAME TO \"database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_analysis\" RENAME TO \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\" FROM \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_analysis\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(`DROP TABLE \"database_analysis\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1664886479051-database-analysis-expiration-groups.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseAnalysisExpirationGroups1664886479051\n  implements MigrationInterface\n{\n  name = 'databaseAnalysisExpirationGroups1664886479051';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"expirationGroups\" blob, CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\" FROM \"database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_analysis\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_analysis\" RENAME TO \"database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_analysis\" RENAME TO \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\" FROM \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_analysis\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1667368983699-workbench-execution-time.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class workbenchExecutionTime1667368983699 implements MigrationInterface {\n  name = 'workbenchExecutionTime1667368983699';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, \"executionTime\" integer, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\" FROM \"command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_command_execution\" RENAME TO \"command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"command_execution\" RENAME TO \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\" FROM \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_command_execution\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1667477693934-database.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class database1667477693934 implements MigrationInterface {\n  name = 'database1667477693934';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean NOT NULL, \"verifyServerCert\" boolean NOT NULL, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar, \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1670252337342-database-new.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseNew1670252337342 implements MigrationInterface {\n  name = 'databaseNew1670252337342';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1673035852335-ssh-options.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class sshOptions1673035852335 implements MigrationInterface {\n  name = 'sshOptions1673035852335';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"ssh_options\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"encryption\" varchar, \"username\" varchar, \"password\" varchar, \"privateKey\" varchar, \"passphrase\" varchar, \"databaseId\" varchar, CONSTRAINT \"REL_fe3c3f8b1246e4824a3fb83047\" UNIQUE (\"databaseId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_ssh_options\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"encryption\" varchar, \"username\" varchar, \"password\" varchar, \"privateKey\" varchar, \"passphrase\" varchar, \"databaseId\" varchar, CONSTRAINT \"REL_fe3c3f8b1246e4824a3fb83047\" UNIQUE (\"databaseId\"), CONSTRAINT \"FK_fe3c3f8b1246e4824a3fb83047d\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_ssh_options\"(\"id\", \"host\", \"port\", \"encryption\", \"username\", \"password\", \"privateKey\", \"passphrase\", \"databaseId\") SELECT \"id\", \"host\", \"port\", \"encryption\", \"username\", \"password\", \"privateKey\", \"passphrase\", \"databaseId\" FROM \"ssh_options\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"ssh_options\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_ssh_options\" RENAME TO \"ssh_options\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"ssh_options\" RENAME TO \"temporary_ssh_options\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ssh_options\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"encryption\" varchar, \"username\" varchar, \"password\" varchar, \"privateKey\" varchar, \"passphrase\" varchar, \"databaseId\" varchar, CONSTRAINT \"REL_fe3c3f8b1246e4824a3fb83047\" UNIQUE (\"databaseId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"ssh_options\"(\"id\", \"host\", \"port\", \"encryption\", \"username\", \"password\", \"privateKey\", \"passphrase\", \"databaseId\") SELECT \"id\", \"host\", \"port\", \"encryption\", \"username\", \"password\", \"privateKey\", \"passphrase\", \"databaseId\" FROM \"temporary_ssh_options\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_ssh_options\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n    await queryRunner.query(`DROP TABLE \"ssh_options\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1673934231410-workbench-and-analysis-db.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class workbenchAndAnalysisDbIndex1673934231410\n  implements MigrationInterface\n{\n  name = 'workbenchAndAnalysisDbIndex1673934231410';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, \"executionTime\" integer, \"db\" integer, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\" FROM \"command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_command_execution\" RENAME TO \"command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"expirationGroups\" blob, \"db\" integer, CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\" FROM \"database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_analysis\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_analysis\" RENAME TO \"database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_analysis\" RENAME TO \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"expirationGroups\" blob, CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\" FROM \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_analysis\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"command_execution\" RENAME TO \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, \"executionTime\" integer, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\" FROM \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_command_execution\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1674539211397-browser-history.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class browserHistory1674539211397 implements MigrationInterface {\n  name = 'browserHistory1674539211397';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"browser_history\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"mode\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d0fb08df31bf1a930aeb4d8862\" ON \"browser_history\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f3780aa1d0b977219e40db27e0\" ON \"browser_history\" (\"createdAt\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_d0fb08df31bf1a930aeb4d8862\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_f3780aa1d0b977219e40db27e0\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_browser_history\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"mode\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_d0fb08df31bf1a930aeb4d8862e\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_browser_history\"(\"id\", \"databaseId\", \"filter\", \"mode\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"filter\", \"mode\", \"encryption\", \"createdAt\" FROM \"browser_history\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"browser_history\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_browser_history\" RENAME TO \"browser_history\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d0fb08df31bf1a930aeb4d8862\" ON \"browser_history\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f3780aa1d0b977219e40db27e0\" ON \"browser_history\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_f3780aa1d0b977219e40db27e0\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d0fb08df31bf1a930aeb4d8862\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"browser_history\" RENAME TO \"temporary_browser_history\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"browser_history\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"mode\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"browser_history\"(\"id\", \"databaseId\", \"filter\", \"mode\", \"encryption\", \"createdAt\") SELECT \"id\", \"databaseId\", \"filter\", \"mode\", \"encryption\", \"createdAt\" FROM \"temporary_browser_history\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_browser_history\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f3780aa1d0b977219e40db27e0\" ON \"browser_history\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d0fb08df31bf1a930aeb4d8862\" ON \"browser_history\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_f3780aa1d0b977219e40db27e0\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d0fb08df31bf1a930aeb4d8862\"`);\n    await queryRunner.query(`DROP TABLE \"browser_history\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1674660306971-database-analysis-recommendations.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseAnalysisRecommendations1674660306971\n  implements MigrationInterface\n{\n  name = 'databaseAnalysisRecommendations1674660306971';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"expirationGroups\" blob, \"db\" integer, \"recommendations\" blob, CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\", \"db\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\", \"db\" FROM \"database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_analysis\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_analysis\" RENAME TO \"database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_d174a8edc2201d6c5781f0126a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_analysis\" RENAME TO \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_analysis\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"filter\" blob, \"delimiter\" varchar NOT NULL, \"progress\" blob, \"totalKeys\" blob, \"totalMemory\" blob, \"topKeysNsp\" blob, \"topMemoryNsp\" blob, \"topKeysLength\" blob, \"topKeysMemory\" blob, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"expirationGroups\" blob, \"db\" integer, CONSTRAINT \"FK_d174a8edc2201d6c5781f0126ae\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_analysis\"(\"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\", \"db\") SELECT \"id\", \"databaseId\", \"filter\", \"delimiter\", \"progress\", \"totalKeys\", \"totalMemory\", \"topKeysNsp\", \"topMemoryNsp\", \"topKeysLength\", \"topKeysMemory\", \"encryption\", \"createdAt\", \"expirationGroups\", \"db\" FROM \"temporary_database_analysis\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_analysis\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d174a8edc2201d6c5781f0126a\" ON \"database_analysis\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_fdd0daeb4d8f226cf1ff79bebb\" ON \"database_analysis\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1675398140189-database-timeout.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseTimeout1675398140189 implements MigrationInterface {\n  name = 'databaseTimeout1675398140189';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1677135091633-custom-tutorials.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class customTutorials1677135091633 implements MigrationInterface {\n  name = 'customTutorials1677135091633';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"custom_tutorials\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"link\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"custom_tutorials\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1678182722874-database-compressor.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseCompressor1678182722874 implements MigrationInterface {\n  name = 'databaseCompressor1678182722874';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1681900503586-database-recommendations.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseRecommendations1681900503586\n  implements MigrationInterface\n{\n  name = 'databaseRecommendations1681900503586';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_2487bdd9dbde3fdf65bcb96fc52\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_recommendations\"(\"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\") SELECT \"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\" FROM \"database_recommendations\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_recommendations\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_recommendations\" RENAME TO \"database_recommendations\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_recommendations\" RENAME TO \"temporary_database_recommendations\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_recommendations\"(\"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\") SELECT \"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\" FROM \"temporary_database_recommendations\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_recommendations\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(`DROP TABLE \"database_recommendations\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1683006064293-database-recommendation-params.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class databaseRecommendationParams1683006064293\n  implements MigrationInterface\n{\n  name = 'databaseRecommendationParams1683006064293';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"params\" blob, \"encryption\" varchar, CONSTRAINT \"FK_2487bdd9dbde3fdf65bcb96fc52\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_recommendations\"(\"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\") SELECT \"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\" FROM \"database_recommendations\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_recommendations\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_recommendations\" RENAME TO \"database_recommendations\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_recommendations\" RENAME TO \"temporary_database_recommendations\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"FK_2487bdd9dbde3fdf65bcb96fc52\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_recommendations\"(\"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\") SELECT \"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\" FROM \"temporary_database_recommendations\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_recommendations\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1684931530343-feature.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Feature1684931530343 implements MigrationInterface {\n  name = 'Feature1684931530343';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"features\" (\"name\" varchar PRIMARY KEY NOT NULL, \"flag\" boolean NOT NULL)`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"features_config\" (\"id\" varchar PRIMARY KEY NOT NULL, \"controlNumber\" float, \"data\" varchar NOT NULL, \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"features_config\"`);\n    await queryRunner.query(`DROP TABLE \"features\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1686719451753-database-redis-server.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DatabaseRedisServer1686719451753 implements MigrationInterface {\n  name = 'DatabaseRedisServer1686719451753';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1687166457712-cloud-database-details.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CloudDatabaseDetails1687166457712 implements MigrationInterface {\n  name = 'CloudDatabaseDetails1687166457712';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"database_cloud_details\" (\"id\" varchar PRIMARY KEY NOT NULL, \"cloudId\" integer NOT NULL, \"subscriptionType\" varchar NOT NULL, \"planMemoryLimit\" integer, \"memoryLimitMeasurementUnit\" integer, \"databaseId\" varchar, CONSTRAINT \"REL_f41ee5027391b3be8ad95e3d15\" UNIQUE (\"databaseId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_cloud_details\" (\"id\" varchar PRIMARY KEY NOT NULL, \"cloudId\" integer NOT NULL, \"subscriptionType\" varchar NOT NULL, \"planMemoryLimit\" integer, \"memoryLimitMeasurementUnit\" integer, \"databaseId\" varchar, CONSTRAINT \"REL_f41ee5027391b3be8ad95e3d15\" UNIQUE (\"databaseId\"), CONSTRAINT \"FK_f41ee5027391b3be8ad95e3d158\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_cloud_details\"(\"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\") SELECT \"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\" FROM \"database_cloud_details\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_cloud_details\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_cloud_details\" RENAME TO \"database_cloud_details\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_cloud_details\" RENAME TO \"temporary_database_cloud_details\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_cloud_details\" (\"id\" varchar PRIMARY KEY NOT NULL, \"cloudId\" integer NOT NULL, \"subscriptionType\" varchar NOT NULL, \"planMemoryLimit\" integer, \"memoryLimitMeasurementUnit\" integer, \"databaseId\" varchar, CONSTRAINT \"REL_f41ee5027391b3be8ad95e3d15\" UNIQUE (\"databaseId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_cloud_details\"(\"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\") SELECT \"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\" FROM \"temporary_database_cloud_details\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_cloud_details\"`);\n    await queryRunner.query(`DROP TABLE \"database_cloud_details\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1687435940110-database-recommendation-unique.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DatabaseRecommendationUnique1687435940110\n  implements MigrationInterface\n{\n  name = 'DatabaseRecommendationUnique1687435940110';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"params\" blob, \"encryption\" varchar, CONSTRAINT \"UQ_b772d2856a42685ce4227321251\" UNIQUE (\"databaseId\", \"name\"), CONSTRAINT \"FK_2487bdd9dbde3fdf65bcb96fc52\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT OR IGNORE INTO \"temporary_database_recommendations\"(\"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\", \"params\", \"encryption\") SELECT \"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\", \"params\", \"encryption\" FROM \"database_recommendations\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_recommendations\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_recommendations\" RENAME TO \"database_recommendations\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_d6107e5e16648b038c511f3b00\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_recommendations\" RENAME TO \"temporary_database_recommendations\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_recommendations\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"read\" boolean NOT NULL DEFAULT (0), \"disabled\" boolean NOT NULL DEFAULT (0), \"vote\" varchar, \"hide\" boolean NOT NULL DEFAULT (0), \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"params\" blob, \"encryption\" varchar, CONSTRAINT \"FK_2487bdd9dbde3fdf65bcb96fc52\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_recommendations\"(\"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\", \"params\", \"encryption\") SELECT \"id\", \"databaseId\", \"name\", \"read\", \"disabled\", \"vote\", \"hide\", \"createdAt\", \"params\", \"encryption\" FROM \"temporary_database_recommendations\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_recommendations\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d6107e5e16648b038c511f3b00\" ON \"database_recommendations\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2487bdd9dbde3fdf65bcb96fc5\" ON \"database_recommendations\" (\"databaseId\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1688989337247-freeCloudDatabase.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class FreeCloudDatabase1688989337247 implements MigrationInterface {\n  name = 'FreeCloudDatabase1688989337247';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_cloud_details\" (\"id\" varchar PRIMARY KEY NOT NULL, \"cloudId\" integer NOT NULL, \"subscriptionType\" varchar NOT NULL, \"planMemoryLimit\" integer, \"memoryLimitMeasurementUnit\" integer, \"databaseId\" varchar, \"free\" boolean DEFAULT (0), CONSTRAINT \"REL_f41ee5027391b3be8ad95e3d15\" UNIQUE (\"databaseId\"), CONSTRAINT \"FK_f41ee5027391b3be8ad95e3d158\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_cloud_details\"(\"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\") SELECT \"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\" FROM \"database_cloud_details\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_cloud_details\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_cloud_details\" RENAME TO \"database_cloud_details\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_cloud_details\" RENAME TO \"temporary_database_cloud_details\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_cloud_details\" (\"id\" varchar PRIMARY KEY NOT NULL, \"cloudId\" integer NOT NULL, \"subscriptionType\" varchar NOT NULL, \"planMemoryLimit\" integer, \"memoryLimitMeasurementUnit\" integer, \"databaseId\" varchar, CONSTRAINT \"REL_f41ee5027391b3be8ad95e3d15\" UNIQUE (\"databaseId\"), CONSTRAINT \"FK_f41ee5027391b3be8ad95e3d158\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_cloud_details\"(\"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\") SELECT \"id\", \"cloudId\", \"subscriptionType\", \"planMemoryLimit\", \"memoryLimitMeasurementUnit\", \"databaseId\" FROM \"temporary_database_cloud_details\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_cloud_details\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1691061058385-cloud-capi-keys.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CloudCapiKeys1691061058385 implements MigrationInterface {\n  name = 'CloudCapiKeys1691061058385';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"cloud_capi_key\" (\"id\" varchar PRIMARY KEY NOT NULL, \"userId\" varchar NOT NULL, \"name\" varchar NOT NULL, \"cloudAccountId\" integer NOT NULL, \"cloudUserId\" integer NOT NULL, \"capiKey\" varchar, \"capiSecret\" varchar, \"valid\" boolean DEFAULT (1), \"encryption\" varchar, \"createdAt\" datetime, \"lastUsed\" datetime, CONSTRAINT \"UQ_9de67df9deb5d91c09c03b8d719\" UNIQUE (\"userId\", \"cloudAccountId\", \"cloudUserId\"))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"cloud_capi_key\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1691476419592-feature-sso.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class FeatureSso1691476419592 implements MigrationInterface {\n  name = 'FeatureSso1691476419592';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_features\" (\"name\" varchar PRIMARY KEY NOT NULL, \"flag\" boolean NOT NULL, \"strategy\" varchar, \"data\" text)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_features\"(\"name\", \"flag\") SELECT \"name\", \"flag\" FROM \"features\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"features\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_features\" RENAME TO \"features\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"features\" RENAME TO \"temporary_features\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"features\" (\"name\" varchar PRIMARY KEY NOT NULL, \"flag\" boolean NOT NULL)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"features\"(\"name\", \"flag\") SELECT \"name\", \"flag\" FROM \"temporary_features\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_features\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1713515657364-ai-history.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AiHistory1713515657364 implements MigrationInterface {\n  name = 'AiHistory1713515657364';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"ai_query_message\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"accountId\" varchar NOT NULL, \"type\" varchar NOT NULL, \"content\" blob NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"encryption\" varchar)`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_51d5d60bfc249e9a20443376e1\" ON \"ai_query_message\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\" ON \"ai_query_message\" (\"accountId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\" ON \"ai_query_message\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_51d5d60bfc249e9a20443376e1\"`);\n    await queryRunner.query(`DROP TABLE \"ai_query_message\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1714501203616-ai-history-steps.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AiHistorySteps1714501203616 implements MigrationInterface {\n  name = 'Migration1714501203616';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_51d5d60bfc249e9a20443376e1\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_ai_query_message\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"accountId\" varchar NOT NULL, \"type\" varchar NOT NULL, \"content\" blob NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"encryption\" varchar, \"steps\" blob)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_ai_query_message\"(\"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\") SELECT \"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\" FROM \"ai_query_message\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"ai_query_message\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_ai_query_message\" RENAME TO \"ai_query_message\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\" ON \"ai_query_message\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\" ON \"ai_query_message\" (\"accountId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_51d5d60bfc249e9a20443376e1\" ON \"ai_query_message\" (\"databaseId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_51d5d60bfc249e9a20443376e1\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"ai_query_message\" RENAME TO \"temporary_ai_query_message\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ai_query_message\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"accountId\" varchar NOT NULL, \"type\" varchar NOT NULL, \"content\" blob NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"encryption\" varchar)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"ai_query_message\"(\"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\") SELECT \"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\" FROM \"temporary_ai_query_message\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_ai_query_message\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_51d5d60bfc249e9a20443376e1\" ON \"ai_query_message\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\" ON \"ai_query_message\" (\"accountId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\" ON \"ai_query_message\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1716370509836-rdi.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Rdi1716370509836 implements MigrationInterface {\n  name = 'Rdi1716370509836';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"rdi\" (\"id\" varchar PRIMARY KEY NOT NULL, \"url\" varchar, \"name\" varchar NOT NULL, \"username\" varchar NOT NULL, \"password\" varchar NOT NULL, \"lastConnection\" datetime, \"version\" varchar NOT NULL, \"encryption\" varchar)`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"rdi\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1718260230164-ai-history.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AiHistory1718260230164 implements MigrationInterface {\n  name = 'AiHistory1718260230164';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_51d5d60bfc249e9a20443376e1\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_ai_query_message\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"accountId\" varchar NOT NULL, \"type\" varchar NOT NULL, \"content\" blob NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"encryption\" varchar, \"steps\" blob, \"conversationId\" varchar)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_ai_query_message\"(\"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\", \"steps\") SELECT \"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\", \"steps\" FROM \"ai_query_message\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"ai_query_message\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_ai_query_message\" RENAME TO \"ai_query_message\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_51d5d60bfc249e9a20443376e1\" ON \"ai_query_message\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\" ON \"ai_query_message\" (\"accountId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\" ON \"ai_query_message\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_51d5d60bfc249e9a20443376e1\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"ai_query_message\" RENAME TO \"temporary_ai_query_message\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ai_query_message\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"accountId\" varchar NOT NULL, \"type\" varchar NOT NULL, \"content\" blob NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"encryption\" varchar, \"steps\" blob)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"ai_query_message\"(\"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\", \"steps\") SELECT \"id\", \"databaseId\", \"accountId\", \"type\", \"content\", \"createdAt\", \"encryption\", \"steps\" FROM \"temporary_ai_query_message\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_ai_query_message\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5c051504f4efe6f20c5a7f64f6\" ON \"ai_query_message\" (\"createdAt\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_f0a6e0873ac71f323e9880b4a8\" ON \"ai_query_message\" (\"accountId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_51d5d60bfc249e9a20443376e1\" ON \"ai_query_message\" (\"databaseId\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1726058563737-command-execution.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CommandExecution1726058563737 implements MigrationInterface {\n  name = 'CommandExecution1726058563737';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, \"executionTime\" integer, \"db\" integer, \"type\" varchar NOT NULL DEFAULT ('WORKBENCH'), CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\", \"db\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\", \"db\" FROM \"command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"command_execution\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_command_execution\" RENAME TO \"command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"command_execution\" RENAME TO \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"command_execution\" (\"id\" varchar PRIMARY KEY NOT NULL, \"databaseId\" varchar NOT NULL, \"command\" text NOT NULL, \"result\" text NOT NULL, \"role\" varchar, \"nodeOptions\" varchar, \"encryption\" varchar, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"mode\" varchar, \"resultsMode\" varchar, \"summary\" varchar, \"executionTime\" integer, \"db\" integer, CONSTRAINT \"FK_ea8adfe9aceceb79212142206b8\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"command_execution\"(\"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\", \"db\") SELECT \"id\", \"databaseId\", \"command\", \"result\", \"role\", \"nodeOptions\", \"encryption\", \"createdAt\", \"mode\", \"resultsMode\", \"summary\", \"executionTime\", \"db\" FROM \"temporary_command_execution\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_command_execution\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5cd90dd6def1fd7c521e53fb2c\" ON \"command_execution\" (\"createdAt\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1729085495444-cloud-session.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CloudSession1729085495444 implements MigrationInterface {\n  name = 'CloudSession1729085495444';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"cloud_session\" (\"id\" varchar PRIMARY KEY NOT NULL, \"data\" varchar, \"encryption\" varchar)`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"cloud_session\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1733740794737-database-createdAt.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DatabaseCreatedAt1733740794737 implements MigrationInterface {\n  name = 'DatabaseCreatedAt1733740794737';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1737362130798-db-settings.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DbSettings1737362130798 implements MigrationInterface {\n  name = 'DbSettings1737362130798';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"database_settings\" (\"id\" integer PRIMARY KEY AUTOINCREMENT NOT NULL, \"databaseId\" varchar NOT NULL, \"data\" varchar, CONSTRAINT \"REL_548c7a02b802a053a80a60bdc9\" UNIQUE (\"databaseId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE UNIQUE INDEX \"IDX_548c7a02b802a053a80a60bdc9\" ON \"database_settings\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_548c7a02b802a053a80a60bdc9\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_settings\" (\"id\" integer PRIMARY KEY AUTOINCREMENT NOT NULL, \"databaseId\" varchar NOT NULL, \"data\" varchar, CONSTRAINT \"REL_548c7a02b802a053a80a60bdc9\" UNIQUE (\"databaseId\"), CONSTRAINT \"FK_548c7a02b802a053a80a60bdc96\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_settings\"(\"id\", \"databaseId\", \"data\") SELECT \"id\", \"databaseId\", \"data\" FROM \"database_settings\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_settings\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_settings\" RENAME TO \"database_settings\"`,\n    );\n    await queryRunner.query(\n      `CREATE UNIQUE INDEX \"IDX_548c7a02b802a053a80a60bdc9\" ON \"database_settings\" (\"databaseId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_548c7a02b802a053a80a60bdc9\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_settings\" RENAME TO \"temporary_database_settings\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_settings\" (\"id\" integer PRIMARY KEY AUTOINCREMENT NOT NULL, \"databaseId\" varchar NOT NULL, \"data\" varchar, CONSTRAINT \"REL_548c7a02b802a053a80a60bdc9\" UNIQUE (\"databaseId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_settings\"(\"id\", \"databaseId\", \"data\") SELECT \"id\", \"databaseId\", \"data\" FROM \"temporary_database_settings\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_settings\"`);\n    await queryRunner.query(\n      `CREATE UNIQUE INDEX \"IDX_548c7a02b802a053a80a60bdc9\" ON \"database_settings\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_548c7a02b802a053a80a60bdc9\"`);\n    await queryRunner.query(`DROP TABLE \"database_settings\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1738829743482-database-forceStandalone.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DatabaseForceStandalone1738829743482\n  implements MigrationInterface\n{\n  name = 'DatabaseForceStandalone1738829743482';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), \"forceStandalone\" boolean, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1740579711635-rdi-optional-auth.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class RdiOptionalAuth1740579711635 implements MigrationInterface {\n  name = 'RdiOptionalAuth1740579711635';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_rdi\" (\"id\" varchar PRIMARY KEY NOT NULL, \"url\" varchar, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"lastConnection\" datetime, \"version\" varchar NOT NULL, \"encryption\" varchar)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_rdi\"(\"id\", \"url\", \"name\", \"username\", \"password\", \"lastConnection\", \"version\", \"encryption\") SELECT \"id\", \"url\", \"name\", \"username\", \"password\", \"lastConnection\", \"version\", \"encryption\" FROM \"rdi\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"rdi\"`);\n    await queryRunner.query(`ALTER TABLE \"temporary_rdi\" RENAME TO \"rdi\"`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"rdi\" RENAME TO \"temporary_rdi\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"rdi\" (\"id\" varchar PRIMARY KEY NOT NULL, \"url\" varchar, \"name\" varchar NOT NULL, \"username\" varchar NOT NULL, \"password\" varchar NOT NULL, \"lastConnection\" datetime, \"version\" varchar NOT NULL, \"encryption\" varchar)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"rdi\"(\"id\", \"url\", \"name\", \"username\", \"password\", \"lastConnection\", \"version\", \"encryption\") SELECT \"id\", \"url\", \"name\", \"username\", \"password\", \"lastConnection\", \"version\", \"encryption\" FROM \"temporary_rdi\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_rdi\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1741610039177-database-tags.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DatabaseTags1741610039177 implements MigrationInterface {\n  name = 'DatabaseTags1741610039177';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"tag\" (\"id\" varchar PRIMARY KEY NOT NULL, \"key\" varchar NOT NULL, \"value\" varchar NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"UQ_5d6110d4eee64a5a4529ea8fcdc\" UNIQUE (\"key\", \"value\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, CONSTRAINT \"FK_c1958471fa6e48e80e0dc4b27a5\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT \"FK_3f0a9100c363114a8af7d1cae55\" FOREIGN KEY (\"tagId\") REFERENCES \"tag\" (\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_tag\"(\"databaseId\", \"tagId\") SELECT \"databaseId\", \"tagId\" FROM \"database_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_tag\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_tag\" RENAME TO \"database_tag\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_tag\" RENAME TO \"temporary_database_tag\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_tag\"(\"databaseId\", \"tagId\") SELECT \"databaseId\", \"tagId\" FROM \"temporary_database_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_tag\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(`DROP TABLE \"database_tag\"`);\n    await queryRunner.query(`DROP TABLE \"tag\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1741786803681-pre-setup-databases.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class PreSetupDatabases1741786803681 implements MigrationInterface {\n  name = 'PreSetupDatabases1741786803681';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"encryption\" varchar, \"certificate\" varchar, \"isPreSetup\" boolean, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_ca_certificate\"(\"id\", \"name\", \"encryption\", \"certificate\") SELECT \"id\", \"name\", \"encryption\", \"certificate\" FROM \"ca_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"ca_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_ca_certificate\" RENAME TO \"ca_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"encryption\" varchar, \"certificate\" varchar, \"key\" varchar, \"isPreSetup\" boolean, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_client_certificate\"(\"id\", \"name\", \"encryption\", \"certificate\", \"key\") SELECT \"id\", \"name\", \"encryption\", \"certificate\", \"key\" FROM \"client_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"client_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_client_certificate\" RENAME TO \"client_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), \"forceStandalone\" boolean, \"isPreSetup\" boolean, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), \"forceStandalone\" boolean, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"client_certificate\" RENAME TO \"temporary_client_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"client_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"encryption\" varchar, \"certificate\" varchar, \"key\" varchar, CONSTRAINT \"UQ_4966cf1c0e299df01049ebd53a5\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"client_certificate\"(\"id\", \"name\", \"encryption\", \"certificate\", \"key\") SELECT \"id\", \"name\", \"encryption\", \"certificate\", \"key\" FROM \"temporary_client_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_client_certificate\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"ca_certificate\" RENAME TO \"temporary_ca_certificate\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"ca_certificate\" (\"id\" varchar PRIMARY KEY NOT NULL, \"name\" varchar NOT NULL, \"encryption\" varchar, \"certificate\" varchar, CONSTRAINT \"UQ_23be613e4fb204fd5a66916b0b3\" UNIQUE (\"name\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"ca_certificate\"(\"id\", \"name\", \"encryption\", \"certificate\") SELECT \"id\", \"name\", \"encryption\", \"certificate\" FROM \"temporary_ca_certificate\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_ca_certificate\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1742303245547-key-name-format.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class KeyNameFormatAdded1742303245547 implements MigrationInterface {\n  name = 'KeyNameFormatAdded1742303245547';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), \"forceStandalone\" boolean, \"isPreSetup\" boolean, \"keyNameFormat\" varchar DEFAULT ('Unicode'), CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\", \"isPreSetup\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\", \"isPreSetup\" FROM \"database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_instance\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_instance\" RENAME TO \"database_instance\"`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" RENAME TO \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_instance\" (\"id\" varchar PRIMARY KEY NOT NULL, \"host\" varchar NOT NULL, \"port\" integer NOT NULL, \"name\" varchar NOT NULL, \"username\" varchar, \"password\" varchar, \"tls\" boolean, \"verifyServerCert\" boolean, \"lastConnection\" datetime, \"caCertId\" varchar, \"clientCertId\" varchar, \"connectionType\" varchar NOT NULL DEFAULT ('STANDALONE'), \"nodes\" varchar DEFAULT ('[]'), \"nameFromProvider\" varchar, \"sentinelMasterName\" varchar, \"sentinelMasterUsername\" varchar, \"sentinelMasterPassword\" varchar, \"provider\" varchar DEFAULT ('UNKNOWN'), \"modules\" varchar NOT NULL DEFAULT ('[]'), \"db\" integer, \"encryption\" varchar, \"tlsServername\" varchar, \"new\" boolean, \"ssh\" boolean, \"timeout\" integer, \"compressor\" varchar NOT NULL DEFAULT ('NONE'), \"version\" varchar, \"createdAt\" datetime DEFAULT (datetime('now')), \"forceStandalone\" boolean, \"isPreSetup\" boolean, CONSTRAINT \"FK_3b9b625266c00feb2d66a9f36e4\" FOREIGN KEY (\"clientCertId\") REFERENCES \"client_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT \"FK_d1bc747b5938e22b4b708d8e9a5\" FOREIGN KEY (\"caCertId\") REFERENCES \"ca_certificate\" (\"id\") ON DELETE SET NULL ON UPDATE NO ACTION)`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_instance\"(\"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\", \"isPreSetup\") SELECT \"id\", \"host\", \"port\", \"name\", \"username\", \"password\", \"tls\", \"verifyServerCert\", \"lastConnection\", \"caCertId\", \"clientCertId\", \"connectionType\", \"nodes\", \"nameFromProvider\", \"sentinelMasterName\", \"sentinelMasterUsername\", \"sentinelMasterPassword\", \"provider\", \"modules\", \"db\", \"encryption\", \"tlsServername\", \"new\", \"ssh\", \"timeout\", \"compressor\", \"version\", \"createdAt\", \"forceStandalone\", \"isPreSetup\" FROM \"temporary_database_instance\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_instance\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1743432519891-cascade-tags.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CascadeTags1743432519891 implements MigrationInterface {\n  name = 'CascadeTags1743432519891';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, CONSTRAINT \"FK_c1958471fa6e48e80e0dc4b27a5\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_tag\"(\"databaseId\", \"tagId\") SELECT \"databaseId\", \"tagId\" FROM \"database_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_tag\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_tag\" RENAME TO \"database_tag\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, CONSTRAINT \"FK_c1958471fa6e48e80e0dc4b27a5\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT \"FK_3f0a9100c363114a8af7d1cae55\" FOREIGN KEY (\"tagId\") REFERENCES \"tag\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_database_tag\"(\"databaseId\", \"tagId\") SELECT \"databaseId\", \"tagId\" FROM \"database_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"database_tag\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"temporary_database_tag\" RENAME TO \"database_tag\"`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_tag\" RENAME TO \"temporary_database_tag\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, CONSTRAINT \"FK_c1958471fa6e48e80e0dc4b27a5\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_tag\"(\"databaseId\", \"tagId\") SELECT \"databaseId\", \"tagId\" FROM \"temporary_database_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_tag\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n    await queryRunner.query(`DROP INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\"`);\n    await queryRunner.query(`DROP INDEX \"IDX_3f0a9100c363114a8af7d1cae5\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"database_tag\" RENAME TO \"temporary_database_tag\"`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"database_tag\" (\"databaseId\" varchar NOT NULL, \"tagId\" varchar NOT NULL, CONSTRAINT \"FK_3f0a9100c363114a8af7d1cae55\" FOREIGN KEY (\"tagId\") REFERENCES \"tag\" (\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT \"FK_c1958471fa6e48e80e0dc4b27a5\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (\"databaseId\", \"tagId\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"database_tag\"(\"databaseId\", \"tagId\") SELECT \"databaseId\", \"tagId\" FROM \"temporary_database_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_database_tag\"`);\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_c1958471fa6e48e80e0dc4b27a\" ON \"database_tag\" (\"databaseId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_3f0a9100c363114a8af7d1cae5\" ON \"database_tag\" (\"tagId\") `,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1743606395647-encrypt-tags.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class EncryptTags1743606395647 implements MigrationInterface {\n  name = 'EncryptTags1743606395647';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"temporary_tag\" (\"id\" varchar PRIMARY KEY NOT NULL, \"key\" varchar NOT NULL, \"value\" varchar NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')), \"encryption\" varchar, CONSTRAINT \"UQ_5d6110d4eee64a5a4529ea8fcdc\" UNIQUE (\"key\", \"value\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"temporary_tag\"(\"id\", \"key\", \"value\", \"createdAt\", \"updatedAt\") SELECT \"id\", \"key\", \"value\", \"createdAt\", \"updatedAt\" FROM \"tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"tag\"`);\n    await queryRunner.query(`ALTER TABLE \"temporary_tag\" RENAME TO \"tag\"`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"tag\" RENAME TO \"temporary_tag\"`);\n    await queryRunner.query(\n      `CREATE TABLE \"tag\" (\"id\" varchar PRIMARY KEY NOT NULL, \"key\" varchar NOT NULL, \"value\" varchar NOT NULL, \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')), \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT \"UQ_5d6110d4eee64a5a4529ea8fcdc\" UNIQUE (\"key\", \"value\"))`,\n    );\n    await queryRunner.query(\n      `INSERT INTO \"tag\"(\"id\", \"key\", \"value\", \"createdAt\", \"updatedAt\") SELECT \"id\", \"key\", \"value\", \"createdAt\", \"updatedAt\" FROM \"temporary_tag\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"temporary_tag\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1755086732238-update-provider-names.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class UpdateProviderNames1755086732238 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`\n            UPDATE database_instance\n            SET provider = CASE provider\n                WHEN 'RE_CLOUD' THEN 'REDIS_CLOUD'\n                WHEN 'RE_CLUSTER' THEN 'REDIS_SOFTWARE'\n                WHEN 'REDIS_ENTERPRISE' THEN 'OTHER_REDIS_MANAGED'\n                ELSE provider\n            END\n            WHERE provider IN ('RE_CLOUD', 'RE_CLUSTER', 'REDIS_ENTERPRISE');\n          `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`\n            UPDATE database_instance\n            SET provider = CASE provider\n                WHEN 'REDIS_CLOUD' THEN 'RE_CLOUD'\n                WHEN 'REDIS_SOFTWARE' THEN 'RE_CLUSTER'\n                WHEN 'OTHER_REDIS_MANAGED' THEN 'REDIS_ENTERPRISE'\n                ELSE provider\n            END\n            WHERE provider IN ('REDIS_CLOUD', 'REDIS_SOFTWARE', 'OTHER_REDIS_MANAGED');\n          `);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/1769785218000-provider-details.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class ProviderDetails1769785218000 implements MigrationInterface {\n  name = 'ProviderDetails1769785218000';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" ADD COLUMN \"providerDetails\" text`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"database_instance\" DROP COLUMN \"providerDetails\"`,\n    );\n  }\n}\n\n"
  },
  {
    "path": "redisinsight/api/migration/1771500000000-query-library.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class QueryLibrary1771500000000 implements MigrationInterface {\n  name = 'QueryLibrary1771500000000';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"query_library\" (\n        \"id\" varchar PRIMARY KEY NOT NULL,\n        \"databaseId\" varchar NOT NULL,\n        \"indexName\" varchar NOT NULL,\n        \"type\" varchar NOT NULL DEFAULT ('SAVED'),\n        \"name\" varchar NOT NULL,\n        \"description\" text,\n        \"query\" text NOT NULL,\n        \"encryption\" varchar,\n        \"createdAt\" datetime NOT NULL DEFAULT (datetime('now')),\n        \"updatedAt\" datetime NOT NULL DEFAULT (datetime('now')),\n        CONSTRAINT \"FK_query_library_databaseId\" FOREIGN KEY (\"databaseId\") REFERENCES \"database_instance\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION\n      )`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_query_library_db_index_created\" ON \"query_library\" (\"databaseId\", \"indexName\", \"createdAt\")`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"IDX_query_library_db_index_created\"`);\n    await queryRunner.query(`DROP TABLE \"query_library\"`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/migration/index.ts",
    "content": "import { initialMigration1614164490968 } from './1614164490968-initial-migration';\nimport { connectionType1615480887019 } from './1615480887019-connection-type';\nimport { databaseNameFromProvider1615990079125 } from './1615990079125-database-name-from-provider';\nimport { removeDatabaseType1615992183565 } from './1615992183565-remove-database-type';\nimport { ossSentinel1616520395940 } from './1616520395940-oss-sentinel';\nimport { agreements1625771635418 } from './1625771635418-agreements';\nimport { serverInfo1626086601057 } from './1626086601057-server-info';\nimport { databaseHostingProvider1626904405170 } from './1626904405170-database-hosting-provider';\nimport { settings1627556171227 } from './1627556171227-settings';\nimport { databaseModules1629729923740 } from './1629729923740-database-modules';\nimport { databaseDbIndex1634219846022 } from './1634219846022-database-db-index';\nimport { encryption1634557312500 } from './1634557312500-encryption';\nimport { commandExecution1641795882696 } from './1641795882696-command-execution';\nimport { pluginState1641805606399 } from './1641805606399-plugin-state';\nimport { sni1650278664000 } from './1650278664000-sni';\nimport { notification1655821010349 } from './1655821010349-notification';\nimport { notificationCategory1659687030433 } from './1659687030433-notification-category';\nimport { workbenchMode1660664717573 } from './1660664717573-workbench-mode';\nimport { workbenchGroupMode1663093411715 } from './1663093411715-workbench-group-mode';\nimport { databaseAnalysis1664785208236 } from './1664785208236-database-analysis';\nimport { databaseAnalysisExpirationGroups1664886479051 } from './1664886479051-database-analysis-expiration-groups';\nimport { workbenchExecutionTime1667368983699 } from './1667368983699-workbench-execution-time';\nimport { database1667477693934 } from './1667477693934-database';\nimport { databaseNew1670252337342 } from './1670252337342-database-new';\nimport { sshOptions1673035852335 } from './1673035852335-ssh-options';\nimport { workbenchAndAnalysisDbIndex1673934231410 } from './1673934231410-workbench-and-analysis-db';\nimport { browserHistory1674539211397 } from './1674539211397-browser-history';\nimport { databaseAnalysisRecommendations1674660306971 } from './1674660306971-database-analysis-recommendations';\nimport { databaseTimeout1675398140189 } from './1675398140189-database-timeout';\nimport { databaseCompressor1678182722874 } from './1678182722874-database-compressor';\nimport { customTutorials1677135091633 } from './1677135091633-custom-tutorials';\nimport { databaseRecommendations1681900503586 } from './1681900503586-database-recommendations';\nimport { databaseRecommendationParams1683006064293 } from './1683006064293-database-recommendation-params';\nimport { Feature1684931530343 } from './1684931530343-feature';\nimport { DatabaseRedisServer1686719451753 } from './1686719451753-database-redis-server';\nimport { DatabaseRecommendationUnique1687435940110 } from './1687435940110-database-recommendation-unique';\nimport { CloudDatabaseDetails1687166457712 } from './1687166457712-cloud-database-details';\nimport { FreeCloudDatabase1688989337247 } from './1688989337247-freeCloudDatabase';\nimport { CloudCapiKeys1691061058385 } from './1691061058385-cloud-capi-keys';\nimport { FeatureSso1691476419592 } from './1691476419592-feature-sso';\nimport { AiHistory1713515657364 } from './1713515657364-ai-history';\nimport { AiHistorySteps1714501203616 } from './1714501203616-ai-history-steps';\nimport { Rdi1716370509836 } from './1716370509836-rdi';\nimport { AiHistory1718260230164 } from './1718260230164-ai-history';\nimport { CloudSession1729085495444 } from './1729085495444-cloud-session';\nimport { CommandExecution1726058563737 } from './1726058563737-command-execution';\nimport { DatabaseCreatedAt1733740794737 } from './1733740794737-database-createdAt';\nimport { DbSettings1737362130798 } from './1737362130798-db-settings';\nimport { DatabaseForceStandalone1738829743482 } from './1738829743482-database-forceStandalone';\nimport { RdiOptionalAuth1740579711635 } from './1740579711635-rdi-optional-auth';\nimport { DatabaseTags1741610039177 } from './1741610039177-database-tags';\nimport { PreSetupDatabases1741786803681 } from './1741786803681-pre-setup-databases';\nimport { KeyNameFormatAdded1742303245547 } from './1742303245547-key-name-format';\nimport { CascadeTags1743432519891 } from './1743432519891-cascade-tags';\nimport { EncryptTags1743606395647 } from './1743606395647-encrypt-tags';\nimport { UpdateProviderNames1755086732238 } from './1755086732238-update-provider-names';\nimport { ProviderDetails1769785218000 } from './1769785218000-provider-details';\nimport { QueryLibrary1771500000000 } from './1771500000000-query-library';\n\nexport default [\n  initialMigration1614164490968,\n  connectionType1615480887019,\n  databaseNameFromProvider1615990079125,\n  removeDatabaseType1615992183565,\n  ossSentinel1616520395940,\n  agreements1625771635418,\n  serverInfo1626086601057,\n  databaseHostingProvider1626904405170,\n  settings1627556171227,\n  databaseModules1629729923740,\n  databaseDbIndex1634219846022,\n  encryption1634557312500,\n  commandExecution1641795882696,\n  pluginState1641805606399,\n  sni1650278664000,\n  notification1655821010349,\n  notificationCategory1659687030433,\n  workbenchMode1660664717573,\n  workbenchGroupMode1663093411715,\n  databaseAnalysis1664785208236,\n  databaseAnalysisExpirationGroups1664886479051,\n  workbenchExecutionTime1667368983699,\n  database1667477693934,\n  databaseNew1670252337342,\n  sshOptions1673035852335,\n  workbenchAndAnalysisDbIndex1673934231410,\n  databaseAnalysisRecommendations1674660306971,\n  browserHistory1674539211397,\n  databaseTimeout1675398140189,\n  databaseCompressor1678182722874,\n  customTutorials1677135091633,\n  databaseRecommendations1681900503586,\n  databaseRecommendationParams1683006064293,\n  Feature1684931530343,\n  DatabaseRedisServer1686719451753,\n  DatabaseRecommendationUnique1687435940110,\n  CloudDatabaseDetails1687166457712,\n  FreeCloudDatabase1688989337247,\n  CloudCapiKeys1691061058385,\n  FeatureSso1691476419592,\n  AiHistory1713515657364,\n  AiHistorySteps1714501203616,\n  Rdi1716370509836,\n  AiHistory1718260230164,\n  CloudSession1729085495444,\n  CommandExecution1726058563737,\n  DatabaseCreatedAt1733740794737,\n  DbSettings1737362130798,\n  DatabaseForceStandalone1738829743482,\n  RdiOptionalAuth1740579711635,\n  DatabaseTags1741610039177,\n  PreSetupDatabases1741786803681,\n  KeyNameFormatAdded1742303245547,\n  CascadeTags1743432519891,\n  EncryptTags1743606395647,\n  UpdateProviderNames1755086732238,\n  ProviderDetails1769785218000,\n  QueryLibrary1771500000000,\n];\n"
  },
  {
    "path": "redisinsight/api/nest-cli.json",
    "content": "{\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\",\n  \"compilerOptions\": {\n    \"assets\": [\n      {\n        \"include\": \"../static/**/*\",\n        \"outDir\": \"dist/static\"\n      },\n      {\n        \"include\": \"../defaults/**/*\",\n        \"outDir\": \"dist/defaults\"\n      },\n      {\n        \"include\": \"../data/**/*\",\n        \"outDir\": \"dist/data\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/package.json",
    "content": "{\n  \"name\": \"redisinsight-api\",\n  \"version\": \"3.2.0\",\n  \"description\": \"Redis Insight API\",\n  \"private\": true,\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"scripts\": {\n    \"postinstall\": \"npx patch-package\",\n    \"build:defaults:commands\": \"ts-node ./scripts/default-commands.ts\",\n    \"build:defaults:tutorials\": \"ts-node ./scripts/default-tutorials.ts\",\n    \"build:defaults:content\": \"ts-node ./scripts/default-content.ts\",\n    \"build:defaults\": \"yarn build:defaults:commands && yarn build:defaults:content && yarn build:defaults:tutorials\",\n    \"prebuild\": \"rimraf dist\",\n    \"build\": \"nest build\",\n    \"build:prod\": \"rimraf dist && nest build -p ./tsconfig.build.prod.json && cross-env NODE_ENV=production\",\n    \"build:stage\": \"rimraf dist && nest build && cross-env NODE_ENV=staging\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\"\",\n    \"minify:prod\": \"node ./esbuild.js --production\",\n    \"minify:dev\": \"node ./esbuild.js --watch\",\n    \"start\": \"nest start\",\n    \"start:dev\": \"cross-env NODE_ENV=development nest start --watch\",\n    \"start:debug\": \"nest start --debug --watch\",\n    \"start:stage\": \"cross-env NODE_ENV=staging node dist/src/main\",\n    \"start:prod\": \"cross-env NODE_ENV=production node dist/src/main\",\n    \"test\": \"cross-env NODE_ENV=test ./node_modules/.bin/jest -w 1\",\n    \"test:watch\": \"cross-env NODE_ENV=test jest --watch -w 1\",\n    \"test:cov\": \"cross-env NODE_ENV=test ./node_modules/.bin/jest --testLocationInResults --json --outputFile=\\\"report/coverage/report.json\\\" --forceExit --coverage --runInBand\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand -w 1\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json -w 1\",\n    \"typeorm\": \"ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d ./config/ormconfig.ts\",\n    \"test:api\": \"cross-env NODE_ENV=test ts-mocha --paths --config ./test/api/.mocharc.yml\",\n    \"test:api:cov\": \"nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api\",\n    \"test:api:ci:cov\": \"cross-env nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json\",\n    \"typeorm:migrate\": \"cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration\",\n    \"typeorm:run\": \"yarn typeorm migration:run\",\n    \"typeorm:run:stage\": \"cross-env NODE_ENV=staging yarn typeorm migration:run\"\n  },\n  \"resolutions\": {\n    \"word-wrap\": \"1.2.4\",\n    \"jest/**/micromatch\": \"^4.0.8\",\n    \"mocha/minimatch\": \"^3.0.5\",\n    \"**/semver\": \"^7.5.2\",\n    \"**/cpu-features\": \"file:./stubs/cpu-features\",\n    \"**/cross-spawn\": \"^7.0.5\",\n    \"**/redis-parser\": \"3.0.0\",\n    \"winston-daily-rotate-file/**/file-stream-rotator\": \"^1.0.0\",\n    \"**/form-data\": \"^4.0.4\",\n    \"form-data\": \"^4.0.4\",\n    \"supertest/**/form-data\": \"^4.0.4\",\n    \"@nestjs/cli/glob\": \"11.1.0\",\n    \"@nestjs/swagger/js-yaml\": \"4.1.1\",\n    \"**/multer\": \"^2.0.2\"\n  },\n  \"dependencies\": {\n    \"@azure/msal-node\": \"^5.0.2\",\n    \"@nestjs/common\": \"^11.0.20\",\n    \"@nestjs/core\": \"^11.0.20\",\n    \"@nestjs/event-emitter\": \"^3.0.1\",\n    \"@nestjs/platform-express\": \"^11.1.3\",\n    \"@nestjs/platform-socket.io\": \"^11.0.20\",\n    \"@nestjs/serve-static\": \"^5.0.3\",\n    \"@nestjs/swagger\": \"^11.1.3\",\n    \"@nestjs/typeorm\": \"^11.0.0\",\n    \"@nestjs/websockets\": \"^11.0.20\",\n    \"@okta/okta-auth-js\": \"^7.12.1\",\n    \"@segment/analytics-node\": \"^2.1.3\",\n    \"@supercharge/promise-pool\": \"^3.2.0\",\n    \"@types/json-bigint\": \"^1.0.4\",\n    \"adm-zip\": \"^0.5.9\",\n    \"axios\": \"^1.13.5\",\n    \"body-parser\": \"^1.20.3\",\n    \"busboy\": \"^1.6.0\",\n    \"class-transformer\": \"^0.5.1\",\n    \"class-validator\": \"^0.14.1\",\n    \"combined-stream\": \"^1.0.8\",\n    \"connect-timeout\": \"^1.9.1\",\n    \"date-fns\": \"^2.29.3\",\n    \"detect-port\": \"^1.5.1\",\n    \"dotenv\": \"^16.0.0\",\n    \"express\": \"5.2.0\",\n    \"form-data\": \"^4.0.4\",\n    \"fs-extra\": \"^10.0.0\",\n    \"ioredis\": \"^5.2.2\",\n    \"is-glob\": \"^4.0.1\",\n    \"json-bigint\": \"^1.0.0\",\n    \"jsonwebtoken\": \"^9.0.2\",\n    \"keytar\": \"^7.9.0\",\n    \"lodash\": \"^4.17.23\",\n    \"nest-winston\": \"^1.10.2\",\n    \"nestjs-form-data\": \"~1.9.93\",\n    \"node-version-compare\": \"^1.0.3\",\n    \"quicktype-core\": \"^23.0.116\",\n    \"redis\": \"^4.6.10\",\n    \"redis-parser\": \"3.0.0\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"rxjs\": \"^7.5.6\",\n    \"socket.io\": \"^4.8.1\",\n    \"socket.io-client\": \"^4.8.1\",\n    \"source-map-support\": \"^0.5.19\",\n    \"sqlite3\": \"5.1.7\",\n    \"swagger-ui-express\": \"^4.1.4\",\n    \"tunnel-ssh\": \"^5.1.2\",\n    \"typeorm\": \"^0.3.26\",\n    \"uuid\": \"^8.3.2\",\n    \"winston\": \"^3.3.3\",\n    \"winston-daily-rotate-file\": \"^4.5.0\"\n  },\n  \"devDependencies\": {\n    \"@faker-js/faker\": \"^8.4.1\",\n    \"@mochajs/json-file-reporter\": \"^1.3.0\",\n    \"@nestjs/cli\": \"^11.0.10\",\n    \"@nestjs/schematics\": \"^11.0.5\",\n    \"@nestjs/testing\": \"^11.0.20\",\n    \"@types/adm-zip\": \"^0.5.0\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/ioredis-mock\": \"^8\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/lodash\": \"^4.14.167\",\n    \"@types/node\": \"^18.11.18\",\n    \"@types/ssh2\": \"^1.11.6\",\n    \"@types/supertest\": \"^2.0.8\",\n    \"chai\": \"^4.3.4\",\n    \"chai-deep-equal-ignore-undefined\": \"^1.1.1\",\n    \"concurrently\": \"^5.3.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"esbuild\": \"^0.25.2\",\n    \"fishery\": \"^2.3.1\",\n    \"ioredis-mock\": \"^8.9.0\",\n    \"jest\": \"^29.7.0\",\n    \"jest-html-reporters\": \"^3.1.7\",\n    \"jest-junit\": \"^16.0.0\",\n    \"jest-when\": \"^3.2.1\",\n    \"joi\": \"^17.4.0\",\n    \"mocha\": \"^11.7.5\",\n    \"mocha-junit-reporter\": \"^2.2.1\",\n    \"mocha-multi-reporters\": \"^1.5.1\",\n    \"nock\": \"^13.3.0\",\n    \"nyc\": \"^15.1.0\",\n    \"object-diff\": \"^0.0.4\",\n    \"rimraf\": \"^3.0.2\",\n    \"socket.io-mock\": \"^1.3.2\",\n    \"supertest\": \"^4.0.2\",\n    \"ts-jest\": \"^29.2.5\",\n    \"ts-loader\": \"^6.2.1\",\n    \"ts-mocha\": \"^11.1.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^3.9.0\",\n    \"tsconfig-paths-webpack-plugin\": \"^3.3.0\",\n    \"typescript\": \"^4.8.2\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\n      \"js\",\n      \"json\",\n      \"ts\"\n    ],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".spec.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"coverageDirectory\": \"../report/coverage\",\n    \"coveragePathIgnorePatterns\": [\n      \"/node_modules/\",\n      \".entity.ts$\",\n      \".spec.ts$\"\n    ],\n    \"testEnvironment\": \"node\",\n    \"setupFilesAfterEnv\": [\n      \"<rootDir>/../.jest.setup.ts\"\n    ],\n    \"moduleNameMapper\": {\n      \"src/(.*)\": \"<rootDir>/$1\",\n      \"apiSrc/(.*)\": \"<rootDir>/$1\",\n      \"tests/(.*)\": \"<rootDir>/__tests__/$1\"\n    },\n    \"reporters\": [\n      \"default\",\n      [\n        \"jest-html-reporters\",\n        {\n          \"publicPath\": \"./report\",\n          \"filename\": \"index.html\"\n        }\n      ]\n    ]\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/package.tmp.json",
    "content": "{\n  \"name\": \"redisinsight-api\",\n  \"version\": \"2.1.0\",\n  \"description\": \"RedisInsight API\",\n  \"author\": \"Artyom Podymov <artyom.podymov@softeq.com>,\",\n  \"private\": true,\n  \"license\": \"UNLICENSED\",\n  \"scripts\": {\n    \"prebuild\": \"rimraf dist\",\n    \"build\": \"nest build\",\n    \"build:prod\": \"rimraf dist && nest build && cross-env NODE_ENV=production\",\n    \"build:stage\": \"rimraf dist && nest build && cross-env NODE_ENV=staging\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"start\": \"nest start\",\n    \"start:dev\": \"cross-env NODE_ENV=development nest start --watch\",\n    \"start:debug\": \"nest start --debug --watch\",\n    \"start:stage\": \"rimraf dist && nest build && cross-env NODE_ENV=staging node dist/src/main\",\n    \"start:prod\": \"rimraf dist && nest build && cross-env NODE_ENV=production node dist/src/main\",\n    \"test\": \"../../node_modules/.bin/jest -w 1\",\n    \"test:watch\": \"jest --watch -w 1\",\n    \"test:cov\": \"../../node_modules/.bin/jest --coverage -w 1\",\n    \"test:debug\": \"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand -w 1\",\n    \"test:e2e\": \"jest --config ./test/jest-e2e.json -w 1\",\n    \"typeorm\": \"ts-node -r tsconfig-paths/register ../../node_modules/typeorm/cli.js --config ./config/ormconfig.ts\",\n    \"typeorm:migrate\": \"cross-env NODE_ENV=production yarn typeorm migration:generate -- -n migration\",\n    \"typeorm:run\": \"yarn typeorm migration:run\"\n  },\n  \"dependencies\": {\n    \"sql.js\": \"^1.4.0\"\n  },\n  \"devDependencies\": {\n    \"@nestjs/cli\": \"^7.5.4\",\n    \"cross-env\": \"^7.0.3\",\n    \"jest\": \"^26.6.3\",\n    \"jest-when\": \"^3.2.1\",\n    \"rimraf\": \"^3.0.2\"\n  },\n  \"jest\": {\n    \"moduleFileExtensions\": [\"js\", \"json\", \"ts\"],\n    \"rootDir\": \"src\",\n    \"testRegex\": \".spec.ts$\",\n    \"transform\": {\n      \"^.+\\\\.(t|j)s$\": \"ts-jest\"\n    },\n    \"coverageDirectory\": \"../coverage\",\n    \"coveragePathIgnorePatterns\": [\n      \"/node_modules/\",\n      \".entity.ts$\",\n      \".spec.ts$\"\n    ],\n    \"testEnvironment\": \"node\",\n    \"moduleNameMapper\": {\n      \"src/(.*)\": \"<rootDir>/$1\",\n      \"apiSrc/(.*)\": \"<rootDir>/$1\",\n      \"tests/(.*)\": \"<rootDir>/__tests__/$1\"\n    },\n    \"setupFilesAfterEnv\": [\"../test/jest.setup.ts\"]\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/patches/redis-parser+3.0.0.patch",
    "content": "diff --git a/node_modules/redis-parser/lib/parser.js b/node_modules/redis-parser/lib/parser.js\nindex 5d2532a..9f1bfe0 100644\n--- a/node_modules/redis-parser/lib/parser.js\n+++ b/node_modules/redis-parser/lib/parser.js\n@@ -12,6 +12,10 @@ var interval = null\n var counter = 0\n var notDecreased = 0\n\n+const MAX_STRING_SIZE = parseInt(process.env.RI_CLIENTS_MAX_STRING_SIZE, 10) || Number.MAX_SAFE_INTEGER;\n+const TRUNCATED_STRING_SIZE = parseInt(process.env.RI_CLIENTS_TRUNCATED_STRING_SIZE, 10) || 30;\n+const TRUNCATED_PREFIX = Buffer.from(process.env.RI_CLIENTS_TRUNCATED_STRING_PREFIX || '[Truncated due to length]')\n+\n /**\n  * Used for integer numbers only\n  * @param {JavascriptRedisParser} parser\n@@ -92,12 +96,21 @@ function parseSimpleString (parser) {\n   while (offset < length) {\n     if (buffer[offset++] === 13) { // \\r\\n\n       parser.offset = offset + 1\n+\n+      // truncate buffer if needed\n+      const responseBuffer = truncateBufferIfNeeded(parser.buffer.slice(start, offset - 1))\n+\n       if (parser.optionReturnBuffers === true) {\n-        return parser.buffer.slice(start, offset - 1)\n+        return responseBuffer\n       }\n-      return parser.buffer.toString('utf8', start, offset - 1)\n+      return responseBuffer.toString('utf8')\n     }\n   }\n+\n+  // handle big simple strings\n+  if (parser.buffer.length - parser.offset > MAX_STRING_SIZE) {\n+    parser.buffer = parser.buffer.slice(0, MAX_STRING_SIZE + parser.offset + 1)\n+  }\n }\n\n /**\n@@ -159,10 +172,30 @@ function parseBulkString (parser) {\n   }\n   const start = parser.offset\n   parser.offset = offset + 2\n+\n+  // truncate buffer before response if needed\n+  const responseBuffer = truncateBufferIfNeeded(parser.buffer.slice(start, offset))\n+\n   if (parser.optionReturnBuffers === true) {\n-    return parser.buffer.slice(start, offset)\n+    return responseBuffer\n   }\n-  return parser.buffer.toString('utf8', start, offset)\n+  return responseBuffer.toString('utf8')\n+}\n+\n+/**\n+ * Truncates Buffer when exceeded MAX_STRING_SIZE to TRUNCATED_STRING_SIZE\n+ * Adds prefix and suffix to the value\n+ * It will be not possible to use such values in the future\n+ * Truncated values should be just shown without any actions on them\n+ * @param {Buffer} buffer\n+ * @returns {Buffer}\n+ */\n+function truncateBufferIfNeeded(buffer) {\n+  if (buffer.length >= MAX_STRING_SIZE) {\n+    return Buffer.concat([TRUNCATED_PREFIX, Buffer.from(' '), buffer.subarray(0, TRUNCATED_STRING_SIZE), Buffer.from('...')])\n+  }\n+\n+  return buffer;\n }\n\n /**\n@@ -418,7 +451,7 @@ function concatBulkBuffer (parser) {\n   }\n   list[i].copy(bufferPool, bufferOffset, 0, offset - 2)\n   bufferOffset += offset - 2\n-  return bufferPool.slice(start, bufferOffset)\n+  return truncateBufferIfNeeded(bufferPool.slice(start, bufferOffset))\n }\n\n class JavascriptRedisParser {\n@@ -522,8 +555,14 @@ class JavascriptRedisParser {\n       }\n       this.returnReply(tmp)\n     } else {\n-      this.bufferCache.push(buffer)\n-      this.totalChunkSize += buffer.length\n+      // ignore entire chunk\n+      const rem = this.totalChunkSize - this.offset;\n+      if (rem < MAX_STRING_SIZE || (rem + buffer.length <= MAX_STRING_SIZE)) {\n+        this.bufferCache.push(buffer)\n+        this.totalChunkSize += buffer.length\n+      } else {\n+        this.bigStrSize -= buffer.length\n+      }\n       return\n     }\n\n"
  },
  {
    "path": "redisinsight/api/scripts/default-commands.ts",
    "content": "import axios from 'axios';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { get } from '../src/utils/config';\n\nconst PATH_CONFIG = get('dir_path');\nconst COMMANDS_CONFIG = get('commands');\n\nasync function init() {\n  try {\n    await Promise.all(\n      COMMANDS_CONFIG.map(async ({ name, url, defaultUrl }) => {\n        try {\n          console.log(`Trying to get ${name} commands...`);\n          const { data } = await axios.get(defaultUrl || url, {\n            responseType: 'text',\n            transformResponse: [(raw) => raw],\n          });\n\n          if (!fs.existsSync(PATH_CONFIG.defaultCommandsDir)) {\n            fs.mkdirSync(PATH_CONFIG.defaultCommandsDir, { recursive: true });\n          }\n\n          fs.writeFileSync(\n            path.join(PATH_CONFIG.defaultCommandsDir, `${name}.json`),\n            JSON.stringify(JSON.parse(data)), // check that we received proper json object\n          );\n          console.log(`Successfully generated default ${name} commands`);\n        } catch (error) {\n          console.error(`Unable to update ${name} commands`, error);\n        }\n      }),\n    );\n\n    process.exit(0);\n  } catch (e) {\n    console.error(\n      'Something went wrong trying to get default commands jsons',\n      e,\n    );\n    process.exit(1);\n  }\n}\n\ninit();\n"
  },
  {
    "path": "redisinsight/api/scripts/default-content.ts",
    "content": "import * as path from 'path';\nimport {\n  getFile,\n  updateFolderFromArchive,\n  updateFile,\n} from '../src/utils/file-helper';\nimport { get } from '../src/utils/config';\n\nconst PATH_CONFIG = get('dir_path');\nconst CONTENT_CONFIG = get('content');\n\nconst archiveUrl = new URL(\n  path.join(CONTENT_CONFIG.updateUrl, CONTENT_CONFIG.zip),\n).toString();\n\nconst buildInfoUrl = new URL(\n  path.join(CONTENT_CONFIG.updateUrl, CONTENT_CONFIG.buildInfo),\n).toString();\n\nasync function init() {\n  try {\n    // get archive\n    const data = await getFile(archiveUrl);\n\n    // extract archive to default folder\n    await updateFolderFromArchive(PATH_CONFIG.defaultContent, data);\n\n    // get build info\n    const buildInfo = await getFile(buildInfoUrl);\n\n    // save build info to default folder\n    await updateFile(\n      PATH_CONFIG.defaultContent,\n      CONTENT_CONFIG.buildInfo,\n      buildInfo,\n    );\n\n    process.exit(0);\n  } catch (e) {\n    console.error(\n      'Something went wrong trying to get default commands jsons',\n      e,\n    );\n    process.exit(1);\n  }\n}\n\ninit();\n"
  },
  {
    "path": "redisinsight/api/scripts/default-tutorials.ts",
    "content": "import * as path from 'path';\nimport {\n  getFile,\n  updateFolderFromArchive,\n  updateFile,\n} from '../src/utils/file-helper';\nimport { get } from '../src/utils/config';\n\nconst PATH_CONFIG = get('dir_path');\nconst TUTORIALS_CONFIG = get('tutorials');\n\nconst archiveUrl = new URL(\n  path.join(TUTORIALS_CONFIG.updateUrl, TUTORIALS_CONFIG.zip),\n).toString();\n\nconst buildInfoUrl = new URL(\n  path.join(TUTORIALS_CONFIG.updateUrl, TUTORIALS_CONFIG.buildInfo),\n).toString();\n\nasync function init() {\n  try {\n    // get archive\n    const data = await getFile(archiveUrl);\n\n    // extract archive to default folder\n    await updateFolderFromArchive(PATH_CONFIG.defaultTutorials, data);\n\n    // get build info\n    const buildInfo = await getFile(buildInfoUrl);\n\n    // save build info to default folder\n    await updateFile(\n      PATH_CONFIG.defaultTutorials,\n      TUTORIALS_CONFIG.buildInfo,\n      buildInfo,\n    );\n\n    process.exit(0);\n  } catch (e) {\n    console.error(\n      'Something went wrong trying to get default tutorials archive',\n      e,\n    );\n    process.exit(1);\n  }\n}\n\ninit();\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/ai.ts",
    "content": "import {\n  AiChat,\n  AiChatMessage,\n  AiChatMessageType,\n} from 'src/modules/ai/chat/models';\nimport { Readable } from 'stream';\nimport * as MockSocket from 'socket.io-mock';\nimport { AxiosError } from 'axios';\nimport { SendAiChatMessageDto } from 'src/modules/ai/chat/dto/send.ai-chat.message.dto';\nimport { mockCloudSession } from 'src/__mocks__/cloud-session';\nimport {\n  AiQueryIntermediateStep,\n  AiQueryIntermediateStepType,\n  AiQueryMessage,\n} from 'src/modules/ai/query/models';\nimport { AiQueryMessageEntity } from 'src/modules/ai/query/entities/ai-query.message.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\n\nexport const mockAiChatId =\n  '0539879dc020add5abb33f6f60a07fe8d5a0b9d61c81c9d79d77f9b1b2f2e239';\n\nexport const mockAiChatBadRequestError = {\n  message: 'Bad request',\n  response: {\n    status: 400,\n  },\n} as AxiosError;\n\nexport const mockAiChatUnauthorizedError = {\n  message: 'Request failed with status code 401',\n  response: {\n    status: 401,\n  },\n} as AxiosError;\n\nexport const mockAiChatAccessDeniedError = {\n  message: 'Access denied',\n  response: {\n    status: 403,\n  },\n} as AxiosError;\n\nexport const mockAiChatNotFoundError = {\n  message: 'Requested resource was not found',\n  response: {\n    status: 404,\n  },\n} as AxiosError;\n\nexport const mockAiChatInternalServerError = {\n  message: 'Server error',\n  response: {\n    status: 500,\n  },\n} as AxiosError;\n\nexport const mockHumanMessage1Response = {\n  type: AiChatMessageType.HumanMessage,\n  content: 'Question 1',\n};\n\nexport const mockHumanMessage2Response = {\n  type: AiChatMessageType.HumanMessage,\n  content: 'Question 2',\n};\n\nexport const mockAiMessage1Response = {\n  type: AiChatMessageType.AiMessage,\n  content: 'Answer 1',\n};\nexport const mockAiMessage2Response = {\n  type: AiChatMessageType.AiMessage,\n  content: 'Answer 2',\n};\n\nexport const mockAiHistoryApiResponse = [\n  mockHumanMessage1Response,\n  mockAiMessage1Response,\n  mockHumanMessage2Response,\n  mockAiMessage2Response,\n];\n\nexport const mockAiChat = Object.assign(new AiChat(), {\n  id: mockAiChatId,\n  messages: [\n    Object.assign(new AiChatMessage(), mockHumanMessage1Response),\n    Object.assign(new AiChatMessage(), mockAiMessage1Response),\n    Object.assign(new AiChatMessage(), mockHumanMessage2Response),\n    Object.assign(new AiChatMessage(), mockAiMessage2Response),\n  ],\n});\nexport const getMockedReadableStream = () => new Readable();\nexport const mockAiResponseStream = getMockedReadableStream();\n\nexport const mockSendAiChatMessageDto = Object.assign(\n  new SendAiChatMessageDto(),\n  {\n    content: mockHumanMessage1Response.content,\n  },\n);\n\nexport const mockAiQueryConversationId = 'conversation-id-uuid';\nexport const mockAiQueryDatabaseId = 'database-id-uuid';\nexport const mockAiQueryAccountId = 'account-id';\n\nexport const mockAiQueryHumanMessage: AiQueryMessage = Object.assign(\n  new AiQueryMessage(),\n  {\n    id: 'uuid-for-human-message-1',\n    type: AiChatMessageType.HumanMessage,\n    content: 'human message 1',\n    conversationId: mockAiQueryConversationId,\n    databaseId: mockAiQueryDatabaseId,\n    accountId: mockAiQueryAccountId,\n  },\n);\n\nexport const mockAiQueryHumanMessageEntity: AiQueryMessageEntity =\n  Object.assign(new AiQueryMessageEntity(), {\n    ...mockAiQueryHumanMessage,\n    content: 'human message 1 ENCRYPTED',\n    encryption: EncryptionStrategy.KEYTAR,\n  });\n\nexport const mockAiQueryHumanMessage2: AiQueryMessage = Object.assign(\n  new AiQueryMessage(),\n  {\n    id: 'uuid-for-human-message-2',\n    type: AiChatMessageType.HumanMessage,\n    content: 'human message 2',\n    conversationId: mockAiQueryConversationId,\n    databaseId: mockAiQueryDatabaseId,\n    accountId: mockAiQueryAccountId,\n  },\n);\n\nexport const mockAiQueryHumanMessageEntity2: AiQueryMessageEntity =\n  Object.assign(new AiQueryMessageEntity(), {\n    ...mockAiQueryHumanMessage2,\n    content: 'human message 2',\n    encryption: EncryptionStrategy.PLAIN,\n  });\n\nexport const mockAiQueryAiIntermediateStep: AiQueryIntermediateStep =\n  Object.assign(new AiQueryIntermediateStep(), {\n    type: AiQueryIntermediateStepType.TOOL,\n    data: 'intermediate tool 1',\n  });\n\nexport const mockAiQueryAiIntermediateStep2: AiQueryIntermediateStep =\n  Object.assign(new AiQueryIntermediateStep(), {\n    type: AiQueryIntermediateStepType.TOOL_CALL,\n    data: 'intermediate tool call 2',\n  });\n\nexport const mockAiQueryAiResponse: AiQueryMessage = Object.assign(\n  new AiQueryMessage(),\n  {\n    id: 'uuid-for-ai-response-1',\n    type: AiChatMessageType.AiMessage,\n    content: 'ai response 1',\n    steps: [mockAiQueryAiIntermediateStep, mockAiQueryAiIntermediateStep2],\n    conversationId: mockAiQueryConversationId,\n    databaseId: mockAiQueryDatabaseId,\n    accountId: mockAiQueryAccountId,\n  },\n);\n\nexport const mockAiQueryAiResponseEntity: AiQueryMessageEntity = Object.assign(\n  new AiQueryMessageEntity(),\n  {\n    ...mockAiQueryAiResponse,\n    content: 'ai response 1 ENCRYPTED',\n    encryption: EncryptionStrategy.KEYTAR,\n  },\n);\n\nexport const mockAiQueryAiResponse2: AiQueryMessage = Object.assign(\n  new AiQueryMessage(),\n  {\n    id: 'uuid-for-ai-response-2',\n    type: AiChatMessageType.AiMessage,\n    content: 'ai response 2',\n    steps: [],\n    conversationId: mockAiQueryConversationId,\n    databaseId: mockAiQueryDatabaseId,\n    accountId: mockAiQueryAccountId,\n  },\n);\n\nexport const mockAiQueryAiResponseEntity2: AiQueryMessageEntity = Object.assign(\n  new AiQueryMessageEntity(),\n  {\n    ...mockAiQueryAiResponse2,\n    content: 'ai response 2',\n    encryption: EncryptionStrategy.PLAIN,\n    steps: '[]',\n  },\n);\n\nexport const mockAiQueryHistory = [\n  mockAiQueryHumanMessage,\n  mockAiQueryAiResponse,\n];\n\nexport const mockAiQueryIndex = 'idx:bicycle';\n\nexport const mockAiQueryIndexInfoReply = [\n  'index_name',\n  mockAiQueryIndex,\n  'index_options',\n  [],\n  'index_definition',\n  ['key_type', 'JSON', 'prefixes', ['bicycle:'], 'default_score', '1'],\n  'attributes',\n  [\n    [\n      'identifier',\n      '$.description',\n      'attribute',\n      'description',\n      'type',\n      'TEXT',\n      'WEIGHT',\n      '1',\n    ],\n    ['identifier', '$.price', 'attribute', 'price', 'type', 'NUMERIC'],\n    [\n      'identifier',\n      '$.type',\n      'attribute',\n      'type',\n      'type',\n      'TAG',\n      'SEPARATOR',\n      '',\n    ],\n  ],\n  'num_docs',\n  '122',\n  'max_doc_id',\n  '122',\n  'num_terms',\n  '550',\n  'num_records',\n  '2964',\n  'inverted_sz_mb',\n  '0.0145721435546875',\n  'vector_index_sz_mb',\n  '0',\n  'total_inverted_index_blocks',\n  '576',\n  'offset_vectors_sz_mb',\n  '0.0024938583374023438',\n  'doc_table_size_mb',\n  '0.009075164794921875',\n  'sortable_values_size_mb',\n  '0',\n  'key_table_size_mb',\n  '0.0038166046142578125',\n  'records_per_doc_avg',\n  '24.295082092285156',\n  'bytes_per_record_avg',\n  '5.1551957130432129',\n  'offsets_per_term_avg',\n  '0.88225370645523071',\n  'offset_bits_per_record_avg',\n  '8',\n  'hash_indexing_failures',\n  '0',\n  'indexing',\n  '0',\n  'percent_indexed',\n  '1',\n  'gc_stats',\n  [\n    'bytes_collected',\n    '0',\n    'total_ms_run',\n    '0',\n    'total_cycles',\n    '0',\n    'average_cycle_time_ms',\n    '-nan',\n    'last_run_time_ms',\n    '0',\n    'gc_numeric_trees_missed',\n    '0',\n    'gc_blocks_denied',\n    '0',\n  ],\n  'cursor_stats',\n  [\n    'global_idle',\n    0,\n    'global_total',\n    0,\n    'index_capacity',\n    128,\n    'index_total',\n    0,\n  ],\n];\n\nexport const mockAiQueryIndexInfoObject = {\n  index_name: mockAiQueryIndex,\n  index_options: [],\n  index_definition: {\n    key_type: 'JSON',\n    prefixes: ['bicycle:'],\n    default_score: '1',\n  },\n  attributes: [\n    {\n      identifier: '$.description',\n      attribute: 'description',\n      type: 'TEXT',\n      WEIGHT: '1',\n    },\n    {\n      identifier: '$.price',\n      attribute: 'price',\n      type: 'NUMERIC',\n    },\n    {\n      identifier: '$.type',\n      attribute: 'type',\n      type: 'TAG',\n      SEPARATOR: '',\n    },\n  ],\n  num_docs: '122',\n  max_doc_id: '122',\n  num_terms: '550',\n  num_records: '2964',\n  inverted_sz_mb: '0.0145721435546875',\n  vector_index_sz_mb: '0',\n  total_inverted_index_blocks: '576',\n  offset_vectors_sz_mb: '0.0024938583374023438',\n  doc_table_size_mb: '0.009075164794921875',\n  sortable_values_size_mb: '0',\n  key_table_size_mb: '0.0038166046142578125',\n  records_per_doc_avg: '24.295082092285156',\n  bytes_per_record_avg: '5.1551957130432129',\n  offsets_per_term_avg: '0.88225370645523071',\n  offset_bits_per_record_avg: '8',\n  hash_indexing_failures: '0',\n  indexing: '0',\n  percent_indexed: '1',\n  gc_stats: [\n    'bytes_collected',\n    '0',\n    'total_ms_run',\n    '0',\n    'total_cycles',\n    '0',\n    'average_cycle_time_ms',\n    '-nan',\n    'last_run_time_ms',\n    '0',\n    'gc_numeric_trees_missed',\n    '0',\n    'gc_blocks_denied',\n    '0',\n  ],\n  cursor_stats: [\n    'global_idle',\n    0,\n    'global_total',\n    0,\n    'index_capacity',\n    128,\n    'index_total',\n    0,\n  ],\n};\n\nexport const mockAiQuerySchema = {\n  $schema: 'http://json-schema.org/draft-06/schema#',\n  $ref: '#/definitions/IdxBicycle',\n  definitions: {\n    IdxBicycle: {\n      type: 'object',\n      additionalProperties: false,\n      properties: {\n        price: { type: 'integer' },\n        type: { type: 'string' },\n        description: { type: 'string' },\n      },\n      required: ['description', 'price', 'type'],\n      title: 'IdxBicycle',\n    },\n  },\n};\n\nexport const mockAiQuerySchemaForHash = {\n  $schema: 'http://json-schema.org/draft-06/schema#',\n  $ref: '#/definitions/IdxBicycle',\n  definitions: {\n    IdxBicycle: {\n      type: 'object',\n      additionalProperties: false,\n      properties: {\n        price: { type: 'string', format: 'integer' },\n        type: { type: 'string' },\n        description: { type: 'string' },\n      },\n      required: ['description', 'price', 'type'],\n      title: 'IdxBicycle',\n    },\n  },\n};\n\nexport const mockAiQueryGetDescriptionTopValuesReply = [\n  '3',\n  [\n    '1',\n    'The Freedom offers eco-friendly mobility without compromising on style and performance!',\n  ],\n  [\n    '1',\n    'The Explorer conquers rugged trails and mountain peaks with confidence and agility!',\n  ],\n  [\n    '1',\n    'The Pioneer leads the way through challenging trails and rocky terrain with confidence!',\n  ],\n];\n\nexport const mockAiQueryGetPriceTopValuesReply = [\n  '3',\n  ['1', '320'],\n  ['1', '329'],\n  ['1', '380'],\n];\n\nexport const mockAiQueryGetTypeTopValuesReply = [\n  '3',\n  ['1', 'Mountain'],\n  ['1', 'City'],\n  ['1', 'Kids'],\n];\n\nexport const mockAiQueryIndexContext = {\n  index_name: mockAiQueryIndex,\n  create_statement: `FT.CREATE ${mockAiQueryIndex} ON JSON PREFIX 1 bicycle: SCHEMA $.description AS description TEXT $.price AS price NUMERIC $.type AS type TAG`,\n  documents_schema: mockAiQuerySchema,\n  documents_type: 'JSON',\n  attributes: {\n    description: {\n      identifier: '$.description',\n      attribute: 'description',\n      type: 'TEXT',\n      WEIGHT: '1',\n      distinct_count: 3,\n      top_values: [\n        {\n          value:\n            'The Freedom offers eco-friendly mobility without compromising on style and performance!',\n        },\n        {\n          value:\n            'The Explorer conquers rugged trails and mountain peaks with confidence and agility!',\n        },\n        {\n          value:\n            'The Pioneer leads the way through challenging trails and rocky terrain with confidence!',\n        },\n      ],\n    },\n    price: {\n      identifier: '$.price',\n      attribute: 'price',\n      type: 'NUMERIC',\n      distinct_count: 3,\n      top_values: [{ value: '320' }, { value: '329' }, { value: '380' }],\n    },\n    type: {\n      identifier: '$.type',\n      attribute: 'type',\n      type: 'TAG',\n      SEPARATOR: '',\n      distinct_count: 3,\n      top_values: [{ value: 'Mountain' }, { value: 'City' }, { value: 'Kids' }],\n    },\n  },\n};\n\nexport const mockAiQueryFullDbContext = {\n  [mockAiQueryIndex]: {\n    index_name: mockAiQueryIndex,\n    attributes: [\n      {\n        identifier: '$.description',\n        attribute: 'description',\n        type: 'TEXT',\n        WEIGHT: '1',\n      },\n      {\n        identifier: '$.price',\n        attribute: 'price',\n        type: 'NUMERIC',\n      },\n      {\n        identifier: '$.type',\n        attribute: 'type',\n        type: 'TAG',\n        SEPARATOR: '',\n      },\n    ],\n    documents_type: 'JSON',\n  },\n};\n\nexport const mockAiQueryJsonReply = JSON.stringify({\n  price: 490,\n  type: 'Touring',\n  description:\n    'The Wanderer takes you on epic adventures across varied landscapes with comfort and endurance!',\n});\n\nexport const mockAiQueryHScanReply = [\n  0,\n  [\n    'price',\n    '490',\n    'type',\n    'Touring',\n    'description',\n    'The Wanderer takes you on epic adventures across varied landscapes with comfort and endurance!',\n  ],\n];\n\nexport const mockAiQueryAuth = {\n  accountId: mockCloudSession.user.currentAccountId,\n  csrf: mockCloudSession.csrf,\n  sessionId: mockCloudSession.apiSessionId,\n};\n\nexport const mockAiQuerySocket = new MockSocket();\n\nexport const mockConvAiProvider = jest.fn(() => ({\n  auth: jest.fn().mockResolvedValue(mockAiChatId),\n  postMessage: jest.fn().mockResolvedValue(mockAiResponseStream),\n  getHistory: jest.fn().mockResolvedValue(mockAiHistoryApiResponse),\n  reset: jest.fn(),\n}));\n\nexport const mockAiQueryProvider = jest.fn(() => ({\n  getSocket: jest.fn().mockResolvedValue(mockAiQuerySocket.socketClient),\n}));\n\nexport const mockAiQueryAuthProvider = jest.fn(() => ({\n  getAuthData: jest.fn().mockResolvedValue(mockAiQueryAuth),\n}));\n\nexport const mockAiQueryMessageRepository = jest.fn(() => ({\n  list: jest.fn().mockResolvedValue(mockAiQueryHistory),\n  createMany: jest.fn(),\n  clearHistory: jest.fn(),\n}));\n\nexport const mockAiQueryContextRepository = jest.fn(() => ({\n  getFullDbContext: jest.fn().mockResolvedValue(mockAiQueryFullDbContext),\n  setFullDbContext: jest.fn().mockResolvedValue(mockAiQueryFullDbContext),\n  getIndexContext: jest.fn().mockResolvedValue(mockAiQueryIndexContext),\n  setIndexContext: jest.fn().mockResolvedValue(mockAiQueryIndexContext),\n  reset: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/analytics.ts",
    "content": "export const mockCliAnalyticsService = () => ({\n  sendClientCreatedEvent: jest.fn(),\n  sendClientCreationFailedEvent: jest.fn(),\n  sendClientDeletedEvent: jest.fn(),\n  sendClientRecreatedEvent: jest.fn(),\n  sendCommandExecutedEvent: jest.fn(),\n  sendCommandErrorEvent: jest.fn(),\n  sendClusterCommandExecutedEvent: jest.fn(),\n  sendConnectionErrorEvent: jest.fn(),\n  sendIndexInfoEvent: jest.fn(),\n});\n\nexport const mockWorkbenchAnalyticsService = () => ({\n  sendCommandExecutedEvents: jest.fn(),\n  sendCommandExecutedEvent: jest.fn(),\n  sendCommandDeletedEvent: jest.fn(),\n  sendIndexInfoEvent: jest.fn(),\n});\n\nexport const mockSettingsAnalyticsService = () => ({\n  sendAnalyticsAgreementChange: jest.fn(),\n  sendSettingsUpdatedEvent: jest.fn(),\n});\n\nexport const mockPubSubAnalyticsService = () => ({\n  sendMessagePublishedEvent: jest.fn(),\n  sendChannelSubscribeEvent: jest.fn(),\n  sendChannelUnsubscribeEvent: jest.fn(),\n});\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/app-settings.ts",
    "content": "import { Settings } from 'src/modules/settings/models/settings';\nimport { Agreements } from 'src/modules/settings/models/agreements';\nimport { mockUserId } from 'src/__mocks__/user';\nimport { GetAppSettingsResponse } from 'src/modules/settings/dto/settings.dto';\nimport { AgreementsEntity } from 'src/modules/settings/entities/agreements.entity';\nimport { SettingsEntity } from 'src/modules/settings/entities/settings.entity';\n\nexport const mockSettings = Object.assign(new Settings(), {\n  id: mockUserId,\n  data: {\n    theme: 'DARK',\n    scanThreshold: 500,\n    batchSize: 10,\n    dateFormat: null,\n    timezone: null,\n  },\n});\n\nexport const mockSettingsEntity = Object.assign(new SettingsEntity(), {\n  id: mockSettings.id,\n  data: JSON.stringify(mockSettings.data),\n});\n\nexport const mockAgreements = Object.assign(new Agreements(), {\n  id: mockUserId,\n  version: '1.0.0',\n  data: {\n    eula: true,\n    analytics: true,\n    encryption: true,\n    notifications: true,\n  },\n});\n\nexport const mockAgreementsEntity = Object.assign(new AgreementsEntity(), {\n  id: mockAgreements.id,\n  version: mockAgreements.version,\n  data: JSON.stringify(mockAgreements.data),\n});\n\nexport const mockAppSettings = Object.assign(new GetAppSettingsResponse(), {\n  ...mockSettings.data,\n  agreements: {\n    version: mockAgreements.version,\n    ...mockAgreements.data,\n  },\n});\n\nexport const mockAppSettingsWithoutPermissions = Object.assign(\n  new GetAppSettingsResponse(),\n  {\n    ...mockSettings.data,\n    agreements: {\n      version: mockAgreements.version,\n      eula: false,\n      analytics: false,\n      encryption: false,\n      notifications: false,\n    },\n  },\n);\n\nexport const mockAppSettingsInitial = Object.assign(\n  new GetAppSettingsResponse(),\n  {\n    agreements: null,\n  },\n);\n\nexport const mockAgreementsRepository = jest.fn(() => ({\n  getOrCreate: jest.fn(),\n  update: jest.fn(),\n}));\n\nexport const mockSettingsRepository = jest.fn(() => ({\n  getOrCreate: jest.fn(),\n  update: jest.fn(),\n}));\n\nexport const mockSettingsService = jest.fn(() => ({\n  getAppSettings: jest.fn(),\n  updateAppSettings: jest.fn(),\n  getAgreementsSpec: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/autodiscovery.ts",
    "content": "export const mockWinNetstat =\n  '' +\n  'Proto  Local Address          Foreign Address        State           PID\\n' +\n  'TCP    0.0.0.0:5000           0.0.0.0:0              LISTENING       13728\\n' +\n  'TCP    0.0.0.0:6379           0.0.0.0:0              LISTENING       13728\\n' +\n  'TCP    127.0.0.1:6379         0.0.0.0:0              LISTENING       13728\\n' +\n  'TCP    *:6380                 0.0.0.0:0              LISTENING       13728\\n' +\n  'TCP    [::]:135               [::]:0                 LISTENING       1100\\n' +\n  'TCP    [::]:445               [::]:0                 LISTENING       4\\n' +\n  'TCP    [::]:808               [::]:0                 LISTENING       6084\\n' +\n  'TCP    [::]:2701              [::]:0                 LISTENING       6056\\n' +\n  'TCP    [::]:5000              [::]:0                 LISTENING       6056\\n' +\n  'TCP                           *:*                    LISTENING       6056';\n\nexport const mockLinuxNetstat =\n  '' +\n  'Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    \\n' +\n  'tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      -                   \\n' +\n  'tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      -                   \\n' +\n  'tcp        0      0 127.0.0.1:6379          0.0.0.0:*               LISTEN      -                   \\n' +\n  'tcp        0      0 *:6380                  0.0.0.0:*               LISTEN      -                   \\n' +\n  'tcp6       0      0 :::28100                :::*                    LISTEN      -                   \\n' +\n  'tcp6       0      0 :::8100                 :::*                    LISTEN      -                   \\n' +\n  'tcp6       0      0 :::8101                 :::*                    LISTEN      -                   \\n' +\n  'tcp6       0      0 :::8102                 :::*                    LISTEN      -                   \\n' +\n  'tcp6       0      0 :::8103                 :::*                    LISTEN      -                   \\n' +\n  'tcp6       0      0 :::8200                 :::*                    LISTEN      -                   \\n' +\n  'tcp6       0      0 ::1:6379                :::*                    LISTEN      -                   \\n';\n\n/* eslint-disable max-len */\nexport const mockMacNetstat =\n  '' +\n  'Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)     rhiwat shiwat    pid   epid  state    options\\n' +\n  'tcp4       0      0  10.55.1.235.5000       10.55.1.235.52217      FIN_WAIT_2  407280 146988  30555      0 0x2131 0x00000104\\n' +\n  'tcp4       0      0  10.55.1.235.6379       10.55.1.235.5001       CLOSE_WAIT  407682 146988    872      0 0x0122 0x00000008\\n' +\n  'tcp4       0      0  127.0.0.1.6379         127.0.0.1.52216        FIN_WAIT_2  403346 146988  24687      0 0x2131 0x00000104\\n' +\n  'tcp46      0      0  *.6380                 *.*                    LISTEN      131072 131072  31195      0 0x0100 0x00000106\\n' +\n  'tcp6       0      0  ::1.5002               ::1.52167              ESTABLISHED 405692 146808  31195      0 0x0102 0x00000104\\n' +\n  'tcp6       0      0  ::1.52167              ::1.5002               ESTABLISHED 406172 146808  31200      0 0x0102 0x00000008\\n';\n/* eslint-enable max-len */\n\nexport const mockAutodiscoveryEndpoint = {\n  host: '127.0.0.1',\n  port: 6379,\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/browser-history.ts",
    "content": "import { plainToInstance } from 'class-transformer';\nimport { v4 as uuidv4 } from 'uuid';\nimport { mockDatabase } from 'src/__mocks__';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport {\n  CreateBrowserHistoryDto,\n  BrowserHistory,\n  ScanFilter,\n} from 'src/modules/browser/browser-history/dto';\nimport { BrowserHistoryEntity } from 'src/modules/browser/browser-history/entities/browser-history.entity';\n\nexport const mockBrowserHistoryService = () => ({\n  create: jest.fn(),\n  get: jest.fn(),\n  list: jest.fn(),\n  delete: jest.fn(),\n  bulkDelete: jest.fn(),\n});\n\nexport const mockBrowserHistoryRepository = jest.fn(() => ({\n  create: jest.fn(),\n  get: jest.fn(),\n  list: jest.fn(),\n  delete: jest.fn(),\n  cleanupDatabaseHistory: jest.fn(),\n}));\n\nexport const mockCreateBrowserHistoryDto: CreateBrowserHistoryDto = {\n  mode: BrowserHistoryMode.Pattern,\n  filter: plainToInstance(ScanFilter, {\n    type: RedisDataType.String,\n    match: 'key*',\n  }),\n};\n\nexport const mockBrowserHistoryEntity = new BrowserHistoryEntity({\n  id: uuidv4(),\n  databaseId: mockDatabase.id,\n  filter: 'ENCRYPTED:filter',\n  encryption: 'KEYTAR',\n  createdAt: new Date(),\n});\n\nexport const mockBrowserHistoryPartial: Partial<BrowserHistory> = {\n  ...mockCreateBrowserHistoryDto,\n  databaseId: mockDatabase.id,\n};\n\nexport const mockBrowserHistory = {\n  ...mockBrowserHistoryPartial,\n  id: mockBrowserHistoryEntity.id,\n  createdAt: mockBrowserHistoryEntity.createdAt,\n} as BrowserHistory;\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/bulk-actions.ts",
    "content": "import {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { BulkActionProgress } from 'src/modules/bulk-actions/models/bulk-action-progress';\nimport { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\n\nexport const mockCreateBulkActionDto = {\n  id: 'bulk-action-id',\n  databaseId: 'database-id',\n  type: BulkActionType.Delete,\n};\n\nexport const mockBulkActionOverview = {\n  ...mockCreateBulkActionDto,\n  duration: 100,\n  filter: { match: '*', type: null },\n  progress: {\n    scanned: 0,\n    total: 0,\n  },\n  status: BulkActionStatus.Completed,\n  summary: {\n    failed: 0,\n    processed: 0,\n    succeed: 0,\n    errors: [],\n    keys: [],\n  },\n};\n\nexport const mockDefaultDataManifest = {\n  files: [\n    {\n      path: 'test_common',\n    },\n    {\n      path: 'test_json',\n      modules: ['rejson'],\n    },\n    {\n      path: 'not_existing',\n      modules: ['not_existing_module'],\n    },\n  ],\n};\n\nexport const mockCombinedStream = {\n  append: jest.fn(),\n};\n\nexport const mockBulkActionOverviewMatcher = {\n  ...mockBulkActionOverview,\n  duration: expect.any(Number),\n};\n\nexport const mockBulkActionFilter = new BulkActionFilter();\n\nconst mockKey = 'mockedKey';\nconst mockKeyBuffer = Buffer.from(mockKey);\nconst mockRESPError = 'Reply Error: NOPERM for delete.';\nconst mockRESPErrorBuffer = Buffer.from(mockRESPError);\n\nexport const generateMockBulkActionErrors = (amount: number, raw = true): any =>\n  new Array(amount).fill(1).map(() => ({\n    key: raw ? mockKeyBuffer : mockKey,\n    error: raw ? mockRESPErrorBuffer : mockRESPError,\n  }));\n\nexport const generateMockBulkActionProgress = () => {\n  const progress = new BulkActionProgress();\n\n  progress['total'] = 1_000_000;\n  progress['scanned'] = 1_000_000;\n\n  return progress;\n};\n\nexport const generateMockBulkActionSummary = () => {\n  const summary = new BulkActionSummary();\n\n  summary['processed'] = 1_000_000;\n  summary['succeed'] = 900_000;\n  summary['failed'] = 100_000;\n  summary['errors'] = generateMockBulkActionErrors(500);\n\n  return summary;\n};\n\nexport const mockBulkActionsAnalytics = () => ({\n  sendActionStarted: jest.fn(),\n  sendActionStopped: jest.fn(),\n  sendActionSucceed: jest.fn(),\n  sendActionFailed: jest.fn(),\n  sendImportSamplesUploaded: jest.fn(),\n});\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/certificates.ts",
    "content": "import { pick, omit } from 'lodash';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\n\n// ================== CA Certificate ==================\nexport const mockCaCertificateId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805';\n\nexport const mockCaCertificateCertificateEncrypted =\n  'caCertificate.certificate_ENCRYPTED';\n\nexport const mockCaCertificateCertificatePlain =\n  '-----BEGIN CERTIFICATE-----\\nMIIDejCCAmKgAwIBAgIUehUr5AHdJM';\n\nexport const mockCaCertificate = Object.assign(new CaCertificate(), {\n  id: mockCaCertificateId,\n  name: 'ca-cert',\n  certificate: mockCaCertificateCertificatePlain,\n});\n\nexport const mockCreateCaCertificateDto = Object.assign(\n  new CreateCaCertificateDto(),\n  {\n    ...omit(mockCaCertificate, 'id'),\n  },\n);\n\nexport const mockCaCertificateEntity = Object.assign(\n  new CaCertificateEntity(),\n  {\n    ...mockCaCertificate,\n    certificate: mockCaCertificateCertificateEncrypted,\n    encryption: EncryptionStrategy.KEYTAR,\n  },\n);\n\nexport const mockCaCertificateRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockCaCertificate),\n  list: jest\n    .fn()\n    .mockResolvedValueOnce([\n      pick(mockCaCertificate, 'id', 'name'),\n      pick(mockCaCertificate, 'id', 'name'),\n    ]),\n  create: jest.fn().mockResolvedValue(mockCaCertificate),\n  delete: jest.fn().mockResolvedValue(undefined),\n  cleanupPreSetup: jest.fn().mockResolvedValue({ affected: 0 }),\n}));\n\nexport const mockCaCertificateService = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockCaCertificate),\n}));\n\n// ================== Client Certificate ==================\nexport const mockClientCertificateId = 'a77b23c1-7816-4ea4-b61f-d37f2915f805';\n\nexport const mockClientCertificateCertificateEncrypted =\n  'clientCertificate.certificate_ENCRYPTED';\n\nexport const mockClientCertificateCertificatePlain =\n  '-----BEGIN CERTIFICATE-----\\nMICLIENTCERTIDejCCAmKgAwIB';\n\nexport const mockClientCertificateKeyEncrypted =\n  'clientCertificate.key_ENCRYPTED';\n\nexport const mockClientCertificateKeyPlain =\n  '-----BEGIN PRIVATE KEY-----\\nMICLIENTCERTIDejCCAmKgAwIB';\n\nexport const mockClientCertificate = Object.assign(new ClientCertificate(), {\n  id: mockClientCertificateId,\n  name: 'client-cert',\n  certificate: mockClientCertificateCertificatePlain,\n  key: mockClientCertificateKeyPlain,\n});\n\nexport const mockCreateClientCertificateDto = Object.assign(\n  new CreateClientCertificateDto(),\n  {\n    ...omit(mockClientCertificate, 'id'),\n  },\n);\n\nexport const mockClientCertificateEntity = Object.assign(\n  new ClientCertificateEntity(),\n  {\n    ...mockClientCertificate,\n    certificate: mockClientCertificateCertificateEncrypted,\n    key: mockClientCertificateKeyEncrypted,\n    encryption: EncryptionStrategy.KEYTAR,\n  },\n);\n\nexport const mockClientCertificateRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockClientCertificate),\n  list: jest\n    .fn()\n    .mockResolvedValueOnce([\n      pick(mockClientCertificate, 'id', 'name'),\n      pick(mockClientCertificate, 'id', 'name'),\n    ]),\n  create: jest.fn().mockResolvedValue(mockClientCertificate),\n  delete: jest.fn().mockResolvedValue(undefined),\n  cleanupPreSetup: jest.fn().mockResolvedValue({ affected: 0 }),\n}));\n\nexport const mockClientCertificateService = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockClientCertificate),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-auth.ts",
    "content": "import {\n  CloudAuthIdpType,\n  CloudAuthRequest,\n  CloudAuthResponse,\n  CloudAuthStatus,\n} from 'src/modules/cloud/auth/models';\nimport { mockSessionMetadata } from 'src/__mocks__/common';\n\nexport const mockCloudAuthCode = 'ac_p6vA6A5tF36Jf6twH2cBOqtt7n';\nexport const mockCloudAccessToken = 'at_p6vA6A5tF36Jf6twH2cBOqtt7n';\nexport const mockCloudRefreshToken = 'rt_p6vA6A5tF36Jf6twH2cBOqtt7n';\nexport const mockCloudAccessTokenNew = 'at_p6vA6A5tF36Jf6twH2cBOqtt7n-new';\nexport const mockCloudRefreshTokenNew = 'rt_p6vA6A5tF36Jf6twH2cBOqtt7n-new';\nexport const mockCloudRevokeRefreshTokenHint = 'refresh_token';\n\nexport const mockCloudAuthGoogleIdpConfig = {\n  idpType: 'google',\n  authorizeUrl: 'oauth2/authorize',\n  tokenUrl: 'oauth2/token',\n  revokeTokenUrl: 'oauth2/revoke',\n  issuer: 'https://authorization.server.com',\n  clientId: 'cid_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  pkce: true,\n  redirectUri: 'redisinsight:/cloud/oauth/callback',\n  idp: 'idp_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  scopes: ['offline_access', 'openid', 'email', 'profile'],\n  responseMode: 'query',\n  responseType: 'code',\n};\n\nexport const mockCloudAuthGithubIdpConfig = {\n  idpType: 'github',\n};\n\nexport const mockCloudAuthGoogleTokenParams = {\n  ...mockCloudAuthGoogleIdpConfig,\n  state: 'state_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  nonce: 'nonce_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  ignoreSignature: false,\n  codeVerifier: 'cv_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  codeChallenge: 'cch_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  codeChallengeMethod: 'S256',\n};\n\nexport const mockCloudAuthGithubTokenParams = {\n  ...mockCloudAuthGoogleTokenParams,\n  state: 'state_p6vA6A5tF36Jf6twH2cBOqtt7ngn',\n  idpType: 'github',\n};\n\nexport const mockCloudAuthSsoTokenParams = {\n  ...mockCloudAuthGoogleTokenParams,\n  state: 'state_p6vA6A5tF36Jf6twH2cBOqtt7ssp',\n  idpType: 'sso',\n};\n\nexport const mockCloudAuthGoogleRequest = Object.assign(\n  new CloudAuthRequest(),\n  {\n    ...mockCloudAuthGoogleTokenParams,\n    sessionMetadata: {\n      ...mockSessionMetadata,\n    },\n    tokenManager: { storage: {} },\n    createdAt: new Date(),\n  },\n);\n\nexport const mockCloudAuthSsoRequest = Object.assign(new CloudAuthRequest(), {\n  ...mockCloudAuthGoogleRequest,\n  ...mockCloudAuthSsoTokenParams,\n  tokenManager: { storage: {} },\n  createdAt: new Date(),\n  idpType: CloudAuthIdpType.Sso,\n  idp: 'idp_p6vA6A5tF36Jf6twH2cBOqtSSO',\n});\n\nexport const mockCloudAuthGoogleAuthUrl =\n  `${mockCloudAuthGoogleRequest.issuer}` +\n  `/${mockCloudAuthGoogleRequest.authorizeUrl}` +\n  `?client_id=${mockCloudAuthGoogleRequest.clientId}` +\n  `&${new URLSearchParams({ redirect_uri: mockCloudAuthGoogleRequest.redirectUri }).toString()}` +\n  `&response_type=${mockCloudAuthGoogleRequest.responseType}` +\n  `&response_mode=${mockCloudAuthGoogleRequest.responseMode}` +\n  `&idp=${mockCloudAuthGoogleRequest.idp}` +\n  `&state=${mockCloudAuthGoogleRequest.state}` +\n  `&nonce=${mockCloudAuthGoogleRequest.nonce}` +\n  `&code_challenge_method=${mockCloudAuthGoogleRequest.codeChallengeMethod}` +\n  `&code_challenge=${mockCloudAuthGoogleRequest.codeChallenge}` +\n  `&${new URLSearchParams({ scope: mockCloudAuthGoogleRequest.scopes.join(' ') }).toString()}` +\n  '&prompt=login';\n\nexport const mockCloudAuthSsoAuthUrl =\n  `${mockCloudAuthSsoRequest.issuer}` +\n  `/${mockCloudAuthSsoRequest.authorizeUrl}` +\n  `?client_id=${mockCloudAuthSsoRequest.clientId}` +\n  `&${new URLSearchParams({ redirect_uri: mockCloudAuthSsoRequest.redirectUri }).toString()}` +\n  `&response_type=${mockCloudAuthSsoRequest.responseType}` +\n  `&response_mode=${mockCloudAuthSsoRequest.responseMode}` +\n  `&idp=${mockCloudAuthSsoRequest.idp}` +\n  `&state=${mockCloudAuthSsoRequest.state}` +\n  `&nonce=${mockCloudAuthSsoRequest.nonce}` +\n  `&code_challenge_method=${mockCloudAuthSsoRequest.codeChallengeMethod}` +\n  `&code_challenge=${mockCloudAuthSsoRequest.codeChallenge}` +\n  `&${new URLSearchParams({ scope: mockCloudAuthSsoRequest.scopes.join(' ') }).toString()}` +\n  '&prompt=login';\n\nexport const mockCloudAuthGoogleTokenUrl =\n  `${mockCloudAuthGoogleRequest.issuer}` +\n  `/${mockCloudAuthGoogleRequest.tokenUrl}` +\n  `?client_id=${mockCloudAuthGoogleRequest.clientId}` +\n  '&grant_type=authorization_code' +\n  `&code=${mockCloudAuthCode}` +\n  `&code_verifier=${mockCloudAuthGoogleRequest.codeVerifier}` +\n  `&${new URLSearchParams({ redirect_uri: mockCloudAuthGoogleRequest.redirectUri }).toString()}` +\n  `&state=${mockCloudAuthGoogleRequest.state}` +\n  `&nonce=${mockCloudAuthGoogleRequest.nonce}` +\n  `&idp=${mockCloudAuthGoogleRequest.idp}`;\n\nexport const mockCloudAuthGoogleRevokeTokenUrl =\n  `${mockCloudAuthGoogleRequest.issuer}` +\n  `/${mockCloudAuthGoogleIdpConfig.revokeTokenUrl}` +\n  `?client_id=${mockCloudAuthGoogleRequest.clientId}` +\n  `&token_type_hint=${mockCloudRevokeRefreshTokenHint}` +\n  `&token=${mockCloudRefreshToken}`;\n\nexport const mockCloudAuthSsoRevokeTokenUrl =\n  `${mockCloudAuthSsoRequest.issuer}` +\n  `/${mockCloudAuthGoogleIdpConfig.revokeTokenUrl}` +\n  `?client_id=${mockCloudAuthSsoRequest.clientId}` +\n  `&token_type_hint=${mockCloudRevokeRefreshTokenHint}` +\n  `&token=${mockCloudRefreshToken}`;\n\nexport const mockCloudAuthGoogleRenewTokenUrl =\n  `${mockCloudAuthGoogleRequest.issuer}` +\n  `/${mockCloudAuthGoogleIdpConfig.tokenUrl}` +\n  `?client_id=${mockCloudAuthGoogleRequest.clientId}` +\n  '&grant_type=refresh_token' +\n  `&${new URLSearchParams({ redirect_uri: mockCloudAuthGoogleRequest.redirectUri }).toString()}` +\n  `&${new URLSearchParams({ scope: mockCloudAuthGoogleRequest.scopes.join(' ') }).toString()}` +\n  `&refresh_token=${mockCloudRefreshToken}`;\n\nexport const mockCloudAuthSsoRenewTokenUrl =\n  `${mockCloudAuthSsoRequest.issuer}` +\n  `/${mockCloudAuthGoogleIdpConfig.tokenUrl}` +\n  `?client_id=${mockCloudAuthSsoRequest.clientId}` +\n  '&grant_type=refresh_token' +\n  `&${new URLSearchParams({ redirect_uri: mockCloudAuthSsoRequest.redirectUri }).toString()}` +\n  `&${new URLSearchParams({ scope: mockCloudAuthSsoRequest.scopes.join(' ') }).toString()}` +\n  `&refresh_token=${mockCloudRefreshToken}`;\n\nexport const mockCloudAuthGithubRequest = Object.assign(\n  new CloudAuthRequest(),\n  {\n    ...mockCloudAuthGithubTokenParams,\n    sessionMetadata: {\n      ...mockSessionMetadata,\n    },\n    tokenManager: { storage: {} },\n    createdAt: new Date(),\n  },\n);\n\nexport const mockCloudAuthGithubAuthUrl =\n  `${mockCloudAuthGithubRequest.issuer}` +\n  `/${mockCloudAuthGithubRequest.authorizeUrl}` +\n  `?client_id=${mockCloudAuthGithubRequest.clientId}` +\n  `&${new URLSearchParams({ redirect_uri: mockCloudAuthGithubRequest.redirectUri }).toString()}` +\n  `&response_type=${mockCloudAuthGithubRequest.responseType}` +\n  `&response_mode=${mockCloudAuthGithubRequest.responseMode}` +\n  `&idp=${mockCloudAuthGithubRequest.idp}` +\n  `&state=${mockCloudAuthGithubRequest.state}` +\n  `&nonce=${mockCloudAuthGithubRequest.nonce}` +\n  `&code_challenge_method=${mockCloudAuthGithubRequest.codeChallengeMethod}` +\n  `&code_challenge=${mockCloudAuthGithubRequest.codeChallenge}` +\n  `&${new URLSearchParams({ scope: mockCloudAuthGithubRequest.scopes.join(' ') }).toString()}` +\n  '&prompt=login';\n\nexport const mockCloudIdToken = 'id_token_p6vA6A5tF36Jf6twH2cBOqtt7n';\nexport const mockCloudIdTokenNew = 'id_token_p6vA6A5tF36Jf6twH2cBOqtt7n-new';\n\nexport const mockTokenResponse = {\n  access_token: mockCloudAccessToken,\n  refresh_token: mockCloudRefreshToken,\n  id_token: mockCloudIdToken,\n};\n\nexport const mockTokenResponseNew = {\n  access_token: mockCloudAccessTokenNew,\n  refresh_token: mockCloudRefreshTokenNew,\n  id_token: mockCloudIdTokenNew,\n};\n\nexport const mockCloudAuthGoogleCallbackQueryObject = {\n  state: mockCloudAuthGoogleTokenParams.state,\n  code: mockCloudAuthCode,\n};\n\nexport const mockCloudAuthGithubCallbackQueryObject = {\n  state: mockCloudAuthGithubTokenParams.state,\n  code: mockCloudAuthCode,\n};\n\nexport const mockCloudAuthResponse = Object.assign(new CloudAuthResponse(), {\n  status: CloudAuthStatus.Succeed,\n  message: 'Successfully authenticated',\n});\n\nexport const mockCloudAuthFailedResponse = Object.assign(\n  new CloudAuthResponse(),\n  {\n    status: CloudAuthStatus.Failed,\n    message: 'Successfully authenticated',\n  },\n);\n\nexport const mockOktaAuthClient = {\n  token: {\n    prepareTokenParams: jest\n      .fn()\n      .mockResolvedValue(mockCloudAuthGoogleTokenParams),\n  },\n};\n\nexport const mockGithubIdpCloudAuthStrategy = jest.fn(() => ({\n  generateAuthRequest: jest.fn().mockResolvedValue(mockCloudAuthGithubRequest),\n}));\n\nexport const mockGoogleIdpCloudAuthStrategy = jest.fn(() => ({\n  generateAuthRequest: jest.fn().mockResolvedValue(mockCloudAuthGoogleRequest),\n  generateRevokeTokensUrl: jest\n    .fn()\n    .mockReturnValue(new URL(mockCloudAuthGoogleRevokeTokenUrl)),\n  generateRenewTokensUrl: jest\n    .fn()\n    .mockReturnValue(new URL(mockCloudAuthGoogleRenewTokenUrl)),\n}));\n\nexport const mockSsoIdpCloudAuthStrategy = jest.fn(() => ({\n  generateAuthRequest: jest.fn().mockResolvedValue(mockCloudAuthSsoRequest),\n  generateRevokeTokensUrl: jest\n    .fn()\n    .mockReturnValue(new URL(mockCloudAuthSsoRevokeTokenUrl)),\n  generateRenewTokensUrl: jest\n    .fn()\n    .mockReturnValue(new URL(mockCloudAuthSsoRenewTokenUrl)),\n}));\n\nexport const mockCloudAuthService = jest.fn(() => ({\n  renewTokens: jest.fn().mockResolvedValue(undefined),\n}));\n\nexport const mockCloudAuthAnalytics = jest.fn(() => ({\n  sendCloudSignInSucceeded: jest.fn(),\n  sendCloudSignInFailed: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-autodiscovery.ts",
    "content": "import { ActionStatus } from 'src/common/models';\nimport {\n  ImportCloudDatabaseDto,\n  ImportCloudDatabaseResponse,\n} from 'src/modules/cloud/autodiscovery/dto';\nimport {\n  mockCloudDatabase,\n  mockCloudDatabaseFixed,\n  mockGetCloudSubscriptionDatabaseDto,\n} from 'src/__mocks__/cloud-database';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { mockCloudAccountInfo } from 'src/__mocks__/cloud-user';\nimport {\n  mockCloudSubscription,\n  mockCloudSubscriptionFixed,\n} from 'src/__mocks__/cloud-subscription';\n\nexport const mockImportCloudDatabaseDto = Object.assign(\n  new ImportCloudDatabaseDto(),\n  {\n    ...mockGetCloudSubscriptionDatabaseDto,\n  },\n);\n\nexport const mockImportCloudDatabaseDtoFixed = Object.assign(\n  new ImportCloudDatabaseDto(),\n  {\n    ...mockGetCloudSubscriptionDatabaseDto,\n    subscriptionType: CloudSubscriptionType.Fixed,\n    free: true,\n  },\n);\n\nexport const mockImportCloudDatabaseResponse = Object.assign(\n  new ImportCloudDatabaseResponse(),\n  {\n    ...mockImportCloudDatabaseDto,\n    status: ActionStatus.Success,\n    message: 'Added',\n    databaseDetails: mockCloudDatabase,\n  },\n);\n\nexport const mockImportCloudDatabaseResponseFixed = Object.assign(\n  new ImportCloudDatabaseResponse(),\n  {\n    ...mockImportCloudDatabaseDtoFixed,\n    status: ActionStatus.Success,\n    message: 'Added',\n    databaseDetails: mockCloudDatabaseFixed,\n  },\n);\n\nexport const mockCloudAutodiscoveryService = jest.fn(() => ({\n  getAccount: jest.fn().mockResolvedValue(mockCloudAccountInfo),\n  discoverSubscriptions: jest\n    .fn()\n    .mockResolvedValue([mockCloudSubscription, mockCloudSubscriptionFixed]),\n  discoverDatabases: jest\n    .fn()\n    .mockResolvedValue([mockCloudDatabase, mockCloudDatabaseFixed]),\n  addRedisCloudDatabases: jest\n    .fn()\n    .mockResolvedValue([\n      mockImportCloudDatabaseResponse,\n      mockImportCloudDatabaseResponseFixed,\n    ]),\n}));\n\nexport const mockCloudAutodiscoveryAnalytics = jest.fn(() => ({\n  sendGetRedisCloudSubsSucceedEvent: jest.fn(),\n  sendGetRedisCloudSubsFailedEvent: jest.fn(),\n  sendGetRedisCloudDbsSucceedEvent: jest.fn(),\n  sendGetRedisCloudDbsFailedEvent: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-capi-key.ts",
    "content": "import { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport {\n  CloudCapiKey,\n  ICloudApiCapiAccessKey,\n  ICloudApiCapiKey,\n} from 'src/modules/cloud/capi-key/model';\nimport { CloudCapiKeyEntity } from 'src/modules/cloud/capi-key/entity/cloud-capi-key.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { mockServer } from 'src/__mocks__/server';\n\nexport const mockCloudCapiAuthDto: CloudCapiAuthDto = {\n  capiKey: 'capi_key',\n  capiSecret: 'capi_secret_key',\n};\n\nexport const mockCloudApiCapiAccessKey: ICloudApiCapiAccessKey = {\n  accessKey: mockCloudCapiAuthDto.capiKey,\n};\n\nexport const mockCloudApiCapiKey: ICloudApiCapiKey = {\n  id: 3001,\n  name: 'capi-key-name',\n  user_account: 40131,\n  secret_key: mockCloudCapiAuthDto.capiSecret,\n};\n\nexport const mockCloudCapiKey = Object.assign(new CloudCapiKey(), {\n  id: '56070e1e-cc50-41c2-b695-585405736af4',\n  name: `RedisInsight-${mockServer.id.slice(0, 13)}-1577836800000`,\n  userId: '84cece4b-b074-49be-88e0-44c5f3f59123',\n  cloudUserId: 10001,\n  cloudAccountId: 20001,\n  capiKey: mockCloudCapiAuthDto.capiKey,\n  capiSecret: mockCloudCapiAuthDto.capiSecret,\n  valid: true,\n  createdAt: new Date('2020-01-01T00:00:00.000Z'),\n  lastUsed: new Date(),\n});\n\nexport const mockCapiKeyEncrypted = 'cloudCapiKey.capiKey_ENCRYPTED';\nexport const mockCapiSecretEncrypted = 'cloudCapiKey.capiSecret_ENCRYPTED';\n\nexport const mockCloudCapiKeyEntity = Object.assign(new CloudCapiKeyEntity(), {\n  ...mockCloudCapiKey,\n  capiKey: mockCapiKeyEncrypted,\n  capiSecret: mockCapiSecretEncrypted,\n  encryption: EncryptionStrategy.KEYTAR,\n});\n\nexport const mockCloudCapiKeyRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockCloudCapiKey),\n  update: jest.fn().mockResolvedValue(mockCloudCapiKey),\n  getByUserAccount: jest.fn().mockResolvedValue(mockCloudCapiKey),\n  create: jest.fn().mockResolvedValue({\n    ...mockCloudCapiKey,\n    capiSecret: undefined,\n  }),\n  list: jest.fn().mockResolvedValue([mockCloudCapiKey]),\n  delete: jest.fn(),\n  deleteAll: jest.fn(),\n}));\n\nexport const mockCloudCapiKeyApiProvider = jest.fn(() => ({\n  enableCapi: jest.fn().mockResolvedValue(mockCloudApiCapiAccessKey.accessKey),\n  createCapiKey: jest.fn().mockResolvedValue(mockCloudApiCapiKey),\n}));\n\nexport const mockCloudCapiKeyService = jest.fn(() => ({\n  getCapiCredentials: jest.fn().mockResolvedValue(mockCloudCapiAuthDto),\n  handleCapiKeyUnauthorizedError: jest.fn().mockImplementation((e) => e),\n}));\n\nexport const mockCloudCapiKeyAnalytics = jest.fn(() => ({\n  sendCloudAccountKeyGenerated: jest.fn(),\n  sendCloudAccountKeyGenerationFailed: jest.fn(),\n  sendCloudAccountSecretGenerated: jest.fn(),\n  sendCloudAccountSecretGenerationFailed: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-common.ts",
    "content": "import { HttpStatus } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport const mockCapiUnauthorizedError = {\n  message: 'Custom unauthorized message',\n  response: {\n    status: 401,\n  },\n};\n\nexport const mockSmApiUnauthorizedError = mockCapiUnauthorizedError;\n\nexport const mockSmApiInternalServerError = {\n  message: 'Custom server error message',\n  response: {\n    status: 500,\n  },\n};\n\nexport const mockSmApiBadRequestError = {\n  message: 'Custom bad request message',\n  response: {\n    status: 400,\n  },\n};\n\nexport const mockUtm = {\n  source: 'redisinsight',\n  medium: 'sso',\n  campaign: 'workbench',\n};\n\nexport const mockCloudApiUnauthorizedExceptionResponse = {\n  error: 'CloudApiUnauthorized',\n  errorCode: CustomErrorCodes.CloudApiUnauthorized,\n  message: 'Request failed with status code 401',\n  statusCode: HttpStatus.UNAUTHORIZED,\n};\n\nexport const mockCloudApiBadRequestExceptionResponse = {\n  error: 'CloudApiBadRequest',\n  errorCode: CustomErrorCodes.CloudApiBadRequest,\n  message: 'Request failed with status code 400',\n  statusCode: HttpStatus.BAD_REQUEST,\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-database.ts",
    "content": "import {\n  CloudDatabase,\n  CloudDatabaseAlert,\n  CloudDatabaseAlertName,\n  CloudDatabaseDataEvictionPolicy,\n  CloudDatabaseDetails,\n  CloudDatabasePersistencePolicy,\n  CloudDatabaseProtocol,\n  CloudDatabaseStatus,\n  ICloudCapiDatabase,\n  ICloudCapiDatabaseTag,\n} from 'src/modules/cloud/database/models';\nimport {\n  mockCloudSubscription,\n  mockCloudSubscriptionFixed,\n} from 'src/__mocks__/cloud-subscription';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { mockCloudAccountInfo } from 'src/__mocks__/cloud-user';\nimport {\n  CreateFreeCloudDatabaseDto,\n  GetCloudSubscriptionDatabaseDto,\n  GetCloudSubscriptionDatabasesDto,\n} from 'src/modules/cloud/database/dto';\nimport { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/cloud-database-details.entity';\nimport { mockCloudTaskInit } from 'src/__mocks__/cloud-task';\nimport config from 'src/utils/config';\n\nconst cloudConfig = config.get('cloud');\n\nexport const mockCloudCapiDatabaseTags: ICloudCapiDatabaseTag[] = [\n  {\n    key: 'tag1',\n    value: 'value1',\n    createdAt: '2020-01-01T00:00:00Z',\n    updatedAt: '2020-01-01T00:00:00Z',\n    links: ['link1', 'link2'],\n  },\n  {\n    key: 'tag2',\n    value: 'value2',\n    createdAt: '2020-01-01T00:00:00Z',\n    updatedAt: '2020-01-01T00:00:00Z',\n    links: ['link3', 'link4'],\n  },\n];\n\nexport const mockCloudCapiDatabase: ICloudCapiDatabase = {\n  databaseId: 50859754,\n  name: 'bdb',\n  protocol: CloudDatabaseProtocol.Redis,\n  provider: 'GCP',\n  region: 'us-central1',\n  redisVersionCompliance: '5.0.5',\n  status: CloudDatabaseStatus.Active,\n  memoryLimitInGb: 1.0,\n  memoryUsedInMb: 6.0,\n  memoryStorage: 'ram',\n  supportOSSClusterApi: false,\n  dataPersistence: CloudDatabasePersistencePolicy.None,\n  replication: true,\n  dataEvictionPolicy: CloudDatabaseDataEvictionPolicy.VolatileLru,\n  throughputMeasurement: {\n    by: 'operations-per-second',\n    value: 25000,\n  },\n  activatedOn: '2019-12-31T09:38:41Z',\n  lastModified: '2019-12-31T09:38:41Z',\n  publicEndpoint:\n    'redis-14621.c34097.us-central1-mz.gcp.qa-cloud.rlrcp.com:14621',\n  privateEndpoint:\n    'redis-14621.internal.c34097.us-central1-mz.gcp.qa-cloud.rlrcp.com:14621',\n  replicaOf: {\n    endpoints: [\n      'redis-19669.c9244.us-central1-mz.gcp.cloud.rlrcp.com:19669',\n      'redis-14074.c9243.us-central1-mz.gcp.cloud.rlrcp.com:14074',\n    ],\n  },\n  clustering: {\n    numberOfShards: 1,\n    regexRules: [],\n    hashingPolicy: 'standard',\n  },\n  security: {\n    sslClientAuthentication: false,\n    sourceIps: ['0.0.0.0/0'],\n  },\n  modules: [\n    {\n      id: 1,\n      name: 'ReJSON',\n      version: 'v10007',\n    },\n  ],\n  alerts: [],\n};\n\nexport const mockCloudCapiDatabaseFixed: ICloudCapiDatabase = {\n  ...mockCloudCapiDatabase,\n  name: cloudConfig.freeDatabaseName,\n  protocol: CloudDatabaseProtocol.Stack,\n  planMemoryLimit: 256,\n  memoryLimitMeasurementUnit: 'MB',\n};\n\nexport const mockCloudDatabase = Object.assign(new CloudDatabase(), {\n  subscriptionId: mockCloudSubscription.id,\n  subscriptionType: CloudSubscriptionType.Flexible,\n  databaseId: mockCloudCapiDatabase.databaseId,\n  name: mockCloudCapiDatabase.name,\n  publicEndpoint: mockCloudCapiDatabase.publicEndpoint,\n  password: undefined,\n  status: mockCloudCapiDatabase.status,\n  sslClientAuthentication: false,\n  modules: ['ReJSON'],\n  options: {\n    enabledBackup: false,\n    enabledClustering: false,\n    enabledDataPersistence: false,\n    enabledRedisFlash: false,\n    enabledReplication: true,\n    isReplicaDestination: true,\n    persistencePolicy: 'none',\n  },\n  cloudDetails: {\n    cloudId: mockCloudCapiDatabase.databaseId,\n    subscriptionType: CloudSubscriptionType.Flexible,\n    memoryLimitMeasurementUnit: undefined,\n    planMemoryLimit: undefined,\n    subscriptionId: mockCloudSubscription.id,\n    free: false,\n  },\n  tags: [],\n});\n\nexport const mockCloudDatabaseFixed = Object.assign(new CloudDatabase(), {\n  ...mockCloudDatabase,\n  name: cloudConfig.freeDatabaseName,\n  subscriptionType: CloudSubscriptionType.Fixed,\n  cloudDetails: {\n    cloudId: mockCloudCapiDatabaseFixed.databaseId,\n    subscriptionType: CloudSubscriptionType.Fixed,\n    subscriptionId: mockCloudSubscription.id,\n    planMemoryLimit: mockCloudCapiDatabaseFixed.planMemoryLimit,\n    memoryLimitMeasurementUnit:\n      mockCloudCapiDatabaseFixed.memoryLimitMeasurementUnit,\n    free: true,\n  },\n});\n\nexport const mockCloudDatabaseFromList = Object.assign(new CloudDatabase(), {\n  ...mockCloudDatabase,\n  options: {\n    ...mockCloudDatabase.options,\n    isReplicaSource: false,\n  },\n});\n\nexport const mockCloudDatabaseFromListFixed = Object.assign(\n  new CloudDatabase(),\n  {\n    ...mockCloudDatabaseFixed,\n    options: {\n      ...mockCloudDatabaseFixed.options,\n      isReplicaSource: false,\n    },\n  },\n);\n\nexport const mockCloudCapiSubscriptionDatabases = {\n  accountId: mockCloudAccountInfo.accountId,\n  subscription: [\n    {\n      subscriptionId: mockCloudSubscription.id,\n      numberOfDatabases: mockCloudSubscription.numberOfDatabases,\n      databases: [mockCloudCapiDatabase],\n    },\n  ],\n};\n\nexport const mockCloudCapiSubscriptionDatabasesFixed = {\n  ...mockCloudCapiSubscriptionDatabases,\n  subscription: {\n    ...mockCloudCapiSubscriptionDatabases.subscription[0],\n    databases: [mockCloudCapiDatabaseFixed],\n  },\n};\n\nexport const mockCloudDatabaseDetails = Object.assign(\n  new CloudDatabaseDetails(),\n  {\n    cloudId: mockCloudDatabase.databaseId,\n    subscriptionType: mockCloudDatabase.subscriptionType,\n    planMemoryLimit: 30,\n    memoryLimitMeasurementUnit: 'MB',\n    free: false,\n  },\n);\n\nexport const mockCloudDatabaseDetailsEntity = Object.assign(\n  new CloudDatabaseDetailsEntity(),\n  {\n    ...mockCloudDatabaseDetails,\n  },\n);\n\nexport const mockGetCloudSubscriptionDatabasesDto = Object.assign(\n  new GetCloudSubscriptionDatabasesDto(),\n  {\n    subscriptionId: mockCloudSubscription.id,\n    subscriptionType: mockCloudSubscription.type,\n    free: false,\n  },\n);\n\nexport const mockGetCloudSubscriptionDatabasesDtoFixed = Object.assign(\n  new GetCloudSubscriptionDatabasesDto(),\n  {\n    subscriptionId: mockCloudSubscription.id,\n    subscriptionType: CloudSubscriptionType.Fixed,\n    free: true,\n  },\n);\n\nexport const mockGetCloudSubscriptionDatabaseDto = Object.assign(\n  new GetCloudSubscriptionDatabaseDto(),\n  {\n    subscriptionId: mockCloudSubscription.id,\n    subscriptionType: mockCloudSubscription.type,\n    databaseId: mockCloudDatabase.databaseId,\n    free: false,\n  },\n);\n\nexport const mockGetCloudSubscriptionDatabaseDtoFixed = Object.assign(\n  new GetCloudSubscriptionDatabaseDto(),\n  {\n    ...mockGetCloudSubscriptionDatabaseDto,\n    subscriptionType: mockCloudSubscriptionFixed.type,\n    free: true,\n  },\n);\n\nexport const mockCloudDatabaseConnectionLimitAlert = Object.assign(\n  new CloudDatabaseAlert(),\n  {\n    name: CloudDatabaseAlertName.ConnectionsLimit,\n    value: 80,\n  },\n);\n\nexport const mockCloudDatabaseDatasetsSizeAlert = Object.assign(\n  new CloudDatabaseAlert(),\n  {\n    name: CloudDatabaseAlertName.DatasetsSize,\n    value: 80,\n  },\n);\n\nexport const mockCreateFreeCloudDatabaseDto = Object.assign(\n  new CreateFreeCloudDatabaseDto(),\n  {\n    name: mockCloudDatabaseFixed.name,\n    subscriptionId: mockCloudSubscriptionFixed.id,\n    subscriptionType: mockCloudSubscriptionFixed.type,\n    protocol: CloudDatabaseProtocol.Stack,\n    dataPersistence: CloudDatabasePersistencePolicy.None,\n    dataEvictionPolicy: CloudDatabaseDataEvictionPolicy.VolatileLru,\n    replication: false,\n    free: true,\n    alerts: [\n      mockCloudDatabaseConnectionLimitAlert,\n      mockCloudDatabaseDatasetsSizeAlert,\n    ],\n  },\n);\n\nexport const mockCloudDatabaseCapiProvider = jest.fn(() => ({\n  getDatabase: jest.fn().mockResolvedValue(mockCloudCapiDatabase),\n  getDatabases: jest.fn().mockResolvedValue(mockCloudCapiSubscriptionDatabases),\n  getDatabaseTags: jest.fn().mockResolvedValue(mockCloudCapiDatabaseTags),\n  createFreeDatabase: jest.fn().mockResolvedValue(mockCloudTaskInit),\n}));\n\nexport const mockCloudDatabaseCapiService = jest.fn(() => ({\n  getDatabase: jest.fn().mockResolvedValue(mockCloudDatabase),\n  getDatabases: jest.fn().mockResolvedValue([mockCloudDatabase]),\n  createFreeDatabase: jest.fn().mockResolvedValue(mockCloudTaskInit),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-job.ts",
    "content": "import {\n  CloudJobStatus,\n  CloudJobStep,\n} from 'src/modules/cloud/job/models/cloud-job-info';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudJobRunMode } from 'src/modules/cloud/job/models';\nimport { CloudJob } from 'src/modules/cloud/job/jobs';\nimport { mockSessionMetadata } from 'src/__mocks__/common';\n\nexport const mockCreateDatabaseCloudJobDataDto = {\n  name: CloudJobName.CreateFreeDatabase,\n  runMode: CloudJobRunMode.Async,\n  data: { planId: 123 },\n};\n\nexport const mockCloudJobInfo = {\n  id: 'job-id',\n  name: CloudJobName.CreateFreeDatabase,\n  status: CloudJobStatus.Running,\n  step: CloudJobStep.Database,\n};\n\nexport abstract class MockCloudJob extends CloudJob {\n  constructor() {\n    super({\n      abortController: new AbortController(),\n      sessionMetadata: mockSessionMetadata,\n    });\n  }\n}\n\nMockCloudJob['getWriteStream'] = jest.fn();\nMockCloudJob['addProfilerClient'] = jest.fn();\nMockCloudJob['removeProfilerClient'] = jest.fn();\nMockCloudJob['setAlias'] = jest.fn();\nMockCloudJob['destroy'] = jest.fn();\nMockCloudJob['getState'] = jest.fn().mockReturnValue(mockCloudJobInfo);\n\nexport const mockCloudJobProvider = jest.fn(() => ({\n  addJob: jest.fn().mockResolvedValue(mockCloudJobInfo),\n  get: jest.fn().mockResolvedValue(MockCloudJob),\n  findUserJobs: jest.fn().mockResolvedValue([MockCloudJob]),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-session.ts",
    "content": "import { ICloudApiCredentials } from 'src/modules/cloud/common/models';\nimport { CloudAuthIdpType } from 'src/modules/cloud/auth/models';\nimport { ICloudApiCsrfToken } from 'src/modules/cloud/user/models';\nimport { CloudSessionData } from 'src/modules/cloud/session/models/cloud-session';\nimport { CloudSessionEntity } from 'src/modules/cloud/session/entities/cloud.session.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\n\nexport const mockCloudApiCsrfToken: ICloudApiCsrfToken = {\n  csrf_token: 'csrf_p6vA6A5tF36Jf6twH2cBOqtt7n',\n};\n\nexport const mockCloudApiAuthDto: ICloudApiCredentials = {\n  accessToken: 'at_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  refreshToken: 'rt_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  idToken: 'id_token_p6vA6A5tF36Jf6twH2cBOqtt7n',\n  idpType: CloudAuthIdpType.Google,\n  csrf: mockCloudApiCsrfToken.csrf_token,\n  apiSessionId: 'asid_p6v-A6A5tF36J-f6twH2cB!@#$_^&*()Oqtt7n',\n};\n\nexport const mockCloudSessionData = Object.assign(new CloudSessionData(), {\n  id: '1',\n  data: { idpType: CloudAuthIdpType.Google },\n});\n\nexport const mockCloudSessionEntity = Object.assign(new CloudSessionEntity(), {\n  ...mockCloudSessionData,\n  data: 'encryptedCloudSessionData',\n  encryption: EncryptionStrategy.KEYTAR,\n});\n\nexport const mockCloudSession = {\n  ...mockCloudApiAuthDto,\n  user: {\n    id: 'cloud_id_1',\n    name: 'user name',\n    currentAccountId: 'cloud_account_id_1',\n    accounts: [\n      {\n        id: 'cloud_account_id_1',\n        name: 'cloud account 1',\n        active: false,\n      },\n      {\n        id: 'cloud_account_id_2',\n        name: 'cloud account 2',\n        active: false,\n      },\n    ],\n  },\n};\n\nexport const mockCloudSessionRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(null),\n  save: jest.fn(),\n}));\n\nexport const mockCloudSessionService = jest.fn(() => ({\n  getSession: jest.fn().mockResolvedValue(mockCloudSession),\n  updateSessionData: jest.fn().mockResolvedValue(mockCloudSession),\n  deleteSessionData: jest.fn(),\n  invalidateApiSession: jest.fn().mockResolvedValue(undefined),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-subscription.ts",
    "content": "import {\n  CloudSubscriptionPlanResponse,\n  CreateFreeCloudSubscriptionDto,\n} from 'src/modules/cloud/subscription/dto';\nimport {\n  CloudSubscription,\n  CloudSubscriptionPlan,\n  CloudSubscriptionRegion,\n  CloudSubscriptionStatus,\n  CloudSubscriptionType,\n  ICloudCapiSubscription,\n  ICloudApiSubscriptionCloudRegion,\n} from 'src/modules/cloud/subscription/models';\nimport { mockCloudTaskInit } from 'src/__mocks__/cloud-task';\n\nexport const mockCloudCapiSubscription: ICloudCapiSubscription = {\n  id: 108353,\n  name: 'external CA',\n  status: CloudSubscriptionStatus.Active,\n  paymentMethodId: 8240,\n  memoryStorage: 'ram',\n  storageEncryption: false,\n  numberOfDatabases: 7,\n  subscriptionPricing: [\n    {\n      type: 'Shards',\n      typeDetails: 'high-throughput',\n      quantity: 2,\n      quantityMeasurement: 'shards',\n      pricePerUnit: 0.124,\n      priceCurrency: 'USD',\n      pricePeriod: 'hour',\n    },\n  ],\n  cloudDetails: [\n    {\n      provider: 'AWS',\n      cloudAccountId: 16424,\n      totalSizeInGb: 0.0323,\n      regions: [\n        {\n          region: 'us-east-1',\n          networking: [\n            {\n              deploymentCIDR: '10.0.0.0/24',\n              subnetId: 'subnet-0a2dd5829daf83024',\n            },\n          ],\n          preferredAvailabilityZones: ['us-east-1a'],\n          multipleAvailabilityZones: false,\n        },\n      ],\n    },\n  ],\n  price: 2,\n};\n\nexport const mockCloudCapiSubscriptionFixed: ICloudCapiSubscription = {\n  ...mockCloudCapiSubscription,\n  price: 0,\n};\n\nexport const mockCloudSubscription = Object.assign(new CloudSubscription(), {\n  id: mockCloudCapiSubscription.id,\n  type: CloudSubscriptionType.Flexible,\n  name: mockCloudCapiSubscription.name,\n  numberOfDatabases: mockCloudCapiSubscription.numberOfDatabases,\n  provider: mockCloudCapiSubscription.cloudDetails[0].provider,\n  region: mockCloudCapiSubscription.cloudDetails[0].regions[0].region,\n  status: mockCloudCapiSubscription.status,\n  price: mockCloudCapiSubscription.price,\n  free: false,\n});\n\nexport const mockCloudSubscriptionFixed = Object.assign(\n  new CloudSubscription(),\n  {\n    ...mockCloudSubscription,\n    type: CloudSubscriptionType.Fixed,\n    price: mockCloudCapiSubscriptionFixed.price,\n    free: true,\n  },\n);\n\nexport const mockCloudApiCloudRegion1: ICloudApiSubscriptionCloudRegion = {\n  id: '1',\n  region_id: 1,\n  zone_id: null,\n  name: 'us-east-1',\n  cloud: 'aws',\n  support_maz: true,\n  country_name: 'USA',\n  city_name: 'Vegas',\n  flag: 'fr',\n  longitude: null,\n  latitude: null,\n  display_order: 1,\n};\n\nexport const mockCloudApiCloudRegion2: ICloudApiSubscriptionCloudRegion = {\n  id: '2',\n  region_id: 2,\n  zone_id: null,\n  name: 'asia-northeast2',\n  cloud: 'gpc',\n  support_maz: true,\n  country_name: 'Poland',\n  city_name: 'Warsaw',\n  flag: 'pl',\n  longitude: null,\n  latitude: null,\n  display_order: 2,\n};\n\nexport const mockCloudApiCloudRegions: ICloudApiSubscriptionCloudRegion[] = [\n  mockCloudApiCloudRegion1,\n  mockCloudApiCloudRegion2,\n];\n\nexport const mockFreeCloudSubscriptionPlan1: CloudSubscriptionPlan = {\n  id: 1,\n  regionId: 1,\n  type: CloudSubscriptionType.Fixed,\n  name: 'Cache 30MB',\n  provider: 'AWS',\n  region: 'us-east-1',\n  price: 0,\n};\n\nexport const mockFreeCloudSubscriptionPlan2: CloudSubscriptionPlan = {\n  id: 2,\n  regionId: 2,\n  type: CloudSubscriptionType.Fixed,\n  name: 'Cache 30MB',\n  provider: 'GCP',\n  region: 'asia-northeast2',\n  price: 0,\n};\n\nexport const mockFreeCloudSubscriptionPlans: CloudSubscriptionPlan[] = [\n  mockFreeCloudSubscriptionPlan1,\n  mockFreeCloudSubscriptionPlan2,\n];\n\nexport const mockCloudSubscriptionRegion1 = Object.assign(\n  new CloudSubscriptionRegion(),\n  {\n    id: mockFreeCloudSubscriptionPlan1.id,\n    cityName: mockCloudApiCloudRegion1.city_name,\n    cloud: mockCloudApiCloudRegion1.cloud,\n    countryName: mockCloudApiCloudRegion1.country_name,\n    displayOrder: mockCloudApiCloudRegion1.display_order,\n    flag: mockCloudApiCloudRegion1.flag,\n    name: mockCloudApiCloudRegion1.name,\n    regionId: mockCloudApiCloudRegion1.region_id,\n  },\n);\n\nexport const mockCloudSubscriptionRegion2 = Object.assign(\n  new CloudSubscriptionRegion(),\n  {\n    id: mockFreeCloudSubscriptionPlan2.id,\n    cityName: mockCloudApiCloudRegion2.city_name,\n    cloud: mockCloudApiCloudRegion2.cloud,\n    countryName: mockCloudApiCloudRegion2.country_name,\n    displayOrder: mockCloudApiCloudRegion2.display_order,\n    flag: mockCloudApiCloudRegion2.flag,\n    name: mockCloudApiCloudRegion2.name,\n    regionId: mockCloudApiCloudRegion2.region_id,\n  },\n);\n\nexport const mockCloudSubscriptionRegions: CloudSubscriptionRegion[] = [\n  mockCloudSubscriptionRegion1,\n  mockCloudSubscriptionRegion2,\n];\n\nexport const mockSubscriptionPlanResponse: CloudSubscriptionPlanResponse[] = [\n  {\n    ...mockFreeCloudSubscriptionPlan1,\n    type: CloudSubscriptionType.Fixed,\n    details: {\n      ...mockCloudSubscriptionRegion1,\n    },\n  },\n  {\n    type: CloudSubscriptionType.Fixed,\n    ...mockFreeCloudSubscriptionPlan2,\n    details: {\n      ...mockCloudSubscriptionRegion2,\n    },\n  },\n];\n\nexport const mockCreateFreeCloudSubscriptionDto = Object.assign(\n  new CreateFreeCloudSubscriptionDto(),\n  {\n    name: mockCloudSubscription.name,\n    planId: mockFreeCloudSubscriptionPlan1.id,\n    subscriptionType: CloudSubscriptionType.Fixed,\n  },\n);\n\nexport const mockCloudSubscriptionApiProvider = jest.fn(() => ({\n  getCloudRegions: jest.fn().mockResolvedValue(mockCloudApiCloudRegions),\n}));\n\nexport const mockCloudSubscriptionCapiProvider = jest.fn(() => ({\n  getSubscriptionsByType: jest\n    .fn()\n    .mockResolvedValue([mockCloudCapiSubscription]),\n  getSubscriptionByType: jest.fn().mockResolvedValue(mockCloudCapiSubscription),\n  getSubscriptionsPlansByType: jest\n    .fn()\n    .mockResolvedValue([mockFreeCloudSubscriptionPlan1]),\n  createFreeSubscription: jest.fn().mockResolvedValue(mockCloudTaskInit),\n}));\n\nexport const mockCloudSubscriptionCapiService = jest.fn(() => ({\n  getSubscriptions: jest.fn().mockResolvedValue([mockCloudSubscription]),\n  getSubscription: jest.fn().mockResolvedValue(mockCloudSubscription),\n  getSubscriptionsPlans: jest\n    .fn()\n    .mockResolvedValue(mockFreeCloudSubscriptionPlans),\n  createFreeSubscription: jest.fn().mockResolvedValue(mockCloudTaskInit),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-task.ts",
    "content": "import { ICloudCapiTask } from 'src/modules/cloud/task/models';\n\nexport const mockCloudTaskInit: ICloudCapiTask = {\n  taskId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',\n  commandType: 'createSubscription',\n  status: 'initialized',\n  timestamp: '2023-07-01T00:00:00.000Z',\n};\n\nexport const mockCloudTaskCapiProvider = jest.fn(() => ({\n  getTask: jest.fn().mockResolvedValue(mockCloudTaskInit),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/cloud-user.ts",
    "content": "import {\n  CloudAccountInfo,\n  CloudUser,\n  CloudUserAccount,\n  ICloudApiAccount,\n  ICloudApiUser,\n  ICloudCapiAccount,\n} from 'src/modules/cloud/user/models';\nimport config, { Config } from 'src/utils/config';\nimport { instanceToPlain } from 'class-transformer';\nimport {\n  mockCloudApiCapiAccessKey,\n  mockCloudCapiAuthDto,\n  mockCloudCapiKey,\n} from 'src/__mocks__/cloud-capi-key';\nimport {\n  mockCloudApiAuthDto,\n  mockCloudSession,\n} from 'src/__mocks__/cloud-session';\n\nconst serverConfig = config.get('server') as Config['server'];\nconst cloudConfig = config.get('cloud');\n\n// ======================================= CAPI =======================================\nexport const mockCloudCapiAccount: ICloudCapiAccount = {\n  id: 40131,\n  name: 'Redis Labs',\n  createdTimestamp: '2018-12-23T15:15:31Z',\n  updatedTimestamp: '2020-06-03T13:16:59Z',\n  key: {\n    name: 'QA-HashedIn-Test-API-Key-2',\n    accountId: 40131,\n    accountName: 'Redis Labs',\n    allowedSourceIps: ['0.0.0.0/0'],\n    createdTimestamp: '2020-04-06T09:22:38Z',\n    owner: {\n      name: 'Cloud Account',\n      email: 'cloud.account@redislabs.com',\n    },\n    httpSourceIp: '198.141.36.229',\n  },\n};\n\nexport const mockCloudAccountInfo = Object.assign(new CloudAccountInfo(), {\n  accountId: mockCloudCapiAccount.id,\n  accountName: mockCloudCapiAccount.name,\n  ownerEmail: mockCloudCapiAccount.key.owner.email,\n  ownerName: mockCloudCapiAccount.key.owner.name,\n});\n\nexport const mockCloudUserCapiProvider = jest.fn(() => ({\n  getCurrentAccount: jest.fn().mockResolvedValue(mockCloudCapiAccount),\n}));\n\nexport const mockCloudUserCapiService = jest.fn(() => ({\n  getCurrentAccount: jest.fn().mockResolvedValue(mockCloudAccountInfo),\n}));\n\n// ======================================= API =======================================\nexport const mockCloudCapiHeaders = {\n  headers: {\n    'x-api-key': mockCloudCapiAuthDto.capiKey,\n    'x-api-secret-key': mockCloudCapiAuthDto.capiSecret,\n    'User-Agent': `RedisInsight/${serverConfig.version}`,\n  },\n};\n\nexport const mockCloudUserAccount = Object.assign(new CloudUserAccount(), {\n  id: mockCloudCapiAccount.id,\n  name: mockCloudCapiAccount.name,\n  capiKey: mockCloudCapiAuthDto.capiKey,\n  capiSecret: mockCloudCapiAuthDto.capiSecret,\n});\n\n//\n// export const mockCloudUserAccount2 = Object.assign(new CloudUserAccount(), {\n//   id: mockCloudCapiAccount2.id,\n//   name: mockCloudCapiAccount2.name,\n// });\n\nexport const mockDefaultCloudApiHeaders = {\n  'User-Agent': `RedisInsight/${serverConfig.version}`,\n  'x-redisinsight-token': cloudConfig.apiToken,\n};\n\nexport const mockCloudApiHeaders = {\n  headers: {\n    ...mockDefaultCloudApiHeaders,\n    authorization: `Bearer ${mockCloudApiAuthDto.accessToken}`,\n    'x-csrf-token': mockCloudApiAuthDto.csrf,\n    cookie: `JSESSIONID=${mockCloudApiAuthDto.apiSessionId}`,\n    'Sm-Id-Token': mockCloudApiAuthDto.idToken,\n  },\n};\n\nexport const mockCloudApiUser: ICloudApiUser = {\n  id: '66999',\n  current_account_id: `${mockCloudCapiAccount.id}`,\n  name: 'User 1',\n  email: 'user1@mail.com',\n  user_id: 10001, // is it okta id?\n  role: 'owner',\n};\n\nexport const mockCloudUser = Object.assign(new CloudUser(), {\n  id: +mockCloudApiUser.id,\n  name: mockCloudApiUser.name,\n  currentAccountId: +mockCloudApiUser.current_account_id,\n  accounts: [mockCloudUserAccount],\n  capiKey: mockCloudCapiKey,\n});\n\nexport const mockCloudUserSafe = instanceToPlain(mockCloudUser); // omits all data in the \"security\" group\n\nexport const mockCloudApiAccount: ICloudApiAccount = {\n  id: mockCloudCapiAccount.id,\n  name: mockCloudCapiAccount.name,\n  api_access_key: mockCloudApiCapiAccessKey.accessKey,\n};\n\nexport const mockCloudUserApiProvider = jest.fn(() => ({\n  getCurrentAccount: jest.fn().mockResolvedValue(mockCloudCapiAccount),\n}));\n\nexport const mockCloudUserRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockCloudUser),\n  update: jest.fn().mockResolvedValue(mockCloudUser),\n}));\n\nexport const mockCloudUserApiService = jest.fn(() => ({\n  getCapiKeys: jest.fn().mockResolvedValue(mockCloudCapiAuthDto),\n  getUserSession: jest.fn().mockResolvedValue(mockCloudSession),\n  invalidateApiSession: jest.fn().mockResolvedValue(undefined),\n  me: jest.fn().mockResolvedValue(mockCloudUser),\n  getCloudUser: jest.fn().mockResolvedValue(mockCloudUser),\n  setCurrentAccount: jest.fn(),\n  updateUser: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/commands.ts",
    "content": "export const mockMainCommands = {\n  'ACL LOAD': {\n    summary: 'Reload the ACLs from the configured ACL file',\n    complexity: 'O(N). Where N is the number of configured users.',\n    since: '6.0.0',\n    group: 'server',\n    provider: 'main',\n  },\n};\n\nexport const mockRedisearchCommands = {\n  'FT.CREATE': {\n    summary: 'Creates an index with the given spec',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'key',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'search',\n  },\n};\n\nexport const mockRedijsonCommands = {\n  'JSON.DEL': {\n    summary: 'Deletes a value',\n    complexity: 'O(N), where N is the size of the deleted value',\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n      },\n      {\n        name: 'path',\n        type: 'json path string',\n        optional: true,\n      },\n    ],\n    since: '1.0.0',\n    group: 'json',\n    provider: 'json',\n  },\n};\n\nexport const mockRedistimeseriesCommands = {\n  'TS.CREATE': {\n    summary: 'Create a new time-series',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n      },\n      {\n        type: 'integer',\n        command: 'RETENTION',\n        name: 'retentionTime',\n        optional: true,\n      },\n      {\n        type: 'enum',\n        command: 'ENCODING',\n        enum: ['UNCOMPRESSED', 'COMPRESSED'],\n        optional: true,\n      },\n      {\n        type: 'integer',\n        command: 'CHUNK_SIZE',\n        name: 'size',\n        optional: true,\n      },\n      {\n        type: 'enum',\n        command: 'DUPLICATE_POLICY',\n        name: 'policy',\n        enum: ['BLOCK', 'FIRST', 'LAST', 'MIN', 'MAX', 'SUM'],\n        optional: true,\n      },\n      {\n        command: 'LABELS',\n        name: ['label', 'value'],\n        type: ['string', 'string'],\n        multiple: true,\n        optional: true,\n      },\n    ],\n    since: '1.0.0',\n    group: 'timeseries',\n    provider: 'timeseries',\n  },\n};\n\nexport const mockRedisgraphCommands = {\n  'GRAPH.QUERY': {\n    summary: 'Queries the graph',\n    arguments: [\n      {\n        name: 'graph',\n        type: 'key',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'graph',\n    provider: 'graph',\n  },\n};\n\nexport const mockCommandsJsonProvider = () => ({\n  updateLatestJson: jest.fn(),\n  getCommands: jest.fn(),\n  getDefaultCommands: jest.fn(),\n});\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/common.ts",
    "content": "import {\n  ClientContext,\n  ClientMetadata,\n  SessionMetadata,\n} from 'src/common/models';\nimport { mockDatabase } from 'src/__mocks__/databases';\nimport { v4 as uuidv4 } from 'uuid';\nimport { mockAccountId, mockUserId } from 'src/__mocks__/user';\nimport {\n  DEFAULT_ACCOUNT_ID,\n  DEFAULT_SESSION_ID,\n  DEFAULT_USER_ID,\n} from 'src/common/constants';\n\nexport type MockType<T> = {\n  [P in keyof T]: jest.Mock<any>;\n};\n\nexport const mockQueryBuilderWhere = jest.fn().mockReturnThis();\nexport const mockQueryBuilderWhereInIds = jest.fn().mockReturnThis();\nexport const mockQueryBuilderSelect = jest.fn().mockReturnThis();\nexport const mockQueryBuilderLeftJoinAndSelect = jest.fn().mockReturnThis();\nexport const mockQueryBuilderGetOne = jest.fn();\nexport const mockQueryBuilderGetMany = jest.fn();\nexport const mockQueryBuilderGetManyRaw = jest.fn();\nexport const mockQueryBuilderGetCount = jest.fn();\nexport const mockQueryBuilderExecute = jest.fn();\nexport const mockCreateQueryBuilder = jest.fn(() => ({\n  // where: jest.fn().mockReturnThis(),\n  where: mockQueryBuilderWhere,\n  whereInIds: mockQueryBuilderWhereInIds,\n  orWhere: mockQueryBuilderWhere,\n  update: jest.fn().mockReturnThis(),\n  select: mockQueryBuilderSelect,\n  set: jest.fn().mockReturnThis(),\n  orderBy: jest.fn().mockReturnThis(),\n  groupBy: jest.fn().mockReturnThis(),\n  having: jest.fn().mockReturnThis(),\n  limit: jest.fn().mockReturnThis(),\n  leftJoin: jest.fn().mockReturnThis(),\n  leftJoinAndSelect: mockQueryBuilderLeftJoinAndSelect,\n  offset: jest.fn().mockReturnThis(),\n  delete: jest.fn().mockReturnThis(),\n  execute: mockQueryBuilderExecute,\n  getCount: mockQueryBuilderGetCount,\n  getRawMany: mockQueryBuilderGetManyRaw,\n  getMany: mockQueryBuilderGetMany,\n  getOne: mockQueryBuilderGetOne,\n}));\n\nexport const mockRepository = jest.fn(() => ({\n  findOne: jest.fn(),\n  findOneBy: jest.fn(),\n  find: jest.fn(),\n  findByIds: jest.fn(),\n  merge: jest.fn(),\n  create: jest.fn(),\n  save: jest.fn(),\n  insert: jest.fn(),\n  update: jest.fn(),\n  upsert: jest.fn(),\n  delete: jest.fn(),\n  remove: jest.fn(),\n  createQueryBuilder: mockCreateQueryBuilder,\n}));\n\nexport const mockSessionMetadata: SessionMetadata = {\n  userId: mockUserId,\n  accountId: mockAccountId,\n  sessionId: uuidv4(),\n};\n\nexport const mockDefaultSessionMetadata: SessionMetadata = {\n  userId: DEFAULT_USER_ID,\n  accountId: DEFAULT_ACCOUNT_ID,\n  sessionId: DEFAULT_SESSION_ID,\n};\n\nexport const mockClientMetadata: ClientMetadata = {\n  sessionMetadata: mockSessionMetadata,\n  databaseId: mockDatabase.id,\n  context: ClientContext.Common,\n};\n\nexport const mockCliClientMetadata: ClientMetadata = {\n  sessionMetadata: mockSessionMetadata,\n  databaseId: mockDatabase.id,\n  context: ClientContext.CLI,\n  uniqueId: uuidv4(),\n};\n\nexport const mockWorkbenchClientMetadata: ClientMetadata = {\n  sessionMetadata: mockSessionMetadata,\n  databaseId: mockDatabase.id,\n  context: ClientContext.Workbench,\n};\n\nexport const mockBrowserClientMetadata: ClientMetadata = {\n  sessionMetadata: mockSessionMetadata,\n  databaseId: mockDatabase.id,\n  context: ClientContext.Browser,\n};\n\nexport const mockCommonClientMetadata: ClientMetadata = {\n  sessionMetadata: mockSessionMetadata,\n  databaseId: mockDatabase.id,\n  context: ClientContext.Common,\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/constants.ts",
    "content": "import { mockSessionMetadata } from 'src/__mocks__/common';\n\nexport const mockConstantsProvider = jest.fn(() => ({\n  getSystemSessionMetadata: jest.fn().mockReturnValue(mockSessionMetadata),\n  getAnonymousId: jest.fn().mockReturnValue(mockSessionMetadata.userId),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/custom-tutorial.ts",
    "content": "import {\n  CustomTutorial,\n  CustomTutorialActions,\n} from 'src/modules/custom-tutorial/models/custom-tutorial';\nimport { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';\nimport { CustomTutorialManifestType } from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';\nimport { MemoryStoredFile } from 'nestjs-form-data';\nimport { UploadCustomTutorialDto } from 'src/modules/custom-tutorial/dto/upload.custom-tutorial.dto';\nimport AdmZip from 'adm-zip';\n\nexport const mockCustomTutorialId =\n  'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id';\n\nexport const mockCustomTutorialId2 =\n  'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id-2';\n\nexport const mockCustomTutorialTmpPath = '/tmp/path';\n\nexport const mockCustomTutorialsHttpLink = 'https://github.com/archive.zip';\nexport const mockCustomTutorialsHttpLink2 =\n  'https://raw.githubusercontent.com/archive.zip';\n\nexport const mockCustomTutorial = Object.assign(new CustomTutorial(), {\n  id: mockCustomTutorialId,\n  name: 'custom tutorial',\n  createdAt: new Date(),\n});\n\nexport const mockCustomTutorialEntity = Object.assign(\n  new CustomTutorialEntity(),\n  {\n    ...mockCustomTutorial,\n  },\n);\n\nexport const mockCustomTutorial2 = Object.assign(new CustomTutorial(), {\n  id: mockCustomTutorialId2,\n  name: 'custom tutorial 2',\n  link: mockCustomTutorialsHttpLink,\n  createdAt: new Date(),\n});\n\nexport const mockCustomTutorialZipFile = Object.assign(new MemoryStoredFile(), {\n  size: 100,\n  buffer: Buffer.from('zip-content', 'utf8'),\n});\n\nexport const mockCustomTutorialZipFileAxiosResponse = {\n  data: mockCustomTutorialZipFile.buffer,\n};\n\nexport const mockCustomTutorialAdmZipEntry = {\n  entryName: 'somefolder/info.md',\n} as AdmZip.IZipEntry;\n\nexport const mockCustomTutorialMacosxAdmZipEntry = {\n  entryName: '__MACOSX/info.md',\n} as AdmZip.IZipEntry;\n\nexport const mockUploadCustomTutorialDto = Object.assign(\n  new UploadCustomTutorialDto(),\n  {\n    file: mockCustomTutorialZipFile,\n  },\n);\n\nexport const mockUploadCustomTutorialExternalLinkDto = Object.assign(\n  new UploadCustomTutorialDto(),\n  {\n    link: mockCustomTutorialsHttpLink,\n  },\n);\n\nexport const mockCustomTutorialManifestJson = {\n  type: CustomTutorialManifestType.Group,\n  id: mockCustomTutorialId,\n  label: mockCustomTutorial.name,\n  children: [\n    {\n      type: 'group',\n      id: 'ct-folder-1',\n      label: 'ct-folder-1',\n      children: [\n        {\n          type: CustomTutorialManifestType.Group,\n          id: 'ct-sub-folder-1',\n          label: 'ct-sub-folder-1',\n          children: [\n            {\n              type: CustomTutorialManifestType.InternalLink,\n              id: 'introduction',\n              label: 'introduction',\n              summary: 'Introduction summary',\n              args: {\n                path: '/ct-folder-1/ct-sub-folder-1/introduction.md',\n              },\n            },\n            {\n              type: CustomTutorialManifestType.InternalLink,\n              id: 'working-with-hashes',\n              label: 'working-with-hashes',\n              args: {\n                path: '/ct-folder-1/ct-sub-folder-1/working-with-hashes.md',\n              },\n            },\n          ],\n        },\n        {\n          type: CustomTutorialManifestType.Group,\n          id: 'ct-sub-folder-2',\n          label: 'ct-sub-folder-2',\n          children: [\n            {\n              type: CustomTutorialManifestType.InternalLink,\n              id: 'introduction',\n              label: 'introduction',\n              args: {\n                path: '/ct-folder-1/ct-sub-folder-2/introduction.md',\n              },\n            },\n            {\n              type: CustomTutorialManifestType.InternalLink,\n              id: 'working-with-graphs',\n              label: 'working-with-graphs',\n              args: {\n                path: '/ct-folder-1/ct-sub-folder-2/working-with-graphs.md',\n              },\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\n\nexport const mockCustomTutorialManifest = {\n  ...mockCustomTutorialManifestJson,\n  type: CustomTutorialManifestType.Group,\n  id: mockCustomTutorialId,\n  label: mockCustomTutorial.name,\n  _actions: mockCustomTutorial.actions,\n  _path: mockCustomTutorial.path,\n};\n\nexport const mockCustomTutorialManifest2 = {\n  type: CustomTutorialManifestType.Group,\n  id: mockCustomTutorialId2,\n  label: mockCustomTutorial2.name,\n  _actions: mockCustomTutorial2.actions,\n  _path: mockCustomTutorial2.path,\n  children: [mockCustomTutorialManifestJson],\n};\n\nexport const globalCustomTutorialManifest = {\n  type: CustomTutorialManifestType.Group,\n  id: 'custom-tutorials',\n  label: 'My tutorials',\n  _actions: [CustomTutorialActions.CREATE],\n  args: {\n    withBorder: true,\n    initialIsOpen: false,\n  },\n  children: [mockCustomTutorialManifest, mockCustomTutorialManifest2],\n};\n\nexport const mockCustomTutorialFsProvider = jest.fn(() => ({\n  unzipFromMemoryStoredFile: jest\n    .fn()\n    .mockResolvedValue(mockCustomTutorialTmpPath),\n  unzipFromExternalLink: jest.fn().mockResolvedValue(mockCustomTutorialTmpPath),\n  unzipToTmpFolder: jest.fn().mockResolvedValue(mockCustomTutorialTmpPath),\n  moveFolder: jest.fn(),\n  removeFolder: jest.fn(),\n}));\n\nexport const mockCustomTutorialManifestProvider = jest.fn(() => ({\n  getOriginalManifestJson: jest\n    .fn()\n    .mockResolvedValue(mockCustomTutorialManifestJson),\n  getManifestJson: jest.fn().mockResolvedValue(mockCustomTutorialManifestJson),\n  generateTutorialManifest: jest\n    .fn()\n    .mockResolvedValue(mockCustomTutorialManifest),\n  isOriginalManifestExists: jest.fn().mockResolvedValue(true),\n}));\n\nexport const mockCustomTutorialRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockCustomTutorial),\n  create: jest.fn().mockResolvedValue(mockCustomTutorial),\n  delete: jest.fn(),\n  list: jest.fn().mockResolvedValue([mockCustomTutorial, mockCustomTutorial2]),\n}));\n\nexport const mockCustomTutorialAnalytics = jest.fn(() => ({\n  sendImportSucceeded: jest.fn(),\n  sendImportFailed: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/database-discovery.ts",
    "content": "import { Database } from 'src/modules/database/models/database';\n\nexport const mockDefaultDatabaseFields = {\n  isPreSetup: true,\n  compressor: 'NONE',\n  connectionType: 'NOT CONNECTED',\n  modules: [],\n  caCert: null,\n  clientCert: null,\n  db: 0,\n  nameFromProvider: null,\n  password: null,\n  provider: null,\n  ssh: null,\n  sshOptions: null,\n  tls: false,\n  tlsServername: null,\n  username: null,\n  verifyServerCert: null,\n};\n\nexport const mockDatabaseToImportFromEnvsInput = {\n  id: '0',\n  host: 'localhost',\n  port: 6379,\n  name: 'local database',\n  db: 0,\n} as Partial<Database>;\n\nexport const mockDatabaseToImportFromEnvsPrepared = {\n  ...mockDefaultDatabaseFields,\n  ...mockDatabaseToImportFromEnvsInput,\n} as Database;\n\nexport const mockDatabaseToImportWithCertsFromEnvsInput = {\n  id: '_1',\n  host: 'host1',\n  port: 6370,\n  name: 'local database',\n  tls: true,\n  caCert: {\n    certificate: 'CA cert',\n  },\n  clientCert: {\n    certificate: 'User cert',\n    key: 'User key',\n  },\n  verifyServerCert: true,\n  db: 0,\n} as Partial<Database>;\n\nexport const mockDatabaseToImportWithCertsFromEnvsPrepared = {\n  ...mockDefaultDatabaseFields,\n  ...mockDatabaseToImportWithCertsFromEnvsInput,\n  caCert: {\n    id: mockDatabaseToImportWithCertsFromEnvsInput.id,\n    name: `${mockDatabaseToImportWithCertsFromEnvsInput.id}_${mockDatabaseToImportWithCertsFromEnvsInput.name}`,\n    certificate: mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n    isPreSetup: true,\n  },\n  clientCert: {\n    id: mockDatabaseToImportWithCertsFromEnvsInput.id,\n    name: `${mockDatabaseToImportWithCertsFromEnvsInput.id}_${mockDatabaseToImportWithCertsFromEnvsInput.name}`,\n    certificate:\n      mockDatabaseToImportWithCertsFromEnvsInput.clientCert.certificate,\n    key: mockDatabaseToImportWithCertsFromEnvsInput.clientCert.key,\n    isPreSetup: true,\n  },\n} as Database;\n\nexport const mockDatabaseToImportFromFileInput = {\n  compressor: 'NONE',\n  id: '_3',\n  host: '172.30.100.102',\n  port: 6379,\n  name: 'standalone-auth',\n  db: null,\n  username: null,\n  password: null,\n  connectionType: 'NOT CONNECTED',\n  nameFromProvider: null,\n  provider: null,\n  lastConnection: null,\n  modules: [],\n  tls: false,\n  tlsServername: null,\n  verifyServerCert: null,\n  caCert: null,\n  clientCert: null,\n  ssh: null,\n  sshOptions: null,\n} as Partial<Database>;\n\nexport const mockDatabaseToImportFromFilePrepared = {\n  ...mockDefaultDatabaseFields,\n  ...mockDatabaseToImportFromFileInput,\n  isPreSetup: true,\n} as Database;\n\nexport const mockDatabaseToImportWithCertsFromFileInput = {\n  compressor: 'Brotli',\n  id: '_5',\n  host: '172.30.100.103',\n  port: 6379,\n  name: 'standalone-tls-auth',\n  db: null,\n  username: 'admin',\n  password: 'pass',\n  connectionType: 'STANDALONE', // will be overwritten\n  nameFromProvider: null,\n  provider: null,\n  lastConnection: null,\n  modules: [],\n  tls: true,\n  tlsServername: null,\n  verifyServerCert: true,\n  caCert: {\n    id: 'will be overwritten',\n    name: 'will be overwritten',\n    certificate:\n      '-----BEGIN CERTIFICATE-----\\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW...',\n    isPreSetup: false, // will be overwritten\n  },\n  clientCert: {\n    id: 'will be overwritten',\n    name: 'will be overwritten',\n    certificate:\n      '-----BEGIN CERTIFICATE-----\\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG...',\n    key: '-----BEGIN PRIVATE KEY-----\\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggko...',\n    isPreSetup: false, // will be overwritten\n  },\n  ssh: null,\n  sshOptions: null,\n  isPreSetup: false, // will be overwritten\n} as Partial<Database>;\n\nexport const mockDatabaseToImportWithCertsFromFilePrepared = {\n  ...mockDatabaseToImportWithCertsFromFileInput,\n  isPreSetup: true,\n  compressor: 'Brotli',\n  connectionType: 'NOT CONNECTED',\n  modules: [],\n  caCert: {\n    id: mockDatabaseToImportWithCertsFromFileInput.id,\n    name: `${mockDatabaseToImportWithCertsFromFileInput.id}_${mockDatabaseToImportWithCertsFromFileInput.name}`,\n    certificate: mockDatabaseToImportWithCertsFromFileInput.caCert.certificate,\n    isPreSetup: true,\n  },\n  clientCert: {\n    id: mockDatabaseToImportWithCertsFromFileInput.id,\n    name: `${mockDatabaseToImportWithCertsFromFileInput.id}_${mockDatabaseToImportWithCertsFromFileInput.name}`,\n    certificate:\n      mockDatabaseToImportWithCertsFromFileInput.clientCert.certificate,\n    key: mockDatabaseToImportWithCertsFromFileInput.clientCert.key,\n    isPreSetup: true,\n  },\n  db: null,\n  nameFromProvider: null,\n  provider: null,\n  ssh: null,\n  sshOptions: null,\n  tlsServername: null,\n} as Database;\n\nexport const cleanupTestEnvs = () => {\n  delete process.env.RI_REDIS_HOST;\n  delete process.env.RI_REDIS_PORT;\n  delete process.env.RI_REDIS_ALIAS;\n  delete process.env.RI_REDIS_USENAME;\n  delete process.env.RI_REDIS_PASSWORD;\n\n  delete process.env.RI_REDIS_HOST_1;\n  delete process.env.RI_REDIS_PORT_1;\n  delete process.env.RI_REDIS_ALIAS_1;\n  delete process.env.RI_REDIS_TLS_1;\n  delete process.env.RI_REDIS_TLS_CA_PATH_1;\n  delete process.env.RI_REDIS_TLS_CA_BASE64_1;\n  delete process.env.RI_REDIS_TLS_CERT_PATH_1;\n  delete process.env.RI_REDIS_TLS_CERT_BASE64_1;\n  delete process.env.RI_REDIS_TLS_KEY_PATH_1;\n  delete process.env.RI_REDIS_TLS_KEY_BASE64_1;\n\n  delete process.env.RI_REDIS_HOST_2;\n  delete process.env.RI_REDIS_HHOST;\n};\n\nexport const mockPreSetupDatabaseDiscoveryService = () => ({\n  discover: jest.fn().mockResolvedValue({ discovered: 0 }),\n});\n\nexport const mockAutoDatabaseDiscoveryService = () => ({\n  discover: jest.fn(),\n});\n\nexport const mockDatabaseDiscoveryService = () => ({\n  discover: jest.fn(),\n});\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/database-import.ts",
    "content": "import {\n  DatabaseImportResponse,\n  DatabaseImportStatus,\n} from 'src/modules/database-import/dto/database-import.response';\nimport { BadRequestException, ForbiddenException } from '@nestjs/common';\nimport {\n  mockDatabase,\n  mockSentinelDatabaseWithTlsAuth,\n} from 'src/__mocks__/databases';\nimport { ValidationException } from 'src/common/exceptions';\nimport {\n  mockCaCertificate,\n  mockClientCertificate,\n} from 'src/__mocks__/certificates';\nimport {\n  InvalidCaCertificateBodyException,\n  InvalidCertificateNameException,\n} from 'src/modules/database-import/exceptions';\nimport { mockSshOptionsPrivateKey } from 'src/__mocks__/ssh';\n\nexport const mockDatabasesToImportArray = new Array(10).fill(\n  mockSentinelDatabaseWithTlsAuth,\n);\n\nexport const mockDatabaseImportFile = {\n  originalname: 'filename.json',\n  mimetype: 'application/json',\n  size: 1,\n  buffer: Buffer.from(JSON.stringify(mockDatabasesToImportArray)),\n};\n\nexport const mockDatabaseImportResultSuccess = {\n  index: 0,\n  status: DatabaseImportStatus.Success,\n  host: mockDatabase.host,\n  port: mockDatabase.port,\n};\n\nexport const mockDatabaseImportResultFail = {\n  index: 0,\n  status: DatabaseImportStatus.Fail,\n  host: mockDatabase.host,\n  port: mockDatabase.port,\n  errors: [new BadRequestException()],\n};\n\nexport const mockDatabaseImportResultPartial = {\n  index: 0,\n  status: DatabaseImportStatus.Partial,\n  host: mockDatabase.host,\n  port: mockDatabase.port,\n  errors: [new InvalidCaCertificateBodyException()],\n};\n\nexport const mockDatabaseImportResponse = Object.assign(\n  new DatabaseImportResponse(),\n  {\n    total: 10,\n    success: new Array(5)\n      .fill(mockDatabaseImportResultSuccess)\n      .map((v, index) => ({\n        ...v,\n        index: index + 5,\n      })),\n    partial: [\n      [\n        new InvalidCaCertificateBodyException(),\n        new InvalidCertificateNameException(),\n      ],\n      [new InvalidCertificateNameException()],\n    ].map((errors, index) => ({\n      ...mockDatabaseImportResultPartial,\n      index: index + 3,\n      errors,\n    })),\n    fail: [\n      new ValidationException('Bad request'),\n      new BadRequestException(),\n      new ForbiddenException(),\n    ].map((error, index) => ({\n      ...mockDatabaseImportResultFail,\n      index,\n      errors: [error],\n    })),\n  },\n);\n\nexport const mockDatabaseImportPartialAnalyticsPayload = {\n  partially: mockDatabaseImportResponse.partial.length,\n  errors: [\n    'InvalidCaCertificateBodyException',\n    'InvalidCertificateNameException',\n  ],\n};\n\nexport const mockDatabaseImportFailedAnalyticsPayload = {\n  failed: mockDatabaseImportResponse.fail.length,\n  errors: ['ValidationException', 'BadRequestException', 'ForbiddenException'],\n};\n\nexport const mockDatabaseImportSucceededAnalyticsPayload = {\n  succeed: mockDatabaseImportResponse.success.length,\n};\n\nexport const mockDatabaseImportAnalytics = jest.fn(() => ({\n  sendImportResults: jest.fn(),\n  sendImportFailed: jest.fn(),\n}));\n\nexport const mockCertificateImportService = jest.fn(() => ({\n  processCaCertificate: jest.fn().mockResolvedValue(mockCaCertificate),\n  processClientCertificate: jest.fn().mockResolvedValue(mockClientCertificate),\n}));\n\nexport const mockSshImportService = jest.fn(() => ({\n  processSshOptions: jest.fn().mockResolvedValue({\n    ...mockSshOptionsPrivateKey,\n    id: undefined,\n  }),\n}));\n\nexport const mockDatabaseImportService = jest.fn(() => ({\n  import: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/database-info.ts",
    "content": "export const mockRedisClientList =\n  'id=29 addr=172.17.0.1:60702 laddr=172.17.0.4:6379 fd=20 ' +\n  'name=redisinsight-common-f9d59780 age=319 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 ' +\n  'qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default resp=2 redir=-1\\n' +\n  'id=31 addr=172.17.0.1:63984 laddr=172.17.0.4:6379 fd=22 name=redisinsight-cli-bc36ecf0 age=15 ' +\n  'idle=15 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 argv-mem=0 obl=0 oll=0 omem=0 ' +\n  'tot-mem=20512 events=r cmd=client user=default redir=-1\\n';\n\nexport const mockRedisClientListResult = [\n  {\n    addr: '172.17.0.1:60702',\n    age: '319',\n    'argv-mem': '10',\n    cmd: 'client',\n    db: '0',\n    events: 'r',\n    fd: '20',\n    flags: 'N',\n    id: '29',\n    idle: '0',\n    laddr: '172.17.0.4:6379',\n    multi: '-1',\n    name: 'redisinsight-common-f9d59780',\n    obl: '0',\n    oll: '0',\n    omem: '0',\n    psub: '0',\n    qbuf: '26',\n    'qbuf-free': '40928',\n    redir: '-1',\n    sub: '0',\n    'tot-mem': '61466',\n    user: 'default',\n    resp: '2',\n  },\n  {\n    addr: '172.17.0.1:63984',\n    age: '15',\n    'argv-mem': '0',\n    cmd: 'client',\n    db: '0',\n    events: 'r',\n    fd: '22',\n    flags: 'N',\n    id: '31',\n    idle: '15',\n    laddr: '172.17.0.4:6379',\n    multi: '-1',\n    name: 'redisinsight-cli-bc36ecf0',\n    obl: '0',\n    oll: '0',\n    omem: '0',\n    psub: '0',\n    qbuf: '0',\n    'qbuf-free': '0',\n    redir: '-1',\n    sub: '0',\n    'tot-mem': '20512',\n    user: 'default',\n  },\n];\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/database-recommendation.ts",
    "content": "import { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\nimport { DatabaseRecommendationEntity } from 'src/modules/database-recommendation/entities/database-recommendation.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { mockDatabaseId } from 'src/__mocks__/databases';\n\nexport const mockDatabaseRecommendationId = 'databaseRecommendationID';\n\nexport const mockRecommendationName = 'string';\n\nexport const mockDatabaseRecommendationParamsEncrypted =\n  'recommendation.params_ENCRYPTED';\n\nexport const mockDatabaseRecommendationParamsPlain = {};\n\nexport const mockDatabaseRecommendation = Object.assign(\n  new DatabaseRecommendation(),\n  {\n    id: mockDatabaseRecommendationId,\n    name: mockRecommendationName,\n    databaseId: mockDatabaseId,\n    read: false,\n    disabled: false,\n    hide: false,\n    params: mockDatabaseRecommendationParamsPlain,\n  },\n);\n\nexport const mockDatabaseRecommendationEntity =\n  new DatabaseRecommendationEntity({\n    ...mockDatabaseRecommendation,\n    params: mockDatabaseRecommendationParamsEncrypted,\n    encryption: EncryptionStrategy.KEYTAR,\n  });\n\nexport const mockDatabaseRecommendationService = () => ({\n  create: jest.fn(),\n  list: jest.fn(),\n  check: jest.fn(),\n  checkMulti: jest.fn(),\n  read: jest.fn(),\n});\n\nexport const mockDatabaseRecommendationProvider = jest.fn(() => ({\n  getStrategy: jest.fn(),\n}));\n\nexport const mockDatabaseRecommendationRepository = jest.fn(() => ({\n  create: jest.fn(),\n  list: jest.fn(),\n  read: jest.fn(),\n  update: jest.fn(),\n  isExist: jest.fn(),\n  get: jest.fn(),\n  sync: jest.fn(),\n  delete: jest.fn(),\n  getTotalUnread: jest.fn(),\n}));\n\nexport const mockRecommendationScanner = jest.fn(() => ({\n  determineRecommendation: jest.fn(),\n}));\n\nexport const mockDatabaseRecommendationAnalytics = jest.fn(() => ({\n  sendCreatedRecommendationEvent: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/database-settings.ts",
    "content": "import { mockDatabaseId } from 'src/__mocks__/databases';\nimport { DatabaseSettings } from 'src/modules/database-settings/models/database-settings';\nimport { DatabaseSettingsEntity } from 'src/modules/database-settings/entities/database-setting.entity';\nimport { classToClass } from 'src/utils';\nimport { CreateOrUpdateDatabaseSettingDto } from 'src/modules/database-settings/dto/database-setting.dto';\n\nexport const mockDatabaseSettings = Object.assign(new DatabaseSettings(), {\n  databaseId: mockDatabaseId,\n  data: JSON.stringify({\n    notShowConfirmationRunTutorial: false,\n    showHiddenRecommendations: true,\n    slowLogDurationUnit: 1,\n    treeViewDelimiter: { label: 'test' },\n    treeViewSort: ':',\n  }),\n});\nexport const mockDatabaseSettingsEntity = new DatabaseSettingsEntity({\n  ...mockDatabaseSettings,\n  id: 1,\n});\n\nexport const mockDatabaseSettingsCreateDto = Object.assign(\n  new CreateOrUpdateDatabaseSettingDto(),\n  {\n    data: {\n      test: 'value',\n    },\n  },\n);\nexport const mockDatabaseSettingsDto = () =>\n  classToClass(DatabaseSettings, mockDatabaseSettingsEntity);\n\nexport const mockDatabaseSettingsRepository = jest.fn(() => ({\n  createOrUpdate: jest.fn(),\n  get: jest.fn(),\n  delete: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/databases-client.ts",
    "content": "import { mockStandaloneRedisClient } from 'src/__mocks__/redis-client';\n\nexport const mockDatabaseClientFactory = jest.fn(() => ({\n  getOrCreateClient: jest.fn().mockResolvedValue(mockStandaloneRedisClient),\n  createClient: jest.fn().mockResolvedValue(mockStandaloneRedisClient),\n}));\n\nexport const mockCredentialProvider = jest.fn(() => ({\n  resolve: jest.fn().mockImplementation((database) => Promise.resolve(database)),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/databases.ts",
    "content": "import { Database } from 'src/modules/database/models/database';\nimport {\n  mockCaCertificate,\n  mockClientCertificate,\n} from 'src/__mocks__/certificates';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport {\n  Compressor,\n  ConnectionType,\n  DatabaseEntity,\n} from 'src/modules/database/entities/database.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { mockIORedisClient } from 'src/__mocks__/redis';\nimport { mockSentinelMasterDto } from 'src/__mocks__/redis-sentinel';\nimport { pick } from 'lodash';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { DatabaseOverview } from 'src/modules/database/models/database-overview';\nimport {\n  mockSshOptionsBasic,\n  mockSshOptionsBasicEntity,\n  mockSshOptionsPrivateKey,\n  mockSshOptionsPrivateKeyEntity,\n} from 'src/__mocks__/ssh';\nimport { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/cloud-database-details.entity';\nimport {\n  mockCloudDatabaseDetails,\n  mockCloudDatabaseDetailsEntity,\n} from 'src/__mocks__/cloud-database';\nimport { mockRedisClientListResult } from 'src/__mocks__/database-info';\nimport { DatabaseOverviewKeyspace } from 'src/modules/database/constants/overview';\nimport { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';\nimport { AzureAuthType } from 'src/modules/azure/constants';\nimport { CloudProvider } from 'src/modules/database/models/provider-details';\nimport { mockTags } from 'src/__mocks__/tags';\n\nexport const mockDatabaseId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id';\n\nexport const mockDatabasePasswordEncrypted = 'database.password_ENCRYPTED';\n\nexport const mockDatabasePasswordPlain = 'some pass';\n\nexport const mockDatabaseSentinelMasterPasswordEncrypted =\n  'database.sentinelMasterPassword_ENCRYPTED';\n\nexport const mockDatabaseSentinelMasterPasswordPlain = 'some sentinel pass';\n\nexport const mockDBSize = 1;\n\nexport const mockDatabase = Object.assign(new Database(), {\n  id: mockDatabaseId,\n  name: 'database-name',\n  host: '127.0.100.1',\n  port: 6379,\n  connectionType: ConnectionType.STANDALONE,\n  timeout: 30_000,\n  new: false,\n  compressor: Compressor.NONE,\n  version: '7.0',\n});\n\nexport const mockCreateDatabaseDto = Object.assign(new CreateDatabaseDto(), {\n  name: mockDatabase.name,\n  host: mockDatabase.host,\n  port: mockDatabase.port,\n});\n\nexport const mockDatabaseModules = [\n  {\n    name: 'rg',\n    version: 10204,\n    semanticVersion: '1.2.4',\n  },\n  {\n    name: 'bf',\n    version: 20209,\n    semanticVersion: '2.2.9',\n  },\n  {\n    name: 'timeseries',\n    version: 10616,\n    semanticVersion: '1.6.16',\n  },\n  {\n    name: 'search',\n    version: 999999,\n    semanticVersion: '99.99.99',\n  },\n  {\n    name: 'graph',\n    version: 20815,\n    semanticVersion: '2.8.15',\n  },\n  {\n    name: 'ReJSON',\n    version: 20011,\n    semanticVersion: '2.0.11',\n  },\n  {\n    name: 'ai',\n    version: 10205,\n    semanticVersion: '1.2.5',\n  },\n];\n\nexport const mockDatabaseWithModules = Object.assign(new Database(), {\n  ...mockDatabase,\n  modules: mockDatabaseModules,\n});\n\nexport const mockDatabaseWithCloudDetails = Object.assign(new Database(), {\n  ...mockDatabase,\n  cloudDetails: mockCloudDatabaseDetails,\n});\n\nexport const mockDatabaseWithProviderDetails = Object.assign(new Database(), {\n  ...mockDatabase,\n  providerDetails: {\n    provider: CloudProvider.Azure,\n    authType: AzureAuthType.EntraId,\n    azureAccountId: 'mock-azure-account-id',\n  },\n});\n\nexport const mockDatabaseEntity = Object.assign(new DatabaseEntity(), {\n  ...mockDatabase,\n  encryption: null,\n});\n\nexport const mockDatabaseEntityWithCloudDetails = Object.assign(\n  new DatabaseEntity(),\n  {\n    ...mockDatabaseEntity,\n    cloudDetails: Object.assign(new CloudDatabaseDetailsEntity(), {\n      id: 'some-uuid',\n      ...mockCloudDatabaseDetailsEntity,\n    }),\n  },\n);\n\nexport const mockDatabaseWithSshBasic = Object.assign(new Database(), {\n  ...mockDatabase,\n  ssh: true,\n  sshOptions: mockSshOptionsBasic,\n});\n\nexport const mockDatabaseWithSshBasicEntity = Object.assign(\n  new DatabaseEntity(),\n  {\n    ...mockDatabaseWithSshBasic,\n    encryption: null,\n    sshOptions: mockSshOptionsBasicEntity,\n  },\n);\n\nexport const mockDatabaseWithSshPrivateKey = Object.assign(new Database(), {\n  ...mockDatabase,\n  ssh: true,\n  sshOptions: mockSshOptionsPrivateKey,\n});\n\nexport const mockDatabaseWithSshPrivateKeyEntity = Object.assign(\n  new DatabaseEntity(),\n  {\n    ...mockDatabaseWithSshPrivateKey,\n    sshOptions: mockSshOptionsPrivateKeyEntity,\n  },\n);\n\nexport const mockDatabaseWithAuth = Object.assign(new Database(), {\n  ...mockDatabase,\n  username: 'some username',\n  password: mockDatabasePasswordPlain,\n});\n\nexport const mockDatabaseWithAuthEntity = Object.assign(new DatabaseEntity(), {\n  ...mockDatabaseWithAuth,\n  password: mockDatabasePasswordEncrypted,\n  encryption: EncryptionStrategy.KEYTAR,\n});\n\nexport const mockDatabaseWithTls = Object.assign(new Database(), {\n  ...mockDatabaseWithAuth,\n  tls: true,\n  verifyServerCert: true,\n  tlsServername: 'some.local',\n  caCert: mockCaCertificate,\n});\n\nexport const mockDatabaseWithTlsEntity = Object.assign(new DatabaseEntity(), {\n  ...mockDatabaseWithTls,\n  password: mockDatabasePasswordEncrypted,\n  encryption: EncryptionStrategy.KEYTAR,\n  caCert: mockCaCertificate, // !not client ca entity since it managed on own repository\n});\n\nexport const mockDatabaseWithTlsAuth = Object.assign(new Database(), {\n  ...mockDatabaseWithTls,\n  clientCert: mockClientCertificate,\n});\n\nexport const mockDatabaseWithTlsAuthEntity = Object.assign(\n  new DatabaseEntity(),\n  {\n    ...mockDatabaseWithTlsEntity,\n    clientCert: mockClientCertificate, // !not client cert entity since it managed on own repository\n  },\n);\n\nexport const mockDatabaseWithTags = Object.assign(new Database(), {\n  ...mockDatabase,\n  tags: mockTags,\n});\n\nexport const mockDatabaseWithTagsEntity = Object.assign(new DatabaseEntity(), {\n  ...mockDatabaseWithTags,\n  encryption: null,\n});\n\nexport const mockSentinelMaster = Object.assign(new SentinelMaster(), {\n  name: 'mymaster',\n  username: 'master_group_username',\n  password: mockDatabaseSentinelMasterPasswordPlain,\n});\n\nexport const mockSentinelDatabaseWithTlsAuth = Object.assign(new Database(), {\n  ...mockDatabaseWithTlsAuth,\n  sentinelMaster: mockSentinelMaster,\n  connectionType: ConnectionType.SENTINEL,\n  nodes: mockSentinelMasterDto.nodes,\n});\n\nexport const mockSentinelDatabaseWithTlsAuthEntity = Object.assign(\n  new DatabaseEntity(),\n  {\n    ...mockDatabaseWithTlsAuthEntity,\n    sentinelMasterName: mockSentinelMaster.name,\n    sentinelMasterUsername: mockSentinelMaster.username,\n    sentinelMasterPassword: mockDatabaseSentinelMasterPasswordEncrypted,\n    connectionType: ConnectionType.SENTINEL,\n    nodes: JSON.stringify(mockSentinelDatabaseWithTlsAuth.nodes),\n  },\n);\nexport const mockClusterNodes = [\n  {\n    host: '127.0.100.1',\n    port: 6379,\n  },\n  {\n    host: '127.0.100.2',\n    port: 6379,\n  },\n];\n\nexport const mockClusterDatabaseWithTlsAuth = Object.assign(new Database(), {\n  ...mockDatabaseWithTlsAuth,\n  connectionType: ConnectionType.CLUSTER,\n  nodes: mockClusterNodes,\n});\n\nexport const mockClusterDatabaseWithTlsAuthEntity = Object.assign(\n  new DatabaseEntity(),\n  {\n    ...mockDatabaseWithTlsAuthEntity,\n    connectionType: ConnectionType.CLUSTER,\n    nodes: JSON.stringify(mockClusterNodes),\n  },\n);\n\nexport const mockNewDatabase = Object.assign(new Database(), {\n  ...mockDatabase,\n  new: true,\n});\n\nexport const mockDatabaseOverview: DatabaseOverview = {\n  version: '6.2.4',\n  usedMemory: 1,\n  totalKeys: 2,\n  totalKeysPerDb: {\n    db0: 1,\n  },\n  connectedClients: 1,\n  opsPerSecond: 1,\n  networkInKbps: 1,\n  networkOutKbps: 1,\n  cpuUsagePercentage: null,\n};\n\nexport const mockDatabaseOverviewCurrentKeyspace =\n  DatabaseOverviewKeyspace.Current;\n\nexport const mockRedisServerInfoDto = {\n  redis_version: '7.0.5',\n  redis_mode: 'standalone',\n  server_name: 'valkey',\n  os: 'Linux 4.15.0-1087-gcp x86_64',\n  arch_bits: '64',\n  tcp_port: '11113',\n  uptime_in_seconds: '1000',\n};\n\nexport const mockRedisGeneralInfo: RedisDatabaseInfoResponse = {\n  version: mockRedisServerInfoDto.redis_version,\n  databases: 16,\n  role: 'master',\n  server: mockRedisServerInfoDto,\n  usedMemory: 1000000,\n  totalKeys: 1,\n  connectedClients: 1,\n  uptimeInSeconds: 1000,\n  hitRatio: 1,\n};\n\nexport const mockDatabaseRepository = jest.fn(() => ({\n  exists: jest.fn().mockResolvedValue(true),\n  get: jest.fn().mockResolvedValue(mockDatabase),\n  create: jest.fn().mockResolvedValue(mockDatabase),\n  update: jest.fn().mockResolvedValue(mockDatabase),\n  delete: jest.fn(),\n  list: jest\n    .fn()\n    .mockResolvedValue([\n      pick(mockDatabase, 'id', 'name'),\n      pick(mockDatabase, 'id', 'name'),\n    ]),\n  cleanupPreSetup: jest.fn().mockResolvedValue({ affected: 0 }),\n}));\n\nexport const mockDatabaseService = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockDatabase),\n  create: jest.fn().mockResolvedValue(mockDatabase),\n  update: jest.fn().mockResolvedValue(mockDatabase),\n  clone: jest.fn().mockResolvedValue(mockDatabase),\n  testConnection: jest.fn().mockResolvedValue(undefined),\n  delete: jest.fn().mockResolvedValue(undefined),\n  bulkDelete: jest.fn().mockResolvedValue({ affected: 0 }),\n  list: jest.fn(),\n}));\n\nexport const mockDatabaseConnectionService = jest.fn(() => ({\n  getOrCreateClient: jest.fn().mockResolvedValue(mockIORedisClient),\n  createClient: jest.fn().mockResolvedValue(mockIORedisClient),\n}));\n\nexport const mockDatabaseInfoProvider = jest.fn(() => ({\n  determineDatabaseModules: jest.fn(),\n  determineDatabaseServer: jest.fn(),\n  determineSentinelMasterGroups: jest\n    .fn()\n    .mockReturnValue([mockSentinelMasterDto]),\n  determineClusterNodes: jest.fn().mockResolvedValue(mockClusterNodes),\n  getRedisGeneralInfo: jest.fn().mockResolvedValueOnce(mockRedisGeneralInfo),\n  getRedisDBSize: jest.fn().mockResolvedValue(mockDBSize),\n  getClientListInfo: jest.fn().mockReturnValue(mockRedisClientListResult),\n}));\n\nexport const mockDatabaseOverviewProvider = jest.fn(() => ({\n  getOverview: jest.fn().mockResolvedValue(mockDatabaseOverview),\n}));\n\nexport const mockDatabaseFactory = jest.fn(() => ({\n  createDatabaseModel: jest.fn().mockResolvedValue(mockDatabase),\n  createStandaloneDatabaseModel: jest.fn().mockResolvedValue(mockDatabase),\n  createClusterDatabaseModel: jest\n    .fn()\n    .mockResolvedValue(mockClusterDatabaseWithTlsAuth),\n  createSentinelDatabaseModel: jest\n    .fn()\n    .mockResolvedValue(mockSentinelDatabaseWithTlsAuth),\n}));\n\nexport const mockDatabaseAnalytics = jest.fn(() => ({\n  sendInstanceListReceivedEvent: jest.fn(),\n  sendConnectionFailedEvent: jest.fn(),\n  sendInstanceAddedEvent: jest.fn(),\n  sendInstanceAddFailedEvent: jest.fn(),\n  sendInstanceEditedEvent: jest.fn(),\n  sendInstanceDeletedEvent: jest.fn(),\n  sendDatabaseConnectedClientListEvent: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/encryption.ts",
    "content": "import { EncryptionStrategy } from 'src/modules/encryption/models';\n\nexport const mockDataToEncrypt = 'stringtoencrypt';\nexport const mockKeytarPassword = 'somepassword';\nexport const mockEncryptionKey = 'somepassword';\n\nexport const mockEncryptionStrategy = EncryptionStrategy.KEYTAR;\n\nexport const mockEncryptResult = {\n  data: '4a558dfef5c1abbdf745232614194ee9',\n  encryption: mockEncryptionStrategy,\n};\n\nexport const mockKeyEncryptionStrategy = EncryptionStrategy.KEY;\n\nexport const mockKeyEncryptResult = {\n  data: '4a558dfef5c1abbdf745232614194ee9',\n  encryption: mockKeyEncryptionStrategy,\n};\n\nexport const mockEncryptionService = jest.fn(() => ({\n  getAvailableEncryptionStrategies: jest.fn(),\n  isEncryptionAvailable: jest.fn().mockResolvedValue(true),\n  encrypt: jest.fn(),\n  decrypt: jest.fn(),\n  getEncryptionStrategy: jest.fn(),\n}));\n\nexport const mockEncryptionStrategyInstance = jest.fn(() => ({\n  isAvailable: jest.fn(),\n  encrypt: jest.fn(),\n  decrypt: jest.fn(),\n}));\n\nexport const mockKeyEncryptionStrategyInstance = jest.fn(() => ({\n  isAvailable: jest.fn(),\n  encrypt: jest.fn(),\n  decrypt: jest.fn(),\n}));\n\nexport const mockKeytarModule = {\n  getPassword: jest.fn(),\n  setPassword: jest.fn(),\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/errors.ts",
    "content": "import { ReplyError } from 'src/models';\nimport { AxiosError } from 'axios';\n\nexport const mockRedisNoAuthError: ReplyError = {\n  name: 'ReplyError',\n  command: 'AUTH',\n  message: 'NOAUTH authentication is required',\n};\n\nexport const mockRedisNoPasswordError: ReplyError = {\n  name: 'ReplyError',\n  command: 'AUTH',\n  message: 'ERR Client sent AUTH, but no password is set',\n};\n\nexport const mockRedisNoPermError: ReplyError = {\n  name: 'ReplyError',\n  command: 'GET',\n  message: 'NOPERM this user has no permissions.',\n};\n\nexport const mockRedisUnknownIndexName: ReplyError = {\n  name: 'ReplyError',\n  command: 'FT.INFO',\n  message: 'Unknown Index name',\n};\n\nexport const mockRedisUnknownIndexNameV8: ReplyError = {\n  name: 'ReplyError',\n  command: 'FT.INFO',\n  message: 'idx: no such index',\n};\n\nexport const mockRedisWrongNumberOfArgumentsError: ReplyError = {\n  name: 'ReplyError',\n  command: 'GET',\n  message: 'ERR wrong number of arguments.',\n};\n\nexport const mockRedisWrongTypeError: ReplyError = {\n  name: 'ReplyError',\n  command: 'GET',\n  message: 'WRONGTYPE Operation against a key holding the wrong kind of value.',\n};\n\nexport const mockRedisMovedError: ReplyError = {\n  name: 'ReplyError',\n  command: 'GET',\n  message: 'MOVED 7008 127.0.0.1:7002',\n};\n\nexport const mockRedisAskError: ReplyError = {\n  name: 'ReplyError',\n  command: 'GET',\n  message: 'ASK 7008 127.0.0.1:7002',\n};\n\nexport const mockAxiosBadRequestError: AxiosError = {\n  name: '',\n  message: 'Bad Request',\n  isAxiosError: true,\n  config: null,\n  response: {\n    statusText: 'BadRequest',\n    data: null,\n    headers: {},\n    config: null,\n    status: 400,\n  },\n  toJSON: () => null,\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/event-emitter.ts",
    "content": "export const mockEventEmitter = {\n  emit: jest.fn(),\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/feature.ts",
    "content": "import { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';\nimport {\n  FeatureConfig,\n  FeatureConfigFilter,\n  FeatureConfigFilterAnd,\n  FeatureConfigFilterOr,\n  FeaturesConfig,\n  FeaturesConfigData,\n} from 'src/modules/feature/model/features-config';\nimport { classToClass } from 'src/utils';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { FeatureEntity } from 'src/modules/feature/entities/feature.entity';\nimport { mockAppSettings } from 'src/__mocks__/app-settings';\nimport config from 'src/utils/config';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.flag';\nimport * as defaultConfig from '../../config/features-config.json';\n\nexport const mockFeaturesConfigId = '1';\nexport const mockFeaturesConfigVersion = defaultConfig.version + 0.111;\nexport const mockControlNumber = 7.68;\nexport const mockControlGroup = '7';\nexport const mockAppVersion = '2.1.0';\n\nexport const mockFeaturesConfigJson = {\n  version: mockFeaturesConfigVersion,\n  features: {\n    [KnownFeatures.InsightsRecommendations]: {\n      perc: [[1.25, 8.45]],\n      flag: true,\n      filters: [\n        {\n          name: 'agreements.analytics',\n          value: true,\n          cond: 'eq',\n        },\n      ],\n    },\n  },\n};\n\nexport const mockFeaturesConfigJsonComplex = {\n  ...mockFeaturesConfigJson,\n  features: {\n    [KnownFeatures.InsightsRecommendations]: {\n      ...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],\n      filters: [\n        {\n          or: [\n            {\n              name: 'settings.testValue',\n              value: 'test',\n              cond: 'eq',\n            },\n            {\n              and: [\n                {\n                  name: 'agreements.analytics',\n                  value: true,\n                  cond: 'eq',\n                },\n                {\n                  or: [\n                    {\n                      name: 'settings.scanThreshold',\n                      value: mockAppSettings.scanThreshold,\n                      cond: 'eq',\n                    },\n                    {\n                      name: 'settings.batchSize',\n                      value: mockAppSettings.batchSize,\n                      cond: 'eq',\n                    },\n                  ],\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n  },\n};\n\nexport const mockFeaturesConfigData = Object.assign(new FeaturesConfigData(), {\n  ...mockFeaturesConfigJson,\n  features: new Map(\n    Object.entries({\n      [KnownFeatures.InsightsRecommendations]: Object.assign(\n        new FeatureConfig(),\n        {\n          ...mockFeaturesConfigJson.features[\n            KnownFeatures.InsightsRecommendations\n          ],\n          filters: [\n            Object.assign(new FeatureConfigFilter(), {\n              ...mockFeaturesConfigJson.features[\n                KnownFeatures.InsightsRecommendations\n              ].filters[0],\n            }),\n          ],\n        },\n      ),\n    }),\n  ),\n});\n\nexport const mockFeaturesConfigDataComplex = Object.assign(\n  new FeaturesConfigData(),\n  {\n    ...mockFeaturesConfigJson,\n    features: new Map(\n      Object.entries({\n        [KnownFeatures.InsightsRecommendations]: Object.assign(\n          new FeatureConfig(),\n          {\n            ...mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ],\n            filters: [\n              Object.assign(new FeatureConfigFilterOr(), {\n                or: [\n                  Object.assign(new FeatureConfigFilter(), {\n                    name: 'settings.testValue',\n                    value: 'test',\n                    cond: 'eq',\n                  }),\n                  Object.assign(new FeatureConfigFilterAnd(), {\n                    and: [\n                      Object.assign(new FeatureConfigFilter(), {\n                        name: 'agreements.analytics',\n                        value: true,\n                        cond: 'eq',\n                      }),\n                      Object.assign(new FeatureConfigFilterOr(), {\n                        or: [\n                          Object.assign(new FeatureConfigFilter(), {\n                            name: 'settings.scanThreshold',\n                            value: mockAppSettings.scanThreshold,\n                            cond: 'eq',\n                          }),\n                          Object.assign(new FeatureConfigFilter(), {\n                            name: 'settings.batchSize',\n                            value: mockAppSettings.batchSize,\n                            cond: 'eq',\n                          }),\n                        ],\n                      }),\n                    ],\n                  }),\n                ],\n              }),\n            ],\n          },\n        ),\n      }),\n    ),\n  },\n);\n\nexport const mockFeaturesConfig = Object.assign(new FeaturesConfig(), {\n  controlNumber: mockControlNumber,\n  data: mockFeaturesConfigData,\n});\n\nexport const mockFeaturesConfigComplex = Object.assign(new FeaturesConfig(), {\n  controlNumber: mockControlNumber,\n  data: mockFeaturesConfigDataComplex,\n});\n\nexport const mockFeaturesConfigEntity = Object.assign(\n  new FeaturesConfigEntity(),\n  {\n    ...classToClass(FeaturesConfigEntity, mockFeaturesConfig),\n    id: mockFeaturesConfigId,\n  },\n);\n\nexport const mockFeaturesConfigEntityComplex = Object.assign(\n  new FeaturesConfigEntity(),\n  {\n    ...classToClass(FeaturesConfigEntity, mockFeaturesConfigComplex),\n    id: mockFeaturesConfigId,\n  },\n);\n\nexport const mockFeature = Object.assign(new Feature(), {\n  name: KnownFeatures.InsightsRecommendations,\n  flag: true,\n});\n\nexport const mockFeatureSso = Object.assign(new Feature(), {\n  name: KnownFeatures.CloudSso,\n  flag: true,\n  strategy: CloudSsoFeatureStrategy.DeepLink,\n  data: {\n    selectPlan: {\n      components: {\n        redisStackPreview: [\n          {\n            provider: 'AWS',\n            regions: ['us-east-2', 'ap-southeast-1', 'sa-east-1'],\n          },\n          {\n            provider: 'GCP',\n            regions: ['asia-northeast1', 'europe-west1', 'us-central1'],\n          },\n        ],\n      },\n    },\n  },\n});\n\nexport const mockFeatureDatabaseManagement = Object.assign(new Feature(), {\n  name: KnownFeatures.DatabaseManagement,\n  flag: true,\n});\n\nexport const mockFeatureRedisClient = Object.assign(new Feature(), {\n  name: KnownFeatures.RedisClient,\n  flag: true,\n  data: {\n    strategy: 'ioredis',\n  },\n});\n\nexport const mockUnknownFeature = Object.assign(new Feature(), {\n  name: 'unknown',\n  flag: true,\n});\n\nexport const mockFeatureEntity = Object.assign(new FeatureEntity(), {\n  id: 'lr-1',\n  name: KnownFeatures.InsightsRecommendations,\n  flag: true,\n});\n\nexport const mockServerState = {\n  settings: mockAppSettings,\n  agreements: mockAppSettings.agreements,\n  config: config.get(),\n  env: process.env,\n};\n\nexport const mockFeaturesConfigRepository = jest.fn(() => ({\n  getOrCreate: jest.fn().mockResolvedValue(mockFeaturesConfig),\n  update: jest.fn().mockResolvedValue(mockFeaturesConfig),\n}));\n\nexport const mockFeatureRepository = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockFeature),\n  upsert: jest.fn().mockResolvedValue({ updated: 1 }),\n  list: jest.fn().mockResolvedValue([mockFeature, mockFeatureSso]),\n  delete: jest.fn().mockResolvedValue({ deleted: 1 }),\n}));\n\nexport const mockFeaturesConfigService = jest.fn(() => ({\n  sync: jest.fn(),\n  getControlInfo: jest.fn().mockResolvedValue({\n    controlNumber: mockControlNumber,\n    controlGroup: mockControlGroup,\n  }),\n}));\n\nexport const mockFeatureService = jest.fn(() => ({\n  getByName: jest.fn().mockResolvedValue(undefined),\n  isFeatureEnabled: jest.fn().mockResolvedValue(true),\n}));\n\nexport const mockFeatureAnalytics = jest.fn(() => ({\n  sendFeatureFlagConfigUpdated: jest.fn(),\n  sendFeatureFlagConfigUpdateError: jest.fn(),\n  sendFeatureFlagInvalidRemoteConfig: jest.fn(),\n  sendFeatureFlagRecalculated: jest.fn(),\n}));\n\nexport const mockInsightsRecommendationsFlagStrategy = {\n  calculate: jest.fn().mockResolvedValue(mockFeature),\n};\n\nexport const mockFeatureFlagProvider = jest.fn(() => ({\n  getStrategy: jest\n    .fn()\n    .mockResolvedValue(mockInsightsRecommendationsFlagStrategy),\n  calculate: jest.fn().mockResolvedValue(mockFeature),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/index.ts",
    "content": "export * from './ai';\nexport * from './certificates';\nexport * from './commands';\nexport * from './common';\nexport * from './constants';\nexport * from './encryption';\nexport * from './errors';\n// export * from './redis-databases';\nexport * from './redis-info';\nexport * from './redis-rs';\nexport * from './app-settings';\nexport * from './analytics';\nexport * from './profiler';\nexport * from './user';\nexport * from './tags';\nexport * from './databases';\nexport * from './databases-client';\nexport * from './bulk-actions';\nexport * from './custom-tutorial';\nexport * from './autodiscovery';\nexport * from './redis';\nexport * from './server';\nexport * from './redis-enterprise';\nexport * from './redis-sentinel';\nexport * from './database-discovery';\nexport * from './database-import';\nexport * from './redis-client';\nexport * from './redis-utils';\nexport * from './ssh';\nexport * from './browser-history';\nexport * from './database-recommendation';\nexport * from './database-settings';\nexport * from './feature';\nexport * from './notification';\nexport * from './cloud-autodiscovery';\nexport * from './cloud-capi-key';\nexport * from './cloud-database';\nexport * from './cloud-subscription';\nexport * from './cloud-task';\nexport * from './cloud-user';\nexport * from './cloud-common';\nexport * from './session';\nexport * from './cloud-session';\nexport * from './database-info';\nexport * from './cloud-job';\nexport * from './rdi';\nexport * from './workbench';\nexport * from './utm';\nexport * from './event-emitter';\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/notification.ts",
    "content": "import { NotificationEntity } from 'src/modules/notification/entities/notification.entity';\nimport { NotificationType } from 'src/modules/notification/constants';\nimport { Notification } from 'src/modules/notification/models/notification';\nimport { NotificationsDto } from 'src/modules/notification/dto';\n\nexport const mockGlobalNotificationsJson = {\n  notifications: [\n    {\n      title: 'RedisGraph End of Life',\n      body:\n        'Redis Inc. has announced the end-of-life of <b>RedisGraph</b>. ' +\n        'We will carry out the process gradually, and in accordance with our commitment to our customers. ' +\n        '<p> If you are using RedisGraph - please read the ' +\n        '<a href=\"https://redis.com/blog/redisgraph-eol\" target=\"_blank\">following</a> carefully.',\n      category: 'important',\n      categoryColor: '#800D0D',\n      timestamp: 1688549037,\n    },\n    {\n      title: 'Missing a feature or found a bug?',\n      body:\n        'Would you like to see specific features added or have a bug to report? ' +\n        '<p>' +\n        ' <a href=\"https://github.com/RedisInsight/RedisInsight/issues\" target=\"_blank\">Share</a> ' +\n        'it with us! <p> ' +\n        'And <b>star</b> the repository if you like RedisInsight!',\n      category: 'feedback',\n      categoryColor: '#330D80',\n      timestamp: 1662381434,\n    },\n  ],\n};\n\nexport const mockNotification1 = Object.assign(new Notification(), {\n  ...mockGlobalNotificationsJson.notifications[0],\n  type: NotificationType.Global,\n  read: true,\n});\n\nexport const mockNotification1Entity = Object.assign(\n  new NotificationEntity(),\n  mockNotification1,\n);\n\nexport const mockNotification1UPD = Object.assign(new Notification(), {\n  ...mockNotification1,\n  title: 'UPD RedisGraph End of Life',\n});\n\nexport const mockNotification1UPDEntity = Object.assign(\n  new NotificationEntity(),\n  mockNotification1UPD,\n);\n\nexport const mockNotification2 = Object.assign(new Notification(), {\n  ...mockGlobalNotificationsJson.notifications[1],\n  type: NotificationType.Global,\n  read: false,\n});\nexport const mockNotification2Entity = Object.assign(\n  new NotificationEntity(),\n  mockNotification2,\n);\n\nexport const mockNotificationsDto = Object.assign(new NotificationsDto(), {\n  notifications: [mockNotification1, mockNotification2],\n  totalUnread: 1,\n});\n\nexport const mockNotificationRepository = jest.fn(() => ({\n  getNotifications: jest\n    .fn()\n    .mockResolvedValue([mockNotification1Entity, mockNotification2Entity]),\n  getTotalUnread: jest.fn().mockResolvedValue(mockNotificationsDto.totalUnread),\n  readNotifications: jest.fn().mockResolvedValue([]),\n  insertNotifications: jest.fn(),\n  getGlobalNotifications: jest.fn(),\n  deleteGlobalNotifications: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/profiler.ts",
    "content": "import { ILogsEmitter } from 'src/modules/profiler/interfaces/logs-emitter.interface';\nimport { ProfilerClient } from 'src/modules/profiler/models/profiler.client';\nimport { IShardObserver } from 'src/modules/profiler/interfaces/shard-observer.interface';\nimport { LogFile } from 'src/modules/profiler/models/log-file';\nimport { ProfilerAnalyticsService } from 'src/modules/profiler/profiler-analytics.service';\nimport { MockType } from 'src/__mocks__/common';\nimport { TelemetryEvents } from 'src/constants';\nimport * as MockedSocket from 'socket.io-mock';\nimport { IMonitorData } from 'src/modules/profiler/interfaces/monitor-data.interface';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport { MonitorSettings } from 'src/modules/profiler/models/monitor-settings';\n\nexport const mockMonitorDataItemEmitted = {\n  time: '14239988881.12341',\n  args: ['set', 'foo', 'bar'],\n  source: '127.0.0.1',\n  database: 0,\n};\n\nexport const mockMonitorDataItem: IMonitorData = {\n  ...mockMonitorDataItemEmitted,\n  shardOptions: undefined,\n};\n\nexport const mockSocket = new MockedSocket();\nmockSocket['emit'] = jest.fn();\n\nexport const mockRedisShardObserver: IShardObserver = {\n  addListener: jest.fn(),\n  eventNames: jest.fn(),\n  getMaxListeners: jest.fn(),\n  listenerCount: jest.fn(),\n  listeners: jest.fn(),\n  prependListener: jest.fn(),\n  prependOnceListener: jest.fn(),\n  removeAllListeners: jest.fn(),\n  removeListener: jest.fn(),\n  rawListeners: jest.fn(),\n  setMaxListeners: jest.fn(),\n  on: jest.fn(),\n  emit: jest.fn(),\n  off: jest.fn(),\n  once: jest.fn(),\n  disconnect: jest.fn(),\n};\n\nexport const mockProfilerAnalyticsEvents = new Map();\nmockProfilerAnalyticsEvents.set(\n  TelemetryEvents.ProfilerLogDownloaded,\n  jest.fn(),\n);\nmockProfilerAnalyticsEvents.set(TelemetryEvents.ProfilerLogDeleted, jest.fn());\n\nexport const mockProfilerAnalyticsService: MockType<ProfilerAnalyticsService> =\n  {\n    sendLogDeleted: jest.fn(),\n    sendLogDownloaded: jest.fn(),\n    getEventsEmitters: jest\n      .fn()\n      .mockImplementation(() => mockProfilerAnalyticsEvents),\n  };\n\nexport const mockLogEmitter: ILogsEmitter = {\n  id: 'test',\n  emit: jest.fn(),\n  addProfilerClient: jest.fn(),\n  removeProfilerClient: jest.fn(),\n  flushLogs: jest.fn(),\n};\n\nexport const mockWriteStream = {\n  write: jest.fn(),\n};\nexport const testLogFileId = 'test-log-file-id';\nexport const mockLogFile: LogFile = new LogFile(\n  'instanceid',\n  testLogFileId,\n  mockProfilerAnalyticsEvents,\n);\nmockLogFile['getWriteStream'] = jest.fn();\nmockLogFile['addProfilerClient'] = jest.fn();\nmockLogFile['removeProfilerClient'] = jest.fn();\nmockLogFile['setAlias'] = jest.fn();\nmockLogFile['destroy'] = jest.fn();\n\nexport const mockProfilerClient: ProfilerClient = new ProfilerClient(\n  mockSocket.id,\n  mockSocket,\n);\nmockProfilerClient['handleOnData'] = jest.fn();\nmockProfilerClient['handleOnDisconnect'] = jest.fn();\n\nexport const mockMonitorSettings: MonitorSettings = {\n  logFileId: testLogFileId,\n};\n\nexport const mockLogFileProvider: MockType<LogFileProvider> = {\n  getOrCreate: jest.fn(),\n  get: jest.fn(),\n  getDownloadData: jest.fn(),\n  onModuleDestroy: jest.fn(),\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/rdi.ts",
    "content": "import {\n  Rdi,\n  RdiClientMetadata,\n  RdiPipeline,\n  RdiStatisticsData,\n} from 'src/modules/rdi/models';\nimport { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client';\nimport { RdiEntity } from 'src/modules/rdi/entities/rdi.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { RdiDryRunJobDto } from 'src/modules/rdi/dto';\nimport { sign } from 'jsonwebtoken';\n\nexport const mockRdiId = 'rdiId';\nexport const mockRdiPasswordEncrypted = 'password_ENCRYPTED';\n\nexport const mockRdiPasswordPlain = 'some pass';\n\nexport const mockedRdiAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\nexport const mockedAccessToken = mockedRdiAccessToken;\n\nexport class MockRdiClient extends ApiRdiClient {\n  constructor(metadata: RdiClientMetadata, client: any = jest.fn()) {\n    super(metadata, client);\n  }\n\n  public getSchema = jest.fn();\n\n  public getPipeline = jest.fn();\n\n  public getConfigTemplate = jest.fn();\n\n  public getJobTemplate = jest.fn();\n\n  public getStrategies = jest.fn();\n\n  public deploy = jest.fn();\n\n  public startPipeline = jest.fn();\n\n  public stopPipeline = jest.fn();\n\n  public resetPipeline = jest.fn();\n\n  public deployJob = jest.fn();\n\n  public dryRunJob = jest.fn();\n\n  public testConnections = jest.fn();\n\n  public getStatistics = jest.fn();\n\n  public getPipelineStatus = jest.fn();\n\n  public getJobFunctions = jest.fn();\n\n  public connect = jest.fn();\n\n  public ensureAuth = jest.fn();\n}\n\nexport const generateMockRdiClient = (\n  metadata: RdiClientMetadata,\n  client = jest.fn(),\n): MockRdiClient => new MockRdiClient(metadata as RdiClientMetadata, client);\n\nexport const mockRdiClientMetadata: RdiClientMetadata = {\n  sessionMetadata: undefined,\n  id: mockRdiId,\n};\n\nexport const mockRdi = Object.assign(new Rdi(), {\n  name: 'name',\n  version: '1.2',\n  url: 'http://localhost:4000',\n  password: 'pass',\n  username: 'user',\n});\n\nexport const mockRdiPipeline = Object.assign(new RdiPipeline(), {\n  jobs: { some_job: {} },\n  config: {},\n});\n\nexport const mockRdiDryRunJob: RdiDryRunJobDto = Object.assign(\n  new RdiDryRunJobDto(),\n  {\n    input_data: {},\n    job: {},\n  },\n);\n\nexport const mockRdiStatisticsData = Object.assign(new RdiStatisticsData(), {});\n\nexport const mockRdiDecrypted = Object.assign(new Rdi(), {\n  id: '1',\n  name: 'name',\n  version: '1.0',\n  url: 'http://test.com',\n  username: 'testuser',\n  password: mockRdiPasswordPlain,\n  lastConnection: new Date(),\n});\n\nexport const mockRdiEntityEncrypted = Object.assign(new RdiEntity(), {\n  ...mockRdiDecrypted,\n  password: mockRdiPasswordEncrypted,\n  encryption: EncryptionStrategy.KEYTAR,\n});\n\nexport const mockRdiUnauthorizedError = {\n  message: 'Request failed with status code 401',\n  response: {\n    status: 401,\n  },\n};\n\nexport const mockRdiConfigSchema = {\n  title: 'Redis Data Integration Configuration File',\n  type: ['object', 'null'],\n  properties: {\n    sources: {\n      title: 'Source collectors',\n      type: 'object',\n      additionalProperties: {\n        type: 'object',\n        properties: {\n          connection: {\n            $ref: '#/definitions/connection',\n          },\n          type: {\n            title: 'Collector type',\n            type: 'string',\n            const: 'cdc',\n          },\n          logging: {\n            title: 'Logging configuration',\n            type: 'object',\n            properties: {\n              level: {\n                title: 'Logging level',\n                description:\n                  'Logging level for the source collector (trace|debug|info|warn|error)',\n                type: 'string',\n                enum: ['trace', 'debug', 'info', 'warn', 'error'],\n                default: 'info',\n              },\n            },\n            additionalProperties: false,\n          },\n          tables: {\n            type: 'object',\n            title: 'Tables to capture',\n            description:\n              'Tables to capture from the source database (table.include.list)',\n            additionalProperties: {\n              type: 'object',\n              properties: {\n                snapshot_sql: {\n                  title: 'Snapshot SQL',\n                  description:\n                    'SQL statement used to override the statement for the initial snapshot (snapshot.select.statement.overrides)',\n                  type: 'string',\n                },\n                columns: {\n                  type: 'array',\n                  title: 'Columns to capture',\n                  description:\n                    'Columns to capture from the source database (column.include.list)',\n                  items: {\n                    type: 'string',\n                  },\n                },\n                keys: {\n                  type: 'array',\n                  title: 'Message keys',\n                  description:\n                    'Fields to use as keys in the generated messages (message.key.columns)',\n                  items: {\n                    type: 'string',\n                  },\n                },\n              },\n            },\n          },\n          sink: {\n            type: 'object',\n            title: 'Sink configuration',\n            properties: {\n              nopass: {\n                title: 'Nopass in the Redis sink',\n                description:\n                  'Flag to disable password in the sink configuration. If set to true, the password will not be included in the sink configuration',\n                type: 'boolean',\n                default: false,\n              },\n            },\n          },\n          advanced: {\n            type: 'object',\n            title: 'Advanced configuration',\n            properties: {\n              sink: {\n                type: 'object',\n                title: 'Sink configuration',\n              },\n              source: {\n                type: 'object',\n                title: 'Source configuration',\n              },\n              quarkus: {\n                type: 'object',\n                title: 'Quarkus configuration',\n              },\n            },\n            additionalProperties: false,\n          },\n        },\n        required: ['type', 'connection'],\n      },\n    },\n    processors: {\n      title: 'Configuration details of Redis Data Integration Processors',\n      type: ['object', 'null'],\n      properties: {\n        on_failed_retry_interval: {\n          title: 'Interval (in seconds) on which to perform retry on failure',\n          type: ['integer', 'string'],\n          minimum: 1,\n          pattern: '^\\\\${.*}$',\n          default: 5,\n        },\n        read_batch_size: {\n          title: 'The batch size for reading data from source database',\n          type: ['integer', 'string'],\n          minimum: 1,\n          pattern: '^\\\\${.*}$',\n          default: 2000,\n        },\n        debezium_lob_encoded_placeholder: {\n          title: 'Enable Debezium LOB placeholders',\n          type: 'string',\n          default: 'X19kZWJleml1bV91bmF2YWlsYWJsZV92YWx1ZQ==',\n        },\n        dedup: {\n          title: 'Enable deduplication mechanism',\n          type: 'boolean',\n          default: false,\n        },\n        dedup_max_size: {\n          title: 'Max size of the deduplication set',\n          type: 'integer',\n          minimum: 1,\n          default: 1024,\n        },\n        dedup_strategy: {\n          title:\n            'Deduplication strategy: reject - reject messages(dlq), ignore - ignore messages',\n          type: 'string',\n          default: 'ignore',\n          enum: ['reject', 'ignore'],\n          deprecated: true,\n          description:\n            \"Property 'dedup_strategy' is now deprecated. The only supported strategy is 'ignore'. Please remove from the configuration.\",\n        },\n        duration: {\n          title:\n            'Time (in ms) after which data will be read from stream even if read_batch_size was not reached',\n          type: ['integer', 'string'],\n          minimum: 1,\n          pattern: '^\\\\${.*}$',\n          default: 100,\n        },\n        write_batch_size: {\n          title:\n            'The batch size for writing data to target Redis database. Should be less or equal to the read_batch_size',\n          type: ['integer', 'string'],\n          minimum: 1,\n          pattern: '^\\\\${.*}$',\n          default: 200,\n        },\n        error_handling: {\n          title:\n            'Error handling strategy: ignore - skip, dlq - store rejected messages in a dead letter queue',\n          type: 'string',\n          pattern: '^\\\\${.*}$|ignore|dlq',\n          default: 'dlq',\n        },\n        dlq_max_messages: {\n          title: 'Dead letter queue max messages per stream',\n          type: ['integer', 'string'],\n          minimum: 1,\n          pattern: '^\\\\${.*}$',\n          default: 1000,\n        },\n        target_data_type: {\n          title:\n            'Target data type: hash/json - JSON module must be in use in the target DB',\n          type: 'string',\n          pattern: '^\\\\${.*}$|hash|json',\n          default: 'hash',\n        },\n        json_update_strategy: {\n          title:\n            'Target update strategy: replace/merge - JSON module must be in use in the target DB',\n          type: 'string',\n          pattern: '^\\\\${.*}$|replace|merge',\n          default: 'replace',\n          deprecated: true,\n          description:\n            \"Property 'json_update_strategy' will be deprecated in future releases. Use 'on_update' job-level property to define the json update strategy.\",\n        },\n        initial_sync_processes: {\n          title:\n            'Number of processes RDI Engine creates to process the initial sync with the source',\n          type: ['integer', 'string'],\n          minimum: 1,\n          maximum: 32,\n          pattern: '^\\\\${.*}$',\n          default: 4,\n        },\n        idle_sleep_time_ms: {\n          title: 'Idle sleep time (in milliseconds) between batches',\n          type: ['integer', 'string'],\n          minimum: 1,\n          maximum: 999999,\n          pattern: '^\\\\${.*}$',\n          default: 200,\n        },\n        idle_streams_check_interval_ms: {\n          title:\n            'Interval (in milliseconds) for checking new streams when the stream processor is idling',\n          type: ['integer', 'string'],\n          minimum: 1,\n          maximum: 999999,\n          pattern: '^\\\\${.*}$',\n          default: 1000,\n        },\n        busy_streams_check_interval_ms: {\n          title:\n            'Interval (in milliseconds) for checking new streams when the stream processor is busy',\n          type: ['integer', 'string'],\n          minimum: 1,\n          maximum: 999999,\n          pattern: '^\\\\${.*}$',\n          default: 5000,\n        },\n        wait_enabled: {\n          title: 'Checks if the data has been written to the replica shard',\n          type: 'boolean',\n          default: false,\n        },\n        wait_timeout: {\n          title:\n            'Timeout in milliseconds when checking write to the replica shard',\n          type: ['integer', 'string'],\n          minimum: 1,\n          pattern: '^\\\\${.*}$',\n          default: 1000,\n        },\n        retry_on_replica_failure: {\n          title:\n            'Ensures that the data has been written to the replica shard and keeps retrying if not',\n          type: 'boolean',\n          default: true,\n        },\n      },\n      additionalProperties: false,\n    },\n    targets: {\n      title: 'Target connections',\n      type: 'object',\n      properties: {\n        connection: {\n          $ref: '#/definitions/connection',\n        },\n      },\n    },\n  },\n  definitions: {\n    connection: {\n      title: 'Connection details',\n      type: 'object',\n      patternProperties: {\n        '.*': {\n          oneOf: [\n            {\n              title: 'Redis DB connection details',\n              type: 'object',\n              properties: {\n                type: {\n                  description: 'DB type',\n                  const: 'redis',\n                },\n                host: {\n                  title: 'Redis target DB host',\n                  type: 'string',\n                },\n                port: {\n                  title: 'Redis DB port',\n                  type: ['integer', 'string'],\n                  minimum: 1,\n                  maximum: 65535,\n                  pattern: '^\\\\${.*}$',\n                },\n                user: {\n                  title: 'Redis DB user',\n                  type: 'string',\n                },\n                password: {\n                  title: 'Redis DB password',\n                  type: 'string',\n                },\n                key: {\n                  title: 'Private key file to authenticate with',\n                  type: 'string',\n                },\n                key_password: {\n                  title: 'Password for unlocking an encrypted private key',\n                  type: 'string',\n                },\n                cert: {\n                  title: 'Client certificate file to authenticate with',\n                  type: 'string',\n                },\n                cacert: {\n                  title: 'CA certificate file to verify with',\n                  type: 'string',\n                },\n              },\n              required: ['host', 'port'],\n              dependentRequired: {\n                key: ['cert'],\n                cert: ['key'],\n                key_password: ['key'],\n              },\n              additionalProperties: false,\n            },\n            {\n              type: 'object',\n              description: 'SQL database',\n              properties: {\n                type: {\n                  description: 'DB type',\n                  type: 'string',\n                  enum: ['db2', 'mysql', 'oracle', 'postgresql', 'sqlserver'],\n                },\n                host: {\n                  description: 'DB host',\n                  type: 'string',\n                },\n                port: {\n                  description: 'DB port',\n                  type: 'integer',\n                  minimum: 1,\n                  maximum: 65535,\n                },\n                database: {\n                  description: 'DB name',\n                  type: 'string',\n                },\n                user: {\n                  description: 'DB user',\n                  type: 'string',\n                },\n                password: {\n                  description: 'DB password',\n                  type: 'string',\n                },\n                db_connection_pool_size: {\n                  description: 'Database connection pool size',\n                  type: 'integer',\n                  minimum: 1,\n                  default: 10,\n                },\n                statement_timeout: {\n                  description: 'Statement timeout in seconds (only for Oracle)',\n                  type: 'integer',\n                  minimum: 1,\n                  default: 60,\n                },\n                connect_args: {\n                  description:\n                    'Additional arguments to use when connecting to the DB',\n                  type: 'object',\n                  additionalProperties: true,\n                },\n                query_args: {\n                  description:\n                    'Additional query string arguments to use when connecting to the DB',\n                  type: 'object',\n                  additionalProperties: true,\n                },\n              },\n              additionalProperties: false,\n              oneOf: [\n                {\n                  properties: {\n                    type: {\n                      const: 'oracle',\n                    },\n                    database: {\n                      type: 'string',\n                    },\n                    query_args: {\n                      not: {\n                        required: ['service_name'],\n                      },\n                    },\n                  },\n                  required: ['type', 'database'],\n                },\n                {\n                  properties: {\n                    type: {\n                      const: 'oracle',\n                    },\n                    database: {\n                      not: {\n                        type: 'string',\n                      },\n                    },\n                    query_args: {\n                      required: ['service_name'],\n                    },\n                  },\n                  required: ['type', 'query_args'],\n                },\n                {\n                  properties: {\n                    type: {\n                      enum: ['db2', 'mysql', 'postgresql', 'sqlserver'],\n                    },\n                    database: {\n                      type: 'string',\n                    },\n                  },\n                  required: ['type', 'database'],\n                },\n              ],\n              examples: [\n                {\n                  hr: {\n                    type: 'postgresql',\n                    host: 'localhost',\n                    port: 5432,\n                    database: 'postgres',\n                    user: 'postgres',\n                    password: 'postgres',\n                    connect_args: {\n                      connect_timeout: 10,\n                    },\n                    query_args: {\n                      sslmode: 'verify-ca',\n                      sslrootcert: '/opt/ssl/ca.crt',\n                      sslcert: '/opt/ssl/client.crt',\n                      sslkey: '/opt/ssl/client.key',\n                    },\n                  },\n                },\n                {\n                  'my-oracle': {\n                    type: 'oracle',\n                    host: '172.17.0.4',\n                    port: 1521,\n                    user: 'c##dbzuser',\n                    password: 'dbz',\n                    query_args: {\n                      service_name: 'ORCLPDB1',\n                    },\n                  },\n                },\n              ],\n            },\n            {\n              description: 'Cassandra',\n              properties: {\n                type: {\n                  description: 'DB type',\n                  type: 'string',\n                  const: 'cassandra',\n                },\n                hosts: {\n                  description: 'Cassandra hosts',\n                  type: 'array',\n                  items: {\n                    type: 'string',\n                    title: 'Address of Cassandra node',\n                  },\n                },\n                port: {\n                  description: 'Cassandra DB port',\n                  type: 'integer',\n                  minimum: 1,\n                  maximum: 65535,\n                  default: 9042,\n                },\n                database: {\n                  description: 'DB name',\n                  type: 'string',\n                },\n                user: {\n                  description: 'DB user',\n                  type: 'string',\n                },\n                password: {\n                  description: 'DB password',\n                  type: 'string',\n                },\n              },\n              additionalProperties: false,\n              required: ['type', 'hosts'],\n              examples: [\n                {\n                  cache: {\n                    type: 'cassandra',\n                    hosts: ['localhost'],\n                    port: 9042,\n                    database: 'myDB',\n                    user: 'myUser',\n                    password: 'myPassword',\n                  },\n                },\n              ],\n            },\n          ],\n        },\n        additionalProperties: false,\n      },\n    },\n  },\n};\n\nexport const mockRdiJobsSchema = {\n  oneOf: [\n    {\n      title: 'Ingest Job Definition',\n      type: 'object',\n      properties: {\n        name: {\n          $ref: '#/$defs/job_name',\n        },\n        source: {\n          title: 'Source',\n          type: 'object',\n          properties: {\n            case_insensitive: {\n              $ref: '#/$defs/source_definition/case_insensitive',\n            },\n            server_name: {\n              $ref: '#/$defs/source_definition/server_name',\n            },\n            db: {\n              $ref: '#/$defs/source_definition/db',\n            },\n            schema: {\n              $ref: '#/$defs/source_definition/schema',\n            },\n            table: {\n              $ref: '#/$defs/source_definition/table',\n            },\n            row_format: {\n              $ref: '#/$defs/row_format',\n            },\n          },\n          required: ['table'],\n          additionalProperties: false,\n        },\n        transform: {\n          title: 'Transformation steps',\n          type: 'array',\n          items: {\n            type: 'object',\n            properties: {\n              uses: {\n                title: 'Block name',\n                type: 'string',\n              },\n              with: {\n                title: 'Block arguments',\n                type: 'object',\n              },\n            },\n            required: ['uses', 'with'],\n            additionalProperties: false,\n          },\n        },\n        output: {\n          $ref: '#/$defs/output',\n        },\n      },\n      anyOf: [\n        {\n          required: ['source', 'transform'],\n        },\n        {\n          required: ['source', 'output'],\n        },\n      ],\n      additionalProperties: false,\n    },\n    {\n      title: 'Write Behind Job Definition',\n      type: 'object',\n      properties: {\n        name: {\n          $ref: '#/$defs/job_name',\n        },\n        source: {\n          title: 'Source',\n          type: 'object',\n          properties: {\n            redis: {\n              title: 'Keyspace properties',\n              type: 'object',\n              properties: {\n                trigger: {\n                  const: 'write-behind',\n                },\n                key_pattern: {\n                  $ref: '#/$defs/source_definition/redis_key_pattern',\n                },\n                exclude_commands: {\n                  title: 'Redis commands to exclude from CDC',\n                  type: 'array',\n                  uniqueItems: true,\n                  maxItems: 16,\n                  items: {\n                    type: 'string',\n                    enum: [\n                      'del',\n                      'hdel',\n                      'hincrby',\n                      'hincrbyfloat',\n                      'hmset',\n                      'hset',\n                      'hsetnx',\n                      'unlink',\n                      'json.arrappend',\n                      'json.arrinsert',\n                      'json.arrpop',\n                      'json.arrtrim',\n                      'json.del',\n                      'json.numincrby',\n                      'json.set',\n                      'json.strappend',\n                      'json.toggle',\n                    ],\n                  },\n                },\n              },\n              required: ['key_pattern'],\n              additionalProperties: false,\n            },\n            row_format: {\n              $ref: '#/$defs/row_format',\n            },\n          },\n          required: ['redis'],\n          additionalProperties: false,\n        },\n        transform: {\n          title: 'Transformation steps',\n          type: 'array',\n          items: {\n            type: 'object',\n            properties: {\n              uses: {\n                title: 'Block name',\n                type: 'string',\n              },\n              with: {\n                title: 'Block arguments',\n                type: 'object',\n              },\n            },\n            required: ['uses', 'with'],\n            additionalProperties: false,\n          },\n        },\n        output: {\n          $ref: '#/$defs/output',\n        },\n      },\n      required: ['output'],\n      additionalProperties: false,\n    },\n  ],\n  $defs: {\n    job_name: {\n      title: 'Job name',\n      type: 'string',\n      pattern: '^[A-Za-z0-9_-]+$',\n      examples: ['my-job'],\n    },\n    row_format: {\n      title:\n        'Format of the data to be transformed: data_only - only payload, full - complete change record',\n      type: 'string',\n      enum: ['data_only', 'full'],\n    },\n    source_definition: {\n      case_insensitive: {\n        title: 'Case insensitive comparison',\n        type: 'boolean',\n        default: true,\n      },\n      server_name: {\n        title: 'Collector logical server name',\n        type: 'string',\n      },\n      db: {\n        title: 'DB name',\n        type: 'string',\n      },\n      schema: {\n        title: 'DB schema name',\n        type: 'string',\n      },\n      table: {\n        title: 'DB table name',\n        type: 'string',\n      },\n      redis_key_pattern: {\n        title: 'Redis key pattern',\n        type: 'string',\n      },\n    },\n    output_definition: {\n      expire: {\n        title: 'TTL in seconds for the target key in Redis database',\n        type: 'integer',\n        minimum: 0,\n        default: '0',\n      },\n      on_update: {\n        title: 'Target update strategy',\n        type: 'string',\n        enum: ['merge', 'replace'],\n        default: 'replace',\n      },\n      data_type: {\n        title: 'Target data type',\n        description: '',\n        type: 'string',\n        enum: ['hash', 'json', 'lua', 'set', 'sorted_set', 'stream', 'string'],\n        default: 'hash',\n      },\n    },\n    'redis.write_with-nest': {\n      title: 'Redis nesting settings',\n      type: 'object',\n      properties: {\n        parent: {\n          type: 'object',\n          title: 'Parent Source',\n          description: 'Parent collection to be used for child objects nesting',\n          properties: {\n            server_name: {\n              $ref: '#/$defs/source_definition/server_name',\n            },\n            schema: {\n              $ref: '#/$defs/source_definition/schema',\n            },\n            table: {\n              $ref: '#/$defs/source_definition/table',\n            },\n          },\n          required: ['table'],\n          additionalProperties: false,\n        },\n        parent_key: {\n          description:\n            'Foreign key field that is used to identify the parent record',\n          type: 'string',\n          examples: ['InvoiceId'],\n        },\n        child_key: {\n          description:\n            'Foreign key field in a child table, if different from the parent_key',\n          type: 'string',\n          examples: ['ParentInvoiceId'],\n        },\n        nesting_key: {\n          description:\n            'Primary key that is used to identify child element in a parent collection',\n          type: 'string',\n          examples: ['InvoiceLineId'],\n        },\n        path: {\n          description: 'Path used to embed child elements to a parent object',\n          type: 'string',\n          pattern: '^\\\\$\\\\.[a-zA-Z0-9_]+$',\n          examples: ['$.InvoiceLineItems'],\n        },\n        structure: {\n          description: 'Structure type used to embed children objects',\n          type: 'string',\n          enum: ['map'],\n          default: 'map',\n        },\n      },\n      required: ['parent', 'parent_key', 'nesting_key', 'path'],\n      additionalProperties: false,\n    },\n    'supported-arguments-for-sets': {\n      if: {\n        properties: {\n          data_type: {\n            const: 'set',\n          },\n        },\n        required: ['data_type'],\n      },\n      then: {\n        properties: {\n          args: {\n            properties: {\n              member: {\n                title: 'Member to be added to the set',\n                type: 'string',\n              },\n            },\n            required: ['member'],\n            additionalProperties: false,\n          },\n        },\n        required: ['args'],\n      },\n    },\n    'supported-arguments-for-sorted-sets': {\n      if: {\n        properties: {\n          data_type: {\n            const: 'sorted_set',\n          },\n        },\n        required: ['data_type'],\n      },\n      then: {\n        properties: {\n          args: {\n            properties: {\n              member: {\n                title: 'Member of the sorted set',\n                type: 'string',\n              },\n              score: {\n                title: 'Score associated with the member in the sorted set',\n                type: 'string',\n              },\n            },\n            required: ['member', 'score'],\n            additionalProperties: false,\n          },\n        },\n        required: ['args'],\n      },\n    },\n    'supported-arguments-for-strings': {\n      if: {\n        properties: {\n          data_type: {\n            const: 'string',\n          },\n        },\n        required: ['data_type'],\n      },\n      then: {\n        properties: {\n          args: {\n            properties: {\n              value: {\n                title: 'Value for the string data type',\n                type: 'string',\n              },\n            },\n            required: ['value'],\n            additionalProperties: false,\n          },\n        },\n        required: ['args'],\n      },\n    },\n    'supported-arguments-for-lua': {\n      if: {\n        properties: {\n          data_type: {\n            const: 'lua',\n          },\n        },\n        required: ['data_type'],\n      },\n      then: {\n        properties: {\n          args: {\n            properties: {\n              script: {\n                title: 'Lua script to be executed',\n                type: 'string',\n              },\n            },\n            required: ['script'],\n            additionalProperties: false,\n          },\n        },\n        required: ['args'],\n      },\n    },\n    'unsupported-legacy-rt-job': {\n      title: 'Read Through Job Definition',\n      type: 'object',\n      properties: {\n        source: {\n          title: 'Source',\n          type: 'object',\n          properties: {\n            redis: {\n              title: 'Keyspace properties',\n              type: 'object',\n              properties: {\n                trigger: {\n                  const: 'read-through',\n                },\n                key_pattern: {\n                  $ref: '#/$defs/source_definition/redis_key_pattern',\n                },\n              },\n              required: ['key_pattern'],\n              additionalProperties: false,\n            },\n            keys: {\n              title: 'Key parts',\n              type: 'object',\n              properties: {\n                expression: {\n                  title: 'Regular expression',\n                  type: 'string',\n                  examples: ['branch:(\\\\d+):emp:(\\\\d+)'],\n                },\n                delimiter: {\n                  title: 'Delimiter',\n                  type: 'string',\n                  minLength: 1,\n                  default: ':',\n                  examples: [':'],\n                },\n                fields: {\n                  title: 'Key parts mapping',\n                  type: 'object',\n                  minProperties: 1,\n                  additionalProperties: {\n                    type: 'integer',\n                    minimum: 0,\n                    examples: [2, 4],\n                  },\n                },\n              },\n              oneOf: [\n                {\n                  required: ['fields', 'expression'],\n                  not: {\n                    required: ['delimiter'],\n                  },\n                },\n                {\n                  required: ['fields'],\n                  not: {\n                    required: ['expression'],\n                  },\n                },\n              ],\n              additionalProperties: false,\n            },\n            connection: {\n              title: 'Connection',\n              type: 'string',\n            },\n            schema: {\n              $ref: '#/$defs/source_definition/schema',\n            },\n            table: {\n              title: 'Table to fetch data from',\n              type: 'string',\n              examples: ['EMP'],\n            },\n            columns: {\n              title: 'List of columns',\n              type: 'array',\n              items: {\n                type: 'string',\n                title: 'Column',\n              },\n              examples: [['first_name', 'last_name', 'birth_date']],\n            },\n            sql: {\n              title: 'SQL',\n              type: 'string',\n              examples: [\n                'SELECT emp.*, address.*, kids.* FROM emp LEFT JOIN address ON emp.employee_id = address.employee_id LEFT JOIN kids ON emp.employee_id = kids.employee_id WHERE emp.employee_fname = :first_name AND emp.employee_lname = :last_name;',\n              ],\n            },\n            retries: {\n              title: 'Number of attempts to read from the database',\n              type: 'integer',\n              minimum: 1,\n              default: '1',\n            },\n          },\n          required: ['redis', 'keys', 'connection'],\n          oneOf: [\n            {\n              required: ['table', 'columns', 'schema'],\n              not: {\n                required: ['sql'],\n              },\n            },\n            {\n              required: ['sql'],\n              allOf: [\n                {\n                  not: {\n                    required: ['table'],\n                  },\n                },\n                {\n                  not: {\n                    required: ['columns'],\n                  },\n                },\n                {\n                  not: {\n                    required: ['schema'],\n                  },\n                },\n              ],\n            },\n          ],\n          additionalProperties: false,\n        },\n        transform: {\n          title: 'Transformation steps',\n          type: 'array',\n          items: {\n            type: 'object',\n            properties: {\n              uses: {\n                title: 'Block name',\n                type: 'string',\n              },\n              with: {\n                title: 'Block arguments',\n                type: 'object',\n              },\n            },\n            required: ['uses', 'with'],\n            additionalProperties: false,\n          },\n        },\n        output: {\n          title: 'Output definitions',\n          type: 'object',\n          properties: {\n            expire: {\n              $ref: '#/$defs/output_definition/expire',\n            },\n            additionalProperties: false,\n          },\n        },\n      },\n      required: ['source'],\n      additionalProperties: false,\n    },\n    output: {\n      title: 'Output definitions',\n      type: 'array',\n      items: {\n        type: 'object',\n        properties: {\n          uses: {\n            title: 'Output writer',\n            type: 'string',\n            enum: ['cassandra.write', 'redis.write', 'relational.write'],\n          },\n          with: {\n            title: 'Output writer arguments',\n            type: 'object',\n            properties: {\n              nest: {\n                $ref: '#/$defs/redis.write_with-nest',\n              },\n              connection: {\n                title: 'Target database connection',\n                type: 'string',\n                default: 'target',\n              },\n              keyspace: {\n                type: 'string',\n                title: 'Keyspace',\n                description: 'Keyspace',\n                examples: ['employees'],\n              },\n              key: {\n                title: 'key',\n                description:\n                  'Expression to form the target Redis key when using redis.write block',\n                type: 'object',\n              },\n              keys: {\n                title: 'keys',\n                description:\n                  'List of fields to uniquely identify target record when using relational.write block',\n                type: 'array',\n              },\n              data_type: {\n                $ref: '#/$defs/output_definition/data_type',\n              },\n              schema: {\n                type: 'string',\n                title: 'The table schema of the target table',\n                description:\n                  'If left blank, the default schema of this connection will be used as defined in the `connections.yaml`',\n                examples: ['dbo'],\n              },\n              table: {\n                type: 'string',\n                title: 'The target table name',\n                description: 'Target table name',\n                examples: ['employees'],\n              },\n              mapping: {\n                title: 'Fields to write',\n                type: 'array',\n                items: {\n                  type: ['string', 'object'],\n                  title: 'Name of column',\n                },\n              },\n              foreach: {\n                type: 'string',\n                title:\n                  'Split a column into multiple records with a JMESPath expression',\n                description:\n                  'Use a JMESPath expression to split a column into multiple records. The expression should be in the format column: expression.',\n                pattern: '^(?!:).*:.*(?<!:)$',\n                examples: ['order_line: lines[]'],\n              },\n              args: {\n                title: 'Arguments for Redis writers',\n                type: 'object',\n              },\n              options: {\n                title: 'Redis writer options passed to the writer',\n                type: 'array',\n                default: [],\n              },\n              on_update: {\n                $ref: '#/$defs/output_definition/on_update',\n              },\n              expire: {\n                $ref: '#/$defs/output_definition/expire',\n              },\n            },\n            anyOf: [\n              {\n                properties: {\n                  on_update: {\n                    const: 'merge',\n                  },\n                  data_type: {\n                    const: 'json',\n                  },\n                },\n                required: ['nest', 'on_update', 'data_type'],\n              },\n              {\n                required: ['key'],\n                not: {\n                  required: ['nest'],\n                },\n              },\n              {\n                required: ['keys'],\n                not: {\n                  required: ['nest'],\n                },\n              },\n              {\n                required: ['data_type'],\n                not: {\n                  required: ['nest'],\n                },\n              },\n            ],\n            dependentSchemas: {\n              mapping: {\n                properties: {\n                  data_type: {\n                    enum: ['hash', 'json', 'stream'],\n                  },\n                },\n              },\n            },\n            allOf: [\n              {\n                $ref: '#/$defs/supported-arguments-for-sets',\n              },\n              {\n                $ref: '#/$defs/supported-arguments-for-sorted-sets',\n              },\n              {\n                $ref: '#/$defs/supported-arguments-for-strings',\n              },\n              {\n                $ref: '#/$defs/supported-arguments-for-lua',\n              },\n            ],\n            additionalProperties: false,\n          },\n        },\n        required: ['uses', 'with'],\n        additionalProperties: false,\n      },\n    },\n  },\n};\n\nexport const mockRdiSchema = {\n  config: mockRdiConfigSchema,\n  jobs: mockRdiJobsSchema,\n};\n\nexport const mockRdiRepository = jest.fn(() => ({\n  get: jest.fn(),\n  list: jest.fn(),\n  create: jest.fn(),\n  update: jest.fn(),\n  delete: jest.fn(),\n}));\n\nexport const mockRdiClientProvider = jest.fn(() => ({\n  getOrCreate: jest.fn(),\n  create: jest.fn(),\n  delete: jest.fn(),\n  deleteById: jest.fn(),\n  deleteManyByRdiId: jest.fn(),\n}));\n\nexport const mockRdiClientFactory = jest.fn(() => ({\n  createClient: jest.fn(),\n}));\n\nexport const mockRdiClientStorage = jest.fn(() => ({\n  getByMetadata: jest.fn(),\n  set: jest.fn(),\n  delete: jest.fn(),\n  deleteManyByRdiId: jest.fn(),\n}));\n\nexport const mockRdiPipelineAnalytics = jest.fn(() => ({\n  sendRdiPipelineFetched: jest.fn(),\n  sendRdiPipelineFetchFailed: jest.fn(),\n  sendRdiPipelineDeployed: jest.fn(),\n  sendRdiPipelineDeployFailed: jest.fn(),\n}));\n\nexport const mockRdiAnalytics = jest.fn(() => ({\n  sendRdiInstanceDeleted: jest.fn(),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-client.ts",
    "content": "import { ClientMetadata } from 'src/common/models';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\nimport { RedisClientLib } from 'src/modules/redis/redis.client.factory';\nimport { mockCommonClientMetadata } from 'src/__mocks__/common';\nimport { mockDatabase } from 'src/__mocks__/databases';\nimport { BadRequestException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { Database } from 'src/modules/database/models/database';\n\nexport const mockClientDatabase: Partial<Database> = {\n  providerDetails: mockDatabase.providerDetails,\n};\n\nexport interface IRedisClientInstance {\n  id: string;\n  clientMetadata: ClientMetadata;\n  client: any;\n  lastTimeUsed: number;\n}\n\nexport const mockInvalidClientMetadataError = new BadRequestException(\n  ERROR_MESSAGES.INVALID_CLIENT_METADATA,\n);\nexport const mockInvalidSessionMetadataError = new BadRequestException(\n  ERROR_MESSAGES.INVALID_SESSION_METADATA,\n);\n\n// todo: NEW. remove everything above\nexport class MockRedisClient extends RedisClient {\n  constructor(\n    clientMetadata: ClientMetadata,\n    client: any = jest.fn(),\n    options = {},\n    database: Partial<Database> = mockClientDatabase,\n  ) {\n    super(clientMetadata, client, options, database);\n  }\n\n  public isConnected = jest.fn().mockReturnValue(true);\n\n  public getConnectionType = jest\n    .fn()\n    .mockReturnValue(RedisClientConnectionType.STANDALONE);\n\n  public nodes = jest.fn().mockResolvedValue([this]);\n\n  public sendCommand = jest.fn().mockResolvedValue(undefined);\n\n  public sendPipeline = jest.fn().mockResolvedValue(undefined);\n\n  public publish = jest.fn().mockResolvedValue(undefined);\n\n  public subscribe = jest.fn().mockResolvedValue(undefined);\n\n  public pSubscribe = jest.fn().mockResolvedValue(undefined);\n\n  public unsubscribe = jest.fn().mockResolvedValue(undefined);\n\n  public pUnsubscribe = jest.fn().mockResolvedValue(undefined);\n\n  // public sendMulti = jest.fn().mockResolvedValue(undefined);\n\n  public call = jest.fn().mockResolvedValue(undefined);\n\n  public monitor = jest.fn().mockResolvedValue(undefined);\n\n  public disconnect = jest.fn().mockResolvedValue(undefined);\n\n  public isFeatureSupported = jest.fn().mockResolvedValue(undefined);\n\n  public getInfo = jest.fn().mockResolvedValue(undefined);\n\n  public quit = jest.fn().mockResolvedValue(undefined); // todo: should return commands results\n\n  public getCurrentDbIndex = jest.fn().mockResolvedValue(0);\n}\n\nexport const mockStandaloneRedisClient = new MockRedisClient(\n  mockCommonClientMetadata,\n);\n\nexport class MockClusterRedisClient extends MockRedisClient {\n  constructor(clientMetadata: ClientMetadata, client: any = jest.fn()) {\n    super(clientMetadata, client);\n  }\n\n  public getConnectionType = jest\n    .fn()\n    .mockReturnValue(RedisClientConnectionType.CLUSTER);\n\n  public nodes = jest\n    .fn()\n    .mockResolvedValue([mockStandaloneRedisClient, mockStandaloneRedisClient]);\n}\n\nexport const mockClusterRedisClient = new MockClusterRedisClient(\n  mockCommonClientMetadata,\n);\n\nexport class MockSentinelRedisClient extends MockRedisClient {\n  constructor(clientMetadata: ClientMetadata, client: any = jest.fn()) {\n    super(clientMetadata, client);\n  }\n\n  public getConnectionType = jest\n    .fn()\n    .mockReturnValue(RedisClientConnectionType.SENTINEL);\n}\n\nexport const mockSentinelRedisClient = new MockSentinelRedisClient(\n  mockCommonClientMetadata,\n);\n\nexport const generateMockRedisClient = (\n  clientMetadata: Partial<ClientMetadata>,\n  client = jest.fn(),\n  options = {},\n  database: Partial<Database> = {},\n): MockRedisClient =>\n  new MockRedisClient(\n    clientMetadata as ClientMetadata,\n    client,\n    options,\n    { ...mockClientDatabase, ...database },\n  );\n\nexport const mockRedisClientInstance: IRedisClientInstance = {\n  id: RedisClient.generateId(mockCommonClientMetadata),\n  clientMetadata: mockCommonClientMetadata,\n  client: mockStandaloneRedisClient,\n  lastTimeUsed: 1619791508019,\n};\n\nexport const generateMockRedisClientInstance = (\n  clientMetadata: Partial<ClientMetadata>,\n): IRedisClientInstance => ({\n  id: RedisClient.generateId(clientMetadata as ClientMetadata),\n  clientMetadata: clientMetadata as ClientMetadata,\n  client: mockStandaloneRedisClient,\n  lastTimeUsed: Date.now(),\n});\n\nexport const mockRedisClientStorage = jest.fn(() => ({\n  get: jest.fn().mockResolvedValue(mockStandaloneRedisClient),\n  getByMetadata: jest.fn().mockResolvedValue(mockStandaloneRedisClient),\n  set: jest.fn().mockResolvedValue(mockStandaloneRedisClient),\n  remove: jest.fn().mockResolvedValue(1),\n  removeByMetadata: jest.fn().mockResolvedValue(1),\n  removeManyByMetadata: jest.fn().mockResolvedValue(1),\n}));\n\nexport const mockIoRedisRedisConnectionStrategy = jest.fn(() => ({\n  lib: RedisClientLib.IOREDIS,\n  createStandaloneClient: jest\n    .fn()\n    .mockResolvedValue(mockStandaloneRedisClient),\n  createClusterClient: jest.fn().mockResolvedValue(mockClusterRedisClient),\n  createSentinelClient: jest.fn().mockResolvedValue(mockSentinelRedisClient),\n}));\n\nexport const mockNodeRedisConnectionStrategy = jest.fn(() => ({\n  lib: RedisClientLib.NODE_REDIS,\n  createStandaloneClient: jest\n    .fn()\n    .mockResolvedValue(mockStandaloneRedisClient),\n  createClusterClient: jest.fn().mockResolvedValue(mockClusterRedisClient),\n  createSentinelClient: jest.fn().mockResolvedValue(mockSentinelRedisClient),\n}));\n\nexport const mockRedisClientFactory = jest.fn(() => ({\n  getConnectionStrategy: jest\n    .fn()\n    .mockReturnValue(mockIoRedisRedisConnectionStrategy()),\n  createClient: jest\n    .fn()\n    .mockImplementation(async (clientMetadata: ClientMetadata) =>\n      Promise.resolve(new MockRedisClient(clientMetadata)),\n    ),\n  createClientAutomatically: jest\n    .fn()\n    .mockResolvedValue(mockStandaloneRedisClient),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-databases.ts",
    "content": "// import {\n//   ConnectionType,\n//   DatabaseInstanceEntity,\n//   HostingProvider,\n// } from 'src/modules/core/models/database-instance.entity';\n// import { mockCaCertEntity, mockClientCertEntity } from './certificates';\n//\n// export const mockStandaloneDatabaseEntity: DatabaseInstanceEntity = {\n//   id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805',\n//   host: 'localhost',\n//   port: 6379,\n//   db: 0,\n//   name: 'redis-database',\n//   nameFromProvider: null,\n//   username: null,\n//   password: null,\n//   tls: true,\n//   verifyServerCert: true,\n//   caCert: mockCaCertEntity,\n//   clientCert: mockClientCertEntity,\n//   lastConnection: null,\n//   connectionType: ConnectionType.STANDALONE,\n//   sentinelMasterName: null,\n//   sentinelMasterUsername: null,\n//   sentinelMasterPassword: null,\n//   nodes: null,\n//   provider: HostingProvider.LOCALHOST,\n//   modules: '[]',\n//   encryption: null,\n//   tlsServername: 'server-name',\n// };\n//\n// export const mockOSSClusterDatabaseEntity: DatabaseInstanceEntity = {\n//   id: '3a41f8ea-a36a-11eb-bcbc-0242ac130002',\n//   host: 'localhost',\n//   port: 7001,\n//   db: null,\n//   name: 'oss-cluster',\n//   nameFromProvider: null,\n//   username: null,\n//   password: null,\n//   tls: true,\n//   verifyServerCert: true,\n//   caCert: mockCaCertEntity,\n//   clientCert: mockClientCertEntity,\n//   lastConnection: null,\n//   connectionType: ConnectionType.CLUSTER,\n//   sentinelMasterName: null,\n//   sentinelMasterUsername: null,\n//   sentinelMasterPassword: null,\n//   nodes: '[{\"host\":\"localhost\",\"port\":7001},{\"host\":\"localhost\",\"port\":7002}]',\n//   provider: HostingProvider.LOCALHOST,\n//   modules: '[]',\n//   encryption: null,\n// };\n//\n// export const mockSentinelDatabaseEntity: DatabaseInstanceEntity = {\n//   id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805',\n//   host: 'localhost',\n//   port: 26379,\n//   db: 0,\n//   name: 'sentinel-database',\n//   nameFromProvider: null,\n//   username: null,\n//   password: null,\n//   tls: true,\n//   verifyServerCert: true,\n//   caCert: mockCaCertEntity,\n//   clientCert: mockClientCertEntity,\n//   lastConnection: null,\n//   connectionType: ConnectionType.SENTINEL,\n//   sentinelMasterName: 'master-group',\n//   sentinelMasterUsername: null,\n//   sentinelMasterPassword: null,\n//   nodes: '[{\"host\":\"localhost\",\"port\":5001}]',\n//   provider: HostingProvider.LOCALHOST,\n//   modules: '[]',\n//   encryption: null,\n// };\n//\n// export const mockDatabasesProvider = () => ({\n//   exists: jest.fn(),\n//   getAll: jest.fn(),\n//   getOneById: jest.fn(),\n//   update: jest.fn(),\n//   patch: jest.fn(),\n//   save: jest.fn(),\n// });\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-enterprise.ts",
    "content": "import { RedisEnterpriseDatabase } from 'src/modules/redis-enterprise/dto/cluster.dto';\nimport { RedisEnterpriseDatabaseStatus } from 'src/modules/redis-enterprise/models/redis-enterprise-database';\nimport { AddRedisEnterpriseDatabasesDto } from 'src/modules/redis-enterprise/dto/redis-enterprise-cluster.dto';\n\nexport const mockRedisEnterpriseDatabaseDto: RedisEnterpriseDatabase = {\n  uid: 1,\n  address: '172.17.0.2',\n  dnsName: 'redis-12000.clus.local',\n  modules: [],\n  tags: [],\n  name: 'db',\n  options: {},\n  port: 12000,\n  status: RedisEnterpriseDatabaseStatus.Active,\n  tls: false,\n  password: null,\n};\n\nexport const mockAddRedisEnterpriseDatabasesDto = Object.assign(\n  new AddRedisEnterpriseDatabasesDto(),\n  {\n    host: 'localhost',\n    port: 9443,\n    username: 'admin',\n    password: 'password',\n    uids: [1],\n  },\n);\n\nexport const mockRedisEnterpriseAnalytics = jest.fn(() => ({\n  sendGetRedisSoftwareDbsSucceedEvent: jest.fn(),\n  sendGetRedisSoftwareDbsFailedEvent: jest.fn(),\n}));\n\nexport const mockRedisEnterpriseService = jest.fn(() => ({\n  addRedisEnterpriseDatabases: jest.fn().mockResolvedValue([]),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-info.ts",
    "content": "export const mockRedisServerInfoResponse: string =\n  ' # Server\\r\\n' +\n  'redis_version:6.0.5\\r\\n' +\n  'redis_mode:standalone\\r\\n' +\n  'os:Linux 4.15.0-1087-gcp x86_64\\r\\n' +\n  'uptime_in_seconds:1000\\r\\n' +\n  'arch_bits:64\\r\\n' +\n  'tcp_port:11113\\r\\n';\n\nexport const mockRedisClientsInfoResponse: string =\n  '# Clients\\r\\n' +\n  'connected_clients:1\\r\\n' +\n  'client_longest_output_list:0\\r\\n' +\n  'client_biggest_input_buf:0\\r\\n' +\n  'blocked_clients:0\\r\\n';\n\nexport const mockRedisKeyspaceInfoResponse: string =\n  '# Keyspace\\r\\ndb0:keys=1,expires=0,avg_ttl=0\\r\\n';\n\nexport const mockRedisKeyspaceInfoResponseNoKeyspaceData: string =\n  '# Keyspace\\r\\n \\r\\n';\n\nexport const mockRedisMemoryInfoResponse: string =\n  '# Memory\\r\\n' +\n  'used_memory:1000000\\r\\n' +\n  'used_memory_human:1M\\r\\n' +\n  'used_memory_rss:1000000\\r\\n' +\n  'used_memory_peak:1000000\\r\\n' +\n  'used_memory_peak_human:1M\\r\\n' +\n  'used_memory_lua:37888\\r\\n' +\n  'mem_fragmentation_ratio:1\\r\\n' +\n  'mem_allocator:jemalloc-5.1.0\\r\\n';\n\nexport const mockRedisReplicationInfoResponse: string =\n  '# Replication\\r\\n' +\n  'role:master\\r\\n' +\n  'connected_slaves:0\\r\\n' +\n  'master_repl_offset:0\\r\\n' +\n  'repl_backlog_active:0\\r\\n' +\n  'repl_backlog_size:1000\\r\\n' +\n  'repl_backlog_first_byte_offset:0\\r\\n' +\n  'repl_backlog_histlen:0\\r\\n';\n\nexport const mockRedisStatsInfoResponse: string =\n  '# Stats\\r\\nkeyspace_hits:1000\\r\\nkeyspace_misses:0\\r\\n';\n\nexport const mockRedisClusterOkInfoResponse: string =\n  ' # Cluster\\r\\n' +\n  'cluster_state:ok\\r\\n' +\n  'cluster_slots_assigned:16384\\r\\n' +\n  'cluster_slots_ok:16384\\r\\n' +\n  'cluster_slots_pfail:0\\r\\n' +\n  'cluster_slots_fail:0\\r\\n' +\n  'cluster_known_nodes:6\\r\\n' +\n  'cluster_size:3\\r\\n' +\n  'cluster_current_epoch:6\\r\\n' +\n  'cluster_my_epoch:2\\r\\n' +\n  'cluster_current_epoch:6\\r\\n' +\n  'cluster_slots_fail:0\\r\\n';\n\nexport const mockRedisClusterFailInfoResponse: string =\n  ' # Cluster\\r\\n' +\n  'cluster_state:fail\\r\\n' +\n  'cluster_slots_assigned:16384\\r\\n' +\n  'cluster_slots_ok:16384\\r\\n' +\n  'cluster_slots_pfail:0\\r\\n' +\n  'cluster_slots_fail:0\\r\\n' +\n  'cluster_known_nodes:6\\r\\n' +\n  'cluster_size:3\\r\\n' +\n  'cluster_current_epoch:6\\r\\n' +\n  'cluster_my_epoch:2\\r\\n' +\n  'cluster_current_epoch:6\\r\\n' +\n  'cluster_slots_fail:0\\r\\n';\n\nexport const mockRedisClusterDisabledInfoResponse: string =\n  '# Cluster\\r\\ncluster_enabled:0\\r\\n';\n\nexport const mockSentinelMasterInOkState: string[] = [\n  'name',\n  'mymaster',\n  'ip',\n  '127.0.0.1',\n  'port',\n  '6379',\n  'num-slaves',\n  '1',\n  'flags',\n  'master',\n];\nexport const mockSentinelMasterInDownState: string[] = [\n  'name',\n  'mymaster',\n  'ip',\n  '127.0.0.1',\n  'port',\n  '6379',\n  'num-slaves',\n  '1',\n  'flags',\n  's_down,masrer',\n];\n\nexport const mockRedisSentinelMasterResponse: Array<string[]> = [\n  mockSentinelMasterInOkState,\n];\n\n// eslint-disable-next-line max-len\nexport const mockRedisClusterNodesResponse: string =\n  '07c37dfeb235213a872192d90877d0cd55635b91 127.0.0.1:30004@31004 slave e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 0 1426238317239 4 connected\\n' +\n  'e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 127.0.0.1:30001@31001 myself,master - 0 0 1 connected 0-16383';\n\n// eslint-disable-next-line max-len\nexport const mockRedisClusterNodesResponseIPv6: string =\n  '07c37dfeb235213a872192d90877d0cd55635b91 2001:db8::1:7001@17001 slave e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 0 1426238317239 4 connected\\n' +\n  'e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca 2001:db8::2:7002@17002 myself,master - 0 0 1 connected 0-16383';\n\nexport const mockStandaloneRedisInfoReply: string = `${\n  mockRedisServerInfoResponse\n}\\r\\n${mockRedisClientsInfoResponse}\\r\\n${mockRedisMemoryInfoResponse}\\r\\n${\n  mockRedisStatsInfoResponse\n}\\r\\n${mockRedisReplicationInfoResponse}\\r\\n${\n  mockRedisClusterDisabledInfoResponse\n}\\r\\n${mockRedisKeyspaceInfoResponse}`;\n\nexport const mockWhitelistCommandsResponse = ['get', 'custom.command'];\n\nexport const mockRedisCommandReply: any[][] = [\n  ['get', 0, ['readonly']],\n  ['role', 0, ['readonly']],\n  ['set', 0, ['write']],\n  ['xread', 0, ['readonly']],\n  ['custom.command', 0, ['readonly']],\n];\n\nexport const mockPluginWhiteListCommandsResponse: string[] = [\n  'get',\n  'custom.command',\n];\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-rs.ts",
    "content": "import { isArray, isString } from 'lodash';\n\nexport const mockRedisFtInfoReply = [\n  'index_name',\n  'idx:bicycle',\n  'index_options',\n  [],\n  'index_definition',\n  ['key_type', 'HASH', 'prefixes', ['bicycle:'], 'default_score', '1'],\n  'attributes',\n  [\n    [\n      'identifier',\n      '$.brand',\n      'attribute',\n      'brand',\n      'type',\n      'TEXT',\n      'WEIGHT',\n      '1',\n    ],\n    [\n      'identifier',\n      '$.model',\n      'attribute',\n      'model',\n      'type',\n      'TEXT',\n      'WEIGHT',\n      '1',\n      'NOSTEM',\n    ],\n    [\n      'identifier',\n      '$.description',\n      'attribute',\n      'description',\n      'type',\n      'TEXT',\n      'WEIGHT',\n      '1',\n    ],\n    [\n      'identifier',\n      '$.price',\n      'attribute',\n      'price',\n      'type',\n      'NUMERIC',\n      'NOINDEX',\n    ],\n    [\n      'identifier',\n      '$.condition',\n      'attribute',\n      'condition',\n      'type',\n      'TAG',\n      'SEPARATOR',\n      ',',\n      'CASESENSITIVE',\n      'SORTABLE',\n    ],\n  ],\n  'num_docs',\n  '0',\n  'max_doc_id',\n  '0',\n  'num_terms',\n  '0',\n  'num_records',\n  '0',\n  'inverted_sz_mb',\n  '0',\n  'vector_index_sz_mb',\n  '0',\n  'total_inverted_index_blocks',\n  '0',\n  'offset_vectors_sz_mb',\n  '0',\n  'doc_table_size_mb',\n  '0',\n  'sortable_values_size_mb',\n  '0',\n  'key_table_size_mb',\n  '0',\n  'records_per_doc_avg',\n  '-nan',\n  'bytes_per_record_avg',\n  '-nan',\n  'offsets_per_term_avg',\n  '-nan',\n  'offset_bits_per_record_avg',\n  '-nan',\n  'hash_indexing_failures',\n  '0',\n  'indexing',\n  '0',\n  'percent_indexed',\n  '1',\n  'gc_stats',\n  [\n    'bytes_collected',\n    '0',\n    'total_ms_run',\n    '0',\n    'total_cycles',\n    '0',\n    'average_cycle_time_ms',\n    '-nan',\n    'last_run_time_ms',\n    '0',\n    'gc_numeric_trees_missed',\n    '0',\n    'gc_blocks_denied',\n    '0',\n  ],\n  'cursor_stats',\n  [\n    'global_idle',\n    0,\n    'global_total',\n    0,\n    'index_capacity',\n    128,\n    'index_total',\n    0,\n  ],\n  'dialect_stats',\n  ['dialect_1', '0', 'dialect_2', '0'],\n];\n\nexport const mockFtInfoAnalyticsData = {\n  attributes: [\n    {\n      type: 'TEXT',\n      weight: '1',\n    },\n    {\n      nostem: true,\n      type: 'TEXT',\n      weight: '1',\n    },\n    {\n      type: 'TEXT',\n      weight: '1',\n    },\n    {\n      noindex: true,\n      type: 'NUMERIC',\n    },\n    {\n      casesensitive: true,\n      sortable: true,\n      type: 'TAG',\n    },\n  ],\n  default_score: '1',\n  key_type: 'HASH',\n  max_doc_id: '0',\n  num_docs: '0',\n  num_records: '0',\n  num_terms: '0',\n  dialect_stats: {\n    dialect_1: '0',\n    dialect_2: '0',\n  },\n};\n\ntype InfoReplyRaw = string | number | InfoReplyRaw[];\nexport const replyToBuffer = (input: InfoReplyRaw[]) => {\n  if (isArray(input)) {\n    return input.map(replyToBuffer);\n  }\n\n  return isString(input) ? Buffer.from(input) : input;\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-sentinel.ts",
    "content": "import {\n  SentinelMaster,\n  SentinelMasterStatus,\n} from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { Endpoint } from 'src/common/models';\nimport { CreateSentinelDatabasesDto } from 'src/modules/redis-sentinel/dto/create.sentinel.databases.dto';\n\nexport const mockOtherSentinelsReply = [['ip', '127.0.0.2', 'port', '26379']];\n\nexport const mockOtherSentinelEndpoint: Endpoint = {\n  host: '127.0.0.2',\n  port: 26379,\n};\n\nexport const mockSentinelMasterEndpoint: Endpoint = {\n  host: '127.0.0.1',\n  port: 6379,\n};\n\nexport const mockSentinelMasterDto: SentinelMaster = {\n  name: 'mymaster',\n  host: mockSentinelMasterEndpoint.host,\n  port: mockSentinelMasterEndpoint.port,\n  numberOfSlaves: 1,\n  status: SentinelMasterStatus.Active,\n  nodes: [mockOtherSentinelEndpoint],\n};\n\nexport const mockCreateSentinelDatabasesDto = Object.assign(\n  new CreateSentinelDatabasesDto(),\n  {\n    ...mockOtherSentinelEndpoint,\n    masters: [\n      {\n        name: mockSentinelMasterDto.name,\n        alias: mockSentinelMasterDto.name,\n      },\n    ],\n  },\n);\n\nexport const mockRedisSentinelAnalytics = jest.fn(() => ({\n  sendGetSentinelMastersSucceedEvent: jest.fn(),\n  sendGetSentinelMastersFailedEvent: jest.fn(),\n}));\n\nexport const mockRedisSentinelService = jest.fn(() => ({\n  getSentinelMasters: jest.fn().mockResolvedValue([mockSentinelMasterDto]),\n  createSentinelDatabases: jest.fn().mockResolvedValue([]),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis-utils.ts",
    "content": "import {\n  mockOtherSentinelEndpoint,\n  mockSentinelMasterDto,\n} from 'src/__mocks__/redis-sentinel';\n\nexport const mockDbSizeKeysCount = 10_000;\n\nexport const mockInfoKeysCount = 20_000;\n\nexport const mockRedisKeysUtil = {\n  __esModule: true,\n  getTotalKeysFromInfo: jest.fn().mockResolvedValue(mockInfoKeysCount),\n  getTotalKeysFromDBSize: jest.fn().mockResolvedValue(mockDbSizeKeysCount),\n  getTotalKeys: jest.fn().mockResolvedValue(mockDbSizeKeysCount),\n};\n\nexport const mockRedisKeysUtilModule = () => mockRedisKeysUtil;\n\nexport const mockRedisSentinelUtil = {\n  __esModule: true,\n  isSentinel: jest.fn().mockResolvedValue(true),\n  discoverOtherSentinels: jest\n    .fn()\n    .mockResolvedValue([mockOtherSentinelEndpoint]),\n  discoverSentinelMasterGroups: jest\n    .fn()\n    .mockResolvedValue([mockSentinelMasterDto]),\n};\n\nexport const mockRedisSentinelUtilModule = () => mockRedisSentinelUtil;\n\nexport const mockRedisClusterUtil = {\n  __esModule: true,\n  isCluster: jest.fn().mockResolvedValue(true),\n  discoverClusterNodes: jest\n    .fn()\n    .mockResolvedValue([mockOtherSentinelEndpoint]),\n};\n\nexport const mockRedisClusterUtilModule = () => mockRedisClusterUtil;\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redis.ts",
    "content": "import IORedis from 'ioredis';\n\nexport const mockIORedisClientExec = jest.fn();\nconst getRedisCommanderMockFunctions = jest.fn(() => ({\n  sendCommand: jest.fn(),\n  info: jest.fn(),\n  monitor: jest.fn(),\n  disconnect: jest.fn(),\n  duplicate: jest.fn(),\n  call: jest.fn(),\n  subscribe: jest.fn(),\n  psubscribe: jest.fn(),\n  unsubscribe: jest.fn(),\n  punsubscribe: jest.fn(),\n  publish: jest.fn(),\n  pipeline: jest.fn().mockReturnThis(),\n  exec: mockIORedisClientExec,\n  cluster: jest.fn(),\n  quit: jest.fn(),\n}));\n\nexport const mockIORedisClient = {\n  ...Object.create(IORedis.prototype),\n  ...getRedisCommanderMockFunctions(),\n  status: 'ready',\n  options: {\n    db: 0,\n  },\n};\n\nexport const mockIOClusterNode1 = {\n  ...Object.create(IORedis.prototype),\n  ...getRedisCommanderMockFunctions(),\n  status: 'ready',\n  options: {\n    host: '127.0.100.1',\n    port: 6379,\n    db: 0,\n  },\n};\n\nexport const mockIOClusterNode2 = {\n  ...Object.create(IORedis.prototype),\n  ...getRedisCommanderMockFunctions(),\n  status: 'ready',\n  options: {\n    host: '127.0.100.2',\n    port: 6379,\n    db: 0,\n  },\n};\n\nexport const mockIORedisSentinel = {\n  ...Object.create(IORedis.prototype),\n  ...getRedisCommanderMockFunctions(),\n  status: 'ready',\n  options: {\n    db: 0,\n  },\n};\n\nexport const mockIORedisCluster = {\n  ...Object.create(IORedis.Cluster.prototype),\n  ...getRedisCommanderMockFunctions(),\n  isCluster: true,\n  status: 'ready',\n  nodes: jest.fn().mockReturnValue([mockIOClusterNode1, mockIOClusterNode2]),\n};\n\nexport const mockRedisService = jest.fn(() => ({\n  getClientInstance: jest.fn().mockResolvedValue({\n    client: mockIORedisClient,\n  }),\n  setClientInstance: jest.fn().mockReturnValue({\n    client: mockIORedisClient,\n  }),\n  isClientConnected: jest.fn().mockReturnValue(true),\n  removeClientInstance: jest.fn(),\n  removeClientInstances: jest.fn(),\n  findClientInstances: jest.fn(),\n}));\n\nexport const mockRedisConnectionFactory = jest.fn(() => ({\n  createRedisConnection: jest.fn().mockResolvedValue(mockIORedisClient),\n  createStandaloneConnection: jest.fn().mockResolvedValue(mockIORedisClient),\n  createSentinelConnection: jest.fn().mockResolvedValue(mockIORedisSentinel),\n  createClusterConnection: jest.fn().mockResolvedValue(mockIORedisCluster),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/redisearch.ts",
    "content": "import { IndexInfoDto } from 'src/modules/browser/redisearch/dto';\n\ninterface IndexInfoRawOverrides {\n  indexName?: string;\n  keyType?: string;\n  prefixes?: string[];\n  attributes?: string[][];\n  numDocs?: string;\n}\n\nexport const buildIndexInfoRaw = (overrides: IndexInfoRawOverrides = {}) => [\n  'index_name',\n  overrides.indexName ?? 'idx:test',\n  'index_options',\n  [],\n  'index_definition',\n  [\n    'key_type',\n    overrides.keyType ?? 'HASH',\n    'prefixes',\n    overrides.prefixes ?? ['test:'],\n    'default_score',\n    '1',\n  ],\n  'attributes',\n  overrides.attributes ?? [\n    ['identifier', 'field', 'attribute', 'field', 'type', 'TEXT'],\n  ],\n  'num_docs',\n  overrides.numDocs ?? '0',\n];\n\nexport const mockIndexInfoRaw = [\n  'index_name',\n  'idx:movie',\n  'index_options',\n  [],\n  'index_definition',\n  ['key_type', 'HASH', 'prefixes', ['movie:'], 'default_score', '1'],\n  'attributes',\n  [\n    [\n      'identifier',\n      'title',\n      'attribute',\n      'title',\n      'type',\n      'TEXT',\n      'WEIGHT',\n      '1',\n      'SORTABLE',\n    ],\n    [\n      'identifier',\n      'release_year',\n      'attribute',\n      'release_year',\n      'type',\n      'NUMERIC',\n      'SORTABLE',\n      'UNF',\n    ],\n    [\n      'identifier',\n      'rating',\n      'attribute',\n      'rating',\n      'type',\n      'NUMERIC',\n      'SORTABLE',\n      'UNF',\n    ],\n    [\n      'identifier',\n      'genre',\n      'attribute',\n      'genre',\n      'type',\n      'TAG',\n      'SEPARATOR',\n      ',',\n      'SORTABLE',\n    ],\n  ],\n  'num_docs',\n  '2',\n  'max_doc_id',\n  '2',\n  'num_terms',\n  '13',\n  'num_records',\n  '19',\n  'inverted_sz_mb',\n  '0.0016384124755859375',\n  'vector_index_sz_mb',\n  '0',\n  'total_inverted_index_blocks',\n  '17',\n  'offset_vectors_sz_mb',\n  '1.239776611328125e-5',\n  'doc_table_size_mb',\n  '1.468658447265625e-4',\n  'sortable_values_size_mb',\n  '2.498626708984375e-4',\n  'key_table_size_mb',\n  '8.296966552734375e-5',\n  'tag_overhead_sz_mb',\n  '5.53131103515625e-5',\n  'text_overhead_sz_mb',\n  '4.3392181396484375e-4',\n  'total_index_memory_sz_mb',\n  '0.0026903152465820313',\n  'geoshapes_sz_mb',\n  '0',\n  'records_per_doc_avg',\n  '9.5',\n  'bytes_per_record_avg',\n  '90.42105102539063',\n  'offsets_per_term_avg',\n  '0.6842105388641357',\n  'offset_bits_per_record_avg',\n  '8',\n  'hash_indexing_failures',\n  '0',\n  'total_indexing_time',\n  '0.890999972820282',\n  'indexing',\n  '0',\n  'percent_indexed',\n  '1',\n  'number_of_uses',\n  17,\n  'cleaning',\n  0,\n  'gc_stats',\n  [\n    'bytes_collected',\n    '0',\n    'total_ms_run',\n    '0',\n    'total_cycles',\n    '0',\n    'average_cycle_time_ms',\n    'nan',\n    'last_run_time_ms',\n    '0',\n    'gc_numeric_trees_missed',\n    '0',\n    'gc_blocks_denied',\n    '0',\n  ],\n  'cursor_stats',\n  [\n    'global_idle',\n    0,\n    'global_total',\n    0,\n    'index_capacity',\n    128,\n    'index_total',\n    0,\n  ],\n  'dialect_stats',\n  ['dialect_1', 1, 'dialect_2', 0, 'dialect_3', 0, 'dialect_4', 0],\n  'Index Errors',\n  [\n    'indexing failures',\n    0,\n    'last indexing error',\n    'N/A',\n    'last indexing error key',\n    'N/A',\n  ],\n  'field statistics',\n  [\n    [\n      'identifier',\n      'title',\n      'attribute',\n      'title',\n      'Index Errors',\n      [\n        'indexing failures',\n        0,\n        'last indexing error',\n        'N/A',\n        'last indexing error key',\n        'N/A',\n      ],\n    ],\n    [\n      'identifier',\n      'release_year',\n      'attribute',\n      'release_year',\n      'Index Errors',\n      [\n        'indexing failures',\n        0,\n        'last indexing error',\n        'N/A',\n        'last indexing error key',\n        'N/A',\n      ],\n    ],\n    [\n      'identifier',\n      'rating',\n      'attribute',\n      'rating',\n      'Index Errors',\n      [\n        'indexing failures',\n        0,\n        'last indexing error',\n        'N/A',\n        'last indexing error key',\n        'N/A',\n      ],\n    ],\n    [\n      'identifier',\n      'genre',\n      'attribute',\n      'genre',\n      'Index Errors',\n      [\n        'indexing failures',\n        0,\n        'last indexing error',\n        'N/A',\n        'last indexing error key',\n        'N/A',\n      ],\n    ],\n  ],\n];\n\nexport const mockIndexInfoDto: IndexInfoDto = {\n  index_name: 'idx:movie',\n  index_options: {},\n  index_definition: {\n    key_type: 'HASH',\n    prefixes: ['movie:'],\n    default_score: '1',\n  },\n  attributes: [\n    {\n      identifier: 'title',\n      attribute: 'title',\n      type: 'TEXT',\n      WEIGHT: '1',\n      SORTABLE: true,\n      NOINDEX: undefined,\n      CASESENSITIVE: undefined,\n      UNF: undefined,\n      NOSTEM: undefined,\n    },\n    {\n      identifier: 'release_year',\n      attribute: 'release_year',\n      type: 'NUMERIC',\n      SORTABLE: true,\n      NOINDEX: undefined,\n      CASESENSITIVE: undefined,\n      UNF: true,\n      NOSTEM: undefined,\n    },\n    {\n      identifier: 'rating',\n      attribute: 'rating',\n      type: 'NUMERIC',\n      SORTABLE: true,\n      NOINDEX: undefined,\n      CASESENSITIVE: undefined,\n      UNF: true,\n      NOSTEM: undefined,\n    },\n    {\n      identifier: 'genre',\n      attribute: 'genre',\n      type: 'TAG',\n      SEPARATOR: ',',\n      SORTABLE: true,\n      NOINDEX: undefined,\n      CASESENSITIVE: undefined,\n      UNF: undefined,\n      NOSTEM: undefined,\n    },\n  ],\n  num_docs: '2',\n  max_doc_id: '2',\n  num_terms: '13',\n  num_records: '19',\n  inverted_sz_mb: '0.0016384124755859375',\n  vector_index_sz_mb: '0',\n  total_inverted_index_blocks: '17',\n  offset_vectors_sz_mb: '1.239776611328125e-5',\n  doc_table_size_mb: '1.468658447265625e-4',\n  sortable_values_size_mb: '2.498626708984375e-4',\n  tag_overhead_sz_mb: '5.53131103515625e-5',\n  text_overhead_sz_mb: '4.3392181396484375e-4',\n  total_index_memory_sz_mb: '0.0026903152465820313',\n\n  key_table_size_mb: '8.296966552734375e-5',\n  geoshapes_sz_mb: '0',\n  records_per_doc_avg: '9.5',\n  bytes_per_record_avg: '90.42105102539063',\n  offsets_per_term_avg: '0.6842105388641357',\n  offset_bits_per_record_avg: '8',\n  hash_indexing_failures: '0',\n  total_indexing_time: '0.890999972820282',\n  indexing: '0',\n  percent_indexed: '1',\n  number_of_uses: 17,\n  cleaning: 0,\n  gc_stats: {\n    bytes_collected: '0',\n    total_ms_run: '0',\n    total_cycles: '0',\n    average_cycle_time_ms: 'nan',\n    last_run_time_ms: '0',\n    gc_numeric_trees_missed: '0',\n    gc_blocks_denied: '0',\n  },\n  cursor_stats: {\n    global_idle: 0,\n    global_total: 0,\n    index_capacity: 128,\n    index_total: 0,\n  },\n  dialect_stats: {\n    dialect_1: 1,\n    dialect_2: 0,\n    dialect_3: 0,\n    dialect_4: 0,\n  },\n  'Index Errors': {\n    'indexing failures': 0,\n    'last indexing error': 'N/A',\n    'last indexing error key': 'N/A',\n  },\n  'field statistics': [\n    {\n      identifier: 'title',\n      attribute: 'title',\n      'Index Errors': {\n        'indexing failures': 0,\n        'last indexing error': 'N/A',\n        'last indexing error key': 'N/A',\n      },\n    },\n    {\n      identifier: 'release_year',\n      attribute: 'release_year',\n      'Index Errors': {\n        'indexing failures': 0,\n        'last indexing error': 'N/A',\n        'last indexing error key': 'N/A',\n      },\n    },\n    {\n      identifier: 'rating',\n      attribute: 'rating',\n      'Index Errors': {\n        'indexing failures': 0,\n        'last indexing error': 'N/A',\n        'last indexing error key': 'N/A',\n      },\n    },\n    {\n      identifier: 'genre',\n      attribute: 'genre',\n      'Index Errors': {\n        'indexing failures': 0,\n        'last indexing error': 'N/A',\n        'last indexing error key': 'N/A',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/server.ts",
    "content": "import { AppType, PackageType, Server } from 'src/modules/server/models/server';\nimport { ServerEntity } from 'src/modules/server/entities/server.entity';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport config, { Config } from 'src/utils/config';\nimport { GetServerInfoResponse } from 'src/modules/server/dto/server.dto';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\nexport const mockServerId = 'a77b23c1-7816-4ea4-b61f-d37a0f805ser';\n\nexport const mockServer = Object.assign(new Server(), {\n  id: mockServerId,\n  createDateTime: new Date(),\n});\n\nexport const mockServerEntity = Object.assign(new ServerEntity(), {\n  id: mockServerId,\n  createDateTime: mockServer.createDateTime,\n});\n\nexport const mockGetServerInfoResponse = Object.assign(\n  new GetServerInfoResponse(),\n  {\n    ...mockServer,\n    appVersion: SERVER_CONFIG.appVersion,\n    osPlatform: process.platform,\n    buildType: SERVER_CONFIG.buildType,\n    appType: AppType.Docker,\n    encryptionStrategies: [EncryptionStrategy.PLAIN, EncryptionStrategy.KEYTAR],\n    packageType: PackageType.Unknown,\n  },\n);\n\nexport const mockServerRepository = jest.fn(() => ({\n  exists: jest.fn().mockResolvedValue(true),\n  getOrCreate: jest.fn().mockResolvedValue(mockServer),\n}));\n\nexport const mockServerService = jest.fn(() => ({\n  getInfo: jest.fn().mockResolvedValue(mockGetServerInfoResponse),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/session.ts",
    "content": "import { mockUserId } from 'src/__mocks__/user';\n\nexport const mockSessionId = '1';\n\nexport const mockInitSession = {\n  id: mockSessionId,\n  userId: mockUserId,\n  data: {},\n};\n\nexport const mockSessionCustomData = {\n  custom: {\n    field: 1,\n    nested: {\n      name: 'object0',\n      value: true,\n    },\n    arrayField: [\n      {\n        name: 'object1',\n      },\n      {\n        name: 'object2',\n        array: ['some', 'array'],\n      },\n    ],\n  },\n};\n\nexport const mockSessionStorage = jest.fn(() => ({\n  createSession: jest.fn().mockResolvedValue(mockInitSession),\n  getSession: jest.fn().mockResolvedValue(mockInitSession),\n  updateSessionData: jest.fn().mockResolvedValue(mockInitSession),\n  deleteSession: jest.fn(),\n}));\nexport const mockSessionProvider = mockSessionStorage;\nexport const mockSessionService = mockSessionStorage;\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/ssh.ts",
    "content": "import { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport { SshTunnel } from 'src/modules/ssh/models/ssh-tunnel';\nimport { Server } from 'net';\nimport { Client } from 'ssh2';\n\nexport const mockSshOptionsId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ssh-id';\n\nexport const mockSshOptionsUsernamePlain = 'ssh-username';\nexport const mockSshOptionsUsernameEncrypted = 'ssh.username.ENCRYPTED';\nexport const mockSshOptionsPasswordPlain = 'ssh-password';\nexport const mockSshOptionsPasswordEncrypted = 'ssh.password.ENCRYPTED';\nexport const mockSshOptionsPrivateKeyPlain =\n  '-----BEGIN OPENSSH PRIVATE KEY-----\\nssh-private-key';\nexport const mockSshOptionsPrivateKeyEncrypted = 'ssh.privateKey.ENCRYPTED';\nexport const mockSshOptionsPassphrasePlain = 'ssh-passphrase';\nexport const mockSshOptionsPassphraseEncrypted = 'ssh.passphrase.ENCRYPTED';\n\nexport const mockSshOptionsBasic = Object.assign(new SshOptions(), {\n  id: mockSshOptionsId,\n  host: 'ssh.host.test',\n  port: 22,\n  username: mockSshOptionsUsernamePlain,\n  password: mockSshOptionsPasswordPlain,\n  privateKey: undefined,\n  passphrase: undefined,\n});\n\nexport const mockSshOptionsBasicEntity = Object.assign(new SshOptionsEntity(), {\n  ...mockSshOptionsBasic,\n  username: mockSshOptionsUsernameEncrypted,\n  password: mockSshOptionsPasswordEncrypted,\n  encryption: EncryptionStrategy.KEYTAR,\n});\n\nexport const mockSshOptionsPrivateKey = Object.assign(new SshOptions(), {\n  ...mockSshOptionsBasic,\n  password: undefined,\n  privateKey: mockSshOptionsPrivateKeyPlain,\n  passphrase: mockSshOptionsPassphrasePlain,\n});\n\nexport const mockSshOptionsPrivateKeyEntity = Object.assign(\n  new SshOptionsEntity(),\n  {\n    ...mockSshOptionsBasicEntity,\n    password: null,\n    privateKey: mockSshOptionsPrivateKeyEncrypted,\n    passphrase: mockSshOptionsPassphraseEncrypted,\n  },\n);\n\nexport const mockSshTunnelClient = jest.fn(() => ({\n  on: jest.fn(),\n}));\nexport const mockSshTunnelServer = jest.fn(() => ({\n  address: jest.fn().mockReturnValue({\n    address: mockSshOptionsBasic.host,\n    port: mockSshOptionsBasic.port,\n  }),\n  on: jest.fn(),\n}));\nexport const mockSshTunnel = new SshTunnel(\n  mockSshTunnelServer() as unknown as Server,\n  mockSshTunnelClient() as unknown as Client,\n  {\n    targetHost: mockSshOptionsBasic.host,\n    targetPort: mockSshOptionsBasic.port,\n  },\n);\n\nexport const mockSshTunnelProvider = jest.fn(() => ({\n  createTunnel: jest.fn().mockResolvedValue(mockSshTunnel),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/tags.ts",
    "content": "import { CreateTagDto, UpdateTagDto } from 'src/modules/tag/dto';\nimport { Tag } from 'src/modules/tag/models/tag';\nimport { TagRepository } from 'src/modules/tag/repository/tag.repository';\n\nexport const mockTags: Tag[] = [\n  Object.assign(new Tag(), {\n    id: '1',\n    key: 'environment',\n    value: 'production',\n    createdAt: new Date('2025-03-05T08:54:53.322Z'),\n    updatedAt: new Date('2025-03-05T08:54:53.322Z'),\n  }),\n  Object.assign(new Tag(), {\n    id: '2',\n    key: 'environment',\n    value: 'staging',\n    createdAt: new Date('2025-03-05T08:54:53.322Z'),\n    updatedAt: new Date('2025-03-05T08:54:53.322Z'),\n  }),\n  Object.assign(new Tag(), {\n    id: '3',\n    key: 'size',\n    value: 'large',\n    createdAt: new Date('2025-03-05T08:54:53.322Z'),\n    updatedAt: new Date('2025-03-05T08:54:53.322Z'),\n  }),\n  Object.assign(new Tag(), {\n    id: '4',\n    key: 'size',\n    value: 'small',\n    createdAt: new Date('2025-03-05T08:54:53.322Z'),\n    updatedAt: new Date('2025-03-05T08:54:53.322Z'),\n  }),\n];\n\nexport const createTagDto: CreateTagDto = { key: 'key1', value: 'value1' };\nexport const updateTagDto: UpdateTagDto = {\n  key: 'updatedKey',\n  value: 'updatedValue',\n};\n\nexport const mockTagsService = jest.fn(() => ({\n  create: jest.fn().mockResolvedValue(mockTags[0]),\n  list: jest.fn().mockResolvedValue(mockTags),\n  get: jest.fn().mockResolvedValue(mockTags[0]),\n  getByKeyValuePair: jest.fn().mockResolvedValue(mockTags[0]),\n  getOrCreateByKeyValuePairs: jest\n    .fn()\n    .mockResolvedValue([mockTags[0], mockTags[1]]),\n  update: jest.fn().mockResolvedValue(mockTags[0]),\n  delete: jest.fn().mockResolvedValue(undefined),\n  cleanupUnusedTags: jest.fn(),\n}));\n\nexport const mockTagsRepository = jest.fn(\n  () =>\n    ({\n      create: jest.fn().mockResolvedValue(mockTags[0]),\n      list: jest.fn().mockResolvedValue(mockTags),\n      get: jest.fn().mockResolvedValue(mockTags[0]),\n      getByKeyValuePair: jest.fn().mockResolvedValue(mockTags[0]),\n      update: jest.fn(),\n      delete: jest.fn(),\n      encryptTagEntities: jest.fn().mockImplementation(async (tags) => tags),\n      decryptTagEntities: jest.fn().mockImplementation(async (tags) => tags),\n      getOrCreateByKeyValuePairs: jest\n        .fn()\n        .mockImplementation(async (tags) => tags),\n      cleanupUnusedTags: jest.fn(),\n    }) as TagRepository,\n);\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/triggered-functions.ts",
    "content": "export const mockCommonLibraryReply = [\n  'api_version',\n  '1.0',\n  'engine',\n  'js',\n  'configuration',\n  null,\n  'name',\n  'libraryName',\n  'pending_jobs',\n  0,\n  'user',\n  'default',\n];\n\nexport const mockSimpleLibraryReply = [\n  'api_version',\n  '1.0',\n  'engine',\n  'js',\n  'configuration',\n  null,\n  'functions',\n  ['foo'],\n  'keyspace_triggers',\n  ['keyspace'],\n  'cluster_functions',\n  ['cluster'],\n  'stream_triggers',\n  ['stream'],\n  'name',\n  'libraryName',\n  'pending_jobs',\n  0,\n  'user',\n  'default',\n];\n\nexport const mockVerboseLibraryReply = [\n  'api_version',\n  '1.0',\n  'engine',\n  'js',\n  'configuration',\n  null,\n  'name',\n  'libraryName',\n  'pending_jobs',\n  0,\n  'user',\n  'default',\n  'functions',\n  [\n    [\n      'name',\n      'function',\n      'description',\n      'description',\n      'is_async',\n      1,\n      'flags',\n      ['flag1'],\n    ],\n  ],\n  'keyspace_triggers',\n  [],\n  'cluster_functions',\n  ['foo', 'bar'],\n  'stream_triggers',\n  [\n    [\n      'name',\n      'stream',\n      'description',\n      'description',\n      'prefix',\n      'prefix',\n      'trim',\n      0,\n      'window',\n      1,\n      'streams',\n      [['key', 'value']],\n    ],\n  ],\n];\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/user.ts",
    "content": "export const mockUserId = '1';\nexport const mockAccountId = '1';\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/utm.ts",
    "content": "import { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport { mockGetServerInfoResponse } from 'src/__mocks__/server';\n\nexport const mockCloudRequestUtm = Object.assign(new CloudRequestUtm(), {\n  source: 'utm_source',\n  medium: 'utm_medium',\n  campaign: 'utm_campaign',\n});\n\nexport const mockCalculatedUtmParameters = {\n  amp: mockGetServerInfoResponse.id,\n  package: mockGetServerInfoResponse.packageType,\n};\n\nexport const mockCompleteCloudUtm = Object.assign(new CloudRequestUtm(), {\n  ...mockCloudRequestUtm,\n  ...mockCalculatedUtmParameters,\n});\n\nexport const mockUtmBody = {\n  utm_source: mockCloudRequestUtm.source,\n  utm_medium: mockCloudRequestUtm.medium,\n  utm_campaign: mockCloudRequestUtm.campaign,\n};\n\nexport const mockUtmCompleteBody = {\n  ...mockUtmBody,\n  utm_amp: mockCompleteCloudUtm.amp,\n  utm_package: mockCompleteCloudUtm.package,\n};\n"
  },
  {
    "path": "redisinsight/api/src/__mocks__/workbench.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport {\n  CommandExecution,\n  CommandExecutionType,\n  ResultsMode,\n  RunQueryMode,\n} from 'src/modules/workbench/models/command-execution';\nimport { CommandExecutionEntity } from 'src/modules/workbench/entities/command-execution.entity';\nimport { mockDatabase } from 'src/__mocks__/databases';\nimport { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto';\nimport { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution';\nimport { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { PluginCommandExecution } from 'src/modules/workbench/models/plugin-command-execution';\n\nexport const mockCommandExecutionUnsupportedCommandResult = Object.assign(\n  new CommandExecutionResult(),\n  {\n    response: ERROR_MESSAGES.PLUGIN_COMMAND_NOT_SUPPORTED(\n      'subscribe'.toUpperCase(),\n    ),\n    status: CommandExecutionStatus.Fail,\n  },\n);\n\nexport const mockCommandExecutionSuccessResult = Object.assign(\n  new CommandExecutionResult(),\n  {\n    status: CommandExecutionStatus.Success,\n    response: 'bar',\n  },\n);\n\nexport const mockCommendExecutionHugeResultPlaceholder = Object.assign(\n  new CommandExecutionResult(),\n  {\n    status: CommandExecutionStatus.Success,\n    response:\n      'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.',\n    sizeLimitExceeded: true,\n  },\n);\n\nexport const mockCommendExecutionHugeResultPlaceholderEncrypted =\n  'huge_result_placeholder_encrypted';\n\nexport const mockCommandExecution = Object.assign(new CommandExecution(), {\n  id: uuidv4(),\n  databaseId: mockDatabase.id,\n  command: 'get foo',\n  mode: RunQueryMode.ASCII,\n  resultsMode: ResultsMode.Default,\n  type: CommandExecutionType.Workbench,\n  result: [mockCommandExecutionSuccessResult],\n  createdAt: new Date(),\n  db: 0,\n});\n\nexport const mockCommandExecutionEntity = Object.assign(\n  new CommandExecutionEntity(),\n  {\n    ...mockCommandExecution,\n    command: 'encrypted_command',\n    result: `${JSON.stringify([mockCommandExecutionSuccessResult])}_encrypted`,\n    encryption: 'KEYTAR',\n  },\n);\n\nexport const mockShortCommandExecution = Object.assign(\n  new ShortCommandExecution(),\n  {\n    id: mockCommandExecution.id,\n    databaseId: mockCommandExecution.id,\n    command: mockCommandExecution.command,\n    createdAt: mockCommandExecution.createdAt,\n    mode: mockCommandExecution.mode,\n    summary: mockCommandExecution.summary,\n    resultsMode: mockCommandExecution.resultsMode,\n    executionTime: mockCommandExecution.executionTime,\n    db: mockCommandExecution.db,\n    type: mockCommandExecution.type,\n  },\n);\n\nexport const mockShortCommandExecutionEntity = Object.assign(\n  new CommandExecutionEntity(),\n  {\n    ...mockShortCommandExecution,\n    command: mockCommandExecutionEntity.command,\n    encryption: mockCommandExecutionEntity.encryption,\n  },\n);\n\nexport const mockCreateCommandExecutionDto = Object.assign(\n  new CreateCommandExecutionDto(),\n  {\n    command: mockCommandExecution.command,\n    mode: mockCommandExecution.mode,\n    resultsMode: mockCommandExecution.resultsMode,\n    type: mockCommandExecution.type,\n  },\n);\n\nexport const mockCommandExecutionFilter = Object.assign(\n  new CommandExecutionFilter(),\n  {\n    type: mockCommandExecution.type,\n  },\n);\n\nexport const mockPluginCommandExecution = Object.assign(\n  new PluginCommandExecution(),\n  {\n    ...mockCreateCommandExecutionDto,\n    databaseId: mockDatabase.id,\n    result: [mockCommandExecutionSuccessResult],\n  },\n);\n\nexport const mockWorkbenchCommandsExecutor = () => ({\n  sendCommand: jest.fn().mockResolvedValue([mockCommandExecutionSuccessResult]),\n});\n\nexport const mockCommandExecutionRepository = () => ({\n  createMany: jest.fn().mockResolvedValue([mockCommandExecution]),\n  getList: jest.fn().mockResolvedValue([mockCommandExecution]),\n  getOne: jest.fn().mockResolvedValue(mockCommandExecution),\n  delete: jest.fn(),\n  deleteAll: jest.fn(),\n});\n"
  },
  {
    "path": "redisinsight/api/src/app.module.ts",
    "content": "import * as fs from 'fs';\nimport {\n  MiddlewareConsumer,\n  Module,\n  NestModule,\n  OnModuleInit,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport config, { Config } from 'src/utils/config';\nimport { PluginModule } from 'src/modules/plugin/plugin.module';\nimport { CommandsModule } from 'src/modules/commands/commands.module';\nimport { WorkbenchModule } from 'src/modules/workbench/workbench.module';\nimport { SlowLogModule } from 'src/modules/slow-log/slow-log.module';\nimport { PubSubModule } from 'src/modules/pub-sub/pub-sub.module';\nimport { NotificationModule } from 'src/modules/notification/notification.module';\nimport { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module';\nimport { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module';\nimport { DatabaseAnalysisModule } from 'src/modules/database-analysis/database-analysis.module';\nimport { LocalDatabaseModule } from 'src/local-database.module';\nimport { CoreModule } from 'src/core.module';\nimport { DatabaseImportModule } from 'src/modules/database-import/database-import.module';\nimport { SingleUserAuthMiddleware } from 'src/common/middlewares/single-user-auth.middleware';\nimport { CustomTutorialModule } from 'src/modules/custom-tutorial/custom-tutorial.module';\nimport { CloudModule } from 'src/modules/cloud/cloud.module';\nimport { AzureModule } from 'src/modules/azure/azure.module';\nimport { RdiModule } from 'src/modules/rdi/rdi.module';\nimport { AiChatModule } from 'src/modules/ai/chat/ai-chat.module';\nimport { AiQueryModule } from 'src/modules/ai/query/ai-query.module';\nimport { InitModule } from 'src/modules/init/init.module';\nimport { AnalyticsModule } from 'src/modules/analytics/analytics.module';\nimport { BrowserModule } from './modules/browser/browser.module';\nimport { RedisEnterpriseModule } from './modules/redis-enterprise/redis-enterprise.module';\nimport { RedisSentinelModule } from './modules/redis-sentinel/redis-sentinel.module';\nimport { ProfilerModule } from './modules/profiler/profiler.module';\nimport { CliModule } from './modules/cli/cli.module';\nimport { StaticsManagementModule } from './modules/statics-management/statics-management.module';\nimport { ExcludeRouteMiddleware } from './middleware/exclude-route.middleware';\nimport SubpathProxyMiddleware from './middleware/subpath-proxy.middleware';\nimport XFrameOptionsMiddleware from './middleware/x-frame-options.middleware';\nimport { routes } from './app.routes';\nimport {\n  RedisConnectionMiddleware,\n  redisConnectionControllers,\n} from './middleware/redis-connection';\nimport { DatabaseSettingsModule } from './modules/database-settings/database-settings.module';\nimport { CredentialsModule } from './modules/database/credentials/credentials.module';\nimport { QueryLibraryModule } from './modules/query-library/query-library.module';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\nconst PATH_CONFIG = config.get('dir_path') as Config['dir_path'];\nconst STATICS_CONFIG = config.get('statics') as Config['statics'];\n\n@Module({\n  imports: [\n    LocalDatabaseModule,\n    CoreModule,\n    RouterModule.register(routes),\n    RedisEnterpriseModule,\n    CloudModule.register(),\n    RedisSentinelModule,\n    BrowserModule.register(),\n    CliModule,\n    WorkbenchModule.register(),\n    PluginModule,\n    CommandsModule,\n    ProfilerModule,\n    PubSubModule,\n    SlowLogModule,\n    NotificationModule.register(),\n    BulkActionsModule,\n    ClusterMonitorModule,\n    CustomTutorialModule.register(),\n    DatabaseAnalysisModule,\n    DatabaseImportModule,\n    CloudModule.register(),\n    AzureModule,\n    AiChatModule,\n    AiQueryModule.register(),\n    RdiModule.register(),\n    StaticsManagementModule.register({\n      initDefaults: STATICS_CONFIG.initDefaults,\n      autoUpdate: STATICS_CONFIG.autoUpdate,\n    }),\n    InitModule.register([AnalyticsModule]),\n    DatabaseSettingsModule.register(),\n    CredentialsModule.register(),\n    QueryLibraryModule.register(),\n  ],\n  controllers: [],\n  providers: [],\n})\nexport class AppModule implements OnModuleInit, NestModule {\n  onModuleInit() {\n    // creating required folders\n    const foldersToCreate = [\n      PATH_CONFIG.pluginsAssets,\n      PATH_CONFIG.customPlugins,\n    ];\n\n    foldersToCreate.forEach((folder) => {\n      if (!fs.existsSync(folder)) {\n        fs.mkdirSync(folder, { recursive: true });\n      }\n    });\n  }\n\n  configure(consumer: MiddlewareConsumer) {\n    consumer\n      .apply(SubpathProxyMiddleware, XFrameOptionsMiddleware)\n      .forRoutes('*');\n\n    consumer\n      .apply(SingleUserAuthMiddleware)\n      .exclude(...SERVER_CONFIG.excludeAuthRoutes)\n      .forRoutes('*');\n\n    consumer\n      .apply(ExcludeRouteMiddleware)\n      .forRoutes(...SERVER_CONFIG.excludeRoutes);\n\n    consumer\n      .apply(RedisConnectionMiddleware)\n      .forRoutes(...redisConnectionControllers);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/app.routes.ts",
    "content": "import { CliModule } from 'src/modules/cli/cli.module';\nimport { WorkbenchModule } from 'src/modules/workbench/workbench.module';\nimport { SlowLogModule } from 'src/modules/slow-log/slow-log.module';\nimport { PubSubModule } from 'src/modules/pub-sub/pub-sub.module';\nimport { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module';\nimport { DatabaseAnalysisModule } from 'src/modules/database-analysis/database-analysis.module';\nimport { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module';\nimport { DatabaseRecommendationModule } from 'src/modules/database-recommendation/database-recommendation.module';\nimport { QueryLibraryModule } from 'src/modules/query-library/query-library.module';\n\nexport const routes = [\n  {\n    path: '/databases',\n    children: [\n      {\n        path: '/:dbInstance',\n        module: CliModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: WorkbenchModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: SlowLogModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: PubSubModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: ClusterMonitorModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: DatabaseAnalysisModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: BulkActionsModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: DatabaseRecommendationModule,\n      },\n      {\n        path: '/:dbInstance',\n        module: QueryLibraryModule,\n      },\n    ],\n  },\n];\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/api.ts",
    "content": "export const API_PARAM_DATABASE_ID = 'dbInstance';\nexport const API_HEADER_DATABASE_INDEX = 'ri-db-index';\nexport const API_HEADER_WINDOW_ID = 'x-window-id';\nexport const API_PARAM_CLI_CLIENT_ID = 'uuid';\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/general.ts",
    "content": "export enum TransformGroup {\n  Secure = 'security',\n}\n\nexport const UNKNOWN_REDIS_INFO = {\n  server: {\n    redis_version: 'unknown',\n    redis_mode: 'standalone',\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/history.ts",
    "content": "export enum BrowserHistoryMode {\n  Pattern = 'pattern',\n  Redisearch = 'redisearch',\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/index.ts",
    "content": "export * from './redis-string';\nexport * from './api';\nexport * from './history';\nexport * from './recommendations';\nexport * from './user';\nexport * from './general';\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/recommendations.ts",
    "content": "export enum SearchVisualizationCommands {\n  FT_INFO = 'FT.INFO',\n  FT_SEARCH = 'FT.SEARCH',\n  FT_AGGREGATE = 'FT.AGGREGATE',\n  FT_PROFILE = 'FT.PROFILE',\n  FT_EXPLAIN = 'FT.EXPLAIN',\n  TS_RANGE = 'TS.RANGE',\n  TS_MRANGE = 'TS.MRANGE',\n}\n\nexport const LUA_SCRIPT_RECOMMENDATION_COUNT = 10;\nexport const BIG_HASHES_RECOMMENDATION_LENGTH = 5000;\nexport const COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_MEMORY = 200;\nexport const USE_SMALLER_KEYS_RECOMMENDATION_TOTAL = 1_000_000;\nexport const COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION_LENGTH = 1000;\nexport const COMPRESSION_FOR_LIST_RECOMMENDATION_LENGTH = 1000;\nexport const BIG_SETS_RECOMMENDATION_LENGTH = 1_000;\nexport const BIG_AMOUNT_OF_CONNECTED_CLIENTS_RECOMMENDATION_CLIENTS = 100;\nexport const BIG_STRINGS_RECOMMENDATION_MEMORY = 100_000;\nexport const SEARCH_INDEXES_RECOMMENDATION_KEYS_FOR_CHECK = 100;\nexport const REDIS_VERSION_RECOMMENDATION_VERSION = '7.3';\nexport const COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_KEYS_COUNT = 10;\nexport const SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK = 50;\nexport const SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH = 2;\nexport const RTS_KEYS_FOR_CHECK = 100;\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/redis-string.ts",
    "content": "import { TransformOptions } from 'class-transformer';\n\nexport const REDIS_STRING_ENCODING_QUERY_PARAM_NAME = 'encoding';\n\nexport enum RedisStringResponseEncoding {\n  UTF8 = 'utf8',\n  ASCII = 'ascii',\n  Buffer = 'buffer',\n}\n\nexport type RedisString = string | Buffer;\n\nexport interface RedisStringTransformOptions extends TransformOptions {\n  each: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/constants/user.ts",
    "content": "export const DEFAULT_USER_ID = '1';\nexport const DEFAULT_ACCOUNT_ID = '1';\nexport const DEFAULT_SESSION_ID = '1';\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts",
    "content": "import {\n  BadRequestException,\n  createParamDecorator,\n  ExecutionContext,\n} from '@nestjs/common';\nimport { plainToInstance } from 'class-transformer';\nimport { ClientContext, ClientMetadata } from 'src/common/models';\nimport { Validator } from 'class-validator';\nimport {\n  API_HEADER_DATABASE_INDEX,\n  API_PARAM_DATABASE_ID,\n} from 'src/common/constants';\nimport { ApiHeader, ApiParam } from '@nestjs/swagger';\nimport { sessionMetadataFromRequestExecutionContext } from 'src/common/decorators/session/session-metadata.decorator';\n\nconst validator = new Validator();\n\nexport interface IClientMetadataParamOptions {\n  databaseIdParam?: string;\n  uniqueIdParam?: string;\n  context?: ClientContext;\n  ignoreDbIndex?: boolean;\n}\n\nexport const clientMetadataParamFactory = (\n  options: IClientMetadataParamOptions,\n  ctx: ExecutionContext,\n): ClientMetadata => {\n  const req = ctx.switchToHttp().getRequest();\n\n  let databaseId;\n  if (options?.databaseIdParam) {\n    databaseId = req.params?.[options.databaseIdParam];\n  }\n\n  let uniqueId;\n  if (options?.uniqueIdParam) {\n    uniqueId = req.params?.[options.uniqueIdParam];\n  }\n\n  const clientMetadata = plainToInstance(\n    ClientMetadata,\n    {\n      sessionMetadata: sessionMetadataFromRequestExecutionContext(\n        undefined,\n        ctx,\n      ),\n      databaseId,\n      uniqueId,\n      context: options?.context || ClientContext.Common,\n      db: options?.ignoreDbIndex\n        ? undefined\n        : req?.headers?.[API_HEADER_DATABASE_INDEX],\n    },\n    {\n      groups: ['security'],\n    },\n  );\n\n  const errors = validator.validateSync(clientMetadata, {\n    whitelist: false, // we need this to allow additional fields if needed for flexibility\n  });\n\n  if (errors?.length) {\n    throw new BadRequestException(\n      Object.values(errors[0].constraints) || 'Bad request',\n    );\n  }\n\n  return clientMetadata;\n};\n\nexport const ClientMetadataParam = (options?: IClientMetadataParamOptions) => {\n  const opts: IClientMetadataParamOptions = {\n    context: ClientContext.Common,\n    databaseIdParam: API_PARAM_DATABASE_ID,\n    ignoreDbIndex: false,\n    ...options,\n  };\n\n  return createParamDecorator(clientMetadataParamFactory, [\n    (target: any, key: string) => {\n      // Here it is. Use the `@ApiQuery` decorator purely as a function to define the meta only once here.\n      ApiParam({\n        name: opts.databaseIdParam,\n        schema: { type: 'string' },\n        required: true,\n      })(target, key, Object.getOwnPropertyDescriptor(target, key));\n      if (!opts.ignoreDbIndex) {\n        ApiHeader({\n          name: API_HEADER_DATABASE_INDEX,\n          schema: {\n            default: undefined,\n            type: 'number',\n            minimum: 0,\n          },\n          required: false,\n        })(target, key, Object.getOwnPropertyDescriptor(target, key));\n      }\n    },\n  ])(opts);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/client-metadata/index.ts",
    "content": "export * from './client-metadata.decorator';\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/data-as-json-string.decorator.ts",
    "content": "import { applyDecorators } from '@nestjs/common';\nimport { Transform } from 'class-transformer';\n\nexport function DataAsJsonString() {\n  return applyDecorators(\n    Transform(({ value }) => JSON.stringify(value), { toClassOnly: true }),\n    Transform(\n      ({ value }) => {\n        try {\n          return JSON.parse(value);\n        } catch (e) {\n          return undefined;\n        }\n      },\n      { toPlainOnly: true },\n    ),\n  );\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/database-management.decorator.ts",
    "content": "import { applyDecorators, UseGuards } from '@nestjs/common';\nimport { DatabaseManagementGuard } from 'src/common/guards/database-management.guard';\n\nexport function DatabaseManagement() {\n  return applyDecorators(UseGuards(new DatabaseManagementGuard()));\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/default.ts",
    "content": "import { Transform } from 'class-transformer';\nimport { cloneDeep } from 'lodash';\n\nexport function Default(defaultValue: unknown): PropertyDecorator {\n  return Transform(({ value }) => value ?? cloneDeep(defaultValue));\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/hidden-field.decorator.ts",
    "content": "import { Transform } from 'class-transformer';\n\nexport function HiddenField(field: any): PropertyDecorator {\n  return Transform(({ value }) => (value ? field : undefined), {\n    toPlainOnly: true,\n  });\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/index.ts",
    "content": "export * from './redis-string';\nexport * from './zset-score';\nexport * from './default';\nexport * from './data-as-json-string.decorator';\nexport * from './session';\nexport * from './client-metadata';\nexport * from './object-as-map.decorator';\nexport * from './is-multi-number.decorator';\nexport * from './is-bigger-than.decorator';\nexport * from './is-github-link.decorator';\nexport * from './database-management.decorator';\nexport * from './no-duplicates.decorator';\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/is-bigger-than.decorator.ts",
    "content": "import { registerDecorator, ValidationOptions } from 'class-validator';\nimport { BiggerThan } from 'src/common/validators/bigger-than.validator';\n\nexport function IsBiggerThan(\n  property: string,\n  validationOptions?: ValidationOptions,\n) {\n  return (object: any, propertyName: string) => {\n    registerDecorator({\n      name: 'IsBiggerThan',\n      target: object.constructor,\n      propertyName,\n      constraints: [property],\n      options: validationOptions,\n      validator: BiggerThan,\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/is-github-link.decorator.ts",
    "content": "import { registerDecorator, ValidationOptions } from 'class-validator';\nimport { GitHubLink } from 'src/common/validators';\n\nexport function IsGitHubLink(validationOptions?: ValidationOptions) {\n  return (object: any, propertyName: string) => {\n    registerDecorator({\n      name: 'IsGitHubLink',\n      target: object.constructor,\n      propertyName,\n      constraints: [],\n      options: validationOptions,\n      validator: GitHubLink,\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/is-multi-number.decorator.ts",
    "content": "import { registerDecorator, ValidationOptions } from 'class-validator';\nimport { MultiNumberValidator } from 'src/common/validators';\n\nexport function IsMultiNumber(validationOptions?: ValidationOptions) {\n  return (object: any, propertyName: string) => {\n    registerDecorator({\n      name: 'IsMultiNumber',\n      target: object.constructor,\n      propertyName,\n      options: validationOptions,\n      validator: MultiNumberValidator,\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/no-duplicates.decorator.ts",
    "content": "import { registerDecorator, ValidationOptions } from 'class-validator';\nimport { NoDuplicates } from 'src/common/validators';\n\nexport function NoDuplicatesByKey(\n  key: string,\n  validationOptions?: ValidationOptions,\n) {\n  return (object: any, propertyName: string) => {\n    registerDecorator({\n      name: 'NoDuplicatesByKey',\n      target: object.constructor,\n      propertyName,\n      constraints: [key],\n      options: validationOptions,\n      validator: NoDuplicates,\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/object-as-map.decorator.ts",
    "content": "import { forEach } from 'lodash';\nimport { applyDecorators } from '@nestjs/common';\nimport { instanceToPlain, plainToInstance, Transform } from 'class-transformer';\nimport { ClassConstructor } from 'class-transformer/types/interfaces';\n\nexport function ObjectAsMap<T>(targetClass: ClassConstructor<T>) {\n  return applyDecorators(\n    Transform(\n      ({ value: object }): Map<string, T> => {\n        const result = new Map();\n\n        try {\n          forEach(object, (value, key) => {\n            result.set(key, plainToInstance(targetClass, value));\n          });\n\n          return result;\n        } catch (e) {\n          return result;\n        }\n      },\n      { toClassOnly: true },\n    ),\n    Transform(\n      ({ value: map }): object => {\n        try {\n          const result = {};\n\n          forEach(Array.from(map), ([key, value]) => {\n            result[key] = instanceToPlain(value);\n          });\n\n          return result;\n        } catch (e) {\n          return undefined;\n        }\n      },\n      { toPlainOnly: true },\n    ),\n  );\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/any-to-redis-string.decorator.ts",
    "content": "import { AnyToRedisStringTransformer } from 'src/common/transformers';\nimport { RedisStringTransformOptions } from 'src/common/constants';\n\nexport const AnyToRedisString = (opts: RedisStringTransformOptions) =>\n  AnyToRedisStringTransformer(opts);\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/api-query-redis-string-encoding.decorator.ts",
    "content": "import { ApiQuery } from '@nestjs/swagger';\nimport {\n  REDIS_STRING_ENCODING_QUERY_PARAM_NAME,\n  RedisStringResponseEncoding,\n} from 'src/common/constants';\n\nexport const ApiQueryRedisStringEncoding = () =>\n  ApiQuery({\n    name: REDIS_STRING_ENCODING_QUERY_PARAM_NAME,\n    enum: RedisStringResponseEncoding,\n  });\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/index.ts",
    "content": "export * from './is-redis-string.decorator';\nexport * from './api-query-redis-string-encoding.decorator';\nexport * from './redis-string-to-ascii.decorator';\nexport * from './redis-string-to-utf8.decorator';\nexport * from './redis-string-to-buffer.decorator';\nexport * from './any-to-redis-string.decorator';\nexport * from './redis-string-type.decorator';\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/is-redis-string.decorator.ts",
    "content": "import { registerDecorator, ValidationOptions } from 'class-validator';\nimport { RedisStringValidator } from 'src/common/validators';\n\nexport function IsRedisString(validationOptions?: ValidationOptions) {\n  return (object: any, propertyName: string) => {\n    registerDecorator({\n      name: 'IsRedisString',\n      target: object.constructor,\n      propertyName,\n      options: validationOptions,\n      validator: RedisStringValidator,\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/redis-string-to-ascii.decorator.ts",
    "content": "import { RedisStringToASCIITransformer } from 'src/common/transformers';\nimport { RedisStringTransformOptions } from 'src/common/constants';\n\nexport const RedisStringToASCII = (opts: RedisStringTransformOptions) =>\n  RedisStringToASCIITransformer(opts);\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/redis-string-to-buffer.decorator.ts",
    "content": "import { RedisStringToBufferTransformer } from 'src/common/transformers';\nimport { RedisStringTransformOptions } from 'src/common/constants';\n\nexport const RedisStringToBuffer = (opts: RedisStringTransformOptions) =>\n  RedisStringToBufferTransformer(opts);\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/redis-string-to-utf8.decorator.ts",
    "content": "import { RedisStringToUTF8Transformer } from 'src/common/transformers';\nimport { RedisStringTransformOptions } from 'src/common/constants';\n\nexport const RedisStringToUTF8 = (opts: RedisStringTransformOptions) =>\n  RedisStringToUTF8Transformer(opts);\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/redis-string/redis-string-type.decorator.ts",
    "content": "import { applyDecorators } from '@nestjs/common';\nimport {\n  AnyToRedisString,\n  RedisStringToASCII,\n  RedisStringToBuffer,\n  RedisStringToUTF8,\n} from 'src/common/decorators';\nimport {\n  RedisStringResponseEncoding,\n  RedisStringTransformOptions,\n} from 'src/common/constants';\n\nexport function RedisStringType(opts?: RedisStringTransformOptions) {\n  return applyDecorators(\n    RedisStringToASCII({\n      groups: [RedisStringResponseEncoding.ASCII],\n      toPlainOnly: true,\n      ...opts,\n    }),\n    RedisStringToUTF8({\n      groups: [RedisStringResponseEncoding.UTF8],\n      toPlainOnly: true,\n      ...opts,\n    }),\n    RedisStringToBuffer({\n      groups: [RedisStringResponseEncoding.Buffer],\n      toPlainOnly: true,\n      ...opts,\n    }),\n    AnyToRedisString({\n      toClassOnly: true,\n      ...opts,\n    }),\n  );\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/session/index.ts",
    "content": "export * from './session-metadata.decorator';\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/session/session-metadata.decorator.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport {\n  BadRequestException,\n  createParamDecorator,\n  ExecutionContext,\n} from '@nestjs/common';\nimport { plainToInstance } from 'class-transformer';\nimport { Validator } from 'class-validator';\nimport { Request } from 'express';\nimport { SessionMetadata } from 'src/common/models';\nimport { omit } from 'lodash';\n\nconst validator = new Validator();\n\nexport const sessionMetadataFromRequest = (\n  request: Request,\n): SessionMetadata => {\n  const userId = request.res?.locals?.session?.data?.userId?.toString();\n  const accountId = request.res?.locals?.session?.data?.accountId?.toString();\n  const sessionId = request.res?.locals?.session?.data?.sessionId?.toString();\n  const data = omit(request.res?.locals?.session?.data, [\n    'userId',\n    'accountId',\n    'sessionId',\n    'correlationId',\n  ]);\n  const correlationId = request.res?.locals?.session?.correlationId || uuidv4();\n  const requestMetadata = request.res?.locals?.session?.requestMetadata;\n\n  const requestSession = {\n    userId,\n    accountId,\n    data,\n    sessionId,\n    correlationId,\n    requestMetadata,\n  };\n\n  const session = plainToInstance(SessionMetadata, requestSession, {\n    groups: ['security'],\n  });\n\n  const errors = validator.validateSync(session, {\n    whitelist: false, // we need this to allow additional fields if needed for flexibility\n  });\n\n  if (errors?.length) {\n    throw new BadRequestException(\n      Object.values(errors[0].constraints) || 'Bad request',\n    );\n  }\n\n  return session;\n};\n\nexport const sessionMetadataFromRequestExecutionContext = (\n  _: unknown,\n  ctx: ExecutionContext,\n): SessionMetadata => {\n  const request = ctx.switchToHttp().getRequest();\n\n  return sessionMetadataFromRequest(request);\n};\n\nexport const RequestSessionMetadata = createParamDecorator(\n  sessionMetadataFromRequestExecutionContext,\n);\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/transform-to-map.decorator.spec.ts",
    "content": "import { Expose, instanceToPlain, plainToInstance } from 'class-transformer';\nimport { TransformToMap } from './transform-to-map.decorator';\n\nclass DummyClass {\n  @Expose()\n  value: string;\n\n  @Expose({ groups: ['security'] })\n  secret?: string;\n}\n\nclass TestDto {\n  @TransformToMap(DummyClass)\n  @Expose()\n  data: Record<string, DummyClass>;\n}\n\ndescribe('TransformToMap decorator', () => {\n  it('should transform each map value into an instance of DummyClass (without security group fields)', () => {\n    const input = {\n      data: {\n        key1: { value: 'test1', secret: 'secret1' },\n        key2: { value: 'test2', secret: 'secret2' },\n      },\n    };\n\n    const instance = plainToInstance(TestDto, input);\n\n    expect(instance).toBeInstanceOf(TestDto);\n    expect(instance.data.key1).toBeInstanceOf(DummyClass);\n    expect(instance.data.key2).toBeInstanceOf(DummyClass);\n    expect(instance.data.key1.value).toEqual('test1');\n    expect(instance.data.key1.secret).toEqual(undefined);\n    expect(instance.data.key2.value).toEqual('test2');\n    expect(instance.data.key2.secret).toEqual(undefined);\n  });\n\n  it('should transform each map value into an instance of DummyClass (with security group fields)', () => {\n    const input = {\n      data: {\n        key1: { value: 'test1', secret: 'secret1' },\n        key2: { value: 'test2', secret: 'secret2' },\n      },\n    };\n\n    const instance = plainToInstance(TestDto, input, { groups: ['security'] });\n\n    expect(instance).toBeInstanceOf(TestDto);\n    expect(instance.data.key1).toBeInstanceOf(DummyClass);\n    expect(instance.data.key2).toBeInstanceOf(DummyClass);\n    expect(instance.data.key1.value).toEqual('test1');\n    expect(instance.data.key1.secret).toEqual('secret1');\n    expect(instance.data.key2.value).toEqual('test2');\n    expect(instance.data.key2.secret).toEqual('secret2');\n  });\n\n  it('should handle empty objects gracefully', () => {\n    const input = { data: {} };\n    const instance = plainToInstance(TestDto, input);\n\n    expect(instance).toBeInstanceOf(TestDto);\n    expect(instance.data).toEqual({});\n  });\n\n  it('should handle undefined values gracefully', () => {\n    const input = { data: undefined };\n    const instance = plainToInstance(TestDto, input);\n\n    expect(instance).toBeInstanceOf(TestDto);\n    expect(instance.data).toEqual(undefined);\n  });\n\n  it('should convert a class instance to a plain object (without security group fields)', () => {\n    const instance = Object.assign(new TestDto(), {\n      data: {\n        key1: Object.assign(new DummyClass(), {\n          value: 'test1',\n          secret: 'secret1',\n        }),\n        key2: Object.assign(new DummyClass(), {\n          value: 'test2',\n          secret: 'secret2',\n        }),\n      },\n    });\n\n    const plain = instanceToPlain(instance);\n\n    expect(Object.getPrototypeOf(plain)).toBe(Object.prototype);\n    expect(Object.getPrototypeOf(plain.data.key1)).toBe(Object.prototype);\n    expect(Object.getPrototypeOf(plain.data.key2)).toBe(Object.prototype);\n    expect(plain).toEqual({\n      data: {\n        key1: { value: 'test1' },\n        key2: { value: 'test2' },\n      },\n    });\n  });\n\n  it('should convert a class instance to a plain object (with security group fields)', () => {\n    const instance = Object.assign(new TestDto(), {\n      data: {\n        key1: Object.assign(new DummyClass(), {\n          value: 'test1',\n          secret: 'secret1',\n        }),\n        key2: Object.assign(new DummyClass(), {\n          value: 'test2',\n          secret: 'secret2',\n        }),\n      },\n    });\n\n    const plain = instanceToPlain(instance, { groups: ['security'] });\n\n    expect(Object.getPrototypeOf(plain)).toBe(Object.prototype);\n    expect(Object.getPrototypeOf(plain.data.key1)).toBe(Object.prototype);\n    expect(Object.getPrototypeOf(plain.data.key2)).toBe(Object.prototype);\n    expect(plain).toEqual({\n      data: {\n        key1: { value: 'test1', secret: 'secret1' },\n        key2: { value: 'test2', secret: 'secret2' },\n      },\n    });\n  });\n\n  it('should handle an empty Map gracefully on reverse conversion', () => {\n    const instance = Object.assign(new TestDto(), {\n      data: {},\n    });\n\n    const plain = instanceToPlain(instance);\n\n    expect(Object.getPrototypeOf(plain)).toBe(Object.prototype);\n    expect(plain.data).toEqual({});\n  });\n\n  it('should handle undefined values gracefully on reverse conversion', () => {\n    const instance = Object.assign(new TestDto(), {\n      data: undefined,\n    });\n\n    const plain = instanceToPlain(instance);\n\n    expect(Object.getPrototypeOf(plain)).toBe(Object.prototype);\n    expect(plain.data).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/transform-to-map.decorator.ts",
    "content": "import { applyDecorators } from '@nestjs/common';\nimport { Transform, instanceToPlain, plainToInstance } from 'class-transformer';\nimport { ClassConstructor } from 'class-transformer/types/interfaces';\n\nexport function TransformToMap<T>(targetClass: ClassConstructor<T>) {\n  return applyDecorators(\n    Transform(\n      ({ value, options }) => {\n        if (!value) {\n          return value;\n        }\n\n        return Object.fromEntries(\n          Object.entries(value).map(([key, val]) => [\n            key,\n            plainToInstance(targetClass, val, options),\n          ]),\n        );\n      },\n      { toClassOnly: true },\n    ),\n\n    Transform(\n      ({ value, options }) => {\n        if (!value) {\n          return value;\n        }\n\n        return Object.fromEntries(\n          Object.entries(value).map(([key, instance]) => [\n            key,\n            instanceToPlain(instance, options),\n          ]),\n        );\n      },\n      { toPlainOnly: true },\n    ),\n  );\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/zset-score/index.ts",
    "content": "export * from './zset-score.decorator';\n"
  },
  {
    "path": "redisinsight/api/src/common/decorators/zset-score/zset-score.decorator.ts",
    "content": "import { registerDecorator, ValidationOptions } from 'class-validator';\nimport { ZSetScoreValidator } from 'src/common/validators';\n\nexport function isZSetScore(validationOptions?: ValidationOptions) {\n  return (object: any, propertyName: string) => {\n    registerDecorator({\n      name: 'isZSetScore',\n      target: object.constructor,\n      propertyName,\n      options: validationOptions,\n      validator: ZSetScoreValidator,\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/exceptions/index.ts",
    "content": "export * from './validation.exception';\n"
  },
  {
    "path": "redisinsight/api/src/common/exceptions/validation.exception.ts",
    "content": "import { BadRequestException } from '@nestjs/common';\n\nexport class ValidationException extends BadRequestException {}\n"
  },
  {
    "path": "redisinsight/api/src/common/guards/database-management.guard.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { DatabaseManagementGuard } from 'src/common/guards/database-management.guard';\nimport { ForbiddenException } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\ndescribe('DatabaseManagementGuard', () => {\n  let guard: DatabaseManagementGuard;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    configGetSpy = jest.spyOn(config, 'get');\n\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    guard = new DatabaseManagementGuard();\n  });\n\n  it('should return true', () => {\n    mockServerConfig.databaseManagement = true;\n\n    expect(guard.canActivate()).toEqual(true);\n  });\n\n  it('should throw an error when database management is disabled', () => {\n    mockServerConfig.databaseManagement = false;\n\n    expect(guard.canActivate).toThrowError(\n      new ForbiddenException('Database connection management is disabled.'),\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/guards/database-management.guard.ts",
    "content": "import { CanActivate, ForbiddenException, Injectable } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Injectable()\nexport class DatabaseManagementGuard implements CanActivate {\n  canActivate(): boolean {\n    if (!SERVER_CONFIG.databaseManagement) {\n      throw new ForbiddenException(\n        ERROR_MESSAGES.DATABASE_MANAGEMENT_IS_DISABLED,\n      );\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/interceptors/browser-serialize.interceptor.ts",
    "content": "import { map } from 'rxjs/operators';\nimport {\n  CallHandler,\n  ClassSerializerInterceptor,\n  ExecutionContext,\n  PlainLiteralObject,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { RedisStringResponseEncoding } from 'src/common/constants';\n\nexport class BrowserSerializeInterceptor extends ClassSerializerInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    const req = context.switchToHttp().getRequest();\n    const encoding = req?.query?.encoding || RedisStringResponseEncoding.UTF8;\n\n    const contextOptions = this.getContextOptions(context);\n    const options = {\n      ...this.defaultOptions,\n      ...contextOptions,\n    };\n\n    if (options?.groups?.length) {\n      options.groups.push(encoding);\n    } else {\n      options.groups = [encoding];\n    }\n\n    return next\n      .handle()\n      .pipe(\n        map((res: PlainLiteralObject | Array<PlainLiteralObject>) =>\n          this.serialize(res, options),\n        ),\n      );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/interceptors/index.ts",
    "content": "export * from './browser-serialize.interceptor';\nexport * from './timeout.interceptor';\n"
  },
  {
    "path": "redisinsight/api/src/common/interceptors/timeout.interceptor.ts",
    "content": "import {\n  Injectable,\n  NestInterceptor,\n  ExecutionContext,\n  CallHandler,\n  Logger,\n  BadGatewayException,\n} from '@nestjs/common';\nimport { Observable, throwError, TimeoutError } from 'rxjs';\nimport { catchError, timeout } from 'rxjs/operators';\nimport config, { Config } from 'src/utils/config';\n\nconst serverConfig = config.get('server') as Config['server'];\n\n@Injectable()\nexport class TimeoutInterceptor implements NestInterceptor {\n  private logger = new Logger('TimeoutInterceptor');\n\n  private readonly message: string;\n\n  private readonly timeout: number;\n\n  constructor(message: string = 'Request timeout', timeoutMs?: number) {\n    this.message = message;\n    this.timeout = timeoutMs ?? serverConfig.requestTimeout;\n  }\n\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    return next.handle().pipe(\n      timeout(this.timeout),\n      catchError((err) => {\n        if (err instanceof TimeoutError) {\n          const { method, url } = context.switchToHttp().getRequest();\n          this.logger.error(`Request Timeout. ${method} ${url}`);\n          return throwError(() => new BadGatewayException(this.message));\n        }\n        return throwError(() => err);\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/logger/app-logger.spec.ts",
    "content": "import { plainToInstance } from 'class-transformer';\nimport { cloneDeep } from 'lodash';\nimport { WinstonModule } from 'nest-winston';\nimport { AppLogger } from 'src/common/logger/app-logger';\nimport {\n  ClientContext,\n  ClientMetadata,\n  SessionMetadata,\n} from 'src/common/models';\n\nconst loggerConfig = {};\nconst mockWinstonLogger = {\n  log: jest.fn(),\n  verbose: jest.fn(),\n  debug: jest.fn(),\n  warn: jest.fn(),\n  error: jest.fn(),\n  fatal: jest.fn(),\n};\nconst logLevels = Object.keys(mockWinstonLogger);\n\njest.spyOn(WinstonModule, 'createLogger').mockReturnValue(mockWinstonLogger);\n\nconst getSessionMetadata = () =>\n  plainToInstance(\n    SessionMetadata,\n    {\n      userId: '123',\n      sessionId: 'test-session-id',\n      requestMetadata: {\n        any: 'data',\n      },\n    },\n    { groups: ['security'] },\n  );\n\nconst getClientMetadata = () =>\n  plainToInstance(\n    ClientMetadata,\n    {\n      sessionMetadata: getSessionMetadata(),\n      databaseId: 'db-123',\n      context: ClientContext.Browser,\n      uniqueId: 'unique-id',\n      db: 1,\n    },\n    { groups: ['security'] },\n  );\n\ndescribe('AppLogger', () => {\n  let logger: AppLogger;\n\n  beforeEach(() => {\n    logger = new AppLogger(loggerConfig);\n    jest.clearAllMocks();\n  });\n\n  test.each(logLevels)(\n    'should get context from last optional param if it is a string - logger.%s',\n    (level) => {\n      logger[level]('Test message', 'optional arg', 'Test context');\n\n      expect(mockWinstonLogger[level]).toHaveBeenCalledTimes(1);\n      expect(mockWinstonLogger[level]).toHaveBeenCalledWith({\n        message: 'Test message',\n        context: 'Test context',\n        data: ['optional arg'],\n        error: undefined,\n      });\n    },\n  );\n\n  test.each(logLevels)(\n    'should find and separate the first error object if exists - logger.%s',\n    (level) => {\n      const error1 = new Error('Test error 1');\n      const error2 = new Error('Test error 2');\n      logger[level]('Test message', error1, error2);\n\n      expect(mockWinstonLogger[level]).toHaveBeenCalledTimes(1);\n      expect(mockWinstonLogger[level]).toHaveBeenCalledWith({\n        message: 'Test message',\n        context: null,\n        data: [error2],\n        error: error1,\n      });\n    },\n  );\n\n  test.each(logLevels)(\n    'should get error response and include it if exists - logger.%s',\n    (level) => {\n      const error1 = new Error('Test error 1');\n      (error1 as Error & { response: unknown }).response = {\n        status: 500,\n        data: 'Internal server error',\n      };\n      logger[level]('Test message', error1);\n\n      expect(mockWinstonLogger[level]).toHaveBeenCalledTimes(1);\n      expect(mockWinstonLogger[level]).toHaveBeenCalledWith({\n        message: 'Test message',\n        context: null,\n        data: undefined,\n        error: error1,\n      });\n    },\n  );\n\n  test.each(logLevels)(\n    'should parse client metadata from optional params - logger.%s',\n    (level) => {\n      const clientMetadata = getClientMetadata();\n      logger[level](\n        'Test message',\n        clientMetadata,\n        { foo: 'bar' },\n        'Test context',\n      );\n\n      expect(mockWinstonLogger[level]).toHaveBeenCalledTimes(1);\n      expect(mockWinstonLogger[level]).toHaveBeenCalledWith({\n        message: 'Test message',\n        context: 'Test context',\n        clientMetadata: {\n          ...clientMetadata,\n          sessionMetadata: undefined,\n        },\n        sessionMetadata: {\n          ...clientMetadata.sessionMetadata,\n          requestMetadata: undefined,\n        },\n        data: [{ foo: 'bar' }],\n        error: undefined,\n      });\n    },\n  );\n\n  test.each(logLevels)(\n    'should parse session metadata from optional params - logger.%s',\n    (level) => {\n      const sessionMetadata = getSessionMetadata();\n      logger[level](\n        'Test message',\n        sessionMetadata,\n        { foo: 'bar' },\n        'Test context',\n      );\n\n      expect(mockWinstonLogger[level]).toHaveBeenCalledTimes(1);\n      expect(mockWinstonLogger[level]).toHaveBeenCalledWith({\n        message: 'Test message',\n        context: 'Test context',\n        sessionMetadata: {\n          ...sessionMetadata,\n          requestMetadata: undefined,\n        },\n        data: [{ foo: 'bar' }],\n        error: undefined,\n      });\n    },\n  );\n\n  test.each(logLevels)(\n    'should not mutate original arguments - logger.%s',\n    (level) => {\n      const clientMetadata = getClientMetadata();\n\n      const error = new Error('test 123');\n      const optionalParams = [\n        clientMetadata,\n        error,\n        { foo: 'bar' },\n        'Test context',\n      ];\n      const optionalParamsOriginal = cloneDeep(optionalParams);\n\n      logger[level]('Test message', ...optionalParams);\n\n      expect(mockWinstonLogger[level]).toHaveBeenCalledTimes(1);\n      expect(mockWinstonLogger[level]).toHaveBeenCalledWith({\n        message: 'Test message',\n        context: 'Test context',\n        clientMetadata: {\n          ...clientMetadata,\n          sessionMetadata: undefined,\n        },\n        sessionMetadata: {\n          ...clientMetadata.sessionMetadata,\n          requestMetadata: undefined,\n        },\n        data: [{ foo: 'bar' }],\n        error,\n      });\n      expect(optionalParams).toEqual(optionalParamsOriginal);\n    },\n  );\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/logger/app-logger.ts",
    "content": "import { LoggerService, Injectable } from '@nestjs/common';\nimport { WinstonModule, WinstonModuleOptions } from 'nest-winston';\nimport { cloneDeep, isString } from 'lodash';\nimport { ClientMetadata, SessionMetadata } from 'src/common/models';\nimport { instanceToPlain } from 'class-transformer';\nimport { logDataToPlain } from 'src/utils/logsFormatter';\n\ntype LogMeta = object;\n\ntype ErrorOrMeta = Error | LogMeta | string | ClientMetadata | SessionMetadata;\n\n@Injectable()\nexport class AppLogger implements LoggerService {\n  private readonly logger: ReturnType<typeof WinstonModule.createLogger>;\n\n  constructor(loggerConfig: WinstonModuleOptions) {\n    this.logger = WinstonModule.createLogger(loggerConfig);\n  }\n\n  /**\n   * Get context from optional arguments\n   * If the last argument is a string - it will be handled like a context\n   * since nest passes the logger context as the last argument\n   * Note: args array might be mutated\n   * @param args\n   */\n  static getContext(args: ErrorOrMeta[] = []) {\n    const lastArg = args?.[args.length - 1];\n\n    if (isString(lastArg)) {\n      return args.pop() as string;\n    }\n\n    return null;\n  }\n\n  /**\n   * Get an error from the optional arguments\n   * Will find first entry which is error type\n   * Note: args array might be mutated\n   * @param args\n   */\n  static getError(args: ErrorOrMeta[] = []): Error {\n    let error = null;\n    const index = args.findIndex((arg) => arg instanceof Error);\n    if (index > -1) {\n      [error] = args.splice(index, 1);\n    }\n\n    return error || undefined;\n  }\n\n  /**\n   * Get clientMetadata and/or sessionMetadata object(s) from args\n   * Will find first entry of ClientMetadata and get SessionMetadata, from it and return both\n   * otherwise will find SessionMetadata and return only it\n   * otherwise will return empty object\n   * Note: args array might be mutated\n   * @param args\n   */\n  static getUserMetadata(args: ErrorOrMeta[] = []): {\n    clientMetadata?: Partial<ClientMetadata>;\n    sessionMetadata?: SessionMetadata;\n  } {\n    // check for client metadata in args\n    const clientMetadataIndex = args.findIndex(\n      (arg) => arg instanceof ClientMetadata,\n    );\n    if (clientMetadataIndex > -1) {\n      const [clientMetadata] = args.splice(\n        clientMetadataIndex,\n        1,\n      ) as ClientMetadata[];\n      return {\n        clientMetadata: {\n          ...clientMetadata,\n          sessionMetadata: undefined,\n        },\n        sessionMetadata: clientMetadata.sessionMetadata,\n      };\n    }\n\n    // check for session metadata in args\n    const sessionMetadataIndex = args.findIndex(\n      (arg) => arg instanceof SessionMetadata,\n    );\n    if (sessionMetadataIndex > -1) {\n      const [sessionMetadata] = args.splice(\n        sessionMetadataIndex,\n        1,\n      ) as SessionMetadata[];\n      return {\n        sessionMetadata,\n      };\n    }\n\n    // by default will return empty object\n    return {};\n  }\n\n  private parseLoggerArgs(message: string, optionalParams: ErrorOrMeta[] = []) {\n    const optionalParamsCopy = cloneDeep(optionalParams);\n    const context = AppLogger.getContext(optionalParamsCopy);\n    const error = AppLogger.getError(optionalParamsCopy);\n    const userMetadata = AppLogger.getUserMetadata(optionalParamsCopy);\n\n    return {\n      message,\n      context,\n      error,\n      ...instanceToPlain(userMetadata),\n      data: optionalParamsCopy?.length\n        ? logDataToPlain(optionalParamsCopy)\n        : undefined,\n    };\n  }\n\n  /**\n   * Write a 'log' level log.\n   */\n  log(message: string, ...optionalParams: ErrorOrMeta[]) {\n    this.logger.log(this.parseLoggerArgs(message, optionalParams));\n  }\n\n  /**\n   * Write a 'fatal' level log.\n   */\n  fatal(message: string, ...optionalParams: ErrorOrMeta[]) {\n    this.logger.fatal(this.parseLoggerArgs(message, optionalParams));\n  }\n\n  /**\n   * Write an 'error' level log.\n   */\n  error(message: string, ...optionalParams: ErrorOrMeta[]) {\n    this.logger.error(this.parseLoggerArgs(message, optionalParams));\n  }\n\n  /**\n   * Write a 'warn' level log.\n   */\n  warn(message: string, ...optionalParams: ErrorOrMeta[]) {\n    this.logger.warn(this.parseLoggerArgs(message, optionalParams));\n  }\n\n  /**\n   * Write a 'debug' level log.\n   */\n  debug?(message: string, ...optionalParams: ErrorOrMeta[]) {\n    this.logger.debug(this.parseLoggerArgs(message, optionalParams));\n  }\n\n  /**\n   * Write a 'verbose' level log.\n   */\n  verbose?(message: string, ...optionalParams: ErrorOrMeta[]) {\n    this.logger.verbose(this.parseLoggerArgs(message, optionalParams));\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/middlewares/body-parser.middleware.ts",
    "content": "import { NextFunction, Request, Response } from 'express';\nimport { PayloadTooLargeException, HttpStatus } from '@nestjs/common';\nimport { Config, get } from 'src/utils';\n\nconst serverConfig = get('server') as Config['server'];\n\ninterface BodyParserError extends Error {\n  type?: 'entity.too.large';\n}\n\nexport default (\n  err: BodyParserError,\n  _req: Request,\n  res: Response,\n  next: NextFunction,\n) => {\n  if (err.type === 'entity.too.large') {\n    const exception = new PayloadTooLargeException(\n      `The request is too large. Maximum allowed size is ${serverConfig.maxPayloadSize}`,\n    );\n\n    return res\n      .status(HttpStatus.PAYLOAD_TOO_LARGE)\n      .set('Access-Control-Allow-Origin', serverConfig.cors.origin)\n      .set(\n        'Access-Control-Allow-Credentials',\n        `${serverConfig.cors.credentials}`,\n      )\n      .json(exception.getResponse());\n  }\n\n  next(err);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/middlewares/single-user-auth.middleware.ts",
    "content": "import { Injectable, NestMiddleware } from '@nestjs/common';\nimport { NextFunction, Request, Response } from 'express';\nimport {\n  ISessionMetadata,\n  Session,\n  SessionMetadata,\n} from 'src/common/models/session';\nimport {\n  DEFAULT_ACCOUNT_ID,\n  DEFAULT_SESSION_ID,\n  DEFAULT_USER_ID,\n} from 'src/common/constants';\nimport { SessionService } from 'src/modules/session/session.service';\nimport { plainToInstance } from 'class-transformer';\n\n@Injectable()\nexport class SingleUserAuthMiddleware implements NestMiddleware {\n  constructor(private readonly sessionService: SessionService) {}\n\n  async use(req: Request, res: Response, next: NextFunction): Promise<any> {\n    if (!(await this.sessionService.getSession(DEFAULT_SESSION_ID))) {\n      await this.sessionService.createSession(\n        plainToInstance(Session, {\n          id: DEFAULT_SESSION_ID,\n          userId: DEFAULT_USER_ID,\n          accountId: DEFAULT_ACCOUNT_ID,\n          data: {\n            cloud: {\n              accessToken: process.env.MOCK_AKEY || undefined,\n              refreshToken: process.env.MOCK_RKEY || undefined,\n              idpType: process.env.MOCK_IDP_TYPE || undefined,\n            },\n          },\n        }),\n      );\n    }\n\n    res.locals.session = {\n      data: <ISessionMetadata>Object.freeze(\n        plainToInstance(SessionMetadata, {\n          userId: DEFAULT_USER_ID,\n          accountId: DEFAULT_ACCOUNT_ID,\n          sessionId: DEFAULT_SESSION_ID,\n        }),\n      ),\n    };\n\n    next();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/models/client-metadata.ts",
    "content": "import { SessionMetadata } from 'src/common/models/session';\nimport { Type } from 'class-transformer';\nimport {\n  IsEnum,\n  IsNotEmpty,\n  IsNumber,\n  IsOptional,\n  IsString,\n  Max,\n  Min,\n} from 'class-validator';\nimport { BadRequestException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport enum ClientContext {\n  Common = 'Common',\n  Browser = 'Browser',\n  CLI = 'CLI',\n  Workbench = 'Workbench',\n  Profiler = 'Profiler',\n  AI = 'AI',\n}\n\nexport class ClientMetadata {\n  @IsNotEmpty()\n  @Type(() => SessionMetadata)\n  sessionMetadata: SessionMetadata;\n\n  @IsNotEmpty()\n  @IsString()\n  databaseId: string;\n\n  @IsNotEmpty()\n  @IsEnum(ClientContext, {\n    message: `context must be a valid enum value. Valid values: ${Object.values(\n      ClientContext,\n    )}.`,\n  })\n  context: ClientContext;\n\n  @IsOptional()\n  @IsString()\n  uniqueId?: string;\n\n  @IsOptional()\n  @IsNumber()\n  @Type(() => Number)\n  @Min(0)\n  @Max(2147483647)\n  db?: number;\n\n  /**\n   * Validates client metadata required properties to be defined\n   * Must be used in all the places that works with clients\n   * @param clientMetadata\n   */\n  static validate(clientMetadata: ClientMetadata) {\n    // validate session metadata\n    SessionMetadata.validate(clientMetadata?.sessionMetadata);\n\n    if (!clientMetadata?.databaseId || !clientMetadata?.context) {\n      throw new BadRequestException(ERROR_MESSAGES.INVALID_CLIENT_METADATA);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/models/common.ts",
    "content": "export enum ActionStatus {\n  Success = 'success',\n  Fail = 'fail',\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/models/database-index.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { ClientMetadata } from 'src/common/models/client-metadata';\n\nexport class DatabaseIndex extends PickType(ClientMetadata, ['db'] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/common/models/endpoint.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsInt, IsNotEmpty, IsString } from 'class-validator';\nimport { Expose, Type } from 'class-transformer';\n\nexport interface IEndpoint {\n  host: string;\n  port: number;\n}\n\nexport class Endpoint implements IEndpoint {\n  @ApiProperty({\n    description:\n      'The hostname of your Redis database, for example redis.acme.com.' +\n      ' If your Redis server is running on your local machine, you can enter either 127.0.0.1 or localhost.',\n    type: String,\n    default: 'localhost',\n  })\n  @IsNotEmpty()\n  @IsString({ always: true })\n  @Expose()\n  host: string;\n\n  @ApiProperty({\n    description: 'The port your Redis database is available on.',\n    type: Number,\n    default: 6379,\n  })\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  @Expose()\n  port: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/models/index.ts",
    "content": "export * from './common';\nexport * from './endpoint';\nexport * from './session';\nexport * from './client-metadata';\nexport * from './database-index';\n"
  },
  {
    "path": "redisinsight/api/src/common/models/session.ts",
    "content": "import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';\nimport { BadRequestException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { Expose } from 'class-transformer';\n\nexport interface ISessionMetadata {\n  userId: string;\n  accountId: string;\n  sessionId: string;\n  uniqueId?: string;\n}\n\nexport class SessionMetadata implements ISessionMetadata {\n  @Expose()\n  @IsNotEmpty()\n  @IsString()\n  userId: string;\n\n  @Expose()\n  @IsNotEmpty()\n  @IsString()\n  accountId: string;\n\n  @Expose()\n  @IsObject()\n  data?: Record<string, any> = {};\n\n  @Expose({ groups: ['security'] })\n  @IsObject()\n  @IsOptional()\n  requestMetadata?: Record<string, any> = {};\n\n  @Expose()\n  @IsNotEmpty()\n  @IsString()\n  sessionId: string;\n\n  @Expose()\n  @IsOptional()\n  @IsString()\n  uniqueId?: string;\n\n  @Expose()\n  @IsOptional()\n  @IsString()\n  correlationId?: string;\n\n  /**\n   * Validates session metadata required properties to be defined\n   * Must be used in all the places that works with clients\n   * @param sessionMetadata\n   */\n  static validate(sessionMetadata: SessionMetadata) {\n    if (\n      !sessionMetadata?.sessionId ||\n      !sessionMetadata?.userId ||\n      !sessionMetadata?.accountId\n    ) {\n      throw new BadRequestException(ERROR_MESSAGES.INVALID_SESSION_METADATA);\n    }\n  }\n}\n\nexport class Session {\n  @IsNotEmpty()\n  @IsString()\n  id: string;\n\n  @IsOptional()\n  @IsObject()\n  data?: Record<string, any> = {};\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/pipes/database-index.validation.pipe.ts",
    "content": "import { ArgumentMetadata, ValidationPipe } from '@nestjs/common';\n\nexport class DbIndexValidationPipe extends ValidationPipe {\n  async transform(db, metadata: ArgumentMetadata) {\n    return super.transform({ db }, metadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/pipes/index.ts",
    "content": "export * from './database-index.validation.pipe';\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/index.ts",
    "content": "export * from './redis-string';\nexport * from './redis-reply';\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/formatter-manager.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { UTF8FormatterStrategy } from './strategies/utf8-formatter.strategy';\nimport { FormatterTypes } from './formatter.interface';\nimport { FormatterManager } from './formatter-manager';\n\nconst strategyName = FormatterTypes.UTF8;\nconst testStrategy = new UTF8FormatterStrategy();\n\ndescribe('FormatterManager', () => {\n  let formatter: FormatterManager;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [FormatterManager],\n    }).compile();\n\n    formatter = module.get<FormatterManager>(FormatterManager);\n  });\n  it('Should throw error if no strategy', () => {\n    try {\n      formatter.getStrategy(strategyName);\n    } catch (e) {\n      expect(e.message).toEqual(\n        `Unsupported formatter strategy: ${strategyName}`,\n      );\n    }\n  });\n  it('Should add strategy to formatter and get it back', () => {\n    formatter.addStrategy(strategyName, testStrategy);\n    expect(formatter.getStrategy(strategyName)).toEqual(testStrategy);\n  });\n  it('Should support TextFormatter strategy', () => {\n    formatter.addStrategy(FormatterTypes.UTF8, new UTF8FormatterStrategy());\n    expect(formatter.getStrategy(FormatterTypes.UTF8)).toBeInstanceOf(\n      UTF8FormatterStrategy,\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/formatter-manager.ts",
    "content": "import { FormatterTypes, IFormatterStrategy } from './formatter.interface';\n\nexport class FormatterManager {\n  private strategies = {};\n\n  addStrategy(name: FormatterTypes, strategy: IFormatterStrategy): void {\n    this.strategies[name] = strategy;\n  }\n\n  getStrategy(name: FormatterTypes): IFormatterStrategy {\n    if (!this.strategies[name]) {\n      throw new Error(`Unsupported formatter strategy: ${name}`);\n    }\n\n    return this.strategies[name];\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/formatter.interface.ts",
    "content": "export enum FormatterTypes {\n  ASCII = 'ASCII',\n  UTF8 = 'UTF8',\n}\n\nexport interface IFormatterStrategy {\n  format(reply: any): any;\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/index.ts",
    "content": "export * from './formatter-manager';\nexport * from './formatter.interface';\nexport * from './strategies/ascii-formatter.strategy';\nexport * from './strategies/utf8-formatter.strategy';\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/strategies/ascii-formatter.strategey.spec.ts",
    "content": "import { ASCIIFormatterStrategy } from './ascii-formatter.strategy';\n\ndescribe('ASCIIFormatterStrategy', () => {\n  let strategy;\n  beforeEach(async () => {\n    strategy = new ASCIIFormatterStrategy();\n  });\n\n  describe('format', () => {\n    it('should return correct value for null', () => {\n      const input = null;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(null);\n    });\n    it('should return correct value for integer', () => {\n      const input = 1;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(input);\n    });\n    it('should return correct value for string', () => {\n      const input = Buffer.from('string value');\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual('string value');\n    });\n    it('should return correct value for empty array', () => {\n      const input = [];\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual([]);\n    });\n    it('should return correct value for nested array', () => {\n      const input = [\n        Buffer.from('0'),\n        [\n          Buffer.from('key'),\n          Buffer.from('\"quoted\"\"key\"'),\n          Buffer.from('\"quoted key\"'),\n        ],\n      ];\n      const mockResponse = [\n        '0',\n        ['key', '\\\\\"quoted\\\\\"\\\\\"key\\\\\"', '\\\\\"quoted key\\\\\"'],\n      ];\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should return correct value for object', () => {\n      const input = {\n        field: Buffer.from('value'),\n        secondField: Buffer.from('value'),\n      };\n      const mockResponse = {\n        field: 'value',\n        secondField: 'value',\n      };\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should correctly return stringified json', () => {\n      const object = {\n        key: 'value',\n      };\n      const input = Buffer.from(JSON.stringify(object));\n      const output = strategy.format(input);\n\n      expect(output).toEqual('{\\\\\"key\\\\\":\\\\\"value\\\\\"}');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/strategies/ascii-formatter.strategy.ts",
    "content": "import { isArray, isObject } from 'lodash';\nimport { getASCIISafeStringFromBuffer } from 'src/utils/cli-helper';\nimport { IFormatterStrategy } from '../formatter.interface';\n\nexport class ASCIIFormatterStrategy implements IFormatterStrategy {\n  public format(reply: any): any {\n    if (reply instanceof Buffer) {\n      return getASCIISafeStringFromBuffer(reply);\n    }\n    if (isArray(reply)) {\n      return this.formatRedisArrayReply(reply);\n    }\n    if (isObject(reply)) {\n      return this.formatRedisObjectReply(reply);\n    }\n    return reply;\n  }\n\n  private formatRedisArrayReply(reply: Buffer | Buffer[]): any[] {\n    let result: any;\n    if (isArray(reply)) {\n      if (!reply.length) {\n        result = [];\n      } else {\n        result = reply.map((item) => this.formatRedisArrayReply(item));\n      }\n    } else {\n      result = this.format(reply);\n    }\n    return result;\n  }\n\n  private formatRedisObjectReply(reply: Object): object | string {\n    const result = {};\n\n    if (reply instanceof Error) {\n      return reply.toString();\n    }\n\n    Object.keys(reply).forEach((key) => {\n      result[key] = this.format(reply[key]);\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/strategies/utf8-formatter.strategy.spec.ts",
    "content": "import { UTF8FormatterStrategy } from './utf8-formatter.strategy';\n\ndescribe('UTF8FormatterStrategy', () => {\n  let strategy;\n  beforeEach(async () => {\n    strategy = new UTF8FormatterStrategy();\n  });\n\n  describe('format', () => {\n    it('should return correct value for null', () => {\n      const input = null;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(null);\n    });\n    it('should return correct value for integer', () => {\n      const input = 1;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(input);\n    });\n    it('should return correct value for string', () => {\n      const input = Buffer.from('string value');\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual('string value');\n    });\n    it('should return correct value for empty array', () => {\n      const input = [];\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual([]);\n    });\n    it('should return correct value for nested array', () => {\n      const input = [\n        Buffer.from('0'),\n        [\n          Buffer.from('key'),\n          Buffer.from('\"quoted\"\"key\"'),\n          Buffer.from('\"quoted key\"'),\n        ],\n      ];\n      const mockResponse = ['0', ['key', '\"quoted\"\"key\"', '\"quoted key\"']];\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should return correct value for object', () => {\n      const input = {\n        field: Buffer.from('value'),\n        secondField: Buffer.from('value'),\n      };\n      const mockResponse = {\n        field: 'value',\n        secondField: 'value',\n      };\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should return correct value for object', () => {\n      const input = {\n        field: Buffer.from('value'),\n        secondField: Buffer.from('value'),\n      };\n      const mockResponse = {\n        field: 'value',\n        secondField: 'value',\n      };\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should correctly return string', () => {\n      const string = '名字';\n      const input = Buffer.from(string);\n      const output = strategy.format(input);\n\n      expect(output).toEqual('名字');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-reply/strategies/utf8-formatter.strategy.ts",
    "content": "import { isArray, isObject } from 'lodash';\nimport { getUTF8FromBuffer } from 'src/utils/cli-helper';\nimport { IFormatterStrategy } from '../formatter.interface';\n\nexport class UTF8FormatterStrategy implements IFormatterStrategy {\n  public format(reply: any): any {\n    if (reply instanceof Buffer) {\n      return getUTF8FromBuffer(reply);\n    }\n    if (isArray(reply)) {\n      return this.formatRedisArrayReply(reply);\n    }\n    if (isObject(reply)) {\n      return this.formatRedisObjectReply(reply);\n    }\n    return reply;\n  }\n\n  private formatRedisArrayReply(reply: Buffer | Buffer[]): any[] {\n    let result: any;\n    if (isArray(reply)) {\n      if (!reply.length) {\n        result = [];\n      } else {\n        result = reply.map((item) => this.formatRedisArrayReply(item));\n      }\n    } else {\n      result = this.format(reply);\n    }\n    return result;\n  }\n\n  private formatRedisObjectReply(reply: Object): object | string {\n    const result = {};\n\n    if (reply instanceof Error) {\n      return reply.toString();\n    }\n\n    Object.keys(reply).forEach((key) => {\n      result[key] = this.format(reply[key]);\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-string/any-to-redis-string.transformer.ts",
    "content": "import { RedisString, RedisStringTransformOptions } from 'src/common/constants';\nimport { isArray, isObject, isString } from 'lodash';\nimport { getBufferFromSafeASCIIString } from 'src/utils/cli-helper';\nimport { Transform } from 'class-transformer';\n\nconst SingleToRedisStringTransformer = ({ value }): RedisString => {\n  if (value?.type === 'Buffer') {\n    if (isArray(value.data)) {\n      return Buffer.from(value);\n    }\n\n    if (isObject(value.data)) {\n      return Buffer.from(Object.values(value.data as object));\n    }\n  }\n\n  if (isString(value)) {\n    return getBufferFromSafeASCIIString(value);\n  }\n\n  return value;\n};\n\nconst ArrayToRedisStringTransformer = ({ value }) => {\n  if (isArray(value)) {\n    return value.map((val) => SingleToRedisStringTransformer({ value: val }));\n  }\n\n  return value;\n};\n\nexport const AnyToRedisStringTransformer = (\n  opts?: RedisStringTransformOptions,\n) => {\n  if (opts?.each === true) {\n    return Transform(ArrayToRedisStringTransformer, opts);\n  }\n\n  return Transform(SingleToRedisStringTransformer, opts);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-string/index.ts",
    "content": "export * from './redis-string-to-ascii.transformer';\nexport * from './redis-string-to-utf8.transformer';\nexport * from './redis-string-to-buffer.transformer';\nexport * from './any-to-redis-string.transformer';\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-string/redis-string-to-ascii.transformer.ts",
    "content": "import { isArray } from 'lodash';\nimport { getASCIISafeStringFromBuffer } from 'src/utils/cli-helper';\nimport { RedisStringTransformOptions } from 'src/common/constants';\nimport { Transform } from 'class-transformer';\n\nconst SingleRedisStringToASCII = ({ value }) => {\n  if (value instanceof Buffer) {\n    return getASCIISafeStringFromBuffer(value);\n  }\n\n  // todo: double check. probably no need to convert utf8 to ascii\n  return value;\n};\n\nconst ArrayRedisStringToASCII = ({ value }) => {\n  if (isArray(value)) {\n    return value.map((val) => SingleRedisStringToASCII({ value: val }));\n  }\n\n  return value;\n};\n\nexport const RedisStringToASCIITransformer = (\n  opts?: RedisStringTransformOptions,\n) => {\n  if (opts?.each === true) {\n    return Transform(ArrayRedisStringToASCII, opts);\n  }\n\n  return Transform(SingleRedisStringToASCII, opts);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-string/redis-string-to-buffer.transformer.ts",
    "content": "import { isArray } from 'lodash';\nimport { RedisStringTransformOptions } from 'src/common/constants';\nimport { Transform } from 'class-transformer';\n\nconst SingleRedisStringToBuffer = ({ value }) => {\n  if (value instanceof Buffer) {\n    return value;\n  }\n\n  return Buffer.from(value);\n};\n\nconst ArrayRedisStringToBuffer = ({ value }) => {\n  if (isArray(value)) {\n    return value.map((val) => SingleRedisStringToBuffer({ value: val }));\n  }\n\n  return Buffer.from(value);\n};\n\nexport const RedisStringToBufferTransformer = (\n  opts?: RedisStringTransformOptions,\n) => {\n  if (opts?.each === true) {\n    return Transform(ArrayRedisStringToBuffer, opts);\n  }\n  return Transform(SingleRedisStringToBuffer, opts);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/transformers/redis-string/redis-string-to-utf8.transformer.ts",
    "content": "import { isArray } from 'lodash';\nimport { RedisStringTransformOptions } from 'src/common/constants';\nimport { Transform } from 'class-transformer';\n\nconst SingleRedisStringToUTF8 = ({ value }) => {\n  if (value instanceof Buffer) {\n    return value.toString('utf8');\n  }\n\n  return value;\n};\n\nconst ArrayRedisStringToUTF8 = ({ value }) => {\n  if (isArray(value)) {\n    return value.map((val) => SingleRedisStringToUTF8({ value: val }));\n  }\n\n  return value;\n};\n\nexport const RedisStringToUTF8Transformer = (\n  opts?: RedisStringTransformOptions,\n) => {\n  if (opts?.each) {\n    return Transform(ArrayRedisStringToUTF8, opts);\n  }\n\n  return Transform(SingleRedisStringToUTF8, opts);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/utils/certificate-import.util.ts",
    "content": "import { parse } from 'path';\nimport { readFileSync } from 'fs';\n\nexport const isValidPemCertificate = (cert: string): boolean =>\n  cert.startsWith('-----BEGIN CERTIFICATE-----');\nexport const isValidPemPrivateKey = (cert: string): boolean =>\n  cert.startsWith('-----BEGIN PRIVATE KEY-----');\nexport const isValidSshPrivateKey = (cert: string): boolean =>\n  cert.startsWith('-----BEGIN OPENSSH PRIVATE KEY-----');\nexport const getPemBodyFromFileSync = (path: string): string =>\n  readFileSync(path).toString('utf8');\nexport const getCertNameFromFilename = (path: string): string =>\n  parse(path).name;\n"
  },
  {
    "path": "redisinsight/api/src/common/utils/errors.util.ts",
    "content": "import { HttpException, InternalServerErrorException } from '@nestjs/common';\nimport { AxiosError } from 'axios';\n\nexport const wrapHttpError = (error: Error | AxiosError, message?: string) => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  const { response } = error as any;\n  const errorMessage = error.message || message || response?.data?.message;\n  const descriptionOrOptions =\n    response?.data?.description || response?.data?.options;\n\n  return new InternalServerErrorException(errorMessage, descriptionOrOptions);\n};\n"
  },
  {
    "path": "redisinsight/api/src/common/utils/index.ts",
    "content": "export * from './certificate-import.util';\nexport * from './errors.util';\nexport * from './merge.util';\n"
  },
  {
    "path": "redisinsight/api/src/common/utils/merge.util.spec.ts",
    "content": "import { deepMerge } from 'src/common/utils/merge.util';\n\nconst deepMergeTests = [\n  { obj1: {}, obj2: {}, result: {} },\n  { obj1: { value: 1 }, obj2: { value: 2 }, result: { value: 2 } },\n  {\n    obj1: { value: 1 },\n    obj2: { value: 1.0000001 },\n    result: { value: 1.0000001 },\n  },\n  { obj1: { value: 1 }, obj2: { value: '2' }, result: { value: '2' } },\n  { obj1: { value: 1 }, obj2: { value: undefined }, result: { value: 1 } },\n  { obj1: { value: 1 }, obj2: { value: null }, result: { value: null } },\n  { obj1: { value: 0 }, obj2: { value: null }, result: { value: null } },\n  { obj1: { value: false }, obj2: { value: 1 }, result: { value: 1 } },\n  { obj1: { value: false }, obj2: { value: true }, result: { value: true } },\n  { obj1: { value: false }, obj2: { value: '1' }, result: { value: '1' } },\n  { obj1: { value: false }, obj2: { value: false }, result: { value: false } },\n  { obj1: { value: false }, obj2: { value: 1 }, result: { value: 1 } },\n  { obj1: { value: false }, obj2: { value: true }, result: { value: true } },\n  { obj1: { value: false }, obj2: { value: '1' }, result: { value: '1' } },\n  { obj1: { value: '1' }, obj2: { value: '2' }, result: { value: '2' } },\n  { obj1: { value: undefined }, obj2: { value: 2 }, result: { value: 2 } },\n  {\n    obj1: { value: undefined },\n    obj2: { value: null },\n    result: { value: null },\n  },\n  { obj1: { value: null }, obj2: { value: null }, result: { value: null } },\n  { obj1: {}, obj2: { value: 2 }, result: { value: 2 } },\n  { obj1: { value: {} }, obj2: { value: 1 }, result: { value: 1 } },\n  { obj1: { value: [] }, obj2: { value: 0 }, result: { value: 0 } },\n  { obj1: { value: [] }, obj2: { value: [1] }, result: { value: [1] } },\n  { obj1: { value: [] }, obj2: { value: undefined }, result: { value: [] } },\n  { obj1: { value: { name: 1 } }, obj2: [], result: { value: { name: 1 } } },\n  {\n    obj1: { value: [] },\n    obj2: { value: { name: 1 } },\n    result: { value: { name: 1 } },\n  },\n  { obj1: [1, 2, 3], obj2: [3, 5, 6], result: [3, 5, 6] },\n\n  {\n    obj1: {\n      value: 1,\n      value2: 'string',\n      value3: null,\n      value4: undefined,\n      nested: { nestedValue1: 1, nestedValue2: 2 },\n    },\n    obj2: {\n      value2: undefined,\n      value3: 1,\n      value4: null,\n      value5: 'new',\n      nested: { nestedValue2: 4, nestedValue3: 'value' },\n    },\n    result: {\n      value: 1,\n      value2: 'string',\n      value3: 1,\n      value4: null,\n      value5: 'new',\n      nested: { nestedValue1: 1, nestedValue2: 4, nestedValue3: 'value' },\n    },\n  },\n  {\n    obj1: {\n      value: 0,\n      value2: '',\n      value3: [],\n      value4: {},\n      nested: {\n        nestedValue1: undefined,\n        nestedValue2: {\n          key1: null,\n          key2: undefined,\n          key3: {},\n          key4: [],\n        },\n        nestedValue3: 'value',\n      },\n    },\n    obj2: {\n      value: 'value',\n      value3: { name: 1 },\n      value4: [1, 2, 3],\n      value5: null,\n      nested: {\n        nestedValue1: { key1: 0, key2: undefined, key3: null },\n        nestedValue2: {\n          key1: 'value',\n          key3: { name: 1 },\n          key5: null,\n          key6: 1,\n        },\n        nestedValue4: 1.2,\n      },\n    },\n    result: {\n      value: 'value',\n      value2: '',\n      value3: { name: 1 },\n      value4: [1, 2, 3],\n      value5: null,\n      nested: {\n        nestedValue1: { key1: 0, key2: undefined, key3: null },\n        nestedValue2: {\n          key1: 'value',\n          key2: undefined,\n          key3: { name: 1 },\n          key4: [],\n          key5: null,\n          key6: 1,\n        },\n        nestedValue3: 'value',\n        nestedValue4: 1.2,\n      },\n    },\n  },\n];\n\ndescribe('deepMerge', () => {\n  test.each(deepMergeTests)('%j', ({ obj1, obj2, result }) => {\n    expect(JSON.parse(JSON.stringify(deepMerge(obj1, obj2)))).toStrictEqual(\n      JSON.parse(JSON.stringify(result)),\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/common/utils/merge.util.ts",
    "content": "import { isObjectLike, isUndefined, mergeWith, isArray } from 'lodash';\n\nexport const deepMerge = (target: object, source: object) =>\n  mergeWith(target, source, (targetValue, sourceValue) => {\n    if (isUndefined(sourceValue)) {\n      return targetValue;\n    }\n\n    if (\n      isObjectLike(sourceValue) &&\n      !isArray(sourceValue) &&\n      !isArray(targetValue)\n    ) {\n      return deepMerge(targetValue, sourceValue);\n    }\n\n    return sourceValue;\n  });\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/bigger-than.validator.ts",
    "content": "import {\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'BiggerThan', async: true })\nexport class BiggerThan implements ValidatorConstraintInterface {\n  validate(value: any, args: ValidationArguments) {\n    const [relatedPropertyName] = args.constraints;\n    const relatedValue = (args.object as any)[relatedPropertyName];\n    return (\n      typeof value === 'number' &&\n      typeof relatedValue === 'number' &&\n      value > relatedValue\n    );\n  }\n\n  defaultMessage(args: ValidationArguments) {\n    return `${args.property} must be bigger than ${args.constraints.join(', ')}`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/github-link.validator.ts",
    "content": "import {\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'GitHubLink', async: false })\nexport class GitHubLink implements ValidatorConstraintInterface {\n  validate(value: any) {\n    // Regular expression to match any GitHub URL\n    const githubUrlRegex =\n      /^https:\\/\\/github\\.com(?:\\/[^\\s/]+(?:\\/[^\\s/]+)*)?\\/?$/;\n    return typeof value === 'string' && githubUrlRegex.test(value);\n  }\n\n  defaultMessage() {\n    return 'Enter a full GitHub link';\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/index.ts",
    "content": "export * from './redis-string.validator';\nexport * from './zset-score.validator';\nexport * from './multi-number.validator';\nexport * from './bigger-than.validator';\nexport * from './github-link.validator';\nexport * from './no-duplicates.validator';\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/multi-number.validator.ts",
    "content": "import { isNumber, isArray } from 'lodash';\nimport {\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'MultiNumberValidator', async: true })\nexport class MultiNumberValidator implements ValidatorConstraintInterface {\n  async validate(value: any) {\n    if (!isArray(value)) {\n      return false;\n    }\n\n    return value.every((numbersArray) => {\n      if (!isArray(numbersArray)) {\n        return false;\n      }\n\n      return numbersArray.every(isNumber);\n    });\n  }\n\n  defaultMessage(args: ValidationArguments) {\n    return `${args.property || 'field'} must be a multidimensional array of numbers`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/no-duplicates.validator.ts",
    "content": "import {\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'NoDuplicates', async: true })\nexport class NoDuplicates implements ValidatorConstraintInterface {\n  validate(value: any, args: ValidationArguments) {\n    if (!Array.isArray(value)) {\n      return false;\n    }\n\n    const [key] = args.constraints;\n    if (typeof key !== 'string') {\n      return false;\n    }\n\n    const seen = new Set();\n    for (const item of value) {\n      if (typeof item !== 'object' || item === null || !(key in item)) {\n        return false;\n      }\n      const keyValue = item[key];\n      if (seen.has(keyValue)) {\n        return false;\n      }\n      seen.add(keyValue);\n    }\n\n    return true;\n  }\n\n  defaultMessage(args: ValidationArguments) {\n    const [key] = args.constraints;\n    return `Array must not contain duplicate objects based on the key \"${key}\".`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/redis-string.validator.ts",
    "content": "import { isString } from 'lodash';\nimport {\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'RedisStringValidator', async: true })\nexport class RedisStringValidator implements ValidatorConstraintInterface {\n  async validate(value: any) {\n    return isString(value) || value instanceof Buffer;\n  }\n\n  defaultMessage(args: ValidationArguments) {\n    return `${args.property || 'field'} must be a string or a Buffer`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/common/validators/zset-score.validator.ts",
    "content": "import { isNumber } from 'lodash';\nimport {\n  ValidationArguments,\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'RedisStringValidator', async: true })\nexport class ZSetScoreValidator implements ValidatorConstraintInterface {\n  async validate(value: any) {\n    return value === 'inf' || value === '-inf' || isNumber(value);\n  }\n\n  defaultMessage(args: ValidationArguments) {\n    return `${args.property || 'field'} must be a string or a number`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/agreements-spec.json",
    "content": "{\n  \"version\": \"1.0.6\",\n  \"agreements\": {\n    \"analytics\": {\n      \"defaultValue\": false,\n      \"displayInSetting\": true,\n      \"required\": false,\n      \"editable\": true,\n      \"disabled\": false,\n      \"linkToPrivacyPolicy\": true,\n      \"category\": \"privacy\",\n      \"since\": \"1.0.1\",\n      \"title\": \"Usage Data\",\n      \"label\": \"Usage Data\",\n      \"description\": \"Help improve Redis Insight by sharing anonymous usage data. This helps us understand feature usage and make the app better. By enabling this, you agree to our \"\n    },\n    \"notifications\": {\n      \"defaultValue\": false,\n      \"displayInSetting\": true,\n      \"required\": false,\n      \"editable\": true,\n      \"disabled\": false,\n      \"linkToPrivacyPolicy\": false,\n      \"category\": \"notifications\",\n      \"since\": \"1.0.6\",\n      \"title\": \"Notification\",\n      \"label\": \"Show notification\",\n      \"description\": \"Select to display notification. Otherwise, notifications are shown in the Notification Center.\"\n    },\n    \"encryption\": {\n      \"conditional\": true,\n      \"checker\": \"KEYTAR\",\n      \"defaultOption\": \"false\",\n\n      \"options\": {\n        \"true\": {\n          \"defaultValue\": true,\n          \"displayInSetting\": false,\n          \"required\": false,\n          \"editable\": true,\n          \"disabled\": false,\n          \"linkToPrivacyPolicy\": false,\n          \"category\": \"privacy\",\n          \"since\": \"1.0.3\",\n          \"title\": \"Encryption\",\n          \"label\": \"Encrypt sensitive information\",\n          \"description\": \"Select to encrypt sensitive information using system keychain. Otherwise, this information is stored locally in plain text, which may incur security risk.\"\n        },\n        \"false\": {\n          \"defaultValue\": false,\n          \"displayInSetting\": false,\n          \"required\": false,\n          \"editable\": true,\n          \"disabled\": true,\n          \"linkToPrivacyPolicy\": false,\n          \"category\": \"privacy\",\n          \"since\": \"1.0.3\",\n          \"title\": \"Encryption\",\n          \"label\": \"Encrypt sensitive information\",\n          \"description\": \"Install or enable the system keychain to encrypt and securely store your sensitive information added before using the application. Otherwise, this information will be stored locally in plain text and may lead to security risks.\"\n        },\n        \"stack_false\": {\n          \"defaultValue\": false,\n          \"displayInSetting\": false,\n          \"required\": false,\n          \"editable\": true,\n          \"disabled\": true,\n          \"linkToPrivacyPolicy\": false,\n          \"category\": \"privacy\",\n          \"since\": \"1.0.5\",\n          \"title\": \"Encryption\",\n          \"label\": \"Encrypt sensitive information\",\n          \"description\": \"Data encryption is available in the desktop application version. Download it <a target=\\\"_blank\\\" href=\\\"https://redis.com/redis-enterprise/redis-insight\\\">here</a>.\"\n        }\n      }\n    },\n    \"eula\": {\n      \"defaultValue\": false,\n      \"displayInSetting\": false,\n      \"required\": true,\n      \"editable\": false,\n      \"disabled\": false,\n      \"linkToPrivacyPolicy\": false,\n      \"since\": \"1.0.4\",\n      \"title\": \"Server Side Public License\",\n      \"label\": \"I have read and understood the Terms\",\n      \"requiredText\": \"Accept the Server Side Public License\"\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/app-events.ts",
    "content": "export enum AppAnalyticsEvents {\n  Initialize = 'analytics.initialize',\n  Track = 'analytics.track',\n  Page = 'analytics.page',\n}\n\nexport enum AppRedisInstanceEvents {\n  Deleted = 'instance.deleted',\n}\n\nexport enum RedisClientEvents {\n  ClientStored = 'redis.client.stored',\n  ClientRemoved = 'redis.client.removed',\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/custom-error-codes.ts",
    "content": "export enum CustomErrorCodes {\n  // General [10000, 10899]\n  WindowUnauthorized = 10_001,\n\n  // Redis Connection [10900, 10999]\n  RedisConnectionFailed = 10_900,\n  RedisConnectionTimeout = 10_901,\n  RedisConnectionUnauthorized = 10_902,\n  RedisConnectionClusterNodesUnavailable = 10_903,\n  RedisConnectionUnavailable = 10_904,\n  RedisConnectionAuthUnsupported = 10_905,\n  RedisConnectionSentinelMasterRequired = 10_906,\n  RedisConnectionIncorrectCertificate = 10_907,\n  RedisConnectionDefaultUserDisabled = 10_908,\n\n  // Cloud API [11001, 11099]\n  CloudApiInternalServerError = 11_000,\n  CloudApiUnauthorized = 11_001,\n  CloudApiForbidden = 11_002,\n  CloudApiBadRequest = 11_003,\n  CloudApiNotFound = 11_004,\n  CloudOauthMisconfiguration = 11_005,\n  CloudOauthGithubEmailPermission = 11_006,\n  CloudOauthUnknownAuthorizationRequest = 11_007,\n  CloudOauthUnexpectedError = 11_008,\n  CloudOauthMissedRequiredData = 11_009,\n  CloudOauthCanceled = 11_010,\n  CloudOauthSsoUnsupportedEmail = 11_011,\n  CloudCapiUnauthorized = 11_021,\n  CloudCapiKeyUnauthorized = 11_022,\n  CloudCapiKeyNotFound = 11_023,\n  AzureEntraIdTokenExpired = 11_024,\n\n  // Cloud Job errors [11100, 11199]\n  CloudJobUnexpectedError = 11_100,\n  CloudJobAborted = 11_101,\n  CloudJobUnsupported = 11_102,\n  CloudTaskProcessingError = 11_103,\n  CloudTaskNoResourceId = 11_104,\n  CloudSubscriptionIsInTheFailedState = 11_105,\n  CloudSubscriptionIsInUnexpectedState = 11_106,\n  CloudDatabaseIsInTheFailedState = 11_107,\n  CloudDatabaseAlreadyExistsFree = 11_108,\n  CloudDatabaseIsInUnexpectedState = 11_109,\n  CloudPlanUnableToFindFree = 11_110,\n  CloudSubscriptionUnableToDetermine = 11_111,\n  CloudTaskNotFound = 11_112,\n  CloudJobNotFound = 11_113,\n  CloudSubscriptionAlreadyExistsFree = 11_114,\n  CloudDatabaseImportForbidden = 11_115,\n  CloudDatabaseEndpointInvalid = 11_116,\n\n  // General database errors [11200, 11299]\n  DatabaseAlreadyExists = 11_200,\n\n  // AI errors [11300, 11399]\n  ConvAiInternalServerError = 11_300,\n  ConvAiUnauthorized = 11_301,\n  ConvAiForbidden = 11_302,\n  ConvAiBadRequest = 11_303,\n  ConvAiNotFound = 11_304,\n\n  QueryAiInternalServerError = 11_351,\n  QueryAiUnauthorized = 11_351,\n  QueryAiForbidden = 11_352,\n  QueryAiBadRequest = 11_353,\n  QueryAiNotFound = 11_354,\n  QueryAiRateLimitRequest = 11_360,\n  QueryAiRateLimitToken = 11_361,\n  QueryAiRateLimitMaxTokens = 11_362,\n\n  // RDI errors [11400, 11599]\n  RdiDeployPipelineFailure = 11_401,\n  RdiUnauthorized = 11_402,\n  RdiInternalServerError = 11_403,\n  RdiValidationError = 11_404,\n  RdiNotFound = 11_405,\n  RdiForbidden = 11_406,\n  RdiResetPipelineFailure = 11_407,\n  RdiStartPipelineFailure = 11_408,\n  RdiStopPipelineFailure = 11_409,\n  RdiBadRequest = 11_410,\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/error-messages.ts",
    "content": "/* eslint-disable max-len */\nimport { numberWithSpaces } from 'src/utils/base.helper';\n\nexport default {\n  UNAUTHORIZED: 'Authorization failed',\n  FORBIDDEN: 'Access denied',\n  BAD_REQUEST: 'Bad request',\n  NOT_FOUND: 'Resource was not found',\n  INTERNAL_SERVER_ERROR: 'Server error',\n  REQUEST_TIMEOUT: 'Request timeout',\n\n  INVALID_CLIENT_METADATA: 'Client metadata missed required properties',\n  INVALID_SESSION_METADATA: 'Session metadata missed required properties',\n\n  INVALID_DATABASE_INSTANCE_ID: 'Invalid database instance id.',\n  COMMAND_EXECUTION_NOT_FOUND: 'Command execution was not found.',\n  DATABASE_ANALYSIS_NOT_FOUND: 'Database analysis was not found.',\n  DATABASE_RECOMMENDATION_NOT_FOUND: 'Database recommendation was not found.',\n  BROWSER_HISTORY_ITEM_NOT_FOUND: 'Browser history item was not found.',\n  PROFILER_LOG_FILE_NOT_FOUND: 'Profiler log file was not found.',\n  CONSUMER_GROUP_NOT_FOUND: 'Consumer Group with such name was not found.',\n  PLUGIN_STATE_NOT_FOUND: 'Plugin state was not found.',\n  CUSTOM_TUTORIAL_NOT_FOUND: 'Custom Tutorial was not found.',\n  CUSTOM_TUTORIAL_UNABLE_TO_FETCH_FROM_EXTERNAL:\n    'Unable fetch zip file from external source.',\n  CUSTOM_TUTORIAL_UNSUPPORTED_ORIGIN: 'Unsupported origin for tutorial.',\n  UNDEFINED_INSTANCE_ID: 'Undefined redis database instance id.',\n  NO_CONNECTION_TO_REDIS_DB: 'No connection to the Redis Database.',\n  WRONG_DATABASE_TYPE: 'Wrong database type.',\n  CONNECTION_TIMEOUT:\n    'The connection has timed out, please check the connection details.',\n  DB_CONNECTION_TIMEOUT:\n    'The connection timed out. Try increasing the timeout in the connection settings.',\n  DB_CLUSTER_CONNECT_FAILED:\n    'Redis Insight requires connectivity to all nodes of your clustered database. Ensure all nodes are accessible or increase the timeout.',\n  SERVER_CLOSED_CONNECTION: 'Server closed the connection.',\n  UNABLE_TO_ESTABLISH_CONNECTION: 'Unable to establish connection.',\n  RECONNECTING_TO_DATABASE: 'Reconnecting to the redis database.',\n  AUTHENTICATION_FAILED: () =>\n    'Failed to authenticate, please check the username or password.',\n  INCORRECT_DATABASE_URL: (url) =>\n    `Could not connect to ${url}, please check the connection details.`,\n  INCORRECT_CERTIFICATES: (url) =>\n    `Could not connect to ${url}, please check the CA or Client certificate.`,\n  INCORRECT_CREDENTIALS: (url) =>\n    `Could not connect to ${url}, please check the Username or Password.`,\n  DATABASE_DEFAULT_USER_DISABLED:\n    'Database does not have default user enabled.',\n  DATABASE_MANAGEMENT_IS_DISABLED:\n    'Database connection management is disabled.',\n  CA_CERT_EXIST: 'This ca certificate name is already in use.',\n  INVALID_CA_BODY: 'Invalid CA body',\n  INVALID_SSH_PRIVATE_KEY_BODY: 'Invalid SSH private key body',\n  SSH_AGENTS_ARE_NOT_SUPPORTED: 'SSH Agents are not supported',\n  INVALID_SSH_BODY: 'Invalid SSH body',\n  INVALID_CERTIFICATE_BODY: 'Invalid certificate body',\n  INVALID_PRIVATE_KEY: 'Invalid private key',\n  INVALID_COMPRESSOR: 'Invalid compressor',\n  CERTIFICATE_NAME_IS_NOT_DEFINED: 'Certificate name is not defined',\n  CLIENT_CERT_EXIST: 'This client certificate name is already in use.',\n  INVALID_CERTIFICATE_ID: 'Invalid certificate id.',\n  SENTINEL_MASTER_NAME_REQUIRED: 'Sentinel master name must be specified.',\n  MASTER_GROUP_NOT_EXIST: \"Master group with this name doesn't exist\",\n\n  KEY_NAME_EXIST: 'This key name is already in use.',\n  REDISEARCH_INDEX_EXIST: 'This index name is already in use.',\n  KEY_NOT_EXIST: 'Key with this name does not exist.',\n  PATH_NOT_EXISTS: () => 'There is no such path.',\n  INDEX_OUT_OF_RANGE: () => 'Index is out of range.',\n  MEMBER_IN_SET_NOT_EXIST: 'This member does not exist.',\n  NEW_KEY_NAME_EXIST: 'New key name is already in use.',\n  KEY_OR_TIMEOUT_NOT_EXIST:\n    'Key with this name does not exist or does not have an associated timeout.',\n  SERVER_NOT_AVAILABLE: 'Server is not available. Please try again later.',\n  REDIS_CLOUD_FORBIDDEN: 'Error fetching account details.',\n\n  DATABASE_IS_INACTIVE: 'The database is inactive.',\n  DATABASE_ALREADY_EXISTS: 'The database already exists.',\n\n  INCORRECT_CLUSTER_CURSOR_FORMAT: 'Incorrect cluster cursor format.',\n  REMOVING_MULTIPLE_ELEMENTS_NOT_SUPPORT: () =>\n    'Removing multiple elements is available for Redis databases v. 6.2 or later.',\n  SCAN_PER_KEY_TYPE_NOT_SUPPORT: () =>\n    'Filtering per Key types is available for Redis databases v. 6.0 or later.',\n  WRONG_DISCOVERY_TOOL: () =>\n    'Selected discovery tool is incorrect, please add this database manually using Host and Port.',\n  COMMAND_NOT_SUPPORTED: (command: string) =>\n    `Redis does not support '${command}' command.`,\n  PLUGIN_COMMAND_NOT_SUPPORTED: (command: string) =>\n    `Plugin ERROR: The '${command}' command is not allowed by the Redis Insight Plugins.`,\n  PLUGIN_STATE_MAX_SIZE: (size: number) =>\n    `State should be less then ${size} bytes.`,\n  WORKBENCH_COMMAND_NOT_SUPPORTED: (command) =>\n    `Workbench ERROR: The '${command}' command is not supported by the Redis Insight Workbench.`,\n  WORKBENCH_RESPONSE_TOO_BIG: () =>\n    'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.',\n  CLI_COMMAND_NOT_SUPPORTED: (command: string) =>\n    `CLI ERROR: The '${command}' command is not supported by the Redis Insight CLI.`,\n  CLI_UNTERMINATED_QUOTES: () => 'Invalid argument(s): Unterminated quotes.',\n  CLI_INVALID_QUOTES_CLOSING: () =>\n    'Invalid argument(s): Closing quote must be followed by a space or nothing at all.',\n  CLUSTER_NODE_NOT_FOUND: (node: string) =>\n    `Node ${node} not exist in OSS Cluster.`,\n  REDIS_MODULE_IS_REQUIRED: (module: string) =>\n    `Required ${module} module is not loaded.`,\n  APP_SETTINGS_NOT_FOUND: () => 'Could not find application settings.',\n  SERVER_INFO_NOT_FOUND: () => 'Could not find server info.',\n  INCREASE_MINIMUM_LIMIT: (count?: number) =>\n    count\n      ? `Set MAXSEARCHRESULTS to at least ${numberWithSpaces(count)}.`\n      : 'Increase MAXSEARCHRESULTS value to search more.',\n  INVALID_WINDOW_ID: 'Invalid window id.',\n  UNDEFINED_WINDOW_ID: 'Undefined window id.',\n  LIBRARY_NOT_EXIST: 'This library does not exist.',\n\n  REDIS_CONNECTION_FAILED: 'Unable to connect to the Redis database',\n\n  CLOUD_CAPI_KEY_UNAUTHORIZED: 'Unable to authorize such CAPI key',\n  CLOUD_OAUTH_CANCELED: 'Authorization request was canceled.',\n  CLOUD_OAUTH_MISCONFIGURATION: 'Authorization server misconfiguration.',\n  CLOUD_OAUTH_GITHUB_EMAIL_PERMISSION:\n    'Unable to get an email from the GitHub account. Make sure that it is available.',\n  CLOUD_OAUTH_SSO_UNSUPPORTED_EMAIL: 'Invalid email.',\n  CLOUD_OAUTH_MISSED_REQUIRED_DATA:\n    'Unable to get required data from the user profile.',\n  CLOUD_OAUTH_UNKNOWN_AUTHORIZATION_REQUEST: 'Unknown authorization request.',\n  CLOUD_OAUTH_UNEXPECTED_ERROR: 'Unexpected error.',\n\n  CLOUD_JOB_UNEXPECTED_ERROR: 'Unexpected error occurred',\n  CLOUD_JOB_ABORTED: 'Cloud job aborted',\n  CLOUD_JOB_NOT_FOUND: 'Cloud job was not found',\n  CLOUD_JOB_UNSUPPORTED: 'Unsupported cloud job',\n  CLOUD_SUBSCRIPTION_IN_FAILED_STATE:\n    'Cloud subscription is in the failed state',\n  CLOUD_SUBSCRIPTION_IN_UNEXPECTED_STATE:\n    'Cloud subscription is in unexpected state',\n  CLOUD_SUBSCRIPTION_UNABLE_TO_DETERMINE:\n    'Unable to determine or create free cloud subscription',\n  CLOUD_TASK_PROCESSING_ERROR: 'Cloud task processing returned an error',\n  CLOUD_TASK_NO_RESOURCE_ID: 'Cloud task respond without resource id',\n  CLOUD_TASK_NOT_FOUND: 'Cloud task was not found',\n  CLOUD_DATABASE_IN_FAILED_STATE: 'Cloud database is in the failed state',\n  CLOUD_DATABASE_IN_UNEXPECTED_STATE: 'Cloud database is in unexpected state',\n  CLOUD_DATABASE_ALREADY_EXISTS_FREE: 'Free database already exists',\n  CLOUD_DATABASE_IMPORT_FORBIDDEN:\n    'Adding your Redis Cloud database to Redis Insight is disabled due to a setting restricting database connection management.',\n  CLOUD_DATABASE_ENDPOINT_INVALID:\n    'Database endpoint is unavailable. It may still be provisioning or has been disabled.',\n  CLOUD_PLAN_NOT_FOUND_FREE: 'Unable to find free cloud plan',\n  CLOUD_SUBSCRIPTION_ALREADY_EXISTS_FREE: 'Free subscription already exists',\n  COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data',\n  AI_QUERY_REQUEST_RATE_LIMIT: 'Exceeded limit for requests',\n  AI_QUERY_TOKEN_RATE_LIMIT:\n    'Exceeded limit for characters in the conversation',\n  AI_QUERY_MAX_TOKENS_RATE_LIMIT: 'Token count exceeds the conversation limit',\n\n  RDI_DEPLOY_PIPELINE_FAILURE: 'Failed to deploy pipeline',\n  RDI_RESET_PIPELINE_FAILURE: 'Failed to reset pipeline',\n  RDI_STOP_PIPELINE_FAILURE: 'Failed to stop pipeline',\n  RDI_START_PIPELINE_FAILURE: 'Failed to start pipeline',\n  RDI_TIMEOUT_ERROR:\n    'Encountered a timeout error while attempting to retrieve data',\n  RDI_VALIDATION_ERROR: 'Validation error',\n  INVALID_RDI_INSTANCE_ID: 'Invalid rdi instance id.',\n  UNSAFE_BIG_JSON_LENGTH:\n    'This JSON is too large. Try opening it with Redis Insight Desktop.',\n\n  // database settings\n  DATABASE_SETTINGS_NOT_FOUND: 'Could not find settings for this database',\n\n  // Azure autodiscovery\n  AZURE_DATABASE_NOT_FOUND: 'Database not found',\n  AZURE_FAILED_TO_GET_CONNECTION_DETAILS: 'Failed to get connection details',\n  AZURE_ENTRA_ID_AUTH_FAILED:\n    'Failed to authenticate with Entra ID. Please make sure your user has the correct permissions (Data Owner, Data Contributor, or Data Reader role).',\n  AZURE_UNEXPECTED_ERROR: 'An unexpected error occurred',\n  AZURE_TLS_CERTIFICATE_ERROR:\n    'Could not establish a secure connection. The server certificate could not be verified.',\n  AZURE_ENTRA_ID_TOKEN_EXPIRED:\n    'Azure Entra ID token expired. Sign in to Azure again to continue.',\n};\n"
  },
  {
    "path": "redisinsight/api/src/constants/exceptions.ts",
    "content": "import { HttpException, HttpStatus } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AgreementIsNotDefinedException extends HttpException {\n  constructor(message) {\n    super(\n      {\n        statusCode: HttpStatus.BAD_REQUEST,\n        message,\n        error: 'Bad Request',\n      },\n      HttpStatus.BAD_REQUEST,\n    );\n  }\n}\n\nexport class ServerInfoNotFoundException extends HttpException {\n  constructor(message = ERROR_MESSAGES.SERVER_INFO_NOT_FOUND()) {\n    super(\n      {\n        statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n        message,\n        error: 'Internal Server Error',\n      },\n      HttpStatus.INTERNAL_SERVER_ERROR,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/index.ts",
    "content": "export * from './error-messages';\nexport * from './sort';\nexport * from './regex';\nexport * from './redis-error-codes';\nexport * from './redis-keys';\nexport * from './redis-modules';\nexport * from './exceptions';\nexport * from './redis-commands';\nexport * from './telemetry-events';\nexport * from './app-events';\nexport * from './redis-connection';\nexport * from './recommendations';\nexport * from './custom-error-codes';\n"
  },
  {
    "path": "redisinsight/api/src/constants/recommendations.ts",
    "content": "export const RECOMMENDATION_NAMES = Object.freeze({\n  // todo: moved SET_PASSWORD to top [acl list].\n  //  Investigate why there is no response when placed between ZSET_HASHTABLE_TO_ZIPLIST and RSA\n  SET_PASSWORD: 'setPassword',\n  LUA_SCRIPT: 'luaScript',\n  BIG_HASHES: 'bigHashes',\n  BIG_STRINGS: 'bigStrings',\n  BIG_SETS: 'bigSets',\n  BIG_AMOUNT_OF_CONNECTED_CLIENTS: 'bigAmountOfConnectedClients',\n  USE_SMALLER_KEYS: 'useSmallerKeys',\n  AVOID_LOGICAL_DATABASES: 'avoidLogicalDatabases',\n  COMBINE_SMALL_STRINGS_TO_HASHES: 'combineSmallStringsToHashes',\n  INCREASE_SET_MAX_INTSET_ENTRIES: 'increaseSetMaxIntsetEntries',\n  HASH_HASHTABLE_TO_ZIPLIST: 'hashHashtableToZiplist',\n  COMPRESS_HASH_FIELD_NAMES: 'compressHashFieldNames',\n  COMPRESSION_FOR_LIST: 'compressionForList',\n  ZSET_HASHTABLE_TO_ZIPLIST: 'zSetHashtableToZiplist',\n  RTS: 'RTS',\n  REDIS_VERSION: 'redisVersion',\n  TRY_RDI: 'tryRDI',\n  SEARCH_INDEXES: 'searchIndexes',\n  SEARCH_JSON: 'searchJSON',\n  STRING_TO_JSON: 'stringToJson',\n  SEARCH_VISUALIZATION: 'searchVisualization',\n  SEARCH_HASH: 'searchHash',\n});\n\nexport const ONE_NODE_RECOMMENDATIONS = [\n  RECOMMENDATION_NAMES.LUA_SCRIPT,\n  RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n  RECOMMENDATION_NAMES.REDIS_VERSION,\n  RECOMMENDATION_NAMES.SET_PASSWORD,\n];\n\nexport const REDIS_STACK = [\n  RECOMMENDATION_NAMES.BIG_HASHES,\n  RECOMMENDATION_NAMES.BIG_SETS,\n  RECOMMENDATION_NAMES.RTS,\n  RECOMMENDATION_NAMES.REDIS_VERSION,\n  RECOMMENDATION_NAMES.SEARCH_INDEXES,\n  RECOMMENDATION_NAMES.SEARCH_JSON,\n  RECOMMENDATION_NAMES.STRING_TO_JSON,\n];\n"
  },
  {
    "path": "redisinsight/api/src/constants/redis-commands.ts",
    "content": "export const pluginUnsupportedCommands = [\n  'role',\n  'slowlog',\n  'failover',\n  'bgrewriteaof',\n  'psync',\n  'shutdown',\n  'lastsave',\n  'bgsave',\n  'restore',\n  'cluster',\n  'save',\n  'debug',\n  'pfselftest',\n  'flushdb',\n  'monitor',\n  'pfdebug',\n  'sync',\n  'slaveof',\n  'flushall',\n  'migrate',\n  'info',\n  'keys',\n  'replconf',\n  'config',\n  'replicaof',\n  'acl',\n  'client',\n  'sort',\n  'latency',\n  'restore-asking',\n  'module',\n  'swapdb',\n];\n\nexport const pluginBlockingCommands = [\n  'xreadgroup',\n  'bzpopmax',\n  'blmove',\n  'blpop',\n  'bzpopmin',\n  'brpoplpush',\n  'xread',\n  'brpop',\n];\n"
  },
  {
    "path": "redisinsight/api/src/constants/redis-connection.ts",
    "content": "export const CONNECTION_NAME_GLOBAL_PREFIX = 'redisinsight';\n"
  },
  {
    "path": "redisinsight/api/src/constants/redis-error-codes.ts",
    "content": "export enum RedisErrorCodes {\n  WrongType = 'WRONGTYPE',\n  NoPermission = 'NOPERM',\n  ConnectionRefused = 'ECONNREFUSED',\n  InvalidPassword = 'WRONGPASS',\n  AuthRequired = 'NOAUTH',\n  ConnectionNotFound = 'ENOTFOUND',\n  DNSTimeoutError = 'EAI_AGAIN',\n  ClusterAllFailedError = 'ClusterAllFailedError',\n  SentinelParamsRequired = 'SENTINEL_PARAMS_REQUIRED',\n  ConnectionReset = 'ECONNRESET',\n  Timeout = 'ETIMEDOUT',\n  CommandSyntaxError = 'syntax error',\n  BusyGroup = 'BUSYGROUP',\n  NoGroup = 'NOGROUP',\n  UnknownCommand = 'unknown command',\n  RedisearchLimit = 'LIMIT',\n}\n\n/**\n * RediSearch client error patterns.\n * Verified against actual Redis/RediSearch error messages.\n * These indicate client-side errors and should return 400 Bad Request.\n */\nexport enum RedisearchErrorCodes {\n  Invalid = 'Invalid',\n  BadArguments = 'Bad arguments',\n  Duplicate = 'Duplicate',\n  Missing = 'Missing',\n  WrongNumberOfArguments = 'ERR wrong number of arguments',\n  UnknownIndex = 'Unknown index',\n  NoSuchIndex = 'no such index',\n}\n\nexport enum CertificatesErrorCodes {\n  IncorrectCertificates = 'UNCERTAIN_STATE',\n  DepthZeroSelfSignedCert = 'DEPTH_ZERO_SELF_SIGNED_CERT',\n  SelfSignedCertInChain = 'SELF_SIGNED_CERT_IN_CHAIN',\n  OSSLError = 'ERR_OSSL',\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/redis-keys.ts",
    "content": "export const MAX_TTL_NUMBER = 2147483647;\n\nexport const DEFAULT_MATCH = '*';\n"
  },
  {
    "path": "redisinsight/api/src/constants/redis-modules.ts",
    "content": "export enum AdditionalRedisModuleName {\n  RedisAI = 'ai',\n  RedisGraph = 'graph',\n  RedisGears = 'rg',\n  RedisBloom = 'bf',\n  RedisJSON = 'ReJSON',\n  RediSearch = 'search',\n  RedisTimeSeries = 'timeseries',\n}\n\nexport enum AdditionalSearchModuleName {\n  SearchLight = 'searchlight',\n  FT = 'ft',\n  FTL = 'ftl',\n}\n\nexport const SUPPORTED_REDIS_MODULES = Object.freeze({\n  ai: AdditionalRedisModuleName.RedisAI,\n  graph: AdditionalRedisModuleName.RedisGraph,\n  rg: AdditionalRedisModuleName.RedisGears,\n  bf: AdditionalRedisModuleName.RedisBloom,\n  ReJSON: AdditionalRedisModuleName.RedisJSON,\n  search: AdditionalRedisModuleName.RediSearch,\n  timeseries: AdditionalRedisModuleName.RedisTimeSeries,\n});\n\nexport const REDIS_CLOUD_MODULES_NAMES = Object.freeze({\n  RedisAI: AdditionalRedisModuleName.RedisAI,\n  RedisGraph: AdditionalRedisModuleName.RedisGraph,\n  RedisGears: AdditionalRedisModuleName.RedisGears,\n  RedisBloom: AdditionalRedisModuleName.RedisBloom,\n  RedisJSON: AdditionalRedisModuleName.RedisJSON,\n  RediSearch: AdditionalRedisModuleName.RediSearch,\n  RedisTimeSeries: AdditionalRedisModuleName.RedisTimeSeries,\n});\n\nexport const REDIS_SOFTWARE_MODULES_NAMES = Object.freeze({\n  ai: AdditionalRedisModuleName.RedisAI,\n  graph: AdditionalRedisModuleName.RedisGraph,\n  gears: AdditionalRedisModuleName.RedisGears,\n  bf: AdditionalRedisModuleName.RedisBloom,\n  ReJSON: AdditionalRedisModuleName.RedisJSON,\n  search: AdditionalRedisModuleName.RediSearch,\n  timeseries: AdditionalRedisModuleName.RedisTimeSeries,\n});\n\nexport const REDIS_MODULES_COMMANDS = new Map([\n  [AdditionalRedisModuleName.RedisAI, ['ai.info']],\n  [AdditionalRedisModuleName.RedisGraph, ['graph.delete']],\n  [AdditionalRedisModuleName.RedisGears, ['rg.pyexecute']],\n  [\n    AdditionalRedisModuleName.RedisBloom,\n    ['bf.info', 'cf.info', 'cms.info', 'topk.info'],\n  ],\n  [AdditionalRedisModuleName.RedisJSON, ['json.get']],\n  [AdditionalRedisModuleName.RediSearch, ['ft.info']],\n  [AdditionalRedisModuleName.RedisTimeSeries, ['ts.mrange', 'ts.info']],\n]);\n\nexport const REDISEARCH_MODULES: string[] = [\n  AdditionalRedisModuleName.RediSearch,\n  AdditionalSearchModuleName.SearchLight,\n  AdditionalSearchModuleName.FT,\n  AdditionalSearchModuleName.FTL,\n];\n"
  },
  {
    "path": "redisinsight/api/src/constants/regex.ts",
    "content": "export const ARG_IN_QUOTATION_MARKS_REGEX = /\"[^\"]*|'[^']*'|\"+/g;\nexport const IS_INTEGER_NUMBER_REGEX = /^\\d+$/;\nexport const IS_NUMBER_REGEX = /^-?\\d*(\\.\\d+)?$/;\nexport const IS_NON_PRINTABLE_ASCII_CHARACTER = /[^ -~\\u0007\\b\\t\\n\\r]/;\nexport const IP_ADDRESS_REGEX =\n  /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;\nexport const PRIVATE_IP_ADDRESS_REGEX =\n  /(^127\\.)|(^10\\.)|(^172\\.1[6-9]\\.)|(^172\\.2[0-9]\\.)|(^172\\.3[0-1]\\.)|(^192\\.168\\.)/;\nexport const IS_TIMESTAMP = /^(\\d{10}|\\d{13}|\\d{16}|\\d{19})$/;\n"
  },
  {
    "path": "redisinsight/api/src/constants/sort.ts",
    "content": "export enum SortOrder {\n  Asc = 'ASC',\n  Desc = 'DESC',\n}\n"
  },
  {
    "path": "redisinsight/api/src/constants/telemetry-events.ts",
    "content": "export enum TelemetryEvents {\n  // Main events\n  ApplicationFirstStart = 'APPLICATION_FIRST_START',\n  ApplicationStarted = 'APPLICATION_STARTED',\n  AnalyticsPermission = 'ANALYTICS_PERMISSION',\n  SettingsScanThresholdChanged = 'SETTINGS_KEYS_TO_SCAN_CHANGED',\n  SettingsWorkbenchPipelineChanged = 'SETTINGS_WORKBENCH_PIPELINE_CHANGED',\n  DatabaseConnectedClientList = 'DATABASE_CONNECTED_CLIENT_LIST',\n\n  // Events for redis instances\n  RedisInstanceAdded = 'CONFIG_DATABASES_DATABASE_ADDED',\n  RedisInstanceAddFailed = 'CONFIG_DATABASES_DATABASE_ADD_FAILED',\n  RedisInstanceDeleted = 'CONFIG_DATABASES_DATABASE_DELETED',\n  RedisInstanceEditedByUser = 'CONFIG_DATABASES_DATABASE_EDITED_BY_USER',\n  RedisInstanceConnectionFailed = 'DATABASE_CONNECTION_FAILED',\n\n  // Databases import\n  DatabaseImportParseFailed = 'CONFIG_DATABASES_REDIS_IMPORT_PARSE_FAILED',\n  DatabaseImportFailed = 'CONFIG_DATABASES_REDIS_IMPORT_FAILED',\n  DatabaseImportSucceeded = 'CONFIG_DATABASES_REDIS_IMPORT_SUCCEEDED',\n  DatabaseImportPartiallySucceeded = 'CONFIG_DATABASES_REDIS_IMPORT_PARTIALLY_SUCCEEDED',\n\n  // Events for autodiscovery flows\n  RedisSoftwareDiscoverySucceed = 'CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_SUCCEEDED',\n  RedisSoftwareDiscoveryFailed = 'CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_FAILED',\n  RedisCloudSubscriptionsDiscoverySucceed = 'CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_SUBSCRIPTIONS_SUCCEEDED',\n  RedisCloudSubscriptionsDiscoveryFailed = 'CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_SUBSCRIPTIONS_FAILED',\n  RedisCloudDatabasesDiscoverySucceed = 'CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_DATABASES_SUCCEEDED',\n  RedisCloudDatabasesDiscoveryFailed = 'CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_DATABASES_FAILED',\n  SentinelMasterGroupsDiscoverySucceed = 'CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUCCEEDED',\n  SentinelMasterGroupsDiscoveryFailed = 'CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_FAILED',\n\n  // Events for cloud oauth\n  CloudSignInSucceeded = 'CLOUD_SIGN_IN_SUCCEEDED',\n  CloudSignInFailed = 'CLOUD_SIGN_IN_FAILED',\n  CloudFreeDatabaseCreated = 'CLOUD_FREE_DATABASE_CREATED',\n  CloudFreeDatabaseFailed = 'CLOUD_FREE_DATABASE_FAILED',\n\n  // Events for Azure oauth\n  AzureSignInSucceeded = 'AZURE_SIGN_IN_SUCCEEDED',\n  AzureSignInFailed = 'AZURE_SIGN_IN_FAILED',\n\n  // Events for Azure autodiscovery\n  AzureSubscriptionsDiscoverySucceeded = 'AZURE_SUBSCRIPTIONS_AUTODISCOVERY_SUCCEEDED',\n  AzureSubscriptionsDiscoveryFailed = 'AZURE_SUBSCRIPTIONS_AUTODISCOVERY_FAILED',\n  AzureDatabasesDiscoverySucceeded = 'AZURE_DATABASES_AUTODISCOVERY_SUCCEEDED',\n  AzureDatabasesDiscoveryFailed = 'AZURE_DATABASES_AUTODISCOVERY_FAILED',\n  AzureDatabaseAdded = 'AZURE_DATABASE_ADDED',\n  AzureDatabaseAddFailed = 'AZURE_DATABASE_ADD_FAILED',\n\n  // Event for cloud CAPI keys\n  CloudAccountKeyGenerated = 'CLOUD_ACCOUNT_KEY_GENERATED',\n  CloudAccountKeyGenerationFailed = 'CLOUD_ACCOUNT_KEY_GENERATION_FAILED',\n  CloudAccountSecretGenerated = 'CLOUD_ACCOUNT_SECRET_GENERATED',\n  CloudAccountSecretGenerationFailed = 'CLOUD_ACCOUNT_SECRET_GENERATION_FAILED',\n\n  // Events for cli tool\n  CliClientCreated = 'CLI_CLIENT_CREATED',\n  CliClientCreationFailed = 'CLI_CLIENT_CREATION_FAILED',\n  CliClientConnectionError = 'CLI_CLIENT_CONNECTION_ERROR',\n  CliClientDeleted = 'CLI_CLIENT_DELETED',\n  CliClientRecreated = 'CLI_CLIENT_RECREATED',\n  CliCommandExecuted = 'CLI_COMMAND_EXECUTED',\n  CliIndexInfoSubmitted = 'CLI_INDEX_INFO_SUBMITTED',\n  CliClusterNodeCommandExecuted = 'CLI_CLUSTER_COMMAND_EXECUTED',\n  CliCommandErrorReceived = 'CLI_COMMAND_ERROR_RECEIVED',\n\n  // Events for workbench tool\n  WorkbenchCommandExecuted = 'WORKBENCH_COMMAND_EXECUTED',\n  WorkbenchIndexInfoSubmitted = 'WORKBENCH_INDEX_INFO_SUBMITTED',\n  WorkbenchCommandErrorReceived = 'WORKBENCH_COMMAND_ERROR_RECEIVED',\n  WorkbenchCommandDeleted = 'WORKBENCH_COMMAND_DELETE_COMMAND',\n\n  // Events for search tool\n  SearchCommandExecuted = 'SEARCH_COMMAND_EXECUTED',\n  SearchIndexInfoSubmitted = 'SEARCH_INDEX_INFO_SUBMITTED',\n  SearchCommandErrorReceived = 'SEARCH_COMMAND_ERROR_RECEIVED',\n\n  // Custom tutorials\n  WorkbenchEnablementAreaImportSucceeded = 'WORKBENCH_ENABLEMENT_AREA_IMPORT_SUCCEEDED',\n  WorkbenchEnablementAreaImportFailed = 'WORKBENCH_ENABLEMENT_AREA_IMPORT_FAILED',\n\n  // Profiler\n  ProfilerLogDownloaded = 'PROFILER_LOG_DOWNLOADED',\n  ProfilerLogDeleted = 'PROFILER_LOG_DELETED',\n\n  // Slowlog\n  SlowlogSetLogSlowerThan = 'SLOWLOG_SET_LOG_SLOWER_THAN',\n  SlowlogSetMaxLen = 'SLOWLOG_SET_MAX_LEN',\n\n  // Pub/Sub\n  PubSubMessagePublished = 'PUBSUB_MESSAGE_PUBLISHED',\n  PubSubChannelSubscribed = 'PUBSUB_CHANNEL_SUBSCRIBED',\n  PubSubChannelUnsubscribed = 'PUBSUB_CHANNEL_UNSUBSCRIBED',\n\n  // Bulk Actions\n  BulkActionsStarted = 'BULK_ACTIONS_STARTED',\n  BulkActionsStopped = 'BULK_ACTIONS_STOPPED',\n  BulkActionsSucceed = 'BULK_ACTIONS_SUCCEED',\n  BulkActionsFailed = 'BULK_ACTIONS_FAILED',\n  ImportSamplesUploaded = 'IMPORT_SAMPLES_UPLOADED',\n\n  // Feature\n  FeatureFlagConfigUpdated = 'FEATURE_FLAG_CONFIG_UPDATED',\n  FeatureFlagConfigUpdateError = 'FEATURE_FLAG_CONFIG_UPDATE_ERROR',\n  FeatureFlagInvalidRemoteConfig = 'FEATURE_FLAG_INVALID_REMOTE_CONFIG',\n  FeatureFlagRecalculated = 'FEATURE_FLAG_RECALCULATED',\n\n  // Insights\n  InsightsTipGenerated = 'INSIGHTS_TIP_GENERATED',\n\n  // RDI\n  RdiInstanceDeleted = 'RDI_INSTANCE_DELETED',\n  RdiPipelineDeploymentSucceeded = 'RDI_PIPELINE_DEPLOYMENT_SUCCEEDED',\n  RdiPipelineDeploymentFailed = 'RDI_PIPELINE_DEPLOYMENT_FAILED',\n  RdiPipelineUploaded = 'RDI_PIPELINE_UPLOAD_SUCCEEDED',\n  RdiPipelineUploadFailed = 'RDI_PIPELINE_UPLOAD_FAILED',\n}\n\nexport enum CommandType {\n  Core = 'core',\n  Module = 'module',\n}\n\nexport const unknownCommand = 'unknown';\n"
  },
  {
    "path": "redisinsight/api/src/constants/websocket-rooms.ts",
    "content": "export const getUserRoom = (userId: string) => `user:${userId}`;\n"
  },
  {
    "path": "redisinsight/api/src/core.module.ts",
    "content": "import { Global, Module } from '@nestjs/common';\nimport { EncryptionModule } from 'src/modules/encryption/encryption.module';\nimport { SettingsModule } from 'src/modules/settings/settings.module';\nimport { DatabaseModule } from 'src/modules/database/database.module';\nimport { CertificateModule } from 'src/modules/certificate/certificate.module';\nimport { DatabaseRecommendationModule } from 'src/modules/database-recommendation/database-recommendation.module';\nimport { EventEmitterModule } from '@nestjs/event-emitter';\nimport { RedisModule } from 'src/modules/redis/redis.module';\nimport { AnalyticsModule } from 'src/modules/analytics/analytics.module';\nimport { SshModule } from 'src/modules/ssh/ssh.module';\nimport { NestjsFormDataModule } from 'nestjs-form-data';\nimport { FeatureModule } from 'src/modules/feature/feature.module';\nimport { AuthModule } from 'src/modules/auth/auth.module';\nimport { SessionModule } from 'src/modules/session/session.module';\nimport { ServerModule } from 'src/modules/server/server.module';\nimport { ConstantsModule } from 'src/modules/constants/constants.module';\nimport { DatabaseDiscoveryModule } from 'src/modules/database-discovery/database-discovery.module';\nimport { TagModule } from 'src/modules/tag/tag.module';\n\n@Global()\n@Module({\n  imports: [\n    ConstantsModule.register(),\n    EventEmitterModule.forRoot(),\n    DatabaseDiscoveryModule,\n    TagModule,\n    AnalyticsModule,\n    EncryptionModule.register(),\n    SettingsModule.register(),\n    CertificateModule.register(),\n    DatabaseModule.register(),\n    RedisModule.register(),\n    DatabaseRecommendationModule.register(),\n    SshModule,\n    NestjsFormDataModule,\n    FeatureModule.register(),\n    AuthModule.register(),\n    SessionModule.register(),\n    ServerModule.register(),\n  ],\n  exports: [\n    ConstantsModule,\n    EncryptionModule,\n    DatabaseDiscoveryModule,\n    TagModule,\n    SettingsModule,\n    CertificateModule,\n    DatabaseModule,\n    DatabaseRecommendationModule,\n    RedisModule,\n    SshModule,\n    NestjsFormDataModule,\n    FeatureModule,\n    SessionModule,\n    ServerModule,\n  ],\n})\nexport class CoreModule {}\n"
  },
  {
    "path": "redisinsight/api/src/decorators/api-endpoint.decorator.ts",
    "content": "import { applyDecorators, HttpCode } from '@nestjs/common';\nimport { ApiExcludeEndpoint, ApiOperation, ApiResponse } from '@nestjs/swagger';\nimport { ApiResponseOptions } from '@nestjs/swagger/dist/decorators/api-response.decorator';\nimport config, { Config } from 'src/utils/config';\nimport { BuildType } from 'src/modules/server/models/server';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\nexport interface IApiEndpointOptions {\n  description: string;\n  statusCode?: number;\n  responses?: ApiResponseOptions[];\n  excludeFor?: BuildType[];\n}\n\nexport function ApiEndpoint(\n  options: IApiEndpointOptions,\n): MethodDecorator & ClassDecorator {\n  const { description, statusCode, responses = [], excludeFor = [] } = options;\n  return applyDecorators(\n    ApiOperation({ description }),\n    ApiExcludeEndpoint(\n      excludeFor.includes(SERVER_CONFIG.buildType as BuildType),\n    ),\n    HttpCode(statusCode),\n    ...responses?.map((response) => ApiResponse(response)),\n  );\n}\n"
  },
  {
    "path": "redisinsight/api/src/decorators/api-redis-instance-operation.decorator.ts",
    "content": "import { applyDecorators } from '@nestjs/common';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport {\n  ApiEndpoint,\n  IApiEndpointOptions,\n} from 'src/decorators/api-endpoint.decorator';\n\nexport function ApiRedisInstanceOperation(\n  options: IApiEndpointOptions,\n): MethodDecorator & ClassDecorator {\n  return applyDecorators(ApiRedisParams(), ApiEndpoint(options));\n}\n"
  },
  {
    "path": "redisinsight/api/src/decorators/api-redis-params.decorator.ts",
    "content": "import { applyDecorators } from '@nestjs/common';\nimport { ApiParam } from '@nestjs/swagger';\n\nexport function ApiRedisParams(): MethodDecorator & ClassDecorator {\n  return applyDecorators(\n    ApiParam({\n      name: 'dbInstance',\n      description: 'Database instance id.',\n      type: String,\n      required: true,\n    }),\n  );\n}\n"
  },
  {
    "path": "redisinsight/api/src/dto/dto-transformer.spec.ts",
    "content": "import { pickDefinedAgreements } from 'src/dto/dto-transformer';\n\ndescribe('pickDefinedAgreements', () => {\n  it('should pick only agreements that defined in specification', () => {\n    const value = new Map([\n      ['eula', true],\n      ['undefined', true],\n    ]);\n\n    const output = pickDefinedAgreements({ value });\n\n    expect(output).toEqual(new Map([['eula', true]]));\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/dto/dto-transformer.ts",
    "content": "import { isMap } from 'lodash';\nimport * as AGREEMENTS_SPEC from 'src/constants/agreements-spec.json';\n\n// Delete all keys from the validated Map that are not included in the settings specification.\nexport const pickDefinedAgreements = ({ value }) => {\n  if (isMap(value)) {\n    for (const k of value?.keys()) {\n      if (!AGREEMENTS_SPEC.agreements[k]) {\n        value.delete(k);\n      }\n    }\n  }\n  return value;\n};\n"
  },
  {
    "path": "redisinsight/api/src/exceptions/global-exception.filter.ts",
    "content": "import { BaseExceptionFilter } from '@nestjs/core';\nimport { ArgumentsHost, Logger } from '@nestjs/common';\nimport { Request, Response } from 'express';\n\nexport class GlobalExceptionFilter extends BaseExceptionFilter {\n  private staticServerLogger = new Logger('GlobalExceptionFilter');\n\n  catch(exception: Error, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const request = ctx.getRequest<Request>();\n\n    if (/^\\/(?:plugins|static)\\//i.test(request.url)) {\n      const response = ctx.getResponse<Response>();\n      const statusCode = exception['statusCode'] || 500;\n      const message = `Error when trying to fetch ${request.url}`;\n\n      this.staticServerLogger.error(message, { ...exception } as any);\n      return response.status(statusCode).json({\n        statusCode,\n        message,\n      });\n    }\n\n    return super.catch(exception, host);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/init-helper.ts",
    "content": "import * as fs from 'fs-extra';\nimport { join } from 'path';\n\nimport config from 'src/utils/config';\n\nconst PATH_CONFIG = config.get('dir_path');\nconst DB_CONFIG = config.get('db');\n\n/**\n * Copy source if exists\n * @param source\n * @param destination\n */\nconst copySource = async (source, destination) => {\n  if (await fs.pathExists(source)) {\n    await fs.copy(source, destination).catch();\n  }\n};\n\n/**\n * Migrate data from previous home folder defined in configs\n */\nexport const migrateHomeFolder = async () => {\n  try {\n    if (\n      !(await fs.pathExists(DB_CONFIG.database)) &&\n      (await fs.pathExists(PATH_CONFIG.prevHomedir))\n    ) {\n      await fs.ensureDir(PATH_CONFIG.homedir);\n\n      await Promise.all(\n        ['redisinsight.db', 'plugins', 'custom-tutorials'].map((target) =>\n          copySource(\n            join(PATH_CONFIG.prevHomedir, target),\n            join(PATH_CONFIG.homedir, target),\n          ),\n        ),\n      );\n    }\n\n    return true;\n  } catch (e) {\n    // continue initialization even without migration\n    return false;\n  }\n};\n\n/**\n * Remove a folder\n */\nexport const removeFolder = async (path: string) => {\n  try {\n    if (await fs.pathExists(path)) {\n      await fs.rm(path, { recursive: true, force: true });\n    }\n  } catch (e) {\n    // continue initialization even without removing\n  }\n};\n\n/**\n * Remove old folders\n */\nexport const removeOldFolders = async () => {\n  try {\n    // remove old folders\n    await PATH_CONFIG.oldFolders?.map(removeFolder);\n  } catch (e) {\n    // continue initialization even without removing\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/local-database.module.ts",
    "content": "import { Global, Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';\nimport { ormModuleOptions } from '../config/ormconfig';\n\n@Global()\n@Module({\n  imports: [\n    TypeOrmModule.forRoot(ormModuleOptions),\n    TypeOrmModule.forFeature(\n      ormModuleOptions.entities as EntityClassOrSchema[],\n    ),\n  ],\n  exports: [TypeOrmModule],\n})\nexport class LocalDatabaseModule {}\n"
  },
  {
    "path": "redisinsight/api/src/main.ts",
    "content": "import { posix } from 'path';\nimport 'dotenv/config';\nimport * as qs from 'qs';\nimport { NestFactory } from '@nestjs/core';\nimport { SwaggerModule } from '@nestjs/swagger';\nimport { NestExpressApplication } from '@nestjs/platform-express';\nimport {\n  INestApplication,\n  Logger,\n  NestApplicationOptions,\n} from '@nestjs/common';\nimport * as bodyParser from 'body-parser';\nimport bodyParserMiddleware from 'src/common/middlewares/body-parser.middleware';\nimport { GlobalExceptionFilter } from 'src/exceptions/global-exception.filter';\nimport { get, Config } from 'src/utils';\nimport { migrateHomeFolder, removeOldFolders } from 'src/init-helper';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport { WindowsAuthAdapter } from 'src/modules/auth/window-auth/adapters/window-auth.adapter';\nimport { AppLogger } from 'src/common/logger/app-logger';\nimport { BuildType } from './modules/server/models/server';\nimport { CloudAuthModule } from 'src/modules/cloud/auth/cloud-auth.module';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\nimport { AppModule } from './app.module';\nimport SWAGGER_CONFIG from '../config/swagger';\nimport LOGGER_CONFIG from '../config/logger';\nimport { createHttpOptions } from './utils/createHttpOptions';\nimport { SessionMetadataAdapter } from './modules/auth/session-metadata/adapters/session-metadata.adapter';\n\nconst serverConfig = get('server') as Config['server'];\n\ninterface IApp {\n  app: INestApplication;\n  gracefulShutdown: Function;\n  cloudAuthService: CloudAuthService;\n}\n\nexport default async function bootstrap(apiPort?: number): Promise<IApp> {\n  if (serverConfig.migrateOldFolders) {\n    if (await migrateHomeFolder()) {\n      await removeOldFolders();\n    }\n  }\n\n  if (apiPort) {\n    serverConfig.port = apiPort;\n  }\n\n  const logger = new AppLogger(LOGGER_CONFIG);\n\n  const options: NestApplicationOptions = {\n    logger,\n  };\n\n  if (serverConfig.tlsCert && serverConfig.tlsKey) {\n    options.httpsOptions = await createHttpOptions(serverConfig);\n  }\n\n  const app = await NestFactory.create<NestExpressApplication>(\n    AppModule,\n    options,\n  );\n  app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));\n  // set qs as parser to support nested objects in the query string\n  app.set('query parser', qs.parse);\n  app.use(bodyParser.json({ limit: serverConfig.maxPayloadSize }));\n  app.use(\n    bodyParser.urlencoded({\n      limit: serverConfig.maxPayloadSize,\n      extended: true,\n    }),\n  );\n  app.use(bodyParserMiddleware);\n  app.enableCors();\n\n  if (\n    process.env.RI_BUILD_TYPE !== BuildType.Electron ||\n    process.env.NODE_ENV === 'development'\n  ) {\n    let prefix = serverConfig.globalPrefix;\n    if (serverConfig.proxyPath) {\n      prefix = posix.join(serverConfig.proxyPath, prefix);\n    }\n\n    app.setGlobalPrefix(prefix, { exclude: ['/'] });\n\n    SwaggerModule.setup(\n      serverConfig.docPrefix,\n      app,\n      SwaggerModule.createDocument(app, SWAGGER_CONFIG),\n      {\n        swaggerOptions: {\n          docExpansion: 'none',\n          tagsSorter: 'alpha',\n          operationsSorter: 'alpha',\n        },\n      },\n    );\n  } else {\n    app.setGlobalPrefix(serverConfig.globalPrefix);\n    app.useWebSocketAdapter(new WindowsAuthAdapter(app));\n  }\n\n  app.useWebSocketAdapter(new SessionMetadataAdapter(app));\n\n  const logFileProvider = app.get(LogFileProvider);\n\n  const { port, host } = serverConfig;\n\n  await app.listen(port, host);\n\n  const bootstrapLogger = new Logger('boostrap');\n  bootstrapLogger.log(`Server is running on http(s)://${host}:${port}`);\n\n  const gracefulShutdown = (signal) => {\n    try {\n      bootstrapLogger.log(`Signal ${signal} received. Shutting down...`);\n      logFileProvider.onModuleDestroy();\n    } catch (e) {\n      // ignore errors if any\n    }\n    process.exit(0);\n  };\n\n  process.on('SIGTERM', gracefulShutdown);\n  process.on('SIGINT', gracefulShutdown);\n\n  const cloudAuthService = app.select(CloudAuthModule).get(CloudAuthService);\n\n  return { app, gracefulShutdown, cloudAuthService };\n}\n\nif (serverConfig.autoBootstrap) {\n  bootstrap();\n}\n"
  },
  {
    "path": "redisinsight/api/src/middleware/exclude-route.middleware.ts",
    "content": "import { Injectable, NestMiddleware, NotFoundException } from '@nestjs/common';\nimport { Request } from 'express';\n\n@Injectable()\nexport class ExcludeRouteMiddleware implements NestMiddleware {\n  use(req: Request) {\n    throw new NotFoundException(`Cannot ${req.method} ${req.originalUrl}`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/middleware/redis-connection/index.ts",
    "content": "export * from './redis-connection.middleware';\nexport * from './route-controllers';\n"
  },
  {
    "path": "redisinsight/api/src/middleware/redis-connection/redis-connection.middleware.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NestMiddleware,\n  NotFoundException,\n} from '@nestjs/common';\nimport { NextFunction, Request, Response } from 'express';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { sessionMetadataFromRequest } from 'src/common/decorators';\n\n@Injectable()\nexport class RedisConnectionMiddleware implements NestMiddleware {\n  private logger = new Logger('RedisConnectionMiddleware');\n\n  constructor(private databaseService: DatabaseService) {}\n\n  async use(req: Request, res: Response, next: NextFunction): Promise<any> {\n    const { instanceIdFromReq } =\n      RedisConnectionMiddleware.getConnectionConfigFromReq(req);\n    if (!instanceIdFromReq) {\n      this.throwError(req, ERROR_MESSAGES.UNDEFINED_INSTANCE_ID);\n    }\n\n    const sessionMetadata = sessionMetadataFromRequest(req);\n\n    const existDatabaseInstance = await this.databaseService.exists(\n      sessionMetadata,\n      instanceIdFromReq,\n    );\n    if (!existDatabaseInstance) {\n      throw new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n    }\n\n    next();\n  }\n\n  private static getConnectionConfigFromReq(req: Request) {\n    return { instanceIdFromReq: req.params.dbInstance };\n  }\n\n  private throwError(req: Request, message: string) {\n    const { method, url } = req;\n    this.logger.error(`${message} ${method} ${url}`);\n    throw new BadRequestException(message);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/middleware/redis-connection/route-controllers.ts",
    "content": "import { HashController } from 'src/modules/browser/hash/hash.controller';\nimport { KeysController } from 'src/modules/browser/keys/keys.controller';\nimport { ListController } from 'src/modules/browser/list/list.controller';\nimport { RejsonRlController } from 'src/modules/browser/rejson-rl/rejson-rl.controller';\nimport { SetController } from 'src/modules/browser/set/set.controller';\nimport { StreamController } from 'src/modules/browser/stream/controllers/stream.controller';\nimport { ConsumerGroupController } from 'src/modules/browser/stream/controllers/consumer-group.controller';\nimport { ConsumerController } from 'src/modules/browser/stream/controllers/consumer.controller';\nimport { StringController } from 'src/modules/browser/string/string.controller';\nimport { ZSetController } from 'src/modules/browser/z-set/z-set.controller';\nimport { CliController } from 'src/modules/cli/controllers/cli.controller';\nimport { WorkbenchController } from 'src/modules/workbench/workbench.controller';\nimport { QueryLibraryController } from 'src/modules/query-library/query-library.controller';\n\nexport const redisConnectionControllers = [\n  HashController,\n  KeysController,\n  ListController,\n  RejsonRlController,\n  SetController,\n  StreamController,\n  ConsumerGroupController,\n  ConsumerController,\n  StringController,\n  ZSetController,\n  CliController,\n  WorkbenchController,\n  QueryLibraryController,\n];\n"
  },
  {
    "path": "redisinsight/api/src/middleware/subpath-proxy.middleware.ts",
    "content": "import { NestMiddleware, Injectable } from '@nestjs/common';\nimport { Request, Response, NextFunction } from 'express';\nimport { trim } from 'lodash';\nimport * as fs from 'fs';\n\n@Injectable()\nexport default class SubpathProxyMiddleware implements NestMiddleware {\n  async use(req: Request, res: Response, next: NextFunction) {\n    const originalSendFile = res.sendFile;\n    const proxyPath = trim(process.env.RI_PROXY_PATH, '/');\n    res.sendFile = function (\n      this: Response,\n      path: string,\n      options: any,\n      callback?: (err?: Error) => void,\n    ) {\n      if (path.endsWith('.html') || path.endsWith('.js')) {\n        let content = fs.readFileSync(path, 'utf8');\n        const regex = /\\/?__RIPROXYPATH__/g;\n\n        // for vite build proxyPath if exists should starts with '/'\n        content = content.replace(regex, proxyPath ? '/' + proxyPath : '');\n        res.send(content);\n        return;\n      }\n      originalSendFile.call(this, path, options, callback);\n    };\n\n    next();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/middleware/x-frame-options.middleware.ts",
    "content": "import { NestMiddleware, Injectable } from '@nestjs/common';\nimport { Request, Response, NextFunction } from 'express';\n\n@Injectable()\nexport default class XFrameOptionsMiddleware implements NestMiddleware {\n  use(req: Request, res: Response, next: NextFunction) {\n    res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n    next();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/models/index.ts",
    "content": "export * from './redis-client';\nexport * from './redis-cluster';\n"
  },
  {
    "path": "redisinsight/api/src/models/redis-client.ts",
    "content": "export class RedisError extends Error {\n  name: string;\n\n  command: any;\n}\nexport class ReplyError extends RedisError {\n  previousErrors?: RedisError[];\n\n  code?: string;\n}\n\nexport enum AppTool {\n  Common = 'Common',\n  Browser = 'Browser',\n  CLI = 'CLI',\n  Workbench = 'Workbench',\n}\n\nexport class IRedisModule {\n  name: string;\n\n  ver: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/models/redis-cluster.ts",
    "content": "export interface IRedisClusterInfo {\n  cluster_state: string;\n  cluster_slots_assigned: string;\n  cluster_slots_ok: string;\n  cluster_slots_pfail: string;\n  cluster_slots_fail: string;\n  cluster_known_nodes: string;\n  cluster_size: string;\n  cluster_current_epoch: string;\n  cluster_my_epoch: string;\n  cluster_stats_messages_sent: string;\n  cluster_stats_messages_received: string;\n}\nexport interface IRedisClusterNodeAddress {\n  host: string;\n  port: number;\n}\n\nexport interface IRedisClusterNode extends IRedisClusterNodeAddress {\n  id: string;\n  replicaOf: string;\n  linkState: RedisClusterNodeLinkState;\n  slot: string;\n}\n\nexport enum RedisClusterNodeLinkState {\n  Connected = 'connected',\n  Disconnected = 'disconnected',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/ai-chat.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Post,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiTags, PickType } from '@nestjs/swagger';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\nimport { AiChatService } from 'src/modules/ai/chat/ai-chat.service';\nimport { AiChat } from 'src/modules/ai/chat/models';\nimport { SendAiChatMessageDto } from 'src/modules/ai/chat/dto/send.ai-chat.message.dto';\nimport { Response } from 'express';\n\n@ApiTags('AI')\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('ai/assistant/chats')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class AiChatController {\n  constructor(private readonly service: AiChatService) {}\n\n  @Post('/')\n  @ApiEndpoint({\n    description: 'Create a new chat',\n    statusCode: 200,\n    responses: [{ type: PickType(AiChat, ['id']) }],\n  })\n  async create(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<Partial<AiChat>> {\n    return this.service.create(sessionMetadata);\n  }\n\n  @Get('/:id')\n  @ApiEndpoint({\n    description: 'Get chat history',\n    statusCode: 200,\n    responses: [{ type: AiChat }],\n  })\n  async getHistory(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n  ) {\n    return this.service.getHistory(sessionMetadata, id);\n  }\n\n  @Post('/:id/messages')\n  @ApiEndpoint({\n    description: 'Post a message',\n    statusCode: 200,\n    responses: [{ type: String }],\n  })\n  async postMessage(\n    @Res() res: Response,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n    @Body() dto: SendAiChatMessageDto,\n  ) {\n    const stream = await this.service.postMessage(sessionMetadata, id, dto);\n    stream.pipe(res);\n  }\n\n  @Delete('/:id')\n  @ApiEndpoint({\n    description: 'Reset chat',\n    statusCode: 200,\n  })\n  async delete(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n  ): Promise<void> {\n    return this.service.delete(sessionMetadata, id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/ai-chat.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { AiChatController } from 'src/modules/ai/chat/ai-chat.controller';\nimport { AiChatService } from 'src/modules/ai/chat/ai-chat.service';\nimport { ConvAiProvider } from 'src/modules/ai/chat/providers/conv-ai.provider';\n\n@Module({\n  controllers: [AiChatController],\n  providers: [ConvAiProvider, AiChatService],\n})\nexport class AiChatModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/ai-chat.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockAiChat,\n  mockAiChatId,\n  mockAiResponseStream,\n  mockConvAiProvider,\n  mockSendAiChatMessageDto,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { AiChatService } from 'src/modules/ai/chat/ai-chat.service';\nimport { ConvAiProvider } from 'src/modules/ai/chat/providers/conv-ai.provider';\nimport { AiChat, AiChatMessage } from 'src/modules/ai/chat/models';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('AiChatService', () => {\n  let service: AiChatService;\n  let convAiProvider: MockType<ConvAiProvider>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AiChatService,\n        {\n          provide: ConvAiProvider,\n          useFactory: mockConvAiProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(AiChatService);\n    convAiProvider = module.get(ConvAiProvider);\n  });\n\n  describe('create', () => {\n    it('should create chat and return id', async () => {\n      const result = await service.create(mockSessionMetadata);\n      expect(result).toEqual({ id: mockAiChatId });\n      expect(result).toBeInstanceOf(AiChat);\n      expect(convAiProvider.auth).toHaveBeenCalledWith(mockSessionMetadata);\n    });\n  });\n  describe('postMessage', () => {\n    it('should send message and return stream as result', async () => {\n      const result = await service.postMessage(\n        mockSessionMetadata,\n        mockAiChatId,\n        mockSendAiChatMessageDto,\n      );\n      expect(result).toEqual(mockAiResponseStream);\n      expect(convAiProvider.postMessage).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockAiChatId,\n        mockSendAiChatMessageDto.content,\n      );\n    });\n  });\n  describe('getHistory', () => {\n    it('should get history', async () => {\n      const result = await service.getHistory(\n        mockSessionMetadata,\n        mockAiChatId,\n      );\n      expect(result).toEqual(mockAiChat);\n      expect(result).toBeInstanceOf(AiChat);\n      result.messages.forEach((message) => {\n        expect(message).toBeInstanceOf(AiChatMessage);\n      });\n      expect(convAiProvider.getHistory).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockAiChatId,\n      );\n    });\n  });\n  describe('delete', () => {\n    it('should delete chat', async () => {\n      expect(await service.delete(mockSessionMetadata, mockAiChatId)).toEqual(\n        undefined,\n      );\n      expect(convAiProvider.reset).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockAiChatId,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/ai-chat.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\nimport { ConvAiProvider } from 'src/modules/ai/chat/providers/conv-ai.provider';\nimport { plainToInstance } from 'class-transformer';\nimport { AiChat } from 'src/modules/ai/chat/models';\nimport { SendAiChatMessageDto } from 'src/modules/ai/chat/dto/send.ai-chat.message.dto';\n\n@Injectable()\nexport class AiChatService {\n  constructor(private readonly convAiProvider: ConvAiProvider) {}\n\n  async create(sessionMetadata: SessionMetadata): Promise<Partial<AiChat>> {\n    const id = await this.convAiProvider.auth(sessionMetadata);\n    return plainToInstance(AiChat, { id });\n  }\n\n  async postMessage(\n    sessionMetadata: SessionMetadata,\n    chatId: string,\n    dto: SendAiChatMessageDto,\n  ) {\n    return this.convAiProvider.postMessage(\n      sessionMetadata,\n      chatId,\n      dto.content,\n    );\n  }\n\n  async getHistory(\n    sessionMetadata: SessionMetadata,\n    chatId: string,\n  ): Promise<AiChat> {\n    return plainToInstance(AiChat, {\n      id: chatId,\n      messages: await this.convAiProvider.getHistory(sessionMetadata, chatId),\n    });\n  }\n\n  async delete(\n    sessionMetadata: SessionMetadata,\n    chatId: string,\n  ): Promise<void> {\n    return this.convAiProvider.reset(sessionMetadata, chatId);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/dto/send.ai-chat.message.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class SendAiChatMessageDto {\n  @ApiProperty({\n    description: 'Message content',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  content: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.bad-request.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class ConvAiBadRequestException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.BAD_REQUEST,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'ConvAiBadRequest',\n      errorCode: CustomErrorCodes.ConvAiBadRequest,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.error.handler.spec.ts",
    "content": "import { AxiosError } from 'axios';\nimport {\n  ConvAiBadRequestException,\n  ConvAiForbiddenException,\n  ConvAiUnauthorizedException,\n  ConvAiNotFoundException,\n  ConvAiInternalServerErrorException,\n} from 'src/modules/ai/chat/exceptions';\nimport { BadRequestException } from '@nestjs/common';\nimport {\n  mockAiChatAccessDeniedError,\n  mockAiChatBadRequestError,\n  mockAiChatInternalServerError,\n  mockAiChatNotFoundError,\n  mockAiChatUnauthorizedError,\n} from 'src/__mocks__';\nimport { wrapConvAiError } from './conv-ai.error.handler';\n\ndescribe('wrapConvAiError', () => {\n  it('Should return ConvAiBadRequestException of status code is 400', async () => {\n    const error = wrapConvAiError(mockAiChatBadRequestError);\n    expect(error).toBeInstanceOf(ConvAiBadRequestException);\n    expect(error).toEqual(\n      new ConvAiBadRequestException(mockAiChatBadRequestError.message),\n    );\n  });\n  it('Should return ConvAiUnauthorizedException of status code is 401', async () => {\n    const error = wrapConvAiError(mockAiChatUnauthorizedError);\n    expect(error).toBeInstanceOf(ConvAiUnauthorizedException);\n    expect(error).toEqual(\n      new ConvAiUnauthorizedException(mockAiChatUnauthorizedError.message),\n    );\n  });\n  it('Should return ConvAiForbiddenException of status code is 403', async () => {\n    const error = wrapConvAiError(mockAiChatAccessDeniedError);\n    expect(error).toBeInstanceOf(ConvAiForbiddenException);\n    expect(error).toEqual(\n      new ConvAiForbiddenException(mockAiChatAccessDeniedError.message),\n    );\n  });\n  it('Should return ConvAiNotFoundException of status code is 404', async () => {\n    const error = wrapConvAiError(mockAiChatNotFoundError);\n    expect(error).toBeInstanceOf(ConvAiNotFoundException);\n    expect(error).toEqual(\n      new ConvAiNotFoundException(mockAiChatNotFoundError.message),\n    );\n  });\n  it('Should return ConvAiInternalServerErrorException of status code is 500', async () => {\n    const error = wrapConvAiError(mockAiChatInternalServerError);\n    expect(error).toBeInstanceOf(ConvAiInternalServerErrorException);\n    expect(error).toEqual(\n      new ConvAiInternalServerErrorException(\n        mockAiChatInternalServerError.message,\n      ),\n    );\n  });\n  it('Should return ConvAiInternalServerErrorException by default', async () => {\n    const errorMessage = 'Unreachable error';\n    const mockAxiosError = {\n      response: {\n        status: 503,\n        data: {\n          message: errorMessage,\n        },\n      },\n    } as AxiosError;\n\n    const error = wrapConvAiError(mockAxiosError);\n    expect(error).toBeInstanceOf(ConvAiInternalServerErrorException);\n    expect(error).toEqual(new ConvAiInternalServerErrorException(errorMessage));\n  });\n  it('Should return ConvAiInternalServerErrorException if no response field', async () => {\n    const errorMessage = 'some other error';\n    const mockAxiosError = new Error(errorMessage) as AxiosError;\n\n    const error = wrapConvAiError(mockAxiosError);\n    expect(error).toBeInstanceOf(ConvAiInternalServerErrorException);\n    expect(error).toEqual(new ConvAiInternalServerErrorException(errorMessage));\n  });\n  it('Should return HttpException if passed children of it', async () => {\n    const mockAxiosError = new BadRequestException() as unknown as AxiosError;\n\n    const error = wrapConvAiError(mockAxiosError);\n    expect(error).toBeInstanceOf(BadRequestException);\n    expect(error).toEqual(mockAxiosError);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.error.handler.ts",
    "content": "import { AxiosError } from 'axios';\nimport { HttpException } from '@nestjs/common';\nimport {\n  ConvAiBadRequestException,\n  ConvAiForbiddenException,\n  ConvAiInternalServerErrorException,\n  ConvAiUnauthorizedException,\n} from 'src/modules/ai/chat/exceptions';\nimport { ConvAiNotFoundException } from 'src/modules/ai/chat/exceptions/conv-ai.not-found.exception';\n\nexport const wrapConvAiError = (\n  error: AxiosError,\n  message?: string,\n): HttpException => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  const { response } = error;\n  let errorMessage = message || error?.message;\n\n  if (!errorMessage) {\n    const data = response?.data as any;\n    errorMessage = data?.message;\n  }\n\n  if (response) {\n    const errorOptions = { cause: new Error(response?.data as string) };\n    switch (response?.status) {\n      case 401:\n        return new ConvAiUnauthorizedException(errorMessage, errorOptions);\n      case 403:\n        return new ConvAiForbiddenException(errorMessage, errorOptions);\n      case 400:\n        return new ConvAiBadRequestException(errorMessage, errorOptions);\n      case 404:\n        return new ConvAiNotFoundException(errorMessage, errorOptions);\n      default:\n        return new ConvAiInternalServerErrorException(\n          errorMessage,\n          errorOptions,\n        );\n    }\n  }\n\n  return new ConvAiInternalServerErrorException(errorMessage);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.forbidden.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class ConvAiForbiddenException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FORBIDDEN,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.FORBIDDEN,\n      error: 'ConvAiForbidden',\n      errorCode: CustomErrorCodes.ConvAiForbidden,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.internal-server-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class ConvAiInternalServerErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.INTERNAL_SERVER_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'ConvAiInternalServerError',\n      errorCode: CustomErrorCodes.ConvAiInternalServerError,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class ConvAiNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.NOT_FOUND,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'ConvAiNotFound',\n      errorCode: CustomErrorCodes.ConvAiNotFound,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/conv-ai.unauthorized.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class ConvAiUnauthorizedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.UNAUTHORIZED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'ConvAiUnauthorized',\n      errorCode: CustomErrorCodes.ConvAiUnauthorized,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/exceptions/index.ts",
    "content": "export * from './conv-ai.bad-request.exception';\nexport * from './conv-ai.error.handler';\nexport * from './conv-ai.forbidden.exception';\nexport * from './conv-ai.not-found.exception';\nexport * from './conv-ai.internal-server-error.exception';\nexport * from './conv-ai.unauthorized.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/models/ai-chat.message.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport enum AiChatMessageType {\n  HumanMessage = 'HumanMessage',\n  AiMessage = 'AIMessage',\n}\n\nexport class AiChatMessageContextRecord {\n  @Expose()\n  title: string;\n\n  @Expose()\n  category: string;\n}\n\nexport class AiChatMessage {\n  @ApiProperty({\n    enum: AiChatMessageType,\n  })\n  @Expose()\n  type: AiChatMessageType;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  content: string;\n\n  @ApiProperty({\n    type: Object,\n    example: {\n      'https://redis.io/docs/about': {\n        title: 'Introduction to Redis',\n        category: 'oss',\n      },\n      'https://redis.io/docs/get-started': {\n        title: 'Quick starts',\n        category: 'oss',\n      },\n    },\n  })\n  @Expose()\n  context: Record<string, AiChatMessageContextRecord>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/models/ai-chat.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport { AiChatMessage } from 'src/modules/ai/chat/models/ai-chat.message';\n\nexport class AiChat {\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    isArray: true,\n    type: () => AiChatMessage,\n  })\n  @Type(() => AiChatMessage)\n  @Expose()\n  messages: AiChatMessage[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/models/index.ts",
    "content": "export * from './ai-chat';\nexport * from './ai-chat.message';\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/providers/conv-ai.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  getMockedReadableStream,\n  mockAiChatId,\n  mockAiChatUnauthorizedError,\n  mockAiHistoryApiResponse,\n  mockHumanMessage1Response,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { ConvAiProvider } from 'src/modules/ai/chat/providers/conv-ai.provider';\nimport { ConvAiUnauthorizedException } from 'src/modules/ai/chat/exceptions';\nimport config, { Config } from 'src/utils/config';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\nconst aiConfig = config.get('ai') as Config['ai'];\n\ndescribe('ConvAiProvider', () => {\n  let service: ConvAiProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ConvAiProvider],\n    }).compile();\n\n    service = module.get(ConvAiProvider);\n  });\n\n  describe('auth', () => {\n    it('get session id', async () => {\n      mockedAxios.post.mockResolvedValue({\n        status: 200,\n        data: {\n          convai_session_id: mockAiChatId,\n        },\n      });\n\n      expect(await service.auth(mockSessionMetadata)).toEqual(mockAiChatId);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        '/auth',\n        {},\n        {\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'kb-tokens': aiConfig.convAiToken,\n          },\n        },\n      );\n    });\n\n    it('throw ConvAiUnauthorized exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockAiChatUnauthorizedError);\n\n      await expect(service.auth(mockSessionMetadata)).rejects.toThrow(\n        ConvAiUnauthorizedException,\n      );\n    });\n  });\n  describe('getHistory', () => {\n    it('should get chat history chat history', async () => {\n      mockedAxios.get.mockResolvedValue({\n        status: 200,\n        data: mockAiHistoryApiResponse,\n      });\n\n      expect(\n        await service.getHistory(mockSessionMetadata, mockAiChatId),\n      ).toEqual(mockAiHistoryApiResponse);\n      expect(mockedAxios.get).toHaveBeenCalledWith('/history', {\n        headers: {\n          'session-id': mockAiChatId,\n        },\n      });\n    });\n    it('throw ConvAiUnauthorized exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockAiChatUnauthorizedError);\n\n      await expect(\n        service.getHistory(mockSessionMetadata, mockAiChatId),\n      ).rejects.toThrow(ConvAiUnauthorizedException);\n    });\n  });\n  describe('postMessage', () => {\n    it('should post send a message and get stream as result', async () => {\n      const mockStream = getMockedReadableStream();\n      mockedAxios.post.mockResolvedValue({\n        status: 200,\n        data: mockStream,\n      });\n\n      expect(\n        await service.postMessage(\n          mockSessionMetadata,\n          mockAiChatId,\n          mockHumanMessage1Response.content,\n        ),\n      ).toEqual(mockStream);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        '/chat',\n        {},\n        {\n          params: {\n            q: mockHumanMessage1Response.content,\n          },\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'session-id': mockAiChatId,\n          },\n          responseType: 'stream',\n        },\n      );\n    });\n    it('throw ConvAiUnauthorized exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockAiChatUnauthorizedError);\n\n      await expect(\n        service.postMessage(\n          mockSessionMetadata,\n          mockAiChatId,\n          mockHumanMessage1Response.content,\n        ),\n      ).rejects.toThrow(ConvAiUnauthorizedException);\n    });\n  });\n  describe('reset', () => {\n    it('reset chat history', async () => {\n      mockedAxios.post.mockResolvedValue({\n        status: 200,\n        data: '',\n      });\n\n      expect(await service.reset(mockSessionMetadata, mockAiChatId)).toEqual(\n        undefined,\n      );\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        '/reset',\n        {},\n        {\n          headers: {\n            'session-id': mockAiChatId,\n          },\n        },\n      );\n    });\n    it('throw ConvAiUnauthorized exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockAiChatUnauthorizedError);\n\n      await expect(\n        service.reset(mockSessionMetadata, mockAiChatId),\n      ).rejects.toThrow(ConvAiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/chat/providers/conv-ai.provider.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport config, { Config } from 'src/utils/config';\nimport axios from 'axios';\nimport { wrapConvAiError } from 'src/modules/ai/chat/exceptions';\nimport { Stream } from 'typeorm';\n\nconst aiConfig = config.get('ai') as Config['ai'];\n\nexport class ConvAiProvider {\n  protected api = axios.create({\n    baseURL: aiConfig.convAiApiUrl,\n  });\n\n  async auth(_sessionMetadata: SessionMetadata): Promise<string> {\n    try {\n      const { data } = await this.api.post(\n        '/auth',\n        {},\n        {\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'kb-tokens': aiConfig.convAiToken,\n          },\n        },\n      );\n\n      return data.convai_session_id;\n    } catch (e) {\n      throw wrapConvAiError(e);\n    }\n  }\n\n  async getHistory(\n    _sessionMetadata: SessionMetadata,\n    chatId: string,\n  ): Promise<object[]> {\n    try {\n      const { data } = await this.api.get('/history', {\n        headers: {\n          'session-id': chatId,\n        },\n      });\n\n      return data;\n    } catch (e) {\n      throw wrapConvAiError(e);\n    }\n  }\n\n  async postMessage(\n    _sessionMetadata: SessionMetadata,\n    chatId: string,\n    message: string,\n  ): Promise<Stream> {\n    const messageTransformed = message.replace(/(\\r\\n|\\n|\\r)/gm, ' ').trim();\n    try {\n      const { data } = await this.api.post(\n        '/chat',\n        {},\n        {\n          params: {\n            q: messageTransformed,\n          },\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            'session-id': chatId,\n          },\n          responseType: 'stream',\n        },\n      );\n\n      return data;\n    } catch (e) {\n      throw wrapConvAiError(e);\n    }\n  }\n\n  async reset(\n    _sessionMetadata: SessionMetadata,\n    chatId: string,\n  ): Promise<void> {\n    try {\n      await this.api.post(\n        '/reset',\n        {},\n        {\n          headers: {\n            'session-id': chatId,\n          },\n        },\n      );\n    } catch (e) {\n      throw wrapConvAiError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/ai-query.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Logger,\n  Param,\n  Post,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiTags } from '@nestjs/swagger';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\nimport { AiQueryService } from 'src/modules/ai/query/ai-query.service';\nimport { SendAiQueryMessageDto } from 'src/modules/ai/query/dto/send.ai-query.message.dto';\nimport { Response } from 'express';\n\n@ApiTags('AI')\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('ai/expert/:id/messages')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class AiQueryController {\n  private readonly logger = new Logger('AiQueryController');\n\n  constructor(private readonly service: AiQueryService) {}\n\n  @Post()\n  @ApiEndpoint({\n    description: 'Generate new query',\n    statusCode: 200,\n    responses: [{ type: String }],\n  })\n  async streamQuestion(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') databaseId: string,\n    @Body() dto: SendAiQueryMessageDto,\n    @Res() res: Response,\n  ) {\n    await this.service.stream(sessionMetadata, databaseId, dto, res);\n  }\n\n  @Get()\n  @ApiEndpoint({\n    description: 'Generate new query',\n    statusCode: 200,\n    responses: [{ type: String }],\n  })\n  async getHistory(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') databaseId: string,\n  ) {\n    return this.service.getHistory(sessionMetadata, databaseId);\n  }\n\n  @Delete()\n  @ApiEndpoint({\n    description: 'Generate new query',\n    statusCode: 200,\n    responses: [{ type: String }],\n  })\n  async clearHistory(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') databaseId: string,\n  ) {\n    return this.service.clearHistory(sessionMetadata, databaseId);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/ai-query.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { AiQueryController } from 'src/modules/ai/query/ai-query.controller';\nimport { AiQueryProvider } from 'src/modules/ai/query/providers/ai-query.provider';\nimport { AiQueryService } from 'src/modules/ai/query/ai-query.service';\nimport { AiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/ai-query-auth.provider';\nimport { LocalAiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/local.ai-query-auth.provider';\nimport { AiQueryMessageRepository } from 'src/modules/ai/query/repositories/ai-query.message.repository';\nimport { LocalAiQueryMessageRepository } from 'src/modules/ai/query/repositories/local.ai-query.message.repository';\nimport { AiQueryContextRepository } from 'src/modules/ai/query/repositories/ai-query.context.repository';\nimport { InMemoryAiQueryContextRepository } from 'src/modules/ai/query/repositories/in-memory.ai-query.context.repository';\n\n@Module({})\nexport class AiQueryModule {\n  static register(\n    aiQueryAuthProvider: Type<AiQueryAuthProvider> = LocalAiQueryAuthProvider,\n    aiQueryMessageRepository: Type<AiQueryMessageRepository> = LocalAiQueryMessageRepository,\n    aiQueryContextRepository: Type<AiQueryContextRepository> = InMemoryAiQueryContextRepository,\n  ) {\n    return {\n      module: AiQueryModule,\n      controllers: [AiQueryController],\n      providers: [\n        AiQueryProvider,\n        AiQueryService,\n        {\n          provide: AiQueryAuthProvider,\n          useClass: aiQueryAuthProvider,\n        },\n        {\n          provide: AiQueryMessageRepository,\n          useClass: aiQueryMessageRepository,\n        },\n        {\n          provide: AiQueryContextRepository,\n          useClass: aiQueryContextRepository,\n        },\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/ai-query.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockAiQueryAiIntermediateStep,\n  mockAiQueryAiIntermediateStep2,\n  mockAiQueryAiResponse,\n  mockAiQueryContextRepository,\n  mockAiQueryDatabaseId,\n  mockAiQueryHistory,\n  mockAiQueryIndex,\n  mockAiQueryIndexContext,\n  mockAiQueryMessageRepository,\n  mockAiQueryProvider,\n  mockCloudUserApiService,\n  mockDatabaseClientFactory,\n  mockSendAiChatMessageDto,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalAiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/local.ai-query-auth.provider';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { AiQueryService } from 'src/modules/ai/query/ai-query.service';\nimport { AiQueryProvider } from 'src/modules/ai/query/providers/ai-query.provider';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { AiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/ai-query-auth.provider';\nimport { AiQueryMessageRepository } from 'src/modules/ai/query/repositories/ai-query.message.repository';\nimport { AiQueryContextRepository } from 'src/modules/ai/query/repositories/ai-query.context.repository';\nimport {\n  AiQueryServerErrors,\n  AiQueryWsEvents,\n} from 'src/modules/ai/query/models';\nimport { Server } from 'socket.io';\nimport { io } from 'socket.io-client';\nimport { createServer } from 'http';\nimport { AddressInfo } from 'net';\nimport { AiQueryRateLimitRequestException } from 'src/modules/ai/query/exceptions';\nimport * as ContextUtil from './utils/context.util';\n\ndescribe('AiQueryService', () => {\n  let wsServer;\n  let serverSocket;\n  let clientSocket;\n  let mockResponse;\n  let httpServer;\n  let service: AiQueryService;\n  let aiQueryProvider: MockType<AiQueryProvider>;\n  let cloudUserApiService: MockType<CloudUserApiService>;\n  let aiQueryContextRepository: MockType<AiQueryContextRepository>;\n\n  beforeAll((done) => {\n    httpServer = createServer();\n    wsServer = new Server(httpServer);\n    httpServer.listen(() => {\n      done();\n    });\n  });\n\n  afterAll(() => {\n    clientSocket.disconnect();\n    serverSocket.disconnect();\n    wsServer.close();\n    httpServer?.stop?.();\n  });\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AiQueryService,\n        {\n          provide: AiQueryProvider,\n          useFactory: mockAiQueryProvider,\n        },\n        {\n          provide: AiQueryAuthProvider,\n          useClass: LocalAiQueryAuthProvider,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: AiQueryMessageRepository,\n          useFactory: mockAiQueryMessageRepository,\n        },\n        {\n          provide: AiQueryContextRepository,\n          useFactory: mockAiQueryContextRepository,\n        },\n        {\n          provide: AiQueryMessageRepository,\n          useFactory: mockAiQueryMessageRepository,\n        },\n        {\n          provide: CloudUserApiService,\n          useFactory: mockCloudUserApiService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(AiQueryService);\n    aiQueryProvider = module.get(AiQueryProvider);\n    cloudUserApiService = module.get(CloudUserApiService);\n    aiQueryContextRepository = module.get(AiQueryContextRepository);\n  });\n\n  describe('stream', () => {\n    let getIndexContextSpy;\n\n    beforeEach((done) => {\n      mockResponse = {\n        content: '',\n        write(chunk) {\n          this.content += chunk;\n        },\n        end: jest.fn(),\n        reset() {\n          this.content = '';\n        },\n      };\n\n      getIndexContextSpy = jest.spyOn(ContextUtil, 'getIndexContext');\n      getIndexContextSpy.mockResolvedValue(mockAiQueryIndexContext);\n\n      clientSocket = io(\n        `http://localhost:${(httpServer.address() as AddressInfo).port}`,\n      );\n      wsServer.on('connection', (socket) => {\n        serverSocket = socket;\n      });\n\n      aiQueryProvider.getSocket.mockResolvedValue(clientSocket);\n\n      clientSocket.on('connect', done);\n    });\n\n    afterEach(() => {\n      expect(clientSocket.connected).toEqual(false);\n    });\n\n    it('should stream an answer', async () => {\n      serverSocket.once(\n        AiQueryWsEvents.STREAM,\n        (_content, _context, _history, _opts, cb) => {\n          serverSocket.emit(\n            AiQueryWsEvents.REPLY_CHUNK,\n            mockAiQueryAiResponse.content,\n          );\n          cb({ status: 'ok' });\n        },\n      );\n\n      await expect(\n        service.stream(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockSendAiChatMessageDto,\n          mockResponse as any,\n        ),\n      ).resolves.toEqual(undefined);\n\n      expect(mockResponse.content).toEqual(mockAiQueryAiResponse.content);\n    });\n    it('should not fail in case of empty ack', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        'aggregate',\n        'results',\n      ]);\n      serverSocket.once(\n        AiQueryWsEvents.STREAM,\n        (_content, _context, _history, _opts, cb) => {\n          serverSocket.emit(\n            AiQueryWsEvents.TOOL_CALL,\n            JSON.stringify(mockAiQueryAiIntermediateStep),\n          );\n          serverSocket.emit(\n            AiQueryWsEvents.TOOL_REPLY,\n            JSON.stringify(mockAiQueryAiIntermediateStep2),\n          );\n          serverSocket\n            .emitWithAck(AiQueryWsEvents.GET_INDEX, mockAiQueryIndex)\n            .then((indexContext) => {\n              expect(indexContext).toEqual(mockAiQueryIndexContext);\n\n              return serverSocket.emitWithAck(AiQueryWsEvents.RUN_QUERY, [\n                'ft.aggregate',\n              ]);\n            })\n            .then((queryResult) => {\n              expect(queryResult).toEqual(['aggregate', 'results']);\n              serverSocket.emit(\n                AiQueryWsEvents.REPLY_CHUNK,\n                mockAiQueryAiResponse.content,\n              );\n              cb();\n            });\n        },\n      );\n\n      await expect(\n        service.stream(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockSendAiChatMessageDto,\n          mockResponse as any,\n        ),\n      ).resolves.toEqual(undefined);\n\n      expect(mockResponse.content).toEqual(mockAiQueryAiResponse.content);\n    });\n    it('should calculate index and get from cache on 2nd attempt and send error on 3d attempt', async () => {\n      serverSocket.once(\n        AiQueryWsEvents.STREAM,\n        (_content, _context, _history, _opts, cb) => {\n          aiQueryContextRepository.getIndexContext.mockResolvedValueOnce(null);\n          aiQueryContextRepository.getIndexContext.mockResolvedValueOnce({\n            cached: 'cached',\n          });\n          aiQueryContextRepository.getIndexContext.mockRejectedValueOnce(\n            new Error('Something is wrong'),\n          );\n          serverSocket\n            .emitWithAck(AiQueryWsEvents.GET_INDEX, mockAiQueryIndex)\n            .then((indexContext) => {\n              // calculated\n              expect(indexContext).toEqual(mockAiQueryIndexContext);\n\n              return serverSocket.emitWithAck(\n                AiQueryWsEvents.GET_INDEX,\n                mockAiQueryIndex,\n              );\n            })\n            .then((indexContext) => {\n              // cached\n              expect(indexContext).toEqual({ cached: 'cached' });\n\n              return serverSocket.emitWithAck(\n                AiQueryWsEvents.GET_INDEX,\n                mockAiQueryIndex,\n              );\n            })\n            .then((error) => {\n              // error\n              expect(error).toEqual('Something is wrong');\n              serverSocket.emit(\n                AiQueryWsEvents.REPLY_CHUNK,\n                mockAiQueryAiResponse.content,\n              );\n              cb();\n            });\n        },\n      );\n\n      await expect(\n        service.stream(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockSendAiChatMessageDto,\n          mockResponse as any,\n        ),\n      ).resolves.toEqual(undefined);\n\n      expect(mockResponse.content).toEqual(mockAiQueryAiResponse.content);\n    });\n    it('should return errors for run queries', async () => {\n      serverSocket.once(\n        AiQueryWsEvents.STREAM,\n        (_content, _context, _history, _opts, cb) => {\n          when(mockStandaloneRedisClient.sendCommand)\n            .calledWith(\n              expect.arrayContaining(['FT.AGGREGATE', 'BAD ARGS']),\n              expect.anything(),\n            )\n            .mockRejectedValue(new Error('ERR: invalid command syntax'));\n\n          serverSocket\n            .emitWithAck(AiQueryWsEvents.RUN_QUERY, ['FLUSHALL'])\n            .then((result) => {\n              // error due to white list\n              expect(result).toEqual('-ERR: This command is not allowed');\n\n              return serverSocket.emitWithAck(AiQueryWsEvents.RUN_QUERY, null);\n            })\n            .then((result) => {\n              // error due to white list when no query was sent\n              expect(result).toEqual('-ERR: This command is not allowed');\n\n              return serverSocket.emitWithAck(AiQueryWsEvents.RUN_QUERY, [\n                'FT.AGGREGATE',\n                'BAD ARGS',\n              ]);\n            })\n            .then((result) => {\n              // execution error\n              expect(result).toEqual('ERR: invalid command syntax');\n              serverSocket.emit(\n                AiQueryWsEvents.REPLY_CHUNK,\n                mockAiQueryAiResponse.content,\n              );\n              cb();\n            });\n        },\n      );\n\n      await expect(\n        service.stream(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockSendAiChatMessageDto,\n          mockResponse as any,\n        ),\n      ).resolves.toEqual(undefined);\n\n      expect(mockResponse.content).toEqual(mockAiQueryAiResponse.content);\n    });\n    it('should fail when ack has error property', async () => {\n      serverSocket.once(\n        AiQueryWsEvents.STREAM,\n        (_content, _context, _history, _opts, cb) => {\n          cb({ error: { error: AiQueryServerErrors.RateLimitRequest } });\n        },\n      );\n\n      await expect(\n        service.stream(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockSendAiChatMessageDto,\n          mockResponse as any,\n        ),\n      ).rejects.toThrow(AiQueryRateLimitRequestException);\n    });\n  });\n\n  describe('getHistory', () => {\n    it('should get history', async () => {\n      expect(\n        await service.getHistory(mockSessionMetadata, mockAiQueryDatabaseId),\n      ).toEqual(mockAiQueryHistory);\n    });\n\n    it('should get history from 2nd attempt', async () => {\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n\n      expect(\n        await service.getHistory(mockSessionMetadata, mockAiQueryDatabaseId),\n      ).toEqual(mockAiQueryHistory);\n    });\n\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n\n      await expect(\n        service.getHistory(mockSessionMetadata, mockAiQueryDatabaseId),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n\n  describe('clearHistory', () => {\n    it('should clear history', async () => {\n      expect(\n        await service.clearHistory(mockSessionMetadata, mockAiQueryDatabaseId),\n      ).toEqual(undefined);\n    });\n\n    it('should clear history from 2nd attempt', async () => {\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n\n      expect(\n        await service.clearHistory(mockSessionMetadata, mockAiQueryDatabaseId),\n      ).toEqual(undefined);\n    });\n\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n\n      await expect(\n        service.clearHistory(mockSessionMetadata, mockAiQueryDatabaseId),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/ai-query.service.ts",
    "content": "import { isArray } from 'lodash';\nimport { v4 as uuidv4 } from 'uuid';\nimport { Socket } from 'socket.io-client';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { ClientContext, SessionMetadata } from 'src/common/models';\nimport { AiQueryProvider } from 'src/modules/ai/query/providers/ai-query.provider';\nimport { SendAiQueryMessageDto } from 'src/modules/ai/query/dto/send.ai-query.message.dto';\nimport { wrapAiQueryError } from 'src/modules/ai/query/exceptions';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  getFullDbContext,\n  getIndexContext,\n} from 'src/modules/ai/query/utils/context.util';\nimport { Response } from 'express';\nimport {\n  AiQueryMessage,\n  AiQueryMessageType,\n  AiQueryMessageRole,\n  AiQueryWsEvents,\n  AiQueryIntermediateStepType,\n  AiQueryIntermediateStep,\n} from 'src/modules/ai/query/models';\nimport { AiQueryMessageRepository } from 'src/modules/ai/query/repositories/ai-query.message.repository';\nimport { AiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/ai-query-auth.provider';\nimport { classToClass, Config } from 'src/utils';\nimport { plainToInstance } from 'class-transformer';\nimport { AiQueryContextRepository } from 'src/modules/ai/query/repositories/ai-query.context.repository';\nimport config from 'src/utils/config';\n\nconst aiConfig = config.get('ai') as Config['ai'];\n\nconst COMMANDS_WHITELIST = {\n  'ft.search': true,\n  'ft.aggregate': true,\n};\n\n@Injectable()\nexport class AiQueryService {\n  private readonly logger = new Logger('AiQueryService');\n\n  constructor(\n    private readonly aiQueryProvider: AiQueryProvider,\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly aiQueryMessageRepository: AiQueryMessageRepository,\n    private readonly aiQueryAuthProvider: AiQueryAuthProvider,\n    private readonly aiQueryContextRepository: AiQueryContextRepository,\n  ) {}\n\n  static prepareHistoryIntermediateSteps(\n    message: AiQueryMessage,\n  ): [AiQueryMessageRole, string][] {\n    const steps = [];\n    message.steps.forEach((step) => {\n      switch (step.type) {\n        case AiQueryIntermediateStepType.TOOL:\n          steps.push([AiQueryMessageRole.TOOL, step.data]);\n          break;\n        case AiQueryIntermediateStepType.TOOL_CALL:\n          steps.push([AiQueryMessageRole.TOOL_CALL, step.data]);\n          break;\n        default:\n        // ignore\n      }\n    });\n\n    return steps;\n  }\n\n  static limitQueryReply(reply: any, maxResults = aiConfig.queryMaxResults) {\n    let results = reply;\n    if (isArray(reply)) {\n      results = reply.slice(0, maxResults);\n      results = results.map((nested) => {\n        if (Array.isArray(nested)) {\n          AiQueryService.limitQueryReply(\n            nested,\n            aiConfig.queryMaxNestedElements,\n          );\n        }\n        return nested;\n      });\n      return results;\n    }\n\n    return results;\n  }\n\n  static prepareHistory(messages: AiQueryMessage[]): string[][] {\n    const history = [];\n    messages.forEach((message) => {\n      switch (message.type) {\n        case AiQueryMessageType.AiMessage:\n          history.push([AiQueryMessageRole.AI, message.content]);\n          if (message.steps.length) {\n            history.push(\n              ...AiQueryService.prepareHistoryIntermediateSteps(message),\n            );\n          }\n          break;\n        case AiQueryMessageType.HumanMessage:\n          history.push([AiQueryMessageRole.HUMAN, message.content]);\n          break;\n        default:\n        // ignore\n      }\n    });\n\n    return history;\n  }\n\n  static getConversationId(messages: AiQueryMessage[]): string {\n    return messages?.[messages.length - 1]?.conversationId || uuidv4();\n  }\n\n  async stream(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    dto: SendAiQueryMessageDto,\n    res: Response,\n  ) {\n    return this.aiQueryAuthProvider.callWithAuthRetry(\n      sessionMetadata,\n      async () => {\n        let socket: Socket;\n\n        try {\n          const auth =\n            await this.aiQueryAuthProvider.getAuthData(sessionMetadata);\n          const history = await this.aiQueryMessageRepository.list(\n            sessionMetadata,\n            databaseId,\n            auth.accountId,\n          );\n          const conversationId = AiQueryService.getConversationId(history);\n\n          const client = await this.databaseClientFactory.getOrCreateClient({\n            sessionMetadata,\n            databaseId,\n            context: ClientContext.AI,\n          });\n\n          let context = await this.aiQueryContextRepository.getFullDbContext(\n            sessionMetadata,\n            databaseId,\n            auth.accountId,\n          );\n\n          if (!context) {\n            context = await this.aiQueryContextRepository.setFullDbContext(\n              sessionMetadata,\n              databaseId,\n              auth.accountId,\n              await getFullDbContext(client),\n            );\n          }\n\n          const question = classToClass(AiQueryMessage, {\n            type: AiQueryMessageType.HumanMessage,\n            content: dto.content,\n            databaseId,\n            conversationId,\n            accountId: auth.accountId,\n            createdAt: new Date(),\n          });\n\n          const answer = classToClass(AiQueryMessage, {\n            type: AiQueryMessageType.AiMessage,\n            content: '',\n            databaseId,\n            conversationId,\n            accountId: auth.accountId,\n          });\n\n          socket = await this.aiQueryProvider.getSocket(auth);\n\n          socket.on(AiQueryWsEvents.REPLY_CHUNK, (chunk) => {\n            answer.content += chunk;\n            res.write(chunk);\n          });\n\n          socket.on(AiQueryWsEvents.GET_INDEX, async (index, cb) => {\n            try {\n              const indexContext =\n                await this.aiQueryContextRepository.getIndexContext(\n                  sessionMetadata,\n                  databaseId,\n                  auth.accountId,\n                  index,\n                );\n\n              if (!indexContext) {\n                return cb(\n                  await this.aiQueryContextRepository.setIndexContext(\n                    sessionMetadata,\n                    databaseId,\n                    auth.accountId,\n                    index,\n                    await getIndexContext(client, index),\n                  ),\n                );\n              }\n\n              return cb(indexContext);\n            } catch (e) {\n              this.logger.warn(\n                'Unable to create index context',\n                e,\n                sessionMetadata,\n              );\n              return cb(e.message);\n            }\n          });\n\n          socket.on(AiQueryWsEvents.RUN_QUERY, async (data, cb) => {\n            try {\n              if (!COMMANDS_WHITELIST[(data?.[0] || '').toLowerCase()]) {\n                return cb('-ERR: This command is not allowed');\n              }\n\n              return cb(\n                await client.sendCommand(data, { replyEncoding: 'utf8' }),\n              );\n            } catch (e) {\n              this.logger.warn('Query execution error', e, sessionMetadata);\n              return cb(e.message);\n            }\n          });\n\n          socket.on(AiQueryWsEvents.TOOL_CALL, async (data) => {\n            answer.steps.push(\n              plainToInstance(AiQueryIntermediateStep, {\n                type: AiQueryIntermediateStepType.TOOL_CALL,\n                data,\n              }),\n            );\n          });\n\n          socket.on(AiQueryWsEvents.TOOL_REPLY, async (data) => {\n            answer.steps.push(\n              plainToInstance(AiQueryIntermediateStep, {\n                type: AiQueryIntermediateStepType.TOOL,\n                data,\n              }),\n            );\n          });\n\n          await new Promise((resolve, reject) => {\n            socket.on(AiQueryWsEvents.ERROR, async (error) => {\n              reject(error);\n            });\n\n            socket\n              .emitWithAck(\n                AiQueryWsEvents.STREAM,\n                dto.content,\n                context,\n                AiQueryService.prepareHistory(history),\n                {\n                  conversationId,\n                },\n              )\n              .then((ack) => {\n                if (ack?.error) {\n                  return reject(ack.error);\n                }\n\n                return resolve(ack);\n              })\n              .catch(reject);\n          });\n          socket.close();\n          await this.aiQueryMessageRepository.createMany(sessionMetadata, [\n            question,\n            answer,\n          ]);\n\n          return res.end();\n        } catch (e) {\n          socket?.close?.();\n          throw wrapAiQueryError(e, 'Unable to send the question');\n        }\n      },\n    );\n  }\n\n  async getHistory(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<AiQueryMessage[]> {\n    return this.aiQueryAuthProvider.callWithAuthRetry(\n      sessionMetadata,\n      async () => {\n        try {\n          const auth =\n            await this.aiQueryAuthProvider.getAuthData(sessionMetadata);\n          return await this.aiQueryMessageRepository.list(\n            sessionMetadata,\n            databaseId,\n            auth.accountId,\n          );\n        } catch (e) {\n          throw wrapAiQueryError(e, 'Unable to get history');\n        }\n      },\n    );\n  }\n\n  async clearHistory(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<void> {\n    return this.aiQueryAuthProvider.callWithAuthRetry(\n      sessionMetadata,\n      async () => {\n        try {\n          const auth =\n            await this.aiQueryAuthProvider.getAuthData(sessionMetadata);\n\n          await this.aiQueryContextRepository.reset(\n            sessionMetadata,\n            databaseId,\n            auth.accountId,\n          );\n\n          return this.aiQueryMessageRepository.clearHistory(\n            sessionMetadata,\n            databaseId,\n            auth.accountId,\n          );\n        } catch (e) {\n          throw wrapAiQueryError(e, 'Unable to clear history');\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/dto/send.ai-query.message.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class SendAiQueryMessageDto {\n  @ApiProperty({\n    description: 'Message content',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  content: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/entities/ai-query.message.entity.ts",
    "content": "import { Expose } from 'class-transformer';\nimport {\n  Column,\n  CreateDateColumn,\n  Entity,\n  Index,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('ai_query_message')\nexport class AiQueryMessageEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Index()\n  @Expose()\n  databaseId: string;\n\n  @Column({ nullable: false })\n  @Index()\n  @Expose()\n  accountId: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  conversationId: string;\n\n  @Column({ nullable: false })\n  @Expose()\n  type: string;\n\n  @Column({ nullable: false, type: 'blob' })\n  @Expose()\n  content: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  steps: string;\n\n  @CreateDateColumn()\n  @Index()\n  @Expose()\n  createdAt: Date;\n\n  @Column({ nullable: true })\n  encryption: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/a-queryi.error.handler.spec.ts",
    "content": "import { AxiosError } from 'axios';\nimport { BadRequestException, HttpStatus } from '@nestjs/common';\nimport {\n  mockAiChatAccessDeniedError,\n  mockAiChatBadRequestError,\n  mockAiChatInternalServerError,\n  mockAiChatUnauthorizedError,\n} from 'src/__mocks__';\nimport {\n  AiQueryBadRequestException,\n  AiQueryForbiddenException,\n  AiQueryInternalServerErrorException,\n  AiQueryNotFoundException,\n  AiQueryRateLimitMaxTokensException,\n  AiQueryRateLimitRequestException,\n  AiQueryRateLimitTokenException,\n  AiQueryUnauthorizedException,\n} from 'src/modules/ai/query/exceptions';\nimport { AiQueryServerErrors } from 'src/modules/ai/query/models';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { wrapAiQueryError } from './ai-query.error.handler';\n\ndescribe('wrapConvAiError', () => {\n  it('Should return AiQueryBadRequestException of status code is 400', async () => {\n    const error = wrapAiQueryError(mockAiChatBadRequestError);\n    expect(error).toBeInstanceOf(AiQueryBadRequestException);\n    expect(error).toEqual(new AiQueryBadRequestException());\n  });\n  it('Should return AiQueryUnauthorizedException of status code is 401', async () => {\n    const error = wrapAiQueryError(mockAiChatUnauthorizedError);\n    expect(error).toBeInstanceOf(AiQueryUnauthorizedException);\n    expect(error).toEqual(new AiQueryUnauthorizedException());\n  });\n  it('Should return AiQueryForbiddenException of status code is 403', async () => {\n    const error = wrapAiQueryError(mockAiChatAccessDeniedError);\n    expect(error).toBeInstanceOf(AiQueryForbiddenException);\n    expect(error).toEqual(new AiQueryForbiddenException());\n  });\n  it('Should return AiQueryNotFoundException if statusCode is 404', async () => {\n    const error = wrapAiQueryError({\n      message: 'Requested resource was not found',\n      response: {\n        statusCode: 404,\n      },\n    });\n    expect(error).toBeInstanceOf(AiQueryNotFoundException);\n    expect(error).toEqual(new AiQueryNotFoundException());\n  });\n  it('Should return AiQueryInternalServerErrorException of status code is 500', async () => {\n    const error = wrapAiQueryError(mockAiChatInternalServerError);\n    expect(error).toBeInstanceOf(AiQueryInternalServerErrorException);\n    expect(error).toEqual(new AiQueryInternalServerErrorException());\n  });\n  it('Should return AiQueryInternalServerErrorException by default', async () => {\n    const mockAxiosError = {\n      response: {\n        status: 503,\n        data: {\n          message: 'Unreachable error',\n        },\n      },\n    } as AxiosError;\n\n    const error = wrapAiQueryError(mockAxiosError);\n    expect(error).toBeInstanceOf(AiQueryInternalServerErrorException);\n    expect(error).toEqual(new AiQueryInternalServerErrorException());\n  });\n  it('Should return AiQueryInternalServerErrorException if no response field', async () => {\n    const mockAxiosError = new Error('some other error') as AxiosError;\n\n    const error = wrapAiQueryError(mockAxiosError);\n    expect(error).toBeInstanceOf(AiQueryInternalServerErrorException);\n    expect(error).toEqual(new AiQueryInternalServerErrorException());\n  });\n  it('Should return HttpException if passed children of it', async () => {\n    const mockAxiosError = new BadRequestException() as unknown as AxiosError;\n\n    const error = wrapAiQueryError(mockAxiosError);\n    expect(error).toBeInstanceOf(BadRequestException);\n    expect(error).toEqual(mockAxiosError);\n  });\n  it('Should throw AiQueryRateLimitTokenException', async () => {\n    const error = wrapAiQueryError({\n      error: AiQueryServerErrors.RateLimitToken,\n      message: 'Rate limit of user for tokens exceeded',\n      data: { limiterType: 'token', limiterKind: 'user', limiterSeconds: 10 },\n    });\n    expect(error).toBeInstanceOf(AiQueryRateLimitTokenException);\n    expect(error.getResponse()).toEqual({\n      message: 'Rate limit of user for tokens exceeded',\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitToken',\n      errorCode: CustomErrorCodes.QueryAiRateLimitToken,\n      details: {\n        limiterType: 'token',\n        limiterKind: 'user',\n        limiterSeconds: 10,\n      },\n    });\n  });\n  it('Should throw AiQueryRateLimitTokenException with default message', async () => {\n    const error = wrapAiQueryError({\n      error: AiQueryServerErrors.RateLimitToken,\n      data: { limiterType: 'token', limiterKind: 'user', limiterSeconds: 10 },\n    });\n    expect(error).toBeInstanceOf(AiQueryRateLimitTokenException);\n    expect(error.getResponse()).toEqual({\n      message: ERROR_MESSAGES.AI_QUERY_TOKEN_RATE_LIMIT,\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitToken',\n      errorCode: CustomErrorCodes.QueryAiRateLimitToken,\n      details: {\n        limiterType: 'token',\n        limiterKind: 'user',\n        limiterSeconds: 10,\n      },\n    });\n  });\n  it('Should throw AiQueryRateLimitRequestException', async () => {\n    const error = wrapAiQueryError({\n      error: AiQueryServerErrors.RateLimitRequest,\n      message: 'Rate limit of user for requests exceeded',\n      data: { limiterType: 'request', limiterKind: 'user', limiterSeconds: 1 },\n    });\n    expect(error).toBeInstanceOf(AiQueryRateLimitRequestException);\n    expect(error.getResponse()).toEqual({\n      message: 'Rate limit of user for requests exceeded',\n      statusCode: HttpStatus.TOO_MANY_REQUESTS,\n      error: 'AiQueryRateLimitRequest',\n      errorCode: CustomErrorCodes.QueryAiRateLimitRequest,\n      details: {\n        limiterType: 'request',\n        limiterKind: 'user',\n        limiterSeconds: 1,\n      },\n    });\n  });\n  it('Should throw AiQueryRateLimitRequestException with default message', async () => {\n    const error = wrapAiQueryError({\n      error: AiQueryServerErrors.RateLimitRequest,\n      data: { limiterType: 'request', limiterKind: 'user', limiterSeconds: 1 },\n    });\n    expect(error).toBeInstanceOf(AiQueryRateLimitRequestException);\n    expect(error.getResponse()).toEqual({\n      message: ERROR_MESSAGES.AI_QUERY_REQUEST_RATE_LIMIT,\n      statusCode: HttpStatus.TOO_MANY_REQUESTS,\n      error: 'AiQueryRateLimitRequest',\n      errorCode: CustomErrorCodes.QueryAiRateLimitRequest,\n      details: {\n        limiterType: 'request',\n        limiterKind: 'user',\n        limiterSeconds: 1,\n      },\n    });\n  });\n  it('Should throw AiQueryRateLimitMaxTokensException', async () => {\n    const error = wrapAiQueryError({\n      error: AiQueryServerErrors.MaxTokens,\n      message: 'Token count exceeds the conversation limit',\n      data: { tokenLimit: 20000, tokenCount: 575 },\n    });\n    expect(error).toBeInstanceOf(AiQueryRateLimitMaxTokensException);\n    expect(error.getResponse()).toEqual({\n      message: 'Token count exceeds the conversation limit',\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitMaxTokens',\n      errorCode: CustomErrorCodes.QueryAiRateLimitMaxTokens,\n      details: {\n        tokenLimit: 20000,\n        tokenCount: 575,\n      },\n    });\n  });\n  it('Should throw AiQueryRateLimitMaxTokensException with default message', async () => {\n    const error = wrapAiQueryError({\n      error: AiQueryServerErrors.MaxTokens,\n      data: { tokenLimit: 20000, tokenCount: 575 },\n    });\n    expect(error).toBeInstanceOf(AiQueryRateLimitMaxTokensException);\n    expect(error.getResponse()).toEqual({\n      message: ERROR_MESSAGES.AI_QUERY_MAX_TOKENS_RATE_LIMIT,\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitMaxTokens',\n      errorCode: CustomErrorCodes.QueryAiRateLimitMaxTokens,\n      details: {\n        tokenLimit: 20000,\n        tokenCount: 575,\n      },\n    });\n  });\n  it('Should throw AiQueryInternalServerErrorException when unsupported rate limiter error', async () => {\n    const error = wrapAiQueryError({\n      error: 'unsupported',\n      message: 'Token count exceeds the conversation limit',\n      data: { tokenLimit: 20000, tokenCount: 575 },\n    });\n    expect(error).toBeInstanceOf(AiQueryInternalServerErrorException);\n    expect(error).toEqual(new AiQueryInternalServerErrorException());\n  });\n  it('additional checks for default values for rate limits errors', async () => {\n    expect(new AiQueryRateLimitTokenException().getResponse()).toEqual({\n      message: ERROR_MESSAGES.AI_QUERY_TOKEN_RATE_LIMIT,\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitToken',\n      errorCode: CustomErrorCodes.QueryAiRateLimitToken,\n    });\n    expect(new AiQueryRateLimitRequestException().getResponse()).toEqual({\n      message: ERROR_MESSAGES.AI_QUERY_REQUEST_RATE_LIMIT,\n      statusCode: HttpStatus.TOO_MANY_REQUESTS,\n      error: 'AiQueryRateLimitRequest',\n      errorCode: CustomErrorCodes.QueryAiRateLimitRequest,\n    });\n    expect(new AiQueryRateLimitMaxTokensException().getResponse()).toEqual({\n      message: ERROR_MESSAGES.AI_QUERY_MAX_TOKENS_RATE_LIMIT,\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitMaxTokens',\n      errorCode: CustomErrorCodes.QueryAiRateLimitMaxTokens,\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.bad-request.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryBadRequestException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.BAD_REQUEST,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'AiQueryBadRequest',\n      errorCode: CustomErrorCodes.QueryAiBadRequest,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.error.handler.ts",
    "content": "import { get } from 'lodash';\nimport { HttpException } from '@nestjs/common';\nimport {\n  AiQueryUnauthorizedException,\n  AiQueryForbiddenException,\n  AiQueryBadRequestException,\n  AiQueryNotFoundException,\n  AiQueryInternalServerErrorException,\n  AiQueryRateLimitRequestException,\n  AiQueryRateLimitTokenException,\n  AiQueryRateLimitMaxTokensException,\n} from 'src/modules/ai/query/exceptions';\nimport { AiQueryServerErrors } from 'src/modules/ai/query/models';\n\nexport const wrapAiQueryError = (\n  error: any,\n  message?: string,\n): HttpException => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  // ai errors to handle\n  if (error.error) {\n    switch (error.error) {\n      case AiQueryServerErrors.RateLimitRequest:\n        return new AiQueryRateLimitRequestException(error.message, {\n          details: error.data,\n        });\n      case AiQueryServerErrors.RateLimitToken:\n        return new AiQueryRateLimitTokenException(error.message, {\n          details: error.data,\n        });\n      case AiQueryServerErrors.MaxTokens:\n        return new AiQueryRateLimitMaxTokensException(error.message, {\n          details: error.data,\n        });\n      default:\n      // go further\n    }\n  }\n\n  // TransportError or Axios error\n  const response = get(\n    error,\n    ['description', 'target', '_req', 'res'],\n    error.response,\n  );\n\n  if (response) {\n    const errorOptions = { cause: new Error(response.data as string) };\n    switch (response.status || response.statusCode) {\n      case 401:\n        return new AiQueryUnauthorizedException(message, errorOptions);\n      case 403:\n        return new AiQueryForbiddenException(message, errorOptions);\n      case 400:\n        return new AiQueryBadRequestException(message, errorOptions);\n      case 404:\n        return new AiQueryNotFoundException(message, errorOptions);\n      default:\n        return new AiQueryInternalServerErrorException(message, errorOptions);\n    }\n  }\n\n  return new AiQueryInternalServerErrorException(message);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.forbidden.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryForbiddenException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FORBIDDEN,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.FORBIDDEN,\n      error: 'AiQueryForbidden',\n      errorCode: CustomErrorCodes.QueryAiForbidden,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.internal-server-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryInternalServerErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.INTERNAL_SERVER_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'AiQueryInternalServerError',\n      errorCode: CustomErrorCodes.QueryAiInternalServerError,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.NOT_FOUND,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'AiQueryNotFound',\n      errorCode: CustomErrorCodes.QueryAiNotFound,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.rate-limit.max-tokens.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryRateLimitMaxTokensException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.AI_QUERY_MAX_TOKENS_RATE_LIMIT,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitMaxTokens',\n      errorCode: CustomErrorCodes.QueryAiRateLimitMaxTokens,\n      details: options?.details,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.rate-limit.request.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryRateLimitRequestException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.AI_QUERY_REQUEST_RATE_LIMIT,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.TOO_MANY_REQUESTS,\n      error: 'AiQueryRateLimitRequest',\n      errorCode: CustomErrorCodes.QueryAiRateLimitRequest,\n      details: options?.details,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.rate-limit.token.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryRateLimitTokenException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.AI_QUERY_TOKEN_RATE_LIMIT,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.PAYLOAD_TOO_LARGE,\n      error: 'AiQueryRateLimitToken',\n      errorCode: CustomErrorCodes.QueryAiRateLimitToken,\n      details: options?.details,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/ai-query.unauthorized.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class AiQueryUnauthorizedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.UNAUTHORIZED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'AiQueryUnauthorized',\n      errorCode: CustomErrorCodes.QueryAiUnauthorized,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/exceptions/index.ts",
    "content": "export * from './ai-query.bad-request.exception';\nexport * from './ai-query.error.handler';\nexport * from './ai-query.forbidden.exception';\nexport * from './ai-query.internal-server-error.exception';\nexport * from './ai-query.not-found.exception';\nexport * from './ai-query.rate-limit.max-tokens.exception';\nexport * from './ai-query.rate-limit.request.exception';\nexport * from './ai-query.rate-limit.token.exception';\nexport * from './ai-query.unauthorized.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/models/ai-query.auth-data.ts",
    "content": "import { Expose } from 'class-transformer';\n\nexport class AiQueryAuthData {\n  @Expose()\n  sessionId: string;\n\n  @Expose()\n  csrf: string;\n\n  @Expose()\n  accountId: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/models/ai-query.common.ts",
    "content": "export enum AiQueryWsEvents {\n  ERROR = 'error',\n  CONNECT = 'connect',\n  CONNECT_ERROR = 'connect_error',\n  TOOL_CALL = 'tool_call', // non-ackable, signals a tool invocation by the agent, client should record in history\n  TOOL_REPLY = 'tool', // non-ackable, signals a tool's reply, client should record in history\n  REPLY_CHUNK = 'chunk', // non-ackable, signals a chunk of of the final reply, client should append it\n  REPLY_FINAL = 'reply', // non-ackable, signals the final (non-streaming) reply, client should display it\n  GET_INDEX = 'get_index', // ackable, signals a request to the client for an index context\n  RUN_QUERY = 'run_query', // ackable, signals a request to the client to run a query\n  STREAM = 'stream',\n}\n\nexport enum AiQueryMessageRole {\n  HUMAN = 'human',\n  AI = 'ai',\n  TOOL = 'tool',\n  TOOL_CALL = 'tool_call',\n}\n\nexport enum AiQueryServerErrors {\n  RateLimitRequest = 'RateLimitRequest',\n  RateLimitToken = 'RateLimitToken',\n  MaxTokens = 'MaxTokens',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/models/ai-query.intermediate-step.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport enum AiQueryIntermediateStepType {\n  TOOL = 'tool',\n  TOOL_CALL = 'tool_call',\n}\n\nexport class AiQueryIntermediateStep {\n  @ApiProperty({\n    enum: AiQueryIntermediateStepType,\n  })\n  @Expose()\n  type: AiQueryIntermediateStepType;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  data: string = '';\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/models/ai-query.message.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport { AiQueryIntermediateStep } from 'src/modules/ai/query/models/ai-query.intermediate-step';\nimport { Default } from 'src/common/decorators';\n\nexport enum AiQueryMessageType {\n  HumanMessage = 'HumanMessage',\n  AiMessage = 'AIMessage',\n}\n\nexport class AiQueryMessage {\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    enum: AiQueryMessageType,\n  })\n  @Expose()\n  @IsEnum(AiQueryMessageType)\n  @IsNotEmpty()\n  type: AiQueryMessageType;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  databaseId: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  accountId: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  conversationId: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  @IsString()\n  content: string = '';\n\n  @ApiProperty({\n    type: String,\n    isArray: true,\n  })\n  @Expose()\n  @Type(() => AiQueryIntermediateStep)\n  @Default([])\n  steps?: AiQueryIntermediateStep[] = [];\n\n  @ApiProperty({\n    type: Date,\n  })\n  @Expose()\n  createdAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/models/index.ts",
    "content": "export * from './ai-query.auth-data';\nexport * from './ai-query.common';\nexport * from './ai-query.intermediate-step';\nexport * from './ai-query.message';\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/providers/ai-query.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { io } from 'socket.io-client';\nimport * as MockedSocket from 'socket.io-mock';\nimport { mockAiQueryAuth } from 'src/__mocks__';\nimport { AiQueryProvider } from 'src/modules/ai/query/providers/ai-query.provider';\nimport { AiQueryWsEvents } from 'src/modules/ai/query/models';\nimport { BadRequestException } from '@nestjs/common';\nimport { AiQueryBadRequestException } from 'src/modules/ai/query/exceptions';\n\nconst mockSocketClient = new MockedSocket();\njest.mock('socket.io-client');\n\ndescribe('AiQueryProvider', () => {\n  let service: AiQueryProvider;\n\n  beforeEach(async () => {\n    (io as jest.Mock).mockReturnValue(mockSocketClient);\n\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [AiQueryProvider],\n    }).compile();\n\n    service = module.get(AiQueryProvider);\n  });\n\n  describe('getSocket', () => {\n    it('should successfully connect', (done) => {\n      service\n        .getSocket(mockAiQueryAuth)\n        .then((socket) => {\n          expect(socket).toEqual(mockSocketClient);\n          done();\n        })\n        .catch(done);\n\n      mockSocketClient.socketClient.emit(AiQueryWsEvents.CONNECT);\n    });\n    it('should fail with AiQueryBadRequestException', (done) => {\n      service\n        .getSocket(mockAiQueryAuth)\n        .then(() => {\n          done('Should fail');\n        })\n        .catch((e) => {\n          expect(e).toEqual(\n            new AiQueryBadRequestException('Unable to establish connection'),\n          );\n          done();\n        });\n\n      mockSocketClient.socketClient.emit(\n        AiQueryWsEvents.CONNECT_ERROR,\n        new BadRequestException(),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/providers/ai-query.provider.ts",
    "content": "import { io, Socket } from 'socket.io-client';\nimport config, { Config } from 'src/utils/config';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { AiQueryAuthData } from 'src/modules/ai/query/models/ai-query.auth-data';\nimport { AiQueryWsEvents } from 'src/modules/ai/query/models';\nimport { wrapAiQueryError } from 'src/modules/ai/query/exceptions';\n\nconst aiConfig = config.get('ai') as Config['ai'];\n\n@Injectable()\nexport class AiQueryProvider {\n  private readonly logger = new Logger('AiQueryProvider');\n\n  async getSocket(auth: AiQueryAuthData): Promise<Socket> {\n    try {\n      return await new Promise((resolve, reject) => {\n        const socket = io(aiConfig.querySocketUrl, {\n          path: aiConfig.querySocketPath,\n          reconnection: false,\n          transports: ['websocket'],\n          extraHeaders: {\n            'X-Csrf-Token': auth.csrf,\n            Cookie: `JSESSIONID=${auth.sessionId}`,\n          },\n        });\n\n        socket.on(AiQueryWsEvents.CONNECT_ERROR, (e) => {\n          this.logger.error('Unable to establish AI socket connection', e);\n          reject(e);\n        });\n\n        socket.on(AiQueryWsEvents.CONNECT, async () => {\n          this.logger.debug('AI socket connection established');\n          resolve(socket);\n        });\n      });\n    } catch (e) {\n      throw wrapAiQueryError(e, 'Unable to establish connection');\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/providers/auth/ai-query-auth.provider.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { AiQueryAuthData } from 'src/modules/ai/query/models/ai-query.auth-data';\n\nexport abstract class AiQueryAuthProvider {\n  abstract getAuthData(\n    sessionMetadata: SessionMetadata,\n  ): Promise<AiQueryAuthData>;\n  abstract callWithAuthRetry(\n    sessionId: SessionMetadata,\n    fn: () => Promise<any>,\n    retries?: number,\n  ): Promise<any>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/providers/auth/local.ai-query-auth.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAiQueryAuth,\n  mockCloudUserApiService,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalAiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/local.ai-query-auth.provider';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport {\n  CloudApiForbiddenException,\n  CloudApiUnauthorizedException,\n} from 'src/modules/cloud/common/exceptions';\n\nconst mockedResult = 'mockedResult';\nconst mockedFn = jest.fn().mockResolvedValue(mockedResult);\n\ndescribe('LocalAiQueryAuthProvider', () => {\n  let service: LocalAiQueryAuthProvider;\n  let cloudUserApiService: MockType<CloudUserApiService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalAiQueryAuthProvider,\n        {\n          provide: CloudUserApiService,\n          useFactory: mockCloudUserApiService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalAiQueryAuthProvider);\n    cloudUserApiService = module.get(CloudUserApiService);\n  });\n\n  describe('getAuthData', () => {\n    it('should get auth data', async () => {\n      expect(await service.getAuthData(mockSessionMetadata)).toEqual(\n        mockAiQueryAuth,\n      );\n    });\n\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      cloudUserApiService.getUserSession.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n\n      await expect(service.getAuthData(mockSessionMetadata)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n\n  describe('callWithAuthRetry', () => {\n    it('should return result from 1st attempt', async () => {\n      expect(\n        await service.callWithAuthRetry(mockSessionMetadata, mockedFn),\n      ).toEqual(mockedResult);\n      expect(cloudUserApiService.invalidateApiSession).toHaveBeenCalledTimes(0);\n    });\n    it('should not fail when session invalidation throw an error', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      cloudUserApiService.invalidateApiSession.mockRejectedValueOnce(\n        new Error('Unable to invalidate'),\n      );\n      expect(\n        await service.callWithAuthRetry(mockSessionMetadata, mockedFn),\n      ).toEqual(mockedResult);\n      expect(cloudUserApiService.invalidateApiSession).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(cloudUserApiService.invalidateApiSession).toHaveBeenCalledTimes(1);\n    });\n    it('should throw an error from 1st attempt if not CloudApiUnauthorizedException (and keep session)', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiForbiddenException());\n      await expect(\n        service.callWithAuthRetry(mockSessionMetadata, mockedFn),\n      ).rejects.toBeInstanceOf(CloudApiForbiddenException);\n      expect(cloudUserApiService.invalidateApiSession).toHaveBeenCalledTimes(0);\n    });\n    it('should throw CloudApiForbiddenException error from 2nd attempt (by default)', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      await expect(\n        service.callWithAuthRetry(mockSessionMetadata, mockedFn),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(mockedFn).toHaveBeenCalledTimes(2);\n      expect(cloudUserApiService.invalidateApiSession).toHaveBeenCalledTimes(1);\n    });\n    it('should throw CloudApiForbiddenException error from 3rd attempt (custom attempts)', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      await expect(\n        service.callWithAuthRetry(mockSessionMetadata, mockedFn, 2),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(mockedFn).toHaveBeenCalledTimes(3);\n      expect(cloudUserApiService.invalidateApiSession).toHaveBeenCalledTimes(2);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/providers/auth/local.ai-query-auth.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\nimport { AiQueryAuthData } from 'src/modules/ai/query/models/ai-query.auth-data';\nimport { AiQueryAuthProvider } from 'src/modules/ai/query/providers/auth/ai-query-auth.provider';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { AiQueryUnauthorizedException } from 'src/modules/ai/query/exceptions';\n\n@Injectable()\nexport class LocalAiQueryAuthProvider extends AiQueryAuthProvider {\n  constructor(private readonly cloudUserApiService: CloudUserApiService) {\n    super();\n  }\n\n  async getAuthData(\n    sessionMetadata: SessionMetadata,\n  ): Promise<AiQueryAuthData> {\n    const session =\n      await this.cloudUserApiService.getUserSession(sessionMetadata);\n\n    return {\n      sessionId: session.apiSessionId,\n      csrf: session.csrf,\n      accountId: `${session.user.currentAccountId}`,\n    };\n  }\n\n  async callWithAuthRetry(\n    sessionMetadata: SessionMetadata,\n    fn: () => Promise<any>,\n    retries = 1,\n  ) {\n    try {\n      return await fn();\n    } catch (e) {\n      if (\n        retries > 0 &&\n        (e instanceof CloudApiUnauthorizedException ||\n          e instanceof AiQueryUnauthorizedException)\n      ) {\n        await this.cloudUserApiService\n          .invalidateApiSession(sessionMetadata)\n          .catch(() => {});\n        return this.callWithAuthRetry(sessionMetadata, fn, retries - 1);\n      }\n\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/repositories/ai-query.context.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\n\nexport abstract class AiQueryContextRepository {\n  /**\n   * Should return saved db context if exists in particular chat\n   * @param sessionMetadata\n   * @param databaseId\n   * @param accountId\n   */\n  abstract getFullDbContext(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<object>;\n\n  /**\n   * Should save db context for particular chat\n   * @param sessionMetadata\n   * @param databaseId\n   * @param accountId\n   * @param context\n   */\n  abstract setFullDbContext(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n    context: object,\n  ): Promise<object>;\n\n  /**\n   * Should return saved index context if exists in particular chat\n   * @param sessionMetadata\n   * @param databaseId\n   * @param accountId\n   * @param index\n   */\n  abstract getIndexContext(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n    index: string,\n  ): Promise<object>;\n\n  /**\n   * Should save index context for particular chat\n   * @param sessionMetadata\n   * @param databaseId\n   * @param accountId\n   * @param index\n   * @param context\n   */\n  abstract setIndexContext(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n    index: string,\n    context: object,\n  ): Promise<object>;\n\n  /**\n   * Reset all index and db contexts for particular chat\n   * @param sessionMetadata\n   * @param databaseId\n   * @param accountId\n   */\n  abstract reset(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/repositories/ai-query.message.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { AiQueryMessage } from 'src/modules/ai/query/models';\n\nexport abstract class AiQueryMessageRepository {\n  abstract list(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<AiQueryMessage[]>;\n  abstract createMany(\n    sessionMetadata: SessionMetadata,\n    messages: AiQueryMessage[],\n  ): Promise<void>;\n  abstract clearHistory(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/repositories/in-memory.ai-query.context.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAiQueryAccountId,\n  mockAiQueryDatabaseId,\n  mockAiQueryFullDbContext,\n  mockAiQueryIndex,\n  mockAiQueryIndexContext,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { InMemoryAiQueryContextRepository } from 'src/modules/ai/query/repositories/in-memory.ai-query.context.repository';\n\ndescribe('InMemoryAiQueryContextRepository', () => {\n  let service: InMemoryAiQueryContextRepository;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [InMemoryAiQueryContextRepository],\n    }).compile();\n\n    service = module.get(InMemoryAiQueryContextRepository);\n  });\n\n  it('should return null since no data cached', async () => {\n    await expect(\n      service.getFullDbContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n      ),\n    ).resolves.toEqual(null);\n\n    await expect(\n      service.getIndexContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n        mockAiQueryIndex,\n      ),\n    ).resolves.toEqual(null);\n  });\n\n  it('should return cached data', async () => {\n    await service.setFullDbContext(\n      mockSessionMetadata,\n      mockAiQueryDatabaseId,\n      mockAiQueryAccountId,\n      mockAiQueryFullDbContext,\n    );\n\n    await service.setIndexContext(\n      mockSessionMetadata,\n      mockAiQueryDatabaseId,\n      mockAiQueryAccountId,\n      mockAiQueryIndex,\n      mockAiQueryIndexContext,\n    );\n\n    await service.setFullDbContext(mockSessionMetadata, '2', '2', {});\n\n    await expect(\n      service.getFullDbContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n      ),\n    ).resolves.toEqual(mockAiQueryFullDbContext);\n\n    await expect(\n      service.getFullDbContext(mockSessionMetadata, '2', '2'),\n    ).resolves.toEqual({});\n  });\n\n  it('should return cached index context data', async () => {\n    await service.setIndexContext(\n      mockSessionMetadata,\n      mockAiQueryDatabaseId,\n      mockAiQueryAccountId,\n      mockAiQueryIndex,\n      mockAiQueryIndexContext,\n    );\n\n    await service.setFullDbContext(\n      mockSessionMetadata,\n      mockAiQueryDatabaseId,\n      mockAiQueryAccountId,\n      mockAiQueryFullDbContext,\n    );\n\n    await service.setFullDbContext(mockSessionMetadata, '2', '2', {\n      full: 'context',\n    });\n\n    await service.setIndexContext(mockSessionMetadata, '2', '2', '2', {});\n\n    await service.setIndexContext(mockSessionMetadata, '2', '2', '3', {\n      index: '3',\n    });\n\n    await expect(\n      service.getIndexContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n        mockAiQueryIndex,\n      ),\n    ).resolves.toEqual(mockAiQueryIndexContext);\n\n    await expect(\n      service.getFullDbContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n      ),\n    ).resolves.toEqual(mockAiQueryFullDbContext);\n\n    await expect(\n      service.getIndexContext(mockSessionMetadata, '2', '2', '2'),\n    ).resolves.toEqual({});\n    await expect(\n      service.getIndexContext(mockSessionMetadata, '2', '2', '3'),\n    ).resolves.toEqual({ index: '3' });\n    await expect(\n      service.getIndexContext(mockSessionMetadata, '2', '2', '4'),\n    ).resolves.toEqual(null);\n\n    // reset\n    await expect(\n      service.reset(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n      ),\n    ).resolves.toEqual(undefined);\n\n    await expect(\n      service.getIndexContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n        mockAiQueryIndex,\n      ),\n    ).resolves.toEqual(null);\n\n    await expect(\n      service.getFullDbContext(\n        mockSessionMetadata,\n        mockAiQueryDatabaseId,\n        mockAiQueryAccountId,\n      ),\n    ).resolves.toEqual(null);\n\n    await expect(\n      service.getIndexContext(mockSessionMetadata, '2', '2', '2'),\n    ).resolves.toEqual({});\n    await expect(\n      service.getIndexContext(mockSessionMetadata, '2', '2', '3'),\n    ).resolves.toEqual({ index: '3' });\n    await expect(\n      service.getIndexContext(mockSessionMetadata, '2', '2', '4'),\n    ).resolves.toEqual(null);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/repositories/in-memory.ai-query.context.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { get, set, unset } from 'lodash';\nimport { SessionMetadata } from 'src/common/models';\nimport { AiQueryContextRepository } from 'src/modules/ai/query/repositories/ai-query.context.repository';\n\n@Injectable()\nexport class InMemoryAiQueryContextRepository extends AiQueryContextRepository {\n  private chats: Record<string, { index: Record<string, object>; db: object }> =\n    {};\n\n  static getChatId(databaseId: string, accountId: string) {\n    return `${databaseId}_${accountId}`;\n  }\n\n  /**\n   * @inheritdoc\n   */\n  async getFullDbContext(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<object> {\n    const chatId = InMemoryAiQueryContextRepository.getChatId(\n      databaseId,\n      accountId,\n    );\n\n    return get(this.chats, [chatId, 'db'], null);\n  }\n\n  /**\n   * @inheritdoc\n   */\n  async setFullDbContext(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n    context: object,\n  ): Promise<object> {\n    const chatId = InMemoryAiQueryContextRepository.getChatId(\n      databaseId,\n      accountId,\n    );\n\n    set(this.chats, [chatId, 'db'], context);\n\n    return context;\n  }\n\n  /**\n   * @inheritdoc\n   */\n  async getIndexContext(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n    index: string,\n  ): Promise<object> {\n    const chatId = InMemoryAiQueryContextRepository.getChatId(\n      databaseId,\n      accountId,\n    );\n\n    return get(this.chats, [chatId, 'index', index], null);\n  }\n\n  /**\n   * @inheritdoc\n   */\n  async setIndexContext(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n    index: string,\n    context: object,\n  ): Promise<object> {\n    const chatId = InMemoryAiQueryContextRepository.getChatId(\n      databaseId,\n      accountId,\n    );\n\n    set(this.chats, [chatId, 'index', index], context);\n\n    return context;\n  }\n\n  /**\n   * @inheritdoc\n   */\n  async reset(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<void> {\n    const chatId = InMemoryAiQueryContextRepository.getChatId(\n      databaseId,\n      accountId,\n    );\n\n    unset(this.chats, [chatId]);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/repositories/local.ai-query.message.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAiQueryAccountId,\n  mockAiQueryAiResponse,\n  mockAiQueryAiResponse2,\n  mockAiQueryAiResponseEntity,\n  mockAiQueryAiResponseEntity2,\n  mockAiQueryDatabaseId,\n  mockAiQueryHumanMessage,\n  mockAiQueryHumanMessage2,\n  mockAiQueryHumanMessageEntity,\n  mockAiQueryHumanMessageEntity2,\n  mockEncryptionService,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { when } from 'jest-when';\nimport { LocalAiQueryMessageRepository } from 'src/modules/ai/query/repositories/local.ai-query.message.repository';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { AiQueryMessageEntity } from 'src/modules/ai/query/entities/ai-query.message.entity';\nimport { Repository } from 'typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { KeytarEncryptionErrorException } from 'src/modules/encryption/exceptions';\n\ndescribe('LocalAiQueryAuthProvider', () => {\n  let service: LocalAiQueryMessageRepository;\n  let repository: MockType<Repository<AiQueryMessageEntity>>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalAiQueryMessageRepository,\n        {\n          provide: getRepositoryToken(AiQueryMessageEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalAiQueryMessageRepository);\n    encryptionService = module.get(EncryptionService);\n    repository = module.get(getRepositoryToken(AiQueryMessageEntity));\n\n    encryptionService.decrypt.mockImplementation((value) => value);\n    when(encryptionService.decrypt)\n      .calledWith(mockAiQueryHumanMessageEntity.content, expect.anything())\n      .mockResolvedValue(mockAiQueryHumanMessage.content)\n      .calledWith(mockAiQueryAiResponseEntity.content, expect.anything())\n      .mockResolvedValue(mockAiQueryAiResponse.content)\n      .calledWith(mockAiQueryAiResponseEntity.steps, expect.anything())\n      .mockResolvedValue(JSON.stringify(mockAiQueryAiResponse.steps));\n\n    encryptionService.encrypt.mockImplementation((value) => value);\n    when(encryptionService.encrypt)\n      .calledWith(mockAiQueryHumanMessage.content, expect.anything())\n      .mockResolvedValue(mockAiQueryHumanMessageEntity.content)\n      .calledWith(mockAiQueryAiResponse.content, expect.anything())\n      .mockResolvedValue(mockAiQueryAiResponseEntity.content)\n      .calledWith(\n        JSON.stringify(mockAiQueryAiResponse.steps),\n        expect.anything(),\n      )\n      .mockResolvedValue(mockAiQueryAiResponseEntity.steps);\n  });\n\n  describe('cleanupDatabaseHistory', () => {\n    beforeEach(() => {\n      repository\n        .createQueryBuilder()\n        .getRawMany.mockResolvedValueOnce([mockAiQueryHumanMessage.id]);\n    });\n\n    it('should get id to and remove it', async () => {\n      await expect(\n        service['cleanupDatabaseHistory'](\n          mockAiQueryDatabaseId,\n          mockAiQueryAccountId,\n        ),\n      ).resolves.toEqual(undefined);\n    });\n  });\n\n  describe('list', () => {\n    beforeEach(() => {\n      repository\n        .createQueryBuilder()\n        .getMany.mockResolvedValueOnce([\n          mockAiQueryHumanMessageEntity,\n          mockAiQueryAiResponseEntity,\n          mockAiQueryHumanMessageEntity2,\n          mockAiQueryAiResponseEntity2,\n        ]);\n    });\n\n    it('should return list of messages', async () => {\n      await expect(\n        service.list(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockAiQueryAccountId,\n        ),\n      ).resolves.toEqual([\n        mockAiQueryHumanMessage,\n        mockAiQueryAiResponse,\n        mockAiQueryHumanMessage2,\n        mockAiQueryAiResponse2,\n      ]);\n    });\n\n    it('should ignore messages on decryption error', async () => {\n      encryptionService.decrypt.mockRejectedValueOnce(\n        new Error('Unable to decrypt'),\n      );\n\n      await expect(\n        service.list(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockAiQueryAccountId,\n        ),\n      ).resolves.toEqual([\n        mockAiQueryAiResponse,\n        mockAiQueryHumanMessage2,\n        mockAiQueryAiResponse2,\n      ]);\n    });\n  });\n\n  describe('createMany', () => {\n    it('should 2 messages', async () => {\n      await expect(\n        service.createMany(mockSessionMetadata, [\n          mockAiQueryHumanMessage,\n          mockAiQueryAiResponse,\n        ]),\n      ).resolves.toEqual(undefined);\n    });\n\n    it('should reject on encryption error', async () => {\n      encryptionService.encrypt.mockRejectedValueOnce(\n        new KeytarEncryptionErrorException(),\n      );\n\n      await expect(\n        service.createMany(mockSessionMetadata, [\n          mockAiQueryHumanMessage,\n          mockAiQueryAiResponse,\n        ]),\n      ).rejects.toEqual(new KeytarEncryptionErrorException());\n    });\n  });\n\n  describe('clearHistory', () => {\n    it('should clear history', async () => {\n      await expect(\n        service.clearHistory(\n          mockSessionMetadata,\n          mockAiQueryDatabaseId,\n          mockAiQueryAccountId,\n        ),\n      ).resolves.toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/repositories/local.ai-query.message.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { AiQueryMessage } from 'src/modules/ai/query/models';\nimport { AiQueryMessageRepository } from 'src/modules/ai/query/repositories/ai-query.message.repository';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { AiQueryMessageEntity } from 'src/modules/ai/query/entities/ai-query.message.entity';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { filter, isNull } from 'lodash';\nimport { classToClass, Config } from 'src/utils';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport config from 'src/utils/config';\nimport { Logger } from '@nestjs/common';\n\nconst aiConfig = config.get('ai') as Config['ai'];\n\nexport class LocalAiQueryMessageRepository extends AiQueryMessageRepository {\n  private logger = new Logger('LocalAiQueryMessageRepository');\n\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(AiQueryMessageEntity)\n    private readonly repository: Repository<AiQueryMessageEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, [\n      'content',\n      'steps',\n    ]);\n  }\n\n  /**\n   * Clean history for particular database to fit N items limitation\n   * @param databaseId\n   * @param accountId\n   */\n  private async cleanupDatabaseHistory(\n    databaseId: string,\n    accountId: string,\n  ): Promise<void> {\n    // todo: investigate why delete with sub-query doesn't works\n    const idsToDelete = (\n      await this.repository\n        .createQueryBuilder()\n        .where({ databaseId, accountId })\n        .select('id')\n        .orderBy('createdAt', 'DESC')\n        .offset(aiConfig.queryHistoryLimit)\n        .getRawMany()\n    ).map((item) => item.id);\n\n    await this.repository\n      .createQueryBuilder()\n      .delete()\n      .whereInIds(idsToDelete)\n      .execute();\n  }\n\n  async list(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<AiQueryMessage[]> {\n    const entities = await this.repository\n      .createQueryBuilder('e')\n      .where({ databaseId, accountId })\n      .orderBy('e.createdAt', 'ASC')\n      .limit(aiConfig.queryHistoryLimit)\n      .getMany();\n\n    const decryptedEntities = await Promise.all(\n      entities.map<Promise<AiQueryMessageEntity>>(async (entity) => {\n        try {\n          return await this.modelEncryptor.decryptEntity(entity);\n        } catch (e) {\n          return null;\n        }\n      }),\n    );\n\n    return filter(decryptedEntities, (entity) => !isNull(entity)).map(\n      (entity) => classToClass(AiQueryMessage, entity),\n    );\n  }\n\n  async createMany(\n    sessionMetadata: SessionMetadata,\n    messages: AiQueryMessage[],\n  ): Promise<void> {\n    const entities = await Promise.all(\n      messages.map(async (message) => {\n        const entity = classToClass(AiQueryMessageEntity, message);\n\n        return this.modelEncryptor.encryptEntity(entity);\n      }),\n    );\n\n    await this.repository.save(entities);\n\n    // cleanup history and ignore error if any\n    try {\n      await this.cleanupDatabaseHistory(\n        entities[0].databaseId,\n        entities[0].accountId,\n      );\n    } catch (e) {\n      this.logger.error(\n        'Error when trying to cleanup history after insert',\n        e,\n        sessionMetadata,\n      );\n    }\n  }\n\n  async clearHistory(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    accountId: string,\n  ): Promise<void> {\n    await this.repository\n      .createQueryBuilder()\n      .delete()\n      .where({ databaseId, accountId })\n      .execute();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/utils/context.util.spec.ts",
    "content": "import {\n  mockAiQueryFullDbContext,\n  mockAiQueryGetDescriptionTopValuesReply,\n  mockAiQueryGetPriceTopValuesReply,\n  mockAiQueryGetTypeTopValuesReply,\n  mockAiQueryHScanReply,\n  mockAiQueryIndex,\n  mockAiQueryIndexContext,\n  mockAiQueryIndexInfoObject,\n  mockAiQueryIndexInfoReply,\n  mockAiQueryJsonReply,\n  mockAiQuerySchema,\n  mockAiQuerySchemaForHash,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { when } from 'jest-when';\nimport {\n  quotesIfNeeded,\n  convertArrayReplyToObject,\n  convertIndexInfoAttributeReply,\n  convertIndexInfoReply,\n  getAttributeTopValues,\n  createIndexCreateStatement,\n  createIndexContext,\n  getDocumentsSchema,\n  getIndexContext,\n  getFullDbContext,\n} from './context.util';\n\ndescribe('ContextUtility', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('quotesIfNeeded', () => {\n    it.each([\n      { input: 'string', result: 'string' },\n      { input: 'string with spaces', result: '\"string with spaces\"' },\n      {\n        input: 'string with special characters\"',\n        result: '\"string with special characters\\\\\"\"',\n      },\n      { input: null, result: null },\n      { input: undefined, result: undefined },\n      { input: { some: 'obj' } as unknown as string, result: { some: 'obj' } },\n    ])('should add quotes when needed', async ({ input, result }) => {\n      expect(quotesIfNeeded(input)).toEqual(result);\n    });\n  });\n  describe('quotesIfNeeded', () => {\n    it.each([\n      { input: ['key', 'value'], result: { key: 'value' } },\n      { input: [], result: {} },\n      {\n        input: ['key', 'value', 'array', ['some', 'array']],\n        result: { key: 'value', array: ['some', 'array'] },\n      },\n      { input: null, result: {} },\n      { input: undefined, result: {} },\n      { input: { some: 'obj' } as any, result: {} },\n      {\n        input: [{ some: 'obj' }, 'value'] as any,\n        result: { '[object Object]': 'value' },\n      },\n    ])('should return object', async ({ input, result }) => {\n      expect(convertArrayReplyToObject(input)).toEqual(result);\n    });\n  });\n  describe('convertIndexInfoAttributeReply', () => {\n    it.each([\n      {\n        input: ['key', 'value'],\n        result: {\n          key: 'value',\n        },\n      },\n      {\n        input: [\n          'key',\n          'value',\n          'SORTABLE',\n          'NOINDEX',\n          'CASESENSITIVE',\n          'UNF',\n          'NOSTEM',\n        ],\n        result: {\n          key: 'value',\n          SORTABLE: true,\n          NOINDEX: true,\n          CASESENSITIVE: true,\n          UNF: true,\n          NOSTEM: true,\n        },\n      },\n      { input: [], result: {} },\n      { input: null, result: {} },\n      { input: undefined, result: {} },\n      { input: { some: 'obj' } as any, result: {} },\n      {\n        input: [{ some: 'obj' }, 'value'] as any,\n        result: { '[object Object]': 'value' },\n      },\n    ])('should return attribute info', async ({ input, result }) => {\n      expect(convertIndexInfoAttributeReply(input)).toEqual(result);\n    });\n  });\n  describe('convertIndexInfoReply', () => {\n    it.each([\n      {\n        input: ['key', 'value'],\n        result: {\n          key: 'value',\n          index_definition: {},\n        },\n      },\n      {\n        input: [\n          'key',\n          'value',\n          'index_definition',\n          ['index_name', 'idx:index'],\n          'attributes',\n          [\n            [\n              'identifier',\n              '$.brand',\n              'attribute',\n              'brand',\n              'type',\n              'TEXT',\n              'WEIGHT',\n              '1',\n              'SORTABLE',\n              'NOINDEX',\n              'CASESENSITIVE',\n              'UNF',\n              'NOSTEM',\n            ],\n          ],\n        ],\n        result: {\n          key: 'value',\n          index_definition: { index_name: 'idx:index' },\n          attributes: [\n            {\n              identifier: '$.brand',\n              attribute: 'brand',\n              type: 'TEXT',\n              WEIGHT: '1',\n              SORTABLE: true,\n              NOINDEX: true,\n              CASESENSITIVE: true,\n              UNF: true,\n              NOSTEM: true,\n            },\n          ],\n        },\n      },\n      { input: [], result: { index_definition: {} } },\n      { input: null, result: { index_definition: {} } },\n      { input: undefined, result: { index_definition: {} } },\n      { input: { some: 'obj' } as any, result: { index_definition: {} } },\n      {\n        input: [{ some: 'obj' }, 'value'] as any,\n        result: { '[object Object]': 'value', index_definition: {} },\n      },\n    ])('should return attribute info', async ({ input, result }) => {\n      expect(convertIndexInfoReply(input)).toEqual(result);\n    });\n  });\n  describe('getAttributeTopValues', () => {\n    beforeEach(() => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValue([\n        '3',\n        ['1', 'v1'],\n        ['1', 'v2'],\n        ['1', 'v3'],\n      ]);\n    });\n\n    const mockResult = {\n      distinct_count: 3,\n      top_values: [{ value: 'v1' }, { value: 'v2' }, { value: 'v3' }],\n    };\n\n    it.each([\n      { input: ['idx', { type: 'text' }], result: mockResult },\n      { input: ['idx', { type: 'tag' }], result: mockResult },\n      { input: ['idx', { type: 'numeric' }], result: mockResult },\n      { input: ['idx', { type: 'geo' }], result: mockResult },\n      { input: ['idx', { type: 'hash' }], result: {} },\n      { input: ['idx', { type: 'rejson' }], result: {} },\n      { input: [], result: {} },\n      { input: [null, null], result: {} },\n      { input: [{ some: 'obj' }, 'value'] as any, result: {} },\n    ])(\n      'should return top values',\n      async ({ input: [index, attribute], result }) => {\n        expect(\n          await getAttributeTopValues(\n            mockStandaloneRedisClient,\n            index,\n            attribute,\n          ),\n        ).toEqual(result);\n      },\n    );\n\n    it('should not fail when empty array received and set count to 0', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([]);\n      expect(\n        await getAttributeTopValues(mockStandaloneRedisClient, 'idx', {\n          type: 'geo',\n        }),\n      ).toEqual({ distinct_count: 0, top_values: [] });\n    });\n\n    it('should not fail when count 0 and no keys', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce(['0']);\n      expect(\n        await getAttributeTopValues(mockStandaloneRedisClient, 'idx', {\n          type: 'geo',\n        }),\n      ).toEqual({ distinct_count: 0, top_values: [] });\n    });\n\n    it('should not fail', async () => {\n      mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce(\n        new Error('ERR: syntax error'),\n      );\n      expect(\n        await getAttributeTopValues(mockStandaloneRedisClient, 'idx', {\n          type: 'geo',\n        }),\n      ).toEqual({});\n    });\n  });\n  describe('createIndexCreateStatement', () => {\n    it.each([\n      {\n        input: {\n          index_name: 'idx',\n          index_definition: {\n            prefixes: [],\n            key_type: 'HASH',\n          },\n          attributes: [\n            {\n              identifier: '$.brand',\n              attribute: 'brand',\n              type: 'TAG',\n              WEIGHT: '1',\n              SORTABLE: true,\n              NOINDEX: true,\n              CASESENSITIVE: true,\n              UNF: true,\n              NOSTEM: true,\n            },\n          ],\n        },\n        result: 'FT.CREATE idx ON HASH SCHEMA $.brand AS brand TAG',\n      },\n      {\n        input: {\n          index_name: 'idx',\n          index_definition: {\n            prefixes: ['*'],\n            key_type: 'HASH',\n            filter: 'type',\n          },\n          attributes: [\n            {\n              identifier: '$.brand',\n              attribute: 'brand',\n              type: 'TAG',\n              WEIGHT: '1',\n              SORTABLE: true,\n              NOINDEX: true,\n              CASESENSITIVE: true,\n              UNF: true,\n              NOSTEM: true,\n            },\n          ],\n        },\n        result:\n          'FT.CREATE idx ON HASH PREFIX 1 * FILTER type SCHEMA $.brand AS brand TAG',\n      },\n      { input: {}, result: undefined },\n      { input: undefined, result: undefined },\n      { input: null, result: undefined },\n      { input: [{ some: 'obj' }, 'value'] as any, result: undefined },\n    ])('should return object', async ({ input, result }) => {\n      expect(createIndexCreateStatement(input)).toEqual(result);\n    });\n  });\n  describe('createIndexContext', () => {\n    it.each([\n      {\n        input: {\n          index_name: 'idx',\n          index_definition: {\n            prefixes: ['*'],\n            key_type: 'HASH',\n            filter: 'type',\n          },\n          attributes: [\n            {\n              identifier: '$.brand',\n              attribute: 'brand',\n              type: 'TAG',\n              WEIGHT: '1',\n              SORTABLE: true,\n              NOINDEX: true,\n              CASESENSITIVE: true,\n              UNF: true,\n              NOSTEM: true,\n            },\n          ],\n        },\n        result: {\n          index_name: 'idx',\n          create_statement:\n            'FT.CREATE idx ON HASH PREFIX 1 * FILTER type SCHEMA $.brand AS brand TAG',\n          attributes: {\n            brand: {\n              CASESENSITIVE: true,\n              NOINDEX: true,\n              NOSTEM: true,\n              SORTABLE: true,\n              UNF: true,\n              WEIGHT: '1',\n              attribute: 'brand',\n              identifier: '$.brand',\n              type: 'TAG',\n            },\n          },\n        },\n      },\n      { input: {}, result: { attributes: {} } },\n      { input: undefined, result: { attributes: {} } },\n      { input: null, result: { attributes: {} } },\n      { input: [{ some: 'obj' }, 'value'] as any, result: { attributes: {} } },\n    ])('should return object', async ({ input, result }) => {\n      expect(createIndexContext(input)).toEqual(result);\n    });\n  });\n  describe('getDocumentsSchema', () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.SEARCH']), expect.anything())\n        .mockResolvedValueOnce(['0', 'key']);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['json.get']), expect.anything())\n        .mockResolvedValueOnce(mockAiQueryJsonReply);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['HSCAN']), expect.anything())\n        .mockResolvedValueOnce(mockAiQueryHScanReply);\n    });\n\n    it('Should get document schema for json type', async () => {\n      expect(\n        await getDocumentsSchema(\n          mockStandaloneRedisClient,\n          mockAiQueryIndex,\n          mockAiQueryIndexInfoObject,\n        ),\n      ).toEqual(mockAiQuerySchema);\n    });\n    it('Should get document schema for hash type', async () => {\n      expect(\n        await getDocumentsSchema(mockStandaloneRedisClient, mockAiQueryIndex, {\n          index_definition: { key_type: 'HASH' },\n        }),\n      ).toEqual(mockAiQuerySchemaForHash);\n    });\n    it('Should return empty schema object for non-supported type', async () => {\n      expect(\n        await getDocumentsSchema(mockStandaloneRedisClient, mockAiQueryIndex, {\n          index_definition: { key_type: 'STRING' },\n        }),\n      ).toEqual({\n        $ref: '#/definitions/IdxBicycle',\n        $schema: 'http://json-schema.org/draft-06/schema#',\n        definitions: {\n          IdxBicycle: {\n            additionalProperties: false,\n            title: 'IdxBicycle',\n            type: 'object',\n          },\n        },\n      });\n    });\n  });\n  describe('getIndexContext', () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.SEARCH']), expect.anything())\n        .mockResolvedValue(['0', 'key']);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(\n          expect.arrayContaining(['FT.AGGREGATE', '@price']),\n          expect.anything(),\n        )\n        .mockResolvedValue(mockAiQueryGetPriceTopValuesReply)\n        .calledWith(\n          expect.arrayContaining(['FT.AGGREGATE', '@description']),\n          expect.anything(),\n        )\n        .mockResolvedValue(mockAiQueryGetDescriptionTopValuesReply)\n        .calledWith(\n          expect.arrayContaining(['FT.AGGREGATE', '@type']),\n          expect.anything(),\n        )\n        .mockResolvedValue(mockAiQueryGetTypeTopValuesReply);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']), expect.anything())\n        .mockResolvedValue(mockAiQueryIndexInfoReply);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['json.get']), expect.anything())\n        .mockResolvedValue(mockAiQueryJsonReply);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['HSCAN']), expect.anything())\n        .mockResolvedValue(mockAiQueryHScanReply);\n    });\n\n    it('Should get index context', async () => {\n      expect(\n        await getIndexContext(mockStandaloneRedisClient, mockAiQueryIndex),\n      ).toEqual(mockAiQueryIndexContext);\n    });\n  });\n  describe('getFullDbContext', () => {\n    beforeEach(() => {\n      jest.clearAllMocks();\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT._LIST']), expect.anything())\n        .mockResolvedValue([mockAiQueryIndex]);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']), expect.anything())\n        .mockResolvedValue(mockAiQueryIndexInfoReply);\n    });\n\n    it('Should get index context', async () => {\n      expect(await getFullDbContext(mockStandaloneRedisClient)).toEqual(\n        mockAiQueryFullDbContext,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ai/query/utils/context.util.ts",
    "content": "import { chunk, isArray, keyBy } from 'lodash';\nimport {\n  quicktype,\n  InputData,\n  jsonInputForTargetLanguage,\n} from 'quicktype-core';\n\ntype ArrayReplyEntry = string | string[];\n// todo: find a way to avoid this\ntype RedisClient = { sendCommand: (args: any, options: any) => Promise<any> };\n\nconst DOCUMENT_SAMPLES_PER_PREFIX = 5;\nconst HSCAN_COUNT = 500;\n\nexport const quotesIfNeeded = (str: string) =>\n  str?.indexOf?.(' ') > -1 ? JSON.stringify(str) : str;\n\n// ====================================================================\n// Reply converter\n// ====================================================================\nexport const convertArrayReplyToObject = (\n  input: ArrayReplyEntry[],\n): { [key: string]: any } => {\n  const obj = {};\n\n  chunk(input, 2).forEach(([key, value]) => {\n    obj[key as string] = value;\n  });\n\n  return obj;\n};\n\nexport const convertIndexInfoAttributeReply = (input: string[]): object => {\n  const attribute = convertArrayReplyToObject(input);\n\n  if (isArray(input)) {\n    attribute['SORTABLE'] = input.includes('SORTABLE') || undefined;\n    attribute['NOINDEX'] = input.includes('NOINDEX') || undefined;\n    attribute['CASESENSITIVE'] = input.includes('CASESENSITIVE') || undefined;\n    attribute['UNF'] = input.includes('UNF') || undefined;\n    attribute['NOSTEM'] = input.includes('NOSTEM') || undefined;\n  }\n\n  return attribute;\n};\n\nexport const convertIndexInfoReply = (input: ArrayReplyEntry[]): object => {\n  const infoReply = convertArrayReplyToObject(input);\n  infoReply['index_definition'] = convertArrayReplyToObject(\n    infoReply['index_definition'],\n  );\n  infoReply['attributes'] = infoReply['attributes']?.map?.(\n    convertIndexInfoAttributeReply,\n  );\n\n  return infoReply;\n};\n\n// ====================================================================\n// Context creation\n// ====================================================================\nexport const getAttributeTopValues = async (\n  client: RedisClient,\n  index: string,\n  attribute: object,\n): Promise<object> => {\n  try {\n    switch (attribute?.['type']?.toLowerCase?.()) {\n      case 'text':\n      case 'tag':\n      case 'numeric':\n      case 'geo':\n        const [distinct, ...top] = (await client.sendCommand(\n          [\n            'FT.AGGREGATE',\n            index,\n            '*',\n            'GROUPBY',\n            '1',\n            `@${attribute['attribute']}`,\n            'REDUCE',\n            'COUNT',\n            '0',\n            'AS',\n            'count',\n            'SORTBY',\n            '2',\n            '@count',\n            'DESC',\n            'MAX',\n            '5',\n          ],\n          { replyEncoding: 'utf8' },\n        )) as [string, ...string[]];\n\n        return {\n          distinct_count: parseInt(distinct, 10) || 0,\n          top_values: top.map(([, value]) => ({ value })),\n        };\n      default:\n        return {};\n    }\n  } catch (e) {\n    // ignore error\n    return {};\n  }\n};\n\nexport const createIndexCreateStatement = (info: object) => {\n  try {\n    const definition = info['index_definition'];\n\n    let statement = `FT.CREATE ${quotesIfNeeded(info['index_name'])} ON ${quotesIfNeeded(definition['key_type'])}`;\n\n    if (definition['prefixes'].length) {\n      statement += ` PREFIX ${definition['prefixes'].length}`;\n\n      definition['prefixes'].forEach((prefix) => {\n        statement += ` ${quotesIfNeeded(prefix)}`;\n      });\n    }\n\n    if (definition.filter) {\n      statement += ` FILTER ${definition.filter}`;\n    }\n\n    statement += ' SCHEMA';\n\n    info['attributes'].forEach((attr) => {\n      statement += ` ${attr.identifier} AS ${attr.attribute} ${attr.type}`;\n    });\n\n    return statement;\n  } catch (e) {\n    // ignore error\n    return undefined;\n  }\n};\n\nexport const createIndexContext = (info: object): object => {\n  const context = {\n    index_name: info?.['index_name'],\n    create_statement: createIndexCreateStatement(info),\n    attributes: {},\n  };\n\n  context['attributes'] = keyBy(info?.['attributes'], 'attribute');\n\n  return context;\n};\n\nexport const getDocumentsSchema = async (\n  client: RedisClient,\n  index: string,\n  info: object,\n) => {\n  const [, ...keys] = (await client.sendCommand(\n    [\n      'FT.SEARCH',\n      info['index_name'],\n      '*',\n      'NOCONTENT',\n      'LIMIT',\n      '0',\n      DOCUMENT_SAMPLES_PER_PREFIX,\n    ],\n    {\n      replyEncoding: 'utf8',\n    },\n  )) as string[];\n\n  const documents = (await Promise.all(\n    keys.map(async (key) => {\n      switch (info['index_definition']['key_type'].toLowerCase()) {\n        case 'hash':\n          return convertArrayReplyToObject(\n            (\n              await client.sendCommand(\n                ['HSCAN', key, '0', 'COUNT', HSCAN_COUNT],\n                {\n                  replyEncoding: 'utf8',\n                },\n              )\n            )[1] as string[][],\n          );\n        case 'json':\n          return JSON.parse(\n            await client.sendCommand(['json.get', key, '.'], {\n              replyEncoding: 'utf8',\n            }),\n          );\n        default:\n          // Should not be other types\n          return {};\n      }\n    }),\n  )) as object[];\n\n  const inputData = new InputData();\n  const jsonInput = jsonInputForTargetLanguage('schema');\n  await jsonInput.addSource({\n    name: index,\n    samples: documents.map((doc) => JSON.stringify(doc)),\n  });\n  inputData.addInput(jsonInput);\n\n  const schemaReply = await quicktype({\n    inputData,\n    lang: 'schema',\n  });\n\n  return JSON.parse(schemaReply.lines.join(''));\n};\n\n/**\n * Get complete index context with data schema and top values\n * @param client\n * @param index\n */\nexport const getIndexContext = async (client: RedisClient, index: string) => {\n  const infoReply = (await client.sendCommand(['FT.INFO', index], {\n    replyEncoding: 'utf8',\n  })) as string[][];\n\n  const info = convertIndexInfoReply(infoReply);\n\n  return {\n    index_name: index,\n    create_statement: createIndexCreateStatement(info),\n    documents_schema: await getDocumentsSchema(client, index, info),\n    documents_type: info['index_definition']['key_type'],\n    attributes: keyBy(\n      await Promise.all(\n        info['attributes'].map(async (attr) => ({\n          ...attr,\n          ...(await getAttributeTopValues(client, info['index_name'], attr)),\n        })),\n      ),\n      'attribute',\n    ),\n  };\n};\n\n/**\n * Get \"general\" context without additional data for all indexes inside database\n */\nexport const getFullDbContext = async (\n  client: RedisClient,\n): Promise<object> => {\n  const context = {};\n\n  const indexes = (await client.sendCommand(['FT._LIST'], {\n    replyEncoding: 'utf8',\n  })) as string[];\n\n  await Promise.all(\n    indexes.map(async (index) => {\n      const infoReply = (await client.sendCommand(['FT.INFO', index], {\n        replyEncoding: 'utf8',\n      })) as string[][];\n\n      const info = convertIndexInfoReply(infoReply);\n\n      context[index] = {\n        index_name: index,\n        attributes: info['attributes'],\n        documents_type: info['index_definition']['key_type'],\n      };\n    }),\n  );\n\n  return context;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/analytics.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Post,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { SendEventDto } from 'src/modules/analytics/dto/analytics.dto';\nimport { AnalyticsService } from 'src/modules/analytics/analytics.service';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\n\n@ApiTags('Analytics')\n@Controller('analytics')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class AnalyticsController {\n  constructor(private service: AnalyticsService) {}\n\n  @Post('send-event')\n  @ApiEndpoint({\n    description: 'Send telemetry event',\n    statusCode: 204,\n    responses: [\n      {\n        status: 204,\n      },\n    ],\n  })\n  async sendEvent(\n    @Body() dto: SendEventDto,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    return this.service.sendEvent(sessionMetadata, dto);\n  }\n\n  @Post('send-page')\n  @ApiEndpoint({\n    description: 'Send telemetry page',\n    statusCode: 204,\n    responses: [\n      {\n        status: 204,\n      },\n    ],\n  })\n  async sendPage(\n    @Body() dto: SendEventDto,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    return this.service.sendPage(sessionMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/analytics.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { AnalyticsService } from 'src/modules/analytics/analytics.service';\nimport { AnalyticsController } from './analytics.controller';\n\n@Module({\n  providers: [AnalyticsService],\n  controllers: [AnalyticsController],\n  exports: [AnalyticsService],\n})\nexport class AnalyticsModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/analytics.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAppSettings,\n  mockAppSettingsWithoutPermissions,\n  mockAppVersion,\n  mockControlGroup,\n  mockControlNumber,\n  mockSessionMetadata,\n  mockSettingsService,\n  MockType,\n} from 'src/__mocks__';\nimport { TelemetryEvents } from 'src/constants';\nimport { AppType } from 'src/modules/server/models/server';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { LocalConstantsProvider } from 'src/modules/constants/providers/local.constants.provider';\nimport { convertAnyStringToPositiveInteger } from 'src/utils';\nimport {\n  AnalyticsService,\n  Telemetry,\n  NON_TRACKING_ANONYMOUS_ID,\n} from './analytics.service';\n\nlet mockAnalyticsTrack;\nlet mockAnalyticsPage;\njest.mock('@segment/analytics-node', () => ({\n  Analytics: jest.fn().mockImplementation(() => ({\n    track: mockAnalyticsTrack,\n    page: mockAnalyticsPage,\n  })),\n}));\n\nconst mockAnonymousId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805';\n\ndescribe('AnalyticsService', () => {\n  let service: AnalyticsService;\n  let settingsService: MockType<SettingsService>;\n  const sessionId = new Date().getTime();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AnalyticsService,\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n        {\n          provide: ConstantsProvider,\n          useClass: LocalConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    settingsService = module.get(SettingsService);\n    service = module.get(AnalyticsService);\n  });\n\n  describe('init', () => {\n    let sendEventSpy;\n\n    beforeEach(() => {\n      sendEventSpy = jest.spyOn(service, 'sendEvent');\n    });\n\n    it('should set anonymousId and send application started event', () => {\n      service.init({\n        anonymousId: mockAnonymousId,\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: false,\n        sessionMetadata: mockSessionMetadata,\n      });\n\n      const anonymousId = service.getAnonymousId();\n\n      expect(anonymousId).toEqual(mockAnonymousId);\n      expect(sendEventSpy).toHaveBeenCalledTimes(1);\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        expect.objectContaining({\n          event: TelemetryEvents.ApplicationStarted,\n        }),\n      );\n    });\n    it('should NOT send application started event since sessionMetadata was not provided', () => {\n      service.init({\n        anonymousId: mockAnonymousId,\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: false,\n      });\n\n      const anonymousId = service.getAnonymousId();\n\n      expect(anonymousId).toEqual(mockAnonymousId);\n      expect(sendEventSpy).toHaveBeenCalledTimes(0);\n    });\n    it('should set anonymousId and send application first start event', () => {\n      service.init({\n        anonymousId: mockAnonymousId,\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: true,\n        sessionMetadata: mockSessionMetadata,\n      });\n\n      const anonymousId = service.getAnonymousId();\n\n      expect(anonymousId).toEqual(mockAnonymousId);\n      expect(sendEventSpy).toHaveBeenCalledTimes(1);\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        expect.objectContaining({\n          event: TelemetryEvents.ApplicationFirstStart,\n        }),\n      );\n    });\n  });\n\n  describe('getAnonymousId', () => {\n    it('should always return anonymousId defined with init', () => {\n      service.init({\n        anonymousId: mockAnonymousId,\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: true,\n      });\n\n      expect(service.getAnonymousId()).toEqual(mockAnonymousId);\n      expect(service.getAnonymousId(mockSessionMetadata)).toEqual(\n        mockAnonymousId,\n      );\n    });\n    it('should return anonymousId from sessionMetadata or \"unknown\"', () => {\n      service.init({\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: true,\n      });\n\n      expect(service.getAnonymousId()).toEqual('unknown');\n      expect(service.getAnonymousId(mockSessionMetadata)).toEqual(\n        mockSessionMetadata.userId,\n      );\n    });\n  });\n\n  describe('getSessionId', () => {\n    it('should always return sessionId defined with init method', () => {\n      service.init({\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: true,\n      });\n\n      expect(service.getSessionId()).toEqual(sessionId);\n      expect(service.getSessionId(mockSessionMetadata)).toEqual(sessionId);\n    });\n    it('should return sessionId from sessionMetadata or -1', () => {\n      service.init({\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n        firstStart: true,\n      });\n\n      expect(service.getSessionId()).toEqual(-1);\n      expect(service.getSessionId(mockSessionMetadata)).toEqual(\n        convertAnyStringToPositiveInteger(mockSessionMetadata.sessionId),\n      );\n    });\n  });\n\n  describe('sendEvent', () => {\n    beforeEach(() => {\n      mockAnalyticsTrack = jest.fn();\n      service.init({\n        anonymousId: mockAnonymousId,\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n      });\n    });\n    it('should send event with anonymousId if permission are granted', async () => {\n      settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n\n      await service.sendEvent(mockSessionMetadata, {\n        event: TelemetryEvents.ApplicationStarted,\n        eventData: {},\n        nonTracking: false,\n      });\n\n      expect(mockAnalyticsTrack).toHaveBeenCalledWith({\n        anonymousId: mockAnonymousId,\n        integrations: { Amplitude: { session_id: sessionId } },\n        event: TelemetryEvents.ApplicationStarted,\n        context: {\n          traits: {\n            telemetry: Telemetry.Enabled,\n          },\n        },\n        properties: {\n          anonymousId: mockAnonymousId,\n          buildType: AppType.Electron,\n          controlNumber: mockControlNumber,\n          controlGroup: mockControlGroup,\n          appVersion: mockAppVersion,\n        },\n      });\n    });\n    it('should not send event if permission are not granted', async () => {\n      settingsService.getAppSettings.mockResolvedValue(\n        mockAppSettingsWithoutPermissions,\n      );\n      mockAnalyticsTrack.mockReset(); // reset invocation during init()\n\n      await service.sendEvent(mockSessionMetadata, {\n        event: 'SOME_EVENT',\n        eventData: {},\n        nonTracking: false,\n      });\n\n      expect(mockAnalyticsTrack).not.toHaveBeenCalled();\n    });\n    it('should send event for non tracking events event if permission are not granted', async () => {\n      settingsService.getAppSettings.mockResolvedValue(\n        mockAppSettingsWithoutPermissions,\n      );\n      mockAnalyticsTrack.mockReset(); // reset invocation during init()\n\n      await service.sendEvent(mockSessionMetadata, {\n        event: TelemetryEvents.ApplicationStarted,\n        eventData: {},\n        nonTracking: true,\n      });\n\n      expect(mockAnalyticsTrack).toHaveBeenCalledWith({\n        anonymousId: NON_TRACKING_ANONYMOUS_ID,\n        integrations: { Amplitude: { session_id: sessionId } },\n        event: TelemetryEvents.ApplicationStarted,\n        context: {\n          traits: {\n            telemetry: Telemetry.Disabled,\n          },\n        },\n        properties: {\n          anonymousId: mockAnonymousId,\n          buildType: AppType.Electron,\n          controlNumber: mockControlNumber,\n          controlGroup: mockControlGroup,\n          appVersion: mockAppVersion,\n        },\n      });\n    });\n    it('should send event for non tracking with regular payload', async () => {\n      settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n      mockAnalyticsTrack.mockReset(); // reset invocation during init()\n\n      await service.sendEvent(mockSessionMetadata, {\n        event: TelemetryEvents.ApplicationStarted,\n        eventData: {},\n        nonTracking: true,\n      });\n\n      expect(mockAnalyticsTrack).toHaveBeenCalledWith({\n        anonymousId: mockAnonymousId,\n        integrations: { Amplitude: { session_id: sessionId } },\n        event: TelemetryEvents.ApplicationStarted,\n        context: {\n          traits: {\n            telemetry: Telemetry.Enabled,\n          },\n        },\n        properties: {\n          anonymousId: mockAnonymousId,\n          buildType: AppType.Electron,\n          controlNumber: mockControlNumber,\n          controlGroup: mockControlGroup,\n          appVersion: mockAppVersion,\n        },\n      });\n    });\n  });\n\n  describe('sendPage', () => {\n    beforeEach(() => {\n      mockAnalyticsPage = jest.fn();\n      service.init({\n        anonymousId: mockAnonymousId,\n        sessionId,\n        appType: AppType.Electron,\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n        appVersion: mockAppVersion,\n      });\n    });\n    it('should send page with anonymousId if permission are granted', async () => {\n      settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n\n      await service.sendPage(mockSessionMetadata, {\n        event: TelemetryEvents.ApplicationStarted,\n        eventData: {},\n        nonTracking: false,\n        traits: {\n          telemetry: 'will be overwritten',\n          custom: 'trait',\n        },\n      });\n\n      expect(mockAnalyticsPage).toHaveBeenCalledWith({\n        anonymousId: mockAnonymousId,\n        integrations: { Amplitude: { session_id: sessionId } },\n        name: TelemetryEvents.ApplicationStarted,\n        context: {\n          traits: {\n            telemetry: Telemetry.Enabled,\n            custom: 'trait',\n          },\n        },\n        properties: {\n          anonymousId: mockAnonymousId,\n          buildType: AppType.Electron,\n          controlNumber: mockControlNumber,\n          controlGroup: mockControlGroup,\n          appVersion: mockAppVersion,\n        },\n      });\n    });\n    it('should not send page if permission are not granted', async () => {\n      settingsService.getAppSettings.mockResolvedValue(\n        mockAppSettingsWithoutPermissions,\n      );\n\n      await service.sendPage(mockSessionMetadata, {\n        event: 'SOME_EVENT',\n        eventData: {},\n        nonTracking: false,\n      });\n\n      expect(mockAnalyticsPage).not.toHaveBeenCalled();\n    });\n    it('should send page for non tracking events event if permission are not granted', async () => {\n      settingsService.getAppSettings.mockResolvedValue(\n        mockAppSettingsWithoutPermissions,\n      );\n\n      await service.sendPage(mockSessionMetadata, {\n        event: TelemetryEvents.ApplicationStarted,\n        eventData: {},\n        nonTracking: true,\n      });\n\n      expect(mockAnalyticsPage).toHaveBeenCalledWith({\n        anonymousId: NON_TRACKING_ANONYMOUS_ID,\n        integrations: { Amplitude: { session_id: sessionId } },\n        name: TelemetryEvents.ApplicationStarted,\n        context: {\n          traits: {\n            telemetry: Telemetry.Disabled,\n          },\n        },\n        properties: {\n          anonymousId: mockAnonymousId,\n          buildType: AppType.Electron,\n          controlNumber: mockControlNumber,\n          controlGroup: mockControlGroup,\n          appVersion: mockAppVersion,\n        },\n      });\n    });\n    it('should send page for non tracking events with regular payload', async () => {\n      settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n\n      await service.sendPage(mockSessionMetadata, {\n        event: TelemetryEvents.ApplicationStarted,\n        eventData: {},\n        nonTracking: true,\n      });\n\n      expect(mockAnalyticsPage).toHaveBeenCalledWith({\n        anonymousId: mockAnonymousId,\n        integrations: { Amplitude: { session_id: sessionId } },\n        name: TelemetryEvents.ApplicationStarted,\n        context: {\n          traits: {\n            telemetry: Telemetry.Enabled,\n          },\n        },\n        properties: {\n          anonymousId: mockAnonymousId,\n          buildType: AppType.Electron,\n          controlNumber: mockControlNumber,\n          controlGroup: mockControlGroup,\n          appVersion: mockAppVersion,\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/analytics.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { get } from 'lodash';\nimport { Analytics } from '@segment/analytics-node';\nimport { AppAnalyticsEvents, TelemetryEvents } from 'src/constants';\nimport config, { Config } from 'src/utils/config';\nimport axios from 'axios';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { convertAnyStringToPositiveInteger } from 'src/utils';\n\nexport const NON_TRACKING_ANONYMOUS_ID = '00000000-0000-0000-0000-000000000001';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\nconst ANALYTICS_CONFIG = config.get('analytics') as Config['analytics'];\n\nexport interface ITelemetryEvent {\n  event: string;\n  eventData: Object;\n  nonTracking: boolean;\n  traits?: Object;\n}\n\nexport interface ITelemetryInitEvent {\n  anonymousId?: string;\n  sessionId?: number;\n  appType: string;\n  controlNumber: number;\n  controlGroup: string;\n  appVersion: string;\n  firstStart?: boolean;\n  sessionMetadata?: SessionMetadata;\n}\n\nexport enum Telemetry {\n  Enabled = 'enabled',\n  Disabled = 'disabled',\n}\n\n@Injectable()\nexport class AnalyticsService {\n  private anonymousId: string;\n\n  private sessionId: number = -1;\n\n  private appType: string = 'unknown';\n\n  private controlNumber: number = -1;\n\n  private controlGroup: string = '-1';\n\n  private appVersion: string = '2.0.0';\n\n  private analytics: Analytics;\n\n  constructor(\n    private readonly settingsService: SettingsService,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {}\n\n  /**\n   * Returns default anonymous id if was set during service initialization\n   * Otherwise sessionMetadata.userId will be returned or 'unknown' string\n   *\n   * If we want to have single anonymousId it should be set during initialization. E.g. init({ anonymousId: 'id' })\n   * If we want to distinguish between several requests then sessionMetadata should have proper userId field, and\n   * we shouldn't pass anonymousId during initialization\n   *\n   * @param sessionMetadata\n   */\n  public getAnonymousId(sessionMetadata?: SessionMetadata): string {\n    return (\n      this.anonymousId ?? this.constantsProvider.getAnonymousId(sessionMetadata)\n    );\n  }\n\n  /**\n   * Returns default sessionId if was set during service initialization\n   * Otherwise sessionMetadata.sessionId will be returned or -1\n   *\n   * Behaves the same way as getAnonymousId.\n   *\n   * @param sessionMetadata\n   */\n  public getSessionId(sessionMetadata?: SessionMetadata): number {\n    if (this.sessionId) {\n      return this.sessionId;\n    }\n\n    if (sessionMetadata?.sessionId) {\n      return convertAnyStringToPositiveInteger(sessionMetadata?.sessionId);\n    }\n\n    return -1;\n  }\n\n  public async init(initConfig: ITelemetryInitEvent) {\n    const {\n      anonymousId,\n      sessionId,\n      appType,\n      controlNumber,\n      controlGroup,\n      appVersion,\n      firstStart,\n      sessionMetadata,\n    } = initConfig;\n    this.sessionId = sessionId;\n    this.anonymousId = anonymousId;\n    this.appType = appType;\n    this.controlGroup = controlGroup;\n    this.appVersion = appVersion;\n    this.controlNumber = controlNumber;\n    this.analytics = new Analytics({\n      writeKey: ANALYTICS_CONFIG.writeKey,\n      flushInterval: ANALYTICS_CONFIG.flushInterval,\n      httpClient: (url, requestInit) =>\n        axios.request({\n          ...requestInit,\n          url,\n          data: requestInit.body,\n        }),\n    });\n\n    if (ANALYTICS_CONFIG.startEvents && sessionMetadata) {\n      this.sendEvent(sessionMetadata, {\n        event: firstStart\n          ? TelemetryEvents.ApplicationFirstStart\n          : TelemetryEvents.ApplicationStarted,\n        eventData: {\n          appVersion: SERVER_CONFIG.appVersion,\n          osPlatform: process.platform,\n          buildType: SERVER_CONFIG.buildType,\n          port: SERVER_CONFIG.port,\n          packageType: ServerService.getPackageType(SERVER_CONFIG.buildType),\n        },\n        nonTracking: true,\n      }).catch();\n    }\n  }\n\n  @OnEvent(AppAnalyticsEvents.Track)\n  async sendEvent(sessionMetadata: SessionMetadata, payload: ITelemetryEvent) {\n    try {\n      // The event is reported only if the user's permission is granted.\n      // The anonymousId is also sent along with the event.\n      //\n      // The `nonTracking` argument can be set to True to mark an event that doesn't track the specific\n      // user in any way. When `nonTracking` is True, the event is sent regardless of whether the user's permission\n      // for analytics is granted or not.\n      // If permissions not granted\n      // anonymousId will includes \"00000000-0000-0000-0000-000000000001\" value without any user identifiers.\n      const trackParams = await this.prepareEventData(sessionMetadata, payload);\n\n      if (trackParams) {\n        this.analytics.track({\n          ...trackParams,\n          event: payload.event,\n        });\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  @OnEvent(AppAnalyticsEvents.Page)\n  async sendPage(sessionMetadata: SessionMetadata, payload: ITelemetryEvent) {\n    try {\n      // The event is reported only if the user's permission is granted.\n      // The anonymousId is also sent along with the event.\n      //\n      // The `nonTracking` argument can be set to True to mark an event that doesn't track the specific\n      // user in any way. When `nonTracking` is True, the event is sent regardless of whether the user's permission\n      // for analytics is granted or not.\n      // If permissions not granted anonymousId includes \"UNSET\" value without any user identifiers.\n      const pageParams = await this.prepareEventData(sessionMetadata, payload);\n\n      if (pageParams) {\n        this.analytics.page({\n          ...pageParams,\n          name: payload.event,\n        });\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  private async prepareEventData(\n    sessionMetadata: SessionMetadata,\n    payload: ITelemetryEvent,\n  ) {\n    try {\n      const { eventData, nonTracking, traits = {} } = payload;\n      const isAnalyticsGranted =\n        await this.checkIsAnalyticsGranted(sessionMetadata);\n\n      if (isAnalyticsGranted || nonTracking) {\n        return {\n          anonymousId:\n            !isAnalyticsGranted && nonTracking\n              ? NON_TRACKING_ANONYMOUS_ID\n              : this.getAnonymousId(sessionMetadata),\n          integrations: {\n            Amplitude: { session_id: this.getSessionId(sessionMetadata) },\n          },\n          context: {\n            traits: {\n              ...traits,\n              telemetry: isAnalyticsGranted\n                ? Telemetry.Enabled\n                : Telemetry.Disabled,\n            },\n          },\n          properties: {\n            ...eventData,\n            anonymousId: this.getAnonymousId(sessionMetadata),\n            buildType: this.appType,\n            controlNumber: this.controlNumber,\n            controlGroup: this.controlGroup,\n            appVersion: this.appVersion,\n          },\n        };\n      }\n    } catch (e) {\n      // ignore errors\n    }\n\n    return null;\n  }\n\n  private async checkIsAnalyticsGranted(sessionMetadata: SessionMetadata) {\n    return !!get(\n      await this.settingsService.getAppSettings(sessionMetadata),\n      'agreements.analytics',\n      false,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/command.telemetry.base.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { CommandType } from 'src/constants';\nimport { CommandTelemetryBaseService } from 'src/modules/analytics/command.telemetry.base.service';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { MockType } from 'src/__mocks__';\n\nclass Service extends CommandTelemetryBaseService {\n  constructor(\n    protected eventEmitter: EventEmitter2,\n    protected readonly commandsService: CommandsService,\n  ) {\n    super(eventEmitter, commandsService);\n  }\n}\n\nconst mockCommandsService = {\n  getCommandsGroups: jest.fn(),\n};\n\ndescribe('CommandTelemetryBaseService', () => {\n  let service;\n  let eventEmitter: EventEmitter2;\n  let commandsService: MockType<CommandsService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: EventEmitter2,\n          useFactory: () => ({\n            emit: jest.fn(),\n          }),\n        },\n        {\n          provide: CommandsService,\n          useFactory: () => mockCommandsService,\n        },\n      ],\n    }).compile();\n\n    eventEmitter = await module.get<EventEmitter2>(EventEmitter2);\n    commandsService = await module.get(CommandsService);\n    service = new Service(\n      eventEmitter,\n      commandsService as unknown as CommandsService,\n    );\n    commandsService.getCommandsGroups.mockResolvedValue({\n      main: {\n        SET: {\n          summary: 'Set the string value of a key',\n          since: '1.0.0',\n          group: 'string',\n          complexity: 'O(1)',\n          acl_categories: ['@write', '@string', '@slow'],\n        },\n      },\n      redisbloom: {\n        'BF.RESERVE': {\n          summary: 'Creates a new Bloom Filter',\n          complexity: 'O(1)',\n          since: '1.0.0',\n          group: 'bf',\n        },\n      },\n      custommodule: {\n        'CUSTOM.COMMAND': {\n          summary: 'Creates a new Bloom Filter',\n          complexity: 'O(1)',\n          since: '1.0.0',\n        },\n      },\n    });\n  });\n\n  describe('getCommandAdditionalInfo', () => {\n    it('should get command additional info (core module)', async () => {\n      expect(await service.getCommandAdditionalInfo('set')).toEqual({\n        commandType: CommandType.Core,\n        moduleName: 'n/a',\n        capability: 'string',\n      });\n    });\n    it('should get command additional info (known module)', async () => {\n      expect(await service.getCommandAdditionalInfo('BF.RESErve')).toEqual({\n        commandType: CommandType.Module,\n        moduleName: 'redisbloom',\n        capability: 'bf',\n      });\n    });\n    it('should get command additional info (known module w\\\\o cap.)', async () => {\n      expect(await service.getCommandAdditionalInfo('CUSTOM.COMMAND')).toEqual({\n        commandType: CommandType.Module,\n        moduleName: 'custommodule',\n        capability: 'n/a',\n      });\n    });\n    it('should get command additional info (custom module)', async () => {\n      expect(await service.getCommandAdditionalInfo('some.cmd')).toEqual({\n        commandType: CommandType.Module,\n        moduleName: 'custom',\n        capability: 'n/a',\n      });\n    });\n    it('should return empty object if no command provided', async () => {\n      expect(await service.getCommandAdditionalInfo('')).toEqual({});\n    });\n    it('should return empty object in case of an error', async () => {\n      commandsService.getCommandsGroups.mockRejectedValueOnce(new Error());\n      expect(await service.getCommandAdditionalInfo('set')).toEqual({});\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/command.telemetry.base.service.ts",
    "content": "import { EventEmitter2 } from '@nestjs/event-emitter';\nimport { CommandType } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { forEach } from 'lodash';\n\nexport abstract class CommandTelemetryBaseService extends TelemetryBaseService {\n  protected constructor(\n    protected eventEmitter: EventEmitter2,\n    protected readonly commandsService: CommandsService,\n  ) {\n    super(eventEmitter);\n  }\n\n  protected async getCommandAdditionalInfo(command: string): Promise<object> {\n    try {\n      const result = {\n        commandType: CommandType.Module,\n        moduleName: 'custom',\n        capability: 'n/a',\n      };\n\n      if (!command) {\n        return {};\n      }\n\n      const modules = await this.commandsService.getCommandsGroups();\n\n      const commandToFind = command.toUpperCase();\n      forEach(modules, (module, moduleName) => {\n        if (module[commandToFind]) {\n          result.commandType =\n            moduleName === 'main' ? CommandType.Core : CommandType.Module;\n          result.moduleName = moduleName === 'main' ? 'n/a' : moduleName;\n          result.capability = module[commandToFind]?.group\n            ? module[commandToFind]?.group\n            : 'n/a';\n        }\n      });\n\n      return result;\n    } catch (e) {\n      return {};\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/dto/analytics.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsBoolean,\n  IsDefined,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\n\nexport class SendEventDto {\n  @ApiProperty({\n    description: 'Telemetry event name.',\n    type: String,\n    example: 'APPLICATION_UPDATED',\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString()\n  event: string;\n\n  @ApiPropertyOptional({\n    description: 'Telemetry event data.',\n    type: Object,\n    example: { length: 5 },\n  })\n  @IsOptional()\n  @ValidateNested()\n  eventData: Object = {};\n\n  @ApiPropertyOptional({\n    description: 'Does not track the specific user in any way?',\n    type: Boolean,\n    example: false,\n  })\n  @IsOptional()\n  @IsBoolean()\n  nonTracking: boolean = false;\n\n  @ApiPropertyOptional({\n    description: 'User data.',\n    type: Object,\n    example: { telemetry: true },\n  })\n  @IsOptional()\n  @ValidateNested()\n  traits: Object = {};\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/telemetry.base.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { AppAnalyticsEvents, TelemetryEvents } from 'src/constants';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport { TelemetryBaseService } from './telemetry.base.service';\n\nclass Service extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n}\nconst httpException = new InternalServerErrorException('Message');\n\ndescribe('TelemetryBaseService', () => {\n  let service: Service;\n  let eventEmitter: EventEmitter2;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: EventEmitter2,\n          useFactory: () => ({\n            emit: jest.fn(),\n          }),\n        },\n      ],\n    }).compile();\n\n    eventEmitter = module.get<EventEmitter2>(EventEmitter2);\n    service = new Service(eventEmitter);\n  });\n\n  describe('sendEvent', () => {\n    it('should emit event', () => {\n      service['sendEvent'](\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        { data: 'Some data', command: 'lowercase' },\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.RedisInstanceAdded,\n          eventData: { data: 'Some data', command: 'LOWERCASE' },\n        },\n      );\n    });\n    it('should emit event with empty event data', () => {\n      service['sendEvent'](\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.RedisInstanceAdded,\n          eventData: {},\n        },\n      );\n    });\n    it('should emit event for undefined event data', () => {\n      service['sendEvent'](\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        undefined,\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.RedisInstanceAdded,\n          eventData: {},\n        },\n      );\n    });\n    it('should not throw on error', () => {\n      eventEmitter.emit = jest.fn().mockImplementation(() => {\n        throw new Error();\n      });\n\n      expect(() =>\n        service['sendEvent'](\n          mockSessionMetadata,\n          TelemetryEvents.RedisInstanceAdded,\n        ),\n      ).not.toThrow();\n    });\n  });\n\n  describe('sendFailedEvent', () => {\n    it('should emit event for custom exception', () => {\n      service['sendFailedEvent'](\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAddFailed,\n        httpException,\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.RedisInstanceAddFailed,\n          eventData: {\n            error: 'Internal Server Error',\n          },\n        },\n      );\n    });\n    it('should emit event for default exception', () => {\n      service['sendFailedEvent'](\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAddFailed,\n        new BadRequestException(),\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.RedisInstanceAddFailed,\n          eventData: {\n            error: 'Bad Request',\n          },\n        },\n      );\n    });\n    it('should emit event with additional event data', () => {\n      service['sendFailedEvent'](\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAddFailed,\n        httpException,\n        { data: 'Some data', command: 'lowercase' },\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.RedisInstanceAddFailed,\n          eventData: {\n            error: 'Internal Server Error',\n            data: 'Some data',\n            command: 'LOWERCASE',\n          },\n        },\n      );\n    });\n    it('should not throw on error', () => {\n      eventEmitter.emit = jest.fn().mockImplementation(() => {\n        throw new Error();\n      });\n\n      expect(() =>\n        service['sendFailedEvent'](\n          mockSessionMetadata,\n          TelemetryEvents.RedisInstanceAdded,\n          httpException,\n        ),\n      ).not.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/analytics/telemetry.base.service.ts",
    "content": "import { isString } from 'lodash';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { HttpException, Injectable } from '@nestjs/common';\nimport { AppAnalyticsEvents } from 'src/constants';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport abstract class TelemetryBaseService {\n  constructor(protected readonly eventEmitter: EventEmitter2) {}\n\n  protected sendEvent(\n    sessionMetadata: SessionMetadata,\n    event: string,\n    eventData: object = {},\n  ): void {\n    try {\n      this.eventEmitter.emit(AppAnalyticsEvents.Track, sessionMetadata, {\n        event,\n        eventData: {\n          ...eventData,\n          command: isString(eventData['command'])\n            ? eventData['command'].toUpperCase()\n            : eventData['command'],\n        },\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  protected sendFailedEvent(\n    sessionMetadata: SessionMetadata,\n    event: string,\n    exception: HttpException,\n    eventData: object = {},\n  ): void {\n    try {\n      this.eventEmitter.emit(AppAnalyticsEvents.Track, sessionMetadata, {\n        event,\n        eventData: {\n          error: exception.getResponse?.()['error'] || exception.message,\n          ...eventData,\n          command: isString(eventData['command'])\n            ? eventData['command'].toUpperCase()\n            : eventData['command'],\n        },\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/auth.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { WindowAuthModule } from './window-auth/window-auth.module';\nimport { BuildType } from '../server/models/server';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Module({})\nexport class AuthModule {\n  static register() {\n    const imports = [];\n\n    if (SERVER_CONFIG.buildType === BuildType.Electron) {\n      imports.push(WindowAuthModule);\n    }\n\n    return {\n      module: AuthModule,\n      imports,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/session-metadata/adapters/session-metadata.adapter.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication } from '@nestjs/common';\nimport { Socket } from 'socket.io';\nimport { IoAdapter } from '@nestjs/platform-socket.io';\nimport { SessionMetadataAdapter } from 'src/modules/auth/session-metadata/adapters/session-metadata.adapter';\nimport { mockDefaultSessionMetadata } from 'src/__mocks__';\n\nconst createBaseBindMessageHandlersMock = () => {\n  const mockBaseBindMessageHandlers = jest.fn();\n\n  jest\n    .spyOn(IoAdapter.prototype, 'bindMessageHandlers')\n    .mockImplementation(() => {\n      mockBaseBindMessageHandlers();\n    });\n\n  return mockBaseBindMessageHandlers;\n};\n\nconst createMockSocket = () =>\n  ({\n    request: {},\n    disconnect: jest.fn(),\n    data: {},\n    join: jest.fn(),\n  }) as unknown as Socket;\n\ndescribe('Session metadata adapater', () => {\n  let app: INestApplication;\n  let sessionMetadataAdapter: SessionMetadataAdapter;\n  let mockSocket: Socket;\n  let mockBaseBindMessageHandlers: ReturnType<\n    typeof createBaseBindMessageHandlersMock\n  >;\n\n  beforeEach(() => {\n    jest.resetAllMocks();\n    mockSocket = createMockSocket();\n    mockBaseBindMessageHandlers = createBaseBindMessageHandlersMock();\n  });\n\n  beforeAll(async () => {\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      providers: [],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    sessionMetadataAdapter = new SessionMetadataAdapter();\n    app.useWebSocketAdapter(sessionMetadataAdapter);\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  it('should attach session metadata and join a room for the user id', async () => {\n    await sessionMetadataAdapter.bindMessageHandlers(mockSocket, [], jest.fn());\n\n    expect(mockBaseBindMessageHandlers).toHaveBeenCalledTimes(1);\n    expect(mockSocket.data).toEqual({\n      sessionMetadata: mockDefaultSessionMetadata,\n    });\n    expect(mockSocket.join).toHaveBeenCalledTimes(1);\n    expect(mockSocket.join).toHaveBeenCalledWith('user:1');\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/session-metadata/adapters/session-metadata.adapter.ts",
    "content": "/* eslint-disable no-param-reassign */\nimport { Injectable } from '@nestjs/common';\nimport { IoAdapter } from '@nestjs/platform-socket.io';\nimport { MessageMappingProperties } from '@nestjs/websockets';\nimport { Observable } from 'rxjs';\nimport { Socket } from 'socket.io';\nimport {\n  DEFAULT_ACCOUNT_ID,\n  DEFAULT_SESSION_ID,\n  DEFAULT_USER_ID,\n} from 'src/common/constants';\nimport { SessionMetadata } from 'src/common/models';\nimport { getUserRoom } from 'src/constants/websocket-rooms';\n\n@Injectable()\nexport class SessionMetadataAdapter extends IoAdapter {\n  async bindMessageHandlers(\n    socket: Socket,\n    handlers: MessageMappingProperties[],\n    transform: (data: any) => Observable<any>,\n  ) {\n    const sessionMetadata: SessionMetadata = {\n      userId: DEFAULT_USER_ID,\n      accountId: DEFAULT_ACCOUNT_ID,\n      sessionId: DEFAULT_SESSION_ID,\n    };\n\n    socket.data['sessionMetadata'] = sessionMetadata;\n\n    // join room for the userId\n    socket.join(getUserRoom(sessionMetadata.userId));\n\n    super.bindMessageHandlers(socket, handlers, transform);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/session-metadata/decorators/ws-session-metadata.decorator.ts",
    "content": "import { ExecutionContext, createParamDecorator } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\n\nexport const WSSessionMetadata = createParamDecorator(\n  (data: unknown, ctx: ExecutionContext): SessionMetadata => {\n    const socket = ctx.switchToWs().getClient();\n    return socket.data.sessionMetadata;\n  },\n);\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/adapters/window-auth.adapter.ts",
    "content": "import { INestApplication, Logger } from '@nestjs/common';\nimport { IoAdapter } from '@nestjs/platform-socket.io';\nimport { MessageMappingProperties } from '@nestjs/websockets';\nimport { get } from 'lodash';\nimport { Observable } from 'rxjs';\nimport { Socket } from 'socket.io';\nimport { API_HEADER_WINDOW_ID } from 'src/common/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { WindowAuthService } from '../window-auth.service';\n\nexport class WindowsAuthAdapter extends IoAdapter {\n  private windowAuthService: WindowAuthService;\n\n  private logger = new Logger('WindowsAuthAdapter');\n\n  constructor(private app: INestApplication) {\n    super(app);\n    this.windowAuthService = this.app.get(WindowAuthService);\n  }\n\n  async bindMessageHandlers(\n    socket: Socket,\n    handlers: MessageMappingProperties[],\n    transform: (data: any) => Observable<any>,\n  ) {\n    const windowId =\n      (get(socket, `handshake.headers.${API_HEADER_WINDOW_ID}`) as string) ||\n      '';\n    const isAuthorized = await this.windowAuthService?.isAuthorized(windowId);\n\n    if (!isAuthorized) {\n      this.logger.error(ERROR_MESSAGES.UNDEFINED_WINDOW_ID);\n      return;\n    }\n\n    super.bindMessageHandlers(socket, handlers, transform);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/constants/exceptions.ts",
    "content": "import { HttpException, HttpStatus } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class WindowUnauthorizedException extends HttpException {\n  constructor(message) {\n    super(\n      {\n        statusCode: HttpStatus.UNAUTHORIZED,\n        errorCode: CustomErrorCodes.WindowUnauthorized,\n        message,\n        error: 'Window Unauthorized',\n      },\n      HttpStatus.UNAUTHORIZED,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/middleware/window.auth.middleware.ts",
    "content": "import { Injectable, Logger, NestMiddleware } from '@nestjs/common';\nimport { NextFunction, Request, Response } from 'express';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { API_HEADER_WINDOW_ID } from 'src/common/constants';\nimport { WindowAuthService } from '../window-auth.service';\nimport { WindowUnauthorizedException } from '../constants/exceptions';\n\n@Injectable()\nexport class WindowAuthMiddleware implements NestMiddleware {\n  private logger = new Logger('WindowAuthMiddleware');\n\n  constructor(private windowAuthService: WindowAuthService) {}\n\n  async use(req: Request, res: Response, next: NextFunction): Promise<any> {\n    const { windowId } = WindowAuthMiddleware.getWindowIdFromReq(req);\n\n    const isAuthorized = await this.windowAuthService.isAuthorized(windowId);\n\n    if (!isAuthorized) {\n      this.throwError(req, ERROR_MESSAGES.UNDEFINED_WINDOW_ID);\n    }\n\n    next();\n  }\n\n  private static getWindowIdFromReq(req: Request) {\n    return { windowId: `${req?.headers?.[API_HEADER_WINDOW_ID]}` };\n  }\n\n  private throwError(req: Request, message: string) {\n    const { method, url } = req;\n    this.logger.error(`${message} ${method} ${url}`);\n\n    throw new WindowUnauthorizedException(message);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/strategies/abstract.window.auth.strategy.ts",
    "content": "export abstract class AbstractWindowAuthStrategy {\n  abstract isAuthorized(data: any): Promise<boolean>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/window-auth.module.ts",
    "content": "import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { WindowAuthService } from './window-auth.service';\nimport { WindowAuthMiddleware } from './middleware/window.auth.middleware';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Module({\n  providers: [WindowAuthService],\n})\nexport class WindowAuthModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer\n      .apply(WindowAuthMiddleware)\n      .exclude(...SERVER_CONFIG.excludeAuthRoutes)\n      .forRoutes('*');\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/window-auth.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { WindowAuthService } from './window-auth.service';\nimport { AbstractWindowAuthStrategy } from './strategies/abstract.window.auth.strategy';\n\nexport class TestAuthStrategy extends AbstractWindowAuthStrategy {\n  async isAuthorized(): Promise<boolean> {\n    return true;\n  }\n}\n\nconst testStrategy = new TestAuthStrategy();\n\ndescribe('WindowAuthService', () => {\n  let windowAuthService: WindowAuthService;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [WindowAuthService],\n      exports: [WindowAuthService],\n    }).compile();\n\n    windowAuthService = module.get<WindowAuthService>(WindowAuthService);\n  });\n  it('Should set strategy to window auth service and call it', async () => {\n    windowAuthService.setStrategy(testStrategy);\n    expect(await windowAuthService.isAuthorized('')).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/auth/window-auth/window-auth.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { AbstractWindowAuthStrategy } from './strategies/abstract.window.auth.strategy';\n\n@Injectable()\nexport class WindowAuthService {\n  private strategy: AbstractWindowAuthStrategy = null;\n\n  /**\n   * Return strategy on how we are going to work with app(electron) windows auth\n   * @param strategy\n   */\n  setStrategy(strategy: AbstractWindowAuthStrategy): void {\n    this.strategy = strategy;\n  }\n\n  isAuthorized(id: string = ''): Promise<boolean> {\n    return this.strategy?.isAuthorized?.(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/azure-auth-callback.template.ts",
    "content": "import { AzureAuthStatus, AZURE_OAUTH_STORAGE_KEY } from '../constants';\n\n/**\n * Result data stored in localStorage for the opener window to read.\n */\nexport interface AzureOAuthCallbackResult {\n  status: AzureAuthStatus;\n  account?: {\n    id: string;\n    username: string;\n    name?: string;\n  };\n  error?: string;\n}\n\nexport interface GenerateCallbackOptions {\n  result: AzureOAuthCallbackResult;\n  /**\n   * When true, indicates running in development mode where API (5540) and UI (8080)\n   * are on different ports. In this case, we always redirect to the UI port.\n   * When false (Docker/production), both are on the same port and we use localStorage directly.\n   */\n  isDevMode?: boolean;\n}\n\n/**\n * Escape JSON string for safe embedding in HTML script context.\n * Prevents XSS by escaping sequences that could break out of script tags.\n */\nconst escapeJsonForScript = (json: string): string =>\n  json\n    .replace(/</g, '\\\\u003c') // Escape < to prevent </script> attacks\n    .replace(/>/g, '\\\\u003e') // Escape > for completeness\n    .replace(/&/g, '\\\\u0026'); // Escape & for HTML entity safety\n\n/**\n * Generate HTML page for Azure OAuth web callback.\n * This page redirects to the UI origin with the result, allowing\n * the popup to communicate with the main window via localStorage.\n */\nexport const generateCallbackHtml = (\n  options: GenerateCallbackOptions,\n): string => {\n  const { result, isDevMode = false } = options;\n\n  // Escape JSON for safe embedding in script context.\n  // escapeJsonForScript prevents </script> injection by escaping < > &\n  // No HTML encoding needed since we're in a script context, not HTML content.\n  const resultJson = escapeJsonForScript(JSON.stringify(result));\n\n  return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Azure Authentication</title>\n  <style>\n    body {\n      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      min-height: 100vh;\n      margin: 0;\n      background-color: #f5f5f5;\n    }\n    .container {\n      text-align: center;\n      padding: 40px;\n      background: white;\n      border-radius: 8px;\n      box-shadow: 0 2px 10px rgba(0,0,0,0.1);\n      max-width: 400px;\n    }\n    .title {\n      font-size: 24px;\n      margin-bottom: 16px;\n      color: #333;\n    }\n    .message {\n      color: #666;\n      margin-bottom: 20px;\n    }\n    .spinner {\n      width: 40px;\n      height: 40px;\n      border: 3px solid #f3f3f3;\n      border-top: 3px solid #3498db;\n      border-radius: 50%;\n      animation: spin 1s linear infinite;\n      margin: 0 auto 20px;\n    }\n    @keyframes spin {\n      0% { transform: rotate(0deg); }\n      100% { transform: rotate(360deg); }\n    }\n  </style>\n</head>\n<body>\n  <div class=\"container\">\n    <div class=\"spinner\"></div>\n    <div class=\"title\">Completing authentication...</div>\n    <div class=\"message\">This window will close automatically.</div>\n  </div>\n  <script>\n    (function() {\n      var result = ${resultJson};\n      var isDevMode = ${isDevMode};\n      var STORAGE_KEY = '${AZURE_OAUTH_STORAGE_KEY}';\n      var DEV_UI_PORT = '8080';\n\n      // Function to store result and close\n      function storeAndClose() {\n        try {\n          localStorage.setItem(STORAGE_KEY, JSON.stringify({\n            timestamp: Date.now(),\n            result: result\n          }));\n        } catch (e) {\n          // Storage failed\n        }\n\n        setTimeout(function() { window.close(); }, 1000);\n      }\n\n      // Dev mode: API (5540) and UI (8080) are on different ports/origins\n      // We must redirect to UI origin so localStorage is accessible to the main window\n      // Note: window.opener is null after OAuth redirect through Microsoft, so we can't check it\n      if (isDevMode) {\n        var uiOrigin = window.location.protocol + '//' + window.location.hostname + ':' + DEV_UI_PORT;\n        // Use encodeURIComponent + unescape to handle non-ASCII characters (e.g., international names)\n        // btoa only accepts Latin1 characters, so we must encode Unicode first\n        var encodedResult = encodeURIComponent(btoa(unescape(encodeURIComponent(JSON.stringify(result)))));\n        var redirectUrl = uiOrigin + '/azure-auth-callback?result=' + encodedResult;\n        window.location.href = redirectUrl;\n        return;\n      }\n\n      // Production/Docker: API and UI on same origin, localStorage is shared\n      storeAndClose();\n    })();\n  </script>\n</body>\n</html>`;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/azure-auth.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class AzureAuthAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendAzureSignInSucceeded(sessionMetadata: SessionMetadata) {\n    this.sendEvent(sessionMetadata, TelemetryEvents.AzureSignInSucceeded);\n  }\n\n  sendAzureSignInFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.AzureSignInFailed,\n      exception,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/azure-auth.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  Post,\n  Query,\n  Param,\n  Res,\n  UsePipes,\n  ValidationPipe,\n  Logger,\n  BadRequestException,\n} from '@nestjs/common';\nimport { Response } from 'express';\nimport {\n  ApiTags,\n  ApiOperation,\n  ApiResponse,\n  ApiParam,\n  ApiQuery,\n} from '@nestjs/swagger';\nimport { AzureAuthService } from './azure-auth.service';\nimport { AzureAuthAnalytics } from './azure-auth.analytics';\nimport { AzureAuthStatus, AzureOAuthRedirectType } from '../constants';\nimport { AzureAuthLoginDto, AzureOAuthPrompt } from './dto';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\nimport { wrapHttpError } from 'src/common/utils';\nimport { generateCallbackHtml } from './azure-auth-callback.template';\n\n@ApiTags('Azure Auth')\n@Controller('azure/auth')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class AzureAuthController {\n  private readonly logger = new Logger(AzureAuthController.name);\n\n  constructor(\n    private readonly azureAuthService: AzureAuthService,\n    private readonly analytics: AzureAuthAnalytics,\n  ) {}\n\n  @Get('login')\n  @ApiOperation({\n    summary: 'Get Azure OAuth authorization URL',\n    description:\n      'Returns a URL to redirect the user to Microsoft login for Azure Entra ID authentication.',\n  })\n  @ApiQuery({\n    name: 'prompt',\n    required: false,\n    enum: AzureOAuthPrompt,\n    description:\n      'OAuth prompt parameter: \"select_account\" to show account picker, \"login\" to force re-auth, \"consent\" to force consent dialog',\n  })\n  @ApiQuery({\n    name: 'redirectType',\n    required: false,\n    enum: AzureOAuthRedirectType,\n    description:\n      'Redirect type: \"deeplink\" for Electron app, \"web\" for browser/Docker deployments',\n  })\n  @ApiResponse({\n    status: 200,\n    description: 'Authorization URL generated successfully',\n  })\n  async login(@Query() dto: AzureAuthLoginDto): Promise<{ url: string }> {\n    this.logger.log(\n      `Initiating Azure OAuth login with redirect type: ${dto.redirectType || 'deeplink'}`,\n    );\n    const { url } = await this.azureAuthService.getAuthorizationUrl(\n      dto.prompt,\n      dto.redirectType,\n    );\n    return { url };\n  }\n\n  @Get('callback')\n  @ApiOperation({\n    summary: 'Handle OAuth callback',\n    description:\n      'Exchanges authorization code for tokens. For web redirects, returns HTML with postMessage.',\n  })\n  @ApiResponse({\n    status: 200,\n    description: 'Callback handled successfully',\n  })\n  async callback(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Res({ passthrough: true }) res: Response,\n    @Query('code') code: string,\n    @Query('state') state: string,\n    @Query('error') error: string,\n    @Query('error_description') errorDescription: string,\n  ) {\n    this.logger.log('Handling Azure OAuth callback');\n\n    // Handle OAuth errors from Azure (user denial, consent issues, etc.)\n    if (error) {\n      this.logger.error(`Azure OAuth error: ${error}, ${errorDescription}`);\n      this.analytics.sendAzureSignInFailed(\n        sessionMetadata,\n        new BadRequestException(errorDescription || error),\n      );\n\n      // Clean up the auth request and get redirect type (state may be present even on error)\n      const redirectType = state\n        ? this.azureAuthService.removeAuthRequest(state)\n        : null;\n\n      const errorResult = {\n        status: AzureAuthStatus.Failed,\n        error: errorDescription || error,\n      };\n\n      // For deeplink flows (Electron dev mode), return JSON\n      if (redirectType === AzureOAuthRedirectType.Deeplink) {\n        return errorResult;\n      }\n\n      // For web flows (or unknown), return HTML\n      res.type('text/html');\n      return generateCallbackHtml({\n        result: errorResult,\n        isDevMode: process.env.NODE_ENV === 'development',\n      });\n    }\n\n    if (!code || !state) {\n      this.analytics.sendAzureSignInFailed(\n        sessionMetadata,\n        new BadRequestException('Missing code or state parameter'),\n      );\n\n      // Clean up auth request if state is present (code missing case)\n      const redirectType = state\n        ? this.azureAuthService.removeAuthRequest(state)\n        : null;\n\n      const errorResult = {\n        status: AzureAuthStatus.Failed,\n        error: 'Missing code or state parameter',\n      };\n\n      // For deeplink flows (Electron dev mode), return JSON\n      if (redirectType === AzureOAuthRedirectType.Deeplink) {\n        return errorResult;\n      }\n\n      // No state or web flow - return HTML\n      res.type('text/html');\n      return generateCallbackHtml({\n        result: errorResult,\n        isDevMode: process.env.NODE_ENV === 'development',\n      });\n    }\n\n    try {\n      const result = await this.azureAuthService.handleCallback(code, state);\n      const { redirectType, ...resultWithoutRedirectType } = result;\n\n      if (result.status === AzureAuthStatus.Succeed) {\n        this.analytics.sendAzureSignInSucceeded(sessionMetadata);\n      } else {\n        this.analytics.sendAzureSignInFailed(\n          sessionMetadata,\n          new BadRequestException(result.error || 'Authentication failed'),\n        );\n      }\n\n      // For web redirects, return HTML page with postMessage\n      if (redirectType === AzureOAuthRedirectType.Web) {\n        const callbackResult = {\n          status: result.status,\n          account: result.account\n            ? {\n                id: result.account.homeAccountId,\n                username: result.account.username,\n                name: result.account.name,\n              }\n            : undefined,\n          error: result.error,\n        };\n\n        res.type('text/html');\n        return generateCallbackHtml({\n          result: callbackResult,\n          isDevMode: process.env.NODE_ENV === 'development',\n        });\n      }\n\n      // For deeplink redirects, return JSON (for Electron IPC handling)\n      return resultWithoutRedirectType;\n    } catch (e) {\n      this.logger.error('Azure OAuth callback failed', e);\n      this.analytics.sendAzureSignInFailed(sessionMetadata, wrapHttpError(e));\n\n      // Try to get redirect type from state (might still exist if error happened early)\n      // This ensures Electron dev mode (which uses HTTP for deeplinks) gets JSON response\n      const redirectType = this.azureAuthService.removeAuthRequest(state);\n\n      const errorResult = {\n        status: AzureAuthStatus.Failed,\n        error: e instanceof Error ? e.message : 'Authentication failed',\n      };\n\n      // For deeplink flows, return JSON (for Electron IPC handling)\n      if (redirectType === AzureOAuthRedirectType.Deeplink) {\n        return errorResult;\n      }\n\n      // For web flows (or unknown - state already deleted), return HTML\n      // This ensures the popup can communicate the error to the main window via localStorage.\n      res.type('text/html');\n      return generateCallbackHtml({\n        result: errorResult,\n        isDevMode: process.env.NODE_ENV === 'development',\n      });\n    }\n  }\n\n  @Get('status')\n  @ApiOperation({\n    summary: 'Get authentication status',\n    description:\n      'Returns current auth status and list of authenticated accounts.',\n  })\n  @ApiResponse({\n    status: 200,\n    description: 'Status retrieved successfully',\n  })\n  async status() {\n    return this.azureAuthService.getStatus();\n  }\n\n  @Post('logout/:accountId')\n  @ApiOperation({\n    summary: 'Logout an Azure account',\n    description: 'Removes the account from the token cache.',\n  })\n  @ApiParam({\n    name: 'accountId',\n    description: 'The account ID (homeAccountId) to logout',\n  })\n  @ApiResponse({\n    status: 200,\n    description: 'Logout successful',\n  })\n  async logout(\n    @Param('accountId') accountId: string,\n  ): Promise<{ success: boolean }> {\n    this.logger.log(`Logging out Azure account: ${accountId}`);\n\n    try {\n      await this.azureAuthService.logout(accountId);\n      return { success: true };\n    } catch (error) {\n      return { success: false };\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/azure-auth.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { faker } from '@faker-js/faker';\nimport { PublicClientApplication } from '@azure/msal-node';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { AzureAuthService } from './azure-auth.service';\nimport {\n  AzureAuthStatus,\n  AzureOAuthRedirectType,\n  AzureRedisTokenEvents,\n} from '../constants';\nimport { AzureOAuthPrompt } from './dto';\n\njest.mock('@azure/msal-node');\n\nconst mockEventEmitter = {\n  emit: jest.fn(),\n};\n\nconst MockedPublicClientApplication =\n  PublicClientApplication as jest.MockedClass<typeof PublicClientApplication>;\n\nconst createMockAccount = () => ({\n  homeAccountId: faker.string.uuid(),\n  environment: 'login.microsoftonline.com',\n  tenantId: faker.string.uuid(),\n  username: faker.internet.email(),\n  localAccountId: faker.string.uuid(),\n  name: faker.person.fullName(),\n});\n\ndescribe('AzureAuthService', () => {\n  let service: AzureAuthService;\n  let mockPca: jest.Mocked<PublicClientApplication>;\n  let mockTokenCache: {\n    getAllAccounts: jest.Mock;\n    removeAccount: jest.Mock;\n  };\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    mockTokenCache = {\n      getAllAccounts: jest.fn().mockResolvedValue([]),\n      removeAccount: jest.fn().mockResolvedValue(undefined),\n    };\n\n    mockPca = {\n      getAuthCodeUrl: jest.fn().mockResolvedValue('https://example.com'),\n      acquireTokenByCode: jest.fn(),\n      acquireTokenSilent: jest.fn(),\n      getTokenCache: jest.fn().mockReturnValue(mockTokenCache),\n    } as unknown as jest.Mocked<PublicClientApplication>;\n\n    MockedPublicClientApplication.mockImplementation(() => mockPca);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AzureAuthService,\n        {\n          provide: EventEmitter2,\n          useValue: mockEventEmitter,\n        },\n      ],\n    }).compile();\n\n    service = module.get<AzureAuthService>(AzureAuthService);\n  });\n\n  describe('getAuthorizationUrl', () => {\n    it('should allow concurrent auth requests (multiple tabs or users)', async () => {\n      const mockAccount = createMockAccount();\n      mockPca.acquireTokenByCode.mockResolvedValue({\n        accessToken: faker.string.alphanumeric(100),\n        account: mockAccount,\n      } as any);\n\n      // Create first auth request\n      const { state: firstState } = await service.getAuthorizationUrl();\n\n      // Create second auth request (should NOT clear the first)\n      const { state: secondState } = await service.getAuthorizationUrl();\n\n      // Both states should still be valid\n      const result1 = await service.handleCallback('auth-code-1', firstState);\n      const result2 = await service.handleCallback('auth-code-2', secondState);\n\n      expect(result1.status).toBe(AzureAuthStatus.Succeed);\n      expect(result2.status).toBe(AzureAuthStatus.Succeed);\n    });\n\n    it('should clean up expired auth requests on new request', async () => {\n      mockPca.acquireTokenByCode.mockRejectedValue(new Error('Token error'));\n\n      // Create first auth request\n      const { state: expiredState } = await service.getAuthorizationUrl();\n\n      // Fast-forward time past expiration (10 minutes + 1 second)\n      const originalDateNow = Date.now;\n      const startTime = Date.now();\n      Date.now = jest.fn(() => startTime + 10 * 60 * 1000 + 1000);\n\n      // Create second auth request (should clean up expired first request)\n      await service.getAuthorizationUrl();\n\n      // Restore Date.now\n      Date.now = originalDateNow;\n\n      // First state should no longer be valid (expired and cleaned up)\n      const result = await service.handleCallback('auth-code', expiredState);\n      expect(result.status).toBe(AzureAuthStatus.Failed);\n      expect(result.error).toBe('Invalid or expired authentication state');\n    });\n\n    it('should pass prompt parameter to MSAL when provided', async () => {\n      await service.getAuthorizationUrl(AzureOAuthPrompt.SelectAccount);\n\n      expect(mockPca.getAuthCodeUrl).toHaveBeenCalledWith(\n        expect.objectContaining({\n          prompt: 'select_account',\n        }),\n      );\n    });\n\n    it('should not include prompt parameter when not provided', async () => {\n      await service.getAuthorizationUrl();\n\n      expect(mockPca.getAuthCodeUrl).toHaveBeenCalledWith(\n        expect.not.objectContaining({\n          prompt: expect.anything(),\n        }),\n      );\n    });\n  });\n\n  describe('handleCallback', () => {\n    it('should return failed status with error for unknown state', async () => {\n      const result = await service.handleCallback('auth-code', 'unknown-state');\n\n      expect(result.status).toBe(AzureAuthStatus.Failed);\n      expect(result.error).toBe('Invalid or expired authentication state');\n      expect(result.account).toBeUndefined();\n    });\n\n    it('should return failed status with error when token acquisition fails', async () => {\n      mockPca.acquireTokenByCode.mockRejectedValue(new Error('Token error'));\n\n      const { state } = await service.getAuthorizationUrl();\n      const result = await service.handleCallback('auth-code', state);\n\n      expect(result.status).toBe(AzureAuthStatus.Failed);\n      expect(result.error).toBe('Token error');\n    });\n\n    it('should clean up state after callback', async () => {\n      mockPca.acquireTokenByCode.mockRejectedValue(new Error('Token error'));\n\n      const { state } = await service.getAuthorizationUrl();\n      await service.handleCallback('auth-code', state);\n\n      // Second call with same state should fail (state was cleaned up)\n      const result = await service.handleCallback('auth-code', state);\n      expect(result.status).toBe(AzureAuthStatus.Failed);\n      expect(result.error).toBe('Invalid or expired authentication state');\n    });\n\n    it('should return success status with account on successful token acquisition', async () => {\n      const mockAccount = createMockAccount();\n      mockPca.acquireTokenByCode.mockResolvedValue({\n        accessToken: faker.string.alphanumeric(100),\n        account: mockAccount,\n      } as any);\n\n      const { state } = await service.getAuthorizationUrl();\n      const result = await service.handleCallback('auth-code', state);\n\n      expect(result.status).toBe(AzureAuthStatus.Succeed);\n      expect(result.account).toEqual(mockAccount);\n      expect(result.error).toBeUndefined();\n    });\n  });\n\n  describe('removeAuthRequest', () => {\n    it('should return redirect type and remove state from map', async () => {\n      const { state } = await service.getAuthorizationUrl();\n\n      // Remove should return the redirect type\n      const redirectType = service.removeAuthRequest(state);\n      expect(redirectType).toBe(AzureOAuthRedirectType.Deeplink);\n\n      // handleCallback should fail since state was removed\n      const result = await service.handleCallback('auth-code', state);\n      expect(result.status).toBe(AzureAuthStatus.Failed);\n      expect(result.error).toBe('Invalid or expired authentication state');\n    });\n\n    it('should return null for unknown state', () => {\n      const redirectType = service.removeAuthRequest('unknown-state');\n      expect(redirectType).toBeNull();\n    });\n  });\n\n  describe('getStatus', () => {\n    it('should map accounts to response format', async () => {\n      const mockAccounts = [createMockAccount(), createMockAccount()];\n      mockTokenCache.getAllAccounts.mockResolvedValue(mockAccounts);\n\n      const result = await service.getStatus();\n\n      expect(result.authenticated).toBe(true);\n      expect(result.accounts).toHaveLength(2);\n      expect(result.accounts[0]).toEqual({\n        id: mockAccounts[0].homeAccountId,\n        username: mockAccounts[0].username,\n        name: mockAccounts[0].name,\n      });\n    });\n\n    it('should return not authenticated when no accounts', async () => {\n      mockTokenCache.getAllAccounts.mockResolvedValue([]);\n\n      const result = await service.getStatus();\n\n      expect(result.authenticated).toBe(false);\n      expect(result.accounts).toHaveLength(0);\n    });\n\n    it('should return empty accounts on error', async () => {\n      mockTokenCache.getAllAccounts.mockRejectedValue(new Error('Cache error'));\n\n      const result = await service.getStatus();\n\n      expect(result.authenticated).toBe(false);\n      expect(result.accounts).toHaveLength(0);\n    });\n  });\n\n  describe('logout', () => {\n    it('should not throw when account not found', async () => {\n      mockTokenCache.getAllAccounts.mockResolvedValue([]);\n\n      await expect(\n        service.logout('non-existent-account-id'),\n      ).resolves.not.toThrow();\n    });\n\n    it('should propagate cache errors', async () => {\n      const mockAccount = createMockAccount();\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockTokenCache.removeAccount.mockRejectedValue(new Error('Remove error'));\n\n      await expect(service.logout(mockAccount.homeAccountId)).rejects.toThrow(\n        'Remove error',\n      );\n    });\n  });\n\n  describe('getRedisTokenByAccountId', () => {\n    it('should return null when account not found', async () => {\n      mockTokenCache.getAllAccounts.mockResolvedValue([]);\n\n      const result = await service.getRedisTokenByAccountId('unknown-id');\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when token acquisition fails', async () => {\n      const mockAccount = createMockAccount();\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockRejectedValue(new Error('Silent error'));\n\n      const result = await service.getRedisTokenByAccountId(\n        mockAccount.homeAccountId,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when result has no accessToken', async () => {\n      const mockAccount = createMockAccount();\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockResolvedValue({\n        accessToken: null,\n        expiresOn: new Date(),\n        account: mockAccount,\n      } as any);\n\n      const result = await service.getRedisTokenByAccountId(\n        mockAccount.homeAccountId,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when result has no account', async () => {\n      const mockAccount = createMockAccount();\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockResolvedValue({\n        accessToken: faker.string.alphanumeric(100),\n        expiresOn: new Date(),\n        account: null,\n      } as any);\n\n      const result = await service.getRedisTokenByAccountId(\n        mockAccount.homeAccountId,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should return token result on successful acquisition', async () => {\n      const mockAccount = createMockAccount();\n      const mockExpiresOn = new Date();\n      const mockAccessToken = faker.string.alphanumeric(100);\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockResolvedValue({\n        accessToken: mockAccessToken,\n        expiresOn: mockExpiresOn,\n        account: mockAccount,\n      } as any);\n\n      const result = await service.getRedisTokenByAccountId(\n        mockAccount.homeAccountId,\n      );\n\n      expect(result).toEqual({\n        token: mockAccessToken,\n        expiresOn: mockExpiresOn,\n        account: mockAccount,\n      });\n    });\n\n    it('should emit token acquired event on successful acquisition', async () => {\n      const mockAccount = createMockAccount();\n      const mockExpiresOn = new Date();\n      const mockAccessToken = faker.string.alphanumeric(100);\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockResolvedValue({\n        accessToken: mockAccessToken,\n        expiresOn: mockExpiresOn,\n        account: mockAccount,\n      } as any);\n\n      await service.getRedisTokenByAccountId(mockAccount.homeAccountId);\n\n      expect(mockEventEmitter.emit).toHaveBeenCalledWith(\n        AzureRedisTokenEvents.Acquired,\n        {\n          accountId: mockAccount.homeAccountId,\n          tokenResult: {\n            token: mockAccessToken,\n            expiresOn: mockExpiresOn,\n            account: mockAccount,\n          },\n        },\n      );\n    });\n\n    it('should not emit event when token acquisition fails', async () => {\n      mockTokenCache.getAllAccounts.mockResolvedValue([]);\n\n      await service.getRedisTokenByAccountId('unknown-id');\n\n      expect(mockEventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('getManagementTokenByAccountId', () => {\n    it('should return null when account not found', async () => {\n      mockTokenCache.getAllAccounts.mockResolvedValue([]);\n\n      const result = await service.getManagementTokenByAccountId('unknown-id');\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when token acquisition fails', async () => {\n      const mockAccount = createMockAccount();\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockRejectedValue(new Error('Silent error'));\n\n      const result = await service.getManagementTokenByAccountId(\n        mockAccount.homeAccountId,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should return token result on successful acquisition', async () => {\n      const mockAccount = createMockAccount();\n      const mockExpiresOn = new Date();\n      const mockAccessToken = faker.string.alphanumeric(100);\n      mockTokenCache.getAllAccounts.mockResolvedValue([mockAccount]);\n      mockPca.acquireTokenSilent.mockResolvedValue({\n        accessToken: mockAccessToken,\n        expiresOn: mockExpiresOn,\n        account: mockAccount,\n      } as any);\n\n      const result = await service.getManagementTokenByAccountId(\n        mockAccount.homeAccountId,\n      );\n\n      expect(result).toEqual({\n        token: mockAccessToken,\n        expiresOn: mockExpiresOn,\n        account: mockAccount,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/azure-auth.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport * as crypto from 'crypto';\nimport {\n  PublicClientApplication,\n  Configuration,\n  AccountInfo,\n} from '@azure/msal-node';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  AZURE_AUTHORITY,\n  AZURE_CLIENT_ID,\n  AZURE_REDIS_SCOPE,\n  AZURE_MANAGEMENT_SCOPE,\n  AZURE_OAUTH_DEEPLINK_REDIRECT_PATH,\n  AZURE_OAUTH_SCOPES,\n  AZURE_OAUTH_WEB_CALLBACK_ENDPOINT,\n  AzureAuthStatus,\n  AzureOAuthRedirectType,\n  AzureRedisTokenEvents,\n} from '../constants';\nimport { get, Config } from 'src/utils';\nimport { AzureTokenResult, AzureAuthStatusResponse } from './models';\nimport { AzureOAuthPrompt } from './dto';\n\n/**\n * PKCE (Proof Key for Code Exchange) utilities.\n *\n * Note: MSAL Node <5.x exported CryptoProvider for PKCE generation, but v5.x\n * removed it from the public API. We use Node's built-in crypto module instead,\n * following RFC 7636 (https://tools.ietf.org/html/rfc7636#section-4).\n */\n\n/**\n * Generate a random string for PKCE verifier (43-128 characters).\n * Per RFC 7636, we use 32 random bytes encoded as base64url.\n */\nconst generateCodeVerifier = (): string =>\n  crypto.randomBytes(32).toString('base64url');\n\n/**\n * Generate code challenge from verifier using SHA-256.\n * Per RFC 7636 Section 4.2, this is the S256 method.\n */\nconst generateCodeChallenge = (verifier: string): string =>\n  crypto.createHash('sha256').update(verifier).digest('base64url');\n\n/**\n * Generate a random UUID for state parameter.\n */\nconst generateUuid = (): string => crypto.randomUUID();\n\n/**\n * Service for handling Azure Entra ID authentication.\n * Uses MSAL (Microsoft Authentication Library) for OAuth 2.0 flows.\n */\n/**\n * Data stored for each pending auth request\n */\ninterface AuthRequestData {\n  verifier: string;\n  redirectUri: string;\n  redirectType: AzureOAuthRedirectType;\n  createdAt: number;\n}\n\n/**\n * Maximum age for auth requests before they're considered stale (10 minutes).\n * OAuth flows should complete well within this time.\n */\nconst AUTH_REQUEST_MAX_AGE_MS = 10 * 60 * 1000;\n\n@Injectable()\nexport class AzureAuthService {\n  private readonly logger = new Logger(AzureAuthService.name);\n\n  private pca: PublicClientApplication | null = null;\n\n  /**\n   * Map of state -> auth request data (PKCE verifier + redirect URI)\n   */\n  private authRequests: Map<string, AuthRequestData> = new Map();\n\n  constructor(private readonly eventEmitter: EventEmitter2) {}\n\n  /**\n   * Remove expired auth requests to prevent memory leaks from abandoned flows.\n   */\n  private cleanupExpiredAuthRequests(): void {\n    const now = Date.now();\n    this.authRequests.forEach((data, state) => {\n      if (now - data.createdAt > AUTH_REQUEST_MAX_AGE_MS) {\n        this.authRequests.delete(state);\n        this.logger.debug(`Cleaned up expired auth request: ${state}`);\n      }\n    });\n  }\n\n  /**\n   * Remove an auth request by state and return its redirect type.\n   * Used for cleanup when OAuth errors occur before handleCallback is called.\n   * @returns The redirect type if found, null otherwise\n   */\n  removeAuthRequest(state: string): AzureOAuthRedirectType | null {\n    const authRequest = this.authRequests.get(state);\n    if (authRequest) {\n      this.authRequests.delete(state);\n      return authRequest.redirectType;\n    }\n    return null;\n  }\n\n  /**\n   * Get the redirect URI based on the redirect type.\n   * For web flow, uses externalUrl config if set, otherwise constructs localhost URL.\n   * This allows users to set RI_EXTERNAL_URL when running behind a proxy or custom port.\n   */\n  private getRedirectUri(\n    redirectType: AzureOAuthRedirectType = AzureOAuthRedirectType.Deeplink,\n  ): string {\n    if (redirectType === AzureOAuthRedirectType.Web) {\n      const serverConfig = get('server') as Config['server'];\n      // Use external URL if configured (for Docker port mapping or reverse proxy)\n      if (serverConfig.externalUrl) {\n        const baseUrl = serverConfig.externalUrl.replace(/\\/$/, ''); // Remove trailing slash\n        return `${baseUrl}/api${AZURE_OAUTH_WEB_CALLBACK_ENDPOINT}`;\n      }\n      return `http://localhost:${serverConfig.port}/api${AZURE_OAUTH_WEB_CALLBACK_ENDPOINT}`;\n    }\n    return AZURE_OAUTH_DEEPLINK_REDIRECT_PATH;\n  }\n\n  private getMsalClient(): PublicClientApplication {\n    if (this.pca) {\n      return this.pca;\n    }\n\n    const msalConfig: Configuration = {\n      auth: {\n        clientId: AZURE_CLIENT_ID,\n        authority: AZURE_AUTHORITY,\n      },\n    };\n\n    this.pca = new PublicClientApplication(msalConfig);\n\n    this.logger.debug('MSAL client initialized');\n    return this.pca;\n  }\n\n  /**\n   * Generate authorization URL for OAuth flow.\n   * Returns URL to redirect user to Microsoft login.\n   * @param prompt - Optional prompt parameter to control login behavior.\n   * @param redirectType - Type of redirect (deeplink for Electron, web for browser/Docker)\n   * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-authorization-code\n   */\n  async getAuthorizationUrl(\n    prompt?: AzureOAuthPrompt,\n    redirectType: AzureOAuthRedirectType = AzureOAuthRedirectType.Deeplink,\n  ): Promise<{ url: string; state: string }> {\n    const pca = this.getMsalClient();\n\n    const verifier = generateCodeVerifier();\n    const challenge = generateCodeChallenge(verifier);\n    const state = generateUuid();\n    const redirectUri = this.getRedirectUri(redirectType);\n\n    // Clean up any expired auth requests (abandoned flows) before adding new one\n    this.cleanupExpiredAuthRequests();\n\n    // Store auth request data keyed by unique state UUID\n    this.authRequests.set(state, {\n      verifier,\n      redirectUri,\n      redirectType,\n      createdAt: Date.now(),\n    });\n\n    const authUrl = await pca.getAuthCodeUrl({\n      scopes: AZURE_OAUTH_SCOPES,\n      redirectUri,\n      codeChallenge: challenge,\n      codeChallengeMethod: 'S256',\n      state,\n      ...(prompt && { prompt }),\n    });\n\n    this.logger.debug(\n      `Generated authorization URL with redirect type: ${redirectType}`,\n    );\n    return { url: authUrl, state };\n  }\n\n  /**\n   * Handle OAuth callback - exchange authorization code for tokens.\n   * Returns the result along with the redirect type used for this request.\n   */\n  async handleCallback(\n    code: string,\n    state: string,\n  ): Promise<{\n    status: AzureAuthStatus;\n    account?: AccountInfo;\n    error?: string;\n    redirectType: AzureOAuthRedirectType;\n  }> {\n    const pca = this.getMsalClient();\n\n    const authRequest = this.authRequests.get(state);\n    if (!authRequest) {\n      this.logger.warn(`No auth request found for state: ${state}`);\n      return {\n        status: AzureAuthStatus.Failed,\n        error: 'Invalid or expired authentication state',\n        // Default to Web since deeplink flows redirect to redisinsight:// and never reach HTTP callback\n        redirectType: AzureOAuthRedirectType.Web,\n      };\n    }\n\n    const { verifier, redirectUri, redirectType } = authRequest;\n\n    // Clean up the auth request\n    this.authRequests.delete(state);\n\n    try {\n      const result = await pca.acquireTokenByCode({\n        code,\n        scopes: AZURE_OAUTH_SCOPES,\n        redirectUri,\n        codeVerifier: verifier,\n      });\n\n      this.logger.log(\n        `Authentication successful for account: ${result.account?.username}`,\n      );\n\n      return {\n        status: AzureAuthStatus.Succeed,\n        account: result.account,\n        redirectType,\n      };\n    } catch (error: any) {\n      this.logger.error(`Token acquisition failed: ${error.message}`);\n      return {\n        status: AzureAuthStatus.Failed,\n        error: error.message || 'Token acquisition failed',\n        redirectType,\n      };\n    }\n  }\n\n  async getStatus(): Promise<AzureAuthStatusResponse> {\n    try {\n      const pca = this.getMsalClient();\n      const cache = pca.getTokenCache();\n      const accounts = await cache.getAllAccounts();\n\n      return {\n        authenticated: accounts.length > 0,\n        accounts: accounts.map((account) => ({\n          id: account.homeAccountId,\n          username: account.username,\n          name: account.name,\n        })),\n      };\n    } catch (error: any) {\n      this.logger.error(`Failed to get auth status: ${error.message}`);\n      return {\n        authenticated: false,\n        accounts: [],\n      };\n    }\n  }\n\n  /**\n   * Logout a specific account by removing it from the token cache.\n   */\n  async logout(accountId: string): Promise<void> {\n    try {\n      const pca = this.getMsalClient();\n      const cache = pca.getTokenCache();\n      const accounts = await cache.getAllAccounts();\n\n      const account = accounts.find((a) => a.homeAccountId === accountId);\n      if (account) {\n        await cache.removeAccount(account);\n        this.logger.log(`Logged out account: ${account.username}`);\n      }\n    } catch (error: any) {\n      this.logger.error(`Failed to logout: ${error.message}`);\n      throw error;\n    }\n  }\n\n  /**\n   * Get an access token for Azure Cache for Redis.\n   *\n   * This token is used to authenticate directly with Azure Redis databases\n   * using Entra ID (Azure AD) authentication instead of access keys.\n   *\n   * @see https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication\n   */\n  async getRedisTokenByAccountId(\n    accountId: string,\n  ): Promise<AzureTokenResult | null> {\n    const tokenResult = await this.getTokenByAccountId(\n      accountId,\n      AZURE_REDIS_SCOPE,\n    );\n\n    if (tokenResult) {\n      this.eventEmitter.emit(AzureRedisTokenEvents.Acquired, {\n        accountId,\n        tokenResult,\n      });\n    }\n\n    return tokenResult;\n  }\n\n  /**\n   * Get an access token for Azure Resource Manager (ARM) API.\n   *\n   * This token is used to call Azure Management APIs for autodiscovery:\n   * - List subscriptions the user has access to\n   * - List Azure Cache for Redis instances in each subscription\n   * - Get connection details (host, port, SSL settings)\n   *\n   * Note: Azure AD doesn't allow requesting tokens for multiple resources\n   * (redis.azure.com AND management.azure.com) in a single OAuth request.\n   * We request the Redis scope during login and acquire this scope silently\n   * when needed for autodiscovery.\n   *\n   * @see https://learn.microsoft.com/en-us/rest/api/redis/\n   */\n  async getManagementTokenByAccountId(\n    accountId: string,\n  ): Promise<AzureTokenResult | null> {\n    return this.getTokenByAccountId(accountId, AZURE_MANAGEMENT_SCOPE);\n  }\n\n  /**\n   * Get an access token for a specific account and scope.\n   * Uses silent token acquisition with cached refresh token.\n   */\n  private async getTokenByAccountId(\n    accountId: string,\n    scope: string,\n  ): Promise<AzureTokenResult | null> {\n    try {\n      const pca = this.getMsalClient();\n      const cache = pca.getTokenCache();\n      const accounts = await cache.getAllAccounts();\n\n      const account = accounts.find((a) => a.homeAccountId === accountId);\n      if (!account) {\n        this.logger.warn(`Account not found: ${accountId}`);\n        return null;\n      }\n\n      const result = await pca.acquireTokenSilent({\n        account,\n        scopes: [scope],\n      });\n\n      if (!result?.accessToken || !result?.expiresOn || !result?.account) {\n        return null;\n      }\n\n      return {\n        token: result.accessToken,\n        expiresOn: result.expiresOn,\n        account: result.account,\n      };\n    } catch (error: any) {\n      this.logger.error(\n        `Failed to get token for ${accountId}: ${error.message}`,\n      );\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/dto/azure-auth-login.dto.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsEnum, IsOptional } from 'class-validator';\nimport { AzureOAuthRedirectType } from '../../constants';\n\n/**\n * Valid OAuth prompt parameter values for Azure Entra ID.\n * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-authorization-code\n */\nexport enum AzureOAuthPrompt {\n  /**\n   * Force the account picker to appear, allowing the user to select a different account.\n   */\n  SelectAccount = 'select_account',\n\n  /**\n   * Force re-authentication, even if the user has a valid session.\n   */\n  Login = 'login',\n\n  /**\n   * Force the consent dialog to appear, even if consent was previously granted.\n   */\n  Consent = 'consent',\n}\n\nexport class AzureAuthLoginDto {\n  @ApiPropertyOptional({\n    description:\n      'OAuth prompt parameter to control login behavior. ' +\n      '\"select_account\" shows account picker, \"login\" forces re-auth, \"consent\" forces consent dialog.',\n    enum: AzureOAuthPrompt,\n    example: AzureOAuthPrompt.SelectAccount,\n  })\n  @IsOptional()\n  @IsEnum(AzureOAuthPrompt, {\n    message: `prompt must be a valid value. Valid values: ${Object.values(AzureOAuthPrompt).join(', ')}.`,\n  })\n  prompt?: AzureOAuthPrompt;\n\n  @ApiPropertyOptional({\n    description:\n      'OAuth redirect type. ' +\n      '\"deeplink\" uses redisinsight:// protocol for Electron, \"web\" uses localhost HTTP callback for browser/Docker.',\n    enum: AzureOAuthRedirectType,\n    example: AzureOAuthRedirectType.Deeplink,\n    default: AzureOAuthRedirectType.Deeplink,\n  })\n  @IsOptional()\n  @IsEnum(AzureOAuthRedirectType, {\n    message: `redirectType must be a valid value. Valid values: ${Object.values(AzureOAuthRedirectType).join(', ')}.`,\n  })\n  redirectType?: AzureOAuthRedirectType;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/dto/index.ts",
    "content": "export * from './azure-auth-login.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/models/azure-auth.ts",
    "content": "import { AccountInfo } from '@azure/msal-node';\n\nexport interface AzureTokenResult {\n  token: string;\n  expiresOn: Date;\n  account: AccountInfo;\n}\n\nexport interface AzureAuthStatusResponse {\n  authenticated: boolean;\n  accounts: Array<{\n    id: string;\n    username: string;\n    name?: string;\n  }>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/auth/models/index.ts",
    "content": "export * from './azure-auth';\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/azure-autodiscovery.analytics.ts",
    "content": "import { countBy } from 'lodash';\nimport { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { AzureSubscription, AzureRedisDatabase } from '../models';\nimport {\n  AzureRedisType,\n  AzureSubscriptionState,\n  AzureProvisioningState,\n} from '../constants';\n\n@Injectable()\nexport class AzureAutodiscoveryAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendAzureSubscriptionsDiscoverySucceeded(\n    sessionMetadata: SessionMetadata,\n    subscriptions: AzureSubscription[] = [],\n  ) {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.AzureSubscriptionsDiscoverySucceeded,\n        {\n          totalSubscriptions: subscriptions.length,\n          activeSubscriptions: subscriptions.filter(\n            (sub) => sub.state === AzureSubscriptionState.Enabled,\n          ).length,\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendAzureSubscriptionsDiscoveryFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.AzureSubscriptionsDiscoveryFailed,\n      exception,\n    );\n  }\n\n  sendAzureDatabasesDiscoverySucceeded(\n    sessionMetadata: SessionMetadata,\n    databases: AzureRedisDatabase[] = [],\n  ) {\n    try {\n      const typeCount = countBy(databases, 'type');\n\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.AzureDatabasesDiscoverySucceeded,\n        {\n          totalDatabases: databases.length,\n          standardDatabases: typeCount[AzureRedisType.Standard] || 0,\n          enterpriseDatabases: typeCount[AzureRedisType.Enterprise] || 0,\n          activeDatabases: databases.filter(\n            (db) => db.provisioningState === AzureProvisioningState.Succeeded,\n          ).length,\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendAzureDatabasesDiscoveryFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.AzureDatabasesDiscoveryFailed,\n      exception,\n    );\n  }\n\n  sendAzureDatabaseAdded(\n    sessionMetadata: SessionMetadata,\n    databaseType: AzureRedisType,\n  ) {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.AzureDatabaseAdded, {\n        databaseType,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendAzureDatabaseAddFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    databaseType?: AzureRedisType,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.AzureDatabaseAddFailed,\n      exception,\n      { databaseType },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/azure-autodiscovery.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  Post,\n  Body,\n  Query,\n  Param,\n  Res,\n  HttpStatus,\n  HttpException,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';\nimport { Response } from 'express';\nimport { AzureAutodiscoveryService } from './azure-autodiscovery.service';\nimport { AzureAutodiscoveryAnalytics } from './azure-autodiscovery.analytics';\nimport { AzureAuthService } from '../auth/azure-auth.service';\nimport { AZURE_SUBSCRIPTION_ID_REGEX } from '../constants';\nimport { AzureSubscription, AzureRedisDatabase } from '../models';\nimport { ImportAzureDatabasesDto, ImportAzureDatabaseResponse } from './dto';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { wrapHttpError } from 'src/common/utils';\n\n@ApiTags('Azure')\n@Controller('azure')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class AzureAutodiscoveryController {\n  constructor(\n    private readonly autodiscoveryService: AzureAutodiscoveryService,\n    private readonly authService: AzureAuthService,\n    private readonly analytics: AzureAutodiscoveryAnalytics,\n  ) {}\n\n  private validateSubscriptionId(subscriptionId: string): void {\n    if (!subscriptionId || !AZURE_SUBSCRIPTION_ID_REGEX.test(subscriptionId)) {\n      throw new HttpException(\n        'Invalid subscription ID format',\n        HttpStatus.BAD_REQUEST,\n      );\n    }\n  }\n\n  private async ensureAuthenticated(accountId: string): Promise<void> {\n    const status = await this.authService.getStatus();\n\n    if (!status.authenticated) {\n      throw new HttpException(\n        'Not authenticated with Azure',\n        HttpStatus.UNAUTHORIZED,\n      );\n    }\n\n    const accountExists = status.accounts.some((acc) => acc.id === accountId);\n    if (!accountExists) {\n      throw new HttpException(\n        'Invalid Azure account ID',\n        HttpStatus.UNAUTHORIZED,\n      );\n    }\n  }\n\n  @Get('subscriptions')\n  @ApiOperation({ summary: 'List Azure subscriptions' })\n  @ApiQuery({\n    name: 'accountId',\n    description: 'Azure account ID (homeAccountId)',\n  })\n  @ApiResponse({\n    status: 200,\n    description: 'Returns list of subscriptions',\n    type: AzureSubscription,\n    isArray: true,\n  })\n  @ApiResponse({ status: 401, description: 'Not authenticated' })\n  async listSubscriptions(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Query('accountId') accountId: string,\n  ): Promise<AzureSubscription[]> {\n    try {\n      await this.ensureAuthenticated(accountId);\n      const subscriptions =\n        await this.autodiscoveryService.listSubscriptions(accountId);\n      this.analytics.sendAzureSubscriptionsDiscoverySucceeded(\n        sessionMetadata,\n        subscriptions,\n      );\n      return subscriptions;\n    } catch (e) {\n      this.analytics.sendAzureSubscriptionsDiscoveryFailed(\n        sessionMetadata,\n        wrapHttpError(e),\n      );\n      throw e;\n    }\n  }\n\n  @Get('subscriptions/:subscriptionId/databases')\n  @ApiOperation({ summary: 'List Redis databases in a specific subscription' })\n  @ApiQuery({\n    name: 'accountId',\n    description: 'Azure account ID (homeAccountId)',\n  })\n  @ApiResponse({\n    status: 200,\n    description: 'Returns list of databases in subscription',\n    type: AzureRedisDatabase,\n    isArray: true,\n  })\n  @ApiResponse({ status: 400, description: 'Invalid subscription ID format' })\n  @ApiResponse({ status: 401, description: 'Not authenticated' })\n  async listDatabasesInSubscription(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Query('accountId') accountId: string,\n    @Param('subscriptionId') subscriptionId: string,\n  ): Promise<AzureRedisDatabase[]> {\n    try {\n      this.validateSubscriptionId(subscriptionId);\n      await this.ensureAuthenticated(accountId);\n      const databases =\n        await this.autodiscoveryService.listDatabasesInSubscription(\n          accountId,\n          subscriptionId,\n        );\n      this.analytics.sendAzureDatabasesDiscoverySucceeded(\n        sessionMetadata,\n        databases,\n      );\n      return databases;\n    } catch (e) {\n      this.analytics.sendAzureDatabasesDiscoveryFailed(\n        sessionMetadata,\n        wrapHttpError(e),\n      );\n      throw e;\n    }\n  }\n\n  @Post('autodiscovery/databases')\n  @ApiOperation({ summary: 'Add Azure databases from autodiscovery' })\n  @ApiResponse({\n    status: 201,\n    description: 'Added databases list',\n    type: ImportAzureDatabaseResponse,\n    isArray: true,\n  })\n  @ApiResponse({ status: 401, description: 'Not authenticated' })\n  async addDiscoveredDatabases(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: ImportAzureDatabasesDto,\n    @Res() res: Response,\n  ): Promise<Response> {\n    await this.ensureAuthenticated(dto.accountId);\n\n    const result = await this.autodiscoveryService.addDatabases(\n      sessionMetadata,\n      dto.accountId,\n      dto.databases,\n    );\n\n    const hasSuccessResult = result.some(\n      (addResponse: ImportAzureDatabaseResponse) =>\n        addResponse.status === ActionStatus.Success,\n    );\n\n    if (!hasSuccessResult) {\n      return res.status(200).json(result);\n    }\n\n    return res.status(201).json(result);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/azure-autodiscovery.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { faker } from '@faker-js/faker';\nimport axios from 'axios';\nimport { AzureAutodiscoveryService } from './azure-autodiscovery.service';\nimport { AzureAutodiscoveryAnalytics } from './azure-autodiscovery.analytics';\nimport { AzureAuthService } from '../auth/azure-auth.service';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport {\n  AzureRedisType,\n  AzureAuthType,\n  AzureSubscriptionState,\n  AzureProvisioningState,\n} from '../constants';\nimport { AzureRedisDatabase } from '../models';\nimport { ActionStatus } from 'src/common/models';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { CloudProvider } from 'src/modules/database/models/provider-details';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\njest.mock('axios');\n\nconst mockDatabaseService = {\n  create: jest.fn(),\n};\n\nconst mockAnalytics = {\n  sendAzureDatabaseAdded: jest.fn(),\n  sendAzureDatabaseAddFailed: jest.fn(),\n};\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\nconst createMockAccount = () => ({\n  homeAccountId: faker.string.uuid(),\n  localAccountId: faker.string.uuid(),\n  environment: 'login.microsoftonline.com',\n  tenantId: faker.string.uuid(),\n  username: faker.internet.email(),\n  name: faker.person.fullName(),\n});\n\nconst createMockSubscription = () => ({\n  subscriptionId: faker.string.uuid(),\n  displayName: faker.company.name(),\n  state: AzureSubscriptionState.Enabled,\n});\n\nconst createMockStandardRedis = (subscriptionId: string) => {\n  const name = faker.word.noun();\n  const resourceGroup = faker.word.noun();\n  return {\n    id: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Cache/redis/${name}`,\n    name,\n    location: faker.location.city(),\n    properties: {\n      hostName: `${name}.redis.cache.windows.net`,\n      port: 6379,\n      sslPort: 6380,\n      provisioningState: AzureProvisioningState.Succeeded,\n      sku: { name: 'Basic', family: 'C', capacity: 0 },\n    },\n  };\n};\n\nconst createMockEnterpriseCluster = (subscriptionId: string) => {\n  const name = faker.word.noun();\n  const resourceGroup = faker.word.noun();\n  return {\n    id: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Cache/redisEnterprise/${name}`,\n    name,\n    location: 'East US',\n    sku: { name: 'Enterprise_E10' },\n  };\n};\n\nconst createMockEnterpriseDatabase = (\n  subscriptionId: string,\n  resourceGroup: string,\n  clusterName: string,\n) => ({\n  id: `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Cache/redisEnterprise/${clusterName}/databases/default`,\n  name: 'default',\n  properties: {\n    port: 10000,\n    provisioningState: AzureProvisioningState.Succeeded,\n  },\n});\n\nconst createMockDatabase = (\n  type: AzureRedisType = AzureRedisType.Standard,\n): AzureRedisDatabase => {\n  const subscriptionId = faker.string.uuid();\n  const resourceGroup = faker.word.noun();\n  const name = faker.word.noun();\n  const provider =\n    type === AzureRedisType.Standard\n      ? 'Microsoft.Cache/redis'\n      : 'Microsoft.Cache/redisEnterprise';\n  const id = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${provider}/${name}`;\n\n  return {\n    id,\n    name,\n    subscriptionId,\n    resourceGroup,\n    location: faker.location.city(),\n    type,\n    host: faker.internet.domainName(),\n    port: type === AzureRedisType.Standard ? 6379 : 10000,\n    sslPort: type === AzureRedisType.Standard ? 6380 : undefined,\n    provisioningState: AzureProvisioningState.Succeeded,\n  };\n};\n\n// Helper to create raw API response that maps to the database\nconst createStandardRedisApiResponse = (database: AzureRedisDatabase) => ({\n  id: database.id,\n  name: database.name,\n  location: database.location,\n  properties: {\n    hostName: database.host,\n    port: database.port,\n    sslPort: database.sslPort,\n    provisioningState: database.provisioningState,\n    sku: database.sku,\n  },\n});\n\ndescribe('AzureAutodiscoveryService', () => {\n  let service: AzureAutodiscoveryService;\n  let mockAuthService: jest.Mocked<AzureAuthService>;\n  let mockAxiosInstance: {\n    get: jest.Mock;\n    post: jest.Mock;\n  };\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    mockAxiosInstance = {\n      get: jest.fn(),\n      post: jest.fn(),\n    };\n\n    mockedAxios.create.mockReturnValue(mockAxiosInstance as any);\n\n    mockAuthService = {\n      getManagementTokenByAccountId: jest.fn(),\n      getRedisTokenByAccountId: jest.fn(),\n    } as unknown as jest.Mocked<AzureAuthService>;\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AzureAutodiscoveryService,\n        { provide: AzureAuthService, useValue: mockAuthService },\n        { provide: DatabaseService, useValue: mockDatabaseService },\n        { provide: AzureAutodiscoveryAnalytics, useValue: mockAnalytics },\n      ],\n    }).compile();\n\n    service = module.get<AzureAutodiscoveryService>(AzureAutodiscoveryService);\n  });\n\n  describe('listSubscriptions', () => {\n    it('should throw error when no token available', async () => {\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue(null);\n\n      await expect(service.listSubscriptions('account-id')).rejects.toThrow(\n        'Failed to get authenticated client',\n      );\n    });\n\n    it('should return subscriptions on success', async () => {\n      const mockSubs = [createMockSubscription(), createMockSubscription()];\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get.mockResolvedValue({ data: { value: mockSubs } });\n\n      const result = await service.listSubscriptions('account-id');\n\n      expect(result).toHaveLength(2);\n      expect(result[0].subscriptionId).toBe(mockSubs[0].subscriptionId);\n      expect(result[0].displayName).toBe(mockSubs[0].displayName);\n    });\n\n    it('should throw error on API error', async () => {\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get.mockRejectedValue(new Error('API error'));\n\n      await expect(service.listSubscriptions('account-id')).rejects.toThrow(\n        'API error',\n      );\n    });\n\n    it('should handle paginated responses', async () => {\n      const mockSubsPage1 = [\n        createMockSubscription(),\n        createMockSubscription(),\n      ];\n      const mockSubsPage2 = [createMockSubscription()];\n      const nextLink =\n        'https://management.azure.com/subscriptions?$skiptoken=abc123';\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: mockSubsPage1, nextLink } })\n        .mockResolvedValueOnce({ data: { value: mockSubsPage2 } });\n\n      const result = await service.listSubscriptions('account-id');\n\n      expect(result).toHaveLength(3);\n      expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2);\n      expect(mockAxiosInstance.get).toHaveBeenNthCalledWith(2, nextLink);\n    });\n  });\n\n  describe('listDatabasesInSubscription', () => {\n    const subscriptionId = faker.string.uuid();\n\n    it('should throw error when subscription ID is invalid', async () => {\n      await expect(\n        service.listDatabasesInSubscription('account-id', 'invalid-sub-id'),\n      ).rejects.toThrow('Invalid subscription ID format');\n      expect(\n        mockAuthService.getManagementTokenByAccountId,\n      ).not.toHaveBeenCalled();\n    });\n\n    it('should throw error when no token available', async () => {\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue(null);\n\n      await expect(\n        service.listDatabasesInSubscription('account-id', subscriptionId),\n      ).rejects.toThrow('Failed to get authenticated client');\n    });\n\n    it('should return standard Redis databases', async () => {\n      const mockRedis = createMockStandardRedis(subscriptionId);\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [mockRedis] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n\n      const result = await service.listDatabasesInSubscription(\n        'account-id',\n        subscriptionId,\n      );\n\n      expect(result).toHaveLength(1);\n      expect(result[0].type).toBe(AzureRedisType.Standard);\n      expect(result[0].name).toBe(mockRedis.name);\n    });\n\n    it('should return enterprise Redis databases', async () => {\n      const mockCluster = createMockEnterpriseCluster(subscriptionId);\n      const resourceGroup = mockCluster.id.match(\n        /resourceGroups\\/([^/]+)/i,\n      )?.[1];\n      const mockDb = createMockEnterpriseDatabase(\n        subscriptionId,\n        resourceGroup!,\n        mockCluster.name,\n      );\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [] } })\n        .mockResolvedValueOnce({ data: { value: [mockCluster] } })\n        .mockResolvedValueOnce({ data: { value: [mockDb] } });\n\n      const result = await service.listDatabasesInSubscription(\n        'account-id',\n        subscriptionId,\n      );\n\n      expect(result).toHaveLength(1);\n      expect(result[0].type).toBe(AzureRedisType.Enterprise);\n      expect(result[0].name).toBe(`${mockCluster.name}/default`);\n    });\n\n    it('should throw error on API error', async () => {\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get.mockRejectedValue(new Error('API error'));\n\n      await expect(\n        service.listDatabasesInSubscription('account-id', subscriptionId),\n      ).rejects.toThrow('API error');\n    });\n  });\n\n  describe('getConnectionDetails', () => {\n    it('should throw error when no token available', async () => {\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue(null);\n      const database = createMockDatabase();\n\n      await expect(\n        service.getConnectionDetails('account-id', database.id),\n      ).rejects.toThrow('Failed to get authenticated client');\n    });\n\n    it('should return Entra ID connection details when Redis token available', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n      const mockAccount = createMockAccount();\n      const apiResponse = createStandardRedisApiResponse(database);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      // Mock get calls for listDatabasesInSubscription (standard + enterprise)\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'redis-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n\n      const result = await service.getConnectionDetails(\n        'account-id',\n        database.id,\n      );\n\n      expect(result).not.toBeNull();\n      expect(result!.authType).toBe(AzureAuthType.EntraId);\n      expect(result!.username).toBe(mockAccount.localAccountId);\n      expect(result!.azureAccountId).toBe('account-id');\n      expect(result!.port).toBe(database.sslPort);\n    });\n\n    it('should return null when Entra ID token is not available', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n      const apiResponse = createStandardRedisApiResponse(database);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      // Mock get calls for listDatabasesInSubscription (standard + enterprise)\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      // Entra ID token not available\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue(null);\n\n      const result = await service.getConnectionDetails(\n        'account-id',\n        database.id,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when database is not found', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      // Mock get calls to return empty lists (database not found)\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n\n      const result = await service.getConnectionDetails(\n        'account-id',\n        database.id,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when resource ID format is invalid', async () => {\n      const invalidDatabaseId = 'invalid-resource-id';\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n\n      const result = await service.getConnectionDetails(\n        'account-id',\n        invalidDatabaseId,\n      );\n\n      expect(result).toBeNull();\n    });\n\n    it('should find database with case-insensitive resource ID comparison', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n      const mockAccount = createMockAccount();\n      const apiResponse = createStandardRedisApiResponse(database);\n      // Use different casing for the resource ID\n      const differentCaseId = database.id.toUpperCase();\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'redis-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n\n      const result = await service.getConnectionDetails(\n        'account-id',\n        differentCaseId,\n      );\n\n      expect(result).not.toBeNull();\n      expect(result!.authType).toBe(AzureAuthType.EntraId);\n    });\n  });\n\n  describe('addDatabases', () => {\n    const accountId = 'account-id';\n    const sessionMetadata = {\n      userId: 'user-id',\n      sessionId: 'session-id',\n      accountId: 'session-account-id',\n    };\n\n    beforeEach(() => {\n      mockDatabaseService.create.mockReset();\n    });\n\n    it('should successfully add a standard Redis database', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n      const mockAccount = createMockAccount();\n      const apiResponse = createStandardRedisApiResponse(database);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'redis-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockDatabaseService.create.mockResolvedValue({ id: 'new-db-id' });\n\n      const result = await service.addDatabases(sessionMetadata, accountId, [\n        { id: database.id },\n      ]);\n\n      expect(result).toHaveLength(1);\n      expect(result[0].id).toBe(database.id);\n      expect(result[0].status).toBe(ActionStatus.Success);\n      expect(result[0].message).toBeUndefined();\n      expect(mockDatabaseService.create).toHaveBeenCalledWith(\n        sessionMetadata,\n        expect.objectContaining({\n          host: database.host,\n          port: database.sslPort,\n          name: database.name,\n          tls: true,\n          provider: HostingProvider.AZURE_CACHE,\n          providerDetails: {\n            provider: CloudProvider.Azure,\n            authType: AzureAuthType.EntraId,\n            azureAccountId: accountId,\n          },\n        }),\n      );\n    });\n\n    it('should successfully add an enterprise Redis database', async () => {\n      const subscriptionId = faker.string.uuid();\n      const mockCluster = createMockEnterpriseCluster(subscriptionId);\n      const resourceGroup = mockCluster.id.match(\n        /resourceGroups\\/([^/]+)/i,\n      )?.[1];\n      const mockDb = createMockEnterpriseDatabase(\n        subscriptionId,\n        resourceGroup!,\n        mockCluster.name,\n      );\n      const mockAccount = createMockAccount();\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      // Mock: empty standard, then cluster, then database within cluster\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [] } })\n        .mockResolvedValueOnce({ data: { value: [mockCluster] } })\n        .mockResolvedValueOnce({ data: { value: [mockDb] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'redis-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockDatabaseService.create.mockResolvedValue({ id: 'new-db-id' });\n\n      const result = await service.addDatabases(sessionMetadata, accountId, [\n        { id: mockDb.id },\n      ]);\n\n      expect(result).toHaveLength(1);\n      expect(result[0].status).toBe(ActionStatus.Success);\n      expect(mockDatabaseService.create).toHaveBeenCalledWith(\n        sessionMetadata,\n        expect.objectContaining({\n          provider: HostingProvider.AZURE_CACHE_REDIS_ENTERPRISE,\n        }),\n      );\n    });\n\n    it('should return fail status when database is not found', async () => {\n      const testSubscriptionId = faker.string.uuid();\n      const databaseId = `/subscriptions/${testSubscriptionId}/resourceGroups/rg/providers/Microsoft.Cache/redis/not-found`;\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n\n      const result = await service.addDatabases(sessionMetadata, accountId, [\n        { id: databaseId },\n      ]);\n\n      expect(result).toHaveLength(1);\n      expect(result[0].id).toBe(databaseId);\n      expect(result[0].status).toBe(ActionStatus.Fail);\n      expect(result[0].message).toBe(ERROR_MESSAGES.AZURE_DATABASE_NOT_FOUND);\n      expect(mockDatabaseService.create).not.toHaveBeenCalled();\n    });\n\n    it('should return fail status when connection details are not available', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n      const apiResponse = createStandardRedisApiResponse(database);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: createMockAccount(),\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue(null);\n\n      const result = await service.addDatabases(sessionMetadata, accountId, [\n        { id: database.id },\n      ]);\n\n      expect(result).toHaveLength(1);\n      expect(result[0].status).toBe(ActionStatus.Fail);\n      expect(result[0].message).toBe(\n        ERROR_MESSAGES.AZURE_FAILED_TO_GET_CONNECTION_DETAILS,\n      );\n      expect(mockDatabaseService.create).not.toHaveBeenCalled();\n    });\n\n    it('should return fail status with user-friendly error message on creation error', async () => {\n      const database = createMockDatabase(AzureRedisType.Standard);\n      const mockAccount = createMockAccount();\n      const apiResponse = createStandardRedisApiResponse(database);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'redis-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockDatabaseService.create.mockRejectedValue(\n        new Error('WRONGPASS invalid username-password pair'),\n      );\n\n      const result = await service.addDatabases(sessionMetadata, accountId, [\n        { id: database.id },\n      ]);\n\n      expect(result).toHaveLength(1);\n      expect(result[0].status).toBe(ActionStatus.Fail);\n      expect(result[0].message).toBe(ERROR_MESSAGES.AZURE_ENTRA_ID_AUTH_FAILED);\n    });\n\n    it('should handle multiple databases with mixed results', async () => {\n      const database1 = createMockDatabase(AzureRedisType.Standard);\n      const database2 = createMockDatabase(AzureRedisType.Standard);\n      const mockAccount = createMockAccount();\n      const apiResponse1 = createStandardRedisApiResponse(database1);\n\n      mockAuthService.getManagementTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      // First database found, second not found\n      mockAxiosInstance.get\n        .mockResolvedValueOnce({ data: { value: [apiResponse1] } })\n        .mockResolvedValueOnce({ data: { value: [] } })\n        .mockResolvedValueOnce({ data: { value: [] } })\n        .mockResolvedValueOnce({ data: { value: [] } });\n      mockAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'redis-token',\n        expiresOn: new Date(),\n        account: mockAccount,\n      });\n      mockDatabaseService.create.mockResolvedValue({ id: 'new-db-id' });\n\n      const result = await service.addDatabases(sessionMetadata, accountId, [\n        { id: database1.id },\n        { id: database2.id },\n      ]);\n\n      expect(result).toHaveLength(2);\n      expect(result[0].status).toBe(ActionStatus.Success);\n      expect(result[1].status).toBe(ActionStatus.Fail);\n      expect(result[1].message).toBe(ERROR_MESSAGES.AZURE_DATABASE_NOT_FOUND);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/azure-autodiscovery.service.ts",
    "content": "import { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport axios, { AxiosInstance } from 'axios';\nimport { PromisePool } from '@supercharge/promise-pool';\nimport { AzureAuthService } from '../auth/azure-auth.service';\nimport { AzureAutodiscoveryAnalytics } from './azure-autodiscovery.analytics';\nimport {\n  AZURE_API_BASE,\n  AUTODISCOVERY_MAX_CONCURRENT_REQUESTS,\n  AZURE_SUBSCRIPTION_ID_REGEX,\n  AzureApiUrls,\n  AzureRedisType,\n  AzureAuthType,\n} from '../constants';\nimport {\n  AzureSubscription,\n  AzureRedisDatabase,\n  AzureConnectionDetails,\n} from '../models';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { CloudProvider } from 'src/modules/database/models/provider-details';\nimport { ImportAzureDatabaseDto, ImportAzureDatabaseResponse } from './dto';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\n@Injectable()\nexport class AzureAutodiscoveryService {\n  private readonly logger = new Logger(AzureAutodiscoveryService.name);\n\n  constructor(\n    private readonly authService: AzureAuthService,\n    private readonly databaseService: DatabaseService,\n    private readonly analytics: AzureAutodiscoveryAnalytics,\n  ) {}\n\n  /**\n   * Maps raw error messages to user-friendly messages\n   */\n  private getUserFriendlyErrorMessage(error: Error): string {\n    const message = error?.message?.toLowerCase() || '';\n\n    if (\n      message.includes('wrongpass') ||\n      message.includes('noauth') ||\n      message.includes('please check the username or password')\n    ) {\n      return ERROR_MESSAGES.AZURE_ENTRA_ID_AUTH_FAILED;\n    }\n\n    if (message.includes('please check the ca or client certificate')) {\n      return ERROR_MESSAGES.AZURE_TLS_CERTIFICATE_ERROR;\n    }\n\n    return error?.message || ERROR_MESSAGES.AZURE_UNEXPECTED_ERROR;\n  }\n\n  private isValidSubscriptionId(subscriptionId: string): boolean {\n    return !!subscriptionId && AZURE_SUBSCRIPTION_ID_REGEX.test(subscriptionId);\n  }\n\n  private async getAuthenticatedClient(\n    accountId: string,\n  ): Promise<AxiosInstance | null> {\n    const tokenResult =\n      await this.authService.getManagementTokenByAccountId(accountId);\n\n    if (!tokenResult) {\n      this.logger.warn('No valid management token available');\n      return null;\n    }\n\n    return axios.create({\n      baseURL: AZURE_API_BASE,\n      headers: {\n        Authorization: `Bearer ${tokenResult.token}`,\n        'Content-Type': 'application/json',\n      },\n    });\n  }\n\n  /**\n   * Fetches all pages from a paginated Azure API endpoint.\n   * Azure REST APIs return paginated results with a `nextLink` property when\n   * there are more items than fit in a single response (typically 1000+ items).\n   * @see https://learn.microsoft.com/en-us/rest/api/azure/#async-operations-throttling-and-paging\n   */\n  private async fetchAllPages<T>(\n    client: AxiosInstance,\n    initialUrl: string,\n  ): Promise<T[]> {\n    const allItems: T[] = [];\n    let url: string | null = initialUrl;\n\n    while (url) {\n      const response = await client.get(url);\n      allItems.push(...(response.data.value || []));\n      url = response.data.nextLink || null;\n    }\n\n    return allItems;\n  }\n\n  async listSubscriptions(accountId: string): Promise<AzureSubscription[]> {\n    const client = await this.getAuthenticatedClient(accountId);\n\n    if (!client) {\n      throw new BadRequestException('Failed to get authenticated client');\n    }\n\n    const subscriptions = await this.fetchAllPages<any>(\n      client,\n      AzureApiUrls.getSubscriptions(),\n    );\n\n    return subscriptions.map((sub: any) => ({\n      subscriptionId: sub.subscriptionId,\n      displayName: sub.displayName,\n      state: sub.state,\n    }));\n  }\n\n  async listDatabasesInSubscription(\n    accountId: string,\n    subscriptionId: string,\n  ): Promise<AzureRedisDatabase[]> {\n    if (!this.isValidSubscriptionId(subscriptionId)) {\n      throw new BadRequestException(\n        `Invalid subscription ID format: ${subscriptionId}`,\n      );\n    }\n\n    const client = await this.getAuthenticatedClient(accountId);\n\n    if (!client) {\n      throw new BadRequestException('Failed to get authenticated client');\n    }\n\n    const results = await Promise.allSettled([\n      this.fetchStandardRedis(client, subscriptionId),\n      this.fetchEnterpriseRedis(client, subscriptionId),\n    ]);\n\n    const databases: AzureRedisDatabase[] = [];\n    const errors: Error[] = [];\n\n    results.forEach((result) => {\n      if (result.status === 'fulfilled') {\n        databases.push(...result.value);\n      } else {\n        errors.push(result.reason);\n      }\n    });\n\n    errors.forEach((error) => {\n      this.logger.error('Failed to fetch some Azure databases', error);\n    });\n\n    if (databases.length === 0 && errors.length > 0) {\n      throw errors[0];\n    }\n\n    return databases;\n  }\n\n  async getConnectionDetails(\n    accountId: string,\n    databaseId: string,\n  ): Promise<AzureConnectionDetails | null> {\n    const database = await this.findDatabaseById(accountId, databaseId);\n\n    if (!database) {\n      this.logger.warn(`Database not found: ${databaseId}`);\n      return null;\n    }\n\n    // Use Entra ID authentication (Microsoft's recommended approach)\n    // Access Keys support will be added in a future update with proper UX\n    return this.getEntraIdConnectionDetails(accountId, database);\n  }\n\n  private async findDatabaseById(\n    accountId: string,\n    resourceId: string,\n  ): Promise<AzureRedisDatabase | null> {\n    if (!resourceId) {\n      return null;\n    }\n\n    // Extract subscription ID from resource ID\n    // Format: /subscriptions/{subscriptionId}/resourceGroups/...\n    const subscriptionMatch = resourceId.match(/^\\/subscriptions\\/([^/]+)\\//i);\n\n    if (!subscriptionMatch) {\n      this.logger.warn(`Invalid resource ID format: ${resourceId}`);\n      return null;\n    }\n\n    const subscriptionId = subscriptionMatch[1];\n    const databases = await this.listDatabasesInSubscription(\n      accountId,\n      subscriptionId,\n    );\n\n    // Azure resource IDs are case-insensitive\n    const resourceIdLower = resourceId.toLowerCase();\n    return (\n      databases.find((db) => db.id.toLowerCase() === resourceIdLower) || null\n    );\n  }\n\n  private async fetchStandardRedis(\n    client: AxiosInstance,\n    subscriptionId: string,\n  ): Promise<AzureRedisDatabase[]> {\n    const redisInstances = await this.fetchAllPages<any>(\n      client,\n      AzureApiUrls.getStandardRedisInSubscription(subscriptionId),\n    );\n\n    return redisInstances.map((redis: any) =>\n      this.mapStandardRedis(redis, subscriptionId),\n    );\n  }\n\n  private async fetchEnterpriseRedis(\n    client: AxiosInstance,\n    subscriptionId: string,\n  ): Promise<AzureRedisDatabase[]> {\n    const clusters = await this.fetchAllPages<any>(\n      client,\n      AzureApiUrls.getEnterpriseRedisInSubscription(subscriptionId),\n    );\n\n    if (clusters.length === 0) {\n      return [];\n    }\n\n    const { results } = await PromisePool.for(clusters)\n      .withConcurrency(AUTODISCOVERY_MAX_CONCURRENT_REQUESTS)\n      .handleError((error, cluster: any) => {\n        this.logger.warn(\n          `Failed to fetch databases for cluster ${cluster?.name}`,\n          error?.message,\n        );\n      })\n      .process((cluster: any) =>\n        this.listEnterpriseDatabases(client, cluster, subscriptionId),\n      );\n\n    return results.flat();\n  }\n\n  private mapStandardRedis(\n    redis: any,\n    subscriptionId: string,\n  ): AzureRedisDatabase {\n    const resourceGroup = this.extractResourceGroup(redis.id);\n\n    return {\n      id: redis.id,\n      name: redis.name,\n      subscriptionId,\n      resourceGroup,\n      location: redis.location,\n      type: AzureRedisType.Standard,\n      host:\n        redis.properties?.hostName || `${redis.name}.redis.cache.windows.net`,\n      port: redis.properties?.port || 6379,\n      sslPort: redis.properties?.sslPort || 6380,\n      provisioningState: redis.properties?.provisioningState,\n      sku: redis.properties?.sku,\n    };\n  }\n\n  private async listEnterpriseDatabases(\n    client: AxiosInstance,\n    cluster: any,\n    subscriptionId: string,\n  ): Promise<AzureRedisDatabase[]> {\n    const resourceGroup = this.extractResourceGroup(cluster.id);\n\n    try {\n      const dbs = await this.fetchAllPages<any>(\n        client,\n        AzureApiUrls.getEnterpriseDatabases(\n          subscriptionId,\n          resourceGroup,\n          cluster.name,\n        ),\n      );\n\n      return dbs.map((db: any) => {\n        const normalizedLocation = cluster.location\n          .toLowerCase()\n          .replace(/\\s+/g, '');\n\n        const host =\n          cluster.hostName ||\n          cluster.properties?.hostName ||\n          (db.properties?.clusteringPolicy === 'EnterpriseCluster'\n            ? `${cluster.name}.${normalizedLocation}.redisenterprise.cache.azure.net`\n            : `${cluster.name}-${db.name}.${normalizedLocation}.redisenterprise.cache.azure.net`);\n\n        return {\n          id: db.id,\n          name: `${cluster.name}/${db.name}`,\n          subscriptionId,\n          resourceGroup,\n          location: cluster.location,\n          type: AzureRedisType.Enterprise,\n          host,\n          port: db.properties?.port || 10000,\n          provisioningState: db.properties?.provisioningState,\n          sku: cluster.sku,\n          accessKeysAuthentication: db.properties?.accessKeysAuthentication,\n        };\n      });\n    } catch (error: any) {\n      this.logger.warn(\n        `Failed to list databases in cluster ${cluster.name}`,\n        error?.message,\n      );\n      return [];\n    }\n  }\n\n  private extractResourceGroup(resourceId: string): string {\n    const match = resourceId.match(/resourceGroups\\/([^/]+)/i);\n    return match ? match[1] : '';\n  }\n\n  private async getEntraIdConnectionDetails(\n    accountId: string,\n    database: AzureRedisDatabase,\n  ): Promise<AzureConnectionDetails | null> {\n    const tokenResult =\n      await this.authService.getRedisTokenByAccountId(accountId);\n\n    if (!tokenResult) {\n      this.logger.debug(\n        `No Redis token available for Entra ID auth on ${database.name}`,\n      );\n      return null;\n    }\n\n    const port = this.getTlsPort(database);\n    this.logger.debug(\n      `Using Entra ID auth for ${database.name} (type=${database.type}, port=${port})`,\n    );\n\n    return {\n      host: database.host,\n      port,\n      username: tokenResult.account.localAccountId,\n      tls: true,\n      authType: AzureAuthType.EntraId,\n      azureAccountId: accountId,\n      subscriptionId: database.subscriptionId,\n      resourceGroup: database.resourceGroup,\n      resourceId: database.id,\n    };\n  }\n\n  private getTlsPort(database: AzureRedisDatabase): number {\n    // Standard Redis uses sslPort (6380) for TLS\n    // Enterprise Redis uses port 10000 for BOTH TLS and non-TLS connections\n    // See: https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-tls-configuration\n    if (database.type === AzureRedisType.Standard) {\n      return database.sslPort || 6380;\n    }\n    return database.port || 10000;\n  }\n\n  /**\n   * Add Azure databases from autodiscovery\n   * Fetches connection details and creates databases\n   */\n  async addDatabases(\n    sessionMetadata: SessionMetadata,\n    accountId: string,\n    databases: ImportAzureDatabaseDto[],\n  ): Promise<ImportAzureDatabaseResponse[]> {\n    this.logger.debug(\n      `Adding ${databases.length} Azure database(s) for account ${accountId}`,\n    );\n\n    return Promise.all(\n      databases.map(async (dto): Promise<ImportAzureDatabaseResponse> => {\n        let database: AzureRedisDatabase | null = null;\n\n        try {\n          this.logger.debug(`[${dto.id}] Fetching database details...`);\n\n          database = await this.findDatabaseById(accountId, dto.id);\n\n          if (!database) {\n            this.logger.debug(`[${dto.id}] Database not found`);\n            this.analytics.sendAzureDatabaseAddFailed(\n              sessionMetadata,\n              new BadRequestException(ERROR_MESSAGES.AZURE_DATABASE_NOT_FOUND),\n            );\n            return {\n              id: dto.id,\n              status: ActionStatus.Fail,\n              message: ERROR_MESSAGES.AZURE_DATABASE_NOT_FOUND,\n            };\n          }\n\n          const connectionDetails = await this.getEntraIdConnectionDetails(\n            accountId,\n            database,\n          );\n\n          if (!connectionDetails) {\n            this.logger.debug(\n              `[${dto.id}] Failed to get connection details - no details returned`,\n            );\n            this.analytics.sendAzureDatabaseAddFailed(\n              sessionMetadata,\n              new BadRequestException(\n                ERROR_MESSAGES.AZURE_FAILED_TO_GET_CONNECTION_DETAILS,\n              ),\n              database.type,\n            );\n            return {\n              id: dto.id,\n              status: ActionStatus.Fail,\n              message: ERROR_MESSAGES.AZURE_FAILED_TO_GET_CONNECTION_DETAILS,\n            };\n          }\n\n          this.logger.debug(\n            `[${dto.id}] Connection details: host=${connectionDetails.host}, port=${connectionDetails.port}, tls=${connectionDetails.tls}`,\n          );\n\n          const providerDetails = {\n            provider: CloudProvider.Azure,\n            authType: connectionDetails.authType,\n            azureAccountId: connectionDetails.azureAccountId,\n          };\n\n          const provider =\n            database.type === AzureRedisType.Enterprise\n              ? HostingProvider.AZURE_CACHE_REDIS_ENTERPRISE\n              : HostingProvider.AZURE_CACHE;\n\n          this.logger.debug(\n            `[${dto.id}] Creating database: name=${database.name}, type=${database.type}, provider=${provider}`,\n          );\n\n          await this.databaseService.create(sessionMetadata, {\n            host: connectionDetails.host,\n            port: connectionDetails.port,\n            name: database.name,\n            nameFromProvider: database.name,\n            username: connectionDetails.username,\n            password: connectionDetails.password,\n            tls: connectionDetails.tls,\n            provider,\n            providerDetails,\n          });\n\n          this.logger.debug(`[${dto.id}] Successfully added database`);\n          this.analytics.sendAzureDatabaseAdded(sessionMetadata, database.type);\n\n          return {\n            id: dto.id,\n            status: ActionStatus.Success,\n          };\n        } catch (error) {\n          this.logger.error(\n            `[${dto.id}] Failed to add database: ${error.message}`,\n          );\n          this.analytics.sendAzureDatabaseAddFailed(\n            sessionMetadata,\n            new BadRequestException(this.getUserFriendlyErrorMessage(error)),\n            database?.type,\n          );\n          return {\n            id: dto.id,\n            status: ActionStatus.Fail,\n            message: this.getUserFriendlyErrorMessage(error),\n          };\n        }\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/dto/import-azure-database.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined, IsNotEmpty, IsString } from 'class-validator';\n\nexport class ImportAzureDatabaseDto {\n  @ApiProperty({\n    description: 'Azure resource ID of the database',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString()\n  id: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/dto/import-azure-database.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { ActionStatus } from 'src/common/models';\n\nexport class ImportAzureDatabaseResponse {\n  @ApiProperty({\n    description: 'Azure resource ID',\n    type: String,\n  })\n  id: string;\n\n  @ApiProperty({\n    description: 'Import Azure database status',\n    default: ActionStatus.Success,\n    enum: ActionStatus,\n  })\n  status: ActionStatus;\n\n  @ApiPropertyOptional({\n    description: 'Error message (only present when status is Fail)',\n    type: String,\n  })\n  message?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/dto/import-azure-databases.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  IsNotEmpty,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { ImportAzureDatabaseDto } from './import-azure-database.dto';\n\nexport class ImportAzureDatabasesDto {\n  @ApiProperty({\n    description: 'Azure account ID (homeAccountId)',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString()\n  accountId: string;\n\n  @ApiProperty({\n    description: 'Azure databases list',\n    type: ImportAzureDatabaseDto,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested({ each: true })\n  @Type(() => ImportAzureDatabaseDto)\n  databases: ImportAzureDatabaseDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/autodiscovery/dto/index.ts",
    "content": "export * from './import-azure-database.dto';\nexport * from './import-azure-databases.dto';\nexport * from './import-azure-database.response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/azure-token-refresh.manager.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { faker } from '@faker-js/faker';\nimport { AzureTokenRefreshManager } from './azure-token-refresh.manager';\nimport { AzureAuthService } from './auth/azure-auth.service';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { MIN_REFRESH_DELAY_MS, TOKEN_REFRESH_BUFFER_MS } from './constants';\n\nconst createMockAccount = () => ({\n  homeAccountId: faker.string.uuid(),\n  environment: 'login.microsoftonline.com',\n  tenantId: faker.string.uuid(),\n  username: faker.internet.email(),\n  localAccountId: faker.string.uuid(),\n  name: faker.person.fullName(),\n});\n\nconst createMockTokenResult = () => {\n  const expiresOn = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now\n  return {\n    token: faker.string.alphanumeric(100),\n    expiresOn,\n    account: createMockAccount(),\n  };\n};\n\nconst createMockClient = (tokenExpiresOn?: Date) => ({\n  id: faker.string.uuid(),\n  call: jest.fn().mockResolvedValue('OK'),\n  database: {\n    providerDetails: {\n      azureAccountId: faker.string.uuid(),\n      tokenExpiresOn,\n    },\n  },\n});\n\ndescribe('AzureTokenRefreshManager', () => {\n  let manager: AzureTokenRefreshManager;\n  let mockAzureAuthService: { getRedisTokenByAccountId: jest.Mock };\n  let mockRedisClientStorage: { getClientsByDatabaseField: jest.Mock };\n\n  beforeEach(async () => {\n    jest.useFakeTimers();\n    jest.clearAllMocks();\n\n    mockAzureAuthService = {\n      getRedisTokenByAccountId: jest.fn(),\n    };\n\n    mockRedisClientStorage = {\n      getClientsByDatabaseField: jest.fn().mockReturnValue([]),\n    };\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AzureTokenRefreshManager,\n        {\n          provide: AzureAuthService,\n          useValue: mockAzureAuthService,\n        },\n        {\n          provide: RedisClientStorage,\n          useValue: mockRedisClientStorage,\n        },\n      ],\n    }).compile();\n\n    manager = module.get<AzureTokenRefreshManager>(AzureTokenRefreshManager);\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  describe('scheduleRefresh', () => {\n    it('should schedule a timer based on token expiry minus buffer', () => {\n      const azureAccountId = faker.string.uuid();\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000); // 1 hour\n\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      expect(jest.getTimerCount()).toBe(1);\n    });\n\n    it('should clear existing timer when scheduling for same account with different expiry', () => {\n      const azureAccountId = faker.string.uuid();\n      const expiresOn1 = new Date(Date.now() + 60 * 60 * 1000);\n      const expiresOn2 = new Date(Date.now() + 2 * 60 * 60 * 1000);\n\n      manager.scheduleRefresh(azureAccountId, expiresOn1);\n      manager.scheduleRefresh(azureAccountId, expiresOn2);\n\n      expect(jest.getTimerCount()).toBe(1);\n    });\n\n    it('should skip scheduling when timer already exists for same expiry time', () => {\n      const azureAccountId = faker.string.uuid();\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000);\n\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      // Should still be just 1 timer (not cleared and rescheduled)\n      expect(jest.getTimerCount()).toBe(1);\n    });\n\n    it('should allow multiple timers for different accounts', () => {\n      const accountId1 = faker.string.uuid();\n      const accountId2 = faker.string.uuid();\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000);\n\n      manager.scheduleRefresh(accountId1, expiresOn);\n      manager.scheduleRefresh(accountId2, expiresOn);\n\n      expect(jest.getTimerCount()).toBe(2);\n    });\n\n    describe('race condition handling', () => {\n      it('should handle rapid successive calls with same expiry (duplicate events)', () => {\n        const azureAccountId = faker.string.uuid();\n        const expiresOn = new Date(Date.now() + 60 * 60 * 1000);\n\n        // Simulate multiple token events arriving rapidly (e.g., from concurrent requests)\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n\n        // Should only have 1 timer, not 5\n        expect(jest.getTimerCount()).toBe(1);\n      });\n\n      it('should handle client reconnection while timer is pending', async () => {\n        const azureAccountId = faker.string.uuid();\n        const mockClient = createMockClient();\n        const initialExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour\n        const newExpiry = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours\n        const newTokenResult = {\n          ...createMockTokenResult(),\n          expiresOn: newExpiry,\n        };\n\n        mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n          mockClient,\n        ]);\n\n        // Initial timer scheduled\n        manager.scheduleRefresh(azureAccountId, initialExpiry);\n        expect(jest.getTimerCount()).toBe(1);\n\n        // Client reconnects 10 minutes later, gets new token with different expiry\n        await jest.advanceTimersByTimeAsync(10 * 60 * 1000);\n\n        // New token event arrives - should replace old timer\n        await manager.handleTokenAcquired({\n          accountId: azureAccountId,\n          tokenResult: newTokenResult,\n        });\n\n        // Should still only have 1 timer (old one cleared, new one set)\n        expect(jest.getTimerCount()).toBe(1);\n\n        // Verify old timer doesn't fire (it was cleared)\n        mockAzureAuthService.getRedisTokenByAccountId.mockClear();\n        await jest.advanceTimersByTimeAsync(50 * 60 * 1000); // Original would have fired\n\n        // Should not have called refresh yet (new timer has longer delay)\n        expect(\n          mockAzureAuthService.getRedisTokenByAccountId,\n        ).not.toHaveBeenCalled();\n      });\n\n      it('should atomically replace timer entry (no deletion window)', () => {\n        const azureAccountId = faker.string.uuid();\n        const expiresOn1 = new Date(Date.now() + 60 * 60 * 1000);\n        const expiresOn2 = new Date(Date.now() + 2 * 60 * 60 * 1000);\n\n        manager.scheduleRefresh(azureAccountId, expiresOn1);\n\n        // Access internal timers map to verify behavior\n        const timersMap = (\n          manager as unknown as { timers: Map<string, unknown> }\n        ).timers;\n        expect(timersMap.has(azureAccountId)).toBe(true);\n\n        // Schedule with new expiry - should overwrite, not delete then set\n        manager.scheduleRefresh(azureAccountId, expiresOn2);\n\n        // Entry should still exist (was overwritten atomically)\n        expect(timersMap.has(azureAccountId)).toBe(true);\n        expect(jest.getTimerCount()).toBe(1);\n      });\n    });\n\n    describe('minimum delay enforcement', () => {\n      it('should use minimum delay when token expires within buffer', () => {\n        const azureAccountId = faker.string.uuid();\n        // Token expires in 2 minutes (within 5-minute buffer)\n        const expiresOn = new Date(Date.now() + 2 * 60 * 1000);\n\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n\n        expect(jest.getTimerCount()).toBe(1);\n        // Timer should not fire immediately - minimum delay enforced\n        jest.advanceTimersByTime(MIN_REFRESH_DELAY_MS - 1);\n        expect(jest.getTimerCount()).toBe(1); // Still pending\n      });\n\n      it('should use minimum delay when token is already expired', () => {\n        const azureAccountId = faker.string.uuid();\n        // Token already expired 1 minute ago\n        const expiresOn = new Date(Date.now() - 60 * 1000);\n\n        manager.scheduleRefresh(azureAccountId, expiresOn);\n\n        expect(jest.getTimerCount()).toBe(1);\n        // Timer should not fire immediately - minimum delay enforced\n        jest.advanceTimersByTime(MIN_REFRESH_DELAY_MS - 1);\n        expect(jest.getTimerCount()).toBe(1); // Still pending\n      });\n\n      it('should prevent rapid refresh loop with near-expiry tokens', async () => {\n        const azureAccountId = faker.string.uuid();\n        const mockClient = createMockClient();\n\n        // Token that expires in 1 minute (within buffer)\n        const nearExpiryToken = {\n          token: faker.string.alphanumeric(100),\n          expiresOn: new Date(Date.now() + 60 * 1000),\n          account: createMockAccount(),\n        };\n\n        let refreshCallCount = 0;\n        mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n          async () => {\n            refreshCallCount++;\n            // Simulate event being emitted with same near-expiry token\n            await manager.handleTokenAcquired({\n              accountId: azureAccountId,\n              tokenResult: nearExpiryToken,\n            });\n            return nearExpiryToken;\n          },\n        );\n        mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n          mockClient,\n        ]);\n\n        // Initial schedule\n        manager.scheduleRefresh(azureAccountId, nearExpiryToken.expiresOn);\n\n        // Advance past minimum delay to trigger refresh\n        await jest.advanceTimersByTimeAsync(MIN_REFRESH_DELAY_MS);\n\n        // Should have called refresh once, not in a rapid loop\n        expect(refreshCallCount).toBe(1);\n        // A new timer should be scheduled (not firing immediately)\n        expect(jest.getTimerCount()).toBe(1);\n      });\n    });\n  });\n\n  describe('handleTokenAcquired', () => {\n    it('should schedule refresh when token acquired event is received', async () => {\n      const accountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([]);\n\n      await manager.handleTokenAcquired({ accountId, tokenResult });\n\n      expect(jest.getTimerCount()).toBe(1);\n    });\n\n    it('should immediately re-authenticate clients with different token', async () => {\n      const accountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const clientWithOldToken = createMockClient(\n        new Date(Date.now() - 60 * 60 * 1000),\n      );\n\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        clientWithOldToken,\n      ]);\n\n      await manager.handleTokenAcquired({ accountId, tokenResult });\n\n      expect(clientWithOldToken.call).toHaveBeenCalledWith([\n        'AUTH',\n        tokenResult.account.localAccountId,\n        tokenResult.token,\n      ]);\n    });\n\n    it('should skip immediate re-auth for clients with current token', async () => {\n      const accountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const clientWithCurrentToken = createMockClient(tokenResult.expiresOn);\n\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        clientWithCurrentToken,\n      ]);\n\n      await manager.handleTokenAcquired({ accountId, tokenResult });\n\n      expect(clientWithCurrentToken.call).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('clearTimer', () => {\n    it('should clear timer for specific account', () => {\n      const azureAccountId = faker.string.uuid();\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000);\n\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n      expect(jest.getTimerCount()).toBe(1);\n\n      manager.clearTimer(azureAccountId);\n      expect(jest.getTimerCount()).toBe(0);\n    });\n\n    it('should not throw when clearing non-existent timer', () => {\n      expect(() => manager.clearTimer(faker.string.uuid())).not.toThrow();\n    });\n  });\n\n  describe('clearAllTimers', () => {\n    it('should clear all timers', () => {\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000);\n\n      manager.scheduleRefresh(faker.string.uuid(), expiresOn);\n      manager.scheduleRefresh(faker.string.uuid(), expiresOn);\n      manager.scheduleRefresh(faker.string.uuid(), expiresOn);\n      expect(jest.getTimerCount()).toBe(3);\n\n      manager.clearAllTimers();\n      expect(jest.getTimerCount()).toBe(0);\n    });\n  });\n\n  describe('onModuleDestroy', () => {\n    it('should clear all timers on module destroy', () => {\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000);\n\n      manager.scheduleRefresh(faker.string.uuid(), expiresOn);\n      manager.scheduleRefresh(faker.string.uuid(), expiresOn);\n      expect(jest.getTimerCount()).toBe(2);\n\n      manager.onModuleDestroy();\n      expect(jest.getTimerCount()).toBe(0);\n    });\n  });\n\n  // Use a delay that's greater than MIN_REFRESH_DELAY_MS to avoid minimum delay enforcement\n  const TEST_DELAY_MS = MIN_REFRESH_DELAY_MS + 1000;\n\n  describe('refreshToken (timer callback)', () => {\n    it('should refresh token when timer fires (re-auth happens via event)', async () => {\n      const azureAccountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const mockClient = createMockClient();\n\n      // Simulate event flow: when getRedisTokenByAccountId succeeds,\n      // AzureAuthService emits event -> handleTokenAcquired is called\n      mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n        async () => {\n          // Simulate event triggering handleTokenAcquired\n          await manager.handleTokenAcquired({\n            accountId: azureAccountId,\n            tokenResult,\n          });\n          return tokenResult;\n        },\n      );\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        mockClient,\n      ]);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      expect(\n        mockAzureAuthService.getRedisTokenByAccountId,\n      ).toHaveBeenCalledWith(azureAccountId);\n      expect(\n        mockRedisClientStorage.getClientsByDatabaseField,\n      ).toHaveBeenCalledWith('providerDetails.azureAccountId', azureAccountId);\n      expect(mockClient.call).toHaveBeenCalledWith([\n        'AUTH',\n        tokenResult.account.localAccountId,\n        tokenResult.token,\n      ]);\n    });\n\n    it('should not re-authenticate when token refresh fails', async () => {\n      const azureAccountId = faker.string.uuid();\n      const mockClient = createMockClient();\n\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        mockClient,\n      ]);\n      mockAzureAuthService.getRedisTokenByAccountId.mockResolvedValue(null);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      expect(\n        mockRedisClientStorage.getClientsByDatabaseField,\n      ).toHaveBeenCalled();\n      expect(mockAzureAuthService.getRedisTokenByAccountId).toHaveBeenCalled();\n      // No re-auth should happen, no retry timer scheduled\n      expect(mockClient.call).not.toHaveBeenCalled();\n      expect(jest.getTimerCount()).toBe(0);\n    });\n\n    it('should stop refresh cycle when no clients are using the account', async () => {\n      const azureAccountId = faker.string.uuid();\n\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([]);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      // Should check for clients first\n      expect(\n        mockRedisClientStorage.getClientsByDatabaseField,\n      ).toHaveBeenCalled();\n      // Should NOT call getRedisTokenByAccountId since no clients exist\n      expect(\n        mockAzureAuthService.getRedisTokenByAccountId,\n      ).not.toHaveBeenCalled();\n      // Should NOT schedule another timer\n      expect(jest.getTimerCount()).toBe(0);\n    });\n\n    it('should continue re-authenticating other clients if one fails', async () => {\n      const azureAccountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const mockClient1 = createMockClient();\n      const mockClient2 = createMockClient();\n\n      mockClient1.call.mockRejectedValue(new Error('Auth failed'));\n\n      mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n        async () => {\n          await manager.handleTokenAcquired({\n            accountId: azureAccountId,\n            tokenResult,\n          });\n          return tokenResult;\n        },\n      );\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        mockClient1,\n        mockClient2,\n      ]);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      expect(mockClient1.call).toHaveBeenCalled();\n      expect(mockClient2.call).toHaveBeenCalled();\n    });\n\n    it('should skip re-auth for clients that already have current token', async () => {\n      const azureAccountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const clientWithCurrentToken = createMockClient(tokenResult.expiresOn);\n      const clientWithOldToken = createMockClient(\n        new Date(Date.now() - 60 * 60 * 1000),\n      );\n\n      mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n        async () => {\n          await manager.handleTokenAcquired({\n            accountId: azureAccountId,\n            tokenResult,\n          });\n          return tokenResult;\n        },\n      );\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        clientWithCurrentToken,\n        clientWithOldToken,\n      ]);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      // Client with current token should NOT be re-authenticated\n      expect(clientWithCurrentToken.call).not.toHaveBeenCalled();\n      // Client with old token should be re-authenticated\n      expect(clientWithOldToken.call).toHaveBeenCalledWith([\n        'AUTH',\n        tokenResult.account.localAccountId,\n        tokenResult.token,\n      ]);\n    });\n\n    it('should update tokenExpiresOn after successful re-auth', async () => {\n      const azureAccountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const mockClient = createMockClient();\n\n      mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n        async () => {\n          await manager.handleTokenAcquired({\n            accountId: azureAccountId,\n            tokenResult,\n          });\n          return tokenResult;\n        },\n      );\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        mockClient,\n      ]);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      expect(mockClient.database.providerDetails.tokenExpiresOn).toBe(\n        tokenResult.expiresOn,\n      );\n    });\n\n    it('should skip all re-auth when all clients have current token', async () => {\n      const azureAccountId = faker.string.uuid();\n      const tokenResult = createMockTokenResult();\n      const client1 = createMockClient(tokenResult.expiresOn);\n      const client2 = createMockClient(tokenResult.expiresOn);\n\n      mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n        async () => {\n          await manager.handleTokenAcquired({\n            accountId: azureAccountId,\n            tokenResult,\n          });\n          return tokenResult;\n        },\n      );\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        client1,\n        client2,\n      ]);\n\n      const expiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, expiresOn);\n\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      expect(client1.call).not.toHaveBeenCalled();\n      expect(client2.call).not.toHaveBeenCalled();\n    });\n\n    it('should reschedule timer when MSAL returns cached token with same expiresOn', async () => {\n      const azureAccountId = faker.string.uuid();\n      const mockClient = createMockClient();\n\n      // Token with same expiry throughout (simulates MSAL returning cached token)\n      const expiresOn = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now\n      const tokenResult = {\n        token: faker.string.alphanumeric(100),\n        expiresOn,\n        account: createMockAccount(),\n      };\n\n      // Simulate MSAL returning cached token (same expiresOn after timer fires)\n      mockAzureAuthService.getRedisTokenByAccountId.mockImplementation(\n        async () => {\n          await manager.handleTokenAcquired({\n            accountId: azureAccountId,\n            tokenResult, // Same expiresOn as the original schedule\n          });\n          return tokenResult;\n        },\n      );\n      mockRedisClientStorage.getClientsByDatabaseField.mockReturnValue([\n        mockClient,\n      ]);\n\n      // Schedule refresh with expiresOn that triggers after buffer (> MIN_REFRESH_DELAY_MS)\n      const scheduleExpiresOn = new Date(\n        Date.now() + TOKEN_REFRESH_BUFFER_MS + TEST_DELAY_MS,\n      );\n      manager.scheduleRefresh(azureAccountId, scheduleExpiresOn);\n      expect(jest.getTimerCount()).toBe(1);\n\n      // Fire the timer\n      await jest.advanceTimersByTimeAsync(TEST_DELAY_MS);\n\n      // Key assertion: even though MSAL returned same expiresOn, a new timer should be scheduled\n      // This verifies the fix for \"stale timer entry prevents refresh cycle rescheduling\"\n      expect(jest.getTimerCount()).toBe(1);\n      expect(\n        mockAzureAuthService.getRedisTokenByAccountId,\n      ).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/azure-token-refresh.manager.ts",
    "content": "import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { AzureAuthService } from './auth/azure-auth.service';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport {\n  AzureRedisTokenEvents,\n  MIN_REFRESH_DELAY_MS,\n  TOKEN_REFRESH_BUFFER_MS,\n} from './constants';\nimport { AzureTokenResult } from './auth/models';\n\n/**\n * Manages automatic token refresh for Azure Entra ID authenticated Redis clients.\n *\n * When a token is acquired, the AzureRedisTokenEvents.Acquired event triggers:\n * 1. Schedule a timer to refresh before expiry\n * 2. Re-authenticate active Redis clients with the new token\n *\n * When the timer fires, it acquires a fresh token which emits the event again,\n * continuing the cycle. The cycle stops when no clients are using the account.\n */\ninterface ScheduledTimer {\n  timeout: NodeJS.Timeout;\n  expiresOn: Date;\n}\n\n@Injectable()\nexport class AzureTokenRefreshManager implements OnModuleDestroy {\n  private readonly logger = new Logger(AzureTokenRefreshManager.name);\n\n  private readonly timers: Map<string, ScheduledTimer> = new Map();\n\n  constructor(\n    private readonly azureAuthService: AzureAuthService,\n    private readonly redisClientStorage: RedisClientStorage,\n  ) {}\n\n  onModuleDestroy(): void {\n    this.clearAllTimers();\n  }\n\n  @OnEvent(AzureRedisTokenEvents.Acquired)\n  async handleTokenAcquired({\n    accountId,\n    tokenResult,\n  }: {\n    accountId: string;\n    tokenResult: AzureTokenResult;\n  }): Promise<void> {\n    try {\n      this.scheduleRefresh(accountId, tokenResult.expiresOn);\n      await this.reAuthenticateClients(accountId, tokenResult);\n    } catch (error) {\n      this.logger.error(\n        `Failed to handle token acquired event for account ${accountId}: ${error.message}`,\n      );\n    }\n  }\n\n  scheduleRefresh(azureAccountId: string, expiresOn: Date): void {\n    const existing = this.timers.get(azureAccountId);\n\n    // Skip if already scheduled for the same expiry time (race condition protection)\n    if (existing?.expiresOn?.getTime() === expiresOn.getTime()) {\n      return;\n    }\n\n    // Clear existing timeout but don't remove from map to avoid race conditions\n    // The map entry will be overwritten by timers.set() below\n    if (existing) {\n      clearTimeout(existing.timeout);\n    }\n\n    const now = Date.now();\n    const expiresAt = expiresOn.getTime();\n    const refreshAt = expiresAt - TOKEN_REFRESH_BUFFER_MS;\n    const calculatedDelay = refreshAt - now;\n\n    // Enforce minimum delay to prevent rapid refresh loops when token is near/past expiry.\n    // This can happen when MSAL returns a cached token with a short remaining lifetime.\n    const delay = Math.max(calculatedDelay, MIN_REFRESH_DELAY_MS);\n\n    if (calculatedDelay < MIN_REFRESH_DELAY_MS) {\n      this.logger.warn(\n        `Token for account ${azureAccountId} expires soon (${Math.round(calculatedDelay / 1000)}s), ` +\n          `using minimum delay of ${MIN_REFRESH_DELAY_MS / 1000}s`,\n      );\n    }\n\n    this.logger.debug(\n      `Scheduling token refresh for account ${azureAccountId} in ${Math.round(delay / 1000)}s (expires: ${expiresOn.toISOString()})`,\n    );\n\n    const timeout = setTimeout(() => {\n      this.refreshToken(azureAccountId).catch((error) => {\n        this.logger.error(\n          `Token refresh failed for account ${azureAccountId}: ${error.message}`,\n        );\n      });\n    }, delay);\n\n    this.timers.set(azureAccountId, { timeout, expiresOn });\n  }\n\n  clearTimer(azureAccountId: string): void {\n    const existing = this.timers.get(azureAccountId);\n    if (existing) {\n      clearTimeout(existing.timeout);\n      this.timers.delete(azureAccountId);\n    }\n  }\n\n  clearAllTimers(): void {\n    this.timers.forEach(({ timeout }) => clearTimeout(timeout));\n    this.timers.clear();\n  }\n\n  private async refreshToken(azureAccountId: string): Promise<void> {\n    this.logger.debug(`Refreshing token for account ${azureAccountId}`);\n\n    // Clear the stale timer entry - the timer has fired, so the entry is no longer valid.\n    // This ensures that when getRedisTokenByAccountId emits the Acquired event,\n    // scheduleRefresh won't skip due to matching expiresOn (e.g., MSAL cached token).\n    this.clearTimer(azureAccountId);\n\n    // Stop the refresh cycle if no clients are using this account\n    const clients = this.redisClientStorage.getClientsByDatabaseField(\n      'providerDetails.azureAccountId',\n      azureAccountId,\n    );\n\n    if (clients.length === 0) {\n      this.logger.debug(\n        `No active clients for account ${azureAccountId}, stopping refresh cycle`,\n      );\n      return;\n    }\n\n    await this.azureAuthService.getRedisTokenByAccountId(azureAccountId);\n  }\n\n  private async reAuthenticateClients(\n    azureAccountId: string,\n    tokenResult: AzureTokenResult,\n  ): Promise<void> {\n    const clients = this.redisClientStorage.getClientsByDatabaseField(\n      'providerDetails.azureAccountId',\n      azureAccountId,\n    );\n\n    if (clients.length === 0) {\n      return;\n    }\n\n    // Filter out clients that already have the current token\n    const clientsToReauth = clients.filter(\n      (client) =>\n        client.database.providerDetails?.tokenExpiresOn?.getTime() !==\n        tokenResult.expiresOn.getTime(),\n    );\n\n    if (clientsToReauth.length === 0) {\n      this.logger.debug(\n        `All clients for account ${azureAccountId} already have current token`,\n      );\n      return;\n    }\n\n    this.logger.debug(\n      `Re-authenticating ${clientsToReauth.length} of ${clients.length} client(s) for account ${azureAccountId}`,\n    );\n\n    await Promise.all(\n      clientsToReauth.map(async (client) => {\n        try {\n          await client.call([\n            'AUTH',\n            tokenResult.account.localAccountId,\n            tokenResult.token,\n          ]);\n          // Update the client's token expiry after successful re-auth\n          // eslint-disable-next-line no-param-reassign\n          client.database.providerDetails.tokenExpiresOn =\n            tokenResult.expiresOn;\n        } catch (error) {\n          this.logger.warn(\n            `Failed to re-authenticate client ${client.id}: ${error.message}`,\n          );\n        }\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/azure.module.ts",
    "content": "import { Global, Module } from '@nestjs/common';\nimport { AzureAuthService } from './auth/azure-auth.service';\nimport { AzureAuthController } from './auth/azure-auth.controller';\nimport { AzureAuthAnalytics } from './auth/azure-auth.analytics';\nimport { AzureAutodiscoveryService } from './autodiscovery/azure-autodiscovery.service';\nimport { AzureAutodiscoveryController } from './autodiscovery/azure-autodiscovery.controller';\nimport { AzureAutodiscoveryAnalytics } from './autodiscovery/azure-autodiscovery.analytics';\nimport { AzureTokenRefreshManager } from './azure-token-refresh.manager';\nimport { DatabaseModule } from '../database/database.module';\n\n@Global()\n@Module({\n  imports: [DatabaseModule],\n  providers: [\n    AzureAuthService,\n    AzureAuthAnalytics,\n    AzureAutodiscoveryService,\n    AzureAutodiscoveryAnalytics,\n    AzureTokenRefreshManager,\n  ],\n  controllers: [AzureAuthController, AzureAutodiscoveryController],\n  exports: [\n    AzureAuthService,\n    AzureAutodiscoveryService,\n    AzureTokenRefreshManager,\n  ],\n})\nexport class AzureModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/constants.ts",
    "content": "/**\n * LocalStorage key for Azure OAuth callback data.\n * Used for cross-window communication between popup and main window.\n * IMPORTANT: This key is also referenced in the frontend components.\n */\nexport const AZURE_OAUTH_STORAGE_KEY = 'ri_azure_oauth_result';\n\n/**\n * Azure AD authority URL for multi-tenant authentication.\n * Uses 'common' endpoint to allow any Azure AD tenant.\n */\nexport const AZURE_AUTHORITY = 'https://login.microsoftonline.com/common';\n\n/**\n * Azure App Registration Client ID.\n */\nexport const AZURE_CLIENT_ID = '61f3d82d-2bf3-432a-ba1b-c056e4cf0fd0';\n\n/**\n * Azure Redis scope for Entra ID authentication.\n * This scope is required to get access tokens for Azure Cache for Redis.\n */\nexport const AZURE_REDIS_SCOPE = 'https://redis.azure.com/.default';\n\n/**\n * Azure Management scope for Azure Resource Manager API.\n * Used for autodiscovery of Azure Redis resources.\n */\nexport const AZURE_MANAGEMENT_SCOPE = 'https://management.azure.com/.default';\n\n/**\n * Azure OAuth redirect type for the application.\n * - deeplink: Uses custom protocol (redisinsight://) for Electron app\n * - web: Uses HTTP localhost callback for web/Docker deployments\n */\nexport enum AzureOAuthRedirectType {\n  Deeplink = 'deeplink',\n  Web = 'web',\n}\n\n/**\n * Azure OAuth redirect path for Electron app (deeplink).\n */\nexport const AZURE_OAUTH_DEEPLINK_REDIRECT_PATH =\n  'redisinsight://azure/oauth/callback';\n\n/**\n * Azure OAuth redirect path for the application.\n * @deprecated Use AZURE_OAUTH_DEEPLINK_REDIRECT_PATH or getAzureOAuthRedirectUri() instead\n */\nexport const AZURE_OAUTH_REDIRECT_PATH = AZURE_OAUTH_DEEPLINK_REDIRECT_PATH;\n\n/**\n * Azure OAuth web callback endpoint path (relative to API base).\n */\nexport const AZURE_OAUTH_WEB_CALLBACK_ENDPOINT = '/azure/auth/callback';\n\n/**\n * Scopes requested during the initial OAuth login flow.\n *\n * IMPORTANT: Azure AD does not allow requesting scopes from multiple resources\n * (e.g., redis.azure.com AND management.azure.com) in a single authorization request.\n * This results in error AADSTS70011: \"static scope limit exceeded\".\n *\n * We request only the Redis scope during login. The Management scope can be\n * acquired later using acquireTokenSilent() when needed.\n *\n * @see https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc\n */\nexport const AZURE_OAUTH_SCOPES = [\n  AZURE_REDIS_SCOPE,\n  'offline_access', // Required for refresh tokens\n  'openid', // Required for ID token\n  'profile', // Required for user info (name, etc.)\n];\n\nexport enum AzureAuthStatus {\n  Processing = 'processing',\n  Succeed = 'succeed',\n  Failed = 'failed',\n}\n\nexport enum AzureRedisType {\n  Standard = 'standard',\n  Enterprise = 'enterprise',\n}\n\nexport enum AzureAuthType {\n  AccessKey = 'accessKey',\n  EntraId = 'entraId',\n}\n\nexport enum AzureAccessKeysStatus {\n  Enabled = 'Enabled',\n  Disabled = 'Disabled',\n}\n\n/**\n * Azure subscription states from Azure Resource Manager API.\n * Values match Azure API response casing (PascalCase).\n * @see https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list#subscriptionstate\n */\nexport enum AzureSubscriptionState {\n  Enabled = 'Enabled',\n}\n\n/**\n * Azure resource provisioning states.\n * Values match Azure API response casing (PascalCase).\n * @see https://learn.microsoft.com/en-us/rest/api/redis/redis/get#provisioningstate\n */\nexport enum AzureProvisioningState {\n  Succeeded = 'Succeeded',\n}\n\nexport const AZURE_API_BASE = 'https://management.azure.com';\n\nexport enum AzureRedisTokenEvents {\n  Acquired = 'azure.redis.token.acquired',\n}\n\nexport const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\n// Minimum delay between refresh attempts to prevent rapid loops when token is near/past expiry\nexport const MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n// API versions - latest stable as of January 2025\n\n// https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list\nexport const API_VERSION_SUBSCRIPTIONS = '2022-12-01';\n// https://learn.microsoft.com/en-us/rest/api/redis/redis\nexport const API_VERSION_REDIS = '2024-11-01';\n// https://learn.microsoft.com/en-us/rest/api/redis/redisenterprisecache/redis-enterprise\nexport const API_VERSION_REDIS_ENTERPRISE = '2025-07-01';\n\nexport const AUTODISCOVERY_MAX_CONCURRENT_REQUESTS = 20;\n\n// Azure subscription IDs are standard UUIDs (8-4-4-4-12 hex pattern)\nexport const AZURE_SUBSCRIPTION_ID_REGEX =\n  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\nexport const AzureApiUrls = {\n  getSubscriptions: () =>\n    `/subscriptions?api-version=${API_VERSION_SUBSCRIPTIONS}`,\n\n  getStandardRedisInSubscription: (subscriptionId: string) =>\n    `/subscriptions/${subscriptionId}/providers/Microsoft.Cache/redis?api-version=${API_VERSION_REDIS}`,\n\n  getEnterpriseRedisInSubscription: (subscriptionId: string) =>\n    `/subscriptions/${subscriptionId}/providers/Microsoft.Cache/redisEnterprise?api-version=${API_VERSION_REDIS_ENTERPRISE}`,\n\n  getEnterpriseDatabases: (\n    subscriptionId: string,\n    resourceGroup: string,\n    clusterName: string,\n  ) =>\n    `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Cache/redisEnterprise/${clusterName}/databases?api-version=${API_VERSION_REDIS_ENTERPRISE}`,\n\n  postStandardRedisKeys: (\n    subscriptionId: string,\n    resourceGroup: string,\n    name: string,\n  ) =>\n    `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Cache/redis/${name}/listKeys?api-version=${API_VERSION_REDIS}`,\n\n  postEnterpriseRedisKeys: (\n    subscriptionId: string,\n    resourceGroup: string,\n    clusterName: string,\n    databaseName: string = 'default',\n  ) =>\n    `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Cache/redisEnterprise/${clusterName}/databases/${databaseName}/listKeys?api-version=${API_VERSION_REDIS_ENTERPRISE}`,\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/exceptions/azure-entra-id-token-expired.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class AzureEntraIdTokenExpiredException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.AZURE_ENTRA_ID_TOKEN_EXPIRED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'AzureEntraIdTokenExpired',\n      errorCode: CustomErrorCodes.AzureEntraIdTokenExpired,\n      additionalInfo: {\n        errorCode: CustomErrorCodes.AzureEntraIdTokenExpired,\n      },\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/exceptions/index.ts",
    "content": "export * from './azure-entra-id-token-expired.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/models/azure-resource.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  AzureAccessKeysStatus,\n  AzureAuthType,\n  AzureRedisType,\n} from '../constants';\n\nexport class AzureSubscription {\n  @ApiProperty({\n    description: 'Azure subscription ID',\n    type: String,\n  })\n  subscriptionId: string;\n\n  @ApiProperty({\n    description: 'Subscription display name',\n    type: String,\n  })\n  displayName: string;\n\n  @ApiProperty({\n    description: 'Subscription state',\n    type: String,\n  })\n  state: string;\n}\n\nclass AzureRedisSku {\n  @ApiProperty({\n    description: 'SKU name',\n    type: String,\n  })\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'SKU family',\n    type: String,\n  })\n  family?: string;\n\n  @ApiPropertyOptional({\n    description: 'SKU capacity',\n    type: Number,\n  })\n  capacity?: number;\n}\n\nexport class AzureRedisDatabase {\n  @ApiProperty({\n    description: 'Azure resource ID',\n    type: String,\n  })\n  id: string;\n\n  @ApiProperty({\n    description: 'Database name',\n    type: String,\n  })\n  name: string;\n\n  @ApiProperty({\n    description: 'Azure subscription ID',\n    type: String,\n  })\n  subscriptionId: string;\n\n  @ApiProperty({\n    description: 'Azure resource group',\n    type: String,\n  })\n  resourceGroup: string;\n\n  @ApiProperty({\n    description: 'Azure region location',\n    type: String,\n  })\n  location: string;\n\n  @ApiProperty({\n    description: 'Redis type (standard or enterprise)',\n    enum: AzureRedisType,\n  })\n  type: AzureRedisType;\n\n  @ApiProperty({\n    description: 'Redis host',\n    type: String,\n  })\n  host: string;\n\n  @ApiProperty({\n    description: 'Redis port',\n    type: Number,\n  })\n  port: number;\n\n  @ApiPropertyOptional({\n    description: 'Redis SSL port',\n    type: Number,\n  })\n  sslPort?: number;\n\n  @ApiProperty({\n    description: 'Provisioning state',\n    type: String,\n  })\n  provisioningState: string;\n\n  @ApiPropertyOptional({\n    description: 'SKU information',\n    type: AzureRedisSku,\n  })\n  sku?: AzureRedisSku;\n\n  @ApiPropertyOptional({\n    description: 'Access keys authentication status',\n    enum: AzureAccessKeysStatus,\n  })\n  accessKeysAuthentication?: AzureAccessKeysStatus;\n}\n\nexport class AzureConnectionDetails {\n  @ApiProperty({\n    description: 'Redis host',\n    type: String,\n  })\n  host: string;\n\n  @ApiProperty({\n    description: 'Redis port',\n    type: Number,\n  })\n  port: number;\n\n  @ApiPropertyOptional({\n    description: 'Redis password',\n    type: String,\n  })\n  password?: string;\n\n  @ApiPropertyOptional({\n    description: 'Redis username',\n    type: String,\n  })\n  username?: string;\n\n  @ApiProperty({\n    description: 'Whether TLS is enabled',\n    type: Boolean,\n  })\n  tls: boolean;\n\n  @ApiProperty({\n    description: 'Authentication type',\n    enum: AzureAuthType,\n  })\n  authType: AzureAuthType;\n\n  @ApiPropertyOptional({\n    description: 'Azure account ID',\n    type: String,\n  })\n  azureAccountId?: string;\n\n  @ApiProperty({\n    description: 'Azure subscription ID',\n    type: String,\n  })\n  subscriptionId: string;\n\n  @ApiProperty({\n    description: 'Azure resource group',\n    type: String,\n  })\n  resourceGroup: string;\n\n  @ApiProperty({\n    description: 'Azure resource ID',\n    type: String,\n  })\n  resourceId: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/azure/models/index.ts",
    "content": "export * from './azure-resource';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/hash.ts",
    "content": "import {\n  AddFieldsToHashDto,\n  CreateHashWithExpireDto,\n  DeleteFieldsFromHashDto,\n  GetHashFieldsDto,\n  GetHashFieldsResponse,\n  HashFieldDto,\n  HashFieldTtlDto,\n  UpdateHashFieldsTtlDto,\n} from 'src/modules/browser/hash/dto';\nimport { mockKeyDto } from 'src/modules/browser/__mocks__/keys';\nimport { flatMap } from 'lodash';\n\nexport const mockHashField: HashFieldDto = {\n  field: Buffer.from('field1'),\n  value: Buffer.from('value'),\n};\n\nexport const mockHashFieldWithExpire: HashFieldDto = {\n  field: Buffer.from('field-exp1'),\n  value: Buffer.from('value-exp1'),\n  expire: 111,\n};\nexport const mockHashFieldWithExpire2: HashFieldDto = {\n  field: Buffer.from('field-exp2'),\n  value: Buffer.from('value-exp2'),\n  expire: 222,\n};\n\nexport const mockAddFieldsDto: AddFieldsToHashDto = {\n  keyName: mockKeyDto.keyName,\n  fields: [mockHashField],\n};\n\nexport const mockAddFieldsWithExpirationDto: CreateHashWithExpireDto = {\n  keyName: mockKeyDto.keyName,\n  fields: [mockHashField, mockHashFieldWithExpire, mockHashFieldWithExpire2],\n};\n\nexport const mockHashFieldTtlDto = Object.assign(new HashFieldTtlDto(), {\n  field: Buffer.from('field-ttl'),\n  expire: -1,\n});\n\nexport const mockHashFieldTtlDto2 = Object.assign(new HashFieldTtlDto(), {\n  field: Buffer.from('field-ttl2'),\n  expire: 0,\n});\n\nexport const mockHashFieldTtlDto3 = Object.assign(new HashFieldTtlDto(), {\n  field: Buffer.from('field-ttl3'),\n  expire: 123123,\n});\n\nexport const mockUpdateHashFieldsTtlDto: UpdateHashFieldsTtlDto = Object.assign(\n  new UpdateHashFieldsTtlDto(),\n  {\n    keyName: mockKeyDto.keyName,\n    fields: [mockHashFieldTtlDto, mockHashFieldTtlDto2, mockHashFieldTtlDto3],\n  },\n);\n\nexport const mockCreateHashWithExpireDto: CreateHashWithExpireDto = {\n  keyName: mockKeyDto.keyName,\n  fields: [mockHashField],\n  expire: 3000,\n};\n\nexport const mockCreateHashWithExpireAndFieldsExpireDto: CreateHashWithExpireDto =\n  {\n    ...mockAddFieldsWithExpirationDto,\n    expire: 3000,\n  };\n\nexport const mockDeleteFieldsDto: DeleteFieldsFromHashDto = {\n  keyName: mockAddFieldsDto.keyName,\n  fields: mockAddFieldsDto.fields.map((item) => item.field),\n};\nexport const mockGetFieldsDto: GetHashFieldsDto = {\n  keyName: mockAddFieldsDto.keyName,\n  cursor: 0,\n  count: 15,\n  match: '*',\n};\nexport const mockGetFieldsResponse: GetHashFieldsResponse = {\n  keyName: mockGetFieldsDto.keyName,\n  nextCursor: 0,\n  total: mockAddFieldsDto.fields.length,\n  fields: mockAddFieldsDto.fields,\n};\nexport const mockGetFieldsWithTtlResponse: GetHashFieldsResponse = {\n  keyName: mockGetFieldsDto.keyName,\n  nextCursor: 0,\n  total: mockCreateHashWithExpireAndFieldsExpireDto.fields.length,\n  fields: mockCreateHashWithExpireAndFieldsExpireDto.fields.map((field) => ({\n    ...field,\n    expire: field.expire || -1,\n  })),\n};\nexport const mockRedisHScanResponse = [\n  0,\n  flatMap(mockAddFieldsDto.fields, ({ field, value }: HashFieldDto) => [\n    field,\n    value,\n  ]),\n];\nexport const mockRedisHScanWithFieldsExpireResponse = [\n  0,\n  flatMap(\n    mockCreateHashWithExpireAndFieldsExpireDto.fields,\n    ({ field, value }: HashFieldDto) => [field, value],\n  ),\n];\nexport const mockRedisHTtlResponse = flatMap(\n  mockCreateHashWithExpireAndFieldsExpireDto.fields,\n  ({ expire }: HashFieldDto) => [expire || -1],\n);\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/index.ts",
    "content": "export * from './keys';\nexport * from './streams';\nexport * from './z-set';\nexport * from './set';\nexport * from './list';\nexport * from './hash';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/keys.ts",
    "content": "export const mockKeyDto = {\n  keyName: Buffer.from('keyName'),\n};\n\nexport const mockScannerStrategy = {\n  getKeys: jest.fn().mockResolvedValue([]),\n  getKeyInfo: jest.fn().mockResolvedValue({}),\n  getKeysInfo: jest.fn().mockResolvedValue([]),\n  getKeysTtl: jest.fn().mockResolvedValue([]),\n  getKeysType: jest.fn().mockResolvedValue([]),\n  getKeysSize: jest.fn().mockResolvedValue([]),\n};\n\nexport const mockTypeInfoStrategy = {\n  getInfo: jest.fn().mockResolvedValue([]),\n};\n\nexport const mockScanner = jest.fn(() => ({\n  getStrategy: jest.fn().mockReturnValue(mockScannerStrategy),\n}));\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/list.ts",
    "content": "import {\n  DeleteListElementsDto,\n  GetListElementResponse,\n  GetListElementsDto,\n  GetListElementsResponse,\n  ListElementDestination,\n  PushElementToListDto,\n  SetListElementDto,\n} from 'src/modules/browser/list/dto';\nimport { mockKeyDto } from 'src/modules/browser/__mocks__/keys';\n\nexport const mockIndex: number = 0;\nexport const mockListElement = Buffer.from('Lorem ipsum dolor sit amet.');\nexport const mockListElement2 = Buffer.from('Lorem ipsum dolor sit amet2.');\nexport const mockListElements = [mockListElement];\nexport const mockPushElementDto: PushElementToListDto = {\n  keyName: mockKeyDto.keyName,\n  elements: mockListElements,\n  destination: ListElementDestination.Tail,\n};\nexport const mockGetListElementsDto: GetListElementsDto = {\n  keyName: mockKeyDto.keyName,\n  offset: 0,\n  count: 10,\n};\nexport const mockSetListElementDto: SetListElementDto = {\n  keyName: mockKeyDto.keyName,\n  element: mockListElement,\n  index: 0,\n};\nexport const mockDeleteElementsDto: DeleteListElementsDto = {\n  keyName: mockKeyDto.keyName,\n  destination: ListElementDestination.Tail,\n  count: 1,\n};\nexport const mockGetListElementsResponse: GetListElementsResponse = {\n  keyName: mockPushElementDto.keyName,\n  total: mockListElements.length,\n  elements: mockListElements,\n};\nexport const mockGetListElementResponse: GetListElementResponse = {\n  keyName: mockKeyDto.keyName,\n  value: mockListElement,\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/set.ts",
    "content": "import {\n  AddMembersToSetDto,\n  DeleteMembersFromSetDto,\n  GetSetMembersDto,\n  GetSetMembersResponse,\n} from 'src/modules/browser/set/dto';\n\nexport const mockSetMember = Buffer.from('Lorem ipsum dolor sit amet.');\nexport const mockSetMembers = [mockSetMember];\nexport const mockAddMembersToSetDto: AddMembersToSetDto = {\n  keyName: Buffer.from('testSet'),\n  members: [mockSetMember],\n};\nexport const mockDeleteMembersFromSetDto: DeleteMembersFromSetDto = {\n  keyName: mockAddMembersToSetDto.keyName,\n  members: mockAddMembersToSetDto.members,\n};\nexport const mockGetSetMembersDto: GetSetMembersDto = {\n  keyName: mockAddMembersToSetDto.keyName,\n  cursor: 0,\n  count: 15,\n  match: '*',\n};\nexport const mockGetSetMembersResponse: GetSetMembersResponse = {\n  keyName: mockGetSetMembersDto.keyName,\n  nextCursor: 0,\n  total: mockSetMembers.length,\n  members: mockSetMembers,\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/streams.ts",
    "content": "import {\n  AddStreamEntriesDto,\n  ConsumerDto,\n  CreateConsumerGroupDto,\n  GetStreamEntriesDto,\n  StreamEntryDto,\n  StreamEntryFieldDto,\n} from 'src/modules/browser/stream/dto';\nimport { SortOrder } from 'src/constants';\nimport { mockKeyDto } from 'src/modules/browser/__mocks__/keys';\n\n// stream entries\nexport const mockEntryId = '1651130346487-0';\nexport const mockEntryId2 = '1651130346487-1';\nexport const mockEntryField: StreamEntryFieldDto = {\n  name: Buffer.from('field1'),\n  value: Buffer.from('value1'),\n};\nexport const mockEntryField2: StreamEntryFieldDto = {\n  name: Buffer.from('field2'),\n  value: Buffer.from('value2'),\n};\nexport const mockStreamEntry: StreamEntryDto = {\n  id: mockEntryId,\n  fields: [mockEntryField],\n};\nexport const mockStreamEntries = [\n  { id: mockEntryId2, fields: [mockEntryField, mockEntryField2] },\n  { id: mockEntryId, fields: [mockEntryField, mockEntryField2] },\n];\nexport const mockAddStreamEntriesDto: AddStreamEntriesDto = {\n  keyName: Buffer.from('testList'),\n  entries: [mockStreamEntry],\n};\nexport const mockGetStreamEntriesDto: GetStreamEntriesDto = {\n  keyName: mockAddStreamEntriesDto.keyName,\n  start: '-',\n  end: '+',\n  sortOrder: SortOrder.Desc,\n};\n\n// stream info\nexport const mockEmptyStreamInfo = {\n  keyName: mockAddStreamEntriesDto.keyName,\n  total: 0,\n  lastGeneratedId: mockEntryId2,\n  firstEntry: null,\n  lastEntry: null,\n};\nexport const mockStreamInfo = {\n  keyName: mockAddStreamEntriesDto.keyName,\n  total: 2,\n  lastGeneratedId: mockEntryId2,\n  firstEntry: {\n    id: mockEntryId,\n    fields: [mockEntryField, mockEntryField2],\n  },\n  lastEntry: {\n    id: mockEntryId2,\n    fields: [mockEntryField, mockEntryField2],\n  },\n};\n\n// consumer groups\nexport const mockConsumerGroup = {\n  name: Buffer.from('consumer-1'),\n  consumers: 0,\n  pending: 0,\n  lastDeliveredId: mockEntryId2,\n  smallestPendingId: mockEntryId,\n  greatestPendingId: mockEntryId2,\n};\nexport const mockCreateConsumerGroupDto: CreateConsumerGroupDto = {\n  name: mockConsumerGroup.name,\n  lastDeliveredId: mockConsumerGroup.lastDeliveredId,\n};\n\n// consumers\nexport const mockConsumer: ConsumerDto = {\n  name: Buffer.from('consumer-1'),\n  pending: 0,\n  idle: 10,\n};\nexport const mockPendingMessage = {\n  id: mockEntryId,\n  consumerName: mockConsumer.name,\n  idle: mockConsumer.idle,\n  delivered: 1,\n};\nexport const mockGetPendingMessagesDto = {\n  ...mockKeyDto,\n  groupName: mockConsumerGroup.name,\n  start: '-',\n  end: '+',\n  count: 10,\n  consumerName: mockConsumer.name,\n};\nexport const mockAckPendingMessagesDto = {\n  ...mockKeyDto,\n  groupName: mockConsumerGroup.name,\n  entries: [mockPendingMessage.id, mockPendingMessage.id],\n};\nexport const mockClaimPendingEntriesDto = {\n  ...mockKeyDto,\n  groupName: mockConsumerGroup.name,\n  consumerName: mockConsumer.name,\n  entries: [mockPendingMessage.id, mockPendingMessage.id],\n  minIdleTime: 0,\n};\nexport const mockAdditionalClaimPendingEntriesDto = {\n  time: 0,\n  retryCount: 1,\n  force: true,\n};\n\n// Redis replies\nexport const mockStreamInfoReply = [\n  Buffer.from('length'),\n  2,\n  Buffer.from('radix-tree-keys'),\n  1,\n  Buffer.from('radix-tree-nodes'),\n  2,\n  Buffer.from('last-generated-id'),\n  Buffer.from(mockEntryId2),\n  Buffer.from('groups'),\n  0,\n  Buffer.from('first-entry'),\n  [\n    Buffer.from(mockEntryId),\n    [\n      mockEntryField.name,\n      mockEntryField.value,\n      mockEntryField2.name,\n      mockEntryField2.value,\n    ],\n  ],\n  Buffer.from('last-entry'),\n  [\n    Buffer.from(mockEntryId2),\n    [\n      mockEntryField.name,\n      mockEntryField.value,\n      mockEntryField2.name,\n      mockEntryField2.value,\n    ],\n  ],\n];\nexport const mockEmptyStreamInfoReply = [\n  Buffer.from('length'),\n  0,\n  Buffer.from('radix-tree-keys'),\n  1,\n  Buffer.from('radix-tree-nodes'),\n  2,\n  Buffer.from('last-generated-id'),\n  Buffer.from(mockEntryId2),\n  Buffer.from('groups'),\n  0,\n  Buffer.from('first-entry'),\n  null,\n  Buffer.from('last-entry'),\n  null,\n];\nexport const mockStreamEntriesReply = [\n  [\n    mockEntryId2,\n    [\n      mockEntryField.name,\n      mockEntryField.value,\n      mockEntryField2.name,\n      mockEntryField2.value,\n    ],\n  ],\n  [\n    mockEntryId,\n    [\n      mockEntryField.name,\n      mockEntryField.value,\n      mockEntryField2.name,\n      mockEntryField2.value,\n    ],\n  ],\n];\nexport const mockEmptyStreamEntriesReply = [];\nexport const mockConsumerGroupsReply = [\n  'name',\n  mockConsumerGroup.name,\n  'consumers',\n  mockConsumerGroup.consumers,\n  'pending',\n  mockConsumerGroup.pending,\n  'last-delivered-id',\n  mockConsumerGroup.lastDeliveredId,\n];\nexport const mockConsumerReply = [\n  'name',\n  mockConsumer.name,\n  'pending',\n  mockConsumer.pending,\n  'idle',\n  mockConsumer.idle,\n];\nexport const mockPendingMessageReply = Object.values(mockPendingMessage);\nexport const mockClaimPendingEntriesReply = [\n  mockPendingMessage.id,\n  mockPendingMessage.id,\n];\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/__mocks__/z-set.ts",
    "content": "import {\n  AddMembersToZSetDto,\n  DeleteMembersFromZSetDto,\n  GetZSetMembersDto,\n  SearchZSetMembersDto,\n  SearchZSetMembersResponse,\n  UpdateMemberInZSetDto,\n  ZSetMemberDto,\n} from 'src/modules/browser/z-set/dto';\nimport { SortOrder } from 'src/constants';\n\nexport const mockZSetMemberDto: ZSetMemberDto = {\n  name: Buffer.from('member1'),\n  score: '-inf',\n};\nexport const mockZSetMemberDto2: ZSetMemberDto = {\n  name: Buffer.from('member2'),\n  score: 0,\n};\nexport const mockZSetMemberDto3: ZSetMemberDto = {\n  name: Buffer.from('member3'),\n  score: 2,\n};\n\nexport const mockZSetMemberDto4: ZSetMemberDto = {\n  name: Buffer.from('member4'),\n  score: 'inf',\n};\nexport const mockGetMembersDto: GetZSetMembersDto = {\n  keyName: Buffer.from('zSet'),\n  offset: 0,\n  count: 15,\n  sortOrder: SortOrder.Asc,\n};\nexport const mockSearchMembersDto: SearchZSetMembersDto = {\n  keyName: Buffer.from('zSet'),\n  cursor: 0,\n  count: 15,\n  match: '*',\n};\nexport const mockAddMembersDto: AddMembersToZSetDto = {\n  keyName: mockGetMembersDto.keyName,\n  members: [\n    mockZSetMemberDto,\n    mockZSetMemberDto2,\n    mockZSetMemberDto3,\n    mockZSetMemberDto4,\n  ],\n};\nexport const mockUpdateMemberDto: UpdateMemberInZSetDto = {\n  keyName: mockGetMembersDto.keyName,\n  member: mockAddMembersDto.members[0],\n};\nexport const mockMembersForZAddCommand = [\n  mockZSetMemberDto.score,\n  mockZSetMemberDto.name,\n  mockZSetMemberDto2.score,\n  mockZSetMemberDto2.name,\n  mockZSetMemberDto3.score,\n  mockZSetMemberDto3.name,\n  mockZSetMemberDto4.score,\n  mockZSetMemberDto4.name,\n];\nexport const mockDeleteMembersDto: DeleteMembersFromZSetDto = {\n  keyName: mockAddMembersDto.keyName,\n  members: [\n    mockZSetMemberDto.name,\n    mockZSetMemberDto2.name,\n    mockZSetMemberDto3.name,\n    mockZSetMemberDto4.name,\n  ],\n};\nexport const getZSetMembersInAscResponse = {\n  keyName: mockGetMembersDto.keyName,\n  total: mockAddMembersDto.members.length,\n  members: [...mockAddMembersDto.members],\n};\nexport const getZSetMembersInDescResponse = {\n  keyName: mockGetMembersDto.keyName,\n  total: mockAddMembersDto.members.length,\n  members: mockAddMembersDto.members.slice().reverse(),\n};\nexport const mockSearchZSetMembersResponse: SearchZSetMembersResponse = {\n  keyName: mockGetMembersDto.keyName,\n  total: mockAddMembersDto.members.length,\n  nextCursor: 0,\n  members: [...mockAddMembersDto.members],\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/browser-history.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Query,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiQuery, ApiTags } from '@nestjs/swagger';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport {\n  BrowserHistory,\n  DeleteBrowserHistoryItemsDto,\n  DeleteBrowserHistoryItemsResponse,\n  ListBrowserHistoryDto,\n} from 'src/modules/browser/browser-history/dto';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { DeleteBrowserHistoryQueryDto } from 'src/modules/browser/browser-history/dto/delete.browser-history.query.dto';\n\n@UseInterceptors(BrowserSerializeInterceptor)\n@UsePipes(new ValidationPipe({ transform: true }))\n@ApiTags('Browser: Browser History')\n@Controller('history')\nexport class BrowserHistoryController {\n  constructor(private readonly service: BrowserHistoryService) {}\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get browser history',\n    responses: [\n      {\n        status: 200,\n        type: BrowserHistory,\n      },\n    ],\n  })\n  @Get('')\n  @ApiQuery({\n    name: 'mode',\n    enum: BrowserHistoryMode,\n  })\n  async list(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('dbInstance') databaseId: string,\n    @Query() dto: ListBrowserHistoryDto,\n  ): Promise<BrowserHistory[]> {\n    return this.service.list(sessionMetadata, databaseId, dto.mode);\n  }\n\n  @Delete('/:id')\n  @ApiRedisParams()\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete browser history item by id',\n  })\n  async delete(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('dbInstance') databaseId: string,\n    @Query() query: DeleteBrowserHistoryQueryDto,\n    @Param('id') id: string,\n  ): Promise<void> {\n    await this.service.delete(sessionMetadata, databaseId, query.mode, id);\n  }\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete bulk browser history items',\n    responses: [\n      {\n        status: 200,\n        type: DeleteBrowserHistoryItemsResponse,\n      },\n    ],\n  })\n  @ApiRedisParams()\n  @Delete('')\n  async bulkDelete(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('dbInstance') databaseId: string,\n    @Query() query: DeleteBrowserHistoryQueryDto,\n    @Body() dto: DeleteBrowserHistoryItemsDto,\n  ): Promise<DeleteBrowserHistoryItemsResponse> {\n    return this.service.bulkDelete(\n      sessionMetadata,\n      databaseId,\n      query.mode,\n      dto.ids,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/browser-history.module.ts",
    "content": "import { DynamicModule, Global, Module, Type } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { BrowserHistoryController } from 'src/modules/browser/browser-history/browser-history.controller';\nimport { BrowserHistoryRepository } from './repositories/browser-history.repository';\n\n@Global()\n@Module({})\nexport class BrowserHistoryModule {\n  static register(\n    { route },\n    browserHistoryRepository: Type<BrowserHistoryRepository>,\n  ): DynamicModule {\n    return {\n      module: BrowserHistoryModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: BrowserHistoryModule,\n          },\n        ]),\n      ],\n      controllers: [BrowserHistoryController],\n      providers: [\n        BrowserHistoryService,\n        {\n          provide: BrowserHistoryRepository,\n          useClass: browserHistoryRepository,\n        },\n      ],\n      exports: [BrowserHistoryService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/browser-history.service.spec.ts",
    "content": "import { NotFoundException } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockDatabase,\n  MockType,\n  mockDatabaseId,\n  mockBrowserHistoryRepository,\n  mockBrowserHistory,\n  mockClientMetadata,\n  mockSessionMetadata,\n  mockCreateBrowserHistoryDto,\n} from 'src/__mocks__';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { BrowserHistoryRepository } from './repositories/browser-history.repository';\n\ndescribe('BrowserHistoryService', () => {\n  let service: BrowserHistoryService;\n  let browserHistoryRepository: MockType<BrowserHistoryRepository>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        BrowserHistoryService,\n        {\n          provide: BrowserHistoryRepository,\n          useFactory: mockBrowserHistoryRepository,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(BrowserHistoryService);\n    browserHistoryRepository = await module.get(BrowserHistoryRepository);\n  });\n\n  describe('create', () => {\n    it('should create new database and send analytics event', async () => {\n      browserHistoryRepository.create.mockResolvedValue(mockBrowserHistory);\n      expect(\n        await service.create(mockClientMetadata, mockBrowserHistory),\n      ).toEqual(mockBrowserHistory);\n    });\n    it('should throw NotFound if no browser history?', async () => {\n      browserHistoryRepository.create.mockRejectedValueOnce(new Error());\n      await expect(\n        service.create(mockClientMetadata, mockBrowserHistory),\n      ).rejects.toThrow(new Error());\n    });\n  });\n\n  describe('get', () => {\n    it('should return browser history by id', async () => {\n      browserHistoryRepository.get.mockResolvedValue(\n        mockCreateBrowserHistoryDto,\n      );\n      expect(await service.get(mockSessionMetadata, mockDatabase.id)).toEqual(\n        mockCreateBrowserHistoryDto,\n      );\n    });\n  });\n\n  describe('list', () => {\n    it('should return browser history items', async () => {\n      browserHistoryRepository.list.mockResolvedValue([\n        mockBrowserHistory,\n        mockBrowserHistory,\n      ]);\n      expect(\n        await service.list(\n          mockSessionMetadata,\n          mockDatabaseId,\n          BrowserHistoryMode.Pattern,\n        ),\n      ).toEqual([mockBrowserHistory, mockBrowserHistory]);\n    });\n    it('should throw Error?', async () => {\n      browserHistoryRepository.list.mockRejectedValueOnce(new Error());\n      await expect(\n        service.list(\n          mockSessionMetadata,\n          mockDatabaseId,\n          BrowserHistoryMode.Pattern,\n        ),\n      ).rejects.toThrow(Error);\n    });\n  });\n\n  describe('delete', () => {\n    it('should remove existing browser history item', async () => {\n      browserHistoryRepository.delete.mockResolvedValue(mockBrowserHistory);\n      expect(\n        await service.delete(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          BrowserHistoryMode.Pattern,\n          mockBrowserHistory.id,\n        ),\n      ).toEqual(mockBrowserHistory);\n    });\n    it('should throw NotFoundException? on any error during deletion', async () => {\n      browserHistoryRepository.delete.mockRejectedValueOnce(\n        new NotFoundException(),\n      );\n      await expect(\n        service.delete(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          BrowserHistoryMode.Pattern,\n          mockBrowserHistory.id,\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('bulkDelete', () => {\n    it('should remove multiple browser history items', async () => {\n      expect(\n        await service.bulkDelete(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          BrowserHistoryMode.Pattern,\n          [mockBrowserHistory.id],\n        ),\n      ).toEqual({ affected: 1 });\n    });\n    it('should ignore errors and do not count affected', async () => {\n      browserHistoryRepository.delete.mockRejectedValueOnce(\n        new NotFoundException(),\n      );\n      expect(\n        await service.bulkDelete(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          BrowserHistoryMode.Pattern,\n          [mockBrowserHistory.id],\n        ),\n      ).toEqual({ affected: 0 });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/browser-history.service.ts",
    "content": "import { HttpException, Injectable, Logger } from '@nestjs/common';\nimport { catchAclError } from 'src/utils';\nimport { sum } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport { ClientMetadata, SessionMetadata } from 'src/common/models';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport {\n  BrowserHistory,\n  CreateBrowserHistoryDto,\n  DeleteBrowserHistoryItemsResponse,\n} from 'src/modules/browser/browser-history/dto';\nimport { BrowserHistoryRepository } from './repositories/browser-history.repository';\n\n@Injectable()\nexport class BrowserHistoryService {\n  private logger = new Logger('BrowserHistoryService');\n\n  constructor(\n    private readonly browserHistoryRepository: BrowserHistoryRepository,\n  ) {}\n\n  /**\n   * Create a new browser history item\n   * @param clientMetadata\n   * @param dto\n   */\n  public async create(\n    clientMetadata: ClientMetadata,\n    dto: CreateBrowserHistoryDto,\n  ): Promise<BrowserHistory> {\n    try {\n      const history = plainToInstance(BrowserHistory, {\n        ...dto,\n        databaseId: clientMetadata.databaseId,\n      });\n      return this.browserHistoryRepository.create(\n        clientMetadata.sessionMetadata,\n        history,\n      );\n    } catch (e) {\n      this.logger.error(\n        'Unable to create browser history item',\n        e,\n        clientMetadata,\n      );\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Get browser history with all fields by id\n   * @param id\n   */\n  async get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<BrowserHistory> {\n    return this.browserHistoryRepository.get(sessionMetadata, id);\n  }\n\n  /**\n   * Get browser history list for particular database with id and createdAt fields only\n   * @param databaseId\n   * @param mode\n   */\n  async list(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n  ): Promise<BrowserHistory[]> {\n    return this.browserHistoryRepository.list(\n      sessionMetadata,\n      databaseId,\n      mode,\n    );\n  }\n\n  /**\n   * Delete browser history item by id\n   * @param databaseId\n   * @param mode\n   * @param id\n   */\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n    id: string,\n  ): Promise<void> {\n    return this.browserHistoryRepository.delete(\n      sessionMetadata,\n      databaseId,\n      mode,\n      id,\n    );\n  }\n\n  /**\n   * Bulk delete browser history items. Uses \"delete\" method and skipping error\n   * Returns successfully deleted browser history items number\n   * @param sessionMetadata\n   * @param databaseId\n   * @param mode\n   * @param ids\n   */\n  async bulkDelete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n    ids: string[],\n  ): Promise<DeleteBrowserHistoryItemsResponse> {\n    this.logger.debug(\n      `Deleting many browser history items: ${ids}`,\n      sessionMetadata,\n    );\n\n    return {\n      affected: sum(\n        await Promise.all(\n          ids.map(async (id) => {\n            try {\n              await this.delete(sessionMetadata, databaseId, mode, id);\n              return 1;\n            } catch (e) {\n              return 0;\n            }\n          }),\n        ),\n      ),\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/create.browser-history.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsOptional } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { ScanFilter } from './get.browser-history.dto';\n\nexport class CreateBrowserHistoryDto {\n  @ApiProperty({\n    description: 'Filters for scan operation',\n    type: () => ScanFilter,\n    default: new ScanFilter(),\n  })\n  @IsOptional()\n  @Type(() => ScanFilter)\n  filter: ScanFilter = new ScanFilter();\n\n  @ApiProperty({\n    description: 'Search mode',\n    type: String,\n    example: BrowserHistoryMode.Pattern,\n  })\n  @IsOptional()\n  @IsEnum(BrowserHistoryMode, {\n    message: `mode must be a valid enum value. Valid values: ${Object.values(\n      BrowserHistoryMode,\n    )}.`,\n  })\n  mode?: BrowserHistoryMode = BrowserHistoryMode.Pattern;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/delete.browser-history.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class DeleteBrowserHistoryItemsDto {\n  @ApiProperty({\n    description: 'The unique ID of the browser history requested',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  ids: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/delete.browser-history.query.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum } from 'class-validator';\nimport { BrowserHistoryMode } from 'src/common/constants';\n\nexport class DeleteBrowserHistoryQueryDto {\n  @ApiProperty({\n    description: 'Search mode',\n    type: String,\n    example: BrowserHistoryMode.Pattern,\n  })\n  @IsEnum(BrowserHistoryMode, {\n    message: `mode must be a valid enum value. Valid values: ${Object.values(\n      BrowserHistoryMode,\n    )}.`,\n  })\n  mode: BrowserHistoryMode = BrowserHistoryMode.Pattern;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/delete.browser-history.response.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteBrowserHistoryItemsResponse {\n  @ApiProperty({\n    description: 'Number of affected browser history items',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/get.browser-history.dto.ts",
    "content": "import { Expose, Type } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsOptional, IsString } from 'class-validator';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\n\nexport class ScanFilter {\n  @ApiProperty({\n    description: 'Key type',\n    type: String,\n    example: 'list',\n  })\n  @IsOptional()\n  @Expose()\n  @IsEnum(RedisDataType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      RedisDataType,\n    )}.`,\n  })\n  type?: RedisDataType = null;\n\n  @ApiProperty({\n    description: 'Match glob patterns',\n    type: String,\n    example: 'device:*',\n    default: '*',\n  })\n  @IsOptional()\n  @IsString()\n  @Expose()\n  match?: string = '*';\n}\n\nexport class BrowserHistory {\n  @ApiProperty({\n    description: 'History id',\n    type: String,\n    default: '76dd5654-814b-4e49-9c72-b236f50891f4',\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: String,\n    default: '76dd5654-814b-4e49-9c72-b236f50891f4',\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiProperty({\n    description: 'Filters for scan operation',\n    type: () => ScanFilter,\n  })\n  @Expose()\n  @Type(() => ScanFilter)\n  filter: ScanFilter = new ScanFilter();\n\n  @ApiProperty({\n    description: 'Mode of history',\n    default: BrowserHistoryMode.Pattern,\n    enum: BrowserHistoryMode,\n  })\n  @Expose()\n  mode?: BrowserHistoryMode = BrowserHistoryMode.Pattern;\n\n  @ApiProperty({\n    description: 'History created date (ISO string)',\n    type: Date,\n    default: '2022-09-16T06:29:20.000Z',\n  })\n  @Expose()\n  createdAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/index.ts",
    "content": "export * from './create.browser-history.dto';\nexport * from './delete.browser-history.dto';\nexport * from './delete.browser-history.response.dto';\nexport * from './get.browser-history.dto';\nexport * from './list.browser-history.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/dto/list.browser-history.dto.ts",
    "content": "import { IsEnum, IsOptional } from 'class-validator';\nimport { BrowserHistoryMode } from 'src/common/constants';\n\nexport class ListBrowserHistoryDto {\n  @IsEnum(BrowserHistoryMode)\n  @IsOptional()\n  mode?: BrowserHistoryMode;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/entities/browser-history.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  Index,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { BrowserHistoryMode } from 'src/common/constants';\n\n@Entity('browser_history')\nexport class BrowserHistoryEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Index()\n  @Expose()\n  databaseId: string;\n\n  @ManyToOne(() => DatabaseEntity, {\n    nullable: false,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn({ name: 'databaseId' })\n  database: DatabaseEntity;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  filter: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  mode?: string = BrowserHistoryMode.Pattern;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @CreateDateColumn()\n  @Index()\n  @Expose()\n  createdAt: Date;\n\n  constructor(entity: Partial<BrowserHistoryEntity>) {\n    Object.assign(this, entity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/repositories/browser-history.repository.ts",
    "content": "import { BrowserHistoryMode } from 'src/common/constants';\nimport { SessionMetadata } from 'src/common/models';\nimport { BrowserHistory } from '../dto';\n\nexport abstract class BrowserHistoryRepository {\n  abstract create(\n    sessionMetadata: SessionMetadata,\n    history: Partial<BrowserHistory>,\n  ): Promise<BrowserHistory>;\n  abstract get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<BrowserHistory>;\n  abstract list(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n  ): Promise<BrowserHistory[]>;\n  abstract delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n    id: string,\n  ): Promise<void>;\n  abstract cleanupDatabaseHistory(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: string,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/repositories/local.browser-history.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport {\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { Repository } from 'typeorm';\nimport {\n  mockEncryptionService,\n  mockEncryptResult,\n  mockRepository,\n  mockDatabase,\n  MockType,\n  mockQueryBuilderGetMany,\n  mockQueryBuilderGetManyRaw,\n  mockBrowserHistory,\n  mockBrowserHistoryEntity,\n  mockBrowserHistoryPartial,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { BrowserHistoryEntity } from 'src/modules/browser/browser-history/entities/browser-history.entity';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';\nimport { BrowserHistory } from 'src/modules/browser/browser-history/dto';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { LocalBrowserHistoryRepository } from './local.browser-history.repository';\n\ndescribe('LocalBrowserHistoryRepository', () => {\n  let browserHistoryRepository: LocalBrowserHistoryRepository;\n  let repository: MockType<Repository<BrowserHistory>>;\n  let encryptionService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalBrowserHistoryRepository,\n        {\n          provide: getRepositoryToken(BrowserHistoryEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    browserHistoryRepository = module.get<LocalBrowserHistoryRepository>(\n      LocalBrowserHistoryRepository,\n    );\n    repository = module.get(getRepositoryToken(BrowserHistoryEntity));\n    encryptionService = module.get<EncryptionService>(EncryptionService);\n\n    // encryption mocks\n    ['filter'].forEach((field) => {\n      when(encryptionService.encrypt)\n        .calledWith(JSON.stringify(mockBrowserHistory[field]))\n        .mockReturnValue({\n          ...mockEncryptResult,\n          data: mockBrowserHistoryEntity[field],\n        });\n      when(encryptionService.decrypt)\n        .calledWith(\n          mockBrowserHistoryEntity[field],\n          mockEncryptResult.encryption,\n        )\n        .mockReturnValue(JSON.stringify(mockBrowserHistory[field]));\n    });\n  });\n\n  describe('create', () => {\n    it('should process new entity', async () => {\n      repository.save.mockReturnValueOnce(mockBrowserHistoryEntity);\n      expect(\n        await browserHistoryRepository.create(\n          mockSessionMetadata,\n          mockBrowserHistoryPartial,\n        ),\n      ).toEqual(mockBrowserHistory);\n    });\n  });\n\n  describe('get', () => {\n    it('should get browser history item', async () => {\n      repository.findOneBy.mockReturnValueOnce(mockBrowserHistoryEntity);\n\n      expect(\n        await browserHistoryRepository.get(\n          mockSessionMetadata,\n          mockBrowserHistory.id,\n        ),\n      ).toEqual(mockBrowserHistory);\n    });\n    it('should return null fields in case of decryption errors', async () => {\n      when(encryptionService.decrypt)\n        .calledWith(\n          mockBrowserHistoryEntity['filter'],\n          mockEncryptResult.encryption,\n        )\n        .mockRejectedValueOnce(new KeytarDecryptionErrorException());\n      repository.findOneBy.mockReturnValueOnce(mockBrowserHistoryEntity);\n\n      expect(\n        await browserHistoryRepository.get(\n          mockSessionMetadata,\n          mockBrowserHistory.id,\n        ),\n      ).toEqual({\n        ...mockBrowserHistory,\n        filter: null,\n      });\n    });\n    it('should throw an error', async () => {\n      repository.findOneBy.mockReturnValueOnce(null);\n\n      try {\n        await browserHistoryRepository.get(\n          mockSessionMetadata,\n          mockBrowserHistory.id,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(\n          ERROR_MESSAGES.BROWSER_HISTORY_ITEM_NOT_FOUND,\n        );\n      }\n    });\n  });\n\n  describe('list', () => {\n    it('should get list of browser history', async () => {\n      mockQueryBuilderGetMany.mockReturnValueOnce([mockBrowserHistoryEntity]);\n      expect(\n        await browserHistoryRepository.list(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          BrowserHistoryMode.Pattern,\n        ),\n      ).toMatchObject([\n        {\n          id: mockBrowserHistoryEntity.id,\n          createdAt: mockBrowserHistoryEntity.createdAt,\n          mode: mockBrowserHistoryEntity.mode,\n          filter: {\n            type: mockBrowserHistory.filter.type,\n            match: mockBrowserHistory.filter.match,\n          },\n        },\n      ]);\n    });\n  });\n\n  describe('delete', () => {\n    it('Should not return anything on cleanup', async () => {\n      repository.delete.mockReturnValueOnce(mockBrowserHistoryEntity);\n      expect(\n        await browserHistoryRepository.delete(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          mockBrowserHistory.mode,\n          mockBrowserHistory.id,\n        ),\n      ).toEqual(undefined);\n    });\n    it('Should throw InternalServerErrorException when error during delete', async () => {\n      repository.delete.mockRejectedValueOnce(new Error());\n      await expect(\n        browserHistoryRepository.delete(\n          mockSessionMetadata,\n          mockBrowserHistory.databaseId,\n          mockBrowserHistory.mode,\n          mockBrowserHistory.id,\n        ),\n      ).rejects.toThrowError(InternalServerErrorException);\n    });\n  });\n\n  describe('cleanupDatabaseHistory', () => {\n    it('Should not return anything on cleanup', async () => {\n      mockQueryBuilderGetManyRaw.mockReturnValue([\n        { id: mockBrowserHistoryEntity.id },\n        { id: mockBrowserHistoryEntity.id },\n      ]);\n\n      expect(\n        await browserHistoryRepository.cleanupDatabaseHistory(\n          mockSessionMetadata,\n          mockDatabase.id,\n          BrowserHistoryMode.Pattern,\n        ),\n      ).toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser-history/repositories/local.browser-history.repository.ts",
    "content": "import {\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { plainToInstance } from 'class-transformer';\nimport { classToClass } from 'src/utils';\nimport config from 'src/utils/config';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { BrowserHistoryMode } from 'src/common/constants';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { SessionMetadata } from 'src/common/models';\nimport { BrowserHistoryEntity } from '../entities/browser-history.entity';\nimport { BrowserHistoryRepository } from './browser-history.repository';\nimport { BrowserHistory } from '../dto';\n\nconst BROWSER_HISTORY_CONFIG = config.get('browser_history');\n\n@Injectable()\nexport class LocalBrowserHistoryRepository extends BrowserHistoryRepository {\n  private readonly logger = new Logger('BrowserHistoryRepository');\n\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(BrowserHistoryEntity)\n    private readonly repository: Repository<BrowserHistoryEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, ['filter']);\n  }\n\n  /**\n   * Encrypt browser history and save entire entity\n   * Should always throw and error in case when unable to encrypt for some reason\n   * @param sessionMetadata\n   * @param history\n   */\n  async create(\n    sessionMetadata: SessionMetadata,\n    history: Partial<BrowserHistory>,\n  ): Promise<BrowserHistory> {\n    const encryptedDto = await this.modelEncryptor.encryptEntity(\n      plainToInstance(BrowserHistoryEntity, history),\n    );\n    const entity = await this.repository.save(encryptedDto);\n\n    // cleanup history and ignore error if any\n    try {\n      await this.cleanupDatabaseHistory(\n        sessionMetadata,\n        entity.databaseId,\n        entity.mode,\n      );\n    } catch (e) {\n      this.logger.error(\n        'Error when trying to cleanup history after insert',\n        e,\n        sessionMetadata,\n      );\n    }\n\n    return classToClass(\n      BrowserHistory,\n      await this.modelEncryptor.decryptEntity(entity),\n    );\n  }\n\n  /**\n   * Fetches entity, decrypt and return full BrowserHistory model\n   * @param sessionMetadata\n   * @param id\n   */\n  async get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<BrowserHistory> {\n    const entity = await this.repository.findOneBy({ id });\n\n    if (!entity) {\n      this.logger.error(\n        `Browser history item with id:${id} was not Found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(\n        ERROR_MESSAGES.BROWSER_HISTORY_ITEM_NOT_FOUND,\n      );\n    }\n\n    return classToClass(\n      BrowserHistory,\n      await this.modelEncryptor.decryptEntity(entity, true),\n    );\n  }\n\n  /**\n   * Return list of browser history with several fields only\n   * @param sessionMetadata\n   * @param databaseId\n   * @param mode\n   */\n  async list(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n  ): Promise<BrowserHistory[]> {\n    this.logger.debug('Getting browser history list', sessionMetadata);\n    const entities = await this.repository\n      .createQueryBuilder('a')\n      .where({ databaseId, mode })\n      .select(['a.id', 'a.filter', 'a.mode', 'a.encryption'])\n      .orderBy('a.createdAt', 'DESC')\n      .limit(BROWSER_HISTORY_CONFIG.maxItemsPerModeInDb)\n      .getMany();\n\n    this.logger.debug('Succeed to get history list', sessionMetadata);\n\n    const decryptedEntities = await Promise.all(\n      entities.map<Promise<BrowserHistoryEntity>>(async (entity) => {\n        try {\n          return await this.modelEncryptor.decryptEntity(entity, true);\n        } catch (e) {\n          return null;\n        }\n      }),\n    );\n\n    return decryptedEntities.map((entity) =>\n      classToClass(BrowserHistory, entity),\n    );\n  }\n\n  /**\n   * Delete history item by id\n   * @param sessionMetadata\n   * @param databaseId\n   * @param mode\n   * @param id\n   */\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: BrowserHistoryMode,\n    id: string,\n  ): Promise<void> {\n    this.logger.debug(`Deleting browser history item: ${id}`, sessionMetadata);\n    try {\n      await this.repository.delete({ id, databaseId, mode });\n      // todo: rethink\n      this.logger.debug(\n        'Succeed to delete browser history item.',\n        sessionMetadata,\n      );\n    } catch (error) {\n      this.logger.error(\n        `Failed to delete history items: ${id}`,\n        error,\n        sessionMetadata,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n\n  /**\n   * Clean history for particular database to fit 5 items limitation for each mode\n   * and remove duplicates\n   * @param _sessionMetadata\n   * @param databaseId\n   * @param mode\n   */\n  async cleanupDatabaseHistory(\n    _sessionMetadata: SessionMetadata,\n    databaseId: string,\n    mode: string,\n  ): Promise<void> {\n    // todo: investigate why delete with sub-query doesn't works\n    const idsDuplicates = (\n      await this.repository\n        .createQueryBuilder()\n        .where({ databaseId, mode })\n        .select('id')\n        .groupBy('filter')\n        .having('COUNT(filter) > 1')\n        .getRawMany()\n    ).map((item) => item.id);\n\n    const idsOverLimit = (\n      await this.repository\n        .createQueryBuilder()\n        .where({ databaseId, mode })\n        .select('id')\n        .orderBy('createdAt', 'DESC')\n        .offset(\n          BROWSER_HISTORY_CONFIG.maxItemsPerModeInDb + idsDuplicates.length,\n        )\n        .getRawMany()\n    ).map((item) => item.id);\n\n    await this.repository\n      .createQueryBuilder()\n      .delete()\n      .whereInIds([...idsOverLimit, ...idsDuplicates])\n      .execute();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser.base.controller.ts",
    "content": "import { UseInterceptors } from '@nestjs/common';\nimport { TimeoutInterceptor } from 'src/common/interceptors';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\n@UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.REQUEST_TIMEOUT))\nexport class BrowserBaseController {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/browser.module.ts",
    "content": "import { DynamicModule, Module, Type } from '@nestjs/common';\nimport { ListModule } from 'src/modules/browser/list/list.module';\nimport { HashModule } from 'src/modules/browser/hash/hash.module';\nimport { ZSetModule } from 'src/modules/browser/z-set/z-set.module';\nimport { StringModule } from 'src/modules/browser/string/string.module';\nimport { SetModule } from 'src/modules/browser/set/set.module';\nimport { BrowserHistoryModule } from 'src/modules/browser/browser-history/browser-history.module';\nimport { RejsonRlModule } from 'src/modules/browser/rejson-rl/rejson-rl.module';\nimport { StreamModule } from 'src/modules/browser/stream/stream.module';\nimport { RedisearchModule } from 'src/modules/browser/redisearch/redisearch.module';\nimport { KeysModule } from 'src/modules/browser/keys/keys.module';\nimport { BrowserHistoryRepository } from './browser-history/repositories/browser-history.repository';\nimport { LocalBrowserHistoryRepository } from './browser-history/repositories/local.browser-history.repository';\n\nconst route = '/databases/:dbInstance';\n\n@Module({})\nexport class BrowserModule {\n  static register(\n    browserHistoryRepository: Type<BrowserHistoryRepository> = LocalBrowserHistoryRepository,\n  ): DynamicModule {\n    return {\n      module: BrowserModule,\n      imports: [\n        ListModule.register({ route }),\n        HashModule.register({ route }),\n        ZSetModule.register({ route }),\n        StringModule.register({ route }),\n        SetModule.register({ route }),\n        BrowserHistoryModule.register({ route }, browserHistoryRepository),\n        StreamModule.register({ route }),\n        RejsonRlModule.register({ route }),\n        RedisearchModule.register({ route }),\n        KeysModule.register({ route }),\n      ],\n      exports: [BrowserHistoryModule],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/constants/browser-tool-commands.ts",
    "content": "export enum BrowserToolKeysCommands {\n  Scan = 'scan',\n  Ttl = 'ttl',\n  Type = 'type',\n  Exists = 'exists',\n  Expire = 'expire',\n  Persist = 'persist',\n  Del = 'del',\n  Unlink = 'unlink',\n  Rename = 'rename',\n  RenameNX = 'renamenx',\n  MemoryUsage = 'memory usage',\n}\n\nexport enum BrowserToolStringCommands {\n  Set = 'set',\n  Get = 'get',\n  Getrange = 'getrange',\n  StrLen = 'strlen',\n}\n\nexport enum BrowserToolHashCommands {\n  HSet = 'hset',\n  HGetAll = 'hgetall',\n  HGETALL = 'HGETALL',\n  HGet = 'hget',\n  HLen = 'hlen',\n  HScan = 'hscan',\n  HDel = 'hdel',\n  HExpire = 'hexpire',\n  HPersist = 'hpersist',\n  HTtl = 'httl',\n}\n\nexport enum BrowserToolListCommands {\n  LLen = 'llen',\n  Lrange = 'lrange',\n  LSet = 'lset',\n  LPush = 'lpush',\n  LPop = 'lpop',\n  RPush = 'rpush',\n  RPushX = 'rpushx',\n  LPushX = 'lpushx',\n  RPop = 'rpop',\n  LIndex = 'lindex',\n}\n\nexport enum BrowserToolSetCommands {\n  SScan = 'sscan',\n  SAdd = 'sadd',\n  SCard = 'scard',\n  SRem = 'srem',\n  SIsMember = 'sismember',\n}\n\nexport enum BrowserToolZSetCommands {\n  ZCard = 'zcard',\n  ZScan = 'zscan',\n  ZRange = 'zrange',\n  ZRevRange = 'zrevrange',\n  ZAdd = 'zadd',\n  ZRem = 'zrem',\n  ZScore = 'zscore',\n}\n\nexport enum BrowserToolRejsonRlCommands {\n  JsonDel = 'json.del',\n  JsonSet = 'json.set',\n  JsonGet = 'json.get',\n  JsonType = 'json.type',\n  JsonObjKeys = 'json.objkeys',\n  JsonObjLen = 'json.objlen',\n  JsonArrLen = 'json.arrlen',\n  JsonStrLen = 'json.strlen',\n  JsonArrAppend = 'json.arrappend',\n  JsonDebug = 'json.debug',\n}\n\nexport enum BrowserToolGraphCommands {\n  GraphQuery = 'graph.query',\n}\nexport enum BrowserToolStreamCommands {\n  XLen = 'xlen',\n  XInfoStream = 'xinfo stream',\n  XRange = 'xrange',\n  XRevRange = 'xrevrange',\n  XAdd = 'xadd',\n  XDel = 'xdel',\n  XInfoGroups = 'xinfo groups',\n  XInfoConsumers = 'xinfo consumers',\n  XPending = 'xpending',\n  XAck = 'xack',\n  XClaim = 'xclaim',\n  XGroupCreate = 'xgroup create',\n  XGroupSetId = 'xgroup setid',\n  XGroupDestroy = 'xgroup destroy',\n  XGroupDelConsumer = 'xgroup delconsumer',\n}\n\nexport enum BrowserToolTSCommands {\n  TSInfo = 'ts.info',\n}\n\nexport type BrowserToolCommands =\n  | BrowserToolKeysCommands\n  | BrowserToolStringCommands\n  | BrowserToolSetCommands\n  | BrowserToolListCommands\n  | BrowserToolHashCommands\n  | BrowserToolZSetCommands\n  | BrowserToolRejsonRlCommands\n  | BrowserToolStreamCommands\n  | BrowserToolGraphCommands\n  | BrowserToolTSCommands;\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/decorators/browser-client-metadata.decorator.ts",
    "content": "import { API_PARAM_DATABASE_ID } from 'src/common/constants';\nimport { ClientContext } from 'src/common/models';\nimport { ClientMetadataParam } from 'src/common/decorators';\n\nexport const BrowserClientMetadata = (\n  databaseIdParam = API_PARAM_DATABASE_ID,\n) =>\n  ClientMetadataParam({\n    context: ClientContext.Browser,\n    databaseIdParam,\n  });\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/add.fields-to-hash.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { HashFieldDto } from 'src/modules/browser/hash/dto';\n\nexport class AddFieldsToHashDto extends KeyDto {\n  @ApiProperty({\n    description: 'Hash fields',\n    isArray: true,\n    type: HashFieldDto,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested()\n  @Type(() => HashFieldDto)\n  fields: HashFieldDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/create.hash-with-expire.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { AddFieldsToHashDto } from 'src/modules/browser/hash/dto';\n\nexport class CreateHashWithExpireDto extends IntersectionType(\n  AddFieldsToHashDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/delete.fields-from-hash.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class DeleteFieldsFromHashDto extends KeyDto {\n  @ApiProperty({\n    description: 'Hash fields',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  fields: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/delete.fields-from-hash.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteFieldsFromHashResponse {\n  @ApiProperty({\n    description: 'Number of affected fields',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/get.hash-fields.dto.ts",
    "content": "import { ScanDataTypeDto } from 'src/modules/browser/keys/dto';\n\nexport class GetHashFieldsDto extends ScanDataTypeDto {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/get.hash-fields.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { HashFieldDto } from 'src/modules/browser/hash/dto';\n\nexport class HashScanResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    minimum: 0,\n    description:\n      'The new cursor to use in the next call.' +\n      ' If the property has value of 0, then the iteration is completed.',\n  })\n  nextCursor: number;\n\n  @ApiProperty({\n    type: () => HashFieldDto,\n    description: 'Array of members.',\n    isArray: true,\n  })\n  fields: HashFieldDto[];\n}\n\nexport class GetHashFieldsResponse extends HashScanResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'The number of fields in the currently-selected hash.',\n  })\n  total: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/hash-field-ttl.dto.ts",
    "content": "import { ApiProperty, PickType } from '@nestjs/swagger';\nimport { IsInt, IsNotEmpty, Max } from 'class-validator';\nimport { MAX_TTL_NUMBER } from 'src/constants';\nimport { HashFieldDto } from 'src/modules/browser/hash/dto/hash-field.dto';\n\nexport class HashFieldTtlDto extends PickType(HashFieldDto, [\n  'field',\n] as const) {\n  @ApiProperty({\n    type: Number,\n    description:\n      'Set a timeout on key in seconds. After the timeout has expired, the field will automatically be deleted. ' +\n      'If the property has value of -1, then the field timeout will be removed.',\n    maximum: MAX_TTL_NUMBER,\n  })\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Max(MAX_TTL_NUMBER)\n  expire: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/hash-field.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsDefined, IsInt, IsOptional, Max, Min } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { MAX_TTL_NUMBER } from 'src/constants';\n\nexport class HashFieldDto {\n  @ApiProperty({\n    description: 'Field',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  field: RedisString;\n\n  @ApiProperty({\n    description: 'Field',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  value: RedisString;\n\n  @ApiPropertyOptional({\n    type: Number,\n    description: 'Set timeout on field in seconds',\n    minimum: 1,\n    maximum: MAX_TTL_NUMBER,\n  })\n  @IsOptional()\n  @IsInt({ always: true })\n  @Min(1)\n  @Max(MAX_TTL_NUMBER)\n  expire?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/index.ts",
    "content": "export * from './hash-field.dto';\nexport * from './hash-field-ttl.dto';\nexport * from './update.hash-fields-ttl.dto';\nexport * from './add.fields-to-hash.dto';\nexport * from './create.hash-with-expire.dto';\nexport * from './delete.fields-from-hash.dto';\nexport * from './delete.fields-from-hash.response';\nexport * from './get.hash-fields.dto';\nexport * from './get.hash-fields.response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/dto/update.hash-fields-ttl.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { HashFieldTtlDto } from 'src/modules/browser/hash/dto';\n\nexport class UpdateHashFieldsTtlDto extends KeyDto {\n  @ApiProperty({\n    description: 'Hash fields',\n    isArray: true,\n    type: HashFieldTtlDto,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested()\n  @Type(() => HashFieldTtlDto)\n  fields: HashFieldTtlDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/hash.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  HttpCode,\n  Patch,\n  Post,\n  Put,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport {\n  AddFieldsToHashDto,\n  CreateHashWithExpireDto,\n  DeleteFieldsFromHashDto,\n  DeleteFieldsFromHashResponse,\n  GetHashFieldsDto,\n  GetHashFieldsResponse,\n  UpdateHashFieldsTtlDto,\n} from 'src/modules/browser/hash/dto';\nimport { HashService } from 'src/modules/browser/hash/hash.service';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: Hash')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('hash')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class HashController extends BrowserBaseController {\n  constructor(private hashService: HashService) {\n    super();\n  }\n\n  @Post('')\n  @ApiOperation({ description: 'Set key to hold Hash data type' })\n  @ApiRedisParams()\n  @ApiBody({ type: CreateHashWithExpireDto })\n  @ApiQueryRedisStringEncoding()\n  async createHash(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateHashWithExpireDto,\n  ): Promise<void> {\n    return await this.hashService.createHash(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/get-fields')\n  @HttpCode(200)\n  @ApiOperation({\n    description:\n      'Get specified fields of the hash stored at key by cursor position',\n  })\n  @ApiRedisParams()\n  @ApiOkResponse({\n    description: 'Specified fields of the hash stored at key.',\n    type: GetHashFieldsResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getMembers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetHashFieldsDto,\n  ): Promise<GetHashFieldsResponse> {\n    return await this.hashService.getFields(clientMetadata, dto);\n  }\n\n  @Put('')\n  @ApiOperation({\n    description: 'Add the specified fields to the Hash stored at key',\n  })\n  @ApiRedisParams()\n  @ApiBody({ type: AddFieldsToHashDto })\n  @ApiQueryRedisStringEncoding()\n  async addMember(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: AddFieldsToHashDto,\n  ): Promise<void> {\n    return await this.hashService.addFields(clientMetadata, dto);\n  }\n\n  @Patch('/ttl')\n  @ApiOperation({\n    description: 'Update hash fields ttl',\n  })\n  @ApiRedisParams()\n  @ApiBody({ type: UpdateHashFieldsTtlDto })\n  @ApiQueryRedisStringEncoding()\n  async updateTtl(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: UpdateHashFieldsTtlDto,\n  ): Promise<void> {\n    return await this.hashService.updateTtl(clientMetadata, dto);\n  }\n\n  @Delete('/fields')\n  @ApiOperation({\n    description: 'Remove the specified fields from the Hash stored at key',\n  })\n  @ApiRedisParams()\n  @ApiBody({ type: DeleteFieldsFromHashDto })\n  @ApiQueryRedisStringEncoding()\n  async deleteFields(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteFieldsFromHashDto,\n  ): Promise<DeleteFieldsFromHashResponse> {\n    return await this.hashService.deleteFields(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/hash.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { HashService } from 'src/modules/browser/hash/hash.service';\nimport { HashController } from 'src/modules/browser/hash/hash.controller';\n\n@Module({})\nexport class HashModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: HashModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: HashModule,\n          },\n        ]),\n      ],\n      controllers: [HashController],\n      providers: [HashService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/hash.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  ConflictException,\n  ForbiddenException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { when } from 'jest-when';\nimport { flatMap } from 'lodash';\nimport { ReplyError } from 'src/models/redis-client';\nimport {\n  mockRedisNoPermError,\n  mockRedisWrongTypeError,\n  mockBrowserClientMetadata,\n  mockDatabaseRecommendationService,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { GetHashFieldsDto, HashFieldDto } from 'src/modules/browser/hash/dto';\nimport {\n  BrowserToolHashCommands,\n  BrowserToolKeysCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  mockAddFieldsDto,\n  mockAddFieldsWithExpirationDto,\n  mockCreateHashWithExpireAndFieldsExpireDto,\n  mockCreateHashWithExpireDto,\n  mockDeleteFieldsDto,\n  mockGetFieldsDto,\n  mockGetFieldsResponse,\n  mockGetFieldsWithTtlResponse,\n  mockHashField,\n  mockHashFieldTtlDto,\n  mockHashFieldTtlDto2,\n  mockHashFieldTtlDto3,\n  mockHashFieldWithExpire,\n  mockHashFieldWithExpire2,\n  mockRedisHScanResponse,\n  mockRedisHScanWithFieldsExpireResponse,\n  mockRedisHTtlResponse,\n  mockUpdateHashFieldsTtlDto,\n} from 'src/modules/browser/__mocks__';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { HashService } from 'src/modules/browser/hash/hash.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisFeature } from 'src/modules/redis/client';\nimport apiConfig, { Config } from 'src/utils/config';\n\nconst REDIS_SCAN_CONFIG = apiConfig.get('redis_scan') as Config['redis_scan'];\n\ndescribe('HashService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: HashService;\n  let recommendationService;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        HashService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<HashService>(HashService);\n    recommendationService = module.get<DatabaseRecommendationService>(\n      DatabaseRecommendationService,\n    );\n    client.sendPipeline = jest.fn().mockResolvedValue([[null, '1']]);\n  });\n\n  describe('createHash', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddFieldsDto.keyName])\n        .mockResolvedValue(false);\n    });\n    it('create hash with expiration', async () => {\n      expect(\n        await service.createHash(\n          mockBrowserClientMetadata,\n          mockCreateHashWithExpireDto,\n        ),\n      ).toEqual(undefined);\n      expect(mockStandaloneRedisClient.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HSet,\n          mockCreateHashWithExpireDto.keyName,\n          mockHashField.field,\n          mockHashField.value,\n        ],\n        [\n          BrowserToolKeysCommands.Expire,\n          mockCreateHashWithExpireDto.keyName,\n          mockCreateHashWithExpireDto.expire,\n        ],\n      ]);\n      expect(mockStandaloneRedisClient.isFeatureSupported).toHaveBeenCalledWith(\n        RedisFeature.HashFieldsExpiration,\n      );\n    });\n    it('create hash with expiration but without fields exp since no expire was provided for fields', async () => {\n      mockStandaloneRedisClient.isFeatureSupported.mockResolvedValueOnce(true);\n\n      expect(\n        await service.createHash(\n          mockBrowserClientMetadata,\n          mockCreateHashWithExpireDto,\n        ),\n      ).toEqual(undefined);\n      expect(mockStandaloneRedisClient.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HSet,\n          mockCreateHashWithExpireDto.keyName,\n          mockHashField.field,\n          mockHashField.value,\n        ],\n        [\n          BrowserToolKeysCommands.Expire,\n          mockCreateHashWithExpireDto.keyName,\n          mockCreateHashWithExpireDto.expire,\n        ],\n      ]);\n      expect(mockStandaloneRedisClient.isFeatureSupported).toHaveBeenCalledWith(\n        RedisFeature.HashFieldsExpiration,\n      );\n    });\n    it('create hash with expiration and fields expiration', async () => {\n      mockStandaloneRedisClient.isFeatureSupported.mockResolvedValueOnce(true);\n\n      expect(\n        await service.createHash(\n          mockBrowserClientMetadata,\n          mockCreateHashWithExpireAndFieldsExpireDto,\n        ),\n      ).toEqual(undefined);\n      expect(mockStandaloneRedisClient.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HSet,\n          mockCreateHashWithExpireAndFieldsExpireDto.keyName,\n          mockHashField.field,\n          mockHashField.value,\n          mockHashFieldWithExpire.field,\n          mockHashFieldWithExpire.value,\n          mockHashFieldWithExpire2.field,\n          mockHashFieldWithExpire2.value,\n        ],\n        [\n          BrowserToolKeysCommands.Expire,\n          mockCreateHashWithExpireAndFieldsExpireDto.keyName,\n          mockCreateHashWithExpireAndFieldsExpireDto.expire,\n        ],\n        [\n          BrowserToolHashCommands.HExpire,\n          mockCreateHashWithExpireAndFieldsExpireDto.keyName,\n          mockHashFieldWithExpire.expire,\n          'fields',\n          '1',\n          mockHashFieldWithExpire.field,\n        ],\n        [\n          BrowserToolHashCommands.HExpire,\n          mockCreateHashWithExpireAndFieldsExpireDto.keyName,\n          mockHashFieldWithExpire2.expire,\n          'fields',\n          '1',\n          mockHashFieldWithExpire2.field,\n        ],\n      ]);\n      expect(mockStandaloneRedisClient.isFeatureSupported).toHaveBeenCalledWith(\n        RedisFeature.HashFieldsExpiration,\n      );\n    });\n    it('create hash without expiration', async () => {\n      const { keyName, fields } = mockAddFieldsDto;\n      const commandArgs = flatMap(fields, ({ field, value }: HashFieldDto) => [\n        field,\n        value,\n      ]);\n\n      when(client.sendCommand)\n        .calledWith([BrowserToolHashCommands.HSet, keyName, ...commandArgs])\n        .mockResolvedValue(1);\n\n      expect(\n        await service.createHash(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).toEqual(undefined);\n      expect(mockStandaloneRedisClient.sendPipeline).toHaveBeenCalledWith([\n        [BrowserToolHashCommands.HSet, keyName, ...commandArgs],\n      ]);\n    });\n    it('key with this name exist', async () => {\n      const { keyName, fields } = mockAddFieldsDto;\n      const args = flatMap(fields, ({ field, value }: HashFieldDto) => [\n        field,\n        value,\n      ]);\n\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, keyName])\n        .mockResolvedValue(true);\n\n      await expect(\n        service.createHash(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).rejects.toThrow(ConflictException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HSet,\n        keyName,\n        ...args,\n      ]);\n    });\n    it(\"user don't have required permissions for createHash\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'HSET',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.createHash(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('getFields', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolHashCommands.HLen, mockAddFieldsDto.keyName])\n        .mockResolvedValue(mockAddFieldsDto.fields.length);\n    });\n    it('succeed to get fields of the hash', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHScanResponse);\n\n      const result = await service.getFields(\n        mockBrowserClientMetadata,\n        mockGetFieldsDto,\n      );\n      expect(result).toEqual(mockGetFieldsResponse);\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        expect.arrayContaining([\n          BrowserToolHashCommands.HScan,\n          expect.anything(),\n        ]),\n      );\n    });\n    it('succeed to get fields of the hash with ttls', async () => {\n      mockStandaloneRedisClient.isFeatureSupported.mockResolvedValueOnce(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHScanWithFieldsExpireResponse);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HLen,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockGetFieldsWithTtlResponse.total);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HTtl,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHTtlResponse);\n\n      const result = await service.getFields(\n        mockBrowserClientMetadata,\n        mockGetFieldsDto,\n      );\n      expect(result).toEqual(mockGetFieldsWithTtlResponse);\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        expect.arrayContaining([\n          BrowserToolHashCommands.HScan,\n          expect.anything(),\n        ]),\n      );\n    });\n    it('should not fail in case of ttl query error and return results without ttl field', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'httl',\n      };\n      mockStandaloneRedisClient.isFeatureSupported.mockResolvedValueOnce(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHScanWithFieldsExpireResponse);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HLen,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockGetFieldsWithTtlResponse.total);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HTtl,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValueOnce(replyError);\n\n      const result = await service.getFields(\n        mockBrowserClientMetadata,\n        mockGetFieldsDto,\n      );\n      expect(result).toEqual({\n        ...mockGetFieldsWithTtlResponse,\n        fields: mockGetFieldsWithTtlResponse.fields.map((field) => ({\n          ...field,\n          expire: undefined,\n        })),\n      });\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        expect.arrayContaining([\n          BrowserToolHashCommands.HScan,\n          expect.anything(),\n        ]),\n      );\n    });\n    it('succeed to find exact field in the hash', async () => {\n      const item = mockAddFieldsDto.fields[0];\n      const dto: GetHashFieldsDto = {\n        ...mockGetFieldsDto,\n        match: item.field.toString(),\n      };\n      when(client.sendCommand)\n        .calledWith([BrowserToolHashCommands.HGet, dto.keyName, dto.match])\n        .mockResolvedValue(item.value);\n\n      const result = await service.getFields(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual(mockGetFieldsResponse);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HScan,\n        expect.anything(),\n      ]);\n    });\n    it('failed to find exact field in the hash', async () => {\n      const dto: GetHashFieldsDto = {\n        ...mockGetFieldsDto,\n        match: 'field',\n      };\n      when(client.sendCommand)\n        .calledWith([BrowserToolHashCommands.HGet, dto.keyName, dto.match])\n        .mockResolvedValue(null);\n\n      const result = await service.getFields(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual({ ...mockGetFieldsResponse, fields: [] });\n    });\n    it('should not call scan when math contains escaped glob', async () => {\n      const item = {\n        field: Buffer.from('fi[a-e]ld'),\n        value: Buffer.from('value'),\n      };\n      const dto: GetHashFieldsDto = {\n        ...mockGetFieldsDto,\n        match: 'fi\\\\[a-e\\\\]ld',\n      };\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolHashCommands.HGet,\n          dto.keyName,\n          item.field.toString(),\n        ])\n        .mockResolvedValue('value');\n\n      const result = await service.getFields(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual({ ...mockGetFieldsResponse, fields: [item] });\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HScan,\n        expect.anything(),\n      ]);\n    });\n    // TODO: uncomment after enabling threshold for hash scan\n    // it('should stop hash full scan', async () => {\n    //   const dto: GetHashFieldsDto = {\n    //     ...mockGetFieldsDto,\n    //     count: REDIS_SCAN_CONFIG.countDefault,\n    //     match: '*un-exist-field*',\n    //   };\n    //   const maxScanCalls = Math.round(\n    //     REDIS_SCAN_CONFIG.countThreshold / REDIS_SCAN_CONFIG.countDefault,\n    //   );\n    //   when(browserTool.execCommand)\n    //     .calledWith(\n    //       mockBrowserClientMetadata,\n    //       BrowserToolHashCommands.HScan,\n    //       expect.anything(),\n    //     )\n    //     .mockResolvedValue(['200', []]);\n    //\n    //   await service.getFields(mockBrowserClientMetadata, dto);\n    //\n    //   expect(browserTool.execCommand).toHaveBeenCalledTimes(maxScanCalls + 1);\n    // });\n    it('key with this name does not exist for getFields', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolHashCommands.HLen, mockGetFieldsDto.keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.getFields(mockBrowserClientMetadata, mockGetFieldsDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"try to use 'HLEN' command not for hash data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'HLEN',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getFields(mockBrowserClientMetadata, mockGetFieldsDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for getFields\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'HLEN',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getFields(mockBrowserClientMetadata, mockGetFieldsDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('should call recommendationService', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHScanResponse);\n\n      const result = await service.getFields(\n        mockBrowserClientMetadata,\n        mockGetFieldsDto,\n      );\n      expect(result).toEqual(mockGetFieldsResponse);\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        expect.arrayContaining([\n          BrowserToolHashCommands.HScan,\n          expect.anything(),\n        ]),\n      );\n\n      expect(recommendationService.check).toBeCalledWith(\n        mockBrowserClientMetadata,\n        RECOMMENDATION_NAMES.BIG_HASHES,\n        { total: result.total, keyName: result.keyName },\n      );\n\n      expect(recommendationService.check).toBeCalledTimes(1);\n    });\n  });\n\n  describe('scanHash', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolHashCommands.HLen, mockAddFieldsDto.keyName])\n        .mockResolvedValue(mockAddFieldsDto.fields.length);\n    });\n    it('should scan with match=\"*\" by default and default count', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHScanResponse);\n\n      const result = await service.scanHash(mockStandaloneRedisClient, {\n        keyName: mockGetFieldsDto.keyName,\n        cursor: 0,\n      });\n      expect(result).toEqual({ ...mockGetFieldsResponse, total: undefined });\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolHashCommands.HScan,\n        mockGetFieldsDto.keyName,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        REDIS_SCAN_CONFIG.countDefault,\n      ]);\n    });\n    it('should scan with passed arguments', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockRedisHScanResponse);\n\n      const result = await service.scanHash(\n        mockStandaloneRedisClient,\n        mockGetFieldsDto,\n      );\n      expect(result).toEqual({ ...mockGetFieldsResponse, total: undefined });\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolHashCommands.HScan,\n        mockGetFieldsDto.keyName,\n        '0',\n        'MATCH',\n        mockGetFieldsDto.match,\n        'COUNT',\n        mockGetFieldsDto.count,\n      ]);\n    });\n  });\n\n  describe('addFields', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddFieldsDto.keyName])\n        .mockResolvedValue(true);\n    });\n    it('succeed to add/update fields to the Hash data type', async () => {\n      expect(\n        await service.addFields(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).toEqual(undefined);\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolKeysCommands.Exists,\n        mockAddFieldsDto.keyName,\n      ]);\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HSet,\n          mockAddFieldsDto.keyName,\n          mockHashField.field,\n          mockHashField.value,\n        ],\n      ]);\n    });\n    it('succeed add/update fields to the Hash data type without expiration fields when feature disabled', async () => {\n      expect(\n        await service.addFields(\n          mockBrowserClientMetadata,\n          mockAddFieldsWithExpirationDto,\n        ),\n      ).toEqual(undefined);\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolKeysCommands.Exists,\n        mockAddFieldsDto.keyName,\n      ]);\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HSet,\n          mockAddFieldsDto.keyName,\n          mockHashField.field,\n          mockHashField.value,\n          mockHashFieldWithExpire.field,\n          mockHashFieldWithExpire.value,\n          mockHashFieldWithExpire2.field,\n          mockHashFieldWithExpire2.value,\n        ],\n      ]);\n    });\n    it('succeed to add/update fields to the Hash data type with fields expiration', async () => {\n      mockStandaloneRedisClient.isFeatureSupported.mockResolvedValueOnce(true);\n      expect(\n        await service.addFields(\n          mockBrowserClientMetadata,\n          mockAddFieldsWithExpirationDto,\n        ),\n      ).toEqual(undefined);\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolKeysCommands.Exists,\n        mockAddFieldsWithExpirationDto.keyName,\n      ]);\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HSet,\n          mockAddFieldsWithExpirationDto.keyName,\n          mockHashField.field,\n          mockHashField.value,\n          mockHashFieldWithExpire.field,\n          mockHashFieldWithExpire.value,\n          mockHashFieldWithExpire2.field,\n          mockHashFieldWithExpire2.value,\n        ],\n        [\n          BrowserToolHashCommands.HExpire,\n          mockAddFieldsWithExpirationDto.keyName,\n          mockHashFieldWithExpire.expire,\n          'fields',\n          '1',\n          mockHashFieldWithExpire.field,\n        ],\n        [\n          BrowserToolHashCommands.HExpire,\n          mockAddFieldsWithExpirationDto.keyName,\n          mockHashFieldWithExpire2.expire,\n          'fields',\n          '1',\n          mockHashFieldWithExpire2.field,\n        ],\n      ]);\n    });\n    it('key with this name does not exist for addFields', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddFieldsDto.keyName])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.addFields(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HSet,\n        expect.anything(),\n      ]);\n    });\n    it(\"try to use 'HSET' command not for hash data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'HSET',\n      };\n      client.sendPipeline.mockRejectedValue(replyError);\n\n      await expect(\n        service.addFields(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for addFields\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'HSET',\n      };\n      client.sendPipeline.mockRejectedValue(replyError);\n\n      await expect(\n        service.addFields(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('updateTtl', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockUpdateHashFieldsTtlDto.keyName,\n        ])\n        .mockResolvedValue(true);\n    });\n    it('should update ttl for 2 fields and persist one', async () => {\n      expect(\n        await service.updateTtl(\n          mockBrowserClientMetadata,\n          mockUpdateHashFieldsTtlDto,\n        ),\n      ).toEqual(undefined);\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolKeysCommands.Exists,\n        mockUpdateHashFieldsTtlDto.keyName,\n      ]);\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolHashCommands.HPersist,\n          mockUpdateHashFieldsTtlDto.keyName,\n          'fields',\n          '1',\n          mockHashFieldTtlDto.field,\n        ],\n        [\n          BrowserToolHashCommands.HExpire,\n          mockUpdateHashFieldsTtlDto.keyName,\n          mockHashFieldTtlDto2.expire,\n          'fields',\n          '1',\n          mockHashFieldTtlDto2.field,\n        ],\n        [\n          BrowserToolHashCommands.HExpire,\n          mockUpdateHashFieldsTtlDto.keyName,\n          mockHashFieldTtlDto3.expire,\n          'fields',\n          '1',\n          mockHashFieldTtlDto3.field,\n        ],\n      ]);\n    });\n    it('key with this name does not exist for addFields', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockUpdateHashFieldsTtlDto.keyName,\n        ])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.updateTtl(\n          mockBrowserClientMetadata,\n          mockUpdateHashFieldsTtlDto,\n        ),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HExpire,\n        expect.anything(),\n      ]);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HPersist,\n        expect.anything(),\n      ]);\n    });\n    it(\"try to use 'HEXPIRE' command not for hash data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'HEXPIRE',\n      };\n      client.sendPipeline.mockResolvedValueOnce([[replyError, null]]);\n\n      await expect(\n        service.updateTtl(\n          mockBrowserClientMetadata,\n          mockUpdateHashFieldsTtlDto,\n        ),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for addFields\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'HEXPIRE',\n      };\n      client.sendPipeline.mockResolvedValueOnce([[replyError, null]]);\n\n      await expect(\n        service.addFields(mockBrowserClientMetadata, mockAddFieldsDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('deleteFields', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockDeleteFieldsDto.keyName,\n        ])\n        .mockResolvedValue(true);\n    });\n    it('succeeded to delete fields from Hash data type', async () => {\n      const { fields } = mockDeleteFieldsDto;\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolHashCommands.HDel,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(fields.length);\n\n      const result = await service.deleteFields(\n        mockBrowserClientMetadata,\n        mockDeleteFieldsDto,\n      );\n\n      expect(result).toEqual({ affected: fields.length });\n    });\n    it('key with this name does not exist for deleteFields', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockDeleteFieldsDto.keyName,\n        ])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.deleteFields(mockBrowserClientMetadata, mockDeleteFieldsDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolHashCommands.HDel,\n        expect.anything(),\n      ]);\n    });\n    it(\"try to use 'HDEL' command not for Hash data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'HDEL',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteFields(mockBrowserClientMetadata, mockDeleteFieldsDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for deleteFields\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'HDEL',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteFields(mockBrowserClientMetadata, mockDeleteFieldsDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/hash/hash.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { chunk, flatMap, isNull } from 'lodash';\nimport {\n  catchAclError,\n  catchMultiTransactionError,\n  isRedisGlob,\n  unescapeRedisGlob,\n} from 'src/utils';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RECOMMENDATION_NAMES, RedisErrorCodes } from 'src/constants';\nimport config, { Config } from 'src/utils/config';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  BrowserToolHashCommands,\n  BrowserToolKeysCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport {\n  AddFieldsToHashDto,\n  CreateHashWithExpireDto,\n  DeleteFieldsFromHashDto,\n  DeleteFieldsFromHashResponse,\n  GetHashFieldsDto,\n  GetHashFieldsResponse,\n  HashFieldDto,\n  HashScanResponse,\n  UpdateHashFieldsTtlDto,\n} from 'src/modules/browser/hash/dto';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  RedisClient,\n  RedisClientCommand,\n  RedisFeature,\n} from 'src/modules/redis/client';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\nimport { RedisString } from 'src/common/constants';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan') as Config['redis_scan'];\n\n@Injectable()\nexport class HashService {\n  private logger = new Logger('HashService');\n\n  constructor(\n    private databaseClientFactory: DatabaseClientFactory,\n    private recommendationService: DatabaseRecommendationService,\n  ) {}\n\n  static getFieldExpireCommands(keyName: RedisString, fields: HashFieldDto[]) {\n    return fields\n      .filter(({ expire }) => expire)\n      .map(\n        (field) =>\n          [\n            BrowserToolHashCommands.HExpire,\n            keyName,\n            field.expire,\n            'fields',\n            '1',\n            field.field,\n          ] as RedisClientCommand,\n      );\n  }\n\n  public async createHash(\n    clientMetadata: ClientMetadata,\n    dto: CreateHashWithExpireDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating Hash data type.', clientMetadata);\n      const { keyName, fields, expire } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyExists(keyName, client);\n\n      const args = flatMap(fields, ({ field, value }: HashFieldDto) => [\n        field,\n        value,\n      ]);\n\n      const commands = [\n        [BrowserToolHashCommands.HSet, keyName, ...args] as RedisClientCommand,\n      ];\n\n      if (expire) {\n        commands.push([\n          BrowserToolKeysCommands.Expire,\n          keyName,\n          expire,\n        ] as RedisClientCommand);\n      }\n\n      if (await client.isFeatureSupported(RedisFeature.HashFieldsExpiration)) {\n        commands.push(...HashService.getFieldExpireCommands(keyName, fields));\n      }\n\n      const transactionResults = await client.sendPipeline(commands);\n      // todo: rethink\n      catchMultiTransactionError(transactionResults);\n\n      this.logger.debug('Succeed to create Hash data type.', clientMetadata);\n    } catch (error) {\n      this.logger.error(\n        'Failed to create Hash data type.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  public async getFields(\n    clientMetadata: ClientMetadata,\n    dto: GetHashFieldsDto,\n  ): Promise<GetHashFieldsResponse> {\n    try {\n      this.logger.debug(\n        'Getting fields of the Hash data type stored at key.',\n        clientMetadata,\n      );\n      const { keyName, cursor, match } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      let result: GetHashFieldsResponse = {\n        keyName,\n        total: 0,\n        fields: [],\n        nextCursor: cursor,\n      };\n\n      result.total = (await client.sendCommand([\n        BrowserToolHashCommands.HLen,\n        keyName,\n      ])) as number;\n      if (!result.total) {\n        this.logger.error(\n          `Failed to get fields of the Hash data type. Not Found key: ${keyName}.`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n      if (match && !isRedisGlob(match)) {\n        const field = unescapeRedisGlob(match);\n        result.nextCursor = 0;\n        const value = await client.sendCommand([\n          BrowserToolHashCommands.HGet,\n          keyName,\n          field,\n        ]);\n        if (!isNull(value)) {\n          result.fields.push(plainToInstance(HashFieldDto, { field, value }));\n        }\n      } else {\n        const scanResult = await this.scanHash(client, dto);\n        result = { ...result, ...scanResult };\n      }\n\n      try {\n        if (\n          await client.isFeatureSupported(RedisFeature.HashFieldsExpiration)\n        ) {\n          const ttls = (await client.sendCommand([\n            BrowserToolHashCommands.HTtl,\n            result.keyName,\n            'fields',\n            result.fields.length,\n            ...result.fields.map(({ field }) => field),\n          ])) as string[];\n\n          ttls.forEach((ttl, index) => {\n            result.fields[index].expire = +ttl;\n          });\n        }\n      } catch (e) {\n        this.logger.warn(\n          'Unable to get ttl for hash fields',\n          e,\n          clientMetadata,\n        );\n        // ignore error\n      }\n\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.BIG_HASHES,\n        { total: result.total, keyName },\n      );\n\n      this.logger.debug(\n        'Succeed to get fields of the Hash data type.',\n        clientMetadata,\n      );\n      return plainToInstance(GetHashFieldsResponse, result);\n    } catch (error) {\n      this.logger.error(\n        'Failed to get fields of the Hash data type.',\n        error,\n        clientMetadata,\n      );\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async addFields(\n    clientMetadata: ClientMetadata,\n    dto: AddFieldsToHashDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Adding fields to the Hash data type.', clientMetadata);\n      const { keyName, fields } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const args = flatMap(fields, ({ field, value }: HashFieldDto) => [\n        field,\n        value,\n      ]);\n\n      const commands = [\n        [BrowserToolHashCommands.HSet, keyName, ...args] as RedisClientCommand,\n      ];\n\n      if (await client.isFeatureSupported(RedisFeature.HashFieldsExpiration)) {\n        commands.push(...HashService.getFieldExpireCommands(keyName, fields));\n      }\n\n      const transactionResults = await client.sendPipeline(commands);\n      // todo: rethink\n      catchMultiTransactionError(transactionResults);\n\n      this.logger.debug(\n        'Succeed to add fields to Hash data type.',\n        clientMetadata,\n      );\n    } catch (error) {\n      this.logger.error(\n        'Failed to add fields to Hash data type.',\n        error,\n        clientMetadata,\n      );\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async updateTtl(\n    clientMetadata: ClientMetadata,\n    dto: UpdateHashFieldsTtlDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Updating hash fields ttl.', clientMetadata);\n      const { keyName, fields } = dto;\n\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const commands = [];\n\n      fields.forEach(({ field, expire }) => {\n        if (expire === -1) {\n          commands.push([\n            BrowserToolHashCommands.HPersist,\n            keyName,\n            'fields',\n            '1',\n            field,\n          ]);\n        } else {\n          commands.push([\n            BrowserToolHashCommands.HExpire,\n            keyName,\n            expire,\n            'fields',\n            '1',\n            field,\n          ]);\n        }\n      });\n\n      if (commands.length) {\n        const transactionResults = await client.sendPipeline(commands);\n        // todo: rethink\n        catchMultiTransactionError(transactionResults);\n      }\n\n      this.logger.debug('Successfully updated hash fields ttl', clientMetadata);\n    } catch (error) {\n      this.logger.error(\n        'Failed to update hash fields ttl.',\n        error,\n        clientMetadata,\n      );\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async deleteFields(\n    clientMetadata: ClientMetadata,\n    dto: DeleteFieldsFromHashDto,\n  ): Promise<DeleteFieldsFromHashResponse> {\n    try {\n      this.logger.debug(\n        'Deleting fields from the Hash data type.',\n        clientMetadata,\n      );\n      const { keyName, fields } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = (await client.sendCommand([\n        BrowserToolHashCommands.HDel,\n        keyName,\n        ...fields,\n      ])) as number;\n\n      this.logger.debug(\n        'Succeed to delete fields from the Hash data type.',\n        clientMetadata,\n      );\n      return { affected: result };\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete fields from the Hash data type.',\n        error,\n        clientMetadata,\n      );\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async scanHash(\n    client: RedisClient,\n    dto: GetHashFieldsDto,\n  ): Promise<HashScanResponse> {\n    const { keyName, cursor } = dto;\n    const count = dto.count || REDIS_SCAN_CONFIG.countDefault;\n    const match = dto.match !== undefined ? dto.match : '*';\n    let result: HashScanResponse = {\n      keyName,\n      nextCursor: null,\n      fields: [],\n    };\n    while (result.nextCursor !== 0 && result.fields.length < count) {\n      const scanResult = await client.sendCommand([\n        BrowserToolHashCommands.HScan,\n        keyName,\n        `${result.nextCursor || cursor}`,\n        'MATCH',\n        match,\n        'COUNT',\n        count,\n      ]);\n      const nextCursor = scanResult[0];\n      const fieldsArray = scanResult[1];\n      const fields: HashFieldDto[] = chunk(fieldsArray, 2).map(\n        ([field, value]: string[]) =>\n          plainToInstance(HashFieldDto, { field, value }),\n      );\n      result = {\n        ...result,\n        nextCursor: parseInt(nextCursor, 10),\n        fields: [...result.fields, ...fields],\n      };\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/delete.keys.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class DeleteKeysDto {\n  @ApiProperty({\n    description: 'Key name',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @RedisStringType({ each: true })\n  @IsRedisString({ each: true })\n  keyNames: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/delete.keys.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteKeysResponse {\n  @ApiProperty({\n    description: 'Number of affected keys',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/get.keys-info.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsDefined, IsEnum, IsOptional } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { KeyDto, RedisDataType } from './key.dto';\n\nexport class GetKeyInfoDto extends KeyDto {\n  @ApiPropertyOptional({\n    description:\n      'Flag to determine if size should be requested and shown in the response',\n    type: Boolean,\n    default: false,\n  })\n  @IsOptional()\n  includeSize?: boolean;\n}\n\nexport class GetKeysInfoDto {\n  @ApiProperty({\n    description: 'List of keys',\n    type: String,\n    isArray: true,\n    example: ['keys', 'key2'],\n  })\n  @IsDefined()\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  keys: RedisString[];\n\n  @ApiPropertyOptional({\n    description:\n      'Iterate through the database looking for keys of a specific type.',\n    enum: RedisDataType,\n    example: RedisDataType.Hash,\n  })\n  @IsEnum(RedisDataType, {\n    message: `destination must be a valid enum value. Valid values: ${Object.values(\n      RedisDataType,\n    )}.`,\n  })\n  @IsOptional()\n  type?: RedisDataType;\n\n  @ApiPropertyOptional({\n    description:\n      'Flag to determine if keys should be requested and shown in the response',\n    type: Boolean,\n    default: true,\n  })\n  @IsOptional()\n  includeSize?: boolean;\n\n  @ApiPropertyOptional({\n    description:\n      'Flag to determine if TTL should be requested and shown in the response',\n    type: Boolean,\n    default: true,\n  })\n  @IsOptional()\n  includeTTL?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/get.keys-info.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class GetKeyInfoResponse {\n  @ApiProperty({\n    type: String,\n  })\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    type: String,\n  })\n  type?: string;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The remaining time to live of a key.' +\n      ' If the property has value of -1, then the key has no expiration time (no limit).',\n  })\n  ttl?: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The number of bytes that a key and its value require to be stored in RAM.',\n  })\n  size?: number;\n\n  @ApiPropertyOptional({\n    type: Number,\n    description: 'The length of the value stored in a key.',\n  })\n  length?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/get.keys-with-details.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsArray } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { GetKeyInfoResponse } from './get.keys-info.response';\n\nexport class GetKeysWithDetailsResponse {\n  @ApiProperty({\n    type: Number,\n    default: 0,\n    description:\n      'The new cursor to use in the next call.' +\n      ' If the property has value of 0, then the iteration is completed.',\n  })\n  cursor: number;\n\n  @ApiProperty({\n    type: Number,\n    description: 'The number of keys in the currently-selected database.',\n  })\n  total: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The number of keys we tried to scan. Be aware that ' +\n      'scanned is sum of COUNT parameters from redis commands',\n  })\n  scanned: number;\n\n  @ApiProperty({\n    type: () => GetKeyInfoResponse,\n    description: 'Array of Keys.',\n    isArray: true,\n  })\n  @IsArray()\n  @Type(() => GetKeyInfoResponse)\n  keys: GetKeyInfoResponse[];\n\n  @ApiPropertyOptional({\n    type: String,\n    description: 'Node host. In case when we are working with cluster',\n  })\n  host?: string;\n\n  @ApiPropertyOptional({\n    type: Number,\n    description: 'Node port. In case when we are working with cluster',\n  })\n  port?: number;\n\n  @ApiPropertyOptional({\n    type: Number,\n    description:\n      'The maximum number of results.' +\n      ' For RediSearch this number is a value from \"FT.CONFIG GET maxsearchresults\" command.',\n  })\n  maxResults?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/get.keys.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Transform, Type } from 'class-transformer';\nimport {\n  IsBoolean,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  Min,\n  Max,\n} from 'class-validator';\nimport config, { Config } from 'src/utils/config';\nimport { RedisDataType } from './key.dto';\n\nconst scanConfig = config.get('redis_scan') as Config['redis_scan'];\nconst { scanThreshold, scanThresholdMax } = scanConfig;\n\nexport class GetKeysDto {\n  @ApiProperty({\n    description:\n      'Iteration cursor. ' +\n      'An iteration starts when the cursor is set to 0, and terminates when the cursor returned by the server is 0.',\n    type: String,\n    default: '0',\n  })\n  @Type(() => String)\n  @IsNotEmpty()\n  cursor: string;\n\n  @ApiPropertyOptional({\n    description: 'Specifying the number of elements to return.',\n    type: Number,\n    minimum: 1,\n    default: 15,\n  })\n  @IsInt()\n  @Min(1)\n  @Type(() => Number)\n  @IsNotEmpty()\n  @IsOptional()\n  count?: number;\n\n  @ApiPropertyOptional({\n    description: 'Iterate only elements matching a given pattern.',\n    type: String,\n    default: '*',\n  })\n  @IsString()\n  @IsOptional()\n  match?: string;\n\n  @ApiPropertyOptional({\n    description:\n      'Iterate through the database looking for keys of a specific type.',\n    enum: RedisDataType,\n  })\n  @IsEnum(RedisDataType, {\n    message: `destination must be a valid enum value. Valid values: ${Object.values(\n      RedisDataType,\n    )}.`,\n  })\n  @IsOptional()\n  type?: RedisDataType;\n\n  @ApiPropertyOptional({\n    description: 'Fetch keys info (type, size, ttl, length)',\n    type: Boolean,\n    default: true,\n  })\n  @IsBoolean()\n  @IsOptional()\n  @Transform(({ value }) => value === true || value === 'true')\n  keysInfo?: boolean = true;\n\n  @ApiPropertyOptional({\n    description: 'The maximum number of keys to scan',\n    type: Number,\n    default: true,\n  })\n  @IsOptional()\n  @IsInt()\n  @Max(scanThresholdMax)\n  scanThreshold: number = scanThreshold;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/get.namespace-searchable.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsDefined, IsString } from 'class-validator';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class GetNamespaceSearchableDto {\n  @ApiProperty({\n    description: 'List of namespace prefixes to check for searchable keys',\n    type: [String],\n    example: ['user:', 'session:'],\n  })\n  @IsDefined()\n  @IsString({ each: true })\n  @ArrayNotEmpty()\n  prefixes: string[];\n}\n\nexport class NamespaceSearchableKeyResponse {\n  @ApiProperty({\n    description: 'Key name',\n    type: String,\n  })\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    description: 'Key type (hash or ReJSON-RL)',\n    type: String,\n  })\n  type: string;\n}\n\nexport class NamespaceSearchableResponse {\n  @ApiProperty({\n    description: 'Namespace prefix',\n    type: String,\n  })\n  prefix: string;\n\n  @ApiPropertyOptional({\n    description: 'First searchable key found in the namespace, if any',\n    type: NamespaceSearchableKeyResponse,\n  })\n  key?: NamespaceSearchableKeyResponse;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/index.ts",
    "content": "export * from './delete.keys.dto';\nexport * from './delete.keys.response';\nexport * from './get.keys.dto';\nexport * from './get.keys-info.dto';\nexport * from './get.keys-info.response';\nexport * from './get.keys-with-details.response';\nexport * from './key.dto';\nexport * from './key-with-expire.dto';\nexport * from './rename.key.dto';\nexport * from './rename.key.response';\nexport * from './scan-data-type.dto';\nexport * from './update.key-ttl.dto';\nexport * from './update.key-ttl.response';\nexport * from './get.namespace-searchable.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/key-with-expire.dto.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { MAX_TTL_NUMBER } from 'src/constants';\nimport { IsInt, IsOptional, Max, Min } from 'class-validator';\nimport { KeyDto } from './key.dto';\n\nexport class KeyWithExpireDto extends KeyDto {\n  @ApiPropertyOptional({\n    type: Number,\n    description:\n      'Set a timeout on key in seconds. After the timeout has expired, the key will automatically be deleted.',\n    minimum: 1,\n    maximum: MAX_TTL_NUMBER,\n  })\n  @IsOptional()\n  @IsInt({ always: true })\n  @Min(1)\n  @Max(MAX_TTL_NUMBER)\n  expire?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/key.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport enum RedisDataType {\n  String = 'string',\n  Hash = 'hash',\n  List = 'list',\n  Set = 'set',\n  ZSet = 'zset',\n  Stream = 'stream',\n  JSON = 'ReJSON-RL',\n  Graph = 'graphdata',\n  TS = 'TSDB-TYPE',\n}\n\nexport class KeyDto {\n  @ApiProperty({\n    description: 'Key Name',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  keyName: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/rename.key.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { KeyDto } from './key.dto';\n\nexport class RenameKeyDto extends KeyDto {\n  @ApiProperty({\n    description: 'New key name',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  newKeyName: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/rename.key.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class KeyResponse {\n  @ApiProperty({\n    description: 'Key Name',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  keyName: RedisString;\n}\n\nexport class RenameKeyResponse extends KeyResponse {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/scan-data-type.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { KeyDto } from './key.dto';\n\nexport class ScanDataTypeDto extends KeyDto {\n  @ApiProperty({\n    description:\n      'Iteration cursor. ' +\n      'An iteration starts when the cursor is set to 0, and terminates when the cursor returned by the server is 0.',\n    type: Number,\n    minimum: 0,\n    default: 0,\n  })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  @IsNotEmpty()\n  cursor: number;\n\n  @ApiPropertyOptional({\n    description: 'Specifying the number of elements to return.',\n    type: Number,\n    minimum: 1,\n    default: 15,\n  })\n  @IsInt()\n  @Min(1)\n  @Type(() => Number)\n  @IsNotEmpty()\n  @IsOptional()\n  count?: number;\n\n  @ApiPropertyOptional({\n    description: 'Iterate only elements matching a given pattern.',\n    type: String,\n    default: '*',\n  })\n  @IsOptional()\n  @IsString()\n  match?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/update.key-ttl.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { MAX_TTL_NUMBER } from 'src/constants';\nimport { IsInt, IsNotEmpty, Max } from 'class-validator';\nimport { KeyDto } from './key.dto';\n\nexport class UpdateKeyTtlDto extends KeyDto {\n  @ApiProperty({\n    type: Number,\n    description:\n      'Set a timeout on key in seconds. After the timeout has expired, the key will automatically be deleted. ' +\n      'If the property has value of -1, then the key timeout will be removed.',\n    maximum: MAX_TTL_NUMBER,\n  })\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Max(MAX_TTL_NUMBER)\n  ttl: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/dto/update.key-ttl.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { MAX_TTL_NUMBER } from 'src/constants';\n\nexport class KeyTtlResponse {\n  @ApiProperty({\n    type: Number,\n    description:\n      'The remaining time to live of a key that has a timeout. ' +\n      'If value equals -2 then the key does not exist or has deleted. ' +\n      'If value equals -1 then the key has no associated expire (No limit).',\n    maximum: MAX_TTL_NUMBER,\n  })\n  ttl: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/key-info.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { KeyInfoProvider } from 'src/modules/browser/keys/key-info/key-info.provider';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { UnsupportedKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/unsupported.key-info.strategy';\nimport { GraphKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/graph.key-info.strategy';\nimport { HashKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/hash.key-info.strategy';\nimport { ListKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/list.key-info.strategy';\nimport { RejsonRlKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/rejson-rl.key-info.strategy';\nimport { SetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/set.key-info.strategy';\nimport { StreamKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/stream.key-info.strategy';\nimport { StringKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/string.key-info.strategy';\nimport { TsKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/ts.key-info.strategy';\nimport { ZSetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/z-set.key-info.strategy';\n\ndescribe('KeyInfoProvider', () => {\n  let service: KeyInfoProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        KeyInfoProvider,\n        GraphKeyInfoStrategy,\n        HashKeyInfoStrategy,\n        ListKeyInfoStrategy,\n        RejsonRlKeyInfoStrategy,\n        SetKeyInfoStrategy,\n        StreamKeyInfoStrategy,\n        StringKeyInfoStrategy,\n        TsKeyInfoStrategy,\n        ZSetKeyInfoStrategy,\n        UnsupportedKeyInfoStrategy,\n      ],\n    }).compile();\n\n    service = module.get(KeyInfoProvider);\n  });\n\n  describe('getStrategy', () => {\n    const testCases = [\n      {\n        input: 'unknown' as RedisDataType,\n        strategy: UnsupportedKeyInfoStrategy,\n      },\n      { input: RedisDataType.Graph, strategy: GraphKeyInfoStrategy },\n      { input: RedisDataType.Hash, strategy: HashKeyInfoStrategy },\n      { input: RedisDataType.List, strategy: ListKeyInfoStrategy },\n      { input: RedisDataType.JSON, strategy: RejsonRlKeyInfoStrategy },\n      { input: RedisDataType.Set, strategy: SetKeyInfoStrategy },\n      { input: RedisDataType.Stream, strategy: StreamKeyInfoStrategy },\n      { input: RedisDataType.String, strategy: StringKeyInfoStrategy },\n      { input: RedisDataType.TS, strategy: TsKeyInfoStrategy },\n      { input: RedisDataType.ZSet, strategy: ZSetKeyInfoStrategy },\n    ];\n\n    testCases.forEach((tc) => {\n      it(`Should return ${tc.strategy} strategy for ${tc.input} type`, () => {\n        expect(service.getStrategy(tc.input)).toBeInstanceOf(tc.strategy);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/key-info.provider.ts",
    "content": "import { GraphKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/graph.key-info.strategy';\nimport { HashKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/hash.key-info.strategy';\nimport { ListKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/list.key-info.strategy';\nimport { RejsonRlKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/rejson-rl.key-info.strategy';\nimport { SetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/set.key-info.strategy';\nimport { StreamKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/stream.key-info.strategy';\nimport { StringKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/string.key-info.strategy';\nimport { TsKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/ts.key-info.strategy';\nimport { UnsupportedKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/unsupported.key-info.strategy';\nimport { ZSetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/z-set.key-info.strategy';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class KeyInfoProvider {\n  constructor(\n    private readonly graphKeyInfoStrategy: GraphKeyInfoStrategy,\n    private readonly hashKeyInfoStrategy: HashKeyInfoStrategy,\n    private readonly listKeyInfoStrategy: ListKeyInfoStrategy,\n    private readonly rejsonRlKeyInfoStrategy: RejsonRlKeyInfoStrategy,\n    private readonly setKeyInfoStrategy: SetKeyInfoStrategy,\n    private readonly streamKeyInfoStrategy: StreamKeyInfoStrategy,\n    private readonly stringKeyInfoStrategy: StringKeyInfoStrategy,\n    private readonly tsKeyInfoStrategy: TsKeyInfoStrategy,\n    private readonly unsupportedKeyInfoStrategy: UnsupportedKeyInfoStrategy,\n    private readonly zSetKeyInfoStrategy: ZSetKeyInfoStrategy,\n  ) {}\n\n  getStrategy(type?: string): KeyInfoStrategy {\n    switch (type) {\n      case RedisDataType.Graph:\n        return this.graphKeyInfoStrategy;\n      case RedisDataType.Hash:\n        return this.hashKeyInfoStrategy;\n      case RedisDataType.List:\n        return this.listKeyInfoStrategy;\n      case RedisDataType.JSON:\n        return this.rejsonRlKeyInfoStrategy;\n      case RedisDataType.Set:\n        return this.setKeyInfoStrategy;\n      case RedisDataType.Stream:\n        return this.streamKeyInfoStrategy;\n      case RedisDataType.String:\n        return this.stringKeyInfoStrategy;\n      case RedisDataType.TS:\n        return this.tsKeyInfoStrategy;\n      case RedisDataType.ZSet:\n        return this.zSetKeyInfoStrategy;\n      default:\n        return this.unsupportedKeyInfoStrategy;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/graph.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolGraphCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { ReplyError } from 'src/models';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { GraphKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/graph.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testGraph',\n  type: 'graphdata',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\nconst mockGraphQueryReply = [\n  [[1, 'count(r)']],\n  [[[3, getKeyInfoResponse.length]]],\n  [\n    'Cached execution: 1',\n    'Query internal execution time: 0.093200 milliseconds',\n  ],\n];\n\ndescribe('GraphKeyInfoStrategy', () => {\n  let strategy: GraphKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [GraphKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(GraphKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [null, 50],\n        ]);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolGraphCommands.GraphQuery,\n          key,\n          'MATCH (r) RETURN count(r)',\n          '--compact',\n        ])\n        .mockResolvedValue(mockGraphQueryReply);\n    });\n    it('should return appropriate value', async () => {\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.Graph,\n      );\n\n      expect(result).toEqual(getKeyInfoResponse);\n    });\n    it('should return size with null value', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolKeysCommands.MemoryUsage,\n        message: \"ERR unknown command 'memory'\",\n      };\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [replyError, null],\n        ]);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.Graph,\n      );\n\n      expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n    });\n    it('should return result without length', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolGraphCommands.GraphQuery,\n        message: \"ERR unknown command 'graph.query\",\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolGraphCommands.GraphQuery,\n          key,\n          'MATCH (r) RETURN count(r)',\n          '--compact',\n        ])\n        .mockResolvedValue(replyError);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.Graph,\n      );\n\n      expect(result).toEqual({ ...getKeyInfoResponse, length: undefined });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/graph.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolGraphCommands,\n  BrowserToolKeysCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class GraphKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.Graph} type info.`);\n    const [[, ttl = null], [, size = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n    ])) as [any, number][];\n\n    const length = await this.getNodesCount(client, key);\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n\n  private async getNodesCount(\n    client: RedisClient,\n    key: RedisString,\n  ): Promise<number> {\n    try {\n      const queryReply = await client.sendCommand([\n        BrowserToolGraphCommands.GraphQuery,\n        key,\n        'MATCH (r) RETURN count(r)',\n        '--compact',\n      ]);\n\n      return queryReply[1][0][0][1];\n    } catch (error) {\n      return undefined;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/hash.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport {\n  BrowserToolHashCommands,\n  BrowserToolKeysCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { ReplyError } from 'src/models';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { HashKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/hash.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testHash',\n  type: 'hash',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('HashKeyInfoStrategy', () => {\n  let strategy: HashKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [HashKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(HashKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n\n    describe('when includeSize is true', () => {\n      it('should return all info in single pipeline', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolHashCommands.HLen, key],\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n            [null, 50],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Hash,\n          true,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n    });\n\n    describe('when includeSize is false', () => {\n      it('should return appropriate value', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolHashCommands.HLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Hash,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return size with null value when memory usage fails', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          command: BrowserToolKeysCommands.MemoryUsage,\n          message: \"ERR unknown command 'memory'\",\n        };\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolHashCommands.HLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[replyError, null]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Hash,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n      });\n\n      it('should not check size when length >= 50,000', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolHashCommands.HLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 50000],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Hash,\n          false,\n        );\n\n        expect(result).toEqual({\n          ...getKeyInfoResponse,\n          length: 50000,\n          size: -1,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/hash.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolHashCommands,\n  BrowserToolKeysCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class HashKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.Hash} type info.`);\n\n    if (includeSize !== false) {\n      const [[, ttl = null], [, length = null], [, size = null]] =\n        (await client.sendPipeline([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolHashCommands.HLen, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])) as [any, number][];\n\n      return {\n        name: key,\n        type,\n        ttl,\n        size,\n        length,\n      };\n    }\n\n    const [[, ttl = null], [, length = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolHashCommands.HLen, key],\n    ])) as [any, number][];\n\n    let size = -1;\n    if (length < 50_000) {\n      const sizeData = (await client.sendPipeline([\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n      size = sizeData && sizeData[0] && sizeData[0][1];\n    }\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/key-info.strategy.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { RedisString } from 'src/common/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\n@Injectable()\nexport abstract class KeyInfoStrategy {\n  protected readonly logger = new Logger(this.constructor.name);\n\n  abstract getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/list.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolListCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { ReplyError } from 'src/models';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { ListKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/list.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testList',\n  type: 'list',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('ListKeyInfoStrategy', () => {\n  let strategy: ListKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ListKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(ListKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n\n    describe('when includeSize is true', () => {\n      it('should return all info in single pipeline', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolListCommands.LLen, key],\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n            [null, 50],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.List,\n          true,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n    });\n\n    describe('when includeSize is false', () => {\n      it('should return appropriate value', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolListCommands.LLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.List,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return size with null when memory usage fails', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          command: BrowserToolKeysCommands.MemoryUsage,\n          message: \"ERR unknown command 'memory'\",\n        };\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolListCommands.LLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[replyError, null]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.List,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n      });\n\n      it('should not check size when length >= 50,000', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolListCommands.LLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 50000],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.List,\n          false,\n        );\n\n        expect(result).toEqual({\n          ...getKeyInfoResponse,\n          length: 50000,\n          size: -1,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/list.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolListCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class ListKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.List} type info.`);\n    if (includeSize !== false) {\n      const [[, ttl = null], [, length = null], [, size = null]] =\n        (await client.sendPipeline([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolListCommands.LLen, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])) as [any, number][];\n\n      return {\n        name: key,\n        type,\n        ttl,\n        size,\n        length,\n      };\n    }\n\n    const [[, ttl = null], [, length = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolListCommands.LLen, key],\n    ])) as [any, number][];\n\n    let size = -1;\n    if (length < 50_000) {\n      const sizeData = (await client.sendPipeline([\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n      size = sizeData && sizeData[0] && sizeData[0][1];\n    }\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/rejson-rl.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolRejsonRlCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { mockKeyDto } from 'src/modules/browser/__mocks__';\nimport { RejsonRlKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/rejson-rl.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: mockKeyDto.keyName,\n  type: 'ReJSON-RL',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('RejsonRlKeyInfoStrategy', () => {\n  let strategy: RejsonRlKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [RejsonRlKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(RejsonRlKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n\n    describe('when includeSize is true', () => {\n      it('should return all info in single pipeline for object type', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 50],\n          ]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('object');\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonObjLen, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(10);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          true,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n    });\n\n    describe('when includeSize is false', () => {\n      it('should return appropriate value for key that store object', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([[BrowserToolKeysCommands.Ttl, key]])\n          .mockResolvedValueOnce([[null, -1]]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('object');\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonObjLen, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(10);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return appropriate value for key that store array', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([[BrowserToolKeysCommands.Ttl, key]])\n          .mockResolvedValueOnce([[null, -1]]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('array');\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonArrLen, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(10);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return appropriate value for key that store string', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([[BrowserToolKeysCommands.Ttl, key]])\n          .mockResolvedValueOnce([[null, -1]]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('string');\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonStrLen, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(10);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return appropriate value for key that store not iterable type', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([[BrowserToolKeysCommands.Ttl, key]])\n          .mockResolvedValueOnce([[null, -1]]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('boolean');\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, length: null });\n      });\n\n      it('should return size with null when memory usage fails', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          command: BrowserToolKeysCommands.MemoryUsage,\n          message: \"ERR unknown command 'memory'\",\n        };\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([[BrowserToolKeysCommands.Ttl, key]])\n          .mockResolvedValueOnce([[null, -1]]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('object');\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonObjLen, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(10);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[replyError, null]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n      });\n\n      it('should not check size when length >= 100', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([[BrowserToolKeysCommands.Ttl, key]])\n          .mockResolvedValueOnce([[null, -1]]);\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue('object');\n\n        when(mockStandaloneRedisClient.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonObjLen, key], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(100);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.JSON,\n          false,\n        );\n\n        expect(result).toEqual({\n          ...getKeyInfoResponse,\n          length: 100,\n          size: -1,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/rejson-rl.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolRejsonRlCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class RejsonRlKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.JSON} type info.`);\n\n    if (includeSize !== false) {\n      const [[, ttl = null], [, size = null]] = (await client.sendPipeline([\n        [BrowserToolKeysCommands.Ttl, key],\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n\n      const length = await this.getLength(client, key);\n\n      return {\n        name: key,\n        type,\n        ttl,\n        size,\n        length,\n      };\n    }\n\n    const [[, ttl = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n    ])) as [any, number][];\n\n    const length = await this.getLength(client, key);\n\n    let size = -1;\n    if (length < 100) {\n      const sizeData = (await client.sendPipeline([\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n      size = sizeData && sizeData[0] && sizeData[0][1];\n    }\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n\n  private async getLength(\n    client: RedisClient,\n    key: RedisString,\n  ): Promise<number> {\n    try {\n      const objectKeyType = await client.sendCommand(\n        [BrowserToolRejsonRlCommands.JsonType, key],\n        { replyEncoding: 'utf8' },\n      );\n\n      switch (objectKeyType) {\n        case 'object':\n          return (await client.sendCommand(\n            [BrowserToolRejsonRlCommands.JsonObjLen, key],\n            { replyEncoding: 'utf8' },\n          )) as number;\n        case 'array':\n          return (await client.sendCommand(\n            [BrowserToolRejsonRlCommands.JsonArrLen, key],\n            { replyEncoding: 'utf8' },\n          )) as number;\n        case 'string':\n          return (await client.sendCommand(\n            [BrowserToolRejsonRlCommands.JsonStrLen, key],\n            { replyEncoding: 'utf8' },\n          )) as number;\n        default:\n          return null;\n      }\n    } catch (error) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/set.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { ReplyError } from 'src/models';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { SetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/set.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testSet',\n  type: 'set',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('SetKeyInfoStrategy', () => {\n  let strategy: SetKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [SetKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(SetKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n\n    describe('when includeSize is true', () => {\n      it('should return all info in single pipeline', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolSetCommands.SCard, key],\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n            [null, 50],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Set,\n          true,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n    });\n\n    describe('when includeSize is false', () => {\n      it('should return appropriate value', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolSetCommands.SCard, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Set,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return size with null when memory usage fails', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          command: BrowserToolKeysCommands.MemoryUsage,\n          message: \"ERR unknown command 'memory'\",\n        };\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolSetCommands.SCard, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[replyError, null]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Set,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n      });\n\n      it('should not check size when length >= 50,000', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolSetCommands.SCard, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 50000],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Set,\n          false,\n        );\n\n        expect(result).toEqual({\n          ...getKeyInfoResponse,\n          length: 50000,\n          size: -1,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/set.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class SetKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.Set} type info.`);\n\n    if (includeSize !== false) {\n      const [[, ttl = null], [, length = null], [, size = null]] =\n        (await client.sendPipeline([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolSetCommands.SCard, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])) as [any, number][];\n\n      return {\n        name: key,\n        type,\n        ttl,\n        size,\n        length,\n      };\n    }\n\n    const [[, ttl = null], [, length = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolSetCommands.SCard, key],\n    ])) as [any, number][];\n\n    let size = -1;\n    if (length < 50_000) {\n      const sizeData = (await client.sendPipeline([\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n      size = sizeData && sizeData[0] && sizeData[0][1];\n    }\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/stream.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { StreamKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/stream.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testStream',\n  type: 'stream',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('StreamKeyInfoStrategy', () => {\n  let strategy: StreamKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [StreamKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(StreamKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n\n    describe('when includeSize is true', () => {\n      it('should return all info in single pipeline', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolStreamCommands.XLen, key],\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n            [null, 50],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Stream,\n          true,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n    });\n\n    describe('when includeSize is false', () => {\n      it('should return appropriate value', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolStreamCommands.XLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Stream,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return size with null when memory usage fails', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          command: BrowserToolKeysCommands.MemoryUsage,\n          message: \"ERR unknown command 'memory'\",\n        };\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolStreamCommands.XLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[replyError, null]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Stream,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n      });\n\n      it('should not check size when length >= 50,000', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolStreamCommands.XLen, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 50000],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.Stream,\n          false,\n        );\n\n        expect(result).toEqual({\n          ...getKeyInfoResponse,\n          length: 50000,\n          size: -1,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/stream.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class StreamKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.Stream} type info.`);\n\n    if (includeSize !== false) {\n      const [[, ttl = null], [, length = null], [, size = null]] =\n        (await client.sendPipeline([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolStreamCommands.XLen, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])) as [any, number][];\n\n      return {\n        name: key,\n        type,\n        ttl,\n        size,\n        length,\n      };\n    }\n\n    const [[, ttl = null], [, length = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolStreamCommands.XLen, key],\n    ])) as [any, number][];\n\n    let size = -1;\n    if (length < 50_000) {\n      const sizeData = (await client.sendPipeline([\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n      size = sizeData && sizeData[0] && sizeData[0][1];\n    }\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/string.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStringCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { ReplyError } from 'src/models';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { StringKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/string.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testString',\n  type: 'string',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('StringTypeInfoStrategy', () => {\n  let strategy: StringKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [StringKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(StringKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n    it('should return appropriate value', async () => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          [BrowserToolStringCommands.StrLen, key],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [null, 50],\n          [null, 10],\n        ]);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.String,\n      );\n\n      expect(result).toEqual(getKeyInfoResponse);\n    });\n    it('should return size with null value', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolKeysCommands.MemoryUsage,\n        message: \"ERR unknown command 'memory'\",\n      };\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          [BrowserToolStringCommands.StrLen, key],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [replyError, null],\n          [null, 10],\n        ]);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.String,\n      );\n\n      expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/string.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStringCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class StringKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.String} type info.`);\n\n    const [[, ttl = null], [, size = null], [, length = null]] =\n      (await client.sendPipeline([\n        [BrowserToolKeysCommands.Ttl, key],\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        [BrowserToolStringCommands.StrLen, key],\n      ])) as [any, number][];\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/ts.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolTSCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { ReplyError } from 'src/models';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { TsKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/ts.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testTS',\n  type: 'TSDB-TYPE',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\nconst mockTSInfoReply = [\n  'totalSamples',\n  10,\n  'memoryUsage',\n  4239,\n  'firstTimestamp',\n  0,\n  'lastTimestamp',\n  0,\n  'retentionTime',\n  6000,\n  'chunkCount',\n  1,\n  'chunkSize',\n  4096,\n];\n\ndescribe('TsKeyInfoStrategy', () => {\n  let strategy: TsKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [TsKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(TsKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [null, 50],\n        ]);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolTSCommands.TSInfo, key], {\n          replyEncoding: 'utf8',\n        })\n        .mockResolvedValue(mockTSInfoReply);\n    });\n    it('should return appropriate value', async () => {\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.TS,\n      );\n\n      expect(result).toEqual(getKeyInfoResponse);\n    });\n    it('should return size with null value', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolKeysCommands.MemoryUsage,\n        message: \"ERR unknown command 'memory'\",\n      };\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [replyError, null],\n        ]);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.TS,\n      );\n\n      expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n    });\n    it('should return result without length', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolTSCommands.TSInfo,\n        message: \"ERR unknown command 'ts.info'\",\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolTSCommands.TSInfo, key], {\n          replyEncoding: 'utf8',\n        })\n        .mockResolvedValue(replyError);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        RedisDataType.TS,\n      );\n\n      expect(result).toEqual({ ...getKeyInfoResponse, length: undefined });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/ts.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolTSCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class TsKeyInfoStrategy extends KeyInfoStrategy {\n  private async getTotalSamples(\n    client: RedisClient,\n    key: RedisString,\n  ): Promise<number> {\n    try {\n      const info = (await client.sendCommand(\n        [BrowserToolTSCommands.TSInfo, key],\n        { replyEncoding: 'utf8' },\n      )) as string[];\n\n      return convertArrayReplyToObject(info).totalsamples;\n    } catch (error) {\n      return undefined;\n    }\n  }\n\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.TS} type info.`);\n\n    const [[, ttl = null], [, size = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n    ])) as [any, number][];\n\n    const length = await this.getTotalSamples(client, key);\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/unsupported.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { UnsupportedKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/unsupported.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testKey',\n  type: 'custom-type',\n  ttl: -1,\n  size: 50,\n};\n\ndescribe('UnsupportedKeyInfoStrategy', () => {\n  let strategy: UnsupportedKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [UnsupportedKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(UnsupportedKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n    it('should return appropriate value', async () => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [null, 50],\n        ]);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        'custom-type',\n      );\n\n      expect(result).toEqual(getKeyInfoResponse);\n    });\n    it('should return size with null value', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolKeysCommands.MemoryUsage,\n        message: \"ERR unknown command 'memory'\",\n      };\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])\n        .mockResolvedValue([\n          [null, -1],\n          [replyError, null],\n        ]);\n\n      const result = await strategy.getInfo(\n        mockStandaloneRedisClient,\n        key,\n        'custom-type',\n      );\n\n      expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/unsupported.key-info.strategy.ts",
    "content": "import { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class UnsupportedKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${type} type info.`);\n\n    const [[, ttl = null], [, size = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n    ])) as [any, number][];\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/z-set.key-info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolZSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { ZSetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/z-set.key-info.strategy';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: 'testZSet',\n  type: 'zset',\n  ttl: -1,\n  size: 50,\n  length: 10,\n};\n\ndescribe('ZSetKeyInfoStrategy', () => {\n  let strategy: ZSetKeyInfoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ZSetKeyInfoStrategy],\n    }).compile();\n\n    strategy = module.get(ZSetKeyInfoStrategy);\n  });\n\n  describe('getInfo', () => {\n    const key = getKeyInfoResponse.name;\n\n    describe('when includeSize is true', () => {\n      it('should return all info in single pipeline', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolZSetCommands.ZCard, key],\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n            [null, 50],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.ZSet,\n          true,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n    });\n\n    describe('when includeSize is false', () => {\n      it('should return appropriate value', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolZSetCommands.ZCard, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[null, 50]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.ZSet,\n          false,\n        );\n\n        expect(result).toEqual(getKeyInfoResponse);\n      });\n\n      it('should return size with null when memory usage fails', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          command: BrowserToolKeysCommands.MemoryUsage,\n          message: \"ERR unknown command 'memory'\",\n        };\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolZSetCommands.ZCard, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 10],\n          ]);\n\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n          ])\n          .mockResolvedValueOnce([[replyError, null]]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.ZSet,\n          false,\n        );\n\n        expect(result).toEqual({ ...getKeyInfoResponse, size: null });\n      });\n\n      it('should not check size when length >= 50,000', async () => {\n        when(mockStandaloneRedisClient.sendPipeline)\n          .calledWith([\n            [BrowserToolKeysCommands.Ttl, key],\n            [BrowserToolZSetCommands.ZCard, key],\n          ])\n          .mockResolvedValueOnce([\n            [null, -1],\n            [null, 50000],\n          ]);\n\n        const result = await strategy.getInfo(\n          mockStandaloneRedisClient,\n          key,\n          RedisDataType.ZSet,\n          false,\n        );\n\n        expect(result).toEqual({\n          ...getKeyInfoResponse,\n          length: 50000,\n          size: -1,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/key-info/strategies/z-set.key-info.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolZSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { KeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/key-info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class ZSetKeyInfoStrategy extends KeyInfoStrategy {\n  public async getInfo(\n    client: RedisClient,\n    key: RedisString,\n    type: string,\n    includeSize: boolean,\n  ): Promise<GetKeyInfoResponse> {\n    this.logger.debug(`Getting ${RedisDataType.ZSet} type info.`);\n\n    if (includeSize !== false) {\n      const [[, ttl = null], [, length = null], [, size = null]] =\n        (await client.sendPipeline([\n          [BrowserToolKeysCommands.Ttl, key],\n          [BrowserToolZSetCommands.ZCard, key],\n          [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n        ])) as [any, number][];\n\n      return {\n        name: key,\n        type,\n        ttl,\n        size,\n        length,\n      };\n    }\n\n    const [[, ttl = null], [, length = null]] = (await client.sendPipeline([\n      [BrowserToolKeysCommands.Ttl, key],\n      [BrowserToolZSetCommands.ZCard, key],\n    ])) as [any, number][];\n\n    let size = -1;\n    if (length < 50_000) {\n      const sizeData = (await client.sendPipeline([\n        [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'],\n      ])) as [any, number][];\n      size = sizeData && sizeData[0] && sizeData[0][1];\n    }\n\n    return {\n      name: key,\n      type,\n      ttl,\n      size,\n      length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/keys.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  HttpCode,\n  Patch,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { KeysService } from 'src/modules/browser/keys/keys.service';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  DeleteKeysDto,\n  DeleteKeysResponse,\n  GetKeyInfoDto,\n  GetKeysDto,\n  GetKeysWithDetailsResponse,\n  GetKeyInfoResponse,\n  RenameKeyDto,\n  RenameKeyResponse,\n  UpdateKeyTtlDto,\n  KeyTtlResponse,\n  GetKeysInfoDto,\n  GetNamespaceSearchableDto,\n  NamespaceSearchableResponse,\n} from 'src/modules/browser/keys/dto';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\n\n@ApiTags('Browser: Keys')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('keys')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class KeysController {\n  constructor(private keysService: KeysService) {}\n\n  @Post('')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Get keys by cursor position' })\n  @ApiRedisParams()\n  @ApiOkResponse({\n    description: 'Keys list',\n    type: GetKeysWithDetailsResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getKeys(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetKeysDto,\n  ): Promise<GetKeysWithDetailsResponse[]> {\n    return this.keysService.getKeys(clientMetadata, dto);\n  }\n\n  @Post('get-metadata')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Get info for multiple keys' })\n  @ApiBody({ type: GetKeysInfoDto })\n  @ApiRedisParams()\n  @ApiOkResponse({\n    description: 'Info for multiple keys',\n    type: GetKeysWithDetailsResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getKeysInfo(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetKeysInfoDto,\n  ): Promise<GetKeyInfoResponse[]> {\n    return this.keysService.getKeysInfo(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/get-info')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Get key info' })\n  @ApiRedisParams()\n  @ApiBody({ type: GetKeyInfoDto })\n  @ApiOkResponse({\n    description: 'Keys info',\n    type: GetKeyInfoResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getKeyInfo(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetKeyInfoDto,\n  ): Promise<GetKeyInfoResponse> {\n    return await this.keysService.getKeyInfo(\n      clientMetadata,\n      dto.keyName,\n      dto.includeSize,\n    );\n  }\n\n  @Post('get-namespace-searchable')\n  @HttpCode(200)\n  @ApiOperation({\n    description: 'Check if namespaces contain searchable keys (hash/json)',\n  })\n  @ApiBody({ type: GetNamespaceSearchableDto })\n  @ApiRedisParams()\n  @ApiOkResponse({\n    description: 'Searchable key info per namespace prefix',\n    type: [NamespaceSearchableResponse],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getNamespaceSearchable(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetNamespaceSearchableDto,\n  ): Promise<NamespaceSearchableResponse[]> {\n    return this.keysService.getNamespaceSearchable(clientMetadata, dto);\n  }\n\n  @Delete('')\n  @ApiOperation({ description: 'Delete key' })\n  @ApiRedisParams()\n  @ApiBody({ type: DeleteKeysDto })\n  @ApiOkResponse({\n    description: 'Number of affected keys.',\n    type: DeleteKeysResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async deleteKey(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteKeysDto,\n  ): Promise<DeleteKeysResponse> {\n    return await this.keysService.deleteKeys(clientMetadata, dto.keyNames);\n  }\n\n  @Patch('/name')\n  @ApiOperation({ description: 'Rename key' })\n  @ApiRedisParams()\n  @ApiBody({ type: RenameKeyDto })\n  @ApiOkResponse({\n    description: 'New key name.',\n    type: RenameKeyResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async renameKey(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: RenameKeyDto,\n  ): Promise<RenameKeyResponse> {\n    return await this.keysService.renameKey(clientMetadata, dto);\n  }\n\n  @Patch('/ttl')\n  @ApiOperation({ description: 'Update the remaining time to live of a key' })\n  @ApiRedisParams()\n  @ApiBody({ type: UpdateKeyTtlDto })\n  @ApiOkResponse({\n    description: 'The remaining time to live of a key.',\n    type: KeyTtlResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async updateTtl(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: UpdateKeyTtlDto,\n  ): Promise<KeyTtlResponse> {\n    return await this.keysService.updateTtl(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/keys.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { KeysController } from 'src/modules/browser/keys/keys.controller';\nimport { StandaloneScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/standalone.scanner.strategy';\nimport { ClusterScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/cluster.scanner.strategy';\nimport { Scanner } from 'src/modules/browser/keys/scanner/scanner';\nimport { KeysService } from 'src/modules/browser/keys/keys.service';\nimport { KeyInfoProvider } from 'src/modules/browser/keys/key-info/key-info.provider';\nimport { GraphKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/graph.key-info.strategy';\nimport { HashKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/hash.key-info.strategy';\nimport { ListKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/list.key-info.strategy';\nimport { RejsonRlKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/rejson-rl.key-info.strategy';\nimport { SetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/set.key-info.strategy';\nimport { StreamKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/stream.key-info.strategy';\nimport { StringKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/string.key-info.strategy';\nimport { TsKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/ts.key-info.strategy';\nimport { UnsupportedKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/unsupported.key-info.strategy';\nimport { ZSetKeyInfoStrategy } from 'src/modules/browser/keys/key-info/strategies/z-set.key-info.strategy';\n\n@Module({})\nexport class KeysModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: KeysModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: KeysModule,\n          },\n        ]),\n      ],\n      controllers: [KeysController],\n      providers: [\n        Scanner,\n        StandaloneScannerStrategy,\n        ClusterScannerStrategy,\n        KeysService,\n        KeyInfoProvider,\n        // scanner strategies\n        StandaloneScannerStrategy,\n        ClusterScannerStrategy,\n        // key info strategies\n        GraphKeyInfoStrategy,\n        HashKeyInfoStrategy,\n        ListKeyInfoStrategy,\n        RejsonRlKeyInfoStrategy,\n        SetKeyInfoStrategy,\n        StreamKeyInfoStrategy,\n        StringKeyInfoStrategy,\n        TsKeyInfoStrategy,\n        UnsupportedKeyInfoStrategy,\n        ZSetKeyInfoStrategy,\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/keys.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  ForbiddenException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { when } from 'jest-when';\nimport { ReplyError } from 'src/models/redis-client';\nimport {\n  mockRedisNoPermError,\n  MockType,\n  mockBrowserClientMetadata,\n  mockBrowserHistoryService,\n  mockDatabaseRecommendationService,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n  mockClusterRedisClient,\n} from 'src/__mocks__';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport {\n  GetKeyInfoResponse,\n  GetKeysDto,\n  GetKeysWithDetailsResponse,\n  RedisDataType,\n  RenameKeyDto,\n} from 'src/modules/browser/keys/dto';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { KeysService } from 'src/modules/browser/keys/keys.service';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { Scanner } from 'src/modules/browser/keys/scanner/scanner';\nimport {\n  mockScanner,\n  mockScannerStrategy,\n  mockTypeInfoStrategy,\n} from 'src/modules/browser/__mocks__';\nimport { KeyInfoProvider } from 'src/modules/browser/keys/key-info/key-info.provider';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nconst getKeyInfoResponse: GetKeyInfoResponse = {\n  name: Buffer.from('testString'),\n  type: 'string',\n  ttl: -1,\n  size: 50,\n};\n\nconst mockGetKeysWithDetailsResponse: GetKeysWithDetailsResponse = {\n  cursor: 0,\n  total: 1,\n  scanned: 0,\n  keys: [getKeyInfoResponse],\n};\n\ndescribe('KeysService', () => {\n  let service: KeysService;\n  let databaseClientFactory: MockType<DatabaseClientFactory>;\n  let browserHistoryService: MockType<BrowserHistoryService>;\n  let recommendationService: MockType<DatabaseRecommendationService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        KeysService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: BrowserHistoryService,\n          useFactory: mockBrowserHistoryService,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n        {\n          provide: Scanner,\n          useFactory: mockScanner,\n        },\n        {\n          provide: KeyInfoProvider,\n          useFactory: () => ({\n            getStrategy: jest.fn().mockReturnValue(mockTypeInfoStrategy),\n          }),\n        },\n      ],\n    }).compile();\n\n    service = module.get<KeysService>(KeysService);\n    databaseClientFactory = module.get(DatabaseClientFactory);\n    recommendationService = module.get(DatabaseRecommendationService);\n    browserHistoryService = module.get(BrowserHistoryService);\n  });\n\n  describe('getKeyInfo', () => {\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Type, getKeyInfoResponse.name], {\n          replyEncoding: 'utf8',\n        })\n        .mockResolvedValue(RedisDataType.String);\n    });\n\n    it('should return appropriate value', async () => {\n      const mockResult: GetKeyInfoResponse = {\n        ...getKeyInfoResponse,\n        length: 10,\n      };\n      mockTypeInfoStrategy.getInfo.mockResolvedValue(mockResult);\n\n      const result = await service.getKeyInfo(\n        mockBrowserClientMetadata,\n        getKeyInfoResponse.name,\n      );\n\n      expect(result).toEqual(mockResult);\n    });\n    it('throw NotFound error when key not found for getKeyInfo', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Type, getKeyInfoResponse.name], {\n          replyEncoding: 'utf8',\n        })\n        .mockResolvedValue('none');\n\n      await expect(\n        service.getKeyInfo(mockBrowserClientMetadata, getKeyInfoResponse.name),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"user don't have required permissions for getKeyInfo\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'TYPE',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Type, getKeyInfoResponse.name], {\n          replyEncoding: 'utf8',\n        })\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.getKeyInfo(mockBrowserClientMetadata, getKeyInfoResponse.name),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('should call recommendationService', async () => {\n      const mockResult: GetKeyInfoResponse = {\n        ...getKeyInfoResponse,\n        length: 10,\n      };\n      mockTypeInfoStrategy.getInfo.mockResolvedValue(mockResult);\n\n      const result = await service.getKeyInfo(\n        mockBrowserClientMetadata,\n        getKeyInfoResponse.name,\n      );\n\n      expect(recommendationService.checkMulti).toBeCalledWith(\n        mockBrowserClientMetadata,\n        [\n          RECOMMENDATION_NAMES.BIG_SETS,\n          RECOMMENDATION_NAMES.BIG_STRINGS,\n          RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n        ],\n        result,\n      );\n    });\n  });\n\n  describe('getKeysInfo', () => {\n    beforeEach(() => {\n      mockScannerStrategy.getKeysInfo.mockResolvedValue([getKeyInfoResponse]);\n    });\n\n    it('should return keys with info', async () => {\n      const result = await service.getKeysInfo(mockBrowserClientMetadata, {\n        keys: [getKeyInfoResponse.name],\n      });\n\n      expect(result).toEqual([getKeyInfoResponse]);\n    });\n    it('should call recommendationService', async () => {\n      const result = await service.getKeysInfo(mockBrowserClientMetadata, {\n        keys: [getKeyInfoResponse.name],\n      });\n\n      expect(recommendationService.check).toBeCalledTimes(1);\n      expect(recommendationService.check).toBeCalledWith(\n        mockBrowserClientMetadata,\n        RECOMMENDATION_NAMES.SEARCH_JSON,\n        {\n          keys: result,\n          client: mockStandaloneRedisClient,\n          databaseId: mockBrowserClientMetadata.databaseId,\n        },\n      );\n    });\n    it(\"user don't have required permissions for getKeyInfo\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'TYPE',\n      };\n\n      mockScannerStrategy.getKeysInfo.mockRejectedValueOnce(replyError);\n\n      await expect(\n        service.getKeysInfo(mockBrowserClientMetadata, {\n          keys: [getKeyInfoResponse.name],\n        }),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('getKeys', () => {\n    const getKeysDto: GetKeysDto = {\n      cursor: '0',\n      count: 15,\n      scanThreshold: 10000,\n    };\n    it('should return appropriate value for standalone database', async () => {\n      mockScannerStrategy.getKeys.mockResolvedValue([\n        mockGetKeysWithDetailsResponse,\n      ]);\n\n      const result = await service.getKeys(\n        mockBrowserClientMetadata,\n        getKeysDto,\n      );\n\n      expect(mockScannerStrategy.getKeys).toHaveBeenCalled();\n      expect(result).toEqual([mockGetKeysWithDetailsResponse]);\n    });\n    it('should return appropriate value for cluster', async () => {\n      databaseClientFactory.getOrCreateClient.mockResolvedValueOnce(\n        mockClusterRedisClient,\n      );\n      mockScannerStrategy.getKeys.mockResolvedValue([\n        mockGetKeysWithDetailsResponse,\n      ]);\n\n      const result = await service.getKeys(\n        mockBrowserClientMetadata,\n        getKeysDto,\n      );\n\n      expect(mockScannerStrategy.getKeys).toHaveBeenCalled();\n      expect(result).toEqual([mockGetKeysWithDetailsResponse]);\n    });\n    it(\"user don't have required permissions for getKeys\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SCAN',\n      };\n      mockScannerStrategy.getKeys.mockRejectedValue(replyError);\n\n      await expect(\n        service.getKeys(mockBrowserClientMetadata, getKeysDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('scan per type not supported', async () => {\n      const dto: GetKeysDto = {\n        ...getKeysDto,\n        type: RedisDataType.String,\n      };\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        message: 'ERR syntax error',\n        command: 'SCAN',\n      };\n      mockScannerStrategy.getKeys.mockRejectedValue(replyError);\n\n      try {\n        await service.getKeys(mockBrowserClientMetadata, dto);\n        fail('Should throw an error');\n      } catch (err) {\n        expect(err).toBeInstanceOf(BadRequestException);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.SCAN_PER_KEY_TYPE_NOT_SUPPORT(),\n        );\n      }\n    });\n    it('should call create browser history item if match !== \"*\"', async () => {\n      mockScannerStrategy.getKeys.mockResolvedValue([\n        mockGetKeysWithDetailsResponse,\n      ]);\n\n      await service.getKeys(mockBrowserClientMetadata, {\n        ...getKeysDto,\n        match: '1',\n      });\n\n      expect(mockScannerStrategy.getKeys).toHaveBeenCalled();\n      expect(browserHistoryService.create).toHaveBeenCalled();\n    });\n    it('should do not call create browser history item if match === \"*\"', async () => {\n      mockScannerStrategy.getKeys.mockResolvedValue([\n        mockGetKeysWithDetailsResponse,\n      ]);\n\n      await service.getKeys(mockBrowserClientMetadata, {\n        ...getKeysDto,\n        match: '*',\n      });\n\n      expect(mockScannerStrategy.getKeys).toHaveBeenCalled();\n      expect(browserHistoryService.create).not.toHaveBeenCalled();\n    });\n    it('should call recommendationService', async () => {\n      const response = [mockGetKeysWithDetailsResponse];\n      mockScannerStrategy.getKeys.mockResolvedValue(response);\n\n      await service.getKeys(mockBrowserClientMetadata, {\n        ...getKeysDto,\n        match: '*',\n      });\n\n      expect(recommendationService.check).toBeCalledWith(\n        mockBrowserClientMetadata,\n        RECOMMENDATION_NAMES.USE_SMALLER_KEYS,\n        response[0].total,\n      );\n    });\n  });\n\n  describe('deleteKeys', () => {\n    const keyNames = ['testString1', 'testString2'];\n\n    it('succeeded to delete keys', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Del, ...keyNames])\n        .mockResolvedValue(keyNames.length);\n\n      const result = await service.deleteKeys(mockBrowserClientMetadata, [\n        'testString1',\n        'testString2',\n      ]);\n      expect(result).toEqual({ affected: keyNames.length });\n    });\n    it('keys not found', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Del, ...keyNames])\n        .mockResolvedValue(null);\n\n      await expect(\n        service.deleteKeys(mockBrowserClientMetadata, keyNames),\n      ).rejects.toThrow(new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST));\n    });\n    it(\"user don't have required permissions for deleteKeys\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'DEL',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteKeys(mockBrowserClientMetadata, keyNames),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('renameKey', () => {\n    const renameKeyDto: RenameKeyDto = {\n      keyName: 'testString1',\n      newKeyName: 'testString2',\n    };\n\n    it('succeeded to rename key', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, renameKeyDto.keyName])\n        .mockResolvedValue(true);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.RenameNX,\n          renameKeyDto.keyName,\n          renameKeyDto.newKeyName,\n        ])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.renameKey(mockBrowserClientMetadata, renameKeyDto),\n      ).resolves.not.toThrow();\n    });\n    it('key with keyName not exist', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, renameKeyDto.keyName])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.renameKey(mockBrowserClientMetadata, renameKeyDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it('key with newKeyName already exists', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, renameKeyDto.keyName])\n        .mockResolvedValue(true);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.RenameNX,\n          renameKeyDto.keyName,\n          renameKeyDto.newKeyName,\n        ])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.renameKey(mockBrowserClientMetadata, renameKeyDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for renameKey\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'RENAMENX',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.renameKey(mockBrowserClientMetadata, renameKeyDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('updateTtl', () => {\n    const keyName = 'testString';\n    it('set expiration time', async () => {\n      const dto = { keyName, ttl: 1000 };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Ttl, keyName])\n        .mockResolvedValue(-1);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Expire, keyName, dto.ttl])\n        .mockResolvedValue(1);\n\n      const result = await service.updateTtl(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual({ ttl: dto.ttl });\n    });\n    it('remove the existing timeout on key', async () => {\n      const dto = { keyName, ttl: -1 };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Ttl, keyName])\n        .mockResolvedValue(1000);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Persist, keyName])\n        .mockResolvedValue(1);\n\n      const result = await service.updateTtl(mockBrowserClientMetadata, dto);\n      expect(result).toEqual({ ttl: dto.ttl });\n    });\n    it('key not found', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Expire, keyName, 1000])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.updateTtl(mockBrowserClientMetadata, { keyName, ttl: 1000 }),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"user don't have required permissions for updateTtl\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'EXPIRE',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.updateTtl(mockBrowserClientMetadata, { keyName, ttl: 1000 }),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('getNamespaceSearchable', () => {\n    const dto = { prefixes: ['user:', 'session:'] };\n\n    beforeEach(() => {\n      mockStandaloneRedisClient.sendPipeline.mockReset();\n    });\n\n    it('should return searchable key when hash key found', async () => {\n      mockStandaloneRedisClient.sendPipeline\n        .mockResolvedValueOnce([\n          [null, ['0', ['user:1']]],\n          [null, ['0', []]],\n        ])\n        .mockResolvedValueOnce([\n          [null, ['0', []]],\n          [null, ['0', []]],\n        ]);\n\n      const result = await service.getNamespaceSearchable(\n        mockBrowserClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual([\n        { prefix: 'user:', key: { name: 'user:1', type: 'hash' } },\n        { prefix: 'session:' },\n      ]);\n    });\n\n    it('should return searchable key when json key found', async () => {\n      mockStandaloneRedisClient.sendPipeline\n        .mockResolvedValueOnce([\n          [null, ['0', []]],\n          [null, ['0', ['user:json1']]],\n        ])\n        .mockResolvedValueOnce([\n          [null, ['0', []]],\n          [null, ['0', []]],\n        ]);\n\n      const result = await service.getNamespaceSearchable(\n        mockBrowserClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual([\n        { prefix: 'user:', key: { name: 'user:json1', type: 'ReJSON-RL' } },\n        { prefix: 'session:' },\n      ]);\n    });\n\n    it('should return empty when no searchable keys found', async () => {\n      mockStandaloneRedisClient.sendPipeline.mockResolvedValue([\n        [null, ['0', []]],\n        [null, ['0', []]],\n      ]);\n\n      const result = await service.getNamespaceSearchable(\n        mockBrowserClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual([{ prefix: 'user:' }, { prefix: 'session:' }]);\n    });\n\n    it('should iterate scan until key is found', async () => {\n      const singleDto = { prefixes: ['user:'] };\n\n      mockStandaloneRedisClient.sendPipeline\n        .mockResolvedValueOnce([\n          [null, ['42', []]],\n          [null, ['0', []]],\n        ])\n        .mockResolvedValueOnce([[null, ['0', ['user:2']]]]);\n\n      const result = await service.getNamespaceSearchable(\n        mockBrowserClientMetadata,\n        singleDto,\n      );\n\n      expect(result).toEqual([\n        { prefix: 'user:', key: { name: 'user:2', type: 'hash' } },\n      ]);\n      expect(mockStandaloneRedisClient.sendPipeline).toHaveBeenCalledTimes(2);\n    });\n\n    it('should throw on ACL error', async () => {\n      const replyError = {\n        ...mockRedisNoPermError,\n        command: 'SCAN',\n      };\n      mockStandaloneRedisClient.sendPipeline.mockRejectedValue(replyError);\n\n      await expect(\n        service.getNamespaceSearchable(mockBrowserClientMetadata, dto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('removeKeyExpiration', () => {\n    const keyName = 'testString';\n    it('should remove key expiration', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Ttl, keyName])\n        .mockResolvedValue(1000);\n\n      const result = await service.removeKeyExpiration(\n        mockBrowserClientMetadata,\n        {\n          keyName,\n          ttl: -1,\n        },\n      );\n      expect(result).toEqual({ ttl: -1 });\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith([\n        BrowserToolKeysCommands.Persist,\n        keyName,\n      ]);\n    });\n    it('key not found', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Ttl, keyName])\n        .mockResolvedValue(-2);\n\n      await expect(\n        service.removeKeyExpiration(mockBrowserClientMetadata, {\n          keyName,\n          ttl: -1,\n        }),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"user don't have required permissions for removeKeyExpiration\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'TTL',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.removeKeyExpiration(mockBrowserClientMetadata, {\n          keyName,\n          ttl: -1,\n        }),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/keys.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport {\n  DEFAULT_MATCH,\n  RECOMMENDATION_NAMES,\n  RedisErrorCodes,\n} from 'src/constants';\nimport { catchAclError } from 'src/utils';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  DeleteKeysResponse,\n  GetKeyInfoResponse,\n  GetKeysDto,\n  GetKeysInfoDto,\n  GetKeysWithDetailsResponse,\n  GetNamespaceSearchableDto,\n  KeyTtlResponse,\n  NamespaceSearchableResponse,\n  RenameKeyDto,\n  RenameKeyResponse,\n  UpdateKeyTtlDto,\n} from 'src/modules/browser/keys/dto';\nimport { RedisDataType } from 'src/modules/browser/keys/dto/key.dto';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisClientCommand } from 'src/modules/redis/client';\nimport { ClientMetadata } from 'src/common/models';\nimport { Scanner } from 'src/modules/browser/keys/scanner/scanner';\nimport { BrowserHistoryMode, RedisString } from 'src/common/constants';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { pick } from 'lodash';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { CreateBrowserHistoryDto } from 'src/modules/browser/browser-history/dto';\nimport { KeyInfoProvider } from 'src/modules/browser/keys/key-info/key-info.provider';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { checkIfKeyNotExists } from 'src/modules/browser/utils';\n\n@Injectable()\nexport class KeysService {\n  private logger = new Logger('KeysService');\n\n  constructor(\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly scanner: Scanner,\n    private readonly keyInfoProvider: KeyInfoProvider,\n    private readonly browserHistory: BrowserHistoryService,\n    private readonly recommendationService: DatabaseRecommendationService,\n  ) {}\n\n  public async getKeys(\n    clientMetadata: ClientMetadata,\n    dto: GetKeysDto,\n  ): Promise<GetKeysWithDetailsResponse[]> {\n    try {\n      this.logger.debug('Getting keys with details.', clientMetadata);\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const scanner = this.scanner.getStrategy(client.getConnectionType());\n      const result = await scanner.getKeys(client, dto);\n\n      // Do not save default match \"*\"\n      if (dto.match !== DEFAULT_MATCH) {\n        await this.browserHistory.create(\n          clientMetadata,\n          plainToInstance(CreateBrowserHistoryDto, {\n            filter: pick(dto, 'type', 'match'),\n            mode: BrowserHistoryMode.Pattern,\n          }),\n        );\n      }\n\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.USE_SMALLER_KEYS,\n        result[0]?.total,\n      );\n\n      return result.map((nodeResult) =>\n        plainToInstance(GetKeysWithDetailsResponse, nodeResult),\n      );\n    } catch (error) {\n      this.logger.error(\n        `Failed to get keys with details info. ${error.message}.`,\n        error,\n        clientMetadata,\n      );\n      if (\n        error.message.includes(RedisErrorCodes.CommandSyntaxError) &&\n        dto.type\n      ) {\n        throw new BadRequestException(\n          ERROR_MESSAGES.SCAN_PER_KEY_TYPE_NOT_SUPPORT(),\n        );\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Fetch additional keys info (type, size, ttl)\n   * For standalone instances will use pipeline\n   * For cluster instances will use single commands\n   * @param clientMetadata\n   * @param dto\n   */\n  public async getKeysInfo(\n    clientMetadata: ClientMetadata,\n    dto: GetKeysInfoDto,\n  ): Promise<GetKeyInfoResponse[]> {\n    try {\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const scanner = this.scanner.getStrategy(client.getConnectionType());\n      const result = await scanner.getKeysInfo(\n        client,\n        dto.keys,\n        dto.type,\n        dto.includeSize,\n        dto.includeTTL,\n      );\n\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.SEARCH_JSON,\n        { keys: result, client, databaseId: clientMetadata.databaseId },\n      );\n\n      return plainToInstance(GetKeyInfoResponse, result);\n    } catch (error) {\n      this.logger.error(\n        `Failed to get keys info: ${error.message}.`,\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  public async getKeyInfo(\n    clientMetadata: ClientMetadata,\n    key: RedisString,\n    includeSize: boolean = false,\n  ): Promise<GetKeyInfoResponse> {\n    try {\n      this.logger.debug('Getting key info.', clientMetadata);\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const type = (await client.sendCommand(\n        [BrowserToolKeysCommands.Type, key],\n        {\n          replyEncoding: 'utf8',\n        },\n      )) as string;\n\n      if (type === 'none') {\n        this.logger.error(\n          `Failed to get key info. Not found key: ${key}`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      const result = await this.keyInfoProvider\n        .getStrategy(type)\n        .getInfo(client, key, type, includeSize);\n      this.logger.debug('Succeed to get key info', clientMetadata);\n\n      this.recommendationService.checkMulti(\n        clientMetadata,\n        [\n          RECOMMENDATION_NAMES.BIG_SETS,\n          RECOMMENDATION_NAMES.BIG_STRINGS,\n          RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n        ],\n        result,\n      );\n\n      return plainToInstance(GetKeyInfoResponse, result);\n    } catch (error) {\n      this.logger.error('Failed to get key info.', error, clientMetadata);\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Delete multiple keys\n   * @param clientMetadata\n   * @param keys\n   */\n  public async deleteKeys(\n    clientMetadata: ClientMetadata,\n    keys: RedisString[],\n  ): Promise<DeleteKeysResponse> {\n    try {\n      this.logger.debug('Deleting keys', clientMetadata);\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const result = (await client.sendCommand([\n        BrowserToolKeysCommands.Del,\n        ...keys,\n      ])) as number;\n\n      if (!result) {\n        this.logger.error(\n          'Failed to delete keys. Not Found keys',\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      this.logger.debug('Succeed to delete keys', clientMetadata);\n\n      return { affected: result };\n    } catch (error) {\n      this.logger.error('Failed to delete keys.', error, clientMetadata);\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Rename particular key\n   * @param clientMetadata\n   * @param dto\n   */\n  public async renameKey(\n    clientMetadata: ClientMetadata,\n    dto: RenameKeyDto,\n  ): Promise<RenameKeyResponse> {\n    try {\n      this.logger.debug('Renaming key', clientMetadata);\n      const { keyName, newKeyName } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = await client.sendCommand([\n        BrowserToolKeysCommands.RenameNX,\n        keyName,\n        newKeyName,\n      ]);\n\n      if (!result) {\n        this.logger.error(\n          `Failed to rename key. ${ERROR_MESSAGES.NEW_KEY_NAME_EXIST} key: ${newKeyName}`,\n        );\n        return Promise.reject(\n          new BadRequestException(ERROR_MESSAGES.NEW_KEY_NAME_EXIST),\n        );\n      }\n      this.logger.debug('Succeed to rename key', clientMetadata);\n      return plainToInstance(RenameKeyResponse, { keyName: newKeyName });\n    } catch (error) {\n      this.logger.error('Failed to rename key.', error, clientMetadata);\n      throw catchAclError(error);\n    }\n  }\n\n  private static readonly SEARCHABLE_TYPES = [\n    RedisDataType.Hash,\n    RedisDataType.JSON,\n  ];\n\n  private static readonly SCAN_SEARCHABLE_COUNT = 500;\n\n  /**\n   * Check if namespaces contain searchable keys (hash/json)\n   * Uses SCAN with TYPE filter, fully iterating until a match\n   * is found or the keyspace is exhausted\n   * @param clientMetadata\n   * @param dto\n   */\n  public async getNamespaceSearchable(\n    clientMetadata: ClientMetadata,\n    dto: GetNamespaceSearchableDto,\n  ): Promise<NamespaceSearchableResponse[]> {\n    try {\n      this.logger.debug('Checking namespace searchable keys.', clientMetadata);\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const results = await Promise.all(\n        dto.prefixes.map((prefix) =>\n          this.findFirstSearchableKey(client, prefix),\n        ),\n      );\n\n      this.logger.debug(\n        'Succeed to check namespace searchable keys.',\n        clientMetadata,\n      );\n\n      return results;\n    } catch (error) {\n      this.logger.error(\n        `Failed to check namespace searchable keys. ${error.message}.`,\n        error,\n        clientMetadata,\n      );\n\n      if (error.message?.includes(RedisErrorCodes.CommandSyntaxError)) {\n        return dto.prefixes.map((prefix) => ({ prefix }));\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  private async findFirstSearchableKey(\n    client: any,\n    prefix: string,\n  ): Promise<NamespaceSearchableResponse> {\n    const scanCursors = KeysService.SEARCHABLE_TYPES.map(() => '0');\n    const isTypeExhausted = KeysService.SEARCHABLE_TYPES.map(() => false);\n\n    while (!isTypeExhausted.every(Boolean)) {\n      const scanCommands: RedisClientCommand[] = [];\n      const pendingTypeIndexes: number[] = [];\n\n      for (\n        let typeIndex = 0;\n        typeIndex < KeysService.SEARCHABLE_TYPES.length;\n        typeIndex++\n      ) {\n        if (isTypeExhausted[typeIndex]) continue;\n        pendingTypeIndexes.push(typeIndex);\n        scanCommands.push([\n          BrowserToolKeysCommands.Scan,\n          scanCursors[typeIndex],\n          'MATCH',\n          `${prefix}*`,\n          'COUNT',\n          `${KeysService.SCAN_SEARCHABLE_COUNT}`,\n          'TYPE',\n          KeysService.SEARCHABLE_TYPES[typeIndex],\n        ]);\n      }\n\n      const pipelineResults = (await client.sendPipeline(scanCommands, {\n        replyEncoding: 'utf8',\n      })) as [any, [string, string[]]][];\n\n      for (\n        let resultIndex = 0;\n        resultIndex < pipelineResults.length;\n        resultIndex++\n      ) {\n        const typeIndex = pendingTypeIndexes[resultIndex];\n        const [scanError, scanResult] = pipelineResults[resultIndex];\n\n        if (scanError || !scanResult) {\n          isTypeExhausted[typeIndex] = true;\n          continue;\n        }\n\n        const [nextCursor, matchedKeys] = scanResult;\n\n        if (matchedKeys?.length > 0) {\n          return plainToInstance(NamespaceSearchableResponse, {\n            prefix,\n            key: {\n              name: matchedKeys[0],\n              type: KeysService.SEARCHABLE_TYPES[typeIndex],\n            },\n          });\n        }\n\n        scanCursors[typeIndex] = nextCursor;\n        if (nextCursor === '0') {\n          isTypeExhausted[typeIndex] = true;\n        }\n      }\n    }\n\n    return plainToInstance(NamespaceSearchableResponse, { prefix });\n  }\n\n  public async updateTtl(\n    clientMetadata: ClientMetadata,\n    dto: UpdateKeyTtlDto,\n  ): Promise<KeyTtlResponse> {\n    if (dto.ttl === -1) {\n      return await this.removeKeyExpiration(clientMetadata, dto);\n    }\n    return await this.setKeyExpiration(clientMetadata, dto);\n  }\n\n  /**\n   * Set ttl for particular key\n   * @param clientMetadata\n   * @param dto\n   */\n  public async setKeyExpiration(\n    clientMetadata: ClientMetadata,\n    dto: UpdateKeyTtlDto,\n  ): Promise<KeyTtlResponse> {\n    try {\n      this.logger.debug('Setting a timeout on key.', clientMetadata);\n      const { keyName, ttl } = dto;\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const result = await client.sendCommand([\n        BrowserToolKeysCommands.Expire,\n        keyName,\n        ttl,\n      ]);\n\n      if (!result) {\n        this.logger.error(\n          `Failed to set a timeout on key. ${ERROR_MESSAGES.KEY_NOT_EXIST} key: ${keyName}`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      this.logger.debug('Succeed to set a timeout on key.', clientMetadata);\n      return {\n        ttl: ttl >= 0 ? ttl : -2,\n      };\n    } catch (error) {\n      this.logger.error(\n        'Failed to set a timeout on key.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Remove existing ttl for particular key\n   * @param clientMetadata\n   * @param dto\n   */\n  public async removeKeyExpiration(\n    clientMetadata: ClientMetadata,\n    dto: UpdateKeyTtlDto,\n  ): Promise<KeyTtlResponse> {\n    try {\n      this.logger.debug(\n        'Removing the existing timeout on key.',\n        clientMetadata,\n      );\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const currentTtl = await client.sendCommand([\n        BrowserToolKeysCommands.Ttl,\n        dto.keyName,\n      ]);\n\n      if (currentTtl === -2) {\n        this.logger.error(\n          `Failed to remove the existing timeout on key. ${ERROR_MESSAGES.KEY_NOT_EXIST} key: ${dto.keyName}`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      if (currentTtl > 0) {\n        await client.sendCommand([\n          BrowserToolKeysCommands.Persist,\n          dto.keyName,\n        ]);\n      }\n\n      this.logger.debug(\n        'Succeed to remove the existing timeout on key.',\n        clientMetadata,\n      );\n      return { ttl: -1 };\n    } catch (error) {\n      this.logger.error(\n        'Failed to remove the existing timeout on key.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/scanner.interface.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { RedisString } from 'src/common/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport interface IScannerGetKeysArgs {\n  cursor: string;\n  count?: number;\n  match?: string;\n  type?: RedisDataType;\n  keysInfo?: boolean;\n  scanThreshold: number;\n}\n\nexport interface IScannerNodeKeys {\n  total: number;\n  scanned: number;\n  cursor: number;\n  keys: any[];\n  node?: RedisClient;\n  host?: string;\n  port?: number;\n}\n\nexport interface IScannerStrategy {\n  /**\n   * Scan database starting from provided cursor.\n   * For Cluster databases will scan each node\n   * For cluster database used custom cursor composed of each cursor per node (172.17.0.1:7001@-1||172.17.0.1:7002@33)\n   * @param client\n   * @param args\n   */\n  getKeys(\n    client: RedisClient,\n    args: IScannerGetKeysArgs,\n  ): Promise<IScannerNodeKeys[]>;\n\n  getKeysInfo(\n    client: RedisClient,\n    keys: RedisString[],\n    type?: RedisDataType,\n    includeSize?: boolean,\n    includeTTL?: boolean,\n  ): Promise<GetKeyInfoResponse[]>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/scanner.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { Scanner } from 'src/modules/browser/keys/scanner/scanner';\nimport { mockSettingsService } from 'src/__mocks__';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { ConnectionType } from 'src/modules/database/entities/database.entity';\nimport { ClusterScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/cluster.scanner.strategy';\nimport { StandaloneScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/standalone.scanner.strategy';\n\ndescribe('Scanner Manager', () => {\n  let scanner;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        Scanner,\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n        {\n          provide: StandaloneScannerStrategy,\n          useFactory: () => ConnectionType.STANDALONE,\n        },\n        {\n          provide: ClusterScannerStrategy,\n          useFactory: () => ConnectionType.CLUSTER,\n        },\n      ],\n    }).compile();\n\n    scanner = module.get<Scanner>(Scanner);\n  });\n  it('Should support Standalone strategy for standalone connection type', () => {\n    expect(scanner.getStrategy(ConnectionType.STANDALONE)).toEqual(\n      ConnectionType.STANDALONE,\n    );\n  });\n  it('Should support Standalone strategy for sentinel connection type', () => {\n    expect(scanner.getStrategy(ConnectionType.SENTINEL)).toEqual(\n      ConnectionType.STANDALONE,\n    );\n  });\n  it('Should support Cluster strategy for cluster connection type', () => {\n    expect(scanner.getStrategy(ConnectionType.CLUSTER)).toEqual(\n      ConnectionType.CLUSTER,\n    );\n  });\n  it('Should throw error if no strategy', () => {\n    try {\n      scanner.getStrategy(ConnectionType.NOT_CONNECTED);\n      fail();\n    } catch (e) {\n      expect(e.message).toEqual(\n        `Unsupported scan strategy: ${ConnectionType.NOT_CONNECTED}`,\n      );\n    }\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/scanner.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { ClusterScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/cluster.scanner.strategy';\nimport { StandaloneScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/standalone.scanner.strategy';\nimport { ScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/scanner.strategy';\nimport { RedisClientConnectionType } from 'src/modules/redis/client';\n\n@Injectable()\nexport class Scanner {\n  constructor(\n    private readonly standaloneStrategy: StandaloneScannerStrategy,\n    private readonly clusterStrategy: ClusterScannerStrategy,\n  ) {}\n\n  getStrategy(connectionType: RedisClientConnectionType): ScannerStrategy {\n    switch (connectionType) {\n      case RedisClientConnectionType.STANDALONE:\n      case RedisClientConnectionType.SENTINEL:\n        return this.standaloneStrategy;\n      case RedisClientConnectionType.CLUSTER:\n        return this.clusterStrategy;\n      default:\n        throw new Error(`Unsupported scan strategy: ${connectionType}`);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/strategies/cluster.scanner.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockRedisNoPermError,\n  mockBrowserClientMetadata,\n  mockClusterRedisClient,\n  generateMockRedisClient,\n  MockRedisClient,\n} from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  GetKeyInfoResponse,\n  GetKeysDto,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { IScannerNodeKeys } from 'src/modules/browser/keys/scanner/scanner.interface';\nimport * as Utils from 'src/modules/redis/utils/keys.util';\nimport { ClusterScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/cluster.scanner.strategy';\n\nconst getKeyInfoResponse = {\n  name: 'testString',\n  type: 'string',\n  ttl: -1,\n  size: 50,\n};\nconst mockNodeEmptyResult: IScannerNodeKeys = {\n  total: 0,\n  scanned: 0,\n  cursor: 0,\n  keys: [],\n};\nconst mockClusterNodes = [\n  { host: '172.1.0.1', port: 7000 },\n  { host: '172.1.0.1', port: 7001 },\n  { host: '172.1.0.1', port: 7002 },\n];\n\nconst mockClusterNodesEmptyResult: IScannerNodeKeys[] = [\n  { ...mockNodeEmptyResult, ...mockClusterNodes[0] },\n  { ...mockNodeEmptyResult, ...mockClusterNodes[1] },\n  { ...mockNodeEmptyResult, ...mockClusterNodes[2] },\n];\n\nconst mockGetTotalResponse0: number = 0;\nconst mockGetTotalResponse1: number = 1;\nconst mockGetTotalResponse10: number = 10;\nconst mockGetTotalResponse1000: number = 1000;\nconst mockGetTotalResponse2000: number = 2000;\nconst mockGetTotalResponse3000: number = 3000;\nconst mockGetTotalResponse1000000: number = 1000000;\n\nconst mockGetKeysInfoFn = jest.fn().mockImplementation(async (_, keys) => {\n  if (keys.length < 1) {\n    return [];\n  }\n  return new Array(keys.length).fill(getKeyInfoResponse);\n});\n\nconst mockKeyInfo: GetKeyInfoResponse = {\n  name: 'testString',\n  type: 'string',\n  ttl: -1,\n  size: 50,\n};\n\ndescribe('Cluster Scanner Strategy', () => {\n  let strategy: ClusterScannerStrategy;\n  let mockNode1: MockRedisClient;\n  let mockNode2: MockRedisClient;\n  let mockNode3: MockRedisClient;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ClusterScannerStrategy],\n    }).compile();\n\n    strategy = module.get(ClusterScannerStrategy);\n    mockGetKeysInfoFn.mockClear();\n\n    mockNode1 = generateMockRedisClient(\n      mockBrowserClientMetadata,\n      jest.fn(),\n      mockClusterNodes[0],\n    );\n    mockNode2 = generateMockRedisClient(\n      mockBrowserClientMetadata,\n      jest.fn(),\n      mockClusterNodes[1],\n    );\n    mockNode3 = generateMockRedisClient(\n      mockBrowserClientMetadata,\n      jest.fn(),\n      mockClusterNodes[2],\n    );\n\n    mockClusterRedisClient.nodes.mockResolvedValue([\n      mockNode1,\n      mockNode2,\n      mockNode3,\n    ]);\n  });\n\n  describe('getKeys', () => {\n    const getKeysDto: GetKeysDto = {\n      cursor: '0',\n      count: 15,\n      keysInfo: true,\n      scanThreshold: 1000,\n    };\n\n    it('should return appropriate value with filter by type', async () => {\n      const args = {\n        ...getKeysDto,\n        type: RedisDataType.String,\n        match: 'pattern*',\n      };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      when(mockNode1.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode2.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode3.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]);\n\n      const result = await strategy.getKeys(mockClusterRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockClusterNodesEmptyResult[0],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n        {\n          ...mockClusterNodesEmptyResult[1],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n        {\n          ...mockClusterNodesEmptyResult[2],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n      expect(mockNode2.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n    });\n    it('should work with custom cursor', async () => {\n      const args = {\n        ...getKeysDto,\n        type: RedisDataType.String,\n        match: 'pattern*',\n        cursor: '172.1.0.1:7000@11||172.1.0.1:7001@22||172.1.0.1:7002@33',\n      };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      when(mockNode1.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode2.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode3.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]);\n\n      const result = await strategy.getKeys(mockClusterRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockClusterNodesEmptyResult[0],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n        {\n          ...mockClusterNodesEmptyResult[1],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n        {\n          ...mockClusterNodesEmptyResult[2],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '11',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n      expect(mockNode2.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '22',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '33',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n    });\n    it('should skip nodes with negative cursors custom cursor', async () => {\n      const args = {\n        ...getKeysDto,\n        cursor: '172.1.0.1:7000@0||172.1.0.1:7001@-1||172.1.0.1:7002@-22',\n      };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse1);\n\n      when(mockNode1.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [Buffer.from(getKeyInfoResponse.name)]]);\n\n      strategy.getKeysInfo = mockGetKeysInfoFn;\n\n      const result = await strategy.getKeys(mockClusterRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockClusterNodesEmptyResult[0],\n          total: mockGetTotalResponse1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toBeCalledTimes(1);\n      expect(mockNode1.sendCommand).toBeCalledTimes(1);\n      expect(mockNode1.sendCommand).toBeCalledWith([\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode2.sendCommand).toBeCalledTimes(0);\n      expect(mockNode3.sendCommand).toBeCalledTimes(0);\n    });\n    it('should call scan 3,2,1 times per nodes and return appropriate value', async () => {\n      const args = { ...getKeysDto };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse3000);\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse2000);\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse1000);\n\n      // Node1 mocks (3 iterations)\n      when(mockNode1.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '0',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['1', [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode1.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '1',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['2', [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode1.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '2',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['0', [Buffer.from(getKeyInfoResponse.name)]]);\n      // Node2 mocks (2 iterations)\n      when(mockNode2.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '0',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['1', [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode2.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '1',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['0', [Buffer.from(getKeyInfoResponse.name)]]);\n      // Node3 mocks (1 iteration)\n      when(mockNode3.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue(['0', [Buffer.from(getKeyInfoResponse.name)]]);\n\n      strategy.getKeysInfo = mockGetKeysInfoFn;\n\n      const result = await strategy.getKeys(mockClusterRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockClusterNodesEmptyResult[0],\n          total: mockGetTotalResponse3000,\n          scanned: getKeysDto.count * 3,\n          keys: new Array(3).fill(getKeyInfoResponse),\n        },\n        {\n          ...mockClusterNodesEmptyResult[1],\n          total: mockGetTotalResponse2000,\n          scanned: getKeysDto.count * 2,\n          keys: new Array(2).fill(getKeyInfoResponse),\n        },\n        {\n          ...mockClusterNodesEmptyResult[2],\n          total: mockGetTotalResponse1000,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n\n      expect(mockNode1.sendCommand).toBeCalledTimes(3);\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(2, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(3, [\n        BrowserToolKeysCommands.Scan,\n        '2',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n\n      expect(mockNode2.sendCommand).toBeCalledTimes(2);\n      expect(mockNode2.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode2.sendCommand).toHaveBeenNthCalledWith(2, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n\n      expect(mockNode3.sendCommand).toBeCalledTimes(1);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n    });\n    it.only('should call scan 3,2,N times per nodes until threshold exceeds', async () => {\n      const args = { ...getKeysDto, count: 100 };\n      const expectedNode3CallsBeforeThreshold = Math.trunc(\n        // -5 is number of scans for node1 (3) and node2 (2)\n        // since threshold applied for sum of all nodes scanned\n        getKeysDto.scanThreshold / args.count - 5,\n      );\n\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse3000);\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse2000);\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValueOnce(mockGetTotalResponse1000000);\n\n      // Node1 mocks (3 iterations)\n      when(mockNode1.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '0',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['1', [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode1.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '1',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['2', [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode1.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '2',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['0', [Buffer.from(getKeyInfoResponse.name)]]);\n      // Node2 mocks (2 iterations)\n      when(mockNode2.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '0',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['1', [Buffer.from(getKeyInfoResponse.name)]]);\n      when(mockNode2.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '1',\n          'MATCH',\n          '*',\n          'COUNT',\n          args.count,\n        ])\n        .mockResolvedValue(['0', [Buffer.from(getKeyInfoResponse.name)]]);\n      // Node3 mocks (infinite number of iterations limited by threshold)\n      when(mockNode3.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue(['1', []]);\n\n      strategy.getKeysInfo = mockGetKeysInfoFn;\n\n      const result = await strategy.getKeys(mockClusterRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockClusterNodesEmptyResult[0],\n          total: mockGetTotalResponse3000,\n          scanned: args.count * 3,\n          keys: new Array(3).fill(getKeyInfoResponse),\n        },\n        {\n          ...mockClusterNodesEmptyResult[1],\n          total: mockGetTotalResponse2000,\n          scanned: args.count * 2,\n          keys: new Array(2).fill(getKeyInfoResponse),\n        },\n        {\n          ...mockClusterNodesEmptyResult[2],\n          total: mockGetTotalResponse1000000,\n          cursor: 1,\n          scanned:\n            Math.trunc(getKeysDto.scanThreshold / args.count) * args.count -\n            5 * args.count, // 5 = scan for other shards (3 and 2)\n          keys: [],\n        },\n      ]);\n\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n\n      expect(mockNode1.sendCommand).toBeCalledTimes(3);\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(2, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode1.sendCommand).toHaveBeenNthCalledWith(3, [\n        BrowserToolKeysCommands.Scan,\n        '2',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n\n      expect(mockNode2.sendCommand).toBeCalledTimes(2);\n      expect(mockNode2.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode2.sendCommand).toHaveBeenNthCalledWith(2, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n\n      expect(mockNode3.sendCommand).toBeCalledTimes(\n        expectedNode3CallsBeforeThreshold,\n      );\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(2, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(3, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(4, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        args.count,\n      ]);\n      expect(mockNode3.sendCommand).toHaveBeenNthCalledWith(\n        expectedNode3CallsBeforeThreshold,\n        [BrowserToolKeysCommands.Scan, '1', 'MATCH', '*', 'COUNT', args.count],\n      );\n    });\n    it('should not call scan when total is 0', async () => {\n      const args = { ...getKeysDto, count: undefined };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse0);\n\n      strategy.getKeysInfo = mockGetKeysInfoFn;\n\n      const result = await strategy.getKeys(mockClusterRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockClusterNodesEmptyResult[0],\n          total: 0,\n          scanned: 0,\n          keys: [],\n        },\n        {\n          ...mockClusterNodesEmptyResult[1],\n          total: 0,\n          scanned: 0,\n          keys: [],\n        },\n        {\n          ...mockClusterNodesEmptyResult[2],\n          total: 0,\n          scanned: 0,\n          keys: [],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toBeCalledTimes(0);\n      expect(mockNode1.sendCommand).toBeCalledTimes(0);\n      expect(mockNode2.sendCommand).toBeCalledTimes(0);\n      expect(mockNode3.sendCommand).toBeCalledTimes(0);\n    });\n    it('should throw error if incorrect cursor passed', async () => {\n      try {\n        const args = {\n          ...getKeysDto,\n          cursor: '172.1.0.1asd00@0||172.1.0.1:7001@0||172.1.0.1:7002@0',\n        };\n        await strategy.getKeys(mockClusterRedisClient, args);\n        fail();\n      } catch (err) {\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.INCORRECT_CLUSTER_CURSOR_FORMAT,\n        );\n      }\n    });\n    it('should throw error on scan command', async () => {\n      const args = { ...getKeysDto };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SCAN',\n      };\n\n      when(mockNode1.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockRejectedValue(replyError);\n\n      try {\n        await strategy.getKeys(mockClusterRedisClient, args);\n        fail();\n      } catch (err) {\n        expect(err.message).toEqual(replyError.message);\n      }\n    });\n    describe('get keys by glob patter', () => {\n      beforeEach(async () => {\n        jest\n          .spyOn(Utils, 'getTotalKeys')\n          .mockResolvedValue(mockGetTotalResponse10);\n\n        strategy['scanNodes'] = jest.fn();\n      });\n      it(\"should call scan when math contains '?' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 'test?tring' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(strategy['scanNodes']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '*' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 'test*' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(strategy['scanNodes']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '[ae]' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't[ae]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(strategy['scanNodes']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '[a-e]' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't[a-e]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(strategy['scanNodes']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '[^e]' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't[^e]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(strategy['scanNodes']).toHaveBeenCalled();\n      });\n      it('should not call scan when math contains escaped glob', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't\\\\[a-e\\\\]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(strategy['scanNodes']).not.toHaveBeenCalled();\n      });\n    });\n    describe('find exact key', () => {\n      const key = getKeyInfoResponse.name;\n      beforeEach(async () => {\n        jest\n          .spyOn(Utils, 'getTotalKeys')\n          .mockResolvedValue(mockGetTotalResponse10);\n\n        strategy['scanNodes'] = jest.fn();\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n      });\n      it('should find exact key when match is not glob patter', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: key };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        const result = await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockClusterNodesEmptyResult[0],\n            total: 10,\n            scanned: 10,\n            keys: [getKeyInfoResponse],\n          },\n          {\n            ...mockClusterNodesEmptyResult[1],\n            total: 10,\n            scanned: 10,\n          },\n          {\n            ...mockClusterNodesEmptyResult[2],\n            total: 10,\n            scanned: 10,\n          },\n        ]);\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockClusterRedisClient,\n          [Buffer.from(key)],\n        );\n        expect(strategy['scanNodes']).not.toHaveBeenCalled();\n      });\n      it('should find exact key when match is escaped glob patter', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 'testString\\\\*' };\n        const searchPattern = 'testString*';\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([{ ...getKeyInfoResponse, name: searchPattern }]);\n\n        const result = await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockClusterNodesEmptyResult[0],\n            total: 10,\n            scanned: 10,\n            keys: [{ ...getKeyInfoResponse, name: searchPattern }],\n          },\n          {\n            ...mockClusterNodesEmptyResult[1],\n            total: 10,\n            scanned: 10,\n          },\n          {\n            ...mockClusterNodesEmptyResult[2],\n            total: 10,\n            scanned: 10,\n          },\n        ]);\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockClusterRedisClient,\n          [Buffer.from(searchPattern)],\n        );\n        expect(strategy['scanNodes']).not.toHaveBeenCalled();\n      });\n      it('should find exact key with correct type', async () => {\n        const dto: GetKeysDto = {\n          ...getKeysDto,\n          match: key,\n          type: RedisDataType.String,\n        };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        const result = await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockClusterNodesEmptyResult[0],\n            total: 10,\n            scanned: 10,\n            keys: [getKeyInfoResponse],\n          },\n          {\n            ...mockClusterNodesEmptyResult[1],\n            total: 10,\n            scanned: 10,\n          },\n          {\n            ...mockClusterNodesEmptyResult[2],\n            total: 10,\n            scanned: 10,\n          },\n        ]);\n\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockClusterRedisClient,\n          [Buffer.from(key)],\n        );\n        expect(strategy['scanNodes']).not.toHaveBeenCalled();\n      });\n      it('should return empty array if key not exist', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: key };\n        strategy.getKeysInfo = jest.fn().mockResolvedValue([\n          {\n            name: 'testString',\n            type: 'none',\n            ttl: -2,\n            size: null,\n          },\n        ]);\n\n        const result = await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockClusterNodesEmptyResult[0],\n            total: 10,\n            scanned: 10,\n            keys: [],\n          },\n          {\n            ...mockClusterNodesEmptyResult[1],\n            total: 10,\n            scanned: 10,\n          },\n          {\n            ...mockClusterNodesEmptyResult[2],\n            total: 10,\n            scanned: 10,\n          },\n        ]);\n\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockClusterRedisClient,\n          [Buffer.from(key)],\n        );\n        expect(strategy['scanNodes']).not.toHaveBeenCalled();\n      });\n      it('should return empty array if key has wrong type', async () => {\n        const dto: GetKeysDto = {\n          ...getKeysDto,\n          match: key,\n          type: RedisDataType.Hash,\n        };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        const result = await strategy.getKeys(mockClusterRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockClusterNodesEmptyResult[0],\n            total: 10,\n            scanned: 10,\n            keys: [],\n          },\n          {\n            ...mockClusterNodesEmptyResult[1],\n            total: 10,\n            scanned: 10,\n          },\n          {\n            ...mockClusterNodesEmptyResult[2],\n            total: 10,\n            scanned: 10,\n          },\n        ]);\n\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockClusterRedisClient,\n          [Buffer.from(key)],\n        );\n        expect(strategy['scanNodes']).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('getKeysInfo', () => {\n    const keys = ['key1', 'key2'];\n\n    beforeEach(() => {\n      when(mockClusterRedisClient.sendPipeline)\n        .calledWith(\n          [\n            expect.arrayContaining([BrowserToolKeysCommands.Ttl]),\n            expect.arrayContaining(['memory', 'usage']),\n            expect.arrayContaining([BrowserToolKeysCommands.Type]),\n          ],\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue([\n          [null, -1],\n          [null, 50],\n          [null, 'string'],\n        ]);\n      when(mockClusterRedisClient.sendPipeline)\n        .calledWith(\n          [\n            expect.arrayContaining([BrowserToolKeysCommands.Ttl]),\n            expect.arrayContaining(['memory', 'usage']),\n          ],\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue([\n          [null, 999],\n          [null, 555],\n        ]);\n    });\n    it('should return correct keys info (cluster)', async () => {\n      const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({\n        ...mockKeyInfo,\n        name: key,\n      }));\n\n      const result = await strategy.getKeysInfo(mockClusterRedisClient, keys);\n\n      expect(result).toEqual(mockResult);\n    });\n    it('should not call TYPE pipeline for keys with known type', async () => {\n      const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({\n        ...mockKeyInfo,\n        ttl: 999,\n        size: 555,\n        name: key,\n      }));\n\n      const result = await strategy.getKeysInfo(\n        mockClusterRedisClient,\n        keys,\n        RedisDataType.String,\n      );\n\n      expect(result).toEqual(mockResult);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/strategies/cluster.scanner.strategy.ts",
    "content": "import { isNull, omit, toNumber } from 'lodash';\nimport config from 'src/utils/config';\nimport { isRedisGlob, unescapeRedisGlob } from 'src/utils';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  GetKeyInfoResponse,\n  GetKeysWithDetailsResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { parseClusterCursor } from 'src/modules/browser/utils';\nimport { Injectable } from '@nestjs/common';\nimport { ScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/scanner.strategy';\nimport {\n  RedisClient,\n  RedisClientCommand,\n  RedisClientNodeRole,\n} from 'src/modules/redis/client';\nimport { getTotalKeys } from 'src/modules/redis/utils';\nimport { RedisString } from 'src/common/constants';\nimport {\n  IScannerGetKeysArgs,\n  IScannerNodeKeys,\n} from 'src/modules/browser/keys/scanner/scanner.interface';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\n\n@Injectable()\nexport class ClusterScannerStrategy extends ScannerStrategy {\n  private async getNodesToScan(\n    client: RedisClient,\n    initialCursor: string,\n  ): Promise<IScannerNodeKeys[]> {\n    const nodesClients = await client.nodes(RedisClientNodeRole.PRIMARY);\n\n    if (Number.isNaN(toNumber(initialCursor))) {\n      // parse existing cursor\n      const nodes = parseClusterCursor(initialCursor);\n      // add client to each node\n      nodes.forEach((node, index) => {\n        nodes[index].node = nodesClients.find(\n          ({ options: { host, port, natHost, natPort } }) =>\n            (host === node.host && port === node.port) ||\n            (natHost === node.host && natPort === node.port),\n        );\n      });\n\n      return nodes;\n    }\n\n    return nodesClients.map(\n      (node) =>\n        ({\n          node,\n          host: node.options.natHost || node.options.host,\n          port: node.options.natPort || node.options.port,\n          cursor: 0,\n          keys: [],\n          total: 0,\n          scanned: 0,\n        }) as any,\n    );\n  }\n\n  private async calculateNodesTotalKeys(\n    nodes: IScannerNodeKeys[],\n  ): Promise<void> {\n    await Promise.all(\n      nodes.map(async (node) => {\n        // eslint-disable-next-line no-param-reassign\n        node.total = await getTotalKeys(node?.node);\n      }),\n    );\n  }\n\n  /**\n   * Scan keys for each node and mutates input data\n   */\n  private async scanNodes(\n    nodes: IScannerNodeKeys[],\n    match: string,\n    count: number,\n    type?: RedisDataType,\n  ): Promise<void> {\n    await Promise.all(\n      nodes.map(async (node) => {\n        // ignore full scanned nodes or nodes with no items\n        if ((node.cursor === 0 && node.scanned !== 0) || node.total === 0) {\n          return;\n        }\n\n        const commandArgs = [`${node.cursor}`, 'MATCH', match, 'COUNT', count];\n        if (type) {\n          commandArgs.push('TYPE', type);\n        }\n\n        const result = await node.node.sendCommand([\n          BrowserToolKeysCommands.Scan,\n          ...commandArgs,\n        ]);\n\n        /* eslint-disable no-param-reassign */\n        node.cursor = parseInt(result[0], 10);\n        node.keys = node.keys.concat(result[1]);\n        node.scanned += count;\n        /* eslint-enable no-param-reassign */\n      }),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getKeys(\n    client: RedisClient,\n    args: IScannerGetKeysArgs,\n  ): Promise<GetKeysWithDetailsResponse[]> {\n    const match = args.match !== undefined ? args.match : '*';\n    const count = args.count || REDIS_SCAN_CONFIG.countDefault;\n    const scanThreshold = args.scanThreshold || REDIS_SCAN_CONFIG.scanThreshold;\n    const nodes = await this.getNodesToScan(client, args.cursor);\n\n    await this.calculateNodesTotalKeys(nodes);\n\n    if (!isRedisGlob(match)) {\n      const keyName = Buffer.from(unescapeRedisGlob(match));\n      nodes.forEach((node) => {\n        // eslint-disable-next-line no-param-reassign\n        node.cursor = 0;\n        // eslint-disable-next-line no-param-reassign\n        node.scanned = isNull(node.total) ? 1 : node.total;\n      });\n      nodes[0].keys = await this.getKeysInfo(\n        client,\n        [keyName],\n        undefined,\n        true,\n        true,\n      );\n      nodes[0].keys = nodes[0].keys.filter((key: GetKeyInfoResponse) => {\n        if (key.ttl === -2) {\n          return false;\n        }\n        if (args.type) {\n          return key.type === args.type;\n        }\n        return true;\n      });\n      return nodes.map((node) => omit(node, 'node'));\n    }\n\n    let allNodesScanned = false;\n    while (\n      !allNodesScanned &&\n      nodes.reduce((prev, cur) => prev + cur.keys.length, 0) < count &&\n      ((nodes.reduce((prev, cur) => prev + cur.total, 0) < scanThreshold &&\n        nodes.find((node) => !!node.cursor)) ||\n        nodes.reduce((prev, cur) => prev + cur.scanned, 0) < scanThreshold)\n    ) {\n      await this.scanNodes(nodes, match, count, args.type);\n      allNodesScanned = !nodes.some((node) => node.cursor !== 0);\n    }\n\n    await Promise.all(\n      nodes.map(async (node) => {\n        if (node.keys.length && args.keysInfo) {\n          // eslint-disable-next-line no-param-reassign\n          node.keys = await this.getKeysInfo(\n            node.node,\n            node.keys,\n            args.type,\n            true,\n            true,\n          );\n        } else {\n          // eslint-disable-next-line no-param-reassign\n          node.keys = node.keys.map((name) => ({\n            name,\n            type: args.type || undefined,\n          }));\n        }\n      }),\n    );\n\n    return nodes.map((node) => omit(node, 'node'));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getKeysInfo(\n    client: RedisClient,\n    keys: RedisString[],\n    filterType?: RedisDataType,\n    includeSize?: boolean,\n    includeTTL?: boolean,\n  ): Promise<GetKeyInfoResponse[]> {\n    return Promise.all(\n      keys.map(async (key) => {\n        const commands: RedisClientCommand[] = [];\n        const responseMap = {\n          ttl: null,\n          size: null,\n          type: null,\n        };\n\n        if (includeTTL) {\n          responseMap.ttl = commands.length;\n          commands.push([BrowserToolKeysCommands.Ttl, key]);\n        }\n\n        if (includeSize) {\n          responseMap.size = commands.length;\n          commands.push(['memory', 'usage', key, 'samples', '0']);\n        }\n\n        if (!filterType) {\n          responseMap.type = commands.length;\n          commands.push([BrowserToolKeysCommands.Type, key]);\n        }\n\n        const result = (await client.sendPipeline(commands, {\n          replyEncoding: 'utf8',\n        })) as any[];\n\n        if (filterType) {\n          responseMap.type = commands.length;\n          result.push([null, filterType]);\n        }\n\n        return {\n          name: key,\n          type: result[responseMap.type]?.[1],\n          ttl:\n            responseMap.ttl !== null ? result[responseMap.ttl][1] : undefined,\n          size:\n            responseMap.size !== null ? result[responseMap.size][1] : undefined,\n        };\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/strategies/scanner.strategy.ts",
    "content": "import {\n  GetKeyInfoResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { RedisString } from 'src/common/constants';\nimport { Injectable } from '@nestjs/common';\nimport { RedisClient } from 'src/modules/redis/client';\nimport {\n  IScannerStrategy,\n  IScannerGetKeysArgs,\n  IScannerNodeKeys,\n} from 'src/modules/browser/keys/scanner/scanner.interface';\n\n@Injectable()\nexport abstract class ScannerStrategy implements IScannerStrategy {\n  abstract getKeys(\n    client: RedisClient,\n    args: IScannerGetKeysArgs,\n  ): Promise<IScannerNodeKeys[]>;\n\n  abstract getKeysInfo(\n    client: RedisClient,\n    keys: RedisString[],\n    filterType?: RedisDataType,\n    includeSize?: boolean,\n    includeTTL?: boolean,\n  ): Promise<GetKeyInfoResponse[]>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/strategies/standalone.scanner.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { mockRedisNoPermError, mockStandaloneRedisClient } from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport config from 'src/utils/config';\nimport {\n  GetKeyInfoResponse,\n  GetKeysDto,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { IScannerNodeKeys } from 'src/modules/browser/keys/scanner/scanner.interface';\nimport * as Utils from 'src/modules/redis/utils/keys.util';\nimport { StandaloneScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/standalone.scanner.strategy';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\n\nconst getKeyInfoResponse = {\n  name: 'testString',\n  type: 'string',\n  ttl: -1,\n  size: 50,\n};\nconst mockNodeEmptyResult: IScannerNodeKeys = {\n  total: 0,\n  scanned: 0,\n  cursor: 0,\n  keys: [],\n};\n\nconst mockGetTotalResponse1: number = 1;\nconst mockGetTotalResponse1000000: number = 1000000;\nconst mockGetTotalResponse0: number = 0;\nconst mockGetTotalResponse10: number = 10;\n\ndescribe('StandaloneScannerStrategy', () => {\n  let strategy: StandaloneScannerStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [StandaloneScannerStrategy],\n    }).compile();\n\n    strategy = module.get(StandaloneScannerStrategy);\n  });\n\n  describe('getKeys', () => {\n    const getKeysDto: GetKeysDto = {\n      cursor: '0',\n      count: 15,\n      keysInfo: true,\n      scanThreshold: 1000,\n    };\n    it('should return appropriate value with filter by type', async () => {\n      const args = {\n        ...getKeysDto,\n        type: RedisDataType.String,\n        match: 'pattern*',\n      };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue(['0', [getKeyInfoResponse.name]]);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]);\n\n      const result = await strategy.getKeys(mockStandaloneRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockNodeEmptyResult,\n          total: 1,\n          scanned: getKeysDto.count,\n          keys: [getKeyInfoResponse],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        args.match,\n        'COUNT',\n        args.count,\n        'TYPE',\n        args.type,\n      ]);\n    });\n    it('should scan 2000 items when count provided more then 2k', async () => {\n      const args = { ...getKeysDto, count: 10_000, match: '*' };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [getKeyInfoResponse.name]]);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]);\n\n      const result = await strategy.getKeys(mockStandaloneRedisClient, args);\n\n      expect(result).toEqual([\n        {\n          ...mockNodeEmptyResult,\n          total: 1,\n          scanned: 2000,\n          keys: [getKeyInfoResponse],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        args.match,\n        'COUNT',\n        2000,\n      ]);\n    });\n    it('should return keys names and type only', async () => {\n      const args = {\n        ...getKeysDto,\n        type: RedisDataType.String,\n        match: 'pattern*',\n        keysInfo: false,\n      };\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue([0, [getKeyInfoResponse.name]]);\n\n      strategy.getKeysInfo = jest.fn();\n\n      const result = await strategy.getKeys(mockStandaloneRedisClient, args);\n\n      expect(strategy.getKeysInfo).not.toHaveBeenCalled();\n      expect(result).toEqual([\n        {\n          ...mockNodeEmptyResult,\n          total: 1,\n          scanned: getKeysDto.count,\n          keys: [\n            { name: getKeyInfoResponse.name, type: getKeyInfoResponse.type },\n          ],\n        },\n      ]);\n    });\n    it('should call scan 3 times and return appropriate value', async () => {\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1000000);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '0',\n          'MATCH',\n          '*',\n          'COUNT',\n          getKeysDto.count,\n        ])\n        .mockResolvedValue(['1', new Array(3).fill(getKeyInfoResponse.name)]);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '1',\n          'MATCH',\n          '*',\n          'COUNT',\n          getKeysDto.count,\n        ])\n        .mockResolvedValue(['2', new Array(3).fill(getKeyInfoResponse.name)]);\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Scan,\n          '2',\n          'MATCH',\n          '*',\n          'COUNT',\n          getKeysDto.count,\n        ])\n        .mockResolvedValue(['0', new Array(3).fill(getKeyInfoResponse.name)]);\n\n      strategy.getKeysInfo = jest\n        .fn()\n        .mockResolvedValue(new Array(9).fill(getKeyInfoResponse));\n\n      const result = await strategy.getKeys(\n        mockStandaloneRedisClient,\n        getKeysDto,\n      );\n\n      expect(result).toEqual([\n        {\n          ...mockNodeEmptyResult,\n          total: 1000000,\n          scanned: getKeysDto.count * 3,\n          keys: new Array(9).fill(getKeyInfoResponse),\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalled();\n      expect(mockStandaloneRedisClient.sendCommand).toBeCalledTimes(3);\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Scan,\n        '0',\n        'MATCH',\n        '*',\n        'COUNT',\n        getKeysDto.count,\n      ]);\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenNthCalledWith(2, [\n        BrowserToolKeysCommands.Scan,\n        '1',\n        'MATCH',\n        '*',\n        'COUNT',\n        getKeysDto.count,\n      ]);\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenNthCalledWith(3, [\n        BrowserToolKeysCommands.Scan,\n        '2',\n        'MATCH',\n        '*',\n        'COUNT',\n        getKeysDto.count,\n      ]);\n    });\n    it('should call scan N times until threshold exceeds', async () => {\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1000000);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue(['1', []]);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([]);\n\n      const result = await strategy.getKeys(\n        mockStandaloneRedisClient,\n        getKeysDto,\n      );\n\n      expect(result).toEqual([\n        {\n          ...mockNodeEmptyResult,\n          cursor: 1,\n          total: 1000000,\n          scanned:\n            Math.trunc(getKeysDto.scanThreshold / getKeysDto.count) *\n              getKeysDto.count +\n            getKeysDto.count,\n          keys: [],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalledTimes(0);\n    });\n    it('should call scan N times until threshold exceeds (even when total 0)', async () => {\n      jest.spyOn(Utils, 'getTotalKeys').mockResolvedValue(0);\n\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockResolvedValue(['1', []]);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([]);\n\n      const result = await strategy.getKeys(\n        mockStandaloneRedisClient,\n        getKeysDto,\n      );\n\n      expect(result).toEqual([\n        {\n          ...mockNodeEmptyResult,\n          cursor: 1,\n          total: null,\n          scanned:\n            Math.trunc(getKeysDto.scanThreshold / getKeysDto.count) *\n              getKeysDto.count +\n            getKeysDto.count,\n          keys: [],\n        },\n      ]);\n      expect(strategy.getKeysInfo).toHaveBeenCalledTimes(0);\n    });\n    it('should call scan with required args', async () => {\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse0);\n\n      strategy.getKeysInfo = jest.fn().mockResolvedValue([]);\n      strategy['scan'] = jest.fn().mockResolvedValue(undefined);\n\n      const result = await strategy.getKeys(mockStandaloneRedisClient, {\n        cursor: '0',\n        type: RedisDataType.String,\n        scanThreshold: 1000,\n      });\n\n      expect(strategy['scan']).toHaveBeenLastCalledWith(\n        mockStandaloneRedisClient,\n        mockNodeEmptyResult,\n        '*',\n        REDIS_SCAN_CONFIG.countDefault,\n        1000,\n        RedisDataType.String,\n      );\n      expect(result).toEqual([mockNodeEmptyResult]);\n      expect(strategy.getKeysInfo).toBeCalledTimes(0);\n    });\n    it('should throw error on scan command', async () => {\n      jest\n        .spyOn(Utils, 'getTotalKeys')\n        .mockResolvedValue(mockGetTotalResponse1);\n\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SCAN',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Scan]))\n        .mockRejectedValue(replyError);\n\n      try {\n        await strategy.getKeys(mockStandaloneRedisClient, getKeysDto);\n        fail('Should throw an error');\n      } catch (err) {\n        expect(err.message).toEqual(replyError.message);\n      }\n    });\n    describe('get keys by glob patter', () => {\n      beforeEach(async () => {\n        jest\n          .spyOn(Utils, 'getTotalKeys')\n          .mockResolvedValue(mockGetTotalResponse10);\n\n        strategy['scan'] = jest.fn();\n      });\n      it(\"should call scan when math contains '?' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 'test?tring' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(strategy['scan']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '*' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 'test*' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(strategy['scan']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '[ae]' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't[ae]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(strategy['scan']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '[a-e]' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't[a-e]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(strategy['scan']).toHaveBeenCalled();\n      });\n      it(\"should call scan when math contains '[^e]' glob\", async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't[^e]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(strategy['scan']).toHaveBeenCalled();\n      });\n      it('should not call scan when math contains escaped glob', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 't\\\\[a-e\\\\]stString' };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(strategy['scan']).not.toHaveBeenCalled();\n      });\n    });\n    describe('find exact key', () => {\n      const key = getKeyInfoResponse.name;\n      const total = 10;\n      beforeEach(async () => {\n        jest\n          .spyOn(Utils, 'getTotalKeys')\n          .mockResolvedValue(mockGetTotalResponse10);\n\n        strategy['scan'] = jest.fn();\n      });\n      it('should find exact key when match is not glob patter', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: key };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        const result = await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockNodeEmptyResult,\n            total,\n            scanned: total,\n            keys: [getKeyInfoResponse],\n          },\n        ]);\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockStandaloneRedisClient,\n          [Buffer.from(key)],\n          undefined,\n          true,\n          true,\n        );\n        expect(strategy['scan']).not.toHaveBeenCalled();\n      });\n      it('should find exact key when match is escaped glob patter', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: 'testString\\\\*' };\n        const mockSearchPattern = 'testString*';\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([\n            { ...getKeyInfoResponse, name: mockSearchPattern },\n          ]);\n\n        const result = await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockNodeEmptyResult,\n            total,\n            scanned: total,\n            keys: [{ ...getKeyInfoResponse, name: mockSearchPattern }],\n          },\n        ]);\n        expect(strategy.getKeysInfo).toHaveBeenCalledWith(\n          mockStandaloneRedisClient,\n          [Buffer.from(mockSearchPattern)],\n          undefined,\n          true,\n          true,\n        );\n        expect(strategy['scan']).not.toHaveBeenCalled();\n      });\n      it('should find exact key with correct type', async () => {\n        const dto: GetKeysDto = {\n          ...getKeysDto,\n          match: key,\n          type: RedisDataType.String,\n        };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        const result = await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockNodeEmptyResult,\n            total,\n            scanned: total,\n            keys: [getKeyInfoResponse],\n          },\n        ]);\n      });\n      it('should return empty array if key not exist', async () => {\n        const dto: GetKeysDto = { ...getKeysDto, match: key };\n        strategy.getKeysInfo = jest.fn().mockResolvedValue([\n          {\n            name: 'testString',\n            type: 'none',\n            ttl: -2,\n            size: null,\n          },\n        ]);\n\n        const result = await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockNodeEmptyResult,\n            total,\n            scanned: total,\n            keys: [],\n          },\n        ]);\n      });\n      it('should return empty array if key has wrong type', async () => {\n        const dto: GetKeysDto = {\n          ...getKeysDto,\n          match: key,\n          type: RedisDataType.Hash,\n        };\n        strategy.getKeysInfo = jest\n          .fn()\n          .mockResolvedValue([getKeyInfoResponse]);\n\n        const result = await strategy.getKeys(mockStandaloneRedisClient, dto);\n\n        expect(result).toEqual([\n          {\n            ...mockNodeEmptyResult,\n            total,\n            scanned: total,\n            keys: [],\n          },\n        ]);\n      });\n    });\n  });\n\n  describe('getKeysInfo', () => {\n    const keys = ['key1', 'key2'];\n\n    it('should return correct keys info with all info', async () => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Ttl, key]),\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, -1]));\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [\n            BrowserToolKeysCommands.MemoryUsage,\n            key,\n            'samples',\n            '0',\n          ]),\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, 50]));\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, 'string']));\n\n      const result = await strategy.getKeysInfo(\n        mockStandaloneRedisClient,\n        keys,\n        undefined,\n        true, // includeSize\n        true, // includeTTL\n      );\n\n      const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({\n        name: key,\n        type: 'string',\n        ttl: -1,\n        size: 50,\n      }));\n      expect(result).toEqual(mockResult);\n    });\n\n    it('should not get TTL when includeTTL is false', async () => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, 'string']));\n\n      const result = await strategy.getKeysInfo(\n        mockStandaloneRedisClient,\n        keys,\n        undefined,\n        false, // includeSize\n        false, // includeTTL\n      );\n\n      const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({\n        name: key,\n        type: 'string',\n      }));\n      expect(result).toEqual(mockResult);\n    });\n\n    it('should not get size when includeSize is false', async () => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Ttl, key]),\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, -1]));\n\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, 'string']));\n\n      const result = await strategy.getKeysInfo(\n        mockStandaloneRedisClient,\n        keys,\n        undefined,\n        false, // includeSize\n        true, // includeTTL\n      );\n\n      const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({\n        name: key,\n        type: 'string',\n        ttl: -1,\n      }));\n      expect(result).toEqual(mockResult);\n    });\n\n    it('should return null for size when memory usage fails', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: BrowserToolKeysCommands.MemoryUsage,\n        message: \"ERR unknown command 'memory'\",\n      };\n\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Ttl, key]),\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, -1]));\n\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [\n            BrowserToolKeysCommands.MemoryUsage,\n            key,\n            'samples',\n            '0',\n          ]),\n        )\n        .mockResolvedValue(Array(keys.length).fill([replyError, null]));\n\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith(\n          keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue(Array(keys.length).fill([null, 'string']));\n\n      const result = await strategy.getKeysInfo(\n        mockStandaloneRedisClient,\n        keys,\n        undefined,\n        true, // includeSize\n        true, // includeTTL\n      );\n\n      const mockResult: GetKeyInfoResponse[] = keys.map((key) => ({\n        name: key,\n        type: 'string',\n        ttl: -1,\n        size: null,\n      }));\n      expect(result).toEqual(mockResult);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/keys/scanner/strategies/standalone.scanner.strategy.ts",
    "content": "import { isNull } from 'lodash';\nimport config from 'src/utils/config';\nimport { isRedisGlob, unescapeRedisGlob } from 'src/utils';\nimport {\n  GetKeyInfoResponse,\n  GetKeysWithDetailsResponse,\n  RedisDataType,\n} from 'src/modules/browser/keys/dto';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  IScannerNodeKeys,\n  IScannerGetKeysArgs,\n} from 'src/modules/browser/keys/scanner/scanner.interface';\nimport { Injectable } from '@nestjs/common';\nimport { ScannerStrategy } from 'src/modules/browser/keys/scanner/strategies/scanner.strategy';\nimport { RedisClient, RedisClientCommandReply } from 'src/modules/redis/client';\nimport { getTotalKeys } from 'src/modules/redis/utils';\nimport { RedisString } from 'src/common/constants';\nimport { ReplyError } from 'src/models';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\n\n@Injectable()\nexport class StandaloneScannerStrategy extends ScannerStrategy {\n  /**\n   * Get bulk keys ttl values\n   * @param client\n   * @param keys\n   * @protected\n   */\n  private async getKeysTtl(\n    client: RedisClient,\n    keys: RedisString[],\n  ): Promise<number[]> {\n    const result = await client.sendPipeline(\n      keys.map((key: string) => [BrowserToolKeysCommands.Ttl, key]),\n    );\n\n    return result.map((item: [ReplyError, any]) => (item[0] ? null : item[1]));\n  }\n\n  /**\n   * Get bulk keys types\n   * @param client\n   * @param keys\n   * @protected\n   */\n  private async getKeysType(\n    client: RedisClient,\n    keys: RedisString[],\n  ): Promise<string[]> {\n    const result = (await client.sendPipeline(\n      keys.map((key: string) => [BrowserToolKeysCommands.Type, key]),\n      { replyEncoding: 'utf8' },\n    )) as [any, string][];\n\n    return result.map((item: [ReplyError, any]) => (item[0] ? null : item[1]));\n  }\n\n  /**\n   * Get bulk keys sizes\n   * @param client\n   * @param keys\n   * @private\n   */\n  private async getKeysSize(\n    client: RedisClient,\n    keys: RedisString[],\n  ): Promise<number[]> {\n    const result = (await client.sendPipeline(\n      keys.map((key) => [\n        BrowserToolKeysCommands.MemoryUsage,\n        key,\n        'samples',\n        '0',\n      ]),\n    )) as [any, number][];\n\n    return result.map((item: [ReplyError, any]) => (item[0] ? null : item[1]));\n  }\n\n  private async scan(\n    client: RedisClient,\n    node: IScannerNodeKeys,\n    match: string,\n    count: number,\n    scanThreshold: number,\n    type?: RedisDataType,\n  ): Promise<void> {\n    const COUNT = Math.min(2000, count);\n\n    let fullScanned = false;\n    while (\n      (node.total >= 0 || isNull(node.total)) &&\n      !fullScanned &&\n      node.keys.length < count &&\n      node.scanned < scanThreshold\n    ) {\n      let commandArgs = [`${node.cursor}`, 'MATCH', match, 'COUNT', COUNT];\n      if (type) {\n        commandArgs = [...commandArgs, 'TYPE', type];\n      }\n      const execResult = (await client.sendCommand([\n        BrowserToolKeysCommands.Scan,\n        ...commandArgs,\n      ])) as [string, RedisClientCommandReply[]];\n\n      const [nextCursor, keys] = execResult;\n      /* eslint-disable no-param-reassign */\n      node.cursor = parseInt(nextCursor, 10);\n      node.scanned += COUNT;\n      node.keys = node.keys.concat(keys);\n      /* eslint-enable no-param-reassign */\n      fullScanned = node.cursor === 0;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getKeys(\n    client: RedisClient,\n    args: IScannerGetKeysArgs,\n  ): Promise<GetKeysWithDetailsResponse[]> {\n    const match = args.match !== undefined ? args.match : '*';\n    const count = args.count || REDIS_SCAN_CONFIG.countDefault;\n    const scanThreshold = args.scanThreshold || REDIS_SCAN_CONFIG.scanThreshold;\n\n    const node = {\n      total: 0,\n      scanned: 0,\n      keys: [],\n      cursor: parseInt(args.cursor, 10),\n    };\n\n    node.total = await getTotalKeys(client);\n\n    if (!isRedisGlob(match)) {\n      const keyName = Buffer.from(unescapeRedisGlob(match));\n      node.cursor = 0;\n      node.scanned = isNull(node.total) ? 1 : node.total;\n      node.keys = await this.getKeysInfo(\n        client,\n        [keyName],\n        undefined,\n        true,\n        true,\n      );\n      node.keys = node.keys.filter((key: GetKeyInfoResponse) => {\n        if (key.ttl === -2) {\n          return false;\n        }\n        if (args.type) {\n          return key.type === args.type;\n        }\n        return true;\n      });\n      return [node];\n    }\n\n    await this.scan(client, node, match, count, scanThreshold, args.type);\n\n    if (node.keys.length && args.keysInfo) {\n      node.keys = await this.getKeysInfo(\n        client,\n        node.keys,\n        args.type,\n        true,\n        true,\n      );\n    } else {\n      node.keys = node.keys.map((name) => ({\n        name,\n        type: args.type || undefined,\n      }));\n    }\n\n    // workaround for \"pika\" databases\n    if (!node.total && (node.cursor > 0 || node.keys?.length)) {\n      node.total = null;\n    }\n\n    return [node];\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getKeysInfo(\n    client: RedisClient,\n    keys: RedisString[],\n    filterType?: RedisDataType,\n    includeSize?: boolean,\n    includeTTL?: boolean,\n  ): Promise<GetKeyInfoResponse[]> {\n    const sizeResults = includeSize ? await this.getKeysSize(client, keys) : [];\n    const typeResults = filterType\n      ? Array(keys.length).fill(filterType)\n      : await this.getKeysType(client, keys);\n    const ttlResults = includeTTL ? await this.getKeysTtl(client, keys) : [];\n\n    return keys.map((key, index) => ({\n      name: key,\n      type: typeResults[index],\n      ...(includeTTL && { ttl: ttlResults[index] }),\n      ...(includeSize && { size: sizeResults[index] }),\n    }));\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/create.list-with-expire.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { PushElementToListDto } from './push.element-to-list.dto';\n\nexport class CreateListWithExpireDto extends IntersectionType(\n  PushElementToListDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/delete.list-elements.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined, IsEnum, IsInt, IsNotEmpty, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { ListElementDestination } from './push.element-to-list.dto';\n\nexport class DeleteListElementsDto extends KeyDto {\n  @ApiProperty({\n    description:\n      'In order to remove last elements of the list, use the TAIL value, else HEAD value',\n    default: ListElementDestination.Tail,\n    enum: ListElementDestination,\n  })\n  @IsDefined()\n  @IsEnum(ListElementDestination, {\n    message: `destination must be a valid enum value. Valid values: ${Object.values(\n      ListElementDestination,\n    )}.`,\n  })\n  destination: ListElementDestination;\n\n  @ApiProperty({\n    description: 'Specifying the number of elements to remove from list.',\n    type: Number,\n    minimum: 1,\n    default: 1,\n  })\n  @IsInt()\n  @Min(1)\n  @Type(() => Number)\n  @IsNotEmpty()\n  count: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/delete.list-elements.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class DeleteListElementsResponse {\n  @ApiProperty({\n    type: String,\n    isArray: true,\n    description: 'Removed elements from list',\n  })\n  @RedisStringType({ each: true })\n  elements: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/get.list-element.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class GetListElementResponse extends KeyResponse {\n  @ApiProperty({\n    type: () => String,\n    description: 'Element value',\n  })\n  @RedisStringType()\n  value: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/get.list-elements.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsInt, IsNotEmpty, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class GetListElementsDto extends KeyDto {\n  @ApiProperty({\n    description: 'Specifying the number of elements to skip.',\n    type: Number,\n    minimum: 0,\n    default: '0',\n  })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  @IsNotEmpty()\n  offset: number;\n\n  @ApiProperty({\n    description:\n      'Specifying the number of elements to return from starting at offset.',\n    type: Number,\n    minimum: 1,\n    default: 15,\n  })\n  @IsInt()\n  @Min(1)\n  @Type(() => Number)\n  @IsNotEmpty()\n  count: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/get.list-elements.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class GetListElementsResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'The number of elements in the currently-selected list.',\n  })\n  total: number;\n\n  @ApiProperty({\n    type: () => String,\n    description: 'Array of elements.',\n    isArray: true,\n  })\n  @RedisStringType({ each: true })\n  elements: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/index.ts",
    "content": "export * from './create.list-with-expire.dto';\nexport * from './delete.list-elements.dto';\nexport * from './delete.list-elements.response';\nexport * from './get.list-element.response';\nexport * from './get.list-elements.dto';\nexport * from './get.list-elements.response';\nexport * from './push.element-to-list.dto';\nexport * from './push.list-elements.response';\nexport * from './set.list-element.dto';\nexport * from './set.list-element.response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/push.element-to-list.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsArray, IsDefined, IsEnum } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport enum ListElementDestination {\n  Tail = 'TAIL',\n  Head = 'HEAD',\n}\n\nexport class PushElementToListDto extends KeyDto {\n  @ApiProperty({\n    description: 'List element(s)',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  elements: RedisString[];\n\n  @ApiPropertyOptional({\n    description:\n      'In order to append elements to the end of the list, ' +\n      'use the TAIL value, to prepend use HEAD value. ' +\n      'Default: TAIL (when not specified)',\n    default: ListElementDestination.Tail,\n    enum: ListElementDestination,\n  })\n  @IsEnum(ListElementDestination, {\n    message: `destination must be a valid enum value. Valid values: ${Object.values(\n      ListElementDestination,\n    )}.`,\n  })\n  destination: ListElementDestination = ListElementDestination.Tail;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/push.list-elements.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class PushListElementsResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'The number of elements in the list after current operation.',\n  })\n  total: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/set.list-element.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined, IsInt, IsNotEmpty, Min } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class SetListElementDto extends KeyDto {\n  @ApiProperty({\n    description: 'List element',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  element: RedisString;\n\n  @ApiProperty({\n    description: 'Element index',\n    type: Number,\n    minimum: 0,\n  })\n  @IsDefined()\n  @Min(0)\n  @IsInt({ always: true })\n  @IsNotEmpty()\n  index: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/dto/set.list-element.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class SetListElementResponse {\n  @ApiProperty({\n    description: 'Element index',\n    type: Number,\n    minimum: 0,\n  })\n  index: number;\n\n  @ApiProperty({\n    description: 'List element',\n    type: String,\n  })\n  @RedisStringType()\n  element: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/list.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  HttpCode,\n  Param,\n  Patch,\n  Post,\n  Put,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport {\n  ApiBody,\n  ApiOkResponse,\n  ApiOperation,\n  ApiParam,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport {\n  PushElementToListDto,\n  CreateListWithExpireDto,\n  GetListElementsDto,\n  GetListElementsResponse,\n  SetListElementDto,\n  SetListElementResponse,\n  GetListElementResponse,\n  DeleteListElementsDto,\n  DeleteListElementsResponse,\n  PushListElementsResponse,\n} from 'src/modules/browser/list/dto';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { ListService } from 'src/modules/browser/list/list.service';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: List')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('list')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class ListController extends BrowserBaseController {\n  constructor(private listService: ListService) {\n    super();\n  }\n\n  @Post('')\n  @ApiOperation({ description: 'Set key to hold list data type' })\n  @ApiRedisParams()\n  @ApiBody({ type: CreateListWithExpireDto })\n  @ApiQueryRedisStringEncoding()\n  async createList(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateListWithExpireDto,\n  ): Promise<void> {\n    return await this.listService.createList(clientMetadata, dto);\n  }\n\n  @Put('')\n  @ApiRedisInstanceOperation({\n    description: 'Insert element at the head/tail of the List data type',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Length of the list after the push operation',\n        type: PushListElementsResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async pushElement(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: PushElementToListDto,\n  ): Promise<PushListElementsResponse> {\n    return await this.listService.pushElement(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/get-elements')\n  @HttpCode(200)\n  @ApiOperation({\n    description: 'Get specified elements of the list stored at key',\n  })\n  @ApiRedisParams()\n  @ApiOkResponse({\n    description: 'Specified elements of the list stored at key.',\n    type: GetListElementsResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getElements(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetListElementsDto,\n  ): Promise<GetListElementsResponse> {\n    return this.listService.getElements(clientMetadata, dto);\n  }\n\n  @Patch('')\n  @ApiOperation({\n    description: 'Update list element by index.',\n  })\n  @ApiRedisParams()\n  @ApiBody({ type: SetListElementDto })\n  @ApiQueryRedisStringEncoding()\n  async updateElement(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SetListElementDto,\n  ): Promise<SetListElementResponse> {\n    return await this.listService.setElement(clientMetadata, dto);\n  }\n\n  @Post('/get-elements/:index')\n  @ApiParam({\n    name: 'index',\n    description:\n      'Zero-based index. 0 - first element, 1 - second element and so on. ' +\n      'Negative indices can be used to designate elements starting at the tail of the list. ' +\n      'Here, -1 means the last element',\n    type: Number,\n    required: true,\n  })\n  @ApiRedisInstanceOperation({\n    description: 'Get specified List element by index',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Specified elements of the list stored at key.',\n        type: GetListElementsResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getElement(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Param('index') index: number,\n    @Body() dto: KeyDto,\n  ): Promise<GetListElementResponse> {\n    return this.listService.getElement(clientMetadata, index, dto);\n  }\n\n  @Delete('/elements')\n  @ApiRedisInstanceOperation({\n    description:\n      'Remove and return the elements from the tail/head of list stored at key.',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Removed elements.',\n        type: GetListElementsResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async deleteElement(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteListElementsDto,\n  ): Promise<DeleteListElementsResponse> {\n    return this.listService.deleteElements(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/list.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { ListService } from 'src/modules/browser/list/list.service';\nimport { ListController } from 'src/modules/browser/list/list.controller';\n\n@Module({})\nexport class ListModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: ListModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: ListModule,\n          },\n        ]),\n      ],\n      controllers: [ListController],\n      providers: [ListService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/list.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  ConflictException,\n  ForbiddenException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { when } from 'jest-when';\nimport { ReplyError } from 'src/models/redis-client';\nimport {\n  mockRedisNoPermError,\n  mockRedisWrongNumberOfArgumentsError,\n  mockRedisWrongTypeError,\n  mockBrowserClientMetadata,\n} from 'src/__mocks__';\nimport {\n  CreateListWithExpireDto,\n  ListElementDestination,\n} from 'src/modules/browser/list/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolListCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  mockDeleteElementsDto,\n  mockGetListElementResponse,\n  mockGetListElementsDto,\n  mockGetListElementsResponse,\n  mockIndex,\n  mockKeyDto,\n  mockListElement,\n  mockListElement2,\n  mockListElements,\n  mockPushElementDto,\n  mockSetListElementDto,\n} from 'src/modules/browser/__mocks__';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { mockDatabaseClientFactory } from 'src/__mocks__/databases-client';\nimport { mockStandaloneRedisClient } from 'src/__mocks__/redis-client';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ListService } from './list.service';\n\ndescribe('ListService', () => {\n  let service: ListService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ListService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<ListService>(ListService);\n    mockStandaloneRedisClient.sendCommand = jest\n      .fn()\n      .mockResolvedValue(undefined);\n  });\n\n  describe('createList', () => {\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockPushElementDto.keyName,\n        ])\n        .mockResolvedValue(false);\n      service.createListWithExpiration = jest.fn();\n    });\n\n    it('create list with expiration', async () => {\n      service.createListWithExpiration = jest.fn().mockResolvedValue(undefined);\n\n      await expect(\n        service.createList(mockBrowserClientMetadata, {\n          ...mockPushElementDto,\n          expire: 1000,\n        }),\n      ).resolves.not.toThrow();\n      expect(service.createListWithExpiration).toHaveBeenCalled();\n    });\n    it('create list without expiration', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.LPush,\n          mockPushElementDto.keyName,\n          ...mockPushElementDto.elements,\n        ])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.createList(mockBrowserClientMetadata, mockPushElementDto),\n      ).resolves.not.toThrow();\n      expect(service.createListWithExpiration).not.toHaveBeenCalled();\n    });\n\n    it('create list with expiration and push at the head', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.LPush,\n          mockPushElementDto.keyName,\n          ...mockPushElementDto.elements,\n        ])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.createList(mockBrowserClientMetadata, mockPushElementDto),\n      ).resolves.not.toThrow();\n      expect(service.createListWithExpiration).not.toHaveBeenCalled();\n    });\n\n    it('key with this name exist', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockPushElementDto.keyName,\n        ])\n        .mockResolvedValue(true);\n\n      await expect(\n        service.createList(mockBrowserClientMetadata, mockPushElementDto),\n      ).rejects.toThrow(new ConflictException(ERROR_MESSAGES.KEY_NAME_EXIST));\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledTimes(1);\n      expect(mockStandaloneRedisClient.sendPipeline).not.toHaveBeenCalled();\n    });\n    it(\"user don't have required permissions for createList\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'LPUSH',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.createList(mockBrowserClientMetadata, mockPushElementDto),\n      ).rejects.toThrow(ForbiddenException);\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledTimes(1);\n      expect(mockStandaloneRedisClient.sendPipeline).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('pushElement', () => {\n    it('succeed to insert element(s) at the tail of the list data type', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.RPushX,\n          mockPushElementDto.keyName,\n          ...mockPushElementDto.elements,\n        ])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.pushElement(mockBrowserClientMetadata, mockPushElementDto),\n      ).resolves.not.toThrow();\n    });\n    it('succeed to insert element(s) at the head of the list data type', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.LPushX,\n          mockPushElementDto.keyName,\n          ...mockPushElementDto.elements,\n        ])\n        .mockResolvedValue(12);\n\n      const result = await service.pushElement(mockBrowserClientMetadata, {\n        ...mockPushElementDto,\n        destination: ListElementDestination.Head,\n      });\n      expect(result.keyName).toEqual(mockPushElementDto.keyName);\n      expect(result.total).toEqual(12);\n    });\n    it('key with this name does not exist for pushElement', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.RPushX,\n          mockPushElementDto.keyName,\n          ...mockPushElementDto.elements,\n        ])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.pushElement(mockBrowserClientMetadata, mockPushElementDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"user don't have required permissions for pushElement\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'RPUSHX',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.pushElement(mockBrowserClientMetadata, mockPushElementDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('getElements', () => {\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolListCommands.LLen, mockPushElementDto.keyName])\n        .mockResolvedValue(mockListElements.length);\n    });\n    it('succeed to get elements of the list', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.Lrange,\n          mockPushElementDto.keyName,\n          0,\n          9,\n        ])\n        .mockResolvedValue(mockListElements);\n\n      const result = await service.getElements(\n        mockBrowserClientMetadata,\n        mockGetListElementsDto,\n      );\n      await expect(result).toEqual(mockGetListElementsResponse);\n    });\n    it('key with this name does not exist for getElements', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolListCommands.LLen, mockPushElementDto.keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.getElements(mockBrowserClientMetadata, mockGetListElementsDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"try to use 'LLEN' command not for list data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'LLEN',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolListCommands.LLen, mockPushElementDto.keyName])\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.getElements(mockBrowserClientMetadata, mockGetListElementsDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for getElements\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'LRANGE',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getElements(mockBrowserClientMetadata, mockGetListElementsDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('getElement', () => {\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockKeyDto.keyName])\n        .mockResolvedValue(1);\n    });\n    it('try to use LINDEX command not for list data type', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'LINDEX',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.LIndex,\n          mockKeyDto.keyName,\n          expect.anything(),\n        ])\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user hasn't permissions to LINDEX\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'LINDEX',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolListCommands.LIndex,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it(\"user hasn't permissions to EXISTS\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'EXISTS',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('key with this name does not exists', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockKeyDto.keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto),\n      ).rejects.toThrow(new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST));\n    });\n    it('index is out of range', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolListCommands.LIndex,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(null);\n\n      await expect(\n        service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto),\n      ).rejects.toThrow(\n        new NotFoundException(ERROR_MESSAGES.INDEX_OUT_OF_RANGE()),\n      );\n    });\n    it('succeed to get List element by index', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolListCommands.LIndex,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue(mockGetListElementResponse.value);\n\n      const result = await service.getElement(\n        mockBrowserClientMetadata,\n        mockIndex,\n        mockKeyDto,\n      );\n      await expect(result).toEqual(mockGetListElementResponse);\n    });\n  });\n\n  describe('setElement', () => {\n    beforeEach(() => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockSetListElementDto.keyName,\n        ])\n        .mockResolvedValue(true);\n    });\n    it('succeed to set the list element at index', async () => {\n      const { keyName, index, element } = mockSetListElementDto;\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolListCommands.LSet, keyName, index, element])\n        .mockResolvedValue('OK');\n\n      await expect(\n        service.setElement(mockBrowserClientMetadata, mockSetListElementDto),\n      ).resolves.not.toThrow();\n    });\n    it('key with this name does not exist for setElement', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockSetListElementDto.keyName,\n        ])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.setElement(mockBrowserClientMetadata, mockSetListElementDto),\n      ).rejects.toThrow(new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST));\n    });\n    it(\"try to use 'LSET' command not for list data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'LSET',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.setElement(mockBrowserClientMetadata, mockSetListElementDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it('index for LSET coomand is of out of range', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: 'LSET',\n        message: 'ERR index out of range',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.setElement(mockBrowserClientMetadata, mockSetListElementDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'LSET',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.setElement(mockBrowserClientMetadata, mockSetListElementDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('deleteElements', () => {\n    it('succeed to remove element from the tail', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.RPop,\n          mockDeleteElementsDto.keyName,\n        ])\n        .mockResolvedValue(mockListElements[0]);\n\n      const result = await service.deleteElements(\n        mockBrowserClientMetadata,\n        mockDeleteElementsDto,\n      );\n\n      await expect(result).toEqual({ elements: [mockListElements[0]] });\n    });\n    it('succeed to remove element from the head', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.LPop,\n          mockDeleteElementsDto.keyName,\n        ])\n        .mockResolvedValue(mockListElements[0]);\n\n      const result = await service.deleteElements(mockBrowserClientMetadata, {\n        ...mockDeleteElementsDto,\n        destination: ListElementDestination.Head,\n      });\n\n      await expect(result).toEqual({ elements: [mockListElements[0]] });\n    });\n    it('succeed to remove multiple elements from the tail', async () => {\n      const mockDeletedElements = [mockListElement, mockListElement2];\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.RPop,\n          mockDeleteElementsDto.keyName,\n          2,\n        ])\n        .mockResolvedValue(mockDeletedElements);\n\n      const result = await service.deleteElements(mockBrowserClientMetadata, {\n        ...mockDeleteElementsDto,\n        count: 2,\n      });\n      await expect(result).toEqual({ elements: mockDeletedElements });\n    });\n    it('try to use RPOP command not for list data type', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'RPOP',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolListCommands.RPop, expect.anything()])\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteElements(\n          mockBrowserClientMetadata,\n          mockDeleteElementsDto,\n        ),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"redis doesn't support 'RPOP' with 'count' argument\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongNumberOfArgumentsError,\n        command: {\n          name: 'rpop',\n          args: [mockDeleteElementsDto.keyName, 2],\n        },\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.RPop,\n          mockDeleteElementsDto.keyName,\n          2,\n        ])\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteElements(mockBrowserClientMetadata, {\n          ...mockDeleteElementsDto,\n          count: 2,\n        }),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user hasn't permissions to RPOP\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'RPOP',\n      };\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([BrowserToolListCommands.RPop, expect.anything()])\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteElements(\n          mockBrowserClientMetadata,\n          mockDeleteElementsDto,\n        ),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('key with this name does not exists', async () => {\n      when(mockStandaloneRedisClient.sendCommand)\n        .calledWith([\n          BrowserToolListCommands.RPop,\n          mockDeleteElementsDto.keyName,\n        ])\n        .mockResolvedValue(null);\n\n      await expect(\n        service.deleteElements(\n          mockBrowserClientMetadata,\n          mockDeleteElementsDto,\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('_createListWithExpiration', () => {\n    const dto: CreateListWithExpireDto = {\n      ...mockPushElementDto,\n      expire: 1000,\n    };\n    it(\"shouldn't throw error\", async () => {\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolListCommands.RPush, dto.keyName, ...dto.elements],\n          [BrowserToolKeysCommands.Expire, dto.keyName, dto.expire],\n        ])\n        .mockResolvedValue([\n          [null, 1],\n          [null, 1],\n        ]);\n\n      await expect(\n        service.createListWithExpiration(mockStandaloneRedisClient, dto),\n      ).resolves.not.toThrow();\n    });\n    it('should throw error', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'RPUSH',\n      };\n      when(mockStandaloneRedisClient.sendPipeline)\n        .calledWith([\n          [BrowserToolListCommands.RPush, dto.keyName, ...dto.elements],\n          [BrowserToolKeysCommands.Expire, dto.keyName, dto.expire],\n        ])\n        .mockResolvedValue([[replyError, []]]);\n\n      try {\n        await service.createListWithExpiration(mockStandaloneRedisClient, dto);\n        fail('Should throw an error');\n      } catch (err) {\n        expect(err.message).toEqual(replyError.message);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/list/list.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { isNull, isArray } from 'lodash';\nimport { RedisErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { catchAclError, catchMultiTransactionError } from 'src/utils';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  CreateListWithExpireDto,\n  DeleteListElementsDto,\n  DeleteListElementsResponse,\n  GetListElementResponse,\n  GetListElementsDto,\n  GetListElementsResponse,\n  ListElementDestination,\n  PushElementToListDto,\n  PushListElementsResponse,\n  SetListElementDto,\n  SetListElementResponse,\n} from 'src/modules/browser/list/dto';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolListCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient, RedisClientCommandReply } from 'src/modules/redis/client';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\n\n@Injectable()\nexport class ListService {\n  private logger = new Logger('ListService');\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  public async createList(\n    clientMetadata: ClientMetadata,\n    dto: CreateListWithExpireDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating list data type.', clientMetadata);\n      const { keyName, expire } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyExists(keyName, client);\n\n      if (expire) {\n        await this.createListWithExpiration(client, dto);\n      } else {\n        await this.createSimpleList(client, dto);\n      }\n\n      this.logger.debug('Succeed to create list data type.', clientMetadata);\n      return null;\n    } catch (error) {\n      this.logger.error(\n        'Failed to create list data type.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  public async pushElement(\n    clientMetadata: ClientMetadata,\n    dto: PushElementToListDto,\n  ): Promise<PushListElementsResponse> {\n    try {\n      this.logger.debug(\n        'Insert element at the tail/head of the list data type.',\n        clientMetadata,\n      );\n      const { keyName, elements, destination } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const total: RedisClientCommandReply = await client.sendCommand([\n        BrowserToolListCommands[\n          destination === ListElementDestination.Tail ? 'RPushX' : 'LPushX'\n        ],\n        keyName,\n        ...elements,\n      ]);\n      if (!total) {\n        this.logger.error(\n          `Failed to inserts element at the ${destination} of the list data type. Key not found. key: ${keyName}`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      this.logger.debug(\n        `Succeed to insert element at the ${destination} of the list data type.`,\n        clientMetadata,\n      );\n      return plainToInstance(PushListElementsResponse, { keyName, total });\n    } catch (error) {\n      this.logger.error(\n        'Failed to inserts element to the list data type.',\n        error,\n        clientMetadata,\n      );\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async getElements(\n    clientMetadata: ClientMetadata,\n    dto: GetListElementsDto,\n  ): Promise<GetListElementsResponse> {\n    try {\n      this.logger.debug(\n        'Getting elements of the list stored at key.',\n        clientMetadata,\n      );\n      const { keyName, offset, count } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const total = await client.sendCommand([\n        BrowserToolListCommands.LLen,\n        keyName,\n      ]);\n      if (!total) {\n        this.logger.error(\n          `Failed to get elements of the list. Key not found. key: ${keyName}`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      const elements = await client.sendCommand([\n        BrowserToolListCommands.Lrange,\n        keyName,\n        offset,\n        offset + count - 1,\n      ]);\n\n      this.logger.debug('Succeed to get elements of the list.', clientMetadata);\n      return plainToInstance(GetListElementsResponse, {\n        keyName,\n        total,\n        elements,\n      });\n    } catch (error) {\n      this.logger.error(\n        'Failed to to get elements of the list.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Get List element by index\n   * NotFound exception when redis return null\n   * @param clientMetadata\n   * @param index\n   * @param dto\n   */\n  public async getElement(\n    clientMetadata: ClientMetadata,\n    index: number,\n    dto: KeyDto,\n  ): Promise<GetListElementResponse> {\n    try {\n      this.logger.debug('Getting List element by index.', clientMetadata);\n      const { keyName } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const value = await client.sendCommand([\n        BrowserToolListCommands.LIndex,\n        keyName,\n        index,\n      ]);\n      if (value === null) {\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.INDEX_OUT_OF_RANGE()),\n        );\n      }\n\n      this.logger.debug(\n        'Succeed to get List element by index.',\n        clientMetadata,\n      );\n      return plainToInstance(GetListElementResponse, { keyName, value });\n    } catch (error) {\n      this.logger.error(\n        'Failed to to get List element by index.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async setElement(\n    clientMetadata: ClientMetadata,\n    dto: SetListElementDto,\n  ): Promise<SetListElementResponse> {\n    try {\n      this.logger.debug('Setting the list element at index', clientMetadata);\n      const { keyName, element, index } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n      await client.sendCommand([\n        BrowserToolListCommands.LSet,\n        keyName,\n        index,\n        element,\n      ]);\n\n      this.logger.debug(\n        'Succeed to set the list element at index.',\n        clientMetadata,\n      );\n      return plainToInstance(SetListElementResponse, { index, element });\n    } catch (error) {\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      if (error?.message.includes('index out of range')) {\n        throw new BadRequestException(error.message);\n      }\n      this.logger.error(\n        'Failed to set the list element at index.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Delete and return the elements from the tail/head of list stored at key\n   * NotFound exception when redis return null\n   * @param clientMetadata\n   * @param dto\n   */\n  public async deleteElements(\n    clientMetadata: ClientMetadata,\n    dto: DeleteListElementsDto,\n  ): Promise<DeleteListElementsResponse> {\n    try {\n      this.logger.debug(\n        'Deleting elements from the list stored at key.',\n        clientMetadata,\n      );\n      const { keyName, count, destination } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const execArgs = !!count && count > 1 ? [keyName, count] : [keyName];\n      let result;\n\n      if (destination === ListElementDestination.Head) {\n        result = await client.sendCommand([\n          BrowserToolListCommands.LPop,\n          ...execArgs,\n        ]);\n      } else {\n        result = await client.sendCommand([\n          BrowserToolListCommands.RPop,\n          ...execArgs,\n        ]);\n      }\n      if (isNull(result)) {\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      return plainToInstance(DeleteListElementsResponse, {\n        elements: isArray(result) ? [...result] : [result],\n      });\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete elements from the list stored at key.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      if (\n        error?.message.includes('wrong number of arguments') &&\n        error?.command?.args?.length === 2\n      ) {\n        throw new BadRequestException(\n          ERROR_MESSAGES.REMOVING_MULTIPLE_ELEMENTS_NOT_SUPPORT(),\n        );\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async createSimpleList(\n    client: RedisClient,\n    dto: PushElementToListDto,\n  ): Promise<void> {\n    const { keyName, elements, destination } = dto;\n    await client.sendCommand([\n      BrowserToolListCommands[\n        destination === ListElementDestination.Tail ? 'RPush' : 'LPush'\n      ],\n      keyName,\n      ...elements,\n    ]);\n  }\n\n  public async createListWithExpiration(\n    client: RedisClient,\n    dto: CreateListWithExpireDto,\n  ): Promise<void> {\n    const { keyName, elements, expire, destination } = dto;\n    const transactionResults = await client.sendPipeline([\n      [\n        BrowserToolListCommands[\n          destination === ListElementDestination.Tail ? 'RPush' : 'LPush'\n        ],\n        keyName,\n        ...elements,\n      ],\n      [BrowserToolKeysCommands.Expire, keyName, expire],\n    ]);\n    catchMultiTransactionError(transactionResults);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/create.redisearch-index.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  ArrayMinSize,\n  IsDefined,\n  IsEnum,\n  IsOptional,\n  ValidateNested,\n} from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { Type } from 'class-transformer';\n\nexport enum RedisearchIndexKeyType {\n  HASH = 'hash',\n  JSON = 'json',\n}\n\nexport enum RedisearchIndexDataType {\n  TEXT = 'text',\n  TAG = 'tag',\n  NUMERIC = 'numeric',\n  GEO = 'geo',\n  GEOSHAPE = 'geoshape',\n  VECTOR = 'vector',\n}\n\nexport class CreateRedisearchIndexFieldDto {\n  @ApiProperty({\n    description: 'Name of field to be indexed',\n    type: String,\n  })\n  @IsDefined()\n  @RedisStringType()\n  @IsRedisString()\n  name: RedisString;\n\n  @ApiProperty({\n    description: 'Type of how data must be indexed',\n    enum: RedisearchIndexDataType,\n  })\n  @IsDefined()\n  @IsEnum(RedisearchIndexDataType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      RedisearchIndexDataType,\n    )}.`,\n  })\n  type: RedisearchIndexDataType;\n}\n\nexport class CreateRedisearchIndexDto {\n  @ApiProperty({\n    description: 'Index Name',\n    type: String,\n  })\n  @IsDefined()\n  @RedisStringType()\n  @IsRedisString()\n  index: RedisString;\n\n  @ApiProperty({\n    description: 'Type of keys to index',\n    enum: RedisearchIndexKeyType,\n  })\n  @IsDefined()\n  @IsEnum(RedisearchIndexKeyType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      RedisearchIndexKeyType,\n    )}.`,\n  })\n  type: RedisearchIndexKeyType;\n\n  @ApiPropertyOptional({\n    description: 'Keys prefixes to find keys for index',\n    isArray: true,\n    type: String,\n  })\n  @IsOptional()\n  @RedisStringType({ each: true })\n  @IsRedisString({ each: true })\n  prefixes?: RedisString[];\n\n  @ApiProperty({\n    description: 'Fields to index',\n    isArray: true,\n    type: CreateRedisearchIndexFieldDto,\n  })\n  @Type(() => CreateRedisearchIndexFieldDto)\n  @ValidateNested()\n  @ArrayMinSize(1)\n  fields: CreateRedisearchIndexFieldDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/index.delete.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { RedisString } from 'src/common/constants';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\n\nexport class IndexDeleteRequestBodyDto {\n  @ApiProperty({\n    description: 'Index name',\n    type: String,\n  })\n  @IsDefined()\n  @RedisStringType()\n  @IsRedisString()\n  index: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/index.info.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { Expose } from 'class-transformer';\n\nexport class IndexInfoRequestBodyDto {\n  @ApiProperty({\n    description: 'Index name',\n    type: String,\n  })\n  @IsDefined()\n  @RedisStringType()\n  @IsRedisString()\n  index: RedisString;\n}\n\nexport class IndexOptionsDto {\n  @ApiProperty({\n    description:\n      'is a filter expression with the full RediSearch aggregation expression language.',\n    type: String,\n  })\n  @Expose()\n  filter?: string;\n\n  @ApiProperty({\n    description:\n      'if set, indicates the default language for documents in the index. Default is English.',\n    type: String,\n  })\n  @Expose()\n  default_lang?: string;\n}\n\nexport class IndexDefinitionDto {\n  @ApiProperty({\n    description: 'key_type, hash or JSON',\n    type: String,\n  })\n  @Expose()\n  key_type: string;\n\n  @ApiProperty({\n    description: 'Index prefixes given during create',\n    type: String,\n    isArray: true,\n  })\n  @Expose()\n  prefixes: Array<string>;\n\n  @ApiProperty({\n    description: 'Index default_score',\n    type: String,\n  })\n  @Expose()\n  default_score: string;\n\n  @ApiProperty({\n    description:\n      'Indicates whether all fields of a JSON document are automatically indexed by RediSearch',\n    type: String,\n  })\n  @Expose()\n  indexes_all?: string;\n}\n\nexport class IndexAttibuteDto {\n  @ApiProperty({\n    description: 'Field identifier',\n    type: String,\n  })\n  @Expose()\n  identifier: string;\n\n  @ApiProperty({\n    description: 'Field attribute',\n    type: String,\n  })\n  @Expose()\n  attribute: string;\n\n  @ApiProperty({\n    description: 'Field type',\n    type: String,\n  })\n  @Expose()\n  type: string;\n\n  @ApiProperty({\n    description: 'Field weight',\n    type: String,\n  })\n  @Expose()\n  WEIGHT?: string;\n\n  @ApiProperty({\n    description: 'Field can be sorted',\n    type: Boolean,\n  })\n  @Expose()\n  SORTABLE?: boolean;\n\n  @ApiProperty({\n    description:\n      'Attributes can have the NOINDEX option, which means they will not be indexed. ',\n    type: Boolean,\n  })\n  @Expose()\n  NOINDEX?: boolean;\n\n  @ApiProperty({\n    description: 'Attribute is case sensitive',\n    type: Boolean,\n  })\n  @Expose()\n  CASESENSITIVE?: boolean;\n\n  @ApiProperty({\n    description: `By default, for hashes (not with JSON) SORTABLE applies a normalization to the indexed value\n      (characters set to lowercase, removal of diacritics).`,\n    type: Boolean,\n  })\n  @Expose()\n  UNF?: boolean;\n\n  @ApiProperty({\n    description: `Text attributes can have the NOSTEM argument that disables stemming when indexing its values.\n      This may be ideal for things like proper names.`,\n    type: Boolean,\n  })\n  @Expose()\n  NOSTEM?: boolean;\n\n  @ApiProperty({\n    description: `Indicates how the text contained in the attribute is to be split into individual tags.\n      The default is ,. The value must be a single character.`,\n    type: String,\n  })\n  @Expose()\n  SEPARATOR?: string;\n}\n\nexport class FieldStatisticsDto {\n  @ApiProperty({\n    description: 'Field identifier',\n    type: String,\n  })\n  @Expose()\n  identifier: string;\n\n  @ApiProperty({\n    description: 'Field attribute',\n    type: String,\n  })\n  @Expose()\n  attribute: string;\n\n  @ApiProperty({\n    description: 'Field errors',\n    type: Object,\n  })\n  @Expose()\n  ['Index Errors']: object;\n}\n\n// The list of return fields from redis: https://redis.io/docs/latest/commands/ft.info/\n\nexport class IndexInfoDto {\n  // General\n  @ApiProperty({\n    description: 'The index name that was defined when index was created',\n    type: String,\n  })\n  @Expose()\n  @IsDefined()\n  index_name: string;\n\n  @ApiProperty({\n    description:\n      'The index options selected during FT.CREATE such as FILTER {filter}, LANGUAGE {default_lang}, etc.',\n    type: IndexOptionsDto,\n  })\n  @Expose()\n  index_options: IndexOptionsDto;\n\n  @ApiProperty({\n    description:\n      'Includes key_type, hash or JSON; prefixes, if any; and default_score.',\n    type: IndexDefinitionDto,\n  })\n  @Expose()\n  index_definition: IndexDefinitionDto;\n\n  @ApiProperty({\n    description: 'The index schema field names, types, and attributes.',\n    type: IndexAttibuteDto,\n    isArray: true,\n  })\n  @Expose()\n  attributes: IndexAttibuteDto[];\n\n  @ApiProperty({\n    description: 'The number of documents.',\n    type: String,\n  })\n  @Expose()\n  num_docs: string;\n\n  @ApiProperty({\n    description: 'The maximum document ID.',\n    type: String,\n  })\n  @Expose()\n  max_doc_id?: string;\n\n  @ApiProperty({\n    description: 'The number of distinct terms.',\n    type: String,\n  })\n  @Expose()\n  num_terms?: string;\n\n  @ApiProperty({\n    description: 'The total number of records.',\n    type: String,\n  })\n  @Expose()\n  num_records?: string;\n\n  // Various size statistics\n  @ApiProperty({\n    description: `The memory used by the inverted index, which is the core data structure\n      used for searching in RediSearch. The size is given in megabytes.`,\n    type: String,\n  })\n  @Expose()\n  inverted_sz_mb?: string;\n\n  @ApiProperty({\n    description: `The memory used by the vector index,\n      which stores any vectors associated with each document.`,\n    type: String,\n  })\n  @Expose()\n  vector_index_sz_mb?: string;\n\n  @ApiProperty({\n    description: 'The total number of blocks in the inverted index.',\n    type: String,\n  })\n  @Expose()\n  total_inverted_index_blocks?: string;\n\n  @ApiProperty({\n    description: `The memory used by the offset vectors,\n      which store positional information for terms in documents.`,\n    type: String,\n  })\n  @Expose()\n  offset_vectors_sz_mb?: string;\n\n  @ApiProperty({\n    description: `The memory used by the document table,\n      which contains metadata about each document in the index.`,\n    type: String,\n  })\n  @Expose()\n  doc_table_size_mb?: string;\n\n  @ApiProperty({\n    description: `The memory used by sortable values,\n      which are values associated with documents and used for sorting purposes.`,\n    type: String,\n  })\n  @Expose()\n  sortable_values_size_mb?: string;\n\n  @ApiProperty({\n    description: 'Tag overhead memory usage in mb',\n    type: String,\n  })\n  @Expose()\n  tag_overhead_sz_mb?: string;\n\n  @ApiProperty({\n    description: 'Text overhead memory usage in mb',\n    type: String,\n  })\n  @Expose()\n  text_overhead_sz_mb?: string;\n\n  @ApiProperty({\n    description: 'Total index memory size in mb',\n    type: String,\n  })\n  @Expose()\n  total_index_memory_sz_mb?: string;\n\n  @ApiProperty({\n    description: `The memory used by the key table,\n      which stores the mapping between document IDs and Redis keys`,\n    type: String,\n  })\n  @Expose()\n  key_table_size_mb?: string;\n\n  @ApiProperty({\n    description: 'The memory used by GEO-related fields.',\n    type: String,\n  })\n  @Expose()\n  geoshapes_sz_mb?: string;\n\n  @ApiProperty({\n    description:\n      'The average number of records (including deletions) per document.',\n    type: String,\n  })\n  @Expose()\n  records_per_doc_avg?: string;\n\n  @ApiProperty({\n    description: 'The average size of each record in bytes.',\n    type: String,\n  })\n  @Expose()\n  bytes_per_record_avg?: string;\n\n  @ApiProperty({\n    description:\n      'The average number of offsets (position information) per term.',\n    type: String,\n  })\n  @Expose()\n  offsets_per_term_avg?: string;\n\n  @ApiProperty({\n    description: 'The average number of bits used for offsets per record.',\n    type: String,\n  })\n  @Expose()\n  offset_bits_per_record_avg?: string;\n\n  // Indexing-related statistics\n  @ApiProperty({\n    description: 'The number of failures encountered during indexing.',\n    type: String,\n  })\n  @Expose()\n  hash_indexing_failures?: string;\n\n  @ApiProperty({\n    description: 'The total time taken for indexing in seconds.',\n    type: String,\n  })\n  @Expose()\n  total_indexing_time?: string;\n\n  @ApiProperty({\n    description: 'Indicates whether the index is currently being generated.',\n    type: String,\n  })\n  @Expose()\n  indexing?: string;\n\n  @ApiProperty({\n    description:\n      'The percentage of the index that has been successfully generated.',\n    type: String,\n  })\n  @Expose()\n  percent_indexed?: string;\n\n  @ApiProperty({\n    description: 'The number of times the index has been used.',\n    type: Number,\n  })\n  @Expose()\n  number_of_uses?: number;\n\n  @ApiProperty({\n    description:\n      'The index deletion flag. A value of 1 indicates index deletion is in progress.',\n    type: Number,\n  })\n  @Expose()\n  cleaning?: number;\n\n  // Other\n  @ApiProperty({\n    description: 'Garbage collection statistics',\n    type: Object,\n  })\n  @Expose()\n  gc_stats?: object;\n\n  @ApiProperty({\n    description: 'Cursor statistics',\n    type: Object,\n  })\n  @Expose()\n  cursor_stats?: object;\n\n  @ApiProperty({\n    description:\n      'Dialect statistics: the number of times the index was searched using each DIALECT, 1 - 4.',\n    type: Object,\n  })\n  @Expose()\n  dialect_stats?: object;\n\n  @ApiProperty({\n    description: `Index error statistics, including indexing failures, last indexing error,\n      and last indexing error key.`,\n    type: Object,\n  })\n  @Expose()\n  ['Index Errors']?: object;\n\n  @ApiProperty({\n    description:\n      'Dialect statistics: the number of times the index was searched using each DIALECT, 1 - 4.',\n    type: FieldStatisticsDto,\n    isArray: true,\n  })\n  @Expose()\n  ['field statistics']?: Array<FieldStatisticsDto>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/index.ts",
    "content": "export * from './create.redisearch-index.dto';\nexport * from './search.redisearch.dto';\nexport * from './list.redisearch-indexes.response';\nexport * from './index.info.dto';\nexport * from './index.delete.dto';\nexport * from './key-indexes.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/key-indexes.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { Expose, Type } from 'class-transformer';\n\nexport class KeyIndexesDto {\n  @ApiProperty({\n    description: 'Key name to look up matching indexes for',\n    type: String,\n  })\n  @IsDefined()\n  @RedisStringType()\n  @IsRedisString()\n  key: RedisString;\n}\n\nexport class IndexSummaryDto {\n  @ApiProperty({\n    description: 'Index name',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'Key prefixes that this index covers',\n    type: String,\n    isArray: true,\n  })\n  @Expose()\n  prefixes: string[];\n\n  @ApiProperty({\n    description: 'Key type (HASH or JSON)',\n    type: String,\n  })\n  @Expose()\n  key_type: string;\n}\n\nexport class KeyIndexesResponse {\n  @ApiProperty({\n    description: 'Indexes that cover the given key',\n    type: IndexSummaryDto,\n    isArray: true,\n  })\n  @Expose()\n  @Type(() => IndexSummaryDto)\n  indexes: IndexSummaryDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/list.redisearch-indexes.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class ListRedisearchIndexesResponse {\n  @ApiProperty({\n    description: 'Indexes names',\n    type: String,\n  })\n  @RedisStringType({ each: true })\n  indexes: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/dto/search.redisearch.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined, IsInt, IsString } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class SearchRedisearchDto {\n  @ApiProperty({\n    description: 'Index Name',\n    type: String,\n  })\n  @IsDefined()\n  @RedisStringType()\n  @IsRedisString()\n  index: RedisString;\n\n  @ApiProperty({\n    description: 'Query to search inside data fields',\n    type: String,\n  })\n  @IsDefined()\n  @IsString()\n  query: string;\n\n  @ApiProperty({\n    description: 'Limit number of results to be returned',\n    type: Number,\n  })\n  @IsDefined()\n  @IsInt()\n  limit: number = 500; // todo use @Default from another PR\n\n  @ApiProperty({\n    description: 'Offset position to start searching',\n    type: Number,\n  })\n  @IsDefined()\n  @IsInt()\n  offset: number = 0; // todo use @Default from another PR\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/key-indexes.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { ForbiddenException } from '@nestjs/common';\nimport { when } from 'jest-when';\nimport {\n  mockBrowserClientMetadata,\n  mockClusterRedisClient,\n  mockDatabaseClientFactory,\n  mockRedisNoPermError,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { buildIndexInfoRaw } from 'src/__mocks__/redisearch';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { KeyIndexesService } from 'src/modules/browser/redisearch/key-indexes.service';\n\nconst mockMovieInfoRaw = buildIndexInfoRaw({\n  indexName: 'idx:movie',\n  prefixes: ['movie:'],\n  numDocs: '10',\n});\n\nconst mockUserInfoRaw = buildIndexInfoRaw({\n  indexName: 'idx:user',\n  prefixes: ['user:'],\n  numDocs: '5',\n});\n\nconst mockGlobalInfoRaw = buildIndexInfoRaw({\n  indexName: 'idx:global',\n  prefixes: [],\n  numDocs: '100',\n});\n\nconst mockMultiPrefixInfoRaw = buildIndexInfoRaw({\n  indexName: 'idx:multi',\n  keyType: 'JSON',\n  prefixes: ['product:', 'item:'],\n  attributes: [['identifier', 'sku', 'attribute', 'sku', 'type', 'TAG']],\n  numDocs: '20',\n});\n\ndescribe('KeyIndexesService', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let service: KeyIndexesService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        KeyIndexesService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<KeyIndexesService>(KeyIndexesService);\n\n    standaloneClient.sendCommand = jest.fn().mockResolvedValue(undefined);\n    clusterClient.sendCommand = jest.fn().mockResolvedValue(undefined);\n    clusterClient.nodes.mockReturnValue([\n      mockStandaloneRedisClient,\n      mockStandaloneRedisClient,\n    ]);\n  });\n\n  describe('getKeyIndexes', () => {\n    it('should return matching index when key matches a prefix', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:movie')]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:movie'], expect.anything())\n        .mockResolvedValue(mockMovieInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'movie:1',\n      });\n\n      expect(result.indexes).toHaveLength(1);\n      expect(result.indexes[0].name).toBe('idx:movie');\n      expect(result.indexes[0].prefixes).toEqual(['movie:']);\n      expect(result.indexes[0].key_type).toBe('HASH');\n    });\n\n    it('should return empty array when key matches no prefix', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:movie')]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:movie'], expect.anything())\n        .mockResolvedValue(mockMovieInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'session:abc',\n      });\n\n      expect(result.indexes).toHaveLength(0);\n    });\n\n    it('should return multiple indexes when key matches several', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([\n          Buffer.from('idx:user'),\n          Buffer.from('idx:global'),\n        ]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:user'], expect.anything())\n        .mockResolvedValue(mockUserInfoRaw);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:global'], expect.anything())\n        .mockResolvedValue(mockGlobalInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'user:42',\n      });\n\n      expect(result.indexes).toHaveLength(2);\n      const names = result.indexes.map((i) => i.name);\n      expect(names).toContain('idx:user');\n      expect(names).toContain('idx:global');\n    });\n\n    it('should match index with empty prefixes to any key', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:global')]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:global'], expect.anything())\n        .mockResolvedValue(mockGlobalInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'anything:here',\n      });\n\n      expect(result.indexes).toHaveLength(1);\n      expect(result.indexes[0].name).toBe('idx:global');\n    });\n\n    it('should match key against index with multiple prefixes', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:multi')]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:multi'], expect.anything())\n        .mockResolvedValue(mockMultiPrefixInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'item:99',\n      });\n\n      expect(result.indexes).toHaveLength(1);\n      expect(result.indexes[0].name).toBe('idx:multi');\n      expect(result.indexes[0].key_type).toBe('JSON');\n    });\n\n    it('should return empty when no indexes exist', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([]);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'movie:1',\n      });\n\n      expect(result.indexes).toHaveLength(0);\n    });\n\n    it('should skip indexes whose FT.INFO fails', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([\n          Buffer.from('idx:movie'),\n          Buffer.from('idx:broken'),\n        ]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:movie'], expect.anything())\n        .mockResolvedValue(mockMovieInfoRaw);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:broken'], expect.anything())\n        .mockRejectedValue(new Error('Unknown index'));\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'movie:1',\n      });\n\n      expect(result.indexes).toHaveLength(1);\n      expect(result.indexes[0].name).toBe('idx:movie');\n    });\n\n    it('should deduplicate index names from cluster shards', async () => {\n      when(clusterClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:movie')]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:movie')]);\n      when(clusterClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:movie'], expect.anything())\n        .mockResolvedValue(mockMovieInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: 'movie:1',\n      });\n\n      expect(result.indexes).toHaveLength(1);\n    });\n\n    it('should throw when FT._LIST fails', async () => {\n      standaloneClient.sendCommand.mockRejectedValue(mockRedisNoPermError);\n\n      await expect(\n        service.getKeyIndexes(mockBrowserClientMetadata, { key: 'movie:1' }),\n      ).rejects.toThrow(ForbiddenException);\n    });\n\n    it('should handle Buffer keys', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT._LIST'])\n        .mockResolvedValue([Buffer.from('idx:movie')]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', 'idx:movie'], expect.anything())\n        .mockResolvedValue(mockMovieInfoRaw);\n\n      const result = await service.getKeyIndexes(mockBrowserClientMetadata, {\n        key: Buffer.from('movie:1'),\n      });\n\n      expect(result.indexes).toHaveLength(1);\n      expect(result.indexes[0].name).toBe('idx:movie');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/key-indexes.service.ts",
    "content": "import { uniq } from 'lodash';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { catchRedisSearchError } from 'src/utils';\nimport { ClientMetadata } from 'src/common/models';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport {\n  IndexInfoDto,\n  IndexSummaryDto,\n  KeyIndexesDto,\n  KeyIndexesResponse,\n} from './dto';\nimport { convertIndexInfoReply } from '../utils/redisIndexInfo';\nimport { getShards } from '../utils';\n\ninterface IndexEntry {\n  name: string;\n  info: IndexInfoDto;\n}\n\n@Injectable()\nexport class KeyIndexesService {\n  private logger = new Logger('KeyIndexesService');\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  /**\n   * Find all indexes whose prefixes cover the given key.\n   * An index with no prefixes matches all keys of its key_type.\n   */\n  public async getKeyIndexes(\n    clientMetadata: ClientMetadata,\n    dto: KeyIndexesDto,\n  ): Promise<KeyIndexesResponse> {\n    this.logger.debug('Getting indexes for key.', clientMetadata);\n\n    try {\n      const { key } = dto;\n      const keyStr = key instanceof Buffer ? key.toString('utf8') : String(key);\n\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const indexNames = await this.listIndexNames(client);\n      const entries = await this.fetchIndexesInfo(client, indexNames);\n      const matchingIndexes = this.findMatchingIndexes(keyStr, entries);\n\n      return plainToInstance(KeyIndexesResponse, { indexes: matchingIndexes });\n    } catch (e) {\n      this.logger.error('Failed to get indexes for key', e, clientMetadata);\n\n      throw catchRedisSearchError(e);\n    }\n  }\n\n  private async listIndexNames(client: RedisClient): Promise<string[]> {\n    const nodes = await getShards(client);\n\n    const listResults = await Promise.all(\n      nodes.map(async (node) => node.sendCommand(['FT._LIST'])),\n    );\n\n    return uniq(\n      (listResults.flat() as Buffer[]).map((idx) => idx.toString('hex')),\n    ).map((hex) => Buffer.from(hex, 'hex').toString('utf8'));\n  }\n\n  private async fetchIndexesInfo(\n    client: RedisClient,\n    indexNames: string[],\n  ): Promise<IndexEntry[]> {\n    const results = await Promise.allSettled(\n      indexNames.map(async (name) => {\n        const infoReply = (await client.sendCommand(['FT.INFO', name], {\n          replyEncoding: 'utf8',\n        })) as string[][];\n\n        return {\n          name,\n          info: convertIndexInfoReply(infoReply) as IndexInfoDto,\n        };\n      }),\n    );\n\n    return results\n      .filter(\n        (r): r is PromiseFulfilledResult<IndexEntry> =>\n          r.status === 'fulfilled',\n      )\n      .map((r) => r.value);\n  }\n\n  private findMatchingIndexes(\n    keyStr: string,\n    entries: IndexEntry[],\n  ): IndexSummaryDto[] {\n    const matching: IndexSummaryDto[] = [];\n\n    for (const { name, info } of entries) {\n      const { index_definition: definition } = info;\n\n      if (!definition) {\n        continue;\n      }\n\n      const { prefixes = [], key_type: keyType = '' } = definition;\n\n      const isMatch =\n        prefixes.length === 0 || prefixes.some((p) => keyStr.startsWith(p));\n\n      if (isMatch) {\n        matching.push(\n          plainToInstance(IndexSummaryDto, {\n            name,\n            prefixes,\n            key_type: keyType,\n          }),\n        );\n      }\n    }\n\n    return matching;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/redisearch.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  HttpCode,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport {\n  CreateRedisearchIndexDto,\n  IndexInfoDto,\n  IndexInfoRequestBodyDto,\n  KeyIndexesDto,\n  KeyIndexesResponse,\n  ListRedisearchIndexesResponse,\n  SearchRedisearchDto,\n} from 'src/modules/browser/redisearch/dto';\nimport { GetKeysWithDetailsResponse } from 'src/modules/browser/keys/dto';\nimport { RedisearchService } from 'src/modules/browser/redisearch/redisearch.service';\nimport { KeyIndexesService } from 'src/modules/browser/redisearch/key-indexes.service';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\nimport { IndexDeleteRequestBodyDto } from './dto/index.delete.dto';\n\n@ApiTags('Browser: RediSearch')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('redisearch')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class RedisearchController extends BrowserBaseController {\n  constructor(\n    private service: RedisearchService,\n    private keyIndexesService: KeyIndexesService,\n  ) {\n    super();\n  }\n\n  @Get('')\n  @ApiOperation({ description: 'Get list of available indexes' })\n  @ApiOkResponse({ type: ListRedisearchIndexesResponse })\n  @ApiRedisParams()\n  @ApiQueryRedisStringEncoding()\n  async list(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<ListRedisearchIndexesResponse> {\n    return this.service.list(clientMetadata);\n  }\n\n  @Post('')\n  @ApiOperation({ description: 'Create redisearch index' })\n  @ApiRedisParams()\n  @HttpCode(201)\n  @ApiBody({ type: CreateRedisearchIndexDto })\n  async createIndex(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateRedisearchIndexDto,\n  ): Promise<void> {\n    return this.service.createIndex(clientMetadata, dto);\n  }\n\n  @Post('search')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Search for keys in index' })\n  @ApiOkResponse({ type: GetKeysWithDetailsResponse })\n  @ApiRedisParams()\n  @ApiQueryRedisStringEncoding()\n  async search(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SearchRedisearchDto,\n  ): Promise<GetKeysWithDetailsResponse> {\n    return this.service.search(clientMetadata, dto);\n  }\n\n  @Post('info')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Get index info' })\n  @ApiOkResponse({ type: IndexInfoDto })\n  async info(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: IndexInfoRequestBodyDto,\n  ): Promise<IndexInfoDto> {\n    return this.service.getInfo(clientMetadata, dto);\n  }\n\n  @Delete('')\n  @HttpCode(204)\n  @ApiOperation({ description: 'Delete index' })\n  async delete(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: IndexDeleteRequestBodyDto,\n  ): Promise<void> {\n    return this.service.deleteIndex(clientMetadata, dto);\n  }\n\n  @Post('key-indexes')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Get indexes that cover a given key' })\n  @ApiOkResponse({ type: KeyIndexesResponse })\n  async getKeyIndexes(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: KeyIndexesDto,\n  ): Promise<KeyIndexesResponse> {\n    return this.keyIndexesService.getKeyIndexes(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/redisearch.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { RedisearchService } from 'src/modules/browser/redisearch/redisearch.service';\nimport { KeyIndexesService } from 'src/modules/browser/redisearch/key-indexes.service';\nimport { RedisearchController } from 'src/modules/browser/redisearch/redisearch.controller';\n\n@Module({})\nexport class RedisearchModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: RedisearchModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: RedisearchModule,\n          },\n        ]),\n      ],\n      controllers: [RedisearchController],\n      providers: [RedisearchService, KeyIndexesService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/redisearch.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  ConflictException,\n  ForbiddenException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { when } from 'jest-when';\nimport {\n  mockBrowserClientMetadata,\n  mockBrowserHistoryService,\n  mockClusterRedisClient,\n  mockDatabaseClientFactory,\n  mockRedisNoPermError,\n  mockRedisUnknownIndexName,\n  mockRedisUnknownIndexNameV8,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { RedisearchService } from 'src/modules/browser/redisearch/redisearch.service';\nimport {\n  RedisearchIndexDataType,\n  RedisearchIndexKeyType,\n} from 'src/modules/browser/redisearch/dto';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { mockIndexInfoDto, mockIndexInfoRaw } from 'src/__mocks__/redisearch';\nimport { QueryLibraryService } from 'src/modules/query-library/query-library.service';\n\nconst keyName1 = Buffer.from('keyName1');\nconst keyName2 = Buffer.from('keyName2');\n\nconst mockCreateRedisearchIndexDto = {\n  index: 'indexName',\n  type: RedisearchIndexKeyType.HASH,\n  prefixes: ['device:', 'user:'],\n  fields: [\n    {\n      name: 'text:field',\n      type: RedisearchIndexDataType.TEXT,\n    },\n    {\n      name: 'coordinates:field',\n      type: RedisearchIndexDataType.GEO,\n    },\n  ],\n};\nconst mockSearchRedisearchDto = {\n  index: 'indexName',\n  query: 'somequery:',\n  limit: 10,\n  offset: 0,\n};\n\ndescribe('RedisearchService', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let service: RedisearchService;\n  let databaseClientFactory: DatabaseClientFactory;\n  let browserHistory;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RedisearchService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: BrowserHistoryService,\n          useFactory: mockBrowserHistoryService,\n        },\n        {\n          provide: QueryLibraryService,\n          useValue: {\n            deleteByIndex: jest.fn().mockResolvedValue(undefined),\n          },\n        },\n      ],\n    }).compile();\n\n    service = module.get<RedisearchService>(RedisearchService);\n    browserHistory = module.get<BrowserHistoryService>(BrowserHistoryService);\n    databaseClientFactory = module.get<DatabaseClientFactory>(\n      DatabaseClientFactory,\n    );\n    browserHistory.create.mockResolvedValue({});\n\n    standaloneClient.sendCommand = jest.fn().mockResolvedValue(undefined);\n    clusterClient.sendCommand = jest.fn().mockResolvedValue(undefined);\n    clusterClient.nodes.mockReturnValue([\n      mockStandaloneRedisClient,\n      mockStandaloneRedisClient,\n    ]);\n  });\n\n  describe('list', () => {\n    it('should get list of indexes for standalone', async () => {\n      standaloneClient.sendCommand.mockResolvedValue([keyName1, keyName2]);\n\n      const list = await service.list(mockBrowserClientMetadata);\n\n      expect(list).toEqual({\n        indexes: [keyName1, keyName2],\n      });\n    });\n    it('should get list of indexes for cluster (handle unique index name)', async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(clusterClient);\n      mockStandaloneRedisClient.sendCommand.mockResolvedValue([\n        keyName1,\n        keyName2,\n      ]);\n\n      const list = await service.list(mockBrowserClientMetadata);\n\n      expect(list).toEqual({\n        indexes: [keyName1, keyName2],\n      });\n    });\n    it('should handle ACL error', async () => {\n      standaloneClient.sendCommand.mockRejectedValueOnce(mockRedisNoPermError);\n\n      try {\n        await service.list(mockBrowserClientMetadata);\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('createIndex', () => {\n    it('should create index for standalone', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']))\n        .mockRejectedValue(mockRedisUnknownIndexName);\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.CREATE']))\n        .mockResolvedValue('OK');\n\n      await service.createIndex(\n        mockBrowserClientMetadata,\n        mockCreateRedisearchIndexDto,\n      );\n\n      expect(standaloneClient.sendCommand).toHaveBeenCalledTimes(2);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        [\n          'FT.CREATE',\n          mockCreateRedisearchIndexDto.index,\n          'ON',\n          mockCreateRedisearchIndexDto.type,\n          'PREFIX',\n          2,\n          ...mockCreateRedisearchIndexDto.prefixes,\n          'SCHEMA',\n          mockCreateRedisearchIndexDto.fields[0].name,\n          mockCreateRedisearchIndexDto.fields[0].type,\n          mockCreateRedisearchIndexDto.fields[1].name,\n          mockCreateRedisearchIndexDto.fields[1].type,\n        ],\n        { replyEncoding: 'utf8' },\n      );\n    });\n    it('should create index for standalone v8', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']))\n        .mockRejectedValue(mockRedisUnknownIndexNameV8);\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.CREATE']))\n        .mockResolvedValue('OK');\n\n      await service.createIndex(\n        mockBrowserClientMetadata,\n        mockCreateRedisearchIndexDto,\n      );\n\n      expect(standaloneClient.sendCommand).toHaveBeenCalledTimes(2);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        [\n          'FT.CREATE',\n          mockCreateRedisearchIndexDto.index,\n          'ON',\n          mockCreateRedisearchIndexDto.type,\n          'PREFIX',\n          2,\n          ...mockCreateRedisearchIndexDto.prefixes,\n          'SCHEMA',\n          mockCreateRedisearchIndexDto.fields[0].name,\n          mockCreateRedisearchIndexDto.fields[0].type,\n          mockCreateRedisearchIndexDto.fields[1].name,\n          mockCreateRedisearchIndexDto.fields[1].type,\n        ],\n        { replyEncoding: 'utf8' },\n      );\n    });\n    it('should create index for cluster', async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(clusterClient);\n      when(clusterClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']))\n        .mockRejectedValue(mockRedisUnknownIndexName);\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.CREATE']))\n        .mockResolvedValueOnce('OK')\n        .mockRejectedValue(new Error('ReplyError: MOVED to somenode'));\n\n      await service.createIndex(\n        mockBrowserClientMetadata,\n        mockCreateRedisearchIndexDto,\n      );\n\n      expect(clusterClient.sendCommand).toHaveBeenCalledTimes(1);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledTimes(2);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        [\n          'FT.CREATE',\n          mockCreateRedisearchIndexDto.index,\n          'ON',\n          mockCreateRedisearchIndexDto.type,\n          'PREFIX',\n          2,\n          ...mockCreateRedisearchIndexDto.prefixes,\n          'SCHEMA',\n          mockCreateRedisearchIndexDto.fields[0].name,\n          mockCreateRedisearchIndexDto.fields[0].type,\n          mockCreateRedisearchIndexDto.fields[1].name,\n          mockCreateRedisearchIndexDto.fields[1].type,\n        ],\n        { replyEncoding: 'utf8' },\n      );\n    });\n    it('should handle already existing index error', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']))\n        .mockReturnValue({ any: 'data' });\n\n      try {\n        await service.createIndex(\n          mockBrowserClientMetadata,\n          mockCreateRedisearchIndexDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(ConflictException);\n      }\n    });\n    it('should handle ACL error (ft.info command)', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']))\n        .mockRejectedValue(mockRedisNoPermError);\n\n      try {\n        await service.createIndex(\n          mockBrowserClientMetadata,\n          mockCreateRedisearchIndexDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n    it('should handle ACL error (ft.create command)', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.INFO']))\n        .mockRejectedValue(mockRedisUnknownIndexName);\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.CREATE']))\n        .mockRejectedValue(mockRedisNoPermError);\n\n      try {\n        await service.createIndex(\n          mockBrowserClientMetadata,\n          mockCreateRedisearchIndexDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('search', () => {\n    it('should search in standalone', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.SEARCH']))\n        .mockResolvedValue([100, keyName1, keyName2]);\n\n      const res = await service.search(\n        mockBrowserClientMetadata,\n        mockSearchRedisearchDto,\n      );\n\n      expect(res).toEqual({\n        cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset,\n        scanned: 2,\n        total: 100,\n        maxResults: null,\n        keys: [\n          {\n            name: keyName1,\n          },\n          {\n            name: keyName2,\n          },\n        ],\n      });\n\n      expect(standaloneClient.sendCommand).toHaveBeenCalledTimes(3);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['FT.CONFIG', 'GET', 'MAXSEARCHRESULTS'],\n        { replyEncoding: 'utf8' },\n      );\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith([\n        'FT.SEARCH',\n        mockSearchRedisearchDto.index,\n        mockSearchRedisearchDto.query,\n        'NOCONTENT',\n        'LIMIT',\n        mockSearchRedisearchDto.offset,\n        mockSearchRedisearchDto.limit,\n      ]);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['TYPE', keyName1],\n        { replyEncoding: 'utf8' },\n      );\n      expect(browserHistory.create).toHaveBeenCalled();\n    });\n    it('should search in cluster', async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(clusterClient);\n      when(clusterClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.SEARCH']))\n        .mockResolvedValue([100, keyName1, keyName2]);\n\n      const res = await service.search(\n        mockBrowserClientMetadata,\n        mockSearchRedisearchDto,\n      );\n\n      expect(res).toEqual({\n        cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset,\n        scanned: 2,\n        total: 100,\n        maxResults: null,\n        keys: [{ name: keyName1 }, { name: keyName2 }],\n      });\n\n      expect(clusterClient.sendCommand).toHaveBeenCalledTimes(3);\n      expect(clusterClient.sendCommand).toHaveBeenCalledWith(\n        ['FT.CONFIG', 'GET', 'MAXSEARCHRESULTS'],\n        { replyEncoding: 'utf8' },\n      );\n      expect(clusterClient.sendCommand).toHaveBeenCalledWith([\n        'FT.SEARCH',\n        mockSearchRedisearchDto.index,\n        mockSearchRedisearchDto.query,\n        'NOCONTENT',\n        'LIMIT',\n        mockSearchRedisearchDto.offset,\n        mockSearchRedisearchDto.limit,\n      ]);\n      expect(clusterClient.sendCommand).toHaveBeenCalledWith(\n        ['TYPE', keyName1],\n        { replyEncoding: 'utf8' },\n      );\n      expect(browserHistory.create).toHaveBeenCalled();\n    });\n    it('should handle ACL error (ft.info command)', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.SEARCH']))\n        .mockRejectedValue(mockRedisNoPermError);\n\n      try {\n        await service.search(\n          mockBrowserClientMetadata,\n          mockSearchRedisearchDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n    it('should call \"TYPE\" and once \"FT.CONFIG GET MAXSEARCHRESULTS\" for all requests', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.SEARCH']))\n        .mockResolvedValue([100, keyName1, keyName2]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.CONFIG', 'GET', 'MAXSEARCHRESULTS'], {\n          replyEncoding: 'utf8',\n        })\n        .mockResolvedValue([['MAXSEARCHRESULTS', '10000']]);\n\n      const res = await service.search(\n        mockBrowserClientMetadata,\n        mockSearchRedisearchDto,\n      );\n      await service.search(mockBrowserClientMetadata, mockSearchRedisearchDto);\n      await service.search(mockBrowserClientMetadata, mockSearchRedisearchDto);\n\n      expect(res).toEqual({\n        cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset,\n        scanned: 2,\n        total: 100,\n        maxResults: 10_000,\n        keys: [\n          {\n            name: keyName1,\n          },\n          {\n            name: keyName2,\n          },\n        ],\n      });\n\n      expect(standaloneClient.sendCommand).toHaveBeenCalledTimes(7);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['FT.CONFIG', 'GET', 'MAXSEARCHRESULTS'],\n        { replyEncoding: 'utf8' },\n      );\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith([\n        'FT.SEARCH',\n        mockSearchRedisearchDto.index,\n        mockSearchRedisearchDto.query,\n        'NOCONTENT',\n        'LIMIT',\n        mockSearchRedisearchDto.offset,\n        mockSearchRedisearchDto.limit,\n      ]);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['TYPE', keyName1],\n        { replyEncoding: 'utf8' },\n      );\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith([\n        'FT.SEARCH',\n        mockSearchRedisearchDto.index,\n        mockSearchRedisearchDto.query,\n        'NOCONTENT',\n        'LIMIT',\n        mockSearchRedisearchDto.offset,\n        mockSearchRedisearchDto.limit,\n      ]);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['TYPE', keyName1],\n        { replyEncoding: 'utf8' },\n      );\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith([\n        'FT.SEARCH',\n        mockSearchRedisearchDto.index,\n        mockSearchRedisearchDto.query,\n        'NOCONTENT',\n        'LIMIT',\n        mockSearchRedisearchDto.offset,\n        mockSearchRedisearchDto.limit,\n      ]);\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['TYPE', keyName1],\n        { replyEncoding: 'utf8' },\n      );\n      expect(browserHistory.create).toHaveBeenCalled();\n    });\n  });\n\n  describe('getInfo', () => {\n    it('should get indexInfo', async () => {\n      const mockIndexName = 'idx:movie';\n      when(standaloneClient.sendCommand)\n        .calledWith(['FT.INFO', mockIndexName], { replyEncoding: 'utf8' })\n        .mockResolvedValue(mockIndexInfoRaw);\n\n      const res = await service.getInfo(mockBrowserClientMetadata, {\n        index: mockIndexName,\n      });\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['FT.INFO', mockIndexName],\n        { replyEncoding: 'utf8' },\n      );\n      expect(res).toEqual(mockIndexInfoDto);\n    });\n\n    it('should throw error if index name was not provided', async () => {\n      await expect(\n        service.getInfo(mockBrowserClientMetadata, { index: '' }),\n      ).rejects.toThrow('Index was not provided');\n    });\n\n    it('should throw error if client was not created', async () => {\n      const error = new Error('Client was not created');\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockRejectedValue(error);\n\n      await expect(\n        service.getInfo(mockBrowserClientMetadata, { index: 'indexName' }),\n      ).rejects.toThrow(InternalServerErrorException);\n    });\n  });\n\n  describe('deleteIndex', () => {\n    it('should delete index for standalone', async () => {\n      const mockIndexName = 'idx:movie';\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.DROPINDEX']))\n        .mockResolvedValue(undefined);\n\n      await service.deleteIndex(mockBrowserClientMetadata, {\n        index: mockIndexName,\n      });\n\n      expect(standaloneClient.sendCommand).toHaveBeenCalledWith(\n        ['FT.DROPINDEX', mockIndexName],\n        { replyEncoding: 'utf8' },\n      );\n    });\n\n    it('should delete index for cluster', async () => {\n      const mockIndexName = 'idx:movie';\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(clusterClient);\n      when(clusterClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.DROPINDEX']))\n        .mockResolvedValue(undefined);\n\n      await service.deleteIndex(mockBrowserClientMetadata, {\n        index: mockIndexName,\n      });\n\n      expect(clusterClient.sendCommand).toHaveBeenCalledWith(\n        ['FT.DROPINDEX', mockIndexName],\n        { replyEncoding: 'utf8' },\n      );\n    });\n\n    it('should handle index not found error', async () => {\n      const mockIndexName = 'idx:movie';\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.DROPINDEX']))\n        .mockRejectedValue(mockRedisUnknownIndexName);\n\n      try {\n        await service.deleteIndex(mockBrowserClientMetadata, {\n          index: mockIndexName,\n        });\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n\n    it('should handle ACL error', async () => {\n      const mockIndexName = 'idx:movie';\n      when(standaloneClient.sendCommand)\n        .calledWith(expect.arrayContaining(['FT.DROPINDEX']))\n        .mockRejectedValue(mockRedisNoPermError);\n\n      try {\n        await service.deleteIndex(mockBrowserClientMetadata, {\n          index: mockIndexName,\n        });\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/redisearch/redisearch.service.ts",
    "content": "import { isUndefined, toNumber, uniq } from 'lodash';\nimport { ConflictException, Injectable, Logger } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { catchRedisSearchError } from 'src/utils';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  CreateRedisearchIndexDto,\n  IndexInfoDto,\n  IndexInfoRequestBodyDto,\n  ListRedisearchIndexesResponse,\n  SearchRedisearchDto,\n} from 'src/modules/browser/redisearch/dto';\nimport { GetKeysWithDetailsResponse } from 'src/modules/browser/keys/dto';\nimport { DEFAULT_MATCH } from 'src/constants';\nimport { plainToInstance } from 'class-transformer';\nimport { BrowserHistoryMode, RedisString } from 'src/common/constants';\nimport { CreateBrowserHistoryDto } from 'src/modules/browser/browser-history/dto';\nimport { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { QueryLibraryService } from 'src/modules/query-library/query-library.service';\nimport {\n  RedisClient,\n  RedisClientCommandArgument,\n} from 'src/modules/redis/client';\nimport { convertIndexInfoReply } from '../utils/redisIndexInfo';\nimport { getShards } from '../utils';\nimport { IndexDeleteRequestBodyDto } from './dto/index.delete.dto';\n\n@Injectable()\nexport class RedisearchService {\n  private maxSearchResults: Map<string, null | number> = new Map();\n\n  private logger = new Logger('RedisearchService');\n\n  constructor(\n    private databaseClientFactory: DatabaseClientFactory,\n    private browserHistory: BrowserHistoryService,\n    private queryLibraryService: QueryLibraryService,\n  ) {}\n\n  /**\n   * Get list of all available redisearch indexes\n   * @param clientMetadata\n   */\n  public async list(\n    clientMetadata: ClientMetadata,\n  ): Promise<ListRedisearchIndexesResponse> {\n    this.logger.debug('Getting all redisearch indexes.', clientMetadata);\n\n    try {\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const nodes = await getShards(client);\n\n      const res = await Promise.all(\n        nodes.map(async (node) => node.sendCommand(['FT._LIST'])),\n      );\n\n      return plainToInstance(ListRedisearchIndexesResponse, {\n        indexes: uniq([].concat(...res).map((idx) => idx.toString('hex'))).map(\n          (idx) => Buffer.from(idx, 'hex'),\n        ),\n      });\n    } catch (e) {\n      this.logger.error('Failed to get redisearch indexes', e, clientMetadata);\n\n      throw catchRedisSearchError(e);\n    }\n  }\n\n  /**\n   * Creates redisearch index\n   * @param clientMetadata\n   * @param dto\n   */\n  public async createIndex(\n    clientMetadata: ClientMetadata,\n    dto: CreateRedisearchIndexDto,\n  ): Promise<void> {\n    this.logger.debug('Creating redisearch index.', clientMetadata);\n\n    try {\n      const { index, type, prefixes, fields } = dto;\n\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      try {\n        const indexInfo = await client.sendCommand(['FT.INFO', index], {\n          replyEncoding: 'utf8',\n        });\n\n        if (indexInfo) {\n          this.logger.error(\n            `Failed to create redisearch index. ${ERROR_MESSAGES.REDISEARCH_INDEX_EXIST}`,\n            clientMetadata,\n          );\n          return Promise.reject(\n            new ConflictException(ERROR_MESSAGES.REDISEARCH_INDEX_EXIST),\n          );\n        }\n      } catch (error) {\n        const noIndexMessages = ['unknown index name', 'no such index'];\n\n        if (\n          !noIndexMessages.some((keyword) =>\n            error.message?.toLowerCase().includes(keyword),\n          )\n        ) {\n          throw error;\n        }\n      }\n\n      const nodes = await getShards(client);\n\n      const commandArgs: any[] = [index, 'ON', type];\n\n      if (prefixes && prefixes.length) {\n        commandArgs.push('PREFIX', prefixes.length, ...prefixes);\n      }\n\n      commandArgs.push(\n        'SCHEMA',\n        ...[].concat(...fields.map((field) => [field.name, field.type])),\n      );\n\n      await Promise.all(\n        nodes.map(async (node) => {\n          try {\n            await node.sendCommand(['FT.CREATE', ...commandArgs], {\n              replyEncoding: 'utf8',\n            });\n          } catch (e) {\n            if (\n              !e.message.includes('MOVED') &&\n              !e.message.includes('already exists')\n            ) {\n              throw e;\n            }\n          }\n        }),\n      );\n\n      return undefined;\n    } catch (e) {\n      this.logger.error('Failed to create redisearch index', e, clientMetadata);\n\n      throw catchRedisSearchError(e);\n    }\n  }\n\n  /**\n   * Gets the info of a given index\n   * @param clientMetadata\n   * @param dto\n   */\n  public async getInfo(\n    clientMetadata: ClientMetadata,\n    dto: IndexInfoRequestBodyDto,\n  ): Promise<IndexInfoDto> {\n    this.logger.debug('Getting index info', clientMetadata);\n\n    try {\n      const { index } = dto;\n\n      if (!index) {\n        throw new Error('Index was not provided');\n      }\n\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const infoReply = (await client.sendCommand(['FT.INFO', index], {\n        replyEncoding: 'utf8',\n      })) as string[][];\n\n      return plainToInstance(IndexInfoDto, convertIndexInfoReply(infoReply));\n    } catch (e) {\n      this.logger.error('Failed to get index info', e, clientMetadata);\n\n      throw catchRedisSearchError(e);\n    }\n  }\n\n  /**\n   * Search for key names using RediSearch module\n   * Response is the same as for keys \"scan\" to have the same behaviour in the browser\n   * @param clientMetadata\n   * @param dto\n   */\n  public async search(\n    clientMetadata: ClientMetadata,\n    dto: SearchRedisearchDto,\n  ): Promise<GetKeysWithDetailsResponse> {\n    this.logger.debug('Searching keys using redisearch.', clientMetadata);\n\n    try {\n      const { index, query, offset, limit } = dto;\n\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      if (isUndefined(this.maxSearchResults.get(clientMetadata.databaseId))) {\n        try {\n          const [[, maxSearchResults]] = (await client.sendCommand(\n            ['FT.CONFIG', 'GET', 'MAXSEARCHRESULTS'],\n            { replyEncoding: 'utf8' },\n          )) as [[string, string]];\n\n          this.maxSearchResults.set(\n            clientMetadata.databaseId,\n            toNumber(maxSearchResults),\n          );\n        } catch (error) {\n          this.maxSearchResults.set(clientMetadata.databaseId, null);\n        }\n      }\n      // Workaround: recalculate limit to not query more then MAXSEARCHRESULTS\n      let safeLimit = limit;\n      const maxSearchResult = this.maxSearchResults.get(\n        clientMetadata.databaseId,\n      );\n\n      if (maxSearchResult && offset + limit > maxSearchResult) {\n        safeLimit =\n          offset <= maxSearchResult ? maxSearchResult - offset : limit;\n      }\n\n      const [total, ...keyNames] = (await client.sendCommand([\n        'FT.SEARCH',\n        index,\n        query,\n        'NOCONTENT',\n        'LIMIT',\n        offset,\n        safeLimit,\n      ])) as [number, RedisString[]];\n\n      let type;\n      if (keyNames.length) {\n        type = await client.sendCommand(\n          ['TYPE', keyNames[0] as unknown as RedisClientCommandArgument],\n          { replyEncoding: 'utf8' },\n        );\n      }\n\n      // Do not save default match \"*\"\n      if (query !== DEFAULT_MATCH) {\n        await this.browserHistory.create(\n          clientMetadata,\n          plainToInstance(CreateBrowserHistoryDto, {\n            filter: { match: query, type: null },\n            mode: BrowserHistoryMode.Redisearch,\n          }),\n        );\n      }\n\n      return plainToInstance(GetKeysWithDetailsResponse, {\n        cursor: limit + offset >= total ? 0 : limit + offset,\n        total,\n        scanned: keyNames.length + offset,\n        keys: keyNames.map((name) => ({ name, type })),\n        maxResults: maxSearchResult,\n      });\n    } catch (e) {\n      this.logger.error(\n        'Failed to search keys using redisearch index',\n        e,\n        clientMetadata,\n      );\n\n      throw catchRedisSearchError(e, { searchLimit: dto.limit });\n    }\n  }\n\n  public async deleteIndex(\n    clientMetadata: ClientMetadata,\n    dto: IndexDeleteRequestBodyDto,\n  ): Promise<void> {\n    this.logger.debug('Deleting redisearch index ', clientMetadata);\n\n    try {\n      const { index } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await client.sendCommand(['FT.DROPINDEX', index], {\n        replyEncoding: 'utf8',\n      });\n\n      try {\n        const indexName =\n          index instanceof Buffer ? index.toString('utf8') : String(index);\n        await this.queryLibraryService.deleteByIndex(\n          clientMetadata.sessionMetadata,\n          clientMetadata.databaseId,\n          indexName,\n        );\n      } catch (e) {\n        this.logger.error(\n          'Failed to cleanup query library items after index deletion',\n          e,\n          clientMetadata,\n        );\n      }\n\n      this.logger.debug(\n        'Successfully deleted redisearch index ',\n        clientMetadata,\n      );\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete redisearch index ',\n        error,\n        clientMetadata,\n      );\n\n      throw catchRedisSearchError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/create.rejson-rl-with-expire.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { CreateRejsonRlDto } from './create.rejson-rl.dto';\n\nexport class CreateRejsonRlWithExpireDto extends IntersectionType(\n  CreateRejsonRlDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/create.rejson-rl.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString, Validate } from 'class-validator';\nimport { SerializedJsonValidator } from 'src/validators';\n\nexport class CreateRejsonRlDto extends KeyDto {\n  @ApiProperty({\n    description: 'Valid json string',\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  @Validate(SerializedJsonValidator)\n  data: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/get.rejson-rl.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsBoolean, IsNotEmpty, IsString } from 'class-validator';\n\nexport class GetRejsonRlDto extends KeyDto {\n  @ApiPropertyOptional({\n    type: String,\n    description: 'Path to look for data',\n  })\n  @IsString()\n  @IsNotEmpty()\n  path?: string = '$';\n\n  @ApiPropertyOptional({\n    type: Boolean,\n    description:\n      \"Don't check for json size and return whole json in path when enabled\",\n  })\n  @IsBoolean()\n  forceRetrieve?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/get.rejson-rl.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { SafeRejsonRlDataDto } from './safe.rejson-rl-data.dto';\n\nexport class GetRejsonRlResponseDto {\n  @ApiProperty({\n    type: Boolean,\n    description: 'Determines if json value was downloaded',\n  })\n  downloaded: boolean;\n\n  @ApiPropertyOptional({\n    type: String,\n    description: 'Type of data in the requested path',\n  })\n  type?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n    description: 'Requested path',\n  })\n  path?: string;\n\n  @ApiProperty({\n    type: () => SafeRejsonRlDataDto,\n    isArray: true,\n  })\n  data: SafeRejsonRlDataDto[] | string | number | boolean | null;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/index.ts",
    "content": "export * from './create.rejson-rl.dto';\nexport * from './create.rejson-rl-with-expire.dto';\nexport * from './get.rejson-rl.dto';\nexport * from './get.rejson-rl.response';\nexport * from './modify.rejson-rl-arr-append.dto';\nexport * from './modify.rejson-rl-set.dto';\nexport * from './remove.rejson-rl.dto';\nexport * from './remove.rejson-rl.response';\nexport * from './safe.rejson-rl-data.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/modify.rejson-rl-arr-append.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsNotEmpty, IsString, Validate } from 'class-validator';\nimport { SerializedJsonValidator } from 'src/validators';\n\nexport class ModifyRejsonRlArrAppendDto extends KeyDto {\n  @ApiProperty({\n    type: String,\n    description: 'Path of the json field',\n  })\n  @IsString()\n  @IsNotEmpty()\n  path: string;\n\n  @ApiProperty({\n    description: 'Array of valid serialized jsons',\n    type: String,\n    isArray: true,\n  })\n  @IsArray()\n  @Validate(SerializedJsonValidator, {\n    each: true,\n  })\n  @IsNotEmpty({ each: true })\n  @IsString({ each: true })\n  data: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/modify.rejson-rl-set.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString, Validate } from 'class-validator';\nimport { SerializedJsonValidator } from 'src/validators';\n\nexport class ModifyRejsonRlSetDto extends KeyDto {\n  @ApiProperty({\n    type: String,\n    description: 'Path of the json field',\n  })\n  @IsString()\n  @IsNotEmpty()\n  path: string;\n\n  @ApiProperty({\n    description: 'Array of valid serialized jsons',\n    type: String,\n  })\n  @Validate(SerializedJsonValidator)\n  @IsNotEmpty()\n  @IsString()\n  data: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/remove.rejson-rl.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class RemoveRejsonRlDto extends KeyDto {\n  @ApiProperty({\n    type: String,\n    description: 'Path of the json field',\n  })\n  @IsString()\n  @IsNotEmpty()\n  path: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/remove.rejson-rl.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class RemoveRejsonRlResponse {\n  @ApiProperty({\n    description: 'Integer , specifically the number of paths deleted (0 or 1).',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/dto/safe.rejson-rl-data.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nenum RejsonRlDataType {\n  String = 'string',\n  Number = 'number',\n  Integer = 'integer',\n  Boolean = 'boolean',\n  Null = 'null',\n  Array = 'array',\n  Object = 'object',\n}\n\nexport class SafeRejsonRlDataDto {\n  @ApiProperty({\n    type: String,\n    description: 'Key inside json data',\n  })\n  key: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Path of the json field',\n  })\n  path: string;\n\n  @ApiPropertyOptional({\n    type: Number,\n    description:\n      'Number of properties/elements inside field (for object and arrays only)',\n  })\n  cardinality?: number;\n\n  @ApiProperty({\n    enum: RejsonRlDataType,\n    description: 'Type of the field',\n  })\n  type: RejsonRlDataType;\n\n  @ApiPropertyOptional({\n    type: String,\n    description: 'Any value',\n  })\n  value?: string | number | boolean | null;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Patch,\n  Post,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport {\n  GetRejsonRlDto,\n  GetRejsonRlResponseDto,\n  CreateRejsonRlWithExpireDto,\n  ModifyRejsonRlSetDto,\n  ModifyRejsonRlArrAppendDto,\n  RemoveRejsonRlDto,\n  RemoveRejsonRlResponse,\n} from 'src/modules/browser/rejson-rl/dto';\nimport { RejsonRlService } from 'src/modules/browser/rejson-rl/rejson-rl.service';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: REJSON-RL')\n@Controller('rejson-rl')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class RejsonRlController extends BrowserBaseController {\n  constructor(private service: RejsonRlService) {\n    super();\n  }\n\n  @Post('/get')\n  @ApiRedisInstanceOperation({\n    description: 'Get json properties by path',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description:\n          'Download full data by path or returns description of data inside',\n        type: GetRejsonRlResponseDto,\n      },\n    ],\n  })\n  async getJson(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetRejsonRlDto,\n  ): Promise<GetRejsonRlResponseDto> {\n    return this.service.getJson(clientMetadata, dto);\n  }\n\n  @Post('')\n  @ApiRedisInstanceOperation({\n    description: 'Create new REJSON-RL data type',\n    statusCode: 201,\n  })\n  async createJson(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateRejsonRlWithExpireDto,\n  ): Promise<void> {\n    return this.service.create(clientMetadata, dto);\n  }\n\n  @Patch('/set')\n  @ApiRedisInstanceOperation({\n    description: 'Modify REJSON-RL data type by path',\n    statusCode: 200,\n  })\n  async jsonSet(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: ModifyRejsonRlSetDto,\n  ): Promise<void> {\n    return this.service.jsonSet(clientMetadata, dto);\n  }\n\n  @Patch('/arrappend')\n  @ApiRedisInstanceOperation({\n    description: 'Append item inside REJSON-RL array',\n    statusCode: 200,\n  })\n  async arrAppend(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: ModifyRejsonRlArrAppendDto,\n  ): Promise<void> {\n    return this.service.arrAppend(clientMetadata, dto);\n  }\n\n  @Delete('')\n  @ApiRedisInstanceOperation({\n    description: 'Removes path in the REJSON-RL',\n    statusCode: 200,\n  })\n  async remove(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: RemoveRejsonRlDto,\n  ): Promise<RemoveRejsonRlResponse> {\n    return this.service.remove(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { RejsonRlController } from 'src/modules/browser/rejson-rl/rejson-rl.controller';\nimport { RejsonRlService } from 'src/modules/browser/rejson-rl/rejson-rl.service';\n\n@Module({})\nexport class RejsonRlModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: RejsonRlModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: RejsonRlModule,\n          },\n        ]),\n      ],\n      controllers: [RejsonRlController],\n      providers: [RejsonRlService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.spec.ts",
    "content": "import {\n  BadRequestException,\n  ConflictException,\n  ForbiddenException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { randomBytes } from 'crypto';\nimport { when } from 'jest-when';\nimport {\n  mockRedisNoPermError,\n  mockRedisWrongTypeError,\n  mockBrowserClientMetadata,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n  mockDatabaseService,\n  MockType,\n  mockDatabaseWithModules,\n} from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolRejsonRlCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { mockAddFieldsDto } from 'src/modules/browser/__mocks__';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport config, { Config } from 'src/utils/config';\nimport { RejsonRlService } from './rejson-rl.service';\n\nconst mockModulesConfig = config.get('modules') as Config['modules'];\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\nconst testKey = Buffer.from('somejson');\nconst testSerializedObject = JSON.stringify({ some: 'object' });\nconst testPath = '$';\nconst testExpire = 30;\n\ndescribe('JsonService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: RejsonRlService;\n  let databaseService: MockType<DatabaseService>;\n\n  beforeEach(async () => {\n    mockModulesConfig.json.lengthThreshold = -1;\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RejsonRlService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<RejsonRlService>(RejsonRlService);\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n    databaseService = module.get(DatabaseService);\n  });\n\n  describe('getJson', () => {\n    const mockRedisCallsForSafeResponse = (\n      path,\n      key,\n      type,\n      value,\n      cardinality = 0,\n    ) => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolRejsonRlCommands.JsonType, testKey, path], {\n          replyEncoding: 'utf8',\n        })\n        .mockReturnValue([type]);\n\n      if (value !== undefined) {\n        when(client.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonGet, testKey, path], {\n            replyEncoding: 'utf8',\n          })\n          .mockReturnValue(JSON.stringify([value]));\n      }\n\n      switch (type) {\n        case 'array':\n          when(client.sendCommand)\n            .calledWith(\n              [BrowserToolRejsonRlCommands.JsonArrLen, testKey, path],\n              { replyEncoding: 'utf8' },\n            )\n            .mockReturnValue([cardinality]);\n          break;\n        case 'object':\n          when(client.sendCommand)\n            .calledWith(\n              [BrowserToolRejsonRlCommands.JsonObjLen, testKey, path],\n              { replyEncoding: 'utf8' },\n            )\n            .mockReturnValue([cardinality]);\n          break;\n        default:\n      }\n    };\n\n    describe('full json download', () => {\n      beforeEach(() => {\n        databaseService.get.mockResolvedValue(mockDatabaseWithModules);\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            '.',\n          ])\n          .mockReturnValue(10);\n      });\n\n      it('should throw BadRequest error when no key found in the database', async () => {\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            '.',\n          ])\n          .mockResolvedValue(null);\n\n        try {\n          await service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n          });\n        } catch (err) {\n          expect(err).toBeInstanceOf(BadRequestException);\n          expect(err.message).toEqual(\n            `There is no such path: \"${testPath}\" in key: \"${testKey}\"`,\n          );\n        }\n      });\n      it('should throw BadRequest error when incorrect type of a key', async () => {\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockResolvedValue(null);\n\n        try {\n          await service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n            forceRetrieve: true,\n          });\n        } catch (err) {\n          expect(err).toBeInstanceOf(BadRequestException);\n        }\n      });\n      it('should throw BadRequest when try to force get not existing path/key', async () => {\n        const replyError: ReplyError = {\n          ...mockRedisWrongTypeError,\n          command: 'JSON.DEBUG',\n        };\n        client.sendCommand.mockRejectedValue(replyError);\n\n        try {\n          await service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n          });\n        } catch (err) {\n          expect(err).toBeInstanceOf(BadRequestException);\n        }\n      });\n      it('should throw BadRequest error when module not loaded for getJson', async () => {\n        const replyError: ReplyError = {\n          name: 'ReplyError',\n          message: `unknown command ${BrowserToolRejsonRlCommands.JsonGet}`,\n          command: BrowserToolRejsonRlCommands.JsonGet,\n        };\n        client.sendCommand.mockRejectedValue(replyError);\n\n        try {\n          await service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n          });\n        } catch (err) {\n          expect(err).toBeInstanceOf(BadRequestException);\n          expect(err.message).toEqual(\n            ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n          );\n        }\n      });\n      it('should throw InternalError when some unexpected error happened', async () => {\n        client.sendCommand.mockRejectedValue(new Error()); // no message here\n\n        try {\n          await service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n          });\n        } catch (err) {\n          expect(err).toBeInstanceOf(InternalServerErrorException);\n        }\n      });\n      it('should return data (string)', async () => {\n        const testData = 'some string';\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return data (number)', async () => {\n        const testData = 3.14;\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return data (integer)', async () => {\n        const testData = 123;\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return data (boolean)', async () => {\n        const testData = true;\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return data (null)', async () => {\n        const testData = null;\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return data (array)', async () => {\n        const testData = [\n          1,\n          'str',\n          false,\n          null,\n          0.98,\n          [1, 2],\n          { some: 'field' },\n        ];\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return data (object)', async () => {\n        const testData = {\n          someStr: 'field',\n          someArr: [],\n          someBool: true,\n          someNumber: 12.22,\n          someInt: 1222,\n        };\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should return full json data when forceRetrieve is true', async () => {\n        const testData = {\n          someStr: 'field',\n          someArr: [],\n          someBool: true,\n          someNumber: 12.22,\n          someInt: 1222,\n        };\n\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            '.',\n          ])\n          .mockReturnValue(1025);\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          forceRetrieve: true,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should handle json with \"constructor\" key when forceRetrieve is true', async () => {\n        const testData = {\n          constructor: 'example value',\n          nested: { constructor: 123 },\n        };\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          forceRetrieve: true,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n      it('should handle json with \"__proto__\" key when forceRetrieve is true', async () => {\n        const rawJson = '{\"__proto__\":\"proto_value\",\"normal\":\"data\"}';\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(`[${rawJson}]`);\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          forceRetrieve: true,\n        });\n\n        expect(result.downloaded).toBe(true);\n        expect(result.path).toBe(testPath);\n        expect(result.data).toContain('__proto__');\n        expect(result.data).toContain('proto_value');\n      });\n    });\n    describe('user has no PERM for JSON.DEBUG', () => {\n      beforeEach(() => {\n        databaseService.get.mockResolvedValue(mockDatabaseWithModules);\n        const replyError: ReplyError = {\n          ...mockRedisNoPermError,\n          command: 'JSON.DEBUG',\n        };\n\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            '.',\n          ])\n          .mockRejectedValue(replyError);\n      });\n\n      it('should return data (string)', async () => {\n        const testData = 'some string';\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n\n      it('should return full json value even if size is above the limit', async () => {\n        const testData = { arr: [randomBytes(2000).toString('hex')] };\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonType, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue('object');\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: true,\n          path: testPath,\n          data: JSON.stringify(testData),\n        });\n      });\n    });\n    describe('partial json download', () => {\n      beforeEach(() => {\n        databaseService.get.mockResolvedValue(mockDatabaseWithModules);\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            '.',\n          ])\n          .mockReturnValue(1025);\n      });\n\n      it('should return full string value even if size is above the limit', async () => {\n        const testData = randomBytes(2000).toString('hex');\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonGet, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(JSON.stringify([testData]));\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonType, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(['string']);\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: false,\n          path: testPath,\n          data: JSON.stringify(testData),\n          type: 'string',\n        });\n      });\n      it('should return array with scalar values as strings and safe struct types descriptions', async () => {\n        const testData = [\n          12,\n          3.14,\n          'str',\n          false,\n          null,\n          [1, 2, 3],\n          { key1: 'value1', key2: 'value2' },\n        ];\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonType, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(['array']);\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonArrLen, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue([7]);\n\n        mockRedisCallsForSafeResponse('$[0]', 0, 'integer', testData[0]);\n        mockRedisCallsForSafeResponse('$[1]', 1, 'number', testData[1]);\n        mockRedisCallsForSafeResponse('$[2]', 2, 'string', testData[2]);\n        mockRedisCallsForSafeResponse('$[3]', 3, 'boolean', testData[3]);\n        mockRedisCallsForSafeResponse('$[4]', 4, 'null', testData[4]);\n        mockRedisCallsForSafeResponse('$[5]', 5, 'array', undefined, 3);\n        mockRedisCallsForSafeResponse('$[6]', 6, 'object', undefined, 2);\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: false,\n          path: testPath,\n          type: 'array',\n          data: [\n            {\n              key: 0,\n              path: '$[0]',\n              cardinality: 1,\n              type: 'integer',\n              value: String(testData[0]),\n            },\n            {\n              key: 1,\n              path: '$[1]',\n              cardinality: 1,\n              type: 'number',\n              value: String(testData[1]),\n            },\n            {\n              key: 2,\n              path: '$[2]',\n              cardinality: 1,\n              type: 'string',\n              value: `\"${testData[2]}\"`,\n            },\n            {\n              key: 3,\n              path: '$[3]',\n              cardinality: 1,\n              type: 'boolean',\n              value: String(testData[3]),\n            },\n            {\n              key: 4,\n              path: '$[4]',\n              cardinality: 1,\n              type: 'null',\n              value: String(testData[4]),\n            },\n            {\n              key: 5,\n              path: '$[5]',\n              cardinality: 3,\n              type: 'array',\n            },\n            {\n              key: 6,\n              path: '$[6]',\n              cardinality: 2,\n              type: 'object',\n            },\n          ],\n        });\n      });\n      it('should throw an error when array length exceeds threshold', async () => {\n        mockModulesConfig.json.lengthThreshold = 5_000;\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonType, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(['array']);\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonArrLen, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue([5001]);\n\n        await expect(\n          service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n          }),\n        ).rejects.toThrow(\n          new BadRequestException(ERROR_MESSAGES.UNSAFE_BIG_JSON_LENGTH),\n        );\n      });\n      it('should return array with scalar values in a custom path', async () => {\n        const path = '$[\"customPath\"]';\n        const testData = [12, 'str'];\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            path,\n          ])\n          .mockReturnValue(1025);\n        when(client.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, testKey, path], {\n            replyEncoding: 'utf8',\n          })\n          .mockReturnValue(['array']);\n        when(client.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonArrLen, testKey, path], {\n            replyEncoding: 'utf8',\n          })\n          .mockReturnValue([2]);\n\n        mockRedisCallsForSafeResponse(`${path}[0]`, 0, 'integer', testData[0]);\n        mockRedisCallsForSafeResponse(`${path}[1]`, 1, 'string', testData[1]);\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path,\n        });\n\n        expect(result).toEqual({\n          downloaded: false,\n          path,\n          type: 'array',\n          data: [\n            {\n              key: 0,\n              path: `${path}[0]`,\n              cardinality: 1,\n              type: 'integer',\n              value: String(testData[0]),\n            },\n            {\n              key: 1,\n              path: `${path}[1]`,\n              cardinality: 1,\n              type: 'string',\n              value: `\"${testData[1]}\"`,\n            },\n          ],\n        });\n      });\n      it('should return object with scalar values as strings and safe struct types descriptions', async () => {\n        const testData = {\n          fInt: 12,\n          fNum: 3.14,\n          fStr: 'str',\n          fBool: false,\n          fNull: null,\n          fArr: [1, 2, 3],\n          fObj: { key1: 'value1', key2: 'value2' },\n        };\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonType, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(['object']);\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonObjKeys, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue([Object.keys(testData)]);\n\n        mockRedisCallsForSafeResponse(\n          '$[\"fInt\"]',\n          'fInt',\n          'integer',\n          testData.fInt,\n        );\n        mockRedisCallsForSafeResponse(\n          '$[\"fNum\"]',\n          'fNum',\n          'number',\n          testData.fNum,\n        );\n        mockRedisCallsForSafeResponse(\n          '$[\"fStr\"]',\n          'fStr',\n          'string',\n          testData.fStr,\n        );\n        mockRedisCallsForSafeResponse(\n          '$[\"fBool\"]',\n          'fBool',\n          'boolean',\n          testData.fBool,\n        );\n        mockRedisCallsForSafeResponse(\n          '$[\"fNull\"]',\n          'fNull',\n          'null',\n          testData.fNull,\n        );\n        mockRedisCallsForSafeResponse(\n          '$[\"fArr\"]',\n          'fArr',\n          'array',\n          undefined,\n          3,\n        );\n        mockRedisCallsForSafeResponse(\n          '$[\"fObj\"]',\n          'fObj',\n          'object',\n          undefined,\n          2,\n        );\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n\n        expect(result).toEqual({\n          downloaded: false,\n          path: testPath,\n          type: 'object',\n          data: [\n            {\n              key: 'fInt',\n              path: '$[\"fInt\"]',\n              cardinality: 1,\n              type: 'integer',\n              value: String(testData.fInt),\n            },\n            {\n              key: 'fNum',\n              path: '$[\"fNum\"]',\n              cardinality: 1,\n              type: 'number',\n              value: String(testData.fNum),\n            },\n            {\n              key: 'fStr',\n              path: '$[\"fStr\"]',\n              cardinality: 1,\n              type: 'string',\n              value: `\"${testData.fStr}\"`,\n            },\n            {\n              key: 'fBool',\n              path: '$[\"fBool\"]',\n              cardinality: 1,\n              type: 'boolean',\n              value: String(testData.fBool),\n            },\n            {\n              key: 'fNull',\n              path: '$[\"fNull\"]',\n              cardinality: 1,\n              type: 'null',\n              value: String(testData.fNull),\n            },\n            {\n              key: 'fArr',\n              path: '$[\"fArr\"]',\n              cardinality: 3,\n              type: 'array',\n            },\n            {\n              key: 'fObj',\n              path: '$[\"fObj\"]',\n              cardinality: 2,\n              type: 'object',\n            },\n          ],\n        });\n      });\n      it('should throw an error when number of object entries exceeds threshold', async () => {\n        mockModulesConfig.json.lengthThreshold = 5_000;\n\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonType, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue(['object']);\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonObjLen, testKey, testPath],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue([10_000]);\n\n        await expect(\n          service.getJson(mockBrowserClientMetadata, {\n            keyName: testKey,\n            path: testPath,\n          }),\n        ).rejects.toThrow(\n          new BadRequestException(ERROR_MESSAGES.UNSAFE_BIG_JSON_LENGTH),\n        );\n      });\n      it('should return object with scalar values as strings in a custom path', async () => {\n        const path = '$[\"customPath\"]';\n        const testData = {\n          fInt: 12,\n          fStr: 'str',\n        };\n\n        when(client.sendCommand)\n          .calledWith([\n            BrowserToolRejsonRlCommands.JsonDebug,\n            'MEMORY',\n            testKey,\n            path,\n          ])\n          .mockReturnValue(1025);\n        when(client.sendCommand)\n          .calledWith([BrowserToolRejsonRlCommands.JsonType, testKey, path], {\n            replyEncoding: 'utf8',\n          })\n          .mockReturnValue(['object']);\n        when(client.sendCommand)\n          .calledWith(\n            [BrowserToolRejsonRlCommands.JsonObjKeys, testKey, path],\n            { replyEncoding: 'utf8' },\n          )\n          .mockReturnValue([Object.keys(testData)]);\n\n        mockRedisCallsForSafeResponse(\n          `${path}[\"fInt\"]`,\n          'fInt',\n          'integer',\n          testData.fInt,\n        );\n        mockRedisCallsForSafeResponse(\n          `${path}[\"fStr\"]`,\n          'fStr',\n          'string',\n          testData.fStr,\n        );\n\n        const result = await service.getJson(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path,\n        });\n\n        expect(result).toEqual({\n          downloaded: false,\n          path,\n          type: 'object',\n          data: [\n            {\n              key: 'fInt',\n              path: `${path}[\"fInt\"]`,\n              cardinality: 1,\n              type: 'integer',\n              value: String(testData.fInt),\n            },\n            {\n              key: 'fStr',\n              path: `${path}[\"fStr\"]`,\n              cardinality: 1,\n              type: 'string',\n              value: `\"${testData.fStr}\"`,\n            },\n          ],\n        });\n      });\n    });\n\n    describe('prepareJsonPath', () => {\n      it('should convert \"$\" to \".\" for RedisJSON v1 (legacy)', async () => {\n        const database = {\n          ...mockDatabaseWithModules,\n          modules: [{ name: 'ReJSON', semanticVersion: ['1', '0', '0'] }],\n        };\n        databaseService.get.mockResolvedValue(database);\n\n        const result = await service['prepareJsonPath'](\n          mockBrowserClientMetadata,\n          '$',\n        );\n\n        expect(result).toBe('.');\n      });\n\n      it('should strip \"$\" from path for RedisJSON v1', async () => {\n        const database = {\n          ...mockDatabaseWithModules,\n          modules: [{ name: 'ReJSON', semanticVersion: ['1', '4', '2'] }],\n        };\n        databaseService.get.mockResolvedValue(database);\n\n        const result = await service['prepareJsonPath'](\n          mockBrowserClientMetadata,\n          '$.user.name',\n        );\n\n        expect(result).toBe('.user.name');\n      });\n\n      it('should fallback to dot path when semanticVersion is missing', async () => {\n        const database = {\n          ...mockDatabaseWithModules,\n          modules: [\n            { name: 'ReJSON' }, // no semanticVersion field\n          ],\n        };\n        databaseService.get.mockResolvedValue(database);\n\n        const result = await service['prepareJsonPath'](\n          mockBrowserClientMetadata,\n          '$',\n        );\n\n        expect(result).toBe('.');\n      });\n\n      it('should return original path for RedisJSON v2', async () => {\n        const database = {\n          ...mockDatabaseWithModules,\n          modules: [{ name: 'ReJSON', semanticVersion: ['2', '0', '0'] }],\n        };\n        databaseService.get.mockResolvedValue(database);\n\n        const result = await service['prepareJsonPath'](\n          mockBrowserClientMetadata,\n          '$.user.name',\n        );\n\n        expect(result).toBe('$.user.name');\n      });\n    });\n  });\n  describe('create', () => {\n    beforeEach(() => {\n      databaseService.get.mockResolvedValue(mockDatabaseWithModules);\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddFieldsDto.keyName])\n        .mockResolvedValue(false);\n      client.sendCommand.mockReturnValue('OK');\n    });\n    it('should throw Conflict error when key is already in the database', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddFieldsDto.keyName])\n        .mockResolvedValue(true);\n      client.sendCommand.mockReturnValue(null);\n\n      try {\n        await service.create(mockBrowserClientMetadata, {\n          keyName: testKey,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(ConflictException);\n        expect(err.message).toEqual(ERROR_MESSAGES.KEY_NAME_EXIST);\n      }\n    });\n    it('should throw Forbidden error when no perms for an action for create', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.create(mockBrowserClientMetadata, {\n          keyName: testKey,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(ForbiddenException);\n      }\n    });\n    it('should throw BadRequest error when module not loaded for create', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        message: `unknown command ${BrowserToolRejsonRlCommands.JsonSet}`,\n        command: BrowserToolRejsonRlCommands.JsonSet,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.create(mockBrowserClientMetadata, {\n          keyName: testKey,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(BadRequestException);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        );\n      }\n    });\n    it('should silently handle key expire error and log it', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n      };\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolRejsonRlCommands.JsonSet,\n          testKey,\n          testPath,\n          testSerializedObject,\n          'NX',\n        ])\n        .mockReturnValue('OK');\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Expire, testKey, testExpire])\n        .mockRejectedValue(replyError);\n\n      await service.create(mockBrowserClientMetadata, {\n        keyName: testKey,\n        data: testSerializedObject,\n        expire: testExpire,\n      });\n      expect(client.sendCommand).lastCalledWith([\n        BrowserToolKeysCommands.Expire,\n        testKey,\n        testExpire,\n      ]);\n    });\n    it('should successful create key', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddFieldsDto.keyName])\n        .mockResolvedValue(true);\n      await service.create(mockBrowserClientMetadata, {\n        keyName: testKey,\n        data: testSerializedObject,\n      });\n    });\n  });\n  describe('jsonSet', () => {\n    beforeEach(() => {\n      databaseService.get.mockResolvedValue(mockDatabaseWithModules);\n      client.sendCommand.mockReturnValue('OK');\n    });\n    it('should throw NotFound error when key does not exists for jsonSet', async () => {\n      client.sendCommand.mockReturnValue(0);\n\n      try {\n        await service.jsonSet(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(NotFoundException);\n        expect(err.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw BadRequest error when module not loaded for jsonSet', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        message: `unknown command ${BrowserToolRejsonRlCommands.JsonSet}`,\n        command: BrowserToolRejsonRlCommands.JsonSet,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.jsonSet(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(BadRequestException);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        );\n      }\n    });\n    it('should throw NotFound error when try to set to the incorrect path', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        command: 'json.set',\n        message: \"ERR index '[7]' out of range at level 1 in path\",\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.jsonSet(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(NotFoundException);\n        expect(err.message).toEqual(ERROR_MESSAGES.PATH_NOT_EXISTS());\n      }\n    });\n    it('should throw Forbidden error when no perms for an action for jsonSet', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.jsonSet(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: testSerializedObject,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(ForbiddenException);\n      }\n    });\n    it('should successful modify data', async () => {\n      await service.jsonSet(mockBrowserClientMetadata, {\n        keyName: testKey,\n        path: testPath,\n        data: testSerializedObject,\n      });\n\n      expect(client.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Exists,\n        testKey,\n      ]);\n      expect(client.sendCommand).lastCalledWith([\n        BrowserToolRejsonRlCommands.JsonSet,\n        testKey,\n        testPath,\n        testSerializedObject,\n      ]);\n    });\n  });\n  describe('arrAppend', () => {\n    beforeEach(() => {\n      client.sendCommand.mockReturnValue('OK');\n    });\n    it('should throw NotFound error when key does not exists', async () => {\n      client.sendCommand.mockReturnValue(0);\n\n      try {\n        await service.arrAppend(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: [testSerializedObject],\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(NotFoundException);\n        expect(err.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw BadRequest error when module not loaded', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        message: `unknown command ${BrowserToolRejsonRlCommands.JsonArrAppend}`,\n        command: BrowserToolRejsonRlCommands.JsonArrAppend,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.arrAppend(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: [testSerializedObject],\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(BadRequestException);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        );\n      }\n    });\n    it('should throw Forbidden error when no perms for an action', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.arrAppend(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n          data: [testSerializedObject],\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(ForbiddenException);\n      }\n    });\n    it('should successful modify data', async () => {\n      client.sendCommand.mockReturnValueOnce('OK');\n      // JSON.ARRAPEND returns an array of integer replies for each path, the array's new size,\n      // or nil, if the matching JSON value is not an array\n      client.sendCommand.mockReturnValueOnce([10]);\n      const database = {\n        ...mockDatabaseWithModules,\n        modules: [{ name: 'ReJSON', semanticVersion: ['2', '0', '0'] }],\n      };\n      databaseService.get.mockResolvedValue(database);\n      await service.arrAppend(mockBrowserClientMetadata, {\n        keyName: testKey,\n        path: testPath,\n        data: [testSerializedObject, testSerializedObject],\n      });\n\n      expect(client.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Exists,\n        testKey,\n      ]);\n      expect(client.sendCommand).lastCalledWith([\n        BrowserToolRejsonRlCommands.JsonArrAppend,\n        testKey,\n        testPath,\n        testSerializedObject,\n        testSerializedObject,\n      ]);\n    });\n  });\n  describe('remove', () => {\n    beforeEach(() => {\n      databaseService.get.mockResolvedValue(mockDatabaseWithModules);\n      client.sendCommand.mockReturnValue('OK');\n    });\n    it('should throw NotFound error when key does not exists', async () => {\n      client.sendCommand.mockReturnValue(0);\n\n      try {\n        await service.remove(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(NotFoundException);\n        expect(err.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw BadRequest error when module not loaded', async () => {\n      const replyError: ReplyError = {\n        name: 'ReplyError',\n        message: `unknown command ${BrowserToolRejsonRlCommands.JsonDel}`,\n        command: BrowserToolRejsonRlCommands.JsonDel,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.remove(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(BadRequestException);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        );\n      }\n    });\n    it('should throw Forbidden error when no perms for an action', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      try {\n        await service.remove(mockBrowserClientMetadata, {\n          keyName: testKey,\n          path: testPath,\n        });\n      } catch (err) {\n        expect(err).toBeInstanceOf(ForbiddenException);\n      }\n    });\n    it('should successful remove path', async () => {\n      await service.remove(mockBrowserClientMetadata, {\n        keyName: testKey,\n        path: testPath,\n      });\n\n      expect(client.sendCommand).toHaveBeenNthCalledWith(1, [\n        BrowserToolKeysCommands.Exists,\n        testKey,\n      ]);\n      expect(client.sendCommand).lastCalledWith([\n        BrowserToolRejsonRlCommands.JsonDel,\n        testKey,\n        testPath,\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.ts",
    "content": "import {\n  BadRequestException,\n  ConflictException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport * as JSONBigInt from 'json-bigint';\nimport { AdditionalRedisModuleName, RedisErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { catchAclError } from 'src/utils';\nimport config, { Config } from 'src/utils/config';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  CreateRejsonRlWithExpireDto,\n  GetRejsonRlDto,\n  GetRejsonRlResponseDto,\n  ModifyRejsonRlArrAppendDto,\n  ModifyRejsonRlSetDto,\n  RemoveRejsonRlDto,\n  RemoveRejsonRlResponse,\n  SafeRejsonRlDataDto,\n} from 'src/modules/browser/rejson-rl/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolRejsonRlCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { RedisString } from 'src/common/constants';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { DatabaseService } from 'src/modules/database/database.service';\n\nconst MODULES_CONFIG = config.get('modules') as Config['modules'];\nconst JSONbig = JSONBigInt({\n  protoAction: 'preserve',\n  constructorAction: 'preserve',\n});\n\n@Injectable()\nexport class RejsonRlService {\n  private logger = new Logger('JsonService');\n\n  constructor(\n    private databaseClientFactory: DatabaseClientFactory,\n    private databaseService: DatabaseService,\n  ) {}\n\n  private async prepareJsonPath(\n    clientMetadata: ClientMetadata,\n    path: string,\n  ): Promise<string> {\n    const database = await this.databaseService.get(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n    );\n\n    const jsonModule = database.modules?.find(\n      (module) => module.name === AdditionalRedisModuleName.RedisJSON,\n    );\n\n    // RedisJSON v1 (legacy) uses a different path format.\n    // If the module version isn't reported, assume legacy format just to be safe.\n    // Ref: https://redis.io/docs/latest/develop/data-types/json/path/#legacy-path-syntax\n    if (!jsonModule?.semanticVersion || jsonModule.semanticVersion[0] === '1') {\n      if (path.length === 1) {\n        return '.';\n      }\n      return path[0] === '$' ? path.slice(1) : path;\n    }\n\n    return path;\n  }\n\n  private async forceGetJson(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n  ): Promise<any> {\n    const data = (await client.sendCommand(\n      [BrowserToolRejsonRlCommands.JsonGet, keyName, path],\n      { replyEncoding: 'utf8' },\n    )) as string;\n\n    if (data === null) {\n      throw new BadRequestException(\n        `There is no such path: \"${path}\" in key: \"${keyName}\"`,\n      );\n    }\n\n    return path[0] === '$' ? JSONbig.stringify(JSONbig.parse(data)[0]) : data;\n  }\n\n  private async estimateSize(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n  ): Promise<number | null> {\n    let size = 0;\n\n    try {\n      size = (await client.sendCommand([\n        BrowserToolRejsonRlCommands.JsonDebug,\n        'MEMORY',\n        keyName,\n        path === '$' ? '.' : path,\n      ])) as number;\n    } catch (error) {\n      this.logger.error('Failed to estimate size of json.', error);\n    }\n\n    if (size === null) {\n      throw new BadRequestException(\n        `There is no such path: \"${path}\" in key: \"${keyName}\"`,\n      );\n    }\n\n    return size;\n  }\n\n  private async getObjectKeys(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n  ): Promise<string[]> {\n    const keys = await client.sendCommand(\n      [BrowserToolRejsonRlCommands.JsonObjKeys, keyName, path],\n      { replyEncoding: 'utf8' },\n    );\n\n    return path[0] === '$' ? keys[0] : keys;\n  }\n\n  private async getJsonDataType(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n  ): Promise<string> {\n    const type = await client.sendCommand(\n      [BrowserToolRejsonRlCommands.JsonType, keyName, path],\n      { replyEncoding: 'utf8' },\n    );\n\n    return path[0] === '$' ? type[0] : type;\n  }\n\n  private async isUnsafeBigJsonLength(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n  ) {\n    const type = await this.getJsonDataType(client, keyName, path);\n\n    let length: number | null;\n\n    switch (type) {\n      case 'object':\n        length = (await client.sendCommand(\n          [BrowserToolRejsonRlCommands.JsonObjLen, keyName, path],\n          { replyEncoding: 'utf8' },\n        )) as number;\n\n        break;\n      case 'array':\n        length = (await client.sendCommand(\n          [BrowserToolRejsonRlCommands.JsonArrLen, keyName, path],\n          { replyEncoding: 'utf8' },\n        )) as number;\n\n        break;\n      default:\n        // for the rest types we can safely continue processing\n        // Even for big strings since it should be handled by \"sizeThreshold\" before\n        return false;\n    }\n\n    if (length === null) {\n      throw new BadRequestException(\n        `There is no such path: \"${path}\" in key: \"${keyName}\"`,\n      );\n    }\n\n    return (\n      MODULES_CONFIG.json.lengthThreshold > 0 &&\n      length > MODULES_CONFIG.json.lengthThreshold\n    );\n  }\n\n  private async getDetails(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n    key: string | number,\n  ): Promise<any> {\n    const details = {\n      key,\n      path,\n      cardinality: 1,\n    };\n\n    const objectKeyType = await this.getJsonDataType(client, keyName, path);\n\n    details['type'] = objectKeyType;\n    let cardinality;\n    switch (objectKeyType) {\n      case 'object':\n        cardinality = await client.sendCommand(\n          [BrowserToolRejsonRlCommands.JsonObjLen, keyName, path],\n          { replyEncoding: 'utf8' },\n        );\n\n        details['cardinality'] = path[0] === '$' ? cardinality[0] : cardinality;\n        break;\n      case 'array':\n        cardinality = await client.sendCommand(\n          [BrowserToolRejsonRlCommands.JsonArrLen, keyName, path],\n          { replyEncoding: 'utf8' },\n        );\n\n        details['cardinality'] = path[0] === '$' ? cardinality[0] : cardinality;\n        break;\n      default:\n        details['value'] = await this.forceGetJson(client, keyName, path);\n    }\n\n    return details;\n  }\n\n  private async safeGetJsonByType(\n    client: RedisClient,\n    keyName: RedisString,\n    path: string,\n    type: string,\n  ): Promise<SafeRejsonRlDataDto[]> {\n    const promises = [];\n    let objectKeys: string[];\n    let arrayLength: number;\n\n    switch (type) {\n      case 'object':\n        objectKeys = await this.getObjectKeys(client, keyName, path);\n\n        for (const objectKey of objectKeys) {\n          const rootPath = path === '.' ? '' : path;\n          const childPath = objectKey.includes('\"')\n            ? `['${objectKey}']`\n            : `[\"${objectKey}\"]`;\n          const fullObjectKeyPath = `${rootPath}${childPath}`;\n          promises.push(\n            this.getDetails(client, keyName, fullObjectKeyPath, objectKey),\n          );\n        }\n\n        return Promise.all(promises);\n      case 'array':\n        arrayLength = (await client.sendCommand(\n          [BrowserToolRejsonRlCommands.JsonArrLen, keyName, path],\n          { replyEncoding: 'utf8' },\n        )) as number;\n        if (Array.isArray(arrayLength)) {\n          [arrayLength] = arrayLength;\n        }\n\n        for (let i = 0; i < arrayLength; i += 1) {\n          const fullObjectKeyPath = `${path === '.' ? '' : path}[${i}]`;\n          promises.push(this.getDetails(client, keyName, fullObjectKeyPath, i));\n        }\n\n        return Promise.all(promises);\n      default:\n        return this.forceGetJson(client, keyName, path);\n    }\n  }\n\n  /**\n   * Method to create REJSON-RL type\n   * Supports key TTL\n   *\n   * @param clientMetadata\n   * @param dto\n   */\n  public async create(\n    clientMetadata: ClientMetadata,\n    dto: CreateRejsonRlWithExpireDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating REJSON-RL data type.', clientMetadata);\n      const { keyName, data, expire } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const path = await this.prepareJsonPath(clientMetadata, '$');\n\n      await checkIfKeyExists(keyName, client);\n\n      await client.sendCommand([\n        BrowserToolRejsonRlCommands.JsonSet,\n        keyName,\n        path,\n        data,\n        'NX',\n      ]);\n\n      if (expire) {\n        try {\n          await client.sendCommand([\n            BrowserToolKeysCommands.Expire,\n            keyName,\n            expire,\n          ]);\n        } catch (err) {\n          this.logger.error(\n            `Unable to set expire ${expire} for REJSON-RL key ${keyName}.`,\n            err,\n            clientMetadata,\n          );\n        }\n      }\n\n      this.logger.debug(\n        'Succeed to create REJSON-RL key type.',\n        clientMetadata,\n      );\n    } catch (error) {\n      this.logger.error(\n        'Failed to create REJSON-RL key type.',\n        error,\n        clientMetadata,\n      );\n\n      if (error instanceof ConflictException) {\n        throw error;\n      }\n\n      if (error.message.includes(RedisErrorCodes.UnknownCommand)) {\n        throw new BadRequestException({\n          message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        });\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  public async getJson(\n    clientMetadata: ClientMetadata,\n    dto: GetRejsonRlDto,\n  ): Promise<GetRejsonRlResponseDto> {\n    try {\n      this.logger.debug('Getting json by key.', clientMetadata);\n      const { keyName, path, forceRetrieve } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const jsonPath = await this.prepareJsonPath(clientMetadata, path);\n\n      const result: GetRejsonRlResponseDto = {\n        downloaded: true,\n        path,\n        data: null,\n      };\n\n      // Get value in the path without any checks\n      if (forceRetrieve) {\n        result.data = await this.forceGetJson(client, keyName, jsonPath);\n        return result;\n      }\n\n      const jsonSize = await this.estimateSize(client, keyName, jsonPath);\n\n      if (jsonSize > MODULES_CONFIG.json.sizeThreshold) {\n        // Additional check for cardinality\n        if (await this.isUnsafeBigJsonLength(client, keyName, jsonPath)) {\n          throw new BadRequestException(ERROR_MESSAGES.UNSAFE_BIG_JSON_LENGTH);\n        }\n\n        const type = await this.getJsonDataType(client, keyName, jsonPath);\n        result.downloaded = false;\n        result.type = type;\n        result.data = await this.safeGetJsonByType(\n          client,\n          keyName,\n          jsonPath,\n          type,\n        );\n      } else {\n        result.data = await this.forceGetJson(client, keyName, jsonPath);\n      }\n\n      return result;\n    } catch (error) {\n      this.logger.error('Failed to get json.', error, clientMetadata);\n\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      if (error.message.includes(RedisErrorCodes.UnknownCommand)) {\n        throw new BadRequestException({\n          message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        });\n      }\n\n      // todo: refactor error handling across the project\n      if (error instanceof BadRequestException) {\n        throw error;\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Method to modify REJSON-RL type using JSON.SET command\n   * @param clientMetadata\n   * @param dto\n   */\n  public async jsonSet(\n    clientMetadata: ClientMetadata,\n    dto: ModifyRejsonRlSetDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Modifying REJSON-RL data type.', clientMetadata);\n      const { keyName, path, data } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const jsonPath = await this.prepareJsonPath(clientMetadata, path);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      await this.getJsonDataType(client, keyName, jsonPath);\n      await client.sendCommand([\n        BrowserToolRejsonRlCommands.JsonSet,\n        keyName,\n        jsonPath,\n        data,\n      ]);\n\n      this.logger.debug(\n        'Succeed to modify REJSON-RL key type.',\n        clientMetadata,\n      );\n    } catch (error) {\n      this.logger.error(\n        'Failed to modify REJSON-RL key type.',\n        error,\n        clientMetadata,\n      );\n\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (\n        error.message.includes('index') &&\n        error.message.includes('out of range')\n      ) {\n        throw new NotFoundException(ERROR_MESSAGES.PATH_NOT_EXISTS());\n      }\n\n      if (error.message.includes(RedisErrorCodes.UnknownCommand)) {\n        throw new BadRequestException({\n          message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        });\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Method to modify REJSON-RL type using JSON.ARRAPPEND command\n   * @param clientMetadata\n   * @param dto\n   */\n  public async arrAppend(\n    clientMetadata: ClientMetadata,\n    dto: ModifyRejsonRlArrAppendDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Modifying REJSON-RL data type.', clientMetadata);\n      const { keyName, path, data } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const jsonPath = await this.prepareJsonPath(clientMetadata, path);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = await client.sendCommand([\n        BrowserToolRejsonRlCommands.JsonArrAppend,\n        keyName,\n        jsonPath,\n        ...data,\n      ]);\n\n      // JSON.ARRAPEND returns an array of integer replies for each path, the array's new size,\n      // or nil, if the matching JSON value is not an array.\n      if (jsonPath[0] === '$' && typeof result?.[0] !== 'number') {\n        throw new BadRequestException({\n          message: `ReplyError: ERR Path ${jsonPath} does not exist or not an array`,\n        });\n      }\n\n      this.logger.log('Succeed to modify REJSON-RL key type.', clientMetadata);\n    } catch (error) {\n      this.logger.error(\n        'Failed to modify REJSON-RL key type',\n        error,\n        clientMetadata,\n      );\n\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error.message.includes(RedisErrorCodes.UnknownCommand)) {\n        throw new BadRequestException({\n          message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        });\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Method to remove REJSON-RL path using JSON.DEL command\n   * @param clientMetadata\n   * @param dto\n   */\n  public async remove(\n    clientMetadata: ClientMetadata,\n    dto: RemoveRejsonRlDto,\n  ): Promise<RemoveRejsonRlResponse> {\n    try {\n      this.logger.debug('Removing REJSON-RL data.', clientMetadata);\n      const { keyName, path } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const jsonPath = await this.prepareJsonPath(clientMetadata, path);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const affected = (await client.sendCommand([\n        BrowserToolRejsonRlCommands.JsonDel,\n        keyName,\n        jsonPath,\n      ])) as number;\n\n      this.logger.debug('Succeed to remove REJSON-RL path.', clientMetadata);\n      return { affected };\n    } catch (error) {\n      this.logger.error(\n        'Failed to remove REJSON-RL path.',\n        error,\n        clientMetadata,\n      );\n\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error.message.includes(RedisErrorCodes.UnknownCommand)) {\n        throw new BadRequestException({\n          message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'),\n        });\n      }\n\n      throw catchAclError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/add.members-to-set.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class AddMembersToSetDto extends KeyDto {\n  @ApiProperty({\n    description: 'Set members',\n    isArray: true,\n    type: String,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  members: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/create.set-with-expire.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { AddMembersToSetDto } from './add.members-to-set.dto';\n\nexport class CreateSetWithExpireDto extends IntersectionType(\n  AddMembersToSetDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/delete.members-from-set.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class DeleteMembersFromSetDto extends KeyDto {\n  @ApiProperty({\n    description: 'Key members',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  members: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/delete.members-from-set.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteMembersFromSetResponse {\n  @ApiProperty({\n    description: 'Number of affected members',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/get.set-members.dto.ts",
    "content": "import { ScanDataTypeDto } from 'src/modules/browser/keys/dto';\n\nexport class GetSetMembersDto extends ScanDataTypeDto {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/get.set-members.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class SetScanResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    minimum: 0,\n    description:\n      'The new cursor to use in the next call.' +\n      ' If the property has value of 0, then the iteration is completed.',\n  })\n  nextCursor: number;\n\n  @ApiProperty({\n    type: () => String,\n    description: 'Array of members.',\n    isArray: true,\n  })\n  @RedisStringType({ each: true })\n  members: RedisString[];\n}\n\nexport class GetSetMembersResponse extends SetScanResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'The number of members in the currently-selected set.',\n  })\n  total: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/dto/index.ts",
    "content": "export * from './add.members-to-set.dto';\nexport * from './create.set-with-expire.dto';\nexport * from './delete.members-from-set.dto';\nexport * from './delete.members-from-set.response';\nexport * from './get.set-members.dto';\nexport * from './get.set-members.response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/set.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  HttpCode,\n  Post,\n  Put,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport {\n  AddMembersToSetDto,\n  CreateSetWithExpireDto,\n  DeleteMembersFromSetDto,\n  DeleteMembersFromSetResponse,\n  GetSetMembersDto,\n  GetSetMembersResponse,\n} from 'src/modules/browser/set/dto';\nimport { SetService } from 'src/modules/browser/set/set.service';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: Set')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('set')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class SetController extends BrowserBaseController {\n  constructor(private setService: SetService) {\n    super();\n  }\n\n  @Post('')\n  @ApiOperation({ description: 'Set key to hold Set data type' })\n  @ApiRedisParams()\n  @ApiBody({ type: CreateSetWithExpireDto })\n  @ApiQueryRedisStringEncoding()\n  async createSet(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateSetWithExpireDto,\n  ): Promise<void> {\n    return await this.setService.createSet(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/get-members')\n  @HttpCode(200)\n  @ApiOperation({\n    description:\n      'Get specified members of the set stored at key by cursor position',\n  })\n  @ApiRedisParams()\n  @ApiOkResponse({\n    description: 'Specified members of the set stored at key.',\n    type: GetSetMembersResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getMembers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetSetMembersDto,\n  ): Promise<GetSetMembersResponse> {\n    return await this.setService.getMembers(clientMetadata, dto);\n  }\n\n  @Put('')\n  @ApiOperation({\n    description: 'Add the specified members to the Set stored at key',\n  })\n  @ApiRedisParams()\n  @ApiBody({ type: AddMembersToSetDto })\n  @ApiQueryRedisStringEncoding()\n  async addMembers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: AddMembersToSetDto,\n  ): Promise<void> {\n    return await this.setService.addMembers(clientMetadata, dto);\n  }\n\n  @Delete('/members')\n  @ApiOperation({\n    description: 'Remove the specified members from the Set stored at key',\n  })\n  @ApiRedisParams()\n  @ApiBody({ type: DeleteMembersFromSetDto })\n  @ApiQueryRedisStringEncoding()\n  async deleteMembers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteMembersFromSetDto,\n  ): Promise<DeleteMembersFromSetResponse> {\n    return await this.setService.deleteMembers(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/set.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { SetService } from 'src/modules/browser/set/set.service';\nimport { SetController } from 'src/modules/browser/set/set.controller';\n\n@Module({})\nexport class SetModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: SetModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: SetModule,\n          },\n        ]),\n      ],\n      controllers: [SetController],\n      providers: [SetService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/set.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  ConflictException,\n  ForbiddenException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { when } from 'jest-when';\nimport {\n  mockRedisNoPermError,\n  mockRedisWrongTypeError,\n  mockBrowserClientMetadata,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { ReplyError } from 'src/models';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  mockAddMembersToSetDto,\n  mockDeleteMembersDto,\n  mockGetSetMembersDto,\n  mockGetSetMembersResponse,\n  mockSetMembers,\n} from 'src/modules/browser/__mocks__';\nimport { SetService } from 'src/modules/browser/set/set.service';\nimport {\n  CreateSetWithExpireDto,\n  GetSetMembersDto,\n} from 'src/modules/browser/set/dto';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('SetService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: SetService;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        SetService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<SetService>(SetService);\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('createSet', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddMembersToSetDto.keyName,\n        ])\n        .mockResolvedValue(false);\n      service.createSetWithExpiration = jest.fn();\n    });\n    it('create set with expiration', async () => {\n      service.createSetWithExpiration = jest.fn().mockResolvedValue(undefined);\n\n      await expect(\n        service.createSet(mockBrowserClientMetadata, {\n          ...mockAddMembersToSetDto,\n          expire: 1000,\n        }),\n      ).resolves.not.toThrow();\n      expect(service.createSetWithExpiration).toHaveBeenCalled();\n    });\n    it('create set without expiration', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolSetCommands.SAdd,\n          mockAddMembersToSetDto.keyName,\n          ...mockAddMembersToSetDto.members,\n        ])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).resolves.not.toThrow();\n      expect(service.createSetWithExpiration).not.toHaveBeenCalled();\n    });\n    it('key with this name exist', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddMembersToSetDto.keyName,\n        ])\n        .mockResolvedValue(true);\n\n      await expect(\n        service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).rejects.toThrow(ConflictException);\n      expect(client.sendCommand).toHaveBeenCalledTimes(1);\n      expect(client.sendPipeline).not.toHaveBeenCalled();\n    });\n    it(\"try to use 'SADD' command not for set data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'SADD',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolSetCommands.SAdd,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for createSet\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SADD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('createSetWithExpiration', () => {\n    const dto: CreateSetWithExpireDto = {\n      ...mockAddMembersToSetDto,\n      expire: 1000,\n    };\n    it('succeed to create Set data type with expiration', async () => {\n      when(client.sendPipeline)\n        .calledWith([\n          [BrowserToolSetCommands.SAdd, dto.keyName, ...dto.members],\n          [BrowserToolKeysCommands.Expire, dto.keyName, dto.expire],\n        ])\n        .mockResolvedValue([\n          [null, mockAddMembersToSetDto.members.length],\n          [null, 1],\n        ]);\n\n      const result = await service.createSetWithExpiration(client, dto);\n      expect(result).toBe(mockAddMembersToSetDto.members.length);\n    });\n    it('throw transaction error', async () => {\n      const transactionError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'SADD',\n      };\n      client.sendPipeline.mockResolvedValue([[transactionError, null]]);\n\n      await expect(\n        service.createSetWithExpiration(client, dto),\n      ).rejects.toEqual(transactionError);\n    });\n  });\n\n  describe('getMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolSetCommands.SCard,\n          mockGetSetMembersDto.keyName,\n        ])\n        .mockResolvedValue(mockSetMembers.length);\n    });\n    it('succeed to get members of the set', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolSetCommands.SScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue([Buffer.from('0'), mockSetMembers]);\n\n      const result = await service.getMembers(\n        mockBrowserClientMetadata,\n        mockGetSetMembersDto,\n      );\n\n      expect(result).toEqual(mockGetSetMembersResponse);\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        expect.arrayContaining([\n          BrowserToolSetCommands.SScan,\n          expect.anything(),\n        ]),\n      );\n    });\n    it('succeed to find exact member in the set', async () => {\n      const dto: GetSetMembersDto = {\n        ...mockGetSetMembersDto,\n        match: mockSetMembers[0].toString(),\n      };\n      when(client.sendCommand)\n        .calledWith([BrowserToolSetCommands.SIsMember, dto.keyName, dto.match])\n        .mockResolvedValue(1);\n\n      const result = await service.getMembers(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual(mockGetSetMembersResponse);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolSetCommands.SScan,\n        expect.anything(),\n      ]);\n    });\n    it('failed to find exact member in the set', async () => {\n      const dto: GetSetMembersDto = {\n        ...mockGetSetMembersDto,\n        match: mockSetMembers[0].toString(),\n      };\n      when(client.sendCommand)\n        .calledWith([BrowserToolSetCommands.SIsMember, dto.keyName, dto.match])\n        .mockResolvedValue(0);\n\n      const result = await service.getMembers(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual({ ...mockGetSetMembersResponse, members: [] });\n    });\n    it('should not call scan when math contains escaped glob', async () => {\n      const dto: GetSetMembersDto = {\n        ...mockGetSetMembersDto,\n        match: 'm\\\\[a-e\\\\]mber',\n      };\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolSetCommands.SIsMember,\n          dto.keyName,\n          'm[a-e]mber',\n        ])\n        .mockResolvedValue(1);\n\n      const result = await service.getMembers(mockBrowserClientMetadata, dto);\n\n      expect(result).toEqual({\n        ...mockGetSetMembersResponse,\n        members: [Buffer.from('m[a-e]mber')],\n      });\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolSetCommands.SScan,\n        expect.anything(),\n      ]);\n    });\n    // TODO: uncomment after enabling threshold for set scan\n    // it('should stop set full scan', async () => {\n    //   const dto: GetSetMembersDto = {\n    //     ...mockGetMembersDto,\n    //     count: REDIS_SCAN_CONFIG.countDefault,\n    //     match: '*un-exist-member*',\n    //   };\n    //   const maxScanCalls = Math.round(\n    //     REDIS_SCAN_CONFIG.countThreshold / REDIS_SCAN_CONFIG.countDefault,\n    //   );\n    //   when(browserTool.execCommand)\n    //     .calledWith(\n    //       mockBrowserClientMetadata,\n    //       BrowserToolSetCommands.SScan,\n    //       expect.anything(),\n    //     )\n    //     .mockResolvedValue(['200', []]);\n    //\n    //   await service.getMembers(mockBrowserClientMetadata, dto);\n    //\n    //   expect(browserTool.execCommand).toHaveBeenCalledTimes(maxScanCalls + 1);\n    // });\n    it('key with this name does not exist for getMembers', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolSetCommands.SCard,\n          mockGetSetMembersDto.keyName,\n        ])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.getMembers(mockBrowserClientMetadata, mockGetSetMembersDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"try to use 'SCARD' command not for list data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'SCARD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getMembers(mockBrowserClientMetadata, mockGetSetMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for getMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SCARD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getMembers(mockBrowserClientMetadata, mockGetSetMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('addMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddMembersToSetDto.keyName,\n        ])\n        .mockResolvedValue(true);\n    });\n    it('succeed to add members to the Set data type', async () => {\n      const { keyName, members } = mockAddMembersToSetDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolSetCommands.SAdd, keyName, ...members])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).resolves.not.toThrow();\n    });\n    it('key with this name does not exist for addMembers', async () => {\n      const { keyName, members } = mockAddMembersToSetDto;\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddMembersToSetDto.keyName,\n        ])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolSetCommands.SAdd,\n        keyName,\n        ...members,\n      ]);\n    });\n    it(\"try to use 'SADD' command not for set data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'SADD',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolSetCommands.SAdd,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for addMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SADD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('deleteMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockDeleteMembersDto.keyName,\n        ])\n        .mockResolvedValue(true);\n    });\n    it('succeeded to delete members from Set data type', async () => {\n      const { members, keyName } = mockDeleteMembersDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolSetCommands.SRem, keyName, ...members])\n        .mockResolvedValue(members.length);\n\n      const result = await service.deleteMembers(\n        mockBrowserClientMetadata,\n        mockDeleteMembersDto,\n      );\n\n      expect(result).toEqual({ affected: members.length });\n    });\n    it('key with this name does not exist for deleteMembers', async () => {\n      const { members, keyName } = mockDeleteMembersDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, keyName])\n        .mockResolvedValue(false);\n\n      await expect(\n        service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolSetCommands.SRem,\n        keyName,\n        ...members,\n      ]);\n    });\n    it(\"try to use 'SREM' command not for set data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'SREM',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolSetCommands.SRem,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for deleteMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SREM',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/set/set.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { RedisErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport config from 'src/utils/config';\nimport {\n  catchAclError,\n  catchMultiTransactionError,\n  isRedisGlob,\n  unescapeRedisGlob,\n} from 'src/utils';\nimport { ReplyError } from 'src/models';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  AddMembersToSetDto,\n  CreateSetWithExpireDto,\n  DeleteMembersFromSetDto,\n  DeleteMembersFromSetResponse,\n  GetSetMembersDto,\n  GetSetMembersResponse,\n  SetScanResponse,\n} from 'src/modules/browser/set/dto';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\n\n@Injectable()\nexport class SetService {\n  private logger = new Logger('SetService');\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  public async createSet(\n    clientMetadata: ClientMetadata,\n    dto: CreateSetWithExpireDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating Set data type.', clientMetadata);\n      const { keyName, expire } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyExists(keyName, client);\n\n      if (expire) {\n        await this.createSetWithExpiration(client, dto);\n      } else {\n        await this.createSimpleSet(client, dto);\n      }\n\n      this.logger.debug('Succeed to create Set data type.', clientMetadata);\n      return null;\n    } catch (error) {\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      this.logger.error(\n        'Failed to create Set data type.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  public async getMembers(\n    clientMetadata: ClientMetadata,\n    dto: GetSetMembersDto,\n  ): Promise<GetSetMembersResponse> {\n    try {\n      this.logger.debug(\n        'Getting members of the Set data type stored at key.',\n        clientMetadata,\n      );\n      const { keyName, cursor, match } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      let result: GetSetMembersResponse = {\n        keyName,\n        total: 0,\n        members: [],\n        nextCursor: cursor,\n      };\n\n      result.total = (await client.sendCommand([\n        BrowserToolSetCommands.SCard,\n        keyName,\n      ])) as number;\n      if (!result.total) {\n        this.logger.error(\n          `Failed to get members of the Set data type. Not Found key: ${keyName}.`,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n      if (match && !isRedisGlob(match)) {\n        const member = unescapeRedisGlob(match);\n        result.nextCursor = 0;\n        const memberIsExist = await client.sendCommand([\n          BrowserToolSetCommands.SIsMember,\n          keyName,\n          member,\n        ]);\n        if (memberIsExist) {\n          result.members.push(member);\n        }\n      } else {\n        const scanResult = await this.scanSet(client, dto);\n        result = { ...result, ...scanResult };\n      }\n\n      this.logger.debug(\n        'Succeed to get members of the Set data type.',\n        clientMetadata,\n      );\n      return plainToInstance(GetSetMembersResponse, result);\n    } catch (error) {\n      this.logger.error(\n        'Failed to get members of the Set data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async addMembers(\n    clientMetadata: ClientMetadata,\n    dto: AddMembersToSetDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Adding members to the Set data type.', clientMetadata);\n      const { keyName, members } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      await client.sendCommand([\n        BrowserToolSetCommands.SAdd,\n        keyName,\n        ...members,\n      ]);\n\n      this.logger.debug(\n        'Succeed to add members to Set data type.',\n        clientMetadata,\n      );\n      return null;\n    } catch (error) {\n      this.logger.error(\n        'Failed to add members to Set data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async deleteMembers(\n    clientMetadata: ClientMetadata,\n    dto: DeleteMembersFromSetDto,\n  ): Promise<DeleteMembersFromSetResponse> {\n    try {\n      this.logger.debug(\n        'Deleting members from the Set data type.',\n        clientMetadata,\n      );\n      const { keyName, members } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = (await client.sendCommand([\n        BrowserToolSetCommands.SRem,\n        keyName,\n        ...members,\n      ])) as number;\n\n      this.logger.debug(\n        'Succeed to delete members from the Set data type.',\n        clientMetadata,\n      );\n      return { affected: result };\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete members from the Set data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async createSimpleSet(\n    client: RedisClient,\n    dto: AddMembersToSetDto,\n  ): Promise<void> {\n    const { keyName, members } = dto;\n    await client.sendCommand([\n      BrowserToolSetCommands.SAdd,\n      keyName,\n      ...members,\n    ]);\n  }\n\n  public async createSetWithExpiration(\n    client: RedisClient,\n    dto: CreateSetWithExpireDto,\n  ): Promise<void> {\n    const { keyName, members, expire } = dto;\n    const transactionResults = await client.sendPipeline([\n      [BrowserToolSetCommands.SAdd, keyName, ...members],\n      [BrowserToolKeysCommands.Expire, keyName, expire],\n    ]);\n    catchMultiTransactionError(transactionResults);\n\n    const execResult = transactionResults.map(\n      (item: [ReplyError, any]) => item[1],\n    );\n    const [added] = execResult;\n    return added;\n  }\n\n  public async scanSet(\n    client: RedisClient,\n    dto: GetSetMembersDto,\n  ): Promise<SetScanResponse> {\n    const { keyName, cursor } = dto;\n    const count = dto.count || REDIS_SCAN_CONFIG.countDefault;\n    const match = dto.match !== undefined ? dto.match : '*';\n    let result: SetScanResponse = {\n      keyName,\n      nextCursor: null,\n      members: [],\n    };\n\n    while (result.nextCursor !== 0 && result.members.length < count) {\n      const scanResult = await client.sendCommand([\n        BrowserToolSetCommands.SScan,\n        keyName,\n        `${result.nextCursor || cursor}`,\n        'MATCH',\n        match,\n        'COUNT',\n        count,\n      ]);\n      const nextCursor = scanResult[0];\n      const members = scanResult[1];\n      result = {\n        ...result,\n        nextCursor: parseInt(nextCursor, 10),\n        members: [...result.members, ...members],\n      };\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/controllers/consumer-group.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Patch,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport {\n  ConsumerGroupDto,\n  CreateConsumerGroupsDto,\n  DeleteConsumerGroupsDto,\n  DeleteConsumerGroupsResponse,\n  UpdateConsumerGroupDto,\n} from 'src/modules/browser/stream/dto';\nimport { ConsumerGroupService } from 'src/modules/browser/stream/services/consumer-group.service';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: Streams')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('streams/consumer-groups')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class ConsumerGroupController extends BrowserBaseController {\n  constructor(private service: ConsumerGroupService) {\n    super();\n  }\n\n  @Post('/get')\n  @ApiRedisInstanceOperation({\n    description: 'Get consumer groups list',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Returns stream consumer groups.',\n        type: ConsumerGroupDto,\n        isArray: true,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getGroups(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: KeyDto,\n  ): Promise<ConsumerGroupDto[]> {\n    return this.service.getGroups(clientMetadata, dto);\n  }\n\n  @Post('')\n  @ApiRedisInstanceOperation({\n    description: 'Create stream consumer group',\n    statusCode: 201,\n  })\n  async createGroups(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateConsumerGroupsDto,\n  ): Promise<void> {\n    return this.service.createGroups(clientMetadata, dto);\n  }\n\n  @Patch('')\n  @ApiRedisInstanceOperation({\n    description: 'Modify last delivered ID of the Consumer Group',\n    statusCode: 200,\n  })\n  async updateGroup(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: UpdateConsumerGroupDto,\n  ): Promise<void> {\n    return this.service.updateGroup(clientMetadata, dto);\n  }\n\n  @Delete('')\n  @ApiRedisInstanceOperation({\n    description: 'Delete Consumer Group',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Returns number of affected consumer groups.',\n        type: DeleteConsumerGroupsResponse,\n      },\n    ],\n  })\n  async deleteGroup(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteConsumerGroupsDto,\n  ): Promise<DeleteConsumerGroupsResponse> {\n    return this.service.deleteGroup(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/controllers/consumer.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport {\n  AckPendingEntriesDto,\n  AckPendingEntriesResponse,\n  ClaimPendingEntriesResponse,\n  ClaimPendingEntryDto,\n  ConsumerDto,\n  ConsumerGroupDto,\n  DeleteConsumersDto,\n  GetConsumersDto,\n  GetPendingEntriesDto,\n  PendingEntryDto,\n} from 'src/modules/browser/stream/dto';\nimport { ConsumerService } from 'src/modules/browser/stream/services/consumer.service';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: Streams')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('streams/consumer-groups/consumers')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class ConsumerController extends BrowserBaseController {\n  constructor(private service: ConsumerService) {\n    super();\n  }\n\n  @Post('/get')\n  @ApiRedisInstanceOperation({\n    description: 'Get consumers list in the group',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: ConsumerGroupDto,\n        isArray: true,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getConsumers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetConsumersDto,\n  ): Promise<ConsumerDto[]> {\n    return this.service.getConsumers(clientMetadata, dto);\n  }\n\n  @Delete('')\n  @ApiRedisInstanceOperation({\n    description: 'Delete Consumer(s) from the Consumer Group',\n    statusCode: 200,\n  })\n  async deleteConsumers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteConsumersDto,\n  ): Promise<void> {\n    return this.service.deleteConsumers(clientMetadata, dto);\n  }\n\n  @Post('/pending-messages/get')\n  @ApiRedisInstanceOperation({\n    description: 'Get pending entries list',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: PendingEntryDto,\n        isArray: true,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getPendingEntries(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetPendingEntriesDto,\n  ): Promise<PendingEntryDto[]> {\n    return this.service.getPendingEntries(clientMetadata, dto);\n  }\n\n  @Post('/pending-messages/ack')\n  @ApiRedisInstanceOperation({\n    description: 'Ack pending entries',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: AckPendingEntriesResponse,\n      },\n    ],\n  })\n  async ackPendingEntries(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: AckPendingEntriesDto,\n  ): Promise<AckPendingEntriesResponse> {\n    return this.service.ackPendingEntries(clientMetadata, dto);\n  }\n\n  @Post('/pending-messages/claim')\n  @ApiRedisInstanceOperation({\n    description: 'Claim pending entries',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: ClaimPendingEntriesResponse,\n      },\n    ],\n  })\n  async claimPendingEntries(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: ClaimPendingEntryDto,\n  ): Promise<ClaimPendingEntriesResponse> {\n    return this.service.claimPendingEntries(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/controllers/stream.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport {\n  AddStreamEntriesDto,\n  AddStreamEntriesResponse,\n  CreateStreamDto,\n  GetStreamEntriesDto,\n  GetStreamEntriesResponse,\n  DeleteStreamEntriesDto,\n  DeleteStreamEntriesResponse,\n} from 'src/modules/browser/stream/dto';\nimport { StreamService } from 'src/modules/browser/stream/services/stream.service';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\n\n@ApiTags('Browser: Streams')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('streams')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class StreamController {\n  constructor(private service: StreamService) {}\n\n  @Post('')\n  @ApiRedisInstanceOperation({\n    description: 'Create stream',\n    statusCode: 201,\n  })\n  async createStream(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateStreamDto,\n  ): Promise<void> {\n    return this.service.createStream(clientMetadata, dto);\n  }\n\n  @Post('entries')\n  @ApiRedisInstanceOperation({\n    description: 'Add entries to the stream',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Returns entries IDs added',\n        type: AddStreamEntriesResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async addEntries(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: AddStreamEntriesDto,\n  ): Promise<AddStreamEntriesResponse> {\n    return this.service.addEntries(clientMetadata, dto);\n  }\n\n  @Post('/entries/get')\n  @ApiRedisInstanceOperation({\n    description: 'Get stream entries',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Returns ordered stream entries in defined range.',\n        type: GetStreamEntriesResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getEntries(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetStreamEntriesDto,\n  ): Promise<GetStreamEntriesResponse> {\n    return this.service.getEntries(clientMetadata, dto);\n  }\n\n  @Delete('/entries')\n  @ApiRedisInstanceOperation({\n    description: 'Remove the specified entries from the Stream stored at key',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Ok',\n        type: DeleteStreamEntriesResponse,\n      },\n    ],\n  })\n  async deleteEntries(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteStreamEntriesDto,\n  ): Promise<DeleteStreamEntriesResponse> {\n    return await this.service.deleteEntries(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/ack.pending-entries.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined, IsNotEmpty } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { GetConsumersDto } from './get.consumers.dto';\n\nexport class AckPendingEntriesDto extends GetConsumersDto {\n  @ApiProperty({\n    description: 'Entries IDs',\n    type: String,\n    isArray: true,\n    example: ['1650985323741-0', '1650985323770-0'],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsRedisString({ each: true })\n  @IsNotEmpty({ each: true })\n  @RedisStringType({ each: true })\n  entries: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/ack.pending-entries.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class AckPendingEntriesResponse {\n  @ApiProperty({\n    description: 'Number of affected entries',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/add.stream-entries.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { StreamEntryDto } from './stream-entry.dto';\n\nexport class AddStreamEntriesDto extends KeyDto {\n  @ApiProperty({\n    description: 'Entries to push',\n    type: StreamEntryDto,\n    isArray: true,\n    example: [\n      {\n        id: '*',\n        fields: [\n          { name: 'field1', value: 'value1' },\n          { name: 'field2', value: 'value2' },\n        ],\n      },\n      {\n        id: '*',\n        fields: [\n          { name: 'field1', value: 'value1' },\n          { name: 'field2', value: 'value2' },\n        ],\n      },\n    ],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested({ each: true })\n  @Type(() => StreamEntryDto)\n  entries: StreamEntryDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/add.stream-entries.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class AddStreamEntriesResponse extends KeyResponse {\n  @ApiProperty({\n    description: 'Entries IDs',\n    type: String,\n    isArray: true,\n  })\n  entries: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/claim.pending-entries.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class ClaimPendingEntriesResponse {\n  @ApiProperty({\n    description: 'Entries IDs were affected by claim command',\n    type: String,\n    isArray: true,\n    example: ['1650985323741-0', '1650985323770-0'],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  affected: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/claim.pending-entry.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsBoolean,\n  IsDefined,\n  IsInt,\n  IsNotEmpty,\n  Min,\n  NotEquals,\n  ValidateIf,\n} from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class ClaimPendingEntryDto extends KeyDto {\n  @ApiProperty({\n    type: String,\n    description: 'Consumer group name',\n    example: 'group-1',\n  })\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  groupName: RedisString;\n\n  @ApiProperty({\n    type: String,\n    description: 'Consumer name',\n    example: 'consumer-1',\n  })\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  consumerName: RedisString;\n\n  @ApiProperty({\n    description:\n      'Claim only if its idle time is greater the minimum idle time ',\n    type: Number,\n    minimum: 0,\n    default: 0,\n  })\n  @IsInt()\n  @Min(0)\n  minIdleTime: number = 0;\n\n  @ApiProperty({\n    description: 'Entries IDs',\n    type: String,\n    isArray: true,\n    example: ['1650985323741-0', '1650985323770-0'],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsNotEmpty({ each: true })\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  entries: string[];\n\n  @ApiPropertyOptional({\n    description:\n      'Set the idle time (last time it was delivered) of the message',\n    type: Number,\n    minimum: 0,\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt()\n  @Min(0)\n  idle?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'This is the same as IDLE but instead of a relative amount of milliseconds, ' +\n      'it sets the idle time to a specific Unix time (in milliseconds)',\n    type: Number,\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt()\n  time?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Set the retry counter to the specified value. ' +\n      'This counter is incremented every time a message is delivered again. ' +\n      'Normally XCLAIM does not alter this counter, which is just served to clients when the XPENDING command ' +\n      'is called: this way clients can detect anomalies, like messages that are never processed ' +\n      'for some reason after a big number of delivery attempts',\n    type: Number,\n    minimum: 0,\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt()\n  @Min(0)\n  retryCount?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Creates the pending message entry in the PEL even if certain specified IDs are not already ' +\n      'in the PEL assigned to a different client',\n    type: Boolean,\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsBoolean()\n  force?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/create.consumer-groups.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsNotEmpty,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport { Type } from 'class-transformer';\n\nexport class ConsumerGroupDto {\n  @ApiProperty({\n    type: String,\n    description: 'Consumer Group name',\n    example: 'group',\n  })\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    type: Number,\n    description: 'Number of consumers',\n    example: 2,\n  })\n  consumers: number = 0;\n\n  @ApiProperty({\n    type: Number,\n    description: 'Number of pending messages',\n    example: 2,\n  })\n  pending: number = 0;\n\n  @ApiProperty({\n    type: String,\n    description: 'Smallest Id of the message that is pending in the group',\n    example: '1657892649-0',\n  })\n  smallestPendingId: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Greatest Id of the message that is pending in the group',\n    example: '1657892680-0',\n  })\n  greatestPendingId: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Id of last delivered message',\n    example: '1657892648-0',\n  })\n  lastDeliveredId: string;\n}\n\nexport class CreateConsumerGroupDto {\n  @ApiProperty({\n    type: String,\n    description: 'Consumer group name',\n    example: 'group',\n  })\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    type: String,\n    description: 'Id of last delivered message',\n    example: '1657892648-0',\n  })\n  @IsNotEmpty()\n  @IsString()\n  lastDeliveredId: string;\n}\n\nexport class CreateConsumerGroupsDto extends KeyDto {\n  @ApiProperty({\n    type: () => CreateConsumerGroupDto,\n    isArray: true,\n    description: 'List of consumer groups to create',\n  })\n  @ValidateNested()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => CreateConsumerGroupDto)\n  consumerGroups: CreateConsumerGroupDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/create.stream.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { AddStreamEntriesDto } from './add.stream-entries.dto';\n\nexport class CreateStreamDto extends IntersectionType(\n  AddStreamEntriesDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/delete.consumer-groups.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class DeleteConsumerGroupsDto extends KeyDto {\n  @ApiProperty({\n    description: 'Consumer group names',\n    type: String,\n    isArray: true,\n    example: ['Group-1', 'Group-1'],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  consumerGroups: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/delete.consumer-groups.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteConsumerGroupsResponse {\n  @ApiProperty({\n    description: 'Number of deleted consumer groups',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/delete.consumers.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined, IsNotEmpty } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { GetConsumersDto } from './get.consumers.dto';\n\nexport class DeleteConsumersDto extends GetConsumersDto {\n  @ApiProperty({\n    description: 'Names of consumers to delete',\n    type: String,\n    isArray: true,\n    example: ['consumer-1', 'consumer-2'],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsNotEmpty({ each: true })\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  consumerNames: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/delete.stream-entries.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class DeleteStreamEntriesDto extends KeyDto {\n  @ApiProperty({\n    description: 'Entries IDs',\n    type: String,\n    isArray: true,\n    example: ['1650985323741-0', '1650985323770-0'],\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  entries: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/delete.stream-entries.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteStreamEntriesResponse {\n  @ApiProperty({\n    description: 'Number of deleted entries',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/get.consumers.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class ConsumerDto {\n  @ApiProperty({\n    type: String,\n    description: \"The consumer's name\",\n    example: 'consumer-2',\n  })\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The number of pending messages for the client, ' +\n      'which are messages that were delivered but are yet to be acknowledged',\n    example: 2,\n  })\n  pending: number = 0;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The number of milliseconds that have passed since the consumer last interacted with the server',\n    example: 22442,\n  })\n  idle: number = 0;\n}\n\nexport class GetConsumersDto extends KeyDto {\n  @ApiProperty({\n    type: String,\n    description: 'Consumer group name',\n    example: 'group-1',\n  })\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  groupName: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/get.pending-entries.dto.ts",
    "content": "import {\n  ApiProperty,\n  ApiPropertyOptional,\n  IntersectionType,\n} from '@nestjs/swagger';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport { IsInt, IsNotEmpty, IsString, Min } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { GetConsumersDto } from './get.consumers.dto';\n\nexport class PendingEntryDto {\n  @ApiProperty({\n    type: String,\n    description: 'Entry ID',\n    example: '*',\n  })\n  id: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Consumer name',\n    example: 'consumer-1',\n  })\n  @RedisStringType()\n  consumerName: RedisString;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The number of milliseconds that elapsed since the last time ' +\n      'this message was delivered to this consumer',\n    example: 22442,\n  })\n  idle: number = 0;\n\n  @ApiProperty({\n    type: Number,\n    description: 'The number of times this message was delivered',\n    example: 2,\n  })\n  delivered: number = 0;\n}\n\nexport class GetPendingEntriesDto extends IntersectionType(\n  KeyDto,\n  GetConsumersDto,\n) {\n  @ApiProperty({\n    type: String,\n    description: 'Consumer name',\n    example: 'consumer-1',\n  })\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  consumerName: RedisString;\n\n  @ApiPropertyOptional({\n    description: 'Specifying the start id',\n    type: String,\n    default: '-',\n  })\n  @IsString()\n  start?: string = '-';\n\n  @ApiPropertyOptional({\n    description: 'Specifying the end id',\n    type: String,\n    default: '+',\n  })\n  @IsString()\n  end?: string = '+';\n\n  @ApiPropertyOptional({\n    description: 'Specifying the number of pending messages to return.',\n    type: Number,\n    minimum: 1,\n    default: 500,\n  })\n  @IsInt()\n  @Min(1)\n  count?: number = 500;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/get.stream-entries.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsEnum, IsInt, IsString, Min } from 'class-validator';\nimport { SortOrder } from 'src/constants';\n\nexport class GetStreamEntriesDto extends KeyDto {\n  @ApiPropertyOptional({\n    description: 'Specifying the start id',\n    type: String,\n    default: '-',\n  })\n  @IsString()\n  start?: string = '-';\n\n  @ApiPropertyOptional({\n    description: 'Specifying the end id',\n    type: String,\n    default: '+',\n  })\n  @IsString()\n  end?: string = '+';\n\n  @ApiPropertyOptional({\n    description: 'Specifying the number of entries to return.',\n    type: Number,\n    minimum: 1,\n    default: 500,\n  })\n  @IsInt()\n  @Min(1)\n  count?: number = 500;\n\n  @ApiProperty({\n    description: 'Get entries sort by IDs order.',\n    default: SortOrder.Desc,\n    enum: SortOrder,\n  })\n  @IsEnum(SortOrder, {\n    message: `sortOrder must be a valid enum value. Valid values: ${Object.values(\n      SortOrder,\n    )}.`,\n  })\n  sortOrder?: SortOrder = SortOrder.Desc;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/get.stream-entries.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { StreamEntryDto } from './stream-entry.dto';\n\nexport class GetStreamEntriesResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'Total number of entries',\n  })\n  total: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'Last generated id in the stream',\n  })\n  lastGeneratedId: string;\n\n  @ApiProperty({\n    description: 'First stream entry',\n    type: StreamEntryDto,\n  })\n  @Type(() => StreamEntryDto)\n  firstEntry: StreamEntryDto;\n\n  @ApiProperty({\n    description: 'Last stream entry',\n    type: StreamEntryDto,\n  })\n  @Type(() => StreamEntryDto)\n  lastEntry: StreamEntryDto;\n\n  @ApiProperty({\n    description: 'Stream entries',\n    type: StreamEntryDto,\n    isArray: true,\n  })\n  @Type(() => StreamEntryDto)\n  entries: StreamEntryDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/index.ts",
    "content": "export * from './ack.pending-entries.dto';\nexport * from './ack.pending-entries.response';\nexport * from './add.stream-entries.dto';\nexport * from './add.stream-entries.response';\nexport * from './claim.pending-entry.dto';\nexport * from './claim.pending-entries.response';\nexport * from './create.consumer-groups.dto';\nexport * from './create.stream.dto';\nexport * from './delete.consumer-groups.dto';\nexport * from './delete.consumer-groups.response';\nexport * from './delete.consumers.dto';\nexport * from './delete.stream-entries.dto';\nexport * from './delete.stream-entries.response';\nexport * from './get.consumers.dto';\nexport * from './get.pending-entries.dto';\nexport * from './get.stream-entries.dto';\nexport * from './get.stream-entries.response';\nexport * from './stream-entry.dto';\nexport * from './update.consumer-group.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/stream-entry.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  IsNotEmpty,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\nimport { Type } from 'class-transformer';\n\nexport class StreamEntryFieldDto {\n  @ApiProperty({\n    type: String,\n    description: 'Entry field name',\n    example: 'field1',\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    type: String,\n    description: 'Entry value',\n    example: 'value1',\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsRedisString()\n  @RedisStringType()\n  value: RedisString;\n}\n\nexport class StreamEntryDto {\n  @ApiProperty({\n    type: String,\n    description: 'Entry ID',\n    example: '*',\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString()\n  id: string;\n\n  @ApiProperty({\n    type: Object,\n    description: 'Entry fields',\n    example: [\n      { name: 'field1', value: 'value1' },\n      { name: 'field2', value: 'value2' },\n    ],\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @ArrayNotEmpty()\n  @IsArray()\n  @Type(() => StreamEntryFieldDto)\n  @ValidateNested({ each: true })\n  fields: StreamEntryFieldDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/dto/update.consumer-group.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport { CreateConsumerGroupDto } from './create.consumer-groups.dto';\n\nexport class UpdateConsumerGroupDto extends IntersectionType(\n  KeyDto,\n  CreateConsumerGroupDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/services/consumer-group.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockBrowserClientMetadata,\n  mockStandaloneRedisClient,\n  mockDatabaseClientFactory,\n} from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  BadRequestException,\n  ConflictException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RedisErrorCodes } from 'src/constants';\nimport { ConsumerGroupService } from 'src/modules/browser/stream/services/consumer-group.service';\nimport {\n  mockAddStreamEntriesDto,\n  mockConsumerGroup,\n  mockConsumerGroupsReply,\n  mockCreateConsumerGroupDto,\n  mockEntryId2,\n  mockKeyDto,\n} from 'src/modules/browser/__mocks__';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport * as bigStringUtil from 'src/utils/big-string';\nimport config, { Config } from 'src/utils/config';\n\nconst REDIS_CLIENTS_CONFIG = config.get(\n  'redis_clients',\n) as Config['redis_clients'];\nconst BIG_STRING_PREFIX = REDIS_CLIENTS_CONFIG.truncatedStringPrefix;\n\ndescribe('ConsumerGroupService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: ConsumerGroupService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ConsumerGroupService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get(ConsumerGroupService);\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n\n    when(client.sendCommand)\n      .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n      .mockResolvedValue(true);\n  });\n\n  describe('getGroups', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoGroups]),\n        )\n        .mockResolvedValue([mockConsumerGroupsReply, mockConsumerGroupsReply]);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockResolvedValue([\n          's',\n          mockConsumerGroup.smallestPendingId,\n          mockConsumerGroup.greatestPendingId,\n        ]);\n    });\n    it('should get consumer groups with info', async () => {\n      const groups = await service.getGroups(\n        mockBrowserClientMetadata,\n        mockKeyDto,\n      );\n      expect(groups).toEqual([mockConsumerGroup, mockConsumerGroup]);\n    });\n    it('should get consumer groups (one with n/a info)', async () => {\n      const spy = jest.spyOn(bigStringUtil, 'isTruncatingEnabled');\n      spy.mockReturnValue(true);\n\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoGroups]),\n        )\n        .mockResolvedValueOnce([\n          mockConsumerGroupsReply,\n          [\n            'name',\n            `${BIG_STRING_PREFIX} group1`,\n            'consumers',\n            mockConsumerGroup.consumers,\n            'pending',\n            mockConsumerGroup.pending,\n            'last-delivered-id',\n            mockConsumerGroup.lastDeliveredId,\n          ],\n        ]);\n\n      const groups = await service.getGroups(\n        mockBrowserClientMetadata,\n        mockKeyDto,\n      );\n      expect(groups).toEqual([\n        mockConsumerGroup,\n        {\n          name: Buffer.from(`${BIG_STRING_PREFIX} group1`),\n          consumers: 0,\n          pending: 0,\n          lastDeliveredId: mockEntryId2,\n          smallestPendingId: 'n/a',\n          greatestPendingId: 'n/a',\n        },\n      ]);\n    });\n    it('should throw error when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.getGroups(mockBrowserClientMetadata, mockKeyDto);\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.getGroups(mockBrowserClientMetadata, mockKeyDto);\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.getGroups(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.getGroups(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('createGroups', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValue(true);\n      client.sendPipeline.mockResolvedValue([[null, '123-1']]);\n    });\n    it('add groups', async () => {\n      await expect(\n        service.createGroups(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto,\n            mockCreateConsumerGroupDto,\n          ],\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolStreamCommands.XGroupCreate,\n          mockKeyDto.keyName,\n          mockConsumerGroup.name,\n          mockConsumerGroup.lastDeliveredId,\n        ],\n        [\n          BrowserToolStreamCommands.XGroupCreate,\n          mockKeyDto.keyName,\n          mockConsumerGroup.name,\n          mockConsumerGroup.lastDeliveredId,\n        ],\n      ]);\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.createGroups(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto,\n            mockCreateConsumerGroupDto,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.createGroups(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto,\n            mockCreateConsumerGroupDto,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error(RedisErrorCodes.WrongType), '123-1'],\n      ]);\n\n      try {\n        await service.createGroups(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto,\n            mockCreateConsumerGroupDto,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Conflict when trying to create existing group', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error('BUSYGROUP such group already there!'), '123-1'],\n      ]);\n\n      try {\n        await service.createGroups(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto,\n            mockCreateConsumerGroupDto,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ConflictException);\n        expect(e.message).toEqual('BUSYGROUP such group already there!');\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      client.sendPipeline.mockResolvedValue([[new Error('oO'), '123-1']]);\n\n      try {\n        await service.createGroups(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto,\n            mockCreateConsumerGroupDto,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('updateGroup', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XGroupSetId]),\n        )\n        .mockResolvedValue('OK');\n    });\n    it('update group', async () => {\n      await expect(\n        service.updateGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          ...mockCreateConsumerGroupDto,\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStreamCommands.XGroupSetId,\n        mockKeyDto.keyName,\n        mockCreateConsumerGroupDto.name,\n        mockCreateConsumerGroupDto.lastDeliveredId,\n      ]);\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.updateGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          ...mockCreateConsumerGroupDto,\n        });\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.updateGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          ...mockCreateConsumerGroupDto,\n        });\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      client.sendCommand.mockRejectedValueOnce(\n        new Error(RedisErrorCodes.WrongType),\n      );\n\n      try {\n        await service.updateGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          ...mockCreateConsumerGroupDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw NotFound when trying to modify not-existing group', async () => {\n      client.sendCommand.mockRejectedValueOnce(\n        new Error('NOGROUP no such group'),\n      );\n\n      try {\n        await service.updateGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          ...mockCreateConsumerGroupDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      client.sendCommand.mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.updateGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          ...mockCreateConsumerGroupDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('deleteGroups', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValue(true);\n      client.sendPipeline.mockResolvedValue([[null, '123-1']]);\n    });\n    it('add groups', async () => {\n      await expect(\n        service.deleteGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto.name,\n            mockCreateConsumerGroupDto.name,\n          ],\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolStreamCommands.XGroupDestroy,\n          mockKeyDto.keyName,\n          mockConsumerGroup.name,\n        ],\n        [\n          BrowserToolStreamCommands.XGroupDestroy,\n          mockKeyDto.keyName,\n          mockConsumerGroup.name,\n        ],\n      ]);\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.deleteGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto.name,\n            mockCreateConsumerGroupDto.name,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.deleteGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto.name,\n            mockCreateConsumerGroupDto.name,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error(RedisErrorCodes.WrongType), '123-1'],\n      ]);\n\n      try {\n        await service.deleteGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto.name,\n            mockCreateConsumerGroupDto.name,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      client.sendPipeline.mockResolvedValue([[new Error('oO'), '123-1']]);\n\n      try {\n        await service.deleteGroup(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          consumerGroups: [\n            mockCreateConsumerGroupDto.name,\n            mockCreateConsumerGroupDto.name,\n          ],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/services/consumer-group.service.ts",
    "content": "import {\n  BadRequestException,\n  ConflictException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { RedisErrorCodes } from 'src/constants';\nimport {\n  catchAclError,\n  catchMultiTransactionError,\n  isTruncatedString,\n} from 'src/utils';\nimport {\n  BrowserToolCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { KeyDto } from 'src/modules/browser/keys/dto';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  ConsumerGroupDto,\n  CreateConsumerGroupsDto,\n  DeleteConsumerGroupsDto,\n  DeleteConsumerGroupsResponse,\n  UpdateConsumerGroupDto,\n} from 'src/modules/browser/stream/dto';\nimport { plainToInstance } from 'class-transformer';\nimport { RedisString } from 'src/common/constants';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { checkIfKeyNotExists } from 'src/modules/browser/utils';\n\n@Injectable()\nexport class ConsumerGroupService {\n  private logger = new Logger('ConsumerGroupService');\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  /**\n   * Get consumer groups list for particular stream\n   * In addition fetch pending messages info for each group\n   * !May be slow on huge streams as 'XPENDING' command tagged with as @slow\n   * @param clientMetadata\n   * @param dto\n   */\n  async getGroups(\n    clientMetadata: ClientMetadata,\n    dto: KeyDto,\n  ): Promise<ConsumerGroupDto[]> {\n    try {\n      this.logger.debug('Getting consumer groups list.', clientMetadata);\n      const { keyName } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const groups = ConsumerGroupService.formatReplyToDto(\n        (await client.sendCommand([\n          BrowserToolStreamCommands.XInfoGroups,\n          keyName,\n        ])) as string[][],\n      );\n\n      return await Promise.all(\n        groups.map((group) => this.getGroupInfo(client, dto, group)),\n      );\n    } catch (error) {\n      this.logger.error(\n        'Error getting consumer groups list',\n        error,\n        clientMetadata,\n      );\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Get consumer group pending info using 'XPENDING' command\n   * @param client\n   * @param dto\n   * @param group\n   */\n  async getGroupInfo(\n    client: RedisClient,\n    dto: KeyDto,\n    group: ConsumerGroupDto,\n  ): Promise<ConsumerGroupDto> {\n    let info = ['n/a', 'n/a', 'n/a'];\n\n    if (!isTruncatedString(group.name)) {\n      info = (await client.sendCommand([\n        BrowserToolStreamCommands.XPending,\n        dto.keyName,\n        group.name,\n      ])) as string[];\n    }\n\n    return plainToInstance(ConsumerGroupDto, {\n      ...group,\n      smallestPendingId: info?.[1] || null,\n      greatestPendingId: info?.[2] || null,\n    });\n  }\n\n  /**\n   * Create consumer group(s)\n   * @param clientMetadata\n   * @param dto\n   */\n  async createGroups(\n    clientMetadata: ClientMetadata,\n    dto: CreateConsumerGroupsDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating consumer groups.', clientMetadata);\n      const { keyName, consumerGroups } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const toolCommands: Array<\n        [\n          toolCommand: BrowserToolCommands,\n          ...args: Array<string | number | Buffer>,\n        ]\n      > = consumerGroups.map((group) => [\n        BrowserToolStreamCommands.XGroupCreate,\n        keyName,\n        group.name,\n        group.lastDeliveredId,\n      ]);\n\n      const transactionResults = await client.sendPipeline(toolCommands);\n      catchMultiTransactionError(transactionResults);\n\n      this.logger.debug('Stream consumer group(s) created.', clientMetadata);\n      return undefined;\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      if (error?.message.includes(RedisErrorCodes.BusyGroup)) {\n        throw new ConflictException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Updates last delivered id for Consumer Group\n   * @param clientMetadata\n   * @param dto\n   */\n  async updateGroup(\n    clientMetadata: ClientMetadata,\n    dto: UpdateConsumerGroupDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Updating consumer group.', clientMetadata);\n      const { keyName, name, lastDeliveredId } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      await client.sendCommand([\n        BrowserToolStreamCommands.XGroupSetId,\n        keyName,\n        name,\n        lastDeliveredId,\n      ]);\n\n      this.logger.debug('Consumer group was updated.', clientMetadata);\n      return undefined;\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      if (error?.message.includes(RedisErrorCodes.NoGroup)) {\n        throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Delete consumer groups in batch\n   * @param clientMetadata\n   * @param dto\n   */\n  async deleteGroup(\n    clientMetadata: ClientMetadata,\n    dto: DeleteConsumerGroupsDto,\n  ): Promise<DeleteConsumerGroupsResponse> {\n    try {\n      this.logger.debug('Deleting consumer group.', clientMetadata);\n      const { keyName, consumerGroups } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const toolCommands: Array<\n        [\n          toolCommand: BrowserToolCommands,\n          ...args: Array<string | number | Buffer>,\n        ]\n      > = consumerGroups.map((group) => [\n        BrowserToolStreamCommands.XGroupDestroy,\n        keyName,\n        group,\n      ]);\n\n      const transactionResults = await client.sendPipeline(toolCommands);\n      catchMultiTransactionError(transactionResults);\n\n      this.logger.debug(\n        'Consumer group(s) successfully deleted.',\n        clientMetadata,\n      );\n      return { affected: toolCommands.length };\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Converts RESP response from Redis\n   * [\n   *  ['name', 'g1', 'consumers', 0, 'pending', 0, 'last-delivered-id', '1653034260278-0'],\n   *  ['name', 'g2', 'consumers', 0, 'pending', 0, 'last-delivered-id', '1653034260278-0'],\n   *  ...\n   * ]\n   *\n   * to DTO\n   *\n   * [\n   *  {\n   *    name: 'g1',\n   *    consumers: 0,\n   *    pending: 0,\n   *    lastDeliveredId: '1653034260278-0'\n   *  },\n   *  {\n   *    name: 'g2',\n   *    consumers: 0,\n   *    pending: 0,\n   *    lastDeliveredId: '1653034260278-0'\n   *  },\n   *   ...\n   * ]\n   * @param reply\n   */\n  static formatReplyToDto(\n    reply: Array<Array<string | number>>,\n  ): ConsumerGroupDto[] {\n    return reply.map(ConsumerGroupService.formatArrayToDto);\n  }\n\n  /**\n   * Format single reply entry to DTO\n   * @param entry\n   */\n  static formatArrayToDto(\n    entry: Array<RedisString | number>,\n  ): ConsumerGroupDto {\n    if (!entry?.length) {\n      return null;\n    }\n\n    const [, name, , consumers, , pending, , lastDeliveredId] = entry;\n\n    return plainToInstance(ConsumerGroupDto, {\n      name,\n      consumers,\n      pending,\n      lastDeliveredId: lastDeliveredId.toString(),\n      smallestPendingId: null,\n      greatestPendingId: null,\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/services/consumer.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockBrowserClientMetadata,\n  mockStandaloneRedisClient,\n  mockDatabaseClientFactory,\n} from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RedisErrorCodes } from 'src/constants';\nimport { ConsumerService } from 'src/modules/browser/stream/services/consumer.service';\nimport {\n  mockAckPendingMessagesDto,\n  mockAdditionalClaimPendingEntriesDto,\n  mockClaimPendingEntriesDto,\n  mockClaimPendingEntriesReply,\n  mockConsumer,\n  mockConsumerGroup,\n  mockConsumerReply,\n  mockGetPendingMessagesDto,\n  mockKeyDto,\n  mockPendingMessage,\n  mockPendingMessageReply,\n} from 'src/modules/browser/__mocks__';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('ConsumerService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: ConsumerService;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ConsumerService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get(ConsumerService);\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('getGroups', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockKeyDto.keyName])\n        .mockResolvedValue(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoConsumers]),\n        )\n        .mockResolvedValue([mockConsumerReply, mockConsumerReply]);\n    });\n    it('should get consumers list', async () => {\n      const consumers = await service.getConsumers(mockBrowserClientMetadata, {\n        ...mockKeyDto,\n        groupName: mockConsumerGroup.name,\n      });\n      expect(consumers).toEqual([mockConsumer, mockConsumer]);\n    });\n    it('should throw error when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockKeyDto.keyName])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.getConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.getConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Not Found error when no group', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoConsumers]),\n        )\n        .mockRejectedValueOnce(new Error('NOGROUP no such group'));\n\n      try {\n        await service.getConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoConsumers]),\n        )\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.getConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoConsumers]),\n        )\n        .mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.getConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('deleteConsumers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockKeyDto.keyName])\n        .mockResolvedValue(true);\n      client.sendPipeline.mockResolvedValue([[null, '123-1']]);\n    });\n    it('delete consumers', async () => {\n      await expect(\n        service.deleteConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n          consumerNames: [mockConsumer.name, mockConsumer.name],\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolStreamCommands.XGroupDelConsumer,\n          mockKeyDto.keyName,\n          mockConsumerGroup.name,\n          mockConsumer.name,\n        ],\n        [\n          BrowserToolStreamCommands.XGroupDelConsumer,\n          mockKeyDto.keyName,\n          mockConsumerGroup.name,\n          mockConsumer.name,\n        ],\n      ]);\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.deleteConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n          consumerNames: [mockConsumer.name, mockConsumer.name],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, expect.anything()])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.deleteConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n          consumerNames: [mockConsumer.name, mockConsumer.name],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Not Found error when group does not exists', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error(RedisErrorCodes.NoGroup), '123-1'],\n      ]);\n\n      try {\n        await service.deleteConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n          consumerNames: [mockConsumer.name, mockConsumer.name],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error(RedisErrorCodes.WrongType), '123-1'],\n      ]);\n\n      try {\n        await service.deleteConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n          consumerNames: [mockConsumer.name, mockConsumer.name],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      client.sendPipeline.mockResolvedValue([[new Error('oO'), '123-1']]);\n\n      try {\n        await service.deleteConsumers(mockBrowserClientMetadata, {\n          ...mockKeyDto,\n          groupName: mockConsumerGroup.name,\n          consumerNames: [mockConsumer.name, mockConsumer.name],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('getPendingEntries', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockGetPendingMessagesDto.keyName,\n        ])\n        .mockResolvedValue(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockResolvedValue([mockPendingMessageReply, mockPendingMessageReply]);\n    });\n    it('should get consumers list', async () => {\n      const consumers = await service.getPendingEntries(\n        mockBrowserClientMetadata,\n        mockGetPendingMessagesDto,\n      );\n      expect(consumers).toEqual([mockPendingMessage, mockPendingMessage]);\n\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStreamCommands.XPending,\n        mockGetPendingMessagesDto.keyName,\n        mockGetPendingMessagesDto.groupName,\n        mockGetPendingMessagesDto.start,\n        mockGetPendingMessagesDto.end,\n        mockGetPendingMessagesDto.count,\n        mockGetPendingMessagesDto.consumerName,\n      ]);\n    });\n    it('should throw error when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockGetPendingMessagesDto.keyName,\n        ])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.getPendingEntries(\n          mockBrowserClientMetadata,\n          mockGetPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockKeyDto.keyName])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.getPendingEntries(\n          mockBrowserClientMetadata,\n          mockGetPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Not Found error when no group', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockRejectedValueOnce(new Error('NOGROUP no such group'));\n\n      try {\n        await service.getPendingEntries(\n          mockBrowserClientMetadata,\n          mockGetPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.getPendingEntries(\n          mockBrowserClientMetadata,\n          mockGetPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XPending]),\n        )\n        .mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.getPendingEntries(\n          mockBrowserClientMetadata,\n          mockGetPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('ackPendingEntries', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAckPendingMessagesDto.keyName,\n        ])\n        .mockResolvedValue(true);\n      client.sendCommand.mockResolvedValue(2);\n    });\n    it('ack pending entries', async () => {\n      const result = await service.ackPendingEntries(\n        mockBrowserClientMetadata,\n        mockAckPendingMessagesDto,\n      );\n      expect(result).toEqual({ affected: 2 });\n\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStreamCommands.XAck,\n        mockAckPendingMessagesDto.keyName,\n        mockAckPendingMessagesDto.groupName,\n        ...mockAckPendingMessagesDto.entries,\n      ]);\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAckPendingMessagesDto.keyName,\n        ])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.ackPendingEntries(\n          mockBrowserClientMetadata,\n          mockAckPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should proxy Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XAck]))\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.ackPendingEntries(\n          mockBrowserClientMetadata,\n          mockAckPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Bad Request when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XAck]))\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.ackPendingEntries(\n          mockBrowserClientMetadata,\n          mockAckPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XAck]))\n        .mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.ackPendingEntries(\n          mockBrowserClientMetadata,\n          mockAckPendingMessagesDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('claimPendingEntries', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockClaimPendingEntriesDto.keyName,\n        ])\n        .mockResolvedValue(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XClaim]),\n          { replyEncoding: 'utf8' },\n        )\n        .mockResolvedValue(mockClaimPendingEntriesReply);\n    });\n    it('claim pending entries', async () => {\n      const result = await service.claimPendingEntries(\n        mockBrowserClientMetadata,\n        mockClaimPendingEntriesDto,\n      );\n      expect(result).toEqual({ affected: mockClaimPendingEntriesReply });\n\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        [\n          BrowserToolStreamCommands.XClaim,\n          mockClaimPendingEntriesDto.keyName,\n          mockClaimPendingEntriesDto.groupName,\n          mockClaimPendingEntriesDto.consumerName,\n          mockClaimPendingEntriesDto.minIdleTime,\n          ...mockClaimPendingEntriesDto.entries,\n          'justid',\n        ],\n        { replyEncoding: 'utf8' },\n      );\n    });\n    it('claim pending entries with additional args', async () => {\n      const result = await service.claimPendingEntries(\n        mockBrowserClientMetadata,\n        {\n          ...mockClaimPendingEntriesDto,\n          ...mockAdditionalClaimPendingEntriesDto,\n        },\n      );\n      expect(result).toEqual({ affected: mockClaimPendingEntriesReply });\n\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        [\n          BrowserToolStreamCommands.XClaim,\n          mockClaimPendingEntriesDto.keyName,\n          mockClaimPendingEntriesDto.groupName,\n          mockClaimPendingEntriesDto.consumerName,\n          mockClaimPendingEntriesDto.minIdleTime,\n          ...mockClaimPendingEntriesDto.entries,\n          'time',\n          mockAdditionalClaimPendingEntriesDto.time,\n          'retrycount',\n          mockAdditionalClaimPendingEntriesDto.retryCount,\n          'force',\n          'justid',\n        ],\n        { replyEncoding: 'utf8' },\n      );\n    });\n    it('claim pending entries with additional args and \"idle\" instead of \"time\"', async () => {\n      const result = await service.claimPendingEntries(\n        mockBrowserClientMetadata,\n        {\n          ...mockClaimPendingEntriesDto,\n          ...mockAdditionalClaimPendingEntriesDto,\n          idle: 0,\n        },\n      );\n      expect(result).toEqual({ affected: mockClaimPendingEntriesReply });\n\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        [\n          BrowserToolStreamCommands.XClaim,\n          mockClaimPendingEntriesDto.keyName,\n          mockClaimPendingEntriesDto.groupName,\n          mockClaimPendingEntriesDto.consumerName,\n          mockClaimPendingEntriesDto.minIdleTime,\n          ...mockClaimPendingEntriesDto.entries,\n          'idle',\n          0,\n          'retrycount',\n          mockAdditionalClaimPendingEntriesDto.retryCount,\n          'force',\n          'justid',\n        ],\n        { replyEncoding: 'utf8' },\n      );\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockClaimPendingEntriesDto.keyName,\n        ])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.claimPendingEntries(\n          mockBrowserClientMetadata,\n          mockClaimPendingEntriesDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should proxy Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XClaim]))\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.claimPendingEntries(\n          mockBrowserClientMetadata,\n          mockClaimPendingEntriesDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Bad Request when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XClaim]))\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.claimPendingEntries(\n          mockBrowserClientMetadata,\n          mockClaimPendingEntriesDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XClaim]))\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.NoGroup));\n\n      try {\n        await service.claimPendingEntries(\n          mockBrowserClientMetadata,\n          mockClaimPendingEntriesDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XClaim]))\n        .mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.claimPendingEntries(\n          mockBrowserClientMetadata,\n          mockClaimPendingEntriesDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/services/consumer.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { RedisErrorCodes } from 'src/constants';\nimport { catchAclError, catchMultiTransactionError } from 'src/utils';\nimport {\n  BrowserToolCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  AckPendingEntriesDto,\n  AckPendingEntriesResponse,\n  ClaimPendingEntriesResponse,\n  ClaimPendingEntryDto,\n  ConsumerDto,\n  DeleteConsumersDto,\n  GetConsumersDto,\n  GetPendingEntriesDto,\n  PendingEntryDto,\n} from 'src/modules/browser/stream/dto';\nimport { plainToInstance } from 'class-transformer';\nimport { ClientMetadata } from 'src/common/models';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { checkIfKeyNotExists } from 'src/modules/browser/utils';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\n@Injectable()\nexport class ConsumerService {\n  private logger = new Logger('ConsumerService');\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  /**\n   * Get consumers list inside particular group\n   * @param clientMetadata\n   * @param dto\n   */\n  async getConsumers(\n    clientMetadata: ClientMetadata,\n    dto: GetConsumersDto,\n  ): Promise<ConsumerDto[]> {\n    try {\n      this.logger.debug('Getting consumers list.', clientMetadata);\n      const { keyName, groupName } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      return ConsumerService.formatReplyToDto(\n        (await client.sendCommand([\n          BrowserToolStreamCommands.XInfoConsumers,\n          keyName,\n          groupName,\n        ])) as string[][],\n      );\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.NoGroup)) {\n        throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Get consumers list inside particular group\n   * @param clientMetadata\n   * @param dto\n   */\n  async deleteConsumers(\n    clientMetadata: ClientMetadata,\n    dto: DeleteConsumersDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Deleting consumers from the group.', clientMetadata);\n      const { keyName, groupName, consumerNames } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const toolCommands: Array<\n        [\n          toolCommand: BrowserToolCommands,\n          ...args: Array<string | number | Buffer>,\n        ]\n      > = consumerNames.map((consumerName) => [\n        BrowserToolStreamCommands.XGroupDelConsumer,\n        keyName,\n        groupName,\n        consumerName,\n      ]);\n\n      const transactionResults = await client.sendPipeline(toolCommands);\n      catchMultiTransactionError(transactionResults);\n\n      return undefined;\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.NoGroup)) {\n        throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Get list of pending entries info for particular consumer\n   * @param clientMetadata\n   * @param dto\n   */\n  async getPendingEntries(\n    clientMetadata: ClientMetadata,\n    dto: GetPendingEntriesDto,\n  ): Promise<PendingEntryDto[]> {\n    try {\n      this.logger.debug('Getting pending entries list.', clientMetadata);\n      const { keyName, groupName, start, end, count, consumerName } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      return ConsumerService.formatReplyToPendingEntriesDto(\n        (await client.sendCommand([\n          BrowserToolStreamCommands.XPending,\n          keyName,\n          groupName,\n          start,\n          end,\n          count,\n          consumerName,\n        ])) as string[][],\n      );\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.NoGroup)) {\n        throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Acknowledge pending entries\n   * @param clientMetadata\n   * @param dto\n   */\n  async ackPendingEntries(\n    clientMetadata: ClientMetadata,\n    dto: AckPendingEntriesDto,\n  ): Promise<AckPendingEntriesResponse> {\n    try {\n      this.logger.debug('Acknowledging pending entries.', clientMetadata);\n      const { keyName, groupName, entries } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const affected = (await client.sendCommand([\n        BrowserToolStreamCommands.XAck,\n        keyName,\n        groupName,\n        ...entries,\n      ])) as number;\n\n      this.logger.debug(\n        'Successfully acknowledged pending entries.',\n        clientMetadata,\n      );\n      return { affected };\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Claim pending entries with additional parameters\n   * @param clientMetadata\n   * @param dto\n   */\n  async claimPendingEntries(\n    clientMetadata: ClientMetadata,\n    dto: ClaimPendingEntryDto,\n  ): Promise<ClaimPendingEntriesResponse> {\n    try {\n      this.logger.debug('Claiming pending entries.', clientMetadata);\n      const {\n        keyName,\n        groupName,\n        consumerName,\n        minIdleTime,\n        entries,\n        idle,\n        time,\n        retryCount,\n        force,\n      } = dto;\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const args = [keyName, groupName, consumerName, minIdleTime, ...entries];\n\n      if (idle !== undefined) {\n        args.push('idle', idle);\n      } else if (time !== undefined) {\n        args.push('time', time);\n      }\n\n      if (retryCount !== undefined) {\n        args.push('retrycount', retryCount);\n      }\n\n      if (force) {\n        args.push('force');\n      }\n\n      // Return just an array of IDs of messages successfully claimed, without returning the actual message.\n      args.push('justid');\n\n      const affected = (await client.sendCommand(\n        [BrowserToolStreamCommands.XClaim, ...args],\n        { replyEncoding: 'utf8' },\n      )) as string[];\n\n      this.logger.debug(\n        'Successfully claimed pending entries.',\n        clientMetadata,\n      );\n      return { affected };\n    } catch (error) {\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.NoGroup)) {\n        throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Converts RESP response from Redis\n   * [\n   *  ['name', 'consumer-1', 'pending', 0, 'idle', 258741],\n   *  ['name', 'consumer-2', 'pending', 0, 'idle', 258741],\n   *  ...\n   * ]\n   *\n   * to DTO\n   *\n   * [\n   *  {\n   *    name: 'consumer-1',\n   *    pending: 0,\n   *    idle: 258741,\n   *  },\n   *  {\n   *    name: 'consumer-2',\n   *    pending: 0,\n   *    idle: 258741,\n   *  },\n   *   ...\n   * ]\n   * @param reply\n   */\n  static formatReplyToDto(reply: Array<Array<string | number>>): ConsumerDto[] {\n    return reply.map(ConsumerService.formatArrayToDto);\n  }\n\n  /**\n   * Format single reply entry to DTO\n   * @param entry\n   */\n  static formatArrayToDto(entry: Array<string | number>): ConsumerDto {\n    if (!entry?.length) {\n      return null;\n    }\n\n    const [, name, , pending, , idle] = entry;\n\n    return plainToInstance(ConsumerDto, {\n      name,\n      pending,\n      idle,\n    });\n  }\n\n  /**\n   * Converts RESP response from Redis\n   * [\n   *  ['1567352639-0', 'consumer-1', 258741, 2],\n   *  ...\n   * ]\n   *\n   * to DTO\n   *\n   * [\n   *  {\n   *    id: '1567352639-0',\n   *    name: 'consumer-1',\n   *    idle: 258741,\n   *    delivered: 2,\n   *  },\n   *   ...\n   * ]\n   * @param reply\n   */\n  static formatReplyToPendingEntriesDto(\n    reply: Array<Array<string | number>>,\n  ): PendingEntryDto[] {\n    return reply.map(ConsumerService.formatArrayToPendingEntryDto);\n  }\n\n  /**\n   * Format single reply entry to DTO\n   * @param entry\n   */\n  static formatArrayToPendingEntryDto(\n    entry: Array<string | number>,\n  ): PendingEntryDto {\n    if (!entry?.length) {\n      return null;\n    }\n\n    return plainToInstance(PendingEntryDto, {\n      id: `${entry[0]}`,\n      consumerName: entry[1],\n      idle: +entry[2],\n      delivered: +entry[3],\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/services/stream.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockBrowserClientMetadata,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { StreamService } from 'src/modules/browser/stream/services/stream.service';\nimport {\n  BadRequestException,\n  ConflictException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RedisErrorCodes, SortOrder } from 'src/constants';\nimport {\n  mockAddStreamEntriesDto,\n  mockEmptyStreamEntriesReply,\n  mockEmptyStreamInfo,\n  mockEmptyStreamInfoReply,\n  mockGetStreamEntriesDto,\n  mockStreamEntries,\n  mockStreamEntriesReply,\n  mockStreamEntry,\n  mockStreamInfo,\n  mockStreamInfoReply,\n} from 'src/modules/browser/__mocks__';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('StreamService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: StreamService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        StreamService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get(StreamService);\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('createStream', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValue(false);\n      client.sendPipeline.mockResolvedValue([[null, '123-1']]);\n    });\n    it('create stream with expiration', async () => {\n      await expect(\n        service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n          expire: 1000,\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolStreamCommands.XAdd,\n          mockAddStreamEntriesDto.keyName,\n          mockStreamEntry.id,\n          mockStreamEntry.fields[0].name,\n          mockStreamEntry.fields[0].value,\n        ],\n        [BrowserToolKeysCommands.Expire, mockAddStreamEntriesDto.keyName, 1000],\n      ]);\n    });\n    it('create stream without expiration', async () => {\n      await expect(\n        service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolStreamCommands.XAdd,\n          mockAddStreamEntriesDto.keyName,\n          mockStreamEntry.id,\n          mockStreamEntry.fields[0].name,\n          mockStreamEntry.fields[0].value,\n        ],\n      ]);\n    });\n    it('should throw error key exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValueOnce(true);\n\n      try {\n        await service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ConflictException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NAME_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error(RedisErrorCodes.WrongType), '123-1'],\n      ]);\n\n      try {\n        await service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Bad Request when incorrect ID', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error('ID specified in XADD is equal or smaller'), '123-1'],\n      ]);\n\n      try {\n        await service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('ID specified in XADD is equal or smaller');\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      client.sendPipeline.mockResolvedValue([[new Error('oO'), '123-1']]);\n\n      try {\n        await service.createStream(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('addEntries', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValue(true);\n      client.sendPipeline.mockResolvedValue([[null, '123-1']]);\n    });\n    it('add entries', async () => {\n      await expect(\n        service.addEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        }),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith([\n        [\n          BrowserToolStreamCommands.XAdd,\n          mockAddStreamEntriesDto.keyName,\n          mockStreamEntry.id,\n          mockStreamEntry.fields[0].name,\n          mockStreamEntry.fields[0].value,\n        ],\n      ]);\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.addEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.addEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error(RedisErrorCodes.WrongType), '123-1'],\n      ]);\n\n      try {\n        await service.addEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Bad Request when incorrect ID', async () => {\n      client.sendPipeline.mockResolvedValue([\n        [new Error('ID specified in XADD is equal or smaller'), '123-1'],\n      ]);\n\n      try {\n        await service.addEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('ID specified in XADD is equal or smaller');\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      client.sendPipeline.mockResolvedValue([[new Error('oO'), '123-1']]);\n\n      try {\n        await service.addEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('get entries from empty stream', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValue(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoStream]),\n        )\n        .mockResolvedValue(mockEmptyStreamInfoReply);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XRevRange]),\n        )\n        .mockResolvedValue(mockEmptyStreamEntriesReply);\n    });\n    it('Should return stream with 0 entries', async () => {\n      const result = await service.getEntries(mockBrowserClientMetadata, {\n        ...mockGetStreamEntriesDto,\n      });\n      expect(result).toEqual({\n        ...mockEmptyStreamInfo,\n        entries: mockEmptyStreamEntriesReply,\n      });\n    });\n  });\n  describe('getEntries', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolKeysCommands.Exists]))\n        .mockResolvedValue(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoStream]),\n        )\n        .mockResolvedValue(mockStreamInfoReply);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XRevRange]),\n        )\n        .mockResolvedValue(mockStreamEntriesReply);\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XRange]))\n        .mockResolvedValue(mockStreamEntriesReply);\n    });\n    it('get entries DESC', async () => {\n      const result = await service.getEntries(mockBrowserClientMetadata, {\n        ...mockGetStreamEntriesDto,\n      });\n      expect(result).toEqual({\n        ...mockStreamInfo,\n        entries: mockStreamEntries,\n      });\n    });\n    it('get entries ASC', async () => {\n      const result = await service.getEntries(mockBrowserClientMetadata, {\n        ...mockGetStreamEntriesDto,\n        sortOrder: SortOrder.Asc,\n      });\n      expect(result).toEqual({\n        ...mockStreamInfo,\n        entries: mockStreamEntries,\n      });\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.getEntries(mockBrowserClientMetadata, {\n          ...mockGetStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Not Found error', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockRejectedValueOnce(\n          new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID),\n        );\n\n      try {\n        await service.getEntries(mockBrowserClientMetadata, {\n          ...mockGetStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolStreamCommands.XInfoStream,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.getEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n    it('should throw Internal Server error', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolStreamCommands.XInfoStream,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockRejectedValueOnce(new Error('oO'));\n\n      try {\n        await service.getEntries(mockBrowserClientMetadata, {\n          ...mockGetStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('oO');\n      }\n    });\n  });\n  describe('deleteEntries', () => {\n    const mockEntriesIds = mockStreamEntries.map(({ id }) => id);\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValue(true);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XInfoStream]),\n        )\n        .mockResolvedValue(mockStreamInfoReply);\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([BrowserToolStreamCommands.XRevRange]),\n        )\n        .mockResolvedValue(mockStreamEntriesReply);\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XRange]))\n        .mockResolvedValue(mockStreamEntriesReply);\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining([BrowserToolStreamCommands.XDel]))\n        .mockResolvedValue(mockStreamEntries.length);\n    });\n    it('delete entries', async () => {\n      const result = await service.deleteEntries(mockBrowserClientMetadata, {\n        keyName: mockAddStreamEntriesDto.keyName,\n        entries: mockEntriesIds,\n      });\n      expect(result).toEqual({ affected: mockStreamEntries.length });\n    });\n    it('should throw Not Found when key does not exists', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockResolvedValueOnce(false);\n\n      try {\n        await service.deleteEntries(mockBrowserClientMetadata, {\n          keyName: mockAddStreamEntriesDto.keyName,\n          entries: mockEntriesIds,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.KEY_NOT_EXIST);\n      }\n    });\n    it('should throw Wrong Type error', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolStreamCommands.XInfoStream,\n          mockAddStreamEntriesDto.keyName,\n        ])\n        .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType));\n\n      try {\n        await service.getEntries(mockBrowserClientMetadata, {\n          ...mockAddStreamEntriesDto,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(RedisErrorCodes.WrongType);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/services/stream.service.ts",
    "content": "import { chunk, flatMap, map } from 'lodash';\nimport {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { catchAclError, catchMultiTransactionError } from 'src/utils';\nimport { SortOrder } from 'src/constants/sort';\nimport {\n  BrowserToolCommands,\n  BrowserToolKeysCommands,\n  BrowserToolStreamCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  AddStreamEntriesDto,\n  AddStreamEntriesResponse,\n  CreateStreamDto,\n  GetStreamEntriesDto,\n  GetStreamEntriesResponse,\n  DeleteStreamEntriesDto,\n  DeleteStreamEntriesResponse,\n  StreamEntryDto,\n  StreamEntryFieldDto,\n} from 'src/modules/browser/stream/dto';\nimport { RedisErrorCodes } from 'src/constants';\nimport { plainToInstance } from 'class-transformer';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\n\n@Injectable()\nexport class StreamService {\n  private logger = new Logger('StreamService');\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  /**\n   * Get stream entries\n   * Could be used for lazy loading with \"start\", \"end\" and \"count\" parameters\n   * Could be sorted using \"sortOrder\" in ASC and DESC order\n   *\n   * @param clientMetadata\n   * @param dto\n   */\n  public async getEntries(\n    clientMetadata: ClientMetadata,\n    dto: GetStreamEntriesDto,\n  ): Promise<GetStreamEntriesResponse> {\n    try {\n      this.logger.debug(\n        'Getting entries of the Stream data type stored at key.',\n        clientMetadata,\n      );\n      const { keyName, sortOrder } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const info = convertArrayReplyToObject(\n        (await client.sendCommand([\n          BrowserToolStreamCommands.XInfoStream,\n          keyName,\n        ])) as string[],\n      );\n\n      let entries = [];\n      if (sortOrder && sortOrder === SortOrder.Asc) {\n        entries = await this.getRange(client, dto);\n      } else {\n        entries = await this.getRevRange(client, dto);\n      }\n\n      this.logger.debug(\n        'Succeed to get entries from the stream.',\n        clientMetadata,\n      );\n\n      return plainToInstance(GetStreamEntriesResponse, {\n        keyName,\n        total: info['length'],\n        lastGeneratedId: info['last-generated-id'].toString(),\n        firstEntry: StreamService.formatArrayToDto(info['first-entry']),\n        lastEntry: StreamService.formatArrayToDto(info['last-entry']),\n        entries,\n      });\n    } catch (error) {\n      this.logger.error(\n        'Failed to get entries from the stream.',\n        error,\n        clientMetadata,\n      );\n\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Return specified number of entries in the time range in ASC order\n   *\n   * @param client\n   * @param dto\n   */\n  public async getRange(\n    client: RedisClient,\n    dto: GetStreamEntriesDto,\n  ): Promise<StreamEntryDto[]> {\n    const { keyName, start, end, count } = dto;\n\n    const execResult = (await client.sendCommand([\n      BrowserToolStreamCommands.XRange,\n      keyName,\n      start,\n      end,\n      'COUNT',\n      count,\n    ])) as string[];\n\n    return StreamService.formatReplyToDto(execResult);\n  }\n\n  /**\n   * Return specified number of entries in the time range in DESC order\n   *\n   * @param client\n   * @param dto\n   */\n  public async getRevRange(\n    client: RedisClient,\n    dto: GetStreamEntriesDto,\n  ): Promise<StreamEntryDto[]> {\n    const { keyName, start, end, count } = dto;\n\n    const execResult = (await client.sendCommand([\n      BrowserToolStreamCommands.XRevRange,\n      keyName,\n      end,\n      start,\n      'COUNT',\n      count,\n    ])) as string[];\n\n    return StreamService.formatReplyToDto(execResult);\n  }\n\n  /**\n   * Create streams with\\without expiration time and add multiple entries in a transaction\n   * @param clientMetadata\n   * @param dto\n   */\n  public async createStream(\n    clientMetadata: ClientMetadata,\n    dto: CreateStreamDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating stream data type.', clientMetadata);\n      const { keyName, entries } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyExists(keyName, client);\n\n      const entriesArray = entries.map((entry) => [\n        entry.id,\n        ...flatMap(map(entry.fields, (field) => [field.name, field.value])),\n      ]);\n\n      const toolCommands: Array<\n        [\n          toolCommand: BrowserToolCommands,\n          ...args: Array<string | number | Buffer>,\n        ]\n      > = entriesArray.map((entry) => [\n        BrowserToolStreamCommands.XAdd,\n        keyName,\n        ...entry,\n      ]);\n\n      if (dto.expire) {\n        toolCommands.push([\n          BrowserToolKeysCommands.Expire,\n          keyName,\n          dto.expire,\n        ]);\n      }\n\n      const transactionResults = await client.sendPipeline(toolCommands);\n      catchMultiTransactionError(transactionResults);\n\n      this.logger.debug('Succeed to create stream.', clientMetadata);\n      return undefined;\n    } catch (error) {\n      this.logger.error('Failed to create stream.', error, clientMetadata);\n\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (\n        error?.message.includes(RedisErrorCodes.WrongType) ||\n        error?.message.includes('ID specified in XADD is equal or smaller')\n      ) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Add entries to the existing stream and return entries IDs list\n   * @param clientMetadata\n   * @param dto\n   */\n  public async addEntries(\n    clientMetadata: ClientMetadata,\n    dto: AddStreamEntriesDto,\n  ): Promise<AddStreamEntriesResponse> {\n    try {\n      this.logger.debug('Adding entries to stream.', clientMetadata);\n      const { keyName, entries } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const entriesArray = entries.map((entry) => [\n        entry.id,\n        ...flatMap(map(entry.fields, (field) => [field.name, field.value])),\n      ]);\n\n      const toolCommands: Array<\n        [\n          toolCommand: BrowserToolCommands,\n          ...args: Array<string | number | Buffer>,\n        ]\n      > = entriesArray.map((entry) => [\n        BrowserToolStreamCommands.XAdd,\n        keyName,\n        ...entry,\n      ]);\n\n      const transactionResults = await client.sendPipeline(toolCommands);\n      catchMultiTransactionError(transactionResults);\n\n      this.logger.debug(\n        'Succeed to add entries to the stream.',\n        clientMetadata,\n      );\n      return plainToInstance(AddStreamEntriesResponse, {\n        keyName,\n        entries: transactionResults.map((entryResult) =>\n          entryResult[1].toString(),\n        ),\n      });\n    } catch (error) {\n      this.logger.error(\n        'Failed to add entries to the stream.',\n        error,\n        clientMetadata,\n      );\n\n      if (error instanceof NotFoundException) {\n        throw error;\n      }\n\n      if (\n        error?.message.includes(RedisErrorCodes.WrongType) ||\n        error?.message.includes('ID specified in XADD is equal or smaller')\n      ) {\n        throw new BadRequestException(error.message);\n      }\n\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Delete entries from the existing stream and return number of deleted entries\n   * @param clientMetadata\n   * @param dto\n   */\n  public async deleteEntries(\n    clientMetadata: ClientMetadata,\n    dto: DeleteStreamEntriesDto,\n  ): Promise<DeleteStreamEntriesResponse> {\n    try {\n      this.logger.debug(\n        'Deleting entries from the Stream data type.',\n        clientMetadata,\n      );\n      const { keyName, entries } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = (await client.sendCommand([\n        BrowserToolStreamCommands.XDel,\n        keyName,\n        ...entries,\n      ])) as number;\n\n      this.logger.debug(\n        'Succeed to delete entries from the Stream data type.',\n        clientMetadata,\n      );\n      return { affected: result };\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete entries from the Stream data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Converts RESP response from Redis\n   * [\n   *   [ '1650985323741-0', [ 'field', 'value' ] ],\n   *   [ '1650985351882-0', [ 'field', 'value2' ] ],\n   *   ...\n   * ]\n   *\n   * to DTO\n   *\n   * [\n   *   { id: '1650985323741-0', fields: [ ['field', 'value'] ] },\n   *   { id: '1650985351882-0', fields: [ ['field', 'value2 ] },\n   *   ...\n   * ]\n   * @param reply\n   */\n  static formatReplyToDto(reply: Array<string | string[]>): StreamEntryDto[] {\n    return reply.map(StreamService.formatArrayToDto);\n  }\n\n  /**\n   * Format single reply entry to DTO\n   * @param entry\n   */\n  static formatArrayToDto(entry: Array<string>): StreamEntryDto {\n    if (!entry?.length) {\n      return null;\n    }\n\n    return {\n      id: entry[0].toString(),\n      fields: chunk(entry[1] || [], 2).map((field) =>\n        plainToInstance(StreamEntryFieldDto, {\n          name: field[0],\n          value: field[1],\n        }),\n      ),\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/stream/stream.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { StreamController } from 'src/modules/browser/stream/controllers/stream.controller';\nimport { ConsumerController } from 'src/modules/browser/stream/controllers/consumer.controller';\nimport { ConsumerGroupController } from 'src/modules/browser/stream/controllers/consumer-group.controller';\nimport { StreamService } from 'src/modules/browser/stream/services/stream.service';\nimport { ConsumerService } from 'src/modules/browser/stream/services/consumer.service';\nimport { ConsumerGroupService } from 'src/modules/browser/stream/services/consumer-group.service';\n\n@Module({})\nexport class StreamModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: StreamModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: StreamModule,\n          },\n        ]),\n      ],\n      controllers: [\n        StreamController,\n        ConsumerController,\n        ConsumerGroupController,\n      ],\n      providers: [StreamService, ConsumerService, ConsumerGroupService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/dto/get.string-info.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsInt, IsOptional, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { IsBiggerThan } from 'src/common/decorators';\n\nexport class GetStringInfoDto extends KeyDto {\n  @ApiProperty({\n    description: 'Start of string',\n    type: Number,\n    default: 0,\n  })\n  @IsOptional()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  @Min(0)\n  start?: number = 0;\n\n  @ApiProperty({\n    description: 'End of string',\n    type: Number,\n  })\n  @IsOptional()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  @Min(1)\n  @IsBiggerThan('start')\n  end?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/dto/get.string-value.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class GetStringValueResponse extends KeyResponse {\n  @ApiProperty({\n    description: 'Key value',\n    type: String,\n  })\n  @RedisStringType()\n  value: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/dto/index.ts",
    "content": "export * from './get.string-info.dto';\nexport * from './get.string-value.response';\nexport * from './set.string.dto';\nexport * from './set.string-with-expire.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/dto/set.string-with-expire.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { SetStringDto } from './set.string.dto';\n\nexport class SetStringWithExpireDto extends IntersectionType(\n  SetStringDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/dto/set.string.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class SetStringDto extends KeyDto {\n  @ApiProperty({\n    description: 'Key value',\n    type: String,\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  value: RedisString;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/string.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  HttpCode,\n  Post,\n  Put,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport {\n  SetStringDto,\n  GetStringValueResponse,\n  SetStringWithExpireDto,\n  GetStringInfoDto,\n} from 'src/modules/browser/string/dto';\nimport { GetKeyInfoDto } from 'src/modules/browser/keys/dto';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { Response } from 'express';\nimport { StringService } from 'src/modules/browser/string/string.service';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: String')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('string')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class StringController extends BrowserBaseController {\n  constructor(private stringService: StringService) {\n    super();\n  }\n\n  @Post('')\n  @ApiOperation({ description: 'Set key to hold string value' })\n  @ApiRedisParams()\n  @ApiBody({ type: SetStringWithExpireDto })\n  @ApiQueryRedisStringEncoding()\n  async setString(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SetStringWithExpireDto,\n  ): Promise<void> {\n    return this.stringService.setString(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/get-value')\n  @HttpCode(200)\n  @ApiOperation({ description: 'Get string value' })\n  @ApiRedisParams()\n  @ApiBody({ type: GetStringInfoDto })\n  @ApiOkResponse({\n    description: 'String value',\n    type: GetStringValueResponse,\n  })\n  @ApiQueryRedisStringEncoding()\n  async getStringValue(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetStringInfoDto,\n  ): Promise<GetStringValueResponse> {\n    return this.stringService.getStringValue(clientMetadata, dto);\n  }\n\n  @ApiEndpoint({\n    description: 'Endpoint do download string value',\n    statusCode: 200,\n  })\n  @Post('/download-value')\n  @ApiRedisParams()\n  @ApiBody({ type: GetKeyInfoDto })\n  @ApiQueryRedisStringEncoding()\n  async downloadStringFile(\n    @Res() res: Response,\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetKeyInfoDto,\n  ): Promise<void> {\n    const { stream } = await this.stringService.downloadStringValue(\n      clientMetadata,\n      dto,\n    );\n\n    res.setHeader('Content-Type', 'application/octet-stream');\n    res.setHeader('Content-Disposition', 'attachment;filename=\"string_value\"');\n    res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');\n\n    stream.on('error', () => res.status(404).send()).pipe(res);\n  }\n\n  @Put('')\n  @ApiOperation({ description: 'Update string value' })\n  @ApiRedisParams()\n  @ApiBody({ type: SetStringDto })\n  @ApiQueryRedisStringEncoding()\n  async updateStringValue(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SetStringDto,\n  ): Promise<void> {\n    return this.stringService.updateStringValue(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/string.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { StringService } from 'src/modules/browser/string/string.service';\nimport { StringController } from 'src/modules/browser/string/string.controller';\n\n@Module({})\nexport class StringModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: StringModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: StringModule,\n          },\n        ]),\n      ],\n      controllers: [StringController],\n      providers: [StringService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/string.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  ConflictException,\n  ForbiddenException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { when } from 'jest-when';\nimport { ReplyError } from 'src/models/redis-client';\nimport {\n  mockBrowserClientMetadata,\n  mockDatabaseClientFactory,\n  mockDatabaseRecommendationService,\n  mockRedisNoPermError,\n  mockRedisWrongTypeError,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport {\n  SetStringDto,\n  SetStringWithExpireDto,\n} from 'src/modules/browser/string/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStringCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { KeytarUnavailableException } from 'src/modules/encryption/exceptions';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { StringService } from 'src/modules/browser/string/string.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nconst mockSetStringDto: SetStringDto = {\n  keyName: Buffer.from('foo'),\n  value: Buffer.from('Lorem ipsum dolor sit amet.'),\n};\n\ndescribe('StringService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: StringService;\n  let recommendationService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        StringService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<StringService>(StringService);\n    recommendationService = module.get<DatabaseRecommendationService>(\n      DatabaseRecommendationService,\n    );\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('setString', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockSetStringDto.keyName])\n        .mockResolvedValue(false);\n    });\n    it('set string with expiration', async () => {\n      const dto: SetStringWithExpireDto = { ...mockSetStringDto, expire: 1000 };\n      const { keyName, value, expire } = dto;\n      const args = [keyName, value, 'EX', `${expire}`, 'NX'];\n      when(client.sendCommand)\n        .calledWith([BrowserToolStringCommands.Set, ...args])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.setString(mockBrowserClientMetadata, dto),\n      ).resolves.not.toThrow();\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStringCommands.Set,\n        ...args,\n      ]);\n    });\n    it('set string without expiration', async () => {\n      const { keyName, value } = mockSetStringDto;\n      const args = [keyName, value, 'NX'];\n      when(client.sendCommand)\n        .calledWith([BrowserToolStringCommands.Set, ...args])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.setString(mockBrowserClientMetadata, mockSetStringDto),\n      ).resolves.not.toThrow();\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStringCommands.Set,\n        ...args,\n      ]);\n    });\n    it('key with this name exist', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockSetStringDto.keyName])\n        .mockResolvedValue(true);\n\n      await expect(\n        service.setString(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(ConflictException);\n    });\n    it(\"user don't have required permissions for setString\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SET',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.setString(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('Should proxy EncryptionService errors', async () => {\n      client.sendCommand.mockRejectedValueOnce(\n        new KeytarUnavailableException(),\n      );\n\n      await expect(\n        service.setString(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(KeytarUnavailableException);\n    });\n  });\n\n  describe('getStringValue', () => {\n    it('succeed to get string value', async () => {\n      client.sendCommand.mockResolvedValue(mockSetStringDto.value);\n\n      const result = await service.getStringValue(\n        mockBrowserClientMetadata,\n        mockSetStringDto,\n      );\n\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStringCommands.Get,\n        mockSetStringDto.keyName,\n      ]);\n      expect(result).toEqual({\n        value: mockSetStringDto.value,\n        keyName: mockSetStringDto.keyName,\n      });\n    });\n    it(\"try to use 'GET' command not for string data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'GET',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getStringValue(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it('key not found', async () => {\n      client.sendCommand.mockResolvedValue(null);\n\n      await expect(\n        service.getStringValue(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"user don't have required permissions for getStringValue\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'GET',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getStringValue(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('should call recommendationService', async () => {\n      client.sendCommand.mockResolvedValue(mockSetStringDto.value);\n\n      const result = await service.getStringValue(\n        mockBrowserClientMetadata,\n        mockSetStringDto,\n      );\n\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStringCommands.Get,\n        mockSetStringDto.keyName,\n      ]);\n      expect(result).toEqual({\n        value: mockSetStringDto.value,\n        keyName: mockSetStringDto.keyName,\n      });\n      expect(recommendationService.check).toBeCalledWith(\n        mockBrowserClientMetadata,\n        RECOMMENDATION_NAMES.STRING_TO_JSON,\n        { value: result.value, keyName: mockSetStringDto.keyName },\n      );\n\n      expect(recommendationService.check).toBeCalledTimes(1);\n    });\n  });\n\n  describe('updateStringValue', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockSetStringDto.keyName])\n        .mockResolvedValue(true);\n    });\n    it('succeed to update string without expiration', async () => {\n      const dto: SetStringDto = mockSetStringDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Ttl, dto.keyName])\n        .mockResolvedValue(-1);\n\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolStringCommands.Set,\n          dto.keyName,\n          dto.value,\n          'XX',\n        ])\n        .mockResolvedValue('OK');\n\n      await expect(\n        service.updateStringValue(mockBrowserClientMetadata, dto),\n      ).resolves.not.toThrow();\n      expect(client.sendCommand).toHaveBeenLastCalledWith([\n        BrowserToolStringCommands.Set,\n        dto.keyName,\n        dto.value,\n        'XX',\n      ]);\n    });\n    it('succeed to update string with expiration', async () => {\n      const dto: SetStringDto = mockSetStringDto;\n      const currentTtl = 1000;\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Ttl, dto.keyName])\n        .mockResolvedValue(currentTtl);\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolStringCommands.Set,\n          dto.keyName,\n          dto.value,\n          'XX',\n        ])\n        .mockResolvedValue('OK');\n\n      await expect(\n        service.updateStringValue(mockBrowserClientMetadata, dto),\n      ).resolves.not.toThrow();\n      expect(client.sendCommand).toHaveBeenCalledWith([\n        BrowserToolStringCommands.Set,\n        dto.keyName,\n        dto.value,\n        'XX',\n      ]);\n      expect(client.sendCommand).toHaveBeenLastCalledWith([\n        BrowserToolKeysCommands.Expire,\n        dto.keyName,\n        currentTtl,\n      ]);\n    });\n    it('key with this name does not exist', async () => {\n      client.sendCommand.mockResolvedValue(null);\n\n      await expect(\n        service.updateStringValue(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"user don't have required permissions for updateStringValue\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SET',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.updateStringValue(mockBrowserClientMetadata, mockSetStringDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/string/string.service.ts",
    "content": "import { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { RECOMMENDATION_NAMES, RedisErrorCodes } from 'src/constants';\nimport { catchAclError } from 'src/utils';\nimport {\n  GetStringInfoDto,\n  GetStringValueResponse,\n  SetStringDto,\n  SetStringWithExpireDto,\n} from 'src/modules/browser/string/dto';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolStringCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { plainToInstance } from 'class-transformer';\nimport { GetKeyInfoDto } from 'src/modules/browser/keys/dto';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { Readable } from 'stream';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient, RedisClientCommand } from 'src/modules/redis/client';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\n\n@Injectable()\nexport class StringService {\n  private logger = new Logger('StringService');\n\n  constructor(\n    private databaseClientFactory: DatabaseClientFactory,\n    private recommendationService: DatabaseRecommendationService,\n  ) {}\n\n  public async setString(\n    clientMetadata: ClientMetadata,\n    dto: SetStringWithExpireDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Setting string key type.', clientMetadata);\n      const { keyName, value, expire } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyExists(keyName, client);\n\n      if (expire) {\n        await client.sendCommand([\n          BrowserToolStringCommands.Set,\n          keyName,\n          value,\n          'EX',\n          `${expire}`,\n          'NX',\n        ]);\n      } else {\n        await client.sendCommand([\n          BrowserToolStringCommands.Set,\n          keyName,\n          value,\n          'NX',\n        ]);\n      }\n\n      this.logger.debug('Succeed to set string key type.', clientMetadata);\n      return null;\n    } catch (error) {\n      this.logger.error('Failed to set string key type', error, clientMetadata);\n      throw catchAclError(error);\n    }\n  }\n\n  public async getStringValue(\n    clientMetadata: ClientMetadata,\n    dto: GetStringInfoDto,\n  ): Promise<GetStringValueResponse> {\n    try {\n      this.logger.debug('Getting string value.', clientMetadata);\n      const { keyName, start, end } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      let value;\n      if (end) {\n        value = await client.sendCommand([\n          BrowserToolStringCommands.Getrange,\n          keyName,\n          `${start}`,\n          `${end}`,\n        ]);\n      } else {\n        value = await client.sendCommand([\n          BrowserToolStringCommands.Get,\n          keyName,\n        ]);\n      }\n\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.STRING_TO_JSON,\n        { value, keyName },\n      );\n\n      this.logger.debug('Succeed to get string value.', clientMetadata);\n      return plainToInstance(GetStringValueResponse, { value, keyName });\n    } catch (error) {\n      this.logger.error('Failed to get string value.', error, clientMetadata);\n      if (error.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async downloadStringValue(\n    clientMetadata: ClientMetadata,\n    dto: GetKeyInfoDto,\n  ): Promise<{ stream: Readable }> {\n    const result = await this.getStringValue(clientMetadata, dto);\n\n    const stream = Readable.from(result.value);\n    return { stream };\n  }\n\n  public async updateStringValue(\n    clientMetadata: ClientMetadata,\n    dto: SetStringDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Updating string value.', clientMetadata);\n      const { keyName, value } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const ttl = await client.sendCommand([\n        BrowserToolKeysCommands.Ttl,\n        keyName,\n      ]);\n      const result = await client.sendCommand([\n        BrowserToolStringCommands.Set,\n        keyName,\n        value,\n        'XX',\n      ]);\n      if (result && ttl > 0) {\n        await client.sendCommand(<RedisClientCommand>[\n          BrowserToolKeysCommands.Expire,\n          keyName,\n          ttl,\n        ]);\n      }\n\n      this.logger.debug('Succeed to update string value.', clientMetadata);\n      return null;\n    } catch (error) {\n      this.logger.error(\n        'Failed to update string value.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/checkKeyExistsing.spec.ts",
    "content": "describe('', () => {\n  xit('should ', () => {});\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/checkKeyExistsing.ts",
    "content": "import { ConflictException, NotFoundException } from '@nestjs/common';\nimport { RedisString } from 'src/common/constants';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport const checkIfKeyNotExists = async (\n  keyName: RedisString,\n  client: RedisClient,\n): Promise<void> => {\n  const isExist = await client.sendCommand([\n    BrowserToolKeysCommands.Exists,\n    keyName,\n  ]);\n  if (!isExist) {\n    throw new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST);\n  }\n};\n\nexport const checkIfKeyExists = async (\n  keyName: RedisString,\n  client: RedisClient,\n): Promise<void> => {\n  const isExist = await client.sendCommand([\n    BrowserToolKeysCommands.Exists,\n    keyName,\n  ]);\n  if (isExist) {\n    throw new ConflictException(ERROR_MESSAGES.KEY_NAME_EXIST);\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/clusterCursor.spec.ts",
    "content": "import ERROR_MESSAGES from 'src/constants/error-messages';\nimport { isClusterCursorValid, parseClusterCursor } from './clusterCursor';\n\nconst isClusterCursorValidTests = [\n  { input: '172.17.0.1:7001@22||172.17.0.1:7002@33', expected: true },\n  { input: '172.17.0.1:7001@-1||172.17.0.1:7002@-1', expected: true },\n  {\n    input:\n      '172.17.0.1:7001@10' +\n      '||172.17.0.1:7002@10' +\n      '||172.17.0.1:7003@10' +\n      '||172.17.0.1:7004@10' +\n      '||172.17.0.1:7005@10' +\n      '||172.17.0.1:7006@10',\n    expected: true,\n  },\n  { input: '172.17.0.1:7001@-1', expected: true },\n  { input: 'domain.com:7001@-1', expected: true },\n  { input: 'domain-with-hyphens.com:7001@-1', expected: true },\n  { input: '172.17.0.1:7001@1228822', expected: true },\n  { input: '172.17.0.1:7001@', expected: false },\n  { input: '172.17.0.1:7001@text', expected: false },\n  { input: '172,17,0,1:7001@-1', expected: false },\n  { input: 'plain text', expected: false },\n  { input: 'text@text||text@text', expected: false },\n  { input: 'text@text', expected: false },\n  { input: '', expected: false },\n];\n\ndescribe('isClusterCursorValid', () => {\n  it.each(isClusterCursorValidTests)('%j', ({ input, expected }) => {\n    expect(isClusterCursorValid(input)).toBe(expected);\n  });\n});\n\nconst defaultNodeScanResult = {\n  total: 0,\n  scanned: 0,\n  host: '172.17.0.1',\n  port: 0,\n  cursor: 0,\n  keys: [],\n};\nconst parsingError = new Error(ERROR_MESSAGES.INCORRECT_CLUSTER_CURSOR_FORMAT);\nconst parseClusterCursorTests = [\n  {\n    input: '172.17.0.1:7001@22||172.17.0.1:7002@33',\n    expected: [\n      { ...defaultNodeScanResult, port: 7001, cursor: 22 },\n      { ...defaultNodeScanResult, port: 7002, cursor: 33 },\n    ],\n  },\n  {\n    input:\n      '172.17.0.1:7001@-1' +\n      '||172.17.0.1:7002@10' +\n      '||172.17.0.1:7003@-1' +\n      '||172.17.0.1:7004@10' +\n      '||172.17.0.1:7005@-1' +\n      '||172.17.0.1:7006@10',\n    expected: [\n      { ...defaultNodeScanResult, port: 7002, cursor: 10 },\n      { ...defaultNodeScanResult, port: 7004, cursor: 10 },\n      { ...defaultNodeScanResult, port: 7006, cursor: 10 },\n    ],\n  },\n  {\n    input: '172.17.0.1:7001@-1||172.17.0.1:7002@-1',\n    expected: [],\n  },\n  { input: '172.17.0.1:7001@', expected: parsingError },\n  { input: '172.17.0.1:7001@text', expected: parsingError },\n  { input: '172,17,0,1:7001@-1', expected: parsingError },\n  { input: 'plain text', expected: parsingError },\n  { input: 'text@text||text@text', expected: parsingError },\n  { input: 'text@text', expected: parsingError },\n  { input: '', expected: parsingError },\n  { input: '', expected: parsingError },\n];\n\ndescribe('parseClusterCursor', () => {\n  it.each(parseClusterCursorTests)('%j', ({ input, expected }) => {\n    if (expected instanceof Error) {\n      try {\n        parseClusterCursor(input);\n      } catch (e) {\n        expect(e.message).toEqual(expected.message);\n      }\n    } else {\n      expect(parseClusterCursor(input)).toEqual(expected);\n    }\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/clusterCursor.ts",
    "content": "import ERROR_MESSAGES from 'src/constants/error-messages';\nimport { IScannerNodeKeys } from 'src/modules/browser/keys/scanner/scanner.interface';\n\nconst NODES_SEPARATOR = '||';\nconst CURSOR_SEPARATOR = '@';\n// Correct format 172.17.0.1:7001@-1||172.17.0.1:7002@33||:::4554@1423\nconst CLUSTER_CURSOR_REGEX =\n  /^(([a-z0-9.:-])+:[0-9]+(@-?\\d+)(?:\\|{2}(?!$)|$))+$/;\n\nexport const isClusterCursorValid = (cursor: string) =>\n  CLUSTER_CURSOR_REGEX.test(cursor);\n\n/**\n * Parses composed custom cursor from FE and returns nodes\n * Format: 172.17.0.1:7001@22||172.17.0.1:7002@33\n * Also ipv6 is supported :::6379@22:::7001@33\n */\nexport const parseClusterCursor = (cursor: string): IScannerNodeKeys[] => {\n  if (!isClusterCursorValid(cursor)) {\n    throw new Error(ERROR_MESSAGES.INCORRECT_CLUSTER_CURSOR_FORMAT);\n  }\n  const nodeStrings = cursor.split(NODES_SEPARATOR);\n  const nodes = [];\n\n  nodeStrings.forEach((item: string) => {\n    const [address, nextCursor] = item.split(CURSOR_SEPARATOR);\n    const [, host, port] = address.match(/(.+):(\\d+)$/);\n\n    // ignore nodes with cursor -1 (fully scanned)\n    if (parseInt(nextCursor, 10) >= 0) {\n      nodes.push({\n        total: 0,\n        scanned: 0,\n        host,\n        port: parseInt(port, 10),\n        cursor: parseInt(nextCursor, 10),\n        keys: [],\n      });\n    }\n  });\n  return nodes;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/getShards.ts",
    "content": "import {\n  RedisClient,\n  RedisClientConnectionType,\n  RedisClientNodeRole,\n} from 'src/modules/redis/client';\n\n/**\n * Get array of shards (client per each master node).\n * For STANDALONE returns array with a single client.\n */\nexport async function getShards(client: RedisClient): Promise<RedisClient[]> {\n  if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n    return client.nodes(RedisClientNodeRole.PRIMARY);\n  }\n\n  return [client];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/index.ts",
    "content": "export * from './checkKeyExistsing';\nexport * from './clusterCursor';\nexport * from './getShards';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/utils/redisIndexInfo.ts",
    "content": "import { chunk, isArray } from 'lodash';\n\ntype ArrayReplyEntry = string | string[];\nconst errorField = 'Index Errors';\n\nconst infoFieldsToConvert = [\n  'index_options',\n  'index_definition',\n  'gc_stats',\n  'cursor_stats',\n  'dialect_stats',\n  errorField,\n];\n\nexport const convertArrayReplyToObject = (\n  input: ArrayReplyEntry[],\n): { [key: string]: any } => {\n  const obj = {};\n\n  chunk(input, 2).forEach(([key, value]) => {\n    obj[key as string] = value;\n  });\n\n  return obj;\n};\n\nexport const convertIndexInfoAttributeReply = (input: string[]): object => {\n  const attribute = convertArrayReplyToObject(input);\n\n  if (isArray(input)) {\n    attribute['SORTABLE'] = input.includes('SORTABLE') || undefined;\n    attribute['NOINDEX'] = input.includes('NOINDEX') || undefined;\n    attribute['CASESENSITIVE'] = input.includes('CASESENSITIVE') || undefined;\n    attribute['UNF'] = input.includes('UNF') || undefined;\n    attribute['NOSTEM'] = input.includes('NOSTEM') || undefined;\n  }\n\n  return attribute;\n};\n\nexport const convertIndexInfoReply = (input: ArrayReplyEntry[]): object => {\n  const infoReply = convertArrayReplyToObject(input);\n  infoFieldsToConvert.forEach((field) => {\n    infoReply[field] = convertArrayReplyToObject(infoReply[field]);\n  });\n\n  infoReply['attributes'] = infoReply['attributes']?.map?.(\n    convertIndexInfoAttributeReply,\n  );\n  infoReply['field statistics'] = infoReply['field statistics']?.map?.(\n    (sField) => {\n      const convertedField = convertArrayReplyToObject(sField);\n      if (\n        convertedField[errorField] &&\n        Array.isArray(convertedField[errorField])\n      ) {\n        convertedField[errorField] = convertArrayReplyToObject(\n          convertedField[errorField],\n        );\n      }\n      return convertedField;\n    },\n  );\n  return infoReply;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/add.members-to-z-set.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { ZSetMemberDto } from './z-set-member.dto';\n\nexport class AddMembersToZSetDto extends KeyDto {\n  @ApiProperty({\n    description: 'ZSet members',\n    isArray: true,\n    type: ZSetMemberDto,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested()\n  @Type(() => ZSetMemberDto)\n  members: ZSetMemberDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/create.z-set-with-expire.dto.ts",
    "content": "import { IntersectionType } from '@nestjs/swagger';\nimport { KeyWithExpireDto } from 'src/modules/browser/keys/dto';\nimport { AddMembersToZSetDto } from './add.members-to-z-set.dto';\n\nexport class CreateZSetWithExpireDto extends IntersectionType(\n  AddMembersToZSetDto,\n  KeyWithExpireDto,\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/delete.members-from-z-set.dto.ts",
    "content": "import { DeleteMembersFromSetDto } from 'src/modules/browser/set/dto';\n\nexport class DeleteMembersFromZSetDto extends DeleteMembersFromSetDto {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/delete.members-from-z-set.response.ts",
    "content": "import { DeleteMembersFromSetResponse } from 'src/modules/browser/set/dto';\n\nexport class DeleteMembersFromZSetResponse extends DeleteMembersFromSetResponse {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/get.z-set-members.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsInt, IsNotEmpty, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { SortOrder } from 'src/constants';\n\nexport class GetZSetMembersDto extends KeyDto {\n  @ApiProperty({\n    description: 'Specifying the number of elements to skip.',\n    type: Number,\n    minimum: 0,\n    default: '0',\n  })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  @IsNotEmpty()\n  offset: number;\n\n  @ApiProperty({\n    description:\n      'Specifying the number of elements to return from starting at offset.',\n    type: Number,\n    minimum: 1,\n    default: 15,\n  })\n  @IsInt()\n  @Min(1)\n  @Type(() => Number)\n  @IsNotEmpty()\n  count: number;\n\n  @ApiProperty({\n    description:\n      'Get elements sorted by score.' +\n      ' In order to sort the members from the highest to the lowest score, use the DESC value, else ASC value',\n    default: SortOrder.Desc,\n    enum: SortOrder,\n  })\n  @IsNotEmpty()\n  @IsEnum(SortOrder, {\n    message: `sortOrder must be a valid enum value. Valid values: ${Object.values(\n      SortOrder,\n    )}.`,\n  })\n  sortOrder: SortOrder;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/get.z-set.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ZSetMemberDto } from './z-set-member.dto';\n\nexport class GetZSetResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'The number of members in the currently-selected z-set.',\n  })\n  total: number;\n\n  @ApiProperty({\n    description: 'Array of Members.',\n    isArray: true,\n    type: () => ZSetMemberDto,\n  })\n  members: ZSetMemberDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/index.ts",
    "content": "export * from './add.members-to-z-set.dto';\nexport * from './create.z-set-with-expire.dto';\nexport * from './delete.members-from-z-set.dto';\nexport * from './delete.members-from-z-set.response';\nexport * from './get.z-set-members.dto';\nexport * from './get.z-set.response';\nexport * from './search.z-set.response';\nexport * from './search.z-set-members.dto';\nexport * from './search.z-set-members.response';\nexport * from './update.member-in-z-set.dto';\nexport * from './z-set-member.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/search.z-set-members.dto.ts",
    "content": "import { ApiProperty, PickType } from '@nestjs/swagger';\nimport { ScanDataTypeDto } from 'src/modules/browser/keys/dto';\nimport { IsDefined, IsString } from 'class-validator';\n\nexport class SearchZSetMembersDto extends PickType(ScanDataTypeDto, [\n  'keyName',\n  'count',\n  'cursor',\n] as const) {\n  @ApiProperty({\n    description: 'Iterate only elements matching a given pattern.',\n    type: String,\n    default: '*',\n  })\n  @IsDefined()\n  @IsString()\n  match: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/search.z-set-members.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ScanZSetResponse } from './search.z-set.response';\n\nexport class SearchZSetMembersResponse extends ScanZSetResponse {\n  @ApiProperty({\n    type: Number,\n    description: 'The number of members in the currently-selected z-set.',\n  })\n  total: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/search.z-set.response.ts",
    "content": "import { KeyResponse } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { ZSetMemberDto } from './z-set-member.dto';\n\nexport class ScanZSetResponse extends KeyResponse {\n  @ApiProperty({\n    type: Number,\n    minimum: 0,\n    description:\n      'The new cursor to use in the next call.' +\n      ' If the property has value of 0, then the iteration is completed.',\n  })\n  nextCursor: number;\n\n  @ApiProperty({\n    description: 'Array of Members.',\n    isArray: true,\n    type: () => ZSetMemberDto,\n  })\n  members: ZSetMemberDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/update.member-in-z-set.dto.ts",
    "content": "import { KeyDto } from 'src/modules/browser/keys/dto';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined, IsNotEmptyObject, ValidateNested } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { ZSetMemberDto } from './z-set-member.dto';\n\nexport class UpdateMemberInZSetDto extends KeyDto {\n  @ApiProperty({\n    description: 'ZSet member',\n    type: ZSetMemberDto,\n  })\n  @IsDefined()\n  @IsNotEmptyObject()\n  @ValidateNested()\n  @Type(() => ZSetMemberDto)\n  member: ZSetMemberDto;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/dto/z-set-member.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined } from 'class-validator';\nimport {\n  IsRedisString,\n  isZSetScore,\n  RedisStringType,\n} from 'src/common/decorators';\nimport { RedisString } from 'src/common/constants';\n\nexport class ZSetMemberDto {\n  @ApiProperty({\n    type: String,\n    description: 'Member name value.',\n  })\n  @IsDefined()\n  @IsRedisString()\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    description: 'Member score value.',\n    type: Number || String,\n    default: 1,\n  })\n  @IsDefined()\n  @isZSetScore()\n  score: number | 'inf' | '-inf';\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/z-set.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Patch,\n  Post,\n  Put,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { ApiQueryRedisStringEncoding } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  AddMembersToZSetDto,\n  CreateZSetWithExpireDto,\n  DeleteMembersFromZSetDto,\n  DeleteMembersFromZSetResponse,\n  GetZSetMembersDto,\n  GetZSetResponse,\n  SearchZSetMembersDto,\n  SearchZSetMembersResponse,\n  UpdateMemberInZSetDto,\n} from 'src/modules/browser/z-set/dto';\nimport { ZSetService } from 'src/modules/browser/z-set/z-set.service';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { BrowserBaseController } from 'src/modules/browser/browser.base.controller';\n\n@ApiTags('Browser: ZSet')\n@UseInterceptors(BrowserSerializeInterceptor)\n@Controller('zSet')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class ZSetController extends BrowserBaseController {\n  constructor(private zSetService: ZSetService) {\n    super();\n  }\n\n  @Post('')\n  @ApiRedisInstanceOperation({\n    description: 'Set key to hold ZSet data type',\n    statusCode: 201,\n  })\n  @ApiQueryRedisStringEncoding()\n  async createSet(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateZSetWithExpireDto,\n  ): Promise<void> {\n    return await this.zSetService.createZSet(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/get-members')\n  @ApiRedisInstanceOperation({\n    description: 'Get specified members of the ZSet stored at key',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Ok',\n        type: GetZSetResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async getZSet(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: GetZSetMembersDto,\n  ): Promise<GetZSetResponse> {\n    return await this.zSetService.getMembers(clientMetadata, dto);\n  }\n\n  @Put('')\n  @ApiRedisInstanceOperation({\n    description: 'Add the specified members to the ZSet stored at key',\n    statusCode: 200,\n  })\n  @ApiQueryRedisStringEncoding()\n  async addMembers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: AddMembersToZSetDto,\n  ): Promise<void> {\n    return await this.zSetService.addMembers(clientMetadata, dto);\n  }\n\n  @Patch('')\n  @ApiRedisInstanceOperation({\n    description: 'Update the specified member in the ZSet stored at key',\n    statusCode: 200,\n  })\n  @ApiQueryRedisStringEncoding()\n  async updateMember(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: UpdateMemberInZSetDto,\n  ): Promise<void> {\n    return await this.zSetService.updateMember(clientMetadata, dto);\n  }\n\n  @Delete('/members')\n  @ApiRedisInstanceOperation({\n    description: 'Remove the specified members from the Set stored at key',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Ok',\n        type: DeleteMembersFromZSetResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async deleteMembers(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteMembersFromZSetDto,\n  ): Promise<DeleteMembersFromZSetResponse> {\n    return await this.zSetService.deleteMembers(clientMetadata, dto);\n  }\n\n  // The key name can be very large, so it is better to send it in the request body\n  @Post('/search')\n  @ApiRedisInstanceOperation({\n    description: 'Search members in ZSet stored at key',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Ok',\n        type: SearchZSetMembersResponse,\n      },\n    ],\n  })\n  @ApiQueryRedisStringEncoding()\n  async searchZSet(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SearchZSetMembersDto,\n  ): Promise<SearchZSetMembersResponse> {\n    return await this.zSetService.searchMembers(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/z-set.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { ZSetService } from 'src/modules/browser/z-set/z-set.service';\nimport { ZSetController } from 'src/modules/browser/z-set/z-set.controller';\n\n@Module({})\nexport class ZSetModule {\n  static register({ route }): DynamicModule {\n    return {\n      module: ZSetModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: ZSetModule,\n          },\n        ]),\n      ],\n      controllers: [ZSetController],\n      providers: [ZSetService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/z-set.service.spec.ts",
    "content": "import {\n  BadRequestException,\n  ConflictException,\n  ForbiddenException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { SortOrder } from 'src/constants/sort';\nimport { ReplyError } from 'src/models';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport {\n  mockBrowserClientMetadata,\n  mockRedisNoPermError,\n  mockRedisWrongTypeError,\n  mockDatabaseRecommendationService,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolZSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport {\n  getZSetMembersInAscResponse,\n  getZSetMembersInDescResponse,\n  mockAddMembersDto,\n  mockDeleteMembersDto,\n  mockGetMembersDto,\n  mockMembersForZAddCommand,\n  mockSearchMembersDto,\n  mockSearchZSetMembersResponse,\n  mockUpdateMemberDto,\n} from 'src/modules/browser/__mocks__';\nimport {\n  CreateZSetWithExpireDto,\n  SearchZSetMembersDto,\n} from 'src/modules/browser/z-set/dto';\nimport { ZSetService } from 'src/modules/browser/z-set/z-set.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('ZSetService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: ZSetService;\n  let recommendationService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ZSetService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<ZSetService>(ZSetService);\n    recommendationService = module.get<DatabaseRecommendationService>(\n      DatabaseRecommendationService,\n    );\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('createZSet', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddMembersDto.keyName])\n        .mockResolvedValue(0);\n      service.createZSetWithExpiration = jest.fn();\n    });\n    it('create zset with expiration', async () => {\n      service.createZSetWithExpiration = jest\n        .fn()\n        .mockResolvedValue(mockAddMembersDto.members.length);\n\n      await expect(\n        service.createZSet(mockBrowserClientMetadata, {\n          ...mockAddMembersDto,\n          expire: 1000,\n        }),\n      ).resolves.not.toThrow();\n      expect(service.createZSetWithExpiration).toHaveBeenCalled();\n    });\n    it('create zset without expiration', async () => {\n      const { keyName } = mockAddMembersDto;\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZAdd,\n          keyName,\n          ...mockMembersForZAddCommand,\n        ])\n        .mockResolvedValue(mockAddMembersDto.members.length);\n\n      await expect(\n        service.createZSet(mockBrowserClientMetadata, mockAddMembersDto),\n      ).resolves.not.toThrow();\n      expect(service.createZSetWithExpiration).not.toHaveBeenCalled();\n    });\n    it('key with this name exist', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddMembersDto.keyName])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.createZSet(mockBrowserClientMetadata, mockAddMembersDto),\n      ).rejects.toThrow(ConflictException);\n      expect(client.sendCommand).toHaveBeenCalledTimes(1);\n      expect(client.sendPipeline).not.toHaveBeenCalled();\n    });\n    it(\"try to use 'ZADD' command not for zset data type for createZSet\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZADD',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZAdd,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.createZSet(mockBrowserClientMetadata, mockAddMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for createZSet\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'ZADD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.createZSet(mockBrowserClientMetadata, mockAddMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('createZSetWithExpiration', () => {\n    const dto: CreateZSetWithExpireDto = {\n      ...mockAddMembersDto,\n      expire: 1000,\n    };\n    it('succeed to create ZSet data type with expiration', async () => {\n      when(client.sendPipeline)\n        .calledWith(expect.arrayContaining([expect.anything()]))\n        .mockResolvedValue([\n          [null, mockAddMembersDto.members.length],\n          [null, 1],\n        ]);\n\n      const result = await service.createZSetWithExpiration(client, dto);\n      expect(result).toBe(mockAddMembersDto.members.length);\n    });\n    it('throw transaction error', async () => {\n      const transactionError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZADD',\n      };\n      client.sendPipeline.mockResolvedValue([[transactionError, null]]);\n\n      await expect(\n        service.createZSetWithExpiration(client, dto),\n      ).rejects.toEqual(transactionError);\n    });\n  });\n\n  describe('getMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolZSetCommands.ZCard, mockGetMembersDto.keyName])\n        .mockResolvedValue(mockAddMembersDto.members.length);\n    });\n    it('get members sorted in asc', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZRange,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue([\n          'member1',\n          '-inf',\n          'member2',\n          '0',\n          'member3',\n          '2',\n          'member4',\n          'inf',\n        ]);\n\n      const result = await service.getMembers(\n        mockBrowserClientMetadata,\n        mockGetMembersDto,\n      );\n      await expect(result).toEqual(getZSetMembersInAscResponse);\n    });\n    it('get members sorted in desc', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZRevRange,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue([\n          'member4',\n          'inf',\n          'member3',\n          '2',\n          'member2',\n          '0',\n          'member1',\n          '-inf',\n        ]);\n\n      const result = await service.getMembers(mockBrowserClientMetadata, {\n        ...mockGetMembersDto,\n        sortOrder: SortOrder.Desc,\n      });\n      await expect(result).toEqual(getZSetMembersInDescResponse);\n    });\n    it('should call recommendationService', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZRevRange,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue([\n          'member4',\n          'inf',\n          'member3',\n          '2',\n          'member2',\n          '0',\n          'member1',\n          '-inf',\n        ]);\n\n      const result = await service.getMembers(mockBrowserClientMetadata, {\n        ...mockGetMembersDto,\n        sortOrder: SortOrder.Desc,\n      });\n\n      expect(recommendationService.check).toBeCalledWith(\n        mockBrowserClientMetadata,\n        RECOMMENDATION_NAMES.RTS,\n        { members: result.members, keyName: result.keyName },\n      );\n    });\n    it('key with this name does not exist for getMembers', async () => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolZSetCommands.ZCard, mockGetMembersDto.keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.getMembers(mockBrowserClientMetadata, mockGetMembersDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).toHaveBeenCalledTimes(1);\n    });\n    it(\"try to use 'ZCARD' command not for zset data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZCARD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getMembers(mockBrowserClientMetadata, mockGetMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for getMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'ZCARD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.getMembers(mockBrowserClientMetadata, mockGetMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('addMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddMembersDto.keyName])\n        .mockResolvedValue(1);\n    });\n    it('succeed to add members to the ZSet data type', async () => {\n      const { keyName } = mockAddMembersDto;\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZAdd,\n          keyName,\n          ...mockMembersForZAddCommand,\n        ])\n        .mockResolvedValue(mockAddMembersDto.members.length);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersDto),\n      ).resolves.not.toThrow();\n    });\n    it('key with this name does not exist for addMembers', async () => {\n      const { keyName } = mockAddMembersDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolZSetCommands.ZAdd,\n        expect.anything(),\n      ]);\n    });\n    it(\"try to use 'ZADD' command not for zset data type for addMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZADD',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZAdd,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for addMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'ZADD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.addMembers(mockBrowserClientMetadata, mockAddMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('updateMember', () => {\n    beforeEach(() =>\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, mockAddMembersDto.keyName])\n        .mockResolvedValue(1),\n    );\n\n    it('succeed to update member in key', async () => {\n      const { keyName, member } = mockUpdateMemberDto;\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZAdd,\n          keyName,\n          'XX',\n          'CH',\n          `${member.score}`,\n          member.name,\n        ])\n        .mockResolvedValue(1);\n\n      await expect(\n        service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto),\n      ).resolves.not.toThrow();\n    });\n    it('key with this name does not exist for updateMember', async () => {\n      const { keyName } = mockUpdateMemberDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolZSetCommands.ZAdd,\n        expect.anything(),\n      ]);\n    });\n    it('member does not exist in key', async () => {\n      const { keyName, member } = mockUpdateMemberDto;\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZAdd,\n          keyName,\n          'XX',\n          'CH',\n          `${member.score}`,\n          member.name,\n        ])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it(\"try to use 'ZADD' command not for zset data type for updateMember\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZADD',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZAdd,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for updateMember\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'ZADD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('deleteMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolKeysCommands.Exists,\n          mockDeleteMembersDto.keyName,\n        ])\n        .mockResolvedValue(1);\n    });\n    it('succeeded to delete members from ZSet data type', async () => {\n      const { members, keyName } = mockDeleteMembersDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolZSetCommands.ZRem, keyName, ...members])\n        .mockResolvedValue(members.length);\n\n      const result = await service.deleteMembers(\n        mockBrowserClientMetadata,\n        mockDeleteMembersDto,\n      );\n\n      expect(result).toEqual({ affected: members.length });\n    });\n    it('key with this name does not exist for deleteMembers', async () => {\n      const { members, keyName } = mockDeleteMembersDto;\n      when(client.sendCommand)\n        .calledWith([BrowserToolKeysCommands.Exists, keyName])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolZSetCommands.ZRem,\n        keyName,\n        ...members,\n      ]);\n    });\n    it(\"try to use 'ZREM' command not for set data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZREM',\n      };\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZRem,\n            expect.anything(),\n          ]),\n        )\n        .mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for deleteMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'ZREM',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('searchMembers', () => {\n    beforeEach(() => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZCard,\n          mockSearchMembersDto.keyName,\n        ])\n        .mockResolvedValue(mockAddMembersDto.members.length);\n    });\n    it('succeeded to search members in ZSet data type', async () => {\n      when(client.sendCommand)\n        .calledWith(\n          expect.arrayContaining([\n            BrowserToolZSetCommands.ZScan,\n            expect.anything(),\n          ]),\n        )\n        .mockResolvedValue([\n          0,\n          ['member1', '-inf', 'member2', '0', 'member3', '2', 'member4', 'inf'],\n        ]);\n\n      const result = await service.searchMembers(\n        mockBrowserClientMetadata,\n        mockSearchMembersDto,\n      );\n      await expect(result).toEqual(mockSearchZSetMembersResponse);\n      expect(client.sendCommand).toHaveBeenCalledWith(\n        expect.arrayContaining([\n          BrowserToolZSetCommands.ZScan,\n          expect.anything(),\n        ]),\n      );\n    });\n    it('succeed to find exact member in the z-set', async () => {\n      const item = { name: Buffer.from('member'), score: 2 };\n      const dto: SearchZSetMembersDto = {\n        ...mockSearchMembersDto,\n        match: item.name.toString(),\n      };\n      when(client.sendCommand)\n        .calledWith([BrowserToolZSetCommands.ZScore, dto.keyName, dto.match])\n        .mockResolvedValue(item.score);\n\n      const result = await service.searchMembers(\n        mockBrowserClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual({\n        ...mockSearchZSetMembersResponse,\n        members: [item],\n      });\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolZSetCommands.ZScan,\n        expect.anything(),\n      ]);\n    });\n    it('failed to find exact member in the set', async () => {\n      const dto: SearchZSetMembersDto = {\n        ...mockSearchMembersDto,\n        match: 'member',\n      };\n      when(client.sendCommand)\n        .calledWith([BrowserToolZSetCommands.ZScore, dto.keyName, dto.match])\n        .mockResolvedValue(null);\n\n      const result = await service.searchMembers(\n        mockBrowserClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual({ ...mockSearchZSetMembersResponse, members: [] });\n    });\n    it('should not call scan when math contains escaped glob', async () => {\n      const mockMatch = 'm\\\\[a-e\\\\]mber';\n      const mockSpecialMember = Buffer.from('m[a-e]mber');\n      const dto: SearchZSetMembersDto = {\n        ...mockSearchMembersDto,\n        match: mockMatch,\n      };\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZScore,\n          dto.keyName,\n          mockSpecialMember.toString(),\n        ])\n        .mockResolvedValue(1);\n\n      const result = await service.searchMembers(\n        mockBrowserClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual({\n        ...mockSearchZSetMembersResponse,\n        members: [{ name: mockSpecialMember, score: 1 }],\n      });\n      expect(client.sendCommand).not.toHaveBeenCalledWith([\n        BrowserToolZSetCommands.ZScan,\n        expect.anything(),\n      ]);\n    });\n    // TODO: uncomment after enabling threshold for z-set scan\n    // it('should stop z-set full scan', async () => {\n    //   const dto: SearchZSetMembersDto = {\n    //     ...mockSearchMembersDto,\n    //     count: REDIS_SCAN_CONFIG.countDefault,\n    //     match: '*un-exist-member*',\n    //   };\n    //   const maxScanCalls = Math.round(\n    //     REDIS_SCAN_CONFIG.countThreshold / REDIS_SCAN_CONFIG.countDefault,\n    //   );\n    //   when(browserTool.execCommand)\n    //     .calledWith(\n    //       mockBrowserClientMetadata,\n    //       BrowserToolZSetCommands.ZScan,\n    //       expect.anything(),\n    //     )\n    //     .mockResolvedValue(['200', []]);\n    //\n    //   await service.searchMembers(mockBrowserClientMetadata, dto);\n    //\n    //   expect(browserTool.execCommand).toHaveBeenCalledTimes(maxScanCalls + 1);\n    // });\n    it('key with this name does not exist for searchMembers', async () => {\n      when(client.sendCommand)\n        .calledWith([\n          BrowserToolZSetCommands.ZCard,\n          mockSearchMembersDto.keyName,\n        ])\n        .mockResolvedValue(0);\n\n      await expect(\n        service.searchMembers(mockBrowserClientMetadata, mockSearchMembersDto),\n      ).rejects.toThrow(NotFoundException);\n      expect(client.sendCommand).toHaveBeenCalledTimes(1);\n    });\n    it(\"try to use 'ZCARD' command not for zset data type\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        command: 'ZCARD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.searchMembers(mockBrowserClientMetadata, mockSearchMembersDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions for searchMembers\", async () => {\n      const replyError: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'ZCARD',\n      };\n      client.sendCommand.mockRejectedValue(replyError);\n\n      await expect(\n        service.searchMembers(mockBrowserClientMetadata, mockSearchMembersDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/browser/z-set/z-set.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { isNull, isNaN } from 'lodash';\nimport config from 'src/utils/config';\nimport {\n  catchAclError,\n  catchMultiTransactionError,\n  isRedisGlob,\n  unescapeRedisGlob,\n} from 'src/utils';\nimport { SortOrder } from 'src/constants/sort';\nimport { RedisErrorCodes, RECOMMENDATION_NAMES } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ReplyError } from 'src/models';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport {\n  BrowserToolKeysCommands,\n  BrowserToolZSetCommands,\n} from 'src/modules/browser/constants/browser-tool-commands';\nimport { plainToInstance } from 'class-transformer';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  AddMembersToZSetDto,\n  CreateZSetWithExpireDto,\n  DeleteMembersFromZSetDto,\n  DeleteMembersFromZSetResponse,\n  GetZSetMembersDto,\n  GetZSetResponse,\n  ScanZSetResponse,\n  SearchZSetMembersDto,\n  SearchZSetMembersResponse,\n  UpdateMemberInZSetDto,\n  ZSetMemberDto,\n} from 'src/modules/browser/z-set/dto';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  checkIfKeyExists,\n  checkIfKeyNotExists,\n} from 'src/modules/browser/utils';\nimport { RedisClient } from 'src/modules/redis/client';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\n\n@Injectable()\nexport class ZSetService {\n  private logger = new Logger('ZSetService');\n\n  constructor(\n    private databaseClientFactory: DatabaseClientFactory,\n    private recommendationService: DatabaseRecommendationService,\n  ) {}\n\n  public async createZSet(\n    clientMetadata: ClientMetadata,\n    dto: CreateZSetWithExpireDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Creating ZSet data type.', clientMetadata);\n      const { keyName, expire } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyExists(keyName, client);\n\n      if (expire) {\n        await this.createZSetWithExpiration(client, dto);\n      } else {\n        await this.createSimpleZSet(client, dto);\n      }\n\n      this.logger.debug('Succeed to create ZSet data type.', clientMetadata);\n      return null;\n    } catch (error) {\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      this.logger.error(\n        'Failed to create ZSet data type.',\n        error,\n        clientMetadata,\n      );\n      throw catchAclError(error);\n    }\n  }\n\n  public async getMembers(\n    clientMetadata: ClientMetadata,\n    getZSetDto: GetZSetMembersDto,\n  ): Promise<GetZSetResponse> {\n    try {\n      this.logger.debug(\n        'Getting members of the ZSet data type stored at key.',\n        clientMetadata,\n      );\n      const { keyName, sortOrder } = getZSetDto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const total = await client.sendCommand([\n        BrowserToolZSetCommands.ZCard,\n        keyName,\n      ]);\n      if (!total) {\n        this.logger.error(\n          `Failed to get members of the ZSet data type. Not Found key: ${keyName}.`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n\n      let members: ZSetMemberDto[] = [];\n      if (sortOrder && sortOrder === SortOrder.Asc) {\n        members = await this.getZRange(client, getZSetDto);\n      } else {\n        members = await this.getZRevRange(client, getZSetDto);\n      }\n\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.RTS,\n        { members, keyName },\n      );\n\n      this.logger.debug(\n        'Succeed to get members of the ZSet data type.',\n        clientMetadata,\n      );\n      return plainToInstance(GetZSetResponse, {\n        keyName,\n        total,\n        members,\n      });\n    } catch (error) {\n      this.logger.error(\n        'Failed to get members of the ZSet data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async addMembers(\n    clientMetadata: ClientMetadata,\n    dto: AddMembersToZSetDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug(\n        'Adding members to the ZSet data type.',\n        clientMetadata,\n      );\n      const { keyName, members } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const args = this.formatMembersDtoToCommandArgs(members);\n      await client.sendCommand([\n        BrowserToolZSetCommands.ZAdd,\n        keyName,\n        ...args,\n      ]);\n\n      this.logger.debug(\n        'Succeed to add members to ZSet data type.',\n        clientMetadata,\n      );\n      return null;\n    } catch (error) {\n      this.logger.error(\n        'Failed to add members to Set data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async updateMember(\n    clientMetadata: ClientMetadata,\n    dto: UpdateMemberInZSetDto,\n  ): Promise<void> {\n    try {\n      this.logger.debug('Updating member in ZSet data type.', clientMetadata);\n      const { keyName, member } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = await client.sendCommand([\n        BrowserToolZSetCommands.ZAdd,\n        keyName,\n        'XX',\n        'CH',\n        `${member.score}`,\n        member.name,\n      ]);\n      if (!result) {\n        this.logger.error(\n          `Failed to update member in ZSet data type. ${ERROR_MESSAGES.MEMBER_IN_SET_NOT_EXIST}`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.MEMBER_IN_SET_NOT_EXIST),\n        );\n      }\n\n      this.logger.debug(\n        'Succeed to update member in ZSet data type.',\n        clientMetadata,\n      );\n      return null;\n    } catch (error) {\n      this.logger.error(\n        'Failed to update member in ZSet data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async deleteMembers(\n    clientMetadata: ClientMetadata,\n    dto: DeleteMembersFromZSetDto,\n  ): Promise<DeleteMembersFromZSetResponse> {\n    try {\n      this.logger.debug(\n        'Deleting members from the ZSet data type.',\n        clientMetadata,\n      );\n      const { keyName, members } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      await checkIfKeyNotExists(keyName, client);\n\n      const result = (await client.sendCommand([\n        BrowserToolZSetCommands.ZRem,\n        keyName,\n        ...members,\n      ])) as number;\n\n      this.logger.debug(\n        'Succeed to delete members from the ZSet data type.',\n        clientMetadata,\n      );\n      return { affected: result };\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete members from the ZSet data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async searchMembers(\n    clientMetadata: ClientMetadata,\n    dto: SearchZSetMembersDto,\n  ): Promise<SearchZSetMembersResponse> {\n    try {\n      this.logger.debug(\n        'Search members of the ZSet data type stored at key.',\n        clientMetadata,\n      );\n      const { keyName, cursor, match } = dto;\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      let result: SearchZSetMembersResponse = {\n        keyName,\n        total: 0,\n        members: [],\n        nextCursor: cursor,\n      };\n\n      result.total = (await client.sendCommand([\n        BrowserToolZSetCommands.ZCard,\n        keyName,\n      ])) as number;\n      if (!result.total) {\n        this.logger.error(\n          `Failed to search members of the ZSet data type. Not Found key: ${keyName}.`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST),\n        );\n      }\n      if (match && !isRedisGlob(match)) {\n        const member = unescapeRedisGlob(match);\n        result.nextCursor = 0;\n        const score = (await client.sendCommand([\n          BrowserToolZSetCommands.ZScore,\n          keyName,\n          member,\n        ])) as string;\n        const formattedScore = isNaN(parseFloat(score))\n          ? String(score)\n          : parseFloat(score);\n\n        if (!isNull(score)) {\n          result.members.push(\n            plainToInstance(ZSetMemberDto, {\n              name: member,\n              score: formattedScore,\n            }),\n          );\n        }\n      } else {\n        const scanResult = await this.scanZSet(client, dto);\n        result = { ...result, ...scanResult };\n      }\n\n      this.logger.debug(\n        'Succeed to search members of the ZSet data type.',\n        clientMetadata,\n      );\n      return plainToInstance(SearchZSetMembersResponse, result);\n    } catch (error) {\n      this.logger.error(\n        'Failed to search members of the ZSet data type.',\n        error,\n        clientMetadata,\n      );\n      if (error?.message.includes(RedisErrorCodes.WrongType)) {\n        throw new BadRequestException(error.message);\n      }\n      throw catchAclError(error);\n    }\n  }\n\n  public async getZRange(\n    client: RedisClient,\n    getZSetDto: GetZSetMembersDto,\n  ): Promise<ZSetMemberDto[]> {\n    const { keyName, offset, count } = getZSetDto;\n\n    const execResult = (await client.sendCommand([\n      BrowserToolZSetCommands.ZRange,\n      keyName,\n      offset,\n      offset + count - 1,\n      'WITHSCORES',\n    ])) as string[];\n\n    return this.formatZRangeWithScoresReply(execResult);\n  }\n\n  public async getZRevRange(\n    client: RedisClient,\n    getZSetDto: GetZSetMembersDto,\n  ): Promise<ZSetMemberDto[]> {\n    const { keyName, offset, count } = getZSetDto;\n\n    const execResult = (await client.sendCommand([\n      BrowserToolZSetCommands.ZRevRange,\n      keyName,\n      offset,\n      offset + count - 1,\n      'WITHSCORES',\n    ])) as string[];\n\n    return this.formatZRangeWithScoresReply(execResult);\n  }\n\n  public async createSimpleZSet(\n    client: RedisClient,\n    dto: CreateZSetWithExpireDto,\n  ): Promise<number> {\n    const { keyName, members } = dto;\n    const args = this.formatMembersDtoToCommandArgs(members);\n\n    return (await client.sendCommand([\n      BrowserToolZSetCommands.ZAdd,\n      keyName,\n      ...args,\n    ])) as number;\n  }\n\n  public async createZSetWithExpiration(\n    client: RedisClient,\n    dto: CreateZSetWithExpireDto,\n  ): Promise<number> {\n    const { keyName, members, expire } = dto;\n\n    const args = this.formatMembersDtoToCommandArgs(members);\n    const transactionResults = await client.sendPipeline([\n      [BrowserToolZSetCommands.ZAdd, keyName, ...args],\n      [BrowserToolKeysCommands.Expire, keyName, expire],\n    ]);\n    catchMultiTransactionError(transactionResults);\n\n    const execResult = transactionResults.map(\n      (item: [ReplyError, any]) => item[1],\n    );\n    const [added] = execResult;\n    return added;\n  }\n\n  public async scanZSet(\n    client: RedisClient,\n    dto: SearchZSetMembersDto,\n  ): Promise<ScanZSetResponse> {\n    const { keyName, cursor } = dto;\n    const count = dto.count || REDIS_SCAN_CONFIG.countDefault;\n    const match = dto.match !== undefined ? dto.match : '*';\n    let result: ScanZSetResponse = {\n      keyName,\n      nextCursor: null,\n      members: [],\n    };\n    while (result.nextCursor !== 0 && result.members.length < count) {\n      const scanResult = await client.sendCommand([\n        BrowserToolZSetCommands.ZScan,\n        keyName,\n        `${result.nextCursor || cursor}`,\n        'MATCH',\n        match,\n        'COUNT',\n        count,\n      ]);\n      const nextCursor = scanResult[0];\n      const membersArray = scanResult[1];\n      const members: ZSetMemberDto[] =\n        this.formatZRangeWithScoresReply(membersArray);\n      result = {\n        ...result,\n        nextCursor: parseInt(nextCursor, 10),\n        members: [...result.members, ...members],\n      };\n    }\n    return result;\n  }\n\n  private formatZRangeWithScoresReply(reply: string[]): ZSetMemberDto[] {\n    const result: ZSetMemberDto[] = [];\n    while (reply.length) {\n      const member = reply.splice(0, 2);\n      const score = isNaN(parseFloat(member[1]))\n        ? String(member[1])\n        : parseFloat(member[1]);\n      result.push(\n        plainToInstance(ZSetMemberDto, {\n          name: member[0],\n          score,\n        }),\n      );\n    }\n\n    return result;\n  }\n\n  private formatMembersDtoToCommandArgs(\n    members: ZSetMemberDto[],\n  ): (string | Buffer)[] {\n    return members.reduce<(string | Buffer)[]>(\n      (prev: string[], cur: ZSetMemberDto) => [\n        ...prev,\n        ...[`${cur.score}`, cur.name],\n      ],\n      [],\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockBulkActionOverview,\n  mockRedisNoAuthError,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { TelemetryEvents } from 'src/constants';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\n\ndescribe('BulkActionsAnalytics', () => {\n  let service: BulkActionsAnalytics;\n  let sendEventSpy;\n  let sendFailedEventSpy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [BulkActionsAnalytics, EventEmitter2],\n    }).compile();\n\n    service = await module.get(BulkActionsAnalytics);\n    sendEventSpy = jest.spyOn(service as any, 'sendEvent');\n    sendFailedEventSpy = jest.spyOn(service as any, 'sendFailedEvent');\n  });\n\n  describe('sendActionStarted', () => {\n    it('should emit event when action started (without summary)', () => {\n      service.sendActionStarted(mockSessionMetadata, mockBulkActionOverview);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsStarted,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: '*',\n            type: mockBulkActionOverview.filter?.type,\n          },\n          progress: {\n            scanned: mockBulkActionOverview.progress.scanned,\n            scannedRange: '0 - 5 000',\n            total: mockBulkActionOverview.progress.total,\n            totalRange: '0 - 5 000',\n          },\n        },\n      );\n    });\n    it('should emit event when action started without progress data and filter as \"PATTERN\"', () => {\n      service.sendActionStarted(mockSessionMetadata, {\n        ...mockBulkActionOverview,\n        filter: { match: 'some query', type: null },\n        progress: undefined,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsStarted,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: 'PATTERN',\n            type: mockBulkActionOverview.filter?.type,\n          },\n          progress: {},\n        },\n      );\n    });\n    it('should emit event when action started without progress and filter', () => {\n      service.sendActionStarted(mockSessionMetadata, {\n        ...mockBulkActionOverview,\n        filter: undefined,\n        progress: undefined,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsStarted,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: 'PATTERN', // todo: is this expected behavior?\n          },\n          progress: {},\n        },\n      );\n    });\n    it('should not emit event in case of an error and should not fail', () => {\n      service.sendActionStarted(mockSessionMetadata, undefined);\n      expect(sendEventSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendActionStopped', () => {\n    it('should emit event when action paused/stopped', () => {\n      service.sendActionStopped(mockSessionMetadata, mockBulkActionOverview);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsStopped,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: '*',\n            type: mockBulkActionOverview.filter.type,\n          },\n          progress: {\n            scanned: mockBulkActionOverview.progress.scanned,\n            scannedRange: '0 - 5 000',\n            total: mockBulkActionOverview.progress.total,\n            totalRange: '0 - 5 000',\n          },\n          summary: {\n            processed: mockBulkActionOverview.summary.processed,\n            processedRange: '0 - 5 000',\n            succeed: mockBulkActionOverview.summary.succeed,\n            succeedRange: '0 - 5 000',\n            failed: mockBulkActionOverview.summary.failed,\n            failedRange: '0 - 5 000',\n          },\n        },\n      );\n    });\n    it('should emit event when action paused/stopped without progress, filter and summary', () => {\n      service.sendActionStopped(mockSessionMetadata, {\n        ...mockBulkActionOverview,\n        filter: undefined,\n        progress: undefined,\n        summary: undefined,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsStopped,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: 'PATTERN', // todo: is this expected behavior?\n          },\n          progress: {},\n          summary: {},\n        },\n      );\n    });\n    it('should not emit event in case of an error and should not fail', () => {\n      service.sendActionStopped(mockSessionMetadata, undefined);\n      expect(sendEventSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendActionSucceed', () => {\n    it('should emit event when action succeed (without progress)', () => {\n      service.sendActionSucceed(mockSessionMetadata, mockBulkActionOverview);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsSucceed,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: '*',\n            type: mockBulkActionOverview.filter.type,\n          },\n          summary: {\n            processed: mockBulkActionOverview.summary.processed,\n            processedRange: '0 - 5 000',\n            succeed: mockBulkActionOverview.summary.succeed,\n            succeedRange: '0 - 5 000',\n            failed: mockBulkActionOverview.summary.failed,\n            failedRange: '0 - 5 000',\n          },\n        },\n      );\n    });\n    it('should emit event when action succeed without filter and summary', () => {\n      service.sendActionSucceed(mockSessionMetadata, {\n        ...mockBulkActionOverview,\n        filter: undefined,\n        summary: undefined,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsSucceed,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          filter: {\n            match: 'PATTERN', // todo: is this expected behavior?\n          },\n          summary: {},\n        },\n      );\n    });\n    it('should not emit event in case of an error and should not fail', () => {\n      service.sendActionSucceed(mockSessionMetadata, undefined);\n      expect(sendEventSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendActionFailed', () => {\n    it('should emit event when action failed (without progress)', () => {\n      service.sendActionFailed(\n        mockSessionMetadata,\n        mockBulkActionOverview,\n        mockRedisNoAuthError,\n      );\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.BulkActionsFailed,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          error: mockRedisNoAuthError,\n        },\n      );\n    });\n    it('should not emit event in case of an error and should not fail', () => {\n      service.sendActionFailed(mockSessionMetadata, undefined, undefined);\n      expect(sendFailedEventSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendImportSamplesUploaded', () => {\n    it('should emit event when action succeed (without progress)', () => {\n      service.sendImportSamplesUploaded(\n        mockSessionMetadata,\n        mockBulkActionOverview,\n      );\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.ImportSamplesUploaded,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          summary: {\n            processed: mockBulkActionOverview.summary.processed,\n            processedRange: '0 - 5 000',\n            succeed: mockBulkActionOverview.summary.succeed,\n            succeedRange: '0 - 5 000',\n            failed: mockBulkActionOverview.summary.failed,\n            failedRange: '0 - 5 000',\n          },\n        },\n      );\n    });\n    it('should emit event when action succeed without filter and summary', () => {\n      service.sendImportSamplesUploaded(mockSessionMetadata, {\n        ...mockBulkActionOverview,\n        filter: undefined,\n        summary: undefined,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.ImportSamplesUploaded,\n        {\n          databaseId: mockBulkActionOverview.databaseId,\n          action: mockBulkActionOverview.type,\n          duration: mockBulkActionOverview.duration,\n          summary: {},\n        },\n      );\n    });\n    it('should not emit event in case of an error and should not fail', () => {\n      service.sendImportSamplesUploaded(mockSessionMetadata, undefined);\n      expect(sendEventSpy).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { getRangeForNumber, BULK_ACTIONS_BREAKPOINTS } from 'src/utils';\nimport { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class BulkActionsAnalytics extends TelemetryBaseService {\n  sendActionStarted(\n    sessionMetadata: SessionMetadata,\n    overview: IBulkActionOverview,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.BulkActionsStarted, {\n        databaseId: overview.databaseId,\n        action: overview.type,\n        duration: overview.duration,\n        filter: {\n          match: overview.filter?.match === '*' ? '*' : 'PATTERN',\n          type: overview.filter?.type,\n        },\n        progress: {\n          scanned: overview.progress?.scanned,\n          scannedRange: getRangeForNumber(\n            overview.progress?.scanned,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          total: overview.progress?.total,\n          totalRange: getRangeForNumber(\n            overview.progress?.total,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n        },\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendActionStopped(\n    sessionMetadata: SessionMetadata,\n    overview: IBulkActionOverview,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.BulkActionsStopped, {\n        databaseId: overview.databaseId,\n        action: overview.type,\n        duration: overview.duration,\n        filter: {\n          match: overview.filter?.match === '*' ? '*' : 'PATTERN',\n          type: overview.filter?.type,\n        },\n        progress: {\n          scanned: overview.progress?.scanned,\n          scannedRange: getRangeForNumber(\n            overview.progress?.scanned,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          total: overview.progress?.total,\n          totalRange: getRangeForNumber(\n            overview.progress?.total,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n        },\n        summary: {\n          processed: overview.summary?.processed,\n          processedRange: getRangeForNumber(\n            overview.summary?.processed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          succeed: overview.summary?.succeed,\n          succeedRange: getRangeForNumber(\n            overview.summary?.succeed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          failed: overview.summary?.failed,\n          failedRange: getRangeForNumber(\n            overview.summary?.failed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n        },\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendActionSucceed(\n    sessionMetadata: SessionMetadata,\n    overview: IBulkActionOverview,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.BulkActionsSucceed, {\n        databaseId: overview.databaseId,\n        action: overview.type,\n        duration: overview.duration,\n        filter: {\n          match: overview.filter?.match === '*' ? '*' : 'PATTERN',\n          type: overview.filter?.type,\n        },\n        summary: {\n          processed: overview.summary?.processed,\n          processedRange: getRangeForNumber(\n            overview.summary?.processed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          succeed: overview.summary?.succeed,\n          succeedRange: getRangeForNumber(\n            overview.summary?.succeed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          failed: overview.summary?.failed,\n          failedRange: getRangeForNumber(\n            overview.summary?.failed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n        },\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendActionFailed(\n    sessionMetadata: SessionMetadata,\n    overview: IBulkActionOverview,\n    error: HttpException | Error,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.BulkActionsFailed, {\n        databaseId: overview.databaseId,\n        action: overview.type,\n        error,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendImportSamplesUploaded(\n    sessionMetadata: SessionMetadata,\n    overview: IBulkActionOverview,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.ImportSamplesUploaded, {\n        databaseId: overview.databaseId,\n        action: overview.type,\n        duration: overview.duration,\n        summary: {\n          processed: overview.summary?.processed,\n          processedRange: getRangeForNumber(\n            overview.summary?.processed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          succeed: overview.summary?.succeed,\n          succeedRange: getRangeForNumber(\n            overview.summary?.succeed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n          failed: overview.summary?.failed,\n          failedRange: getRangeForNumber(\n            overview.summary?.failed,\n            BULK_ACTIONS_BREAKPOINTS,\n          ),\n        },\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.controller.ts",
    "content": "import { Response } from 'express';\nimport {\n  Controller,\n  Get,\n  Param,\n  Res,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiParam, ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { BulkActionsService } from 'src/modules/bulk-actions/bulk-actions.service';\nimport { BulkActionIdDto } from 'src/modules/bulk-actions/dto/bulk-action-id.dto';\n\n@UsePipes(new ValidationPipe({ transform: true }))\n@ApiTags('Bulk Actions')\n@Controller('bulk-actions')\nexport class BulkActionsController {\n  constructor(private readonly service: BulkActionsService) {}\n\n  @ApiEndpoint({\n    description: 'Stream bulk action report as downloadable file',\n    statusCode: 200,\n  })\n  @ApiParam({\n    name: 'id',\n    description: 'Bulk action id',\n    type: String,\n  })\n  @Get(':id/report/download')\n  async downloadReport(\n    @Param() { id }: BulkActionIdDto,\n    @Res() res: Response,\n  ): Promise<void> {\n    await this.service.streamReport(id, res);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.gateway.ts",
    "content": "import { Socket, Server } from 'socket.io';\nimport {\n  ConnectedSocket,\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n  SubscribeMessage,\n  WebSocketGateway,\n  WebSocketServer,\n} from '@nestjs/websockets';\nimport {\n  Body,\n  Logger,\n  UseFilters,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { BulkActionsServerEvents } from 'src/modules/bulk-actions/constants';\nimport { CreateBulkActionDto } from 'src/modules/bulk-actions/dto/create-bulk-action.dto';\nimport { BulkActionsService } from 'src/modules/bulk-actions/bulk-actions.service';\nimport { AckWsExceptionFilter } from 'src/modules/pub-sub/filters/ack-ws-exception.filter';\nimport { BulkActionIdDto } from 'src/modules/bulk-actions/dto/bulk-action-id.dto';\nimport { SessionMetadata } from 'src/common/models';\nimport { WSSessionMetadata } from 'src/modules/auth/session-metadata/decorators/ws-session-metadata.decorator';\n\nconst SOCKETS_CONFIG = config.get('sockets') as Config['sockets'];\n\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseFilters(AckWsExceptionFilter)\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  namespace: 'bulk-actions',\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n  serveClient: SOCKETS_CONFIG.serveClient,\n})\nexport class BulkActionsGateway\n  implements OnGatewayConnection, OnGatewayDisconnect\n{\n  @WebSocketServer() wss: Server;\n\n  private logger: Logger = new Logger('BulkActionsGateway');\n\n  constructor(private service: BulkActionsService) {}\n\n  @SubscribeMessage(BulkActionsServerEvents.Create)\n  create(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @ConnectedSocket() socket: Socket,\n    @Body() dto: CreateBulkActionDto,\n  ) {\n    this.logger.debug('Creating new bulk action.');\n    return this.service.create(sessionMetadata, dto, socket);\n  }\n\n  @SubscribeMessage(BulkActionsServerEvents.Get)\n  get(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: BulkActionIdDto,\n  ) {\n    this.logger.debug('Subscribing to bulk action.');\n    return this.service.get(dto);\n  }\n\n  @SubscribeMessage(BulkActionsServerEvents.Abort)\n  abort(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: BulkActionIdDto,\n  ) {\n    this.logger.debug('Aborting bulk action.');\n    return this.service.abort(dto);\n  }\n\n  async handleConnection(socket: Socket): Promise<void> {\n    this.logger.debug(`Client connected: ${socket.id}`);\n  }\n\n  async handleDisconnect(socket: Socket): Promise<void> {\n    this.logger.debug(`Client disconnected: ${socket.id}`);\n    this.service.disconnect(socket.id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { BulkActionsService } from 'src/modules/bulk-actions/bulk-actions.service';\nimport { BulkActionsProvider } from 'src/modules/bulk-actions/providers/bulk-actions.provider';\nimport { BulkActionsGateway } from 'src/modules/bulk-actions/bulk-actions.gateway';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport { BulkActionsController } from 'src/modules/bulk-actions/bulk-actions.controller';\nimport { BulkImportController } from 'src/modules/bulk-actions/bulk-import.controller';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\n\n@Module({\n  controllers: [BulkActionsController, BulkImportController],\n  providers: [\n    BulkActionsGateway,\n    BulkActionsService,\n    BulkActionsProvider,\n    BulkActionsAnalytics,\n    BulkImportService,\n  ],\n  exports: [BulkImportService, BulkActionsAnalytics],\n})\nexport class BulkActionsModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts",
    "content": "import * as MockedSocket from 'socket.io-mock';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  MockType,\n  mockBulkActionsAnalytics,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { BulkActionsProvider } from 'src/modules/bulk-actions/providers/bulk-actions.provider';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { BulkActionType } from 'src/modules/bulk-actions/constants';\nimport { CreateBulkActionDto } from 'src/modules/bulk-actions/dto/create-bulk-action.dto';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { BulkActionsService } from 'src/modules/bulk-actions/bulk-actions.service';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\n\nexport const mockSocket1 = new MockedSocket();\nmockSocket1.id = '1';\nmockSocket1['emit'] = jest.fn();\n\nconst mockBulkActionFilter = Object.assign(new BulkActionFilter(), {\n  count: 10_000,\n  match: '*',\n  type: RedisDataType.Set,\n});\n\nconst mockCreateBulkActionDto = Object.assign(new CreateBulkActionDto(), {\n  id: 'bulk-action-id',\n  databaseId: 'database-id',\n  type: BulkActionType.Delete,\n  filter: mockBulkActionFilter,\n});\n\nconst mockBulkAction = new BulkAction(\n  mockCreateBulkActionDto.id,\n  mockCreateBulkActionDto.databaseId,\n  mockCreateBulkActionDto.type,\n  mockBulkActionFilter,\n  mockSocket1,\n  mockBulkActionsAnalytics as any,\n);\nconst mockOverview = 'mocked overview...';\n\nmockBulkAction['getOverview'] = jest.fn().mockReturnValue(mockOverview);\n\ndescribe('BulkActionsService', () => {\n  let service: BulkActionsService;\n  let bulkActionProvider: MockType<BulkActionsProvider>;\n  let analyticsService: MockType<BulkActionsAnalytics>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        BulkActionsService,\n        {\n          provide: BulkActionsProvider,\n          useFactory: () => ({\n            create: jest.fn().mockResolvedValue(mockBulkAction),\n            get: jest.fn().mockReturnValue(mockBulkAction),\n            abort: jest.fn().mockReturnValue(mockBulkAction),\n            abortUsersBulkActions: jest.fn().mockReturnValue(2),\n          }),\n        },\n        {\n          provide: BulkActionsAnalytics,\n          useFactory: mockBulkActionsAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = module.get(BulkActionsService);\n    bulkActionProvider = module.get(BulkActionsProvider);\n    analyticsService = module.get(BulkActionsAnalytics);\n  });\n\n  describe('create', () => {\n    it('should create and return overview', async () => {\n      expect(\n        await service.create(\n          mockSessionMetadata,\n          mockCreateBulkActionDto,\n          mockSocket1,\n        ),\n      ).toEqual(mockOverview);\n      expect(bulkActionProvider.create).toHaveBeenCalledTimes(1);\n      expect(analyticsService.sendActionStarted).toHaveBeenCalledTimes(1);\n    });\n  });\n  describe('get', () => {\n    it('should get and return overview', async () => {\n      expect(await service.get({ id: mockCreateBulkActionDto.id })).toEqual(\n        mockOverview,\n      );\n      expect(bulkActionProvider.get).toHaveBeenCalledTimes(1);\n    });\n  });\n  describe('abort', () => {\n    it('should abort and return overview', async () => {\n      expect(await service.abort({ id: mockCreateBulkActionDto.id })).toEqual(\n        mockOverview,\n      );\n      expect(bulkActionProvider.abort).toHaveBeenCalledTimes(1);\n    });\n  });\n  describe('disconnect', () => {\n    it('should call abortUsersBulkActions on disconnect', async () => {\n      expect(service.disconnect(mockSocket1.id)).toEqual(undefined);\n      expect(bulkActionProvider.abortUsersBulkActions).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('streamReport', () => {\n    let mockResponse: any;\n    let mockBulkActionWithReport: any;\n\n    beforeEach(() => {\n      mockResponse = {\n        setHeader: jest.fn(),\n        write: jest.fn(),\n        end: jest.fn(),\n      };\n\n      mockBulkActionWithReport = {\n        setStreamingResponse: jest.fn(),\n        isReportEnabled: jest.fn().mockReturnValue(true),\n      };\n    });\n\n    it('should throw NotFoundException when bulk action not found', async () => {\n      bulkActionProvider.get = jest.fn().mockReturnValue(null);\n\n      await expect(\n        service.streamReport('non-existent-id', mockResponse),\n      ).rejects.toThrow('Bulk action not found');\n    });\n\n    it('should throw BadRequestException when report not enabled', async () => {\n      mockBulkActionWithReport.isReportEnabled.mockReturnValue(false);\n      bulkActionProvider.get = jest\n        .fn()\n        .mockReturnValue(mockBulkActionWithReport);\n\n      await expect(\n        service.streamReport('bulk-action-id', mockResponse),\n      ).rejects.toThrow(\n        'Report generation was not enabled for this bulk action',\n      );\n    });\n\n    it('should set headers and attach stream to bulk action', async () => {\n      bulkActionProvider.get = jest\n        .fn()\n        .mockReturnValue(mockBulkActionWithReport);\n      const mockTimestamp = '1733047200000'; // 2024-12-01T10:00:00.000Z\n      const expectedFilename =\n        'bulk-delete-report-2024-12-01T10-00-00-000Z.txt';\n\n      await service.streamReport(mockTimestamp, mockResponse);\n\n      expect(mockResponse.setHeader).toHaveBeenCalledWith(\n        'Content-Type',\n        'text/plain',\n      );\n      expect(mockResponse.setHeader).toHaveBeenCalledWith(\n        'Content-Disposition',\n        `attachment; filename=\"${expectedFilename}\"`,\n      );\n      expect(mockResponse.setHeader).toHaveBeenCalledWith(\n        'Access-Control-Expose-Headers',\n        'Content-Disposition',\n      );\n      expect(mockResponse.setHeader).toHaveBeenCalledWith(\n        'Transfer-Encoding',\n        'chunked',\n      );\n      expect(\n        mockBulkActionWithReport.setStreamingResponse,\n      ).toHaveBeenCalledWith(mockResponse);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-actions.service.ts",
    "content": "import { Socket } from 'socket.io';\nimport { Response } from 'express';\nimport {\n  BadRequestException,\n  Injectable,\n  NotFoundException,\n} from '@nestjs/common';\nimport { BulkActionsProvider } from 'src/modules/bulk-actions/providers/bulk-actions.provider';\nimport { CreateBulkActionDto } from 'src/modules/bulk-actions/dto/create-bulk-action.dto';\nimport { BulkActionIdDto } from 'src/modules/bulk-actions/dto/bulk-action-id.dto';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class BulkActionsService {\n  constructor(\n    private readonly bulkActionsProvider: BulkActionsProvider,\n    private readonly analytics: BulkActionsAnalytics,\n  ) {}\n\n  async create(\n    sessionMetadata: SessionMetadata,\n    dto: CreateBulkActionDto,\n    socket: Socket,\n  ) {\n    const bulkAction = await this.bulkActionsProvider.create(\n      sessionMetadata,\n      dto,\n      socket,\n    );\n    const overview = bulkAction.getOverview();\n\n    this.analytics.sendActionStarted(sessionMetadata, overview);\n\n    return overview;\n  }\n\n  async get(dto: BulkActionIdDto) {\n    const bulkAction = await this.bulkActionsProvider.get(dto.id);\n    return bulkAction.getOverview();\n  }\n\n  async abort(dto: BulkActionIdDto) {\n    const bulkAction = await this.bulkActionsProvider.abort(dto.id);\n\n    return bulkAction.getOverview();\n  }\n\n  disconnect(socketId: string) {\n    this.bulkActionsProvider.abortUsersBulkActions(socketId);\n  }\n\n  /**\n   * Stream bulk action report as downloadable file\n   * @param id Bulk action id\n   * @param res Express response object\n   */\n  async streamReport(id: string, res: Response): Promise<void> {\n    const bulkAction = this.bulkActionsProvider.get(id);\n\n    if (!bulkAction) {\n      throw new NotFoundException('Bulk action not found');\n    }\n\n    if (!bulkAction.isReportEnabled()) {\n      throw new BadRequestException(\n        'Report generation was not enabled for this bulk action',\n      );\n    }\n\n    // Set headers for file download\n    const timestamp = new Date(Number(id)).toISOString().replace(/[:.]/g, '-');\n    res.setHeader('Content-Type', 'text/plain');\n    res.setHeader(\n      'Content-Disposition',\n      `attachment; filename=\"bulk-delete-report-${timestamp}.txt\"`,\n    );\n    res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');\n    res.setHeader('Transfer-Encoding', 'chunked');\n\n    // Attach the response stream to the bulk action\n    // This will trigger the bulk action to start processing\n    bulkAction.setStreamingResponse(res);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-import.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  HttpCode,\n  Post,\n  Req,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport * as Busboy from 'busboy';\nimport { Readable } from 'stream';\nimport { Request } from 'express';\nimport { ApiConsumes, ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\nimport { ClientMetadataParam } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';\nimport {\n  UploadImportFileByPathDto,\n  ImportVectorCollectionDto,\n} from 'src/modules/bulk-actions/dto/upload-import-file-by-path.dto';\n\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(ClassSerializerInterceptor)\n@ApiTags('Bulk Actions')\n@Controller('/bulk-actions/import')\nexport class BulkImportController {\n  constructor(private readonly service: BulkImportService) {}\n\n  @Post()\n  @ApiConsumes('multipart/form-data')\n  @HttpCode(200)\n  @ApiEndpoint({\n    description: 'Import data from file',\n    responses: [\n      {\n        type: Object,\n      },\n    ],\n  })\n  async import(\n    @Req() req: Request,\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n  ): Promise<IBulkActionOverview> {\n    return new Promise((res, rej) => {\n      const busboy = Busboy({ headers: req.headers });\n\n      busboy.on('file', (_fieldName: string, fileStream: Readable) => {\n        this.service.import(clientMetadata, fileStream).then(res).catch(rej);\n      });\n\n      req.pipe(busboy);\n    });\n  }\n\n  @Post('/tutorial-data')\n  @HttpCode(200)\n  @ApiEndpoint({\n    description: 'Import data from tutorial by path',\n    responses: [\n      {\n        type: Object,\n      },\n    ],\n  })\n  async uploadFromTutorial(\n    @Body() dto: UploadImportFileByPathDto,\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n  ): Promise<IBulkActionOverview> {\n    return this.service.uploadFromTutorial(clientMetadata, dto);\n  }\n\n  @Post('/default-data')\n  @HttpCode(200)\n  @ApiEndpoint({\n    description: 'Import default data',\n    responses: [\n      {\n        type: Object,\n      },\n    ],\n  })\n  async importDefaultData(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n  ): Promise<IBulkActionOverview> {\n    return this.service.importDefaultData(clientMetadata);\n  }\n\n  @Post('/vector-collection')\n  @HttpCode(200)\n  @ApiEndpoint({\n    description: 'Import vector collection data',\n    responses: [\n      {\n        type: Object,\n      },\n    ],\n  })\n  async importVectorCollection(\n    @Body() dto: ImportVectorCollectionDto,\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n  ): Promise<IBulkActionOverview> {\n    return this.service.importVectorCollection(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-import.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\nimport {\n  mockBulkActionsAnalytics,\n  mockBulkActionOverviewMatcher,\n  mockClientMetadata,\n  mockClusterRedisClient,\n  mockCombinedStream,\n  mockDatabase,\n  mockDatabaseClientFactory,\n  mockDatabaseModules,\n  mockDatabaseService,\n  mockDefaultDataManifest,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n  MockType,\n} from 'src/__mocks__';\nimport { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\nimport { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';\nimport {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport * as fs from 'fs-extra';\nimport * as CombinedStream from 'combined-stream';\nimport config from 'src/utils/config';\nimport { join } from 'path';\nimport { wrapHttpError } from 'src/common/utils';\nimport { RedisClientCommand } from 'src/modules/redis/client';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { Readable } from 'stream';\nimport { DatabaseService } from 'src/modules/database/database.service';\n\nconst PATH_CONFIG = config.get('dir_path');\n\nconst generateNCommandsBuffer = (n: number) =>\n  Buffer.from(\n    new Array(n)\n      .fill(1)\n      .map(() => ['set', ['foo', 'bar']])\n      .join('\\n'),\n  );\nconst generateNBatchCommands = (n: number): RedisClientCommand[] =>\n  new Array(n).fill(1).map(() => ['set', 'foo', 'bar']);\nconst generateNBatchCommandsResults = (n: number) =>\n  new Array(n).fill(1).map(() => [null, 'OK']);\nconst mockBatchCommands = generateNBatchCommands(100);\nconst mockBatchCommandsResult = generateNBatchCommandsResults(100);\nconst mockBatchCommandsResultWithErrors = [\n  ...new Array(99).fill(1).map(() => [null, 'OK']),\n  [new Error('ReplyError')],\n];\nconst mockSummary: BulkActionSummary = Object.assign(new BulkActionSummary(), {\n  processed: 100,\n  succeed: 100,\n  failed: 0,\n  errors: [],\n});\n\nconst mockEmptySummary: BulkActionSummary = Object.assign(\n  new BulkActionSummary(),\n  {\n    processed: 0,\n    succeed: 0,\n    failed: 0,\n    errors: [],\n  },\n);\n\nconst mockSummaryWithErrors = Object.assign(new BulkActionSummary(), {\n  processed: 100,\n  succeed: 99,\n  failed: 1,\n  errors: [],\n});\n\nconst mockImportResult: IBulkActionOverview = {\n  id: 'empty',\n  databaseId: mockClientMetadata.databaseId,\n  type: BulkActionType.Upload,\n  summary: mockSummary.getOverview(),\n  progress: null,\n  filter: null,\n  status: BulkActionStatus.Completed,\n  duration: 100,\n};\n\nconst mockEmptyImportResult: IBulkActionOverview = {\n  id: 'empty',\n  databaseId: mockClientMetadata.databaseId,\n  type: BulkActionType.Upload,\n  summary: mockEmptySummary.getOverview(),\n  progress: null,\n  filter: null,\n  status: BulkActionStatus.Completed,\n  duration: 0,\n};\n\nconst mockReadableStream = Readable.from(Buffer.from('SET foo bar'));\n\nconst mockUploadImportFileByPathDto = {\n  path: '/some/path',\n};\n\njest.mock('fs-extra');\nconst mockedFs = fs as jest.Mocked<typeof fs>;\n\njest.mock('combined-stream');\nconst mockedCombinedStream = CombinedStream as jest.Mocked<\n  typeof CombinedStream\n>;\n\ndescribe('BulkImportService', () => {\n  let service: BulkImportService;\n  let databaseClientFactory: MockType<DatabaseClientFactory>;\n  let deviceService: MockType<DatabaseService>;\n  let analytics: MockType<BulkActionsAnalytics>;\n\n  beforeEach(async () => {\n    jest.mock('fs-extra', () => mockedFs);\n    jest.mock('combined-stream', () => mockedCombinedStream);\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        BulkImportService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: BulkActionsAnalytics,\n          useFactory: mockBulkActionsAnalytics,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(BulkImportService);\n    databaseClientFactory = module.get(DatabaseClientFactory);\n    analytics = module.get(BulkActionsAnalytics);\n    deviceService = module.get(DatabaseService);\n  });\n\n  describe('executeBatch', () => {\n    it('should execute batch in pipeline for standalone', async () => {\n      mockStandaloneRedisClient.sendPipeline.mockResolvedValueOnce(\n        mockBatchCommandsResult,\n      );\n      expect(\n        await service['executeBatch'](\n          mockStandaloneRedisClient,\n          mockBatchCommands,\n        ),\n      ).toEqual(mockSummary);\n    });\n    it('should execute batch in pipeline for standalone with errors', async () => {\n      mockStandaloneRedisClient.sendPipeline.mockResolvedValueOnce(\n        mockBatchCommandsResultWithErrors,\n      );\n      expect(\n        await service['executeBatch'](\n          mockStandaloneRedisClient,\n          mockBatchCommands,\n        ),\n      ).toEqual(mockSummaryWithErrors);\n    });\n    it('should return all failed in case of global error', async () => {\n      mockStandaloneRedisClient.sendPipeline.mockRejectedValueOnce(new Error());\n      expect(\n        await service['executeBatch'](\n          mockStandaloneRedisClient,\n          mockBatchCommands,\n        ),\n      ).toEqual({\n        ...mockSummary.getOverview(),\n        succeed: 0,\n        failed: mockSummary.getOverview().processed,\n      });\n    });\n    it('should execute batch of commands without pipeline for cluster', async () => {\n      mockClusterRedisClient.call.mockRejectedValueOnce(new Error());\n      mockClusterRedisClient.call.mockResolvedValue('OK');\n      expect(\n        await service['executeBatch'](\n          mockClusterRedisClient,\n          mockBatchCommands,\n        ),\n      ).toEqual(mockSummaryWithErrors);\n    });\n  });\n\n  describe('import', () => {\n    let spy;\n\n    beforeEach(() => {\n      spy = jest.spyOn(service as any, 'executeBatch');\n    });\n\n    it('should import data', async () => {\n      spy.mockResolvedValue(mockSummary);\n      expect(\n        await service.import(mockClientMetadata, mockReadableStream),\n      ).toEqual({\n        ...mockImportResult,\n        duration: expect.anything(),\n      });\n      expect(analytics.sendActionSucceed).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockImportResult,\n          duration: expect.anything(),\n        },\n      );\n    });\n\n    it('should import data (100K) from file in batches 10K each', async () => {\n      spy.mockResolvedValue(\n        Object.assign(new BulkActionSummary(), {\n          processed: 10_000,\n          succeed: 10_000,\n          failed: 0,\n        }),\n      );\n      expect(\n        await service.import(\n          mockClientMetadata,\n          Readable.from(generateNCommandsBuffer(100_000)),\n        ),\n      ).toEqual({\n        ...mockImportResult,\n        summary: {\n          processed: 100_000,\n          succeed: 100_000,\n          failed: 0,\n          errors: [],\n          keys: [],\n        },\n        duration: expect.anything(),\n      });\n    });\n\n    it('should import data (10K) from file in batches 10K each', async () => {\n      spy.mockResolvedValue(\n        Object.assign(new BulkActionSummary(), {\n          processed: 10_000,\n          succeed: 10_000,\n          failed: 0,\n        }),\n      );\n      expect(\n        await service.import(\n          mockClientMetadata,\n          Readable.from(generateNCommandsBuffer(10_000)),\n        ),\n      ).toEqual({\n        ...mockImportResult,\n        summary: {\n          processed: 10_000,\n          succeed: 10_000,\n          failed: 0,\n          errors: [],\n          keys: [],\n        },\n        duration: expect.anything(),\n      });\n    });\n\n    it('should not import any data due to parse error', async () => {\n      spy.mockResolvedValue(\n        Object.assign(new BulkActionSummary(), {\n          processed: 0,\n          succeed: 0,\n          failed: 0,\n        }),\n      );\n      expect(\n        await service.import(\n          mockClientMetadata,\n          Readable.from(Buffer.from('{\"incorrectdata\"}\\n{\"incorrectdata\"}')),\n        ),\n      ).toEqual({\n        ...mockImportResult,\n        summary: {\n          processed: 2,\n          succeed: 0,\n          failed: 2,\n          errors: [],\n          keys: [],\n        },\n        duration: expect.anything(),\n      });\n      expect(mockStandaloneRedisClient.disconnect).toHaveBeenCalled();\n    });\n\n    it('should ignore blank lines', async () => {\n      await service.import(\n        mockClientMetadata,\n        Readable.from(\n          Buffer.from('\\n SET foo bar \\n     \\n SET foo bar \\n    '),\n        ),\n      );\n      expect(spy).toBeCalledWith(mockStandaloneRedisClient, [\n        ['SET', 'foo', 'bar'],\n        ['SET', 'foo', 'bar'],\n      ]);\n      expect(mockStandaloneRedisClient.disconnect).toHaveBeenCalled();\n    });\n\n    it('should throw an error in case of global error', async () => {\n      try {\n        databaseClientFactory.createClient.mockRejectedValueOnce(\n          new NotFoundException(),\n        );\n\n        await service.import(mockClientMetadata, mockReadableStream);\n\n        fail();\n      } catch (e) {\n        expect(mockStandaloneRedisClient.disconnect).not.toHaveBeenCalled();\n        expect(analytics.sendActionFailed).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          { ...mockEmptyImportResult },\n          wrapHttpError(e),\n        );\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n  });\n\n  describe('uploadFromTutorial', () => {\n    let spy;\n\n    beforeEach(() => {\n      spy = jest.spyOn(service as any, 'import');\n      spy.mockResolvedValue(mockSummary);\n      mockedFs.readFile.mockResolvedValue(Buffer.from('set foo bar'));\n    });\n\n    it('should import file by path', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => true);\n\n      await service.uploadFromTutorial(\n        mockClientMetadata,\n        mockUploadImportFileByPathDto,\n      );\n\n      expect(mockedFs.createReadStream).toHaveBeenCalledWith(\n        join(PATH_CONFIG.homedir, mockUploadImportFileByPathDto.path),\n      );\n    });\n\n    it('should import file by path with static', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => true);\n\n      await service.uploadFromTutorial(mockClientMetadata, {\n        path: '/static/guides/_data.file',\n      });\n\n      expect(mockedFs.createReadStream).toHaveBeenCalledWith(\n        join(PATH_CONFIG.homedir, '/guides/_data.file'),\n      );\n    });\n\n    it('should normalize path before importing and not search for file outside home folder', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => true);\n\n      await service.uploadFromTutorial(mockClientMetadata, {\n        path: '/../../../danger',\n      });\n\n      expect(mockedFs.createReadStream).toHaveBeenCalledWith(\n        join(PATH_CONFIG.homedir, 'danger'),\n      );\n    });\n\n    it('should normalize path before importing and throw an error when search for file outside home folder (relative)', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => true);\n\n      try {\n        await service.uploadFromTutorial(mockClientMetadata, {\n          path: '../../../danger',\n        });\n\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('Data file was not found');\n      }\n    });\n\n    it('should throw BadRequest when no file found', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => false);\n\n      try {\n        await service.uploadFromTutorial(mockClientMetadata, {\n          path: '../../../danger',\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('Data file was not found');\n      }\n    });\n  });\n\n  describe('importDefaultData', () => {\n    let spy;\n\n    beforeEach(() => {\n      spy = jest.spyOn(service as any, 'import');\n      spy.mockResolvedValue(mockSummary);\n      mockedCombinedStream.create.mockReturnValue(mockCombinedStream);\n    });\n\n    it('should import default data for 2 known modules', async () => {\n      mockedFs.readFileSync.mockImplementationOnce(() =>\n        Buffer.from(JSON.stringify(mockDefaultDataManifest)),\n      );\n      mockedFs.createReadStream.mockImplementationOnce(\n        () => new fs.ReadStream(),\n      );\n      deviceService.get.mockResolvedValue({\n        ...mockDatabase,\n        modules: mockDatabaseModules,\n      });\n\n      await service.importDefaultData(mockClientMetadata);\n\n      expect(mockCombinedStream.append).toHaveBeenCalledTimes(4);\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(spy).toHaveBeenCalledWith(mockClientMetadata, mockCombinedStream);\n    });\n\n    it('should import default data for search module', async () => {\n      mockedFs.readFileSync.mockImplementationOnce(() =>\n        Buffer.from(\n          JSON.stringify({\n            files: [\n              {\n                path: 'some-path',\n                modules: ['search', 'searchlight', 'ft', 'ftl'],\n              },\n            ],\n          }),\n        ),\n      );\n\n      mockedFs.createReadStream.mockImplementationOnce(\n        () => new fs.ReadStream(),\n      );\n      deviceService.get.mockResolvedValue({\n        ...mockDatabase,\n        modules: [\n          {\n            name: 'search',\n            version: 999999,\n            semanticVersion: '99.99.99',\n          },\n        ],\n      });\n\n      await service.importDefaultData(mockClientMetadata);\n\n      expect(mockCombinedStream.append).toHaveBeenCalledTimes(2);\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(spy).toHaveBeenCalledWith(mockClientMetadata, mockCombinedStream);\n    });\n\n    it('should import default data for searchlight module', async () => {\n      mockedFs.readFileSync.mockImplementationOnce(() =>\n        Buffer.from(\n          JSON.stringify({\n            files: [\n              {\n                path: 'some-path',\n                modules: ['search', 'searchlight', 'ft', 'ftl'],\n              },\n            ],\n          }),\n        ),\n      );\n\n      mockedFs.createReadStream.mockImplementationOnce(\n        () => new fs.ReadStream(),\n      );\n      deviceService.get.mockResolvedValue({\n        ...mockDatabase,\n        modules: [\n          {\n            name: 'searchlight',\n            version: 999999,\n            semanticVersion: '99.99.99',\n          },\n        ],\n      });\n\n      await service.importDefaultData(mockClientMetadata);\n\n      expect(mockCombinedStream.append).toHaveBeenCalledTimes(2);\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(spy).toHaveBeenCalledWith(mockClientMetadata, mockCombinedStream);\n    });\n\n    it('should import default data for core module only', async () => {\n      mockedFs.readFileSync.mockImplementationOnce(() =>\n        Buffer.from(JSON.stringify(mockDefaultDataManifest)),\n      );\n      mockedFs.createReadStream.mockImplementationOnce(\n        () => new fs.ReadStream(),\n      );\n      deviceService.get.mockResolvedValue(mockDatabase);\n\n      await service.importDefaultData(mockClientMetadata);\n\n      expect(mockCombinedStream.append).toHaveBeenCalledTimes(2);\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(spy).toHaveBeenCalledWith(mockClientMetadata, mockCombinedStream);\n    });\n\n    it('should throw an error in case when something went wrong', async () => {\n      mockedFs.readFileSync.mockImplementationOnce(() => {\n        throw new Error();\n      });\n\n      try {\n        await service.importDefaultData(mockClientMetadata);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to import default data');\n        expect(spy).toHaveBeenCalledTimes(0);\n      }\n    });\n  });\n\n  describe('importVectorCollection', () => {\n    afterEach(() => {\n      jest.clearAllMocks();\n    });\n\n    beforeEach(() => {\n      (mockedFs.pathExists as jest.Mock).mockReset();\n      (mockedFs.createReadStream as jest.Mock).mockReset();\n    });\n\n    it('should import vector collection successfully', async () => {\n      const spy = jest.spyOn(service, 'import');\n      spy.mockResolvedValue(mockBulkActionOverviewMatcher);\n\n      (mockedFs.pathExists as jest.Mock).mockResolvedValue(true);\n      (mockedFs.createReadStream as jest.Mock).mockReturnValue(new Readable());\n\n      const result = await service.importVectorCollection(mockClientMetadata, {\n        collectionName: 'bikes',\n      });\n\n      expect(mockedFs.pathExists).toHaveBeenCalledWith(\n        expect.stringContaining('vector-collections/bikes'),\n      );\n      expect(mockedFs.createReadStream).toHaveBeenCalledWith(\n        expect.stringContaining('vector-collections/bikes'),\n      );\n      expect(spy).toHaveBeenCalledWith(\n        mockClientMetadata,\n        expect.any(Readable),\n      );\n      expect(result).toEqual(mockBulkActionOverviewMatcher);\n    });\n\n    it('should throw BadRequestException when collectionName file does not exist', async () => {\n      (mockedFs.pathExists as jest.Mock).mockResolvedValue(false);\n\n      await expect(\n        service.importVectorCollection(mockClientMetadata, {\n          collectionName: 'bikes',\n        }),\n      ).rejects.toThrow('No data file found for collection: bikes');\n\n      expect(mockedFs.pathExists).toHaveBeenCalledWith(\n        expect.stringContaining('vector-collections/bikes'),\n      );\n    });\n\n    it('should handle import errors', async () => {\n      const spy = jest.spyOn(service, 'import');\n      const importError = new Error('Import failed');\n      spy.mockRejectedValue(importError);\n\n      (mockedFs.pathExists as jest.Mock).mockResolvedValue(true);\n      (mockedFs.createReadStream as jest.Mock).mockReturnValue(new Readable());\n\n      await expect(\n        service.importVectorCollection(mockClientMetadata, {\n          collectionName: 'bikes',\n        }),\n      ).rejects.toThrow('Import failed');\n\n      expect(spy).toHaveBeenCalledWith(\n        mockClientMetadata,\n        expect.any(Readable),\n      );\n    });\n\n    it('should throw BadRequestException when collectionName is not in allowed list', async () => {\n      await expect(\n        service.importVectorCollection(mockClientMetadata, {\n          collectionName: '../../etc/passwd', // malicious input\n        }),\n      ).rejects.toThrow('Invalid collection name');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/bulk-import.service.ts",
    "content": "import { join } from 'path';\nimport * as fs from 'fs-extra';\nimport {\n  BadRequestException,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { Readable } from 'stream';\nimport * as readline from 'readline';\nimport { wrapHttpError } from 'src/common/utils';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { ClientMetadata } from 'src/common/models';\nimport { splitCliCommandLine } from 'src/utils/cli-helper';\nimport { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\nimport { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';\nimport {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport {\n  UploadImportFileByPathDto,\n  ImportVectorCollectionDto,\n} from 'src/modules/bulk-actions/dto/upload-import-file-by-path.dto';\nimport {\n  RedisClient,\n  RedisClientCommand,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\nimport config, { Config } from 'src/utils/config';\nimport * as CombinedStream from 'combined-stream';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nconst BATCH_LIMIT = 10_000;\nconst PATH_CONFIG = config.get('dir_path') as Config['dir_path'];\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\nconst ALLOWED_VECTOR_INDEX_COLLECTIONS = ['bikes', 'movies'];\n\n@Injectable()\nexport class BulkImportService {\n  private logger = new Logger('BulkImportService');\n\n  constructor(\n    private readonly databaseService: DatabaseService,\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly analytics: BulkActionsAnalytics,\n  ) {}\n\n  private async executeBatch(\n    client: RedisClient,\n    batch: RedisClientCommand[],\n  ): Promise<BulkActionSummary> {\n    const result = new BulkActionSummary();\n    result.addProcessed(batch.length);\n\n    try {\n      if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n        await Promise.all(\n          batch.map(async (command) => {\n            try {\n              await client.call(command);\n              result.addSuccess(1);\n            } catch (e) {\n              result.addFailed(1);\n            }\n          }),\n        );\n      } else {\n        (await client.sendPipeline(batch, { unknownCommands: true })).forEach(\n          ([err]) => {\n            if (err) {\n              result.addFailed(1);\n            } else {\n              result.addSuccess(1);\n            }\n          },\n        );\n      }\n    } catch (e) {\n      this.logger.error('Unable to execute batch of commands', e);\n      result.addFailed(batch.length);\n    }\n\n    return result;\n  }\n\n  /**\n   * @param clientMetadata\n   * @param fileStream\n   */\n  public async import(\n    clientMetadata: ClientMetadata,\n    fileStream: Readable,\n  ): Promise<IBulkActionOverview> {\n    const startTime = Date.now();\n    const result: IBulkActionOverview = {\n      id: 'empty',\n      databaseId: clientMetadata.databaseId,\n      type: BulkActionType.Upload,\n      summary: {\n        processed: 0,\n        succeed: 0,\n        failed: 0,\n        errors: [],\n        keys: [],\n      },\n      progress: null,\n      filter: null,\n      status: BulkActionStatus.Completed,\n      duration: 0,\n    };\n\n    this.analytics.sendActionStarted(clientMetadata.sessionMetadata, result);\n\n    let parseErrors = 0;\n\n    let client;\n\n    try {\n      client = await this.databaseClientFactory.createClient(clientMetadata);\n\n      let batch = [];\n\n      const batchResults: BulkActionSummary[] = [];\n\n      try {\n        const rl = readline.createInterface({\n          input: fileStream,\n        });\n\n        for await (const line of rl) {\n          try {\n            const command = splitCliCommandLine(line.trim());\n            if (batch.length >= BATCH_LIMIT) {\n              batchResults.push(await this.executeBatch(client, batch));\n              batch = [];\n            }\n            if (command?.[0]) {\n              batch.push(command);\n            }\n          } catch (e) {\n            parseErrors += 1;\n          }\n        }\n      } catch (e) {\n        result.summary.errors.push(e);\n        result.status = BulkActionStatus.Failed;\n        this.analytics.sendActionFailed(\n          clientMetadata.sessionMetadata,\n          result,\n          e,\n        );\n      }\n\n      batchResults.push(await this.executeBatch(client, batch));\n\n      batchResults.forEach((batchResult) => {\n        result.summary.processed += batchResult.getOverview().processed;\n        result.summary.succeed += batchResult.getOverview().succeed;\n        result.summary.failed += batchResult.getOverview().failed;\n      });\n\n      result.duration = Date.now() - startTime;\n      result.summary.processed += parseErrors;\n      result.summary.failed += parseErrors;\n\n      if (result.status === BulkActionStatus.Completed) {\n        this.analytics.sendActionSucceed(\n          clientMetadata.sessionMetadata,\n          result,\n        );\n      }\n\n      client.disconnect();\n\n      return result;\n    } catch (e) {\n      this.logger.error('Unable to process an import file', e, clientMetadata);\n      const exception = wrapHttpError(e);\n      this.analytics.sendActionFailed(\n        clientMetadata.sessionMetadata,\n        result,\n        exception,\n      );\n      client?.disconnect();\n      throw exception;\n    }\n  }\n\n  /**\n   * Upload file from tutorial by path\n   * @param clientMetadata\n   * @param dto\n   */\n  public async uploadFromTutorial(\n    clientMetadata: ClientMetadata,\n    dto: UploadImportFileByPathDto,\n  ): Promise<IBulkActionOverview> {\n    try {\n      const filePath = join(dto.path);\n\n      const staticPath = join(SERVER_CONFIG.base, SERVER_CONFIG.staticUri);\n\n      let trimmedPath = filePath;\n      if (filePath.indexOf(staticPath) === 0) {\n        trimmedPath = filePath.slice(staticPath.length);\n      }\n\n      const path = join(PATH_CONFIG.homedir, trimmedPath);\n\n      if (\n        !path.startsWith(PATH_CONFIG.homedir) ||\n        !(await fs.pathExists(path))\n      ) {\n        throw new BadRequestException('Data file was not found');\n      }\n\n      return this.import(clientMetadata, fs.createReadStream(path));\n    } catch (e) {\n      this.logger.error(\n        'Unable to process an import file path from tutorial',\n        e,\n        clientMetadata,\n      );\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Upload data from predefined files\n   * @param clientMetadata\n   */\n  public async importDefaultData(\n    clientMetadata: ClientMetadata,\n  ): Promise<IBulkActionOverview> {\n    try {\n      const database = await this.databaseService.get(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n      );\n      const databaseModules =\n        database.modules?.map((module) => module.name.toLowerCase()) || [];\n\n      const manifest = JSON.parse(\n        fs.readFileSync(join(PATH_CONFIG.dataDir, 'manifest.json')).toString(),\n      );\n\n      const commandsStream = CombinedStream.create();\n\n      manifest.files.forEach((file) => {\n        if (file.modules) {\n          const hasModule = file.modules.find((module) =>\n            databaseModules.includes(module),\n          );\n\n          if (!hasModule) {\n            return;\n          }\n        }\n        commandsStream.append(\n          fs.createReadStream(join(PATH_CONFIG.dataDir, file.path)),\n        );\n        commandsStream.append('\\r\\n');\n      });\n\n      const result = await this.import(clientMetadata, commandsStream);\n\n      this.analytics.sendImportSamplesUploaded(\n        clientMetadata.sessionMetadata,\n        result,\n      );\n\n      return result;\n    } catch (e) {\n      this.logger.error(\n        'Unable to process an import file path from tutorial',\n        e,\n        clientMetadata,\n      );\n      throw new InternalServerErrorException(\n        ERROR_MESSAGES.COMMON_DEFAULT_IMPORT_ERROR,\n      );\n    }\n  }\n\n  /**\n   * Import vector collection data\n   * @param clientMetadata\n   * @param dto\n   */\n  public async importVectorCollection(\n    clientMetadata: ClientMetadata,\n    dto: ImportVectorCollectionDto,\n  ): Promise<IBulkActionOverview> {\n    try {\n      if (!ALLOWED_VECTOR_INDEX_COLLECTIONS.includes(dto.collectionName)) {\n        throw new BadRequestException('Invalid collection name');\n      }\n\n      const collectionFilePath = join(\n        PATH_CONFIG.dataDir,\n        'vector-collections',\n        dto.collectionName,\n      );\n\n      if (!(await fs.pathExists(collectionFilePath))) {\n        throw new BadRequestException(\n          `No data file found for collection: ${dto.collectionName}`,\n        );\n      }\n\n      const fileStream = fs.createReadStream(collectionFilePath);\n      return this.import(clientMetadata, fileStream);\n    } catch (e) {\n      this.logger.error(\n        'Unable to import vector collection data',\n        e,\n        clientMetadata,\n      );\n      throw wrapHttpError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/constants/index.ts",
    "content": "export enum BulkActionsServerEvents {\n  Create = 'create',\n  Get = 'get',\n  Abort = 'abort',\n}\n\nexport enum BulkActionType {\n  Delete = 'delete',\n  Upload = 'upload',\n  Unlink = 'unlink',\n}\n\nexport enum BulkActionStatus {\n  Initializing = 'initializing',\n  Initialized = 'initialized',\n  Preparing = 'preparing',\n  Ready = 'ready',\n  Running = 'running',\n  Completed = 'completed',\n  Failed = 'failed',\n  Aborted = 'aborted',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/dto/bulk-action-id.dto.ts",
    "content": "import { IsNotEmpty, IsString } from 'class-validator';\n\nexport class BulkActionIdDto {\n  @IsNotEmpty()\n  @IsString()\n  id: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/dto/create-bulk-action.dto.ts",
    "content": "import { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { BulkActionType } from 'src/modules/bulk-actions/constants';\nimport {\n  IsBoolean,\n  IsEnum,\n  IsNotEmpty,\n  IsNumber,\n  IsOptional,\n  IsString,\n  Max,\n  Min,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { BulkActionIdDto } from 'src/modules/bulk-actions/dto/bulk-action-id.dto';\n\nexport class CreateBulkActionDto extends BulkActionIdDto {\n  @IsNotEmpty()\n  @IsString()\n  databaseId: string;\n\n  @IsNotEmpty()\n  @IsEnum(BulkActionType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      BulkActionType,\n    )}.`,\n  })\n  type: BulkActionType;\n\n  @IsNotEmpty()\n  @Type(() => BulkActionFilter)\n  filter: BulkActionFilter;\n\n  @IsOptional()\n  @IsNumber()\n  @Type(() => Number)\n  @Min(0)\n  @Max(2147483647)\n  db?: number;\n\n  @IsOptional()\n  @IsBoolean()\n  @Type(() => Boolean)\n  generateReport?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/dto/upload-import-file-by-path.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class UploadImportFileByPathDto {\n  @ApiProperty({\n    type: 'string',\n    description: 'Internal path to data file',\n  })\n  @IsString()\n  @IsNotEmpty()\n  path: string;\n}\n\nexport class ImportVectorCollectionDto {\n  @ApiProperty({\n    type: 'string',\n    description: 'Collection name to load vector data',\n    example: 'bikes',\n  })\n  @IsString()\n  @IsNotEmpty()\n  collectionName: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action-filter-overview.interface.ts",
    "content": "import { RedisDataType } from 'src/modules/browser/keys/dto';\n\nexport interface IBulkActionFilterOverview {\n  type: RedisDataType;\n  match: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action-overview.interface.ts",
    "content": "import {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\nimport { IBulkActionProgressOverview } from './bulk-action-progress-overview.interface';\nimport { IBulkActionSummaryOverview } from './bulk-action-summary-overview.interface';\nimport { IBulkActionFilterOverview } from './bulk-action-filter-overview.interface';\n\nexport interface IBulkActionOverview {\n  id: string;\n  databaseId: string;\n  duration: number;\n  type: BulkActionType;\n  status: BulkActionStatus; // Note: This can be null, according to the API response\n  filter: IBulkActionFilterOverview; // Note: This can be null, according to the API response\n  progress: IBulkActionProgressOverview;\n  summary: IBulkActionSummaryOverview;\n  downloadUrl?: string;\n  error?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action-progress-overview.interface.ts",
    "content": "export interface IBulkActionProgressOverview {\n  total: number;\n  scanned: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action-summary-overview.interface.ts",
    "content": "import { RedisString } from 'src/common/constants';\n\nexport interface IBulkActionSummaryOverview {\n  processed: number;\n  succeed: number;\n  failed: number;\n  errors: Array<Record<string, string>>;\n  keys: Array<RedisString>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action.interface.ts",
    "content": "import { BulkActionStatus } from 'src/modules/bulk-actions/constants';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { Socket } from 'socket.io';\n\nexport interface IBulkAction {\n  getStatus(): BulkActionStatus;\n  getFilter(): BulkActionFilter;\n  changeState(): void;\n  getSocket(): Socket;\n  writeToReport(keyName: Buffer, success: boolean, error?: string): void;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/bulk-action.runner.interface.ts",
    "content": "import { BulkActionProgress } from 'src/modules/bulk-actions/models/bulk-action-progress';\nimport { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\n\nexport interface IBulkActionRunner {\n  prepareToStart(): void;\n  run(): void;\n  getProgress(): BulkActionProgress;\n  getSummary(): BulkActionSummary;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/interfaces/index.ts",
    "content": "export * from './bulk-action.interface';\nexport * from './bulk-action.runner.interface';\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action-filter.spec.ts",
    "content": "import { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\n\ndescribe('BulkActionSummary', () => {\n  let filter: BulkActionFilter;\n\n  beforeEach(() => {\n    filter = new BulkActionFilter();\n  });\n\n  describe('getScanArgsArray', () => {\n    it('should get arguments for scan without type', async () => {\n      expect(filter.getScanArgsArray()).toEqual([\n        'count',\n        10_000,\n        'match',\n        '*',\n      ]);\n      expect(filter.getCount()).toEqual(10_000);\n    });\n    it('should get arguments for scan with type', async () => {\n      filter.match = 'device:*';\n      filter.type = RedisDataType.Set;\n      filter.count = 9_999;\n      expect(filter.getScanArgsArray()).toEqual([\n        'count',\n        9_999,\n        'match',\n        'device:*',\n        'type',\n        RedisDataType.Set,\n      ]);\n      expect(filter.getCount()).toEqual(9_999);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action-filter.ts",
    "content": "import { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { IsEnum, IsInt, IsOptional, IsString } from 'class-validator';\nimport { IBulkActionFilterOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-filter-overview.interface';\n\nexport class BulkActionFilter {\n  @IsOptional()\n  @IsEnum(RedisDataType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      RedisDataType,\n    )}.`,\n  })\n  type?: RedisDataType = null;\n\n  @IsOptional()\n  @IsString()\n  match?: string = '*';\n\n  @IsOptional()\n  @IsInt()\n  count?: number = 10_000;\n\n  /**\n   * Generate scan args array for filter\n   */\n  getScanArgsArray(): Array<number | string> {\n    const args = ['count', this.count, 'match', this.match];\n\n    if (this.type) {\n      args.push('type', this.type);\n    }\n\n    return args;\n  }\n\n  getCount(): number {\n    return this.count;\n  }\n\n  getOverview(): IBulkActionFilterOverview {\n    return {\n      match: this.match,\n      type: this.type,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action-progress.spec.ts",
    "content": "import { BulkActionProgress } from 'src/modules/bulk-actions/models/bulk-action-progress';\n\ndescribe('BulkActionSummary', () => {\n  let progress: BulkActionProgress;\n\n  beforeEach(() => {\n    progress = new BulkActionProgress();\n    progress.setTotal(10_000);\n  });\n\n  describe('setTotal', () => {\n    it('should set total', async () => {\n      expect(progress['total']).toEqual(10_000);\n\n      progress.setTotal(30_0000);\n\n      expect(progress['total']).toEqual(30_0000);\n    });\n  });\n\n  describe('setCursor + getCursor', () => {\n    it('should set cursor and change scanned when cursor = 0', async () => {\n      expect(progress['total']).toEqual(10_000);\n      expect(progress['scanned']).toEqual(0);\n      expect(progress['cursor']).toEqual(0);\n      expect(progress.getCursor()).toEqual(0);\n\n      progress.setCursor(1000);\n\n      expect(progress['total']).toEqual(10_000);\n      expect(progress['scanned']).toEqual(0);\n      expect(progress['cursor']).toEqual(1000);\n      expect(progress.getCursor()).toEqual(1000);\n\n      progress.setCursor(0);\n\n      expect(progress['total']).toEqual(10_000);\n      expect(progress['scanned']).toEqual(10_000);\n      expect(progress['cursor']).toEqual(-1);\n      expect(progress.getCursor()).toEqual(-1);\n    });\n  });\n\n  describe('addScanned + getOverview', () => {\n    it('should add scanned but not more than total', async () => {\n      expect(progress['total']).toEqual(10_000);\n      expect(progress['scanned']).toEqual(0);\n      expect(progress.getOverview()).toEqual({\n        total: 10_000,\n        scanned: 0,\n      });\n\n      progress.addScanned(1000);\n\n      expect(progress['total']).toEqual(10_000);\n      expect(progress['scanned']).toEqual(1000);\n      expect(progress.getOverview()).toEqual({\n        total: 10_000,\n        scanned: 1_000,\n      });\n\n      progress.addScanned(200_000);\n\n      expect(progress['total']).toEqual(10_000);\n      expect(progress['scanned']).toEqual(10_000);\n      expect(progress.getOverview()).toEqual({\n        total: 10_000,\n        scanned: 10_000,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action-progress.ts",
    "content": "import { IBulkActionProgressOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-progress-overview.interface';\n\nexport class BulkActionProgress {\n  private total: number = 0;\n\n  private scanned: number = 0;\n\n  private cursor: number = 0;\n\n  setTotal(total) {\n    this.total = total;\n  }\n\n  setCursor(cursor) {\n    if (cursor === 0) {\n      this.scanned = this.total;\n      this.cursor = -1;\n    } else {\n      this.cursor = cursor;\n    }\n  }\n\n  getCursor(): number {\n    return this.cursor;\n  }\n\n  addScanned(scanned) {\n    this.scanned += scanned;\n\n    if (this.scanned > this.total) {\n      this.scanned = this.total;\n    }\n  }\n\n  getOverview(): IBulkActionProgressOverview {\n    return {\n      total: this.total,\n      scanned: this.scanned,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action-summary.spec.ts",
    "content": "import { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\n\nconst mockKey = 'mockedKey';\nconst mockKeyBuffer = Buffer.from(mockKey);\nconst mockRESPError = 'Reply Error: NOPERM for delete.';\nconst mockRESPErrorBuffer = Buffer.from(mockRESPError);\n\nconst generateErrors = (amount: number): any =>\n  new Array(amount).fill(1).map(() => ({\n    key: mockKeyBuffer,\n    error: mockRESPErrorBuffer,\n  }));\n\ndescribe('BulkActionSummary', () => {\n  let summary: BulkActionSummary;\n\n  beforeEach(() => {\n    summary = new BulkActionSummary();\n  });\n\n  describe('addProcessed', () => {\n    it('should increase processed', async () => {\n      expect(summary['processed']).toEqual(0);\n\n      summary.addProcessed(1);\n\n      expect(summary['processed']).toEqual(1);\n\n      summary.addProcessed(100);\n\n      expect(summary['processed']).toEqual(101);\n    });\n  });\n  describe('addSuccess', () => {\n    it('should increase succeed', async () => {\n      expect(summary['succeed']).toEqual(0);\n\n      summary.addSuccess(1);\n\n      expect(summary['succeed']).toEqual(1);\n\n      summary.addSuccess(100);\n\n      expect(summary['succeed']).toEqual(101);\n    });\n  });\n  describe('addFailed', () => {\n    it('should increase failed', async () => {\n      expect(summary['failed']).toEqual(0);\n\n      summary.addFailed(1);\n\n      expect(summary['failed']).toEqual(1);\n\n      summary.addFailed(100);\n\n      expect(summary['failed']).toEqual(101);\n    });\n  });\n  describe('addErrors', () => {\n    it('should increase fails and store errors (up to 500)', async () => {\n      expect(summary['failed']).toEqual(0);\n\n      summary.addErrors([]);\n\n      expect(summary['failed']).toEqual(0);\n\n      summary.addErrors(generateErrors(1));\n\n      expect(summary['failed']).toEqual(1);\n      expect(summary['errors']).toEqual(generateErrors(1));\n\n      summary.addErrors(generateErrors(100));\n\n      expect(summary['failed']).toEqual(101);\n      expect(summary['errors']).toEqual(generateErrors(101));\n\n      summary.addErrors(generateErrors(1000));\n\n      expect(summary['failed']).toEqual(1101);\n      expect(summary['errors']).toEqual(generateErrors(500));\n    });\n  });\n  describe('addKeys', () => {\n    it('should add keys', async () => {\n      expect(summary['keys']).toEqual([]);\n\n      summary.addKeys([Buffer.from('key1')]);\n\n      expect(summary['keys']).toEqual([Buffer.from('key1')]);\n\n      summary.addKeys([Buffer.from('key2'), Buffer.from('key3')]);\n\n      expect(summary['keys']).toEqual([\n        Buffer.from('key1'),\n        Buffer.from('key2'),\n        Buffer.from('key3'),\n      ]);\n    });\n  });\n  describe('getOverview', () => {\n    it('should get overview and clear errors', async () => {\n      expect(summary['processed']).toEqual(0);\n      expect(summary['succeed']).toEqual(0);\n      expect(summary['failed']).toEqual(0);\n      expect(summary['errors']).toEqual([]);\n      expect(summary['keys']).toEqual([]);\n\n      summary.addProcessed(1500);\n      summary.addSuccess(500);\n      summary.addErrors(generateErrors(1000));\n\n      expect(summary.getOverview()).toEqual({\n        processed: 1500,\n        succeed: 500,\n        failed: 1000,\n        errors: generateErrors(500),\n        keys: [],\n      });\n\n      expect(summary['processed']).toEqual(1500);\n      expect(summary['succeed']).toEqual(500);\n      expect(summary['failed']).toEqual(1000);\n      expect(summary['errors']).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action-summary.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { IBulkActionSummaryOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-summary-overview.interface';\n\nexport class BulkActionSummary {\n  private processed: number = 0;\n\n  private succeed: number = 0;\n\n  private failed: number = 0;\n\n  private errors: Array<Record<string, string>> = [];\n\n  private keys: Array<RedisString> = [];\n\n  addProcessed(count: number) {\n    this.processed += count;\n  }\n\n  addSuccess(count: number) {\n    this.succeed += count;\n  }\n\n  addFailed(count: number) {\n    this.failed += count;\n  }\n\n  addErrors(err: Array<Record<string, string>>) {\n    if (err.length) {\n      this.failed += err.length;\n\n      this.errors = err.concat(this.errors).slice(0, 500);\n    }\n  }\n\n  addKeys(keys: Array<RedisString>) {\n    this.keys.push(...keys);\n  }\n\n  getOverview(): IBulkActionSummaryOverview {\n    const overview = {\n      processed: this.processed,\n      succeed: this.succeed,\n      failed: this.failed,\n      errors: this.errors,\n      keys: this.keys,\n    };\n\n    this.errors = [];\n\n    return overview;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts",
    "content": "import { mockRedisKeysUtilModule } from 'src/__mocks__/redis-utils';\n\njest.doMock('src/modules/redis/utils/keys.util', mockRedisKeysUtilModule);\n\nimport { omit } from 'lodash';\nimport {\n  mockSocket,\n  mockBulkActionsAnalytics,\n  mockCreateBulkActionDto,\n  mockBulkActionFilter,\n  mockStandaloneRedisClient,\n  mockClusterRedisClient,\n  generateMockBulkActionSummary,\n  generateMockBulkActionProgress,\n  generateMockBulkActionErrors,\n  MockType,\n  mockBulkActionOverviewMatcher,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { BulkActionStatus } from 'src/modules/bulk-actions/constants';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\n\nlet bulkAction: BulkAction;\nlet mockRunner;\nlet analytics: MockType<BulkActionsAnalytics>;\n\ndescribe('AbstractBulkActionSimpleRunner', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n\n    bulkAction = new BulkAction(\n      mockCreateBulkActionDto.id,\n      mockCreateBulkActionDto.databaseId,\n      mockCreateBulkActionDto.type,\n      mockBulkActionFilter,\n      mockSocket,\n      mockBulkActionsAnalytics() as any,\n    );\n\n    analytics = bulkAction[\n      'analytics'\n    ] as unknown as MockType<BulkActionsAnalytics>;\n  });\n\n  describe('prepare', () => {\n    it('should generate single runner for standalone', async () => {\n      expect(bulkAction['runners']).toEqual([]);\n\n      await bulkAction.prepare(\n        mockStandaloneRedisClient,\n        DeleteBulkActionSimpleRunner,\n      );\n\n      expect(bulkAction['status']).toEqual(BulkActionStatus.Ready);\n      expect(bulkAction['runners'].length).toEqual(1);\n      expect(bulkAction['runners'][0]).toBeInstanceOf(\n        DeleteBulkActionSimpleRunner,\n      );\n      expect(bulkAction['runners'][0]['progress']['total']).toEqual(10_000);\n    });\n    it('should generate 2 runners for cluster with 2 master nodes', async () => {\n      mockClusterRedisClient.nodes.mockResolvedValueOnce([\n        mockStandaloneRedisClient,\n        mockStandaloneRedisClient,\n      ]);\n      expect(bulkAction['runners']).toEqual([]);\n\n      await bulkAction.prepare(\n        mockClusterRedisClient,\n        DeleteBulkActionSimpleRunner,\n      );\n\n      expect(bulkAction['status']).toEqual(BulkActionStatus.Ready);\n      expect(bulkAction['runners'].length).toEqual(2);\n      expect(bulkAction['runners'][0]).toBeInstanceOf(\n        DeleteBulkActionSimpleRunner,\n      );\n      expect(bulkAction['runners'][0]['progress']['total']).toEqual(10_000);\n      expect(bulkAction['runners'][1]).toBeInstanceOf(\n        DeleteBulkActionSimpleRunner,\n      );\n      expect(bulkAction['runners'][1]['progress']['total']).toEqual(10_000);\n    });\n    it('should fail when bulk action in inappropriate state', async () => {\n      try {\n        bulkAction['status'] = BulkActionStatus.Ready;\n        await bulkAction.prepare(\n          mockStandaloneRedisClient,\n          DeleteBulkActionSimpleRunner,\n        );\n        fail();\n      } catch (e) {\n        expect(e.message).toEqual(\n          `Unable to prepare bulk action with \"${BulkActionStatus.Ready}\" status`,\n        );\n      }\n    });\n  });\n\n  describe('start', () => {\n    let runnerRunSpy;\n    beforeEach(() => {\n      mockRunner = new DeleteBulkActionSimpleRunner(\n        bulkAction,\n        mockStandaloneRedisClient,\n      );\n      runnerRunSpy = jest.spyOn(mockRunner, 'run');\n    });\n\n    it('should throw an error when status is not READY', async () => {\n      try {\n        await bulkAction.start();\n        fail();\n      } catch (e) {\n        expect(e.message).toEqual(\n          `Unable to start bulk action with \"${BulkActionStatus.Initialized}\" status`,\n        );\n      }\n    });\n    it('should start and run until the end', async () => {\n      bulkAction['status'] = BulkActionStatus.Ready;\n      bulkAction['runners'] = [mockRunner];\n      runnerRunSpy.mockResolvedValue();\n\n      const overview = await bulkAction.start();\n\n      expect(overview.id).toEqual(mockCreateBulkActionDto.id);\n      expect(overview.type).toEqual(mockCreateBulkActionDto.type);\n      expect(overview.duration).toBeGreaterThanOrEqual(0);\n      expect(overview.filter).toEqual(omit(mockBulkActionFilter, 'count'));\n      expect(overview.progress).toEqual({\n        total: 0,\n        scanned: 0,\n      });\n      expect(overview.summary).toEqual({\n        processed: 0,\n        succeed: 0,\n        failed: 0,\n        errors: [],\n        keys: [],\n      });\n\n      await new Promise((res) => setTimeout(res, 100));\n\n      expect(runnerRunSpy).toHaveBeenCalledTimes(1);\n      expect(bulkAction['status']).toEqual(BulkActionStatus.Completed);\n    });\n    it('should start and run until the end for clusters', async () => {\n      bulkAction['status'] = BulkActionStatus.Ready;\n      bulkAction['runners'] = [mockRunner, mockRunner];\n      runnerRunSpy.mockResolvedValue();\n\n      const overview = await bulkAction.start();\n\n      expect(overview.id).toEqual(mockCreateBulkActionDto.id);\n      expect(overview.type).toEqual(mockCreateBulkActionDto.type);\n      expect(overview.duration).toBeGreaterThanOrEqual(0);\n      expect(overview.filter).toEqual(omit(mockBulkActionFilter, 'count'));\n      expect(overview.progress).toEqual({\n        total: 0,\n        scanned: 0,\n      });\n      expect(overview.summary).toEqual({\n        processed: 0,\n        succeed: 0,\n        failed: 0,\n        errors: [],\n        keys: [],\n      });\n\n      await new Promise((res) => setTimeout(res, 100));\n\n      expect(runnerRunSpy).toHaveBeenCalledTimes(2);\n      expect(bulkAction['status']).toEqual(BulkActionStatus.Completed);\n    });\n    it('should start and run until the error occur', async () => {\n      bulkAction['status'] = BulkActionStatus.Ready;\n      bulkAction['runners'] = [mockRunner];\n      runnerRunSpy.mockRejectedValue();\n\n      const overview = await bulkAction.start();\n\n      expect(overview.id).toEqual(mockCreateBulkActionDto.id);\n      expect(overview.type).toEqual(mockCreateBulkActionDto.type);\n      expect(overview.duration).toBeGreaterThanOrEqual(0);\n      expect(overview.filter).toEqual(omit(mockBulkActionFilter, 'count'));\n      expect(overview.progress).toEqual({\n        total: 0,\n        scanned: 0,\n      });\n      expect(overview.summary).toEqual({\n        processed: 0,\n        succeed: 0,\n        failed: 0,\n        errors: [],\n        keys: [],\n      });\n\n      await new Promise((res) => setTimeout(res, 100));\n\n      expect(bulkAction.getStatus()).toEqual(BulkActionStatus.Failed);\n    });\n  });\n\n  describe('getOverview', () => {\n    let mockSummary;\n    let mockProgress;\n\n    beforeEach(() => {\n      mockSummary = generateMockBulkActionSummary();\n      mockProgress = generateMockBulkActionProgress();\n      mockRunner = new DeleteBulkActionSimpleRunner(\n        bulkAction,\n        mockStandaloneRedisClient,\n      );\n      mockRunner['progress'] = mockProgress;\n      mockRunner['summary'] = mockSummary;\n      bulkAction['status'] = BulkActionStatus.Completed;\n    });\n\n    it('should return overview for standalone', async () => {\n      bulkAction['runners'] = [mockRunner];\n\n      const overview = await bulkAction.getOverview();\n\n      expect(overview.id).toEqual(mockCreateBulkActionDto.id);\n      expect(overview.type).toEqual(mockCreateBulkActionDto.type);\n      expect(overview.duration).toBeGreaterThanOrEqual(0);\n      expect(overview.filter).toEqual(omit(mockBulkActionFilter, 'count'));\n      expect(overview.progress).toEqual({\n        total: 1_000_000,\n        scanned: 1_000_000,\n      });\n      expect(overview.summary).toEqual({\n        processed: 1_000_000,\n        succeed: 900_000,\n        failed: 100_000,\n        errors: generateMockBulkActionErrors(500, false),\n        keys: [],\n      });\n    });\n    it('should return overview for cluster', async () => {\n      bulkAction['runners'] = [mockRunner, mockRunner, mockRunner];\n\n      const overview = await bulkAction.getOverview();\n\n      expect(overview.id).toEqual(mockCreateBulkActionDto.id);\n      expect(overview.type).toEqual(mockCreateBulkActionDto.type);\n      expect(overview.duration).toBeGreaterThanOrEqual(0);\n      expect(overview.filter).toEqual(omit(mockBulkActionFilter, 'count'));\n      expect(overview.progress).toEqual({\n        total: 3_000_000,\n        scanned: 3_000_000,\n      });\n      expect(overview.summary).toEqual({\n        processed: 3_000_000,\n        succeed: 2_700_000,\n        failed: 300_000,\n        errors: generateMockBulkActionErrors(500, false),\n        keys: [],\n      });\n    });\n  });\n\n  describe('getOverview - keys aggregation', () => {\n    let mockSummary1;\n    let mockSummary2;\n    let mockSummary3;\n    let mockRunner1;\n    let mockRunner2;\n    let mockRunner3;\n    let mockProgress1;\n    let mockProgress2;\n    let mockProgress3;\n\n    beforeEach(() => {\n      mockProgress1 = {\n        getOverview: jest.fn().mockReturnValue({ total: 500, scanned: 400 }),\n      };\n      mockProgress2 = {\n        getOverview: jest.fn().mockReturnValue({ total: 1000, scanned: 800 }),\n      };\n      mockProgress3 = {\n        getOverview: jest.fn().mockReturnValue({ total: 1500, scanned: 1200 }),\n      };\n\n      mockSummary1 = {\n        getOverview: jest.fn().mockReturnValue({\n          processed: 100,\n          succeed: 90,\n          failed: 10,\n          errors: [],\n          keys: [],\n        }),\n      };\n      mockSummary2 = {\n        getOverview: jest.fn().mockReturnValue({\n          processed: 200,\n          succeed: 180,\n          failed: 20,\n          errors: [],\n          keys: [],\n        }),\n      };\n      mockSummary3 = {\n        getOverview: jest.fn().mockReturnValue({\n          processed: 300,\n          succeed: 270,\n          failed: 30,\n          errors: [],\n          keys: [],\n        }),\n      };\n\n      mockRunner1 = {\n        getSummary: jest.fn().mockReturnValue(mockSummary1),\n        getProgress: jest.fn().mockReturnValue(mockProgress1),\n      };\n      mockRunner2 = {\n        getSummary: jest.fn().mockReturnValue(mockSummary2),\n        getProgress: jest.fn().mockReturnValue(mockProgress2),\n      };\n      mockRunner3 = {\n        getSummary: jest.fn().mockReturnValue(mockSummary3),\n        getProgress: jest.fn().mockReturnValue(mockProgress3),\n      };\n\n      bulkAction['runners'] = [mockRunner1, mockRunner2, mockRunner3];\n      bulkAction['status'] = BulkActionStatus.Completed;\n    });\n\n    it('should correctly aggregate summary data from all runners', async () => {\n      const overview = bulkAction.getOverview();\n\n      expect(overview.summary.processed).toBeGreaterThan(0);\n      expect(overview.summary.succeed).toBeGreaterThan(0);\n      expect(overview.summary.failed).toBeGreaterThan(0);\n      expect(Array.isArray(overview.summary.errors)).toBe(true);\n    });\n  });\n\n  describe('setStatus', () => {\n    const testCases = [\n      { input: BulkActionStatus.Completed, affect: true },\n      { input: BulkActionStatus.Failed, affect: true },\n      { input: BulkActionStatus.Aborted, affect: true },\n      { input: BulkActionStatus.Initializing, affect: false },\n      { input: BulkActionStatus.Initialized, affect: false },\n      { input: BulkActionStatus.Preparing, affect: false },\n      { input: BulkActionStatus.Ready, affect: false },\n    ];\n\n    testCases.forEach((testCase) => {\n      it(`should change state to ${testCase.input} and ${testCase.affect ? '' : 'do not'} set a time`, () => {\n        expect(bulkAction['status']).toEqual(BulkActionStatus.Initialized);\n        expect(bulkAction['endTime']).toEqual(undefined);\n\n        bulkAction.setStatus(testCase.input);\n\n        expect(bulkAction['status']).toEqual(testCase.input);\n        if (testCase.affect) {\n          expect(bulkAction['endTime']).not.toEqual(undefined);\n        } else {\n          expect(bulkAction['endTime']).toEqual(undefined);\n        }\n      });\n    });\n\n    const currentStatusTestCases = [\n      { input: BulkActionStatus.Completed, ignore: true },\n      { input: BulkActionStatus.Failed, ignore: true },\n      { input: BulkActionStatus.Aborted, ignore: true },\n      { input: BulkActionStatus.Initialized, ignore: false },\n      { input: BulkActionStatus.Preparing, ignore: false },\n      { input: BulkActionStatus.Ready, ignore: false },\n    ];\n\n    currentStatusTestCases.forEach((testCase) => {\n      it(`should ${testCase.ignore ? 'not' : ''} change state from ${testCase.input}`, () => {\n        expect(bulkAction['status']).toEqual(BulkActionStatus.Initialized);\n        bulkAction.setStatus(testCase.input);\n        expect(bulkAction['status']).toEqual(testCase.input);\n\n        bulkAction.setStatus(BulkActionStatus.Running);\n\n        if (testCase.ignore) {\n          expect(bulkAction['status']).toEqual(testCase.input);\n        } else {\n          expect(bulkAction['status']).toEqual(BulkActionStatus.Running);\n        }\n      });\n    });\n  });\n\n  describe('sendOverview', () => {\n    let sendOverviewSpy;\n\n    beforeEach(() => {\n      sendOverviewSpy = jest.spyOn(bulkAction, 'sendOverview');\n    });\n\n    it('Should not fail on emit error', () => {\n      mockSocket.emit.mockImplementation(() => {\n        throw new Error('some error');\n      });\n\n      bulkAction.sendOverview(mockSessionMetadata);\n    });\n    it('Should send overview', () => {\n      mockSocket.emit.mockReturnValue();\n\n      bulkAction.sendOverview(mockSessionMetadata);\n\n      expect(sendOverviewSpy).toHaveBeenCalledTimes(1);\n    });\n\n    it('Should call sendActionSucceed', () => {\n      mockSocket.emit.mockReturnValue();\n\n      bulkAction['status'] = BulkActionStatus.Completed;\n\n      bulkAction.sendOverview(mockSessionMetadata);\n\n      expect(sendOverviewSpy).toHaveBeenCalledTimes(1);\n      expect(analytics.sendActionFailed).not.toHaveBeenCalled();\n      expect(analytics.sendActionStopped).not.toHaveBeenCalled();\n      expect(analytics.sendActionSucceed).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockBulkActionOverviewMatcher,\n      );\n    });\n\n    it('Should call sendActionFailed', () => {\n      mockSocket.emit.mockReturnValue();\n\n      bulkAction['status'] = BulkActionStatus.Failed;\n      bulkAction['error'] = new Error('some error');\n\n      bulkAction.sendOverview(mockSessionMetadata);\n\n      expect(sendOverviewSpy).toHaveBeenCalledTimes(1);\n      expect(analytics.sendActionSucceed).not.toHaveBeenCalled();\n      expect(analytics.sendActionStopped).not.toHaveBeenCalled();\n      expect(analytics.sendActionFailed).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockBulkActionOverviewMatcher,\n          status: 'failed',\n          error: 'some error',\n        },\n        new Error('some error'),\n      );\n    });\n\n    it('Should call sendActionStopped', () => {\n      mockSocket.emit.mockReturnValue();\n\n      bulkAction['status'] = BulkActionStatus.Aborted;\n\n      bulkAction.sendOverview(mockSessionMetadata);\n\n      expect(sendOverviewSpy).toHaveBeenCalledTimes(1);\n      expect(analytics.sendActionSucceed).not.toHaveBeenCalled();\n      expect(analytics.sendActionFailed).not.toHaveBeenCalled();\n      expect(analytics.sendActionStopped).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockBulkActionOverviewMatcher,\n          status: 'aborted',\n        },\n      );\n    });\n  });\n\n  describe('Other', () => {\n    it('getters', () => {\n      expect(bulkAction.getSocket()).toEqual(bulkAction['socket']);\n      expect(bulkAction.getFilter()).toEqual(bulkAction['filter']);\n      expect(bulkAction.getId()).toEqual(bulkAction['id']);\n    });\n  });\n\n  describe('Report streaming', () => {\n    let mockResponse: any;\n\n    beforeEach(() => {\n      mockResponse = {\n        write: jest.fn(),\n        end: jest.fn(),\n      };\n    });\n\n    describe('isReportEnabled', () => {\n      it('should return false when generateReport is not set', () => {\n        expect(bulkAction.isReportEnabled()).toBe(false);\n      });\n\n      it('should return true when generateReport is true', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n        expect(bulkActionWithReport.isReportEnabled()).toBe(true);\n      });\n    });\n\n    describe('setStreamingResponse', () => {\n      it('should set streaming response and resolve promise when waiting', async () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        // Start waiting for stream\n        const waitPromise = bulkActionWithReport['waitForStreamIfNeeded']();\n\n        // Set streaming response\n        bulkActionWithReport.setStreamingResponse(mockResponse);\n\n        await waitPromise;\n\n        expect(bulkActionWithReport['streamingResponse']).toBe(mockResponse);\n      });\n\n      it('should write header when response is set', async () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        // Start waiting for stream\n        const waitPromise = bulkActionWithReport['waitForStreamIfNeeded']();\n\n        // Set streaming response\n        bulkActionWithReport.setStreamingResponse(mockResponse);\n\n        await waitPromise;\n\n        expect(mockResponse.write).toHaveBeenCalled();\n        const headerCall = mockResponse.write.mock.calls[0][0];\n        expect(headerCall).toContain('Bulk Delete Report');\n        expect(headerCall).toContain('Command Executed for each key:');\n      });\n\n      it('should immediately end response when called without waiting', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        // Call setStreamingResponse without waitForStreamIfNeeded being called first\n        bulkActionWithReport.setStreamingResponse(mockResponse);\n\n        // Should immediately end the response\n        expect(mockResponse.write).toHaveBeenCalledWith(\n          'Unable to generate report. Please try again.\\n',\n        );\n        expect(mockResponse.end).toHaveBeenCalled();\n        expect(bulkActionWithReport['streamingResponse']).toBeNull();\n      });\n    });\n\n    describe('writeToReport', () => {\n      it('should not write when streaming response is not set', () => {\n        bulkAction.writeToReport(Buffer.from('testKey'), true);\n        // No error thrown, just no-op\n      });\n\n      it('should write success entry to stream', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n        bulkActionWithReport['streamingResponse'] = mockResponse;\n\n        bulkActionWithReport.writeToReport(Buffer.from('testKey'), true);\n\n        expect(mockResponse.write).toHaveBeenCalledWith('testKey - OK\\n');\n      });\n\n      it('should write error entry to stream', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n        bulkActionWithReport['streamingResponse'] = mockResponse;\n\n        bulkActionWithReport.writeToReport(\n          Buffer.from('testKey'),\n          false,\n          'NOPERM',\n        );\n\n        expect(mockResponse.write).toHaveBeenCalledWith(\n          'testKey - Error: NOPERM\\n',\n        );\n      });\n\n      it('should write unknown error when error message not provided', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n        bulkActionWithReport['streamingResponse'] = mockResponse;\n\n        bulkActionWithReport.writeToReport(Buffer.from('testKey'), false);\n\n        expect(mockResponse.write).toHaveBeenCalledWith(\n          'testKey - Error: Unknown error\\n',\n        );\n      });\n    });\n\n    describe('finalizeReport', () => {\n      it('should not finalize when streaming response is not set', () => {\n        bulkAction['finalizeReport']();\n        // No error thrown, just no-op\n      });\n\n      it('should write summary and close stream', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n        bulkActionWithReport['streamingResponse'] = mockResponse;\n        bulkActionWithReport['status'] = BulkActionStatus.Completed;\n\n        bulkActionWithReport['finalizeReport']();\n\n        expect(mockResponse.write).toHaveBeenCalled();\n        expect(mockResponse.end).toHaveBeenCalled();\n\n        const summaryCall = mockResponse.write.mock.calls[0][0];\n        expect(summaryCall).toContain('Summary');\n        expect(summaryCall).toContain('Processed:');\n        expect(summaryCall).toContain('Succeeded:');\n        expect(summaryCall).toContain('Failed:');\n        expect(summaryCall).toContain('Status:');\n      });\n    });\n\n    describe('getOverview with downloadUrl', () => {\n      it('should not include downloadUrl when generateReport is false', () => {\n        const overview = bulkAction.getOverview();\n        expect(overview.downloadUrl).toBeUndefined();\n      });\n\n      it('should include downloadUrl when generateReport is true', () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        const overview = bulkActionWithReport.getOverview();\n\n        expect(overview.downloadUrl).toBe(\n          `databases/${mockCreateBulkActionDto.databaseId}/bulk-actions/${mockCreateBulkActionDto.id}/report/download`,\n        );\n      });\n    });\n\n    describe('waitForStreamIfNeeded', () => {\n      it('should resolve immediately when generateReport is false', async () => {\n        await bulkAction['waitForStreamIfNeeded']();\n        // Should not hang\n      });\n\n      it('should wait for stream when generateReport is true', async () => {\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        // Start waiting in background\n        const waitPromise = bulkActionWithReport['waitForStreamIfNeeded']();\n\n        // Set streaming response after a short delay\n        setTimeout(() => {\n          bulkActionWithReport.setStreamingResponse(mockResponse);\n        }, 10);\n\n        // Wait should resolve after setStreamingResponse is called\n        await waitPromise;\n\n        expect(bulkActionWithReport['streamingResponse']).toBe(mockResponse);\n      });\n\n      it('should reject with timeout error when stream is not set in time', async () => {\n        jest.useFakeTimers();\n\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        const waitPromise = bulkActionWithReport['waitForStreamIfNeeded']();\n\n        jest.advanceTimersByTime(BulkAction['STREAM_TIMEOUT_MS'] + 100);\n\n        await expect(waitPromise).rejects.toThrow(\n          'Unable to start report download. Please try again.',\n        );\n\n        jest.useRealTimers();\n      });\n\n      it('should clear timeout when stream is set before timeout', async () => {\n        jest.useFakeTimers();\n\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        const waitPromise = bulkActionWithReport['waitForStreamIfNeeded']();\n\n        jest.advanceTimersByTime(100);\n        bulkActionWithReport.setStreamingResponse(mockResponse);\n\n        await waitPromise;\n\n        jest.advanceTimersByTime(BulkAction['STREAM_TIMEOUT_MS']);\n\n        expect(bulkActionWithReport['streamingResponse']).toBe(mockResponse);\n\n        jest.useRealTimers();\n      });\n\n      it('should immediately end response when stream arrives after timeout', async () => {\n        jest.useFakeTimers();\n\n        const bulkActionWithReport = new BulkAction(\n          mockCreateBulkActionDto.id,\n          mockCreateBulkActionDto.databaseId,\n          mockCreateBulkActionDto.type,\n          mockBulkActionFilter,\n          mockSocket,\n          mockBulkActionsAnalytics() as any,\n          true,\n        );\n\n        const waitPromise = bulkActionWithReport['waitForStreamIfNeeded']();\n\n        // Let the timeout expire\n        jest.advanceTimersByTime(BulkAction['STREAM_TIMEOUT_MS'] + 100);\n\n        await expect(waitPromise).rejects.toThrow(\n          'Unable to start report download. Please try again.',\n        );\n\n        // Now the late stream arrives\n        const lateResponse = {\n          write: jest.fn(),\n          end: jest.fn(),\n        };\n        bulkActionWithReport.setStreamingResponse(lateResponse as any);\n\n        // Should immediately write error and end response\n        expect(lateResponse.write).toHaveBeenCalledWith(\n          'Unable to generate report. Please try again.\\n',\n        );\n        expect(lateResponse.end).toHaveBeenCalled();\n\n        // Should NOT set the streaming response\n        expect(bulkActionWithReport['streamingResponse']).toBeNull();\n\n        jest.useRealTimers();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts",
    "content": "import { debounce } from 'lodash';\nimport { Response } from 'express';\nimport {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { Socket } from 'socket.io';\nimport { Logger } from '@nestjs/common';\nimport {\n  IBulkAction,\n  IBulkActionRunner,\n} from 'src/modules/bulk-actions/interfaces';\nimport { IBulkActionOverview } from 'src/modules/bulk-actions/interfaces/bulk-action-overview.interface';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport { RedisClient, RedisClientNodeRole } from 'src/modules/redis/client';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class BulkAction implements IBulkAction {\n  private logger: Logger = new Logger('BulkAction');\n\n  private startTime: number = Date.now();\n\n  private endTime: number;\n\n  private error: Error;\n\n  private status: BulkActionStatus;\n\n  private runners: IBulkActionRunner[] = [];\n\n  private readonly debounce: Function;\n\n  private readonly generateReport: boolean;\n\n  private streamingResponse: Response | null = null;\n\n  private streamReadyResolver: (() => void) | null = null;\n\n  constructor(\n    private readonly id: string,\n    private readonly databaseId: string,\n    private readonly type: BulkActionType,\n    private readonly filter: BulkActionFilter,\n    private readonly socket: Socket,\n    private readonly analytics: BulkActionsAnalytics,\n    generateReport: boolean = false,\n  ) {\n    this.debounce = debounce(this.sendOverview.bind(this), 1000, {\n      maxWait: 1000,\n    });\n    this.status = BulkActionStatus.Initialized;\n    this.generateReport = generateReport;\n  }\n\n  /**\n   * Setup runners and fetch total keys once before run\n   * @param redisClient\n   * @param RunnerClassName\n   */\n  async prepare(redisClient: RedisClient, RunnerClassName) {\n    if (this.status !== BulkActionStatus.Initialized) {\n      throw new Error(\n        `Unable to prepare bulk action with \"${this.status}\" status`,\n      );\n    }\n\n    this.status = BulkActionStatus.Preparing;\n\n    this.runners = (await redisClient.nodes(RedisClientNodeRole.PRIMARY)).map(\n      (node) => new RunnerClassName(this, node),\n    );\n\n    await Promise.all(this.runners.map((runner) => runner.prepareToStart()));\n\n    this.status = BulkActionStatus.Ready;\n  }\n\n  /**\n   * Start bulk operation in case if it was prepared before only\n   */\n  async start() {\n    if (this.status !== BulkActionStatus.Ready) {\n      throw new Error(\n        `Unable to start bulk action with \"${this.status}\" status`,\n      );\n    }\n\n    this.run().catch();\n\n    return this.getOverview();\n  }\n\n  /**\n   * Run bulk action on each runner\n   * @private\n   */\n  private async run() {\n    try {\n      // Wait for streaming response to be attached if report generation is enabled\n      await this.waitForStreamIfNeeded();\n\n      this.setStatus(BulkActionStatus.Running);\n\n      await Promise.all(this.runners.map((runner) => runner.run()));\n\n      this.setStatus(BulkActionStatus.Completed);\n    } catch (e) {\n      this.logger.error('Error on BulkAction Runner', e);\n      this.error = e;\n      this.setStatus(BulkActionStatus.Failed);\n    } finally {\n      this.finalizeReport();\n    }\n  }\n\n  private static readonly STREAM_TIMEOUT_MS = 5_000;\n\n  private async waitForStreamIfNeeded(): Promise<void> {\n    if (!this.generateReport) {\n      return;\n    }\n\n    if (this.streamingResponse) {\n      return;\n    }\n\n    return new Promise((resolve, reject) => {\n      const timeout = setTimeout(() => {\n        this.streamReadyResolver = null;\n        reject(new Error('Unable to start report download. Please try again.'));\n      }, BulkAction.STREAM_TIMEOUT_MS);\n\n      this.streamReadyResolver = () => {\n        clearTimeout(timeout);\n        resolve();\n      };\n    });\n  }\n\n  isReportEnabled(): boolean {\n    return this.generateReport;\n  }\n\n  setStreamingResponse(res: Response): void {\n    // If stream arrives too late (after timeout/failure), immediately end it\n    if (!this.streamReadyResolver) {\n      res.write('Unable to generate report. Please try again.\\n');\n      res.end();\n      return;\n    }\n\n    this.streamingResponse = res;\n\n    this.writeReportHeader();\n\n    this.streamReadyResolver();\n    this.streamReadyResolver = null;\n  }\n\n  private writeReportHeader(): void {\n    if (!this.streamingResponse) return;\n\n    const header = [\n      'Bulk Delete Report',\n      `Command Executed for each key: ${this.type.toUpperCase()} key_name`,\n      'A summary is provided at the end of this file.',\n      '==================',\n      '',\n    ].join('\\n');\n\n    this.streamingResponse.write(header);\n  }\n\n  writeToReport(keyName: Buffer, success: boolean, error?: string): void {\n    if (!this.streamingResponse) return;\n\n    const keyNameStr = keyName.toString();\n    const line = success\n      ? `${keyNameStr} - OK\\n`\n      : `${keyNameStr} - Error: ${error || 'Unknown error'}\\n`;\n\n    this.streamingResponse.write(line);\n  }\n\n  private finalizeReport(): void {\n    if (!this.streamingResponse) return;\n\n    const overview = this.getOverview();\n\n    const footer = [\n      '',\n      '=============',\n      'Summary:',\n      '=============',\n      `Status: ${overview.status}`,\n      `Processed: ${overview.summary.processed} keys`,\n      `Succeeded: ${overview.summary.succeed} keys`,\n      `Failed: ${overview.summary.failed} keys`,\n      '',\n    ].join('\\n');\n\n    this.streamingResponse.write(footer);\n    this.streamingResponse.end();\n  }\n\n  getOverview(): IBulkActionOverview {\n    const progress = this.runners\n      .map((runner) => runner.getProgress().getOverview())\n      .reduce(\n        (cur, prev) => ({\n          total: prev.total + cur.total,\n          scanned: prev.scanned + cur.scanned,\n        }),\n        {\n          total: 0,\n          scanned: 0,\n        },\n      );\n\n    const summary = this.runners\n      .map((runner) => runner.getSummary().getOverview())\n      .reduce(\n        (cur, prev) => ({\n          processed: prev.processed + cur.processed,\n          succeed: prev.succeed + cur.succeed,\n          failed: prev.failed + cur.failed,\n          errors: prev.errors.concat(cur.errors),\n          keys: [...prev.keys, ...cur.keys],\n        }),\n        {\n          processed: 0,\n          succeed: 0,\n          failed: 0,\n          errors: [],\n          keys: [],\n        },\n      );\n\n    summary.errors = summary.errors.slice(0, 500).map((error) => ({\n      key: error.key.toString(),\n      error: error.error.toString(),\n    }));\n\n    const overview: IBulkActionOverview = {\n      id: this.id,\n      databaseId: this.databaseId,\n      type: this.type,\n      duration: (this.endTime || Date.now()) - this.startTime,\n      status: this.status,\n      filter: this.filter.getOverview(),\n      progress,\n      summary,\n    };\n\n    if (this.generateReport) {\n      overview.downloadUrl = this.getDownloadUrl();\n    }\n\n    if (this.error) {\n      overview.error = this.error.message;\n    }\n\n    return overview;\n  }\n\n  private getDownloadUrl(): string {\n    return `databases/${this.databaseId}/bulk-actions/${this.id}/report/download`;\n  }\n\n  getId() {\n    return this.id;\n  }\n\n  getStatus(): BulkActionStatus {\n    return this.status;\n  }\n\n  setStatus(status) {\n    switch (this.status) {\n      case BulkActionStatus.Completed:\n      case BulkActionStatus.Failed:\n      case BulkActionStatus.Aborted:\n        return;\n      default:\n        this.status = status;\n    }\n\n    switch (status) {\n      case BulkActionStatus.Aborted:\n      case BulkActionStatus.Failed:\n      case BulkActionStatus.Completed:\n        if (!this.endTime) {\n          this.endTime = Date.now();\n        }\n        // Queue the state change, then flush immediately for terminal states\n        this.changeState();\n        (this.debounce as ReturnType<typeof debounce>).flush();\n        break;\n      default:\n        this.changeState();\n    }\n  }\n\n  getFilter(): BulkActionFilter {\n    return this.filter;\n  }\n\n  getSocket(): Socket {\n    return this.socket;\n  }\n\n  changeState() {\n    this.debounce();\n  }\n\n  /**\n   * Send overview to a client\n   * @param sessionMetadata\n   */\n  sendOverview(sessionMetadata: SessionMetadata) {\n    const overview = this.getOverview();\n    if (overview.status === BulkActionStatus.Completed) {\n      this.analytics.sendActionSucceed(sessionMetadata, overview);\n    }\n    if (overview.status === BulkActionStatus.Failed) {\n      this.analytics.sendActionFailed(sessionMetadata, overview, this.error);\n    }\n    if (overview.status === BulkActionStatus.Aborted) {\n      this.analytics.sendActionStopped(sessionMetadata, overview);\n    }\n    try {\n      this.socket.emit('overview', overview);\n    } catch (e) {\n      this.logger.error('Unable to send overview', e, sessionMetadata);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts",
    "content": "import {\n  mockSocket,\n  mockBulkActionsAnalytics,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { BulkActionType } from 'src/modules/bulk-actions/constants';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { BulkActionProgress } from 'src/modules/bulk-actions/models/bulk-action-progress';\nimport { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\n\nconst mockBulkActionFilter = new BulkActionFilter();\n\nconst mockCreateBulkActionDto = {\n  id: 'bulk-action-id',\n  databaseId: 'database-id',\n  type: BulkActionType.Delete,\n};\n\nlet bulkAction;\n\ndescribe('AbstractBulkActionRunner', () => {\n  const client = mockStandaloneRedisClient;\n  let deleteRunner: DeleteBulkActionSimpleRunner;\n\n  beforeEach(() => {\n    bulkAction = new BulkAction(\n      mockCreateBulkActionDto.id,\n      mockCreateBulkActionDto.databaseId,\n      mockCreateBulkActionDto.type,\n      mockBulkActionFilter,\n      mockSocket,\n      mockBulkActionsAnalytics as any,\n    );\n\n    deleteRunner = new DeleteBulkActionSimpleRunner(bulkAction, client);\n  });\n\n  describe('getProgress + getSummary', () => {\n    it('should get progress', async () => {\n      expect(deleteRunner.getProgress()).toBeInstanceOf(BulkActionProgress);\n      expect(deleteRunner.getSummary()).toBeInstanceOf(BulkActionSummary);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.ts",
    "content": "import { BulkActionProgress } from 'src/modules/bulk-actions/models/bulk-action-progress';\nimport { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary';\nimport {\n  IBulkAction,\n  IBulkActionRunner,\n} from 'src/modules/bulk-actions/interfaces';\n\nexport abstract class AbstractBulkActionRunner implements IBulkActionRunner {\n  protected bulkAction: IBulkAction;\n\n  protected progress: BulkActionProgress;\n\n  protected summary: BulkActionSummary;\n\n  protected constructor(bulkAction) {\n    this.bulkAction = bulkAction;\n    this.progress = new BulkActionProgress();\n    this.summary = new BulkActionSummary();\n  }\n\n  /**\n   * Assign node client and calculate total keys\n   * before run and not recalculate on any iteration\n   */\n  abstract prepareToStart();\n\n  /**\n   * Start bulk operation\n   */\n  abstract run();\n\n  getProgress(): BulkActionProgress {\n    return this.progress;\n  }\n\n  getSummary() {\n    return this.summary;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts",
    "content": "import { mockRedisKeysUtilModule } from 'src/__mocks__/redis-utils';\n\njest.doMock('src/modules/redis/utils/keys.util', mockRedisKeysUtilModule);\n\nimport {\n  mockSocket,\n  mockBulkActionsAnalytics,\n  mockRedisKeysUtil,\n  mockBulkActionFilter,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\n\nconst mockCreateBulkActionDto = {\n  id: 'bulk-action-id',\n  databaseId: 'database-id',\n  type: BulkActionType.Delete,\n};\n\nlet bulkAction;\n\nconst mockKey = 'mockedKey';\nconst mockKeyBuffer = Buffer.from(mockKey);\nconst mockCursorString = '12345';\nconst mockCursorNumber = parseInt(mockCursorString, 10);\nconst mockCursorBuffer = Buffer.from(mockCursorString);\nconst mockZeroCursorBuffer = Buffer.from('0');\nconst mockRESPError = 'Reply Error: NOPERM for delete.';\nconst mockReplyError = new Error(mockRESPError);\n\ndescribe('AbstractBulkActionSimpleRunner', () => {\n  let deleteRunner: DeleteBulkActionSimpleRunner;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n\n    bulkAction = new BulkAction(\n      mockCreateBulkActionDto.id,\n      mockCreateBulkActionDto.databaseId,\n      mockCreateBulkActionDto.type,\n      mockBulkActionFilter,\n      mockSocket,\n      mockBulkActionsAnalytics as any,\n    );\n\n    deleteRunner = new DeleteBulkActionSimpleRunner(\n      bulkAction,\n      mockStandaloneRedisClient,\n    );\n  });\n\n  describe('prepareToStart', () => {\n    it('should get total before start', async () => {\n      mockRedisKeysUtil.getTotalKeys.mockResolvedValue(100);\n\n      expect(deleteRunner['progress']['total']).toEqual(0);\n      expect(deleteRunner['progress']['scanned']).toEqual(0);\n      expect(deleteRunner['progress']['cursor']).toEqual(0);\n\n      await deleteRunner.prepareToStart();\n\n      expect(deleteRunner['progress']['total']).toEqual(100);\n      expect(deleteRunner['progress']['scanned']).toEqual(0);\n      expect(deleteRunner['progress']['cursor']).toEqual(0);\n    });\n  });\n\n  describe('getKeysToProcess', () => {\n    beforeEach(() => {\n      deleteRunner['bulkAction']['status'] = BulkActionStatus.Running;\n      deleteRunner['progress']['total'] = 1_000_000;\n    });\n\n    it('Should get keys to process and change cursor', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockCursorBuffer,\n        [mockKeyBuffer, mockKeyBuffer],\n      ]);\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockCursorBuffer,\n        [mockKeyBuffer, mockKeyBuffer],\n      ]);\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockZeroCursorBuffer,\n        [mockKeyBuffer, mockKeyBuffer],\n      ]);\n\n      expect(deleteRunner['progress']['cursor']).toEqual(0);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      await deleteRunner.getKeysToProcess();\n\n      expect(deleteRunner['progress']['cursor']).toEqual(mockCursorNumber);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      await deleteRunner.getKeysToProcess();\n\n      expect(deleteRunner['progress']['cursor']).toEqual(mockCursorNumber);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      await deleteRunner.getKeysToProcess();\n\n      expect(deleteRunner['progress']['cursor']).toEqual(-1);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      await deleteRunner.getKeysToProcess();\n\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledTimes(3);\n    });\n  });\n\n  describe('runIteration', () => {\n    beforeEach(() => {\n      deleteRunner['bulkAction']['status'] = BulkActionStatus.Running;\n      deleteRunner['progress']['total'] = 1_000_000;\n    });\n\n    it('Should get keys to process and change cursor', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockCursorBuffer,\n        [mockKeyBuffer, mockKeyBuffer],\n      ]);\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockCursorBuffer,\n        [mockKeyBuffer, mockKeyBuffer],\n      ]);\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockZeroCursorBuffer,\n        [mockKeyBuffer, mockKeyBuffer],\n      ]);\n      mockStandaloneRedisClient.sendPipeline.mockResolvedValue([\n        [null, 1],\n        [mockReplyError, null],\n      ]);\n\n      expect(deleteRunner['progress']['cursor']).toEqual(0);\n      expect(deleteRunner['progress']['scanned']).toEqual(0);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      expect(deleteRunner['summary']['processed']).toEqual(0);\n      expect(deleteRunner['summary']['succeed']).toEqual(0);\n      expect(deleteRunner['summary']['failed']).toEqual(0);\n\n      await deleteRunner.runIteration();\n\n      expect(deleteRunner['progress']['cursor']).toEqual(mockCursorNumber);\n      expect(deleteRunner['progress']['scanned']).toEqual(10_000);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      expect(deleteRunner['summary']['processed']).toEqual(2);\n      expect(deleteRunner['summary']['succeed']).toEqual(1);\n      expect(deleteRunner['summary']['failed']).toEqual(1);\n\n      await deleteRunner.runIteration();\n\n      expect(deleteRunner['progress']['cursor']).toEqual(mockCursorNumber);\n      expect(deleteRunner['progress']['scanned']).toEqual(20_000);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      expect(deleteRunner['summary']['processed']).toEqual(4);\n      expect(deleteRunner['summary']['succeed']).toEqual(2);\n      expect(deleteRunner['summary']['failed']).toEqual(2);\n\n      await deleteRunner.runIteration();\n\n      expect(deleteRunner['progress']['cursor']).toEqual(-1);\n      expect(deleteRunner['progress']['scanned']).toEqual(1_000_000);\n      expect(deleteRunner['progress']['total']).toEqual(1_000_000);\n\n      expect(deleteRunner['summary']['processed']).toEqual(6);\n      expect(deleteRunner['summary']['succeed']).toEqual(3);\n      expect(deleteRunner['summary']['failed']).toEqual(3);\n    });\n  });\n\n  describe('run', () => {\n    let runIterationSpy;\n\n    beforeEach(() => {\n      runIterationSpy = jest.spyOn(deleteRunner, 'runIteration');\n      runIterationSpy.mockImplementation(async () => {\n        await new Promise((res) => {\n          setTimeout(() => res(''), 50);\n        });\n      });\n      deleteRunner['bulkAction']['status'] = BulkActionStatus.Running;\n    });\n\n    it('should should run if cursor 0 and status is Running and stop on status change', async () => {\n      expect(deleteRunner['progress']['cursor']).toEqual(0);\n      expect(deleteRunner['bulkAction']['status']).toEqual(\n        BulkActionStatus.Running,\n      );\n      setTimeout(() => {\n        deleteRunner['bulkAction']['status'] = BulkActionStatus.Aborted;\n      }, 90);\n      await deleteRunner.run();\n\n      expect(runIterationSpy).toHaveBeenCalledTimes(2);\n    });\n\n    it('should should run if cursor 0 and status is Running and stop on wen cursor -1', async () => {\n      expect(deleteRunner['progress']['cursor']).toEqual(0);\n      expect(deleteRunner['bulkAction']['status']).toEqual(\n        BulkActionStatus.Running,\n      );\n      setTimeout(() => {\n        deleteRunner['progress']['cursor'] = -1;\n      }, 90);\n      await deleteRunner.run();\n\n      expect(runIterationSpy).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('processIterationResults', () => {\n    let addProcessedSpy;\n    let addSuccessSpy;\n    let addFailedSpy;\n    let writeToReportSpy;\n\n    beforeEach(() => {\n      addProcessedSpy = jest.spyOn(deleteRunner['summary'], 'addProcessed');\n      addSuccessSpy = jest.spyOn(deleteRunner['summary'], 'addSuccess');\n      addFailedSpy = jest.spyOn(deleteRunner['summary'], 'addFailed');\n      writeToReportSpy = jest.spyOn(bulkAction, 'writeToReport');\n    });\n\n    it('should correctly process results and update summary counters', () => {\n      const keys = [\n        Buffer.from('key1'),\n        Buffer.from('key2'),\n        Buffer.from('key3'),\n      ];\n      const results: [Error | null, number | null][] = [\n        [null, 1], // Success\n        [mockReplyError, null], // Error\n        [null, 1], // Success\n      ];\n\n      deleteRunner.processIterationResults(keys, results);\n\n      expect(addProcessedSpy).toHaveBeenCalledWith(3);\n\n      expect(addSuccessSpy).toHaveBeenNthCalledWith(1, 1); // first call\n      expect(addSuccessSpy).toHaveBeenNthCalledWith(2, 1); // second call\n      expect(addFailedSpy).toHaveBeenCalledWith(1);\n    });\n\n    it('should call writeToReport for each key result', () => {\n      const keys = [\n        Buffer.from('key1'),\n        Buffer.from('key2'),\n        Buffer.from('key3'),\n      ];\n      const results: [Error | null, number | null][] = [\n        [null, 1], // Success\n        [mockReplyError, null], // Error\n        [null, 1], // Success\n      ];\n\n      deleteRunner.processIterationResults(keys, results);\n\n      expect(writeToReportSpy).toHaveBeenCalledTimes(3);\n      expect(writeToReportSpy).toHaveBeenNthCalledWith(1, keys[0], true);\n      expect(writeToReportSpy).toHaveBeenNthCalledWith(\n        2,\n        keys[1],\n        false,\n        mockRESPError,\n      );\n      expect(writeToReportSpy).toHaveBeenNthCalledWith(3, keys[2], true);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.ts",
    "content": "import { BulkActionStatus } from 'src/modules/bulk-actions/constants';\nimport { AbstractBulkActionRunner } from 'src/modules/bulk-actions/models/runners/abstract.bulk-action.runner';\nimport {\n  RedisClient,\n  RedisClientCommand,\n  RedisClientCommandReply,\n} from 'src/modules/redis/client';\nimport { getTotalKeys } from 'src/modules/redis/utils';\n\nexport abstract class AbstractBulkActionSimpleRunner extends AbstractBulkActionRunner {\n  protected node: RedisClient;\n\n  constructor(bulkAction, node) {\n    super(bulkAction);\n    this.node = node;\n  }\n\n  /**\n   * Commands to be executed in a pipeline\n   * @param keys\n   */\n  abstract prepareCommands(keys: Buffer[]): RedisClientCommand[];\n\n  /**\n   * @inheritDoc\n   */\n  async prepareToStart() {\n    this.progress.setTotal(await getTotalKeys(this.node));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async run() {\n    while (\n      this.progress.getCursor() > -1 &&\n      this.bulkAction.getStatus() === BulkActionStatus.Running\n    ) {\n      await this.runIteration();\n    }\n  }\n\n  /**\n   * Just run single iteration for some batch of keys and process results\n   */\n  async runIteration() {\n    const keys = await this.getKeysToProcess();\n    this.progress.addScanned(this.bulkAction.getFilter().getCount());\n\n    if (keys.length) {\n      const commands = this.prepareCommands(keys);\n      const res = await this.node.sendPipeline(commands);\n      this.processIterationResults(keys, res);\n    }\n\n    this.bulkAction.changeState();\n  }\n\n  /**\n   * Get batch of keys to process\n   */\n  async getKeysToProcess(): Promise<Buffer[]> {\n    if (this.progress.getCursor() < 0) {\n      return [];\n    }\n    // @ts-ignore\n    const [cursorBuffer, keys] = await this.node.sendCommand([\n      'scan',\n      this.progress.getCursor(),\n      ...this.bulkAction.getFilter().getScanArgsArray(),\n    ]);\n\n    const cursor = parseInt(cursorBuffer, 10);\n    this.progress.setCursor(cursor);\n\n    return keys;\n  }\n\n  /**\n   * Process results\n   * @param keys\n   * @param res\n   */\n  processIterationResults(\n    keys: Buffer[],\n    res: [Error | null, RedisClientCommandReply][],\n  ) {\n    this.summary.addProcessed(res.length);\n\n    res.forEach(([err], i) => {\n      const keyName = keys[i];\n\n      if (err) {\n        this.summary.addFailed(1);\n        this.bulkAction.writeToReport(keyName, false, err.message);\n      } else {\n        this.summary.addSuccess(1);\n        this.bulkAction.writeToReport(keyName, true);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts",
    "content": "import {\n  mockSocket,\n  mockBulkActionsAnalytics,\n  mockCreateBulkActionDto,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\n\nconst mockBulkActionFilter = Object.assign(new BulkActionFilter(), {\n  count: 10_000,\n  match: '*',\n  type: RedisDataType.Set,\n});\n\nconst bulkAction = new BulkAction(\n  mockCreateBulkActionDto.id,\n  mockCreateBulkActionDto.databaseId,\n  mockCreateBulkActionDto.type,\n  mockBulkActionFilter,\n  mockSocket,\n  mockBulkActionsAnalytics as any,\n);\n\nconst mockKey = 'mockedKey';\nconst mockKeyBuffer = Buffer.from(mockKey);\n\ndescribe('DeleteBulkActionSimpleRunner', () => {\n  const client = mockStandaloneRedisClient;\n  let deleteRunner: DeleteBulkActionSimpleRunner;\n\n  beforeEach(() => {\n    deleteRunner = new DeleteBulkActionSimpleRunner(bulkAction, client);\n  });\n\n  it('prepareCommands 3 commands', () => {\n    const commands = deleteRunner.prepareCommands([\n      mockKeyBuffer,\n      mockKeyBuffer,\n      mockKeyBuffer,\n    ]);\n    expect(commands).toEqual([\n      ['del', mockKeyBuffer],\n      ['del', mockKeyBuffer],\n      ['del', mockKeyBuffer],\n    ]);\n  });\n\n  it('prepareCommands 0 commands', () => {\n    const commands = deleteRunner.prepareCommands([]);\n    expect(commands).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.ts",
    "content": "import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { AbstractBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner';\nimport { RedisClientCommand } from 'src/modules/redis/client';\n\nexport class DeleteBulkActionSimpleRunner extends AbstractBulkActionSimpleRunner {\n  prepareCommands(keys: Buffer[]): RedisClientCommand[] {\n    return keys.map((key) => [BrowserToolKeysCommands.Del, key]);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/simple/unlink.bulk-action.simple.runner.spec.ts",
    "content": "import {\n  mockSocket,\n  mockBulkActionsAnalytics,\n  mockCreateBulkActionDto,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { UnlinkBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/unlink.bulk-action.simple.runner';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { RedisFeature } from 'src/modules/redis/client';\n\nconst mockBulkActionFilter = Object.assign(new BulkActionFilter(), {\n  count: 10_000,\n  match: '*',\n  type: RedisDataType.Set,\n});\n\nconst bulkAction = new BulkAction(\n  mockCreateBulkActionDto.id,\n  mockCreateBulkActionDto.databaseId,\n  mockCreateBulkActionDto.type,\n  mockBulkActionFilter,\n  mockSocket,\n  mockBulkActionsAnalytics as any,\n);\n\nconst mockKey = 'mockedKey';\nconst mockKeyBuffer = Buffer.from(mockKey);\n\ndescribe('UnlinkBulkActionSimpleRunner', () => {\n  const client = mockStandaloneRedisClient;\n  let unlinkRunner: UnlinkBulkActionSimpleRunner;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    unlinkRunner = new UnlinkBulkActionSimpleRunner(bulkAction, client);\n  });\n\n  describe('prepareCommands', () => {\n    it('should use UNLINK command when Redis supports it (Redis 4.0.0+)', () => {\n      // Default behavior - UNLINK is supported\n      const commands = unlinkRunner.prepareCommands([\n        mockKeyBuffer,\n        mockKeyBuffer,\n        mockKeyBuffer,\n      ]);\n      expect(commands).toEqual([\n        [BrowserToolKeysCommands.Unlink, mockKeyBuffer],\n        [BrowserToolKeysCommands.Unlink, mockKeyBuffer],\n        [BrowserToolKeysCommands.Unlink, mockKeyBuffer],\n      ]);\n    });\n\n    it('should return empty array for 0 commands', () => {\n      const commands = unlinkRunner.prepareCommands([]);\n      expect(commands).toEqual([]);\n    });\n  });\n\n  describe('prepareToStart', () => {\n    it('should use UNLINK when Redis version supports it (4.0.0+)', async () => {\n      client.isFeatureSupported.mockResolvedValueOnce(true);\n      client.sendCommand.mockResolvedValueOnce('100'); // getTotalKeys mock\n\n      await unlinkRunner.prepareToStart();\n\n      expect(client.isFeatureSupported).toHaveBeenCalledWith(\n        RedisFeature.UnlinkCommand,\n      );\n\n      const commands = unlinkRunner.prepareCommands([mockKeyBuffer]);\n      expect(commands).toEqual([\n        [BrowserToolKeysCommands.Unlink, mockKeyBuffer],\n      ]);\n    });\n\n    it('should fall back to DEL when Redis version does not support UNLINK (< 4.0.0)', async () => {\n      client.isFeatureSupported.mockResolvedValueOnce(false);\n      client.sendCommand.mockResolvedValueOnce('100'); // getTotalKeys mock\n\n      await unlinkRunner.prepareToStart();\n\n      expect(client.isFeatureSupported).toHaveBeenCalledWith(\n        RedisFeature.UnlinkCommand,\n      );\n\n      const commands = unlinkRunner.prepareCommands([mockKeyBuffer]);\n      expect(commands).toEqual([[BrowserToolKeysCommands.Del, mockKeyBuffer]]);\n    });\n\n    it('should use DEL for multiple keys when UNLINK is not supported', async () => {\n      client.isFeatureSupported.mockResolvedValueOnce(false);\n      client.sendCommand.mockResolvedValueOnce('100'); // getTotalKeys mock\n\n      await unlinkRunner.prepareToStart();\n\n      const commands = unlinkRunner.prepareCommands([\n        mockKeyBuffer,\n        mockKeyBuffer,\n        mockKeyBuffer,\n      ]);\n      expect(commands).toEqual([\n        [BrowserToolKeysCommands.Del, mockKeyBuffer],\n        [BrowserToolKeysCommands.Del, mockKeyBuffer],\n        [BrowserToolKeysCommands.Del, mockKeyBuffer],\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/models/runners/simple/unlink.bulk-action.simple.runner.ts",
    "content": "import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { AbstractBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner';\nimport { RedisClientCommand, RedisFeature } from 'src/modules/redis/client';\n\nexport class UnlinkBulkActionSimpleRunner extends AbstractBulkActionSimpleRunner {\n  private useDelFallback = false;\n\n  /**\n   * @inheritDoc\n   * Check if UNLINK command is supported, fall back to DEL if not\n   */\n  async prepareToStart(): Promise<void> {\n    await super.prepareToStart();\n\n    // Check if UNLINK is supported (Redis 4.0.0+)\n    // If not, fall back to DEL command for Redis 3.x compatibility\n    const isUnlinkSupported = await this.node.isFeatureSupported(\n      RedisFeature.UnlinkCommand,\n    );\n    this.useDelFallback = !isUnlinkSupported;\n  }\n\n  prepareCommands(keys: Buffer[]): RedisClientCommand[] {\n    const command = this.useDelFallback\n      ? BrowserToolKeysCommands.Del\n      : BrowserToolKeysCommands.Unlink;\n    return keys.map((key) => [command, key]);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts",
    "content": "import * as MockedSocket from 'socket.io-mock';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockBulkActionsAnalytics,\n  mockDatabaseClientFactory,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { BulkActionsProvider } from 'src/modules/bulk-actions/providers/bulk-actions.provider';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { BulkActionType } from 'src/modules/bulk-actions/constants';\nimport { CreateBulkActionDto } from 'src/modules/bulk-actions/dto/create-bulk-action.dto';\nimport { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { BadRequestException, NotFoundException } from '@nestjs/common';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nexport const mockSocket1 = new MockedSocket();\nmockSocket1.id = '1';\nmockSocket1['emit'] = jest.fn();\n\nexport const mockSocket2 = new MockedSocket();\nmockSocket2.id = '2';\nmockSocket2['emit'] = jest.fn();\n\nconst mockBulkActionFilter = Object.assign(new BulkActionFilter(), {\n  count: 10_000,\n  match: '*',\n  type: RedisDataType.Set,\n});\n\nconst mockCreateBulkActionDto = Object.assign(new CreateBulkActionDto(), {\n  id: 'bulk-action-id',\n  databaseId: 'database-id',\n  type: BulkActionType.Delete,\n  filter: mockBulkActionFilter,\n});\n\ndescribe('BulkActionsProvider', () => {\n  let service: BulkActionsProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        BulkActionsProvider,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: BulkActionsAnalytics,\n          useFactory: mockBulkActionsAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = module.get(BulkActionsProvider);\n  });\n\n  describe('create', () => {\n    it('should create only once with the same id', async () => {\n      expect(service['bulkActions'].size).toEqual(0);\n\n      const bulkAction = await service.create(\n        mockSessionMetadata,\n        mockCreateBulkActionDto,\n        mockSocket1,\n      );\n\n      expect(bulkAction).toBeInstanceOf(BulkAction);\n      expect(service['bulkActions'].size).toEqual(1);\n\n      try {\n        await service.create(\n          mockSessionMetadata,\n          mockCreateBulkActionDto,\n          mockSocket1,\n        );\n        fail();\n      } catch (e) {\n        expect(e.message).toEqual('You already have bulk action with such id');\n      }\n\n      expect(service['bulkActions'].size).toEqual(1);\n\n      await service.create(\n        mockSessionMetadata,\n        { ...mockCreateBulkActionDto, id: 'new one' },\n        mockSocket1,\n      );\n\n      expect(service['bulkActions'].size).toEqual(2);\n    });\n    it('should support unlink type', async () => {\n      const bulkAction = await service.create(\n        mockSessionMetadata,\n        { ...mockCreateBulkActionDto, type: BulkActionType.Unlink },\n        mockSocket1,\n      );\n\n      expect(bulkAction).toBeInstanceOf(BulkAction);\n      expect(service['bulkActions'].size).toEqual(1);\n    });\n    it('should fail when unsupported runner class', async () => {\n      try {\n        await service.create(\n          mockSessionMetadata,\n          {\n            ...mockCreateBulkActionDto,\n            type: undefined,\n          },\n          mockSocket1,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n  });\n  describe('get', () => {\n    it('should get by id', async () => {\n      const bulkAction = await service.create(\n        mockSessionMetadata,\n        mockCreateBulkActionDto,\n        mockSocket1,\n      );\n      await service.create(\n        mockSessionMetadata,\n        { ...mockCreateBulkActionDto, id: 'new one' },\n        mockSocket1,\n      );\n\n      expect(service['bulkActions'].size).toEqual(2);\n\n      expect(service.get(mockCreateBulkActionDto.id)).toEqual(bulkAction);\n    });\n    it('should throw not found error', async () => {\n      try {\n        expect(service['bulkActions'].size).toEqual(0);\n\n        service.get(mockCreateBulkActionDto.id);\n\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n  });\n  describe('abort', () => {\n    it('should abort by id and remove', async () => {\n      const bulkAction = await service.create(\n        mockSessionMetadata,\n        mockCreateBulkActionDto,\n        mockSocket1,\n      );\n      await service.create(\n        mockSessionMetadata,\n        { ...mockCreateBulkActionDto, id: 'new one' },\n        mockSocket1,\n      );\n\n      expect(service['bulkActions'].size).toEqual(2);\n\n      expect(service.abort(mockCreateBulkActionDto.id)).toEqual(bulkAction);\n\n      expect(service['bulkActions'].size).toEqual(1);\n    });\n    it('should throw not found error', async () => {\n      try {\n        expect(service['bulkActions'].size).toEqual(0);\n\n        service.abort(mockCreateBulkActionDto.id);\n\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n  });\n  describe('abortUsersBulkActions', () => {\n    it('should abort all users bulk actions', async () => {\n      await service.create(\n        mockSessionMetadata,\n        mockCreateBulkActionDto,\n        mockSocket1,\n      );\n      await service.create(\n        mockSessionMetadata,\n        { ...mockCreateBulkActionDto, id: 'new one' },\n        mockSocket1,\n      );\n      await service.create(\n        mockSessionMetadata,\n        { ...mockCreateBulkActionDto, id: 'new one 2' },\n        mockSocket2,\n      );\n\n      expect(service['bulkActions'].size).toEqual(3);\n\n      expect(service.abortUsersBulkActions(mockSocket1.id)).toEqual(2);\n      expect(service.abortUsersBulkActions(mockSocket1.id)).toEqual(0);\n\n      expect(service['bulkActions'].size).toEqual(1);\n      const bulkAction3 = service.get('new one 2');\n\n      expect(bulkAction3['socket']['id']).toEqual('2');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { BulkAction } from 'src/modules/bulk-actions/models/bulk-action';\nimport { CreateBulkActionDto } from 'src/modules/bulk-actions/dto/create-bulk-action.dto';\nimport { Socket } from 'socket.io';\nimport {\n  BulkActionStatus,\n  BulkActionType,\n} from 'src/modules/bulk-actions/constants';\nimport { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner';\nimport { UnlinkBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/unlink.bulk-action.simple.runner';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\nimport { ClientContext, SessionMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\n@Injectable()\nexport class BulkActionsProvider {\n  private bulkActions: Map<string, BulkAction> = new Map();\n\n  private logger: Logger = new Logger('BulkActionsProvider');\n\n  constructor(\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly analytics: BulkActionsAnalytics,\n  ) {}\n\n  /**\n   * Create and run new bulk action\n   * @param dto\n   * @param socket\n   */\n  async create(\n    sessionMetadata: SessionMetadata,\n    dto: CreateBulkActionDto,\n    socket: Socket,\n  ): Promise<BulkAction> {\n    if (this.bulkActions.get(dto.id)) {\n      throw new Error('You already have bulk action with such id');\n    }\n\n    const bulkAction = new BulkAction(\n      dto.id,\n      dto.databaseId,\n      dto.type,\n      dto.filter,\n      socket,\n      this.analytics,\n      dto.generateReport,\n    );\n\n    this.bulkActions.set(dto.id, bulkAction);\n\n    // todo: add multi user support\n    // todo: use own client and close it after\n    const client = await this.databaseClientFactory.getOrCreateClient({\n      sessionMetadata,\n      databaseId: dto.databaseId,\n      context: ClientContext.Common,\n      db: dto.db,\n    });\n\n    await bulkAction.prepare(\n      client,\n      BulkActionsProvider.getSimpleRunnerClass(dto),\n    );\n\n    bulkAction.start().catch();\n\n    return bulkAction;\n  }\n\n  /**\n   * Return class name for simple (implemented on BE) bulk runners\n   * @param dto\n   */\n  static getSimpleRunnerClass(dto: CreateBulkActionDto) {\n    // eslint-disable-next-line sonarjs/no-small-switch\n    switch (dto.type) {\n      case BulkActionType.Delete:\n        return DeleteBulkActionSimpleRunner;\n      case BulkActionType.Unlink:\n        return UnlinkBulkActionSimpleRunner;\n      default:\n        throw new BadRequestException(\n          `Unsupported type: ${dto.type} for Bulk Actions`,\n        );\n    }\n  }\n\n  /**\n   * Get bulk action by id\n   * @param id\n   */\n  get(id: string): BulkAction {\n    const bulkAction = this.bulkActions.get(id);\n\n    if (!bulkAction) {\n      throw new NotFoundException(`Bulk action with id: ${id} was not found`);\n    }\n\n    return bulkAction;\n  }\n\n  /**\n   * Get bulk action by id, abort it and remove from bulk actions map\n   * @param id\n   */\n  abort(id: string): BulkAction {\n    const bulkAction = this.get(id);\n\n    bulkAction.setStatus(BulkActionStatus.Aborted);\n\n    this.bulkActions.delete(id);\n\n    return bulkAction;\n  }\n\n  /**\n   * Abort all bulk user's actions\n   * Usually done on socket connection lost\n   * @param socketId\n   */\n  abortUsersBulkActions(socketId: string): number {\n    let aborted = 0;\n\n    this.bulkActions.forEach((bulkAction) => {\n      if (bulkAction.getSocket().id === socketId) {\n        try {\n          this.abort(bulkAction.getId());\n          aborted += 1;\n        } catch (e) {\n          // ignore errors\n        }\n      }\n    });\n\n    this.logger.debug(`Aborted ${aborted} bulk actions`);\n\n    return aborted;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/ca-certificate.controller.ts",
    "content": "import {\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiParam,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { CaCertificateService } from 'src/modules/certificate/ca-certificate.service';\n\n@ApiTags('TLS Certificates')\n@Controller('certificates/ca')\n@UseInterceptors(ClassSerializerInterceptor)\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CaCertificateController {\n  constructor(private service: CaCertificateService) {}\n\n  @Get('')\n  @ApiOperation({ description: 'Get Ca Certificate list' })\n  @ApiOkResponse({\n    description: 'Ca Certificate list',\n    isArray: true,\n    type: CaCertificate,\n  })\n  async list(): Promise<CaCertificate[]> {\n    return await this.service.list();\n  }\n\n  @Delete(':id')\n  @ApiOperation({ description: 'Delete Ca Certificate by id' })\n  @ApiParam({ name: 'id', type: String })\n  async delete(@Param('id') id: string): Promise<void> {\n    await this.service.delete(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/ca-certificate.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  mockCaCertificate,\n  mockCaCertificateRepository,\n  mockCreateCaCertificateDto,\n  MockType,\n  mockRedisClientStorage,\n} from 'src/__mocks__';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { pick } from 'lodash';\nimport { KeytarEncryptionErrorException } from 'src/modules/encryption/exceptions';\nimport { CaCertificateService } from './ca-certificate.service';\n\ndescribe('CaCertificateService', () => {\n  let service: CaCertificateService;\n  let repository: MockType<CaCertificateRepository>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CaCertificateService,\n        {\n          provide: CaCertificateRepository,\n          useFactory: mockCaCertificateRepository,\n        },\n        {\n          provide: RedisClientStorage,\n          useFactory: mockRedisClientStorage,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CaCertificateService);\n    repository = await module.get(CaCertificateRepository);\n  });\n\n  describe('get', () => {\n    it('should return ca certificate model', async () => {\n      expect(await service.get(mockCaCertificate.id)).toEqual(\n        mockCaCertificate,\n      );\n    });\n    it('should return NotFound error if no certificated found', async () => {\n      repository.get.mockResolvedValueOnce(null);\n\n      try {\n        await service.get(mockCaCertificate.id);\n        fail();\n      } catch (e) {\n        // why BadRequest instead of NotFound?\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_CERTIFICATE_ID);\n      }\n    });\n  });\n\n  describe('list', () => {\n    it('get all certificates from the repository', async () => {\n      const result = await service.list();\n\n      expect(result).toEqual([\n        pick(mockCaCertificate, 'id', 'name'),\n        pick(mockCaCertificate, 'id', 'name'),\n      ]);\n    });\n  });\n\n  describe('create', () => {\n    it('should return ca certificate model', async () => {\n      expect(await service.create(mockCreateCaCertificateDto)).toEqual(\n        mockCaCertificate,\n      );\n    });\n    it('should throw encryption error', async () => {\n      repository.create.mockRejectedValueOnce(\n        new KeytarEncryptionErrorException(),\n      );\n\n      try {\n        await service.create(mockCreateCaCertificateDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(KeytarEncryptionErrorException);\n      }\n    });\n    it('should throw 500 error in any other case (why?)', async () => {\n      repository.create.mockRejectedValueOnce(new BadRequestException());\n\n      try {\n        await service.create(mockCreateCaCertificateDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n\n  describe('delete', () => {\n    const mockId = 'mock-ca-cert-id';\n    const mockAffectedDatabases = ['db1', 'db2'];\n\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    it('should delete CA certificate and remove affected database clients', async () => {\n      jest\n        .spyOn(repository, 'delete')\n        .mockResolvedValue({ affectedDatabases: mockAffectedDatabases });\n      jest\n        .spyOn(service['redisClientStorage'], 'removeManyByMetadata')\n        .mockResolvedValue(undefined);\n\n      await service.delete(mockId);\n\n      expect(repository.delete).toHaveBeenCalledWith(mockId);\n      expect(\n        service['redisClientStorage'].removeManyByMetadata,\n      ).toHaveBeenCalledTimes(mockAffectedDatabases.length);\n      mockAffectedDatabases.forEach((databaseId) => {\n        expect(\n          service['redisClientStorage'].removeManyByMetadata,\n        ).toHaveBeenCalledWith({ databaseId });\n      });\n    });\n\n    it('should throw encryption error', async () => {\n      repository.delete.mockRejectedValueOnce(\n        new KeytarEncryptionErrorException(),\n      );\n\n      try {\n        await service.delete(mockCaCertificate.id);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(KeytarEncryptionErrorException);\n      }\n    });\n    it('should throw 500 error in any other case (why?)', async () => {\n      repository.delete.mockRejectedValueOnce(new Error());\n\n      try {\n        await service.delete(mockCaCertificate.id);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/ca-certificate.service.ts",
    "content": "import {\n  BadRequestException,\n  HttpException,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\nimport { classToClass } from 'src/utils';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\n\n@Injectable()\nexport class CaCertificateService {\n  private logger = new Logger('CaCertificateService');\n\n  constructor(\n    private readonly repository: CaCertificateRepository,\n    private redisClientStorage: RedisClientStorage,\n  ) {}\n\n  async get(id: string): Promise<CaCertificate> {\n    this.logger.debug(`Getting CA certificate with id: ${id}.`);\n    const model = await this.repository.get(id);\n\n    if (!model) {\n      this.logger.error(`Unable to find CA certificate with id: ${id}`);\n      throw new BadRequestException(ERROR_MESSAGES.INVALID_CERTIFICATE_ID); // todo: why BadRequest?\n    }\n\n    return model;\n  }\n\n  async list(): Promise<CaCertificate[]> {\n    this.logger.debug('Getting CA certificate list.');\n\n    return this.repository.list();\n  }\n\n  async create(dto: CreateCaCertificateDto): Promise<CaCertificate> {\n    this.logger.debug('Creating certificate.');\n    try {\n      return await this.repository.create(classToClass(CaCertificate, dto));\n    } catch (error) {\n      this.logger.error('Failed to create certificate.', error);\n\n      // todo: move this logic to the global exception filter\n      if (error instanceof EncryptionServiceErrorException) {\n        throw error;\n      }\n\n      throw new InternalServerErrorException();\n    }\n  }\n\n  async delete(id: string): Promise<void> {\n    try {\n      const { affectedDatabases } = await this.repository.delete(id);\n\n      await Promise.all(\n        affectedDatabases.map(async (databaseId) => {\n          // If the certificate is used by the database, remove the client\n          await this.redisClientStorage.removeManyByMetadata({ databaseId });\n        }),\n      );\n    } catch (error) {\n      this.logger.error(`Failed to delete certificate ${id}`, error);\n\n      // todo: move this logic to the global exception filter\n      if (error instanceof HttpException) {\n        throw error;\n      }\n\n      throw new InternalServerErrorException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/certificate.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { CaCertificateController } from 'src/modules/certificate/ca-certificate.controller';\nimport { CaCertificateService } from 'src/modules/certificate/ca-certificate.service';\nimport { ClientCertificateService } from 'src/modules/certificate/client-certificate.service';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { LocalCaCertificateRepository } from 'src/modules/certificate/repositories/local.ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { LocalClientCertificateRepository } from 'src/modules/certificate/repositories/local.client-certificate.repository';\nimport { ClientCertificateController } from 'src/modules/certificate/client-certificate.controller';\n\n@Module({})\nexport class CertificateModule {\n  static register(\n    caCertificateRepository: Type<CaCertificateRepository> = LocalCaCertificateRepository,\n    clientCertificateRepository: Type<ClientCertificateRepository> = LocalClientCertificateRepository,\n  ) {\n    return {\n      module: CertificateModule,\n      controllers: [CaCertificateController, ClientCertificateController],\n      providers: [\n        CaCertificateService,\n        ClientCertificateService,\n        {\n          provide: CaCertificateRepository,\n          useClass: caCertificateRepository,\n        },\n        {\n          provide: ClientCertificateRepository,\n          useClass: clientCertificateRepository,\n        },\n      ],\n      exports: [\n        CaCertificateService,\n        ClientCertificateService,\n        CaCertificateRepository,\n        ClientCertificateRepository,\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/client-certificate.controller.ts",
    "content": "import {\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport {\n  ApiOkResponse,\n  ApiOperation,\n  ApiParam,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { ClientCertificateService } from 'src/modules/certificate/client-certificate.service';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\n\n@ApiTags('TLS Certificates')\n@Controller('certificates/client')\n@UseInterceptors(ClassSerializerInterceptor)\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class ClientCertificateController {\n  constructor(private service: ClientCertificateService) {}\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Get('')\n  @ApiOperation({ description: 'Get Client Certificate list' })\n  @ApiOkResponse({\n    description: 'Client Certificate list',\n    isArray: true,\n    type: ClientCertificate,\n  })\n  async getClientCertList(): Promise<ClientCertificate[]> {\n    return await this.service.list();\n  }\n\n  @Delete(':id')\n  @ApiOperation({ description: 'Delete Client Certificate pair by id' })\n  @ApiParam({ name: 'id', type: String })\n  async deleteClientCertificatePair(@Param('id') id: string): Promise<void> {\n    await this.service.delete(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/client-certificate.service.spec.ts",
    "content": "import { pick } from 'lodash';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  mockClientCertificate,\n  mockClientCertificateRepository,\n  mockCreateClientCertificateDto,\n  MockType,\n  mockRedisClientStorage,\n} from 'src/__mocks__';\nimport { KeytarEncryptionErrorException } from 'src/modules/encryption/exceptions';\nimport { ClientCertificateService } from 'src/modules/certificate/client-certificate.service';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\n\ndescribe('ClientCertificateService', () => {\n  let service: ClientCertificateService;\n  let repository: MockType<ClientCertificateRepository>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ClientCertificateService,\n        {\n          provide: ClientCertificateRepository,\n          useFactory: mockClientCertificateRepository,\n        },\n        {\n          provide: RedisClientStorage,\n          useFactory: mockRedisClientStorage,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(ClientCertificateService);\n    repository = await module.get(ClientCertificateRepository);\n  });\n\n  describe('get', () => {\n    it('should return client certificate model', async () => {\n      expect(await service.get(mockClientCertificate.id)).toEqual(\n        mockClientCertificate,\n      );\n    });\n    it('should return NotFound error if no certificated found', async () => {\n      repository.get.mockResolvedValueOnce(null);\n\n      try {\n        await service.get(mockClientCertificate.id);\n        fail();\n      } catch (e) {\n        // why BadRequest instead of NotFound?\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(ERROR_MESSAGES.INVALID_CERTIFICATE_ID);\n      }\n    });\n  });\n\n  describe('list', () => {\n    it('get all certificates from the repository', async () => {\n      const result = await service.list();\n\n      expect(result).toEqual([\n        pick(mockClientCertificate, 'id', 'name'),\n        pick(mockClientCertificate, 'id', 'name'),\n      ]);\n    });\n  });\n\n  describe('create', () => {\n    it('should return client certificate model', async () => {\n      expect(await service.create(mockCreateClientCertificateDto)).toEqual(\n        mockClientCertificate,\n      );\n    });\n    it('should throw encryption error', async () => {\n      repository.create.mockRejectedValueOnce(\n        new KeytarEncryptionErrorException(),\n      );\n\n      try {\n        await service.create(mockCreateClientCertificateDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(KeytarEncryptionErrorException);\n      }\n    });\n    it('should throw 500 error in any other case (why?)', async () => {\n      repository.create.mockRejectedValueOnce(new BadRequestException());\n\n      try {\n        await service.create(mockCreateClientCertificateDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n\n  describe('delete', () => {\n    const mockId = 'mock-client-cert-id';\n    const mockAffectedDatabases = ['db1', 'db2'];\n\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    it('should delete client certificate and remove affected database clients', async () => {\n      jest\n        .spyOn(repository, 'delete')\n        .mockResolvedValue({ affectedDatabases: mockAffectedDatabases });\n      jest\n        .spyOn(service['redisClientStorage'], 'removeManyByMetadata')\n        .mockResolvedValue(undefined);\n\n      await service.delete(mockId);\n\n      expect(repository.delete).toHaveBeenCalledWith(mockId);\n      expect(\n        service['redisClientStorage'].removeManyByMetadata,\n      ).toHaveBeenCalledTimes(mockAffectedDatabases.length);\n      mockAffectedDatabases.forEach((databaseId) => {\n        expect(\n          service['redisClientStorage'].removeManyByMetadata,\n        ).toHaveBeenCalledWith({ databaseId });\n      });\n    });\n\n    it('should throw encryption error', async () => {\n      repository.delete.mockRejectedValueOnce(\n        new KeytarEncryptionErrorException(),\n      );\n\n      try {\n        await service.delete(mockClientCertificate.id);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(KeytarEncryptionErrorException);\n      }\n    });\n    it('should throw 500 error in any other case (why?)', async () => {\n      repository.delete.mockRejectedValueOnce(new Error());\n\n      try {\n        await service.delete(mockClientCertificate.id);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/client-certificate.service.ts",
    "content": "import {\n  BadRequestException,\n  HttpException,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\nimport { classToClass } from 'src/utils';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\n\n@Injectable()\nexport class ClientCertificateService {\n  private logger = new Logger('ClientCertificateService');\n\n  constructor(\n    private readonly repository: ClientCertificateRepository,\n    private redisClientStorage: RedisClientStorage,\n  ) {}\n\n  /**\n   * Get full Client certificate entity by id with decrypted fields\n   * @param id\n   */\n  async get(id: string): Promise<ClientCertificate> {\n    this.logger.debug(`Getting client certificate with id: ${id}.`);\n    const model = await this.repository.get(id);\n\n    if (!model) {\n      this.logger.error(`Unable to find client certificate with id: ${id}`);\n      throw new BadRequestException(ERROR_MESSAGES.INVALID_CERTIFICATE_ID); // todo: why BadRequest?\n    }\n\n    return model;\n  }\n\n  /**\n   * Get list of shortened CA certificates (id, name only)\n   */\n  async list(): Promise<ClientCertificate[]> {\n    this.logger.debug('Getting client certificates list.');\n\n    return this.repository.list();\n  }\n\n  async create(dto: CreateClientCertificateDto): Promise<ClientCertificate> {\n    this.logger.debug('Creating client certificate.');\n\n    try {\n      return await this.repository.create(classToClass(ClientCertificate, dto));\n    } catch (error) {\n      this.logger.error('Failed to create client certificate.', error);\n\n      // todo: move this logic to the global exception filter\n      if (error instanceof EncryptionServiceErrorException) {\n        throw error;\n      }\n\n      throw new InternalServerErrorException();\n    }\n  }\n\n  async delete(id: string): Promise<void> {\n    this.logger.debug(`Deleting client certificate. id: ${id}`);\n\n    try {\n      const { affectedDatabases } = await this.repository.delete(id);\n\n      await Promise.all(\n        affectedDatabases.map(async (databaseId) => {\n          // If the certificate is used by the database, remove the client\n          await this.redisClientStorage.removeManyByMetadata({ databaseId });\n        }),\n      );\n    } catch (error) {\n      this.logger.error(`Failed to delete certificate ${id}`, error);\n\n      // todo: move this logic to the global exception filter\n      if (error instanceof HttpException) {\n        throw error;\n      }\n\n      throw new InternalServerErrorException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/dto/create.ca-certificate.dto.ts",
    "content": "import { OmitType } from '@nestjs/swagger';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { Expose } from 'class-transformer';\n\nexport class CreateCaCertificateDto extends OmitType(CaCertificate, [\n  'id',\n] as const) {\n  @Expose()\n  certificate: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/dto/create.client-certificate.dto.ts",
    "content": "import { OmitType } from '@nestjs/swagger';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport { Expose } from 'class-transformer';\n\nexport class CreateClientCertificateDto extends OmitType(ClientCertificate, [\n  'id',\n] as const) {\n  @Expose()\n  certificate: string;\n\n  @Expose()\n  key: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/dto/use.ca-certificate.dto.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\n\nexport class UseCaCertificateDto extends PickType(CaCertificate, [\n  'id',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/dto/use.client-certificate.dto.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\n\nexport class UseClientCertificateDto extends PickType(ClientCertificate, [\n  'id',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/entities/ca-certificate.entity.ts",
    "content": "import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { Expose } from 'class-transformer';\n\n@Entity('ca_certificate')\nexport class CaCertificateEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: false, unique: true })\n  name: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  certificate: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  isPreSetup: boolean;\n\n  @OneToMany(() => DatabaseEntity, (database) => database.caCert)\n  databases: DatabaseEntity[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/entities/client-certificate.entity.ts",
    "content": "import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { Expose } from 'class-transformer';\n\n@Entity('client_certificate')\nexport class ClientCertificateEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: false, unique: true })\n  name: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  certificate: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  key: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  isPreSetup: boolean;\n\n  @OneToMany(() => DatabaseEntity, (database) => database.clientCert)\n  public databases: DatabaseEntity[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/models/ca-certificate.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';\n\nexport class CaCertificate {\n  @ApiProperty({\n    description: 'Certificate id',\n    type: String,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  id: string;\n\n  @ApiProperty({\n    description: 'Certificate name',\n    type: String,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  name: string;\n\n  @ApiProperty({\n    description: 'Certificate body',\n    type: String,\n  })\n  @Expose({ groups: ['security'] })\n  @IsNotEmpty()\n  @IsString({ always: true })\n  certificate: string;\n\n  @ApiPropertyOptional({\n    description:\n      'Whether the certificate was created from a file or environment variables at startup',\n    type: Boolean,\n  })\n  @Expose()\n  @IsBoolean()\n  @IsOptional()\n  isPreSetup?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/models/client-certificate.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';\n\nexport class ClientCertificate {\n  @ApiProperty({\n    description: 'Certificate id',\n    type: String,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  id: string;\n\n  @ApiProperty({\n    description: 'Certificate name',\n    type: String,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  name: string;\n\n  @ApiProperty({\n    description: 'Certificate body',\n    type: String,\n  })\n  @Expose({ groups: ['security'] })\n  @IsNotEmpty()\n  @IsString({ always: true })\n  certificate: string;\n\n  @ApiProperty({\n    description: 'Key body',\n    type: String,\n  })\n  @Expose({ groups: ['security'] })\n  @IsNotEmpty()\n  @IsString({ always: true })\n  key: string;\n\n  @ApiPropertyOptional({\n    description:\n      'Whether the certificate was created from a file or environment variables at startup',\n    type: Boolean,\n  })\n  @Expose()\n  @IsBoolean()\n  @IsOptional()\n  isPreSetup?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/repositories/ca-certificate.repository.ts",
    "content": "import { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\n\nexport abstract class CaCertificateRepository {\n  /**\n   * Get full CaCertificate by id with all fields\n   * @param id\n   * @return CaCertificate\n   */\n  abstract get(id: string): Promise<CaCertificate>;\n\n  /**\n   * Get list of certificates with [id, name] fields only\n   */\n  abstract list(): Promise<CaCertificate[]>;\n\n  /**\n   * Create a new CA certificate.\n   * @param caCertificate\n   * @param uniqueCheck\n   * @return CaCertificate\n   * @throws BadRequestException with ERROR_MESSAGES.CA_CERT_EXIST when such CA exists\n   */\n  abstract create(\n    caCertificate: CaCertificate,\n    uniqueCheck?: boolean,\n  ): Promise<CaCertificate>;\n\n  /**\n   * Delete certificate by id\n   * @param id\n   * @throws NotFoundException in case when try to delete not existing cert (?)\n   */\n  abstract delete(id: string): Promise<{ affectedDatabases: string[] }>;\n\n  /**\n   * Cleanup CA certificates which were created on startup from a file or env variables\n   * @param excludeIds\n   */\n  abstract cleanupPreSetup(\n    excludeIds?: string[],\n  ): Promise<{ affected: number }>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/repositories/client-certificate.repository.ts",
    "content": "import { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\n\nexport abstract class ClientCertificateRepository {\n  /**\n   * Get full ClientCertificate by id with all fields\n   * @param id\n   * @return ClientCertificate\n   */\n  abstract get(id: string): Promise<ClientCertificate>;\n  /**\n   * Get list of certificates with [id, name] fields only\n   */\n  abstract list(): Promise<ClientCertificate[]>;\n  /**\n   * Create a new client certificate.\n   * @param clientCertificate\n   * @param uniqueCheck\n   * @return ClientCertificate\n   * @throws BadRequestException with ERROR_MESSAGES.CLIENT_CERT_EXIST when such CA exists\n   */\n  abstract create(\n    clientCertificate: ClientCertificate,\n    uniqueCheck?: boolean,\n  ): Promise<ClientCertificate>;\n\n  /**\n   * Delete certificate by id\n   * @param id\n   * @throws NotFoundException in case when try to delete not existing cert (?)\n   */\n  abstract delete(id: string): Promise<{ affectedDatabases: string[] }>;\n\n  /**\n   * Cleanup CA certificates which were created on startup from a file or env variables\n   * @param excludeIds\n   */\n  abstract cleanupPreSetup(\n    excludeIds?: string[],\n  ): Promise<{ affected: number }>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/repositories/local.ca-certificate.repository.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { pick } from 'lodash';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { In, Not, Repository } from 'typeorm';\nimport {\n  mockCaCertificate,\n  mockCaCertificateCertificateEncrypted,\n  mockCaCertificateCertificatePlain,\n  mockCaCertificateEntity,\n  mockCaCertificateId,\n  mockEncryptionService,\n  mockRepository,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalCaCertificateRepository } from 'src/modules/certificate/repositories/local.ca-certificate.repository';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { BadRequestException, NotFoundException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\ndescribe('LocalCaCertificateRepository', () => {\n  let service: LocalCaCertificateRepository;\n  let encryptionService: MockType<EncryptionService>;\n  let repository: MockType<Repository<CaCertificateEntity>>;\n  let databaseRepository: MockType<Repository<DatabaseEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalCaCertificateRepository,\n        {\n          provide: getRepositoryToken(CaCertificateEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: getRepositoryToken(DatabaseEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(CaCertificateEntity));\n    databaseRepository = await module.get(getRepositoryToken(DatabaseEntity));\n    encryptionService = await module.get(EncryptionService);\n    service = await module.get(LocalCaCertificateRepository);\n\n    repository.findOneBy.mockResolvedValue(mockCaCertificateEntity);\n    repository\n      .createQueryBuilder()\n      .getMany.mockResolvedValue([\n        pick(mockCaCertificateEntity, 'id', 'name'),\n        pick(mockCaCertificateEntity, 'id', 'name'),\n      ]);\n    repository.save.mockResolvedValue(mockCaCertificateEntity);\n    repository.create.mockReturnValue(mockCaCertificate); // not entity since it happens before encryption\n\n    when(encryptionService.decrypt)\n      .calledWith(mockCaCertificateCertificateEncrypted, expect.anything())\n      .mockResolvedValue(mockCaCertificateCertificatePlain);\n    when(encryptionService.encrypt)\n      .calledWith(mockCaCertificateCertificatePlain)\n      .mockResolvedValue({\n        data: mockCaCertificateCertificateEncrypted,\n        encryption: mockCaCertificateEntity.encryption,\n      });\n  });\n\n  describe('get', () => {\n    it('should return ca certificate model', async () => {\n      const result = await service.get(mockCaCertificateId);\n\n      expect(result).toEqual(mockCaCertificate);\n    });\n  });\n\n  describe('list', () => {\n    it('should return ca certificates list', async () => {\n      const result = await service.list();\n\n      expect(result).toEqual([\n        pick(mockCaCertificate, 'id', 'name'),\n        pick(mockCaCertificate, 'id', 'name'),\n      ]);\n    });\n  });\n\n  describe('create', () => {\n    it('should create ca certificate', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.create(mockCaCertificate);\n\n      expect(result).toEqual(mockCaCertificate);\n    });\n\n    it('should throw an error when ca certificate with such name already exists', async () => {\n      try {\n        await service.create(mockCaCertificate);\n        fail();\n      } catch (e) {\n        // todo: why not ConflictException?\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CA_CERT_EXIST);\n        expect(repository.save).not.toHaveBeenCalled();\n      }\n    });\n\n    it('should ignore unique check when explicitly disabled via flag', async () => {\n      const result = await service.create(mockCaCertificate, false);\n\n      expect(result).toEqual(mockCaCertificate);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete ca certificate and return affected databases', async () => {\n      const mockId = 'mock-ca-cert-id';\n      const mockAffectedDatabases = ['db1', 'db2'];\n\n      // Mock findOneBy to return a certificate\n      repository.findOneBy.mockResolvedValue(mockCaCertificate);\n      databaseRepository\n        .createQueryBuilder()\n        .getMany.mockResolvedValue(mockAffectedDatabases.map((id) => ({ id })));\n\n      // Mock delete operation\n      repository.delete.mockResolvedValue(undefined);\n\n      const result = await service.delete(mockId);\n\n      expect(result).toEqual({ affectedDatabases: mockAffectedDatabases });\n      expect(repository.findOneBy).toHaveBeenCalledWith({ id: mockId });\n      expect(databaseRepository.createQueryBuilder).toHaveBeenCalledWith('d');\n      expect(\n        databaseRepository.createQueryBuilder().leftJoinAndSelect,\n      ).toHaveBeenCalledWith('d.caCert', 'c');\n      expect(\n        databaseRepository.createQueryBuilder().where,\n      ).toHaveBeenCalledWith({ caCert: mockId });\n      expect(\n        databaseRepository.createQueryBuilder().select,\n      ).toHaveBeenCalledWith(['d.id']);\n      expect(repository.delete).toHaveBeenCalledWith(mockId);\n    });\n\n    it('should throw NotFoundException when trying to delete non-existing ca certificate', async () => {\n      const mockId = 'non-existent-id';\n\n      // Mock findOneBy to return null (certificate not found)\n      repository.findOneBy.mockResolvedValue(null);\n\n      await expect(service.delete(mockId)).rejects.toThrow(NotFoundException);\n      expect(repository.findOneBy).toHaveBeenCalledWith({ id: mockId });\n      expect(repository.delete).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('cleanupPreSetup', () => {\n    it('should delete ca certificates with isPreSetup flag enabled', async () => {\n      const excludeIds = ['_1', '_2'];\n\n      repository\n        .createQueryBuilder()\n        .delete()\n        .execute.mockResolvedValue({ raw: [], affected: 1 });\n\n      const result = await service.cleanupPreSetup(excludeIds);\n\n      expect(result).toEqual({ affected: 1 });\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        isPreSetup: true,\n        id: Not(In(excludeIds)),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/repositories/local.ca-certificate.repository.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { In, Not, Repository } from 'typeorm';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { classToClass } from 'src/utils';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Injectable()\nexport class LocalCaCertificateRepository extends CaCertificateRepository {\n  private readonly logger = new Logger('LocalCaCertificateRepository');\n\n  private modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(CaCertificateEntity)\n    private readonly repository: Repository<CaCertificateEntity>,\n    @InjectRepository(DatabaseEntity)\n    private readonly databaseRepository: Repository<DatabaseEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, [\n      'certificate',\n    ]);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async get(id: string): Promise<CaCertificate> {\n    return classToClass(\n      CaCertificate,\n      await this.modelEncryptor.decryptEntity(\n        await this.repository.findOneBy({ id }),\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async list(): Promise<CaCertificate[]> {\n    return (\n      await this.repository\n        .createQueryBuilder('c')\n        .select(['c.id', 'c.name'])\n        .getMany()\n    ).map((entity) => classToClass(CaCertificate, entity));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async create(\n    caCertificate: CaCertificate,\n    uniqueCheck = true,\n  ): Promise<CaCertificate> {\n    if (uniqueCheck) {\n      // todo: use unique constraint and proper error handling to check for duplications\n      const found = await this.repository.findOneBy({\n        name: caCertificate.name,\n      });\n\n      if (found) {\n        this.logger.error(\n          `Failed to create certificate: ${caCertificate.name}. ${ERROR_MESSAGES.CA_CERT_EXIST}`,\n        );\n        throw new BadRequestException(ERROR_MESSAGES.CA_CERT_EXIST);\n      }\n    }\n\n    const entity = await this.repository.save(\n      await this.modelEncryptor.encryptEntity(\n        this.repository.create(caCertificate),\n      ),\n    );\n\n    return this.get(entity.id);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async delete(id: string): Promise<{ affectedDatabases: string[] }> {\n    this.logger.debug(`Deleting certificate. id: ${id}`);\n\n    // todo: 1. why we need to check if entity exists?\n    //  2. why we fetch it instead of check delete response?\n    //  3. why we need to fail if no cert found?\n    //  4. why there is no error message?\n    const found = await this.repository.findOneBy({ id });\n    if (!found) {\n      this.logger.error(`Failed to delete ca certificate: ${id}`);\n      throw new NotFoundException();\n    }\n\n    const affectedDatabases = (\n      await this.databaseRepository\n        .createQueryBuilder('d')\n        .leftJoinAndSelect('d.caCert', 'c')\n        .where({ caCert: id })\n        .select(['d.id'])\n        .getMany()\n    ).map((e) => e.id);\n\n    await this.repository.delete(id);\n    this.logger.debug(`Succeed to delete ca certificate: ${id}`);\n\n    return { affectedDatabases };\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async cleanupPreSetup(excludeIds?: string[]): Promise<{ affected: number }> {\n    const { affected } = await this.repository\n      .createQueryBuilder()\n      .delete()\n      .where({\n        isPreSetup: true,\n        id: Not(In(excludeIds)),\n      })\n      .execute();\n\n    return { affected };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/repositories/local.client-certificate.repository.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { pick } from 'lodash';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { In, Not, Repository } from 'typeorm';\nimport {\n  mockClientCertificate,\n  mockClientCertificateCertificateEncrypted,\n  mockClientCertificateCertificatePlain,\n  mockClientCertificateEntity,\n  mockClientCertificateId,\n  mockClientCertificateKeyEncrypted,\n  mockClientCertificateKeyPlain,\n  mockEncryptionService,\n  mockRepository,\n  MockType,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { BadRequestException, NotFoundException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { LocalClientCertificateRepository } from 'src/modules/certificate/repositories/local.client-certificate.repository';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\ndescribe('LocalClientCertificateRepository', () => {\n  let service: LocalClientCertificateRepository;\n  let encryptionService: MockType<EncryptionService>;\n  let repository: MockType<Repository<ClientCertificateEntity>>;\n  let databaseRepository: MockType<Repository<DatabaseEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalClientCertificateRepository,\n        {\n          provide: getRepositoryToken(ClientCertificateEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: getRepositoryToken(DatabaseEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(ClientCertificateEntity));\n    databaseRepository = await module.get(getRepositoryToken(DatabaseEntity));\n    encryptionService = await module.get(EncryptionService);\n    service = await module.get(LocalClientCertificateRepository);\n\n    repository.findOneBy.mockResolvedValue(mockClientCertificateEntity);\n    repository\n      .createQueryBuilder()\n      .getMany.mockResolvedValue([\n        pick(mockClientCertificateEntity, 'id', 'name'),\n        pick(mockClientCertificateEntity, 'id', 'name'),\n      ]);\n    repository.save.mockResolvedValue(mockClientCertificateEntity);\n    repository.create.mockReturnValue(mockClientCertificate); // not an entity since create happens before encryption\n\n    when(encryptionService.decrypt)\n      .calledWith(mockClientCertificateCertificateEncrypted, expect.anything())\n      .mockResolvedValue(mockClientCertificateCertificatePlain)\n      .calledWith(mockClientCertificateKeyEncrypted, expect.anything())\n      .mockResolvedValue(mockClientCertificateKeyPlain);\n    when(encryptionService.encrypt)\n      .calledWith(mockClientCertificateCertificatePlain)\n      .mockResolvedValue({\n        data: mockClientCertificateCertificateEncrypted,\n        encryption: mockClientCertificateEntity.encryption,\n      })\n      .calledWith(mockClientCertificateKeyPlain)\n      .mockResolvedValue({\n        data: mockClientCertificateKeyEncrypted,\n        encryption: mockClientCertificateEntity.encryption,\n      });\n  });\n\n  describe('get', () => {\n    it('should return client certificate model', async () => {\n      const result = await service.get(mockClientCertificateId);\n\n      expect(result).toEqual(mockClientCertificate);\n    });\n  });\n\n  describe('list', () => {\n    it('should return client certificates list', async () => {\n      const result = await service.list();\n\n      expect(result).toEqual([\n        pick(mockClientCertificate, 'id', 'name'),\n        pick(mockClientCertificate, 'id', 'name'),\n      ]);\n    });\n  });\n\n  describe('create', () => {\n    it('should create client certificate', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.create(mockClientCertificate);\n\n      expect(result).toEqual(mockClientCertificate);\n    });\n\n    it('should throw an error when client certificate with such name already exists', async () => {\n      try {\n        await service.create(mockClientCertificate);\n        fail();\n      } catch (e) {\n        // todo: why not ConflictException?\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CLIENT_CERT_EXIST);\n        expect(repository.save).not.toHaveBeenCalled();\n      }\n    });\n\n    it('should ignore unique check when explicitly disabled via flag', async () => {\n      const result = await service.create(mockClientCertificate, false);\n\n      expect(result).toEqual(mockClientCertificate);\n    });\n  });\n\n  describe('delete', () => {\n    const mockId = 'mock-client-cert-id';\n    const mockAffectedDatabases = ['db1', 'db2'];\n\n    beforeEach(() => {\n      jest.clearAllMocks();\n    });\n\n    it('should delete client certificate and return affected databases', async () => {\n      jest\n        .spyOn(repository, 'findOneBy')\n        .mockResolvedValue(mockClientCertificate);\n\n      databaseRepository\n        .createQueryBuilder()\n        .getMany.mockResolvedValue(mockAffectedDatabases.map((id) => ({ id })));\n\n      jest.spyOn(repository, 'delete').mockResolvedValue(undefined);\n\n      const result = await service.delete(mockId);\n\n      expect(result).toEqual({ affectedDatabases: mockAffectedDatabases });\n      expect(repository.findOneBy).toHaveBeenCalledWith({ id: mockId });\n      expect(\n        service['databaseRepository'].createQueryBuilder,\n      ).toHaveBeenCalledWith('d');\n      expect(\n        databaseRepository.createQueryBuilder().leftJoinAndSelect,\n      ).toHaveBeenCalledWith('d.clientCert', 'c');\n      expect(\n        databaseRepository.createQueryBuilder().where,\n      ).toHaveBeenCalledWith({ clientCert: mockId });\n      expect(\n        databaseRepository.createQueryBuilder().select,\n      ).toHaveBeenCalledWith(['d.id']);\n      expect(repository.delete).toHaveBeenCalledWith(mockId);\n    });\n\n    it('should throw NotFoundException when trying to delete non-existing client certificate', async () => {\n      jest.spyOn(repository, 'findOneBy').mockResolvedValue(null);\n\n      await expect(service.delete(mockId)).rejects.toThrow(NotFoundException);\n      expect(repository.findOneBy).toHaveBeenCalledWith({ id: mockId });\n      expect(repository.delete).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('cleanupPreSetup', () => {\n    it('should delete certificates with isPreSetup flag enabled', async () => {\n      const excludeIds = ['_1', '_2'];\n\n      repository\n        .createQueryBuilder()\n        .delete()\n        .execute.mockResolvedValue({ raw: [], affected: 1 });\n\n      const result = await service.cleanupPreSetup(excludeIds);\n\n      expect(result).toEqual({ affected: 1 });\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        isPreSetup: true,\n        id: Not(In(excludeIds)),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/repositories/local.client-certificate.repository.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { In, Not, Repository } from 'typeorm';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { classToClass } from 'src/utils';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Injectable()\nexport class LocalClientCertificateRepository extends ClientCertificateRepository {\n  private readonly logger = new Logger('LocalCaCertificateRepository');\n\n  private modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(ClientCertificateEntity)\n    private readonly repository: Repository<ClientCertificateEntity>,\n    @InjectRepository(DatabaseEntity)\n    private readonly databaseRepository: Repository<DatabaseEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, [\n      'certificate',\n      'key',\n    ]);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async get(id: string): Promise<ClientCertificate> {\n    return classToClass(\n      ClientCertificate,\n      await this.modelEncryptor.decryptEntity(\n        await this.repository.findOneBy({ id }),\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async list(): Promise<ClientCertificate[]> {\n    return (\n      await this.repository\n        .createQueryBuilder('c')\n        .select(['c.id', 'c.name'])\n        .getMany()\n    ).map((model) => classToClass(ClientCertificate, model));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async create(\n    clientCertificate: ClientCertificate,\n    uniqueCheck = true,\n  ): Promise<ClientCertificate> {\n    if (uniqueCheck) {\n      // todo: use unique constraint and proper error handling to check for duplications\n      const found = await this.repository.findOneBy({\n        name: clientCertificate.name,\n      });\n      if (found) {\n        this.logger.error(\n          `Failed to create certificate: ${clientCertificate.name}. ${ERROR_MESSAGES.CLIENT_CERT_EXIST}`,\n        );\n        throw new BadRequestException(ERROR_MESSAGES.CLIENT_CERT_EXIST);\n      }\n    }\n\n    const entity = await this.repository.save(\n      await this.modelEncryptor.encryptEntity(\n        this.repository.create(clientCertificate),\n      ),\n    );\n\n    return this.get(entity.id);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async delete(id: string): Promise<{ affectedDatabases: string[] }> {\n    this.logger.debug(`Deleting certificate. id: ${id}`);\n\n    // todo: 1. why we need to check if entity exists?\n    //  2. why we fetch it instead of check delete response?\n    //  3. why we need to fail if no cert found?\n    const found = await this.repository.findOneBy({ id });\n    if (!found) {\n      this.logger.error(`Failed to delete client certificate: ${id}`);\n      throw new NotFoundException();\n    }\n\n    const affectedDatabases = (\n      await this.databaseRepository\n        .createQueryBuilder('d')\n        .leftJoinAndSelect('d.clientCert', 'c')\n        .where({ clientCert: id })\n        .select(['d.id'])\n        .getMany()\n    ).map((e) => e.id);\n\n    await this.repository.delete(id);\n    this.logger.debug(`Succeed to delete client certificate: ${id}`);\n\n    return { affectedDatabases };\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async cleanupPreSetup(excludeIds?: string[]): Promise<{ affected: number }> {\n    const { affected } = await this.repository\n      .createQueryBuilder()\n      .delete()\n      .where({\n        isPreSetup: true,\n        id: Not(In(excludeIds)),\n      })\n      .execute();\n\n    return { affected };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/transformers/ca-cert.transformer.spec.ts",
    "content": "import { TypeHelpOptions } from 'class-transformer';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\nimport { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto';\nimport { caCertTransformer } from './ca-cert.transformer';\n\ndescribe('caCertTransformer', () => {\n  [\n    {\n      input: {\n        object: { caCert: { id: 'some-id-uuid' } },\n      } as unknown as TypeHelpOptions,\n      output: UseCaCertificateDto,\n    },\n    {\n      input: { object: { caCert: { id: null } } } as unknown as TypeHelpOptions,\n      output: CreateCaCertificateDto,\n    },\n    {\n      input: {\n        object: { caCert: { some: 'field' } },\n      } as unknown as TypeHelpOptions,\n      output: CreateCaCertificateDto,\n    },\n    {\n      input: { object: { caCert: null } } as unknown as TypeHelpOptions,\n      output: CreateCaCertificateDto,\n    },\n    {\n      input: { object: null } as unknown as TypeHelpOptions,\n      output: CreateCaCertificateDto,\n    },\n    {\n      input: null,\n      output: CreateCaCertificateDto,\n    },\n  ].forEach((tc) => {\n    it(`Should return ${tc.output} when input is: ${tc.input}`, () => {\n      expect(caCertTransformer(tc.input)).toEqual(tc.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/transformers/ca-cert.transformer.ts",
    "content": "import { get } from 'lodash';\nimport { TypeHelpOptions } from 'class-transformer';\nimport { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\n\nexport const caCertTransformer = (data: TypeHelpOptions) => {\n  if (get(data?.object, 'caCert.id')) {\n    return UseCaCertificateDto;\n  }\n  return CreateCaCertificateDto;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/transformers/client-cert.transformer.spec.ts",
    "content": "import { TypeHelpOptions } from 'class-transformer';\nimport { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\nimport { clientCertTransformer } from './client-cert.transformer';\n\ndescribe('caCertTransformer', () => {\n  [\n    {\n      input: {\n        object: { clientCert: { id: 'some-id-uuid' } },\n      } as unknown as TypeHelpOptions,\n      output: UseClientCertificateDto,\n    },\n    {\n      input: {\n        object: { clientCert: { id: null } },\n      } as unknown as TypeHelpOptions,\n      output: CreateClientCertificateDto,\n    },\n    {\n      input: {\n        object: { clientCert: { some: 'field' } },\n      } as unknown as TypeHelpOptions,\n      output: CreateClientCertificateDto,\n    },\n    {\n      input: { object: { clientCert: null } } as unknown as TypeHelpOptions,\n      output: CreateClientCertificateDto,\n    },\n    {\n      input: { object: null } as unknown as TypeHelpOptions,\n      output: CreateClientCertificateDto,\n    },\n    {\n      input: null,\n      output: CreateClientCertificateDto,\n    },\n  ].forEach((tc) => {\n    it(`Should return ${tc.output} when input is: ${tc.input}`, () => {\n      expect(clientCertTransformer(tc.input)).toEqual(tc.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/certificate/transformers/client-cert.transformer.ts",
    "content": "import { get } from 'lodash';\nimport { TypeHelpOptions } from 'class-transformer';\nimport { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\n\nexport const clientCertTransformer = (data: TypeHelpOptions) => {\n  if (get(data?.object, 'clientCert.id')) {\n    return UseClientCertificateDto;\n  }\n  return CreateClientCertificateDto;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/cli.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CommandsModule } from 'src/modules/commands/commands.module';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider';\nimport config from 'src/utils/config';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { CliController } from './controllers/cli.controller';\nimport { CliBusinessService } from './services/cli-business/cli-business.service';\nimport { CliAnalyticsService } from './services/cli-analytics/cli-analytics.service';\n\nconst COMMANDS_CONFIGS = config.get('commands');\n\n@Module({\n  imports: [CommandsModule],\n  controllers: [CliController],\n  providers: [\n    CliBusinessService,\n    CliAnalyticsService,\n    {\n      provide: CommandsService,\n      useFactory: () =>\n        new CommandsService(\n          COMMANDS_CONFIGS.map(\n            ({ name, url }) => new CommandsJsonProvider(name, url),\n          ),\n        ),\n    },\n    DatabaseClientFactory,\n    DatabaseAnalytics,\n  ],\n})\nexport class CliModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/constants/errors.ts",
    "content": "import { ReplyError } from 'src/models';\n\nexport class CommandParsingError extends ReplyError {\n  constructor(args) {\n    super(args);\n    this.name = 'CommandParsingError';\n  }\n}\n\nexport class RedirectionParsingError extends ReplyError {\n  constructor(args = 'Could not parse redirection error.') {\n    super(args);\n    this.name = 'RedirectionParsingError';\n  }\n}\n\nexport class CommandNotSupportedError extends ReplyError {\n  constructor(args) {\n    super(args);\n    this.name = 'CommandNotSupportedError';\n  }\n}\n\nexport class WrongDatabaseTypeError extends Error {\n  constructor(args) {\n    super(args);\n    this.name = 'WrongDatabaseTypeError';\n  }\n}\n\nexport class ClusterNodeNotFoundError extends Error {\n  constructor(args) {\n    super(args);\n    this.name = 'ClusterNodeNotFoundError';\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/controllers/cli.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Patch,\n  Post,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport {\n  CreateCliClientResponse,\n  DeleteClientResponse,\n  SendCommandDto,\n  SendCommandResponse,\n} from 'src/modules/cli/dto/cli.dto';\nimport { CliBusinessService } from 'src/modules/cli/services/cli-business/cli-business.service';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiCLIParams } from 'src/modules/cli/decorators/api-cli-params.decorator';\nimport { CliClientMetadata } from 'src/modules/cli/decorators/cli-client-metadata.decorator';\nimport { ClientMetadata } from 'src/common/models';\n\n@ApiTags('CLI')\n@Controller('cli')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CliController {\n  constructor(private service: CliBusinessService) {}\n\n  @Post('')\n  @ApiCLIParams(false)\n  @ApiEndpoint({\n    description: 'Create Redis client for CLI',\n    statusCode: 201,\n    responses: [\n      {\n        status: 201,\n        description: 'Create Redis client for CLI',\n        type: CreateCliClientResponse,\n      },\n    ],\n  })\n  async getClient(\n    @CliClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<CreateCliClientResponse> {\n    return this.service.getClient(clientMetadata);\n  }\n\n  @Post('/:uuid/send-command')\n  @ApiCLIParams()\n  @ApiEndpoint({\n    description: 'Send Redis CLI command',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Redis CLI command response',\n        type: SendCommandResponse,\n      },\n    ],\n  })\n  async sendCommand(\n    @CliClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SendCommandDto,\n  ): Promise<SendCommandResponse> {\n    return this.service.sendCommand(clientMetadata, dto);\n  }\n\n  @Post('/:uuid/send-cluster-command')\n  @ApiCLIParams()\n  @ApiEndpoint({\n    description: 'Send Redis CLI command',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Redis CLI command response',\n        type: SendCommandResponse,\n        isArray: true,\n      },\n    ],\n  })\n  async sendClusterCommand(\n    @CliClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: SendCommandDto,\n  ): Promise<SendCommandResponse> {\n    return this.service.sendCommand(clientMetadata, dto);\n  }\n\n  @Delete('/:uuid')\n  @ApiCLIParams()\n  @ApiEndpoint({\n    description: 'Delete Redis CLI client',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Delete Redis CLI client response',\n        type: DeleteClientResponse,\n      },\n    ],\n  })\n  async deleteClient(\n    @CliClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<DeleteClientResponse> {\n    return this.service.deleteClient(clientMetadata);\n  }\n\n  @Patch('/:uuid')\n  @ApiCLIParams()\n  @ApiEndpoint({\n    description: 'Re-create Redis client for CLI',\n    statusCode: 200,\n  })\n  async reCreateClient(\n    @CliClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<CreateCliClientResponse> {\n    return this.service.reCreateClient(clientMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/decorators/api-cli-params.decorator.ts",
    "content": "import { applyDecorators } from '@nestjs/common';\nimport { ApiParam } from '@nestjs/swagger';\n\nexport function ApiCLIParams(\n  requireClientUuid: boolean = true,\n): MethodDecorator & ClassDecorator {\n  const decorators = [\n    ApiParam({\n      name: 'dbInstance',\n      description: 'Database instance id.',\n      type: String,\n      required: true,\n    }),\n  ];\n  if (requireClientUuid) {\n    decorators.push(\n      ApiParam({\n        name: 'uuid',\n        description: 'CLI client uuid',\n        type: String,\n        required: true,\n      }),\n    );\n  }\n  return applyDecorators(...decorators);\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/decorators/cli-client-metadata.decorator.ts",
    "content": "import {\n  API_PARAM_CLI_CLIENT_ID,\n  API_PARAM_DATABASE_ID,\n} from 'src/common/constants';\nimport { createParamDecorator } from '@nestjs/common';\nimport { ClientContext } from 'src/common/models';\nimport { clientMetadataParamFactory } from 'src/common/decorators';\n\nexport const CliClientMetadata = (\n  databaseIdParam = API_PARAM_DATABASE_ID,\n  uniqueIdParam = API_PARAM_CLI_CLIENT_ID,\n) =>\n  createParamDecorator(clientMetadataParamFactory)({\n    context: ClientContext.CLI,\n    databaseIdParam,\n    uniqueIdParam,\n  });\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/dto/cli.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport { CliOutputFormatterTypes } from 'src/modules/cli/services/cli-business/output-formatter/output-formatter.interface';\n\nexport enum CommandExecutionStatus {\n  Success = 'success',\n  Fail = 'fail',\n}\n\nexport interface ICliExecResultFromNode {\n  host: string;\n  port: number;\n  response: any;\n  status: CommandExecutionStatus;\n  slot?: number;\n  error?: any;\n}\n\nexport class SendCommandDto {\n  @ApiProperty({\n    type: String,\n    description: 'Redis CLI command',\n  })\n  @IsString()\n  @IsNotEmpty()\n  command: string;\n\n  @ApiPropertyOptional({\n    description: 'Define output format',\n    default: CliOutputFormatterTypes.Raw,\n    enum: CliOutputFormatterTypes,\n  })\n  @IsOptional()\n  @IsEnum(CliOutputFormatterTypes, {\n    message: `outputFormat must be a valid enum value. Valid values: ${Object.values(\n      CliOutputFormatterTypes,\n    )}.`,\n  })\n  outputFormat?: CliOutputFormatterTypes;\n}\n\nexport class SendCommandResponse {\n  @ApiProperty({\n    type: String,\n    description: 'Redis CLI response',\n  })\n  response: any;\n\n  @ApiProperty({\n    description: 'Redis CLI command execution status',\n    default: CommandExecutionStatus.Success,\n    enum: CommandExecutionStatus,\n  })\n  status: CommandExecutionStatus;\n}\n\nexport class CreateCliClientResponse {\n  @ApiProperty({\n    type: String,\n    description: 'Client uuid',\n  })\n  uuid: string;\n}\n\nexport class DeleteClientResponse {\n  @ApiProperty({\n    description: 'Number of affected clients',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport {\n  mockRedisWrongTypeError,\n  mockDatabase,\n  MockType,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { CommandType, TelemetryEvents } from 'src/constants';\nimport { ReplyError } from 'src/models';\nimport { CommandParsingError } from 'src/modules/cli/constants/errors';\nimport {\n  CommandExecutionStatus,\n  ICliExecResultFromNode,\n} from 'src/modules/cli/dto/cli.dto';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { CliAnalyticsService } from './cli-analytics.service';\n\nconst mockCommandsService = {\n  getCommandsGroups: jest.fn(),\n};\n\nconst redisReplyError: ReplyError = {\n  ...mockRedisWrongTypeError,\n  command: { name: 'sadd' },\n};\nconst databaseId = mockDatabase.id;\nconst httpException = new InternalServerErrorException();\nconst mockCustomData = { data: 'Some data' };\nconst mockSetCommandName = 'set';\nconst mockAdditionalData = { command: mockSetCommandName };\n\ndescribe('CliAnalyticsService', () => {\n  let service: CliAnalyticsService;\n  let sendEventMethod: jest.SpyInstance<CliAnalyticsService, unknown[]>;\n  let sendFailedEventMethod: jest.SpyInstance<CliAnalyticsService, unknown[]>;\n  let commandsService: MockType<CommandsService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: CommandsService,\n          useFactory: () => mockCommandsService,\n        },\n        EventEmitter2,\n        CliAnalyticsService,\n      ],\n    }).compile();\n\n    service = module.get<CliAnalyticsService>(CliAnalyticsService);\n    sendEventMethod = jest.spyOn<CliAnalyticsService, any>(\n      service,\n      'sendEvent',\n    );\n    sendFailedEventMethod = jest.spyOn<CliAnalyticsService, any>(\n      service,\n      'sendFailedEvent',\n    );\n\n    commandsService = module.get(CommandsService);\n    commandsService.getCommandsGroups.mockResolvedValue({\n      main: {\n        SET: {\n          summary: 'Set the string value of a key',\n          since: '1.0.0',\n          group: 'string',\n          complexity: 'O(1)',\n          acl_categories: ['@write', '@string', '@slow'],\n        },\n      },\n      redisbloom: {\n        'BF.RESERVE': {\n          summary: 'Creates a new Bloom Filter',\n          complexity: 'O(1)',\n          since: '1.0.0',\n          group: 'bf',\n        },\n      },\n      custommodule: {\n        'CUSTOM.COMMAND': {\n          summary: 'Creates a new Bloom Filter',\n          complexity: 'O(1)',\n          since: '1.0.0',\n        },\n      },\n    });\n  });\n\n  describe('sendCliClientCreatedEvent', () => {\n    it('should emit CliIndexInfoSubmitted event', () => {\n      service.sendIndexInfoEvent(\n        mockSessionMetadata,\n        databaseId,\n        mockCustomData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliIndexInfoSubmitted,\n        {\n          databaseId,\n          ...mockCustomData,\n        },\n      );\n    });\n    it('should not fail and should not emit when there is no data', () => {\n      service.sendIndexInfoEvent(mockSessionMetadata, databaseId, null);\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendCliClientCreatedEvent', () => {\n    it('should emit CliClientCreated event', () => {\n      service.sendClientCreatedEvent(\n        mockSessionMetadata,\n        databaseId,\n        mockCustomData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientCreated,\n        {\n          databaseId,\n          ...mockCustomData,\n        },\n      );\n    });\n    it('should emit CliClientCreated event without additional data', () => {\n      service.sendClientCreatedEvent(mockSessionMetadata, databaseId);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientCreated,\n        {\n          databaseId,\n        },\n      );\n    });\n  });\n\n  describe('sendCliClientCreationFailedEvent', () => {\n    it('should emit CliClientCreationFailed event', () => {\n      service.sendClientCreationFailedEvent(\n        mockSessionMetadata,\n        databaseId,\n        httpException,\n        mockCustomData,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientCreationFailed,\n        httpException,\n        {\n          databaseId,\n          ...mockCustomData,\n        },\n      );\n    });\n    it('should emit CliClientCreationFailed event without additional data', () => {\n      service.sendClientCreationFailedEvent(\n        mockSessionMetadata,\n        databaseId,\n        httpException,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientCreationFailed,\n        httpException,\n        {\n          databaseId,\n        },\n      );\n    });\n  });\n\n  describe('sendCliClientRecreatedEvent', () => {\n    it('should emit CliClientRecreated event', () => {\n      service.sendClientRecreatedEvent(\n        mockSessionMetadata,\n        databaseId,\n        mockCustomData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientRecreated,\n        {\n          databaseId,\n          ...mockCustomData,\n        },\n      );\n    });\n    it('should emit CliClientRecreated event without additional data', () => {\n      service.sendClientRecreatedEvent(mockSessionMetadata, databaseId);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientRecreated,\n        {\n          databaseId,\n        },\n      );\n    });\n  });\n\n  describe('sendCliClientDeletedEvent', () => {\n    it('should emit CliClientDeleted event', () => {\n      service.sendClientDeletedEvent(\n        mockSessionMetadata,\n        1,\n        databaseId,\n        mockCustomData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientDeleted,\n        {\n          databaseId,\n          ...mockCustomData,\n        },\n      );\n    });\n    it('should emit CliClientDeleted event without additional data', () => {\n      service.sendClientDeletedEvent(mockSessionMetadata, 1, databaseId);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientDeleted,\n        {\n          databaseId,\n        },\n      );\n    });\n    it('should not emit event', () => {\n      service.sendClientDeletedEvent(mockSessionMetadata, 0, databaseId);\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n    it('should not emit event on invalid input values', () => {\n      const input: any = {};\n      service.sendClientDeletedEvent(mockSessionMetadata, input, databaseId);\n\n      expect(() =>\n        service.sendClientDeletedEvent(mockSessionMetadata, input, databaseId),\n      ).not.toThrow();\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendCliCommandExecutedEvent', () => {\n    it('should emit CliCommandExecuted event', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        databaseId,\n        mockAdditionalData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandExecuted,\n        {\n          databaseId,\n          command: mockAdditionalData.command,\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit CliCommandExecuted event without additional data', async () => {\n      await service.sendCommandExecutedEvent(mockSessionMetadata, databaseId);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandExecuted,\n        {\n          databaseId,\n        },\n      );\n    });\n  });\n\n  describe('sendCliCommandErrorEvent', () => {\n    it('should emit CliCommandError event', async () => {\n      await service.sendCommandErrorEvent(\n        mockSessionMetadata,\n        databaseId,\n        redisReplyError,\n        mockAdditionalData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandErrorReceived,\n        {\n          databaseId,\n          error: ReplyError.name,\n          command: mockAdditionalData.command,\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit CliCommandError event without additional data', async () => {\n      await service.sendCommandErrorEvent(\n        mockSessionMetadata,\n        databaseId,\n        redisReplyError,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandErrorReceived,\n        {\n          databaseId,\n          error: ReplyError.name,\n          command: 'sadd',\n        },\n      );\n    });\n    it('should emit event for custom error', async () => {\n      const error: any = CommandParsingError;\n      await service.sendCommandErrorEvent(\n        mockSessionMetadata,\n        databaseId,\n        error,\n        mockAdditionalData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandErrorReceived,\n        {\n          databaseId,\n          error: CommandParsingError.name,\n          command: mockAdditionalData.command,\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n  });\n\n  describe('sendCliClientCreationFailedEvent', () => {\n    it('should emit CliConnectionError event', async () => {\n      await service.sendConnectionErrorEvent(\n        mockSessionMetadata,\n        databaseId,\n        httpException,\n        mockAdditionalData,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientConnectionError,\n        httpException,\n        {\n          databaseId,\n          command: mockAdditionalData.command,\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit CliConnectionError event without additional data', async () => {\n      await service.sendConnectionErrorEvent(\n        mockSessionMetadata,\n        databaseId,\n        httpException,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClientConnectionError,\n        httpException,\n        {\n          databaseId,\n        },\n      );\n    });\n  });\n\n  describe('sendCliClusterCommandExecutedEvent', () => {\n    it('should emit success event', async () => {\n      const nodExecResult: ICliExecResultFromNode = {\n        response: '(integer) 5',\n        host: '127.0.0.1',\n        port: 7002,\n        status: CommandExecutionStatus.Success,\n      };\n\n      await service.sendClusterCommandExecutedEvent(\n        mockSessionMetadata,\n        databaseId,\n        nodExecResult,\n        mockAdditionalData,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliClusterNodeCommandExecuted,\n        {\n          databaseId,\n          command: mockAdditionalData.command,\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit event failed event for [RedisReply] error', async () => {\n      const nodExecResult: ICliExecResultFromNode = {\n        response: redisReplyError.message,\n        host: '127.0.0.1',\n        port: 7002,\n        error: redisReplyError,\n        status: CommandExecutionStatus.Fail,\n      };\n\n      await service.sendClusterCommandExecutedEvent(\n        mockSessionMetadata,\n        databaseId,\n        nodExecResult,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandErrorReceived,\n        {\n          databaseId,\n          error: redisReplyError.name,\n          command: 'sadd',\n        },\n      );\n    });\n    it('should emit event failed for custom error', async () => {\n      const nodExecResult: ICliExecResultFromNode = {\n        response: redisReplyError.message,\n        host: '127.0.0.1',\n        port: 7002,\n        error: CommandParsingError,\n        status: CommandExecutionStatus.Fail,\n      };\n\n      await service.sendClusterCommandExecutedEvent(\n        mockSessionMetadata,\n        databaseId,\n        nodExecResult,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CliCommandErrorReceived,\n        {\n          databaseId,\n          error: CommandParsingError.name,\n        },\n      );\n    });\n    it('should not emit event event', async () => {\n      const nodExecResult: any = {\n        response: redisReplyError.message,\n        host: '127.0.0.1',\n        port: 7002,\n        status: 'undefined status',\n      };\n      await service.sendClusterCommandExecutedEvent(\n        mockSessionMetadata,\n        databaseId,\n        nodExecResult,\n      );\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { ReplyError } from 'src/models';\nimport {\n  CommandExecutionStatus,\n  ICliExecResultFromNode,\n} from 'src/modules/cli/dto/cli.dto';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { CommandTelemetryBaseService } from 'src/modules/analytics/command.telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CliAnalyticsService extends CommandTelemetryBaseService {\n  constructor(\n    protected eventEmitter: EventEmitter2,\n    protected readonly commandsService: CommandsService,\n  ) {\n    super(eventEmitter, commandsService);\n  }\n\n  sendClientCreatedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    additionalData: object = {},\n  ): void {\n    this.sendEvent(sessionMetadata, TelemetryEvents.CliClientCreated, {\n      databaseId,\n      ...additionalData,\n    });\n  }\n\n  sendClientCreationFailedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    exception: HttpException,\n    additionalData: object = {},\n  ): void {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.CliClientCreationFailed,\n      exception,\n      {\n        databaseId,\n        ...additionalData,\n      },\n    );\n  }\n\n  sendClientRecreatedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    additionalData: object = {},\n  ): void {\n    this.sendEvent(sessionMetadata, TelemetryEvents.CliClientRecreated, {\n      databaseId,\n      ...additionalData,\n    });\n  }\n\n  sendClientDeletedEvent(\n    sessionMetadata: SessionMetadata,\n    affected: number,\n    databaseId: string,\n    additionalData: object = {},\n  ): void {\n    try {\n      if (affected > 0) {\n        this.sendEvent(sessionMetadata, TelemetryEvents.CliClientDeleted, {\n          databaseId,\n          ...additionalData,\n        });\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendIndexInfoEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    additionalData: object,\n  ): void {\n    if (!additionalData) {\n      return;\n    }\n\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.CliIndexInfoSubmitted, {\n        databaseId,\n        ...additionalData,\n      });\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  public async sendCommandExecutedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    additionalData: object = {},\n  ): Promise<void> {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.CliCommandExecuted, {\n        databaseId,\n        ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n        ...additionalData,\n      });\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  public async sendCommandErrorEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    error: ReplyError,\n    additionalData: object = {},\n  ): Promise<void> {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.CliCommandErrorReceived, {\n        databaseId,\n        error: error?.name,\n        command: error?.command?.name,\n        ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n        ...additionalData,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  public async sendClusterCommandExecutedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    result: ICliExecResultFromNode,\n    additionalData: object = {},\n  ): Promise<void> {\n    const { status, error } = result;\n    try {\n      if (status === CommandExecutionStatus.Success) {\n        this.sendEvent(\n          sessionMetadata,\n          TelemetryEvents.CliClusterNodeCommandExecuted,\n          {\n            databaseId,\n            ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n            ...additionalData,\n          },\n        );\n      }\n      if (status === CommandExecutionStatus.Fail) {\n        this.sendEvent(\n          sessionMetadata,\n          TelemetryEvents.CliCommandErrorReceived,\n          {\n            databaseId,\n            error: error.name,\n            command: error?.command?.name,\n            ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n            ...additionalData,\n          },\n        );\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  public async sendConnectionErrorEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    exception: HttpException,\n    additionalData: object = {},\n  ): Promise<void> {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.CliClientConnectionError,\n      exception,\n      {\n        databaseId,\n        ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n        ...additionalData,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { get } from 'lodash';\n\nimport { when } from 'jest-when';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  mockRedisServerInfoResponse,\n  mockRedisWrongTypeError,\n  mockCliAnalyticsService,\n  MockType,\n  mockDatabaseRecommendationService,\n  mockCliClientMetadata,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n  mockClusterRedisClient,\n  mockRedisFtInfoReply,\n  mockFtInfoAnalyticsData,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport {\n  CommandExecutionStatus,\n  SendCommandDto,\n  SendCommandResponse,\n} from 'src/modules/cli/dto/cli.dto';\nimport { ReplyError } from 'src/models';\nimport { CliToolUnsupportedCommands } from 'src/modules/cli/utils/getUnsupportedCommands';\nimport {\n  CommandNotSupportedError,\n  CommandParsingError,\n} from 'src/modules/cli/constants/errors';\nimport { RECOMMENDATION_NAMES, unknownCommand } from 'src/constants';\nimport { CliAnalyticsService } from 'src/modules/cli/services/cli-analytics/cli-analytics.service';\nimport { KeytarUnavailableException } from 'src/modules/encryption/exceptions';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { OutputFormatterManager } from './output-formatter/output-formatter-manager';\nimport {\n  CliOutputFormatterTypes,\n  IOutputFormatterStrategy,\n} from './output-formatter/output-formatter.interface';\nimport { CliBusinessService } from './cli-business.service';\n\njest.mock(\n  'uuid',\n  jest.fn(() => ({\n    ...(jest.requireActual('uuid') as object),\n    v4: jest.fn().mockReturnValue('68df9760-b7fa-4300-9841-0b726e0d8b67'),\n  })),\n);\n\nconst mockCommandsService = () => ({\n  getCommandsGroups: jest.fn(),\n});\n\nconst mockENotFoundMessage = 'ENOTFOUND some message';\nconst mockMemoryUsageCommand = 'memory usage key';\nconst mockGetEscapedKeyCommand = 'get \"\\\\\\\\key';\nconst mockServerInfoCommand = 'info server';\nconst mockIntegerResponse = 5;\n\ndescribe('CliBusinessService', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let service: CliBusinessService;\n  let databaseClientFactory: DatabaseClientFactory;\n  let recommendationService;\n  let rawFormatter: IOutputFormatterStrategy;\n  let analyticsService: MockType<CliAnalyticsService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CliBusinessService,\n        {\n          provide: CliAnalyticsService,\n          useFactory: mockCliAnalyticsService,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: CommandsService,\n          useFactory: mockCommandsService,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<CliBusinessService>(CliBusinessService);\n    databaseClientFactory = module.get<DatabaseClientFactory>(\n      DatabaseClientFactory,\n    );\n    analyticsService = module.get(CliAnalyticsService);\n    recommendationService = module.get<DatabaseRecommendationService>(\n      DatabaseRecommendationService,\n    );\n\n    clusterClient.nodes.mockReturnValue([\n      mockStandaloneRedisClient,\n      mockStandaloneRedisClient,\n    ]);\n\n    const outputFormatterManager: OutputFormatterManager = get(\n      service,\n      'outputFormatterManager',\n    );\n    rawFormatter = outputFormatterManager.getStrategy(\n      CliOutputFormatterTypes.Raw,\n    );\n  });\n\n  describe('getClient', () => {\n    it('should successfully create new redis client', async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(standaloneClient);\n\n      const result = await service.getClient(mockCliClientMetadata);\n\n      expect(result).toEqual({ uuid: mockCliClientMetadata.uniqueId });\n      expect(analyticsService.sendClientCreatedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n      );\n    });\n\n    it('should throw internal exception on getClient error', async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockRejectedValue(\n          new InternalServerErrorException(mockENotFoundMessage),\n        );\n\n      try {\n        await service.getClient(mockCliClientMetadata);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n        expect(\n          analyticsService.sendClientCreationFailedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockCliClientMetadata.databaseId,\n          new InternalServerErrorException(mockENotFoundMessage),\n        );\n      }\n    });\n\n    it('Should proxy EncryptionService errors on getClient', async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockRejectedValue(new KeytarUnavailableException());\n\n      try {\n        await service.getClient(mockCliClientMetadata);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(KeytarUnavailableException);\n        expect(\n          analyticsService.sendClientCreationFailedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockCliClientMetadata.databaseId,\n          new KeytarUnavailableException(),\n        );\n      }\n    });\n  });\n\n  describe('reCreateClient', () => {\n    it('should successfully create new redis client', async () => {\n      databaseClientFactory.deleteClient = jest.fn().mockResolvedValue(1);\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(standaloneClient);\n\n      const result = await service.reCreateClient(mockCliClientMetadata);\n\n      expect(result).toEqual({ uuid: mockCliClientMetadata.uniqueId });\n      expect(analyticsService.sendClientRecreatedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n      );\n    });\n\n    it('should throw internal exception on reCreateClient', async () => {\n      databaseClientFactory.deleteClient = jest.fn().mockResolvedValue(1);\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockRejectedValue(\n          new InternalServerErrorException(mockENotFoundMessage),\n        );\n\n      try {\n        await service.reCreateClient(mockCliClientMetadata);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n        expect(\n          analyticsService.sendClientCreationFailedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockCliClientMetadata.databaseId,\n          new InternalServerErrorException(mockENotFoundMessage),\n        );\n      }\n    });\n\n    it('Should proxy EncryptionService errors on reCreateClient', async () => {\n      databaseClientFactory.deleteClient = jest.fn().mockResolvedValue(1);\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockRejectedValue(new KeytarUnavailableException());\n\n      try {\n        await service.reCreateClient(mockCliClientMetadata);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(KeytarUnavailableException);\n        expect(\n          analyticsService.sendClientCreationFailedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockCliClientMetadata.databaseId,\n          new KeytarUnavailableException(),\n        );\n      }\n    });\n  });\n\n  describe('deleteClient', () => {\n    it('should successfully close redis client', async () => {\n      databaseClientFactory.deleteClient = jest.fn().mockResolvedValue(1);\n\n      const result = await service.deleteClient(mockCliClientMetadata);\n\n      expect(result).toEqual({ affected: 1 });\n      expect(analyticsService.sendClientDeletedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        1,\n        mockCliClientMetadata.databaseId,\n      );\n    });\n\n    it('should throw internal exception on deleteClient', async () => {\n      databaseClientFactory.deleteClient = jest\n        .fn()\n        .mockRejectedValue(new Error(mockENotFoundMessage));\n\n      try {\n        await service.deleteClient(mockCliClientMetadata);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n        expect(analyticsService.sendClientDeletedEvent).not.toHaveBeenCalled();\n      }\n    });\n  });\n\n  describe('sendCommand', () => {\n    it('should successfully execute ft.info command', async () => {\n      const dto: SendCommandDto = { command: 'ft.info idx' };\n      const formatSpy = jest.spyOn(rawFormatter, 'format');\n      const mockResult: SendCommandResponse = {\n        response: mockRedisFtInfoReply,\n        status: CommandExecutionStatus.Success,\n      };\n      when(standaloneClient.sendCommand)\n        .calledWith(['ft.info', 'idx'], expect.anything())\n        .mockReturnValue(mockRedisFtInfoReply);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(formatSpy).toHaveBeenCalled();\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'ft.info',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n      expect(analyticsService.sendIndexInfoEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        mockFtInfoAnalyticsData,\n      );\n    });\n\n    it('should successfully execute command (RAW format)', async () => {\n      const dto: SendCommandDto = { command: mockMemoryUsageCommand };\n      const formatSpy = jest.spyOn(rawFormatter, 'format');\n      const mockResult: SendCommandResponse = {\n        response: mockIntegerResponse,\n        status: CommandExecutionStatus.Success,\n      };\n      when(standaloneClient.sendCommand)\n        .calledWith(['memory', 'usage', 'key'], expect.anything())\n        .mockReturnValue(5);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(formatSpy).toHaveBeenCalled();\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'memory',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should successfully execute command and return raw response', async () => {\n      const dto: SendCommandDto = {\n        command: mockMemoryUsageCommand,\n        outputFormat: CliOutputFormatterTypes.Raw,\n      };\n      const formatSpy = jest.spyOn(rawFormatter, 'format');\n      const mockResult: SendCommandResponse = {\n        response: 5,\n        status: CommandExecutionStatus.Success,\n      };\n      when(standaloneClient.sendCommand)\n        .calledWith(['memory', 'usage', 'key'], expect.anything())\n        .mockReturnValue(5);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(formatSpy).toHaveBeenCalled();\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'memory',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with [CLI_COMMAND_NOT_SUPPORTED] error for sendCommand', async () => {\n      const command = CliToolUnsupportedCommands.ScriptDebug;\n      const dto: SendCommandDto = { command };\n      const mockResult: SendCommandResponse = {\n        response: ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(\n          command.toUpperCase(),\n        ),\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        new CommandNotSupportedError(\n          ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(command.toUpperCase()),\n        ),\n        {\n          command: 'script',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with [CLI_UNTERMINATED_QUOTES] error for sendCommand', async () => {\n      const command = mockGetEscapedKeyCommand;\n      const dto: SendCommandDto = { command };\n      const mockResult: SendCommandResponse = {\n        response: ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()),\n        {\n          command: unknownCommand,\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with redis reply error', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        name: 'ReplyError',\n        command: 'GET',\n      };\n      standaloneClient.sendCommand.mockRejectedValue(replyError);\n      const dto: SendCommandDto = { command: 'get hashKey' };\n      const mockResult: SendCommandResponse = {\n        response: replyError.message,\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        replyError,\n        {\n          command: 'get',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with internal exception for sendCommand', async () => {\n      const error = new Error(mockENotFoundMessage);\n      const dto: SendCommandDto = { command: 'get key' };\n      standaloneClient.sendCommand.mockRejectedValue(error);\n      const mockResult: SendCommandResponse = {\n        response: error.message,\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        new Error(mockENotFoundMessage),\n        {\n          command: 'get',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('Should proxy EncryptionService errors for sendCommand', async () => {\n      const dto: SendCommandDto = { command: 'get key' };\n      standaloneClient.sendCommand.mockRejectedValue(\n        new KeytarUnavailableException(),\n      );\n\n      try {\n        await service.sendCommand(mockCliClientMetadata, dto);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(KeytarUnavailableException);\n        expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockCliClientMetadata.databaseId,\n          new KeytarUnavailableException(),\n          {\n            command: 'get',\n            outputFormat: CliOutputFormatterTypes.Raw,\n          },\n        );\n      }\n    });\n\n    it('should return response in correct format for human-readable commands for sendCommand', async () => {\n      const dto: SendCommandDto = { command: mockServerInfoCommand };\n      const mockResult: SendCommandResponse = {\n        response: mockRedisServerInfoResponse,\n        status: CommandExecutionStatus.Success,\n      };\n      when(standaloneClient.sendCommand)\n        .calledWith(['info', 'server'], { replyEncoding: 'utf8' })\n        .mockReturnValue(mockRedisServerInfoResponse);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'info',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should call recommendationService', async () => {\n      const dto: SendCommandDto = { command: mockMemoryUsageCommand };\n      const [command] = dto.command.split(' ');\n\n      await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(recommendationService.check).toBeCalledWith(\n        mockCliClientMetadata,\n        RECOMMENDATION_NAMES.SEARCH_VISUALIZATION,\n        command,\n      );\n      expect(recommendationService.check).toBeCalledTimes(1);\n    });\n  });\n\n  describe('sendClusterCommand', () => {\n    beforeEach(async () => {\n      databaseClientFactory.getOrCreateClient = jest\n        .fn()\n        .mockResolvedValue(clusterClient);\n    });\n\n    it('should successfully execute command (RAW format)', async () => {\n      const dto: SendCommandDto = { command: mockMemoryUsageCommand };\n      const formatSpy = jest.spyOn(rawFormatter, 'format');\n      const mockResult: SendCommandResponse = {\n        response: mockIntegerResponse,\n        status: CommandExecutionStatus.Success,\n      };\n      when(clusterClient.sendCommand)\n        .calledWith(['memory', 'usage', 'key'], expect.anything())\n        .mockReturnValue(5);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(formatSpy).toHaveBeenCalled();\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'memory',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should successfully execute command and return raw response', async () => {\n      const dto: SendCommandDto = {\n        command: mockMemoryUsageCommand,\n        outputFormat: CliOutputFormatterTypes.Raw,\n      };\n      const formatSpy = jest.spyOn(rawFormatter, 'format');\n      const mockResult: SendCommandResponse = {\n        response: 5,\n        status: CommandExecutionStatus.Success,\n      };\n      when(clusterClient.sendCommand)\n        .calledWith(['memory', 'usage', 'key'], expect.anything())\n        .mockReturnValue(5);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(formatSpy).toHaveBeenCalled();\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'memory',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with [CLI_COMMAND_NOT_SUPPORTED] error for sendCommand', async () => {\n      const command = CliToolUnsupportedCommands.ScriptDebug;\n      const dto: SendCommandDto = { command };\n      const mockResult: SendCommandResponse = {\n        response: ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(\n          command.toUpperCase(),\n        ),\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        new CommandNotSupportedError(\n          ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(command.toUpperCase()),\n        ),\n        {\n          command: 'script',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with [CLI_UNTERMINATED_QUOTES] error for sendCommand', async () => {\n      const command = mockGetEscapedKeyCommand;\n      const dto: SendCommandDto = { command };\n      const mockResult: SendCommandResponse = {\n        response: ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()),\n        {\n          command: unknownCommand,\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with redis reply error', async () => {\n      const replyError: ReplyError = {\n        ...mockRedisWrongTypeError,\n        name: 'ReplyError',\n        command: 'GET',\n      };\n      clusterClient.sendCommand.mockRejectedValue(replyError);\n      const dto: SendCommandDto = { command: 'get hashKey' };\n      const mockResult: SendCommandResponse = {\n        response: replyError.message,\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        replyError,\n        {\n          command: 'get',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should return response with internal exception for sendCommand', async () => {\n      const error = new Error(mockENotFoundMessage);\n      const dto: SendCommandDto = { command: 'get key' };\n      clusterClient.sendCommand.mockRejectedValue(error);\n      const mockResult: SendCommandResponse = {\n        response: error.message,\n        status: CommandExecutionStatus.Fail,\n      };\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        new Error(mockENotFoundMessage),\n        {\n          command: 'get',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('Should proxy EncryptionService errors for sendCommand', async () => {\n      const dto: SendCommandDto = { command: 'get key' };\n      clusterClient.sendCommand.mockRejectedValue(\n        new KeytarUnavailableException(),\n      );\n\n      try {\n        await service.sendCommand(mockCliClientMetadata, dto);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(KeytarUnavailableException);\n        expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockCliClientMetadata.databaseId,\n          new KeytarUnavailableException(),\n          {\n            command: 'get',\n            outputFormat: CliOutputFormatterTypes.Raw,\n          },\n        );\n      }\n    });\n\n    it('should return response in correct format for human-readable commands for sendCommand', async () => {\n      const dto: SendCommandDto = { command: mockServerInfoCommand };\n      const mockResult: SendCommandResponse = {\n        response: mockRedisServerInfoResponse,\n        status: CommandExecutionStatus.Success,\n      };\n      when(clusterClient.sendCommand)\n        .calledWith(['info', 'server'], { replyEncoding: 'utf8' })\n        .mockReturnValue(mockRedisServerInfoResponse);\n\n      const result = await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(result).toEqual(mockResult);\n      expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCliClientMetadata.databaseId,\n        {\n          command: 'info',\n          outputFormat: CliOutputFormatterTypes.Raw,\n        },\n      );\n    });\n\n    it('should call recommendationService', async () => {\n      const dto: SendCommandDto = { command: mockMemoryUsageCommand };\n      const [command] = dto.command.split(' ');\n\n      await service.sendCommand(mockCliClientMetadata, dto);\n\n      expect(recommendationService.check).toBeCalledWith(\n        mockCliClientMetadata,\n        RECOMMENDATION_NAMES.SEARCH_VISUALIZATION,\n        command,\n      );\n      expect(recommendationService.check).toBeCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts",
    "content": "import {\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { ClientMetadata } from 'src/common/models';\nimport { RECOMMENDATION_NAMES, unknownCommand } from 'src/constants';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport {\n  CommandExecutionStatus,\n  CreateCliClientResponse,\n  DeleteClientResponse,\n  SendCommandDto,\n  SendCommandResponse,\n} from 'src/modules/cli/dto/cli.dto';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  checkHumanReadableCommands,\n  splitCliCommandLine,\n} from 'src/utils/cli-helper';\nimport {\n  CommandNotSupportedError,\n  CommandParsingError,\n} from 'src/modules/cli/constants/errors';\nimport { CliAnalyticsService } from 'src/modules/cli/services/cli-analytics/cli-analytics.service';\nimport { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions';\nimport { getUnsupportedCommands } from 'src/modules/cli/utils/getUnsupportedCommands';\nimport { ClientNotFoundErrorException } from 'src/modules/redis/exceptions/client-not-found-error.exception';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { v4 as uuidv4 } from 'uuid';\nimport { getAnalyticsDataFromIndexInfo } from 'src/utils';\nimport { OutputFormatterManager } from './output-formatter/output-formatter-manager';\nimport { CliOutputFormatterTypes } from './output-formatter/output-formatter.interface';\nimport { TextFormatterStrategy } from './output-formatter/strategies/text-formatter.strategy';\nimport { RawFormatterStrategy } from './output-formatter/strategies/raw-formatter.strategy';\n\n@Injectable()\nexport class CliBusinessService {\n  private logger = new Logger('CliService');\n\n  private outputFormatterManager: OutputFormatterManager;\n\n  constructor(\n    private cliAnalyticsService: CliAnalyticsService,\n    private recommendationService: DatabaseRecommendationService,\n    private readonly commandsService: CommandsService,\n    private databaseClientFactory: DatabaseClientFactory,\n  ) {\n    this.outputFormatterManager = new OutputFormatterManager();\n    this.outputFormatterManager.addStrategy(\n      CliOutputFormatterTypes.Text,\n      new TextFormatterStrategy(),\n    );\n    this.outputFormatterManager.addStrategy(\n      CliOutputFormatterTypes.Raw,\n      new RawFormatterStrategy(),\n    );\n  }\n\n  /**\n   * Method to create new redis client and return uuid\n   * @param clientMetadata\n   */\n  public async getClient(\n    clientMetadata: ClientMetadata,\n  ): Promise<CreateCliClientResponse> {\n    this.logger.debug('Create Redis client for CLI.', clientMetadata);\n    try {\n      const uuid = uuidv4();\n      await this.databaseClientFactory.getOrCreateClient({\n        ...clientMetadata,\n        uniqueId: uuid,\n      });\n\n      this.logger.debug(\n        'Succeed to create Redis client for CLI.',\n        clientMetadata,\n      );\n      this.cliAnalyticsService.sendClientCreatedEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n      );\n      return { uuid };\n    } catch (error) {\n      this.logger.error(\n        'Failed to create redis client for CLI.',\n        error,\n        clientMetadata,\n      );\n      this.cliAnalyticsService.sendClientCreationFailedEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n        error,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Method to close exist client and create a new one\n   * @param clientMetadata\n   */\n  public async reCreateClient(\n    clientMetadata: ClientMetadata,\n  ): Promise<CreateCliClientResponse> {\n    this.logger.debug('re-create Redis client for CLI.', clientMetadata);\n    try {\n      await this.databaseClientFactory.deleteClient(clientMetadata);\n\n      const uuid = uuidv4();\n      await this.databaseClientFactory.getOrCreateClient({\n        ...clientMetadata,\n        uniqueId: uuid,\n      });\n\n      this.logger.debug(\n        'Succeed to re-create Redis client for CLI.',\n        clientMetadata,\n      );\n      this.cliAnalyticsService.sendClientRecreatedEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n      );\n      return { uuid };\n    } catch (error) {\n      this.logger.error(\n        'Failed to re-create redis client for CLI.',\n        error,\n        clientMetadata,\n      );\n      this.cliAnalyticsService.sendClientCreationFailedEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n        error,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Method to close exist redis client\n   * @param clientMetadata\n   */\n  public async deleteClient(\n    clientMetadata: ClientMetadata,\n  ): Promise<DeleteClientResponse> {\n    this.logger.debug('Deleting Redis client for CLI.', clientMetadata);\n    try {\n      const affected = (await this.databaseClientFactory.deleteClient(\n        clientMetadata,\n      )) as unknown as number;\n      this.logger.debug(\n        'Succeed to delete Redis client for CLI.',\n        clientMetadata,\n      );\n\n      if (affected) {\n        this.cliAnalyticsService.sendClientDeletedEvent(\n          clientMetadata.sessionMetadata,\n          affected,\n          clientMetadata.databaseId,\n        );\n      }\n      return { affected };\n    } catch (error) {\n      this.logger.error(\n        'Failed to delete Redis client for CLI.',\n        error,\n        clientMetadata,\n      );\n      throw new InternalServerErrorException(error.message);\n    }\n  }\n\n  /**\n   * Method to execute cli command for redis client and return result\n   * @param clientMetadata\n   * @param dto\n   */\n  public async sendCommand(\n    clientMetadata: ClientMetadata,\n    dto: SendCommandDto,\n  ): Promise<SendCommandResponse> {\n    this.logger.debug('Executing redis CLI command.', clientMetadata);\n    const { command: commandLine } = dto;\n    const outputFormat = dto.outputFormat || CliOutputFormatterTypes.Raw;\n    let command: string = unknownCommand;\n    let args: string[] = [];\n\n    try {\n      const client: RedisClient =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const formatter = this.outputFormatterManager.getStrategy(outputFormat);\n      [command, ...args] = splitCliCommandLine(commandLine);\n      const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`)\n        ? 'utf8'\n        : undefined;\n      this.checkUnsupportedCommands(`${command} ${args[0]}`);\n\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.SEARCH_VISUALIZATION,\n        command,\n      );\n\n      const reply = await client.sendCommand([command, ...args], {\n        replyEncoding,\n      });\n\n      this.cliAnalyticsService.sendCommandExecutedEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n        {\n          command,\n          outputFormat,\n        },\n      );\n\n      if (command.toLowerCase() === 'ft.info') {\n        this.cliAnalyticsService.sendIndexInfoEvent(\n          clientMetadata.sessionMetadata,\n          clientMetadata.databaseId,\n          getAnalyticsDataFromIndexInfo(reply as string[]),\n        );\n      }\n\n      this.logger.debug(\n        'Succeed to execute redis CLI command.',\n        clientMetadata,\n      );\n\n      return {\n        response: formatter.format(reply),\n        status: CommandExecutionStatus.Success,\n      };\n    } catch (error) {\n      this.logger.error(\n        'Failed to execute redis CLI command.',\n        error,\n        clientMetadata,\n      );\n\n      if (\n        error instanceof CommandParsingError ||\n        error instanceof CommandNotSupportedError ||\n        error?.name === 'ReplyError'\n      ) {\n        this.cliAnalyticsService.sendCommandErrorEvent(\n          clientMetadata.sessionMetadata,\n          clientMetadata.databaseId,\n          error,\n          {\n            command,\n            outputFormat,\n          },\n        );\n\n        return { response: error.message, status: CommandExecutionStatus.Fail };\n      }\n\n      this.cliAnalyticsService.sendConnectionErrorEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n        error,\n        {\n          command,\n          outputFormat,\n        },\n      );\n\n      if (\n        error instanceof EncryptionServiceErrorException ||\n        error instanceof ClientNotFoundErrorException\n      ) {\n        throw error;\n      }\n\n      return { response: error.message, status: CommandExecutionStatus.Fail };\n    }\n  }\n\n  private checkUnsupportedCommands(commandLine: string) {\n    const unsupportedCommand = getUnsupportedCommands().find((command) =>\n      commandLine.toLowerCase().startsWith(command),\n    );\n    if (unsupportedCommand) {\n      throw new CommandNotSupportedError(\n        ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(\n          unsupportedCommand.toUpperCase(),\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter-manager.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { TextFormatterStrategy } from './strategies/text-formatter.strategy';\nimport {\n  CliOutputFormatterTypes,\n  IOutputFormatterStrategy,\n} from './output-formatter.interface';\nimport { OutputFormatterManager } from './output-formatter-manager';\n\nclass TestFormatterStrategy implements IOutputFormatterStrategy {\n  public format() {\n    return '';\n  }\n}\nconst strategyName = CliOutputFormatterTypes.Raw;\nconst testStrategy = new TestFormatterStrategy();\n\ndescribe('OutputFormatterManager', () => {\n  let outputFormatter: OutputFormatterManager;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [OutputFormatterManager],\n    }).compile();\n\n    outputFormatter = module.get<OutputFormatterManager>(\n      OutputFormatterManager,\n    );\n  });\n  it('Should throw error if no strategy', () => {\n    try {\n      outputFormatter.getStrategy(strategyName);\n    } catch (e) {\n      expect(e.message).toEqual(\n        `Unsupported formatter strategy: ${strategyName}`,\n      );\n    }\n  });\n  it('Should add strategy to formatter and get it back', () => {\n    outputFormatter.addStrategy(strategyName, testStrategy);\n    expect(outputFormatter.getStrategy(strategyName)).toEqual(testStrategy);\n  });\n  it('Should support TextFormatter strategy', () => {\n    outputFormatter.addStrategy(\n      CliOutputFormatterTypes.Text,\n      new TextFormatterStrategy(),\n    );\n    expect(\n      outputFormatter.getStrategy(CliOutputFormatterTypes.Text),\n    ).toBeInstanceOf(TextFormatterStrategy);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter-manager.ts",
    "content": "import {\n  CliOutputFormatterTypes,\n  IOutputFormatterStrategy,\n} from './output-formatter.interface';\n\nexport class OutputFormatterManager {\n  private strategies = {};\n\n  addStrategy(\n    name: CliOutputFormatterTypes,\n    strategy: IOutputFormatterStrategy,\n  ): void {\n    this.strategies[name] = strategy;\n  }\n\n  getStrategy(name: CliOutputFormatterTypes): IOutputFormatterStrategy {\n    if (!this.strategies[name]) {\n      throw new Error(`Unsupported formatter strategy: ${name}`);\n    }\n\n    return this.strategies[name];\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter.interface.ts",
    "content": "export enum CliOutputFormatterTypes {\n  Text = 'TEXT',\n  Raw = 'RAW',\n}\n\nexport interface IRedirectionInfo {\n  slot: string;\n  address: string;\n}\n\nexport interface IOutputFormatterStrategy {\n  format(reply: any): any;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/strategies/raw-formatter.strategy.spec.ts",
    "content": "import { RawFormatterStrategy } from './raw-formatter.strategy';\n\ndescribe('Cli RawFormatterStrategy', () => {\n  let strategy;\n  beforeEach(async () => {\n    strategy = new RawFormatterStrategy();\n  });\n\n  describe('format', () => {\n    it('should return correct value for null', () => {\n      const input = null;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(null);\n    });\n    it('should return correct value for integer', () => {\n      const input = 1;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(input);\n    });\n    it('should return correct value for string', () => {\n      const input = Buffer.from('string value');\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual('string value');\n    });\n    it('should return correct value for empty array', () => {\n      const input = [];\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual([]);\n    });\n    it('should return correct value for nested array', () => {\n      const input = [\n        Buffer.from('0'),\n        [\n          Buffer.from('key'),\n          Buffer.from('\"quoted\"\"key\"'),\n          Buffer.from('\"quoted key\"'),\n        ],\n      ];\n      const mockResponse = [\n        '0',\n        ['key', '\\\\\"quoted\\\\\"\\\\\"key\\\\\"', '\\\\\"quoted key\\\\\"'],\n      ];\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should return correct value for object', () => {\n      const input = {\n        field: Buffer.from('value'),\n        secondField: Buffer.from('value'),\n      };\n      const mockResponse = {\n        field: 'value',\n        secondField: 'value',\n      };\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should correctly return stringified json', () => {\n      const object = {\n        key: 'value',\n      };\n      const input = Buffer.from(JSON.stringify(object));\n      const output = strategy.format(input);\n\n      expect(output).toEqual('{\\\\\"key\\\\\":\\\\\"value\\\\\"}');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/strategies/raw-formatter.strategy.ts",
    "content": "import { isArray, isObject } from 'lodash';\nimport { getASCIISafeStringFromBuffer } from 'src/utils/cli-helper';\nimport { IOutputFormatterStrategy } from '../output-formatter.interface';\n\nexport class RawFormatterStrategy implements IOutputFormatterStrategy {\n  public format(reply: any): any {\n    if (reply instanceof Buffer) {\n      return getASCIISafeStringFromBuffer(reply);\n    }\n    if (isArray(reply)) {\n      return this.formatRedisArrayReply(reply);\n    }\n    if (isObject(reply)) {\n      return this.formatRedisObjectReply(reply);\n    }\n    return reply;\n  }\n\n  private formatRedisArrayReply(reply: Buffer | Buffer[]): any[] {\n    let result: any;\n    if (isArray(reply)) {\n      if (!reply.length) {\n        result = [];\n      } else {\n        result = reply.map((item) => this.formatRedisArrayReply(item));\n      }\n    } else {\n      result = this.format(reply);\n    }\n    return result;\n  }\n\n  private formatRedisObjectReply(reply: Object): object | string {\n    const result = {};\n\n    if (reply instanceof Error) {\n      return reply.toString();\n    }\n\n    Object.keys(reply).forEach((key) => {\n      result[key] = this.format(reply[key]);\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/strategies/text-formatter.strategy.spec.ts",
    "content": "import { TextFormatterStrategy } from './text-formatter.strategy';\n\ndescribe('Cli TextFormatterStrategy', () => {\n  let strategy;\n  beforeEach(async () => {\n    strategy = new TextFormatterStrategy();\n  });\n\n  describe('format', () => {\n    it('should return correct value for null', () => {\n      const input = null;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual('(nil)');\n    });\n    it('should return correct value for integer', () => {\n      const input = 1;\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(`(integer) ${input}`);\n    });\n    it('should return correct value for string', () => {\n      const input = Buffer.from('string value');\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual(`\"${input}\"`);\n    });\n    it('should return correct value for empty array', () => {\n      const input = [];\n\n      const output = strategy.format(input);\n\n      expect(output).toEqual('(empty list or set)');\n    });\n    it('should return correct value for nested array', () => {\n      const input = [\n        Buffer.from('0'),\n        [\n          Buffer.from('key'),\n          Buffer.from('\"quoted\"\"key\"'),\n          Buffer.from('\"quoted key\"'),\n        ],\n      ];\n      const mockResponse =\n        '1) \"0\"\\n2) 1) \"key\"\\n   2) \"\\\\\"quoted\\\\\"\\\\\"key\\\\\"\"\\n   3) \"\\\\\"quoted key\\\\\"\"';\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should return correct value for object', () => {\n      const input = {\n        field: Buffer.from('value'),\n        secondField: Buffer.from('value'),\n      };\n      const mockResponse =\n        '1) \"field\"\\n2) \"value\"\\n3) \"secondField\"\\n4) \"value\"';\n      const output = strategy.format(input);\n\n      expect(output).toEqual(mockResponse);\n    });\n    it('should correctly handle special characters', () => {\n      const input = Buffer.from('\\u0007\\b\\t\\n\\r\\\\');\n      const output = strategy.format(input);\n\n      expect(output).toEqual('\"\\\\a\\\\b\\\\t\\\\n\\\\r\\\\\\\\\"');\n    });\n    it('should correctly handle hexadecimal', () => {\n      const input = Buffer.from('aced000573720008456d706c6f796565', 'hex');\n      const output = strategy.format(input);\n\n      expect(output).toEqual('\"\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\bEmployee\"');\n    });\n    it('should correctly stringified json', () => {\n      const object = {\n        key: 'value',\n      };\n      const input = Buffer.from(JSON.stringify(object));\n      const output = strategy.format(input);\n\n      expect(output).toEqual('\"{\\\\\"key\\\\\":\\\\\"value\\\\\"}\"');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/strategies/text-formatter.strategy.ts",
    "content": "import { flattenDeep, isArray, isInteger, isNull, isObject } from 'lodash';\nimport { IS_NON_PRINTABLE_ASCII_CHARACTER } from 'src/constants';\nimport { decimalToHexString } from 'src/utils/cli-helper';\nimport { IOutputFormatterStrategy } from '../output-formatter.interface';\n\nexport class TextFormatterStrategy implements IOutputFormatterStrategy {\n  public format(reply: any): string {\n    let result;\n    if (isNull(reply)) {\n      result = '(nil)';\n    } else if (isInteger(reply)) {\n      result = `(integer) ${reply}`;\n    } else if (reply instanceof Buffer) {\n      result = this.formatRedisBufferReply(reply);\n    } else if (isArray(reply)) {\n      result = this.formatRedisArrayReply(reply);\n    } else if (isObject(reply)) {\n      result = this.formatRedisArrayReply(flattenDeep(Object.entries(reply)));\n    } else {\n      result = reply;\n    }\n    return result;\n  }\n\n  private formatRedisArrayReply(reply: Buffer | Buffer[], level = 0): string {\n    let result: string;\n    if (isArray(reply)) {\n      if (!reply.length) {\n        result = '(empty list or set)';\n      } else {\n        result = reply\n          .map((item, index) => {\n            const leftMargin = index > 0 ? '   '.repeat(level) : '';\n            const lineIndex = `${leftMargin}${index + 1})`;\n            const value = this.formatRedisArrayReply(item, level + 1);\n            return `${lineIndex} ${value}`;\n          })\n          .join('\\n');\n      }\n    } else {\n      result =\n        reply instanceof Buffer\n          ? this.formatRedisBufferReply(reply)\n          : JSON.stringify(reply);\n    }\n    return result;\n  }\n\n  private formatRedisBufferReply(reply: Buffer): string {\n    // Produces an escaped string representation of a byte string.\n    // Ported from sdscatrepr() function in sds.c from Redis source code.\n    // This is the function redis-cli uses to escape strings for output.\n    let result = '\"';\n    reply.forEach((byte: number) => {\n      const char = Buffer.from([byte]).toString();\n      if (IS_NON_PRINTABLE_ASCII_CHARACTER.test(char)) {\n        result += `\\\\x${decimalToHexString(byte)}`;\n      } else {\n        switch (char) {\n          case '\\\\':\n            result += `\\\\${char}`;\n            break;\n          case '\\u0007': // Bell character\n            result += '\\\\a';\n            break;\n          case '\"':\n            result += `\\\\${char}`;\n            break;\n          case '\\b':\n            result += '\\\\b';\n            break;\n          case '\\t':\n            result += '\\\\t';\n            break;\n          case '\\n':\n            result += '\\\\n';\n            break;\n          case '\\r':\n            result += '\\\\r';\n            break;\n          default:\n            result += char;\n        }\n      }\n    });\n    result += '\"';\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/services/cli-business/output-formatter/strategies/utf-8-formatter.strategy.ts",
    "content": "import { isArray, isObject } from 'lodash';\nimport { getUTF8FromBuffer } from 'src/utils/cli-helper';\nimport { IOutputFormatterStrategy } from '../output-formatter.interface';\n\nexport class UTF8FormatterStrategy implements IOutputFormatterStrategy {\n  public format(reply: any): any {\n    if (reply instanceof Buffer) {\n      return getUTF8FromBuffer(reply);\n    }\n    if (isArray(reply)) {\n      return this.formatRedisArrayReply(reply);\n    }\n    if (isObject(reply)) {\n      return this.formatRedisObjectReply(reply);\n    }\n    return reply;\n  }\n\n  private formatRedisArrayReply(reply: Buffer | Buffer[]): any[] {\n    let result: any;\n    if (isArray(reply)) {\n      if (!reply.length) {\n        result = [];\n      } else {\n        result = reply.map((item) => this.formatRedisArrayReply(item));\n      }\n    } else {\n      result = this.format(reply);\n    }\n    return result;\n  }\n\n  private formatRedisObjectReply(reply: Object): object | string {\n    const result = {};\n\n    if (reply instanceof Error) {\n      return reply.toString();\n    }\n\n    Object.keys(reply).forEach((key) => {\n      result[key] = this.format(reply[key]);\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.spec.ts",
    "content": "import { getUnsupportedCommands } from './getUnsupportedCommands';\n\ndescribe('cli unsupported commands', () => {\n  it('should return correct list', () => {\n    const expectedResult = [\n      'monitor',\n      'subscribe',\n      'psubscribe',\n      'ssubscribe',\n      'sync',\n      'psync',\n      'script debug',\n      'hello 3',\n    ];\n\n    expect(getUnsupportedCommands()).toEqual(expectedResult);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cli/utils/getUnsupportedCommands.ts",
    "content": "import config from 'src/utils/config';\n\nconst REDIS_CLI_CONFIG = config.get('redis_cli');\n\nexport enum CliToolUnsupportedCommands {\n  Monitor = 'monitor',\n  Subscribe = 'subscribe',\n  PSubscribe = 'psubscribe',\n  SSubscribe = 'ssubscribe',\n  Sync = 'sync',\n  PSync = 'psync',\n  ScriptDebug = 'script debug',\n  Hello3 = 'hello 3',\n}\n\nexport const getUnsupportedCommands = (): string[] => [\n  ...Object.values(CliToolUnsupportedCommands),\n  ...REDIS_CLI_CONFIG.unsupportedCommands,\n];\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/cloud-auth.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { GithubIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy';\nimport { mockCloudApiAuthDto, mockSessionMetadata } from 'src/__mocks__';\nimport {\n  mockCloudAuthCode,\n  mockCloudAuthGithubRequest,\n  mockCloudAuthGithubTokenParams,\n  mockCloudAuthGoogleAuthUrl,\n  mockCloudAuthGoogleRenewTokenUrl,\n  mockCloudAuthGoogleRequest,\n  mockCloudAuthGoogleRevokeTokenUrl,\n  mockCloudAuthGoogleTokenUrl,\n  mockCloudRefreshToken,\n  mockCloudRevokeRefreshTokenHint,\n  mockOktaAuthClient,\n} from 'src/__mocks__/cloud-auth';\nimport { OktaAuth } from '@okta/okta-auth-js';\nimport { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';\nimport { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';\n\njest.mock('@okta/okta-auth-js');\n\ndescribe('CloudAuthStrategy', () => {\n  let googleStrategy: GoogleIdpCloudAuthStrategy;\n  let githubStrategy: GithubIdpCloudAuthStrategy;\n\n  beforeEach(async () => {\n    (OktaAuth as any).mockReturnValueOnce(mockOktaAuthClient);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EventEmitter2,\n        GithubIdpCloudAuthStrategy,\n        GoogleIdpCloudAuthStrategy,\n      ],\n    }).compile();\n\n    googleStrategy = await module.get(GoogleIdpCloudAuthStrategy);\n    githubStrategy = await module.get(GithubIdpCloudAuthStrategy);\n  });\n\n  describe('generateAuthRequest', () => {\n    it('Check that Google auth request is generated', async () => {\n      expect(\n        await googleStrategy.generateAuthRequest(mockSessionMetadata),\n      ).toEqual({\n        ...mockCloudAuthGoogleRequest,\n        createdAt: expect.anything(),\n      });\n    });\n\n    it('Check that Github auth request is generated', async () => {\n      mockOktaAuthClient.token.prepareTokenParams.mockResolvedValueOnce(\n        mockCloudAuthGithubTokenParams,\n      );\n      expect(\n        await githubStrategy.generateAuthRequest(mockSessionMetadata),\n      ).toEqual({\n        ...mockCloudAuthGithubRequest,\n        createdAt: expect.anything(),\n      });\n    });\n  });\n\n  describe('generateAuthUrl', () => {\n    it('Should generate proper auth url', () => {\n      expect(\n        CloudAuthStrategy.generateAuthUrl(mockCloudAuthGoogleRequest),\n      ).toEqual(new URL(mockCloudAuthGoogleAuthUrl));\n    });\n  });\n\n  describe('generateRenewTokensUrl', () => {\n    it('Should generate proper renew url', () => {\n      expect(\n        googleStrategy.generateRenewTokensUrl(mockCloudApiAuthDto.refreshToken),\n      ).toEqual(new URL(mockCloudAuthGoogleRenewTokenUrl));\n    });\n  });\n\n  describe('generateRevokeTokensUrl', () => {\n    it('Should generate proper revoke url', () => {\n      expect(\n        googleStrategy.generateRevokeTokensUrl(\n          mockCloudRefreshToken,\n          mockCloudRevokeRefreshTokenHint,\n        ),\n      ).toEqual(new URL(mockCloudAuthGoogleRevokeTokenUrl));\n    });\n  });\n\n  describe('generateExchangeCodeUrl', () => {\n    it('Should generate exchange code url', () => {\n      expect(\n        CloudAuthStrategy.generateExchangeCodeUrl(\n          mockCloudAuthGoogleRequest,\n          mockCloudAuthCode,\n        ),\n      ).toEqual(new URL(mockCloudAuthGoogleTokenUrl));\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/cloud-auth.strategy.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport {\n  CloudAuthRequest,\n  CloudAuthRequestOptions,\n} from 'src/modules/cloud/auth/models/cloud-auth-request';\nimport { SessionMetadata } from 'src/common/models';\nimport { OktaAuth } from '@okta/okta-auth-js';\nimport { plainToInstance } from 'class-transformer';\n\n@Injectable()\nexport abstract class CloudAuthStrategy {\n  protected config;\n\n  /**\n   * Create and store auth request params\n   */\n  async generateAuthRequest(\n    sessionMetadata: SessionMetadata,\n    _options?: CloudAuthRequestOptions,\n  ): Promise<CloudAuthRequest> {\n    const authClient = new OktaAuth(this.config);\n    const tokenParams = await authClient.token.prepareTokenParams(this.config);\n\n    return plainToInstance(CloudAuthRequest, {\n      ...this.config,\n      ...tokenParams,\n      sessionMetadata,\n      createdAt: new Date(),\n    });\n  }\n\n  generateRenewTokensUrl(refreshToken: string): URL {\n    const url = new URL(this.config.tokenUrl, this.config.issuer);\n    url.searchParams.append('client_id', this.config.clientId);\n    url.searchParams.append('grant_type', 'refresh_token');\n    url.searchParams.append('redirect_uri', this.config.redirectUri);\n    url.searchParams.append('scope', this.config.scopes.join(' '));\n    url.searchParams.append('refresh_token', refreshToken);\n\n    return url;\n  }\n\n  generateRevokeTokensUrl(token: string, hint: string): URL {\n    const url = new URL(this.config.revokeTokenUrl, this.config.issuer);\n    url.searchParams.append('client_id', this.config.clientId);\n    url.searchParams.append('token_type_hint', hint);\n    url.searchParams.append('token', token);\n\n    return url;\n  }\n\n  static generateAuthUrl(authRequest: any): URL {\n    const url = new URL(authRequest.authorizeUrl, authRequest.issuer);\n    url.searchParams.append('client_id', authRequest.clientId);\n    url.searchParams.append('redirect_uri', authRequest.redirectUri);\n    url.searchParams.append('response_type', authRequest.responseType);\n    url.searchParams.append('response_mode', authRequest.responseMode);\n    url.searchParams.append('idp', authRequest.idp);\n    url.searchParams.append('state', authRequest.state);\n    url.searchParams.append('nonce', authRequest.nonce);\n    url.searchParams.append(\n      'code_challenge_method',\n      authRequest.codeChallengeMethod,\n    );\n    url.searchParams.append('code_challenge', authRequest.codeChallenge);\n    url.searchParams.append('scope', authRequest.scopes.join(' '));\n    url.searchParams.append('prompt', 'login');\n\n    return url;\n  }\n\n  static generateExchangeCodeUrl(authRequest: any, code: string): URL {\n    const url = new URL(authRequest.tokenUrl, authRequest.issuer);\n    url.searchParams.append('client_id', authRequest.clientId);\n    url.searchParams.append('grant_type', 'authorization_code');\n    url.searchParams.append('code', code);\n    url.searchParams.append('code_verifier', authRequest.codeVerifier);\n    url.searchParams.append('redirect_uri', authRequest.redirectUri);\n    url.searchParams.append('state', authRequest.state);\n    url.searchParams.append('nonce', authRequest.nonce);\n    url.searchParams.append('idp', authRequest.idp);\n\n    return url;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy.ts",
    "content": "import { SimpleStorage } from '@okta/okta-auth-js';\nimport config from 'src/utils/config';\nimport { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';\nimport { CloudAuthIdpType } from 'src/modules/cloud/auth/models/cloud-auth-request';\n\nconst {\n  idp: { github: idpConfig },\n} = config.get('cloud');\n\nexport class GithubIdpCloudAuthStrategy extends CloudAuthStrategy {\n  constructor() {\n    super();\n\n    this.config = {\n      idpType: CloudAuthIdpType.GitHub,\n      authorizeUrl: idpConfig.authorizeUrl,\n      tokenUrl: idpConfig.tokenUrl,\n      revokeTokenUrl: idpConfig.revokeTokenUrl,\n      issuer: idpConfig.issuer,\n      clientId: idpConfig.clientId,\n      pkce: true,\n      redirectUri: idpConfig.redirectUri,\n      idp: idpConfig.idp,\n      scopes: ['offline_access', 'openid', 'email', 'profile'],\n      responseMode: 'query',\n      responseType: 'code',\n      tokenManager: {\n        storage: {} as SimpleStorage,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy.ts",
    "content": "import { SimpleStorage } from '@okta/okta-auth-js';\nimport config from 'src/utils/config';\nimport { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';\nimport { CloudAuthIdpType } from 'src/modules/cloud/auth/models/cloud-auth-request';\n\nconst {\n  idp: { google: idpConfig },\n} = config.get('cloud');\n\nexport class GoogleIdpCloudAuthStrategy extends CloudAuthStrategy {\n  constructor() {\n    super();\n\n    this.config = {\n      idpType: CloudAuthIdpType.Google,\n      authorizeUrl: idpConfig.authorizeUrl,\n      tokenUrl: idpConfig.tokenUrl,\n      revokeTokenUrl: idpConfig.revokeTokenUrl,\n      issuer: idpConfig.issuer,\n      clientId: idpConfig.clientId,\n      pkce: true,\n      redirectUri: idpConfig.redirectUri,\n      idp: idpConfig.idp,\n      scopes: ['offline_access', 'openid', 'email', 'profile'],\n      responseMode: 'query',\n      responseType: 'code',\n      tokenManager: {\n        storage: {} as SimpleStorage,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy.spec.ts",
    "content": "import axios from 'axios';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport {\n  mockCloudAuthSsoRequest,\n  mockCloudAuthSsoTokenParams,\n  mockOktaAuthClient,\n} from 'src/__mocks__/cloud-auth';\nimport { OktaAuth } from '@okta/okta-auth-js';\nimport { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy';\nimport { CloudAuthIdpType } from 'src/modules/cloud/auth/models';\nimport { CloudOauthSsoUnsupportedEmailException } from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception';\n\njest.mock('@okta/okta-auth-js');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\n\ndescribe('CloudAuthStrategy', () => {\n  let ssoStrategy: SsoIdpCloudAuthStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('axios', () => mockedAxios);\n    (OktaAuth as any).mockReturnValueOnce(mockOktaAuthClient);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, SsoIdpCloudAuthStrategy],\n    }).compile();\n\n    ssoStrategy = await module.get(SsoIdpCloudAuthStrategy);\n  });\n\n  describe('generateAuthRequest', () => {\n    it('Check that Sso auth request is generated', async () => {\n      mockOktaAuthClient.token.prepareTokenParams.mockResolvedValueOnce(\n        mockCloudAuthSsoTokenParams,\n      );\n      mockedAxios.get.mockResolvedValue({ data: mockCloudAuthSsoRequest.idp });\n\n      expect(\n        await ssoStrategy.generateAuthRequest(mockSessionMetadata, {\n          strategy: CloudAuthIdpType.Sso,\n          data: {\n            email: '1@mail.com',\n          },\n        }),\n      ).toEqual({\n        ...mockCloudAuthSsoRequest,\n        createdAt: expect.anything(),\n      });\n    });\n    it('should throw CloudOauthSsoUnsupportedEmailException in case of idp check error ', async () => {\n      mockOktaAuthClient.token.prepareTokenParams.mockResolvedValueOnce(\n        mockCloudAuthSsoTokenParams,\n      );\n      mockedAxios.get.mockRejectedValueOnce(new Error());\n\n      await expect(\n        ssoStrategy.generateAuthRequest(mockSessionMetadata, {\n          strategy: CloudAuthIdpType.Sso,\n        }),\n      ).rejects.toThrow(CloudOauthSsoUnsupportedEmailException);\n    });\n    it('should throw CloudOauthSsoUnsupportedEmailException in case of idp check error ', async () => {\n      mockOktaAuthClient.token.prepareTokenParams.mockResolvedValueOnce(\n        mockCloudAuthSsoTokenParams,\n      );\n      mockedAxios.get.mockRejectedValueOnce(new Error());\n\n      await expect(\n        ssoStrategy.generateAuthRequest(mockSessionMetadata),\n      ).rejects.toThrow(CloudOauthSsoUnsupportedEmailException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy.ts",
    "content": "import { OktaAuth, SimpleStorage } from '@okta/okta-auth-js';\nimport config from 'src/utils/config';\nimport { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';\nimport {\n  CloudAuthIdpType,\n  CloudAuthRequest,\n  CloudAuthRequestOptions,\n} from 'src/modules/cloud/auth/models/cloud-auth-request';\nimport { SessionMetadata } from 'src/common/models';\nimport { plainToInstance } from 'class-transformer';\nimport axios from 'axios';\nimport * as path from 'path';\nimport { CloudOauthSsoUnsupportedEmailException } from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception';\nimport { Logger } from '@nestjs/common';\n\nconst {\n  idp: { sso: idpConfig },\n} = config.get('cloud');\nconst cloudConfig = config.get('cloud');\n\nexport class SsoIdpCloudAuthStrategy extends CloudAuthStrategy {\n  private logger = new Logger('SsoIdpCloudAuthStrategy');\n\n  constructor() {\n    super();\n\n    this.config = {\n      idpType: CloudAuthIdpType.Sso,\n      authorizeUrl: idpConfig.authorizeUrl,\n      tokenUrl: idpConfig.tokenUrl,\n      revokeTokenUrl: idpConfig.revokeTokenUrl,\n      issuer: idpConfig.issuer,\n      clientId: idpConfig.clientId,\n      pkce: true,\n      redirectUri: idpConfig.redirectUri,\n      idp: idpConfig.idp,\n      scopes: ['offline_access', 'openid', 'email', 'profile'],\n      responseMode: 'query',\n      responseType: 'code',\n      tokenManager: {\n        storage: {} as SimpleStorage,\n      },\n    };\n  }\n\n  private async determineIdp(email: string) {\n    try {\n      const apiUrl = new URL(\n        path.posix.join(cloudConfig.apiUrl, idpConfig.emailVerificationUri),\n      );\n      apiUrl.searchParams.set('email', email);\n      const { data: idp } = await axios.get(apiUrl.toString());\n\n      return idp;\n    } catch (e) {\n      this.logger.error('Unable to get idp by email', e);\n      throw new CloudOauthSsoUnsupportedEmailException();\n    }\n  }\n\n  /**\n   * Create and store auth request params\n   */\n  async generateAuthRequest(\n    sessionMetadata: SessionMetadata,\n    options?: CloudAuthRequestOptions,\n  ): Promise<CloudAuthRequest> {\n    const idp = await this.determineIdp(options?.data?.email);\n    const authClient = new OktaAuth(this.config);\n    const tokenParams = await authClient.token.prepareTokenParams(this.config);\n\n    return plainToInstance(CloudAuthRequest, {\n      ...this.config,\n      ...tokenParams,\n      idp,\n      sessionMetadata,\n      createdAt: new Date(),\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/auth-strategy/tcp-cloud.auth.strategy.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { createServer, Server } from 'net';\nimport { CloudAuthStrategy } from './cloud-auth.strategy';\nimport { CloudAuthService } from '../cloud-auth.service';\n\n@Injectable()\nexport class TcpCloudAuthStrategy extends CloudAuthStrategy {\n  private authPort = process.env.TCP_LOCAL_CLOUD_AUTH_PORT\n    ? parseInt(process.env.TCP_LOCAL_CLOUD_AUTH_PORT)\n    : 5542;\n\n  private server: Server;\n\n  private readonly logger = new Logger('TcpCloudAuthStrategy');\n\n  constructor(private readonly cloudAuthService: CloudAuthService) {\n    super();\n\n    this.initServer();\n  }\n\n  private initServer() {\n    this.server = createServer((socket) => {\n      socket.setEncoding('utf8');\n\n      socket.on('data', async (data) => {\n        try {\n          this.logger.debug(`Received raw data: ${data.toString()}`);\n          const { action, options } = JSON.parse(data.toString());\n          this.logger.debug('Parsed request:', { action, options });\n\n          if (action === 'getAuthUrl') {\n            try {\n              const url = await this.cloudAuthService.getAuthorizationUrl(\n                options.sessionMetadata,\n                options.authOptions,\n              );\n              this.logger.debug('Generated URL:', url);\n              socket.write(JSON.stringify({ success: true, url }));\n            } catch (err) {\n              this.logger.error('Error getting authorization URL:', err);\n              socket.write(\n                JSON.stringify({\n                  success: false,\n                  error: err.message,\n                  details: err.stack,\n                  context: { action, options }, // Add the context to help debug\n                }),\n              );\n            }\n          } else if (action === 'handleCallback') {\n            try {\n              this.logger.debug('Handling callback with query:', options.query);\n              const result = await this.cloudAuthService.handleCallback(\n                options.query,\n              );\n              socket.write(JSON.stringify({ success: true, result }));\n            } catch (err) {\n              this.logger.error('Error handling callback:', err);\n              socket.write(\n                JSON.stringify({\n                  success: false,\n                  error: err.message,\n                  details: err.stack,\n                }),\n              );\n            }\n          }\n        } catch (err) {\n          this.logger.error('Error processing request:', err);\n          socket.write(\n            JSON.stringify({\n              success: false,\n              error: err.message,\n              details: err.stack,\n            }),\n          );\n        }\n        socket.end();\n      });\n\n      socket.on('end', () => {\n        this.logger.debug('Client connection ended');\n      });\n\n      socket.on('error', (err) => {\n        this.logger.error('Socket error:', err);\n      });\n    });\n\n    this.server.listen(this.authPort, () => {\n      this.logger.log(`TCP server listening on port ${this.authPort}`);\n    });\n\n    this.server.on('error', (err) => {\n      this.logger.error('Server error:', err);\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/cloud-auth.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { HttpException, InternalServerErrorException } from '@nestjs/common';\nimport { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics';\nimport { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.flag';\nimport { mockSessionMetadata } from 'src/__mocks__';\n\ndescribe('CloudAuthAnalytics', () => {\n  let service: CloudAuthAnalytics;\n  let sendEventMethod;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, CloudAuthAnalytics],\n    }).compile();\n\n    service = await module.get(CloudAuthAnalytics);\n    sendEventMethod = jest.spyOn<CloudAuthAnalytics, any>(service, 'sendEvent');\n    sendFailedEventMethod = jest.spyOn<CloudAuthAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendCloudSignInSucceeded', () => {\n    it('should emit event with deep link flow', () => {\n      service.sendCloudSignInSucceeded(\n        mockSessionMetadata,\n        CloudSsoFeatureStrategy.DeepLink,\n        'import-database',\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInSucceeded,\n        {\n          flow: CloudSsoFeatureStrategy.DeepLink,\n          action: 'import-database',\n        },\n      );\n    });\n    it('should emit event with web flow', () => {\n      service.sendCloudSignInSucceeded(\n        mockSessionMetadata,\n        CloudSsoFeatureStrategy.Web,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInSucceeded,\n        {\n          flow: CloudSsoFeatureStrategy.Web,\n        },\n      );\n    });\n    it('should emit event without flow and not fail', () => {\n      service.sendCloudSignInSucceeded(\n        mockSessionMetadata,\n        undefined as CloudSsoFeatureStrategy,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInSucceeded,\n        {\n          flow: undefined,\n        },\n      );\n    });\n  });\n\n  describe('sendGetRedisCloudSubsFailedEvent', () => {\n    it('should emit error event with deep link flow', () => {\n      service.sendCloudSignInFailed(\n        mockSessionMetadata,\n        httpException,\n        CloudSsoFeatureStrategy.DeepLink,\n        'import',\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInFailed,\n        httpException,\n        { flow: CloudSsoFeatureStrategy.DeepLink, action: 'import' },\n      );\n    });\n    it('should emit error event with web flow', () => {\n      service.sendCloudSignInFailed(\n        mockSessionMetadata,\n        httpException,\n        CloudSsoFeatureStrategy.Web,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInFailed,\n        httpException,\n        { flow: CloudSsoFeatureStrategy.Web },\n      );\n    });\n    it('should not fail if no exception passed', () => {\n      service.sendCloudSignInFailed(\n        mockSessionMetadata,\n        undefined,\n        CloudSsoFeatureStrategy.Web,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInFailed,\n        undefined,\n        { flow: CloudSsoFeatureStrategy.Web },\n      );\n    });\n    it('should emit error event without flow and not fail', () => {\n      const exception = new InternalServerErrorException() as any;\n      exception.options = undefined;\n\n      service.sendCloudSignInFailed(\n        mockSessionMetadata,\n        exception as unknown as HttpException,\n        undefined as CloudSsoFeatureStrategy,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudSignInFailed,\n        exception,\n        { flow: undefined },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/cloud-auth.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.flag';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CloudAuthAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendCloudSignInSucceeded(\n    sessionMetadata: SessionMetadata,\n    flow: CloudSsoFeatureStrategy,\n    action?: string,\n  ) {\n    this.sendEvent(sessionMetadata, TelemetryEvents.CloudSignInSucceeded, {\n      flow,\n      action,\n    });\n  }\n\n  sendCloudSignInFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    flow?: CloudSsoFeatureStrategy,\n    action?: string,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.CloudSignInFailed,\n      exception,\n      {\n        flow,\n        action,\n        errorDescription: exception?.['options']?.['description'],\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/cloud-auth.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  Query,\n  UsePipes,\n  ValidationPipe,\n  Render,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\n\n@ApiTags('Cloud Auth')\n@Controller('cloud')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CloudAuthController {\n  constructor(private readonly cloudAuthService: CloudAuthService) {}\n\n  @Get('oauth/callback')\n  @ApiEndpoint({\n    description: 'OAuth callback',\n    statusCode: 200,\n  })\n  @Render('cloud_oauth_callback')\n  async callback(@Query() query) {\n    return this.cloudAuthService.handleCallback(query);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/cloud-auth.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudSessionModule } from 'src/modules/cloud/session/cloud-session.module';\nimport { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';\nimport { GithubIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy';\nimport { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\nimport { CloudAuthController } from 'src/modules/cloud/auth/cloud-auth.controller';\nimport { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics';\nimport { TcpCloudAuthStrategy } from './auth-strategy/tcp-cloud.auth.strategy';\n\n@Module({\n  imports: [CloudSessionModule],\n  providers: [\n    GoogleIdpCloudAuthStrategy,\n    GithubIdpCloudAuthStrategy,\n    SsoIdpCloudAuthStrategy,\n    CloudAuthService,\n    CloudAuthAnalytics,\n    ...(process.env.USE_TCP_CLOUD_AUTH === 'true'\n      ? [TcpCloudAuthStrategy]\n      : []),\n  ],\n  controllers: [CloudAuthController],\n  exports: [\n    CloudAuthService,\n    ...(process.env.USE_TCP_CLOUD_AUTH === 'true'\n      ? [TcpCloudAuthStrategy]\n      : []),\n  ],\n})\nexport class CloudAuthModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/cloud-auth.service.spec.ts",
    "content": "import axios from 'axios';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\nimport {\n  mockCloudAccessTokenNew,\n  mockCloudAuthAnalytics,\n  mockCloudAuthCode,\n  mockCloudAuthGithubAuthUrl,\n  mockCloudAuthGithubCallbackQueryObject,\n  mockCloudAuthGithubRequest,\n  mockCloudAuthGoogleAuthUrl,\n  mockCloudAuthGoogleCallbackQueryObject,\n  mockCloudAuthGoogleRenewTokenUrl,\n  mockCloudAuthGoogleRequest,\n  mockCloudAuthGoogleRevokeTokenUrl,\n  mockCloudAuthGoogleTokenUrl,\n  mockCloudAuthResponse,\n  mockCloudRefreshTokenNew,\n  mockGithubIdpCloudAuthStrategy,\n  mockGoogleIdpCloudAuthStrategy,\n  mockSsoIdpCloudAuthStrategy,\n  mockTokenResponse,\n  mockTokenResponseNew,\n} from 'src/__mocks__/cloud-auth';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport {\n  mockAxiosBadRequestError,\n  mockCloudApiAuthDto,\n  mockCloudSessionService,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { GithubIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy';\nimport { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';\nimport { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics';\nimport {\n  CloudAuthIdpType,\n  CloudAuthStatus,\n} from 'src/modules/cloud/auth/models';\nimport {\n  CloudOauthMisconfigurationException,\n  CloudOauthMissedRequiredDataException,\n  CloudOauthUnexpectedErrorException,\n  CloudOauthUnknownAuthorizationRequestException,\n} from 'src/modules/cloud/auth/exceptions';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.flag';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy';\nimport { CloudOauthSsoUnsupportedEmailException } from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\n\ndescribe('CloudAuthService', () => {\n  let service: CloudAuthService;\n  let analytics: MockType<CloudAuthAnalytics>;\n  let sessionService: MockType<CloudSessionService>;\n  let ssoIdpCLoudAuthStrategy: MockType<SsoIdpCloudAuthStrategy>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('axios', () => mockedAxios);\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EventEmitter2,\n        CloudAuthService,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n        {\n          provide: GithubIdpCloudAuthStrategy,\n          useFactory: mockGithubIdpCloudAuthStrategy,\n        },\n        {\n          provide: GoogleIdpCloudAuthStrategy,\n          useFactory: mockGoogleIdpCloudAuthStrategy,\n        },\n        {\n          provide: SsoIdpCloudAuthStrategy,\n          useFactory: mockSsoIdpCloudAuthStrategy,\n        },\n        {\n          provide: CloudAuthAnalytics,\n          useFactory: mockCloudAuthAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CloudAuthService);\n    analytics = await module.get(CloudAuthAnalytics);\n    sessionService = await module.get(CloudSessionService);\n    ssoIdpCLoudAuthStrategy = await module.get(SsoIdpCloudAuthStrategy);\n  });\n\n  describe('getAuthStrategy', () => {\n    it('should get Google auth strategy', async () => {\n      expect(service.getAuthStrategy(CloudAuthIdpType.Google)).toEqual(\n        service['googleIdpAuthStrategy'],\n      );\n    });\n    it('should get GitHub auth strategy', async () => {\n      expect(service.getAuthStrategy(CloudAuthIdpType.GitHub)).toEqual(\n        service['githubIdpCloudAuthStrategy'],\n      );\n    });\n    it('should get Sso auth strategy', async () => {\n      expect(service.getAuthStrategy(CloudAuthIdpType.Sso)).toEqual(\n        service['ssoIdpCloudAuthStrategy'],\n      );\n    });\n    it('should throw CloudOauthUnknownAuthorizationRequestException error for unsupported strategy', async () => {\n      try {\n        service.getAuthStrategy('cognito' as CloudAuthIdpType);\n      } catch (e) {\n        expect(e).toEqual(\n          new CloudOauthUnknownAuthorizationRequestException(\n            'Unknown cloud auth strategy',\n          ),\n        );\n      }\n    });\n  });\n  describe('getAuthorizationUrl', () => {\n    let logoutSpy;\n\n    beforeEach(() => {\n      logoutSpy = jest.spyOn(service, 'logout');\n    });\n\n    it('should get Google auth url and add auth request to the pool', async () => {\n      expect(service['authRequests'].size).toEqual(0);\n      expect(\n        await service.getAuthorizationUrl(mockSessionMetadata, {\n          strategy: CloudAuthIdpType.Google,\n        }),\n      ).toEqual(mockCloudAuthGoogleAuthUrl);\n      expect(logoutSpy).toHaveBeenCalled();\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        service['authRequests'].get(mockCloudAuthGoogleRequest.state),\n      ).toEqual(mockCloudAuthGoogleRequest);\n    });\n    it('should get GitHub auth url and add request to the pool but before clear it', async () => {\n      service['authRequests'].set(\n        mockCloudAuthGoogleRequest.state,\n        mockCloudAuthGoogleRequest,\n      );\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        await service.getAuthorizationUrl(mockSessionMetadata, {\n          strategy: CloudAuthIdpType.GitHub,\n        }),\n      ).toEqual(mockCloudAuthGithubAuthUrl);\n      expect(logoutSpy).toHaveBeenCalled();\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        service['authRequests'].get(mockCloudAuthGithubRequest.state),\n      ).toEqual(mockCloudAuthGithubRequest);\n    });\n    it('should throw an error if logout failed', async () => {\n      sessionService.deleteSessionData.mockRejectedValueOnce(\n        new Error('Unable to delete session'),\n      );\n      service['authRequests'].set(\n        mockCloudAuthGoogleRequest.state,\n        mockCloudAuthGoogleRequest,\n      );\n      expect(service['authRequests'].size).toEqual(1);\n      await expect(\n        service.getAuthorizationUrl(mockSessionMetadata, {\n          strategy: CloudAuthIdpType.GitHub,\n        }),\n      ).rejects.toThrow(CloudOauthMisconfigurationException);\n      expect(logoutSpy).toHaveBeenCalled();\n      // previous request should stay\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        service['authRequests'].get(mockCloudAuthGoogleRequest.state),\n      ).toEqual(mockCloudAuthGoogleRequest);\n    });\n    it('should throw CloudOauthSsoUnsupportedEmailException when no email assign to SAML config', async () => {\n      ssoIdpCLoudAuthStrategy.generateAuthRequest.mockRejectedValueOnce(\n        new CloudOauthSsoUnsupportedEmailException(),\n      );\n      service['authRequests'].set(\n        mockCloudAuthGoogleRequest.state,\n        mockCloudAuthGoogleRequest,\n      );\n      expect(service['authRequests'].size).toEqual(1);\n      await expect(\n        service.getAuthorizationUrl(mockSessionMetadata, {\n          strategy: CloudAuthIdpType.Sso,\n        }),\n      ).rejects.toThrow(CloudOauthSsoUnsupportedEmailException);\n      expect(logoutSpy).not.toHaveBeenCalled();\n      // previous request should stay\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        service['authRequests'].get(mockCloudAuthGoogleRequest.state),\n      ).toEqual(mockCloudAuthGoogleRequest);\n    });\n  });\n  describe('exchangeCode', () => {\n    it('should exchange auth code to access token', async () => {\n      mockedAxios.post.mockResolvedValueOnce({ data: mockTokenResponse });\n      const url = new URL(mockCloudAuthGoogleTokenUrl);\n\n      expect(\n        await service['exchangeCode'](\n          mockCloudAuthGoogleRequest,\n          mockCloudAuthCode,\n        ),\n      ).toEqual(mockTokenResponse);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        `${url.origin}${url.pathname}`,\n        url.searchParams,\n        {\n          headers: {\n            accept: 'application/json',\n            'cache-control': 'no-cache',\n            'content-type': 'application/x-www-form-urlencoded',\n          },\n        },\n      );\n    });\n    it('should throw http error in case of an error', async () => {\n      mockedAxios.post.mockRejectedValue(mockAxiosBadRequestError);\n\n      await expect(\n        service['exchangeCode'](mockCloudAuthGoogleRequest, mockCloudAuthCode),\n      ).rejects.toThrow(InternalServerErrorException); // todo: handle this?\n    });\n  });\n  describe('getAuthRequestInfo', () => {\n    it(\"get only few fields from r0equest and don't remove it\", async () => {\n      service['authRequests'] = new Map([\n        [mockCloudAuthGoogleRequest.state, mockCloudAuthGoogleRequest],\n      ]);\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        await service['getAuthRequestInfo'](\n          mockCloudAuthGoogleCallbackQueryObject,\n        ),\n      ).toEqual({\n        action: mockCloudAuthGoogleRequest.action,\n        idpType: mockCloudAuthGoogleRequest.idpType,\n        sessionMetadata: mockCloudAuthGoogleRequest.sessionMetadata,\n      });\n      expect(service['authRequests'].size).toEqual(1);\n    });\n    it('should throw an error if request not found', async () => {\n      service['authRequests'] = new Map([\n        [mockCloudAuthGoogleRequest.state, mockCloudAuthGoogleRequest],\n      ]);\n      expect(service['authRequests'].size).toEqual(1);\n      await expect(\n        service['getAuthRequestInfo'](mockCloudAuthGithubCallbackQueryObject),\n      ).rejects.toThrow(CloudOauthUnknownAuthorizationRequestException);\n    });\n  });\n  describe('callback', () => {\n    let spy;\n\n    beforeEach(() => {\n      service['authRequests'] = new Map([\n        [mockCloudAuthGoogleRequest.state, mockCloudAuthGoogleRequest],\n      ]);\n      spy = jest.spyOn(service as any, 'exchangeCode');\n      spy.mockResolvedValue(mockTokenResponse);\n    });\n\n    it('should exchange code and remove auth request from the pool', async () => {\n      expect(service['authRequests'].size).toEqual(1);\n      expect(\n        await service['callback'](mockCloudAuthGoogleCallbackQueryObject),\n      ).toEqual(mockCloudAuthGoogleRequest.callback);\n      expect(spy).toHaveBeenCalledWith(\n        mockCloudAuthGoogleRequest,\n        mockCloudAuthGoogleCallbackQueryObject.code,\n      );\n      expect(service['authRequests'].size).toEqual(0);\n    });\n    it('should store idToken in session when present in token response', async () => {\n      expect(service['authRequests'].size).toEqual(1);\n      await service['callback'](mockCloudAuthGoogleCallbackQueryObject);\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockCloudAuthGoogleRequest.sessionMetadata.sessionId,\n        {\n          accessToken: mockTokenResponse.access_token,\n          refreshToken: mockTokenResponse.refresh_token,\n          idToken: mockTokenResponse.id_token,\n          idpType: mockCloudAuthGoogleRequest.idpType,\n        },\n      );\n    });\n    it('should handle missing idToken gracefully', async () => {\n      const tokenResponseWithoutIdToken = {\n        access_token: mockCloudAccessTokenNew,\n        refresh_token: mockCloudRefreshTokenNew,\n      };\n      spy.mockResolvedValue(tokenResponseWithoutIdToken);\n      expect(service['authRequests'].size).toEqual(1);\n      await service['callback'](mockCloudAuthGoogleCallbackQueryObject);\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockCloudAuthGoogleRequest.sessionMetadata.sessionId,\n        {\n          accessToken: tokenResponseWithoutIdToken.access_token,\n          refreshToken: tokenResponseWithoutIdToken.refresh_token,\n          idToken: undefined,\n          idpType: mockCloudAuthGoogleRequest.idpType,\n        },\n      );\n    });\n    it('should throw an error if error field in query parameters (CloudOauthMisconfigurationException)', async () => {\n      expect(service['authRequests'].size).toEqual(1);\n      await expect(\n        service['callback']({\n          ...mockCloudAuthGoogleCallbackQueryObject,\n          error: 'bad request',\n          error_description: 'some unknown error message',\n        }),\n      ).rejects.toThrow(CloudOauthUnexpectedErrorException);\n    });\n    it('should throw an error if error field in query parameters (CloudOauthMissedRequiredDataException)', async () => {\n      expect(service['authRequests'].size).toEqual(1);\n      await expect(\n        service['callback']({\n          ...mockCloudAuthGoogleCallbackQueryObject,\n          error: 'access_denied',\n          error_description:\n            'Some required properties are missing: email and lastName',\n        }),\n      ).rejects.toThrow(\n        new CloudOauthMissedRequiredDataException(\n          'Some required properties are missing: email and lastName',\n        ),\n      );\n    });\n    it('should throw an error if request not found', async () => {\n      expect(service['authRequests'].size).toEqual(1);\n      await expect(\n        service['callback'](mockCloudAuthGithubCallbackQueryObject),\n      ).rejects.toThrow(CloudOauthUnknownAuthorizationRequestException);\n    });\n  });\n  describe('revokeRefreshToken', () => {\n    let spy;\n\n    beforeEach(() => {\n      spy = jest.spyOn(service as any, 'exchangeCode');\n      spy.mockResolvedValue(mockTokenResponse);\n    });\n\n    it('should revoke refresh token', async () => {\n      mockedAxios.post.mockResolvedValueOnce({ data: undefined });\n      const url = new URL(mockCloudAuthGoogleRevokeTokenUrl);\n\n      expect(await service['revokeRefreshToken'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        `${url.origin}${url.pathname}`,\n        url.searchParams,\n        {\n          headers: {\n            accept: 'application/json',\n            'cache-control': 'no-cache',\n            'content-type': 'application/x-www-form-urlencoded',\n          },\n        },\n      );\n    });\n\n    it('should not fail and should not make a http call when there is no refreshToken', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      expect(await service['revokeRefreshToken'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(mockedAxios.post).not.toHaveBeenCalled();\n    });\n\n    it('should not fail in case of an any error', async () => {\n      sessionService.getSession.mockRejectedValueOnce(new Error());\n\n      expect(await service['revokeRefreshToken'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(mockedAxios.post).not.toHaveBeenCalled();\n    });\n  });\n  describe('renewTokens', () => {\n    let spy;\n\n    beforeEach(() => {\n      spy = jest.spyOn(service as any, 'exchangeCode');\n      spy.mockResolvedValue(mockTokenResponse);\n    });\n\n    it('should renew tokens', async () => {\n      mockedAxios.post.mockResolvedValueOnce({ data: mockTokenResponseNew });\n      const url = new URL(mockCloudAuthGoogleRenewTokenUrl);\n\n      expect(\n        await service['renewTokens'](\n          mockSessionMetadata,\n          mockCloudApiAuthDto.idpType,\n          mockCloudApiAuthDto.refreshToken,\n        ),\n      ).toEqual(undefined);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        `${url.origin}${url.pathname}`,\n        url.searchParams,\n        {\n          headers: {\n            accept: 'application/json',\n            'cache-control': 'no-cache',\n            'content-type': 'application/x-www-form-urlencoded',\n          },\n        },\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          accessToken: mockCloudAccessTokenNew,\n          refreshToken: mockCloudRefreshTokenNew,\n          idToken: mockTokenResponseNew.id_token,\n          idpType: mockCloudApiAuthDto.idpType,\n          csrf: null,\n          apiSessionId: null,\n        },\n      );\n    });\n\n    it('should store idToken in session when present in token response', async () => {\n      mockedAxios.post.mockResolvedValueOnce({ data: mockTokenResponseNew });\n\n      await service['renewTokens'](\n        mockSessionMetadata,\n        mockCloudApiAuthDto.idpType,\n        mockCloudApiAuthDto.refreshToken,\n      );\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          accessToken: mockCloudAccessTokenNew,\n          refreshToken: mockCloudRefreshTokenNew,\n          idToken: mockTokenResponseNew.id_token,\n          idpType: mockCloudApiAuthDto.idpType,\n          csrf: null,\n          apiSessionId: null,\n        },\n      );\n    });\n\n    it('should handle missing idToken gracefully', async () => {\n      const tokenResponseWithoutIdToken = {\n        access_token: mockCloudAccessTokenNew,\n        refresh_token: mockCloudRefreshTokenNew,\n      };\n      mockedAxios.post.mockResolvedValueOnce({\n        data: tokenResponseWithoutIdToken,\n      });\n\n      await service['renewTokens'](\n        mockSessionMetadata,\n        mockCloudApiAuthDto.idpType,\n        mockCloudApiAuthDto.refreshToken,\n      );\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          accessToken: tokenResponseWithoutIdToken.access_token,\n          refreshToken: tokenResponseWithoutIdToken.refresh_token,\n          idToken: undefined,\n          idpType: mockCloudApiAuthDto.idpType,\n          csrf: null,\n          apiSessionId: null,\n        },\n      );\n    });\n\n    it('should throw CloudApiUnauthorizedException in case of an any error', async () => {\n      sessionService.getSession.mockRejectedValueOnce(new Error());\n\n      await expect(\n        service['renewTokens'](\n          mockSessionMetadata,\n          mockCloudApiAuthDto.idpType,\n          mockCloudApiAuthDto.refreshToken,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('handleCallback', () => {\n    let spy;\n    let callback;\n\n    beforeEach(() => {\n      service['authRequests'] = new Map([\n        [mockCloudAuthGoogleRequest.state, mockCloudAuthGoogleRequest],\n      ]);\n      spy = jest.spyOn(service as any, 'callback');\n      callback = jest.fn();\n      spy.mockResolvedValue(callback);\n    });\n\n    it('should successfully handle auth callback', async () => {\n      expect(\n        await service['handleCallback'](mockCloudAuthGoogleCallbackQueryObject),\n      ).toEqual(mockCloudAuthResponse);\n      expect(callback).toHaveBeenCalledWith(mockCloudAuthResponse);\n      expect(analytics.sendCloudSignInSucceeded).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        CloudSsoFeatureStrategy.DeepLink,\n        mockCloudAuthGoogleRequest.action,\n      );\n    });\n    it('should not fail if async callback failed', async () => {\n      callback.mockRejectedValueOnce(new Error('some error'));\n      expect(\n        await service['handleCallback'](mockCloudAuthGoogleCallbackQueryObject),\n      ).toEqual(mockCloudAuthResponse);\n      expect(callback).toHaveBeenCalledWith(mockCloudAuthResponse);\n    });\n    it('should not fail if sync callback failed', async () => {\n      callback.mockImplementationOnce(() => {\n        throw new Error('some error');\n      });\n      expect(\n        await service['handleCallback'](mockCloudAuthGoogleCallbackQueryObject),\n      ).toEqual(mockCloudAuthResponse);\n      expect(callback).toHaveBeenCalledWith(mockCloudAuthResponse);\n    });\n\n    it('should response with an error and call callback', async () => {\n      const error = new CloudOauthUnknownAuthorizationRequestException();\n      const errorResponse = {\n        status: CloudAuthStatus.Failed,\n        error: error.getResponse(),\n      };\n\n      spy.mockRejectedValueOnce(error);\n      expect(\n        await service['handleCallback'](mockCloudAuthGoogleCallbackQueryObject),\n      ).toEqual(errorResponse);\n      expect(callback).not.toHaveBeenCalled();\n      expect(analytics.sendCloudSignInFailed).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        error,\n        CloudSsoFeatureStrategy.DeepLink,\n        mockCloudAuthGoogleRequest.action,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport axios from 'axios';\nimport { GoogleIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/google-idp.cloud.auth-strategy';\nimport {\n  CloudAuthIdpType,\n  CloudAuthRequest,\n  CloudAuthRequestOptions,\n} from 'src/modules/cloud/auth/models/cloud-auth-request';\nimport { CloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/cloud-auth.strategy';\nimport { SessionMetadata } from 'src/common/models';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { GithubIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/github-idp.cloud.auth-strategy';\nimport { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy';\nimport { wrapHttpError } from 'src/common/utils';\nimport {\n  CloudOauthCanceledException,\n  CloudOauthGithubEmailPermissionException,\n  CloudOauthMisconfigurationException,\n  CloudOauthMissedRequiredDataException,\n  CloudOauthUnexpectedErrorException,\n  CloudOauthUnknownAuthorizationRequestException,\n} from 'src/modules/cloud/auth/exceptions';\nimport {\n  CloudAuthRequestInfo,\n  CloudAuthResponse,\n  CloudAuthStatus,\n} from 'src/modules/cloud/auth/models';\nimport { CloudAuthAnalytics } from 'src/modules/cloud/auth/cloud-auth.analytics';\nimport { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.flag';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { CloudAuthServerEvent } from 'src/modules/cloud/common/constants';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudOauthSsoUnsupportedEmailException } from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception';\n\n@Injectable()\nexport class CloudAuthService {\n  private readonly logger = new Logger('CloudAuthService');\n\n  private authRequests: Map<string, CloudAuthRequest> = new Map();\n\n  private inProgressRequests: Map<string, CloudAuthRequest> = new Map();\n\n  constructor(\n    private readonly sessionService: CloudSessionService,\n    private readonly googleIdpAuthStrategy: GoogleIdpCloudAuthStrategy,\n    private readonly githubIdpCloudAuthStrategy: GithubIdpCloudAuthStrategy,\n    private readonly ssoIdpCloudAuthStrategy: SsoIdpCloudAuthStrategy,\n    private readonly analytics: CloudAuthAnalytics,\n    private readonly eventEmitter: EventEmitter2,\n  ) {}\n\n  static getOAuthHttpRequestHeaders() {\n    return {\n      accept: 'application/json',\n      'cache-control': 'no-cache',\n      'content-type': 'application/x-www-form-urlencoded',\n    };\n  }\n\n  static getAuthorizationServerRedirectError(\n    query: { error_description: string; error: string },\n    authRequest?: CloudAuthRequest,\n  ) {\n    if (query?.error_description?.indexOf('canceled') > -1) {\n      return new CloudOauthCanceledException();\n    }\n\n    if (\n      query?.error_description?.indexOf('propert') > -1 &&\n      query?.error_description?.indexOf('required') > -1 &&\n      query?.error_description?.indexOf('miss') > -1\n    ) {\n      return authRequest?.idpType === CloudAuthIdpType.GitHub &&\n        query?.error_description?.indexOf('email') > -1\n        ? new CloudOauthGithubEmailPermissionException(query.error_description)\n        : new CloudOauthMissedRequiredDataException(query.error_description, {\n            description: query.error_description,\n          });\n    }\n\n    return new CloudOauthUnexpectedErrorException(undefined, {\n      description: query.error_description,\n    });\n  }\n\n  getAuthStrategy(strategy: CloudAuthIdpType): CloudAuthStrategy {\n    switch (strategy) {\n      case CloudAuthIdpType.Google:\n        return this.googleIdpAuthStrategy;\n      case CloudAuthIdpType.GitHub:\n        return this.githubIdpCloudAuthStrategy;\n      case CloudAuthIdpType.Sso:\n        return this.ssoIdpCloudAuthStrategy;\n      default:\n        throw new CloudOauthUnknownAuthorizationRequestException(\n          'Unknown cloud auth strategy',\n        );\n    }\n  }\n\n  /**\n   * Returns authorization url to open in the native browser to initialize oauth flow\n   * @param sessionMetadata\n   * @param options\n   */\n  async getAuthorizationUrl(\n    sessionMetadata: SessionMetadata,\n    options: CloudAuthRequestOptions,\n  ): Promise<string> {\n    try {\n      const authRequest: any = await this.getAuthStrategy(\n        options?.strategy,\n      ).generateAuthRequest(sessionMetadata, options);\n      authRequest.callback = options?.callback;\n      authRequest.action = options?.action;\n\n      // based on requirements we must support only single auth request at the moment\n      // and logout user before\n      await this.logout(sessionMetadata);\n      this.authRequests.clear();\n      this.authRequests.set(authRequest.state, authRequest);\n\n      return CloudAuthStrategy.generateAuthUrl(authRequest).toString();\n    } catch (e) {\n      this.logger.error('Unable to generate authorization url', e);\n\n      if (e instanceof CloudOauthSsoUnsupportedEmailException) {\n        throw e;\n      }\n\n      throw new CloudOauthMisconfigurationException(undefined, { cause: e });\n    }\n  }\n\n  /**\n   * Exchange authorization code for a tokens\n   * @param authRequest\n   * @param code\n   */\n  private async exchangeCode(\n    authRequest: CloudAuthRequest,\n    code: string,\n  ): Promise<any> {\n    try {\n      const tokenUrl = CloudAuthStrategy.generateExchangeCodeUrl(\n        authRequest,\n        code,\n      );\n\n      const { data } = await axios.post(\n        tokenUrl.toString().split('?')[0],\n        tokenUrl.searchParams,\n        {\n          headers: CloudAuthService.getOAuthHttpRequestHeaders(),\n        },\n      );\n\n      return data;\n    } catch (e) {\n      this.logger.error('Unable to exchange code', e);\n\n      // todo: handle this?\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get some useful not sensitive information about auth request for analytics purpose\n   * Will not remove auth request from the pool\n   * @param query\n   * @private\n   */\n  private async getAuthRequestInfo(query): Promise<CloudAuthRequestInfo> {\n    if (!this.authRequests.has(query?.state)) {\n      this.logger.log(\n        `${query?.state ? 'Auth Request matching query state not found' : 'Query state field is empty'}`,\n      );\n      throw new CloudOauthUnknownAuthorizationRequestException();\n    }\n\n    const authRequest = this.authRequests.get(query.state);\n\n    return {\n      idpType: authRequest.idpType,\n      action: authRequest.action,\n      sessionMetadata: authRequest.sessionMetadata,\n    };\n  }\n\n  /**\n   * Process oauth callback\n   * Exchanges code and modify user session\n   * Generates proper errors\n   * @param query\n   */\n  private async callback(query): Promise<Function | void> {\n    if (!this.authRequests.has(query?.state)) {\n      this.logger.log(\n        `${query?.state ? 'Auth Request matching query state not found' : 'Query state field is empty'}`,\n      );\n      throw new CloudOauthUnknownAuthorizationRequestException();\n    }\n\n    const authRequest = this.authRequests.get(query.state);\n\n    if (query?.error) {\n      this.logger.error(`Query has error field: query.error: ${query.error},\n        query.error_description: ${query.error_description}`);\n      throw CloudAuthService.getAuthorizationServerRedirectError(\n        query,\n        authRequest,\n      );\n    }\n\n    // delete authRequest on this step\n    // allow to redirect with authorization code only once\n    this.authRequests.delete(query.state);\n    // Track in progress auth requests to avoid errors when for some reason many we receive many the same calls\n    this.inProgressRequests.set(query.state, authRequest);\n\n    const tokens = await this.exchangeCode(authRequest, query.code);\n\n    await this.sessionService.updateSessionData(\n      authRequest.sessionMetadata.sessionId,\n      {\n        accessToken: tokens.access_token,\n        refreshToken: tokens.refresh_token,\n        idToken: tokens.id_token,\n        idpType: authRequest.idpType,\n      },\n    );\n\n    return authRequest.callback;\n  }\n\n  private async revokeRefreshToken(\n    sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    try {\n      const session = await this.sessionService.getSession(\n        sessionMetadata.sessionId,\n      );\n      if (!session?.refreshToken) {\n        return;\n      }\n\n      const strategy = this.getAuthStrategy(session.idpType);\n\n      const tokenUrl = strategy.generateRevokeTokensUrl(\n        session.refreshToken,\n        'refresh_token',\n      );\n\n      await axios.post(\n        tokenUrl.toString().split('?')[0],\n        tokenUrl.searchParams,\n        {\n          headers: CloudAuthService.getOAuthHttpRequestHeaders(),\n        },\n      );\n    } catch (e) {\n      // ignore error\n      this.logger.error('Unable to revoke tokens', e, sessionMetadata);\n    }\n  }\n\n  /**\n   * Handle OAuth callback from Web or by deep link\n   * @param query\n   * @param from\n   */\n  async handleCallback(\n    query,\n    from = CloudSsoFeatureStrategy.DeepLink,\n  ): Promise<CloudAuthResponse> {\n    this.logger.log(\n      `Handling a callback with a query having ${Object.keys(query || {}).toString()} keys`,\n    );\n    let result: CloudAuthResponse = {\n      status: CloudAuthStatus.Succeed,\n      message: 'Successfully authenticated',\n    };\n\n    let callback;\n    let reqInfo: CloudAuthRequestInfo;\n    try {\n      reqInfo = await this.getAuthRequestInfo(query);\n      callback = await this.callback(query);\n      this.analytics.sendCloudSignInSucceeded(\n        reqInfo.sessionMetadata,\n        from,\n        reqInfo?.action,\n      );\n    } catch (e) {\n      this.logger.error(\n        `Error on ${from} cloud oauth callback: ${e.message}`,\n        e,\n      );\n\n      this.analytics.sendCloudSignInFailed(\n        reqInfo?.sessionMetadata,\n        e,\n        from,\n        reqInfo?.action,\n      );\n\n      result = {\n        status: CloudAuthStatus.Failed,\n        error: wrapHttpError(e).getResponse(),\n      };\n    }\n\n    try {\n      if (!callback) {\n        this.logger.log('Callback is undefined');\n      }\n      callback?.(result)?.catch((e: Error) =>\n        this.logger.error('Async callback failed', e),\n      );\n    } catch (e) {\n      this.logger.error('Callback failed', e);\n    }\n\n    return result;\n  }\n\n  async renewTokens(\n    sessionMetadata: SessionMetadata,\n    idpType: CloudAuthIdpType,\n    refreshToken: string,\n  ) {\n    try {\n      const strategy = this.getAuthStrategy(idpType);\n\n      const tokenUrl = strategy.generateRenewTokensUrl(refreshToken);\n\n      const { data } = await axios.post(\n        tokenUrl.toString().split('?')[0],\n        tokenUrl.searchParams,\n        {\n          headers: CloudAuthService.getOAuthHttpRequestHeaders(),\n        },\n      );\n\n      await this.sessionService.updateSessionData(sessionMetadata.sessionId, {\n        accessToken: data.access_token,\n        refreshToken: data.refresh_token,\n        idToken: data.id_token,\n        idpType,\n        csrf: null,\n        apiSessionId: null,\n      });\n    } catch (e) {\n      this.logger.error('Unable to renew tokens', e);\n      throw new CloudApiUnauthorizedException(e.message);\n    }\n  }\n\n  /**\n   * Logout user\n   * Basically cleans current user session\n   * @param sessionMetadata\n   */\n  async logout(sessionMetadata: SessionMetadata): Promise<void> {\n    try {\n      this.logger.debug('Logout cloud user', sessionMetadata);\n\n      await this.revokeRefreshToken(sessionMetadata);\n\n      await this.sessionService.deleteSessionData(sessionMetadata.sessionId);\n\n      this.eventEmitter.emit(CloudAuthServerEvent.Logout, sessionMetadata);\n    } catch (e) {\n      this.logger.error('Unable to logout', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  isRequestInProgress(query) {\n    return !!this.inProgressRequests.has(query?.state);\n  }\n\n  finishInProgressRequest(query) {\n    this.inProgressRequests.delete(query?.state);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.canceled.exception.ts",
    "content": "import { HttpException, HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthCanceledException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_CANCELED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: 499,\n      error: 'CloudOauthCanceled',\n      errorCode: CustomErrorCodes.CloudOauthCanceled,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.github-email-permission.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthGithubEmailPermissionException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_GITHUB_EMAIL_PERMISSION,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudOauthGithubEmailPermission',\n      errorCode: CustomErrorCodes.CloudOauthGithubEmailPermission,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.misconfiguration.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthMisconfigurationException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_MISCONFIGURATION,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudOauthMisconfiguration',\n      errorCode: CustomErrorCodes.CloudOauthMisconfiguration,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.missed-required-data.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthMissedRequiredDataException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_MISSED_REQUIRED_DATA,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudOauthMissedRequiredData',\n      errorCode: CustomErrorCodes.CloudOauthMissedRequiredData,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthSsoUnsupportedEmailException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_SSO_UNSUPPORTED_EMAIL,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudOauthSsoUnsupportedEmail',\n      errorCode: CustomErrorCodes.CloudOauthSsoUnsupportedEmail,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.unexpected-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthUnexpectedErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_UNEXPECTED_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudOauthUnexpectedError',\n      errorCode: CustomErrorCodes.CloudOauthUnexpectedError,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/cloud-oauth.unknown-authorization-request.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudOauthUnknownAuthorizationRequestException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_OAUTH_UNKNOWN_AUTHORIZATION_REQUEST,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudOauthUnknownAuthorizationRequest',\n      errorCode: CustomErrorCodes.CloudOauthUnknownAuthorizationRequest,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/exceptions/index.ts",
    "content": "export * from './cloud-oauth.canceled.exception';\nexport * from './cloud-oauth.misconfiguration.exception';\nexport * from './cloud-oauth.github-email-permission.exception';\nexport * from './cloud-oauth.missed-required-data.exception';\nexport * from './cloud-oauth.unknown-authorization-request.exception';\nexport * from './cloud-oauth.unexpected-error.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/models/cloud-auth-request-info.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { CloudAuthRequest } from 'src/modules/cloud/auth/models/cloud-auth-request';\n\nexport class CloudAuthRequestInfo extends PickType(CloudAuthRequest, [\n  'idpType',\n  'action',\n  'sessionMetadata',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/models/cloud-auth-request.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\n\nexport enum CloudAuthIdpType {\n  Google = 'google',\n  GitHub = 'github',\n  Sso = 'sso',\n}\n\nexport class CloudAuthRequestOptions {\n  strategy: CloudAuthIdpType;\n\n  action?: string;\n\n  data?: Record<string, any>;\n\n  callback?: Function;\n}\n\nexport class CloudAuthRequest extends CloudAuthRequestOptions {\n  idpType: CloudAuthIdpType;\n\n  sessionMetadata: SessionMetadata;\n\n  createdAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/models/cloud-auth-response.ts",
    "content": "export enum CloudAuthStatus {\n  Succeed = 'succeed',\n  Failed = 'failed',\n}\n\nexport class CloudAuthResponse {\n  status: CloudAuthStatus;\n\n  message?: string;\n\n  error?: object | string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/auth/models/index.ts",
    "content": "export * from './cloud-auth-request';\nexport * from './cloud-auth-request-info';\nexport * from './cloud-auth-response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodicovery.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { CloudAutodiscoveryAnalytics } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.analytics';\nimport {\n  mockCloudDatabase,\n  mockCloudDatabaseFixed,\n  mockCloudSubscription,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport {\n  CloudSubscriptionStatus,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport { CloudDatabaseStatus } from 'src/modules/cloud/database/models';\n\ndescribe('CloudAutodiscoveryAnalytics', () => {\n  let service: CloudAutodiscoveryAnalytics;\n  let sendEventMethod;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, CloudAutodiscoveryAnalytics],\n    }).compile();\n\n    service = await module.get(CloudAutodiscoveryAnalytics);\n    sendEventMethod = jest.spyOn<CloudAutodiscoveryAnalytics, any>(\n      service,\n      'sendEvent',\n    );\n    sendFailedEventMethod = jest.spyOn<CloudAutodiscoveryAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendGetRedisCloudSubsSucceedEvent', () => {\n    it('should emit event with active subscriptions', () => {\n      service.sendGetRedisCloudSubsSucceedEvent(\n        mockSessionMetadata,\n        [mockCloudSubscription, mockCloudSubscription],\n        CloudSubscriptionType.Flexible,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoverySucceed,\n        {\n          numberOfActiveSubscriptions: 2,\n          totalNumberOfSubscriptions: 2,\n          type: CloudSubscriptionType.Flexible,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should emit event with active and not active subscription', () => {\n      service.sendGetRedisCloudSubsSucceedEvent(\n        mockSessionMetadata,\n        [\n          {\n            ...mockCloudSubscription,\n            status: CloudSubscriptionStatus.Error,\n          },\n          mockCloudSubscription,\n        ],\n        CloudSubscriptionType.Flexible,\n        CloudAutodiscoveryAuthType.Sso,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoverySucceed,\n        {\n          numberOfActiveSubscriptions: 1,\n          totalNumberOfSubscriptions: 2,\n          type: CloudSubscriptionType.Flexible,\n          authType: CloudAutodiscoveryAuthType.Sso,\n        },\n      );\n    });\n    it('should emit event without active subscriptions', () => {\n      service.sendGetRedisCloudSubsSucceedEvent(\n        mockSessionMetadata,\n        [\n          {\n            ...mockCloudSubscription,\n            status: CloudSubscriptionStatus.Error,\n          },\n          {\n            ...mockCloudSubscription,\n            status: CloudSubscriptionStatus.Error,\n          },\n        ],\n        CloudSubscriptionType.Flexible,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoverySucceed,\n        {\n          numberOfActiveSubscriptions: 0,\n          totalNumberOfSubscriptions: 2,\n          type: CloudSubscriptionType.Flexible,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should emit GetRedisCloudSubsSucceedEvent event for empty list', () => {\n      service.sendGetRedisCloudSubsSucceedEvent(\n        mockSessionMetadata,\n        [],\n        CloudSubscriptionType.Flexible,\n        CloudAutodiscoveryAuthType.Sso,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoverySucceed,\n        {\n          numberOfActiveSubscriptions: 0,\n          totalNumberOfSubscriptions: 0,\n          type: CloudSubscriptionType.Flexible,\n          authType: CloudAutodiscoveryAuthType.Sso,\n        },\n      );\n    });\n    it('should emit GetRedisCloudSubsSucceedEvent event for undefined input value', () => {\n      service.sendGetRedisCloudSubsSucceedEvent(\n        mockSessionMetadata,\n        undefined,\n        CloudSubscriptionType.Fixed,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoverySucceed,\n        {\n          numberOfActiveSubscriptions: 0,\n          totalNumberOfSubscriptions: 0,\n          type: CloudSubscriptionType.Fixed,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should not throw on error when sending GetRedisCloudSubsSucceedEvent event', () => {\n      const input: any = {};\n\n      expect(() =>\n        service.sendGetRedisCloudSubsSucceedEvent(\n          mockSessionMetadata,\n          input,\n          CloudSubscriptionType.Flexible,\n          CloudAutodiscoveryAuthType.Credentials,\n        ),\n      ).not.toThrow();\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendGetRedisCloudSubsFailedEvent', () => {\n    it('should emit GetRedisCloudSubsFailedEvent event', () => {\n      service.sendGetRedisCloudSubsFailedEvent(\n        mockSessionMetadata,\n        httpException,\n        CloudSubscriptionType.Fixed,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoveryFailed,\n        httpException,\n        {\n          type: CloudSubscriptionType.Fixed,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n  });\n\n  describe('sendGetRedisCloudDbsSucceedEvent', () => {\n    it('should emit event with active databases', () => {\n      service.sendGetRedisCloudDbsSucceedEvent(\n        mockSessionMetadata,\n        [mockCloudDatabase, mockCloudDatabaseFixed],\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 2,\n          numberOfFreeDatabases: 1,\n          totalNumberOfDatabases: 2,\n          fixed: 1,\n          flexible: 1,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should emit event with active and not active database', () => {\n      service.sendGetRedisCloudDbsSucceedEvent(\n        mockSessionMetadata,\n        [\n          {\n            ...mockCloudDatabase,\n            status: CloudDatabaseStatus.Pending,\n          },\n          mockCloudDatabase,\n        ],\n        CloudAutodiscoveryAuthType.Sso,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 1,\n          numberOfFreeDatabases: 0,\n          totalNumberOfDatabases: 2,\n          fixed: 0,\n          flexible: 2,\n          authType: CloudAutodiscoveryAuthType.Sso,\n        },\n      );\n    });\n    it('should emit event without active databases', () => {\n      service.sendGetRedisCloudDbsSucceedEvent(\n        mockSessionMetadata,\n        [\n          {\n            ...mockCloudDatabase,\n            status: CloudDatabaseStatus.Pending,\n          },\n        ],\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 0,\n          numberOfFreeDatabases: 0,\n          totalNumberOfDatabases: 1,\n          fixed: 0,\n          flexible: 1,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should emit event for empty list', () => {\n      service.sendGetRedisCloudDbsSucceedEvent(\n        mockSessionMetadata,\n        [],\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 0,\n          numberOfFreeDatabases: 0,\n          totalNumberOfDatabases: 0,\n          fixed: 0,\n          flexible: 0,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should emit event for undefined input value', () => {\n      service.sendGetRedisCloudDbsSucceedEvent(\n        mockSessionMetadata,\n        undefined,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 0,\n          numberOfFreeDatabases: 0,\n          totalNumberOfDatabases: 0,\n          fixed: 0,\n          flexible: 0,\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n    it('should not throw on error', () => {\n      const input: any = {};\n\n      expect(() =>\n        service.sendGetRedisCloudDbsSucceedEvent(\n          mockSessionMetadata,\n          input,\n          CloudAutodiscoveryAuthType.Credentials,\n        ),\n      ).not.toThrow();\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendGetRedisCloudDbsFailedEvent', () => {\n    it('should emit event', () => {\n      service.sendGetRedisCloudDbsFailedEvent(\n        mockSessionMetadata,\n        httpException,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoveryFailed,\n        httpException,\n        {\n          authType: CloudAutodiscoveryAuthType.Credentials,\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.analytics.ts",
    "content": "import { countBy } from 'lodash';\nimport { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport {\n  CloudSubscription,\n  CloudSubscriptionStatus,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport {\n  CloudDatabase,\n  CloudDatabaseStatus,\n} from 'src/modules/cloud/database/models';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CloudAutodiscoveryAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendGetRedisCloudSubsSucceedEvent(\n    sessionMetadata: SessionMetadata,\n    subscriptions: CloudSubscription[] = [],\n    type: CloudSubscriptionType,\n    authType: CloudAutodiscoveryAuthType,\n  ) {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.RedisCloudSubscriptionsDiscoverySucceed,\n        {\n          numberOfActiveSubscriptions: subscriptions.filter(\n            (sub) => sub.status === CloudSubscriptionStatus.Active,\n          ).length,\n          totalNumberOfSubscriptions: subscriptions.length,\n          type,\n          authType,\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendGetRedisCloudSubsFailedEvent(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    type: CloudSubscriptionType,\n    authType: CloudAutodiscoveryAuthType,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RedisCloudSubscriptionsDiscoveryFailed,\n      exception,\n      {\n        type,\n        authType,\n      },\n    );\n  }\n\n  sendGetRedisCloudDbsSucceedEvent(\n    sessionMetadata: SessionMetadata,\n    databases: CloudDatabase[] = [],\n    authType: CloudAutodiscoveryAuthType,\n  ) {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.RedisCloudDatabasesDiscoverySucceed,\n        {\n          numberOfActiveDatabases: databases.filter(\n            (db) => db.status === CloudDatabaseStatus.Active,\n          ).length,\n          numberOfFreeDatabases: databases.filter(\n            (db) => db.cloudDetails?.free === true,\n          ).length,\n          totalNumberOfDatabases: databases.length,\n          fixed: 0,\n          flexible: 0,\n          ...countBy(databases, 'subscriptionType'),\n          authType,\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendGetRedisCloudDbsFailedEvent(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    authType: CloudAutodiscoveryAuthType,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RedisCloudDatabasesDiscoveryFailed,\n      exception,\n      { authType },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { NotFoundException } from '@nestjs/common';\nimport {\n  mockCloudAccountInfo,\n  mockCloudAutodiscoveryAnalytics,\n  mockCloudCapiAuthDto,\n  mockCloudDatabase,\n  mockCloudDatabaseCapiService,\n  mockCloudDatabaseFixed,\n  mockCloudSubscription,\n  mockCloudSubscriptionCapiService,\n  mockCloudUserCapiService,\n  mockDatabaseService,\n  mockImportCloudDatabaseDto,\n  mockImportCloudDatabaseDtoFixed,\n  mockImportCloudDatabaseResponse,\n  mockImportCloudDatabaseResponseFixed,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { CloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.service';\nimport { CloudAutodiscoveryAnalytics } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.analytics';\nimport { ActionStatus } from 'src/common/models';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport { CloudDatabaseStatus } from 'src/modules/cloud/database/models';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudUserCapiService } from 'src/modules/cloud/user/cloud-user.capi.service';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\ndescribe('CloudAutodiscoveryService', () => {\n  let service: CloudAutodiscoveryService;\n  let cloudUserCapiService: MockType<CloudUserCapiService>;\n  let cloudSubscriptionCapiService: MockType<CloudSubscriptionCapiService>;\n  let cloudDatabaseCapiService: MockType<CloudDatabaseCapiService>;\n  let analytics: MockType<CloudAutodiscoveryAnalytics>;\n  let databaseService: MockType<DatabaseService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudAutodiscoveryService,\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: CloudUserCapiService,\n          useFactory: mockCloudUserCapiService,\n        },\n        {\n          provide: CloudSubscriptionCapiService,\n          useFactory: mockCloudSubscriptionCapiService,\n        },\n        {\n          provide: CloudDatabaseCapiService,\n          useFactory: mockCloudDatabaseCapiService,\n        },\n        {\n          provide: CloudAutodiscoveryAnalytics,\n          useFactory: mockCloudAutodiscoveryAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudAutodiscoveryService);\n    analytics = module.get(CloudAutodiscoveryAnalytics);\n    databaseService = module.get(DatabaseService);\n    cloudUserCapiService = module.get(CloudUserCapiService);\n    cloudSubscriptionCapiService = module.get(CloudSubscriptionCapiService);\n    cloudDatabaseCapiService = module.get(CloudDatabaseCapiService);\n  });\n\n  describe('getAccount', () => {\n    it('successfully get cloud account info', async () => {\n      expect(await service.getAccount(mockCloudCapiAuthDto)).toEqual(\n        mockCloudAccountInfo,\n      );\n    });\n    it('should throw CloudApiUnauthorizedException exception', async () => {\n      cloudUserCapiService.getCurrentAccount.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(service.getAccount(mockCloudCapiAuthDto)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n  describe('discoverSubscriptions', () => {\n    it('successfully discover fixed and flexible cloud subscriptions', async () => {\n      expect(\n        await service.discoverSubscriptions(\n          mockSessionMetadata,\n          mockCloudCapiAuthDto,\n          CloudAutodiscoveryAuthType.Credentials,\n        ),\n      ).toEqual([mockCloudSubscription, mockCloudSubscription]);\n      expect(analytics.sendGetRedisCloudSubsSucceedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        [mockCloudSubscription],\n        CloudSubscriptionType.Fixed,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n      expect(analytics.sendGetRedisCloudSubsSucceedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        [mockCloudSubscription],\n        CloudSubscriptionType.Flexible,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      cloudSubscriptionCapiService.getSubscriptions.mockRejectedValue(\n        new CloudApiUnauthorizedException(),\n      );\n\n      await expect(\n        service.discoverSubscriptions(\n          mockSessionMetadata,\n          mockCloudCapiAuthDto,\n          CloudAutodiscoveryAuthType.Credentials,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n\n      expect(analytics.sendGetRedisCloudSubsFailedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        new CloudApiUnauthorizedException(),\n        CloudSubscriptionType.Fixed,\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n    });\n  });\n  describe('discoverDatabases', () => {\n    it('should call getDatabases 2 times', async () => {\n      expect(\n        await service.discoverDatabases(\n          mockSessionMetadata,\n          mockCloudCapiAuthDto,\n          {\n            subscriptions: [\n              {\n                subscriptionId: 86070,\n                subscriptionType: CloudSubscriptionType.Flexible,\n                free: false,\n              },\n              {\n                subscriptionId: 86071,\n                subscriptionType: CloudSubscriptionType.Fixed,\n                free: true,\n              },\n            ],\n          },\n          CloudAutodiscoveryAuthType.Credentials,\n        ),\n      ).toEqual([mockCloudDatabase, mockCloudDatabase]);\n\n      expect(cloudDatabaseCapiService.getDatabases).toHaveBeenCalledTimes(2);\n      expect(cloudDatabaseCapiService.getDatabases).toHaveBeenCalledWith(\n        mockCloudCapiAuthDto,\n        {\n          subscriptionId: 86070,\n          subscriptionType: CloudSubscriptionType.Flexible,\n          free: false,\n        },\n      );\n      expect(cloudDatabaseCapiService.getDatabases).toHaveBeenCalledWith(\n        mockCloudCapiAuthDto,\n        {\n          subscriptionId: 86071,\n          subscriptionType: CloudSubscriptionType.Fixed,\n          free: true,\n        },\n      );\n      expect(analytics.sendGetRedisCloudDbsSucceedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        [mockCloudDatabase, mockCloudDatabase],\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n    });\n    it('should call getDatabases 2 times (same id but different types)', async () => {\n      await service.discoverDatabases(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        {\n          subscriptions: [\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Flexible,\n            },\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Fixed,\n            },\n          ],\n        },\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(cloudDatabaseCapiService.getDatabases).toHaveBeenCalledTimes(2);\n    });\n    it('should call getDatabases 2 times (uniq by id and type)', async () => {\n      await service.discoverDatabases(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        {\n          subscriptions: [\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Flexible,\n            },\n            {\n              subscriptionId: 86071,\n              subscriptionType: CloudSubscriptionType.Fixed,\n            },\n            {\n              subscriptionId: 86071,\n              subscriptionType: CloudSubscriptionType.Fixed,\n            },\n          ],\n        },\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n\n      expect(cloudDatabaseCapiService.getDatabases).toHaveBeenCalledTimes(2);\n    });\n    it('subscription not found', async () => {\n      cloudDatabaseCapiService.getDatabases = jest\n        .fn()\n        .mockRejectedValue(new NotFoundException());\n\n      await expect(\n        service.discoverDatabases(\n          mockSessionMetadata,\n          mockCloudCapiAuthDto,\n          {\n            subscriptions: [\n              {\n                subscriptionId: 86070,\n                subscriptionType: CloudSubscriptionType.Flexible,\n              },\n              {\n                subscriptionId: 86071,\n                subscriptionType: CloudSubscriptionType.Fixed,\n              },\n            ],\n          },\n          CloudAutodiscoveryAuthType.Credentials,\n        ),\n      ).rejects.toThrow(NotFoundException);\n\n      expect(analytics.sendGetRedisCloudDbsFailedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        new NotFoundException(),\n        CloudAutodiscoveryAuthType.Credentials,\n      );\n    });\n  });\n  describe('addRedisCloudDatabases', () => {\n    it('should successfully add 1 fixed and 1 flexible databases', async () => {\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce(\n        mockCloudDatabase,\n      );\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce(\n        mockCloudDatabaseFixed,\n      );\n\n      const result = await service.addRedisCloudDatabases(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        [mockImportCloudDatabaseDto, mockImportCloudDatabaseDtoFixed],\n      );\n\n      expect(result).toEqual([\n        mockImportCloudDatabaseResponse,\n        mockImportCloudDatabaseResponseFixed,\n      ]);\n    });\n    it('should successfully add 1 fixed database and report 1 error without database details (404)', async () => {\n      cloudDatabaseCapiService.getDatabase.mockRejectedValueOnce(\n        new NotFoundException(),\n      );\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce(\n        mockCloudDatabaseFixed,\n      );\n\n      const result = await service.addRedisCloudDatabases(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        [mockImportCloudDatabaseDto, mockImportCloudDatabaseDtoFixed],\n      );\n\n      expect(result).toEqual([\n        {\n          ...mockImportCloudDatabaseResponse,\n          error: {\n            message: 'Not Found',\n            statusCode: 404,\n          },\n          message: 'Not Found',\n          status: 'fail',\n          databaseDetails: undefined, // no database details when database wasn't fetched from cloud\n        },\n        mockImportCloudDatabaseResponseFixed,\n      ]);\n    });\n    it('should successfully add 1 fixed database and report 1 error with database details', async () => {\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce(\n        mockCloudDatabase,\n      );\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce(\n        mockCloudDatabaseFixed,\n      );\n      databaseService.create.mockRejectedValueOnce(\n        new Error('Connectivity issue'),\n      );\n\n      const result = await service.addRedisCloudDatabases(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        [mockImportCloudDatabaseDto, mockImportCloudDatabaseDtoFixed],\n      );\n\n      expect(result).toEqual([\n        {\n          ...mockImportCloudDatabaseResponse,\n          message: 'Connectivity issue',\n          status: ActionStatus.Fail,\n        },\n        mockImportCloudDatabaseResponseFixed,\n      ]);\n    });\n    it('should successfully add 1 fixed database and report 1 error if db is not actives', async () => {\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce({\n        ...mockCloudDatabase,\n        status: CloudDatabaseStatus.Pending,\n      });\n      cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce(\n        mockCloudDatabaseFixed,\n      );\n\n      const result = await service.addRedisCloudDatabases(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        [mockImportCloudDatabaseDto, mockImportCloudDatabaseDtoFixed],\n      );\n\n      expect(result).toEqual([\n        {\n          ...mockImportCloudDatabaseResponse,\n          error: {\n            error: 'Service Unavailable',\n            message: 'The database is inactive.',\n            statusCode: 503,\n          },\n          message: 'The database is inactive.',\n          status: ActionStatus.Fail,\n          databaseDetails: {\n            ...mockImportCloudDatabaseResponse.databaseDetails,\n            status: CloudDatabaseStatus.Pending,\n          },\n        },\n        mockImportCloudDatabaseResponseFixed,\n      ]);\n    });\n    it.each([\n      {\n        description: 'null',\n        publicEndpoint: null,\n      },\n      {\n        description: 'undefined',\n        publicEndpoint: undefined,\n      },\n    ])(\n      'should return error when publicEndpoint is $description',\n      async ({ publicEndpoint }) => {\n        cloudDatabaseCapiService.getDatabase.mockResolvedValueOnce({\n          ...mockCloudDatabase,\n          publicEndpoint,\n          status: CloudDatabaseStatus.Active,\n        });\n\n        const result = await service.addRedisCloudDatabases(\n          mockSessionMetadata,\n          mockCloudCapiAuthDto,\n          [mockImportCloudDatabaseDto],\n        );\n\n        expect(result).toEqual([\n          {\n            ...mockImportCloudDatabaseResponse,\n            status: ActionStatus.Fail,\n            message: ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,\n            error: {\n              message: ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,\n              statusCode: 400,\n              error: 'CloudDatabaseEndpointInvalid',\n              errorCode: CustomErrorCodes.CloudDatabaseEndpointInvalid,\n            },\n            databaseDetails: {\n              ...mockCloudDatabase,\n              publicEndpoint,\n              status: CloudDatabaseStatus.Active,\n            },\n          },\n        ]);\n        expect(databaseService.create).not.toHaveBeenCalled();\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/cloud-autodiscovery.service.ts",
    "content": "import {\n  Injectable,\n  Logger,\n  ServiceUnavailableException,\n} from '@nestjs/common';\nimport { CloudDatabaseEndpointInvalidException } from 'src/modules/cloud/job/exceptions';\nimport { uniqBy } from 'lodash';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  DiscoverCloudDatabasesDto,\n  ImportCloudDatabaseDto,\n  ImportCloudDatabaseResponse,\n} from 'src/modules/cloud/autodiscovery/dto';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { CloudAutodiscoveryAnalytics } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.analytics';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudUserCapiService } from 'src/modules/cloud/user/cloud-user.capi.service';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { CloudAccountInfo } from 'src/modules/cloud/user/models';\nimport {\n  CloudSubscription,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport {\n  CloudDatabase,\n  CloudDatabaseStatus,\n} from 'src/modules/cloud/database/models';\nimport config from 'src/utils/config';\n\nconst cloudConfig = config.get('cloud');\n\n@Injectable()\nexport class CloudAutodiscoveryService {\n  private logger = new Logger('CloudAutodiscoveryService');\n\n  constructor(\n    private readonly databaseService: DatabaseService,\n    private readonly cloudSubscriptionCapiService: CloudSubscriptionCapiService,\n    private readonly cloudUserCapiService: CloudUserCapiService,\n    private readonly cloudDatabaseCapiService: CloudDatabaseCapiService,\n    private readonly analytics: CloudAutodiscoveryAnalytics,\n  ) {}\n\n  /**\n   * Get cloud account short info\n   * @param authDto\n   */\n  async getAccount(authDto: CloudCapiAuthDto): Promise<CloudAccountInfo> {\n    try {\n      return await this.cloudUserCapiService.getCurrentAccount(authDto);\n    } catch (e) {\n      this.logger.error('Error when getting current user account', e);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get subscriptions by type\n   * @param sessionMetadata\n   * @param authDto\n   * @param type\n   * @param authType\n   */\n  private async getSubscriptions(\n    sessionMetadata: SessionMetadata,\n    authDto: CloudCapiAuthDto,\n    type: CloudSubscriptionType,\n    authType: CloudAutodiscoveryAuthType,\n  ): Promise<CloudSubscription[]> {\n    try {\n      const subscriptions =\n        await this.cloudSubscriptionCapiService.getSubscriptions(authDto, type);\n      this.analytics.sendGetRedisCloudSubsSucceedEvent(\n        sessionMetadata,\n        subscriptions,\n        type,\n        authType,\n      );\n      return subscriptions;\n    } catch (e) {\n      this.logger.error(\n        'Failed get redis cloud subscriptions',\n        sessionMetadata,\n        e,\n      );\n      this.analytics.sendGetRedisCloudSubsFailedEvent(\n        sessionMetadata,\n        e,\n        type,\n        authType,\n      );\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Discover all subscriptions\n   * @param sessionMetadata\n   * @param authDto\n   * @param authType\n   */\n  async discoverSubscriptions(\n    sessionMetadata: SessionMetadata,\n    authDto: CloudCapiAuthDto,\n    authType: CloudAutodiscoveryAuthType,\n  ): Promise<CloudSubscription[]> {\n    return [].concat(\n      ...(await Promise.all([\n        this.getSubscriptions(\n          sessionMetadata,\n          authDto,\n          CloudSubscriptionType.Fixed,\n          authType,\n        ),\n        this.getSubscriptions(\n          sessionMetadata,\n          authDto,\n          CloudSubscriptionType.Flexible,\n          authType,\n        ),\n      ])),\n    );\n  }\n\n  /**\n   * Get all databases from specified multiple subscriptions\n   * @param sessionMetadata\n   * @param authDto\n   * @param dto\n   * @param authType\n   */\n  async discoverDatabases(\n    sessionMetadata: SessionMetadata,\n    authDto: CloudCapiAuthDto,\n    dto: DiscoverCloudDatabasesDto,\n    authType: CloudAutodiscoveryAuthType,\n  ): Promise<CloudDatabase[]> {\n    let result = [];\n    try {\n      this.logger.debug(\n        'Discovering cloud databases from subscription(s)',\n        sessionMetadata,\n      );\n\n      const subscriptions = uniqBy(\n        dto.subscriptions,\n        ({ subscriptionId, subscriptionType }) =>\n          [subscriptionId, subscriptionType].join(),\n      );\n\n      await Promise.all(\n        subscriptions.map(async (subscription) => {\n          const databases = await this.cloudDatabaseCapiService.getDatabases(\n            authDto,\n            subscription,\n          );\n          result = result.concat(databases);\n        }),\n      );\n\n      this.analytics.sendGetRedisCloudDbsSucceedEvent(\n        sessionMetadata,\n        result,\n        authType,\n      );\n      return result;\n    } catch (e) {\n      this.logger.error(\n        'Error when discovering cloud databases from subscription(s)',\n        sessionMetadata,\n        e,\n      );\n      this.analytics.sendGetRedisCloudDbsFailedEvent(\n        sessionMetadata,\n        e,\n        authType,\n      );\n\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Add database from cloud\n   * @param sessionMetadata\n   * @param authDto\n   * @param addDatabasesDto\n   */\n  async addRedisCloudDatabases(\n    sessionMetadata: SessionMetadata,\n    authDto: CloudCapiAuthDto,\n    addDatabasesDto: ImportCloudDatabaseDto[],\n  ): Promise<ImportCloudDatabaseResponse[]> {\n    this.logger.debug('Adding Redis Cloud databases.', sessionMetadata);\n\n    return Promise.all(\n      addDatabasesDto.map(\n        async (\n          dto: ImportCloudDatabaseDto,\n        ): Promise<ImportCloudDatabaseResponse> => {\n          let database: CloudDatabase;\n          try {\n            database = await this.cloudDatabaseCapiService.getDatabase(\n              authDto,\n              dto,\n            );\n\n            const { publicEndpoint, name, password, status, tags } = database;\n            if (status !== CloudDatabaseStatus.Active) {\n              const exception = new ServiceUnavailableException(\n                ERROR_MESSAGES.DATABASE_IS_INACTIVE,\n              );\n              return {\n                ...dto,\n                status: ActionStatus.Fail,\n                message: exception.message,\n                error: exception?.getResponse(),\n                databaseDetails: database,\n              };\n            }\n            if (!publicEndpoint) {\n              const exception = new CloudDatabaseEndpointInvalidException();\n              return {\n                ...dto,\n                status: ActionStatus.Fail,\n                message: exception.message,\n                error: exception?.getResponse(),\n                databaseDetails: database,\n              };\n            }\n            const [host, port] = publicEndpoint.split(':');\n\n            await this.databaseService.create(sessionMetadata, {\n              host,\n              port: parseInt(port, 10),\n              name,\n              nameFromProvider: name,\n              password,\n              provider: HostingProvider.REDIS_CLOUD,\n              cloudDetails: database?.cloudDetails,\n              tags,\n              timeout: cloudConfig.cloudDatabaseConnectionTimeout,\n            });\n\n            return {\n              ...dto,\n              status: ActionStatus.Success,\n              message: 'Added',\n              databaseDetails: database,\n            };\n          } catch (error) {\n            this.logger.error(\n              'Adding cloud database failed with an error',\n              sessionMetadata,\n              error,\n            );\n            return {\n              ...dto,\n              status: ActionStatus.Fail,\n              message: error.message,\n              error: error?.response,\n              databaseDetails: database,\n            };\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/cloud.autodiscovery.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  Post,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor';\nimport { ApiHeaders, ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { Response } from 'express';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { BuildType } from 'src/modules/server/models/server';\nimport { CloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.service';\nimport { CloudAuthHeaders } from 'src/modules/cloud/common/decorators/cloud-auth.decorator';\nimport config from 'src/utils/config';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { CloudAccountInfo } from 'src/modules/cloud/user/models';\nimport { CloudSubscription } from 'src/modules/cloud/subscription/models';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport { CloudDatabase } from 'src/modules/cloud/database/models';\nimport {\n  DiscoverCloudDatabasesDto,\n  ImportCloudDatabaseResponse,\n  ImportCloudDatabasesDto,\n} from 'src/modules/cloud/autodiscovery/dto';\nimport { RequestSessionMetadata } from 'src/common/decorators';\n\nconst cloudConf = config.get('cloud');\n\n@ApiTags('Cloud Autodiscovery')\n@ApiHeaders([\n  {\n    name: 'x-cloud-api-key',\n  },\n  {\n    name: 'x-cloud-api-secret',\n  },\n])\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(new TimeoutInterceptor(undefined, cloudConf.discoveryTimeout))\n@Controller('cloud/autodiscovery')\nexport class CloudAutodiscoveryController {\n  constructor(private service: CloudAutodiscoveryService) {}\n\n  @Get('account')\n  @ApiEndpoint({\n    description: 'Get current account',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Account Details.',\n        type: CloudAccountInfo,\n      },\n    ],\n  })\n  async getAccount(\n    @CloudAuthHeaders() authDto: CloudCapiAuthDto,\n  ): Promise<CloudAccountInfo> {\n    return await this.service.getAccount(authDto);\n  }\n\n  @Get('subscriptions')\n  @ApiEndpoint({\n    description: 'Get information about current account’s subscriptions.',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Redis cloud subscription list.',\n        type: CloudSubscription,\n        isArray: true,\n      },\n    ],\n  })\n  async discoverSubscriptions(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @CloudAuthHeaders() authDto: CloudCapiAuthDto,\n  ): Promise<CloudSubscription[]> {\n    return await this.service.discoverSubscriptions(\n      sessionMetadata,\n      authDto,\n      CloudAutodiscoveryAuthType.Credentials,\n    );\n  }\n\n  @Post('get-databases')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiEndpoint({\n    description: 'Get databases belonging to subscriptions',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Databases list.',\n        type: CloudDatabase,\n        isArray: true,\n      },\n    ],\n  })\n  async discoverDatabases(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @CloudAuthHeaders() authDto: CloudCapiAuthDto,\n    @Body() dto: DiscoverCloudDatabasesDto,\n  ): Promise<CloudDatabase[]> {\n    return await this.service.discoverDatabases(\n      sessionMetadata,\n      authDto,\n      dto,\n      CloudAutodiscoveryAuthType.Credentials,\n    );\n  }\n\n  @Post('databases')\n  @ApiEndpoint({\n    description: 'Add databases from Redis Enterprise Cloud Pro account.',\n    statusCode: 201,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 201,\n        description: 'Added databases list.',\n        type: ImportCloudDatabaseResponse,\n        isArray: true,\n      },\n    ],\n  })\n  async addDiscoveredDatabases(\n    @RequestSessionMetadata() sessionMetadata,\n    @CloudAuthHeaders() authDto: CloudCapiAuthDto,\n    @Body() dto: ImportCloudDatabasesDto,\n    @Res() res: Response,\n  ): Promise<Response> {\n    const result = await this.service.addRedisCloudDatabases(\n      sessionMetadata,\n      authDto,\n      dto.databases,\n    );\n    const hasSuccessResult = result.some(\n      (addResponse: ImportCloudDatabaseResponse) =>\n        addResponse.status === ActionStatus.Success,\n    );\n    if (!hasSuccessResult) {\n      return res.status(200).json(result);\n    }\n    return res.json(result);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/cloud.autodiscovery.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudAutodiscoveryController } from 'src/modules/cloud/autodiscovery/cloud.autodiscovery.controller';\nimport { CloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.service';\nimport { CloudAutodiscoveryAnalytics } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.analytics';\nimport { CloudDatabaseModule } from 'src/modules/cloud/database/cloud-database.module';\nimport { CloudSubscriptionModule } from 'src/modules/cloud/subscription/cloud-subscription.module';\nimport { CloudUserModule } from 'src/modules/cloud/user/cloud-user.module';\nimport { MeCloudAutodiscoveryController } from 'src/modules/cloud/autodiscovery/me.cloud.autodiscovery.controller';\nimport { MeCloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/me.cloud-autodiscovery.service';\nimport { CloudCapiKeyModule } from 'src/modules/cloud/capi-key/cloud-capi-key.module';\n\n@Module({\n  imports: [\n    CloudDatabaseModule,\n    CloudSubscriptionModule,\n    CloudUserModule,\n    CloudCapiKeyModule,\n  ],\n  controllers: [CloudAutodiscoveryController, MeCloudAutodiscoveryController],\n  providers: [\n    CloudAutodiscoveryService,\n    MeCloudAutodiscoveryService,\n    CloudAutodiscoveryAnalytics,\n  ],\n})\nexport class CloudAutodiscoveryModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/dto/discover-cloud-databases.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { GetCloudSubscriptionDatabasesDto } from 'src/modules/cloud/database/dto/get-cloud-subscription-databases.dto';\n\nexport class DiscoverCloudDatabasesDto {\n  @ApiProperty({\n    description: 'Subscriptions where to discover databases',\n    type: GetCloudSubscriptionDatabasesDto,\n    isArray: true,\n  })\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested()\n  @Type(() => GetCloudSubscriptionDatabasesDto)\n  subscriptions: GetCloudSubscriptionDatabasesDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/dto/import-cloud-database.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsBoolean,\n  IsDefined,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsOptional,\n} from 'class-validator';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\n\nexport class ImportCloudDatabaseDto {\n  @ApiProperty({\n    description: 'Subscription id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  subscriptionId: number;\n\n  @IsEnum(CloudSubscriptionType, {\n    message: `subscriptionType must be a valid enum value. Valid values: ${Object.values(\n      CloudSubscriptionType,\n    )}.`,\n  })\n  @IsNotEmpty()\n  subscriptionType: CloudSubscriptionType;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  databaseId: number;\n\n  @ApiPropertyOptional({\n    type: Boolean,\n  })\n  @IsOptional()\n  @IsBoolean()\n  free?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/dto/import-cloud-database.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { ActionStatus } from 'src/common/models';\nimport { CloudDatabase } from 'src/modules/cloud/database/models';\n\nexport class ImportCloudDatabaseResponse {\n  @ApiProperty({\n    description: 'Subscription id',\n    type: Number,\n  })\n  subscriptionId: number;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: Number,\n  })\n  databaseId: number;\n\n  @ApiProperty({\n    description: 'Add Redis Cloud database status',\n    default: ActionStatus.Success,\n    enum: ActionStatus,\n  })\n  status: ActionStatus;\n\n  @ApiProperty({\n    description: 'Message',\n    type: String,\n  })\n  message: string;\n\n  @ApiPropertyOptional({\n    description: 'The database details.',\n    type: CloudDatabase,\n  })\n  databaseDetails?: CloudDatabase;\n\n  @ApiPropertyOptional({\n    description: 'Error',\n  })\n  error?: string | object;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/dto/import-cloud-databases.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { ImportCloudDatabaseDto } from 'src/modules/cloud/autodiscovery/dto/import-cloud-database.dto';\n\nexport class ImportCloudDatabasesDto {\n  @ApiProperty({\n    description: 'Cloud databases list.',\n    type: ImportCloudDatabaseDto,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested()\n  @Type(() => ImportCloudDatabaseDto)\n  databases: ImportCloudDatabaseDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/dto/index.ts",
    "content": "export * from './discover-cloud-databases.dto';\nexport * from './import-cloud-database.dto';\nexport * from './import-cloud-database.response';\nexport * from './import-cloud-databases.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/me.cloud-autodiscovery.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCloudAccountInfo,\n  mockCloudAutodiscoveryService,\n  mockCloudCapiAuthDto,\n  mockCloudCapiKeyService,\n  mockCloudDatabase,\n  mockCloudDatabaseFixed,\n  mockCloudSessionService,\n  mockCloudSubscription,\n  mockCloudSubscriptionFixed,\n  mockImportCloudDatabaseDto,\n  mockImportCloudDatabaseDtoFixed,\n  mockImportCloudDatabaseResponse,\n  mockImportCloudDatabaseResponseFixed,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { CloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.service';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport {\n  CloudApiBadRequestException,\n  CloudApiForbiddenException,\n  CloudApiInternalServerErrorException,\n  CloudApiUnauthorizedException,\n} from 'src/modules/cloud/common/exceptions';\nimport { MeCloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/me.cloud-autodiscovery.service';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { CloudCapiKeyApiProvider } from 'src/modules/cloud/capi-key/cloud-capi-key.api.provider';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\n\ndescribe('MeCloudAutodiscoveryService', () => {\n  let service: MeCloudAutodiscoveryService;\n  let cloudAutodiscoveryService: MockType<CloudAutodiscoveryService>;\n  let cloudCapiKeyService: MockType<CloudCapiKeyService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        MeCloudAutodiscoveryService,\n        CloudCapiKeyApiProvider,\n        {\n          provide: CloudAutodiscoveryService,\n          useFactory: mockCloudAutodiscoveryService,\n        },\n        {\n          provide: CloudCapiKeyService,\n          useFactory: mockCloudCapiKeyService,\n        },\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(MeCloudAutodiscoveryService);\n    cloudAutodiscoveryService = module.get(CloudAutodiscoveryService);\n    cloudCapiKeyService = module.get(CloudCapiKeyService);\n  });\n\n  describe('getAccount', () => {\n    it('successfully get cloud account info', async () => {\n      expect(await service.getAccount(mockSessionMetadata)).toEqual(\n        mockCloudAccountInfo,\n      );\n    });\n    it('should throw CloudApiUnauthorizedException exception if failed twice', async () => {\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(service.getAccount(mockSessionMetadata)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n    it('should throw CloudApiForbiddenException exception', async () => {\n      cloudAutodiscoveryService.getAccount.mockRejectedValueOnce(\n        new CloudApiForbiddenException(),\n      );\n      await expect(service.getAccount(mockSessionMetadata)).rejects.toThrow(\n        CloudApiForbiddenException,\n      );\n    });\n  });\n  describe('discoverSubscriptions', () => {\n    it('successfully discover fixed and flexible cloud subscriptions', async () => {\n      expect(await service.discoverSubscriptions(mockSessionMetadata)).toEqual([\n        mockCloudSubscription,\n        mockCloudSubscriptionFixed,\n      ]);\n      expect(\n        cloudAutodiscoveryService.discoverSubscriptions,\n      ).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        CloudAutodiscoveryAuthType.Sso,\n      );\n    });\n    it('should throw CloudApiUnauthorizedException exception if failed twice', async () => {\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.discoverSubscriptions(mockSessionMetadata),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n    it('should throw CloudApiForbiddenException exception', async () => {\n      cloudAutodiscoveryService.discoverSubscriptions.mockRejectedValueOnce(\n        new CloudApiForbiddenException(),\n      );\n      await expect(\n        service.discoverSubscriptions(mockSessionMetadata),\n      ).rejects.toThrow(CloudApiForbiddenException);\n    });\n  });\n  describe('discoverDatabases', () => {\n    it('should call getDatabases 2 times', async () => {\n      expect(\n        await service.discoverDatabases(mockSessionMetadata, {\n          subscriptions: [\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Flexible,\n              free: false,\n            },\n            {\n              subscriptionId: 86071,\n              subscriptionType: CloudSubscriptionType.Fixed,\n              free: true,\n            },\n          ],\n        }),\n      ).toEqual([mockCloudDatabase, mockCloudDatabaseFixed]);\n\n      expect(cloudAutodiscoveryService.discoverDatabases).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCloudCapiAuthDto,\n        {\n          subscriptions: [\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Flexible,\n              free: false,\n            },\n            {\n              subscriptionId: 86071,\n              subscriptionType: CloudSubscriptionType.Fixed,\n              free: true,\n            },\n          ],\n        },\n        CloudAutodiscoveryAuthType.Sso,\n      );\n    });\n    it('should throw CloudApiUnauthorizedException exception if failed twice', async () => {\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.discoverDatabases(mockSessionMetadata, {\n          subscriptions: [\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Flexible,\n              free: false,\n            },\n          ],\n        }),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n    it('should throw CloudApiBadRequestException exception', async () => {\n      cloudAutodiscoveryService.discoverDatabases.mockRejectedValueOnce(\n        new CloudApiBadRequestException(),\n      );\n      await expect(\n        service.discoverDatabases(mockSessionMetadata, {\n          subscriptions: [\n            {\n              subscriptionId: 86070,\n              subscriptionType: CloudSubscriptionType.Flexible,\n              free: false,\n            },\n          ],\n        }),\n      ).rejects.toThrow(CloudApiBadRequestException);\n    });\n  });\n  describe('addRedisCloudDatabases', () => {\n    it('should successfully add 1 fixed and 1 flexible databases', async () => {\n      const result = await service.addRedisCloudDatabases(mockSessionMetadata, [\n        mockImportCloudDatabaseDto,\n        mockImportCloudDatabaseDtoFixed,\n      ]);\n\n      expect(result).toEqual([\n        mockImportCloudDatabaseResponse,\n        mockImportCloudDatabaseResponseFixed,\n      ]);\n\n      expect(\n        cloudAutodiscoveryService.addRedisCloudDatabases,\n      ).toHaveBeenCalledWith(mockSessionMetadata, mockCloudCapiAuthDto, [\n        mockImportCloudDatabaseDto,\n        mockImportCloudDatabaseDtoFixed,\n      ]);\n    });\n    it('should throw CloudApiUnauthorizedException exception if failed twice', async () => {\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      cloudCapiKeyService.getCapiCredentials.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.addRedisCloudDatabases(mockSessionMetadata, [\n          mockImportCloudDatabaseDto,\n          mockImportCloudDatabaseDtoFixed,\n        ]),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n    it('should throw CloudApiInternalServerErrorException exception', async () => {\n      cloudAutodiscoveryService.addRedisCloudDatabases.mockRejectedValueOnce(\n        new CloudApiInternalServerErrorException(),\n      );\n      await expect(\n        service.addRedisCloudDatabases(mockSessionMetadata, [\n          mockImportCloudDatabaseDto,\n          mockImportCloudDatabaseDtoFixed,\n        ]),\n      ).rejects.toThrow(CloudApiInternalServerErrorException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/me.cloud-autodiscovery.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport {\n  DiscoverCloudDatabasesDto,\n  ImportCloudDatabaseDto,\n  ImportCloudDatabaseResponse,\n} from 'src/modules/cloud/autodiscovery/dto';\nimport { CloudAutodiscoveryAuthType } from 'src/modules/cloud/autodiscovery/models';\nimport { SessionMetadata } from 'src/common/models';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudAccountInfo } from 'src/modules/cloud/user/models';\nimport { CloudSubscription } from 'src/modules/cloud/subscription/models';\nimport { CloudDatabase } from 'src/modules/cloud/database/models';\nimport { CloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/cloud-autodiscovery.service';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { CloudCapiKeyApiProvider } from 'src/modules/cloud/capi-key/cloud-capi-key.api.provider';\n\n@Injectable()\nexport class MeCloudAutodiscoveryService {\n  constructor(\n    private readonly cloudAutodiscoveryService: CloudAutodiscoveryService,\n    private readonly cloudCapiKeyService: CloudCapiKeyService,\n    private readonly api: CloudCapiKeyApiProvider,\n  ) {}\n\n  private async getCapiCredentials(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudCapiAuthDto> {\n    return this.cloudCapiKeyService.getCapiCredentials(sessionMetadata, utm);\n  }\n\n  /**\n   * Get cloud account short info\n   * @param sessionMetadata\n   * @param utm\n   */\n  async getAccount(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudAccountInfo> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        return await this.cloudAutodiscoveryService.getAccount(\n          await this.getCapiCredentials(sessionMetadata, utm),\n        );\n      } catch (e) {\n        throw wrapHttpError(\n          await this.cloudCapiKeyService.handleCapiKeyUnauthorizedError(\n            e,\n            sessionMetadata,\n          ),\n        );\n      }\n    });\n  }\n\n  /**\n   * Discover all subscriptions\n   * @param sessionMetadata\n   * @param utm\n   */\n  async discoverSubscriptions(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudSubscription[]> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        return await this.cloudAutodiscoveryService.discoverSubscriptions(\n          sessionMetadata,\n          await this.getCapiCredentials(sessionMetadata, utm),\n          CloudAutodiscoveryAuthType.Sso,\n        );\n      } catch (e) {\n        throw wrapHttpError(\n          await this.cloudCapiKeyService.handleCapiKeyUnauthorizedError(\n            e,\n            sessionMetadata,\n          ),\n        );\n      }\n    });\n  }\n\n  /**\n   * Get all databases from specified multiple subscriptions\n   * @param sessionMetadata\n   * @param dto\n   * @param utm\n   */\n  async discoverDatabases(\n    sessionMetadata: SessionMetadata,\n    dto: DiscoverCloudDatabasesDto,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudDatabase[]> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        return await this.cloudAutodiscoveryService.discoverDatabases(\n          sessionMetadata,\n          await this.getCapiCredentials(sessionMetadata, utm),\n          dto,\n          CloudAutodiscoveryAuthType.Sso,\n        );\n      } catch (e) {\n        throw wrapHttpError(\n          await this.cloudCapiKeyService.handleCapiKeyUnauthorizedError(\n            e,\n            sessionMetadata,\n          ),\n        );\n      }\n    });\n  }\n\n  /**\n   * Add database from cloud\n   * @param sessionMetadata\n   * @param addDatabasesDto\n   * @param utm\n   */\n  async addRedisCloudDatabases(\n    sessionMetadata: SessionMetadata,\n    addDatabasesDto: ImportCloudDatabaseDto[],\n    utm?: CloudRequestUtm,\n  ): Promise<ImportCloudDatabaseResponse[]> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        return await this.cloudAutodiscoveryService.addRedisCloudDatabases(\n          sessionMetadata,\n          await this.getCapiCredentials(sessionMetadata, utm),\n          addDatabasesDto,\n        );\n      } catch (e) {\n        throw wrapHttpError(\n          await this.cloudCapiKeyService.handleCapiKeyUnauthorizedError(\n            e,\n            sessionMetadata,\n          ),\n        );\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/me.cloud.autodiscovery.controller.ts",
    "content": "import {\n  Body,\n  Query,\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  Post,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { Response } from 'express';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { BuildType } from 'src/modules/server/models/server';\nimport config from 'src/utils/config';\nimport { CloudAccountInfo } from 'src/modules/cloud/user/models';\nimport { CloudSubscription } from 'src/modules/cloud/subscription/models';\nimport { CloudDatabase } from 'src/modules/cloud/database/models';\nimport {\n  DiscoverCloudDatabasesDto,\n  ImportCloudDatabaseResponse,\n  ImportCloudDatabasesDto,\n} from 'src/modules/cloud/autodiscovery/dto';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { MeCloudAutodiscoveryService } from 'src/modules/cloud/autodiscovery/me.cloud-autodiscovery.service';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\n\nconst cloudConf = config.get('cloud');\n\n@ApiTags('Cloud Autodiscovery')\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(new TimeoutInterceptor(undefined, cloudConf.discoveryTimeout))\n@Controller('cloud/me/autodiscovery')\nexport class MeCloudAutodiscoveryController {\n  constructor(private service: MeCloudAutodiscoveryService) {}\n\n  @Get('account')\n  @ApiEndpoint({\n    description: 'Get current account',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Account Details.',\n        type: CloudAccountInfo,\n      },\n    ],\n  })\n  async getAccount(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<CloudAccountInfo> {\n    return await this.service.getAccount(sessionMetadata, utm);\n  }\n\n  @Get('subscriptions')\n  @ApiEndpoint({\n    description: 'Get information about current account’s subscriptions.',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Redis cloud subscription list.',\n        type: CloudSubscription,\n        isArray: true,\n      },\n    ],\n  })\n  async discoverSubscriptions(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<CloudSubscription[]> {\n    return await this.service.discoverSubscriptions(sessionMetadata, utm);\n  }\n\n  @Post('get-databases')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiEndpoint({\n    description: 'Get databases belonging to subscriptions',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Databases list.',\n        type: CloudDatabase,\n        isArray: true,\n      },\n    ],\n  })\n  async discoverDatabases(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: DiscoverCloudDatabasesDto,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<CloudDatabase[]> {\n    return await this.service.discoverDatabases(sessionMetadata, dto, utm);\n  }\n\n  @Post('databases')\n  @ApiEndpoint({\n    description: 'Add databases from Redis Enterprise Cloud Pro account.',\n    statusCode: 201,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 201,\n        description: 'Added databases list.',\n        type: ImportCloudDatabaseResponse,\n        isArray: true,\n      },\n    ],\n  })\n  async addDiscoveredDatabases(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: ImportCloudDatabasesDto,\n    @Res() res: Response,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<Response> {\n    const result = await this.service.addRedisCloudDatabases(\n      sessionMetadata,\n      dto.databases,\n      utm,\n    );\n    const hasSuccessResult = result.some(\n      (addResponse: ImportCloudDatabaseResponse) =>\n        addResponse.status === ActionStatus.Success,\n    );\n    if (!hasSuccessResult) {\n      return res.status(200).json(result);\n    }\n    return res.json(result);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/models/cloud-autodiscovery-auth-type.ts",
    "content": "export enum CloudAutodiscoveryAuthType {\n  Credentials = 'credentials',\n  Sso = 'sso',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/autodiscovery/models/index.ts",
    "content": "export * from './cloud-autodiscovery-auth-type';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { CloudCapiKeyAnalytics } from 'src/modules/cloud/capi-key/cloud-capi-key.analytics';\nimport { mockSessionMetadata } from 'src/__mocks__';\n\ndescribe('CloudCapiKeyAnalytics', () => {\n  let service: CloudCapiKeyAnalytics;\n  let sendEventSpy;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, CloudCapiKeyAnalytics],\n    }).compile();\n\n    service = await module.get(CloudCapiKeyAnalytics);\n    sendEventSpy = jest.spyOn<CloudCapiKeyAnalytics, any>(service, 'sendEvent');\n    sendFailedEventMethod = jest.spyOn<CloudCapiKeyAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendCloudAccountKeyGenerated', () => {\n    it('should emit succeed event with manifest \"yes\"', () => {\n      service.sendCloudAccountKeyGenerated(mockSessionMetadata);\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.CloudAccountKeyGenerated,\n      );\n    });\n  });\n\n  describe('sendCloudAccountKeyGenerationFailed', () => {\n    it('should emit 1 event with \"Error\" cause', () => {\n      service.sendCloudAccountKeyGenerationFailed(\n        mockSessionMetadata,\n        httpException,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.CloudAccountKeyGenerationFailed,\n        httpException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CloudCapiKeyAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendCloudAccountKeyGenerated(sessionMetadata: SessionMetadata) {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.CloudAccountKeyGenerated);\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendCloudAccountKeyGenerationFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.CloudAccountKeyGenerationFailed,\n      exception,\n    );\n  }\n\n  sendCloudAccountSecretGenerated(sessionMetadata: SessionMetadata) {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.CloudAccountSecretGenerated,\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendCloudAccountSecretGenerationFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.CloudAccountSecretGenerationFailed,\n      exception,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.api.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudApiCapiAccessKey,\n  mockCloudApiCapiKey,\n  mockCloudApiHeaders,\n  mockCloudSession,\n  mockCloudSessionService,\n  mockCloudUser,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudCapiKeyApiProvider } from 'src/modules/cloud/capi-key/cloud-capi-key.api.provider';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudCapiKeyApiProvider', () => {\n  let service: CloudCapiKeyApiProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudCapiKeyApiProvider,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudCapiKeyApiProvider);\n  });\n\n  describe('enableCapi', () => {\n    it('successfully get capi access key', async () => {\n      const response = {\n        status: 200,\n        data: { cloudApiAccessKey: mockCloudApiCapiAccessKey },\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(await service.enableCapi(mockCloudSession)).toEqual(\n        mockCloudApiCapiAccessKey.accessKey,\n      );\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        '/accounts/cloud-api/cloudApiAccessKey',\n        {},\n        mockCloudApiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.enableCapi(mockCloudSession)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n\n  describe('createCapiKey', () => {\n    it('successfully create cpi key (secret)', async () => {\n      const response = {\n        status: 200,\n        data: { cloudApiKey: mockCloudApiCapiKey },\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.createCapiKey(\n          mockCloudSession,\n          mockCloudUser.id,\n          mockCloudApiCapiKey.name,\n        ),\n      ).toEqual(mockCloudApiCapiKey);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        '/accounts/cloud-api/cloudApiKeys',\n        {\n          cloudApiKey: {\n            name: mockCloudApiCapiKey.name,\n            user_account: mockCloudUser.id,\n            ip_whitelist: [],\n          },\n        },\n        mockCloudApiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.createCapiKey(\n          mockCloudSession,\n          mockCloudUser.id,\n          mockCloudApiCapiKey.name,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.api.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { ICloudApiCapiKey } from 'src/modules/cloud/capi-key/model';\nimport { wrapCloudApiError } from 'src/modules/cloud/common/exceptions';\nimport { ICloudApiCredentials } from 'src/modules/cloud/common/models';\nimport { CloudApiProvider } from 'src/modules/cloud/common/providers/cloud.api.provider';\n\n@Injectable()\nexport class CloudCapiKeyApiProvider extends CloudApiProvider {\n  /**\n   * Get list of CApi keys\n   * @param credentials\n   */\n  async enableCapi(credentials: ICloudApiCredentials): Promise<string> {\n    try {\n      const { data } = await this.api.post(\n        '/accounts/cloud-api/cloudApiAccessKey',\n        {},\n        CloudApiProvider.getHeaders(credentials),\n      );\n\n      return data?.cloudApiAccessKey?.accessKey;\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n\n  /**\n   * Create new CApi key\n   * @param credentials\n   * @param userId\n   * @param name\n   */\n  async createCapiKey(\n    credentials: ICloudApiCredentials,\n    userId: number,\n    name: string,\n  ): Promise<ICloudApiCapiKey> {\n    try {\n      const { data } = await this.api.post(\n        '/accounts/cloud-api/cloudApiKeys',\n        {\n          cloudApiKey: {\n            name,\n            user_account: userId,\n            ip_whitelist: [],\n          },\n        },\n        CloudApiProvider.getHeaders(credentials),\n      );\n\n      return data?.cloudApiKey;\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.controller.ts",
    "content": "import {\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { CloudCapiKey } from 'src/modules/cloud/capi-key/model';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\n\n@ApiTags('Cloud CAPI keys')\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('cloud/me/capi-keys')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CloudCapiKeyController {\n  constructor(private readonly service: CloudCapiKeyService) {}\n\n  @Get('')\n  @ApiEndpoint({\n    description: \"Return list of user's existing capi keys\",\n    statusCode: 200,\n    responses: [{ type: CloudCapiKey, isArray: true }],\n  })\n  async list(\n    @RequestSessionMetadata() sessionMetadata,\n  ): Promise<CloudCapiKey[]> {\n    return this.service.list(sessionMetadata);\n  }\n\n  @Delete(':id')\n  @ApiEndpoint({\n    description: \"Removes user's capi keys by id\",\n    statusCode: 200,\n  })\n  async delete(\n    @RequestSessionMetadata() sessionMetadata,\n    @Param('id') id: string,\n  ): Promise<void> {\n    return this.service.delete(sessionMetadata, id);\n  }\n\n  @Delete('')\n  @ApiEndpoint({\n    description: \"Removes all user's capi keys\",\n    statusCode: 200,\n  })\n  async deleteAll(@RequestSessionMetadata() sessionMetadata): Promise<void> {\n    return this.service.deleteAll(sessionMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudCapiKeyController } from 'src/modules/cloud/capi-key/cloud-capi-key.controller';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { LocalCloudCapiKeyRepository } from 'src/modules/cloud/capi-key/repository/local.cloud-capi-key.repository';\nimport { CloudCapiKeyRepository } from 'src/modules/cloud/capi-key/repository/cloud-capi-key.repository';\nimport { CloudCapiKeyApiProvider } from 'src/modules/cloud/capi-key/cloud-capi-key.api.provider';\nimport { CloudUserModule } from 'src/modules/cloud/user/cloud-user.module';\nimport { CloudSessionModule } from 'src/modules/cloud/session/cloud-session.module';\nimport { CloudCapiKeyAnalytics } from 'src/modules/cloud/capi-key/cloud-capi-key.analytics';\n\n@Module({\n  imports: [CloudUserModule, CloudSessionModule],\n  controllers: [CloudCapiKeyController],\n  providers: [\n    CloudCapiKeyApiProvider,\n    CloudCapiKeyService,\n    CloudCapiKeyAnalytics,\n    {\n      provide: CloudCapiKeyRepository,\n      useClass: LocalCloudCapiKeyRepository,\n    },\n  ],\n  exports: [CloudCapiKeyService, CloudCapiKeyApiProvider],\n})\nexport class CloudCapiKeyModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { CloudCapiKeyApiProvider } from 'src/modules/cloud/capi-key/cloud-capi-key.api.provider';\nimport { CloudCapiKeyRepository } from 'src/modules/cloud/capi-key/repository/cloud-capi-key.repository';\nimport {\n  mockCloudCapiKey,\n  mockCloudCapiKeyAnalytics,\n  mockCloudSessionService,\n  mockCloudUserApiService,\n  mockServerService,\n  mockSessionMetadata,\n  mockCloudUser,\n  mockUtm,\n  MockType,\n  mockCloudCapiKeyRepository,\n  mockCloudApiCapiKey,\n  mockServer,\n  mockCloudApiCapiAccessKey,\n  mockCloudSession,\n} from 'src/__mocks__';\nimport { when, resetAllWhenMocks } from 'jest-when';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { CloudCapiKeyAnalytics } from 'src/modules/cloud/capi-key/cloud-capi-key.analytics';\nimport {\n  CloudApiBadRequestException,\n  CloudCapiUnauthorizedException,\n} from 'src/modules/cloud/common/exceptions';\nimport axios from 'axios';\nimport {\n  CloudCapiKeyNotFoundException,\n  CloudCapiKeyUnauthorizedException,\n} from 'src/modules/cloud/capi-key/exceptions';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudCapiKeyService', () => {\n  let service: CloudCapiKeyService;\n  let repository: MockType<CloudCapiKeyRepository>;\n  let cloudUserApiService: MockType<CloudUserApiService>;\n  let cloudSessionService: MockType<CloudSessionService>;\n  let serverService: MockType<ServerService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    resetAllWhenMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudCapiKeyService,\n        CloudCapiKeyApiProvider,\n        {\n          provide: CloudCapiKeyRepository,\n          useFactory: mockCloudCapiKeyRepository,\n        },\n        {\n          provide: CloudUserApiService,\n          useFactory: mockCloudUserApiService,\n        },\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n        {\n          provide: ServerService,\n          useFactory: mockServerService,\n        },\n        {\n          provide: CloudCapiKeyAnalytics,\n          useFactory: mockCloudCapiKeyAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CloudCapiKeyService);\n    repository = await module.get(CloudCapiKeyRepository);\n    cloudUserApiService = await module.get(CloudUserApiService);\n    cloudSessionService = await module.get(CloudSessionService);\n    serverService = await module.get(ServerService);\n\n    when(mockedAxios.post)\n      .calledWith(\n        '/accounts/cloud-api/cloudApiKeys',\n        expect.anything(),\n        expect.anything(),\n      )\n      .mockResolvedValue({\n        status: 200,\n        data: { cloudApiKey: mockCloudApiCapiKey },\n      });\n    when(mockedAxios.post)\n      .calledWith(\n        '/accounts/cloud-api/cloudApiAccessKey',\n        expect.anything(),\n        expect.anything(),\n      )\n      .mockResolvedValue({\n        status: 200,\n        data: { cloudApiAccessKey: mockCloudApiCapiAccessKey },\n      });\n  });\n\n  describe('generateName', () => {\n    it('successfully generate capi key name', async () => {\n      expect(\n        await service['generateName'](mockSessionMetadata, mockCloudCapiKey),\n      ).toEqual(mockCloudCapiKey.name);\n      expect(serverService.getInfo).toHaveBeenCalledWith(mockSessionMetadata);\n    });\n    it('successfully generate capi key name when no createdAt field', async () => {\n      expect(\n        await service['generateName'](mockSessionMetadata, {\n          ...mockCloudCapiKey,\n          createdAt: undefined,\n        }),\n      ).toEqual(`RedisInsight-${mockServer.id.slice(0, 13)}-undefined`);\n      expect(serverService.getInfo).toHaveBeenCalledWith(mockSessionMetadata);\n    });\n    it('successfully generate capi key name when no capi key was provided', async () => {\n      expect(await service['generateName'](mockSessionMetadata, null)).toEqual(\n        `RedisInsight-${mockServer.id.slice(0, 13)}-undefined`,\n      );\n      expect(serverService.getInfo).toHaveBeenCalledWith(mockSessionMetadata);\n    });\n  });\n\n  describe('ensureCapiKeys', () => {\n    it('Should return exist capi key', async () => {\n      expect(\n        await service['ensureCapiKeys'](mockSessionMetadata, mockUtm),\n      ).toEqual(mockCloudUser.capiKey);\n      expect(mockedAxios.post).toHaveBeenCalledTimes(0);\n    });\n    it('Should generate new capi key', async () => {\n      repository.getByUserAccount.mockResolvedValueOnce(null);\n\n      expect(\n        await service['ensureCapiKeys'](mockSessionMetadata, mockUtm),\n      ).toEqual(mockCloudUser.capiKey);\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        '/accounts/cloud-api/cloudApiKeys',\n        expect.anything(),\n        expect.anything(),\n      );\n    });\n    it('Should generate new capi key but enable CAPI before', async () => {\n      repository.getByUserAccount.mockResolvedValueOnce(null);\n      cloudUserApiService.getCloudUser.mockResolvedValueOnce({\n        ...mockCloudUser,\n        capiKey: undefined,\n        accounts: [\n          {\n            ...mockCloudUser.accounts[0],\n            capiKey: undefined,\n          },\n        ],\n      });\n\n      expect(\n        await service['ensureCapiKeys'](mockSessionMetadata, mockUtm),\n      ).toEqual(mockCloudUser.capiKey);\n      expect(mockedAxios.post).toHaveBeenCalledTimes(2);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        '/accounts/cloud-api/cloudApiAccessKey',\n        expect.anything(),\n        expect.anything(),\n      );\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        2,\n        '/accounts/cloud-api/cloudApiKeys',\n        expect.anything(),\n        expect.anything(),\n      );\n    });\n    it('Should throw CloudCapiKeyUnauthorizedException if capiKey is not valid', async () => {\n      repository.getByUserAccount.mockResolvedValueOnce({\n        ...mockCloudCapiKey,\n        valid: false,\n      });\n\n      await expect(\n        service['ensureCapiKeys'](mockSessionMetadata, mockUtm),\n      ).rejects.toBeInstanceOf(CloudCapiKeyUnauthorizedException);\n      expect(mockedAxios.post).toHaveBeenCalledTimes(0);\n    });\n    it('Should throw CloudApiBadRequestException', async () => {\n      cloudUserApiService.getCloudUser.mockResolvedValue(null);\n      CloudUserApiService.getCurrentAccount(null);\n      await expect(\n        service['ensureCapiKeys'](mockSessionMetadata, mockUtm),\n      ).rejects.toThrowError(CloudApiBadRequestException);\n    });\n  });\n\n  describe('getCapiCredentials', () => {\n    it('Should generate new capi key', async () => {\n      expect(await service.getCapiCredentials(mockSessionMetadata)).toEqual(\n        mockCloudCapiKey,\n      );\n      expect(repository.update).toHaveBeenCalledWith(mockCloudCapiKey.id, {\n        lastUsed: expect.any(Date),\n      });\n    });\n  });\n\n  describe('get', () => {\n    it('Should get capi key', async () => {\n      expect(await service.get(mockCloudCapiKey.id)).toEqual(mockCloudCapiKey);\n    });\n    it('Should throw CloudCapiKeyNotFoundException when there is no capi key', async () => {\n      repository.get.mockReturnValueOnce(null);\n\n      await expect(service.get(mockCloudCapiKey.id)).rejects.toBeInstanceOf(\n        CloudCapiKeyNotFoundException,\n      );\n    });\n    it('Should wrap an error in case of any', async () => {\n      repository.get.mockRejectedValueOnce(new Error());\n\n      await expect(service.get(mockCloudCapiKey.id)).rejects.toBeInstanceOf(\n        InternalServerErrorException,\n      );\n    });\n  });\n\n  describe('getByUserAccount', () => {\n    it('Should get capi key', async () => {\n      expect(\n        await service.getByUserAccount(\n          mockSessionMetadata,\n          mockCloudUser.id,\n          mockCloudUser.currentAccountId,\n        ),\n      ).toEqual(mockCloudCapiKey);\n    });\n    it('Should throw CloudCapiKeyNotFoundException when there is no capi key', async () => {\n      repository.getByUserAccount.mockReturnValueOnce(null);\n\n      await expect(\n        service.getByUserAccount(\n          mockSessionMetadata,\n          mockCloudUser.id,\n          mockCloudUser.currentAccountId,\n        ),\n      ).rejects.toBeInstanceOf(CloudCapiKeyNotFoundException);\n    });\n  });\n\n  describe('list', () => {\n    it('Should get list of capi keys', async () => {\n      expect(await service.list(mockSessionMetadata)).toEqual([\n        mockCloudCapiKey,\n      ]);\n    });\n    it('Should wrap an error in case of any', async () => {\n      repository.list.mockRejectedValueOnce(new Error());\n\n      await expect(service.list(mockSessionMetadata)).rejects.toBeInstanceOf(\n        InternalServerErrorException,\n      );\n    });\n  });\n\n  describe('delete', () => {\n    it('Should delete capi key', async () => {\n      expect(\n        await service.delete(mockSessionMetadata, mockCloudCapiKey.id),\n      ).toEqual(undefined);\n    });\n    it('Should wrap an error in case of any', async () => {\n      repository.delete.mockRejectedValueOnce(new Error());\n\n      await expect(\n        service.delete(mockSessionMetadata, mockCloudCapiKey.id),\n      ).rejects.toBeInstanceOf(InternalServerErrorException);\n    });\n  });\n\n  describe('deleteAll', () => {\n    it('Should delete all capi keys', async () => {\n      expect(await service.deleteAll(mockSessionMetadata)).toEqual(undefined);\n    });\n    it('Should wrap an error in case of any', async () => {\n      repository.deleteAll.mockRejectedValueOnce(new Error());\n\n      await expect(\n        service.deleteAll(mockSessionMetadata),\n      ).rejects.toBeInstanceOf(InternalServerErrorException);\n    });\n  });\n\n  describe('handleCapiKeyUnauthorizedError', () => {\n    it('should show BadRequestException error', async () => {\n      const mockError = new BadRequestException('error');\n      expect(\n        await service.handleCapiKeyUnauthorizedError(\n          mockError,\n          mockSessionMetadata,\n        ),\n      ).toEqual(new BadRequestException('error'));\n    });\n    it('should throw CloudCapiKeyUnauthorizedException error and mark as invalid', async () => {\n      cloudSessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudSession,\n        user: { capiKey: mockCloudCapiKey },\n      });\n      const mockError = new CloudCapiUnauthorizedException();\n\n      expect(\n        await service.handleCapiKeyUnauthorizedError(\n          mockError,\n          mockSessionMetadata,\n        ),\n      ).toEqual(\n        new CloudCapiKeyUnauthorizedException(undefined, {\n          resourceId: mockCloudCapiKey.id,\n        }),\n      );\n    });\n    it('should throw CloudCapiUnauthorizedException if no key', async () => {\n      cloudSessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudSession,\n        user: { capiKey: null },\n      });\n      const mockError = new CloudCapiUnauthorizedException();\n\n      expect(\n        await service.handleCapiKeyUnauthorizedError(\n          mockError,\n          mockSessionMetadata,\n        ),\n      ).toEqual(mockError);\n    });\n    it('should throw CloudCapiUnauthorizedException if no user', async () => {\n      cloudSessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudSession,\n        user: null,\n      });\n      const mockError = new CloudCapiUnauthorizedException();\n\n      expect(\n        await service.handleCapiKeyUnauthorizedError(\n          mockError,\n          mockSessionMetadata,\n        ),\n      ).toEqual(mockError);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/cloud-capi-key.service.ts",
    "content": "import { CloudCapiKeyRepository } from 'src/modules/cloud/capi-key/repository/cloud-capi-key.repository';\nimport { CloudCapiKey } from 'src/modules/cloud/capi-key/model';\nimport { wrapHttpError } from 'src/common/utils';\nimport { SessionMetadata } from 'src/common/models';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport {\n  CloudApiBadRequestException,\n  CloudCapiUnauthorizedException,\n} from 'src/modules/cloud/common/exceptions';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { CloudCapiKeyApiProvider } from 'src/modules/cloud/capi-key/cloud-capi-key.api.provider';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  CloudCapiKeyNotFoundException,\n  CloudCapiKeyUnauthorizedException,\n} from 'src/modules/cloud/capi-key/exceptions';\nimport { CloudCapiKeyAnalytics } from 'src/modules/cloud/capi-key/cloud-capi-key.analytics';\n\n@Injectable()\nexport class CloudCapiKeyService {\n  private logger = new Logger('CloudCapiKeyService');\n\n  constructor(\n    private readonly api: CloudCapiKeyApiProvider,\n    private readonly repository: CloudCapiKeyRepository,\n    private readonly cloudUserApiService: CloudUserApiService,\n    private readonly cloudSessionService: CloudSessionService,\n    private readonly serverService: ServerService,\n    private readonly analytics: CloudCapiKeyAnalytics,\n  ) {}\n\n  private async generateName(\n    sessionMetadata: SessionMetadata,\n    capiKey: Partial<CloudCapiKey>,\n  ): Promise<string> {\n    const serverInfo = await this.serverService.getInfo(sessionMetadata);\n\n    return `RedisInsight-${serverInfo.id.substring(0, 13)}-${capiKey?.createdAt?.getTime()}`;\n  }\n\n  /**\n   * Generate CAPI key + secret if needed\n   * @param sessionMetadata\n   * @param utm\n   */\n  private async ensureCapiKeys(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudCapiKey> {\n    try {\n      let user = await this.cloudUserApiService.getCloudUser(\n        sessionMetadata,\n        false,\n        utm,\n      );\n\n      let currentAccount = CloudUserApiService.getCurrentAccount(user);\n\n      if (!currentAccount) {\n        this.logger.error('Cannot get current account', sessionMetadata);\n        throw new CloudApiBadRequestException('No active account');\n      }\n\n      let capiKey = await this.repository.getByUserAccount(\n        sessionMetadata.userId,\n        user.id,\n        user.currentAccountId,\n      );\n\n      if (!capiKey) {\n        try {\n          const session = await this.cloudSessionService.getSession(\n            sessionMetadata.sessionId,\n          );\n\n          // enable capi if needed\n          if (!currentAccount.capiKey) {\n            this.logger.debug('Trying to enable capi', sessionMetadata);\n\n            await this.api.enableCapi(session);\n\n            this.logger.debug('Successfully enabled capi', sessionMetadata);\n\n            user = await this.cloudUserApiService.getCloudUser(\n              sessionMetadata,\n              true,\n              utm,\n            );\n            currentAccount = CloudUserApiService.getCurrentAccount(user);\n          }\n\n          this.logger.debug('Creating new capi key', sessionMetadata);\n\n          capiKey = {\n            userId: sessionMetadata.userId,\n            cloudUserId: user.id,\n            cloudAccountId: user.currentAccountId,\n            capiKey: currentAccount.capiKey,\n            createdAt: new Date(),\n          } as CloudCapiKey;\n          capiKey.name = await this.generateName(sessionMetadata, capiKey);\n\n          capiKey = await this.repository.create(\n            plainToInstance(CloudCapiKey, capiKey),\n          );\n\n          this.analytics.sendCloudAccountKeyGenerated(sessionMetadata);\n        } catch (e) {\n          this.logger.error(\n            'Failed to create new capi key',\n            sessionMetadata,\n            e,\n          );\n          this.analytics.sendCloudAccountKeyGenerationFailed(\n            sessionMetadata,\n            e,\n          );\n          throw e;\n        }\n      }\n\n      // Throw an error. User action required in this case\n      if (capiKey.valid === false) {\n        this.logger.error('Capi key is not valid', sessionMetadata);\n        return Promise.reject(\n          new CloudCapiKeyUnauthorizedException(undefined, {\n            resourceId: capiKey.id,\n          }),\n        );\n      }\n\n      if (!capiKey.capiSecret) {\n        try {\n          const session = await this.cloudSessionService.getSession(\n            sessionMetadata.sessionId,\n          );\n\n          const capiSecret = await this.api.createCapiKey(\n            session,\n            user.id,\n            capiKey.name,\n          );\n          capiKey = await this.repository.update(capiKey.id, {\n            capiSecret: capiSecret.secret_key,\n          });\n\n          await this.cloudUserApiService.updateUser(sessionMetadata, {\n            capiKey,\n            accounts: user.accounts,\n          });\n\n          this.analytics.sendCloudAccountSecretGenerated(sessionMetadata);\n        } catch (e) {\n          this.logger.error('Failed create capi secret', sessionMetadata);\n          this.analytics.sendCloudAccountSecretGenerationFailed(\n            sessionMetadata,\n            e,\n          );\n          throw e;\n        }\n      }\n\n      return capiKey;\n    } catch (e) {\n      this.logger.error('Unable to generate capi keys', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Returns CAPI credentials and ensures CAPI keys and updates last usage time\n   * @param sessionMetadata\n   * @param utm\n   */\n  async getCapiCredentials(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudCapiKey> {\n    const capiKey = await this.ensureCapiKeys(sessionMetadata, utm);\n\n    await this.repository.update(capiKey.id, { lastUsed: new Date() });\n\n    return capiKey;\n  }\n\n  /**\n   * Get CAPI key by id\n   * @param id\n   */\n  async get(id: string): Promise<CloudCapiKey> {\n    try {\n      this.logger.debug('Getting capi key by id');\n\n      const model = await this.repository.get(id);\n\n      if (!model) {\n        return Promise.reject(new CloudCapiKeyNotFoundException());\n      }\n\n      return model;\n    } catch (e) {\n      this.logger.debug('Unable to get capi key by id', e);\n\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get user's capi key by cloud user id and cloud account id\n   * @param sessionMetadata\n   * @param cloudUserId\n   * @param cloudAccountId\n   */\n  async getByUserAccount(\n    sessionMetadata: SessionMetadata,\n    cloudUserId: number,\n    cloudAccountId: number,\n  ): Promise<CloudCapiKey> {\n    try {\n      this.logger.debug(\n        \"Getting user's capi key by cloud user and cloud account\",\n        sessionMetadata,\n      );\n\n      const model = await this.repository.getByUserAccount(\n        sessionMetadata.userId,\n        cloudUserId,\n        cloudAccountId,\n      );\n\n      if (!model) {\n        throw new CloudCapiKeyNotFoundException();\n      }\n\n      return model;\n    } catch (e) {\n      this.logger.error(\n        \"Unable to get user's capi key by cloud user and cloud account\",\n        e,\n        sessionMetadata,\n      );\n\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get user's capi keys list\n   * @param sessionMetadata\n   */\n  async list(sessionMetadata: SessionMetadata): Promise<CloudCapiKey[]> {\n    try {\n      this.logger.debug('Getting list of local capi keys', sessionMetadata);\n\n      return await this.repository.list(sessionMetadata.userId);\n    } catch (e) {\n      this.logger.error(\n        'Unable to get list of local capi keys',\n        e,\n        sessionMetadata,\n      );\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Removes user's capi key by id\n   * @param sessionMetadata\n   * @param id\n   */\n  async delete(sessionMetadata: SessionMetadata, id: string): Promise<void> {\n    try {\n      this.logger.debug('Removing capi key');\n\n      await this.repository.delete(sessionMetadata.userId, id);\n    } catch (e) {\n      this.logger.error('Unable to remove capi key', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Removes all user's capi keys\n   * @param sessionMetadata\n   */\n  async deleteAll(sessionMetadata: SessionMetadata): Promise<void> {\n    try {\n      this.logger.debug('Removing all capi keys', sessionMetadata);\n\n      await this.repository.deleteAll(sessionMetadata.userId);\n    } catch (e) {\n      this.logger.error('Unable to remove all capi keys', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Determines if capi key unauthorized error\n   * @param e\n   * @param sessionMetadata\n   */\n  async handleCapiKeyUnauthorizedError(\n    e: Error,\n    sessionMetadata: SessionMetadata,\n  ): Promise<Error> {\n    try {\n      if (e instanceof CloudCapiUnauthorizedException) {\n        const cloudSession = await this.cloudSessionService.getSession(\n          sessionMetadata.sessionId,\n        );\n\n        if (cloudSession.user?.capiKey?.id) {\n          // mark key as invalid\n          await this.repository.update(cloudSession.user.capiKey.id, {\n            valid: false,\n          });\n          // remove current key from the user\n          await this.cloudUserApiService.updateUser(sessionMetadata, {\n            capiKey: null,\n          });\n\n          return new CloudCapiKeyUnauthorizedException(\n            undefined, // default message\n            { resourceId: cloudSession.user.capiKey.id },\n          );\n        }\n      }\n    } catch (error) {\n      // ignore error\n    }\n\n    return e;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/entity/cloud-capi-key.entity.ts",
    "content": "import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';\nimport { Expose } from 'class-transformer';\n\n@Entity('cloud_capi_key')\n@Unique(['userId', 'cloudAccountId', 'cloudUserId'])\nexport class CloudCapiKeyEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  userId: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  name: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  cloudAccountId: number;\n\n  @Expose()\n  @Column({ nullable: false })\n  cloudUserId: number;\n\n  @Expose()\n  @Column({ nullable: true })\n  capiKey: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  capiSecret: string;\n\n  @Expose()\n  @Column({ nullable: true, default: true })\n  valid: boolean;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Expose()\n  @Column({ type: 'datetime', nullable: true })\n  createdAt: Date;\n\n  @Expose()\n  @Column({ type: 'datetime', nullable: true })\n  lastUsed: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/exceptions/cloud-capi-key.not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudCapiKeyNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.NOT_FOUND,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'CloudCapiKeyNotFound',\n      errorCode: CustomErrorCodes.CloudCapiKeyNotFound,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/exceptions/cloud-capi-key.unauthorized.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudCapiKeyUnauthorizedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED,\n    options?: HttpExceptionOptions & { resourceId?: string },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'CloudCapiKeyUnauthorized',\n      errorCode: CustomErrorCodes.CloudCapiKeyUnauthorized,\n      resourceId: options?.resourceId,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/exceptions/index.ts",
    "content": "export * from './cloud-capi-key.not-found.exception';\nexport * from './cloud-capi-key.unauthorized.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/model/api.interface.ts",
    "content": "export interface ICloudApiCapiAccessKey {\n  accessKey: string;\n}\n\nexport interface ICloudApiCapiKey {\n  id: number;\n  name: string;\n  user_account: number;\n  secret_key?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/model/cloud-capi-key.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class CloudCapiKey {\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  userId: string;\n\n  @ApiProperty({\n    description:\n      'Autogenerated name of capi key (Redisinsight-<RI id>-<ISO date of creation>',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    type: Number,\n  })\n  @Expose()\n  cloudAccountId: number;\n\n  @ApiProperty({\n    type: Number,\n  })\n  @Expose()\n  cloudUserId: number;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  capiKey: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  capiSecret: string;\n\n  @ApiProperty({\n    type: Boolean,\n  })\n  @Expose()\n  valid?: boolean;\n\n  @ApiProperty({\n    type: Date,\n  })\n  @Expose()\n  createdAt?: Date;\n\n  @ApiProperty({\n    type: Date,\n  })\n  @Expose()\n  lastUsed?: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/model/index.ts",
    "content": "export * from './api.interface';\nexport * from './cloud-capi-key';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/repository/cloud-capi-key.repository.ts",
    "content": "import { CloudCapiKey } from 'src/modules/cloud/capi-key/model';\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport abstract class CloudCapiKeyRepository {\n  /**\n   * Get Cloud CAPI key by id\n   * Note: for internal use only\n   * @param id\n   */\n  abstract get(id: string): Promise<CloudCapiKey>;\n\n  /**\n   * Update Cloud CAPI key by id\n   * Note: for internal use only\n   * @param id\n   * @param data\n   */\n  abstract update(\n    id: string,\n    data: Partial<CloudCapiKey>,\n  ): Promise<CloudCapiKey>;\n\n  /**\n   * Get current user CAPI key by cloud user id and cloud account id\n   * @param userId\n   * @param cloudUserId\n   * @param cloudAccountId\n   */\n  abstract getByUserAccount(\n    userId: string,\n    cloudUserId: number,\n    cloudAccountId: number,\n  ): Promise<CloudCapiKey>;\n\n  /**\n   * Create CAPI key\n   * @param model\n   * @throws CloudApiBadRequestException - in case of unique constraint error\n   */\n  abstract create(model: CloudCapiKey): Promise<CloudCapiKey>;\n\n  /**\n   * List of CAPI keys for current user\n   * @param userId\n   */\n  abstract list(userId: string): Promise<CloudCapiKey[]>;\n\n  /**\n   * Delete current user CAPI key by id\n   * @param userId\n   * @param id\n   */\n  abstract delete(userId: string, id: string): Promise<void>;\n\n  /**\n   * Delete all CAPI keys for current user\n   * @param userId\n   */\n  abstract deleteAll(userId: string): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/repository/local.cloud-capi-key.repository.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { pick } from 'lodash';\nimport {\n  mockFeatureEntity,\n  mockRepository,\n  MockType,\n  mockDatabase,\n  mockCloudApiCapiKey,\n  mockEncryptionService,\n  mockCloudCapiKeyEntity,\n  mockCapiKeyEncrypted,\n  mockCapiSecretEncrypted,\n  mockCloudCapiAuthDto,\n  mockCloudCapiKey,\n} from 'src/__mocks__';\nimport { LocalCloudCapiKeyRepository } from 'src/modules/cloud/capi-key/repository/local.cloud-capi-key.repository';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';\nimport { CloudCapiKeyEntity } from 'src/modules/cloud/capi-key/entity/cloud-capi-key.entity';\nimport { CloudApiBadRequestException } from 'src/modules/cloud/common/exceptions';\n\ndescribe('LocalCloudCapiKeyRepository', () => {\n  let service: LocalCloudCapiKeyRepository;\n  let repository: MockType<Repository<CloudCapiKeyEntity>>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalCloudCapiKeyRepository,\n        {\n          provide: getRepositoryToken(CloudCapiKeyEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(CloudCapiKeyEntity));\n    service = await module.get(LocalCloudCapiKeyRepository);\n    encryptionService = await module.get(EncryptionService);\n\n    repository.findOneBy.mockResolvedValue(mockCloudCapiKeyEntity);\n    repository.find.mockResolvedValue([\n      mockCloudCapiKeyEntity,\n      mockCloudCapiKeyEntity,\n    ]);\n    repository.save.mockResolvedValue(mockCloudCapiKeyEntity);\n    repository.delete.mockResolvedValue({ deleted: 1 });\n    repository.merge.mockResolvedValue(mockCloudApiCapiKey);\n    repository\n      .createQueryBuilder()\n      .getMany.mockResolvedValue([\n        pick(mockCloudCapiKey, 'id', 'name', 'valid', 'createdAt', 'lastUsed'),\n        pick(mockCloudCapiKey, 'id', 'name', 'valid', 'createdAt', 'lastUsed'),\n      ]);\n    repository.merge.mockReturnValue(mockFeatureEntity);\n\n    when(encryptionService.decrypt)\n      .calledWith(mockCapiKeyEncrypted, expect.anything())\n      .mockResolvedValue(mockCloudCapiAuthDto.capiKey)\n      .calledWith(mockCapiSecretEncrypted, expect.anything())\n      .mockResolvedValue(mockCloudCapiAuthDto.capiSecret);\n\n    when(encryptionService.encrypt)\n      .calledWith(mockCloudCapiAuthDto.capiKey)\n      .mockResolvedValue({\n        data: mockCapiKeyEncrypted,\n        encryption: mockCloudCapiKeyEntity.encryption,\n      })\n      .calledWith(mockCloudCapiAuthDto.capiSecret)\n      .mockResolvedValue({\n        data: mockCapiSecretEncrypted,\n        encryption: mockCloudCapiKeyEntity.encryption,\n      });\n  });\n\n  describe('get', () => {\n    it('should return decrypted and transformed capi key', async () => {\n      expect(await service.get(mockDatabase.id)).toEqual(mockCloudCapiKey);\n    });\n    it('should return null fields in case of decryption errors', async () => {\n      when(encryptionService.decrypt)\n        .calledWith(mockCapiKeyEncrypted, expect.anything())\n        .mockRejectedValueOnce(new KeytarDecryptionErrorException());\n\n      expect(await service.get(mockDatabase.id)).toEqual({\n        ...mockCloudCapiKey,\n        capiKey: null,\n      });\n    });\n    it('should return null', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      expect(await service.get(mockDatabase.id)).toEqual(null);\n    });\n  });\n\n  describe('update', () => {\n    it('should return features', async () => {\n      const result = await service.update('id', mockCloudCapiKey);\n\n      expect(result).toEqual(mockCloudCapiKey);\n    });\n  });\n\n  describe('getByUserAccount', () => {\n    it('should return decrypted and transformed capi key', async () => {\n      const result = await service.getByUserAccount(\n        mockCloudCapiKey.userId,\n        mockCloudCapiKey.cloudUserId,\n        mockCloudCapiKey.cloudAccountId,\n      );\n\n      expect(result).toEqual(mockCloudCapiKey);\n    });\n\n    it('should return null', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.getByUserAccount(\n        mockCloudCapiKey.userId,\n        mockCloudCapiKey.cloudUserId,\n        mockCloudCapiKey.cloudAccountId,\n      );\n\n      expect(result).toEqual(null);\n    });\n  });\n\n  describe('create', () => {\n    it('should delete and do not return anything', async () => {\n      const result = await service.create(mockCloudCapiKey);\n\n      expect(result).toEqual(mockCloudCapiKey);\n    });\n\n    it('should throw CloudApiBadRequestException ON SQL constraint error', async () => {\n      const constraintError: any = new Error('FOREIGN_KEY error');\n      constraintError.code = 'SQLITE_CONSTRAINT';\n\n      repository.save.mockRejectedValueOnce(constraintError);\n\n      try {\n        await service.create(mockCloudCapiKey);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(CloudApiBadRequestException);\n        expect(e.message).toEqual('Such capi key already exists');\n      }\n    });\n\n    it('should throw error', async () => {\n      const constraintError: any = new Error('error');\n      constraintError.code = 'custom_code';\n\n      repository.save.mockRejectedValueOnce(constraintError);\n\n      try {\n        await service.create(mockCloudCapiKey);\n        fail();\n      } catch (e) {\n        expect(e.message).toEqual('error');\n      }\n    });\n  });\n\n  describe('list', () => {\n    it('should delete and do not return anything', async () => {\n      const result = await service.list(mockCloudCapiKey.userId);\n\n      expect(result).toEqual([\n        pick(mockCloudCapiKey, 'id', 'name', 'valid', 'createdAt', 'lastUsed'),\n        pick(mockCloudCapiKey, 'id', 'name', 'valid', 'createdAt', 'lastUsed'),\n      ]);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete and do not return anything', async () => {\n      const result = await service.delete(\n        mockCloudCapiKey.userId,\n        mockCloudCapiKey.id,\n      );\n\n      expect(result).toEqual(undefined);\n    });\n  });\n\n  describe('deleteAll', () => {\n    it('should delete and do not return anything', async () => {\n      const result = await service.deleteAll(mockCloudCapiKey.userId);\n\n      expect(result).toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/capi-key/repository/local.cloud-capi-key.repository.ts",
    "content": "import { CloudCapiKey } from 'src/modules/cloud/capi-key/model';\nimport { CloudCapiKeyRepository } from 'src/modules/cloud/capi-key/repository/cloud-capi-key.repository';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CloudCapiKeyEntity } from 'src/modules/cloud/capi-key/entity/cloud-capi-key.entity';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { classToClass } from 'src/utils';\nimport { CloudApiBadRequestException } from 'src/modules/cloud/common/exceptions';\n\nexport class LocalCloudCapiKeyRepository extends CloudCapiKeyRepository {\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(CloudCapiKeyEntity)\n    private readonly repository: Repository<CloudCapiKeyEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, [\n      'capiKey',\n      'capiSecret',\n    ]);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async get(id: string): Promise<CloudCapiKey> {\n    const entity = await this.repository.findOneBy({ id });\n    if (!entity) {\n      return null;\n    }\n\n    return classToClass(\n      CloudCapiKey,\n      await this.modelEncryptor.decryptEntity(entity, true),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async update(\n    id: string,\n    data: Partial<CloudCapiKey>,\n  ): Promise<CloudCapiKey> {\n    const oldEntity = await this.modelEncryptor.decryptEntity(\n      await this.repository.findOneBy({ id }),\n      true,\n    );\n    const newEntity = classToClass(CloudCapiKeyEntity, data);\n\n    const encrypted = await this.modelEncryptor.encryptEntity(\n      this.repository.merge(oldEntity, newEntity),\n    );\n    await this.repository.save(encrypted);\n\n    return this.get(id);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getByUserAccount(\n    userId: string,\n    cloudUserId: number,\n    cloudAccountId: number,\n  ): Promise<CloudCapiKey> {\n    const entity = await this.repository.findOneBy({\n      userId,\n      cloudUserId,\n      cloudAccountId,\n    });\n\n    if (!entity) {\n      return null;\n    }\n\n    return classToClass(\n      CloudCapiKey,\n      await this.modelEncryptor.decryptEntity(entity, true),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async create(model: CloudCapiKey): Promise<CloudCapiKey> {\n    try {\n      const entity = classToClass(CloudCapiKeyEntity, model);\n\n      return classToClass(\n        CloudCapiKey,\n        await this.modelEncryptor.decryptEntity(\n          await this.repository.save(\n            await this.modelEncryptor.encryptEntity(entity),\n          ),\n          true,\n        ),\n      );\n    } catch (e) {\n      if (e.code === 'SQLITE_CONSTRAINT') {\n        throw new CloudApiBadRequestException('Such capi key already exists');\n      }\n\n      throw e;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async list(userId: string): Promise<CloudCapiKey[]> {\n    const entities = await this.repository\n      .createQueryBuilder('k')\n      .select(['k.id', 'k.name', 'k.valid', 'k.createdAt', 'k.lastUsed'])\n      .where({ userId })\n      .orderBy('k.createdAt', 'DESC')\n      .getMany();\n\n    return entities.map((entity) => classToClass(CloudCapiKey, entity));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async delete(userId: string, id: string): Promise<void> {\n    await this.repository.delete({ id, userId });\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async deleteAll(userId: string): Promise<void> {\n    await this.repository.delete({ userId });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/cloud-sso.feature.flag.ts",
    "content": "import { BuildType } from 'src/modules/server/models/server';\nimport config, { Config } from 'src/utils/config';\nimport { Feature } from 'src/modules/feature/model/feature';\n\nconst serverConfig = config.get('server') as Config['server'];\n\nexport enum CloudSsoFeatureStrategy {\n  DeepLink = 'deepLink',\n  Web = 'web',\n}\n\nexport class CloudSsoFeatureFlag {\n  static getFeature(): Partial<Feature> {\n    if (serverConfig.buildType === BuildType.Electron) {\n      return {\n        strategy: CloudSsoFeatureStrategy.DeepLink,\n      };\n    }\n\n    return {};\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/cloud.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudAutodiscoveryModule } from 'src/modules/cloud/autodiscovery/cloud.autodiscovery.module';\nimport { CloudAuthModule } from 'src/modules/cloud/auth/cloud-auth.module';\nimport { CloudUserModule } from 'src/modules/cloud/user/cloud-user.module';\nimport { CloudTaskModule } from 'src/modules/cloud/task/cloud-task.module';\nimport { CloudJobModule } from 'src/modules/cloud/job/cloud-job.module';\nimport { CloudCapiKeyModule } from 'src/modules/cloud/capi-key/cloud-capi-key.module';\nimport { CloudSessionModule } from './session/cloud-session.module';\n\n@Module({})\nexport class CloudModule {\n  static register() {\n    return {\n      module: CloudModule,\n      imports: [\n        CloudSessionModule.register(),\n        CloudAuthModule,\n        CloudUserModule,\n        CloudAutodiscoveryModule,\n        CloudTaskModule,\n        CloudJobModule,\n        CloudCapiKeyModule,\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/constants/index.ts",
    "content": "export enum CloudAuthServerEvent {\n  Logout = 'logout',\n}\n\nexport enum CloudJobEvents {\n  Monitor = 'cloud:job:monitor',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/decorators/cloud-auth.decorator.ts",
    "content": "import {\n  createParamDecorator,\n  ExecutionContext,\n  UnauthorizedException,\n} from '@nestjs/common';\nimport { Validator } from 'class-validator';\nimport { plainToInstance } from 'class-transformer';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\n\nconst validator = new Validator();\n\nexport const cloudAuthDtoFromRequestHeadersFactory = (\n  data: unknown,\n  ctx: ExecutionContext,\n): CloudCapiAuthDto => {\n  const request = ctx.switchToHttp().getRequest();\n\n  const dto = plainToInstance(CloudCapiAuthDto, {\n    capiKey: request.headers['x-cloud-api-key'],\n    capiSecret: request.headers['x-cloud-api-secret'],\n  });\n\n  const errors = validator.validateSync(dto);\n\n  if (errors?.length) {\n    throw new UnauthorizedException(\n      'Required authentication credentials were not provided',\n    );\n  }\n\n  return dto;\n};\n\nexport const CloudAuthHeaders = createParamDecorator(\n  cloudAuthDtoFromRequestHeadersFactory,\n);\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/dto/cloud.capi.auth.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsDefined, IsNotEmpty, IsString } from 'class-validator';\nimport { ICloudCapiCredentials } from 'src/modules/cloud/common/models';\n\nexport class CloudCapiAuthDto implements ICloudCapiCredentials {\n  @ApiProperty({\n    description: 'Cloud API account key',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  capiKey: string;\n\n  @ApiProperty({\n    description: 'Cloud API secret key',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  capiSecret: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/dto/index.ts",
    "content": "export * from './cloud.capi.auth.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-api.bad-request.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudApiBadRequestException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.BAD_REQUEST,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudApiBadRequest',\n      errorCode: CustomErrorCodes.CloudApiBadRequest,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-api.error.handler.ts",
    "content": "import { AxiosError } from 'axios';\nimport { HttpException } from '@nestjs/common';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions/cloud-api.unauthorized.exception';\nimport { CloudApiForbiddenException } from 'src/modules/cloud/common/exceptions/cloud-api.forbidden.exception';\nimport { CloudApiBadRequestException } from 'src/modules/cloud/common/exceptions/cloud-api.bad-request.exception';\nimport { CloudApiNotFoundException } from 'src/modules/cloud/common/exceptions/cloud-api.not-found.exception';\nimport { CloudApiInternalServerErrorException } from 'src/modules/cloud/common/exceptions/cloud-api.internal-server-error.exception';\n\nexport const wrapCloudApiError = (\n  error: AxiosError,\n  message?: string,\n): HttpException => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  const { response } = error;\n\n  let errorMessage = message || error.message;\n  if (!errorMessage) {\n    const data = response?.data as any;\n    errorMessage = data?.message;\n  }\n\n  if (response) {\n    const errorOptions = { cause: response?.data };\n    switch (response?.status) {\n      case 401:\n        return new CloudApiUnauthorizedException(errorMessage, errorOptions);\n      case 403:\n        return new CloudApiForbiddenException(errorMessage, errorOptions);\n      case 400:\n        return new CloudApiBadRequestException(errorMessage, errorOptions);\n      case 404:\n        return new CloudApiNotFoundException(errorMessage, errorOptions);\n      default:\n        return new CloudApiInternalServerErrorException(\n          errorMessage,\n          errorOptions,\n        );\n    }\n  }\n\n  return new CloudApiInternalServerErrorException(errorMessage);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-api.forbidden.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudApiForbiddenException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FORBIDDEN,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.FORBIDDEN,\n      error: 'CloudApiForbidden',\n      errorCode: CustomErrorCodes.CloudApiForbidden,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-api.internal-server-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudApiInternalServerErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.INTERNAL_SERVER_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudApiInternalServerError',\n      errorCode: CustomErrorCodes.CloudApiInternalServerError,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-api.not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudApiNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.NOT_FOUND,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'CloudApiNotFound',\n      errorCode: CustomErrorCodes.CloudApiNotFound,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-api.unauthorized.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudApiUnauthorizedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.UNAUTHORIZED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'CloudApiUnauthorized',\n      errorCode: CustomErrorCodes.CloudApiUnauthorized,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-capi.error.handler.ts",
    "content": "import { AxiosError } from 'axios';\nimport { HttpException } from '@nestjs/common';\nimport { wrapCloudApiError } from 'src/modules/cloud/common/exceptions/cloud-api.error.handler';\nimport { CloudCapiUnauthorizedException } from 'src/modules/cloud/common/exceptions/cloud-capi.unauthorized.exception';\n\nexport const wrapCloudCapiError = (\n  error: AxiosError,\n  message?: string,\n): HttpException => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  if (error.response?.status === 401) {\n    return new CloudCapiUnauthorizedException(message || error.message, {\n      cause: error.response?.data,\n    });\n  }\n\n  return wrapCloudApiError(error, message);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/cloud-capi.unauthorized.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class CloudCapiUnauthorizedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.UNAUTHORIZED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'CloudCapiUnauthorized',\n      errorCode: CustomErrorCodes.CloudCapiUnauthorized,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/exceptions/index.ts",
    "content": "export * from './cloud-api.unauthorized.exception';\nexport * from './cloud-api.bad-request.exception';\nexport * from './cloud-api.forbidden.exception';\nexport * from './cloud-api.internal-server-error.exception';\nexport * from './cloud-api.not-found.exception';\nexport * from './cloud-api.error.handler';\nexport * from './cloud-capi.unauthorized.exception';\nexport * from './cloud-capi.error.handler';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/models/api.interface.ts",
    "content": "import { CloudAuthIdpType } from 'src/modules/cloud/auth/models';\n\nexport interface ICloudApiCredentials {\n  accessToken?: string;\n  refreshToken?: string;\n  idToken?: string;\n  idpType?: CloudAuthIdpType;\n  apiSessionId?: string;\n  csrf?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/models/capi.interface.ts",
    "content": "export interface ICloudCapiCredentials {\n  capiKey?: string;\n  capiSecret?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/models/cloud-request-utm.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport { Default } from 'src/common/decorators';\n\nexport class CloudRequestUtm {\n  @ApiPropertyOptional({\n    type: String,\n    default: 'redisinsight',\n  })\n  @IsOptional()\n  @IsString()\n  @IsNotEmpty()\n  @Default('redisinsight')\n  source? = 'redisinsight';\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  @IsOptional()\n  @IsString()\n  @IsNotEmpty()\n  @Default('sso')\n  medium? = 'sso';\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  @IsOptional()\n  @IsString()\n  @IsNotEmpty()\n  campaign?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  @IsOptional()\n  @IsString()\n  @IsNotEmpty()\n  amp?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  @IsOptional()\n  @IsString()\n  @IsNotEmpty()\n  package?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/models/index.ts",
    "content": "export * from './api.interface';\nexport * from './capi.interface';\nexport * from './cloud-request-utm';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/providers/cloud.api.provider.spec.ts",
    "content": "import {\n  CloudRequestUtm,\n  ICloudApiCredentials,\n} from 'src/modules/cloud/common/models';\nimport {\n  mockCloudSessionService,\n  mockDefaultCloudApiHeaders,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { CloudUserApiProvider } from 'src/modules/cloud/user/providers/cloud-user.api.provider';\nimport {\n  CloudApiForbiddenException,\n  CloudApiUnauthorizedException,\n} from 'src/modules/cloud/common/exceptions';\nimport { CloudApiProvider } from './cloud.api.provider';\n\nconst generateUtmBodyTests = [\n  {\n    input: null,\n    expected: {},\n  },\n  {\n    input: { source: 'source' },\n    expected: {\n      utm_source: 'source',\n    },\n  },\n  {\n    input: { medium: 'medium' },\n    expected: {\n      utm_medium: 'medium',\n    },\n  },\n  {\n    input: { source: 'source', medium: 'medium', campaign: 'campaign' },\n    expected: {\n      utm_source: 'source',\n      utm_medium: 'medium',\n      utm_campaign: 'campaign',\n    },\n  },\n  {\n    input: { campaign: 'campaign' },\n    expected: {\n      utm_campaign: 'campaign',\n    },\n  },\n];\n\nconst getHeadersTests = [\n  {\n    expected: { ...mockDefaultCloudApiHeaders },\n  },\n  {\n    input: {},\n    expected: { ...mockDefaultCloudApiHeaders },\n  },\n  {\n    input: { accessToken: 'jwt-token' },\n    expected: {\n      ...mockDefaultCloudApiHeaders,\n      authorization: 'Bearer jwt-token',\n    },\n  },\n  {\n    input: { apiSessionId: 'id' },\n    expected: { ...mockDefaultCloudApiHeaders, cookie: 'JSESSIONID=id' },\n  },\n  {\n    input: { csrf: 'csrf-token' },\n    expected: { ...mockDefaultCloudApiHeaders, 'x-csrf-token': 'csrf-token' },\n  },\n  {\n    input: { idToken: 'id-token-value' },\n    expected: {\n      ...mockDefaultCloudApiHeaders,\n      'Sm-Id-Token': 'id-token-value',\n    },\n  },\n  {\n    input: {\n      accessToken: 'jwt-token',\n      idToken: 'id-token-value',\n      csrf: 'csrf-token',\n    },\n    expected: {\n      ...mockDefaultCloudApiHeaders,\n      authorization: 'Bearer jwt-token',\n      'x-csrf-token': 'csrf-token',\n      'Sm-Id-Token': 'id-token-value',\n    },\n  },\n];\n\nconst mockedResult = 'mockedResult';\nconst mockedFn = jest.fn().mockResolvedValue(mockedResult);\n\ndescribe('CloudApiProvider', () => {\n  let service: CloudUserApiProvider;\n  let sessionService: MockType<CloudSessionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudUserApiProvider,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CloudUserApiProvider);\n    sessionService = await module.get(CloudSessionService);\n  });\n\n  describe('callWithAuthRetry', () => {\n    it('should return result from 1st attempt', async () => {\n      expect(\n        await service.callWithAuthRetry(\n          mockSessionMetadata.sessionId,\n          mockedFn,\n        ),\n      ).toEqual(mockedResult);\n      expect(sessionService.invalidateApiSession).toHaveBeenCalledTimes(0);\n    });\n    it('should not fail when session invalidation throw an error', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      sessionService.invalidateApiSession.mockRejectedValueOnce(\n        new Error('Unable to invalidate'),\n      );\n      expect(\n        await service.callWithAuthRetry(\n          mockSessionMetadata.sessionId,\n          mockedFn,\n        ),\n      ).toEqual(mockedResult);\n      expect(sessionService.invalidateApiSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.invalidateApiSession).toHaveBeenCalledTimes(1);\n    });\n    it('should throw an error from 1st attempt if not CloudApiUnauthorizedException (and keep session)', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiForbiddenException());\n      await expect(\n        service.callWithAuthRetry(mockSessionMetadata.sessionId, mockedFn),\n      ).rejects.toBeInstanceOf(CloudApiForbiddenException);\n      expect(sessionService.invalidateApiSession).toHaveBeenCalledTimes(0);\n    });\n    it('should throw CloudApiForbiddenException error from 2nd attempt (by default)', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      await expect(\n        service.callWithAuthRetry(mockSessionMetadata.sessionId, mockedFn),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(mockedFn).toHaveBeenCalledTimes(2);\n      expect(sessionService.invalidateApiSession).toHaveBeenCalledTimes(1);\n    });\n    it('should throw CloudApiForbiddenException error from 3rd attempt (custom attempts)', async () => {\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      mockedFn.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      await expect(\n        service.callWithAuthRetry(mockSessionMetadata.sessionId, mockedFn, 2),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(mockedFn).toHaveBeenCalledTimes(3);\n      expect(sessionService.invalidateApiSession).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('generateUtmQuery', () => {\n    test.each(generateUtmBodyTests)('%j', ({ input, expected }) => {\n      expect(\n        CloudApiProvider.generateUtmBody(input as CloudRequestUtm),\n      ).toEqual(expected);\n    });\n  });\n\n  describe('getHeaders', () => {\n    test.each(getHeadersTests)('%j', ({ input, expected }) => {\n      expect(\n        CloudApiProvider.getHeaders(input as ICloudApiCredentials),\n      ).toEqual({ headers: expected });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/providers/cloud.api.provider.ts",
    "content": "import axios from 'axios';\nimport {\n  CloudRequestUtm,\n  ICloudApiCredentials,\n} from 'src/modules/cloud/common/models';\nimport config, { Config } from 'src/utils/config';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { Injectable } from '@nestjs/common';\n\nconst serverConfig = config.get('server') as Config['server'];\nconst cloudConfig = config.get('cloud');\n\n@Injectable()\nexport class CloudApiProvider {\n  protected api = axios.create({\n    baseURL: cloudConfig.apiUrl,\n  });\n\n  constructor(private readonly cloudSessionService: CloudSessionService) {}\n\n  async callWithAuthRetry(\n    sessionId: string,\n    fn: () => Promise<any>,\n    retries = 1,\n  ) {\n    try {\n      return await fn();\n    } catch (e) {\n      if (retries > 0 && e instanceof CloudApiUnauthorizedException) {\n        await this.cloudSessionService\n          .invalidateApiSession(sessionId)\n          .catch(() => {});\n        return this.callWithAuthRetry(sessionId, fn, retries - 1);\n      }\n\n      throw e;\n    }\n  }\n\n  /**\n   * Generates utm query parameters object\n   * @param utm\n   */\n  static generateUtmBody(utm: CloudRequestUtm): Record<string, string> {\n    return {\n      utm_source: utm?.source,\n      utm_medium: utm?.medium,\n      utm_campaign: utm?.campaign,\n      utm_amp: utm?.amp,\n      utm_package: utm?.package,\n    };\n  }\n\n  /**\n   * Prepare header for authorized requests\n   * @param credentials\n   */\n  static getHeaders(credentials: ICloudApiCredentials): { headers: {} } {\n    const headers = {\n      'User-Agent': `RedisInsight/${serverConfig.version}`,\n      'x-redisinsight-token': cloudConfig.apiToken,\n    };\n\n    if (credentials?.accessToken) {\n      headers['authorization'] = `Bearer ${credentials.accessToken}`;\n    }\n\n    if (credentials?.apiSessionId) {\n      headers['cookie'] = `JSESSIONID=${credentials.apiSessionId}`;\n    }\n\n    if (credentials?.csrf) {\n      headers['x-csrf-token'] = credentials.csrf;\n    }\n\n    if (credentials?.idToken) {\n      headers['Sm-Id-Token'] = credentials.idToken;\n    }\n\n    return {\n      headers,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/providers/cloud.capi.provider.spec.ts",
    "content": "import { ICloudCapiCredentials } from 'src/modules/cloud/common/models';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport config, { Config } from 'src/utils/config';\nimport { CloudCapiProvider } from './cloud.capi.provider';\n\nconst serverConfig = config.get('server') as Config['server'];\n\nconst getPrefixTests = [\n  {\n    input: undefined,\n    expected: '',\n  },\n  {\n    input: CloudSubscriptionType.Fixed,\n    expected: '/fixed',\n  },\n  {\n    input: CloudSubscriptionType.Flexible,\n    expected: '',\n  },\n];\n\nconst userAgent = `RedisInsight/${serverConfig.version}`;\n\nconst getHeadersTests = [\n  {\n    input: {},\n    expected: {\n      'x-api-key': undefined,\n      'x-api-secret-key': undefined,\n      'User-Agent': userAgent,\n    },\n  },\n  {\n    input: { capiKey: 'key' },\n    expected: {\n      'x-api-key': 'key',\n      'x-api-secret-key': undefined,\n      'User-Agent': userAgent,\n    },\n  },\n  {\n    input: { capiSecret: 'secret' },\n    expected: {\n      'x-api-key': undefined,\n      'x-api-secret-key': 'secret',\n      'User-Agent': userAgent,\n    },\n  },\n  {\n    input: { capiKey: 'key', capiSecret: 'secret' },\n    expected: {\n      'x-api-key': 'key',\n      'x-api-secret-key': 'secret',\n      'User-Agent': userAgent,\n    },\n  },\n];\n\ndescribe('CloudCapiProvider', () => {\n  describe('getPrefix', () => {\n    test.each(getPrefixTests)('%j', ({ input, expected }) => {\n      expect(\n        CloudCapiProvider.getPrefix(input as CloudSubscriptionType),\n      ).toEqual(expected);\n    });\n  });\n\n  describe('getHeaders', () => {\n    test.each(getHeadersTests)('%j', ({ input, expected }) => {\n      expect(\n        CloudCapiProvider.getHeaders(input as ICloudCapiCredentials),\n      ).toEqual({ headers: expected });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/common/providers/cloud.capi.provider.ts",
    "content": "import axios from 'axios';\nimport { ICloudCapiCredentials } from 'src/modules/cloud/common/models';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport config, { Config } from 'src/utils/config';\n\nconst cloudConfig = config.get('cloud');\nconst serverConfig = config.get('server') as Config['server'];\n\nexport class CloudCapiProvider {\n  protected api = axios.create({\n    baseURL: cloudConfig.capiUrl,\n  });\n\n  /**\n   * Get api base for fixed subscriptions\n   * @param type\n   * @private\n   */\n  static getPrefix(type?: CloudSubscriptionType): string {\n    return `${type === CloudSubscriptionType.Fixed ? '/fixed' : ''}`;\n  }\n\n  /**\n   * Generates auth headers to attach to the request\n   * @param credentials\n   * @private\n   */\n  static getHeaders(credentials: ICloudCapiCredentials): {\n    headers: Record<string, string>;\n  } {\n    return {\n      headers: {\n        'x-api-key': credentials?.capiKey,\n        'x-api-secret-key': credentials?.capiSecret,\n        'User-Agent': `RedisInsight/${serverConfig.version}`,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport {\n  mockFreeCloudSubscriptionPlan1,\n  mockCloudSubscriptionCapiService,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\n\ndescribe('CloudDatabaseAnalytics', () => {\n  let service: CloudDatabaseAnalytics;\n  let sendEventMethod;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EventEmitter2,\n        CloudDatabaseAnalytics,\n        CloudSubscriptionCapiService,\n        {\n          provide: CloudSubscriptionCapiService,\n          useFactory: mockCloudSubscriptionCapiService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CloudDatabaseAnalytics);\n    sendEventMethod = jest.spyOn<CloudDatabaseAnalytics, any>(\n      service,\n      'sendEvent',\n    );\n    sendFailedEventMethod = jest.spyOn<CloudDatabaseAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendCloudSignInSucceeded', () => {\n    it('should emit event with eventData', () => {\n      service.sendCloudFreeDatabaseCreated(mockSessionMetadata, {\n        data: 'custom',\n      });\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudFreeDatabaseCreated,\n        {\n          data: 'custom',\n        },\n      );\n    });\n\n    it('should emit event with eventData = {}', () => {\n      service.sendCloudFreeDatabaseCreated(mockSessionMetadata);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudFreeDatabaseCreated,\n        {},\n      );\n    });\n  });\n\n  describe('sendCloudFreeDatabaseFailed', () => {\n    it('should emit error event with selected plan', async () => {\n      service.sendCloudFreeDatabaseFailed(mockSessionMetadata, httpException, {\n        region: mockFreeCloudSubscriptionPlan1.region,\n        provider: mockFreeCloudSubscriptionPlan1.provider,\n      });\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.CloudFreeDatabaseFailed,\n        httpException,\n        {\n          region: mockFreeCloudSubscriptionPlan1.region,\n          provider: mockFreeCloudSubscriptionPlan1.provider,\n        },\n      );\n    });\n  });\n\n  it('should emit error event with selected plan', async () => {\n    service.sendCloudFreeDatabaseFailed(\n      mockSessionMetadata,\n      httpException,\n      undefined,\n    );\n\n    expect(sendFailedEventMethod).toHaveBeenCalledWith(\n      mockSessionMetadata,\n      TelemetryEvents.CloudFreeDatabaseFailed,\n      httpException,\n      {},\n    );\n  });\n\n  it('should emit error event when free subscription is not exist', async () => {\n    service.sendCloudFreeDatabaseFailed(\n      mockSessionMetadata,\n      httpException,\n      undefined,\n    );\n\n    expect(sendFailedEventMethod).toHaveBeenCalledWith(\n      mockSessionMetadata,\n      TelemetryEvents.CloudFreeDatabaseFailed,\n      httpException,\n      {\n        region: undefined,\n        provider: undefined,\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CloudDatabaseAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendCloudFreeDatabaseCreated(\n    sessionMetadata: SessionMetadata,\n    eventData: object = {},\n  ) {\n    this.sendEvent(\n      sessionMetadata,\n      TelemetryEvents.CloudFreeDatabaseCreated,\n      eventData,\n    );\n  }\n\n  sendCloudFreeDatabaseFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    eventData: object = {},\n  ) {\n    try {\n      this.sendFailedEvent(\n        sessionMetadata,\n        TelemetryEvents.CloudFreeDatabaseFailed,\n        exception,\n        eventData,\n      );\n    } catch (error) {\n      // ignore\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.capi.provider.spec.ts",
    "content": "import { pick } from 'lodash';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudCapiAuthDto,\n  mockCloudCapiHeaders,\n  mockCloudTaskInit,\n  mockGetCloudSubscriptionDatabaseDto,\n  mockCloudCapiDatabase,\n  mockGetCloudSubscriptionDatabaseDtoFixed,\n  mockCloudCapiDatabaseFixed,\n  mockCreateFreeCloudDatabaseDto,\n  mockCloudCapiSubscriptionDatabasesFixed,\n  mockCloudCapiSubscriptionDatabases,\n} from 'src/__mocks__';\nimport { CloudCapiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudDatabaseCapiProvider } from 'src/modules/cloud/database/cloud-database.capi.provider';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudDatabaseCapiProvider', () => {\n  let service: CloudDatabaseCapiProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [CloudDatabaseCapiProvider],\n    }).compile();\n\n    service = module.get(CloudDatabaseCapiProvider);\n  });\n\n  describe('getDatabase', () => {\n    it('successfully get flexible cloud database', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudCapiDatabase,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).toEqual(mockCloudCapiDatabase);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/subscriptions/${mockGetCloudSubscriptionDatabaseDto.subscriptionId}/databases/${mockGetCloudSubscriptionDatabaseDto.databaseId}`,\n        mockCloudCapiHeaders,\n      );\n    });\n    it('successfully get fixed cloud database', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudCapiDatabaseFixed,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDtoFixed,\n        ),\n      ).toEqual(mockCloudCapiDatabaseFixed);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/fixed/subscriptions/${mockGetCloudSubscriptionDatabaseDtoFixed.subscriptionId}/databases/${mockGetCloudSubscriptionDatabaseDtoFixed.databaseId}`,\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n  describe('getDatabases', () => {\n    it('successfully get flexible cloud databases', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudCapiSubscriptionDatabases,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getDatabases(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).toEqual(mockCloudCapiSubscriptionDatabases);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/subscriptions/${mockGetCloudSubscriptionDatabaseDto.subscriptionId}/databases`,\n        mockCloudCapiHeaders,\n      );\n    });\n    it('successfully get fixed cloud databases', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudCapiSubscriptionDatabasesFixed,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getDatabases(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDtoFixed,\n        ),\n      ).toEqual(mockCloudCapiSubscriptionDatabasesFixed);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/fixed/subscriptions/${mockGetCloudSubscriptionDatabaseDtoFixed.subscriptionId}/databases`,\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getDatabases(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n  describe('createFreeDatabase', () => {\n    it('successfully create fixed cloud database', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudTaskInit,\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.createFreeDatabase(\n          mockCloudCapiAuthDto,\n          mockCreateFreeCloudDatabaseDto,\n        ),\n      ).toEqual(mockCloudTaskInit);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        `/fixed/subscriptions/${mockGetCloudSubscriptionDatabaseDto.subscriptionId}/databases`,\n        pick(mockCreateFreeCloudDatabaseDto, [\n          'name',\n          'protocol',\n          'replication',\n          'alerts',\n          'dataEvictionPolicy',\n          'dataPersistence',\n          'free',\n        ]),\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.createFreeDatabase(\n          mockCloudCapiAuthDto,\n          mockCreateFreeCloudDatabaseDto,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n\n  describe('getDatabaseTags', () => {\n    const mockTags = [\n      {\n        key: 'tag1',\n        value: 'value1',\n      },\n      {\n        key: 'tag2',\n        value: 'value2',\n      },\n    ];\n\n    it('successfully get flexible cloud database tags', async () => {\n      const response = {\n        status: 200,\n        data: {\n          tags: mockTags,\n        },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getDatabaseTags(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).toEqual(mockTags);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/subscriptions/${mockGetCloudSubscriptionDatabaseDto.subscriptionId}/databases/${mockGetCloudSubscriptionDatabaseDto.databaseId}/tags`,\n        mockCloudCapiHeaders,\n      );\n    });\n\n    it('successfully get fixed cloud database tags', async () => {\n      const response = {\n        status: 200,\n        data: {\n          tags: mockTags,\n        },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getDatabaseTags(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDtoFixed,\n        ),\n      ).toEqual(mockTags);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/fixed/subscriptions/${mockGetCloudSubscriptionDatabaseDtoFixed.subscriptionId}/databases/${mockGetCloudSubscriptionDatabaseDtoFixed.databaseId}/tags`,\n        mockCloudCapiHeaders,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.capi.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { CloudCapiProvider } from 'src/modules/cloud/common/providers/cloud.capi.provider';\nimport {\n  ICloudCapiDatabase,\n  ICloudCapiSubscriptionDatabases,\n  ICloudCapiDatabaseTag,\n} from 'src/modules/cloud/database/models';\nimport { ICloudCapiCredentials } from 'src/modules/cloud/common/models';\nimport {\n  CreateFreeCloudDatabaseDto,\n  GetCloudSubscriptionDatabaseDto,\n  GetCloudSubscriptionDatabasesDto,\n} from 'src/modules/cloud/database/dto';\nimport { wrapCloudCapiError } from 'src/modules/cloud/common/exceptions';\nimport { ICloudCapiTask } from 'src/modules/cloud/task/models';\n\n@Injectable()\nexport class CloudDatabaseCapiProvider extends CloudCapiProvider {\n  /**\n   * Get single database details\n   * @param credentials\n   * @param dto\n   */\n  async getDatabase(\n    credentials: ICloudCapiCredentials,\n    dto: GetCloudSubscriptionDatabaseDto,\n  ): Promise<ICloudCapiDatabase> {\n    try {\n      const { subscriptionId, databaseId, subscriptionType } = dto;\n\n      const { data } = await this.api.get(\n        `${CloudCapiProvider.getPrefix(subscriptionType)}/subscriptions/${subscriptionId}/databases/${databaseId}`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (e) {\n      throw wrapCloudCapiError(e);\n    }\n  }\n\n  /**\n   * Get list of databases for subscription\n   * @param credentials\n   * @param dto\n   */\n  async getDatabases(\n    credentials: ICloudCapiCredentials,\n    dto: GetCloudSubscriptionDatabasesDto,\n  ): Promise<ICloudCapiSubscriptionDatabases> {\n    try {\n      const { subscriptionId, subscriptionType } = dto;\n\n      const { data } = await this.api.get(\n        `${CloudCapiProvider.getPrefix(subscriptionType)}/subscriptions/${subscriptionId}/databases`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (e) {\n      throw wrapCloudCapiError(e);\n    }\n  }\n\n  /**\n   * Get single database details\n   * @param credentials\n   * @param dto\n   */\n  async createFreeDatabase(\n    credentials: ICloudCapiCredentials,\n    dto: CreateFreeCloudDatabaseDto,\n  ): Promise<ICloudCapiTask> {\n    try {\n      const { subscriptionId, subscriptionType, ...createDto } = dto;\n\n      const { data } = await this.api.post(\n        `${CloudCapiProvider.getPrefix(subscriptionType)}/subscriptions/${subscriptionId}/databases`,\n        createDto,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (e) {\n      throw wrapCloudCapiError(e);\n    }\n  }\n\n  /**\n   * Get single database tags\n   * @param credentials\n   * @param dto\n   */\n  async getDatabaseTags(\n    credentials: ICloudCapiCredentials,\n    dto: GetCloudSubscriptionDatabaseDto,\n  ): Promise<ICloudCapiDatabaseTag[]> {\n    try {\n      const { subscriptionId, databaseId, subscriptionType } = dto;\n\n      const response = await this.api.get(\n        `${CloudCapiProvider.getPrefix(subscriptionType)}/subscriptions/${subscriptionId}/databases/${databaseId}/tags`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n      const tags: ICloudCapiDatabaseTag[] = response.data?.tags || [];\n\n      return tags;\n    } catch (e) {\n      // failed to get tags\n      return [];\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.capi.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCloudCapiAuthDto,\n  mockCloudCapiDatabaseTags,\n  mockCloudCapiSubscriptionDatabasesFixed,\n  mockCloudDatabase,\n  mockCloudDatabaseCapiProvider,\n  mockCloudDatabaseFromList,\n  mockCloudDatabaseFromListFixed,\n  mockCloudTaskInit,\n  mockCreateFreeCloudDatabaseDto,\n  mockGetCloudSubscriptionDatabaseDto,\n  mockGetCloudSubscriptionDatabasesDto,\n  mockGetCloudSubscriptionDatabasesDtoFixed,\n  MockType,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { CloudDatabaseCapiProvider } from 'src/modules/cloud/database/cloud-database.capi.provider';\n\ndescribe('CloudDatabaseCapiService', () => {\n  let service: CloudDatabaseCapiService;\n  let capi: MockType<CloudDatabaseCapiProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudDatabaseCapiService,\n        {\n          provide: CloudDatabaseCapiProvider,\n          useFactory: mockCloudDatabaseCapiProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudDatabaseCapiService);\n    capi = module.get(CloudDatabaseCapiProvider);\n  });\n\n  describe('getDatabase', () => {\n    it('successfully get cloud databases', async () => {\n      expect(\n        await service.getDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).toEqual({\n        ...mockCloudDatabase,\n        tags: mockCloudCapiDatabaseTags,\n      });\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.getDatabase.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('getDatabases', () => {\n    it('successfully get cloud databases', async () => {\n      expect(\n        await service.getDatabases(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabasesDto,\n        ),\n      ).toEqual([mockCloudDatabaseFromList]);\n    });\n    it('successfully get cloud fixed databases', async () => {\n      capi.getDatabases.mockResolvedValueOnce(\n        mockCloudCapiSubscriptionDatabasesFixed,\n      );\n      expect(\n        await service.getDatabases(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabasesDtoFixed,\n        ),\n      ).toEqual([mockCloudDatabaseFromListFixed]);\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.getDatabases.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getDatabases(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabasesDto,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('createFreeDatabase', () => {\n    it('successfully create free cloud database', async () => {\n      expect(\n        await service.createFreeDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabasesDtoFixed,\n        ),\n      ).toEqual(mockCloudTaskInit);\n      expect(capi.createFreeDatabase).toHaveBeenCalledWith(\n        mockCloudCapiAuthDto,\n        mockCreateFreeCloudDatabaseDto,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.createFreeDatabase.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.createFreeDatabase(\n          mockCloudCapiAuthDto,\n          mockGetCloudSubscriptionDatabaseDto,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.capi.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport {\n  GetCloudSubscriptionDatabaseDto,\n  GetCloudSubscriptionDatabasesDto,\n} from 'src/modules/cloud/database/dto';\nimport { CloudDatabaseCapiProvider } from 'src/modules/cloud/database/cloud-database.capi.provider';\nimport {\n  CloudDatabase,\n  CloudDatabaseAlertName,\n  CloudDatabaseDataEvictionPolicy,\n  CloudDatabasePersistencePolicy,\n  CloudDatabaseProtocol,\n} from 'src/modules/cloud/database/models';\nimport {\n  parseCloudDatabaseCapiResponse,\n  parseCloudDatabasesCapiResponse,\n} from 'src/modules/cloud/database/utils';\nimport config from 'src/utils/config';\nimport { parseCloudTaskCapiResponse } from 'src/modules/cloud/task/utils';\n\nconst cloudConfig = config.get('cloud');\n\n@Injectable()\nexport class CloudDatabaseCapiService {\n  private logger = new Logger('CloudDatabaseCapiService');\n\n  constructor(private readonly capi: CloudDatabaseCapiProvider) {}\n\n  /**\n   * Get single database details\n   * @param authDto\n   * @param dto\n   */\n  async getDatabase(\n    authDto: CloudCapiAuthDto,\n    dto: GetCloudSubscriptionDatabaseDto,\n  ): Promise<CloudDatabase> {\n    try {\n      this.logger.debug('Getting cloud database', dto);\n\n      const [database, tags] = await Promise.all([\n        this.capi.getDatabase(authDto, dto),\n        this.capi.getDatabaseTags(authDto, dto),\n      ]);\n\n      this.logger.debug('Succeed to get databases in RE cloud subscription.');\n\n      return parseCloudDatabaseCapiResponse(\n        database,\n        tags,\n        dto.subscriptionId,\n        dto.subscriptionType,\n        dto.free,\n      );\n    } catch (e) {\n      this.logger.error('Failed to get cloud database', e);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get list of databases from subscription\n   * @param authDto\n   * @param dto\n   */\n  async getDatabases(\n    authDto: CloudCapiAuthDto,\n    dto: GetCloudSubscriptionDatabasesDto,\n  ): Promise<CloudDatabase[]> {\n    try {\n      this.logger.debug('Getting cloud databases from subscription');\n\n      const data = await this.capi.getDatabases(authDto, dto);\n\n      this.logger.debug('Succeed to get cloud databases from subscription.');\n\n      return parseCloudDatabasesCapiResponse(\n        data,\n        dto.subscriptionType,\n        dto.free,\n      );\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Creating free database along with subscription if needed\n   * @param authDto\n   * @param dto\n   */\n  async createFreeDatabase(\n    authDto: CloudCapiAuthDto,\n    dto: GetCloudSubscriptionDatabasesDto,\n  ) {\n    try {\n      this.logger.debug('Creating free database');\n\n      const task = await this.capi.createFreeDatabase(authDto, {\n        ...dto,\n        name: cloudConfig.freeDatabaseName,\n        protocol: CloudDatabaseProtocol.Stack,\n        dataPersistence: CloudDatabasePersistencePolicy.None,\n        dataEvictionPolicy: CloudDatabaseDataEvictionPolicy.VolatileLru,\n        replication: false,\n        alerts: [\n          {\n            name: CloudDatabaseAlertName.ConnectionsLimit,\n            value: 80,\n          },\n          {\n            name: CloudDatabaseAlertName.DatasetsSize,\n            value: 80,\n          },\n        ],\n      });\n\n      return parseCloudTaskCapiResponse(task);\n    } catch (e) {\n      this.logger.error('Unable to create free database', e);\n      throw wrapHttpError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/cloud-database.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudDatabaseCapiProvider } from 'src/modules/cloud/database/cloud-database.capi.provider';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';\n\n@Module({\n  providers: [\n    CloudDatabaseCapiProvider,\n    CloudDatabaseCapiService,\n    CloudDatabaseAnalytics,\n  ],\n  controllers: [],\n  exports: [CloudDatabaseCapiService, CloudDatabaseAnalytics],\n})\nexport class CloudDatabaseModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/dto/create-free-cloud-database.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n  IsArray,\n  IsBoolean,\n  IsDefined,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsString,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport {\n  CloudDatabaseAlert,\n  CloudDatabaseDataEvictionPolicy,\n  CloudDatabasePersistencePolicy,\n  CloudDatabaseProtocol,\n} from 'src/modules/cloud/database/models';\n\nexport class CreateFreeCloudDatabaseDto {\n  @ApiProperty({\n    description: 'Database name',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  @ApiProperty({\n    description: 'Subscription Id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  subscriptionId: number;\n\n  @IsEnum(CloudSubscriptionType)\n  @IsNotEmpty()\n  subscriptionType: CloudSubscriptionType;\n\n  @IsEnum(CloudDatabaseProtocol)\n  @IsNotEmpty()\n  protocol: CloudDatabaseProtocol;\n\n  @IsEnum(CloudDatabasePersistencePolicy)\n  @IsNotEmpty()\n  dataPersistence: CloudDatabasePersistencePolicy;\n\n  @IsEnum(CloudDatabaseDataEvictionPolicy)\n  @IsNotEmpty()\n  dataEvictionPolicy: CloudDatabaseDataEvictionPolicy;\n\n  @IsBoolean()\n  @IsNotEmpty()\n  replication: boolean;\n\n  @IsEnum(CloudDatabaseAlert, { each: true })\n  @IsNotEmpty({ each: true })\n  @IsArray()\n  alerts: CloudDatabaseAlert[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/dto/get-cloud-subscription-database.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsBoolean,\n  IsDefined,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsOptional,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\n\nexport class GetCloudSubscriptionDatabaseDto {\n  @ApiProperty({\n    description: 'Subscription Id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  subscriptionId: number;\n\n  @IsEnum(CloudSubscriptionType, {\n    message: `subscriptionType must be a valid enum value. Valid values: ${Object.values(\n      CloudSubscriptionType,\n    )}.`,\n  })\n  @IsNotEmpty()\n  subscriptionType: CloudSubscriptionType;\n\n  @ApiProperty({\n    description: 'Database Id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  databaseId: number;\n\n  @ApiPropertyOptional({\n    type: Boolean,\n  })\n  @IsOptional()\n  @IsBoolean()\n  free?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/dto/get-cloud-subscription-databases.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsBoolean,\n  IsDefined,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsOptional,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\n\nexport class GetCloudSubscriptionDatabasesDto {\n  @ApiProperty({\n    description: 'Subscription Id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  subscriptionId: number;\n\n  @ApiProperty({\n    description: 'Subscription Id',\n    enum: CloudSubscriptionType,\n  })\n  @IsEnum(CloudSubscriptionType, {\n    message: `subscriptionType must be a valid enum value. Valid values: ${Object.values(\n      CloudSubscriptionType,\n    )}.`,\n  })\n  @IsNotEmpty()\n  subscriptionType: CloudSubscriptionType;\n\n  @ApiPropertyOptional({\n    type: Boolean,\n  })\n  @IsOptional()\n  @IsBoolean()\n  free?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/dto/index.ts",
    "content": "export * from './create-free-cloud-database.dto';\nexport * from './get-cloud-subscription-database.dto';\nexport * from './get-cloud-subscription-databases.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/entities/cloud-database-details.entity.ts",
    "content": "import {\n  Column,\n  Entity,\n  JoinColumn,\n  OneToOne,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Entity('database_cloud_details')\nexport class CloudDatabaseDetailsEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  cloudId: number;\n\n  @Expose()\n  @Column({ nullable: false })\n  subscriptionType: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  planMemoryLimit: number;\n\n  @Expose()\n  @Column({ nullable: true })\n  memoryLimitMeasurementUnit: number;\n\n  @Expose()\n  @Column({ nullable: true, default: false })\n  free: boolean;\n\n  @OneToOne(() => DatabaseEntity, (database) => database.cloudDetails, {\n    nullable: true,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn()\n  database: DatabaseEntity;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/models/capi.interface.ts",
    "content": "import {\n  CloudDatabaseProtocol,\n  CloudDatabaseStatus,\n} from 'src/modules/cloud/database/models';\n\ninterface ICloudCapiDatabaseAlert {\n  name: string;\n  value: number;\n}\n\ninterface ICloudCapiDatabaseClustering {\n  numberOfShards: number;\n  regexRules: any[];\n  hashingPolicy: string;\n}\n\nexport interface ICloudCapiDatabaseModule {\n  id: number;\n  name: string;\n  version: string;\n  description?: string;\n  parameters?: any[];\n}\n\ninterface ICloudCapiDatabaseSecurity {\n  password?: string;\n  sslClientAuthentication: boolean;\n  sourceIps: string[];\n}\n\nexport interface ICloudCapiDatabaseTag {\n  key: string;\n  value: string;\n  createdAt: string;\n  updatedAt: string;\n  links: string[];\n}\n\nexport interface ICloudCapiDatabase {\n  databaseId: number;\n  name: string;\n  protocol: CloudDatabaseProtocol;\n  provider: string;\n  region: string;\n  redisVersionCompliance: string;\n  status: CloudDatabaseStatus;\n  memoryLimitInGb: number;\n  memoryUsedInMb: number;\n  memoryStorage: string;\n  supportOSSClusterApi: boolean;\n  dataPersistence: string;\n  replication: boolean;\n  periodicBackupPath?: string;\n  dataEvictionPolicy: string;\n  throughputMeasurement: {\n    by: string;\n    value: number;\n  };\n  activatedOn: string;\n  lastModified: string;\n  publicEndpoint: string;\n  privateEndpoint: string;\n  replicaOf: {\n    endpoints: string[];\n  };\n  clustering: ICloudCapiDatabaseClustering;\n  security: ICloudCapiDatabaseSecurity;\n  modules: ICloudCapiDatabaseModule[];\n  alerts: ICloudCapiDatabaseAlert[];\n  planMemoryLimit?: number;\n  memoryLimitMeasurementUnit?: string;\n}\n\nexport interface ICloudCapiSubscriptionDatabasesSubscription {\n  subscriptionId: number;\n  numberOfDatabases: number;\n  databases: ICloudCapiDatabase[];\n}\n\nexport interface ICloudCapiSubscriptionDatabases {\n  accountId: number;\n  subscription:\n    | ICloudCapiSubscriptionDatabasesSubscription\n    | ICloudCapiSubscriptionDatabasesSubscription[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/models/cloud-database-details.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport {\n  IsBoolean,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsNumber,\n  IsOptional,\n  IsString,\n} from 'class-validator';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models/cloud-subscription';\n\nexport class CloudDatabaseDetails {\n  @ApiProperty({\n    description: 'Database id from the cloud',\n    type: Number,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  cloudId: number;\n\n  @ApiProperty({\n    description: 'Subscription id from the cloud',\n    type: Number,\n  })\n  @Expose()\n  @IsOptional()\n  subscriptionId: number;\n\n  @ApiProperty({\n    description: 'Subscription type',\n    enum: () => CloudSubscriptionType,\n    example: CloudSubscriptionType.Flexible,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsEnum(CloudSubscriptionType, {\n    message: `subscriptionType must be a valid enum value. Valid values: ${Object.values(\n      CloudSubscriptionType,\n    )}.`,\n  })\n  subscriptionType: CloudSubscriptionType;\n\n  @ApiPropertyOptional({\n    description: 'Plan memory limit',\n    type: Number,\n    example: 256,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNumber()\n  planMemoryLimit?: number;\n\n  @ApiPropertyOptional({\n    description: 'Memory limit units',\n    type: String,\n    example: 'MB',\n  })\n  @Expose()\n  @IsOptional()\n  @IsString()\n  memoryLimitMeasurementUnit?: string;\n\n  @ApiPropertyOptional({\n    description: 'Is free database',\n    type: Boolean,\n    example: false,\n  })\n  @Expose()\n  @IsOptional()\n  @IsBoolean()\n  free?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Is subscription using bdb packages',\n    type: Boolean,\n    example: false,\n  })\n  @Expose()\n  @IsOptional()\n  @IsBoolean()\n  isBdbPackage?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/models/cloud-database.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models/cloud-subscription';\nimport { CloudDatabaseDetails } from 'src/modules/cloud/database/models/cloud-database-details';\nimport { IsArray, IsEnum, IsInt, IsNotEmpty } from 'class-validator';\nimport { Tag } from 'src/modules/tag/models/tag';\n\nexport enum CloudDatabaseProtocol {\n  Redis = 'redis',\n  Stack = 'stack',\n  Memcached = 'memcached',\n}\n\nexport enum CloudDatabasePersistencePolicy {\n  AofEveryOneSecond = 'aof-every-1-second',\n  AofEveryWrite = 'aof-every-write',\n  SnapshotEveryOneHour = 'snapshot-every-1-hour',\n  SnapshotEverySixHours = 'snapshot-every-6-hours',\n  SnapshotEveryTwelveHours = 'snapshot-every-12-hours',\n  None = 'none',\n}\n\nexport enum CloudDatabaseDataEvictionPolicy {\n  AllKeysLru = 'allkeys-lru',\n  AllKeysLfu = 'allkeys-lfu',\n  AllKeysRandom = 'allkeys-random',\n  VolatileLru = 'volatile-lru',\n  VolatileLfu = 'volatile-lfu',\n  VolatileRandom = 'volatile-random',\n  VolatileTtl = 'volatile-ttl',\n  NoEviction = 'noeviction',\n}\n\nexport enum CloudDatabaseMemoryStorage {\n  Ram = 'ram',\n  RamAndFlash = 'ram-and-flash',\n}\n\nexport enum CloudDatabaseStatus {\n  Draft = 'draft',\n  Pending = 'pending',\n  CreationFailed = 'creation-failed',\n  Active = 'active',\n  ActiveChangePending = 'active-change-pending',\n  ImportPending = 'import-pending',\n  DeletePending = 'delete-pending',\n  Recovery = 'recovery',\n}\n\nexport enum CloudDatabaseAlertName {\n  DatasetSize = 'dataset-size',\n  DatasetsSize = 'datasets-size',\n  ThroughputHigherThan = 'throughput-higher-than',\n  ThroughputLowerThan = 'throughput-lower-than',\n  Latency = 'latency',\n  SyncSourceError = 'syncsource-error',\n  SyncSourceLag = 'syncsource-lag',\n  ConnectionsLimit = 'connections-limit',\n}\n\nexport class CloudDatabaseAlert {\n  @ApiProperty({\n    description: 'Database alert name',\n    type: String,\n  })\n  @Expose()\n  @IsEnum(CloudDatabaseAlertName)\n  @IsNotEmpty()\n  name: CloudDatabaseAlertName;\n\n  @ApiProperty({\n    description: 'Database alert value',\n    type: Number,\n  })\n  @Expose()\n  @IsInt()\n  @IsNotEmpty()\n  value: number;\n}\n\nexport class CloudDatabase {\n  @ApiProperty({\n    description: 'Subscription id',\n    type: Number,\n  })\n  @Expose()\n  subscriptionId: number;\n\n  @ApiProperty({\n    description: 'Subscription type',\n    enum: CloudSubscriptionType,\n  })\n  @Expose()\n  subscriptionType: CloudSubscriptionType;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: Number,\n  })\n  @Expose()\n  databaseId: number;\n\n  @ApiProperty({\n    description: 'Database name',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'Address your Redis Cloud database is available on',\n    type: String,\n  })\n  @Expose()\n  publicEndpoint: string;\n\n  @ApiProperty({\n    description: 'Database status',\n    enum: CloudDatabaseStatus,\n    default: CloudDatabaseStatus.Active,\n  })\n  @Expose()\n  status: CloudDatabaseStatus;\n\n  @ApiProperty({\n    description: 'Is ssl authentication enabled or not',\n    type: Boolean,\n  })\n  @Expose()\n  sslClientAuthentication: boolean;\n\n  @ApiProperty({\n    description: 'Information about the modules loaded to the database',\n    type: String,\n    isArray: true,\n  })\n  @Expose()\n  modules: string[];\n\n  @ApiProperty({\n    description: 'Additional database options',\n    type: Object,\n  })\n  @Expose()\n  options: any;\n\n  @Expose({ groups: ['security'] })\n  password?: string;\n\n  @Expose()\n  @Type(() => CloudDatabaseDetails)\n  cloudDetails?: CloudDatabaseDetails;\n\n  @ApiProperty({\n    description: 'Tags associated with the database.',\n    type: Tag,\n    isArray: true,\n  })\n  @Expose()\n  @IsArray()\n  @Type(() => Tag)\n  tags: Tag[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/models/index.ts",
    "content": "export * from './capi.interface';\nexport * from './cloud-database';\nexport * from './cloud-database-details';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/utils/cloud-data-converter.spec.ts",
    "content": "import { AdditionalRedisModuleName } from 'src/constants';\nimport { convertRedisCloudModuleName } from 'src/modules/cloud/database/utils/cloud-data-converter';\n\ndescribe('convertRedisCloudModuleName', () => {\n  it('should return exist module name', () => {\n    const input = 'RedisJSON';\n\n    const output = convertRedisCloudModuleName(input);\n\n    expect(output).toEqual(AdditionalRedisModuleName.RedisJSON);\n  });\n  it('should return non-exist module name', () => {\n    const input = 'RedisNewModule';\n\n    const output = convertRedisCloudModuleName(input);\n\n    expect(output).toEqual(input);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/utils/cloud-data-converter.ts",
    "content": "import { find, get, isArray } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  CloudDatabase,\n  CloudDatabaseMemoryStorage,\n  CloudDatabasePersistencePolicy,\n  CloudDatabaseProtocol,\n  ICloudCapiDatabase,\n  ICloudCapiSubscriptionDatabases,\n  ICloudCapiDatabaseTag,\n} from 'src/modules/cloud/database/models';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { REDIS_CLOUD_MODULES_NAMES } from 'src/constants';\nimport { Tag } from 'src/modules/tag/models/tag';\n\nexport function convertRedisCloudModuleName(name: string): string {\n  return REDIS_CLOUD_MODULES_NAMES[name] ?? name;\n}\n\nexport const parseCloudDatabaseCapiResponse = (\n  database: ICloudCapiDatabase,\n  tags: ICloudCapiDatabaseTag[],\n  subscriptionId: number,\n  subscriptionType: CloudSubscriptionType,\n  free?: boolean,\n): CloudDatabase => {\n  const {\n    databaseId,\n    name,\n    publicEndpoint,\n    status,\n    security,\n    planMemoryLimit,\n    memoryLimitMeasurementUnit,\n  } = database;\n\n  return plainToInstance(\n    CloudDatabase,\n    {\n      subscriptionId,\n      subscriptionType,\n      databaseId,\n      name,\n      publicEndpoint,\n      status,\n      password: security?.password,\n      sslClientAuthentication: security.sslClientAuthentication,\n      modules: database.modules.map((module) =>\n        convertRedisCloudModuleName(module.name),\n      ),\n      options: {\n        enabledDataPersistence:\n          database.dataPersistence !== CloudDatabasePersistencePolicy.None,\n        persistencePolicy: database.dataPersistence,\n        enabledRedisFlash:\n          database.memoryStorage === CloudDatabaseMemoryStorage.RamAndFlash,\n        enabledReplication: database.replication,\n        enabledBackup: !!database.periodicBackupPath,\n        enabledClustering: database.clustering.numberOfShards > 1,\n        isReplicaDestination: !!database.replicaOf,\n      },\n      tags: tags.map((tag) => plainToInstance(Tag, tag)),\n      cloudDetails: {\n        cloudId: databaseId,\n        subscriptionId,\n        subscriptionType,\n        planMemoryLimit,\n        memoryLimitMeasurementUnit,\n        free,\n      },\n    },\n    { groups: ['security'] },\n  );\n};\n\nexport const findReplicasForDatabase = (\n  databases: any[],\n  sourceDatabaseId: number,\n): any[] => {\n  const sourceDatabase = find(databases, {\n    databaseId: sourceDatabaseId,\n  });\n  if (!sourceDatabase) {\n    return [];\n  }\n  return databases.filter((replica): boolean => {\n    const endpoints = get(replica, ['replicaOf', 'endpoints']);\n    if (\n      replica.databaseId === sourceDatabaseId ||\n      !endpoints ||\n      !endpoints.length\n    ) {\n      return false;\n    }\n    return endpoints.some(\n      (endpoint: string): boolean =>\n        endpoint.includes(sourceDatabase.publicEndpoint) ||\n        endpoint.includes(sourceDatabase.privateEndpoint),\n    );\n  });\n};\n\nexport const parseCloudDatabasesCapiResponse = (\n  response: ICloudCapiSubscriptionDatabases,\n  subscriptionType: CloudSubscriptionType,\n  free?: boolean,\n): CloudDatabase[] => {\n  const subscription = isArray(response.subscription)\n    ? response.subscription[0]\n    : response.subscription;\n\n  const { subscriptionId, databases } = subscription;\n\n  let result: CloudDatabase[] = [];\n  databases.forEach((database): void => {\n    // We do not send the databases which have 'memcached' as their protocol.\n    if (\n      [CloudDatabaseProtocol.Redis, CloudDatabaseProtocol.Stack].includes(\n        database.protocol,\n      )\n    ) {\n      result.push(\n        parseCloudDatabaseCapiResponse(\n          database,\n          [],\n          subscriptionId,\n          subscriptionType,\n          free,\n        ),\n      );\n    }\n  });\n  result = result.map((database) => ({\n    ...database,\n    subscriptionType,\n    options: {\n      ...database.options,\n      isReplicaSource: !!findReplicasForDatabase(databases, database.databaseId)\n        .length,\n    },\n  }));\n  return result;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/database/utils/index.ts",
    "content": "export * from './cloud-data-converter';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.controller.ts",
    "content": "import {\n  Body,\n  Query,\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  Param,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { ApiExtraModels, ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { CloudJobService } from 'src/modules/cloud/job/cloud-job.service';\nimport { CreateDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-database.cloud-job.data.dto';\nimport { CreateCloudJobDto } from 'src/modules/cloud/job/dto/create.cloud-job.dto';\nimport { CloudJobInfo } from 'src/modules/cloud/job/models';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\n\n@ApiExtraModels(CreateDatabaseCloudJobDataDto)\n@ApiTags('Cloud Jobs')\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('cloud/me/jobs')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CloudJobController {\n  constructor(private readonly service: CloudJobService) {}\n\n  @Post('/')\n  @ApiEndpoint({\n    description: 'Create cloud job',\n    statusCode: 200,\n    responses: [{ type: CloudJobInfo }],\n  })\n  async createFreeDatabase(\n    @RequestSessionMetadata() sessionMetadata,\n    @Body() dto: CreateCloudJobDto,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<CloudJobInfo> {\n    return this.service.create(sessionMetadata, dto, utm);\n  }\n\n  @Get('/')\n  @ApiEndpoint({\n    description: 'Get list of user jobs',\n    statusCode: 200,\n    responses: [{ type: CloudJobInfo, isArray: true }],\n  })\n  async getUserJobsInfo(\n    @RequestSessionMetadata() sessionMetadata,\n  ): Promise<CloudJobInfo[]> {\n    return this.service.getUserJobsInfo(sessionMetadata);\n  }\n\n  @Get('/:id')\n  @ApiEndpoint({\n    description: 'Get user jobs',\n    statusCode: 200,\n    responses: [{ type: CloudJobInfo }],\n  })\n  async getJobInfo(\n    @RequestSessionMetadata() sessionMetadata,\n    @Param('id') id: string,\n  ): Promise<CloudJobInfo> {\n    return this.service.getJobInfo(sessionMetadata, id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.factory.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport {\n  CloudJob,\n  CreateFreeSubscriptionAndDatabaseCloudJob,\n  ImportFreeDatabaseCloudJob,\n} from 'src/modules/cloud/job/jobs';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CreateFreeDatabaseCloudJob } from 'src/modules/cloud/job/jobs/create-free-database.cloud-job';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { CloudJobUnsupportedException } from 'src/modules/cloud/job/exceptions';\nimport { SessionMetadata } from 'src/common/models';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { CloudSubscriptionApiService } from 'src/modules/cloud/subscription/cloud-subscription.api.service';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\nimport { DatabaseInfoService } from 'src/modules/database/database-info.service';\nimport { FeatureService } from 'src/modules/feature/feature.service';\n\n@Injectable()\nexport class CloudJobFactory {\n  constructor(\n    private readonly cloudDatabaseCapiService: CloudDatabaseCapiService,\n    private readonly cloudSubscriptionCapiService: CloudSubscriptionCapiService,\n    private readonly cloudTaskCapiService: CloudTaskCapiService,\n    private readonly cloudDatabaseAnalytics: CloudDatabaseAnalytics,\n    private readonly databaseService: DatabaseService,\n    private readonly databaseInfoService: DatabaseInfoService,\n    private readonly bulkImportService: BulkImportService,\n    private readonly cloudCapiKeyService: CloudCapiKeyService,\n    private readonly cloudSubscriptionApiService: CloudSubscriptionApiService,\n    private readonly featureService: FeatureService,\n  ) {}\n\n  async create(\n    name: CloudJobName,\n    data: any,\n    options: {\n      sessionMetadata: SessionMetadata;\n      utm?: CloudRequestUtm;\n      capiCredentials?: CloudCapiAuthDto;\n      stateCallbacks?: ((self: CloudJob) => any)[];\n    },\n  ): Promise<CloudJob> {\n    switch (name) {\n      case CloudJobName.CreateFreeSubscriptionAndDatabase:\n        return new CreateFreeSubscriptionAndDatabaseCloudJob(\n          {\n            abortController: new AbortController(),\n            ...options,\n          },\n          data,\n          {\n            cloudDatabaseCapiService: this.cloudDatabaseCapiService,\n            cloudSubscriptionCapiService: this.cloudSubscriptionCapiService,\n            cloudTaskCapiService: this.cloudTaskCapiService,\n            cloudDatabaseAnalytics: this.cloudDatabaseAnalytics,\n            databaseService: this.databaseService,\n            databaseInfoService: this.databaseInfoService,\n            bulkImportService: this.bulkImportService,\n            cloudCapiKeyService: this.cloudCapiKeyService,\n            cloudSubscriptionApiService: this.cloudSubscriptionApiService,\n            featureService: this.featureService,\n          },\n        );\n      case CloudJobName.CreateFreeDatabase:\n        return new CreateFreeDatabaseCloudJob(\n          {\n            abortController: new AbortController(),\n            ...options,\n          },\n          data,\n          {\n            cloudDatabaseCapiService: this.cloudDatabaseCapiService,\n            cloudSubscriptionCapiService: this.cloudSubscriptionCapiService,\n            cloudTaskCapiService: this.cloudTaskCapiService,\n            cloudDatabaseAnalytics: this.cloudDatabaseAnalytics,\n            databaseService: this.databaseService,\n            databaseInfoService: this.databaseInfoService,\n            bulkImportService: this.bulkImportService,\n            cloudCapiKeyService: this.cloudCapiKeyService,\n            featureService: this.featureService,\n          },\n        );\n      case CloudJobName.ImportFreeDatabase:\n        return new ImportFreeDatabaseCloudJob(\n          {\n            abortController: new AbortController(),\n            ...options,\n          },\n          data,\n          {\n            cloudDatabaseCapiService: this.cloudDatabaseCapiService,\n            cloudSubscriptionCapiService: this.cloudSubscriptionCapiService,\n            cloudTaskCapiService: this.cloudTaskCapiService,\n            cloudDatabaseAnalytics: this.cloudDatabaseAnalytics,\n            databaseService: this.databaseService,\n            cloudCapiKeyService: this.cloudCapiKeyService,\n            featureService: this.featureService,\n          },\n        );\n      default:\n        throw new CloudJobUnsupportedException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.gateway.ts",
    "content": "import { Socket, Server } from 'socket.io';\nimport {\n  ConnectedSocket,\n  SubscribeMessage,\n  WebSocketGateway,\n  WebSocketServer,\n  WsException,\n} from '@nestjs/websockets';\nimport { Body, Logger, ValidationPipe } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { CloudJobEvents } from 'src/modules/cloud/common/constants';\nimport { CloudJobService } from 'src/modules/cloud/job/cloud-job.service';\nimport { MonitorCloudJobDto } from 'src/modules/cloud/job/dto/monitor.cloud-job.dto';\nimport { Validator } from 'class-validator';\nimport { plainToInstance } from 'class-transformer';\nimport { CloudJobInfo } from 'src/modules/cloud/job/models';\nimport { SessionMetadata } from 'src/common/models';\nimport { WSSessionMetadata } from 'src/modules/auth/session-metadata/decorators/ws-session-metadata.decorator';\n\nconst SOCKETS_CONFIG = config.get('sockets') as Config['sockets'];\n\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n})\nexport class CloudJobGateway {\n  @WebSocketServer() wss: Server;\n\n  private readonly validator = new Validator();\n\n  private exceptionFactory = new ValidationPipe().createExceptionFactory();\n\n  private logger: Logger = new Logger('CloudJobGateway');\n\n  constructor(private readonly cloudJobService: CloudJobService) {}\n\n  @SubscribeMessage(CloudJobEvents.Monitor)\n  async monitor(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @ConnectedSocket() client: Socket,\n    @Body() data: MonitorCloudJobDto,\n  ): Promise<CloudJobInfo> {\n    try {\n      const dto = plainToInstance(MonitorCloudJobDto, data);\n\n      const errors = await this.validator.validate(dto, { whitelist: true });\n\n      if (errors?.length) {\n        throw this.exceptionFactory(errors);\n      }\n\n      return await this.cloudJobService.monitorJob(\n        sessionMetadata,\n        dto,\n        client,\n      );\n    } catch (error) {\n      this.logger.error('Unable to add listener', error);\n      throw new WsException(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudTaskModule } from 'src/modules/cloud/task/cloud-task.module';\nimport { CloudJobController } from 'src/modules/cloud/job/cloud-job.controller';\nimport { CloudJobService } from 'src/modules/cloud/job/cloud-job.service';\nimport { CloudSubscriptionModule } from 'src/modules/cloud/subscription/cloud-subscription.module';\nimport { CloudDatabaseModule } from 'src/modules/cloud/database/cloud-database.module';\nimport { CloudJobFactory } from 'src/modules/cloud/job/cloud-job.factory';\nimport { CloudJobProvider } from 'src/modules/cloud/job/cloud-job.provider';\nimport { CloudJobGateway } from 'src/modules/cloud/job/cloud-job.gateway';\nimport { CloudCapiKeyModule } from 'src/modules/cloud/capi-key/cloud-capi-key.module';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\nimport { BulkActionsAnalytics } from 'src/modules/bulk-actions/bulk-actions.analytics';\n\n@Module({\n  imports: [\n    CloudTaskModule,\n    CloudSubscriptionModule,\n    CloudDatabaseModule,\n    CloudCapiKeyModule,\n  ],\n  providers: [\n    CloudJobService,\n    CloudJobFactory,\n    CloudJobProvider,\n    CloudJobGateway,\n    BulkImportService,\n    BulkActionsAnalytics,\n  ],\n  controllers: [CloudJobController],\n})\nexport class CloudJobModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.provider.ts",
    "content": "import { filter } from 'lodash';\nimport { CloudJob } from 'src/modules/cloud/job/jobs';\nimport { SessionMetadata } from 'src/common/models';\nimport { CreateCloudJobDto } from 'src/modules/cloud/job/dto/create.cloud-job.dto';\nimport { CloudJobInfo, CloudJobRunMode } from 'src/modules/cloud/job/models';\nimport { CloudJobFactory } from 'src/modules/cloud/job/cloud-job.factory';\nimport { wrapHttpError } from 'src/common/utils';\nimport { Injectable } from '@nestjs/common';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\n\n@Injectable()\nexport class CloudJobProvider {\n  private jobs: Map<string, CloudJob> = new Map();\n\n  constructor(private readonly cloudJobFactory: CloudJobFactory) {}\n\n  async addJob(\n    sessionMetadata: SessionMetadata,\n    dto: CreateCloudJobDto,\n    utm: CloudRequestUtm,\n  ): Promise<CloudJobInfo> {\n    try {\n      const job = await this.cloudJobFactory.create(dto.name, dto.data || {}, {\n        sessionMetadata,\n        utm,\n      });\n\n      // tmp: clear all jobs due to current requirements (1 at time)\n      if (this.jobs.size) {\n        this.jobs.forEach((ongoingJob) => {\n          ongoingJob.abort('Another job was added');\n        });\n        this.jobs.clear();\n      }\n\n      this.jobs.set(job.id, job);\n\n      if (dto.runMode === CloudJobRunMode.Async) {\n        job.run(sessionMetadata).catch(() => {});\n\n        return job.getState();\n      }\n\n      await job.run(sessionMetadata);\n      return job.getState();\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  async get(id: string): Promise<CloudJob> {\n    return this.jobs.get(id);\n  }\n\n  async findUserJobs(sessionMetadata: SessionMetadata): Promise<CloudJob[]> {\n    return filter(\n      [...this.jobs.values()],\n      (job: CloudJob) =>\n        job.options?.sessionMetadata?.userId === sessionMetadata.userId,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { ForbiddenException } from '@nestjs/common';\nimport {\n  mockUtm,\n  mockSessionMetadata,\n  mockCreateDatabaseCloudJobDataDto,\n  mockCloudJobProvider,\n  mockCloudJobInfo,\n  MockType,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudJobNotFoundException } from 'src/modules/cloud/job/exceptions';\nimport { CloudJobProvider } from 'src/modules/cloud/job/cloud-job.provider';\nimport { CloudJobService } from './cloud-job.service';\n\ndescribe('CloudJobService', () => {\n  let service: CloudJobService;\n  let cloudJobProvider: MockType<CloudJobProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudJobService,\n        {\n          provide: CloudJobProvider,\n          useFactory: mockCloudJobProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudJobService);\n    cloudJobProvider = module.get(CloudJobProvider);\n  });\n\n  describe('create', () => {\n    it('successfully create cloud job', async () => {\n      expect(\n        await service.create(\n          mockSessionMetadata,\n          mockCreateDatabaseCloudJobDataDto,\n          mockUtm,\n        ),\n      ).toEqual(mockCloudJobInfo);\n    });\n    it('should throw CloudApiUnauthorizedException exception', async () => {\n      cloudJobProvider.addJob.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.create(\n          mockSessionMetadata,\n          mockCreateDatabaseCloudJobDataDto,\n          mockUtm,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n\n  describe('getUserJobsInfo', () => {\n    it('successfully create cloud job', async () => {\n      expect(await service.getUserJobsInfo(mockSessionMetadata)).toEqual([\n        mockCloudJobInfo,\n      ]);\n    });\n    it('should throw CloudApiUnauthorizedException exception', async () => {\n      cloudJobProvider.findUserJobs.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getUserJobsInfo(mockSessionMetadata),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('get', () => {\n    it('should throw CloudJobNotFoundException exception', async () => {\n      cloudJobProvider.get = jest.fn().mockReturnValue(null);\n      await expect(service.get(mockSessionMetadata, 'id')).rejects.toThrow(\n        CloudJobNotFoundException,\n      );\n    });\n    it('should throw CloudApiUnauthorizedException exception', async () => {\n      cloudJobProvider.get.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(service.get(mockSessionMetadata, 'id')).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n    it('should throw ForbiddenException exception', async () => {\n      await expect(service.get(mockSessionMetadata, 'id')).rejects.toThrow(\n        ForbiddenException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/cloud-job.service.ts",
    "content": "import { ForbiddenException, Injectable } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\nimport { CreateCloudJobDto } from 'src/modules/cloud/job/dto/create.cloud-job.dto';\nimport { MonitorCloudJobDto } from 'src/modules/cloud/job/dto/monitor.cloud-job.dto';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudJobProvider } from 'src/modules/cloud/job/cloud-job.provider';\nimport { CloudJobInfo } from 'src/modules/cloud/job/models';\nimport { Socket } from 'socket.io';\nimport { CloudJobNotFoundException } from 'src/modules/cloud/job/exceptions';\nimport { CloudJobEvents } from 'src/modules/cloud/common/constants';\nimport { CloudJob } from 'src/modules/cloud/job/jobs';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\n\n@Injectable()\nexport class CloudJobService {\n  constructor(private readonly cloudJobProvider: CloudJobProvider) {}\n\n  /**\n   * Create cloud job\n   * @param sessionMetadata\n   * @param dto\n   * @param utm\n   */\n  async create(\n    sessionMetadata: SessionMetadata,\n    dto: CreateCloudJobDto,\n    utm: CloudRequestUtm,\n  ): Promise<CloudJobInfo> {\n    try {\n      return await this.cloudJobProvider.addJob(sessionMetadata, dto, utm);\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get list of current user jobs infos\n   * @param sessionMetadata\n   */\n  async getUserJobsInfo(\n    sessionMetadata: SessionMetadata,\n  ): Promise<CloudJobInfo[]> {\n    try {\n      const jobs = await this.cloudJobProvider.findUserJobs(sessionMetadata);\n\n      return jobs.map((job) => job.getState());\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get user job\n   * @param sessionMetadata\n   * @param id\n   */\n  async get(sessionMetadata: SessionMetadata, id: string): Promise<CloudJob> {\n    try {\n      const job = await this.cloudJobProvider.get(id);\n\n      if (!job) {\n        throw new CloudJobNotFoundException();\n      }\n\n      if (job.options?.sessionMetadata?.userId !== sessionMetadata.userId) {\n        throw new ForbiddenException(\"This job doesn't belong to the user\");\n      }\n\n      return job;\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get user job info\n   * @param sessionMetadata\n   * @param id\n   */\n  async getJobInfo(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<CloudJobInfo> {\n    try {\n      const job = await this.get(sessionMetadata, id);\n\n      return job.getState();\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  async monitorJob(\n    sessionMetadata: SessionMetadata,\n    dto: MonitorCloudJobDto,\n    client: Socket,\n  ): Promise<CloudJobInfo> {\n    const job = await this.get(sessionMetadata, dto.jobId);\n\n    job.addStateCallback(async (cloudJob) => {\n      client.emit(CloudJobEvents.Monitor, cloudJob.getState());\n    });\n\n    return job.getState();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/constants/index.ts",
    "content": "export enum CloudJobName {\n  CreateFreeSubscriptionAndDatabase = 'CREATE_FREE_SUBSCRIPTION_AND_DATABASE',\n  CreateFreeDatabase = 'CREATE_FREE_DATABASE',\n  CreateFreeSubscription = 'CREATE_FREE_SUBSCRIPTION',\n  ImportFreeDatabase = 'IMPORT_FREE_DATABASE',\n  WaitForActiveDatabase = 'WAIT_FOR_ACTIVE_DATABASE',\n  WaitForActiveSubscription = 'WAIT_FOR_ACTIVE_SUBSCRIPTION',\n  WaitForTask = 'WAIT_FOR_TASK',\n  Unknown = 'UNKNOWN',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/dto/create-database.cloud-job.data.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber } from 'class-validator';\n\nexport class CreateDatabaseCloudJobDataDto {\n  @ApiProperty({\n    description: 'Subscription id for create a database.',\n    type: Number,\n  })\n  @IsNumber()\n  @IsNotEmpty()\n  subscriptionId: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/dto/create-subscription-and-database.cloud-job.data.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsBoolean,\n  IsNotEmpty,\n  IsNumber,\n  IsOptional,\n  ValidateIf,\n} from 'class-validator';\n\nexport class CreateSubscriptionAndDatabaseCloudJobDataDto {\n  @ApiProperty({\n    description: 'Plan id for create a subscription.',\n    type: Number,\n  })\n  @ValidateIf((object) => !object.isRecommendedSettings)\n  @IsNumber()\n  @IsNotEmpty()\n  planId: number;\n\n  @ApiPropertyOptional({\n    description: 'Use recommended settings',\n    type: Boolean,\n  })\n  @IsBoolean()\n  @IsOptional()\n  isRecommendedSettings?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/dto/create.cloud-job.dto.ts",
    "content": "import {\n  ApiExtraModels,\n  ApiProperty,\n  ApiPropertyOptional,\n  getSchemaPath,\n} from '@nestjs/swagger';\nimport {\n  IsEnum,\n  IsNotEmpty,\n  IsNotEmptyObject,\n  IsOptional,\n  ValidateNested,\n} from 'class-validator';\nimport { Expose, Type } from 'class-transformer';\nimport { cloudJobDataTransformer } from 'src/modules/cloud/job/transformers/cloud-job-data.transformer';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudJobRunMode } from 'src/modules/cloud/job/models';\nimport { CreateDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-database.cloud-job.data.dto';\nimport { CreateSubscriptionAndDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-subscription-and-database.cloud-job.data.dto';\nimport { ImportDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/import-database.cloud-job.data.dto';\n\n@ApiExtraModels(\n  CreateDatabaseCloudJobDataDto,\n  CreateSubscriptionAndDatabaseCloudJobDataDto,\n  ImportDatabaseCloudJobDataDto,\n)\nexport class CreateCloudJobDto {\n  @ApiProperty({\n    description: 'Job name to create',\n    enum: CloudJobName,\n  })\n  @IsEnum(CloudJobName)\n  @IsNotEmpty()\n  name: CloudJobName;\n\n  @ApiProperty({\n    description: 'Mod in which to run the job.',\n    enum: CloudJobRunMode,\n  })\n  @IsOptional()\n  @Expose()\n  @IsEnum(CloudJobRunMode)\n  @IsNotEmpty()\n  runMode: CloudJobRunMode;\n\n  @ApiPropertyOptional({\n    description: 'Any data for create a job.',\n    oneOf: [\n      { $ref: getSchemaPath(CreateDatabaseCloudJobDataDto) },\n      { $ref: getSchemaPath(CreateSubscriptionAndDatabaseCloudJobDataDto) },\n      { $ref: getSchemaPath(ImportDatabaseCloudJobDataDto) },\n    ],\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(cloudJobDataTransformer)\n  @ValidateNested()\n  data?:\n    | CreateDatabaseCloudJobDataDto\n    | CreateSubscriptionAndDatabaseCloudJobDataDto\n    | ImportDatabaseCloudJobDataDto;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/dto/import-database.cloud-job.data.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class ImportDatabaseCloudJobDataDto {\n  @ApiProperty({\n    description: 'Subscription id of database',\n    type: Number,\n  })\n  @IsNumber()\n  @IsNotEmpty()\n  subscriptionId: number;\n\n  @ApiProperty({\n    description: 'Database id to import',\n    type: Number,\n  })\n  @IsNumber()\n  @IsNotEmpty()\n  databaseId: number;\n\n  @ApiProperty({\n    description: 'Subscription region',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  region: string;\n\n  @ApiProperty({\n    description: 'Subscription provider',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  provider: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/dto/monitor.cloud-job.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class MonitorCloudJobDto {\n  @ApiProperty({\n    description: 'Task id to monitor',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  jobId: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-database-already-exists-free.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudDatabaseAlreadyExistsFreeException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_DATABASE_ALREADY_EXISTS_FREE,\n    options?: HttpExceptionOptions & {\n      subscriptionId?: number;\n      databaseId?: number;\n      region?: string;\n      provider?: string;\n    },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.CONFLICT,\n      error: 'CloudDatabaseAlreadyExistsFree',\n      errorCode: CustomErrorCodes.CloudDatabaseAlreadyExistsFree,\n      resource: {\n        subscriptionId: options?.subscriptionId,\n        databaseId: options?.databaseId,\n        region: options?.region,\n        provider: options?.provider,\n      },\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-database-endpoint-invalid.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudDatabaseEndpointInvalidException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_DATABASE_ENDPOINT_INVALID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudDatabaseEndpointInvalid',\n      errorCode: CustomErrorCodes.CloudDatabaseEndpointInvalid,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-database-import-forbidden.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudDatabaseImportForbiddenException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_DATABASE_IMPORT_FORBIDDEN,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.FORBIDDEN,\n      error: 'CloudDatabaseImportForbidden',\n      errorCode: CustomErrorCodes.CloudDatabaseImportForbidden,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-database-in-failed-state.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudDatabaseInFailedStateException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_DATABASE_IN_FAILED_STATE,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudDatabaseInFailedState',\n      errorCode: CustomErrorCodes.CloudDatabaseIsInTheFailedState,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-database-in-unexpected-state.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudDatabaseInUnexpectedStateException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_DATABASE_IN_UNEXPECTED_STATE,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_IMPLEMENTED,\n      error: 'CloudDatabaseInUnexpectedState',\n      errorCode: CustomErrorCodes.CloudDatabaseIsInUnexpectedState,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-job-aborted.exception.ts",
    "content": "import { HttpException, HttpExceptionOptions } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudJobAbortedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_JOB_ABORTED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: 499,\n      error: 'CloudJobAborted',\n      errorCode: CustomErrorCodes.CloudJobAborted,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-job-not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudJobNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_JOB_NOT_FOUND,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'CloudJobNotFound',\n      errorCode: CustomErrorCodes.CloudJobNotFound,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-job-unexpected-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudJobUnexpectedErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_JOB_UNEXPECTED_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudJobUnexpectedError',\n      errorCode: CustomErrorCodes.CloudJobUnexpectedError,\n      // cause: options?.cause, // todo: check\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-job-unsupported.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudJobUnsupportedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_JOB_UNSUPPORTED,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_IMPLEMENTED,\n      error: 'CloudJobUnsupported',\n      errorCode: CustomErrorCodes.CloudJobUnsupported,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-job.error.handler.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { CloudJobUnexpectedErrorException } from 'src/modules/cloud/job/exceptions/cloud-job-unexpected-error.exception';\n\nexport const wrapCloudJobError = (error: Error, message?: string) => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  if (error instanceof Error) {\n    return new CloudJobUnexpectedErrorException(error.message || message, {\n      cause: error,\n    });\n  }\n\n  return new CloudJobUnexpectedErrorException(message);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-plan-not-found-free.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudPlanNotFoundFreeException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_PLAN_NOT_FOUND_FREE,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'CloudPlanNotFoundFree',\n      errorCode: CustomErrorCodes.CloudPlanUnableToFindFree,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-subscription-already-exists-free.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudSubscriptionAlreadyExistsFreeException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_SUBSCRIPTION_ALREADY_EXISTS_FREE,\n    options?: HttpExceptionOptions & { subscriptionId?: number },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.CONFLICT,\n      error: 'CloudSubscriptionAlreadyExistsFree',\n      errorCode: CustomErrorCodes.CloudSubscriptionAlreadyExistsFree,\n      resource: {\n        subscriptionId: options?.subscriptionId,\n      },\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-subscription-in-failed-state.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudSubscriptionInFailedStateException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_SUBSCRIPTION_IN_FAILED_STATE,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudSubscriptionInFailedState',\n      errorCode: CustomErrorCodes.CloudSubscriptionIsInTheFailedState,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-subscription-in-unexpected-state.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudSubscriptionInUnexpectedStateException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_SUBSCRIPTION_IN_UNEXPECTED_STATE,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudSubscriptionInUnexpectedState',\n      errorCode: CustomErrorCodes.CloudSubscriptionIsInUnexpectedState,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-subscription-unable-to-determine.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudSubscriptionUnableToDetermineException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_SUBSCRIPTION_UNABLE_TO_DETERMINE,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'CloudSubscriptionUnableToDetermine',\n      errorCode: CustomErrorCodes.CloudSubscriptionUnableToDetermine,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-task-no-resource-id.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudTaskNoResourceIdException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_TASK_NO_RESOURCE_ID,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'CloudTaskNoResourceId',\n      errorCode: CustomErrorCodes.CloudTaskNoResourceId,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-task-not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudTaskNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_TASK_NOT_FOUND,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'CloudTaskNotFound',\n      errorCode: CustomErrorCodes.CloudTaskNotFound,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/cloud-task-processing-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class CloudTaskProcessingErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.CLOUD_TASK_PROCESSING_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'CloudTaskProcessingError',\n      errorCode: CustomErrorCodes.CloudTaskProcessingError,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/exceptions/index.ts",
    "content": "export * from './cloud-database-already-exists-free.exception';\nexport * from './cloud-database-endpoint-invalid.exception';\nexport * from './cloud-database-import-forbidden.exception';\nexport * from './cloud-database-in-failed-state.exception';\nexport * from './cloud-database-in-unexpected-state.exception';\nexport * from './cloud-job.error.handler';\nexport * from './cloud-job-aborted.exception';\nexport * from './cloud-job-not-found.exception';\nexport * from './cloud-job-unexpected-error.exception';\nexport * from './cloud-job-unsupported.exception';\nexport * from './cloud-plan-not-found-free.exception';\nexport * from './cloud-subscription-in-failed-state.exception';\nexport * from './cloud-subscription-in-unexpected-state.exception';\nexport * from './cloud-subscription-unable-to-determine.exception';\nexport * from './cloud-task-no-resource-id.exception';\nexport * from './cloud-task-not-found.exception';\nexport * from './cloud-task-processing-error.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/cloud-job.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { BadRequestException } from '@nestjs/common';\nimport {\n  mockDatabaseService,\n  mockRedisClientInstance,\n  mockClientMetadata,\n  mockStandaloneRedisClient,\n  mockDatabaseClientFactory,\n} from 'src/__mocks__';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('CloudJob', () => {\n  let consumerInstance;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    consumerInstance = await module.get<DatabaseClientFactory>(\n      DatabaseClientFactory,\n    );\n  });\n\n  describe('getRedisClient', () => {\n    beforeEach(() => {\n      consumerInstance.createClient = jest.fn();\n    });\n    it('create new redis client', async () => {\n      consumerInstance.getOrCreateClient.mockResolvedValue(\n        mockStandaloneRedisClient,\n      );\n\n      const result =\n        await consumerInstance.getOrCreateClient(mockClientMetadata);\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(consumerInstance.getOrCreateClient).toHaveBeenCalled();\n    });\n    it('existing client has connection', async () => {\n      const result =\n        await consumerInstance.getOrCreateClient(mockClientMetadata);\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(consumerInstance.createClient).not.toHaveBeenCalled();\n    });\n    it('existing client has no connection', async () => {\n      consumerInstance.getOrCreateClient.mockResolvedValue(\n        mockStandaloneRedisClient,\n      );\n\n      const result =\n        await consumerInstance.getOrCreateClient(mockClientMetadata);\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(consumerInstance.getOrCreateClient).toHaveBeenCalled();\n    });\n    it('select redis database by number', async () => {\n      await expect(\n        consumerInstance.getOrCreateClient({\n          ...mockClientMetadata,\n        }),\n      ).resolves.not.toThrow();\n\n      expect(consumerInstance.createClient).not.toHaveBeenCalled();\n    });\n    it(\"can't create redis client\", async () => {\n      const error = new BadRequestException(\n        ' Could not connect to localhost, please check the connection details.',\n      );\n      consumerInstance.getOrCreateClient.mockRejectedValue(error);\n\n      await expect(\n        consumerInstance.getOrCreateClient({\n          ...mockClientMetadata,\n          dbNumber: 1,\n        }),\n      ).rejects.toThrow(error);\n    });\n    // it('should throw error if autoConnection disabled', async () => {\n    //   redisService.getClientInstance.mockReturnValue(null);\n    //   // @ts-ignore\n    //   class Tool extends RedisConsumerAbstractService {\n    //     constructor() {\n    //       super(\n    //         ClientContext.CLI,\n    //         redisService,\n    //         redisConnectionFactory,\n    //         instancesBusinessService,\n    //         { enableAutoConnection: false },\n    //       );\n    //     }\n    //   }\n    //\n    //   await expect(new Tool().getRedisClient(mockClientMetadata))\n    //     .rejects.toThrow(new ClientNotFoundErrorException());\n    // });\n  });\n\n  describe('createNewClient', () => {\n    it('create new redis client', async () => {\n      consumerInstance.createClient.mockResolvedValue(\n        mockStandaloneRedisClient,\n      );\n\n      const result = await consumerInstance.createClient(\n        mockRedisClientInstance.clientMetadata,\n      );\n      expect(result).toEqual(mockStandaloneRedisClient);\n    });\n    it(\"can't create redis client\", async () => {\n      const error = new BadRequestException(\n        ' Could not connect to localhost, please check the connection details.',\n      );\n      consumerInstance.createClient.mockRejectedValue(error);\n\n      await expect(\n        consumerInstance.createClient(mockRedisClientInstance.clientMetadata),\n      ).rejects.toThrow(error);\n    });\n  });\n\n  describe('execPipelineFromClient', () => {\n    const client = mockStandaloneRedisClient;\n    const mockPipelineCommands = ['module list', 'keys', '*'];\n    it('succeed to execute pipeline from redis client', async () => {\n      client.sendPipeline.mockResolvedValue([null, []]);\n\n      await expect(\n        client.sendPipeline(client, mockPipelineCommands),\n      ).resolves.not.toThrow();\n      expect(client.sendPipeline).toHaveBeenCalledWith(\n        client,\n        mockPipelineCommands,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/cloud-job.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport config from 'src/utils/config';\nimport {\n  CloudJobInfo,\n  CloudJobStatus,\n  CloudJobStep,\n} from 'src/modules/cloud/job/models/cloud-job-info';\nimport { HttpException, Logger } from '@nestjs/common';\nimport {\n  CloudJobAbortedException,\n  wrapCloudJobError,\n} from 'src/modules/cloud/job/exceptions';\nimport { SessionMetadata } from 'src/common/models';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport { debounce } from 'lodash';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { ClassConstructor } from 'class-transformer/types/interfaces';\n\nconst cloudConfig = config.get('cloud');\n\nexport class CloudJobOptions {\n  abortController: AbortController;\n\n  sessionMetadata: SessionMetadata;\n\n  capiCredentials?: CloudCapiAuthDto;\n\n  utm?: CloudRequestUtm;\n\n  stateCallbacks?: ((self: CloudJob) => any)[] = [];\n\n  name?: CloudJobName;\n\n  child?: boolean;\n}\n\nexport abstract class CloudJob {\n  protected logger = new Logger(this.constructor.name);\n\n  public id = uuidv4();\n\n  protected name = CloudJobName.Unknown;\n\n  protected status = CloudJobStatus.Initializing;\n\n  protected step = CloudJobStep.Credentials;\n\n  protected error?: HttpException;\n\n  protected child?: CloudJob;\n\n  protected result?: any;\n\n  public options: CloudJobOptions;\n\n  protected dependencies: any;\n\n  private readonly debounce: any;\n\n  protected constructor(options: CloudJobOptions) {\n    this.options = options;\n\n    if (!this.options.stateCallbacks) {\n      this.options.stateCallbacks = [];\n    }\n\n    this.debounce = debounce(\n      () => {\n        this.triggerChangeStateCallbacks();\n      },\n      1_000,\n      {\n        maxWait: 2_000,\n      },\n    );\n  }\n\n  private triggerChangeStateCallbacks() {\n    try {\n      (this.options?.stateCallbacks || []).forEach((cb) => {\n        cb?.(this)?.catch?.(() => {});\n      });\n    } catch (e) {\n      // silently ignore callback\n    }\n  }\n\n  public async run(sessionMetadata: SessionMetadata): Promise<void> {\n    try {\n      this.changeState({\n        status: CloudJobStatus.Running,\n      });\n\n      if (!this.options.capiCredentials) {\n        this.logger.debug('Generating capi credentials');\n\n        this.changeState({ step: CloudJobStep.Credentials });\n\n        this.options.capiCredentials =\n          await this.dependencies.cloudCapiKeyService.getCapiCredentials(\n            this.options.sessionMetadata,\n            this.options.utm,\n          );\n      }\n\n      return await this.iteration(sessionMetadata);\n    } catch (e) {\n      this.logger.error('Cloud job failed', e);\n\n      const error = wrapCloudJobError(\n        await this.dependencies.cloudCapiKeyService.handleCapiKeyUnauthorizedError(\n          e,\n          this.options.sessionMetadata,\n        ),\n      );\n\n      this.changeState({\n        status: CloudJobStatus.Failed,\n        error,\n      });\n\n      throw error;\n    }\n  }\n\n  public abort(reason?: string) {\n    // @ts-ignore\n    this.options?.abortController?.abort(reason);\n  }\n\n  public getState(): CloudJobInfo {\n    return {\n      id: this.id,\n      name: this.options?.name || this.name,\n      status: this.status,\n      result: this.result,\n      error: this.error\n        ? wrapCloudJobError(this.error).getResponse()\n        : undefined,\n      child: this.child?.getState(),\n      step: this.step,\n    };\n  }\n\n  public createChildJob<T>(\n    TargetJob: ClassConstructor<T>,\n    data: {},\n    options = {},\n  ): T {\n    return new TargetJob(\n      {\n        ...this.options,\n        ...options,\n        stateCallbacks: [() => this.changeState()],\n        child: true,\n      },\n      data,\n      this.dependencies,\n    );\n  }\n\n  public async runChildJob(\n    sessionMetadata: SessionMetadata,\n    TargetJob: ClassConstructor<CloudJob>,\n    data: {},\n    options: CloudJobOptions,\n  ): Promise<any> {\n    const child = this.createChildJob(TargetJob, data, options);\n\n    this.changeState({ child });\n\n    const result = await child.run(sessionMetadata);\n\n    this.changeState({ child: null });\n\n    return result;\n  }\n\n  public addStateCallback(callback: (self: CloudJob) => any) {\n    this.options.stateCallbacks.push(callback);\n  }\n\n  protected changeState(state = {}) {\n    Object.entries(state).forEach(([key, value]) => {\n      this[key] = value;\n    });\n\n    if (this.options.child) {\n      this.triggerChangeStateCallbacks();\n    } else {\n      this.debounce();\n    }\n  }\n\n  protected checkSignal() {\n    if (this.options?.abortController?.signal?.aborted === true) {\n      // @ts-ignore\n      const reason = this.abortController?.signal?.reason;\n      this.logger.error(`Job ${this.name} aborted with reason: ${reason}`);\n      throw new CloudJobAbortedException(undefined, { cause: reason });\n    }\n  }\n\n  protected runNextIteration(\n    sessionMetadata: SessionMetadata,\n    timeout = cloudConfig.jobIterationInterval,\n  ): Promise<any> {\n    return new Promise((res, rej) => {\n      setTimeout(() => {\n        this.iteration(sessionMetadata).then(res).catch(rej);\n      }, timeout);\n    });\n  }\n\n  protected abstract iteration(sessionMetadata: SessionMetadata): Promise<any>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/create-free-database.cloud-job.ts",
    "content": "import {\n  CloudJob,\n  CloudJobOptions,\n  WaitForTaskCloudJob,\n} from 'src/modules/cloud/job/jobs';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport {\n  CloudSubscription,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CloudDatabase } from 'src/modules/cloud/database/models';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { WaitForActiveDatabaseCloudJob } from 'src/modules/cloud/job/jobs/wait-for-active-database.cloud-job';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudJobStatus, CloudJobStep } from 'src/modules/cloud/job/models';\nimport {\n  CloudDatabaseEndpointInvalidException,\n  CloudDatabaseImportForbiddenException,\n  CloudJobUnexpectedErrorException,\n  CloudTaskNoResourceIdException,\n} from 'src/modules/cloud/job/exceptions';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { Database } from 'src/modules/database/models/database';\nimport config from 'src/utils/config';\nimport { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\nimport { ClientContext, SessionMetadata } from 'src/common/models';\nimport { DatabaseInfoService } from 'src/modules/database/database-info.service';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\n\nconst cloudConfig = config.get('cloud');\n\nexport class CreateFreeDatabaseCloudJob extends CloudJob {\n  protected name = CloudJobName.CreateFreeDatabase;\n\n  constructor(\n    readonly options: CloudJobOptions,\n    private readonly data: {\n      subscriptionId: number;\n    },\n    protected readonly dependencies: {\n      cloudDatabaseCapiService: CloudDatabaseCapiService;\n      cloudSubscriptionCapiService: CloudSubscriptionCapiService;\n      cloudTaskCapiService: CloudTaskCapiService;\n      cloudDatabaseAnalytics: CloudDatabaseAnalytics;\n      databaseService: DatabaseService;\n      databaseInfoService: DatabaseInfoService;\n      bulkImportService: BulkImportService;\n      cloudCapiKeyService: CloudCapiKeyService;\n      featureService: FeatureService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(sessionMetadata: SessionMetadata): Promise<Database> {\n    let freeSubscription: CloudSubscription;\n    try {\n      this.logger.debug('Create free database');\n\n      this.checkSignal();\n\n      this.changeState({ step: CloudJobStep.Database });\n\n      this.logger.debug('Getting subscription metadata');\n\n      freeSubscription =\n        await this.dependencies.cloudSubscriptionCapiService.getSubscription(\n          this.options.capiCredentials,\n          this.data.subscriptionId,\n          CloudSubscriptionType.Fixed,\n        );\n      let cloudDatabase: CloudDatabase;\n\n      let createFreeDatabaseTask =\n        await this.dependencies.cloudDatabaseCapiService.createFreeDatabase(\n          this.options.capiCredentials,\n          {\n            subscriptionId: freeSubscription.id,\n            subscriptionType: freeSubscription.type,\n          },\n        );\n\n      this.checkSignal();\n\n      createFreeDatabaseTask = await this.runChildJob(\n        sessionMetadata,\n        WaitForTaskCloudJob,\n        {\n          taskId: createFreeDatabaseTask.taskId,\n        },\n        this.options,\n      );\n\n      const freeDatabaseId = createFreeDatabaseTask?.response?.resourceId;\n\n      if (!freeDatabaseId) {\n        throw new CloudTaskNoResourceIdException();\n      }\n\n      cloudDatabase = {\n        databaseId: freeDatabaseId,\n      } as CloudDatabase;\n\n      if (!cloudDatabase) {\n        throw new CloudJobUnexpectedErrorException(\n          'Unable to create free cloud database',\n        );\n      }\n\n      this.checkSignal();\n\n      cloudDatabase = await this.runChildJob(\n        sessionMetadata,\n        WaitForActiveDatabaseCloudJob,\n        {\n          databaseId: cloudDatabase.databaseId,\n          subscriptionId: this.data.subscriptionId,\n          subscriptionType: CloudSubscriptionType.Fixed,\n        },\n        this.options,\n      );\n\n      this.checkSignal();\n\n      const isDatabaseManagementEnabled =\n        await this.dependencies.featureService.isFeatureEnabled(\n          sessionMetadata,\n          KnownFeatures.DatabaseManagement,\n        );\n\n      if (!isDatabaseManagementEnabled) {\n        throw new CloudDatabaseImportForbiddenException();\n      }\n\n      const { publicEndpoint, name, password } = cloudDatabase;\n\n      if (!publicEndpoint) {\n        throw new CloudDatabaseEndpointInvalidException();\n      }\n\n      const [host, port] = publicEndpoint.split(':');\n\n      const database = await this.dependencies.databaseService.create(\n        this.options.sessionMetadata,\n        {\n          host,\n          port: parseInt(port, 10),\n          name,\n          nameFromProvider: name,\n          password,\n          provider: HostingProvider.REDIS_CLOUD,\n          cloudDetails: {\n            ...cloudDatabase?.cloudDetails,\n            free: true,\n          },\n          timeout: cloudConfig.cloudDatabaseConnectionTimeout,\n        },\n      );\n\n      try {\n        const clientMetadata = {\n          databaseId: database.id,\n          sessionMetadata: this.options.sessionMetadata,\n          context: ClientContext.Common,\n          db: database.db,\n        };\n        const dbSize =\n          await this.dependencies.databaseInfoService.getDBSize(clientMetadata);\n\n        if (dbSize === 0) {\n          this.dependencies.bulkImportService.importDefaultData(clientMetadata);\n        }\n      } catch (e) {\n        this.logger.error(\n          'Error when trying to feed the db with default data',\n          e,\n        );\n      }\n\n      this.result = {\n        resourceId: database.id,\n        region: freeSubscription?.region,\n        provider: freeSubscription?.provider,\n      };\n\n      this.changeState({ status: CloudJobStatus.Finished });\n\n      this.dependencies.cloudDatabaseAnalytics.sendCloudFreeDatabaseCreated(\n        sessionMetadata,\n        {\n          region: freeSubscription?.region || '',\n          provider: freeSubscription?.provider || '',\n        },\n      );\n\n      return database;\n    } catch (e) {\n      this.dependencies.cloudDatabaseAnalytics.sendCloudFreeDatabaseFailed(\n        sessionMetadata,\n        e,\n        {\n          region: freeSubscription?.region,\n          provider: freeSubscription?.provider,\n        },\n      );\n\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/create-free-subscription-and-database.cloud-job.ts",
    "content": "import { sortBy } from 'lodash';\nimport {\n  CloudJob,\n  CloudJobOptions,\n  CreateFreeDatabaseCloudJob,\n} from 'src/modules/cloud/job/jobs';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CreateFreeSubscriptionCloudJob } from 'src/modules/cloud/job/jobs/create-free-subscription.cloud-job';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudJobStatus, CloudJobStep } from 'src/modules/cloud/job/models';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { Database } from 'src/modules/database/models/database';\nimport { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { CloudSubscription } from 'src/modules/cloud/subscription/models';\nimport { DatabaseInfoService } from 'src/modules/database/database-info.service';\nimport { BulkImportService } from 'src/modules/bulk-actions/bulk-import.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { CloudSubscriptionApiService } from '../../subscription/cloud-subscription.api.service';\nimport { CloudSubscriptionPlanResponse } from '../../subscription/dto';\n\nexport class CreateFreeSubscriptionAndDatabaseCloudJob extends CloudJob {\n  protected name = CloudJobName.CreateFreeSubscriptionAndDatabase;\n\n  constructor(\n    readonly options: CloudJobOptions,\n\n    private data: {\n      planId?: number;\n      isRecommendedSettings?: boolean;\n    },\n\n    protected readonly dependencies: {\n      cloudDatabaseCapiService: CloudDatabaseCapiService;\n      cloudSubscriptionCapiService: CloudSubscriptionCapiService;\n      cloudTaskCapiService: CloudTaskCapiService;\n      cloudDatabaseAnalytics: CloudDatabaseAnalytics;\n      databaseService: DatabaseService;\n      databaseInfoService: DatabaseInfoService;\n      bulkImportService: BulkImportService;\n      cloudCapiKeyService: CloudCapiKeyService;\n      cloudSubscriptionApiService: CloudSubscriptionApiService;\n      featureService: FeatureService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(sessionMetadata: SessionMetadata): Promise<Database> {\n    let planId = this.data?.planId;\n\n    this.logger.debug('Create free subscription and database');\n\n    this.checkSignal();\n\n    this.changeState({ step: CloudJobStep.Subscription });\n\n    this.logger.debug('Get or create free subscription');\n\n    if (this.data?.isRecommendedSettings) {\n      const plans =\n        await this.dependencies.cloudSubscriptionApiService.getSubscriptionPlans(\n          this.options.sessionMetadata,\n        );\n\n      planId = this.getRecommendedPlanId(plans);\n    }\n\n    const freeSubscription: CloudSubscription = await this.runChildJob(\n      sessionMetadata,\n      CreateFreeSubscriptionCloudJob,\n      { planId },\n      this.options,\n    );\n\n    this.logger.debug('Get free subscription databases');\n\n    this.checkSignal();\n\n    this.changeState({ step: CloudJobStep.Database });\n\n    const database = await this.runChildJob(\n      sessionMetadata,\n      CreateFreeDatabaseCloudJob,\n      {\n        subscriptionId: freeSubscription.id,\n      },\n      this.options,\n    );\n\n    this.result = {\n      resourceId: database.id,\n      region: freeSubscription?.region,\n      provider: freeSubscription?.provider,\n    };\n\n    this.changeState({ status: CloudJobStatus.Finished });\n\n    return database;\n  }\n\n  private getRecommendedPlanId(plans: CloudSubscriptionPlanResponse[]) {\n    const defaultPlan = sortBy(plans, ['details.displayOrder']);\n    return defaultPlan[0]?.id;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/create-free-subscription.cloud-job.ts",
    "content": "import {\n  CloudJob,\n  CloudJobOptions,\n} from 'src/modules/cloud/job/jobs/cloud-job';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport {\n  CloudSubscription,\n  CloudSubscriptionStatus,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { WaitForTaskCloudJob } from 'src/modules/cloud/job/jobs/wait-for-task.cloud-job';\nimport { WaitForActiveSubscriptionCloudJob } from 'src/modules/cloud/job/jobs/wait-for-active-subscription.cloud-job';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudJobStatus } from 'src/modules/cloud/job/models';\nimport {\n  CloudDatabaseAlreadyExistsFreeException,\n  CloudPlanNotFoundFreeException,\n  CloudSubscriptionUnableToDetermineException,\n  CloudTaskNoResourceIdException,\n} from 'src/modules/cloud/job/exceptions';\nimport { SessionMetadata } from 'src/common/models';\nimport { CloudSubscriptionAlreadyExistsFreeException } from '../exceptions/cloud-subscription-already-exists-free.exception';\n\nexport class CreateFreeSubscriptionCloudJob extends CloudJob {\n  protected name = CloudJobName.CreateFreeSubscription;\n\n  constructor(\n    readonly options: CloudJobOptions,\n    private readonly data: {\n      planId: number;\n    },\n    protected readonly dependencies: {\n      cloudSubscriptionCapiService: CloudSubscriptionCapiService;\n      cloudTaskCapiService: CloudTaskCapiService;\n      cloudDatabaseCapiService: CloudDatabaseCapiService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(\n    sessionMetadata: SessionMetadata,\n  ): Promise<CloudSubscription> {\n    this.logger.debug('Ensure free cloud subscription');\n\n    this.checkSignal();\n\n    let freeSubscription: CloudSubscription;\n\n    this.logger.debug('Fetching fixed subscriptions');\n\n    const fixedSubscriptions =\n      await this.dependencies.cloudSubscriptionCapiService.getSubscriptions(\n        this.options.capiCredentials,\n        CloudSubscriptionType.Fixed,\n      );\n\n    freeSubscription =\n      CloudSubscriptionCapiService.findFreeSubscription(fixedSubscriptions);\n\n    if (freeSubscription) {\n      this.logger.debug('Fetching fixed subscription databases');\n\n      const databases =\n        await this.dependencies.cloudDatabaseCapiService.getDatabases(\n          this.options.capiCredentials,\n          {\n            subscriptionId: freeSubscription.id,\n            subscriptionType: CloudSubscriptionType.Fixed,\n          },\n        );\n\n      if (databases?.length) {\n        // throw specific error to be handled on FE side to ask user if he wants to import existing database\n        throw new CloudDatabaseAlreadyExistsFreeException(undefined, {\n          subscriptionId: freeSubscription.id,\n          databaseId: databases[0].databaseId,\n          region: freeSubscription?.region,\n          provider: freeSubscription?.provider,\n        });\n      }\n\n      // throw specific error to be handled on FE side to ask user if he wants to create new database\n      // in the existing subscription\n      throw new CloudSubscriptionAlreadyExistsFreeException(undefined, {\n        subscriptionId: freeSubscription.id,\n      });\n    }\n\n    // in case when no free subscriptions found we must create one\n    if (!freeSubscription) {\n      this.logger.debug('No free subscription found. Creating one');\n      this.checkSignal();\n      this.logger.debug('Fetching free plans');\n\n      // Get available fixed plans\n      const fixedPlans =\n        await this.dependencies.cloudSubscriptionCapiService.getSubscriptionsPlans(\n          this.options.capiCredentials,\n          CloudSubscriptionType.Fixed,\n        );\n\n      const freePlan = CloudSubscriptionCapiService.findFreePlanById(\n        fixedPlans,\n        this.data.planId,\n      );\n\n      if (!freePlan) {\n        throw new CloudPlanNotFoundFreeException();\n      }\n\n      this.logger.debug('Create free subscription');\n      this.checkSignal();\n\n      let createSubscriptionTask =\n        await this.dependencies.cloudSubscriptionCapiService.createFreeSubscription(\n          this.options.capiCredentials,\n          freePlan.id,\n        );\n\n      this.logger.debug(\n        'Create free subscription: cloud task created. Waiting...',\n      );\n      this.checkSignal();\n\n      createSubscriptionTask = await this.runChildJob(\n        sessionMetadata,\n        WaitForTaskCloudJob,\n        {\n          taskId: createSubscriptionTask.taskId,\n        },\n        this.options,\n      );\n\n      const freeSubscriptionId = createSubscriptionTask?.response?.resourceId;\n\n      if (!freeSubscriptionId) {\n        throw new CloudTaskNoResourceIdException();\n      }\n\n      freeSubscription = {\n        id: freeSubscriptionId,\n      } as CloudSubscription;\n    }\n\n    if (!freeSubscription) {\n      throw new CloudSubscriptionUnableToDetermineException();\n    }\n\n    this.checkSignal();\n\n    if (freeSubscription.status !== CloudSubscriptionStatus.Active) {\n      freeSubscription = await this.runChildJob(\n        sessionMetadata,\n        WaitForActiveSubscriptionCloudJob,\n        {\n          subscriptionId: freeSubscription.id,\n          subscriptionType: CloudSubscriptionType.Fixed,\n        },\n        this.options,\n      );\n    }\n\n    this.changeState({ status: CloudJobStatus.Finished });\n\n    return freeSubscription;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/import-free-database.cloud-job.ts",
    "content": "import { CloudJob, CloudJobOptions } from 'src/modules/cloud/job/jobs';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CloudDatabase } from 'src/modules/cloud/database/models';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport { WaitForActiveDatabaseCloudJob } from 'src/modules/cloud/job/jobs/wait-for-active-database.cloud-job';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CloudJobStatus, CloudJobStep } from 'src/modules/cloud/job/models';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { Database } from 'src/modules/database/models/database';\nimport config from 'src/utils/config';\nimport { CloudDatabaseAnalytics } from 'src/modules/cloud/database/cloud-database.analytics';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport {\n  CloudDatabaseEndpointInvalidException,\n  CloudDatabaseImportForbiddenException,\n} from 'src/modules/cloud/job/exceptions';\n\nconst cloudConfig = config.get('cloud');\n\nexport class ImportFreeDatabaseCloudJob extends CloudJob {\n  protected name = CloudJobName.ImportFreeDatabase;\n\n  constructor(\n    readonly options: CloudJobOptions,\n    private readonly data: {\n      subscriptionId: number;\n      databaseId: number;\n      region: string;\n      provider: string;\n    },\n    protected readonly dependencies: {\n      cloudDatabaseCapiService: CloudDatabaseCapiService;\n      cloudSubscriptionCapiService: CloudSubscriptionCapiService;\n      cloudTaskCapiService: CloudTaskCapiService;\n      cloudDatabaseAnalytics: CloudDatabaseAnalytics;\n      databaseService: DatabaseService;\n      cloudCapiKeyService: CloudCapiKeyService;\n      featureService: FeatureService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(sessionMetadata: SessionMetadata): Promise<Database> {\n    this.logger.debug('Importing free database');\n\n    this.checkSignal();\n\n    this.changeState({ step: CloudJobStep.Import });\n\n    this.logger.debug('Getting database metadata');\n\n    const cloudDatabase: CloudDatabase = await this.runChildJob(\n      sessionMetadata,\n      WaitForActiveDatabaseCloudJob,\n      {\n        databaseId: this.data.databaseId,\n        subscriptionId: this.data.subscriptionId,\n        subscriptionType: CloudSubscriptionType.Fixed,\n      },\n      this.options,\n    );\n\n    if (!cloudDatabase) {\n      // todo: throw\n    }\n\n    this.checkSignal();\n\n    const isDatabaseManagementEnabled =\n      await this.dependencies.featureService.isFeatureEnabled(\n        sessionMetadata,\n        KnownFeatures.DatabaseManagement,\n      );\n\n    if (!isDatabaseManagementEnabled) {\n      throw new CloudDatabaseImportForbiddenException();\n    }\n\n    const { publicEndpoint, name, password } = cloudDatabase;\n\n    if (!publicEndpoint) {\n      throw new CloudDatabaseEndpointInvalidException();\n    }\n\n    const [host, port] = publicEndpoint.split(':');\n\n    const database = await this.dependencies.databaseService.create(\n      this.options.sessionMetadata,\n      {\n        host,\n        port: parseInt(port, 10),\n        name,\n        nameFromProvider: name,\n        password,\n        provider: HostingProvider.REDIS_CLOUD,\n        cloudDetails: {\n          ...cloudDatabase?.cloudDetails,\n          free: true,\n        },\n        timeout: cloudConfig.cloudDatabaseConnectionTimeout,\n      },\n    );\n\n    this.result = {\n      resourceId: database.id,\n      region: this.data?.region,\n      provider: this.data?.provider,\n    };\n\n    this.changeState({ status: CloudJobStatus.Finished });\n\n    return database;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/index.ts",
    "content": "export * from './cloud-job';\nexport * from './create-free-database.cloud-job';\nexport * from './create-free-subscription.cloud-job';\nexport * from './create-free-subscription-and-database.cloud-job';\nexport * from './import-free-database.cloud-job';\nexport * from './wait-for-active-database.cloud-job';\nexport * from './wait-for-active-subscription.cloud-job';\nexport * from './wait-for-task.cloud-job';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/wait-for-active-database.cloud-job.ts",
    "content": "import {\n  CloudJob,\n  CloudJobOptions,\n} from 'src/modules/cloud/job/jobs/cloud-job';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { CloudDatabaseCapiService } from 'src/modules/cloud/database/cloud-database.capi.service';\nimport {\n  CloudDatabase,\n  CloudDatabaseStatus,\n} from 'src/modules/cloud/database/models';\nimport { CloudJobStatus } from 'src/modules/cloud/job/models';\nimport {\n  CloudDatabaseInFailedStateException,\n  CloudDatabaseInUnexpectedStateException,\n} from 'src/modules/cloud/job/exceptions';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class WaitForActiveDatabaseCloudJob extends CloudJob {\n  protected name = CloudJobName.WaitForActiveDatabase;\n\n  constructor(\n    readonly options: CloudJobOptions,\n    private readonly data: {\n      databaseId: number;\n      subscriptionId: number;\n      subscriptionType: CloudSubscriptionType;\n    },\n    protected readonly dependencies: {\n      cloudDatabaseCapiService: CloudDatabaseCapiService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(sessionMetadata: SessionMetadata): Promise<CloudDatabase> {\n    this.logger.debug('Waiting for cloud database active state');\n\n    this.checkSignal();\n\n    this.logger.debug('Fetching cloud database');\n\n    const database =\n      await this.dependencies.cloudDatabaseCapiService.getDatabase(\n        this.options.capiCredentials,\n        {\n          subscriptionId: this.data.subscriptionId,\n          subscriptionType: this.data.subscriptionType,\n          databaseId: this.data.databaseId,\n        },\n      );\n\n    switch (database?.status) {\n      case CloudDatabaseStatus.Active:\n        this.logger.debug('Cloud database is in the active states');\n\n        this.changeState({ status: CloudJobStatus.Finished });\n\n        return database;\n      case CloudDatabaseStatus.ImportPending:\n      case CloudDatabaseStatus.ActiveChangePending:\n      case CloudDatabaseStatus.Pending:\n      case CloudDatabaseStatus.Draft:\n        this.logger.debug(\n          'Cloud database is not in the active state. Scheduling new iteration',\n        );\n\n        return await this.runNextIteration(sessionMetadata);\n      case CloudDatabaseStatus.CreationFailed:\n        throw new CloudDatabaseInFailedStateException();\n      case CloudDatabaseStatus.DeletePending:\n      case CloudDatabaseStatus.Recovery:\n      default:\n        throw new CloudDatabaseInUnexpectedStateException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/wait-for-active-subscription.cloud-job.ts",
    "content": "import {\n  CloudJob,\n  CloudJobOptions,\n} from 'src/modules/cloud/job/jobs/cloud-job';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport {\n  CloudSubscription,\n  CloudSubscriptionStatus,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport { CloudJobStatus } from 'src/modules/cloud/job/models';\nimport {\n  CloudSubscriptionInFailedStateException,\n  CloudSubscriptionInUnexpectedStateException,\n} from 'src/modules/cloud/job/exceptions';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class WaitForActiveSubscriptionCloudJob extends CloudJob {\n  protected name = CloudJobName.WaitForActiveSubscription;\n\n  constructor(\n    readonly options: CloudJobOptions,\n    private readonly data: {\n      subscriptionId: number;\n      subscriptionType: CloudSubscriptionType;\n    },\n    protected readonly dependencies: {\n      cloudSubscriptionCapiService: CloudSubscriptionCapiService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(\n    sessionMetadata: SessionMetadata,\n  ): Promise<CloudSubscription> {\n    this.logger.debug('Waiting for cloud subscription active state');\n\n    this.checkSignal();\n\n    this.logger.debug('Fetching cloud subscription');\n\n    const subscription =\n      await this.dependencies.cloudSubscriptionCapiService.getSubscription(\n        this.options.capiCredentials,\n        this.data.subscriptionId,\n        this.data.subscriptionType,\n      );\n\n    switch (subscription?.status) {\n      case CloudSubscriptionStatus.Active:\n        this.logger.debug('Cloud subscription is in the active states');\n\n        this.changeState({ status: CloudJobStatus.Finished });\n\n        return subscription;\n      case CloudSubscriptionStatus.Pending:\n      case CloudSubscriptionStatus.NotActivated:\n        this.logger.debug(\n          'Cloud subscription is not in the active state. Scheduling new iteration',\n        );\n\n        return await this.runNextIteration(sessionMetadata);\n      case CloudSubscriptionStatus.Error:\n        this.logger.debug('Cloud subscription is in the failed state');\n\n        throw new CloudSubscriptionInFailedStateException();\n      case CloudSubscriptionStatus.Deleting:\n      default:\n        throw new CloudSubscriptionInUnexpectedStateException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/jobs/wait-for-task.cloud-job.ts",
    "content": "import {\n  CloudJob,\n  CloudJobOptions,\n} from 'src/modules/cloud/job/jobs/cloud-job';\nimport { CloudTask, CloudTaskStatus } from 'src/modules/cloud/task/models';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport { CloudJobStatus } from 'src/modules/cloud/job/models';\nimport {\n  CloudJobUnexpectedErrorException,\n  CloudTaskProcessingErrorException,\n} from 'src/modules/cloud/job/exceptions';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class WaitForTaskCloudJob extends CloudJob {\n  protected name = CloudJobName.WaitForTask;\n\n  constructor(\n    readonly options: CloudJobOptions,\n    private readonly data: {\n      taskId: string;\n    },\n    protected readonly dependencies: {\n      cloudTaskCapiService: CloudTaskCapiService;\n    },\n  ) {\n    super(options);\n  }\n\n  async iteration(sessionMetadata: SessionMetadata): Promise<CloudTask> {\n    this.logger.debug('Wait for cloud task complete');\n\n    this.checkSignal();\n\n    this.logger.debug('Fetching cloud task');\n\n    const task = await this.dependencies.cloudTaskCapiService.getTask(\n      this.options.capiCredentials,\n      this.data.taskId,\n    );\n\n    switch (task?.status) {\n      case CloudTaskStatus.Initialized:\n      case CloudTaskStatus.Received:\n      case CloudTaskStatus.ProcessingInProgress:\n        this.logger.debug(\n          'Cloud task processing is in progress. Scheduling new iteration.',\n        );\n        return await this.runNextIteration(sessionMetadata);\n      case CloudTaskStatus.ProcessingCompleted:\n        this.logger.debug('Cloud task processing successfully completed.');\n\n        this.changeState({ status: CloudJobStatus.Finished });\n\n        return task;\n      case CloudTaskStatus.ProcessingError:\n        throw new CloudTaskProcessingErrorException(undefined, {\n          cause: task.response?.error,\n        });\n      default:\n        throw new CloudJobUnexpectedErrorException(\n          'Something went wrong. Unknown task status or task was not found',\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/models/cloud-job-info.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { HttpException } from '@nestjs/common';\n\nexport enum CloudJobRunMode {\n  Async = 'async',\n  Sync = 'sync',\n}\n\nexport enum CloudJobStatus {\n  Initializing = 'initializing',\n  Running = 'running',\n  Finished = 'finished',\n  Failed = 'failed',\n}\n\nexport enum CloudJobStep {\n  Credentials = 'credentials',\n  Subscription = 'subscription',\n  Database = 'database',\n  Import = 'import',\n}\n\nexport class CloudJobInfo {\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    enum: CloudJobName,\n  })\n  @Expose()\n  name: CloudJobName;\n\n  @ApiProperty({\n    enum: CloudJobStatus,\n  })\n  @Expose()\n  status: CloudJobStatus;\n\n  @ApiPropertyOptional({\n    description: 'Children job if any',\n    type: () => CloudJobInfo,\n  })\n  @Expose()\n  @Type(() => CloudJobInfo)\n  child?: CloudJobInfo;\n\n  @ApiPropertyOptional({\n    description: 'Error if any',\n    type: () => HttpException,\n  })\n  @Expose()\n  error?: string | object;\n\n  @ApiPropertyOptional({\n    description: 'Job result',\n  })\n  @Expose()\n  result?: any;\n\n  @ApiPropertyOptional({\n    description: 'Job step',\n  })\n  @Expose()\n  step?: CloudJobStep;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/models/index.ts",
    "content": "export * from './cloud-job-info';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/transformers/cloud-job-data.transformer.spec.ts",
    "content": "import { TypeHelpOptions } from 'class-transformer';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CreateDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-database.cloud-job.data.dto';\nimport { cloudJobDataTransformer } from 'src/modules/cloud/job/transformers/cloud-job-data.transformer';\nimport { CreateSubscriptionAndDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-subscription-and-database.cloud-job.data.dto';\nimport { ImportDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/import-database.cloud-job.data.dto';\n\ndescribe('cloudJobDataTransformer', () => {\n  [\n    {\n      input: {\n        object: { name: CloudJobName.CreateFreeDatabase },\n      } as unknown as TypeHelpOptions,\n      output: CreateDatabaseCloudJobDataDto,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.CreateFreeSubscription },\n      } as unknown as TypeHelpOptions,\n      output: CreateSubscriptionAndDatabaseCloudJobDataDto,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.CreateFreeSubscriptionAndDatabase },\n      } as unknown as TypeHelpOptions,\n      output: CreateSubscriptionAndDatabaseCloudJobDataDto,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.ImportFreeDatabase },\n      } as unknown as TypeHelpOptions,\n      output: ImportDatabaseCloudJobDataDto,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.WaitForActiveDatabase },\n      } as unknown as TypeHelpOptions,\n      output: undefined,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.WaitForActiveSubscription },\n      } as unknown as TypeHelpOptions,\n      output: undefined,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.WaitForTask },\n      } as unknown as TypeHelpOptions,\n      output: undefined,\n    },\n    {\n      input: {\n        object: { name: CloudJobName.Unknown },\n      } as unknown as TypeHelpOptions,\n      output: undefined,\n    },\n    {\n      input: null,\n      output: undefined,\n    },\n  ].forEach((tc) => {\n    it(`Should return ${tc.output} when input is: ${tc.input}`, () => {\n      expect(cloudJobDataTransformer(tc.input)).toEqual(tc.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/job/transformers/cloud-job-data.transformer.ts",
    "content": "import { get } from 'lodash';\nimport { TypeHelpOptions } from 'class-transformer';\nimport { CreateDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-database.cloud-job.data.dto';\nimport { CloudJobName } from 'src/modules/cloud/job/constants';\nimport { CreateSubscriptionAndDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/create-subscription-and-database.cloud-job.data.dto';\nimport { ImportDatabaseCloudJobDataDto } from 'src/modules/cloud/job/dto/import-database.cloud-job.data.dto';\n\nexport const cloudJobDataTransformer = (data: TypeHelpOptions) => {\n  const jobName = get(data?.object, 'name');\n\n  switch (jobName) {\n    case CloudJobName.ImportFreeDatabase:\n      return ImportDatabaseCloudJobDataDto;\n    case CloudJobName.CreateFreeDatabase:\n      return CreateDatabaseCloudJobDataDto;\n    case CloudJobName.CreateFreeSubscription:\n    case CloudJobName.CreateFreeSubscriptionAndDatabase:\n      return CreateSubscriptionAndDatabaseCloudJobDataDto;\n\n    default:\n      return undefined;\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/cloud-session.module.ts",
    "content": "import { Global, Type } from '@nestjs/common';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { CloudSessionRepository } from './repositories/cloud.session.repository';\nimport { LocalCloudSessionRepository } from './repositories/local.cloud.session.repository';\n\n@Global()\nexport class CloudSessionModule {\n  static register(\n    cloudSessionRepository: Type<CloudSessionRepository> = LocalCloudSessionRepository,\n  ) {\n    return {\n      module: CloudSessionModule,\n      providers: [\n        CloudSessionService,\n        {\n          provide: CloudSessionRepository,\n          useClass: cloudSessionRepository,\n        },\n      ],\n      exports: [CloudSessionService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/cloud-session.service.spec.ts",
    "content": "import { TestingModule, Test } from '@nestjs/testing';\nimport { mockInitSession, mockSessionService, MockType } from 'src/__mocks__';\nimport { SessionService } from 'src/modules/session/session.service';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport {\n  mockCloudSession,\n  mockCloudSessionRepository,\n} from 'src/__mocks__/cloud-session';\nimport { CloudSessionRepository } from './repositories/cloud.session.repository';\n\ndescribe('CloudSessionService', () => {\n  let service: CloudSessionService;\n  let sessionService: MockType<SessionService>;\n  let cloudSessionRepository: MockType<CloudSessionRepository>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudSessionService,\n        {\n          provide: SessionService,\n          useFactory: mockSessionService,\n        },\n        {\n          provide: CloudSessionRepository,\n          useFactory: mockCloudSessionRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudSessionService);\n    sessionService = module.get(SessionService);\n    cloudSessionRepository = module.get(CloudSessionRepository);\n  });\n\n  describe('getSession', () => {\n    it('Should return null when there is no cloud data in user session', async () => {\n      const result = await service.getSession(mockInitSession.id);\n\n      expect(result).toEqual(null);\n    });\n\n    it('should take some additional data in repository if it is not in session', async () => {\n      cloudSessionRepository.get.mockResolvedValueOnce({\n        id: 1,\n        data: { idpType: 'test' },\n      });\n      const result = await service.getSession(mockInitSession.id);\n      expect(result.idpType).toBe('test');\n    });\n\n    it('should not fail if data in repository is null', async () => {\n      cloudSessionRepository.get.mockResolvedValueOnce({ id: 1, data: null });\n      const result = await service.getSession(mockInitSession.id);\n      expect(result).toEqual(null);\n    });\n\n    it('Should return cloud data only', async () => {\n      sessionService.getSession.mockResolvedValueOnce({\n        data: {\n          cloud: {\n            ...mockCloudSession,\n          },\n        },\n      });\n\n      const result = await service.getSession(mockInitSession.id);\n\n      expect(result).toEqual(mockCloudSession);\n    });\n  });\n\n  describe('updateSessionData', () => {\n    it('Should return null when there is nothing to update', async () => {\n      await service.updateSessionData(mockInitSession.id, {});\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        { cloud: {} },\n      );\n    });\n    it('Should update cloud data', async () => {\n      await service.updateSessionData(mockInitSession.id, mockCloudSession);\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        {\n          cloud: mockCloudSession,\n        },\n      );\n    });\n\n    it('Should update data in cloud sesssion repository when necessary fields included', async () => {\n      sessionService.updateSessionData.mockResolvedValue({\n        data: { cloud: mockCloudSession },\n      });\n      await service.updateSessionData(mockInitSession.id, mockCloudSession);\n\n      expect(cloudSessionRepository.save).toHaveBeenCalled();\n    });\n\n    it('Should merge and update cloud data', async () => {\n      sessionService.getSession.mockResolvedValueOnce({\n        data: {\n          cloud: {\n            ...mockCloudSession,\n          },\n        },\n      });\n\n      const toUpdate = {\n        accessToken: 'to-update-at',\n        apiSessionId: 'to0update-session-id',\n        user: {\n          id: 'to-update-user-id',\n          accounts: [\n            {\n              id: 99999,\n              name: 'new account name',\n            },\n          ],\n        },\n      };\n\n      await service.updateSessionData(mockInitSession.id, toUpdate);\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        {\n          cloud: {\n            ...mockCloudSession,\n            ...toUpdate,\n          },\n        },\n      );\n    });\n  });\n\n  describe('deleteSession', () => {\n    it('should delete cloud session data by id and clear cloud session repository data', async () => {\n      await service.deleteSessionData(mockInitSession.id);\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        { cloud: null },\n      );\n      expect(cloudSessionRepository.save).toHaveBeenCalledWith({ data: null });\n    });\n  });\n\n  describe('invalidateApiSession', () => {\n    it('should invalidate cloud api session data by id', async () => {\n      const spy = jest.spyOn(service, 'updateSessionData');\n\n      await service.invalidateApiSession(mockInitSession.id);\n      expect(spy).toHaveBeenCalledWith(mockInitSession.id, {\n        csrf: null,\n        apiSessionId: null,\n        user: null,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/cloud-session.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { SessionService } from 'src/modules/session/session.service';\nimport { CloudSession } from 'src/modules/cloud/session/models/cloud-session';\nimport { instanceToPlain, plainToInstance } from 'class-transformer';\nimport { TransformGroup } from 'src/common/constants';\nimport { CloudSessionRepository } from './repositories/cloud.session.repository';\n\n@Injectable()\nexport class CloudSessionService {\n  constructor(\n    private readonly sessionService: SessionService,\n    private readonly cloudSessionRepository: CloudSessionRepository,\n  ) {}\n\n  async getSession(id: string): Promise<CloudSession> {\n    const session = await this.sessionService.getSession(id);\n    const cloud = session?.data?.cloud;\n    if (!cloud?.refreshToken) {\n      try {\n        const cloudSessionData = await this.cloudSessionRepository.get();\n        if (cloudSessionData?.data) {\n          const { data } = cloudSessionData;\n\n          return {\n            ...cloud,\n            refreshToken: data.refreshToken,\n            idpType: data.idpType,\n          };\n        }\n      } catch (e) {\n        // ignore\n      }\n    }\n    return cloud || null;\n  }\n\n  async updateSessionData(id: string, cloud: any): Promise<CloudSession> {\n    const session = await this.getSession(id);\n\n    const cloudSession =\n      (\n        await this.sessionService.updateSessionData(id, {\n          cloud: plainToInstance(\n            CloudSession,\n            {\n              ...instanceToPlain(session, { groups: [TransformGroup.Secure] }),\n              ...instanceToPlain(cloud, { groups: [TransformGroup.Secure] }),\n            },\n            { groups: [TransformGroup.Secure] },\n          ),\n        })\n      )?.data?.cloud || null;\n\n    if (cloudSession && cloud?.refreshToken && cloud?.idpType) {\n      try {\n        this.cloudSessionRepository.save({\n          data: {\n            refreshToken: cloud.refreshToken,\n            idpType: cloud.idpType,\n          },\n        });\n      } catch (e) {\n        // ignore\n      }\n    }\n\n    return cloudSession;\n  }\n\n  async deleteSessionData(id: string): Promise<void> {\n    await this.sessionService.updateSessionData(id, { cloud: null });\n\n    try {\n      await this.cloudSessionRepository.save({ data: null });\n    } catch (e) {\n      // ignore\n    }\n  }\n\n  async invalidateApiSession(id: string): Promise<void> {\n    await this.updateSessionData(id, {\n      csrf: null,\n      apiSessionId: null,\n      user: null,\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/entities/cloud.session.entity.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\nimport { Column, Entity } from 'typeorm';\n\n@Entity('cloud_session')\nexport class CloudSessionEntity {\n  @Column({ nullable: false, primary: true })\n  @Expose()\n  id: string;\n\n  @Column({ nullable: true })\n  @DataAsJsonString()\n  @Expose()\n  data: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/models/cloud-session.ts",
    "content": "import { CloudUser } from 'src/modules/cloud/user/models';\nimport { CloudAuthIdpType } from 'src/modules/cloud/auth/models';\nimport { Expose, Type } from 'class-transformer';\n\nexport class CloudSession {\n  @Expose()\n  accessToken?: string;\n\n  @Expose()\n  refreshToken?: string;\n\n  @Expose()\n  idToken?: string;\n\n  @Expose()\n  idpType?: CloudAuthIdpType;\n\n  @Expose()\n  csrf?: string;\n\n  @Expose()\n  apiSessionId?: string;\n\n  @Expose()\n  user?: CloudUser;\n}\n\nexport class CloudSessionData {\n  @Expose()\n  id: number;\n\n  @Expose()\n  @Type(() => CloudSession)\n  data: CloudSession;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/repositories/cloud.session.repository.ts",
    "content": "// import { SessionMetadata } from 'src/common/models';\nimport { CloudSessionData } from '../models/cloud-session';\n\nexport abstract class CloudSessionRepository {\n  abstract get(): Promise<CloudSessionData>;\n  abstract save(data: Partial<CloudSessionData>): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.spec.ts",
    "content": "import {\n  MockType,\n  mockCloudSessionData,\n  mockCloudSessionEntity,\n  mockEncryptionService,\n  mockRepository,\n} from 'src/__mocks__';\nimport { Repository } from 'typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { when } from 'jest-when';\nimport { LocalCloudSessionRepository } from './local.cloud.session.repository';\nimport { CloudSessionEntity } from '../entities/cloud.session.entity';\nimport { CloudAuthIdpType } from '../../auth/models';\n\ndescribe('LocalCloudSessionRepository', () => {\n  let service: LocalCloudSessionRepository;\n  let repository: MockType<Repository<CloudSessionEntity>>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalCloudSessionRepository,\n        {\n          provide: getRepositoryToken(CloudSessionEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalCloudSessionRepository);\n    encryptionService = module.get(EncryptionService);\n    repository = module.get(getRepositoryToken(CloudSessionEntity));\n\n    encryptionService.decrypt.mockImplementation((value) => value);\n    when(encryptionService.decrypt)\n      .calledWith(mockCloudSessionEntity.data, expect.anything())\n      .mockResolvedValue(JSON.stringify(mockCloudSessionData.data));\n\n    encryptionService.encrypt.mockImplementation((value) => value);\n    when(encryptionService.encrypt)\n      .calledWith(JSON.stringify(mockCloudSessionData.data))\n      .mockResolvedValue({\n        data: mockCloudSessionEntity.data,\n        encryption: mockCloudSessionEntity.encryption,\n      });\n  });\n\n  describe('get', () => {\n    it('should return null if no cloud session data in the repository', async () => {\n      await expect(service.get()).resolves.toEqual(null);\n    });\n\n    it('should return cloudSession data if it is in the repository', async () => {\n      repository.findOneBy.mockResolvedValue(mockCloudSessionEntity);\n      await expect(service.get()).resolves.toEqual(mockCloudSessionData);\n    });\n  });\n\n  describe('save', () => {\n    it('should upsert data into repository', async () => {\n      repository.upsert.mockResolvedValue(mockCloudSessionEntity);\n\n      await expect(\n        service.save({ data: { idpType: CloudAuthIdpType.Google } }),\n      ).resolves.toEqual(undefined);\n    });\n\n    it('encrypts the data before upsertion', async () => {\n      await service.save({ data: { idpType: CloudAuthIdpType.Google } });\n\n      expect(repository.upsert).toHaveBeenCalledWith(mockCloudSessionEntity, [\n        'id',\n      ]);\n    });\n\n    it('not fail when null data is stored', async () => {\n      await expect(service.save({ data: null })).resolves.toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.ts",
    "content": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { classToClass } from 'src/utils';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { plainToInstance } from 'class-transformer';\nimport { CloudSessionRepository } from './cloud.session.repository';\nimport { CloudSessionEntity } from '../entities/cloud.session.entity';\nimport { CloudSessionData } from '../models/cloud-session';\n\nconst SESSION_ID = '1';\n\nexport class LocalCloudSessionRepository extends CloudSessionRepository {\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(CloudSessionEntity)\n    private readonly repository: Repository<CloudSessionEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(this.encryptionService, ['data']);\n  }\n\n  async get(): Promise<CloudSessionData> {\n    const entity = await this.repository.findOneBy({ id: SESSION_ID });\n\n    if (!entity) {\n      return null;\n    }\n\n    const decrypted = await this.modelEncryptor.decryptEntity(entity, false);\n\n    return classToClass(CloudSessionData, decrypted);\n  }\n\n  async save(cloudAuth: Partial<CloudSessionData>): Promise<void> {\n    const entity = await this.modelEncryptor.encryptEntity(\n      plainToInstance(CloudSessionEntity, { ...cloudAuth, id: SESSION_ID }),\n    );\n\n    await this.repository.upsert(entity, ['id']);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/cloud-subscription.api.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudApiAuthDto,\n  mockCloudApiCloudRegions,\n  mockCloudCapiKeyService,\n  mockCloudSessionService,\n  mockCloudSubscriptionCapiService,\n  mockCloudSubscriptionRegions,\n  mockFeatureService,\n  mockSessionMetadata,\n  mockSubscriptionPlanResponse,\n  MockType,\n} from 'src/__mocks__';\nimport { when, resetAllWhenMocks } from 'jest-when';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport axios from 'axios';\nimport { CloudSubscriptionApiService } from './cloud-subscription.api.service';\nimport { CloudSessionService } from '../session/cloud-session.service';\nimport { CloudSubscriptionCapiService } from './cloud-subscription.capi.service';\nimport { CloudSubscriptionApiProvider } from './providers/cloud-subscription.api.provider';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudSubscriptionApiService', () => {\n  let service: CloudSubscriptionApiService;\n  let capi: MockType<CloudSubscriptionCapiService>;\n  let featureService: MockType<FeatureService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    resetAllWhenMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudSubscriptionApiService,\n        CloudSubscriptionApiProvider,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n        {\n          provide: CloudSubscriptionCapiService,\n          useFactory: mockCloudSubscriptionCapiService,\n        },\n        {\n          provide: CloudCapiKeyService,\n          useFactory: mockCloudCapiKeyService,\n        },\n        {\n          provide: FeatureService,\n          useFactory: mockFeatureService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudSubscriptionApiService);\n    capi = module.get(CloudSubscriptionCapiService);\n    featureService = module.get(FeatureService);\n\n    when(mockedAxios.get)\n      .calledWith('/plans/cloud_regions', expect.anything())\n      .mockResolvedValue({\n        status: 200,\n        data: mockCloudApiCloudRegions,\n      });\n  });\n\n  describe('getSubscriptionPlans', () => {\n    it('successfully get plans and cloud regions', async () => {\n      expect(await service.getSubscriptionPlans(mockSessionMetadata)).toEqual(\n        mockSubscriptionPlanResponse,\n      );\n    });\n    it('successfully get plans and cloud regions from 2nd attempt', async () => {\n      when(mockedAxios.get)\n        .calledWith('/plans/cloud_regions', expect.anything())\n        .mockRejectedValueOnce(mockCapiUnauthorizedError);\n      expect(await service.getSubscriptionPlans(mockSessionMetadata)).toEqual(\n        mockSubscriptionPlanResponse,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      when(mockedAxios.get)\n        .calledWith('/plans/cloud_regions', expect.anything())\n        .mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getSubscriptionPlans(mockSessionMetadata),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n\n    describe('filter', () => {\n      beforeEach(() => {\n        featureService.getByName.mockResolvedValueOnce({\n          flag: true,\n          data: {\n            filterFreePlan: [\n              {\n                field: 'name',\n                expression: '^(No HA?.)|(Cache?.)',\n                options: 'i',\n              },\n            ],\n          },\n        });\n      });\n\n      it('get empty list due to filter', async () => {\n        capi.getSubscriptionsPlans.mockResolvedValueOnce([\n          { name: 'some name', price: 0 },\n        ]);\n        expect(await service.getSubscriptionPlans(mockSessionMetadata)).toEqual(\n          [],\n        );\n      });\n\n      it('filter only \"no ha\" and \"cache\" plans', async () => {\n        capi.getSubscriptionsPlans.mockResolvedValueOnce([\n          { name: 'some name', price: 0 },\n          { name: 'no thing', price: 0 },\n          { name: 'No HA', price: 0 },\n          { name: 'No HA 30MB price:0', price: 0 },\n          { name: 'No HA 30MB price:1', price: 1 },\n          { name: 'no ha 30MB', price: 0 },\n          { name: 'no ha', price: 0 },\n          { name: 'Cache', price: 0 },\n          { name: 'Cache 30MB', price: 0 },\n          { name: 'cache', price: 0 },\n          { name: 'cache 30MB', price: 0 },\n          { name: '', price: 0 },\n        ]);\n        expect(await service.getSubscriptionPlans(mockSessionMetadata)).toEqual(\n          [\n            { name: 'No HA', price: 0 },\n            { name: 'No HA 30MB price:0', price: 0 },\n            { name: 'no ha 30MB', price: 0 },\n            { name: 'no ha', price: 0 },\n            { name: 'Cache', price: 0 },\n            { name: 'Cache 30MB', price: 0 },\n            { name: 'cache', price: 0 },\n            { name: 'cache 30MB', price: 0 },\n          ],\n        );\n      });\n    });\n  });\n\n  describe('getCloudRegions', () => {\n    it('successfully get cloud regions', async () => {\n      expect(await service['getCloudRegions'](mockCloudApiAuthDto)).toEqual(\n        mockCloudSubscriptionRegions,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      when(mockedAxios.get)\n        .calledWith('/plans/cloud_regions', expect.anything())\n        .mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service['getCloudRegions'](mockCloudApiAuthDto),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/cloud-subscription.api.service.ts",
    "content": "import { filter, find } from 'lodash';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\nimport { wrapHttpError } from 'src/common/utils';\nimport {\n  CloudRequestUtm,\n  ICloudApiCredentials,\n} from 'src/modules/cloud/common/models';\nimport { CloudCapiKeyService } from 'src/modules/cloud/capi-key/cloud-capi-key.service';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { CloudSubscriptionCapiService } from './cloud-subscription.capi.service';\nimport { CloudSubscriptionRegion, CloudSubscriptionType } from './models';\nimport { CloudSessionService } from '../session/cloud-session.service';\nimport { parseCloudSubscriptionsCloudRegionsApiResponse } from './utils';\nimport { CloudSubscriptionApiProvider } from './providers/cloud-subscription.api.provider';\nimport { CloudSubscriptionPlanResponse } from './dto';\n\n@Injectable()\nexport class CloudSubscriptionApiService {\n  private logger = new Logger('CloudSubscriptionApiService');\n\n  constructor(\n    private readonly api: CloudSubscriptionApiProvider,\n    private readonly sessionService: CloudSessionService,\n    private readonly cloudCapiKeyService: CloudCapiKeyService,\n    private readonly cloudSubscriptionCapiService: CloudSubscriptionCapiService,\n    private readonly featureService: FeatureService,\n  ) {}\n\n  /**\n   * Get list of subscription plans and cloud regions\n   * @param sessionMetadata\n   * @param utm\n   */\n  async getSubscriptionPlans(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudSubscriptionPlanResponse[]> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        const [fixedPlans, regions] = await Promise.all([\n          this.cloudSubscriptionCapiService.getSubscriptionsPlans(\n            await this.cloudCapiKeyService.getCapiCredentials(\n              sessionMetadata,\n              utm,\n            ),\n            CloudSubscriptionType.Fixed,\n          ),\n          this.getCloudRegions(\n            await this.sessionService.getSession(sessionMetadata.sessionId),\n          ),\n        ]);\n\n        const cloudSsoFeature = await this.featureService.getByName(\n          sessionMetadata,\n          KnownFeatures.CloudSso,\n        );\n\n        const freePlans = filter(fixedPlans, (plan) => {\n          if (plan.price !== 0) {\n            return false;\n          }\n\n          if (!cloudSsoFeature?.data?.filterFreePlan?.length) {\n            return true;\n          }\n\n          return !!cloudSsoFeature.data.filterFreePlan.find(\n            (f) =>\n              f.expression &&\n              new RegExp(f.expression, f.options).test(plan[f?.field]),\n          );\n        });\n\n        return freePlans.map((plan) => ({\n          ...plan,\n          details: find(regions, { regionId: plan.regionId }),\n        }));\n      } catch (e) {\n        this.logger.error('Error getting subscription plans', e);\n        throw wrapHttpError(\n          await this.cloudCapiKeyService.handleCapiKeyUnauthorizedError(\n            e,\n            sessionMetadata,\n          ),\n        );\n      }\n    });\n  }\n\n  /**\n   * Get list of cloud regions\n   * @param credentials\n   */\n  private async getCloudRegions(\n    credentials: ICloudApiCredentials,\n  ): Promise<CloudSubscriptionRegion[]> {\n    this.logger.debug('Getting cloud regions.');\n    try {\n      const regions = await this.api.getCloudRegions(credentials);\n\n      this.logger.debug('Succeed to get cloud regions');\n\n      return parseCloudSubscriptionsCloudRegionsApiResponse(regions);\n    } catch (error) {\n      this.logger.error('Error getting cloud regions', error);\n      throw wrapHttpError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/cloud-subscription.capi.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCloudCapiAuthDto,\n  mockCloudSubscription,\n  mockCloudSubscriptionCapiProvider,\n  mockCloudTaskInit,\n  mockCreateFreeCloudSubscriptionDto,\n  mockFreeCloudSubscriptionPlan1,\n  MockType,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { CloudSubscriptionCapiProvider } from 'src/modules/cloud/subscription/providers/cloud-subscription.capi.provider';\nimport { CloudSubscriptionCapiService } from './cloud-subscription.capi.service';\n\ndescribe('CloudSubscriptionCapiService', () => {\n  let service: CloudSubscriptionCapiService;\n  let capi: MockType<CloudSubscriptionCapiProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudSubscriptionCapiService,\n        {\n          provide: CloudSubscriptionCapiProvider,\n          useFactory: mockCloudSubscriptionCapiProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudSubscriptionCapiService);\n    capi = module.get(CloudSubscriptionCapiProvider);\n  });\n\n  describe('getSubscriptions', () => {\n    it('successfully get cloud subscriptions', async () => {\n      expect(\n        await service.getSubscriptions(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Flexible,\n        ),\n      ).toEqual([mockCloudSubscription]);\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.getSubscriptionsByType.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getSubscriptions(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('getSubscription', () => {\n    it('successfully get cloud subscription', async () => {\n      expect(\n        await service.getSubscription(\n          mockCloudCapiAuthDto,\n          mockCloudSubscription.id,\n          CloudSubscriptionType.Flexible,\n        ),\n      ).toEqual(mockCloudSubscription);\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.getSubscriptionByType.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getSubscription(\n          mockCloudCapiAuthDto,\n          mockCloudSubscription.id,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('getSubscriptionsPlans', () => {\n    it('successfully get cloud subscriptions plans', async () => {\n      expect(\n        await service.getSubscriptionsPlans(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).toEqual([mockFreeCloudSubscriptionPlan1]);\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.getSubscriptionsPlansByType.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getSubscriptionsPlans(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n  describe('createFreeSubscription', () => {\n    it('successfully initialize cloud subscription creation', async () => {\n      expect(\n        await service.createFreeSubscription(\n          mockCloudCapiAuthDto,\n          mockCreateFreeCloudSubscriptionDto.planId,\n        ),\n      ).toEqual(mockCloudTaskInit);\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capi.createFreeSubscription.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.createFreeSubscription(\n          mockCloudCapiAuthDto,\n          mockCreateFreeCloudSubscriptionDto.planId,\n        ),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/cloud-subscription.capi.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport {\n  CloudSubscription,\n  CloudSubscriptionPlan,\n  CloudSubscriptionPlanProvider,\n  CloudSubscriptionType,\n} from 'src/modules/cloud/subscription/models';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport {\n  parseCloudSubscriptionCapiResponse,\n  parseCloudSubscriptionsCapiResponse,\n  parseCloudSubscriptionsPlansCapiResponse,\n} from 'src/modules/cloud/subscription/utils';\nimport { filter, find } from 'lodash';\nimport config from 'src/utils/config';\nimport { parseCloudTaskCapiResponse } from 'src/modules/cloud/task/utils';\nimport { CloudTask } from 'src/modules/cloud/task/models';\nimport { CloudSubscriptionCapiProvider } from './providers/cloud-subscription.capi.provider';\n\nconst cloudConfig = config.get('cloud');\n\n@Injectable()\nexport class CloudSubscriptionCapiService {\n  private logger = new Logger('CloudSubscriptionCapiService');\n\n  constructor(private readonly capi: CloudSubscriptionCapiProvider) {}\n\n  static findFreeSubscription(\n    subscriptions: CloudSubscription[],\n  ): CloudSubscription {\n    const freeSubscriptions = filter(subscriptions, { price: 0 });\n\n    return (\n      find(freeSubscriptions, { name: cloudConfig.freeSubscriptionName }) ||\n      freeSubscriptions[0]\n    );\n  }\n\n  static findFreePlan(plans: CloudSubscriptionPlan[]): CloudSubscriptionPlan {\n    const freePlans = filter(plans, { price: 0 });\n\n    return (\n      find(\n        freePlans,\n        (plan) =>\n          plan.provider === CloudSubscriptionPlanProvider.AWS &&\n          plan.region === cloudConfig.defaultPlanRegion &&\n          plan.name.toLowerCase().includes('standard'),\n      ) ||\n      find(freePlans, {\n        provider: CloudSubscriptionPlanProvider.AWS,\n        region: cloudConfig.defaultPlanRegion,\n      }) ||\n      find(freePlans, { provider: CloudSubscriptionPlanProvider.AWS }) ||\n      freePlans[0]\n    );\n  }\n\n  static findFreePlanById(\n    plans: CloudSubscriptionPlan[],\n    planId: number,\n  ): CloudSubscriptionPlan {\n    const freePlans = filter(plans, { price: 0 });\n\n    return find(freePlans, { id: planId });\n  }\n\n  /**\n   * Get list of account subscriptions\n   * @param authDto\n   * @param type\n   */\n  async getSubscriptions(\n    authDto: CloudCapiAuthDto,\n    type: CloudSubscriptionType,\n  ): Promise<CloudSubscription[]> {\n    this.logger.debug(`Getting cloud ${type} subscriptions.`);\n    try {\n      const subscriptions = await this.capi.getSubscriptionsByType(\n        authDto,\n        type,\n      );\n\n      this.logger.debug(`Succeed to get cloud ${type} subscriptions.`);\n\n      return parseCloudSubscriptionsCapiResponse(subscriptions, type);\n    } catch (error) {\n      this.logger.error(`Error getting ${type} subscriptions`, error);\n      throw wrapHttpError(error);\n    }\n  }\n\n  /**\n   * Get subscription by id\n   * @param authDto\n   * @param id\n   * @param type\n   */\n  async getSubscription(\n    authDto: CloudCapiAuthDto,\n    id: number,\n    type: CloudSubscriptionType,\n  ): Promise<CloudSubscription> {\n    this.logger.debug(`Getting cloud ${type} subscription.`);\n    try {\n      const subscription = await this.capi.getSubscriptionByType(\n        authDto,\n        id,\n        type,\n      );\n\n      this.logger.debug(`Succeed to get cloud ${type} subscription.`);\n\n      return parseCloudSubscriptionCapiResponse(subscription, type);\n    } catch (error) {\n      this.logger.error(`Error getting ${type} subscription`, error);\n      throw wrapHttpError(error);\n    }\n  }\n\n  /**\n   * Get list of subscription plans\n   * @param authDto\n   * @param type\n   */\n  async getSubscriptionsPlans(\n    authDto: CloudCapiAuthDto,\n    type: CloudSubscriptionType,\n  ): Promise<CloudSubscriptionPlan[]> {\n    this.logger.debug(`Getting cloud ${type} plans.`);\n    try {\n      const plans = await this.capi.getSubscriptionsPlansByType(authDto, type);\n\n      this.logger.debug(`Succeed to get cloud ${type} plans.`);\n\n      return parseCloudSubscriptionsPlansCapiResponse(plans, type);\n    } catch (error) {\n      this.logger.error('Error getting subscriptions plans', error);\n      throw wrapHttpError(error);\n    }\n  }\n\n  /**\n   * Get list of account subscriptions\n   * @param authDto\n   * @param planId\n   */\n  async createFreeSubscription(\n    authDto: CloudCapiAuthDto,\n    planId: number,\n  ): Promise<CloudTask> {\n    this.logger.debug('Creating free subscription');\n    try {\n      const task = await this.capi.createFreeSubscription(authDto, {\n        name: cloudConfig.freeSubscriptionName,\n        planId,\n        subscriptionType: CloudSubscriptionType.Fixed,\n      });\n\n      this.logger.debug('Task to creating free subscription was sent');\n\n      return parseCloudTaskCapiResponse(task);\n    } catch (error) {\n      this.logger.error('Error when creating free subscription task', error);\n      throw wrapHttpError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/cloud-subscription.controller.ts",
    "content": "import {\n  Query,\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { CloudSubscriptionApiService } from './cloud-subscription.api.service';\nimport { CloudRequestUtm } from '../common/models';\nimport { CloudSubscriptionPlanResponse } from './dto';\n\n@ApiTags('Cloud Subscription')\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('cloud/me/subscription')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CloudSubscriptionController {\n  constructor(private readonly service: CloudSubscriptionApiService) {}\n\n  @Get('/plans')\n  @ApiEndpoint({\n    description: 'Get list of plans with cloud regions',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'List of plans with cloud regions',\n        type: CloudSubscriptionPlanResponse,\n        isArray: true,\n      },\n    ],\n  })\n  async getPlans(\n    @RequestSessionMetadata() sessionMetadata,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<CloudSubscriptionPlanResponse[]> {\n    return this.service.getSubscriptionPlans(sessionMetadata, utm);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/cloud-subscription.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudSubscriptionCapiService } from 'src/modules/cloud/subscription/cloud-subscription.capi.service';\nimport { CloudCapiKeyModule } from 'src/modules/cloud/capi-key/cloud-capi-key.module';\nimport { CloudSubscriptionController } from './cloud-subscription.controller';\nimport { CloudUserModule } from '../user/cloud-user.module';\nimport { CloudSubscriptionApiService } from './cloud-subscription.api.service';\nimport { CloudSessionModule } from '../session/cloud-session.module';\nimport { CloudSubscriptionApiProvider } from './providers/cloud-subscription.api.provider';\nimport { CloudSubscriptionCapiProvider } from './providers/cloud-subscription.capi.provider';\n\n@Module({\n  imports: [CloudUserModule, CloudSessionModule, CloudCapiKeyModule],\n  providers: [\n    CloudSubscriptionApiService,\n    CloudSubscriptionApiProvider,\n    CloudSubscriptionCapiProvider,\n    CloudSubscriptionCapiService,\n  ],\n  controllers: [CloudSubscriptionController],\n  exports: [CloudSubscriptionCapiService, CloudSubscriptionApiService],\n})\nexport class CloudSubscriptionModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/dto/create-free-cloud-subscription.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n  IsDefined,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsString,\n} from 'class-validator';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\n\nexport class CreateFreeCloudSubscriptionDto {\n  @ApiProperty({\n    description: 'Subscription name',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  @ApiProperty({\n    description: 'Subscription plan id',\n    type: Number,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsInt()\n  planId: number;\n\n  @IsEnum(CloudSubscriptionType)\n  @IsNotEmpty()\n  subscriptionType: CloudSubscriptionType;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/dto/index.ts",
    "content": "export * from './create-free-cloud-subscription.dto';\nexport * from './plans.cloud-subscription.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/dto/plans.cloud-subscription.dto.ts",
    "content": "import { CloudSubscriptionPlan, CloudSubscriptionRegion } from '../models';\n\nexport class CloudSubscriptionPlanResponse extends CloudSubscriptionPlan {\n  details: CloudSubscriptionRegion;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/models/api.interface.ts",
    "content": "export interface ICloudApiSubscriptionCloudRegion {\n  id: string;\n  region_id: number;\n  zone_id: null;\n  name: string;\n  cloud: string;\n  support_maz: boolean;\n  country_name: string;\n  city_name: string;\n  flag: string;\n  longitude: null;\n  latitude: null;\n  display_order: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/models/capi.interface.ts",
    "content": "import { CloudSubscriptionStatus } from 'src/modules/cloud/subscription/models/cloud-subscription';\n\ninterface ICloudCapiSubscriptionPricing {\n  type: string;\n  typeDetails?: string;\n  quantity: number;\n  quantityMeasurement: string;\n  pricePerUnit?: number;\n  priceCurrency?: string;\n  pricePeriod?: string;\n}\n\ninterface ICloudCapiSubscriptionRegion {\n  region: string;\n  networking: any[];\n  preferredAvailabilityZones: string[];\n  multipleAvailabilityZones: boolean;\n}\n\ninterface ICloudCapiSubscriptionDetails {\n  provider: string;\n  cloudAccountId: number;\n  totalSizeInGb: number;\n  regions: ICloudCapiSubscriptionRegion[];\n}\n\nexport interface ICloudCapiSubscription {\n  id: number;\n  name: string;\n  status: CloudSubscriptionStatus;\n  paymentMethodId: number;\n  memoryStorage: string;\n  storageEncryption: boolean;\n  numberOfDatabases: number;\n  subscriptionPricing: ICloudCapiSubscriptionPricing[];\n  cloudDetails: ICloudCapiSubscriptionDetails[];\n  price?: number;\n}\n\nexport interface ICloudCapiSubscriptions {\n  accountId: number;\n  subscriptions: ICloudCapiSubscription[];\n}\n\nexport interface ICloudCapiSubscriptionPlan {\n  id: number;\n  name: string;\n  price: number;\n  provider: string;\n  region: string;\n  regionId: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/models/cloud-subscription-plan.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models/cloud-subscription';\n\nexport enum CloudSubscriptionPlanProvider {\n  AWS = 'AWS',\n  GCP = 'GCP',\n  Azure = 'Azure',\n}\n\nexport class CloudSubscriptionPlan {\n  @ApiProperty({\n    type: Number,\n  })\n  id: number;\n\n  @ApiProperty({\n    type: Number,\n  })\n  regionId: number;\n\n  @ApiProperty({\n    description: 'Subscription type',\n    enum: CloudSubscriptionType,\n  })\n  type: CloudSubscriptionType;\n\n  @ApiProperty({\n    type: String,\n  })\n  name: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  provider: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  region?: string;\n\n  @ApiPropertyOptional({\n    type: Number,\n  })\n  price?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/models/cloud-subscription-region.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nexport class CloudSubscriptionRegion {\n  @ApiProperty({\n    type: String,\n  })\n  id: string;\n\n  @ApiProperty({\n    type: Number,\n  })\n  regionId: number;\n\n  @ApiProperty({\n    type: String,\n  })\n  name: string;\n\n  @ApiProperty({\n    type: Number,\n  })\n  displayOrder: number;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  region?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  provider?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  cloud?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  countryName?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  cityName?: string;\n\n  @ApiPropertyOptional({\n    type: String,\n  })\n  flag?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/models/cloud-subscription.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nexport enum CloudSubscriptionStatus {\n  Active = 'active',\n  NotActivated = 'not_activated',\n  Deleting = 'deleting',\n  Pending = 'pending',\n  Error = 'error',\n}\n\nexport enum CloudSubscriptionType {\n  Flexible = 'flexible',\n  Fixed = 'fixed',\n}\n\nexport class CloudSubscription {\n  @ApiProperty({\n    description: 'Subscription id',\n    type: Number,\n  })\n  id: number;\n\n  @ApiProperty({\n    description: 'Subscription name',\n    type: String,\n  })\n  name: string;\n\n  @ApiProperty({\n    description: 'Subscription type',\n    enum: CloudSubscriptionType,\n  })\n  type: CloudSubscriptionType;\n\n  @ApiProperty({\n    description: 'Number of databases in subscription',\n    type: Number,\n  })\n  numberOfDatabases?: number;\n\n  @ApiProperty({\n    description: 'Subscription status',\n    enum: CloudSubscriptionStatus,\n    default: CloudSubscriptionStatus.Active,\n  })\n  status: CloudSubscriptionStatus;\n\n  @ApiPropertyOptional({\n    description: 'Subscription provider',\n    type: String,\n  })\n  provider?: string;\n\n  @ApiPropertyOptional({\n    description: 'Subscription region',\n    type: String,\n  })\n  region?: string;\n\n  @ApiPropertyOptional({\n    description: 'Subscription price',\n    type: Number,\n  })\n  price?: number;\n\n  @ApiPropertyOptional({\n    description: 'Determines if subscription is 0 price',\n    type: Boolean,\n  })\n  free?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/models/index.ts",
    "content": "export * from './api.interface';\nexport * from './capi.interface';\nexport * from './cloud-subscription';\nexport * from './cloud-subscription-plan';\nexport * from './cloud-subscription-region';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/providers/cloud-subscription.api.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudApiHeaders,\n  mockCloudApiCloudRegions,\n  mockCloudSession,\n  mockCloudSessionService,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { CloudSubscriptionApiProvider } from './cloud-subscription.api.provider';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudSubscriptionApiProvider', () => {\n  let service: CloudSubscriptionApiProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudSubscriptionApiProvider,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudSubscriptionApiProvider);\n  });\n\n  describe('getCloudRegions', () => {\n    it('successfully get cloud regions', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudApiCloudRegions,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(await service.getCloudRegions(mockCloudSession)).toEqual(\n        mockCloudApiCloudRegions,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/plans/cloud_regions',\n        mockCloudApiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.getCloudRegions(mockCloudSession)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/providers/cloud-subscription.api.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { ICloudApiSubscriptionCloudRegion } from 'src/modules/cloud/subscription/models';\nimport { wrapCloudApiError } from 'src/modules/cloud/common/exceptions';\nimport { ICloudApiCredentials } from 'src/modules/cloud/common/models';\nimport { CloudApiProvider } from '../../common/providers/cloud.api.provider';\n\n@Injectable()\nexport class CloudSubscriptionApiProvider extends CloudApiProvider {\n  /**\n   * Get cloud regions\n   * @param credentials\n   */\n  async getCloudRegions(\n    credentials: ICloudApiCredentials,\n  ): Promise<ICloudApiSubscriptionCloudRegion[]> {\n    try {\n      const { data } = await this.api.get(\n        '/plans/cloud_regions',\n        CloudApiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/providers/cloud-subscription.capi.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCreateFreeCloudSubscriptionDto,\n  mockCapiUnauthorizedError,\n  mockCloudCapiAuthDto,\n  mockCloudCapiHeaders,\n  mockCloudSubscription,\n  mockCloudSubscriptionFixed,\n  mockCloudTaskInit,\n  mockFreeCloudSubscriptionPlan1,\n} from 'src/__mocks__';\nimport { CloudSubscriptionCapiProvider } from 'src/modules/cloud/subscription/providers/cloud-subscription.capi.provider';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { CloudCapiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudSubscriptionApiProvider', () => {\n  let service: CloudSubscriptionCapiProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [CloudSubscriptionCapiProvider],\n    }).compile();\n\n    service = module.get(CloudSubscriptionCapiProvider);\n  });\n\n  describe('getSubscriptionsByType', () => {\n    it('successfully get fixed cloud subscriptions', async () => {\n      const response = {\n        status: 200,\n        data: { subscriptions: [mockCloudSubscriptionFixed] },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getSubscriptionsByType(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).toEqual([mockCloudSubscriptionFixed]);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/fixed/subscriptions',\n        mockCloudCapiHeaders,\n      );\n    });\n    it('successfully get flexible cloud subscriptions', async () => {\n      const response = {\n        status: 200,\n        data: { subscriptions: [mockCloudSubscription] },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getSubscriptionsByType(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Flexible,\n        ),\n      ).toEqual([mockCloudSubscription]);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/fixed/subscriptions',\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getSubscriptionsByType(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n  describe('getSubscriptionByType', () => {\n    it('successfully get fixed cloud subscription', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudSubscriptionFixed,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getSubscriptionByType(\n          mockCloudCapiAuthDto,\n          mockCloudSubscriptionFixed.id,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).toEqual(mockCloudSubscriptionFixed);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/fixed/subscriptions/${mockCloudSubscriptionFixed.id}`,\n        mockCloudCapiHeaders,\n      );\n    });\n    it('successfully get flexible cloud subscription', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudSubscription,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getSubscriptionByType(\n          mockCloudCapiAuthDto,\n          mockCloudSubscription.id,\n          CloudSubscriptionType.Flexible,\n        ),\n      ).toEqual(mockCloudSubscription);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `/subscriptions/${mockCloudSubscription.id}`,\n        mockCloudCapiHeaders,\n      );\n    });\n\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getSubscriptionByType(\n          mockCloudCapiAuthDto,\n          mockCloudSubscription.id,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n  describe('getSubscriptionsPlansByType', () => {\n    it('successfully get fixed plans', async () => {\n      const response = {\n        status: 200,\n        data: { plans: [mockFreeCloudSubscriptionPlan1] },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(\n        await service.getSubscriptionsPlansByType(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).toEqual([mockFreeCloudSubscriptionPlan1]);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/fixed/plans',\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getSubscriptionsPlansByType(\n          mockCloudCapiAuthDto,\n          CloudSubscriptionType.Fixed,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n  describe('createFreeSubscription', () => {\n    it('successfully create task for free subscription creation', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudTaskInit,\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.createFreeSubscription(\n          mockCloudCapiAuthDto,\n          mockCreateFreeCloudSubscriptionDto,\n        ),\n      ).toEqual(mockCloudTaskInit);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        '/fixed/subscriptions',\n        {\n          name: mockCreateFreeCloudSubscriptionDto.name,\n          paymentMethodId: null,\n          planId: mockCreateFreeCloudSubscriptionDto.planId,\n        },\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.createFreeSubscription(\n          mockCloudCapiAuthDto,\n          mockCreateFreeCloudSubscriptionDto,\n        ),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/providers/cloud-subscription.capi.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport {\n  CloudSubscriptionType,\n  ICloudCapiSubscription,\n  ICloudCapiSubscriptionPlan,\n} from 'src/modules/cloud/subscription/models';\nimport { wrapCloudCapiError } from 'src/modules/cloud/common/exceptions';\nimport { CloudCapiProvider } from 'src/modules/cloud/common/providers/cloud.capi.provider';\nimport { ICloudCapiCredentials } from 'src/modules/cloud/common/models';\nimport { CreateFreeCloudSubscriptionDto } from 'src/modules/cloud/subscription/dto';\nimport { ICloudCapiTask } from 'src/modules/cloud/task/models';\n\n@Injectable()\nexport class CloudSubscriptionCapiProvider extends CloudCapiProvider {\n  /**\n   * Get list of account subscriptions based on type\n   * @param credentials\n   * @param type\n   */\n  async getSubscriptionsByType(\n    credentials: ICloudCapiCredentials,\n    type: CloudSubscriptionType,\n  ): Promise<ICloudCapiSubscription[]> {\n    try {\n      const { data } = await this.api.get(\n        `${CloudCapiProvider.getPrefix(type)}/subscriptions`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data?.subscriptions;\n    } catch (error) {\n      throw wrapCloudCapiError(error);\n    }\n  }\n\n  /**\n   * Get subscription by id based on type\n   * @param credentials\n   * @param id\n   * @param type\n   */\n  async getSubscriptionByType(\n    credentials: ICloudCapiCredentials,\n    id: number,\n    type: CloudSubscriptionType,\n  ): Promise<ICloudCapiSubscription> {\n    try {\n      const { data } = await this.api.get(\n        `${CloudCapiProvider.getPrefix(type)}/subscriptions/${id}`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (error) {\n      throw wrapCloudCapiError(error);\n    }\n  }\n\n  /**\n   * Get list of available subscription plans\n   * @param credentials\n   * @param type\n   */\n  async getSubscriptionsPlansByType(\n    credentials: ICloudCapiCredentials,\n    type: CloudSubscriptionType,\n  ): Promise<ICloudCapiSubscriptionPlan[]> {\n    try {\n      const { data } = await this.api.get(\n        `${CloudCapiProvider.getPrefix(type)}/plans`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data?.plans;\n    } catch (error) {\n      throw wrapCloudCapiError(error);\n    }\n  }\n\n  /**\n   * Create free subscription\n   * @param credentials\n   * @param dto\n   */\n  async createFreeSubscription(\n    credentials: ICloudCapiCredentials,\n    dto: CreateFreeCloudSubscriptionDto,\n  ): Promise<ICloudCapiTask> {\n    try {\n      const { data } = await this.api.post(\n        `${CloudCapiProvider.getPrefix(CloudSubscriptionType.Fixed)}/subscriptions`,\n        {\n          name: dto.name,\n          planId: dto.planId,\n          paymentMethodId: null,\n        },\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (error) {\n      throw wrapCloudCapiError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/utils/cloud-data-converter.spec.ts",
    "content": "import {\n  mockCloudCapiSubscription,\n  mockCloudSubscription,\n  mockCloudApiCloudRegion1,\n  mockCloudSubscriptionRegion1,\n} from 'src/__mocks__/cloud-subscription';\nimport {\n  CloudSubscriptionType,\n  ICloudCapiSubscriptionPlan,\n  CloudSubscriptionPlan,\n} from 'src/modules/cloud/subscription/models';\nimport {\n  parseCloudSubscriptionCapiResponse,\n  parseCloudSubscriptionsCapiResponse,\n  parseCloudSubscriptionsPlansCapiResponse,\n  parseCloudSubscriptionsCloudRegionsApiResponse,\n} from './cloud-data-converter';\n\nconst mockCloudCapiSubscriptionPlan: ICloudCapiSubscriptionPlan = {\n  id: 1,\n  regionId: 1,\n  name: 'Cache 30MB',\n  provider: 'AWS',\n  region: 'us-east-1',\n  price: 0,\n};\n\nexport const mockCloudSubscriptionPlan: CloudSubscriptionPlan = {\n  ...mockCloudCapiSubscriptionPlan,\n  type: CloudSubscriptionType.Fixed,\n};\n\ndescribe('parseCloudSubscriptionCapiResponse', () => {\n  it('Should return subscription', () => {\n    expect(\n      parseCloudSubscriptionCapiResponse(\n        mockCloudCapiSubscription,\n        CloudSubscriptionType.Flexible,\n      ),\n    ).toEqual(mockCloudSubscription);\n  });\n});\n\ndescribe('parseCloudSubscriptionCapiResponse', () => {\n  it('Should return empty array', () => {\n    expect(\n      parseCloudSubscriptionsCapiResponse([], CloudSubscriptionType.Flexible),\n    ).toEqual([]);\n  });\n\n  it('Should return parsed array of subscriptions', () => {\n    expect(\n      parseCloudSubscriptionsCapiResponse(\n        [mockCloudCapiSubscription],\n        CloudSubscriptionType.Flexible,\n      ),\n    ).toEqual([mockCloudSubscription]);\n  });\n});\n\ndescribe('parseCloudSubscriptionsPlansCapiResponse', () => {\n  it('Should return parsed array of regions', () => {\n    expect(\n      parseCloudSubscriptionsPlansCapiResponse(\n        [mockCloudCapiSubscriptionPlan],\n        CloudSubscriptionType.Fixed,\n      ),\n    ).toEqual([mockCloudSubscriptionPlan]);\n  });\n});\n\ndescribe('parseCloudSubscriptionsCloudRegionsApiResponse', () => {\n  it('Should return empty array', () => {\n    expect(parseCloudSubscriptionsCloudRegionsApiResponse([])).toEqual([]);\n  });\n\n  it('Should return parsed array of regions', () => {\n    expect(\n      parseCloudSubscriptionsCloudRegionsApiResponse([\n        mockCloudApiCloudRegion1,\n      ]),\n    ).toEqual([mockCloudSubscriptionRegion1]);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/utils/cloud-data-converter.ts",
    "content": "import { get, toNumber } from 'lodash';\nimport {\n  CloudSubscription,\n  CloudSubscriptionPlan,\n  CloudSubscriptionRegion,\n  CloudSubscriptionType,\n  ICloudCapiSubscription,\n  ICloudApiSubscriptionCloudRegion,\n  ICloudCapiSubscriptionPlan,\n} from 'src/modules/cloud/subscription/models';\nimport { plainToInstance } from 'class-transformer';\n\nexport const parseCloudSubscriptionCapiResponse = (\n  subscription: ICloudCapiSubscription,\n  type: CloudSubscriptionType,\n): CloudSubscription =>\n  plainToInstance(CloudSubscription, {\n    id: subscription.id,\n    type,\n    name: subscription.name,\n    numberOfDatabases: subscription.numberOfDatabases,\n    status: subscription.status,\n    provider: get(\n      subscription,\n      ['cloudDetails', 0, 'provider'],\n      get(subscription, 'provider'),\n    ),\n    region: get(\n      subscription,\n      ['cloudDetails', 0, 'regions', 0, 'region'],\n      get(subscription, 'region'),\n    ),\n    price: subscription?.price,\n    free: subscription?.price === 0,\n  });\n\nexport const parseCloudSubscriptionsCapiResponse = (\n  subscriptions: ICloudCapiSubscription[],\n  type: CloudSubscriptionType,\n): CloudSubscription[] => {\n  const result: CloudSubscription[] = [];\n  if (subscriptions?.length) {\n    subscriptions?.forEach?.((subscription): void => {\n      result.push(parseCloudSubscriptionCapiResponse(subscription, type));\n    });\n  }\n  return result;\n};\n\nexport const parseCloudSubscriptionsPlansCapiResponse = (\n  plans: ICloudCapiSubscriptionPlan[],\n  type: CloudSubscriptionType,\n): CloudSubscriptionPlan[] => {\n  const result: CloudSubscriptionPlan[] = [];\n  if (plans?.length) {\n    plans?.forEach?.((plan): void => {\n      result.push(\n        plainToInstance(CloudSubscriptionPlan, {\n          id: plan.id,\n          type,\n          name: plan.name,\n          provider: plan.provider,\n          price: plan?.price,\n          region: plan.region,\n          regionId: plan.regionId,\n        }),\n      );\n    });\n  }\n  return result;\n};\n\nexport const parseCloudSubscriptionsCloudRegionsApiResponse = (\n  regions: ICloudApiSubscriptionCloudRegion[],\n): CloudSubscriptionRegion[] => {\n  const result: CloudSubscriptionRegion[] = [];\n  if (regions?.length) {\n    regions?.forEach?.((plan): void => {\n      result.push(\n        plainToInstance(CloudSubscriptionRegion, {\n          id: toNumber(plan.id),\n          name: plan.name,\n          cloud: plan.cloud,\n          displayOrder: plan.display_order,\n          countryName: plan.country_name,\n          cityName: plan.city_name,\n          regionId: plan.region_id,\n          flag: plan?.flag,\n        }),\n      );\n    });\n  }\n  return result;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/subscription/utils/index.ts",
    "content": "export * from './cloud-data-converter';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/cloud-task.capi.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCloudCapiAuthDto,\n  MockType,\n  mockCloudTaskCapiProvider,\n  mockCloudTaskInit,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudTaskNotFoundException } from 'src/modules/cloud/job/exceptions';\nimport { CloudTaskCapiProvider } from 'src/modules/cloud/task/providers/cloud-task.capi.provider';\nimport { CloudTaskCapiService } from './cloud-task.capi.service';\n\ndescribe('CloudTaskCapiService', () => {\n  let service: CloudTaskCapiService;\n  let cloudTaskCapiProvider: MockType<CloudTaskCapiProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudTaskCapiService,\n        {\n          provide: CloudTaskCapiProvider,\n          useFactory: mockCloudTaskCapiProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudTaskCapiService);\n    cloudTaskCapiProvider = module.get(CloudTaskCapiProvider);\n  });\n\n  describe('getTask', () => {\n    it('successfully get task', async () => {\n      expect(await service.getTask(mockCloudCapiAuthDto, 'id')).toEqual(\n        mockCloudTaskInit,\n      );\n    });\n    it('should throw CloudApiUnauthorizedException exception', async () => {\n      cloudTaskCapiProvider.getTask.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(service.getTask(mockCloudCapiAuthDto, 'id')).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n    it('should throw CloudTaskNotFoundException exception', async () => {\n      cloudTaskCapiProvider.getTask.mockReturnValue(null);\n      await expect(service.getTask(mockCloudCapiAuthDto, 'id')).rejects.toThrow(\n        CloudTaskNotFoundException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/cloud-task.capi.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { CloudTaskCapiProvider } from 'src/modules/cloud/task/providers/cloud-task.capi.provider';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { parseCloudTaskCapiResponse } from 'src/modules/cloud/task/utils';\nimport { CloudTask } from 'src/modules/cloud/task/models';\nimport { CloudTaskNotFoundException } from 'src/modules/cloud/job/exceptions';\n\n@Injectable({})\nexport class CloudTaskCapiService {\n  private logger = new Logger('CloudTaskCapiService');\n\n  constructor(private readonly cloudTaskCapiProvider: CloudTaskCapiProvider) {}\n\n  async getTask(credentials: CloudCapiAuthDto, id: string): Promise<CloudTask> {\n    try {\n      this.logger.debug('Trying to get cloud task', { id });\n      const task = await this.cloudTaskCapiProvider.getTask(credentials, id);\n\n      if (!task) {\n        throw new CloudTaskNotFoundException();\n      }\n\n      this.logger.debug('Successfully fetched cloud task', task);\n      return parseCloudTaskCapiResponse(task);\n    } catch (e) {\n      this.logger.error('Unable to get cloud task', e);\n      throw wrapHttpError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/cloud-task.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CloudTaskCapiService } from 'src/modules/cloud/task/cloud-task.capi.service';\nimport { CloudTaskCapiProvider } from 'src/modules/cloud/task/providers/cloud-task.capi.provider';\n\n@Module({\n  providers: [CloudTaskCapiProvider, CloudTaskCapiService],\n  exports: [CloudTaskCapiService],\n})\nexport class CloudTaskModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/models/capi.interface.ts",
    "content": "export interface ICloudCapiTask {\n  taskId: string;\n  commandType: string;\n  status: string;\n  description?: string;\n  timestamp: string;\n  response?: any;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/models/cloud-task.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nexport enum CloudTaskStatus {\n  Initialized = 'initialized',\n  Received = 'received',\n  ProcessingInProgress = 'processing-in-progress',\n  ProcessingCompleted = 'processing-completed',\n  ProcessingError = 'processing-error',\n}\n\nexport class CloudTask {\n  @ApiProperty({\n    type: String,\n  })\n  taskId: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  commandType: string;\n\n  @ApiProperty({\n    description: 'Current status of the task',\n    enum: CloudTaskStatus,\n  })\n  status: CloudTaskStatus;\n\n  @ApiProperty({\n    type: Date,\n  })\n  timestamp: Date;\n\n  @ApiPropertyOptional({\n    description: 'Short info on what happened',\n    type: String,\n  })\n  description?: string;\n\n  @ApiPropertyOptional({\n    description: 'Additional info',\n  })\n  response?: any;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/models/index.ts",
    "content": "export * from './capi.interface';\nexport * from './cloud-task';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/providers/cloud-task.capi.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudTaskInit,\n  mockCloudCapiAuthDto,\n  mockCloudCapiHeaders,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudTaskCapiProvider } from 'src/modules/cloud/task/providers/cloud-task.capi.provider';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudTaskCapiProvider', () => {\n  let service: CloudTaskCapiProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [CloudTaskCapiProvider],\n    }).compile();\n\n    service = module.get(CloudTaskCapiProvider);\n  });\n\n  describe('getTask', () => {\n    it('successfully get capi access key', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudTaskInit,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(await service.getTask(mockCloudCapiAuthDto, 'id')).toEqual(\n        mockCloudTaskInit,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/tasks/id',\n        mockCloudCapiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.getTask(mockCloudCapiAuthDto, 'id')).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/providers/cloud-task.capi.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { CloudCapiProvider } from 'src/modules/cloud/common/providers/cloud.capi.provider';\nimport { ICloudCapiTask } from 'src/modules/cloud/task/models';\nimport { ICloudCapiCredentials } from 'src/modules/cloud/common/models';\nimport { wrapCloudApiError } from 'src/modules/cloud/common/exceptions';\n\n@Injectable()\nexport class CloudTaskCapiProvider extends CloudCapiProvider {\n  /**\n   * Get task details by id\n   * @param credentials\n   * @param id\n   */\n  async getTask(\n    credentials: ICloudCapiCredentials,\n    id: string,\n  ): Promise<ICloudCapiTask> {\n    try {\n      const { data } = await this.api.get(\n        `/tasks/${id}`,\n        CloudCapiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (error) {\n      throw wrapCloudApiError(error);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/utils/cloud-data-converter.ts",
    "content": "import { plainToInstance } from 'class-transformer';\nimport { CloudTask, ICloudCapiTask } from 'src/modules/cloud/task/models';\n\nexport const parseCloudTaskCapiResponse = (task: ICloudCapiTask): CloudTask =>\n  plainToInstance(CloudTask, task);\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/task/utils/index.ts",
    "content": "export * from './cloud-data-converter';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/cloud-user.api.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { sign } from 'jsonwebtoken';\nimport {\n  mockCloudApiAuthDto,\n  mockCloudSessionService,\n  mockCloudUserRepository,\n  mockSessionMetadata,\n  mockCloudUser,\n  MockType,\n  mockCapiUnauthorizedError,\n  mockCloudSession,\n  mockCloudCapiAccount,\n  mockCloudApiUser,\n  mockCloudApiCsrfToken,\n  mockServerService,\n  mockCloudRequestUtm,\n  mockCompleteCloudUtm,\n  mockUtmCompleteBody,\n  mockUtmBody,\n} from 'src/__mocks__';\nimport { when, resetAllWhenMocks } from 'jest-when';\nimport {\n  CloudApiInternalServerErrorException,\n  CloudApiUnauthorizedException,\n} from 'src/modules/cloud/common/exceptions';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudUserApiProvider } from 'src/modules/cloud/user/providers/cloud-user.api.provider';\nimport { CloudUserRepository } from 'src/modules/cloud/user/repositories/cloud-user.repository';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\nimport { mockCloudAuthService } from 'src/__mocks__/cloud-auth';\nimport axios from 'axios';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { CloudAuthIdpType } from 'src/modules/cloud/auth/models';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudUserApiService', () => {\n  let service: CloudUserApiService;\n  let repository: MockType<CloudUserRepository>;\n  let sessionService: MockType<CloudSessionService>;\n  let authService: MockType<CloudAuthService>;\n  let serverService: MockType<ServerService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    resetAllWhenMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudUserApiService,\n        CloudUserApiProvider,\n        {\n          provide: CloudUserRepository,\n          useFactory: mockCloudUserRepository,\n        },\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n        {\n          provide: CloudAuthService,\n          useFactory: mockCloudAuthService,\n        },\n        {\n          provide: ServerService,\n          useFactory: mockServerService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudUserApiService);\n    repository = module.get(CloudUserRepository);\n    sessionService = module.get(CloudSessionService);\n    authService = module.get(CloudAuthService);\n    serverService = module.get(ServerService);\n  });\n\n  describe('ensureCsrf', () => {\n    beforeEach(async () => {\n      when(mockedAxios.get)\n        .calledWith('csrf', expect.anything())\n        .mockResolvedValue({\n          status: 200,\n          data: { csrfToken: mockCloudApiCsrfToken },\n        });\n    });\n\n    it('should pass when there is existing csrf', async () => {\n      expect(await service['ensureCsrf'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).not.toHaveBeenCalled();\n      expect(mockedAxios.get).toHaveBeenCalledTimes(0);\n    });\n    it('should get csrf when no csrf in the session', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      expect(await service['ensureCsrf'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          csrf: mockCloudApiAuthDto.csrf,\n        },\n      );\n      expect(mockedAxios.get).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        'csrf',\n        expect.anything(),\n      );\n    });\n    it('should throw unauthorized error when no csrf returned', async () => {\n      when(mockedAxios.get)\n        .calledWith('csrf', expect.anything())\n        .mockResolvedValue({\n          status: 200,\n          data: { different: mockCloudApiCsrfToken },\n        });\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      await expect(\n        service['ensureCsrf'](mockSessionMetadata),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).not.toHaveBeenCalled();\n      expect(mockedAxios.get).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        'csrf',\n        expect.anything(),\n      );\n    });\n    it('should throw unauthorized error when fetching api call returned 401', async () => {\n      when(mockedAxios.get)\n        .calledWith('csrf', expect.anything())\n        .mockRejectedValueOnce(mockCapiUnauthorizedError);\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      await expect(\n        service['ensureCsrf'](mockSessionMetadata),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).not.toHaveBeenCalled();\n      expect(mockedAxios.get).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        'csrf',\n        expect.anything(),\n      );\n    });\n  });\n\n  describe('ensureAccessToken', () => {\n    it('Should not renew when access token is not expired', async () => {\n      const mockedAccessToken = sign(\n        { exp: Math.trunc(Date.now() / 1000) + 3600 },\n        'test',\n      );\n      sessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudApiAuthDto,\n        accessToken: mockedAccessToken,\n      });\n      await service['ensureAccessToken'](mockSessionMetadata);\n\n      expect(authService.renewTokens).not.toHaveBeenCalled();\n    });\n    it('Should not renew when access token is not expired and 3m until expiration time', async () => {\n      const mockedAccessToken = sign(\n        { exp: Math.trunc(Date.now() / 1000) + 180 },\n        'test',\n      );\n      sessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudApiAuthDto,\n        accessToken: mockedAccessToken,\n      });\n      await service['ensureAccessToken'](mockSessionMetadata);\n\n      expect(authService.renewTokens).not.toHaveBeenCalled();\n    });\n    it('Should renew tokens when access token is expired', async () => {\n      const mockedAccessToken = sign(\n        { exp: Math.trunc(Date.now() / 1000) - 3600 },\n        'test',\n      );\n      sessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudApiAuthDto,\n        accessToken: mockedAccessToken,\n      });\n      await service['ensureAccessToken'](mockSessionMetadata);\n\n      expect(authService.renewTokens).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCloudApiAuthDto.idpType,\n        mockCloudApiAuthDto.refreshToken,\n      );\n    });\n    it('Should renew tokens when access token is not expired but < 2m until exp time', async () => {\n      const mockedAccessToken = sign(\n        { exp: Math.trunc(Date.now() / 1000) + 100 },\n        'test',\n      );\n      sessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudApiAuthDto,\n        accessToken: mockedAccessToken,\n      });\n      await service['ensureAccessToken'](mockSessionMetadata);\n\n      expect(authService.renewTokens).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCloudApiAuthDto.idpType,\n        mockCloudApiAuthDto.refreshToken,\n      );\n    });\n    it('Should throw CloudApiUnauthorizedException in case of any error', async () => {\n      authService.renewTokens.mockRejectedValueOnce(new Error());\n\n      const mockedAccessToken = sign(\n        { exp: Math.trunc(Date.now() / 1000) },\n        'test',\n      );\n      sessionService.getSession.mockResolvedValueOnce({\n        ...mockCloudApiAuthDto,\n        accessToken: mockedAccessToken,\n      });\n\n      await expect(\n        service['ensureAccessToken'](mockSessionMetadata),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n      expect(authService.renewTokens).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCloudApiAuthDto.idpType,\n        mockCloudApiAuthDto.refreshToken,\n      );\n    });\n    it('Should throw CloudApiUnauthorizedException error if there is no session', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      await expect(\n        service['ensureAccessToken'](mockSessionMetadata),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n      expect(authService.renewTokens).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('ensureLogin', () => {\n    let spyEnsureAccessToken;\n    let spyEnsureCsrf;\n\n    beforeEach(async () => {\n      spyEnsureAccessToken = jest.spyOn(service as any, 'ensureAccessToken');\n      spyEnsureAccessToken.mockResolvedValue(undefined);\n      spyEnsureCsrf = jest.spyOn(service as any, 'ensureCsrf');\n      spyEnsureCsrf.mockResolvedValue(undefined);\n      when(mockedAxios.post)\n        .calledWith('login', expect.anything(), expect.anything())\n        .mockResolvedValue({\n          status: 200,\n          headers: {\n            'set-cookie': [\n              `anything;JSESSIONID=${mockCloudApiAuthDto.apiSessionId};anything;`,\n            ],\n          },\n        });\n    });\n\n    it('should pass when there is existing user in session', async () => {\n      expect(await service['ensureLogin'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).not.toHaveBeenCalled();\n      expect(mockedAxios.post).toHaveBeenCalledTimes(0);\n    });\n    it('should login and get csrf when no apiSessionId (should ignore utm)', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      expect(await service['ensureLogin'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          apiSessionId: mockCloudApiAuthDto.apiSessionId,\n        },\n      );\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        {},\n        expect.anything(),\n      );\n    });\n    it('should login and get csrf when no apiSessionId and use passed utm parameters', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      expect(\n        await service['ensureLogin'](mockSessionMetadata, mockCompleteCloudUtm),\n      ).toEqual(undefined);\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          apiSessionId: mockCloudApiAuthDto.apiSessionId,\n        },\n      );\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        mockUtmCompleteBody,\n        expect.anything(),\n      );\n      expect(serverService.getInfo).not.toHaveBeenCalled();\n    });\n    it('should login and get csrf when no apiSessionId and calculate additional utm parameters', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      expect(\n        await service['ensureLogin'](mockSessionMetadata, mockCloudRequestUtm),\n      ).toEqual(undefined);\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          apiSessionId: mockCloudApiAuthDto.apiSessionId,\n        },\n      );\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        mockUtmCompleteBody,\n        expect.anything(),\n      );\n      expect(serverService.getInfo).toHaveBeenCalledWith(mockSessionMetadata);\n    });\n    it('should login and get csrf when no apiSessionId and not fail when calculating utm caused an error', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n      serverService.getInfo.mockRejectedValueOnce(new Error());\n\n      expect(\n        await service['ensureLogin'](mockSessionMetadata, mockCloudRequestUtm),\n      ).toEqual(undefined);\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          apiSessionId: mockCloudApiAuthDto.apiSessionId,\n        },\n      );\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        mockUtmBody,\n        expect.anything(),\n      );\n      expect(serverService.getInfo).toHaveBeenCalledWith(mockSessionMetadata);\n    });\n    it('should login and get csrf when no apiSessionId login should be sent with \"auth_mode\"', async () => {\n      sessionService.getSession.mockResolvedValueOnce({\n        idpType: CloudAuthIdpType.Sso,\n      });\n\n      expect(await service['ensureLogin'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        {\n          apiSessionId: mockCloudApiAuthDto.apiSessionId,\n        },\n      );\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        expect.objectContaining({\n          auth_mode: CloudAuthIdpType.Sso,\n        }),\n        expect.anything(),\n      );\n    });\n    it('should throw unauthorized error when no session id successfully fetched', async () => {\n      when(mockedAxios.post)\n        .calledWith('login', expect.anything(), expect.anything())\n        .mockResolvedValue({\n          status: 200,\n          headers: { 'set-cookie': ['anything;anything;'] },\n        });\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      await expect(\n        service['ensureLogin'](mockSessionMetadata),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(0);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).not.toHaveBeenCalled();\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        expect.anything(),\n        expect.anything(),\n      );\n    });\n    it('should throw unauthorized error when no fetching api call returns 401', async () => {\n      when(mockedAxios.post)\n        .calledWith('login', expect.anything(), expect.anything())\n        .mockRejectedValueOnce(mockCapiUnauthorizedError);\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      await expect(\n        service['ensureLogin'](mockSessionMetadata),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(spyEnsureAccessToken).toHaveBeenCalledTimes(1);\n      expect(spyEnsureCsrf).toHaveBeenCalledTimes(0);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(sessionService.updateSessionData).not.toHaveBeenCalled();\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.post).toHaveBeenNthCalledWith(\n        1,\n        'login',\n        expect.anything(),\n        expect.anything(),\n      );\n    });\n  });\n\n  describe('ensureCloudUser', () => {\n    let spy;\n\n    beforeEach(async () => {\n      spy = jest.spyOn(service as any, 'ensureLogin');\n      spy.mockResolvedValue(undefined);\n      when(mockedAxios.get)\n        .calledWith('/users/me', expect.anything())\n        .mockResolvedValue({\n          status: 200,\n          data: mockCloudApiUser,\n        });\n      when(mockedAxios.get)\n        .calledWith('/accounts', expect.anything())\n        .mockResolvedValue({\n          status: 200,\n          data: { accounts: [mockCloudCapiAccount] },\n        });\n    });\n\n    it('should pass when there is existing user in session', async () => {\n      expect(await service['ensureCloudUser'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(repository.get).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledTimes(0);\n    });\n    it('should fetch user when force flag submitted', async () => {\n      expect(\n        await service['ensureCloudUser'](mockSessionMetadata, true),\n      ).toEqual(undefined);\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(repository.get).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledTimes(2);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        '/users/me',\n        expect.anything(),\n      );\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        2,\n        '/accounts',\n        expect.anything(),\n      );\n    });\n    it('should fetch user when there is no user in the repo', async () => {\n      repository.get.mockResolvedValue(null);\n      expect(await service['ensureCloudUser'](mockSessionMetadata)).toEqual(\n        undefined,\n      );\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(repository.get).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledTimes(2);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        '/users/me',\n        expect.anything(),\n      );\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        2,\n        '/accounts',\n        expect.anything(),\n      );\n    });\n    it('should wrap Unauthorized error when /user/me returned 401 status code', async () => {\n      when(mockedAxios.get)\n        .calledWith('/users/me', expect.anything())\n        .mockRejectedValueOnce(mockCapiUnauthorizedError);\n\n      await expect(\n        service['ensureCloudUser'](mockSessionMetadata, true),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(mockedAxios.get).toHaveBeenCalledTimes(1);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        '/users/me',\n        expect.anything(),\n      );\n    });\n    it('should wrap Unauthorized error when /accounts returned 401 status code', async () => {\n      when(mockedAxios.get)\n        .calledWith('/accounts', expect.anything())\n        .mockRejectedValueOnce(mockCapiUnauthorizedError);\n\n      await expect(\n        service['ensureCloudUser'](mockSessionMetadata, true),\n      ).rejects.toBeInstanceOf(CloudApiUnauthorizedException);\n      expect(mockedAxios.get).toHaveBeenCalledTimes(2);\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        1,\n        '/users/me',\n        expect.anything(),\n      );\n      expect(mockedAxios.get).toHaveBeenNthCalledWith(\n        2,\n        '/accounts',\n        expect.anything(),\n      );\n    });\n  });\n\n  describe('me', () => {\n    let spy;\n\n    beforeEach(async () => {\n      spy = jest.spyOn(service as any, 'ensureCloudUser');\n      spy.mockResolvedValue(undefined);\n    });\n\n    it('should get user profile', async () => {\n      expect(await service.me(mockSessionMetadata)).toEqual(mockCloudUser);\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(repository.get).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n    });\n    it('should get user profile from 2nd attempt', async () => {\n      spy.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      spy.mockResolvedValueOnce(undefined);\n\n      expect(await service.me(mockSessionMetadata)).toEqual(mockCloudUser);\n      expect(spy).toHaveBeenCalledTimes(2);\n    });\n    it('should throw an error if retries attempts exceeded', async () => {\n      spy.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      spy.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n\n      await expect(service.me(mockSessionMetadata)).rejects.toEqual(\n        new CloudApiUnauthorizedException(),\n      );\n      expect(spy).toHaveBeenCalledTimes(2);\n    });\n    it('should throw an error from 1st attempt when no Anauthorized Error', async () => {\n      spy.mockRejectedValueOnce(new CloudApiInternalServerErrorException());\n\n      await expect(service.me(mockSessionMetadata)).rejects.toEqual(\n        new CloudApiInternalServerErrorException(),\n      );\n      expect(spy).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('getUserSession', () => {\n    let spy;\n\n    beforeEach(async () => {\n      spy = jest.spyOn(service as any, 'ensureCloudUser');\n      spy.mockResolvedValue(undefined);\n    });\n\n    it('should get user session', async () => {\n      expect(await service.getUserSession(mockSessionMetadata)).toEqual(\n        mockCloudSession,\n      );\n      expect(spy).toHaveBeenCalledTimes(1);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n      );\n    });\n    it('should get user session from 2nd attempt', async () => {\n      spy.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      spy.mockResolvedValueOnce(undefined);\n\n      expect(await service.getUserSession(mockSessionMetadata)).toEqual(\n        mockCloudSession,\n      );\n      expect(spy).toHaveBeenCalledTimes(2);\n    });\n    it('should throw an error if retries attempts exceeded', async () => {\n      spy.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n      spy.mockRejectedValueOnce(new CloudApiUnauthorizedException());\n\n      await expect(service.getUserSession(mockSessionMetadata)).rejects.toEqual(\n        new CloudApiUnauthorizedException(),\n      );\n      expect(spy).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('setCurrentAccount', () => {\n    let spy;\n    let response;\n\n    beforeEach(async () => {\n      jest\n        .spyOn(service as any, 'ensureCloudUser')\n        .mockResolvedValue(undefined);\n      spy = jest.spyOn(service, 'getCloudUser');\n      spy.mockResolvedValue(mockCloudUser);\n    });\n\n    it('should set user account', async () => {\n      response = {\n        status: 200,\n        data: {},\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.setCurrentAccount(\n          mockSessionMetadata,\n          mockCloudUser.currentAccountId,\n        ),\n      ).toEqual(mockCloudUser);\n      expect(mockedAxios.post).toHaveBeenCalledTimes(1);\n    });\n    it('should set user account from 2nd attempt', async () => {\n      response = {\n        status: 200,\n        data: {},\n      };\n      mockedAxios.post.mockRejectedValueOnce(mockCapiUnauthorizedError);\n      mockedAxios.post.mockResolvedValueOnce(mockCapiUnauthorizedError);\n\n      expect(\n        await service.setCurrentAccount(\n          mockSessionMetadata,\n          mockCloudUser.currentAccountId,\n        ),\n      ).toEqual(mockCloudUser);\n      expect(mockedAxios.post).toHaveBeenCalledTimes(2);\n    });\n    it('should throw an error if retries attempts exceeded', async () => {\n      response = {\n        status: 200,\n        data: {},\n      };\n      mockedAxios.post.mockRejectedValueOnce(mockCapiUnauthorizedError);\n      mockedAxios.post.mockRejectedValueOnce(mockCapiUnauthorizedError);\n\n      await expect(\n        service.setCurrentAccount(\n          mockSessionMetadata,\n          mockCloudUser.currentAccountId,\n        ),\n      ).rejects.toEqual(\n        new CloudApiUnauthorizedException(mockCapiUnauthorizedError.message),\n      );\n      expect(mockedAxios.post).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('updateUser', () => {\n    it('should update cloud user', async () => {\n      expect(\n        await service.updateUser(mockSessionMetadata, { currentAccountId: 1 }),\n      ).toEqual(mockCloudUser);\n      expect(repository.update).toHaveBeenCalledWith(\n        mockSessionMetadata.sessionId,\n        { currentAccountId: 1 },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/cloud-user.api.service.ts",
    "content": "import { find } from 'lodash';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\nimport { CloudUserRepository } from 'src/modules/cloud/user/repositories/cloud-user.repository';\nimport { CloudUser, CloudUserAccount } from 'src/modules/cloud/user/models';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudUserApiProvider } from 'src/modules/cloud/user/providers/cloud-user.api.provider';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\nimport { CloudSession } from 'src/modules/cloud/session/models/cloud-session';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { isValidToken } from './utils';\n\n@Injectable()\nexport class CloudUserApiService {\n  private logger = new Logger('CloudUserApiService');\n\n  constructor(\n    private readonly cloudAuthService: CloudAuthService,\n    private readonly repository: CloudUserRepository,\n    private readonly sessionService: CloudSessionService,\n    private readonly api: CloudUserApiProvider,\n    private readonly serverService: ServerService,\n  ) {}\n\n  /**\n   * Find current account in accounts list by currentAccountId\n   * @param user\n   */\n  static getCurrentAccount(user: CloudUser): CloudUserAccount {\n    return find(user?.accounts, { id: user?.currentAccountId });\n  }\n\n  /**\n   * Fetch csrf token if needed\n   * @param sessionMetadata\n   * @private\n   */\n  private async ensureCsrf(sessionMetadata: SessionMetadata): Promise<void> {\n    try {\n      const session = await this.sessionService.getSession(\n        sessionMetadata.sessionId,\n      );\n\n      if (!session?.csrf) {\n        this.logger.debug('Trying to get csrf token', sessionMetadata);\n        const csrf = await this.api.getCsrfToken(session);\n\n        if (!csrf) {\n          throw new CloudApiUnauthorizedException();\n        }\n\n        await this.sessionService.updateSessionData(sessionMetadata.sessionId, {\n          csrf,\n        });\n      }\n    } catch (e) {\n      this.logger.error('Unable to get csrf token', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Renew jwt tokens if needed\n   * @param sessionMetadata\n   * @private\n   */\n  private async ensureAccessToken(\n    sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    try {\n      const session = await this.sessionService.getSession(\n        sessionMetadata.sessionId,\n      );\n\n      if (!isValidToken(session?.accessToken)) {\n        if (!session?.refreshToken) {\n          this.logger.error('Refresh token is undefined');\n          throw new CloudApiUnauthorizedException();\n        }\n\n        await this.cloudAuthService.renewTokens(\n          sessionMetadata,\n          session?.idpType,\n          session?.refreshToken,\n        );\n      }\n    } catch (e) {\n      this.logger.error('Error trying renew token', e);\n      throw new CloudApiUnauthorizedException(e.message);\n    }\n  }\n\n  /**\n   * Login user to api using accessToken from oauth flow\n   * @param sessionMetadata\n   * @param utm\n   * @private\n   */\n  private async ensureLogin(\n    sessionMetadata: SessionMetadata,\n    utm?: CloudRequestUtm,\n  ): Promise<void> {\n    try {\n      await this.ensureAccessToken(sessionMetadata);\n\n      const session = await this.sessionService.getSession(\n        sessionMetadata.sessionId,\n      );\n\n      if (!session?.apiSessionId) {\n        this.logger.debug('Trying to login user', sessionMetadata);\n\n        const preparedUtm = utm && { ...utm };\n\n        if (preparedUtm && (!preparedUtm.amp || !preparedUtm.package)) {\n          await this.serverService\n            .getInfo(sessionMetadata)\n            .then(({ id, packageType }) => {\n              preparedUtm.amp = preparedUtm.amp || id;\n              preparedUtm.package = preparedUtm.package || packageType;\n            })\n            .catch(() => {\n              this.logger.warn(\n                'Unable to get server id for utm parameters',\n                sessionMetadata,\n              );\n            });\n        }\n\n        const apiSessionId = await this.api.getApiSessionId(\n          session,\n          preparedUtm,\n        );\n\n        if (!apiSessionId) {\n          throw new CloudApiUnauthorizedException();\n        }\n\n        await this.sessionService.updateSessionData(sessionMetadata.sessionId, {\n          apiSessionId,\n        });\n      }\n\n      await this.ensureCsrf(sessionMetadata);\n    } catch (e) {\n      this.logger.error('Unable to login user', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Sync cloud user profile when needed\n   * Always sync with force=true\n   * @param sessionMetadata\n   * @param force\n   * @param utm\n   * @private\n   */\n  private async ensureCloudUser(\n    sessionMetadata: SessionMetadata,\n    force = false,\n    utm?: CloudRequestUtm,\n  ) {\n    try {\n      await this.ensureLogin(sessionMetadata, utm);\n\n      const session = await this.sessionService.getSession(\n        sessionMetadata.sessionId,\n      );\n\n      const existingUser = await this.repository.get(sessionMetadata.sessionId);\n\n      if (existingUser?.id && !force) {\n        return;\n      }\n\n      this.logger.debug('Trying to sync user profile', sessionMetadata);\n\n      const userData = await this.api.getCurrentUser(session);\n\n      const user: CloudUser = {\n        id: +userData.id,\n        name: userData.name,\n        currentAccountId: +userData.current_account_id,\n      };\n\n      const accounts = await this.api.getAccounts(session);\n\n      // todo: remember existing CApi key?\n      user.accounts = accounts.map((account) => ({\n        id: account.id,\n        name: account.name,\n        capiKey: account.api_access_key,\n      }));\n\n      await this.repository.update(sessionMetadata.sessionId, user);\n      this.logger.debug(\n        'Successfully synchronized user profile',\n        sessionMetadata,\n      );\n    } catch (e) {\n      this.logger.error('Unable to sync user profile', e, sessionMetadata);\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get cloud user profile\n   * @param sessionMetadata\n   * @param forceSync\n   * @param utm\n   */\n  async getCloudUser(\n    sessionMetadata: SessionMetadata,\n    forceSync = false,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudUser> {\n    try {\n      await this.ensureCloudUser(sessionMetadata, forceSync, utm);\n\n      return await this.repository.get(sessionMetadata.sessionId);\n    } catch (e) {\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get cloud user profile\n   * @param sessionMetadata\n   * @param forceSync\n   * @param utm\n   */\n  async me(\n    sessionMetadata: SessionMetadata,\n    forceSync = false,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudUser> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () =>\n      this.getCloudUser(sessionMetadata, forceSync, utm),\n    );\n  }\n\n  /**\n   * Get complete cloud user session\n   * @param sessionMetadata\n   * @param forceSync\n   * @param utm\n   */\n  async getUserSession(\n    sessionMetadata: SessionMetadata,\n    forceSync = false,\n    utm?: CloudRequestUtm,\n  ): Promise<CloudSession> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        await this.ensureCloudUser(sessionMetadata, forceSync, utm);\n\n        return await this.sessionService.getSession(sessionMetadata.sessionId);\n      } catch (e) {\n        throw wrapHttpError(e);\n      }\n    });\n  }\n\n  /**\n   * Invalidate user SM API session\n   * @param sessionMetadata\n   */\n  async invalidateApiSession(sessionMetadata: SessionMetadata): Promise<void> {\n    await this.sessionService.invalidateApiSession(sessionMetadata.sessionId);\n  }\n\n  /**\n   * Select current account to work with\n   * @param sessionMetadata\n   * @param accountId\n   */\n  async setCurrentAccount(\n    sessionMetadata: SessionMetadata,\n    accountId: string | number,\n  ): Promise<CloudUser> {\n    return this.api.callWithAuthRetry(sessionMetadata.sessionId, async () => {\n      try {\n        await this.ensureCloudUser(sessionMetadata);\n\n        this.logger.debug('Switching user account', sessionMetadata);\n\n        const session = await this.sessionService.getSession(\n          sessionMetadata.sessionId,\n        );\n\n        await this.api.setCurrentAccount(session, +accountId);\n\n        return this.getCloudUser(sessionMetadata, true);\n      } catch (e) {\n        this.logger.error(\n          'Unable to switch current account',\n          e,\n          sessionMetadata,\n        );\n        throw wrapHttpError(e);\n      }\n    });\n  }\n\n  /**\n   * Update user data\n   * @param sessionMetadata\n   * @param data\n   */\n  async updateUser(\n    sessionMetadata: SessionMetadata,\n    data: Partial<CloudUser>,\n  ): Promise<CloudUser> {\n    return this.repository.update(sessionMetadata.sessionId, data);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/cloud-user.capi.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCloudAccountInfo,\n  mockCloudCapiAuthDto,\n  mockCloudUserCapiProvider,\n  MockType,\n} from 'src/__mocks__';\nimport { CloudUserCapiService } from 'src/modules/cloud/user/cloud-user.capi.service';\nimport { CloudUserCapiProvider } from 'src/modules/cloud/user/providers/cloud-user.capi.provider';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\n\ndescribe('CloudUserCapiService', () => {\n  let service: CloudUserCapiService;\n  let capiProvider: MockType<CloudUserCapiProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudUserCapiService,\n        {\n          provide: CloudUserCapiProvider,\n          useFactory: mockCloudUserCapiProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudUserCapiService);\n    capiProvider = module.get(CloudUserCapiProvider);\n  });\n\n  describe('getCurrentAccount', () => {\n    it('successfully get current account info', async () => {\n      expect(await service.getCurrentAccount(mockCloudCapiAuthDto)).toEqual(\n        mockCloudAccountInfo,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      capiProvider.getCurrentAccount.mockRejectedValueOnce(\n        new CloudApiUnauthorizedException(),\n      );\n      await expect(\n        service.getCurrentAccount(mockCloudCapiAuthDto),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/cloud-user.capi.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { wrapHttpError } from 'src/common/utils';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { CloudAccountInfo } from 'src/modules/cloud/user/models';\nimport { parseCloudAccountCapiResponse } from 'src/modules/cloud/user/utils';\nimport { CloudUserCapiProvider } from 'src/modules/cloud/user/providers/cloud-user.capi.provider';\n\n@Injectable()\nexport class CloudUserCapiService {\n  private logger = new Logger('CloudUserCapiService');\n\n  constructor(private readonly capi: CloudUserCapiProvider) {}\n\n  /**\n   * Get cloud account short info\n   * @param authDto\n   */\n  async getCurrentAccount(\n    authDto: CloudCapiAuthDto,\n  ): Promise<CloudAccountInfo> {\n    this.logger.debug('Getting cloud account.');\n    try {\n      const account = await this.capi.getCurrentAccount(authDto);\n\n      this.logger.debug('Succeed to get cloud account.');\n\n      return parseCloudAccountCapiResponse(account);\n    } catch (e) {\n      this.logger.error('Failed to get cloud account', e);\n      throw wrapHttpError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/cloud-user.controller.ts",
    "content": "import {\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  Param,\n  Put,\n  Query,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { ApiParam, ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { CloudUser } from 'src/modules/cloud/user/models';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudRequestUtm } from 'src/modules/cloud/common/models';\nimport { SessionMetadata } from 'src/common/models';\nimport { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service';\n\n@ApiTags('Cloud User')\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('cloud/me')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class CloudUserController {\n  constructor(\n    private readonly service: CloudUserApiService,\n    private readonly cloudAuthService: CloudAuthService,\n  ) {}\n\n  @Get('')\n  @ApiEndpoint({\n    description: 'Return user general info with accounts list',\n    statusCode: 200,\n    responses: [{ type: CloudUser }],\n  })\n  async me(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Query() utm: CloudRequestUtm,\n  ): Promise<CloudUser> {\n    return this.service.me(sessionMetadata, false, utm);\n  }\n\n  @Put('/accounts/:id/current')\n  @ApiEndpoint({\n    description: 'Activate user account',\n    statusCode: 200,\n    responses: [{ type: CloudUser }],\n  })\n  @ApiParam({ name: 'id', type: String })\n  async setCurrentAccount(\n    @Param('id') id: string,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<CloudUser> {\n    return this.service.setCurrentAccount(sessionMetadata, id);\n  }\n\n  @Get('logout')\n  @ApiEndpoint({\n    description: 'Logout user',\n    statusCode: 200,\n  })\n  async logout(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    return this.cloudAuthService.logout(sessionMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/cloud-user.module.ts",
    "content": "import { Global, Module } from '@nestjs/common';\nimport { CloudSessionModule } from 'src/modules/cloud/session/cloud-session.module';\nimport { CloudUserRepository } from 'src/modules/cloud/user/repositories/cloud-user.repository';\nimport { InSessionCloudUserRepository } from 'src/modules/cloud/user/repositories/in-session.cloud-user.repository';\nimport { CloudUserController } from 'src/modules/cloud/user/cloud-user.controller';\nimport { CloudUserCapiService } from 'src/modules/cloud/user/cloud-user.capi.service';\nimport { CloudUserCapiProvider } from 'src/modules/cloud/user/providers/cloud-user.capi.provider';\nimport { CloudUserApiProvider } from 'src/modules/cloud/user/providers/cloud-user.api.provider';\nimport { CloudUserApiService } from 'src/modules/cloud/user/cloud-user.api.service';\nimport { CloudAuthModule } from 'src/modules/cloud/auth/cloud-auth.module';\n\n@Global()\n@Module({\n  imports: [CloudSessionModule, CloudAuthModule],\n  providers: [\n    CloudUserApiProvider,\n    CloudUserCapiProvider,\n    CloudUserApiService,\n    CloudUserCapiService,\n    {\n      provide: CloudUserRepository,\n      useClass: InSessionCloudUserRepository,\n    },\n  ],\n  controllers: [CloudUserController],\n  exports: [CloudUserCapiService, CloudUserApiService],\n})\nexport class CloudUserModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/models/api.interface.ts",
    "content": "export interface ICloudApiUser {\n  id: string; // what difference between user_id?\n  current_account_id: string;\n  name: string;\n  email: string;\n  user_id: number;\n  role: string;\n}\n\nexport interface ICloudApiAccount {\n  id: number;\n  name: string;\n  api_access_key: string;\n}\n\nexport interface ICloudApiCsrfToken {\n  csrf_token: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/models/capi.interface.ts",
    "content": "export interface ICloudCapiAccountOwner {\n  name: string;\n  email: string;\n}\n\ninterface ICloudCapiAccountKey {\n  name: string;\n  accountId: number;\n  accountName: string;\n  allowedSourceIps: string[];\n  createdTimestamp: string;\n  owner: ICloudCapiAccountOwner;\n  httpSourceIp: string;\n}\n\nexport interface ICloudCapiAccount {\n  id: number;\n  name: string;\n  createdTimestamp: string;\n  updatedTimestamp: string;\n  key: ICloudCapiAccountKey;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/models/cloud-account-info.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class CloudAccountInfo {\n  @ApiProperty({\n    description: 'Account id',\n    type: Number,\n  })\n  accountId: number;\n\n  @ApiProperty({\n    description: 'Account name',\n    type: String,\n  })\n  accountName: string;\n\n  @ApiProperty({\n    description: 'Account owner name',\n    type: String,\n  })\n  ownerName: string;\n\n  @ApiProperty({\n    description: 'Account owner email',\n    type: String,\n  })\n  ownerEmail: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/models/cloud-user-account.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { TransformGroup } from 'src/common/constants';\n\nexport class CloudUserAccount {\n  @Expose()\n  id: number;\n\n  @Expose()\n  name: string;\n\n  @Expose({ groups: [TransformGroup.Secure] })\n  capiKey?: string; // api_access_key\n\n  @Expose({ groups: [TransformGroup.Secure] })\n  capiSecret?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/models/cloud-user.ts",
    "content": "import { Expose, Type } from 'class-transformer';\nimport { CloudUserAccount } from 'src/modules/cloud/user/models/cloud-user-account';\nimport { TransformGroup } from 'src/common/constants';\nimport { CloudCapiKey } from 'src/modules/cloud/capi-key/model';\n\nexport class CloudUser {\n  @Expose()\n  id?: number;\n\n  @Expose()\n  name?: string;\n\n  @Expose()\n  currentAccountId?: number;\n\n  @Type(() => CloudCapiKey)\n  @Expose({ groups: [TransformGroup.Secure] })\n  capiKey?: CloudCapiKey;\n\n  @Type(() => CloudUserAccount)\n  @Expose()\n  accounts?: CloudUserAccount[] = [];\n\n  @Expose()\n  data?: Record<string, any>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/models/index.ts",
    "content": "export * from './api.interface';\nexport * from './capi.interface';\nexport * from './cloud-user';\nexport * from './cloud-user-account';\nexport * from './cloud-account-info';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/providers/cloud-user.api.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudApiAuthDto,\n  mockCloudApiCsrfToken,\n  mockCloudApiHeaders,\n  mockCloudApiUser,\n  mockCloudCapiAccount,\n  mockCloudSession,\n  mockCloudSessionService,\n} from 'src/__mocks__';\nimport { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\nimport { CloudUserApiProvider } from 'src/modules/cloud/user/providers/cloud-user.api.provider';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudUserApiProvider', () => {\n  let service: CloudUserApiProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CloudUserApiProvider,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(CloudUserApiProvider);\n  });\n\n  describe('getCsrfToken', () => {\n    it('successfully get capi access key', async () => {\n      const response = {\n        status: 200,\n        data: { csrfToken: mockCloudApiCsrfToken },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(await service.getCsrfToken(mockCloudSession)).toEqual(\n        mockCloudApiCsrfToken.csrf_token,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith('csrf', mockCloudApiHeaders);\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.getCsrfToken(mockCloudSession)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n\n  describe('getApiSessionId', () => {\n    it('successfully get api session id (login to api)', async () => {\n      const response = {\n        status: 200,\n        headers: {\n          'set-cookie': [\n            `anything;JSESSIONID=${mockCloudApiAuthDto.apiSessionId};anything;`,\n          ],\n        },\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(await service.getApiSessionId(mockCloudSession)).toEqual(\n        mockCloudApiAuthDto.apiSessionId,\n      );\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        'login',\n        {\n          auth_mode: mockCloudSession.idpType,\n        },\n        {\n          ...mockCloudApiHeaders,\n        },\n      );\n    });\n    it('successfully get api session id (login to api) with utm parameters', async () => {\n      const response = {\n        status: 200,\n        headers: {\n          'set-cookie': [\n            `anything;JSESSIONID=${mockCloudApiAuthDto.apiSessionId};anything;`,\n          ],\n        },\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.getApiSessionId(mockCloudSession, {\n          source: 's',\n          medium: 'm',\n          campaign: 'c',\n        }),\n      ).toEqual(mockCloudApiAuthDto.apiSessionId);\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        'login',\n        {\n          auth_mode: mockCloudSession.idpType,\n          utm_source: 's',\n          utm_medium: 'm',\n          utm_campaign: 'c',\n        },\n        {\n          ...mockCloudApiHeaders,\n        },\n      );\n    });\n    it('successfully get api session id (login to api) with defined only utm parameters', async () => {\n      const response = {\n        status: 200,\n        headers: {\n          'set-cookie': [\n            `anything;JSESSIONID=${mockCloudApiAuthDto.apiSessionId};anything;`,\n          ],\n        },\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.getApiSessionId(mockCloudSession, { medium: 'm' }),\n      ).toEqual(mockCloudApiAuthDto.apiSessionId);\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        'login',\n        {\n          auth_mode: mockCloudSession.idpType,\n          utm_medium: 'm',\n        },\n        {\n          ...mockCloudApiHeaders,\n        },\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.getApiSessionId(mockCloudSession)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n\n  describe('getCurrentUser', () => {\n    it('successfully get current user', async () => {\n      const response = {\n        status: 200,\n        data: mockCloudApiUser,\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(await service.getCurrentUser(mockCloudSession)).toEqual(\n        mockCloudApiUser,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/users/me',\n        mockCloudApiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.getCurrentUser(mockCloudSession)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n\n  describe('getAccounts', () => {\n    it('successfully get user accounts', async () => {\n      const response = {\n        status: 200,\n        data: { accounts: [mockCloudCapiAccount] },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(await service.getAccounts(mockCloudSession)).toEqual([\n        mockCloudCapiAccount,\n      ]);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        '/accounts',\n        mockCloudApiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(service.getAccounts(mockCloudSession)).rejects.toThrow(\n        CloudApiUnauthorizedException,\n      );\n    });\n  });\n\n  describe('setCurrentAccount', () => {\n    it('successfully set current accounts', async () => {\n      const response = {\n        status: 200,\n        data: {},\n      };\n      mockedAxios.post.mockResolvedValue(response);\n\n      expect(\n        await service.setCurrentAccount(\n          mockCloudSession,\n          mockCloudCapiAccount.id,\n        ),\n      ).toEqual(undefined);\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        `/accounts/setcurrent/${mockCloudCapiAccount.id}`,\n        {},\n        mockCloudApiHeaders,\n      );\n    });\n    it('throw CloudApiUnauthorizedException exception', async () => {\n      mockedAxios.post.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.setCurrentAccount(mockCloudSession, mockCloudCapiAccount.id),\n      ).rejects.toThrow(CloudApiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/providers/cloud-user.api.provider.ts",
    "content": "import { get } from 'lodash';\nimport { Injectable } from '@nestjs/common';\nimport { ICloudApiAccount, ICloudApiUser } from 'src/modules/cloud/user/models';\nimport { wrapCloudApiError } from 'src/modules/cloud/common/exceptions';\nimport {\n  CloudRequestUtm,\n  ICloudApiCredentials,\n} from 'src/modules/cloud/common/models';\nimport { CloudApiProvider } from 'src/modules/cloud/common/providers/cloud.api.provider';\n\n@Injectable()\nexport class CloudUserApiProvider extends CloudApiProvider {\n  /**\n   * Login user to api using accessToken from oauth flow\n   * returns JSESSIONID\n   * @param credentials\n   * @private\n   */\n  async getCsrfToken(credentials: ICloudApiCredentials): Promise<string> {\n    try {\n      const { data } = await this.api.get(\n        'csrf',\n        CloudApiProvider.getHeaders(credentials),\n      );\n\n      return data?.csrfToken?.csrf_token;\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n\n  /**\n   * Login user to api using accessToken from oauth flow\n   * returns JSESSIONID\n   * @param credentials\n   * @param utm\n   * @private\n   */\n  async getApiSessionId(\n    credentials: ICloudApiCredentials,\n    utm?: CloudRequestUtm,\n  ): Promise<string> {\n    try {\n      const { headers } = await this.api.post(\n        'login',\n        {\n          ...CloudApiProvider.generateUtmBody(utm),\n          auth_mode: credentials?.idpType,\n        },\n        {\n          ...CloudApiProvider.getHeaders(credentials),\n        },\n      );\n\n      return get(headers, 'set-cookie', [])\n        .find((header) => header.indexOf('JSESSIONID=') > -1)\n        ?.match(/JSESSIONID=([^;]+)/)?.[1];\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n\n  /**\n   * Get current user profile\n   * @param credentials\n   */\n  async getCurrentUser(\n    credentials: ICloudApiCredentials,\n  ): Promise<ICloudApiUser> {\n    try {\n      const { data } = await this.api.get(\n        '/users/me',\n        CloudApiProvider.getHeaders(credentials),\n      );\n\n      return data;\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n\n  /**\n   * Fetch list of user accounts\n   * @param credentials\n   */\n  async getAccounts(\n    credentials: ICloudApiCredentials,\n  ): Promise<ICloudApiAccount[]> {\n    try {\n      const { data } = await this.api.get(\n        '/accounts',\n        CloudApiProvider.getHeaders(credentials),\n      );\n\n      return data?.accounts;\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n\n  /**\n   * Select current account to work with\n   * @param credentials\n   * @param accountId\n   */\n  async setCurrentAccount(\n    credentials: ICloudApiCredentials,\n    accountId: number,\n  ): Promise<void> {\n    try {\n      await this.api.post(\n        `/accounts/setcurrent/${accountId}`,\n        {},\n        CloudApiProvider.getHeaders(credentials),\n      );\n    } catch (e) {\n      throw wrapCloudApiError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/providers/cloud-user.capi.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockCapiUnauthorizedError,\n  mockCloudCapiAccount,\n  mockCloudCapiAuthDto,\n} from 'src/__mocks__';\nimport { CloudUserCapiProvider } from 'src/modules/cloud/user/providers/cloud-user.capi.provider';\nimport { CloudCapiUnauthorizedException } from 'src/modules/cloud/common/exceptions';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('CloudUserCapiProvider', () => {\n  let service: CloudUserCapiProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [CloudUserCapiProvider],\n    }).compile();\n\n    service = module.get(CloudUserCapiProvider);\n  });\n\n  describe('getCurrentAccount', () => {\n    it('successfully get cloud capi account', async () => {\n      const response = {\n        status: 200,\n        data: { account: mockCloudCapiAccount },\n      };\n      mockedAxios.get.mockResolvedValue(response);\n\n      expect(await service.getCurrentAccount(mockCloudCapiAuthDto)).toEqual(\n        mockCloudCapiAccount,\n      );\n    });\n    it('throw CloudCapiUnauthorizedException exception', async () => {\n      mockedAxios.get.mockRejectedValue(mockCapiUnauthorizedError);\n\n      await expect(\n        service.getCurrentAccount(mockCloudCapiAuthDto),\n      ).rejects.toThrow(CloudCapiUnauthorizedException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/providers/cloud-user.capi.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { wrapCloudCapiError } from 'src/modules/cloud/common/exceptions';\nimport { CloudCapiProvider } from 'src/modules/cloud/common/providers/cloud.capi.provider';\nimport { CloudCapiAuthDto } from 'src/modules/cloud/common/dto';\nimport { AxiosResponse } from 'axios';\nimport { ICloudCapiAccount } from 'src/modules/cloud/user/models';\n\n@Injectable()\nexport class CloudUserCapiProvider extends CloudCapiProvider {\n  /**\n   * Get cloud account short info\n   * @param authDto\n   */\n  async getCurrentAccount(\n    authDto: CloudCapiAuthDto,\n  ): Promise<ICloudCapiAccount> {\n    try {\n      const { data }: AxiosResponse = await this.api.get(\n        '/',\n        CloudCapiProvider.getHeaders(authDto),\n      );\n\n      return data?.account;\n    } catch (e) {\n      throw wrapCloudCapiError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/repositories/cloud-user.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { CloudUser } from 'src/modules/cloud/user/models';\n\n@Injectable()\nexport abstract class CloudUserRepository {\n  abstract get(userId: string): Promise<CloudUser>;\n  abstract update(userId: string, data: Partial<CloudUser>): Promise<CloudUser>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/repositories/in-session.cloud-user.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCloudSession,\n  mockCloudSessionService,\n  mockInitSession,\n  MockType,\n} from 'src/__mocks__';\nimport { InSessionCloudUserRepository } from 'src/modules/cloud/user/repositories/in-session.cloud-user.repository';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\n\ndescribe('InSessionCloudUserRepository', () => {\n  let service: InSessionCloudUserRepository;\n  let sessionService: MockType<CloudSessionService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        InSessionCloudUserRepository,\n        {\n          provide: CloudSessionService,\n          useFactory: mockCloudSessionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(InSessionCloudUserRepository);\n    sessionService = module.get(CloudSessionService);\n  });\n\n  describe('get', () => {\n    it('successfully get current user', async () => {\n      expect(await service.get(mockInitSession.id)).toEqual(\n        mockCloudSession.user,\n      );\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockInitSession.id,\n      );\n    });\n    it('should return null when there is no cloud session data', async () => {\n      sessionService.getSession.mockResolvedValueOnce(null);\n\n      expect(await service.get(mockInitSession.id)).toEqual(null);\n      expect(sessionService.getSession).toHaveBeenCalledWith(\n        mockInitSession.id,\n      );\n    });\n  });\n\n  describe('update', () => {\n    it('successfully update current user data', async () => {\n      await service.update(mockInitSession.id, {\n        name: 'new name',\n      });\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        {\n          user: {\n            ...mockCloudSession.user,\n            name: 'new name',\n          },\n        },\n      );\n    });\n    it('successfully update current user accounts (replace array data)', async () => {\n      const account = {\n        id: 9999,\n        name: 'new name',\n      };\n\n      await service.update(mockInitSession.id, {\n        accounts: [account],\n      });\n\n      expect(sessionService.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        {\n          user: {\n            ...mockCloudSession.user,\n            accounts: [account],\n          },\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/repositories/in-session.cloud-user.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { instanceToPlain, plainToInstance } from 'class-transformer';\nimport { CloudUserRepository } from 'src/modules/cloud/user/repositories/cloud-user.repository';\nimport { CloudUser } from 'src/modules/cloud/user/models';\nimport { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service';\nimport { TransformGroup } from 'src/common/constants';\n\n@Injectable()\nexport class InSessionCloudUserRepository extends CloudUserRepository {\n  constructor(private readonly sessionService: CloudSessionService) {\n    super();\n  }\n\n  /**\n   * Get user model from current session\n   * @param sessionId\n   */\n  async get(sessionId: string): Promise<CloudUser> {\n    const session = await this.sessionService.getSession(sessionId);\n\n    return (\n      plainToInstance(CloudUser, session?.user, {\n        groups: [TransformGroup.Secure],\n      }) || null\n    );\n  }\n\n  /**\n   * Update user data in session\n   * @param sessionId\n   * @param data\n   */\n  async update(\n    sessionId: string,\n    data: Partial<CloudUser>,\n  ): Promise<CloudUser> {\n    const user = await this.get(sessionId);\n    await this.sessionService.updateSessionData(sessionId, {\n      user: plainToInstance(\n        CloudUser,\n        {\n          ...instanceToPlain(user, { groups: [TransformGroup.Secure] }),\n          ...instanceToPlain(data, { groups: [TransformGroup.Secure] }),\n        },\n        { groups: [TransformGroup.Secure] },\n      ),\n    });\n\n    return this.get(sessionId);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/utils/cloud-data-converter.ts",
    "content": "import { get } from 'lodash';\nimport {\n  CloudAccountInfo,\n  ICloudCapiAccount,\n} from 'src/modules/cloud/user/models';\nimport { plainToInstance } from 'class-transformer';\n\nexport const parseCloudAccountCapiResponse = (\n  account: ICloudCapiAccount,\n): CloudAccountInfo =>\n  plainToInstance(CloudAccountInfo, {\n    accountId: account.id,\n    accountName: account.name,\n    ownerName: get(account, ['key', 'owner', 'name']),\n    ownerEmail: get(account, ['key', 'owner', 'email']),\n  });\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/utils/index.ts",
    "content": "export * from './cloud-data-converter';\nexport * from './token';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/utils/token.spec.ts",
    "content": "import { sign } from 'jsonwebtoken';\nimport { isValidToken } from './token';\n\ndescribe('isValidToken', () => {\n  it('should return false if no token has been provided', () => {\n    expect(isValidToken()).toEqual(false);\n  });\n\n  it('should be falsy if token has been expired', () => {\n    const expired = sign({ exp: Math.trunc(Date.now() / 1000) - 3600 }, 'test');\n    expect(isValidToken(expired)).toBe(false);\n  });\n\n  it('should return true if token has not been expired', () => {\n    const valid = sign({ exp: Math.trunc(Date.now() / 1000) + 3600 }, 'test');\n    expect(isValidToken(valid)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cloud/user/utils/token.ts",
    "content": "import config from 'src/utils/config';\n\nconst cloudConfig = config.get('cloud');\n\nexport const isValidToken = (token?: string) => {\n  if (!token) {\n    return false;\n  }\n\n  const { exp } = JSON.parse(\n    Buffer.from(token.split('.')[1], 'base64').toString(),\n  );\n\n  const expiresIn = exp * 1_000 - Date.now();\n\n  return expiresIn > cloudConfig.renewTokensBeforeExpire;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ClusterMonitorService } from 'src/modules/cluster-monitor/cluster-monitor.service';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ClusterDetails } from 'src/modules/cluster-monitor/models';\nimport { ClientMetadata } from 'src/common/models';\nimport { ClientMetadataParam } from 'src/common/decorators';\n\n@ApiTags('Cluster Monitor')\n@Controller('/cluster-details')\nexport class ClusterMonitorController {\n  constructor(private readonly clusterMonitorService: ClusterMonitorService) {}\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get cluster details',\n    responses: [\n      {\n        status: 200,\n        type: ClusterDetails,\n      },\n    ],\n  })\n  @Get()\n  async getClusterDetails(\n    @ClientMetadataParam({\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n  ): Promise<ClusterDetails> {\n    return this.clusterMonitorService.getClusterDetails(clientMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/cluster-monitor.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ClusterMonitorController } from 'src/modules/cluster-monitor/cluster-monitor.controller';\nimport { ClusterMonitorService } from 'src/modules/cluster-monitor/cluster-monitor.service';\n\n@Module({\n  providers: [ClusterMonitorService],\n  controllers: [ClusterMonitorController],\n})\nexport class ClusterMonitorModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts",
    "content": "import { get } from 'lodash';\nimport {\n  BadRequestException,\n  HttpException,\n  Injectable,\n  Logger,\n} from '@nestjs/common';\nimport { catchAclError } from 'src/utils';\nimport { IClusterInfo } from 'src/modules/cluster-monitor/strategies/cluster.info.interface';\nimport { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy';\nimport { ClusterShardsInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-shards.info.strategy';\nimport { ClusterDetails } from 'src/modules/cluster-monitor/models';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClientConnectionType } from 'src/modules/redis/client';\n\nexport enum ClusterInfoStrategies {\n  CLUSTER_NODES = 'CLUSTER_NODES',\n  CLUSTER_SHARDS = 'CLUSTER_SHARDS',\n}\n\n@Injectable()\nexport class ClusterMonitorService {\n  private logger = new Logger('ClusterMonitorService');\n\n  private infoStrategies: Map<string, IClusterInfo> = new Map();\n\n  constructor(private readonly databaseClientFactory: DatabaseClientFactory) {\n    this.infoStrategies.set(\n      ClusterInfoStrategies.CLUSTER_NODES,\n      new ClusterNodesInfoStrategy(),\n    );\n    this.infoStrategies.set(\n      ClusterInfoStrategies.CLUSTER_SHARDS,\n      new ClusterShardsInfoStrategy(),\n    );\n  }\n\n  /**\n   * Get cluster details and details for all nodes\n   * @param clientMetadata\n   */\n  public async getClusterDetails(\n    clientMetadata: ClientMetadata,\n  ): Promise<ClusterDetails> {\n    try {\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      if (client.getConnectionType() !== RedisClientConnectionType.CLUSTER) {\n        return Promise.reject(\n          new BadRequestException('Current database is not in a cluster mode'),\n        );\n      }\n\n      const info = await client.getInfo('server');\n\n      const strategy = this.getClusterInfoStrategy(\n        get(info, 'server.redis_version'),\n      );\n\n      return await strategy.getClusterDetails(client);\n    } catch (e) {\n      this.logger.error('Unable to get cluster details', e, clientMetadata);\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Return strategy on how we are going to fetch topology and other cluster info\n   * based on Redis version\n   * @param version\n   * @private\n   */\n  private getClusterInfoStrategy(version: string): IClusterInfo {\n    const intVersion = parseInt(version, 10) || 0;\n    if (intVersion >= 7) {\n      return this.infoStrategies.get(ClusterInfoStrategies.CLUSTER_SHARDS);\n    }\n\n    return this.infoStrategies.get(ClusterInfoStrategies.CLUSTER_NODES);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/models/cluster-details.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ClusterNodeDetails } from 'src/modules/cluster-monitor/models/cluster-node-details';\n\nexport class ClusterDetails {\n  @ApiProperty({\n    type: String,\n    description: 'Redis version',\n    example: '7.0.2',\n  })\n  version: string;\n\n  @ApiProperty({\n    type: String,\n    description:\n      'Redis mode. Currently one of: standalone, cluster or sentinel',\n    example: 'cluster',\n  })\n  mode: string;\n\n  @ApiProperty({\n    type: String,\n    description:\n      'Username from the connection or undefined in case when connected with default user',\n    example: 'user1',\n  })\n  user?: string;\n\n  @ApiProperty({\n    type: Number,\n    description: 'Maximum value uptime_in_seconds from all nodes',\n    example: 3600,\n  })\n  uptimeSec: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_state from CLUSTER INFO command',\n    example: 'ok',\n  })\n  state: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_slots_assigned from CLUSTER INFO command',\n    example: 16384,\n  })\n  slotsAssigned: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_slots_ok from CLUSTER INFO command',\n    example: 16384,\n  })\n  slotsOk: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_slots_pfail from CLUSTER INFO command',\n    example: 0,\n  })\n  slotsPFail: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_slots_fail from CLUSTER INFO command',\n    example: 0,\n  })\n  slotsFail: number;\n\n  @ApiProperty({\n    type: String,\n    description:\n      'Calculated from (16384 - cluster_slots_assigned from CLUSTER INFO command)',\n    example: 0,\n  })\n  slotsUnassigned: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_stats_messages_sent from CLUSTER INFO command',\n    example: 2451,\n  })\n  statsMessagesSent: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_stats_messages_received from CLUSTER INFO command',\n    example: 2451,\n  })\n  statsMessagesReceived: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_current_epoch from CLUSTER INFO command',\n    example: 6,\n  })\n  currentEpoch: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'cluster_my_epoch from CLUSTER INFO command',\n    example: 2,\n  })\n  myEpoch: number;\n\n  @ApiProperty({\n    type: String,\n    description: 'Number of shards. cluster_size from CLUSTER INFO command',\n    example: 3,\n  })\n  size: number;\n\n  @ApiProperty({\n    type: String,\n    description:\n      'All nodes number in the Cluster. cluster_known_nodes from CLUSTER INFO command',\n    example: 9,\n  })\n  knownNodes: number;\n\n  @ApiProperty({\n    type: () => ClusterNodeDetails,\n    isArray: true,\n    description: 'Details per each node',\n  })\n  nodes: ClusterNodeDetails[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/models/cluster-node-details.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport enum NodeRole {\n  Primary = 'primary',\n  Replica = 'replica',\n}\n\nexport enum HealthStatus {\n  Online = 'online',\n  Offline = 'offline',\n  Loading = 'loading',\n}\n\nexport class ClusterNodeDetails {\n  @ApiProperty({\n    type: String,\n    description: 'Node id',\n    example: 'c33218e9ff2faf8749bfb6585ba1e6d40a4e94fb',\n  })\n  id: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Redis version',\n    example: '7.0.2',\n  })\n  version: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Redis mode',\n    example: 'cluster',\n  })\n  mode: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Node IP address',\n    example: '172.30.0.101',\n  })\n  host: string;\n\n  @ApiProperty({\n    type: Number,\n    description: 'Node IP address',\n    example: 6379,\n  })\n  port: number;\n\n  @ApiProperty({\n    type: String,\n    enum: NodeRole,\n    description: 'Node role in cluster',\n  })\n  role: NodeRole;\n\n  @ApiProperty({\n    type: String,\n    description: 'ID of primary node (for replica only)',\n    example: 'c33218e9ff2faf8749bfb6585ba1e6d40a4e94fb',\n  })\n  primary?: string;\n\n  @ApiProperty({\n    type: String,\n    enum: HealthStatus,\n    description: \"Node's current health status\",\n  })\n  health: HealthStatus;\n\n  @ApiProperty({\n    type: String,\n    isArray: true,\n    description:\n      'Array of assigned slots or slots ranges. Shown for primary nodes only',\n    example: ['0-5638', '11256'],\n  })\n  slots?: string[];\n\n  @ApiProperty({\n    type: Number,\n    description: 'Total keys stored inside this node',\n    example: 256478,\n  })\n  totalKeys: number;\n\n  @ApiProperty({\n    type: Number,\n    description: 'Memory used by node. \"memory.used_memory\" from INFO command',\n    example: 256478,\n  })\n  usedMemory: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Current operations per second. \"stats.instantaneous_ops_per_sec\" from INFO command',\n    example: 12569,\n  })\n  opsPerSecond: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Total connections received by node. \"stats.total_connections_received\" from INFO command',\n    example: 3256,\n  })\n  connectionsReceived: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Currently connected clients. \"clients.connected_clients\" from INFO command',\n    example: 3256,\n  })\n  connectedClients: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Total commands processed by node. \"stats.total_commands_processed\" from INFO command',\n    example: 32560000000,\n  })\n  commandsProcessed: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Current input network usage in KB/s. \"stats.instantaneous_input_kbps\" from INFO command',\n    example: 12000,\n  })\n  networkInKbps: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Current output network usage in KB/s. \"stats.instantaneous_output_kbps\" from INFO command',\n    example: 12000,\n  })\n  networkOutKbps: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'Ratio for cache hits and misses [0 - 1]. Ideally should be close to 1',\n    example: 0.8,\n  })\n  cacheHitRatio?: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'The replication offset of this node. This information can be used to ' +\n      'send commands to the most up to date replicas.',\n    example: 12000,\n  })\n  replicationOffset: number;\n\n  @ApiProperty({\n    type: Number,\n    description:\n      'For replicas only. Determines on how much replica is behind of primary.',\n    example: 0,\n  })\n  replicationLag?: number;\n\n  @ApiProperty({\n    type: Number,\n    description: 'Current node uptime_in_seconds',\n    example: 12000,\n  })\n  uptimeSec: number;\n\n  @ApiProperty({\n    type: () => ClusterNodeDetails,\n    isArray: true,\n    description: 'For primary nodes only. Replica node(s) details',\n    example: [],\n  })\n  replicas?: ClusterNodeDetails[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/models/index.ts",
    "content": "export * from './cluster-details';\nexport * from './cluster-node-details';\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { set } from 'lodash';\nimport { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy';\nimport {\n  ClusterDetails,\n  ClusterNodeDetails,\n} from 'src/modules/cluster-monitor/models';\nimport {\n  mockClusterRedisClient,\n  mockStandaloneRedisClient,\n  mockStandaloneRedisInfoReply,\n} from 'src/__mocks__';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\n\nconst m1 = {\n  id: 'm1',\n  health: 'online',\n  host: '172.30.100.1',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['0-5000'],\n};\nconst m2 = {\n  id: 'm2',\n  health: 'online',\n  host: '172.30.100.4',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['5001-10921', '10922'],\n};\nconst m3 = {\n  id: 'm3',\n  health: 'online',\n  host: '172.30.100.7',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['10923-16383'],\n};\n\nconst node1 = Object.create(mockStandaloneRedisClient);\nset(node1, 'options', {\n  host: m1.host,\n  port: m1.port,\n});\n\nconst node2 = Object.create(mockStandaloneRedisClient);\nset(node2, 'options', {\n  host: m2.host,\n  port: m2.port,\n});\n\nconst clusterClient = mockClusterRedisClient;\nclusterClient.nodes.mockReturnValue([node1, node2]);\n\nconst mockClusterInfo: Partial<ClusterDetails> = {\n  state: 'ok',\n  slotsAssigned: 16374,\n  slotsOk: 16360,\n  slotsPFail: 10,\n  slotsFail: 4,\n  slotsUnassigned: 10,\n  statsMessagesSent: 1000,\n  statsMessagesReceived: 999,\n  knownNodes: 9,\n  size: 3,\n  myEpoch: 2,\n  currentEpoch: 6,\n};\n\nconst baseNodeDetails: Partial<ClusterNodeDetails> = {\n  cacheHitRatio: 1,\n  connectedClients: 1,\n  replicas: [],\n  replicationOffset: 0,\n  totalKeys: 1,\n  uptimeSec: 1000,\n  usedMemory: 1000000,\n  version: '6.0.5',\n  mode: 'standalone',\n};\n\nconst mockNode1Details = {\n  ...baseNodeDetails,\n  ...m1,\n} as ClusterNodeDetails;\n\nconst mockNode2Details = {\n  ...baseNodeDetails,\n  ...m2,\n} as ClusterNodeDetails;\n\nconst mockClusterDetails: Partial<ClusterDetails> = {\n  ...mockClusterInfo,\n  version: '6.0.5',\n  mode: 'standalone',\n  uptimeSec: 1000,\n  nodes: [mockNode1Details, mockNode2Details],\n};\n\nconst mockClusterInfoReply =\n  '' +\n  `cluster_state:${mockClusterInfo.state}\\r\\n` +\n  `cluster_slots_assigned:${mockClusterInfo.slotsAssigned}\\r\\n` +\n  `cluster_slots_ok:${mockClusterInfo.slotsOk}\\r\\n` +\n  `cluster_slots_pfail:${mockClusterInfo.slotsPFail}\\r\\n` +\n  `cluster_slots_fail:${mockClusterInfo.slotsFail}\\r\\n` +\n  `cluster_stats_messages_sent:${mockClusterInfo.statsMessagesSent}\\r\\n` +\n  `cluster_stats_messages_received:${mockClusterInfo.statsMessagesReceived}\\r\\n` +\n  `cluster_known_nodes:${mockClusterInfo.knownNodes}\\r\\n` +\n  `cluster_size:${mockClusterInfo.size}\\r\\n` +\n  `cluster_current_epoch:${mockClusterInfo.currentEpoch}\\r\\n` +\n  `cluster_my_epoch:${mockClusterInfo.myEpoch}\\r\\n`;\n\ndescribe('AbstractInfoStrategy', () => {\n  let service: ClusterNodesInfoStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ClusterNodesInfoStrategy],\n    }).compile();\n\n    service = module.get(ClusterNodesInfoStrategy);\n    service['getClusterNodesFromRedis'] = jest\n      .fn()\n      .mockResolvedValue([m1, m2, m3]);\n  });\n\n  describe('getClusterInfo', () => {\n    beforeEach(() => {\n      when(clusterClient.sendCommand).mockResolvedValue(mockClusterInfoReply);\n    });\n    it('should return cluster info', async () => {\n      const info = await ClusterNodesInfoStrategy.getClusterInfo(clusterClient);\n      expect(info).toEqual(mockClusterInfo);\n    });\n  });\n\n  describe('getClusterDetails', () => {\n    beforeEach(() => {\n      clusterClient.sendCommand.mockResolvedValue(mockClusterInfoReply);\n      node1.getInfo.mockResolvedValue(\n        convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n      );\n      node2.getInfo.mockResolvedValue(\n        convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n      );\n    });\n    it('should return cluster info', async () => {\n      const info = await service.getClusterDetails(clusterClient);\n      expect(info).toEqual(mockClusterDetails);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.ts",
    "content": "import { IClusterInfo } from 'src/modules/cluster-monitor/strategies/cluster.info.interface';\nimport { convertStringToNumber } from 'src/utils';\nimport { get, map, sum } from 'lodash';\nimport {\n  ClusterDetails,\n  ClusterNodeDetails,\n} from 'src/modules/cluster-monitor/models';\nimport { plainToInstance } from 'class-transformer';\nimport { convertMultilineReplyToObject } from 'src/modules/redis/utils';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport abstract class AbstractInfoStrategy implements IClusterInfo {\n  /**\n   * Get cluster detailed information\n   * with each node details and cluster topology\n   * @param client\n   */\n  async getClusterDetails(client: RedisClient): Promise<ClusterDetails> {\n    let clusterDetails = await AbstractInfoStrategy.getClusterInfo(client);\n\n    const redisClusterNodes = await this.getClusterNodesFromRedis(client);\n\n    const nodes = await this.getClusterNodesInfo(client, redisClusterNodes);\n\n    clusterDetails = {\n      ...clusterDetails,\n      ...AbstractInfoStrategy.calculateAdditionalClusterMetrics(client, nodes),\n      nodes: AbstractInfoStrategy.createClusterHierarchy(nodes),\n      version: get(nodes, '0.version'),\n      mode: get(nodes, '0.mode'),\n    };\n\n    return plainToInstance(ClusterDetails, clusterDetails);\n  }\n\n  /**\n   * Get array of ClusterNodeDetails\n   * @param client\n   * @param nodes\n   * @private\n   */\n  private async getClusterNodesInfo(\n    client: RedisClient,\n    nodes,\n  ): Promise<ClusterNodeDetails[]> {\n    const clientNodes = await client.nodes();\n    return await Promise.all(\n      nodes\n        .map((node) => {\n          const clientNode = clientNodes.find(\n            (n) =>\n              (n.options?.host === node.host &&\n                n.options?.port === node.port) ||\n              (n.options?.natHost === node.host &&\n                n.options?.natPort === node.port),\n          );\n\n          if (clientNode) {\n            return this.getClusterNodeInfo(clientNode, node);\n          }\n\n          return undefined;\n        })\n        .filter((n) => n),\n    );\n  }\n\n  /**\n   * Get info (ClusterNodeDetails) for particular node + some extra fields\n   * which will be ignored on the later stage\n   * @param nodeClient\n   * @param node\n   * @private\n   */\n  private async getClusterNodeInfo(\n    nodeClient: RedisClient,\n    node,\n  ): Promise<ClusterNodeDetails> {\n    const info = await nodeClient.getInfo();\n\n    return {\n      ...node,\n      totalKeys: sum(\n        map(get(info, 'keyspace', {}), (dbKeys): number => {\n          const { keys } = convertMultilineReplyToObject(dbKeys, ',', '=');\n          return parseInt(keys, 10);\n        }),\n      ),\n      usedMemory: convertStringToNumber(get(info, 'memory.used_memory')),\n      opsPerSecond: convertStringToNumber(\n        get(info, 'stats.instantaneous_ops_per_sec'),\n      ),\n      connectionsReceived: convertStringToNumber(\n        get(info, 'stats.total_connections_received'),\n      ),\n      connectedClients: convertStringToNumber(\n        get(info, 'clients.connected_clients'),\n      ),\n      commandsProcessed: convertStringToNumber(\n        get(info, 'stats.total_commands_processed'),\n      ),\n      networkInKbps: convertStringToNumber(\n        get(info, 'stats.instantaneous_input_kbps'),\n      ),\n      networkOutKbps: convertStringToNumber(\n        get(info, 'stats.instantaneous_output_kbps'),\n      ),\n      cacheHitRatio: AbstractInfoStrategy.calculateCacheHitRatio(\n        convertStringToNumber(get(info, 'stats.keyspace_hits'), 0),\n        convertStringToNumber(get(info, 'stats.keyspace_misses'), 0),\n      ),\n      replicationOffset: convertStringToNumber(\n        get(info, 'replication.master_repl_offset'),\n      ),\n      uptimeSec: convertStringToNumber(\n        get(info, 'server.uptime_in_seconds'),\n        0,\n      ),\n      version: get(info, 'server.redis_version'),\n      mode: get(info, 'server.redis_mode'),\n    };\n  }\n\n  /**\n   * Get bunch of fields from CLUSTER INFO command\n   * @param client\n   */\n  static async getClusterInfo(\n    client: RedisClient,\n  ): Promise<Partial<ClusterDetails>> {\n    const info = convertMultilineReplyToObject(\n      (await client.sendCommand(['cluster', 'info'], {\n        replyEncoding: 'utf8',\n      })) as string,\n    );\n\n    const slotsState = {\n      slotsAssigned: convertStringToNumber(info.cluster_slots_assigned, 0),\n      slotsOk: convertStringToNumber(info.cluster_slots_ok, 0),\n      slotsPFail: convertStringToNumber(info.cluster_slots_pfail, 0),\n      slotsFail: convertStringToNumber(info.cluster_slots_fail, 0),\n    };\n\n    return {\n      state: info.cluster_state,\n      ...slotsState,\n      slotsUnassigned: 16384 - slotsState.slotsAssigned,\n      statsMessagesSent: convertStringToNumber(\n        info.cluster_stats_messages_sent,\n        0,\n      ),\n      statsMessagesReceived: convertStringToNumber(\n        info.cluster_stats_messages_received,\n        0,\n      ),\n      currentEpoch: convertStringToNumber(info.cluster_current_epoch, 0),\n      myEpoch: convertStringToNumber(info.cluster_my_epoch, 0),\n      size: convertStringToNumber(info.cluster_size, 0),\n      knownNodes: convertStringToNumber(info.cluster_known_nodes, 0),\n    };\n  }\n\n  /**\n   * Create cluster's topology and calculate primary/slave related metric such as replicationLag\n   * @param nodes\n   */\n  static createClusterHierarchy(nodes): ClusterNodeDetails[] {\n    const primaryNodes = {};\n\n    // get primary nodes\n    nodes.forEach((node) => {\n      if (node.role === 'primary') {\n        primaryNodes[node.id] = {\n          ...node,\n          replicas: [],\n        };\n      }\n    });\n\n    // assign replicas to primary nodes\n    // also calculate replicationLag\n    nodes.forEach((node) => {\n      if (node.primary && primaryNodes[node.primary]) {\n        const replicationLag =\n          primaryNodes[node.primary].replicationOffset - node.replicationOffset;\n        primaryNodes[node.primary].replicas.push({\n          ...node,\n          replicationLag: replicationLag > -1 ? replicationLag : 0,\n        });\n      }\n    });\n\n    return Object.values(primaryNodes);\n  }\n\n  /**\n   * Calculate hit ratio based on hits and misses values\n   * Will not fail in case of an error\n   * @param hits\n   * @param misses\n   */\n  static calculateCacheHitRatio(hits: number, misses: number): number {\n    try {\n      const cacheHitRate = hits / (hits + misses);\n      return cacheHitRate >= 0 ? cacheHitRate : null;\n    } catch (e) {\n      // ignore error\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Calculate additional cluster metrics based on current connection and nodes details\n   * @param client\n   * @param nodes\n   */\n  static calculateAdditionalClusterMetrics(\n    client: RedisClient,\n    nodes: ClusterNodeDetails[],\n  ): Partial<ClusterDetails> {\n    const additionalDetails: Partial<ClusterDetails> = {\n      user: get(client, 'options.redisOptions.username'),\n      uptimeSec: 0,\n    };\n\n    nodes.forEach((node) => {\n      if (additionalDetails.uptimeSec < node.uptimeSec) {\n        additionalDetails.uptimeSec = node.uptimeSec;\n      }\n    });\n\n    return additionalDetails;\n  }\n\n  abstract getClusterNodesFromRedis(client: RedisClient);\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy';\nimport { mockClusterRedisClient } from 'src/__mocks__';\n\nconst clusterClient = mockClusterRedisClient;\n\nconst m1 = {\n  id: 'm1',\n  health: 'online',\n  host: '172.30.100.1',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['0-5000'],\n};\nconst m2 = {\n  id: 'm2',\n  health: 'loading',\n  host: '172.30.100.4',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['5001-10921', '10922'],\n};\nconst m3 = {\n  id: 'm3',\n  health: 'offline',\n  host: '172.30.100.7',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['10923-16383'],\n};\n\nconst mockClusterNodesReply =\n  '' +\n  'm1 172.30.100.1:6379@16379 master - 0 1661415706000 2 connected 0-5000\\n' +\n  's11 172.30.100.2:6379@16379 slave m1 0 1661415705000 3 connected\\n' +\n  's12 172.30.100.3:6379@16379 slave m1 0 1661415705000 3 connected\\n' +\n  'm2 172.30.100.4:6379@16379 myself,pfail - 0 1661415702000 1 connected 5001-10921 10922\\n' +\n  's21 172.30.100.5:6379@16379 slave m2 0 1661415704000 2 connected\\n' +\n  's22 172.30.100.6:6379@16379 slave m2 0 1661415705230 2 connected\\n' +\n  'm3 172.30.100.7:6379@16379 master,fail - 0 1661415702000 1 connected 10923-16383\\n' +\n  's31 172.30.100.8:6379@16379 slave m3 0 1661415704000 2 connected\\n' +\n  's32 172.30.100.9:6379@16379 slave m3 0 1661415705230 2 connected\\n';\n\ndescribe('ClusterNodesInfoStrategy', () => {\n  let service: ClusterNodesInfoStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ClusterNodesInfoStrategy],\n    }).compile();\n\n    service = module.get(ClusterNodesInfoStrategy);\n  });\n\n  describe('getClusterNodesFromRedis', () => {\n    beforeEach(() => {\n      when(clusterClient.sendCommand).mockResolvedValue(mockClusterNodesReply);\n    });\n    it('should return cluster info', async () => {\n      const info = await service.getClusterNodesFromRedis(clusterClient);\n      expect(info).toEqual([m1, m2, m3]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.ts",
    "content": "import { AbstractInfoStrategy } from 'src/modules/cluster-monitor/strategies/abstract.info.strategy';\nimport {\n  ClusterNodeDetails,\n  HealthStatus,\n  NodeRole,\n} from 'src/modules/cluster-monitor/models';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class ClusterNodesInfoStrategy extends AbstractInfoStrategy {\n  async getClusterNodesFromRedis(\n    client: RedisClient,\n  ): Promise<Partial<ClusterNodeDetails>[]> {\n    const resp = (await client.sendCommand(['cluster', 'nodes'], {\n      replyEncoding: 'utf8',\n    })) as string;\n\n    return resp\n      .split('\\n')\n      .filter((e) => e)\n      .map((nodeString) => {\n        const [id, endpoint, flags, primary, , , , , ...slots] =\n          nodeString.split(' ');\n        const [host, ports] = endpoint.split(':');\n        const [port] = ports.split('@');\n        return {\n          id,\n          host,\n          port: parseInt(port, 10),\n          role:\n            primary && primary !== '-' ? NodeRole.Replica : NodeRole.Primary,\n          primary: primary && primary !== '-' ? primary : undefined,\n          slots: slots?.length ? slots : undefined,\n          health: ClusterNodesInfoStrategy.determineNodeHealth(flags),\n        };\n      })\n      .filter((node) => node.role === NodeRole.Primary); // tmp work with primary nodes only;\n  }\n\n  static determineNodeHealth(flags: string): HealthStatus {\n    if (flags.indexOf('fail') > -1 && flags.indexOf('pfail') < 0) {\n      return HealthStatus.Offline;\n    }\n\n    if (flags.indexOf('master') > -1 || flags.indexOf('slave') > -1) {\n      return HealthStatus.Online;\n    }\n\n    return HealthStatus.Loading;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { ClusterShardsInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-shards.info.strategy';\nimport { mockClusterRedisClient } from 'src/__mocks__';\n\nconst clusterClient = mockClusterRedisClient;\n\nconst m1 = {\n  id: 'm1',\n  health: 'online',\n  host: '172.30.100.1',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['0-5000'],\n};\nconst m2 = {\n  id: 'm2',\n  health: 'loading',\n  host: '172.30.100.4',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['5001-10921', '10922'],\n};\nconst m3 = {\n  id: 'm3',\n  health: 'offline',\n  host: '172.30.100.7',\n  primary: undefined,\n  port: 6379,\n  role: 'primary',\n  slots: ['10923-16383'],\n};\n\nconst mockClusterShardsReply = [\n  [\n    'slots',\n    [0, 5000],\n    'nodes',\n    [\n      [\n        'id',\n        'm1',\n        'port',\n        6379,\n        'ip',\n        '172.30.100.1',\n        'endpoint',\n        '172.30.100.212',\n        'hostname',\n        '',\n        'role',\n        'master',\n        'replication-offset',\n        107870,\n        'health',\n        'online',\n      ],\n      [\n        'id',\n        's11',\n        'port',\n        6379,\n        'ip',\n        '172.30.100.2',\n        'endpoint',\n        '172.30.100.212',\n        'hostname',\n        '',\n        'role',\n        'slave',\n        'replication-offset',\n        107870,\n        'health',\n        'online',\n      ],\n    ],\n  ],\n  [\n    'slots',\n    [5001, 10921, 10922, 10922],\n    'nodes',\n    [\n      [\n        'id',\n        'm2',\n        'port',\n        6379,\n        'ip',\n        '172.30.100.4',\n        'endpoint',\n        '172.30.100.212',\n        'hostname',\n        '',\n        'role',\n        'master',\n        'replication-offset',\n        107870,\n        'health',\n        'loading',\n      ],\n      [\n        'id',\n        's21',\n        'port',\n        6379,\n        'ip',\n        '172.30.100.5',\n        'endpoint',\n        '172.30.100.212',\n        'hostname',\n        '',\n        'role',\n        'slave',\n        'replication-offset',\n        107870,\n        'health',\n        'online',\n      ],\n    ],\n  ],\n  [\n    'slots',\n    [10923, 16383],\n    'nodes',\n    [\n      [\n        'id',\n        'm3',\n        'port',\n        6379,\n        'ip',\n        '172.30.100.7',\n        'endpoint',\n        '172.30.100.212',\n        'hostname',\n        '',\n        'role',\n        'master',\n        'replication-offset',\n        107870,\n        'health',\n        'offline',\n      ],\n      [\n        'id',\n        's31',\n        'port',\n        6379,\n        'ip',\n        '172.30.100.8',\n        'endpoint',\n        '172.30.100.212',\n        'hostname',\n        '',\n        'role',\n        'slave',\n        'replication-offset',\n        107870,\n        'health',\n        'online',\n      ],\n    ],\n  ],\n];\n\ndescribe('ClusterShardsInfoStrategy', () => {\n  let service: ClusterShardsInfoStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [ClusterShardsInfoStrategy],\n    }).compile();\n\n    service = module.get(ClusterShardsInfoStrategy);\n  });\n\n  describe('getClusterNodesFromRedis', () => {\n    beforeEach(() => {\n      when(clusterClient.sendCommand).mockResolvedValue(mockClusterShardsReply);\n    });\n    it('should return cluster info', async () => {\n      const info = await service.getClusterNodesFromRedis(clusterClient);\n      expect(info).toEqual([m1, m2, m3]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.ts",
    "content": "import { chunk } from 'lodash';\nimport { AbstractInfoStrategy } from 'src/modules/cluster-monitor/strategies/abstract.info.strategy';\nimport {\n  ClusterNodeDetails,\n  NodeRole,\n} from 'src/modules/cluster-monitor/models';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class ClusterShardsInfoStrategy extends AbstractInfoStrategy {\n  async getClusterNodesFromRedis(client: RedisClient) {\n    const resp = (await client.sendCommand(['cluster', 'shards'], {\n      replyEncoding: 'utf8',\n    })) as any[];\n\n    return [].concat(\n      ...resp.map((shardArray) => {\n        const shard = convertArrayReplyToObject(shardArray);\n        const slots = ClusterShardsInfoStrategy.calculateSlots(shard.slots);\n        return ClusterShardsInfoStrategy.processShardNodes(shard.nodes, slots);\n      }),\n    );\n  }\n\n  static calculateSlots(slots: number[]): string[] {\n    return chunk(slots, 2).map(([slot1, slot2]) => {\n      if (slot1 === slot2) {\n        return `${slot1}`;\n      }\n\n      return `${slot1}-${slot2}`;\n    });\n  }\n\n  static processShardNodes(\n    shardNodes: any[],\n    slots: string[],\n  ): Partial<ClusterNodeDetails>[] {\n    let primary;\n    const nodes = shardNodes.map((nodeArray) => {\n      const nodeObj = convertArrayReplyToObject(nodeArray);\n      const node = {\n        id: nodeObj.id,\n        host: nodeObj.ip,\n        port: nodeObj.port || nodeObj['tls-port'],\n        tlsPort: nodeObj['tls-port'],\n        role: nodeObj.role === 'master' ? NodeRole.Primary : NodeRole.Replica,\n        health: nodeObj.health,\n      };\n\n      if (node.role === 'primary') {\n        primary = node.id;\n        node['slots'] = slots;\n      }\n\n      return node;\n    });\n\n    return nodes\n      .map((node) => {\n        if (node.role !== NodeRole.Primary) {\n          return {\n            ...node,\n            primary,\n          };\n        }\n\n        return node;\n      })\n      .filter((node) => node.role === NodeRole.Primary); // tmp work with primary nodes only\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/cluster-monitor/strategies/cluster.info.interface.ts",
    "content": "import { ClusterDetails } from 'src/modules/cluster-monitor/models';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport interface IClusterInfo {\n  getClusterDetails(client: RedisClient): Promise<ClusterDetails>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/commands/commands-json.provider.spec.ts",
    "content": "import axios from 'axios';\nimport * as fs from 'fs-extra';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { mockMainCommands, mockRedijsonCommands } from 'src/__mocks__';\nimport { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider';\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\njest.mock('fs-extra');\nconst mockedFs = fs as jest.Mocked<typeof fs>;\n\ndescribe('CommandsJsonProvider', () => {\n  let service: CommandsJsonProvider;\n\n  beforeEach(async () => {\n    jest.mock('fs-extra', () => mockedFs);\n\n    mockedAxios.get.mockResolvedValue({\n      data: JSON.stringify(mockMainCommands),\n    });\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: 'service',\n          useFactory: () => new CommandsJsonProvider('name', 'someurl'),\n        },\n      ],\n    }).compile();\n\n    service = module.get('service');\n  });\n\n  describe('updateLatestJson', () => {\n    it('should not fail when incorrect data retrieved', async () => {\n      mockedAxios.get.mockResolvedValueOnce('json');\n      await service.updateLatestJson();\n\n      expect(mockedFs.writeFile).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('getCommands', () => {\n    it('should return default config when file was not found', async () => {\n      mockedFs.readFile.mockRejectedValueOnce(new Error('No file'));\n      mockedFs.readFile.mockResolvedValueOnce(\n        Buffer.from(JSON.stringify(mockMainCommands)),\n      );\n\n      expect(await service.getCommands()).toEqual({ name: mockMainCommands });\n    });\n    it('should return default config when incorrect json received from file', async () => {\n      mockedFs.readFile.mockResolvedValueOnce(Buffer.from('incorrect json'));\n      mockedFs.readFile.mockResolvedValueOnce(\n        Buffer.from(JSON.stringify(mockMainCommands)),\n      );\n\n      expect(await service.getCommands()).toEqual({ name: mockMainCommands });\n    });\n    it('should return latest commands', async () => {\n      mockedFs.readFile.mockResolvedValue(\n        Buffer.from(JSON.stringify(mockRedijsonCommands)),\n      );\n\n      expect(await service.getCommands()).toEqual({\n        name: mockRedijsonCommands,\n      });\n    });\n  });\n\n  describe('getDefaultCommands', () => {\n    it('should return empty object when file was not found', async () => {\n      mockedFs.readFile.mockRejectedValue(new Error('No file'));\n\n      expect(await service.getDefaultCommands()).toEqual({});\n    });\n    it('should return empty object when incorrect json received from file', async () => {\n      mockedFs.readFile.mockResolvedValue(Buffer.from('incorrect json'));\n\n      expect(await service.getDefaultCommands()).toEqual({});\n    });\n    it('should return default commands', async () => {\n      mockedFs.readFile.mockResolvedValue(\n        Buffer.from(JSON.stringify(mockRedijsonCommands)),\n      );\n\n      expect(await service.getDefaultCommands()).toEqual({\n        name: mockRedijsonCommands,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/commands/commands-json.provider.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport axios from 'axios';\nimport * as fs from 'fs-extra';\nimport * as path from 'path';\nimport config from 'src/utils/config';\n\nconst PATH_CONFIG = config.get('dir_path');\n\n@Injectable()\nexport class CommandsJsonProvider {\n  private readonly logger: Logger;\n\n  private readonly name: string;\n\n  private readonly url: string;\n\n  constructor(name, url) {\n    this.name = name;\n    this.url = url;\n    this.logger = new Logger(`CommandsJsonProvider:${this.name}`);\n  }\n\n  /**\n   * Get latest json from external resource and save it locally\n   */\n  async updateLatestJson() {\n    try {\n      this.logger.debug(`Trying to update ${this.name} commands...`);\n      const { data } = await axios.get(this.url, {\n        responseType: 'text',\n        transformResponse: [(raw) => raw],\n      });\n\n      await fs.ensureDir(PATH_CONFIG.commands);\n\n      await fs.writeFile(\n        path.join(PATH_CONFIG.commands, `${this.name}.json`),\n        JSON.stringify(JSON.parse(data)), // check that we received proper json object\n      );\n      this.logger.debug(`Successfully updated ${this.name} commands`);\n    } catch (error) {\n      this.logger.error(`Unable to update ${this.name} commands`, error);\n    }\n  }\n\n  /**\n   * Try to return latest commands\n   * In case of any errors will return default one\n   */\n  async getCommands() {\n    try {\n      return {\n        [this.name]: JSON.parse(\n          await fs.readFile(\n            path.join(PATH_CONFIG.commands, `${this.name}.json`),\n            'utf8',\n          ),\n        ),\n      };\n    } catch (error) {\n      this.logger.warn(\n        `Unable to get latest ${this.name} commands. Return default.`,\n        error,\n      );\n      return this.getDefaultCommands();\n    }\n  }\n\n  /**\n   * Try to get default json that was delivered with build\n   * In case when no default data we will return empty object to not fail api call\n   */\n  async getDefaultCommands() {\n    try {\n      return {\n        [this.name]: JSON.parse(\n          await fs.readFile(\n            path.join(PATH_CONFIG.defaultCommandsDir, `${this.name}.json`),\n            'utf8',\n          ),\n        ),\n      };\n    } catch (error) {\n      this.logger.error(`Unable to get default ${this.name} commands.`, error);\n      return {};\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/commands/commands.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { ApiTags } from '@nestjs/swagger';\n\n@ApiTags('Commands')\n@Controller('commands')\nexport class CommandsController {\n  constructor(private readonly commandsService: CommandsService) {}\n\n  @Get()\n  async getAll(): Promise<Record<string, any>> {\n    return this.commandsService.getAll();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/commands/commands.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { CommandsController } from 'src/modules/commands/commands.controller';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider';\nimport config from 'src/utils/config';\n\nconst COMMANDS_CONFIGS = config.get('commands');\n\n@Module({\n  controllers: [CommandsController],\n  providers: [\n    {\n      provide: CommandsService,\n      useFactory: () =>\n        new CommandsService(\n          COMMANDS_CONFIGS.map(\n            ({ name, url }) => new CommandsJsonProvider(name, url),\n          ),\n        ),\n    },\n  ],\n  exports: [CommandsService],\n})\nexport class CommandsModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/commands/commands.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport {\n  mockCommandsJsonProvider,\n  mockMainCommands,\n  mockRedijsonCommands,\n  mockRedisearchCommands,\n  mockRedisgraphCommands,\n  mockRedistimeseriesCommands,\n  MockType,\n} from 'src/__mocks__';\nimport { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider';\n\ndescribe('CommandsService', () => {\n  let service: CommandsService;\n\n  const mainCommandsProvider: MockType<CommandsJsonProvider> =\n    mockCommandsJsonProvider();\n  const redisearchCommandsProvider: MockType<CommandsJsonProvider> =\n    mockCommandsJsonProvider();\n  const redijsonCommandsProvider: MockType<CommandsJsonProvider> =\n    mockCommandsJsonProvider();\n  const redistimeseriesCommandsProvider: MockType<CommandsJsonProvider> =\n    mockCommandsJsonProvider();\n  const redisgraphCommandsProvider: MockType<CommandsJsonProvider> =\n    mockCommandsJsonProvider();\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const commandsProviders = [\n      mainCommandsProvider,\n      redisearchCommandsProvider,\n      redijsonCommandsProvider,\n      redistimeseriesCommandsProvider,\n      redisgraphCommandsProvider,\n    ];\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: CommandsService,\n          // @ts-ignore\n          useFactory: () => new CommandsService(commandsProviders),\n        },\n      ],\n    }).compile();\n\n    service = module.get(CommandsService);\n\n    mainCommandsProvider.getCommands.mockResolvedValue({\n      main: mockMainCommands,\n    });\n    redisearchCommandsProvider.getCommands.mockResolvedValue({\n      search: mockRedisearchCommands,\n    });\n    redijsonCommandsProvider.getCommands.mockResolvedValue({\n      json: mockRedijsonCommands,\n    });\n    redistimeseriesCommandsProvider.getCommands.mockResolvedValue({\n      timeseries: mockRedistimeseriesCommands,\n    });\n    redisgraphCommandsProvider.getCommands.mockResolvedValue({\n      graph: mockRedisgraphCommands,\n    });\n  });\n\n  describe('onModuleInit', () => {\n    it('should trigger updateLatestJson function', async () => {\n      await service.onModuleInit();\n\n      expect(mainCommandsProvider.updateLatestJson).toHaveBeenCalled();\n      expect(redisearchCommandsProvider.updateLatestJson).toHaveBeenCalled();\n      expect(redijsonCommandsProvider.updateLatestJson).toHaveBeenCalled();\n      expect(\n        redistimeseriesCommandsProvider.updateLatestJson,\n      ).toHaveBeenCalled();\n      expect(redisgraphCommandsProvider.updateLatestJson).toHaveBeenCalled();\n    });\n  });\n\n  describe('getAll', () => {\n    it('Should return merged commands into one', async () => {\n      expect(await service.getAll()).toEqual({\n        ...mockRedisearchCommands,\n        ...mockRedijsonCommands,\n        ...mockRedistimeseriesCommands,\n        ...mockRedisgraphCommands,\n        ...mockMainCommands,\n      });\n    });\n  });\n\n  describe('getCommandsGroups', () => {\n    it('Should return commands groups', async () => {\n      expect(await service.getCommandsGroups()).toEqual({\n        search: { ...mockRedisearchCommands },\n        json: { ...mockRedijsonCommands },\n        timeseries: { ...mockRedistimeseriesCommands },\n        graph: { ...mockRedisgraphCommands },\n        main: { ...mockMainCommands },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/commands/commands.service.ts",
    "content": "import { assign, forEach } from 'lodash';\nimport { Injectable, OnModuleInit } from '@nestjs/common';\nimport { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider';\n\n// 5 min\nconst COMMANDS_TTL = 300000;\n\n@Injectable()\nexport class CommandsService implements OnModuleInit {\n  private commandsProviders;\n\n  private commandsGroups;\n\n  private timer;\n\n  constructor(commandsProviders: CommandsJsonProvider[] = []) {\n    this.commandsProviders = commandsProviders;\n  }\n\n  /**\n   * Updates latest jsons on startup\n   */\n  async onModuleInit() {\n    // async operation to not wait for it and not block user in case when no internet connection\n    Promise.all(\n      this.commandsProviders.map((provider) => provider.updateLatestJson()),\n    );\n  }\n\n  /**\n   * Get all commands merged into single object\n   */\n  async getAll(): Promise<any> {\n    const commands = {};\n\n    Object.entries(await this.getCommandsGroups()).forEach(\n      ([provider, groupCommands]) => {\n        return forEach(groupCommands as {}, (value: {}, command) => {\n          commands[command] = { ...value, provider };\n        });\n      },\n    );\n\n    return commands;\n  }\n\n  async getCommandsGroups(): Promise<any> {\n    if (!!this.timer && this.timer + COMMANDS_TTL > new Date().getTime()) {\n      return this.commandsGroups;\n    }\n    this.commandsGroups = assign(\n      {},\n      ...(await Promise.all(\n        this.commandsProviders.map((provider) => provider.getCommands()),\n      )),\n    );\n    this.timer = new Date().getTime();\n    return this.commandsGroups;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/constants/constants.module.ts",
    "content": "import { DynamicModule, Global, Type } from '@nestjs/common';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { LocalConstantsProvider } from 'src/modules/constants/providers/local.constants.provider';\n\n@Global()\nexport class ConstantsModule {\n  static register(\n    provider: Type<ConstantsProvider> = LocalConstantsProvider,\n  ): DynamicModule {\n    return {\n      module: ConstantsModule,\n      providers: [\n        {\n          provide: ConstantsProvider,\n          useClass: provider,\n        },\n      ],\n      exports: [ConstantsProvider],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/constants/providers/constants.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport abstract class ConstantsProvider {\n  /**\n   * Should return hardcoded SessionMetadata for cases when it is not possible to determine the user\n   * For example onModuleInit step or in some automatic actions generated by system\n   */\n  abstract getSystemSessionMetadata(): SessionMetadata;\n\n  /**\n   * Should return generated anonymous id based on sessionMetadata or default value\n   * @param sessionMetadata\n   */\n  abstract getAnonymousId(sessionMetadata?: SessionMetadata): string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/constants/providers/local.constants.provider.ts",
    "content": "import { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  DEFAULT_ACCOUNT_ID,\n  DEFAULT_SESSION_ID,\n  DEFAULT_USER_ID,\n} from 'src/common/constants';\n\nexport class LocalConstantsProvider extends ConstantsProvider {\n  /**\n   * @inheritDoc\n   */\n  getSystemSessionMetadata(): SessionMetadata {\n    return {\n      userId: DEFAULT_USER_ID,\n      accountId: DEFAULT_ACCOUNT_ID,\n      sessionId: DEFAULT_SESSION_ID,\n    };\n  }\n\n  /**\n   * @inheritDoc\n   */\n  getAnonymousId(sessionMetadata?: SessionMetadata): string {\n    return sessionMetadata?.userId ?? 'unknown';\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/custom-tutorial.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { CustomTutorialAnalytics } from 'src/modules/custom-tutorial/custom-tutorial.analytics';\nimport { BadRequestException } from '@nestjs/common';\nimport { mockSessionMetadata } from 'src/__mocks__';\n\ndescribe('CustomTutorialAnalytics', () => {\n  let service: CustomTutorialAnalytics;\n  let sendEventSpy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, CustomTutorialAnalytics],\n    }).compile();\n\n    service = await module.get(CustomTutorialAnalytics);\n    sendEventSpy = jest.spyOn(service as any, 'sendEvent');\n  });\n\n  describe('sendImportSucceeded', () => {\n    it('should emit succeed event with manifest \"yes\"', () => {\n      service.sendImportSucceeded(mockSessionMetadata, { manifest: true });\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchEnablementAreaImportSucceeded,\n        {\n          manifest: 'yes',\n        },\n      );\n    });\n    it('should emit succeed event with manifest \"no\"', () => {\n      service.sendImportSucceeded(mockSessionMetadata, { manifest: false });\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchEnablementAreaImportSucceeded,\n        {\n          manifest: 'no',\n        },\n      );\n    });\n  });\n\n  describe('sendImportFailed', () => {\n    it('should emit 1 event with \"Error\" cause', () => {\n      service.sendImportFailed(mockSessionMetadata, new Error());\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchEnablementAreaImportFailed,\n        {\n          error: 'Error',\n        },\n      );\n    });\n    it('should emit 1 event with \"BadRequestException\" cause', () => {\n      service.sendImportFailed(mockSessionMetadata, new BadRequestException());\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchEnablementAreaImportFailed,\n        {\n          error: 'BadRequestException',\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/custom-tutorial.analytics.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CustomTutorialAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendImportSucceeded(sessionMetadata: SessionMetadata, data: any = {}): void {\n    this.sendEvent(\n      sessionMetadata,\n      TelemetryEvents.WorkbenchEnablementAreaImportSucceeded,\n      {\n        manifest: data?.manifest ? 'yes' : 'no',\n      },\n    );\n  }\n\n  sendImportFailed(sessionMetadata: SessionMetadata, e: Error): void {\n    this.sendEvent(\n      sessionMetadata,\n      TelemetryEvents.WorkbenchEnablementAreaImportFailed,\n      {\n        error: e?.constructor?.name || 'UncaughtError',\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/custom-tutorial.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  HttpCode,\n  Param,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiConsumes, ApiExtraModels, ApiTags } from '@nestjs/swagger';\nimport { CustomTutorialService } from 'src/modules/custom-tutorial/custom-tutorial.service';\nimport { UploadCustomTutorialDto } from 'src/modules/custom-tutorial/dto/upload.custom-tutorial.dto';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { FormDataRequest } from 'nestjs-form-data';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\nimport { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\nimport { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';\nimport { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';\nimport { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';\nimport { RootCustomTutorialManifest } from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\n\n@ApiExtraModels(\n  CreateCaCertificateDto,\n  UseCaCertificateDto,\n  CreateClientCertificateDto,\n  UseClientCertificateDto,\n  CreateBasicSshOptionsDto,\n  CreateCertSshOptionsDto,\n)\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(ClassSerializerInterceptor)\n@ApiTags('Tutorials')\n@Controller('/custom-tutorials')\nexport class CustomTutorialController {\n  constructor(private readonly service: CustomTutorialService) {}\n\n  @Post('')\n  @HttpCode(201)\n  @ApiConsumes('multipart/form-data')\n  @FormDataRequest()\n  @ApiEndpoint({\n    description: 'Create new tutorial',\n    statusCode: 201,\n    responses: [\n      {\n        type: Object,\n      },\n    ],\n  })\n  async create(\n    @Body() dto: UploadCustomTutorialDto,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<RootCustomTutorialManifest> {\n    return this.service.create(sessionMetadata, dto);\n  }\n\n  @Get('manifest')\n  @ApiEndpoint({\n    description: 'Get global manifest for custom tutorials',\n    statusCode: 200,\n    responses: [\n      {\n        type: Object,\n      },\n    ],\n  })\n  async getGlobalManifest(): Promise<RootCustomTutorialManifest> {\n    return await this.service.getGlobalManifest();\n  }\n\n  @Delete('/:id')\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete custom tutorial and its files',\n  })\n  async delete(@Param('id') id: string): Promise<void> {\n    return this.service.delete(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/custom-tutorial.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { CustomTutorialController } from 'src/modules/custom-tutorial/custom-tutorial.controller';\nimport { CustomTutorialService } from 'src/modules/custom-tutorial/custom-tutorial.service';\nimport { CustomTutorialFsProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.fs.provider';\nimport { CustomTutorialManifestProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider';\nimport { CustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/custom-tutorial.repository';\nimport { LocalCustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/local.custom-tutorial.repository';\nimport { CustomTutorialAnalytics } from 'src/modules/custom-tutorial/custom-tutorial.analytics';\n\n@Module({})\nexport class CustomTutorialModule {\n  static register(\n    repository: Type<CustomTutorialRepository> = LocalCustomTutorialRepository,\n  ) {\n    return {\n      module: CustomTutorialModule,\n      controllers: [CustomTutorialController],\n      providers: [\n        CustomTutorialService,\n        CustomTutorialFsProvider,\n        CustomTutorialManifestProvider,\n        CustomTutorialAnalytics,\n        {\n          provide: CustomTutorialRepository,\n          useClass: repository,\n        },\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  globalCustomTutorialManifest,\n  mockCustomTutorial,\n  mockCustomTutorialAnalytics,\n  mockCustomTutorialFsProvider,\n  mockCustomTutorialId,\n  mockCustomTutorialManifest,\n  mockCustomTutorialManifest2,\n  mockCustomTutorialManifestProvider,\n  mockCustomTutorialRepository,\n  mockSessionMetadata,\n  MockType,\n  mockUploadCustomTutorialDto,\n  mockUploadCustomTutorialExternalLinkDto,\n} from 'src/__mocks__';\nimport * as fs from 'fs-extra';\nimport { CustomTutorialFsProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.fs.provider';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { CustomTutorialService } from 'src/modules/custom-tutorial/custom-tutorial.service';\nimport { CustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/custom-tutorial.repository';\nimport { CustomTutorialManifestProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomTutorialAnalytics } from 'src/modules/custom-tutorial/custom-tutorial.analytics';\n\njest.mock('fs-extra');\nconst mockedFs = fs as jest.Mocked<typeof fs>;\n\nconst mockedAdmZip = {\n  extractAllTo: jest.fn(),\n};\njest.mock('adm-zip', () => jest.fn().mockImplementation(() => mockedAdmZip));\n\ndescribe('CustomTutorialService', () => {\n  let service: CustomTutorialService;\n  let customTutorialRepository: MockType<CustomTutorialRepository>;\n  let customTutorialFsProvider: MockType<CustomTutorialFsProvider>;\n  let customTutorialManifestProvider: MockType<CustomTutorialManifestProvider>;\n  let analytics: MockType<CustomTutorialAnalytics>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('fs-extra', () => mockedFs);\n    jest.mock('adm-zip', () =>\n      jest.fn().mockImplementation(() => mockedAdmZip),\n    );\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CustomTutorialService,\n        {\n          provide: CustomTutorialRepository,\n          useFactory: mockCustomTutorialRepository,\n        },\n        {\n          provide: CustomTutorialFsProvider,\n          useFactory: mockCustomTutorialFsProvider,\n        },\n        {\n          provide: CustomTutorialManifestProvider,\n          useFactory: mockCustomTutorialManifestProvider,\n        },\n        {\n          provide: CustomTutorialAnalytics,\n          useFactory: mockCustomTutorialAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CustomTutorialService);\n    customTutorialRepository = await module.get(CustomTutorialRepository);\n    customTutorialFsProvider = await module.get(CustomTutorialFsProvider);\n    customTutorialManifestProvider = await module.get(\n      CustomTutorialManifestProvider,\n    );\n    analytics = await module.get(CustomTutorialAnalytics);\n  });\n\n  describe('determineTutorialName', () => {\n    const entries = [\n      'name.zip',\n      'name',\n      'https://some.com/name',\n      'https://some.com/name?some=query&might=be&here',\n      'https://some.com/name.zip',\n      'https://some.com/name.zip?some=query&might=be&here',\n      'file://some/folder/name',\n      'file://some/folder/name.zip',\n      '/some/unix/path/name',\n      '/some/unix/path/name.zip',\n      'C:\\\\\\\\Windows\\\\name',\n      'C:\\\\\\\\Windows\\\\name.zip',\n    ];\n\n    it('Should generate proper tutorial name for all possible inputs', async () => {\n      customTutorialManifestProvider.getManifestJson.mockResolvedValue(null);\n      await Promise.all(\n        entries.map(async (entry) => {\n          expect({\n            entry,\n            name: await service['determineTutorialName']('/na', entry),\n          }).toEqual({\n            entry,\n            name: 'name',\n          });\n        }),\n      );\n    });\n  });\n\n  describe('create', () => {\n    it('Should create custom tutorial from file', async () => {\n      const result = await service.create(\n        mockSessionMetadata,\n        mockUploadCustomTutorialDto,\n      );\n\n      expect(result).toEqual(mockCustomTutorialManifest);\n      expect(analytics.sendImportSucceeded).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        { manifest: true },\n      );\n    });\n\n    it('Should create custom tutorial from external url (w/o manifest)', async () => {\n      customTutorialManifestProvider.getOriginalManifestJson.mockResolvedValue(\n        null,\n      );\n      customTutorialManifestProvider.isOriginalManifestExists.mockResolvedValue(\n        false,\n      );\n\n      const result = await service.create(\n        mockSessionMetadata,\n        mockUploadCustomTutorialExternalLinkDto,\n      );\n\n      expect(result).toEqual(mockCustomTutorialManifest);\n      expect(analytics.sendImportSucceeded).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        { manifest: false },\n      );\n    });\n\n    it('Should throw BadRequestException in case when either link or file was not provided', async () => {\n      try {\n        await service.create(mockSessionMetadata, {} as any);\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('File or external link should be provided');\n      }\n    });\n\n    it('Should throw BadRequestException in case when manifest exists but unable to parse it', async () => {\n      customTutorialManifestProvider.getOriginalManifestJson.mockResolvedValueOnce(\n        null,\n      );\n      customTutorialManifestProvider.isOriginalManifestExists.mockResolvedValueOnce(\n        true,\n      );\n\n      try {\n        await service.create(\n          mockSessionMetadata,\n          mockUploadCustomTutorialExternalLinkDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('Unable to parse manifest.json file');\n      }\n    });\n\n    it('Should throw BadRequestException in case when manifest is not an object', async () => {\n      customTutorialManifestProvider.getOriginalManifestJson.mockResolvedValue([\n        mockCustomTutorialManifest,\n      ]);\n\n      try {\n        await service.create(\n          mockSessionMetadata,\n          mockUploadCustomTutorialExternalLinkDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('Manifest json should be an object');\n      }\n    });\n\n    it('Should throw BadRequestException in case when manifest json has invalid schema', async () => {\n      customTutorialManifestProvider.getOriginalManifestJson.mockResolvedValue({\n        ...mockCustomTutorialManifest,\n        id: undefined,\n        label: undefined,\n      });\n\n      try {\n        await service.create(\n          mockSessionMetadata,\n          mockUploadCustomTutorialExternalLinkDto,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.response?.message).toEqual([\n          'id should not be empty',\n          'label should not be empty',\n        ]);\n      }\n    });\n\n    it('Should throw InternalServerError in case of any non-HttpException error', async () => {\n      customTutorialRepository.create.mockRejectedValueOnce(\n        new Error('Unable to create'),\n      );\n\n      try {\n        await service.create(mockSessionMetadata, mockUploadCustomTutorialDto);\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to create');\n      }\n    });\n  });\n\n  describe('getGlobalManifest', () => {\n    it('Should return global manifest with 2 tutorials', async () => {\n      customTutorialManifestProvider.generateTutorialManifest\n        .mockResolvedValueOnce(mockCustomTutorialManifest)\n        .mockResolvedValueOnce(mockCustomTutorialManifest2);\n\n      const result = await service.getGlobalManifest();\n\n      expect(result).toEqual(globalCustomTutorialManifest);\n    });\n\n    it('Should return global manifest with 1 tutorials since 1 failed to fetch', async () => {\n      customTutorialManifestProvider.generateTutorialManifest.mockResolvedValueOnce(\n        null,\n      );\n\n      const result = await service.getGlobalManifest();\n\n      expect(result).toEqual({\n        ...globalCustomTutorialManifest,\n        children: [mockCustomTutorialManifest],\n      });\n    });\n\n    it('Should return global manifest without children in case of any error', async () => {\n      customTutorialRepository.list.mockRejectedValueOnce(\n        new Error('Unable to get list of tutorials'),\n      );\n\n      const result = await service.getGlobalManifest();\n\n      expect(result).toEqual({\n        ...globalCustomTutorialManifest,\n        children: [],\n      });\n    });\n  });\n\n  describe('delete', () => {\n    it('Should successfully delete entity and remove related directory', async () => {\n      await service.delete(mockCustomTutorialId);\n\n      expect(customTutorialFsProvider.removeFolder).toHaveBeenCalledWith(\n        mockCustomTutorial.absolutePath,\n      );\n    });\n\n    it('Should throw NotFound error when try to delete not existing tutorial', async () => {\n      customTutorialRepository.get.mockResolvedValueOnce(null);\n\n      try {\n        await service.delete(mockCustomTutorialId);\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.CUSTOM_TUTORIAL_NOT_FOUND);\n      }\n    });\n\n    it('Should throw InternalServerError in case of any non-HttpException error', async () => {\n      customTutorialRepository.delete.mockRejectedValueOnce(\n        new Error('Unable to delete'),\n      );\n\n      try {\n        await service.delete(mockCustomTutorialId);\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to delete');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  Logger,\n  NotFoundException,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { v4 as uuidv4 } from 'uuid';\nimport { CustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/custom-tutorial.repository';\nimport {\n  CustomTutorial,\n  CustomTutorialActions,\n} from 'src/modules/custom-tutorial/models/custom-tutorial';\nimport { UploadCustomTutorialDto } from 'src/modules/custom-tutorial/dto/upload.custom-tutorial.dto';\nimport { plainToInstance } from 'class-transformer';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomTutorialFsProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.fs.provider';\nimport { CustomTutorialManifestProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider';\nimport {\n  CustomTutorialManifestType,\n  RootCustomTutorialManifest,\n} from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';\nimport { wrapHttpError } from 'src/common/utils';\nimport { parse } from 'path';\nimport { isPlainObject } from 'lodash';\nimport * as URL from 'url';\nimport { Validator } from 'class-validator';\nimport { CustomTutorialAnalytics } from 'src/modules/custom-tutorial/custom-tutorial.analytics';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class CustomTutorialService {\n  private logger = new Logger('CustomTutorialService');\n\n  private validator = new Validator();\n\n  private exceptionFactory = new ValidationPipe().createExceptionFactory();\n\n  constructor(\n    private readonly customTutorialRepository: CustomTutorialRepository,\n    private readonly customTutorialFsProvider: CustomTutorialFsProvider,\n    private readonly customTutorialManifestProvider: CustomTutorialManifestProvider,\n    private readonly analytics: CustomTutorialAnalytics,\n  ) {}\n\n  private async validateManifestJson(path: string): Promise<void> {\n    const manifest =\n      await this.customTutorialManifestProvider.getOriginalManifestJson(path);\n\n    if (\n      !manifest &&\n      (await this.customTutorialManifestProvider.isOriginalManifestExists(path))\n    ) {\n      throw new BadRequestException('Unable to parse manifest.json file');\n    }\n\n    if (manifest) {\n      if (!isPlainObject(manifest)) {\n        throw new BadRequestException('Manifest json should be an object');\n      }\n\n      const errors = await this.validator.validate(\n        plainToInstance(RootCustomTutorialManifest, manifest),\n        { whitelist: true },\n      );\n\n      if (errors?.length) {\n        throw this.exceptionFactory(errors);\n      }\n    }\n  }\n\n  private async determineTutorialName(path: string, link: string) {\n    const manifest =\n      await this.customTutorialManifestProvider.getManifestJson(path);\n\n    if (!manifest?.label) {\n      return parse(URL.parse(link).pathname).name;\n    }\n\n    return manifest.label;\n  }\n\n  /**\n   * Create custom tutorial entity + static files based on input\n   * Currently from zip file only\n   * @param sessionMetadata\n   * @param dto\n   */\n  public async create(\n    sessionMetadata: SessionMetadata,\n    dto: UploadCustomTutorialDto,\n  ): Promise<RootCustomTutorialManifest> {\n    try {\n      let tmpPath = '';\n\n      if (dto.file) {\n        tmpPath = await this.customTutorialFsProvider.unzipFromMemoryStoredFile(\n          dto.file,\n        );\n      } else if (dto.link) {\n        tmpPath = await this.customTutorialFsProvider.unzipFromExternalLink(\n          dto.link,\n        );\n      } else {\n        throw new BadRequestException(\n          'File or external link should be provided',\n        );\n      }\n\n      await this.validateManifestJson(tmpPath);\n\n      // create tutorial model\n      const model = plainToInstance(CustomTutorial, {\n        ...dto,\n        id: uuidv4(),\n      });\n\n      await this.customTutorialFsProvider.moveFolder(\n        tmpPath,\n        model.absolutePath,\n      );\n\n      model.name = await this.determineTutorialName(\n        model.absolutePath,\n        dto?.file?.originalName || dto.link,\n      );\n      const tutorial = await this.customTutorialRepository.create(model);\n\n      this.analytics.sendImportSucceeded(sessionMetadata, {\n        manifest:\n          !!(await this.customTutorialManifestProvider.getOriginalManifestJson(\n            tutorial.absolutePath,\n          )),\n      });\n\n      return await this.customTutorialManifestProvider.generateTutorialManifest(\n        tutorial,\n      );\n    } catch (e) {\n      this.analytics.sendImportFailed(sessionMetadata, e);\n      this.logger.error(\n        'Unable to create custom tutorials',\n        e,\n        sessionMetadata,\n      );\n      throw wrapHttpError(e);\n    }\n  }\n\n  /**\n   * Get global manifest for all custom tutorials\n   * In the future will be removed with some kind of partial load\n   */\n  public async getGlobalManifest(): Promise<RootCustomTutorialManifest> {\n    const children = [];\n\n    try {\n      const tutorials = await this.customTutorialRepository.list();\n\n      const manifests = (await Promise.all(\n        tutorials.map(\n          this.customTutorialManifestProvider.generateTutorialManifest.bind(\n            this.customTutorialManifestProvider,\n          ),\n        ),\n      )) as Record<string, any>[];\n\n      manifests.forEach((manifest) => {\n        if (manifest) {\n          children.push(manifest);\n        }\n      });\n    } catch (e) {\n      this.logger.warn(\n        'Unable to generate entire custom tutorials manifest',\n        e,\n      );\n    }\n\n    return {\n      type: CustomTutorialManifestType.Group,\n      id: 'custom-tutorials',\n      label: 'My tutorials',\n      _actions: [CustomTutorialActions.CREATE],\n      args: {\n        withBorder: true,\n        initialIsOpen: false,\n      },\n      children,\n    };\n  }\n\n  public async get(id: string): Promise<CustomTutorial> {\n    const model = await this.customTutorialRepository.get(id);\n\n    if (!model) {\n      this.logger.error(`Custom Tutorial with ${id} was not Found`);\n      throw new NotFoundException(ERROR_MESSAGES.CUSTOM_TUTORIAL_NOT_FOUND);\n    }\n\n    return model;\n  }\n\n  public async delete(id: string): Promise<void> {\n    try {\n      const tutorial = await this.get(id);\n      await this.customTutorialRepository.delete(id);\n      await this.customTutorialFsProvider.removeFolder(tutorial.absolutePath);\n    } catch (e) {\n      this.logger.error('Unable to delete custom tutorial', e);\n      throw wrapHttpError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/dto/upload.custom-tutorial.dto.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport {\n  HasMimeType,\n  IsFile,\n  MaxFileSize,\n  MemoryStoredFile,\n} from 'nestjs-form-data';\n\nexport class UploadCustomTutorialDto {\n  @ApiPropertyOptional({\n    type: 'string',\n    format: 'binary',\n    description: 'ZIP archive with tutorial static files',\n  })\n  @IsOptional()\n  @IsFile()\n  @HasMimeType(['application/zip'])\n  @MaxFileSize(10 * 1024 * 1024)\n  file?: MemoryStoredFile;\n\n  @ApiPropertyOptional({\n    type: 'string',\n    description: 'External link to zip archive',\n  })\n  @IsOptional()\n  @IsString()\n  @IsNotEmpty()\n  link?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/entities/custom-tutorial.entity.ts",
    "content": "import {\n  Entity,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  Column,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\n\n@Entity('custom_tutorials')\nexport class CustomTutorialEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Expose()\n  name: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  link?: string;\n\n  @CreateDateColumn()\n  @Expose()\n  createdAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/models/custom-tutorial.manifest.ts",
    "content": "import { CustomTutorialActions } from 'src/modules/custom-tutorial/models/custom-tutorial';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport {\n  IsArray,\n  IsBoolean,\n  IsEnum,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\n\nexport enum CustomTutorialManifestType {\n  CodeButton = 'code-button',\n  Group = 'group',\n  InternalLink = 'internal-link',\n}\n\nexport interface ICustomTutorialManifest {\n  id: string;\n  type: CustomTutorialManifestType;\n  label: string;\n  children?: Record<string, ICustomTutorialManifest>;\n  args?: Record<string, any>;\n  _actions?: CustomTutorialActions[];\n  _path?: string;\n}\n\nexport class CustomTutorialManifestArgs {\n  @ApiPropertyOptional({ type: Boolean })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  path?: string;\n\n  @ApiPropertyOptional({ type: Boolean })\n  @IsOptional()\n  @Expose()\n  @IsBoolean()\n  initialIsOpen?: boolean;\n\n  @ApiPropertyOptional({ type: Boolean })\n  @IsOptional()\n  @Expose()\n  @IsBoolean()\n  withBorder?: boolean;\n}\n\nexport class CustomTutorialManifest {\n  @ApiProperty({ type: String })\n  @Expose()\n  @IsNotEmpty()\n  id: string;\n\n  @ApiProperty({ enum: CustomTutorialManifestType })\n  @Expose()\n  @IsEnum(CustomTutorialManifestType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      CustomTutorialManifestType,\n    )}.`,\n  })\n  type: CustomTutorialManifestType;\n\n  @ApiProperty({ type: String })\n  @Expose()\n  @IsNotEmpty()\n  label: string;\n\n  @ApiProperty({ type: String })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  summary?: string;\n\n  @ApiPropertyOptional({ type: CustomTutorialManifestArgs })\n  @IsOptional()\n  @Expose()\n  @ValidateNested()\n  @Type(() => CustomTutorialManifestArgs)\n  args?: CustomTutorialManifestArgs;\n\n  @ApiPropertyOptional({ type: CustomTutorialManifest })\n  @IsOptional()\n  @Expose()\n  @ValidateNested({ each: true })\n  @IsArray()\n  @Type(() => CustomTutorialManifest)\n  children?: CustomTutorialManifest[];\n}\n\nexport class RootCustomTutorialManifest extends CustomTutorialManifest {\n  @ApiPropertyOptional({ enum: CustomTutorialActions })\n  @IsOptional()\n  @Expose()\n  @IsArray()\n  @IsEnum(CustomTutorialActions, { each: true })\n  _actions?: CustomTutorialActions[];\n\n  @ApiPropertyOptional({ type: String })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  _path?: string;\n\n  @ApiPropertyOptional({ type: String, isArray: true })\n  @IsOptional()\n  @Expose()\n  @IsArray()\n  @IsString({ each: true })\n  @IsNotEmpty({ each: true })\n  keywords?: string[];\n\n  @ApiPropertyOptional({ type: String })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  author?: string;\n\n  @ApiPropertyOptional({ type: String })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  url?: string;\n\n  @ApiPropertyOptional({ type: String, isArray: true })\n  @IsOptional()\n  @Expose()\n  @IsArray()\n  @IsString({ each: true })\n  @IsNotEmpty({ each: true })\n  industry?: string[];\n\n  @ApiPropertyOptional({ type: String })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  description?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/models/custom-tutorial.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { join } from 'path';\nimport config from 'src/utils/config';\n\nconst PATH_CONFIG = config.get('dir_path');\n\nexport enum CustomTutorialActions {\n  CREATE = 'create',\n  DELETE = 'delete',\n  SYNC = 'sync',\n}\n\nexport class CustomTutorial {\n  @Expose()\n  id: string;\n\n  @Expose()\n  name: string;\n\n  @Expose()\n  uri: string;\n\n  @Expose()\n  link?: string;\n\n  @Expose()\n  createdAt: Date;\n\n  get actions(): CustomTutorialActions[] {\n    const actions = [CustomTutorialActions.DELETE];\n\n    if (this.link) {\n      actions.push(CustomTutorialActions.SYNC);\n    }\n\n    return actions;\n  }\n\n  get path(): string {\n    return `/${this.id}`;\n  }\n\n  get absolutePath(): string {\n    return join(PATH_CONFIG.customTutorials, this.id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCustomTutorial,\n  mockCustomTutorialAdmZipEntry,\n  mockCustomTutorialMacosxAdmZipEntry,\n  mockCustomTutorialsHttpLink,\n  mockCustomTutorialsHttpLink2,\n  mockCustomTutorialTmpPath,\n  mockCustomTutorialZipFile,\n  mockCustomTutorialZipFileAxiosResponse,\n} from 'src/__mocks__';\nimport * as fs from 'fs-extra';\nimport axios from 'axios';\nimport { CustomTutorialFsProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.fs.provider';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport AdmZip from 'adm-zip';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport config from 'src/utils/config';\nimport { Dirent, Stats } from 'fs';\n\nconst PATH_CONFIG = config.get('dir_path');\n\njest.mock('fs-extra');\nconst mFs = fs as jest.Mocked<typeof fs>;\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\nconst mockedAdmZip = {\n  extractAllTo: jest.fn(),\n  getEntries: jest.fn(),\n  extractEntryTo: jest.fn(),\n} as unknown as jest.Mocked<AdmZip>;\n\njest.mock('adm-zip', () => jest.fn().mockImplementation(() => mockedAdmZip));\n\ndescribe('CustomTutorialFsProvider', () => {\n  let service: CustomTutorialFsProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('fs-extra', () => mFs);\n    jest.mock('adm-zip', () =>\n      jest.fn().mockImplementation(() => mockedAdmZip),\n    );\n    jest.mock('axios', () => mockedAxios);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [CustomTutorialFsProvider],\n    }).compile();\n\n    service = await module.get(CustomTutorialFsProvider);\n  });\n\n  describe('unzipToTmpFolder', () => {\n    let prepareTmpFolderSpy;\n\n    beforeEach(() => {\n      mockedAxios.get.mockResolvedValueOnce(\n        mockCustomTutorialZipFileAxiosResponse,\n      );\n      mockedAdmZip.getEntries.mockReturnValue([]);\n      mFs.ensureDir.mockImplementationOnce(() => Promise.resolve());\n      mFs.remove.mockImplementationOnce(() => Promise.resolve());\n      mFs.readdir.mockResolvedValue([]);\n      prepareTmpFolderSpy = jest.spyOn(\n        CustomTutorialFsProvider,\n        'prepareTmpFolder',\n      );\n      prepareTmpFolderSpy.mockResolvedValueOnce(mockCustomTutorialTmpPath);\n    });\n\n    describe('unzipFromMemoryStoredFile', () => {\n      it('should unzip data', async () => {\n        const result = await service.unzipFromMemoryStoredFile(\n          mockCustomTutorialZipFile,\n        );\n        expect(result).toEqual(mockCustomTutorialTmpPath);\n        expect(mFs.copy).not.toHaveBeenCalled();\n      });\n      it('should unzip data into just generated tmp folder', async () => {\n        mFs.lstat.mockResolvedValueOnce({ isDirectory: () => true } as Stats);\n        mFs.readdir.mockResolvedValue(['singleFolder'] as unknown as Dirent[]);\n\n        prepareTmpFolderSpy.mockRestore();\n        const result = await service.unzipFromMemoryStoredFile(\n          mockCustomTutorialZipFile,\n        );\n        expect(result).toContain(\n          `${PATH_CONFIG.tmpDir}/RedisInsight/custom-tutorials`,\n        );\n        expect(mFs.copy).toHaveBeenCalled();\n      });\n    });\n\n    describe('unzipFromExternalLink', () => {\n      it.each([mockCustomTutorialsHttpLink, mockCustomTutorialsHttpLink2])(\n        'should unzip data from external link',\n        async (url) => {\n          const result = await service.unzipFromExternalLink(url);\n          expect(result).toEqual(mockCustomTutorialTmpPath);\n        },\n      );\n\n      it.each([\n        'http://hithub.com',\n        'http://raw.githubusercontent.com',\n        'http://raw.amy.other.com',\n      ])('should unzip data from external link', async (url) => {\n        await expect(service.unzipFromExternalLink(url)).rejects.toThrow(\n          new BadRequestException(\n            ERROR_MESSAGES.CUSTOM_TUTORIAL_UNSUPPORTED_ORIGIN,\n          ),\n        );\n      });\n\n      it('should throw InternalServerError when incorrect external link provided', async () => {\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: 'resource not found' },\n          },\n        };\n\n        mockedAxios.get.mockReset().mockRejectedValueOnce(responsePayload);\n\n        try {\n          await service.unzipFromExternalLink(mockCustomTutorialsHttpLink);\n          fail();\n        } catch (e) {\n          expect(e).toBeInstanceOf(InternalServerErrorException);\n          expect(e.message).toEqual(\n            ERROR_MESSAGES.CUSTOM_TUTORIAL_UNABLE_TO_FETCH_FROM_EXTERNAL,\n          );\n        }\n      });\n    });\n\n    it('should unzip data to particular tmp folder', async () => {\n      mockedAdmZip.getEntries.mockReturnValueOnce([\n        mockCustomTutorialAdmZipEntry,\n        mockCustomTutorialMacosxAdmZipEntry,\n      ]);\n\n      const result = await service.unzipToTmpFolder(mockedAdmZip);\n\n      expect(result).toEqual(mockCustomTutorialTmpPath);\n      expect(mockedAdmZip.extractEntryTo).toHaveBeenCalledTimes(1);\n      expect(mockedAdmZip.extractEntryTo).toHaveBeenCalledWith(\n        mockCustomTutorialAdmZipEntry,\n        mockCustomTutorialTmpPath,\n        true,\n        true,\n        false,\n      );\n    });\n\n    it('should throw InternalServerError', async () => {\n      mockedAdmZip.getEntries.mockReturnValueOnce([\n        mockCustomTutorialAdmZipEntry,\n        mockCustomTutorialMacosxAdmZipEntry,\n      ]);\n      mockedAdmZip.extractEntryTo.mockImplementationOnce(() => {\n        throw new Error('Unable to extract file');\n      });\n\n      try {\n        await service.unzipToTmpFolder(mockedAdmZip);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to extract file');\n      }\n    });\n  });\n\n  describe('moveFolder', () => {\n    it('should move folder', async () => {\n      mFs.move.mockImplementationOnce(() => Promise.resolve());\n\n      await service.moveFolder(\n        mockCustomTutorialTmpPath,\n        mockCustomTutorial.absolutePath,\n      );\n\n      expect(mFs.pathExists).not.toHaveBeenCalled();\n      expect(mFs.remove).not.toHaveBeenCalled();\n      expect(mFs.move).toHaveBeenCalledWith(\n        mockCustomTutorialTmpPath,\n        mockCustomTutorial.absolutePath,\n      );\n    });\n\n    it('should move folder when there is no such folder in the dest path', async () => {\n      mFs.pathExists.mockImplementationOnce(() => Promise.resolve(false));\n      mFs.move.mockImplementationOnce(() => Promise.resolve());\n\n      await service.moveFolder(\n        mockCustomTutorialTmpPath,\n        mockCustomTutorial.absolutePath,\n        true,\n      );\n\n      expect(mFs.pathExists).toHaveBeenCalledWith(\n        mockCustomTutorial.absolutePath,\n      );\n      expect(mFs.remove).not.toHaveBeenCalled();\n      expect(mFs.move).toHaveBeenCalledWith(\n        mockCustomTutorialTmpPath,\n        mockCustomTutorial.absolutePath,\n      );\n    });\n\n    it('should move folder when and remove existing one before', async () => {\n      mFs.pathExists.mockImplementationOnce(() => Promise.resolve(true));\n      mFs.remove.mockImplementationOnce(() => Promise.resolve());\n      mFs.move.mockImplementationOnce(() => Promise.resolve());\n\n      await service.moveFolder(\n        mockCustomTutorialTmpPath,\n        mockCustomTutorial.absolutePath,\n        true,\n      );\n\n      expect(mFs.pathExists).toHaveBeenCalledWith(\n        mockCustomTutorial.absolutePath,\n      );\n      expect(mFs.remove).toHaveBeenCalledWith(mockCustomTutorial.absolutePath);\n      expect(mFs.move).toHaveBeenCalledWith(\n        mockCustomTutorialTmpPath,\n        mockCustomTutorial.absolutePath,\n      );\n    });\n\n    it('should throw InternalServerError', async () => {\n      mFs.pathExists.mockImplementationOnce(() => Promise.resolve(true));\n      mFs.remove.mockImplementationOnce(() => Promise.resolve());\n      mFs.move.mockImplementationOnce(() =>\n        Promise.reject(new Error('dest folder exists')),\n      );\n\n      try {\n        await service.moveFolder(\n          mockCustomTutorialTmpPath,\n          mockCustomTutorial.absolutePath,\n          true,\n        );\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('dest folder exists');\n      }\n    });\n  });\n\n  describe('removeFolder', () => {\n    it('should remove folder', async () => {\n      mFs.remove.mockResolvedValueOnce();\n\n      await service.removeFolder(mockCustomTutorial.absolutePath);\n\n      expect(mFs.remove).toHaveBeenCalledWith(mockCustomTutorial.absolutePath);\n    });\n\n    it('should not fail in case of any error', async () => {\n      mFs.remove.mockReset().mockRejectedValueOnce(new Error('No file'));\n\n      await service.removeFolder(mockCustomTutorial.absolutePath);\n\n      expect(mFs.remove).toHaveBeenCalledWith(mockCustomTutorial.absolutePath);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.fs.provider.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { MemoryStoredFile } from 'nestjs-form-data';\nimport { join } from 'path';\nimport { v4 as uuidv4 } from 'uuid';\nimport * as fs from 'fs-extra';\nimport config from 'src/utils/config';\nimport * as AdmZip from 'adm-zip';\nimport axios from 'axios';\nimport { wrapHttpError } from 'src/common/utils';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nconst PATH_CONFIG = config.get('dir_path');\n\nconst TMP_FOLDER = `${PATH_CONFIG.tmpDir}/RedisInsight/custom-tutorials`;\n\nconst UPLOAD_FROM_REMOTE_ORIGINS_WHITELIST = [\n  'https://github.com',\n  'https://raw.githubusercontent.com',\n];\n\n@Injectable()\nexport class CustomTutorialFsProvider {\n  private logger = new Logger('CustomTutorialFsProvider');\n\n  /**\n   * Custom implementation of AdmZip.extractAllTo to ignore __MACOSX folder in the root of archive\n   * In some cases when we try to delete __MACOSX folder Electron app might crash\n   * As workaround we will never extract this folder to user's FS\n   * @param zip\n   * @param targetPath\n   * @param overwrite\n   * @param keepOriginalPermission\n   * @private\n   */\n  private async extractAll(\n    zip: AdmZip,\n    targetPath,\n    overwrite = true,\n    keepOriginalPermission = false,\n  ) {\n    zip.getEntries().forEach((entry) => {\n      if (!entry.entryName.includes('__MACOSX')) {\n        zip.extractEntryTo(\n          entry,\n          targetPath,\n          true,\n          overwrite,\n          keepOriginalPermission,\n        );\n      }\n    });\n  }\n\n  /**\n   * Unzip custom tutorials archive to temporary folder\n   * @param zip\n   */\n  public async unzipToTmpFolder(zip: AdmZip): Promise<string> {\n    try {\n      const path = await CustomTutorialFsProvider.prepareTmpFolder();\n\n      await fs.remove(path);\n      await this.extractAll(zip, path, true);\n\n      return CustomTutorialFsProvider.prepareTutorialFolder(path);\n    } catch (e) {\n      this.logger.error('Unable to unzip archive', e);\n      throw new InternalServerErrorException(e.message);\n    }\n  }\n\n  /**\n   * Unzip archive from multipart/form-data file input\n   * @param file\n   */\n  public async unzipFromMemoryStoredFile(\n    file: MemoryStoredFile,\n  ): Promise<string> {\n    return this.unzipToTmpFolder(new AdmZip(file.buffer));\n  }\n\n  /**\n   * Download zip archive from external source and unzip it to temporary directory\n   * @param link\n   */\n  public async unzipFromExternalLink(link: string): Promise<string> {\n    try {\n      const url = new URL(link);\n\n      if (!UPLOAD_FROM_REMOTE_ORIGINS_WHITELIST.includes(url.origin)) {\n        return Promise.reject(\n          new BadRequestException(\n            ERROR_MESSAGES.CUSTOM_TUTORIAL_UNSUPPORTED_ORIGIN,\n          ),\n        );\n      }\n\n      // false positive. we have whitelist checks above.\n      const { data } = await axios.get(link, {\n        // lgtm[js/request-forgery]\n        responseType: 'arraybuffer',\n      });\n\n      return this.unzipToTmpFolder(new AdmZip(data));\n    } catch (e) {\n      this.logger.error('Unable to fetch zip file from external source');\n      throw wrapHttpError(\n        e,\n        ERROR_MESSAGES.CUSTOM_TUTORIAL_UNABLE_TO_FETCH_FROM_EXTERNAL,\n      );\n    }\n  }\n\n  /**\n   * Move custom tutorial from tmp folder to proper path to serve static files\n   * force - default false, will remove existing folder\n   * @param tmpPath\n   * @param dest\n   * @param force\n   */\n  public async moveFolder(tmpPath: string, dest: string, force = false) {\n    try {\n      if (force && (await fs.pathExists(dest))) {\n        await fs.remove(dest);\n      }\n\n      await fs.move(tmpPath, dest);\n    } catch (e) {\n      this.logger.error('Unable to move tutorial to a folder', e);\n      throw new InternalServerErrorException(e.message);\n    }\n  }\n\n  /**\n   * Delete Tutorial folder\n   * Will silently log an error if any\n   * @param path\n   */\n  public async removeFolder(path: string) {\n    try {\n      await fs.remove(path);\n    } catch (e) {\n      this.logger.warn('Unable to delete tutorial folder', e);\n    }\n  }\n\n  /**\n   * Create tmp folder in user's temporary directory and return path to it\n   */\n  static async prepareTmpFolder(): Promise<string> {\n    const path = join(TMP_FOLDER, uuidv4());\n    await fs.ensureDir(path);\n\n    return path;\n  }\n\n  /**\n   * Check for data structure\n   * in case when and a single folder presented on the root level\n   * we will ignore it and work with everything inside it\n   * @private\n   */\n  static async prepareTutorialFolder(path: string): Promise<string> {\n    const entries = await fs.readdir(path);\n    const firstEntryPath = join(path, entries[0] || '');\n\n    if (\n      entries?.length === 1 &&\n      (await fs.lstat(firstEntryPath)).isDirectory()\n    ) {\n      const newPath = await CustomTutorialFsProvider.prepareTmpFolder();\n\n      await fs.copy(firstEntryPath, newPath);\n\n      return newPath;\n    }\n\n    return path;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { CustomTutorialManifestProvider } from 'src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider';\nimport * as fs from 'fs-extra';\nimport { Dirent, Stats } from 'fs';\nimport { join } from 'path';\nimport {\n  mockCustomTutorial,\n  mockCustomTutorialManifest,\n  mockCustomTutorialManifestJson,\n} from 'src/__mocks__';\nimport * as Utils from 'src/utils/path';\n\njest.mock('fs-extra');\nconst mFs = fs as jest.Mocked<typeof fs>;\n\ndescribe('CustomTutorialManifestProvider', () => {\n  let service: CustomTutorialManifestProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('fs-extra', () => mFs);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [CustomTutorialManifestProvider],\n    }).compile();\n\n    service = await module.get(CustomTutorialManifestProvider);\n  });\n\n  describe('generateManifestFile', () => {\n    it('should return empty manifest for empty folder', async () => {\n      mFs.readdir.mockResolvedValueOnce([]);\n      mFs.writeFile.mockImplementationOnce(() => Promise.resolve());\n\n      await service['generateManifestFile'](mockCustomTutorial.absolutePath);\n\n      expect(mFs.writeFile).toHaveBeenCalledWith(\n        join(mockCustomTutorial.absolutePath, '_manifest.json'),\n        JSON.stringify({ children: [] }),\n        'utf8',\n      );\n    });\n  });\n\n  describe('getManifestJson', () => {\n    it('should return null in case of an error', async () => {\n      jest\n        .spyOn(service as any, 'getManifestJsonFile')\n        .mockRejectedValueOnce(new Error('any error'));\n\n      const result = await service.getManifestJson(\n        mockCustomTutorial.absolutePath,\n      );\n\n      expect(result).toEqual(null);\n    });\n  });\n\n  describe('generateManifestEntry', () => {\n    it('should return empty array for empty folder', async () => {\n      mFs.readdir.mockResolvedValueOnce([]);\n\n      const result = await service['generateManifestEntry'](\n        mockCustomTutorial.absolutePath,\n      );\n\n      expect(result).toEqual([]);\n    });\n    it('should return empty array for empty folder', async () => {\n      const spy = jest.spyOn(Utils as any, 'winPathToNormalPath');\n\n      // root level entries\n      const mockRootLevelEntries = [\n        'intro.md',\n        '.idea', // should be ignored since starts with .\n        'subfolder',\n        'manifest.json', // should be ignored since not md file\n        '_manifest.json', // should be ignored since starts with _\n        '_some.md', // should be ignored since starts with _\n      ] as unknown as Dirent[];\n\n      // subfolder entries\n      const mockSubFolderEntries = [\n        'file.md',\n        'file2.md',\n        'subsubfolder',\n        '.idea', // should be ignored since starts with .\n        '_some.md', // should be ignored since starts with _\n      ] as unknown as Dirent[];\n\n      const mockSubSubFolderEntries = [\n        'file.md',\n        'file2.md',\n        '.idea', // should be ignored since starts with .\n        '_some.md', // should be ignored since starts with _\n      ] as unknown as Dirent[];\n\n      mFs.readdir\n        .mockResolvedValueOnce(mockRootLevelEntries)\n        .mockResolvedValueOnce(mockSubFolderEntries)\n        .mockResolvedValueOnce(mockSubSubFolderEntries);\n\n      mFs.lstat\n        .mockResolvedValueOnce({ isDirectory: () => false } as Stats) // intro.md\n        .mockResolvedValueOnce({ isDirectory: () => true } as Stats) // subfolder/\n        .mockResolvedValueOnce({ isDirectory: () => false } as Stats) // subfolder/file.md\n        .mockResolvedValueOnce({ isDirectory: () => false } as Stats) // subfolder/file2.md\n        .mockResolvedValueOnce({ isDirectory: () => true } as Stats) // subfolder/subsubfolder/\n        .mockResolvedValueOnce({ isDirectory: () => false } as Stats) // subfolder/subsubfolder/file.md\n        .mockResolvedValueOnce({ isDirectory: () => false } as Stats) // subfolder/subsubfolder/file2.md\n        .mockResolvedValueOnce({ isDirectory: () => false } as Stats); // manifest.json\n\n      const result = await service['generateManifestEntry'](\n        mockCustomTutorial.absolutePath,\n      );\n\n      expect(result).toEqual([\n        {\n          args: {\n            path: '/intro.md',\n          },\n          id: 'intro.md',\n          label: 'intro',\n          type: 'internal-link',\n        },\n        {\n          children: [\n            {\n              args: {\n                path: '/subfolder/file.md',\n              },\n              id: 'file.md',\n              label: 'file',\n              type: 'internal-link',\n            },\n            {\n              args: {\n                path: '/subfolder/file2.md',\n              },\n              id: 'file2.md',\n              label: 'file2',\n              type: 'internal-link',\n            },\n            {\n              children: [\n                {\n                  args: {\n                    path: '/subfolder/subsubfolder/file.md',\n                  },\n                  id: 'file.md',\n                  label: 'file',\n                  type: 'internal-link',\n                },\n                {\n                  args: {\n                    path: '/subfolder/subsubfolder/file2.md',\n                  },\n                  id: 'file2.md',\n                  label: 'file2',\n                  type: 'internal-link',\n                },\n              ],\n              id: 'subsubfolder',\n              label: 'subsubfolder',\n              type: 'group',\n            },\n          ],\n          id: 'subfolder',\n          label: 'subfolder',\n          type: 'group',\n        },\n      ]);\n      expect(spy).toBeCalledTimes(5); // Should call util to fix win path\n    });\n  });\n\n  describe('getManifest', () => {\n    it('should successfully get manifest', async () => {\n      mFs.readFile.mockResolvedValueOnce(\n        Buffer.from(JSON.stringify(mockCustomTutorialManifestJson)),\n      );\n\n      const result = await service.getManifestJson(\n        mockCustomTutorial.absolutePath,\n      );\n\n      expect(result).toEqual(mockCustomTutorialManifestJson);\n    });\n\n    it('should return null when no manifest found', async () => {\n      mFs.readFile.mockRejectedValueOnce(new Error('No file'));\n\n      const result = await service.getManifestJson(\n        mockCustomTutorial.absolutePath,\n      );\n\n      expect(result).toEqual(null);\n    });\n  });\n\n  describe('generateTutorialManifest', () => {\n    it('should successfully generate manifest', async () => {\n      mFs.readFile.mockResolvedValueOnce(\n        Buffer.from(JSON.stringify(mockCustomTutorialManifestJson)),\n      );\n\n      const result = await service.generateTutorialManifest(mockCustomTutorial);\n\n      expect(result).toEqual(mockCustomTutorialManifest);\n    });\n\n    it('should generate manifest without children', async () => {\n      mFs.readFile.mockRejectedValueOnce(new Error('No file'));\n\n      const result = await service.generateTutorialManifest(mockCustomTutorial);\n\n      expect(result).toEqual({\n        ...mockCustomTutorialManifest,\n        children: [],\n      });\n    });\n\n    it('should return null in case of any error', async () => {\n      const result = await service.generateTutorialManifest(null);\n\n      expect(result).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/providers/custom-tutorial.manifest.provider.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { join, parse } from 'path';\nimport { isPlainObject } from 'lodash';\nimport * as fs from 'fs-extra';\nimport { CustomTutorial } from 'src/modules/custom-tutorial/models/custom-tutorial';\nimport {\n  CustomTutorialManifest,\n  CustomTutorialManifestType,\n  RootCustomTutorialManifest,\n} from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';\nimport { plainToInstance } from 'class-transformer';\nimport { winPathToNormalPath } from 'src/utils';\n\nconst MANIFEST_FILE = 'manifest.json';\nconst SYS_MANIFEST_FILE = '_manifest.json';\n\n@Injectable()\nexport class CustomTutorialManifestProvider {\n  private logger = new Logger('CustomTutorialManifestProvider');\n\n  /**\n   * Auto generate system manifest json (_manifest.json)\n   * @param path\n   * @private\n   */\n  private async generateManifestFile(\n    path: string,\n  ): Promise<Partial<RootCustomTutorialManifest>> {\n    try {\n      const manifest = {\n        children: await this.generateManifestEntry(path, '/'),\n      };\n\n      await fs.writeFile(\n        join(path, SYS_MANIFEST_FILE),\n        JSON.stringify(manifest),\n        'utf8',\n      );\n\n      return manifest;\n    } catch (e) {\n      this.logger.warn('Unable to automatically generate manifest file', e);\n      return null;\n    }\n  }\n\n  /**\n   * Discover all .md files and folders and generate manifest based on it\n   * Manifest labels will be created based on files and folders names\n   * For files [.md] will be excluded\n   * All folders and files which starts from \"_\" and \".\" will be excluded also\n   * @param path\n   * @param relativePath\n   * @private\n   */\n  private async generateManifestEntry(\n    path: string,\n    relativePath: string = '/',\n  ): Promise<CustomTutorialManifest[]> {\n    const manifest = [];\n    const entries = await fs.readdir(path);\n\n    for (let i = 0; i < entries.length; i += 1) {\n      const entry = entries[i];\n\n      if (entry.startsWith('.') || entry.startsWith('_')) {\n        // eslint-disable-next-line no-continue\n        continue;\n      }\n\n      const isDirectory = (await fs.lstat(join(path, entry))).isDirectory();\n\n      const { name, ext } = parse(entry);\n\n      if (isDirectory) {\n        manifest.push({\n          id: entry,\n          label: name,\n          type: CustomTutorialManifestType.Group,\n          children: await this.generateManifestEntry(\n            join(path, entry),\n            join(relativePath, entry),\n          ),\n        });\n      } else if (ext === '.md') {\n        manifest.push({\n          id: entry,\n          label: name,\n          type: CustomTutorialManifestType.InternalLink,\n          args: {\n            path: winPathToNormalPath(join(relativePath, entry)),\n          },\n        });\n      }\n    }\n\n    return manifest;\n  }\n\n  public async isOriginalManifestExists(path: string): Promise<boolean> {\n    return fs.existsSync(join(path, MANIFEST_FILE));\n  }\n\n  public async getOriginalManifestJson(\n    path: string,\n  ): Promise<RootCustomTutorialManifest> {\n    try {\n      return JSON.parse(await fs.readFile(join(path, MANIFEST_FILE), 'utf8'));\n    } catch (e) {\n      this.logger.warn('Unable to find original manifest.json');\n    }\n\n    return null;\n  }\n\n  private async getManifestJsonFile(\n    path,\n  ): Promise<Partial<RootCustomTutorialManifest>> {\n    const manifest = await this.getOriginalManifestJson(path);\n\n    if (manifest) {\n      return manifest;\n    }\n\n    try {\n      return JSON.parse(\n        await fs.readFile(join(path, SYS_MANIFEST_FILE), 'utf8'),\n      );\n    } catch (e) {\n      this.logger.warn('Unable to get _manifest for tutorial');\n    }\n\n    return await this.generateManifestFile(path);\n  }\n\n  /**\n   * Try to get and parse manifest.json\n   * In case of any error will not throw an error but return null\n   * In this case tutorial will be displayed but without anything inside\n   * So user will be able to fix (re-import) tutorial or remove it\n   * @param path\n   */\n  public async getManifestJson(\n    path: string,\n  ): Promise<RootCustomTutorialManifest> {\n    try {\n      const manifestJson = await this.getManifestJsonFile(path);\n\n      if (!isPlainObject(manifestJson)) {\n        return null;\n      }\n\n      return plainToInstance(RootCustomTutorialManifest, manifestJson, {\n        excludeExtraneousValues: true,\n      });\n    } catch (e) {\n      this.logger.warn('Unable to get manifest for tutorial');\n      return null;\n    }\n  }\n\n  /**\n   * Generate custom manifest based on manifest.json inside tutorial folder and\n   * additional data from local database\n   * @param tutorial\n   */\n  public async generateTutorialManifest(\n    tutorial: CustomTutorial,\n  ): Promise<RootCustomTutorialManifest> {\n    try {\n      const manifest =\n        (await this.getManifestJson(tutorial.absolutePath)) ||\n        ({} as RootCustomTutorialManifest);\n\n      return {\n        ...manifest,\n        _actions: tutorial.actions,\n        _path: tutorial.path,\n        type: CustomTutorialManifestType.Group,\n        id: tutorial.id,\n        label: tutorial.name || manifest?.label,\n        children: manifest?.children || [],\n      };\n    } catch (e) {\n      this.logger.warn('Unable to generate manifest for tutorial', e);\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/repositories/custom-tutorial.repository.ts",
    "content": "import { CustomTutorial } from 'src/modules/custom-tutorial/models/custom-tutorial';\n\nexport abstract class CustomTutorialRepository {\n  /**\n   * Create custom tutorial entity\n   * @param model\n   * @return CustomTutorial\n   */\n  abstract create(model: CustomTutorial): Promise<CustomTutorial>;\n\n  /**\n   * Create custom tutorial entity\n   * @param id\n   * @return CustomTutorial\n   */\n  abstract get(id: string): Promise<CustomTutorial>;\n\n  /**\n   * Get list of custom tutorials\n   */\n  abstract list(): Promise<CustomTutorial[]>;\n\n  /**\n   * Delete custom tutorial by id\n   */\n  abstract delete(id: string): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/repositories/local.custom-tutorial.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockCustomTutorial,\n  mockCustomTutorialEntity,\n  mockCustomTutorialId,\n  mockRepository,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalCustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/local.custom-tutorial.repository';\nimport { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';\n\ndescribe('LocalCustomTutorialRepository', () => {\n  let service: LocalCustomTutorialRepository;\n  let repository: MockType<Repository<CustomTutorialEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalCustomTutorialRepository,\n        {\n          provide: getRepositoryToken(CustomTutorialEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(CustomTutorialEntity));\n    service = await module.get(LocalCustomTutorialRepository);\n\n    repository.findOneBy.mockResolvedValue(mockCustomTutorialEntity);\n    repository\n      .createQueryBuilder()\n      .getMany.mockResolvedValue([\n        mockCustomTutorialEntity,\n        mockCustomTutorialEntity,\n      ]);\n    repository.save.mockResolvedValue(mockCustomTutorialEntity);\n  });\n\n  describe('get', () => {\n    it('should return custom tutorial model', async () => {\n      const result = await service.get(mockCustomTutorialId);\n\n      expect(result).toEqual(mockCustomTutorial);\n    });\n\n    it('should return null when custom tutorial was not found', async () => {\n      repository.findOneBy.mockResolvedValue(undefined);\n\n      const result = await service.get(mockCustomTutorialId);\n\n      expect(result).toEqual(undefined);\n    });\n  });\n\n  describe('list', () => {\n    it('should return list of custom tutorials', async () => {\n      expect(await service.list()).toEqual([\n        mockCustomTutorial,\n        mockCustomTutorial,\n      ]);\n    });\n  });\n\n  describe('create', () => {\n    it('should create custom tutorial', async () => {\n      const result = await service.create(mockCustomTutorial);\n\n      expect(result).toEqual(mockCustomTutorial);\n      expect(repository.save).toHaveBeenCalledWith({\n        ...mockCustomTutorialEntity,\n        createdAt: expect.anything(),\n      });\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete custom tutorial by id', async () => {\n      expect(await service.delete(mockCustomTutorialId)).toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/custom-tutorial/repositories/local.custom-tutorial.repository.ts",
    "content": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { classToClass } from 'src/utils';\nimport { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';\nimport { CustomTutorial } from 'src/modules/custom-tutorial/models/custom-tutorial';\nimport { CustomTutorialRepository } from 'src/modules/custom-tutorial/repositories/custom-tutorial.repository';\n\nexport class LocalCustomTutorialRepository extends CustomTutorialRepository {\n  constructor(\n    @InjectRepository(CustomTutorialEntity)\n    private readonly repository: Repository<CustomTutorialEntity>,\n  ) {\n    super();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async create(model: CustomTutorial): Promise<CustomTutorial> {\n    const entity = classToClass(CustomTutorialEntity, model);\n\n    entity.createdAt = new Date();\n\n    return classToClass(CustomTutorial, await this.repository.save(entity));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async list(): Promise<CustomTutorial[]> {\n    const entities = await this.repository\n      .createQueryBuilder('t')\n      .orderBy('t.createdAt', 'DESC')\n      .getMany();\n\n    return entities.map((entity) => classToClass(CustomTutorial, entity));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async get(id: string): Promise<CustomTutorial> {\n    return classToClass(\n      CustomTutorial,\n      await this.repository.findOneBy({ id }),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async delete(id: string): Promise<void> {\n    await this.repository.delete({ id });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/constants/events.ts",
    "content": "export enum DatabaseConnectionEvent {\n  DatabaseConnectionFailed = 'DatabaseConnectionFailed',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/constants/overview.ts",
    "content": "export enum DatabaseOverviewKeyspace {\n  Full = 'full',\n  Current = 'current',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/credential-strategy.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Database } from 'src/modules/database/models/database';\n\nexport interface ICredentialStrategy {\n  canHandle(database: Database): boolean;\n  resolve(database: Database): Promise<Database>;\n}\n\n@Injectable()\nexport abstract class CredentialStrategyProvider {\n  abstract resolve(database: Database): Promise<Database>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/credentials.module.ts",
    "content": "import { DynamicModule, Global, Type } from '@nestjs/common';\nimport {\n  CredentialStrategyProvider,\n  ICredentialStrategy,\n} from './credential-strategy.provider';\nimport { LocalCredentialStrategyProvider } from './local.credential-strategy.provider';\nimport { DefaultCredentialStrategy } from './strategies/default.credential-strategy';\nimport { AzureEntraIdCredentialStrategy } from './strategies/azure-entra-id.credential-strategy';\n\n@Global()\nexport class CredentialsModule {\n  static register(\n    provider: Type<CredentialStrategyProvider> = LocalCredentialStrategyProvider,\n    strategies: Type<ICredentialStrategy>[] = [\n      AzureEntraIdCredentialStrategy,\n      DefaultCredentialStrategy,\n    ],\n  ): DynamicModule {\n    return {\n      module: CredentialsModule,\n      providers: [\n        ...strategies,\n        {\n          provide: CredentialStrategyProvider,\n          useClass: provider,\n        },\n      ],\n      exports: [CredentialStrategyProvider],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/local.credential-strategy.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { mockDatabase } from 'src/__mocks__';\nimport { AzureAuthService } from 'src/modules/azure/auth/azure-auth.service';\nimport { AzureAuthType } from 'src/modules/azure/constants';\nimport { CloudProvider } from 'src/modules/database/models/provider-details';\nimport { Database } from 'src/modules/database/models/database';\nimport { LocalCredentialStrategyProvider } from './local.credential-strategy.provider';\nimport { DefaultCredentialStrategy } from './strategies/default.credential-strategy';\nimport { AzureEntraIdCredentialStrategy } from './strategies/azure-entra-id.credential-strategy';\n\nconst mockAzureAuthService = {\n  getRedisTokenByAccountId: jest.fn(),\n};\n\ndescribe('LocalCredentialStrategyProvider', () => {\n  let provider: LocalCredentialStrategyProvider;\n  let defaultStrategy: DefaultCredentialStrategy;\n  let azureStrategy: AzureEntraIdCredentialStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalCredentialStrategyProvider,\n        DefaultCredentialStrategy,\n        AzureEntraIdCredentialStrategy,\n        {\n          provide: AzureAuthService,\n          useValue: mockAzureAuthService,\n        },\n      ],\n    }).compile();\n\n    provider = module.get(LocalCredentialStrategyProvider);\n    defaultStrategy = module.get(DefaultCredentialStrategy);\n    azureStrategy = module.get(AzureEntraIdCredentialStrategy);\n  });\n\n  describe('resolve', () => {\n    it('should resolve database using default strategy', async () => {\n      const result = await provider.resolve(mockDatabase);\n\n      expect(result).toEqual(mockDatabase);\n    });\n\n    it('should call default strategy resolve for regular database', async () => {\n      const resolveSpy = jest.spyOn(defaultStrategy, 'resolve');\n\n      await provider.resolve(mockDatabase);\n\n      expect(resolveSpy).toHaveBeenCalledWith(mockDatabase);\n    });\n\n    it('should use Azure strategy for database with Entra ID auth', async () => {\n      const mockAzureDatabase = Object.assign(new Database(), {\n        ...mockDatabase,\n        providerDetails: {\n          provider: CloudProvider.Azure,\n          authType: AzureAuthType.EntraId,\n          azureAccountId: 'test-account-id',\n        },\n      });\n\n      mockAzureAuthService.getRedisTokenByAccountId.mockResolvedValue({\n        token: 'mock-token',\n        expiresOn: new Date(),\n        account: {\n          homeAccountId: 'test-account-id',\n          localAccountId: 'test-local-account-id',\n          username: 'test@example.com',\n        },\n      });\n\n      const resolveSpy = jest.spyOn(azureStrategy, 'resolve');\n\n      await provider.resolve(mockAzureDatabase);\n\n      expect(resolveSpy).toHaveBeenCalledWith(mockAzureDatabase);\n    });\n  });\n\n  describe('getStrategy', () => {\n    it('should return default strategy for regular database', () => {\n      const result = provider.getStrategy(mockDatabase);\n\n      expect(result).toBe(defaultStrategy);\n    });\n\n    it('should return Azure strategy for database with Entra ID auth', () => {\n      const mockAzureDatabase = Object.assign(new Database(), {\n        ...mockDatabase,\n        providerDetails: {\n          provider: CloudProvider.Azure,\n          authType: AzureAuthType.EntraId,\n          azureAccountId: 'test-account-id',\n        },\n      });\n\n      const result = provider.getStrategy(mockAzureDatabase);\n\n      expect(result).toBe(azureStrategy);\n    });\n\n    it('should return default strategy for Azure database with access key auth', () => {\n      const mockAzureDatabase = Object.assign(new Database(), {\n        ...mockDatabase,\n        providerDetails: {\n          provider: CloudProvider.Azure,\n          authType: AzureAuthType.AccessKey,\n        },\n      });\n\n      const result = provider.getStrategy(mockAzureDatabase);\n\n      expect(result).toBe(defaultStrategy);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/local.credential-strategy.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Database } from 'src/modules/database/models/database';\nimport {\n  CredentialStrategyProvider,\n  ICredentialStrategy,\n} from './credential-strategy.provider';\nimport { DefaultCredentialStrategy } from './strategies/default.credential-strategy';\nimport { AzureEntraIdCredentialStrategy } from './strategies/azure-entra-id.credential-strategy';\n\n@Injectable()\nexport class LocalCredentialStrategyProvider extends CredentialStrategyProvider {\n  private strategies: ICredentialStrategy[];\n\n  constructor(\n    private readonly azureEntraIdCredentialStrategy: AzureEntraIdCredentialStrategy,\n    private readonly defaultCredentialStrategy: DefaultCredentialStrategy,\n  ) {\n    super();\n    // Order matters: first match wins. DefaultCredentialStrategy is always last as fallback.\n    this.strategies = [\n      this.azureEntraIdCredentialStrategy,\n      this.defaultCredentialStrategy,\n    ];\n  }\n\n  getStrategy(database: Database): ICredentialStrategy | undefined {\n    return this.strategies.find((strategy) => strategy.canHandle(database));\n  }\n\n  async resolve(database: Database): Promise<Database> {\n    const strategy = this.getStrategy(database);\n    if (!strategy) {\n      throw new Error(\n        `No credential strategy available to handle database ${database.id}`,\n      );\n    }\n    return strategy.resolve(database);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/strategies/azure-entra-id.credential-strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { BadRequestException } from '@nestjs/common';\nimport { faker } from '@faker-js/faker';\nimport { mockDatabase } from 'src/__mocks__';\nimport { AzureAuthService } from 'src/modules/azure/auth/azure-auth.service';\nimport { AzureAuthType } from 'src/modules/azure/constants';\nimport { AzureEntraIdTokenExpiredException } from 'src/modules/azure/exceptions';\nimport { CloudProvider } from 'src/modules/database/models/provider-details';\nimport { Database } from 'src/modules/database/models/database';\nimport { AzureEntraIdCredentialStrategy } from './azure-entra-id.credential-strategy';\n\nconst mockAzureAuthService = {\n  getRedisTokenByAccountId: jest.fn(),\n};\n\nconst createMockAzureDatabase = (overrides = {}): Database =>\n  Object.assign(new Database(), {\n    ...mockDatabase,\n    providerDetails: {\n      provider: CloudProvider.Azure,\n      authType: AzureAuthType.EntraId,\n      azureAccountId: faker.string.uuid(),\n    },\n    ...overrides,\n  });\n\nconst createMockTokenResult = () => ({\n  token: faker.string.alphanumeric(100),\n  expiresOn: new Date(),\n  account: {\n    homeAccountId: faker.string.uuid(),\n    localAccountId: faker.string.uuid(),\n    username: faker.internet.email(),\n    name: faker.person.fullName(),\n    environment: 'login.microsoftonline.com',\n    tenantId: faker.string.uuid(),\n  },\n});\n\ndescribe('AzureEntraIdCredentialStrategy', () => {\n  let strategy: AzureEntraIdCredentialStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AzureEntraIdCredentialStrategy,\n        {\n          provide: AzureAuthService,\n          useValue: mockAzureAuthService,\n        },\n      ],\n    }).compile();\n\n    strategy = module.get(AzureEntraIdCredentialStrategy);\n  });\n\n  describe('isAzureProviderDetails', () => {\n    it('should return true for valid Azure provider details', () => {\n      const details = {\n        provider: CloudProvider.Azure,\n        authType: AzureAuthType.EntraId,\n      };\n\n      expect(\n        AzureEntraIdCredentialStrategy.isAzureProviderDetails(details),\n      ).toBe(true);\n    });\n\n    it('should return false for null', () => {\n      expect(AzureEntraIdCredentialStrategy.isAzureProviderDetails(null)).toBe(\n        false,\n      );\n    });\n\n    it('should return false for undefined', () => {\n      expect(\n        AzureEntraIdCredentialStrategy.isAzureProviderDetails(undefined),\n      ).toBe(false);\n    });\n\n    it('should return false for details without provider', () => {\n      const details = { authType: AzureAuthType.EntraId } as any;\n\n      expect(\n        AzureEntraIdCredentialStrategy.isAzureProviderDetails(details),\n      ).toBe(false);\n    });\n\n    it('should return false for details without authType', () => {\n      const details = { provider: CloudProvider.Azure } as any;\n\n      expect(\n        AzureEntraIdCredentialStrategy.isAzureProviderDetails(details),\n      ).toBe(false);\n    });\n  });\n\n  describe('isAzureEntraIdAuth', () => {\n    it('should return true for Azure Entra ID auth', () => {\n      const details = {\n        provider: CloudProvider.Azure,\n        authType: AzureAuthType.EntraId,\n      };\n\n      expect(AzureEntraIdCredentialStrategy.isAzureEntraIdAuth(details)).toBe(\n        true,\n      );\n    });\n\n    it('should return false for Azure Access Key auth', () => {\n      const details = {\n        provider: CloudProvider.Azure,\n        authType: AzureAuthType.AccessKey,\n      };\n\n      expect(AzureEntraIdCredentialStrategy.isAzureEntraIdAuth(details)).toBe(\n        false,\n      );\n    });\n\n    it('should return false for null', () => {\n      expect(AzureEntraIdCredentialStrategy.isAzureEntraIdAuth(null)).toBe(\n        false,\n      );\n    });\n\n    it('should return false for undefined', () => {\n      expect(AzureEntraIdCredentialStrategy.isAzureEntraIdAuth(undefined)).toBe(\n        false,\n      );\n    });\n  });\n\n  describe('canHandle', () => {\n    it('should return true for database with Azure Entra ID auth', () => {\n      const database = createMockAzureDatabase();\n\n      expect(strategy.canHandle(database)).toBe(true);\n    });\n\n    it('should return false for database without providerDetails', () => {\n      expect(strategy.canHandle(mockDatabase)).toBe(false);\n    });\n\n    it('should return false for database with Azure access key auth', () => {\n      const database = createMockAzureDatabase({\n        providerDetails: {\n          provider: CloudProvider.Azure,\n          authType: AzureAuthType.AccessKey,\n        },\n      });\n\n      expect(strategy.canHandle(database)).toBe(false);\n    });\n  });\n\n  describe('resolve', () => {\n    it('should throw BadRequestException when azureAccountId is missing', async () => {\n      const database = createMockAzureDatabase({\n        providerDetails: {\n          provider: CloudProvider.Azure,\n          authType: AzureAuthType.EntraId,\n          azureAccountId: undefined,\n        },\n      });\n\n      await expect(strategy.resolve(database)).rejects.toThrow(\n        BadRequestException,\n      );\n    });\n\n    it('should throw AzureEntraIdTokenExpiredException when token acquisition fails', async () => {\n      const database = createMockAzureDatabase();\n      mockAzureAuthService.getRedisTokenByAccountId.mockResolvedValue(null);\n\n      await expect(strategy.resolve(database)).rejects.toThrow(\n        AzureEntraIdTokenExpiredException,\n      );\n    });\n\n    it('should return database with credentials from token result', async () => {\n      const database = createMockAzureDatabase();\n      const tokenResult = createMockTokenResult();\n      mockAzureAuthService.getRedisTokenByAccountId.mockResolvedValue(\n        tokenResult,\n      );\n\n      const result = await strategy.resolve(database);\n\n      expect(result.username).toBe(tokenResult.account.localAccountId);\n      expect(result.password).toBe(tokenResult.token);\n      expect(\n        mockAzureAuthService.getRedisTokenByAccountId,\n      ).toHaveBeenCalledWith(database.providerDetails?.azureAccountId);\n    });\n\n    it('should preserve other database properties', async () => {\n      const database = createMockAzureDatabase();\n      const tokenResult = createMockTokenResult();\n      mockAzureAuthService.getRedisTokenByAccountId.mockResolvedValue(\n        tokenResult,\n      );\n\n      const result = await strategy.resolve(database);\n\n      expect(result.id).toBe(database.id);\n      expect(result.host).toBe(database.host);\n      expect(result.port).toBe(database.port);\n      expect(result.name).toBe(database.name);\n    });\n\n    it('should set tokenExpiresOn in providerDetails', async () => {\n      const database = createMockAzureDatabase();\n      const tokenResult = createMockTokenResult();\n      mockAzureAuthService.getRedisTokenByAccountId.mockResolvedValue(\n        tokenResult,\n      );\n\n      const result = await strategy.resolve(database);\n\n      expect(result.providerDetails?.tokenExpiresOn).toEqual(\n        tokenResult.expiresOn,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/strategies/azure-entra-id.credential-strategy.ts",
    "content": "import { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { plainToInstance } from 'class-transformer';\nimport { Database } from 'src/modules/database/models/database';\nimport {\n  AzureProviderDetails,\n  CloudProvider,\n} from 'src/modules/database/models/provider-details';\nimport { AzureAuthType } from 'src/modules/azure/constants';\nimport { AzureAuthService } from 'src/modules/azure/auth/azure-auth.service';\nimport { ICredentialStrategy } from '../credential-strategy.provider';\nimport { AzureEntraIdTokenExpiredException } from 'src/modules/azure/exceptions';\n\n@Injectable()\nexport class AzureEntraIdCredentialStrategy implements ICredentialStrategy {\n  private readonly logger = new Logger(AzureEntraIdCredentialStrategy.name);\n\n  constructor(private readonly azureAuthService: AzureAuthService) {}\n\n  static isAzureProviderDetails(\n    details: AzureProviderDetails | null | undefined,\n  ): details is AzureProviderDetails {\n    if (!details) return false;\n    return (\n      'provider' in details &&\n      details.provider === CloudProvider.Azure &&\n      'authType' in details\n    );\n  }\n\n  static isAzureEntraIdAuth(\n    details: AzureProviderDetails | null | undefined,\n  ): boolean {\n    return (\n      AzureEntraIdCredentialStrategy.isAzureProviderDetails(details) &&\n      details.authType === AzureAuthType.EntraId\n    );\n  }\n\n  canHandle(database: Database): boolean {\n    return AzureEntraIdCredentialStrategy.isAzureEntraIdAuth(\n      database.providerDetails,\n    );\n  }\n\n  async resolve(database: Database): Promise<Database> {\n    const { providerDetails } = database;\n\n    if (!providerDetails?.azureAccountId) {\n      this.logger.warn(\n        `Database ${database.id} has Entra ID auth but no azureAccountId`,\n      );\n      throw new BadRequestException(\n        'Azure account not found. Please remove this database and re-add it through Azure autodiscovery.',\n      );\n    }\n\n    const tokenResult = await this.azureAuthService.getRedisTokenByAccountId(\n      providerDetails.azureAccountId,\n    );\n\n    if (!tokenResult) {\n      this.logger.warn(\n        `Failed to acquire token for database ${database.id} - re-authentication needed`,\n      );\n      throw new AzureEntraIdTokenExpiredException();\n    }\n\n    // Use plainToInstance to ensure the result is a proper Database class instance\n    return plainToInstance(\n      Database,\n      {\n        ...database,\n        username: tokenResult.account.localAccountId,\n        password: tokenResult.token,\n        providerDetails: {\n          ...providerDetails,\n          tokenExpiresOn: tokenResult.expiresOn,\n        },\n      },\n      { groups: ['security'] },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/strategies/default.credential-strategy.spec.ts",
    "content": "import { mockDatabase } from 'src/__mocks__';\nimport { ICredentialStrategy } from '../credential-strategy.provider';\nimport { DefaultCredentialStrategy } from './default.credential-strategy';\n\ndescribe('DefaultCredentialStrategy', () => {\n  let strategy: ICredentialStrategy;\n\n  beforeEach(() => {\n    strategy = new DefaultCredentialStrategy();\n  });\n\n  describe('canHandle', () => {\n    it('should always return true', () => {\n      expect(strategy.canHandle(mockDatabase)).toBe(true);\n    });\n\n    it('should return true for any database', () => {\n      const databases = [\n        mockDatabase,\n        { ...mockDatabase, id: 'different-id' },\n        { ...mockDatabase, name: 'different-name' },\n      ];\n\n      databases.forEach((db) => {\n        expect(strategy.canHandle(db)).toBe(true);\n      });\n    });\n  });\n\n  describe('resolve', () => {\n    it('should return database as-is', async () => {\n      const result = await strategy.resolve(mockDatabase);\n\n      expect(result).toBe(mockDatabase);\n    });\n\n    it('should not modify the database', async () => {\n      const originalDatabase = { ...mockDatabase };\n\n      const result = await strategy.resolve(mockDatabase);\n\n      expect(result).toEqual(originalDatabase);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/credentials/strategies/default.credential-strategy.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Database } from 'src/modules/database/models/database';\nimport { ICredentialStrategy } from '../credential-strategy.provider';\n\n/**\n * Default credential strategy that returns the database as-is.\n */\n@Injectable()\nexport class DefaultCredentialStrategy implements ICredentialStrategy {\n  canHandle(_database: Database): boolean {\n    return true;\n  }\n\n  async resolve(database: Database): Promise<Database> {\n    return database;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database-connection.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { get } from 'lodash';\nimport { resetAllWhenMocks } from 'jest-when';\nimport {\n  mockCommonClientMetadata,\n  mockDatabase,\n  mockDatabaseAnalytics,\n  mockDatabaseInfoProvider,\n  mockDatabaseRepository,\n  mockDatabaseService,\n  mockDatabaseRecommendationService,\n  MockType,\n  mockRedisGeneralInfo,\n  mockRedisClientListResult,\n  mockDatabaseClientFactory,\n  mockFeatureService,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { DatabaseConnectionService } from 'src/modules/database/database-connection.service';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { getHostingProvider } from 'src/utils/hosting-provider-helper';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\n\njest.mock('src/utils/hosting-provider-helper');\n\ndescribe('DatabaseConnectionService', () => {\n  let service: DatabaseConnectionService;\n  let analytics: MockType<DatabaseAnalytics>;\n  let recommendationService: MockType<DatabaseRecommendationService>;\n  let databaseInfoProvider: MockType<DatabaseInfoProvider>;\n  let featureService: MockType<FeatureService>;\n  let repository: MockType<DatabaseRepository>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    resetAllWhenMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseConnectionService,\n        {\n          provide: DatabaseRepository,\n          useFactory: mockDatabaseRepository,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: DatabaseInfoProvider,\n          useFactory: mockDatabaseInfoProvider,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: DatabaseAnalytics,\n          useFactory: mockDatabaseAnalytics,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n        {\n          provide: FeatureService,\n          useFactory: mockFeatureService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseConnectionService);\n    analytics = await module.get(DatabaseAnalytics);\n    recommendationService = module.get(DatabaseRecommendationService);\n    databaseInfoProvider = module.get(DatabaseInfoProvider);\n    featureService = module.get(FeatureService);\n    repository = module.get(DatabaseRepository);\n\n    featureService.getByName.mockResolvedValue({\n      flag: false,\n    });\n  });\n\n  describe('connect', () => {\n    it('should connect to database', async () => {\n      expect(await service.connect(mockCommonClientMetadata)).toEqual(\n        undefined,\n      );\n    });\n\n    it('should call recommendationService', async () => {\n      expect(await service.connect(mockCommonClientMetadata)).toEqual(\n        undefined,\n      );\n\n      expect(recommendationService.checkMulti).toHaveBeenCalledTimes(1);\n\n      expect(recommendationService.checkMulti).toBeCalledWith(\n        mockCommonClientMetadata,\n        [\n          RECOMMENDATION_NAMES.REDIS_VERSION,\n          RECOMMENDATION_NAMES.LUA_SCRIPT,\n          RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n        ],\n        mockRedisGeneralInfo,\n      );\n    });\n\n    it('should recalculate provider if not available in the list of providers', async () => {\n      const testHost = 'localhost';\n      repository.get.mockResolvedValue({\n        host: testHost,\n        provider: 'not-in-providers-enum',\n      });\n\n      (getHostingProvider as jest.Mock).mockResolvedValue(\n        HostingProvider.REDIS_STACK,\n      );\n\n      await service.connect(mockCommonClientMetadata);\n\n      expect(getHostingProvider).toHaveBeenCalledWith(\n        expect.any(Object),\n        testHost,\n      );\n\n      expect(repository.update).toHaveBeenCalledWith(\n        mockCommonClientMetadata.sessionMetadata,\n        mockCommonClientMetadata.databaseId,\n        expect.objectContaining({\n          provider: HostingProvider.REDIS_STACK,\n        }),\n      );\n    });\n\n    it('should call check try rdi recommendation', async () => {\n      featureService.getByName.mockResolvedValueOnce({\n        flag: true,\n      });\n\n      expect(await service.connect(mockCommonClientMetadata)).toEqual(\n        undefined,\n      );\n\n      expect(recommendationService.check).toHaveBeenCalledTimes(1);\n      expect(recommendationService.checkMulti).toHaveBeenCalledTimes(1);\n\n      expect(recommendationService.checkMulti).toBeCalledWith(\n        mockCommonClientMetadata,\n        [\n          RECOMMENDATION_NAMES.REDIS_VERSION,\n          RECOMMENDATION_NAMES.LUA_SCRIPT,\n          RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n        ],\n        mockRedisGeneralInfo,\n      );\n\n      expect(recommendationService.check).toBeCalledWith(\n        mockCommonClientMetadata,\n        RECOMMENDATION_NAMES.TRY_RDI,\n        { connectionType: 'STANDALONE', provider: undefined },\n      );\n    });\n\n    it('should call databaseInfoProvider', async () => {\n      expect(await service.connect(mockCommonClientMetadata)).toEqual(\n        undefined,\n      );\n\n      expect(databaseInfoProvider.determineDatabaseServer).toHaveBeenCalled();\n      expect(databaseInfoProvider.determineDatabaseModules).toHaveBeenCalled();\n    });\n\n    it('should call getClientListInfo', async () => {\n      expect(await service.connect(mockCommonClientMetadata)).toEqual(\n        undefined,\n      );\n\n      expect(databaseInfoProvider.getClientListInfo).toHaveBeenCalled();\n      expect(\n        analytics.sendDatabaseConnectedClientListEvent,\n      ).toHaveBeenCalledWith(mockSessionMetadata, {\n        databaseId: mockDatabase.id,\n        clients: mockRedisClientListResult.map((c) => ({\n          version: mockRedisGeneralInfo.version,\n          resp: get(c, 'resp', 'n/a'),\n          libVer: get(c, 'lib-ver', 'n/a'),\n          libName: get(c, 'lib-name', 'n/a'),\n        })),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database-connection.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { Database } from 'src/modules/database/models/database';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { getHostingProvider } from 'src/utils/hosting-provider-helper';\n\n@Injectable()\nexport class DatabaseConnectionService {\n  private logger = new Logger('DatabaseConnectionService');\n\n  constructor(\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly databaseInfoProvider: DatabaseInfoProvider,\n    private readonly repository: DatabaseRepository,\n    private readonly analytics: DatabaseAnalytics,\n    private readonly featureService: FeatureService,\n    private recommendationService: DatabaseRecommendationService,\n  ) {}\n\n  /**\n   * Connects to database and updates modules list, last connected time\n   * and provider if not available in the list of providers\n   * @param clientMetadata\n   */\n  async connect(clientMetadata: ClientMetadata): Promise<void> {\n    const client =\n      await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n    // refresh modules list and last connected time\n    // mark database as not a new\n    // will be refreshed after user navigate to particular database from the databases list\n    // Note: move to a different place in case if we need to update such info more often\n    const toUpdate: Partial<Database> = {\n      new: false,\n      lastConnection: new Date(),\n      modules: await this.databaseInfoProvider.determineDatabaseModules(client),\n      version: await this.databaseInfoProvider.determineDatabaseServer(client),\n    };\n\n    const { host, provider } = await this.repository.get(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n    );\n\n    if (!HostingProvider[provider]) {\n      toUpdate.provider = await getHostingProvider(client, host);\n    }\n\n    const connectionType = client?.getConnectionType();\n    // Update cluster nodes db record\n    if (connectionType === RedisClientConnectionType.CLUSTER) {\n      toUpdate.nodes = (await client.nodes()).map(({ options }) => ({\n        host: options.host,\n        port: options.port,\n      }));\n    }\n\n    await this.repository.update(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      toUpdate,\n    );\n\n    const generalInfo =\n      await this.databaseInfoProvider.getRedisGeneralInfo(client);\n\n    this.recommendationService.checkMulti(\n      clientMetadata,\n      [\n        RECOMMENDATION_NAMES.REDIS_VERSION,\n        RECOMMENDATION_NAMES.LUA_SCRIPT,\n        RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n      ],\n      generalInfo,\n    );\n\n    const rdiFeature = await this.featureService.getByName(\n      clientMetadata.sessionMetadata,\n      KnownFeatures.Rdi,\n    );\n\n    if (rdiFeature?.flag) {\n      const database = await this.repository.get(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n      );\n      this.recommendationService.check(\n        clientMetadata,\n        RECOMMENDATION_NAMES.TRY_RDI,\n        { connectionType, provider: database.provider },\n      );\n    }\n\n    this.collectClientInfo(clientMetadata, client, generalInfo?.version);\n\n    this.logger.debug(\n      `Succeed to connect to database ${clientMetadata.databaseId}`,\n      clientMetadata,\n    );\n  }\n\n  private async collectClientInfo(\n    clientMetadata: ClientMetadata,\n    client: RedisClient,\n    version?: string,\n  ) {\n    try {\n      const intVersion = parseInt(version, 10) || 0;\n      const clients =\n        (await this.databaseInfoProvider.getClientListInfo(client)) || [];\n\n      this.analytics.sendDatabaseConnectedClientListEvent(\n        clientMetadata.sessionMetadata,\n        {\n          databaseId: clientMetadata.databaseId,\n          ...(client.isInfoCommandDisabled\n            ? { info_command_is_disabled: true }\n            : {}),\n          clients: clients.map((c) => ({\n            version: version || 'n/a',\n            resp: intVersion < 7 ? undefined : c?.['resp'] || 'n/a',\n            libName: intVersion < 7 ? undefined : c?.['lib-name'] || 'n/a',\n            libVer: intVersion < 7 ? undefined : c?.['lib-ver'] || 'n/a',\n          })),\n        },\n      );\n    } catch (error) {\n      // ignore errors\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database-info.controller.ts",
    "content": "import { ApiQuery, ApiTags } from '@nestjs/swagger';\nimport {\n  Controller,\n  Get,\n  Param,\n  Query,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor';\nimport { DatabaseInfoService } from 'src/modules/database/database-info.service';\nimport { DatabaseOverview } from 'src/modules/database/models/database-overview';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { ClientMetadata, DatabaseIndex } from 'src/common/models';\nimport { ClientMetadataParam } from 'src/common/decorators';\nimport { DbIndexValidationPipe } from 'src/common/pipes';\nimport { DatabaseOverviewKeyspace } from './constants/overview';\n\n@ApiTags('Database Instances')\n@Controller('databases')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class DatabaseInfoController {\n  constructor(private databaseInfoService: DatabaseInfoService) {}\n\n  @Get(':id/info')\n  @UseInterceptors(new TimeoutInterceptor())\n  @ApiEndpoint({\n    description: 'Get Redis database config info',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Redis database info',\n        type: RedisDatabaseInfoResponse,\n      },\n    ],\n  })\n  async getInfo(\n    @ClientMetadataParam({\n      databaseIdParam: 'id',\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n  ): Promise<RedisDatabaseInfoResponse> {\n    return this.databaseInfoService.getInfo(clientMetadata);\n  }\n\n  @Get(':id/overview')\n  @UseInterceptors(new TimeoutInterceptor())\n  @ApiEndpoint({\n    description: 'Get Redis database overview',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Redis database overview',\n        type: DatabaseOverview,\n      },\n    ],\n  })\n  @ApiQuery({\n    name: 'keyspace',\n    required: false,\n    enum: DatabaseOverviewKeyspace,\n  })\n  async getDatabaseOverview(\n    @ClientMetadataParam({\n      databaseIdParam: 'id',\n      ignoreDbIndex: false, // do not ignore db index to calculate current (selected) keys in db\n    })\n    clientMetadata: ClientMetadata,\n    @Query()\n    {\n      keyspace = DatabaseOverviewKeyspace.Current,\n    }: { keyspace: DatabaseOverviewKeyspace },\n  ): Promise<DatabaseOverview> {\n    return this.databaseInfoService.getOverview(clientMetadata, keyspace);\n  }\n\n  @Get(':id/db/:index')\n  @UseInterceptors(new TimeoutInterceptor())\n  @ApiEndpoint({\n    description: 'Try to create connection to specified database index',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n      },\n    ],\n  })\n  async getDatabaseIndex(\n    @Param('index', new DbIndexValidationPipe({ transform: true }))\n    databaseIndexDto: DatabaseIndex,\n    @ClientMetadataParam({\n      databaseIdParam: 'id',\n      ignoreDbIndex: false,\n    })\n    clientMetadata: ClientMetadata,\n  ): Promise<void> {\n    return this.databaseInfoService.getDatabaseIndex(\n      clientMetadata,\n      databaseIndexDto.db,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database-info.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCommonClientMetadata,\n  mockDatabaseClientFactory,\n  mockDatabaseInfoProvider,\n  mockDatabaseOverview,\n  mockDatabaseOverviewProvider,\n  mockDatabaseOverviewCurrentKeyspace,\n  mockDatabaseRecommendationService,\n  mockDatabaseService,\n  mockDBSize,\n  mockRedisGeneralInfo,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n  MockType,\n} from 'src/__mocks__';\nimport { DatabaseInfoService } from 'src/modules/database/database-info.service';\nimport { DatabaseOverviewProvider } from 'src/modules/database/providers/database-overview.provider';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { DatabaseService } from './database.service';\n\ndescribe('DatabaseInfoService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: DatabaseInfoService;\n  let databaseClientFactory: MockType<DatabaseClientFactory>;\n  let recommendationService: MockType<DatabaseRecommendationService>;\n  let databaseService: MockType<DatabaseService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseInfoService,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n        {\n          provide: DatabaseOverviewProvider,\n          useFactory: mockDatabaseOverviewProvider,\n        },\n        {\n          provide: DatabaseInfoProvider,\n          useFactory: mockDatabaseInfoProvider,\n        },\n        {\n          provide: DatabaseRecommendationService,\n          useFactory: mockDatabaseRecommendationService,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseInfoService);\n    databaseClientFactory = await module.get(DatabaseClientFactory);\n    recommendationService = module.get(DatabaseRecommendationService);\n    databaseService = module.get(DatabaseService);\n  });\n\n  describe('getInfo', () => {\n    it('should create client and get general info', async () => {\n      expect(await service.getInfo(mockCommonClientMetadata)).toEqual(\n        mockRedisGeneralInfo,\n      );\n    });\n  });\n\n  describe('getOverview', () => {\n    it('should create client and get overview', async () => {\n      expect(\n        await service.getOverview(\n          mockCommonClientMetadata,\n          mockDatabaseOverviewCurrentKeyspace,\n        ),\n      ).toEqual(mockDatabaseOverview);\n    });\n  });\n\n  describe('getDBSize', () => {\n    it('should create client and gets db size', async () => {\n      expect(await service.getDBSize(mockCommonClientMetadata)).toEqual(\n        mockDBSize,\n      );\n    });\n  });\n\n  describe('getDatabaseIndex', () => {\n    it('should not return a new client', async () => {\n      expect(\n        await service.getDatabaseIndex(mockCommonClientMetadata, 0),\n      ).toEqual(undefined);\n    });\n    it('Should throw Error when error during creating a client', async () => {\n      databaseClientFactory.createClient.mockRejectedValueOnce(new Error());\n      await expect(\n        service.getDatabaseIndex(mockCommonClientMetadata, 0),\n      ).rejects.toThrow(Error);\n    });\n    it('getDatabaseIndex should call databaseService.get() if previous clientMetadata.db is Undefined', async () => {\n      const db = 2;\n      databaseClientFactory.createClient.mockResolvedValueOnce(client);\n      await service.getDatabaseIndex(mockCommonClientMetadata, db);\n\n      expect(databaseService.get).toBeCalledWith(\n        mockSessionMetadata,\n        mockCommonClientMetadata.databaseId,\n      );\n    });\n    describe('recommendationService', () => {\n      it('getDatabaseIndex should call recommendationService', async () => {\n        const db = 2;\n        databaseClientFactory.createClient.mockResolvedValueOnce(client);\n        await service.getDatabaseIndex(mockCommonClientMetadata, db);\n\n        expect(recommendationService.check).toBeCalledWith(\n          { ...mockCommonClientMetadata, db },\n          RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n          { db, prevDb: 0 },\n        );\n      });\n      it('getDatabaseIndex should not call recommendationService if Error exists', async () => {\n        databaseClientFactory.createClient.mockRejectedValueOnce(new Error());\n        await expect(\n          service.getDatabaseIndex(mockCommonClientMetadata, 2),\n        ).rejects.toThrow(Error);\n        await expect(recommendationService.check).toBeCalledTimes(0);\n        await expect(databaseService.get).toBeCalledTimes(1);\n        await expect(databaseService.get).toBeCalledWith(\n          mockSessionMetadata,\n          mockCommonClientMetadata.databaseId,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database-info.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { DatabaseOverviewProvider } from 'src/modules/database/providers/database-overview.provider';\nimport { DatabaseOverview } from 'src/modules/database/models/database-overview';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { DatabaseService } from './database.service';\nimport { DatabaseOverviewKeyspace } from './constants/overview';\n\n@Injectable()\nexport class DatabaseInfoService {\n  private logger = new Logger('DatabaseInfoService');\n\n  constructor(\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly databaseOverviewProvider: DatabaseOverviewProvider,\n    private readonly databaseInfoProvider: DatabaseInfoProvider,\n    private readonly recommendationService: DatabaseRecommendationService,\n    private readonly databaseService: DatabaseService,\n  ) {}\n\n  /**\n   * Get database general info\n   * @param clientMetadata\n   */\n  public async getInfo(\n    clientMetadata: ClientMetadata,\n  ): Promise<RedisDatabaseInfoResponse> {\n    this.logger.debug(\n      `Getting database info for: ${clientMetadata.databaseId}`,\n      clientMetadata,\n    );\n\n    const client =\n      await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n    return this.databaseInfoProvider.getRedisGeneralInfo(client);\n  }\n\n  /**\n   * Get redis database overview\n   *\n   * @param clientMetadata\n   * @param keyspace\n   */\n  public async getOverview(\n    clientMetadata: ClientMetadata,\n    keyspace: DatabaseOverviewKeyspace,\n  ): Promise<DatabaseOverview> {\n    this.logger.debug(\n      `Getting database overview for: ${clientMetadata.databaseId}`,\n      clientMetadata,\n    );\n\n    const client: RedisClient =\n      await this.databaseClientFactory.getOrCreateClient({\n        ...clientMetadata,\n        db: undefined, // connect to default db index\n      });\n\n    return this.databaseOverviewProvider.getOverview(\n      clientMetadata,\n      client,\n      keyspace,\n    );\n  }\n\n  /**\n   * Get redis database number of keys\n   *\n   * @param clientMetadata\n   */\n  async getDBSize(clientMetadata: ClientMetadata): Promise<number> {\n    const client: RedisClient =\n      await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n    return this.databaseInfoProvider.getRedisDBSize(client);\n  }\n\n  /**\n   * Create connection to specified database index\n   *\n   * @param clientMetadata\n   * @param db\n   */\n  public async getDatabaseIndex(\n    clientMetadata: ClientMetadata,\n    db: number,\n  ): Promise<void> {\n    this.logger.debug(`Connection to database index: ${db}`, clientMetadata);\n\n    let client;\n    const prevDb =\n      clientMetadata.db ??\n      (\n        await this.databaseService.get(\n          clientMetadata.sessionMetadata,\n          clientMetadata.databaseId,\n        )\n      )?.db ??\n      0;\n\n    try {\n      client = await this.databaseClientFactory.createClient({\n        ...clientMetadata,\n        db,\n      });\n      client?.disconnect?.();\n\n      this.recommendationService.check(\n        { ...clientMetadata, db },\n        RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n        { db, prevDb },\n      );\n      return undefined;\n    } catch (e) {\n      this.logger.error(\n        `Unable to connect to logical database: ${db}`,\n        e,\n        clientMetadata,\n      );\n      client?.disconnect?.();\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.analytics.spec.ts",
    "content": "import { InternalServerErrorException } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockDatabase,\n  mockDatabaseWithTlsAuth,\n  mockRedisGeneralInfo,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { TelemetryEvents } from 'src/constants';\nimport { DEFAULT_SUMMARY as DEFAULT_REDIS_MODULES_SUMMARY } from 'src/utils/redis-modules-summary';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport {\n  Encoding,\n  HostingProvider,\n} from 'src/modules/database/entities/database.entity';\n\ndescribe('DatabaseAnalytics', () => {\n  let service: DatabaseAnalytics;\n  let sendEventSpy;\n  let sendFailedEventSpy;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, DatabaseAnalytics],\n    }).compile();\n\n    service = await module.get(DatabaseAnalytics);\n    sendEventSpy = jest.spyOn(service as any, 'sendEvent');\n    sendFailedEventSpy = jest.spyOn(service as any, 'sendFailedEvent');\n  });\n\n  describe('sendInstanceAddedEvent', () => {\n    it('should emit event with enabled tls and sni, and ssh', () => {\n      service.sendInstanceAddedEvent(\n        mockSessionMetadata,\n        {\n          ...mockDatabaseWithTlsAuth,\n          ssh: true,\n        },\n        mockRedisGeneralInfo,\n      );\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        {\n          databaseId: mockDatabaseWithTlsAuth.id,\n          connectionType: mockDatabaseWithTlsAuth.connectionType,\n          provider: mockDatabaseWithTlsAuth.provider,\n          useTLS: 'enabled',\n          verifyTLSCertificate: 'enabled',\n          useTLSAuthClients: 'enabled',\n          useSNI: 'enabled',\n          useSSH: 'enabled',\n          version: mockRedisGeneralInfo.version,\n          numberOfKeys: mockRedisGeneralInfo.totalKeys,\n          numberOfKeysRange: '0 - 500 000',\n          totalMemory: mockRedisGeneralInfo.usedMemory,\n          numberedDatabases: mockRedisGeneralInfo.databases,\n          numberOfModules: 0,\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          databaseIndex: 0,\n          forceStandalone: 'false',\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          serverName: 'valkey',\n          keyNameFormat: Encoding.UNICODE,\n          ...DEFAULT_REDIS_MODULES_SUMMARY,\n        },\n      );\n    });\n    it('should emit event with disabled tls and sni', () => {\n      const instance = {\n        ...mockDatabase,\n      };\n\n      service.sendInstanceAddedEvent(\n        mockSessionMetadata,\n        instance,\n        mockRedisGeneralInfo,\n      );\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        {\n          databaseId: instance.id,\n          connectionType: instance.connectionType,\n          provider: instance.provider,\n          useTLS: 'disabled',\n          verifyTLSCertificate: 'disabled',\n          useTLSAuthClients: 'disabled',\n          useSNI: 'disabled',\n          useSSH: 'disabled',\n          version: mockRedisGeneralInfo.version,\n          numberOfKeys: mockRedisGeneralInfo.totalKeys,\n          numberOfKeysRange: '0 - 500 000',\n          totalMemory: mockRedisGeneralInfo.usedMemory,\n          numberedDatabases: mockRedisGeneralInfo.databases,\n          numberOfModules: 0,\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          databaseIndex: 0,\n          forceStandalone: 'false',\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          serverName: 'valkey',\n          keyNameFormat: Encoding.UNICODE,\n          ...DEFAULT_REDIS_MODULES_SUMMARY,\n        },\n      );\n    });\n    it('should emit event without additional info', () => {\n      const instance = {\n        ...mockDatabaseWithTlsAuth,\n        modules: [\n          { name: 'search', version: 20000 },\n          { name: 'rediSQL', version: 1 },\n        ],\n      };\n      service.sendInstanceAddedEvent(mockSessionMetadata, instance, {\n        version: mockRedisGeneralInfo.version,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        {\n          databaseId: instance.id,\n          connectionType: instance.connectionType,\n          provider: instance.provider,\n          useTLS: 'enabled',\n          verifyTLSCertificate: 'enabled',\n          useTLSAuthClients: 'enabled',\n          useSNI: 'enabled',\n          useSSH: 'disabled',\n          version: mockRedisGeneralInfo.version,\n          numberOfKeys: undefined,\n          numberOfKeysRange: undefined,\n          totalMemory: undefined,\n          numberedDatabases: undefined,\n          numberOfModules: 2,\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          databaseIndex: 0,\n          forceStandalone: 'false',\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          keyNameFormat: Encoding.UNICODE,\n          ...DEFAULT_REDIS_MODULES_SUMMARY,\n          RediSearch: {\n            loaded: true,\n            version: 20000,\n          },\n          customModules: [{ name: 'rediSQL', version: 1 }],\n          serverName: null,\n        },\n      );\n    });\n    it('should emit event without db index', () => {\n      const instance = {\n        ...mockDatabaseWithTlsAuth,\n        db: 2,\n        modules: [\n          { name: 'search', version: 20000 },\n          { name: 'rediSQL', version: 1 },\n        ],\n      };\n      service.sendInstanceAddedEvent(mockSessionMetadata, instance, {\n        version: mockRedisGeneralInfo.version,\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        {\n          databaseId: instance.id,\n          connectionType: instance.connectionType,\n          provider: instance.provider,\n          useTLS: 'enabled',\n          verifyTLSCertificate: 'enabled',\n          useTLSAuthClients: 'enabled',\n          useSNI: 'enabled',\n          useSSH: 'disabled',\n          version: mockRedisGeneralInfo.version,\n          numberOfKeys: undefined,\n          numberOfKeysRange: undefined,\n          totalMemory: undefined,\n          numberedDatabases: undefined,\n          numberOfModules: 2,\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          databaseIndex: 2,\n          forceStandalone: 'false',\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          keyNameFormat: Encoding.UNICODE,\n          ...DEFAULT_REDIS_MODULES_SUMMARY,\n          RediSearch: {\n            loaded: true,\n            version: 20000,\n          },\n          customModules: [{ name: 'rediSQL', version: 1 }],\n          serverName: null,\n        },\n      );\n    });\n\n    it('should emit event with keyNameFormat', () => {\n      service.sendInstanceAddedEvent(\n        mockSessionMetadata,\n        {\n          ...mockDatabaseWithTlsAuth,\n          keyNameFormat: Encoding.HEX,\n        },\n        mockRedisGeneralInfo,\n      );\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAdded,\n        expect.objectContaining({\n          keyNameFormat: Encoding.HEX,\n        }),\n      );\n    });\n  });\n\n  describe('sendInstanceEditedEvent', () => {\n    it('should emit event for manual update by user with disabled tls', () => {\n      const prev = mockDatabaseWithTlsAuth;\n      const cur = {\n        ...mockDatabaseWithTlsAuth,\n        provider: HostingProvider.REDIS_SOFTWARE,\n        tls: undefined,\n        verifyServerCert: false,\n        caCert: null,\n        clientCert: null,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceEditedByUser,\n        {\n          databaseId: cur.id,\n          connectionType: cur.connectionType,\n          provider: HostingProvider.REDIS_SOFTWARE,\n          useTLS: 'disabled',\n          verifyTLSCertificate: 'disabled',\n          useTLSAuthClients: 'disabled',\n          useSNI: 'enabled',\n          useSSH: 'disabled',\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          forceStandalone: 'false',\n          keyNameFormat: Encoding.UNICODE,\n          previousValues: {\n            connectionType: prev.connectionType,\n            provider: prev.provider,\n            useTLS: 'enabled',\n            verifyTLSCertificate: 'enabled',\n            useTLSAuthClients: 'enabled',\n            useSNI: 'enabled',\n            useSSH: 'disabled',\n            forceStandalone: 'false',\n            keyNameFormat: Encoding.UNICODE,\n          },\n        },\n      );\n    });\n    it('should emit event for manual update by user with enabled tls', () => {\n      const prev = {\n        ...mockDatabase,\n        tls: undefined,\n      };\n      const cur = {\n        ...mockDatabaseWithTlsAuth,\n        provider: HostingProvider.REDIS_SOFTWARE,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceEditedByUser,\n        {\n          databaseId: cur.id,\n          connectionType: cur.connectionType,\n          provider: HostingProvider.REDIS_SOFTWARE,\n          useTLS: 'enabled',\n          verifyTLSCertificate: 'enabled',\n          useTLSAuthClients: 'enabled',\n          useSNI: 'enabled',\n          useSSH: 'disabled',\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          forceStandalone: 'false',\n          keyNameFormat: Encoding.UNICODE,\n          previousValues: {\n            connectionType: prev.connectionType,\n            provider: prev.provider,\n            useTLS: 'disabled',\n            useSNI: 'disabled',\n            useSSH: 'disabled',\n            verifyTLSCertificate: 'disabled',\n            useTLSAuthClients: 'disabled',\n            forceStandalone: 'false',\n            keyNameFormat: Encoding.UNICODE,\n          },\n        },\n      );\n    });\n\n    it('should emit event with forceStandalone included', () => {\n      const prev = {\n        ...mockDatabase,\n      };\n      const cur = {\n        ...mockDatabase,\n        forceStandalone: true,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceEditedByUser,\n        {\n          databaseId: cur.id,\n          connectionType: cur.connectionType,\n          provider: undefined,\n          useTLS: 'disabled',\n          verifyTLSCertificate: 'disabled',\n          useTLSAuthClients: 'disabled',\n          useSNI: 'disabled',\n          useSSH: 'disabled',\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          forceStandalone: 'true',\n          keyNameFormat: Encoding.UNICODE,\n          previousValues: {\n            connectionType: prev.connectionType,\n            provider: prev.provider,\n            useTLS: 'disabled',\n            useSNI: 'disabled',\n            useSSH: 'disabled',\n            verifyTLSCertificate: 'disabled',\n            useTLSAuthClients: 'disabled',\n            forceStandalone: 'false',\n            keyNameFormat: Encoding.UNICODE,\n          },\n        },\n      );\n    });\n\n    it('should emit event with forceStandalone not included', () => {\n      const prev = {\n        ...mockDatabase,\n      };\n      const cur = {\n        ...mockDatabase,\n        forceStandalone: undefined,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceEditedByUser,\n        {\n          databaseId: cur.id,\n          connectionType: cur.connectionType,\n          provider: undefined,\n          useTLS: 'disabled',\n          verifyTLSCertificate: 'disabled',\n          useTLSAuthClients: 'disabled',\n          useSNI: 'disabled',\n          useSSH: 'disabled',\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          forceStandalone: 'false',\n          keyNameFormat: Encoding.UNICODE,\n          previousValues: {\n            connectionType: prev.connectionType,\n            provider: prev.provider,\n            useTLS: 'disabled',\n            useSNI: 'disabled',\n            useSSH: 'disabled',\n            verifyTLSCertificate: 'disabled',\n            useTLSAuthClients: 'disabled',\n            forceStandalone: 'false',\n            keyNameFormat: Encoding.UNICODE,\n          },\n        },\n      );\n    });\n\n    it('should emit event with forceStandalone true for curr and prev', () => {\n      const prev = {\n        ...mockDatabase,\n        forceStandalone: true,\n      };\n      const cur = {\n        ...mockDatabase,\n        forceStandalone: true,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceEditedByUser,\n        {\n          databaseId: cur.id,\n          connectionType: cur.connectionType,\n          provider: undefined,\n          useTLS: 'disabled',\n          verifyTLSCertificate: 'disabled',\n          useTLSAuthClients: 'disabled',\n          useSNI: 'disabled',\n          useSSH: 'disabled',\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          forceStandalone: 'true',\n          keyNameFormat: Encoding.UNICODE,\n          previousValues: {\n            connectionType: prev.connectionType,\n            provider: prev.provider,\n            useTLS: 'disabled',\n            useSNI: 'disabled',\n            useSSH: 'disabled',\n            verifyTLSCertificate: 'disabled',\n            useTLSAuthClients: 'disabled',\n            forceStandalone: 'true',\n            keyNameFormat: Encoding.UNICODE,\n          },\n        },\n      );\n    });\n\n    it('should emit event when keyNameFormat is changed', () => {\n      const prev = mockDatabaseWithTlsAuth;\n      const cur = {\n        ...mockDatabaseWithTlsAuth,\n        provider: HostingProvider.REDIS_SOFTWARE,\n        tls: undefined,\n        verifyServerCert: false,\n        caCert: null,\n        clientCert: null,\n        keyNameFormat: Encoding.HEX,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceEditedByUser,\n        {\n          databaseId: cur.id,\n          connectionType: cur.connectionType,\n          provider: HostingProvider.REDIS_SOFTWARE,\n          useTLS: 'disabled',\n          verifyTLSCertificate: 'disabled',\n          useTLSAuthClients: 'disabled',\n          useSNI: 'enabled',\n          useSSH: 'disabled',\n          timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds\n          useDecompression: mockDatabaseWithTlsAuth.compressor,\n          forceStandalone: 'false',\n          keyNameFormat: Encoding.HEX,\n          previousValues: {\n            connectionType: prev.connectionType,\n            provider: prev.provider,\n            useTLS: 'enabled',\n            verifyTLSCertificate: 'enabled',\n            useTLSAuthClients: 'enabled',\n            useSNI: 'enabled',\n            useSSH: 'disabled',\n            forceStandalone: 'false',\n            keyNameFormat: Encoding.UNICODE,\n          },\n        },\n      );\n    });\n\n    it('should not emit event if instance updated not by user', () => {\n      const prev = mockDatabaseWithTlsAuth;\n      const cur = {\n        ...mockDatabase,\n        provider: HostingProvider.REDIS_SOFTWARE,\n        tls: undefined,\n      };\n      service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur, false);\n\n      expect(sendEventSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendInstanceAddFailedEvent', () => {\n    it('should emit AddFailed event', () => {\n      service.sendInstanceAddFailedEvent(mockSessionMetadata, httpException);\n\n      expect(sendFailedEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceAddFailed,\n        httpException,\n      );\n    });\n  });\n\n  describe('sendInstanceDeletedEvent', () => {\n    it('should emit Deleted event', () => {\n      service.sendInstanceDeletedEvent(mockSessionMetadata, mockDatabase);\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceDeleted,\n        {\n          databaseId: mockDatabase.id,\n          provider: mockDatabase.provider,\n        },\n      );\n    });\n  });\n\n  describe('sendConnectionFailedEvent', () => {\n    it('should emit ConnectionFailed event', () => {\n      service.sendConnectionFailedEvent(\n        mockSessionMetadata,\n        mockDatabase,\n        httpException,\n      );\n\n      expect(sendFailedEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisInstanceConnectionFailed,\n        httpException,\n        {\n          databaseId: mockDatabase.id,\n        },\n      );\n    });\n  });\n\n  describe('sendDatabaseConnectedClientListEvent', () => {\n    it('should emit event', () => {\n      service.sendDatabaseConnectedClientListEvent(mockSessionMetadata, {\n        databaseId: mockDatabase.id,\n        version: mockDatabase.version,\n        resp: '2',\n      });\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseConnectedClientList,\n        {\n          databaseId: mockDatabase.id,\n          version: mockDatabase.version,\n          resp: '2',\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { Database } from 'src/modules/database/models/database';\nimport { TelemetryEvents } from 'src/constants';\nimport { getRedisModulesSummary } from 'src/utils/redis-modules-summary';\nimport { getRangeForNumber, TOTAL_KEYS_BREAKPOINTS } from 'src/utils';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class DatabaseAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendConnectionFailedEvent(\n    sessionMetadata: SessionMetadata,\n    instance: Database,\n    exception: HttpException,\n  ): void {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RedisInstanceConnectionFailed,\n      exception,\n      { databaseId: instance.id },\n    );\n  }\n\n  sendInstanceAddedEvent(\n    sessionMetadata: SessionMetadata,\n    instance: Database,\n    additionalInfo?: RedisDatabaseInfoResponse,\n  ): void {\n    try {\n      const modulesSummary = getRedisModulesSummary(instance.modules);\n      this.sendEvent(sessionMetadata, TelemetryEvents.RedisInstanceAdded, {\n        databaseId: instance.id,\n        connectionType: instance.connectionType,\n        provider: instance.provider,\n        useTLS: instance.tls ? 'enabled' : 'disabled',\n        verifyTLSCertificate: instance?.verifyServerCert\n          ? 'enabled'\n          : 'disabled',\n        useTLSAuthClients: instance?.clientCert ? 'enabled' : 'disabled',\n        useSNI: instance?.tlsServername ? 'enabled' : 'disabled',\n        useSSH: instance?.ssh ? 'enabled' : 'disabled',\n        version: additionalInfo?.version,\n        numberOfKeys: additionalInfo?.totalKeys,\n        numberOfKeysRange: getRangeForNumber(\n          additionalInfo?.totalKeys,\n          TOTAL_KEYS_BREAKPOINTS,\n        ),\n        totalMemory: additionalInfo?.usedMemory,\n        numberedDatabases: additionalInfo?.databases,\n        numberOfModules: instance.modules?.length || 0,\n        timeout: instance.timeout / 1_000, // milliseconds to seconds\n        databaseIndex: instance.db || 0,\n        useDecompression: instance.compressor || null,\n        serverName: additionalInfo?.server?.server_name || null,\n        forceStandalone: instance?.forceStandalone ? 'true' : 'false',\n        keyNameFormat: instance?.keyNameFormat || null,\n        ...modulesSummary,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendInstanceAddFailedEvent(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ): void {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RedisInstanceAddFailed,\n      exception,\n    );\n  }\n\n  sendInstanceEditedEvent(\n    sessionMetadata: SessionMetadata,\n    prev: Database,\n    cur: Database,\n    manualUpdate: boolean = true,\n  ): void {\n    try {\n      if (manualUpdate) {\n        this.sendEvent(\n          sessionMetadata,\n          TelemetryEvents.RedisInstanceEditedByUser,\n          {\n            databaseId: cur.id,\n            connectionType: cur.connectionType,\n            provider: cur.provider,\n            useTLS: cur.tls ? 'enabled' : 'disabled',\n            verifyTLSCertificate: cur?.verifyServerCert\n              ? 'enabled'\n              : 'disabled',\n            useTLSAuthClients: cur?.clientCert ? 'enabled' : 'disabled',\n            useSNI: cur?.tlsServername ? 'enabled' : 'disabled',\n            useSSH: cur?.ssh ? 'enabled' : 'disabled',\n            timeout: cur?.timeout / 1_000, // milliseconds to seconds\n            useDecompression: cur?.compressor || null,\n            forceStandalone: cur?.forceStandalone ? 'true' : 'false',\n            keyNameFormat: cur?.keyNameFormat || null,\n            previousValues: {\n              connectionType: prev.connectionType,\n              provider: prev.provider,\n              useTLS: prev.tls ? 'enabled' : 'disabled',\n              useSNI: prev?.tlsServername ? 'enabled' : 'disabled',\n              useSSH: prev?.ssh ? 'enabled' : 'disabled',\n              verifyTLSCertificate: prev?.verifyServerCert\n                ? 'enabled'\n                : 'disabled',\n              useTLSAuthClients: prev?.clientCert ? 'enabled' : 'disabled',\n              forceStandalone: prev?.forceStandalone ? 'true' : 'false',\n              keyNameFormat: prev?.keyNameFormat || null,\n            },\n          },\n        );\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendInstanceDeletedEvent(\n    sessionMetadata: SessionMetadata,\n    instance: Database,\n  ): void {\n    this.sendEvent(sessionMetadata, TelemetryEvents.RedisInstanceDeleted, {\n      databaseId: instance.id,\n      provider: instance.provider,\n    });\n  }\n\n  sendDatabaseConnectedClientListEvent(\n    sessionMetadata: SessionMetadata,\n    additionalData: object = {},\n  ): void {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.DatabaseConnectedClientList,\n        additionalData,\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.controller.spec.ts",
    "content": "import { when } from 'jest-when';\nimport * as request from 'supertest';\nimport { Test } from '@nestjs/testing';\nimport {\n  ForbiddenException,\n  INestApplication,\n  MiddlewareConsumer,\n  Module,\n  NestModule,\n} from '@nestjs/common';\nimport {\n  mockCreateDatabaseDto,\n  mockDatabase,\n  mockDatabaseConnectionService,\n  mockDatabaseService,\n  mockSessionService,\n} from 'src/__mocks__';\nimport { SingleUserAuthMiddleware } from 'src/common/middlewares/single-user-auth.middleware';\nimport { SessionService } from 'src/modules/session/session.service';\nimport config, { Config } from 'src/utils/config';\nimport { DatabaseController } from 'src/modules/database/database.controller';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseConnectionService } from 'src/modules/database/database-connection.service';\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\n@Module({\n  controllers: [DatabaseController],\n  providers: [\n    {\n      provide: DatabaseService,\n      useFactory: mockDatabaseService,\n    },\n    {\n      provide: DatabaseConnectionService,\n      useFactory: mockDatabaseConnectionService,\n    },\n    {\n      provide: SessionService,\n      useFactory: mockSessionService,\n    },\n  ],\n})\nclass TestModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(SingleUserAuthMiddleware).forRoutes('*');\n  }\n}\n\ndescribe('DatabaseController', () => {\n  let app: INestApplication;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const moduleRef = await Test.createTestingModule({\n      imports: [TestModule],\n    }).compile();\n\n    app = moduleRef.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  describe('POST /databases', () => {\n    it('should create database', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .post('/databases')\n        .send(mockCreateDatabaseDto)\n        .expect(201);\n    });\n\n    it('should fail to create database when database management disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .post('/databases')\n        .send(mockCreateDatabaseDto)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n\n  describe('PATCH /databases/:id', () => {\n    it('should update database', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .patch(`/databases/${mockDatabase.id}`)\n        .send(mockCreateDatabaseDto)\n        .expect(200);\n    });\n\n    it('should fail to update database when database management is disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .patch(`/databases/${mockDatabase.id}`)\n        .send(mockCreateDatabaseDto)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n\n  describe('POST /databases/clone/:id', () => {\n    it('should clone database', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .post(`/databases/clone/${mockDatabase.id}`)\n        .send(mockCreateDatabaseDto)\n        .expect(200);\n    });\n\n    it('should fail to clone database when database management is disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .post(`/databases/clone/${mockDatabase.id}`)\n        .send(mockCreateDatabaseDto)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n\n  describe('POST /databases/test', () => {\n    it('should test database connection', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .post('/databases/test')\n        .send(mockCreateDatabaseDto)\n        .expect(200);\n    });\n\n    it('should fail to test database connection when database management disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .post('/databases/test')\n        .send(mockCreateDatabaseDto)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n\n  describe('DELETE /databases/:id', () => {\n    it('should delete database', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .delete(`/databases/${mockDatabase.id}`)\n        .expect(200);\n    });\n\n    it('should fail to delete database when database management is disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .delete(`/databases/${mockDatabase.id}`)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n\n  describe('DELETE /databases', () => {\n    it('should delete databases', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .delete('/databases')\n        .send({ ids: [mockDatabase.id] })\n        .expect(200);\n    });\n\n    it('should fail to delete databases when database management is disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .delete('/databases')\n        .send({ ids: [mockDatabase.id] })\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n\n  describe('POST /export', () => {\n    it('should export databases', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .delete('/databases/export')\n        .send({ ids: [mockDatabase.id] })\n        .expect(200);\n    });\n\n    it('should fail to export databases when database management is disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .delete('/databases/export')\n        .send({ ids: [mockDatabase.id] })\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.controller.ts",
    "content": "import { ApiTags } from '@nestjs/swagger';\nimport {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Patch,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { Database } from 'src/modules/database/models/database';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseConnectionService } from 'src/modules/database/database-connection.service';\nimport { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';\nimport { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto';\nimport { BuildType } from 'src/modules/server/models/server';\nimport { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dto';\nimport { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response';\nimport {\n  ClientMetadataParam,\n  RequestSessionMetadata,\n  DatabaseManagement,\n} from 'src/common/decorators';\nimport { ClientMetadata, SessionMetadata } from 'src/common/models';\nimport { ExportDatabasesDto } from 'src/modules/database/dto/export.databases.dto';\nimport { ExportDatabase } from 'src/modules/database/models/export-database';\nimport { DatabaseResponse } from 'src/modules/database/dto/database.response';\nimport { classToClass } from 'src/utils';\n\n@ApiTags('Database')\n@Controller('databases')\nexport class DatabaseController {\n  constructor(\n    private service: DatabaseService,\n    private connectionService: DatabaseConnectionService,\n  ) {}\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Get('')\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get databases list',\n    responses: [\n      {\n        status: 200,\n        isArray: true,\n        type: Database,\n      },\n    ],\n  })\n  async list(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<Database[]> {\n    return this.service.list(sessionMetadata);\n  }\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Get('/:id')\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get database instance by id',\n    responses: [\n      {\n        status: 200,\n        description: 'Database instance',\n        type: DatabaseResponse,\n      },\n    ],\n  })\n  async get(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n  ): Promise<DatabaseResponse> {\n    return classToClass(\n      DatabaseResponse,\n      await this.service.get(sessionMetadata, id),\n    );\n  }\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Post('')\n  @ApiEndpoint({\n    description: 'Add database instance',\n    statusCode: 201,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 201,\n        description: 'Created database instance',\n        type: DatabaseResponse,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  @DatabaseManagement()\n  async create(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: CreateDatabaseDto,\n  ): Promise<DatabaseResponse> {\n    return classToClass(\n      DatabaseResponse,\n      await this.service.create(sessionMetadata, dto, true),\n    );\n  }\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))\n  @Patch(':id')\n  @ApiEndpoint({\n    description: 'Update database instance by id',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: \"Updated database instance' response\",\n        type: DatabaseResponse,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  @DatabaseManagement()\n  async update(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n    @Body() database: UpdateDatabaseDto,\n  ): Promise<DatabaseResponse> {\n    return classToClass(\n      DatabaseResponse,\n      await this.service.update(sessionMetadata, id, database, true),\n    );\n  }\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))\n  @Post('clone/:id')\n  @ApiEndpoint({\n    description: 'Update database instance by id',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: \"Updated database instance' response\",\n        type: DatabaseResponse,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  @DatabaseManagement()\n  async clone(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n    @Body() database: UpdateDatabaseDto,\n  ): Promise<DatabaseResponse> {\n    return classToClass(\n      DatabaseResponse,\n      await this.service.clone(sessionMetadata, id, database),\n    );\n  }\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Post('/test')\n  @ApiEndpoint({\n    description: 'Test connection',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n    }),\n  )\n  @DatabaseManagement()\n  async testConnection(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: CreateDatabaseDto,\n  ): Promise<void> {\n    return await this.service.testConnection(sessionMetadata, dto);\n  }\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Post('/test/:id')\n  @ApiEndpoint({\n    description: 'Test connection',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  async testExistConnection(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n    @Body() dto: UpdateDatabaseDto,\n  ): Promise<void> {\n    return this.service.testConnection(sessionMetadata, dto, id);\n  }\n\n  @Delete('/:id')\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete database instance by id',\n    excludeFor: [BuildType.RedisStack],\n  })\n  @DatabaseManagement()\n  async deleteDatabaseInstance(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('id') id: string,\n  ): Promise<void> {\n    await this.service.delete(sessionMetadata, id);\n  }\n\n  @Delete('')\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete many databases by ids',\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'Delete many databases by ids response',\n        type: DeleteDatabasesDto,\n      },\n    ],\n  })\n  @UsePipes(new ValidationPipe({ transform: true }))\n  @DatabaseManagement()\n  async bulkDeleteDatabaseInstance(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: DeleteDatabasesDto,\n  ): Promise<DeleteDatabasesResponse> {\n    return await this.service.bulkDelete(sessionMetadata, dto.ids);\n  }\n\n  @Get(':id/connect')\n  @ApiEndpoint({\n    description: 'Connect to database instance by id',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Successfully connected to database instance',\n      },\n    ],\n  })\n  @UsePipes(new ValidationPipe({ transform: true }))\n  async connect(\n    @ClientMetadataParam({\n      databaseIdParam: 'id',\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n  ): Promise<void> {\n    await this.connectionService.connect(clientMetadata);\n  }\n\n  @Post('export')\n  @ApiEndpoint({\n    statusCode: 201,\n    excludeFor: [BuildType.RedisStack],\n    description:\n      'Export many databases by ids. With or without passwords and certificates bodies.',\n    responses: [\n      {\n        status: 201,\n        description: 'Export many databases by ids response',\n        type: ExportDatabase,\n      },\n    ],\n  })\n  @UsePipes(new ValidationPipe({ transform: true }))\n  @DatabaseManagement()\n  async exportConnections(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: ExportDatabasesDto,\n  ): Promise<ExportDatabase[]> {\n    return await this.service.export(sessionMetadata, dto.ids, dto.withSecrets);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.module.ts",
    "content": "import config, { Config } from 'src/utils/config';\nimport {\n  MiddlewareConsumer,\n  Module,\n  RequestMethod,\n  Type,\n} from '@nestjs/common';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseController } from 'src/modules/database/database.controller';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { LocalDatabaseRepository } from 'src/modules/database/repositories/local.database.repository';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { DatabaseConnectionService } from 'src/modules/database/database-connection.service';\nimport { DatabaseFactory } from 'src/modules/database/providers/database.factory';\nimport { DatabaseInfoController } from 'src/modules/database/database-info.controller';\nimport { DatabaseInfoService } from 'src/modules/database/database-info.service';\nimport { DatabaseOverviewProvider } from 'src/modules/database/providers/database-overview.provider';\nimport { StackDatabasesRepository } from 'src/modules/database/repositories/stack.databases.repository';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { DatabaseInfoProvider } from './providers/database-info.provider';\nimport { ConnectionMiddleware } from './middleware/connection.middleware';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Module({})\nexport class DatabaseModule {\n  static register(\n    databaseRepository: Type<DatabaseRepository> = SERVER_CONFIG.buildType ===\n    'REDIS_STACK'\n      ? StackDatabasesRepository\n      : LocalDatabaseRepository,\n    databaseOverviewProvider: Type<DatabaseOverviewProvider> = DatabaseOverviewProvider,\n  ) {\n    return {\n      module: DatabaseModule,\n      controllers: [DatabaseController, DatabaseInfoController],\n      providers: [\n        DatabaseService,\n        DatabaseConnectionService,\n        DatabaseClientFactory,\n        DatabaseInfoProvider,\n        DatabaseAnalytics,\n        DatabaseFactory,\n        DatabaseInfoService,\n        {\n          provide: DatabaseOverviewProvider,\n          useClass: databaseOverviewProvider,\n        },\n        {\n          provide: DatabaseRepository,\n          useClass: databaseRepository,\n        },\n      ],\n      exports: [\n        DatabaseRepository,\n        DatabaseService,\n        DatabaseConnectionService,\n        DatabaseClientFactory,\n        // todo: rethink everything below\n        DatabaseFactory,\n        DatabaseInfoService,\n        DatabaseInfoProvider,\n      ],\n    };\n  }\n\n  configure(consumer: MiddlewareConsumer): any {\n    consumer\n      .apply(ConnectionMiddleware)\n      .forRoutes(\n        { path: 'databases', method: RequestMethod.POST },\n        { path: 'databases/test', method: RequestMethod.POST },\n        { path: 'databases/:id/connect', method: RequestMethod.GET },\n      );\n  }\n  // todo: check if still needed\n  // configure(consumer: MiddlewareConsumer): any {\n  //   consumer\n  //     .apply(RedisConnectionMiddleware)\n  //     .forRoutes({ path: 'instance/:dbInstance/connect', method: RequestMethod.GET });\n  // }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.service.spec.ts",
    "content": "import {\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { omit, get, update } from 'lodash';\n\nimport { classToClass } from 'src/utils';\nimport {\n  mockDatabase,\n  mockDatabaseAnalytics,\n  mockDatabaseFactory,\n  mockDatabaseInfoProvider,\n  mockDatabaseRepository,\n  mockCaCertificate,\n  mockClientCertificate,\n  MockType,\n  mockRedisGeneralInfo,\n  mockDatabaseWithTls,\n  mockDatabaseWithTlsAuth,\n  mockDatabaseWithSshPrivateKey,\n  mockSentinelDatabaseWithTlsAuth,\n  mockDatabaseWithCloudDetails,\n  mockDatabaseWithProviderDetails,\n  mockRedisClientFactory,\n  mockRedisClientStorage,\n  mockSessionMetadata,\n  mockCredentialProvider,\n} from 'src/__mocks__';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { DatabaseFactory } from 'src/modules/database/providers/database.factory';\nimport { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { Compressor } from 'src/modules/database/entities/database.entity';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport {\n  RedisConnectionSentinelMasterRequiredException,\n  RedisConnectionUnavailableException,\n} from 'src/modules/redis/exceptions/connection';\nimport { CredentialStrategyProvider } from 'src/modules/database/credentials/credential-strategy.provider';\nimport { ExportDatabase } from './models/export-database';\n\nconst updateDatabaseTests = [\n  { input: {}, expected: 0 },\n  { input: { name: 'new name' }, expected: 0 },\n  { input: { port: 6379 }, expected: 1 },\n  { input: { host: 'new host' }, expected: 1 },\n  { input: { username: 'user' }, expected: 1 },\n  { input: { password: 'pass' }, expected: 1 },\n  { input: { ssh: true }, expected: 1 },\n  { input: { sshOptions: { password: 'pass' } }, expected: 1 },\n  { input: { sentinelMaster: 'master' }, expected: 1 },\n  { input: { caCert: mockCaCertificate }, expected: 1 },\n  { input: { clientCert: mockClientCertificate }, expected: 1 },\n  { input: { compressor: Compressor.NONE }, expected: 0 },\n  { input: { timeout: 45_000 }, expected: 0 },\n  { input: { port: 6379, timeout: 45_000 }, expected: 1 },\n];\n\ndescribe('DatabaseService', () => {\n  let service: DatabaseService;\n  let databaseRepository: MockType<DatabaseRepository>;\n  let databaseFactory: MockType<DatabaseFactory>;\n  let redisClientFactory: MockType<RedisClientFactory>;\n  let analytics: MockType<DatabaseAnalytics>;\n  const exportSecurityFields: string[] = [\n    'password',\n    'clientCert.key',\n    'sshOptions.password',\n    'sshOptions.passphrase',\n    'sshOptions.privateKey',\n    'sentinelMaster.password',\n  ];\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EventEmitter2,\n        DatabaseService,\n        {\n          provide: DatabaseRepository,\n          useFactory: mockDatabaseRepository,\n        },\n        {\n          provide: RedisClientFactory,\n          useFactory: mockRedisClientFactory,\n        },\n        {\n          provide: RedisClientStorage,\n          useFactory: mockRedisClientStorage,\n        },\n        {\n          provide: DatabaseInfoProvider,\n          useFactory: mockDatabaseInfoProvider,\n        },\n        {\n          provide: DatabaseFactory,\n          useFactory: mockDatabaseFactory,\n        },\n        {\n          provide: DatabaseAnalytics,\n          useFactory: mockDatabaseAnalytics,\n        },\n        {\n          provide: CredentialStrategyProvider,\n          useFactory: mockCredentialProvider,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseService);\n    databaseRepository = await module.get(DatabaseRepository);\n    databaseFactory = await module.get(DatabaseFactory);\n    redisClientFactory = await module.get(RedisClientFactory);\n    analytics = await module.get(DatabaseAnalytics);\n  });\n\n  describe('exists', () => {\n    it('should return true if database exists', async () => {\n      expect(\n        await service.exists(mockSessionMetadata, mockDatabase.id),\n      ).toEqual(true);\n    });\n  });\n\n  describe('list', () => {\n    it('should return databases and send analytics event', async () => {\n      databaseRepository.list.mockResolvedValue([mockDatabase, mockDatabase]);\n      expect(await service.list(mockSessionMetadata)).toEqual([\n        mockDatabase,\n        mockDatabase,\n      ]);\n    });\n    it('should throw InternalServerErrorException?', async () => {\n      databaseRepository.list.mockRejectedValueOnce(new Error());\n      await expect(service.list(mockSessionMetadata)).rejects.toThrow(\n        InternalServerErrorException,\n      );\n    });\n  });\n\n  describe('get', () => {\n    it('should return database by id', async () => {\n      expect(await service.get(mockSessionMetadata, mockDatabase.id)).toEqual(\n        mockDatabase,\n      );\n    });\n    it('should throw NotFound if no database found', async () => {\n      databaseRepository.get.mockResolvedValueOnce(null);\n      await expect(\n        service.get(mockSessionMetadata, mockDatabase.id),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it('should throw NotFound if no database id was provided', async () => {\n      await expect(service.get(mockSessionMetadata, '')).rejects.toThrow(\n        NotFoundException,\n      );\n    });\n  });\n\n  describe('create', () => {\n    it('should create new database and send analytics event', async () => {\n      expect(await service.create(mockSessionMetadata, mockDatabase)).toEqual(\n        mockDatabase,\n      );\n      expect(analytics.sendInstanceAddedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase,\n        mockRedisGeneralInfo,\n      );\n      expect(analytics.sendInstanceAddFailedEvent).not.toHaveBeenCalled();\n    });\n    it('should create new database with cloud details and send analytics event', async () => {\n      databaseRepository.create.mockResolvedValueOnce(\n        mockDatabaseWithCloudDetails,\n      );\n      expect(\n        await service.create(mockSessionMetadata, mockDatabaseWithCloudDetails),\n      ).toEqual(mockDatabaseWithCloudDetails);\n      expect(analytics.sendInstanceAddedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseWithCloudDetails,\n        mockRedisGeneralInfo,\n      );\n      expect(analytics.sendInstanceAddFailedEvent).not.toHaveBeenCalled();\n    });\n    it('should create new database with provider details', async () => {\n      databaseRepository.create.mockResolvedValueOnce(\n        mockDatabaseWithProviderDetails,\n      );\n      expect(\n        await service.create(\n          mockSessionMetadata,\n          mockDatabaseWithProviderDetails,\n        ),\n      ).toEqual(mockDatabaseWithProviderDetails);\n    });\n    it('should not fail when collecting data for analytics event', async () => {\n      redisClientFactory.createClient.mockRejectedValueOnce(new Error());\n      expect(await service.create(mockSessionMetadata, mockDatabase)).toEqual(\n        mockDatabase,\n      );\n      expect(analytics.sendInstanceAddedEvent).not.toHaveBeenCalled();\n      expect(analytics.sendInstanceAddFailedEvent).not.toHaveBeenCalled();\n    });\n    it('should throw NotFound if no database?', async () => {\n      databaseRepository.create.mockRejectedValueOnce(new NotFoundException());\n      await expect(\n        service.create(mockSessionMetadata, mockDatabase),\n      ).rejects.toThrow(NotFoundException);\n      expect(analytics.sendInstanceAddFailedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        new NotFoundException(),\n      );\n    });\n  });\n\n  describe('update', () => {\n    it('should update existing database and send analytics event', async () => {\n      databaseRepository.update.mockReturnValue({\n        ...mockDatabase,\n        port: 6380,\n        password: 'password',\n      });\n\n      expect(\n        await service.update(\n          mockSessionMetadata,\n          mockDatabase.id,\n          classToClass(UpdateDatabaseDto, { password: 'password', port: 6380 }),\n          true,\n        ),\n      ).toEqual({ ...mockDatabase, port: 6380, password: 'password' });\n      expect(analytics.sendInstanceEditedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase,\n        { ...mockDatabase, port: 6380, password: 'password' },\n        true,\n      );\n    });\n\n    it('should update existing database with merged ssh options', async () => {\n      databaseRepository.get.mockResolvedValueOnce(\n        mockDatabaseWithSshPrivateKey,\n      );\n\n      await service.update(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          password: 'pass',\n          sshOptions: { password: 'new password' },\n        }),\n        true,\n      );\n      expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n        mockSessionMetadata,\n        {\n          timeout: 30000,\n          compressor: Compressor.NONE,\n          id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id',\n          name: 'database-name',\n          host: '127.0.100.1',\n          port: 6379,\n          connectionType: 'STANDALONE',\n          new: false,\n          version: '7.0',\n          ssh: true,\n          keyNameFormat: 'Unicode',\n          sshOptions: {\n            id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ssh-id',\n            host: 'ssh.host.test',\n            port: 22,\n            username: 'ssh-username',\n            password: 'new password',\n            privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\\nssh-private-key',\n            passphrase: 'ssh-passphrase',\n          },\n          password: 'pass',\n        },\n      );\n    });\n\n    it('should update existing database with new ssh options', async () => {\n      await service.update(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          ssh: true,\n          sshOptions: {\n            id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ssh-id',\n            host: 'ssh.host.test',\n            port: 22,\n            username: 'ssh-username',\n            password: null,\n            privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\\nssh-private-key',\n            passphrase: 'ssh-passphrase',\n          },\n        }),\n        true,\n      );\n      expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n        mockSessionMetadata,\n        {\n          timeout: 30000,\n          compressor: Compressor.NONE,\n          name: 'database-name',\n          id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id',\n          host: '127.0.100.1',\n          port: 6380,\n          password: 'password',\n          connectionType: 'STANDALONE',\n          new: false,\n          version: '7.0',\n          ssh: true,\n          keyNameFormat: 'Unicode',\n          sshOptions: {\n            host: 'ssh.host.test',\n            port: 22,\n            username: 'ssh-username',\n            password: null,\n            privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\\nssh-private-key',\n            passphrase: 'ssh-passphrase',\n          },\n        },\n      );\n    });\n\n    describe('test connection', () => {\n      test.each(updateDatabaseTests)('%j', async ({ input, expected }) => {\n        databaseRepository.update.mockReturnValue(null);\n        await service.update(\n          mockSessionMetadata,\n          mockDatabase.id,\n          input as UpdateDatabaseDto,\n          true,\n        );\n        expect(databaseFactory.createDatabaseModel).toBeCalledTimes(expected);\n      });\n    });\n    it('should throw NotFound if no database?', async () => {\n      databaseRepository.update.mockRejectedValueOnce(new NotFoundException());\n      await expect(\n        service.update(\n          mockSessionMetadata,\n          mockDatabase.id,\n          classToClass(UpdateDatabaseDto, { password: 'password' }),\n          true,\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('test', () => {\n    describe('new connection', () => {\n      it('should successfully test valid connection config', async () => {\n        expect(\n          await service.testConnection(mockSessionMetadata, mockDatabase),\n        ).toEqual(undefined);\n      });\n      it('should successfully test valid sentinel config (without sentinelMaster)', async () => {\n        databaseFactory.createDatabaseModel.mockRejectedValueOnce(\n          new RedisConnectionSentinelMasterRequiredException(),\n        );\n        expect(\n          await service.testConnection(mockSessionMetadata, mockDatabase),\n        ).toEqual(undefined);\n      });\n      it('should throw connection error', async () => {\n        databaseFactory.createDatabaseModel.mockRejectedValueOnce(\n          new RedisConnectionUnavailableException(),\n        );\n\n        await expect(\n          service.testConnection(mockSessionMetadata, mockDatabase),\n        ).rejects.toThrow(RedisConnectionUnavailableException);\n      });\n      it('should not call get database by id', async () => {\n        const spy = jest.spyOn(service as any, 'get');\n\n        await service.testConnection(mockSessionMetadata, mockDatabase);\n        expect(spy).not.toBeCalled();\n      });\n    });\n\n    describe('exist connection', () => {\n      it('should get database by id', async () => {\n        const spy = jest\n          .spyOn(service as any, 'get')\n          .mockResolvedValueOnce(mockDatabase);\n\n        await service.testConnection(\n          mockSessionMetadata,\n          classToClass(UpdateDatabaseDto, {}),\n          mockDatabase.id,\n        );\n\n        expect(spy).toBeCalledWith(mockSessionMetadata, mockDatabase.id, false);\n      });\n\n      it('should test database connection with merged ssh options', async () => {\n        databaseRepository.get.mockResolvedValueOnce(\n          mockDatabaseWithSshPrivateKey,\n        );\n\n        await service.testConnection(\n          mockSessionMetadata,\n          classToClass(UpdateDatabaseDto, {\n            password: 'pass',\n            sshOptions: { passphrase: 'new passphrase' },\n          }),\n          mockDatabase.id,\n        );\n        expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n          mockSessionMetadata,\n          {\n            ...mockDatabaseWithSshPrivateKey,\n            password: 'pass',\n            sshOptions: {\n              ...mockDatabaseWithSshPrivateKey.sshOptions,\n              passphrase: 'new passphrase',\n            },\n          },\n        );\n      });\n\n      it('should test connection with new tls', async () => {\n        databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTlsAuth);\n\n        await service.testConnection(\n          mockSessionMetadata,\n          classToClass(UpdateDatabaseDto, {\n            compressor: Compressor.GZIP,\n            tls: true,\n            caCert: {\n              name: 'name',\n              certificate: '-----BEGIN CERTIFICATE-----\\ncertificate',\n            },\n          }),\n          mockDatabase.id,\n        );\n        expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n          mockSessionMetadata,\n          {\n            ...mockDatabaseWithTlsAuth,\n            compressor: Compressor.GZIP,\n            caCert: {\n              certificate: '-----BEGIN CERTIFICATE-----\\ncertificate',\n              name: 'name',\n            },\n          },\n        );\n      });\n\n      it('should test connection with exist tls', async () => {\n        databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTlsAuth);\n\n        await service.testConnection(\n          mockSessionMetadata,\n          classToClass(UpdateDatabaseDto, {\n            compressor: Compressor.GZIP,\n            tls: true,\n            caCert: {\n              id: 'new id',\n            },\n          }),\n          mockDatabase.id,\n        );\n        expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n          mockSessionMetadata,\n          {\n            ...mockDatabaseWithTlsAuth,\n            compressor: Compressor.GZIP,\n            caCert: {\n              id: 'new id',\n            },\n          },\n        );\n      });\n    });\n  });\n\n  describe('delete', () => {\n    it('should remove existing database', async () => {\n      expect(\n        await service.delete(mockSessionMetadata, mockDatabase.id),\n      ).toEqual(undefined);\n      expect(analytics.sendInstanceDeletedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase,\n      );\n    });\n    it('should throw NotFound if no database', async () => {\n      databaseRepository.get.mockResolvedValueOnce(null);\n      await expect(\n        service.delete(mockSessionMetadata, mockDatabase.id),\n      ).rejects.toThrow(NotFoundException);\n    });\n    it('should throw InternalServerErrorException? on any error during deletion', async () => {\n      databaseRepository.delete.mockRejectedValueOnce(new NotFoundException());\n      await expect(\n        service.delete(mockSessionMetadata, mockDatabase.id),\n      ).rejects.toThrow(InternalServerErrorException);\n    });\n  });\n\n  describe('bulkDelete', () => {\n    it('should remove multiple databases', async () => {\n      expect(\n        await service.bulkDelete(mockSessionMetadata, [mockDatabase.id]),\n      ).toEqual({ affected: 1 });\n    });\n    it('should ignore errors and do not count affected', async () => {\n      databaseRepository.delete.mockRejectedValueOnce(new NotFoundException());\n      expect(\n        await service.bulkDelete(mockSessionMetadata, [mockDatabase.id]),\n      ).toEqual({ affected: 0 });\n    });\n  });\n\n  describe('export', () => {\n    it('should return multiple databases without Standalone secrets', async () => {\n      databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTlsAuth);\n\n      expect(\n        await service.export(\n          mockSessionMetadata,\n          [mockDatabaseWithTlsAuth.id],\n          false,\n        ),\n      ).toEqual([\n        classToClass(ExportDatabase, omit(mockDatabaseWithTlsAuth, 'password')),\n      ]);\n    });\n\n    it('should return multiple databases without SSH secrets', async () => {\n      // remove SSH secrets\n      const mockDatabaseWithSshPrivateKeyTemp = {\n        ...mockDatabaseWithSshPrivateKey,\n      };\n      exportSecurityFields.forEach((field) => {\n        if (get(mockDatabaseWithSshPrivateKeyTemp, field)) {\n          update(mockDatabaseWithSshPrivateKeyTemp, field, () => undefined);\n        }\n      });\n\n      databaseRepository.get.mockResolvedValueOnce(\n        mockDatabaseWithSshPrivateKey,\n      );\n      expect(\n        await service.export(\n          mockSessionMetadata,\n          [mockDatabaseWithSshPrivateKey.id],\n          false,\n        ),\n      ).toEqual([\n        classToClass(ExportDatabase, mockDatabaseWithSshPrivateKeyTemp),\n      ]);\n    });\n\n    it('should return multiple databases without Sentinel secrets', async () => {\n      // remove secrets\n      const mockSentinelDatabaseWithTlsAuthTemp = {\n        ...mockSentinelDatabaseWithTlsAuth,\n      };\n      exportSecurityFields.forEach((field) => {\n        if (get(mockSentinelDatabaseWithTlsAuthTemp, field)) {\n          update(mockSentinelDatabaseWithTlsAuthTemp, field, () => null);\n        }\n      });\n\n      databaseRepository.get.mockResolvedValue(mockSentinelDatabaseWithTlsAuth);\n\n      expect(\n        await service.export(\n          mockSessionMetadata,\n          [mockSentinelDatabaseWithTlsAuth.id],\n          false,\n        ),\n      ).toEqual([\n        classToClass(\n          ExportDatabase,\n          omit(mockSentinelDatabaseWithTlsAuthTemp, 'password'),\n        ),\n      ]);\n    });\n\n    it('should return multiple databases with secrets', async () => {\n      // Standalone\n      databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTls);\n      expect(\n        await service.export(\n          mockSessionMetadata,\n          [mockDatabaseWithTls.id],\n          true,\n        ),\n      ).toEqual([classToClass(ExportDatabase, mockDatabaseWithTls)]);\n\n      // SSH\n      databaseRepository.get.mockResolvedValueOnce(\n        mockDatabaseWithSshPrivateKey,\n      );\n      expect(\n        await service.export(\n          mockSessionMetadata,\n          [mockDatabaseWithSshPrivateKey.id],\n          true,\n        ),\n      ).toEqual([classToClass(ExportDatabase, mockDatabaseWithSshPrivateKey)]);\n\n      // Sentinel\n      databaseRepository.get.mockResolvedValueOnce(\n        mockSentinelDatabaseWithTlsAuth,\n      );\n      expect(\n        await service.export(\n          mockSessionMetadata,\n          [mockSentinelDatabaseWithTlsAuth.id],\n          true,\n        ),\n      ).toEqual([\n        classToClass(ExportDatabase, mockSentinelDatabaseWithTlsAuth),\n      ]);\n    });\n\n    it('should ignore errors', async () => {\n      databaseRepository.get.mockRejectedValueOnce(new NotFoundException());\n      expect(\n        await service.export(mockSessionMetadata, [mockDatabase.id]),\n      ).toEqual([]);\n    });\n\n    it('should throw NotFoundException', async () => {\n      await expect(service.export(mockSessionMetadata, [])).rejects.toThrow(\n        NotFoundException,\n      );\n      try {\n        await service.export(mockSessionMetadata, []);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(NotFoundException);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID,\n        );\n      }\n    });\n\n    it('should return Azure Entra ID database without credentials and with providerDetails (withSecrets = false)', async () => {\n      databaseRepository.get.mockResolvedValueOnce(\n        mockDatabaseWithProviderDetails,\n      );\n\n      const result = await service.export(\n        mockSessionMetadata,\n        [mockDatabaseWithProviderDetails.id],\n        false,\n      );\n\n      expect(result).toHaveLength(1);\n      // Should strip username and password for Azure Entra ID databases\n      expect(result[0].username).toBeUndefined();\n      expect(result[0].password).toBeUndefined();\n      // Should include providerDetails\n      expect(result[0].providerDetails).toEqual(\n        mockDatabaseWithProviderDetails.providerDetails,\n      );\n    });\n\n    it('should return Azure Entra ID database without credentials even when withSecrets is true', async () => {\n      databaseRepository.get.mockResolvedValueOnce(\n        mockDatabaseWithProviderDetails,\n      );\n\n      const result = await service.export(\n        mockSessionMetadata,\n        [mockDatabaseWithProviderDetails.id],\n        true,\n      );\n\n      expect(result).toHaveLength(1);\n      // Credentials should always be stripped for Azure Entra ID\n      expect(result[0].username).toBeUndefined();\n      expect(result[0].password).toBeUndefined();\n      // Should include providerDetails\n      expect(result[0].providerDetails).toEqual(\n        mockDatabaseWithProviderDetails.providerDetails,\n      );\n    });\n  });\n\n  describe('clone', () => {\n    it('should create new database', async () => {\n      const spy = jest.spyOn(service as any, 'create');\n      await service.clone(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          username: 'new-name',\n          timeout: 40_000,\n        }),\n      );\n      expect(spy).toBeCalledWith(\n        mockSessionMetadata,\n        omit({ ...mockDatabase, username: 'new-name', timeout: 40_000 }, [\n          'sshOptions.id',\n        ]),\n      );\n      expect(databaseRepository.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase.id,\n        false,\n        ['id', 'sshOptions.id', 'createdAt'],\n      );\n    });\n\n    it('should create new database that was created by discovery process without pre setup flag', async () => {\n      databaseRepository.get.mockResolvedValueOnce({\n        ...mockDatabase,\n        isPreSetup: true,\n      });\n\n      const spy = jest.spyOn(service as any, 'create');\n      await service.clone(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          username: 'new-name',\n          timeout: 40_000,\n        }),\n      );\n      expect(spy).toBeCalledWith(\n        mockSessionMetadata,\n        omit(\n          {\n            ...mockDatabase,\n            username: 'new-name',\n            timeout: 40_000,\n            isPreSetup: false,\n          },\n          ['sshOptions.id'],\n        ),\n      );\n      expect(databaseRepository.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase.id,\n        false,\n        ['id', 'sshOptions.id', 'createdAt'],\n      );\n    });\n\n    it('should create new database with merged ssh options', async () => {\n      databaseRepository.get.mockResolvedValueOnce(\n        mockDatabaseWithSshPrivateKey,\n      );\n\n      await service.clone(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          password: 'pass',\n          sshOptions: {\n            password: 'new password',\n            passphrase: null,\n            privateKey: null,\n          },\n        }),\n      );\n      expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n        mockSessionMetadata,\n        {\n          ...omit(mockDatabaseWithSshPrivateKey),\n          password: 'pass',\n          sshOptions: {\n            ...mockDatabaseWithSshPrivateKey.sshOptions,\n            password: 'new password',\n            passphrase: null,\n            privateKey: null,\n          },\n        },\n        {},\n      );\n    });\n\n    it('should update existing database with new ssh options', async () => {\n      databaseRepository.get.mockResolvedValue(mockDatabase);\n\n      await service.clone(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          ssh: true,\n          sshOptions: {\n            host: 'ssh.host.test',\n            port: 22,\n            username: 'ssh-username',\n            password: null,\n            privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\\nssh-private-key',\n            passphrase: 'ssh-passphrase',\n          },\n        }),\n      );\n      expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          ssh: true,\n          sshOptions: {\n            host: 'ssh.host.test',\n            port: 22,\n            username: 'ssh-username',\n            password: null,\n            privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\\nssh-private-key',\n            passphrase: 'ssh-passphrase',\n          },\n        },\n        {},\n      );\n    });\n\n    it('should create new database with new tls', async () => {\n      databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTlsAuth);\n\n      await service.clone(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          compressor: Compressor.GZIP,\n          tls: true,\n          caCert: {\n            name: 'name',\n            certificate: '-----BEGIN CERTIFICATE-----\\ncertificate',\n          },\n        }),\n      );\n      expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockDatabaseWithTlsAuth,\n          compressor: Compressor.GZIP,\n          caCert: {\n            certificate: '-----BEGIN CERTIFICATE-----\\ncertificate',\n            name: 'name',\n          },\n        },\n        {},\n      );\n    });\n\n    it('should create new database with exist tls', async () => {\n      databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTlsAuth);\n\n      await service.clone(\n        mockSessionMetadata,\n        mockDatabase.id,\n        classToClass(UpdateDatabaseDto, {\n          compressor: Compressor.GZIP,\n          tls: true,\n          caCert: {\n            id: 'new id',\n          },\n        }),\n      );\n      expect(databaseFactory.createDatabaseModel).toBeCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockDatabaseWithTlsAuth,\n          compressor: Compressor.GZIP,\n          caCert: {\n            id: 'new id',\n          },\n        },\n        {},\n      );\n    });\n\n    describe('create database model', () => {\n      test.each(updateDatabaseTests)('%j', async ({ input, expected }) => {\n        const spy = jest.spyOn(service as any, 'create');\n\n        databaseRepository.update.mockReturnValue(null);\n        await service.clone(\n          mockSessionMetadata,\n          mockDatabase.id,\n          input as UpdateDatabaseDto,\n        );\n        expect(spy).toBeCalledTimes(expected);\n        expect(analytics.sendInstanceAddedEvent).toBeCalled();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/database.service.ts",
    "content": "import {\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { isEmpty, omit, reject, sum, omitBy, isUndefined } from 'lodash';\nimport { Database } from 'src/modules/database/models/database';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { classToClass } from 'src/utils';\nimport { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { DatabaseFactory } from 'src/modules/database/providers/database.factory';\nimport { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto';\nimport { AppRedisInstanceEvents } from 'src/constants';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response';\nimport { ClientContext, SessionMetadata } from 'src/common/models';\nimport { ExportDatabase } from 'src/modules/database/models/export-database';\nimport { deepMerge } from 'src/common/utils';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport {\n  IRedisConnectionOptions,\n  RedisClientFactory,\n} from 'src/modules/redis/redis.client.factory';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { RedisConnectionSentinelMasterRequiredException } from 'src/modules/redis/exceptions/connection';\nimport { CredentialStrategyProvider } from 'src/modules/database/credentials/credential-strategy.provider';\nimport { AzureEntraIdCredentialStrategy } from 'src/modules/database/credentials/strategies/azure-entra-id.credential-strategy';\n\n@Injectable()\nexport class DatabaseService {\n  private logger = new Logger('DatabaseService');\n\n  private readonly exportSecurityFields: string[] = [\n    'password',\n    'clientCert.key',\n    'sshOptions.password',\n    'sshOptions.passphrase',\n    'sshOptions.privateKey',\n    'sentinelMaster.password',\n  ];\n\n  static connectionFields: string[] = [\n    'host',\n    'port',\n    'db',\n    'username',\n    'password',\n    'tls',\n    'tlsServername',\n    'verifyServerCert',\n    'sentinelMaster',\n    'ssh',\n    'sshOptions',\n    'caCert',\n    'clientCert',\n  ];\n\n  static endpointFields: string[] = ['host', 'port'];\n\n  constructor(\n    private repository: DatabaseRepository,\n    private redisClientStorage: RedisClientStorage,\n    private redisClientFactory: RedisClientFactory,\n    private databaseInfoProvider: DatabaseInfoProvider,\n    private databaseFactory: DatabaseFactory,\n    private analytics: DatabaseAnalytics,\n    private eventEmitter: EventEmitter2,\n    private credentialProvider: CredentialStrategyProvider,\n  ) {}\n\n  static isConnectionAffected(dto: object) {\n    return Object.keys(omitBy(dto, isUndefined)).some((field) =>\n      this.connectionFields.includes(field),\n    );\n  }\n\n  static isEndpointAffected(dto: object) {\n    return Object.keys(omitBy(dto, isUndefined)).some((field) =>\n      this.endpointFields.includes(field),\n    );\n  }\n\n  private async merge(\n    database: Database,\n    dto: UpdateDatabaseDto,\n  ): Promise<Database> {\n    const updatedDatabase = database;\n    if (dto?.caCert) {\n      updatedDatabase.caCert = dto.caCert as CaCertificate;\n    }\n\n    if (dto?.clientCert) {\n      updatedDatabase.clientCert = dto.clientCert as ClientCertificate;\n    }\n    return deepMerge(updatedDatabase, dto) as Database;\n  }\n\n  /**\n   * Simply checks if database exists\n   * @param sessionMetadata\n   * @param id\n   */\n  async exists(sessionMetadata: SessionMetadata, id: string): Promise<boolean> {\n    this.logger.debug(\n      `Checking if database with ${id} exists.`,\n      sessionMetadata,\n    );\n    return this.repository.exists(sessionMetadata, id);\n  }\n\n  /**\n   * Get list of databases\n   * TBD add pagination, filters, sorting, search, etc.\n   * @param sessionMetadata\n   */\n  async list(sessionMetadata: SessionMetadata): Promise<Database[]> {\n    try {\n      this.logger.debug('Getting databases list', sessionMetadata);\n      return await this.repository.list(sessionMetadata);\n    } catch (e) {\n      this.logger.error(\n        'Failed to get database instance list.',\n        e,\n        sessionMetadata,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n\n  /**\n   * Gets full database model by id\n   * @param sessionMetadata\n   * @param id\n   * @param ignoreEncryptionErrors\n   */\n  async get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    ignoreEncryptionErrors = false,\n    omitFields?: string[],\n  ): Promise<Database> {\n    this.logger.debug(`Getting database ${id}`, sessionMetadata);\n\n    if (!id) {\n      this.logger.error('Database id was not provided', sessionMetadata);\n      throw new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n    }\n\n    const model = await this.repository.get(\n      sessionMetadata,\n      id,\n      ignoreEncryptionErrors,\n      omitFields,\n    );\n\n    if (!model) {\n      this.logger.error(`Database with ${id} was not Found`, sessionMetadata);\n      throw new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n    }\n\n    return model;\n  }\n\n  /**\n   * Create new database with auto-detection of database type, modules, etc.\n   * @param sessionMetadata\n   * @param dto\n   * @param uniqueCheck\n   * @param options\n   */\n  async create(\n    sessionMetadata: SessionMetadata,\n    dto: CreateDatabaseDto,\n    uniqueCheck = false,\n    options: IRedisConnectionOptions = {},\n  ): Promise<Database> {\n    try {\n      this.logger.debug('Creating new database.', sessionMetadata);\n\n      let database = await this.repository.create(\n        sessionMetadata,\n        {\n          ...(await this.databaseFactory.createDatabaseModel(\n            sessionMetadata,\n            classToClass(Database, dto),\n            options,\n          )),\n          new: true,\n        },\n        uniqueCheck,\n      );\n\n      // todo: clarify if we need this and if yes - rethink implementation\n      try {\n        const databaseWithCredentials =\n          await this.credentialProvider.resolve(database);\n\n        const client = await this.redisClientFactory.createClient(\n          {\n            sessionMetadata,\n            databaseId: database.id,\n            context: ClientContext.Common,\n          },\n          databaseWithCredentials,\n        );\n        const redisInfo =\n          await this.databaseInfoProvider.getRedisGeneralInfo(client);\n        this.analytics.sendInstanceAddedEvent(\n          sessionMetadata,\n          database,\n          redisInfo,\n        );\n        await client.disconnect();\n      } catch (e) {\n        // ignore error\n      }\n\n      return database;\n    } catch (error) {\n      this.logger.error('Failed to add database.', error, sessionMetadata);\n\n      this.analytics.sendInstanceAddFailedEvent(sessionMetadata, error);\n\n      throw error;\n    }\n  }\n\n  /**\n   * Update database model by id\n   * @param sessionMetadata\n   * @param id\n   * @param dto\n   * @param manualUpdate\n   */\n  public async update(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    dto: UpdateDatabaseDto,\n    manualUpdate: boolean = true, // todo: remove manualUpdate flag logic\n  ): Promise<Database> {\n    this.logger.debug(`Updating database: ${id}`, sessionMetadata);\n    const oldDatabase = await this.get(sessionMetadata, id, true);\n\n    let database: Database;\n    try {\n      database = await this.merge(oldDatabase, dto);\n\n      if (DatabaseService.isConnectionAffected(dto)) {\n        if (DatabaseService.isEndpointAffected(dto)) {\n          database.provider = undefined;\n        }\n\n        database = await this.databaseFactory.createDatabaseModel(\n          sessionMetadata,\n          database,\n        );\n\n        await this.redisClientStorage.removeManyByMetadata({ databaseId: id });\n      }\n\n      database = await this.repository.update(sessionMetadata, id, database);\n\n      // todo: rethink\n      this.analytics.sendInstanceEditedEvent(\n        sessionMetadata,\n        oldDatabase,\n        database,\n        manualUpdate,\n      );\n\n      return database;\n    } catch (error) {\n      this.logger.error(\n        `Failed to update database instance ${id}`,\n        error,\n        sessionMetadata,\n      );\n\n      throw error;\n    }\n  }\n\n  /**\n   * Test connection for new/modified config before creating/updating database\n   * @param sessionMetadata\n   * @param dto\n   * @param id\n   */\n  public async testConnection(\n    sessionMetadata: SessionMetadata,\n    dto: CreateDatabaseDto | UpdateDatabaseDto,\n    id?: string,\n  ): Promise<void> {\n    let database: Database;\n\n    if (id) {\n      this.logger.debug(\n        'Testing existing database connection',\n        sessionMetadata,\n      );\n\n      database = await this.merge(\n        await this.get(sessionMetadata, id, false),\n        dto,\n      );\n    } else {\n      this.logger.debug('Testing new database connection', sessionMetadata);\n      database = classToClass(Database, dto);\n    }\n\n    try {\n      await this.databaseFactory.createDatabaseModel(sessionMetadata, database);\n\n      return;\n    } catch (error) {\n      // don't throw an error to support sentinel autodiscovery flow\n      if (error instanceof RedisConnectionSentinelMasterRequiredException) {\n        return;\n      }\n\n      this.logger.error('Connection test failed', error, sessionMetadata);\n\n      throw error;\n    }\n  }\n\n  /**\n   * Clone database with updated fields\n   * @param sessionMetadata\n   * @param id\n   * @param dto\n   */\n  public async clone(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    dto: UpdateDatabaseDto,\n  ): Promise<Database> {\n    this.logger.debug('Clone existing database', sessionMetadata);\n    const database = await this.merge(\n      await this.get(sessionMetadata, id, false, [\n        'id',\n        'sshOptions.id',\n        'createdAt',\n      ]),\n      dto,\n    );\n\n    // disable pre setup flag so database won't be automatically removed\n    database.isPreSetup = false;\n\n    if (DatabaseService.isConnectionAffected(dto)) {\n      return await this.create(sessionMetadata, database);\n    }\n\n    const createdDatabase = await this.repository.create(\n      sessionMetadata,\n      {\n        ...classToClass(Database, database),\n        new: true,\n      },\n      false,\n    );\n\n    this.analytics.sendInstanceAddedEvent(sessionMetadata, createdDatabase);\n    return createdDatabase;\n  }\n\n  /**\n   * Delete database instance by id\n   * Also close all opened connections for this database\n   * Also emit an event to entire app to be processed by other parts\n   * @param sessionMetadata\n   * @param id\n   */\n  async delete(sessionMetadata: SessionMetadata, id: string): Promise<void> {\n    this.logger.debug(`Deleting database: ${id}`, sessionMetadata);\n    const database = await this.get(sessionMetadata, id, true);\n    try {\n      await this.repository.delete(sessionMetadata, id);\n      // todo: rethink\n      await this.redisClientStorage.removeManyByMetadata({ databaseId: id });\n      this.logger.debug(\n        'Succeed to delete database instance.',\n        sessionMetadata,\n      );\n\n      this.analytics.sendInstanceDeletedEvent(sessionMetadata, database);\n      this.eventEmitter.emit(AppRedisInstanceEvents.Deleted, id);\n    } catch (error) {\n      this.logger.error(\n        `Failed to delete database: ${id}`,\n        error,\n        sessionMetadata,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n\n  /**\n   * Bulk delete databases. Uses \"delete\" method and skipping error\n   * Returns successfully deleted databases number\n   * @param sessionMetadata\n   * @param ids\n   */\n  async bulkDelete(\n    sessionMetadata: SessionMetadata,\n    ids: string[],\n  ): Promise<DeleteDatabasesResponse> {\n    this.logger.debug(`Deleting many database: ${ids}`, sessionMetadata);\n\n    return {\n      affected: sum(\n        await Promise.all(\n          ids.map(async (id) => {\n            try {\n              await this.delete(sessionMetadata, id);\n              return 1;\n            } catch (e) {\n              return 0;\n            }\n          }),\n        ),\n      ),\n    };\n  }\n\n  /**\n   * Export many databases by ids.\n   * Get full database model. With or without passwords and certificates bodies.\n   * @param sessionMetadata\n   * @param ids\n   * @param withSecrets\n   */\n  async export(\n    sessionMetadata: SessionMetadata,\n    ids: string[],\n    withSecrets = false,\n  ): Promise<ExportDatabase[]> {\n    this.logger.debug(`Exporting many database: ${ids}`, sessionMetadata);\n\n    if (!ids.length) {\n      this.logger.error('Database ids were not provided', sessionMetadata);\n      throw new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID);\n    }\n\n    const entities: Database[] = reject(\n      await Promise.all(\n        ids.map(async (id) => {\n          try {\n            return await this.get(sessionMetadata, id);\n          } catch (e) {\n            // ignore\n          }\n        }),\n      ),\n      isEmpty,\n    );\n\n    return entities.map((database) => {\n      // For Azure Entra ID databases, always strip credentials\n      // (username/password are temporary tokens) and include providerDetails\n      const isAzureEntraId = AzureEntraIdCredentialStrategy.isAzureEntraIdAuth(\n        database.providerDetails,\n      );\n\n      let paths: string[];\n      if (isAzureEntraId) {\n        // Always strip credentials for Azure Entra ID databases\n        paths = [...this.exportSecurityFields, 'username'];\n      } else {\n        paths = !withSecrets ? this.exportSecurityFields : [];\n      }\n\n      return classToClass(ExportDatabase, omit(database, paths), {\n        groups: ['security'],\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/create.database.dto.ts",
    "content": "import {\n  ApiExtraModels,\n  ApiPropertyOptional,\n  getSchemaPath,\n  PickType,\n} from '@nestjs/swagger';\nimport { Database } from 'src/modules/database/models/database';\nimport { Expose, Type } from 'class-transformer';\nimport {\n  IsArray,\n  IsNotEmptyObject,\n  IsOptional,\n  ValidateNested,\n} from 'class-validator';\nimport { NoDuplicatesByKey } from 'src/common/decorators';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\nimport { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto';\nimport { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';\nimport { caCertTransformer } from 'src/modules/certificate/transformers/ca-cert.transformer';\nimport { clientCertTransformer } from 'src/modules/certificate/transformers/client-cert.transformer';\nimport { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';\nimport { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';\nimport { sshOptionsTransformer } from 'src/modules/ssh/transformers/ssh-options.transformer';\nimport { CloudDatabaseDetails } from 'src/modules/cloud/database/models/cloud-database-details';\nimport { AzureProviderDetails } from 'src/modules/database/models/provider-details';\nimport { CreateTagDto } from 'src/modules/tag/dto';\n\n@ApiExtraModels(\n  CreateCaCertificateDto,\n  UseCaCertificateDto,\n  CreateClientCertificateDto,\n  UseClientCertificateDto,\n  CreateBasicSshOptionsDto,\n  CreateCertSshOptionsDto,\n)\nexport class CreateDatabaseDto extends PickType(Database, [\n  'host',\n  'port',\n  'name',\n  'db',\n  'username',\n  'password',\n  'timeout',\n  'nameFromProvider',\n  'provider',\n  'tls',\n  'tlsServername',\n  'verifyServerCert',\n  'sentinelMaster',\n  'ssh',\n  'compressor',\n  'cloudDetails',\n  'providerDetails',\n  'forceStandalone',\n  'keyNameFormat',\n] as const) {\n  @ApiPropertyOptional({\n    description: 'CA Certificate',\n    oneOf: [\n      { $ref: getSchemaPath(CreateCaCertificateDto) },\n      { $ref: getSchemaPath(UseCaCertificateDto) },\n    ],\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(caCertTransformer)\n  @ValidateNested()\n  caCert?: CreateCaCertificateDto | UseCaCertificateDto;\n\n  @ApiPropertyOptional({\n    description: 'Client Certificate',\n    oneOf: [\n      { $ref: getSchemaPath(CreateClientCertificateDto) },\n      { $ref: getSchemaPath(UseCaCertificateDto) },\n    ],\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(clientCertTransformer)\n  @ValidateNested()\n  clientCert?: CreateClientCertificateDto | UseClientCertificateDto;\n\n  @ApiPropertyOptional({\n    description: 'SSH Options',\n    oneOf: [\n      { $ref: getSchemaPath(CreateBasicSshOptionsDto) },\n      { $ref: getSchemaPath(CreateCertSshOptionsDto) },\n    ],\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(sshOptionsTransformer)\n  @ValidateNested()\n  sshOptions?: CreateBasicSshOptionsDto | CreateCertSshOptionsDto;\n\n  @ApiPropertyOptional({\n    description: 'Cloud details',\n    type: CloudDatabaseDetails,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => CloudDatabaseDetails)\n  @ValidateNested()\n  cloudDetails?: CloudDatabaseDetails;\n\n  @ApiPropertyOptional({\n    description: 'Provider-specific metadata',\n    type: AzureProviderDetails,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => AzureProviderDetails)\n  @ValidateNested()\n  providerDetails?: AzureProviderDetails;\n\n  @ApiPropertyOptional({\n    description: 'Tags associated with the database.',\n    type: CreateTagDto,\n    isArray: true,\n  })\n  @Expose()\n  @IsOptional()\n  @IsArray()\n  @NoDuplicatesByKey('key', {\n    message: 'Tags must not contain duplicates by key.',\n  })\n  @Type(() => CreateTagDto)\n  tags?: CreateTagDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/database.response.ts",
    "content": "import { ApiPropertyOptional, OmitType } from '@nestjs/swagger';\nimport { Database } from 'src/modules/database/models/database';\nimport { SshOptionsResponse } from 'src/modules/ssh/dto/ssh-options.response';\nimport { SentinelMasterResponse } from 'src/modules/redis-sentinel/dto/sentinel.master.response.dto';\nimport { Expose, Type } from 'class-transformer';\nimport { HiddenField } from 'src/common/decorators/hidden-field.decorator';\n\nexport class DatabaseResponse extends OmitType(Database, [\n  'password',\n  'sshOptions',\n  'sentinelMaster',\n] as const) {\n  @ApiPropertyOptional({\n    description: 'The database password flag (true if password was set)',\n    type: Boolean,\n  })\n  @Expose()\n  @HiddenField(true)\n  password?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Ssh options',\n    type: SshOptionsResponse,\n  })\n  @Expose()\n  @Type(() => SshOptionsResponse)\n  sshOptions?: SshOptionsResponse;\n\n  @ApiPropertyOptional({\n    description: 'Sentinel master',\n    type: SentinelMasterResponse,\n  })\n  @Expose()\n  @Type(() => SentinelMasterResponse)\n  sentinelMaster?: SentinelMasterResponse;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/delete.databases.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class DeleteDatabasesDto {\n  @ApiProperty({\n    description: 'The unique ID of the database requested',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  ids: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/delete.databases.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteDatabasesResponse {\n  @ApiProperty({\n    description: 'Number of affected database instances',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/export.databases.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsBoolean,\n  IsDefined,\n  IsOptional,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class ExportDatabasesDto {\n  @ApiProperty({\n    description: 'The unique IDs of the databases requested',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  ids: string[] = [];\n\n  @ApiPropertyOptional({\n    description: 'Export passwords and certificate bodies',\n    type: Boolean,\n  })\n  @IsBoolean()\n  @IsOptional()\n  withSecrets?: boolean = false;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/redis-info.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nclass RedisDatabaseStatsDto {\n  @ApiProperty({\n    type: String,\n  })\n  instantaneous_input_kbps: string | undefined;\n\n  @ApiProperty({\n    type: String,\n  })\n  instantaneous_ops_per_sec: string | undefined;\n\n  @ApiProperty({\n    type: String,\n  })\n  instantaneous_output_kbps: string | undefined;\n\n  @ApiProperty({\n    type: Number,\n  })\n  maxmemory_policy: string | undefined;\n\n  @ApiProperty({\n    description: 'Redis database mode',\n    type: String,\n  })\n  numberOfKeysRange: string | undefined;\n\n  @ApiProperty({\n    description: 'Redis database role',\n    type: String,\n  })\n  uptime_in_days: string | undefined;\n}\n\nexport class RedisNodeInfoResponse {\n  @ApiProperty({\n    description: 'Redis database version',\n    type: String,\n  })\n  version: string;\n\n  @ApiPropertyOptional({\n    description:\n      'Value is \"master\" if the instance is replica of no one, ' +\n      'or \"slave\" if the instance is a replica of some master instance',\n    enum: ['master', 'slave'],\n    default: 'master',\n  })\n  role?: 'master' | 'slave';\n\n  @ApiPropertyOptional({\n    description: 'Redis database info from server section',\n    type: Object,\n  })\n  server?: any;\n\n  @ApiPropertyOptional({\n    description: 'Various Redis stats',\n    type: RedisDatabaseStatsDto,\n  })\n  stats?: RedisDatabaseStatsDto;\n\n  @ApiPropertyOptional({\n    description: 'The number of Redis databases',\n    type: Number,\n    default: 16,\n  })\n  databases?: number;\n\n  @ApiPropertyOptional({\n    description: 'Total number of bytes allocated by Redis using',\n    type: Number,\n  })\n  usedMemory?: number;\n\n  @ApiPropertyOptional({\n    description: 'Total number of keys inside Redis database',\n    type: Number,\n  })\n  totalKeys?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Number of client connections (excluding connections from replicas)',\n    type: Number,\n  })\n  connectedClients?: number;\n\n  @ApiPropertyOptional({\n    description: 'Number of seconds since Redis server start',\n    type: Number,\n  })\n  uptimeInSeconds?: number;\n\n  @ApiPropertyOptional({\n    description: 'The cache hit ratio represents the efficiency of cache usage',\n    type: Number,\n  })\n  hitRatio?: number;\n\n  @ApiPropertyOptional({\n    description: 'The number of the cached lua scripts',\n    type: Number,\n  })\n  cashedScripts?: number;\n}\n\nexport class RedisDatabaseInfoResponse extends RedisNodeInfoResponse {\n  @ApiProperty({\n    description: 'Redis database version',\n    type: String,\n  })\n  version: string;\n\n  @ApiPropertyOptional({\n    description: 'Nodes info',\n    type: RedisNodeInfoResponse,\n    isArray: true,\n  })\n  nodes?: RedisNodeInfoResponse[];\n}\n\nexport class RedisDatabaseModuleDto {\n  @ApiProperty({\n    description: 'Redis module name',\n    type: String,\n  })\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'Redis module version',\n    type: Number,\n    isArray: true,\n  })\n  ver?: number;\n}\n\nexport class RedisDatabaseHelloResponse {\n  @ApiProperty({\n    description: 'Redis database id',\n    type: Number,\n  })\n  id: number;\n\n  @ApiProperty({\n    description: 'Redis database server name',\n    type: String,\n  })\n  server: string;\n\n  @ApiProperty({\n    description: 'Redis database version',\n    type: String,\n  })\n  version: string;\n\n  @ApiProperty({\n    description: 'Redis database proto',\n    type: Number,\n  })\n  proto: number;\n\n  @ApiProperty({\n    description: 'Redis database mode',\n    type: String,\n  })\n  mode: 'standalone' | 'sentinel' | 'cluster';\n\n  @ApiProperty({\n    description: 'Redis database role',\n    type: String,\n  })\n  role: 'master' | 'slave';\n\n  @ApiProperty({\n    description: 'Redis database modules',\n    type: RedisDatabaseModuleDto,\n    isArray: true,\n  })\n  modules: RedisDatabaseModuleDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/dto/update.database.dto.ts",
    "content": "import { ApiPropertyOptional, OmitType, PartialType } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport {\n  IsInt,\n  IsNotEmpty,\n  IsNotEmptyObject,\n  IsOptional,\n  Max,\n  Min,\n  ValidateNested,\n  ValidateIf,\n  IsString,\n  MaxLength,\n} from 'class-validator';\nimport { UpdateSshOptionsDto } from 'src/modules/ssh/dto/update.ssh-options.dto';\nimport { UpdateSentinelMasterDto } from 'src/modules/redis-sentinel/dto/update.sentinel.master.dto';\nimport { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';\n\nexport class UpdateDatabaseDto extends PartialType(\n  OmitType(CreateDatabaseDto, [\n    'sshOptions',\n    'timeout',\n    'sentinelMaster',\n  ] as const),\n) {\n  @ValidateIf((object, value) => value !== undefined)\n  @IsString({ always: true })\n  @MaxLength(500)\n  name: string;\n\n  @ValidateIf((object, value) => value !== undefined)\n  @IsString({ always: true })\n  host: string;\n\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt({ always: true })\n  port: number;\n\n  @ApiPropertyOptional({\n    description: 'Updated ssh options fields',\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => UpdateSshOptionsDto)\n  @ValidateNested()\n  sshOptions?: UpdateSshOptionsDto;\n\n  @ApiPropertyOptional({\n    description: 'Connection timeout',\n    type: Number,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsOptional()\n  @Min(1_000)\n  @Max(1_000_000_000)\n  @IsInt({ always: true })\n  timeout?: number;\n\n  @ApiPropertyOptional({\n    description: 'Updated sentinel master fields',\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => UpdateSentinelMasterDto)\n  @ValidateNested()\n  sentinelMaster?: UpdateSentinelMasterDto;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/entities/database.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  OneToOne,\n  PrimaryGeneratedColumn,\n  ManyToMany,\n  JoinTable,\n} from 'typeorm';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { DataAsJsonString } from 'src/common/decorators';\nimport { Expose, Transform, Type } from 'class-transformer';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/cloud-database-details.entity';\nimport { DatabaseSettingsEntity } from 'src/modules/database-settings/entities/database-setting.entity';\nimport { TagEntity } from 'src/modules/tag/entities/tag.entity';\n\nexport enum HostingProvider {\n  REDIS_SOFTWARE = 'REDIS_SOFTWARE',\n  REDIS_CLOUD = 'REDIS_CLOUD',\n  REDIS_STACK = 'REDIS_STACK',\n  OTHER_REDIS_MANAGED = 'OTHER_REDIS_MANAGED',\n  AZURE_CACHE = 'AZURE_CACHE',\n  AZURE_CACHE_REDIS_ENTERPRISE = 'AZURE_CACHE_REDIS_ENTERPRISE',\n  REDIS_COMMUNITY_EDITION = 'REDIS_COMMUNITY_EDITION',\n  AWS_ELASTICACHE = 'AWS_ELASTICACHE',\n  AWS_MEMORYDB = 'AWS_MEMORYDB',\n  VALKEY = 'VALKEY',\n  MEMORYSTORE = 'MEMORYSTORE',\n  DRAGONFLY = 'DRAGONFLY',\n  KEYDB = 'KEYDB',\n  GARNET = 'GARNET',\n  KVROCKS = 'KVROCKS',\n  REDICT = 'REDICT',\n  UPSTASH = 'UPSTASH',\n  UNKNOWN_LOCALHOST = 'UNKNOWN_LOCALHOST',\n  UNKNOWN = 'UNKNOWN',\n}\n\nexport enum ConnectionType {\n  STANDALONE = 'STANDALONE',\n  CLUSTER = 'CLUSTER',\n  SENTINEL = 'SENTINEL',\n  NOT_CONNECTED = 'NOT CONNECTED',\n}\n\nexport enum Compressor {\n  NONE = 'NONE',\n  GZIP = 'GZIP',\n  ZSTD = 'ZSTD',\n  LZ4 = 'LZ4',\n  SNAPPY = 'SNAPPY',\n  Brotli = 'Brotli',\n  PHPGZCompress = 'PHPGZCompress',\n}\n\nexport enum Encoding {\n  UNICODE = 'Unicode',\n  HEX = 'HEX',\n}\n\n@Entity('database_instance')\nexport class DatabaseEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  host: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  port: number;\n\n  @Expose()\n  @Column({ nullable: false })\n  name: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  db: number;\n\n  @Expose()\n  @Column({ nullable: true })\n  username: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  password: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  timeout: number;\n\n  @Expose()\n  @Column({ nullable: true })\n  @Transform(({ obj }) => obj?.sentinelMaster?.name, { toClassOnly: true })\n  sentinelMasterName: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  @Transform(({ obj }) => obj?.sentinelMaster?.username, { toClassOnly: true })\n  sentinelMasterUsername: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  @Transform(({ obj }) => obj?.sentinelMaster?.password, { toClassOnly: true })\n  sentinelMasterPassword: string;\n\n  @Expose()\n  @Transform(\n    ({ obj }) => {\n      if (obj?.sentinelMasterName) {\n        return {\n          name: obj?.sentinelMasterName,\n          username: obj?.sentinelMasterUsername,\n          password: obj?.sentinelMasterPassword,\n        };\n      }\n\n      return undefined;\n    },\n    { toPlainOnly: true },\n  )\n  @Transform(() => undefined, { toClassOnly: true })\n  sentinelMaster: SentinelMaster;\n\n  @Expose()\n  @Column({ nullable: true })\n  tls: boolean;\n\n  @Expose()\n  @Column({ nullable: true })\n  tlsServername?: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  verifyServerCert: boolean;\n\n  @Expose()\n  @ManyToOne(\n    () => CaCertificateEntity,\n    (caCertificate) => caCertificate.databases,\n    {\n      eager: true,\n      onDelete: 'SET NULL',\n    },\n  )\n  caCert: CaCertificateEntity;\n\n  @Expose()\n  @ManyToOne(\n    () => ClientCertificateEntity,\n    (clientCertificate) => clientCertificate.databases,\n    {\n      eager: true,\n      onDelete: 'SET NULL',\n    },\n  )\n  clientCert: ClientCertificateEntity;\n\n  @Expose()\n  @Column({\n    nullable: false,\n    default: ConnectionType.STANDALONE,\n  })\n  connectionType: ConnectionType;\n\n  @Expose()\n  @Column({ nullable: true })\n  nameFromProvider: string;\n\n  @Expose()\n  @Column({ nullable: true, default: '[]' })\n  @DataAsJsonString()\n  nodes: string;\n\n  @Expose()\n  @Column({ type: 'datetime', nullable: true })\n  lastConnection: Date;\n\n  @CreateDateColumn({\n    nullable: true,\n  })\n  @Expose()\n  createdAt: Date;\n\n  @Expose()\n  @Column({\n    nullable: true,\n    default: HostingProvider.UNKNOWN,\n  })\n  provider: string;\n\n  @Expose()\n  @Column({ nullable: false, default: '[]' })\n  @DataAsJsonString()\n  modules: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  new: boolean;\n\n  @Expose()\n  @Column({ nullable: true })\n  ssh: boolean;\n\n  @Expose()\n  @OneToOne(() => SshOptionsEntity, (sshOptions) => sshOptions.database, {\n    eager: true,\n    onDelete: 'CASCADE',\n    cascade: true,\n  })\n  @Type(() => SshOptionsEntity)\n  sshOptions: SshOptionsEntity;\n\n  @Expose()\n  @OneToOne(\n    () => CloudDatabaseDetailsEntity,\n    (cloudDetails) => cloudDetails.database,\n    {\n      eager: true,\n      onDelete: 'CASCADE',\n      cascade: true,\n    },\n  )\n  @Type(() => CloudDatabaseDetailsEntity)\n  cloudDetails: CloudDatabaseDetailsEntity;\n\n  @Expose()\n  @Column({ nullable: true, type: 'text' })\n  @DataAsJsonString()\n  providerDetails: string;\n\n  @Expose()\n  @OneToOne(() => DatabaseSettingsEntity, (dbSettings) => dbSettings.database, {\n    eager: true,\n    onDelete: 'CASCADE',\n    cascade: true,\n  })\n  @Type(() => DatabaseSettingsEntity)\n  dbSettings: DatabaseSettingsEntity;\n\n  @Expose()\n  @Column({\n    nullable: false,\n    default: Compressor.NONE,\n  })\n  compressor: Compressor;\n\n  @Expose()\n  @Column({ nullable: true })\n  version: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  forceStandalone: boolean;\n\n  @Expose()\n  @ManyToMany(() => TagEntity, (tag) => tag.databases, {\n    eager: true,\n    cascade: true,\n    onDelete: 'CASCADE',\n  })\n  @JoinTable({\n    name: 'database_tag',\n    joinColumn: {\n      name: 'databaseId',\n      referencedColumnName: 'id',\n    },\n    inverseJoinColumn: {\n      name: 'tagId',\n      referencedColumnName: 'id',\n    },\n  })\n  @Type(() => TagEntity)\n  tags: TagEntity[];\n\n  @Expose()\n  @Column({ nullable: true })\n  isPreSetup: boolean;\n\n  @Expose()\n  @Column({ nullable: true, default: Encoding.UNICODE })\n  keyNameFormat: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/exeptions/database-already-exists.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class DatabaseAlreadyExistsException extends HttpException {\n  constructor(\n    databaseId: string,\n    message = ERROR_MESSAGES.DATABASE_ALREADY_EXISTS,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.CONFLICT,\n      error: 'DatabaseAlreadyExists',\n      errorCode: CustomErrorCodes.DatabaseAlreadyExists,\n      resource: {\n        databaseId,\n      },\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/exeptions/index.ts",
    "content": "export * from './database-already-exists.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/middleware/connection.middleware.ts",
    "content": "import {\n  BadGatewayException,\n  BadRequestException,\n  Injectable,\n  Logger,\n  NestMiddleware,\n} from '@nestjs/common';\nimport * as connectTimeout from 'connect-timeout';\nimport { NextFunction, Request, Response } from 'express';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RedisErrorCodes } from 'src/constants';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { plainToInstance } from 'class-transformer';\nimport { sessionMetadataFromRequest } from 'src/common/decorators';\nimport { Database } from '../models/database';\n\n@Injectable()\nexport class ConnectionMiddleware implements NestMiddleware {\n  private logger = new Logger('ConnectionMiddleware');\n\n  constructor(private databaseService: DatabaseService) {}\n\n  async use(req: Request, res: Response, next: NextFunction): Promise<any> {\n    let { timeout, instanceIdFromReq } =\n      ConnectionMiddleware.getConnectionConfigFromReq(req);\n\n    const sessionMetadata = sessionMetadataFromRequest(req);\n\n    if (instanceIdFromReq) {\n      timeout = plainToInstance(\n        Database,\n        await this.databaseService.get(sessionMetadata, instanceIdFromReq),\n      )?.timeout;\n    }\n\n    const cb = (err?: any) => {\n      if (\n        err?.code === RedisErrorCodes.Timeout ||\n        err?.message?.includes('timeout')\n      ) {\n        next(\n          this.returnError(\n            req,\n            new BadGatewayException(ERROR_MESSAGES.DB_CONNECTION_TIMEOUT),\n          ),\n        );\n      } else {\n        next();\n      }\n    };\n\n    connectTimeout?.(timeout)?.(req, res, cb);\n  }\n\n  private static getConnectionConfigFromReq(req: Request) {\n    return {\n      timeout: req.body?.timeout,\n      instanceIdFromReq: req.params?.id,\n    };\n  }\n\n  private returnError(req: Request, err: Error) {\n    const { method, url } = req;\n    this.logger.error(`${err?.message} ${method} ${url}`);\n    return new BadRequestException(err?.message);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/models/additional.redis.module.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsNumber, IsString } from 'class-validator';\nimport { Expose } from 'class-transformer';\nimport { AdditionalRedisModuleName } from 'src/constants';\n\nexport class AdditionalRedisModule {\n  @ApiProperty({\n    description: 'Name of the module.',\n    type: String,\n    example: AdditionalRedisModuleName.RediSearch,\n  })\n  @IsString()\n  @Expose()\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'Integer representation of a module version.',\n    type: Number,\n    example: 20008,\n  })\n  @IsNumber()\n  @Expose()\n  version?: number;\n\n  @ApiPropertyOptional({\n    description: 'Semantic versioning representation of a module version.',\n    type: String,\n    example: '2.0.8',\n  })\n  @IsString()\n  @Expose()\n  semanticVersion?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/models/database-overview.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { CloudDatabaseDetails } from 'src/modules/cloud/database/models';\n\nexport class DatabaseOverview {\n  @ApiProperty({\n    description: 'Redis database version',\n    type: String,\n  })\n  version: string;\n\n  @ApiPropertyOptional({\n    description: 'Total number of bytes allocated by Redis primary shards',\n    type: Number,\n  })\n  usedMemory?: number;\n\n  @ApiPropertyOptional({\n    description: 'Cloud details',\n    type: CloudDatabaseDetails,\n  })\n  @Type(() => CloudDatabaseDetails)\n  cloudDetails?: CloudDatabaseDetails;\n\n  @ApiPropertyOptional({\n    description: 'Total number of keys inside Redis primary shards',\n    type: Number,\n  })\n  totalKeys?: number;\n\n  @ApiPropertyOptional({\n    description: 'Nested object with total number of keys per logical database',\n    type: Number,\n  })\n  totalKeysPerDb?: Record<string, number>;\n\n  @ApiPropertyOptional({\n    description: 'Median for connected clients in the all shards',\n    type: Number,\n  })\n  connectedClients?: number;\n\n  @ApiPropertyOptional({\n    description: 'Sum of current commands per second in the all shards',\n    type: Number,\n  })\n  opsPerSecond?: number;\n\n  @ApiPropertyOptional({\n    description: 'Sum of current network input in the all shards (kbps)',\n    type: Number,\n  })\n  networkInKbps?: number;\n\n  @ApiPropertyOptional({\n    description: 'Sum of current network out in the all shards (kbps)',\n    type: Number,\n  })\n  networkOutKbps?: number;\n\n  @ApiPropertyOptional({\n    description: 'Sum of current cpu usage in the all shards (%)',\n    type: Number,\n  })\n  cpuUsagePercentage?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Maximum CPU usage percentage when multiple threads/shards are present (%)',\n    type: Number,\n  })\n  maxCpuUsagePercentage?: number;\n\n  @ApiProperty({\n    description: 'Database server name',\n    type: String,\n  })\n  serverName?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/models/database.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport config from 'src/utils/config';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport {\n  Compressor,\n  ConnectionType,\n  Encoding,\n  HostingProvider,\n} from 'src/modules/database/entities/database.entity';\nimport {\n  IsBoolean,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsNotEmptyObject,\n  IsOptional,\n  IsString,\n  Max,\n  MaxLength,\n  Min,\n  ValidateNested,\n  IsArray,\n} from 'class-validator';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { Endpoint } from 'src/common/models';\nimport { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\nimport { CloudDatabaseDetails } from 'src/modules/cloud/database/models/cloud-database-details';\nimport { Tag } from 'src/modules/tag/models/tag';\nimport { AzureProviderDetails } from './provider-details';\n\nconst CONNECTIONS_CONFIG = config.get('connections');\n\nexport class Database {\n  @ApiProperty({\n    description: 'Database id.',\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description:\n      'The hostname of your Redis database, for example redis.acme.com.' +\n      ' If your Redis server is running on your local machine, you can enter either 127.0.0.1 or localhost.',\n    type: String,\n    default: 'localhost',\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  host: string;\n\n  @ApiProperty({\n    description: 'The port your Redis database is available on.',\n    type: Number,\n    default: 6379,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  port: number;\n\n  @ApiProperty({\n    description: 'A name for your Redis database.',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @MaxLength(500)\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'Logical database number.',\n    type: Number,\n  })\n  @Expose()\n  @IsInt()\n  @Min(0)\n  @IsOptional()\n  db?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Database username, if your database is ACL enabled, otherwise leave this field empty.',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsOptional()\n  username?: string;\n\n  @ApiPropertyOptional({\n    description:\n      'The password, if any, for your Redis database. ' +\n      'If your database doesn’t require a password, leave this field empty.',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsOptional()\n  password?: string;\n\n  @ApiPropertyOptional({\n    description: 'Connection timeout',\n    type: Number,\n    default: 30_000,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsOptional()\n  @Min(1_000)\n  @Max(1_000_000_000)\n  @IsInt({ always: true })\n  timeout?: number = CONNECTIONS_CONFIG.timeout;\n\n  @ApiProperty({\n    description: 'Connection Type',\n    default: ConnectionType.STANDALONE,\n    enum: ConnectionType,\n  })\n  @Expose()\n  @IsEnum(ConnectionType)\n  connectionType: ConnectionType;\n\n  @ApiPropertyOptional({\n    description: 'The database name from provider',\n  })\n  @Expose()\n  @IsOptional()\n  @IsString()\n  nameFromProvider?: string | null;\n\n  @ApiPropertyOptional({\n    description: 'The redis database hosting provider',\n    example: HostingProvider.REDIS_CLOUD,\n  })\n  @Expose()\n  @IsOptional()\n  @IsString()\n  provider?: string;\n\n  @ApiProperty({\n    description: 'Time of the last connection to the database.',\n    type: String,\n    format: 'date-time',\n    example: '2021-01-06T12:44:39.000Z',\n  })\n  @Expose()\n  lastConnection?: Date;\n\n  @ApiProperty({\n    description: 'Date of creation',\n    type: Date,\n  })\n  @Expose()\n  createdAt?: Date;\n\n  @ApiPropertyOptional({\n    description: 'Redis OSS Sentinel master group.',\n    type: SentinelMaster,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => SentinelMaster)\n  @ValidateNested()\n  sentinelMaster?: SentinelMaster;\n\n  @ApiPropertyOptional({\n    description: 'OSS Cluster Nodes',\n    type: Endpoint,\n    isArray: true,\n  })\n  @IsOptional()\n  @Type(() => Endpoint)\n  @Expose()\n  nodes?: Endpoint[];\n\n  @ApiPropertyOptional({\n    description: 'Loaded Redis modules.',\n    type: AdditionalRedisModule,\n    isArray: true,\n  })\n  @Expose()\n  @IsOptional()\n  @IsArray()\n  @Type(() => AdditionalRedisModule)\n  modules?: AdditionalRedisModule[];\n\n  @ApiPropertyOptional({\n    description: 'Use TLS to connect.',\n    type: Boolean,\n  })\n  @Expose()\n  @IsBoolean()\n  @IsOptional()\n  tls?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'SNI servername',\n    type: String,\n  })\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  @IsOptional()\n  tlsServername?: string;\n\n  @ApiPropertyOptional({\n    description: 'The certificate returned by the server needs to be verified.',\n    type: Boolean,\n    default: false,\n  })\n  @Expose()\n  @IsOptional()\n  @IsBoolean({ always: true })\n  verifyServerCert?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'CA Certificate',\n    type: CaCertificate,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => CaCertificate)\n  @ValidateNested()\n  caCert?: CaCertificate;\n\n  @ApiPropertyOptional({\n    description: 'Client Certificate',\n    type: ClientCertificate,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => ClientCertificate)\n  @ValidateNested()\n  clientCert?: ClientCertificate;\n\n  @ApiPropertyOptional({\n    description: 'A new created connection',\n    type: Boolean,\n    default: false,\n  })\n  @Expose()\n  @IsOptional()\n  @IsBoolean({ always: true })\n  new?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Use SSH tunnel to connect.',\n    type: Boolean,\n  })\n  @Expose()\n  @IsBoolean()\n  @IsOptional()\n  ssh?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'SSH options',\n    type: SshOptions,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => SshOptions)\n  @ValidateNested()\n  sshOptions?: SshOptions;\n\n  @ApiPropertyOptional({\n    description: 'Cloud details',\n    type: CloudDatabaseDetails,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => CloudDatabaseDetails)\n  @ValidateNested()\n  cloudDetails?: CloudDatabaseDetails;\n\n  @ApiPropertyOptional({\n    description: 'Provider-specific metadata',\n    type: AzureProviderDetails,\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(() => AzureProviderDetails)\n  @ValidateNested()\n  providerDetails?: AzureProviderDetails;\n\n  @ApiPropertyOptional({\n    description: 'Database compressor',\n    default: Compressor.NONE,\n    enum: Compressor,\n  })\n  @Expose()\n  @IsEnum(Compressor, {\n    message: `compressor must be a valid enum value. Valid values: ${Object.values(\n      Compressor,\n    )}.`,\n  })\n  @IsOptional()\n  compressor?: Compressor = Compressor.NONE;\n\n  @ApiPropertyOptional({\n    description: 'Key name format',\n    default: Encoding.UNICODE,\n    enum: Encoding,\n  })\n  @Expose()\n  @IsEnum(Encoding, {\n    message: `Key name format must be a valid enum value. Valid values: ${Object.values(\n      Encoding,\n    )}.`,\n  })\n  @IsOptional()\n  keyNameFormat?: Encoding = Encoding.UNICODE;\n\n  @ApiPropertyOptional({\n    description: 'The version your Redis server',\n    type: String,\n  })\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  @IsOptional()\n  version?: string;\n\n  @ApiPropertyOptional({\n    description: 'Force client connection as standalone',\n    type: Boolean,\n  })\n  @Expose()\n  @IsBoolean()\n  @IsOptional()\n  forceStandalone?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Tags associated with the database.',\n    type: Tag,\n    isArray: true,\n  })\n  @Expose()\n  @IsOptional()\n  @IsArray()\n  @Type(() => Tag)\n  tags?: Tag[];\n\n  @ApiPropertyOptional({\n    description:\n      'Whether the database was created from a file or environment variables at startup',\n    type: Boolean,\n  })\n  @Expose()\n  @IsBoolean()\n  @IsOptional()\n  isPreSetup?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/models/export-database.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { Database } from './database';\n\nexport class ExportDatabase extends PickType(Database, [\n  'id',\n  'host',\n  'port',\n  'name',\n  'db',\n  'username',\n  'password',\n  'connectionType',\n  'nameFromProvider',\n  'provider',\n  'lastConnection',\n  'sentinelMaster',\n  'modules',\n  'tls',\n  'tlsServername',\n  'verifyServerCert',\n  'caCert',\n  'clientCert',\n  'ssh',\n  'sshOptions',\n  'compressor',\n  'forceStandalone',\n  'tags',\n  'providerDetails',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/models/provider-details.ts",
    "content": "/**\n * Provider-specific metadata stored in the providerDetails JSON column.\n * This is used to store additional information about databases added through\n * cloud provider autodiscovery.\n */\n\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { IsEnum, IsOptional, IsString } from 'class-validator';\nimport { AzureAuthType } from 'src/modules/azure/constants';\n\nexport enum CloudProvider {\n  Azure = 'azure',\n}\n\nexport class AzureProviderDetails {\n  @ApiProperty({\n    description: 'Cloud provider',\n    enum: CloudProvider,\n    example: CloudProvider.Azure,\n  })\n  @Expose()\n  @IsEnum(CloudProvider)\n  provider: CloudProvider.Azure;\n\n  @ApiProperty({\n    description: 'Authentication type',\n    enum: AzureAuthType,\n    example: AzureAuthType.EntraId,\n  })\n  @Expose()\n  @IsEnum(AzureAuthType)\n  authType: AzureAuthType;\n\n  @ApiPropertyOptional({\n    description: 'MSAL account ID for token refresh (homeAccountId)',\n    type: String,\n  })\n  @Expose()\n  @IsOptional()\n  @IsString()\n  azureAccountId?: string;\n\n  @ApiPropertyOptional({\n    description: 'Token expiration time for filtering during re-authentication',\n    type: Date,\n  })\n  @Expose()\n  @IsOptional()\n  tokenExpiresOn?: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database-info.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockFeatureService,\n  mockRedisClientList,\n  mockRedisClientListResult,\n  mockRedisClientsInfoResponse,\n  mockRedisServerInfoResponse,\n  mockStandaloneRedisInfoReply,\n  MockType,\n  mockStandaloneRedisClient,\n  mockClusterRedisClient,\n} from 'src/__mocks__';\nimport {\n  REDIS_MODULES_COMMANDS,\n  AdditionalRedisModuleName,\n} from 'src/constants';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport {\n  ForbiddenException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\n\nconst mockRedisServerInfoDto = {\n  redis_version: '6.0.5',\n  redis_mode: 'standalone',\n  os: 'Linux 4.15.0-1087-gcp x86_64',\n  arch_bits: '64',\n  tcp_port: '11113',\n  uptime_in_seconds: '1000',\n};\nconst mockRedisStatsDto = {\n  instantaneous_input_kbps: undefined,\n  instantaneous_ops_per_sec: undefined,\n  instantaneous_output_kbps: undefined,\n  maxmemory_policy: undefined,\n  numberOfKeysRange: '0 - 500 000',\n  uptime_in_days: undefined,\n};\n\nconst mockRedisGeneralInfo: RedisDatabaseInfoResponse = {\n  version: mockRedisServerInfoDto.redis_version,\n  databases: 16,\n  role: 'master',\n  server: mockRedisServerInfoDto,\n  stats: mockRedisStatsDto,\n  usedMemory: 1000000,\n  totalKeys: 1,\n  connectedClients: 1,\n  uptimeInSeconds: 1000,\n  hitRatio: 1,\n};\n\nconst mockRedisModuleList = [\n  { name: 'ai', ver: 10000 },\n  { name: 'graph', ver: 10000 },\n  { name: 'rg', ver: 10000 },\n  { name: 'bf', ver: 10000 },\n  { name: 'ReJSON', ver: 10000 },\n  { name: 'search', ver: 10000 },\n  { name: 'timeseries', ver: 10000 },\n  { name: 'customModule', ver: 10000 },\n].map((item) => [].concat(...Object.entries(item)));\n\nconst mockUnknownCommandModule = new Error(\"unknown command 'module'\");\n\ndescribe('DatabaseInfoProvider', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let service: DatabaseInfoProvider;\n  let featureService: MockType<FeatureService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseInfoProvider,\n        {\n          provide: FeatureService,\n          useFactory: mockFeatureService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseInfoProvider);\n    featureService = await module.get(FeatureService);\n  });\n\n  describe('getDatabasesCount', () => {\n    it('get databases count', async () => {\n      when(standaloneClient.call)\n        .calledWith(['config', 'get', 'databases'], expect.anything())\n        .mockResolvedValue(['databases', '16']);\n\n      const result = await service.getDatabasesCount(standaloneClient);\n\n      expect(result).toBe(16);\n    });\n    it('get databases count for limited redis db', async () => {\n      when(standaloneClient.call)\n        .calledWith(['config', 'get', 'databases'], expect.anything())\n        .mockResolvedValue([]);\n\n      const result = await service.getDatabasesCount(standaloneClient);\n\n      expect(result).toBe(1);\n    });\n    it('failed to get databases config', async () => {\n      when(standaloneClient.call)\n        .calledWith(['config', 'get', 'databases'], expect.anything())\n        .mockRejectedValue(new Error(\"unknown command 'config'\"));\n\n      const result = await service.getDatabasesCount(standaloneClient);\n\n      expect(result).toBe(1);\n    });\n  });\n\n  describe('getClientListInfo', () => {\n    it('get client list info', async () => {\n      when(standaloneClient.call)\n        .calledWith(['client', 'list'], expect.anything())\n        .mockResolvedValue(mockRedisClientList);\n\n      const result = await service.getClientListInfo(standaloneClient);\n\n      expect(result).toEqual(mockRedisClientListResult);\n    });\n    it('failed to get client list', async () => {\n      when(standaloneClient.call)\n        .calledWith(['client', 'list'], expect.anything())\n        .mockRejectedValue(new Error(\"unknown command 'client'\"));\n\n      try {\n        await service.getClientListInfo(standaloneClient);\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n\n  describe('getDatabaseCountFromKeyspace', () => {\n    it('should return 1 since db0 keys presented only', async () => {\n      const result = await service['getDatabaseCountFromKeyspace']({\n        db0: 'keys=11,expires=0,avg_ttl=0',\n      });\n\n      expect(result).toBe(1);\n    });\n    it('should return 7 since db6 is the last logical databases with known keys', async () => {\n      const result = await service['getDatabaseCountFromKeyspace']({\n        db0: 'keys=21,expires=0,avg_ttl=0',\n        db1: 'keys=31,expires=0,avg_ttl=0',\n        db6: 'keys=41,expires=0,avg_ttl=0',\n      });\n\n      expect(result).toBe(7);\n    });\n    it('should return 1 when empty keySpace provided', async () => {\n      const result = await service['getDatabaseCountFromKeyspace']({});\n\n      expect(result).toBe(1);\n    });\n    it('should return 1 when incorrect keySpace provided', async () => {\n      const result = await service['getDatabaseCountFromKeyspace'](null);\n\n      expect(result).toBe(1);\n    });\n  });\n\n  describe('determineDatabaseModules', () => {\n    it('get modules by using MODULE LIST command (without filters)', async () => {\n      when(standaloneClient.call)\n        .calledWith(['module', 'list'], expect.anything())\n        .mockResolvedValue(mockRedisModuleList);\n\n      const result = await service.determineDatabaseModules(standaloneClient);\n\n      expect(standaloneClient.call).not.toHaveBeenCalledWith(\n        'command',\n        expect.anything(),\n      );\n      expect(result).toEqual([\n        {\n          name: AdditionalRedisModuleName.RedisAI,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisGraph,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisGears,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisBloom,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisJSON,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RediSearch,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisTimeSeries,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        { name: 'customModule', version: 10000, semanticVersion: undefined },\n      ]);\n    });\n    it('get modules by using MODULE LIST command (with filters applied)', async () => {\n      when(standaloneClient.call)\n        .calledWith(['module', 'list'], expect.anything())\n        .mockResolvedValue(mockRedisModuleList);\n      featureService.getByName.mockResolvedValue({\n        flag: true,\n        data: {\n          hideByName: [\n            {\n              expression: 'rejSoN',\n              options: 'i',\n            },\n          ],\n        },\n      });\n\n      const result = await service.determineDatabaseModules(standaloneClient);\n\n      expect(standaloneClient.call).not.toHaveBeenCalledWith(\n        'command',\n        expect.anything(),\n      );\n      expect(result).toEqual([\n        {\n          name: AdditionalRedisModuleName.RedisAI,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisGraph,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisGears,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisBloom,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        // { name: AdditionalRedisModuleName.RedisJSON, version: 10000, semanticVersion: '1.0.0' }, should be ignored\n        {\n          name: AdditionalRedisModuleName.RediSearch,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        {\n          name: AdditionalRedisModuleName.RedisTimeSeries,\n          version: 10000,\n          semanticVersion: '1.0.0',\n        },\n        { name: 'customModule', version: 10000, semanticVersion: undefined },\n      ]);\n    });\n    it('detect all modules by using COMMAND INFO command (without filter)', async () => {\n      when(standaloneClient.call)\n        .calledWith(['module', 'list'], expect.anything())\n        .mockRejectedValue(mockUnknownCommandModule);\n      when(standaloneClient.call)\n        .calledWith(\n          expect.arrayContaining(['command', 'info']),\n          expect.anything(),\n        )\n        .mockResolvedValue([\n          null,\n          ['somecommand', -1, ['readonly'], 0, 0, -1, []],\n        ]);\n\n      const result = await service.determineDatabaseModules(standaloneClient);\n\n      expect(standaloneClient.call).toHaveBeenCalledTimes(\n        REDIS_MODULES_COMMANDS.size + 1,\n      );\n      expect(result).toEqual([\n        { name: AdditionalRedisModuleName.RedisAI },\n        { name: AdditionalRedisModuleName.RedisGraph },\n        { name: AdditionalRedisModuleName.RedisGears },\n        { name: AdditionalRedisModuleName.RedisBloom },\n        { name: AdditionalRedisModuleName.RedisJSON },\n        { name: AdditionalRedisModuleName.RediSearch },\n        { name: AdditionalRedisModuleName.RedisTimeSeries },\n      ]);\n    });\n    it('detect all modules by using COMMAND INFO command (with filter)', async () => {\n      when(standaloneClient.call)\n        .calledWith(['module', 'list'], expect.anything())\n        .mockRejectedValue(mockUnknownCommandModule);\n      when(standaloneClient.call)\n        .calledWith(\n          expect.arrayContaining(['command', 'info']),\n          expect.anything(),\n        )\n        .mockResolvedValue([\n          null,\n          ['somecommand', -1, ['readonly'], 0, 0, -1, []],\n        ]);\n      featureService.getByName.mockResolvedValue({\n        flag: true,\n        data: {\n          hideByName: [\n            {\n              expression: 'rejSoN',\n              options: 'i',\n            },\n          ],\n        },\n      });\n\n      const result = await service.determineDatabaseModules(standaloneClient);\n\n      expect(standaloneClient.call).toHaveBeenCalledTimes(\n        REDIS_MODULES_COMMANDS.size + 1,\n      );\n      expect(result).toEqual([\n        { name: AdditionalRedisModuleName.RedisAI },\n        { name: AdditionalRedisModuleName.RedisGraph },\n        { name: AdditionalRedisModuleName.RedisGears },\n        { name: AdditionalRedisModuleName.RedisBloom },\n        // { name: AdditionalRedisModuleName.RedisJSON }, should be ignored\n        { name: AdditionalRedisModuleName.RediSearch },\n        { name: AdditionalRedisModuleName.RedisTimeSeries },\n      ]);\n    });\n    it('detect only RediSearch module by using COMMAND INFO command', async () => {\n      when(standaloneClient.call)\n        .calledWith(['module', 'list'], expect.anything())\n        .mockRejectedValue(mockUnknownCommandModule);\n      when(standaloneClient.call)\n        .calledWith(\n          [\n            'command',\n            'info',\n            ...REDIS_MODULES_COMMANDS.get(AdditionalRedisModuleName.RediSearch),\n          ],\n          expect.anything(),\n        )\n        .mockResolvedValue([['FT.INFO', -1, ['readonly'], 0, 0, -1, []]]);\n\n      const result = await service.determineDatabaseModules(standaloneClient);\n\n      expect(standaloneClient.call).toHaveBeenCalledTimes(\n        REDIS_MODULES_COMMANDS.size + 1,\n      );\n      expect(result).toEqual([{ name: AdditionalRedisModuleName.RediSearch }]);\n    });\n    it('should return empty array if MODULE LIST and COMMAND command not allowed', async () => {\n      when(standaloneClient.call)\n        .calledWith(['module', 'list'], expect.anything())\n        .mockRejectedValue(mockUnknownCommandModule);\n      when(standaloneClient.call)\n        .calledWith(\n          expect.arrayContaining(['command', 'info']),\n          expect.anything(),\n        )\n        .mockRejectedValue(mockUnknownCommandModule);\n\n      const result = await service.determineDatabaseModules(standaloneClient);\n\n      expect(result).toEqual([]);\n    });\n  });\n\n  describe('determineDatabaseServer', () => {\n    it('get modules by using MODULE LIST command', async () => {\n      when(standaloneClient.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisServerInfoResponse),\n      );\n\n      const result = await service.determineDatabaseServer(standaloneClient);\n\n      expect(result).toEqual(mockRedisGeneralInfo.version);\n    });\n  });\n\n  describe('getRedisDBSize', () => {\n    it('get dbsize for redis standalone', async () => {\n      when(standaloneClient.sendCommand)\n        .calledWith(['dbsize'], { replyEncoding: 'utf8' })\n        .mockResolvedValue('1');\n\n      const result = await service.getRedisDBSize(standaloneClient);\n      expect(result).toEqual(1);\n    });\n\n    it('get general info for redis cluster', async () => {\n      clusterClient.nodes.mockResolvedValueOnce([\n        standaloneClient,\n        standaloneClient,\n      ]);\n      when(standaloneClient.sendCommand)\n        .calledWith(['dbsize'], { replyEncoding: 'utf8' })\n        .mockResolvedValueOnce('1')\n        .mockResolvedValueOnce('2');\n\n      const result = await service.getRedisDBSize(clusterClient);\n\n      expect(result).toEqual(3);\n    });\n  });\n\n  describe('getRedisGeneralInfo', () => {\n    beforeEach(() => {\n      service.getDatabasesCount = jest.fn().mockResolvedValue(16);\n    });\n    it('get general info for redis standalone', async () => {\n      when(standaloneClient.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n      );\n\n      const result = await service.getRedisGeneralInfo(standaloneClient);\n\n      expect(result).toEqual(mockRedisGeneralInfo);\n    });\n    it('get general info for redis standalone without some optional fields', async () => {\n      const reply: string = `${mockRedisServerInfoResponse}\\r\\n${\n        mockRedisClientsInfoResponse\n      }\\r\\n`;\n      when(standaloneClient.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(reply),\n      );\n\n      const result = await service.getRedisGeneralInfo(standaloneClient);\n\n      expect(result).toEqual({\n        ...mockRedisGeneralInfo,\n        stats: {\n          ...mockRedisStatsDto,\n          numberOfKeysRange: undefined,\n        },\n        totalKeys: undefined,\n        usedMemory: undefined,\n        hitRatio: undefined,\n        role: undefined,\n      });\n    });\n    it('get general info for redis cluster', async () => {\n      clusterClient.nodes.mockResolvedValueOnce([\n        standaloneClient,\n        standaloneClient,\n      ]);\n      when(standaloneClient.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n      );\n\n      const result = await service.getRedisGeneralInfo(clusterClient);\n\n      expect(result).toEqual({\n        version: mockRedisGeneralInfo.version,\n        totalKeys: mockRedisGeneralInfo.totalKeys * 2,\n        usedMemory: mockRedisGeneralInfo.usedMemory * 2,\n        nodes: [mockRedisGeneralInfo, mockRedisGeneralInfo],\n      });\n    });\n    it('should get info from hello command when info command is not available', async () => {\n      when(standaloneClient.getInfo).mockResolvedValue({\n        replication: {\n          role: mockRedisGeneralInfo.role,\n        },\n        server: {\n          redis_mode: mockRedisServerInfoDto.redis_mode,\n          redis_version: mockRedisGeneralInfo.version,\n          server_name: 'redis',\n        },\n      });\n\n      const result = await service.getRedisGeneralInfo(standaloneClient);\n\n      expect(result).toEqual({\n        ...mockRedisGeneralInfo,\n        stats: {\n          ...mockRedisStatsDto,\n          numberOfKeysRange: undefined,\n        },\n        server: {\n          redis_mode: mockRedisServerInfoDto.redis_mode,\n          redis_version: mockRedisGeneralInfo.version,\n          server_name: 'redis',\n        },\n        uptimeInSeconds: undefined,\n        totalKeys: undefined,\n        usedMemory: undefined,\n        hitRatio: undefined,\n        connectedClients: undefined,\n        cashedScripts: undefined,\n      });\n    });\n    it(\"should throw an error if no permission to run 'info' and 'hello' commands\", async () => {\n      when(standaloneClient.getInfo).mockRejectedValue({\n        message:\n          \"NOPERM this user has no permissions to run the 'hello' command\",\n      });\n\n      try {\n        await service.getRedisGeneralInfo(standaloneClient);\n        fail('Should throw an error');\n      } catch (err) {\n        expect(err).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database-info.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport {\n  calculateRedisHitRatio,\n  catchAclError,\n  convertIntToSemanticVersion,\n  getRangeForNumber,\n  TOTAL_KEYS_BREAKPOINTS,\n} from 'src/utils';\nimport { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';\nimport { REDIS_MODULES_COMMANDS, SUPPORTED_REDIS_MODULES } from 'src/constants';\nimport { get, isNil } from 'lodash';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport {\n  convertArrayReplyToObject,\n  convertMultilineReplyToObject,\n} from 'src/modules/redis/utils';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class DatabaseInfoProvider {\n  constructor(private readonly featureService: FeatureService) {}\n\n  public async filterRawModules(\n    sessionMetadata: SessionMetadata,\n    modules: any[],\n  ): Promise<any[]> {\n    let filteredModules = modules;\n\n    try {\n      const filterModules = await this.featureService.getByName(\n        sessionMetadata,\n        KnownFeatures.RedisModuleFilter,\n      );\n\n      if (filterModules?.flag && filterModules.data?.hideByName?.length) {\n        filteredModules = modules.filter(({ name }) => {\n          const match = filterModules.data.hideByName.find(\n            (filter) =>\n              filter.expression &&\n              new RegExp(filter.expression, filter.options).test(name),\n          );\n\n          return !match;\n        });\n      }\n    } catch (e) {\n      // ignore\n    }\n\n    return filteredModules;\n  }\n\n  /**\n   * Determine database modules using \"module list\" command\n   * In case when \"module\" command is not available use \"command info\" approach\n   * @param client\n   */\n  public async determineDatabaseModules(\n    client: RedisClient,\n  ): Promise<AdditionalRedisModule[]> {\n    try {\n      const reply = (await client.call(['module', 'list'], {\n        replyEncoding: 'utf8',\n      })) as string[][];\n      const modules = await this.filterRawModules(\n        client.clientMetadata.sessionMetadata,\n        reply.map((module: any[]) => convertArrayReplyToObject(module)),\n      );\n\n      return modules.map(({ name, ver }) => ({\n        name: SUPPORTED_REDIS_MODULES[name] ?? name,\n        version: ver,\n        semanticVersion: SUPPORTED_REDIS_MODULES[name]\n          ? convertIntToSemanticVersion(ver)\n          : undefined,\n      }));\n    } catch (e) {\n      return this.determineDatabaseModulesUsingInfo(client);\n    }\n  }\n\n  /**\n   * Determine database server version using \"module list\" command\n   * @param client\n   */\n  public async determineDatabaseServer(client: RedisClient): Promise<string> {\n    try {\n      const reply = await client.getInfo();\n      return reply['server']?.redis_version;\n    } catch (e) {\n      // continue regardless of error\n    }\n    return null;\n  }\n\n  /**\n   * Determine database modules by using \"command info\" command for each listed (known/supported) module\n   * @param client\n   * @private\n   */\n  public async determineDatabaseModulesUsingInfo(\n    client: RedisClient,\n  ): Promise<AdditionalRedisModule[]> {\n    const modules: AdditionalRedisModule[] = [];\n    await Promise.all(\n      Array.from(REDIS_MODULES_COMMANDS, async ([moduleName, commands]) => {\n        try {\n          let commandsInfo = (await client.call(\n            ['command', 'info', ...commands],\n            { replyEncoding: 'utf8' },\n          )) as string[];\n          commandsInfo = commandsInfo.filter((info) => !isNil(info));\n          if (commandsInfo.length) {\n            modules.push({ name: moduleName });\n          }\n        } catch (e) {\n          // continue regardless of error\n        }\n      }),\n    );\n\n    return await this.filterRawModules(\n      client.clientMetadata.sessionMetadata,\n      modules,\n    );\n  }\n\n  public async getRedisDBSize(client: RedisClient): Promise<number> {\n    if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n      const nodesResult: number[] = await Promise.all(\n        (await client.nodes()).map(async (node) =>\n          this.getRedisNodeDBSize(node),\n        ),\n      );\n      return nodesResult.reduce((ac, cur) => ac + cur, 0);\n    }\n    return await this.getRedisNodeDBSize(client);\n  }\n\n  public async getRedisGeneralInfo(\n    client: RedisClient,\n  ): Promise<RedisDatabaseInfoResponse> {\n    if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n      return this.getRedisMasterNodesGeneralInfo(client);\n    }\n    return this.getRedisNodeGeneralInfo(client);\n  }\n\n  private async getRedisNodeGeneralInfo(\n    client: RedisClient,\n  ): Promise<RedisDatabaseInfoResponse> {\n    try {\n      const info = await client.getInfo();\n      const serverInfo = info['server'];\n      const memoryInfo = info['memory'];\n      const keyspaceInfo = info['keyspace'];\n      const clientsInfo = info['clients'];\n      const statsInfo = info['stats'];\n      const replicationInfo = info['replication'];\n      const databases = await this.getDatabasesCount(client, keyspaceInfo);\n      const totalKeys = this.getRedisNodeTotalKeysCount(keyspaceInfo);\n      return {\n        version: serverInfo?.redis_version,\n        databases,\n        role: get(replicationInfo, 'role'),\n        totalKeys,\n        usedMemory: parseInt(get(memoryInfo, 'used_memory'), 10) || undefined,\n        connectedClients:\n          parseInt(get(clientsInfo, 'connected_clients'), 10) || undefined,\n        uptimeInSeconds:\n          parseInt(get(serverInfo, 'uptime_in_seconds'), 10) || undefined,\n        hitRatio: this.getRedisHitRatio(statsInfo),\n        cashedScripts:\n          parseInt(get(memoryInfo, 'number_of_cached_scripts'), 10) ||\n          undefined,\n        server: serverInfo,\n        stats: {\n          instantaneous_ops_per_sec: get(\n            statsInfo,\n            'instantaneous_ops_per_sec',\n          ),\n          instantaneous_input_kbps: get(statsInfo, 'instantaneous_input_kbps'),\n          instantaneous_output_kbps: get(\n            statsInfo,\n            'instantaneous_output_kbps',\n          ),\n          uptime_in_days: get(serverInfo, 'uptime_in_days', undefined),\n          maxmemory_policy: get(memoryInfo, 'maxmemory_policy', undefined),\n          numberOfKeysRange: getRangeForNumber(\n            totalKeys,\n            TOTAL_KEYS_BREAKPOINTS,\n          ),\n        },\n      };\n    } catch (error) {\n      throw catchAclError(error);\n    }\n  }\n\n  private async getRedisMasterNodesGeneralInfo(\n    client: RedisClient,\n  ): Promise<RedisDatabaseInfoResponse> {\n    const nodesResult: RedisDatabaseInfoResponse[] = await Promise.all(\n      (await client.nodes()).map(async (node) =>\n        this.getRedisNodeGeneralInfo(node),\n      ),\n    );\n    return nodesResult.reduce((prev, cur) => ({\n      version: cur.version,\n      usedMemory: prev.usedMemory + cur.usedMemory,\n      totalKeys: prev.totalKeys + cur.totalKeys,\n      nodes: prev?.nodes ? [...prev.nodes, cur] : [prev, cur],\n    }));\n  }\n\n  public async getDatabasesCount(\n    client: RedisClient,\n    keyspaceInfo?: object,\n  ): Promise<number> {\n    try {\n      const reply = (await client.call(['config', 'get', 'databases'], {\n        replyEncoding: 'utf8',\n      })) as string;\n      return reply.length ? parseInt(reply[1], 10) : 1;\n    } catch (e) {\n      return this.getDatabaseCountFromKeyspace(keyspaceInfo);\n    }\n  }\n\n  public async getClientListInfo(client: RedisClient): Promise<any[]> {\n    try {\n      const clientListResponse = (await client.call(['client', 'list'], {\n        replyEncoding: 'utf8',\n      })) as string;\n\n      return clientListResponse\n        .split(/\\r?\\n/)\n        .filter(Boolean)\n        .map((r) => convertMultilineReplyToObject(r, ' ', '='));\n    } catch (error) {\n      throw catchAclError(error);\n    }\n  }\n\n  /**\n   * Try to determine number of logical database from the `info keyspace`\n   *\n   * Note: This is unreliable method which may return less logical databases count that database has\n   * However this is needed for workaround when `config` command is disabled to understand if we need\n   * to show logical database switcher on UI\n   * @param keyspaceInfo\n   * @private\n   */\n  private getDatabaseCountFromKeyspace(keyspaceInfo: object): number {\n    try {\n      const keySpaces = Object.keys(keyspaceInfo);\n      const matches = keySpaces[keySpaces.length - 1].match(/(\\d+)/);\n\n      return matches[0] ? parseInt(matches[0], 10) + 1 : 1;\n    } catch (e) {\n      return 1;\n    }\n  }\n\n  private getRedisNodeTotalKeysCount(keyspaceInfo: object): number {\n    try {\n      return Object.values(keyspaceInfo).reduce<number>(\n        (prev: number, cur: string) => {\n          const { keys } = convertMultilineReplyToObject(cur, ',', '=');\n          return prev + parseInt(keys, 10);\n        },\n        0,\n      );\n    } catch (error) {\n      return undefined;\n    }\n  }\n\n  private getRedisHitRatio(statsInfo: object): number {\n    try {\n      const keyspaceHits = get(statsInfo, 'keyspace_hits');\n      const keyspaceMisses = get(statsInfo, 'keyspace_misses');\n      return calculateRedisHitRatio(keyspaceHits, keyspaceMisses);\n    } catch (error) {\n      return undefined;\n    }\n  }\n\n  private async getRedisNodeDBSize(client: RedisClient): Promise<number> {\n    try {\n      const total = (await client.sendCommand(['dbsize'], {\n        replyEncoding: 'utf8',\n      })) as string;\n      return parseInt(total, 10);\n    } catch (e) {\n      throw catchAclError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database-overview.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockClientMetadata,\n  mockStandaloneRedisClient,\n  mockClusterRedisClient,\n  mockStandaloneRedisInfoReply,\n} from 'src/__mocks__';\nimport { DatabaseOverview } from 'src/modules/database/models/database-overview';\nimport { DatabaseOverviewProvider } from 'src/modules/database/providers/database-overview.provider';\nimport * as Utils from 'src/modules/redis/utils/keys.util';\nimport { DatabaseOverviewKeyspace } from 'src/modules/database/constants/overview';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\nimport { RedisClientNodeRole } from 'src/modules/redis/client';\n\nconst mockServerInfo = {\n  redis_version: '6.2.4',\n  uptime_in_seconds: '1',\n};\nconst mockReplicationInfo = {\n  role: 'master',\n};\nconst mockMemoryInfo = {\n  used_memory: '1',\n};\nconst mockStatsInfo = {\n  instantaneous_ops_per_sec: '1',\n  instantaneous_input_kbps: '1',\n  instantaneous_output_kbps: '1',\n};\nconst mockCpu = {\n  used_cpu_sys: '1',\n  used_cpu_user: '1',\n};\nconst standaloneClientsInfo = {\n  connected_clients: '1',\n};\nconst mockCurrentKeyspace = DatabaseOverviewKeyspace.Current;\nconst mockKeyspace = {\n  db0: 'keys=1,expires=0,avg_ttl=0',\n  db1: 'keys=0,expires=0,avg_ttl=0',\n  db2: 'keys=1,expires=0,avg_ttl=0',\n};\nconst mockNodeInfo = {\n  host: 'localhost',\n  port: 6379,\n  server: mockServerInfo,\n  replication: mockReplicationInfo,\n  stats: mockStatsInfo,\n  memory: mockMemoryInfo,\n  cpu: mockCpu,\n  clients: standaloneClientsInfo,\n  keyspace: mockKeyspace,\n};\n\nconst mockGetTotalResponse1 = 1;\n\nexport const mockDatabaseOverview: DatabaseOverview = {\n  version: mockServerInfo.redis_version,\n  serverName: null,\n  usedMemory: 1,\n  totalKeys: 2,\n  totalKeysPerDb: {\n    db0: 1,\n  },\n  connectedClients: 1,\n  opsPerSecond: 1,\n  networkInKbps: 1,\n  networkOutKbps: 1,\n  cpuUsagePercentage: null,\n};\n\ndescribe('OverviewService', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let service: DatabaseOverviewProvider;\n  let spyGetNodeInfo;\n  let spyCalculateTotalKeys;\n  let spyCalculateNodesTotalKeys;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [DatabaseOverviewProvider],\n    }).compile();\n\n    service = await module.get(DatabaseOverviewProvider);\n    spyGetNodeInfo = jest.spyOn<any, any>(service, 'getNodeInfo');\n    spyCalculateTotalKeys = jest.spyOn<any, any>(service, 'calculateTotalKeys');\n    spyCalculateNodesTotalKeys = jest.spyOn<any, any>(\n      service,\n      'calculateNodesTotalKeys',\n    );\n    standaloneClient.call = jest.fn();\n    standaloneClient.sendCommand = jest.fn();\n  });\n\n  describe('getOverview', () => {\n    describe('Standalone', () => {\n      it('should return proper overview', async () => {\n        when(standaloneClient.getInfo).mockResolvedValue(\n          convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n        );\n        const result = await service.getOverview(\n          mockClientMetadata,\n          standaloneClient,\n          mockCurrentKeyspace,\n        );\n\n        expect(result).toEqual({\n          ...mockDatabaseOverview,\n          version: '6.0.5',\n          connectedClients: 1,\n          totalKeys: 1,\n          totalKeysPerDb: undefined,\n          usedMemory: 1000000,\n          cpuUsagePercentage: undefined,\n          maxCpuUsagePercentage: 100,\n          opsPerSecond: undefined,\n          networkInKbps: undefined,\n          networkOutKbps: undefined,\n        });\n      });\n      it('should return overview with serverName if server_name is present in redis info', async () => {\n        const redisInfoReplyWithServerName = `${mockStandaloneRedisInfoReply.slice(0, 11)}server_name:valkey\\r\\n${mockStandaloneRedisInfoReply.slice(11)}`;\n        when(standaloneClient.getInfo).mockResolvedValue(\n          convertRedisInfoReplyToObject(redisInfoReplyWithServerName),\n        );\n        const result = await service.getOverview(\n          mockClientMetadata,\n          standaloneClient,\n          mockCurrentKeyspace,\n        );\n\n        expect(result).toEqual({\n          ...mockDatabaseOverview,\n          version: '6.0.5',\n          serverName: 'valkey',\n          connectedClients: 1,\n          totalKeys: 1,\n          totalKeysPerDb: undefined,\n          usedMemory: 1000000,\n          cpuUsagePercentage: undefined,\n          maxCpuUsagePercentage: 100,\n          opsPerSecond: undefined,\n          networkInKbps: undefined,\n          networkOutKbps: undefined,\n        });\n      });\n      it('should return total 0 and empty total per db object', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          keyspace: {\n            db0: 'keys=0,expires=0,avg_ttl=0',\n          },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          totalKeys: 0,\n          totalKeysPerDb: undefined,\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('should return total 3 and empty total per db object (even when role is not master)', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          replication: {\n            role: 'slave',\n          },\n          keyspace: {\n            db0: 'keys=3,expires=0,avg_ttl=0',\n          },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          totalKeys: 3,\n          totalKeysPerDb: undefined,\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('should not return particular fields when metrics are not available', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          replication: {\n            role: 'slave',\n          },\n          keyspace: undefined,\n          memory: undefined,\n          clients: undefined,\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          totalKeys: undefined,\n          usedMemory: undefined,\n          totalKeysPerDb: undefined,\n          connectedClients: undefined,\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('check for cpu on second attempt', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce(mockNodeInfo);\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          server: {\n            ...mockNodeInfo.server,\n            uptime_in_seconds: '3',\n          },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          maxCpuUsagePercentage: 100,\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          cpuUsagePercentage: 50,\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('check for cpu max value > 100', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce(mockNodeInfo);\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          server: {\n            ...mockNodeInfo.server,\n            uptime_in_seconds: '2',\n          },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.51',\n            used_cpu_user: '1.50002',\n          },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          maxCpuUsagePercentage: 100,\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          cpuUsagePercentage: 101.002,\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('should not return cpu (undefined) when used_cpu_sys = 0', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          server: {\n            ...mockNodeInfo.server,\n            uptime_in_seconds: '2',\n          },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '0',\n            used_cpu_user: '1.50002',\n          },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          cpuUsagePercentage: undefined,\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('should full data of keyspace if query keyspace = \"full\" ', async () => {\n        spyGetNodeInfo.mockResolvedValueOnce(mockNodeInfo);\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            standaloneClient,\n            DatabaseOverviewKeyspace.Full,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          totalKeysPerDb: {\n            db0: 1,\n            db1: 0,\n            db2: 1,\n          },\n          maxCpuUsagePercentage: 100,\n        });\n      });\n      it('should include maxCpuUsagePercentage for standalone with I/O threads', async () => {\n        when(standaloneClient.getInfo).mockResolvedValue(\n          convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n        );\n        when(standaloneClient.call).mockResolvedValue(['io-threads', '4']);\n\n        spyGetNodeInfo.mockResolvedValue(mockNodeInfo);\n\n        const result = await service.getOverview(\n          mockClientMetadata,\n          standaloneClient,\n          mockCurrentKeyspace,\n        );\n\n        expect(result.maxCpuUsagePercentage).toBe(400);\n      });\n      it('should not include maxCpuUsagePercentage when I/O threads detection fails', async () => {\n        when(standaloneClient.getInfo).mockResolvedValue(\n          convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n        );\n        when(standaloneClient.call).mockRejectedValue(new Error('ACL error'));\n\n        spyGetNodeInfo.mockResolvedValue(mockNodeInfo);\n\n        const result = await service.getOverview(\n          mockClientMetadata,\n          standaloneClient,\n          mockCurrentKeyspace,\n        );\n\n        expect(result.maxCpuUsagePercentage).toBe(100);\n      });\n    });\n    describe('Cluster', () => {\n      it('Should calculate overview and ignore replica where needed', async () => {\n        const getTotal = jest\n          .spyOn(Utils, 'getTotalKeys')\n          .mockResolvedValue(mockGetTotalResponse1);\n        clusterClient.nodes = jest\n          .fn()\n          .mockReturnValue(new Array(6).fill(Promise.resolve()));\n\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12001,\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12002,\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12003,\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12004,\n          replication: { role: 'slave' },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12005,\n          replication: { role: 'slave' },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12006,\n          replication: { role: 'slave' },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            clusterClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          connectedClients: 1,\n          totalKeys: 6,\n          totalKeysPerDb: undefined,\n          usedMemory: 3,\n          networkInKbps: 6,\n          networkOutKbps: 6,\n          opsPerSecond: 6,\n          cpuUsagePercentage: null,\n          maxCpuUsagePercentage: 600,\n        });\n        expect(spyCalculateTotalKeys).toHaveBeenCalledTimes(0);\n        expect(spyCalculateNodesTotalKeys).toHaveBeenCalledTimes(1);\n        expect(getTotal).toHaveBeenCalledTimes(6); // 6 nodes\n\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12001,\n          server: { ...mockNodeInfo.server, uptime_in_seconds: '3' },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12002,\n          server: { ...mockNodeInfo.server, uptime_in_seconds: '3' },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12003,\n          server: { ...mockNodeInfo.server, uptime_in_seconds: '3' },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12004,\n          replication: { role: 'slave' },\n          server: { ...mockNodeInfo.server, uptime_in_seconds: '3' },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12005,\n          replication: { role: 'slave' },\n          server: { ...mockNodeInfo.server, uptime_in_seconds: '3' },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n        spyGetNodeInfo.mockResolvedValueOnce({\n          ...mockNodeInfo,\n          port: 12006,\n          replication: { role: 'slave' },\n          server: { ...mockNodeInfo.server, uptime_in_seconds: '3' },\n          cpu: {\n            ...mockNodeInfo.cpu,\n            used_cpu_sys: '1.5',\n            used_cpu_user: '1.5',\n          },\n        });\n\n        expect(\n          await service.getOverview(\n            mockClientMetadata,\n            clusterClient,\n            mockCurrentKeyspace,\n          ),\n        ).toEqual({\n          ...mockDatabaseOverview,\n          connectedClients: 1,\n          totalKeys: 6,\n          totalKeysPerDb: undefined,\n          usedMemory: 3,\n          networkInKbps: 6,\n          networkOutKbps: 6,\n          opsPerSecond: 6,\n          cpuUsagePercentage: 300,\n          maxCpuUsagePercentage: 600,\n        });\n      });\n      it('should include maxCpuUsagePercentage for cluster with 4 nodes', async () => {\n        const mockPrimaryNodes = [\n          { call: jest.fn(), options: { host: 'localhost', port: 7001 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7002 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7003 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7004 } },\n        ];\n\n        // Mock io-threads = 1 (default) for each node\n        mockPrimaryNodes.forEach((node) => {\n          when(node.call)\n            .calledWith(['config', 'get', 'io-threads'], {\n              replyEncoding: 'utf8',\n            })\n            .mockResolvedValue(['io-threads', '1']);\n        });\n\n        when(clusterClient.getConnectionType).mockReturnValue('CLUSTER' as any);\n        when(clusterClient.nodes)\n          .calledWith(RedisClientNodeRole.PRIMARY)\n          .mockResolvedValue(mockPrimaryNodes);\n        when(clusterClient.getInfo).mockResolvedValue(\n          convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply),\n        );\n\n        spyGetNodeInfo.mockResolvedValue(mockNodeInfo);\n\n        const result = await service.getOverview(\n          mockClientMetadata,\n          clusterClient,\n          mockCurrentKeyspace,\n        );\n\n        expect(result.maxCpuUsagePercentage).toBe(400);\n      });\n    });\n  });\n\n  describe('calculateMaxCpuPercentage', () => {\n    describe('Cluster', () => {\n      it('should return max CPU based on number of primary nodes (4 nodes = 400%)', async () => {\n        const mockPrimaryNodes = [\n          { call: jest.fn(), options: { host: 'localhost', port: 7001 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7002 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7003 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7004 } },\n        ];\n\n        // Mock io-threads = 1 (default) for each node\n        mockPrimaryNodes.forEach((node) => {\n          when(node.call)\n            .calledWith(['config', 'get', 'io-threads'], {\n              replyEncoding: 'utf8',\n            })\n            .mockResolvedValue(['io-threads', '1']);\n        });\n\n        when(clusterClient.getConnectionType).mockReturnValue('CLUSTER' as any);\n        when(clusterClient.nodes)\n          .calledWith(RedisClientNodeRole.PRIMARY)\n          .mockResolvedValue(mockPrimaryNodes);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          clusterClient,\n        );\n\n        expect(result).toBe(400);\n      });\n\n      it('should return max CPU for single node cluster (100%)', async () => {\n        const mockPrimaryNodes = [\n          { call: jest.fn(), options: { host: 'localhost', port: 7001 } },\n        ];\n\n        // Mock io-threads = 1 (default) for the node\n        when(mockPrimaryNodes[0].call)\n          .calledWith(['config', 'get', 'io-threads'], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(['io-threads', '1']);\n\n        when(clusterClient.getConnectionType).mockReturnValue('CLUSTER' as any);\n        when(clusterClient.nodes)\n          .calledWith(RedisClientNodeRole.PRIMARY)\n          .mockResolvedValue(mockPrimaryNodes);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          clusterClient,\n        );\n\n        expect(result).toBe(100);\n      });\n\n      it('should return undefined if cluster nodes call fails', async () => {\n        when(clusterClient.getConnectionType).mockReturnValue('CLUSTER' as any);\n        when(clusterClient.nodes)\n          .calledWith(RedisClientNodeRole.PRIMARY)\n          .mockRejectedValue(new Error('Connection failed'));\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          clusterClient,\n        );\n\n        expect(result).toBeUndefined();\n      });\n\n      it('should sum io-threads across all primary nodes (3 nodes with 4, 2, 1 threads = 700%)', async () => {\n        const mockPrimaryNodes = [\n          { call: jest.fn(), options: { host: 'localhost', port: 7001 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7002 } },\n          { call: jest.fn(), options: { host: 'localhost', port: 7003 } },\n        ];\n\n        // Mock different io-threads for each node\n        when(mockPrimaryNodes[0].call)\n          .calledWith(['config', 'get', 'io-threads'], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(['io-threads', '4']);\n        when(mockPrimaryNodes[1].call)\n          .calledWith(['config', 'get', 'io-threads'], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(['io-threads', '2']);\n        when(mockPrimaryNodes[2].call)\n          .calledWith(['config', 'get', 'io-threads'], {\n            replyEncoding: 'utf8',\n          })\n          .mockResolvedValue(['io-threads', '1']);\n\n        when(clusterClient.getConnectionType).mockReturnValue('CLUSTER' as any);\n        when(clusterClient.nodes)\n          .calledWith(RedisClientNodeRole.PRIMARY)\n          .mockResolvedValue(mockPrimaryNodes);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          clusterClient,\n        );\n\n        // (4 + 2 + 1) * 100 = 700%\n        expect(result).toBe(700);\n      });\n    });\n\n    describe('Standalone', () => {\n      it('should return max CPU when I/O threads are detected (4 threads = 400%)', async () => {\n        when(standaloneClient.call).mockResolvedValue(['io-threads', '4']);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          standaloneClient,\n        );\n\n        expect(result).toBe(400);\n        expect(standaloneClient.call).toHaveBeenCalledWith(\n          ['config', 'get', 'io-threads'],\n          { replyEncoding: 'utf8' },\n        );\n      });\n\n      it('should return 100% when I/O threads = 1', async () => {\n        when(standaloneClient.call).mockResolvedValue(['io-threads', '1']);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          standaloneClient,\n        );\n\n        expect(result).toBe(100);\n      });\n\n      it('should return 100% when CONFIG GET returns empty array', async () => {\n        when(standaloneClient.call).mockResolvedValue([]);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          standaloneClient,\n        );\n\n        expect(result).toBe(100);\n      });\n\n      it('should return 100% when CONFIG GET returns invalid format', async () => {\n        when(standaloneClient.call).mockResolvedValue(['io-threads']);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          standaloneClient,\n        );\n\n        expect(result).toBe(100);\n      });\n\n      it('should handle ACL/permission errors gracefully and return 100%', async () => {\n        const loggerWarnSpy = jest.spyOn(service['logger'], 'warn');\n        const aclError = new Error('NOAUTH Authentication required');\n        when(standaloneClient.call).mockRejectedValue(aclError);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          standaloneClient,\n        );\n\n        expect(result).toBe(100);\n        expect(loggerWarnSpy).toHaveBeenCalledWith(\n          'Error getting io-threads, defaulting to 1 thread',\n          aclError,\n        );\n        loggerWarnSpy.mockRestore();\n      });\n\n      it('should handle network errors gracefully and return 100%', async () => {\n        const loggerWarnSpy = jest.spyOn(service['logger'], 'warn');\n        const networkError = new Error('Connection timeout');\n        when(standaloneClient.call).mockRejectedValue(networkError);\n\n        const result = await (service as any).calculateMaxCpuPercentage(\n          standaloneClient,\n        );\n\n        expect(result).toBe(100);\n        expect(loggerWarnSpy).toHaveBeenCalledWith(\n          'Error getting io-threads, defaulting to 1 thread',\n          networkError,\n        );\n        loggerWarnSpy.mockRestore();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database-overview.provider.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { get, filter, map, keyBy, sum, sumBy, isNumber } from 'lodash';\nimport {\n  getTotalKeys,\n  convertMultilineReplyToObject,\n} from 'src/modules/redis/utils';\nimport { DatabaseOverview } from 'src/modules/database/models/database-overview';\nimport { ClientMetadata } from 'src/common/models';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n  RedisClientNodeRole,\n} from 'src/modules/redis/client';\nimport { DatabaseOverviewKeyspace } from '../constants/overview';\n\n@Injectable()\nexport class DatabaseOverviewProvider {\n  private readonly logger = new Logger(DatabaseOverviewProvider.name);\n\n  private previousCpuStats = new Map();\n\n  /**\n   * Calculates redis database metrics based on connection type (eg Cluster or Standalone)\n   * @param clientMetadata\n   * @param client\n   * @param keyspace\n   */\n  async getOverview(\n    clientMetadata: ClientMetadata,\n    client: RedisClient,\n    keyspace: DatabaseOverviewKeyspace,\n  ): Promise<DatabaseOverview> {\n    let nodesInfo = [];\n    let totalKeys;\n    let totalKeysPerDb;\n\n    const currentDbIndex = isNumber(clientMetadata.db)\n      ? clientMetadata.db\n      : await client.getCurrentDbIndex();\n\n    if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n      nodesInfo = await this.getNodesInfo(client);\n      totalKeys = await this.calculateNodesTotalKeys(client);\n    } else {\n      nodesInfo = [await this.getNodeInfo(client)];\n      const [calculatedTotalKeys, calculatedTotalKeysPerDb] =\n        this.calculateTotalKeys(nodesInfo, currentDbIndex, keyspace);\n      totalKeys = calculatedTotalKeys;\n      totalKeysPerDb = calculatedTotalKeysPerDb;\n    }\n\n    return {\n      version: this.getVersion(nodesInfo),\n      serverName: this.getServerName(nodesInfo),\n      totalKeys,\n      totalKeysPerDb,\n      usedMemory: this.calculateUsedMemory(nodesInfo),\n      connectedClients: this.calculateConnectedClients(nodesInfo),\n      opsPerSecond: this.calculateOpsPerSec(nodesInfo),\n      networkInKbps: this.calculateNetworkIn(nodesInfo),\n      networkOutKbps: this.calculateNetworkOut(nodesInfo),\n      cpuUsagePercentage: this.calculateCpuUsage(\n        clientMetadata.databaseId,\n        nodesInfo,\n      ),\n      maxCpuUsagePercentage: await this.calculateMaxCpuPercentage(client),\n    };\n  }\n\n  /**\n   * Get redis info (executing \"info\" command) for node\n   * @param client\n   * @private\n   */\n  private async getNodeInfo(client: RedisClient) {\n    const { host, port } = client.options;\n    const infoData = await client.getInfo();\n\n    return {\n      ...infoData,\n      host,\n      port,\n    };\n  }\n\n  /**\n   * Get info for each node in cluster\n   * @param client\n   * @private\n   */\n  private async getNodesInfo(client: RedisClient) {\n    return Promise.all((await client.nodes()).map(this.getNodeInfo));\n  }\n\n  /**\n   * Get median value from array of numbers\n   * Will return 0 when empty array received\n   * @param values\n   * @private\n   */\n  private getMedianValue(values: number[]): number {\n    if (!values.length) {\n      return 0;\n    }\n\n    values.sort((a, b) => a - b);\n\n    const middleIndex = Math.floor(values.length / 2);\n\n    // process odd array\n    if (values.length % 2) {\n      return values[middleIndex];\n    }\n\n    return (values[middleIndex - 1] + values[middleIndex]) / 2;\n  }\n\n  /**\n   * Get redis version from the first chard in the list\n   * @param nodes\n   * @private\n   */\n  private getVersion(nodes = []): string {\n    return get(nodes, [0, 'server', 'redis_version'], null);\n  }\n\n  /**\n   * Get server_name from the first shard in the list\n   * @param nodes\n   * @private\n   */\n  private getServerName(nodes = []): string {\n    return get(nodes, [0, 'server', 'server_name'], null);\n  }\n\n  /**\n   * Sum of current ops per second (instantaneous_ops_per_sec) for all shards\n   * @param nodes\n   * @private\n   */\n  private calculateOpsPerSec(nodes = []): number {\n    if (\n      !this.isMetricsAvailable(nodes, 'stats.instantaneous_ops_per_sec', [\n        undefined,\n      ])\n    ) {\n      return undefined;\n    }\n\n    return sumBy(nodes, (node) =>\n      parseInt(get(node, 'stats.instantaneous_ops_per_sec', '0'), 10),\n    );\n  }\n\n  /**\n   * Sum of current network input (instantaneous_input_kbps) for all shards\n   * @param nodes\n   * @private\n   */\n  private calculateNetworkIn(nodes = []): number {\n    if (\n      !this.isMetricsAvailable(nodes, 'stats.instantaneous_input_kbps', [\n        undefined,\n      ])\n    ) {\n      return undefined;\n    }\n\n    return sumBy(nodes, (node) =>\n      parseInt(get(node, 'stats.instantaneous_input_kbps', '0'), 10),\n    );\n  }\n\n  /**\n   * Sum of current network output (instantaneous_output_kbps) for all shards\n   * @param nodes\n   * @private\n   */\n  private calculateNetworkOut(nodes = []): number {\n    if (\n      !this.isMetricsAvailable(nodes, 'stats.instantaneous_output_kbps', [\n        undefined,\n      ])\n    ) {\n      return undefined;\n    }\n\n    return sumBy(nodes, (node) =>\n      parseInt(get(node, 'stats.instantaneous_output_kbps', '0'), 10),\n    );\n  }\n\n  /**\n   * Median of connected clients (connected_clients) to all shards\n   * @param nodes\n   * @private\n   */\n  private calculateConnectedClients(nodes = []): number {\n    if (\n      !this.isMetricsAvailable(nodes, 'clients.connected_clients', [undefined])\n    ) {\n      return undefined;\n    }\n\n    const clientsPerNode = map(nodes, (node) =>\n      parseInt(get(node, 'clients.connected_clients', '0'), 10),\n    );\n    return this.getMedianValue(clientsPerNode);\n  }\n\n  /**\n   * Sum of used memory (used_memory) for primary shards\n   * @param nodes\n   * @private\n   */\n  private calculateUsedMemory(nodes = []): number {\n    try {\n      const masterNodes =\n        DatabaseOverviewProvider.getMasterNodesToWorkWith(nodes);\n\n      if (\n        !this.isMetricsAvailable(masterNodes, 'memory.used_memory', [undefined])\n      ) {\n        return undefined;\n      }\n\n      return sumBy(masterNodes, (node) =>\n        parseInt(get(node, 'memory.used_memory', '0'), 10),\n      );\n    } catch (e) {\n      return null;\n    }\n  }\n\n  /**\n   * Sum of keys for primary shards\n   * In case when shard has multiple logical databases shard total keys = sum of all dbs keys\n   * @param nodes\n   * @param index\n   * @param keyspace\n   * @private\n   */\n  private calculateTotalKeys(\n    nodes = [],\n    index: number,\n    keyspace: DatabaseOverviewKeyspace,\n  ): [number, Record<string, number>] {\n    try {\n      const masterNodes =\n        DatabaseOverviewProvider.getMasterNodesToWorkWith(nodes);\n\n      if (!this.isMetricsAvailable(masterNodes, 'keyspace', [undefined])) {\n        return [undefined, undefined];\n      }\n\n      const totalKeysPerDb: Record<string, number> = {};\n\n      masterNodes.forEach((node) => {\n        map(get(node, 'keyspace', {}), (dbKeys, dbNumber): void => {\n          const { keys } = convertMultilineReplyToObject(dbKeys, ',', '=');\n\n          if (!totalKeysPerDb[dbNumber]) {\n            totalKeysPerDb[dbNumber] = 0;\n          }\n\n          totalKeysPerDb[dbNumber] += parseInt(keys, 10);\n        });\n      });\n\n      const totalKeys = totalKeysPerDb\n        ? sum(Object.values(totalKeysPerDb))\n        : undefined;\n      const dbIndexKeys = totalKeysPerDb[`db${index}`] || 0;\n      const calculatedTotalKeysPerDb =\n        keyspace === DatabaseOverviewKeyspace.Full\n          ? totalKeysPerDb\n          : { [`db${index}`]: dbIndexKeys };\n\n      return [\n        totalKeys,\n        dbIndexKeys === totalKeys ? undefined : calculatedTotalKeysPerDb,\n      ];\n    } catch (e) {\n      return [null, null];\n    }\n  }\n\n  private async calculateNodesTotalKeys(client: RedisClient): Promise<number> {\n    const nodesTotal: number[] = await Promise.all(\n      (await client.nodes(RedisClientNodeRole.PRIMARY)).map(async (node) =>\n        getTotalKeys(node),\n      ),\n    );\n    return nodesTotal.reduce((prev, cur) => prev + cur, 0);\n  }\n\n  /**\n   * Gets the io-threads configuration for a Redis client/node\n   * Returns the number of io-threads, defaulting to 1 if detection fails\n   * @param nodeClient The Redis client to query\n   * @returns The number of io-threads (defaults to 1)\n   */\n  private async getIoThreadsForNode(nodeClient: RedisClient): Promise<number> {\n    try {\n      const ioThreadsResult = await nodeClient.call(\n        ['config', 'get', 'io-threads'],\n        {\n          replyEncoding: 'utf8',\n        },\n      );\n\n      return parseInt(ioThreadsResult?.[1] || '1', 10);\n    } catch (error) {\n      this.logger.warn(\n        'Error getting io-threads, defaulting to 1 thread',\n        error,\n      );\n      return 1;\n    }\n  }\n\n  /**\n   * Calculates maximum CPU percentage based on number of nodes/shards\n   * For clusters: sum of io-threads across all primary nodes * 100%\n   * For standalone: detect I/O threads via CONFIG GET\n   *\n   * Example of calculation:\n   * 1 standalone with 4 threads: 4 * 100 = 400%\n   * 3 shards, each with 4 threads: (4+4+4) * 100 = 1200%\n   * 3 shards, default (1 thread each): (1+1+1) * 100 = 300%\n   */\n  private async calculateMaxCpuPercentage(\n    client: RedisClient,\n  ): Promise<number | undefined> {\n    // For cluster, sum io-threads across all primary nodes\n    if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n      try {\n        const primaryNodes = await client.nodes(RedisClientNodeRole.PRIMARY);\n        let totalIoThreads = 0;\n\n        // Get io-threads for each primary node\n        for (const node of primaryNodes) {\n          const ioThreads = await this.getIoThreadsForNode(node);\n          totalIoThreads += ioThreads;\n        }\n\n        return totalIoThreads * 100;\n      } catch (error) {\n        // If we can't get nodes, return undefined (don't fall through to standalone logic)\n        this.logger.warn(\n          'Error occurred when trying to calculate max CPU usage percentage for cluster',\n          error,\n        );\n        return undefined;\n      }\n    }\n\n    // For standalone, detect I/O threads via CONFIG GET\n    const ioThreads = await this.getIoThreadsForNode(client);\n    return ioThreads * 100;\n  }\n\n  /**\n   * Calculates sum of cpu usage in percentage for all shards\n   * CPU% = ((used_cpu_sys_t2+used_cpu_user_t2)-(used_cpu_sys_t1+used_cpu_user_t1)) / (t2-t1)\n   *\n   * Example of calculation:\n   * Shard 1 CPU: 55%\n   * Shard 2 CPU: 15%\n   * Shard 3 CPU: 50%\n   * Total displayed: 120% (55%+15%+50%).\n   * @param id\n   * @param nodes\n   * @private\n   */\n  private calculateCpuUsage(id: string, nodes = []): number {\n    if (\n      !this.isMetricsAvailable(nodes, 'cpu.used_cpu_sys', [\n        0,\n        '0',\n        '0.0',\n        '0.00',\n        undefined,\n      ])\n    ) {\n      return undefined;\n    }\n\n    const previousCpuStats = this.previousCpuStats.get(id);\n\n    const currentCpuStats = keyBy(\n      map(nodes, (node) => ({\n        node: `${node.host}:${node.port}`,\n        cpuSys: parseFloat(get(node, 'cpu.used_cpu_sys')),\n        cpuUser: parseFloat(get(node, 'cpu.used_cpu_user')),\n        upTime: parseFloat(get(node, 'server.uptime_in_seconds')),\n      })),\n      'node',\n    );\n\n    this.previousCpuStats.set(id, currentCpuStats);\n\n    // return null as it is impossible to calculate percentage without previous results\n    if (!previousCpuStats) {\n      return null;\n    }\n    return sum(\n      map(currentCpuStats, (current) => {\n        const previous = previousCpuStats[current.node];\n        if (\n          !previous ||\n          previous.upTime >= current.upTime // in case when server was restarted or too often requests\n        ) {\n          return 0;\n        }\n\n        const currentUsage = current.cpuUser + current.cpuSys;\n        const previousUsage = previous.cpuUser + previous.cpuSys;\n        const timeDelta = current.upTime - previous.upTime;\n\n        const usage = ((currentUsage - previousUsage) / timeDelta) * 100;\n\n        // let's return 0 in case of incorrect data retrieved from redis\n        if (usage < 0) {\n          return 0;\n        }\n\n        return usage;\n      }),\n    );\n  }\n\n  /**\n   * Check that metric has expected value or provided\n   *\n   * @param nodes\n   * @param path\n   * @param values\n   * @private\n   */\n  private isMetricsAvailable(\n    nodes = [],\n    path: string[] | string,\n    values: any[],\n  ): boolean {\n    for (let i = 0; i < nodes.length; i += 1) {\n      const node = nodes[i];\n\n      if (values.includes(get(node, path))) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  static getMasterNodesToWorkWith(nodes = []): any[] {\n    let masterNodes = nodes;\n\n    if (nodes?.length > 1) {\n      masterNodes = filter(nodes, (node) =>\n        ['master', undefined].includes(get(node, 'replication.role')),\n      );\n    }\n\n    return masterNodes;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database.client.factory.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCommonClientMetadata,\n  mockCredentialProvider,\n  mockDatabase,\n  mockDatabaseAnalytics,\n  mockDatabaseRepository,\n  mockDatabaseService,\n  MockType,\n  mockRedisClientFactory,\n  mockStandaloneRedisClient,\n  mockSessionMetadata,\n  MockRedisClient,\n  mockEventEmitter,\n} from 'src/__mocks__';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { ClientContext, ClientMetadata } from 'src/common/models';\nimport { v4 as uuidv4 } from 'uuid';\nimport { LocalRedisClientFactory } from 'src/modules/redis/local.redis.client.factory';\nimport { IoredisRedisConnectionStrategy } from 'src/modules/redis/connection/ioredis.redis.connection.strategy';\nimport { NodeRedisConnectionStrategy } from 'src/modules/redis/connection/node.redis.connection.strategy';\nimport {\n  mockIoRedisRedisConnectionStrategy,\n  mockNodeRedisConnectionStrategy,\n} from 'src/__mocks__/redis-client';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { ConnectionType } from 'src/modules/database/entities/database.entity';\nimport { RedisConnectionTimeoutException } from 'src/modules/redis/exceptions/connection';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { DatabaseConnectionEvent } from 'src/modules/database/constants/events';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { CredentialStrategyProvider } from 'src/modules/database/credentials/credential-strategy.provider';\n\ndescribe('DatabaseClientFactory', () => {\n  let service: DatabaseClientFactory;\n  let databaseService: MockType<DatabaseService>;\n  let databaseRepository: MockType<DatabaseRepository>;\n  let redisClientStorage: RedisClientStorage;\n  let redisClientFactory: LocalRedisClientFactory;\n  let analytics: MockType<DatabaseAnalytics>;\n  let eventEmitter: MockType<EventEmitter2>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseClientFactory,\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: DatabaseRepository,\n          useFactory: mockDatabaseRepository,\n        },\n        {\n          provide: DatabaseAnalytics,\n          useFactory: mockDatabaseAnalytics,\n        },\n        RedisClientStorage,\n        {\n          provide: RedisClientFactory,\n          useClass: mockRedisClientFactory,\n        },\n        {\n          provide: IoredisRedisConnectionStrategy,\n          useFactory: mockIoRedisRedisConnectionStrategy,\n        },\n        {\n          provide: NodeRedisConnectionStrategy,\n          useFactory: mockNodeRedisConnectionStrategy,\n        },\n        {\n          provide: EventEmitter2,\n          useValue: mockEventEmitter,\n        },\n        {\n          provide: CredentialStrategyProvider,\n          useFactory: mockCredentialProvider,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseClientFactory);\n    databaseService = await module.get(DatabaseService);\n    databaseRepository = await module.get(DatabaseRepository);\n    redisClientStorage = await module.get(RedisClientStorage);\n    redisClientFactory = await module.get(RedisClientFactory);\n    analytics = await module.get(DatabaseAnalytics);\n    eventEmitter = await module.get(EventEmitter2);\n  });\n\n  describe('getOrCreateClient', () => {\n    it('should get existing client', async () => {\n      const spyOnGetByMetadata = jest\n        .spyOn(redisClientStorage, 'getByMetadata')\n        .mockResolvedValueOnce(mockStandaloneRedisClient);\n      const spyOnSet = jest.spyOn(redisClientStorage, 'set');\n\n      expect(await service.getOrCreateClient(mockCommonClientMetadata)).toEqual(\n        mockStandaloneRedisClient,\n      );\n      expect(spyOnGetByMetadata).toHaveBeenCalledWith(mockCommonClientMetadata);\n      expect(spyOnSet).not.toHaveBeenCalled();\n    });\n\n    it('should create new and save it client', async () => {\n      const spyOnGetByMetadata = jest\n        .spyOn(redisClientStorage, 'getByMetadata')\n        .mockResolvedValueOnce(null);\n      const spyOnSet = jest.spyOn(redisClientStorage, 'set');\n\n      const result = await service.getOrCreateClient(mockCommonClientMetadata);\n      expect(result).toBeInstanceOf(RedisClient);\n      expect(result.clientMetadata.sessionMetadata).toBe(mockSessionMetadata);\n      expect(spyOnGetByMetadata).toHaveBeenCalledWith(mockCommonClientMetadata);\n      expect(spyOnSet).toHaveBeenCalledWith(result);\n    });\n\n    it('should only instantiate a single client per unique client metadata', async () => {\n      const mockClientMetadata2: ClientMetadata = {\n        sessionMetadata: {\n          userId: 'user-4',\n          accountId: 'acc-4',\n          sessionId: uuidv4(),\n        },\n        databaseId: uuidv4(),\n        context: ClientContext.Common,\n      };\n\n      const clients1 = await Promise.all([\n        service.getOrCreateClient(mockCommonClientMetadata),\n        service.getOrCreateClient(mockCommonClientMetadata),\n        service.getOrCreateClient(mockCommonClientMetadata),\n      ]);\n\n      // assert that all returned clients are the same instance\n      let currentClient = clients1.shift();\n      expect(currentClient).toBeInstanceOf(MockRedisClient);\n      expect(currentClient.clientMetadata).toEqual(mockCommonClientMetadata);\n      while (clients1.length) {\n        expect(currentClient).toBe(clients1[0]);\n        currentClient = clients1.shift();\n      }\n\n      // test with a separate user/metadata\n      const clients2 = await Promise.all([\n        service.getOrCreateClient(mockClientMetadata2),\n        service.getOrCreateClient(mockClientMetadata2),\n        service.getOrCreateClient(mockClientMetadata2),\n      ]);\n      currentClient = clients2.shift();\n      expect(currentClient).toBeInstanceOf(MockRedisClient);\n      expect(currentClient.clientMetadata).toEqual(mockClientMetadata2);\n      while (clients2.length) {\n        expect(currentClient).toBe(clients2[0]);\n        currentClient = clients2.shift();\n      }\n\n      expect(redisClientFactory.createClient).toHaveBeenCalledTimes(2);\n    });\n\n    it('should reject multiple failed calls with the same error instance', async () => {\n      const mockCommonClientMetadata2: ClientMetadata = {\n        sessionMetadata: {\n          userId: 'user-4',\n          accountId: 'acc-4',\n          sessionId: uuidv4(),\n        },\n        databaseId: uuidv4(),\n        context: ClientContext.Common,\n      };\n      const error1 = new Error('Error 1');\n      const error2 = new Error('Error 2');\n\n      const createClientSpy = jest\n        .spyOn(service, 'createClient')\n        .mockImplementationOnce(\n          () =>\n            new Promise((_, reject) => {\n              reject(error1);\n            }),\n        )\n        .mockImplementationOnce(\n          () =>\n            new Promise((_, reject) => {\n              reject(error2);\n            }),\n        );\n\n      const clients = await Promise.all([\n        service\n          .getOrCreateClient(mockCommonClientMetadata)\n          .catch((err) => ({ error: err })),\n        service\n          .getOrCreateClient(mockCommonClientMetadata)\n          .catch((err) => ({ error: err })),\n        service\n          .getOrCreateClient(mockCommonClientMetadata)\n          .catch((err) => ({ error: err })),\n        service\n          .getOrCreateClient(mockCommonClientMetadata2)\n          .catch((err) => ({ error: err })),\n        service\n          .getOrCreateClient(mockCommonClientMetadata2)\n          .catch((err) => ({ error: err })),\n      ]);\n\n      expect(createClientSpy).toHaveBeenCalledTimes(2);\n\n      for (let a = 0; a < 3; a += 1) {\n        const resp = clients[a] as { error?: any };\n        expect(resp.error).toBe(error1);\n      }\n      for (let a = 3; a < clients.length; a += 1) {\n        const resp = clients[a] as { error?: any };\n        expect(resp.error).toBe(error2);\n      }\n    });\n  });\n\n  describe('createClient', () => {\n    it('should create new client and not update connection type', async () => {\n      jest\n        .spyOn(redisClientFactory, 'createClient')\n        .mockResolvedValueOnce(mockStandaloneRedisClient);\n      expect(await service.createClient(mockCommonClientMetadata)).toEqual(\n        mockStandaloneRedisClient,\n      );\n      expect(databaseService.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCommonClientMetadata.databaseId,\n      );\n      expect(databaseRepository.update).not.toHaveBeenCalled();\n    });\n    it('should create new client and update connection type (first connection)', async () => {\n      jest\n        .spyOn(redisClientFactory, 'createClient')\n        .mockResolvedValueOnce(mockStandaloneRedisClient);\n      databaseService.get.mockResolvedValueOnce({\n        ...mockDatabase,\n        connectionType: ConnectionType.NOT_CONNECTED,\n      });\n      expect(await service.createClient(mockCommonClientMetadata)).toEqual(\n        mockStandaloneRedisClient,\n      );\n      expect(databaseService.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCommonClientMetadata.databaseId,\n      );\n      expect(databaseRepository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockCommonClientMetadata.databaseId,\n        {\n          connectionType: mockDatabase.connectionType,\n        },\n      );\n    });\n\n    it('should throw original error and emit connection failed event for RedisConnection* errors', async () => {\n      jest\n        .spyOn(redisClientFactory, 'createClient')\n        .mockRejectedValue(new RedisConnectionTimeoutException());\n      await expect(\n        service.createClient(mockCommonClientMetadata),\n      ).rejects.toThrow(RedisConnectionTimeoutException);\n      expect(analytics.sendConnectionFailedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase,\n        new RedisConnectionTimeoutException(),\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        DatabaseConnectionEvent.DatabaseConnectionFailed,\n        mockCommonClientMetadata,\n      );\n    });\n\n    it('should throw original error and not emit connection failed when not RedisConnection* errors', async () => {\n      jest\n        .spyOn(redisClientFactory, 'createClient')\n        .mockRejectedValue(new InternalServerErrorException());\n      await expect(\n        service.createClient(mockCommonClientMetadata),\n      ).rejects.toThrow(InternalServerErrorException);\n      expect(analytics.sendConnectionFailedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabase,\n        new InternalServerErrorException(),\n      );\n\n      expect(eventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database.client.factory.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseAnalytics } from 'src/modules/database/database.analytics';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { ConnectionType } from 'src/modules/database/entities/database.entity';\nimport { ClientMetadata } from 'src/common/models';\nimport { RedisClient } from 'src/modules/redis/client';\nimport {\n  IRedisConnectionOptions,\n  RedisClientFactory,\n} from 'src/modules/redis/redis.client.factory';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { RedisConnectionFailedException } from 'src/modules/redis/exceptions/connection';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { DatabaseConnectionEvent } from 'src/modules/database/constants/events';\nimport { CredentialStrategyProvider } from 'src/modules/database/credentials/credential-strategy.provider';\n\ntype IsClientConnectingMap = {\n  [key: string]: boolean;\n};\n\ntype PendingGetByClientIdMap = {\n  [key: string]: {\n    resolve: (value: RedisClient) => void;\n    reject: (reason?: any) => void;\n  }[];\n};\n\n@Injectable()\nexport class DatabaseClientFactory {\n  private logger = new Logger('DatabaseClientFactory');\n\n  private isConnecting: IsClientConnectingMap = {};\n\n  private pendingGetClient: PendingGetByClientIdMap = {};\n\n  constructor(\n    private readonly databaseService: DatabaseService,\n    private readonly repository: DatabaseRepository,\n    private readonly analytics: DatabaseAnalytics,\n    private readonly redisClientStorage: RedisClientStorage,\n    private readonly redisClientFactory: RedisClientFactory,\n    private readonly eventEmitter: EventEmitter2,\n    private readonly credentialProvider: CredentialStrategyProvider,\n  ) {}\n\n  private async processGetClient(\n    clientId: string,\n    clientMetadata: ClientMetadata,\n  ) {\n    if (this.isConnecting[clientId]) {\n      this.logger.debug(\n        'Client already connecting. Queueing get client request',\n        { clientId },\n      );\n      return;\n    }\n    if (!this.pendingGetClient[clientId].length) {\n      return;\n    }\n\n    const { resolve, reject } = this.pendingGetClient[clientId].shift();\n    this.isConnecting[clientId] = true;\n    try {\n      this.logger.debug('Creating new client', { clientId });\n      const newClient = await this.createClient(clientMetadata);\n      await this.redisClientStorage.set(newClient);\n\n      resolve(newClient);\n\n      // resolve pending gets\n      while (this.pendingGetClient[clientId]?.length) {\n        const next = this.pendingGetClient[clientId].shift();\n        next?.resolve(newClient);\n      }\n    } catch (error) {\n      reject(error);\n\n      // reject pending gets\n      while (this.pendingGetClient[clientId]?.length) {\n        const next = this.pendingGetClient[clientId].shift();\n        next?.reject(error);\n      }\n    } finally {\n      delete this.pendingGetClient[clientId];\n      delete this.isConnecting[clientId];\n    }\n  }\n\n  /**\n   * Gets existing database client by client metadata or\n   * fetches database and create client new client for it\n   * Also saves client to the clients pool to not create the same client in the future\n   * Client from the pool of clients will be automatically deleted by idle time\n   * @param clientMetadata\n   */\n  async getOrCreateClient(\n    clientMetadata: ClientMetadata,\n  ): Promise<RedisClient> {\n    this.logger.debug('Trying to get existing redis client.', clientMetadata);\n\n    const client = await this.redisClientStorage.getByMetadata(clientMetadata);\n\n    if (client) {\n      return client;\n    }\n\n    const clientId = RedisClient.generateId(\n      RedisClient.prepareClientMetadata(clientMetadata),\n    );\n\n    // add promise to queue and then process queue immediately\n    // in case another fetch is not already running\n    return new Promise((resolve, reject) => {\n      if (!this.pendingGetClient[clientId]) {\n        this.pendingGetClient[clientId] = [];\n      }\n      this.pendingGetClient[clientId].push({ resolve, reject });\n      this.processGetClient(clientId, clientMetadata);\n    });\n  }\n\n  /**\n   * Simply gets database and creates a client.\n   * Will always return new client. There is no check for the same client already exists\n   * Could be used to create temporary client for some purposes or to \"isolate\" client\n   * for some business logic\n   * ! Will be not automatically closed by idle time\n   * @param clientMetadata\n   * @param options\n   */\n  async createClient(\n    clientMetadata: ClientMetadata,\n    options?: IRedisConnectionOptions,\n  ): Promise<RedisClient> {\n    this.logger.debug('Creating new redis client.', clientMetadata);\n    let database = await this.databaseService.get(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n    );\n\n    database = await this.credentialProvider.resolve(database);\n\n    try {\n      const client = await this.redisClientFactory.createClient(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      if (database.connectionType === ConnectionType.NOT_CONNECTED) {\n        await this.repository.update(\n          clientMetadata.sessionMetadata,\n          database.id,\n          {\n            connectionType:\n              client.getConnectionType() as unknown as ConnectionType,\n          },\n        );\n      }\n\n      return client;\n    } catch (error) {\n      this.logger.error('Failed to create database client', error);\n\n      if (error instanceof RedisConnectionFailedException) {\n        this.eventEmitter.emit(\n          DatabaseConnectionEvent.DatabaseConnectionFailed,\n          clientMetadata,\n        );\n      }\n\n      this.analytics.sendConnectionFailedEvent(\n        clientMetadata.sessionMetadata,\n        database,\n        error,\n      );\n\n      throw error;\n    }\n  }\n\n  /**\n   * Delete existing database client by client metadata.\n   * @param clientMetadata\n   */\n  async deleteClient(clientMetadata: ClientMetadata): Promise<number> {\n    this.logger.debug('Trying to delete existing redis client.');\n\n    const client = await this.redisClientStorage.getByMetadata(clientMetadata);\n    return this.redisClientStorage.remove(client?.id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database.factory.spec.ts",
    "content": "import {\n  mockRedisSentinelUtilModule,\n  mockRedisClusterUtilModule,\n  mockRedisClusterUtil,\n  mockRedisSentinelUtil,\n} from 'src/__mocks__/redis-utils';\n\njest.doMock('src/modules/redis/utils/cluster.util', mockRedisClusterUtilModule);\njest.doMock(\n  'src/modules/redis/utils/sentinel.util',\n  mockRedisSentinelUtilModule,\n);\n\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCaCertificateService,\n  mockClientCertificateService,\n  mockClusterDatabaseWithTlsAuth,\n  mockClusterRedisClient,\n  mockCredentialProvider,\n  mockDatabase,\n  mockDatabaseInfoProvider,\n  mockDatabaseWithTlsAuth,\n  mockRedisClientFactory,\n  mockRedisNoPermError,\n  mockSentinelDatabaseWithTlsAuth,\n  mockSentinelRedisClient,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { DatabaseFactory } from 'src/modules/database/providers/database.factory';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { CaCertificateService } from 'src/modules/certificate/ca-certificate.service';\nimport { ClientCertificateService } from 'src/modules/certificate/client-certificate.service';\nimport { ConnectionType } from 'src/modules/database/entities/database.entity';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { NotFoundException } from '@nestjs/common';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { CredentialStrategyProvider } from 'src/modules/database/credentials/credential-strategy.provider';\n\ndescribe('DatabaseFactory', () => {\n  let service: DatabaseFactory;\n  let credentialProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseFactory,\n        {\n          provide: RedisClientFactory,\n          useFactory: mockRedisClientFactory,\n        },\n        {\n          provide: DatabaseInfoProvider,\n          useFactory: mockDatabaseInfoProvider,\n        },\n        {\n          provide: CaCertificateService,\n          useFactory: mockCaCertificateService,\n        },\n        {\n          provide: ClientCertificateService,\n          useFactory: mockClientCertificateService,\n        },\n        {\n          provide: CredentialStrategyProvider,\n          useFactory: mockCredentialProvider,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseFactory);\n    credentialProvider = await module.get(CredentialStrategyProvider);\n  });\n\n  describe('createDatabaseModel', () => {\n    it('should create standalone database model', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(false);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(false);\n\n      const result = await service.createDatabaseModel(\n        mockSessionMetadata,\n        mockDatabase,\n      );\n\n      expect(result).toEqual(mockDatabase);\n    });\n    it('should create sentinel database model', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(true);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(false);\n\n      const result = await service.createDatabaseModel(\n        mockSessionMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockSentinelDatabaseWithTlsAuth);\n    });\n    it('should throw an error for sentinel without sentinelMaster field provided', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(true);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(false);\n      try {\n        await service.createDatabaseModel(mockSessionMetadata, mockDatabase);\n        fail();\n      } catch (e) {\n        expect(e.message).toEqual(ERROR_MESSAGES.SENTINEL_MASTER_NAME_REQUIRED);\n      }\n    });\n    it('should create cluster database model', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(false);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(true);\n\n      const result = await service.createDatabaseModel(\n        mockSessionMetadata,\n        mockClusterDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockClusterDatabaseWithTlsAuth);\n    });\n    it('should create standalone model when cluster database is passed but with standalone flag on', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(false);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(true);\n\n      const result = await service.createDatabaseModel(mockSessionMetadata, {\n        ...mockClusterDatabaseWithTlsAuth,\n        forceStandalone: true,\n      });\n\n      expect({\n        forceStandalone: result.forceStandalone,\n        connectionType: result.connectionType,\n      }).toEqual({\n        forceStandalone: true,\n        connectionType: ConnectionType.STANDALONE,\n      });\n    });\n\n    it('should resolve credentials before creating standalone client', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(false);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(false);\n\n      await service.createDatabaseModel(mockSessionMetadata, mockDatabase);\n\n      expect(credentialProvider.resolve).toHaveBeenCalledWith(mockDatabase);\n    });\n\n    it('should resolve credentials before creating cluster client', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(false);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(true);\n\n      await service.createDatabaseModel(\n        mockSessionMetadata,\n        mockClusterDatabaseWithTlsAuth,\n      );\n\n      expect(credentialProvider.resolve).toHaveBeenCalledWith(\n        mockClusterDatabaseWithTlsAuth,\n      );\n    });\n\n    it('should resolve credentials before creating sentinel client', async () => {\n      mockRedisSentinelUtil.isSentinel.mockResolvedValue(true);\n      mockRedisClusterUtil.isCluster.mockResolvedValue(false);\n\n      await service.createDatabaseModel(\n        mockSessionMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n      );\n\n      expect(credentialProvider.resolve).toHaveBeenCalledWith(\n        mockSentinelDatabaseWithTlsAuth,\n      );\n    });\n  });\n\n  describe('createStandaloneDatabaseModel', () => {\n    it('should create standalone database model without certs', async () => {\n      const result = await service.createStandaloneDatabaseModel(mockDatabase);\n\n      expect(result).toEqual(mockDatabase);\n    });\n    it('should create standalone database model and fetch existing certs', async () => {\n      const result = await service.createStandaloneDatabaseModel(\n        mockDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTlsAuth);\n    });\n  });\n\n  describe('createClusterDatabaseModel', () => {\n    it('should create cluster database model', async () => {\n      const result = await service.createClusterDatabaseModel(\n        mockSessionMetadata,\n        {\n          ...mockClusterDatabaseWithTlsAuth,\n          connectionType: ConnectionType.STANDALONE,\n        },\n        mockStandaloneRedisClient,\n      );\n\n      expect(result).toEqual(mockClusterDatabaseWithTlsAuth);\n      expect(mockClusterRedisClient.disconnect).toHaveBeenCalled();\n    });\n\n    it('should throw ACL error if no permissions', async () => {\n      mockRedisClusterUtil.discoverClusterNodes.mockRejectedValueOnce(\n        mockRedisNoPermError,\n      );\n\n      try {\n        await service.createClusterDatabaseModel(\n          mockSessionMetadata,\n          {\n            ...mockSentinelDatabaseWithTlsAuth,\n            connectionType: ConnectionType.STANDALONE,\n          },\n          mockStandaloneRedisClient,\n        );\n        fail();\n      } catch (e) {\n        // todo: returned BadRequest. why not Forbidden?\n        // expect(e).toBeInstanceOf(ForbiddenException);\n        expect(e.message).toEqual(mockRedisNoPermError.message);\n      }\n    });\n  });\n\n  describe('createSentinelDatabaseModel', () => {\n    it('should create sentinel database model', async () => {\n      const result = await service.createSentinelDatabaseModel(\n        mockSessionMetadata,\n        {\n          ...mockSentinelDatabaseWithTlsAuth,\n          connectionType: ConnectionType.STANDALONE,\n        },\n        mockStandaloneRedisClient,\n      );\n\n      expect(result).toEqual(mockSentinelDatabaseWithTlsAuth);\n      expect(mockSentinelRedisClient.disconnect).toHaveBeenCalled();\n    });\n\n    it('should throw NotFound error if no such master group', async () => {\n      try {\n        await service.createSentinelDatabaseModel(\n          mockSessionMetadata,\n          {\n            ...mockSentinelDatabaseWithTlsAuth,\n            connectionType: ConnectionType.STANDALONE,\n            sentinelMaster: {\n              ...mockSentinelDatabaseWithTlsAuth.sentinelMaster,\n              name: 'not existing master group',\n            },\n          },\n          mockStandaloneRedisClient,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.MASTER_GROUP_NOT_EXIST);\n      }\n    });\n\n    it('should throw ACL error if no permissions', async () => {\n      mockRedisSentinelUtil.discoverSentinelMasterGroups.mockRejectedValueOnce(\n        mockRedisNoPermError,\n      );\n\n      try {\n        await service.createSentinelDatabaseModel(\n          mockSessionMetadata,\n          {\n            ...mockSentinelDatabaseWithTlsAuth,\n            connectionType: ConnectionType.STANDALONE,\n            sentinelMaster: {\n              ...mockSentinelDatabaseWithTlsAuth.sentinelMaster,\n              name: 'not existing master group',\n            },\n          },\n          mockStandaloneRedisClient,\n        );\n        fail();\n      } catch (e) {\n        // todo: returned BadRequest. why not Forbidden?\n        // expect(e).toBeInstanceOf(ForbiddenException);\n        expect(e.message).toEqual(mockRedisNoPermError.message);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/providers/database.factory.ts",
    "content": "import { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { v4 as uuidv4 } from 'uuid';\nimport {\n  ConnectionType,\n  HostingProvider,\n} from 'src/modules/database/entities/database.entity';\nimport { getHostingProvider, getRedisConnectionException } from 'src/utils';\nimport { Database } from 'src/modules/database/models/database';\nimport { ClientContext, SessionMetadata } from 'src/common/models';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';\nimport { RedisErrorCodes } from 'src/constants';\nimport { CaCertificateService } from 'src/modules/certificate/ca-certificate.service';\nimport { ClientCertificateService } from 'src/modules/certificate/client-certificate.service';\nimport {\n  IRedisConnectionOptions,\n  RedisClientFactory,\n} from 'src/modules/redis/redis.client.factory';\nimport {\n  discoverClusterNodes,\n  discoverSentinelMasterGroups,\n  isCluster,\n  isSentinel,\n} from 'src/modules/redis/utils';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { ReplyError } from 'src/models';\nimport { CredentialStrategyProvider } from 'src/modules/database/credentials/credential-strategy.provider';\n\n@Injectable()\nexport class DatabaseFactory {\n  private readonly logger = new Logger('DatabaseFactory');\n\n  constructor(\n    private redisClientFactory: RedisClientFactory,\n    private databaseInfoProvider: DatabaseInfoProvider,\n    private caCertificateService: CaCertificateService,\n    private clientCertificateService: ClientCertificateService,\n    private credentialProvider: CredentialStrategyProvider,\n  ) {}\n\n  /**\n   * Create model\n   * @param sessionMetadata\n   * @param database\n   * @param options\n   */\n  async createDatabaseModel(\n    sessionMetadata: SessionMetadata,\n    originalDatabase: Database,\n    options: IRedisConnectionOptions = {},\n  ): Promise<Database> {\n    const database = await this.credentialProvider.resolve(originalDatabase);\n\n    let model = await this.createStandaloneDatabaseModel(database);\n\n    const client = await this.redisClientFactory\n      .getConnectionStrategy()\n      .createStandaloneClient(\n        {\n          sessionMetadata,\n          databaseId: database.id || uuidv4(), // we assume that if no database id defined we are in creation process\n          context: ClientContext.Common,\n        },\n        database,\n        { ...options, useRetry: true },\n      );\n\n    if (!HostingProvider[model.provider]) {\n      model.provider = await getHostingProvider(client, model.host);\n    }\n\n    if (await isSentinel(client)) {\n      if (!database.sentinelMaster) {\n        throw getRedisConnectionException(\n          new ReplyError(RedisErrorCodes.SentinelParamsRequired),\n          database,\n        );\n      }\n      model = await this.createSentinelDatabaseModel(\n        sessionMetadata,\n        database,\n        client,\n      );\n    } else if (!database.forceStandalone && (await isCluster(client))) {\n      model = await this.createClusterDatabaseModel(\n        sessionMetadata,\n        database,\n        client,\n      );\n    }\n\n    model.modules =\n      await this.databaseInfoProvider.determineDatabaseModules(client);\n    model.version =\n      await this.databaseInfoProvider.determineDatabaseServer(client);\n    model.lastConnection = new Date();\n\n    await client.disconnect();\n\n    return model;\n  }\n\n  /**\n   * Determines provider and creates or fetches certificates\n   * Creates database model for standalone connection type\n   * This method is a parent one for other databases types (CLUSTER, SENTINEL)\n   * @param database\n   * @private\n   */\n  async createStandaloneDatabaseModel(database: Database): Promise<Database> {\n    const model = database;\n\n    model.connectionType = ConnectionType.STANDALONE;\n\n    // fetch ca cert if needed to be able to connect\n    if (model.caCert?.id) {\n      model.caCert = await this.caCertificateService.get(model.caCert?.id);\n    }\n\n    // fetch client cert if needed to be able to connect\n    if (model.clientCert?.id) {\n      model.clientCert = await this.clientCertificateService.get(\n        model.clientCert?.id,\n      );\n    }\n\n    return model;\n  }\n\n  /**\n   * Fetches cluster nodes\n   * Creates cluster client to validate connection. Disconnect after check\n   * Creates database model for cluster connection type\n   * @param sessionMetadata\n   * @param database\n   * @param client\n   * @private\n   */\n  async createClusterDatabaseModel(\n    sessionMetadata: SessionMetadata,\n    database: Database,\n    client: RedisClient,\n  ): Promise<Database> {\n    try {\n      const model = database;\n\n      model.nodes = await discoverClusterNodes(client);\n\n      const clusterClient = await this.redisClientFactory\n        .getConnectionStrategy()\n        .createClusterClient(\n          {\n            sessionMetadata,\n            databaseId: model.id || uuidv4(), // we assume that if no database id defined we are in creation process\n            context: ClientContext.Common,\n          },\n          model,\n          { useRetry: true },\n        );\n\n      model.connectionType = ConnectionType.CLUSTER;\n\n      await clusterClient.disconnect();\n\n      return model;\n    } catch (error) {\n      this.logger.error('Failed to add oss cluster.', error, sessionMetadata);\n\n      throw error;\n    }\n  }\n\n  /**\n   * Fetches sentinel masters and align with defined one\n   * Creates sentinel client to validate connection. Disconnect after check\n   * Creates database model for cluster connection type\n   * @param sessionMetadata\n   * @param database\n   * @param client\n   * @private\n   */\n  async createSentinelDatabaseModel(\n    sessionMetadata: SessionMetadata,\n    database: Database,\n    client: RedisClient,\n  ): Promise<Database> {\n    try {\n      const model = database;\n      const masterGroups = await discoverSentinelMasterGroups(client);\n      const selectedMaster = masterGroups.find(\n        (master) => master.name === model.sentinelMaster.name,\n      );\n\n      if (!selectedMaster) {\n        return Promise.reject(\n          new NotFoundException(ERROR_MESSAGES.MASTER_GROUP_NOT_EXIST),\n        );\n      }\n\n      const sentinelClient = await this.redisClientFactory\n        .getConnectionStrategy()\n        .createSentinelClient(\n          {\n            sessionMetadata,\n            databaseId: model.id || uuidv4(), // we assume that if no database id defined we are in creation process\n            context: ClientContext.Common,\n          },\n          model,\n          { useRetry: true },\n        );\n\n      model.connectionType = ConnectionType.SENTINEL;\n      model.nodes = selectedMaster.nodes;\n      await sentinelClient.disconnect();\n\n      return model;\n    } catch (error) {\n      this.logger.error(\n        'Failed to create database sentinel model.',\n        error,\n        sessionMetadata,\n      );\n\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/repositories/database.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { Database } from 'src/modules/database/models/database';\n\nexport abstract class DatabaseRepository {\n  /**\n   * Fast check if database exists by id\n   * No need to retrieve any fields should return boolean only\n   * @param sessionMetadata\n   * @param id\n   * @return boolean\n   */\n  abstract exists(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<boolean>;\n\n  /**\n   * Get single database by id with all fields\n   * @param sessionMetadata\n   * @param id\n   * @param ignoreEncryptionErrors\n   * @param omitFields\n   * @return Database\n   */\n  abstract get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    ignoreEncryptionErrors?: boolean,\n    omitFields?: string[],\n  ): Promise<Database>;\n\n  /**\n   * List of databases (limited fields only)\n   * Fields: ['id', 'name', 'host', 'port', 'db', 'connectionType', 'modules', 'lastConnection]\n   * @param sessionMetadata\n   * @return Database[]\n   */\n  abstract list(sessionMetadata: SessionMetadata): Promise<Database[]>;\n\n  /**\n   * Create database\n   * @param sessionMetadata\n   * @param database\n   * @param uniqueCheck\n   */\n  abstract create(\n    sessionMetadata: SessionMetadata,\n    database: Database,\n    uniqueCheck: boolean,\n  ): Promise<Database>;\n\n  /**\n   * Update database with new data\n   * @param sessionMetadata\n   * @param id\n   * @param database\n   */\n  abstract update(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    database: Partial<Database>,\n  ): Promise<Database>;\n\n  /**\n   * Delete database by id\n   * @param sessionMetadata\n   * @param id\n   */\n  abstract delete(sessionMetadata: SessionMetadata, id: string): Promise<void>;\n\n  /**\n   * Cleanup databases which were created on startup from a file or env variables\n   * @param excludeIds\n   */\n  abstract cleanupPreSetup(\n    excludeIds?: string[],\n  ): Promise<{ affected: number }>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/repositories/local.database.repository.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { pick, omit } from 'lodash';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { In, Not, Repository } from 'typeorm';\nimport {\n  mockCaCertificateRepository,\n  mockClientCertificateRepository,\n  mockClusterDatabaseWithTlsAuth,\n  mockClusterDatabaseWithTlsAuthEntity,\n  mockDatabase,\n  mockDatabaseEntity,\n  mockDatabaseEntityWithCloudDetails,\n  mockDatabaseId,\n  mockDatabasePasswordEncrypted,\n  mockDatabasePasswordPlain,\n  mockDatabaseSentinelMasterPasswordEncrypted,\n  mockDatabaseSentinelMasterPasswordPlain,\n  mockDatabaseWithCloudDetails,\n  mockDatabaseWithSshBasic,\n  mockDatabaseWithSshBasicEntity,\n  mockDatabaseWithSshPrivateKey,\n  mockDatabaseWithSshPrivateKeyEntity,\n  mockDatabaseWithTls,\n  mockDatabaseWithTlsAuth,\n  mockDatabaseWithTlsAuthEntity,\n  mockDatabaseWithTlsEntity,\n  mockDatabaseWithTags,\n  mockDatabaseWithTagsEntity,\n  mockEncryptionService,\n  mockRepository,\n  mockSentinelDatabaseWithTlsAuth,\n  mockSentinelDatabaseWithTlsAuthEntity,\n  mockSessionMetadata,\n  mockSshOptionsBasicEntity,\n  mockSshOptionsPassphraseEncrypted,\n  mockSshOptionsPassphrasePlain,\n  mockSshOptionsPasswordEncrypted,\n  mockSshOptionsPasswordPlain,\n  mockSshOptionsPrivateKeyEncrypted,\n  mockSshOptionsPrivateKeyEntity,\n  mockSshOptionsPrivateKeyPlain,\n  mockSshOptionsUsernameEncrypted,\n  mockSshOptionsUsernamePlain,\n  MockType,\n  mockTagsRepository,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { LocalDatabaseRepository } from 'src/modules/database/repositories/local.database.repository';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { cloneClassInstance } from 'src/utils';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseAlreadyExistsException } from 'src/modules/database/exeptions';\nimport { TagRepository } from 'src/modules/tag/repository/tag.repository';\n\nconst listFields = [\n  'id',\n  'name',\n  'host',\n  'port',\n  'db',\n  'timeout',\n  'connectionType',\n  'modules',\n  'lastConnection',\n  'version',\n  'cloudDetails',\n  'tags',\n];\n\ndescribe('LocalDatabaseRepository', () => {\n  let service: LocalDatabaseRepository;\n  let encryptionService: MockType<EncryptionService>;\n  let repository: MockType<Repository<DatabaseEntity>>;\n  let tagRepository: MockType<TagRepository>;\n  let sshOptionsRepository: MockType<Repository<SshOptionsEntity>>;\n  let caCertRepository: MockType<CaCertificateRepository>;\n  let clientCertRepository: MockType<ClientCertificateRepository>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalDatabaseRepository,\n        {\n          provide: getRepositoryToken(DatabaseEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: getRepositoryToken(SshOptionsEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n        {\n          provide: CaCertificateRepository,\n          useFactory: mockCaCertificateRepository,\n        },\n        {\n          provide: ClientCertificateRepository,\n          useFactory: mockClientCertificateRepository,\n        },\n        {\n          provide: TagRepository,\n          useFactory: mockTagsRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(DatabaseEntity));\n    tagRepository = await module.get(TagRepository);\n    sshOptionsRepository = await module.get(\n      getRepositoryToken(SshOptionsEntity),\n    );\n    caCertRepository = await module.get(CaCertificateRepository);\n    clientCertRepository = await module.get(ClientCertificateRepository);\n    encryptionService = await module.get(EncryptionService);\n    service = await module.get(LocalDatabaseRepository);\n\n    repository.findOne.mockResolvedValue(mockDatabaseEntity);\n    repository\n      .createQueryBuilder()\n      .getOne.mockResolvedValue(mockDatabaseEntity);\n    repository\n      .createQueryBuilder()\n      .getMany.mockResolvedValue([\n        Object.assign(\n          new DatabaseEntity(),\n          pick(mockDatabaseWithTlsAuthEntity, ...listFields),\n        ),\n        Object.assign(\n          new DatabaseEntity(),\n          pick(mockDatabaseWithTlsAuthEntity, ...listFields),\n        ),\n      ]);\n    repository.save.mockResolvedValue(mockDatabaseEntity);\n    repository.update.mockResolvedValue(mockDatabaseEntity);\n\n    when(encryptionService.decrypt)\n      .defaultImplementation(async (data) => data || undefined)\n      .calledWith(mockDatabasePasswordEncrypted, expect.anything())\n      .mockResolvedValue(mockDatabasePasswordPlain)\n      .calledWith(\n        mockDatabaseSentinelMasterPasswordEncrypted,\n        expect.anything(),\n      )\n      .mockResolvedValue(mockDatabaseSentinelMasterPasswordPlain)\n      .calledWith(mockSshOptionsUsernameEncrypted, expect.anything())\n      .mockResolvedValue(mockSshOptionsUsernamePlain)\n      .calledWith(mockSshOptionsPasswordEncrypted, expect.anything())\n      .mockResolvedValue(mockSshOptionsPasswordPlain)\n      .calledWith(mockSshOptionsPrivateKeyEncrypted, expect.anything())\n      .mockResolvedValue(mockSshOptionsPrivateKeyPlain)\n      .calledWith(mockSshOptionsPassphraseEncrypted, expect.anything())\n      .mockResolvedValue(mockSshOptionsPassphrasePlain);\n    when(encryptionService.encrypt)\n      .defaultImplementation(async (data) => data || undefined)\n      .calledWith(mockDatabasePasswordPlain)\n      .mockResolvedValue({\n        data: mockDatabasePasswordEncrypted,\n        encryption: mockDatabaseWithTlsAuthEntity.encryption,\n      })\n      .calledWith(mockDatabaseSentinelMasterPasswordPlain)\n      .mockResolvedValue({\n        data: mockDatabaseSentinelMasterPasswordEncrypted,\n        encryption: mockDatabaseWithTlsAuthEntity.encryption,\n      })\n      .calledWith(mockSshOptionsUsernamePlain)\n      .mockResolvedValue({\n        data: mockSshOptionsUsernameEncrypted,\n        encryption: mockSshOptionsBasicEntity.encryption,\n      })\n      .calledWith(mockSshOptionsPasswordPlain)\n      .mockResolvedValue({\n        data: mockSshOptionsPasswordEncrypted,\n        encryption: mockSshOptionsBasicEntity.encryption,\n      })\n      .calledWith(mockSshOptionsPrivateKeyPlain)\n      .mockResolvedValue({\n        data: mockSshOptionsPrivateKeyEncrypted,\n        encryption: mockSshOptionsPrivateKeyEntity.encryption,\n      })\n      .calledWith(mockSshOptionsPassphrasePlain)\n      .mockResolvedValue({\n        data: mockSshOptionsPassphraseEncrypted,\n        encryption: mockSshOptionsPrivateKeyEntity.encryption,\n      });\n  });\n\n  describe('exists', () => {\n    it('should return true when receive database entity', async () => {\n      expect(await service.exists(mockSessionMetadata, mockDatabaseId)).toEqual(\n        true,\n      );\n    });\n\n    it('should return false when no database received', async () => {\n      repository.createQueryBuilder().getOne.mockResolvedValue(null);\n      expect(await service.exists(mockSessionMetadata, mockDatabaseId)).toEqual(\n        false,\n      );\n    });\n  });\n\n  describe('get', () => {\n    it('should return standalone database model', async () => {\n      const result = await service.get(mockSessionMetadata, mockDatabaseId);\n\n      expect(result).toEqual(mockDatabase);\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n\n    it('should return standalone database model with ssh enabled (basic)', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithSshBasicEntity);\n      const result = await service.get(\n        mockSessionMetadata,\n        mockDatabaseWithSshBasic.id,\n      );\n\n      expect(result).toEqual(mockDatabaseWithSshBasic);\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n\n    it('should return standalone database model with ssh enabled (privateKey + passphrase)', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithSshPrivateKeyEntity);\n      const result = await service.get(\n        mockSessionMetadata,\n        mockDatabaseWithSshPrivateKey.id,\n      );\n\n      expect(result).toEqual(mockDatabaseWithSshPrivateKey);\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n\n    it('should return standalone model with ca tls', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithTlsEntity);\n\n      const result = await service.get(mockSessionMetadata, mockDatabaseId);\n\n      expect(result).toEqual(mockDatabaseWithTls);\n      expect(caCertRepository.get).toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n\n    it('should return sentinel tls database model (with fields decryption)', async () => {\n      repository.findOne.mockResolvedValue(\n        mockSentinelDatabaseWithTlsAuthEntity,\n      );\n\n      const result = await service.get(mockSessionMetadata, mockDatabaseId);\n\n      expect(result).toEqual(mockSentinelDatabaseWithTlsAuth);\n      expect(caCertRepository.get).toHaveBeenCalled();\n      expect(clientCertRepository.get).toHaveBeenCalled();\n    });\n\n    it('should return cluster database model (with fields decryption)', async () => {\n      repository.findOne.mockResolvedValue(\n        mockClusterDatabaseWithTlsAuthEntity,\n      );\n\n      const result = await service.get(mockSessionMetadata, mockDatabaseId);\n\n      expect(result).toEqual(mockClusterDatabaseWithTlsAuth);\n      expect(caCertRepository.get).toHaveBeenCalled();\n      expect(clientCertRepository.get).toHaveBeenCalled();\n    });\n\n    it('should return null when database was not found', async () => {\n      repository.findOne.mockResolvedValue(undefined);\n\n      const result = await service.get(mockSessionMetadata, mockDatabaseId);\n\n      expect(result).toEqual(null);\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n\n    it('should return standalone database model without omit fields', async () => {\n      const omitFields = ['compressor', 'connectionType'];\n      const result = await service.get(\n        mockSessionMetadata,\n        mockDatabaseId,\n        false,\n        omitFields,\n      );\n\n      expect(result).toEqual(omit(mockDatabase, omitFields));\n    });\n\n    it('should return standalone database model without nested fields', async () => {\n      const omitFields = [\n        'compressor',\n        'sshOptions.passphrase',\n        'sshOptions.privateKey',\n      ];\n\n      repository.findOne.mockResolvedValueOnce(\n        mockDatabaseWithSshPrivateKeyEntity,\n      );\n      const result = await service.get(\n        mockSessionMetadata,\n        mockDatabaseWithSshPrivateKey.id,\n        false,\n        omitFields,\n      );\n\n      expect(result).toEqual(\n        omit(cloneClassInstance(mockDatabaseWithSshPrivateKey), omitFields),\n      );\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n\n    it('should return standalone database model with tags', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithTagsEntity);\n      const result = await service.get(\n        mockSessionMetadata,\n        mockDatabaseWithTags.id,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTags);\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('list', () => {\n    it('should return list of databases with specific fields only', async () => {\n      expect(await service.list(mockSessionMetadata)).toEqual([\n        pick(mockDatabaseWithTlsAuth, ...listFields),\n        pick(mockDatabaseWithTlsAuth, ...listFields),\n      ]);\n    });\n    it('should return list with cloud details', async () => {\n      repository\n        .createQueryBuilder()\n        .getMany.mockResolvedValue([\n          Object.assign(\n            new DatabaseEntity(),\n            pick(mockDatabaseEntityWithCloudDetails, ...listFields),\n          ),\n          Object.assign(\n            new DatabaseEntity(),\n            pick(mockDatabaseEntityWithCloudDetails, ...listFields),\n          ),\n        ]);\n\n      expect(await service.list(mockSessionMetadata)).toEqual([\n        pick(mockDatabaseWithCloudDetails, ...listFields),\n        pick(mockDatabaseWithCloudDetails, ...listFields),\n      ]);\n    });\n\n    it('should return list of databases with tags', async () => {\n      repository\n        .createQueryBuilder()\n        .getMany.mockResolvedValue([\n          Object.assign(\n            new DatabaseEntity(),\n            pick(mockDatabaseWithTagsEntity, ...listFields),\n          ),\n          Object.assign(\n            new DatabaseEntity(),\n            pick(mockDatabaseWithTagsEntity, ...listFields),\n          ),\n        ]);\n\n      expect(await service.list(mockSessionMetadata)).toEqual([\n        pick(mockDatabaseWithTags, ...listFields),\n        pick(mockDatabaseWithTags, ...listFields),\n      ]);\n    });\n  });\n\n  describe('create', () => {\n    it('should create standalone database', async () => {\n      const result = await service.create(\n        mockSessionMetadata,\n        mockDatabase,\n        false,\n      );\n\n      expect(result).toEqual(mockDatabase);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should create standalone database with cloud details', async () => {\n      repository.save.mockResolvedValue(mockDatabaseEntityWithCloudDetails);\n\n      const result = await service.create(\n        mockSessionMetadata,\n        mockDatabaseWithCloudDetails,\n        false,\n      );\n\n      expect(result).toEqual(mockDatabaseWithCloudDetails);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should create standalone database (with existing certificates)', async () => {\n      repository.save.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);\n\n      const result = await service.create(\n        mockSessionMetadata,\n        mockDatabaseWithTlsAuth,\n        false,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTlsAuth);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should create standalone database (and certificates)', async () => {\n      repository.save.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);\n\n      const result = await service.create(\n        mockSessionMetadata,\n        omit(\n          cloneClassInstance(mockDatabaseWithTlsAuth),\n          'caCert.id',\n          'clientCert.id',\n        ),\n        false,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTlsAuth);\n      expect(caCertRepository.create).toHaveBeenCalled();\n      expect(clientCertRepository.create).toHaveBeenCalled();\n    });\n\n    it('should throw an error if create called with cloud details and have the same entity', async () => {\n      repository.findOne.mockResolvedValueOnce(mockDatabaseEntity);\n      try {\n        await service.create(\n          mockSessionMetadata,\n          mockDatabaseEntityWithCloudDetails,\n          true,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(DatabaseAlreadyExistsException);\n        expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_ALREADY_EXISTS);\n        expect(e.response?.resource?.databaseId).toEqual(mockDatabaseEntity.id);\n        expect(repository.save).not.toHaveBeenCalled();\n      }\n    });\n\n    it('should create standalone database with tags', async () => {\n      repository.save.mockResolvedValue(mockDatabaseWithTagsEntity);\n\n      const result = await service.create(\n        mockSessionMetadata,\n        mockDatabaseWithTags,\n        false,\n      );\n\n      expect(tagRepository.getOrCreateByKeyValuePairs).toHaveBeenCalledWith(\n        mockDatabaseWithTags.tags,\n      );\n      expect(result).toEqual(mockDatabaseWithTags);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('update', () => {\n    it('should update standalone database', async () => {\n      repository.merge.mockReturnValue(mockDatabaseEntity);\n\n      const result = await service.update(mockSessionMetadata, mockDatabaseId, {\n        ...mockDatabase,\n        caCert: null,\n        clientCert: null,\n        sshOptions: null,\n      });\n\n      expect(result).toEqual({\n        ...mockDatabase,\n        caCert: null,\n        clientCert: null,\n        sshOptions: null,\n      });\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n      expect(sshOptionsRepository.createQueryBuilder).toHaveBeenCalled();\n    });\n\n    it('should update standalone database with ssh enabled (basic)', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithSshBasicEntity);\n      repository.merge.mockReturnValue(mockDatabaseWithSshBasic);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabaseWithSshBasic,\n      );\n\n      expect(result).toEqual(mockDatabaseWithSshBasic);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should update standalone database with ssh enabled (privateKey)', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithSshPrivateKeyEntity);\n      repository.merge.mockReturnValue(mockDatabaseWithSshPrivateKey);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabaseWithSshPrivateKey,\n      );\n\n      expect(result).toEqual(mockDatabaseWithSshPrivateKey);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should update standalone database (with existing certificates)', async () => {\n      repository.merge.mockReturnValue(mockDatabaseWithTlsAuth);\n      repository.findOne.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);\n      repository.findOne.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTlsAuth);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should update standalone database (and certificates)', async () => {\n      repository.merge.mockReturnValue(mockDatabaseWithTlsAuth);\n      repository.findOne.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);\n      repository.findOne.mockResolvedValueOnce(mockDatabaseWithTlsAuthEntity);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        omit(mockDatabaseWithTlsAuth, 'caCert.id', 'clientCert.id'),\n      );\n\n      expect(result).toEqual(mockDatabaseWithTlsAuth);\n      expect(caCertRepository.create).toHaveBeenCalled();\n      expect(clientCertRepository.create).toHaveBeenCalled();\n    });\n\n    it('should update standalone database with tags', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithTagsEntity);\n      repository.merge.mockReturnValue(mockDatabaseWithTags);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabaseWithTags,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTags);\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n\n    it('should update standalone database with tags and cleanup unused tags', async () => {\n      repository.findOne.mockResolvedValue(mockDatabaseWithTagsEntity);\n      repository.merge.mockReturnValue(mockDatabaseWithTags);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabaseWithTags,\n      );\n\n      expect(result).toEqual(mockDatabaseWithTags);\n      expect(tagRepository.cleanupUnusedTags).toHaveBeenCalled();\n      expect(caCertRepository.create).not.toHaveBeenCalled();\n      expect(clientCertRepository.create).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete database by id', async () => {\n      expect(await service.delete(mockSessionMetadata, mockDatabaseId)).toEqual(\n        undefined,\n      );\n    });\n\n    it('should delete database by id and cleanup unused tags', async () => {\n      await service.delete(mockSessionMetadata, mockDatabaseId);\n      expect(tagRepository.cleanupUnusedTags).toHaveBeenCalled();\n    });\n  });\n\n  describe('cleanupPreSetup', () => {\n    it('should delete databases with isPreSetup flag enabled', async () => {\n      const excludeIds = ['_1', '_2'];\n\n      repository\n        .createQueryBuilder()\n        .delete()\n        .execute.mockResolvedValue({ raw: [], affected: 1 });\n\n      const result = await service.cleanupPreSetup(excludeIds);\n\n      expect(result).toEqual({ affected: 1 });\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        isPreSetup: true,\n        id: Not(In(excludeIds)),\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/repositories/local.database.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { FindOptionsWhere, In, Not, Repository } from 'typeorm';\nimport { get, set, omit } from 'lodash';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { Database } from 'src/modules/database/models/database';\nimport { classToClass } from 'src/utils';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport { DatabaseAlreadyExistsException } from 'src/modules/database/exeptions';\nimport { SessionMetadata } from 'src/common/models';\nimport { TagRepository } from 'src/modules/tag/repository/tag.repository';\nimport { TAG_FIELDS_TO_ENCRYPT } from 'src/modules/tag/repository/local.tag.repository';\n\n@Injectable()\nexport class LocalDatabaseRepository extends DatabaseRepository {\n  private readonly modelEncryptor: ModelEncryptor;\n\n  private readonly sshModelEncryptor: ModelEncryptor;\n\n  private readonly tagModelEncryptor: ModelEncryptor;\n\n  private uniqFieldsForCloudDatabase: string[] = [\n    'host',\n    'port',\n    'username',\n    'password',\n    'caCert.certificate',\n    'clientCert.certificate',\n    'clientCert.key',\n  ];\n\n  constructor(\n    @InjectRepository(DatabaseEntity)\n    protected readonly repository: Repository<DatabaseEntity>,\n    @InjectRepository(SshOptionsEntity)\n    protected readonly sshOptionsRepository: Repository<SshOptionsEntity>,\n    protected readonly caCertificateRepository: CaCertificateRepository,\n    protected readonly clientCertificateRepository: ClientCertificateRepository,\n    protected readonly encryptionService: EncryptionService,\n    protected readonly tagRepository: TagRepository,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, [\n      'password',\n      'sentinelMasterPassword',\n    ]);\n    this.sshModelEncryptor = new ModelEncryptor(encryptionService, [\n      'username',\n      'password',\n      'privateKey',\n      'passphrase',\n    ]);\n    this.tagModelEncryptor = new ModelEncryptor(\n      encryptionService,\n      TAG_FIELDS_TO_ENCRYPT,\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async exists(_: SessionMetadata, id: string): Promise<boolean> {\n    return !!(await this.repository\n      .createQueryBuilder('database')\n      .where({ id })\n      .select(['database.id'])\n      .getOne());\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async get(\n    _: SessionMetadata,\n    id: string,\n    ignoreEncryptionErrors: boolean = false,\n    omitFields: string[] = [],\n  ): Promise<Database> {\n    const entity = await this.repository.findOne({ where: { id } });\n    if (!entity) {\n      return null;\n    }\n\n    const model = classToClass(\n      Database,\n      await this.decryptEntity(entity, ignoreEncryptionErrors),\n    );\n\n    if (entity.caCert) {\n      model.caCert = await this.caCertificateRepository.get(entity.caCert.id);\n    }\n\n    if (entity.clientCert) {\n      model.clientCert = await this.clientCertificateRepository.get(\n        entity.clientCert.id,\n      );\n    }\n    return classToClass(Database, omit(model, omitFields));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async list(\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    _: SessionMetadata,\n  ): Promise<Database[]> {\n    const entities = await this.repository\n      .createQueryBuilder('d')\n      .leftJoinAndSelect('d.cloudDetails', 'cd')\n      .leftJoinAndSelect('d.tags', 'tags')\n      .select([\n        'd.id',\n        'd.name',\n        'd.host',\n        'd.port',\n        'd.db',\n        'd.new',\n        'd.timeout',\n        'd.connectionType',\n        'd.modules',\n        'd.lastConnection',\n        'd.provider',\n        'd.version',\n        'cd',\n        'd.createdAt',\n        'tags',\n      ])\n      .getMany();\n\n    return Promise.all(\n      entities.map(async (entity) =>\n        classToClass(Database, await this.decryptEntity(entity)),\n      ),\n    );\n  }\n\n  /**\n   * Create database with encrypted sensitive fields\n   * @param _\n   * @param database\n   * @param uniqueCheck\n   */\n  public async create(\n    _: SessionMetadata,\n    database: Database,\n    uniqueCheck: boolean,\n  ): Promise<Database> {\n    if (uniqueCheck) {\n      await this.checkUniqueness(database);\n    }\n    const entity = classToClass(\n      DatabaseEntity,\n      await this.populateForeignData(database),\n    );\n    return classToClass(\n      Database,\n      await this.decryptEntity(\n        await this.repository.save(await this.encryptEntity(entity)),\n      ),\n    );\n  }\n\n  /**\n   * Update database entity with fields encryption logic\n   * Should always throw an encryption error to determine that something wrong\n   * with encryption strategy\n   *\n   * @param id\n   * @param database\n   * @throws TBD\n   */\n  public async update(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    database: Partial<Database>,\n  ): Promise<Database> {\n    const oldEntity = await this.decryptEntity(\n      await this.repository.findOne({ where: { id } }),\n      true,\n    );\n    const newEntity = classToClass(\n      DatabaseEntity,\n      await this.populateForeignData(database as Database),\n    );\n\n    const mergeResult = this.repository.merge(oldEntity, newEntity);\n    mergeResult.tags = newEntity.tags;\n\n    if (newEntity.caCert === null) {\n      mergeResult.caCert = null;\n    }\n\n    if (newEntity.clientCert === null) {\n      mergeResult.clientCert = null;\n    }\n\n    if (newEntity.sshOptions === null) {\n      mergeResult.sshOptions = null;\n    }\n\n    if (newEntity.tags) {\n      mergeResult.tags = newEntity.tags;\n    }\n\n    const encrypted = await this.encryptEntity(mergeResult);\n\n    await this.repository.save(encrypted);\n\n    if (database.tags) {\n      await this.tagRepository.cleanupUnusedTags();\n    }\n\n    // workaround for one way cascade deletion\n    if (newEntity.sshOptions === null) {\n      await this.sshOptionsRepository\n        .createQueryBuilder()\n        .delete()\n        .where('databaseId IS NULL')\n        .execute();\n    }\n\n    return this.get(sessionMetadata, id);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async delete(_: SessionMetadata, id: string): Promise<void> {\n    await this.repository.delete(id);\n    await this.tagRepository.cleanupUnusedTags();\n  }\n\n  /**\n   * Get certificates or create certificates if needed\n   * TODO: Rethink implementation to avoid possible transaction issues\n   * @param database\n   * @private\n   */\n  private async populateForeignData(database: Database): Promise<Database> {\n    const model = classToClass(Database, database);\n\n    // fetch ca cert if needed to be able to connect\n    if (!model.caCert?.id && model.caCert?.certificate) {\n      model.caCert = await this.caCertificateRepository.create(model.caCert);\n    }\n\n    // fetch client cert if needed to be able to connect\n    if (\n      !model.clientCert?.id &&\n      (model.clientCert?.certificate || model.clientCert?.key)\n    ) {\n      model.clientCert = await this.clientCertificateRepository.create(\n        model.clientCert,\n      );\n    }\n\n    // process tags\n    if (model.tags?.length) {\n      model.tags = await this.tagRepository.getOrCreateByKeyValuePairs(\n        model.tags,\n      );\n    }\n\n    return model;\n  }\n\n  /**\n   * Encrypt Database entity and SshOptions entity if present\n   * @param entity\n   * @private\n   */\n  private async encryptEntity(entity: DatabaseEntity): Promise<DatabaseEntity> {\n    const encryptedEntity = await this.modelEncryptor.encryptEntity(entity);\n\n    if (encryptedEntity.sshOptions) {\n      encryptedEntity.sshOptions = await this.sshModelEncryptor.encryptEntity(\n        encryptedEntity.sshOptions,\n      );\n    }\n\n    if (encryptedEntity.tags?.length > 0) {\n      encryptedEntity.tags = await this.tagModelEncryptor.encryptEntities(\n        encryptedEntity.tags,\n      );\n    }\n\n    return encryptedEntity;\n  }\n\n  /**\n   * Decrypt Database entity and SshOptions entity if present\n   * @param entity\n   * @param ignoreEncryptionErrors\n   * @private\n   */\n  private async decryptEntity(\n    entity: DatabaseEntity,\n    ignoreEncryptionErrors = false,\n  ): Promise<DatabaseEntity> {\n    const decryptedEntity = await this.modelEncryptor.decryptEntity(\n      entity,\n      ignoreEncryptionErrors,\n    );\n\n    if (decryptedEntity.sshOptions) {\n      decryptedEntity.sshOptions = await this.sshModelEncryptor.decryptEntity(\n        decryptedEntity.sshOptions,\n        ignoreEncryptionErrors,\n      );\n    }\n\n    if (decryptedEntity.tags?.length > 0) {\n      decryptedEntity.tags = await this.tagModelEncryptor.decryptEntities(\n        decryptedEntity.tags,\n        ignoreEncryptionErrors,\n      );\n    }\n\n    return decryptedEntity;\n  }\n\n  /**\n   * Check uniqueness of the database\n   * @param database\n   * @private\n   * @throws DatabaseAlreadyExistsException\n   */\n  private async checkUniqueness(database: Database): Promise<void> {\n    // Do not create a connection if it triggered from cloud and have the same fields\n    if (database.cloudDetails?.cloudId) {\n      const entity = await this.encryptEntity(\n        classToClass(DatabaseEntity, { ...database }),\n      );\n\n      if (entity.caCert) {\n        entity.caCert = await new ModelEncryptor(this.encryptionService, [\n          'certificate',\n        ]).encryptEntity(entity.caCert);\n      }\n\n      if (entity.clientCert) {\n        entity.clientCert = await new ModelEncryptor(this.encryptionService, [\n          'certificate',\n          'key',\n        ]).encryptEntity(entity.clientCert);\n      }\n\n      const query: FindOptionsWhere<DatabaseEntity> = {};\n      this.uniqFieldsForCloudDatabase.forEach((field) => {\n        set(query, field, get(entity, field));\n      });\n\n      const existingDatabase = await this.repository.findOne({ where: query });\n      if (existingDatabase) {\n        throw new DatabaseAlreadyExistsException(existingDatabase.id);\n      }\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async cleanupPreSetup(excludeIds?: string[]): Promise<{ affected: number }> {\n    const { affected } = await this.repository\n      .createQueryBuilder()\n      .delete()\n      .where({\n        isPreSetup: true,\n        id: Not(In(excludeIds)),\n      })\n      .execute();\n\n    return { affected };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/repositories/stack.database.repository.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { pick } from 'lodash';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockCaCertificateRepository,\n  mockClientCertificateRepository,\n  mockConstantsProvider,\n  mockDatabase,\n  mockDatabaseEntity,\n  mockDatabaseId,\n  mockDatabasePasswordEncrypted,\n  mockDatabasePasswordPlain,\n  mockDatabaseSentinelMasterPasswordEncrypted,\n  mockDatabaseSentinelMasterPasswordPlain,\n  mockDatabaseWithTlsAuthEntity,\n  mockEncryptionService,\n  mockRepository,\n  mockSessionMetadata,\n  mockTagsRepository,\n  MockType,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport {\n  ConnectionType,\n  DatabaseEntity,\n} from 'src/modules/database/entities/database.entity';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { StackDatabasesRepository } from 'src/modules/database/repositories/stack.databases.repository';\nimport config from 'src/utils/config';\nimport { NotImplementedException } from '@nestjs/common';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { TagRepository } from 'src/modules/tag/repository/tag.repository';\n\nconst REDIS_STACK_CONFIG = config.get('redisStack');\n\nconst listFields = [\n  'id',\n  'name',\n  'host',\n  'port',\n  'db',\n  'connectionType',\n  'modules',\n  'lastConnection',\n];\n\ndescribe('StackDatabasesRepository', () => {\n  let service: StackDatabasesRepository;\n  let encryptionService: MockType<EncryptionService>;\n  let repository: MockType<Repository<DatabaseEntity>>;\n  let caCertRepository: MockType<CaCertificateRepository>;\n  let clientCertRepository: MockType<ClientCertificateRepository>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        StackDatabasesRepository,\n        {\n          provide: getRepositoryToken(DatabaseEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: getRepositoryToken(SshOptionsEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n        {\n          provide: CaCertificateRepository,\n          useFactory: mockCaCertificateRepository,\n        },\n        {\n          provide: ClientCertificateRepository,\n          useFactory: mockClientCertificateRepository,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n        {\n          provide: TagRepository,\n          useFactory: mockTagsRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(DatabaseEntity));\n    caCertRepository = await module.get(CaCertificateRepository);\n    clientCertRepository = await module.get(ClientCertificateRepository);\n    encryptionService = await module.get(EncryptionService);\n    service = await module.get(StackDatabasesRepository);\n\n    repository.findOne.mockResolvedValue(mockDatabaseEntity);\n    repository\n      .createQueryBuilder()\n      .getOne.mockResolvedValue(mockDatabaseEntity);\n    repository\n      .createQueryBuilder()\n      .getMany.mockResolvedValue([\n        pick(mockDatabaseWithTlsAuthEntity, ...listFields),\n        pick(mockDatabaseWithTlsAuthEntity, ...listFields),\n      ]);\n    repository.save.mockResolvedValue(mockDatabaseEntity);\n    repository.update.mockResolvedValue(mockDatabaseEntity);\n\n    when(encryptionService.decrypt)\n      .calledWith(mockDatabasePasswordEncrypted, expect.anything())\n      .mockResolvedValue(mockDatabasePasswordPlain)\n      .calledWith(\n        mockDatabaseSentinelMasterPasswordEncrypted,\n        expect.anything(),\n      )\n      .mockResolvedValue(mockDatabaseSentinelMasterPasswordPlain);\n    when(encryptionService.encrypt)\n      .calledWith(mockDatabasePasswordPlain)\n      .mockResolvedValue({\n        data: mockDatabasePasswordEncrypted,\n        encryption: mockDatabaseWithTlsAuthEntity.encryption,\n      })\n      .calledWith(mockDatabaseSentinelMasterPasswordPlain)\n      .mockResolvedValue({\n        data: mockDatabaseSentinelMasterPasswordEncrypted,\n        encryption: mockDatabaseWithTlsAuthEntity.encryption,\n      });\n  });\n\n  describe('onApplicationBootstrap', () => {\n    it('should create stack database when it is not exist', async () => {\n      repository.createQueryBuilder().getOne.mockResolvedValue(null);\n\n      await service.onApplicationBootstrap();\n\n      expect(repository.save).toHaveBeenCalledWith({\n        id: REDIS_STACK_CONFIG.id,\n        name: 'Redis Stack',\n        host: 'localhost',\n        port: 6379,\n        connectionType: ConnectionType.STANDALONE,\n        tls: false,\n        verifyServerCert: false,\n        lastConnection: null,\n      });\n    });\n\n    it('should not fail in case of creation error', async () => {\n      repository.createQueryBuilder().getOne.mockResolvedValue(null);\n      repository.save.mockRejectedValueOnce(new Error());\n\n      await service.onApplicationBootstrap();\n    });\n\n    it('should not save stack database if it is already exists', async () => {\n      await service.onApplicationBootstrap();\n\n      expect(repository.save).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('exists', () => {\n    it('should return true when receive database entity', async () => {\n      expect(await service.exists(mockSessionMetadata)).toEqual(true);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        id: REDIS_STACK_CONFIG.id,\n      });\n    });\n\n    it('should return false when no database received', async () => {\n      repository.createQueryBuilder().getOne.mockResolvedValue(null);\n      expect(await service.exists(mockSessionMetadata)).toEqual(false);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        id: REDIS_STACK_CONFIG.id,\n      });\n    });\n  });\n\n  describe('get', () => {\n    it('should return standalone database model', async () => {\n      const result = await service.get(mockSessionMetadata, mockDatabaseId);\n\n      expect(result).toEqual(mockDatabase);\n      expect(repository.findOne).toHaveBeenCalled();\n      expect(caCertRepository.get).not.toHaveBeenCalled();\n      expect(clientCertRepository.get).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('list', () => {\n    it('should return list of databases with specific fields only', async () => {\n      expect(await service.list(mockSessionMetadata)).toEqual([mockDatabase]);\n      expect(repository.createQueryBuilder().getMany).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('create', () => {\n    it('should create standalone database', async () => {\n      await expect(service.create()).rejects.toThrow(NotImplementedException);\n    });\n  });\n\n  describe('update', () => {\n    it('should update standalone database', async () => {\n      repository.merge.mockReturnValue(mockDatabase);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabase,\n      );\n\n      expect(result).toEqual(mockDatabase);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database/repositories/stack.databases.repository.ts",
    "content": "import {\n  Injectable,\n  Logger,\n  NotImplementedException,\n  OnApplicationBootstrap,\n} from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { merge } from 'lodash';\nimport { Repository } from 'typeorm';\nimport { SessionMetadata } from 'src/common/models';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport {\n  ConnectionType,\n  DatabaseEntity,\n} from 'src/modules/database/entities/database.entity';\nimport { Database } from 'src/modules/database/models/database';\nimport { LocalDatabaseRepository } from 'src/modules/database/repositories/local.database.repository';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';\nimport { TagRepository } from 'src/modules/tag/repository/tag.repository';\nimport config from 'src/utils/config';\n\nconst REDIS_STACK_CONFIG = config.get('redisStack');\n\n@Injectable()\nexport class StackDatabasesRepository\n  extends LocalDatabaseRepository\n  implements OnApplicationBootstrap\n{\n  protected logger = new Logger('StackDatabasesRepository');\n\n  constructor(\n    @InjectRepository(DatabaseEntity)\n    protected readonly repository: Repository<DatabaseEntity>,\n    @InjectRepository(SshOptionsEntity)\n    protected readonly sshOptionsRepository: Repository<SshOptionsEntity>,\n    protected readonly caCertificateRepository: CaCertificateRepository,\n    protected readonly clientCertificateRepository: ClientCertificateRepository,\n    protected readonly encryptionService: EncryptionService,\n    protected readonly constantsProvider: ConstantsProvider,\n    protected readonly tagRepository: TagRepository,\n  ) {\n    super(\n      repository,\n      sshOptionsRepository,\n      caCertificateRepository,\n      clientCertificateRepository,\n      encryptionService,\n      tagRepository,\n    );\n  }\n\n  async onApplicationBootstrap() {\n    const sessionMetadata = this.constantsProvider.getSystemSessionMetadata(); // TODO: [USER_CONTEXT]\n    await this.setPredefinedDatabase(\n      sessionMetadata,\n      merge(\n        {\n          name: 'Redis Stack',\n          host: 'localhost',\n          port: '6379',\n        },\n        REDIS_STACK_CONFIG,\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async exists(sessionMetadata: SessionMetadata): Promise<boolean> {\n    return super.exists(sessionMetadata, REDIS_STACK_CONFIG.id);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    ignoreEncryptionErrors: boolean = false,\n    omitFields: string[] = [],\n  ): Promise<Database> {\n    return super.get(\n      sessionMetadata,\n      REDIS_STACK_CONFIG.id,\n      ignoreEncryptionErrors,\n      omitFields,\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async list(sessionMetadata: SessionMetadata): Promise<Database[]> {\n    return [await this.get(sessionMetadata, REDIS_STACK_CONFIG.id)];\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async create() {\n    return Promise.reject(\n      new NotImplementedException('This functionality is not supported'),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async update(sessionMetadata: SessionMetadata, id: string, data: Database) {\n    return super.update(sessionMetadata, REDIS_STACK_CONFIG.id, data);\n  }\n\n  /**\n   * Create database entity for Stack\n   *\n   * @param options\n   */\n  private async setPredefinedDatabase(\n    sessionMetadata: SessionMetadata,\n    options: { id: string; name: string; host: string; port: string },\n  ): Promise<void> {\n    try {\n      const { id, name, host, port } = options;\n      const isExist = await this.exists(sessionMetadata);\n      if (!isExist) {\n        await super.create(\n          sessionMetadata,\n          {\n            id,\n            host,\n            port: parseInt(port, 10),\n            name,\n            tls: false,\n            verifyServerCert: false,\n            connectionType: ConnectionType.STANDALONE,\n            lastConnection: null,\n          },\n          false,\n        );\n      }\n      this.logger.debug(\n        `Succeed to set predefined database ${id}`,\n        sessionMetadata,\n      );\n    } catch (error) {\n      this.logger.error(\n        'Failed to set predefined database',\n        error,\n        sessionMetadata,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Get,\n  Param,\n  Post,\n  Patch,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiTags } from '@nestjs/swagger';\nimport { DatabaseAnalysisService } from 'src/modules/database-analysis/database-analysis.service';\nimport {\n  DatabaseAnalysis,\n  ShortDatabaseAnalysis,\n} from 'src/modules/database-analysis/models';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport {\n  ApiQueryRedisStringEncoding,\n  ClientMetadataParam,\n} from 'src/common/decorators';\nimport {\n  CreateDatabaseAnalysisDto,\n  RecommendationVoteDto,\n} from 'src/modules/database-analysis/dto';\nimport { ClientMetadata } from 'src/common/models';\n\n@UseInterceptors(BrowserSerializeInterceptor)\n@UsePipes(new ValidationPipe({ transform: true }))\n@ApiTags('Database Analysis')\n@Controller('/analysis')\nexport class DatabaseAnalysisController {\n  constructor(private readonly service: DatabaseAnalysisService) {}\n\n  @ApiEndpoint({\n    statusCode: 201,\n    description: 'Create new database analysis',\n    responses: [\n      {\n        status: 201,\n        type: DatabaseAnalysis,\n      },\n    ],\n  })\n  @Post()\n  @ApiQueryRedisStringEncoding()\n  async create(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Body() dto: CreateDatabaseAnalysisDto,\n  ): Promise<DatabaseAnalysis> {\n    return this.service.create(clientMetadata, dto);\n  }\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get database analysis',\n    responses: [\n      {\n        status: 200,\n        type: DatabaseAnalysis,\n      },\n    ],\n  })\n  @Get(':id')\n  @ApiQueryRedisStringEncoding()\n  async get(@Param('id') id: string): Promise<DatabaseAnalysis> {\n    return this.service.get(id);\n  }\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get database analysis',\n    responses: [\n      {\n        status: 200,\n        type: DatabaseAnalysis,\n      },\n    ],\n  })\n  @Get('')\n  @ApiQueryRedisStringEncoding()\n  async list(\n    @Param('dbInstance') databaseId: string,\n  ): Promise<ShortDatabaseAnalysis[]> {\n    return this.service.list(databaseId);\n  }\n\n  @Patch(':id')\n  @ApiEndpoint({\n    description: 'Update database instance by id',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: \"Updated database instance' response\",\n        type: DatabaseAnalysis,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  async modify(\n    @Param('id') id: string,\n    @Body() dto: RecommendationVoteDto,\n  ): Promise<DatabaseAnalysis> {\n    return await this.service.vote(id, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/database-analysis.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { DatabaseAnalysisController } from 'src/modules/database-analysis/database-analysis.controller';\nimport { DatabaseAnalysisService } from 'src/modules/database-analysis/database-analysis.service';\nimport { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer';\nimport { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';\nimport { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';\nimport { KeyInfoProvider } from 'src/modules/database-analysis/scanner/key-info/key-info.provider';\nimport { RecommendationModule } from 'src/modules/recommendation/recommendation.module';\n\n@Module({\n  imports: [RecommendationModule],\n  controllers: [DatabaseAnalysisController],\n  providers: [\n    DatabaseAnalysisService,\n    DatabaseAnalyzer,\n    DatabaseAnalysisProvider,\n    KeysScanner,\n    KeyInfoProvider,\n  ],\n})\nexport class DatabaseAnalysisModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/database-analysis.service.ts",
    "content": "import { HttpException, Injectable, Logger } from '@nestjs/common';\nimport { isNull, flatten, concat } from 'lodash';\nimport { RecommendationService } from 'src/modules/recommendation/recommendation.service';\nimport { catchAclError } from 'src/utils';\nimport { ONE_NODE_RECOMMENDATIONS } from 'src/constants';\nimport { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  DatabaseAnalysis,\n  ShortDatabaseAnalysis,\n} from 'src/modules/database-analysis/models';\nimport { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';\nimport {\n  CreateDatabaseAnalysisDto,\n  RecommendationVoteDto,\n} from 'src/modules/database-analysis/dto';\nimport { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\n\n@Injectable()\nexport class DatabaseAnalysisService {\n  private logger = new Logger('DatabaseAnalysisService');\n\n  constructor(\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private readonly recommendationService: RecommendationService,\n    private readonly analyzer: DatabaseAnalyzer,\n    private readonly databaseAnalysisProvider: DatabaseAnalysisProvider,\n    private readonly scanner: KeysScanner,\n    private databaseRecommendationService: DatabaseRecommendationService,\n  ) {}\n\n  /**\n   * Get cluster details and details for all nodes\n   * @param clientMetadata\n   * @param dto\n   */\n  public async create(\n    clientMetadata: ClientMetadata,\n    dto: CreateDatabaseAnalysisDto,\n  ): Promise<DatabaseAnalysis> {\n    let client: RedisClient;\n\n    try {\n      client = await this.databaseClientFactory.createClient(clientMetadata);\n\n      const scanResults = await this.scanner.scan(client, {\n        filter: dto.filter,\n      });\n\n      const progress = {\n        total: 0,\n        scanned: 0,\n        processed: 0,\n      };\n\n      scanResults.forEach((nodeResult) => {\n        progress.scanned += nodeResult.progress.scanned;\n        progress.processed += nodeResult.progress.processed;\n        progress.total += nodeResult.progress.total;\n      });\n\n      let recommendationToExclude = [];\n\n      const recommendations = await scanResults.reduce(\n        async (previousPromise, nodeResult, idx) => {\n          const jobsArray = await previousPromise;\n          const nodeRecommendations =\n            await this.recommendationService.getRecommendations({\n              client: nodeResult.client,\n              keys: nodeResult.keys,\n              indexes: nodeResult.indexes,\n              libraries: nodeResult.libraries,\n              total: progress.total,\n              globalClient: client,\n              exclude: recommendationToExclude,\n            });\n          if (idx === 0) {\n            recommendationToExclude = concat(\n              recommendationToExclude,\n              ONE_NODE_RECOMMENDATIONS,\n            );\n          }\n          const foundedRecommendations = nodeRecommendations.filter(\n            (recommendation) => !isNull(recommendation),\n          );\n          const foundedRecommendationNames = foundedRecommendations.map(\n            ({ name }) => name,\n          );\n          recommendationToExclude = concat(\n            recommendationToExclude,\n            foundedRecommendationNames,\n          );\n          recommendationToExclude.push(...foundedRecommendationNames);\n          jobsArray.push(foundedRecommendations);\n          return flatten(jobsArray);\n        },\n        Promise.resolve([]),\n      );\n      const analysis = plainToInstance(\n        DatabaseAnalysis,\n        await this.analyzer.analyze(\n          {\n            databaseId: clientMetadata.databaseId,\n            db: await client?.getCurrentDbIndex(),\n            ...dto,\n            progress,\n            recommendations,\n          },\n          [].concat(...scanResults.map((nodeResult) => nodeResult.keys)),\n        ),\n      );\n\n      client.disconnect();\n      this.databaseRecommendationService.sync(clientMetadata, recommendations);\n      return this.databaseAnalysisProvider.create(analysis);\n    } catch (e) {\n      client?.disconnect();\n      this.logger.error('Unable to analyze database', e, clientMetadata);\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Get analysis with all fields by id\n   * @param id\n   */\n  async get(id: string): Promise<DatabaseAnalysis> {\n    return this.databaseAnalysisProvider.get(id);\n  }\n\n  /**\n   * Get analysis list for particular database with id and createdAt fields only\n   * @param databaseId\n   */\n  async list(databaseId: string): Promise<ShortDatabaseAnalysis[]> {\n    return this.databaseAnalysisProvider.list(databaseId);\n  }\n\n  /**\n   * Set user vote for recommendation\n   * @param id\n   * @param recommendation\n   */\n  async vote(\n    id: string,\n    recommendation: RecommendationVoteDto,\n  ): Promise<DatabaseAnalysis> {\n    return this.databaseAnalysisProvider.recommendationVote(id, recommendation);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/dto/create-database-analysis.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsOptional, IsString } from 'class-validator';\nimport { ScanFilter } from 'src/modules/database-analysis/models/scan-filter';\nimport { Type } from 'class-transformer';\n\nexport class CreateDatabaseAnalysisDto {\n  @ApiProperty({\n    description: 'Namespace delimiter',\n    type: String,\n    default: ':',\n  })\n  @IsOptional()\n  @IsString()\n  delimiter: string = ':';\n\n  @ApiProperty({\n    description: 'Filters for scan operation',\n    type: () => ScanFilter,\n    default: new ScanFilter(),\n  })\n  @IsOptional()\n  @Type(() => ScanFilter)\n  filter: ScanFilter = new ScanFilter();\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/dto/index.ts",
    "content": "export * from './create-database-analysis.dto';\nexport * from './recommendation-vote.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/dto/recommendation-vote.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class RecommendationVoteDto {\n  @ApiProperty({\n    description: 'Recommendation name',\n    type: String,\n  })\n  @IsString()\n  name: string;\n\n  @ApiProperty({\n    description: 'User vote',\n    type: String,\n  })\n  @IsString()\n  vote: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  Index,\n} from 'typeorm';\nimport { IsInt, Min } from 'class-validator';\nimport { Expose, Transform } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Entity('database_analysis')\nexport class DatabaseAnalysisEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Index()\n  @Expose()\n  databaseId: string;\n\n  @ManyToOne(() => DatabaseEntity, {\n    nullable: false,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn({ name: 'databaseId' })\n  database: DatabaseEntity;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  filter: string;\n\n  @Column({ nullable: false })\n  @Expose()\n  delimiter: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  progress: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  totalKeys: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  totalMemory: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @Transform(({ value }) => JSON.stringify(value), { toClassOnly: true })\n  @Transform(\n    ({ value: str }) => {\n      try {\n        return JSON.parse(str).map((value) => ({\n          ...value,\n          nsp: Buffer.from(value.nsp),\n        }));\n      } catch (e) {\n        return undefined;\n      }\n    },\n    { toPlainOnly: true },\n  )\n  @Expose()\n  topKeysNsp: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @Transform(({ value }) => JSON.stringify(value), { toClassOnly: true })\n  @Transform(\n    ({ value: str }) => {\n      try {\n        return JSON.parse(str).map((value) => ({\n          ...value,\n          nsp: Buffer.from(value.nsp),\n        }));\n      } catch (e) {\n        return undefined;\n      }\n    },\n    { toPlainOnly: true },\n  )\n  @Expose()\n  topMemoryNsp: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @Transform(({ value }) => JSON.stringify(value), { toClassOnly: true })\n  @Transform(\n    ({ value: str }) => {\n      try {\n        return JSON.parse(str).map((value) => ({\n          ...value,\n          name: Buffer.from(value.name),\n        }));\n      } catch (e) {\n        return undefined;\n      }\n    },\n    { toPlainOnly: true },\n  )\n  @Expose()\n  topKeysLength: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @Transform(({ value }) => JSON.stringify(value), { toClassOnly: true })\n  @Transform(\n    ({ value: str }) => {\n      try {\n        return JSON.parse(str).map((value) => ({\n          ...value,\n          name: Buffer.from(value.name),\n        }));\n      } catch (e) {\n        return undefined;\n      }\n    },\n    { toPlainOnly: true },\n  )\n  @Expose()\n  topKeysMemory: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  expirationGroups: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  recommendations: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  @IsInt()\n  @Min(0)\n  db?: number;\n\n  @CreateDateColumn()\n  @Index()\n  @Expose()\n  createdAt: Date;\n\n  constructor(entity: Partial<DatabaseAnalysisEntity>) {\n    Object.assign(this, entity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/analysis-progress.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class AnalysisProgress {\n  @ApiProperty({\n    description: 'Total keys in the database',\n    type: Number,\n    example: 10_000_000,\n  })\n  @Expose()\n  total: number = 0;\n\n  @ApiProperty({\n    description: 'Total keys scanned for entire database',\n    type: Number,\n    example: 30_000,\n  })\n  @Expose()\n  scanned: number = 0;\n\n  @ApiProperty({\n    description:\n      'Total keys processed for entire database. (Filtered keys returned by scan command)',\n    type: Number,\n    example: 5_000,\n  })\n  @Expose()\n  processed: number = 0;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/database-analysis.ts",
    "content": "import { NspSummary } from 'src/modules/database-analysis/models/nsp-summary';\nimport { Key } from 'src/modules/database-analysis/models/key';\nimport { IsInt, IsOptional, Min } from 'class-validator';\nimport { Expose, Type } from 'class-transformer';\nimport { SimpleSummary } from 'src/modules/database-analysis/models/simple-summary';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { ScanFilter } from 'src/modules/database-analysis/models/scan-filter';\nimport { AnalysisProgress } from 'src/modules/database-analysis/models/analysis-progress';\nimport { SumGroup } from 'src/modules/database-analysis/models/sum-group';\nimport { Recommendation } from 'src/modules/database-analysis/models/recommendation';\n\nexport class DatabaseAnalysis {\n  @ApiProperty({\n    description: 'Analysis id',\n    type: String,\n    default: '76dd5654-814b-4e49-9c72-b236f50891f4',\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: String,\n    default: '76dd5654-814b-4e49-9c72-b236f50891f4',\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiProperty({\n    description: 'Filters for scan operation',\n    type: () => ScanFilter,\n  })\n  @Expose()\n  @Type(() => ScanFilter)\n  filter: ScanFilter;\n\n  @ApiProperty({\n    description: 'Namespace delimiter',\n    type: String,\n    default: ':',\n  })\n  @Expose()\n  delimiter: string;\n\n  @ApiProperty({\n    description: 'Analysis progress',\n    type: () => AnalysisProgress,\n  })\n  @Expose()\n  @Type(() => AnalysisProgress)\n  progress: AnalysisProgress;\n\n  @ApiProperty({\n    description: 'Analysis created date (ISO string)',\n    type: Date,\n    default: '2022-09-16T06:29:20.000Z',\n  })\n  @Expose()\n  createdAt: Date;\n\n  @ApiProperty({\n    description: 'Total keys with details by types',\n    type: () => SimpleSummary,\n  })\n  @Expose()\n  @Type(() => SimpleSummary)\n  totalKeys: SimpleSummary;\n\n  @ApiProperty({\n    description: 'Total memory with details by types',\n    type: () => SimpleSummary,\n  })\n  @Expose()\n  @Type(() => SimpleSummary)\n  totalMemory: SimpleSummary;\n\n  @ApiProperty({\n    description: 'Top namespaces by keys number',\n    type: () => NspSummary,\n  })\n  @Expose()\n  @Type(() => NspSummary)\n  topKeysNsp: NspSummary[];\n\n  @ApiProperty({\n    description: 'Top namespaces by memory',\n    type: () => NspSummary,\n  })\n  @Expose()\n  @Type(() => NspSummary)\n  topMemoryNsp: NspSummary[];\n\n  @ApiProperty({\n    description:\n      'Top keys by key length (string length, list elements count, etc.)',\n    isArray: true,\n    type: () => Key,\n  })\n  @Expose()\n  @Type(() => Key)\n  topKeysLength: Key[];\n\n  @ApiProperty({\n    description: 'Top keys by memory used',\n    isArray: true,\n    type: () => Key,\n  })\n  @Expose()\n  @Type(() => Key)\n  topKeysMemory: Key[];\n\n  @ApiProperty({\n    description: 'Expiration groups',\n    isArray: true,\n    type: () => SumGroup,\n  })\n  @Expose()\n  @Type(() => SumGroup)\n  expirationGroups: SumGroup[];\n\n  @ApiProperty({\n    description: 'Recommendations',\n    isArray: true,\n    type: () => Recommendation,\n  })\n  @Expose()\n  @Type(() => Recommendation)\n  recommendations: Recommendation[];\n\n  @ApiPropertyOptional({\n    description: 'Logical database number.',\n    type: Number,\n  })\n  @Expose()\n  @IsInt()\n  @Min(0)\n  @IsOptional()\n  db?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/index.ts",
    "content": "export * from './key';\nexport * from './nsp-type-summary';\nexport * from './nsp-summary';\nexport * from './simple-type-summary';\nexport * from './simple-summary';\nexport * from './database-analysis';\nexport * from './short-database-analysis';\nexport * from './sum-group';\nexport * from './recommendation';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/key.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class Key {\n  @ApiProperty({\n    description: 'Key name',\n    type: String,\n    example: 'key1',\n  })\n  @IsRedisString()\n  @Expose()\n  @RedisStringType()\n  name: RedisString;\n\n  @ApiProperty({\n    description: 'Key type',\n    type: String,\n    example: 'list',\n  })\n  @Expose()\n  type: string;\n\n  @ApiProperty({\n    description: 'Memory used by key in bytes',\n    type: Number,\n    example: 1_000,\n  })\n  @Expose()\n  memory: number;\n\n  @ApiProperty({\n    description: 'Number of characters, elements, etc. based on type',\n    type: Number,\n    example: 100,\n  })\n  @Expose()\n  length: number;\n\n  @ApiProperty({\n    description: 'Key ttl',\n    type: Number,\n    example: -1,\n  })\n  @Expose()\n  ttl: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/nsp-summary.ts",
    "content": "import { NspTypeSummary } from 'src/modules/database-analysis/models/nsp-type-summary';\nimport { RedisString } from 'src/common/constants';\nimport { Expose, Type } from 'class-transformer';\nimport { RedisStringType } from 'src/common/decorators';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class NspSummary {\n  @ApiProperty({\n    description: 'Namespace',\n    type: String,\n    example: 'device',\n  })\n  @RedisStringType()\n  @Expose()\n  nsp: RedisString;\n\n  @ApiProperty({\n    description: 'Total memory used by namespace in bytes',\n    type: Number,\n    example: 10_000_000,\n  })\n  @Expose()\n  memory: number;\n\n  @ApiProperty({\n    description: 'Total keys inside namespace',\n    type: Number,\n    example: 10_000,\n  })\n  @Expose()\n  keys: number;\n\n  @ApiProperty({\n    description: 'Top namespaces by keys number',\n    isArray: true,\n    type: () => NspTypeSummary,\n  })\n  @Expose()\n  @Type(() => NspTypeSummary)\n  types: NspTypeSummary[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/nsp-type-summary.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class NspTypeSummary {\n  @ApiProperty({\n    description: 'Type name',\n    type: String,\n    example: 'hash',\n  })\n  @Expose()\n  type: string;\n\n  @ApiProperty({\n    description: 'Total memory in bytes inside particular data type',\n    type: Number,\n    example: 10_000,\n  })\n  @Expose()\n  memory: number;\n\n  @ApiProperty({\n    description: 'Total keys inside particular data type',\n    type: Number,\n    example: 10_000,\n  })\n  @Expose()\n  keys: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/recommendation.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nexport class Recommendation {\n  @ApiProperty({\n    description: 'Recommendation name',\n    type: String,\n    example: 'luaScript',\n  })\n  @Expose()\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'Additional recommendation params',\n    example: 'luaScript',\n  })\n  @Expose()\n  params?: any;\n\n  @ApiPropertyOptional({\n    description: 'User vote',\n    example: 'useful',\n  })\n  @Expose()\n  vote?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/scan-filter.ts",
    "content": "import { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { IsEnum, IsInt, IsOptional, IsString } from 'class-validator';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class ScanFilter {\n  @ApiProperty({\n    description: 'Key type',\n    type: String,\n    example: 'list',\n  })\n  @IsOptional()\n  @Expose()\n  @IsEnum(RedisDataType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      RedisDataType,\n    )}.`,\n  })\n  type?: RedisDataType = null;\n\n  @ApiProperty({\n    description: 'Match glob patterns',\n    type: String,\n    example: 'device:*',\n    default: '*',\n  })\n  @IsOptional()\n  @IsString()\n  @Expose()\n  match?: string = '*';\n\n  @ApiProperty({\n    description: '\"count\" argument for \"scan\" command per node',\n    type: Number,\n    example: 10_000,\n    default: 10_000,\n  })\n  @IsOptional()\n  @IsInt()\n  @Expose()\n  count?: number = 10_000;\n\n  /**\n   * Generate scan args array for filter\n   */\n  getScanArgsArray(): Array<number | string> {\n    const args = ['match', this.match];\n\n    if (this.type) {\n      args.push('type', this.type);\n    }\n\n    return args;\n  }\n\n  getCount(): number {\n    return this.count;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/short-database-analysis.ts",
    "content": "import { PartialType, PickType } from '@nestjs/swagger';\nimport { DatabaseAnalysis } from 'src/modules/database-analysis/models/database-analysis';\n\nexport class ShortDatabaseAnalysis extends PartialType(\n  PickType(DatabaseAnalysis, ['id', 'createdAt', 'db'] as const),\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/simple-summary.ts",
    "content": "import { SimpleTypeSummary } from 'src/modules/database-analysis/models/simple-type-summary';\nimport { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class SimpleSummary {\n  @ApiProperty({\n    description: 'Total number',\n    type: Number,\n    example: 10000,\n  })\n  @Expose()\n  total: number;\n\n  @ApiProperty({\n    description: 'Array with totals by type',\n    isArray: true,\n    type: () => SimpleTypeSummary,\n  })\n  @Expose()\n  types: SimpleTypeSummary[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/simple-type-summary.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class SimpleTypeSummary {\n  @ApiProperty({\n    description: 'Type name',\n    type: String,\n    example: 'string',\n  })\n  @Expose()\n  type: string;\n\n  @ApiProperty({\n    description: 'Total inside this type of data',\n    type: Number,\n    example: 10000,\n  })\n  @Expose()\n  total: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/models/sum-group.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class SumGroup {\n  @ApiProperty({\n    description: 'Group Label',\n    type: String,\n    example: '1-4 Hrs',\n  })\n  @Expose()\n  label: string;\n\n  @ApiProperty({\n    description: 'Sum of data (e.g. memory, or number of keys)',\n    type: Number,\n    example: 10000,\n  })\n  @Expose()\n  total: number;\n\n  @ApiProperty({\n    description:\n      'Group threshold during analyzing (all values less then (<) threshold)',\n    type: Number,\n    example: -1,\n  })\n  @Expose()\n  threshold: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { v4 as uuidv4 } from 'uuid';\nimport {\n  mockEncryptionService,\n  mockEncryptResult,\n  mockQueryBuilderGetMany,\n  mockQueryBuilderGetManyRaw,\n  mockRepository,\n  mockDatabase,\n  MockType,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { Repository } from 'typeorm';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';\nimport { DatabaseAnalysis } from 'src/modules/database-analysis/models';\nimport {\n  CreateDatabaseAnalysisDto,\n  RecommendationVoteDto,\n} from 'src/modules/database-analysis/dto';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { plainToInstance } from 'class-transformer';\nimport { ScanFilter } from 'src/modules/database-analysis/models/scan-filter';\nimport { DatabaseAnalysisEntity } from 'src/modules/database-analysis/entities/database-analysis.entity';\nimport { NotFoundException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';\n\nexport const mockCreateDatabaseAnalysisDto: CreateDatabaseAnalysisDto = {\n  delimiter: ':',\n  filter: plainToInstance(ScanFilter, {\n    type: RedisDataType.String,\n    match: 'key*',\n    count: 15,\n  }),\n};\n\nconst mockDatabaseAnalysisEntity = new DatabaseAnalysisEntity({\n  id: uuidv4(),\n  databaseId: mockDatabase.id,\n  delimiter: mockCreateDatabaseAnalysisDto.delimiter,\n  filter: 'ENCRYPTED:filter',\n  totalKeys: 'ENCRYPTED:totalKeys',\n  totalMemory: 'ENCRYPTED:totalMemory',\n  topKeysNsp: 'ENCRYPTED:topKeysNsp',\n  topMemoryNsp: 'ENCRYPTED:topMemoryNsp',\n  topKeysLength: 'ENCRYPTED:topKeysLength',\n  topKeysMemory: 'ENCRYPTED:topKeysMemory',\n  expirationGroups: 'ENCRYPTED:expirationGroups',\n  recommendations: 'ENCRYPTED:recommendations',\n  encryption: 'KEYTAR',\n  createdAt: new Date(),\n});\n\nconst mockDatabaseAnalysisPartial: Partial<DatabaseAnalysis> = {\n  ...mockCreateDatabaseAnalysisDto,\n  databaseId: mockDatabase.id,\n};\n\nconst mockDatabaseAnalysis = {\n  ...mockDatabaseAnalysisPartial,\n  id: mockDatabaseAnalysisEntity.id,\n  createdAt: mockDatabaseAnalysisEntity.createdAt,\n  totalKeys: {\n    total: 1,\n    types: [\n      {\n        type: 'string',\n        total: 1,\n      },\n    ],\n  },\n  totalMemory: {\n    total: 10,\n    types: [\n      {\n        type: 'set',\n        total: 10,\n      },\n    ],\n  },\n  topKeysNsp: [\n    {\n      nsp: Buffer.from('nsp1'),\n      keys: 1,\n      memory: 10,\n      types: [\n        {\n          type: 'string',\n          keys: 1,\n          memory: 10,\n        },\n      ],\n    },\n  ],\n  topMemoryNsp: [\n    {\n      nsp: Buffer.from('nsp1'),\n      keys: 1,\n      memory: 10,\n      types: [\n        {\n          type: 'string',\n          keys: 1,\n          memory: 10,\n        },\n      ],\n    },\n  ],\n  topKeysLength: [\n    {\n      name: Buffer.from('nsp1:key1'),\n      type: 'string',\n      memory: 10,\n      length: 1,\n      ttl: -1,\n    },\n  ],\n  topKeysMemory: [\n    {\n      name: Buffer.from('nsp1:key1'),\n      type: 'string',\n      memory: 10,\n      length: 1,\n      ttl: -1,\n    },\n  ],\n  expirationGroups: [\n    {\n      label: 'No Expire',\n      threshold: 0,\n      total: 200000,\n    },\n    {\n      label: '<1 hr',\n      threshold: 3600,\n      total: 0,\n    },\n    {\n      label: '1-4 Hrs',\n      threshold: 14400,\n      total: 0,\n    },\n    {\n      label: '4-12 Hrs',\n      threshold: 43200,\n      total: 0,\n    },\n    {\n      label: '12-24 Hrs',\n      threshold: 86400,\n      total: 0,\n    },\n    {\n      label: '1-7 Days',\n      threshold: 604800,\n      total: 0,\n    },\n    {\n      label: '>7 Days',\n      threshold: 2592000,\n      total: 0,\n    },\n    {\n      label: '>1 Month',\n      threshold: 9007199254740991,\n      total: 0,\n    },\n  ],\n  recommendations: [{ name: 'luaScript' }],\n} as DatabaseAnalysis;\n\nconst mockDatabaseAnalysisWithVote = {\n  ...mockDatabaseAnalysis,\n  recommendations: [{ name: 'luaScript', vote: 'useful' }],\n} as DatabaseAnalysis;\n\nconst mockRecommendationVoteDto: RecommendationVoteDto = {\n  name: 'luaScript',\n  vote: 'useful',\n};\n\ndescribe('DatabaseAnalysisProvider', () => {\n  let service: DatabaseAnalysisProvider;\n  let repository: MockType<Repository<DatabaseAnalysis>>;\n  let encryptionService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseAnalysisProvider,\n        {\n          provide: getRepositoryToken(DatabaseAnalysisEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<DatabaseAnalysisProvider>(DatabaseAnalysisProvider);\n    repository = module.get(getRepositoryToken(DatabaseAnalysisEntity));\n    encryptionService = module.get<EncryptionService>(EncryptionService);\n\n    // encryption mocks\n    [\n      'filter',\n      'totalKeys',\n      'totalMemory',\n      'topKeysNsp',\n      'topMemoryNsp',\n      'topKeysLength',\n      'topKeysMemory',\n      'expirationGroups',\n      'recommendations',\n    ].forEach((field) => {\n      when(encryptionService.encrypt)\n        .calledWith(JSON.stringify(mockDatabaseAnalysis[field]))\n        .mockReturnValue({\n          ...mockEncryptResult,\n          data: mockDatabaseAnalysisEntity[field],\n        });\n      when(encryptionService.decrypt)\n        .calledWith(\n          mockDatabaseAnalysisEntity[field],\n          mockEncryptResult.encryption,\n        )\n        .mockReturnValue(JSON.stringify(mockDatabaseAnalysis[field]));\n    });\n  });\n\n  describe('create', () => {\n    it('should process new entity', async () => {\n      repository.save.mockReturnValueOnce(mockDatabaseAnalysisEntity);\n      expect(await service.create(mockDatabaseAnalysisPartial)).toEqual(\n        mockDatabaseAnalysis,\n      );\n    });\n  });\n\n  describe('get', () => {\n    it('should get analysis', async () => {\n      repository.findOneBy.mockReturnValueOnce(mockDatabaseAnalysisEntity);\n\n      expect(await service.get(mockDatabaseAnalysis.id)).toEqual(\n        mockDatabaseAnalysis,\n      );\n    });\n    it('should return null fields in case of decryption errors', async () => {\n      when(encryptionService.decrypt)\n        .calledWith(\n          mockDatabaseAnalysisEntity['filter'],\n          mockEncryptResult.encryption,\n        )\n        .mockRejectedValueOnce(new KeytarDecryptionErrorException());\n      repository.findOneBy.mockReturnValueOnce(mockDatabaseAnalysisEntity);\n\n      expect(await service.get(mockDatabaseAnalysis.id)).toEqual({\n        ...mockDatabaseAnalysis,\n        filter: null,\n      });\n    });\n    it('should throw an error', async () => {\n      repository.findOneBy.mockReturnValueOnce(null);\n\n      try {\n        await service.get(mockDatabaseAnalysis.id);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);\n      }\n    });\n  });\n\n  describe('list', () => {\n    it('should get list of analysis', async () => {\n      mockQueryBuilderGetMany.mockReturnValueOnce([\n        {\n          id: mockDatabaseAnalysis.id,\n          createdAt: mockDatabaseAnalysis.createdAt,\n          notExposed: 'field',\n        },\n      ]);\n      expect(await service.list(mockDatabaseAnalysis.databaseId)).toEqual([\n        {\n          id: mockDatabaseAnalysis.id,\n          createdAt: mockDatabaseAnalysis.createdAt,\n        },\n      ]);\n    });\n  });\n\n  describe('cleanupDatabaseHistory', () => {\n    it('Should not return anything on cleanup', async () => {\n      mockQueryBuilderGetManyRaw.mockReturnValueOnce([\n        { id: mockDatabaseAnalysisEntity.id },\n        { id: mockDatabaseAnalysisEntity.id },\n      ]);\n\n      expect(await service.cleanupDatabaseHistory(mockDatabase.id)).toEqual(\n        undefined,\n      );\n    });\n  });\n\n  describe('recommendationVote', () => {\n    it('should return updated database analysis', async () => {\n      repository.findOneBy.mockReturnValueOnce(mockDatabaseAnalysisEntity);\n      repository.update.mockReturnValueOnce(true);\n      await encryptionService.encrypt.mockReturnValue(mockEncryptResult);\n\n      expect(\n        await service.recommendationVote(\n          mockDatabaseAnalysis.id,\n          mockRecommendationVoteDto,\n        ),\n      ).toEqual(mockDatabaseAnalysisWithVote);\n    });\n\n    it('should throw an error', async () => {\n      repository.findOneBy.mockReturnValueOnce(null);\n\n      try {\n        await service.recommendationVote(\n          mockDatabaseAnalysis.id,\n          mockRecommendationVoteDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts",
    "content": "import { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { DatabaseAnalysisEntity } from 'src/modules/database-analysis/entities/database-analysis.entity';\nimport { isUndefined } from 'lodash';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  DatabaseAnalysis,\n  ShortDatabaseAnalysis,\n} from 'src/modules/database-analysis/models';\nimport { RecommendationVoteDto } from 'src/modules/database-analysis/dto';\nimport { classToClass } from 'src/utils';\nimport config from 'src/utils/config';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nconst DATABASE_ANALYSIS_CONFIG = config.get('database_analysis');\n\n@Injectable()\nexport class DatabaseAnalysisProvider {\n  private readonly logger = new Logger('DatabaseAnalysisProvider');\n\n  private readonly encryptedFields = [\n    'totalKeys',\n    'totalMemory',\n    'topKeysNsp',\n    'topMemoryNsp',\n    'topKeysLength',\n    'topKeysMemory',\n    'filter',\n    'progress',\n    'expirationGroups',\n    'recommendations',\n  ];\n\n  constructor(\n    @InjectRepository(DatabaseAnalysisEntity)\n    private readonly repository: Repository<DatabaseAnalysisEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {}\n\n  /**\n   * Encrypt database analysis and save entire entity\n   * Should always throw and error in case when unable to encrypt for some reason\n   * @param analysis\n   */\n  async create(analysis: Partial<DatabaseAnalysis>): Promise<DatabaseAnalysis> {\n    const entity = await this.repository.save(\n      await this.encryptEntity(\n        plainToInstance(DatabaseAnalysisEntity, analysis),\n      ),\n    );\n\n    // cleanup history and ignore error if any\n    try {\n      await this.cleanupDatabaseHistory(entity.databaseId);\n    } catch (e) {\n      this.logger.error('Error when trying to cleanup history after insert', e);\n    }\n\n    return classToClass(DatabaseAnalysis, await this.decryptEntity(entity));\n  }\n\n  /**\n   * Fetches entity, decrypt and return full DatabaseAnalysis model\n   * @param id\n   */\n  async get(id: string): Promise<DatabaseAnalysis> {\n    const entity = await this.repository.findOneBy({ id });\n\n    if (!entity) {\n      this.logger.error(`Database analysis with id:${id} was not Found`);\n      throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);\n    }\n\n    return classToClass(\n      DatabaseAnalysis,\n      await this.decryptEntity(entity, true),\n    );\n  }\n\n  /**\n   * Fetches entity, decrypt, update and return updated DatabaseAnalysis model\n   * @param id\n   * @param dto\n   */\n  async recommendationVote(\n    id: string,\n    dto: RecommendationVoteDto,\n  ): Promise<DatabaseAnalysis> {\n    this.logger.debug('Updating database analysis with recommendation vote');\n    const { name, vote } = dto;\n    const oldDatabaseAnalysis = await this.repository.findOneBy({ id });\n\n    if (!oldDatabaseAnalysis) {\n      this.logger.error(`Database analysis with id:${id} was not Found`);\n      throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);\n    }\n\n    const entity = classToClass(\n      DatabaseAnalysis,\n      await this.decryptEntity(oldDatabaseAnalysis, true),\n    );\n\n    entity.recommendations = entity.recommendations.map((recommendation) =>\n      recommendation.name === name\n        ? { ...recommendation, vote }\n        : recommendation,\n    );\n\n    await this.repository.update(\n      id,\n      await this.encryptEntity(plainToInstance(DatabaseAnalysisEntity, entity)),\n    );\n\n    return entity;\n  }\n\n  /**\n   * Return list of database analysis with several fields only\n   * @param databaseId\n   */\n  async list(databaseId: string): Promise<ShortDatabaseAnalysis[]> {\n    this.logger.debug('Getting database analysis list');\n    const entities = await this.repository\n      .createQueryBuilder('a')\n      .where({ databaseId })\n      .select(['a.id', 'a.createdAt', 'a.db'])\n      .orderBy('a.createdAt', 'DESC')\n      .limit(DATABASE_ANALYSIS_CONFIG.maxItemsPerDb)\n      .getMany();\n\n    this.logger.debug('Succeed to get command executions');\n\n    return entities.map((entity) =>\n      classToClass(ShortDatabaseAnalysis, entity),\n    );\n  }\n\n  /**\n   * Clean history for particular database to fit 30 items limitation\n   * @param databaseId\n   */\n  async cleanupDatabaseHistory(databaseId: string): Promise<void> {\n    // todo: investigate why delete with sub-query doesn't works\n    const idsToDelete = (\n      await this.repository\n        .createQueryBuilder()\n        .where({ databaseId })\n        .select('id')\n        .orderBy('createdAt', 'DESC')\n        .offset(DATABASE_ANALYSIS_CONFIG.maxItemsPerDb)\n        .getRawMany()\n    ).map((item) => item.id);\n\n    await this.repository\n      .createQueryBuilder()\n      .delete()\n      .whereInIds(idsToDelete)\n      .execute();\n  }\n\n  /**\n   * Encrypt required database analysis fields based on picked encryption strategy\n   * Should always throw an encryption error to determine that something wrong\n   * with encryption strategy\n   *\n   * @param entity\n   * @private\n   */\n  private async encryptEntity(\n    entity: DatabaseAnalysisEntity,\n  ): Promise<DatabaseAnalysisEntity> {\n    const encryptedEntity = {\n      ...entity,\n    };\n\n    await Promise.all(\n      this.encryptedFields.map(async (field) => {\n        if (entity[field]) {\n          const { data, encryption } = await this.encryptionService.encrypt(\n            entity[field],\n          );\n          encryptedEntity[field] = data;\n          encryptedEntity['encryption'] = encryption;\n        }\n      }),\n    );\n\n    return encryptedEntity;\n  }\n\n  /**\n   * Decrypt required database analysis fields\n   * This method should optionally not fail (to not block users to navigate across app\n   * on decryption error, for example, to be able change encryption strategy in the future)\n   *\n   * When ignoreErrors = true will return null for failed fields.\n   * It will cause 401 Unauthorized errors when user tries to connect to redis database\n   *\n   * @param entity\n   * @param ignoreErrors\n   * @private\n   */\n  private async decryptEntity(\n    entity: DatabaseAnalysisEntity,\n    ignoreErrors: boolean = false,\n  ): Promise<DatabaseAnalysisEntity> {\n    const decrypted = {\n      ...entity,\n    };\n\n    await Promise.all(\n      this.encryptedFields.map(async (field) => {\n        decrypted[field] = await this.decryptField(entity, field, ignoreErrors);\n      }),\n    );\n\n    return new DatabaseAnalysisEntity({\n      ...decrypted,\n    });\n  }\n\n  /**\n   * Decrypt single field if exists\n   *\n   * @param entity\n   * @param field\n   * @param ignoreErrors\n   * @private\n   */\n  private async decryptField(\n    entity: DatabaseAnalysisEntity,\n    field: string,\n    ignoreErrors: boolean,\n  ): Promise<string> {\n    if (isUndefined(entity[field])) {\n      return undefined;\n    }\n\n    try {\n      return await this.encryptionService.decrypt(\n        entity[field],\n        entity.encryption,\n      );\n    } catch (error) {\n      this.logger.error(\n        `Unable to decrypt database analysis ${entity.id} fields: ${field}`,\n        error,\n      );\n      if (!ignoreErrors) {\n        throw error;\n      }\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/providers/database-analyzer.spec.ts",
    "content": "import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer';\nimport { Key } from 'src/modules/database-analysis/models';\n\nconst keysCount = 100;\nlet genericTotal = 0;\nconst numberOfNsp = 20;\n\nexport const getGenericTotal = (start: number, end: number) => {\n  let total = 0;\n  for (let i = start; i <= end; i += 1) {\n    total += i;\n  }\n  return total;\n};\n\nexport const calculateTypeIdx = (idx: number, perGroup = 25) =>\n  Math.trunc(idx / perGroup) + (idx % perGroup === 0 ? 0 : 1);\n\nconst shuffleKeys = (keys) => [...keys].sort(() => Math.random() - 0.5);\n\n// 100 keys:\n// {\n//   name: 'nsp_[1-20]:key_[1-100]',\n//   type: 'type_[1-4]',\n//   memory: '[1-100]',\n//   ttl: -1,\n//   length: '[1-100]',\n// }\nconst mockKeys: Key[] = [];\nfor (let i = 1; i < keysCount; i += numberOfNsp) {\n  for (let j = 1; j <= numberOfNsp; j += 1) {\n    const idx = j + i - 1;\n    genericTotal += idx;\n    mockKeys.push({\n      name: Buffer.from(`nsp_${calculateTypeIdx(idx)}:key_${idx}`),\n      type: `type_${calculateTypeIdx(idx)}`,\n      memory: idx,\n      ttl: -1,\n      length: keysCount - idx + 1,\n    });\n  }\n}\n\nconst mockKeysWithNulls = [\n  {\n    name: 'wo',\n    type: 'wo',\n    memory: null,\n    length: null,\n    ttl: null,\n  } as Key,\n  {\n    name: 'wo',\n    type: 'type_1',\n    memory: null,\n    length: null,\n    ttl: null,\n  } as Key,\n  ...mockKeys.slice(0, 2),\n];\n\nconst mockPartialAnalysis = {\n  delimiter: ':',\n};\n\ndescribe('DatabaseAnalyzer', () => {\n  const analyzer = new DatabaseAnalyzer();\n\n  describe('calculateSimpleSummary', () => {\n    it('should calculate simple summary by memory', async () => {\n      const summary = await analyzer.calculateSimpleSummary(\n        shuffleKeys(mockKeys),\n        'memory',\n      );\n      expect(summary).toEqual({\n        total: genericTotal,\n        types: [\n          {\n            type: 'type_4',\n            total: getGenericTotal(76, 100),\n          },\n          {\n            type: 'type_3',\n            total: getGenericTotal(51, 75),\n          },\n          {\n            type: 'type_2',\n            total: getGenericTotal(26, 50),\n          },\n          {\n            type: 'type_1',\n            total: getGenericTotal(1, 25),\n          },\n        ],\n      });\n    });\n    it('should calculate simple summary by memory (null handled)', async () => {\n      const summary = await analyzer.calculateSimpleSummary(\n        shuffleKeys(mockKeysWithNulls),\n        'memory',\n      );\n      expect(summary).toEqual({\n        total: 3,\n        types: [\n          {\n            type: 'type_1',\n            total: 3,\n          },\n          {\n            type: 'wo',\n            total: 0,\n          },\n        ],\n      });\n    });\n    it('should calculate simple summary by keys number', async () => {\n      const summary = await analyzer.calculateSimpleSummary(\n        shuffleKeys(mockKeys),\n        1,\n      );\n      expect(summary.total).toEqual(keysCount);\n      expect(summary.types.length).toEqual(4);\n      summary.types.forEach((type) => {\n        expect(type.total).toEqual(25);\n        expect(\n          ['type_1', 'type_2', 'type_3', 'type_4'].includes(type.type),\n        ).toEqual(true);\n      });\n    });\n    it('should calculate simple summary by keys number (null handled)', async () => {\n      const summary = await analyzer.calculateSimpleSummary(\n        shuffleKeys(mockKeysWithNulls),\n        1,\n      );\n      expect(summary).toEqual({\n        total: mockKeysWithNulls.length,\n        types: [\n          {\n            type: 'type_1',\n            total: 3,\n          },\n          {\n            type: 'wo',\n            total: 1,\n          },\n        ],\n      });\n    });\n  });\n\n  describe('calculateTopKeys', () => {\n    it('should calculate top keys by memory', async () => {\n      const summary = await analyzer.calculateTopKeys(\n        [shuffleKeys(mockKeys)],\n        'memory',\n      );\n      expect(summary).toEqual([...mockKeys].reverse().slice(0, 15));\n    });\n    it('should calculate top keys by memory (with null)', async () => {\n      const summary = await analyzer.calculateTopKeys(\n        [mockKeysWithNulls],\n        'memory',\n      );\n      expect(summary).toEqual([\n        ...mockKeys.slice(0, 2).reverse(),\n        mockKeysWithNulls[0],\n        mockKeysWithNulls[1],\n      ]);\n    });\n    it('should calculate top keys by length', async () => {\n      const summary = await analyzer.calculateTopKeys(\n        [shuffleKeys(mockKeys)],\n        'length',\n      );\n      expect(summary).toEqual(mockKeys.slice(0, 15));\n    });\n    it('should calculate top keys by length (with null)', async () => {\n      const summary = await analyzer.calculateTopKeys(\n        [mockKeysWithNulls],\n        'length',\n      );\n      expect(summary).toEqual([\n        ...mockKeys.slice(0, 2),\n        mockKeysWithNulls[0],\n        mockKeysWithNulls[1],\n      ]);\n    });\n  });\n\n  describe('getNamespacesMap', () => {\n    it('should get namespaces map', async () => {\n      const summary = await analyzer.getNamespacesMap(\n        mockKeys,\n        mockPartialAnalysis.delimiter,\n      );\n      expect(summary).toEqual(\n        new Map([\n          [\n            Buffer.from('nsp_1').toString('hex'),\n            {\n              keys: 25,\n              memory: getGenericTotal(1, 25),\n              types: new Map([\n                ['type_1', { keys: 25, memory: getGenericTotal(1, 25) }],\n              ]),\n            },\n          ],\n          [\n            Buffer.from('nsp_2').toString('hex'),\n            {\n              keys: 25,\n              memory: getGenericTotal(26, 50),\n              types: new Map([\n                ['type_2', { keys: 25, memory: getGenericTotal(26, 50) }],\n              ]),\n            },\n          ],\n          [\n            Buffer.from('nsp_3').toString('hex'),\n            {\n              keys: 25,\n              memory: getGenericTotal(51, 75),\n              types: new Map([\n                ['type_3', { keys: 25, memory: getGenericTotal(51, 75) }],\n              ]),\n            },\n          ],\n          [\n            Buffer.from('nsp_4').toString('hex'),\n            {\n              keys: 25,\n              memory: getGenericTotal(76, 100),\n              types: new Map([\n                ['type_4', { keys: 25, memory: getGenericTotal(76, 100) }],\n              ]),\n            },\n          ],\n        ]),\n      );\n    });\n    it('should get namespaces map (for keys without nsps)', async () => {\n      const summary = await analyzer.getNamespacesMap(\n        mockKeysWithNulls,\n        mockPartialAnalysis.delimiter,\n      );\n      expect(summary).toEqual(\n        new Map([\n          [\n            Buffer.from('nsp_1').toString('hex'),\n            {\n              keys: 2,\n              memory: getGenericTotal(1, 2),\n              types: new Map([\n                ['type_1', { keys: 2, memory: getGenericTotal(1, 2) }],\n              ]),\n            },\n          ],\n        ]),\n      );\n    });\n  });\n\n  describe('analyze', () => {\n    it('should return analysis', async () => {\n      const summary = await analyzer.analyze(mockPartialAnalysis, mockKeys);\n      expect(summary).toEqual({\n        ...mockPartialAnalysis,\n        topKeysMemory: [...mockKeys].reverse().slice(0, 15),\n        topKeysLength: mockKeys.slice(0, 15),\n        topMemoryNsp: [\n          {\n            nsp: Buffer.from('nsp_4'),\n            keys: 25,\n            memory: getGenericTotal(76, 100),\n            types: [\n              {\n                type: 'type_4',\n                keys: 25,\n                memory: getGenericTotal(76, 100),\n              },\n            ],\n          },\n          {\n            nsp: Buffer.from('nsp_3'),\n            keys: 25,\n            memory: getGenericTotal(51, 75),\n            types: [\n              {\n                type: 'type_3',\n                keys: 25,\n                memory: getGenericTotal(51, 75),\n              },\n            ],\n          },\n          {\n            nsp: Buffer.from('nsp_2'),\n            keys: 25,\n            memory: getGenericTotal(26, 50),\n            types: [\n              {\n                type: 'type_2',\n                keys: 25,\n                memory: getGenericTotal(26, 50),\n              },\n            ],\n          },\n          {\n            nsp: Buffer.from('nsp_1'),\n            keys: 25,\n            memory: getGenericTotal(1, 25),\n            types: [\n              {\n                type: 'type_1',\n                keys: 25,\n                memory: getGenericTotal(1, 25),\n              },\n            ],\n          },\n        ],\n        topKeysNsp: [\n          {\n            nsp: Buffer.from('nsp_4'),\n            keys: 25,\n            memory: getGenericTotal(76, 100),\n            types: [\n              {\n                type: 'type_4',\n                keys: 25,\n                memory: getGenericTotal(76, 100),\n              },\n            ],\n          },\n          {\n            nsp: Buffer.from('nsp_3'),\n            keys: 25,\n            memory: getGenericTotal(51, 75),\n            types: [\n              {\n                type: 'type_3',\n                keys: 25,\n                memory: getGenericTotal(51, 75),\n              },\n            ],\n          },\n          {\n            nsp: Buffer.from('nsp_2'),\n            keys: 25,\n            memory: getGenericTotal(26, 50),\n            types: [\n              {\n                type: 'type_2',\n                keys: 25,\n                memory: getGenericTotal(26, 50),\n              },\n            ],\n          },\n          {\n            nsp: Buffer.from('nsp_1'),\n            keys: 25,\n            memory: getGenericTotal(1, 25),\n            types: [\n              {\n                type: 'type_1',\n                keys: 25,\n                memory: getGenericTotal(1, 25),\n              },\n            ],\n          },\n        ],\n        totalMemory: {\n          total: genericTotal,\n          types: [\n            {\n              type: 'type_4',\n              total: getGenericTotal(76, 100),\n            },\n            {\n              type: 'type_3',\n              total: getGenericTotal(51, 75),\n            },\n            {\n              type: 'type_2',\n              total: getGenericTotal(26, 50),\n            },\n            {\n              type: 'type_1',\n              total: getGenericTotal(1, 25),\n            },\n          ],\n        },\n        totalKeys: {\n          total: mockKeys.length,\n          types: [\n            {\n              type: 'type_4',\n              total: 25,\n            },\n            {\n              type: 'type_3',\n              total: 25,\n            },\n            {\n              type: 'type_2',\n              total: 25,\n            },\n            {\n              type: 'type_1',\n              total: 25,\n            },\n          ],\n        },\n        expirationGroups: [\n          {\n            label: 'No Expiry',\n            threshold: 0,\n            total: genericTotal,\n          },\n          {\n            label: '<1 hr',\n            threshold: 3600,\n            total: 0,\n          },\n          {\n            label: '1-4 Hrs',\n            threshold: 14400,\n            total: 0,\n          },\n          {\n            label: '4-12 Hrs',\n            threshold: 43200,\n            total: 0,\n          },\n          {\n            label: '12-24 Hrs',\n            threshold: 86400,\n            total: 0,\n          },\n          {\n            label: '1-7 Days',\n            threshold: 604800,\n            total: 0,\n          },\n          {\n            label: '>7 Days',\n            threshold: 2592000,\n            total: 0,\n          },\n          {\n            label: '>1 Month',\n            threshold: 9007199254740991,\n            total: 0,\n          },\n        ],\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/providers/database-analyzer.ts",
    "content": "import { sortBy, isNumber } from 'lodash';\nimport {\n  DatabaseAnalysis,\n  Key,\n  NspSummary,\n  NspTypeSummary,\n  SimpleSummary,\n  SimpleTypeSummary,\n  SumGroup,\n} from 'src/modules/database-analysis/models';\nimport { RedisString } from 'src/common/constants';\nimport { Injectable } from '@nestjs/common';\nimport { sortByNumberField } from 'src/utils/base.helper';\n\nconst TOP_KEYS_LIMIT = 15;\nconst TOP_NSP_LIMIT = 15;\n\n@Injectable()\nexport class DatabaseAnalyzer {\n  async analyze(\n    analysis: Partial<DatabaseAnalysis>,\n    keys: Key[],\n  ): Promise<Partial<DatabaseAnalysis>> {\n    const namespaces = await this.getNamespacesMap(keys, analysis.delimiter);\n\n    return {\n      ...analysis,\n      totalKeys: await this.calculateSimpleSummary(keys, 1),\n      totalMemory: await this.calculateSimpleSummary(keys, 'memory'),\n      topKeysNsp: await this.calculateNspSummary(namespaces, 'keys'),\n      topMemoryNsp: await this.calculateNspSummary(namespaces, 'memory'),\n      topKeysLength: await this.calculateTopKeys([keys], 'length'),\n      topKeysMemory: await this.calculateTopKeys([keys], 'memory'),\n      expirationGroups: await this.calculateExpirationTimeGroups(keys),\n    };\n  }\n\n  /**\n   * Calculate summary based on field name (string) or number incr value\n   * @param keys\n   * @param field\n   */\n  async calculateSimpleSummary(\n    keys: Key[],\n    field: string | number,\n  ): Promise<SimpleSummary> {\n    const summary = {\n      total: 0,\n      types: new Map(),\n    };\n\n    if (isNumber(field)) {\n      keys.forEach((key) => {\n        summary.total += 1;\n        summary.types.set(key.type, (summary.types.get(key.type) || 0) + 1);\n      });\n    } else {\n      keys.forEach((key) => {\n        summary.total += key[field];\n        summary.types.set(\n          key.type,\n          (summary.types.get(key.type) || 0) + key[field],\n        );\n      });\n    }\n\n    return {\n      ...summary,\n      types: this.calculateSimpleTypeSummary(summary.types),\n    };\n  }\n\n  /**\n   * Converts type summary Map to SimpleTypeSummary array\n   * Also sort DESC by \"total\" field\n   * @param types\n   */\n  calculateSimpleTypeSummary(types: Map<string, any>): SimpleTypeSummary[] {\n    return sortBy(\n      [...types.keys()].map((type) => ({ type, total: types.get(type) })),\n      'total',\n    ).reverse();\n  }\n\n  /**\n   * Create namespaces map\n   * @param keys\n   * @param delimiter\n   */\n  async getNamespacesMap(\n    keys: Key[],\n    delimiter: string,\n  ): Promise<Map<string, any>> {\n    const namespaces = new Map();\n\n    keys.forEach((key) => {\n      const nsp = this.getNamespace(key.name as Buffer, delimiter);\n      if (!nsp) {\n        return;\n      }\n\n      const namespace = namespaces.get(nsp) || {\n        memory: 0,\n        keys: 0,\n        types: new Map(),\n      };\n\n      namespace.keys += 1;\n      namespace.memory += key.memory;\n\n      const namespaceType = namespace.types.get(key.type) || {\n        memory: 0,\n        keys: 0,\n      };\n\n      namespaceType.keys += 1;\n      namespaceType.memory += key.memory;\n\n      namespace.types.set(key.type, namespaceType);\n      namespaces.set(nsp, namespace);\n    });\n\n    return namespaces;\n  }\n\n  /**\n   * Finds specific delimiter in the Buffer and slice the bytes\n   * Converts bytes to hex to work well with Map()\n   * @param key\n   * @param delimiter\n   */\n  getNamespace(key: Buffer, delimiter = ':'): string {\n    const pos = key.indexOf(delimiter);\n    if (pos > -1) {\n      return key.slice(0, pos).toString('hex');\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Calculate top X namespaces by specific field\n   * Converts Map to NspSummary array\n   * @param namespaces\n   * @param field\n   */\n  async calculateNspSummary(\n    namespaces: Map<RedisString, any>,\n    field: string,\n  ): Promise<NspSummary[]> {\n    const nspSummaries = sortBy(\n      [...namespaces.keys()].map((nsp) => ({ nsp, ...namespaces.get(nsp) })),\n      field,\n    )\n      .reverse()\n      .slice(0, TOP_NSP_LIMIT);\n\n    return nspSummaries.map((nspSummary) => ({\n      ...nspSummary,\n      nsp: Buffer.from(nspSummary.nsp || '', 'hex'), // convert hex string back to Buffer\n      types: this.calculateNspTypeSummary(nspSummary.types, field),\n    }));\n  }\n\n  /**\n   * Converts type summary Map to NspTypeSummary array\n   * Also sort DESC by specific field\n   * @param nspTypes\n   * @param field\n   */\n  calculateNspTypeSummary(\n    nspTypes: Map<string, any>,\n    field: string,\n  ): NspTypeSummary[] {\n    return sortBy(\n      [...nspTypes.keys()].map((type) => ({ type, ...nspTypes.get(type) })),\n      field,\n    ).reverse();\n  }\n\n  /**\n   * Calculates top keys by specific field\n   * Waiting for batches of keys arrays to increase performance\n   * E.g. sorting has O(n*n) complexity so it is better to sort data in small batches,\n   * find top X elements and then find top X tops from the batches.\n   * @param keysBatches\n   * @param field\n   */\n  async calculateTopKeys(keysBatches: Key[][], field: string): Promise<Key[]> {\n    return sortByNumberField(\n      [].concat(\n        ...keysBatches.map((keysBatch) =>\n          sortByNumberField(keysBatch, field)\n            .reverse()\n            .slice(0, TOP_KEYS_LIMIT),\n        ),\n      ),\n      field,\n    )\n      .reverse()\n      .slice(0, TOP_KEYS_LIMIT);\n  }\n\n  async calculateExpirationTimeGroups(keys: Key[]): Promise<SumGroup[]> {\n    const groups = [\n      {\n        threshold: 0,\n        total: 0,\n        label: 'No Expiry',\n      },\n      {\n        threshold: 60 * 60,\n        total: 0,\n        label: '<1 hr',\n      },\n      {\n        threshold: 4 * 60 * 60,\n        total: 0,\n        label: '1-4 Hrs',\n      },\n      {\n        threshold: 12 * 60 * 60,\n        total: 0,\n        label: '4-12 Hrs',\n      },\n      {\n        threshold: 24 * 60 * 60,\n        total: 0,\n        label: '12-24 Hrs',\n      },\n      {\n        threshold: 7 * 24 * 60 * 60,\n        total: 0,\n        label: '1-7 Days',\n      },\n      {\n        threshold: 30 * 24 * 60 * 60,\n        total: 0,\n        label: '>7 Days',\n      },\n      {\n        threshold: Number.MAX_SAFE_INTEGER,\n        total: 0,\n        label: '>1 Month',\n      },\n    ];\n\n    keys.forEach((key) => {\n      for (let i = 0; i < groups.length; i += 1) {\n        if (key.ttl < groups[i].threshold) {\n          groups[i].total += key.memory;\n          break;\n        }\n      }\n    });\n\n    return groups;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/key-info.provider.spec.ts",
    "content": "import { KeyInfoProvider } from 'src/modules/database-analysis/scanner/key-info/key-info.provider';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport {\n  DefaultInfoStrategy,\n  GraphInfoStrategy,\n  HashInfoStrategy,\n  JsonInfoStrategy,\n  ListInfoStrategy,\n  SetInfoStrategy,\n  StreamInfoStrategy,\n  StringInfoStrategy,\n  TsInfoStrategy,\n} from 'src/modules/database-analysis/scanner/key-info/strategies';\n\ndescribe('KeysScanner', () => {\n  const service = new KeyInfoProvider();\n  describe('getStrategy', () => {\n    [\n      [RedisDataType.Graph, new GraphInfoStrategy()],\n      [RedisDataType.Hash, new HashInfoStrategy()],\n      [RedisDataType.JSON, new JsonInfoStrategy()],\n      [RedisDataType.List, new ListInfoStrategy()],\n      [RedisDataType.Set, new SetInfoStrategy()],\n      [RedisDataType.Stream, new StreamInfoStrategy()],\n      [RedisDataType.String, new StringInfoStrategy()],\n      [RedisDataType.TS, new TsInfoStrategy()],\n      [RedisDataType.ZSet, new GraphInfoStrategy()],\n      ['default', new DefaultInfoStrategy()],\n      ['unknown', new DefaultInfoStrategy()],\n      [null, new DefaultInfoStrategy()],\n    ].forEach((tc) => {\n      it(`should return ${tc[1].constructor.name} for type: ${tc[0]}`, () => {\n        expect(service.getStrategy(tc[0] as string)).toEqual(tc[1]);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/key-info.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { IKeyInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/key-info.strategy.interface';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport {\n  DefaultInfoStrategy,\n  GraphInfoStrategy,\n  HashInfoStrategy,\n  JsonInfoStrategy,\n  ListInfoStrategy,\n  SetInfoStrategy,\n  StreamInfoStrategy,\n  StringInfoStrategy,\n  TsInfoStrategy,\n  ZSetInfoStrategy,\n} from 'src/modules/database-analysis/scanner/key-info/strategies';\n\n@Injectable()\nexport class KeyInfoProvider {\n  private strategies: Map<string, IKeyInfoStrategy> = new Map();\n\n  constructor() {\n    this.strategies.set('default', new DefaultInfoStrategy());\n    this.strategies.set(RedisDataType.Graph, new GraphInfoStrategy());\n    this.strategies.set(RedisDataType.Hash, new HashInfoStrategy());\n    this.strategies.set(RedisDataType.JSON, new JsonInfoStrategy());\n    this.strategies.set(RedisDataType.List, new ListInfoStrategy());\n    this.strategies.set(RedisDataType.Set, new SetInfoStrategy());\n    this.strategies.set(RedisDataType.Stream, new StreamInfoStrategy());\n    this.strategies.set(RedisDataType.String, new StringInfoStrategy());\n    this.strategies.set(RedisDataType.TS, new TsInfoStrategy());\n    this.strategies.set(RedisDataType.ZSet, new ZSetInfoStrategy());\n  }\n\n  getStrategy(type: string): IKeyInfoStrategy {\n    const strategy = this.strategies.get(type);\n\n    if (!strategy) {\n      return this.strategies.get('default');\n    }\n\n    return strategy;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/key-info.strategy.interface.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport interface IKeyInfoStrategy {\n  getLength(client: RedisClient, key: RedisString): Promise<number>;\n  getLengthSafe(client: RedisClient, key: RedisString): Promise<number>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { HashInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/hash-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('AbstractInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new HashInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['hlen']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLengthSafe', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLengthSafe(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n    it('should return null in case of error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['hlen']))\n        .mockRejectedValueOnce(new Error('some error'));\n\n      expect(await strategy.getLengthSafe(client, mockKey)).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { IKeyInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/key-info.strategy.interface';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport abstract class AbstractInfoStrategy implements IKeyInfoStrategy {\n  abstract getLength(client: RedisClient, key: RedisString): Promise<number>;\n\n  async getLengthSafe(client: RedisClient, key: RedisString): Promise<number> {\n    try {\n      return await this.getLength(client, key);\n    } catch (e) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/default-info.strategy.spec.ts",
    "content": "import { DefaultInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/default-info.strategy';\n\ndescribe('DefaultInfoStrategy', () => {\n  const strategy = new DefaultInfoStrategy();\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength()).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/default-info.strategy.ts",
    "content": "import { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\n\nexport class DefaultInfoStrategy extends AbstractInfoStrategy {\n  async getLength(): Promise<number> {\n    return null;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/graph-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { GraphInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/graph-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = [\n  [[1, 'count(r)']],\n  [[[3, 999]]],\n  [\n    'Cached execution: 1',\n    'Query internal execution time: 0.093200 milliseconds',\n  ],\n];\n\ndescribe('GraphInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new GraphInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['graph.query']), expect.anything())\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(999);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/graph-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class GraphInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    const resp = await client.sendCommand(\n      ['graph.query', key, 'MATCH (r) RETURN count(r)', '--compact'],\n      { replyEncoding: 'utf8' },\n    );\n\n    return resp[1][0][0][1];\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/hash-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { HashInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/hash-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('HashInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new HashInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['hlen']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should scan standalone database', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/hash-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class HashInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    return (await client.sendCommand(['hlen', key])) as number;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/index.ts",
    "content": "export * from './abstract.info.strategy';\nexport * from './default-info.strategy';\nexport * from './graph-info.strategy';\nexport * from './hash-info.strategy';\nexport * from './json-info.strategy';\nexport * from './list-info.strategy';\nexport * from './set-info.strategy';\nexport * from './stream-info.strategy';\nexport * from './string-info.strategy';\nexport * from './ts-info.strategy';\nexport * from './z-set-info.strategy';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/json-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { JsonInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/json-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\n\ndescribe('JsonInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new JsonInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['json.objlen']), expect.anything())\n      .mockResolvedValue(1)\n      .calledWith(expect.arrayContaining(['json.arrlen']), expect.anything())\n      .mockResolvedValue(2)\n      .calledWith(expect.arrayContaining(['json.strlen']), expect.anything())\n      .mockResolvedValue(3);\n  });\n\n  describe('getLength', () => {\n    it('should get length (object)', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['json.type']), expect.anything())\n        .mockResolvedValue('object');\n\n      expect(await strategy.getLength(client, mockKey)).toEqual(1);\n    });\n    it('should get length (array)', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['json.type']), expect.anything())\n        .mockResolvedValue('array');\n\n      expect(await strategy.getLength(client, mockKey)).toEqual(2);\n    });\n    it('should get length (string)', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['json.type']), expect.anything())\n        .mockResolvedValue('string');\n\n      expect(await strategy.getLength(client, mockKey)).toEqual(3);\n    });\n    it('should get length (undefined)', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['json.type']), expect.anything())\n        .mockResolvedValue('undefined');\n\n      expect(await strategy.getLength(client, mockKey)).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/json-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class JsonInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    const objectKeyType = await client.sendCommand(['json.type', key], {\n      replyEncoding: 'utf8',\n    });\n\n    switch (objectKeyType) {\n      case 'object':\n        return (await client.sendCommand(['json.objlen', key], {\n          replyEncoding: 'utf8',\n        })) as number;\n      case 'array':\n        return (await client.sendCommand(['json.arrlen', key], {\n          replyEncoding: 'utf8',\n        })) as number;\n      case 'string':\n        return (await client.sendCommand(['json.strlen', key], {\n          replyEncoding: 'utf8',\n        })) as number;\n      default:\n        return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/list-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { ListInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/list-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('ListInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new ListInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['llen']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/list-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class ListInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    return (await client.sendCommand(['llen', key])) as number;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/set-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { SetInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/set-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('SetInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new SetInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['scard']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/set-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class SetInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    return (await client.sendCommand(['scard', key])) as number;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/stream-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { StreamInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/stream-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('StreamInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new StreamInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['xlen']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/stream-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class StreamInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    return (await client.sendCommand(['xlen', key])) as number;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/string-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { StringInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/string-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('StringInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new StringInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['strlen']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/string-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class StringInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    return (await client.sendCommand(['strlen', key])) as number;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/ts-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { TsInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/ts-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = [\n  'totalSamples',\n  10,\n  'memoryUsage',\n  4239,\n  'firstTimestamp',\n  0,\n  'lastTimestamp',\n  0,\n  'retentionTime',\n  6000,\n  'chunkCount',\n  1,\n  'chunkSize',\n  4096,\n];\n\ndescribe('TsInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new TsInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['ts.info']), expect.anything())\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(10);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/ts-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class TsInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    const { totalsamples } = convertArrayReplyToObject(\n      (await client.sendCommand(['ts.info', key], {\n        replyEncoding: 'utf8',\n      })) as string[],\n    );\n\n    return totalsamples;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/z-set-info.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { ZSetInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/z-set-info.strategy';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\n\nconst mockKey = Buffer.from('key');\nconst mockRedisResponse = 1;\n\ndescribe('ZSetInfoStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  const strategy = new ZSetInfoStrategy();\n\n  beforeEach(async () => {\n    when(client.sendCommand)\n      .calledWith(expect.arrayContaining(['zcard']))\n      .mockResolvedValue(mockRedisResponse);\n  });\n\n  describe('getLength', () => {\n    it('should get length', async () => {\n      expect(await strategy.getLength(client, mockKey)).toEqual(\n        mockRedisResponse,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/key-info/strategies/z-set-info.strategy.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { AbstractInfoStrategy } from 'src/modules/database-analysis/scanner/key-info/strategies/abstract.info.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class ZSetInfoStrategy extends AbstractInfoStrategy {\n  async getLength(client: RedisClient, key: RedisString): Promise<number> {\n    return (await client.sendCommand(['zcard', key])) as number;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';\nimport { KeyInfoProvider } from 'src/modules/database-analysis/scanner/key-info/key-info.provider';\nimport { mockCreateDatabaseAnalysisDto } from 'src/modules/database-analysis/providers/database-analysis.provider.spec';\nimport * as Utils from 'src/modules/redis/utils/keys.util';\nimport {\n  mockClusterRedisClient,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\n\nconst standaloneClient = mockStandaloneRedisClient;\nconst clusterClient = mockClusterRedisClient;\n\nconst mockInfoStrategy = () => ({\n  getLength: jest.fn(),\n  getLengthSafe: jest.fn(),\n});\n\nconst mockKeyInfoProvider = () => ({\n  getStrategy: jest.fn(),\n});\n\nconst mockGetTotalResponse = 1;\n\nconst mockKey = {\n  name: Buffer.from('key'),\n  type: 'string',\n  ttl: -1,\n  memory: 10,\n  length: 2,\n};\n\nconst mockIndex = 'idx';\n\nconst mockScanResult = {\n  keys: [mockKey],\n  indexes: [mockIndex],\n  progress: {\n    processed: 1,\n    scanned: 15,\n    total: 1,\n  },\n  client: Object.assign(standaloneClient),\n};\n\ndescribe('KeysScanner', () => {\n  let service: KeysScanner;\n  let infoProvider;\n  let infoStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        KeysScanner,\n        {\n          provide: KeyInfoProvider,\n          useFactory: mockKeyInfoProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get<KeysScanner>(KeysScanner);\n    infoProvider = module.get<KeyInfoProvider>(KeyInfoProvider);\n    infoStrategy = mockInfoStrategy();\n    infoProvider.getStrategy.mockReturnValue(infoStrategy);\n    infoStrategy.getLengthSafe.mockResolvedValue(2);\n    clusterClient.nodes.mockReturnValue([\n      standaloneClient,\n      standaloneClient,\n      standaloneClient,\n    ]);\n    when(standaloneClient.sendCommand)\n      .calledWith(expect.arrayContaining(['scan']))\n      .mockResolvedValue(['0', [mockKey.name]]);\n    when(standaloneClient.sendPipeline)\n      .calledWith(expect.arrayContaining([expect.arrayContaining(['memory'])]))\n      .mockReturnValue([[null, 10]]);\n    when(standaloneClient.sendPipeline)\n      .calledWith(expect.arrayContaining([expect.arrayContaining(['ttl'])]))\n      .mockReturnValue([[null, -1]]);\n    when(standaloneClient.sendPipeline)\n      .calledWith(\n        expect.arrayContaining([expect.arrayContaining(['type'])]),\n        expect.anything(),\n      )\n      .mockReturnValue([[null, 'string']]);\n    when(standaloneClient.sendCommand)\n      .calledWith(['FT._LIST'], expect.anything())\n      .mockResolvedValue([mockIndex]);\n  });\n\n  describe('scan', () => {\n    it('should scan standalone database', async () => {\n      jest.spyOn(Utils, 'getTotalKeys').mockResolvedValue(mockGetTotalResponse);\n      expect(\n        await service.scan(standaloneClient, {\n          filter: mockCreateDatabaseAnalysisDto.filter,\n        }),\n      ).toEqual([mockScanResult]);\n    });\n    it('should scan cluster database', async () => {\n      jest.spyOn(Utils, 'getTotalKeys').mockResolvedValue(mockGetTotalResponse);\n      expect(\n        await service.scan(clusterClient, {\n          filter: mockCreateDatabaseAnalysisDto.filter,\n        }),\n      ).toEqual([mockScanResult, mockScanResult, mockScanResult]);\n    });\n  });\n\n  describe('nodeScan', () => {\n    it('should scan node keys', async () => {\n      jest.spyOn(Utils, 'getTotalKeys').mockResolvedValue(mockGetTotalResponse);\n      expect(\n        await service.nodeScan(standaloneClient, {\n          filter: mockCreateDatabaseAnalysisDto.filter,\n        }),\n      ).toEqual(mockScanResult);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { getTotalKeys } from 'src/modules/redis/utils';\nimport { KeyInfoProvider } from 'src/modules/database-analysis/scanner/key-info/key-info.provider';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n  RedisClientNodeRole,\n} from 'src/modules/redis/client';\nimport { ScanFilter } from 'src/modules/database-analysis/models/scan-filter';\n\n@Injectable()\nexport class KeysScanner {\n  constructor(private readonly keyInfoProvider: KeyInfoProvider) {}\n\n  async scan(client: RedisClient, opts: { filter: ScanFilter }) {\n    let nodes = [];\n\n    if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n      nodes = await client.nodes(RedisClientNodeRole.PRIMARY);\n    } else {\n      nodes = [client];\n    }\n\n    return Promise.all(nodes.map((node) => this.nodeScan(node, opts)));\n  }\n\n  async nodeScan(client: RedisClient, opts: { filter: ScanFilter }) {\n    const total = await getTotalKeys(client);\n    let indexes: string[];\n    let libraries: string[];\n\n    try {\n      indexes = (await client.sendCommand(['FT._LIST'], {\n        replyEncoding: 'utf8',\n      })) as string[];\n    } catch (err) {\n      // Ignore errors\n    }\n\n    try {\n      libraries = (await client.sendCommand(['TFUNCTION', 'LIST'], {\n        replyEncoding: 'utf8',\n      })) as string[];\n    } catch (err) {\n      // Ignore errors\n    }\n\n    let keys = [];\n    const COUNT = Math.min(2000, opts.filter.count);\n    let scanned = 0;\n    let cursor: number;\n\n    while (scanned < opts.filter.count && cursor !== 0) {\n      const [cursorResp, keysResp] = (await client.sendCommand([\n        'scan',\n        cursor || 0,\n        'count',\n        COUNT,\n        ...opts.filter.getScanArgsArray(),\n      ])) as [string, Buffer[]];\n\n      cursor = parseInt(cursorResp, 10) || 0;\n      scanned += COUNT;\n      keys = keys.concat(keysResp);\n    }\n\n    const [sizes, types, ttls] = await Promise.all([\n      client.sendPipeline(\n        keys.map((key) => ['memory', 'usage', key, 'samples', '0']),\n      ),\n      client.sendPipeline(\n        keys.map((key) => ['type', key]),\n        { replyEncoding: 'utf8' },\n      ),\n      client.sendPipeline(keys.map((key) => ['ttl', key])),\n    ]);\n\n    const lengths = await Promise.all(\n      keys.map(async (key, i) => {\n        const strategy = this.keyInfoProvider.getStrategy(\n          types[i][1] as string,\n        );\n        return strategy.getLengthSafe(client, key);\n      }),\n    );\n\n    const nodeKeys = [];\n    for (let i = 0; i < keys.length; i += 1) {\n      nodeKeys.push({\n        name: keys[i],\n        memory: sizes[i][0] ? null : sizes[i][1],\n        length: lengths[i],\n        type: types[i][0] ? 'N/A' : types[i][1],\n        ttl: ttls[i][0] ? -2 : ttls[i][1],\n      });\n    }\n\n    return {\n      keys: nodeKeys,\n      indexes,\n      libraries,\n      progress: {\n        total,\n        scanned: opts.filter.count,\n        processed: nodeKeys.length,\n      },\n      client,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/auto.database-discovery.service.spec.ts",
    "content": "import * as utils from 'src/utils';\nimport { getAvailableEndpoints } from 'src/modules/database-discovery/utils/autodiscovery.util';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAutodiscoveryEndpoint,\n  mockDatabase,\n  mockDatabaseService,\n  mockRedisClientFactory,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { AutoDatabaseDiscoveryService } from 'src/modules/database-discovery/auto.database-discovery.service';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { mockConstantsProvider } from 'src/__mocks__/constants';\n\njest.mock(\n  'src/modules/database-discovery/utils/autodiscovery.util',\n  jest.fn(() => ({\n    ...(jest.requireActual(\n      'src/modules/database-discovery/utils/autodiscovery.util',\n    ) as object),\n    __esModule: true,\n    getAvailableEndpoints: jest.fn(),\n  })),\n);\n\njest.mock(\n  'src/utils',\n  jest.fn(() => ({\n    ...(jest.requireActual('src/utils') as object),\n    __esModule: true,\n    convertRedisInfoReplyToObject: jest.fn(),\n  })),\n);\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\ndescribe('AutoDatabaseDiscoveryService', () => {\n  let service: AutoDatabaseDiscoveryService;\n  let databaseService: MockType<DatabaseService>;\n  let redisClientFactory: MockType<RedisClientFactory>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AutoDatabaseDiscoveryService,\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: RedisClientFactory,\n          useFactory: mockRedisClientFactory,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(AutoDatabaseDiscoveryService);\n    databaseService = module.get(DatabaseService);\n    redisClientFactory = module.get(RedisClientFactory);\n\n    (utils.convertRedisInfoReplyToObject as jest.Mock).mockReturnValue({\n      server: {\n        redis_mode: 'standalone',\n      },\n    });\n  });\n\n  describe('discover', () => {\n    let addRedisDatabaseSpy: jest.SpyInstance;\n\n    beforeEach(async () => {\n      (getAvailableEndpoints as jest.Mock).mockResolvedValue([]);\n      addRedisDatabaseSpy = jest.spyOn(service as any, 'addRedisDatabase');\n      addRedisDatabaseSpy.mockResolvedValue(null);\n    });\n\n    it('should not call addRedisDatabase when no endpoints found', async () => {\n      await service.discover(mockSessionMetadata);\n\n      expect(addRedisDatabaseSpy).toHaveBeenCalledTimes(0);\n    });\n\n    it('should call addRedisDatabase 2 times', async () => {\n      (getAvailableEndpoints as jest.Mock).mockResolvedValueOnce([\n        mockAutodiscoveryEndpoint,\n        mockAutodiscoveryEndpoint,\n      ]);\n      databaseService.list.mockResolvedValue([]);\n\n      await service.discover(mockSessionMetadata);\n\n      expect(addRedisDatabaseSpy).toHaveBeenCalledTimes(2);\n      expect(addRedisDatabaseSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockAutodiscoveryEndpoint,\n      );\n    });\n\n    it('should not call addRedisDatabase when there existing databases', async () => {\n      databaseService.list.mockResolvedValue([mockDatabase]);\n\n      await service.discover(mockSessionMetadata);\n\n      expect(addRedisDatabaseSpy).not.toHaveBeenCalled();\n      expect(getAvailableEndpoints as jest.Mock).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('addRedisDatabase', () => {\n    it('should create database if redis_mode is standalone', async () => {\n      redisClientFactory.createClient.mockResolvedValue({\n        getInfo: async () => ({\n          server: {\n            redis_mode: 'standalone',\n          },\n        }),\n      });\n\n      await service['addRedisDatabase'](\n        mockSessionMetadata,\n        mockAutodiscoveryEndpoint,\n      );\n\n      expect(databaseService.create).toHaveBeenCalledTimes(1);\n      expect(databaseService.create).toHaveBeenCalledWith(mockSessionMetadata, {\n        name: `${mockAutodiscoveryEndpoint.host}:${mockAutodiscoveryEndpoint.port}`,\n        ...mockAutodiscoveryEndpoint,\n      });\n    });\n\n    it('should not create database if redis_mode is not standalone', async () => {\n      redisClientFactory.createClient.mockResolvedValue({\n        getInfo: async () => ({\n          server: {\n            redis_mode: 'cluster',\n          },\n        }),\n      });\n\n      await service['addRedisDatabase'](\n        mockSessionMetadata,\n        mockAutodiscoveryEndpoint,\n      );\n\n      expect(databaseService.create).toHaveBeenCalledTimes(0);\n    });\n\n    it('should not fail in case of an error', async () => {\n      redisClientFactory.createClient.mockRejectedValue(new Error());\n\n      await service['addRedisDatabase'](\n        mockSessionMetadata,\n        mockAutodiscoveryEndpoint,\n      );\n\n      expect(databaseService.create).toHaveBeenCalledTimes(0);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/auto.database-discovery.service.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { getAvailableEndpoints } from 'src/modules/database-discovery/utils/autodiscovery.util';\nimport { Database } from 'src/modules/database/models/database';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport {\n  ClientContext,\n  ClientMetadata,\n  SessionMetadata,\n} from 'src/common/models';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { plainToInstance } from 'class-transformer';\n\n@Injectable()\nexport class AutoDatabaseDiscoveryService {\n  private logger = new Logger('AutoDatabaseDiscoveryService');\n\n  constructor(\n    private redisClientFactory: RedisClientFactory,\n    private databaseService: DatabaseService,\n  ) {}\n\n  /**\n   * Try to add standalone databases without auth from processes running on the host machine listening on TCP4\n   * Database alias will be \"host:port\"\n   */\n  async discover(sessionMetadata: SessionMetadata) {\n    try {\n      // additional check for existing databases\n      // We should not start auto discover if any database already exists\n      if ((await this.databaseService.list(sessionMetadata)).length) {\n        return;\n      }\n\n      const endpoints = await getAvailableEndpoints();\n\n      // Add redis databases or resolve after 1s to not block app startup for a long time\n      await Promise.race([\n        Promise.all(\n          endpoints.map((endpoint) =>\n            this.addRedisDatabase(sessionMetadata, endpoint),\n          ),\n        ),\n        new Promise((resolve) => setTimeout(resolve, 1000)),\n      ]);\n    } catch (e) {\n      this.logger.warn('Unable to discover redis database', e);\n    }\n  }\n\n  /**\n   * Add standalone database without credentials using host and port only\n   * @param sessionMetadata\n   * @param endpoint\n   * @private\n   */\n  private async addRedisDatabase(\n    sessionMetadata: SessionMetadata,\n    endpoint: { host: string; port: number },\n  ) {\n    try {\n      const client = await this.redisClientFactory.createClient(\n        {\n          databaseId: uuidv4(),\n          context: ClientContext.Common,\n          sessionMetadata,\n        } as ClientMetadata,\n        plainToInstance(Database, endpoint),\n        { useRetry: false, connectionName: 'redisinsight-auto-discovery' },\n      );\n\n      const info = await client.getInfo();\n\n      if (info?.server?.redis_mode === 'standalone') {\n        await this.databaseService.create(sessionMetadata, {\n          name: `${endpoint.host}:${endpoint.port}`,\n          ...endpoint,\n        } as Database);\n      }\n    } catch (e) {\n      // ignore error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/database-discovery.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { DatabaseDiscoveryService } from 'src/modules/database-discovery/database-discovery.service';\nimport { LocalDatabaseDiscoveryService } from 'src/modules/database-discovery/local.database-discovery.service';\nimport { AutoDatabaseDiscoveryService } from 'src/modules/database-discovery/auto.database-discovery.service';\nimport { PreSetupDatabaseDiscoveryService } from 'src/modules/database-discovery/pre-setup.database-discovery.service';\n\n@Module({\n  providers: [\n    AutoDatabaseDiscoveryService,\n    PreSetupDatabaseDiscoveryService,\n    {\n      provide: DatabaseDiscoveryService,\n      useClass: LocalDatabaseDiscoveryService,\n    },\n  ],\n  exports: [DatabaseDiscoveryService],\n})\nexport class DatabaseDiscoveryModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/database-discovery.service.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\n\nexport abstract class DatabaseDiscoveryService {\n  abstract discover(\n    sessionMetadata: SessionMetadata,\n    firstRun?: boolean,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/local.database-discovery.service.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAutoDatabaseDiscoveryService,\n  mockPreSetupDatabaseDiscoveryService,\n  mockSessionMetadata,\n  mockSettingsService,\n  MockType,\n} from 'src/__mocks__';\nimport config, { Config } from 'src/utils/config';\nimport { PreSetupDatabaseDiscoveryService } from 'src/modules/database-discovery/pre-setup.database-discovery.service';\nimport { LocalDatabaseDiscoveryService } from 'src/modules/database-discovery/local.database-discovery.service';\nimport { AutoDatabaseDiscoveryService } from 'src/modules/database-discovery/auto.database-discovery.service';\nimport { SettingsService } from 'src/modules/settings/settings.service';\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\ndescribe('PreSetupDatabaseDiscoveryService', () => {\n  let service: LocalDatabaseDiscoveryService;\n  let settingsService: MockType<SettingsService>;\n  let preSetupDatabaseDiscoveryService: MockType<PreSetupDatabaseDiscoveryService>;\n  let autoDatabaseDiscoveryService: MockType<AutoDatabaseDiscoveryService>;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    mockServerConfig.buildType = 'ELECTRON';\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalDatabaseDiscoveryService,\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n        {\n          provide: PreSetupDatabaseDiscoveryService,\n          useFactory: mockPreSetupDatabaseDiscoveryService,\n        },\n        {\n          provide: AutoDatabaseDiscoveryService,\n          useFactory: mockAutoDatabaseDiscoveryService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalDatabaseDiscoveryService);\n    settingsService = module.get(SettingsService);\n    preSetupDatabaseDiscoveryService = module.get(\n      PreSetupDatabaseDiscoveryService,\n    );\n    autoDatabaseDiscoveryService = module.get(AutoDatabaseDiscoveryService);\n  });\n\n  describe('discover', () => {\n    it('should skip when buildType = REDIS_STACK', async () => {\n      mockServerConfig.buildType = 'REDIS_STACK';\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual(\n        undefined,\n      );\n\n      expect(settingsService.getAppSettings).not.toHaveBeenCalled();\n      expect(preSetupDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n      expect(autoDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n\n    it('should skip there is no eula consent', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({});\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual(\n        undefined,\n      );\n\n      expect(settingsService.getAppSettings).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(preSetupDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n      expect(autoDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n\n    it('should discover pre setup databases', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        agreements: { eula: true },\n      });\n      preSetupDatabaseDiscoveryService.discover.mockResolvedValueOnce({\n        discovered: 3,\n      });\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual(\n        undefined,\n      );\n\n      expect(settingsService.getAppSettings).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(preSetupDatabaseDiscoveryService.discover).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(autoDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n\n    it('should discover pre setup databases and not auto discover on first start', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        agreements: { eula: true },\n      });\n      preSetupDatabaseDiscoveryService.discover.mockResolvedValueOnce({\n        discovered: 3,\n      });\n\n      await expect(\n        service.discover(mockSessionMetadata, true),\n      ).resolves.toEqual(undefined);\n\n      expect(settingsService.getAppSettings).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(preSetupDatabaseDiscoveryService.discover).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(autoDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n\n    it('should not run auto discover when no pre setup databases discovered but it is not first start', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        agreements: { eula: true },\n      });\n      preSetupDatabaseDiscoveryService.discover.mockResolvedValueOnce({\n        discovered: 0,\n      });\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual(\n        undefined,\n      );\n\n      expect(settingsService.getAppSettings).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(preSetupDatabaseDiscoveryService.discover).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(autoDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n\n    it('should run auto discover when no pre setup databases discovered and it is first start', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        agreements: { eula: true },\n      });\n      preSetupDatabaseDiscoveryService.discover.mockResolvedValueOnce({\n        discovered: 0,\n      });\n\n      await expect(\n        service.discover(mockSessionMetadata, true),\n      ).resolves.toEqual(undefined);\n\n      expect(settingsService.getAppSettings).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(preSetupDatabaseDiscoveryService.discover).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(autoDatabaseDiscoveryService.discover).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n    });\n\n    it('should not fail inn case of any error', async () => {\n      settingsService.getAppSettings.mockRejectedValueOnce(new Error());\n\n      await expect(\n        service.discover(mockSessionMetadata, true),\n      ).resolves.toEqual(undefined);\n\n      expect(settingsService.getAppSettings).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n      expect(preSetupDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n      expect(autoDatabaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/local.database-discovery.service.ts",
    "content": "import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { SessionMetadata } from 'src/common/models';\nimport { PreSetupDatabaseDiscoveryService } from 'src/modules/database-discovery/pre-setup.database-discovery.service';\nimport { AutoDatabaseDiscoveryService } from 'src/modules/database-discovery/auto.database-discovery.service';\nimport { DatabaseDiscoveryService } from 'src/modules/database-discovery/database-discovery.service';\nimport { SettingsService } from 'src/modules/settings/settings.service';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Injectable()\nexport class LocalDatabaseDiscoveryService extends DatabaseDiscoveryService {\n  private logger = new Logger('LocalDatabaseDiscoveryService');\n\n  constructor(\n    @Inject(forwardRef(() => SettingsService))\n    private readonly settingsService: SettingsService,\n    private readonly autoDatabaseDiscoveryService: AutoDatabaseDiscoveryService,\n    private readonly preSetupDatabaseDiscoveryService: PreSetupDatabaseDiscoveryService,\n  ) {\n    super();\n  }\n\n  async discover(\n    sessionMetadata: SessionMetadata,\n    firstRun?: boolean,\n  ): Promise<void> {\n    try {\n      // No need to auto discover for Redis Stack - quick check\n      if (SERVER_CONFIG.buildType === 'REDIS_STACK') {\n        return;\n      }\n\n      // check agreements to understand if it is first launch\n      const settings =\n        await this.settingsService.getAppSettings(sessionMetadata);\n\n      if (!settings?.agreements?.eula) {\n        return;\n      }\n\n      const { discovered } =\n        await this.preSetupDatabaseDiscoveryService.discover(sessionMetadata);\n\n      if (!discovered && firstRun) {\n        await this.autoDatabaseDiscoveryService.discover(sessionMetadata);\n      }\n    } catch (e) {\n      // ignore error\n      this.logger.error('Unable to discover databases', e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/pre-setup.database-discovery.service.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport * as preSetupDiscoveryUtil from 'src/modules/database-discovery/utils/pre-setup.discovery.util';\nimport {\n  mockCaCertificateRepository,\n  mockClientCertificateRepository,\n  mockDatabaseRepository,\n  mockDatabaseToImportFromEnvsPrepared,\n  mockDatabaseToImportFromFilePrepared,\n  mockDatabaseToImportWithCertsFromEnvsPrepared,\n  mockDatabaseToImportWithCertsFromFilePrepared,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport config, { Config } from 'src/utils/config';\nimport { PreSetupDatabaseDiscoveryService } from 'src/modules/database-discovery/pre-setup.database-discovery.service';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\ndescribe('PreSetupDatabaseDiscoveryService', () => {\n  let service: PreSetupDatabaseDiscoveryService;\n  let databaseRepository: MockType<DatabaseRepository>;\n  let caCertificateRepository: MockType<CaCertificateRepository>;\n  let clientCertificateRepository: MockType<ClientCertificateRepository>;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    mockServerConfig.buildType = 'ELECTRON';\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        PreSetupDatabaseDiscoveryService,\n        {\n          provide: DatabaseRepository,\n          useFactory: mockDatabaseRepository,\n        },\n        {\n          provide: CaCertificateRepository,\n          useFactory: mockCaCertificateRepository,\n        },\n        {\n          provide: ClientCertificateRepository,\n          useFactory: mockClientCertificateRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get(PreSetupDatabaseDiscoveryService);\n    databaseRepository = module.get(DatabaseRepository);\n    caCertificateRepository = module.get(CaCertificateRepository);\n    clientCertificateRepository = module.get(ClientCertificateRepository);\n  });\n\n  describe('addDatabase', () => {\n    it('should add simple database', async () => {\n      await expect(\n        service['addDatabase'](\n          mockSessionMetadata,\n          mockDatabaseToImportFromEnvsPrepared,\n        ),\n      ).resolves.toEqual(mockDatabaseToImportFromEnvsPrepared.id);\n\n      expect(caCertificateRepository.create).not.toHaveBeenCalled();\n      expect(clientCertificateRepository.create).not.toHaveBeenCalled();\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseToImportFromEnvsPrepared,\n        false,\n      );\n    });\n\n    it('should add database with certificates', async () => {\n      await expect(\n        service['addDatabase'](\n          mockSessionMetadata,\n          mockDatabaseToImportWithCertsFromEnvsPrepared,\n        ),\n      ).resolves.toEqual(mockDatabaseToImportWithCertsFromEnvsPrepared.id);\n\n      expect(caCertificateRepository.create).toHaveBeenCalledWith(\n        mockDatabaseToImportWithCertsFromEnvsPrepared.caCert,\n        false,\n      );\n      expect(clientCertificateRepository.create).toHaveBeenCalledWith(\n        mockDatabaseToImportWithCertsFromEnvsPrepared.clientCert,\n        false,\n      );\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockDatabaseToImportWithCertsFromEnvsPrepared,\n          caCert: {\n            id: mockDatabaseToImportWithCertsFromEnvsPrepared.caCert.id,\n          },\n          clientCert: {\n            id: mockDatabaseToImportWithCertsFromEnvsPrepared.clientCert.id,\n          },\n        },\n        false,\n      );\n    });\n\n    it('should not fail on error', async () => {\n      databaseRepository.create.mockRejectedValueOnce(\n        new Error('some error during adding database'),\n      );\n      await expect(\n        service['addDatabase'](\n          mockSessionMetadata,\n          mockDatabaseToImportFromEnvsPrepared,\n        ),\n      ).resolves.toEqual(null);\n    });\n  });\n\n  describe('cleanupPreSetupData', () => {\n    const mockExcludeIds = ['_1'];\n\n    it('should cleanup databases and certificates', async () => {\n      await expect(\n        service['cleanupPreSetupData'](mockSessionMetadata, ['_1']),\n      ).resolves.toEqual(undefined);\n\n      expect(caCertificateRepository.cleanupPreSetup).toHaveBeenCalledWith(\n        mockExcludeIds,\n      );\n      expect(clientCertificateRepository.cleanupPreSetup).toHaveBeenCalledWith(\n        mockExcludeIds,\n      );\n      expect(databaseRepository.cleanupPreSetup).toHaveBeenCalledWith(\n        mockExcludeIds,\n      );\n    });\n\n    it('should not fail in case of an error', async () => {\n      const mockError = new Error('Unable to cleanup data');\n      caCertificateRepository.cleanupPreSetup.mockRejectedValueOnce(mockError);\n      clientCertificateRepository.cleanupPreSetup.mockRejectedValueOnce(\n        mockError,\n      );\n      databaseRepository.cleanupPreSetup.mockRejectedValueOnce(mockError);\n\n      await expect(\n        service['cleanupPreSetupData'](mockSessionMetadata, ['_1']),\n      ).resolves.toEqual(undefined);\n    });\n  });\n\n  describe('discover', () => {\n    let addDatabaseSpy: jest.SpyInstance;\n    let discoverEnvDatabasesToAddSpy: jest.SpyInstance;\n    let discoverFileDatabasesToAddSpy: jest.SpyInstance;\n    let cleanupPreSetupDataSpy: jest.SpyInstance;\n\n    beforeEach(async () => {\n      addDatabaseSpy = jest.spyOn(service as any, 'addDatabase');\n      addDatabaseSpy.mockResolvedValue('_1');\n      discoverEnvDatabasesToAddSpy = jest.spyOn(\n        preSetupDiscoveryUtil,\n        'discoverEnvDatabasesToAdd',\n      );\n      discoverEnvDatabasesToAddSpy.mockResolvedValue([\n        mockDatabaseToImportFromEnvsPrepared,\n        mockDatabaseToImportWithCertsFromEnvsPrepared,\n      ]);\n      discoverFileDatabasesToAddSpy = jest.spyOn(\n        preSetupDiscoveryUtil,\n        'discoverFileDatabasesToAdd',\n      );\n      discoverFileDatabasesToAddSpy.mockResolvedValue([\n        mockDatabaseToImportFromFilePrepared,\n        mockDatabaseToImportWithCertsFromFilePrepared,\n      ]);\n      cleanupPreSetupDataSpy = jest.spyOn(\n        service as any,\n        'cleanupPreSetupData',\n      );\n    });\n\n    it('should skip when buildType = REDIS_STACK', async () => {\n      mockServerConfig.buildType = 'REDIS_STACK';\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual({\n        discovered: 0,\n      });\n\n      expect(discoverEnvDatabasesToAddSpy).not.toHaveBeenCalled();\n      expect(discoverFileDatabasesToAddSpy).not.toHaveBeenCalled();\n      expect(addDatabaseSpy).not.toHaveBeenCalled();\n      expect(cleanupPreSetupDataSpy).not.toHaveBeenCalled();\n    });\n\n    it('should not try to add database when nothing discovered but run cleanup function', async () => {\n      discoverEnvDatabasesToAddSpy.mockResolvedValueOnce([]);\n      discoverFileDatabasesToAddSpy.mockResolvedValueOnce([]);\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual({\n        discovered: 0,\n      });\n\n      expect(addDatabaseSpy).not.toHaveBeenCalled();\n      expect(cleanupPreSetupDataSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        [],\n      );\n    });\n\n    it('should add 4 databases', async () => {\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual({\n        discovered: 4,\n      });\n\n      expect(addDatabaseSpy).toHaveBeenCalledTimes(4);\n      expect(cleanupPreSetupDataSpy).toHaveBeenCalledWith(mockSessionMetadata, [\n        '_1',\n        '_1',\n        '_1',\n        '_1',\n      ]);\n    });\n\n    it('should add 3 out of 4 databases due to unique by id (env takes precedence)', async () => {\n      discoverFileDatabasesToAddSpy.mockResolvedValue([\n        {\n          ...mockDatabaseToImportFromFilePrepared,\n          id: mockDatabaseToImportFromEnvsPrepared.id,\n        },\n        mockDatabaseToImportWithCertsFromFilePrepared,\n      ]);\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual({\n        discovered: 3,\n      });\n\n      expect(addDatabaseSpy).toHaveBeenCalledTimes(3);\n      expect(addDatabaseSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        mockDatabaseToImportFromEnvsPrepared,\n      );\n      expect(addDatabaseSpy).toHaveBeenNthCalledWith(\n        2,\n        mockSessionMetadata,\n        mockDatabaseToImportWithCertsFromEnvsPrepared,\n      );\n      expect(addDatabaseSpy).toHaveBeenNthCalledWith(\n        3,\n        mockSessionMetadata,\n        mockDatabaseToImportWithCertsFromFilePrepared,\n      );\n      expect(cleanupPreSetupDataSpy).toHaveBeenCalledWith(mockSessionMetadata, [\n        '_1',\n        '_1',\n        '_1',\n      ]);\n    });\n\n    it('should add 2 out of 4 database filtered by null', async () => {\n      addDatabaseSpy.mockResolvedValueOnce(null);\n      addDatabaseSpy.mockResolvedValueOnce(null);\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual({\n        discovered: 2,\n      });\n\n      expect(addDatabaseSpy).toHaveBeenCalledTimes(4);\n      expect(cleanupPreSetupDataSpy).toHaveBeenCalledWith(mockSessionMetadata, [\n        '_1',\n        '_1',\n      ]);\n    });\n\n    it('should not fail in case of an error', async () => {\n      addDatabaseSpy.mockRejectedValueOnce(new Error('some error'));\n\n      await expect(service.discover(mockSessionMetadata)).resolves.toEqual({\n        discovered: 0,\n      });\n\n      expect(cleanupPreSetupDataSpy).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/pre-setup.database-discovery.service.ts",
    "content": "import { uniqBy, isNull } from 'lodash';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { CaCertificateRepository } from 'src/modules/certificate/repositories/ca-certificate.repository';\nimport { ClientCertificateRepository } from 'src/modules/certificate/repositories/client-certificate.repository';\nimport { Injectable, Logger } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { SessionMetadata } from 'src/common/models';\nimport { Database } from 'src/modules/database/models/database';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { classToClass } from 'src/utils';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport {\n  discoverEnvDatabasesToAdd,\n  discoverFileDatabasesToAdd,\n} from 'src/modules/database-discovery/utils/pre-setup.discovery.util';\n\nconst DIR_CONFIG = config.get('dir_path') as Config['dir_path'];\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Injectable()\nexport class PreSetupDatabaseDiscoveryService {\n  private logger = new Logger('LocalPreSetupDatabaseDiscoveryService');\n\n  constructor(\n    private readonly databaseRepository: DatabaseRepository,\n    private readonly caCertificateRepository: CaCertificateRepository,\n    private readonly clientCertificateRepository: ClientCertificateRepository,\n  ) {}\n\n  private async addDatabase(\n    sessionMetadata: SessionMetadata,\n    toAdd: Database,\n  ): Promise<string> {\n    try {\n      const database = classToClass(Database, toAdd);\n\n      if (database.caCert) {\n        await this.caCertificateRepository.create(database.caCert, false);\n        database.caCert = { id: database.caCert.id } as CaCertificate;\n      }\n\n      if (database.clientCert) {\n        await this.clientCertificateRepository.create(\n          database.clientCert,\n          false,\n        );\n        database.clientCert = {\n          id: database.clientCert.id,\n        } as ClientCertificate;\n      }\n\n      await this.databaseRepository.create(sessionMetadata, database, false);\n\n      return database.id;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  private async cleanupPreSetupData(\n    sessionMetadata: SessionMetadata,\n    excludeIds: string[],\n  ): Promise<void> {\n    await Promise.all([\n      // cleanup databases\n      this.databaseRepository.cleanupPreSetup(excludeIds).catch((e) => {\n        this.logger.warn(\n          'Unable to cleanup pre setup databases',\n          e,\n          sessionMetadata,\n        );\n      }),\n      // cleanup databases\n      this.caCertificateRepository.cleanupPreSetup(excludeIds).catch((e) => {\n        this.logger.warn(\n          'Unable to cleanup pre setup CA certificates',\n          e,\n          sessionMetadata,\n        );\n      }),\n      // cleanup user certificates\n      this.clientCertificateRepository\n        .cleanupPreSetup(excludeIds)\n        .catch((e) => {\n          this.logger.warn(\n            'Unable to cleanup pre setup user certificates',\n            e,\n            sessionMetadata,\n          );\n        }),\n    ]);\n  }\n\n  async discover(\n    sessionMetadata: SessionMetadata,\n  ): Promise<{ discovered: number }> {\n    let addedIds: string[] = [];\n\n    try {\n      // no need to auto discover for Redis Stack\n      if (SERVER_CONFIG.buildType === 'REDIS_STACK') {\n        return { discovered: 0 };\n      }\n\n      const envDatabasesToAdd = await discoverEnvDatabasesToAdd();\n      const fileDatabasesToAdd = await discoverFileDatabasesToAdd(\n        DIR_CONFIG.preSetupDatabases,\n      );\n      const databasesToAdd = uniqBy(\n        [...envDatabasesToAdd, ...fileDatabasesToAdd],\n        'id',\n      );\n\n      if (databasesToAdd.length > 0) {\n        addedIds = (\n          await Promise.all(\n            databasesToAdd.map((database) =>\n              this.addDatabase(sessionMetadata, database),\n            ),\n          )\n        ).filter((v) => !isNull(v));\n      }\n\n      await this.cleanupPreSetupData(sessionMetadata, addedIds);\n    } catch (e) {\n      // ignore error\n      this.logger.error('Unable to discover databases', e);\n    }\n\n    return {\n      discovered: addedIds.length,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/utils/autodiscovery.util.spec.ts",
    "content": "import {\n  mockLinuxNetstat,\n  mockMacNetstat,\n  mockWinNetstat,\n} from 'src/__mocks__';\nimport * as os from 'os';\nimport * as ch from 'child_process';\nimport * as net from 'net';\nimport * as events from 'events';\nimport * as stream from 'stream';\nimport { ChildProcess } from 'child_process';\nimport * as autodiscoveryUtility from './autodiscovery.util';\n\njest.mock('os', () => ({\n  ...(jest.requireActual('os') as object),\n  __esModule: true,\n  type: jest.fn(),\n}));\n\njest.mock('child_process', () => ({\n  ...(jest.requireActual('child_process') as object),\n  __esModule: true,\n  spawn: jest.fn(),\n}));\n\njest.mock('net', () => ({\n  ...(jest.requireActual('net') as object),\n  __esModule: true,\n  createConnection: jest.fn(),\n}));\n\nconst mockStdout = new events.EventEmitter() as stream.Readable;\nconst mockChildProcess = new events.EventEmitter() as ChildProcess;\nmockChildProcess['stdout'] = mockStdout;\n\nconst mockSocket = new events.EventEmitter() as net.Socket;\nmockSocket.end = jest.fn();\n\ndescribe('getSpawnArgs', () => {\n  const getSpawnArgsTests = [\n    {\n      name: 'Linux',\n      before: () => (os.type as jest.Mock).mockReturnValue('Linux'),\n      output: ['netstat', ['-anpt']],\n    },\n    {\n      name: 'Darwin',\n      before: () => (os.type as jest.Mock).mockReturnValue('Darwin'),\n      output: ['netstat', ['-anvp', 'tcp']],\n    },\n    {\n      name: 'Windows_NT',\n      before: () => (os.type as jest.Mock).mockReturnValue('Windows_NT'),\n      output: ['netstat.exe', ['-a', '-n', '-o']],\n    },\n  ];\n\n  getSpawnArgsTests.forEach((test) => {\n    it(`Should return args for ${test.name}`, async () => {\n      await test.before();\n\n      const result = autodiscoveryUtility.getSpawnArgs();\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\ndescribe('getRunningProcesses', () => {\n  beforeEach(() => {\n    (os.type as jest.Mock).mockReturnValue('Linux');\n    (ch.spawn as jest.Mock).mockReturnValue(mockChildProcess);\n  });\n  const getRunningProcessesTests = [\n    {\n      name: 'netstat entries array for Linux',\n      emit: () => {\n        mockStdout.emit('data', mockLinuxNetstat);\n        mockStdout.emit('end');\n      },\n      output: mockLinuxNetstat.split('\\n'),\n    },\n  ];\n\n  getRunningProcessesTests.forEach((test) => {\n    it(`Should return ${test.name}`, (done) => {\n      autodiscoveryUtility.getRunningProcesses().then((result) => {\n        expect(result).toEqual(test.output);\n        done();\n      });\n\n      test.emit();\n    });\n  });\n\n  it('Should throw an error for unsupported platform', async () => {\n    (os.type as jest.Mock).mockReturnValueOnce('custom_os');\n\n    try {\n      await autodiscoveryUtility.getRunningProcesses();\n      fail();\n    } catch (e) {\n      expect(e.message).toEqual('Unsupported operation system');\n    }\n  });\n\n  it('Should throw an error if child process fail', (done) => {\n    autodiscoveryUtility\n      .getRunningProcesses()\n      .then(() => {\n        fail();\n      })\n      .catch((e) => {\n        expect(e.message).toEqual('Child process error');\n        done();\n      });\n\n    mockChildProcess.emit('error', new Error('Child process error'));\n  });\n});\n\ndescribe('getTCPEndpoints', () => {\n  const getTCPEndpointsTests = [\n    {\n      name: 'win output',\n      input: mockWinNetstat.split('\\n'),\n      output: [\n        { host: '127.0.0.1', port: 5000 },\n        { host: '127.0.0.1', port: 6379 },\n        { host: '127.0.0.1', port: 6380 },\n        { host: '127.0.0.1', port: 135 },\n        { host: '127.0.0.1', port: 445 },\n        { host: '127.0.0.1', port: 808 },\n        { host: '127.0.0.1', port: 2701 },\n      ],\n    },\n    {\n      name: 'linux output',\n      input: mockLinuxNetstat.split('\\n'),\n      output: [\n        { host: '127.0.0.1', port: 5000 },\n        { host: '127.0.0.1', port: 6379 },\n        { host: '127.0.0.1', port: 6380 },\n        { host: '127.0.0.1', port: 28100 },\n        { host: '127.0.0.1', port: 8100 },\n        { host: '127.0.0.1', port: 8101 },\n        { host: '127.0.0.1', port: 8102 },\n        { host: '127.0.0.1', port: 8103 },\n        { host: '127.0.0.1', port: 8200 },\n      ],\n    },\n    {\n      name: 'mac output',\n      input: mockMacNetstat.split('\\n'),\n      output: [\n        { host: '127.0.0.1', port: 5000 },\n        { host: '127.0.0.1', port: 6379 },\n        { host: '127.0.0.1', port: 6380 },\n        { host: '127.0.0.1', port: 5002 },\n        { host: '127.0.0.1', port: 52167 },\n      ],\n    },\n  ];\n\n  getTCPEndpointsTests.forEach((test) => {\n    it(`Should return endpoints to test ${test.name}`, async () => {\n      const result = autodiscoveryUtility.getTCPEndpoints(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\ndescribe('testEndpoint', () => {\n  beforeEach(() => {\n    (net.createConnection as jest.Mock).mockReturnValue(mockSocket);\n  });\n  const testEndpointTests = [\n    {\n      name: 'endpoint if +PONG received',\n      emit: () => {\n        mockSocket.emit('data', '+PONG');\n      },\n      input: { host: 'localhost', port: 5000 },\n      output: { host: 'localhost', port: 5000 },\n    },\n    {\n      name: 'null if no +PONG received',\n      emit: () => {\n        mockSocket.emit('data', 'something else');\n      },\n      input: { host: 'localhost', port: 5000 },\n      output: null,\n    },\n    {\n      name: 'null if error happened',\n      emit: () => {\n        mockSocket.emit('error', new Error('some error'));\n      },\n      input: { host: 'localhost', port: 5000 },\n      output: null,\n    },\n    {\n      name: 'null if no response in 1s',\n      emit: () => {},\n      input: { host: 'localhost', port: 5000 },\n      output: null,\n    },\n  ];\n\n  testEndpointTests.forEach((test) => {\n    it(`Should return ${test.name}`, (done) => {\n      autodiscoveryUtility.testEndpoint(test.input).then((result) => {\n        expect(result).toEqual(test.output);\n        done();\n      });\n\n      test.emit();\n    });\n  });\n});\n\ndescribe('getAvailableEndpoints', () => {\n  beforeEach(() => {\n    const getRunningProcessesSpy = jest.spyOn(\n      autodiscoveryUtility,\n      'getRunningProcesses',\n    );\n    getRunningProcessesSpy.mockResolvedValue(['']);\n    (net.createConnection as jest.Mock).mockReturnValue(mockSocket);\n  });\n  const getAvailableEndpointsTests = [\n    {\n      name: 'only available endpoints',\n      mock: () => {\n        const spy = jest.spyOn(autodiscoveryUtility, 'testEndpoint');\n        const getTCPEndpointsSpy = jest.spyOn(\n          autodiscoveryUtility,\n          'getTCPEndpoints',\n        );\n        getTCPEndpointsSpy.mockReturnValueOnce([\n          { host: 'localhost', port: 5000 },\n          { host: 'localhost', port: 5001 },\n        ]);\n        spy.mockResolvedValueOnce(null);\n        spy.mockResolvedValueOnce({ host: 'localhost', port: 5001 });\n      },\n      output: [{ host: 'localhost', port: 5001 }],\n    },\n  ];\n\n  getAvailableEndpointsTests.forEach((test) => {\n    it(`Should return ${test.name}`, async () => {\n      await test.mock();\n\n      const result = await autodiscoveryUtility.getAvailableEndpoints();\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/utils/autodiscovery.util.ts",
    "content": "import * as os from 'os';\nimport * as net from 'net';\nimport { spawn } from 'child_process';\nimport { isObject } from 'lodash';\nimport { IEndpoint } from 'src/common/models';\n\n/**\n * Get \"netstat\" command and args based on operation system\n */\nexport const getSpawnArgs = (): [string, string[]] => {\n  switch (os.type()) {\n    case 'Linux':\n      return ['netstat', ['-anpt']];\n    case 'Darwin':\n      return ['netstat', ['-anvp', 'tcp']];\n    case 'Windows_NT':\n      return ['netstat.exe', ['-a', '-n', '-o']];\n    default:\n      throw new Error('Unsupported operation system');\n  }\n};\n\n/**\n * Get list of processes running on local machine\n */\nexport const getRunningProcesses = async (): Promise<string[]> =>\n  new Promise((resolve, reject) => {\n    try {\n      let stdoutData = '';\n      const proc = spawn(...getSpawnArgs());\n\n      proc.stdout.on('data', (data) => {\n        stdoutData += data.toString();\n      });\n\n      proc.on('error', (e) => {\n        reject(e);\n      });\n\n      proc.stdout.on('end', () => {\n        resolve(stdoutData.split('\\n'));\n      });\n    } catch (e) {\n      reject(e);\n    }\n  });\n\n/**\n * Return list of unique endpoints (host is hardcoded) to test\n * @param processes\n */\nexport const getTCPEndpoints = (processes: string[]): IEndpoint[] => {\n  const regExp =\n    /\\s((\\d+\\.\\d+\\.\\d+\\.\\d+|\\*)[:.]|([0-9a-fA-F\\][]{0,4}[.:]){1,8})(\\d+)\\s/;\n  const endpoints = new Map();\n\n  processes.forEach((line) => {\n    const match = line.match(regExp);\n\n    if (match) {\n      endpoints.set(match[4], {\n        host: '127.0.0.1',\n        port: parseInt(match[4], 10),\n      });\n    }\n  });\n\n  return [...endpoints.values()];\n};\n\n/**\n * Check RESP protocol response from tcp connection\n * @param endpoint\n */\nexport const testEndpoint = async (endpoint: IEndpoint): Promise<IEndpoint> =>\n  new Promise((resolve) => {\n    const client = net.createConnection(\n      {\n        host: endpoint.host,\n        port: endpoint.port,\n      },\n      () => {\n        client.write('PING\\r\\n');\n      },\n    );\n\n    client.on('data', (data) => {\n      client.end();\n\n      if (data.toString().startsWith('+PONG')) {\n        resolve(endpoint);\n      } else {\n        resolve(null);\n      }\n    });\n\n    client.on('error', () => {\n      resolve(null);\n    });\n\n    setTimeout(() => {\n      client.end();\n      resolve(null);\n    }, 1000);\n  });\n\n/**\n * Get endpoints that we are able to connect and receive expected RESP protocol response\n */\nexport const getAvailableEndpoints = async (): Promise<IEndpoint[]> => {\n  const endpoints = getTCPEndpoints(await getRunningProcesses());\n  return (await Promise.all(endpoints.map(testEndpoint))).filter(isObject);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/utils/pre-setup.discovery.util.spec.ts",
    "content": "import { when } from 'jest-when';\nimport {\n  cleanupTestEnvs,\n  mockDatabaseToImportFromEnvsInput,\n  mockDatabaseToImportFromEnvsPrepared,\n  mockDatabaseToImportFromFileInput,\n  mockDatabaseToImportFromFilePrepared,\n  mockDatabaseToImportWithCertsFromEnvsInput,\n  mockDatabaseToImportWithCertsFromEnvsPrepared,\n  mockDatabaseToImportWithCertsFromFileInput,\n  mockDatabaseToImportWithCertsFromFilePrepared,\n  mockDefaultDatabaseFields,\n} from 'src/__mocks__';\nimport * as fsExtra from 'fs-extra';\nimport { Database } from 'src/modules/database/models/database';\nimport * as preSetupUtil from './pre-setup.discovery.util';\nimport {\n  scanProcessEnv,\n  populateDefaultValues,\n  getCertificateData,\n  prepareDatabaseFromEnvs,\n  discoverEnvDatabasesToAdd,\n  discoverFileDatabasesToAdd,\n} from './pre-setup.discovery.util';\n\ndescribe('preSetupDiscoveryUtil', () => {\n  let fsReadFileSpy: jest.SpyInstance;\n  let fsPathExistsSpy: jest.SpyInstance;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    cleanupTestEnvs();\n\n    fsReadFileSpy = jest.spyOn(fsExtra, 'readFile');\n    when(fsReadFileSpy)\n      .calledWith('/ca.crt', 'utf8')\n      .mockResolvedValue(\n        Buffer.from(\n          mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n          'utf8',\n        ),\n      )\n      .calledWith('/user.crt', 'utf8')\n      .mockResolvedValue(\n        Buffer.from(\n          mockDatabaseToImportWithCertsFromEnvsInput.clientCert.certificate,\n          'utf8',\n        ),\n      )\n      .calledWith('/user.key', 'utf8')\n      .mockResolvedValue(\n        Buffer.from(\n          mockDatabaseToImportWithCertsFromEnvsInput.clientCert.key,\n          'utf8',\n        ),\n      );\n\n    fsPathExistsSpy = jest.spyOn(fsExtra, 'pathExists');\n    when(fsPathExistsSpy)\n      .calledWith('/not-existing.json')\n      .mockResolvedValue(false)\n      .calledWith('/databases.json')\n      .mockResolvedValue(true);\n  });\n\n  describe('scanProcessEnv', () => {\n    it('should return 3 discovered env names', () => {\n      process.env.RI_REDIS_HOST = 'host';\n      process.env.RI_REDIS_HOST_1 = 'host1';\n      process.env.RI_REDIS_HOST_2 = 'host2';\n      process.env.RI_REDIS_HHOST = 'host3';\n\n      expect(scanProcessEnv()).toEqual([\n        'RI_REDIS_HOST',\n        'RI_REDIS_HOST_1',\n        'RI_REDIS_HOST_2',\n      ]);\n    });\n    it('should not discover any envs and not fail', () => {\n      expect(scanProcessEnv()).toEqual([]);\n    });\n  });\n\n  describe('populateDefaultValues', () => {\n    it('should set default port and calculate database name based on host and port', () => {\n      expect(\n        populateDefaultValues({\n          host: 'host',\n        } as Database),\n      ).toMatchObject({\n        name: 'host:6379',\n      });\n    });\n    it('should prepare object with default values for not specified fields', () => {\n      expect(populateDefaultValues(mockDatabaseToImportFromEnvsInput)).toEqual(\n        mockDatabaseToImportFromEnvsPrepared,\n      );\n    });\n    it('should prepare object with default values and certificates', () => {\n      expect(\n        populateDefaultValues(mockDatabaseToImportWithCertsFromEnvsInput),\n      ).toEqual(mockDatabaseToImportWithCertsFromEnvsPrepared);\n    });\n    it('should prepare object with default values for not specified fields', () => {\n      expect(populateDefaultValues(mockDatabaseToImportFromFileInput)).toEqual(\n        mockDatabaseToImportFromFilePrepared,\n      );\n    });\n    it('should prepare object with default values and certificates (from file flow)', () => {\n      expect(\n        populateDefaultValues(mockDatabaseToImportWithCertsFromFileInput),\n      ).toEqual(mockDatabaseToImportWithCertsFromFilePrepared);\n    });\n  });\n\n  describe('getCertificateData', () => {\n    it('should get base64 certificate from env', async () => {\n      process.env.RI_REDIS_TLS_CA_BASE64_1 = Buffer.from(\n        mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n        'utf8',\n      ).toString('base64');\n\n      await expect(\n        getCertificateData('RI_REDIS_TLS_CA', '_1'),\n      ).resolves.toEqual(\n        mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n      );\n      expect(fsReadFileSpy).not.toHaveBeenCalled();\n    });\n    it('should read certificate from path', async () => {\n      process.env.RI_REDIS_TLS_CA_PATH_1 = '/ca.crt';\n\n      await expect(\n        getCertificateData('RI_REDIS_TLS_CA', '_1'),\n      ).resolves.toEqual(\n        mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n      );\n      expect(fsReadFileSpy).toHaveBeenCalledWith('/ca.crt', 'utf8');\n    });\n    it('should return null and not fail when there is an error while reading file', async () => {\n      process.env.RI_REDIS_TLS_CA_PATH_1 = '/path/ca.crt';\n      fsReadFileSpy.mockRejectedValueOnce(new Error('read file error'));\n      await expect(\n        getCertificateData('RI_REDIS_TLS_CA', '_1'),\n      ).resolves.toEqual(null);\n      expect(fsReadFileSpy).toHaveBeenCalledWith('/path/ca.crt', 'utf8');\n    });\n    it('should return null when no proper envs specified', async () => {\n      await expect(\n        getCertificateData('RI_REDIS_TLS_CA', '_1'),\n      ).resolves.toEqual(null);\n      expect(fsReadFileSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('prepareDatabaseFromEnvs', () => {\n    it('should discover database without specific id provided', async () => {\n      process.env.RI_REDIS_HOST = mockDatabaseToImportFromEnvsInput.host;\n      process.env.RI_REDIS_POST = `${mockDatabaseToImportFromEnvsInput.port}`;\n      process.env.RI_REDIS_ALIAS = mockDatabaseToImportFromEnvsInput.name;\n\n      await expect(prepareDatabaseFromEnvs('RI_REDIS_HOST')).resolves.toEqual(\n        mockDatabaseToImportFromEnvsPrepared,\n      );\n    });\n    it('should return null because of validation error', async () => {\n      // no host field\n      process.env.RI_REDIS_POST = `${mockDatabaseToImportFromEnvsInput.port}`;\n      process.env.RI_REDIS_ALIAS = mockDatabaseToImportFromEnvsInput.name;\n\n      await expect(prepareDatabaseFromEnvs('RI_REDIS_HOST')).resolves.toEqual(\n        null,\n      );\n    });\n    it('should return database with minimal fields specified via envs', async () => {\n      process.env.RI_REDIS_HOST = mockDatabaseToImportFromEnvsInput.host;\n\n      await expect(prepareDatabaseFromEnvs('RI_REDIS_HOST')).resolves.toEqual({\n        ...mockDatabaseToImportFromEnvsPrepared,\n        port: 6379, // default port\n        name: `${mockDatabaseToImportFromEnvsPrepared.host}:6379`, // auto generated name\n      });\n    });\n    it('should discover database with certs in base64 format', async () => {\n      process.env.RI_REDIS_HOST_1 =\n        mockDatabaseToImportWithCertsFromEnvsInput.host;\n      process.env.RI_REDIS_PORT_1 = `${mockDatabaseToImportWithCertsFromEnvsInput.port}`;\n      process.env.RI_REDIS_ALIAS_1 =\n        mockDatabaseToImportWithCertsFromEnvsInput.name;\n      process.env.RI_REDIS_TLS_1 = 'true';\n      process.env.RI_REDIS_TLS_CA_BASE64_1 = Buffer.from(\n        mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n        'utf8',\n      ).toString('base64');\n      process.env.RI_REDIS_TLS_CERT_BASE64_1 = Buffer.from(\n        mockDatabaseToImportWithCertsFromEnvsInput.clientCert.certificate,\n        'utf8',\n      ).toString('base64');\n      process.env.RI_REDIS_TLS_KEY_BASE64_1 = Buffer.from(\n        mockDatabaseToImportWithCertsFromEnvsInput.clientCert.key,\n        'utf8',\n      ).toString('base64');\n\n      await expect(prepareDatabaseFromEnvs('RI_REDIS_HOST_1')).resolves.toEqual(\n        mockDatabaseToImportWithCertsFromEnvsPrepared,\n      );\n    });\n    it('should discover database with certs from file', async () => {\n      process.env.RI_REDIS_HOST_1 =\n        mockDatabaseToImportWithCertsFromEnvsInput.host;\n      process.env.RI_REDIS_PORT_1 = `${mockDatabaseToImportWithCertsFromEnvsInput.port}`;\n      process.env.RI_REDIS_ALIAS_1 =\n        mockDatabaseToImportWithCertsFromEnvsInput.name;\n      process.env.RI_REDIS_TLS_1 = 'true';\n      process.env.RI_REDIS_TLS_CA_PATH_1 = '/ca.crt';\n      process.env.RI_REDIS_TLS_CERT_PATH_1 = '/user.crt';\n      process.env.RI_REDIS_TLS_KEY_PATH_1 = '/user.key';\n\n      await expect(prepareDatabaseFromEnvs('RI_REDIS_HOST_1')).resolves.toEqual(\n        mockDatabaseToImportWithCertsFromEnvsPrepared,\n      );\n    });\n  });\n\n  describe('discoverEnvDatabasesToAdd', () => {\n    it('should discover 2 out of 3 env databases due to validation', async () => {\n      process.env.RI_REDIS_HOST = 'host1';\n      process.env.RI_REDIS_HOST_1 = 'host2';\n      process.env.RI_REDIS_HOST_2 = '';\n\n      await expect(discoverEnvDatabasesToAdd()).resolves.toEqual([\n        {\n          ...mockDefaultDatabaseFields,\n          id: '0',\n          host: 'host1',\n          port: 6379,\n          name: 'host1:6379',\n        },\n        {\n          ...mockDefaultDatabaseFields,\n          id: '_1',\n          host: 'host2',\n          port: 6379,\n          name: 'host2:6379',\n        },\n      ]);\n    });\n    it('should not fail in case of an error', async () => {\n      const scanProcessEnvSpy = jest.spyOn(preSetupUtil, 'scanProcessEnv');\n      scanProcessEnvSpy.mockImplementation(() => {\n        throw new Error('Some error');\n      });\n\n      await expect(discoverEnvDatabasesToAdd()).resolves.toEqual([]);\n    });\n  });\n\n  describe('discoverFileDatabasesToAdd', () => {\n    it(\"should return null when file doesn't exist\", async () => {\n      await expect(\n        discoverFileDatabasesToAdd('/not-existing.json'),\n      ).resolves.toEqual([]);\n      expect(fsReadFileSpy).not.toHaveBeenCalled();\n    });\n    it('should return null when unexpected file received', async () => {\n      fsReadFileSpy.mockResolvedValueOnce(Buffer.from('not a \"\" json {}'));\n      await expect(\n        discoverFileDatabasesToAdd('/databases.json'),\n      ).resolves.toEqual([]);\n      expect(fsReadFileSpy).toHaveBeenCalledWith('/databases.json', 'utf8');\n    });\n    it('should prepare 2 out of 3 database because of validation error', async () => {\n      fsReadFileSpy.mockResolvedValueOnce(\n        Buffer.from(\n          JSON.stringify([\n            {\n              password: 'incorrect database',\n            },\n            mockDatabaseToImportFromFileInput,\n            mockDatabaseToImportWithCertsFromFileInput,\n          ]),\n        ),\n      );\n      await expect(\n        discoverFileDatabasesToAdd('/databases.json'),\n      ).resolves.toEqual([\n        mockDatabaseToImportFromFilePrepared,\n        mockDatabaseToImportWithCertsFromFilePrepared,\n      ]);\n      expect(fsReadFileSpy).toHaveBeenCalledWith('/databases.json', 'utf8');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-discovery/utils/pre-setup.discovery.util.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport { pathExists, readFile } from 'fs-extra';\nimport { Database } from 'src/modules/database/models/database';\nimport {\n  Compressor,\n  ConnectionType,\n} from 'src/modules/database/entities/database.entity';\nimport { Logger } from '@nestjs/common';\nimport { Validator } from 'class-validator';\nimport { plainToClass } from 'class-transformer';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\n\nconst logger = new Logger('LocalPreSetupDatabaseDiscoveryService');\n\nconst validator = new Validator();\n\nexport const scanProcessEnv = (): string[] => {\n  const hostEnvs = [];\n\n  Object.entries(process.env).forEach(([env]) => {\n    if (env.startsWith('RI_REDIS_HOST') && process.env[env]) {\n      hostEnvs.push(env);\n    }\n  });\n\n  return hostEnvs;\n};\n\n/**\n * Explicitly set not defined data to default to be overwritten in database\n * @param database\n */\nexport const populateDefaultValues = (\n  database: Partial<Database>,\n): Database => {\n  const {\n    id = uuidv4(),\n    host,\n    port = 6379,\n    name = `${host}:${port}`,\n    db = null,\n    provider = null,\n    modules = [],\n    verifyServerCert = null,\n    ssh = null,\n    sshOptions = null,\n    tls = false,\n    tlsServername = null,\n    caCert = null,\n    clientCert = null,\n    nameFromProvider = null,\n    username = null,\n    password = null,\n    compressor = Compressor.NONE,\n  } = database;\n\n  return {\n    ...database,\n    id,\n    host,\n    port,\n    db,\n    provider,\n    modules,\n    verifyServerCert,\n    ssh,\n    sshOptions,\n    tls,\n    tlsServername,\n    caCert: caCert && {\n      ...caCert,\n      id: database.id,\n      name: `${database.id}_${name}`,\n      isPreSetup: true,\n    },\n    clientCert: clientCert && {\n      ...clientCert,\n      id: database.id,\n      name: `${database.id}_${name}`,\n      isPreSetup: true,\n    },\n    nameFromProvider,\n    username,\n    password,\n    compressor,\n    name,\n    connectionType: ConnectionType.NOT_CONNECTED,\n    isPreSetup: true,\n  };\n};\n\nexport const getCertificateData = async (\n  envPrefix: string,\n  id: string,\n): Promise<string | null> => {\n  try {\n    const base64 = process.env[`${envPrefix}_BASE64${id}`] || '';\n\n    if (base64) {\n      return Buffer.from(base64, 'base64').toString();\n    }\n\n    const path = process.env[`${envPrefix}_PATH${id}`] || '';\n\n    if (path) {\n      return (await readFile(path, 'utf8')).toString();\n    }\n  } catch (error) {\n    // ignore error\n    logger.warn('Unable to get pre setup certificate data', error, {\n      envPrefix,\n      id,\n    });\n  }\n\n  return null;\n};\n\nexport const prepareDatabaseFromEnvs = async (\n  hostEnv: string,\n): Promise<Database> => {\n  try {\n    const id = hostEnv.replace(/^RI_REDIS_HOST/, '');\n\n    const databaseToAdd: Partial<Database> = {\n      id: id || '0',\n      host: process.env[hostEnv],\n      port: parseInt(process.env[`RI_REDIS_PORT${id}`], 10) || 6379,\n      db: parseInt(process.env[`RI_REDIS_DB${id}`], 10) || 0,\n      name: process.env[`RI_REDIS_ALIAS${id}`],\n      username: process.env[`RI_REDIS_USERNAME${id}`],\n      password: process.env[`RI_REDIS_PASSWORD${id}`],\n      tls: process.env[`RI_REDIS_TLS${id}`] === 'true',\n      compressor: process.env[`RI_REDIS_COMPRESSOR${id}`] as Compressor,\n    };\n\n    // CA certificate\n    const tlsCA = await getCertificateData('RI_REDIS_TLS_CA', id);\n\n    if (tlsCA) {\n      databaseToAdd.caCert = {\n        certificate: tlsCA,\n      } as CaCertificate;\n    }\n\n    // User certificate\n    const tlsCertificate = await getCertificateData('RI_REDIS_TLS_CERT', id);\n    const tlsKey = await getCertificateData('RI_REDIS_TLS_KEY', id);\n\n    if (tlsCertificate && tlsKey) {\n      databaseToAdd.clientCert = {\n        certificate: tlsCertificate,\n        key: tlsKey,\n      } as ClientCertificate;\n      databaseToAdd.verifyServerCert = true;\n    }\n\n    const preparedDatabase = populateDefaultValues(databaseToAdd);\n\n    await validator.validateOrReject(\n      plainToClass(Database, preparedDatabase, { groups: ['security'] }),\n    );\n\n    return preparedDatabase;\n  } catch (error) {\n    // ignore error\n    logger.warn('Unable to prepare pre setup database from env', error, {\n      hostEnv,\n    });\n    return null;\n  }\n};\n\nexport const discoverEnvDatabasesToAdd = async (): Promise<Database[]> => {\n  try {\n    const hostEnvs = scanProcessEnv();\n\n    return (await Promise.all(hostEnvs.map(prepareDatabaseFromEnvs))).filter(\n      (v) => !!v,\n    );\n  } catch (e) {\n    // ignore error\n    return [];\n  }\n};\n\nexport const prepareDatabaseFromFile = async (\n  database: Database,\n): Promise<{}> => {\n  try {\n    const databaseToAdd = populateDefaultValues(database);\n\n    await validator.validateOrReject(\n      plainToClass(Database, databaseToAdd, { groups: ['security'] }),\n    );\n    return databaseToAdd;\n  } catch (error) {\n    // ignore error\n    logger.warn('Unable to prepare pre setup database from file', error, {\n      databaseId: database?.['id'],\n    });\n    return null;\n  }\n};\n\nexport const discoverFileDatabasesToAdd = async (\n  path: string,\n): Promise<Database[]> => {\n  try {\n    if (await pathExists(path)) {\n      const fileData = JSON.parse((await readFile(path, 'utf8')).toString());\n\n      return (await Promise.all(fileData.map(prepareDatabaseFromFile))).filter(\n        (v) => !!v,\n      );\n    }\n  } catch (error) {\n    // ignore error\n    logger.warn('Unable to discover pre setup databases from file', error);\n  }\n\n  return [];\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/certificate-import.service.spec.ts",
    "content": "import { when } from 'jest-when';\nimport {\n  mockCaCertificate,\n  mockCaCertificateCertificateEncrypted,\n  mockCaCertificateCertificatePlain,\n  mockCaCertificateEntity,\n  mockClientCertificate,\n  mockClientCertificateCertificateEncrypted,\n  mockClientCertificateCertificatePlain,\n  mockClientCertificateEntity,\n  mockClientCertificateKeyEncrypted,\n  mockClientCertificateKeyPlain,\n  mockEncryptionService,\n  mockRepository,\n  MockType,\n} from 'src/__mocks__';\nimport * as utils from 'src/common/utils';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  InvalidCaCertificateBodyException,\n  InvalidCertificateNameException,\n  InvalidClientCertificateBodyException,\n  InvalidClientPrivateKeyException,\n} from 'src/modules/database-import/exceptions';\nimport { CertificateImportService } from 'src/modules/database-import/certificate-import.service';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { Repository } from 'typeorm';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { getRepositoryToken } from '@nestjs/typeorm';\n\njest.mock('src/common/utils', () => ({\n  ...(jest.requireActual('src/common/utils') as object),\n  getPemBodyFromFileSync: jest.fn(),\n}));\n\ndescribe('CertificateImportService', () => {\n  let service: CertificateImportService;\n  let caRepository: MockType<Repository<CaCertificateEntity>>;\n  let clientRepository: MockType<Repository<ClientCertificateEntity>>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CertificateImportService,\n        {\n          provide: getRepositoryToken(CaCertificateEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: getRepositoryToken(ClientCertificateEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(CertificateImportService);\n    caRepository = await module.get(getRepositoryToken(CaCertificateEntity));\n    clientRepository = await module.get(\n      getRepositoryToken(ClientCertificateEntity),\n    );\n    encryptionService = await module.get(EncryptionService);\n\n    when(encryptionService.decrypt)\n      .calledWith(mockCaCertificateCertificateEncrypted, expect.anything())\n      .mockResolvedValue(mockCaCertificateCertificatePlain);\n    when(encryptionService.encrypt)\n      .calledWith(mockCaCertificateCertificatePlain)\n      .mockResolvedValue({\n        data: mockCaCertificateCertificateEncrypted,\n        encryption: mockCaCertificateEntity.encryption,\n      });\n\n    when(encryptionService.decrypt)\n      .calledWith(mockClientCertificateCertificateEncrypted, expect.anything())\n      .mockResolvedValue(mockClientCertificateCertificatePlain)\n      .calledWith(mockClientCertificateKeyEncrypted, expect.anything())\n      .mockResolvedValue(mockClientCertificateKeyPlain);\n    when(encryptionService.encrypt)\n      .calledWith(mockClientCertificateCertificatePlain)\n      .mockResolvedValue({\n        data: mockClientCertificateCertificateEncrypted,\n        encryption: mockClientCertificateEntity.encryption,\n      })\n      .calledWith(mockClientCertificateKeyPlain)\n      .mockResolvedValue({\n        data: mockClientCertificateKeyEncrypted,\n        encryption: mockClientCertificateEntity.encryption,\n      });\n  });\n\n  let determineAvailableNameSpy;\n  let getPemBodyFromFileSyncSpy;\n  let prepareCaCertificateForImportSpy;\n  let prepareClientCertificateForImportSpy;\n\n  describe('processCaCertificate', () => {\n    beforeEach(() => {\n      getPemBodyFromFileSyncSpy = jest.spyOn(\n        utils as any,\n        'getPemBodyFromFileSync',\n      );\n      getPemBodyFromFileSyncSpy.mockReturnValue(mockCaCertificate.certificate);\n      prepareCaCertificateForImportSpy = jest.spyOn(\n        service as any,\n        'prepareCaCertificateForImport',\n      );\n      prepareCaCertificateForImportSpy.mockResolvedValueOnce(mockCaCertificate);\n    });\n\n    it('should successfully process certificate', async () => {\n      const response = await service['processCaCertificate']({\n        name: mockCaCertificate.name,\n        certificate: mockCaCertificate.certificate,\n      });\n\n      expect(response).toEqual(mockCaCertificate);\n    });\n\n    it('should fail when no name defined', async () => {\n      try {\n        await service['processCaCertificate']({\n          name: undefined,\n          certificate: mockCaCertificate.certificate,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InvalidCertificateNameException);\n      }\n    });\n\n    it('should successfully process certificate from file', async () => {\n      const response = await service['processCaCertificate']({\n        certificate: '/path/ca.crt',\n      });\n\n      expect(response).toEqual(mockCaCertificate);\n      expect(prepareCaCertificateForImportSpy).toHaveBeenCalledWith({\n        name: 'ca',\n        certificate: mockCaCertificate.certificate,\n      });\n    });\n\n    it('should fail when no file found', async () => {\n      getPemBodyFromFileSyncSpy.mockImplementationOnce(() => {\n        throw new Error();\n      });\n\n      try {\n        await service['processCaCertificate']({\n          name: undefined,\n          certificate: '/path/ca.crt',\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InvalidCaCertificateBodyException);\n      }\n    });\n  });\n\n  describe('prepareCaCertificateForImport', () => {\n    beforeEach(() => {\n      determineAvailableNameSpy = jest.spyOn(\n        CertificateImportService,\n        'determineAvailableName',\n      );\n    });\n\n    it('should return existing certificate', async () => {\n      caRepository\n        .createQueryBuilder()\n        .getOne.mockResolvedValueOnce(mockCaCertificate);\n\n      const response = await service['prepareCaCertificateForImport']({\n        name: mockCaCertificate.name,\n        certificate: mockCaCertificate.certificate,\n      });\n\n      expect(response).toEqual(mockCaCertificate);\n      expect(determineAvailableNameSpy).not.toHaveBeenCalled();\n    });\n\n    it('should return new certificate', async () => {\n      caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search\n      caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for name search\n\n      const response = await service['prepareCaCertificateForImport']({\n        name: `${mockCaCertificate.name}_new`,\n        certificate: mockCaCertificate.certificate,\n      });\n\n      expect(response).toEqual({\n        ...mockCaCertificate,\n        id: undefined, // return not-existing model\n        name: `${mockCaCertificate.name}_new`,\n      });\n    });\n\n    it('should generate name with prefix', async () => {\n      caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search\n      caRepository\n        .createQueryBuilder()\n        .getOne.mockResolvedValueOnce(mockCaCertificate); // for name search 1st attempt\n      caRepository\n        .createQueryBuilder()\n        .getOne.mockResolvedValueOnce(mockCaCertificate); // for name search 2nd attempt\n      caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for name search 3rd attempt\n\n      const response = await service['prepareCaCertificateForImport']({\n        name: `${mockCaCertificate.name}_new`,\n        certificate: mockCaCertificate.certificate,\n      });\n\n      expect(response).toEqual({\n        ...mockCaCertificate,\n        id: undefined, // return not-existing model\n        name: `2_${mockCaCertificate.name}_new`,\n      });\n    });\n  });\n\n  describe('processClientCertificate', () => {\n    beforeEach(() => {\n      getPemBodyFromFileSyncSpy = jest.spyOn(\n        utils as any,\n        'getPemBodyFromFileSync',\n      );\n      prepareClientCertificateForImportSpy = jest.spyOn(\n        service as any,\n        'prepareClientCertificateForImport',\n      );\n      prepareClientCertificateForImportSpy.mockResolvedValueOnce(\n        mockClientCertificate,\n      );\n    });\n\n    it('should successfully process client certificate', async () => {\n      const response = await service['processClientCertificate']({\n        name: mockClientCertificate.name,\n        certificate: mockClientCertificate.certificate,\n        key: mockClientCertificate.key,\n      });\n\n      expect(response).toEqual(mockClientCertificate);\n    });\n\n    it('should fail when no name defined', async () => {\n      try {\n        await service['processClientCertificate']({\n          name: undefined,\n          certificate: mockClientCertificate.certificate,\n          key: mockClientCertificate.key,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InvalidCertificateNameException);\n      }\n    });\n\n    it('should successfully process certificate from file', async () => {\n      getPemBodyFromFileSyncSpy.mockReturnValueOnce(\n        mockClientCertificate.certificate,\n      );\n      getPemBodyFromFileSyncSpy.mockReturnValueOnce(mockClientCertificate.key);\n\n      const response = await service['processClientCertificate']({\n        certificate: '/path/client.crt',\n        key: '/path/key.key',\n      });\n\n      expect(response).toEqual(mockClientCertificate);\n      expect(prepareClientCertificateForImportSpy).toHaveBeenCalledWith({\n        name: 'client',\n        certificate: mockClientCertificate.certificate,\n        key: mockClientCertificate.key,\n      });\n    });\n\n    it('should fail when no cert file found', async () => {\n      getPemBodyFromFileSyncSpy.mockImplementationOnce(() => {\n        throw new Error();\n      });\n\n      try {\n        await service['processClientCertificate']({\n          name: undefined,\n          certificate: '/path/client1.crt',\n          key: '/path/key1.key',\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InvalidClientCertificateBodyException);\n      }\n    });\n\n    it('should fail when no key file found', async () => {\n      getPemBodyFromFileSyncSpy.mockReturnValueOnce(\n        mockClientCertificate.certificate,\n      );\n      getPemBodyFromFileSyncSpy.mockImplementationOnce(() => {\n        throw new Error();\n      });\n\n      try {\n        await service['processClientCertificate']({\n          name: undefined,\n          certificate: '/path/client.crt',\n          key: '/path/key.key',\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InvalidClientPrivateKeyException);\n      }\n    });\n  });\n\n  describe('prepareClientCertificateForImport', () => {\n    beforeEach(() => {\n      determineAvailableNameSpy = jest.spyOn(\n        CertificateImportService,\n        'determineAvailableName',\n      );\n    });\n\n    it('should return existing certificate', async () => {\n      clientRepository\n        .createQueryBuilder()\n        .getOne.mockResolvedValueOnce(mockClientCertificate);\n\n      const response = await service['prepareClientCertificateForImport']({\n        name: mockClientCertificate.name,\n        certificate: mockClientCertificate.certificate,\n        key: mockClientCertificate.key,\n      });\n\n      expect(response).toEqual(mockClientCertificate);\n      expect(determineAvailableNameSpy).not.toHaveBeenCalled();\n    });\n\n    it('should return new certificate', async () => {\n      clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search\n      clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for name search\n\n      const response = await service['prepareClientCertificateForImport']({\n        name: `${mockClientCertificate.name}_new`,\n        certificate: mockClientCertificate.certificate,\n        key: mockClientCertificate.key,\n      });\n\n      expect(response).toEqual({\n        ...mockClientCertificate,\n        id: undefined, // return not-existing model\n        name: `${mockClientCertificate.name}_new`,\n      });\n    });\n\n    it('should generate name with prefix', async () => {\n      clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search\n      clientRepository\n        .createQueryBuilder()\n        .getOne.mockResolvedValueOnce(mockClientCertificate); // name 1st attempt\n      clientRepository\n        .createQueryBuilder()\n        .getOne.mockResolvedValueOnce(mockClientCertificate); // name 2nd attempt\n      clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // name 3rd attempt\n\n      const response = await service['prepareClientCertificateForImport']({\n        name: `${mockClientCertificate.name}_new`,\n        certificate: mockClientCertificate.certificate,\n        key: mockClientCertificate.key,\n      });\n\n      expect(response).toEqual({\n        ...mockClientCertificate,\n        id: undefined, // return not-existing model\n        name: `2_${mockClientCertificate.name}_new`,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/certificate-import.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { CaCertificate } from 'src/modules/certificate/models/ca-certificate';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';\nimport { Repository } from 'typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { ClientCertificate } from 'src/modules/certificate/models/client-certificate';\nimport { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';\nimport { classToClass } from 'src/utils';\nimport {\n  getCertNameFromFilename,\n  getPemBodyFromFileSync,\n  isValidPemCertificate,\n  isValidPemPrivateKey,\n} from 'src/common/utils';\nimport {\n  InvalidCaCertificateBodyException,\n  InvalidCertificateNameException,\n  InvalidClientCertificateBodyException,\n  InvalidClientPrivateKeyException,\n} from 'src/modules/database-import/exceptions';\n\n@Injectable()\nexport class CertificateImportService {\n  private caCertEncryptor: ModelEncryptor;\n\n  private clientCertEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(CaCertificateEntity)\n    private readonly caCertRepository: Repository<CaCertificateEntity>,\n    @InjectRepository(ClientCertificateEntity)\n    private readonly clientCertRepository: Repository<ClientCertificateEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    this.caCertEncryptor = new ModelEncryptor(encryptionService, [\n      'certificate',\n    ]);\n    this.clientCertEncryptor = new ModelEncryptor(encryptionService, [\n      'certificate',\n      'key',\n    ]);\n  }\n\n  /**\n   * Validate data + prepare CA certificate to be imported along with new database\n   * @param cert\n   */\n  async processCaCertificate(\n    cert: Partial<CaCertificate>,\n  ): Promise<CaCertificate> {\n    let toImport: Partial<CaCertificate> = {\n      certificate: null,\n      name: cert.name,\n    };\n\n    if (isValidPemCertificate(cert.certificate)) {\n      toImport.certificate = cert.certificate;\n    } else {\n      try {\n        toImport.certificate = getPemBodyFromFileSync(cert.certificate);\n        toImport.name = getCertNameFromFilename(cert.certificate);\n      } catch (e) {\n        // ignore error\n        toImport = null;\n      }\n    }\n\n    if (\n      !toImport?.certificate ||\n      !isValidPemCertificate(toImport.certificate)\n    ) {\n      throw new InvalidCaCertificateBodyException();\n    }\n\n    if (!toImport?.name) {\n      throw new InvalidCertificateNameException();\n    }\n\n    return this.prepareCaCertificateForImport(toImport);\n  }\n\n  /**\n   * Use existing certificate if found\n   * Generate unique name for new certificate\n   * @param cert\n   * @private\n   */\n  private async prepareCaCertificateForImport(\n    cert: Partial<CaCertificate>,\n  ): Promise<CaCertificate> {\n    const encryptedModel = await this.caCertEncryptor.encryptEntity(\n      cert as CaCertificate,\n    );\n    const existing = await this.caCertRepository\n      .createQueryBuilder('c')\n      .select('c.id')\n      .where({ certificate: cert.certificate })\n      .orWhere({ certificate: encryptedModel.certificate })\n      .getOne();\n\n    if (existing) {\n      return existing;\n    }\n\n    const name = await CertificateImportService.determineAvailableName(\n      cert.name,\n      this.caCertRepository,\n    );\n\n    return classToClass(CaCertificate, {\n      ...cert,\n      name,\n    });\n  }\n\n  /**\n   * Validate data + prepare CA certificate to be imported along with new database\n   * @param cert\n   */\n  async processClientCertificate(\n    cert: Partial<ClientCertificateEntity>,\n  ): Promise<ClientCertificate> {\n    const toImport: Partial<ClientCertificate> = {\n      certificate: null,\n      key: null,\n      name: cert.name,\n    };\n\n    if (isValidPemCertificate(cert.certificate)) {\n      toImport.certificate = cert.certificate;\n    } else {\n      try {\n        toImport.certificate = getPemBodyFromFileSync(cert.certificate);\n        toImport.name = getCertNameFromFilename(cert.certificate);\n      } catch (e) {\n        // ignore error\n        toImport.certificate = null;\n        toImport.name = null;\n      }\n    }\n\n    if (isValidPemPrivateKey(cert.key)) {\n      toImport.key = cert.key;\n    } else {\n      try {\n        toImport.key = getPemBodyFromFileSync(cert.key);\n      } catch (e) {\n        // ignore error\n        toImport.key = null;\n      }\n    }\n\n    if (\n      !toImport?.certificate ||\n      !isValidPemCertificate(toImport.certificate)\n    ) {\n      throw new InvalidClientCertificateBodyException();\n    }\n\n    if (!toImport?.key || !isValidPemPrivateKey(toImport.key)) {\n      throw new InvalidClientPrivateKeyException();\n    }\n\n    if (!toImport?.name) {\n      throw new InvalidCertificateNameException();\n    }\n\n    return this.prepareClientCertificateForImport(toImport);\n  }\n\n  /**\n   * Use existing certificate if found\n   * Generate unique name for new certificate\n   * @param cert\n   * @private\n   */\n  private async prepareClientCertificateForImport(\n    cert: Partial<ClientCertificate>,\n  ): Promise<ClientCertificate> {\n    const encryptedModel = await this.clientCertEncryptor.encryptEntity(\n      cert as ClientCertificate,\n    );\n    const existing = await this.clientCertRepository\n      .createQueryBuilder('c')\n      .select('c.id')\n      .where({\n        certificate: cert.certificate,\n        key: cert.key,\n      })\n      .orWhere({\n        certificate: encryptedModel.certificate,\n        key: encryptedModel.key,\n      })\n      .getOne();\n\n    if (existing) {\n      return existing;\n    }\n\n    const name = await CertificateImportService.determineAvailableName(\n      cert.name,\n      this.clientCertRepository,\n    );\n\n    return classToClass(ClientCertificate, {\n      ...cert,\n      name,\n    });\n  }\n\n  /**\n   * Find available name for certificate using such pattern \"{N}_{name}\"\n   * @param originalName\n   * @param repository\n   */\n  static async determineAvailableName(\n    originalName: string,\n    repository: Repository<any>,\n  ): Promise<string> {\n    let index = 0;\n\n    // temporary solution\n    // investigate how to make working \"regexp\" for sqlite\n    // https://github.com/kriasoft/node-sqlite/issues/55\n    // https://www.sqlite.org/c3ref/create_function.html\n    while (true) {\n      let name = originalName;\n\n      if (index) {\n        name = `${index}_${name}`;\n      }\n\n      if (\n        !(await repository\n          .createQueryBuilder('c')\n          .where({ name })\n          .select(['c.id'])\n          .getOne())\n      ) {\n        return name;\n      }\n\n      index += 1;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockDatabaseImportFailedAnalyticsPayload,\n  mockDatabaseImportPartialAnalyticsPayload,\n  mockDatabaseImportResponse,\n  mockDatabaseImportSucceededAnalyticsPayload,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { TelemetryEvents } from 'src/constants';\nimport { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics';\nimport {\n  NoDatabaseImportFileProvidedException,\n  SizeLimitExceededDatabaseImportFileException,\n  UnableToParseDatabaseImportFileException,\n} from 'src/modules/database-import/exceptions';\n\ndescribe('DatabaseImportAnalytics', () => {\n  let service: DatabaseImportAnalytics;\n  let sendEventSpy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, DatabaseImportAnalytics],\n    }).compile();\n\n    service = await module.get(DatabaseImportAnalytics);\n    sendEventSpy = jest.spyOn(service as any, 'sendEvent');\n  });\n\n  describe('sendImportResults', () => {\n    it('should emit 2 events with success and failed results', () => {\n      service.sendImportResults(\n        mockSessionMetadata,\n        mockDatabaseImportResponse,\n      );\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportSucceeded,\n        mockDatabaseImportSucceededAnalyticsPayload,\n      );\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        2,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportFailed,\n        mockDatabaseImportFailedAnalyticsPayload,\n      );\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        3,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportPartiallySucceeded,\n        mockDatabaseImportPartialAnalyticsPayload,\n      );\n    });\n  });\n\n  describe('sendImportFailed', () => {\n    it('should emit 1 event with \"Error\" cause', () => {\n      service.sendImportFailed(mockSessionMetadata, Error());\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportParseFailed,\n        {\n          error: 'Error',\n        },\n      );\n    });\n    it('should emit 1 event with \"UnableToParseDatabaseImportFileException\" cause', () => {\n      service.sendImportFailed(\n        mockSessionMetadata,\n        new UnableToParseDatabaseImportFileException(),\n      );\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportParseFailed,\n        {\n          error: 'UnableToParseDatabaseImportFileException',\n        },\n      );\n    });\n    it('should emit 1 event with \"NoDatabaseImportFileProvidedException\" cause', () => {\n      service.sendImportFailed(\n        mockSessionMetadata,\n        new NoDatabaseImportFileProvidedException(),\n      );\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportParseFailed,\n        {\n          error: 'NoDatabaseImportFileProvidedException',\n        },\n      );\n    });\n    it('should emit 1 event with \"SizeLimitExceededDatabaseImportFileException\" cause', () => {\n      service.sendImportFailed(\n        mockSessionMetadata,\n        new SizeLimitExceededDatabaseImportFileException(),\n      );\n\n      expect(sendEventSpy).toHaveBeenNthCalledWith(\n        1,\n        mockSessionMetadata,\n        TelemetryEvents.DatabaseImportParseFailed,\n        {\n          error: 'SizeLimitExceededDatabaseImportFileException',\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.analytics.ts",
    "content": "import { uniq } from 'lodash';\nimport { Injectable } from '@nestjs/common';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport {\n  DatabaseImportResponse,\n  DatabaseImportResult,\n} from 'src/modules/database-import/dto/database-import.response';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class DatabaseImportAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendImportResults(\n    sessionMetadata: SessionMetadata,\n    importResult: DatabaseImportResponse,\n  ): void {\n    if (importResult.success?.length) {\n      this.sendEvent(sessionMetadata, TelemetryEvents.DatabaseImportSucceeded, {\n        succeed: importResult.success.length,\n      });\n    }\n\n    if (importResult.fail?.length) {\n      this.sendEvent(sessionMetadata, TelemetryEvents.DatabaseImportFailed, {\n        failed: importResult.fail.length,\n        errors: DatabaseImportAnalytics.getUniqueErrorNamesFromResults(\n          importResult.fail,\n        ),\n      });\n    }\n\n    if (importResult.partial?.length) {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.DatabaseImportPartiallySucceeded,\n        {\n          partially: importResult.partial.length,\n          errors: DatabaseImportAnalytics.getUniqueErrorNamesFromResults(\n            importResult.partial,\n          ),\n        },\n      );\n    }\n  }\n\n  sendImportFailed(sessionMetadata: SessionMetadata, e: Error): void {\n    this.sendEvent(sessionMetadata, TelemetryEvents.DatabaseImportParseFailed, {\n      error: e?.constructor?.name || 'UncaughtError',\n    });\n  }\n\n  static getUniqueErrorNamesFromResults(results: DatabaseImportResult[]) {\n    return uniq(\n      [].concat(\n        ...results.map((res) =>\n          (res?.errors || []).map(\n            (error) => error?.constructor?.name || 'UncaughtError',\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.controller.spec.ts",
    "content": "import { when } from 'jest-when';\nimport * as request from 'supertest';\nimport { Test } from '@nestjs/testing';\nimport {\n  ForbiddenException,\n  INestApplication,\n  MiddlewareConsumer,\n  Module,\n  NestModule,\n} from '@nestjs/common';\nimport {\n  mockDatabase,\n  mockDatabaseImportService,\n  mockSessionService,\n} from 'src/__mocks__';\nimport { SingleUserAuthMiddleware } from 'src/common/middlewares/single-user-auth.middleware';\nimport { SessionService } from 'src/modules/session/session.service';\nimport config, { Config } from 'src/utils/config';\nimport { DatabaseImportController } from 'src/modules/database-import/database-import.controller';\nimport { DatabaseImportService } from 'src/modules/database-import/database-import.service';\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\n@Module({\n  controllers: [DatabaseImportController],\n  providers: [\n    {\n      provide: DatabaseImportService,\n      useFactory: mockDatabaseImportService,\n    },\n    {\n      provide: SessionService,\n      useFactory: mockSessionService,\n    },\n  ],\n})\nclass TestModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(SingleUserAuthMiddleware).forRoutes('*');\n  }\n}\n\ndescribe('DatabaseImportController', () => {\n  let app: INestApplication;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const moduleRef = await Test.createTestingModule({\n      imports: [TestModule],\n    }).compile();\n\n    app = moduleRef.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  describe('POST /databases/import', () => {\n    it('should import databases', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .post('/databases/import')\n        .send([mockDatabase])\n        .expect(200);\n    });\n\n    it('should fail to import databases when database management disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .post('/databases/import')\n        .send([mockDatabase])\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.controller.ts",
    "content": "import {\n  ClassSerializerInterceptor,\n  Controller,\n  HttpCode,\n  Post,\n  UploadedFile,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiBody, ApiConsumes, ApiResponse, ApiTags } from '@nestjs/swagger';\nimport { DatabaseImportService } from 'src/modules/database-import/database-import.service';\nimport { FileInterceptor } from '@nestjs/platform-express';\nimport { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response';\nimport {\n  DatabaseManagement,\n  RequestSessionMetadata,\n} from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\n\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(ClassSerializerInterceptor)\n@ApiTags('Database')\n@Controller('/databases')\nexport class DatabaseImportController {\n  constructor(private readonly service: DatabaseImportService) {}\n\n  @Post('import')\n  @ApiConsumes('multipart/form-data')\n  @ApiBody({\n    schema: {\n      type: 'object',\n      properties: {\n        file: {\n          type: 'string',\n          format: 'binary',\n        },\n      },\n    },\n  })\n  @HttpCode(200)\n  @UseInterceptors(FileInterceptor('file'))\n  @ApiResponse({ type: DatabaseImportResponse })\n  @DatabaseManagement()\n  async import(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @UploadedFile() file: any,\n  ): Promise<DatabaseImportResponse> {\n    return this.service.import(sessionMetadata, file);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { DatabaseImportController } from 'src/modules/database-import/database-import.controller';\nimport { DatabaseImportService } from 'src/modules/database-import/database-import.service';\nimport { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics';\nimport { CertificateImportService } from 'src/modules/database-import/certificate-import.service';\nimport { SshImportService } from 'src/modules/database-import/ssh-import.service';\n\n@Module({\n  controllers: [DatabaseImportController],\n  providers: [\n    DatabaseImportService,\n    CertificateImportService,\n    SshImportService,\n    DatabaseImportAnalytics,\n  ],\n})\nexport class DatabaseImportModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.service.spec.ts",
    "content": "import { pick } from 'lodash';\nimport { DatabaseImportService } from 'src/modules/database-import/database-import.service';\nimport {\n  mockCertificateImportService,\n  mockDatabase,\n  mockDatabaseImportAnalytics,\n  mockDatabaseImportFile,\n  mockDatabaseImportResponse,\n  mockSessionMetadata,\n  mockSshImportService,\n  MockType,\n} from 'src/__mocks__';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  ConnectionType,\n  Compressor,\n} from 'src/modules/database/entities/database.entity';\nimport { BadRequestException, ForbiddenException } from '@nestjs/common';\nimport { ValidationError } from 'class-validator';\nimport {\n  InvalidCaCertificateBodyException,\n  InvalidCertificateNameException,\n  InvalidClientCertificateBodyException,\n  NoDatabaseImportFileProvidedException,\n  SizeLimitExceededDatabaseImportFileException,\n  UnableToParseDatabaseImportFileException,\n} from 'src/modules/database-import/exceptions';\nimport { CertificateImportService } from 'src/modules/database-import/certificate-import.service';\nimport { SshImportService } from 'src/modules/database-import/ssh-import.service';\n\ndescribe('DatabaseImportService', () => {\n  let service: DatabaseImportService;\n  let certificateImportService: MockType<CertificateImportService>;\n  let databaseRepository: MockType<DatabaseRepository>;\n  let analytics: MockType<DatabaseImportAnalytics>;\n  let validatoSpy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseImportService,\n        {\n          provide: DatabaseRepository,\n          useFactory: jest.fn(() => ({\n            create: jest.fn().mockResolvedValue(mockDatabase),\n          })),\n        },\n        {\n          provide: CertificateImportService,\n          useFactory: mockCertificateImportService,\n        },\n        {\n          provide: SshImportService,\n          useFactory: mockSshImportService,\n        },\n        {\n          provide: DatabaseImportAnalytics,\n          useFactory: mockDatabaseImportAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseImportService);\n    databaseRepository = await module.get(DatabaseRepository);\n    certificateImportService = await module.get(CertificateImportService);\n    analytics = await module.get(DatabaseImportAnalytics);\n    validatoSpy = jest.spyOn(service['validator'], 'validateOrReject');\n  });\n\n  describe('importDatabase', () => {\n    beforeEach(() => {\n      databaseRepository.create.mockRejectedValueOnce(\n        new BadRequestException(),\n      );\n      databaseRepository.create.mockRejectedValueOnce(new ForbiddenException());\n      validatoSpy.mockRejectedValueOnce([new ValidationError()]);\n      certificateImportService.processCaCertificate\n        .mockRejectedValueOnce(new InvalidCaCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidCaCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidCaCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidCaCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidCertificateNameException());\n      certificateImportService.processClientCertificate\n        .mockRejectedValueOnce(new InvalidClientCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidClientCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidClientCertificateBodyException())\n        .mockRejectedValueOnce(new InvalidCertificateNameException());\n    });\n\n    it('should import databases from json', async () => {\n      const response = await service.import(\n        mockSessionMetadata,\n        mockDatabaseImportFile,\n      );\n\n      expect(response).toEqual(mockDatabaseImportResponse);\n      expect(analytics.sendImportResults).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseImportResponse,\n      );\n    });\n\n    it('should import databases from base64', async () => {\n      const response = await service.import(mockSessionMetadata, {\n        ...mockDatabaseImportFile,\n        mimetype: 'binary/octet-stream',\n        buffer: Buffer.from(mockDatabaseImportFile.buffer.toString('base64')),\n      });\n\n      expect(response).toEqual({\n        ...mockDatabaseImportResponse,\n      });\n      expect(analytics.sendImportResults).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseImportResponse,\n      );\n    });\n\n    it('should fail due to file was not provided', async () => {\n      try {\n        await service.import(mockSessionMetadata, undefined);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NoDatabaseImportFileProvidedException);\n        expect(e.message).toEqual('No import file provided');\n        expect(analytics.sendImportFailed).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          new NoDatabaseImportFileProvidedException('No import file provided'),\n        );\n      }\n    });\n\n    it('should fail due to file exceeded size limitations', async () => {\n      try {\n        await service.import(mockSessionMetadata, {\n          ...mockDatabaseImportFile,\n          size: 10 * 1024 * 1024 + 1,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(SizeLimitExceededDatabaseImportFileException);\n        expect(e.message).toEqual(\n          'Import file is too big. Maximum 10mb allowed',\n        );\n      }\n    });\n\n    it('should fail due to incorrect json', async () => {\n      try {\n        await service.import(mockSessionMetadata, {\n          ...mockDatabaseImportFile,\n          buffer: Buffer.from([0, 21]),\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(UnableToParseDatabaseImportFileException);\n        expect(e.message).toEqual(\n          `Unable to parse ${mockDatabaseImportFile.originalname}`,\n        );\n      }\n    });\n\n    it('should fail due to incorrect base64 + truncate filename', async () => {\n      try {\n        await service.import(mockSessionMetadata, {\n          ...mockDatabaseImportFile,\n          originalname: new Array(1_000).fill(1).join(''),\n          mimetype: 'binary/octet-stream',\n          buffer: Buffer.from([0, 21]),\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(UnableToParseDatabaseImportFileException);\n        expect(e.message).toEqual(\n          `Unable to parse ${new Array(50).fill(1).join('')}...`,\n        );\n      }\n    });\n  });\n\n  describe('createDatabase', () => {\n    it('should create standalone database', async () => {\n      await service['createDatabase'](\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          provider: 'REDIS_CLOUD',\n        },\n        0,\n      );\n\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...pick(mockDatabase, [\n            'host',\n            'port',\n            'name',\n            'connectionType',\n            'compressor',\n            'modules',\n          ]),\n          provider: 'REDIS_CLOUD',\n          new: true,\n        },\n        false,\n      );\n    });\n    it('should create standalone with created name', async () => {\n      await service['createDatabase'](\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          name: undefined,\n        },\n        0,\n      );\n\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...pick(mockDatabase, [\n            'host',\n            'port',\n            'name',\n            'connectionType',\n            'compressor',\n            'modules',\n          ]),\n          name: `${mockDatabase.host}:${mockDatabase.port}`,\n          new: true,\n        },\n        false,\n      );\n    });\n    it('should create standalone with none compressor', async () => {\n      await service['createDatabase'](\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          compressor: 'custom',\n        },\n        0,\n      );\n\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...pick(mockDatabase, [\n            'host',\n            'port',\n            'name',\n            'connectionType',\n            'compressor',\n            'modules',\n          ]),\n          compressor: Compressor.NONE,\n          new: true,\n        },\n        false,\n      );\n    });\n    it('should create standalone with compressor and tlsServername', async () => {\n      await service['createDatabase'](\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          compressor: Compressor.GZIP,\n          tlsServername: 'redis-insight',\n        },\n        0,\n      );\n\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...pick(mockDatabase, [\n            'host',\n            'port',\n            'name',\n            'connectionType',\n            'compressor',\n            'modules',\n            'tlsServername',\n          ]),\n          compressor: Compressor.GZIP,\n          tlsServername: 'redis-insight',\n          new: true,\n        },\n        false,\n      );\n    });\n    it('should create cluster database', async () => {\n      await service['createDatabase'](\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          connectionType: undefined,\n          cluster: true,\n        },\n        0,\n      );\n\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...pick(mockDatabase, [\n            'host',\n            'port',\n            'name',\n            'compressor',\n            'modules',\n          ]),\n          connectionType: ConnectionType.CLUSTER,\n          new: true,\n        },\n        false,\n      );\n    });\n\n    it('should create database with providerDetails for Azure Entra ID', async () => {\n      const providerDetails = {\n        provider: 'azure',\n        authType: 'entraId',\n        azureAccountId: 'test-account-123',\n      };\n\n      await service['createDatabase'](\n        mockSessionMetadata,\n        {\n          ...mockDatabase,\n          providerDetails,\n        },\n        0,\n      );\n\n      expect(databaseRepository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        expect.objectContaining({\n          providerDetails,\n        }),\n        false,\n      );\n    });\n  });\n\n  describe('determineConnectionType', () => {\n    const tcs = [\n      // common\n      { input: {}, output: ConnectionType.NOT_CONNECTED },\n      // isCluster\n      { input: { isCluster: true }, output: ConnectionType.CLUSTER },\n      { input: { isCluster: false }, output: ConnectionType.NOT_CONNECTED },\n      { input: { isCluster: undefined }, output: ConnectionType.NOT_CONNECTED },\n      // sentinelMasterName\n      {\n        input: { sentinelMasterName: 'some name' },\n        output: ConnectionType.SENTINEL,\n      },\n      // connectionType\n      {\n        input: { connectionType: ConnectionType.STANDALONE },\n        output: ConnectionType.STANDALONE,\n      },\n      {\n        input: { connectionType: ConnectionType.CLUSTER },\n        output: ConnectionType.CLUSTER,\n      },\n      {\n        input: { connectionType: ConnectionType.SENTINEL },\n        output: ConnectionType.SENTINEL,\n      },\n      {\n        input: { connectionType: 'something not supported' },\n        output: ConnectionType.NOT_CONNECTED,\n      },\n      // type\n      { input: { type: 'standalone' }, output: ConnectionType.STANDALONE },\n      { input: { type: 'cluster' }, output: ConnectionType.CLUSTER },\n      { input: { type: 'sentinel' }, output: ConnectionType.SENTINEL },\n      {\n        input: { type: 'something not supported' },\n        output: ConnectionType.NOT_CONNECTED,\n      },\n      // priority tests\n      {\n        input: {\n          connectionType: ConnectionType.SENTINEL,\n          type: 'standalone',\n          isCluster: true,\n          sentinelMasterName: 'some name',\n        },\n        output: ConnectionType.SENTINEL,\n      },\n      {\n        input: {\n          type: 'standalone',\n          isCluster: true,\n          sentinelMasterName: 'some name',\n        },\n        output: ConnectionType.STANDALONE,\n      },\n      {\n        input: {\n          isCluster: true,\n          sentinelMasterName: 'some name',\n        },\n        output: ConnectionType.CLUSTER,\n      },\n      {\n        input: {\n          sentinelMasterName: 'some name',\n        },\n        output: ConnectionType.SENTINEL,\n      },\n    ];\n\n    tcs.forEach((tc) => {\n      it(`should return ${tc.output} when called with ${JSON.stringify(tc.input)}`, () => {\n        expect(DatabaseImportService.determineConnectionType(tc.input)).toEqual(\n          tc.output,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/database-import.service.ts",
    "content": "import {\n  HttpException,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { get, isArray, set } from 'lodash';\nimport { Database } from 'src/modules/database/models/database';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  ConnectionType,\n  Compressor,\n} from 'src/modules/database/entities/database.entity';\nimport { DatabaseRepository } from 'src/modules/database/repositories/database.repository';\nimport {\n  DatabaseImportResponse,\n  DatabaseImportResult,\n  DatabaseImportStatus,\n} from 'src/modules/database-import/dto/database-import.response';\nimport { ValidationError, Validator } from 'class-validator';\nimport { ImportDatabaseDto } from 'src/modules/database-import/dto/import.database.dto';\nimport { classToClass } from 'src/utils';\nimport { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics';\nimport {\n  NoDatabaseImportFileProvidedException,\n  SizeLimitExceededDatabaseImportFileException,\n  UnableToParseDatabaseImportFileException,\n  InvalidCompressorException,\n} from 'src/modules/database-import/exceptions';\nimport { ValidationException } from 'src/common/exceptions';\nimport { CertificateImportService } from 'src/modules/database-import/certificate-import.service';\nimport { SshImportService } from 'src/modules/database-import/ssh-import.service';\nimport { SessionMetadata } from 'src/common/models';\n\ntype ImportFileType = {\n  originalname?: string;\n  mimetype?: string;\n  size?: number;\n  buffer?: Buffer;\n};\n\n@Injectable()\nexport class DatabaseImportService {\n  private logger = new Logger('DatabaseImportService');\n\n  private validator = new Validator();\n\n  private fieldsMapSchema: Array<[string, string[]]> = [\n    ['name', ['name', 'connectionName']],\n    ['username', ['username']],\n    ['password', ['password', 'auth']],\n    ['host', ['host']],\n    ['port', ['port']],\n    ['db', ['db']],\n    ['provider', ['provider']],\n    ['isCluster', ['cluster']],\n    ['type', ['type']],\n    ['connectionType', ['connectionType']],\n    ['tls', ['tls', 'ssl']],\n    ['tlsServername', ['tlsServername']],\n    ['tlsCaName', ['caCert.name']],\n    [\n      'tlsCaCert',\n      ['caCert.certificate', 'caCert', 'sslOptions.ca', 'ssl_ca_cert_path'],\n    ],\n    ['tlsClientName', ['clientCert.name']],\n    [\n      'tlsClientCert',\n      [\n        'clientCert.certificate',\n        'certificate',\n        'sslOptions.cert',\n        'ssl_local_cert_path',\n      ],\n    ],\n    [\n      'tlsClientKey',\n      ['clientCert.key', 'keyFile', 'sslOptions.key', 'ssl_private_key_path'],\n    ],\n    [\n      'sentinelMasterName',\n      [\n        'sentinelMaster.name',\n        'sentinelOptions.masterName',\n        'sentinelOptions.name',\n      ],\n    ],\n    ['sentinelMasterUsername', ['sentinelMaster.username']],\n    [\n      'sentinelMasterPassword',\n      [\n        'sentinelMaster.password',\n        'sentinelOptions.nodePassword',\n        'sentinelOptions.sentinelPassword',\n      ],\n    ],\n    ['sshHost', ['sshOptions.host', 'ssh_host', 'sshHost']],\n    ['sshPort', ['sshOptions.port', 'ssh_port', 'sshPort']],\n    ['sshUsername', ['sshOptions.username', 'ssh_user', 'sshUser']],\n    ['sshPassword', ['sshOptions.password', 'ssh_password', 'sshPassword']],\n    [\n      'sshPrivateKey',\n      [\n        'sshOptions.privateKey',\n        'sshOptions.privatekey',\n        'ssh_private_key_path',\n        'sshKeyFile',\n      ],\n    ],\n    ['sshPassphrase', ['sshOptions.passphrase', 'sshKeyPassphrase']],\n    ['sshAgentPath', ['ssh_agent_path']],\n    ['compressor', ['compressor']],\n    ['modules', ['modules']],\n    ['forceStandalone', ['forceStandalone']],\n    ['tags', ['tags']],\n    ['providerDetails', ['providerDetails']],\n  ];\n\n  constructor(\n    private readonly certificateImportService: CertificateImportService,\n    private readonly sshImportService: SshImportService,\n    private readonly databaseRepository: DatabaseRepository,\n    private readonly analytics: DatabaseImportAnalytics,\n  ) {}\n\n  /**\n   * Import databases from the file\n   * @param sessionMetadata\n   * @param file\n   */\n  public async import(\n    sessionMetadata: SessionMetadata,\n    file: ImportFileType,\n  ): Promise<DatabaseImportResponse> {\n    try {\n      // todo: create FileValidation class\n      if (!file) {\n        throw new NoDatabaseImportFileProvidedException(\n          'No import file provided',\n        );\n      }\n      if (file?.size > 1024 * 1024 * 10) {\n        throw new SizeLimitExceededDatabaseImportFileException(\n          'Import file is too big. Maximum 10mb allowed',\n        );\n      }\n\n      const items = DatabaseImportService.parseFile(file);\n\n      if (!isArray(items) || !items?.length) {\n        let filename = file?.originalname || 'import file';\n        if (filename.length > 50) {\n          filename = `${filename.slice(0, 50)}...`;\n        }\n        throw new UnableToParseDatabaseImportFileException(\n          `Unable to parse ${filename}`,\n        );\n      }\n\n      let response = {\n        total: items.length,\n        success: [],\n        partial: [],\n        fail: [],\n      };\n\n      // it is very important to insert databases on-by-one to avoid db constraint errors\n      await items.reduce(\n        (prev, item, index) =>\n          prev.finally(() =>\n            this.createDatabase(sessionMetadata, item, index).then((result) => {\n              switch (result.status) {\n                case DatabaseImportStatus.Fail:\n                  response.fail.push(result);\n                  break;\n                case DatabaseImportStatus.Partial:\n                  response.partial.push(result);\n                  break;\n                case DatabaseImportStatus.Success:\n                  response.success.push(result);\n                  break;\n                default:\n                // do not include into repost, since some unexpected behaviour\n              }\n            }),\n          ),\n        Promise.resolve(),\n      );\n\n      response = plainToInstance(DatabaseImportResponse, response);\n\n      this.analytics.sendImportResults(sessionMetadata, response);\n\n      return response;\n    } catch (e) {\n      this.logger.warn(\n        `Unable to import databases: ${e?.constructor?.name || 'UncaughtError'}`,\n        e,\n        sessionMetadata,\n      );\n\n      this.analytics.sendImportFailed(sessionMetadata, e);\n\n      throw e;\n    }\n  }\n\n  /**\n   * Map data to known model, validate it and create database if possible\n   * Note: will not create connection, simply create database\n   * @parama sessionMetadata\n   * @param sessionMetadata\n   * @param item\n   * @param index\n   * @private\n   */\n  private async createDatabase(\n    sessionMetadata: SessionMetadata,\n    item: any,\n    index: number,\n  ): Promise<DatabaseImportResult> {\n    try {\n      let status = DatabaseImportStatus.Success;\n      const errors = [];\n      const data: any = {};\n\n      // set this is a new connection\n      data.new = true;\n\n      this.fieldsMapSchema.forEach(([field, paths]) => {\n        let value: any;\n        paths.every((path) => {\n          value = get(item, path);\n          return value === undefined;\n        });\n\n        set(data, field, value);\n      });\n\n      // set database name if needed\n      if (!data.name) {\n        data.name = `${data.host}:${data.port}`;\n      }\n\n      data.connectionType = DatabaseImportService.determineConnectionType(data);\n\n      if (data?.sentinelMasterName) {\n        data.sentinelMaster = {\n          name: data.sentinelMasterName,\n          username: data.sentinelMasterUsername || undefined,\n          password: data.sentinelMasterPassword,\n        };\n        data.nodes = [\n          {\n            host: data.host,\n            port: parseInt(data.port, 10),\n          },\n        ];\n      }\n\n      if (data?.sshHost || data?.sshAgentPath) {\n        data.ssh = true;\n        try {\n          data.sshOptions = await this.sshImportService.processSshOptions(data);\n        } catch (e) {\n          status = DatabaseImportStatus.Partial;\n          data.ssh = false;\n          errors.push(e);\n        }\n      }\n\n      if (data?.tlsCaCert) {\n        try {\n          data.tls = true;\n          data.caCert =\n            await this.certificateImportService.processCaCertificate({\n              certificate: data.tlsCaCert,\n              name: data?.tlsCaName,\n            });\n        } catch (e) {\n          status = DatabaseImportStatus.Partial;\n          errors.push(e);\n        }\n      }\n\n      if (data?.tlsClientCert || data?.tlsClientKey) {\n        try {\n          data.tls = true;\n          data.clientCert =\n            await this.certificateImportService.processClientCertificate({\n              certificate: data.tlsClientCert,\n              key: data.tlsClientKey,\n              name: data?.tlsClientName,\n            });\n        } catch (e) {\n          status = DatabaseImportStatus.Partial;\n          errors.push(e);\n        }\n      }\n\n      if (data?.compressor && !(data.compressor in Compressor)) {\n        status = DatabaseImportStatus.Partial;\n        data.compressor = Compressor.NONE;\n        errors.push(new InvalidCompressorException());\n      }\n\n      const dto = plainToInstance(\n        ImportDatabaseDto,\n        // additionally replace empty strings (\"\") with null\n        Object.keys(data).reduce((acc, key) => {\n          acc[key] = data[key] === '' ? null : data[key];\n          return acc;\n        }, {}),\n        {\n          groups: ['security'],\n        },\n      );\n\n      await this.validator.validateOrReject(dto, {\n        whitelist: true,\n      });\n\n      const database = classToClass(Database, dto);\n\n      await this.databaseRepository.create(sessionMetadata, database, false);\n\n      return {\n        index,\n        status,\n        host: database.host,\n        port: database.port,\n        errors: errors?.length ? errors : undefined,\n      };\n    } catch (e) {\n      let errors = [e];\n      if (isArray(e)) {\n        errors = e;\n      }\n\n      errors = errors.map((error) => {\n        if (error instanceof ValidationError) {\n          const messages = Object.values(error?.constraints || {});\n          return new ValidationException(\n            messages[messages.length - 1] || 'Bad request',\n          );\n        }\n\n        if (!(error instanceof HttpException)) {\n          return new InternalServerErrorException(error?.message);\n        }\n\n        return error;\n      });\n\n      this.logger.warn(\n        `Unable to import database: ${errors[0]?.constructor?.name || 'UncaughtError'}`,\n        errors[0],\n        sessionMetadata,\n      );\n\n      return {\n        index,\n        status: DatabaseImportStatus.Fail,\n        host: item?.host,\n        port: item?.port,\n        errors,\n      };\n    }\n  }\n\n  /**\n   * Try to determine connection type based on input data\n   * Should return NOT_CONNECTED when it is not possible\n   * @param data\n   */\n  static determineConnectionType(data: any = {}): ConnectionType {\n    if (data?.connectionType) {\n      return data.connectionType in ConnectionType\n        ? ConnectionType[data.connectionType]\n        : ConnectionType.NOT_CONNECTED;\n    }\n\n    if (data?.type) {\n      switch (data.type) {\n        case 'cluster':\n          return ConnectionType.CLUSTER;\n        case 'sentinel':\n          return ConnectionType.SENTINEL;\n        case 'standalone':\n          return ConnectionType.STANDALONE;\n        default:\n          return ConnectionType.NOT_CONNECTED;\n      }\n    }\n\n    if (data?.isCluster === true) {\n      return ConnectionType.CLUSTER;\n    }\n\n    if (data?.sentinelMasterName) {\n      return ConnectionType.SENTINEL;\n    }\n\n    return ConnectionType.NOT_CONNECTED;\n  }\n\n  /**\n   * Try to parse file based on mimetype and known\\supported formats\n   * @param file\n   */\n  static parseFile(file: ImportFileType): any {\n    const data = file?.buffer?.toString();\n\n    let databases = DatabaseImportService.parseJson(data);\n\n    if (!databases) {\n      databases = DatabaseImportService.parseBase64(data);\n    }\n\n    return databases;\n  }\n\n  static parseBase64(data: string): any {\n    try {\n      return JSON.parse(Buffer.from(data, 'base64').toString('utf8'));\n    } catch (e) {\n      return null;\n    }\n  }\n\n  static parseJson(data: string): any {\n    try {\n      return JSON.parse(data);\n    } catch (e) {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/dto/database-import.response.ts",
    "content": "import { isString, isNumber } from 'lodash';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Transform, Type } from 'class-transformer';\n\nexport enum DatabaseImportStatus {\n  Success = 'success',\n  Partial = 'partial',\n  Fail = 'fail',\n}\n\nexport class DatabaseImportResult {\n  @ApiProperty({\n    description: 'Entry index from original json',\n    type: Number,\n  })\n  @Expose()\n  index: number;\n\n  @ApiProperty({\n    description: 'Import status',\n    enum: DatabaseImportStatus,\n  })\n  @Expose()\n  status: DatabaseImportStatus;\n\n  @ApiPropertyOptional({\n    description: 'Database host',\n    type: String,\n  })\n  @Expose()\n  @Transform(({ value }) => (isString(value) ? value : undefined), {\n    toPlainOnly: true,\n  })\n  host?: string;\n\n  @ApiPropertyOptional({\n    description: 'Database port',\n    type: Number,\n  })\n  @Expose()\n  @Transform(({ value }) => (isNumber(value) ? value : undefined), {\n    toPlainOnly: true,\n  })\n  port?: number;\n\n  @ApiPropertyOptional({\n    description: 'Error message if any',\n    type: String,\n  })\n  @Expose()\n  @Transform(\n    ({ value: e }) => {\n      if (!e) {\n        return undefined;\n      }\n\n      return e.map((error) => {\n        if (error?.response) {\n          return error.response;\n        }\n\n        return {\n          statusCode: 500,\n          message: error?.message || 'Unhandled Error',\n          error: 'Unhandled Error',\n        };\n      });\n    },\n    { toPlainOnly: true },\n  )\n  errors?: Error[];\n}\n\nexport class DatabaseImportResponse {\n  @ApiProperty({\n    description: 'Total elements processed from the import file',\n    type: Number,\n  })\n  @Expose()\n  total: number;\n\n  @ApiProperty({\n    description: 'List of successfully imported database',\n    type: DatabaseImportResult,\n  })\n  @Expose()\n  @Type(() => DatabaseImportResult)\n  success: DatabaseImportResult[];\n\n  @ApiProperty({\n    description: 'List of partially imported database',\n    type: DatabaseImportResult,\n  })\n  @Expose()\n  @Type(() => DatabaseImportResult)\n  partial: DatabaseImportResult[];\n\n  @ApiProperty({\n    description: 'List of databases failed to import',\n    type: DatabaseImportResult,\n  })\n  @Expose()\n  @Type(() => DatabaseImportResult)\n  fail: DatabaseImportResult[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/dto/import.database.dto.ts",
    "content": "import { ApiPropertyOptional, getSchemaPath, PickType } from '@nestjs/swagger';\nimport { Database } from 'src/modules/database/models/database';\nimport { Expose, Type } from 'class-transformer';\nimport {\n  IsArray,\n  IsInt,\n  IsNotEmpty,\n  IsNotEmptyObject,\n  IsOptional,\n  Max,\n  Min,\n  ValidateNested,\n} from 'class-validator';\nimport { NoDuplicatesByKey } from 'src/common/decorators';\nimport { caCertTransformer } from 'src/modules/certificate/transformers/ca-cert.transformer';\nimport { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';\nimport { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto';\nimport { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';\nimport { clientCertTransformer } from 'src/modules/certificate/transformers/client-cert.transformer';\nimport { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';\nimport { Tag } from 'src/modules/tag/models/tag';\n\nexport class ImportDatabaseDto extends PickType(Database, [\n  'host',\n  'port',\n  'name',\n  'db',\n  'username',\n  'password',\n  'connectionType',\n  'tls',\n  'verifyServerCert',\n  'sentinelMaster',\n  'nodes',\n  'new',\n  'ssh',\n  'sshOptions',\n  'provider',\n  'providerDetails',\n  'compressor',\n  'modules',\n  'tlsServername',\n  'forceStandalone',\n  'tags',\n] as const) {\n  @Expose()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  @Type(() => Number)\n  @Min(0)\n  @Max(65535)\n  port: number;\n\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(caCertTransformer)\n  @ValidateNested()\n  caCert?: CreateCaCertificateDto | UseCaCertificateDto;\n\n  @ApiPropertyOptional({\n    description: 'Client Certificate',\n    oneOf: [\n      { $ref: getSchemaPath(CreateClientCertificateDto) },\n      { $ref: getSchemaPath(UseCaCertificateDto) },\n    ],\n  })\n  @Expose()\n  @IsOptional()\n  @IsNotEmptyObject()\n  @Type(clientCertTransformer)\n  @ValidateNested()\n  clientCert?: CreateClientCertificateDto | UseClientCertificateDto;\n\n  @ApiPropertyOptional({\n    description: 'Tags associated with the database.',\n    type: Tag,\n    isArray: true,\n  })\n  @Expose()\n  @IsOptional()\n  @IsArray()\n  @NoDuplicatesByKey('key', {\n    message: 'Tags must not contain duplicates by key.',\n  })\n  @Type(() => Tag)\n  tags?: Tag[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/index.ts",
    "content": "export * from './invalid-ca-certificate-body.exception';\nexport * from './invalid-client-certificate-body.exception';\nexport * from './invalid-client-private-key.exception';\nexport * from './invalid-certificate-name.exception';\nexport * from './invalid-compressor.exception';\nexport * from './invalid-ssh-body.exception';\nexport * from './invalid-ssh-private-key-body.exception';\nexport * from './size-limit-exceeded-database-import-file.exception';\nexport * from './no-database-import-file-provided.exception';\nexport * from './unable-to-parse-database-import-file.exception';\nexport * from './ssh-agents-are-not-supported.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-ca-certificate-body.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidCaCertificateBodyException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.INVALID_CA_BODY) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid Ca Certificate Body',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-certificate-name.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidCertificateNameException extends HttpException {\n  constructor(\n    message: string = ERROR_MESSAGES.CERTIFICATE_NAME_IS_NOT_DEFINED,\n  ) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid Certificate Name',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-client-certificate-body.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidClientCertificateBodyException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.INVALID_CERTIFICATE_BODY) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid Client Certificate Body',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-client-private-key.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidClientPrivateKeyException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.INVALID_PRIVATE_KEY) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid Client Private Key',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-compressor.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidCompressorException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.INVALID_COMPRESSOR) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid compressor',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-ssh-body.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidSshBodyException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.INVALID_SSH_BODY) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid SSH body',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/invalid-ssh-private-key-body.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class InvalidSshPrivateKeyBodyException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.INVALID_SSH_PRIVATE_KEY_BODY) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid SSH Private Key Body',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/no-database-import-file-provided.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\n\nexport class NoDatabaseImportFileProvidedException extends HttpException {\n  constructor(message: string = 'No import file provided') {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'No Database Import File Provided',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/size-limit-exceeded-database-import-file.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\n\nexport class SizeLimitExceededDatabaseImportFileException extends HttpException {\n  constructor(message: string = 'Invalid import file') {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Invalid Database Import File',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/ssh-agents-are-not-supported.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class SshAgentsAreNotSupportedException extends HttpException {\n  constructor(message: string = ERROR_MESSAGES.SSH_AGENTS_ARE_NOT_SUPPORTED) {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Ssh Agents Are Not Supported',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/exceptions/unable-to-parse-database-import-file.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\n\nexport class UnableToParseDatabaseImportFileException extends HttpException {\n  constructor(message: string = 'Unable to parse import file') {\n    const response = {\n      message,\n      statusCode: 400,\n      error: 'Unable To Parse Database Import File',\n    };\n\n    super(response, 400);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/ssh-import.service.spec.ts",
    "content": "import { mockSshOptionsBasic, mockSshOptionsPrivateKey } from 'src/__mocks__';\nimport * as utils from 'src/common/utils';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { SshImportService } from 'src/modules/database-import/ssh-import.service';\nimport {\n  InvalidSshPrivateKeyBodyException,\n  InvalidSshBodyException,\n  SshAgentsAreNotSupportedException,\n} from 'src/modules/database-import/exceptions';\n\njest.mock('src/common/utils', () => ({\n  ...(jest.requireActual('src/common/utils') as object),\n  getPemBodyFromFileSync: jest.fn(),\n}));\n\nconst mockSshImportDataBasic = {\n  sshHost: mockSshOptionsBasic.host,\n  sshPort: mockSshOptionsBasic.port,\n  sshUsername: mockSshOptionsBasic.username,\n  sshPassword: mockSshOptionsBasic.password,\n};\n\nconst mockSshImportDataPK = {\n  ...mockSshImportDataBasic,\n  sshPrivateKey: mockSshOptionsPrivateKey.privateKey,\n  sshPassphrase: mockSshOptionsPrivateKey.passphrase,\n};\n\ndescribe('SshImportService', () => {\n  let service: SshImportService;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [SshImportService],\n    }).compile();\n\n    service = await module.get(SshImportService);\n  });\n\n  let getPemBodyFromFileSyncSpy;\n\n  describe('processSshOptions', () => {\n    beforeEach(() => {\n      getPemBodyFromFileSyncSpy = jest.spyOn(\n        utils as any,\n        'getPemBodyFromFileSync',\n      );\n      getPemBodyFromFileSyncSpy.mockReturnValue(\n        mockSshOptionsPrivateKey.privateKey,\n      );\n    });\n\n    it('should successfully process ssh basic', async () => {\n      const response = await service.processSshOptions({\n        ...mockSshImportDataBasic,\n      });\n\n      expect(response).toEqual({\n        ...mockSshOptionsBasic,\n        id: undefined,\n        privateKey: undefined,\n        passphrase: undefined,\n      });\n    });\n\n    it('should successfully process ssh PKP', async () => {\n      const response = await service.processSshOptions({\n        ...mockSshImportDataPK,\n      });\n\n      expect(response).toEqual({\n        ...mockSshOptionsPrivateKey,\n        id: undefined,\n        password: undefined,\n      });\n    });\n\n    it('should successfully process ssh PKP (from path)', async () => {\n      const response = await service.processSshOptions({\n        ...mockSshImportDataPK,\n        sshPrivateKey: '/some/path',\n      });\n\n      expect(response).toEqual({\n        ...mockSshOptionsPrivateKey,\n        id: undefined,\n        password: undefined,\n      });\n    });\n\n    it('should throw an error when invalid privateKey body provided', async () => {\n      getPemBodyFromFileSyncSpy.mockImplementation(() => {\n        throw new Error('no file');\n      });\n\n      try {\n        await service.processSshOptions({\n          ...mockSshImportDataPK,\n          sshPrivateKey: '/some/path',\n        });\n      } catch (e) {\n        expect(e).toBeInstanceOf(InvalidSshPrivateKeyBodyException);\n      }\n    });\n\n    it('should throw an error when ssh agent provided', async () => {\n      try {\n        await service.processSshOptions({\n          ...mockSshImportDataPK,\n          sshAgentPath: '/agent/path',\n        });\n      } catch (e) {\n        expect(e).toBeInstanceOf(SshAgentsAreNotSupportedException);\n      }\n    });\n  });\n\n  it('should throw an error when no username defined', async () => {\n    try {\n      await service.processSshOptions({\n        ...mockSshImportDataPK,\n        sshUsername: undefined,\n      });\n    } catch (e) {\n      expect(e).toBeInstanceOf(InvalidSshBodyException);\n    }\n  });\n\n  it('should throw an error when no port defined', async () => {\n    try {\n      await service.processSshOptions({\n        ...mockSshImportDataPK,\n        sshPassword: undefined,\n      });\n    } catch (e) {\n      expect(e).toBeInstanceOf(InvalidSshBodyException);\n    }\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-import/ssh-import.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { isUndefined } from 'lodash';\nimport { getPemBodyFromFileSync, isValidSshPrivateKey } from 'src/common/utils';\nimport {\n  InvalidSshPrivateKeyBodyException,\n  InvalidSshBodyException,\n  SshAgentsAreNotSupportedException,\n} from 'src/modules/database-import/exceptions';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\n\n@Injectable()\nexport class SshImportService {\n  /**\n   * Validate data + prepare CA certificate to be imported along with new database\n   * @param data\n   */\n  async processSshOptions(data: any): Promise<Partial<SshOptions>> {\n    let sshOptions: Partial<SshOptions> = {\n      host: data.sshHost,\n    };\n\n    if (isUndefined(data.sshPort) || isUndefined(data.sshUsername)) {\n      throw new InvalidSshBodyException();\n    } else {\n      sshOptions.port = parseInt(data.sshPort, 10);\n      sshOptions.username = data.sshUsername;\n    }\n\n    if (data.sshPrivateKey) {\n      sshOptions.passphrase = data.sshPassphrase || data.sshPassword || null;\n\n      if (isValidSshPrivateKey(data.sshPrivateKey)) {\n        sshOptions.privateKey = data.sshPrivateKey;\n      } else {\n        try {\n          sshOptions.privateKey = getPemBodyFromFileSync(data.sshPrivateKey);\n        } catch (e) {\n          // ignore error\n          sshOptions = null;\n        }\n      }\n    } else {\n      sshOptions.password = data.sshPassword || null;\n    }\n\n    if (\n      !sshOptions ||\n      (sshOptions?.privateKey && !isValidSshPrivateKey(sshOptions.privateKey))\n    ) {\n      throw new InvalidSshPrivateKeyBodyException();\n    }\n\n    if (data.sshAgentPath) {\n      throw new SshAgentsAreNotSupportedException();\n    }\n\n    return sshOptions;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/constants/index.ts",
    "content": "export enum RecommendationServerEvents {\n  Recommendation = 'recommendation',\n}\n\nexport enum RecommendationEvents {\n  NewRecommendation = 'new-recommendation',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockDatabase,\n  mockDatabaseRecommendation,\n  mockDatabaseWithTlsAuth,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { TelemetryEvents } from 'src/constants';\nimport { DatabaseRecommendationAnalytics } from './database-recommendation.analytics';\n\ndescribe('DatabaseRecommendationAnalytics', () => {\n  let service: DatabaseRecommendationAnalytics;\n  let sendEventSpy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, DatabaseRecommendationAnalytics],\n    }).compile();\n\n    service = await module.get(DatabaseRecommendationAnalytics);\n    sendEventSpy = jest.spyOn(service as any, 'sendEvent');\n  });\n\n  describe('sendInstanceAddedEvent', () => {\n    it('should emit event with recommendationName and provider', () => {\n      service.sendCreatedRecommendationEvent(\n        mockSessionMetadata,\n        mockDatabaseRecommendation,\n        mockDatabaseWithTlsAuth,\n      );\n\n      expect(sendEventSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.InsightsTipGenerated,\n        {\n          recommendationName: mockDatabaseRecommendation.name,\n          databaseId: mockDatabase.id,\n          provider: mockDatabaseWithTlsAuth.provider,\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.analytics.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { TelemetryEvents } from 'src/constants';\nimport { SessionMetadata } from 'src/common/models';\nimport { DatabaseRecommendation } from './models';\nimport { Database } from '../database/models/database';\n\n@Injectable()\nexport class DatabaseRecommendationAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendCreatedRecommendationEvent(\n    sessionMetadata: SessionMetadata,\n    recommendation: DatabaseRecommendation,\n    database: Database,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.InsightsTipGenerated, {\n        recommendationName: recommendation.name,\n        databaseId: database.id,\n        provider: database.provider,\n      });\n    } catch (e) {\n      // ignore\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Patch,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { ApiTags } from '@nestjs/swagger';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator';\nimport { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseRecommendationsResponse } from 'src/modules/database-recommendation/dto/database-recommendations.response';\nimport {\n  ModifyDatabaseRecommendationDto,\n  DeleteDatabaseRecommendationDto,\n  DeleteDatabaseRecommendationResponse,\n} from './dto';\n\n@ApiTags('Database Recommendations')\n@Controller('/recommendations')\nexport class DatabaseRecommendationController {\n  constructor(private service: DatabaseRecommendationService) {}\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get database recommendations',\n    responses: [\n      {\n        status: 200,\n        type: DatabaseRecommendationsResponse,\n      },\n    ],\n  })\n  @Get('')\n  async list(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<DatabaseRecommendationsResponse> {\n    return this.service.list(clientMetadata);\n  }\n\n  @Patch('/read')\n  @ApiRedisParams()\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Mark all database recommendations as read',\n    responses: [\n      {\n        status: 200,\n      },\n    ],\n  })\n  async read(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<void> {\n    return this.service.read(clientMetadata);\n  }\n\n  @Patch(':id')\n  @ApiEndpoint({\n    description: 'Update database recommendation by id',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: \"Updated database recommendation' response\",\n        type: DatabaseRecommendation,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  async modify(\n    @Param('id') id: string,\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: ModifyDatabaseRecommendationDto,\n  ): Promise<DatabaseRecommendation> {\n    return await this.service.update(clientMetadata, id, dto);\n  }\n\n  @Delete('')\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete many recommendations by ids',\n    responses: [\n      {\n        status: 200,\n        description: 'Delete many recommendations by ids response',\n        type: DeleteDatabaseRecommendationDto,\n      },\n    ],\n  })\n  @UsePipes(\n    new ValidationPipe({\n      transform: true,\n      whitelist: true,\n      forbidNonWhitelisted: true,\n    }),\n  )\n  async bulkDeleteDatabaseRecommendation(\n    @BrowserClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: DeleteDatabaseRecommendationDto,\n  ): Promise<DeleteDatabaseRecommendationResponse> {\n    return await this.service.bulkDelete(clientMetadata, dto.ids);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.gateway.ts",
    "content": "import { Server } from 'socket.io';\nimport { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';\nimport config, { Config } from 'src/utils/config';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { RecommendationServerEvents } from 'src/modules/database-recommendation/constants';\nimport { DatabaseRecommendationsResponse } from 'src/modules/database-recommendation/dto/database-recommendations.response';\nimport { SessionMetadata } from 'src/common/models';\nimport { getUserRoom } from 'src/constants/websocket-rooms';\n\nconst SOCKETS_CONFIG = config.get('sockets') as Config['sockets'];\n\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n  serveClient: SOCKETS_CONFIG.serveClient,\n})\nexport class DatabaseRecommendationGateway {\n  @WebSocketServer() wss: Server;\n\n  @OnEvent(RecommendationServerEvents.Recommendation)\n  notify({ userId }: SessionMetadata, data: DatabaseRecommendationsResponse) {\n    this.wss\n      .to(getUserRoom(userId))\n      .emit(RecommendationServerEvents.Recommendation, data);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { DatabaseRecommendationController } from 'src/modules/database-recommendation/database-recommendation.controller';\nimport { DatabaseRecommendationService } from 'src/modules/database-recommendation/database-recommendation.service';\nimport { RecommendationScanner } from 'src/modules/database-recommendation/scanner/recommendations.scanner';\nimport { RecommendationProvider } from 'src/modules/database-recommendation/scanner/recommendation.provider';\nimport { DatabaseRecommendationRepository } from 'src/modules/database-recommendation/repositories/database-recommendation.repository';\nimport { LocalDatabaseRecommendationRepository } from 'src/modules/database-recommendation/repositories/local.database.recommendation.repository';\nimport { DatabaseRecommendationGateway } from 'src/modules/database-recommendation/database-recommendation.gateway';\nimport { DatabaseRecommendationEmitter } from 'src/modules/database-recommendation/providers/database-recommendation.emitter';\nimport { DatabaseRecommendationAnalytics } from 'src/modules/database-recommendation/database-recommendation.analytics';\n\n@Module({})\nexport class DatabaseRecommendationModule {\n  static register(\n    databaseRecommendationRepository: Type<DatabaseRecommendationRepository> = LocalDatabaseRecommendationRepository,\n  ) {\n    return {\n      module: DatabaseRecommendationModule,\n      controllers: [DatabaseRecommendationController],\n      providers: [\n        DatabaseRecommendationService,\n        RecommendationScanner,\n        RecommendationProvider,\n        DatabaseRecommendationGateway,\n        DatabaseRecommendationEmitter,\n        DatabaseRecommendationAnalytics,\n        {\n          provide: DatabaseRecommendationRepository,\n          useClass: databaseRecommendationRepository,\n        },\n      ],\n      exports: [DatabaseRecommendationService, RecommendationScanner],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  MockType,\n  mockDatabase,\n  mockDatabaseRecommendationAnalytics,\n  mockDatabaseRecommendationRepository,\n  mockDatabaseService,\n  mockRecommendationScanner,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { ClientContext, ClientMetadata } from 'src/common/models';\nimport { DatabaseService } from '../database/database.service';\nimport { DatabaseRecommendationAnalytics } from './database-recommendation.analytics';\nimport { DatabaseRecommendationService } from './database-recommendation.service';\nimport { ModifyDatabaseRecommendationDto } from './dto';\nimport { DatabaseRecommendation } from './models';\nimport { DatabaseRecommendationRepository } from './repositories/database-recommendation.repository';\nimport { RecommendationScanner } from './scanner/recommendations.scanner';\n\ndescribe('DatabaseRecommendationService', () => {\n  const clientMetadata: ClientMetadata = {\n    sessionMetadata: mockSessionMetadata,\n    databaseId: '1',\n    context: ClientContext.Browser,\n    db: 1,\n  };\n\n  let service: DatabaseRecommendationService;\n  let databaseRecommendationRepository: MockType<DatabaseRecommendationRepository>;\n  let databaseService: MockType<DatabaseService>;\n  let analytics: MockType<DatabaseRecommendationAnalytics>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: DatabaseRecommendationRepository,\n          useFactory: mockDatabaseRecommendationRepository,\n        },\n        {\n          provide: RecommendationScanner,\n          useFactory: mockRecommendationScanner,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: DatabaseRecommendationAnalytics,\n          useFactory: mockDatabaseRecommendationAnalytics,\n        },\n        DatabaseRecommendationService,\n      ],\n    }).compile();\n\n    databaseRecommendationRepository = await module.get(\n      DatabaseRecommendationRepository,\n    );\n    databaseService = await module.get(DatabaseService);\n    analytics = await module.get(DatabaseRecommendationAnalytics);\n    service = module.get(DatabaseRecommendationService);\n  });\n\n  describe('create', () => {\n    it('should create a new recommendation', async () => {\n      const recommendationEntity: DatabaseRecommendation = {\n        id: '1',\n        databaseId: '1',\n        name: 'testDb',\n      };\n      databaseRecommendationRepository.create.mockResolvedValueOnce(\n        recommendationEntity,\n      );\n\n      await service.create(clientMetadata, recommendationEntity);\n\n      expect(databaseRecommendationRepository.create).toHaveBeenCalledWith(\n        clientMetadata.sessionMetadata,\n        recommendationEntity,\n      );\n      expect(databaseService.get).toHaveBeenCalledWith(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n      );\n      expect(analytics.sendCreatedRecommendationEvent).toHaveBeenCalledWith(\n        clientMetadata.sessionMetadata,\n        recommendationEntity,\n        mockDatabase,\n      );\n    });\n  });\n\n  describe('list', () => {\n    it('should return a list of recommendations for client metadata db', async () => {\n      await service.list(clientMetadata);\n\n      expect(databaseRecommendationRepository.list).toHaveBeenLastCalledWith({\n        ...clientMetadata,\n      });\n    });\n\n    it('should return a list of recommendations for database fetched by session metadata', async () => {\n      databaseService.get.mockResolvedValueOnce({ ...mockDatabase, db: 66 });\n      const clientMetadataWithoutDb = { ...clientMetadata, db: undefined };\n      await service.list(clientMetadataWithoutDb);\n      expect(databaseRecommendationRepository.list).toHaveBeenCalledWith({\n        ...clientMetadataWithoutDb,\n        db: 66,\n      });\n    });\n\n    it('should return a list of recommendations - default db to 0 if not passed in or retrieved from service', async () => {\n      const clientMetadataWithoutDb = { ...clientMetadata, db: undefined };\n      await service.list(clientMetadataWithoutDb);\n      expect(databaseRecommendationRepository.list).toHaveBeenCalledWith({\n        ...clientMetadataWithoutDb,\n        db: 0,\n      });\n    });\n  });\n\n  describe('read', () => {\n    it('should mark recommendations as read', async () => {\n      await service.read(clientMetadata);\n      expect(databaseRecommendationRepository.read).toHaveBeenCalledWith(\n        clientMetadata,\n      );\n    });\n  });\n\n  describe('update', () => {\n    it('should update a recommendation', async () => {\n      const id = '55';\n      const dto: ModifyDatabaseRecommendationDto =\n        {} as unknown as ModifyDatabaseRecommendationDto;\n\n      await service.update(clientMetadata, id, dto);\n\n      expect(databaseRecommendationRepository.update).toHaveBeenCalledWith(\n        clientMetadata,\n        id,\n        dto,\n      );\n    });\n  });\n\n  describe('sync', () => {\n    it('should sync recommendations', async () => {\n      const recommendation = {} as unknown as DatabaseRecommendation;\n      const recommendations = [recommendation];\n\n      await service.sync(clientMetadata, recommendations);\n\n      expect(databaseRecommendationRepository.sync).toHaveBeenCalledWith(\n        clientMetadata,\n        recommendations,\n      );\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a recommendation', async () => {\n      const id = '55';\n\n      await service.delete(clientMetadata, id);\n\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        id,\n      );\n    });\n  });\n\n  describe('bulk delete', () => {\n    it('should delete multiple recommendations, all succeed', async () => {\n      const ids = ['1', '2', '3'];\n\n      const result = await service.bulkDelete(clientMetadata, ids);\n\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        '1',\n      );\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        '2',\n      );\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        '3',\n      );\n\n      expect(result.affected).toBe(3);\n    });\n\n    it('should delete multiple recommendations, some fail', async () => {\n      const ids = ['1', '2', '3'];\n\n      databaseRecommendationRepository.delete.mockRejectedValueOnce(\n        new Error('Failed to delete'),\n      );\n\n      const result = await service.bulkDelete(clientMetadata, ids);\n\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        '1',\n      );\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        '2',\n      );\n      expect(databaseRecommendationRepository.delete).toHaveBeenCalledWith(\n        clientMetadata,\n        '3',\n      );\n\n      expect(result.affected).toBe(2);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/database-recommendation.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { sum } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseRecommendationRepository } from 'src/modules/database-recommendation/repositories/database-recommendation.repository';\nimport { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\nimport { RecommendationScanner } from 'src/modules/database-recommendation/scanner/recommendations.scanner';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseRecommendationsResponse } from 'src/modules/database-recommendation/dto/database-recommendations.response';\nimport { Recommendation } from 'src/modules/database-analysis/models/recommendation';\nimport {\n  ModifyDatabaseRecommendationDto,\n  DeleteDatabaseRecommendationResponse,\n} from './dto';\nimport { DatabaseRecommendationAnalytics } from './database-recommendation.analytics';\nimport { DatabaseService } from '../database/database.service';\n\n@Injectable()\nexport class DatabaseRecommendationService {\n  private logger = new Logger('DatabaseRecommendationService');\n\n  constructor(\n    private readonly databaseRecommendationRepository: DatabaseRecommendationRepository,\n    private readonly scanner: RecommendationScanner,\n    private readonly databaseService: DatabaseService,\n    private readonly analytics: DatabaseRecommendationAnalytics,\n  ) {}\n\n  /**\n   * Create recommendation entity\n   * @param clientMetadata\n   * @param entity\n   */\n  public async create(\n    clientMetadata: ClientMetadata,\n    entity: DatabaseRecommendation,\n  ): Promise<DatabaseRecommendation> {\n    const recommendation = await this.databaseRecommendationRepository.create(\n      clientMetadata.sessionMetadata,\n      entity,\n    );\n\n    const database = await this.databaseService.get(\n      clientMetadata.sessionMetadata,\n      clientMetadata?.databaseId,\n    );\n\n    this.analytics.sendCreatedRecommendationEvent(\n      clientMetadata.sessionMetadata,\n      recommendation,\n      database,\n    );\n\n    return recommendation;\n  }\n\n  /**\n   * Get recommendations list for particular database\n   * @param clientMetadata\n   */\n  async list(\n    clientMetadata: ClientMetadata,\n  ): Promise<DatabaseRecommendationsResponse> {\n    this.logger.debug('Getting database recommendations', clientMetadata);\n    const db =\n      clientMetadata.db ??\n      (\n        await this.databaseService.get(\n          clientMetadata.sessionMetadata,\n          clientMetadata.databaseId,\n        )\n      )?.db ??\n      0;\n    return this.databaseRecommendationRepository.list({\n      ...clientMetadata,\n      db,\n    });\n  }\n\n  private async checkRecommendation(\n    recommendationName: string,\n    exists: boolean,\n    clientMetadata: ClientMetadata,\n    data: any,\n  ): Promise<DatabaseRecommendation> {\n    if (!exists) {\n      const recommendation = await this.scanner.determineRecommendation(\n        clientMetadata.sessionMetadata,\n        recommendationName,\n        data,\n      );\n\n      if (recommendation) {\n        const entity = plainToInstance(DatabaseRecommendation, {\n          databaseId: clientMetadata?.databaseId,\n          ...recommendation,\n        });\n\n        return await this.create(clientMetadata, entity);\n      }\n    }\n\n    return null;\n  }\n\n  public async checkMulti(\n    clientMetadata: ClientMetadata,\n    recommendationNames: string[],\n    data: any,\n  ): Promise<Record<string, DatabaseRecommendation>> {\n    try {\n      const newClientMetadata = {\n        ...clientMetadata,\n        db:\n          clientMetadata.db ??\n          (\n            await this.databaseService.get(\n              clientMetadata.sessionMetadata,\n              clientMetadata.databaseId,\n            )\n          )?.db ??\n          0,\n      };\n      const isRecommendationExist =\n        await this.databaseRecommendationRepository.isExistMulti(\n          newClientMetadata,\n          recommendationNames,\n        );\n\n      const results = await Promise.all(\n        recommendationNames.map((name) =>\n          this.checkRecommendation(\n            name,\n            isRecommendationExist[name],\n            newClientMetadata,\n            data,\n          ),\n        ),\n      );\n\n      return results.reduce(\n        (acc, result, idx) => ({\n          ...acc,\n          [recommendationNames[idx]]: result,\n        }),\n        {},\n      );\n    } catch (e) {\n      this.logger.warn('Unable to check recommendation', e, clientMetadata);\n      return {};\n    }\n  }\n\n  /**\n   * Check recommendation condition\n   * @param clientMetadata\n   * @param recommendationName\n   * @param data\n   */\n  public async check(\n    clientMetadata: ClientMetadata,\n    recommendationName: string,\n    data: any,\n  ): Promise<DatabaseRecommendation> {\n    try {\n      const result = await this.checkMulti(\n        clientMetadata,\n        [recommendationName],\n        data,\n      );\n      return result[recommendationName];\n    } catch (e) {\n      return null;\n    }\n  }\n\n  /**\n   * Mark all recommendations as read for particular database\n   * @param clientMetadata\n   */\n  public async read(clientMetadata: ClientMetadata): Promise<void> {\n    this.logger.debug('Reading database recommendations');\n    return this.databaseRecommendationRepository.read(clientMetadata);\n  }\n\n  /**\n   * Update extended recommendation\n   * @param clientMetadata\n   * @param id\n   * @param dto\n   */\n  public async update(\n    clientMetadata: ClientMetadata,\n    id: string,\n    dto: ModifyDatabaseRecommendationDto,\n  ): Promise<DatabaseRecommendation> {\n    this.logger.debug(\n      `Update database extended recommendations id:${id}`,\n      clientMetadata,\n    );\n    return this.databaseRecommendationRepository.update(\n      clientMetadata,\n      id,\n      dto,\n    );\n  }\n\n  /**\n   * Sync db analysis recommendations and live recommendations\n   * @param clientMetadata\n   * @param recommendations\n   */\n  public async sync(\n    clientMetadata: ClientMetadata,\n    recommendations: Recommendation[],\n  ): Promise<void> {\n    return this.databaseRecommendationRepository.sync(\n      clientMetadata,\n      recommendations,\n    );\n  }\n\n  /**\n   * Delete database recommendation by id\n   * @param clientMetadata\n   * @param id\n   */\n  async delete(clientMetadata: ClientMetadata, id: string): Promise<void> {\n    this.logger.debug(`Deleting recommendation: ${id}`, clientMetadata);\n    await this.databaseRecommendationRepository.delete(clientMetadata, id);\n  }\n\n  /**\n   * Bulk delete recommendations. Uses \"delete\" method and skipping error\n   * Returns successfully deleted recommendations number\n   * @param clientMetadata\n   * @param ids\n   */\n  async bulkDelete(\n    clientMetadata: ClientMetadata,\n    ids: string[],\n  ): Promise<DeleteDatabaseRecommendationResponse> {\n    this.logger.debug(`Deleting many recommendations: ${ids}`, clientMetadata);\n\n    return {\n      affected: sum(\n        await Promise.all(\n          ids.map(async (id) => {\n            try {\n              await this.delete(clientMetadata, id);\n              return 1;\n            } catch (e) {\n              return 0;\n            }\n          }),\n        ),\n      ),\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/dto/create.database-recommendation.dto.ts",
    "content": "import { IsNotEmpty, IsString } from 'class-validator';\n\nexport class CreateRecommendationDto {\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  constructor(dto: CreateRecommendationDto) {\n    Object.assign(this, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/dto/database-recommendations.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\n\nexport class DatabaseRecommendationsResponse {\n  @ApiProperty({\n    type: () => DatabaseRecommendation,\n    example: [{ name: 'bigSet', read: false }],\n    isArray: true,\n    description: 'Ordered recommendations list',\n  })\n  @Type(() => DatabaseRecommendation)\n  @Expose()\n  recommendations: DatabaseRecommendation[];\n\n  @ApiProperty({\n    type: Number,\n    example: 2,\n    description: 'Number of unread recommendations',\n  })\n  @Expose()\n  totalUnread: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/dto/delete.database-recommendation.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { ArrayNotEmpty, IsArray, IsDefined } from 'class-validator';\n\nexport class DeleteDatabaseRecommendationDto {\n  @ApiProperty({\n    description: 'The unique IDs of the database recommendation requested',\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  ids: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/dto/delete.database-recommendation.response.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class DeleteDatabaseRecommendationResponse {\n  @ApiProperty({\n    description: 'Number of affected recommendations',\n    type: Number,\n  })\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/dto/index.ts",
    "content": "export * from './create.database-recommendation.dto';\nexport * from './modify.database-recommendation.dto';\nexport * from './delete.database-recommendation.dto';\nexport * from './delete.database-recommendation.response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/dto/modify.database-recommendation.dto.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsBoolean, IsEnum, IsOptional } from 'class-validator';\nimport { Vote } from 'src/modules/database-recommendation/models';\n\nexport class ModifyDatabaseRecommendationDto {\n  @ApiPropertyOptional({\n    description: 'Recommendation vote',\n    default: null,\n    enum: Vote,\n  })\n  @IsOptional()\n  @IsEnum(Vote, {\n    message: `vote must be a valid enum value. Valid values: ${Object.values(\n      Vote,\n    )}.`,\n  })\n  vote?: Vote;\n\n  @ApiPropertyOptional({\n    description: 'Hide recommendation',\n    type: Boolean,\n  })\n  @IsOptional()\n  @IsBoolean()\n  hide?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/entities/database-recommendation.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  Index,\n  Unique,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Entity('database_recommendations')\n@Unique(['databaseId', 'name'])\nexport class DatabaseRecommendationEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Index()\n  @Expose()\n  databaseId: string;\n\n  @ManyToOne(() => DatabaseEntity, {\n    nullable: false,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn({ name: 'databaseId' })\n  database: DatabaseEntity;\n\n  @Expose()\n  @Column({ nullable: false })\n  name: string;\n\n  @Expose()\n  @Column({ nullable: false, default: false })\n  read?: boolean = false;\n\n  @Expose()\n  @Column({ nullable: false, default: false })\n  disabled?: boolean = false;\n\n  @Expose()\n  @Column({\n    nullable: true,\n  })\n  vote?: string;\n\n  @Expose()\n  @Column({ nullable: false, default: false })\n  hide?: boolean = false;\n\n  @CreateDateColumn()\n  @Index()\n  @Expose()\n  createdAt: Date;\n\n  @Column({ nullable: true, type: 'blob' })\n  @DataAsJsonString()\n  @Expose()\n  params?: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  constructor(entity: Partial<DatabaseRecommendationEntity>) {\n    Object.assign(this, entity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/models/database-recommendation-params.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { IsRedisString, RedisStringType } from 'src/common/decorators';\n\nexport class DatabaseRecommendationParams {\n  @IsRedisString({ each: true })\n  @RedisStringType({ each: true })\n  keys?: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/models/database-recommendation.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsEnum, IsOptional, IsBoolean } from 'class-validator';\nimport { DatabaseRecommendationParams } from 'src/modules/database-recommendation/models';\n\nexport enum Vote {\n  DoubleLike = 'very useful',\n  Like = 'useful',\n  Dislike = 'not useful',\n}\n\nexport class DatabaseRecommendation {\n  @ApiProperty({\n    description: 'Recommendation id',\n    type: String,\n    example: 'id',\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Recommendation name',\n    type: String,\n    example: 'luaScript',\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'Database ID to which recommendation belongs',\n    type: String,\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiPropertyOptional({\n    description: 'Determines if recommendation was shown to user',\n    type: Boolean,\n    example: false,\n  })\n  @Expose()\n  @IsOptional()\n  @IsBoolean({ always: true })\n  read?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Should this recommendation shown to user',\n    type: Boolean,\n    example: false,\n  })\n  @Expose()\n  @IsOptional()\n  @IsBoolean()\n  disabled?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Recommendation vote',\n    default: Vote.Like,\n    enum: Vote,\n  })\n  @IsEnum(Vote)\n  @IsOptional()\n  @Expose()\n  vote?: Vote = null;\n\n  @ApiPropertyOptional({\n    description: 'Should this recommendation hidden',\n    type: Boolean,\n    example: false,\n  })\n  @IsOptional()\n  @IsBoolean()\n  @Expose()\n  hide?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'Additional recommendation params',\n    type: Object,\n  })\n  @IsOptional()\n  @Expose()\n  params?: DatabaseRecommendationParams;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/models/index.ts",
    "content": "export * from './database-recommendation';\nexport * from './database-recommendation-params';\nexport * from './searchJSON';\nexport * from './integersInSet';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/models/integersInSet.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport interface IntegersInSets {\n  client: RedisClient;\n  keyName: RedisString;\n  databaseId: string;\n  members: RedisString[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/models/searchJSON.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class SearchJSON {\n  @ApiProperty({\n    description: 'Redis client',\n  })\n  @Expose()\n  client: RedisClient;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: String,\n    example: 'id',\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiProperty({\n    description: 'Keys info',\n    type: GetKeyInfoResponse,\n  })\n  @Expose()\n  keys: GetKeyInfoResponse[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/providers/database-recommendation.emitter.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockDatabaseRecommendation,\n  mockDatabaseRecommendationRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport axios from 'axios';\nimport { RecommendationServerEvents } from 'src/modules/database-recommendation/constants';\nimport { DatabaseRecommendationEmitter } from 'src/modules/database-recommendation/providers/database-recommendation.emitter';\nimport { DatabaseRecommendationRepository } from '../repositories/database-recommendation.repository';\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\nconst mockEventEmitter = {\n  emit: jest.fn(),\n};\n\ndescribe('DatabaseRecommendationEmitter', () => {\n  let service: DatabaseRecommendationEmitter;\n  let databaseRecommendationRepositoryMock: MockType<DatabaseRecommendationRepository>;\n  let emitter: MockType<EventEmitter2>;\n\n  beforeEach(async () => {\n    jest.mock('axios', () => mockedAxios);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseRecommendationEmitter,\n        EventEmitter2,\n        {\n          provide: EventEmitter2,\n          useFactory: () => mockEventEmitter,\n        },\n        {\n          provide: DatabaseRecommendationRepository,\n          useFactory: mockDatabaseRecommendationRepository,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(DatabaseRecommendationEmitter);\n    databaseRecommendationRepositoryMock = await module.get(\n      DatabaseRecommendationRepository,\n    );\n    emitter = await module.get(EventEmitter2);\n    emitter.emit.mockReset();\n  });\n\n  describe('newRecommendation', () => {\n    it('should return undefined if no recommendations passed', async () => {\n      await service.newRecommendation({\n        sessionMetadata: mockSessionMetadata,\n        recommendations: [],\n      });\n      expect(emitter.emit).toHaveBeenCalledTimes(0);\n    });\n    it('should emit 2 new recommendations', async () => {\n      databaseRecommendationRepositoryMock.getTotalUnread.mockResolvedValueOnce(\n        2,\n      );\n\n      await service.newRecommendation({\n        sessionMetadata: mockSessionMetadata,\n        recommendations: [\n          mockDatabaseRecommendation,\n          mockDatabaseRecommendation,\n        ],\n      });\n      expect(emitter.emit).toHaveBeenCalledTimes(1);\n      expect(emitter.emit).toHaveBeenCalledWith(\n        RecommendationServerEvents.Recommendation,\n        mockSessionMetadata,\n        {\n          recommendations: [\n            mockDatabaseRecommendation,\n            mockDatabaseRecommendation,\n          ],\n          totalUnread: 2,\n        },\n      );\n    });\n    it('should log an error but not fail', async () => {\n      databaseRecommendationRepositoryMock.getTotalUnread.mockRejectedValueOnce(\n        new Error('test error'),\n      );\n\n      await service.newRecommendation({\n        sessionMetadata: mockSessionMetadata,\n        recommendations: [mockDatabaseRecommendation],\n      });\n\n      expect(emitter.emit).toHaveBeenCalledTimes(0);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/providers/database-recommendation.emitter.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { EventEmitter2, OnEvent } from '@nestjs/event-emitter';\nimport { plainToInstance } from 'class-transformer';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  RecommendationEvents,\n  RecommendationServerEvents,\n} from 'src/modules/database-recommendation/constants';\nimport { DatabaseRecommendationsResponse } from 'src/modules/database-recommendation/dto/database-recommendations.response';\nimport { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\nimport { DatabaseRecommendationRepository } from '../repositories/database-recommendation.repository';\n\n@Injectable()\nexport class DatabaseRecommendationEmitter {\n  private logger: Logger = new Logger('DatabaseRecommendationEmitter');\n\n  constructor(\n    private readonly databaseRecommendationRepository: DatabaseRecommendationRepository,\n    private readonly eventEmitter: EventEmitter2,\n  ) {}\n\n  @OnEvent(RecommendationEvents.NewRecommendation)\n  async newRecommendation({\n    sessionMetadata,\n    recommendations,\n  }: {\n    sessionMetadata: SessionMetadata;\n    recommendations: DatabaseRecommendation[];\n  }) {\n    try {\n      if (!recommendations?.length) {\n        return;\n      }\n\n      this.logger.debug(\n        `${recommendations.length} new recommendation(s) to emit`,\n      );\n\n      const totalUnread =\n        await this.databaseRecommendationRepository.getTotalUnread(\n          sessionMetadata,\n          recommendations[0].databaseId,\n        );\n\n      this.eventEmitter.emit(\n        RecommendationServerEvents.Recommendation,\n        sessionMetadata,\n        plainToInstance(DatabaseRecommendationsResponse, {\n          totalUnread,\n          recommendations,\n        }),\n      );\n    } catch (e) {\n      this.logger.error('Unable to prepare dto for recommendations', e);\n      // ignore error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/repositories/database-recommendation.repository.ts",
    "content": "import { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\nimport { DatabaseRecommendationsResponse } from 'src/modules/database-recommendation/dto/database-recommendations.response';\nimport { ModifyDatabaseRecommendationDto } from 'src/modules/database-recommendation/dto';\nimport { Recommendation } from 'src/modules/database-analysis/models/recommendation';\nimport { ClientMetadata, SessionMetadata } from 'src/common/models';\n\nexport abstract class DatabaseRecommendationRepository {\n  /**\n   * Create new recommendation\n   * @param sessionMetadata\n   * @param entity\n   * @return DatabaseRecommendation\n   */\n  abstract create(\n    sessionMetadata: SessionMetadata,\n    entity: DatabaseRecommendation,\n  ): Promise<DatabaseRecommendation>;\n\n  /**\n   * Get all recommendations from database\n   * Fields: [r.id', 'r.name', 'r.read', 'r.vote', 'r.hide', 'r.params']\n   * @param clientMetadata\n   * @return DatabaseRecommendationsResponse\n   */\n  abstract list(\n    clientMetadata: ClientMetadata,\n  ): Promise<DatabaseRecommendationsResponse>;\n\n  /**\n   * Mark all recommendations as read by database id\n   * @param clientMetadata\n   */\n  abstract read(clientMetadata: ClientMetadata): Promise<void>;\n\n  /**\n   * Update single recommendation and return updated\n   * @param clientMetadata\n   * @param id\n   * @param recommendation\n   * @return DatabaseRecommendation\n   */\n  abstract update(\n    clientMetadata: ClientMetadata,\n    id: string,\n    recommendation: ModifyDatabaseRecommendationDto,\n  ): Promise<DatabaseRecommendation>;\n\n  /**\n   * Check if recommendation already exist in repository\n   * @param clientMetadata\n   * @param name\n   * @return Boolean\n   */\n  abstract isExist(\n    clientMetadata: ClientMetadata,\n    name: string,\n  ): Promise<boolean>;\n\n  /**\n   * Check if one or more recommendations exist in repository\n   * @param clientMetadata\n   * @param names\n   */\n  abstract isExistMulti(\n    clientMetadata: ClientMetadata,\n    names: string[],\n  ): Promise<Map<string, boolean>>;\n\n  /**\n   * Get database recommendation by id\n   * @param sessionMetadata\n   * @param id\n   * @return DatabaseRecommendation\n   */\n  abstract get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<DatabaseRecommendation>;\n\n  /**\n   * Sync db analysis recommendations with insights recommendations\n   * @param clientMetadata\n   * @param dbAnalysisRecommendations\n   */\n  abstract sync(\n    clientMetadata: ClientMetadata,\n    dbAnalysisRecommendations: Recommendation[],\n  ): Promise<void>;\n\n  /**\n   * Delete database recommendation by id\n   * @param clientMetadata\n   * @param id\n   */\n  abstract delete(clientMetadata: ClientMetadata, id: string): Promise<void>;\n\n  /**\n   * Get total unread recommendations\n   */\n  abstract getTotalUnread(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<number>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/repositories/local.database.recommendation.repository.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { NotFoundException } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockEncryptionService,\n  mockRepository,\n  mockDatabaseRecommendationEntity,\n  mockRecommendationName,\n  mockClientMetadata,\n  mockDatabaseRecommendation,\n  mockEventEmitter,\n  MockType,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { LocalDatabaseRecommendationRepository } from 'src/modules/database-recommendation/repositories/local.database.recommendation.repository';\nimport { DatabaseRecommendationEntity } from 'src/modules/database-recommendation/entities/database-recommendation.entity';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\ndescribe('LocalDatabaseRecommendationRepository', () => {\n  let service: LocalDatabaseRecommendationRepository;\n  let encryptionService: MockType<EncryptionService>;\n  let repository: MockType<Repository<DatabaseRecommendationEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalDatabaseRecommendationRepository,\n        {\n          provide: getRepositoryToken(DatabaseRecommendationEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n        {\n          provide: EventEmitter2,\n          useValue: mockEventEmitter,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(\n      getRepositoryToken(DatabaseRecommendationEntity),\n    );\n    encryptionService = await module.get(EncryptionService);\n    service = module.get(LocalDatabaseRecommendationRepository);\n\n    repository.findOneBy.mockResolvedValue(mockDatabaseRecommendationEntity);\n    repository.save.mockResolvedValue(mockDatabaseRecommendationEntity);\n    repository.update.mockResolvedValue(mockDatabaseRecommendationEntity);\n\n    when(encryptionService.encrypt)\n      .calledWith(JSON.stringify(mockDatabaseRecommendation.params))\n      .mockReturnValue({\n        encryption: mockDatabaseRecommendationEntity.encryption,\n        data: mockDatabaseRecommendationEntity.params,\n      });\n    when(encryptionService.decrypt)\n      .calledWith(mockDatabaseRecommendationEntity.params, expect.anything())\n      .mockReturnValue(JSON.stringify(mockDatabaseRecommendation.params));\n  });\n\n  describe('isExist', () => {\n    it('should return true when receive database entity', async () => {\n      expect(\n        await service.isExist(mockClientMetadata, mockRecommendationName),\n      ).toEqual(true);\n    });\n\n    it('should return false when no database received', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      expect(\n        await service.isExist(mockClientMetadata, mockRecommendationName),\n      ).toEqual(false);\n    });\n\n    it('should return false when received error', async () => {\n      repository.findOneBy.mockRejectedValueOnce(new Error());\n      expect(\n        await service.isExist(mockClientMetadata, mockRecommendationName),\n      ).toEqual(false);\n    });\n  });\n\n  describe('isExistMulti', () => {\n    it('should return results for multiple recommendation names', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.findOneBy.mockResolvedValueOnce({});\n      expect(\n        await service.isExistMulti(mockClientMetadata, ['test1', 'test2']),\n      ).toEqual({\n        test1: false,\n        test2: true,\n      });\n    });\n\n    it('should return empty Map when received error', async () => {\n      repository.findOneBy.mockRejectedValueOnce(new Error());\n      expect(\n        await service.isExistMulti(mockClientMetadata, ['test1', 'test2']),\n      ).toEqual({});\n    });\n  });\n\n  describe('create', () => {\n    it('should create recommendation', async () => {\n      const result = await service.create(\n        mockClientMetadata.sessionMetadata,\n        mockDatabaseRecommendation,\n      );\n\n      expect(result).toEqual(mockDatabaseRecommendation);\n      expect(mockEventEmitter.emit).toHaveBeenCalledTimes(1);\n      expect(mockEventEmitter.emit).toHaveBeenCalledWith('new-recommendation', {\n        sessionMetadata: mockClientMetadata.sessionMetadata,\n        recommendations: [\n          {\n            databaseId: 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id',\n            disabled: false,\n            hide: false,\n            id: 'databaseRecommendationID',\n            name: 'string',\n            params: {},\n            read: false,\n            vote: null,\n          },\n        ],\n      });\n    });\n\n    it('should not create recommendation', async () => {\n      repository.save.mockRejectedValueOnce(new Error());\n\n      const result = await service.create(\n        mockClientMetadata.sessionMetadata,\n        mockDatabaseRecommendation,\n      );\n\n      expect(result).toEqual(null);\n      expect(mockEventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete recommendation by id', async () => {\n      repository.delete.mockResolvedValueOnce({ affected: 1 });\n\n      expect(await service.delete(mockClientMetadata, 'id')).toEqual(undefined);\n    });\n\n    it('should return NotFoundException when recommendation does not found', async () => {\n      repository.delete.mockResolvedValueOnce({ affected: 0 });\n\n      try {\n        await service.delete(mockClientMetadata, 'id');\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(\n          ERROR_MESSAGES.DATABASE_RECOMMENDATION_NOT_FOUND,\n        );\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/repositories/local.database.recommendation.repository.ts",
    "content": "import {\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport { DatabaseRecommendationEntity } from 'src/modules/database-recommendation/entities/database-recommendation.entity';\nimport { DatabaseRecommendationRepository } from 'src/modules/database-recommendation/repositories/database-recommendation.repository';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseRecommendation } from 'src/modules/database-recommendation/models';\nimport { ModifyDatabaseRecommendationDto } from 'src/modules/database-recommendation/dto';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { Recommendation } from 'src/modules/database-analysis/models/recommendation';\nimport { ClientMetadata, SessionMetadata } from 'src/common/models';\nimport { sortRecommendations, classToClass } from 'src/utils';\n\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseRecommendationsResponse } from 'src/modules/database-recommendation/dto/database-recommendations.response';\nimport { RecommendationEvents } from 'src/modules/database-recommendation/constants';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\n\n@Injectable()\nexport class LocalDatabaseRecommendationRepository extends DatabaseRecommendationRepository {\n  private readonly logger = new Logger('DatabaseRecommendationRepository');\n\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(DatabaseRecommendationEntity)\n    private readonly repository: Repository<DatabaseRecommendationEntity>,\n    private eventEmitter: EventEmitter2,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, ['params']);\n  }\n\n  /**\n   * Save entire entity\n   * @param sessionMetadata\n   * @param entity\n   */\n  async create(\n    sessionMetadata: SessionMetadata,\n    entity: DatabaseRecommendation,\n  ): Promise<DatabaseRecommendation> {\n    this.logger.debug('Creating database recommendation', sessionMetadata);\n\n    try {\n      const model = await this.repository.save(\n        await this.modelEncryptor.encryptEntity(\n          plainToInstance(DatabaseRecommendationEntity, entity),\n        ),\n      );\n\n      const recommendation = classToClass(\n        DatabaseRecommendation,\n        await this.modelEncryptor.decryptEntity(model, true),\n      );\n      this.eventEmitter.emit(RecommendationEvents.NewRecommendation, {\n        sessionMetadata,\n        recommendations: [recommendation],\n      });\n\n      return recommendation;\n    } catch (err) {\n      this.logger.error(\n        'Failed to create database recommendation',\n        err,\n        sessionMetadata,\n      );\n\n      return null;\n    }\n  }\n\n  /**\n   * Return list of database recommendations\n   * @param clientMetadata\n   */\n  async list(\n    clientMetadata: ClientMetadata,\n  ): Promise<DatabaseRecommendationsResponse> {\n    const { databaseId } = clientMetadata;\n    this.logger.debug('Getting database recommendations list', clientMetadata);\n    const entities = await this.repository\n      .createQueryBuilder('r')\n      .where({ databaseId })\n      .select([\n        'r.id',\n        'r.name',\n        'r.read',\n        'r.vote',\n        'r.hide',\n        'r.params',\n        'r.encryption',\n      ])\n      .orderBy('r.createdAt', 'DESC')\n      .getMany();\n\n    const totalUnread = await this.repository\n      .createQueryBuilder()\n      .where({ databaseId, read: false })\n      .getCount();\n\n    this.logger.debug('Succeed to get recommendations', clientMetadata);\n    const decryptedEntities = await Promise.all(\n      entities.map(async (entity) => {\n        try {\n          return await this.modelEncryptor.decryptEntity(entity, true);\n        } catch (e) {\n          return null;\n        }\n      }),\n    );\n    return classToClass(DatabaseRecommendationsResponse, {\n      recommendations: decryptedEntities,\n      totalUnread,\n    });\n  }\n\n  /**\n   * Read all recommendations\n   * @param clientMetadata\n   */\n  async read(clientMetadata: ClientMetadata): Promise<void> {\n    const { databaseId } = clientMetadata;\n    this.logger.debug('Marking all recommendations as read', clientMetadata);\n    await this.repository\n      .createQueryBuilder('r')\n      .update()\n      .where({ databaseId })\n      .set({ read: true })\n      .execute();\n  }\n\n  /**\n   * Update and return updated DatabaseRecommendation model\n   * @param clientMetadata\n   * @param id\n   * @param recommendation\n   */\n  async update(\n    clientMetadata: ClientMetadata,\n    id: string,\n    recommendation: ModifyDatabaseRecommendationDto,\n  ): Promise<DatabaseRecommendation> {\n    this.logger.debug(\n      `Updating database recommendation with id:${id}`,\n      clientMetadata,\n    );\n    const oldEntity = await this.modelEncryptor.decryptEntity(\n      await this.repository.findOneBy({ id }),\n    );\n    const newEntity = plainToInstance(\n      DatabaseRecommendationEntity,\n      recommendation,\n    );\n\n    if (!oldEntity) {\n      this.logger.error(\n        `Database recommendation with id:${id} was not Found`,\n        clientMetadata,\n      );\n      throw new NotFoundException(\n        ERROR_MESSAGES.DATABASE_RECOMMENDATION_NOT_FOUND,\n      );\n    }\n\n    const mergeResult = this.repository.merge(oldEntity, newEntity);\n    await this.repository.update(\n      id,\n      await this.modelEncryptor.encryptEntity(mergeResult),\n    );\n\n    this.logger.debug(\n      `Updated database recommendation with id:${id}`,\n      clientMetadata,\n    );\n\n    return this.get(clientMetadata.sessionMetadata, id);\n  }\n\n  /**\n   * Check is recommendation exist in database\n   * @param clientMetadata\n   * @param name\n   */\n  async isExist(\n    clientMetadata: ClientMetadata,\n    name: string,\n  ): Promise<boolean> {\n    const { databaseId } = clientMetadata;\n    try {\n      this.logger.debug(\n        `Checking is recommendation ${name} exist`,\n        clientMetadata,\n      );\n      const recommendation = await this.repository.findOneBy({\n        databaseId,\n        name,\n      });\n\n      this.logger.debug(\n        `Succeed to check is recommendation ${name} exist'`,\n        clientMetadata,\n      );\n      return !!recommendation;\n    } catch (err) {\n      this.logger.error(\n        `Failed to check is recommendation ${name} exist'`,\n        err,\n        clientMetadata,\n      );\n      return false;\n    }\n  }\n\n  /**\n   * Check if one or more recommendations exist in database\n   * @param clientMetadata\n   * @param names\n   */\n  async isExistMulti(\n    clientMetadata: ClientMetadata,\n    names: string[],\n  ): Promise<Map<string, boolean>> {\n    const { databaseId } = clientMetadata;\n\n    try {\n      this.logger.debug(\n        'Checking if recommendations exist',\n        names,\n        clientMetadata,\n      );\n\n      const results = await Promise.all(\n        names.map((name) => this.repository.findOneBy({ databaseId, name })),\n      );\n\n      return results.reduce(\n        (acc, result, idx) => ({\n          ...acc,\n          [names[idx]]: !!result,\n        }),\n        {} as Map<string, boolean>,\n      );\n    } catch (err) {\n      this.logger.error(\n        'Failed to check existence of recommendations',\n        err,\n        names,\n        clientMetadata,\n      );\n      return {} as Map<string, boolean>;\n    }\n  }\n\n  /**\n   * Get recommendation by id\n   * @param sessionMetadata\n   * @param id\n   */\n  public async get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<DatabaseRecommendation> {\n    this.logger.debug(`Getting recommendation with id: ${id}`, sessionMetadata);\n\n    const entity = await this.repository.findOneBy({ id });\n    const model = classToClass(\n      DatabaseRecommendation,\n      await this.modelEncryptor.decryptEntity(entity, true),\n    );\n\n    if (!model) {\n      this.logger.error(\n        `Not found recommendation with id: ${id}'`,\n        sessionMetadata,\n      );\n      return null;\n    }\n\n    this.logger.debug(\n      `Succeed to get recommendation with id: ${id}'`,\n      sessionMetadata,\n    );\n    return model;\n  }\n\n  /**\n   * Sync db analysis recommendations with live recommendations\n   * @param clientMetadata\n   * @param dbAnalysisRecommendations\n   */\n  async sync(\n    clientMetadata: ClientMetadata,\n    dbAnalysisRecommendations: Recommendation[],\n  ): Promise<void> {\n    this.logger.debug('Synchronization of recommendations', clientMetadata);\n    try {\n      const sortedRecommendations = sortRecommendations(\n        dbAnalysisRecommendations,\n      );\n      for (let i = 0; i < sortedRecommendations.length; i += 1) {\n        if (\n          !(await this.isExist(clientMetadata, sortedRecommendations[i].name))\n        ) {\n          const entity = plainToInstance(DatabaseRecommendation, {\n            databaseId: clientMetadata?.databaseId,\n            name: sortedRecommendations[i].name,\n            params: sortedRecommendations[i].params,\n          });\n          await this.create(clientMetadata.sessionMetadata, entity);\n        }\n      }\n    } catch (e) {\n      // ignore errors\n      this.logger.error(e);\n    }\n  }\n\n  /**\n   * Delete recommendation by id\n   * @param clientMetadata,\n   * @param id\n   */\n  public async delete(\n    clientMetadata: ClientMetadata,\n    id: string,\n  ): Promise<void> {\n    const { databaseId } = clientMetadata;\n    try {\n      const { affected } = await this.repository.delete({ databaseId, id });\n\n      if (!affected) {\n        this.logger.error(\n          `Recommendation with id:${id} was not Found`,\n          clientMetadata,\n        );\n        return Promise.reject(\n          new NotFoundException(\n            ERROR_MESSAGES.DATABASE_RECOMMENDATION_NOT_FOUND,\n          ),\n        );\n      }\n\n      this.logger.debug('Succeed to delete recommendation.', clientMetadata);\n    } catch (error) {\n      this.logger.error(`Failed to delete recommendation: ${id}`, error);\n      throw new InternalServerErrorException(error.message);\n    }\n  }\n\n  public async getTotalUnread(\n    _: SessionMetadata,\n    databaseId: string,\n  ): Promise<number> {\n    return await this.repository\n      .createQueryBuilder()\n      .where({ read: false, databaseId })\n      .getCount();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/recommendation.provider.spec.ts",
    "content": "import { Test } from '@nestjs/testing';\nimport { RecommendationProvider } from 'src/modules/database-recommendation/scanner/recommendation.provider';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport {\n  DefaultRecommendationStrategy,\n  RedisVersionStrategy,\n  SearchJSONStrategy,\n  BigSetStrategy,\n  RTSStrategy,\n  AvoidLogicalDatabasesStrategy,\n  ShardHashStrategy,\n  StringToJsonStrategy,\n  UseSmallerKeysStrategy,\n  AvoidLuaScriptsStrategy,\n  BigStringStrategy,\n  CompressionForListStrategy,\n  BigAmountConnectedClientsStrategy,\n  TryRdiStrategyStrategy,\n} from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('RecommendationProvider', () => {\n  beforeAll(async () => {\n    await Test.createTestingModule({\n      providers: [RecommendationProvider],\n    }).compile();\n  });\n  const service = new RecommendationProvider();\n\n  describe('getStrategy', () => {\n    [\n      [RECOMMENDATION_NAMES.SEARCH_JSON, new SearchJSONStrategy()],\n      [RECOMMENDATION_NAMES.REDIS_VERSION, new RedisVersionStrategy()],\n      [RECOMMENDATION_NAMES.BIG_SETS, new BigSetStrategy()],\n      [RECOMMENDATION_NAMES.RTS, new RTSStrategy()],\n      [\n        RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n        new AvoidLogicalDatabasesStrategy(),\n      ],\n      [RECOMMENDATION_NAMES.BIG_HASHES, new ShardHashStrategy()],\n      [RECOMMENDATION_NAMES.STRING_TO_JSON, new StringToJsonStrategy()],\n      [RECOMMENDATION_NAMES.USE_SMALLER_KEYS, new UseSmallerKeysStrategy()],\n      [RECOMMENDATION_NAMES.LUA_SCRIPT, new AvoidLuaScriptsStrategy()],\n      [RECOMMENDATION_NAMES.BIG_STRINGS, new BigStringStrategy()],\n      [\n        RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n        new CompressionForListStrategy(),\n      ],\n      [\n        RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n        new BigAmountConnectedClientsStrategy(),\n      ],\n      [RECOMMENDATION_NAMES.TRY_RDI, new TryRdiStrategyStrategy()],\n      ['default', new DefaultRecommendationStrategy()],\n      ['unknown', new DefaultRecommendationStrategy()],\n      [null, new DefaultRecommendationStrategy()],\n    ].forEach((tc) => {\n      it(`should return ${tc[1].constructor.name} for type: ${tc[0]}`, () => {\n        expect(service.getStrategy(tc[0] as string)).toEqual(tc[1]);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/recommendation.provider.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { IRecommendationStrategy } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport {\n  DefaultRecommendationStrategy,\n  RedisVersionStrategy,\n  SearchJSONStrategy,\n  BigSetStrategy,\n  RTSStrategy,\n  AvoidLogicalDatabasesStrategy,\n  ShardHashStrategy,\n  StringToJsonStrategy,\n  SearchVisualizationStrategy,\n  UseSmallerKeysStrategy,\n  AvoidLuaScriptsStrategy,\n  BigStringStrategy,\n  CompressionForListStrategy,\n  BigAmountConnectedClientsStrategy,\n  TryRdiStrategyStrategy,\n} from 'src/modules/database-recommendation/scanner/strategies';\n\n@Injectable()\nexport class RecommendationProvider {\n  private logger = new Logger('ZSetTypeInfoStrategy');\n\n  private strategies: Map<string, IRecommendationStrategy> = new Map();\n\n  constructor() {\n    this.strategies.set('default', new DefaultRecommendationStrategy());\n    this.strategies.set(\n      RECOMMENDATION_NAMES.REDIS_VERSION,\n      new RedisVersionStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.SEARCH_JSON,\n      new SearchJSONStrategy(),\n    );\n    this.strategies.set(RECOMMENDATION_NAMES.BIG_SETS, new BigSetStrategy());\n    this.strategies.set(RECOMMENDATION_NAMES.RTS, new RTSStrategy());\n    this.strategies.set(\n      RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n      new AvoidLogicalDatabasesStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.BIG_HASHES,\n      new ShardHashStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.STRING_TO_JSON,\n      new StringToJsonStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.SEARCH_VISUALIZATION,\n      new SearchVisualizationStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.USE_SMALLER_KEYS,\n      new UseSmallerKeysStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.LUA_SCRIPT,\n      new AvoidLuaScriptsStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.BIG_STRINGS,\n      new BigStringStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n      new CompressionForListStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n      new BigAmountConnectedClientsStrategy(),\n    );\n    this.strategies.set(\n      RECOMMENDATION_NAMES.TRY_RDI,\n      new TryRdiStrategyStrategy(),\n    );\n  }\n\n  getStrategy(type: string): IRecommendationStrategy {\n    this.logger.debug(`Getting ${type} recommendation strategy.`);\n\n    const strategy = this.strategies.get(type);\n\n    if (!strategy) {\n      this.logger.error(`Failed to get ${type} recommendation strategy.`);\n      return this.strategies.get('default');\n    }\n\n    return strategy;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/recommendation.strategy.interface.ts",
    "content": "import { DatabaseRecommendationParams } from 'src/modules/database-recommendation/models';\n\nexport interface IDatabaseRecommendationStrategyData {\n  isReached: boolean;\n  params?: DatabaseRecommendationParams;\n}\nexport interface IRecommendationStrategy {\n  isRecommendationReached(\n    data: any,\n  ): Promise<IDatabaseRecommendationStrategyData>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/recommendations.scanner.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { RecommendationScanner } from 'src/modules/database-recommendation/scanner/recommendations.scanner';\nimport { RecommendationProvider } from 'src/modules/database-recommendation/scanner/recommendation.provider';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport {\n  mockFeatureService,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { KnownFeatures } from 'src/modules/feature/constants';\n\nconst mockRecommendationStrategy = () => ({\n  isRecommendationReached: jest.fn(),\n});\n\nconst mockRecommendationProvider = () => ({\n  getStrategy: jest.fn(),\n});\n\nconst mockData = 'some data';\n\ndescribe('RecommendationScanner', () => {\n  let service: RecommendationScanner;\n  let recommendationProvider;\n  let recommendationStrategy;\n  let featureService: MockType<FeatureService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RecommendationScanner,\n        {\n          provide: RecommendationProvider,\n          useFactory: mockRecommendationProvider,\n        },\n        {\n          provide: FeatureService,\n          useFactory: mockFeatureService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<RecommendationScanner>(RecommendationScanner);\n    recommendationProvider = module.get<RecommendationProvider>(\n      RecommendationProvider,\n    );\n    featureService = module.get(FeatureService);\n    recommendationStrategy = mockRecommendationStrategy();\n    recommendationProvider.getStrategy.mockReturnValue(recommendationStrategy);\n  });\n\n  describe('determineRecommendation', () => {\n    it('should determine recommendation', async () => {\n      recommendationStrategy.isRecommendationReached.mockResolvedValue({\n        isReached: true,\n      });\n\n      expect(\n        await service.determineRecommendation(mockSessionMetadata, 'name', {\n          data: mockData,\n        }),\n      ).toEqual({ name: 'name' });\n      expect(featureService.isFeatureEnabled).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.InsightsRecommendations,\n      );\n    });\n\n    it('should return null when feature disabled', async () => {\n      featureService.isFeatureEnabled.mockResolvedValueOnce(false);\n\n      recommendationStrategy.isRecommendationReached.mockResolvedValue({\n        isReached: true,\n      });\n\n      expect(\n        await service.determineRecommendation(mockSessionMetadata, 'name', {\n          data: mockData,\n        }),\n      ).toEqual(null);\n    });\n\n    it('should return null when isRecommendationReached throw error', async () => {\n      recommendationStrategy.isRecommendationReached.mockRejectedValueOnce(\n        new Error(),\n      );\n\n      expect(\n        await service.determineRecommendation(mockSessionMetadata, 'name', {\n          data: mockData,\n        }),\n      ).toEqual(null);\n    });\n\n    it('should return null when isReached is false', async () => {\n      recommendationStrategy.isRecommendationReached.mockResolvedValue({\n        isReached: false,\n      });\n\n      expect(\n        await service.determineRecommendation(mockSessionMetadata, 'name', {\n          data: mockData,\n        }),\n      ).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/recommendations.scanner.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { RecommendationProvider } from 'src/modules/database-recommendation/scanner/recommendation.provider';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class RecommendationScanner {\n  constructor(\n    private readonly recommendationProvider: RecommendationProvider,\n    private readonly featureService: FeatureService,\n  ) {}\n\n  async determineRecommendation(\n    sessionMetadata: SessionMetadata,\n    name: string,\n    data: any,\n  ) {\n    if (\n      !(await this.featureService.isFeatureEnabled(\n        sessionMetadata,\n        KnownFeatures.InsightsRecommendations,\n      ))\n    ) {\n      return null;\n    }\n\n    const strategy = this.recommendationProvider.getStrategy(name);\n    try {\n      const recommendation = await strategy.isRecommendationReached(data);\n\n      if (recommendation.isReached) {\n        return { name, params: recommendation?.params };\n      }\n    } catch (err) {\n      // ignore errors\n      return null;\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy.spec.ts",
    "content": "import { DefaultRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('AbstractRecommendationStrategy', () => {\n  const strategy = new DefaultRecommendationStrategy();\n\n  describe('isRecommendationReached', () => {\n    it('should get is recommendation reached', async () => {\n      expect(await strategy.isRecommendationReached()).toEqual({\n        isReached: false,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy.ts",
    "content": "import {\n  IRecommendationStrategy,\n  IDatabaseRecommendationStrategyData,\n} from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport abstract class AbstractRecommendationStrategy\n  implements IRecommendationStrategy\n{\n  abstract isRecommendationReached(\n    data: any,\n    client?: RedisClient,\n  ): Promise<IDatabaseRecommendationStrategyData>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/avoid-logical-databases.strategy.spec.ts",
    "content": "import { AvoidLogicalDatabasesStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('AvoidLogicalDatabasesStrategy', () => {\n  let strategy: AvoidLogicalDatabasesStrategy;\n\n  beforeEach(async () => {\n    strategy = new AvoidLogicalDatabasesStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when database index was not changed', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          db: 2,\n          prevDb: 2,\n        }),\n      ).toEqual({ isReached: false });\n    });\n\n    it('should return true when database index was changed', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          db: 2,\n          prevDb: 0,\n        }),\n      ).toEqual({ isReached: true });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/avoid-logical-databases.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\n\nexport class AvoidLogicalDatabasesStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check avoid use logical databases recommendation\n   * @param databases\n   */\n\n  async isRecommendationReached({\n    prevDb,\n    db,\n  }: {\n    prevDb: number;\n    db: number;\n  }): Promise<IDatabaseRecommendationStrategyData> {\n    return { isReached: prevDb !== db };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/avoid-lua-scripts.strategy.spec.ts",
    "content": "import { AvoidLuaScriptsStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('AvoidLuaScriptsStrategy', () => {\n  let strategy: AvoidLuaScriptsStrategy;\n\n  beforeEach(() => {\n    strategy = new AvoidLuaScriptsStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when number_of_cached_scripts less then 10', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          version: '1',\n          cashedScripts: 1,\n        }),\n      ).toEqual({ isReached: false });\n    });\n\n    it('should return true when number_of_cached_scripts more then 10', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          version: '1',\n          cashedScripts: 11,\n        }),\n      ).toEqual({ isReached: true });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/avoid-lua-scripts.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { LUA_SCRIPT_RECOMMENDATION_COUNT } from 'src/common/constants';\n\nexport class AvoidLuaScriptsStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check lua script recommendation\n   * @param info\n   */\n\n  async isRecommendationReached(\n    info: RedisDatabaseInfoResponse,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    return { isReached: info.cashedScripts > LUA_SCRIPT_RECOMMENDATION_COUNT };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/big-amount-connected-clients.strategy.spec.ts",
    "content": "import { BigAmountConnectedClientsStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('BigAmountConnectedClientsStrategy', () => {\n  let strategy: BigAmountConnectedClientsStrategy;\n\n  beforeEach(() => {\n    strategy = new BigAmountConnectedClientsStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when connectedClients less then 100', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          version: '1',\n          connectedClients: 1,\n        }),\n      ).toEqual({ isReached: false });\n    });\n\n    it('should return true when connectedClients more then 100', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          version: '1',\n          connectedClients: 101,\n        }),\n      ).toEqual({ isReached: true });\n    });\n\n    describe('cluster', () => {\n      it('should return true when connectedClients more then 100 in one of the nodes', async () => {\n        expect(\n          await strategy.isRecommendationReached({\n            version: '1',\n            nodes: [\n              { version: '1', connectedClients: 1 },\n              { version: '2', connectedClients: 101 },\n            ],\n          }),\n        ).toEqual({ isReached: true });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/big-amount-connected-clients.strategy.ts",
    "content": "import { maxBy } from 'lodash';\nimport { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { BIG_AMOUNT_OF_CONNECTED_CLIENTS_RECOMMENDATION_CLIENTS } from 'src/common/constants';\n\nexport class BigAmountConnectedClientsStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check big amount of connected clients recommendation\n   * @param info\n   */\n\n  async isRecommendationReached(\n    info: RedisDatabaseInfoResponse,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    const nodeInfo = info.nodes?.length\n      ? maxBy(info.nodes, 'connectedClients')\n      : info;\n    return {\n      isReached:\n        nodeInfo?.connectedClients >\n        BIG_AMOUNT_OF_CONNECTED_CLIENTS_RECOMMENDATION_CLIENTS,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/big-set.strategy.spec.ts",
    "content": "import { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { BigSetStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockSetInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string1'),\n  type: 'set',\n  ttl: -1,\n  size: 1,\n  length: 1_000,\n};\n\nconst mockBigSetInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string2'),\n  type: 'set',\n  ttl: -1,\n  size: 1,\n  length: 1_001,\n};\n\nconst mockHashInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string3'),\n  type: 'hash',\n  ttl: -1,\n  size: 1,\n  length: 100_001,\n};\n\ndescribe('BigSetStrategy', () => {\n  let strategy: BigSetStrategy;\n\n  beforeEach(async () => {\n    strategy = new BigSetStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when set length < 1 000', async () => {\n      expect(await strategy.isRecommendationReached(mockSetInfo)).toEqual({\n        isReached: false,\n      });\n    });\n\n    it('should return false when not set key', async () => {\n      expect(await strategy.isRecommendationReached(mockHashInfo)).toEqual({\n        isReached: false,\n      });\n    });\n\n    it('should return true when set length > 1 000', async () => {\n      expect(await strategy.isRecommendationReached(mockBigSetInfo)).toEqual({\n        isReached: true,\n        params: { keys: [mockBigSetInfo.name] },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/big-set.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport {\n  RedisDataType,\n  GetKeyInfoResponse,\n} from 'src/modules/browser/keys/dto';\nimport { BIG_SETS_RECOMMENDATION_LENGTH } from 'src/common/constants';\n\nexport class BigSetStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check big set recommendation\n   * @param key\n   */\n\n  async isRecommendationReached(\n    key: GetKeyInfoResponse,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    return key?.type === RedisDataType.Set &&\n      key?.length > BIG_SETS_RECOMMENDATION_LENGTH\n      ? { isReached: true, params: { keys: [key?.name] } }\n      : { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/big-string.strategy.spec.ts",
    "content": "import { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { BigStringStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockStringInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string1'),\n  type: 'string',\n  ttl: -1,\n  size: 1,\n  length: 100_000,\n};\n\nconst mockBigStringInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string2'),\n  type: 'string',\n  ttl: -1,\n  size: 5_100_000,\n  length: 100_001,\n};\n\nconst mockHashInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string3'),\n  type: 'hash',\n  ttl: -1,\n  size: 1,\n  length: 100_001,\n};\n\ndescribe('BigStringStrategy', () => {\n  let strategy: BigStringStrategy;\n\n  beforeEach(async () => {\n    strategy = new BigStringStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when string size < 5 000 000', async () => {\n      expect(await strategy.isRecommendationReached(mockStringInfo)).toEqual({\n        isReached: false,\n      });\n    });\n\n    it('should return true when string size > 5 000 000', async () => {\n      expect(await strategy.isRecommendationReached(mockBigStringInfo)).toEqual(\n        { isReached: true, params: { keys: [mockBigStringInfo.name] } },\n      );\n    });\n\n    it('should return false when not string key', async () => {\n      expect(await strategy.isRecommendationReached(mockHashInfo)).toEqual({\n        isReached: false,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/big-string.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport {\n  RedisDataType,\n  GetKeyInfoResponse,\n} from 'src/modules/browser/keys/dto';\nimport { BIG_STRINGS_RECOMMENDATION_MEMORY } from 'src/common/constants';\n\nexport class BigStringStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check big strings recommendation\n   * @param key\n   */\n\n  async isRecommendationReached(\n    key: GetKeyInfoResponse,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    const isBigString =\n      key.type === RedisDataType.String &&\n      key.size > BIG_STRINGS_RECOMMENDATION_MEMORY;\n\n    return isBigString\n      ? { isReached: true, params: { keys: [key?.name] } }\n      : { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/compression-for-list.strategy.spec.ts",
    "content": "import { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { CompressionForListStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockListInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string1'),\n  type: 'list',\n  ttl: -1,\n  size: 1,\n  length: 100,\n};\n\nconst mockBigListInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string2'),\n  type: 'list',\n  ttl: -1,\n  size: 5_100_000,\n  length: 1_001,\n};\n\nconst mockHashInfo: GetKeyInfoResponse = {\n  name: Buffer.from('string3'),\n  type: 'hash',\n  ttl: -1,\n  size: 1,\n  length: 100_001,\n};\n\ndescribe('CompressionForListStrategy', () => {\n  let strategy: CompressionForListStrategy;\n\n  beforeEach(async () => {\n    strategy = new CompressionForListStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when list length < 1 000', async () => {\n      expect(await strategy.isRecommendationReached(mockListInfo)).toEqual({\n        isReached: false,\n      });\n    });\n\n    it('should return true when list length > 1 000', async () => {\n      expect(await strategy.isRecommendationReached(mockBigListInfo)).toEqual({\n        isReached: true,\n        params: { keys: [mockBigListInfo.name] },\n      });\n    });\n\n    it('should return false when not list key', async () => {\n      expect(await strategy.isRecommendationReached(mockHashInfo)).toEqual({\n        isReached: false,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/compression-for-list.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport {\n  RedisDataType,\n  GetKeyInfoResponse,\n} from 'src/modules/browser/keys/dto';\nimport { COMPRESSION_FOR_LIST_RECOMMENDATION_LENGTH } from 'src/common/constants';\n\nexport class CompressionForListStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check compression for list recommendation\n   * @param key\n   */\n\n  async isRecommendationReached(\n    key: GetKeyInfoResponse,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    const isBigList =\n      key.type === RedisDataType.List &&\n      key.length > COMPRESSION_FOR_LIST_RECOMMENDATION_LENGTH;\n\n    return isBigList\n      ? { isReached: true, params: { keys: [key?.name] } }\n      : { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/default.recommendation.strategy.spec.ts",
    "content": "import { DefaultRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('DefaultRecommendationStrategy', () => {\n  const strategy = new DefaultRecommendationStrategy();\n\n  describe('isRecommendationReached', () => {\n    it('should get is recommendation reached', async () => {\n      expect(await strategy.isRecommendationReached()).toEqual({\n        isReached: false,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/default.recommendation.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\n\nexport class DefaultRecommendationStrategy extends AbstractRecommendationStrategy {\n  async isRecommendationReached(): Promise<IDatabaseRecommendationStrategyData> {\n    return { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/index.ts",
    "content": "export * from './abstract.recommendation.strategy';\nexport * from './default.recommendation.strategy';\nexport * from './redis-version.strategy';\nexport * from './search-JSON.strategy';\nexport * from './big-set.strategy';\nexport * from './rts.strategy';\nexport * from './avoid-logical-databases.strategy';\nexport * from './shard-hash.strategy';\nexport * from './string-to-json.strategy';\nexport * from './search-visualization.strategy';\nexport * from './use-smaller-keys.strategy';\nexport * from './avoid-lua-scripts.strategy';\nexport * from './big-string.strategy';\nexport * from './compression-for-list.strategy';\nexport * from './big-amount-connected-clients.strategy';\nexport * from './try-rdi.strategy';\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/redis-version.strategy.spec.ts",
    "content": "import { RedisVersionStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('RedisVersionStrategy', () => {\n  let strategy: RedisVersionStrategy;\n\n  beforeEach(() => {\n    strategy = new RedisVersionStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    describe('with search module', () => {\n      it('should return false when version is more then 7.3', async () => {\n        expect(\n          await strategy.isRecommendationReached({ version: '7.4.0' }),\n        ).toEqual({ isReached: false });\n      });\n\n      it('should return false when version is equal to 7.3', async () => {\n        expect(\n          await strategy.isRecommendationReached({ version: '7.3' }),\n        ).toEqual({ isReached: false });\n      });\n\n      it('should return true when version less then 7.3', async () => {\n        expect(\n          await strategy.isRecommendationReached({ version: '6.0.0' }),\n        ).toEqual({ isReached: true });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/redis-version.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport * as semverCompare from 'node-version-compare';\nimport { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { REDIS_VERSION_RECOMMENDATION_VERSION } from 'src/common/constants';\n\nexport class RedisVersionStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check redis version recommendation\n   * @param info\n   */\n  async isRecommendationReached(\n    info: RedisDatabaseInfoResponse,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    return {\n      isReached:\n        semverCompare(info.version, REDIS_VERSION_RECOMMENDATION_VERSION) < 0,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/rts.strategy.spec.ts",
    "content": "import { RTSStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockTimestampName = Buffer.from('1234567891');\nconst mockDefaultName = Buffer.from('name');\n\nconst mockTimestampScore = 1234567891;\nconst mockDefaultScore = 1;\n\nconst mockDefaultMembers = {\n  name: mockDefaultName,\n  score: mockDefaultScore,\n};\n\nconst mockTimeStampInMemberName = {\n  name: mockTimestampName,\n  score: mockDefaultScore,\n};\n\nconst mockTimeStampInScore = {\n  name: mockDefaultName,\n  score: mockTimestampScore,\n};\n\nconst mockKeyName = 'name';\n\ndescribe('RTSStrategy', () => {\n  let strategy: RTSStrategy;\n\n  beforeEach(async () => {\n    strategy = new RTSStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when no timestamp in member', async () => {\n      const mockData = { members: [mockDefaultMembers], keyName: mockKeyName };\n      expect(await strategy.isRecommendationReached(mockData)).toEqual({\n        isReached: false,\n      });\n    });\n\n    it('should return true when members has timestamp in memberName', async () => {\n      const mockData = {\n        members: [mockDefaultMembers, mockTimeStampInMemberName],\n        keyName: mockKeyName,\n      };\n      expect(await strategy.isRecommendationReached(mockData)).toEqual({\n        isReached: true,\n        params: { keys: [mockKeyName] },\n      });\n    });\n\n    it('should return true when members has timestamp in score', async () => {\n      const mockData = {\n        members: [mockDefaultMembers, mockTimeStampInScore],\n        keyName: mockKeyName,\n      };\n      expect(await strategy.isRecommendationReached(mockData)).toEqual({\n        isReached: true,\n        params: { keys: [mockKeyName] },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/rts.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { getUTF8FromBuffer } from 'src/utils/cli-helper';\nimport { checkTimestamp } from 'src/utils';\n\nexport class RTSStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check rts recommendation\n   * @param data\n   */\n\n  async isRecommendationReached(\n    data,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    const timestampMemberNames = data?.members.some(({ name }) =>\n      checkTimestamp(getUTF8FromBuffer(name as Buffer)),\n    );\n    const timestampMemberScores = data?.members.some(({ score }) =>\n      checkTimestamp(String(score)),\n    );\n\n    return timestampMemberNames || timestampMemberScores\n      ? { isReached: true, params: { keys: [data?.keyName] } }\n      : { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/search-JSON.strategy.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport { GetKeyInfoResponse } from 'src/modules/browser/keys/dto';\nimport { SearchJSONStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockDatabaseId = 'id';\n\nconst mockJSONInfo: GetKeyInfoResponse = {\n  name: Buffer.from('testString_1'),\n  type: 'ReJSON-RL',\n  ttl: -1,\n  size: 1,\n};\n\nconst mockHashInfo: GetKeyInfoResponse = {\n  name: Buffer.from('testString_2'),\n  type: 'hash',\n  ttl: -1,\n  size: 512 * 1024 + 1,\n};\n\nconst mockEmptyIndexes = [];\nconst mockIndexes = ['foo'];\n\ndescribe('SearchJSONStrategy', () => {\n  const client = mockStandaloneRedisClient;\n  let strategy: SearchJSONStrategy;\n\n  beforeEach(async () => {\n    strategy = new SearchJSONStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    describe('with search module', () => {\n      it('should return true when there is JSON key', async () => {\n        when(client.sendCommand)\n          .calledWith(expect.arrayContaining(['FT._LIST']), expect.anything())\n          .mockResolvedValue(mockEmptyIndexes);\n\n        expect(\n          await strategy.isRecommendationReached({\n            client,\n            databaseId: mockDatabaseId,\n            keys: [mockJSONInfo, mockHashInfo],\n          }),\n        ).toEqual({ isReached: true, params: { keys: [mockJSONInfo.name] } });\n      });\n\n      it('should return false when there is not JSON key', async () => {\n        expect(\n          await strategy.isRecommendationReached({\n            client,\n            databaseId: mockDatabaseId,\n            keys: [mockHashInfo],\n          }),\n        ).toEqual({ isReached: false });\n      });\n\n      it('should return false when FT._LIST return indexes', async () => {\n        when(client.sendCommand)\n          .calledWith(expect.arrayContaining(['FT._LIST']), expect.anything())\n          .mockResolvedValue(mockIndexes);\n\n        expect(\n          await strategy.isRecommendationReached({\n            client,\n            databaseId: mockDatabaseId,\n            keys: [mockJSONInfo, mockHashInfo],\n          }),\n        ).toEqual({ isReached: false });\n      });\n    });\n\n    describe('without search module', () => {\n      beforeEach(() => {\n        when(client.sendCommand)\n          .calledWith(expect.arrayContaining(['FT._LIST']), expect.anything())\n          .mockReturnValue(new Error('Unsupported command'));\n      });\n\n      it('should return true when there is JSON key', async () => {\n        expect(\n          await strategy.isRecommendationReached({\n            client,\n            databaseId: mockDatabaseId,\n            keys: [mockJSONInfo, mockHashInfo],\n          }),\n        ).toEqual({ isReached: true, params: { keys: [mockJSONInfo.name] } });\n      });\n\n      it('should return false when there is not JSON key', async () => {\n        expect(\n          await strategy.isRecommendationReached({\n            client,\n            databaseId: mockDatabaseId,\n            keys: [mockHashInfo],\n          }),\n        ).toEqual({ isReached: false });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/search-JSON.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport {\n  RedisDataType,\n  GetKeyInfoResponse,\n} from 'src/modules/browser/keys/dto';\nimport { SearchJSON } from 'src/modules/database-recommendation/models';\n\nexport class SearchJSONStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check redis JSON recommendation\n   * @param data\n   */\n\n  async isRecommendationReached(\n    data: SearchJSON,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    const jsonKey = data.keys.find(\n      (key: GetKeyInfoResponse) => key.type === RedisDataType.JSON,\n    );\n\n    if (jsonKey) {\n      // todo:improve decision mechanism when store Recommendations to avoid infinite checks when isReached:false\n      try {\n        const indexes = (await data.client.sendCommand(['FT._LIST'], {\n          replyEncoding: 'utf8',\n        })) as string[];\n\n        if (indexes.length) {\n          return { isReached: false };\n        }\n      } catch (e) {\n        // ignore error\n      }\n\n      return { isReached: true, params: { keys: [jsonKey.name] } };\n    }\n\n    return { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/search-visualization.strategy.spec.ts",
    "content": "import { SearchVisualizationStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst isRecommendationReachedTests: any[] = [\n  ['test', { isReached: false }],\n  ['info', { isReached: false }],\n  ['123', { isReached: false }],\n  ['aoeutaoheu', { isReached: false }],\n  ['ft.search', { isReached: true }],\n  ['FT.Search', { isReached: true }],\n  ['FT.INFO test 123', { isReached: true }],\n  ['FT.PROFILE 123', { isReached: true }],\n];\n\ndescribe('SearchVisualizationStrategy', () => {\n  let strategy: SearchVisualizationStrategy;\n\n  beforeEach(async () => {\n    strategy = new SearchVisualizationStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it.each(isRecommendationReachedTests)(\n      'for input: %s (command), should be output: %s',\n      async (command, expected) => {\n        const result = await strategy.isRecommendationReached(command);\n        expect(result).toEqual(expected);\n      },\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/search-visualization.strategy.ts",
    "content": "import { SearchVisualizationCommands } from 'src/common/constants';\nimport { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\n\nexport class SearchVisualizationStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check user runs one of the specified search commands via CLI\n   * @param commandInit\n   */\n\n  async isRecommendationReached(\n    commandInit: string = '',\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    const [command] = commandInit.split(' ');\n    return {\n      isReached: Object.values(SearchVisualizationCommands).includes(\n        command.toUpperCase() as SearchVisualizationCommands,\n      ),\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/shard-hash.strategy.spec.ts",
    "content": "import { ShardHashStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockKeyName = 'name';\n\ndescribe('ShardHashStrategy', () => {\n  let strategy: ShardHashStrategy;\n\n  beforeEach(async () => {\n    strategy = new ShardHashStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when hash length < 5_000', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          total: 5_000,\n          keyName: mockKeyName,\n        }),\n      ).toEqual({ isReached: false });\n    });\n\n    it('should return true when hash length > 5_000', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          total: 5_001,\n          keyName: mockKeyName,\n        }),\n      ).toEqual({ isReached: true, params: { keys: [mockKeyName] } });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/shard-hash.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { BIG_HASHES_RECOMMENDATION_LENGTH } from 'src/common/constants';\n\nexport class ShardHashStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check shard big hashes to small hashes recommendation\n   * @param data\n   */\n\n  async isRecommendationReached(\n    data,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    return data.total > BIG_HASHES_RECOMMENDATION_LENGTH\n      ? { isReached: true, params: { keys: [data.keyName] } }\n      : { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/string-to-json.strategy.spec.ts",
    "content": "import { StringToJsonStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\nconst mockKeyName = 'name';\n\ndescribe('StringToJsonStrategy', () => {\n  let strategy: StringToJsonStrategy;\n\n  beforeEach(async () => {\n    strategy = new StringToJsonStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when string value not object or array', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          value: Buffer.from('value'),\n          keyName: mockKeyName,\n        }),\n      ).toEqual({ isReached: false });\n    });\n\n    it('should return true when string value is object or array', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          value: Buffer.from('[1,2]'),\n          keyName: mockKeyName,\n        }),\n      ).toEqual({ isReached: true, params: { keys: [mockKeyName] } });\n      expect(\n        await strategy.isRecommendationReached({\n          value: Buffer.from('{\"foo\": \"bar\"}'),\n          keyName: mockKeyName,\n        }),\n      ).toEqual({ isReached: true, params: { keys: [mockKeyName] } });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/string-to-json.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { isJson } from 'src/utils/base.helper';\nimport { getUTF8FromBuffer } from 'src/utils/cli-helper';\n\nexport class StringToJsonStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check JSON is used for Strings in Browser recommendation\n   * @param data\n   */\n\n  async isRecommendationReached(\n    data: any,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    return isJson(getUTF8FromBuffer(data?.value))\n      ? { isReached: true, params: { keys: [data.keyName] } }\n      : { isReached: false };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/try-rdi.strategy.spec.ts",
    "content": "import { TryRdiStrategyStrategy } from 'src/modules/database-recommendation/scanner/strategies';\nimport { RedisClientConnectionType } from 'src/modules/redis/client';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\n\nconst mockClusterConnectionType = RedisClientConnectionType.CLUSTER;\nconst mockNotClusterConnectionType = RedisClientConnectionType.STANDALONE;\n\nconst mockREProvider = HostingProvider.REDIS_SOFTWARE;\nconst mockNotREProvider = HostingProvider.REDIS_CLOUD;\n\ndescribe('TryRdiStrategyStrategy', () => {\n  let strategy: TryRdiStrategyStrategy;\n\n  beforeEach(async () => {\n    strategy = new TryRdiStrategyStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return true when connectionType is cluster', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          connectionType: mockClusterConnectionType,\n          provider: mockNotREProvider,\n        }),\n      ).toEqual({ isReached: true });\n    });\n\n    it('should return true when provider is Redis Enterprise', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          connectionType: mockNotClusterConnectionType,\n          provider: mockREProvider,\n        }),\n      ).toEqual({ isReached: true });\n    });\n\n    it('should return true when provider is Redis Enterprise and connectionType is cluster', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          connectionType: mockClusterConnectionType,\n          provider: mockREProvider,\n        }),\n      ).toEqual({ isReached: true });\n    });\n\n    it('should return false when provider is not Redis Enterprise and connectionType is not cluster', async () => {\n      expect(\n        await strategy.isRecommendationReached({\n          connectionType: mockNotClusterConnectionType,\n          provider: mockNotREProvider,\n        }),\n      ).toEqual({ isReached: false });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/try-rdi.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { RedisClientConnectionType } from 'src/modules/redis/client';\n\nexport class TryRdiStrategyStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check try rdi recommendation\n   * @param data\n   */\n\n  async isRecommendationReached(data: {\n    provider: HostingProvider;\n    connectionType: RedisClientConnectionType;\n  }): Promise<IDatabaseRecommendationStrategyData> {\n    const isRedisSoftwareOrCluster =\n      data.provider === HostingProvider.REDIS_SOFTWARE ||\n      data.connectionType === RedisClientConnectionType.CLUSTER;\n\n    return { isReached: isRedisSoftwareOrCluster };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/use-smaller-keys.strategy.spec.ts",
    "content": "import { UseSmallerKeysStrategy } from 'src/modules/database-recommendation/scanner/strategies';\n\ndescribe('UseSmallerKeysStrategy', () => {\n  let strategy: UseSmallerKeysStrategy;\n\n  beforeEach(async () => {\n    strategy = new UseSmallerKeysStrategy();\n  });\n\n  describe('isRecommendationReached', () => {\n    it('should return false when database total less than 1_000_000', async () => {\n      expect(await strategy.isRecommendationReached(1)).toEqual({\n        isReached: false,\n      });\n    });\n\n    it('should return false when database total more than 1_000_000', async () => {\n      expect(await strategy.isRecommendationReached(1_000_001)).toEqual({\n        isReached: true,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-recommendation/scanner/strategies/use-smaller-keys.strategy.ts",
    "content": "import { AbstractRecommendationStrategy } from 'src/modules/database-recommendation/scanner/strategies/abstract.recommendation.strategy';\nimport { IDatabaseRecommendationStrategyData } from 'src/modules/database-recommendation/scanner/recommendation.strategy.interface';\nimport { USE_SMALLER_KEYS_RECOMMENDATION_TOTAL } from 'src/common/constants';\n\nexport class UseSmallerKeysStrategy extends AbstractRecommendationStrategy {\n  /**\n   * Check use smaller keys recommendation\n   * @param total\n   */\n\n  async isRecommendationReached(\n    total: number,\n  ): Promise<IDatabaseRecommendationStrategyData> {\n    return { isReached: total > USE_SMALLER_KEYS_RECOMMENDATION_TOTAL };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/database-settings.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiBody, ApiTags } from '@nestjs/swagger';\nimport { BrowserSerializeInterceptor } from 'src/common/interceptors';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { SessionMetadata } from 'src/common/models';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { CreateOrUpdateDatabaseSettingDto } from 'src/modules/database-settings/dto/database-setting.dto';\nimport { DatabaseSettingsService } from 'src/modules/database-settings/database-settings.service';\nimport { DatabaseSettings } from 'src/modules/database-settings/models/database-settings';\n\n@UseInterceptors(BrowserSerializeInterceptor)\n@UsePipes(new ValidationPipe({ transform: true }))\n@ApiTags('Database: Database settings')\n@Controller('/')\nexport class DatabaseSettingsController {\n  constructor(private readonly service: DatabaseSettingsService) {}\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get database settings',\n    responses: [\n      {\n        status: 200,\n        type: DatabaseSettings,\n      },\n    ],\n  })\n  @Get('')\n  async get(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('dbInstance') databaseId: string,\n  ): Promise<DatabaseSettings> {\n    return this.service.get(sessionMetadata, databaseId);\n  }\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Update database settings',\n    responses: [\n      {\n        status: 200,\n        type: DatabaseSettings,\n      },\n    ],\n  })\n  @Post('')\n  @ApiBody({ type: CreateOrUpdateDatabaseSettingDto })\n  async create(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('dbInstance') databaseId: string,\n    @Body() dto: CreateOrUpdateDatabaseSettingDto,\n  ): Promise<DatabaseSettings> {\n    return this.service.createOrUpdate(sessionMetadata, databaseId, dto);\n  }\n\n  @Delete('')\n  @ApiRedisParams()\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Delete database settings',\n  })\n  async delete(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Param('dbInstance') databaseId: string,\n  ): Promise<void> {\n    await this.service.delete(sessionMetadata, databaseId);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/database-settings.module.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport { DynamicModule, Module, Type } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { DatabaseSettingsController } from './database-settings.controller';\nimport { DatabaseSettingsService } from './database-settings.service';\nimport { DatabaseSettingsRepository } from './repositories/database-settings.repository';\nimport { LocalDatabaseSettingsRepository } from './repositories/local-database-settings.repository';\n\nconst route = '/databases/:dbInstance/settings';\n\n@Module({})\nexport class DatabaseSettingsModule {\n  static register(\n    databaseSettingsRepository: Type<DatabaseSettingsRepository> = LocalDatabaseSettingsRepository,\n  ): DynamicModule {\n    return {\n      module: DatabaseSettingsModule,\n      imports: [\n        RouterModule.register([\n          {\n            path: route,\n            module: DatabaseSettingsModule,\n          },\n        ]),\n      ],\n      controllers: [DatabaseSettingsController],\n      providers: [\n        DatabaseSettingsService,\n        {\n          provide: DatabaseSettingsRepository,\n          useClass: databaseSettingsRepository,\n        },\n      ],\n      exports: [DatabaseSettingsService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/database-settings.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockDatabaseId,\n  mockDatabaseSettingsCreateDto,\n  mockDatabaseSettingsDto,\n  mockDatabaseSettingsEntity,\n  mockDatabaseSettingsRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport {\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { DatabaseSettingsService } from './database-settings.service';\nimport { DatabaseSettingsRepository } from './repositories/database-settings.repository';\n\ndescribe('DatabaseSettingsService', () => {\n  let service: DatabaseSettingsService;\n  let repository: MockType<DatabaseSettingsRepository>;\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DatabaseSettingsService,\n        {\n          provide: DatabaseSettingsRepository,\n          useFactory: mockDatabaseSettingsRepository,\n        },\n      ],\n    }).compile();\n    service = await module.get(DatabaseSettingsService);\n    repository = await module.get(DatabaseSettingsRepository);\n  });\n\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  describe('get', () => {\n    it('should return database settings entity when it exists', async () => {\n      const expected = mockDatabaseSettingsDto();\n      repository.get.mockResolvedValue(expected);\n      const actual = await service.get(mockSessionMetadata, mockDatabaseId);\n      expect(actual).toEqual(expected);\n      expect(repository.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n      );\n    });\n\n    it('should throw NotFoundException when database setting is not found', async () => {\n      repository.get.mockRejectedValueOnce(\n        new NotFoundException(ERROR_MESSAGES.DATABASE_SETTINGS_NOT_FOUND),\n      );\n      await expect(\n        service.get(mockSessionMetadata, mockDatabaseId),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('createOrUpdate', () => {\n    it('should create new database settings entity when it does not exist', async () => {\n      const expected = mockDatabaseSettingsDto();\n      repository.createOrUpdate.mockResolvedValueOnce(expected);\n      const actual = await service.createOrUpdate(\n        mockSessionMetadata,\n        mockDatabaseId,\n        mockDatabaseSettingsCreateDto,\n      );\n      expect(actual).toEqual(expected);\n      expect(repository.createOrUpdate).toHaveBeenCalled();\n    });\n\n    it('should update existing database settings entity', async () => {\n      repository.createOrUpdate.mockResolvedValue({\n        ...mockDatabaseSettingsEntity,\n        data: { treeViewSort: '1' },\n      });\n      const updateDto = {\n        ...mockDatabaseSettingsCreateDto,\n        data: { treeViewSort: '1' },\n      };\n      const result = await service.createOrUpdate(\n        mockSessionMetadata,\n        mockDatabaseId,\n        updateDto,\n      );\n      expect(result.data).toEqual({ treeViewSort: '1' });\n      expect(repository.createOrUpdate).toHaveBeenCalled();\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete database settings entity', async () => {\n      await expect(\n        service.delete(mockSessionMetadata, mockDatabaseId),\n      ).resolves.not.toThrow();\n      expect(repository.delete).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n      );\n    });\n\n    it('should throw when database setting is not found', async () => {\n      repository.delete.mockImplementationOnce(() => {\n        throw new InternalServerErrorException('Error');\n      });\n      await expect(\n        service.delete(mockSessionMetadata, mockDatabaseId),\n      ).rejects.toThrow(InternalServerErrorException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/database-settings.service.ts",
    "content": "import { HttpException, Injectable, Logger } from '@nestjs/common';\nimport { catchAclError } from 'src/utils';\nimport { plainToInstance } from 'class-transformer';\nimport { SessionMetadata } from 'src/common/models';\nimport { DatabaseSettingsRepository } from './repositories/database-settings.repository';\nimport { CreateOrUpdateDatabaseSettingDto } from './dto/database-setting.dto';\nimport { DatabaseSettings } from './models/database-settings';\n\n@Injectable()\nexport class DatabaseSettingsService {\n  private logger = new Logger('DatabaseSettingsService');\n\n  constructor(\n    private readonly databaseSettingsRepository: DatabaseSettingsRepository,\n  ) {}\n\n  /**\n   * Create or update a setting\n   *\n   * @param sessionMetadata\n   * @param databaseId\n   * @param dto\n   */\n  public async createOrUpdate(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    dto: CreateOrUpdateDatabaseSettingDto,\n  ): Promise<DatabaseSettings> {\n    try {\n      const setting = plainToInstance(DatabaseSettings, { ...dto, databaseId });\n      return this.databaseSettingsRepository.createOrUpdate(\n        sessionMetadata,\n        setting,\n      );\n    } catch (e) {\n      this.logger.error(\n        'Unable to create database setting',\n        e,\n        sessionMetadata,\n      );\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Get database settings\n   *\n   * @param sessionMetadata\n   * @param id\n   */\n  async get(\n    sessionMetadata: SessionMetadata,\n    id: string,\n  ): Promise<DatabaseSettings> {\n    return this.databaseSettingsRepository.get(sessionMetadata, id);\n  }\n\n  /**\n   * Delete database settings\n   * @param sessionMetadata\n   * @param databaseId\n   */\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<void> {\n    return this.databaseSettingsRepository.delete(sessionMetadata, databaseId);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/dto/database-setting.dto.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class CreateOrUpdateDatabaseSettingDto {\n  @ApiProperty({\n    description: 'Applied settings by user, by database',\n  })\n  @Expose()\n  data: Record<string, number | string | boolean>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/entities/database-setting.entity.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport {\n  Column,\n  Entity,\n  Index,\n  JoinColumn,\n  OneToOne,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\nimport { DataAsJsonString } from 'src/common/decorators';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Entity('database_settings')\nexport class DatabaseSettingsEntity {\n  @PrimaryGeneratedColumn()\n  @Expose()\n  id: number;\n\n  @Column({ nullable: false })\n  @Index({ unique: true })\n  @Expose()\n  databaseId: string;\n\n  @OneToOne(() => DatabaseEntity, (database) => database.dbSettings, {\n    nullable: true,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn()\n  database: DatabaseEntity;\n\n  @ApiProperty({\n    description: 'Applied settings by user, by database',\n  })\n  @Column({ nullable: true })\n  @DataAsJsonString()\n  @Expose()\n  data: string;\n\n  constructor(entity: Partial<DatabaseSettingsEntity>) {\n    Object.assign(this, entity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/models/database-settings.ts",
    "content": "import { Expose, Type } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\ntype TreeViewDelimiterType = {\n  label: string;\n};\n\nexport class DatabaseSettingsData {\n  @Expose()\n  showHiddenRecommendations?: boolean;\n\n  @Expose()\n  notShowConfirmationRunTutorial?: boolean;\n\n  @Expose()\n  slowLogDurationUnit?: number;\n\n  @Expose()\n  treeViewDelimiter?: TreeViewDelimiterType | TreeViewDelimiterType[];\n\n  @Expose()\n  treeViewSort?: string;\n}\n\nexport class DatabaseSettings {\n  @ApiProperty({\n    description: 'Database id',\n    type: String,\n    default: '123',\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiProperty({\n    description: 'Applied settings by user, by database',\n  })\n  @Expose()\n  @Type(() => DatabaseSettingsData)\n  data: DatabaseSettingsData;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/repositories/database-settings.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { DatabaseSettings } from '../models/database-settings';\n\nexport abstract class DatabaseSettingsRepository {\n  abstract createOrUpdate(\n    sessionMetadata: SessionMetadata,\n    setting: Partial<DatabaseSettings>,\n  ): Promise<DatabaseSettings>;\n\n  abstract get(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<DatabaseSettings>;\n\n  abstract delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/repositories/local-database-settings.repository.spec.ts",
    "content": "import {\n  mockDatabaseId,\n  mockDatabaseSettingsDto,\n  mockDatabaseSettingsEntity,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { Repository } from 'typeorm';\nimport { DatabaseSettingsEntity } from 'src/modules/database-settings/entities/database-setting.entity';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport {\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { LocalDatabaseSettingsRepository } from './local-database-settings.repository';\n\ndescribe('LocalDatabaseSettingsRepository', () => {\n  let localDbRepository: LocalDatabaseSettingsRepository;\n  let repository: MockType<Repository<DatabaseSettingsEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalDatabaseSettingsRepository,\n        {\n          provide: getRepositoryToken(DatabaseSettingsEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(DatabaseSettingsEntity));\n    localDbRepository = module.get(LocalDatabaseSettingsRepository);\n\n    repository.findOneBy.mockResolvedValue(mockDatabaseSettingsEntity);\n    repository.save.mockResolvedValue(mockDatabaseSettingsEntity);\n    repository.delete.mockImplementation(() => {});\n  });\n\n  it('should be defined', () => {\n    expect(localDbRepository).toBeDefined();\n  });\n\n  describe('get', () => {\n    it('should return database settings entity', async () => {\n      const actual = await localDbRepository.get(\n        mockSessionMetadata,\n        mockDatabaseId,\n      );\n      const expected = mockDatabaseSettingsDto();\n      expect(actual).toEqual(expected);\n    });\n\n    it('should throw when database setting is not found', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      await expect(\n        localDbRepository.get(mockSessionMetadata, mockDatabaseId),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n  describe('createOrUpdate', () => {\n    it('should be able to create database settings entity', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      const actual = await localDbRepository.createOrUpdate(\n        mockSessionMetadata,\n        mockDatabaseSettingsDto(),\n      );\n      const expected = mockDatabaseSettingsDto();\n      expect(actual).toEqual(expected);\n    });\n\n    it('should be able to update database setting', async () => {\n      const update = mockDatabaseSettingsDto();\n      update.data = { treeViewSort: '1' };\n      expect(\n        await localDbRepository.createOrUpdate(mockSessionMetadata, update),\n      ).toEqual(mockDatabaseSettingsDto());\n    });\n  });\n  describe('delete', () => {\n    it('should delete database settings entity', async () => {\n      await expect(\n        localDbRepository.delete(mockSessionMetadata, mockDatabaseId),\n      ).resolves.not.toThrow();\n    });\n\n    it('should throw when database setting is not found', async () => {\n      repository.delete.mockImplementationOnce(() => {\n        throw new Error('Error');\n      });\n      await expect(\n        localDbRepository.delete(mockSessionMetadata, mockDatabaseId),\n      ).rejects.toThrow(InternalServerErrorException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/database-settings/repositories/local-database-settings.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { DatabaseSettingsEntity } from 'src/modules/database-settings/entities/database-setting.entity';\nimport { classToClass } from 'src/utils';\nimport {\n  InternalServerErrorException,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { plainToInstance } from 'class-transformer';\nimport { DatabaseSettingsRepository } from './database-settings.repository';\nimport { DatabaseSettings } from '../models/database-settings';\n\nexport class LocalDatabaseSettingsRepository extends DatabaseSettingsRepository {\n  private readonly logger = new Logger('LocalDatabaseSettingsRepository');\n\n  constructor(\n    @InjectRepository(DatabaseSettingsEntity)\n    private readonly repository: Repository<DatabaseSettingsEntity>,\n  ) {\n    super();\n  }\n\n  async createOrUpdate(\n    _sessionMetadata: SessionMetadata,\n    setting: Partial<DatabaseSettings>,\n  ): Promise<DatabaseSettings> {\n    const settingsEntity = plainToInstance(DatabaseSettingsEntity, setting);\n    const existing = await this.repository.findOneBy({\n      databaseId: setting.databaseId,\n    });\n\n    if (existing) {\n      // update\n      settingsEntity.id = existing.id;\n    }\n    const entity = await this.repository.save(settingsEntity);\n    return classToClass(DatabaseSettings, entity);\n  }\n\n  async get(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<DatabaseSettings> {\n    const entity = await this.repository.findOneBy({ databaseId });\n\n    if (!entity) {\n      this.logger.error(\n        `Database settings item with id:${databaseId} was not Found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(ERROR_MESSAGES.DATABASE_SETTINGS_NOT_FOUND);\n    }\n\n    return classToClass(DatabaseSettings, entity);\n  }\n\n  /**\n   * Delete settings per database\n   * @param sessionMetadata\n   * @param databaseId\n   */\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): Promise<void> {\n    try {\n      await this.repository.delete({ databaseId });\n    } catch (error) {\n      this.logger.error(\n        `Failed to delete database settings item: ${databaseId}`,\n        error,\n        sessionMetadata,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/encryption.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { PlainEncryptionStrategy } from 'src/modules/encryption/strategies/plain-encryption.strategy';\nimport { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';\n\n@Module({})\nexport class EncryptionModule {\n  static register() {\n    return {\n      module: EncryptionModule,\n      providers: [\n        PlainEncryptionStrategy,\n        KeytarEncryptionStrategy,\n        KeyEncryptionStrategy,\n        EncryptionService,\n      ],\n      exports: [\n        EncryptionService,\n        // todo: rework to not export strategies\n        PlainEncryptionStrategy,\n        KeytarEncryptionStrategy,\n        KeyEncryptionStrategy,\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/encryption.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAppSettings,\n  mockAppSettingsInitial,\n  mockAppSettingsWithoutPermissions,\n  mockEncryptionStrategyInstance,\n  mockEncryptResult,\n  mockKeyEncryptionStrategyInstance,\n  mockKeyEncryptResult,\n  mockSettingsService,\n  MockType,\n  mockConstantsProvider,\n} from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { PlainEncryptionStrategy } from 'src/modules/encryption/strategies/plain-encryption.strategy';\nimport { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { UnsupportedEncryptionStrategyException } from 'src/modules/encryption/exceptions';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\ndescribe('EncryptionService', () => {\n  let service: EncryptionService;\n  let plainEncryptionStrategy: MockType<PlainEncryptionStrategy>;\n  let keytarEncryptionStrategy: MockType<KeytarEncryptionStrategy>;\n  let keyEncryptionStrategy: MockType<KeyEncryptionStrategy>;\n  let settingsService: MockType<SettingsService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EncryptionService,\n        {\n          provide: PlainEncryptionStrategy,\n          useFactory: mockEncryptionStrategyInstance,\n        },\n        {\n          provide: KeytarEncryptionStrategy,\n          useFactory: mockEncryptionStrategyInstance,\n        },\n        {\n          provide: KeyEncryptionStrategy,\n          useFactory: mockKeyEncryptionStrategyInstance,\n        },\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(EncryptionService);\n    plainEncryptionStrategy = module.get(PlainEncryptionStrategy);\n    keytarEncryptionStrategy = module.get(KeytarEncryptionStrategy);\n    keyEncryptionStrategy = module.get(KeyEncryptionStrategy);\n    settingsService = module.get(SettingsService);\n\n    settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n  });\n\n  describe('getAvailableEncryptionStrategies', () => {\n    it('Should return list 2 strategies available (KEYTAR and PLAIN)', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n\n      expect(await service.getAvailableEncryptionStrategies()).toEqual([\n        EncryptionStrategy.PLAIN,\n        EncryptionStrategy.KEYTAR,\n      ]);\n    });\n    it('Should return list 2 strategies available (KEY and PLAIN)', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      expect(await service.getAvailableEncryptionStrategies()).toEqual([\n        EncryptionStrategy.PLAIN,\n        EncryptionStrategy.KEY,\n      ]);\n    });\n    it('Should return list 2 strategies available (KEY and PLAIN) even when KEYTAR available', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      expect(await service.getAvailableEncryptionStrategies()).toEqual([\n        EncryptionStrategy.PLAIN,\n        EncryptionStrategy.KEY,\n      ]);\n    });\n    it('Should return list with one strategy available', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n\n      expect(await service.getAvailableEncryptionStrategies()).toEqual([\n        EncryptionStrategy.PLAIN,\n      ]);\n    });\n  });\n\n  describe('isEncryptionAvailable', () => {\n    it('should return true when multiple strategies are available (KEYTAR and PLAIN)', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n\n      const result = await service.isEncryptionAvailable();\n\n      expect(result).toBe(true);\n    });\n\n    it('should return true when multiple strategies are available (KEY and PLAIN)', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      const result = await service.isEncryptionAvailable();\n\n      expect(result).toBe(true);\n    });\n\n    it('should return true when all strategies are available (KEY, KEYTAR and PLAIN)', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      const result = await service.isEncryptionAvailable();\n\n      expect(result).toBe(true);\n    });\n\n    it('should return false when only PLAIN strategy is available', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n\n      const result = await service.isEncryptionAvailable();\n\n      expect(result).toBe(false);\n    });\n  });\n\n  describe('getEncryptionStrategy', () => {\n    it('Should return KEYTAR strategy based on app agreements', async () => {\n      expect(await service.getEncryptionStrategy()).toEqual(\n        keytarEncryptionStrategy,\n      );\n    });\n    it('Should return KEY strategy based on app agreements even when KEYTAR available', async () => {\n      keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      expect(await service.getEncryptionStrategy()).toEqual(\n        keyEncryptionStrategy,\n      );\n    });\n    it('Should return PLAIN strategy based on app agreements even when KEY available', async () => {\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      settingsService.getAppSettings.mockResolvedValueOnce(\n        mockAppSettingsWithoutPermissions,\n      );\n\n      expect(await service.getEncryptionStrategy()).toEqual(\n        plainEncryptionStrategy,\n      );\n    });\n    it('Should throw an error if encryption strategy was not set by user', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce(\n        mockAppSettingsInitial,\n      );\n\n      await expect(service.getEncryptionStrategy()).rejects.toThrow(\n        UnsupportedEncryptionStrategyException,\n      );\n    });\n  });\n\n  describe('encrypt', () => {\n    it('Should encrypt data and return proper response (KEYTAR)', async () => {\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n\n      keytarEncryptionStrategy.encrypt.mockResolvedValueOnce(mockEncryptResult);\n\n      expect(await service.encrypt('string')).toEqual(mockEncryptResult);\n    });\n    it('Should encrypt data and return proper response (KEY)', async () => {\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      keyEncryptionStrategy.encrypt.mockResolvedValueOnce(mockKeyEncryptResult);\n\n      expect(await service.encrypt('string')).toEqual(mockKeyEncryptResult);\n    });\n  });\n\n  describe('decrypt', () => {\n    it('Should return decrypted string (KEYTAR)', async () => {\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);\n\n      keytarEncryptionStrategy.decrypt.mockResolvedValueOnce(\n        mockEncryptResult.data,\n      );\n\n      expect(\n        await service.decrypt('string', EncryptionStrategy.KEYTAR),\n      ).toEqual(mockEncryptResult.data);\n    });\n    it('Should return decrypted string (KEY)', async () => {\n      keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);\n\n      keyEncryptionStrategy.decrypt.mockResolvedValueOnce(\n        mockKeyEncryptResult.data,\n      );\n\n      expect(await service.decrypt('string', EncryptionStrategy.KEY)).toEqual(\n        mockKeyEncryptResult.data,\n      );\n    });\n    it('Should return null when no data passed', async () => {\n      expect(await service.decrypt(null, EncryptionStrategy.KEYTAR)).toEqual(\n        null,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/encryption.service.ts",
    "content": "import { forwardRef, Inject, Injectable } from '@nestjs/common';\nimport { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';\nimport { PlainEncryptionStrategy } from 'src/modules/encryption/strategies/plain-encryption.strategy';\nimport {\n  EncryptionResult,\n  EncryptionStrategy,\n} from 'src/modules/encryption/models';\nimport { IEncryptionStrategy } from 'src/modules/encryption/strategies/encryption-strategy.interface';\nimport { UnsupportedEncryptionStrategyException } from 'src/modules/encryption/exceptions';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\n@Injectable()\nexport class EncryptionService {\n  constructor(\n    @Inject(forwardRef(() => SettingsService))\n    private readonly settingsService: SettingsService,\n    private readonly keytarEncryptionStrategy: KeytarEncryptionStrategy,\n    private readonly plainEncryptionStrategy: PlainEncryptionStrategy,\n    private readonly keyEncryptionStrategy: KeyEncryptionStrategy,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {}\n\n  /**\n   * Returns list of available encryption strategies\n   * It is needed for users to choose one and save it in the app settings\n   */\n  async getAvailableEncryptionStrategies(): Promise<string[]> {\n    const strategies = [EncryptionStrategy.PLAIN];\n\n    if (await this.keyEncryptionStrategy.isAvailable()) {\n      strategies.push(EncryptionStrategy.KEY);\n    } else if (await this.keytarEncryptionStrategy.isAvailable()) {\n      strategies.push(EncryptionStrategy.KEYTAR);\n    }\n\n    return strategies;\n  }\n\n  /**\n   * Checks if any encryption strategy other than PLAIN is available\n   */\n  async isEncryptionAvailable(): Promise<boolean> {\n    const strategies = await this.getAvailableEncryptionStrategies();\n    return (\n      strategies.length > 1 ||\n      (strategies.length === 1 && strategies[0] !== EncryptionStrategy.PLAIN)\n    );\n  }\n\n  /**\n   * Get encryption strategy based on app settings\n   * This strategy should be received from app settings but before it should be set by user.\n   * As this settings is required we have to block any action that requires explicit user choice\n   * so we will throw an error when encryption type is null\n   */\n  async getEncryptionStrategy(): Promise<IEncryptionStrategy> {\n    // todo: add encryption provider as a strategy to be configurable\n    const settings = await this.settingsService.getAppSettings(\n      this.constantsProvider.getSystemSessionMetadata(),\n    );\n    switch (settings.agreements?.encryption) {\n      case true:\n        if (await this.keyEncryptionStrategy.isAvailable()) {\n          return this.keyEncryptionStrategy;\n        }\n        return this.keytarEncryptionStrategy;\n      case false:\n        return this.plainEncryptionStrategy;\n      default:\n        throw new UnsupportedEncryptionStrategyException();\n    }\n  }\n\n  /**\n   * Encrypt data based on app encryption strategy\n   * @param data\n   */\n  async encrypt(data: string): Promise<EncryptionResult> {\n    const strategy = await this.getEncryptionStrategy();\n    return strategy.encrypt(data);\n  }\n\n  /**\n   * Try to decrypt data based on app encryption strategy\n   * If data was encrypted before with strategy that is not match to the current one\n   * it will be handled by the app encryption strategy\n   * @param data\n   * @param encryptedWith\n   */\n  async decrypt(data: string, encryptedWith: string): Promise<string | null> {\n    // Nothing to decrypt. Should return null then\n    if (!data) {\n      return null;\n    }\n\n    const strategy = await this.getEncryptionStrategy();\n    return strategy.decrypt(data, encryptedWith);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/encryption-service-error.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\n\nexport class EncryptionServiceErrorException extends HttpException {\n  constructor(\n    response: string | Record<string, any> = {\n      message: 'Encryption service error',\n      name: 'EncryptionServiceError',\n      statusCode: 500,\n    },\n    status = 500,\n  ) {\n    super(response, status);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/index.ts",
    "content": "export * from './encryption-service-error.exception';\nexport * from './key-decryption-error.exception';\nexport * from './key-encryption-error.exception';\nexport * from './key-unavailable.exception';\nexport * from './keytar-decryption-error.exception';\nexport * from './keytar-encryption-error.exception';\nexport * from './keytar-unavailable.exception';\nexport * from './unsupported-encryption-strategy.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/key-decryption-error.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class KeyDecryptionErrorException extends EncryptionServiceErrorException {\n  constructor(message = 'Unable to decrypt data') {\n    super(\n      {\n        message,\n        name: 'KeyDecryptionError',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/key-encryption-error.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class KeyEncryptionErrorException extends EncryptionServiceErrorException {\n  constructor(message = 'Unable to encrypt data') {\n    super(\n      {\n        message,\n        name: 'KeyEncryptionError',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/key-unavailable.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class KeyUnavailableException extends EncryptionServiceErrorException {\n  constructor(message = 'Encryption key unavailable') {\n    super(\n      {\n        message,\n        name: 'KeyUnavailable',\n        statusCode: 503,\n      },\n      503,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/keytar-decryption-error.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class KeytarDecryptionErrorException extends EncryptionServiceErrorException {\n  constructor(message = 'Unable to decrypt data with Keytar') {\n    super(\n      {\n        message,\n        name: 'KeytarDecryptionError',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/keytar-encryption-error.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class KeytarEncryptionErrorException extends EncryptionServiceErrorException {\n  constructor(message = 'Unable to encrypt data with Keytar') {\n    super(\n      {\n        message,\n        name: 'KeytarEncryptionError',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/keytar-unavailable.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class KeytarUnavailableException extends EncryptionServiceErrorException {\n  constructor(message = 'Keytar unavailable') {\n    super(\n      {\n        message,\n        name: 'KeytarUnavailable',\n        statusCode: 503,\n      },\n      503,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/exceptions/unsupported-encryption-strategy.exception.ts",
    "content": "import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions/encryption-service-error.exception';\n\nexport class UnsupportedEncryptionStrategyException extends EncryptionServiceErrorException {\n  constructor(message = 'Unsupported encryption strategy') {\n    super(\n      {\n        message,\n        name: 'UnsupportedEncryptionStrategy',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/model.encryptor.ts",
    "content": "import { isUndefined } from 'lodash';\nimport { Logger } from '@nestjs/common';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { cloneClassInstance } from 'src/utils';\n\nexport class ModelEncryptor {\n  private readonly logger = new Logger('ModelEncryptor');\n\n  constructor(\n    private readonly encryptionService: EncryptionService,\n    // list of properties to encrypt/decrypt\n    protected readonly fields: string[],\n  ) {}\n\n  async encryptEntities<T>(entities: T[]): Promise<T[]> {\n    return Promise.all(\n      entities.map(async (entity) => {\n        return this.encryptEntity(entity);\n      }),\n    );\n  }\n\n  async decryptEntities<T>(\n    entities: T[],\n    ignoreErrors: boolean = false,\n  ): Promise<T[]> {\n    return Promise.all(\n      entities.map(async (entity) => {\n        return this.decryptEntity(entity, ignoreErrors);\n      }),\n    );\n  }\n\n  /**\n   * Encrypt required fields based on picked encryption strategy\n   * Should always throw an encryption error to determine that something wrong\n   * with encryption strategy\n   *\n   * @param entity\n   * @private\n   */\n  async encryptEntity<T>(entity: T): Promise<T> {\n    const encryptedEntity = cloneClassInstance(entity);\n\n    // TODO: implement support depth in field, 'obj.field'\n    await Promise.all(\n      this.fields.map(async (field) => {\n        if (entity[field]) {\n          const { data, encryption } = await this.encryptionService.encrypt(\n            entity[field],\n          );\n          encryptedEntity[field] = data;\n          encryptedEntity['encryption'] = encryption;\n        }\n      }),\n    );\n\n    return encryptedEntity;\n  }\n\n  /**\n   * Decrypt required fields\n   * This method should optionally not fail (to not block users to navigate across the app\n   * on decryption error, for example, to be able change encryption strategy in the future)\n   *\n   * When ignoreErrors = true will return null for failed fields.\n   * It will cause 401 Unauthorized errors when user tries to connect to redis database\n   *\n   * @param entity\n   * @param ignoreErrors\n   * @private\n   */\n  async decryptEntity<T>(entity: T, ignoreErrors: boolean = false): Promise<T> {\n    if (!entity) {\n      return null;\n    }\n\n    const decrypted = cloneClassInstance(entity);\n\n    await Promise.all(\n      this.fields.map(async (field) => {\n        decrypted[field] = await this.decryptField(entity, field, ignoreErrors);\n      }),\n    );\n\n    return decrypted;\n  }\n\n  /**\n   * Decrypt single field if exists\n   *\n   * @param entity\n   * @param field\n   * @param ignoreErrors\n   * @private\n   */\n  async decryptField<T>(\n    entity: T,\n    field: string,\n    ignoreErrors: boolean,\n  ): Promise<string> {\n    if (isUndefined(entity[field])) {\n      return undefined;\n    }\n\n    try {\n      return await this.encryptionService.decrypt(\n        entity[field],\n        entity['encryption'],\n      );\n    } catch (error) {\n      this.logger.error(`Unable to decrypt entity fields: ${field}`, error);\n      if (!ignoreErrors) {\n        throw error;\n      }\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/models/encryption-result.ts",
    "content": "export enum EncryptionStrategy {\n  PLAIN = 'PLAIN',\n  KEYTAR = 'KEYTAR',\n  KEY = 'KEY',\n}\n\nexport class EncryptionResult {\n  encryption?: EncryptionStrategy;\n\n  data: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/models/index.ts",
    "content": "export * from './encryption-result';\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/encryption-strategy.interface.ts",
    "content": "import { EncryptionResult } from 'src/modules/encryption/models';\n\nexport interface IEncryptionStrategy {\n  encrypt(data: string): Promise<EncryptionResult>;\n\n  decrypt(data: string, encryptedWith: string): Promise<string | null>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/key-encryption.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockDataToEncrypt,\n  mockEncryptionKey,\n  mockEncryptResult,\n  mockKeyEncryptResult,\n} from 'src/__mocks__';\nimport {\n  KeyDecryptionErrorException,\n  KeyEncryptionErrorException,\n  KeyUnavailableException,\n} from 'src/modules/encryption/exceptions';\nimport { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';\n\ndescribe('KeyEncryptionStrategy', () => {\n  let service: KeyEncryptionStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [KeyEncryptionStrategy],\n    }).compile();\n\n    service = module.get(KeyEncryptionStrategy);\n    // @ts-ignore\n    service['key'] = mockEncryptionKey;\n  });\n\n  describe('isAvailable', () => {\n    it('Should return true when env specified', async () => {\n      expect(await service.isAvailable()).toEqual(true);\n    });\n\n    it('Should return false when env is not specified', async () => {\n      // @ts-ignore\n      service['key'] = undefined;\n\n      expect(await service.isAvailable()).toEqual(false);\n    });\n  });\n\n  describe('encrypt', () => {\n    it('Should encrypt data', async () => {\n      expect(service['cipherKey']).toEqual(undefined);\n      expect(await service.encrypt(mockDataToEncrypt)).toEqual(\n        mockKeyEncryptResult,\n      );\n      expect(service['cipherKey']).not.toEqual(undefined);\n    });\n    it('Should throw KeyEncryptionError when unable to encrypt', async () => {\n      await expect(service.encrypt(null)).rejects.toThrowError(\n        KeyEncryptionErrorException,\n      );\n    });\n    it('Should throw KeyUnavailable when there is no key but we are trying to encrypt', async () => {\n      // @ts-ignore\n      service['key'] = undefined;\n\n      await expect(service.encrypt(mockDataToEncrypt)).rejects.toThrowError(\n        KeyUnavailableException,\n      );\n    });\n  });\n\n  describe('decrypt', () => {\n    it('Should decrypt data', async () => {\n      expect(service['cipherKey']).toEqual(undefined);\n      expect(\n        await service.decrypt(\n          mockKeyEncryptResult.data,\n          mockKeyEncryptResult.encryption,\n        ),\n      ).toEqual(mockDataToEncrypt);\n      expect(service['cipherKey']).not.toEqual(undefined);\n    });\n    it(\"Should return null when encryption doesn't match KEY\", async () => {\n      expect(await service.decrypt(mockEncryptResult.data, 'PLAIN')).toEqual(\n        null,\n      );\n    });\n    it('Should throw KeyDecryptionError when unable to decrypt', async () => {\n      await expect(\n        service.decrypt(null, mockKeyEncryptResult.encryption),\n      ).rejects.toThrowError(KeyDecryptionErrorException);\n    });\n    it('Should throw KeyUnavailable when there is no key but we are trying to decrypt', async () => {\n      // @ts-ignore\n      service['key'] = undefined;\n\n      await expect(\n        service.decrypt(\n          mockKeyEncryptResult.data,\n          mockKeyEncryptResult.encryption,\n        ),\n      ).rejects.toThrowError(KeyUnavailableException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/key-encryption.strategy.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { createDecipheriv, createCipheriv, createHash } from 'crypto';\nimport {\n  EncryptionResult,\n  EncryptionStrategy,\n} from 'src/modules/encryption/models';\nimport { IEncryptionStrategy } from 'src/modules/encryption/strategies/encryption-strategy.interface';\nimport {\n  KeyDecryptionErrorException,\n  KeyEncryptionErrorException,\n  KeyUnavailableException,\n} from 'src/modules/encryption/exceptions';\nimport config, { Config } from 'src/utils/config';\n\nconst HASH_ALGORITHM = 'sha256';\nconst SERVER_CONFIG = config.get('server') as Config['server'];\nconst ENCRYPTION_CONFIG = config.get('encryption') as Config['encryption'];\n\n@Injectable()\nexport class KeyEncryptionStrategy implements IEncryptionStrategy {\n  private logger = new Logger('KeyEncryptionStrategy');\n\n  private cipherKey: Buffer;\n\n  private readonly key: string;\n\n  constructor() {\n    this.key = SERVER_CONFIG.encryptionKey;\n  }\n\n  private getCipherIV(): Buffer {\n    return Buffer.from(ENCRYPTION_CONFIG.encryptionIV).slice(0, 16);\n  }\n\n  /**\n   * Will return existing cipher stored in-memory or\n   * create new one using specified key and store it in-memory\n   */\n  private async getCipherKey(): Promise<Buffer> {\n    if (!this.cipherKey) {\n      if (!this.key) {\n        throw new KeyUnavailableException();\n      }\n\n      this.cipherKey = createHash(HASH_ALGORITHM)\n        .update(this.key, 'utf8')\n        .digest();\n    }\n\n    return this.cipherKey;\n  }\n\n  /**\n   * Checks if secret key was specified\n   */\n  async isAvailable(): Promise<boolean> {\n    return !!this.key;\n  }\n\n  async encrypt(data: string): Promise<EncryptionResult> {\n    const cipherKey = await this.getCipherKey();\n    try {\n      const cipher = createCipheriv(\n        ENCRYPTION_CONFIG.encryptionAlgorithm,\n        cipherKey,\n        this.getCipherIV(),\n      );\n      let encrypted = cipher.update(data, 'utf8', 'hex');\n      encrypted += cipher.final('hex');\n\n      return {\n        encryption: EncryptionStrategy.KEY,\n        data: encrypted,\n      };\n    } catch (error) {\n      this.logger.error('Unable to encrypt data', error);\n      throw new KeyEncryptionErrorException();\n    }\n  }\n\n  async decrypt(data: string, encryptedWith: string): Promise<string | null> {\n    if (encryptedWith !== EncryptionStrategy.KEY) {\n      return null;\n    }\n\n    const cipherKey = await this.getCipherKey();\n\n    try {\n      const decipher = createDecipheriv(\n        ENCRYPTION_CONFIG.encryptionAlgorithm,\n        cipherKey,\n        this.getCipherIV(),\n      );\n      let decrypted = decipher.update(data, 'hex', 'utf8');\n      decrypted += decipher.final('utf8');\n      return decrypted;\n    } catch (error) {\n      this.logger.error('Unable to decrypt data', error);\n      throw new KeyDecryptionErrorException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/keytar-encryption.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockDataToEncrypt,\n  mockEncryptResult,\n  mockKeytarModule,\n  mockKeytarPassword,\n} from 'src/__mocks__';\nimport { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';\nimport {\n  KeytarDecryptionErrorException,\n  KeytarEncryptionErrorException,\n  KeytarUnavailableException,\n} from 'src/modules/encryption/exceptions';\n\ndescribe('KeytarEncryptionStrategy', () => {\n  let service: KeytarEncryptionStrategy;\n  const keytarModule = mockKeytarModule;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('keytar', () => keytarModule);\n    keytarModule.getPassword.mockReturnValue(mockKeytarPassword);\n    keytarModule.setPassword.mockReturnValue(undefined);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [KeytarEncryptionStrategy],\n    }).compile();\n\n    service = module.get(KeytarEncryptionStrategy);\n  });\n\n  describe('isAvailable', () => {\n    it('Should return true when keytar is available', async () => {\n      expect(await service.isAvailable()).toEqual(true);\n    });\n\n    it('Should return false when keytar is not available', async () => {\n      keytarModule.getPassword.mockRejectedValueOnce(new Error('Some error'));\n\n      expect(await service.isAvailable()).toEqual(false);\n    });\n  });\n\n  describe('encrypt', () => {\n    it('Should encrypt data', async () => {\n      expect(await service.encrypt(mockDataToEncrypt)).toEqual(\n        mockEncryptResult,\n      );\n\n      // check that cached password will be used\n      expect(await service.encrypt(mockDataToEncrypt)).toEqual(\n        mockEncryptResult,\n      );\n      expect(mockKeytarModule.getPassword).toHaveBeenCalledTimes(1);\n      expect(mockKeytarModule.setPassword).not.toHaveBeenCalled();\n    });\n    it('Should encrypt + generate and set password when not exists yet', async () => {\n      keytarModule.getPassword\n        .mockReturnValueOnce(null)\n        .mockReturnValueOnce(mockKeytarPassword);\n      keytarModule.setPassword.mockReturnValueOnce(undefined);\n\n      expect(await service.encrypt(mockDataToEncrypt)).toEqual(\n        mockEncryptResult,\n      );\n\n      expect(mockKeytarModule.setPassword).toHaveBeenCalled();\n    });\n    it('Should throw KeytarEncryptionError when unable to decrypt', async () => {\n      await expect(service.encrypt(null)).rejects.toThrowError(\n        KeytarEncryptionErrorException,\n      );\n    });\n    it('Should throw KeytarUnavailable in getPassword error', async () => {\n      keytarModule.getPassword.mockRejectedValueOnce(new Error());\n\n      await expect(service.encrypt(mockDataToEncrypt)).rejects.toThrowError(\n        KeytarUnavailableException,\n      );\n    });\n    it('Should should throw KeytarUnavailable on setPassword error', async () => {\n      keytarModule.getPassword\n        .mockReturnValueOnce(null)\n        .mockReturnValueOnce(mockKeytarPassword);\n      keytarModule.setPassword.mockRejectedValueOnce(new Error());\n\n      await expect(service.encrypt(mockDataToEncrypt)).rejects.toThrowError(\n        KeytarUnavailableException,\n      );\n    });\n  });\n\n  describe('decrypt', () => {\n    it('Should decrypt data', async () => {\n      expect(\n        await service.decrypt(\n          mockEncryptResult.data,\n          mockEncryptResult.encryption,\n        ),\n      ).toEqual(mockDataToEncrypt);\n\n      // check that cached password will be used\n      expect(\n        await service.decrypt(\n          mockEncryptResult.data,\n          mockEncryptResult.encryption,\n        ),\n      ).toEqual(mockDataToEncrypt);\n      expect(mockKeytarModule.getPassword).toHaveBeenCalledTimes(1);\n      expect(mockKeytarModule.setPassword).not.toHaveBeenCalled();\n    });\n    it(\"Should return null when encryption doesn't match KEYTAR\", async () => {\n      expect(await service.decrypt(mockEncryptResult.data, 'PLAIN')).toEqual(\n        null,\n      );\n    });\n    it('Should decrypt + generate and set password when not exists yet', async () => {\n      keytarModule.getPassword\n        .mockReturnValueOnce(null)\n        .mockReturnValueOnce(mockKeytarPassword);\n      keytarModule.setPassword.mockReturnValueOnce(undefined);\n\n      expect(\n        await service.decrypt(\n          mockEncryptResult.data,\n          mockEncryptResult.encryption,\n        ),\n      ).toEqual(mockDataToEncrypt);\n\n      expect(mockKeytarModule.setPassword).toHaveBeenCalled();\n    });\n    it('Should throw KeytarDecryptionError when unable to decrypt', async () => {\n      await expect(\n        service.decrypt(null, mockEncryptResult.encryption),\n      ).rejects.toThrowError(KeytarDecryptionErrorException);\n    });\n    it('Should throw KeytarUnavailable in getPassword error', async () => {\n      keytarModule.getPassword.mockRejectedValueOnce(new Error());\n\n      await expect(\n        service.decrypt(mockEncryptResult.data, mockEncryptResult.encryption),\n      ).rejects.toThrowError(KeytarUnavailableException);\n    });\n    it('Should should throw KeytarUnavailable on setPassword error', async () => {\n      keytarModule.getPassword\n        .mockReturnValueOnce(null)\n        .mockReturnValueOnce(mockKeytarPassword);\n      keytarModule.setPassword.mockRejectedValueOnce(new Error());\n\n      await expect(\n        service.decrypt(mockEncryptResult.data, mockEncryptResult.encryption),\n      ).rejects.toThrowError(KeytarUnavailableException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/keytar-encryption.strategy.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport {\n  createDecipheriv,\n  createCipheriv,\n  randomBytes,\n  createHash,\n} from 'crypto';\nimport {\n  EncryptionResult,\n  EncryptionStrategy,\n} from 'src/modules/encryption/models';\nimport { IEncryptionStrategy } from 'src/modules/encryption/strategies/encryption-strategy.interface';\nimport {\n  KeytarDecryptionErrorException,\n  KeytarEncryptionErrorException,\n  KeytarUnavailableException,\n} from 'src/modules/encryption/exceptions';\nimport config, { Config } from 'src/utils/config';\n\nconst ACCOUNT = 'app';\nconst SERVER_CONFIG = config.get('server') as Config['server'];\nconst ENCRYPTION_CONFIG = config.get('encryption') as Config['encryption'];\n\n@Injectable()\nexport class KeytarEncryptionStrategy implements IEncryptionStrategy {\n  private logger = new Logger('KeytarEncryptionStrategy');\n\n  private readonly keytar;\n\n  private cipherKey;\n\n  constructor() {\n    try {\n      if (!ENCRYPTION_CONFIG.keytar) {\n        return;\n      }\n\n      // Have to require keytar here since during tests of keytar module\n      // at some point it threw an error when OS secure storage was unavailable\n      // Since it is difficult to reproduce we keep module require here to be\n      // ready for such cases\n      // eslint-disable-next-line global-require\n      this.keytar = require('keytar');\n    } catch (e) {\n      this.logger.error('Failed to initialize keytar module', e);\n    }\n  }\n\n  /**\n   * Generates random password\n   */\n  private generatePassword(): string {\n    return (\n      SERVER_CONFIG.secretStoragePassword || randomBytes(20).toString('base64')\n    );\n  }\n\n  /**\n   * Get password from the OS secret storage\n   * @private\n   */\n  private async getPassword(): Promise<string | null> {\n    try {\n      return await this.keytar.getPassword(\n        ENCRYPTION_CONFIG.keytarService,\n        ACCOUNT,\n      );\n    } catch (error) {\n      this.logger.error('Unable to get password');\n      throw new KeytarUnavailableException();\n    }\n  }\n\n  /**\n   * Save password in the OS secret storage\n   * @param password\n   * @private\n   */\n  private async setPassword(password: string): Promise<void> {\n    try {\n      await this.keytar.setPassword(\n        ENCRYPTION_CONFIG.keytarService,\n        ACCOUNT,\n        password,\n      );\n    } catch (error) {\n      this.logger.error('Unable to set password');\n      throw new KeytarUnavailableException();\n    }\n  }\n\n  private getCipherIV(): Buffer {\n    return Buffer.from(ENCRYPTION_CONFIG.encryptionIV).slice(0, 16);\n  }\n\n  /**\n   * Get password from storage and create cipher key\n   * Note: Will generate new password if it doesn't exists yet\n   */\n  private async getCipherKey(): Promise<Buffer> {\n    if (!this.cipherKey) {\n      let password = await this.getPassword();\n      if (!password) {\n        await this.setPassword(this.generatePassword());\n        password = await this.getPassword();\n      }\n\n      this.cipherKey = await createHash('sha256')\n        .update(password, 'utf8') // lgtm[js/insufficient-password-hash]\n        .digest();\n    }\n\n    return this.cipherKey;\n  }\n\n  /**\n   * Checks if Keytar functionality is available\n   * Basically just try to get a password and checks if this call fails\n   */\n  async isAvailable(): Promise<boolean> {\n    if (!ENCRYPTION_CONFIG.keytar) {\n      return false;\n    }\n\n    try {\n      await this.keytar.getPassword(ENCRYPTION_CONFIG.keytarService, ACCOUNT);\n      return true;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  async encrypt(data: string): Promise<EncryptionResult> {\n    const cipherKey = await this.getCipherKey();\n    try {\n      const cipher = createCipheriv(\n        ENCRYPTION_CONFIG.encryptionAlgorithm,\n        cipherKey,\n        this.getCipherIV(),\n      );\n      let encrypted = cipher.update(data, 'utf8', 'hex');\n      encrypted += cipher.final('hex');\n\n      return {\n        encryption: EncryptionStrategy.KEYTAR,\n        data: encrypted,\n      };\n    } catch (error) {\n      this.logger.error('Unable to encrypt data', error);\n      throw new KeytarEncryptionErrorException();\n    }\n  }\n\n  async decrypt(data: string, encryptedWith: string): Promise<string | null> {\n    if (encryptedWith !== EncryptionStrategy.KEYTAR) {\n      return null;\n    }\n\n    const cipherKey = await this.getCipherKey();\n    try {\n      const decipher = createDecipheriv(\n        ENCRYPTION_CONFIG.encryptionAlgorithm,\n        cipherKey,\n        this.getCipherIV(),\n      );\n      let decrypted = decipher.update(data, 'hex', 'utf8');\n      decrypted += decipher.final('utf8');\n      return decrypted;\n    } catch (error) {\n      this.logger.error('Unable to decrypt data', error);\n      throw new KeytarDecryptionErrorException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/plain-encryption.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { mockDataToEncrypt, mockEncryptResult } from 'src/__mocks__';\nimport { PlainEncryptionStrategy } from 'src/modules/encryption/strategies/plain-encryption.strategy';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\n\ndescribe('PlainEncryptionStrategy', () => {\n  let service: PlainEncryptionStrategy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [PlainEncryptionStrategy],\n    }).compile();\n\n    service = module.get(PlainEncryptionStrategy);\n  });\n\n  describe('encrypt', () => {\n    it('Should return unencrypted data', async () => {\n      expect(await service.encrypt(mockDataToEncrypt)).toEqual({\n        data: mockDataToEncrypt,\n        encryption: EncryptionStrategy.PLAIN,\n      });\n    });\n  });\n\n  describe('decrypt', () => {\n    it('Should return plain data', async () => {\n      expect(\n        await service.decrypt(mockEncryptResult.data, EncryptionStrategy.PLAIN),\n      ).toEqual(mockEncryptResult.data);\n    });\n    it(\"Should return null when encryption doesn't match PLAIN\", async () => {\n      expect(await service.decrypt(mockEncryptResult.data, 'KEYTAR')).toEqual(\n        null,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/encryption/strategies/plain-encryption.strategy.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport {\n  EncryptionResult,\n  EncryptionStrategy,\n} from 'src/modules/encryption/models';\nimport { IEncryptionStrategy } from 'src/modules/encryption/strategies/encryption-strategy.interface';\n\n@Injectable()\nexport class PlainEncryptionStrategy implements IEncryptionStrategy {\n  async encrypt(data: string): Promise<EncryptionResult> {\n    return {\n      encryption: EncryptionStrategy.PLAIN,\n      data,\n    };\n  }\n\n  async decrypt(data: string, encryptedWith: string): Promise<string | null> {\n    if (encryptedWith !== EncryptionStrategy.PLAIN) {\n      return null;\n    }\n\n    return data;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/constants/index.ts",
    "content": "import { Feature } from 'src/modules/feature/model/feature';\n\nexport enum FeatureServerEvents {\n  FeaturesRecalculate = 'FeaturesRecalculate',\n  FeaturesRecalculated = 'FeaturesRecalculated',\n}\n\nexport enum FeatureEvents {\n  Features = 'features',\n}\n\nexport enum FeatureStorage {\n  Env = 'env',\n  Database = 'database',\n  Custom = 'custom',\n}\nexport enum FeatureConfigConfigDestination {\n  Default = 'default',\n  Remote = 'remote',\n}\n\nexport enum KnownFeatures {\n  InsightsRecommendations = 'insightsRecommendations',\n  CloudSso = 'cloudSso',\n  CloudSsoRecommendedSettings = 'cloudSsoRecommendedSettings',\n  RedisModuleFilter = 'redisModuleFilter',\n  RedisClient = 'redisClient',\n  DocumentationChat = 'documentationChat',\n  DatabaseChat = 'databaseChat',\n  Rdi = 'redisDataIntegration',\n  HashFieldExpiration = 'hashFieldExpiration',\n  EnhancedCloudUI = 'enhancedCloudUI',\n  DatabaseManagement = 'databaseManagement',\n  VectorSearchV2 = 'vectorSearchV2',\n  AzureEntraId = 'azureEntraId',\n  DevAzureEntraId = 'dev-azureEntraId',\n  DevBrowser = 'dev-browser',\n}\n\nexport interface IFeatureFlag {\n  name: string;\n  storage: string;\n  factory?: () => Partial<Feature>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/constants/known-features.ts",
    "content": "import {\n  FeatureStorage,\n  IFeatureFlag,\n  KnownFeatures,\n} from 'src/modules/feature/constants/index';\nimport { CloudSsoFeatureFlag } from 'src/modules/cloud/cloud-sso.feature.flag';\nimport config, { Config } from 'src/utils/config';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\nexport const knownFeatures: Record<KnownFeatures, IFeatureFlag> = {\n  [KnownFeatures.InsightsRecommendations]: {\n    name: KnownFeatures.InsightsRecommendations,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.CloudSso]: {\n    name: KnownFeatures.CloudSso,\n    storage: FeatureStorage.Database,\n    factory: CloudSsoFeatureFlag.getFeature,\n  },\n  [KnownFeatures.CloudSsoRecommendedSettings]: {\n    name: KnownFeatures.CloudSsoRecommendedSettings,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.RedisModuleFilter]: {\n    name: KnownFeatures.RedisModuleFilter,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.RedisClient]: {\n    name: KnownFeatures.RedisClient,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.DocumentationChat]: {\n    name: KnownFeatures.DocumentationChat,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.DatabaseChat]: {\n    name: KnownFeatures.DatabaseChat,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.HashFieldExpiration]: {\n    name: KnownFeatures.HashFieldExpiration,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.Rdi]: {\n    name: KnownFeatures.Rdi,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.EnhancedCloudUI]: {\n    name: KnownFeatures.EnhancedCloudUI,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.DatabaseManagement]: {\n    name: KnownFeatures.DatabaseManagement,\n    storage: FeatureStorage.Custom,\n    factory: () => ({\n      name: KnownFeatures.DatabaseManagement,\n      flag: SERVER_CONFIG.databaseManagement,\n    }),\n  },\n  [KnownFeatures.VectorSearchV2]: {\n    name: KnownFeatures.VectorSearchV2,\n    storage: FeatureStorage.Database,\n  },\n\n  [KnownFeatures.AzureEntraId]: {\n    name: KnownFeatures.AzureEntraId,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.DevAzureEntraId]: {\n    name: KnownFeatures.DevAzureEntraId,\n    storage: FeatureStorage.Database,\n  },\n  [KnownFeatures.DevBrowser]: {\n    name: KnownFeatures.DevBrowser,\n    storage: FeatureStorage.Database,\n  },\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/entities/feature.entity.ts",
    "content": "import { Column, Entity, PrimaryColumn } from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('features')\nexport class FeatureEntity {\n  @Expose()\n  @PrimaryColumn()\n  name: string;\n\n  @Expose()\n  @Column()\n  flag: boolean;\n\n  @Expose()\n  @Column({ nullable: true })\n  strategy?: string;\n\n  @Expose()\n  @Column({ nullable: true, type: 'text' })\n  @DataAsJsonString()\n  data?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/entities/features-config.entity.ts",
    "content": "import {\n  Column,\n  Entity,\n  PrimaryGeneratedColumn,\n  UpdateDateColumn,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('features_config')\nexport class FeaturesConfigEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column({ nullable: true, type: 'float' })\n  @Expose()\n  controlNumber: number;\n\n  @Column({ nullable: false })\n  @Expose()\n  @DataAsJsonString()\n  data: string;\n\n  @UpdateDateColumn()\n  @Expose()\n  updatedAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/exceptions/index.ts",
    "content": "export * from './unable-to-fetch-remote-config.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/exceptions/unable-to-fetch-remote-config.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\n\nexport class UnableToFetchRemoteConfigException extends HttpException {\n  constructor(\n    response: string | Record<string, any> = {\n      message: 'Unable to fetch remote config',\n      name: 'UnableToFetchRemoteConfigException',\n      statusCode: 500,\n    },\n    status = 500,\n  ) {\n    super(response, status);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/feature.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { AppAnalyticsEvents, TelemetryEvents } from 'src/constants';\nimport { FeatureAnalytics } from 'src/modules/feature/feature.analytics';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport { UnableToFetchRemoteConfigException } from 'src/modules/feature/exceptions';\nimport { ValidationError } from 'class-validator';\n\ndescribe('FeatureAnalytics', () => {\n  let service: FeatureAnalytics;\n  let eventEmitter: EventEmitter2;\n  let sendEventSpy;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        FeatureAnalytics,\n        {\n          provide: EventEmitter2,\n          useFactory: () => ({\n            emit: jest.fn(),\n          }),\n        },\n      ],\n    }).compile();\n\n    service = module.get(FeatureAnalytics);\n    eventEmitter = module.get<EventEmitter2>(EventEmitter2);\n    sendEventSpy = jest.spyOn(service as any, 'sendEvent');\n  });\n\n  describe('sendFeatureFlagConfigUpdated', () => {\n    it('should emit FEATURE_FLAG_CONFIG_UPDATED telemetry event', async () => {\n      service.sendFeatureFlagConfigUpdated(mockSessionMetadata, {\n        configVersion: 7.78,\n        oldVersion: 7.77,\n        type: 'default',\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagConfigUpdated,\n          eventData: {\n            configVersion: 7.78,\n            oldVersion: 7.77,\n            type: 'default',\n          },\n        },\n      );\n    });\n    it('should not fail and do not send in case of any error', async () => {\n      sendEventSpy.mockImplementationOnce(() => {\n        throw new Error('some kind of an error');\n      });\n\n      service.sendFeatureFlagConfigUpdated(mockSessionMetadata, {} as any);\n\n      expect(eventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendFeatureFlagRecalculated', () => {\n    it('should emit FEATURE_FLAG_RECALCULATED telemetry event', async () => {\n      service.sendFeatureFlagRecalculated(mockSessionMetadata, {\n        configVersion: 7.78,\n        features: {\n          insightsRecommendations: {\n            flag: true,\n          },\n          another_feature: {\n            flag: false,\n          },\n        },\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagRecalculated,\n          eventData: {\n            configVersion: 7.78,\n            features: {\n              insightsRecommendations: true,\n              another_feature: false,\n            },\n          },\n        },\n      );\n    });\n    it('should not fail and do not send in case of an error', async () => {\n      sendEventSpy.mockImplementationOnce(() => {\n        throw new Error();\n      });\n\n      service.sendFeatureFlagRecalculated(mockSessionMetadata, {} as any);\n\n      expect(eventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendFeatureFlagConfigUpdateError', () => {\n    it('should emit telemetry event (common Error)', async () => {\n      service.sendFeatureFlagConfigUpdateError(mockSessionMetadata, {\n        configVersion: 7.78,\n        type: 'default',\n        error: new Error('some sensitive information'),\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagConfigUpdateError,\n          eventData: {\n            configVersion: 7.78,\n            type: 'default',\n            reason: 'Error',\n          },\n        },\n      );\n    });\n    it('should emit telemetry event (UnableToFetchRemoteConfigException)', async () => {\n      service.sendFeatureFlagConfigUpdateError(mockSessionMetadata, {\n        configVersion: 7.78,\n        error: new UnableToFetchRemoteConfigException('some PII'),\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagConfigUpdateError,\n          eventData: {\n            configVersion: 7.78,\n            reason: 'UnableToFetchRemoteConfigException',\n          },\n        },\n      );\n    });\n    it('should emit telemetry event (ValidationError)', async () => {\n      service.sendFeatureFlagConfigUpdateError(mockSessionMetadata, {\n        configVersion: 7.78,\n        type: 'remote',\n        error: new ValidationError(),\n      } as any);\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagConfigUpdateError,\n          eventData: {\n            configVersion: 7.78,\n            type: 'remote',\n            reason: 'ValidationError',\n          },\n        },\n      );\n    });\n    it('should emit telemetry event ([ValidationError] only first exception)', async () => {\n      service.sendFeatureFlagConfigUpdateError(mockSessionMetadata, {\n        configVersion: 7.78,\n        type: 'remote',\n        error: [\n          new ValidationError(),\n          new Error('2nd error which will be ignored'),\n        ] as any[],\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagConfigUpdateError,\n          eventData: {\n            configVersion: 7.78,\n            type: 'remote',\n            reason: 'ValidationError',\n          },\n        },\n      );\n    });\n    it('should not fail and not send in case of an error', async () => {\n      sendEventSpy.mockImplementationOnce(() => {\n        throw new Error('some error');\n      });\n\n      service.sendFeatureFlagConfigUpdateError(mockSessionMetadata, {} as any);\n\n      expect(eventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendFeatureFlagInvalidRemoteConfig', () => {\n    it('should emit telemetry event (common Error)', async () => {\n      service.sendFeatureFlagInvalidRemoteConfig(mockSessionMetadata, {\n        configVersion: 7.78,\n        type: 'default',\n        error: new Error('some sensitive information'),\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagInvalidRemoteConfig,\n          eventData: {\n            configVersion: 7.78,\n            type: 'default',\n            reason: 'Error',\n          },\n        },\n      );\n    });\n    it('should emit telemetry event (UnableToFetchRemoteConfigException)', async () => {\n      service.sendFeatureFlagInvalidRemoteConfig(mockSessionMetadata, {\n        configVersion: 7.78,\n        error: new UnableToFetchRemoteConfigException('some PII'),\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagInvalidRemoteConfig,\n          eventData: {\n            configVersion: 7.78,\n            reason: 'UnableToFetchRemoteConfigException',\n          },\n        },\n      );\n    });\n    it('should emit telemetry event (ValidationError)', async () => {\n      service.sendFeatureFlagInvalidRemoteConfig(mockSessionMetadata, {\n        configVersion: 7.78,\n        type: 'remote',\n        error: new ValidationError() as any,\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagInvalidRemoteConfig,\n          eventData: {\n            configVersion: 7.78,\n            type: 'remote',\n            reason: 'ValidationError',\n          },\n        },\n      );\n    });\n    it('should emit telemetry event ([ValidationError] only first exception)', async () => {\n      service.sendFeatureFlagInvalidRemoteConfig(mockSessionMetadata, {\n        configVersion: 7.78,\n        type: 'remote',\n        error: [\n          new ValidationError(),\n          new Error('2nd error which will be ignored'),\n        ] as any[],\n      });\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.FeatureFlagInvalidRemoteConfig,\n          eventData: {\n            configVersion: 7.78,\n            type: 'remote',\n            reason: 'ValidationError',\n          },\n        },\n      );\n    });\n    it('should not fail and not send in case of an error', async () => {\n      sendEventSpy.mockImplementationOnce(() => {\n        throw new Error('some error');\n      });\n\n      service.sendFeatureFlagInvalidRemoteConfig(\n        mockSessionMetadata,\n        {} as any,\n      );\n\n      expect(eventEmitter.emit).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/feature.analytics.ts",
    "content": "import { forEach, isArray } from 'lodash';\nimport { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class FeatureAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  static getReason(error: Error | Error[]): string {\n    let reason = error;\n\n    if (isArray(error)) {\n      [reason] = error;\n    }\n\n    return reason?.constructor?.name || 'UncaughtError';\n  }\n\n  sendFeatureFlagConfigUpdated(\n    sessionMetadata: SessionMetadata,\n    data: {\n      configVersion: number;\n      oldVersion: number;\n      type?: string;\n    },\n  ): void {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.FeatureFlagConfigUpdated,\n        {\n          configVersion: data.configVersion,\n          oldVersion: data.oldVersion,\n          type: data.type,\n        },\n      );\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  sendFeatureFlagConfigUpdateError(\n    sessionMetadata: SessionMetadata,\n    data: {\n      error: Error | Error[];\n      configVersion?: number;\n      type?: string;\n    },\n  ): void {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.FeatureFlagConfigUpdateError,\n        {\n          configVersion: data.configVersion,\n          type: data.type,\n          reason: FeatureAnalytics.getReason(data.error),\n        },\n      );\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  sendFeatureFlagInvalidRemoteConfig(\n    sessionMetadata: SessionMetadata,\n    data: {\n      error: Error | Error[];\n      configVersion?: number;\n      type?: string;\n    },\n  ): void {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.FeatureFlagInvalidRemoteConfig,\n        {\n          configVersion: data.configVersion,\n          type: data.type,\n          reason: FeatureAnalytics.getReason(data.error),\n        },\n      );\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  sendFeatureFlagRecalculated(\n    sessionMetadata: SessionMetadata,\n    data: {\n      configVersion: number;\n      features: Record<string, { flag: boolean }>;\n      force?: Record<string, boolean>;\n    },\n  ): void {\n    try {\n      const features = {};\n      forEach(data?.features || {}, (value, key) => {\n        features[key] = value?.flag;\n      });\n\n      this.sendEvent(sessionMetadata, TelemetryEvents.FeatureFlagRecalculated, {\n        configVersion: data.configVersion,\n        features,\n        force: data.force,\n      });\n    } catch (e) {\n      // ignore error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/feature.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  HttpCode,\n  Post,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\n\n@ApiTags('Info')\n@Controller('features')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class FeatureController {\n  constructor(\n    private featureService: FeatureService,\n    private featuresConfigService: FeaturesConfigService,\n  ) {}\n\n  @Get('')\n  @ApiEndpoint({\n    description: 'Get list of features',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Get list of features',\n      },\n    ],\n  })\n  async list(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<any> {\n    return this.featureService.list(sessionMetadata);\n  }\n\n  @Post('/sync')\n  @HttpCode(200)\n  async sync(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    return this.featuresConfigService.sync(sessionMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/feature.gateway.ts",
    "content": "import { Server } from 'socket.io';\nimport { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';\nimport config, { Config } from 'src/utils/config';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport {\n  FeatureEvents,\n  FeatureServerEvents,\n} from 'src/modules/feature/constants';\n\nconst SOCKETS_CONFIG = config.get('sockets') as Config['sockets'];\n\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n  serveClient: SOCKETS_CONFIG.serveClient,\n})\nexport class FeatureGateway {\n  @WebSocketServer() wss: Server;\n\n  @OnEvent(FeatureServerEvents.FeaturesRecalculated)\n  feature(data: any) {\n    this.wss.of('/').emit(FeatureEvents.Features, data);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/feature.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { FeatureController } from 'src/modules/feature/feature.controller';\nimport { LocalFeatureService } from 'src/modules/feature/local.feature.service';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { NotificationModule } from 'src/modules/notification/notification.module';\nimport { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';\nimport { LocalFeaturesConfigRepository } from 'src/modules/feature/repositories/local.features-config.repository';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { FeatureRepository } from 'src/modules/feature/repositories/feature.repository';\nimport { LocalFeatureRepository } from 'src/modules/feature/repositories/local.feature.repository';\nimport { FeatureFlagProvider } from 'src/modules/feature/providers/feature-flag/feature-flag.provider';\nimport { FeatureGateway } from 'src/modules/feature/feature.gateway';\nimport { FeatureAnalytics } from 'src/modules/feature/feature.analytics';\nimport { LocalFeaturesConfigService } from 'src/modules/feature/local.features-config.service';\n\n@Module({})\nexport class FeatureModule {\n  static register(\n    featureRepository: Type<FeatureRepository> = LocalFeatureRepository,\n    featuresConfigRepository: Type<FeaturesConfigRepository> = LocalFeaturesConfigRepository,\n    featureService: Type<FeatureService> = LocalFeatureService,\n    featuresConfigService: Type<FeaturesConfigService> = LocalFeaturesConfigService,\n  ) {\n    return {\n      module: FeatureModule,\n      controllers: [FeatureController],\n      providers: [\n        FeatureFlagProvider,\n        FeatureGateway,\n        FeatureAnalytics,\n        {\n          provide: FeatureService,\n          useClass: featureService,\n        },\n        {\n          provide: FeatureRepository,\n          useClass: featureRepository,\n        },\n        {\n          provide: FeaturesConfigRepository,\n          useClass: featuresConfigRepository,\n        },\n        {\n          provide: FeaturesConfigService,\n          useClass: featuresConfigService,\n        },\n      ],\n      exports: [FeatureService, FeaturesConfigService],\n      imports: [NotificationModule],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/feature.service.ts",
    "content": "import { Feature, FeaturesFlags } from 'src/modules/feature/model/feature';\nimport { SessionMetadata } from 'src/common/models';\n\nexport abstract class FeatureService {\n  /**\n   * Fetches entire feature structure\n   * @param sessionMetadata\n   * @param name\n   */\n  abstract getByName(\n    sessionMetadata: SessionMetadata,\n    name: string,\n  ): Promise<Feature>;\n\n  /**\n   * Check if feature enabled by feature name\n   * @param sessionMetadata\n   * @param name\n   */\n  abstract isFeatureEnabled(\n    sessionMetadata: SessionMetadata,\n    name: string,\n  ): Promise<boolean>;\n\n  /**\n   * Get features list with calculated flags and control numbers\n   * @param sessionMetadata\n   */\n  abstract list(sessionMetadata: SessionMetadata): Promise<FeaturesFlags>;\n\n  /**\n   * Recalculate all feature flags based on existing config\n   * @param sessionMetadata\n   */\n  abstract recalculateFeatureFlags(\n    sessionMetadata: SessionMetadata,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/features-config.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport abstract class FeaturesConfigService {\n  protected logger = new Logger(this.constructor.name);\n\n  /**\n   * Should initialize all required values\n   * Sync config on startup (in background)\n   * Set interval to re-sync automatically without waiting for next app start\n   */\n  abstract init(): Promise<void>;\n\n  /**\n   * Get control group and number fields\n   */\n  abstract getControlInfo(\n    sessionMetadata: SessionMetadata,\n  ): Promise<{ controlNumber: number; controlGroup: string }>;\n\n  /**\n   * Get latest config from remote and save it in the local database\n   */\n  abstract sync(sessionMetadata: SessionMetadata): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/local.feature.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockConstantsProvider,\n  mockControlGroup,\n  mockControlNumber,\n  mockFeature,\n  mockFeatureAnalytics,\n  mockFeatureFlagProvider,\n  mockFeatureRepository,\n  mockFeatureDatabaseManagement,\n  mockFeaturesConfig,\n  mockFeaturesConfigJson,\n  mockFeaturesConfigRepository,\n  mockFeaturesConfigService,\n  mockFeatureSso,\n  mockSessionMetadata,\n  MockType,\n  mockUnknownFeature,\n} from 'src/__mocks__';\nimport { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { FeatureAnalytics } from 'src/modules/feature/feature.analytics';\nimport { LocalFeatureService } from 'src/modules/feature/local.feature.service';\nimport { FeatureRepository } from 'src/modules/feature/repositories/feature.repository';\nimport { FeatureFlagProvider } from 'src/modules/feature/providers/feature-flag/feature-flag.provider';\nimport * as fs from 'fs-extra';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\njest.mock('fs-extra');\nconst mockedFs = fs as jest.Mocked<typeof fs>;\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\ndescribe('FeatureService', () => {\n  let service: LocalFeatureService;\n  let repository: MockType<FeatureRepository>;\n  let configsRepository: MockType<FeaturesConfigRepository>;\n  let featureRepository: MockType<FeatureRepository>;\n  let analytics: MockType<FeatureAnalytics>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('fs-extra', () => mockedFs);\n    mockedFs.readFile.mockResolvedValueOnce(\n      JSON.stringify({\n        features: {\n          [KnownFeatures.CloudSso]: false,\n        },\n      }) as any,\n    );\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalFeatureService,\n        {\n          provide: EventEmitter2,\n          useFactory: () => ({\n            emit: jest.fn(),\n          }),\n        },\n        {\n          provide: FeaturesConfigRepository,\n          useFactory: mockFeaturesConfigRepository,\n        },\n        {\n          provide: FeatureRepository,\n          useFactory: mockFeatureRepository,\n        },\n        {\n          provide: FeatureAnalytics,\n          useFactory: mockFeatureAnalytics,\n        },\n        {\n          provide: FeatureFlagProvider,\n          useFactory: mockFeatureFlagProvider,\n        },\n        {\n          provide: FeaturesConfigService,\n          useFactory: mockFeaturesConfigService,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalFeatureService);\n    repository = module.get(FeatureRepository);\n    configsRepository = module.get(FeaturesConfigRepository);\n    featureRepository = module.get(FeatureRepository);\n    analytics = module.get(FeatureAnalytics);\n\n    mockedAxios.get.mockResolvedValue({ data: mockFeaturesConfigJson });\n  });\n\n  describe('getByName', () => {\n    it('should return feature when exists', async () => {\n      expect(\n        await service.getByName(\n          mockSessionMetadata,\n          KnownFeatures.InsightsRecommendations,\n        ),\n      ).toEqual(mockFeature);\n      expect(featureRepository.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.InsightsRecommendations,\n      );\n    });\n    it('should return feature with \"custom\" storage', async () => {\n      expect(\n        await service.getByName(\n          mockSessionMetadata,\n          KnownFeatures.DatabaseManagement,\n        ),\n      ).toEqual(mockFeatureDatabaseManagement);\n      expect(featureRepository.get).not.toHaveBeenCalledWith();\n    });\n    it('should return null for unsupported storage type (undefined in current test)', async () => {\n      expect(\n        await service.getByName(mockSessionMetadata, 'unknown feature'),\n      ).toEqual(null);\n      expect(featureRepository.get).not.toHaveBeenCalledWith();\n    });\n    it(\"should return null when feature doesn't exists\", async () => {\n      featureRepository.get.mockResolvedValueOnce(null);\n      expect(\n        await service.getByName(\n          mockSessionMetadata,\n          KnownFeatures.InsightsRecommendations,\n        ),\n      ).toEqual(null);\n    });\n    it('should return null in case of an error', async () => {\n      featureRepository.get.mockRejectedValueOnce(\n        new Error('Unable to fetch flag from db'),\n      );\n      expect(\n        await service.getByName(\n          mockSessionMetadata,\n          KnownFeatures.InsightsRecommendations,\n        ),\n      ).toEqual(null);\n    });\n  });\n\n  describe('isFeatureEnabled', () => {\n    it('should return true when in db: true', async () => {\n      expect(\n        await service.isFeatureEnabled(\n          mockSessionMetadata,\n          KnownFeatures.InsightsRecommendations,\n        ),\n      ).toEqual(true);\n      expect(featureRepository.get).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.InsightsRecommendations,\n      );\n    });\n    it('should return false when in db: false', async () => {\n      repository.get.mockResolvedValue({ flag: false });\n      expect(\n        await service.isFeatureEnabled(\n          mockSessionMetadata,\n          KnownFeatures.InsightsRecommendations,\n        ),\n      ).toEqual(false);\n    });\n    it('should return true for custom storage', async () => {\n      expect(\n        await service.isFeatureEnabled(\n          mockSessionMetadata,\n          KnownFeatures.DatabaseManagement,\n        ),\n      ).toEqual(true);\n      expect(featureRepository.get).not.toHaveBeenCalledWith();\n    });\n    it('should return false for unsupported storage type (undefined in current test)', async () => {\n      expect(\n        await service.isFeatureEnabled(mockSessionMetadata, 'unknown feature'),\n      ).toEqual(false);\n      expect(featureRepository.get).not.toHaveBeenCalledWith();\n    });\n    it('should return false in case of an error', async () => {\n      repository.get.mockRejectedValueOnce(\n        new Error('Unable to fetch flag from db'),\n      );\n      expect(\n        await service.isFeatureEnabled(\n          mockSessionMetadata,\n          KnownFeatures.InsightsRecommendations,\n        ),\n      ).toEqual(false);\n    });\n  });\n\n  describe('list', () => {\n    it('should return list of features flags', async () => {\n      expect(await service.list(mockSessionMetadata)).toEqual({\n        controlGroup: mockControlGroup,\n        controlNumber: mockControlNumber,\n        features: {\n          [KnownFeatures.InsightsRecommendations]: mockFeature,\n          [KnownFeatures.CloudSso]: mockFeatureSso,\n          [KnownFeatures.DatabaseManagement]: mockFeatureDatabaseManagement,\n        },\n      });\n    });\n  });\n\n  describe('recalculateFeatureFlags', () => {\n    it('should recalculate flags (1 update an 1 delete)', async () => {\n      repository.list.mockResolvedValueOnce([\n        mockFeature,\n        mockFeatureSso,\n        mockUnknownFeature,\n      ]);\n      repository.list.mockResolvedValueOnce([mockFeature, mockFeatureSso]);\n      configsRepository.getOrCreate.mockResolvedValueOnce(mockFeaturesConfig);\n\n      await service.recalculateFeatureFlags();\n\n      expect(repository.delete).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockUnknownFeature.name,\n      );\n      expect(repository.upsert).toHaveBeenCalledWith(mockSessionMetadata, {\n        name: KnownFeatures.InsightsRecommendations,\n        flag: mockFeaturesConfig.data.features.get(\n          KnownFeatures.InsightsRecommendations,\n        ).flag,\n      });\n      expect(analytics.sendFeatureFlagRecalculated).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          configVersion: mockFeaturesConfig.data.version,\n          features: {\n            [KnownFeatures.InsightsRecommendations]: mockFeature,\n            [KnownFeatures.CloudSso]: mockFeatureSso,\n            [KnownFeatures.DatabaseManagement]: mockFeatureDatabaseManagement,\n          },\n          force: {\n            [KnownFeatures.CloudSso]: false,\n          },\n        },\n      );\n    });\n    it('should not fail in case of an error', async () => {\n      repository.list.mockRejectedValueOnce(new Error());\n\n      await service.recalculateFeatureFlags();\n\n      expect(repository.delete).not.toHaveBeenCalled();\n      expect(repository.upsert).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/local.feature.service.ts",
    "content": "import { find, forEach, isBoolean } from 'lodash';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { FeatureRepository } from 'src/modules/feature/repositories/feature.repository';\nimport {\n  FeatureServerEvents,\n  FeatureStorage,\n} from 'src/modules/feature/constants';\nimport { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';\nimport { FeatureFlagProvider } from 'src/modules/feature/providers/feature-flag/feature-flag.provider';\nimport { EventEmitter2, OnEvent } from '@nestjs/event-emitter';\nimport { FeatureAnalytics } from 'src/modules/feature/feature.analytics';\nimport { knownFeatures } from 'src/modules/feature/constants/known-features';\nimport { Feature, FeaturesFlags } from 'src/modules/feature/model/feature';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { SessionMetadata } from 'src/common/models';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\n@Injectable()\nexport class LocalFeatureService extends FeatureService {\n  private logger = new Logger('LocalFeatureService');\n\n  constructor(\n    private readonly repository: FeatureRepository,\n    private readonly featuresConfigRepository: FeaturesConfigRepository,\n    private readonly featureFlagProvider: FeatureFlagProvider,\n    private readonly eventEmitter: EventEmitter2,\n    private readonly analytics: FeatureAnalytics,\n    private readonly featuresConfigService: FeaturesConfigService,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {\n    super();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getByName(\n    sessionMetadata: SessionMetadata,\n    name: string,\n  ): Promise<Feature> {\n    try {\n      switch (knownFeatures[name]?.storage) {\n        case FeatureStorage.Database:\n          return await this.repository.get(sessionMetadata, name);\n        case FeatureStorage.Custom:\n          return knownFeatures[name].factory?.();\n        default:\n          return null;\n      }\n    } catch (e) {\n      return null;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async isFeatureEnabled(\n    sessionMetadata: SessionMetadata,\n    name: string,\n  ): Promise<boolean> {\n    try {\n      switch (knownFeatures[name]?.storage) {\n        case FeatureStorage.Database:\n          return (\n            (await this.repository.get(sessionMetadata, name))?.flag === true\n          );\n        case FeatureStorage.Custom:\n          return knownFeatures[name].factory?.()?.flag === true;\n        default:\n          return false;\n      }\n    } catch (e) {\n      return false;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async list(sessionMetadata: SessionMetadata): Promise<FeaturesFlags> {\n    this.logger.debug('Getting features list', sessionMetadata);\n\n    const features = {};\n\n    const featuresFromDatabase = await this.repository.list(sessionMetadata);\n\n    forEach(knownFeatures, (feature) => {\n      // todo: implement various storage strategies support with next features\n      switch (feature?.storage) {\n        case FeatureStorage.Database: {\n          const dbFeature = find(featuresFromDatabase, { name: feature.name });\n          if (dbFeature) {\n            features[feature.name] = {\n              name: dbFeature.name,\n              flag: dbFeature.flag,\n              strategy: dbFeature.strategy || undefined,\n              data: dbFeature.data || undefined,\n            };\n          }\n          break;\n        }\n        case FeatureStorage.Custom:\n          features[feature.name] = feature?.factory?.();\n          break;\n        default:\n        // do nothing\n      }\n    });\n\n    return {\n      features,\n      ...(await this.featuresConfigService.getControlInfo(sessionMetadata)),\n    };\n  }\n\n  /**\n   * Recalculate flags for database features based on controlGroup and new conditions\n   * Fires by EventEmitter from FeaturesConfigService when feature config was updated\n   * Note: This method is needed when feature config auto update is enabled\n   */\n  @OnEvent(FeatureServerEvents.FeaturesRecalculate)\n  async recalculateFeatureFlags(\n    // todo: [USER_CONTEXT] revise\n    sessionMetadata = this.constantsProvider.getSystemSessionMetadata(),\n  ) {\n    this.logger.debug('Recalculating features flags', sessionMetadata);\n\n    try {\n      const actions = {\n        toUpsert: [],\n        toDelete: [],\n      };\n\n      const featuresFromDatabase = await this.repository.list(sessionMetadata);\n      const featuresConfig =\n        await this.featuresConfigRepository.getOrCreate(sessionMetadata);\n\n      this.logger.debug(\n        'Recalculating features flags for new config',\n        featuresConfig,\n      );\n\n      await Promise.all(\n        Array.from(\n          featuresConfig?.data?.features || new Map(),\n          async ([name, feature]) => {\n            if (knownFeatures[name]) {\n              actions.toUpsert.push({\n                ...(await this.featureFlagProvider.calculate(\n                  sessionMetadata,\n                  knownFeatures[name],\n                  feature,\n                )),\n              });\n            }\n          },\n        ),\n      );\n\n      // calculate to delete features\n      actions.toDelete = featuresFromDatabase.filter(\n        (feature) => !featuresConfig?.data?.features?.has?.(feature.name),\n      );\n\n      // delete features\n      await Promise.all(\n        actions.toDelete.map((feature) =>\n          this.repository.delete(sessionMetadata, feature.name),\n        ),\n      );\n      // upsert modified features\n      await Promise.all(\n        actions.toUpsert.map((feature) =>\n          this.repository.upsert(sessionMetadata, feature),\n        ),\n      );\n\n      this.logger.debug(\n        `Features flags recalculated. Updated: ${actions.toUpsert.length} deleted: ${actions.toDelete.length}`,\n        sessionMetadata,\n      );\n\n      const list = await this.list(sessionMetadata);\n      this.eventEmitter.emit(FeatureServerEvents.FeaturesRecalculated, list);\n\n      try {\n        this.analytics.sendFeatureFlagRecalculated(sessionMetadata, {\n          configVersion: (\n            await this.featuresConfigRepository.getOrCreate(sessionMetadata)\n          )?.data?.version,\n          features: list.features,\n          force: await this.listOfForceFlags(),\n        });\n      } catch (e) {\n        // ignore telemetry error\n      }\n    } catch (e) {\n      this.logger.error(\n        'Unable to recalculate features flags',\n        e,\n        sessionMetadata,\n      );\n    }\n  }\n\n  /**\n   * Find forced flags values from custom config using only known features list\n   * This method is needed during feature flags recalculation only\n   */\n  private async listOfForceFlags(): Promise<Record<string, boolean>> {\n    try {\n      const features = {};\n      const forceFeatures = await FeatureFlagStrategy.getCustomConfig();\n\n      forEach(knownFeatures, (known) => {\n        if (isBoolean(forceFeatures[known.name])) {\n          features[known.name] = forceFeatures[known.name];\n        }\n      });\n\n      return features;\n    } catch (e) {\n      return {};\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/local.features-config.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockConstantsProvider,\n  mockControlGroup,\n  mockControlNumber,\n  mockFeatureAnalytics,\n  mockFeaturesConfig,\n  mockFeaturesConfigJson,\n  mockFeaturesConfigRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { plainToInstance } from 'class-transformer';\nimport { FeaturesConfigData } from 'src/modules/feature/model/features-config';\nimport {\n  FeatureConfigConfigDestination,\n  FeatureServerEvents,\n  KnownFeatures,\n} from 'src/modules/feature/constants';\nimport { FeatureAnalytics } from 'src/modules/feature/feature.analytics';\nimport { UnableToFetchRemoteConfigException } from 'src/modules/feature/exceptions';\nimport { LocalFeaturesConfigService } from 'src/modules/feature/local.features-config.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport * as defaultConfig from '../../../config/features-config.json';\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\ndescribe('LocalFeaturesConfigService', () => {\n  let service: LocalFeaturesConfigService;\n  let repository: MockType<FeaturesConfigRepository>;\n  let analytics: MockType<FeatureAnalytics>;\n  let eventEmitter: EventEmitter2;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalFeaturesConfigService,\n        {\n          provide: EventEmitter2,\n          useFactory: () => ({\n            emit: jest.fn(),\n          }),\n        },\n        {\n          provide: FeaturesConfigRepository,\n          useFactory: mockFeaturesConfigRepository,\n        },\n        {\n          provide: FeatureAnalytics,\n          useFactory: mockFeatureAnalytics,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalFeaturesConfigService);\n    repository = module.get(FeaturesConfigRepository);\n    analytics = module.get(FeatureAnalytics);\n    eventEmitter = module.get(EventEmitter2);\n\n    mockedAxios.get.mockResolvedValue({ data: mockFeaturesConfigJson });\n  });\n\n  describe('onApplicationBootstrap', () => {\n    it('should sync on bootstrap', async () => {\n      const spy = jest.spyOn(service, 'sync');\n      await service['init']();\n\n      expect(spy).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('getNewConfig', () => {\n    it('should return remote config', async () => {\n      const result = await service['getNewConfig'](mockSessionMetadata);\n\n      expect(result).toEqual({\n        data: mockFeaturesConfigJson,\n        type: FeatureConfigConfigDestination.Remote,\n      });\n      expect(\n        analytics.sendFeatureFlagInvalidRemoteConfig,\n      ).not.toHaveBeenCalled();\n    });\n    it('should return default config when unable to fetch remote config', async () => {\n      mockedAxios.get.mockRejectedValueOnce(new Error('404 not found'));\n\n      const result = await service['getNewConfig'](mockSessionMetadata);\n\n      expect(result).toEqual({\n        data: defaultConfig,\n        type: FeatureConfigConfigDestination.Default,\n      });\n      expect(analytics.sendFeatureFlagInvalidRemoteConfig).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          configVersion: undefined, // no config version since unable to fetch\n          error: new UnableToFetchRemoteConfigException(),\n        },\n      );\n    });\n    it('should return default config when invalid remote config fetched', async () => {\n      const validateSpy = jest.spyOn(service['validator'], 'validateOrReject');\n      const validationError = new Error('ValidationError');\n      validateSpy.mockRejectedValueOnce([validationError]);\n      mockedAxios.get.mockResolvedValue({\n        data: {\n          ...mockFeaturesConfigJson,\n          features: {\n            [KnownFeatures.InsightsRecommendations]: {\n              ...mockFeaturesConfigJson.features[\n                KnownFeatures.InsightsRecommendations\n              ],\n              flag: 'not boolean flag',\n            },\n          },\n        },\n      });\n\n      const result = await service['getNewConfig'](mockSessionMetadata);\n\n      expect(result).toEqual({\n        data: defaultConfig,\n        type: FeatureConfigConfigDestination.Default,\n      });\n      expect(analytics.sendFeatureFlagInvalidRemoteConfig).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          configVersion: mockFeaturesConfigJson.version, // no config version since unable to fetch\n          error: [validationError],\n        },\n      );\n    });\n    it('should return default config when remote config version less then default', async () => {\n      mockedAxios.get.mockResolvedValue({\n        data: {\n          ...mockFeaturesConfigJson,\n          version: defaultConfig.version - 0.1,\n        },\n      });\n\n      const result = await service['getNewConfig'](mockSessionMetadata);\n\n      expect(result).toEqual({\n        data: defaultConfig,\n        type: FeatureConfigConfigDestination.Default,\n      });\n      expect(\n        analytics.sendFeatureFlagInvalidRemoteConfig,\n      ).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sync', () => {\n    it('should update to the latest remote config', async () => {\n      repository.getOrCreate.mockResolvedValue({\n        ...mockFeaturesConfig,\n        data: plainToInstance(FeaturesConfigData, defaultConfig),\n      });\n\n      await service['sync'](mockSessionMetadata);\n\n      expect(repository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockFeaturesConfigJson,\n      );\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        FeatureServerEvents.FeaturesRecalculate,\n      );\n      expect(analytics.sendFeatureFlagConfigUpdated).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          oldVersion: defaultConfig.version,\n          configVersion: mockFeaturesConfig.data.version,\n          type: FeatureConfigConfigDestination.Remote,\n        },\n      );\n    });\n    it('should not fail and not emit recalculate event in case of an error', async () => {\n      repository.getOrCreate.mockResolvedValue({\n        ...mockFeaturesConfig,\n        data: plainToInstance(FeaturesConfigData, defaultConfig),\n      });\n      repository.update.mockRejectedValueOnce(new Error('update error'));\n\n      await service['sync'](mockSessionMetadata);\n\n      expect(repository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockFeaturesConfigJson,\n      );\n      expect(eventEmitter.emit).not.toHaveBeenCalledWith(\n        FeatureServerEvents.FeaturesRecalculate,\n      );\n      expect(analytics.sendFeatureFlagConfigUpdated).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('getControlInfo', () => {\n    it('should get controlNumber and controlGroup', async () => {\n      repository.getOrCreate.mockResolvedValue(mockFeaturesConfig);\n\n      const result = await service['getControlInfo'](mockSessionMetadata);\n\n      expect(result).toEqual({\n        controlNumber: mockControlNumber,\n        controlGroup: mockControlGroup,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/local.features-config.service.ts",
    "content": "import axios from 'axios';\nimport { Injectable, OnModuleDestroy } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport config from 'src/utils/config';\nimport { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';\nimport {\n  FeatureConfigConfigDestination,\n  FeatureServerEvents,\n} from 'src/modules/feature/constants';\nimport { Validator } from 'class-validator';\nimport { plainToInstance } from 'class-transformer';\nimport { FeaturesConfigData } from 'src/modules/feature/model/features-config';\nimport { FeatureAnalytics } from 'src/modules/feature/feature.analytics';\nimport { UnableToFetchRemoteConfigException } from 'src/modules/feature/exceptions';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { SessionMetadata } from 'src/common/models';\nimport * as defaultConfig from '../../../config/features-config.json';\n\nconst FEATURES_CONFIG = config.get('features_config');\n\n@Injectable()\nexport class LocalFeaturesConfigService\n  extends FeaturesConfigService\n  implements OnModuleDestroy\n{\n  private validator = new Validator();\n\n  private autoSyncTimeout: NodeJS.Timeout;\n\n  constructor(\n    private readonly repository: FeaturesConfigRepository,\n    private readonly eventEmitter: EventEmitter2,\n    private readonly analytics: FeatureAnalytics,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {\n    super();\n  }\n\n  onModuleDestroy() {\n    clearTimeout(this.autoSyncTimeout);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async init() {\n    // todo: [USER_CONTEXT] revise\n    const sessionMetadata = this.constantsProvider.getSystemSessionMetadata();\n    await this.getControlInfo(sessionMetadata); // init default values\n\n    // initialize auto synchronisation\n    this.autoSync(sessionMetadata).catch();\n  }\n\n  /**\n   * Will try to auto update config file each FEATURES_CONFIG.syncInterval\n   * @param sessionMetadata\n   */\n  async autoSync(sessionMetadata: SessionMetadata): Promise<void> {\n    this.sync(sessionMetadata)\n      .catch()\n      .finally(() => {\n        if (FEATURES_CONFIG.syncInterval > 0) {\n          this.autoSyncTimeout = setTimeout(\n            this.autoSync.bind(this, sessionMetadata),\n            FEATURES_CONFIG.syncInterval,\n          );\n        }\n      });\n  }\n\n  /**\n   * Fetch remote new config from remote server\n   * @private\n   */\n  private async fetchRemoteConfig(): Promise<any> {\n    try {\n      this.logger.debug('Fetching remote config...');\n\n      const { data } = await axios.get(FEATURES_CONFIG.url);\n\n      return data;\n    } catch (error) {\n      this.logger.error('Unable to fetch remote config', error);\n      throw new UnableToFetchRemoteConfigException();\n    }\n  }\n\n  private async getNewConfig(sessionMetadata: SessionMetadata): Promise<{\n    data: any;\n    type: FeatureConfigConfigDestination;\n  }> {\n    let remoteConfig: any;\n    let newConfig: any = {\n      data: defaultConfig,\n      type: FeatureConfigConfigDestination.Default,\n    };\n\n    try {\n      this.logger.debug('Fetching remote config...', sessionMetadata);\n\n      remoteConfig = await this.fetchRemoteConfig();\n\n      // we should use default config in case when remote is invalid\n      await this.validator.validateOrReject(\n        plainToInstance(FeaturesConfigData, remoteConfig),\n      );\n\n      if (remoteConfig?.version > defaultConfig?.version) {\n        newConfig = {\n          data: remoteConfig,\n          type: FeatureConfigConfigDestination.Remote,\n        };\n      }\n    } catch (error) {\n      this.analytics.sendFeatureFlagInvalidRemoteConfig(sessionMetadata, {\n        configVersion: remoteConfig?.version,\n        error,\n      });\n\n      this.logger.error(\n        'Something wrong with remote config',\n        error,\n        sessionMetadata,\n      );\n    }\n\n    return newConfig;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async sync(sessionMetadata: SessionMetadata): Promise<void> {\n    let newConfig;\n\n    try {\n      this.logger.debug('Trying to sync features config...', sessionMetadata);\n\n      const currentConfig = await this.repository.getOrCreate(sessionMetadata);\n      newConfig = await this.getNewConfig(sessionMetadata);\n\n      if (newConfig?.data?.version > currentConfig?.data?.version) {\n        await this.repository.update(sessionMetadata, newConfig.data);\n        this.analytics.sendFeatureFlagConfigUpdated(sessionMetadata, {\n          oldVersion: currentConfig?.data?.version,\n          configVersion: newConfig.data.version,\n          type: newConfig.type,\n        });\n      }\n\n      this.logger.debug(\n        'Successfully updated stored remote config',\n        sessionMetadata,\n      );\n      this.eventEmitter.emit(FeatureServerEvents.FeaturesRecalculate);\n    } catch (error) {\n      this.analytics.sendFeatureFlagConfigUpdateError(sessionMetadata, {\n        configVersion: newConfig?.version,\n        error,\n      });\n\n      this.logger.error(\n        'Unable to update features config',\n        error,\n        sessionMetadata,\n      );\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getControlInfo(sessionMetadata: SessionMetadata): Promise<{\n    controlNumber: number;\n    controlGroup: string;\n  }> {\n    this.logger.debug('Trying to get controlGroup field', sessionMetadata);\n\n    const model = await this.repository.getOrCreate(sessionMetadata);\n\n    return {\n      controlNumber: model.controlNumber,\n      controlGroup: parseInt(model.controlNumber.toString(), 10).toFixed(0),\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/model/feature.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class Feature {\n  @Expose()\n  name: string;\n\n  @Expose()\n  flag: boolean;\n\n  @Expose()\n  strategy?: string;\n\n  @Expose()\n  data?: any;\n}\n\nexport class FeaturesFlags {\n  @ApiProperty({\n    description: 'Control number for A/B testing',\n    type: Number,\n  })\n  @Expose()\n  controlNumber: number;\n\n  @ApiProperty({\n    description: 'Control group (bucket)',\n    type: String,\n  })\n  @Expose()\n  controlGroup: string;\n\n  @ApiProperty({\n    description: 'Features map',\n    type: Object,\n    example: {\n      flagName: {\n        name: 'flagName',\n        flag: true,\n        strategy: 'strategyName',\n        data: { any: 'data' },\n      },\n    },\n  })\n  @Expose()\n  features: Record<string, Feature>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/model/features-config.spec.ts",
    "content": "import {\n  mockFeaturesConfig,\n  mockFeaturesConfigComplex,\n  mockFeaturesConfigEntity,\n  mockFeaturesConfigEntityComplex,\n  mockFeaturesConfigJson,\n  mockFeaturesConfigJsonComplex,\n} from 'src/__mocks__';\nimport { instanceToPlain, plainToInstance } from 'class-transformer';\nimport { FeaturesConfig } from 'src/modules/feature/model/features-config';\nimport { classToClass } from 'src/utils';\nimport { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';\n\nconst testCases = [\n  {\n    plain: {\n      ...mockFeaturesConfig,\n      data: { ...mockFeaturesConfigJson },\n    },\n    model: mockFeaturesConfig,\n    entity: Object.assign(new FeaturesConfigEntity(), {\n      ...mockFeaturesConfigEntity,\n      id: undefined,\n    }),\n  },\n  {\n    plain: {\n      ...mockFeaturesConfigComplex,\n      data: { ...mockFeaturesConfigJsonComplex },\n    },\n    model: mockFeaturesConfigComplex,\n    entity: Object.assign(new FeaturesConfigEntity(), {\n      ...mockFeaturesConfigEntityComplex,\n      id: undefined,\n    }),\n  },\n  {\n    plain: {},\n    model: {},\n    entity: {},\n  },\n  {\n    plain: null,\n    model: null,\n    entity: null,\n  },\n  {\n    plain: undefined,\n    model: undefined,\n    entity: undefined,\n  },\n  {\n    plain: 'incorrectdata',\n    model: 'incorrectdata',\n    entity: 'incorrectdata',\n  },\n];\n\ndescribe('FeaturesConfig', () => {\n  describe('transform', () => {\n    testCases.forEach((tc) => {\n      it(`input ${JSON.stringify(tc.plain)}`, async () => {\n        const modelFromPlain = plainToInstance(FeaturesConfig, tc.plain);\n        const plainFromModel = instanceToPlain(modelFromPlain);\n        const entityFromModel = classToClass(\n          FeaturesConfigEntity,\n          modelFromPlain,\n        );\n        const modelFromEntity = classToClass(FeaturesConfig, entityFromModel);\n\n        expect(tc.model).toEqual(modelFromPlain);\n        expect(tc.plain).toEqual(plainFromModel);\n        expect(tc.entity).toEqual(entityFromModel);\n        expect(tc.model).toEqual(modelFromEntity);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/model/features-config.ts",
    "content": "import { Expose, Transform, Type } from 'class-transformer';\nimport {\n  IsArray,\n  IsBoolean,\n  IsEnum,\n  IsNotEmpty,\n  IsNumber,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { IsMultiNumber, ObjectAsMap } from 'src/common/decorators';\nimport { featureConfigFilterTransformer } from 'src/modules/feature/transformers';\n\nexport enum FeatureConfigFilterCondition {\n  Eq = 'eq',\n  Neq = 'neq',\n  Gt = 'gt',\n  Gte = 'gte',\n  Lt = 'lt',\n  Lte = 'lte',\n}\n\nexport type FeatureConfigFilterType =\n  | FeatureConfigFilter\n  | FeatureConfigFilterOr\n  | FeatureConfigFilterAnd;\n\nexport class FeatureConfigFilter {\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  name: string;\n\n  @Expose()\n  @IsEnum(FeatureConfigFilterCondition)\n  cond: FeatureConfigFilterCondition;\n\n  @Expose()\n  value: any;\n}\n\nexport class FeatureConfigFilterOr {\n  @Expose()\n  @IsArray()\n  @Transform(featureConfigFilterTransformer)\n  @ValidateNested({ each: true })\n  or: FeatureConfigFilterType[];\n}\n\nexport class FeatureConfigFilterAnd {\n  @Expose()\n  @IsArray()\n  @Transform(featureConfigFilterTransformer)\n  @ValidateNested({ each: true })\n  and: FeatureConfigFilterType[];\n}\n\nexport class FeatureConfig {\n  @Expose()\n  @IsNotEmpty()\n  @IsBoolean()\n  flag: boolean;\n\n  @Expose()\n  @IsArray({ each: true })\n  @IsMultiNumber()\n  perc: number[][];\n\n  @Expose()\n  @IsArray()\n  @Transform(featureConfigFilterTransformer)\n  @ValidateNested({ each: true })\n  filters: FeatureConfigFilterType[];\n}\n\nexport class FeaturesConfigData {\n  @Expose()\n  @IsNotEmpty()\n  @IsNumber()\n  version: number;\n\n  @Expose()\n  @ObjectAsMap(FeatureConfig)\n  @ValidateNested({ each: true })\n  features: Map<string, FeatureConfig>;\n}\n\nexport class FeaturesConfig {\n  @Expose()\n  @IsNumber()\n  controlNumber: number;\n\n  @Expose()\n  @Type(() => FeaturesConfigData)\n  @ValidateNested()\n  data: FeaturesConfigData;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockFeature,\n  mockFeaturesConfig,\n  mockFeaturesConfigService,\n  mockInsightsRecommendationsFlagStrategy,\n  mockSessionMetadata,\n  mockSettingsService,\n} from 'src/__mocks__';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { FeatureFlagProvider } from 'src/modules/feature/providers/feature-flag/feature-flag.provider';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { CommonFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/common.flag.strategy';\nimport { DefaultFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/default.flag.strategy';\nimport { knownFeatures } from 'src/modules/feature/constants/known-features';\n\ndescribe('FeatureFlagProvider', () => {\n  let service: FeatureFlagProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        FeatureFlagProvider,\n        {\n          provide: FeaturesConfigService,\n          useFactory: mockFeaturesConfigService,\n        },\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(FeatureFlagProvider);\n  });\n\n  describe('getStrategy', () => {\n    it('should return common strategy', async () => {\n      expect(\n        await service.getStrategy(KnownFeatures.InsightsRecommendations),\n      ).toBeInstanceOf(CommonFlagStrategy);\n    });\n    it('should return common strategy', async () => {\n      expect(\n        await service.getStrategy(KnownFeatures.CloudSsoRecommendedSettings),\n      ).toBeInstanceOf(CommonFlagStrategy);\n    });\n    it('should return default strategy when directly called', async () => {\n      expect(await service.getStrategy('default')).toBeInstanceOf(\n        DefaultFlagStrategy,\n      );\n    });\n    it('should return default strategy when when no strategy found', async () => {\n      expect(\n        await service.getStrategy('some not existing strategy'),\n      ).toBeInstanceOf(DefaultFlagStrategy);\n    });\n  });\n\n  describe('calculate', () => {\n    it('should calculate ', async () => {\n      jest\n        .spyOn(service, 'getStrategy')\n        .mockReturnValue(\n          mockInsightsRecommendationsFlagStrategy as unknown as CommonFlagStrategy,\n        );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.InsightsRecommendations],\n          mockFeaturesConfig[KnownFeatures.InsightsRecommendations],\n        ),\n      ).toEqual(mockFeature);\n      expect(\n        mockInsightsRecommendationsFlagStrategy.calculate,\n      ).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        knownFeatures[KnownFeatures.InsightsRecommendations],\n        mockFeaturesConfig[KnownFeatures.InsightsRecommendations],\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { CommonFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/common.flag.strategy';\nimport { DefaultFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/default.flag.strategy';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { IFeatureFlag, KnownFeatures } from 'src/modules/feature/constants';\nimport { CloudSsoFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/cloud-sso.flag.strategy';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { WithDataFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/with-data.flag.strategy';\nimport { SwitchableFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/switchable.flag.strategy';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class FeatureFlagProvider {\n  private strategies: Map<string, FeatureFlagStrategy> = new Map();\n\n  constructor(\n    private readonly featuresConfigService: FeaturesConfigService,\n    private readonly settingsService: SettingsService,\n  ) {\n    this.strategies.set(\n      'default',\n      new DefaultFlagStrategy(this.featuresConfigService, this.settingsService),\n    );\n    this.strategies.set(\n      KnownFeatures.InsightsRecommendations,\n      new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n    );\n    this.strategies.set(\n      KnownFeatures.Rdi,\n      new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n    );\n    this.strategies.set(\n      KnownFeatures.CloudSso,\n      new CloudSsoFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.CloudSsoRecommendedSettings,\n      new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n    );\n    this.strategies.set(\n      KnownFeatures.RedisModuleFilter,\n      new WithDataFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.RedisClient,\n      new WithDataFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.DocumentationChat,\n      new SwitchableFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.DatabaseChat,\n      new SwitchableFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.HashFieldExpiration,\n      new WithDataFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.EnhancedCloudUI,\n      new WithDataFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.VectorSearchV2,\n      new SwitchableFlagStrategy(\n        this.featuresConfigService,\n        this.settingsService,\n      ),\n    );\n    this.strategies.set(\n      KnownFeatures.AzureEntraId,\n      new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n    );\n    this.strategies.set(\n      KnownFeatures.DevBrowser,\n      new CommonFlagStrategy(this.featuresConfigService, this.settingsService),\n    );\n  }\n\n  getStrategy(name: string): FeatureFlagStrategy {\n    return this.strategies.get(name) || this.getStrategy('default');\n  }\n\n  async calculate(\n    sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n    featureConditions: any,\n  ): Promise<Feature> {\n    const strategy = this.getStrategy(knownFeature.name);\n\n    return strategy.calculate(sessionMetadata, knownFeature, featureConditions);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/cloud-sso.flag.strategy.ts",
    "content": "import { Feature } from 'src/modules/feature/model/feature';\nimport { IFeatureFlag } from 'src/modules/feature/constants';\nimport { SwitchableFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/switchable.flag.strategy';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class CloudSsoFlagStrategy extends SwitchableFlagStrategy {\n  async calculate(\n    sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n    featureConfig: any,\n  ): Promise<Feature> {\n    const feature = await super.calculate(\n      sessionMetadata,\n      knownFeature,\n      featureConfig,\n    );\n\n    if (knownFeature.factory) {\n      return {\n        ...feature,\n        ...(await knownFeature.factory()),\n      };\n    }\n\n    return feature;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/common.flag.strategy.ts",
    "content": "import { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { IFeatureFlag } from 'src/modules/feature/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class CommonFlagStrategy extends FeatureFlagStrategy {\n  async calculate(\n    sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n    featureConfig: any,\n  ): Promise<Feature> {\n    const isInRange = await this.isInTargetRange(\n      sessionMetadata,\n      featureConfig?.perc,\n    );\n\n    return {\n      name: knownFeature.name,\n      flag:\n        isInRange && (await this.filter(featureConfig?.filters))\n          ? !!featureConfig?.flag\n          : !featureConfig?.flag,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/default.flag.strategy.ts",
    "content": "import { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { IFeatureFlag } from 'src/modules/feature/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class DefaultFlagStrategy extends FeatureFlagStrategy {\n  async calculate(\n    _sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n  ): Promise<Feature> {\n    return {\n      name: knownFeature.name,\n      flag: false,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAppSettings,\n  mockFeaturesConfig,\n  mockFeaturesConfigDataComplex,\n  mockFeaturesConfigJson,\n  mockFeaturesConfigService,\n  mockServerState,\n  mockSessionMetadata,\n  mockSettingsService,\n  MockType,\n} from 'src/__mocks__';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { CommonFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/common.flag.strategy';\nimport {\n  FeatureConfigFilter,\n  FeatureConfigFilterAnd,\n  FeatureConfigFilterCondition,\n} from 'src/modules/feature/model/features-config';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { DefaultFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/default.flag.strategy';\nimport { knownFeatures } from 'src/modules/feature/constants/known-features';\n\ndescribe('FeatureFlagStrategy', () => {\n  let service: FeatureFlagStrategy;\n  let settingsService: MockType<SettingsService>;\n  let featuresConfigService: MockType<FeaturesConfigService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n        {\n          provide: FeaturesConfigService,\n          useFactory: mockFeaturesConfigService,\n        },\n      ],\n    }).compile();\n\n    settingsService = module.get(SettingsService);\n    featuresConfigService = module.get(FeaturesConfigService);\n    service = new CommonFlagStrategy(\n      featuresConfigService as unknown as FeaturesConfigService,\n      settingsService as unknown as SettingsService,\n    );\n\n    settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n  });\n\n  describe('isInTargetRange', () => {\n    const testCases = [\n      [[], false], // disable for all\n      [[[0, 100]], true],\n      [[[0, 50]], true],\n      [\n        [\n          [0, 1],\n          [2, 3],\n          [5, 10],\n        ],\n        true,\n      ],\n      [[[0, 1]], false],\n      [[[5, -600]], false],\n      [[[100, -600]], false],\n      [[[0, 0]], false],\n      [[[0, mockFeaturesConfig.controlNumber]], false],\n      [[[0, mockFeaturesConfig.controlNumber + 0.01]], true],\n    ];\n\n    testCases.forEach((tc) => {\n      it(`should return ${tc[1]} for range: [${tc[0]}]`, async () => {\n        expect(\n          await service['isInTargetRange'](\n            mockSessionMetadata,\n            tc[0] as number[][],\n          ),\n        ).toEqual(tc[1]);\n      });\n    });\n\n    it('should return false in case of any error', async () => {\n      featuresConfigService.getControlInfo.mockRejectedValueOnce(\n        new Error('unable to get control info'),\n      );\n\n      expect(\n        await service['isInTargetRange'](mockSessionMetadata, [[0, 100]]),\n      ).toEqual(false);\n    });\n  });\n\n  describe('getServerState', () => {\n    it('should return server state', async () => {\n      expect(await service['getServerState']()).toEqual(mockServerState);\n    });\n    it('should return nulls in case of any error', async () => {\n      settingsService.getAppSettings.mockRejectedValueOnce(\n        new Error('unable to get app settings'),\n      );\n\n      expect(await service['getServerState']()).toEqual({\n        ...mockServerState,\n        agreements: null,\n        settings: null,\n      });\n    });\n  });\n\n  describe('filter', () => {\n    it('should return when no filters defined', async () => {\n      expect(await service['filter']([])).toEqual(true);\n    });\n    it('should return true for single filter by agreements (eq)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'agreements.analytics',\n            value: true,\n            cond: FeatureConfigFilterCondition.Eq,\n          }),\n        ]),\n      ).toEqual(true);\n    });\n    it('should return false for single filter by agreements (eq)', async () => {\n      settingsService.getAppSettings.mockResolvedValue({\n        ...mockAppSettings,\n        agreements: {\n          ...mockAppSettings.agreements,\n          analytics: false,\n        },\n      });\n\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'agreements.analytics',\n            value: true,\n            cond: FeatureConfigFilterCondition.Eq,\n          }),\n        ]),\n      ).toEqual(false);\n    });\n    it('should return false for single filter by agreements (neq)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'agreements.analytics',\n            value: true,\n            cond: FeatureConfigFilterCondition.Neq,\n          }),\n        ]),\n      ).toEqual(false);\n    });\n    it('should return false for unsupported condition (unsupported)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'agreements.analytics',\n            value: true,\n            cond: 'unsupported' as FeatureConfigFilterCondition,\n          }),\n        ]),\n      ).toEqual(false);\n    });\n    it('should return false numeric settings (eq)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold,\n            cond: FeatureConfigFilterCondition.Eq,\n          }),\n        ]),\n      ).toEqual(true);\n    });\n    it('should return false for numeric settings (gt)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold,\n            cond: FeatureConfigFilterCondition.Gt,\n          }),\n        ]),\n      ).toEqual(false);\n    });\n    it('should return true for numeric settings (gt)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold - 1,\n            cond: FeatureConfigFilterCondition.Gt,\n          }),\n        ]),\n      ).toEqual(true);\n    });\n    it('should return true numeric settings (gte)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold,\n            cond: FeatureConfigFilterCondition.Gte,\n          }),\n        ]),\n      ).toEqual(true);\n    });\n    it('should return false for numeric settings (lt)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold,\n            cond: FeatureConfigFilterCondition.Lt,\n          }),\n        ]),\n      ).toEqual(false);\n    });\n    it('should return true for numeric settings (lt)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold + 1,\n            cond: FeatureConfigFilterCondition.Lt,\n          }),\n        ]),\n      ).toEqual(true);\n    });\n    it('should return true numeric settings (lte)', async () => {\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'settings.scanThreshold',\n            value: mockAppSettings.scanThreshold,\n            cond: FeatureConfigFilterCondition.Lte,\n          }),\n        ]),\n      ).toEqual(true);\n    });\n\n    it('should return false in case of an error', async () => {\n      const spy = jest.spyOn(service as any, 'getServerState');\n      spy.mockRejectedValueOnce(new Error('unable to get state'));\n\n      expect(\n        await service['filter']([\n          Object.assign(new FeatureConfigFilter(), {\n            name: 'agreements.analytics',\n            value: true,\n            cond: FeatureConfigFilterCondition.Eq,\n          }),\n        ]),\n      ).toEqual(false);\n    });\n  });\n\n  describe('filter (complex)', () => {\n    it('should return true since 2nd \"or\" condition is true', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        ...mockAppSettings,\n        agreements: { analytics: true },\n      });\n\n      expect(\n        await service['filter'](\n          mockFeaturesConfigDataComplex.features.get(\n            KnownFeatures.InsightsRecommendations,\n          ).filters,\n        ),\n      ).toEqual(true);\n    });\n    it('should return false since 2nd \"or\" condition is false due to \"and\" inside is false', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        ...mockAppSettings,\n        agreements: { analytics: true },\n        scanThreshold: mockAppSettings.scanThreshold + 1,\n        batchSize: mockAppSettings.batchSize + 1,\n      });\n\n      expect(\n        await service['filter'](\n          mockFeaturesConfigDataComplex.features.get(\n            KnownFeatures.InsightsRecommendations,\n          ).filters,\n        ),\n      ).toEqual(false);\n    });\n    it('should return true since 2nd \"or\" condition is true due to \"or\" inside is true', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        ...mockAppSettings,\n        agreements: { analytics: true },\n        scanThreshold: mockAppSettings.scanThreshold + 1,\n      });\n\n      expect(\n        await service['filter'](\n          mockFeaturesConfigDataComplex.features.get(\n            KnownFeatures.InsightsRecommendations,\n          ).filters,\n        ),\n      ).toEqual(true);\n    });\n    it('should return false since all 2 or conditions are false', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        ...mockAppSettings,\n        agreements: { analytics: false },\n      });\n\n      expect(\n        await service['filter'](\n          mockFeaturesConfigDataComplex.features.get(\n            KnownFeatures.InsightsRecommendations,\n          ).filters,\n        ),\n      ).toEqual(false);\n    });\n    it('should return true since 1st \"or\" condition is true', async () => {\n      settingsService.getAppSettings.mockResolvedValueOnce({\n        ...mockAppSettings,\n        testValue: 'test',\n        agreements: { analytics: false },\n      });\n\n      expect(\n        await service['filter'](\n          mockFeaturesConfigDataComplex.features.get(\n            KnownFeatures.InsightsRecommendations,\n          ).filters,\n        ),\n      ).toEqual(true);\n    });\n  });\n\n  describe('checkFilter', () => {\n    it('should return false in case of any error', async () => {\n      const spy = jest.spyOn(service as any, 'checkAndFilters');\n      spy.mockImplementationOnce(() => {\n        throw new Error('some error on \"and\" filters');\n      });\n      expect(\n        await service['checkFilter'](\n          Object.assign(new FeatureConfigFilterAnd(), {}),\n          {},\n        ),\n      ).toEqual(false);\n    });\n  });\n\n  describe('checkAndFilters', () => {\n    let checkFilterSpy;\n    beforeEach(() => {\n      checkFilterSpy = jest.spyOn(service as any, 'checkFilter');\n    });\n\n    it('should return true since all filters returned true', async () => {\n      checkFilterSpy.mockReturnValueOnce(true);\n      checkFilterSpy.mockReturnValueOnce(true);\n      checkFilterSpy.mockReturnValueOnce(true);\n\n      expect(\n        await service['checkAndFilters'](\n          new Array(3).fill(\n            mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ].filters[0],\n          ),\n          {},\n        ),\n      ).toEqual(true);\n    });\n\n    it('should return false since at least one filter returned false', async () => {\n      checkFilterSpy.mockReturnValueOnce(true);\n      checkFilterSpy.mockReturnValueOnce(false);\n      checkFilterSpy.mockReturnValueOnce(true);\n\n      expect(\n        await service['checkAndFilters'](\n          new Array(3).fill(\n            mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ].filters[0],\n          ),\n          {},\n        ),\n      ).toEqual(false);\n    });\n\n    it('should return false due to error', async () => {\n      checkFilterSpy.mockImplementation(() => {\n        throw new Error('error when check filters');\n      });\n\n      expect(\n        await service['checkAndFilters'](\n          new Array(3).fill(\n            mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ].filters[0],\n          ),\n          {},\n        ),\n      ).toEqual(false);\n    });\n  });\n\n  describe('checkOrFilters', () => {\n    let checkFilterSpy;\n    beforeEach(() => {\n      checkFilterSpy = jest.spyOn(service as any, 'checkFilter');\n    });\n\n    it('should return true since at least one filter returned true', async () => {\n      checkFilterSpy.mockReturnValueOnce(false);\n      checkFilterSpy.mockReturnValueOnce(true);\n      checkFilterSpy.mockReturnValueOnce(false);\n\n      expect(\n        await service['checkOrFilters'](\n          new Array(3).fill(\n            mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ].filters[0],\n          ),\n          {},\n        ),\n      ).toEqual(true);\n    });\n\n    it('should return false since all filters returned false', async () => {\n      checkFilterSpy.mockReturnValueOnce(false);\n      checkFilterSpy.mockReturnValueOnce(false);\n      checkFilterSpy.mockReturnValueOnce(false);\n\n      expect(\n        await service['checkOrFilters'](\n          new Array(3).fill(\n            mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ].filters[0],\n          ),\n          {},\n        ),\n      ).toEqual(false);\n    });\n\n    it('should return false due to error', async () => {\n      checkFilterSpy.mockImplementation(() => {\n        throw new Error('error when check filters');\n      });\n\n      expect(\n        await service['checkOrFilters'](\n          new Array(3).fill(\n            mockFeaturesConfigJson.features[\n              KnownFeatures.InsightsRecommendations\n            ].filters[0],\n          ),\n          {},\n        ),\n      ).toEqual(false);\n    });\n  });\n\n  describe('calculate', () => {\n    let isInTargetRangeSpy;\n    let filterSpy;\n\n    beforeEach(() => {\n      isInTargetRangeSpy = jest.spyOn(service as any, 'isInTargetRange');\n      filterSpy = jest.spyOn(service as any, 'filter');\n    });\n\n    it('should return false since feature control number is out of range', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(false);\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.InsightsRecommendations],\n          mockFeaturesConfigJson.features[\n            KnownFeatures.InsightsRecommendations\n          ],\n        ),\n      ).toEqual({\n        name: KnownFeatures.InsightsRecommendations,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations]\n          .perc,\n      );\n      expect(filterSpy).not.toHaveBeenCalled();\n    });\n\n    it('should return false since feature filters does not match', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(false);\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.InsightsRecommendations],\n          mockFeaturesConfigJson.features[\n            KnownFeatures.InsightsRecommendations\n          ],\n        ),\n      ).toEqual({\n        name: KnownFeatures.InsightsRecommendations,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations]\n          .perc,\n      );\n      expect(filterSpy).toHaveBeenCalledWith(\n        mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations]\n          .filters,\n      );\n    });\n    it('should return true since all checks passes', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(true);\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.InsightsRecommendations],\n          mockFeaturesConfigJson.features[\n            KnownFeatures.InsightsRecommendations\n          ],\n        ),\n      ).toEqual({\n        name: KnownFeatures.InsightsRecommendations,\n        flag: true,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations]\n          .perc,\n      );\n      expect(filterSpy).toHaveBeenCalledWith(\n        mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations]\n          .filters,\n      );\n    });\n  });\n\n  describe('DefaultFlagStrategy', () => {\n    it('should always return false', async () => {\n      const strategy = new DefaultFlagStrategy(\n        featuresConfigService as unknown as FeaturesConfigService,\n        settingsService as unknown as SettingsService,\n      );\n\n      expect(\n        await strategy.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.InsightsRecommendations],\n        ),\n      ).toEqual({\n        name: KnownFeatures.InsightsRecommendations,\n        flag: false,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy.ts",
    "content": "import * as fs from 'fs-extra';\nimport { get } from 'lodash';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport {\n  FeatureConfigFilter,\n  FeatureConfigFilterAnd,\n  FeatureConfigFilterCondition,\n  FeatureConfigFilterOr,\n  FeatureConfigFilterType,\n} from 'src/modules/feature/model/features-config';\nimport config, { Config } from 'src/utils/config';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { IFeatureFlag } from 'src/modules/feature/constants';\nimport { SessionMetadata } from 'src/common/models';\nimport { filterVersion } from 'src/utils/feature-version-filter.helper';\nimport {\n  DEFAULT_ACCOUNT_ID,\n  DEFAULT_SESSION_ID,\n  DEFAULT_USER_ID,\n} from 'src/common/constants';\n\nconst PATH_CONFIG = config.get('dir_path') as Config['dir_path'];\n\nexport abstract class FeatureFlagStrategy {\n  constructor(\n    protected readonly featuresConfigService: FeaturesConfigService,\n    protected readonly settingsService: SettingsService,\n  ) {}\n\n  abstract calculate(\n    sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n    data: any,\n  ): Promise<Feature>;\n\n  static async getCustomConfig(): Promise<object> {\n    try {\n      const customConfig = JSON.parse(\n        await fs.readFile(PATH_CONFIG.customConfig, 'utf8'),\n      );\n      return customConfig?.features || {};\n    } catch (e) {\n      return {};\n    }\n  }\n\n  /**\n   * Check if controlNumber is in defined range\n   * Should return false in case of any error\n   * @param sessionMetadata\n   * @param perc\n   * @protected\n   */\n  protected async isInTargetRange(\n    sessionMetadata: SessionMetadata,\n    perc: number[][] = [[-1]],\n  ): Promise<boolean> {\n    try {\n      const { controlNumber } =\n        await this.featuresConfigService.getControlInfo(sessionMetadata);\n\n      return !!perc.find(\n        (range) => controlNumber >= range[0] && controlNumber < range[1],\n      );\n    } catch (e) {\n      return false;\n    }\n  }\n\n  protected async getServerState(): Promise<object> {\n    const state: any = {\n      config: config.get(),\n      env: process.env,\n      agreements: null,\n      settings: null,\n    };\n\n    // determine agreements and settings\n    try {\n      // todo: [USER_CONTEXT] temporary workaround\n      const appSettings = await this.settingsService\n        .getAppSettings({\n          userId: DEFAULT_USER_ID,\n          accountId: DEFAULT_ACCOUNT_ID,\n          sessionId: DEFAULT_SESSION_ID,\n        })\n        .catch(null);\n\n      state.agreements = appSettings?.agreements;\n      state.settings = appSettings;\n    } catch (e) {\n      // silently ignore error\n    }\n    return state;\n  }\n\n  /**\n   * Check all filters (starting from \"AND\" since { filters: [] } equal to filters: [{ and: []}])\n   * @param filters\n   * @protected\n   */\n  protected async filter(filters: FeatureConfigFilterType[]): Promise<boolean> {\n    try {\n      const serverState = await this.getServerState();\n      return this.checkAndFilters(filters, serverState);\n    } catch (e) {\n      return false;\n    }\n  }\n\n  /**\n   * Check all feature filters with recursion\n   * @param filter\n   * @param serverState\n   * @private\n   */\n  private checkFilter(\n    filter: FeatureConfigFilterType,\n    serverState: object,\n  ): boolean {\n    try {\n      if (filter instanceof FeatureConfigFilterAnd) {\n        return this.checkAndFilters(filter.and, serverState);\n      }\n\n      if (filter instanceof FeatureConfigFilterOr) {\n        return this.checkOrFilters(filter.or, serverState);\n      }\n\n      if (filter instanceof FeatureConfigFilter) {\n        const value = get(serverState, filter?.name);\n\n        if (filter?.name.match(/version/i)) {\n          return filterVersion(filter.cond, value, filter?.value);\n        }\n\n        switch (filter?.cond) {\n          case FeatureConfigFilterCondition.Eq:\n            return value === filter?.value;\n          case FeatureConfigFilterCondition.Neq:\n            return value !== filter?.value;\n          case FeatureConfigFilterCondition.Gt:\n            return value > filter?.value;\n          case FeatureConfigFilterCondition.Gte:\n            return value >= filter?.value;\n          case FeatureConfigFilterCondition.Lt:\n            return value < filter?.value;\n          case FeatureConfigFilterCondition.Lte:\n            return value <= filter?.value;\n          default:\n            return false;\n        }\n      }\n    } catch (e) {\n      // ignore error\n    }\n\n    return false;\n  }\n\n  /**\n   * Process \"AND\" filter when all of conditions (including in deep nested OR or AND) should pass\n   * @param filters\n   * @param serverState\n   * @private\n   */\n  private checkAndFilters(\n    filters: FeatureConfigFilterType[],\n    serverState: object,\n  ): boolean {\n    try {\n      return !!filters.every((filter) => this.checkFilter(filter, serverState));\n    } catch (e) {\n      return false;\n    }\n  }\n\n  /**\n   * Process \"OR\" conditions when at least one condition should pass\n   * @param filters\n   * @param serverState\n   * @private\n   */\n  private checkOrFilters(\n    filters: FeatureConfigFilterType[],\n    serverState: object,\n  ): boolean {\n    try {\n      return !!filters.some((filter) => this.checkFilter(filter, serverState));\n    } catch (e) {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/switchable.flag.strategy.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockAppSettings,\n  mockFeaturesConfigService,\n  mockSessionMetadata,\n  mockSettingsService,\n  MockType,\n} from 'src/__mocks__';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { knownFeatures } from 'src/modules/feature/constants/known-features';\nimport { SwitchableFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/switchable.flag.strategy';\nimport * as fs from 'fs-extra';\n\njest.mock('fs-extra');\nconst mockedFs = fs as jest.Mocked<typeof fs>;\n\ndescribe('SwitchableFlagStrategy', () => {\n  let service: SwitchableFlagStrategy;\n  let settingsService: MockType<SettingsService>;\n  let featuresConfigService: MockType<FeaturesConfigService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('fs-extra', () => mockedFs);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: SettingsService,\n          useFactory: mockSettingsService,\n        },\n        {\n          provide: FeaturesConfigService,\n          useFactory: mockFeaturesConfigService,\n        },\n      ],\n    }).compile();\n\n    settingsService = module.get(SettingsService);\n    featuresConfigService = module.get(FeaturesConfigService);\n    service = new SwitchableFlagStrategy(\n      featuresConfigService as unknown as FeaturesConfigService,\n      settingsService as unknown as SettingsService,\n    );\n\n    settingsService.getAppSettings.mockResolvedValue(mockAppSettings);\n  });\n\n  describe('calculate', () => {\n    let isInTargetRangeSpy;\n    let filterSpy;\n\n    beforeEach(() => {\n      isInTargetRangeSpy = jest.spyOn(service as any, 'isInTargetRange');\n      filterSpy = jest.spyOn(service as any, 'filter');\n    });\n\n    it('should not fail when no featureConfig provided', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(false);\n      filterSpy.mockReturnValueOnce(false);\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          null,\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: expect.any(Boolean),\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n\n    it('should return flag:true when no force flag defined (default behavior)', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(true);\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: true,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n    it('should return flag:true when unexpected force flag defined', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(true);\n      mockedFs.readFile.mockResolvedValueOnce(\n        JSON.stringify({\n          features: {\n            [KnownFeatures.DatabaseChat]: 'something',\n          },\n        }) as any,\n      );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: true,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n    it('should return flag:false when feature is force disabled', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(true);\n      mockedFs.readFile.mockResolvedValueOnce(\n        JSON.stringify({\n          features: {\n            [KnownFeatures.DatabaseChat]: false,\n          },\n        }) as any,\n      );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n\n    it('should return flag:false when no force flag defined (default behavior)', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(false);\n      filterSpy.mockReturnValueOnce(true);\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n    it('should return flag:false when unexpected force flag defined', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(false);\n      filterSpy.mockReturnValueOnce(true);\n      mockedFs.readFile.mockResolvedValueOnce(\n        JSON.stringify({\n          feature: {\n            [KnownFeatures.DatabaseChat]: 'unexpected value',\n          },\n        }) as any,\n      );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n    it('should return flag:true when feature force enabled', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(false);\n      filterSpy.mockReturnValueOnce(true);\n      mockedFs.readFile.mockResolvedValueOnce(\n        JSON.stringify({\n          features: {\n            [KnownFeatures.DatabaseChat]: true,\n          },\n        }) as any,\n      );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: true,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n\n    it('should return flag:false even if feature forced enabled but filter returned false', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(false);\n      mockedFs.readFile.mockResolvedValueOnce(\n        JSON.stringify({\n          features: {\n            [KnownFeatures.DatabaseChat]: true,\n          },\n        }) as any,\n      );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 10]],\n            flag: true,\n            filters: [],\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n\n    it('should return flag:false even if feature force enabled but flag in config = false', async () => {\n      isInTargetRangeSpy.mockReturnValueOnce(true);\n      filterSpy.mockReturnValueOnce(true);\n      mockedFs.readFile.mockResolvedValueOnce(\n        JSON.stringify({\n          features: {\n            [KnownFeatures.DatabaseChat]: true,\n          },\n        }) as any,\n      );\n\n      expect(\n        await service.calculate(\n          mockSessionMetadata,\n          knownFeatures[KnownFeatures.DatabaseChat],\n          {\n            perc: [[0, 100]],\n            flag: false,\n          },\n        ),\n      ).toEqual({\n        name: KnownFeatures.DatabaseChat,\n        flag: false,\n      });\n\n      expect(isInTargetRangeSpy).toHaveBeenCalled();\n      expect(filterSpy).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/switchable.flag.strategy.ts",
    "content": "import { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { IFeatureFlag } from 'src/modules/feature/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class SwitchableFlagStrategy extends FeatureFlagStrategy {\n  async calculate(\n    sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n    featureConfig: any,\n  ): Promise<Feature> {\n    const isInRange = await this.isInTargetRange(\n      sessionMetadata,\n      featureConfig?.perc,\n    );\n    const isInFilter = await this.filter(featureConfig?.filters);\n    const originalFlag = !!featureConfig?.flag;\n\n    let flag = isInRange && isInFilter ? originalFlag : !originalFlag;\n\n    const force = (await FeatureFlagStrategy.getCustomConfig())?.[\n      knownFeature.name\n    ];\n\n    if (isInFilter && originalFlag && force === true) {\n      flag = true;\n    } else if (isInFilter && originalFlag && force === false) {\n      flag = false;\n    }\n\n    return {\n      name: knownFeature.name,\n      flag,\n      data: featureConfig?.data,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/providers/feature-flag/strategies/with-data.flag.strategy.ts",
    "content": "import { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';\nimport { Feature } from 'src/modules/feature/model/feature';\nimport { IFeatureFlag } from 'src/modules/feature/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class WithDataFlagStrategy extends FeatureFlagStrategy {\n  async calculate(\n    sessionMetadata: SessionMetadata,\n    knownFeature: IFeatureFlag,\n    featureConfig: any,\n  ): Promise<Feature> {\n    const isInRange = await this.isInTargetRange(\n      sessionMetadata,\n      featureConfig?.perc,\n    );\n\n    return {\n      name: knownFeature.name,\n      flag:\n        isInRange && (await this.filter(featureConfig?.filters))\n          ? !!featureConfig?.flag\n          : !featureConfig?.flag,\n      data: featureConfig?.data,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/repositories/feature.repository.ts",
    "content": "import { Feature } from 'src/modules/feature/model/feature';\nimport { SessionMetadata } from 'src/common/models';\n\nexport abstract class FeatureRepository {\n  abstract get(\n    sessionMetadata: SessionMetadata,\n    name: string,\n  ): Promise<Feature>;\n  abstract upsert(\n    sessionMetadata: SessionMetadata,\n    feature: Feature,\n  ): Promise<Feature>;\n  abstract list(sessionMetadata: SessionMetadata): Promise<Feature[]>;\n  abstract delete(\n    sessionMetadata: SessionMetadata,\n    name: string,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/repositories/features-config.repository.ts",
    "content": "import { FeaturesConfig } from 'src/modules/feature/model/features-config';\nimport { SessionMetadata } from 'src/common/models';\n\nexport abstract class FeaturesConfigRepository {\n  abstract getOrCreate(\n    sessionMetadata: SessionMetadata,\n  ): Promise<FeaturesConfig>;\n  abstract update(\n    sessionMetadata: SessionMetadata,\n    config: Record<string, any>,\n  ): Promise<FeaturesConfig>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/repositories/local.feature.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockFeature,\n  mockFeatureEntity,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalFeatureRepository } from 'src/modules/feature/repositories/local.feature.repository';\nimport { FeatureEntity } from 'src/modules/feature/entities/feature.entity';\n\ndescribe('LocalFeatureRepository', () => {\n  let service: LocalFeatureRepository;\n  let repository: MockType<Repository<FeatureEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalFeatureRepository,\n        {\n          provide: getRepositoryToken(FeatureEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(FeatureEntity));\n    service = await module.get(LocalFeatureRepository);\n\n    repository.findOneBy.mockResolvedValue(mockFeatureEntity);\n    repository.find.mockResolvedValue([\n      mockFeatureEntity,\n      mockFeatureEntity,\n      mockFeatureEntity,\n    ]);\n    repository.upsert.mockResolvedValue({ updated: 1, inserted: 0 });\n    repository.delete.mockResolvedValue({ deleted: 1 });\n  });\n\n  describe('get', () => {\n    it('should return feature by name', async () => {\n      const result = await service.get(mockSessionMetadata, mockFeature.name);\n\n      expect(result).toEqual(mockFeature);\n    });\n    it('should return null when entity not found', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.get(mockSessionMetadata, mockFeature.name);\n\n      expect(result).toEqual(null);\n    });\n  });\n\n  describe('list', () => {\n    it('should return features', async () => {\n      const result = await service.list();\n\n      expect(result).toEqual([mockFeature, mockFeature, mockFeature]);\n    });\n    it('should return empty list', async () => {\n      repository.find.mockResolvedValueOnce([]);\n\n      const result = await service.list();\n\n      expect(result).toEqual([]);\n    });\n  });\n\n  describe('upsert', () => {\n    it('should update or insert and return model', async () => {\n      const result = await service.upsert(mockSessionMetadata, mockFeature);\n\n      expect(result).toEqual(mockFeature);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete and do not return anything', async () => {\n      const result = await service.delete(\n        mockSessionMetadata,\n        mockFeature.name,\n      );\n\n      expect(result).toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/repositories/local.feature.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { classToClass } from 'src/utils';\nimport { SessionMetadata } from 'src/common/models';\nimport { FeatureRepository } from './feature.repository';\nimport { FeatureEntity } from '../entities/feature.entity';\nimport { Feature } from '../model/feature';\n\n@Injectable()\nexport class LocalFeatureRepository extends FeatureRepository {\n  constructor(\n    @InjectRepository(FeatureEntity)\n    private readonly repository: Repository<FeatureEntity>,\n  ) {\n    super();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async get(_sessionMetadata: SessionMetadata, name: string): Promise<Feature> {\n    const entity = await this.repository.findOneBy({ name });\n    return classToClass(Feature, entity);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async list(): Promise<Feature[]> {\n    return (await this.repository.find()).map((entity) =>\n      classToClass(Feature, entity),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async upsert(\n    sessionMetadata: SessionMetadata,\n    feature: Feature,\n  ): Promise<Feature> {\n    await this.repository.upsert(classToClass(FeatureEntity, feature), {\n      skipUpdateIfNoValuesChanged: true,\n      conflictPaths: ['name'],\n    });\n\n    return this.get(sessionMetadata, feature.name);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async delete(_sessionMetadata: SessionMetadata, name: string): Promise<void> {\n    await this.repository.delete({ name });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/repositories/local.features-config.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockFeaturesConfig,\n  mockFeaturesConfigEntity,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalFeaturesConfigRepository } from 'src/modules/feature/repositories/local.features-config.repository';\nimport { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';\nimport { plainToInstance } from 'class-transformer';\nimport * as defaultConfig from '../../../../config/features-config.json';\n\ndescribe('LocalFeaturesConfigRepository', () => {\n  let service: LocalFeaturesConfigRepository;\n  let repository: MockType<Repository<FeaturesConfigEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalFeaturesConfigRepository,\n        {\n          provide: getRepositoryToken(FeaturesConfigEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(FeaturesConfigEntity));\n    service = await module.get(LocalFeaturesConfigRepository);\n\n    repository.findOneBy.mockResolvedValue(mockFeaturesConfigEntity);\n    repository.update.mockResolvedValue({ updated: 1 });\n    repository.save.mockResolvedValue(mockFeaturesConfigEntity);\n  });\n\n  describe('generateControlNumber', () => {\n    const step = 10;\n    const iterations = 10_000;\n    const delta = 100;\n\n    it('check controlNumber generation', async () => {\n      const result = {};\n\n      for (let i = 0; i < 100; i += step) {\n        result[`${i} - ${i + step}`] = 0;\n      }\n\n      new Array(iterations).fill(1).forEach(() => {\n        const controlNumber = service['generateControlNumber']();\n\n        expect(controlNumber).toBeGreaterThanOrEqual(0);\n        expect(controlNumber).toBeLessThan(100);\n\n        for (let j = 0; j < 100; j += step) {\n          if (controlNumber <= j + step) {\n            result[`${j} - ${j + step}`] += 1;\n            break;\n          }\n        }\n      });\n\n      const amountPerGroup = iterations / step;\n\n      Object.entries(result).forEach(([, value]) => {\n        expect(value).toBeGreaterThan(amountPerGroup - delta);\n        expect(value).toBeLessThan(amountPerGroup + delta);\n      });\n    });\n  });\n\n  describe('getOrCreate', () => {\n    it('should return existing config', async () => {\n      const result = await service.getOrCreate();\n\n      expect(result).toEqual(mockFeaturesConfig);\n    });\n    it('should create new config', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.getOrCreate();\n\n      expect(result).toEqual(mockFeaturesConfig);\n    });\n    it('should fail to create with unique constraint and return existing', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.findOneBy.mockResolvedValueOnce(mockFeaturesConfig);\n      repository.save.mockRejectedValueOnce({ code: 'SQLITE_CONSTRAINT' });\n\n      const result = await service.getOrCreate();\n\n      expect(result).toEqual(mockFeaturesConfig);\n    });\n    it('should fail when failed to create new and error is not unique constraint', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.save.mockRejectedValueOnce(new Error());\n\n      await expect(service.getOrCreate()).rejects.toThrow(Error);\n    });\n  });\n\n  describe('update', () => {\n    it('should update config', async () => {\n      const result = await service.update(mockSessionMetadata, defaultConfig);\n\n      expect(result).toEqual(mockFeaturesConfig);\n      expect(repository.update).toHaveBeenCalledWith(\n        { id: service['id'] },\n        plainToInstance(FeaturesConfigEntity, {\n          id: service['id'],\n          data: defaultConfig,\n        }),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/repositories/local.features-config.repository.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { plainToInstance } from 'class-transformer';\nimport { classToClass } from 'src/utils';\nimport { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';\nimport { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';\nimport { FeaturesConfig } from 'src/modules/feature/model/features-config';\nimport { SessionMetadata } from 'src/common/models';\nimport * as defaultConfig from '../../../../config/features-config.json';\n\n@Injectable()\nexport class LocalFeaturesConfigRepository extends FeaturesConfigRepository {\n  private readonly logger = new Logger('LocalFeaturesConfigRepository');\n\n  private readonly id = '1';\n\n  constructor(\n    @InjectRepository(FeaturesConfigEntity)\n    private readonly repository: Repository<FeaturesConfigEntity>,\n  ) {\n    super();\n  }\n\n  /**\n   * Generate control number which should never be updated\n   * @private\n   */\n  private generateControlNumber(): number {\n    const controlNumber = Number(\n      (parseInt((Math.random() * 10_000).toString(), 10) / 100).toFixed(2),\n    );\n    this.logger.debug(`Control number is generated: ${controlNumber}`);\n\n    return controlNumber;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getOrCreate(): Promise<FeaturesConfig> {\n    this.logger.debug('Getting features config entity');\n\n    let entity = await this.repository.findOneBy({ id: this.id });\n\n    if (!entity) {\n      try {\n        this.logger.debug('Creating features config entity');\n\n        entity = await this.repository.save(\n          plainToInstance(FeaturesConfigEntity, {\n            id: this.id,\n            data: defaultConfig,\n            controlNumber: this.generateControlNumber(),\n          }),\n        );\n      } catch (e) {\n        if (e.code === 'SQLITE_CONSTRAINT') {\n          return this.getOrCreate();\n        }\n\n        throw e;\n      }\n    }\n\n    return classToClass(FeaturesConfig, entity);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async update(\n    _sessionMetadata: SessionMetadata,\n    data: Record<string, any>,\n  ): Promise<FeaturesConfig> {\n    await this.repository.update(\n      { id: this.id },\n      plainToInstance(FeaturesConfigEntity, { data, id: this.id }),\n    );\n\n    return this.getOrCreate();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/transformers/feature-config-filter.transformer.ts",
    "content": "import { get, map } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  FeatureConfigFilter,\n  FeatureConfigFilterAnd,\n  FeatureConfigFilterOr,\n} from 'src/modules/feature/model/features-config';\n\nexport const featureConfigFilterTransformer = ({ value, options }) =>\n  map(value || [], (filter) => {\n    let cls: any = FeatureConfigFilter;\n\n    if (get(filter, 'and')) {\n      cls = FeatureConfigFilterAnd;\n    }\n\n    if (get(filter, 'or')) {\n      cls = FeatureConfigFilterOr;\n    }\n\n    return plainToInstance(cls, filter, options);\n  });\n"
  },
  {
    "path": "redisinsight/api/src/modules/feature/transformers/index.ts",
    "content": "export * from './feature-config-filter.transformer';\n"
  },
  {
    "path": "redisinsight/api/src/modules/init/init.module.ts",
    "content": "import { DynamicModule, Module, Type } from '@nestjs/common';\nimport { LocalInitService } from 'src/modules/init/local.init.service';\nimport { InitService } from 'src/modules/init/init.service';\n\n@Module({})\nexport class InitModule {\n  static register(\n    imports = [],\n    initService: Type<InitService> = LocalInitService,\n  ): DynamicModule {\n    return {\n      module: InitModule,\n      imports,\n      providers: [\n        {\n          provide: InitService,\n          useClass: initService,\n        },\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/init/init.service.ts",
    "content": "import { Injectable, OnModuleInit } from '@nestjs/common';\n\n@Injectable()\nexport abstract class InitService implements OnModuleInit {\n  abstract onModuleInit(): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/init/local.init.service.ts",
    "content": "import { InitService } from 'src/modules/init/init.service';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { Injectable } from '@nestjs/common';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { AnalyticsService } from 'src/modules/analytics/analytics.service';\nimport { DatabaseDiscoveryService } from 'src/modules/database-discovery/database-discovery.service';\n\n@Injectable()\nexport class LocalInitService extends InitService {\n  constructor(\n    private readonly constantsProvider: ConstantsProvider,\n    private readonly serverService: ServerService,\n    private readonly featuresConfigService: FeaturesConfigService,\n    private readonly featureService: FeatureService,\n    private readonly redisClientFactory: RedisClientFactory,\n    private readonly databaseDiscoveryService: DatabaseDiscoveryService,\n    private readonly analyticsService: AnalyticsService,\n  ) {\n    super();\n  }\n\n  /**\n   * Initialize everything is needed in proper order\n   */\n  async onModuleInit(): Promise<void> {\n    const sessionMetadata = this.constantsProvider.getSystemSessionMetadata();\n    const firstStart = await this.serverService.init();\n    await this.featuresConfigService.init();\n    await this.initAnalytics(firstStart);\n    await this.featureService.recalculateFeatureFlags(sessionMetadata);\n    await this.redisClientFactory.init();\n    await this.databaseDiscoveryService.discover(sessionMetadata, firstStart);\n  }\n\n  async initAnalytics(firstStart: boolean) {\n    const sessionMetadata = this.constantsProvider.getSystemSessionMetadata();\n    const { id, sessionId, appType, appVersion } =\n      await this.serverService.getInfo(sessionMetadata);\n\n    await this.analyticsService.init({\n      anonymousId: id,\n      sessionId,\n      appType,\n      appVersion,\n      ...(await this.featuresConfigService.getControlInfo(sessionMetadata)),\n      firstStart,\n      sessionMetadata,\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/constants/index.ts",
    "content": "export enum NotificationType {\n  Global = 'global',\n}\n\nexport enum NotificationServerEvents {\n  Notification = 'notification',\n}\n\nexport enum NotificationEvents {\n  NewNotifications = 'new-notifications',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/dto/create-notification.dto.ts",
    "content": "import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';\n\nexport class CreateNotificationDto {\n  @IsNotEmpty()\n  @IsInt()\n  timestamp: number;\n\n  @IsNotEmpty()\n  @IsString()\n  title: string;\n\n  @IsOptional()\n  @IsString()\n  category?: string;\n\n  @IsOptional()\n  @IsString()\n  categoryColor?: string;\n\n  @IsNotEmpty()\n  @IsString()\n  body: string;\n\n  constructor(dto: Partial<CreateNotificationDto>) {\n    Object.assign(this, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/dto/create-notifications.dto.ts",
    "content": "import { IsArray, ValidateNested } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { CreateNotificationDto } from 'src/modules/notification/dto/create-notification.dto';\n\nexport class CreateNotificationsDto {\n  @IsArray()\n  @ValidateNested({ each: true })\n  @Type(() => CreateNotificationDto)\n  notifications: CreateNotificationDto[];\n\n  constructor(dto: Partial<CreateNotificationsDto>) {\n    Object.assign(this, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/dto/index.ts",
    "content": "export * from './notifications.dto';\nexport * from './create-notification.dto';\nexport * from './create-notifications.dto';\nexport * from './read-notifications.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/dto/notifications.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Notification } from 'src/modules/notification/models/notification';\n\nexport class NotificationsDto {\n  @ApiProperty({\n    type: () => Notification,\n    isArray: true,\n    description: 'Ordered notifications list',\n  })\n  notifications: Notification[];\n\n  @ApiProperty({\n    type: Number,\n    example: 2,\n    description: 'Number of unread notifications',\n  })\n  totalUnread: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/dto/read-notifications.dto.ts",
    "content": "import { IsEnum, IsInt, NotEquals, ValidateIf } from 'class-validator';\nimport { NotificationType } from 'src/modules/notification/constants';\nimport { ApiPropertyOptional } from '@nestjs/swagger';\n\nexport class ReadNotificationsDto {\n  @ApiPropertyOptional({\n    type: Number,\n    example: 1655738357,\n    description: 'Timestamp of notification',\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt()\n  timestamp?: number;\n\n  @ApiPropertyOptional({\n    enum: NotificationType,\n    example: NotificationType.Global,\n    description: 'Type of notification',\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsEnum(NotificationType)\n  type?: NotificationType;\n\n  constructor(dto: Partial<ReadNotificationsDto>) {\n    Object.assign(this, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/entities/notification.entity.ts",
    "content": "import { Column, Entity, PrimaryColumn } from 'typeorm';\nimport { NotificationType } from 'src/modules/notification/constants';\nimport { Expose } from 'class-transformer';\n\n@Entity('notification')\nexport class NotificationEntity {\n  @Expose()\n  @PrimaryColumn({ nullable: false, type: 'varchar', enum: NotificationType })\n  type: NotificationType;\n\n  @Expose()\n  @PrimaryColumn({ nullable: false })\n  timestamp: number;\n\n  @Expose()\n  @Column({ nullable: false })\n  title: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  category?: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  categoryColor?: string;\n\n  @Expose()\n  @Column({ nullable: false, type: 'text' })\n  body: string;\n\n  @Expose()\n  @Column({ nullable: false, default: false })\n  read?: boolean = false;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/models/notification.ts",
    "content": "import { NotificationType } from 'src/modules/notification/constants';\nimport { Expose } from 'class-transformer';\n\nexport class Notification {\n  @Expose()\n  type: NotificationType;\n\n  @Expose()\n  timestamp: number;\n\n  @Expose()\n  title: string;\n\n  @Expose()\n  category?: string;\n\n  @Expose()\n  categoryColor?: string;\n\n  @Expose()\n  body: string;\n\n  @Expose()\n  read?: boolean = false;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/notification.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Get,\n  HttpCode,\n  Patch,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  NotificationsDto,\n  ReadNotificationsDto,\n} from 'src/modules/notification/dto';\nimport { NotificationService } from 'src/modules/notification/notification.service';\n\n@ApiTags('Notifications')\n@Controller('notifications')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class NotificationController {\n  constructor(private readonly service: NotificationService) {}\n\n  @HttpCode(200)\n  @ApiOperation({ description: 'Return ordered notifications history' })\n  @ApiOkResponse({\n    type: NotificationsDto,\n  })\n  @Get()\n  getNotifications(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<NotificationsDto> {\n    return this.service.getNotifications(sessionMetadata);\n  }\n\n  @HttpCode(200)\n  @ApiOperation({ description: 'Mark all notifications as read' })\n  @ApiOkResponse({\n    type: NotificationsDto,\n  })\n  @Patch('/read')\n  readNotifications(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: ReadNotificationsDto,\n  ): Promise<NotificationsDto> {\n    return this.service.readNotifications(sessionMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/notification.gateway.ts",
    "content": "import { Socket, Server } from 'socket.io';\nimport {\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n  WebSocketGateway,\n  WebSocketServer,\n} from '@nestjs/websockets';\nimport { Logger } from '@nestjs/common';\nimport config from 'src/utils/config';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { NotificationServerEvents } from 'src/modules/notification/constants';\nimport { NotificationsDto } from 'src/modules/notification/dto';\nimport { GlobalNotificationProvider } from 'src/modules/notification/providers/global-notification.provider';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\nconst SOCKETS_CONFIG = config.get('sockets');\n\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n  serveClient: SOCKETS_CONFIG.serveClient,\n})\nexport class NotificationGateway\n  implements OnGatewayConnection, OnGatewayDisconnect\n{\n  @WebSocketServer() wss: Server;\n\n  private logger: Logger = new Logger('NotificationGateway');\n\n  constructor(\n    private readonly globalNotificationsProvider: GlobalNotificationProvider,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {}\n\n  async handleConnection(client: Socket): Promise<void> {\n    this.logger.debug(`Client connected: ${client.id}`);\n    // TODO: [USER_CONTEXT] how to get middleware into socket connection?\n    this.globalNotificationsProvider.init(\n      this.constantsProvider.getSystemSessionMetadata(),\n    );\n  }\n\n  async handleDisconnect(client: Socket): Promise<void> {\n    this.logger.debug(`Client disconnected: ${client.id}`);\n  }\n\n  @OnEvent(NotificationServerEvents.Notification)\n  notification(data: NotificationsDto) {\n    this.wss.of('/').emit(NotificationServerEvents.Notification, data);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/notification.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { NotificationGateway } from 'src/modules/notification/notification.gateway';\nimport { NotificationService } from 'src/modules/notification/notification.service';\nimport { GlobalNotificationProvider } from 'src/modules/notification/providers/global-notification.provider';\nimport { NotificationEmitter } from 'src/modules/notification/providers/notification.emitter';\nimport { NotificationController } from 'src/modules/notification/notification.controller';\nimport { NotificationRepository } from 'src/modules/notification/repositories/notification.repository';\nimport { LocalNotificationRepository } from './repositories/local.notification.repository';\n\n@Module({})\nexport class NotificationModule {\n  static register(\n    notificationRepository: Type<NotificationRepository> = LocalNotificationRepository,\n  ) {\n    return {\n      module: NotificationModule,\n      providers: [\n        {\n          provide: NotificationRepository,\n          useClass: notificationRepository,\n        },\n        NotificationGateway,\n        NotificationService,\n        GlobalNotificationProvider,\n        NotificationEmitter,\n      ],\n      controllers: [NotificationController],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/notification.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockNotificationRepository,\n  mockNotificationsDto,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { NotificationType } from 'src/modules/notification/constants';\nimport axios from 'axios';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { NotificationService } from 'src/modules/notification/notification.service';\nimport { NotificationRepository } from './repositories/notification.repository';\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\ndescribe('NotificationService', () => {\n  let service: NotificationService;\n  let repository: MockType<NotificationRepository>;\n\n  beforeEach(async () => {\n    jest.mock('axios', () => mockedAxios);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        NotificationService,\n        {\n          provide: NotificationRepository,\n          useFactory: mockNotificationRepository,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(NotificationService);\n    repository = await module.get(NotificationRepository);\n  });\n\n  describe('getNotifications', () => {\n    it('should return list of notifications with calculated unread field', async () => {\n      const result = await service.getNotifications(mockSessionMetadata);\n\n      expect(result).toEqual(mockNotificationsDto);\n    });\n    it('should throw an error if any', async () => {\n      repository.getNotifications.mockRejectedValue(new Error('some error'));\n\n      try {\n        await service.getNotifications(mockSessionMetadata);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to get notifications list');\n      }\n    });\n  });\n\n  describe('readNotifications', () => {\n    it('should update all notifications', async () => {\n      repository.getTotalUnread.mockResolvedValueOnce(0);\n\n      expect(await service.readNotifications(mockSessionMetadata, {})).toEqual({\n        totalUnread: 0,\n        notifications: [],\n      });\n    });\n    it('should throw an error if any', async () => {\n      repository.readNotifications.mockRejectedValue(new Error('some error'));\n\n      try {\n        await service.readNotifications(mockSessionMetadata, {\n          timestamp: 1,\n          type: NotificationType.Global,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to \"read\" notification(s)');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/notification.service.ts",
    "content": "import {\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { plainToInstance } from 'class-transformer';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  NotificationsDto,\n  ReadNotificationsDto,\n} from 'src/modules/notification/dto';\nimport { NotificationRepository } from './repositories/notification.repository';\n\n@Injectable()\nexport class NotificationService {\n  private logger: Logger = new Logger('NotificationService');\n\n  constructor(\n    private readonly notificationRepository: NotificationRepository,\n  ) {}\n\n  async getNotifications(\n    sessionMetadata: SessionMetadata,\n  ): Promise<NotificationsDto> {\n    this.logger.debug('Getting notifications list.', sessionMetadata);\n\n    try {\n      const [notifications, totalUnread] = await Promise.all([\n        this.notificationRepository.getNotifications(sessionMetadata),\n        this.notificationRepository.getTotalUnread(sessionMetadata),\n      ]);\n\n      return plainToInstance(NotificationsDto, {\n        notifications,\n        totalUnread,\n      });\n    } catch (e) {\n      this.logger.error('Unable to get notifications list', e, sessionMetadata);\n      throw new InternalServerErrorException(\n        'Unable to get notifications list',\n      );\n    }\n  }\n\n  /**\n   * Change read=true to notification(s) based on filter type and timestamp.\n   * When \"type\" and \"timestamp\" defined a single notification will be modified\n   * since we guarantee uniqueness by these fields\n   * If no filters - all notifications\n   * @param sessionMetadata\n   * @param dto\n   */\n  async readNotifications(\n    sessionMetadata: SessionMetadata,\n    dto: ReadNotificationsDto,\n  ): Promise<NotificationsDto> {\n    try {\n      this.logger.debug(\n        'Updating \"read=true\" status for notification(s).',\n        sessionMetadata,\n      );\n      const { type, timestamp } = dto;\n\n      const notifications = await this.notificationRepository.readNotifications(\n        sessionMetadata,\n        type,\n        timestamp,\n      );\n\n      return plainToInstance(NotificationsDto, {\n        notifications,\n        totalUnread:\n          await this.notificationRepository.getTotalUnread(sessionMetadata),\n      });\n    } catch (e) {\n      this.logger.error('Unable to \"read\" notification(s)', e, sessionMetadata);\n      throw new InternalServerErrorException(\n        'Unable to \"read\" notification(s)',\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/providers/global-notification.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockNotification1,\n  mockNotification1UPD,\n  mockNotification2,\n  mockNotificationRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { GlobalNotificationProvider } from 'src/modules/notification/providers/global-notification.provider';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport axios from 'axios';\nimport {\n  CreateNotificationDto,\n  CreateNotificationsDto,\n} from 'src/modules/notification/dto';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { NotificationRepository } from '../repositories/notification.repository';\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\ndescribe('GlobalNotificationProvider', () => {\n  let service: GlobalNotificationProvider;\n  let repository: MockType<NotificationRepository>;\n  let getNotificationsFromRemoteSpy: any;\n\n  beforeEach(async () => {\n    jest.mock('axios', () => mockedAxios);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        GlobalNotificationProvider,\n        EventEmitter2,\n        {\n          provide: NotificationRepository,\n          useFactory: mockNotificationRepository,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(GlobalNotificationProvider);\n    repository = await module.get(NotificationRepository);\n\n    getNotificationsFromRemoteSpy = jest.spyOn(\n      service,\n      'getNotificationsFromRemote',\n    );\n  });\n\n  afterEach(() => {\n    clearInterval(service ? service['interval'] : undefined);\n  });\n  describe('init', () => {\n    it('should should init and set interval only once', async () => {\n      const syncSpy = jest.spyOn(service, 'sync');\n\n      expect(service['interval']).toEqual(undefined);\n      service.init(mockSessionMetadata);\n      expect(service['interval']).not.toEqual(undefined);\n      service.init(mockSessionMetadata);\n      expect(syncSpy).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('sync', () => {\n    it('should add new notifications and update existing one and delete one', async () => {\n      getNotificationsFromRemoteSpy.mockResolvedValueOnce({\n        notifications: [mockNotification1UPD, mockNotification2],\n      });\n\n      repository.getGlobalNotifications.mockResolvedValueOnce([\n        mockNotification1,\n      ]);\n\n      await service.sync(mockSessionMetadata);\n\n      expect(repository.deleteGlobalNotifications).toHaveBeenCalledTimes(1);\n      expect(repository.deleteGlobalNotifications).toHaveBeenCalledWith(\n        mockSessionMetadata,\n      );\n\n      expect(repository.insertNotifications).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        [mockNotification2, mockNotification1UPD],\n      );\n    });\n  });\n\n  describe('getNotificationsFromRemote', () => {\n    it('should add new notifications and update existing one and delete one', async () => {\n      mockedAxios.get.mockResolvedValue({\n        data: Buffer.from(\n          JSON.stringify({\n            notifications: [mockNotification1, mockNotification2],\n          }),\n        ),\n      });\n\n      const res = await service.getNotificationsFromRemote();\n\n      expect(res).toEqual(\n        new CreateNotificationsDto({\n          notifications: [\n            new CreateNotificationDto({ ...mockNotification1 }),\n            new CreateNotificationDto({ ...mockNotification2 }),\n          ],\n        }),\n      );\n    });\n    it('should throw an error when incorrect data passed', async () => {\n      mockedAxios.get.mockResolvedValue({\n        data: Buffer.from('incorrect json'),\n      });\n\n      try {\n        await service.getNotificationsFromRemote();\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n        expect(e.message).toEqual('Unable to get and parse file from remote');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/providers/global-notification.provider.ts",
    "content": "import {\n  BadRequestException,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { plainToInstance } from 'class-transformer';\nimport { Validator } from 'class-validator';\nimport { forEach, keyBy, orderBy, values } from 'lodash';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  NotificationEvents,\n  NotificationType,\n} from 'src/modules/notification/constants';\nimport { CreateNotificationsDto } from 'src/modules/notification/dto';\nimport { Notification } from 'src/modules/notification/models/notification';\nimport { getFile } from 'src/utils';\nimport config from 'src/utils/config';\nimport { NotificationRepository } from '../repositories/notification.repository';\n\nconst NOTIFICATIONS_CONFIG = config.get('notifications');\n\n@Injectable()\nexport class GlobalNotificationProvider {\n  private logger: Logger = new Logger('GlobalNotificationProvider');\n\n  private validator = new Validator();\n\n  private interval: NodeJS.Timeout;\n\n  constructor(\n    private notificationRepository: NotificationRepository,\n    private eventEmitter: EventEmitter2,\n  ) {}\n\n  init(sessionMetadata: SessionMetadata) {\n    if (this.interval) {\n      return;\n    }\n\n    this.interval = setInterval(() => {\n      this.sync(sessionMetadata).catch();\n    }, NOTIFICATIONS_CONFIG.syncInterval);\n\n    this.sync(sessionMetadata).catch();\n  }\n\n  async sync(sessionMetadata: SessionMetadata) {\n    try {\n      const remoteNotificationsDto = await this.getNotificationsFromRemote();\n\n      await this.validatedNotifications(remoteNotificationsDto);\n\n      const toInsert = keyBy(\n        remoteNotificationsDto.notifications.map((notification) =>\n          plainToInstance(Notification, {\n            ...notification,\n            type: NotificationType.Global,\n            read: false,\n          }),\n        ),\n        'timestamp',\n      );\n\n      const currentNotifications = keyBy(\n        await this.notificationRepository.getGlobalNotifications(\n          sessionMetadata,\n        ),\n        'timestamp',\n      );\n\n      await this.notificationRepository.deleteGlobalNotifications(\n        sessionMetadata,\n      );\n\n      // process\n\n      const newNotifications = [];\n\n      forEach(toInsert, (notification) => {\n        if (currentNotifications[notification.timestamp]) {\n          toInsert[notification.timestamp].read =\n            currentNotifications[notification.timestamp].read;\n        } else {\n          newNotifications.push(notification);\n        }\n      });\n\n      await this.notificationRepository.insertNotifications(\n        sessionMetadata,\n        values(toInsert),\n      );\n\n      this.eventEmitter.emit(\n        NotificationEvents.NewNotifications,\n        sessionMetadata,\n        orderBy(newNotifications, ['timestamp'], 'desc'),\n      );\n    } catch (e) {\n      this.logger.error(\n        'Unable to sync notifications with remote',\n        e,\n        sessionMetadata,\n      );\n    }\n  }\n\n  async validatedNotifications(dto: CreateNotificationsDto): Promise<void> {\n    this.logger.debug('Validating notifications from remote');\n\n    try {\n      const notificationsDto: CreateNotificationsDto = plainToInstance(\n        CreateNotificationsDto,\n        dto,\n      );\n      await this.validator.validateOrReject(notificationsDto, {\n        whitelist: true,\n      });\n    } catch (e) {\n      this.logger.error(`Invalid notification(s) found. ${e.message}`, e);\n      throw new BadRequestException(e);\n    }\n  }\n\n  async getNotificationsFromRemote(): Promise<CreateNotificationsDto> {\n    this.logger.debug('Getting notifications from remote');\n\n    try {\n      const buffer = await getFile(NOTIFICATIONS_CONFIG.updateUrl);\n      const serializedString = buffer.toString();\n      const json = JSON.parse(serializedString);\n      return plainToInstance(CreateNotificationsDto, json);\n    } catch (e) {\n      this.logger.error(\n        `Unable to download or parse notifications json. ${e.message}`,\n        e,\n      );\n      throw new InternalServerErrorException(\n        'Unable to get and parse file from remote',\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/providers/notification.emitter.spec.ts",
    "content": "import { EventEmitter2 } from '@nestjs/event-emitter';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport {\n  mockNotification1,\n  mockNotification2,\n  mockNotificationRepository,\n  mockNotificationsDto,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { NotificationServerEvents } from 'src/modules/notification/constants';\nimport { NotificationEmitter } from 'src/modules/notification/providers/notification.emitter';\nimport { NotificationRepository } from '../repositories/notification.repository';\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\nconst mockEventEmitter = {\n  emit: jest.fn(),\n};\n\ndescribe('NotificationEmitter', () => {\n  let service: NotificationEmitter;\n  let repository: MockType<NotificationRepository>;\n  let emitter: MockType<EventEmitter2>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    jest.mock('axios', () => mockedAxios);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        NotificationEmitter,\n        EventEmitter2,\n        {\n          provide: EventEmitter2,\n          useFactory: () => mockEventEmitter,\n        },\n        {\n          provide: NotificationRepository,\n          useFactory: mockNotificationRepository,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(NotificationEmitter);\n    repository = await module.get(NotificationRepository);\n    emitter = await module.get(EventEmitter2);\n    emitter.emit.mockReset();\n  });\n\n  describe('notification', () => {\n    it('should return undefined if no notifications', async () => {\n      await service.notification(mockSessionMetadata, []);\n      expect(emitter.emit).toHaveBeenCalledTimes(0);\n    });\n    it('should should init and set interval only once', async () => {\n      await service.notification(mockSessionMetadata, [\n        mockNotification1,\n        mockNotification2,\n      ]);\n      expect(emitter.emit).toHaveBeenCalledTimes(1);\n      expect(emitter.emit).toHaveBeenCalledWith(\n        NotificationServerEvents.Notification,\n        mockNotificationsDto,\n      );\n    });\n    it('should log an error but not fail', async () => {\n      repository.getTotalUnread.mockRejectedValueOnce(new Error('some error'));\n\n      await service.notification(mockSessionMetadata, [mockNotification1]);\n\n      expect(emitter.emit).toHaveBeenCalledTimes(0);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/providers/notification.emitter.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { EventEmitter2, OnEvent } from '@nestjs/event-emitter';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  NotificationEvents,\n  NotificationServerEvents,\n} from 'src/modules/notification/constants';\nimport { NotificationsDto } from 'src/modules/notification/dto';\nimport { Notification } from 'src/modules/notification/models/notification';\nimport { plainToInstance } from 'class-transformer';\nimport { NotificationRepository } from '../repositories/notification.repository';\n\n@Injectable()\nexport class NotificationEmitter {\n  private logger: Logger = new Logger('NotificationEmitter');\n\n  constructor(\n    private readonly notificationRepository: NotificationRepository,\n    private readonly eventEmitter: EventEmitter2,\n  ) {}\n\n  @OnEvent(NotificationEvents.NewNotifications)\n  async notification(\n    sessionMetadata: SessionMetadata,\n    notifications: Notification[],\n  ) {\n    try {\n      if (!notifications?.length) {\n        return;\n      }\n\n      this.logger.debug(`${notifications.length} new notification(s) to emit`);\n\n      const totalUnread =\n        await this.notificationRepository.getTotalUnread(sessionMetadata);\n\n      this.eventEmitter.emit(\n        NotificationServerEvents.Notification,\n        plainToInstance(NotificationsDto, {\n          notifications,\n          totalUnread,\n        }),\n      );\n    } catch (e) {\n      this.logger.error('Unable to prepare dto for notifications', e);\n      // ignore error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/repositories/local.notification.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockNotification1,\n  mockNotification1Entity,\n  mockNotification2,\n  mockNotification2Entity,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalNotificationRepository } from 'src/modules/notification/repositories/local.notification.repository';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { NotificationEntity } from 'src/modules/notification/entities/notification.entity';\nimport { NotificationType } from 'src/modules/notification/constants';\n\ndescribe('LocalNotificationRepository', () => {\n  let service: LocalNotificationRepository;\n  let repository: MockType<Repository<NotificationEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalNotificationRepository,\n        {\n          provide: getRepositoryToken(NotificationEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(LocalNotificationRepository);\n    repository = await module.get(getRepositoryToken(NotificationEntity));\n  });\n\n  describe('getNotifications', () => {\n    it('should return list of notifications', async () => {\n      repository\n        .createQueryBuilder()\n        .getMany.mockResolvedValueOnce([\n          mockNotification1Entity,\n          mockNotification2Entity,\n        ]);\n\n      expect(await service.getNotifications()).toEqual([\n        mockNotification1,\n        mockNotification2,\n      ]);\n    });\n  });\n  describe('getTotalUnread', () => {\n    it('should return number of unread messages', async () => {\n      repository.createQueryBuilder().getCount.mockResolvedValueOnce(3);\n\n      expect(await service.getTotalUnread()).toEqual(3);\n    });\n  });\n  describe('readNotifications', () => {\n    it('should read all notifications', async () => {\n      repository.createQueryBuilder().execute.mockResolvedValueOnce(undefined);\n\n      expect(await service.readNotifications(mockSessionMetadata)).toEqual([]);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({});\n    });\n    it('should read particular notification by timestamp', async () => {\n      repository.createQueryBuilder().execute.mockResolvedValueOnce(undefined);\n\n      expect(\n        await service.readNotifications(\n          mockSessionMetadata,\n          undefined,\n          mockNotification1.timestamp,\n        ),\n      ).toEqual([]);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        timestamp: mockNotification1.timestamp,\n      });\n    });\n    it('should read notifications by type', async () => {\n      repository.createQueryBuilder().execute.mockResolvedValueOnce(undefined);\n\n      expect(\n        await service.readNotifications(\n          mockSessionMetadata,\n          mockNotification1.type,\n        ),\n      ).toEqual([]);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        type: mockNotification1.type,\n      });\n    });\n  });\n  describe('insertNotifications', () => {\n    it('should insert multiple notifications', async () => {\n      repository.createQueryBuilder().execute.mockResolvedValueOnce(undefined);\n\n      expect(\n        await service.insertNotifications(mockSessionMetadata, [\n          mockNotification1,\n          mockNotification2,\n        ]),\n      ).toEqual(undefined);\n      expect(repository.insert).toHaveBeenCalledWith([\n        mockNotification1Entity,\n        mockNotification2Entity,\n      ]);\n    });\n  });\n  describe('getGlobalNotifications', () => {\n    it('should query global notifications particular fields only', async () => {\n      repository.createQueryBuilder().getMany.mockResolvedValueOnce([\n        {\n          timestamp: mockNotification1Entity.timestamp,\n          read: mockNotification1Entity.read,\n        },\n      ]);\n\n      expect(await service.getGlobalNotifications()).toEqual([\n        {\n          timestamp: mockNotification1.timestamp,\n          read: mockNotification1.read,\n        },\n      ]);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        type: NotificationType.Global,\n      });\n      expect(repository.createQueryBuilder().select).toHaveBeenCalledWith([\n        'n.timestamp',\n        'n.read',\n      ]);\n    });\n  });\n  describe('deleteGlobalNotifications', () => {\n    it('should remove only global notifications', async () => {\n      repository.createQueryBuilder().execute.mockResolvedValueOnce(undefined);\n\n      expect(await service.deleteGlobalNotifications()).toEqual(undefined);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        type: NotificationType.Global,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/repositories/local.notification.repository.ts",
    "content": "import { InjectRepository } from '@nestjs/typeorm';\nimport { SessionMetadata } from 'src/common/models';\nimport { Repository } from 'typeorm';\nimport { classToClass } from 'src/utils';\nimport { NotificationType } from '../constants';\nimport { NotificationEntity } from '../entities/notification.entity';\nimport { Notification } from '../models/notification';\nimport { NotificationRepository } from './notification.repository';\n\nexport class LocalNotificationRepository extends NotificationRepository {\n  constructor(\n    @InjectRepository(NotificationEntity)\n    private readonly repository: Repository<NotificationEntity>,\n  ) {\n    super();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getNotifications(): Promise<Notification[]> {\n    const notifications = await this.repository\n      .createQueryBuilder('n')\n      .orderBy('timestamp', 'DESC')\n      // .limit(NOTIFICATIONS_CONFIG.queryLimit) // todo: do not forget when introduce \"local\" notifications\n      .getMany();\n\n    return notifications.map((ne) => classToClass(Notification, ne));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getTotalUnread(): Promise<number> {\n    return this.repository\n      .createQueryBuilder()\n      .where({ read: false })\n      .getCount();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async readNotifications(\n    _: SessionMetadata,\n    notificationType?: NotificationType,\n    timestamp?: number,\n  ): Promise<Notification[]> {\n    const query: Record<string, any> = {};\n\n    if (notificationType) {\n      query.type = notificationType;\n    }\n\n    if (timestamp) {\n      query.timestamp = timestamp;\n    }\n\n    await this.repository\n      .createQueryBuilder('n')\n      .update()\n      .where(query)\n      .set({ read: true })\n      .execute();\n\n    return [];\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async insertNotifications(\n    _: SessionMetadata,\n    notifications: Notification[],\n  ): Promise<void> {\n    await this.repository.insert(\n      notifications.map((n) => classToClass(NotificationEntity, n)),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getGlobalNotifications(): Promise<Partial<Notification>[]> {\n    return this.repository\n      .createQueryBuilder('n')\n      .where({ type: NotificationType.Global })\n      .select(['n.timestamp', 'n.read'])\n      .getMany();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async deleteGlobalNotifications(): Promise<void> {\n    await this.repository\n      .createQueryBuilder('n')\n      .delete()\n      .where({ type: NotificationType.Global })\n      .execute();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/notification/repositories/notification.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { NotificationType } from '../constants';\nimport { Notification } from '../models/notification';\n\nexport abstract class NotificationRepository {\n  /**\n   * Get all notifications ordered (DESC) by timestamp field\n   * @param sessionMetadata\n   */\n  abstract getNotifications(\n    sessionMetadata: SessionMetadata,\n  ): Promise<Notification[]>;\n\n  /**\n   * Get number of total unread notifications\n   * @param sessionMetadata\n   */\n  abstract getTotalUnread(sessionMetadata: SessionMetadata): Promise<number>;\n\n  /**\n   * Mark notifications by type or timestamp as read\n   * Should always return empty array\n   * @param sessionMetadata\n   * @param notificationType\n   * @param timestamp\n   */\n  abstract readNotifications(\n    sessionMetadata: SessionMetadata,\n    notificationType?: NotificationType,\n    timestamp?: number,\n  ): Promise<Notification[]>;\n\n  /**\n   * Simply insert notifications\n   * Might fail due to constraint error. Make sure to remove duplicates before\n   * @param sessionMetadata\n   * @param notifications\n   */\n  abstract insertNotifications(\n    sessionMetadata: SessionMetadata,\n    notifications: Notification[],\n  ): Promise<void>;\n\n  /**\n   * Special function to get only \"global\" type of notifications\n   * Used for auto update notifications from remote\n   * @param sessionMetadata\n   */\n  abstract getGlobalNotifications(\n    sessionMetadata: SessionMetadata,\n  ): Promise<Partial<Notification>[]>;\n\n  /**\n   * Deletes all \"global\" notification\n   * Used during auto update from remote\n   * @param sessionMetadata\n   */\n  abstract deleteGlobalNotifications(\n    sessionMetadata: SessionMetadata,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/plugin/plugin.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { PluginService } from 'src/modules/plugin/plugin.service';\nimport { PluginsResponse } from 'src/modules/plugin/plugin.response';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\n\n@ApiTags('Plugins')\n@Controller('/plugins')\nexport class PluginController {\n  constructor(private readonly pluginService: PluginService) {}\n\n  @ApiEndpoint({\n    statusCode: 200,\n    description: 'Get list of available plugins',\n    responses: [\n      {\n        status: 200,\n        type: PluginsResponse,\n      },\n    ],\n  })\n  @Get()\n  async getAll(): Promise<PluginsResponse> {\n    return this.pluginService.getAll();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/plugin/plugin.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { PluginController } from 'src/modules/plugin/plugin.controller';\nimport { PluginService } from 'src/modules/plugin/plugin.service';\n\n@Module({\n  controllers: [PluginController],\n  providers: [PluginService],\n  exports: [PluginService],\n})\nexport class PluginModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/plugin/plugin.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsBoolean,\n  IsDefined,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class PluginVisualization {\n  @ApiProperty({\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  id: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  activationMethod: string;\n\n  @ApiProperty({\n    type: String,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @Type(() => String)\n  matchCommands: string[];\n\n  @ApiProperty({\n    type: Boolean,\n  })\n  @IsOptional()\n  @IsNotEmpty()\n  @IsBoolean()\n  default?: boolean;\n\n  @ApiProperty({\n    type: String,\n  })\n  @IsOptional()\n  @IsNotEmpty()\n  @IsString()\n  iconDark?: string;\n\n  @ApiProperty({\n    type: String,\n  })\n  @IsOptional()\n  @IsNotEmpty()\n  @IsString()\n  iconLight?: string;\n}\n\nexport class Plugin {\n  @ApiPropertyOptional({\n    description: 'Determine if plugin is built into Redisinsight',\n    type: Boolean,\n  })\n  internal?: boolean;\n\n  @ApiProperty({\n    description: 'Module name from manifest',\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  name: string;\n\n  @ApiProperty({\n    description: 'Plugins base url',\n    type: String,\n  })\n  baseUrl: string;\n\n  @ApiProperty({\n    description: 'Uri to main js file on the local server',\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  main: string;\n\n  @ApiProperty({\n    description: 'Uri to css file on the local server',\n    type: String,\n  })\n  @IsOptional()\n  @IsNotEmpty()\n  @IsString()\n  styles?: string;\n\n  @ApiProperty({\n    description: 'Visualization field from manifest',\n    type: PluginVisualization,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested({ each: true })\n  @Type(() => PluginVisualization)\n  visualizations: PluginVisualization[];\n}\n\nexport class PluginsResponse {\n  @ApiProperty({\n    description: 'Uri to static resources required for plugins',\n    type: String,\n  })\n  static: string;\n\n  @ApiProperty({\n    description: 'List of available plugins',\n    type: Plugin,\n    isArray: true,\n  })\n  plugins: Plugin[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/plugin/plugin.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { plainToInstance } from 'class-transformer';\nimport { Validator } from 'class-validator';\nimport { readdirSync, existsSync, readFileSync } from 'fs';\nimport config, { Config } from 'src/utils/config';\nimport * as path from 'path';\nimport { filter } from 'lodash';\nimport { PluginsResponse, Plugin } from 'src/modules/plugin/plugin.response';\n\nconst PATH_CONFIG = config.get('dir_path') as Config['dir_path'];\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Injectable()\nexport class PluginService {\n  private logger = new Logger('PluginService');\n\n  private validator = new Validator();\n\n  /**\n   * Get all plugins\n   */\n  async getAll(): Promise<PluginsResponse> {\n    return {\n      static: path.posix.join(SERVER_CONFIG.pluginsAssetsUri),\n      plugins: [\n        ...(await this.scanPluginsFolder(\n          PATH_CONFIG.defaultPlugins,\n          SERVER_CONFIG.defaultPluginsUri,\n          true,\n        )),\n        ...(await this.scanPluginsFolder(\n          PATH_CONFIG.customPlugins,\n          SERVER_CONFIG.customPluginsUri,\n        )),\n      ],\n    };\n  }\n\n  private async scanPluginsFolder(\n    pluginsFolder: string,\n    urlPrefix: string,\n    internal: boolean = false,\n  ): Promise<Plugin[]> {\n    const plugins = existsSync(pluginsFolder) ? readdirSync(pluginsFolder) : [];\n    return filter(\n      await Promise.all(\n        plugins.map(async (pluginFolder) => {\n          try {\n            const manifest = JSON.parse(\n              readFileSync(\n                path.join(pluginsFolder, pluginFolder, 'package.json'),\n                'utf8',\n              ),\n            );\n\n            // const plugin = plainToInstance(Plugin, manifest, { excludeExtraneousValues: true, strategy: 'exposeAll' });\n            const plugin = plainToInstance(Plugin, manifest);\n            await this.validator.validateOrReject(plugin, {\n              whitelist: true,\n            });\n\n            plugin.internal = internal || undefined;\n            plugin.baseUrl = path.posix.join(urlPrefix, pluginFolder, '/');\n            plugin.main = path.posix.join(\n              urlPrefix,\n              pluginFolder,\n              manifest.main,\n            );\n            if (plugin.styles) {\n              plugin.styles = path.posix.join(\n                urlPrefix,\n                pluginFolder,\n                manifest.styles,\n              );\n            }\n\n            return plugin;\n          } catch (error) {\n            this.logger.error(\n              `Error when trying to process plugin ${pluginFolder}`,\n              error,\n            );\n            return undefined;\n          }\n        }),\n      ),\n      (plugin) => !!plugin,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/constants/index.ts",
    "content": "export enum ProfilerClientEvents {\n  Monitor = 'monitor',\n  Pause = 'pause',\n  FlushLogs = 'flushLogs',\n}\n\nexport enum ProfilerServerEvents {\n  Data = 'monitorData',\n  Exception = 'exception',\n}\n\nexport enum RedisObserverStatus {\n  Empty = 'empty',\n  Initializing = 'initializing',\n  Connected = 'connected',\n  Wait = 'wait',\n  Ready = 'ready',\n  End = 'end',\n  Error = 'error',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/emitters/client.logs-emiter.spec.ts",
    "content": "import { mockSocket } from 'src/__mocks__';\nimport { ClientLogsEmitter } from 'src/modules/profiler/emitters/client.logs-emitter';\nimport { ProfilerServerEvents } from 'src/modules/profiler/constants';\nimport * as MockedSocket from 'socket.io-mock';\n\ndescribe('ClientLogsEmitter', () => {\n  let emitter: ClientLogsEmitter;\n\n  beforeEach(() => {\n    emitter = new ClientLogsEmitter(mockSocket);\n  });\n\n  it('Initialization', () => {\n    emitter = new ClientLogsEmitter(mockSocket);\n    expect(emitter.id).toEqual(mockSocket.id);\n    expect(emitter['client']).toEqual(mockSocket);\n  });\n\n  it('Emit', async () => {\n    const client = new MockedSocket();\n    client['emit'] = jest.fn();\n\n    const items = [1, 2, 3];\n\n    emitter = new ClientLogsEmitter(client);\n    await emitter.emit(items);\n    expect(emitter['client'].emit).toHaveBeenCalledWith(\n      ProfilerServerEvents.Data,\n      items,\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/emitters/client.logs-emitter.ts",
    "content": "import { Socket } from 'socket.io';\nimport { ILogsEmitter } from 'src/modules/profiler/interfaces/logs-emitter.interface';\nimport { ProfilerServerEvents } from 'src/modules/profiler/constants';\n\nexport class ClientLogsEmitter implements ILogsEmitter {\n  private readonly client: Socket;\n\n  public readonly id: string;\n\n  constructor(client: Socket) {\n    this.id = client.id;\n    this.client = client;\n  }\n\n  public async emit(items: any[]) {\n    return this.client.emit(ProfilerServerEvents.Data, items);\n  }\n\n  public addProfilerClient() {}\n\n  public removeProfilerClient() {}\n\n  public flushLogs() {}\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/emitters/file.logs-emiter.spec.ts",
    "content": "import {\n  mockLogFile,\n  mockMonitorDataItem,\n  mockProfilerClient,\n  mockWriteStream,\n} from 'src/__mocks__';\nimport { FileLogsEmitter } from 'src/modules/profiler/emitters/file.logs-emitter';\n\ndescribe('FileLogsEmitter', () => {\n  let emitter: FileLogsEmitter;\n\n  beforeEach(() => {\n    emitter = new FileLogsEmitter(mockLogFile);\n  });\n\n  it('initialization', () => {\n    expect(emitter.id).toEqual(mockLogFile.id);\n    expect(emitter['logFile']).toEqual(mockLogFile);\n  });\n\n  it('emit', async () => {\n    const items = [mockMonitorDataItem, mockMonitorDataItem];\n\n    emitter['logFile']['getWriteStream'] = jest.fn().mockReturnValue(undefined);\n    await emitter.emit(items);\n    expect(mockWriteStream.write).not.toHaveBeenCalled();\n\n    emitter['logFile']['getWriteStream'] = jest\n      .fn()\n      .mockReturnValue(mockWriteStream);\n    await emitter.emit(items);\n    expect(emitter['logFile'].getWriteStream).toHaveBeenCalled();\n    expect(mockWriteStream.write).toHaveBeenCalled();\n  });\n\n  it('emit', async () => {\n    const items = [mockMonitorDataItem, mockMonitorDataItem];\n    await emitter.emit(items);\n    expect(emitter['logFile'].getWriteStream).toHaveBeenCalled();\n    expect(mockWriteStream.write).toHaveBeenCalled();\n  });\n\n  it('addProfilerClient', () => {\n    emitter.addProfilerClient(mockProfilerClient.id);\n    expect(emitter['logFile'].addProfilerClient).toHaveBeenCalledWith(\n      mockProfilerClient.id,\n    );\n  });\n\n  it('removeProfilerClient', () => {\n    emitter.removeProfilerClient(mockProfilerClient.id);\n    expect(emitter['logFile'].removeProfilerClient).toHaveBeenCalledWith(\n      mockProfilerClient.id,\n    );\n  });\n\n  it('flushLogs', () => {\n    emitter.flushLogs();\n    expect(emitter['logFile'].destroy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/emitters/file.logs-emitter.ts",
    "content": "import { ILogsEmitter } from 'src/modules/profiler/interfaces/logs-emitter.interface';\nimport { LogFile } from 'src/modules/profiler/models/log-file';\n\nexport class FileLogsEmitter implements ILogsEmitter {\n  public readonly id: string;\n\n  private readonly logFile: LogFile;\n\n  constructor(logFile: LogFile) {\n    this.id = logFile.id;\n    this.logFile = logFile;\n  }\n\n  /**\n   * Write batch of logs to a file\n   */\n  async emit(items: any[]) {\n    try {\n      if (!this.logFile.getWriteStream()) {\n        return;\n      }\n\n      const text = items\n        .map((item) => {\n          const args = item.args\n            .map((arg) => `${JSON.stringify(arg)}`)\n            .join(' ');\n          return `${item.time} [${item.database} ${item.source}] ${args}`;\n        })\n        .join('\\n');\n\n      this.logFile.getWriteStream().write(`${text}\\n`);\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  async addProfilerClient(id: string) {\n    return this.logFile.addProfilerClient(id);\n  }\n\n  async removeProfilerClient(id: string) {\n    return this.logFile.removeProfilerClient(id);\n  }\n\n  async flushLogs() {\n    return this.logFile.destroy();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/interfaces/logs-emitter.interface.ts",
    "content": "export interface ILogsEmitter {\n  id: string;\n  emit: (items: any[]) => void;\n  addProfilerClient: (id: string) => void;\n  removeProfilerClient: (id: string) => void;\n  flushLogs: () => void;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/interfaces/monitor-data.interface.ts",
    "content": "import * as IORedis from 'ioredis';\n\nexport interface IMonitorData {\n  time: string;\n  args: string[];\n  source: string;\n  database: number;\n  shardOptions: IORedis.RedisOptions;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/interfaces/shard-observer.interface.ts",
    "content": "import { EventEmitter } from 'events';\nimport * as IORedis from 'ioredis';\n\nexport interface IShardObserver extends EventEmitter {\n  disconnect(): void;\n  options?: IORedis.RedisOptions;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/log-file.spec.ts",
    "content": "import * as fs from 'fs-extra';\nimport { LogFile } from 'src/modules/profiler/models/log-file';\nimport {\n  mockLogFile,\n  mockProfilerAnalyticsEvents,\n  mockSocket,\n} from 'src/__mocks__';\nimport config from 'src/utils/config';\nimport { join } from 'path';\nimport { ReadStream, WriteStream } from 'fs';\nimport { FileLogsEmitter } from 'src/modules/profiler/emitters/file.logs-emitter';\nimport { TelemetryEvents } from 'src/constants';\n\nconst DIR_PATH = config.get('dir_path');\n\ndescribe('LogFile', () => {\n  let logFile: LogFile;\n\n  beforeEach(() => {\n    logFile = new LogFile(\n      mockLogFile.instanceId,\n      mockLogFile.id,\n      mockProfilerAnalyticsEvents,\n    );\n  });\n\n  it('Initialization', () => {\n    const initTime = new Date();\n    logFile = new LogFile(mockLogFile.instanceId, mockLogFile.id);\n\n    expect(logFile.instanceId).toEqual(mockLogFile.instanceId);\n    expect(logFile.id).toEqual(mockLogFile.id);\n    expect(logFile['alias']).toEqual(mockLogFile.id);\n    expect(logFile['filePath']).toEqual(join(DIR_PATH.tmpDir, logFile.id));\n    expect(logFile['startTime'].getTime()).toBeGreaterThanOrEqual(\n      initTime.getTime(),\n    );\n    expect(logFile['startTime'].getTime()).toBeLessThanOrEqual(\n      new Date().getTime(),\n    );\n    expect(logFile['analyticsEvents']).toEqual(new Map());\n    expect(logFile['clientObservers']).toEqual(new Map());\n    expect(logFile['emitter']).toEqual(undefined);\n  });\n\n  it('getWriteStream', () => {\n    const stream = logFile.getWriteStream();\n    expect(stream).toBeInstanceOf(WriteStream);\n    expect(logFile.getWriteStream()).toEqual(stream);\n  });\n\n  it('getReadStream', async () => {\n    logFile.getWriteStream().write('');\n    const stream = logFile.getReadStream();\n    expect(stream).toBeInstanceOf(ReadStream);\n    expect(stream.destroyed).toEqual(false);\n    stream.emit('end');\n    expect(stream.destroyed).toEqual(true);\n    // todo: investigate why didn't pass on circle\n    // expect(mockProfilerAnalyticsEvents.get(TelemetryEvents.ProfilerLogDownloaded)).toHaveBeenCalled();\n    expect(logFile.getReadStream()).not.toEqual(stream);\n  });\n\n  it('getEmitter', () => {\n    const emitter = logFile.getEmitter();\n    expect(emitter).toBeInstanceOf(FileLogsEmitter);\n    expect(emitter.id).toEqual(logFile.id);\n    expect(emitter['logFile']).toEqual(logFile);\n    expect(logFile.getEmitter()).toEqual(emitter);\n  });\n\n  it('getFilename + setAlias', () => {\n    const fileName = logFile.getFilename();\n    expect(logFile['alias']).toEqual(logFile['id']);\n    expect(fileName).toMatch(\n      `${logFile['id']}-${logFile['startTime'].getTime()}-`,\n    );\n    logFile.setAlias('someAlias');\n    expect(logFile['alias']).toEqual('someAlias');\n    expect(logFile.getFilename()).toMatch(\n      `someAlias-${logFile['startTime'].getTime()}-`,\n    );\n  });\n\n  it('addProfilerClient + removeProfilerClient', async () => {\n    logFile['destroy'] = jest.fn();\n\n    expect(logFile['clientObservers'].size).toEqual(0);\n    logFile.addProfilerClient(mockSocket.id);\n    expect(logFile['clientObservers'].size).toEqual(1);\n    expect(logFile['idleSince']).toEqual(0);\n    logFile.removeProfilerClient('007');\n    expect(logFile['clientObservers'].size).toEqual(1);\n    expect(logFile['idleSince']).toEqual(0);\n    logFile.removeProfilerClient(mockSocket.id);\n    expect(logFile['clientObservers'].size).toEqual(0);\n    expect(logFile['idleSince']).toBeGreaterThan(0);\n    expect(logFile.destroy).not.toHaveBeenCalled();\n    // wait until idle threshold pass (2sec for test env)\n    await new Promise((resolve) => setTimeout(resolve, 3000));\n    expect(logFile.destroy).toHaveBeenCalled();\n  });\n\n  it('destroy', async () => {\n    try {\n      fs.unlinkSync(logFile['filePath']);\n    } catch (e) {\n      // ignore file not found\n    }\n    expect(logFile['writeStream']).toEqual(undefined);\n    expect(fs.existsSync(logFile['filePath'])).toEqual(false);\n    const stream = logFile.getWriteStream();\n    expect(stream['_writableState'].ended).toEqual(false);\n    await new Promise((res, rej) => {\n      stream.write('somedata', (err) => {\n        if (err) {\n          return rej(err);\n        }\n\n        expect(fs.existsSync(logFile['filePath'])).toEqual(true);\n        return res(true);\n      });\n    });\n    expect(logFile['writeStream']).toEqual(stream);\n    await logFile.destroy();\n    expect(logFile['writeStream']).toEqual(null);\n    expect(fs.existsSync(logFile['filePath'])).toEqual(false);\n    expect(stream['_writableState'].ended).toEqual(true);\n    expect(\n      mockProfilerAnalyticsEvents.get(TelemetryEvents.ProfilerLogDeleted),\n    ).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/log-file.ts",
    "content": "import { join } from 'path';\nimport * as fs from 'fs-extra';\nimport { ReadStream, WriteStream } from 'fs';\nimport config from 'src/utils/config';\nimport { FileLogsEmitter } from 'src/modules/profiler/emitters/file.logs-emitter';\nimport { TelemetryEvents } from 'src/constants';\n\nconst DIR_PATH = config.get('dir_path');\nconst PROFILER = config.get('profiler');\n\nexport class LogFile {\n  private readonly filePath: string;\n\n  private startTime: Date;\n\n  private writeStream: WriteStream;\n\n  private emitter: FileLogsEmitter;\n\n  private readonly clientObservers: Map<string, string> = new Map();\n\n  private idleSince: number = 0;\n\n  private alias: string;\n\n  private analyticsEvents: Map<TelemetryEvents, Function>;\n\n  public readonly instanceId: string;\n\n  public readonly id: string;\n\n  constructor(\n    instanceId: string,\n    id: string,\n    analyticsEvents?: Map<TelemetryEvents, Function>,\n  ) {\n    this.instanceId = instanceId;\n    this.id = id;\n    this.alias = id;\n    this.filePath = join(DIR_PATH.tmpDir, this.id);\n    this.startTime = new Date();\n    this.analyticsEvents = analyticsEvents || new Map();\n  }\n\n  /**\n   * Get or create file write stream to write logs\n   */\n  getWriteStream(): WriteStream {\n    if (!this.writeStream) {\n      fs.ensureFileSync(this.filePath);\n      this.writeStream = fs.createWriteStream(this.filePath, { flags: 'a' });\n    }\n    this.writeStream.on('error', () => {});\n    return this.writeStream;\n  }\n\n  /**\n   * Get readable stream of the logs file\n   * Used to download file using http server\n   */\n  getReadStream(): ReadStream {\n    fs.ensureFileSync(this.filePath);\n    const stream = fs.createReadStream(this.filePath);\n    stream.once('end', () => {\n      stream.destroy();\n      try {\n        this.analyticsEvents.get(TelemetryEvents.ProfilerLogDownloaded)(\n          this.instanceId,\n          this.getFileSize(),\n        );\n      } catch (e) {\n        // ignore analytics errors\n      }\n      // logFile.destroy();\n    });\n\n    return stream;\n  }\n\n  /**\n   * Get or create logs emitter to use on each 'monitor' event\n   */\n  getEmitter(): FileLogsEmitter {\n    if (!this.emitter) {\n      this.emitter = new FileLogsEmitter(this);\n    }\n\n    return this.emitter;\n  }\n\n  /**\n   * Generate file name\n   */\n  getFilename(): string {\n    return `${this.alias}-${this.startTime.getTime()}-${Date.now()}`;\n  }\n\n  getFileSize(): number {\n    const stats = fs.statSync(this.filePath);\n    return stats.size;\n  }\n\n  setAlias(alias: string) {\n    this.alias = alias;\n  }\n\n  addProfilerClient(id: string) {\n    this.clientObservers.set(id, id);\n    this.idleSince = 0;\n  }\n\n  removeProfilerClient(id: string) {\n    this.clientObservers.delete(id);\n\n    if (!this.clientObservers.size) {\n      this.idleSince = Date.now();\n\n      setTimeout(() => {\n        if (\n          this?.idleSince &&\n          Date.now() - this.idleSince >= PROFILER.logFileIdleThreshold\n        ) {\n          this.destroy();\n        }\n      }, PROFILER.logFileIdleThreshold);\n    }\n  }\n\n  /**\n   * Remove file and delete write stream after finish\n   */\n  destroy() {\n    try {\n      this.writeStream?.close();\n      this.writeStream = null;\n      const size = this.getFileSize();\n      fs.unlinkSync(this.filePath);\n\n      this.analyticsEvents.get(TelemetryEvents.ProfilerLogDeleted)(\n        this.instanceId,\n        size,\n      );\n    } catch (e) {\n      // ignore error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/monitor-settings.ts",
    "content": "import { IsString } from 'class-validator';\n\nexport class MonitorSettings {\n  @IsString()\n  logFileId: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/profiler.client.spec.ts",
    "content": "import { ProfilerClient } from 'src/modules/profiler/models/profiler.client';\nimport {\n  mockLogEmitter,\n  mockMonitorDataItem,\n  mockMonitorDataItemEmitted,\n  mockSocket,\n} from 'src/__mocks__';\nimport { ProfilerServerEvents } from 'src/modules/profiler/constants';\nimport { WsException } from '@nestjs/websockets';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\ndescribe('ProfilerClient', () => {\n  let profilerClient: ProfilerClient;\n\n  beforeEach(() => {\n    profilerClient = new ProfilerClient(mockSocket.id, mockSocket);\n  });\n\n  it('initialization', () => {\n    expect(profilerClient.id).toEqual(mockSocket.id);\n    expect(profilerClient['client']).toEqual(mockSocket);\n    expect(profilerClient['items']).toEqual([]);\n    expect(profilerClient['logsEmitters']).toEqual(new Map());\n    expect(profilerClient['debounce']).toBeInstanceOf(Function);\n  });\n\n  it('handleOnData', async () => {\n    profilerClient.addLogsEmitter(mockLogEmitter);\n    profilerClient.handleOnData(mockMonitorDataItem);\n    profilerClient.handleOnData(mockMonitorDataItem);\n    expect(profilerClient['items'].length).toEqual(2);\n    expect(mockLogEmitter.emit).not.toHaveBeenCalled();\n    await new Promise((res) => setTimeout(res, 100));\n    expect(mockLogEmitter.emit).toHaveBeenCalledWith([\n      mockMonitorDataItemEmitted,\n      mockMonitorDataItemEmitted,\n    ]);\n    expect(profilerClient['items'].length).toEqual(0);\n  });\n\n  it('handleOnDisconnect', () => {\n    profilerClient.handleOnDisconnect();\n    expect(mockSocket.emit).toHaveBeenCalledWith(\n      ProfilerServerEvents.Exception,\n      new WsException(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB),\n    );\n  });\n\n  it('addLogsEmitter', () => {\n    profilerClient.addLogsEmitter(mockLogEmitter);\n    expect(mockLogEmitter.addProfilerClient).toHaveBeenCalledWith(\n      profilerClient.id,\n    );\n    expect(profilerClient['logsEmitters'].size).toEqual(1);\n    profilerClient.addLogsEmitter(mockLogEmitter);\n    expect(mockLogEmitter.addProfilerClient).toHaveBeenCalledWith(\n      profilerClient.id,\n    );\n    expect(profilerClient['logsEmitters'].size).toEqual(1);\n  });\n\n  it('flushLogs + destroy', () => {\n    profilerClient.addLogsEmitter(mockLogEmitter);\n    profilerClient.flushLogs();\n    expect(mockLogEmitter.flushLogs).toHaveBeenCalled();\n    profilerClient.destroy();\n    expect(mockLogEmitter.removeProfilerClient).toHaveBeenCalledWith(\n      profilerClient.id,\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/profiler.client.ts",
    "content": "import { Socket } from 'socket.io';\nimport { debounce } from 'lodash';\nimport { WsException } from '@nestjs/websockets';\nimport { Logger } from '@nestjs/common';\nimport { ProfilerServerEvents } from 'src/modules/profiler/constants';\nimport { ILogsEmitter } from 'src/modules/profiler/interfaces/logs-emitter.interface';\nimport { IMonitorData } from 'src/modules/profiler/interfaces/monitor-data.interface';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class ProfilerClient {\n  private logger = new Logger('ProfilerClient');\n\n  public readonly id: string;\n\n  private readonly client: Socket;\n\n  private logsEmitters: Map<string, ILogsEmitter> = new Map();\n\n  private filters: any[];\n\n  private readonly debounce: any;\n\n  private items: any[];\n\n  constructor(id: string, client: Socket) {\n    this.id = id;\n    this.client = client;\n    this.items = [];\n    this.debounce = debounce(\n      () => {\n        if (this.items.length) {\n          this.logsEmitters.forEach((emitter) => {\n            emitter.emit(this.items);\n          });\n          this.items = [];\n        }\n      },\n      10,\n      {\n        maxWait: 50,\n      },\n    );\n  }\n\n  public handleOnData(payload: IMonitorData) {\n    const { time, args, source, database } = payload;\n\n    // If there's [ in the time, strip it out.\n    //\n    // There is a case of a timestamp coming with '[' on Alibaba\n    // Redis's monitor.\n    const newTime = time.split('[')[0];\n\n    this.items.push({\n      time: newTime,\n      args,\n      source,\n      database,\n    });\n\n    this.debounce();\n  }\n\n  public handleOnDisconnect() {\n    this.client.emit(\n      ProfilerServerEvents.Exception,\n      new WsException(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB),\n    );\n  }\n\n  public addLogsEmitter(emitter: ILogsEmitter) {\n    this.logsEmitters.set(emitter.id, emitter);\n    emitter.addProfilerClient(this.id);\n    this.logCurrentState();\n  }\n\n  async flushLogs() {\n    this.logsEmitters.forEach((emitter) => emitter.flushLogs());\n  }\n\n  public destroy() {\n    this.logsEmitters.forEach((emitter) =>\n      emitter.removeProfilerClient(this.id),\n    );\n  }\n\n  /**\n   * Logs useful information about current state for debug purposes\n   * @private\n   */\n  private logCurrentState() {\n    this.logger.debug(`Emitters: ${this.logsEmitters.size}`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/redis.observer.spec.ts",
    "content": "import { RedisObserver } from 'src/modules/profiler/models/redis.observer';\nimport { RedisObserverStatus } from 'src/modules/profiler/constants';\nimport {\n  mockClusterRedisClient,\n  mockMonitorDataItemEmitted,\n  mockProfilerClient,\n  mockRedisNoPermError,\n  mockRedisShardObserver,\n  mockSocket,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { ProfilerClient } from 'src/modules/profiler/models/profiler.client';\nimport { ReplyError } from 'src/models';\nimport {\n  ForbiddenException,\n  ServiceUnavailableException,\n} from '@nestjs/common';\nimport { IShardObserver } from 'src/modules/profiler/interfaces/shard-observer.interface';\n\nconst getRedisClientFn = jest.fn();\n\nconst NO_PERM_ERROR: ReplyError = {\n  ...mockRedisNoPermError,\n  command: 'MONITOR',\n};\n\ndescribe('RedisObserver', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let redisObserver: RedisObserver;\n\n  const mockClusterNode1 = standaloneClient;\n  const mockClusterNode2 = standaloneClient;\n  Object.assign(mockClusterNode1.options, {\n    ...standaloneClient.options,\n    host: 'localhost',\n    port: 5000,\n  });\n  Object.assign(mockClusterNode2.options, {\n    ...standaloneClient.options,\n    host: 'localhost',\n    port: 5001,\n  });\n\n  beforeEach(() => {\n    jest.resetAllMocks();\n    redisObserver = new RedisObserver();\n    getRedisClientFn.mockResolvedValue(standaloneClient);\n  });\n\n  it('initialization', () => {\n    expect(redisObserver['status']).toEqual(RedisObserverStatus.Empty);\n  });\n\n  describe('init', () => {\n    it('successfully init', async () => {\n      await new Promise((resolve) => {\n        redisObserver['connect'] = jest.fn();\n        redisObserver.init(getRedisClientFn);\n        expect(redisObserver['status']).toEqual(\n          RedisObserverStatus.Initializing,\n        );\n        redisObserver.on('connect', () => {\n          resolve(true);\n        });\n      });\n      expect(redisObserver['status']).toEqual(RedisObserverStatus.Connected);\n      expect(redisObserver['redis']).toEqual(standaloneClient);\n    });\n    it('init error due to redis connection', (done) => {\n      getRedisClientFn.mockRejectedValueOnce(new Error('error'));\n      redisObserver.init(getRedisClientFn);\n      redisObserver.on('connect_error', () => {\n        expect(redisObserver['status']).toEqual(RedisObserverStatus.Error);\n        expect(redisObserver['redis']).toEqual(undefined);\n        done();\n      });\n    });\n  });\n\n  describe('subscribe', () => {\n    beforeEach(() => {\n      redisObserver['connect'] = jest.fn();\n      redisObserver['shardsObservers'] = [\n        standaloneClient as unknown as IShardObserver,\n      ];\n    });\n\n    it('should subscribe to a standalone', async () => {\n      standaloneClient.call.mockResolvedValue('OK');\n\n      await redisObserver.init(getRedisClientFn);\n      await redisObserver.subscribe(mockProfilerClient);\n      redisObserver['status'] = RedisObserverStatus.Ready;\n      await redisObserver.subscribe(mockProfilerClient);\n\n      expect(redisObserver['shardsObservers'].length).toEqual(1);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(1);\n      expect(\n        redisObserver['profilerClientsListeners'].get(mockProfilerClient.id)\n          .length,\n      ).toEqual(2);\n\n      standaloneClient.emit(\n        'monitor',\n        ...Object.values(mockMonitorDataItemEmitted),\n      );\n      expect(mockProfilerClient['handleOnData']).toHaveBeenCalledWith({\n        ...mockMonitorDataItemEmitted,\n        shardOptions: standaloneClient.options,\n      });\n      expect(mockProfilerClient['handleOnData']).toHaveBeenCalledTimes(1);\n\n      standaloneClient.emit('end');\n      expect(mockProfilerClient['handleOnDisconnect']).toHaveBeenCalledTimes(1);\n    });\n\n    it('should subscribe to a cluster', async () => {\n      redisObserver['shardsObservers'] = [\n        mockClusterNode1 as unknown as IShardObserver,\n        mockClusterNode2 as unknown as IShardObserver,\n      ];\n      getRedisClientFn.mockResolvedValueOnce(clusterClient);\n      await redisObserver.init(getRedisClientFn);\n      await redisObserver.subscribe(mockProfilerClient);\n      redisObserver['status'] = RedisObserverStatus.Ready;\n      await redisObserver.subscribe(mockProfilerClient);\n      expect(redisObserver['connect']).toHaveBeenCalledTimes(2);\n      expect(redisObserver['shardsObservers'].length).toEqual(2);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(1);\n      expect(\n        redisObserver['profilerClientsListeners'].get(mockProfilerClient.id)\n          .length,\n      ).toEqual(4);\n\n      standaloneClient.emit(\n        'monitor',\n        ...Object.values(mockMonitorDataItemEmitted),\n      );\n      expect(mockProfilerClient['handleOnData']).toHaveBeenCalledWith({\n        ...mockMonitorDataItemEmitted,\n        shardOptions: { ...mockClusterNode1.options },\n      });\n      expect(mockProfilerClient['handleOnData']).toHaveBeenCalledWith({\n        ...mockMonitorDataItemEmitted,\n        shardOptions: { ...mockClusterNode2.options },\n      });\n      expect(mockProfilerClient['handleOnData']).toHaveBeenCalledTimes(2);\n\n      standaloneClient.emit('end');\n      expect(mockProfilerClient['handleOnDisconnect']).toHaveBeenCalledTimes(2);\n    });\n  });\n\n  describe('unsubscribe', () => {\n    let clearSpy;\n    let profilerClient1;\n    let profilerClient2;\n    let destroySpy1;\n    let destroySpy2;\n\n    beforeEach(async () => {\n      clearSpy = jest.spyOn(redisObserver, 'clear');\n      redisObserver['connect'] = jest.fn();\n      redisObserver['shardsObservers'] = [mockRedisShardObserver];\n      profilerClient1 = new ProfilerClient('1', mockSocket);\n      profilerClient2 = new ProfilerClient('2', mockSocket);\n      destroySpy1 = jest.spyOn(profilerClient1, 'destroy');\n      destroySpy2 = jest.spyOn(profilerClient2, 'destroy');\n\n      await redisObserver.init(getRedisClientFn);\n      await redisObserver.subscribe(profilerClient1);\n      await redisObserver.subscribe(profilerClient2);\n    });\n\n    it('unsubscribe', async () => {\n      expect(redisObserver['profilerClients'].size).toEqual(2);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(2);\n      redisObserver.unsubscribe('1');\n      expect(mockRedisShardObserver.removeListener).toHaveBeenCalledTimes(4);\n      expect(redisObserver['profilerClients'].size).toEqual(1);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(1);\n      expect(clearSpy).not.toHaveBeenCalled();\n\n      redisObserver.unsubscribe('2');\n      expect(mockRedisShardObserver.removeListener).toHaveBeenCalledTimes(8);\n      expect(redisObserver['profilerClients'].size).toEqual(0);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(0);\n      expect(clearSpy).toHaveBeenCalled();\n\n      expect(destroySpy1).not.toHaveBeenCalled();\n      expect(destroySpy2).not.toHaveBeenCalled();\n    });\n\n    it('disconnect', async () => {\n      expect(redisObserver['profilerClients'].size).toEqual(2);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(2);\n      redisObserver.disconnect('1');\n      expect(mockRedisShardObserver.removeListener).toHaveBeenCalledTimes(4);\n      expect(redisObserver['profilerClients'].size).toEqual(1);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(1);\n      expect(clearSpy).not.toHaveBeenCalled();\n\n      redisObserver.disconnect('2');\n      expect(mockRedisShardObserver.removeListener).toHaveBeenCalledTimes(8);\n      expect(redisObserver['profilerClients'].size).toEqual(0);\n      expect(redisObserver['profilerClientsListeners'].size).toEqual(0);\n      expect(clearSpy).toHaveBeenCalled();\n\n      expect(destroySpy1).toHaveBeenCalled();\n      expect(destroySpy2).toHaveBeenCalled();\n    });\n  });\n\n  describe('connect', () => {\n    beforeEach(async () => {\n      standaloneClient.call.mockResolvedValue('OK');\n      standaloneClient.monitor.mockReturnValue(mockRedisShardObserver);\n      standaloneClient.nodes = jest.fn().mockReturnValue([standaloneClient]);\n    });\n\n    it('connect to standalone', async () => {\n      await redisObserver.init(getRedisClientFn);\n      const profilerClient = new ProfilerClient('1', mockSocket);\n      await redisObserver.subscribe(profilerClient);\n      expect(redisObserver['shardsObservers']).toEqual([\n        mockRedisShardObserver,\n      ]);\n      expect(redisObserver['status']).toEqual(RedisObserverStatus.Ready);\n    });\n\n    it('connect fail due to NOPERM', (done) => {\n      standaloneClient.monitor.mockRejectedValueOnce(NO_PERM_ERROR);\n      redisObserver.init(getRedisClientFn);\n      redisObserver.on('connect_error', (e) => {\n        expect(redisObserver['shardsObservers']).toEqual([]);\n        expect(redisObserver['status']).toEqual(RedisObserverStatus.Error);\n        expect(e).toBeInstanceOf(ForbiddenException);\n        done();\n      });\n    });\n\n    it('connect fail due an error', (done) => {\n      standaloneClient.monitor.mockRejectedValueOnce(new Error('some error'));\n      redisObserver.init(getRedisClientFn);\n      redisObserver.on('connect_error', (e) => {\n        expect(e).toBeInstanceOf(ServiceUnavailableException);\n        expect(redisObserver['shardsObservers']).toEqual([]);\n        expect(redisObserver['status']).toEqual(RedisObserverStatus.Error);\n        done();\n      });\n    });\n\n    it('connect to cluster', async () => {\n      getRedisClientFn.mockResolvedValue(clusterClient);\n      clusterClient.nodes = jest\n        .fn()\n        .mockReturnValue([mockClusterNode1, mockClusterNode2]);\n      await redisObserver.init(getRedisClientFn);\n\n      redisObserver['redis'] = clusterClient;\n      expect(redisObserver['shardsObservers']).toEqual([\n        mockRedisShardObserver,\n        mockRedisShardObserver,\n      ]);\n      expect(redisObserver['status']).toEqual(RedisObserverStatus.Ready);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/models/redis.observer.ts",
    "content": "import {\n  ForbiddenException,\n  Logger,\n  ServiceUnavailableException,\n} from '@nestjs/common';\nimport { RedisErrorCodes } from 'src/constants';\nimport { ProfilerClient } from 'src/modules/profiler/models/profiler.client';\nimport { RedisObserverStatus } from 'src/modules/profiler/constants';\nimport { IShardObserver } from 'src/modules/profiler/interfaces/shard-observer.interface';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class RedisObserver extends EventEmitter2 {\n  private logger = new Logger('RedisObserver');\n\n  private redis: RedisClient;\n\n  private profilerClients: Map<string, ProfilerClient> = new Map();\n\n  private profilerClientsListeners: Map<string, any[]> = new Map();\n\n  private shardsObservers: IShardObserver[] = [];\n\n  public status: RedisObserverStatus;\n\n  constructor() {\n    super();\n    this.status = RedisObserverStatus.Empty;\n  }\n\n  init(func: () => Promise<RedisClient>) {\n    this.status = RedisObserverStatus.Initializing;\n\n    return func()\n      .then((redis) => {\n        this.redis = redis;\n        this.status = RedisObserverStatus.Connected;\n      })\n      .then(() => this.connect())\n      .then(() => {\n        this.emit('connect');\n        return Promise.resolve();\n      })\n      .catch((err) => {\n        this.status = RedisObserverStatus.Error;\n        this.emit('connect_error', err);\n        // todo: rethink error handling for profiler\n        // prevent unhandled rejection\n        // return Promise.reject(err);\n      });\n  }\n\n  /**\n   * Create \"monitor\" clients for each shard if not exists\n   * Subscribe profiler client to each each shard\n   * Ignore when profiler client with such id already exists\n   * @param profilerClient\n   */\n  public async subscribe(profilerClient: ProfilerClient) {\n    if (this.status !== RedisObserverStatus.Ready) {\n      await this.connect();\n    }\n\n    if (this.profilerClients.has(profilerClient.id)) {\n      return;\n    }\n\n    if (!this.profilerClientsListeners.get(profilerClient.id)) {\n      this.profilerClientsListeners.set(profilerClient.id, []);\n    }\n\n    const profilerListeners = this.profilerClientsListeners.get(\n      profilerClient.id,\n    );\n\n    this.shardsObservers.forEach((observer) => {\n      const monitorListenerFn = (time, args, source, database) => {\n        profilerClient.handleOnData({\n          time,\n          args,\n          database,\n          source,\n          shardOptions: observer.options,\n        });\n      };\n      const endListenerFn = () => {\n        profilerClient.handleOnDisconnect();\n        this.clear();\n      };\n\n      observer.on('monitor', monitorListenerFn);\n      observer.on('end', endListenerFn);\n\n      profilerListeners.push(monitorListenerFn, endListenerFn);\n      this.logger.debug(\n        `Subscribed to shard observer. Current listeners: ${observer.listenerCount('monitor')}`,\n      );\n    });\n    this.profilerClients.set(profilerClient.id, profilerClient);\n\n    this.logger.debug(`Profiler Client with id:${profilerClient.id} was added`);\n    this.logCurrentState();\n  }\n\n  public removeShardsListeners(profilerClientId: string) {\n    this.shardsObservers.forEach((observer) => {\n      (this.profilerClientsListeners.get(profilerClientId) || []).forEach(\n        (listener) => {\n          observer.removeListener('monitor', listener);\n          observer.removeListener('end', listener);\n        },\n      );\n\n      this.logger.debug(\n        `Unsubscribed from from shard observer. Current listeners: ${observer.listenerCount('monitor')}`,\n      );\n    });\n  }\n\n  public unsubscribe(id: string) {\n    this.removeShardsListeners(id);\n    this.profilerClients.delete(id);\n    this.profilerClientsListeners.delete(id);\n    if (this.profilerClients.size === 0) {\n      this.clear();\n    }\n\n    this.logger.debug(`Profiler Client with id:${id} was unsubscribed`);\n    this.logCurrentState();\n  }\n\n  public disconnect(id: string) {\n    this.removeShardsListeners(id);\n    const profilerClient = this.profilerClients.get(id);\n    if (profilerClient) {\n      profilerClient.destroy();\n    }\n    this.profilerClients.delete(id);\n    this.profilerClientsListeners.delete(id);\n    if (this.profilerClients.size === 0) {\n      this.clear();\n    }\n\n    this.logger.debug(`Profiler Client with id:${id} was disconnected`);\n    this.logCurrentState();\n  }\n\n  /**\n   * Logs useful inforation about current state for debug purposes\n   * @private\n   */\n  private logCurrentState() {\n    this.logger.debug(\n      `Status: ${this.status}; Shards: ${this.shardsObservers.length}; Listeners: ${this.getProfilerClientsSize()}`,\n    );\n  }\n\n  public clear() {\n    this.profilerClients.clear();\n    this.shardsObservers.forEach((observer) => {\n      observer.removeAllListeners('monitor');\n      observer.removeAllListeners('end');\n      observer.disconnect();\n    });\n    this.shardsObservers = [];\n    this.status = RedisObserverStatus.End;\n  }\n\n  /**\n   * Return number of profilerClients for current Redis Observer instance\n   */\n  public getProfilerClientsSize(): number {\n    return this.profilerClients.size;\n  }\n\n  /**\n   * Create shard observer for each Redis shard to receive \"monitor\" data\n   * @private\n   */\n  private async connect(): Promise<void> {\n    try {\n      this.shardsObservers = await Promise.all(\n        (await this.redis.nodes()).map(RedisObserver.createShardObserver),\n      );\n\n      this.shardsObservers.forEach((observer) => {\n        observer.on('error', (e) => {\n          this.logger.error('Error on shard observer', e);\n        });\n      });\n\n      this.status = RedisObserverStatus.Ready;\n    } catch (error) {\n      this.status = RedisObserverStatus.Error;\n\n      if (error?.message?.includes(RedisErrorCodes.NoPermission)) {\n        throw new ForbiddenException(error.message);\n      }\n\n      throw new ServiceUnavailableException(\n        ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB,\n      );\n    }\n  }\n\n  /**\n   * Create and return shard observer using IORedis common client\n   * @param redis\n   */\n  static async createShardObserver(\n    redis: RedisClient,\n  ): Promise<IShardObserver> {\n    return (await redis.monitor()) as IShardObserver;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/monitor.service.spec.ts",
    "content": "xdescribe('dummy', () => {\n  it('dummy', () => {});\n});\n\n// import { ServiceUnavailableException } from '@nestjs/common';\n// import { Test, TestingModule } from '@nestjs/testing';\n// import { v4 as uuidv4 } from 'uuid';\n// import { mockClientMonitorObserver, mockMonitorObserver } from 'src/__mocks__/monitor';\n// import ERROR_MESSAGES from 'src/constants/error-messages';\n// import { RedisService } from 'src/modules/redis/redis.service';\n// import { mockRedisClientInstance } from 'src/modules/redis/redis-consumer.abstract.service.spec';\n// import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service';\n// import { ProfilerService } from './monitor.service';\n// import { RedisMonitorClient } from './helpers/monitor-observer';\n//\n// jest.mock('./helpers/monitor-observer');\n//\n// describe('MonitorService', () => {\n//   let service;\n//   let redisService;\n//   let instancesBusinessService;\n//\n//   beforeEach(async () => {\n//     const module: TestingModule = await Test.createTestingModule({\n//       providers: [\n//         ProfilerService,\n//         {\n//           provide: RedisService,\n//           useFactory: () => ({\n//             getClientInstance: jest.fn(),\n//             isClientConnected: jest.fn(),\n//           }),\n//         },\n//         {\n//           provide: InstancesBusinessService,\n//           useFactory: () => ({\n//             connectToInstance: jest.fn(),\n//           }),\n//         },\n//       ],\n//     }).compile();\n//\n//     service = module.get<ProfilerService>(ProfilerService);\n//     redisService = await module.get<RedisService>(RedisService);\n//     instancesBusinessService = await module.get<InstancesBusinessService>(InstancesBusinessService);\n//   });\n//\n//   describe('addListenerForInstance', () => {\n//     let getRedisClientForInstance;\n//     beforeEach(() => {\n//       getRedisClientForInstance = jest.spyOn(service, 'getRedisClientForInstance');\n//       service.monitorObservers = {};\n//     });\n//\n//     it('should use exist redis client and create new monitor observer', async () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       redisService.getClientInstance.mockReturnValue(mockRedisClientInstance);\n//       redisService.isClientConnected.mockReturnValue(true);\n//\n//       await service.addListenerForInstance(instanceId, mockClientMonitorObserver);\n//\n//       expect(getRedisClientForInstance).toHaveBeenCalledWith(instanceId);\n//       expect(RedisMonitorClient).toHaveBeenCalled();\n//       expect(service.monitorObservers[instanceId]).toBeDefined();\n//     });\n//     it('should use exist monitor observer for instance', async () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       service.monitorObservers = { [instanceId]: { ...mockMonitorObserver, status: 'ready' } };\n//\n//       await service.addListenerForInstance(instanceId, mockClientMonitorObserver);\n//       await service.addListenerForInstance(instanceId, mockClientMonitorObserver);\n//\n//       expect(getRedisClientForInstance).not.toHaveBeenCalled();\n//       expect(Object.keys(service.monitorObservers).length).toEqual(1);\n//     });\n//     it('should recreate exist monitor observer', async () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       service.monitorObservers = { [instanceId]: { ...mockMonitorObserver, status: 'end' } };\n//       redisService.getClientInstance.mockReturnValue(mockRedisClientInstance);\n//       redisService.isClientConnected.mockReturnValue(true);\n//\n//       await service.addListenerForInstance(instanceId, mockClientMonitorObserver);\n//\n//       expect(RedisMonitorClient).toHaveBeenCalled();\n//       expect(getRedisClientForInstance).toHaveBeenCalled();\n//       expect(Object.keys(service.monitorObservers).length).toEqual(1);\n//     });\n//     it('should recreate redis client', async () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       redisService.getClientInstance.mockReturnValue(mockRedisClientInstance);\n//       redisService.isClientConnected.mockReturnValue(false);\n//       instancesBusinessService.connectToInstance.mockResolvedValue(mockRedisClientInstance);\n//\n//       await service.addListenerForInstance(instanceId, mockClientMonitorObserver);\n//\n//       expect(instancesBusinessService.connectToInstance).toHaveBeenCalled();\n//     });\n//     it('should throw timeout exception on create redis client', async () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       redisService.getClientInstance.mockReturnValue(null);\n//       instancesBusinessService.connectToInstance = jest.fn()\n//         .mockReturnValue(new Promise(() => {}));\n//\n//       try {\n//         await service.addListenerForInstance(instanceId, mockClientMonitorObserver);\n//       } catch (error) {\n//         expect(error).toEqual(new ServiceUnavailableException(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB));\n//       }\n//     });\n//   });\n//   describe('removeListenerFromInstance', () => {\n//     beforeEach(() => {\n//       service.monitorObservers = {};\n//     });\n//\n//     it('should unsubscribe listeners from monitor observer', async () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       const listenerId = uuidv4();\n//       const monitorObserver = { ...mockMonitorObserver, status: 'ready', unsubscribe: jest.fn() };\n//       service.monitorObservers = { [instanceId]: monitorObserver };\n//\n//       service.removeListenerFromInstance(instanceId, listenerId);\n//\n//       expect(monitorObserver.unsubscribe).toHaveBeenCalledWith(listenerId);\n//     });\n//     it('should be ignored if monitor observer does not exist for instance', () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       const listenerId = uuidv4();\n//       const monitorObserver = { ...mockMonitorObserver, status: 'ready', unsubscribe: jest.fn() };\n//       service.monitorObservers = { [instanceId]: monitorObserver };\n//\n//       service.removeListenerFromInstance(uuidv4(), listenerId);\n//\n//       expect(monitorObserver.unsubscribe).not.toHaveBeenCalled();\n//     });\n//   });\n//\n//   describe('handleInstanceDeletedEvent', () => {\n//     beforeEach(() => {\n//       service.monitorObservers = {};\n//     });\n//\n//     it('should clear exist monitor observer fro instance', () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       const monitorObserver = { ...mockMonitorObserver, status: 'ready', clear: jest.fn() };\n//       service.monitorObservers = { [instanceId]: monitorObserver };\n//\n//       service.handleInstanceDeletedEvent(instanceId);\n//\n//       expect(monitorObserver.clear).toHaveBeenCalled();\n//       expect(service.monitorObservers[instanceId]).not.toBeDefined();\n//     });\n//     it('should be ignored if monitor observer does not exist for instance', () => {\n//       const { instanceId } = mockRedisClientInstance;\n//       const monitorObserver = { ...mockMonitorObserver, status: 'ready', clear: jest.fn() };\n//       service.monitorObservers = { [instanceId]: monitorObserver };\n//\n//       service.handleInstanceDeletedEvent(uuidv4());\n//\n//       expect(monitorObserver.clear).not.toHaveBeenCalled();\n//       expect(service.monitorObservers[instanceId]).toBeDefined();\n//     });\n//   });\n// });\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/profiler-analytics.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { RedisError, ReplyError } from 'src/models';\nimport { SessionMetadata } from 'src/common/models';\n\nexport interface IExecResult {\n  response: any;\n  status: CommandExecutionStatus;\n  error?: RedisError | ReplyError | Error;\n}\n\n@Injectable()\nexport class ProfilerAnalyticsService extends TelemetryBaseService {\n  private events: Map<TelemetryEvents, Function> = new Map();\n\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n    this.events.set(\n      TelemetryEvents.ProfilerLogDownloaded,\n      this.sendLogDownloaded.bind(this),\n    );\n    this.events.set(\n      TelemetryEvents.ProfilerLogDeleted,\n      this.sendLogDeleted.bind(this),\n    );\n  }\n\n  sendLogDeleted(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    fileSizeBytes: number,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.ProfilerLogDeleted, {\n        databaseId,\n        fileSizeBytes,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendLogDownloaded(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    fileSizeBytes: number,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.ProfilerLogDownloaded, {\n        databaseId,\n        fileSizeBytes,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  getEventsEmitters(): Map<TelemetryEvents, Function> {\n    return this.events;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/profiler.controller.ts",
    "content": "import { Response } from 'express';\nimport { Controller, Get, Param, Res } from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\n\n@ApiTags('Profiler')\n@Controller('profiler')\nexport class ProfilerController {\n  constructor(private logFileProvider: LogFileProvider) {}\n\n  @ApiEndpoint({\n    description: 'Endpoint do download profiler log file',\n    statusCode: 200,\n  })\n  @Get('/logs/:id')\n  async downloadLogsFile(@Res() res: Response, @Param('id') id: string) {\n    const { stream, filename } = await this.logFileProvider.getDownloadData(id);\n\n    res.setHeader('Content-Type', 'application/octet-stream');\n    res.setHeader(\n      'Content-Disposition',\n      `attachment;filename=\"${filename}.txt\"`,\n    );\n    res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');\n\n    stream.on('error', () => res.status(404).send()).pipe(res);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/profiler.gateway.ts",
    "content": "import { get } from 'lodash';\nimport { Socket, Server } from 'socket.io';\nimport {\n  ConnectedSocket,\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n  SubscribeMessage,\n  WebSocketGateway,\n  WebSocketServer,\n  WsException,\n} from '@nestjs/websockets';\nimport { Body, Logger } from '@nestjs/common';\nimport { MonitorSettings } from 'src/modules/profiler/models/monitor-settings';\nimport { ProfilerClientEvents } from 'src/modules/profiler/constants';\nimport { ProfilerService } from 'src/modules/profiler/profiler.service';\nimport { WSSessionMetadata } from 'src/modules/auth/session-metadata/decorators/ws-session-metadata.decorator';\nimport config, { Config } from 'src/utils/config';\nimport { SessionMetadata } from 'src/common/models';\n\nconst SOCKETS_CONFIG = config.get('sockets') as Config['sockets'];\n\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  namespace: 'monitor',\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n  serveClient: SOCKETS_CONFIG.serveClient,\n})\nexport class ProfilerGateway\n  implements OnGatewayConnection, OnGatewayDisconnect\n{\n  @WebSocketServer() wss: Server;\n\n  private logger: Logger = new Logger('MonitorGateway');\n\n  constructor(private service: ProfilerService) {}\n\n  @SubscribeMessage(ProfilerClientEvents.Monitor)\n  async monitor(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @ConnectedSocket() client: Socket,\n    @Body() settings: MonitorSettings = null,\n  ): Promise<any> {\n    try {\n      await this.service.addListenerForInstance(\n        sessionMetadata,\n        ProfilerGateway.getInstanceId(client),\n        client,\n        settings,\n      );\n      return { status: 'ok' };\n    } catch (error) {\n      this.logger.error('Unable to add listener', error);\n      throw new WsException(error);\n    }\n  }\n\n  @SubscribeMessage(ProfilerClientEvents.Pause)\n  async pause(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @ConnectedSocket() client: Socket,\n  ): Promise<any> {\n    try {\n      await this.service.removeListenerFromInstance(\n        ProfilerGateway.getInstanceId(client),\n        client.id,\n      );\n      return { status: 'ok' };\n    } catch (error) {\n      this.logger.error('Unable to pause monitor', error);\n      throw new WsException(error);\n    }\n  }\n\n  @SubscribeMessage(ProfilerClientEvents.FlushLogs)\n  async flushLogs(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @ConnectedSocket() client: Socket,\n  ): Promise<any> {\n    try {\n      await this.service.flushLogs(client.id);\n      return { status: 'ok' };\n    } catch (error) {\n      this.logger.error('Unable to flush logs', error);\n      throw new WsException(error);\n    }\n  }\n\n  async handleConnection(client: Socket): Promise<void> {\n    const instanceId = ProfilerGateway.getInstanceId(client);\n    this.logger.debug(\n      `Client connected: ${client.id}, instanceId: ${instanceId}`,\n    );\n  }\n\n  async handleDisconnect(client: Socket): Promise<void> {\n    const instanceId = ProfilerGateway.getInstanceId(client);\n    await this.service.disconnectListenerFromInstance(instanceId, client.id);\n    this.logger.debug(\n      `Client disconnected: ${client.id}, instanceId: ${instanceId}`,\n    );\n  }\n\n  static getInstanceId(client: Socket): string {\n    return get(client, 'handshake.query.instanceId') as string;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/profiler.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport { ProfilerController } from 'src/modules/profiler/profiler.controller';\nimport { RedisObserverProvider } from 'src/modules/profiler/providers/redis-observer.provider';\nimport { ProfilerClientProvider } from 'src/modules/profiler/providers/profiler-client.provider';\nimport { ProfilerAnalyticsService } from 'src/modules/profiler/profiler-analytics.service';\nimport { ProfilerGateway } from './profiler.gateway';\nimport { ProfilerService } from './profiler.service';\n\n@Module({\n  providers: [\n    ProfilerAnalyticsService,\n    RedisObserverProvider,\n    ProfilerClientProvider,\n    LogFileProvider,\n    ProfilerGateway,\n    ProfilerService,\n  ],\n  controllers: [ProfilerController],\n  exports: [LogFileProvider],\n})\nexport class ProfilerModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/profiler.service.ts",
    "content": "import { Socket } from 'socket.io';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { OnEvent } from '@nestjs/event-emitter';\nimport { AppRedisInstanceEvents } from 'src/constants';\nimport { MonitorSettings } from 'src/modules/profiler/models/monitor-settings';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport { RedisObserverProvider } from 'src/modules/profiler/providers/redis-observer.provider';\nimport { ProfilerClientProvider } from 'src/modules/profiler/providers/profiler-client.provider';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class ProfilerService {\n  private logger = new Logger('ProfilerService');\n\n  constructor(\n    private logFileProvider: LogFileProvider,\n    private redisObserverProvider: RedisObserverProvider,\n    private profilerClientProvider: ProfilerClientProvider,\n  ) {}\n\n  /**\n   * Create or use existing user client to send monitor data from redis client to the user\n   * We are storing user clients to have a possibility to \"pause\" logs without disconnecting\n   *\n   * @param sessionMetadata\n   * @param instanceId\n   * @param client\n   * @param settings\n   */\n  async addListenerForInstance(\n    sessionMetadata: SessionMetadata,\n    instanceId: string,\n    client: Socket,\n    settings: MonitorSettings = null,\n  ) {\n    this.logger.debug(\n      `Add listener for instance: ${instanceId}.`,\n      sessionMetadata,\n    );\n\n    const profilerClient = await this.profilerClientProvider.getOrCreateClient(\n      sessionMetadata,\n      instanceId,\n      client,\n      settings,\n    );\n    const monitorObserver =\n      await this.redisObserverProvider.getOrCreateObserver(\n        sessionMetadata,\n        instanceId,\n      );\n    await monitorObserver.subscribe(profilerClient);\n  }\n\n  /**\n   * Simply remove Profiler Client from the clients list of particular Redis Observer\n   * Basically used to remove listener that triggered by user action, e.g. \"pause\" action\n   * @param instanceId\n   * @param listenerId\n   */\n  async removeListenerFromInstance(instanceId: string, listenerId: string) {\n    this.logger.debug(`Remove listener from instance: ${instanceId}.`);\n    const redisObserver =\n      await this.redisObserverProvider.getObserver(instanceId);\n    if (redisObserver) {\n      redisObserver.unsubscribe(listenerId);\n    }\n  }\n\n  /**\n   * Remove Profiler Client from clients list of the particular Redis Observer\n   * Beside that under the hood will be triggered force remove of emitters, files, etc. after some time threshold\n   * Used when for sme reason socket connection between frontend and backend was lost\n   * @param instanceId\n   * @param listenerId\n   */\n  async disconnectListenerFromInstance(instanceId: string, listenerId: string) {\n    this.logger.debug(`Disconnect listener from instance: ${instanceId}.`);\n    const redisObserver =\n      await this.redisObserverProvider.getObserver(instanceId);\n    if (redisObserver) {\n      redisObserver.disconnect(listenerId);\n    }\n  }\n\n  /**\n   * Flush all persistent logs like FileLog\n   * Trigger by user action\n   * @param listenerId\n   */\n  async flushLogs(listenerId: string) {\n    this.logger.debug(`Flush logs for client ${listenerId}.`);\n    const profilerClient =\n      await this.profilerClientProvider.getClient(listenerId);\n    if (profilerClient) {\n      await profilerClient.flushLogs();\n    }\n  }\n\n  @OnEvent(AppRedisInstanceEvents.Deleted)\n  async handleInstanceDeletedEvent(instanceId: string) {\n    this.logger.debug(\n      `Handle instance deleted event. instance: ${instanceId}.`,\n    );\n    try {\n      const redisObserver =\n        await this.redisObserverProvider.getObserver(instanceId);\n      if (redisObserver) {\n        redisObserver.clear();\n        await this.redisObserverProvider.removeObserver(instanceId);\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/providers/log-file.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport {\n  mockLogFile,\n  mockProfilerAnalyticsEvents,\n  mockProfilerAnalyticsService,\n  mockDatabase,\n} from 'src/__mocks__';\nimport { ProfilerAnalyticsService } from 'src/modules/profiler/profiler-analytics.service';\nimport { NotFoundException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ReadStream } from 'fs';\n\ndescribe('LogFileProvider', () => {\n  let service: LogFileProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LogFileProvider,\n        {\n          provide: ProfilerAnalyticsService,\n          useFactory: () => mockProfilerAnalyticsService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(LogFileProvider);\n  });\n\n  it('getOrCreate', async () => {\n    const logFile1 = await service.getOrCreate(\n      mockLogFile.instanceId,\n      mockLogFile.id,\n    );\n    expect(service['profilerLogFiles'].size).toEqual(1);\n    expect(logFile1['analyticsEvents']).toEqual(mockProfilerAnalyticsEvents);\n\n    const logFile2 = await service.getOrCreate(\n      mockDatabase.id,\n      mockDatabase.id,\n    );\n    expect(service['profilerLogFiles'].size).toEqual(2);\n\n    const logFile22 = await service.getOrCreate(\n      mockDatabase.id,\n      mockDatabase.id,\n    );\n    expect(service['profilerLogFiles'].size).toEqual(2);\n    expect(logFile2).toEqual(logFile22);\n  });\n\n  it('get', async () => {\n    const logFile1 = await service.getOrCreate(\n      mockLogFile.instanceId,\n      mockLogFile.id,\n    );\n    expect(service['profilerLogFiles'].size).toEqual(1);\n\n    const logFile2 = await service.get(mockLogFile.id);\n    expect(logFile2).toEqual(logFile1);\n  });\n\n  it('should throw 404 error', async () => {\n    try {\n      await service.get('notExisting');\n      fail();\n    } catch (e) {\n      expect(e).toBeInstanceOf(NotFoundException);\n      expect(e.message).toEqual(ERROR_MESSAGES.PROFILER_LOG_FILE_NOT_FOUND);\n    }\n  });\n\n  it('getDownloadData', async () => {\n    const logFile1 = await service.getOrCreate(\n      mockLogFile.instanceId,\n      mockLogFile.id,\n    );\n    const { stream, filename } = await service.getDownloadData(logFile1.id);\n    expect(stream).toBeInstanceOf(ReadStream);\n    expect(filename).toMatch(\n      `${logFile1['alias']}-${logFile1['startTime'].getTime()}-`,\n    );\n  });\n\n  it('onModuleDestroy', async () => {\n    service['profilerLogFiles'].set(mockLogFile.id, mockLogFile);\n\n    expect(mockLogFile.destroy).not.toHaveBeenCalled();\n\n    service.onModuleDestroy();\n    expect(mockLogFile.destroy).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/providers/log-file.provider.ts",
    "content": "import { ReadStream } from 'fs';\nimport { Injectable, NotFoundException, OnModuleDestroy } from '@nestjs/common';\nimport { LogFile } from 'src/modules/profiler/models/log-file';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ProfilerAnalyticsService } from 'src/modules/profiler/profiler-analytics.service';\n\n@Injectable()\nexport class LogFileProvider implements OnModuleDestroy {\n  private profilerLogFiles: Map<string, LogFile> = new Map();\n\n  constructor(private analyticsService: ProfilerAnalyticsService) {}\n\n  /**\n   * Get or create Profiler Log File to work with\n   * @param instanceId\n   * @param id\n   */\n  getOrCreate(instanceId: string, id: string): LogFile {\n    if (!this.profilerLogFiles.has(id)) {\n      this.profilerLogFiles.set(\n        id,\n        new LogFile(instanceId, id, this.analyticsService.getEventsEmitters()),\n      );\n    }\n\n    return this.profilerLogFiles.get(id);\n  }\n\n  /**\n   * Get Profiler Log File or throw an error\n   * @param id\n   */\n  get(id: string): LogFile {\n    if (!this.profilerLogFiles.has(id)) {\n      throw new NotFoundException(ERROR_MESSAGES.PROFILER_LOG_FILE_NOT_FOUND);\n    }\n\n    return this.profilerLogFiles.get(id);\n  }\n\n  /**\n   * Get ReadableStream for download and filename\n   * Delete file after download finished\n   * @param id\n   */\n  async getDownloadData(id): Promise<{ stream: ReadStream; filename: string }> {\n    const logFile = await this.get(id);\n    const stream = await logFile.getReadStream();\n\n    return { stream, filename: logFile.getFilename() };\n  }\n\n  onModuleDestroy() {\n    this.profilerLogFiles.forEach((logFile) => {\n      try {\n        logFile.destroy();\n      } catch (e) {\n        // process other files on error\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/providers/profiler-client.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport {\n  mockDatabaseService,\n  mockLogFile,\n  mockLogFileProvider,\n  mockMonitorSettings,\n  mockSessionMetadata,\n  mockSocket,\n  MockType,\n} from 'src/__mocks__';\nimport { ProfilerClientProvider } from 'src/modules/profiler/providers/profiler-client.provider';\nimport { DatabaseService } from 'src/modules/database/database.service';\n\ndescribe('ProfilerClientProvider', () => {\n  let service: ProfilerClientProvider;\n  let logFileProvider: MockType<LogFileProvider>;\n  let databaseService: MockType<DatabaseService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ProfilerClientProvider,\n        {\n          provide: LogFileProvider,\n          useFactory: () => mockLogFileProvider,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(ProfilerClientProvider);\n    logFileProvider = await module.get(LogFileProvider);\n    databaseService = await module.get(DatabaseService);\n\n    logFileProvider.getOrCreate.mockReturnValue(mockLogFile);\n  });\n\n  it('getOrCreateClient', async () => {\n    await service.getOrCreateClient(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n      mockSocket,\n      null,\n    );\n\n    expect(service['profilerClients'].size).toEqual(1);\n    expect(databaseService.get).not.toHaveBeenCalled();\n    expect(logFileProvider.getOrCreate).not.toHaveBeenCalled();\n\n    await service.getOrCreateClient(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n      { ...mockSocket, id: '2' },\n      mockMonitorSettings,\n    );\n\n    expect(service['profilerClients'].size).toEqual(2);\n    expect(databaseService.get).toHaveBeenCalled();\n    expect(logFileProvider.getOrCreate).toHaveBeenCalled();\n  });\n\n  it('getClient', async () => {\n    const profilerClient = await service.getOrCreateClient(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n      mockSocket,\n      null,\n    );\n\n    expect(await service.getClient(profilerClient.id)).toEqual(profilerClient);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/providers/profiler-client.provider.ts",
    "content": "import { get } from 'lodash';\nimport { Socket } from 'socket.io';\nimport { Injectable } from '@nestjs/common';\nimport { ProfilerClient } from 'src/modules/profiler/models/profiler.client';\nimport { ClientLogsEmitter } from 'src/modules/profiler/emitters/client.logs-emitter';\nimport { MonitorSettings } from 'src/modules/profiler/models/monitor-settings';\nimport { LogFileProvider } from 'src/modules/profiler/providers/log-file.provider';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class ProfilerClientProvider {\n  private profilerClients: Map<string, ProfilerClient> = new Map();\n\n  constructor(\n    private logFileProvider: LogFileProvider,\n    private databaseService: DatabaseService,\n  ) {}\n\n  async getOrCreateClient(\n    sessionMetadata: SessionMetadata,\n    instanceId: string,\n    socket: Socket,\n    settings: MonitorSettings,\n  ): Promise<ProfilerClient> {\n    if (!this.profilerClients.has(socket.id)) {\n      const clientObserver = new ProfilerClient(socket.id, socket);\n      this.profilerClients.set(socket.id, clientObserver);\n\n      clientObserver.addLogsEmitter(new ClientLogsEmitter(socket));\n\n      if (settings?.logFileId) {\n        const profilerLogFile = this.logFileProvider.getOrCreate(\n          instanceId,\n          settings.logFileId,\n        );\n\n        // set database alias as part of the log file name\n        const alias = (\n          await this.databaseService.get(\n            sessionMetadata,\n            get(socket, 'handshake.query.instanceId') as string,\n          )\n        ).name;\n        profilerLogFile.setAlias(alias);\n\n        clientObserver.addLogsEmitter(await profilerLogFile.getEmitter());\n      }\n\n      this.profilerClients.set(socket.id, clientObserver);\n    }\n\n    return this.profilerClients.get(socket.id);\n  }\n\n  async getClient(id: string) {\n    return this.profilerClients.get(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/providers/redis-observer.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockDatabaseClientFactory,\n  mockLogFile,\n  mockRedisShardObserver,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { RedisObserverProvider } from 'src/modules/profiler/providers/redis-observer.provider';\nimport { RedisObserverStatus } from 'src/modules/profiler/constants';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('RedisObserverProvider', () => {\n  const client = mockStandaloneRedisClient;\n  let service: RedisObserverProvider;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RedisObserverProvider,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(RedisObserverProvider);\n\n    client.call.mockResolvedValue('OK');\n    client.monitor.mockReturnValue(mockRedisShardObserver);\n  });\n\n  it('getOrCreateObserver new observer', async () => {\n    const redisObserver = await service.getOrCreateObserver(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n    );\n\n    expect(redisObserver['redis']).toEqual(client);\n    expect(service['redisObservers'].size).toEqual(1);\n\n    expect(await service.getObserver(mockLogFile.instanceId)).toEqual(\n      redisObserver,\n    );\n    await service.removeObserver(mockLogFile.instanceId);\n    expect(service['redisObservers'].size).toEqual(0);\n  });\n\n  it('getOrCreateObserver check statuses', async () => {\n    const redisObserver = await service.getOrCreateObserver(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n    );\n\n    redisObserver['init'] = jest.fn().mockResolvedValue(Promise.resolve());\n    expect(redisObserver['redis']).toEqual(client);\n    expect(redisObserver['status']).toEqual(RedisObserverStatus.Ready);\n\n    const promise = service.getOrCreateObserver(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n    );\n    redisObserver.emit('connect');\n    const redisObserver2 = await promise;\n    expect(redisObserver).toEqual(redisObserver2);\n\n    redisObserver['status'] = RedisObserverStatus.Ready;\n    const redisObserver3 = await service.getOrCreateObserver(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n    );\n    expect(redisObserver).toEqual(redisObserver3);\n\n    redisObserver['status'] = RedisObserverStatus.Error;\n    const promise2 = service.getOrCreateObserver(\n      mockSessionMetadata,\n      mockLogFile.instanceId,\n    );\n    redisObserver.emit('connect');\n    const redisObserver4 = await promise2;\n    expect(redisObserver).toEqual(redisObserver4);\n\n    try {\n      redisObserver['status'] = RedisObserverStatus.Empty;\n      const promise3 = service.getOrCreateObserver(\n        mockSessionMetadata,\n        mockLogFile.instanceId,\n      );\n      redisObserver.emit('connect_error', new Error('error'));\n      await promise3;\n      fail();\n    } catch (e) {\n      expect(e.message).toEqual('error');\n    }\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts",
    "content": "import {\n  Injectable,\n  Logger,\n  ServiceUnavailableException,\n} from '@nestjs/common';\nimport { RedisObserver } from 'src/modules/profiler/models/redis.observer';\nimport { RedisObserverStatus } from 'src/modules/profiler/constants';\nimport { withTimeout } from 'src/utils/promise-with-timeout';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport config, { Config } from 'src/utils/config';\nimport {\n  ClientContext,\n  ClientMetadata,\n  SessionMetadata,\n} from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { RedisClientLib } from 'src/modules/redis/redis.client.factory';\n\nconst serverConfig = config.get('server') as Config['server'];\n\n@Injectable()\nexport class RedisObserverProvider {\n  private logger = new Logger('RedisObserverProvider');\n\n  private redisObservers: Map<string, RedisObserver> = new Map();\n\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  /**\n   * Get existing redis observer or create a new one\n   * @param sessionMetadata\n   * @param instanceId\n   */\n  async getOrCreateObserver(\n    sessionMetadata: SessionMetadata,\n    instanceId: string,\n  ): Promise<RedisObserver> {\n    this.logger.debug('Getting redis observer...', sessionMetadata);\n\n    let redisObserver = this.redisObservers.get(instanceId);\n\n    try {\n      if (!redisObserver) {\n        this.logger.debug('Creating new RedisObserver', sessionMetadata);\n        redisObserver = new RedisObserver();\n        this.redisObservers.set(instanceId, redisObserver);\n\n        // todo: add multi user support\n        // initialize redis observer\n        redisObserver\n          .init(\n            this.getRedisClientFn({\n              sessionMetadata,\n              databaseId: instanceId,\n              context: ClientContext.Profiler,\n            }),\n          )\n          .catch();\n      } else {\n        switch (redisObserver.status) {\n          case RedisObserverStatus.Ready:\n            this.logger.debug(\n              `Using existing RedisObserver with status: ${redisObserver.status}`,\n              sessionMetadata,\n            );\n            return redisObserver;\n          case RedisObserverStatus.Empty:\n          case RedisObserverStatus.End:\n          case RedisObserverStatus.Error:\n            this.logger.debug(\n              `Trying to reconnect. Current status: ${redisObserver.status}`,\n              sessionMetadata,\n            );\n            // todo: add multiuser support\n            // try to reconnect\n            redisObserver\n              .init(\n                this.getRedisClientFn({\n                  sessionMetadata,\n                  databaseId: instanceId,\n                  context: ClientContext.Profiler,\n                }),\n              )\n              .catch();\n            break;\n          case RedisObserverStatus.Initializing:\n          case RedisObserverStatus.Wait:\n          case RedisObserverStatus.Connected:\n          default:\n            // wait until connect or error\n            this.logger.debug(\n              `Waiting for ready. Current status: ${redisObserver.status}`,\n              sessionMetadata,\n            );\n        }\n      }\n\n      return new Promise((resolve, reject) => {\n        redisObserver.once('connect', () => {\n          resolve(redisObserver);\n        });\n        redisObserver.once('connect_error', (e) => {\n          reject(e);\n        });\n      });\n    } catch (error) {\n      this.logger.error(\n        `Failed to get monitor observer. ${error.message}.`,\n        error,\n        sessionMetadata,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Get Redis Observer from existing ones\n   * @param instanceId\n   */\n  async getObserver(instanceId: string) {\n    return this.redisObservers.get(instanceId);\n  }\n\n  /**\n   * Remove Redis Observer\n   * @param instanceId\n   */\n  async removeObserver(instanceId: string) {\n    this.redisObservers.delete(instanceId);\n  }\n\n  /**\n   * Get Redis existing common IORedis client for instance or create a new common connection\n   * @param clientMetadata\n   * @private\n   */\n  private getRedisClientFn(\n    clientMetadata: ClientMetadata,\n  ): () => Promise<RedisClient> {\n    return async () =>\n      withTimeout(\n        // workaround: use ioredis client for profiler until node-redis lib add support for \"monitor\" command\n        this.databaseClientFactory.createClient(clientMetadata, {\n          clientLib: RedisClientLib.IOREDIS,\n        }),\n        serverConfig.requestTimeout,\n        new ServiceUnavailableException(\n          ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB,\n        ),\n      );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/constants/index.ts",
    "content": "export enum PubSubClientEvents {\n  Subscribe = 'subscribe',\n  Unsubscribe = 'unsubscribe',\n}\n\nexport enum PubSubServerEvents {\n  Exception = 'exception',\n}\n\nexport enum SubscriptionType {\n  Subscribe = 's',\n  PSubscribe = 'p',\n  SSubscribe = 'ss',\n}\n\nexport enum RedisClientSubscriberStatus {\n  Connecting = 'connecting',\n  Connected = 'connected',\n  Error = 'error',\n  End = 'end',\n}\n\nexport enum RedisClientSubscriberEvents {\n  Connected = 'connected',\n  ConnectionError = 'connection_error',\n  Message = 'message',\n  End = 'end',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/decorators/client.decorator.ts",
    "content": "import { get } from 'lodash';\nimport { createParamDecorator, ExecutionContext } from '@nestjs/common';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\n\nexport const Client = createParamDecorator(\n  (data: unknown, ctx: ExecutionContext): UserClient => {\n    const socket = ctx.switchToWs().getClient();\n\n    return new UserClient(\n      socket.id,\n      socket,\n      get(socket, 'handshake.query.instanceId'),\n    );\n  },\n);\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/dto/index.ts",
    "content": "export * from './subscribe.dto';\nexport * from './subscription.dto';\nexport * from './messages.response';\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/dto/messages.response.ts",
    "content": "import { IMessage } from 'src/modules/pub-sub/interfaces/message.interface';\n\nexport class MessagesResponse {\n  messages: IMessage[];\n\n  count: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/dto/publish.dto.ts",
    "content": "import { IsDefined, IsString } from 'class-validator';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class PublishDto {\n  @ApiProperty({\n    type: String,\n    description: 'Message to send',\n    example: '{\"hello\":\"world\"}',\n  })\n  @IsDefined()\n  @IsString()\n  message: string;\n\n  @ApiProperty({\n    type: String,\n    description: 'Chanel name',\n    example: 'channel-1',\n  })\n  @IsDefined()\n  @IsString()\n  channel: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/dto/publish.response.ts",
    "content": "export class PublishResponse {\n  affected: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/dto/subscribe.dto.ts",
    "content": "import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { SubscriptionDto } from './subscription.dto';\n\nexport class SubscribeDto {\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested({ each: true })\n  @Type(() => SubscriptionDto)\n  subscriptions: SubscriptionDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/dto/subscription.dto.ts",
    "content": "import { SubscriptionType } from 'src/modules/pub-sub/constants';\nimport { IsEnum, IsNotEmpty, IsString } from 'class-validator';\n\nexport class SubscriptionDto {\n  @IsNotEmpty()\n  @IsString()\n  channel: string;\n\n  @IsNotEmpty()\n  @IsEnum(SubscriptionType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      SubscriptionType,\n    )}.`,\n  })\n  type: SubscriptionType;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/errors/pub-sub-ws.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { isString } from 'lodash';\n\nexport class PubSubWsException extends Error {\n  status: number;\n\n  name: string;\n\n  constructor(err: Error | string) {\n    super();\n    this.status = 500;\n    this.message = 'Internal server error';\n    this.name = this.constructor.name;\n\n    if (isString(err)) {\n      this.message = err;\n    } else if (err instanceof HttpException) {\n      this.message = err.getResponse()['message'];\n      this.status = err.getStatus();\n      this.name = err.constructor.name;\n    } else if (err instanceof Error) {\n      this.message = err.message;\n      this.name = 'Error';\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/filters/ack-ws-exception.filter.ts",
    "content": "import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';\nimport { PubSubWsException } from 'src/modules/pub-sub/errors/pub-sub-ws.exception';\n\n@Catch()\nexport class AckWsExceptionFilter {\n  public catch(exception: HttpException, host: ArgumentsHost) {\n    const callback = host.getArgByIndex(2);\n    this.handleError(callback, exception);\n  }\n\n  public handleError(callback: any, exception: Error) {\n    if (callback && typeof callback === 'function') {\n      callback({ status: 'error', error: new PubSubWsException(exception) });\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/interfaces/message.interface.ts",
    "content": "export interface IMessage {\n  message: string;\n\n  channel: string;\n\n  time: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/interfaces/subscription.interface.ts",
    "content": "import { IMessage } from 'src/modules/pub-sub/interfaces/message.interface';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport interface ISubscription {\n  getId(): string;\n\n  getChannel(): string;\n\n  getType(): string;\n\n  pushMessage(message: IMessage): void;\n\n  subscribe(client: RedisClient): Promise<void>;\n\n  unsubscribe(client: RedisClient): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/abstract.subscription.ts",
    "content": "import { debounce } from 'lodash';\nimport { SubscriptionType } from 'src/modules/pub-sub/constants';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { MessagesResponse, SubscriptionDto } from 'src/modules/pub-sub/dto';\nimport { ISubscription } from 'src/modules/pub-sub/interfaces/subscription.interface';\nimport { IMessage } from 'src/modules/pub-sub/interfaces/message.interface';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\n\nconst EMIT_WAIT = 30;\nconst EMIT_MAX_WAIT = 100;\nconst MESSAGES_MAX = 5000;\n\nexport abstract class AbstractSubscription implements ISubscription {\n  protected readonly id: string;\n\n  protected readonly userClient: UserClient;\n\n  protected readonly redisClient: RedisClientSubscriber;\n\n  protected readonly debounce: any;\n\n  protected readonly channel: string;\n\n  protected readonly type: SubscriptionType;\n\n  protected messages: IMessage[] = [];\n\n  constructor(userClient: UserClient, dto: SubscriptionDto) {\n    this.userClient = userClient;\n    this.channel = dto.channel;\n    this.type = dto.type;\n    this.id = `${this.type}:${this.channel}`;\n    this.debounce = debounce(\n      () => {\n        if (this.messages.length) {\n          this.userClient.getSocket().emit(this.id, {\n            messages: this.messages.slice(0, MESSAGES_MAX),\n            count: this.messages.length,\n          } as MessagesResponse);\n          this.messages = [];\n        }\n      },\n      EMIT_WAIT,\n      {\n        maxWait: EMIT_MAX_WAIT,\n      },\n    );\n  }\n\n  getId() {\n    return this.id;\n  }\n\n  getChannel() {\n    return this.channel;\n  }\n\n  getType() {\n    return this.type;\n  }\n\n  abstract subscribe(client: RedisClient): Promise<void>;\n\n  abstract unsubscribe(client: RedisClient): Promise<void>;\n\n  pushMessage(message: IMessage) {\n    this.messages.push(message);\n\n    this.debounce();\n  }\n\n  toString() {\n    return `${this.constructor.name}:${JSON.stringify({\n      id: this.id,\n      mL: this.messages.length,\n    })}`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/pattern.subscription.ts",
    "content": "import { AbstractSubscription } from 'src/modules/pub-sub/model/abstract.subscription';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class PatternSubscription extends AbstractSubscription {\n  async subscribe(client: RedisClient): Promise<void> {\n    await client.pSubscribe(this.channel);\n  }\n\n  async unsubscribe(client: RedisClient): Promise<void> {\n    await client.pUnsubscribe(this.channel);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/redis-client-subscriber.spec.ts",
    "content": "import { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\nimport {\n  RedisClientSubscriberEvents,\n  RedisClientSubscriberStatus,\n} from 'src/modules/pub-sub/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\nconst getRedisClientFn = jest.fn();\n\nconst nodeClient = Object.create(RedisClient.prototype);\nnodeClient.subscribe = jest.fn();\nnodeClient.pSubscribe = jest.fn();\nnodeClient.unsubscribe = jest.fn();\nnodeClient.pUnsubscribe = jest.fn();\nnodeClient.disconnect = jest.fn();\n\ndescribe('RedisClient', () => {\n  let redisClientSubscriber: RedisClientSubscriber;\n\n  beforeEach(() => {\n    jest.resetAllMocks();\n    redisClientSubscriber = new RedisClientSubscriber(\n      'databaseId',\n      getRedisClientFn,\n    );\n    getRedisClientFn.mockResolvedValue(nodeClient);\n    nodeClient.subscribe.mockResolvedValue('OK');\n    nodeClient.pSubscribe.mockResolvedValue('OK');\n    nodeClient.quit = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('getClient', () => {\n    let connectSpy;\n\n    beforeEach(() => {\n      connectSpy = jest.spyOn(redisClientSubscriber as any, 'connect');\n    });\n\n    it('should connect and return client by default', async () => {\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(1);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connected,\n      );\n    });\n    it('should wait until first attempt of connection finish with success', async () => {\n      redisClientSubscriber.getClient().then().catch();\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connecting,\n      );\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(1);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connected,\n      );\n    });\n    it('should wait until first attempt of connection finish with error', async () => {\n      try {\n        getRedisClientFn.mockRejectedValueOnce(new Error('Connection error'));\n        redisClientSubscriber\n          .getClient()\n          .then()\n          .catch(() => {});\n        expect(redisClientSubscriber['status']).toEqual(\n          RedisClientSubscriberStatus.Connecting,\n        );\n        expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n        fail();\n      } catch (e) {\n        expect(connectSpy).toHaveBeenCalledTimes(1);\n        expect(redisClientSubscriber['status']).toEqual(\n          RedisClientSubscriberStatus.Error,\n        );\n      }\n    });\n    it('should return existing connection when status connected', async () => {\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(1);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connected,\n      );\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(1);\n    });\n    it('should return create new connection when status end or error', async () => {\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(1);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connected,\n      );\n      redisClientSubscriber['status'] = RedisClientSubscriberStatus.Error;\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(2);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connected,\n      );\n      redisClientSubscriber['status'] = RedisClientSubscriberStatus.End;\n      expect(await redisClientSubscriber.getClient()).toEqual(nodeClient);\n      expect(connectSpy).toHaveBeenCalledTimes(3);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.Connected,\n      );\n    });\n  });\n\n  describe('connect', () => {\n    it('should connect and emit connected event', async () => {\n      expect(\n        await new Promise((res) => {\n          redisClientSubscriber['connect']();\n          redisClientSubscriber.on(RedisClientSubscriberEvents.Connected, res);\n        }),\n      ).toEqual(nodeClient);\n    });\n    it('should emit message event (message source)', async () => {\n      await redisClientSubscriber['connect']();\n      const [id, message] = await new Promise((res: (value: any[]) => void) => {\n        redisClientSubscriber.on('message', (i, m) => res([i, m]));\n        nodeClient.emit('message', 'channel-a', 'message-a');\n      });\n\n      expect(id).toEqual('s:channel-a');\n      expect(message.channel).toEqual('channel-a');\n      expect(message.message).toEqual('message-a');\n    });\n    it('should emit message event (pmessage source)', async () => {\n      await redisClientSubscriber['connect']();\n      const [id, message] = await new Promise((res: (value: any[]) => void) => {\n        redisClientSubscriber.on('message', (i, m) => res([i, m]));\n        nodeClient.emit('pmessage', '*', 'channel-aa', 'message-aa');\n      });\n      expect(id).toEqual('p:*');\n      expect(message.channel).toEqual('channel-aa');\n      expect(message.message).toEqual('message-aa');\n    });\n    it('should emit end event', async () => {\n      await redisClientSubscriber['connect']();\n      await new Promise((res) => {\n        redisClientSubscriber.on('end', () => {\n          res(null);\n        });\n\n        nodeClient.emit('end');\n      });\n\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.End,\n      );\n    });\n\n    afterAll(() => {\n      nodeClient.removeAllListeners();\n    });\n  });\n\n  describe('destroy', () => {\n    it('should remove all listeners, disconnect, set client to null and emit end event', async () => {\n      const removeAllListenersSpy = jest.spyOn(\n        nodeClient,\n        'removeAllListeners',\n      );\n\n      await redisClientSubscriber['connect']();\n      redisClientSubscriber.destroy();\n\n      expect(redisClientSubscriber['client']).toEqual(null);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.End,\n      );\n      expect(removeAllListenersSpy).toHaveBeenCalled();\n      expect(nodeClient.quit).toHaveBeenCalled();\n    });\n    it('should not crash if quick promise was rejected', async () => {\n      nodeClient.quit = jest\n        .fn()\n        .mockRejectedValueOnce(new Error('Connection is closed'));\n\n      const removeAllListenersSpy = jest.spyOn(\n        nodeClient,\n        'removeAllListeners',\n      );\n\n      await redisClientSubscriber['connect']();\n      redisClientSubscriber.destroy();\n\n      expect(redisClientSubscriber['client']).toEqual(null);\n      expect(redisClientSubscriber['status']).toEqual(\n        RedisClientSubscriberStatus.End,\n      );\n      expect(removeAllListenersSpy).toHaveBeenCalled();\n      expect(nodeClient.quit).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/redis-client-subscriber.ts",
    "content": "import { Logger } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  RedisClientSubscriberEvents,\n  RedisClientSubscriberStatus,\n} from 'src/modules/pub-sub/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class RedisClientSubscriber extends EventEmitter2 {\n  private logger: Logger = new Logger('RedisClientSubscriber');\n\n  private client: RedisClient;\n\n  private readonly databaseId: string;\n\n  private readonly connectFn: () => Promise<RedisClient>;\n\n  private status: RedisClientSubscriberStatus;\n\n  constructor(databaseId: string, connectFn: () => Promise<RedisClient>) {\n    super();\n    this.databaseId = databaseId;\n    this.connectFn = connectFn;\n  }\n\n  /**\n   * Get existing client or wait until previous attempt fulfill or initiate new connection attempt\n   * based on current status\n   */\n  async getClient(): Promise<RedisClient> {\n    try {\n      this.logger.debug(`Get client ${this}`);\n      switch (this.status) {\n        case RedisClientSubscriberStatus.Connected:\n          return this.client;\n        case RedisClientSubscriberStatus.Connecting:\n          // wait until connect or error\n          break;\n        case RedisClientSubscriberStatus.Error:\n        case RedisClientSubscriberStatus.End:\n        default:\n          await this.connect();\n          return this.client;\n      }\n\n      return new Promise((resolve, reject) => {\n        this.once(RedisClientSubscriberEvents.Connected, resolve);\n        this.once(RedisClientSubscriberEvents.ConnectionError, reject);\n      });\n    } catch (e) {\n      this.logger.error('Unable to connect to Redis', e);\n      this.status = RedisClientSubscriberStatus.Error;\n      this.emit(RedisClientSubscriberEvents.ConnectionError, e);\n      throw e;\n    }\n  }\n\n  /**\n   * Connects to redis and change current status to Connected\n   * Also emit Connected event after success\n   * Also subscribe to needed channels\n   * @private\n   */\n  private async connect() {\n    this.status = RedisClientSubscriberStatus.Connecting;\n    this.client = await this.connectFn();\n    this.status = RedisClientSubscriberStatus.Connected;\n    this.emit(RedisClientSubscriberEvents.Connected, this.client);\n\n    this.client.on('message', (channel: string, message: string) => {\n      this.emit(RedisClientSubscriberEvents.Message, `s:${channel}`, {\n        channel,\n        message,\n        time: Date.now(),\n      });\n    });\n\n    this.client.on(\n      'pmessage',\n      (pattern: string, channel: string, message: string) => {\n        this.emit(RedisClientSubscriberEvents.Message, `p:${pattern}`, {\n          channel,\n          message,\n          time: Date.now(),\n        });\n      },\n    );\n\n    this.client.on('end', () => {\n      this.status = RedisClientSubscriberStatus.End;\n      this.emit(RedisClientSubscriberEvents.End);\n    });\n  }\n\n  /**\n   * Unsubscribe all listeners and disconnect\n   * Remove client and set current state to End\n   */\n  destroy() {\n    this.client?.removeAllListeners();\n    this.client?.quit().catch((e) => {\n      this.logger.warn('Error when closing Redis client', e);\n    });\n    this.client = null;\n    this.status = RedisClientSubscriberStatus.End;\n  }\n\n  toString() {\n    return `RedisClient:${JSON.stringify({\n      databaseId: this.databaseId,\n      status: this.status,\n    })}`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/simple.subscription.ts",
    "content": "import { AbstractSubscription } from 'src/modules/pub-sub/model/abstract.subscription';\nimport { RedisClient } from 'src/modules/redis/client';\n\nexport class SimpleSubscription extends AbstractSubscription {\n  async subscribe(client: RedisClient): Promise<void> {\n    await client.subscribe(this.channel);\n  }\n\n  async unsubscribe(client: RedisClient): Promise<void> {\n    await client.unsubscribe(this.channel);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/user-client.ts",
    "content": "import { Socket } from 'socket.io';\n\nexport class UserClient {\n  private readonly socket: Socket;\n\n  private readonly id: string;\n\n  private readonly databaseId: string;\n\n  constructor(id: string, socket: Socket, databaseId: string) {\n    this.id = id;\n    this.socket = socket;\n    this.databaseId = databaseId;\n  }\n\n  getId() {\n    return this.id;\n  }\n\n  getDatabaseId() {\n    return this.databaseId;\n  }\n\n  getSocket() {\n    return this.socket;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/user-session.spec.ts",
    "content": "import { mockSocket } from 'src/__mocks__';\nimport { UserSession } from 'src/modules/pub-sub/model/user-session';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\nimport { SimpleSubscription } from 'src/modules/pub-sub/model/simple.subscription';\nimport { SubscriptionType } from 'src/modules/pub-sub/constants';\nimport { PatternSubscription } from 'src/modules/pub-sub/model/pattern.subscription';\nimport { RedisClient } from 'src/modules/redis/client';\n\nconst getRedisClientFn = jest.fn();\n\nconst nodeClient = Object.create(RedisClient.prototype);\nnodeClient.subscribe = jest.fn();\nnodeClient.pSubscribe = jest.fn();\nnodeClient.unsubscribe = jest.fn();\nnodeClient.pUnsubscribe = jest.fn();\nnodeClient.disconnect = jest.fn();\nnodeClient.quit = jest.fn();\n\nconst mockUserClient = new UserClient('socketId', mockSocket, 'databaseId');\n\nconst mockRedisClientSubscriber = new RedisClientSubscriber(\n  'databaseId',\n  getRedisClientFn,\n);\n\nconst mockSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.Subscribe,\n};\n\nconst mockPSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.PSubscribe,\n};\n\nconst mockSubscription = new SimpleSubscription(\n  mockUserClient,\n  mockSubscriptionDto,\n);\nconst mockPSubscription = new PatternSubscription(\n  mockUserClient,\n  mockPSubscriptionDto,\n);\n\nconst mockMessage = {\n  channel: 'channel-a',\n  message: 'message-a',\n  time: 1234567890,\n};\n\ndescribe('UserSession', () => {\n  let userSession: UserSession;\n\n  beforeEach(() => {\n    jest.resetAllMocks();\n    userSession = new UserSession(mockUserClient, mockRedisClientSubscriber);\n    getRedisClientFn.mockResolvedValue(nodeClient);\n    nodeClient.subscribe.mockResolvedValue('OK');\n    nodeClient.pSubscribe.mockResolvedValue('OK');\n    nodeClient.quit = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('subscribe', () => {\n    it('should subscribe to a channel', async () => {\n      expect(userSession['subscriptions'].size).toEqual(0);\n      await userSession.subscribe(mockSubscription);\n      expect(userSession['subscriptions'].size).toEqual(1);\n      await userSession.subscribe(mockSubscription);\n      expect(userSession['subscriptions'].size).toEqual(1);\n      expect(\n        userSession['subscriptions'].get(mockSubscription.getId()),\n      ).toEqual(mockSubscription);\n      await userSession.subscribe(mockPSubscription);\n      expect(userSession['subscriptions'].size).toEqual(2);\n      await userSession.subscribe(mockPSubscription);\n      expect(userSession['subscriptions'].size).toEqual(2);\n      expect(\n        userSession['subscriptions'].get(mockPSubscription.getId()),\n      ).toEqual(mockPSubscription);\n    });\n  });\n\n  describe('unsubscribe', () => {\n    it('should unsubscribe from a channel', async () => {\n      expect(userSession['subscriptions'].size).toEqual(0);\n      await userSession.subscribe(mockSubscription);\n      expect(userSession['subscriptions'].size).toEqual(1);\n      await userSession.subscribe(mockPSubscription);\n      expect(userSession['subscriptions'].size).toEqual(2);\n      await userSession.unsubscribe(mockSubscription);\n      expect(userSession['subscriptions'].size).toEqual(1);\n      await userSession.unsubscribe(mockSubscription);\n      expect(userSession['subscriptions'].size).toEqual(1);\n      await userSession.unsubscribe(mockPSubscription);\n      expect(userSession['subscriptions'].size).toEqual(0);\n      await userSession.unsubscribe(mockPSubscription);\n      expect(userSession['subscriptions'].size).toEqual(0);\n    });\n  });\n\n  describe('handleMessage', () => {\n    let handleSimpleSpy;\n    let handlePatternSpy;\n\n    beforeEach(async () => {\n      handleSimpleSpy = jest.spyOn(mockSubscription, 'pushMessage');\n      handlePatternSpy = jest.spyOn(mockPSubscription, 'pushMessage');\n      await userSession.subscribe(mockSubscription);\n      await userSession.subscribe(mockPSubscription);\n    });\n    it('should handle message by particular subscription', async () => {\n      userSession.handleMessage('id', mockMessage);\n      expect(handleSimpleSpy).toHaveBeenCalledTimes(0);\n      expect(handlePatternSpy).toHaveBeenCalledTimes(0);\n      userSession.handleMessage(mockSubscription.getId(), mockMessage);\n      expect(handleSimpleSpy).toHaveBeenCalledTimes(1);\n      expect(handlePatternSpy).toHaveBeenCalledTimes(0);\n      userSession.handleMessage(mockPSubscription.getId(), mockMessage);\n      userSession.handleMessage(mockPSubscription.getId(), mockMessage);\n      expect(handleSimpleSpy).toHaveBeenCalledTimes(1);\n      expect(handlePatternSpy).toHaveBeenCalledTimes(2);\n      // wait until debounce process\n      await new Promise((res) => setTimeout(res, 200));\n    });\n  });\n\n  describe('handleDisconnect', () => {\n    beforeEach(async () => {\n      await userSession.subscribe(mockSubscription);\n      await userSession.subscribe(mockPSubscription);\n    });\n    it('should handle message by particular subscription', async () => {\n      userSession.handleDisconnect();\n      expect(userSession['subscriptions'].size).toEqual(0);\n      expect(nodeClient.quit).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/model/user-session.ts",
    "content": "import { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { ISubscription } from 'src/modules/pub-sub/interfaces/subscription.interface';\nimport { IMessage } from 'src/modules/pub-sub/interfaces/message.interface';\nimport {\n  PubSubServerEvents,\n  RedisClientSubscriberEvents,\n} from 'src/modules/pub-sub/constants';\nimport { Logger } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { PubSubWsException } from 'src/modules/pub-sub/errors/pub-sub-ws.exception';\n\nexport class UserSession {\n  private readonly logger: Logger = new Logger('UserSession');\n\n  private readonly id: string;\n\n  private readonly userClient: UserClient;\n\n  private readonly redisClient: RedisClientSubscriber;\n\n  private subscriptions: Map<string, ISubscription> = new Map();\n\n  constructor(userClient: UserClient, redisClient: RedisClientSubscriber) {\n    this.id = userClient.getId();\n    this.userClient = userClient;\n    this.redisClient = redisClient;\n    redisClient.on(\n      RedisClientSubscriberEvents.Message,\n      this.handleMessage.bind(this),\n    );\n    redisClient.on(\n      RedisClientSubscriberEvents.End,\n      this.handleDisconnect.bind(this),\n    );\n  }\n\n  getId() {\n    return this.id;\n  }\n\n  getUserClient() {\n    return this.userClient;\n  }\n\n  getRedisClient() {\n    return this.redisClient;\n  }\n\n  /**\n   * Subscribe to a Pub/Sub channel and create Redis client connection if needed\n   * Also add subscription to the subscriptions list\n   * @param subscription\n   */\n  async subscribe(subscription: ISubscription) {\n    this.logger.debug(\n      `Subscribe ${subscription} ${this}. Getting Redis client...`,\n    );\n\n    const client = await this.redisClient?.getClient();\n\n    if (!client) {\n      throw new Error('There is no Redis client initialized');\n    }\n\n    if (!this.subscriptions.has(subscription.getId())) {\n      this.subscriptions.set(subscription.getId(), subscription);\n      this.logger.debug(`Subscribe to Redis ${subscription} ${this}`);\n      await subscription.subscribe(client);\n    }\n  }\n\n  /**\n   * Unsubscribe from a channel and remove from the list of subscriptions\n   * Also destroy redis client when no subscriptions left\n   * @param subscription\n   */\n  async unsubscribe(subscription: ISubscription) {\n    this.logger.debug(`Unsubscribe ${subscription} ${this}`);\n\n    this.subscriptions.delete(subscription.getId());\n\n    const client = await this.redisClient?.getClient();\n\n    if (client) {\n      this.logger.debug(`Unsubscribe from Redis ${subscription} ${this}`);\n      await subscription.unsubscribe(client);\n\n      if (!this.subscriptions.size) {\n        this.logger.debug(`Unsubscribe: Destroy RedisClient ${this}`);\n        this.redisClient.destroy();\n      }\n    }\n  }\n\n  /**\n   * Redirect message to a proper subscription from the list using id\n   * ID is generated in this way: \"p:channelName\" where \"p\" - is a type of subscription\n   * Subscription types: s - \"subscribe\", p - \"psubscribe\", ss - \"ssubscribe\"\n   * @param id\n   * @param message\n   */\n  handleMessage(id: string, message: IMessage) {\n    const subscription = this.subscriptions.get(id);\n\n    if (subscription) {\n      subscription.pushMessage(message);\n    }\n  }\n\n  /**\n   * Handle socket disconnection\n   * In this case we need to destroy entire session and cascade destroy other models inside\n   * to be sure that there is no open connections left\n   */\n  handleDisconnect() {\n    this.logger.debug(`Handle disconnect ${this}`);\n\n    this.userClient\n      .getSocket()\n      .emit(\n        PubSubServerEvents.Exception,\n        new PubSubWsException(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB),\n      );\n\n    this.destroy();\n  }\n\n  /**\n   * Reset subscriptions map and call and destroy Redis client\n   */\n  destroy() {\n    this.logger.debug(`Destroy ${this}`);\n\n    this.subscriptions = new Map();\n    this.redisClient.destroy();\n\n    this.logger.debug(`Destroyed ${this}`);\n  }\n\n  toString() {\n    return `UserSession:${JSON.stringify({\n      id: this.id,\n      subscriptionsSize: this.subscriptions.size,\n      subscriptions: [...this.subscriptions.keys()],\n    })}`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockCommonClientMetadata,\n  mockDatabaseClientFactory,\n} from 'src/__mocks__';\nimport { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider';\nimport { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\ndescribe('RedisClientProvider', () => {\n  let service: RedisClientProvider;\n\n  beforeEach(async () => {\n    jest.resetAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RedisClientProvider,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(RedisClientProvider);\n  });\n\n  describe('createClient', () => {\n    it('should create redis client', async () => {\n      const redisClientSubscriber = service.createClient(\n        mockCommonClientMetadata,\n      );\n      expect(redisClientSubscriber).toBeInstanceOf(RedisClientSubscriber);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.ts",
    "content": "import { Injectable, ServiceUnavailableException } from '@nestjs/common';\nimport { withTimeout } from 'src/utils/promise-with-timeout';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport config, { Config } from 'src/utils/config';\nimport { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\nimport { ClientMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nconst serverConfig = config.get('server') as Config['server'];\n\n@Injectable()\nexport class RedisClientProvider {\n  constructor(private databaseClientFactory: DatabaseClientFactory) {}\n\n  createClient(clientMetadata: ClientMetadata): RedisClientSubscriber {\n    return new RedisClientSubscriber(\n      clientMetadata.databaseId,\n      this.getConnectFn(clientMetadata),\n    );\n  }\n\n  private getConnectFn(clientMetadata: ClientMetadata) {\n    return () =>\n      withTimeout(\n        this.databaseClientFactory.createClient(clientMetadata),\n        serverConfig.requestTimeout,\n        new ServiceUnavailableException(\n          ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB,\n        ),\n      );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/providers/subscription.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { mockSocket } from 'src/__mocks__';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider';\nimport { SubscriptionType } from 'src/modules/pub-sub/constants';\nimport { SimpleSubscription } from 'src/modules/pub-sub/model/simple.subscription';\nimport { PatternSubscription } from 'src/modules/pub-sub/model/pattern.subscription';\nimport { BadRequestException } from '@nestjs/common';\n\nconst mockUserClient = new UserClient('socketId', mockSocket, 'databaseId');\n\nconst mockSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.Subscribe,\n};\n\nconst mockPSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.PSubscribe,\n};\n\nconst mockSSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.SSubscribe,\n};\n\ndescribe('SubscriptionProvider', () => {\n  let service: SubscriptionProvider;\n\n  beforeEach(async () => {\n    jest.resetAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [SubscriptionProvider],\n    }).compile();\n\n    service = await module.get(SubscriptionProvider);\n  });\n\n  describe('createSubscription', () => {\n    it('should create simple subscription', async () => {\n      const subscription = service.createSubscription(\n        mockUserClient,\n        mockSubscriptionDto,\n      );\n      expect(subscription).toBeInstanceOf(SimpleSubscription);\n    });\n    it('should create pattern subscription', async () => {\n      const subscription = service.createSubscription(\n        mockUserClient,\n        mockPSubscriptionDto,\n      );\n      expect(subscription).toBeInstanceOf(PatternSubscription);\n    });\n    it('should throw error since shard subscription is not supported yet', async () => {\n      try {\n        service.createSubscription(mockUserClient, mockSSubscriptionDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual('Unsupported Subscription type');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/providers/subscription.provider.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { SubscriptionDto } from 'src/modules/pub-sub/dto';\nimport { SubscriptionType } from 'src/modules/pub-sub/constants';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { PatternSubscription } from 'src/modules/pub-sub/model/pattern.subscription';\nimport { SimpleSubscription } from 'src/modules/pub-sub/model/simple.subscription';\nimport { ISubscription } from 'src/modules/pub-sub/interfaces/subscription.interface';\n\n@Injectable()\nexport class SubscriptionProvider {\n  createSubscription(\n    userClient: UserClient,\n    dto: SubscriptionDto,\n  ): ISubscription {\n    switch (dto.type) {\n      case SubscriptionType.PSubscribe:\n        return new PatternSubscription(userClient, dto);\n      case SubscriptionType.Subscribe:\n        return new SimpleSubscription(userClient, dto);\n      case SubscriptionType.SSubscribe:\n      default:\n        throw new BadRequestException('Unsupported Subscription type');\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/providers/user-session.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { mockSocket, MockType, mockSessionMetadata } from 'src/__mocks__';\nimport { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session.provider';\nimport { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\n\nconst mockUserClient = new UserClient('socketId', mockSocket, 'databaseId');\nconst mockUserClient2 = new UserClient('socketId2', mockSocket, 'databaseId');\nconst getRedisClientFn = jest.fn();\nconst mockRedisClient = new RedisClientSubscriber(\n  'databaseId',\n  getRedisClientFn,\n);\n\ndescribe('UserSessionProvider', () => {\n  let service: UserSessionProvider;\n  let redisClientProvider: MockType<RedisClientProvider>;\n\n  beforeEach(async () => {\n    jest.resetAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        UserSessionProvider,\n        {\n          provide: RedisClientProvider,\n          useFactory: () => ({\n            createClient: jest.fn(),\n          }),\n        },\n      ],\n    }).compile();\n\n    service = await module.get(UserSessionProvider);\n    redisClientProvider = await module.get(RedisClientProvider);\n\n    redisClientProvider.createClient.mockReturnValue(mockRedisClient);\n  });\n\n  describe('getOrCreateUserSession', () => {\n    it('should create new UserSession and store it. Ignore the same session', async () => {\n      expect(service['sessions'].size).toEqual(0);\n      const userSession = await service.getOrCreateUserSession(\n        mockSessionMetadata,\n        mockUserClient,\n      );\n      expect(service['sessions'].size).toEqual(1);\n      expect(service.getUserSession(userSession.getId())).toEqual(userSession);\n      await service.getOrCreateUserSession(mockSessionMetadata, mockUserClient);\n      expect(service['sessions'].size).toEqual(1);\n      expect(service.getUserSession(userSession.getId())).toEqual(userSession);\n    });\n  });\n  describe('removeUserSession', () => {\n    it('should remove UserSession', async () => {\n      expect(service['sessions'].size).toEqual(0);\n      await service.getOrCreateUserSession(mockSessionMetadata, mockUserClient);\n      await service.getOrCreateUserSession(\n        mockSessionMetadata,\n        mockUserClient2,\n      );\n      expect(service['sessions'].size).toEqual(2);\n      await service.removeUserSession(mockUserClient.getId());\n      expect(service['sessions'].size).toEqual(1);\n      await service.removeUserSession(mockUserClient.getId());\n      expect(service['sessions'].size).toEqual(1);\n      await service.removeUserSession(mockUserClient2.getId());\n      expect(service['sessions'].size).toEqual(0);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/providers/user-session.provider.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { UserSession } from 'src/modules/pub-sub/model/user-session';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider';\nimport { ClientContext, SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class UserSessionProvider {\n  private readonly logger: Logger = new Logger('UserSessionProvider');\n\n  private sessions: Map<string, UserSession> = new Map();\n\n  constructor(private readonly redisClientProvider: RedisClientProvider) {}\n\n  getOrCreateUserSession(\n    sessionMetadata: SessionMetadata,\n    userClient: UserClient,\n  ) {\n    let session = this.getUserSession(userClient.getId());\n\n    if (!session) {\n      session = new UserSession(\n        userClient,\n        this.redisClientProvider.createClient({\n          sessionMetadata,\n          databaseId: userClient.getDatabaseId(),\n          context: ClientContext.Common,\n        }),\n      );\n      this.sessions.set(session.getId(), session);\n      this.logger.debug(`New session was added ${this}`, sessionMetadata);\n    }\n\n    return session;\n  }\n\n  getUserSession(id: string): UserSession {\n    return this.sessions.get(id);\n  }\n\n  removeUserSession(id: string) {\n    this.logger.debug(`Removing user session ${id}`);\n\n    this.sessions.delete(id);\n\n    this.logger.debug(`User session was removed ${this}`);\n  }\n\n  toString() {\n    return `UserSessionProvider:${JSON.stringify({\n      sessionsSize: this.sessions.size,\n      sessions: [...this.sessions.keys()],\n    })}`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { mockDatabase, mockSessionMetadata } from 'src/__mocks__';\nimport { TelemetryEvents } from 'src/constants';\nimport { PubSubAnalyticsService } from './pub-sub.analytics.service';\nimport { SubscriptionType } from './constants';\n\nconst instanceId = mockDatabase.id;\n\nconst affected = 2;\n\ndescribe('PubSubAnalyticsService', () => {\n  let service: PubSubAnalyticsService;\n  let sendEventMethod: jest.SpyInstance<PubSubAnalyticsService, unknown[]>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, PubSubAnalyticsService],\n    }).compile();\n\n    service = module.get<PubSubAnalyticsService>(PubSubAnalyticsService);\n    sendEventMethod = jest.spyOn<PubSubAnalyticsService, any>(\n      service,\n      'sendEvent',\n    );\n  });\n\n  describe('sendMessagePublishedEvent', () => {\n    it('should emit sendMessagePublished event', () => {\n      service.sendMessagePublishedEvent(\n        mockSessionMetadata,\n        instanceId,\n        affected,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.PubSubMessagePublished,\n        {\n          databaseId: instanceId,\n          clients: affected,\n        },\n      );\n    });\n  });\n\n  describe('sendChannelSubscribeEvent', () => {\n    it('should emit sendChannelSubscribe event for all channels', () => {\n      service.sendChannelSubscribeEvent(mockSessionMetadata, instanceId, [\n        { channel: '*', type: SubscriptionType.Subscribe },\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.PubSubChannelSubscribed,\n        {\n          databaseId: instanceId,\n          allChannels: 'yes',\n        },\n      );\n    });\n\n    it('should emit sendChannelSubscribe event not for all channels', () => {\n      service.sendChannelSubscribeEvent(mockSessionMetadata, instanceId, [\n        { channel: '1', type: SubscriptionType.Subscribe },\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.PubSubChannelSubscribed,\n        {\n          databaseId: instanceId,\n          allChannels: 'no',\n        },\n      );\n    });\n  });\n\n  describe('sendChannelUnsubscribeEvent', () => {\n    it('should emit sendChannelUnsubscribe event', () => {\n      service.sendChannelUnsubscribeEvent(mockSessionMetadata, instanceId);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.PubSubChannelUnsubscribed,\n        {\n          databaseId: instanceId,\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.analytics.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { some } from 'lodash';\nimport { DEFAULT_MATCH, TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { RedisError, ReplyError } from 'src/models';\nimport { SessionMetadata } from 'src/common/models';\nimport { SubscriptionDto } from './dto';\n\nexport interface IExecResult {\n  response: any;\n  status: CommandExecutionStatus;\n  error?: RedisError | ReplyError | Error;\n}\n\n@Injectable()\nexport class PubSubAnalyticsService extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendMessagePublishedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    affected: number,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.PubSubMessagePublished, {\n        databaseId,\n        clients: affected,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendChannelSubscribeEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    subs: SubscriptionDto[],\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.PubSubChannelSubscribed, {\n        databaseId,\n        allChannels: some(subs, { channel: DEFAULT_MATCH }) ? 'yes' : 'no',\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendChannelUnsubscribeEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n  ): void {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.PubSubChannelUnsubscribed,\n        {\n          databaseId,\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Post,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator';\nimport { PubSubService } from 'src/modules/pub-sub/pub-sub.service';\nimport { PublishDto } from 'src/modules/pub-sub/dto/publish.dto';\nimport { PublishResponse } from 'src/modules/pub-sub/dto/publish.response';\nimport { ClientMetadata } from 'src/common/models';\nimport { ClientMetadataParam } from 'src/common/decorators';\n\n@ApiTags('Pub/Sub')\n@Controller('pub-sub')\n@UsePipes(new ValidationPipe())\nexport class PubSubController {\n  constructor(private service: PubSubService) {}\n\n  @Post('messages')\n  @ApiRedisInstanceOperation({\n    description: 'Publish message to a channel',\n    statusCode: 201,\n    responses: [\n      {\n        status: 201,\n        description: 'Returns number of clients message ws delivered',\n        type: PublishResponse,\n      },\n    ],\n  })\n  async publish(\n    @ClientMetadataParam({\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n    @Body() dto: PublishDto,\n  ): Promise<PublishResponse> {\n    return this.service.publish(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.gateway.ts",
    "content": "import { Socket, Server } from 'socket.io';\nimport {\n  OnGatewayConnection,\n  OnGatewayDisconnect,\n  SubscribeMessage,\n  WebSocketGateway,\n  WebSocketServer,\n} from '@nestjs/websockets';\nimport {\n  Body,\n  Logger,\n  UseFilters,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { PubSubService } from 'src/modules/pub-sub/pub-sub.service';\nimport { Client } from 'src/modules/pub-sub/decorators/client.decorator';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { SubscribeDto } from 'src/modules/pub-sub/dto';\nimport { AckWsExceptionFilter } from 'src/modules/pub-sub/filters/ack-ws-exception.filter';\nimport { WSSessionMetadata } from 'src/modules/auth/session-metadata/decorators/ws-session-metadata.decorator';\nimport { SessionMetadata } from 'src/common/models';\nimport { PubSubClientEvents } from './constants';\n\nconst SOCKETS_CONFIG = config.get('sockets') as Config['sockets'];\n\n@UsePipes(new ValidationPipe())\n@UseFilters(AckWsExceptionFilter)\n@WebSocketGateway({\n  path: SOCKETS_CONFIG.path,\n  namespace: `${SOCKETS_CONFIG.namespacePrefix}pub-sub`,\n  cors: SOCKETS_CONFIG.cors.enabled\n    ? {\n        origin: SOCKETS_CONFIG.cors.origin,\n        credentials: SOCKETS_CONFIG.cors.credentials,\n      }\n    : false,\n  serveClient: SOCKETS_CONFIG.serveClient,\n})\nexport class PubSubGateway implements OnGatewayConnection, OnGatewayDisconnect {\n  @WebSocketServer() wss: Server;\n\n  private logger: Logger = new Logger('PubSubGateway');\n\n  constructor(private service: PubSubService) {}\n\n  @SubscribeMessage(PubSubClientEvents.Subscribe)\n  async subscribe(\n    @Client() client: UserClient,\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: SubscribeDto,\n  ): Promise<any> {\n    await this.service.subscribe(sessionMetadata, client, dto);\n    return { status: 'ok' };\n  }\n\n  @SubscribeMessage(PubSubClientEvents.Unsubscribe)\n  async unsubscribe(\n    @WSSessionMetadata() sessionMetadata: SessionMetadata,\n    @Client() client: UserClient,\n    @Body() dto: SubscribeDto,\n  ): Promise<any> {\n    await this.service.unsubscribe(sessionMetadata, client, dto);\n    return { status: 'ok' };\n  }\n\n  async handleConnection(client: Socket): Promise<void> {\n    this.logger.debug(`Client connected: ${client.id}`);\n  }\n\n  async handleDisconnect(client: Socket): Promise<void> {\n    await this.service.handleDisconnect(client.id);\n    this.logger.debug(`Client disconnected: ${client.id}`);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { PubSubGateway } from 'src/modules/pub-sub/pub-sub.gateway';\nimport { PubSubService } from 'src/modules/pub-sub/pub-sub.service';\nimport { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session.provider';\nimport { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider';\nimport { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider';\nimport { PubSubController } from 'src/modules/pub-sub/pub-sub.controller';\nimport { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service';\n\n@Module({\n  providers: [\n    PubSubGateway,\n    PubSubService,\n    PubSubAnalyticsService,\n    UserSessionProvider,\n    SubscriptionProvider,\n    RedisClientProvider,\n  ],\n  controllers: [PubSubController],\n})\nexport class PubSubModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockSocket,\n  MockType,\n  mockPubSubAnalyticsService,\n  mockCommonClientMetadata,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { PubSubService } from 'src/modules/pub-sub/pub-sub.service';\nimport { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session.provider';\nimport { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { SubscriptionType } from 'src/modules/pub-sub/constants';\nimport { UserSession } from 'src/modules/pub-sub/model/user-session';\nimport { RedisClientSubscriber } from 'src/modules/pub-sub/model/redis-client-subscriber';\nimport { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service';\nimport { ForbiddenException, NotFoundException } from '@nestjs/common';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nconst mockUserClient = new UserClient('socketId', mockSocket, 'databaseId');\n\nconst mockSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.Subscribe,\n};\n\nconst mockPSubscriptionDto = {\n  channel: 'channel-a',\n  type: SubscriptionType.PSubscribe,\n};\n\nconst getRedisClientFn = jest.fn();\nconst mockRedisClientSubscriber = new RedisClientSubscriber(\n  'databaseId',\n  getRedisClientFn,\n);\nconst mockUserSession = new UserSession(\n  mockUserClient,\n  mockRedisClientSubscriber,\n);\n\nconst mockSubscribe = jest.fn();\nconst mockUnsubscribe = jest.fn();\nmockUserSession['subscribe'] = mockSubscribe;\nmockUserSession['unsubscribe'] = mockUnsubscribe;\nmockUserSession['destroy'] = jest.fn();\n\nconst mockPublishDto = {\n  message: 'message-a',\n  channel: 'channel-a',\n};\n\ndescribe('PubSubService', () => {\n  const client = mockStandaloneRedisClient;\n  let service: PubSubService;\n  let sessionProvider: MockType<UserSessionProvider>;\n  let databaseClientFactory: MockType<DatabaseClientFactory>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        PubSubService,\n        UserSessionProvider,\n        SubscriptionProvider,\n        {\n          provide: UserSessionProvider,\n          useFactory: () => ({\n            getOrCreateUserSession: jest.fn(),\n            getUserSession: jest.fn(),\n            removeUserSession: jest.fn(),\n          }),\n        },\n        {\n          provide: PubSubAnalyticsService,\n          useFactory: mockPubSubAnalyticsService,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(PubSubService);\n    databaseClientFactory = await module.get(DatabaseClientFactory);\n    sessionProvider = await module.get(UserSessionProvider);\n\n    sessionProvider.getOrCreateUserSession.mockReturnValue(mockUserSession);\n    sessionProvider.getUserSession.mockReturnValue(mockUserSession);\n    sessionProvider.removeUserSession.mockReturnValue(undefined);\n    mockSubscribe.mockResolvedValue('OK');\n    mockUnsubscribe.mockResolvedValue('OK');\n    client.publish.mockResolvedValue(2);\n  });\n\n  describe('subscribe', () => {\n    it('should subscribe to a single channel', async () => {\n      await service.subscribe(mockSessionMetadata, mockUserClient, {\n        subscriptions: [mockSubscriptionDto],\n      });\n      expect(mockUserSession.subscribe).toHaveBeenCalledTimes(1);\n    });\n    it('should subscribe to a multiple channels', async () => {\n      await service.subscribe(mockSessionMetadata, mockUserClient, {\n        subscriptions: [mockSubscriptionDto, mockPSubscriptionDto],\n      });\n      expect(mockUserSession.subscribe).toHaveBeenCalledTimes(2);\n    });\n    it('should handle HTTP error', async () => {\n      try {\n        mockSubscribe.mockRejectedValueOnce(new NotFoundException('Not Found'));\n        await service.subscribe(mockSessionMetadata, mockUserClient, {\n          subscriptions: [mockSubscriptionDto],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n    it('should handle acl error', async () => {\n      try {\n        mockSubscribe.mockRejectedValueOnce(new Error('NOPERM'));\n        await service.subscribe(mockSessionMetadata, mockUserClient, {\n          subscriptions: [mockSubscriptionDto],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('unsubscribe', () => {\n    it('should unsubscribe from a single channel', async () => {\n      await service.unsubscribe(mockSessionMetadata, mockUserClient, {\n        subscriptions: [mockSubscriptionDto],\n      });\n      expect(mockUserSession.unsubscribe).toHaveBeenCalledTimes(1);\n    });\n    it('should unsubscribe from multiple channels', async () => {\n      await service.unsubscribe(mockSessionMetadata, mockUserClient, {\n        subscriptions: [mockSubscriptionDto, mockPSubscriptionDto],\n      });\n      expect(mockUserSession.unsubscribe).toHaveBeenCalledTimes(2);\n    });\n    it('should handle HTTP error', async () => {\n      try {\n        mockUnsubscribe.mockRejectedValueOnce(\n          new NotFoundException('Not Found'),\n        );\n        await service.unsubscribe(mockSessionMetadata, mockUserClient, {\n          subscriptions: [mockSubscriptionDto],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n    it('should handle acl error', async () => {\n      try {\n        mockUnsubscribe.mockRejectedValueOnce(new Error('NOPERM'));\n        await service.unsubscribe(mockSessionMetadata, mockUserClient, {\n          subscriptions: [mockSubscriptionDto],\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('publish', () => {\n    it('should publish using existing client', async () => {\n      const res = await service.publish(\n        mockCommonClientMetadata,\n        mockPublishDto,\n      );\n      expect(res).toEqual({ affected: 2 });\n    });\n    it('should throw an error when client not found during publishing', async () => {\n      databaseClientFactory.getOrCreateClient.mockRejectedValueOnce(\n        new NotFoundException('Not Found'),\n      );\n\n      try {\n        await service.publish(mockCommonClientMetadata, mockPublishDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n      }\n    });\n    it('should throw forbidden error when there is no permissions to publish', async () => {\n      databaseClientFactory.getOrCreateClient.mockRejectedValueOnce(\n        new Error('NOPERM'),\n      );\n\n      try {\n        await service.publish(mockCommonClientMetadata, mockPublishDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('handleDisconnect', () => {\n    it('should not do anything if no sessions', async () => {\n      sessionProvider.getUserSession.mockReturnValueOnce(undefined);\n      await service.handleDisconnect(mockUserClient.getId());\n      expect(sessionProvider.removeUserSession).toHaveBeenCalledTimes(0);\n    });\n    it('should call session.destroy and remove session', async () => {\n      await service.handleDisconnect(mockUserClient.getId());\n      expect(sessionProvider.removeUserSession).toHaveBeenCalledTimes(1);\n      expect(mockUserSession.destroy).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/pub-sub/pub-sub.service.ts",
    "content": "import { HttpException, Injectable, Logger } from '@nestjs/common';\nimport { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session.provider';\nimport { UserClient } from 'src/modules/pub-sub/model/user-client';\nimport { SubscribeDto } from 'src/modules/pub-sub/dto';\nimport { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider';\nimport { PublishResponse } from 'src/modules/pub-sub/dto/publish.response';\nimport { PublishDto } from 'src/modules/pub-sub/dto/publish.dto';\nimport { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service';\nimport { catchAclError } from 'src/utils';\nimport { ClientMetadata, SessionMetadata } from 'src/common/models';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\n@Injectable()\nexport class PubSubService {\n  private logger: Logger = new Logger('PubSubService');\n\n  constructor(\n    private readonly sessionProvider: UserSessionProvider,\n    private readonly subscriptionProvider: SubscriptionProvider,\n    private databaseClientFactory: DatabaseClientFactory,\n    private analyticsService: PubSubAnalyticsService,\n  ) {}\n\n  /**\n   * Subscribe to multiple channels\n   * @param sessionMetadata\n   * @param userClient\n   * @param dto\n   */\n  async subscribe(\n    sessionMetadata: SessionMetadata,\n    userClient: UserClient,\n    dto: SubscribeDto,\n  ) {\n    try {\n      this.logger.debug('Subscribing to channels(s)', sessionMetadata);\n\n      const session = this.sessionProvider.getOrCreateUserSession(\n        sessionMetadata,\n        userClient,\n      );\n      await Promise.all(\n        dto.subscriptions.map((subDto) =>\n          session.subscribe(\n            this.subscriptionProvider.createSubscription(userClient, subDto),\n          ),\n        ),\n      );\n      this.analyticsService.sendChannelSubscribeEvent(\n        sessionMetadata,\n        userClient.getDatabaseId(),\n        dto.subscriptions,\n      );\n    } catch (e) {\n      this.logger.error('Unable to create subscriptions', e, sessionMetadata);\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Unsubscribe from multiple channels\n   * @param sessionMetadata\n   * @param userClient\n   * @param dto\n   */\n  async unsubscribe(\n    sessionMetadata: SessionMetadata,\n    userClient: UserClient,\n    dto: SubscribeDto,\n  ) {\n    try {\n      this.logger.debug('Unsubscribing from channels(s)', sessionMetadata);\n\n      const session = this.sessionProvider.getOrCreateUserSession(\n        sessionMetadata,\n        userClient,\n      );\n      await Promise.all(\n        dto.subscriptions.map((subDto) =>\n          session.unsubscribe(\n            this.subscriptionProvider.createSubscription(userClient, subDto),\n          ),\n        ),\n      );\n      this.analyticsService.sendChannelUnsubscribeEvent(\n        sessionMetadata,\n        userClient.getDatabaseId(),\n      );\n    } catch (e) {\n      this.logger.error('Unable to unsubscribe', e, sessionMetadata);\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Publish a message to a particular channel\n   * @param clientMetadata\n   * @param dto\n   */\n  async publish(\n    clientMetadata: ClientMetadata,\n    dto: PublishDto,\n  ): Promise<PublishResponse> {\n    try {\n      this.logger.debug('Publishing message.', clientMetadata);\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n      const affected = await client.publish(dto.channel, dto.message);\n\n      this.analyticsService.sendMessagePublishedEvent(\n        clientMetadata.sessionMetadata,\n        clientMetadata.databaseId,\n        affected,\n      );\n\n      return {\n        affected,\n      };\n    } catch (e) {\n      this.logger.error('Unable to publish a message', e, clientMetadata);\n\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Handle Socket disconnection event\n   * Basically destroy the UserSession to remove Redis connection\n   * @param id\n   */\n  async handleDisconnect(id: string) {\n    this.logger.debug(`Handle disconnect event: ${id}`);\n    const session = this.sessionProvider.getUserSession(id);\n\n    if (session) {\n      session.destroy();\n      this.sessionProvider.removeUserSession(id);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/__tests__/query-library.factory.ts",
    "content": "import { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\nimport { QueryLibraryItem } from '../models/query-library';\nimport { QueryLibraryType } from '../models/query-library-type.enum';\nimport { QueryLibraryEntity } from '../entities/query-library.entity';\nimport { CreateQueryLibraryItemDto } from '../dto/create-query-library-item.dto';\nimport { SeedQueryLibraryItemDto } from '../dto/seed-query-library-item.dto';\n\nexport const queryLibraryItemFactory = Factory.define<QueryLibraryItem>(() => ({\n  id: faker.string.uuid(),\n  databaseId: faker.string.uuid(),\n  indexName: `idx:${faker.word.noun()}_vss`,\n  type: faker.helpers.enumValue(QueryLibraryType),\n  name: faker.lorem.words(3),\n  description: faker.lorem.sentence(),\n  query: `FT.SEARCH idx:${faker.word.noun()} \"*\"`,\n  createdAt: faker.date.recent(),\n  updatedAt: faker.date.recent(),\n}));\n\nexport const queryLibraryEntityFactory = Factory.define<QueryLibraryEntity>(\n  () => ({\n    ...queryLibraryItemFactory.build(),\n    database: undefined,\n    encryption: null,\n  }),\n);\n\nexport const createQueryLibraryItemDtoFactory =\n  Factory.define<CreateQueryLibraryItemDto>(() => {\n    const { indexName, name, query } = queryLibraryItemFactory.build();\n\n    return { indexName, name, query };\n  });\n\nexport const seedQueryLibraryItemDtoFactory =\n  Factory.define<SeedQueryLibraryItemDto>(() => {\n    const { indexName, name, description, query } =\n      queryLibraryItemFactory.build();\n\n    return { indexName, name, description, query };\n  });\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/dto/create-query-library-item.dto.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { QueryLibraryItem } from 'src/modules/query-library/models/query-library';\n\nexport class CreateQueryLibraryItemDto extends PickType(QueryLibraryItem, [\n  'indexName',\n  'name',\n  'query',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/dto/index.ts",
    "content": "export { CreateQueryLibraryItemDto } from './create-query-library-item.dto';\nexport { UpdateQueryLibraryItemDto } from './update-query-library-item.dto';\nexport { SeedQueryLibraryItemDto } from './seed-query-library-item.dto';\nexport { SeedQueryLibraryDto } from './seed-query-library.dto';\nexport { QueryLibraryFilterDto } from './query-library-filter.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/dto/query-library-filter.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsNotEmpty, IsOptional, IsString } from 'class-validator';\n\nexport class QueryLibraryFilterDto {\n  @ApiProperty({\n    description: 'Filter by index name',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  indexName: string;\n\n  @ApiPropertyOptional({\n    description: 'Search by name, description, or query content',\n    type: String,\n  })\n  @IsString()\n  @IsOptional()\n  search?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/dto/seed-query-library-item.dto.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { QueryLibraryItem } from 'src/modules/query-library/models/query-library';\n\nexport class SeedQueryLibraryItemDto extends PickType(QueryLibraryItem, [\n  'indexName',\n  'name',\n  'description',\n  'query',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/dto/seed-query-library.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { ArrayMinSize, IsArray, ValidateNested } from 'class-validator';\nimport { SeedQueryLibraryItemDto } from './seed-query-library-item.dto';\n\nexport class SeedQueryLibraryDto {\n  @ApiProperty({\n    description: 'Array of sample queries to seed',\n    type: () => SeedQueryLibraryItemDto,\n    isArray: true,\n  })\n  @IsArray()\n  @ArrayMinSize(1)\n  @ValidateNested({ each: true })\n  @Type(() => SeedQueryLibraryItemDto)\n  items: SeedQueryLibraryItemDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/dto/update-query-library-item.dto.ts",
    "content": "import { PartialType, PickType } from '@nestjs/swagger';\nimport { QueryLibraryItem } from 'src/modules/query-library/models/query-library';\n\nexport class UpdateQueryLibraryItemDto extends PartialType(\n  PickType(QueryLibraryItem, ['name', 'description', 'query'] as const),\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/entities/query-library.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  Index,\n  UpdateDateColumn,\n} from 'typeorm';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { Expose } from 'class-transformer';\nimport { QueryLibraryType } from 'src/modules/query-library/models/query-library-type.enum';\n\n@Entity('query_library')\n@Index('IDX_query_library_db_index_created', [\n  'databaseId',\n  'indexName',\n  'createdAt',\n])\nexport class QueryLibraryEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Expose()\n  databaseId: string;\n\n  @ManyToOne(() => DatabaseEntity, {\n    nullable: false,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn({ name: 'databaseId' })\n  @Expose()\n  database: DatabaseEntity;\n\n  @Column({ nullable: false })\n  @Expose()\n  indexName: string;\n\n  @Column({ nullable: false, default: QueryLibraryType.Saved })\n  @Expose()\n  type: string;\n\n  @Column({ nullable: false })\n  @Expose()\n  name: string;\n\n  @Column({ nullable: true, type: 'text' })\n  @Expose()\n  description?: string;\n\n  @Column({ nullable: false, type: 'text' })\n  @Expose()\n  query: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @CreateDateColumn()\n  @Expose()\n  createdAt: Date;\n\n  @UpdateDateColumn()\n  @Expose()\n  updatedAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/models/index.ts",
    "content": "export { QueryLibraryItem } from './query-library';\nexport { QueryLibraryType } from './query-library-type.enum';\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/models/query-library-type.enum.ts",
    "content": "export enum QueryLibraryType {\n  Sample = 'SAMPLE',\n  Saved = 'SAVED',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/models/query-library.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport { Expose } from 'class-transformer';\nimport { QueryLibraryType } from './query-library-type.enum';\n\nexport class QueryLibraryItem {\n  @ApiProperty({\n    description: 'Query library item id',\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: String,\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiProperty({\n    description: 'Index name the query is associated with',\n    type: String,\n    example: 'idx:bikes_vss',\n  })\n  @IsString()\n  @IsNotEmpty()\n  @Expose()\n  indexName: string;\n\n  @ApiProperty({\n    description: 'Query type',\n    enum: QueryLibraryType,\n  })\n  @IsEnum(QueryLibraryType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      QueryLibraryType,\n    )}.`,\n  })\n  @Expose()\n  type: QueryLibraryType;\n\n  @ApiProperty({\n    description: 'Display name for the query',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  @Expose()\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'Description of the query',\n    type: String,\n  })\n  @IsString()\n  @IsOptional()\n  @Expose()\n  description?: string;\n\n  @ApiProperty({\n    description: 'The query string',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  @Expose()\n  query: string;\n\n  @ApiProperty({\n    description: 'Date of creation',\n    type: Date,\n  })\n  @Expose()\n  createdAt: Date;\n\n  @ApiProperty({\n    description: 'Date of last update',\n    type: Date,\n  })\n  @Expose()\n  updatedAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/query-library.controller.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { faker } from '@faker-js/faker';\nimport {\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport { QueryLibraryController } from './query-library.controller';\nimport { QueryLibraryService } from './query-library.service';\nimport { QueryLibraryType } from './models/query-library-type.enum';\nimport {\n  queryLibraryItemFactory,\n  createQueryLibraryItemDtoFactory,\n  seedQueryLibraryItemDtoFactory,\n} from './__tests__/query-library.factory';\n\nconst mockDatabaseId = faker.string.uuid();\n\nconst mockClientMetadata = {\n  sessionMetadata: mockSessionMetadata,\n  databaseId: mockDatabaseId,\n};\n\nconst mockQueryLibraryService = () => ({\n  create: jest.fn(),\n  getList: jest.fn(),\n  getOne: jest.fn(),\n  update: jest.fn(),\n  delete: jest.fn(),\n  seed: jest.fn(),\n});\n\ndescribe('QueryLibraryController', () => {\n  let controller: QueryLibraryController;\n  let service: ReturnType<typeof mockQueryLibraryService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      controllers: [QueryLibraryController],\n      providers: [\n        {\n          provide: QueryLibraryService,\n          useFactory: mockQueryLibraryService,\n        },\n      ],\n    }).compile();\n\n    controller = module.get(QueryLibraryController);\n    service = module.get(QueryLibraryService);\n  });\n\n  describe('create', () => {\n    it('should create a query library item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      const dto = createQueryLibraryItemDtoFactory.build({\n        indexName: item.indexName,\n        name: item.name,\n        query: item.query,\n      });\n      service.create.mockResolvedValueOnce(item);\n\n      const result = await controller.create(mockClientMetadata as any, dto);\n\n      expect(result).toEqual(item);\n      expect(service.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        expect.objectContaining({ indexName: dto.indexName }),\n      );\n    });\n  });\n\n  describe('list', () => {\n    it('should return list of items', async () => {\n      const items = queryLibraryItemFactory.buildList(3, {\n        databaseId: mockDatabaseId,\n        indexName: 'idx:bikes_vss',\n      });\n      service.getList.mockResolvedValueOnce(items);\n\n      const result = await controller.list(mockClientMetadata as any, {\n        indexName: 'idx:bikes_vss',\n      });\n\n      expect(result).toEqual(items);\n      expect(service.getList).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName: 'idx:bikes_vss' },\n      );\n    });\n  });\n\n  describe('getOne', () => {\n    it('should return a single item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      service.getOne.mockResolvedValueOnce(item);\n\n      const result = await controller.getOne(\n        mockClientMetadata as any,\n        item.id,\n      );\n\n      expect(result).toEqual(item);\n    });\n  });\n\n  describe('update', () => {\n    it('should update a query library item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      const updatedName = faker.lorem.words(2);\n      const updatedItem = { ...item, name: updatedName };\n      service.update.mockResolvedValueOnce(updatedItem);\n\n      const result = await controller.update(\n        mockClientMetadata as any,\n        item.id,\n        { name: updatedName },\n      );\n\n      expect(result).toEqual(updatedItem);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a query library item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      service.delete.mockResolvedValueOnce(undefined);\n\n      await controller.delete(mockClientMetadata as any, item.id);\n\n      expect(service.delete).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        item.id,\n      );\n    });\n\n    it('should throw NotFoundException when item not found', async () => {\n      service.delete.mockRejectedValueOnce(new NotFoundException());\n\n      await expect(\n        controller.delete(mockClientMetadata as any, faker.string.uuid()),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('seed', () => {\n    it('should seed sample queries', async () => {\n      const items = queryLibraryItemFactory.buildList(2, {\n        databaseId: mockDatabaseId,\n        type: QueryLibraryType.Sample,\n      });\n      const dto = {\n        items: seedQueryLibraryItemDtoFactory.buildList(2),\n      };\n      service.seed.mockResolvedValueOnce(items);\n\n      const result = await controller.seed(mockClientMetadata as any, dto);\n\n      expect(result).toEqual(items);\n    });\n\n    it('should throw on service error', async () => {\n      const dto = {\n        items: seedQueryLibraryItemDtoFactory.buildList(1),\n      };\n      service.seed.mockRejectedValueOnce(new InternalServerErrorException());\n\n      await expect(\n        controller.seed(mockClientMetadata as any, dto),\n      ).rejects.toThrow(InternalServerErrorException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/query-library.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Patch,\n  Post,\n  Query,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { ClientMetadataParam } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\nimport { QueryLibraryService } from './query-library.service';\nimport { QueryLibraryItem } from './models/query-library';\nimport {\n  CreateQueryLibraryItemDto,\n  UpdateQueryLibraryItemDto,\n  SeedQueryLibraryDto,\n  QueryLibraryFilterDto,\n} from './dto';\n\n@ApiTags('Query Library')\n@UsePipes(new ValidationPipe({ transform: true }))\n@Controller('query-library')\nexport class QueryLibraryController {\n  constructor(private readonly service: QueryLibraryService) {}\n\n  @ApiEndpoint({\n    description: 'Create a query library item',\n    statusCode: 201,\n    responses: [\n      {\n        status: 201,\n        type: QueryLibraryItem,\n      },\n    ],\n  })\n  @Post()\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async create(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Body() dto: CreateQueryLibraryItemDto,\n  ): Promise<QueryLibraryItem> {\n    return this.service.create(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      dto,\n    );\n  }\n\n  @ApiEndpoint({\n    description: 'List query library items',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: QueryLibraryItem,\n        isArray: true,\n      },\n    ],\n  })\n  @Get()\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async list(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Query() filter: QueryLibraryFilterDto,\n  ): Promise<QueryLibraryItem[]> {\n    return this.service.getList(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      filter,\n    );\n  }\n\n  @ApiEndpoint({\n    description: 'Get a query library item by id',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: QueryLibraryItem,\n      },\n    ],\n  })\n  @Get('/:id')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async getOne(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Param('id') id: string,\n  ): Promise<QueryLibraryItem> {\n    return this.service.getOne(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      id,\n    );\n  }\n\n  @ApiEndpoint({\n    description: 'Update a query library item',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: QueryLibraryItem,\n      },\n    ],\n  })\n  @Patch('/:id')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async update(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Param('id') id: string,\n    @Body() dto: UpdateQueryLibraryItemDto,\n  ): Promise<QueryLibraryItem> {\n    return this.service.update(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      id,\n      dto,\n    );\n  }\n\n  @ApiEndpoint({\n    description: 'Delete a query library item',\n    statusCode: 200,\n  })\n  @Delete('/:id')\n  @ApiRedisParams()\n  async delete(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Param('id') id: string,\n  ): Promise<void> {\n    return this.service.delete(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      id,\n    );\n  }\n\n  @ApiEndpoint({\n    description: 'Seed sample queries into the query library',\n    statusCode: 201,\n    responses: [\n      {\n        status: 201,\n        type: QueryLibraryItem,\n        isArray: true,\n      },\n    ],\n  })\n  @Post('/seed')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async seed(\n    @ClientMetadataParam() clientMetadata: ClientMetadata,\n    @Body() dto: SeedQueryLibraryDto,\n  ): Promise<QueryLibraryItem[]> {\n    return this.service.seed(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      dto,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/query-library.module.ts",
    "content": "import { DynamicModule, Global, Module, Type } from '@nestjs/common';\nimport { QueryLibraryController } from './query-library.controller';\nimport { QueryLibraryService } from './query-library.service';\nimport { QueryLibraryRepository } from './repositories/query-library.repository';\nimport { LocalQueryLibraryRepository } from './repositories/local-query-library.repository';\n\n@Global()\n@Module({})\nexport class QueryLibraryModule {\n  static register(\n    queryLibraryRepository: Type<QueryLibraryRepository> = LocalQueryLibraryRepository,\n  ): DynamicModule {\n    return {\n      module: QueryLibraryModule,\n      controllers: [QueryLibraryController],\n      providers: [\n        QueryLibraryService,\n        {\n          provide: QueryLibraryRepository,\n          useClass: queryLibraryRepository,\n        },\n      ],\n      exports: [QueryLibraryService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/query-library.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { faker } from '@faker-js/faker';\nimport {\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport { QueryLibraryService } from './query-library.service';\nimport { QueryLibraryRepository } from './repositories/query-library.repository';\nimport { QueryLibraryType } from './models/query-library-type.enum';\nimport {\n  queryLibraryItemFactory,\n  createQueryLibraryItemDtoFactory,\n  seedQueryLibraryItemDtoFactory,\n} from './__tests__/query-library.factory';\n\nconst mockDatabaseId = faker.string.uuid();\n\nconst mockQueryLibraryRepository = () => ({\n  create: jest.fn(),\n  getList: jest.fn(),\n  getOne: jest.fn(),\n  update: jest.fn(),\n  delete: jest.fn(),\n  deleteByIndex: jest.fn(),\n  createBulk: jest.fn(),\n});\n\ndescribe('QueryLibraryService', () => {\n  let service: QueryLibraryService;\n  let repository: ReturnType<typeof mockQueryLibraryRepository>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        QueryLibraryService,\n        {\n          provide: QueryLibraryRepository,\n          useFactory: mockQueryLibraryRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get(QueryLibraryService);\n    repository = module.get(QueryLibraryRepository);\n  });\n\n  describe('create', () => {\n    it('should create a query library item with type SAVED', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n        type: QueryLibraryType.Saved,\n      });\n      const dto = createQueryLibraryItemDtoFactory.build({\n        indexName: item.indexName,\n        name: item.name,\n        query: item.query,\n      });\n      repository.create.mockResolvedValueOnce(item);\n\n      const result = await service.create(\n        mockSessionMetadata,\n        mockDatabaseId,\n        dto,\n      );\n\n      expect(result).toEqual(item);\n      expect(repository.create).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        expect.objectContaining({\n          indexName: dto.indexName,\n          type: QueryLibraryType.Saved,\n        }),\n      );\n    });\n\n    it('should throw on repository error', async () => {\n      const dto = createQueryLibraryItemDtoFactory.build();\n      repository.create.mockRejectedValueOnce(\n        new InternalServerErrorException(),\n      );\n\n      await expect(\n        service.create(mockSessionMetadata, mockDatabaseId, dto),\n      ).rejects.toThrow(InternalServerErrorException);\n    });\n  });\n\n  describe('getList', () => {\n    it('should return list of items', async () => {\n      const items = queryLibraryItemFactory.buildList(3, {\n        databaseId: mockDatabaseId,\n        indexName: 'idx:bikes_vss',\n      });\n      repository.getList.mockResolvedValueOnce(items);\n\n      const result = await service.getList(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName: 'idx:bikes_vss' },\n      );\n\n      expect(result).toEqual(items);\n    });\n\n    it('should return empty list', async () => {\n      repository.getList.mockResolvedValueOnce([]);\n\n      const result = await service.getList(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName: 'idx:empty_vss' },\n      );\n\n      expect(result).toEqual([]);\n    });\n  });\n\n  describe('getOne', () => {\n    it('should return a single item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      repository.getOne.mockResolvedValueOnce(item);\n\n      const result = await service.getOne(\n        mockSessionMetadata,\n        mockDatabaseId,\n        item.id,\n      );\n\n      expect(result).toEqual(item);\n    });\n\n    it('should throw NotFoundException if item not found', async () => {\n      repository.getOne.mockRejectedValueOnce(new NotFoundException());\n\n      await expect(\n        service.getOne(\n          mockSessionMetadata,\n          mockDatabaseId,\n          faker.string.uuid(),\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('update', () => {\n    it('should update a query library item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      const updatedName = faker.lorem.words(2);\n      const updatedItem = { ...item, name: updatedName };\n      repository.update.mockResolvedValueOnce(updatedItem);\n\n      const result = await service.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        item.id,\n        { name: updatedName },\n      );\n\n      expect(result).toEqual(updatedItem);\n    });\n\n    it('should throw NotFoundException if item not found', async () => {\n      repository.update.mockRejectedValueOnce(new NotFoundException());\n\n      await expect(\n        service.update(\n          mockSessionMetadata,\n          mockDatabaseId,\n          faker.string.uuid(),\n          { name: faker.lorem.words(2) },\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a query library item', async () => {\n      const item = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      repository.delete.mockResolvedValueOnce(undefined);\n\n      await service.delete(mockSessionMetadata, mockDatabaseId, item.id);\n\n      expect(repository.delete).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        item.id,\n      );\n    });\n\n    it('should throw NotFoundException if item not found', async () => {\n      repository.delete.mockRejectedValueOnce(new NotFoundException());\n\n      await expect(\n        service.delete(\n          mockSessionMetadata,\n          mockDatabaseId,\n          faker.string.uuid(),\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('deleteByIndex', () => {\n    it('should delete all items for an index', async () => {\n      const indexName = `idx:${faker.word.noun()}_vss`;\n      repository.deleteByIndex.mockResolvedValueOnce(undefined);\n\n      await service.deleteByIndex(\n        mockSessionMetadata,\n        mockDatabaseId,\n        indexName,\n      );\n\n      expect(repository.deleteByIndex).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        indexName,\n      );\n    });\n  });\n\n  describe('seed', () => {\n    it('should seed sample queries when none exist', async () => {\n      const indexName = 'idx:bikes_vss';\n      const seedItems = seedQueryLibraryItemDtoFactory.buildList(2, {\n        indexName,\n      });\n      const createdItems = queryLibraryItemFactory.buildList(2, {\n        databaseId: mockDatabaseId,\n        indexName,\n        type: QueryLibraryType.Sample,\n      });\n\n      repository.getList.mockResolvedValueOnce([]);\n      repository.createBulk.mockResolvedValueOnce(createdItems);\n\n      const result = await service.seed(mockSessionMetadata, mockDatabaseId, {\n        items: seedItems,\n      });\n\n      expect(result).toHaveLength(2);\n      expect(repository.getList).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName },\n      );\n      expect(repository.createBulk).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        seedItems.map((item) => ({\n          ...item,\n          type: QueryLibraryType.Sample,\n        })),\n      );\n    });\n\n    it('should only seed items that do not already exist by name', async () => {\n      const indexName = 'idx:bikes_vss';\n      const existingSample = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n        indexName,\n        type: QueryLibraryType.Sample,\n        name: 'Existing Query',\n      });\n      const seedItems = [\n        seedQueryLibraryItemDtoFactory.build({\n          indexName,\n          name: 'Existing Query',\n        }),\n        seedQueryLibraryItemDtoFactory.build({\n          indexName,\n          name: 'New Query',\n        }),\n      ];\n      const createdItem = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n        indexName,\n        type: QueryLibraryType.Sample,\n        name: 'New Query',\n      });\n\n      repository.getList.mockResolvedValueOnce([existingSample]);\n      repository.createBulk.mockResolvedValueOnce([createdItem]);\n\n      const result = await service.seed(mockSessionMetadata, mockDatabaseId, {\n        items: seedItems,\n      });\n\n      expect(result).toHaveLength(2);\n      expect(result).toEqual([existingSample, createdItem]);\n      expect(repository.createBulk).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockDatabaseId,\n        [\n          expect.objectContaining({\n            name: 'New Query',\n            type: QueryLibraryType.Sample,\n          }),\n        ],\n      );\n    });\n\n    it('should skip seeding when all sample queries already exist', async () => {\n      const indexName = 'idx:bikes_vss';\n      const existingItems = [\n        queryLibraryItemFactory.build({\n          databaseId: mockDatabaseId,\n          indexName,\n          type: QueryLibraryType.Sample,\n          name: 'Query A',\n        }),\n        queryLibraryItemFactory.build({\n          databaseId: mockDatabaseId,\n          indexName,\n          type: QueryLibraryType.Sample,\n          name: 'Query B',\n        }),\n      ];\n      const seedItems = [\n        seedQueryLibraryItemDtoFactory.build({ indexName, name: 'Query A' }),\n        seedQueryLibraryItemDtoFactory.build({ indexName, name: 'Query B' }),\n      ];\n\n      repository.getList.mockResolvedValueOnce(existingItems);\n\n      const result = await service.seed(mockSessionMetadata, mockDatabaseId, {\n        items: seedItems,\n      });\n\n      expect(result).toEqual(existingItems);\n      expect(repository.createBulk).not.toHaveBeenCalled();\n    });\n\n    it('should return empty array for empty items', async () => {\n      const result = await service.seed(mockSessionMetadata, mockDatabaseId, {\n        items: [],\n      });\n\n      expect(result).toEqual([]);\n      expect(repository.getList).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/query-library.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { SessionMetadata } from 'src/common/models';\nimport { QueryLibraryItem } from 'src/modules/query-library/models/query-library';\nimport { QueryLibraryType } from 'src/modules/query-library/models/query-library-type.enum';\nimport { QueryLibraryRepository } from 'src/modules/query-library/repositories/query-library.repository';\nimport {\n  CreateQueryLibraryItemDto,\n  UpdateQueryLibraryItemDto,\n  SeedQueryLibraryDto,\n  QueryLibraryFilterDto,\n} from 'src/modules/query-library/dto';\n\n@Injectable()\nexport class QueryLibraryService {\n  private logger = new Logger('QueryLibraryService');\n\n  constructor(\n    private readonly queryLibraryRepository: QueryLibraryRepository,\n  ) {}\n\n  async create(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    dto: CreateQueryLibraryItemDto,\n  ): Promise<QueryLibraryItem> {\n    this.logger.debug('Creating query library item', sessionMetadata);\n    return this.queryLibraryRepository.create(sessionMetadata, databaseId, {\n      ...dto,\n      type: QueryLibraryType.Saved,\n    });\n  }\n\n  async getList(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    filter: QueryLibraryFilterDto,\n  ): Promise<QueryLibraryItem[]> {\n    this.logger.debug('Listing query library items', sessionMetadata);\n    return this.queryLibraryRepository.getList(\n      sessionMetadata,\n      databaseId,\n      filter,\n    );\n  }\n\n  async getOne(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<QueryLibraryItem> {\n    this.logger.debug(`Getting query library item ${id}`, sessionMetadata);\n    return this.queryLibraryRepository.getOne(sessionMetadata, databaseId, id);\n  }\n\n  async update(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n    dto: UpdateQueryLibraryItemDto,\n  ): Promise<QueryLibraryItem> {\n    this.logger.debug(`Updating query library item ${id}`, sessionMetadata);\n    return this.queryLibraryRepository.update(\n      sessionMetadata,\n      databaseId,\n      id,\n      dto,\n    );\n  }\n\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<void> {\n    this.logger.debug(`Deleting query library item ${id}`, sessionMetadata);\n    return this.queryLibraryRepository.delete(sessionMetadata, databaseId, id);\n  }\n\n  async deleteByIndex(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    indexName: string,\n  ): Promise<void> {\n    this.logger.debug(\n      `Deleting query library items for index: ${indexName}`,\n      sessionMetadata,\n    );\n    return this.queryLibraryRepository.deleteByIndex(\n      sessionMetadata,\n      databaseId,\n      indexName,\n    );\n  }\n\n  async seed(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    dto: SeedQueryLibraryDto,\n  ): Promise<QueryLibraryItem[]> {\n    this.logger.debug('Seeding query library', sessionMetadata);\n\n    if (!dto.items.length) {\n      return [];\n    }\n\n    const { indexName } = dto.items[0];\n\n    const existing = await this.queryLibraryRepository.getList(\n      sessionMetadata,\n      databaseId,\n      { indexName },\n    );\n\n    const existingSampleNames = new Set(\n      existing\n        .filter((item) => item.type === QueryLibraryType.Sample)\n        .map((item) => item.name),\n    );\n\n    const newItems = dto.items\n      .filter((item) => !existingSampleNames.has(item.name))\n      .map((item) => ({\n        ...item,\n        type: QueryLibraryType.Sample,\n      }));\n\n    if (!newItems.length) {\n      this.logger.debug(\n        `All sample queries already exist for index: ${indexName}, skipping seed`,\n        sessionMetadata,\n      );\n      return existing;\n    }\n\n    const created = await this.queryLibraryRepository.createBulk(\n      sessionMetadata,\n      databaseId,\n      newItems,\n    );\n\n    return [...existing, ...created];\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/repositories/local-query-library.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { faker } from '@faker-js/faker';\nimport { NotFoundException } from '@nestjs/common';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { QueryLibraryEntity } from '../entities/query-library.entity';\nimport { LocalQueryLibraryRepository } from './local-query-library.repository';\nimport {\n  queryLibraryEntityFactory,\n  createQueryLibraryItemDtoFactory,\n  seedQueryLibraryItemDtoFactory,\n} from '../__tests__/query-library.factory';\n\nconst mockDatabaseId = faker.string.uuid();\n\nconst mockEncryptResult = {\n  data: 'encrypted_data',\n  encryption: 'KEYTAR',\n};\n\nconst mockEncryptionServiceFactory = jest.fn(() => ({\n  getAvailableEncryptionStrategies: jest.fn(),\n  isEncryptionAvailable: jest.fn().mockResolvedValue(true),\n  encrypt: jest.fn().mockResolvedValue(mockEncryptResult),\n  decrypt: jest.fn().mockImplementation((data) => data),\n  getEncryptionStrategy: jest.fn(),\n}));\n\nconst mockRepository = () => ({\n  save: jest.fn().mockImplementation((entity) => ({\n    ...entity,\n    id: entity.id || faker.string.uuid(),\n  })),\n  find: jest.fn(),\n  findOneBy: jest.fn(),\n  delete: jest.fn().mockResolvedValue({ affected: 1 }),\n  count: jest.fn().mockResolvedValue(0),\n});\n\ndescribe('LocalQueryLibraryRepository', () => {\n  let repository: LocalQueryLibraryRepository;\n  let typeormRepo: ReturnType<typeof mockRepository>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalQueryLibraryRepository,\n        {\n          provide: getRepositoryToken(QueryLibraryEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionServiceFactory,\n        },\n      ],\n    }).compile();\n\n    repository = module.get(LocalQueryLibraryRepository);\n    typeormRepo = module.get(getRepositoryToken(QueryLibraryEntity));\n  });\n\n  describe('create', () => {\n    it('should create and return a query library item', async () => {\n      const dto = createQueryLibraryItemDtoFactory.build();\n\n      const result = await repository.create(\n        mockSessionMetadata,\n        mockDatabaseId,\n        dto,\n      );\n\n      expect(result).toBeDefined();\n      expect(typeormRepo.save).toHaveBeenCalled();\n    });\n  });\n\n  describe('getList', () => {\n    it('should return list of items with indexName filter', async () => {\n      const entities = queryLibraryEntityFactory.buildList(2, {\n        databaseId: mockDatabaseId,\n        indexName: 'idx:bikes_vss',\n      });\n      typeormRepo.find.mockResolvedValueOnce(entities);\n\n      const result = await repository.getList(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName: 'idx:bikes_vss' },\n      );\n\n      expect(result).toBeDefined();\n      expect(result).toHaveLength(2);\n      expect(typeormRepo.find).toHaveBeenCalledWith({\n        where: {\n          databaseId: mockDatabaseId,\n          indexName: 'idx:bikes_vss',\n        },\n        order: { createdAt: 'ASC' },\n      });\n    });\n\n    it('should return list with only indexName filter', async () => {\n      const indexName = `idx:${faker.word.noun()}_vss`;\n      typeormRepo.find.mockResolvedValueOnce([]);\n\n      await repository.getList(mockSessionMetadata, mockDatabaseId, {\n        indexName,\n      });\n\n      expect(typeormRepo.find).toHaveBeenCalledWith({\n        where: { databaseId: mockDatabaseId, indexName },\n        order: { createdAt: 'ASC' },\n      });\n    });\n\n    it('should filter results by search term on name', async () => {\n      const indexName = `idx:${faker.word.noun()}_vss`;\n      const matchingEntity = Object.assign(new QueryLibraryEntity(), {\n        ...queryLibraryEntityFactory.build({\n          databaseId: mockDatabaseId,\n          indexName,\n        }),\n        name: 'Vector similarity search',\n      });\n      const nonMatchingEntity = Object.assign(new QueryLibraryEntity(), {\n        ...queryLibraryEntityFactory.build({\n          databaseId: mockDatabaseId,\n          indexName,\n        }),\n        name: 'Count documents',\n      });\n      typeormRepo.find.mockResolvedValueOnce([\n        matchingEntity,\n        nonMatchingEntity,\n      ]);\n\n      const result = await repository.getList(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName, search: 'similarity' },\n      );\n\n      expect(result).toHaveLength(1);\n      expect(result[0].name).toBe('Vector similarity search');\n    });\n\n    it('should return all items when search is not provided', async () => {\n      const indexName = `idx:${faker.word.noun()}_vss`;\n      const entities = queryLibraryEntityFactory\n        .buildList(3, {\n          databaseId: mockDatabaseId,\n          indexName,\n        })\n        .map((e) => Object.assign(new QueryLibraryEntity(), e));\n      typeormRepo.find.mockResolvedValueOnce(entities);\n\n      const result = await repository.getList(\n        mockSessionMetadata,\n        mockDatabaseId,\n        { indexName },\n      );\n\n      expect(result).toHaveLength(3);\n    });\n  });\n\n  describe('getOne', () => {\n    it('should return a single item', async () => {\n      const entity = queryLibraryEntityFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      typeormRepo.findOneBy.mockResolvedValueOnce(entity);\n\n      const result = await repository.getOne(\n        mockSessionMetadata,\n        mockDatabaseId,\n        entity.id,\n      );\n\n      expect(result).toBeDefined();\n      expect(typeormRepo.findOneBy).toHaveBeenCalledWith({\n        id: entity.id,\n        databaseId: mockDatabaseId,\n      });\n    });\n\n    it('should throw NotFoundException when item not found', async () => {\n      typeormRepo.findOneBy.mockResolvedValueOnce(null);\n\n      await expect(\n        repository.getOne(\n          mockSessionMetadata,\n          mockDatabaseId,\n          faker.string.uuid(),\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('update', () => {\n    it('should update an existing item', async () => {\n      const entity = queryLibraryEntityFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      typeormRepo.findOneBy.mockResolvedValueOnce(entity);\n\n      const updatedName = faker.lorem.words(2);\n      const result = await repository.update(\n        mockSessionMetadata,\n        mockDatabaseId,\n        entity.id,\n        { name: updatedName },\n      );\n\n      expect(result).toBeDefined();\n      expect(typeormRepo.findOneBy).toHaveBeenCalledWith({\n        id: entity.id,\n        databaseId: mockDatabaseId,\n      });\n      expect(typeormRepo.save).toHaveBeenCalled();\n    });\n\n    it('should re-encrypt all fields on partial update to maintain consistent encryption', async () => {\n      const entity = queryLibraryEntityFactory.build({\n        databaseId: mockDatabaseId,\n        name: 'encrypted_name',\n        query: 'encrypted_query',\n        description: 'encrypted_description',\n        encryption: 'KEYTAR',\n      });\n      typeormRepo.findOneBy.mockResolvedValueOnce(entity);\n\n      await repository.update(mockSessionMetadata, mockDatabaseId, entity.id, {\n        query: 'FT.SEARCH idx:bikes \"*\"',\n      });\n\n      const savedEntity = typeormRepo.save.mock.calls[0][0];\n      expect(savedEntity.encryption).toBe(mockEncryptResult.encryption);\n      expect(savedEntity.name).toBe(mockEncryptResult.data);\n      expect(savedEntity.query).toBe(mockEncryptResult.data);\n      expect(savedEntity.description).toBe(mockEncryptResult.data);\n    });\n\n    it('should throw NotFoundException when item not found', async () => {\n      typeormRepo.findOneBy.mockResolvedValueOnce(null);\n\n      await expect(\n        repository.update(\n          mockSessionMetadata,\n          mockDatabaseId,\n          faker.string.uuid(),\n          { name: faker.lorem.words(2) },\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete an item', async () => {\n      const entity = queryLibraryEntityFactory.build({\n        databaseId: mockDatabaseId,\n      });\n      typeormRepo.findOneBy.mockResolvedValueOnce(entity);\n\n      await repository.delete(mockSessionMetadata, mockDatabaseId, entity.id);\n\n      expect(typeormRepo.findOneBy).toHaveBeenCalledWith({\n        id: entity.id,\n        databaseId: mockDatabaseId,\n      });\n      expect(typeormRepo.delete).toHaveBeenCalledWith({\n        id: entity.id,\n        databaseId: mockDatabaseId,\n      });\n    });\n\n    it('should throw NotFoundException when item not found', async () => {\n      typeormRepo.findOneBy.mockResolvedValueOnce(null);\n\n      await expect(\n        repository.delete(\n          mockSessionMetadata,\n          mockDatabaseId,\n          faker.string.uuid(),\n        ),\n      ).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('deleteByIndex', () => {\n    it('should delete all items for an index', async () => {\n      const indexName = `idx:${faker.word.noun()}_vss`;\n\n      await repository.deleteByIndex(\n        mockSessionMetadata,\n        mockDatabaseId,\n        indexName,\n      );\n\n      expect(typeormRepo.delete).toHaveBeenCalledWith({\n        databaseId: mockDatabaseId,\n        indexName,\n      });\n    });\n  });\n\n  describe('createBulk', () => {\n    it('should bulk create items', async () => {\n      const dtos = seedQueryLibraryItemDtoFactory.buildList(3);\n\n      const result = await repository.createBulk(\n        mockSessionMetadata,\n        mockDatabaseId,\n        dtos,\n      );\n\n      expect(result).toHaveLength(3);\n      expect(typeormRepo.save).toHaveBeenCalledTimes(3);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/repositories/local-query-library.repository.ts",
    "content": "import { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { filter, isNull, omitBy, isUndefined } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { classToClass } from 'src/utils';\nimport { SessionMetadata } from 'src/common/models';\nimport { QueryLibraryEntity } from 'src/modules/query-library/entities/query-library.entity';\nimport { QueryLibraryItem } from 'src/modules/query-library/models/query-library';\nimport { QueryLibraryRepository } from './query-library.repository';\nimport { QueryLibraryFilterDto } from 'src/modules/query-library/dto';\n\n@Injectable()\nexport class LocalQueryLibraryRepository extends QueryLibraryRepository {\n  private logger = new Logger('LocalQueryLibraryRepository');\n\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(QueryLibraryEntity)\n    private readonly repository: Repository<QueryLibraryEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(this.encryptionService, [\n      'name',\n      'query',\n      'description',\n    ]);\n  }\n\n  async create(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    item: Partial<QueryLibraryItem>,\n  ): Promise<QueryLibraryItem> {\n    this.logger.debug('Creating query library item', sessionMetadata);\n\n    const entity = plainToInstance(QueryLibraryEntity, {\n      ...item,\n      databaseId,\n    });\n\n    const saved = await this.repository.save(\n      await this.modelEncryptor.encryptEntity(entity),\n    );\n\n    this.logger.debug('Query library item created', sessionMetadata);\n\n    return classToClass(\n      QueryLibraryItem,\n      await this.modelEncryptor.decryptEntity(saved, true),\n    );\n  }\n\n  async getList(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    queryFilter: QueryLibraryFilterDto,\n  ): Promise<QueryLibraryItem[]> {\n    this.logger.debug('Getting query library items', sessionMetadata);\n\n    const where: Record<string, any> = {\n      databaseId,\n      indexName: queryFilter.indexName,\n    };\n\n    const entities = await this.repository.find({\n      where,\n      order: { createdAt: 'ASC' },\n    });\n\n    this.logger.debug('Succeed to get query library items', sessionMetadata);\n\n    const decryptedEntities = await Promise.all(\n      entities.map<Promise<QueryLibraryEntity>>(async (entity) => {\n        try {\n          return await this.modelEncryptor.decryptEntity(entity);\n        } catch (e) {\n          return null;\n        }\n      }),\n    );\n\n    let items = filter(decryptedEntities, (entity) => !isNull(entity)).map(\n      (entity) => classToClass(QueryLibraryItem, entity),\n    );\n\n    if (queryFilter.search) {\n      const term = queryFilter.search.toLowerCase();\n      items = items.filter(\n        (item) =>\n          item.name?.toLowerCase().includes(term) ||\n          item.description?.toLowerCase().includes(term) ||\n          item.query?.toLowerCase().includes(term),\n      );\n    }\n\n    return items;\n  }\n\n  async getOne(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<QueryLibraryItem> {\n    this.logger.debug('Getting query library item', sessionMetadata);\n\n    const entity = await this.repository.findOneBy({ id, databaseId });\n\n    if (!entity) {\n      this.logger.error(\n        `Query library item with id:${id} and databaseId:${databaseId} was not found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(\n        `Query library item with id ${id} was not found`,\n      );\n    }\n\n    this.logger.debug(\n      `Succeed to get query library item ${id}`,\n      sessionMetadata,\n    );\n\n    return classToClass(\n      QueryLibraryItem,\n      await this.modelEncryptor.decryptEntity(entity, true),\n    );\n  }\n\n  async update(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n    data: Partial<QueryLibraryItem>,\n  ): Promise<QueryLibraryItem> {\n    this.logger.debug('Updating query library item', sessionMetadata);\n\n    const existing = await this.repository.findOneBy({ id, databaseId });\n\n    if (!existing) {\n      this.logger.error(\n        `Query library item with id:${id} and databaseId:${databaseId} was not found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(\n        `Query library item with id ${id} was not found`,\n      );\n    }\n\n    const decrypted = await this.modelEncryptor.decryptEntity(existing, true);\n    const updateData = omitBy(data, isUndefined);\n\n    const merged = plainToInstance(QueryLibraryEntity, {\n      ...decrypted,\n      ...updateData,\n      id,\n      databaseId,\n    });\n\n    const saved = await this.repository.save(\n      await this.modelEncryptor.encryptEntity(merged),\n    );\n\n    this.logger.debug('Query library item updated', sessionMetadata);\n\n    return classToClass(\n      QueryLibraryItem,\n      await this.modelEncryptor.decryptEntity(saved, true),\n    );\n  }\n\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<void> {\n    this.logger.debug('Deleting query library item', sessionMetadata);\n\n    const existing = await this.repository.findOneBy({ id, databaseId });\n\n    if (!existing) {\n      this.logger.error(\n        `Query library item with id:${id} and databaseId:${databaseId} was not found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(\n        `Query library item with id ${id} was not found`,\n      );\n    }\n\n    await this.repository.delete({ id, databaseId });\n\n    this.logger.debug('Query library item deleted', sessionMetadata);\n  }\n\n  async deleteByIndex(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    indexName: string,\n  ): Promise<void> {\n    this.logger.debug(\n      `Deleting query library items for index: ${indexName}`,\n      sessionMetadata,\n    );\n\n    await this.repository.delete({ databaseId, indexName });\n\n    this.logger.debug('Query library items deleted by index', sessionMetadata);\n  }\n\n  async createBulk(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    items: Partial<QueryLibraryItem>[],\n  ): Promise<QueryLibraryItem[]> {\n    this.logger.debug('Bulk creating query library items', sessionMetadata);\n\n    const results = await Promise.all(\n      items.map(async (item) => {\n        const entity = plainToInstance(QueryLibraryEntity, {\n          ...item,\n          databaseId,\n        });\n        return this.repository.save(\n          await this.modelEncryptor.encryptEntity(entity),\n        );\n      }),\n    );\n\n    this.logger.debug('Query library items bulk created', sessionMetadata);\n\n    const decrypted = await Promise.all(\n      results.map(async (entity) =>\n        this.modelEncryptor.decryptEntity(entity, true),\n      ),\n    );\n\n    return decrypted.map((entity) => classToClass(QueryLibraryItem, entity));\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/query-library/repositories/query-library.repository.ts",
    "content": "import { SessionMetadata } from 'src/common/models';\nimport { QueryLibraryItem } from 'src/modules/query-library/models/query-library';\nimport { QueryLibraryFilterDto } from 'src/modules/query-library/dto';\n\nexport abstract class QueryLibraryRepository {\n  abstract create(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    item: Partial<QueryLibraryItem>,\n  ): Promise<QueryLibraryItem>;\n\n  abstract getList(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    filter: QueryLibraryFilterDto,\n  ): Promise<QueryLibraryItem[]>;\n\n  abstract getOne(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<QueryLibraryItem>;\n\n  abstract update(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n    data: Partial<QueryLibraryItem>,\n  ): Promise<QueryLibraryItem>;\n\n  abstract delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<void>;\n\n  abstract deleteByIndex(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    indexName: string,\n  ): Promise<void>;\n\n  abstract createBulk(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    items: Partial<QueryLibraryItem>[],\n  ): Promise<QueryLibraryItem[]>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.spec.ts",
    "content": "import axios from 'axios';\nimport {\n  mockRdi,\n  mockRdiClientMetadata,\n  mockRdiConfigSchema,\n  mockRdiDryRunJob,\n  mockRdiJobsSchema,\n  mockRdiPipeline,\n  mockRdiSchema,\n  mockRdiUnauthorizedError,\n} from 'src/__mocks__';\nimport { sign } from 'jsonwebtoken';\nimport { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client';\nimport {\n  RdiDyRunJobStatus,\n  RdiPipeline,\n  RdiStatisticsStatus,\n} from 'src/modules/rdi/models';\nimport {\n  PipelineActions,\n  RdiUrl,\n  TOKEN_THRESHOLD,\n} from 'src/modules/rdi/constants';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\nconst createMockPostImplementation = (\n  targetsResponses: (any | Error)[],\n  sourcesResponses: (any | Error)[],\n) => {\n  let targetsCallCount = 0;\n  let sourcesCallCount = 0;\n\n  return (url: string) => {\n    if (url === RdiUrl.TestTargetsConnections) {\n      // eslint-disable-next-line no-plusplus\n      const response = targetsResponses[targetsCallCount++];\n      return response instanceof Error\n        ? Promise.reject(response)\n        : Promise.resolve(response);\n    }\n    if (url === RdiUrl.TestSourcesConnections) {\n      // eslint-disable-next-line no-plusplus\n      const response = sourcesResponses[sourcesCallCount++];\n      return response instanceof Error\n        ? Promise.reject(response)\n        : Promise.resolve(response);\n    }\n    return Promise.reject(new Error(`Unexpected URL: ${url}`));\n  };\n};\n\nconst axiosError = (status: number, message = 'Request failed') => ({\n  isAxiosError: true,\n  message,\n  status,\n});\n\ndescribe('ApiRdiClient', () => {\n  let client: ApiRdiClient;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    client = new ApiRdiClient(mockRdiClientMetadata, mockRdi);\n  });\n\n  describe('getSchema', () => {\n    it('should return schema', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: mockRdiConfigSchema });\n      mockedAxios.get.mockResolvedValueOnce({ data: mockRdiJobsSchema });\n\n      const result = await client.getSchema();\n\n      expect(result).toEqual(mockRdiSchema);\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetConfigSchema);\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetJobsSchema);\n    });\n\n    it('should throw error if request fails', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: mockRdiConfigSchema });\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getSchema()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('getPipeline', () => {\n    it('should return pipeline', async () => {\n      const data = { config: {} };\n      const mockedPipeline = Object.assign(new RdiPipeline(), {\n        jobs: {},\n        config: {},\n      });\n      mockedAxios.get.mockResolvedValueOnce({ data });\n\n      const result = await client.getPipeline();\n\n      expect(result).toEqual(mockedPipeline);\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetPipeline);\n    });\n\n    it('should throw error if request fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getPipeline()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('getStrategies', () => {\n    it('should return strategies data when API call is successful', async () => {\n      const mockData = { strategies: [{ id: 1, name: 'Strategy 1' }] };\n      mockedAxios.get.mockResolvedValueOnce({ data: mockData });\n\n      const result = await client.getStrategies();\n\n      expect(result).toEqual(mockData);\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetStrategies);\n    });\n\n    it('should throw an error when API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getStrategies()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n      expect(axios.get).toHaveBeenCalledWith(RdiUrl.GetStrategies);\n    });\n  });\n\n  describe('getConfigTemplate', () => {\n    const pipelineType = 'test-pipeline';\n    const dbType = 'test-db';\n\n    it('should return the config template when the API call is successful', async () => {\n      const expectedResponse = { template: 'some template' };\n      mockedAxios.get.mockResolvedValueOnce({ data: expectedResponse });\n\n      const result = await client.getConfigTemplate(pipelineType, dbType);\n\n      expect(result).toEqual(expectedResponse);\n      expect(axios.get).toHaveBeenCalledWith(\n        `${RdiUrl.GetConfigTemplate}/${pipelineType}/${dbType}`,\n      );\n    });\n\n    it('should throw an error when the API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(\n        client.getConfigTemplate(pipelineType, dbType),\n      ).rejects.toThrowError(mockRdiUnauthorizedError.message);\n    });\n  });\n\n  describe('getJobTemplate', () => {\n    const pipelineType = 'test-pipeline';\n\n    it('should return the job template when the API call is successful', async () => {\n      const expectedResponse = {\n        transformations: {\n          status: RdiDyRunJobStatus.Success,\n        },\n        commands: {\n          status: RdiDyRunJobStatus.Success,\n        },\n      };\n      mockedAxios.get.mockResolvedValueOnce({ data: expectedResponse });\n\n      const result = await client.getJobTemplate(pipelineType);\n\n      expect(result).toEqual(expectedResponse);\n      expect(axios.get).toHaveBeenCalledWith(\n        `${RdiUrl.GetJobTemplate}/${pipelineType}`,\n      );\n    });\n\n    it('should throw an error when the API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getJobTemplate(pipelineType)).rejects.toThrowError(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('deploy', () => {\n    it('should deploy the pipeline and poll for status', async () => {\n      const actionId = '123';\n      const postResponse = { data: { action_id: actionId } };\n      const getResponse = {\n        data: {\n          status: 'completed',\n          data: 'some data',\n          error: '',\n        },\n      };\n      // const postMock = jest.spyOn(client, 'post').mockResolvedValue(response);\n      mockedAxios.post.mockResolvedValueOnce(postResponse);\n      mockedAxios.get.mockResolvedValueOnce(getResponse);\n\n      const result = await client.deploy(mockRdiPipeline);\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        RdiUrl.Deploy,\n        expect.any(Object),\n      );\n      expect(result).toEqual(getResponse.data.data);\n    });\n\n    it('should throw an error if the deployment fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.deploy(mockRdiPipeline)).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('startPipeline', () => {\n    it('should start the pipeline and poll for status', async () => {\n      const actionId = '123';\n      const postResponse = { data: { action_id: actionId } };\n      const getResponse = {\n        data: {\n          status: 'completed',\n          data: 'some data',\n          error: '',\n        },\n      };\n\n      mockedAxios.post.mockResolvedValueOnce(postResponse);\n      mockedAxios.get.mockResolvedValueOnce(getResponse);\n\n      const result = await client.startPipeline();\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        RdiUrl.StartPipeline,\n        expect.any(Object),\n      );\n      expect(result).toEqual(getResponse.data.data);\n    });\n\n    it('should throw an error if start pipeline fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.startPipeline()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('stopPipeline', () => {\n    it('should stop the pipeline and poll for status', async () => {\n      const actionId = '123';\n      const postResponse = { data: { action_id: actionId } };\n      const getResponse = {\n        data: {\n          status: 'completed',\n          data: 'some data',\n          error: '',\n        },\n      };\n\n      mockedAxios.post.mockResolvedValueOnce(postResponse);\n      mockedAxios.get.mockResolvedValueOnce(getResponse);\n\n      const result = await client.stopPipeline();\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        RdiUrl.StopPipeline,\n        expect.any(Object),\n      );\n      expect(result).toEqual(getResponse.data.data);\n    });\n\n    it('should throw an error if stop pipeline fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.stopPipeline()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('resetPipeline', () => {\n    it('should reset the pipeline and poll for status', async () => {\n      const actionId = '123';\n      const postResponse = { data: { action_id: actionId } };\n      const getResponse = {\n        data: {\n          status: 'completed',\n          data: 'some data',\n          error: '',\n        },\n      };\n\n      mockedAxios.post.mockResolvedValueOnce(postResponse);\n      mockedAxios.get.mockResolvedValueOnce(getResponse);\n\n      const result = await client.resetPipeline();\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        RdiUrl.ResetPipeline,\n        expect.any(Object),\n      );\n      expect(result).toEqual(getResponse.data.data);\n    });\n\n    it('should throw an error if reset pipeline fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.resetPipeline()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('dryRunJob', () => {\n    it('should call the RDI client with the correct URL and data', async () => {\n      const mockResponse = {\n        transformations: {\n          status: RdiDyRunJobStatus.Success,\n        },\n        commands: {\n          status: RdiDyRunJobStatus.Success,\n        },\n      };\n      mockedAxios.post.mockResolvedValueOnce({ data: mockResponse });\n\n      const result = await client.dryRunJob(mockRdiDryRunJob);\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        RdiUrl.DryRunJob,\n        mockRdiDryRunJob,\n      );\n      expect(result).toEqual(mockResponse);\n    });\n\n    it('should throw an error if the client call fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.dryRunJob(mockRdiDryRunJob)).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('testConnections', () => {\n    const config = { sources: { source1: {} } };\n\n    it('should return a successful response', async () => {\n      const expectedTargetsResponse = {\n        targets: {\n          target: {\n            status: 'success',\n          },\n        },\n      };\n      const expectedSourcesResponse = {\n        source1: {\n          connected: true,\n          error: '',\n        },\n      };\n\n      const targetsResponses = [{ data: expectedTargetsResponse }];\n\n      const sourcesResponses = [{ data: expectedSourcesResponse.source1 }];\n\n      mockedAxios.post.mockImplementation(\n        createMockPostImplementation(targetsResponses, sourcesResponses),\n      );\n\n      const response = await client.testConnections(config);\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(2);\n\n      expect(response).toEqual({\n        sources: expectedSourcesResponse,\n        ...expectedTargetsResponse,\n      });\n    });\n\n    it('should return a successful response with multiple sources', async () => {\n      const expectedTargetsResponse = {\n        targets: {\n          target: {\n            status: 'success',\n          },\n        },\n      };\n\n      const expectedSourcesResponse = {\n        source1: {\n          connected: true,\n          error: '',\n        },\n        source2: {\n          connected: false,\n          error: 'Connection failed',\n        },\n      };\n\n      const targetsResponses = [{ data: expectedTargetsResponse }];\n\n      const sourcesResponses = [\n        { data: { connected: true, error: '' } },\n        { data: { connected: false, error: 'Connection failed' } },\n      ];\n\n      mockedAxios.post.mockImplementation(\n        createMockPostImplementation(targetsResponses, sourcesResponses),\n      );\n\n      const response = await client.testConnections({\n        sources: {\n          source1: {},\n          source2: {},\n        },\n      });\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(3);\n\n      expect(response).toEqual({\n        sources: expectedSourcesResponse,\n        ...expectedTargetsResponse,\n      });\n    });\n\n    it('should throw an error if the requests fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.testConnections(config)).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n      expect(mockedAxios.post).toHaveBeenCalledWith(\n        RdiUrl.TestTargetsConnections,\n        config,\n      );\n    });\n\n    it('should return targets when single source test fails', async () => {\n      const expectedTargetsResponse = {\n        targets: { target1: { status: 'success' } },\n      };\n\n      const loggerErrorSpy = jest\n        .spyOn(client['logger'], 'error')\n        .mockImplementation();\n\n      const targetsResponses = [{ data: expectedTargetsResponse }];\n\n      const sourcesResponses = [new Error('Sources request failed')];\n\n      mockedAxios.post.mockImplementation(\n        createMockPostImplementation(targetsResponses, sourcesResponses),\n      );\n\n      const response = await client.testConnections(config);\n\n      expect(response).toEqual({\n        targets: expectedTargetsResponse.targets,\n        sources: {\n          source1: {\n            connected: false,\n            error: 'Failed to test source connection.',\n          },\n        },\n      });\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(2);\n      loggerErrorSpy.mockRestore();\n    });\n\n    it('should return targets when one of multiple source tests fails', async () => {\n      const expectedTargetsResponse = {\n        targets: { target1: { status: 'success' } },\n      };\n\n      const loggerErrorSpy = jest\n        .spyOn(client['logger'], 'error')\n        .mockImplementation();\n\n      const targetsResponses = [{ data: expectedTargetsResponse }];\n\n      const sourcesResponses = [\n        { data: { connected: true, error: '' } },\n        new Error('Sources request failed'),\n      ];\n\n      mockedAxios.post.mockImplementation(\n        createMockPostImplementation(targetsResponses, sourcesResponses),\n      );\n\n      const response = await client.testConnections({\n        sources: {\n          source1: {},\n          source2: {},\n        },\n      });\n\n      expect(response).toEqual({\n        targets: expectedTargetsResponse.targets,\n        sources: {\n          source1: { connected: true, error: '' },\n          source2: {\n            connected: false,\n            error: 'Failed to test source connection.',\n          },\n        },\n      });\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(3);\n      loggerErrorSpy.mockRestore();\n    });\n\n    it('should map 405 to the friendly message for a single source', async () => {\n      const expectedTargetsResponse = {\n        targets: { target1: { status: 'success' } },\n      };\n\n      mockedAxios.post\n        .mockResolvedValueOnce({ data: expectedTargetsResponse }) // TestTargetsConnections\n        .mockRejectedValueOnce(axiosError(405)); // TestSourcesConnections\n\n      const response = await client.testConnections({\n        sources: { source1: {} },\n      });\n\n      expect(response).toEqual({\n        targets: expectedTargetsResponse.targets,\n        sources: {\n          source1: {\n            connected: false,\n            error:\n              'Testing source connections is not supported in your RDI version. Please upgrade to version 1.6.0 or later.',\n          },\n        },\n      });\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(2);\n    });\n\n    it('should map the 405 only to the failing source among multiple sources', async () => {\n      const expectedTargetsResponse = {\n        targets: { target1: { status: 'success' } },\n      };\n\n      mockedAxios.post\n        .mockResolvedValueOnce({ data: expectedTargetsResponse }) // TestTargetsConnections\n        .mockResolvedValueOnce({ data: { connected: true, error: '' } }) // source1 OK\n        .mockRejectedValueOnce(axiosError(405)); // source2 405\n\n      const response = await client.testConnections({\n        sources: { source1: {}, source2: {} },\n      });\n\n      expect(response).toEqual({\n        targets: expectedTargetsResponse.targets,\n        sources: {\n          source1: { connected: true, error: '' },\n          source2: {\n            connected: false,\n            error:\n              'Testing source connections is not supported in your RDI version. Please upgrade to version 1.6.0 or later.',\n          },\n        },\n      });\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(3);\n    });\n\n    it('should handle mixed non-405 and 405 source failures', async () => {\n      const expectedTargetsResponse = {\n        targets: { target1: { status: 'success' } },\n      };\n\n      mockedAxios.post\n        .mockResolvedValueOnce({ data: expectedTargetsResponse }) // targets\n        .mockRejectedValueOnce(axiosError(500, 'Internal error')) // source1 500\n        .mockRejectedValueOnce(axiosError(405)); // source2 405\n\n      const response = await client.testConnections({\n        sources: { source1: {}, source2: {} },\n      });\n\n      expect(response).toEqual({\n        targets: expectedTargetsResponse.targets,\n        sources: {\n          source1: {\n            connected: false,\n            error: 'Failed to test source connection.',\n          },\n          source2: {\n            connected: false,\n            error:\n              'Testing source connections is not supported in your RDI version. Please upgrade to version 1.6.0 or later.',\n          },\n        },\n      });\n\n      expect(mockedAxios.post).toHaveBeenCalledTimes(3);\n    });\n  });\n\n  describe('getPipelineStatus', () => {\n    it('should return pipeline status when API call is successful', async () => {\n      const mockApiResponse = {\n        data: {\n          pipelines: {\n            default: {\n              status: 'ready',\n              state: 'cdc',\n            },\n          },\n          components: {\n            processor: { status: 'ready', version: '1.0.0' },\n          },\n        },\n      };\n      mockedAxios.get.mockResolvedValueOnce(mockApiResponse);\n\n      const result = await client.getPipelineStatus();\n\n      // Expect transformed response format\n      expect(result).toEqual({\n        status: 'ready',\n        state: 'cdc',\n      });\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetPipelineStatus);\n    });\n\n    it('should throw an error when API call fails', async () => {\n      mockedAxios.get.mockRejectedValue(mockRdiUnauthorizedError);\n\n      await expect(client.getPipelineStatus()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetPipelineStatus);\n    });\n  });\n\n  describe('getStatistics', () => {\n    it('should return success status and transformed data when API call succeeds', async () => {\n      const mockApiResponse = {\n        rdi_pipeline_status: {\n          rdi_version: '1.0.0',\n          address: 'redis://localhost:6379',\n          run_status: 'running',\n          sync_mode: 'streaming',\n        },\n        processing_performance: {\n          total_batches: 100,\n          batch_size_avg: 1.5,\n          process_time_avg: 50,\n          ack_time_avg: 0.5,\n          read_time_avg: 10,\n          rec_per_sec_avg: 1000,\n          total_time_avg: 60,\n        },\n        connections: {},\n        data_streams: { totals: {}, streams: {} },\n        clients: {},\n        offsets: {},\n        snapshot_status: 'completed',\n      };\n      mockedAxios.get.mockResolvedValue({ data: mockApiResponse });\n\n      const result = await client.getStatistics();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetStatistics);\n      expect(result.status).toBe(RdiStatisticsStatus.Success);\n      expect(result.data).toBeDefined();\n      // 2 sections: General info, Processing performance\n      // (Target Connections, Data Streams, and Clients are filtered out because they have empty data)\n      expect(result.data.sections).toHaveLength(2);\n    });\n\n    it('should return fail status and error message when API call fails', async () => {\n      mockedAxios.get.mockRejectedValue(mockRdiUnauthorizedError);\n\n      const result = await client.getStatistics();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.GetStatistics);\n      expect(result.status).toBe(RdiStatisticsStatus.Fail);\n      expect(result.error).toBe(mockRdiUnauthorizedError.message);\n    });\n  });\n\n  describe('getJobFunctions', () => {\n    it('should return job functions', async () => {\n      const expectedResponse = { jobFunctions: ['jobFunc1', 'jobFunc2'] };\n      mockedAxios.get.mockResolvedValueOnce({ data: expectedResponse });\n\n      const response = await client.getJobFunctions();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrl.JobFunctions);\n      expect(response).toEqual(expectedResponse);\n    });\n\n    it('should throw an error if the API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getJobFunctions()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('connect', () => {\n    it('should set auth and authorization headers on successful login', async () => {\n      const mockedAccessToken = sign(\n        { exp: Math.trunc(Date.now() / 1000) + 3600 },\n        'test',\n      );\n      const expectedAuthorizationHeader = `Bearer ${mockedAccessToken}`;\n\n      mockedAxios.post.mockResolvedValue({\n        status: 200,\n        data: {\n          access_token: mockedAccessToken,\n        },\n      });\n\n      await client.connect();\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, {\n        username: mockRdi.username,\n        password: mockRdi.password,\n      });\n\n      expect(client['auth']['jwt']).toEqual(mockedAccessToken);\n      expect(mockedAxios.defaults.headers.common['Authorization']).toEqual(\n        expectedAuthorizationHeader,\n      );\n    });\n\n    it('should set dummy authorization headers in dev mode when login is disabled', async () => {\n      mockedAxios.post.mockRejectedValueOnce({\n        status: 404,\n      });\n\n      await client.connect();\n\n      expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, {\n        username: mockRdi.username,\n        password: mockRdi.password,\n      });\n\n      expect(client['auth']['jwt']).toEqual(expect.any(String));\n      expect(mockedAxios.defaults.headers.common['Authorization']).toEqual(\n        `Bearer ${client['auth']['jwt']}`,\n      );\n    });\n\n    it('should throw an error if login fails', async () => {\n      mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.connect()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n\n  describe('ensureAuth', () => {\n    let connectSpy: jest.SpyInstance;\n\n    beforeEach(() => {\n      connectSpy = jest.spyOn(client, 'connect').mockResolvedValue(undefined);\n    });\n\n    it('should not call connect if token is not expired', async () => {\n      const exp = Math.trunc(Date.now() / 1000 + TOKEN_THRESHOLD + 3600);\n      const mockedAccessToken = sign({ exp }, 'test');\n      client['auth'] = { exp, jwt: mockedAccessToken };\n      await client.ensureAuth();\n      expect(connectSpy).not.toHaveBeenCalled();\n    });\n\n    it('should call connect if token is expired', async () => {\n      const exp = Math.trunc(Date.now() / 1000 - 3600);\n      const mockedAccessToken = sign({ exp }, 'test');\n      client['auth'] = { exp, jwt: mockedAccessToken };\n      await client.ensureAuth();\n      expect(connectSpy).toHaveBeenCalled();\n    });\n  });\n\n  describe('pollActionStatus', () => {\n    const responseData = 'some data';\n    const actionId = 'test-action-id';\n\n    it('should return response data on success', async () => {\n      mockedAxios.get.mockResolvedValueOnce({\n        data: { status: 'completed', data: responseData },\n      });\n\n      const result = await client['pollActionStatus'](\n        actionId,\n        PipelineActions.Deploy,\n      );\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `${RdiUrl.Action}/${actionId}`,\n        { signal: undefined },\n      );\n      expect(result).toEqual(responseData);\n    });\n\n    it('should throw an error if action status is failed', async () => {\n      mockedAxios.get.mockResolvedValueOnce({\n        data: { status: 'failed', error: { message: 'Test error' } },\n      });\n\n      await expect(\n        client['pollActionStatus'](actionId, PipelineActions.Deploy),\n      ).rejects.toThrow('Test error');\n    });\n\n    it('should throw an error if an error occurs during polling', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(\n        client['pollActionStatus'](actionId, PipelineActions.Deploy),\n      ).rejects.toThrow(mockRdiUnauthorizedError.message);\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        `${RdiUrl.Action}/${actionId}`,\n        { signal: undefined },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.ts",
    "content": "import { sign } from 'jsonwebtoken';\nimport axios, { AxiosInstance } from 'axios';\nimport { plainToInstance } from 'class-transformer';\nimport { HttpStatus, Logger } from '@nestjs/common';\n\nimport { RdiClient } from 'src/modules/rdi/client/rdi.client';\nimport {\n  RdiUrl,\n  RDI_TIMEOUT,\n  TOKEN_THRESHOLD,\n  POLLING_INTERVAL,\n  MAX_POLLING_TIME,\n  WAIT_BEFORE_POLLING,\n  PipelineActions,\n  DEFAULT_RDI_VERSION,\n} from 'src/modules/rdi/constants';\nimport {\n  RdiDryRunJobDto,\n  RdiDryRunJobResponseDto,\n  RdiTestSourceConnectionResult,\n  RdiTemplateResponseDto,\n  RdiTestTargetConnectionResult,\n  RdiTestConnectionsResponseDto,\n} from 'src/modules/rdi/dto';\nimport {\n  RdiPipelineDeployFailedException,\n  RdiPipelineInternalServerErrorException,\n  parseErrorMessage,\n  wrapRdiPipelineError,\n  RdiResetPipelineFailedException,\n  RdiStartPipelineFailedException,\n  RdiStopPipelineFailedException,\n} from 'src/modules/rdi/exceptions';\nimport {\n  RdiPipeline,\n  RdiStatisticsResult,\n  RdiStatisticsStatus,\n  RdiClientMetadata,\n  Rdi,\n  RdiPipelineStatus,\n} from 'src/modules/rdi/models';\nimport { RdiPipelineTimeoutException } from 'src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception';\nimport * as https from 'https';\nimport {\n  convertApiDataToRdiPipeline,\n  convertRdiPipelineToApiPayload,\n} from 'src/modules/rdi/utils/pipeline.util';\nimport {\n  GetStatisticsResponse,\n  GetStatusResponse,\n} from 'src/modules/rdi/client/api/v1/responses';\nimport {\n  transformStatisticsResponse,\n  transformStatus,\n} from 'src/modules/rdi/client/api/v1/transformers';\n\ninterface ConnectionsConfig {\n  sources: Record<string, Record<string, unknown>>;\n}\n\nexport class ApiRdiClient extends RdiClient {\n  protected readonly client: AxiosInstance;\n\n  protected readonly logger = new Logger('ApiRdiClient');\n\n  private auth: { jwt: string; exp: number };\n\n  constructor(clientMetadata: RdiClientMetadata, rdi: Rdi) {\n    super(clientMetadata, rdi);\n    this.client = axios.create({\n      baseURL: rdi.url,\n      timeout: RDI_TIMEOUT,\n      httpsAgent: new https.Agent({\n        // we might work with self-signed certificates for local builds\n        rejectUnauthorized: false, // lgtm[js/disabling-certificate-validation]\n      }),\n    });\n  }\n\n  private async loginDev(): Promise<string> {\n    return sign({}, 'dev', { expiresIn: '1h' });\n  }\n\n  private async login(): Promise<string> {\n    try {\n      const response = await this.client.post(RdiUrl.Login, {\n        username: this.rdi.username,\n        password: this.rdi.password,\n      });\n\n      return response.data.access_token;\n    } catch (e) {\n      // If /login endpoint is not found we assume that RDI is in dev mode\n      if (e.status === HttpStatus.NOT_FOUND) {\n        return this.loginDev();\n      }\n\n      throw e;\n    }\n  }\n\n  async getSchema(): Promise<object> {\n    try {\n      const [config, jobs] = await Promise.all([\n        this.client.get(RdiUrl.GetConfigSchema).then(({ data }) => data),\n        this.client.get(RdiUrl.GetJobsSchema).then(({ data }) => data),\n      ]);\n\n      return {\n        config,\n        jobs,\n      };\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getPipeline(): Promise<RdiPipeline> {\n    try {\n      const { data } = await this.client.get(RdiUrl.GetPipeline);\n\n      return convertApiDataToRdiPipeline(data);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getStrategies(): Promise<object> {\n    try {\n      const response = await this.client.get(RdiUrl.GetStrategies);\n      return response.data;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getConfigTemplate(\n    pipelineType: string,\n    dbType: string,\n  ): Promise<RdiTemplateResponseDto> {\n    try {\n      const response = await this.client.get(\n        `${RdiUrl.GetConfigTemplate}/${pipelineType}/${dbType}`,\n      );\n      return response.data;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getJobTemplate(pipelineType: string): Promise<RdiTemplateResponseDto> {\n    try {\n      const response = await this.client.get(\n        `${RdiUrl.GetJobTemplate}/${pipelineType}`,\n      );\n      return response.data;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async deploy(pipeline: RdiPipeline): Promise<void> {\n    try {\n      const response = await this.client.post(\n        RdiUrl.Deploy,\n        convertRdiPipelineToApiPayload(pipeline),\n      );\n\n      const actionId = response.data.action_id;\n\n      return await this.pollActionStatus(actionId, PipelineActions.Deploy);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async stopPipeline(): Promise<void> {\n    try {\n      const response = await this.client.post(RdiUrl.StopPipeline, {});\n      const actionId = response.data.action_id;\n\n      return await this.pollActionStatus(actionId, PipelineActions.Stop);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async startPipeline(): Promise<void> {\n    try {\n      const response = await this.client.post(RdiUrl.StartPipeline, {});\n      const actionId = response.data.action_id;\n\n      return await this.pollActionStatus(actionId, PipelineActions.Start);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async resetPipeline(): Promise<void> {\n    try {\n      const response = await this.client.post(RdiUrl.ResetPipeline, {});\n      const actionId = response.data.action_id;\n\n      return await this.pollActionStatus(actionId, PipelineActions.Reset);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async dryRunJob(dto: RdiDryRunJobDto): Promise<RdiDryRunJobResponseDto> {\n    try {\n      const { data } = await this.client.post(RdiUrl.DryRunJob, dto);\n\n      return data;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async testConnections(\n    config: ConnectionsConfig,\n  ): Promise<RdiTestConnectionsResponseDto> {\n    let targets: Record<string, RdiTestTargetConnectionResult> = {};\n    const sources: Record<string, RdiTestSourceConnectionResult> = {};\n\n    try {\n      const targetsResponse = await this.client.post(\n        RdiUrl.TestTargetsConnections,\n        config,\n      );\n      targets = targetsResponse.data.targets;\n    } catch (error) {\n      throw wrapRdiPipelineError(error);\n    }\n\n    const sourceConfigs = Object.keys(config.sources || {});\n\n    if (sourceConfigs.length === 0) {\n      return { targets, sources };\n    }\n\n    await Promise.all(\n      sourceConfigs.map(async (source) => {\n        try {\n          const response = await this.client.post(\n            RdiUrl.TestSourcesConnections,\n            { ...config.sources[source] },\n          );\n          sources[source] = response.data;\n        } catch (error: any) {\n          // Older versions of RDI (below 1.6.0) don't support testing sources connections\n          // RDI returns 405 Method Not Allowed for non existing endpoints\n          const status = error?.status;\n          if (status === 405) {\n            sources[source] = {\n              connected: false,\n              error:\n                'Testing source connections is not supported in your RDI version. Please upgrade to version 1.6.0 or later.',\n            };\n          } else {\n            // Something went wrong with testing source connection\n            sources[source] = {\n              connected: false,\n              error: 'Failed to test source connection.',\n            };\n          }\n        }\n      }),\n    );\n\n    return { targets, sources };\n  }\n\n  async getPipelineStatus(): Promise<RdiPipelineStatus> {\n    try {\n      const { data } = await this.client.get<GetStatusResponse>(\n        RdiUrl.GetPipelineStatus,\n      );\n\n      return transformStatus(data);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getVersion(): Promise<string> {\n    try {\n      const { data } = await this.client.get<GetStatusResponse>(\n        RdiUrl.GetPipelineStatus,\n      );\n\n      return data?.components?.processor?.version || DEFAULT_RDI_VERSION;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getStatistics(): Promise<RdiStatisticsResult> {\n    try {\n      const { data } = await this.client.get<GetStatisticsResponse>(\n        RdiUrl.GetStatistics,\n      );\n\n      return plainToInstance(RdiStatisticsResult, {\n        status: RdiStatisticsStatus.Success,\n        data: {\n          sections: transformStatisticsResponse(data),\n        },\n      });\n    } catch (e) {\n      const message: string = parseErrorMessage(e);\n      return { status: RdiStatisticsStatus.Fail, error: message };\n    }\n  }\n\n  async getJobFunctions(): Promise<object> {\n    try {\n      const response = await this.client.get(RdiUrl.JobFunctions);\n      return response.data;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async connect(): Promise<void> {\n    try {\n      const accessToken = await this.login();\n\n      const { exp } = JSON.parse(\n        Buffer.from(accessToken.split('.')[1], 'base64').toString(),\n      );\n\n      this.auth = {\n        jwt: accessToken,\n        exp,\n      };\n      this.client.defaults.headers.common['Authorization'] =\n        `Bearer ${accessToken}`;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async ensureAuth(): Promise<void> {\n    const expiresIn = this.auth.exp * 1_000 - Date.now();\n\n    if (expiresIn < TOKEN_THRESHOLD) {\n      await this.connect();\n    }\n  }\n\n  private async pollActionStatus(\n    actionId: string,\n    action: PipelineActions,\n    abortSignal?: AbortSignal,\n  ): Promise<any> {\n    await new Promise((resolve) => setTimeout(resolve, WAIT_BEFORE_POLLING));\n\n    const startTime = Date.now();\n\n    while (true) {\n      if (abortSignal?.aborted) {\n        throw new RdiPipelineInternalServerErrorException(\n          'Operation is aborted',\n        );\n      }\n      if (Date.now() - startTime > MAX_POLLING_TIME) {\n        throw new RdiPipelineTimeoutException();\n      }\n\n      try {\n        const response = await this.client.get(`${RdiUrl.Action}/${actionId}`, {\n          signal: abortSignal,\n        });\n        const { status, data, error } = response.data;\n\n        if (status === 'failed') {\n          switch (action) {\n            case PipelineActions.Deploy:\n              throw new RdiPipelineDeployFailedException(error?.message);\n            case PipelineActions.Reset:\n              throw new RdiResetPipelineFailedException(error?.message);\n            case PipelineActions.Start:\n              throw new RdiStartPipelineFailedException(error?.message);\n            case PipelineActions.Stop:\n              throw new RdiStopPipelineFailedException(error?.message);\n            default:\n              throw new RdiPipelineDeployFailedException(error?.message);\n          }\n        }\n\n        if (status === 'completed') {\n          return data;\n        }\n      } catch (e) {\n        throw wrapRdiPipelineError(e);\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL));\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/responses/index.ts",
    "content": "export * from './statistics.responses';\nexport * from './status.responses';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/responses/statistics.responses.ts",
    "content": "export interface GetStatisticsResponse {\n  connections: Record<\n    string,\n    {\n      type: string;\n      host: string;\n      port: number;\n      database: string;\n      user: string;\n      password: string;\n      status: string;\n    }\n  >;\n  data_streams: {\n    totals: {\n      total: number;\n      pending: number;\n      inserted: number;\n      updated: number;\n      deleted: number;\n      filtered: number;\n      rejected: number;\n      deduplicated: number;\n    };\n    streams: Record<\n      string,\n      {\n        total: number;\n        pending: number;\n        inserted: number;\n        updated: number;\n        deleted: number;\n        filtered: number;\n        rejected: number;\n        deduplicated: number;\n        last_arrival: string;\n      }\n    >;\n  };\n  processing_performance: {\n    total_batches: number;\n    batch_size_avg: number;\n    read_time_avg: number;\n    process_time_avg: number;\n    ack_time_avg: number;\n    total_time_avg: number;\n    rec_per_sec_avg: number;\n  };\n  rdi_pipeline_status: {\n    rdi_version: string;\n    address: string;\n    run_status: string;\n    sync_mode: string;\n  };\n  clients: Record<\n    string,\n    {\n      id: string;\n      addr: string;\n      age_sec: string;\n      idle_sec: string;\n      user: string;\n    }\n  >;\n  offsets: Record<string, string>;\n  snapshot_status: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/responses/status.responses.ts",
    "content": "export interface GetStatusResponse {\n  components: {\n    'collector-source': {\n      status: string;\n      connected: boolean;\n      version: string;\n    };\n    processor: { status: string; version: string };\n  };\n  pipelines: {\n    default: {\n      status: string;\n      state: string;\n      tasks: {\n        name: string;\n        status: string;\n        created_at: string;\n      }[];\n    };\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/transformers/index.ts",
    "content": "export * from './statistics.transformers';\nexport * from './status.transformers';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/transformers/statistics.transformers.spec.ts",
    "content": "import {\n  RdiStatisticsBlocksSection,\n  RdiStatisticsInfoSection,\n  RdiStatisticsTableSection,\n  RdiStatisticsViewType,\n} from 'src/modules/rdi/models';\nimport { GetStatisticsResponse } from 'src/modules/rdi/client/api/v1/responses';\nimport {\n  transformProcessingPerformance,\n  transformClientStatistics,\n  transformDataStreamsStatistics,\n  transformGeneralInfo,\n  transformConnectionsStatistics,\n  transformStatisticsResponse,\n} from './statistics.transformers';\n\ndescribe('statistics.transformers', () => {\n  describe('transformProcessingPerformance', () => {\n    it('should return RdiStatisticsBlocksSection instance with all blocks when all data is present', () => {\n      const data: GetStatisticsResponse['processing_performance'] = {\n        total_batches: 100,\n        batch_size_avg: 1.5,\n        process_time_avg: 50,\n        ack_time_avg: 0.5,\n        read_time_avg: 10,\n        rec_per_sec_avg: 1000,\n        total_time_avg: 60,\n      };\n\n      const result = transformProcessingPerformance(data);\n\n      expect(result).toBeInstanceOf(RdiStatisticsBlocksSection);\n      expect(result.name).toBe('Processing performance information');\n      expect(result.view).toBe(RdiStatisticsViewType.Blocks);\n      expect(result.data).toHaveLength(7);\n      expect(result.data).toEqual([\n        { label: 'Total batches', value: 100, units: 'Total' },\n        { label: 'Batch size average', value: 1.5, units: 'MB' },\n        { label: 'Process time average', value: 50, units: 'ms' },\n        { label: 'ACK time average', value: 0.5, units: 'sec' },\n        { label: 'Read time average', value: 10, units: 'ms' },\n        { label: 'Records per second average', value: 1000, units: 'sec' },\n        { label: 'Total time average', value: 60, units: 'ms' },\n      ]);\n    });\n\n    it('should return empty data array when data is undefined', () => {\n      const result = transformProcessingPerformance(undefined);\n\n      expect(result).toBeInstanceOf(RdiStatisticsBlocksSection);\n      expect(result.data).toEqual([]);\n    });\n\n    it('should include only non-nil values', () => {\n      const data: Partial<GetStatisticsResponse['processing_performance']> = {\n        total_batches: 100,\n        batch_size_avg: undefined,\n        process_time_avg: 50,\n      };\n\n      const result = transformProcessingPerformance(\n        data as GetStatisticsResponse['processing_performance'],\n      );\n\n      expect(result.data).toHaveLength(2);\n      expect(result.data[0].label).toBe('Total batches');\n      expect(result.data[1].label).toBe('Process time average');\n    });\n\n    it('should include zero values', () => {\n      const data: GetStatisticsResponse['processing_performance'] = {\n        total_batches: 0,\n        batch_size_avg: 0,\n        process_time_avg: 0,\n        ack_time_avg: 0,\n        read_time_avg: 0,\n        rec_per_sec_avg: 0,\n        total_time_avg: 0,\n      };\n\n      const result = transformProcessingPerformance(data);\n\n      expect(result.data).toHaveLength(7);\n    });\n  });\n\n  describe('transformClientStatistics', () => {\n    it('should return RdiStatisticsTableSection instance with clients data', () => {\n      const data: GetStatisticsResponse['clients'] = {\n        client1: {\n          id: '1',\n          addr: '127.0.0.1:6379',\n          age_sec: '100',\n          idle_sec: '10',\n          user: 'default',\n        },\n        client2: {\n          id: '2',\n          addr: '127.0.0.1:6380',\n          age_sec: '200',\n          idle_sec: '20',\n          user: 'admin',\n        },\n      };\n\n      const result = transformClientStatistics(data);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result.name).toBe('Clients');\n      expect(result.view).toBe(RdiStatisticsViewType.Table);\n      expect(result.data).toHaveLength(2);\n      expect(result.columns).toContainEqual({ id: 'id', header: 'ID' });\n      expect(result.columns).toContainEqual({ id: 'addr', header: 'ADDR' });\n    });\n\n    it('should return empty data array when data is undefined', () => {\n      const result = transformClientStatistics(undefined);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result.data).toEqual([]);\n      expect(result.columns).toEqual([]);\n    });\n\n    it('should return empty data array when data is empty object', () => {\n      const result = transformClientStatistics({});\n\n      expect(result.data).toEqual([]);\n      expect(result.columns).toEqual([]);\n    });\n  });\n\n  describe('transformDataStreamsStatistics', () => {\n    it('should return RdiStatisticsTableSection instance with streams data', () => {\n      const data: GetStatisticsResponse['data_streams'] = {\n        totals: {\n          total: 1000,\n          pending: 10,\n          inserted: 500,\n          updated: 300,\n          deleted: 100,\n          filtered: 50,\n          rejected: 30,\n          deduplicated: 10,\n        },\n        streams: {\n          stream1: {\n            total: 600,\n            pending: 5,\n            inserted: 300,\n            updated: 200,\n            deleted: 50,\n            filtered: 25,\n            rejected: 15,\n            deduplicated: 5,\n            last_arrival: '2024-01-01T00:00:00Z',\n          },\n        },\n      };\n\n      const result = transformDataStreamsStatistics(data);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result.name).toBe('Data Streams');\n      expect(result.view).toBe(RdiStatisticsViewType.Table);\n      expect(result.data).toHaveLength(1);\n      expect(result.data[0].name).toBe('stream1');\n      expect(result.footer).toEqual({\n        name: 'Total',\n        total: 1000,\n        pending: 10,\n        inserted: 500,\n        updated: 300,\n        deleted: 100,\n        filtered: 50,\n        rejected: 30,\n        deduplicated: 10,\n      });\n    });\n\n    it('should return empty data array when data is undefined', () => {\n      const result = transformDataStreamsStatistics(undefined);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result.data).toEqual([]);\n      expect(result.columns).toEqual([]);\n    });\n\n    it('should return empty data array when streams is empty', () => {\n      const data: GetStatisticsResponse['data_streams'] = {\n        totals: {\n          total: 0,\n          pending: 0,\n          inserted: 0,\n          updated: 0,\n          deleted: 0,\n          filtered: 0,\n          rejected: 0,\n          deduplicated: 0,\n        },\n        streams: {},\n      };\n\n      const result = transformDataStreamsStatistics(data);\n\n      expect(result.data).toEqual([]);\n      expect(result.columns).toEqual([]);\n    });\n  });\n\n  describe('transformGeneralInfo', () => {\n    it('should return RdiStatisticsInfoSection instance with pipeline status', () => {\n      const data: GetStatisticsResponse['rdi_pipeline_status'] = {\n        rdi_version: '1.0.0',\n        address: 'redis://localhost:6379',\n        run_status: 'running',\n        sync_mode: 'streaming',\n      };\n\n      const result = transformGeneralInfo(data);\n\n      expect(result).toBeInstanceOf(RdiStatisticsInfoSection);\n      expect(result.name).toBe('General info');\n      expect(result.view).toBe(RdiStatisticsViewType.Info);\n      expect(result.data).toEqual([\n        { label: 'RDI version', value: '1.0.0' },\n        { label: 'RDI database address', value: 'redis://localhost:6379' },\n        { label: 'Run status', value: 'running' },\n        { label: 'Sync mode', value: 'streaming' },\n      ]);\n    });\n\n    it('should return empty strings when data is undefined', () => {\n      const result = transformGeneralInfo(undefined);\n\n      expect(result).toBeInstanceOf(RdiStatisticsInfoSection);\n      expect(result.data).toEqual([\n        { label: 'RDI version', value: '' },\n        { label: 'RDI database address', value: '' },\n        { label: 'Run status', value: '' },\n        { label: 'Sync mode', value: '' },\n      ]);\n    });\n\n    it('should handle partial data', () => {\n      const data: Partial<GetStatisticsResponse['rdi_pipeline_status']> = {\n        rdi_version: '1.0.0',\n        run_status: 'stopped',\n      };\n\n      const result = transformGeneralInfo(\n        data as GetStatisticsResponse['rdi_pipeline_status'],\n      );\n\n      expect(result.data).toEqual([\n        { label: 'RDI version', value: '1.0.0' },\n        { label: 'RDI database address', value: '' },\n        { label: 'Run status', value: 'stopped' },\n        { label: 'Sync mode', value: '' },\n      ]);\n    });\n  });\n\n  describe('transformConnectionsStatistics', () => {\n    it('should return RdiStatisticsTableSection instance with connections data', () => {\n      const data: GetStatisticsResponse['connections'] = {\n        target1: {\n          type: 'redis',\n          host: 'localhost',\n          port: 6379,\n          database: '0',\n          user: 'default',\n          password: 'secret',\n          status: 'connected',\n        },\n      };\n\n      const result = transformConnectionsStatistics(data);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result.name).toBe('Target Connections');\n      expect(result.view).toBe(RdiStatisticsViewType.Table);\n      expect(result.data).toHaveLength(1);\n      expect(result.data[0]).toEqual({\n        status: 'connected',\n        name: 'target1',\n        type: 'redis',\n        host_port: 'localhost:6379',\n        database: '0',\n        user: 'default',\n      });\n      expect(result.columns).toContainEqual({\n        id: 'status',\n        header: 'Status',\n        type: 'status',\n      });\n      expect(result.columns).toContainEqual({\n        id: 'host_port',\n        header: 'Host:port',\n      });\n      expect(result.columns).toContainEqual({\n        id: 'user',\n        header: 'Username',\n      });\n    });\n\n    it('should return empty data array when data is undefined', () => {\n      const result = transformConnectionsStatistics(undefined);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result.data).toEqual([]);\n      expect(result.columns).toEqual([]);\n    });\n\n    it('should return empty data array when data is empty object', () => {\n      const result = transformConnectionsStatistics({});\n\n      expect(result.data).toEqual([]);\n      expect(result.columns).toEqual([]);\n    });\n  });\n\n  describe('transformStatisticsResponse', () => {\n    const mockFullResponse: GetStatisticsResponse = {\n      rdi_pipeline_status: {\n        rdi_version: '1.0.0',\n        address: 'redis://localhost:6379',\n        run_status: 'running',\n        sync_mode: 'streaming',\n      },\n      processing_performance: {\n        total_batches: 100,\n        batch_size_avg: 1.5,\n        process_time_avg: 50,\n        ack_time_avg: 0.5,\n        read_time_avg: 10,\n        rec_per_sec_avg: 1000,\n        total_time_avg: 60,\n      },\n      connections: {\n        target1: {\n          type: 'redis',\n          host: 'localhost',\n          port: 6379,\n          database: '0',\n          user: 'default',\n          password: 'secret',\n          status: 'connected',\n        },\n      },\n      data_streams: {\n        totals: {\n          total: 1000,\n          pending: 10,\n          inserted: 500,\n          updated: 300,\n          deleted: 100,\n          filtered: 50,\n          rejected: 30,\n          deduplicated: 10,\n        },\n        streams: {\n          stream1: {\n            total: 600,\n            pending: 5,\n            inserted: 300,\n            updated: 200,\n            deleted: 50,\n            filtered: 25,\n            rejected: 15,\n            deduplicated: 5,\n            last_arrival: '2024-01-01T00:00:00Z',\n          },\n        },\n      },\n      clients: {\n        client1: {\n          id: '1',\n          addr: '127.0.0.1:6379',\n          age_sec: '100',\n          idle_sec: '10',\n          user: 'default',\n        },\n      },\n      offsets: {},\n      snapshot_status: 'completed',\n    };\n\n    it('should return array of 5 sections in correct order', () => {\n      const result = transformStatisticsResponse(mockFullResponse);\n\n      expect(result).toHaveLength(5);\n      expect(result[0].name).toBe('General info');\n      expect(result[0].view).toBe(RdiStatisticsViewType.Info);\n      expect(result[1].name).toBe('Processing performance information');\n      expect(result[1].view).toBe(RdiStatisticsViewType.Blocks);\n      expect(result[2].name).toBe('Target Connections');\n      expect(result[2].view).toBe(RdiStatisticsViewType.Table);\n      expect(result[3].name).toBe('Data Streams');\n      expect(result[3].view).toBe(RdiStatisticsViewType.Table);\n      expect(result[4].name).toBe('Clients');\n      expect(result[4].view).toBe(RdiStatisticsViewType.Table);\n    });\n\n    it('should return correct section types', () => {\n      const result = transformStatisticsResponse(mockFullResponse);\n\n      expect(result[0]).toBeInstanceOf(RdiStatisticsInfoSection);\n      expect(result[1]).toBeInstanceOf(RdiStatisticsBlocksSection);\n      expect(result[2]).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result[3]).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result[4]).toBeInstanceOf(RdiStatisticsTableSection);\n    });\n\n    it('should filter out table sections with empty data', () => {\n      const responseWithEmptyData: GetStatisticsResponse = {\n        rdi_pipeline_status: {\n          rdi_version: '1.0.0',\n          address: '127.0.0.1:6379',\n          run_status: 'started',\n          sync_mode: 'cdc',\n        },\n        processing_performance: {\n          total_batches: 100,\n          batch_size_avg: 1.5,\n          read_time_avg: 10,\n          process_time_avg: 50,\n          ack_time_avg: 0.5,\n          total_time_avg: 60,\n          rec_per_sec_avg: 1000,\n        },\n        data_streams: {\n          streams: {},\n          totals: {\n            total: 0,\n            pending: 0,\n            inserted: 0,\n            updated: 0,\n            deleted: 0,\n            filtered: 0,\n            rejected: 0,\n            deduplicated: 0,\n          },\n        },\n        connections: {},\n        clients: {},\n        offsets: {},\n        snapshot_status: '',\n      };\n\n      const result = transformStatisticsResponse(responseWithEmptyData);\n\n      // Should only include General info and Processing performance (non-empty sections)\n      // Target Connections, Data Streams, and Clients should be filtered out\n      expect(result).toHaveLength(2);\n      expect(result[0].name).toBe('General info');\n      expect(result[1].name).toBe('Processing performance information');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/transformers/statistics.transformers.ts",
    "content": "import { isNil } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  RdiStatisticsBlocksSection,\n  RdiStatisticsInfoSection,\n  RdiStatisticsSection,\n  RdiStatisticsTableSection,\n  RdiStatisticsViewType,\n} from 'src/modules/rdi/models';\nimport { GetStatisticsResponse } from 'src/modules/rdi/client/api/v1/responses';\nimport {\n  generateColumns,\n  hasData,\n} from 'src/modules/rdi/utils/transformer.util';\n\nexport const transformProcessingPerformance = (\n  data: GetStatisticsResponse['processing_performance'],\n): RdiStatisticsBlocksSection => {\n  const blocks = [];\n\n  if (!isNil(data?.total_batches)) {\n    blocks.push({\n      label: 'Total batches',\n      value: data.total_batches,\n      units: 'Total',\n    });\n  }\n\n  if (!isNil(data?.batch_size_avg)) {\n    blocks.push({\n      label: 'Batch size average',\n      value: data.batch_size_avg,\n      units: 'MB',\n    });\n  }\n\n  if (!isNil(data?.process_time_avg)) {\n    blocks.push({\n      label: 'Process time average',\n      value: data.process_time_avg,\n      units: 'ms',\n    });\n  }\n\n  if (!isNil(data?.ack_time_avg)) {\n    blocks.push({\n      label: 'ACK time average',\n      value: data.ack_time_avg,\n      units: 'sec',\n    });\n  }\n\n  if (!isNil(data?.read_time_avg)) {\n    blocks.push({\n      label: 'Read time average',\n      value: data.read_time_avg,\n      units: 'ms',\n    });\n  }\n\n  if (!isNil(data?.rec_per_sec_avg)) {\n    blocks.push({\n      label: 'Records per second average',\n      value: data.rec_per_sec_avg,\n      units: 'sec',\n    });\n  }\n\n  if (!isNil(data?.total_time_avg)) {\n    blocks.push({\n      label: 'Total time average',\n      value: data.total_time_avg,\n      units: 'ms',\n    });\n  }\n\n  return plainToInstance(RdiStatisticsBlocksSection, {\n    name: 'Processing performance information',\n    view: RdiStatisticsViewType.Blocks,\n    data: blocks,\n  });\n};\n\nexport const transformClientStatistics = (\n  data: GetStatisticsResponse['clients'],\n): RdiStatisticsTableSection => {\n  // Convert the Record<string, {...}> to an array of objects\n  const clientsArray = Object.entries(data || {}).map(\n    ([_key, client]) => client,\n  );\n\n  // Custom column headers (override auto-generated ones)\n  const customColumns = {\n    id: 'ID',\n    addr: 'ADDR',\n  };\n\n  return plainToInstance(RdiStatisticsTableSection, {\n    name: 'Clients',\n    view: RdiStatisticsViewType.Table,\n    columns: generateColumns(clientsArray, customColumns),\n    data: clientsArray,\n  });\n};\n\nexport const transformDataStreamsStatistics = (\n  data: GetStatisticsResponse['data_streams'],\n): RdiStatisticsTableSection => {\n  // Convert the Record<string, {...}> to an array of objects\n  const streamsArray = Object.entries(data?.streams || {}).map(\n    ([key, stream]) => ({\n      name: key,\n      ...stream,\n    }),\n  );\n\n  // Custom column configuration for date formatting\n  const customColumns = {\n    last_arrival: { header: 'Last arrival', type: 'date' },\n  };\n\n  return plainToInstance(RdiStatisticsTableSection, {\n    name: 'Data Streams',\n    view: RdiStatisticsViewType.Table,\n    columns: generateColumns(streamsArray, customColumns),\n    data: streamsArray,\n    footer: {\n      name: 'Total',\n      ...data?.totals,\n    },\n  });\n};\n\nexport const transformGeneralInfo = (\n  rdiPipelineStatus: GetStatisticsResponse['rdi_pipeline_status'],\n): RdiStatisticsInfoSection => {\n  const items = [\n    {\n      label: 'RDI version',\n      value: rdiPipelineStatus?.rdi_version || '',\n    },\n    {\n      label: 'RDI database address',\n      value: rdiPipelineStatus?.address || '',\n    },\n    {\n      label: 'Run status',\n      value: rdiPipelineStatus?.run_status || '',\n    },\n    {\n      label: 'Sync mode',\n      value: rdiPipelineStatus?.sync_mode || '',\n    },\n  ];\n\n  return plainToInstance(RdiStatisticsInfoSection, {\n    name: 'General info',\n    view: RdiStatisticsViewType.Info,\n    data: items,\n  });\n};\n\nexport const transformConnectionsStatistics = (\n  data: GetStatisticsResponse['connections'],\n): RdiStatisticsTableSection => {\n  // Convert the Record<string, {...}> to an array of objects\n  // Only include specific fields in the desired order\n  const connectionsArray = Object.entries(data || {}).map(\n    ([key, connection]) => ({\n      status: connection.status,\n      name: key,\n      type: connection.type,\n      host_port: `${connection.host}:${connection.port}`,\n      database: connection.database,\n      user: connection.user,\n    }),\n  );\n\n  // Custom column headers and types\n  const customColumns = {\n    // todo: add type enum in the future\n    status: { header: 'Status', type: 'status' },\n    host_port: { header: 'Host:port' },\n    user: { header: 'Username' },\n  };\n\n  return plainToInstance(RdiStatisticsTableSection, {\n    name: 'Target Connections',\n    view: RdiStatisticsViewType.Table,\n    columns: generateColumns(connectionsArray, customColumns),\n    data: connectionsArray,\n  });\n};\n\nexport const transformStatisticsResponse = (\n  data: GetStatisticsResponse,\n): RdiStatisticsSection[] => {\n  const sections = [\n    transformGeneralInfo(data.rdi_pipeline_status),\n    transformProcessingPerformance(data.processing_performance),\n    transformConnectionsStatistics(data.connections),\n    transformDataStreamsStatistics(data.data_streams),\n    transformClientStatistics(data.clients),\n  ];\n\n  // Filter out sections with no data\n  return sections.filter(hasData);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/transformers/status.transformers.spec.ts",
    "content": "import { RdiPipelineStatus } from 'src/modules/rdi/models';\nimport { GetStatusResponse } from 'src/modules/rdi/client/api/v1/responses';\nimport { transformStatus } from './status.transformers';\n\ndescribe('status.transformers', () => {\n  describe('transformStatus', () => {\n    it('should return RdiPipelineStatus instance with status and state', () => {\n      const data: GetStatusResponse = {\n        components: {\n          'collector-source': {\n            status: 'ready',\n            connected: true,\n            version: '3.3.1.Final-rdi.1',\n          },\n          processor: {\n            status: 'ready',\n            version: '0.0.202512301417',\n          },\n        },\n        pipelines: {\n          default: {\n            status: 'ready',\n            state: 'cdc',\n            tasks: [\n              {\n                name: 'deploy',\n                status: 'completed',\n                created_at: '2026-01-06T14:35:28',\n              },\n            ],\n          },\n        },\n      };\n\n      const result = transformStatus(data);\n\n      expect(result).toBeInstanceOf(RdiPipelineStatus);\n      expect(result.status).toBe('ready');\n      expect(result.state).toBe('cdc');\n    });\n\n    it('should handle different pipeline states', () => {\n      const data: GetStatusResponse = {\n        components: {\n          'collector-source': {\n            status: 'stopped',\n            connected: false,\n            version: '3.3.1.Final-rdi.1',\n          },\n          processor: {\n            status: 'stopped',\n            version: '0.0.202512301417',\n          },\n        },\n        pipelines: {\n          default: {\n            status: 'stopped',\n            state: 'not-running',\n            tasks: [],\n          },\n        },\n      };\n\n      const result = transformStatus(data);\n\n      expect(result.status).toBe('stopped');\n      expect(result.state).toBe('not-running');\n    });\n\n    it('should handle initial-sync state', () => {\n      const data: GetStatusResponse = {\n        components: {\n          'collector-source': {\n            status: 'ready',\n            connected: true,\n            version: '3.3.1.Final-rdi.1',\n          },\n          processor: {\n            status: 'ready',\n            version: '0.0.202512301417',\n          },\n        },\n        pipelines: {\n          default: {\n            status: 'ready',\n            state: 'initial-sync',\n            tasks: [],\n          },\n        },\n      };\n\n      const result = transformStatus(data);\n\n      expect(result.status).toBe('ready');\n      expect(result.state).toBe('initial-sync');\n    });\n\n    it('should handle undefined pipelines gracefully', () => {\n      const data = {\n        components: {\n          'collector-source': {\n            status: 'ready',\n            connected: true,\n            version: '3.3.1.Final-rdi.1',\n          },\n          processor: {\n            status: 'ready',\n            version: '0.0.202512301417',\n          },\n        },\n        pipelines: undefined,\n      } as unknown as GetStatusResponse;\n\n      const result = transformStatus(data);\n\n      expect(result).toBeInstanceOf(RdiPipelineStatus);\n      expect(result.status).toBeUndefined();\n      expect(result.state).toBeUndefined();\n    });\n\n    it('should handle undefined default pipeline gracefully', () => {\n      const data = {\n        components: {\n          'collector-source': {\n            status: 'ready',\n            connected: true,\n            version: '3.3.1.Final-rdi.1',\n          },\n          processor: {\n            status: 'ready',\n            version: '0.0.202512301417',\n          },\n        },\n        pipelines: {\n          default: undefined,\n        },\n      } as unknown as GetStatusResponse;\n\n      const result = transformStatus(data);\n\n      expect(result).toBeInstanceOf(RdiPipelineStatus);\n      expect(result.status).toBeUndefined();\n      expect(result.state).toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v1/transformers/status.transformers.ts",
    "content": "import { plainToInstance } from 'class-transformer';\nimport { RdiPipelineStatus } from 'src/modules/rdi/models';\nimport { GetStatusResponse } from 'src/modules/rdi/client/api/v1/responses';\n\nexport const transformStatus = (data: GetStatusResponse): RdiPipelineStatus => {\n  return plainToInstance(RdiPipelineStatus, {\n    status: data.pipelines?.default?.status,\n    state: data.pipelines?.default?.state,\n  });\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.spec.ts",
    "content": "import axios from 'axios';\nimport {\n  mockRdi,\n  mockRdiClientMetadata,\n  mockRdiUnauthorizedError,\n} from 'src/__mocks__';\nimport { ApiV2RdiClient } from 'src/modules/rdi/client/api/v2/api.v2.rdi.client';\nimport { RdiUrlV2 } from 'src/modules/rdi/constants';\nimport {\n  RdiInfo,\n  RdiPipelineStatus,\n  RdiStatisticsStatus,\n} from 'src/modules/rdi/models';\nimport { RdiPipelineInternalServerErrorException } from 'src/modules/rdi/exceptions';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('ApiV2RdiClient', () => {\n  let client: ApiV2RdiClient;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    client = new ApiV2RdiClient(mockRdiClientMetadata, mockRdi);\n  });\n\n  describe('getInfo', () => {\n    it('should return RDI info when API call is successful', async () => {\n      const mockInfoResponse = { version: '2.0.1' };\n      const expectedRdiInfo = Object.assign(new RdiInfo(), {\n        version: '2.0.1',\n      });\n      mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse });\n\n      const result = await client.getInfo();\n\n      expect(result).toEqual(expectedRdiInfo);\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo);\n    });\n\n    it('should throw wrapped error when API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getInfo()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo);\n    });\n\n    it('should transform response data to RdiInfo instance', async () => {\n      const mockInfoResponse = { version: '2.1.0' };\n      mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse });\n\n      const result = await client.getInfo();\n\n      expect(result).toBeInstanceOf(RdiInfo);\n      expect(result.version).toBe('2.1.0');\n    });\n  });\n\n  describe('selectPipeline', () => {\n    it('should select first pipeline when pipelines are available', async () => {\n      const mockPipelinesResponse = [\n        {\n          name: 'pipeline-1',\n          active: true,\n          config: {},\n          status: 'running',\n          errors: [],\n          components: [],\n          current: true,\n        },\n        {\n          name: 'pipeline-2',\n          active: false,\n          config: {},\n          status: 'stopped',\n          errors: [],\n          components: [],\n          current: false,\n        },\n      ];\n      mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse });\n\n      await client.selectPipeline();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetPipelines);\n      expect(client['selectedPipeline']).toBe('pipeline-1');\n    });\n\n    it('should throw RdiPipelineInternalServerErrorException when no pipelines available', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: [] });\n\n      await expect(client.selectPipeline()).rejects.toThrow(\n        RdiPipelineInternalServerErrorException,\n      );\n    });\n\n    it('should throw error with message \"Unable to select pipeline\" when no pipelines available', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: [] });\n\n      await expect(client.selectPipeline()).rejects.toThrow(\n        'Unable to select pipeline',\n      );\n    });\n\n    it('should throw RdiPipelineInternalServerErrorException when data is null', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: null });\n\n      await expect(client.selectPipeline()).rejects.toThrow(\n        RdiPipelineInternalServerErrorException,\n      );\n    });\n\n    it('should throw RdiPipelineInternalServerErrorException when data is undefined', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: undefined });\n\n      await expect(client.selectPipeline()).rejects.toThrow(\n        RdiPipelineInternalServerErrorException,\n      );\n    });\n\n    it('should throw wrapped error when API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.selectPipeline()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetPipelines);\n    });\n\n    it('should select first pipeline even when multiple pipelines exist', async () => {\n      const mockPipelinesResponse = [\n        {\n          name: 'first',\n          active: false,\n          config: {},\n          status: 'stopped',\n          errors: [],\n          components: [],\n          current: false,\n        },\n        {\n          name: 'second',\n          active: true,\n          config: {},\n          status: 'running',\n          errors: [],\n          components: [],\n          current: true,\n        },\n        {\n          name: 'third',\n          active: false,\n          config: {},\n          status: 'stopped',\n          errors: [],\n          components: [],\n          current: false,\n        },\n      ];\n      mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse });\n\n      await client.selectPipeline();\n\n      expect(client['selectedPipeline']).toBe('first');\n    });\n  });\n\n  describe('getPipelineStatus', () => {\n    it('should return RdiPipelineStatus when API call is successful', async () => {\n      const mockV2Response = {\n        status: 'started',\n        errors: [],\n        components: [\n          {\n            name: 'processor',\n            type: 'stream-processor',\n            version: '0.0.202512301417',\n            status: 'started',\n            errors: [],\n          },\n        ],\n        current: true,\n      };\n\n      mockedAxios.get.mockResolvedValueOnce({ data: mockV2Response });\n\n      const result = await client.getPipelineStatus();\n\n      expect(result).toBeInstanceOf(RdiPipelineStatus);\n      expect(result.status).toBe('started');\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        RdiUrlV2.GetPipelineStatus('default'),\n      );\n    });\n\n    it('should throw wrapped error when API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getPipelineStatus()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n\n    it('should use selectedPipeline in the URL', async () => {\n      client['selectedPipeline'] = 'my-pipeline';\n\n      const mockV2Response = {\n        status: 'started',\n        errors: [],\n        components: [],\n        current: true,\n      };\n\n      mockedAxios.get.mockResolvedValueOnce({ data: mockV2Response });\n\n      await client.getPipelineStatus();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        RdiUrlV2.GetPipelineStatus('my-pipeline'),\n      );\n    });\n  });\n\n  describe('getStatistics', () => {\n    const mockMetricsResponse = [\n      {\n        name: 'processor_metrics',\n        component: 'processor',\n        metrics: {\n          processing_performance: {\n            total_batches: 100,\n            batch_size_avg: 1.5,\n            read_time_avg: 10,\n            transform_time_avg: 5,\n            write_time_avg: 3,\n            process_time_avg: 50,\n            ack_time_avg: 0.5,\n            total_time_avg: 60,\n            rec_per_sec_avg: 1000,\n          },\n          rdi_pipeline_status: {\n            rdi_version: '1.0.0',\n            address: 'redis://localhost:6379',\n            run_status: 'running',\n            sync_mode: 'streaming',\n          },\n          connections: {\n            target1: {\n              type: 'redis',\n              host: 'localhost',\n              port: 6379,\n              database: '0',\n              user: 'default',\n              password: 'secret',\n              status: 'connected',\n            },\n          },\n          data_streams: {\n            totals: {\n              total: 1000,\n              pending: 10,\n              inserted: 500,\n              updated: 300,\n              deleted: 100,\n              filtered: 50,\n              rejected: 30,\n              deduplicated: 10,\n            },\n            streams: {},\n          },\n          clients: {},\n        },\n      },\n    ];\n\n    const mockStatusResponse = {\n      status: 'started',\n      errors: [],\n      components: [\n        {\n          name: 'processor',\n          type: 'stream-processor',\n          version: '1.0.0',\n          status: 'started',\n          errors: [],\n          metric_collections: [],\n        },\n      ],\n      current: true,\n    };\n\n    it('should call both metrics and status endpoints', async () => {\n      mockedAxios.get.mockImplementation((url: string) => {\n        if (url.includes('metric-collections')) {\n          return Promise.resolve({ data: mockMetricsResponse });\n        }\n        if (url.includes('status')) {\n          return Promise.resolve({ data: mockStatusResponse });\n        }\n        return Promise.reject(new Error(`Unexpected URL: ${url}`));\n      });\n\n      await client.getStatistics();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        RdiUrlV2.GetMetricsCollections('default'),\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        RdiUrlV2.GetPipelineStatus('default'),\n      );\n    });\n\n    it('should return success status with sections including Component Status', async () => {\n      mockedAxios.get.mockImplementation((url: string) => {\n        if (url.includes('metric-collections')) {\n          return Promise.resolve({ data: mockMetricsResponse });\n        }\n        if (url.includes('status')) {\n          return Promise.resolve({ data: mockStatusResponse });\n        }\n        return Promise.reject(new Error(`Unexpected URL: ${url}`));\n      });\n\n      const result = await client.getStatistics();\n\n      expect(result.status).toBe(RdiStatisticsStatus.Success);\n      // 4 sections: General info, Processing performance, Target Connections, Component Status\n      // (Data Streams and Clients are filtered out because they have empty data)\n      expect(result.data?.sections).toHaveLength(4);\n      expect(result.data?.sections[3].name).toBe('Component Status');\n    });\n\n    it('should handle status endpoint failure gracefully', async () => {\n      mockedAxios.get.mockImplementation((url: string) => {\n        if (url.includes('metric-collections')) {\n          return Promise.resolve({ data: mockMetricsResponse });\n        }\n        if (url.includes('status')) {\n          return Promise.reject(new Error('Status endpoint failed'));\n        }\n        return Promise.reject(new Error(`Unexpected URL: ${url}`));\n      });\n\n      const result = await client.getStatistics();\n\n      expect(result.status).toBe(RdiStatisticsStatus.Success);\n      // 3 sections: General info, Processing performance, Target Connections\n      // (Data Streams and Clients are filtered out because they have empty data)\n      expect(result.data?.sections).toHaveLength(3);\n      expect(result.data?.sections.map((s) => s.name)).not.toContain(\n        'Component Status',\n      );\n    });\n\n    it('should return fail status when metrics endpoint fails', async () => {\n      mockedAxios.get.mockImplementation((url: string) => {\n        if (url.includes('metric-collections')) {\n          return Promise.reject(new Error('Metrics failed'));\n        }\n        if (url.includes('status')) {\n          return Promise.resolve({ data: mockStatusResponse });\n        }\n        return Promise.reject(new Error(`Unexpected URL: ${url}`));\n      });\n\n      const result = await client.getStatistics();\n\n      expect(result.status).toBe(RdiStatisticsStatus.Fail);\n      expect(result.error).toBe('Metrics failed');\n    });\n\n    it('should use selectedPipeline in the URLs', async () => {\n      client['selectedPipeline'] = 'my-pipeline';\n\n      mockedAxios.get.mockImplementation((url: string) => {\n        if (url.includes('metric-collections')) {\n          return Promise.resolve({ data: mockMetricsResponse });\n        }\n        if (url.includes('status')) {\n          return Promise.resolve({ data: mockStatusResponse });\n        }\n        return Promise.reject(new Error(`Unexpected URL: ${url}`));\n      });\n\n      await client.getStatistics();\n\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        RdiUrlV2.GetMetricsCollections('my-pipeline'),\n      );\n      expect(mockedAxios.get).toHaveBeenCalledWith(\n        RdiUrlV2.GetPipelineStatus('my-pipeline'),\n      );\n    });\n  });\n\n  describe('getVersion', () => {\n    it('should return version from info endpoint', async () => {\n      const mockInfoResponse = { version: '2.1.0' };\n      mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse });\n\n      const result = await client.getVersion();\n\n      expect(result).toBe('2.1.0');\n      expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo);\n    });\n\n    it('should return default version when version is missing', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: {} });\n\n      const result = await client.getVersion();\n\n      expect(result).toBe('-');\n    });\n\n    it('should return default version when data is undefined', async () => {\n      mockedAxios.get.mockResolvedValueOnce({ data: undefined });\n\n      const result = await client.getVersion();\n\n      expect(result).toBe('-');\n    });\n\n    it('should throw wrapped error when API call fails', async () => {\n      mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n      await expect(client.getVersion()).rejects.toThrow(\n        mockRdiUnauthorizedError.message,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.ts",
    "content": "import { plainToInstance } from 'class-transformer';\nimport { Logger } from '@nestjs/common';\nimport { DEFAULT_RDI_VERSION, RdiUrlV2 } from 'src/modules/rdi/constants';\nimport {\n  parseErrorMessage,\n  RdiPipelineInternalServerErrorException,\n  wrapRdiPipelineError,\n} from 'src/modules/rdi/exceptions';\nimport {\n  RdiInfo,\n  RdiPipelineStatus,\n  RdiStatisticsResult,\n  RdiStatisticsStatus,\n} from 'src/modules/rdi/models';\n\nimport { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client';\nimport {\n  GetInfoResponse,\n  GetMetricsCollectionResponse,\n  GetPipelinesResponse,\n  GetStatusResponse,\n} from 'src/modules/rdi/client/api/v2/responses';\nimport { transformMetricsCollectionResponse } from 'src/modules/rdi/client/api/v2/transformers';\n\nexport class ApiV2RdiClient extends ApiRdiClient {\n  protected readonly logger = new Logger('ApiV2RdiClient');\n\n  protected selectedPipeline = 'default';\n\n  /**\n   * Retrieves comprehensive information about the RDI (Redis Data Integration) instance.\n   *\n   * This method is available starting from RDI API v2 and provides detailed metadata\n   * about the RDI instance including version, status, and configuration details.\n   *\n   * @returns {Promise<RdiInfo>} A promise that resolves to an RdiInfo object containing\n   *                             instance metadata such as version, status, and capabilities\n   *\n   * @example\n   * const info = await client.getInfo();\n   * console.log(info.version); // e.g., \"1.2.0\"\n   */\n  async getInfo(): Promise<RdiInfo> {\n    try {\n      const { data } = await this.client.get<GetInfoResponse>(RdiUrlV2.GetInfo);\n\n      return plainToInstance(RdiInfo, data);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  /**\n   * Selects the active pipeline for subsequent RDI operations.\n   *\n   * This method fetches all available pipelines from the RDI instance and automatically\n   * selects the first pipeline in the list. The selected pipeline is stored in the\n   * `selectedPipeline` property and will be used for all pipeline-specific operations.\n   *\n   * In RDI v2, multiple pipelines can exist, but this implementation currently defaults\n   * to selecting the first available pipeline. If no pipelines exist, an error is thrown.\n   *\n   * @returns {Promise<void>} A promise that resolves when the pipeline is successfully selected\n   *\n   * @example\n   * await client.selectPipeline();\n   * // client.selectedPipeline is now set to the first available pipeline name\n   */\n  async selectPipeline(): Promise<void> {\n    try {\n      const { data } = await this.client.get<GetPipelinesResponse>(\n        RdiUrlV2.GetPipelines,\n      );\n\n      // todo: handle cases when no pipelines differently\n      if (!data?.length) {\n        throw new RdiPipelineInternalServerErrorException(\n          'Unable to select pipeline',\n        );\n      }\n\n      this.selectedPipeline = data[0].name;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  /**\n   * Retrieves statistics for the selected pipeline.\n   *\n   * This method fetches statistics for the currently selected pipeline. The statistics\n   * include detailed information about the pipeline's performance, data processing,\n   * and other relevant metrics. Also fetches component status data to include in statistics.\n   *\n   * @returns {Promise<RdiStatisticsResult>} A promise that resolves to an RdiStatisticsResult\n   *                                        object containing the pipeline statistics\n   *\n   * @example\n   * const stats = await client.getStatistics();\n   */\n  async getStatistics(): Promise<RdiStatisticsResult> {\n    try {\n      const [metricsResponse, statusResponse] = await Promise.all([\n        this.client.get<GetMetricsCollectionResponse>(\n          RdiUrlV2.GetMetricsCollections(this.selectedPipeline),\n        ),\n        this.client\n          .get<GetStatusResponse>(\n            RdiUrlV2.GetPipelineStatus(this.selectedPipeline),\n          )\n          .catch(() => ({ data: null })), // Graceful fallback if status fails\n      ]);\n\n      return plainToInstance(RdiStatisticsResult, {\n        status: RdiStatisticsStatus.Success,\n        data: {\n          sections: transformMetricsCollectionResponse(\n            metricsResponse.data,\n            statusResponse.data,\n          ),\n        },\n      });\n    } catch (e) {\n      const message: string = parseErrorMessage(e);\n      return { status: RdiStatisticsStatus.Fail, error: message };\n    }\n  }\n\n  async getPipelineStatus(): Promise<RdiPipelineStatus> {\n    try {\n      const { data } = await this.client.get<GetStatusResponse>(\n        RdiUrlV2.GetPipelineStatus(this.selectedPipeline),\n      );\n\n      return plainToInstance(RdiPipelineStatus, data);\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n\n  async getVersion(): Promise<string> {\n    try {\n      const { data } = await this.client.get<GetInfoResponse>(RdiUrlV2.GetInfo);\n      return data?.version || DEFAULT_RDI_VERSION;\n    } catch (e) {\n      throw wrapRdiPipelineError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/responses/index.ts",
    "content": "export * from './info.responses';\nexport * from './pipeline.responses';\nexport * from './metrics-collections.response';\nexport * from './status.responses';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/responses/info.responses.ts",
    "content": "export interface GetInfoResponse {\n  version: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/responses/metrics-collections.response.ts",
    "content": "export interface ComponentMetricsResponse {\n  name: string;\n  component: string;\n  metrics: object;\n}\n\nexport interface ProcessorMetricsResponse extends ComponentMetricsResponse {\n  component: 'processor';\n  metrics: {\n    processing_performance: {\n      total_batches: number;\n      batch_size_avg: number;\n      read_time_avg: number;\n      transform_time_avg: number;\n      write_time_avg: number;\n      process_time_avg: number;\n      ack_time_avg: number;\n      total_time_avg: number;\n      rec_per_sec_avg: number;\n    };\n    data_streams: {\n      streams: Record<\n        string,\n        {\n          total: number;\n          pending: number;\n          inserted: number;\n          updated: number;\n          deleted: number;\n          filtered: number;\n          rejected: number;\n          deduplicated: number;\n          last_arrival: string;\n        }\n      >;\n      totals: {\n        total: number;\n        pending: number;\n        inserted: number;\n        updated: number;\n        deleted: number;\n        filtered: number;\n        rejected: number;\n        deduplicated: number;\n      };\n    };\n    rdi_pipeline_status: {\n      rdi_version: string;\n      address: string;\n      run_status: string;\n      sync_mode: string;\n    };\n    connections: Record<\n      string,\n      {\n        type: string;\n        host: string;\n        port: number;\n        database: string;\n        user: string;\n        password: string;\n        status: string;\n      }\n    >;\n    clients: Record<\n      string,\n      {\n        id: string;\n        addr: string;\n        user: string;\n        age_sec: string;\n        idle_sec: string;\n      }\n    >;\n  };\n}\n\nexport interface CollectorMetricsResponse extends ComponentMetricsResponse {\n  component: 'collector-source';\n  metrics: object;\n}\n\nexport type GetMetricsCollectionResponse = (\n  | ProcessorMetricsResponse\n  | CollectorMetricsResponse\n)[];\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/responses/pipeline.responses.ts",
    "content": "export interface PipelineResponses {\n  name: string;\n  active: boolean;\n  config: any; // todo: define\n  status: string; // todo: define enum\n  errors: any[]; // todo: define\n  components: any[]; // todo: define\n  current: true;\n}\n\nexport type GetPipelinesResponse = PipelineResponses[];\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/responses/status.responses.ts",
    "content": "export interface GetStatusResponse {\n  status: string;\n  errors: string[];\n  components: {\n    name: string;\n    type: string;\n    version: string;\n    status: string;\n    errors: string[];\n    metric_collections: any[];\n  }[];\n  current: true;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/transformers/index.ts",
    "content": "export * from './metrics-collections.transformer';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/transformers/metrics-collections.transformer.spec.ts",
    "content": "import {\n  RdiStatisticsBlocksSection,\n  RdiStatisticsInfoSection,\n  RdiStatisticsTableSection,\n  RdiStatisticsViewType,\n} from 'src/modules/rdi/models';\nimport {\n  CollectorMetricsResponse,\n  GetMetricsCollectionResponse,\n  GetStatusResponse,\n  ProcessorMetricsResponse,\n} from 'src/modules/rdi/client/api/v2/responses';\nimport {\n  transformProcessingPerformance,\n  transformComponentStatus,\n  transformMetricsCollectionResponse,\n} from './metrics-collections.transformer';\n\ndescribe('metrics-collections.transformer', () => {\n  describe('transformProcessingPerformance', () => {\n    it('should return RdiStatisticsBlocksSection instance with all blocks including v2 fields', () => {\n      const data: ProcessorMetricsResponse['metrics']['processing_performance'] =\n        {\n          total_batches: 100,\n          batch_size_avg: 1.5,\n          read_time_avg: 10,\n          transform_time_avg: 5,\n          write_time_avg: 3,\n          process_time_avg: 50,\n          ack_time_avg: 0.5,\n          total_time_avg: 60,\n          rec_per_sec_avg: 1000,\n        };\n\n      const result = transformProcessingPerformance(data);\n\n      expect(result).toBeInstanceOf(RdiStatisticsBlocksSection);\n      expect(result.name).toBe('Processing performance information');\n      expect(result.view).toBe(RdiStatisticsViewType.Blocks);\n      expect(result.data).toHaveLength(9);\n      // v1 fields first, then v2 fields appended at the end\n      expect(result.data).toEqual([\n        { label: 'Total batches', value: 100, units: 'Total' },\n        { label: 'Batch size average', value: 1.5, units: 'MB' },\n        { label: 'Process time average', value: 50, units: 'ms' },\n        { label: 'ACK time average', value: 0.5, units: 'sec' },\n        { label: 'Read time average', value: 10, units: 'ms' },\n        { label: 'Records per second average', value: 1000, units: 'sec' },\n        { label: 'Total time average', value: 60, units: 'ms' },\n        { label: 'Transform time average', value: 5, units: 'ms' },\n        { label: 'Write time average', value: 3, units: 'ms' },\n      ]);\n    });\n\n    it('should return empty data array when data is undefined', () => {\n      const result = transformProcessingPerformance(undefined);\n\n      expect(result).toBeInstanceOf(RdiStatisticsBlocksSection);\n      expect(result.data).toEqual([]);\n    });\n\n    it('should include only non-nil values', () => {\n      const data: Partial<\n        ProcessorMetricsResponse['metrics']['processing_performance']\n      > = {\n        total_batches: 100,\n        transform_time_avg: 5,\n        write_time_avg: 3,\n      };\n\n      const result = transformProcessingPerformance(\n        data as ProcessorMetricsResponse['metrics']['processing_performance'],\n      );\n\n      expect(result.data).toHaveLength(3);\n      expect(result.data[0].label).toBe('Total batches');\n      // v2 fields appended at the end\n      expect(result.data[1].label).toBe('Transform time average');\n      expect(result.data[2].label).toBe('Write time average');\n    });\n\n    it('should include zero values', () => {\n      const data: ProcessorMetricsResponse['metrics']['processing_performance'] =\n        {\n          total_batches: 0,\n          batch_size_avg: 0,\n          read_time_avg: 0,\n          transform_time_avg: 0,\n          write_time_avg: 0,\n          process_time_avg: 0,\n          ack_time_avg: 0,\n          total_time_avg: 0,\n          rec_per_sec_avg: 0,\n        };\n\n      const result = transformProcessingPerformance(data);\n\n      expect(result.data).toHaveLength(9);\n    });\n  });\n\n  describe('transformComponentStatus', () => {\n    it('should return RdiStatisticsTableSection with correct columns and data', () => {\n      const components: GetStatusResponse['components'] = [\n        {\n          name: 'processor',\n          type: 'stream-processor',\n          version: '1.0.0',\n          status: 'started',\n          errors: [],\n          metric_collections: [],\n        },\n        {\n          name: 'collector',\n          type: 'source-collector',\n          version: '1.0.1',\n          status: 'started',\n          errors: ['Connection timeout', 'Retry failed'],\n          metric_collections: [],\n        },\n      ];\n\n      const result = transformComponentStatus(components);\n\n      expect(result).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result!.name).toBe('Component Status');\n      expect(result!.view).toBe(RdiStatisticsViewType.Table);\n      expect(result!.data).toHaveLength(2);\n      expect(result!.data[0]).toEqual({\n        status: 'started',\n        name: 'processor',\n        type: 'stream-processor',\n        version: '1.0.0',\n        errors: '',\n      });\n      expect(result!.data[1]).toEqual({\n        status: 'started',\n        name: 'collector',\n        type: 'source-collector',\n        version: '1.0.1',\n        errors: 'Connection timeout, Retry failed',\n      });\n    });\n\n    it('should return correct column definitions', () => {\n      const components: GetStatusResponse['components'] = [\n        {\n          name: 'processor',\n          type: 'stream-processor',\n          version: '1.0.0',\n          status: 'started',\n          errors: [],\n          metric_collections: [],\n        },\n      ];\n\n      const result = transformComponentStatus(components);\n\n      expect(result!.columns).toHaveLength(5);\n      // All headers are auto-generated from field names (first letter capitalized)\n      expect(result!.columns.map((c) => c.header)).toEqual([\n        'Status',\n        'Name',\n        'Type',\n        'Version',\n        'Errors',\n      ]);\n    });\n\n    it('should return null when components is empty', () => {\n      const result = transformComponentStatus([]);\n\n      expect(result).toBeNull();\n    });\n\n    it('should return null when components is undefined', () => {\n      const result = transformComponentStatus(undefined as any);\n\n      expect(result).toBeNull();\n    });\n\n    it('should handle components with undefined errors', () => {\n      const components: GetStatusResponse['components'] = [\n        {\n          name: 'processor',\n          type: 'stream-processor',\n          version: '1.0.0',\n          status: 'started',\n          errors: undefined as any,\n          metric_collections: [],\n        },\n      ];\n\n      const result = transformComponentStatus(components);\n\n      expect(result!.data[0].errors).toBe('');\n    });\n  });\n\n  describe('transformMetricsCollectionResponse', () => {\n    const mockProcessorMetrics: ProcessorMetricsResponse = {\n      name: 'processor_metrics',\n      component: 'processor',\n      metrics: {\n        processing_performance: {\n          total_batches: 100,\n          batch_size_avg: 1.5,\n          read_time_avg: 10,\n          transform_time_avg: 5,\n          write_time_avg: 3,\n          process_time_avg: 50,\n          ack_time_avg: 0.5,\n          total_time_avg: 60,\n          rec_per_sec_avg: 1000,\n        },\n        rdi_pipeline_status: {\n          rdi_version: '1.0.0',\n          address: 'redis://localhost:6379',\n          run_status: 'running',\n          sync_mode: 'streaming',\n        },\n        connections: {\n          target1: {\n            type: 'redis',\n            host: 'localhost',\n            port: 6379,\n            database: '0',\n            user: 'default',\n            password: 'secret',\n            status: 'connected',\n          },\n        },\n        data_streams: {\n          totals: {\n            total: 1000,\n            pending: 10,\n            inserted: 500,\n            updated: 300,\n            deleted: 100,\n            filtered: 50,\n            rejected: 30,\n            deduplicated: 10,\n          },\n          streams: {\n            stream1: {\n              total: 600,\n              pending: 5,\n              inserted: 300,\n              updated: 200,\n              deleted: 50,\n              filtered: 25,\n              rejected: 15,\n              deduplicated: 5,\n              last_arrival: '2024-01-01T00:00:00Z',\n            },\n          },\n        },\n        clients: {\n          client1: {\n            id: '1',\n            addr: '127.0.0.1:6379',\n            user: 'default',\n            age_sec: '100',\n            idle_sec: '10',\n          },\n        },\n      },\n    };\n\n    const mockCollectorMetrics: CollectorMetricsResponse = {\n      name: 'collector-source_metrics',\n      component: 'collector-source',\n      metrics: {\n        streaming: { Connected: 1, MilliSecondsSinceLastEvent: 13000 },\n        snapshot: { SnapshotCompleted: 1, TotalTableCount: 3 },\n      },\n    };\n\n    it('should return array of 5 sections in correct order', () => {\n      const response: GetMetricsCollectionResponse = [\n        mockCollectorMetrics,\n        mockProcessorMetrics,\n      ];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      expect(result).toHaveLength(5);\n      expect(result[0].name).toBe('General info');\n      expect(result[0].view).toBe(RdiStatisticsViewType.Info);\n      expect(result[1].name).toBe('Processing performance information');\n      expect(result[1].view).toBe(RdiStatisticsViewType.Blocks);\n      expect(result[2].name).toBe('Target Connections');\n      expect(result[2].view).toBe(RdiStatisticsViewType.Table);\n      expect(result[3].name).toBe('Data Streams');\n      expect(result[3].view).toBe(RdiStatisticsViewType.Table);\n      expect(result[4].name).toBe('Clients');\n      expect(result[4].view).toBe(RdiStatisticsViewType.Table);\n    });\n\n    it('should return correct section types', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      expect(result[0]).toBeInstanceOf(RdiStatisticsInfoSection);\n      expect(result[1]).toBeInstanceOf(RdiStatisticsBlocksSection);\n      expect(result[2]).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result[3]).toBeInstanceOf(RdiStatisticsTableSection);\n      expect(result[4]).toBeInstanceOf(RdiStatisticsTableSection);\n    });\n\n    it('should include extended processing performance fields', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      const processingPerformance = result[1] as RdiStatisticsBlocksSection;\n      expect(processingPerformance.data).toHaveLength(9);\n\n      const labels = processingPerformance.data.map((item) => item.label);\n      expect(labels).toContain('Transform time average');\n      expect(labels).toContain('Write time average');\n    });\n\n    it('should return empty array when no processor metrics found', () => {\n      const response: GetMetricsCollectionResponse = [mockCollectorMetrics];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      expect(result).toEqual([]);\n    });\n\n    it('should return empty array when response is empty', () => {\n      const response: GetMetricsCollectionResponse = [];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      expect(result).toEqual([]);\n    });\n\n    it('should include Component Status section when status data is provided', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n      const statusData: GetStatusResponse = {\n        status: 'started',\n        errors: [],\n        components: [\n          {\n            name: 'processor',\n            type: 'stream-processor',\n            version: '1.0.0',\n            status: 'started',\n            errors: [],\n            metric_collections: [],\n          },\n        ],\n        current: true,\n      };\n\n      const result = transformMetricsCollectionResponse(response, statusData);\n\n      expect(result).toHaveLength(6);\n      expect(result[3].name).toBe('Component Status');\n      expect(result[3].view).toBe(RdiStatisticsViewType.Table);\n    });\n\n    it('should place Component Status after Target Connections', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n      const statusData: GetStatusResponse = {\n        status: 'started',\n        errors: [],\n        components: [\n          {\n            name: 'processor',\n            type: 'stream-processor',\n            version: '1.0.0',\n            status: 'started',\n            errors: [],\n            metric_collections: [],\n          },\n        ],\n        current: true,\n      };\n\n      const result = transformMetricsCollectionResponse(response, statusData);\n\n      expect(result[0].name).toBe('General info');\n      expect(result[1].name).toBe('Processing performance information');\n      expect(result[2].name).toBe('Target Connections');\n      expect(result[3].name).toBe('Component Status');\n      expect(result[4].name).toBe('Data Streams');\n      expect(result[5].name).toBe('Clients');\n    });\n\n    it('should work without status data (backward compatible)', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      expect(result).toHaveLength(5);\n      expect(result.map((s) => s.name)).not.toContain('Component Status');\n    });\n\n    it('should not include Component Status when status data is null', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n\n      const result = transformMetricsCollectionResponse(response, null);\n\n      expect(result).toHaveLength(5);\n      expect(result.map((s) => s.name)).not.toContain('Component Status');\n    });\n\n    it('should not include Component Status when components array is empty', () => {\n      const response: GetMetricsCollectionResponse = [mockProcessorMetrics];\n      const statusData: GetStatusResponse = {\n        status: 'started',\n        errors: [],\n        components: [],\n        current: true,\n      };\n\n      const result = transformMetricsCollectionResponse(response, statusData);\n\n      expect(result).toHaveLength(5);\n      expect(result.map((s) => s.name)).not.toContain('Component Status');\n    });\n\n    it('should filter out table sections with empty data', () => {\n      const metricsWithEmptyData: ProcessorMetricsResponse = {\n        name: 'processor_metrics',\n        component: 'processor',\n        metrics: {\n          processing_performance: {\n            total_batches: 100,\n            batch_size_avg: 1.5,\n            read_time_avg: 10,\n            transform_time_avg: 5,\n            write_time_avg: 3,\n            process_time_avg: 50,\n            ack_time_avg: 0.5,\n            total_time_avg: 60,\n            rec_per_sec_avg: 1000,\n          },\n          data_streams: {\n            streams: {},\n            totals: {\n              total: 0,\n              pending: 0,\n              inserted: 0,\n              updated: 0,\n              deleted: 0,\n              filtered: 0,\n              rejected: 0,\n              deduplicated: 0,\n            },\n          },\n          rdi_pipeline_status: {\n            rdi_version: '1.0.0',\n            address: '127.0.0.1:6379',\n            run_status: 'started',\n            sync_mode: 'cdc',\n          },\n          connections: {},\n          clients: {},\n        },\n      };\n\n      const response: GetMetricsCollectionResponse = [metricsWithEmptyData];\n\n      const result = transformMetricsCollectionResponse(response);\n\n      // Should only include General info and Processing performance (non-empty sections)\n      // Target Connections, Data Streams, and Clients should be filtered out\n      expect(result).toHaveLength(2);\n      expect(result[0].name).toBe('General info');\n      expect(result[1].name).toBe('Processing performance information');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/api/v2/transformers/metrics-collections.transformer.ts",
    "content": "import { isNil } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport {\n  RdiStatisticsBlocksSection,\n  RdiStatisticsSection,\n  RdiStatisticsTableSection,\n  RdiStatisticsViewType,\n} from 'src/modules/rdi/models';\nimport * as v1StatisticsTransformers from 'src/modules/rdi/client/api/v1/transformers';\nimport {\n  GetMetricsCollectionResponse,\n  GetStatusResponse,\n  ProcessorMetricsResponse,\n} from 'src/modules/rdi/client/api/v2/responses';\nimport {\n  generateColumns,\n  hasData,\n} from 'src/modules/rdi/utils/transformer.util';\n\ntype ProcessingPerformance =\n  ProcessorMetricsResponse['metrics']['processing_performance'];\n\n/**\n * Transforms processing performance data to RdiStatisticsBlocksSection.\n * Extends v1 transformer with 2 additional fields: transform_time_avg and write_time_avg\n */\nexport const transformProcessingPerformance = (\n  data: ProcessingPerformance,\n): RdiStatisticsBlocksSection => {\n  const result = v1StatisticsTransformers.transformProcessingPerformance(data);\n\n  // Add new v2 fields\n  if (!isNil(data?.transform_time_avg)) {\n    result.data.push({\n      label: 'Transform time average',\n      value: data.transform_time_avg,\n      units: 'ms',\n    });\n  }\n\n  if (!isNil(data?.write_time_avg)) {\n    result.data.push({\n      label: 'Write time average',\n      value: data.write_time_avg,\n      units: 'ms',\n    });\n  }\n\n  return result;\n};\n\n/**\n * Transforms component status data to RdiStatisticsTableSection.\n * Creates a table section showing component name, type, version, status, and errors.\n */\nexport const transformComponentStatus = (\n  components: GetStatusResponse['components'],\n): RdiStatisticsTableSection | null => {\n  if (!components?.length) {\n    return null;\n  }\n\n  const componentData = components.map((component) => ({\n    status: component.status,\n    name: component.name,\n    type: component.type,\n    version: component.version,\n    errors: component.errors?.join(', ') || '',\n  }));\n\n  return plainToInstance(RdiStatisticsTableSection, {\n    name: 'Component Status',\n    view: RdiStatisticsViewType.Table,\n    columns: generateColumns(componentData),\n    data: componentData,\n  });\n};\n\n/**\n * Extracts processor metrics from the v2 metrics collection response\n */\nconst getProcessorMetrics = (\n  data: GetMetricsCollectionResponse,\n): ProcessorMetricsResponse['metrics'] | null => {\n  const processorMetrics = data.find(\n    (item) => item.component === 'processor',\n  ) as ProcessorMetricsResponse | undefined;\n\n  return processorMetrics?.metrics || null;\n};\n\n/**\n * Transforms v2 metrics collection response to RdiStatisticsSection array.\n * Reuses v1 transformers for most sections, uses extended transformer for processing performance.\n * Optionally includes Component Status section when status data is provided (v2 only).\n */\nexport const transformMetricsCollectionResponse = (\n  data: GetMetricsCollectionResponse,\n  statusData?: GetStatusResponse | null,\n): RdiStatisticsSection[] => {\n  const processorMetrics = getProcessorMetrics(data);\n\n  if (!processorMetrics) {\n    return [];\n  }\n\n  const sections: RdiStatisticsSection[] = [\n    v1StatisticsTransformers.transformGeneralInfo(\n      processorMetrics.rdi_pipeline_status,\n    ),\n    transformProcessingPerformance(processorMetrics.processing_performance),\n    v1StatisticsTransformers.transformConnectionsStatistics(\n      processorMetrics.connections,\n    ),\n  ];\n\n  // Add Component Status after Target Connections (v2 only)\n  const componentStatusSection = statusData\n    ? transformComponentStatus(statusData.components)\n    : null;\n  if (componentStatusSection) {\n    sections.push(componentStatusSection);\n  }\n\n  sections.push(\n    v1StatisticsTransformers.transformDataStreamsStatistics(\n      processorMetrics.data_streams,\n    ),\n    v1StatisticsTransformers.transformClientStatistics(\n      processorMetrics.clients,\n    ),\n  );\n\n  // Filter out sections with no data\n  return sections.filter(hasData);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/client/rdi.client.ts",
    "content": "import {\n  Rdi,\n  RdiClientMetadata,\n  RdiPipeline,\n  RdiPipelineStatus,\n  RdiStatisticsResult,\n} from 'src/modules/rdi/models';\nimport {\n  RdiDryRunJobDto,\n  RdiDryRunJobResponseDto,\n  RdiTemplateResponseDto,\n  RdiTestConnectionsResponseDto,\n} from 'src/modules/rdi/dto';\nimport { IDLE_THRESHOLD } from 'src/modules/rdi/constants';\n\nexport abstract class RdiClient {\n  public readonly id: string;\n\n  public lastUsed: number = Date.now();\n\n  protected constructor(\n    public readonly metadata: RdiClientMetadata,\n    protected readonly rdi: Rdi,\n  ) {\n    this.id = RdiClient.generateId(this.metadata);\n  }\n\n  public isIdle(): boolean {\n    return Date.now() - this.lastUsed > IDLE_THRESHOLD;\n  }\n\n  abstract getSchema(): Promise<object>;\n\n  abstract getPipeline(): Promise<RdiPipeline>;\n\n  abstract getConfigTemplate(\n    pipelineType: string,\n    dbType: string,\n  ): Promise<RdiTemplateResponseDto>;\n\n  abstract getJobTemplate(\n    pipelineType: string,\n  ): Promise<RdiTemplateResponseDto>;\n\n  abstract getStrategies(): Promise<object>;\n\n  abstract deploy(pipeline: RdiPipeline): Promise<void>;\n\n  abstract stopPipeline(): Promise<void>;\n\n  abstract startPipeline(): Promise<void>;\n\n  abstract resetPipeline(): Promise<void>;\n\n  abstract dryRunJob(data: RdiDryRunJobDto): Promise<RdiDryRunJobResponseDto>;\n\n  abstract testConnections(\n    config: object,\n  ): Promise<RdiTestConnectionsResponseDto>;\n\n  abstract getStatistics(): Promise<RdiStatisticsResult>;\n\n  abstract getPipelineStatus(): Promise<RdiPipelineStatus>;\n\n  abstract getVersion(): Promise<string>;\n\n  abstract getJobFunctions(): Promise<object>;\n\n  abstract ensureAuth(): Promise<void>;\n\n  abstract connect(): Promise<void>;\n\n  public setLastUsed(): void {\n    this.lastUsed = Date.now();\n  }\n\n  static generateId(cm: RdiClientMetadata): string {\n    const empty = '(nil)';\n    const separator = '_';\n\n    const id = [cm.id].join(separator);\n\n    const uId = [\n      cm.sessionMetadata?.userId || empty,\n      cm.sessionMetadata?.accountId || empty,\n      cm.sessionMetadata?.sessionId || empty,\n      cm.sessionMetadata?.uniqueId || empty,\n    ].join(separator);\n\n    return [id, uId].join(separator);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/constants/index.ts",
    "content": "export enum RdiUrl {\n  GetConfigSchema = 'api/v1/pipelines/config/schemas',\n  GetJobsSchema = 'api/v1/pipelines/jobs/schemas',\n  GetPipeline = 'api/v1/pipelines',\n  GetStrategies = 'api/v1/pipelines/strategies',\n  GetConfigTemplate = 'api/v1/pipelines/config/templates',\n  GetJobTemplate = 'api/v1/pipelines/jobs/templates',\n  DryRunJob = 'api/v1/pipelines/jobs/dry-run',\n  JobFunctions = '/api/v1/pipelines/jobs/functions',\n  Deploy = 'api/v1/pipelines',\n  StopPipeline = 'api/v1/pipelines/stop',\n  StartPipeline = 'api/v1/pipelines/start',\n  ResetPipeline = 'api/v1/pipelines/reset',\n  TestTargetsConnections = 'api/v1/pipelines/targets/dry-run',\n  TestSourcesConnections = 'api/v1/pipelines/sources/dry-run',\n  GetStatistics = 'api/v1/monitoring/statistics',\n  GetPipelineStatus = 'api/v1/status',\n  Login = 'api/v1/login',\n  Action = 'api/v1/actions',\n}\n\nexport const RdiUrlV2 = {\n  GetInfo: 'api/v2/info',\n  GetPipelines: 'api/v2/pipelines',\n  GetMetricsCollections: (name: string) =>\n    `api/v2/pipelines/${name}/metric-collections`,\n  GetPipelineStatus: (name: string) => `api/v2/pipelines/${name}/status`,\n};\n\nexport const IDLE_THRESHOLD = 10 * 60 * 1000; // 10 min\nexport const RDI_TIMEOUT = 30_000; // 30 sec\nexport const TOKEN_THRESHOLD = 2 * 60 * 1000; // 2 min\nexport const RDI_SYNC_INTERVAL = 5 * 60 * 1_000; // 5 min\nexport const POLLING_INTERVAL = 1_000;\nexport const MAX_POLLING_TIME = 2 * 60 * 1000; // 2 min\nexport const WAIT_BEFORE_POLLING = 1_000;\n\nexport enum PipelineActions {\n  Deploy = 'Deploy',\n  Reset = 'Reset',\n  Start = 'Start',\n  Stop = 'Stop',\n}\n\nexport const DEFAULT_RDI_VERSION = '-';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/decorators/index.ts",
    "content": "export * from './request.rdi.client.metadata.decorator';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/decorators/request.rdi.client.metadata.decorator.ts",
    "content": "import {\n  BadRequestException,\n  createParamDecorator,\n  ExecutionContext,\n} from '@nestjs/common';\nimport { sessionMetadataFromRequest } from 'src/common/decorators';\nimport { plainToInstance } from 'class-transformer';\nimport { RdiClientMetadata } from 'src/modules/rdi/models';\nimport { Validator } from 'class-validator';\nimport { ApiParam } from '@nestjs/swagger';\n\nconst validator = new Validator();\n\nexport const RequestRdiClientMetadata = createParamDecorator(\n  (_: unknown, ctx: ExecutionContext) => {\n    const req = ctx.switchToHttp().getRequest();\n\n    const rdiClientMetadata = plainToInstance(RdiClientMetadata, {\n      id: req.params?.['id'],\n      sessionMetadata: sessionMetadataFromRequest(req),\n    });\n\n    const errors = validator.validateSync(rdiClientMetadata, {\n      whitelist: false, // we need this to allow additional fields if needed for flexibility\n    });\n\n    if (errors?.length) {\n      throw new BadRequestException(\n        Object.values(errors[0].constraints) || 'Bad request',\n      );\n    }\n\n    return rdiClientMetadata;\n  },\n  [\n    (target: any, key: string) => {\n      ApiParam({\n        name: 'id',\n        schema: { type: 'string' },\n        required: true,\n      })(target, key, Object.getOwnPropertyDescriptor(target, key));\n    },\n  ],\n);\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/create.rdi.dto.ts",
    "content": "import { OmitType } from '@nestjs/swagger';\nimport { Rdi } from 'src/modules/rdi/models';\n\nexport class CreateRdiDto extends OmitType(Rdi, [\n  'id',\n  'lastConnection',\n  'version',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/index.ts",
    "content": "export * from './create.rdi.dto';\nexport * from './update.rdi.dto';\nexport * from './rdi.dry-run.job.dto';\nexport * from './rdi.dry-run.job.response.dto';\nexport * from './rdi-test-connections.response.dto';\nexport * from './rdi-template.response.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/rdi-template.response.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class RdiTemplateResponseDto {\n  @ApiProperty({\n    description: 'Template for rdi file',\n    type: String,\n  })\n  @Expose()\n  template: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/rdi-test-connections.response.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\nimport { TransformToMap } from 'src/common/decorators/transform-to-map.decorator';\n\nexport enum RdiTestConnectionStatus {\n  Success = 'success',\n  Fail = 'fail',\n}\n\nclass ErrorDetails {\n  @ApiProperty({\n    description: 'Error code',\n    type: String,\n  })\n  @Expose()\n  code: string;\n\n  @ApiProperty({\n    description: 'Error message',\n    type: String,\n  })\n  @Expose()\n  message: string;\n}\n\nexport class RdiTestTargetConnectionResult {\n  @ApiProperty({\n    description: 'Connection status',\n    enum: RdiTestConnectionStatus,\n  })\n  @Expose()\n  status: RdiTestConnectionStatus;\n\n  @ApiPropertyOptional({\n    description: 'Error details if any',\n    type: ErrorDetails,\n  })\n  @Expose()\n  @Type(() => ErrorDetails)\n  error?: ErrorDetails;\n}\n\nexport class RdiTestSourceConnectionResult {\n  @ApiProperty({ description: 'Indicates if the source is connected' })\n  @Expose()\n  connected: boolean;\n\n  @ApiProperty({\n    description: 'Error message if connection fails',\n    required: false,\n  })\n  @Expose()\n  error?: string;\n}\n\nexport class RdiTestConnectionsResponseDto {\n  @ApiProperty({\n    description: 'Sources connection results',\n  })\n  @Expose()\n  @TransformToMap(RdiTestSourceConnectionResult)\n  sources: Record<string, RdiTestSourceConnectionResult>;\n\n  @ApiProperty({\n    description: 'Targets connection results',\n  })\n  @Expose()\n  @TransformToMap(RdiTestTargetConnectionResult)\n  targets: Record<string, RdiTestTargetConnectionResult>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/rdi.dry-run.job.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { IsNotEmpty } from 'class-validator';\n\nexport class RdiDryRunJobDto {\n  @ApiProperty({\n    description: 'Input data',\n    type: Object,\n  })\n  @Expose()\n  @IsNotEmpty()\n  input_data: object;\n\n  @ApiProperty({\n    description: 'Job file',\n    type: Object,\n  })\n  @Expose()\n  @IsNotEmpty()\n  job: object;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/rdi.dry-run.job.response.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { RdiDryRunJobResult } from 'src/modules/rdi/models/rdi-dry-run';\n\nexport class RdiDryRunJobResponseDto {\n  @ApiProperty({\n    description: 'Dry run job transformations result',\n    type: RdiDryRunJobResult,\n  })\n  @Expose()\n  transformations: RdiDryRunJobResult;\n\n  @ApiProperty({\n    description: 'Dry run job commands result ',\n    type: RdiDryRunJobResult,\n  })\n  @Expose()\n  commands: RdiDryRunJobResult;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/dto/update.rdi.dto.ts",
    "content": "import { OmitType, PartialType } from '@nestjs/swagger';\nimport { Rdi } from 'src/modules/rdi/models';\n\nexport class UpdateRdiDto extends PartialType(\n  OmitType(Rdi, ['id', 'lastConnection', 'url', 'version'] as const),\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/entities/rdi.entity.ts",
    "content": "import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\nimport { Expose } from 'class-transformer';\n\n@Entity('rdi')\nexport class RdiEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  url: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  name: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  username: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  password: string;\n\n  @Expose()\n  @Column({ type: 'datetime', nullable: true })\n  lastConnection: Date;\n\n  @Expose()\n  @Column({ nullable: false })\n  version: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/index.ts",
    "content": "export * from './rdi-deploy-failed.exception';\nexport * from './rdi-pipeline.error.handler';\nexport * from './rdi-pipeline.internal-server-error.exception';\nexport * from './rdi-pipeline.not-found.exception';\nexport * from './rdi-pipeline.unauthorized.exception';\nexport * from './rdi-pipeline.validation.exception';\nexport * from './rdi-reset-pipeline-failed.exception';\nexport * from './rdi-start-pipeline-failed.exception';\nexport * from './rdi-stop-pipeline-failed.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-deploy-failed.exception.spec.ts",
    "content": "import ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\nimport { HttpStatus } from '@nestjs/common';\nimport { RdiPipelineDeployFailedException } from './rdi-deploy-failed.exception';\n\ndescribe('RdiPipelineDeployFailedException', () => {\n  it('should create an exception with default message and status code', () => {\n    const exception = new RdiPipelineDeployFailedException();\n    expect(exception.message).toEqual(\n      ERROR_MESSAGES.RDI_DEPLOY_PIPELINE_FAILURE,\n    );\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: ERROR_MESSAGES.RDI_DEPLOY_PIPELINE_FAILURE,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiPipelineDeployFailed',\n      errorCode: CustomErrorCodes.RdiDeployPipelineFailure,\n      errors: [undefined],\n    });\n  });\n\n  it('should create an exception with custom message and error', () => {\n    const customMessage = 'Custom error message';\n    const customError = 'Custom error';\n    const exception = new RdiPipelineDeployFailedException(customMessage, {\n      error: customError,\n    });\n    expect(exception.message).toEqual(customMessage);\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: customMessage,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiPipelineDeployFailed',\n      errorCode: CustomErrorCodes.RdiDeployPipelineFailure,\n      errors: [customError],\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-deploy-failed.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class RdiPipelineDeployFailedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE_FAILURE,\n    options?: HttpExceptionOptions & { error?: string },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiPipelineDeployFailed',\n      errorCode: CustomErrorCodes.RdiDeployPipelineFailure,\n      errors: [options?.error],\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.bad-request.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineBadRequestException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.BAD_REQUEST,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiBadRequest',\n      errorCode: CustomErrorCodes.RdiBadRequest,\n      detail: options?.details,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.error.handler.spec.ts",
    "content": "import { AxiosError } from 'axios';\nimport { HttpStatus } from '@nestjs/common';\nimport {\n  RdiPipelineInternalServerErrorException,\n  RdiPipelineUnauthorizedException,\n  RdiPipelineNotFoundException,\n  RdiPipelineValidationException,\n} from 'src/modules/rdi/exceptions';\nimport { CustomErrorCodes } from 'src/constants';\nimport errorMessages from 'src/constants/error-messages';\nimport { wrapRdiPipelineError } from './rdi-pipeline.error.handler';\nimport { RdiPipelineForbiddenException } from './rdi-pipeline.forbidden.exception';\n\ndescribe('wrapRdiPipelineError', () => {\n  it('should return the original error if it is an instance of HttpException', () => {\n    const error = new RdiPipelineNotFoundException();\n    const result = wrapRdiPipelineError(error as any);\n    expect(result).toBe(error);\n  });\n\n  it('should return a RdiPipelineUnauthorizedException if the response status is 401', () => {\n    const error = {\n      response: {\n        status: 401,\n        data: {\n          detail: {\n            message: 'Unauthorized',\n          },\n        },\n      },\n    } as AxiosError;\n    const result = wrapRdiPipelineError(error);\n\n    expect(result).toBeInstanceOf(RdiPipelineUnauthorizedException);\n    expect(result.getResponse()).toEqual({\n      message: result.message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'RdiUnauthorized',\n      errorCode: CustomErrorCodes.RdiUnauthorized,\n    });\n  });\n\n  it('should return a RdiPipelineUnauthorizedException with a default unauthorized message if the response status is 401 and message is not provided', () => {\n    const error = {\n      response: {\n        status: 401,\n        data: {\n          detail: {\n            message: 'Unauthorized',\n          },\n        },\n      },\n    } as AxiosError;\n    const result = wrapRdiPipelineError(error);\n\n    expect(result).toBeInstanceOf(RdiPipelineUnauthorizedException);\n    expect(result.message).toBe('Unauthorized');\n  });\n\n  it('should return a RdiPipelineForbiddenException if the response status is 403', () => {\n    const error = {\n      response: {\n        status: 403,\n        data: {\n          detail: {\n            message: 'Unauthorized',\n          },\n        },\n      },\n    } as AxiosError;\n    const result = wrapRdiPipelineError(error);\n\n    expect(result).toBeInstanceOf(RdiPipelineForbiddenException);\n    expect(result.getResponse()).toEqual({\n      message: result.message,\n      statusCode: HttpStatus.FORBIDDEN,\n      error: 'RdiForbidden',\n      errorCode: CustomErrorCodes.RdiForbidden,\n    });\n  });\n\n  it('should return a RdiPipelineValidationException if the response status is 422', () => {\n    const errorDetails = {\n      errors: {\n        email: ['Email is invalid'],\n      },\n    };\n    const error = {\n      response: {\n        status: 422,\n        data: {\n          detail: errorDetails,\n        },\n      },\n    } as AxiosError;\n    const result = wrapRdiPipelineError(error);\n    expect(result).toBeInstanceOf(RdiPipelineValidationException);\n    expect(result.getResponse()).toEqual({\n      message: result.message,\n      statusCode: HttpStatus.UNPROCESSABLE_ENTITY,\n      error: 'RdiValidationError',\n      errorCode: CustomErrorCodes.RdiValidationError,\n      details: errorDetails,\n    });\n  });\n\n  it('should return a RdiPipelineValidationException with a default validation message if the response status is 422 and message is not provided', () => {\n    const errorDetails = {\n      errors: {\n        email: ['Email is invalid'],\n      },\n    };\n    const error = {\n      response: {\n        status: 422,\n        data: {\n          detail: errorDetails,\n        },\n      },\n    } as AxiosError;\n    const result = wrapRdiPipelineError(error);\n    expect(result).toBeInstanceOf(RdiPipelineValidationException);\n    expect(result.message).toBe(errorMessages.RDI_VALIDATION_ERROR);\n  });\n\n  it('should return a RdiPipelineNotFoundException if the response status is not 401 or 422', () => {\n    const errorDetails = {\n      message: 'Not found',\n      details: 'User was not found',\n    };\n\n    const error = {\n      response: {\n        status: 404,\n        data: {\n          detail: errorDetails,\n        },\n      },\n    } as AxiosError;\n    const result = wrapRdiPipelineError(error);\n    expect(result).toBeInstanceOf(RdiPipelineNotFoundException);\n    expect(result.getResponse()).toEqual({\n      message: result.message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'RdiNotFound',\n      errorCode: CustomErrorCodes.RdiNotFound,\n      details: errorDetails.details,\n    });\n  });\n\n  it('should return a RdiPipelineInternalServerErrorException if there is no response', () => {\n    const error = {} as AxiosError;\n    const result = wrapRdiPipelineError(error);\n    expect(result).toBeInstanceOf(RdiPipelineInternalServerErrorException);\n    expect(result.message).toBe(errorMessages.INTERNAL_SERVER_ERROR);\n    expect(result.getResponse()).toEqual({\n      message: errorMessages.INTERNAL_SERVER_ERROR,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'RdiInternalServerError',\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.error.handler.ts",
    "content": "import { AxiosError } from 'axios';\nimport { HttpException } from '@nestjs/common';\nimport {\n  RdiPipelineInternalServerErrorException,\n  RdiPipelineUnauthorizedException,\n  RdiPipelineNotFoundException,\n  RdiPipelineValidationException,\n} from 'src/modules/rdi/exceptions';\nimport { RdiPipelineForbiddenException } from './rdi-pipeline.forbidden.exception';\nimport { RdiPipelineBadRequestException } from 'src/modules/rdi/exceptions/rdi-pipeline.bad-request.exception';\n\nexport const parseErrorMessage = (error: AxiosError<any>): string => {\n  const data = error.response?.data;\n  if (typeof data === 'string') {\n    return data;\n  }\n\n  const detail = data?.detail;\n  if (!detail) return error.message;\n\n  if (typeof detail === 'string') return detail;\n\n  return detail.message || detail.msg;\n};\n\nexport const wrapRdiPipelineError = (error: AxiosError<any>): HttpException => {\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  const { response } = error;\n  const message: string = parseErrorMessage(error);\n\n  if (response) {\n    const errorOptions = response?.data?.detail;\n    switch (response?.status) {\n      case 400:\n        return new RdiPipelineBadRequestException(message, errorOptions);\n      case 401:\n        return new RdiPipelineUnauthorizedException(message, errorOptions);\n      case 403:\n        return new RdiPipelineForbiddenException(message, errorOptions);\n      case 422:\n        return new RdiPipelineValidationException(message, errorOptions);\n      case 404:\n        return new RdiPipelineNotFoundException(message, errorOptions);\n      default:\n        return new RdiPipelineInternalServerErrorException(message);\n    }\n  }\n\n  return new RdiPipelineInternalServerErrorException(message);\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.forbidden.exception.spec.ts",
    "content": "import { HttpStatus } from '@nestjs/common';\nimport errorMessages from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\nimport { RdiPipelineForbiddenException } from './rdi-pipeline.forbidden.exception';\n\ndescribe('RdiPipelineForbiddenException', () => {\n  it('should create a RdiPipelineForbiddenException with default message and status code', () => {\n    const exception = new RdiPipelineForbiddenException();\n    expect(exception.getStatus()).toBe(HttpStatus.FORBIDDEN);\n    expect(exception.getResponse()).toEqual({\n      statusCode: HttpStatus.FORBIDDEN,\n      message: errorMessages.FORBIDDEN,\n      error: 'RdiForbidden',\n      errorCode: CustomErrorCodes.RdiForbidden,\n    });\n  });\n\n  it('should create a RdiPipelineForbiddenException with custom message and status code', () => {\n    const customMessage = 'Custom forbidden message';\n    const exception = new RdiPipelineForbiddenException(customMessage);\n    expect(exception.getStatus()).toBe(HttpStatus.FORBIDDEN);\n    expect(exception.getResponse()).toEqual({\n      statusCode: HttpStatus.FORBIDDEN,\n      message: customMessage,\n      error: 'RdiForbidden',\n      errorCode: CustomErrorCodes.RdiForbidden,\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.forbidden.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineForbiddenException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.FORBIDDEN,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.FORBIDDEN,\n      error: 'RdiForbidden',\n      errorCode: CustomErrorCodes.RdiForbidden,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineInternalServerErrorException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.INTERNAL_SERVER_ERROR,\n    options?: HttpExceptionOptions,\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,\n      error: 'RdiInternalServerError',\n    };\n\n    super(response, HttpStatus.INTERNAL_SERVER_ERROR, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.not-found.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineNotFoundException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.BAD_REQUEST,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.NOT_FOUND,\n      error: 'RdiNotFound',\n      errorCode: CustomErrorCodes.RdiNotFound,\n      details: options?.details,\n    };\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception.spec.ts",
    "content": "import { HttpStatus } from '@nestjs/common';\nimport errorMessages from 'src/constants/error-messages';\nimport { RdiPipelineTimeoutException } from './rdi-pipeline.timeout-error.exception';\n\ndescribe('RdiPipelineTimeoutException', () => {\n  it('should create a RdiPipelineTimeoutException with default message and status code', () => {\n    const exception = new RdiPipelineTimeoutException();\n    expect(exception.getStatus()).toBe(HttpStatus.REQUEST_TIMEOUT);\n    expect(exception.getResponse()).toEqual({\n      statusCode: HttpStatus.REQUEST_TIMEOUT,\n      message: errorMessages.RDI_TIMEOUT_ERROR,\n      error: 'Timeout Error',\n    });\n  });\n\n  it('should create a RdiPipelineTimeoutException with custom message and status code', () => {\n    const customMessage = 'Custom timeout message';\n    const exception = new RdiPipelineTimeoutException(customMessage);\n    expect(exception.getStatus()).toBe(HttpStatus.REQUEST_TIMEOUT);\n    expect(exception.getResponse()).toEqual({\n      statusCode: HttpStatus.REQUEST_TIMEOUT,\n      message: customMessage,\n      error: 'Timeout Error',\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception.ts",
    "content": "import { HttpException, HttpStatus } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineTimeoutException extends HttpException {\n  constructor(message = ERROR_MESSAGES.RDI_TIMEOUT_ERROR) {\n    super(\n      {\n        statusCode: HttpStatus.REQUEST_TIMEOUT,\n        message,\n        error: 'Timeout Error',\n      },\n      HttpStatus.REQUEST_TIMEOUT,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.unauthorized.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineUnauthorizedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.UNAUTHORIZED,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNAUTHORIZED,\n      error: 'RdiUnauthorized',\n      errorCode: CustomErrorCodes.RdiUnauthorized,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.validation.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nexport class RdiPipelineValidationException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.RDI_VALIDATION_ERROR,\n    options?: HttpExceptionOptions & { details?: unknown },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.UNPROCESSABLE_ENTITY,\n      error: 'RdiValidationError',\n      errorCode: CustomErrorCodes.RdiValidationError,\n      details: options,\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-reset-pipeline-failed.exception.spec.ts",
    "content": "import ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\nimport { HttpStatus } from '@nestjs/common';\nimport { RdiResetPipelineFailedException } from './rdi-reset-pipeline-failed.exception';\n\ndescribe('RdiResetPipelineFailedException', () => {\n  it('should create an exception with default message and status code', () => {\n    const exception = new RdiResetPipelineFailedException();\n    expect(exception.message).toEqual(\n      ERROR_MESSAGES.RDI_RESET_PIPELINE_FAILURE,\n    );\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: ERROR_MESSAGES.RDI_RESET_PIPELINE_FAILURE,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiResetPipelineFailed',\n      errorCode: CustomErrorCodes.RdiResetPipelineFailure,\n      errors: [undefined],\n    });\n  });\n\n  it('should create an exception with custom message and error', () => {\n    const customMessage = 'Custom error message';\n    const customError = 'Custom error';\n    const exception = new RdiResetPipelineFailedException(customMessage, {\n      error: customError,\n    });\n    expect(exception.message).toEqual(customMessage);\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: customMessage,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiResetPipelineFailed',\n      errorCode: CustomErrorCodes.RdiResetPipelineFailure,\n      errors: [customError],\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-reset-pipeline-failed.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class RdiResetPipelineFailedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.RDI_RESET_PIPELINE_FAILURE,\n    options?: HttpExceptionOptions & { error?: string },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiResetPipelineFailed',\n      errorCode: CustomErrorCodes.RdiResetPipelineFailure,\n      errors: [options?.error],\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-start-pipeline-failed.exception.spec.ts",
    "content": "import ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\nimport { HttpStatus } from '@nestjs/common';\nimport { RdiStartPipelineFailedException } from './rdi-start-pipeline-failed.exception';\n\ndescribe('RdiStartPipelineFailedException', () => {\n  it('should create an exception with default message and status code', () => {\n    const exception = new RdiStartPipelineFailedException();\n    expect(exception.message).toEqual(\n      ERROR_MESSAGES.RDI_START_PIPELINE_FAILURE,\n    );\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: ERROR_MESSAGES.RDI_START_PIPELINE_FAILURE,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiStartPipelineFailed',\n      errorCode: CustomErrorCodes.RdiStartPipelineFailure,\n      errors: [undefined],\n    });\n  });\n\n  it('should create an exception with custom message and error', () => {\n    const customMessage = 'Custom error message';\n    const customError = 'Custom error';\n    const exception = new RdiStartPipelineFailedException(customMessage, {\n      error: customError,\n    });\n    expect(exception.message).toEqual(customMessage);\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: customMessage,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiStartPipelineFailed',\n      errorCode: CustomErrorCodes.RdiStartPipelineFailure,\n      errors: [customError],\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-start-pipeline-failed.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class RdiStartPipelineFailedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.RDI_START_PIPELINE_FAILURE,\n    options?: HttpExceptionOptions & { error?: string },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiStartPipelineFailed',\n      errorCode: CustomErrorCodes.RdiStartPipelineFailure,\n      errors: [options?.error],\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-stop-pipeline-failed.exception.spec.ts",
    "content": "import ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\nimport { HttpStatus } from '@nestjs/common';\nimport { RdiStopPipelineFailedException } from './rdi-stop-pipeline-failed.exception';\n\ndescribe('RdiStopPipelineFailedException', () => {\n  it('should create an exception with default message and status code', () => {\n    const exception = new RdiStopPipelineFailedException();\n    expect(exception.message).toEqual(ERROR_MESSAGES.RDI_STOP_PIPELINE_FAILURE);\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: ERROR_MESSAGES.RDI_STOP_PIPELINE_FAILURE,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiStopPipelineFailed',\n      errorCode: CustomErrorCodes.RdiStopPipelineFailure,\n      errors: [undefined],\n    });\n  });\n\n  it('should create an exception with custom message and error', () => {\n    const customMessage = 'Custom error message';\n    const customError = 'Custom error';\n    const exception = new RdiStopPipelineFailedException(customMessage, {\n      error: customError,\n    });\n    expect(exception.message).toEqual(customMessage);\n    expect(exception.getStatus()).toEqual(HttpStatus.BAD_REQUEST);\n    expect(exception.getResponse()).toEqual({\n      message: customMessage,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiStopPipelineFailed',\n      errorCode: CustomErrorCodes.RdiStopPipelineFailure,\n      errors: [customError],\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/exceptions/rdi-stop-pipeline-failed.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nexport class RdiStopPipelineFailedException extends HttpException {\n  constructor(\n    message = ERROR_MESSAGES.RDI_STOP_PIPELINE_FAILURE,\n    options?: HttpExceptionOptions & { error?: string },\n  ) {\n    const response = {\n      message,\n      statusCode: HttpStatus.BAD_REQUEST,\n      error: 'RdiStopPipelineFailed',\n      errorCode: CustomErrorCodes.RdiStopPipelineFailure,\n      errors: [options?.error],\n    };\n\n    super(response, response.statusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/index.ts",
    "content": "export * from './rdi.client.metadata';\nexport * from './rdi';\nexport * from './rdi-pipeline';\nexport * from './rdi-dry-run';\nexport * from './rdi-statistics';\nexport * from './rdi-info';\nexport * from './rdi.pipeline.status';\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi-dry-run.ts",
    "content": "export enum RdiDyRunJobStatus {\n  Success = 'success',\n  Fail = 'failed',\n}\n\nexport class RdiDryRunJobResult {\n  status: RdiDyRunJobStatus;\n\n  // TODO validate with RDI team\n  data?: any;\n\n  error?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi-info.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class RdiInfo {\n  @ApiProperty({\n    description: 'Current RDI collector version',\n    type: String,\n  })\n  @Expose()\n  version: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi-pipeline.ts",
    "content": "import { IsObject, IsOptional } from 'class-validator';\nimport { Expose } from 'class-transformer';\n\nexport class RdiPipeline {\n  @Expose()\n  @IsOptional()\n  @IsObject()\n  // todo add validation\n  jobs: { [key: string]: object };\n\n  @Expose()\n  @IsOptional()\n  @IsObject()\n  config: object;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi-statistics.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\n\nexport enum RdiStatisticsStatus {\n  Success = 'success',\n  Fail = 'failed',\n}\n\nexport enum RdiStatisticsViewType {\n  Table = 'table',\n  Blocks = 'blocks',\n  Info = 'info',\n}\n\n// ============ Table View ============\n\nexport class RdiStatisticsColumn {\n  @ApiProperty({\n    description: 'Column identifier',\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Column header text',\n    type: String,\n  })\n  @Expose()\n  header: string;\n\n  @ApiPropertyOptional({\n    description: 'Column type for custom rendering',\n    type: String,\n  })\n  @Expose()\n  type?: string;\n}\n\nexport class RdiStatisticsTableSection {\n  @ApiProperty({\n    description: 'Section name/title',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'View type for rendering',\n    enum: [RdiStatisticsViewType.Table],\n    example: RdiStatisticsViewType.Table,\n  })\n  @Expose()\n  view: RdiStatisticsViewType.Table;\n\n  @ApiProperty({\n    description: 'Column definitions',\n    type: [RdiStatisticsColumn],\n  })\n  @Expose()\n  @Type(() => RdiStatisticsColumn)\n  columns: RdiStatisticsColumn[];\n\n  @ApiProperty({\n    description: 'Table rows data',\n    type: [Object],\n  })\n  @Expose()\n  data: Record<string, unknown>[];\n\n  @ApiPropertyOptional({\n    description: 'Footer row data',\n    type: Object,\n  })\n  @Expose()\n  footer?: Record<string, unknown>;\n}\n\n// ============ Blocks View ============\n\nexport class RdiStatisticsBlockItem {\n  @ApiProperty({\n    description: 'Block label',\n    type: String,\n  })\n  @Expose()\n  label: string;\n\n  @ApiProperty({\n    description: 'Block value',\n    type: Number,\n  })\n  @Expose()\n  value: number;\n\n  @ApiProperty({\n    description: 'Value units',\n    type: String,\n  })\n  @Expose()\n  units: string;\n}\n\nexport class RdiStatisticsBlocksSection {\n  @ApiProperty({\n    description: 'Section name/title',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'View type for rendering',\n    enum: [RdiStatisticsViewType.Blocks],\n    example: RdiStatisticsViewType.Blocks,\n  })\n  @Expose()\n  view: RdiStatisticsViewType.Blocks;\n\n  @ApiProperty({\n    description: 'Block items data',\n    type: [RdiStatisticsBlockItem],\n  })\n  @Expose()\n  @Type(() => RdiStatisticsBlockItem)\n  data: RdiStatisticsBlockItem[];\n}\n\n// ============ Info View ============\n\nexport class RdiStatisticsInfoItem {\n  @ApiProperty({\n    description: 'Info label',\n    type: String,\n  })\n  @Expose()\n  label: string;\n\n  @ApiProperty({\n    description: 'Info value',\n    type: String,\n  })\n  @Expose()\n  value: string;\n}\n\nexport class RdiStatisticsInfoSection {\n  @ApiProperty({\n    description: 'Section name/title',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'View type for rendering',\n    enum: [RdiStatisticsViewType.Info],\n    example: RdiStatisticsViewType.Info,\n  })\n  @Expose()\n  view: RdiStatisticsViewType.Info;\n\n  @ApiProperty({\n    description: 'Info items data',\n    type: [RdiStatisticsInfoItem],\n  })\n  @Expose()\n  @Type(() => RdiStatisticsInfoItem)\n  data: RdiStatisticsInfoItem[];\n}\n\n// ============ Union Type ============\n\nexport type RdiStatisticsSection =\n  | RdiStatisticsTableSection\n  | RdiStatisticsBlocksSection\n  | RdiStatisticsInfoSection;\n\n// ============ Result ============\n\nexport class RdiStatisticsData {\n  @ApiProperty({\n    description: 'Statistics sections',\n    type: 'array',\n    items: {\n      oneOf: [\n        { $ref: '#/components/schemas/RdiStatisticsTableSection' },\n        { $ref: '#/components/schemas/RdiStatisticsBlocksSection' },\n        { $ref: '#/components/schemas/RdiStatisticsInfoSection' },\n      ],\n      discriminator: {\n        propertyName: 'view',\n        mapping: {\n          [RdiStatisticsViewType.Table]:\n            '#/components/schemas/RdiStatisticsTableSection',\n          [RdiStatisticsViewType.Blocks]:\n            '#/components/schemas/RdiStatisticsBlocksSection',\n          [RdiStatisticsViewType.Info]:\n            '#/components/schemas/RdiStatisticsInfoSection',\n        },\n      },\n    },\n  })\n  @Expose()\n  sections: RdiStatisticsSection[];\n}\n\nexport class RdiStatisticsResult {\n  @ApiProperty({\n    description: 'Statistics status',\n    enum: RdiStatisticsStatus,\n  })\n  @Expose()\n  status: RdiStatisticsStatus;\n\n  @ApiPropertyOptional({\n    description: 'Statistics data',\n    type: RdiStatisticsData,\n  })\n  @Expose()\n  @Type(() => RdiStatisticsData)\n  data?: RdiStatisticsData;\n\n  @ApiPropertyOptional({\n    description: 'Error message',\n    type: String,\n  })\n  @Expose()\n  error?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts",
    "content": "import { SessionMetadata } from 'src/common/models/session';\nimport { Type } from 'class-transformer';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class RdiClientMetadata {\n  @IsNotEmpty()\n  @Type(() => SessionMetadata)\n  sessionMetadata: SessionMetadata;\n\n  @IsNotEmpty()\n  @IsString()\n  id: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi.pipeline.status.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose, Type } from 'class-transformer';\n\nexport enum PipelineStatus {\n  // V1 statuses\n  Ready = 'ready',\n  NotReady = 'not-ready',\n  // V1/V2 intersection\n  Stopped = 'stopped',\n  // V2 statutes\n  Started = 'started',\n  Error = 'error',\n  Creating = 'creating',\n  Updating = 'updating',\n  Deleting = 'deleting',\n  Starting = 'starting',\n  Stopping = 'stopping',\n  Resetting = 'resetting',\n  Pending = 'pending',\n  Unknown = 'unknown',\n}\n\nexport enum PipelineState {\n  CDC = 'cdc',\n  InitialSync = 'initial-sync',\n  NotRunning = 'not-running',\n}\n\nexport class ComponentStatus {\n  @ApiProperty({ description: 'Component name', example: 'processor' })\n  @Expose()\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'Component type',\n    example: 'stream-processor',\n  })\n  @Expose()\n  type: string;\n\n  @ApiProperty({ description: 'Component status', example: 'started' })\n  @Expose()\n  status: string;\n\n  @ApiPropertyOptional({\n    description: 'Component version',\n    example: '0.0.202512301417',\n  })\n  @Expose()\n  version: string;\n\n  @ApiPropertyOptional({ description: 'Component errors', type: [String] })\n  @Expose()\n  errors: string[];\n}\n\nexport class RdiPipelineStatus {\n  @ApiProperty({\n    description: 'Pipeline status',\n    example: 'ready',\n    enum: PipelineStatus,\n  })\n  @Expose()\n  status: PipelineStatus;\n\n  @ApiPropertyOptional({\n    description: 'Pipeline state (used in api/v1 only)',\n    example: 'cdc',\n    enum: PipelineState,\n  })\n  @Expose()\n  state?: PipelineState;\n\n  @ApiPropertyOptional({\n    description: 'Pipeline errors',\n    type: [String],\n  })\n  @Expose()\n  errors?: string[];\n\n  @ApiPropertyOptional({\n    description: 'Components statuses array',\n    type: ComponentStatus,\n    isArray: true,\n  })\n  @Expose()\n  @Type(() => ComponentStatus)\n  components?: ComponentStatus[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/models/rdi.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';\n\nexport class Rdi {\n  @ApiProperty({\n    description: 'RDI id.',\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiPropertyOptional({\n    description: 'Base url of API to connect to (for API type only)',\n    example: 'https://example.com',\n    type: String,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString()\n  url: string;\n\n  @ApiProperty({\n    description: 'A name to associate with RDI',\n    type: String,\n  })\n  @Expose()\n  @IsString()\n  @IsNotEmpty()\n  @MaxLength(500)\n  name: string;\n\n  @ApiPropertyOptional({\n    description: 'RDI or API username',\n    type: String,\n  })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  username?: string;\n\n  @ApiPropertyOptional({\n    description: 'RDI or API password',\n    type: String,\n  })\n  @IsOptional()\n  @Expose()\n  @IsString()\n  password?: string;\n\n  @ApiProperty({\n    description: 'Time of the last connection to RDI.',\n    type: String,\n    format: 'date-time',\n    example: '2021-01-06T12:44:39.000Z',\n  })\n  @Expose()\n  lastConnection?: Date;\n\n  @ApiPropertyOptional({\n    description: 'The version of RDI being used',\n    type: String,\n  })\n  @IsOptional()\n  @Expose()\n  @IsNotEmpty()\n  @IsString()\n  version?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport axios from 'axios';\nimport { sign } from 'jsonwebtoken';\nimport {\n  mockRdi,\n  mockRdiClientMetadata,\n  mockRdiUnauthorizedError,\n} from 'src/__mocks__';\nimport { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory';\nimport { RdiUrl, RdiUrlV2 } from 'src/modules/rdi/constants';\nimport { RdiPipelineUnauthorizedException } from 'src/modules/rdi/exceptions';\nimport { ApiV2RdiClient } from 'src/modules/rdi/client/api/v2/api.v2.rdi.client';\nimport { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\n\ndescribe('RdiClientFactory', () => {\n  let module: TestingModule;\n  let service: RdiClientFactory;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    module = await Test.createTestingModule({\n      providers: [RdiClientFactory],\n    }).compile();\n\n    service = await module.get(RdiClientFactory);\n  });\n\n  describe('createClient', () => {\n    describe('v2 client creation', () => {\n      it('should create v2 client when getInfo succeeds', async () => {\n        const mockedAccessToken = sign(\n          { exp: Math.trunc(Date.now() / 1000) + 3600 },\n          'test',\n        );\n        const mockInfoResponse = { version: '2.0.1' };\n        const mockPipelinesResponse = [\n          {\n            name: 'pipeline-1',\n            active: true,\n            config: {},\n            status: 'running',\n            errors: [],\n            components: [],\n            current: true,\n          },\n        ];\n\n        // Mock getInfo call\n        mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse });\n        // Mock connect call\n        mockedAxios.post.mockResolvedValueOnce({\n          status: 200,\n          data: {\n            access_token: mockedAccessToken,\n          },\n        });\n        // Mock selectPipeline call\n        mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse });\n\n        const client = await service.createClient(\n          mockRdiClientMetadata,\n          mockRdi,\n        );\n\n        expect(client).toBeInstanceOf(ApiV2RdiClient);\n        expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo);\n        expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, {\n          password: mockRdi.password,\n          username: mockRdi.username,\n        });\n        expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetPipelines);\n      });\n\n      it('should call connect and selectPipeline for v2 client', async () => {\n        const mockedAccessToken = sign(\n          { exp: Math.trunc(Date.now() / 1000) + 3600 },\n          'test',\n        );\n        const mockInfoResponse = { version: '2.1.0' };\n        const mockPipelinesResponse = [\n          {\n            name: 'default',\n            active: true,\n            config: {},\n            status: 'running',\n            errors: [],\n            components: [],\n            current: true,\n          },\n        ];\n\n        mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse });\n        mockedAxios.post.mockResolvedValueOnce({\n          status: 200,\n          data: { access_token: mockedAccessToken },\n        });\n        mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse });\n\n        const client = await service.createClient(\n          mockRdiClientMetadata,\n          mockRdi,\n        );\n\n        expect(client).toBeInstanceOf(ApiV2RdiClient);\n        expect(client['selectedPipeline']).toBe('default');\n      });\n    });\n\n    describe('v1 client fallback', () => {\n      it('should fallback to v1 client when getInfo fails', async () => {\n        const mockedAccessToken = sign(\n          { exp: Math.trunc(Date.now() / 1000) + 3600 },\n          'test',\n        );\n\n        // Mock getInfo failure (v2 not available)\n        mockedAxios.get.mockRejectedValueOnce(new Error('Not found'));\n        // Mock v1 connect call\n        mockedAxios.post.mockResolvedValueOnce({\n          status: 200,\n          data: {\n            access_token: mockedAccessToken,\n          },\n        });\n\n        const client = await service.createClient(\n          mockRdiClientMetadata,\n          mockRdi,\n        );\n\n        expect(client).toBeInstanceOf(ApiRdiClient);\n        expect(client).not.toBeInstanceOf(ApiV2RdiClient);\n        expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, {\n          password: mockRdi.password,\n          username: mockRdi.username,\n        });\n      });\n\n      it('should fallback to v1 client when getInfo returns null', async () => {\n        const mockedAccessToken = sign(\n          { exp: Math.trunc(Date.now() / 1000) + 3600 },\n          'test',\n        );\n\n        // Mock getInfo returning null (endpoint exists but returns null)\n        mockedAxios.get.mockResolvedValueOnce({ data: null });\n        // Mock v1 connect call\n        mockedAxios.post.mockResolvedValueOnce({\n          status: 200,\n          data: {\n            access_token: mockedAccessToken,\n          },\n        });\n\n        const client = await service.createClient(\n          mockRdiClientMetadata,\n          mockRdi,\n        );\n\n        expect(client).toBeInstanceOf(ApiRdiClient);\n        expect(client).not.toBeInstanceOf(ApiV2RdiClient);\n      });\n\n      it('should not create client if v1 auth request fails', async () => {\n        // Mock getInfo failure (v2 not available)\n        mockedAxios.get.mockRejectedValueOnce(new Error('Not found'));\n        // Mock v1 auth failure\n        mockedAxios.post.mockRejectedValueOnce(mockRdiUnauthorizedError);\n\n        await expect(\n          service.createClient(mockRdiClientMetadata, mockRdi),\n        ).rejects.toThrow(RdiPipelineUnauthorizedException);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { RdiClient } from 'src/modules/rdi/client/rdi.client';\nimport { Rdi, RdiClientMetadata } from 'src/modules/rdi/models';\nimport { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client';\nimport { ApiV2RdiClient } from 'src/modules/rdi/client/api/v2/api.v2.rdi.client';\n\n@Injectable()\nexport class RdiClientFactory {\n  private readonly logger = new Logger('RdiClientFactory');\n\n  async createClient(\n    clientMetadata: RdiClientMetadata,\n    rdi: Rdi,\n  ): Promise<RdiClient> {\n    try {\n      const rdiClientV2 = new ApiV2RdiClient(clientMetadata, rdi);\n      // Probe v2 API endpoint to detect version\n      const info = await rdiClientV2.getInfo();\n\n      // todo: properly verify version from info to determine which client to use\n      if (info) {\n        await rdiClientV2.connect();\n        await rdiClientV2.selectPipeline();\n        return rdiClientV2;\n      }\n    } catch (error) {\n      // v2 info endpoint is not available, falling back to v1 client\n      this.logger.log('RDI API v2 not detected, falling back to v1 client');\n    }\n\n    const rdiClient = new ApiRdiClient(clientMetadata, rdi);\n    await rdiClient.connect();\n    return rdiClient;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/providers/rdi.client.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { RdiRepository } from 'src/modules/rdi/repository/rdi.repository';\nimport { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage';\nimport { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory';\nimport { NotFoundException } from '@nestjs/common';\nimport { RdiClientMetadata } from 'src/modules/rdi/models';\nimport { RdiClient } from 'src/modules/rdi/client/rdi.client';\nimport {\n  MockType,\n  generateMockRdiClient,\n  mockRdi,\n  mockRdiClientFactory,\n  mockRdiClientStorage,\n  mockRdiRepository,\n} from 'src/__mocks__';\nimport { RdiClientProvider } from './rdi.client.provider';\n\ndescribe('RdiClientProvider', () => {\n  let provider: RdiClientProvider;\n  let repository: MockType<RdiRepository>;\n  let rdiClientStorage: MockType<RdiClientStorage>;\n  let rdiClientFactory: MockType<RdiClientFactory>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RdiClientProvider,\n        {\n          provide: RdiRepository,\n          useFactory: mockRdiRepository,\n        },\n        {\n          provide: RdiClientStorage,\n          useFactory: mockRdiClientStorage,\n        },\n        {\n          provide: RdiClientFactory,\n          useFactory: mockRdiClientFactory,\n        },\n      ],\n    }).compile();\n\n    provider = module.get(RdiClientProvider);\n    repository = module.get(RdiRepository);\n    rdiClientStorage = module.get(RdiClientStorage);\n    rdiClientFactory = module.get(RdiClientFactory);\n  });\n\n  describe('getOrCreate', () => {\n    it('should return existing client if found', async () => {\n      const metadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      const client: RdiClient = generateMockRdiClient(metadata);\n      rdiClientStorage.getByMetadata.mockResolvedValue(client);\n      repository.update.mockResolvedValue(mockRdi);\n\n      const result = await provider.getOrCreate(metadata);\n\n      expect(rdiClientStorage.getByMetadata).toHaveBeenCalledWith(metadata);\n      expect(client.ensureAuth).toHaveBeenCalled();\n      expect(repository.update).toHaveBeenCalled();\n      expect(result).toEqual(client);\n    });\n\n    it('should create and return new client if not found', async () => {\n      const metadata: RdiClientMetadata = {\n        id: '124',\n        sessionMetadata: undefined,\n      };\n      const client: RdiClient = generateMockRdiClient(metadata);\n      repository.get.mockResolvedValue(mockRdi);\n      rdiClientFactory.createClient.mockResolvedValue(client);\n      rdiClientStorage.set.mockResolvedValue(client);\n\n      const result = await provider.getOrCreate(metadata);\n\n      expect(repository.get).toHaveBeenCalledWith(metadata.id);\n      expect(rdiClientFactory.createClient).toHaveBeenCalledWith(\n        metadata,\n        mockRdi,\n      );\n      expect(rdiClientStorage.set).toHaveBeenCalledWith(client);\n      expect(result).toEqual(client);\n    });\n  });\n\n  describe('create', () => {\n    it('should throw NotFoundException if RDI not found', async () => {\n      const metadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      repository.get.mockResolvedValue(null);\n\n      await expect(provider.create(metadata)).rejects.toThrowError(\n        NotFoundException,\n      );\n      expect(repository.get).toHaveBeenCalledWith(metadata.id);\n    });\n\n    it('should create and return new client if RDI found', async () => {\n      const metadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      const client: RdiClient = generateMockRdiClient(metadata);\n      repository.get.mockResolvedValue(mockRdi);\n      rdiClientFactory.createClient.mockResolvedValue(client);\n      repository.update.mockResolvedValue(mockRdi);\n\n      const result = await provider.create(metadata);\n\n      expect(repository.get).toHaveBeenCalledWith(metadata.id);\n      expect(rdiClientFactory.createClient).toHaveBeenCalledWith(\n        metadata,\n        mockRdi,\n      );\n      expect(repository.update).toHaveBeenCalled();\n      expect(result).toEqual(client);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete client by metadata id', async () => {\n      const metadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      rdiClientStorage.delete.mockResolvedValue(1);\n\n      const result = await provider.delete(metadata);\n\n      expect(rdiClientStorage.delete).toHaveBeenCalledWith(metadata.id);\n      expect(result).toEqual(1);\n    });\n  });\n\n  describe('deleteById', () => {\n    it('should delete client by id', async () => {\n      const id = '123';\n      rdiClientStorage.delete.mockResolvedValue(1);\n\n      const result = await provider.deleteById(id);\n\n      expect(rdiClientStorage.delete).toHaveBeenCalledWith(id);\n      expect(result).toEqual(1);\n    });\n  });\n\n  describe('deleteManyByRdiId', () => {\n    it('should delete clients by RDI id', async () => {\n      const id = '123';\n      rdiClientStorage.deleteManyByRdiId.mockResolvedValue(2);\n\n      const result = await provider.deleteManyByRdiId(id);\n\n      expect(rdiClientStorage.deleteManyByRdiId).toHaveBeenCalledWith(id);\n      expect(result).toEqual(2);\n    });\n  });\n\n  describe('updateLastConnection', () => {\n    it('should update rdi lastConnection', async () => {\n      const metadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      repository.update.mockResolvedValue(mockRdi);\n\n      await provider['updateLastConnection'](metadata);\n\n      expect(repository.update).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/providers/rdi.client.provider.ts",
    "content": "import { RdiClient } from 'src/modules/rdi/client/rdi.client';\nimport { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { RdiClientMetadata } from 'src/modules/rdi/models';\nimport { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage';\nimport { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory';\nimport { RdiRepository } from 'src/modules/rdi/repository/rdi.repository';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\n@Injectable()\nexport class RdiClientProvider {\n  private logger: Logger = new Logger('RdiClientProvider');\n\n  constructor(\n    private readonly repository: RdiRepository,\n    private readonly rdiClientStorage: RdiClientStorage,\n    private readonly rdiClientFactory: RdiClientFactory,\n  ) {}\n\n  async getOrCreate(rdiClientMetadata: RdiClientMetadata): Promise<RdiClient> {\n    let client = await this.rdiClientStorage.getByMetadata(rdiClientMetadata);\n    if (client) {\n      await client.ensureAuth();\n      this.updateLastConnection(rdiClientMetadata);\n      return client;\n    }\n\n    client = await this.create(rdiClientMetadata);\n\n    return this.rdiClientStorage.set(client);\n  }\n\n  async create(clientMetadata: RdiClientMetadata): Promise<RdiClient> {\n    const rdi = await this.repository.get(clientMetadata.id);\n\n    if (!rdi) {\n      this.logger.error(\n        `RDI with ${clientMetadata.id} was not Found`,\n        clientMetadata,\n      );\n      throw new NotFoundException(ERROR_MESSAGES.INVALID_RDI_INSTANCE_ID);\n    }\n    const rdiClient = await this.rdiClientFactory.createClient(\n      clientMetadata,\n      rdi,\n    );\n    if (rdiClient) {\n      this.updateLastConnection(clientMetadata);\n    }\n    return rdiClient;\n  }\n\n  async delete(rdiClientMetadata: RdiClientMetadata): Promise<number> {\n    return this.rdiClientStorage.delete(rdiClientMetadata.id);\n  }\n\n  async deleteById(id: string): Promise<number> {\n    return this.rdiClientStorage.delete(id);\n  }\n\n  async deleteManyByRdiId(id: string): Promise<number> {\n    return this.rdiClientStorage.deleteManyByRdiId(id);\n  }\n\n  private async updateLastConnection(\n    rdiClientMetadata: RdiClientMetadata,\n  ): Promise<void> {\n    try {\n      await this.repository.update(rdiClientMetadata.id, {\n        lastConnection: new Date(),\n      });\n    } catch (e) {\n      // ignore the error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { BadRequestException } from '@nestjs/common';\nimport { generateMockRdiClient } from 'src/__mocks__';\nimport { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage';\nimport { IDLE_THRESHOLD } from 'src/modules/rdi/constants';\nimport { SessionMetadata } from 'src/common/models';\n\nconst mockClientMetadata1 = {\n  sessionMetadata: {\n    userId: 'u1',\n    accountId: 'a1',\n    sessionId: 's1',\n  },\n  id: 'id1',\n};\n\nconst mockNotExistClientMetadata = {\n  sessionMetadata: {\n    userId: 'not exist',\n    accountId: 'not exist',\n    sessionId: 'not exist',\n  },\n  id: 'not exist',\n};\n\nconst mockRdiClient1 = generateMockRdiClient(mockClientMetadata1);\nconst mockRdiClient2 = generateMockRdiClient({\n  ...mockClientMetadata1,\n  sessionMetadata: { userId: 'u2', sessionId: 's1', accountId: 'a2' },\n});\nconst mockRdiClient3 = generateMockRdiClient({\n  ...mockClientMetadata1,\n  sessionMetadata: { userId: 'u2', sessionId: 's3', accountId: 'a2' },\n});\nconst mockRdiClient4 = generateMockRdiClient({\n  ...mockClientMetadata1,\n  id: 'id2',\n});\nconst mockRdiClient5 = generateMockRdiClient({\n  ...mockClientMetadata1,\n  sessionMetadata: { userId: 'u2', sessionId: 's3', accountId: 'a3' },\n});\n\ndescribe('RdiClientStorage', () => {\n  let service: RdiClientStorage;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [RdiClientStorage],\n    }).compile();\n\n    service = await module.get(RdiClientStorage);\n\n    service['clients'].set(mockRdiClient1.id, mockRdiClient1);\n    service['clients'].set(mockRdiClient2.id, mockRdiClient2);\n    service['clients'].set(mockRdiClient3.id, mockRdiClient3);\n    service['clients'].set(mockRdiClient4.id, mockRdiClient4);\n    service['clients'].set(mockRdiClient5.id, mockRdiClient5);\n  });\n\n  afterEach(() => {\n    service.onModuleDestroy();\n  });\n\n  describe('syncClients', () => {\n    it('should not remove any client since no idle time passed', async () => {\n      expect(service['clients'].size).toEqual(5);\n\n      service['syncClients']();\n\n      expect(service['clients'].size).toEqual(5);\n    });\n\n    it('should remove client with exceeded time in idle', async () => {\n      expect(service['clients'].size).toEqual(5);\n      const toDelete = service['clients'].get(mockRdiClient1.id);\n      toDelete['lastUsed'] = Date.now() - IDLE_THRESHOLD - 1;\n      service['syncClients']();\n\n      expect(service['clients'].size).toEqual(4);\n      expect(service['clients'].get(mockRdiClient1.id)).toEqual(undefined);\n    });\n\n    describe('get', () => {\n      it('should correctly get client instance and update last used time', async () => {\n        // eslint-disable-next-line prefer-destructuring\n        const lastUsed = mockRdiClient1['lastUsed'];\n\n        const result = await service.get(mockRdiClient1.id);\n\n        expect(result).toEqual(service['clients'].get(mockRdiClient1.id));\n        expect(result['lastUsed']).toBeGreaterThan(lastUsed);\n      });\n      it('should not fail when there is no client', async () => {\n        const result = await service.get('not-existing');\n\n        expect(result).toBeUndefined();\n      });\n    });\n\n    describe('getByMetadata', () => {\n      it('should correctly get client instance and update last used time', async () => {\n        // eslint-disable-next-line prefer-destructuring\n        const lastUsed = mockRdiClient1['lastUsed'];\n\n        const result = await service.getByMetadata(mockClientMetadata1);\n\n        expect(result).toEqual(service['clients'].get(mockRdiClient1.id));\n        expect(result['lastUsed']).toBeGreaterThan(lastUsed);\n      });\n\n      it('should not fail when there is no client', async () => {\n        const result = await service.getByMetadata(mockNotExistClientMetadata);\n\n        expect(result).toBeUndefined();\n      });\n\n      it('should throw BadRequestException when metadata is invalid', async () => {\n        await expect(\n          service.getByMetadata({\n            ...mockNotExistClientMetadata,\n            id: undefined,\n          }),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n\n        await expect(\n          service.getByMetadata({\n            ...mockNotExistClientMetadata,\n            sessionMetadata: {\n              ...mockNotExistClientMetadata.sessionMetadata,\n              sessionId: undefined,\n            },\n          }),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n\n        await expect(\n          service.getByMetadata({\n            ...mockNotExistClientMetadata,\n            sessionMetadata: {\n              ...mockNotExistClientMetadata.sessionMetadata,\n              userId: undefined,\n            },\n          }),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n      });\n    });\n\n    describe('set', () => {\n      beforeEach(() => {\n        // @ts-ignore\n        service['clients'] = new Map();\n      });\n\n      it('should add new client', async () => {\n        expect(service['clients'].size).toEqual(0);\n        const result = await service.set(mockRdiClient1);\n\n        expect(result).toEqual(mockRdiClient1);\n        expect(service['clients'].size).toEqual(1);\n        expect(await service.get(mockRdiClient1.id)).toEqual(mockRdiClient1);\n      });\n\n      it('should replace new client with existing', async () => {\n        const existingClient = generateMockRdiClient(mockClientMetadata1);\n\n        expect(service['clients'].size).toEqual(0);\n        expect(await service.set(existingClient)).toEqual(existingClient);\n        expect(service['clients'].size).toEqual(1);\n\n        const newClient = generateMockRdiClient(mockClientMetadata1);\n        const result = await service.set(newClient);\n        expect(result).not.toEqual(existingClient);\n        expect(result).toEqual(newClient);\n        expect(service['clients'].size).toEqual(1);\n      });\n\n      it('should throw BadRequestException when metadata is invalid', async () => {\n        await expect(\n          service.set(\n            generateMockRdiClient({\n              sessionMetadata: {} as SessionMetadata,\n              id: 'id',\n            }),\n          ),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n\n        await expect(\n          service.set(\n            generateMockRdiClient({\n              sessionMetadata: {\n                userId: 'u2',\n                sessionId: 's1',\n              } as SessionMetadata,\n              id: undefined,\n            }),\n          ),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n\n        await expect(\n          service.set(\n            generateMockRdiClient({\n              sessionMetadata: {\n                userId: 'u2',\n                sessionId: undefined,\n              } as SessionMetadata,\n              id: 'id',\n            }),\n          ),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n\n        await expect(\n          service.set(\n            generateMockRdiClient({\n              sessionMetadata: {\n                userId: undefined,\n                sessionId: 's2',\n              } as SessionMetadata,\n              id: 'id',\n            }),\n          ),\n        ).rejects.toThrow(\n          new BadRequestException('Client metadata missed required properties'),\n        );\n      });\n    });\n\n    describe('delete', () => {\n      it('should remove only one', async () => {\n        expect(service['clients'].size).toEqual(5);\n        const result = await service.delete(mockRdiClient1.id);\n\n        expect(result).toEqual(1);\n        expect(service['clients'].size).toEqual(4);\n        expect(service['clients'].get(mockRdiClient1.id)).toEqual(undefined);\n      });\n      it('should not fail in case when no client found', async () => {\n        const result = await service.delete('not-existing');\n\n        expect(result).toEqual(0);\n        expect(service['clients'].size).toEqual(5);\n      });\n    });\n\n    describe('findClients + deleteManyByRdiId', () => {\n      it('should correctly find clients for particular rdi instance', async () => {\n        const result = service['findClientsById'](mockClientMetadata1.id);\n\n        expect(result.length).toEqual(4);\n        result.forEach((id) => {\n          expect(service['clients'].get(id)['metadata'].id).toEqual(\n            mockClientMetadata1.id,\n          );\n        });\n\n        expect(await service.deleteManyByRdiId(mockClientMetadata1.id)).toEqual(\n          4,\n        );\n        expect(service['clients'].size).toEqual(1);\n      });\n\n      it('should not find any instances', async () => {\n        const result = service['findClientsById']('not existing');\n\n        expect(result).toEqual([]);\n\n        expect(await service.deleteManyByRdiId('not existing')).toEqual(0);\n        expect(service['clients'].size).toEqual(5);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts",
    "content": "import { RdiClient } from 'src/modules/rdi/client/rdi.client';\nimport { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { RdiClientMetadata } from 'src/modules/rdi/models';\nimport { sum } from 'lodash';\nimport { RDI_SYNC_INTERVAL } from 'src/modules/rdi/constants';\n\n@Injectable()\nexport class RdiClientStorage {\n  private readonly logger = new Logger('RdiClientStorage');\n\n  private readonly clients: Map<string, RdiClient> = new Map();\n\n  private readonly syncInterval: NodeJS.Timeout;\n\n  constructor() {\n    this.syncInterval = setInterval(\n      this.syncClients.bind(this),\n      RDI_SYNC_INTERVAL,\n    );\n  }\n\n  onModuleDestroy() {\n    clearInterval(this.syncInterval);\n  }\n\n  /**\n   * Removes all clients with exceeded idle threshold\n   * @private\n   */\n  private syncClients(): void {\n    this.clients.forEach((client) => {\n      if (client.isIdle()) {\n        this.clients.delete(client.id);\n      }\n    });\n  }\n\n  async get(id: string): Promise<RdiClient> {\n    const client = this.clients.get(id);\n    if (client) {\n      client.setLastUsed();\n    }\n\n    return client;\n  }\n\n  async getByMetadata(\n    rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiClient> {\n    if (\n      !rdiClientMetadata.id ||\n      !rdiClientMetadata.sessionMetadata?.sessionId ||\n      !rdiClientMetadata.sessionMetadata?.accountId ||\n      !rdiClientMetadata.sessionMetadata.userId\n    ) {\n      throw new BadRequestException(\n        'Client metadata missed required properties',\n      );\n    }\n    return this.get(RdiClient.generateId(rdiClientMetadata));\n  }\n\n  async delete(id: string): Promise<number> {\n    const client = this.clients.get(id);\n\n    if (client) {\n      this.clients.delete(id);\n      return 1;\n    }\n\n    return 0;\n  }\n\n  /**\n   * Finds clients by rdi instance id and returns array of ids\n   * @param id\n   * @private\n   */\n  private findClientsById(id: string): string[] {\n    return [...this.clients.values()]\n      .filter((rdiClient) => rdiClient.metadata.id === id)\n      .map((rdiClient) => rdiClient.id);\n  }\n\n  async deleteManyByRdiId(id: string): Promise<number> {\n    const toRemove = this.findClientsById(id);\n\n    this.logger.debug(`Trying to remove ${toRemove.length} clients`);\n\n    return sum(await Promise.all(toRemove.map(this.delete.bind(this))));\n  }\n\n  /**\n   * Saves client into the clients pool\n   * When client with such \"id\" exists:\n   * Will replace the current client with a new one\n   * @param client\n   */\n  async set(client: RdiClient): Promise<RdiClient> {\n    // Additional validation\n    if (\n      !client.id ||\n      !client.metadata.sessionMetadata?.sessionId ||\n      !client.metadata.sessionMetadata.userId ||\n      !client.metadata.sessionMetadata.accountId ||\n      !client.metadata.id\n    ) {\n      throw new BadRequestException(\n        'Client metadata missed required properties',\n      );\n    }\n    this.clients.set(client.id, client);\n    return client;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-pipeline.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { TelemetryEvents } from 'src/constants';\nimport { RdiPipelineAnalytics } from 'src/modules/rdi/rdi-pipeline.analytics';\nimport { mockSessionMetadata } from 'src/__mocks__';\n\ndescribe('RdiPipelineAnalytics', () => {\n  let service: RdiPipelineAnalytics;\n  let sendEventMethod;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, RdiPipelineAnalytics],\n    }).compile();\n\n    service = await module.get(RdiPipelineAnalytics);\n    sendEventMethod = jest.spyOn<RdiPipelineAnalytics, any>(\n      service,\n      'sendEvent',\n    );\n    sendFailedEventMethod = jest.spyOn<RdiPipelineAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendRdiInstanceDeleted', () => {\n    it('should emit event when rdi pipeline deployed successfully', () => {\n      service.sendRdiPipelineDeployed(mockSessionMetadata, 'id');\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RdiPipelineDeploymentSucceeded,\n        {\n          id: 'id',\n        },\n      );\n    });\n\n    it('should emit event when rdi pipeline is not deployed successfully', () => {\n      service.sendRdiPipelineDeployFailed(\n        mockSessionMetadata,\n        httpException,\n        'id',\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RdiPipelineDeploymentFailed,\n        httpException,\n        { id: 'id' },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-pipeline.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class RdiPipelineAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendRdiPipelineDeployed(sessionMetadata: SessionMetadata, id: string) {\n    this.sendEvent(\n      sessionMetadata,\n      TelemetryEvents.RdiPipelineDeploymentSucceeded,\n      { id },\n    );\n  }\n\n  sendRdiPipelineDeployFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    id: string,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RdiPipelineDeploymentFailed,\n      exception,\n      { id },\n    );\n  }\n\n  sendRdiPipelineFetched(\n    sessionMetadata: SessionMetadata,\n    id: string,\n    pipeline: any,\n  ) {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.RdiPipelineDeploymentSucceeded,\n        {\n          id,\n          jobsNumber: pipeline?.jobs ? Object.keys(pipeline.jobs).length : 0,\n          source: 'server',\n        },\n      );\n    } catch (e) {\n      // ignore\n    }\n  }\n\n  sendRdiPipelineFetchFailed(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n    id: string,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RdiPipelineDeploymentFailed,\n      exception,\n      { id, source: 'server' },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n  Param,\n} from '@nestjs/common';\nimport {\n  RdiPipeline,\n  RdiClientMetadata,\n  RdiPipelineStatus,\n} from 'src/modules/rdi/models';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { RdiPipelineService } from 'src/modules/rdi/rdi-pipeline.service';\nimport { RequestRdiClientMetadata } from 'src/modules/rdi/decorators';\nimport {\n  RdiDryRunJobDto,\n  RdiTemplateResponseDto,\n  RdiTestConnectionsResponseDto,\n} from 'src/modules/rdi/dto';\nimport { RdiDryRunJobResponseDto } from 'src/modules/rdi/dto/rdi.dry-run.job.response.dto';\n\n@ApiTags('RDI')\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('rdi/:id/pipeline')\nexport class RdiPipelineController {\n  constructor(private readonly rdiPipelineService: RdiPipelineService) {}\n\n  @Get('/schema')\n  @ApiEndpoint({\n    description: 'Get pipeline schema',\n    responses: [{ status: 200, type: Object }],\n  })\n  async getSchema(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<object> {\n    return this.rdiPipelineService.getSchema(rdiClientMetadata);\n  }\n\n  @Get('/')\n  @ApiEndpoint({\n    description: 'Get pipeline',\n    responses: [{ status: 200, type: RdiPipeline }],\n  })\n  async getPipeline(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiPipeline> {\n    return this.rdiPipelineService.getPipeline(rdiClientMetadata);\n  }\n\n  @Post('/dry-run-job')\n  @ApiEndpoint({\n    description: 'Dry run job',\n    responses: [{ status: 200, type: RdiDryRunJobResponseDto }],\n  })\n  async dryRunJob(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n    @Body() dto: RdiDryRunJobDto,\n  ): Promise<RdiDryRunJobResponseDto> {\n    return this.rdiPipelineService.dryRunJob(rdiClientMetadata, dto);\n  }\n\n  @Post('/deploy')\n  @ApiEndpoint({\n    description: 'Deploy the pipeline',\n    responses: [{ status: 200 }],\n  })\n  async deploy(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n    @Body() dto: RdiPipeline,\n  ): Promise<void> {\n    return this.rdiPipelineService.deploy(rdiClientMetadata, dto);\n  }\n\n  @Post('/stop')\n  @ApiEndpoint({\n    description: 'Stops running pipeline',\n    responses: [{ status: 200 }],\n  })\n  async stopPipeline(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<void> {\n    return this.rdiPipelineService.stopPipeline(rdiClientMetadata);\n  }\n\n  @Post('/start')\n  @ApiEndpoint({\n    description: 'Starts the stopped pipeline',\n    responses: [{ status: 200 }],\n  })\n  async startPipeline(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<void> {\n    return this.rdiPipelineService.startPipeline(rdiClientMetadata);\n  }\n\n  @Post('/reset')\n  @ApiEndpoint({\n    description: 'Resets default pipeline',\n    responses: [{ status: 200 }],\n  })\n  async resetPipeline(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<void> {\n    return this.rdiPipelineService.resetPipeline(rdiClientMetadata);\n  }\n\n  @Post('/test-connections')\n  @ApiEndpoint({\n    description: 'Test target connections',\n    responses: [{ status: 200, type: RdiTestConnectionsResponseDto }],\n  })\n  async testConnections(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n    @Body() config: object,\n  ): Promise<RdiTestConnectionsResponseDto> {\n    return this.rdiPipelineService.testConnections(rdiClientMetadata, config);\n  }\n\n  @Get('/strategies')\n  @ApiEndpoint({\n    description: 'Get pipeline strategies and db types for template',\n    responses: [{ status: 200, type: Object }],\n  })\n  async getStrategies(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<object> {\n    return this.rdiPipelineService.getStrategies(rdiClientMetadata);\n  }\n\n  @Get('/job/template/:pipelineType')\n  @ApiEndpoint({\n    description: 'Get job template for selected pipeline type',\n    responses: [{ status: 200, type: RdiTemplateResponseDto }],\n  })\n  async getJobTemplate(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n    @Param('pipelineType') pipelineType: string,\n  ): Promise<RdiTemplateResponseDto> {\n    return this.rdiPipelineService.getJobTemplate(\n      rdiClientMetadata,\n      pipelineType,\n    );\n  }\n\n  @Get('/config/template/:pipelineType/:dbType')\n  @ApiEndpoint({\n    description: 'Get config template for selected pipeline and db types',\n    responses: [{ status: 200, type: RdiTemplateResponseDto }],\n  })\n  async getConfigTemplate(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n    @Param('pipelineType') pipelineType: string,\n    @Param('dbType') dbType: string,\n  ): Promise<RdiTemplateResponseDto> {\n    return this.rdiPipelineService.getConfigTemplate(\n      rdiClientMetadata,\n      pipelineType,\n      dbType,\n    );\n  }\n\n  @Get('/status')\n  @ApiEndpoint({\n    description: 'Get pipeline status',\n  })\n  async getPipelineStatus(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiPipelineStatus> {\n    return this.rdiPipelineService.getPipelineStatus(rdiClientMetadata);\n  }\n\n  @Get('/job-functions')\n  @ApiEndpoint({\n    description: 'Get job functions',\n    responses: [{ status: 200 }],\n  })\n  async getJobFunctions(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<object> {\n    return this.rdiPipelineService.getJobFunctions(rdiClientMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-pipeline.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\nimport { RdiPipelineAnalytics } from 'src/modules/rdi/rdi-pipeline.analytics';\nimport { wrapHttpError } from 'src/common/utils';\nimport {\n  MockType,\n  generateMockRdiClient,\n  mockRdiClientProvider,\n  mockRdiDryRunJob,\n  mockRdiPipelineAnalytics,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { RdiPipelineService } from './rdi-pipeline.service';\nimport { RdiDryRunJobDto } from './dto';\nimport { RdiDyRunJobStatus, RdiPipeline } from './models';\n\ndescribe('RdiPipelineService', () => {\n  let service: RdiPipelineService;\n  let rdiClientProvider: MockType<RdiClientProvider>;\n  let analytics: MockType<RdiPipelineAnalytics>;\n  const rdiClientMetadata = { id: '123', sessionMetadata: mockSessionMetadata };\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RdiPipelineService,\n        {\n          provide: RdiClientProvider,\n          useFactory: mockRdiClientProvider,\n        },\n        {\n          provide: RdiPipelineAnalytics,\n          useFactory: mockRdiPipelineAnalytics,\n        },\n      ],\n    }).compile();\n\n    service = module.get(RdiPipelineService);\n    rdiClientProvider = module.get(RdiClientProvider);\n    analytics = module.get(RdiPipelineAnalytics);\n  });\n\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  describe('getSchema', () => {\n    it('should call getSchema on the RdiClientProvider and return the result', async () => {\n      const schema = { schema: {} };\n      const rdiClient = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValueOnce(rdiClient);\n      rdiClient.getSchema.mockResolvedValueOnce(schema);\n\n      const result = await service.getSchema(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n      expect(rdiClient.getSchema).toHaveBeenCalled();\n      expect(result).toEqual(schema);\n    });\n  });\n\n  describe('getPipeline', () => {\n    it('should call getPipeline on the RdiClientProvider and return the result', async () => {\n      const pipeline = { pipeline: {} };\n      const rdiClient = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValueOnce(rdiClient);\n      rdiClient.getPipeline.mockResolvedValueOnce(pipeline);\n\n      const result = await service.getPipeline(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n      expect(result).toEqual(pipeline);\n    });\n\n    it('should call sendRdiPipelineFetched on the RdiPipelineAnalytics if successful', async () => {\n      const pipeline = { pipeline: {} };\n      const rdiClient = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValueOnce(rdiClient);\n      rdiClient.getPipeline.mockResolvedValueOnce(pipeline);\n\n      await service.getPipeline(rdiClientMetadata);\n\n      expect(analytics.sendRdiPipelineFetched).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        rdiClientMetadata.id,\n        pipeline,\n      );\n    });\n\n    it('should call sendRdiPipelineFetchFailed on the RdiPipelineAnalytics and throw an error if unsuccessful', async () => {\n      const error = new Error('Failed to get pipeline');\n      rdiClientProvider.getOrCreate.mockRejectedValue(error);\n\n      await expect(service.getPipeline(rdiClientMetadata)).rejects.toThrow(\n        wrapHttpError(error),\n      );\n      expect(analytics.sendRdiPipelineFetchFailed).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        error,\n        rdiClientMetadata.id,\n      );\n    });\n  });\n\n  describe('dryRunJob', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.dryRunJob(rdiClientMetadata, mockRdiDryRunJob);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call dryRunJob on the client with the correct dto', async () => {\n      const dto = Object.assign(new RdiDryRunJobDto(), {\n        input_data: {\n          some: 'value',\n        },\n        job: { name: 'job1' },\n      });\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.dryRunJob(rdiClientMetadata, dto);\n\n      expect(client.dryRunJob).toHaveBeenCalledWith(dto);\n    });\n\n    it('should return the result of dryRunJob on the client', async () => {\n      const rdiClient = generateMockRdiClient(rdiClientMetadata);\n      const dto = Object.assign(new RdiDryRunJobDto(), {\n        input_data: {\n          some: 'value',\n        },\n        job: { name: 'job1' },\n      });\n      const response = {\n        transformations: {\n          status: RdiDyRunJobStatus.Success,\n        },\n        commands: {\n          status: RdiDyRunJobStatus.Success,\n        },\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(rdiClient);\n      rdiClient.dryRunJob.mockResolvedValueOnce(response);\n\n      const result = await service.dryRunJob(rdiClientMetadata, dto);\n\n      expect(result).toBe(response);\n      expect(rdiClient.dryRunJob).toHaveBeenCalledWith(dto);\n    });\n  });\n\n  describe('deploy', () => {\n    const dto = Object.assign(new RdiPipeline(), {\n      jobs: { job1: {} },\n      config: {},\n    });\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = {\n        deploy: jest.fn(),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.deploy(rdiClientMetadata, dto);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n      expect(client.deploy).toHaveBeenCalledWith(dto);\n    });\n\n    it('should call deploy on the client with the correct dto', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.deploy(rdiClientMetadata, dto);\n\n      expect(client.deploy).toHaveBeenCalledWith(dto);\n    });\n\n    it('should call sendRdiPipelineDeployed on analytics if deploy succeeds', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.deploy(rdiClientMetadata, dto);\n\n      expect(analytics.sendRdiPipelineDeployed).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        rdiClientMetadata.id,\n      );\n    });\n\n    it('should call sendRdiPipelineDeployFailed on analytics if deploy fails', async () => {\n      const error = new Error('Deploy failed');\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.deploy.mockRejectedValueOnce(error);\n      try {\n        await service.deploy(rdiClientMetadata, dto);\n      } catch (e) {\n        expect(analytics.sendRdiPipelineDeployFailed).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          error,\n          rdiClientMetadata.id,\n        );\n      }\n    });\n\n    it('should throw an error if deploy fails', async () => {\n      const error = new Error('Deploy failed');\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.deploy.mockRejectedValueOnce(error);\n\n      await expect(service.deploy(rdiClientMetadata, dto)).rejects.toThrow(\n        error,\n      );\n    });\n  });\n\n  describe('startPipeline', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = {\n        startPipeline: jest.fn(),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.startPipeline(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n      expect(client.startPipeline).toHaveBeenCalled();\n    });\n\n    it('should throw an error if startPipeline fails', async () => {\n      const error = new Error('Start Pipeline failed');\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.startPipeline.mockRejectedValueOnce(error);\n\n      await expect(service.startPipeline(rdiClientMetadata)).rejects.toThrow(\n        error,\n      );\n    });\n  });\n\n  describe('stopPipeline', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = {\n        stopPipeline: jest.fn(),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.stopPipeline(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n      expect(client.stopPipeline).toHaveBeenCalled();\n    });\n\n    it('should throw an error if stopPipeline fails', async () => {\n      const error = new Error('Stop Pipeline failed');\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.stopPipeline.mockRejectedValueOnce(error);\n\n      await expect(service.stopPipeline(rdiClientMetadata)).rejects.toThrow(\n        error,\n      );\n    });\n  });\n\n  describe('resetPipeline', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = {\n        resetPipeline: jest.fn(),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.resetPipeline(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n      expect(client.resetPipeline).toHaveBeenCalled();\n    });\n\n    it('should throw an error if resetPipeline fails', async () => {\n      const error = new Error('Stop Pipeline failed');\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.resetPipeline.mockRejectedValueOnce(error);\n\n      await expect(service.resetPipeline(rdiClientMetadata)).rejects.toThrow(\n        error,\n      );\n    });\n  });\n\n  describe('testConnections', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const config = { data: 'some data' };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.testConnections(rdiClientMetadata, config);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call testConnections on the client with the correct config', async () => {\n      const config = { data: 'some data' };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.testConnections(rdiClientMetadata, config);\n\n      expect(client.testConnections).toHaveBeenCalledWith(config);\n    });\n\n    it('should return the result of testConnections on the client', async () => {\n      const config = { data: 'some data' };\n      const response = { connected: true };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.testConnections.mockResolvedValueOnce(response);\n\n      const result = await service.testConnections(rdiClientMetadata, config);\n\n      expect(result).toBe(response);\n    });\n  });\n\n  describe('getStrategies', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getStrategies(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call getStrategies on the client', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getStrategies(rdiClientMetadata);\n\n      expect(client.getStrategies).toHaveBeenCalled();\n    });\n\n    it('should return the result of getStrategies on the client', async () => {\n      const response = { strategies: [{ id: 1, name: 'Strategy 1' }] };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.getStrategies.mockResolvedValueOnce(response);\n\n      const result = await service.getStrategies(rdiClientMetadata);\n\n      expect(result).toBe(response);\n    });\n  });\n\n  describe('getConfigTemplate', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const pipelineType = 'type';\n      const dbType = 'type';\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getConfigTemplate(rdiClientMetadata, pipelineType, dbType);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call getConfigTemplate on the client with the correct arguments', async () => {\n      const pipelineType = 'type';\n      const dbType = 'type';\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getConfigTemplate(rdiClientMetadata, pipelineType, dbType);\n\n      expect(client.getConfigTemplate).toHaveBeenCalledWith(\n        pipelineType,\n        dbType,\n      );\n    });\n\n    it('should return the result of getConfigTemplate on the client', async () => {\n      const pipelineType = 'type';\n      const dbType = 'type';\n      const response = { template: 'some template' };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.getConfigTemplate.mockResolvedValue(response);\n      const result = await service.getConfigTemplate(\n        rdiClientMetadata,\n        pipelineType,\n        dbType,\n      );\n\n      expect(result).toBe(response);\n    });\n  });\n\n  describe('getPipelineStatus', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getPipelineStatus(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call getPipelineStatus on the client', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getPipelineStatus(rdiClientMetadata);\n\n      expect(client.getPipelineStatus).toHaveBeenCalled();\n    });\n\n    it('should return the result of getPipelineStatus on the client', async () => {\n      const response = { data: { status: 'running' } };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.getPipelineStatus.mockResolvedValueOnce(response);\n\n      const result = await service.getPipelineStatus(rdiClientMetadata);\n\n      expect(result).toBe(response);\n    });\n  });\n\n  describe('getJobFunctions', () => {\n    it('should call getOrCreate on rdiClientProvider with the correct metadata', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getJobFunctions(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call getJobFunctions on the client', async () => {\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getJobFunctions(rdiClientMetadata);\n\n      expect(client.getJobFunctions).toHaveBeenCalled();\n    });\n\n    it('should return the result of getJobFunctions on the client', async () => {\n      const response = { jobFunctions: ['jobFunc1', 'jobFunc2'] };\n      const client = generateMockRdiClient(rdiClientMetadata);\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n      client.getJobFunctions.mockResolvedValueOnce(response);\n      const result = await service.getJobFunctions(rdiClientMetadata);\n\n      expect(result).toBe(response);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport {\n  RdiClientMetadata,\n  RdiPipeline,\n  RdiPipelineStatus,\n} from 'src/modules/rdi/models';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\nimport {\n  RdiDryRunJobDto,\n  RdiTemplateResponseDto,\n  RdiTestConnectionsResponseDto,\n} from 'src/modules/rdi/dto';\nimport { RdiDryRunJobResponseDto } from 'src/modules/rdi/dto/rdi.dry-run.job.response.dto';\nimport { RdiPipelineAnalytics } from 'src/modules/rdi/rdi-pipeline.analytics';\nimport { wrapHttpError } from 'src/common/utils';\n\n@Injectable()\nexport class RdiPipelineService {\n  private logger: Logger = new Logger('RdiPipelineService');\n\n  constructor(\n    private readonly rdiClientProvider: RdiClientProvider,\n    private readonly analytics: RdiPipelineAnalytics,\n  ) {}\n\n  async getSchema(rdiClientMetadata: RdiClientMetadata): Promise<object> {\n    this.logger.debug('Getting RDI pipeline schema', rdiClientMetadata);\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getSchema();\n  }\n\n  async getPipeline(\n    rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiPipeline> {\n    this.logger.debug('Getting RDI pipeline', rdiClientMetadata);\n\n    try {\n      const client =\n        await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n      const pipeline = await client.getPipeline();\n\n      this.analytics.sendRdiPipelineFetched(\n        rdiClientMetadata.sessionMetadata,\n        rdiClientMetadata.id,\n        pipeline,\n      );\n\n      this.logger.debug('Succeed to get RDI pipeline', rdiClientMetadata);\n\n      return pipeline;\n    } catch (e) {\n      this.logger.error('Failed to get RDI pipeline', e, rdiClientMetadata);\n\n      this.analytics.sendRdiPipelineFetchFailed(\n        rdiClientMetadata.sessionMetadata,\n        e,\n        rdiClientMetadata.id,\n      );\n      throw wrapHttpError(e);\n    }\n  }\n\n  async dryRunJob(\n    rdiClientMetadata: RdiClientMetadata,\n    dto: RdiDryRunJobDto,\n  ): Promise<RdiDryRunJobResponseDto> {\n    this.logger.debug('Trying dry run job', rdiClientMetadata);\n\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.dryRunJob(dto);\n  }\n\n  async deploy(\n    rdiClientMetadata: RdiClientMetadata,\n    dto: RdiPipeline,\n  ): Promise<void> {\n    this.logger.debug('Trying to deploy pipeline', rdiClientMetadata);\n\n    try {\n      const client =\n        await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n      await client.deploy(dto);\n\n      this.analytics.sendRdiPipelineDeployed(\n        rdiClientMetadata.sessionMetadata,\n        rdiClientMetadata.id,\n      );\n      this.logger.debug('Succeed to deploy pipeline', rdiClientMetadata);\n    } catch (e) {\n      this.analytics.sendRdiPipelineDeployFailed(\n        rdiClientMetadata.sessionMetadata,\n        e,\n        rdiClientMetadata.id,\n      );\n\n      this.logger.error('Failed to deploy pipeline', e, rdiClientMetadata);\n\n      throw wrapHttpError(e);\n    }\n  }\n\n  async stopPipeline(rdiClientMetadata: RdiClientMetadata): Promise<void> {\n    this.logger.debug('Stopping running pipeline', rdiClientMetadata);\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.stopPipeline();\n  }\n\n  async startPipeline(rdiClientMetadata: RdiClientMetadata): Promise<void> {\n    this.logger.debug('Starting stopped pipeline', rdiClientMetadata);\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.startPipeline();\n  }\n\n  async resetPipeline(rdiClientMetadata: RdiClientMetadata): Promise<void> {\n    this.logger.debug('Resetting default pipeline', rdiClientMetadata);\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.resetPipeline();\n  }\n\n  async testConnections(\n    rdiClientMetadata: RdiClientMetadata,\n    config: object,\n  ): Promise<RdiTestConnectionsResponseDto> {\n    this.logger.debug('Trying to test connections', rdiClientMetadata);\n\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.testConnections(config);\n  }\n\n  async getStrategies(rdiClientMetadata: RdiClientMetadata): Promise<object> {\n    this.logger.debug('Getting RDI pipeline strategies', rdiClientMetadata);\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getStrategies();\n  }\n\n  async getConfigTemplate(\n    rdiClientMetadata: RdiClientMetadata,\n    pipelineType: string,\n    dbType: string,\n  ): Promise<RdiTemplateResponseDto> {\n    this.logger.debug('Getting RDI config template', rdiClientMetadata);\n\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getConfigTemplate(pipelineType, dbType);\n  }\n\n  async getJobTemplate(\n    rdiClientMetadata: RdiClientMetadata,\n    pipelineType: string,\n  ): Promise<RdiTemplateResponseDto> {\n    this.logger.debug('Getting RDI job template', rdiClientMetadata);\n\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getJobTemplate(pipelineType);\n  }\n\n  async getPipelineStatus(\n    rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiPipelineStatus> {\n    this.logger.debug('Getting RDI pipeline status', rdiClientMetadata);\n\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getPipelineStatus();\n  }\n\n  async getJobFunctions(rdiClientMetadata: RdiClientMetadata): Promise<object> {\n    this.logger.debug('Getting RDI job functions', rdiClientMetadata);\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getJobFunctions();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-statistics.controller.ts",
    "content": "import {\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { RequestRdiClientMetadata } from 'src/modules/rdi/decorators';\nimport { RdiClientMetadata, RdiStatisticsResult } from 'src/modules/rdi/models';\nimport { RdiStatisticsService } from 'src/modules/rdi/rdi-statistics.service';\n\n@ApiTags('RDI')\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('rdi/:id/statistics')\nexport class RdiStatisticsController {\n  constructor(private readonly rdiStatisticsService: RdiStatisticsService) {}\n\n  @Get('/')\n  @ApiEndpoint({\n    description: 'Get statistics',\n    responses: [{ status: 200, type: RdiStatisticsResult }],\n  })\n  async getStatistics(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiStatisticsResult> {\n    return this.rdiStatisticsService.getStatistics(rdiClientMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-statistics.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { MockType, mockRdiClientProvider } from 'src/__mocks__';\nimport {\n  RdiClientMetadata,\n  RdiStatisticsResult,\n  RdiStatisticsStatus,\n} from 'src/modules/rdi/models';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\nimport { RdiStatisticsService } from 'src/modules/rdi/rdi-statistics.service';\n\ndescribe('RdiStatisticsService', () => {\n  let service: RdiStatisticsService;\n  let rdiClientProvider: MockType<RdiClientProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RdiStatisticsService,\n        {\n          provide: RdiClientProvider,\n          useFactory: mockRdiClientProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(RdiStatisticsService);\n    rdiClientProvider = module.get(RdiClientProvider);\n  });\n\n  describe('getStatistics', () => {\n    const rdiClientMetadata: RdiClientMetadata = {\n      id: '123',\n      sessionMetadata: undefined,\n    };\n\n    it('should call getOrCreate on RdiClientProvider with correct arguments', async () => {\n      const client = {\n        getStatistics: jest.fn().mockResolvedValue({}),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getStatistics(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should call getStatistics on RdiClient with correct arguments', async () => {\n      const client = {\n        getStatistics: jest.fn().mockResolvedValue({}),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      await service.getStatistics(rdiClientMetadata);\n\n      expect(client.getStatistics).toHaveBeenCalledTimes(1);\n    });\n\n    it('should return the result of getStatistics on RdiClient', async () => {\n      const expectedResult: RdiStatisticsResult = {\n        status: RdiStatisticsStatus.Success,\n      };\n      const client = {\n        getStatistics: jest.fn().mockResolvedValue(expectedResult),\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(client);\n\n      const result = await service.getStatistics(rdiClientMetadata);\n\n      expect(result).toEqual(expectedResult);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi-statistics.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { RdiClientMetadata, RdiStatisticsResult } from 'src/modules/rdi/models';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\n\n@Injectable()\nexport class RdiStatisticsService {\n  private logger: Logger = new Logger('RdiStatisticsService');\n\n  constructor(private readonly rdiClientProvider: RdiClientProvider) {}\n\n  async getStatistics(\n    rdiClientMetadata: RdiClientMetadata,\n  ): Promise<RdiStatisticsResult> {\n    this.logger.debug('Getting RDI statistics', rdiClientMetadata);\n\n    const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n\n    return await client.getStatistics();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { RdiAnalytics } from 'src/modules/rdi/rdi.analytics';\nimport { mockSessionMetadata } from 'src/__mocks__';\n\ndescribe('RdiAnalytics', () => {\n  let service: RdiAnalytics;\n  let sendEventMethod;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, RdiAnalytics],\n    }).compile();\n\n    service = await module.get(RdiAnalytics);\n    sendEventMethod = jest.spyOn<RdiAnalytics, any>(service, 'sendEvent');\n  });\n\n  describe('sendRdiInstanceDeleted', () => {\n    it('should emit event when rdi instance is deleted successfully', () => {\n      service.sendRdiInstanceDeleted(mockSessionMetadata, 1);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RdiInstanceDeleted,\n        {\n          numberOfInstances: 1,\n        },\n      );\n    });\n\n    it('should emit event when rdi instance is not deleted successfully', () => {\n      service.sendRdiInstanceDeleted(mockSessionMetadata, 2, 'error');\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RdiInstanceDeleted,\n        {\n          numberOfInstances: 2,\n          error: 'error',\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi.analytics.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class RdiAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendRdiInstanceDeleted(\n    sessionMetadata: SessionMetadata,\n    numberOfInstances: number,\n    error?: string,\n  ) {\n    try {\n      this.sendEvent(sessionMetadata, TelemetryEvents.RdiInstanceDeleted, {\n        numberOfInstances,\n        error,\n      });\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Patch,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { Rdi, RdiClientMetadata } from 'src/modules/rdi/models';\nimport { ApiTags } from '@nestjs/swagger';\nimport { RdiService } from 'src/modules/rdi/rdi.service';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto';\nimport { RequestRdiClientMetadata } from 'src/modules/rdi/decorators';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\n\n@ApiTags('RDI')\n@UsePipes(new ValidationPipe({ transform: true }))\n@UseInterceptors(ClassSerializerInterceptor)\n@Controller('rdi')\nexport class RdiController {\n  constructor(private readonly rdiService: RdiService) {}\n\n  @Get()\n  @ApiEndpoint({\n    description: 'Get RDI list',\n    responses: [{ status: 200, isArray: true, type: Rdi }],\n  })\n  async list(): Promise<Rdi[]> {\n    return this.rdiService.list();\n  }\n\n  @Get('/:id')\n  @ApiEndpoint({\n    description: 'Get RDI by id',\n    responses: [{ status: 200, type: Rdi }],\n  })\n  async get(@Param('id') id: string): Promise<Rdi> {\n    return this.rdiService.get(id);\n  }\n\n  @Post()\n  @ApiEndpoint({\n    description: 'Create RDI',\n    statusCode: 201,\n    responses: [{ status: 201, type: Rdi }],\n  })\n  async create(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: CreateRdiDto,\n  ): Promise<Rdi> {\n    return this.rdiService.create(sessionMetadata, dto);\n  }\n\n  @Patch('/:id')\n  @ApiEndpoint({\n    description: 'Update RDI',\n    responses: [{ status: 200, type: Rdi }],\n  })\n  async update(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n    @Body() dto: UpdateRdiDto,\n  ): Promise<Rdi> {\n    return this.rdiService.update(rdiClientMetadata, dto);\n  }\n\n  @Delete()\n  @ApiEndpoint({\n    description: 'Delete RDI',\n    responses: [{ status: 200 }],\n  })\n  async delete(\n    @Body() body: { ids: string[] },\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<void> {\n    return this.rdiService.delete(sessionMetadata, body.ids);\n  }\n\n  @Get(':id/connect')\n  @ApiEndpoint({\n    description: 'Connect to RDI',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Successfully connected to rdi instance',\n      },\n    ],\n  })\n  async connect(\n    @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata,\n  ): Promise<void> {\n    return this.rdiService.connect(rdiClientMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { RdiController } from 'src/modules/rdi/rdi.controller';\nimport { RdiPipelineController } from 'src/modules/rdi/rdi-pipeline.controller';\nimport { RdiService } from 'src/modules/rdi/rdi.service';\nimport { RdiPipelineService } from 'src/modules/rdi/rdi-pipeline.service';\nimport { RdiRepository } from 'src/modules/rdi/repository/rdi.repository';\nimport { LocalRdiRepository } from 'src/modules/rdi/repository/local.rdi.repository';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\nimport { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage';\nimport { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory';\nimport { RdiAnalytics } from 'src/modules/rdi/rdi.analytics';\nimport { RdiPipelineAnalytics } from 'src/modules/rdi/rdi-pipeline.analytics';\nimport { RdiStatisticsController } from 'src/modules/rdi/rdi-statistics.controller';\nimport { RdiStatisticsService } from 'src/modules/rdi/rdi-statistics.service';\n\n@Module({})\nexport class RdiModule {\n  static register(rdiRepository: Type<RdiRepository> = LocalRdiRepository) {\n    return {\n      module: RdiModule,\n      controllers: [\n        RdiController,\n        RdiPipelineController,\n        RdiStatisticsController,\n      ],\n      providers: [\n        RdiService,\n        RdiPipelineService,\n        RdiStatisticsService,\n        RdiClientProvider,\n        RdiClientStorage,\n        RdiClientFactory,\n        RdiAnalytics,\n        RdiPipelineAnalytics,\n        {\n          provide: RdiRepository,\n          useClass: rdiRepository,\n        },\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { RdiRepository } from 'src/modules/rdi/repository/rdi.repository';\nimport { RdiAnalytics } from 'src/modules/rdi/rdi.analytics';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\nimport { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory';\nimport { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto';\nimport { Rdi, RdiClientMetadata } from 'src/modules/rdi/models';\nimport {\n  MockType,\n  mockRdi,\n  mockRdiAnalytics,\n  mockRdiClientFactory,\n  mockRdiClientProvider,\n  mockRdiRepository,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { AxiosError } from 'axios';\nimport { wrapRdiPipelineError } from './exceptions';\nimport { RdiService } from './rdi.service';\n\ndescribe('RdiService', () => {\n  let service: RdiService;\n  let repository: MockType<RdiRepository>;\n  let analytics: MockType<RdiAnalytics>;\n  let rdiClientProvider: MockType<RdiClientProvider>;\n  let rdiClientFactory: MockType<RdiClientFactory>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RdiService,\n        {\n          provide: RdiRepository,\n          useFactory: mockRdiRepository,\n        },\n        {\n          provide: RdiAnalytics,\n          useFactory: mockRdiAnalytics,\n        },\n        {\n          provide: RdiClientProvider,\n          useFactory: mockRdiClientProvider,\n        },\n        {\n          provide: RdiClientFactory,\n          useFactory: mockRdiClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get(RdiService);\n    repository = module.get(RdiRepository);\n    analytics = module.get(RdiAnalytics);\n    rdiClientProvider = module.get(RdiClientProvider);\n    rdiClientFactory = module.get(RdiClientFactory);\n  });\n\n  describe('list', () => {\n    it('should return a list of Rdi instances', async () => {\n      const rd1 = new Rdi();\n      const rd2 = new Rdi();\n\n      repository.list.mockResolvedValue([rd1, rd2]);\n      const result = await service.list();\n\n      expect(result).toEqual([rd1, rd2]);\n    });\n  });\n\n  describe('get', () => {\n    it('should return an Rdi instance by id', async () => {\n      const rd = new Rdi();\n      repository.get.mockResolvedValue(rd);\n\n      const result = await service.get('123');\n\n      expect(result).toEqual(rd);\n    });\n\n    it('should throw an error if Rdi instance is not found', async () => {\n      repository.get.mockResolvedValue(undefined);\n\n      await expect(service.get('123')).rejects.toThrowError(\n        'RDI with id 123 was not found',\n      );\n    });\n  });\n\n  describe('update', () => {\n    const rdiClientMetadata: RdiClientMetadata = {\n      id: '123',\n      sessionMetadata: undefined,\n    };\n    it('should update an Rdi instance', async () => {\n      const oldRd = Object.assign(new Rdi(), mockRdi);\n      const newRd = Object.assign(new Rdi(), {\n        ...mockRdi,\n        name: 'Updated name',\n      });\n      const dto: UpdateRdiDto = Object.assign(new UpdateRdiDto(), {\n        name: 'Updated name',\n      });\n      repository.get.mockResolvedValue(oldRd);\n      repository.update.mockResolvedValue(newRd);\n      rdiClientFactory.createClient.mockResolvedValue(undefined);\n      rdiClientProvider.deleteManyByRdiId.mockResolvedValue(undefined);\n\n      const result = await service.update(rdiClientMetadata, dto);\n\n      expect(repository.get).toHaveBeenCalledWith(rdiClientMetadata.id);\n      expect(repository.update).toHaveBeenCalledWith(\n        rdiClientMetadata.id,\n        newRd,\n      );\n      expect(result).toEqual(newRd);\n    });\n\n    it('should create a client and delete rdis if updating connectionFields', async () => {\n      const oldRd = Object.assign(new Rdi(), mockRdi);\n      const dto: UpdateRdiDto = Object.assign(\n        new UpdateRdiDto(),\n        RdiService.connectionFields.reduce((res, key) => {\n          res[key] = 'updated';\n          return res;\n        }, {}),\n      );\n      const newRd = Object.assign(new Rdi(), {\n        ...mockRdi,\n        ...dto,\n      });\n      repository.get.mockResolvedValue(oldRd);\n      repository.update.mockResolvedValue(newRd);\n      rdiClientFactory.createClient.mockResolvedValue(undefined);\n      rdiClientProvider.deleteManyByRdiId.mockResolvedValue(undefined);\n\n      await service.update(rdiClientMetadata, dto);\n\n      expect(RdiService.isConnectionAffected(dto)).toBeTruthy();\n      expect(rdiClientFactory.createClient).toHaveBeenCalledWith(\n        rdiClientMetadata,\n        newRd,\n      );\n      expect(rdiClientProvider.deleteManyByRdiId).toHaveBeenCalledWith(\n        rdiClientMetadata.id,\n      );\n    });\n\n    it('should throw an error if update fails', async () => {\n      const oldRd = Object.assign(new Rdi(), mockRdi);\n      const dto: UpdateRdiDto = Object.assign(new UpdateRdiDto(), {\n        name: 'Updated name',\n      });\n      const error = new AxiosError('Update failed');\n      repository.get.mockResolvedValue(oldRd);\n      repository.update.mockRejectedValue(error);\n      rdiClientFactory.createClient.mockResolvedValue(undefined);\n      rdiClientProvider.deleteManyByRdiId.mockResolvedValue(undefined);\n\n      await expect(service.update(rdiClientMetadata, dto)).rejects.toThrowError(\n        wrapRdiPipelineError(error),\n      );\n    });\n  });\n\n  describe('create', () => {\n    const validGetVersion = () => Promise.resolve('test-version');\n\n    it('should create an Rdi instance', async () => {\n      const dto: CreateRdiDto = {\n        name: 'name',\n        url: 'http://localhost:4000',\n        password: 'pass',\n        username: 'user',\n      };\n      repository.create.mockResolvedValue(mockRdi);\n      rdiClientFactory.createClient.mockReturnValue({\n        getVersion: validGetVersion,\n      });\n\n      const result = await service.create(mockSessionMetadata, dto);\n\n      expect(result.name).toEqual(dto.name);\n      expect(rdiClientFactory.createClient).toHaveBeenCalledWith(\n        { sessionMetadata: mockSessionMetadata, id: expect.any(String) },\n        expect.any(Rdi),\n      );\n    });\n\n    it('should throw an error if create fails', async () => {\n      const dto: CreateRdiDto = {\n        name: 'name',\n        url: 'http://localhost:4000',\n        password: 'pass',\n        username: 'user',\n      };\n      const error = new AxiosError('Create failed');\n      repository.create.mockRejectedValue(error);\n      rdiClientFactory.createClient.mockRejectedValue(error);\n\n      await expect(\n        service.create(mockSessionMetadata, dto),\n      ).rejects.toThrowError(wrapRdiPipelineError(error));\n    });\n\n    it('should get the RDI version', async () => {\n      const dto: CreateRdiDto = {\n        name: 'name',\n        url: 'http://localhost:4000',\n        password: 'pass',\n        username: 'user',\n      };\n\n      repository.create.mockResolvedValue(mockRdi);\n      rdiClientFactory.createClient.mockReturnValue({\n        getVersion: validGetVersion,\n      });\n\n      await service.create(mockSessionMetadata, dto);\n\n      expect(repository.create).toHaveBeenCalledWith(\n        expect.objectContaining({\n          version: 'test-version',\n        }),\n      );\n    });\n\n    it('should get the default RDI version when getVersion fails', async () => {\n      const dto: CreateRdiDto = {\n        name: 'name',\n        url: 'http://localhost:4000',\n        password: 'pass',\n        username: 'user',\n      };\n\n      repository.create.mockResolvedValue(mockRdi);\n      rdiClientFactory.createClient.mockResolvedValue({\n        getVersion: () => Promise.reject(new Error('Version not available')),\n      });\n\n      await expect(service.create(mockSessionMetadata, dto)).rejects.toThrow();\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete Rdi instances', async () => {\n      const ids = ['123', '456'];\n      repository.delete.mockResolvedValue(undefined);\n      rdiClientProvider.deleteManyByRdiId.mockResolvedValue(undefined);\n      jest\n        .spyOn(analytics, 'sendRdiInstanceDeleted')\n        .mockResolvedValue(undefined as never);\n\n      await service.delete(mockSessionMetadata, ids);\n\n      expect(repository.delete).toHaveBeenCalledWith(ids);\n      expect(rdiClientProvider.deleteManyByRdiId).toHaveBeenCalledWith(ids[0]);\n      expect(rdiClientProvider.deleteManyByRdiId).toHaveBeenCalledWith(ids[1]);\n      expect(analytics.sendRdiInstanceDeleted).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        ids.length,\n      );\n    });\n\n    it('should throw an error if delete fails', async () => {\n      const ids = ['123', '456'];\n      repository.delete.mockRejectedValue(new Error('Delete failed'));\n      rdiClientProvider.deleteManyByRdiId.mockRejectedValue(\n        new Error('Delete client failed'),\n      );\n      jest\n        .spyOn(analytics, 'sendRdiInstanceDeleted')\n        .mockResolvedValue(undefined as never);\n\n      await expect(\n        service.delete(mockSessionMetadata, ids),\n      ).rejects.toThrowError('Internal Server Error');\n      expect(analytics.sendRdiInstanceDeleted).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        ids.length,\n        expect.any(String),\n      );\n    });\n  });\n\n  describe('connect', () => {\n    it('should connect to an Rdi instance', async () => {\n      const rdiClientMetadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      rdiClientProvider.getOrCreate.mockResolvedValue(undefined);\n\n      await service.connect(rdiClientMetadata);\n\n      expect(rdiClientProvider.getOrCreate).toHaveBeenCalledWith(\n        rdiClientMetadata,\n      );\n    });\n\n    it('should throw an error if connection fails', async () => {\n      const rdiClientMetadata: RdiClientMetadata = {\n        id: '123',\n        sessionMetadata: undefined,\n      };\n      const error = new AxiosError('Connection failed');\n      rdiClientProvider.getOrCreate.mockRejectedValue(error);\n\n      await expect(service.connect(rdiClientMetadata)).rejects.toThrowError(\n        wrapRdiPipelineError(error),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/rdi.service.ts",
    "content": "import {\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { v4 as uuidv4 } from 'uuid';\n\nimport { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto';\nimport { Rdi, RdiClientMetadata } from 'src/modules/rdi/models';\nimport { RdiRepository } from 'src/modules/rdi/repository/rdi.repository';\nimport { classToClass } from 'src/utils';\nimport { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider';\nimport { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory';\nimport { SessionMetadata } from 'src/common/models';\nimport {\n  RdiPipelineNotFoundException,\n  wrapRdiPipelineError,\n} from 'src/modules/rdi/exceptions';\nimport { isUndefined, omitBy } from 'lodash';\nimport { deepMerge } from 'src/common/utils';\nimport { RdiAnalytics } from './rdi.analytics';\nimport { RdiClient } from './client/rdi.client';\n\n@Injectable()\nexport class RdiService {\n  private logger = new Logger('RdiService');\n\n  static connectionFields: string[] = ['username', 'password'];\n\n  constructor(\n    private readonly repository: RdiRepository,\n    private readonly analytics: RdiAnalytics,\n    private readonly rdiClientProvider: RdiClientProvider,\n    private readonly rdiClientFactory: RdiClientFactory,\n  ) {}\n\n  static isConnectionAffected(dto: UpdateRdiDto) {\n    return Object.keys(omitBy(dto, isUndefined)).some((field) =>\n      this.connectionFields.includes(field),\n    );\n  }\n\n  private static async getRdiVersion(client: RdiClient): Promise<string> {\n    return client.getVersion();\n  }\n\n  async list(): Promise<Rdi[]> {\n    return await this.repository.list();\n  }\n\n  async get(id: string): Promise<Rdi> {\n    const rdi = await this.repository.get(id);\n\n    if (!rdi) {\n      throw new RdiPipelineNotFoundException(`RDI with id ${id} was not found`);\n    }\n\n    return rdi;\n  }\n\n  async update(\n    rdiClientMetadata: RdiClientMetadata,\n    dto: UpdateRdiDto,\n  ): Promise<Rdi> {\n    const oldRdiInstance = await this.get(rdiClientMetadata.id);\n    const newRdiInstance = await deepMerge(oldRdiInstance, dto);\n\n    try {\n      if (RdiService.isConnectionAffected(dto)) {\n        await this.rdiClientFactory.createClient(\n          rdiClientMetadata,\n          newRdiInstance,\n        );\n        await this.rdiClientProvider.deleteManyByRdiId(rdiClientMetadata.id);\n      }\n\n      return await this.repository.update(rdiClientMetadata.id, newRdiInstance);\n    } catch (error) {\n      this.logger.error(\n        `Failed to update rdi instance ${rdiClientMetadata.id}`,\n        error,\n        rdiClientMetadata,\n      );\n      throw wrapRdiPipelineError(error);\n    }\n  }\n\n  async create(\n    sessionMetadata: SessionMetadata,\n    dto: CreateRdiDto,\n  ): Promise<Rdi> {\n    const model = classToClass(Rdi, dto);\n    model.lastConnection = new Date();\n\n    const rdiClientMetadata = {\n      sessionMetadata,\n      id: uuidv4(),\n    };\n\n    try {\n      const client = await this.rdiClientFactory.createClient(\n        rdiClientMetadata,\n        model,\n      );\n      model.version = await RdiService.getRdiVersion(client);\n    } catch (error) {\n      this.logger.error('Failed to create rdi instance', sessionMetadata);\n\n      throw wrapRdiPipelineError(error);\n    }\n\n    this.logger.debug('Succeed to create rdi instance', sessionMetadata);\n    return await this.repository.create(model);\n  }\n\n  async delete(sessionMetadata: SessionMetadata, ids: string[]): Promise<void> {\n    try {\n      await this.repository.delete(ids);\n      await Promise.all(\n        ids.map(async (id) => {\n          await this.rdiClientProvider.deleteManyByRdiId(id);\n        }),\n      );\n\n      this.analytics.sendRdiInstanceDeleted(sessionMetadata, ids.length);\n    } catch (error) {\n      this.logger.error(\n        `Failed to delete instance(s): ${ids}`,\n        error,\n        sessionMetadata,\n      );\n      this.analytics.sendRdiInstanceDeleted(\n        sessionMetadata,\n        ids.length,\n        error.message,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n\n  /**\n   * Connect to rdi instance\n   * @param rdiClientMetadata\n   */\n  async connect(rdiClientMetadata: RdiClientMetadata): Promise<void> {\n    try {\n      await this.rdiClientProvider.getOrCreate(rdiClientMetadata);\n    } catch (error) {\n      this.logger.error(\n        `Failed to connect to rdi instance ${rdiClientMetadata.id}`,\n        error,\n        rdiClientMetadata,\n      );\n      throw wrapRdiPipelineError(error);\n    }\n\n    this.logger.debug(\n      `Succeed to connect to rdi instance ${rdiClientMetadata.id}`,\n      rdiClientMetadata,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/repository/local.rdi.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport { Repository } from 'typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { RdiEntity } from 'src/modules/rdi/entities/rdi.entity';\nimport {\n  MockType,\n  mockEncryptionService,\n  mockRdi,\n  mockRdiDecrypted,\n  mockRdiEntityEncrypted,\n  mockRdiPasswordEncrypted,\n  mockRdiPasswordPlain,\n  mockRepository,\n} from 'src/__mocks__';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { LocalRdiRepository } from './local.rdi.repository';\n\ndescribe('LocalRdiRepository', () => {\n  let repository: LocalRdiRepository;\n  let rdiEntityRepository: MockType<Repository<RdiEntity>>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalRdiRepository,\n        {\n          provide: getRepositoryToken(RdiEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    repository = module.get(LocalRdiRepository);\n    rdiEntityRepository = module.get(getRepositoryToken(RdiEntity));\n    encryptionService = module.get(EncryptionService);\n\n    when(encryptionService.decrypt)\n      .calledWith(mockRdiPasswordEncrypted, expect.anything())\n      .mockResolvedValue(mockRdiPasswordPlain);\n\n    when(encryptionService.encrypt)\n      .calledWith(mockRdiPasswordPlain)\n      .mockResolvedValue({\n        data: mockRdiPasswordEncrypted,\n        encryption: EncryptionStrategy.KEYTAR,\n      });\n  });\n\n  describe('get', () => {\n    it('should return null if entity is not found', async () => {\n      rdiEntityRepository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await repository.get('1');\n\n      expect(result).toBeNull();\n    });\n\n    it('should return decrypted Rdi entity', async () => {\n      rdiEntityRepository.findOneBy.mockResolvedValueOnce(\n        mockRdiEntityEncrypted,\n      );\n\n      const result = await repository.get(mockRdiEntityEncrypted.id);\n\n      expect(result).toEqual(mockRdiDecrypted);\n    });\n\n    it('should return decrypted Rdi entity even if decryption fails and ignoreEncryptionErrors is true', async () => {\n      rdiEntityRepository.findOneBy.mockResolvedValueOnce(\n        mockRdiEntityEncrypted,\n      );\n      encryptionService.decrypt.mockRejectedValueOnce(new Error());\n\n      const result = await repository.get(mockRdiEntityEncrypted.id, true);\n\n      expect(result?.id).toEqual(mockRdiEntityEncrypted.id);\n    });\n  });\n\n  describe('list', () => {\n    it('should return list of Rdi entities', async () => {\n      const encryptedEntities = [\n        Object.assign(new RdiEntity(), { ...mockRdi, id: '1' }),\n        Object.assign(new RdiEntity(), { ...mockRdi, id: '2' }),\n      ];\n\n      const rdis = [\n        { ...mockRdi, id: '1' },\n        { ...mockRdi, id: '2' },\n      ];\n\n      jest\n        .spyOn(rdiEntityRepository, 'createQueryBuilder')\n        .mockReturnValueOnce({\n          select: jest.fn().mockReturnThis(),\n          getMany: jest.fn().mockResolvedValueOnce(encryptedEntities),\n        } as any);\n\n      const result = await repository.list();\n\n      expect(result).toEqual([rdis[0], rdis[1]]);\n    });\n  });\n\n  describe('create', () => {\n    it('should create and return decrypted Rdi entity', async () => {\n      jest\n        .spyOn(rdiEntityRepository, 'save')\n        .mockReturnValueOnce(mockRdiEntityEncrypted);\n      const result = await repository.create(mockRdiDecrypted);\n\n      expect(result).toEqual(mockRdiDecrypted);\n    });\n  });\n\n  describe('update', () => {\n    it('should update and return decrypted Rdi entity', async () => {\n      rdiEntityRepository.findOneBy.mockResolvedValue(mockRdiEntityEncrypted);\n      rdiEntityRepository.merge.mockReturnValue(mockRdiDecrypted);\n      rdiEntityRepository.save.mockResolvedValue(mockRdiEntityEncrypted);\n\n      const result = await repository.update('1', mockRdiDecrypted);\n\n      expect(result).toEqual(mockRdiDecrypted);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete entities', async () => {\n      rdiEntityRepository.delete.mockResolvedValueOnce(undefined);\n\n      const res = await repository.delete(['1', '2']);\n\n      expect(rdiEntityRepository.delete).toHaveBeenCalledWith(['1', '2']);\n      expect(res).toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { RdiEntity } from 'src/modules/rdi/entities/rdi.entity';\nimport { Rdi } from 'src/modules/rdi/models';\nimport { RdiRepository } from 'src/modules/rdi/repository/rdi.repository';\nimport { classToClass } from 'src/utils';\n\n@Injectable()\nexport class LocalRdiRepository extends RdiRepository {\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(RdiEntity)\n    private readonly repository: Repository<RdiEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(this.encryptionService, [\n      'password',\n    ]);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async get(\n    id: string,\n    ignoreEncryptionErrors: boolean = false,\n  ): Promise<Rdi> {\n    const entity = await this.repository.findOneBy({ id });\n\n    if (!entity) {\n      return null;\n    }\n\n    return classToClass(\n      Rdi,\n      await this.modelEncryptor.decryptEntity(entity, ignoreEncryptionErrors),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async list(): Promise<Rdi[]> {\n    const entities = await this.repository\n      .createQueryBuilder('r')\n      .select([\n        'r.id',\n        'r.name',\n        'r.url',\n        'r.version',\n        'r.username',\n        'r.lastConnection',\n      ])\n      .getMany();\n    return entities.map((entity) => classToClass(Rdi, entity));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async create(rdi: Rdi): Promise<Rdi> {\n    const entity = classToClass(RdiEntity, rdi);\n\n    return classToClass(\n      Rdi,\n      await this.modelEncryptor.decryptEntity(\n        await this.repository.save(\n          await this.modelEncryptor.encryptEntity(entity),\n        ),\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async update(id: string, rdi: Rdi): Promise<Rdi> {\n    const oldEntity = await this.modelEncryptor.decryptEntity(\n      await this.repository.findOneBy({ id }),\n      true,\n    );\n    const newEntity = classToClass(RdiEntity, rdi);\n\n    const encrypted = await this.modelEncryptor.encryptEntity(\n      this.repository.merge(oldEntity, newEntity),\n    );\n\n    return classToClass(\n      Rdi,\n      await this.modelEncryptor.decryptEntity(\n        await this.repository.save(encrypted),\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async delete(ids: string[]): Promise<void> {\n    await this.repository.delete(ids);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/repository/rdi.repository.ts",
    "content": "import { Rdi } from 'src/modules/rdi/models';\n\nexport abstract class RdiRepository {\n  /**\n   * List of RDIs (limited fields only)\n   * Fields: ['id', 'name', 'host', 'port', 'type', 'lastConnection']\n   * @return Rdi[]\n   */\n  abstract list(): Promise<Rdi[]>;\n\n  /**\n   * Get RDI connection details by id\n   * @param id\n   * @param ignoreEncryptionErrors\n   * @return Rdi\n   */\n  abstract get(id: string, ignoreEncryptionErrors?: boolean): Promise<Rdi>;\n\n  /**\n   * Create RDI connection\n   * @param rdi\n   */\n  abstract create(rdi: Rdi): Promise<Rdi>;\n\n  /**\n   * Update RDI connection config\n   * @param id\n   * @param rdi\n   */\n  abstract update(id: string, rdi: Partial<Rdi>): Promise<Rdi>;\n\n  /**\n   * Delete RDI by id\n   * @param ids\n   */\n  abstract delete(ids: string[]): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/utils/pipeline.util.spec.ts",
    "content": "import {\n  convertApiDataToRdiJobs,\n  convertApiDataToRdiPipeline,\n  convertRdiJobsToApiPayload,\n  convertRdiPipelineToApiPayload,\n} from 'src/modules/rdi/utils/pipeline.util';\nimport { RdiPipeline } from '../models';\n\nconst job1 = {\n  name: 'job1',\n  source: {\n    redis: {},\n  },\n  transform: [],\n  output: [],\n};\nconst job2 = {\n  name: 'job2',\n  source: {\n    redis: {},\n  },\n  transform: [],\n  output: [],\n};\n\ndescribe('convertApiDataToRdiJobs', () => {\n  it('should return an empty object when no jobs are provided', () => {\n    const result = convertApiDataToRdiJobs();\n    expect(result).toEqual({});\n  });\n\n  it('should return an empty object when an empty array is provided', () => {\n    const result = convertApiDataToRdiJobs(\n      [] as unknown as [Record<string, any>],\n    );\n    expect(result).toEqual({});\n  });\n\n  it('should return a map of jobs with their names as keys', () => {\n    const jobs = [job1, job2] as unknown as [Record<string, any>];\n    const result = convertApiDataToRdiJobs(jobs);\n    expect(result).toEqual({\n      [job1.name]: { ...job1, name: undefined },\n      [job2.name]: { ...job2, name: undefined },\n    });\n  });\n\n  it('should remove the name property from each job', () => {\n    const jobs = [job1, job2] as unknown as [Record<string, any>];\n    const result = convertApiDataToRdiJobs(jobs);\n    expect(result.job1.name).toBeUndefined();\n    expect(result.job2.name).toBeUndefined();\n  });\n\n  it('should ignore jobs without a name property', () => {\n    const jobWithoutName = {\n      source: {\n        redis: {},\n      },\n      transform: [],\n      output: [],\n    };\n    const jobs = [jobWithoutName, job2] as unknown as [Record<string, any>];\n    const result = convertApiDataToRdiJobs(jobs);\n    expect(result).toEqual({\n      [job2.name]: { ...job2, name: undefined },\n    });\n  });\n});\n\ndescribe('convertApiDataToRdiPipeline', () => {\n  it('should return an RdiPipeline object with jobs converted from API data', () => {\n    const apiData = {\n      targets: {\n        target: {},\n      },\n      jobs: [job1, job2],\n      sources: { psql: {} },\n      processors: {},\n    };\n\n    const expectedPipeline: RdiPipeline = Object.assign(new RdiPipeline(), {\n      config: {\n        targets: {\n          target: {},\n        },\n        sources: { psql: {} },\n        processors: undefined,\n      },\n      jobs: {\n        [job1.name]: { ...job1, name: undefined },\n        [job2.name]: { ...job2, name: undefined },\n      },\n    });\n\n    const actualPipeline = convertApiDataToRdiPipeline(apiData);\n\n    expect(actualPipeline).toEqual(expectedPipeline);\n  });\n\n  it('should return an RdiPipeline object with empty jobs array if no jobs in API data', () => {\n    const apiData = {\n      targets: {\n        target: {},\n      },\n      sources: { psql: {} },\n      processors: {},\n    };\n\n    const expectedPipeline: RdiPipeline = Object.assign(new RdiPipeline(), {\n      jobs: {},\n      config: {\n        targets: {\n          target: {},\n        },\n        sources: { psql: {} },\n        processors: undefined,\n      },\n    });\n\n    const actualPipeline = convertApiDataToRdiPipeline(apiData);\n\n    expect(actualPipeline).toEqual(expectedPipeline);\n  });\n\n  it('should return an RdiPipeline object with additional data from API data', () => {\n    const apiData = {\n      targets: {\n        target: {},\n      },\n      jobs: [job1, job2],\n      sources: { psql: {} },\n      processors: {},\n    };\n\n    const expectedPipeline: RdiPipeline = Object.assign(new RdiPipeline(), {\n      config: {\n        targets: {\n          target: {},\n        },\n        sources: { psql: {} },\n        processors: undefined,\n      },\n      jobs: {\n        job1: { ...job1, name: undefined },\n        job2: { ...job2, name: undefined },\n      },\n    });\n\n    const actualPipeline = convertApiDataToRdiPipeline(apiData);\n\n    expect(actualPipeline).toBeInstanceOf(RdiPipeline);\n    expect(actualPipeline.config).toBeTruthy();\n    expect(actualPipeline.jobs.job1).toBeTruthy();\n    expect(Object.keys(actualPipeline.jobs)).toStrictEqual([\n      job1.name,\n      job2.name,\n    ]);\n    expect(actualPipeline).toEqual(expectedPipeline);\n  });\n});\n\ndescribe('convertRdiJobsToApiPayload', () => {\n  it('should convert an object of jobs to an array of payloads', () => {\n    const jobs = {\n      job1: {\n        id: 1,\n        title: 'Job 1',\n        description: 'This is job 1',\n      },\n      job2: {\n        id: 2,\n        title: 'Job 2',\n        description: 'This is job 2',\n      },\n    };\n\n    const expectedPayload = [\n      {\n        id: 1,\n        title: 'Job 1',\n        description: 'This is job 1',\n        name: 'job1',\n      },\n      {\n        id: 2,\n        title: 'Job 2',\n        description: 'This is job 2',\n        name: 'job2',\n      },\n    ];\n\n    const result = convertRdiJobsToApiPayload(jobs);\n\n    expect(result).toEqual(expectedPayload);\n  });\n\n  it('should return an empty array if no jobs are provided', () => {\n    const jobs = {};\n\n    const expectedPayload = [];\n\n    const result = convertRdiJobsToApiPayload(jobs);\n\n    expect(result).toEqual(expectedPayload);\n  });\n});\n\ndescribe('convertRdiPipelineToApiPayload', () => {\n  it('should convert RdiPipeline to API payload', () => {\n    const pipeline: RdiPipeline = Object.assign(new RdiPipeline(), {\n      config: {\n        name: 'my-pipeline',\n        description: 'This is my pipeline',\n      },\n      jobs: {\n        job1: { ...job1, name: undefined },\n        job2: { ...job2, name: undefined },\n      },\n    });\n\n    const expectedPayload = {\n      name: 'my-pipeline',\n      description: 'This is my pipeline',\n      jobs: [job1, job2],\n    };\n\n    const result = convertRdiPipelineToApiPayload(pipeline);\n\n    expect(result).toEqual(expectedPayload);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/utils/pipeline.util.ts",
    "content": "import { isArray, unset, set, forEach, isObjectLike, isEmpty } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport { RdiPipeline } from 'src/modules/rdi/models';\n\nexport const convertApiDataToRdiJobs = (\n  jobs?: [Record<string, any>],\n): Record<string, any> => {\n  const jobsMap = {};\n\n  if (jobs && isArray(jobs)) {\n    jobs.forEach((job) => {\n      if (job?.name) {\n        jobsMap[job.name] = {\n          ...job,\n          name: undefined, // do not show name in the config area\n        };\n      }\n    });\n  }\n\n  return jobsMap;\n};\n\nexport const convertApiDataToRdiPipeline = (\n  data: Record<string, any> = {},\n): RdiPipeline => {\n  const entries = data;\n\n  // ignore empty root-level entries for pipeline\n  forEach(data, (entry, key) => {\n    if (entry && isObjectLike(entry) && isEmpty(entry)) {\n      entries[key] = undefined;\n    }\n  });\n\n  const pipeline = {\n    config: {\n      ...entries,\n    },\n    jobs: convertApiDataToRdiJobs(data.jobs),\n  };\n\n  // do not show jobs in the config area\n  unset(pipeline, 'config.jobs');\n\n  return plainToInstance(RdiPipeline, pipeline, {\n    excludeExtraneousValues: true,\n  });\n};\n\nexport const convertRdiJobsToApiPayload = (\n  jobs: Record<string, any>,\n): Record<string, any>[] => {\n  const payload = [];\n\n  forEach(jobs, (job, key) => {\n    payload.push({\n      ...job,\n      name: key,\n    });\n  });\n\n  return payload;\n};\n\nexport const convertRdiPipelineToApiPayload = (\n  pipeline: RdiPipeline,\n): Record<string, any> => {\n  const payload = {\n    ...pipeline.config,\n  };\n\n  set(payload, 'jobs', convertRdiJobsToApiPayload(pipeline.jobs));\n  return payload;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/utils/transformer.util.spec.ts",
    "content": "import {\n  RdiStatisticsBlocksSection,\n  RdiStatisticsInfoSection,\n  RdiStatisticsTableSection,\n  RdiStatisticsViewType,\n} from 'src/modules/rdi/models';\nimport {\n  generateHeaderFromFieldName,\n  generateColumns,\n  hasData,\n} from './transformer.util';\n\ndescribe('transformer.util', () => {\n  describe('generateHeaderFromFieldName', () => {\n    it('should handle simple lowercase word', () => {\n      expect(generateHeaderFromFieldName('user')).toBe('User');\n    });\n\n    it('should handle simple uppercase word', () => {\n      expect(generateHeaderFromFieldName('ID')).toBe('Id');\n    });\n\n    it('should handle snake_case', () => {\n      expect(generateHeaderFromFieldName('age_sec')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('total_batches')).toBe(\n        'Total batches',\n      );\n      expect(generateHeaderFromFieldName('batch_size_avg')).toBe(\n        'Batch size avg',\n      );\n    });\n\n    it('should handle kebab-case', () => {\n      expect(generateHeaderFromFieldName('age-sec')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('total-batches')).toBe(\n        'Total batches',\n      );\n      expect(generateHeaderFromFieldName('batch-size-avg')).toBe(\n        'Batch size avg',\n      );\n    });\n\n    it('should handle camelCase', () => {\n      expect(generateHeaderFromFieldName('ageSec')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('totalBatches')).toBe('Total batches');\n      expect(generateHeaderFromFieldName('batchSizeAvg')).toBe(\n        'Batch size avg',\n      );\n    });\n\n    it('should handle PascalCase', () => {\n      expect(generateHeaderFromFieldName('AgeSec')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('TotalBatches')).toBe('Total batches');\n      expect(generateHeaderFromFieldName('BatchSizeAvg')).toBe(\n        'Batch size avg',\n      );\n    });\n\n    it('should handle space-separated words', () => {\n      expect(generateHeaderFromFieldName('age sec')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('total batches')).toBe(\n        'Total batches',\n      );\n    });\n\n    it('should handle mixed separators', () => {\n      expect(generateHeaderFromFieldName('age_sec-avg')).toBe('Age sec avg');\n      expect(generateHeaderFromFieldName('total_batches-per-sec')).toBe(\n        'Total batches per sec',\n      );\n    });\n\n    it('should handle multiple spaces', () => {\n      expect(generateHeaderFromFieldName('age  sec')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('total   batches')).toBe(\n        'Total batches',\n      );\n    });\n\n    it('should handle leading/trailing spaces', () => {\n      expect(generateHeaderFromFieldName(' age_sec ')).toBe('Age sec');\n      expect(generateHeaderFromFieldName('  user  ')).toBe('User');\n    });\n\n    it('should handle single character', () => {\n      expect(generateHeaderFromFieldName('a')).toBe('A');\n      expect(generateHeaderFromFieldName('x')).toBe('X');\n    });\n\n    it('should handle empty string', () => {\n      expect(generateHeaderFromFieldName('')).toBe('');\n    });\n\n    it('should handle numbers in field names', () => {\n      expect(generateHeaderFromFieldName('field1')).toBe('Field1');\n      expect(generateHeaderFromFieldName('age_sec_2')).toBe('Age sec 2');\n      expect(generateHeaderFromFieldName('test_123_field')).toBe(\n        'Test 123 field',\n      );\n    });\n\n    it('should handle real-world examples from RDI statistics', () => {\n      expect(generateHeaderFromFieldName('id')).toBe('Id');\n      expect(generateHeaderFromFieldName('addr')).toBe('Addr');\n      expect(generateHeaderFromFieldName('idle_sec')).toBe('Idle sec');\n      expect(generateHeaderFromFieldName('last_arrival')).toBe('Last arrival');\n      expect(generateHeaderFromFieldName('total_batches')).toBe(\n        'Total batches',\n      );\n      expect(generateHeaderFromFieldName('batch_size_avg')).toBe(\n        'Batch size avg',\n      );\n      expect(generateHeaderFromFieldName('read_time_avg')).toBe(\n        'Read time avg',\n      );\n      expect(generateHeaderFromFieldName('process_time_avg')).toBe(\n        'Process time avg',\n      );\n      expect(generateHeaderFromFieldName('ack_time_avg')).toBe('Ack time avg');\n      expect(generateHeaderFromFieldName('total_time_avg')).toBe(\n        'Total time avg',\n      );\n      expect(generateHeaderFromFieldName('rec_per_sec_avg')).toBe(\n        'Rec per sec avg',\n      );\n    });\n  });\n\n  describe('generateColumns', () => {\n    it('should return empty array for empty data', () => {\n      expect(generateColumns([])).toEqual([]);\n    });\n\n    it('should return empty array for null/undefined data', () => {\n      expect(generateColumns(null as any)).toEqual([]);\n      expect(generateColumns(undefined as any)).toEqual([]);\n    });\n\n    it('should auto-generate columns from data keys', () => {\n      const data = [{ id: '1', user_name: 'John', age_sec: 30 }];\n      const result = generateColumns(data);\n\n      expect(result).toEqual([\n        { id: 'id', header: 'Id' },\n        { id: 'user_name', header: 'User name' },\n        { id: 'age_sec', header: 'Age sec' },\n      ]);\n    });\n\n    it('should use custom header when provided as string', () => {\n      const data = [{ id: '1', user_name: 'John' }];\n      const customColumns = { user_name: 'Username' };\n      const result = generateColumns(data, customColumns);\n\n      expect(result).toEqual([\n        { id: 'id', header: 'Id' },\n        { id: 'user_name', header: 'Username' },\n      ]);\n    });\n\n    it('should use custom header when provided as object', () => {\n      const data = [{ id: '1', host_port: 'localhost:6379' }];\n      const customColumns = { host_port: { header: 'Host:port' } };\n      const result = generateColumns(data, customColumns);\n\n      expect(result).toEqual([\n        { id: 'id', header: 'Id' },\n        { id: 'host_port', header: 'Host:port' },\n      ]);\n    });\n\n    it('should include type when provided', () => {\n      const data = [{ status: 'connected', name: 'test' }];\n      const customColumns = {\n        status: { header: 'Status', type: 'status' },\n      };\n      const result = generateColumns(data, customColumns);\n\n      expect(result).toEqual([\n        { id: 'status', header: 'Status', type: 'status' },\n        { id: 'name', header: 'Name' },\n      ]);\n    });\n\n    it('should auto-generate header when only type is provided', () => {\n      const data = [{ connection_status: 'active' }];\n      const customColumns = { connection_status: { type: 'status' } };\n      const result = generateColumns(data, customColumns);\n\n      expect(result).toEqual([\n        {\n          id: 'connection_status',\n          header: 'Connection status',\n          type: 'status',\n        },\n      ]);\n    });\n\n    it('should handle mixed custom column configurations', () => {\n      const data = [\n        { id: '1', status: 'ok', user_name: 'John', host_port: 'localhost' },\n      ];\n      const customColumns = {\n        status: { type: 'status' },\n        user_name: 'Username',\n        host_port: { header: 'Host:port' },\n      };\n      const result = generateColumns(data, customColumns);\n\n      expect(result).toEqual([\n        { id: 'id', header: 'Id' },\n        { id: 'status', header: 'Status', type: 'status' },\n        { id: 'user_name', header: 'Username' },\n        { id: 'host_port', header: 'Host:port' },\n      ]);\n    });\n\n    it('should preserve field order from data', () => {\n      const data = [{ z_field: 1, a_field: 2, m_field: 3 }];\n      const result = generateColumns(data);\n\n      expect(result.map((c) => c.id)).toEqual([\n        'z_field',\n        'a_field',\n        'm_field',\n      ]);\n    });\n  });\n\n  describe('hasData', () => {\n    describe('table sections', () => {\n      it('should return true when table section has columns and data', () => {\n        const section: RdiStatisticsTableSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Table,\n          columns: [{ id: 'name', header: 'Name' }],\n          data: [{ name: 'test' }],\n        };\n\n        expect(hasData(section)).toBe(true);\n      });\n\n      it('should return false when table section has empty columns', () => {\n        const section: RdiStatisticsTableSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Table,\n          columns: [],\n          data: [{ name: 'test' }],\n        };\n\n        expect(hasData(section)).toBe(false);\n      });\n\n      it('should return false when table section has empty data', () => {\n        const section: RdiStatisticsTableSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Table,\n          columns: [{ id: 'name', header: 'Name' }],\n          data: [],\n        };\n\n        expect(hasData(section)).toBe(false);\n      });\n\n      it('should return false when table section has both empty columns and data', () => {\n        const section: RdiStatisticsTableSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Table,\n          columns: [],\n          data: [],\n        };\n\n        expect(hasData(section)).toBe(false);\n      });\n    });\n\n    describe('blocks sections', () => {\n      it('should return true when blocks section has data', () => {\n        const section: RdiStatisticsBlocksSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Blocks,\n          data: [{ label: 'Count', value: 10, units: 'Total' }],\n        };\n\n        expect(hasData(section)).toBe(true);\n      });\n\n      it('should return false when blocks section has empty data', () => {\n        const section: RdiStatisticsBlocksSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Blocks,\n          data: [],\n        };\n\n        expect(hasData(section)).toBe(false);\n      });\n    });\n\n    describe('info sections', () => {\n      it('should return true for info sections regardless of data', () => {\n        const section: RdiStatisticsInfoSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Info,\n          data: [],\n        };\n\n        expect(hasData(section)).toBe(true);\n      });\n\n      it('should return true for info sections with data', () => {\n        const section: RdiStatisticsInfoSection = {\n          name: 'Test',\n          view: RdiStatisticsViewType.Info,\n          data: [{ label: 'Version', value: '1.0.0' }],\n        };\n\n        expect(hasData(section)).toBe(true);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/rdi/utils/transformer.util.ts",
    "content": "import {\n  RdiStatisticsBlocksSection,\n  RdiStatisticsSection,\n  RdiStatisticsTableSection,\n  RdiStatisticsViewType,\n} from 'src/modules/rdi/models';\n\ninterface ColumnConfig {\n  header?: string;\n  type?: string;\n}\n\ninterface Column {\n  id: string;\n  header: string;\n  type?: string;\n}\n\n/**\n * Checks if a statistics section has meaningful data to display.\n * Returns false for table sections with empty columns or data arrays.\n * Returns false for blocks sections with empty data arrays.\n * Info sections are always considered to have data.\n */\nexport const hasData = (section: RdiStatisticsSection): boolean => {\n  if (section.view === RdiStatisticsViewType.Table) {\n    const tableSection = section as RdiStatisticsTableSection;\n    return tableSection.columns?.length > 0 && tableSection.data?.length > 0;\n  }\n\n  if (section.view === RdiStatisticsViewType.Blocks) {\n    const blocksSection = section as RdiStatisticsBlocksSection;\n    return blocksSection.data?.length > 0;\n  }\n\n  // Info sections are always shown\n  return true;\n};\n\n/**\n * Generates a human-readable header from a field name\n * Handles snake_case, kebab-case, camelCase, PascalCase, and space-separated\n * Examples:\n *   'id' -> 'Id'\n *   'age_sec' -> 'Age sec'\n *   'age-sec' -> 'Age sec'\n *   'ageSec' -> 'Age sec'\n *   'user' -> 'User'\n */\nexport const generateHeaderFromFieldName = (fieldName: string): string => {\n  const normalized = fieldName\n    .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase -> camel Case\n    .replace(/[_-]/g, ' ') // snake_case, kebab-case -> space separated\n    .toLowerCase() // convert all to lowercase\n    .replace(/\\s+/g, ' ') // normalize multiple spaces\n    .trim();\n\n  // Capitalize only the first letter\n  return normalized.charAt(0).toUpperCase() + normalized.slice(1);\n};\n\n/**\n * Generates column definitions from data entries\n * @param data - Array of data objects\n * @param customColumns - Optional custom column definitions to override auto-generated ones\n *                        Can be a string (header only) or an object with header and type\n */\nexport const generateColumns = (\n  data: Record<string, unknown>[],\n  customColumns?: Record<string, string | ColumnConfig>,\n): Column[] => {\n  if (!data?.length) {\n    return [];\n  }\n\n  return Object.keys(data[0]).map((fieldName) => {\n    const customConfig = customColumns?.[fieldName];\n    const config: ColumnConfig =\n      typeof customConfig === 'string'\n        ? { header: customConfig }\n        : customConfig || {};\n\n    return {\n      id: fieldName,\n      header: config.header || generateHeaderFromFieldName(fieldName),\n      ...(config.type && { type: config.type }),\n    };\n  });\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts",
    "content": "import { when, resetAllWhenMocks } from 'jest-when';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { mockRedisNoAuthError, mockStandaloneRedisClient } from 'src/__mocks__';\nimport { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider';\nimport { RedisClientConnectionType } from 'src/modules/redis/client';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\n\nconst mockRedisMemoryInfoResponse1: string =\n  '# Memory\\r\\nnumber_of_cached_scripts:10\\r\\n';\nconst mockRedisMemoryInfoResponse2: string =\n  '# Memory\\r\\nnumber_of_cached_scripts:11\\r\\n';\n\nconst mockRedisKeyspaceInfoResponse1: string =\n  '# Keyspace\\r\\ndb0:keys=2,expires=0,avg_ttl=0\\r\\n';\nconst mockRedisKeyspaceInfoResponse2: string = `# Keyspace\\r\\ndb0:keys=2,expires=0,avg_ttl=0\\r\\n\n  db1:keys=0,expires=0,avg_ttl=0\\r\\n`;\nconst mockRedisKeyspaceInfoResponse3: string = `# Keyspace\\r\\ndb0:keys=2,expires=0,avg_ttl=0\\r\\n\n  db2:keys=20,expires=0,avg_ttl=0\\r\\n`;\n\nconst mockRedisConfigResponse = ['name', '512'];\n\nconst mockRedisClientsResponse1: string =\n  '# Clients\\r\\nconnected_clients:100\\r\\n';\nconst mockRedisClientsResponse2: string =\n  '# Clients\\r\\nconnected_clients:101\\r\\n';\n\nconst mockRedisServerResponse1: string = '# Server\\r\\nredis_version:7.4.0\\r\\n';\nconst mockRedisServerResponse2: string = '# Server\\r\\nredis_version:6.0.0\\r\\n';\n\nconst mockRedisAclListResponse1: string[] = [\n  'user <pass off resetchannels -@all',\n  'user default on #d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1 ~* &* +@all',\n];\nconst mockRedisAclListResponse2: string[] = [\n  ...mockRedisAclListResponse1,\n  'user test_2 on nopass ~* &* +@all',\n];\n\nconst mockFTListResponse1 = [];\nconst mockFTListResponse2 = ['idx'];\n\nconst mockKeys = [\n  {\n    name: Buffer.from('name'),\n    type: 'string',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'hash',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'stream',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'set',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'zset',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'ReJSON-RL',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'graphdata',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n  {\n    name: Buffer.from('name'),\n    type: 'TSDB-TYPE',\n    length: 10,\n    memory: 10,\n    ttl: -1,\n  },\n];\n\nconst mockBigHashKey = {\n  name: Buffer.from('name'),\n  type: 'hash',\n  length: 5001,\n  memory: 10,\n  ttl: -1,\n};\n\nconst mockBigHashKey3 = {\n  name: Buffer.from('name'),\n  type: 'hash',\n  length: 513,\n  memory: 10,\n  ttl: -1,\n};\n\nconst mockBigStringKey = {\n  name: Buffer.from('name'),\n  type: 'string',\n  length: 10,\n  memory: 201,\n  ttl: -1,\n};\n\nconst mockHugeStringKey = {\n  name: Buffer.from('name'),\n  type: 'string',\n  length: 10,\n  memory: 100_001,\n  ttl: -1,\n};\n\nconst mockBigSet = {\n  name: Buffer.from('name'),\n  type: 'set',\n  length: 513,\n  memory: 10,\n  ttl: -1,\n};\n\nconst mockHugeSet = {\n  name: Buffer.from('name'),\n  type: 'set',\n  length: 1_001,\n  memory: 10,\n  ttl: -1,\n};\n\nconst mockBigZSetKey = {\n  name: Buffer.from('name'),\n  type: 'zset',\n  length: 513,\n  memory: 10,\n  ttl: -1,\n};\n\nconst mockBigListKey = {\n  name: Buffer.from('name'),\n  type: 'list',\n  length: 1001,\n  memory: 10,\n  ttl: -1,\n};\n\nconst mockSmallStringKey = {\n  name: Buffer.from('name'),\n  type: 'string',\n  length: 10,\n  memory: 199,\n  ttl: -1,\n};\n\nconst mockSearchHashes = new Array(51).fill(mockBigHashKey);\n\nconst generateRTSRecommendationTests = [\n  { input: ['0', ['123', 123]], expected: null },\n  {\n    input: ['0', ['1234567891', 3]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  {\n    input: ['0', ['1234567891', 1234567891]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  {\n    input: ['0', ['123', 1234567891]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  { input: ['0', ['123', 12345678911]], expected: null },\n  {\n    input: ['0', ['123', 1234567891234]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  { input: ['0', ['123', 12345678912345]], expected: null },\n  {\n    input: ['0', ['123', 1234567891234567]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  { input: ['0', ['12345678912345678', 1]], expected: null },\n  {\n    input: ['0', ['1234567891234567891', 1]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  {\n    input: ['0', ['1', 1234567891.2]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  {\n    input: ['0', ['1234567891.2', 1]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  {\n    input: ['0', ['1234567891:12', 1]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  {\n    input: ['0', ['1234567891a12', 1]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  { input: ['0', ['1234567891.2.2', 1]], expected: null },\n  { input: ['0', ['1234567891asd', 1]], expected: null },\n  {\n    input: ['0', ['10-10-2020', 1]],\n    expected: {\n      name: RECOMMENDATION_NAMES.RTS,\n      params: { keys: [Buffer.from('name')] },\n    },\n  },\n  { input: ['0', ['', 1]], expected: null },\n  { input: ['0', ['1', -12]], expected: null },\n  { input: ['0', ['1', -1234567891]], expected: null },\n  { input: ['0', ['1', -1234567891.123]], expected: null },\n  { input: ['0', ['1', -1234567891.123]], expected: null },\n  { input: ['0', ['1234567891.-123', 1]], expected: null },\n];\n\nconst mockSortedSets = new Array(101).fill({\n  name: Buffer.from('name'),\n  type: 'zset',\n  length: 10,\n  memory: 10,\n  ttl: -1,\n});\n\nconst mockZScanResponse2 = ['0', ['12345678910', 12345678910, 1, 1]];\n\nconst mockZScanResponse1 = ['0', ['1', 1, '12345678910', 12345678910]];\n\ndescribe('RecommendationProvider', () => {\n  const client = mockStandaloneRedisClient;\n  const service = new RecommendationProvider();\n\n  describe('determineLuaScriptRecommendation', () => {\n    it('should not return luaScript recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisMemoryInfoResponse1),\n      );\n\n      const luaScriptRecommendation =\n        await service.determineLuaScriptRecommendation(client);\n      expect(luaScriptRecommendation).toEqual(null);\n    });\n\n    it('should return luaScript recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisMemoryInfoResponse2),\n      );\n      const luaScriptRecommendation =\n        await service.determineLuaScriptRecommendation(client);\n      expect(luaScriptRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.LUA_SCRIPT,\n      });\n    });\n\n    it('should not return luaScript recommendation when info command executed with error', async () => {\n      when(client.getInfo).mockRejectedValue('some error');\n\n      const luaScriptRecommendation =\n        await service.determineLuaScriptRecommendation(client);\n      expect(luaScriptRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineBigHashesRecommendation', () => {\n    it('should not return bigHashes recommendation', async () => {\n      const bigHashesRecommendation =\n        await service.determineBigHashesRecommendation(mockKeys);\n      expect(bigHashesRecommendation).toEqual(null);\n    });\n    it('should return bigHashes recommendation', async () => {\n      const bigHashesRecommendation =\n        await service.determineBigHashesRecommendation([\n          ...mockKeys,\n          mockBigHashKey,\n        ]);\n      expect(bigHashesRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.BIG_HASHES,\n        params: { keys: [mockBigHashKey.name] },\n      });\n    });\n  });\n\n  describe('determineBigTotalRecommendation', () => {\n    it('should not return useSmallerKeys recommendation', async () => {\n      const bigTotalRecommendation =\n        await service.determineBigTotalRecommendation(1);\n      expect(bigTotalRecommendation).toEqual(null);\n    });\n    it('should return useSmallerKeys recommendation', async () => {\n      const bigTotalRecommendation =\n        await service.determineBigTotalRecommendation(1_000_001);\n      expect(bigTotalRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.USE_SMALLER_KEYS,\n      });\n    });\n  });\n\n  describe('determineLogicalDatabasesRecommendation', () => {\n    it('should not return avoidLogicalDatabases recommendation when only one logical db', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisKeyspaceInfoResponse1),\n      );\n\n      const avoidLogicalDatabasesRecommendation =\n        await service.determineLogicalDatabasesRecommendation(client);\n      expect(avoidLogicalDatabasesRecommendation).toEqual(null);\n    });\n\n    it('should not return avoidLogicalDatabases recommendation when only on logical db with keys', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisKeyspaceInfoResponse2),\n      );\n\n      const avoidLogicalDatabasesRecommendation =\n        await service.determineLogicalDatabasesRecommendation(client);\n      expect(avoidLogicalDatabasesRecommendation).toEqual(null);\n    });\n\n    it('should return avoidLogicalDatabases recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisKeyspaceInfoResponse3),\n      );\n\n      const avoidLogicalDatabasesRecommendation =\n        await service.determineLogicalDatabasesRecommendation(client);\n      expect(avoidLogicalDatabasesRecommendation).toEqual({\n        name: 'avoidLogicalDatabases',\n      });\n    });\n\n    it('should not return avoidLogicalDatabases recommendation when info command executed with error', async () => {\n      when(client.getInfo).mockRejectedValue('some error');\n\n      const avoidLogicalDatabasesRecommendation =\n        await service.determineLogicalDatabasesRecommendation(client);\n      expect(avoidLogicalDatabasesRecommendation).toEqual(null);\n    });\n\n    it('should not return avoidLogicalDatabases recommendation when isCluster', async () => {\n      client.getConnectionType = jest\n        .fn()\n        .mockReturnValueOnce(RedisClientConnectionType.CLUSTER);\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisKeyspaceInfoResponse3),\n      );\n\n      const avoidLogicalDatabasesRecommendation =\n        await service.determineLogicalDatabasesRecommendation(client);\n      expect(avoidLogicalDatabasesRecommendation).toEqual(null);\n      // nodeClient.isCluster = false;\n    });\n  });\n\n  describe('determineCombineSmallStringsToHashesRecommendation', () => {\n    it('should not return combineSmallStringsToHashes recommendation', async () => {\n      const smallStringRecommendation =\n        await service.determineCombineSmallStringsToHashesRecommendation(\n          new Array(9).fill(mockSmallStringKey),\n        );\n      expect(smallStringRecommendation).toEqual(null);\n    });\n    it('should not return combineSmallStringsToHashes recommendation when strings are big', async () => {\n      const smallStringRecommendation =\n        await service.determineCombineSmallStringsToHashesRecommendation(\n          new Array(10).fill(mockBigStringKey),\n        );\n      expect(smallStringRecommendation).toEqual(null);\n    });\n    it('should return combineSmallStringsToHashes recommendation', async () => {\n      const smallStringRecommendation =\n        await service.determineCombineSmallStringsToHashesRecommendation(\n          new Array(10).fill(mockSmallStringKey),\n        );\n      expect(smallStringRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES,\n        params: { keys: [Buffer.from('name')] },\n      });\n    });\n  });\n\n  describe('determineIncreaseSetMaxIntsetEntriesRecommendation', () => {\n    it('should not return increaseSetMaxIntsetEntries', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockResolvedValue(mockRedisConfigResponse);\n\n      const increaseSetMaxIntsetEntriesRecommendation =\n        await service.determineIncreaseSetMaxIntsetEntriesRecommendation(\n          client,\n          mockKeys,\n        );\n      expect(increaseSetMaxIntsetEntriesRecommendation).toEqual(null);\n    });\n\n    it('should return increaseSetMaxIntsetEntries recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockResolvedValue(mockRedisConfigResponse);\n\n      const increaseSetMaxIntsetEntriesRecommendation =\n        await service.determineIncreaseSetMaxIntsetEntriesRecommendation(\n          client,\n          [...mockKeys, mockBigSet],\n        );\n      expect(increaseSetMaxIntsetEntriesRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES,\n        params: { keys: [mockBigSet.name] },\n      });\n    });\n\n    it('should not return increaseSetMaxIntsetEntries recommendation when config command executed with error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockRejectedValue('some error');\n\n      const increaseSetMaxIntsetEntriesRecommendation =\n        await service.determineIncreaseSetMaxIntsetEntriesRecommendation(\n          client,\n          mockKeys,\n        );\n      expect(increaseSetMaxIntsetEntriesRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineHashHashtableToZiplistRecommendation', () => {\n    it('should not return hashHashtableToZiplist recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockResolvedValue(mockRedisConfigResponse);\n\n      const convertHashtableToZiplistRecommendation =\n        await service.determineHashHashtableToZiplistRecommendation(\n          client,\n          mockKeys,\n        );\n      expect(convertHashtableToZiplistRecommendation).toEqual(null);\n    });\n\n    it('should return hashHashtableToZiplist recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockResolvedValue(mockRedisConfigResponse);\n\n      const convertHashtableToZiplistRecommendation =\n        await service.determineHashHashtableToZiplistRecommendation(client, [\n          ...mockKeys,\n          mockBigHashKey3,\n        ]);\n      expect(convertHashtableToZiplistRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST,\n        params: { keys: [mockBigHashKey3.name] },\n      });\n    });\n\n    it('should not return hashHashtableToZiplist recommendation when config command executed with error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockRejectedValue('some error');\n\n      const convertHashtableToZiplistRecommendation =\n        await service.determineHashHashtableToZiplistRecommendation(\n          client,\n          mockKeys,\n        );\n      expect(convertHashtableToZiplistRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineCompressionForListRecommendation', () => {\n    it('should not return compressionForList recommendation', async () => {\n      const compressHashFieldNamesRecommendation =\n        await service.determineCompressionForListRecommendation(mockKeys);\n      expect(compressHashFieldNamesRecommendation).toEqual(null);\n    });\n    it('should return compressionForList recommendation', async () => {\n      const compressHashFieldNamesRecommendation =\n        await service.determineCompressionForListRecommendation([\n          mockBigListKey,\n        ]);\n      expect(compressHashFieldNamesRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n        params: { keys: [mockBigListKey.name] },\n      });\n    });\n  });\n\n  describe('determineBigStringsRecommendation', () => {\n    it('should not return bigStrings recommendation', async () => {\n      const bigStringsRecommendation =\n        await service.determineBigStringsRecommendation(mockKeys);\n      expect(bigStringsRecommendation).toEqual(null);\n    });\n    it('should return bigStrings recommendation', async () => {\n      const bigStringsRecommendation =\n        await service.determineBigStringsRecommendation([mockHugeStringKey]);\n      expect(bigStringsRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.BIG_STRINGS,\n        params: { keys: [mockHugeStringKey.name] },\n      });\n    });\n  });\n\n  describe('determineZSetHashtableToZiplistRecommendation', () => {\n    it('should not return zSetHashtableToZiplist recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockResolvedValue(mockRedisConfigResponse);\n\n      const zSetHashtableToZiplistRecommendation =\n        await service.determineZSetHashtableToZiplistRecommendation(\n          client,\n          mockKeys,\n        );\n      expect(zSetHashtableToZiplistRecommendation).toEqual(null);\n    });\n\n    it('should return zSetHashtableToZiplist recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockResolvedValue(mockRedisConfigResponse);\n\n      const zSetHashtableToZiplistRecommendation =\n        await service.determineZSetHashtableToZiplistRecommendation(client, [\n          ...mockKeys,\n          mockBigZSetKey,\n        ]);\n      expect(zSetHashtableToZiplistRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST,\n        params: { keys: [mockBigZSetKey.name] },\n      });\n    });\n\n    it('should not return zSetHashtableToZiplist recommendation when config command executed with error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['config']), expect.anything())\n        .mockRejectedValue('some error');\n\n      const zSetHashtableToZiplistRecommendation =\n        await service.determineZSetHashtableToZiplistRecommendation(\n          client,\n          mockKeys,\n        );\n      expect(zSetHashtableToZiplistRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineBigSetsRecommendation', () => {\n    it('should not return bigSets recommendation', async () => {\n      const bigSetsRecommendation =\n        await service.determineBigSetsRecommendation(mockKeys);\n      expect(bigSetsRecommendation).toEqual(null);\n    });\n    it('should return bigSets recommendation', async () => {\n      const bigSetsRecommendation =\n        await service.determineBigSetsRecommendation([mockHugeSet]);\n      expect(bigSetsRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.BIG_SETS,\n        params: { keys: [mockHugeSet.name] },\n      });\n    });\n  });\n\n  describe('determineConnectionClientsRecommendation', () => {\n    it('should not return connectionClients recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisClientsResponse1),\n      );\n\n      const connectionClientsRecommendation =\n        await service.determineConnectionClientsRecommendation(client);\n      expect(connectionClientsRecommendation).toEqual(null);\n    });\n\n    it('should return connectionClients recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisClientsResponse2),\n      );\n\n      const connectionClientsRecommendation =\n        await service.determineConnectionClientsRecommendation(client);\n      expect(connectionClientsRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n      });\n    });\n\n    it('should not return connectionClients recommendation when info command executed with error', async () => {\n      when(client.getInfo).mockRejectedValue('some error');\n\n      const connectionClientsRecommendation =\n        await service.determineConnectionClientsRecommendation(client);\n      expect(connectionClientsRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineSetPasswordRecommendation', () => {\n    it('should not return setPassword recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['acl']), expect.anything())\n        .mockResolvedValue(mockRedisAclListResponse1);\n\n      const setPasswordRecommendation =\n        await service.determineSetPasswordRecommendation(client);\n      expect(setPasswordRecommendation).toEqual(null);\n    });\n\n    it('should return setPassword recommendation', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['acl']), expect.anything())\n        .mockResolvedValue(mockRedisAclListResponse2);\n\n      const setPasswordRecommendation =\n        await service.determineSetPasswordRecommendation(client);\n      expect(setPasswordRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.SET_PASSWORD,\n      });\n    });\n\n    it('should not return setPassword recommendation when acl command executed with error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['acl']), expect.anything())\n        .mockRejectedValue('some error');\n\n      const setPasswordRecommendation =\n        await service.determineSetPasswordRecommendation(client);\n      expect(setPasswordRecommendation).toEqual(null);\n    });\n\n    it('should not return setPassword recommendation when acl command executed with error', async () => {\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['auth']))\n        .mockRejectedValue(mockRedisNoAuthError);\n\n      const setPasswordRecommendation =\n        await service.determineSetPasswordRecommendation(client);\n      expect(setPasswordRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineRedisVersionRecommendation', () => {\n    it('should not return redis version recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisServerResponse1),\n      );\n\n      const redisVersionRecommendation =\n        await service.determineRedisVersionRecommendation(client);\n      expect(redisVersionRecommendation).toEqual(null);\n    });\n\n    it('should return redis version recommendation', async () => {\n      when(client.getInfo).mockResolvedValue(\n        convertRedisInfoReplyToObject(mockRedisServerResponse2),\n      );\n\n      const redisVersionRecommendation =\n        await service.determineRedisVersionRecommendation(client);\n      expect(redisVersionRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.REDIS_VERSION,\n      });\n    });\n\n    it('should not return redis version recommendation when info command executed with error', async () => {\n      resetAllWhenMocks();\n      when(client.getInfo).mockRejectedValue('some error');\n\n      const redisVersionRecommendation =\n        await service.determineRedisVersionRecommendation(client);\n      expect(redisVersionRecommendation).toEqual(null);\n    });\n  });\n\n  describe('determineSearchJSONRecommendation', () => {\n    it('should not return searchJSON', async () => {\n      const searchJSONRecommendation =\n        await service.determineSearchJSONRecommendation(\n          mockKeys,\n          mockFTListResponse2,\n        );\n      expect(searchJSONRecommendation).toEqual(null);\n    });\n\n    it('should return searchJSON recommendation', async () => {\n      const searchJSONRecommendation =\n        await service.determineSearchJSONRecommendation(\n          mockKeys,\n          mockFTListResponse1,\n        );\n      expect(searchJSONRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.SEARCH_JSON,\n        params: { keys: [Buffer.from('name')] },\n      });\n    });\n\n    it('should return not searchJSON recommendation when there is no JSON key', async () => {\n      const searchJSONRecommendation =\n        await service.determineSearchJSONRecommendation(\n          [mockBigSet],\n          mockFTListResponse1,\n        );\n      expect(searchJSONRecommendation).toEqual(null);\n    });\n\n    it('should return searchJSON recommendation when indexes is undefined', async () => {\n      const searchJSONRecommendation =\n        await service.determineSearchJSONRecommendation(mockKeys, undefined);\n      expect(searchJSONRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.SEARCH_JSON,\n        params: { keys: [Buffer.from('name')] },\n      });\n    });\n  });\n\n  describe('determineSearchHashRecommendation', () => {\n    it('should return searchHash recommendation', async () => {\n      const bigHashesRecommendation =\n        await service.determineSearchHashRecommendation(mockSearchHashes);\n      expect(bigHashesRecommendation).toEqual({\n        name: RECOMMENDATION_NAMES.SEARCH_HASH,\n      });\n    });\n\n    it('should not return searchHash recommendation', async () => {\n      const searchHashRecommendation =\n        await service.determineSearchHashRecommendation(mockKeys);\n      expect(searchHashRecommendation).toEqual(null);\n    });\n\n    it('should not return searchHash recommendation if indexes exists', async () => {\n      const searchHashRecommendationWithIndex =\n        await service.determineSearchHashRecommendation(mockSearchHashes, [\n          'idx',\n        ]);\n      expect(searchHashRecommendationWithIndex).toEqual(null);\n    });\n  });\n\n  describe('determineRTSRecommendation', () => {\n    test.each(generateRTSRecommendationTests)(\n      '%j',\n      async ({ input, expected }) => {\n        when(client.sendCommand)\n          .calledWith(expect.arrayContaining(['zscan']), expect.anything())\n          .mockResolvedValue(input);\n\n        const RTSRecommendation = await service.determineRTSRecommendation(\n          client,\n          mockKeys,\n        );\n        expect(RTSRecommendation).toEqual(expected);\n      },\n    );\n\n    it('should not return RTS recommendation when only 101 sorted set contain timestamp', async () => {\n      let counter = 0;\n      while (counter <= 100) {\n        when(client.sendCommand)\n          .calledWith(expect.arrayContaining(['zscan']), expect.anything())\n          .mockResolvedValueOnce(mockZScanResponse1);\n        counter += 1;\n      }\n\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['zscan']), expect.anything())\n        .mockResolvedValueOnce(mockZScanResponse2);\n\n      const RTSRecommendation = await service.determineRTSRecommendation(\n        client,\n        mockSortedSets,\n      );\n      expect(RTSRecommendation).toEqual(null);\n    });\n\n    it('should not return RTS recommendation when zscan command executed with error', async () => {\n      resetAllWhenMocks();\n      when(client.sendCommand)\n        .calledWith(expect.arrayContaining(['zscan']), expect.anything())\n        .mockRejectedValue('some error');\n\n      const RTSRecommendation = await service.determineRTSRecommendation(\n        client,\n        mockKeys,\n      );\n      expect(RTSRecommendation).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { get } from 'lodash';\nimport * as semverCompare from 'node-version-compare';\nimport { checkTimestamp } from 'src/utils';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { RedisDataType } from 'src/modules/browser/keys/dto';\nimport { Recommendation } from 'src/modules/database-analysis/models/recommendation';\nimport { Key } from 'src/modules/database-analysis/models';\nimport {\n  RedisString,\n  LUA_SCRIPT_RECOMMENDATION_COUNT,\n  BIG_HASHES_RECOMMENDATION_LENGTH,\n  USE_SMALLER_KEYS_RECOMMENDATION_TOTAL,\n  COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION_LENGTH,\n  COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_MEMORY,\n  COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_KEYS_COUNT,\n  COMPRESSION_FOR_LIST_RECOMMENDATION_LENGTH,\n  BIG_SETS_RECOMMENDATION_LENGTH,\n  BIG_AMOUNT_OF_CONNECTED_CLIENTS_RECOMMENDATION_CLIENTS,\n  BIG_STRINGS_RECOMMENDATION_MEMORY,\n  REDIS_VERSION_RECOMMENDATION_VERSION,\n  SEARCH_INDEXES_RECOMMENDATION_KEYS_FOR_CHECK,\n  SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK,\n  SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH,\n  RTS_KEYS_FOR_CHECK,\n} from 'src/common/constants';\nimport { convertMultilineReplyToObject } from 'src/modules/redis/utils';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\n\n@Injectable()\nexport class RecommendationProvider {\n  private logger = new Logger('RecommendationProvider');\n\n  /**\n   * Check lua script recommendation\n   * @param redisClient\n   */\n  async determineLuaScriptRecommendation(\n    redisClient: RedisClient,\n  ): Promise<Recommendation> {\n    try {\n      const info = await redisClient.getInfo('memory');\n      const nodesNumbersOfCachedScripts = get(\n        info,\n        'memory.number_of_cached_scripts',\n      );\n\n      return parseInt(nodesNumbersOfCachedScripts, 10) >\n        LUA_SCRIPT_RECOMMENDATION_COUNT\n        ? { name: RECOMMENDATION_NAMES.LUA_SCRIPT }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine Lua script recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check big hashes recommendation\n   * @param keys\n   */\n  async determineBigHashesRecommendation(keys: Key[]): Promise<Recommendation> {\n    try {\n      const bigHash = keys.find(\n        (key) =>\n          key.type === RedisDataType.Hash &&\n          key.length > BIG_HASHES_RECOMMENDATION_LENGTH,\n      );\n      return bigHash\n        ? {\n            name: RECOMMENDATION_NAMES.BIG_HASHES,\n            params: { keys: [bigHash.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine Big Hashes recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check use smaller keys recommendation\n   * @param total\n   */\n  async determineBigTotalRecommendation(\n    total: number,\n  ): Promise<Recommendation> {\n    return total > USE_SMALLER_KEYS_RECOMMENDATION_TOTAL\n      ? { name: RECOMMENDATION_NAMES.USE_SMALLER_KEYS }\n      : null;\n  }\n\n  /**\n   * Check logical databases recommendation\n   * @param redisClient\n   */\n  async determineLogicalDatabasesRecommendation(\n    redisClient: RedisClient,\n  ): Promise<Recommendation> {\n    if (redisClient.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n      return null;\n    }\n    try {\n      const info = await redisClient.getInfo('keyspace');\n      const keyspace = get(info, 'keyspace', {});\n      const databasesWithKeys = Object.values(keyspace).filter((db) => {\n        const { keys } = convertMultilineReplyToObject(db as string, ',', '=');\n        return parseInt(keys, 10) > 0;\n      });\n      return databasesWithKeys.length > 1\n        ? { name: RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Logical database recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check combine small strings to hashes recommendation\n   * @param keys\n   */\n  async determineCombineSmallStringsToHashesRecommendation(\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const smallString = keys.filter(\n        (key) =>\n          key.type === RedisDataType.String &&\n          key.memory < COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_MEMORY,\n      );\n      return smallString.length >=\n        COMBINE_SMALL_STRINGS_TO_HASHES_RECOMMENDATION_KEYS_COUNT\n        ? {\n            name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES,\n            params: { keys: [smallString[0].name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Combine small strings to hashes recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check increase set max intset entries recommendation\n   * @param keys\n   * @param redisClient\n   */\n  async determineIncreaseSetMaxIntsetEntriesRecommendation(\n    redisClient: RedisClient,\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const [, setMaxIntsetEntries] = (await redisClient.sendCommand(\n        ['config', 'get', 'set-max-intset-entries'],\n        { replyEncoding: 'utf8' },\n      )) as string[];\n\n      if (!setMaxIntsetEntries) {\n        return null;\n      }\n      const setMaxIntsetEntriesNumber = parseInt(setMaxIntsetEntries, 10);\n      const bigSet = keys.find(\n        (key) =>\n          key.type === RedisDataType.Set &&\n          key.length > setMaxIntsetEntriesNumber,\n      );\n      return bigSet\n        ? {\n            name: RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES,\n            params: { keys: [bigSet.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Increase set max intset entries recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n  /**\n   * Check convert hashtable to ziplist recommendation\n   * @param keys\n   * @param redisClient\n   */\n\n  async determineHashHashtableToZiplistRecommendation(\n    redisClient: RedisClient,\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const [, hashMaxZiplistEntries] = (await redisClient.sendCommand(\n        ['config', 'get', 'hash-max-ziplist-entries'],\n        { replyEncoding: 'utf8' },\n      )) as string[];\n      const hashMaxZiplistEntriesNumber = parseInt(hashMaxZiplistEntries, 10);\n      const bigHash = keys.find(\n        (key) =>\n          key.type === RedisDataType.Hash &&\n          key.length > hashMaxZiplistEntriesNumber,\n      );\n      return bigHash\n        ? {\n            name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST,\n            params: { keys: [bigHash.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Convert hashtable to ziplist recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check compress hash field names recommendation\n   * @param keys\n   */\n  async determineCompressHashFieldNamesRecommendation(\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const bigHash = keys.find(\n        (key) =>\n          key.type === RedisDataType.Hash &&\n          key.length > COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION_LENGTH,\n      );\n      return bigHash\n        ? {\n            name: RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES,\n            params: { keys: [bigHash.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Compress hash field names recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check compression for list recommendation\n   * @param keys\n   */\n  async determineCompressionForListRecommendation(\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const bigList = keys.find(\n        (key) =>\n          key.type === RedisDataType.List &&\n          key.length > COMPRESSION_FOR_LIST_RECOMMENDATION_LENGTH,\n      );\n      return bigList\n        ? {\n            name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n            params: { keys: [bigList.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Compression for list recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check big strings recommendation\n   * @param keys\n   */\n  async determineBigStringsRecommendation(\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const bigString = keys.find(\n        (key) =>\n          key.type === RedisDataType.String &&\n          key.memory > BIG_STRINGS_RECOMMENDATION_MEMORY,\n      );\n      return bigString\n        ? {\n            name: RECOMMENDATION_NAMES.BIG_STRINGS,\n            params: { keys: [bigString.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine Big strings recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check zSet hashtable to ziplist recommendation\n   * @param keys\n   * @param redisClient\n   */\n\n  async determineZSetHashtableToZiplistRecommendation(\n    redisClient: RedisClient,\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      const [, zSetMaxZiplistEntries] = (await redisClient.sendCommand(\n        ['config', 'get', 'zset-max-ziplist-entries'],\n        { replyEncoding: 'utf8' },\n      )) as string[];\n      const zSetMaxZiplistEntriesNumber = parseInt(zSetMaxZiplistEntries, 10);\n      const bigHash = keys.find(\n        (key) =>\n          key.type === RedisDataType.ZSet &&\n          key.length > zSetMaxZiplistEntriesNumber,\n      );\n      return bigHash\n        ? {\n            name: RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST,\n            params: { keys: [bigHash.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine ZSet hashtable to ziplist recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check big sets recommendation\n   * @param keys\n   */\n\n  async determineBigSetsRecommendation(keys: Key[]): Promise<Recommendation> {\n    try {\n      const bigSet = keys.find(\n        (key) =>\n          key.type === RedisDataType.Set &&\n          key.length > BIG_SETS_RECOMMENDATION_LENGTH,\n      );\n      return bigSet\n        ? {\n            name: RECOMMENDATION_NAMES.BIG_SETS,\n            params: { keys: [bigSet.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine Big sets recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check big connected clients recommendation\n   * @param redisClient\n   */\n\n  async determineConnectionClientsRecommendation(\n    redisClient: RedisClient,\n  ): Promise<Recommendation> {\n    try {\n      const info = await redisClient.getInfo('clients');\n      const connectedClients = parseInt(\n        get(info, 'clients.connected_clients'),\n        10,\n      );\n\n      return connectedClients >\n        BIG_AMOUNT_OF_CONNECTED_CLIENTS_RECOMMENDATION_CLIENTS\n        ? { name: RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS }\n        : null;\n    } catch (err) {\n      this.logger.error(\n        'Can not determine Connection clients recommendation',\n        err,\n      );\n      return null;\n    }\n  }\n\n  /**\n   * Check search JSON recommendation\n   * @param keys\n   * @param indexes\n   */\n  async determineSearchJSONRecommendation(\n    keys: Key[],\n    indexes?: string[],\n  ): Promise<Recommendation> {\n    try {\n      if (indexes?.length) {\n        return null;\n      }\n      const jsonKey = keys.find((key) => key.type === RedisDataType.JSON);\n\n      return jsonKey\n        ? {\n            name: RECOMMENDATION_NAMES.SEARCH_JSON,\n            params: { keys: [jsonKey.name] },\n          }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine search json recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check redis version recommendation\n   * @param redisClient\n   */\n\n  async determineRedisVersionRecommendation(\n    redisClient: RedisClient,\n  ): Promise<Recommendation> {\n    try {\n      const info = await redisClient.getInfo('server');\n      const version = get(info, 'server.redis_version');\n      return semverCompare(version, REDIS_VERSION_RECOMMENDATION_VERSION) >= 0\n        ? null\n        : { name: RECOMMENDATION_NAMES.REDIS_VERSION };\n    } catch (err) {\n      this.logger.error('Can not determine redis version recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check set password recommendation\n   * @param redisClient\n   */\n\n  async determineSetPasswordRecommendation(\n    redisClient: RedisClient,\n  ): Promise<Recommendation> {\n    try {\n      const users = (await redisClient.sendCommand(['acl', 'list'], {\n        replyEncoding: 'utf8',\n      })) as string[];\n\n      const nopassUser = users.some((user) => user.split(' ')[3] === 'nopass');\n\n      return nopassUser ? { name: RECOMMENDATION_NAMES.SET_PASSWORD } : null;\n    } catch (err) {\n      this.logger.error('Can not determine set password recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check search hash recommendation\n   * @param keys\n   * @param indexes\n   */\n\n  async determineSearchHashRecommendation(\n    keys: Key[],\n    indexes?: string[],\n  ): Promise<Recommendation> {\n    try {\n      if (indexes?.length) {\n        return null;\n      }\n      const hashKeys = keys.filter(\n        ({ type, length }) =>\n          type === RedisDataType.Hash &&\n          length > SEARCH_HASH_RECOMMENDATION_KEYS_LENGTH,\n      );\n\n      return hashKeys.length > SEARCH_HASH_RECOMMENDATION_KEYS_FOR_CHECK\n        ? { name: RECOMMENDATION_NAMES.SEARCH_HASH }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine search hash recommendation', err);\n      return null;\n    }\n  }\n\n  /**\n   * Check search indexes recommendation\n   * @param redisClient\n   * @param keys\n   * @param client\n   */\n  // eslint-disable-next-line\n  async determineSearchIndexesRecommendation(\n    redisClient: RedisClient,\n    keys: Key[],\n    client: RedisClient,\n  ): Promise<Recommendation> {\n    try {\n      if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n        const res = await this.determineSearchIndexesForCluster(keys, client);\n        return res\n          ? {\n              name: RECOMMENDATION_NAMES.SEARCH_INDEXES,\n              params: { keys: [res] },\n            }\n          : null;\n      }\n      const res = await this.determineSearchIndexesForStandalone(\n        keys,\n        redisClient,\n      );\n      return res\n        ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES, params: { keys: [res] } }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine search indexes recommendation', err);\n      return null;\n    }\n  }\n\n  private async determineSearchIndexesForCluster(\n    keys: Key[],\n    client: RedisClient,\n  ): Promise<RedisString> {\n    let processedKeysNumber = 0;\n    let keyName;\n    let sortedSetNumber = 0;\n    while (\n      processedKeysNumber < keys.length &&\n      !keyName &&\n      sortedSetNumber <= SEARCH_INDEXES_RECOMMENDATION_KEYS_FOR_CHECK\n    ) {\n      if (keys[processedKeysNumber].type !== RedisDataType.ZSet) {\n        processedKeysNumber += 1;\n      } else {\n        const sortedSetMember = (await client.sendCommand(\n          ['zrange', keys[processedKeysNumber].name, 0, 0],\n          { replyEncoding: 'utf8' },\n        )) as string[];\n        const keyType = (await client.sendCommand(\n          ['type', sortedSetMember[0]],\n          { replyEncoding: 'utf8' },\n        )) as string;\n        if (keyType === RedisDataType.JSON || keyType === RedisDataType.Hash) {\n          keyName = keys[processedKeysNumber].name;\n        }\n        processedKeysNumber += 1;\n        sortedSetNumber += 1;\n      }\n    }\n    return keyName;\n  }\n\n  private async determineSearchIndexesForStandalone(\n    keys: Key[],\n    redisClient: RedisClient,\n  ): Promise<RedisString> {\n    const sortedSets = keys\n      .filter(({ type }) => type === RedisDataType.ZSet)\n      .slice(0, 100);\n\n    const res = await redisClient.sendPipeline(\n      sortedSets.map(({ name }) => ['zrange', name, 0, 0]),\n    );\n\n    const types = await redisClient.sendPipeline(\n      res.map(([, member]) => ['type', member[0]]),\n      { replyEncoding: 'utf8' },\n    );\n\n    const keyIndex = types.findIndex(\n      ([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash,\n    );\n\n    return keyIndex === -1 ? undefined : sortedSets[keyIndex].name;\n  }\n\n  /**\n   * Check RTS recommendation\n   * @param redisClient\n   * @param keys\n   */\n\n  async determineRTSRecommendation(\n    redisClient: RedisClient,\n    keys: Key[],\n  ): Promise<Recommendation> {\n    try {\n      let processedKeysNumber = 0;\n      let timeSeriesKey = null;\n      let sortedSetNumber = 0;\n      while (\n        processedKeysNumber < keys.length &&\n        !timeSeriesKey &&\n        sortedSetNumber <= RTS_KEYS_FOR_CHECK\n      ) {\n        if (keys[processedKeysNumber].type !== RedisDataType.ZSet) {\n          processedKeysNumber += 1;\n        } else {\n          const [, membersArray] = (await redisClient.sendCommand(\n            // get first member-score pair\n            ['zscan', keys[processedKeysNumber].name, '0', 'COUNT', 2],\n            { replyEncoding: 'utf8' },\n          )) as string[];\n          if (\n            checkTimestamp(membersArray[0]) ||\n            checkTimestamp(membersArray[1].toString())\n          ) {\n            timeSeriesKey = keys[processedKeysNumber].name;\n          }\n          processedKeysNumber += 1;\n          sortedSetNumber += 1;\n        }\n      }\n\n      return timeSeriesKey\n        ? { name: RECOMMENDATION_NAMES.RTS, params: { keys: [timeSeriesKey] } }\n        : null;\n    } catch (err) {\n      this.logger.error('Can not determine RTS recommendation', err);\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/recommendation/recommendation.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { RecommendationService } from './recommendation.service';\nimport { RecommendationProvider } from './providers/recommendation.provider';\n\n@Module({\n  providers: [RecommendationService, RecommendationProvider],\n  exports: [RecommendationService],\n})\nexport class RecommendationModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/recommendation/recommendation.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { difference } from 'lodash';\nimport { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider';\nimport { Recommendation } from 'src/modules/database-analysis/models/recommendation';\nimport { RECOMMENDATION_NAMES } from 'src/constants';\nimport { RedisString } from 'src/common/constants';\nimport { Key } from 'src/modules/database-analysis/models';\nimport { RedisClient } from 'src/modules/redis/client';\n\n// TODO: do we need info and libraries?\ninterface RecommendationInput {\n  client?: RedisClient;\n  keys?: Key[];\n  info?: RedisString;\n  total?: number;\n  globalClient?: RedisClient;\n  exclude?: string[];\n  indexes?: string[];\n  libraries?: string[];\n}\n\n@Injectable()\nexport class RecommendationService {\n  constructor(\n    private readonly recommendationProvider: RecommendationProvider,\n  ) {}\n\n  /**\n   * Get recommendations\n   * @param dto\n   */\n  public async getRecommendations(\n    dto: RecommendationInput,\n  ): Promise<Recommendation[]> {\n    // generic solution, if somewhere we will sent info, we don't need determined some recommendations\n    const { client, keys, total, globalClient, exclude, indexes } = dto;\n\n    const recommendations: Map<string, () => Promise<Recommendation | null>> =\n      new Map([\n        [\n          RECOMMENDATION_NAMES.LUA_SCRIPT,\n          async () =>\n            await this.recommendationProvider.determineLuaScriptRecommendation(\n              client,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.BIG_HASHES,\n          async () =>\n            await this.recommendationProvider.determineBigHashesRecommendation(\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.USE_SMALLER_KEYS,\n          async () =>\n            await this.recommendationProvider.determineBigTotalRecommendation(\n              total,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n          async () =>\n            await this.recommendationProvider.determineLogicalDatabasesRecommendation(\n              client,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES,\n          async () =>\n            await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES,\n          async () =>\n            await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(\n              client,\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST,\n          async () =>\n            await this.recommendationProvider.determineHashHashtableToZiplistRecommendation(\n              client,\n              keys,\n            ),\n        ],\n        [RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES, () => null],\n        [\n          RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n          async () =>\n            await this.recommendationProvider.determineCompressionForListRecommendation(\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.BIG_STRINGS,\n          async () =>\n            await this.recommendationProvider.determineBigStringsRecommendation(\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST,\n          async () =>\n            await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(\n              client,\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.BIG_SETS,\n          async () =>\n            await this.recommendationProvider.determineBigSetsRecommendation(\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS,\n          async () =>\n            await this.recommendationProvider.determineConnectionClientsRecommendation(\n              client,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.RTS,\n          async () =>\n            await this.recommendationProvider.determineRTSRecommendation(\n              client,\n              keys,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.REDIS_VERSION,\n          async () =>\n            await this.recommendationProvider.determineRedisVersionRecommendation(\n              client,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.SEARCH_INDEXES,\n          async () =>\n            await this.recommendationProvider.determineSearchIndexesRecommendation(\n              client,\n              keys,\n              globalClient,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.SET_PASSWORD,\n          async () =>\n            await this.recommendationProvider.determineSetPasswordRecommendation(\n              client,\n            ),\n        ],\n        [\n          RECOMMENDATION_NAMES.SEARCH_HASH,\n          async () =>\n            await this.recommendationProvider.determineSearchHashRecommendation(\n              keys,\n              indexes,\n            ),\n        ],\n        // it is live time recommendation (will add later)\n        [RECOMMENDATION_NAMES.STRING_TO_JSON, () => null],\n        [\n          RECOMMENDATION_NAMES.SEARCH_JSON,\n          async () =>\n            await this.recommendationProvider.determineSearchJSONRecommendation(\n              keys,\n              indexes,\n            ),\n        ],\n        [RECOMMENDATION_NAMES.SEARCH_VISUALIZATION, () => null],\n        [RECOMMENDATION_NAMES.TRY_RDI, () => null],\n      ]);\n\n    const recommendationsToDetermine = difference(\n      Object.values(RECOMMENDATION_NAMES),\n      exclude,\n    );\n\n    return Promise.all(\n      recommendationsToDetermine.map((recommendation) =>\n        recommendations.get(recommendation)(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/index.ts",
    "content": "export * from './redis.client';\nexport * from './ioredis/ioredis.client';\nexport * from './ioredis/standalone.ioredis.client';\nexport * from './ioredis/sentinel.ioredis.client';\nexport * from './ioredis/cluster.ioredis.client';\nexport * from './node-redis/node-redis.client';\nexport * from './node-redis/standalone.node-redis.client';\nexport * from './node-redis/cluster.node-redis.client';\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/ioredis/cluster.ioredis.client.ts",
    "content": "import { Cluster } from 'ioredis';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n  IoredisClient,\n  StandaloneIoredisClient,\n  RedisClientNodeRole,\n} from 'src/modules/redis/client';\nimport { findKey } from 'lodash';\n\nenum IoredisNodeRole {\n  PRIMARY = 'master',\n  SECONDARY = 'slave',\n  ALL = 'all',\n}\n\nexport class ClusterIoredisClient extends IoredisClient {\n  protected readonly client: Cluster;\n\n  getConnectionType(): RedisClientConnectionType {\n    return RedisClientConnectionType.CLUSTER;\n  }\n\n  async nodes(role?: RedisClientNodeRole): Promise<RedisClient[]> {\n    return this.client\n      .nodes(role ? IoredisNodeRole[role] : IoredisNodeRole.ALL)\n      .map((node) => {\n        let natAddress = {};\n\n        if (this.client.options.natMap) {\n          const natAddressString = findKey(this.client.options.natMap, {\n            host: node.options.host,\n            port: node.options.port,\n          });\n\n          if (natAddressString) {\n            const [, natHost, natPort] = natAddressString.match(/(.+):(\\d+)$/);\n            natAddress = {\n              natHost,\n              natPort: +natPort,\n            };\n          }\n        }\n\n        return new StandaloneIoredisClient(\n          this.clientMetadata,\n          node,\n          {\n            host: node.options.host,\n            port: node.options.port,\n            ...natAddress,\n          },\n          this.database,\n        );\n      });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/ioredis/ioredis.client.ts",
    "content": "import { get, isNumber } from 'lodash';\nimport Redis, { Cluster, Command } from 'ioredis';\nimport {\n  IRedisClientCommandOptions,\n  IRedisClientOptions,\n  RedisClient,\n  RedisClientCommand,\n  RedisClientCommandReply,\n} from 'src/modules/redis/client';\nimport { RedisString } from 'src/common/constants';\nimport { ClientMetadata } from 'src/common/models';\nimport { BrowserToolHashCommands } from 'src/modules/browser/constants/browser-tool-commands';\nimport { Database } from 'src/modules/database/models/database';\n\n// should return array (same as original reply)\nRedis.Command.setReplyTransformer(\n  BrowserToolHashCommands.HGetAll,\n  (result) => result,\n);\n\nexport abstract class IoredisClient extends RedisClient {\n  constructor(\n    public readonly clientMetadata: ClientMetadata,\n    protected readonly client: Redis | Cluster,\n    public readonly options: IRedisClientOptions,\n    database: Partial<Database>,\n  ) {\n    super(clientMetadata, client, options, database);\n    client.addBuiltinCommand(BrowserToolHashCommands.HExpire);\n    client.addBuiltinCommand(BrowserToolHashCommands.HTtl);\n    client.addBuiltinCommand(BrowserToolHashCommands.HPersist);\n    // fix not existing command in pipeline\n    client.addBuiltinCommand(BrowserToolHashCommands.HGETALL);\n  }\n\n  static prepareCommandOptions(options: IRedisClientCommandOptions): any {\n    let replyEncoding = null;\n\n    if (options?.replyEncoding === 'utf8') {\n      replyEncoding = 'utf8';\n    }\n\n    return {\n      replyEncoding,\n    };\n  }\n\n  static prepareCommandArgs(args: RedisClientCommand): RedisString[] {\n    const strArgs = args.map((arg) =>\n      isNumber(arg) ? arg.toString() : arg,\n    ) as string[];\n    return [...strArgs.shift().split(' '), ...strArgs];\n  }\n\n  /**\n   * @inheritDoc\n   */\n  isConnected(): boolean {\n    try {\n      return this.client.status === 'ready';\n    } catch (e) {\n      return false;\n    }\n  }\n\n  async nodes(): Promise<RedisClient[]> {\n    return [this];\n  }\n\n  async sendPipeline(\n    commands: RedisClientCommand[],\n    options?: IRedisClientCommandOptions,\n  ): Promise<Array<[Error | null, RedisClientCommandReply]>> {\n    let batch = commands.map((command) =>\n      IoredisClient.prepareCommandArgs(command),\n    );\n\n    if (options?.unknownCommands) {\n      batch = commands.map((command) => ['call', ...command]) as string[][];\n    }\n\n    // todo: replyEncoding\n    return (await this.client.pipeline(batch).exec()) as [\n      Error | null,\n      RedisClientCommandReply,\n    ][];\n  }\n\n  async sendCommand(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply> {\n    const [cmd, ...args] = IoredisClient.prepareCommandArgs(\n      command,\n    ) as string[];\n    return (await this.client.sendCommand(\n      new Command(cmd, args, IoredisClient.prepareCommandOptions(options)),\n    )) as RedisClientCommandReply;\n  }\n\n  /** TODO: It's necessary to investigate transactions\n  async sendMulti(\n    commands: RedisClientCommand[],\n    options?: IRedisClientCommandOptions,\n  ): Promise<Array<[Error | null, RedisClientCommandReply]>> {\n    let batch = commands;\n\n    if (options?.unknownCommands) {\n      batch = commands.map((command) => ['call', ...command]);\n    }\n\n    return await this.client.multi(batch).exec() as [Error | null, RedisClientCommandReply][];\n  }\n   */\n\n  async call(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply> {\n    if (IoredisClient.prepareCommandOptions(options).replyEncoding === null) {\n      return (await this.client.callBuffer(\n        ...command,\n      )) as RedisClientCommandReply;\n    }\n\n    return (await this.client.call(...command)) as RedisClientCommandReply;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async publish(channel: string, message: string): Promise<number> {\n    return this.client.publish(channel, message);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async subscribe(channel: string): Promise<void> {\n    const listenerCount = this.client.listenerCount('message');\n    if (listenerCount === 0) {\n      this.client.on('message', (messageChannel: string, message: string) => {\n        this.emit('message', messageChannel, message);\n      });\n    }\n    await this.client.subscribe(channel);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async pSubscribe(channel: string): Promise<void> {\n    const listenerCount = this.client.listenerCount('pmessage');\n    if (listenerCount === 0) {\n      this.client.on(\n        'pmessage',\n        (pattern: string, messageChannel: string, message: string) => {\n          this.emit('pmessage', pattern, messageChannel, message);\n        },\n      );\n    }\n    await this.client.psubscribe(channel);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async unsubscribe(channel: string): Promise<void> {\n    await this.client.unsubscribe(channel);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async pUnsubscribe(channel: string): Promise<void> {\n    await this.client.punsubscribe(channel);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async monitor(): Promise<any> {\n    if (this.client instanceof Redis) {\n      const monitorClient = this.client.monitor();\n      this.client.disconnect();\n      return monitorClient;\n    }\n\n    return undefined;\n  }\n\n  async disconnect(): Promise<void> {\n    this.client.disconnect();\n  }\n\n  async quit(): Promise<void> {\n    await this.client.quit();\n  }\n\n  async getCurrentDbIndex(): Promise<number> {\n    return get(this.client, ['options', 'db'], 0);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/ioredis/sentinel.ioredis.client.ts",
    "content": "import {\n  RedisClientConnectionType,\n  StandaloneIoredisClient,\n} from 'src/modules/redis/client';\n\nexport class SentinelIoredisClient extends StandaloneIoredisClient {\n  /**\n   * @inheritDoc\n   */\n  getConnectionType(): RedisClientConnectionType {\n    return RedisClientConnectionType.SENTINEL;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/ioredis/standalone.ioredis.client.ts",
    "content": "import Redis from 'ioredis';\nimport {\n  IoredisClient,\n  RedisClient,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\n\nexport class StandaloneIoredisClient extends IoredisClient {\n  protected readonly client: Redis;\n\n  /**\n   * @inheritDoc\n   */\n  getConnectionType(): RedisClientConnectionType {\n    return RedisClientConnectionType.STANDALONE;\n  }\n\n  async nodes(): Promise<RedisClient[]> {\n    return [this];\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/node-redis/cluster.node-redis.client.ts",
    "content": "import {\n  IRedisClientCommandOptions,\n  NodeRedisClient,\n  NodeRedisCluster,\n  RedisClient,\n  RedisClientCommand,\n  RedisClientCommandReply,\n  RedisClientConnectionType,\n  RedisClientNodeRole,\n  StandaloneNodeRedisClient,\n} from 'src/modules/redis/client';\n\nexport class ClusterNodeRedisClient extends NodeRedisClient {\n  protected readonly client: NodeRedisCluster;\n\n  /**\n   * @inheritDoc\n   */\n  getConnectionType(): RedisClientConnectionType {\n    return RedisClientConnectionType.CLUSTER;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async nodes(role?: RedisClientNodeRole): Promise<RedisClient[]> {\n    let nodes = [];\n    switch (role) {\n      case RedisClientNodeRole.PRIMARY:\n        nodes = this.client.masters;\n        break;\n      case RedisClientNodeRole.SECONDARY:\n        nodes = this.client.replicas;\n        break;\n      default:\n        nodes = this.client.masters.concat(this.client.replicas);\n    }\n\n    return nodes.map(\n      (node) =>\n        new StandaloneNodeRedisClient(\n          this.clientMetadata,\n          node.client,\n          {\n            host: node.host,\n            port: node.port,\n          },\n          this.database,\n        ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async sendPipeline(\n    commands: RedisClientCommand[],\n    options?: IRedisClientCommandOptions,\n  ): Promise<Array<[Error | null, RedisClientCommandReply]>> {\n    return Promise.all(\n      commands.map((cmd) =>\n        this.sendCommand(cmd, options)\n          .then((res): [null, RedisClientCommandReply] => [null, res])\n          .catch((e): [Error, null] => [e, null]),\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async sendCommand(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply> {\n    return this.client.sendCommand(\n      undefined,\n      false,\n      NodeRedisClient.prepareCommandArgs(command),\n      NodeRedisClient.prepareCommandOptions(options),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  /** TODO: It's necessary to investigate transactions\n  async sendMulti(commands: RedisClientCommand[]): Promise<Array<[Error | null, RedisClientCommandReply]>> {\n    return Promise.all(\n      commands.map(\n        (cmd) => this.sendCommand(cmd)\n          .then((res): [null, RedisClientCommandReply] => [null, res])\n          .catch((e): [Error, null] => [e, null]),\n      ),\n    );\n  }\n   */\n\n  /**\n   * @inheritDoc\n   */\n  async call(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply> {\n    return this.sendCommand(command, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/node-redis/node-redis.client.ts",
    "content": "import { isNull, isNumber } from 'lodash';\nimport { createClient, createCluster } from 'redis';\nimport {\n  IRedisClientCommandOptions,\n  RedisClient,\n  RedisClientCommand,\n} from 'src/modules/redis/client';\nimport { RedisString } from 'src/common/constants';\n\nexport type NodeRedis = ReturnType<typeof createClient>;\nexport type NodeRedisCluster = ReturnType<typeof createCluster>;\n\nexport abstract class NodeRedisClient extends RedisClient {\n  protected readonly client: NodeRedis | NodeRedisCluster;\n\n  static prepareCommandOptions(options: IRedisClientCommandOptions): any {\n    let replyEncoding = null;\n\n    if (options?.replyEncoding === 'utf8') {\n      replyEncoding = 'utf8';\n    }\n\n    return {\n      returnBuffers: isNull(replyEncoding),\n    };\n  }\n\n  static prepareCommandArgs(args: RedisClientCommand): RedisString[] {\n    const strArgs = args.map((arg) =>\n      isNumber(arg) ? arg.toString() : arg,\n    ) as string[];\n    return [...strArgs.shift().split(' '), ...strArgs];\n  }\n\n  async nodes(): Promise<RedisClient[]> {\n    return [this];\n  }\n\n  /**\n   * @inheritDoc\n   */\n  isConnected(): boolean {\n    // todo: find a way\n    return true;\n    //   try {\n    //     return this.client.status === 'ready';\n    //   } catch (e) {\n    //     return false;\n    //   }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async publish(channel: string, message: string): Promise<number> {\n    return this.client.publish(channel, message);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async subscribe(channel: string): Promise<void> {\n    const listener = (message: string, messageChannel: string) => {\n      this.emit('message', messageChannel, message);\n    };\n    return this.client.subscribe(channel, listener);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async pSubscribe(channel: string): Promise<void> {\n    const listener = (message: string, messageChannel: string) => {\n      this.emit('pmessage', channel, messageChannel, message);\n    };\n    return this.client.pSubscribe(channel, listener);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async unsubscribe(channel: string): Promise<void> {\n    return this.client.unsubscribe(channel);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async pUnsubscribe(channel: string): Promise<void> {\n    return this.client.pUnsubscribe(channel);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async monitor(): Promise<any> {\n    // TODO: Implement this method after the monitor in the node-redis is available.\n    throw new Error('Not implemented');\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async disconnect(): Promise<void> {\n    this.client.disconnect();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async quit(): Promise<void> {\n    await this.client.quit();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getCurrentDbIndex(): Promise<number> {\n    return this.clientMetadata.db || 0;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/node-redis/standalone.node-redis.client.ts",
    "content": "import {\n  IRedisClientCommandOptions,\n  RedisClientCommand,\n  RedisClientCommandReply,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\nimport {\n  NodeRedis,\n  NodeRedisClient,\n} from 'src/modules/redis/client/node-redis/node-redis.client';\n\nexport class StandaloneNodeRedisClient extends NodeRedisClient {\n  protected readonly client: NodeRedis;\n\n  /**\n   * @inheritDoc\n   */\n  getConnectionType(): RedisClientConnectionType {\n    return RedisClientConnectionType.STANDALONE;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async sendPipeline(\n    commands: RedisClientCommand[],\n    options?: IRedisClientCommandOptions,\n  ): Promise<Array<[Error | null, RedisClientCommandReply]>> {\n    return Promise.all(\n      commands.map((cmd) =>\n        this.sendCommand(cmd, options)\n          .then((res): [null, RedisClientCommandReply] => [null, res])\n          .catch((e): [Error, null] => [e, null]),\n      ),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async sendCommand(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply> {\n    return this.client.sendCommand(\n      NodeRedisClient.prepareCommandArgs(command),\n      NodeRedisClient.prepareCommandOptions(options),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  /** TODO: It's necessary to investigate transactions\n  async sendMulti(commands: RedisClientCommand[]): Promise<Array<[Error | null, RedisClientCommandReply]>> {\n    return Promise.all(\n      commands.map(\n        (cmd) => this.sendCommand(cmd)\n          .then((res): [null, RedisClientCommandReply] => [null, res])\n          .catch((e): [Error, null] => [e, null]),\n      ),\n    );\n  }\n   */\n\n  /**\n   * @inheritDoc\n   */\n  async call(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply> {\n    return this.sendCommand(command, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/client/redis.client.ts",
    "content": "import { ClientContext, ClientMetadata } from 'src/common/models';\nimport { isNumber, pick } from 'lodash';\nimport { RedisString, UNKNOWN_REDIS_INFO } from 'src/common/constants';\nimport apiConfig from 'src/utils/config';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\nimport { convertArrayReplyToObject } from '../utils';\nimport * as semverCompare from 'node-version-compare';\nimport { RedisDatabaseHelloResponse } from 'src/modules/database/dto/redis-info.dto';\nimport { plainToClass } from 'class-transformer';\nimport { Database } from 'src/modules/database/models/database';\n\nconst REDIS_CLIENTS_CONFIG = apiConfig.get('redis_clients');\n\nexport enum RedisClientConnectionType {\n  STANDALONE = 'STANDALONE',\n  CLUSTER = 'CLUSTER',\n  SENTINEL = 'SENTINEL',\n}\n\nexport enum RedisClientNodeRole {\n  PRIMARY = 'PRIMARY',\n  SECONDARY = 'SECONDARY',\n}\n\nexport interface IRedisClientCommandOptions {\n  firstKey?: RedisString;\n  readOnly?: boolean;\n  replyEncoding?: 'utf8' | null;\n  unknownCommands?: boolean;\n}\n\nexport interface IRedisClientOptions {\n  host?: string;\n  port?: number;\n  natHost?: string;\n  natPort?: number;\n  tlsPort?: number;\n  connectTimeout?: number;\n}\n\nexport type RedisClientCommandArgument = RedisString | number;\nexport type RedisClientCommandArguments = RedisClientCommandArgument[];\nexport type RedisClientCommand = [\n  cmd: string,\n  ...args: RedisClientCommandArguments,\n];\nexport type RedisClientCommandReply =\n  | string\n  | number\n  | Buffer\n  | null\n  | undefined\n  | Array<RedisClientCommandReply>;\n\nexport enum RedisFeature {\n  HashFieldsExpiration = 'HashFieldsExpiration',\n  UnlinkCommand = 'UnlinkCommand',\n}\n\nconst CLIENT_DATABASE_FIELDS: (keyof Database)[] = ['providerDetails'];\n\nexport abstract class RedisClient extends EventEmitter2 {\n  public readonly id: string;\n\n  public readonly database: Partial<Database>;\n\n  protected _redisVersion: string | undefined;\n\n  protected _isInfoCommandDisabled: boolean | undefined;\n\n  protected lastTimeUsed: number;\n\n  constructor(\n    public readonly clientMetadata: ClientMetadata,\n    protected readonly client: unknown,\n    public readonly options: IRedisClientOptions,\n    database: Partial<Database> = {},\n  ) {\n    super();\n    this.clientMetadata = RedisClient.prepareClientMetadata(clientMetadata);\n    this.database = RedisClient.pickDatabaseMetadata(database);\n    this.lastTimeUsed = Date.now();\n    this.id = RedisClient.generateId(this.clientMetadata);\n  }\n\n  static pickDatabaseMetadata(database: Partial<Database>): Partial<Database> {\n    return pick(database, CLIENT_DATABASE_FIELDS);\n  }\n\n  /**\n   * Get native client\n   * @deprecated\n   * For backward compatibility. Will be deleted with next releases\n   */\n  public getClient(): any {\n    return this.client;\n  }\n\n  public setLastUsed(): void {\n    this.lastTimeUsed = Date.now();\n  }\n\n  public isIdle(): boolean {\n    return Date.now() - this.lastTimeUsed > REDIS_CLIENTS_CONFIG.idleThreshold;\n  }\n\n  public get isInfoCommandDisabled() {\n    return this._isInfoCommandDisabled;\n  }\n\n  /**\n   * Checks if client has established connection\n   */\n  abstract isConnected(): boolean;\n\n  /**\n   * Get connection type (STANDALONE, CLUSTER or SENTINEL)\n   */\n  abstract getConnectionType(): RedisClientConnectionType;\n\n  abstract nodes(role?: RedisClientNodeRole): Promise<RedisClient[]>;\n\n  abstract sendCommand(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply>;\n\n  abstract sendPipeline(\n    commands: RedisClientCommand[],\n    options?: IRedisClientCommandOptions,\n  ): Promise<Array<[Error | null, RedisClientCommandReply]>>;\n\n  /** TODO: It's necessary to investigate transactions\n  abstract sendMulti(\n    commands: RedisClientCommand[],\n    options?: IRedisClientCommandOptions,\n  ): Promise<Array<[Error | null, RedisClientCommandReply]>>;\n   */\n\n  abstract call(\n    command: RedisClientCommand,\n    options?: IRedisClientCommandOptions,\n  ): Promise<RedisClientCommandReply>;\n\n  abstract monitor(): Promise<any>;\n\n  /**\n   * Close Redis connection without waiting for pending commands\n   */\n  abstract disconnect(): Promise<void>;\n\n  /**\n   * Wait for pending commands will be processed and then close the connection\n   */\n  abstract quit(): Promise<void>;\n\n  abstract publish(channel: string, message: string): Promise<number>;\n\n  abstract subscribe(channel: string): Promise<void>;\n\n  abstract pSubscribe(channel: string): Promise<void>;\n\n  abstract unsubscribe(channel: string): Promise<void>;\n\n  abstract pUnsubscribe(channel: string): Promise<void>;\n\n  abstract getCurrentDbIndex(): Promise<number>;\n\n  /**\n   * Detects if feature is supported by redis database\n   * todo: move out from here when final requirements will be clear\n   * @param feature\n   */\n  public async isFeatureSupported(feature: RedisFeature): Promise<boolean> {\n    switch (feature) {\n      case RedisFeature.HashFieldsExpiration:\n        try {\n          const redisVersion = await this.getRedisVersion();\n          return redisVersion && semverCompare('7.3', redisVersion) < 1;\n        } catch (e) {\n          return false;\n        }\n      case RedisFeature.UnlinkCommand:\n        try {\n          const redisVersion = await this.getRedisVersion();\n          // UNLINK command was introduced in Redis 4.0.0\n          return redisVersion && semverCompare('4.0.0', redisVersion) < 1;\n        } catch (e) {\n          return false;\n        }\n      default:\n        return false;\n    }\n  }\n\n  private async getRedisVersion(): Promise<string> {\n    if (!this._redisVersion) {\n      const infoData = await this.getInfo('server');\n      this._redisVersion = infoData?.server?.redis_version;\n    }\n\n    return this._redisVersion;\n  }\n\n  /**\n   * Get redis database info\n   * If INFO fails, it will try to get info from HELLO command, which provides limited data\n   * If HELLO fails, it will return a static object\n   * @param force\n   * @param infoSection - e.g. server, clients, memory, etc.\n   */\n  public async getInfo(infoSection?: string) {\n    let infoData: any; // TODO: we should ideally type this\n\n    try {\n      infoData = convertRedisInfoReplyToObject(\n        (await this.call(infoSection ? ['info', infoSection] : ['info'], {\n          replyEncoding: 'utf8',\n        })) as string,\n      );\n      this._isInfoCommandDisabled = false;\n    } catch (error) {\n      this._isInfoCommandDisabled = true;\n      try {\n        // Fallback to getting basic information from `hello` command\n        infoData = await this.getRedisHelloInfo();\n      } catch (_error) {\n        // Ignore: hello is not available pre redis version 6\n      }\n    }\n\n    return infoData ?? UNKNOWN_REDIS_INFO;\n  }\n\n  private async getRedisHelloInfo() {\n    const helloResponse = await this.getRedisHelloResponse();\n\n    return {\n      replication: {\n        role: helloResponse.role,\n      },\n      server: {\n        server_name: helloResponse.server,\n        redis_version: helloResponse.version,\n        redis_mode: helloResponse.mode,\n      },\n      modules: helloResponse.modules,\n    };\n  }\n\n  private async getRedisHelloResponse(): Promise<RedisDatabaseHelloResponse> {\n    const helloResponse = (await this.sendCommand(['hello'], {\n      replyEncoding: 'utf8',\n    })) as any[];\n\n    const helloInfoResponse = convertArrayReplyToObject(helloResponse);\n\n    if (helloInfoResponse.modules?.length) {\n      helloInfoResponse.modules = helloInfoResponse.modules.map(\n        convertArrayReplyToObject,\n      );\n    }\n\n    return plainToClass(RedisDatabaseHelloResponse, helloInfoResponse);\n  }\n\n  /**\n   * Prepare clientMetadata to be used for generating id and other operations with clients\n   * like: find, remove many, etc.\n   * @param clientMetadata\n   */\n  static prepareClientMetadata(clientMetadata: ClientMetadata): ClientMetadata {\n    return {\n      ...clientMetadata,\n      // Workaround: for cli connections we must ignore db index when storing/getting client\n      // since inside CLI itself users are able to \"select\" database manually\n      // uniqueness will be guaranteed by ClientMetadata.uniqueId and each opened CLI terminal\n      // will have own and a single client\n      db:\n        clientMetadata.context === ClientContext.CLI ? null : clientMetadata.db,\n    };\n  }\n\n  static generateId(cm: ClientMetadata): string {\n    const empty = '(nil)';\n    const separator = '_';\n\n    const id = [\n      cm.databaseId,\n      cm.context,\n      cm.uniqueId || empty,\n      isNumber(cm.db) ? cm.db : empty,\n    ].join(separator);\n\n    const uId = [\n      cm.sessionMetadata?.userId || empty,\n      cm.sessionMetadata?.accountId || empty,\n      cm.sessionMetadata?.sessionId || empty,\n      cm.sessionMetadata?.uniqueId || empty,\n    ].join(separator);\n\n    return [id, uId].join(separator);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/connection/ioredis.redis.connection.strategy.spec.ts",
    "content": "import { Test } from '@nestjs/testing';\nimport * as Redis from 'ioredis';\nimport {\n  mockClientMetadata,\n  mockClusterDatabaseWithTlsAuth,\n  mockDatabase,\n  mockDatabaseWithSshBasic,\n  mockDatabaseWithTlsAuth,\n  mockSentinelDatabaseWithTlsAuth,\n  mockSshTunnelProvider,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { EventEmitter } from 'events';\nimport apiConfig, { Config } from 'src/utils/config';\nimport { SshTunnelProvider } from 'src/modules/ssh/ssh-tunnel.provider';\nimport { IoredisRedisConnectionStrategy } from 'src/modules/redis/connection/ioredis.redis.connection.strategy';\nimport {\n  ClusterIoredisClient,\n  SentinelIoredisClient,\n  StandaloneIoredisClient,\n} from 'src/modules/redis/client';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ReplyError } from 'src/models';\n\nconst REDIS_CLIENTS_CONFIG = apiConfig.get(\n  'redis_clients',\n) as Config['redis_clients'];\n\njest.mock('ioredis', () => ({\n  ...(jest.requireActual('ioredis') as object),\n}));\n\nconst mockError = new Error('some error');\nconst checkError = (cb) => (e) => {\n  expect(e).toEqual(mockError);\n  cb();\n};\n\nfunction fail(data?: any) {\n  expect(`Expected to fail but got ${data}`).toBeFalsy();\n}\n\ndescribe('IoredisRedisConnectionStrategy', () => {\n  let service: IoredisRedisConnectionStrategy;\n  let mockIoredisNativeClient;\n  let mockIoredisClusterNativeClient;\n  let spyRedis;\n  let spyCluster;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        IoredisRedisConnectionStrategy,\n        {\n          provide: SshTunnelProvider,\n          useFactory: mockSshTunnelProvider,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(IoredisRedisConnectionStrategy);\n\n    class MockNativeRedisClient extends EventEmitter {\n      addBuiltinCommand = jest.fn();\n    }\n\n    mockIoredisNativeClient = new MockNativeRedisClient();\n    mockIoredisClusterNativeClient = new MockNativeRedisClient();\n    spyRedis = jest.spyOn(Redis, 'default');\n    spyRedis.mockImplementationOnce(() => mockIoredisNativeClient);\n    spyCluster = jest.spyOn(Redis, 'Cluster');\n    spyCluster.mockImplementationOnce(() => mockIoredisClusterNativeClient);\n  });\n\n  describe('retryStrategy', () => {\n    it('should return 500ms delay for first retry', () => {\n      expect(service['retryStrategy'](1)).toEqual(\n        REDIS_CLIENTS_CONFIG.retryDelay,\n      );\n    });\n    it('should return 1000ms delay for second retry', () => {\n      expect(service['retryStrategy'](2)).toEqual(\n        REDIS_CLIENTS_CONFIG.retryDelay * 2,\n      );\n    });\n    it('should return undefined when number of retries exceeded', () => {\n      expect(\n        service['retryStrategy'](REDIS_CLIENTS_CONFIG.retryTimes + 1),\n      ).toEqual(undefined);\n    });\n  });\n\n  describe('createStandaloneClient', () => {\n    it('should successfully create standalone client', (done) => {\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabaseWithTlsAuth, {\n          useRetry: true,\n        })\n        .then((client) => {\n          expect(client).toBeInstanceOf(StandaloneIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisNativeClient);\n          done();\n        });\n      process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n    });\n    it('should successfully create standalone client with reconnect', (done) => {\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabaseWithTlsAuth, {\n          useRetry: true,\n        })\n        .then((client) => {\n          expect(client).toBeInstanceOf(StandaloneIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisNativeClient);\n          done();\n        });\n\n      process.nextTick(() => {\n        mockIoredisNativeClient.emit('reconnecting');\n        process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n      });\n    });\n    it('should successfully create standalone client with ssh', (done) => {\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabaseWithSshBasic, {\n          useRetry: true,\n        })\n        .then((client) => {\n          expect(client).toBeInstanceOf(StandaloneIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisNativeClient);\n          done();\n        });\n\n      process.nextTick(() => {\n        mockIoredisNativeClient.emit('reconnecting');\n        process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n      });\n    });\n    it('should successfully create standalone client with db = 0 for sentinel', (done) => {\n      service\n        .createStandaloneClient(\n          mockClientMetadata,\n          {\n            ...mockSentinelDatabaseWithTlsAuth,\n            db: 1, // will be overwritten to 1 during connection\n          },\n          { useRetry: true },\n        )\n        .then((client) => {\n          expect(client).toBeInstanceOf(StandaloneIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisNativeClient);\n          expect(spyRedis).toHaveBeenCalledWith(\n            expect.objectContaining({ db: 0 }),\n          );\n          done();\n        });\n\n      process.nextTick(() => {\n        process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n      });\n    });\n    it('should successfully create standalone client with db > 0 for !sentinel', (done) => {\n      service\n        .createStandaloneClient(\n          mockClientMetadata,\n          {\n            ...mockDatabase,\n            db: 1, // will still be 1 during connection\n          },\n          { useRetry: true },\n        )\n        .then((client) => {\n          expect(client).toBeInstanceOf(StandaloneIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisNativeClient);\n          expect(spyRedis).toHaveBeenCalledWith(\n            expect.objectContaining({ db: 1 }),\n          );\n          done();\n        });\n\n      process.nextTick(() => {\n        process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n      });\n    });\n    it('should fail to create standalone connection', (done) => {\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabaseWithTlsAuth, {})\n        .then(fail)\n        .catch(checkError(done));\n\n      process.nextTick(() => mockIoredisNativeClient.emit('error', mockError));\n    });\n    it('should fail to create standalone connection due to \"end\" event', (done) => {\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabaseWithTlsAuth, {})\n        .then(fail)\n        .catch((e) => {\n          expect(e).toBeInstanceOf(InternalServerErrorException);\n          expect(e.message).toEqual(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION);\n          done();\n        });\n\n      process.nextTick(() => mockIoredisNativeClient.emit('end'));\n    });\n    it('should handle sync error during standalone client creation', (done) => {\n      spyRedis.mockReset();\n      spyRedis.mockImplementationOnce(() => {\n        throw mockError;\n      });\n\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabaseWithTlsAuth, {})\n        .then(fail)\n        .catch(checkError(done));\n    });\n    it('should include family: 0 for dual-stack IPv4/IPv6 support', (done) => {\n      service\n        .createStandaloneClient(mockClientMetadata, mockDatabase, {})\n        .then(() => {\n          expect(spyRedis).toHaveBeenCalledWith(\n            expect.objectContaining({ family: 0 }),\n          );\n          done();\n        });\n\n      process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n    });\n  });\n\n  describe('createClusterClient', () => {\n    beforeEach(() => {\n      jest\n        .spyOn(service, 'createStandaloneClient')\n        .mockResolvedValue(mockStandaloneRedisClient);\n    });\n\n    it('should successfully create cluster client', (done) => {\n      service\n        .createClusterClient(\n          mockClientMetadata,\n          mockClusterDatabaseWithTlsAuth,\n          {},\n        )\n        .then((client) => {\n          expect(client).toBeInstanceOf(ClusterIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisClusterNativeClient);\n          done();\n        });\n\n      process.nextTick(() => mockIoredisClusterNativeClient.emit('ready'));\n    });\n    it('should fail to create cluster connection due to \"error\" event', (done) => {\n      service\n        .createClusterClient(\n          mockClientMetadata,\n          mockClusterDatabaseWithTlsAuth,\n          {},\n        )\n        .then(fail)\n        .catch(checkError(done));\n\n      process.nextTick(() =>\n        mockIoredisClusterNativeClient.emit('error', mockError),\n      );\n    });\n    it('should fail with lastNodeError', (done) => {\n      const mockedLastNodeError = Object.assign(\n        new ReplyError('some message'),\n        { name: 'Unsupported' },\n      );\n      service\n        .createClusterClient(\n          mockClientMetadata,\n          mockClusterDatabaseWithTlsAuth,\n          {},\n        )\n        .then(fail)\n        .catch((e) => {\n          expect(e).toEqual(mockedLastNodeError);\n          done();\n        });\n\n      process.nextTick(() =>\n        mockIoredisClusterNativeClient.emit('error', {\n          lastNodeError: mockedLastNodeError,\n        }),\n      );\n    });\n    it('should fail to create cluster connection due to \"end\" event', (done) => {\n      service\n        .createClusterClient(\n          mockClientMetadata,\n          mockClusterDatabaseWithTlsAuth,\n          {},\n        )\n        .then(fail)\n        .catch((e) => {\n          expect(e).toBeInstanceOf(InternalServerErrorException);\n          expect(e.message).toEqual(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION);\n          done();\n        });\n\n      process.nextTick(() => mockIoredisClusterNativeClient.emit('end'));\n    });\n    it('should handle sync error during cluster client creation', (done) => {\n      spyCluster.mockReset();\n      spyCluster.mockImplementationOnce(() => {\n        throw mockError;\n      });\n      service\n        .createClusterClient(\n          mockClientMetadata,\n          mockClusterDatabaseWithTlsAuth,\n          {},\n        )\n        .then(fail)\n        .catch(checkError(done));\n    });\n  });\n\n  describe('createSentinelClient', () => {\n    it('should successfully create sentinel client', (done) => {\n      service\n        .createSentinelClient(mockClientMetadata, mockDatabaseWithTlsAuth, {\n          useRetry: true,\n        })\n        .then((client) => {\n          expect(client).toBeInstanceOf(SentinelIoredisClient);\n          expect(client.clientMetadata).toEqual(mockClientMetadata);\n          expect(client['client']).toEqual(mockIoredisNativeClient);\n          done();\n        });\n\n      process.nextTick(() => mockIoredisNativeClient.emit('ready'));\n    });\n    it('should fail to create sentinel connection due to \"error\" event', (done) => {\n      service\n        .createSentinelClient(\n          mockClientMetadata,\n          mockSentinelDatabaseWithTlsAuth,\n          {},\n        )\n        .then(fail)\n        .catch(checkError(done));\n\n      process.nextTick(() => mockIoredisNativeClient.emit('error', mockError));\n    });\n    it('should fail to create sentinel connection due to \"end\" event', (done) => {\n      service\n        .createSentinelClient(\n          mockClientMetadata,\n          mockSentinelDatabaseWithTlsAuth,\n          {},\n        )\n        .then(fail)\n        .catch((e) => {\n          expect(e).toBeInstanceOf(InternalServerErrorException);\n          expect(e.message).toEqual(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION);\n          done();\n        });\n\n      process.nextTick(() => mockIoredisNativeClient.emit('end'));\n    });\n    it('should handle sync error during sentinel client creation', (done) => {\n      spyRedis.mockReset();\n      spyRedis.mockImplementationOnce(() => {\n        throw mockError;\n      });\n\n      service\n        .createSentinelClient(\n          mockClientMetadata,\n          mockSentinelDatabaseWithTlsAuth,\n          {},\n        )\n        .catch(checkError(done));\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/connection/ioredis.redis.connection.strategy.ts",
    "content": "import { RedisConnectionStrategy } from 'src/modules/redis/connection/redis.connection.strategy';\nimport serverConfig from 'src/utils/config';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { ClientMetadata } from 'src/common/models';\nimport { Database } from 'src/modules/database/models/database';\nimport Redis, { Cluster, RedisOptions } from 'ioredis';\nimport { isEmpty, isNumber } from 'lodash';\nimport { IRedisConnectionOptions } from 'src/modules/redis/redis.client.factory';\nimport { ClusterOptions } from 'ioredis/built/cluster/ClusterOptions';\nimport { ConnectionOptions } from 'tls';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisClient,\n  StandaloneIoredisClient,\n  SentinelIoredisClient,\n  ClusterIoredisClient,\n} from 'src/modules/redis/client';\nimport { discoverClusterNodes } from 'src/modules/redis/utils';\nimport { SshTunnel } from 'src/modules/ssh/models/ssh-tunnel';\nimport { getRedisConnectionException } from 'src/utils';\nimport { ReplyError } from 'src/models';\n\nconst REDIS_CLIENTS_CONFIG = serverConfig.get('redis_clients');\n\nexport class IoredisRedisConnectionStrategy extends RedisConnectionStrategy {\n  // common retry strategy\n  private retryStrategy = (times: number): number => {\n    if (times < REDIS_CLIENTS_CONFIG.retryTimes) {\n      return Math.min(times * REDIS_CLIENTS_CONFIG.retryDelay, 2000);\n    }\n    return undefined;\n  };\n\n  // disable function such as retry or checkIdentity\n  private dummyFn = () => undefined;\n\n  /**\n   * Normalize data to be compatible with used redis connection library\n   * @param clientMetadata\n   * @param database\n   * @param options\n   * @private\n   */\n  private async getRedisOptions(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisOptions> {\n    const { host, port, password, username, tls, db, timeout } = database;\n    const redisOptions: RedisOptions = {\n      host,\n      port,\n      username,\n      password,\n      family: 0, // Enable dual-stack IPv4/IPv6 (auto-detect)\n      connectTimeout: timeout,\n      db: isNumber(clientMetadata.db) ? clientMetadata.db : db,\n      connectionName:\n        options?.connectionName ||\n        RedisConnectionStrategy.generateRedisConnectionName(clientMetadata),\n      showFriendlyErrorStack: true,\n      maxRetriesPerRequest: REDIS_CLIENTS_CONFIG.maxRetriesPerRequest,\n      retryStrategy: options?.useRetry\n        ? this.retryStrategy.bind(this)\n        : this.dummyFn.bind(this),\n      autoResendUnfulfilledCommands: false,\n      enableReadyCheck: options.enableReadyCheck ?? true,\n    };\n\n    if (tls) {\n      redisOptions.tls = await this.getTLSConfig(database);\n    }\n\n    return redisOptions;\n  }\n\n  /**\n   * Normalize data to be compatible with redis library cluster connection options\n   * @param clientMetadata\n   * @param database\n   * @param options\n   * @private\n   */\n  private async getRedisClusterOptions(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<ClusterOptions> {\n    return {\n      clusterRetryStrategy: options.useRetry\n        ? this.retryStrategy.bind(this)\n        : this.dummyFn.bind(this),\n      slotsRefreshTimeout: REDIS_CLIENTS_CONFIG.slotsRefreshTimeout,\n      redisOptions: await this.getRedisOptions(\n        clientMetadata,\n        database,\n        options,\n      ),\n      maxRedirections: REDIS_CLIENTS_CONFIG.maxRedirections,\n    };\n  }\n\n  /**\n   * Normalize data to be compatible with redis library sentinel connection options\n   * @param clientMetadata\n   * @param database\n   * @param options\n   * @private\n   */\n  private async getRedisSentinelOptions(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisOptions> {\n    const { sentinelMaster } = database;\n\n    const baseOptions = await this.getRedisOptions(\n      clientMetadata,\n      database,\n      options,\n    );\n    return {\n      ...baseOptions,\n      host: undefined,\n      port: undefined,\n      sentinels: [\n        { host: database.host, port: database.port, ...(database.nodes || []) },\n      ],\n      name: sentinelMaster?.name,\n      sentinelUsername: database.username,\n      sentinelPassword: database.password,\n      username: sentinelMaster?.username,\n      password: sentinelMaster?.password,\n      sentinelTLS: baseOptions.tls,\n      enableTLSForSentinelMode: !!baseOptions.tls, // previously was always `true` for tls connections\n      sentinelRetryStrategy: options?.useRetry\n        ? this.retryStrategy\n        : this.dummyFn,\n    };\n  }\n\n  /**\n   * Normalize tls settings to be compatible with user redis connection library\n   * @param database\n   * @private\n   */\n  private async getTLSConfig(database: Database): Promise<ConnectionOptions> {\n    let config: ConnectionOptions;\n    config = {\n      rejectUnauthorized: database.verifyServerCert,\n      checkServerIdentity: this.dummyFn,\n      servername: database.tlsServername || undefined,\n    };\n    if (database.caCert) {\n      config = {\n        ...config,\n        ca: [database.caCert.certificate],\n      };\n    }\n    if (database.clientCert) {\n      config = {\n        ...config,\n        cert: database.clientCert.certificate,\n        key: database.clientCert.key,\n      };\n    }\n\n    return config;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async createStandaloneClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient> {\n    this.logger.debug('Creating ioredis standalone client', clientMetadata);\n\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    let tnl: SshTunnel;\n\n    try {\n      const config = await this.getRedisOptions(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      if (database.ssh) {\n        tnl = await this.sshTunnelProvider.createTunnel(\n          database,\n          database.sshOptions,\n        );\n        config.host = tnl.serverAddress.host;\n        config.port = tnl.serverAddress.port;\n      }\n\n      return await new Promise((resolve, reject) => {\n        try {\n          const connection = new Redis({\n            ...config,\n            // cover cases when we are connecting to sentinel as to standalone to discover master groups\n            db: config.db > 0 && !database.sentinelMaster ? config.db : 0,\n          });\n          connection.on('error', (e: ReplyError): void => {\n            this.logger.error(\n              'Failed connection to the redis database.',\n              e,\n              clientMetadata,\n            );\n            reject(getRedisConnectionException(e, database));\n          });\n          connection.on('end', (): void => {\n            this.logger.warn(\n              ERROR_MESSAGES.SERVER_CLOSED_CONNECTION,\n              clientMetadata,\n            );\n            reject(\n              new InternalServerErrorException(\n                ERROR_MESSAGES.SERVER_CLOSED_CONNECTION,\n              ),\n            );\n          });\n          connection.on('ready', (): void => {\n            this.logger.debug(\n              'Successfully connected to the redis database',\n              clientMetadata,\n            );\n\n            resolve(\n              new StandaloneIoredisClient(\n                clientMetadata,\n                connection,\n                {\n                  host: database.host,\n                  port: database.port,\n                },\n                database,\n              ),\n            );\n          });\n          connection.on('reconnecting', (): void => {\n            this.logger.debug(\n              'Reconnecting to the redis database',\n              clientMetadata,\n            );\n          });\n        } catch (e) {\n          reject(e);\n        }\n      });\n    } catch (e) {\n      this.addConnectionError(e);\n\n      tnl?.close?.();\n      throw e;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async createClusterClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    let tnls: SshTunnel[] = [];\n    let standaloneClient: RedisClient;\n    let rootNodes = [\n      {\n        host: database.host,\n        port: database.port,\n      },\n    ];\n\n    try {\n      const config = await this.getRedisClusterOptions(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      standaloneClient = await this.createStandaloneClient(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      rootNodes = await discoverClusterNodes(standaloneClient);\n\n      await standaloneClient.disconnect();\n\n      if (database.ssh) {\n        tnls = await Promise.all(\n          rootNodes.map((node) =>\n            this.sshTunnelProvider.createTunnel(node, database.sshOptions),\n          ),\n        );\n\n        // create NAT map\n        config.natMap = {};\n        tnls.forEach((tnl) => {\n          config.natMap[`${tnl.options.targetHost}:${tnl.options.targetPort}`] =\n            {\n              host: tnl.serverAddress.host,\n              port: tnl.serverAddress.port,\n            };\n        });\n\n        // change root nodes\n        rootNodes = tnls.map((tnl) => tnl.serverAddress);\n      }\n\n      return new Promise((resolve, reject) => {\n        try {\n          const cluster = new Cluster(rootNodes, config);\n          cluster.on('error', (e): void => {\n            this.logger.error(\n              'Failed connection to the redis oss cluster',\n              e,\n              clientMetadata,\n            );\n            reject(\n              getRedisConnectionException(\n                !isEmpty(e.lastNodeError) ? e.lastNodeError : (e as ReplyError),\n                database,\n              ),\n            );\n          });\n          cluster.on('end', (): void => {\n            this.logger.warn(\n              ERROR_MESSAGES.SERVER_CLOSED_CONNECTION,\n              clientMetadata,\n            );\n            reject(\n              new InternalServerErrorException(\n                ERROR_MESSAGES.SERVER_CLOSED_CONNECTION,\n              ),\n            );\n          });\n          cluster.on('ready', (): void => {\n            this.logger.debug(\n              'Successfully connected to the redis oss cluster.',\n              clientMetadata,\n            );\n\n            resolve(\n              new ClusterIoredisClient(\n                clientMetadata,\n                cluster,\n                {\n                  host: database.host,\n                  port: database.port,\n                },\n                database,\n              ),\n            );\n          });\n        } catch (e) {\n          reject(e);\n        }\n      });\n    } catch (e) {\n      this.addConnectionError(e);\n\n      tnls.forEach((tnl) => tnl?.close?.());\n      standaloneClient?.disconnect?.().catch();\n      throw e;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async createSentinelClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    const config = await this.getRedisSentinelOptions(\n      clientMetadata,\n      database,\n      options,\n    );\n\n    return new Promise((resolve, reject) => {\n      try {\n        const client = new Redis(config);\n        client.on('error', (e: ReplyError): void => {\n          this.logger.error(\n            'Failed connection to the redis oss sentinel',\n            e,\n            clientMetadata,\n          );\n          reject(getRedisConnectionException(e, database));\n        });\n        client.on('end', (): void => {\n          this.logger.error(\n            ERROR_MESSAGES.SERVER_CLOSED_CONNECTION,\n            clientMetadata,\n          );\n          reject(\n            new InternalServerErrorException(\n              ERROR_MESSAGES.SERVER_CLOSED_CONNECTION,\n            ),\n          );\n        });\n        client.on('ready', (): void => {\n          this.logger.debug(\n            'Successfully connected to the redis oss sentinel.',\n            clientMetadata,\n          );\n\n          resolve(\n            new SentinelIoredisClient(\n              clientMetadata,\n              client,\n              {\n                host: database.host,\n                port: database.port,\n              },\n              database,\n            ),\n          );\n        });\n      } catch (e) {\n        this.addConnectionError(e);\n\n        reject(e);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/connection/node.redis.connection.strategy.spec.ts",
    "content": "import { Test } from '@nestjs/testing';\nimport * as redis from 'redis';\nimport {\n  mockClientMetadata,\n  mockDatabase,\n  mockSshTunnelProvider,\n} from 'src/__mocks__';\nimport { SshTunnelProvider } from 'src/modules/ssh/ssh-tunnel.provider';\nimport { NodeRedisConnectionStrategy } from 'src/modules/redis/connection/node.redis.connection.strategy';\nimport { StandaloneNodeRedisClient } from 'src/modules/redis/client/node-redis/standalone.node-redis.client';\n\njest.mock('redis', () => ({\n  ...jest.requireActual('redis'),\n  createClient: jest.fn(),\n}));\n\ndescribe('NodeRedisConnectionStrategy', () => {\n  let service: NodeRedisConnectionStrategy;\n  let createClientSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        NodeRedisConnectionStrategy,\n        {\n          provide: SshTunnelProvider,\n          useFactory: mockSshTunnelProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(NodeRedisConnectionStrategy);\n\n    createClientSpy = jest.spyOn(redis, 'createClient');\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('createStandaloneClient', () => {\n    it('should include family: 0 in socket options for dual-stack IPv4/IPv6 support', async () => {\n      const mockClient = {\n        on: jest.fn().mockReturnThis(),\n        connect: jest.fn().mockResolvedValue(undefined),\n      };\n      createClientSpy.mockReturnValue(mockClient);\n\n      const result = await service.createStandaloneClient(\n        mockClientMetadata,\n        mockDatabase,\n        {},\n      );\n\n      expect(result).toBeInstanceOf(StandaloneNodeRedisClient);\n      expect(createClientSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          socket: expect.objectContaining({\n            family: 0,\n          }),\n        }),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/connection/node.redis.connection.strategy.ts",
    "content": "import { RedisConnectionStrategy } from 'src/modules/redis/connection/redis.connection.strategy';\nimport serverConfig from 'src/utils/config';\nimport { ClientMetadata, Endpoint } from 'src/common/models';\nimport { Database } from 'src/modules/database/models/database';\nimport {\n  RedisClientOptions,\n  createClient,\n  createCluster,\n  RedisClusterOptions,\n} from 'redis';\nimport { isNumber } from 'lodash';\nimport { IRedisConnectionOptions } from 'src/modules/redis/redis.client.factory';\nimport { ConnectionOptions } from 'tls';\nimport { ClusterNodeRedisClient, RedisClient } from 'src/modules/redis/client';\nimport { StandaloneNodeRedisClient } from 'src/modules/redis/client/node-redis/standalone.node-redis.client';\nimport { SshTunnel } from 'src/modules/ssh/models/ssh-tunnel';\nimport { discoverClusterNodes } from 'src/modules/redis/utils';\n\nconst REDIS_CLIENTS_CONFIG = serverConfig.get('redis_clients');\n\nexport class NodeRedisConnectionStrategy extends RedisConnectionStrategy {\n  // common retry strategy\n  private retryStrategy = (times: number): number | boolean => {\n    if (times < REDIS_CLIENTS_CONFIG.retryTimes) {\n      return Math.min(times * REDIS_CLIENTS_CONFIG.retryDelay, 2000);\n    }\n\n    return false;\n  };\n\n  // disable function such as retry or checkIdentity\n  private dummyFn = () => undefined;\n\n  /**\n   * Normalize data to be compatible with used redis connection library\n   * @param clientMetadata\n   * @param database\n   * @param options\n   * @private\n   */\n  private async getRedisOptions(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClientOptions> {\n    const { host, port, password, username, tls, db, timeout } = database;\n\n    let tlsOptions = {};\n    if (tls) {\n      tlsOptions = {\n        tls: true,\n        ...(await this.getTLSConfig(database)),\n      };\n    }\n\n    return {\n      socket: {\n        host,\n        port,\n        family: 0, // Enable dual-stack IPv4/IPv6 (auto-detect)\n        connectTimeout: timeout,\n        ...tlsOptions,\n        reconnectStrategy: options?.useRetry\n          ? this.retryStrategy.bind(this)\n          : false,\n      },\n      username,\n      password,\n      database: isNumber(clientMetadata.db) ? clientMetadata.db : db,\n      name:\n        options?.connectionName ||\n        RedisConnectionStrategy.generateRedisConnectionName(clientMetadata),\n      // maxRetriesPerRequest: REDIS_CLIENTS_CONFIG.maxRetriesPerRequest,\n    };\n  }\n\n  /**\n   * Normalize data to be compatible with redis library cluster connection options\n   * @param clientMetadata\n   * @param database\n   * @param options\n   * @private\n   */\n  private async getRedisClusterOptions(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClusterOptions> {\n    const config = await this.getRedisOptions(\n      clientMetadata,\n      database,\n      options,\n    );\n\n    return {\n      rootNodes: [\n        {\n          socket: {\n            host: database.host,\n            port: database.port,\n          },\n        },\n      ],\n      // TODO: node-redis issue\n      // create a bug. reconnectStrategy has no effect (both in defaults.socket or rootNodes[].socket)\n      defaults: { ...config },\n      maxCommandRedirections: database.nodes ? database.nodes.length * 16 : 16, // TODO: Temporary solution\n    };\n  }\n\n  // /**\n  //  * Normalize data to be compatible with redis library sentinel connection options\n  //  * @param clientMetadata\n  //  * @param database\n  //  * @param options\n  //  * @private\n  //  */\n  // private async getRedisSentinelOptions(\n  //   clientMetadata: ClientMetadata,\n  //   database: Database,\n  //   options: IRedisConnectionOptions,\n  // ): Promise<RedisOptions> {\n  //   const { sentinelMaster } = database;\n  //\n  //   const baseOptions = await this.getRedisOptions(clientMetadata, database, options);\n  //   return {\n  //     ...baseOptions,\n  //     host: undefined,\n  //     port: undefined,\n  //     sentinels: database.nodes?.length ? database.nodes : [{ host: database.host, port: database.port }],\n  //     name: sentinelMaster?.name,\n  //     sentinelUsername: database.username,\n  //     sentinelPassword: database.password,\n  //     username: sentinelMaster?.username,\n  //     password: sentinelMaster?.password,\n  //     sentinelTLS: baseOptions.tls,\n  //     enableTLSForSentinelMode: !!baseOptions.tls, // previously was always `true` for tls connections\n  //     sentinelRetryStrategy: options?.useRetry ? this.retryStrategy : this.dummyFn,\n  //   };\n  // }\n\n  /**\n   * Normalize tls settings to be compatible with user redis connection library\n   * @param database\n   * @private\n   */\n  private async getTLSConfig(database: Database): Promise<ConnectionOptions> {\n    let config: ConnectionOptions;\n    config = {\n      rejectUnauthorized: database.verifyServerCert,\n      checkServerIdentity: this.dummyFn.bind(this),\n      servername: database.tlsServername || undefined,\n    };\n    if (database.caCert) {\n      config = {\n        ...config,\n        ca: [database.caCert.certificate],\n      };\n    }\n    if (database.clientCert) {\n      config = {\n        ...config,\n        cert: database.clientCert.certificate,\n        key: database.clientCert.key,\n      };\n    }\n\n    return config;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async createStandaloneClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient> {\n    this.logger.debug('Creating node-redis standalone client');\n\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    let tnl: SshTunnel;\n\n    try {\n      const config = await this.getRedisOptions(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      if (database.ssh) {\n        tnl = await this.sshTunnelProvider.createTunnel(\n          database,\n          database.sshOptions,\n        );\n        config.socket = {\n          ...config.socket,\n          // fix typings issue. todo: investigate/fix properly\n          ...{\n            host: tnl.serverAddress.host,\n            port: tnl.serverAddress.port,\n          },\n        };\n      }\n\n      const client = await createClient({\n        ...config,\n        // cover cases when we are connecting to sentinel as to standalone to discover master groups\n        database:\n          config.database > 0 && !database.sentinelMaster ? config.database : 0,\n      })\n        .on('error', (e): void => {\n          this.logger.error('Failed to connect to the redis database.', e);\n        })\n        .on('end', () => {\n          tnl?.close?.();\n        })\n        .connect();\n\n      return new StandaloneNodeRedisClient(\n        clientMetadata,\n        client,\n        {\n          host: database.host,\n          port: database.port,\n          connectTimeout: database.timeout,\n        },\n        database,\n      );\n    } catch (e) {\n      tnl?.close?.();\n      throw e;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async createClusterClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    let tnls: SshTunnel[] = [];\n    let standaloneClient: RedisClient;\n\n    try {\n      const config = await this.getRedisClusterOptions(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      standaloneClient = await this.createStandaloneClient(\n        clientMetadata,\n        database,\n        options,\n      );\n\n      config.rootNodes = (await discoverClusterNodes(standaloneClient)).map(\n        (rootNode) => ({\n          socket: rootNode,\n        }),\n      );\n\n      await standaloneClient.disconnect();\n\n      if (database.ssh) {\n        tnls = await Promise.all(\n          config.rootNodes.map((node) =>\n            this.sshTunnelProvider.createTunnel(\n              node.socket as Endpoint,\n              database.sshOptions,\n            ),\n          ),\n        );\n\n        // create NAT map\n        config.nodeAddressMap = {};\n        tnls.forEach((tnl) => {\n          config.nodeAddressMap[\n            `${tnl.options.targetHost}:${tnl.options.targetPort}`\n          ] = {\n            host: tnl.serverAddress.host,\n            port: tnl.serverAddress.port,\n          };\n        });\n\n        // change root nodes\n        config.rootNodes = tnls.map((tnl) => ({ socket: tnl.serverAddress }));\n      }\n\n      const client = createCluster(config);\n      client.on('error', (e): void => {\n        this.logger.error('Failed connection to the redis oss cluster', e);\n      });\n      // TODO: node-redis issue\n      // currently is not supported. https://github.com/redis/node-redis/issues/1855\n      // client.on('end', (): void => {\n      //   this.logger.warn(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION);\n      // });\n\n      // TODO: node-redis issue\n      // connect() doesn't return the client instance\n      await client.connect();\n\n      return new ClusterNodeRedisClient(\n        clientMetadata,\n        client,\n        {\n          host: database.host,\n          port: database.port,\n          connectTimeout: database.timeout,\n        },\n        database,\n      );\n    } catch (e) {\n      tnls?.forEach((tnl) => tnl?.close?.());\n      // TODO: node-redis issue\n      // Comment until resolved unhandled error during disconnection\n      // standaloneClient?.disconnect().catch();\n      throw e;\n    }\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async createSentinelClient(\n    clientMetadata: ClientMetadata,\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    // const config = await this.getRedisSentinelOptions(clientMetadata, database, options);\n\n    throw new Error('TDB sentinel client');\n    // return new Promise((resolve, reject) => {\n    //   try {\n    //     const client = new Redis(config);\n    //     client.on('error', (e): void => {\n    //       this.logger.error('Failed connection to the redis oss sentinel', e);\n    //       reject(e);\n    //     });\n    //     client.on('end', (): void => {\n    //       this.logger.error(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION);\n    //       reject(new InternalServerErrorException(ERROR_MESSAGES.SERVER_CLOSED_CONNECTION));\n    //     });\n    //     client.on('ready', (): void => {\n    //       this.logger.debug('Successfully connected to the redis oss sentinel.');\n    //       resolve(new IoredisClient(clientMetadata, client));\n    //     });\n    //   } catch (e) {\n    //     reject(e);\n    //   }\n    // });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/connection/redis.connection.strategy.spec.ts",
    "content": "import { ClientContext, ClientMetadata } from 'src/common/models';\nimport { RedisConnectionStrategy } from './redis.connection.strategy';\n\ndescribe('RedisConnectionStrategy', () => {\n  it('should generate a client name with all fields separated by dashes', () => {\n    const clientMetadata: ClientMetadata = {\n      databaseId: 'db123',\n      context: ClientContext.Browser,\n      db: 2,\n      uniqueId: 'uniqueCM',\n      sessionMetadata: {\n        userId: 'userSM',\n        accountId: 'accountSM',\n        sessionId: 'sessionSM',\n        uniqueId: 'uniqueSM',\n      },\n    };\n\n    const result =\n      RedisConnectionStrategy.generateRedisConnectionName(clientMetadata);\n\n    expect(result).toBe(\n      'redisinsight-browser-db123-2-uniquecm-usersm-accountsm-sessionsm-uniquesm',\n    );\n  });\n\n  // type system should prevent this from ever happening,\n  // but in case it does, we should have a default client name\n  it.each([{}, null, undefined])(\n    'should generate a default client name if all fields are missing',\n    (input) => {\n      const clientMetadata = input as ClientMetadata;\n\n      const result =\n        RedisConnectionStrategy.generateRedisConnectionName(clientMetadata);\n\n      expect(result).toBe('redisinsight-custom-------');\n    },\n  );\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/connection/redis.connection.strategy.ts",
    "content": "import { ClientMetadata } from 'src/common/models';\nimport { Database } from 'src/modules/database/models/database';\nimport { SshTunnelProvider } from 'src/modules/ssh/ssh-tunnel.provider';\nimport { HttpException, Injectable, Logger } from '@nestjs/common';\nimport { IRedisConnectionOptions } from 'src/modules/redis/redis.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { CONNECTION_NAME_GLOBAL_PREFIX, CustomErrorCodes } from 'src/constants';\n\n@Injectable()\nexport abstract class RedisConnectionStrategy {\n  protected logger = new Logger(this.constructor.name);\n\n  protected connectionErrors: {};\n\n  constructor(protected readonly sshTunnelProvider: SshTunnelProvider) {\n    this.resetConnectionErrors();\n  }\n\n  /**\n   * Try to create standalone redis connection\n   * @param clientMetadata\n   * @param database\n   * @param options\n   */\n  abstract createStandaloneClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient>;\n\n  /**\n   * Try to create redis cluster connection\n   * @param clientMetadata\n   * @param database\n   * @param options\n   */\n  abstract createClusterClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient>;\n\n  /**\n   * Try to create redis sentinel connection\n   * @param clientMetadata\n   * @param database\n   * @param options\n   */\n  abstract createSentinelClient(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions,\n  ): Promise<RedisClient>;\n\n  /**\n   * Generates client name based on clientMetadata fields\n   * redisinsight-<context>-<databaseId>-[dbIndex]-[uniqueId]-[userId]-[accountId]-[sessionId]-[sessionUniqueId]\n   * Examples:\n   *  cli: redisinsight-cli-658a47c1-0-fa32-de45-457a-5837\n   *  browser: redisinsight-browser-658a47c1-0--de45-457a-18db\n   * @param clientMetadata\n   */\n  static generateRedisConnectionName(clientMetadata: ClientMetadata) {\n    return [\n      CONNECTION_NAME_GLOBAL_PREFIX,\n      clientMetadata?.context || 'custom',\n      clientMetadata?.databaseId || '',\n      clientMetadata?.db >= 0 ? clientMetadata.db : '',\n      clientMetadata?.uniqueId || '',\n      clientMetadata?.sessionMetadata?.userId || '',\n      clientMetadata?.sessionMetadata?.accountId || '',\n      clientMetadata?.sessionMetadata?.sessionId || '',\n      clientMetadata?.sessionMetadata?.uniqueId || '',\n    ]\n      .join('-')\n      .toLowerCase();\n  }\n\n  private resetConnectionErrors() {\n    this.connectionErrors = {\n      [CustomErrorCodes.RedisConnectionFailed]: 0,\n      [CustomErrorCodes.RedisConnectionTimeout]: 0,\n      [CustomErrorCodes.RedisConnectionUnauthorized]: 0,\n      [CustomErrorCodes.RedisConnectionClusterNodesUnavailable]: 0,\n      [CustomErrorCodes.RedisConnectionUnavailable]: 0,\n    };\n  }\n\n  protected addConnectionError(error: HttpException) {\n    const errorCode = error.getResponse?.()?.['errorCode'];\n\n    if (this.connectionErrors[errorCode] !== undefined) {\n      this.connectionErrors[errorCode] += 1;\n    }\n  }\n\n  public getConnectionErrorsAndReset() {\n    const connectionErrors = {\n      ...this.connectionErrors,\n    };\n\n    this.resetConnectionErrors();\n\n    return connectionErrors;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/client-not-found-error.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\n\nexport class ClientNotFoundErrorException extends HttpException {\n  constructor(\n    response: string | Record<string, any> = {\n      message: 'Client not found or it has been disconnected.',\n      name: 'ClientNotFoundError',\n      statusCode: 404,\n    },\n    status = 404,\n  ) {\n    super(response, status);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/index.ts",
    "content": "export * from './redis-connection-auth-unsupported.exception';\nexport * from './redis-connection-cluster-nodes-unavailable.exception';\nexport * from './redis-connection-default-user-disabled.exception';\nexport * from './redis-connection-failed.exception';\nexport * from './redis-connection-incorrect-certificate.exception';\nexport * from './redis-connection-sentinel-master-required.exception';\nexport * from './redis-connection-timeout.exception';\nexport * from './redis-connection-unauthorized.exception';\nexport * from './redis-connection-unavailable.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-auth-unsupported.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionAuthUnsupportedException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.COMMAND_NOT_SUPPORTED('auth'),\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionAuthUnsupportedException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionAuthUnsupported,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-cluster-nodes-unavailable.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionClusterNodesUnavailableException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.DB_CLUSTER_CONNECT_FAILED,\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionClusterNodesUnavailableException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionClusterNodesUnavailable,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-default-user-disabled.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionDefaultUserDisabledException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.DATABASE_DEFAULT_USER_DISABLED,\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionDefaultUserDisabledException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionDefaultUserDisabled,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts",
    "content": "import {\n  HttpException,\n  HttpExceptionOptions,\n  HttpStatus,\n} from '@nestjs/common';\nimport { isString } from 'lodash';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\n// The HTTP 424 Failed Dependency client error response status code indicates\n// that the method could not be performed on the resource because the requested action\n// depended on another action, and that action failed.\nexport const RedisConnectionFailedStatusCode = HttpStatus.FAILED_DEPENDENCY;\n\nexport class RedisConnectionFailedException extends HttpException {\n  constructor(\n    message:\n      | string\n      | Record<string, any> = ERROR_MESSAGES.REDIS_CONNECTION_FAILED,\n    options?: HttpExceptionOptions,\n  ) {\n    let response: Record<string, any>;\n\n    if (isString(message)) {\n      response = {\n        message,\n        error: 'RedisConnectionFailedException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionFailed,\n      };\n    } else {\n      response = message;\n    }\n\n    super(response, RedisConnectionFailedStatusCode, options);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-incorrect-certificate.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionIncorrectCertificateException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.INCORRECT_CERTIFICATES('this host'),\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionIncorrectCertificateException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionIncorrectCertificate,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-sentinel-master-required.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionSentinelMasterRequiredException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.SENTINEL_MASTER_NAME_REQUIRED,\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionSentinelMasterRequiredException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionSentinelMasterRequired,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-timeout.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionTimeoutException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.CONNECTION_TIMEOUT,\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionTimeoutException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionTimeout,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unauthorized.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionUnauthorizedException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.AUTHENTICATION_FAILED(),\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionUnauthorizedException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionUnauthorized,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unavailable.exception.ts",
    "content": "import { HttpExceptionOptions } from '@nestjs/common';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  RedisConnectionFailedException,\n  RedisConnectionFailedStatusCode,\n} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception';\n\nexport class RedisConnectionUnavailableException extends RedisConnectionFailedException {\n  constructor(\n    message: string = ERROR_MESSAGES.INCORRECT_DATABASE_URL('this host'),\n    options?: HttpExceptionOptions,\n  ) {\n    super(\n      {\n        message,\n        error: 'RedisConnectionUnavailableException',\n        statusCode: RedisConnectionFailedStatusCode,\n        errorCode: CustomErrorCodes.RedisConnectionUnavailable,\n      },\n      options,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/local.redis.client.factory.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockClientMetadata,\n  mockClusterDatabaseWithTlsAuth,\n  mockClusterRedisClient,\n  mockConstantsProvider,\n  mockDatabase,\n  mockFeatureRedisClient,\n  mockFeatureService,\n  mockIoRedisRedisConnectionStrategy,\n  mockNodeRedisConnectionStrategy,\n  mockSentinelDatabaseWithTlsAuth,\n  mockSentinelRedisClient,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n  MockType,\n} from 'src/__mocks__';\nimport { Database } from 'src/modules/database/models/database';\nimport {\n  RedisClientFactory,\n  RedisClientLib,\n} from 'src/modules/redis/redis.client.factory';\nimport { IoredisRedisConnectionStrategy } from 'src/modules/redis/connection/ioredis.redis.connection.strategy';\nimport { NodeRedisConnectionStrategy } from 'src/modules/redis/connection/node.redis.connection.strategy';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { LocalRedisClientFactory } from 'src/modules/redis/local.redis.client.factory';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\njest.mock('ioredis', () => ({\n  ...(jest.requireActual('ioredis') as object),\n}));\n\ndescribe('LocalRedisClientFactory', () => {\n  let module: TestingModule;\n  let service: LocalRedisClientFactory;\n  let ioredisRedisConnectionStrategy: MockType<IoredisRedisConnectionStrategy>;\n  let nodeRedisConnectionStrategy: MockType<NodeRedisConnectionStrategy>;\n  let featureService: MockType<FeatureService>;\n\n  beforeEach(async () => {\n    module = await Test.createTestingModule({\n      providers: [\n        LocalRedisClientFactory,\n        {\n          provide: IoredisRedisConnectionStrategy,\n          useFactory: mockIoRedisRedisConnectionStrategy,\n        },\n        {\n          provide: NodeRedisConnectionStrategy,\n          useFactory: mockNodeRedisConnectionStrategy,\n        },\n        {\n          provide: FeatureService,\n          useFactory: mockFeatureService,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(LocalRedisClientFactory);\n    ioredisRedisConnectionStrategy = await module.get(\n      IoredisRedisConnectionStrategy,\n    );\n    nodeRedisConnectionStrategy = await module.get(NodeRedisConnectionStrategy);\n    featureService = await module.get(FeatureService);\n\n    featureService.getByName.mockResolvedValue(mockFeatureRedisClient);\n  });\n\n  describe('init', () => {\n    it('should set ioredis as default strategy from config', async () => {\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(featureService.getByName).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.RedisClient,\n      );\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n    });\n    it('should set default (ioredis for now) strategy if unknown strategy in the config', async () => {\n      featureService.getByName.mockResolvedValueOnce({\n        ...mockFeatureRedisClient,\n        data: {\n          strategy: 'jredis',\n        },\n      });\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(featureService.getByName).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.RedisClient,\n      );\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n    });\n    it('should set default (ioredis for now) strategy if no feature found', async () => {\n      featureService.getByName.mockResolvedValueOnce(undefined);\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(featureService.getByName).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.RedisClient,\n      );\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n    });\n    it('should set default (ioredis for now) strategy if no feature.data specified', async () => {\n      featureService.getByName.mockResolvedValueOnce({\n        ...mockFeatureRedisClient,\n        data: undefined,\n      });\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(featureService.getByName).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.RedisClient,\n      );\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n    });\n    it('should set default (ioredis for now) strategy if no feature.data.strategy specified', async () => {\n      featureService.getByName.mockResolvedValueOnce({\n        ...mockFeatureRedisClient,\n        data: {},\n      });\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(featureService.getByName).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        KnownFeatures.RedisClient,\n      );\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n    });\n    it('should set node-redis as default strategy from config', async () => {\n      featureService.getByName.mockResolvedValueOnce({\n        ...mockFeatureRedisClient,\n        data: {\n          strategy: 'node-redis',\n        },\n      });\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.NODE_REDIS);\n    });\n    it('should nor fail in case of an error', async () => {\n      featureService.getByName.mockRejectedValueOnce(\n        new Error('Unable to get config'),\n      );\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n\n      await service.init();\n\n      expect(service['defaultConnectionStrategy']['lib']) // lib field doesn't exist in the not mocked implementation\n        .toEqual(RedisClientLib.IOREDIS);\n    });\n  });\n\n  describe('createClientAutomatically', () => {\n    it('should create standalone client (when unable to create cluster and sentinel)', async () => {\n      ioredisRedisConnectionStrategy.createClusterClient.mockRejectedValueOnce(\n        new Error('not a cluster'),\n      );\n      const result = await service['createClientAutomatically'](\n        mockClientMetadata,\n        mockDatabase,\n      );\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createSentinelClient,\n      ).not.toHaveBeenCalled();\n      expect(\n        ioredisRedisConnectionStrategy.createClusterClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        mockDatabase,\n        RedisClientFactory.prepareConnectionOptions(),\n      );\n      expect(\n        ioredisRedisConnectionStrategy.createStandaloneClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        mockDatabase,\n        RedisClientFactory.prepareConnectionOptions(),\n      );\n    });\n    it('should create cluster client', async () => {\n      const result = await service['createClientAutomatically'](\n        mockClientMetadata,\n        mockClusterDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockClusterRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createSentinelClient,\n      ).not.toHaveBeenCalled();\n      expect(\n        ioredisRedisConnectionStrategy.createClusterClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        mockClusterDatabaseWithTlsAuth,\n        { useRetry: true },\n      );\n      expect(\n        ioredisRedisConnectionStrategy.createStandaloneClient,\n      ).not.toHaveBeenCalled();\n    });\n    it('should create sentinel client', async () => {\n      const result = await service['createClientAutomatically'](\n        mockClientMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockSentinelRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createSentinelClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n        { useRetry: true },\n      );\n      expect(\n        ioredisRedisConnectionStrategy.createClusterClient,\n      ).not.toHaveBeenCalled();\n      expect(\n        ioredisRedisConnectionStrategy.createStandaloneClient,\n      ).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('createClient', () => {\n    it('should create standalone client (default ioredis strategy)', async () => {\n      const result = await service.createClient(\n        mockClientMetadata,\n        mockDatabase,\n      );\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createStandaloneClient,\n      ).toHaveBeenCalledWith(mockClientMetadata, mockDatabase, {\n        useRetry: true,\n      });\n    });\n    it('should create standalone client (ioredis strategy)', async () => {\n      const result = await service.createClient(\n        mockClientMetadata,\n        mockDatabase,\n        {\n          clientLib: RedisClientLib.IOREDIS,\n        },\n      );\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createStandaloneClient,\n      ).toHaveBeenCalledWith(mockClientMetadata, mockDatabase, {\n        useRetry: true,\n        clientLib: RedisClientLib.IOREDIS,\n      });\n    });\n    it('should create standalone client (node-redis strategy)', async () => {\n      const result = await service.createClient(\n        mockClientMetadata,\n        mockDatabase,\n        {\n          clientLib: RedisClientLib.NODE_REDIS,\n        },\n      );\n\n      expect(result).toEqual(mockStandaloneRedisClient);\n      expect(\n        nodeRedisConnectionStrategy.createStandaloneClient,\n      ).toHaveBeenCalledWith(mockClientMetadata, mockDatabase, {\n        useRetry: true,\n        clientLib: RedisClientLib.NODE_REDIS,\n      });\n    });\n    it('should trigger auto discovery connection type (when no connectionType defined)', async () => {\n      const mockDatabaseWithoutConnectionType = Object.assign(new Database(), {\n        ...mockDatabase,\n        connectionType: null,\n      });\n\n      const result = await service.createClient(\n        mockClientMetadata,\n        mockDatabaseWithoutConnectionType,\n      );\n\n      expect(result).toEqual(mockClusterRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createClusterClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        {\n          ...mockDatabaseWithoutConnectionType,\n          connectionType: undefined,\n        },\n        RedisClientFactory.prepareConnectionOptions(),\n      );\n    });\n    it('should create cluster client', async () => {\n      const result = await service.createClient(\n        mockClientMetadata,\n        mockClusterDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockClusterRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createClusterClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        mockClusterDatabaseWithTlsAuth,\n        { useRetry: true },\n      );\n    });\n    it('should create sentinel client', async () => {\n      const result = await service.createClient(\n        mockClientMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual(mockSentinelRedisClient);\n      expect(\n        ioredisRedisConnectionStrategy.createSentinelClient,\n      ).toHaveBeenCalledWith(\n        mockClientMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n        { useRetry: true },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/local.redis.client.factory.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { RedisConnectionStrategy } from 'src/modules/redis/connection/redis.connection.strategy';\nimport { IoredisRedisConnectionStrategy } from 'src/modules/redis/connection/ioredis.redis.connection.strategy';\nimport { FeatureService } from 'src/modules/feature/feature.service';\nimport { NodeRedisConnectionStrategy } from 'src/modules/redis/connection/node.redis.connection.strategy';\nimport { KnownFeatures } from 'src/modules/feature/constants';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\n\n@Injectable()\nexport class LocalRedisClientFactory extends RedisClientFactory {\n  protected logger = new Logger('LocalRedisClientFactory');\n\n  protected defaultConnectionStrategy: RedisConnectionStrategy;\n\n  constructor(\n    protected readonly ioredisConnectionStrategy: IoredisRedisConnectionStrategy,\n    protected readonly nodeRedisConnectionStrategy: NodeRedisConnectionStrategy,\n    private readonly featureService: FeatureService,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {\n    super(ioredisConnectionStrategy, nodeRedisConnectionStrategy);\n  }\n\n  /**\n   * @inheritDoc\n   * In case of an error or unsupported strategy default config will stay the same (ioredis for now)\n   */\n  async init() {\n    try {\n      const feature = await this.featureService.getByName(\n        // todo: [USER_CONTEXT] revise\n        this.constantsProvider.getSystemSessionMetadata(),\n        KnownFeatures.RedisClient,\n      );\n      this.defaultConnectionStrategy = this.getConnectionStrategy(\n        feature?.data?.strategy,\n      );\n    } catch (e) {\n      this.logger.warn(\n        'Unable to setup default strategy from the feature config',\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/redis.client.factory.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { Database } from 'src/modules/database/models/database';\nimport { cloneClassInstance } from 'src/utils';\nimport { ConnectionType } from 'src/modules/database/entities/database.entity';\nimport { ClientMetadata } from 'src/common/models';\nimport { RedisConnectionStrategy } from 'src/modules/redis/connection/redis.connection.strategy';\nimport { IoredisRedisConnectionStrategy } from 'src/modules/redis/connection/ioredis.redis.connection.strategy';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { NodeRedisConnectionStrategy } from 'src/modules/redis/connection/node.redis.connection.strategy';\nimport serverConfig from 'src/utils/config';\n\nconst REDIS_CLIENTS_CONFIG = serverConfig.get('redis_clients');\n\nexport enum RedisClientLib {\n  IOREDIS = 'ioredis',\n  NODE_REDIS = 'node-redis',\n}\n\nexport interface IRedisConnectionOptions {\n  useRetry?: boolean;\n  connectionName?: string;\n  clientLib?: RedisClientLib;\n  enableReadyCheck?: boolean;\n}\n\n@Injectable()\nexport abstract class RedisClientFactory {\n  protected logger = new Logger('RedisClientFactory');\n\n  protected defaultConnectionStrategy: RedisConnectionStrategy;\n\n  protected constructor(\n    protected readonly ioredisConnectionStrategy: IoredisRedisConnectionStrategy,\n    protected readonly nodeRedisConnectionStrategy: NodeRedisConnectionStrategy,\n  ) {\n    this.defaultConnectionStrategy = ioredisConnectionStrategy;\n  }\n\n  /**\n   * Initialize provider with default value(s).\n   * Currently, set default client strategy based on feature flag\n   */\n  abstract init(): Promise<void>;\n\n  /**\n   * Get strategy to create connection with\n   * Default strategy is set during class initialization (ioredis for now) and overwritten\n   * by feature config on module init\n   * @param strategy\n   * @private\n   */\n  public getConnectionStrategy(\n    strategy?: RedisClientLib,\n  ): RedisConnectionStrategy {\n    switch (strategy || REDIS_CLIENTS_CONFIG.forceStrategy) {\n      case RedisClientLib.NODE_REDIS:\n        return this.nodeRedisConnectionStrategy;\n      case RedisClientLib.IOREDIS:\n        return this.ioredisConnectionStrategy;\n      default:\n        return this.defaultConnectionStrategy;\n    }\n  }\n\n  /**\n   * Based on data fields (except connectionType) will try to create connection of proper type\n   * @param clientMetadata\n   * @param database\n   * @param options\n   */\n  private async createClientAutomatically(\n    clientMetadata: ClientMetadata,\n    database: Database,\n    options: IRedisConnectionOptions = {},\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    const opts = RedisClientFactory.prepareConnectionOptions(options);\n    const connectionStrategy = this.getConnectionStrategy(opts.clientLib);\n\n    // try sentinel connection\n    if (database.sentinelMaster) {\n      try {\n        return connectionStrategy.createSentinelClient(\n          clientMetadata,\n          database,\n          opts,\n        );\n      } catch (e) {\n        // ignore error\n      }\n    }\n\n    // try cluster connection\n    try {\n      return await connectionStrategy.createClusterClient(\n        clientMetadata,\n        database,\n        opts,\n      );\n    } catch (e) {\n      // ignore error\n    }\n\n    // Standalone in any other case\n    return connectionStrategy.createStandaloneClient(\n      clientMetadata,\n      database,\n      opts,\n    );\n  }\n\n  /**\n   * Create connection based on connectionType or try to determine connectionType automatically\n   * @param clientMetadata\n   * @param db\n   * @param options\n   */\n  public async createClient(\n    clientMetadata: ClientMetadata,\n    db: Database,\n    options: IRedisConnectionOptions = {},\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    const database = cloneClassInstance(db);\n\n    Object.keys(database).forEach((key: string) => {\n      if (database[key] === null) {\n        delete database[key];\n      }\n    });\n\n    const opts = RedisClientFactory.prepareConnectionOptions(options);\n    const connectionStrategy = this.getConnectionStrategy(opts.clientLib);\n\n    let client: RedisClient;\n\n    switch (database.connectionType) {\n      case ConnectionType.STANDALONE:\n        client = await connectionStrategy.createStandaloneClient(\n          clientMetadata,\n          database,\n          opts,\n        );\n        break;\n      case ConnectionType.CLUSTER:\n        if (database.forceStandalone) {\n          this.logger.debug('Force standalone connection', {\n            clientMetadata,\n            database,\n          });\n          // if force standalone, ignore connectionType\n          client = await connectionStrategy.createStandaloneClient(\n            clientMetadata,\n            database,\n            opts,\n          );\n        } else {\n          client = await connectionStrategy.createClusterClient(\n            clientMetadata,\n            database,\n            opts,\n          );\n        }\n        break;\n      case ConnectionType.SENTINEL:\n        client = await connectionStrategy.createSentinelClient(\n          clientMetadata,\n          database,\n          opts,\n        );\n        break;\n      default:\n        // AUTO\n        client = await this.createClientAutomatically(\n          clientMetadata,\n          database,\n          opts,\n        );\n    }\n\n    return client;\n  }\n\n  static prepareConnectionOptions(\n    options: IRedisConnectionOptions = {},\n  ): IRedisConnectionOptions {\n    return {\n      useRetry: true,\n      // todo: generate connection name based on clientMetadata\n      ...options,\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/redis.client.storage.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  generateMockRedisClient,\n  mockDatabase,\n  mockInvalidClientMetadataError,\n  mockInvalidSessionMetadataError,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport {\n  ClientContext,\n  ClientMetadata,\n  SessionMetadata,\n} from 'src/common/models';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { RedisClientEvents } from 'src/constants';\nimport apiConfig from 'src/utils/config';\nimport { BadRequestException } from '@nestjs/common';\nimport { RedisClient } from 'src/modules/redis/client';\n\nconst REDIS_CLIENTS_CONFIG = apiConfig.get('redis_clients');\n\nconst mockEventEmitter = {\n  emit: jest.fn(),\n};\n\ndescribe('RedisClientStorage', () => {\n  let service: RedisClientStorage;\n  let eventEmitter: EventEmitter2;\n  const mockClientMetadata1 = {\n    sessionMetadata: {\n      userId: 'u1',\n      accountId: 'a1',\n      sessionId: 's1',\n    },\n    databaseId: mockDatabase.id,\n    context: ClientContext.Common,\n  };\n\n  const mockRedisClient1 = generateMockRedisClient(mockClientMetadata1);\n  const mockRedisClient2 = generateMockRedisClient({\n    ...mockClientMetadata1,\n    context: ClientContext.Browser,\n    db: 0,\n  });\n  const mockRedisClient3 = generateMockRedisClient({\n    ...mockClientMetadata1,\n    sessionMetadata: { userId: 'u2', sessionId: 's2', accountId: 'a2' },\n    context: ClientContext.Workbench,\n    db: 1,\n  });\n  const mockRedisClient4 = generateMockRedisClient({\n    ...mockClientMetadata1,\n    sessionMetadata: { userId: 'u2', sessionId: 's3', accountId: 'a2' },\n    db: 2,\n  });\n  const mockRedisClient5 = generateMockRedisClient({\n    ...mockClientMetadata1,\n    databaseId: 'd2',\n    sessionMetadata: { userId: 'u2', sessionId: 's4', accountId: 'a2' },\n  });\n  const mockRedisClient6 = generateMockRedisClient({\n    ...mockClientMetadata1,\n    databaseId: 'd2',\n    sessionMetadata: { userId: 'u2', sessionId: 's4', accountId: 'a3' },\n  });\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RedisClientStorage,\n        {\n          provide: EventEmitter2,\n          useValue: mockEventEmitter,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(RedisClientStorage);\n    eventEmitter = await module.get(EventEmitter2);\n\n    service['clients'].set(mockRedisClient1.id, mockRedisClient1);\n    service['clients'].set(mockRedisClient2.id, mockRedisClient2);\n    service['clients'].set(mockRedisClient3.id, mockRedisClient3);\n    service['clients'].set(mockRedisClient4.id, mockRedisClient4);\n    service['clients'].set(mockRedisClient5.id, mockRedisClient5);\n    service['clients'].set(mockRedisClient6.id, mockRedisClient6);\n  });\n\n  afterEach(() => {\n    service.onModuleDestroy();\n  });\n\n  describe('syncClients', () => {\n    it('should not remove any client since no idle time passed', async () => {\n      expect(service['clients'].size).toEqual(6);\n\n      service['syncClients']();\n\n      expect(service['clients'].size).toEqual(6);\n    });\n\n    it('should remove client with exceeded time in idle', async () => {\n      expect(service['clients'].size).toEqual(6);\n      const toDelete = service['clients'].get(mockRedisClient1.id);\n      toDelete['lastTimeUsed'] =\n        Date.now() - REDIS_CLIENTS_CONFIG.maxIdleThreshold - 1;\n\n      service['syncClients']();\n\n      expect(service['clients'].size).toEqual(5);\n      expect(service['clients'].get(mockRedisClient1.id)).toEqual(undefined);\n    });\n    it('should remove client with exceeded time in idle and not fail in case of error', async () => {\n      expect(service['clients'].size).toEqual(6);\n      const toDelete = service['clients'].get(mockRedisClient1.id);\n      toDelete['lastTimeUsed'] =\n        Date.now() - REDIS_CLIENTS_CONFIG.maxIdleThreshold - 1;\n      mockRedisClient1.disconnect.mockRejectedValueOnce(\n        new Error('some error'),\n      );\n\n      service['syncClients']();\n\n      expect(service['clients'].size).toEqual(5);\n      expect(service['clients'].get(mockRedisClient1.id)).toEqual(undefined);\n    });\n\n    it('should emit ClientRemoved event when removing idle client', async () => {\n      const toDelete = service['clients'].get(mockRedisClient1.id);\n      toDelete['lastTimeUsed'] =\n        Date.now() - REDIS_CLIENTS_CONFIG.maxIdleThreshold - 1;\n\n      service['syncClients']();\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        RedisClientEvents.ClientRemoved,\n        {\n          clientId: mockRedisClient1.id,\n          databaseId: mockRedisClient1.clientMetadata.databaseId,\n        },\n      );\n    });\n  });\n\n  describe('get', () => {\n    it('should correctly get client instance and update last used time', async () => {\n      // eslint-disable-next-line prefer-destructuring\n      const lastTimeUsed = mockRedisClient1['lastTimeUsed'];\n\n      const result = await service.get(mockRedisClient1.id);\n\n      expect(result).toEqual(service['clients'].get(mockRedisClient1.id));\n      expect(result['lastTimeUsed']).toBeGreaterThan(lastTimeUsed);\n    });\n    it('should return null when client is is disconnected and client will be removed', async () => {\n      expect(service['clients'].get(mockRedisClient1.id)).not.toEqual(\n        undefined,\n      );\n      mockRedisClient1.isConnected.mockReturnValueOnce(false);\n\n      const result = await service.get(mockRedisClient1.id);\n\n      expect(result).toEqual(null);\n      expect(service['clients'].get(mockRedisClient1.id)).toEqual(undefined);\n    });\n    it('should not fail when there is no client', async () => {\n      const result = await service.get('not-existing');\n\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe('getByMetadata', () => {\n    it('should correctly get client instance and update last used time', async () => {\n      // eslint-disable-next-line prefer-destructuring\n      const lastTimeUsed = mockRedisClient1['lastTimeUsed'];\n\n      const result = await service.getByMetadata(\n        mockRedisClient1['clientMetadata'],\n      );\n\n      expect(result).toEqual(service['clients'].get(mockRedisClient1.id));\n      expect(result['lastTimeUsed']).toBeGreaterThan(lastTimeUsed);\n    });\n    it('should find client for CLI ignoring db parameter', async () => {\n      const mockClientMetadata = {\n        ...mockClientMetadata1,\n        db: 3,\n        context: ClientContext.CLI,\n        uniqueId: 'some-unique-id',\n      };\n\n      const mockClient = generateMockRedisClient(mockClientMetadata);\n      service['clients'].set(mockClient.id, mockClient);\n\n      const result1 = await service.getByMetadata(mockClientMetadata);\n      expect(result1).toEqual(service['clients'].get(mockClient.id));\n\n      const result2 = await service.getByMetadata({\n        ...mockClientMetadata,\n        db: 1,\n      });\n      expect(result2).toEqual(service['clients'].get(mockClient.id));\n      expect(result2).toEqual(result1);\n\n      const result3 = await service.getByMetadata({\n        ...mockClientMetadata,\n        db: 0,\n      });\n      expect(result3).toEqual(service['clients'].get(mockClient.id));\n      expect(result3).toEqual(result2);\n    });\n    it('should not fail when there is no client', async () => {\n      const result = await service.getByMetadata({\n        sessionMetadata: {\n          userId: 'uid',\n          accountId: 'acc',\n          sessionId: 'uid',\n        },\n        databaseId: 'invalid-instance-id',\n        context: ClientContext.Common,\n      });\n\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe('set', () => {\n    beforeEach(() => {\n      // @ts-ignore\n      service['clients'] = new Map();\n    });\n\n    it('should add new client', async () => {\n      expect(service['clients'].size).toEqual(0);\n\n      const result = await service.set(mockRedisClient1);\n\n      expect(result).toEqual(mockRedisClient1);\n      expect(service['clients'].size).toEqual(1);\n      expect(await service.get(mockRedisClient1.id)).toEqual(mockRedisClient1);\n    });\n\n    it('should emit ClientStored event when adding new client', async () => {\n      await service.set(mockRedisClient1);\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        RedisClientEvents.ClientStored,\n        {\n          clientId: mockRedisClient1.id,\n          databaseId: mockRedisClient1.clientMetadata.databaseId,\n        },\n      );\n    });\n\n    it('should use existing client instead of replacing with new one', async () => {\n      const existingClient = generateMockRedisClient(mockClientMetadata1);\n\n      expect(service['clients'].size).toEqual(0);\n      expect(await service.set(existingClient)).toEqual(existingClient);\n      expect(service['clients'].size).toEqual(1);\n\n      // sleep\n      await new Promise((res) => setTimeout(res, 100));\n\n      const newClient = generateMockRedisClient(mockClientMetadata1);\n      const result = await service.set(newClient);\n      expect(result).toEqual(existingClient);\n      expect(result).not.toEqual(newClient);\n      expect(service['clients'].size).toEqual(1);\n\n      expect(newClient.disconnect).toHaveBeenCalledTimes(1);\n      expect(existingClient.disconnect).not.toHaveBeenCalled();\n\n      expect(newClient.id).toEqual(existingClient.id);\n    });\n\n    it('should use new client when there is existing client but without active connection', async () => {\n      const existingClient = generateMockRedisClient(mockClientMetadata1);\n      existingClient.isConnected.mockReturnValue(false);\n\n      expect(service['clients'].size).toEqual(0);\n      expect(await service.set(existingClient)).toEqual(existingClient);\n      expect(service['clients'].size).toEqual(1);\n\n      // sleep\n      await new Promise((res) => setTimeout(res, 100));\n\n      const newClient = generateMockRedisClient(mockClientMetadata1);\n      const result = await service.set(newClient);\n      expect(result).toEqual(newClient);\n      expect(result).not.toEqual(existingClient);\n      expect(service['clients'].size).toEqual(1);\n\n      expect(existingClient.disconnect).toHaveBeenCalledTimes(1);\n      expect(newClient.disconnect).not.toHaveBeenCalled();\n\n      expect(newClient.id).toEqual(existingClient.id);\n    });\n\n    it('should emit ClientRemoved and ClientStored events when replacing disconnected client', async () => {\n      const existingClient = generateMockRedisClient(mockClientMetadata1);\n      existingClient.isConnected.mockReturnValue(false);\n\n      await service.set(existingClient);\n      jest.clearAllMocks();\n\n      const newClient = generateMockRedisClient(mockClientMetadata1);\n      await service.set(newClient);\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        RedisClientEvents.ClientRemoved,\n        {\n          clientId: existingClient.id,\n          databaseId: existingClient.clientMetadata.databaseId,\n        },\n      );\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        RedisClientEvents.ClientStored,\n        {\n          clientId: newClient.id,\n          databaseId: newClient.clientMetadata.databaseId,\n        },\n      );\n    });\n\n    it('should throw and error if clientMetadata has not required fields', async () => {\n      await expect(service.set(generateMockRedisClient({}))).rejects.toThrow(\n        BadRequestException,\n      );\n      await expect(\n        service.set(\n          generateMockRedisClient({\n            databaseId: '1',\n          }),\n        ),\n      ).rejects.toThrow(BadRequestException);\n      await expect(\n        service.set(\n          generateMockRedisClient({\n            databaseId: '1',\n            context: ClientContext.Common,\n          }),\n        ),\n      ).rejects.toThrow(BadRequestException);\n      await expect(\n        service.set(\n          generateMockRedisClient({\n            databaseId: '1',\n            context: ClientContext.Common,\n            sessionMetadata: {\n              userId: '1',\n            } as SessionMetadata,\n          }),\n        ),\n      ).rejects.toThrow(BadRequestException);\n      await expect(\n        service.set(\n          generateMockRedisClient({\n            databaseId: '1',\n            context: ClientContext.Common,\n            sessionMetadata: {\n              userId: '1',\n              sessionId: '1',\n            } as SessionMetadata,\n          }),\n        ),\n      ).rejects.toThrow(BadRequestException);\n      await expect(\n        service.set(\n          generateMockRedisClient({\n            databaseId: '1',\n            context: ClientContext.Common,\n            sessionMetadata: {\n              userId: '1',\n              accountId: '1',\n              sessionId: '1',\n            } as SessionMetadata,\n          }),\n        ),\n      ).resolves.toBeInstanceOf(RedisClient);\n    });\n  });\n\n  describe('remove', () => {\n    it('should remove only one', async () => {\n      const result = await service.remove(mockRedisClient1.id);\n\n      expect(result).toEqual(1);\n      expect(service['clients'].size).toEqual(5);\n      expect(service['clients'].get(mockRedisClient1.id)).toEqual(undefined);\n    });\n    it('should not fail in case when no client found', async () => {\n      const result = await service.remove('not-existing');\n\n      expect(result).toEqual(0);\n      expect(service['clients'].size).toEqual(6);\n    });\n    it('should not fail in case when client.disconnect() failed and remove client from the pool', async () => {\n      mockRedisClient1.disconnect.mockRejectedValueOnce(\n        new Error(\"Can't disconnect.\"),\n      );\n      const result = await service.remove(mockRedisClient1.id);\n\n      expect(result).toEqual(1);\n      expect(service['clients'].size).toEqual(5);\n    });\n\n    it('should emit ClientRemoved event when removing client', async () => {\n      await service.remove(mockRedisClient1.id);\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        RedisClientEvents.ClientRemoved,\n        {\n          clientId: mockRedisClient1.id,\n          databaseId: mockRedisClient1.clientMetadata.databaseId,\n        },\n      );\n    });\n\n    it('should not emit ClientRemoved event when client not found', async () => {\n      await service.remove('not-existing');\n\n      expect(eventEmitter.emit).not.toHaveBeenCalledWith(\n        RedisClientEvents.ClientRemoved,\n        expect.anything(),\n      );\n    });\n  });\n\n  describe('removeByMetadata', () => {\n    it('should remove only one', async () => {\n      const result = await service.removeByMetadata(\n        mockRedisClient1['clientMetadata'],\n      );\n\n      expect(result).toEqual(1);\n      expect(service['clients'].size).toEqual(5);\n      expect(service['clients'].get(mockRedisClient1.id)).toEqual(undefined);\n    });\n    it('should not fail in case when no client found', async () => {\n      const result = await service.removeByMetadata({\n        ...mockRedisClient1['clientMetadata'],\n        databaseId: 'not-existing',\n      });\n\n      expect(result).toEqual(0);\n      expect(service['clients'].size).toEqual(6);\n    });\n    // todo: add prepareMetadata check test\n  });\n\n  describe('getClientsByDatabaseField', () => {\n    beforeEach(() => {\n      // @ts-ignore\n      service['clients'] = new Map();\n    });\n\n    it('should find clients by nested database field path', async () => {\n      const clientWithField = generateMockRedisClient(\n        mockClientMetadata1,\n        jest.fn(),\n        {},\n        {\n          providerDetails: { customField: 'value-123' } as any,\n        },\n      );\n      const clientWithoutField = generateMockRedisClient({\n        ...mockClientMetadata1,\n        sessionMetadata: { userId: 'u2', sessionId: 's2', accountId: 'a2' },\n      });\n\n      service['clients'].set(clientWithField.id, clientWithField);\n      service['clients'].set(clientWithoutField.id, clientWithoutField);\n\n      const result = service.getClientsByDatabaseField(\n        'providerDetails.customField',\n        'value-123',\n      );\n\n      expect(result).toHaveLength(1);\n      expect(result[0].id).toBe(clientWithField.id);\n    });\n\n    it('should return multiple clients matching the same field value', async () => {\n      const client1 = generateMockRedisClient(\n        mockClientMetadata1,\n        jest.fn(),\n        {},\n        {\n          providerDetails: { customField: 'shared-value' } as any,\n        },\n      );\n      const client2 = generateMockRedisClient(\n        {\n          ...mockClientMetadata1,\n          sessionMetadata: { userId: 'u2', sessionId: 's2', accountId: 'a2' },\n        },\n        jest.fn(),\n        {},\n        {\n          providerDetails: { customField: 'shared-value' } as any,\n        },\n      );\n\n      service['clients'].set(client1.id, client1);\n      service['clients'].set(client2.id, client2);\n\n      const result = service.getClientsByDatabaseField(\n        'providerDetails.customField',\n        'shared-value',\n      );\n\n      expect(result).toHaveLength(2);\n    });\n\n    it('should return empty array when no clients match', async () => {\n      const client = generateMockRedisClient(\n        mockClientMetadata1,\n        jest.fn(),\n        {},\n        {\n          providerDetails: { customField: 'some-value' } as any,\n        },\n      );\n\n      service['clients'].set(client.id, client);\n\n      const result = service.getClientsByDatabaseField(\n        'providerDetails.customField',\n        'non-existent-value',\n      );\n\n      expect(result).toHaveLength(0);\n    });\n\n    it('should return empty array when field path does not exist', async () => {\n      const client = generateMockRedisClient(mockClientMetadata1);\n\n      service['clients'].set(client.id, client);\n\n      const result = service.getClientsByDatabaseField(\n        'providerDetails.customField',\n        'any-value',\n      );\n\n      expect(result).toHaveLength(0);\n    });\n  });\n\n  describe('findClients + removeManyByMetadata', () => {\n    it('should correctly find clients for particular database', async () => {\n      const query = {\n        databaseId: mockClientMetadata1.databaseId,\n      };\n\n      const result = service['findClients'](query);\n\n      expect(result.length).toEqual(4);\n      result.forEach((id) => {\n        expect(service['clients'].get(id)['clientMetadata'].databaseId).toEqual(\n          query.databaseId,\n        );\n      });\n\n      expect(await service.removeManyByMetadata(query)).toEqual(4);\n      expect(service['clients'].size).toEqual(2);\n    });\n    it('should correctly find clients for particular database and context', async () => {\n      const query = {\n        databaseId: mockClientMetadata1.databaseId,\n        context: ClientContext.Browser,\n      };\n\n      const result = service['findClients'](query);\n\n      expect(result.length).toEqual(1);\n      result.forEach((id) => {\n        expect(service['clients'].get(id)['clientMetadata'].databaseId).toEqual(\n          query.databaseId,\n        );\n        expect(service['clients'].get(id)['clientMetadata'].context).toEqual(\n          query.context,\n        );\n      });\n\n      expect(await service.removeManyByMetadata(query)).toEqual(1);\n      expect(service['clients'].size).toEqual(5);\n    });\n    it('should correctly find clients for particular database and user', async () => {\n      const query = {\n        sessionMetadata: { userId: 'u1' } as SessionMetadata,\n        databaseId: mockDatabase.id,\n      };\n\n      const result = service['findClients'](query);\n\n      expect(result.length).toEqual(2);\n      result.forEach((id) => {\n        expect(service['clients'].get(id)['clientMetadata'].databaseId).toEqual(\n          query.databaseId,\n        );\n        expect(\n          service['clients'].get(id)['clientMetadata'].sessionMetadata.userId,\n        ).toEqual(query.sessionMetadata.userId);\n      });\n\n      expect(await service.removeManyByMetadata(query)).toEqual(2);\n      expect(service['clients'].size).toEqual(4);\n    });\n    it('should correctly find clients for particular user', async () => {\n      const query = {\n        sessionMetadata: { userId: 'u2' } as SessionMetadata,\n      };\n\n      const result = service['findClients'](query);\n\n      expect(result.length).toEqual(4);\n      result.forEach((id) => {\n        expect(\n          service['clients'].get(id)['clientMetadata'].sessionMetadata.userId,\n        ).toEqual(query.sessionMetadata.userId);\n      });\n      expect(\n        service['clients'].get(result[0])['clientMetadata'].databaseId,\n      ).toEqual(mockDatabase.id);\n      expect(\n        service['clients'].get(result[2])['clientMetadata'].databaseId,\n      ).toEqual('d2');\n\n      expect(await service.removeManyByMetadata(query)).toEqual(4);\n      expect(service['clients'].size).toEqual(2);\n    });\n    it('should correctly find clients for particular database and db index', async () => {\n      const query = {\n        databaseId: mockDatabase.id,\n        db: 0,\n      };\n\n      const result = service['findClients'](query);\n\n      expect(result.length).toEqual(1);\n      result.forEach((id) => {\n        expect(service['clients'].get(id)['clientMetadata'].databaseId).toEqual(\n          query.databaseId,\n        );\n        expect(service['clients'].get(id)['clientMetadata'].db).toEqual(\n          query.db,\n        );\n      });\n\n      expect(await service.removeManyByMetadata(query)).toEqual(1);\n      expect(service['clients'].size).toEqual(5);\n    });\n    it('should not find any instances', async () => {\n      const query = {\n        databaseId: 'not existing',\n      };\n\n      const result = service['findClients'](query);\n\n      expect(result).toEqual([]);\n\n      expect(await service.removeManyByMetadata(query)).toEqual(0);\n      expect(service['clients'].size).toEqual(6);\n    });\n  });\n\n  describe('advanced', () => {\n    beforeEach(() => {\n      // @ts-ignore\n      service['clients'] = new Map();\n    });\n\n    const CLIENTS_NUMBER = 10;\n    const getGenericValue = (value, defaultValue) =>\n      value === false ? undefined : value || defaultValue;\n\n    const generateNClients = (n: number, options = {}) => {\n      const result = [];\n\n      for (let i = 0; i < n; i += 1) {\n        const clientMetadata = Object.assign(new ClientMetadata(), {\n          databaseId: getGenericValue(options['databaseId'], `db_${i}`),\n          context: getGenericValue(options['context'], ClientContext.Common),\n          uniqueId: getGenericValue(options['uniqueId'], `unique_${i}`),\n          db: getGenericValue(options['db'], 0),\n          sessionMetadata: {\n            userId: getGenericValue(options['userId'], `user_${i}`),\n            accountId: getGenericValue(options['accountId'], `account_${i}`),\n            sessionId: getGenericValue(options['sessionId'], `session_${i}`),\n            uniqueId: getGenericValue(\n              options['sessionUId'],\n              `session_unique_${i}`,\n            ),\n          },\n        });\n        result.push([clientMetadata, generateMockRedisClient(clientMetadata)]);\n      }\n\n      return result;\n    };\n\n    it.each([\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          uniqueId: 'unid',\n          db: 0,\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_unid_0_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          uniqueId: 'unid',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_unid_(nil)_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_(nil)_(nil)_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        id: 'dbid_Common_(nil)_(nil)_uid_aid_sid_(nil)',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidClientMetadataError,\n      },\n      {\n        clientMetadata: {\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidClientMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {},\n        error: mockInvalidSessionMetadataError,\n      },\n    ] as any)(\n      '%# set: validation and id generation',\n      async ({ clientMetadata, id, error }) => {\n        const mapSetSpy = jest.spyOn(service['clients'], 'set');\n\n        const client = generateMockRedisClient(clientMetadata);\n\n        if (error) {\n          await expect(service.set(client)).rejects.toThrow(error);\n          expect(mapSetSpy).not.toHaveBeenCalled();\n        } else {\n          await expect(service.set(client)).resolves.toEqual(client);\n          expect(mapSetSpy).toHaveBeenCalledTimes(1);\n          expect(mapSetSpy).lastCalledWith(id, client);\n        }\n      },\n    );\n\n    it.each([\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          uniqueId: 'unid',\n          db: 0,\n          sessionMetadata: {\n            userId: 'uid',\n            sessionId: 'sid',\n            accountId: 'aid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_unid_0_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          uniqueId: 'unid',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_unid_(nil)_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_(nil)_(nil)_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        id: 'dbid_Common_(nil)_(nil)_uid_aid_sid_(nil)',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidClientMetadataError,\n      },\n      {\n        clientMetadata: {\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidClientMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {},\n        error: mockInvalidSessionMetadataError,\n      },\n    ] as any)(\n      '%# getByMetadata: validation and id generation',\n      async ({ clientMetadata, id, error }) => {\n        const mapGetSpy = jest.spyOn(service['clients'], 'get');\n\n        if (error) {\n          await expect(service.getByMetadata(clientMetadata)).rejects.toThrow(\n            error,\n          );\n          expect(mapGetSpy).not.toHaveBeenCalled();\n        } else {\n          await expect(service.getByMetadata(clientMetadata)).resolves.toEqual(\n            undefined,\n          );\n          expect(mapGetSpy).toHaveBeenCalledTimes(1);\n          expect(mapGetSpy).lastCalledWith(id);\n        }\n      },\n    );\n\n    it.each([\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          uniqueId: 'unid',\n          db: 0,\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_unid_0_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          uniqueId: 'unid',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_unid_(nil)_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n            uniqueId: 'unsid',\n          },\n        },\n        id: 'dbid_Common_(nil)_(nil)_uid_aid_sid_unsid',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        id: 'dbid_Common_(nil)_(nil)_uid_aid_sid_(nil)',\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidClientMetadataError,\n      },\n      {\n        clientMetadata: {\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidClientMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            accountId: 'aid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            accountId: 'aid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {\n          databaseId: 'dbid',\n          context: 'Common',\n          sessionMetadata: {\n            userId: 'uid',\n            sessionId: 'sid',\n          },\n        },\n        error: mockInvalidSessionMetadataError,\n      },\n      {\n        clientMetadata: {},\n        error: mockInvalidSessionMetadataError,\n      },\n    ] as any)(\n      '%# removeByMetadata: validation and id generation',\n      async ({ clientMetadata, id, error }) => {\n        jest\n          .spyOn(service['clients'], 'get')\n          .mockReturnValue(mockStandaloneRedisClient);\n        const mapDeleteSpy = jest.spyOn(service['clients'], 'delete');\n\n        if (error) {\n          await expect(\n            service.removeByMetadata(clientMetadata),\n          ).rejects.toThrow(error);\n          expect(mapDeleteSpy).not.toHaveBeenCalled();\n        } else {\n          await expect(\n            service.removeByMetadata(clientMetadata),\n          ).resolves.toEqual(1);\n          expect(mapDeleteSpy).toHaveBeenCalledTimes(1);\n          expect(mapDeleteSpy).lastCalledWith(id);\n        }\n      },\n    );\n\n    it('check common use cases', async () => {\n      expect(service['clients'].size).toEqual(0);\n\n      const clients = generateNClients(CLIENTS_NUMBER);\n\n      await Promise.all(\n        clients.map(async ([cm, client]) => {\n          expect(client.clientMetadata).toEqual(cm);\n\n          // should set each client with expect id\n          await service.set(client);\n\n          // should get each client with expect id\n          expect(await service.getByMetadata(cm)).toEqual(client);\n        }),\n      );\n\n      expect(service['clients'].size).toEqual(CLIENTS_NUMBER);\n\n      expect(await service.getByMetadata(clients[0][0])).toEqual(clients[0][1]);\n      await service.removeByMetadata(clients[0][0]);\n      expect(await service.getByMetadata(clients[0][0])).toEqual(undefined);\n\n      expect(service['clients'].size).toEqual(CLIENTS_NUMBER - 1);\n\n      expect(await service.getByMetadata(clients[1][0])).toEqual(clients[1][1]);\n      await service.removeByMetadata(clients[1][0]);\n      expect(await service.getByMetadata(clients[1][0])).toEqual(undefined);\n\n      expect(service['clients'].size).toEqual(CLIENTS_NUMBER - 2);\n\n      await Promise.all(\n        clients.map(async ([cm, client]) => {\n          expect(client.clientMetadata).toEqual(cm);\n\n          // should set each client with expect id\n          await service.removeByMetadata(cm);\n        }),\n      );\n\n      expect(service['clients'].size).toEqual(0);\n    });\n\n    describe('remove many', () => {\n      it('remove all', async () => {\n        expect(service['clients'].size).toEqual(0);\n\n        const clients = generateNClients(100);\n\n        await Promise.all(\n          [...clients].map(async ([, client]) => {\n            await service.set(client);\n          }),\n        );\n\n        expect(service['clients'].size).toEqual(100);\n\n        // removes all clients when no any field specified\n        await service.removeManyByMetadata({});\n\n        expect(service['clients'].size).toEqual(0);\n      });\n\n      it.each([\n        [\n          { databaseId: 'db' },\n          { databaseId: 'not-existing' },\n          { databaseId: 'db' },\n        ],\n        [\n          { context: 'Browser' },\n          { context: 'not-existing' },\n          { context: 'Browser' },\n        ],\n        [\n          { uniqueId: 'uuid' },\n          { uniqueId: 'not-existing' },\n          { uniqueId: 'uuid' },\n        ],\n        [{ db: 1 }, { db: 2 }, { db: 1 }],\n        [\n          { userId: 'uid' },\n          { sessionMetadata: { userId: 'not-existing' } },\n          { sessionMetadata: { userId: 'uid' } },\n        ],\n        [\n          { sessionId: 'sid' },\n          { sessionMetadata: { sessionId: 'not-existing' } },\n          { sessionMetadata: { sessionId: 'sid' } },\n        ],\n        // compound\n        [\n          { databaseId: 'db', sessionId: 'sid', userId: 'uid' },\n          {\n            databaseId: 'db',\n            sessionMetadata: { sessionId: 'not-existing', userId: 'uid' },\n          },\n          {\n            databaseId: 'db',\n            sessionMetadata: { sessionId: 'sid', userId: 'uid' },\n          },\n        ],\n      ] as any)(\n        'remove many by %p',\n        async (generate, notExisting, clientMetadata) => {\n          expect(service['clients'].size).toEqual(0);\n\n          const targetClients = generateNClients(100, generate);\n          const clients = generateNClients(100);\n\n          await Promise.all(\n            [...targetClients, ...clients].map(async ([, client]) => {\n              await service.set(client);\n            }),\n          );\n\n          expect(service['clients'].size).toEqual(200);\n\n          // shouldn't remove since there is no match\n          await service.removeManyByMetadata(notExisting);\n\n          expect(service['clients'].size).toEqual(200);\n\n          await service.removeManyByMetadata(clientMetadata);\n\n          expect(service['clients'].size).toEqual(100);\n\n          await Promise.all(\n            clients.map(async ([cm, client]) => {\n              expect(await service.getByMetadata(cm)).toEqual(client);\n            }),\n          );\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/redis.client.storage.ts",
    "content": "import { get, isMatch, sum } from 'lodash';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { ClientMetadata } from 'src/common/models';\nimport { RedisClientEvents } from 'src/constants';\nimport apiConfig from 'src/utils/config';\n\nconst REDIS_CLIENTS_CONFIG = apiConfig.get('redis_clients');\n\n@Injectable()\nexport class RedisClientStorage {\n  private readonly logger = new Logger('RedisClientStorage');\n\n  private readonly clients: Map<string, RedisClient> = new Map();\n\n  private readonly syncInterval: NodeJS.Timeout;\n\n  constructor(private readonly eventEmitter: EventEmitter2) {\n    this.syncInterval = setInterval(\n      this.syncClients.bind(this),\n      REDIS_CLIENTS_CONFIG.syncInterval,\n    );\n  }\n\n  onModuleDestroy() {\n    clearInterval(this.syncInterval);\n  }\n\n  public getClientsCount() {\n    return this.clients.size;\n  }\n\n  /**\n   * Disconnects and removes all clients with exceeded idle threshold\n   * @private\n   */\n  private syncClients(): void {\n    try {\n      this.clients.forEach((client) => {\n        if (client.isIdle()) {\n          const { id } = client;\n          const { databaseId } = client.clientMetadata;\n\n          client\n            .disconnect()\n            .catch((e) =>\n              this.logger.warn('Unable to disconnect client after idle', e),\n            );\n          this.clients.delete(id);\n\n          this.eventEmitter.emit(RedisClientEvents.ClientRemoved, {\n            clientId: id,\n            databaseId,\n          });\n        }\n      });\n    } catch (e) {\n      // ignore errors;\n    }\n  }\n\n  /**\n   * Finds clients by partial clientMetadata fields and returns array of ids\n   * @param clientMetadata\n   * @private\n   */\n  private findClients(clientMetadata: Partial<ClientMetadata>): string[] {\n    return [...this.clients.values()]\n      .filter((redisClient) =>\n        isMatch(redisClient['clientMetadata'], clientMetadata),\n      )\n      .map((client) => client.id);\n  }\n\n  /**\n   * Gets client by generated id\n   * Will return null if client was not found\n   * @param id\n   */\n  public async get(id: string): Promise<RedisClient> {\n    const client = this.clients.get(id);\n\n    if (client) {\n      if (!client.isConnected()) {\n        await this.remove(client.id);\n        return null;\n      }\n\n      client.setLastUsed();\n    }\n\n    return client;\n  }\n\n  /**\n   * Will generate \"id\" based on client metadata and invoke getClient method\n   * @param clientMetadata\n   */\n  public async getByMetadata(\n    clientMetadata: ClientMetadata,\n  ): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    return this.get(\n      RedisClient.generateId(RedisClient.prepareClientMetadata(clientMetadata)),\n    );\n  }\n\n  /**\n   * Saves client into the clients pool if there is no client with such \"id\"\n   * When client with such \"id\" exists:\n   * 1. If existing client has established connection - will return old client and close connection for the new one\n   * 2. If existing client hasn't established connection - will replace old client with the new one\n   * @param client\n   */\n  public async set(client: RedisClient): Promise<RedisClient> {\n    // Additional validation\n    ClientMetadata.validate(client.clientMetadata);\n\n    // it is safer to generate id based on clientMetadata each time\n    const id = RedisClient.generateId(\n      RedisClient.prepareClientMetadata(client.clientMetadata),\n    );\n\n    const existingClient = this.clients.get(id);\n\n    let removedClientDatabaseId: string | null = null;\n\n    if (existingClient) {\n      if (existingClient.isConnected()) {\n        await client.disconnect().catch();\n        return this.get(id);\n      }\n\n      removedClientDatabaseId = existingClient.clientMetadata.databaseId;\n      await existingClient.disconnect().catch();\n    }\n\n    this.clients.set(id, client);\n\n    if (removedClientDatabaseId) {\n      this.eventEmitter.emit(RedisClientEvents.ClientRemoved, {\n        clientId: id,\n        databaseId: removedClientDatabaseId,\n      });\n    }\n\n    this.eventEmitter.emit(RedisClientEvents.ClientStored, {\n      clientId: id,\n      databaseId: client.clientMetadata.databaseId,\n    });\n\n    return client;\n  }\n\n  /**\n   * Disconnect client without waiting for pending commands\n   * and removes it from the clients pool\n   * @param id\n   */\n  public async remove(id: string): Promise<number> {\n    const client = this.clients.get(id);\n\n    if (client) {\n      const { databaseId } = client.clientMetadata;\n\n      await client\n        .disconnect()\n        .catch((e) => this.logger.warn('Unable to disconnect client', e));\n\n      this.clients.delete(id);\n\n      this.eventEmitter.emit(RedisClientEvents.ClientRemoved, {\n        clientId: id,\n        databaseId,\n      });\n\n      return 1;\n    }\n\n    return 0;\n  }\n\n  /**\n   * Generate id from ClientMetadata and removes client using remove method\n   * @param clientMetadata\n   */\n  public async removeByMetadata(\n    clientMetadata: ClientMetadata,\n  ): Promise<number> {\n    // Additional validation\n    ClientMetadata.validate(clientMetadata);\n\n    return this.remove(\n      RedisClient.generateId(RedisClient.prepareClientMetadata(clientMetadata)),\n    );\n  }\n\n  /**\n   * Closes connections and removes clients by condition\n   * Useful when database was removed and there is no sense wait for \"idle\" before remove clients\n   * @param clientMetadata\n   */\n  public async removeManyByMetadata(\n    clientMetadata: Partial<ClientMetadata>,\n  ): Promise<number> {\n    const toRemove = this.findClients(clientMetadata);\n\n    this.logger.debug(`Trying to remove ${toRemove.length} clients`);\n\n    return sum(await Promise.all(toRemove.map(this.remove.bind(this))));\n  }\n\n  /**\n   * Finds all clients whose database field at the given path matches the value.\n   * Uses lodash.get to support nested paths like 'something.otherThing'.\n   * @param fieldPath - Dot-notation path to the database field\n   * @param value - Value to match\n   */\n  public getClientsByDatabaseField(\n    fieldPath: string,\n    value: unknown,\n  ): RedisClient[] {\n    return [...this.clients.values()].filter(\n      (client) => get(client.database, fieldPath) === value,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/redis.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { IoredisRedisConnectionStrategy } from 'src/modules/redis/connection/ioredis.redis.connection.strategy';\nimport { NodeRedisConnectionStrategy } from 'src/modules/redis/connection/node.redis.connection.strategy';\nimport { RedisClientStorage } from 'src/modules/redis/redis.client.storage';\nimport { LocalRedisClientFactory } from 'src/modules/redis/local.redis.client.factory';\n\n@Module({})\nexport class RedisModule {\n  static register(\n    redisClientFactory: Type<RedisClientFactory> = LocalRedisClientFactory,\n  ) {\n    return {\n      module: RedisModule,\n      providers: [\n        RedisClientStorage,\n        {\n          provide: RedisClientFactory,\n          useClass: redisClientFactory,\n        },\n        IoredisRedisConnectionStrategy,\n        NodeRedisConnectionStrategy,\n      ],\n      exports: [RedisClientStorage, RedisClientFactory],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/cluster.util.spec.ts",
    "content": "import {\n  mockRedisClusterFailInfoResponse,\n  mockRedisClusterNodesResponse,\n  mockRedisClusterOkInfoResponse,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { IRedisClusterNodeAddress, ReplyError } from 'src/models';\nimport { isCluster, discoverClusterNodes } from './cluster.util';\n\ndescribe('isCluster', () => {\n  it('cluster connection ok', async () => {\n    mockStandaloneRedisClient.sendCommand.mockResolvedValue(\n      mockRedisClusterOkInfoResponse,\n    );\n    expect(await isCluster(mockStandaloneRedisClient)).toEqual(true);\n  });\n\n  it('cluster connection false', async () => {\n    mockStandaloneRedisClient.sendCommand.mockResolvedValue(\n      mockRedisClusterFailInfoResponse,\n    );\n    expect(await isCluster(mockStandaloneRedisClient)).toEqual(false);\n  });\n  it('cluster not supported', async () => {\n    mockStandaloneRedisClient.sendCommand.mockRejectedValue({\n      name: 'ReplyError',\n      message: 'ERR This instance has cluster support disabled',\n      command: 'CLUSTER',\n    });\n    expect(await isCluster(mockStandaloneRedisClient)).toEqual(false);\n  });\n});\n\ndescribe('discoverClusterNodes', () => {\n  const mockClusterNodeAddresses: IRedisClusterNodeAddress[] = [\n    {\n      host: '127.0.0.1',\n      port: 30004,\n    },\n    {\n      host: '127.0.0.1',\n      port: 30001,\n    },\n  ];\n\n  it('should return nodes in a defined format', async () => {\n    mockStandaloneRedisClient.sendCommand.mockResolvedValue(\n      mockRedisClusterNodesResponse,\n    );\n    expect(await discoverClusterNodes(mockStandaloneRedisClient)).toEqual(\n      mockClusterNodeAddresses,\n    );\n  });\n  it('cluster not supported', async () => {\n    const replyError: ReplyError = {\n      name: 'ReplyError',\n      message: 'ERR This instance has cluster support disabled',\n      command: 'CLUSTER',\n    };\n    mockStandaloneRedisClient.sendCommand.mockRejectedValue(replyError);\n\n    try {\n      await discoverClusterNodes(mockStandaloneRedisClient);\n      fail('Should throw an error');\n    } catch (err) {\n      expect(err).toEqual(replyError);\n    }\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/cluster.util.ts",
    "content": "import { RedisClient } from 'src/modules/redis/client';\nimport {\n  convertMultilineReplyToObject,\n  parseNodesFromClusterInfoReply,\n} from 'src/modules/redis/utils/reply.util';\nimport {\n  IRedisClusterNodeAddress,\n  RedisClusterNodeLinkState,\n} from 'src/models';\n\n/**\n * Check weather database is a cluster\n * Used to automatically determine db type when connected to a database with standalone client\n * In case when \"cluster info\" command will be not allowed by ACL or in case of any other error\n * we will handle this database as a non-cluster since \"cluster info\" command is required\n * to work properly in the next steps\n * @param client\n */\nexport const isCluster = async (client: RedisClient): Promise<boolean> => {\n  try {\n    const reply = (await client.sendCommand(['cluster', 'info'], {\n      replyEncoding: 'utf8',\n    })) as string;\n\n    const clusterInfo = convertMultilineReplyToObject(reply);\n    return clusterInfo.cluster_state === 'ok';\n  } catch (e) {\n    return false;\n  }\n};\n\n/**\n * Discover all cluster nodes for current connection\n * @param client\n */\nexport const discoverClusterNodes = async (\n  client: RedisClient,\n): Promise<IRedisClusterNodeAddress[]> => {\n  const nodes = parseNodesFromClusterInfoReply(\n    (await client.sendCommand(['cluster', 'nodes'], {\n      replyEncoding: 'utf8',\n    })) as string,\n  ).filter((node) => node.linkState === RedisClusterNodeLinkState.Connected);\n\n  return nodes.map((node) => ({\n    host: node.host,\n    port: node.port,\n  }));\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/index.ts",
    "content": "export * from './reply.util';\nexport * from './keys.util';\nexport * from './sentinel.util';\nexport * from './cluster.util';\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/keys.util.spec.ts",
    "content": "import { getTotalKeys } from 'src/modules/redis/utils/keys.util';\nimport {\n  mockRedisKeyspaceInfoResponse,\n  mockRedisKeyspaceInfoResponseNoKeyspaceData,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\n\ndescribe('getTotalKeys', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  it('Should return total from dbsize', async () => {\n    mockStandaloneRedisClient.sendCommand.mockResolvedValue('1');\n    expect(await getTotalKeys(mockStandaloneRedisClient)).toEqual(1);\n    expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledTimes(1);\n    expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith(\n      ['dbsize'],\n      { replyEncoding: 'utf8' },\n    );\n  });\n\n  it('Should return total from info (when dbsize returned error)', async () => {\n    mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce(\n      new Error('some error'),\n    );\n    mockStandaloneRedisClient.getInfo.mockResolvedValueOnce(\n      convertRedisInfoReplyToObject(mockRedisKeyspaceInfoResponse),\n    );\n    expect(await getTotalKeys(mockStandaloneRedisClient)).toEqual(1);\n    expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledTimes(1);\n    expect(mockStandaloneRedisClient.sendCommand).toHaveBeenNthCalledWith(\n      1,\n      ['dbsize'],\n      { replyEncoding: 'utf8' },\n    );\n    expect(mockStandaloneRedisClient.getInfo).toHaveBeenNthCalledWith(\n      1,\n      'keyspace',\n    );\n  });\n  it(\"Should return 0 since info keyspace hasn't keys values\", async () => {\n    mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce(\n      new Error('some error'),\n    );\n    mockStandaloneRedisClient.getInfo.mockResolvedValueOnce(\n      convertRedisInfoReplyToObject(\n        mockRedisKeyspaceInfoResponseNoKeyspaceData,\n      ),\n    );\n    expect(await getTotalKeys(mockStandaloneRedisClient)).toEqual(0);\n  });\n  it('Should return 0 since info returned empty string', async () => {\n    mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce(\n      new Error('some error'),\n    );\n    mockStandaloneRedisClient.getInfo.mockResolvedValueOnce(\n      convertRedisInfoReplyToObject(''),\n    );\n    expect(await getTotalKeys(mockStandaloneRedisClient)).toEqual(0);\n  });\n  it('Should return -1 when dbsize and info returned error', async () => {\n    mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce(\n      new Error('some error'),\n    );\n    mockStandaloneRedisClient.getInfo.mockRejectedValue(\n      new Error('some error'),\n    );\n    expect(await getTotalKeys(mockStandaloneRedisClient)).toEqual(-1);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/keys.util.ts",
    "content": "import { get } from 'lodash';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { convertMultilineReplyToObject } from 'src/modules/redis/utils/reply.util';\n\nexport const getTotalKeysFromInfo = async (client: RedisClient) => {\n  try {\n    const currentDbIndex = await client.getCurrentDbIndex();\n    const info = await client.getInfo('keyspace');\n\n    const dbInfo = get(info, 'keyspace', {});\n    if (!dbInfo[`db${currentDbIndex}`]) {\n      return 0;\n    }\n\n    const { keys } = convertMultilineReplyToObject(\n      dbInfo[`db${currentDbIndex}`],\n      ',',\n      '=',\n    );\n    return parseInt(keys, 10);\n  } catch (err) {\n    return -1;\n  }\n};\n\nexport const getTotalKeysFromDBSize = async (client: RedisClient) => {\n  const total = (await client.sendCommand(['dbsize'], {\n    replyEncoding: 'utf8',\n  })) as string;\n  return parseInt(total, 10);\n};\n\nexport const getTotalKeys = async (client: RedisClient): Promise<number> => {\n  try {\n    return await getTotalKeysFromDBSize(client);\n  } catch (err) {\n    return await getTotalKeysFromInfo(client);\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/reply.util.spec.ts",
    "content": "import {\n  mockRedisClusterNodesResponse,\n  mockRedisClusterNodesResponseIPv6,\n  mockRedisServerInfoResponse,\n} from 'src/__mocks__';\nimport { flatMap } from 'lodash';\nimport { IRedisClusterNode, RedisClusterNodeLinkState } from 'src/models';\nimport {\n  convertArrayReplyToObject,\n  convertMultilineReplyToObject,\n  parseNodesFromClusterInfoReply,\n} from './reply.util';\n\nconst mockRedisServerInfo = {\n  redis_version: '6.0.5',\n  redis_mode: 'standalone',\n  os: 'Linux 4.15.0-1087-gcp x86_64',\n  arch_bits: '64',\n  tcp_port: '11113',\n  uptime_in_seconds: '1000',\n};\n\nconst mockRedisClusterNodes: IRedisClusterNode[] = [\n  {\n    id: '07c37dfeb235213a872192d90877d0cd55635b91',\n    host: '127.0.0.1',\n    port: 30004,\n    replicaOf: 'e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca',\n    linkState: RedisClusterNodeLinkState.Connected,\n    slot: undefined,\n  },\n  {\n    id: 'e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca',\n    host: '127.0.0.1',\n    port: 30001,\n    replicaOf: undefined,\n    linkState: RedisClusterNodeLinkState.Connected,\n    slot: '0-16383',\n  },\n];\n\n// IPv6 expected results\nconst mockRedisClusterNodesIPv6: IRedisClusterNode[] = [\n  {\n    id: '07c37dfeb235213a872192d90877d0cd55635b91',\n    host: '2001:db8::1',\n    port: 7001,\n    replicaOf: 'e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca',\n    linkState: RedisClusterNodeLinkState.Connected,\n    slot: undefined,\n  },\n  {\n    id: 'e7d1eecce10fd6bb5eb35b9f99a514335d9ba9ca',\n    host: '2001:db8::2',\n    port: 7002,\n    replicaOf: undefined,\n    linkState: RedisClusterNodeLinkState.Connected,\n    slot: '0-16383',\n  },\n];\n\nconst mockIncorrectString = '$6\\r\\nfoobar\\r\\n';\n\ndescribe('convertArrayReplyToObject', () => {\n  it('should return appropriate value', () => {\n    const input = ['key1', 'value1', 'key2', 'value2'];\n\n    const output = convertArrayReplyToObject(input);\n\n    expect(flatMap(Object.entries(output))).toEqual(input);\n  });\n  it('should return empty object', () => {\n    const output = convertArrayReplyToObject([]);\n\n    expect({}).toEqual(output);\n  });\n});\n\ndescribe('convertMultilineReplyToObject', () => {\n  it('should return object in a defined format', async () => {\n    const result = convertMultilineReplyToObject(mockRedisServerInfoResponse);\n\n    expect(result).toEqual(mockRedisServerInfo);\n  });\n  it('should return empty object in case of incorrect string', async () => {\n    const result = convertMultilineReplyToObject(mockIncorrectString);\n\n    expect(result).toEqual({});\n  });\n  it('should return empty object in case of an error', async () => {\n    const result = convertMultilineReplyToObject({} as string);\n\n    expect(result).toEqual({});\n  });\n});\n\ndescribe('parseNodesFromClusterInfoReply', () => {\n  it('should return array object in a defined format', async () => {\n    const result = parseNodesFromClusterInfoReply(\n      mockRedisClusterNodesResponse,\n    );\n\n    expect(result).toEqual(mockRedisClusterNodes);\n  });\n  it('should return empty array when incorrect string passed', async () => {\n    const result = parseNodesFromClusterInfoReply(mockIncorrectString);\n\n    expect(result).toEqual([]);\n  });\n  it('should parse IPv6 addresses correctly', async () => {\n    const result = parseNodesFromClusterInfoReply(\n      mockRedisClusterNodesResponseIPv6,\n    );\n\n    expect(result).toEqual(mockRedisClusterNodesIPv6);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/reply.util.ts",
    "content": "import { chunk, isArray } from 'lodash';\nimport { IRedisClusterNode } from 'src/models';\n\n/**\n * Converts array of strings to object when each even element is a key and odd is a value\n * @Input\n * ```\n * [\n *   \"name\",\n *   \"sentinel-group\",\n *   \"ip\",\n *   \"172.30.100.1\",\n * ]\n * ```\n * @Output\n * ```\n * {\n *   name: \"sentinel-group\",\n *   ip: \"172.30.100.1\"\n * }\n * ```\n * @param input\n * @param options\n */\nexport const convertArrayReplyToObject = (\n  input: string[],\n  options: { utf?: boolean } = {},\n): { [key: string]: any } =>\n  chunk(input, 2).reduce((prev: any, current: string[]) => {\n    const [key, value] = current;\n    return {\n      ...prev,\n      [key.toString().toLowerCase()]:\n        options.utf && !isArray(value) ? value?.toString() : value,\n    };\n  }, {});\n\n/**\n * Based on separators converts multiline RESP reply to object\n * In case of any error will return empty object\n *\n * @Input\n * ```\n * cluster_slots_assigned:16384\\r\\n\n * cluster_slots_ok:16384\\r\\n\n * cluster_slots_pfail:0\\r\\n\n * ```\n * @Output\n * ```\n * {\n *  cluster_slots_assigned: '16384',\n *  cluster_slots_ok: '0',\n *  cluster_slots_pfail: '0'\n * }\n * ```\n * @param info\n * @param lineSeparator\n * @param valueSeparator\n */\nexport const convertMultilineReplyToObject = (\n  info: string,\n  lineSeparator = '\\r\\n',\n  valueSeparator = ':',\n): Record<string, string> => {\n  try {\n    const lines = info.split(lineSeparator);\n    const obj = {};\n\n    lines.forEach((line: string) => {\n      if (line && line.split) {\n        const keyValuePair = line.split(valueSeparator);\n        if (keyValuePair.length > 1) {\n          const key = keyValuePair.shift();\n          obj[key] = keyValuePair.join(valueSeparator);\n        }\n      }\n    });\n\n    return obj;\n  } catch (e) {\n    return {};\n  }\n};\n\n/**\n * Parse and return all endpoints from the nodes list returned by \"cluster info\" command\n * @Input\n * ```\n * 08418e3514990489e48fa05d642efc33e205f5 172.31.100.211:6379@16379 myself,master - 0 1698694904000 1 connected 0-5460\n * d2dee846c715a917ec9a4963e8885b06130f9f 172.31.100.212:6379@16379 master - 0 1698694905285 2 connected 5461-10922\n * 3e92457ab813ad7a62dacf768ec7309210feaf [2001:db8::1]:7001@17001 master - 0 1698694906000 3 connected 10923-16383\n * ```\n * @Output\n * ```\n * [\n *   {\n *     host: \"172.31.100.211\",\n *     port: 6379\n *   },\n *   {\n *     host: \"172.31.100.212\",\n *     port: 6379\n *   },\n *   {\n *     host: \"2001:db8::1\",\n *     port: 7001\n *   }\n * ]\n * ```\n * @param info\n */\nexport const parseNodesFromClusterInfoReply = (\n  info: string,\n): IRedisClusterNode[] => {\n  try {\n    const lines = info.split('\\n');\n    const nodes = [];\n    lines.forEach((line: string) => {\n      if (line && line.split) {\n        // fields = [id, endpoint, flags, master, pingSent, pongRecv, configEpoch, linkState, slot]\n        const fields = line.split(' ');\n        const [id, endpoint, , master, , , , linkState, slot] = fields;\n\n        const hostAndPort = endpoint.split('@')[0];\n        const lastColonIndex = hostAndPort.lastIndexOf(':');\n\n        const host = hostAndPort.substring(0, lastColonIndex);\n        const port = hostAndPort.substring(lastColonIndex + 1);\n        nodes.push({\n          id,\n          host,\n          port: parseInt(port, 10),\n          replicaOf: master !== '-' ? master : undefined,\n          linkState,\n          slot,\n        });\n      }\n    });\n    return nodes;\n  } catch (e) {\n    return [];\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/sentinel.util.spec.ts",
    "content": "import {\n  mockOtherSentinelEndpoint,\n  mockOtherSentinelsReply,\n  mockRedisNoPermError,\n  mockRedisSentinelMasterResponse,\n  mockSentinelMasterDto,\n  mockSentinelMasterInDownState,\n  mockSentinelMasterInOkState,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { SentinelMasterStatus } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { BadRequestException, ForbiddenException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ReplyError } from 'src/models';\nimport {\n  isSentinel,\n  discoverOtherSentinels,\n  discoverSentinelMasterGroups,\n} from './sentinel.util';\n\ndescribe('Sentinel Util', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('isSentinel', () => {\n    it('sentinel connection ok', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce(\n        mockRedisSentinelMasterResponse,\n      );\n      expect(await isSentinel(mockStandaloneRedisClient)).toEqual(true);\n    });\n    it('sentinel not supported', async () => {\n      mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce({\n        name: 'ReplyError',\n        message: 'Unknown command `sentinel`',\n        command: 'SENTINEL',\n      });\n      expect(await isSentinel(mockStandaloneRedisClient)).toEqual(false);\n    });\n  });\n\n  describe('getMasterEndpoints', () => {\n    it('succeed to get sentinel master endpoints', async () => {\n      const masterName = mockSentinelMasterDto.name;\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce(\n        mockOtherSentinelsReply,\n      );\n\n      const result = await discoverOtherSentinels(\n        mockStandaloneRedisClient,\n        masterName,\n      );\n\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith(\n        ['sentinel', 'sentinels', masterName],\n        { replyEncoding: 'utf8' },\n      );\n\n      expect(result).toEqual([mockOtherSentinelEndpoint]);\n    });\n    it('empty list of sentinel master endpoints', async () => {\n      const masterName = mockSentinelMasterDto.name;\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([]);\n\n      const result = await discoverOtherSentinels(\n        mockStandaloneRedisClient,\n        masterName,\n      );\n\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith(\n        ['sentinel', 'sentinels', masterName],\n        { replyEncoding: 'utf8' },\n      );\n\n      expect(result).toEqual([]);\n    });\n    it('wrong database type', async () => {\n      mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce({\n        message:\n          'ERR unknown command `sentinel`, with args beginning with: `masters`',\n      });\n\n      await expect(\n        discoverOtherSentinels(\n          mockStandaloneRedisClient,\n          mockSentinelMasterDto.name,\n        ),\n      ).rejects.toThrow(BadRequestException);\n    });\n    it(\"user don't have required permissions\", async () => {\n      const error: ReplyError = {\n        ...mockRedisNoPermError,\n        command: 'SENTINEL',\n      };\n      mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce(error);\n\n      await expect(\n        discoverOtherSentinels(\n          mockStandaloneRedisClient,\n          mockSentinelMasterDto.name,\n        ),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n\n  describe('discoverSentinelMasterGroups', () => {\n    it('succeed to get sentinel masters', async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce([\n        mockSentinelMasterInOkState,\n        mockSentinelMasterInDownState,\n      ]);\n      mockStandaloneRedisClient.sendCommand\n        .mockResolvedValueOnce(mockOtherSentinelsReply)\n        .mockResolvedValueOnce(mockOtherSentinelsReply);\n\n      const result = await discoverSentinelMasterGroups(\n        mockStandaloneRedisClient,\n      );\n\n      expect(mockStandaloneRedisClient.sendCommand).toHaveBeenCalledWith(\n        ['sentinel', 'masters'],\n        { replyEncoding: 'utf8' },\n      );\n      expect(result).toEqual([\n        mockSentinelMasterDto,\n        {\n          ...mockSentinelMasterDto,\n          status: SentinelMasterStatus.Down,\n        },\n      ]);\n    });\n    it('wrong database type', async () => {\n      mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce({\n        message:\n          'ERR unknown command `sentinel`, with args beginning with: `masters`',\n      });\n\n      try {\n        await discoverSentinelMasterGroups(mockStandaloneRedisClient);\n        fail('Should throw an error');\n      } catch (err) {\n        expect(err).toBeInstanceOf(BadRequestException);\n        expect(err.message).toEqual(ERROR_MESSAGES.WRONG_DISCOVERY_TOOL());\n      }\n    });\n\n    it(\"user don't have required permissions\", async () => {\n      mockStandaloneRedisClient.sendCommand.mockRejectedValueOnce({\n        ...mockRedisNoPermError,\n        command: 'SENTINEL',\n      });\n\n      await expect(\n        discoverSentinelMasterGroups(mockStandaloneRedisClient),\n      ).rejects.toThrow(ForbiddenException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis/utils/sentinel.util.ts",
    "content": "import { RedisClient } from 'src/modules/redis/client';\nimport {\n  SentinelMaster,\n  SentinelMasterStatus,\n} from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { catchAclError } from 'src/utils';\nimport { BadRequestException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils/reply.util';\nimport { Endpoint } from 'src/common/models';\n\n/**\n * Check weather database is a sentinel\n * Used to automatically determine db type when connected to a database with standalone client\n * In case when \"sentinel masters\" command will be not allowed by ACL or in case of any other error\n * we will handle this database as a non-sentinel since \"sentinel masters\" command is required\n * to work properly in the next steps\n * @param client\n */\nexport const isSentinel = async (client: RedisClient): Promise<boolean> => {\n  try {\n    await client.sendCommand(['sentinel', 'masters']);\n    return true;\n  } catch (e) {\n    return false;\n  }\n};\n\n/**\n * Discover other sentinels endpoints for the current connection\n * @param client\n * @param masterGroup\n */\nexport const discoverOtherSentinels = async (\n  client: RedisClient,\n  masterGroup: string,\n): Promise<Endpoint[]> => {\n  let result: Endpoint[];\n  try {\n    const reply = (await client.sendCommand(\n      ['sentinel', 'sentinels', masterGroup],\n      { replyEncoding: 'utf8' },\n    )) as string[][];\n\n    result = reply.map((item) => {\n      const { ip, port } = convertArrayReplyToObject(item);\n      return { host: ip, port: parseInt(port, 10) };\n    });\n\n    return [...result];\n  } catch (error) {\n    // todo: remove error handling\n    if (error.message.includes('unknown command `sentinel`')) {\n      throw new BadRequestException(ERROR_MESSAGES.WRONG_DATABASE_TYPE);\n    }\n\n    throw catchAclError(error);\n  }\n};\n\n/**\n * Discover master groups for the current connection\n * @param client\n */\nexport const discoverSentinelMasterGroups = async (\n  client: RedisClient,\n): Promise<SentinelMaster[]> => {\n  let result: SentinelMaster[];\n  try {\n    const reply = (await client.sendCommand(['sentinel', 'masters'], {\n      replyEncoding: 'utf8',\n    })) as string[][];\n\n    result = reply.map((item) => {\n      const {\n        ip,\n        port,\n        name,\n        'num-slaves': numberOfSlaves,\n        flags,\n      } = convertArrayReplyToObject(item);\n      return {\n        host: ip,\n        port: parseInt(port, 10),\n        name,\n        status: flags.includes('down')\n          ? SentinelMasterStatus.Down\n          : SentinelMasterStatus.Active,\n        numberOfSlaves: parseInt(numberOfSlaves, 10),\n      };\n    });\n\n    await Promise.all(\n      result.map(async (master: SentinelMaster, index: number) => {\n        const nodes = await discoverOtherSentinels(client, master.name);\n        result[index] = {\n          ...master,\n          nodes,\n        };\n      }),\n    );\n\n    return result;\n  } catch (error) {\n    // todo: remove error handling from here\n    if (error.message.includes('unknown command `sentinel`')) {\n      throw new BadRequestException(ERROR_MESSAGES.WRONG_DISCOVERY_TOOL());\n    }\n\n    throw catchAclError(error);\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/dto/cluster.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n  IsArray,\n  IsDefined,\n  IsInt,\n  IsNotEmpty,\n  IsString,\n} from 'class-validator';\nimport { Exclude, Type } from 'class-transformer';\nimport { RedisEnterpriseDatabaseStatus } from 'src/modules/redis-enterprise/models/redis-enterprise-database';\nimport { NoDuplicatesByKey } from 'src/common/decorators';\nimport { CreateTagDto } from 'src/modules/tag/dto';\n\nexport class ClusterConnectionDetailsDto {\n  @ApiProperty({\n    description: 'The hostname of your Redis Enterprise.',\n    type: String,\n    default: 'localhost',\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  host: string;\n\n  @ApiProperty({\n    description: 'The port your Redis Enterprise cluster is available on.',\n    type: Number,\n    default: 9443,\n  })\n  @IsDefined()\n  @Type(() => Number)\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  port: number;\n\n  @ApiProperty({\n    description: 'The admin e-mail/username',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  username: string;\n\n  @ApiProperty({\n    description: 'The admin password',\n    type: String,\n  })\n  @IsDefined()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  password: string;\n}\n\nexport class RedisEnterpriseDatabase {\n  @ApiProperty({\n    description: 'The unique ID of the database.',\n    type: Number,\n  })\n  uid: number;\n\n  @ApiProperty({\n    description: 'Name of database in cluster.',\n    type: String,\n  })\n  name: string;\n\n  @ApiProperty({\n    description:\n      'DNS name your Redis Enterprise cluster database is available on.',\n    type: String,\n  })\n  dnsName: string;\n\n  @ApiProperty({\n    description:\n      'Address your Redis Enterprise cluster database is available on.',\n    type: String,\n  })\n  address: string;\n\n  @ApiProperty({\n    description:\n      'The port your Redis Enterprise cluster database is available on.',\n    type: Number,\n  })\n  port: number;\n\n  @ApiProperty({\n    description: 'Database status',\n    enum: RedisEnterpriseDatabaseStatus,\n    default: RedisEnterpriseDatabaseStatus.Active,\n  })\n  status: RedisEnterpriseDatabaseStatus;\n\n  @ApiProperty({\n    description: 'Information about the modules loaded to the database',\n    type: String,\n    isArray: true,\n  })\n  modules: string[];\n\n  @ApiProperty({\n    description: 'Is TLS mode enabled?',\n    type: Boolean,\n  })\n  tls: boolean;\n\n  @ApiProperty({\n    description: 'Additional database options',\n    type: Object,\n  })\n  options: any;\n\n  @ApiProperty({\n    description: 'Tags associated with the database.',\n    type: CreateTagDto,\n    isArray: true,\n  })\n  @IsArray()\n  @NoDuplicatesByKey('key', {\n    message: 'Tags must not contain duplicates by key.',\n  })\n  @Type(() => CreateTagDto)\n  tags: CreateTagDto[];\n\n  @Exclude()\n  password: string | null;\n\n  constructor(partial: Partial<RedisEnterpriseDatabase>) {\n    Object.assign(this, partial);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/dto/redis-enterprise-cluster.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { ArrayNotEmpty, IsArray, IsDefined, IsNumber } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport {\n  ClusterConnectionDetailsDto,\n  RedisEnterpriseDatabase,\n} from 'src/modules/redis-enterprise/dto/cluster.dto';\nimport { ActionStatus } from 'src/common/models';\n\nexport class AddRedisEnterpriseDatabasesDto extends ClusterConnectionDetailsDto {\n  @ApiProperty({\n    description: 'The unique IDs of the databases.',\n    type: Number,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @IsNumber({}, { each: true })\n  @ArrayNotEmpty()\n  @Type(() => Number)\n  uids: number[];\n}\n\nexport class AddRedisEnterpriseDatabaseResponse {\n  @ApiProperty({\n    description: 'The unique ID of the database',\n    type: Number,\n  })\n  uid: number;\n\n  @ApiProperty({\n    description: 'Add Redis Enterprise database status',\n    default: ActionStatus.Success,\n    enum: ActionStatus,\n  })\n  status: ActionStatus;\n\n  @ApiProperty({\n    description: 'Message',\n    type: String,\n  })\n  message: string;\n\n  @ApiPropertyOptional({\n    description: 'The database details.',\n    type: RedisEnterpriseDatabase,\n  })\n  databaseDetails?: RedisEnterpriseDatabase;\n\n  @ApiPropertyOptional({\n    description: 'Error',\n  })\n  error?: string | object;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/models/redis-enterprise-database.ts",
    "content": "export enum RedisEnterprisePersistencePolicy {\n  AofEveryOneSecond = 'aof-every-1-second',\n  AofEveryWrite = 'aof-every-write',\n  SnapshotEveryOneHour = 'snapshot-every-1-hour',\n  SnapshotEverySixHours = 'snapshot-every-6-hours',\n  SnapshotEveryTwelveHours = 'snapshot-every-12-hours',\n  None = 'none',\n}\n\ntype IRedisEnterpriseDatabaseTag = {\n  key: string;\n  value: string;\n};\n\nexport interface IRedisEnterpriseDatabase {\n  gradual_src_mode: string;\n  group_uid: number;\n  memory_size: number;\n  last_changed_time: string;\n  created_time: string;\n  skip_import_analyze: string;\n  rack_aware: boolean;\n  shard_key_regex: any[];\n  redis_version: string;\n  oss_sharding: false;\n  shard_list: number[];\n  authentication_ssl_client_certs: any[];\n  backup_progress: any;\n  import_status: string;\n  hash_slots_policy: string;\n  dataset_import_sources: any;\n  roles_permissions: any[];\n  replication: boolean;\n  authentication_admin_pass: string;\n  default_user: boolean;\n  name: string;\n  crdt_causal_consistency: boolean;\n  authentication_sasl_pass: string;\n  import_failure_reason: string;\n  oss_cluster: boolean;\n  sync: string;\n  background_op: any[];\n  authentication_ssl_crdt_certs: any;\n  port: number;\n  crdt_guid: string;\n  version: string;\n  email_alerts: boolean;\n  max_aof_load_time: number;\n  crdt_sources: any[];\n  auto_upgrade: boolean;\n  backup_interval: number;\n  slave_ha_priority: number;\n  shards_placement: string;\n  data_persistence: RedisEnterpriseDatabasePersistence;\n  crdt_sync: string;\n  backup_status: string;\n  crdt: boolean;\n  crdt_replicas: any;\n  snapshot_policy: IRedisEnterpriseSnapshotPolicy[];\n  backup: boolean;\n  gradual_sync_max_shards_per_source: number;\n  backup_interval_offset: number;\n  tls_mode: 'enabled' | 'disabled';\n  replica_sync: 'enabled' | 'disabled';\n  authentication_redis_pass: string;\n  implicit_shard_key: boolean;\n  max_aof_file_size: number;\n  bigstore: boolean;\n  max_connections: number;\n  module_list: IRedisEnterpriseModule[];\n  eviction_policy: string;\n  type: string;\n  backup_history: number;\n  sync_sources: any[];\n  crdt_ghost_replica_ids: string;\n  replica_sources: IRedisEnterpriseReplicaSource[];\n  shard_block_foreign_keys: boolean;\n  enforce_client_authentication: string;\n  crdt_replica_id: number;\n  crdt_config_version: number;\n  proxy_policy: string;\n  aof_policy: RedisEnterpriseDatabaseAofPolicy;\n  endpoints: IRedisEnterpriseEndpoint[];\n  wait_command: boolean;\n  uid: number;\n  authentication_sasl_uname: string;\n  backup_failure_reason: string;\n  bigstore_ram_size: number;\n  shard_block_crossslot_keys: boolean;\n  acl: any[];\n  slave_ha: boolean;\n  internal: boolean;\n  shards_count: number;\n  status: RedisEnterpriseDatabaseStatus;\n  gradual_sync_mode: string;\n  mkms: boolean;\n  gradual_src_max_sources: number;\n  sharding: boolean;\n  oss_cluster_api_preferred_ip_type: string;\n  ssl: boolean;\n  dns_address_master: string;\n  import_progress: any;\n  tags: IRedisEnterpriseDatabaseTag[];\n}\n\nexport interface IRedisEnterpriseModule {\n  module_name: string;\n  module_id: string;\n  semantic_version: string;\n  module_args: string;\n}\n\ninterface IRedisEnterpriseSnapshotPolicy {\n  secs: number;\n  writes: number;\n}\n\nexport interface IRedisEnterpriseReplicaSource {\n  status: string;\n  uid: number;\n  uri: string;\n  server_cert?: string;\n  encryption?: boolean;\n  lag?: number;\n  rdb_transferred?: number;\n  last_update?: string;\n  rdb_size?: number;\n  last_error?: string;\n  client_cert?: string;\n  replication_tls_sni?: string;\n  compression?: number;\n}\n\nexport interface IRedisEnterpriseEndpoint {\n  oss_cluster_api_preferred_ip_type: string;\n  uid: string;\n  dns_name: string;\n  addr_type: string;\n  proxy_policy: string;\n  port: number;\n  addr: string[];\n}\nexport enum RedisEnterpriseDatabasePersistence {\n  Disabled = 'disabled',\n  Aof = 'aof',\n  Snapshot = 'snapshot',\n}\n\nexport enum RedisEnterpriseDatabaseAofPolicy {\n  AofEveryOneSecond = 'appendfsync-every-sec',\n  AofEveryWrite = 'appendfsync-always',\n}\n\nexport enum RedisEnterpriseDatabaseStatus {\n  Pending = 'pending',\n  CreationFailed = 'creation-failed',\n  Active = 'active',\n  ActiveChangePending = 'active-change-pending',\n  ImportPending = 'import-pending',\n  DeletePending = 'delete-pending',\n  Recovery = 'recovery',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport {\n  mockRedisEnterpriseDatabaseDto,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { RedisEnterpriseDatabaseStatus } from 'src/modules/redis-enterprise/models/redis-enterprise-database';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { RedisEnterpriseAnalytics } from 'src/modules/redis-enterprise/redis-enterprise.analytics';\n\ndescribe('RedisEnterpriseAnalytics', () => {\n  let service: RedisEnterpriseAnalytics;\n  let sendEventMethod;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, RedisEnterpriseAnalytics],\n    }).compile();\n\n    service = module.get<RedisEnterpriseAnalytics>(RedisEnterpriseAnalytics);\n    sendEventMethod = jest.spyOn<RedisEnterpriseAnalytics, any>(\n      service,\n      'sendEvent',\n    );\n    sendFailedEventMethod = jest.spyOn<RedisEnterpriseAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendGetRedisSoftwareDbsSucceedEvent', () => {\n    it('should emit event with active databases', () => {\n      service.sendGetRedisSoftwareDbsSucceedEvent(mockSessionMetadata, [\n        mockRedisEnterpriseDatabaseDto,\n        mockRedisEnterpriseDatabaseDto,\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 2,\n          totalNumberOfDatabases: 2,\n        },\n      );\n    });\n    it('should emit event with active and not active database', () => {\n      service.sendGetRedisSoftwareDbsSucceedEvent(mockSessionMetadata, [\n        {\n          ...mockRedisEnterpriseDatabaseDto,\n          status: RedisEnterpriseDatabaseStatus.Pending,\n        },\n        mockRedisEnterpriseDatabaseDto,\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 1,\n          totalNumberOfDatabases: 2,\n        },\n      );\n    });\n    it('should emit event without active databases', () => {\n      service.sendGetRedisSoftwareDbsSucceedEvent(mockSessionMetadata, [\n        {\n          ...mockRedisEnterpriseDatabaseDto,\n          status: RedisEnterpriseDatabaseStatus.Pending,\n        },\n        {\n          ...mockRedisEnterpriseDatabaseDto,\n          status: RedisEnterpriseDatabaseStatus.Pending,\n        },\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 0,\n          totalNumberOfDatabases: 2,\n        },\n      );\n    });\n    it('should emit GetRedisSoftwareDbsSucceed event for empty list', () => {\n      service.sendGetRedisSoftwareDbsSucceedEvent(mockSessionMetadata, []);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 0,\n          totalNumberOfDatabases: 0,\n        },\n      );\n    });\n    it('should emit GetRedisSoftwareDbsSucceed event for undefined input value', () => {\n      service.sendGetRedisSoftwareDbsSucceedEvent(\n        mockSessionMetadata,\n        undefined,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoverySucceed,\n        {\n          numberOfActiveDatabases: 0,\n          totalNumberOfDatabases: 0,\n        },\n      );\n    });\n    it('should not throw on error when sending GetRedisSoftwareDbsSucceed event', () => {\n      const input: any = {};\n\n      expect(() =>\n        service.sendGetRedisSoftwareDbsSucceedEvent(mockSessionMetadata, input),\n      ).not.toThrow();\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendGetRedisSoftwareDbsFailedEvent', () => {\n    it('should emit GetRedisSoftwareDbsFailed event', () => {\n      service.sendGetRedisSoftwareDbsFailedEvent(\n        mockSessionMetadata,\n        httpException,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoveryFailed,\n        httpException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { RedisEnterpriseDatabase } from 'src/modules/redis-enterprise/dto/cluster.dto';\nimport { RedisEnterpriseDatabaseStatus } from 'src/modules/redis-enterprise/models/redis-enterprise-database';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class RedisEnterpriseAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendGetRedisSoftwareDbsSucceedEvent(\n    sessionMetadata: SessionMetadata,\n    databases: RedisEnterpriseDatabase[] = [],\n  ): void {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.RedisSoftwareDiscoverySucceed,\n        {\n          numberOfActiveDatabases: databases.filter(\n            (db) => db.status === RedisEnterpriseDatabaseStatus.Active,\n          ).length,\n          totalNumberOfDatabases: databases.length,\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendGetRedisSoftwareDbsFailedEvent(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.RedisSoftwareDiscoveryFailed,\n      exception,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.controller.spec.ts",
    "content": "import { when } from 'jest-when';\nimport * as request from 'supertest';\nimport { Test } from '@nestjs/testing';\nimport {\n  ForbiddenException,\n  INestApplication,\n  MiddlewareConsumer,\n  Module,\n  NestModule,\n} from '@nestjs/common';\nimport {\n  mockAddRedisEnterpriseDatabasesDto,\n  mockRedisEnterpriseService,\n  mockSessionService,\n} from 'src/__mocks__';\nimport { SingleUserAuthMiddleware } from 'src/common/middlewares/single-user-auth.middleware';\nimport { SessionService } from 'src/modules/session/session.service';\nimport config, { Config } from 'src/utils/config';\nimport { RedisEnterpriseController } from 'src/modules/redis-enterprise/redis-enterprise.controller';\nimport { RedisEnterpriseService } from 'src/modules/redis-enterprise/redis-enterprise.service';\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\n@Module({\n  controllers: [RedisEnterpriseController],\n  providers: [\n    {\n      provide: RedisEnterpriseService,\n      useFactory: mockRedisEnterpriseService,\n    },\n    {\n      provide: SessionService,\n      useFactory: mockSessionService,\n    },\n  ],\n})\nclass TestModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(SingleUserAuthMiddleware).forRoutes('*');\n  }\n}\n\ndescribe('RedisEnterpriseController', () => {\n  let app: INestApplication;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const moduleRef = await Test.createTestingModule({\n      imports: [TestModule],\n    }).compile();\n\n    app = moduleRef.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  describe('POST /redis-enterprise/cluster/databases', () => {\n    it('should succeed when database management enabled', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .post('/redis-enterprise/cluster/databases')\n        .send(mockAddRedisEnterpriseDatabasesDto)\n        .expect(200);\n    });\n\n    it('should fail when database management disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .post('/redis-enterprise/cluster/databases')\n        .send(mockAddRedisEnterpriseDatabasesDto)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Post,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor';\nimport { RedisEnterpriseService } from 'src/modules/redis-enterprise/redis-enterprise.service';\nimport {\n  AddRedisEnterpriseDatabaseResponse,\n  AddRedisEnterpriseDatabasesDto,\n} from 'src/modules/redis-enterprise/dto/redis-enterprise-cluster.dto';\nimport { Response } from 'express';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { BuildType } from 'src/modules/server/models/server';\nimport {\n  DatabaseManagement,\n  RequestSessionMetadata,\n} from 'src/common/decorators';\nimport {\n  ClusterConnectionDetailsDto,\n  RedisEnterpriseDatabase,\n} from 'src/modules/redis-enterprise/dto/cluster.dto';\n\n@ApiTags('Redis Enterprise Cluster')\n@UsePipes(new ValidationPipe({ transform: true }))\n@Controller('redis-enterprise/cluster')\nexport class RedisEnterpriseController {\n  constructor(private redisEnterpriseService: RedisEnterpriseService) {}\n\n  @UseInterceptors(ClassSerializerInterceptor)\n  @Post('get-databases')\n  @UseInterceptors(new TimeoutInterceptor())\n  @ApiEndpoint({\n    description: 'Get all databases in the cluster.',\n    statusCode: 200,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 200,\n        description: 'All databases in the cluster.',\n        isArray: true,\n        type: RedisEnterpriseDatabase,\n      },\n    ],\n  })\n  async getDatabases(\n    @Body() dto: ClusterConnectionDetailsDto,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<RedisEnterpriseDatabase[]> {\n    return await this.redisEnterpriseService.getDatabases(sessionMetadata, dto);\n  }\n\n  @Post('databases')\n  @ApiEndpoint({\n    description: 'Add databases from Redis Enterprise cluster',\n    statusCode: 201,\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 201,\n        description: 'Added databases list.',\n        type: AddRedisEnterpriseDatabaseResponse,\n        isArray: true,\n      },\n    ],\n  })\n  @UsePipes(new ValidationPipe({ transform: true }))\n  @DatabaseManagement()\n  async addRedisEnterpriseDatabases(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: AddRedisEnterpriseDatabasesDto,\n    @Res() res: Response,\n  ): Promise<Response> {\n    const { uids, ...connectionDetails } = dto;\n    const result =\n      await this.redisEnterpriseService.addRedisEnterpriseDatabases(\n        sessionMetadata,\n        connectionDetails,\n        uids,\n      );\n    const hasSuccessResult = result.some(\n      (addResponse: AddRedisEnterpriseDatabaseResponse) =>\n        addResponse.status === ActionStatus.Success,\n    );\n    if (!hasSuccessResult) {\n      return res.status(200).json(result);\n    }\n    return res.json(result);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { RedisEnterpriseService } from 'src/modules/redis-enterprise/redis-enterprise.service';\nimport { RedisEnterpriseAnalytics } from 'src/modules/redis-enterprise/redis-enterprise.analytics';\nimport { RedisEnterpriseController } from 'src/modules/redis-enterprise/redis-enterprise.controller';\n\n@Module({\n  controllers: [RedisEnterpriseController],\n  providers: [RedisEnterpriseService, RedisEnterpriseAnalytics],\n})\nexport class RedisEnterpriseModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { BadRequestException, ForbiddenException } from '@nestjs/common';\nimport axios from 'axios';\nimport { RedisErrorCodes } from 'src/constants';\nimport {\n  mockDatabaseService,\n  mockRedisEnterpriseAnalytics,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport {\n  IRedisEnterpriseDatabase,\n  IRedisEnterpriseEndpoint,\n  RedisEnterpriseDatabaseAofPolicy,\n  RedisEnterpriseDatabasePersistence,\n  RedisEnterpriseDatabaseStatus,\n  RedisEnterprisePersistencePolicy,\n} from 'src/modules/redis-enterprise/models/redis-enterprise-database';\nimport { RedisEnterpriseService } from 'src/modules/redis-enterprise/redis-enterprise.service';\nimport { ClusterConnectionDetailsDto } from 'src/modules/redis-enterprise/dto/cluster.dto';\nimport { RedisEnterpriseAnalytics } from 'src/modules/redis-enterprise/redis-enterprise.analytics';\nimport { DatabaseService } from 'src/modules/database/database.service';\n\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\njest.mock('axios');\nmockedAxios.create = jest.fn(() => mockedAxios);\nconst mockGetDatabasesDto: ClusterConnectionDetailsDto = {\n  host: 'localhost',\n  port: 9443,\n  username: 'admin@gmail.com',\n  password: 'adminpassword',\n};\n\nconst mockRedisSoftwareDatabaseEndpoint: IRedisEnterpriseEndpoint = {\n  oss_cluster_api_preferred_ip_type: 'internal',\n  uid: '2:1',\n  addr_type: 'external',\n  dns_name: 'redis-11305.testcluster.local',\n  proxy_policy: 'single',\n  port: 11305,\n  addr: ['172.17.0.2'],\n};\nconst mockRedisSoftwareDatabase: IRedisEnterpriseDatabase = {\n  gradual_src_mode: 'disabled',\n  group_uid: 0,\n  memory_size: 107374182,\n  last_changed_time: '2021-02-15T11:56:40Z',\n  created_time: '2021-02-15T11:56:40Z',\n  skip_import_analyze: 'disabled',\n  rack_aware: false,\n  redis_version: '6.0',\n  oss_sharding: false,\n  shard_list: [2],\n  authentication_ssl_client_certs: [],\n  backup_progress: 0.0,\n  import_status: '',\n  hash_slots_policy: '16k',\n  dataset_import_sources: [],\n  roles_permissions: [],\n  replication: false,\n  authentication_admin_pass: '',\n  default_user: true,\n  name: 'basic',\n  crdt_causal_consistency: false,\n  authentication_sasl_pass: '',\n  import_failure_reason: '',\n  oss_cluster: false,\n  sync: 'disabled',\n  background_op: [{ status: 'idle' }],\n  authentication_ssl_crdt_certs: [],\n  port: 0,\n  crdt_guid: '',\n  version: '6.0.4',\n  email_alerts: false,\n  max_aof_load_time: 3600,\n  crdt_sources: [],\n  auto_upgrade: false,\n  backup_interval: 0,\n  slave_ha_priority: 0,\n  shards_placement: 'dense',\n  data_persistence: RedisEnterpriseDatabasePersistence.Disabled,\n  crdt_sync: 'disabled',\n  backup_status: '',\n  crdt: false,\n  crdt_replicas: '',\n  snapshot_policy: [],\n  backup: false,\n  gradual_sync_max_shards_per_source: 1,\n  backup_interval_offset: 0,\n  tls_mode: 'disabled',\n  replica_sync: 'disabled',\n  authentication_redis_pass: '',\n  implicit_shard_key: false,\n  max_aof_file_size: 322122547200,\n  bigstore: false,\n  max_connections: 0,\n  module_list: [],\n  eviction_policy: 'volatile-lru',\n  type: 'redis',\n  backup_history: 0,\n  sync_sources: [],\n  crdt_ghost_replica_ids: '',\n  replica_sources: [],\n  shard_block_foreign_keys: true,\n  enforce_client_authentication: 'enabled',\n  crdt_replica_id: 0,\n  crdt_config_version: 0,\n  proxy_policy: 'single',\n  aof_policy: RedisEnterpriseDatabaseAofPolicy.AofEveryOneSecond,\n  wait_command: true,\n  uid: 2,\n  authentication_sasl_uname: '',\n  backup_failure_reason: '',\n  bigstore_ram_size: 0,\n  shard_block_crossslot_keys: false,\n  acl: [],\n  slave_ha: false,\n  internal: false,\n  shards_count: 1,\n  shard_key_regex: [],\n  status: RedisEnterpriseDatabaseStatus.Active,\n  gradual_sync_mode: 'auto',\n  mkms: true,\n  gradual_src_max_sources: 1,\n  sharding: false,\n  oss_cluster_api_preferred_ip_type: 'internal',\n  ssl: false,\n  dns_address_master: '',\n  import_progress: 0.0,\n  endpoints: [mockRedisSoftwareDatabaseEndpoint],\n  tags: [],\n};\nconst mockRedisSoftwareDbsResponse: IRedisEnterpriseDatabase[] = [\n  mockRedisSoftwareDatabase,\n];\n\ndescribe('RedisEnterpriseService', () => {\n  let service;\n  let parseClusterDbsResponse;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: RedisEnterpriseAnalytics,\n          useFactory: mockRedisEnterpriseAnalytics,\n        },\n        RedisEnterpriseService,\n      ],\n    }).compile();\n\n    service = await module.get<RedisEnterpriseService>(RedisEnterpriseService);\n    parseClusterDbsResponse = jest.spyOn(service, 'parseClusterDbsResponse');\n  });\n\n  describe('getDatabases', () => {\n    it('successfully get databases from RE cluster', async () => {\n      const response = { status: 200, data: mockRedisSoftwareDbsResponse };\n      mockedAxios.get.mockResolvedValue(response);\n\n      await expect(\n        service.getDatabases(mockSessionMetadata, mockGetDatabasesDto),\n      ).resolves.not.toThrow();\n      expect(mockedAxios.get).toHaveBeenCalled();\n      expect(parseClusterDbsResponse).toHaveBeenCalledWith(\n        mockRedisSoftwareDbsResponse,\n      );\n    });\n    it('the user could not be authenticated', async () => {\n      const apiResponse = {\n        message: 'Request failed with status code 401',\n        response: {\n          status: 401,\n        },\n      };\n      mockedAxios.get.mockRejectedValue(apiResponse);\n\n      await expect(\n        service.getDatabases(mockSessionMetadata, mockGetDatabasesDto),\n      ).rejects.toThrow(ForbiddenException);\n    });\n    it('connection refused', async () => {\n      const apiResponse = {\n        code: RedisErrorCodes.ConnectionRefused,\n        message: 'connect ECONNREFUSED',\n      };\n      mockedAxios.get.mockRejectedValue(apiResponse);\n\n      await expect(\n        service.getDatabases(mockSessionMetadata, mockGetDatabasesDto),\n      ).rejects.toThrow(BadRequestException);\n    });\n  });\n\n  describe('getDatabaseExternalEndpoint', () => {\n    const externalEndpoint: IRedisEnterpriseEndpoint =\n      mockRedisSoftwareDatabaseEndpoint;\n    const internalEndpoint: IRedisEnterpriseEndpoint = {\n      ...mockRedisSoftwareDatabaseEndpoint,\n      addr_type: 'internal',\n    };\n    it('should return only one external endpoints', async () => {\n      const result = service.getDatabaseExternalEndpoint({\n        ...mockRedisSoftwareDatabase,\n        endpoints: [externalEndpoint, internalEndpoint],\n      });\n      expect(result).toEqual(externalEndpoint);\n    });\n    it('should return undefined', async () => {\n      const result = service.getDatabaseExternalEndpoint({\n        ...mockRedisSoftwareDatabase,\n        endpoints: [internalEndpoint],\n      });\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe('getDatabasePersistencePolicy', () => {\n    it('should return AofEveryOneSecond', async () => {\n      const result = service.getDatabasePersistencePolicy({\n        ...mockRedisSoftwareDatabase,\n        data_persistence: RedisEnterpriseDatabasePersistence.Aof,\n        aof_policy: RedisEnterpriseDatabaseAofPolicy.AofEveryOneSecond,\n      });\n      expect(result).toEqual(\n        RedisEnterprisePersistencePolicy.AofEveryOneSecond,\n      );\n    });\n    it('should return AofEveryWrite', async () => {\n      const result = service.getDatabasePersistencePolicy({\n        ...mockRedisSoftwareDatabase,\n        data_persistence: RedisEnterpriseDatabasePersistence.Aof,\n        aof_policy: RedisEnterpriseDatabaseAofPolicy.AofEveryWrite,\n      });\n      expect(result).toEqual(RedisEnterprisePersistencePolicy.AofEveryWrite);\n    });\n    it('should return SnapshotEveryOneHour', async () => {\n      const result = service.getDatabasePersistencePolicy({\n        ...mockRedisSoftwareDatabase,\n        data_persistence: RedisEnterpriseDatabasePersistence.Snapshot,\n        snapshot_policy: [{ secs: 3600 }],\n      });\n      expect(result).toEqual(\n        RedisEnterprisePersistencePolicy.SnapshotEveryOneHour,\n      );\n    });\n    it('should return SnapshotEverySixHours', async () => {\n      const result = service.getDatabasePersistencePolicy({\n        ...mockRedisSoftwareDatabase,\n        data_persistence: RedisEnterpriseDatabasePersistence.Snapshot,\n        snapshot_policy: [{ secs: 21600 }],\n      });\n      expect(result).toEqual(\n        RedisEnterprisePersistencePolicy.SnapshotEverySixHours,\n      );\n    });\n    it('should return SnapshotEveryTwelveHours', async () => {\n      const result = service.getDatabasePersistencePolicy({\n        ...mockRedisSoftwareDatabase,\n        data_persistence: RedisEnterpriseDatabasePersistence.Snapshot,\n        snapshot_policy: [{ secs: 43200 }],\n      });\n      expect(result).toEqual(\n        RedisEnterprisePersistencePolicy.SnapshotEveryTwelveHours,\n      );\n    });\n    it('should return None', async () => {\n      const result = service.getDatabasePersistencePolicy({\n        ...mockRedisSoftwareDatabase,\n        data_persistence: null,\n      });\n      expect(result).toEqual(RedisEnterprisePersistencePolicy.None);\n    });\n  });\n\n  describe('findReplicasForDatabase', () => {\n    it('successfully return replicas', async () => {\n      const soursDatabase = mockRedisSoftwareDatabase;\n      const sourceEndpoint = mockRedisSoftwareDatabase.endpoints[0];\n      const replicaDatabase: IRedisEnterpriseDatabase = {\n        ...mockRedisSoftwareDatabase,\n        uid: 1,\n        replica_sources: [\n          {\n            uid: 2,\n            status: RedisEnterpriseDatabaseStatus.Active,\n            uri: `${sourceEndpoint.dns_name}:${sourceEndpoint.port}`,\n          },\n        ],\n      };\n      const result = service.findReplicasForDatabase(\n        [soursDatabase, replicaDatabase],\n        soursDatabase,\n      );\n\n      expect(result).toEqual([replicaDatabase]);\n    });\n    it('source dont have replicas', async () => {\n      const databases = [\n        mockRedisSoftwareDatabase,\n        {\n          ...mockRedisSoftwareDatabase,\n          uid: 3,\n        },\n        {\n          ...mockRedisSoftwareDatabase,\n          uid: 4,\n          replica_sources: [\n            {\n              uid: 3,\n              status: RedisEnterpriseDatabaseStatus.Active,\n              uri: 'redis-11400.testcluster.local:11400',\n            },\n          ],\n        },\n      ];\n      const result = service.findReplicasForDatabase(\n        databases,\n        mockRedisSoftwareDatabase,\n      );\n\n      expect(result).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/redis-enterprise.service.ts",
    "content": "import {\n  BadRequestException,\n  ForbiddenException,\n  Injectable,\n  Logger,\n  NotFoundException,\n} from '@nestjs/common';\nimport axios from 'axios';\nimport * as https from 'https';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  IRedisEnterpriseDatabase,\n  IRedisEnterpriseEndpoint,\n  IRedisEnterpriseModule,\n  IRedisEnterpriseReplicaSource,\n  RedisEnterpriseDatabaseAofPolicy,\n  RedisEnterpriseDatabasePersistence,\n  RedisEnterprisePersistencePolicy,\n} from 'src/modules/redis-enterprise/models/redis-enterprise-database';\nimport {\n  ClusterConnectionDetailsDto,\n  RedisEnterpriseDatabase,\n} from 'src/modules/redis-enterprise/dto/cluster.dto';\nimport { convertRedisSoftwareModuleName } from 'src/modules/redis-enterprise/utils/redis-enterprise-converter';\nimport { RedisEnterpriseAnalytics } from 'src/modules/redis-enterprise/redis-enterprise.analytics';\nimport { AddRedisEnterpriseDatabaseResponse } from 'src/modules/redis-enterprise/dto/redis-enterprise-cluster.dto';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class RedisEnterpriseService {\n  private logger = new Logger('RedisEnterpriseBusinessService');\n\n  constructor(\n    private readonly databaseService: DatabaseService,\n    private readonly analytics: RedisEnterpriseAnalytics,\n  ) {}\n\n  // TODO: maybe find a workaround without Disabling certificate validation.\n  private api = axios.create({\n    httpsAgent: new https.Agent({\n      // we might work with self-signed certificates for local builds\n      rejectUnauthorized: false, // lgtm[js/disabling-certificate-validation]\n    }),\n  });\n\n  async getDatabases(\n    sessionMetadata: SessionMetadata,\n    dto: ClusterConnectionDetailsDto,\n  ): Promise<RedisEnterpriseDatabase[]> {\n    this.logger.debug('Getting RE cluster databases.', sessionMetadata);\n    const { host, port, username, password } = dto;\n    const auth = { username, password };\n    try {\n      const { data } = await this.api.get(`https://${host}:${port}/v1/bdbs`, {\n        auth,\n      });\n      this.logger.debug(\n        'Succeed to get RE cluster databases.',\n        sessionMetadata,\n      );\n      const result = this.parseClusterDbsResponse(data);\n      this.analytics.sendGetRedisSoftwareDbsSucceedEvent(\n        sessionMetadata,\n        result,\n      );\n      return result;\n    } catch (error) {\n      const { response } = error;\n      let exception;\n      this.logger.error(\n        `Failed to get RE cluster databases. ${error.message}`,\n        error,\n        sessionMetadata,\n      );\n      if (response?.status === 401 || response?.status === 403) {\n        exception = new ForbiddenException(\n          ERROR_MESSAGES.INCORRECT_CREDENTIALS(`${host}:${port}`),\n        );\n      } else {\n        exception = new BadRequestException(\n          ERROR_MESSAGES.INCORRECT_DATABASE_URL(`${host}:${port}`),\n        );\n      }\n      this.analytics.sendGetRedisSoftwareDbsFailedEvent(\n        sessionMetadata,\n        exception,\n      );\n      throw exception;\n    }\n  }\n\n  private parseClusterDbsResponse(\n    databases: IRedisEnterpriseDatabase[],\n  ): RedisEnterpriseDatabase[] {\n    const result: RedisEnterpriseDatabase[] = [];\n    databases.forEach((database) => {\n      const {\n        uid,\n        name,\n        crdt,\n        // eslint-disable-next-line @typescript-eslint/naming-convention\n        tls_mode,\n        // eslint-disable-next-line @typescript-eslint/naming-convention\n        crdt_replica_id,\n        tags,\n      } = database;\n      // Get all external endpoint, ignore others\n      const externalEndpoint = this.getDatabaseExternalEndpoint(database);\n      // Skip this database is there are no external endpoints\n      if (!externalEndpoint) {\n        return;\n      }\n      // For Active-Active (CRDT) databases, append the replica ID to the name\n      // so the name doesn't clash when the other replicas are added.\n      const dbName = crdt ? `${name}-${crdt_replica_id}` : name;\n      const dnsName = externalEndpoint.dns_name;\n      const address = externalEndpoint.addr[0];\n      result.push(\n        new RedisEnterpriseDatabase({\n          uid,\n          name: dbName,\n          dnsName,\n          address,\n          port: externalEndpoint.port,\n          password: database.authentication_redis_pass,\n          status: database.status,\n          tls: tls_mode === 'enabled',\n          modules: database.module_list.map((module: IRedisEnterpriseModule) =>\n            convertRedisSoftwareModuleName(module.module_name),\n          ),\n          options: {\n            enabledDataPersistence:\n              database.data_persistence !==\n              RedisEnterpriseDatabasePersistence.Disabled,\n            persistencePolicy: this.getDatabasePersistencePolicy(database),\n            enabledRedisFlash: database.bigstore,\n            enabledReplication: database.replication,\n            enabledBackup: database.backup,\n            enabledActiveActive: database.crdt,\n            enabledClustering: database.shards_count > 1,\n            isReplicaDestination: !!database?.replica_sources?.length,\n            isReplicaSource: !!this.findReplicasForDatabase(databases, database)\n              .length,\n          },\n          tags,\n        }),\n      );\n    });\n    return result;\n  }\n\n  public getDatabaseExternalEndpoint(\n    database: IRedisEnterpriseDatabase,\n  ): IRedisEnterpriseEndpoint {\n    return database.endpoints?.filter(\n      (endpoint: { addr_type: string }) => endpoint.addr_type === 'external',\n    )[0];\n  }\n\n  private getDatabasePersistencePolicy(\n    database: IRedisEnterpriseDatabase,\n  ): RedisEnterprisePersistencePolicy {\n    // eslint-disable-next-line @typescript-eslint/naming-convention\n    const { data_persistence, aof_policy, snapshot_policy } = database;\n    if (data_persistence === RedisEnterpriseDatabasePersistence.Aof) {\n      return aof_policy === RedisEnterpriseDatabaseAofPolicy.AofEveryOneSecond\n        ? RedisEnterprisePersistencePolicy.AofEveryOneSecond\n        : RedisEnterprisePersistencePolicy.AofEveryWrite;\n    }\n    if (data_persistence === RedisEnterpriseDatabasePersistence.Snapshot) {\n      const { secs } = snapshot_policy.pop();\n      if (secs === 3600) {\n        return RedisEnterprisePersistencePolicy.SnapshotEveryOneHour;\n      }\n      if (secs === 21600) {\n        return RedisEnterprisePersistencePolicy.SnapshotEverySixHours;\n      }\n      if (secs === 43200) {\n        return RedisEnterprisePersistencePolicy.SnapshotEveryTwelveHours;\n      }\n    }\n    return RedisEnterprisePersistencePolicy.None;\n  }\n\n  private findReplicasForDatabase(\n    databases: IRedisEnterpriseDatabase[],\n    sourceDatabase: IRedisEnterpriseDatabase,\n  ): IRedisEnterpriseDatabase[] {\n    const sourceEndpoint = this.getDatabaseExternalEndpoint(sourceDatabase);\n    if (!sourceEndpoint) {\n      return [];\n    }\n    return databases.filter((replica: IRedisEnterpriseDatabase): boolean => {\n      const replicaSources = replica.replica_sources;\n      if (replica.uid === sourceDatabase.uid || !replicaSources?.length) {\n        return false;\n      }\n      return replicaSources.some(\n        (source: IRedisEnterpriseReplicaSource): boolean =>\n          source.uri.includes(\n            `${sourceEndpoint.dns_name}:${sourceEndpoint.port}`,\n          ),\n      );\n    });\n  }\n\n  public async addRedisEnterpriseDatabases(\n    sessionMetadata: SessionMetadata,\n    connectionDetails: ClusterConnectionDetailsDto,\n    uids: number[],\n  ): Promise<AddRedisEnterpriseDatabaseResponse[]> {\n    this.logger.debug('Adding Redis Enterprise databases.', sessionMetadata);\n    let result: AddRedisEnterpriseDatabaseResponse[];\n    try {\n      const databases: RedisEnterpriseDatabase[] = await this.getDatabases(\n        sessionMetadata,\n        connectionDetails,\n      );\n      result = await Promise.all(\n        uids.map(async (uid): Promise<AddRedisEnterpriseDatabaseResponse> => {\n          const database = databases.find(\n            (db: RedisEnterpriseDatabase) => db.uid === uid,\n          );\n          if (!database) {\n            const exception = new NotFoundException();\n            return {\n              uid,\n              status: ActionStatus.Fail,\n              message: exception.message,\n              error: exception?.getResponse(),\n            };\n          }\n          try {\n            const { port, name, dnsName, password, tags } = database;\n            const host =\n              connectionDetails.host === 'localhost' ? 'localhost' : dnsName;\n            delete database.password;\n            await this.databaseService.create(sessionMetadata, {\n              host,\n              port,\n              name,\n              nameFromProvider: name,\n              password,\n              provider: HostingProvider.REDIS_SOFTWARE,\n              tags,\n            });\n            return {\n              uid,\n              status: ActionStatus.Success,\n              message: 'Added',\n              databaseDetails: database,\n            };\n          } catch (error) {\n            return {\n              uid,\n              status: ActionStatus.Fail,\n              message: error.message,\n              databaseDetails: database,\n              error: error?.response,\n            };\n          }\n        }),\n      );\n    } catch (error) {\n      this.logger.error(\n        'Failed to add Redis Enterprise databases',\n        error,\n        sessionMetadata,\n      );\n      throw error;\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/utils/redis-enterprise-converter.spec.ts",
    "content": "import { AdditionalRedisModuleName } from 'src/constants';\nimport { convertRedisSoftwareModuleName } from 'src/modules/redis-enterprise/utils/redis-enterprise-converter';\n\ndescribe('convertRedisCloudModuleName', () => {\n  it('should return exist module name', () => {\n    const input = 'ReJSON';\n\n    const output = convertRedisSoftwareModuleName(input);\n\n    expect(output).toEqual(AdditionalRedisModuleName.RedisJSON);\n  });\n  it('should return non-exist module name', () => {\n    const input = 'RedisNewModule';\n\n    const output = convertRedisSoftwareModuleName(input);\n\n    expect(output).toEqual(input);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-enterprise/utils/redis-enterprise-converter.ts",
    "content": "import {\n  AdditionalRedisModuleName,\n  REDIS_SOFTWARE_MODULES_NAMES,\n} from 'src/constants';\n\nexport function convertRedisSoftwareModuleName(\n  name: string,\n): AdditionalRedisModuleName {\n  return REDIS_SOFTWARE_MODULES_NAMES[name] ?? name;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/dto/create.sentinel.database.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsDefined,\n  IsInt,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  MaxLength,\n  Min,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class CreateSentinelDatabaseDto {\n  @ApiProperty({\n    description:\n      'The name under which the base will be saved in the application.',\n    type: String,\n  })\n  @IsDefined()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @MaxLength(500)\n  alias: string;\n\n  @ApiProperty({\n    description: 'Sentinel master group name.',\n    type: String,\n  })\n  @IsDefined()\n  @IsString({ always: true })\n  name: string;\n\n  @ApiPropertyOptional({\n    description:\n      'The username, if your database is ACL enabled, otherwise leave this field empty.',\n    type: String,\n  })\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  username?: string;\n\n  @ApiPropertyOptional({\n    description:\n      'The password, if any, for your Redis database. ' +\n      'If your database doesn’t require a password, leave this field empty.',\n    type: String,\n  })\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  password?: string;\n\n  @ApiPropertyOptional({\n    description: 'Logical database number.',\n    type: Number,\n    example: 0,\n  })\n  @IsInt()\n  @Min(0)\n  @Type(() => Number)\n  @IsOptional()\n  db?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/dto/create.sentinel.database.response.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { ActionStatus } from 'src/common/models';\nimport { Expose } from 'class-transformer';\n\nexport class CreateSentinelDatabaseResponse {\n  @ApiPropertyOptional({\n    description: 'Database instance id.',\n    type: String,\n  })\n  @Expose()\n  id?: string;\n\n  @ApiProperty({\n    description: 'Sentinel master group name.',\n    type: String,\n  })\n  @Expose()\n  name: string;\n\n  @ApiProperty({\n    description: 'Add Sentinel Master status',\n    default: ActionStatus.Success,\n    enum: ActionStatus,\n  })\n  @Expose()\n  status: ActionStatus;\n\n  @ApiProperty({\n    description: 'Message',\n    type: String,\n  })\n  @Expose()\n  message: string;\n\n  @ApiPropertyOptional({\n    description: 'Error',\n  })\n  @Expose()\n  error?: string | object;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/dto/create.sentinel.databases.dto.ts",
    "content": "import { ApiProperty, OmitType } from '@nestjs/swagger';\nimport {\n  ArrayNotEmpty,\n  IsArray,\n  IsDefined,\n  ValidateNested,\n} from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';\nimport { CreateSentinelDatabaseDto } from 'src/modules/redis-sentinel/dto/create.sentinel.database.dto';\n\nexport class CreateSentinelDatabasesDto extends OmitType(CreateDatabaseDto, [\n  'name',\n] as const) {\n  @ApiProperty({\n    description: 'The Sentinel master group list.',\n    type: CreateSentinelDatabaseDto,\n    isArray: true,\n  })\n  @IsDefined()\n  @IsArray()\n  @ArrayNotEmpty()\n  @ValidateNested()\n  @Type(() => CreateSentinelDatabaseDto)\n  masters: CreateSentinelDatabaseDto[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/dto/discover.sentinel-masters.dto.ts",
    "content": "import { OmitType } from '@nestjs/swagger';\nimport { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto';\n\nexport class DiscoverSentinelMastersDto extends OmitType(CreateDatabaseDto, [\n  'name',\n  'db',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/dto/sentinel.master.response.dto.ts",
    "content": "import { ApiPropertyOptional, OmitType } from '@nestjs/swagger';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { Expose } from 'class-transformer';\nimport { HiddenField } from 'src/common/decorators/hidden-field.decorator';\n\nexport class SentinelMasterResponse extends OmitType(SentinelMaster, [\n  'password',\n] as const) {\n  @ApiPropertyOptional({\n    description:\n      'The password for your Redis Sentinel master. ' +\n      'If your master doesn’t require a password, leave this field empty.',\n    type: Boolean,\n  })\n  @Expose()\n  @HiddenField(true)\n  password?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/dto/update.sentinel.master.dto.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\n\nexport class UpdateSentinelMasterDto extends PickType(SentinelMaster, [\n  'username',\n  'password',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/models/sentinel-master.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport { Expose } from 'class-transformer';\nimport { Endpoint } from 'src/common/models';\n\nexport enum SentinelMasterStatus {\n  Active = 'active',\n  Down = 'down',\n}\n\nexport class SentinelMaster {\n  @ApiProperty({\n    description:\n      'Sentinel master group name. Identifies a group of Redis instances composed of a master and one or more slaves.',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  name: string;\n\n  @ApiPropertyOptional({\n    description:\n      'Sentinel username, if your database is ACL enabled, otherwise leave this field empty.',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  username?: string;\n\n  @ApiPropertyOptional({\n    description:\n      'The password for your Redis Sentinel master. ' +\n      'If your master doesn’t require a password, leave this field empty.',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  password?: string;\n\n  @ApiPropertyOptional({\n    description: 'The hostname of Sentinel master.',\n    type: String,\n    default: 'localhost',\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  host?: string;\n\n  @ApiPropertyOptional({\n    description: 'The port Sentinel master.',\n    type: Number,\n    default: 6379,\n  })\n  @Expose()\n  @IsInt({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  port?: number;\n\n  @ApiPropertyOptional({\n    description: 'Sentinel master status',\n    enum: SentinelMasterStatus,\n    default: SentinelMasterStatus.Active,\n  })\n  status?: SentinelMasterStatus;\n\n  @ApiPropertyOptional({\n    description: 'The number of slaves.',\n    type: Number,\n    default: 0,\n  })\n  numberOfSlaves?: number;\n\n  @ApiPropertyOptional({\n    description: 'Sentinel master endpoints.',\n    type: Endpoint,\n    isArray: true,\n  })\n  nodes?: Endpoint[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/models/sentinel.ts",
    "content": "export interface ISentinelConnectionOptions {\n  name: string;\n  sentinels: Array<{ host: string; port: number }>;\n  sentinelUsername?: string;\n  sentinelPassword?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { mockSentinelMasterDto, mockSessionMetadata } from 'src/__mocks__';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics';\nimport { SentinelMasterStatus } from 'src/modules/redis-sentinel/models/sentinel-master';\n\ndescribe('RedisSentinelAnalytics', () => {\n  let service: RedisSentinelAnalytics;\n  let sendEventMethod;\n  let sendFailedEventMethod;\n  const httpException = new InternalServerErrorException();\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, RedisSentinelAnalytics],\n    }).compile();\n\n    service = await module.get<RedisSentinelAnalytics>(RedisSentinelAnalytics);\n    sendEventMethod = jest.spyOn<RedisSentinelAnalytics, any>(\n      service,\n      'sendEvent',\n    );\n    sendFailedEventMethod = jest.spyOn<RedisSentinelAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n  });\n\n  describe('sendGetSentinelMastersSucceedEvent', () => {\n    it('should emit event with active master groups', () => {\n      service.sendGetSentinelMastersSucceedEvent(mockSessionMetadata, [\n        mockSentinelMasterDto,\n        mockSentinelMasterDto,\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoverySucceed,\n        {\n          numberOfAvailablePrimaryGroups: 2,\n          totalNumberOfPrimaryGroups: 2,\n          totalNumberOfReplicas: 2,\n        },\n      );\n    });\n    it('should emit event with active and not active master groups', () => {\n      service.sendGetSentinelMastersSucceedEvent(mockSessionMetadata, [\n        mockSentinelMasterDto,\n        {\n          ...mockSentinelMasterDto,\n          status: SentinelMasterStatus.Down,\n          numberOfSlaves: 0,\n        },\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoverySucceed,\n        {\n          numberOfAvailablePrimaryGroups: 1,\n          totalNumberOfPrimaryGroups: 2,\n          totalNumberOfReplicas: 1,\n        },\n      );\n    });\n    it('should emit event without active groups', () => {\n      service.sendGetSentinelMastersSucceedEvent(mockSessionMetadata, [\n        {\n          ...mockSentinelMasterDto,\n          status: SentinelMasterStatus.Down,\n          numberOfSlaves: 0,\n        },\n        {\n          ...mockSentinelMasterDto,\n          numberOfSlaves: 0,\n          status: SentinelMasterStatus.Down,\n        },\n      ]);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoverySucceed,\n        {\n          numberOfAvailablePrimaryGroups: 0,\n          totalNumberOfPrimaryGroups: 2,\n          totalNumberOfReplicas: 0,\n        },\n      );\n    });\n    it('should emit event for empty list', () => {\n      service.sendGetSentinelMastersSucceedEvent(mockSessionMetadata, []);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoverySucceed,\n        {\n          numberOfAvailablePrimaryGroups: 0,\n          totalNumberOfPrimaryGroups: 0,\n          totalNumberOfReplicas: 0,\n        },\n      );\n    });\n    it('should emit event for undefined input value', () => {\n      service.sendGetSentinelMastersSucceedEvent(\n        mockSessionMetadata,\n        undefined,\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoverySucceed,\n        {\n          numberOfAvailablePrimaryGroups: 0,\n          totalNumberOfPrimaryGroups: 0,\n          totalNumberOfReplicas: 0,\n        },\n      );\n    });\n    it('should not throw on error', () => {\n      const input: any = {};\n\n      expect(() =>\n        service.sendGetSentinelMastersSucceedEvent(mockSessionMetadata, input),\n      ).not.toThrow();\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('sendGetRedisCloudSubsFailedEvent', () => {\n    it('should emit event', () => {\n      service.sendGetSentinelMastersFailedEvent(\n        mockSessionMetadata,\n        httpException,\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoveryFailed,\n        httpException,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport {\n  SentinelMaster,\n  SentinelMasterStatus,\n} from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class RedisSentinelAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendGetSentinelMastersSucceedEvent(\n    sessionMetadata: SessionMetadata,\n    groups: SentinelMaster[] = [],\n  ) {\n    try {\n      this.sendEvent(\n        sessionMetadata,\n        TelemetryEvents.SentinelMasterGroupsDiscoverySucceed,\n        {\n          numberOfAvailablePrimaryGroups: groups.filter(\n            (db) => db.status === SentinelMasterStatus.Active,\n          ).length,\n          totalNumberOfPrimaryGroups: groups.length,\n          totalNumberOfReplicas: groups.reduce<number>(\n            (sum, group) => sum + group.numberOfSlaves,\n            0,\n          ),\n        },\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendGetSentinelMastersFailedEvent(\n    sessionMetadata: SessionMetadata,\n    exception: HttpException,\n  ) {\n    this.sendFailedEvent(\n      sessionMetadata,\n      TelemetryEvents.SentinelMasterGroupsDiscoveryFailed,\n      exception,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.controller.spec.ts",
    "content": "import { when } from 'jest-when';\nimport * as request from 'supertest';\nimport { Test } from '@nestjs/testing';\nimport {\n  ForbiddenException,\n  INestApplication,\n  MiddlewareConsumer,\n  Module,\n  NestModule,\n} from '@nestjs/common';\nimport { RedisSentinelService } from 'src/modules/redis-sentinel/redis-sentinel.service';\nimport {\n  mockCreateSentinelDatabasesDto,\n  mockRedisSentinelService,\n  mockSessionService,\n} from 'src/__mocks__';\nimport { RedisSentinelController } from 'src/modules/redis-sentinel/redis-sentinel.controller';\nimport { SingleUserAuthMiddleware } from 'src/common/middlewares/single-user-auth.middleware';\nimport { SessionService } from 'src/modules/session/session.service';\nimport config, { Config } from 'src/utils/config';\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\n@Module({\n  controllers: [RedisSentinelController],\n  providers: [\n    {\n      provide: RedisSentinelService,\n      useFactory: mockRedisSentinelService,\n    },\n    {\n      provide: SessionService,\n      useFactory: mockSessionService,\n    },\n  ],\n})\nclass TestModule implements NestModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(SingleUserAuthMiddleware).forRoutes('*');\n  }\n}\n\ndescribe('RedisSentinelController', () => {\n  let app: INestApplication;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const moduleRef = await Test.createTestingModule({\n      imports: [TestModule],\n    }).compile();\n\n    app = moduleRef.createNestApplication();\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  describe('POST /redis-sentinel/databases', () => {\n    it('should succeed when database management enabled', async () => {\n      mockServerConfig.databaseManagement = true;\n\n      return request(app.getHttpServer())\n        .post('/redis-sentinel/databases')\n        .send(mockCreateSentinelDatabasesDto)\n        .expect(200);\n    });\n\n    it('should fail when database management disabled', async () => {\n      mockServerConfig.databaseManagement = false;\n\n      return request(app.getHttpServer())\n        .post('/redis-sentinel/databases')\n        .send(mockCreateSentinelDatabasesDto)\n        .expect(403)\n        .expect(\n          new ForbiddenException(\n            'Database connection management is disabled.',\n          ).getResponse(),\n        );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Post,\n  Res,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { Database } from 'src/modules/database/models/database';\nimport { DiscoverSentinelMastersDto } from 'src/modules/redis-sentinel/dto/discover.sentinel-masters.dto';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { ActionStatus, SessionMetadata } from 'src/common/models';\nimport { Response } from 'express';\nimport { RedisSentinelService } from 'src/modules/redis-sentinel/redis-sentinel.service';\nimport { CreateSentinelDatabasesDto } from 'src/modules/redis-sentinel/dto/create.sentinel.databases.dto';\nimport { CreateSentinelDatabaseResponse } from 'src/modules/redis-sentinel/dto/create.sentinel.database.response';\nimport { BuildType } from 'src/modules/server/models/server';\nimport {\n  DatabaseManagement,\n  RequestSessionMetadata,\n} from 'src/common/decorators';\n\n@ApiTags('Redis OSS Sentinel')\n@Controller('redis-sentinel')\n@UsePipes(\n  new ValidationPipe({\n    transform: true,\n    whitelist: true,\n    forbidNonWhitelisted: true,\n  }),\n)\nexport class RedisSentinelController {\n  constructor(private redisSentinelService: RedisSentinelService) {}\n\n  @Post('get-databases')\n  @UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))\n  @ApiEndpoint({\n    description: 'Get master groups',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: SentinelMaster,\n        isArray: true,\n      },\n    ],\n  })\n  async getMasters(\n    @Body() dto: DiscoverSentinelMastersDto,\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<SentinelMaster[]> {\n    return await this.redisSentinelService.getSentinelMasters(\n      sessionMetadata,\n      dto as Database,\n    );\n  }\n\n  @UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))\n  @Post('databases')\n  @ApiEndpoint({\n    statusCode: 201,\n    description: 'Add masters from Redis Sentinel',\n    excludeFor: [BuildType.RedisStack],\n    responses: [\n      {\n        status: 201,\n        description: 'Ok',\n        type: CreateSentinelDatabaseResponse,\n        isArray: true,\n      },\n    ],\n  })\n  @UsePipes(new ValidationPipe({ transform: true }))\n  @DatabaseManagement()\n  async addSentinelMasters(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: CreateSentinelDatabasesDto,\n    @Res() res: Response,\n  ): Promise<Response> {\n    const result = await this.redisSentinelService.createSentinelDatabases(\n      sessionMetadata,\n      dto,\n    );\n    const hasSuccessResult = result.some(\n      (addResponse: CreateSentinelDatabaseResponse) =>\n        addResponse.status === ActionStatus.Success,\n    );\n    if (!hasSuccessResult) {\n      return res.status(200).json(result);\n    }\n    return res.json(result);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { RedisSentinelService } from 'src/modules/redis-sentinel/redis-sentinel.service';\nimport { RedisSentinelController } from 'src/modules/redis-sentinel/redis-sentinel.controller';\nimport { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics';\n\n@Module({\n  controllers: [RedisSentinelController],\n  providers: [RedisSentinelService, RedisSentinelAnalytics],\n  exports: [RedisSentinelService, RedisSentinelAnalytics],\n})\nexport class RedisSentinelModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.spec.ts",
    "content": "import {\n  mockRedisSentinelUtilModule,\n  mockRedisSentinelUtil,\n} from 'src/__mocks__/redis-utils';\n\njest.doMock(\n  'src/modules/redis/utils/sentinel.util',\n  mockRedisSentinelUtilModule,\n);\n\nimport { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockConstantsProvider,\n  mockDatabaseFactory,\n  mockDatabaseService,\n  mockIORedisClient,\n  mockRedisClientFactory,\n  mockRedisSentinelAnalytics,\n  mockRedisSentinelMasterResponse,\n  mockSentinelDatabaseWithTlsAuth,\n  mockSentinelMasterDto,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { RedisSentinelService } from 'src/modules/redis-sentinel/redis-sentinel.service';\nimport { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { DatabaseFactory } from 'src/modules/database/providers/database.factory';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\nimport { RedisConnectionIncorrectCertificateException } from 'src/modules/redis/exceptions/connection';\n\ndescribe('RedisSentinelService', () => {\n  let service: RedisSentinelService;\n  let redisClientFactory: MockType<RedisClientFactory>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        RedisSentinelService,\n        {\n          provide: RedisSentinelAnalytics,\n          useFactory: mockRedisSentinelAnalytics,\n        },\n        {\n          provide: RedisClientFactory,\n          useFactory: mockRedisClientFactory,\n        },\n        {\n          provide: DatabaseService,\n          useFactory: mockDatabaseService,\n        },\n        {\n          provide: DatabaseFactory,\n          useFactory: mockDatabaseFactory,\n        },\n        {\n          provide: ConstantsProvider,\n          useFactory: mockConstantsProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(RedisSentinelService);\n    redisClientFactory = module.get(RedisClientFactory);\n  });\n\n  describe('getSentinelMasters', () => {\n    it('connect and get sentinel masters', async () => {\n      redisClientFactory\n        .getConnectionStrategy()\n        .createStandaloneClient.mockResolvedValue(mockIORedisClient);\n      mockIORedisClient.call.mockResolvedValue(mockRedisSentinelMasterResponse);\n      mockRedisSentinelUtil.discoverSentinelMasterGroups.mockResolvedValue([\n        mockSentinelMasterDto,\n      ]);\n\n      const result = await service.getSentinelMasters(\n        mockSessionMetadata,\n        mockSentinelDatabaseWithTlsAuth,\n      );\n\n      expect(result).toEqual([mockSentinelMasterDto]);\n      expect(mockIORedisClient.disconnect).toHaveBeenCalled();\n    });\n\n    it('failed connection to the redis database', async () => {\n      redisClientFactory\n        .getConnectionStrategy()\n        .createStandaloneClient.mockRejectedValue(\n          new RedisConnectionIncorrectCertificateException(),\n        );\n\n      await expect(\n        service.getSentinelMasters(\n          mockSessionMetadata,\n          mockSentinelDatabaseWithTlsAuth,\n        ),\n      ).rejects.toThrow(RedisConnectionIncorrectCertificateException);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { v4 as uuidv4 } from 'uuid';\nimport { CreateSentinelDatabaseResponse } from 'src/modules/redis-sentinel/dto/create.sentinel.database.response';\nimport { CreateSentinelDatabasesDto } from 'src/modules/redis-sentinel/dto/create.sentinel.databases.dto';\nimport { Database } from 'src/modules/database/models/database';\nimport {\n  ActionStatus,\n  ClientContext,\n  SessionMetadata,\n} from 'src/common/models';\nimport { DatabaseService } from 'src/modules/database/database.service';\nimport { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master';\nimport { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics';\nimport { DatabaseFactory } from 'src/modules/database/providers/database.factory';\nimport { discoverSentinelMasterGroups } from 'src/modules/redis/utils';\nimport { RedisClientFactory } from 'src/modules/redis/redis.client.factory';\nimport { ConstantsProvider } from 'src/modules/constants/providers/constants.provider';\n\n@Injectable()\nexport class RedisSentinelService {\n  private logger = new Logger('RedisSentinelService');\n\n  constructor(\n    private readonly redisClientFactory: RedisClientFactory,\n    private readonly databaseService: DatabaseService,\n    private readonly databaseFactory: DatabaseFactory,\n    private readonly redisSentinelAnalytics: RedisSentinelAnalytics,\n    private readonly constantsProvider: ConstantsProvider,\n  ) {}\n\n  /**\n   * Bulk create sentinel databases\n   * Will not fail on connection or any other errors during adding each database\n   * Returns statuses instead\n   * todo: Handle unique certificate issue\n   * @param sessionMetadata\n   * @param dto\n   */\n  public async createSentinelDatabases(\n    sessionMetadata: SessionMetadata,\n    dto: CreateSentinelDatabasesDto,\n  ): Promise<CreateSentinelDatabaseResponse[]> {\n    this.logger.debug('Adding Sentinel masters.', sessionMetadata);\n    const result: CreateSentinelDatabaseResponse[] = [];\n    const { masters, ...connectionOptions } = dto;\n    try {\n      //\n      // const client = await this.redisService.createStandaloneClient(\n      //   connectionOptions as Database,\n      //   AppTool.Common,\n      //   false,\n      // );\n      //\n      // const isOssSentinel = await this.redisConfBusinessService.checkSentinelConnection(\n      //   client,\n      // );\n\n      // if (!isOssSentinel) {\n      //   await client.disconnect();\n      //   this.logger.error(\n      //     `Failed to add Sentinel masters. ${ERROR_MESSAGES.WRONG_DATABASE_TYPE}.`,\n      //   );\n      //   const exception = new BadRequestException(\n      //     ERROR_MESSAGES.WRONG_DATABASE_TYPE,\n      //   );\n      //   this.instancesAnalyticsService.sendInstanceAddFailedEvent(exception);\n      //   return Promise.reject(exception);\n      // }\n      //\n\n      await Promise.all(\n        masters.map(async (master) => {\n          const { alias, name, password, username, db } = master;\n          try {\n            const model = await this.databaseService.create(\n              sessionMetadata,\n              {\n                ...connectionOptions,\n                name: alias,\n                db,\n                sentinelMaster: {\n                  name,\n                  username,\n                  password,\n                },\n              } as Database,\n              undefined,\n              { enableReadyCheck: false },\n            );\n\n            result.push({\n              id: model.id,\n              name,\n              status: ActionStatus.Success,\n              message: 'Added',\n            });\n          } catch (error) {\n            result.push({\n              name,\n              status: ActionStatus.Fail,\n              message: error?.response?.message,\n              error: error?.response,\n            });\n          }\n        }),\n      );\n\n      return result;\n    } catch (error) {\n      this.logger.error(\n        'Failed to add Sentinel masters.',\n        error,\n        sessionMetadata,\n      );\n      throw error;\n    }\n  }\n\n  /**\n   * Check connection and get sentinel masters\n   * @param sessionMetadata\n   * @param dto\n   */\n  public async getSentinelMasters(\n    sessionMetadata: SessionMetadata,\n    dto: Database,\n  ): Promise<SentinelMaster[]> {\n    this.logger.debug(\n      'Connection and getting sentinel masters.',\n      sessionMetadata,\n    );\n    let result: SentinelMaster[];\n    try {\n      const database =\n        await this.databaseFactory.createStandaloneDatabaseModel(dto);\n      const client = await this.redisClientFactory\n        .getConnectionStrategy()\n        .createStandaloneClient(\n          {\n            sessionMetadata: this.constantsProvider.getSystemSessionMetadata(),\n            databaseId: database.id || uuidv4(),\n            context: ClientContext.Common,\n          },\n          database,\n          { useRetry: false, enableReadyCheck: false },\n        );\n      result = await discoverSentinelMasterGroups(client);\n      this.redisSentinelAnalytics.sendGetSentinelMastersSucceedEvent(\n        sessionMetadata,\n        result,\n      );\n\n      await client.disconnect();\n    } catch (error) {\n      this.redisSentinelAnalytics.sendGetSentinelMastersFailedEvent(\n        sessionMetadata,\n        error,\n      );\n\n      throw error;\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/dto/server.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { AppType, PackageType } from 'src/modules/server/models/server';\n\nexport class GetServerInfoResponse {\n  @ApiProperty({\n    description: 'Server identifier.',\n    type: String,\n  })\n  id: string;\n\n  @ApiProperty({\n    description: 'Time of the first server launch.',\n    type: String,\n    format: 'date-time',\n    example: '2021-01-06T12:44:39.000Z',\n  })\n  createDateTime: string;\n\n  @ApiProperty({\n    description: 'Version of the application.',\n    type: String,\n    example: '2.0.0',\n  })\n  appVersion: string;\n\n  @ApiProperty({\n    description: 'The operating system platform.',\n    type: String,\n    example: 'linux',\n  })\n  osPlatform: string;\n\n  @ApiProperty({\n    description: 'Application build type.',\n    type: String,\n    example: 'ELECTRON',\n  })\n  buildType: string;\n\n  @ApiProperty({\n    description: 'Application package type.',\n    enum: PackageType,\n    example: 'app-image',\n  })\n  packageType: PackageType;\n\n  @ApiProperty({\n    description: 'Application type.',\n    enum: AppType,\n    example: 'DOCKER',\n  })\n  appType: AppType;\n\n  @ApiPropertyOptional({\n    description: 'Fixed Redis database id.',\n    type: String,\n  })\n  fixedDatabaseId?: string;\n\n  @ApiProperty({\n    description: 'List of available encryption strategies',\n    type: [String],\n    example: ['PLAIN', 'KEYTAR'],\n  })\n  encryptionStrategies: string[];\n\n  @ApiProperty({\n    description: 'Server session id.',\n    type: Number,\n  })\n  sessionId: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/entities/server.entity.ts",
    "content": "import { Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';\nimport { Expose } from 'class-transformer';\n\n@Entity('server')\nexport class ServerEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @CreateDateColumn({ type: 'datetime', nullable: false })\n  @Expose()\n  createDateTime: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/health.controller.ts",
    "content": "import { Controller, Get } from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\n\n@ApiTags('Info')\n@Controller('health')\nexport class HealthController {\n  @Get('')\n  @ApiEndpoint({\n    description: 'Get server info',\n    statusCode: 200,\n  })\n  async health(): Promise<object> {\n    return { status: 'up' };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/local.server.service.spec.ts",
    "content": "import { when } from 'jest-when';\nimport { TestingModule, Test } from '@nestjs/testing';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockEncryptionService,\n  mockFeaturesConfigService,\n  mockGetServerInfoResponse,\n  mockServerRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { ServerInfoNotFoundException } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { EncryptionStrategy } from 'src/modules/encryption/models';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { ServerRepository } from 'src/modules/server/repositories/server.repository';\nimport { FeaturesConfigService } from 'src/modules/feature/features-config.service';\nimport { LocalServerService } from 'src/modules/server/local.server.service';\nimport { AppType, BuildType } from 'src/modules/server/models/server';\nimport config, { Config } from 'src/utils/config';\n\njest.mock(\n  'src/utils/config',\n  jest.fn(() => jest.requireActual('src/utils/config') as object),\n);\n\nconst mockServerConfig = config.get('server') as Config['server'];\n\ndescribe('LocalServerService', () => {\n  let service: ServerService;\n  let serverRepository: MockType<ServerRepository>;\n  let eventEmitter: EventEmitter2;\n  let encryptionService: MockType<EncryptionService>;\n  let configGetSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    configGetSpy = jest.spyOn(config, 'get');\n\n    mockServerConfig.buildType = BuildType.DockerOnPremise;\n    mockServerConfig.appType = AppType.Docker;\n    when(configGetSpy).calledWith('server').mockReturnValue(mockServerConfig);\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EventEmitter2,\n        LocalServerService,\n        {\n          provide: ServerRepository,\n          useFactory: mockServerRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n        {\n          provide: FeaturesConfigService,\n          useFactory: mockFeaturesConfigService,\n        },\n      ],\n    }).compile();\n\n    serverRepository = module.get(ServerRepository);\n    eventEmitter = module.get(EventEmitter2);\n    encryptionService = module.get(EncryptionService);\n    service = module.get(LocalServerService);\n    jest.spyOn(eventEmitter, 'emit');\n  });\n\n  describe('init', () => {\n    it('should create server instance on first application launch', async () => {\n      serverRepository.exists.mockResolvedValueOnce(false);\n\n      expect(await service.init()).toEqual(true);\n\n      expect(serverRepository.exists).toHaveBeenCalled();\n      expect(serverRepository.getOrCreate).toHaveBeenCalled();\n    });\n    it('should not create server instance on the second application launch', async () => {\n      serverRepository.exists.mockResolvedValueOnce(true);\n\n      expect(await service.init()).toEqual(false);\n\n      expect(serverRepository.exists).toHaveBeenCalled();\n      expect(serverRepository.getOrCreate).toHaveBeenCalled();\n    });\n  });\n\n  describe('getInfo', () => {\n    it('should return server info', async () => {\n      encryptionService.getAvailableEncryptionStrategies.mockResolvedValue([\n        EncryptionStrategy.PLAIN,\n        EncryptionStrategy.KEYTAR,\n      ]);\n      const result = await service.getInfo(mockSessionMetadata);\n\n      expect(result).toEqual({\n        ...mockGetServerInfoResponse,\n        sessionId: expect.any(Number),\n        packageType: undefined, // should be undefined for non-electron applications\n      });\n    });\n    it('should throw ServerInfoNotFoundException', async () => {\n      serverRepository.getOrCreate.mockResolvedValue(null);\n\n      try {\n        await service.getInfo(mockSessionMetadata);\n      } catch (err) {\n        expect(err).toBeInstanceOf(ServerInfoNotFoundException);\n        expect(err.message).toEqual(ERROR_MESSAGES.SERVER_INFO_NOT_FOUND());\n      }\n    });\n    it('should throw InternalServerError', async () => {\n      serverRepository.getOrCreate.mockRejectedValue(new Error('some error'));\n\n      try {\n        await service.getInfo(mockSessionMetadata);\n        fail('Should throw an error');\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n\n  describe('getAppType', () => {\n    afterEach(async () => {\n      delete process.env.RI_APP_TYPE;\n    });\n\n    it('should return predefined appType via env variable', () => {\n      mockServerConfig.appType = 'ELECTRON_ENTERPRISE';\n\n      expect(ServerService.getAppType(BuildType.Electron)).toEqual(\n        AppType.ElectronEnterprise,\n      );\n    });\n\n    it('should return predefined appType via env variable (case insensitive)', () => {\n      mockServerConfig.appType = 'elecTron_enterPrise';\n\n      expect(ServerService.getAppType(BuildType.Electron)).toEqual(\n        AppType.ElectronEnterprise,\n      );\n    });\n\n    it('should determine app type based on input when type in app type env', () => {\n      mockServerConfig.appType = 'electron_enterprise1';\n\n      expect(ServerService.getAppType(BuildType.Electron)).toEqual(\n        AppType.Electron,\n      );\n    });\n\n    it('should determine app type based on input when no app type env defined', () => {\n      mockServerConfig.appType = undefined;\n\n      expect(ServerService.getAppType(BuildType.Electron)).toEqual(\n        AppType.Electron,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/local.server.service.ts",
    "content": "import { ServerInfoNotFoundException } from 'src/constants';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { ServerRepository } from 'src/modules/server/repositories/server.repository';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { GetServerInfoResponse } from 'src/modules/server/dto/server.dto';\nimport { Injectable, InternalServerErrorException } from '@nestjs/common';\nimport config, { Config } from 'src/utils/config';\nimport { SessionMetadata } from 'src/common/models';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\nconst REDIS_STACK_CONFIG = config.get('redisStack') as Config['redisStack'];\n\n@Injectable()\nexport class LocalServerService extends ServerService {\n  constructor(\n    private readonly repository: ServerRepository,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async init(sessionMetadata?: SessionMetadata): Promise<boolean> {\n    this.logger.debug('Initializing server module...', sessionMetadata);\n\n    let firstStart = true;\n\n    if (await this.repository.exists(sessionMetadata)) {\n      this.logger.debug('First application launch.', sessionMetadata);\n      firstStart = false;\n    }\n\n    await this.repository.getOrCreate(sessionMetadata);\n\n    return firstStart;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getInfo(\n    sessionMetadata: SessionMetadata,\n  ): Promise<GetServerInfoResponse> {\n    this.logger.debug('Getting server info.', sessionMetadata);\n    try {\n      const info = await this.repository.getOrCreate(sessionMetadata);\n\n      if (!info) {\n        return Promise.reject(new ServerInfoNotFoundException());\n      }\n\n      const result = {\n        ...info,\n        sessionId: this.sessionId,\n        appVersion: SERVER_CONFIG.appVersion,\n        osPlatform: process.platform,\n        buildType: SERVER_CONFIG.buildType,\n        appType: ServerService.getAppType(SERVER_CONFIG.buildType),\n        encryptionStrategies:\n          await this.encryptionService.getAvailableEncryptionStrategies(),\n        fixedDatabaseId: REDIS_STACK_CONFIG?.id,\n        packageType: ServerService.getPackageType(SERVER_CONFIG.buildType),\n      };\n\n      this.logger.debug('Succeed to get server info.', sessionMetadata);\n      return result;\n    } catch (error) {\n      this.logger.error(\n        'Failed to get application settings.',\n        error,\n        sessionMetadata,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/models/server.ts",
    "content": "import { Expose } from 'class-transformer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport enum BuildType {\n  RedisStack = 'REDIS_STACK',\n  Electron = 'ELECTRON',\n  DockerOnPremise = 'DOCKER_ON_PREMISE',\n  VSCode = 'VS_CODE',\n}\n\nexport enum PackageType {\n  Flatpak = 'flatpak',\n  Snap = 'snap',\n  UnknownLinux = 'unknown-linux',\n  AppImage = 'app-image',\n  Mas = 'mas',\n  UnknownDarwin = 'unknown-darwin',\n  WindowsStore = 'windows-store',\n  UnknownWindows = 'unknown-windows',\n  Unknown = 'unknown',\n}\n\nexport enum AppType {\n  RedisStackWeb = 'REDIS_STACK_WEB',\n  RedisStackApp = 'REDIS_STACK_ELECTRON',\n  Electron = 'ELECTRON',\n  ElectronEnterprise = 'ELECTRON_ENTERPRISE',\n  Docker = 'DOCKER',\n  VSCode = 'VS_CODE',\n  VSCodeEnterprise = 'VS_CODE_ENTERPRISE',\n  Unknown = 'UNKNOWN',\n}\n\nexport class Server {\n  @ApiProperty({\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    type: Date,\n  })\n  @Expose()\n  createDateTime: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/repositories/local.server.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockRepository,\n  mockServer,\n  mockServerEntity,\n  MockType,\n} from 'src/__mocks__';\nimport { LocalServerRepository } from 'src/modules/server/repositories/local.server.repository';\nimport { ServerEntity } from 'src/modules/server/entities/server.entity';\n\ndescribe('LocalServerRepository', () => {\n  let service: LocalServerRepository;\n  let repository: MockType<Repository<ServerEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalServerRepository,\n        {\n          provide: getRepositoryToken(ServerEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(ServerEntity));\n    service = await module.get(LocalServerRepository);\n\n    repository.findOneBy.mockResolvedValue(mockServerEntity);\n    repository.save.mockResolvedValue(mockServerEntity);\n    repository.create.mockReturnValue(new ServerEntity());\n  });\n\n  describe('exists', () => {\n    it('should return false if entity does not exists in the database', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      expect(await service.exists()).toEqual(false);\n    });\n\n    it('should return true if entity exists in the database', async () => {\n      expect(await service.exists()).toEqual(true);\n    });\n  });\n\n  describe('getOrCreate', () => {\n    it(\"should create server entity if it doesn't exists before\", async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      expect(await service.getOrCreate()).toEqual(mockServer);\n      expect(repository.save).toHaveBeenCalled();\n    });\n\n    it('should get existing server', async () => {\n      expect(await service.exists()).toEqual(true);\n      expect(repository.save).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/repositories/local.server.repository.ts",
    "content": "import { Server } from 'src/modules/server/models/server';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { ServerEntity } from 'src/modules/server/entities/server.entity';\nimport { ServerRepository } from 'src/modules/server/repositories/server.repository';\nimport { classToClass } from 'src/utils';\n\nexport class LocalServerRepository extends ServerRepository {\n  constructor(\n    @InjectRepository(ServerEntity)\n    private readonly repository: Repository<ServerEntity>,\n  ) {\n    super();\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async exists(): Promise<boolean> {\n    return !!(await this.repository.findOneBy({}));\n  }\n\n  /**\n   * @inheritDoc\n   */\n  public async getOrCreate(): Promise<Server> {\n    let entity = await this.repository.findOneBy({});\n\n    if (!entity) {\n      entity = await this.repository.save(this.repository.create());\n    }\n\n    return classToClass(Server, entity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/repositories/server.repository.ts",
    "content": "import { Server } from 'src/modules/server/models/server';\nimport { SessionMetadata } from 'src/common/models';\n\nexport abstract class ServerRepository {\n  /**\n   * Fast check if server model exists by id\n   * No need to retrieve any fields should return boolean only\n   * @param sessionMetadata\n   * @return boolean\n   */\n  abstract exists(sessionMetadata: SessionMetadata): Promise<boolean>;\n\n  /**\n   * Get Server model or create and return new one\n   * @param sessionMetadata\n   */\n  abstract getOrCreate(sessionMetadata: SessionMetadata): Promise<Server>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/server.controller.ts",
    "content": "import { Controller, Get, UsePipes, ValidationPipe } from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { getBlockingCommands } from 'src/utils/cli-helper';\nimport { getUnsupportedCommands } from 'src/modules/cli/utils/getUnsupportedCommands';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { GetServerInfoResponse } from 'src/modules/server/dto/server.dto';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport { SessionMetadata } from 'src/common/models';\n\n@ApiTags('Info')\n@Controller('info')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class ServerController {\n  constructor(private serverService: ServerService) {}\n\n  @Get('')\n  @ApiEndpoint({\n    description: 'Get server info',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Server Info',\n        type: GetServerInfoResponse,\n      },\n    ],\n  })\n  async getInfo(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<GetServerInfoResponse> {\n    return this.serverService.getInfo(sessionMetadata);\n  }\n\n  @Get('/cli-unsupported-commands')\n  @ApiEndpoint({\n    description: 'Get list of unsupported commands in CLI',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Unsupported commands',\n        type: String,\n        isArray: true,\n      },\n    ],\n  })\n  async getCliUnsupportedCommands(): Promise<string[]> {\n    return getUnsupportedCommands();\n  }\n\n  @Get('/cli-blocking-commands')\n  @ApiEndpoint({\n    description: 'Get list of blocking commands in CLI',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Blocking commands',\n        type: String,\n        isArray: true,\n      },\n    ],\n  })\n  async getCliBlockingCommands(): Promise<string[]> {\n    return getBlockingCommands();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/server.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { ServerController } from 'src/modules/server/server.controller';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { ServerRepository } from 'src/modules/server/repositories/server.repository';\nimport { LocalServerRepository } from 'src/modules/server/repositories/local.server.repository';\nimport { FeatureModule } from 'src/modules/feature/feature.module';\nimport { HealthController } from 'src/modules/server/health.controller';\nimport { LocalServerService } from 'src/modules/server/local.server.service';\n\n@Module({})\nexport class ServerModule {\n  static register(\n    serverRepository: Type<ServerRepository> = LocalServerRepository,\n    serverService: Type<ServerService> = LocalServerService,\n  ) {\n    return {\n      module: ServerModule,\n      controllers: [ServerController, HealthController],\n      providers: [\n        {\n          provide: ServerRepository,\n          useClass: serverRepository,\n        },\n        {\n          provide: ServerService,\n          useClass: serverService,\n        },\n      ],\n      imports: [FeatureModule],\n      exports: [ServerService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/server/server.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport {\n  AppType,\n  BuildType,\n  PackageType,\n} from 'src/modules/server/models/server';\nimport { GetServerInfoResponse } from 'src/modules/server/dto/server.dto';\nimport { SessionMetadata } from 'src/common/models';\nimport config, { Config } from 'src/utils/config';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Injectable()\nexport abstract class ServerService {\n  protected logger = new Logger(this.constructor.name);\n\n  protected sessionId: number = new Date().getTime();\n\n  static getSupportedAppType(): AppType {\n    if (SERVER_CONFIG.appType) {\n      const predefinedAppType = SERVER_CONFIG.appType.toUpperCase();\n      const enumValues = Object.values(AppType);\n\n      return enumValues.find((value) => value === predefinedAppType);\n    }\n  }\n\n  static getAppType(buildType: string): AppType {\n    const appType = ServerService.getSupportedAppType();\n\n    if (appType) {\n      return appType;\n    }\n\n    switch (buildType) {\n      case BuildType.DockerOnPremise:\n        return AppType.Docker;\n      case BuildType.Electron:\n        return AppType.Electron;\n      case BuildType.RedisStack:\n        return AppType.RedisStackWeb;\n      case BuildType.VSCode:\n        return AppType.VSCode;\n      default:\n        return AppType.Unknown;\n    }\n  }\n\n  static getPackageType(buildType: string): PackageType {\n    if (buildType === BuildType.Electron) {\n      // Darwin\n      if (process.platform === 'darwin') {\n        if (process.env.mas || process['mas']) {\n          return PackageType.Mas;\n        }\n\n        return PackageType.UnknownDarwin;\n      }\n\n      // Linux\n      if (process.platform === 'linux') {\n        if (process.env.APPIMAGE) {\n          return PackageType.AppImage;\n        }\n\n        if (process.env.SNAP_INSTANCE_NAME || process.env.SNAP_DATA) {\n          return PackageType.Snap;\n        }\n\n        if (process.env.container) {\n          return PackageType.Flatpak;\n        }\n\n        return PackageType.UnknownLinux;\n      }\n\n      // Windows\n      if (process.platform === 'win32') {\n        if (process.env.windowsStore || process['windowsStore']) {\n          return PackageType.WindowsStore;\n        }\n\n        return PackageType.UnknownWindows;\n      }\n\n      return PackageType.Unknown;\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Initialize Server module.\n   * Note: should be called once\n   */\n  public abstract init(): Promise<boolean>;\n\n  /**\n   * Get general server info\n   * @param sessionMetadata\n   */\n  public abstract getInfo(\n    sessionMetadata: SessionMetadata,\n  ): Promise<GetServerInfoResponse>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/providers/session.provider.ts",
    "content": "import { SessionStorage } from 'src/modules/session/providers/storage/session.storage';\nimport { Session } from 'src/common/models';\nimport { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport abstract class SessionProvider {\n  constructor(protected readonly sessionStorage: SessionStorage) {}\n\n  abstract getSession(id: string): Promise<Session>;\n  abstract createSession(session: Session): Promise<Session>;\n  abstract updateSessionData(id: string, data: object): Promise<Session>;\n  abstract deleteSession(id: string): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/providers/single-user.session.provider.spec.ts",
    "content": "import { TestingModule, Test } from '@nestjs/testing';\nimport { mockInitSession, mockSessionStorage } from 'src/__mocks__';\nimport { SingleUserSessionProvider } from 'src/modules/session/providers/single-user.session.provider';\nimport { SessionStorage } from 'src/modules/session/providers/storage/session.storage';\nimport { DEFAULT_SESSION_ID } from 'src/common/constants';\n\ndescribe('SingleUserSessionProvider', () => {\n  let service: SingleUserSessionProvider;\n  let sessionStorage: SessionStorage;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        SingleUserSessionProvider,\n        {\n          provide: SessionStorage,\n          useFactory: mockSessionStorage,\n        },\n      ],\n    }).compile();\n\n    service = module.get(SingleUserSessionProvider);\n    sessionStorage = module.get(SessionStorage);\n  });\n\n  describe('getSession', () => {\n    it('Should get session by hardcoded id', async () => {\n      const result = await service.getSession();\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionStorage.getSession).toHaveBeenCalledWith(\n        DEFAULT_SESSION_ID,\n      );\n    });\n  });\n\n  describe('createSession', () => {\n    it('Should create new session by hardcoded id', async () => {\n      const result = await service.createSession({\n        ...mockInitSession,\n        id: 'should be overwritten for single user approach',\n      });\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionStorage.createSession).toHaveBeenCalledWith({\n        ...mockInitSession,\n        id: DEFAULT_SESSION_ID,\n      });\n    });\n  });\n\n  describe('updateSessionData', () => {\n    it('Should not affect existing data  by hardcoded id', async () => {\n      const result = await service.updateSessionData(\n        'any id will be overwritten',\n        {},\n      );\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionStorage.updateSessionData).toHaveBeenCalledWith(\n        DEFAULT_SESSION_ID,\n        {},\n      );\n    });\n  });\n\n  describe('deleteSession', () => {\n    it('should delete session by hardcoded id', async () => {\n      await service.deleteSession();\n      expect(sessionStorage.deleteSession).toHaveBeenCalledWith(\n        DEFAULT_SESSION_ID,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/providers/single-user.session.provider.ts",
    "content": "import { SessionProvider } from 'src/modules/session/providers/session.provider';\nimport { Session } from 'src/common/models';\nimport { DEFAULT_SESSION_ID } from 'src/common/constants';\nimport { Injectable } from '@nestjs/common';\nimport { plainToInstance } from 'class-transformer';\n\n@Injectable()\nexport class SingleUserSessionProvider extends SessionProvider {\n  async getSession(): Promise<Session> {\n    return this.sessionStorage.getSession(DEFAULT_SESSION_ID);\n  }\n\n  async createSession(session: Session): Promise<Session> {\n    return this.sessionStorage.createSession(\n      plainToInstance(Session, {\n        ...session,\n        id: DEFAULT_SESSION_ID,\n      }),\n    );\n  }\n\n  async updateSessionData(id: string, data: object) {\n    return this.sessionStorage.updateSessionData(DEFAULT_SESSION_ID, data);\n  }\n\n  async deleteSession(): Promise<void> {\n    return this.sessionStorage.deleteSession(DEFAULT_SESSION_ID);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/providers/storage/in-memory.session.storage.spec.ts",
    "content": "import { TestingModule, Test } from '@nestjs/testing';\nimport { mockInitSession, mockSessionCustomData } from 'src/__mocks__';\nimport { InMemorySessionStorage } from 'src/modules/session/providers/storage/in-memory.session.storage';\n\ndescribe('InMemorySessionStorage', () => {\n  let service: InMemorySessionStorage;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [InMemorySessionStorage],\n    }).compile();\n\n    service = module.get(InMemorySessionStorage);\n  });\n\n  describe('createSession', () => {\n    it('Should create new session if not exists', async () => {\n      expect(service['sessions'].size).toEqual(0);\n\n      const result = await service.createSession(mockInitSession);\n\n      expect(result).toEqual(mockInitSession);\n      expect(service['sessions'].size).toEqual(1);\n      expect(service['sessions'].get(mockInitSession.id)).toEqual(\n        mockInitSession,\n      );\n    });\n\n    it('Should create new session if not exists 2 times', async () => {\n      expect(service['sessions'].size).toEqual(0);\n\n      const session1 = await service.createSession(mockInitSession);\n      const session2 = await service.createSession({\n        ...mockInitSession,\n        id: '_2',\n      });\n\n      expect(service['sessions'].size).toEqual(2);\n      expect(session1).toEqual(mockInitSession);\n      expect(session2).toEqual({ ...mockInitSession, id: '_2' });\n      expect(service['sessions'].get(mockInitSession.id)).toEqual(\n        mockInitSession,\n      );\n      expect(service['sessions'].get('_2')).toEqual({\n        ...mockInitSession,\n        id: '_2',\n      });\n    });\n  });\n\n  describe('updateSessionData', () => {\n    it('Should not affect existing data', async () => {\n      service['sessions'] = new Map([[mockInitSession.id, mockInitSession]]);\n\n      const result = await service.updateSessionData(mockInitSession.id, {});\n\n      expect(result).toEqual(mockInitSession);\n    });\n    it('Should add custom data', async () => {\n      service['sessions'] = new Map([[mockInitSession.id, mockInitSession]]);\n\n      const result = await service.updateSessionData(\n        mockInitSession.id,\n        mockSessionCustomData,\n      );\n\n      expect(result).toEqual({\n        ...mockInitSession,\n        data: mockSessionCustomData,\n      });\n    });\n    it('Should return null if no session found and not affect existing sessions', async () => {\n      service['sessions'] = new Map([[mockInitSession.id, mockInitSession]]);\n\n      expect(service['sessions'].size).toEqual(1);\n      expect(await service.getSession(mockInitSession.id)).toEqual(\n        mockInitSession,\n      );\n\n      const result = await service.updateSessionData(\n        '_2',\n        mockSessionCustomData,\n      );\n\n      expect(result).toEqual(null);\n\n      expect(service['sessions'].size).toEqual(1);\n      expect(await service.getSession(mockInitSession.id)).toEqual(\n        mockInitSession,\n      );\n    });\n  });\n\n  describe('deleteSession + getSession', () => {\n    it('should delete session by id', async () => {\n      service['sessions'] = new Map([\n        [mockInitSession.id, mockInitSession],\n        ['_2', { ...mockInitSession, id: '_2' }],\n      ]);\n\n      expect(service['sessions'].size).toEqual(2);\n\n      expect(await service.getSession(mockInitSession.id)).toEqual(\n        mockInitSession,\n      );\n      expect(await service.getSession('_2')).toEqual({\n        ...mockInitSession,\n        id: '_2',\n      });\n      await service.deleteSession(mockInitSession.id);\n\n      expect(service['sessions'].size).toEqual(1);\n\n      expect(await service.getSession(mockInitSession.id)).toEqual(null);\n      expect(await service.getSession('_2')).toEqual({\n        ...mockInitSession,\n        id: '_2',\n      });\n\n      await service.deleteSession('_2');\n\n      expect(service['sessions'].size).toEqual(0);\n\n      expect(await service.getSession(mockInitSession.id)).toEqual(null);\n      expect(await service.getSession('_2')).toEqual(null);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/providers/storage/in-memory.session.storage.ts",
    "content": "import { Session } from 'src/common/models';\nimport { SessionStorage } from 'src/modules/session/providers/storage/session.storage';\n\nexport class InMemorySessionStorage extends SessionStorage {\n  private sessions: Map<string, Session> = new Map();\n\n  async getSession(id: string): Promise<Session> {\n    return this.sessions.get(id) || null;\n  }\n\n  async createSession(session: Session): Promise<Session> {\n    if (!this.sessions.has(session.id)) {\n      this.sessions.set(session.id, session);\n    }\n\n    return this.getSession(session.id);\n  }\n\n  async updateSessionData(id: string, data: object): Promise<Session> {\n    const session = this.sessions.get(id);\n\n    if (!session) {\n      return null;\n    }\n\n    session.data = data;\n\n    return this.getSession(id);\n  }\n\n  async deleteSession(id: string): Promise<void> {\n    this.sessions.delete(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/providers/storage/session.storage.ts",
    "content": "import { Session } from 'src/common/models';\n\nexport abstract class SessionStorage {\n  abstract getSession(id: string): Promise<Session>;\n  abstract createSession(session: Session, force?: boolean): Promise<Session>;\n  abstract updateSessionData(id: string, data: object): Promise<Session>;\n  abstract deleteSession(id: string): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/session.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { SessionService } from 'src/modules/session/session.service';\nimport { SessionProvider } from 'src/modules/session/providers/session.provider';\nimport { SingleUserSessionProvider } from 'src/modules/session/providers/single-user.session.provider';\nimport { SessionStorage } from 'src/modules/session/providers/storage/session.storage';\nimport { InMemorySessionStorage } from 'src/modules/session/providers/storage/in-memory.session.storage';\n\n@Module({})\nexport class SessionModule {\n  static async register(\n    sessionProvider: Type<SessionProvider> = SingleUserSessionProvider,\n    sessionStorage: Type<SessionStorage> = InMemorySessionStorage,\n  ) {\n    return {\n      module: SessionModule,\n      providers: [\n        SessionService,\n        {\n          provide: SessionProvider,\n          useClass: sessionProvider,\n        },\n        {\n          provide: SessionStorage,\n          useClass: sessionStorage,\n        },\n      ],\n      exports: [SessionService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/session.service.spec.ts",
    "content": "import { TestingModule, Test } from '@nestjs/testing';\nimport {\n  mockInitSession,\n  mockSessionCustomData,\n  mockSessionProvider,\n  MockType,\n} from 'src/__mocks__';\nimport { SessionService } from 'src/modules/session/session.service';\nimport { SessionProvider } from 'src/modules/session/providers/session.provider';\n\ndescribe('SessionService', () => {\n  let service: SessionService;\n  let sessionProvider: MockType<SessionProvider>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        SessionService,\n        {\n          provide: SessionProvider,\n          useFactory: mockSessionProvider,\n        },\n      ],\n    }).compile();\n\n    service = module.get(SessionService);\n    sessionProvider = module.get(SessionProvider);\n  });\n\n  describe('getSession', () => {\n    it('Should get session by id', async () => {\n      const result = await service.getSession(mockInitSession.id);\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionProvider.getSession).toHaveBeenCalledWith(\n        mockInitSession.id,\n      );\n    });\n  });\n\n  describe('createSession', () => {\n    it('Should create new session by id', async () => {\n      const result = await service.createSession(mockInitSession);\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionProvider.createSession).toHaveBeenCalledWith(\n        mockInitSession,\n      );\n    });\n  });\n\n  describe('updateSessionData', () => {\n    it('Should not affect existing data by id', async () => {\n      const result = await service.updateSessionData(mockInitSession.id, {});\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionProvider.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        {},\n      );\n    });\n    it('Should update data by id', async () => {\n      const result = await service.updateSessionData(\n        mockInitSession.id,\n        mockSessionCustomData,\n      );\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionProvider.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        mockSessionCustomData,\n      );\n    });\n    it('Should merge data by id', async () => {\n      sessionProvider.getSession.mockResolvedValueOnce({\n        data: {\n          some: 'data',\n          custom: 'string',\n        },\n      });\n\n      const result = await service.updateSessionData(\n        mockInitSession.id,\n        mockSessionCustomData,\n      );\n\n      expect(result).toEqual(mockInitSession);\n      expect(sessionProvider.updateSessionData).toHaveBeenCalledWith(\n        mockInitSession.id,\n        {\n          ...mockSessionCustomData,\n          some: 'data',\n        },\n      );\n    });\n    it('Should return null when session was not found', async () => {\n      sessionProvider.getSession.mockResolvedValueOnce(null);\n      const result = await service.updateSessionData(mockInitSession.id, {});\n\n      expect(result).toEqual(null);\n      expect(sessionProvider.updateSessionData).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('deleteSession', () => {\n    it('should delete session by id', async () => {\n      await service.deleteSession(mockInitSession.id);\n      expect(sessionProvider.deleteSession).toHaveBeenCalledWith(\n        mockInitSession.id,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/session/session.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Session } from 'src/common/models';\nimport { SessionProvider } from 'src/modules/session/providers/session.provider';\n\n@Injectable()\nexport class SessionService {\n  constructor(private readonly sessionProvider: SessionProvider) {}\n\n  async getSession(id: string): Promise<Session> {\n    return this.sessionProvider.getSession(id);\n  }\n\n  async createSession(session: Session): Promise<Session> {\n    return this.sessionProvider.createSession(session);\n  }\n\n  async updateSessionData(id: string, data: object): Promise<Session> {\n    const session = await this.getSession(id);\n\n    if (!session) {\n      return null;\n    }\n\n    return this.sessionProvider.updateSessionData(\n      id,\n      data\n        ? {\n            ...session.data,\n            ...data,\n          }\n        : {},\n    );\n  }\n\n  async deleteSession(id: string): Promise<void> {\n    return this.sessionProvider.deleteSession(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/constants/settings.ts",
    "content": "export type ToggleAnalyticsReasonType =\n  | 'none'\n  | 'oauth-agreement'\n  | 'google'\n  | 'github'\n  | 'sso'\n  | 'user';\n\nexport enum ToggleAnalyticsReason {\n  None = 'none',\n  OAuthAgreement = 'oauth-agreement',\n  Google = 'google',\n  Github = 'github',\n  Sso = 'sso',\n  User = 'user',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/dto/settings.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsBoolean,\n  IsEnum,\n  IsInstance,\n  IsInt,\n  IsOptional,\n  IsString,\n  Min,\n} from 'class-validator';\nimport { Exclude, Expose, Transform, Type } from 'class-transformer';\nimport { pickDefinedAgreements } from 'src/dto/dto-transformer';\nimport { Default } from 'src/common/decorators';\nimport config from 'src/utils/config';\nimport { IAgreementSpec } from 'src/modules/settings/models/agreements.interface';\nimport {\n  ToggleAnalyticsReason,\n  ToggleAnalyticsReasonType,\n} from 'src/modules/settings/constants/settings';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\nconst WORKBENCH_CONFIG = config.get('workbench');\n\nexport enum TimezoneOption {\n  Local = 'local',\n  UTC = 'UTC',\n}\n\nexport class GetAgreementsSpecResponse {\n  @ApiProperty({\n    description: 'Version of agreements specification.',\n    type: String,\n    example: '1.0.0',\n  })\n  version: string;\n\n  @ApiProperty({\n    description: 'Agreements specification.',\n    type: Object,\n    example: {\n      eula: {\n        defaultValue: false,\n        required: true,\n        since: '1.0.0',\n        editable: false,\n        title: 'License Terms',\n        label: 'I have read and understood the License Terms',\n      },\n    },\n  })\n  agreements: IAgreementSpec;\n}\n\nexport class GetUserAgreementsResponse {\n  @ApiProperty({\n    description: 'Last version on agreements set by the user.',\n    type: String,\n  })\n  version: string;\n\n  eula?: boolean;\n\n  analytics?: boolean;\n\n  notifications?: boolean;\n\n  @Exclude()\n  encryption?: boolean;\n}\n\nexport class GetAppSettingsResponse {\n  @ApiProperty({\n    description: 'Applied application theme.',\n    type: String,\n    example: 'DARK',\n  })\n  @Expose()\n  @Default(null)\n  theme: string = null;\n\n  @ApiProperty({\n    description: 'Applied application date format',\n    type: String,\n    example: 'yyyy-mm-dd',\n  })\n  @Expose()\n  @Default(null)\n  dateFormat: string = null;\n\n  @ApiProperty({\n    description: 'Applied application timezone',\n    enum: TimezoneOption,\n    example: 'local',\n  })\n  @Expose()\n  @Default(null)\n  timezone: TimezoneOption = null;\n\n  @ApiProperty({\n    description: 'Applied the threshold for scan operation.',\n    type: Number,\n    example: 10000,\n  })\n  @Expose()\n  @Default(REDIS_SCAN_CONFIG.scanThreshold)\n  scanThreshold: number = REDIS_SCAN_CONFIG.scanThreshold;\n\n  @ApiProperty({\n    description: 'Applied the batch of the commands for workbench.',\n    type: Number,\n    example: 5,\n  })\n  @Expose()\n  @Default(WORKBENCH_CONFIG.countBatch)\n  batchSize: number = WORKBENCH_CONFIG.countBatch;\n\n  @ApiProperty({\n    description:\n      'Flag indicating that terms and conditions are accepted via environment variable',\n    type: Boolean,\n    example: false,\n  })\n  @Expose()\n  @Default(false)\n  acceptTermsAndConditionsOverwritten: boolean = false;\n\n  @ApiProperty({\n    description: 'Agreements set by the user.',\n    type: GetUserAgreementsResponse,\n    example: {\n      version: '1.0.0',\n      eula: true,\n      analytics: true,\n      encryption: true,\n      notifications: true,\n    },\n  })\n  @Expose()\n  agreements: GetUserAgreementsResponse;\n}\n\nexport class UpdateSettingsDto {\n  @ApiPropertyOptional({\n    description: 'Application theme.',\n    type: String,\n    example: 'DARK',\n  })\n  @IsOptional()\n  @IsString()\n  theme?: string;\n\n  @ApiPropertyOptional({\n    description: 'Application date format.',\n    type: String,\n    example: 'yyyy-mm-dd',\n  })\n  @IsOptional()\n  @IsString()\n  dateFormat?: string;\n\n  @ApiPropertyOptional({\n    description: 'Application timezone.',\n    type: String,\n    example: 'local',\n  })\n  @IsOptional()\n  @IsString()\n  timezone?: string;\n\n  @ApiPropertyOptional({\n    description: 'Threshold for scan operation.',\n    type: Number,\n    example: 10000,\n  })\n  @IsOptional()\n  @IsInt({ always: true })\n  @Min(500)\n  scanThreshold?: number;\n\n  @ApiPropertyOptional({\n    description: 'Batch for workbench pipeline.',\n    type: Number,\n    example: 5,\n  })\n  @IsOptional()\n  @IsInt({ always: true })\n  @Min(0)\n  batchSize?: number;\n\n  @ApiPropertyOptional({\n    description: 'Agreements',\n    type: Map,\n    example: {\n      eula: true,\n    },\n  })\n  @IsOptional()\n  @Type(() => Map)\n  @IsInstance(Map)\n  @Transform(pickDefinedAgreements)\n  @IsBoolean({ each: true })\n  agreements?: Map<string, boolean>;\n\n  @ApiPropertyOptional({\n    description: 'Reason describing why analytics are enabled',\n    type: String,\n    example: 'install',\n  })\n  @IsOptional()\n  @IsString()\n  @IsEnum(ToggleAnalyticsReason)\n  analyticsReason?: ToggleAnalyticsReasonType;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/entities/agreements.entity.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('agreements')\nexport class AgreementsEntity {\n  @PrimaryGeneratedColumn()\n  @Expose()\n  id: number;\n\n  @ApiProperty({\n    description: 'Last accepted version.',\n    type: String,\n  })\n  @Column({ nullable: true })\n  @Expose()\n  version: string;\n\n  @ApiProperty({\n    description: 'User agreements.',\n    type: String,\n  })\n  @Column({ nullable: true })\n  @DataAsJsonString()\n  @Expose()\n  data: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/entities/settings.entity.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('settings')\nexport class SettingsEntity {\n  @PrimaryGeneratedColumn()\n  @Expose()\n  id: number;\n\n  @ApiProperty({\n    description: 'Applied settings by user.',\n    type: String,\n  })\n  @Column({ nullable: true })\n  @DataAsJsonString()\n  @Expose()\n  data: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/models/agreements.interface.ts",
    "content": "export interface IAgreement {\n  defaultValue?: boolean;\n  displayInSetting?: boolean;\n  required?: boolean;\n  category?: string;\n  since?: string;\n  editable?: boolean;\n  disabled?: boolean;\n  title?: string;\n  label?: string;\n  description?: string;\n  requiredText?: string;\n  options?: {\n    [key: string]: IAgreement;\n  };\n}\n\nexport interface IAgreementSpec {\n  [key: string]: IAgreement;\n}\n\nexport interface IAgreementSpecFile {\n  version: string;\n  agreements: IAgreementSpec;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/models/agreements.ts",
    "content": "import { Expose } from 'class-transformer';\n\nexport class Agreements {\n  @Expose()\n  id: string | number;\n\n  @Expose()\n  version: string;\n\n  @Expose()\n  data: Record<string, boolean>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/models/settings.ts",
    "content": "import { Expose } from 'class-transformer';\n\nexport class Settings {\n  @Expose()\n  id: string;\n\n  @Expose()\n  data: Record<string, number | string | boolean>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/repositories/agreements.repository.ts",
    "content": "import { Agreements } from 'src/modules/settings/models/agreements';\nimport { SessionMetadata } from 'src/common/models';\n\nexport interface DefaultAgreementsOptions {\n  version?: string;\n  data?: Record<string, boolean>;\n}\n\nexport abstract class AgreementsRepository {\n  abstract getOrCreate(\n    sessionMetadata: SessionMetadata,\n    defaultOptions?: DefaultAgreementsOptions,\n  ): Promise<Agreements>;\n  abstract update(\n    sessionMetadata: SessionMetadata,\n    agreements: Agreements,\n  ): Promise<Agreements>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/repositories/local.agreements.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockAgreements,\n  mockAgreementsEntity,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n  mockUserId,\n} from 'src/__mocks__';\nimport { AgreementsEntity } from 'src/modules/settings/entities/agreements.entity';\nimport { LocalAgreementsRepository } from 'src/modules/settings/repositories/local.agreements.repository';\n\ndescribe('LocalAgreementsRepository', () => {\n  let service: LocalAgreementsRepository;\n  let repository: MockType<Repository<AgreementsEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalAgreementsRepository,\n        {\n          provide: getRepositoryToken(AgreementsEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(AgreementsEntity));\n    service = await module.get(LocalAgreementsRepository);\n\n    repository.findOneBy.mockResolvedValue(mockAgreementsEntity);\n    repository.update.mockResolvedValue(true); // no meter of response\n    repository.save.mockResolvedValue(\n      Object.assign(new AgreementsEntity(), { id: mockUserId }),\n    );\n    repository.create.mockReturnValue(new AgreementsEntity());\n  });\n\n  describe('getOrCreate', () => {\n    it('should return agreements', async () => {\n      const result = await service.getOrCreate(mockSessionMetadata);\n\n      expect(result).toEqual(mockAgreements);\n    });\n    it('should create new agreements', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.getOrCreate(mockSessionMetadata);\n\n      expect(result).toEqual({\n        ...mockAgreements,\n        version: undefined,\n        data: undefined,\n      });\n    });\n    it('should create new agreements when entity exists but has no data', async () => {\n      // Mock an entity that exists but has no data property\n      const entityWithoutData = Object.assign(new AgreementsEntity(), {\n        id: mockUserId,\n        version: '1.0.0',\n        data: undefined, // This should trigger the !entity?.data check\n      });\n\n      repository.findOneBy.mockResolvedValueOnce(entityWithoutData);\n\n      const result = await service.getOrCreate(mockSessionMetadata);\n\n      // Verify that save was called to create a new entity\n      expect(repository.save).toHaveBeenCalledWith({\n        id: 1,\n        data: undefined,\n      });\n\n      expect(result).toEqual({\n        ...mockAgreements,\n        version: undefined,\n        data: undefined,\n      });\n    });\n    it('should create new agreements when entity exists but has empty string data', async () => {\n      // Mock an entity that exists but has empty string data\n      const entityWithEmptyData = Object.assign(new AgreementsEntity(), {\n        id: mockUserId,\n        version: '1.0.0',\n        data: '', // This should also trigger the !entity?.data check\n      });\n\n      repository.findOneBy.mockResolvedValueOnce(entityWithEmptyData);\n\n      const result = await service.getOrCreate(mockSessionMetadata);\n\n      // Verify that save was called to create a new entity\n      expect(repository.save).toHaveBeenCalledWith({\n        id: 1,\n        data: undefined,\n      });\n\n      expect(result).toEqual({\n        ...mockAgreements,\n        version: undefined,\n        data: undefined,\n      });\n    });\n    it('should fail to create with unique constraint and return existing', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.findOneBy.mockResolvedValueOnce(mockAgreements);\n      repository.save.mockRejectedValueOnce({ code: 'SQLITE_CONSTRAINT' });\n\n      const result = await service.getOrCreate(mockSessionMetadata);\n\n      expect(result).toEqual(mockAgreements);\n    });\n    it('should fail when failed to create new and error is not unique constraint', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.save.mockRejectedValueOnce(new Error());\n\n      await expect(service.getOrCreate(mockSessionMetadata)).rejects.toThrow(\n        Error,\n      );\n    });\n    it('should create new agreements with default data when provided and no entity exists', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      const defaultData = { eula: true, analytics: false };\n\n      await service.getOrCreate(mockSessionMetadata, { data: defaultData });\n\n      expect(repository.save).toHaveBeenCalledWith({\n        id: 1,\n        data: JSON.stringify(defaultData),\n      });\n      expect(repository.save).toHaveBeenCalled();\n    });\n  });\n\n  describe('update', () => {\n    it('should update agreements', async () => {\n      const result = await service.update(mockSessionMetadata, mockAgreements);\n\n      expect(result).toEqual(mockAgreements);\n      expect(repository.save).toHaveBeenCalledWith({\n        ...mockAgreementsEntity,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/repositories/local.agreements.repository.ts",
    "content": "import { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { classToClass } from 'src/utils';\nimport {\n  AgreementsRepository,\n  DefaultAgreementsOptions,\n} from 'src/modules/settings/repositories/agreements.repository';\nimport { AgreementsEntity } from 'src/modules/settings/entities/agreements.entity';\nimport { Agreements } from 'src/modules/settings/models/agreements';\nimport { SessionMetadata } from 'src/common/models';\nimport { plainToInstance } from 'class-transformer';\n\nexport class LocalAgreementsRepository extends AgreementsRepository {\n  constructor(\n    @InjectRepository(AgreementsEntity)\n    private readonly repository: Repository<AgreementsEntity>,\n  ) {\n    super();\n  }\n\n  async getOrCreate(\n    sessionMetadata: SessionMetadata,\n    defaultOptions: DefaultAgreementsOptions = {},\n  ): Promise<Agreements> {\n    let entity = await this.repository.findOneBy({});\n    if (!entity?.data) {\n      try {\n        entity = await this.repository.save(\n          classToClass(\n            AgreementsEntity,\n            plainToInstance(Agreements, {\n              ...defaultOptions,\n              id: 1,\n            }),\n          ),\n        );\n      } catch (e) {\n        if (e.code === 'SQLITE_CONSTRAINT') {\n          return this.getOrCreate(sessionMetadata, defaultOptions);\n        }\n\n        throw e;\n      }\n    }\n\n    return classToClass(Agreements, entity);\n  }\n\n  async update(\n    sessionMetadata: SessionMetadata,\n    agreements: Agreements,\n  ): Promise<Agreements> {\n    const entity = classToClass(AgreementsEntity, agreements);\n\n    await this.repository.save(entity);\n\n    return this.getOrCreate(sessionMetadata);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/repositories/local.settings.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport {\n  mockRepository,\n  mockSessionMetadata,\n  mockSettings,\n  mockSettingsEntity,\n  MockType,\n  mockUserId,\n} from 'src/__mocks__';\nimport { LocalSettingsRepository } from 'src/modules/settings/repositories/local.settings.repository';\nimport { SettingsEntity } from 'src/modules/settings/entities/settings.entity';\n\ndescribe('LocalSettingsRepository', () => {\n  let service: LocalSettingsRepository;\n  let repository: MockType<Repository<SettingsEntity>>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalSettingsRepository,\n        {\n          provide: getRepositoryToken(SettingsEntity),\n          useFactory: mockRepository,\n        },\n      ],\n    }).compile();\n\n    repository = await module.get(getRepositoryToken(SettingsEntity));\n    service = await module.get(LocalSettingsRepository);\n\n    repository.findOneBy.mockResolvedValue(mockSettingsEntity);\n    repository.update.mockResolvedValue(true); // no meter of response\n    repository.save.mockResolvedValue(\n      Object.assign(new SettingsEntity(), { id: mockUserId }),\n    );\n    repository.create.mockReturnValue(new SettingsEntity());\n  });\n\n  describe('getOrCreate', () => {\n    it('should return settings model', async () => {\n      const result = await service.getOrCreate();\n\n      expect(result).toEqual(mockSettings);\n    });\n    it('should create new settings', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      const result = await service.getOrCreate();\n\n      expect(result).toEqual({\n        ...mockSettings,\n        data: undefined,\n      });\n    });\n    it('should fail to create with unique constraint and return existing', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.findOneBy.mockResolvedValueOnce(mockSettings);\n      repository.save.mockRejectedValueOnce({ code: 'SQLITE_CONSTRAINT' });\n\n      const result = await service.getOrCreate();\n\n      expect(result).toEqual(mockSettings);\n    });\n    it('should fail when failed to create new and error is not unique constraint', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n      repository.save.mockRejectedValueOnce(new Error());\n\n      await expect(service.getOrCreate()).rejects.toThrow(Error);\n    });\n  });\n\n  describe('update', () => {\n    it('should update settings', async () => {\n      const result = await service.update(mockSessionMetadata, mockSettings);\n\n      expect(result).toEqual(mockSettings);\n      expect(repository.save).toHaveBeenCalledWith(mockSettingsEntity);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/repositories/local.settings.repository.ts",
    "content": "import { SettingsRepository } from 'src/modules/settings/repositories/settings.repository';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { SettingsEntity } from 'src/modules/settings/entities/settings.entity';\nimport { Settings } from 'src/modules/settings/models/settings';\nimport { classToClass } from 'src/utils';\nimport { SessionMetadata } from 'src/common/models';\n\nexport class LocalSettingsRepository extends SettingsRepository {\n  constructor(\n    @InjectRepository(SettingsEntity)\n    private readonly repository: Repository<SettingsEntity>,\n  ) {\n    super();\n  }\n\n  async getOrCreate(): Promise<Settings> {\n    let entity = await this.repository.findOneBy({});\n\n    if (!entity) {\n      try {\n        entity = await this.repository.save(this.repository.create({ id: 1 }));\n      } catch (e) {\n        if (e.code === 'SQLITE_CONSTRAINT') {\n          return this.getOrCreate();\n        }\n\n        throw e;\n      }\n    }\n\n    return classToClass(Settings, entity);\n  }\n\n  async update(_: SessionMetadata, settings: Settings): Promise<Settings> {\n    const entity = classToClass(SettingsEntity, settings);\n\n    await this.repository.save(entity);\n\n    return this.getOrCreate();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/repositories/settings.repository.ts",
    "content": "import { Settings } from 'src/modules/settings/models/settings';\nimport { SessionMetadata } from 'src/common/models';\n\nexport abstract class SettingsRepository {\n  abstract getOrCreate(sessionMetadata: SessionMetadata): Promise<Settings>;\n  abstract update(\n    sessionMetadata: SessionMetadata,\n    settings: Settings,\n  ): Promise<Settings>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/settings.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { AppAnalyticsEvents, TelemetryEvents } from 'src/constants';\nimport { SettingsAnalytics } from 'src/modules/settings/settings.analytics';\nimport { GetAppSettingsResponse } from 'src/modules/settings/dto/settings.dto';\nimport { mockSessionMetadata } from 'src/__mocks__';\n\ndescribe('SettingsAnalytics', () => {\n  let service: SettingsAnalytics;\n  let eventEmitter: EventEmitter2;\n  let sendEventMethod;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [EventEmitter2, SettingsAnalytics],\n    }).compile();\n\n    service = module.get<SettingsAnalytics>(SettingsAnalytics);\n    eventEmitter = module.get<EventEmitter2>(EventEmitter2);\n    eventEmitter.emit = jest.fn();\n    sendEventMethod = jest.spyOn<SettingsAnalytics, any>(service, 'sendEvent');\n  });\n\n  describe('sendAnalyticsAgreementChange', () => {\n    it('should emit ANALYTICS_PERMISSION with state enabled on first app launch', async () => {\n      service.sendAnalyticsAgreementChange(\n        mockSessionMetadata,\n        new Map([['analytics', true]]),\n        undefined,\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: { state: 'enabled' },\n          nonTracking: true,\n        },\n      );\n    });\n    it('should emit ANALYTICS_PERMISSION with state enabled and reason undefined', async () => {\n      service.sendAnalyticsAgreementChange(\n        mockSessionMetadata,\n        new Map([['analytics', true]]),\n        undefined,\n        undefined,\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: { reason: undefined, state: 'enabled' },\n          nonTracking: true,\n        },\n      );\n    });\n    it('should emit ANALYTICS_PERMISSION with state enabled and reason \"sso\"', async () => {\n      service.sendAnalyticsAgreementChange(\n        mockSessionMetadata,\n        new Map([['analytics', true]]),\n        undefined,\n        'sso',\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: { reason: 'sso', state: 'enabled' },\n          nonTracking: true,\n        },\n      );\n    });\n    it('should emit ANALYTICS_PERMISSION with state disabled on first app launch', async () => {\n      service.sendAnalyticsAgreementChange(\n        mockSessionMetadata,\n        new Map([['analytics', false]]),\n        undefined,\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: { state: 'disabled' },\n          nonTracking: true,\n        },\n      );\n    });\n    it('should not emit ANALYTICS_PERMISSION if agreement did not changed', async () => {\n      service.sendAnalyticsAgreementChange(\n        mockSessionMetadata,\n        new Map([['analytics', false]]),\n        new Map([['analytics', false]]),\n        'none',\n      );\n\n      expect(eventEmitter.emit).not.toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: expect.anything(),\n          nonTracking: true,\n        },\n      );\n    });\n    it('should emit [ANALYTICS_PERMISSION] if agreement changed', async () => {\n      service.sendAnalyticsAgreementChange(\n        mockSessionMetadata,\n        new Map([['analytics', false]]),\n        new Map([['analytics', true]]),\n        'none',\n      );\n\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        AppAnalyticsEvents.Track,\n        mockSessionMetadata,\n        {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: { state: 'disabled', reason: 'none' },\n          nonTracking: true,\n        },\n      );\n    });\n  });\n\n  describe('sendSettingsUpdatedEvent', () => {\n    const defaultSettings: GetAppSettingsResponse = {\n      acceptTermsAndConditionsOverwritten: false,\n      agreements: null,\n      scanThreshold: 10000,\n      batchSize: 5,\n      dateFormat: null,\n      timezone: null,\n      theme: null,\n    };\n    it('should emit [SETTINGS_KEYS_TO_SCAN_CHANGED] event', async () => {\n      service.sendSettingsUpdatedEvent(\n        mockSessionMetadata,\n        { ...defaultSettings, scanThreshold: 100000 },\n        { ...defaultSettings, scanThreshold: 10000 },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SettingsScanThresholdChanged,\n        {\n          currentValue: 100000,\n          currentValueRange: '50 001 - 100 000',\n          previousValue: 10000,\n          previousValueRange: '5 001 - 10 000',\n        },\n      );\n    });\n    it('should not emit [SETTINGS_KEYS_TO_SCAN_CHANGED] for the same value', async () => {\n      service.sendSettingsUpdatedEvent(\n        mockSessionMetadata,\n        { ...defaultSettings, scanThreshold: 10000 },\n        { ...defaultSettings, scanThreshold: 10000 },\n      );\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n    it('should not emit [SETTINGS_WORKBENCH_PIPELINE_CHANGED] for the same value', async () => {\n      service.sendSettingsUpdatedEvent(\n        mockSessionMetadata,\n        { ...defaultSettings, batchSize: 5 },\n        { ...defaultSettings, batchSize: 5 },\n      );\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n    it('should emit [SETTINGS_WORKBENCH_PIPELINE_CHANGED] event', async () => {\n      service.sendSettingsUpdatedEvent(\n        mockSessionMetadata,\n        { ...defaultSettings, batchSize: 5 },\n        { ...defaultSettings, batchSize: 10 },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SettingsWorkbenchPipelineChanged,\n        {\n          newValue: true,\n          newValueSize: 5,\n          currentValue: true,\n          currentValueSize: 10,\n        },\n      );\n    });\n    it('should not emit event on error', async () => {\n      service.sendSettingsUpdatedEvent(\n        mockSessionMetadata,\n        { ...defaultSettings, scanThreshold: 10000 },\n        undefined,\n      );\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/settings.analytics.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { differenceWith, has, isEqual } from 'lodash';\nimport { AppAnalyticsEvents, TelemetryEvents } from 'src/constants';\nimport {\n  getIsPipelineEnable,\n  getRangeForNumber,\n  SCAN_THRESHOLD_BREAKPOINTS,\n} from 'src/utils';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { GetAppSettingsResponse } from 'src/modules/settings/dto/settings.dto';\nimport { SessionMetadata } from 'src/common/models';\nimport { ToggleAnalyticsReasonType } from 'src/modules/settings/constants/settings';\n\n@Injectable()\nexport class SettingsAnalytics extends TelemetryBaseService {\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n  }\n\n  sendSettingsUpdatedEvent(\n    sessionMetadata: SessionMetadata,\n    newSettings: GetAppSettingsResponse,\n    oldSettings: GetAppSettingsResponse,\n  ): void {\n    try {\n      const dif = Object.fromEntries(\n        differenceWith(\n          Object.entries(newSettings),\n          Object.entries(oldSettings),\n          isEqual,\n        ),\n      );\n      if (has(dif, 'scanThreshold')) {\n        this.sendScanThresholdChanged(\n          sessionMetadata,\n          dif.scanThreshold,\n          oldSettings.scanThreshold,\n        );\n      }\n      if (has(dif, 'batchSize')) {\n        this.sendWorkbenchPipelineChanged(\n          sessionMetadata,\n          dif.batchSize,\n          oldSettings.batchSize,\n        );\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  // Detect that analytics agreement was first established or changed\n  sendAnalyticsAgreementChange(\n    sessionMetadata: SessionMetadata,\n    newAgreements: Map<string, boolean>,\n    oldAgreements: Map<string, boolean> = new Map(),\n    reason?: ToggleAnalyticsReasonType,\n  ) {\n    try {\n      const newPermission = newAgreements.get('analytics');\n      const oldPermission = oldAgreements.get('analytics');\n      if (oldPermission !== newPermission) {\n        this.eventEmitter.emit(AppAnalyticsEvents.Track, sessionMetadata, {\n          event: TelemetryEvents.AnalyticsPermission,\n          eventData: {\n            state: newPermission ? 'enabled' : 'disabled',\n            reason,\n          },\n          nonTracking: true,\n        });\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  private sendScanThresholdChanged(\n    sessionMetadata: SessionMetadata,\n    currentValue: number,\n    previousValue: number,\n  ): void {\n    this.sendEvent(\n      sessionMetadata,\n      TelemetryEvents.SettingsScanThresholdChanged,\n      {\n        currentValue,\n        currentValueRange: getRangeForNumber(\n          currentValue,\n          SCAN_THRESHOLD_BREAKPOINTS,\n        ),\n        previousValue,\n        previousValueRange: getRangeForNumber(\n          previousValue,\n          SCAN_THRESHOLD_BREAKPOINTS,\n        ),\n      },\n    );\n  }\n\n  private sendWorkbenchPipelineChanged(\n    sessionMetadata: SessionMetadata,\n    newValue: number,\n    currentValue: number,\n  ): void {\n    this.sendEvent(\n      sessionMetadata,\n      TelemetryEvents.SettingsWorkbenchPipelineChanged,\n      {\n        newValue: getIsPipelineEnable(newValue),\n        newValueSize: newValue,\n        currentValue: getIsPipelineEnable(currentValue),\n        currentValueSize: currentValue,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/settings.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Get,\n  Patch,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { RequestSessionMetadata } from 'src/common/decorators';\nimport {\n  GetAgreementsSpecResponse,\n  GetAppSettingsResponse,\n  UpdateSettingsDto,\n} from './dto/settings.dto';\n\n@ApiTags('Settings')\n@Controller('settings')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class SettingsController {\n  constructor(private settingsService: SettingsService) {}\n\n  @Get('')\n  @ApiEndpoint({\n    description: 'Get info about application settings',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Application settings',\n        type: GetAppSettingsResponse,\n      },\n    ],\n  })\n  async getAppSettings(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n  ): Promise<GetAppSettingsResponse> {\n    return this.settingsService.getAppSettings(sessionMetadata);\n  }\n\n  @Get('/agreements/spec')\n  @ApiEndpoint({\n    description: 'Get json with agreements specification',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Agreements specification',\n        type: GetAgreementsSpecResponse,\n      },\n    ],\n  })\n  async getAgreementsSpec(): Promise<GetAgreementsSpecResponse> {\n    return this.settingsService.getAgreementsSpec();\n  }\n\n  @Patch('')\n  @ApiEndpoint({\n    description: 'Update user application settings and agreements',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Application settings',\n        type: GetAppSettingsResponse,\n      },\n    ],\n  })\n  async update(\n    @RequestSessionMetadata() sessionMetadata: SessionMetadata,\n    @Body() dto: UpdateSettingsDto,\n  ): Promise<GetAppSettingsResponse> {\n    return this.settingsService.updateAppSettings(sessionMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/settings.module.ts",
    "content": "import { Module, Type } from '@nestjs/common';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { SettingsController } from 'src/modules/settings/settings.controller';\nimport { SettingsAnalytics } from 'src/modules/settings/settings.analytics';\nimport { SettingsRepository } from 'src/modules/settings/repositories/settings.repository';\nimport { LocalSettingsRepository } from 'src/modules/settings/repositories/local.settings.repository';\nimport { AgreementsRepository } from 'src/modules/settings/repositories/agreements.repository';\nimport { LocalAgreementsRepository } from 'src/modules/settings/repositories/local.agreements.repository';\n\n@Module({})\nexport class SettingsModule {\n  static register(\n    settingsRepository: Type<SettingsRepository> = LocalSettingsRepository,\n    agreementsRepository: Type<AgreementsRepository> = LocalAgreementsRepository,\n  ) {\n    return {\n      module: SettingsModule,\n      controllers: [SettingsController],\n      providers: [\n        SettingsService,\n        SettingsAnalytics,\n        {\n          provide: SettingsRepository,\n          useClass: settingsRepository,\n        },\n        {\n          provide: AgreementsRepository,\n          useClass: agreementsRepository,\n        },\n      ],\n      exports: [SettingsService],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/settings.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { InternalServerErrorException } from '@nestjs/common';\nimport {\n  mockAgreements,\n  mockAgreementsRepository,\n  mockAppSettings,\n  mockDatabaseDiscoveryService,\n  mockEncryptionService,\n  mockEncryptionStrategyInstance,\n  mockKeyEncryptionStrategyInstance,\n  mockSessionMetadata,\n  mockSettings,\n  mockSettingsAnalyticsService,\n  mockSettingsRepository,\n  MockType,\n} from 'src/__mocks__';\nimport { UpdateSettingsDto } from 'src/modules/settings/dto/settings.dto';\nimport * as AGREEMENTS_SPEC from 'src/constants/agreements-spec.json';\nimport { AgreementIsNotDefinedException } from 'src/constants';\nimport config from 'src/utils/config';\nimport { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';\nimport { SettingsAnalytics } from 'src/modules/settings/settings.analytics';\nimport { SettingsService } from 'src/modules/settings/settings.service';\nimport { AgreementsRepository } from 'src/modules/settings/repositories/agreements.repository';\nimport { SettingsRepository } from 'src/modules/settings/repositories/settings.repository';\nimport { Agreements } from 'src/modules/settings/models/agreements';\nimport { Settings } from 'src/modules/settings/models/settings';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { FeatureServerEvents } from 'src/modules/feature/constants';\nimport { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';\nimport { DatabaseDiscoveryService } from 'src/modules/database-discovery/database-discovery.service';\nimport { ToggleAnalyticsReason } from 'src/modules/settings/constants/settings';\nimport { classToClass } from 'src/utils';\nimport { GetAppSettingsResponse } from 'src/modules/settings/dto/settings.dto';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\n\nconst REDIS_SCAN_CONFIG = config.get('redis_scan');\nconst WORKBENCH_CONFIG = config.get('workbench');\n\nconst mockAgreementsMap = new Map(\n  Object.keys(AGREEMENTS_SPEC.agreements).map((item: string) => [item, true]),\n);\n\ndescribe('SettingsService', () => {\n  let service: SettingsService;\n  let agreementsRepository: MockType<AgreementsRepository>;\n  let databaseDiscoveryService: MockType<DatabaseDiscoveryService>;\n  let settingsRepository: MockType<SettingsRepository>;\n  let analyticsService: SettingsAnalytics;\n  let keytarStrategy: MockType<KeytarEncryptionStrategy>;\n  let eventEmitter: EventEmitter2;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        SettingsService,\n        {\n          provide: DatabaseDiscoveryService,\n          useFactory: mockDatabaseDiscoveryService,\n        },\n        {\n          provide: SettingsAnalytics,\n          useFactory: mockSettingsAnalyticsService,\n        },\n        {\n          provide: AgreementsRepository,\n          useFactory: mockAgreementsRepository,\n        },\n        {\n          provide: SettingsRepository,\n          useFactory: mockSettingsRepository,\n        },\n        {\n          provide: KeytarEncryptionStrategy,\n          useFactory: mockEncryptionStrategyInstance,\n        },\n        {\n          provide: KeyEncryptionStrategy,\n          useFactory: mockKeyEncryptionStrategyInstance,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n        {\n          provide: EventEmitter2,\n          useFactory: () => ({\n            emit: jest.fn(),\n          }),\n        },\n      ],\n    }).compile();\n\n    agreementsRepository = module.get(AgreementsRepository);\n    databaseDiscoveryService = module.get(DatabaseDiscoveryService);\n    settingsRepository = module.get(SettingsRepository);\n    keytarStrategy = module.get(KeytarEncryptionStrategy);\n    analyticsService = module.get(SettingsAnalytics);\n    service = module.get(SettingsService);\n    eventEmitter = module.get(EventEmitter2);\n  });\n\n  describe('getAppSettings', () => {\n    it('should return default application settings', async () => {\n      agreementsRepository.getOrCreate.mockResolvedValue(new Agreements());\n      settingsRepository.getOrCreate.mockResolvedValue(new Settings());\n\n      const result = await service.getAppSettings(mockSessionMetadata);\n\n      expect(result).toEqual({\n        theme: null,\n        scanThreshold: REDIS_SCAN_CONFIG.scanThreshold,\n        batchSize: WORKBENCH_CONFIG.countBatch,\n        dateFormat: null,\n        timezone: null,\n        agreements: null,\n        acceptTermsAndConditionsOverwritten: false,\n      });\n\n      expect(eventEmitter.emit).not.toHaveBeenCalled();\n    });\n\n    it('should return some application settings already defined by user', async () => {\n      agreementsRepository.getOrCreate.mockResolvedValue(mockAgreements);\n      settingsRepository.getOrCreate.mockResolvedValue(mockSettings);\n\n      const result = await service.getAppSettings(mockSessionMetadata);\n\n      expect(result).toEqual({\n        ...mockSettings.data,\n        acceptTermsAndConditionsOverwritten: false,\n        agreements: {\n          version: mockAgreements.version,\n          ...mockAgreements.data,\n        },\n      });\n    });\n\n    it('should verify expected pre-accepted agreements format', async () => {\n      const preselectedAgreements = {\n        analytics: false,\n        encryption: true,\n        eula: true,\n        notifications: false,\n        acceptTermsAndConditionsOverwritten: true,\n      };\n      settingsRepository.getOrCreate.mockResolvedValue(mockSettings);\n\n      // Create a custom instance of the service with an override method\n      const customService = {\n        // Preserve the same data structure expected from the method\n        getAppSettings: async () =>\n          classToClass(GetAppSettingsResponse, {\n            ...mockSettings.data,\n            agreements: preselectedAgreements,\n          }),\n      };\n\n      // Call the customized method\n      const result = await customService.getAppSettings();\n\n      // Verify the result matches the expected format when acceptTermsAndConditions is true\n      expect(result).toHaveProperty('agreements');\n      expect(result.agreements).toEqual(preselectedAgreements);\n    });\n\n    it('should throw InternalServerError', async () => {\n      agreementsRepository.getOrCreate.mockRejectedValue(\n        new Error('some error'),\n      );\n\n      try {\n        await service.getAppSettings(mockSessionMetadata);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n\n  describe('updateAppSettings', () => {\n    beforeEach(() => {\n      settingsRepository.getOrCreate.mockResolvedValue(mockSettings);\n      settingsRepository.update.mockResolvedValue(mockSettings);\n      agreementsRepository.getOrCreate.mockResolvedValue(mockAgreements);\n      agreementsRepository.update.mockResolvedValue(mockAgreements);\n    });\n    it('should run database discovery when accept eula for the very first time', async () => {\n      agreementsRepository.getOrCreate.mockResolvedValueOnce(null);\n\n      const dto: UpdateSettingsDto = {\n        ...mockSettings.data,\n        agreements: new Map(\n          Object.entries({\n            ...mockAgreements.data,\n          }),\n        ),\n      };\n\n      await service.updateAppSettings(mockSessionMetadata, dto);\n\n      // first run (user accepted eula) so should run database discovery\n      expect(databaseDiscoveryService.discover).toHaveBeenCalled();\n    });\n    it('should not fail when database discovery throw an error', async () => {\n      agreementsRepository.getOrCreate.mockResolvedValueOnce(null);\n      databaseDiscoveryService.discover.mockRejectedValueOnce(\n        new Error('some error'),\n      );\n\n      const dto: UpdateSettingsDto = {\n        ...mockSettings.data,\n        agreements: new Map(\n          Object.entries({\n            ...mockAgreements.data,\n          }),\n        ),\n      };\n\n      await service.updateAppSettings(mockSessionMetadata, dto);\n\n      // first run (user accepted eula) so should run database discovery\n      expect(databaseDiscoveryService.discover).toHaveBeenCalled();\n    });\n    it('should update settings only', async () => {\n      const dto: UpdateSettingsDto = {\n        scanThreshold: 1001,\n      };\n\n      const response = await service.updateAppSettings(\n        mockSessionMetadata,\n        dto,\n      );\n      expect(agreementsRepository.update).not.toHaveBeenCalled();\n      expect(settingsRepository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockSettings,\n          data: {\n            ...mockSettings.data,\n            ...dto,\n          },\n        },\n      );\n      expect(response).toEqual(mockAppSettings);\n      expect(eventEmitter.emit).toHaveBeenCalledWith(\n        FeatureServerEvents.FeaturesRecalculate,\n      );\n\n      // not first run so shouldn't run database discovery\n      expect(databaseDiscoveryService.discover).not.toHaveBeenCalled();\n    });\n    it('should update agreements only', async () => {\n      const dto: UpdateSettingsDto = {\n        agreements: new Map(\n          Object.entries({\n            analytics: false,\n          }),\n        ),\n        analyticsReason: ToggleAnalyticsReason.Github,\n      };\n\n      const response = await service.updateAppSettings(\n        mockSessionMetadata,\n        dto,\n      );\n      expect(settingsRepository.update).not.toHaveBeenCalled();\n      expect(agreementsRepository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockAgreements,\n          version: AGREEMENTS_SPEC.version,\n          data: {\n            ...mockAgreements.data,\n            analytics: false,\n          },\n        },\n      );\n      expect(response).toEqual(mockAppSettings);\n      expect(\n        analyticsService.sendAnalyticsAgreementChange,\n      ).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        new Map(\n          Object.entries({\n            analytics: false,\n          }),\n        ),\n        new Map(\n          Object.entries({\n            ...mockAgreements.data,\n          }),\n        ),\n        'github',\n      );\n    });\n    it('should update agreements and settings', async () => {\n      settingsRepository.getOrCreate.mockResolvedValueOnce({\n        ...mockSettings,\n        data: null,\n      });\n      settingsRepository.getOrCreate.mockResolvedValueOnce({\n        ...mockSettings,\n        data: null,\n      });\n      agreementsRepository.getOrCreate.mockResolvedValue(mockAgreements);\n\n      const dto: UpdateSettingsDto = {\n        batchSize: 6,\n        dateFormat: 'hh-mmm-ss',\n        timezone: 'UTC',\n        agreements: new Map(\n          Object.entries({\n            notifications: false,\n          }),\n        ),\n      };\n\n      const response = await service.updateAppSettings(\n        mockSessionMetadata,\n        dto,\n      );\n      expect(settingsRepository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockSettings,\n          data: {\n            batchSize: 6,\n            dateFormat: 'hh-mmm-ss',\n            timezone: 'UTC',\n          },\n        },\n      );\n      expect(agreementsRepository.update).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        {\n          ...mockAgreements,\n          version: AGREEMENTS_SPEC.version,\n          data: {\n            ...mockAgreements.data,\n            notifications: false,\n          },\n        },\n      );\n      expect(response).toEqual(mockAppSettings);\n      expect(\n        analyticsService.sendAnalyticsAgreementChange,\n      ).not.toHaveBeenCalled();\n      expect(analyticsService.sendSettingsUpdatedEvent).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        mockAppSettings,\n        {\n          ...mockAppSettings,\n          scanThreshold: REDIS_SCAN_CONFIG.scanThreshold,\n          batchSize: WORKBENCH_CONFIG.countBatch,\n          theme: null,\n        },\n      );\n    });\n    it('should throw AgreementIsNotDefinedException', async () => {\n      agreementsRepository.getOrCreate.mockResolvedValue({\n        ...mockAgreements,\n        data: null,\n      });\n\n      try {\n        await service.updateAppSettings(mockSessionMetadata, {\n          agreements: new Map([]),\n        });\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(AgreementIsNotDefinedException);\n      }\n    });\n    it('should throw InternalServerError', async () => {\n      agreementsRepository.getOrCreate.mockRejectedValue(\n        new Error('some error'),\n      );\n\n      const dto: UpdateSettingsDto = {\n        agreements: mockAgreementsMap,\n      };\n\n      try {\n        await service.updateAppSettings(mockSessionMetadata, dto);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n\n  describe('getAgreementsSpec', () => {\n    it('should get agreements spec', async () => {\n      keytarStrategy.isAvailable.mockResolvedValue(true);\n\n      const response = await service.getAgreementsSpec();\n      expect(response).toEqual({\n        ...AGREEMENTS_SPEC,\n        agreements: {\n          ...AGREEMENTS_SPEC.agreements,\n          encryption: AGREEMENTS_SPEC.agreements.encryption.options.true,\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/settings/settings.service.ts",
    "content": "import {\n  BadRequestException,\n  forwardRef,\n  Inject,\n  Injectable,\n  InternalServerErrorException,\n  Logger,\n} from '@nestjs/common';\nimport { difference, isEmpty, map, cloneDeep } from 'lodash';\nimport { readFile } from 'fs-extra';\nimport { join } from 'path';\nimport * as AGREEMENTS_SPEC from 'src/constants/agreements-spec.json';\nimport config, { Config } from 'src/utils/config';\nimport { AgreementIsNotDefinedException } from 'src/constants';\nimport { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';\nimport { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';\nimport { SettingsAnalytics } from 'src/modules/settings/settings.analytics';\nimport { SettingsRepository } from 'src/modules/settings/repositories/settings.repository';\nimport { classToClass } from 'src/utils';\nimport { AgreementsRepository } from 'src/modules/settings/repositories/agreements.repository';\nimport { FeatureServerEvents } from 'src/modules/feature/constants';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { IAgreementSpecFile } from 'src/modules/settings/models/agreements.interface';\nimport { SessionMetadata } from 'src/common/models';\n\nimport { DatabaseDiscoveryService } from 'src/modules/database-discovery/database-discovery.service';\nimport { ToggleAnalyticsReasonType } from 'src/modules/settings/constants/settings';\nimport {\n  GetAgreementsSpecResponse,\n  GetAppSettingsResponse,\n  UpdateSettingsDto,\n} from './dto/settings.dto';\nimport { EncryptionService } from '../encryption/encryption.service';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\n\n@Injectable()\nexport class SettingsService {\n  private logger = new Logger('SettingsService');\n\n  constructor(\n    @Inject(forwardRef(() => DatabaseDiscoveryService))\n    private readonly databaseDiscoveryService: DatabaseDiscoveryService,\n    private readonly settingsRepository: SettingsRepository,\n    private readonly agreementRepository: AgreementsRepository,\n    private readonly analytics: SettingsAnalytics,\n    private readonly keytarEncryptionStrategy: KeytarEncryptionStrategy,\n    private readonly keyEncryptionStrategy: KeyEncryptionStrategy,\n    @Inject(forwardRef(() => EncryptionService))\n    private readonly encryptionService: EncryptionService,\n    private eventEmitter: EventEmitter2,\n  ) {}\n\n  /**\n   * Method to get settings\n   */\n  public async getAppSettings(\n    sessionMetadata: SessionMetadata,\n  ): Promise<GetAppSettingsResponse> {\n    this.logger.debug('Getting application settings.', sessionMetadata);\n    try {\n      const settings =\n        await this.settingsRepository.getOrCreate(sessionMetadata);\n\n      let defaultOptions: object;\n      if (SERVER_CONFIG.acceptTermsAndConditions) {\n        const isEncryptionAvailable =\n          await this.encryptionService.isEncryptionAvailable();\n\n        defaultOptions = {\n          data: {\n            analytics: false,\n            encryption: isEncryptionAvailable,\n            eula: true,\n            notifications: false,\n          },\n          version: (await this.getAgreementsSpec()).version,\n        };\n      }\n\n      const agreements = await this.agreementRepository.getOrCreate(\n        sessionMetadata,\n        defaultOptions,\n      );\n\n      this.logger.debug(\n        'Succeed to get application settings.',\n        sessionMetadata,\n      );\n\n      return classToClass(GetAppSettingsResponse, {\n        ...settings?.data,\n        acceptTermsAndConditionsOverwritten:\n          SERVER_CONFIG.acceptTermsAndConditions,\n        agreements: agreements?.version\n          ? {\n              ...agreements?.data,\n              version: agreements?.version,\n            }\n          : null,\n      });\n    } catch (error) {\n      this.logger.error(\n        'Failed to get application settings.',\n        error,\n        sessionMetadata,\n      );\n      throw new InternalServerErrorException();\n    }\n  }\n\n  /**\n   * Method to update application settings and agreements\n   * @param sessionMetadata\n   * @param dto\n   */\n  public async updateAppSettings(\n    sessionMetadata: SessionMetadata,\n    dto: UpdateSettingsDto,\n  ): Promise<GetAppSettingsResponse> {\n    this.logger.debug('Updating application settings.', sessionMetadata);\n    const { agreements, analyticsReason, ...settings } = dto;\n    try {\n      const oldAppSettings = await this.getAppSettings(sessionMetadata);\n      if (!isEmpty(settings)) {\n        const model =\n          await this.settingsRepository.getOrCreate(sessionMetadata);\n        const toUpdate = {\n          ...model,\n          data: {\n            ...model?.data,\n            ...settings,\n          },\n        };\n\n        await this.settingsRepository.update(sessionMetadata, toUpdate);\n      }\n      if (agreements) {\n        await this.updateAgreements(\n          sessionMetadata,\n          agreements,\n          analyticsReason,\n        );\n      }\n      this.logger.debug(\n        'Succeed to update application settings.',\n        sessionMetadata,\n      );\n      const results = await this.getAppSettings(sessionMetadata);\n      this.analytics.sendSettingsUpdatedEvent(\n        sessionMetadata,\n        results,\n        oldAppSettings,\n      );\n\n      this.eventEmitter.emit(FeatureServerEvents.FeaturesRecalculate);\n\n      // Discover databases from envs or autodiscovery flow when eula accept\n      if (!oldAppSettings?.agreements?.eula && results?.agreements?.eula) {\n        try {\n          await this.databaseDiscoveryService.discover(sessionMetadata, true);\n        } catch (e) {\n          // ignore error\n          this.logger.error(\n            'Failed discover databases after eula accepted.',\n            e,\n            sessionMetadata,\n          );\n        }\n      }\n\n      return results;\n    } catch (error) {\n      this.logger.error(\n        'Failed to update application settings.',\n        error,\n        sessionMetadata,\n      );\n      if (\n        error instanceof AgreementIsNotDefinedException ||\n        error instanceof BadRequestException\n      ) {\n        throw error;\n      }\n      throw new InternalServerErrorException();\n    }\n  }\n\n  /**\n   * Call for current system's state check for conditional agreements spec\n   * @param checker\n   * @param defaultOption\n   * @private\n   */\n  private async getAgreementsOption(\n    checker: string,\n    defaultOption: string,\n  ): Promise<string> {\n    try {\n      // Check if any encryption strategy is available (not KEYTAR only)\n      // KEY has a precedence on KEYTAR strategy\n      if (checker === 'KEYTAR') {\n        const isEncryptionAvailable =\n          (await this.keyEncryptionStrategy.isAvailable()) ||\n          (await this.keytarEncryptionStrategy.isAvailable());\n\n        if (\n          !isEncryptionAvailable &&\n          SERVER_CONFIG.buildType === 'REDIS_STACK'\n        ) {\n          return 'stack_false';\n        }\n\n        return `${isEncryptionAvailable}`;\n      }\n    } catch (e) {\n      this.logger.error(`Unable to proceed agreements checker ${checker}`, e);\n    }\n\n    return defaultOption;\n  }\n\n  /**\n   * Try to get agreements from file\n   * Shouldn't throw an error on fail\n   * @private\n   */\n  private async getAgreementsSpecFromFile(): Promise<IAgreementSpecFile> {\n    try {\n      if (SERVER_CONFIG.agreementsPath) {\n        return JSON.parse(\n          await readFile(join(__dirname, SERVER_CONFIG.agreementsPath), 'utf8'),\n        );\n      }\n    } catch (e) {\n      // ignore error\n    }\n\n    return cloneDeep<IAgreementSpecFile>(AGREEMENTS_SPEC);\n  }\n\n  /**\n   * Process conditional agreements where needed and returns proper agreements spec\n   */\n  public async getAgreementsSpec(): Promise<GetAgreementsSpecResponse> {\n    const agreementsSpec = await this.getAgreementsSpecFromFile();\n\n    await Promise.all(\n      map(agreementsSpec.agreements, async (agreement: any, name) => {\n        if (agreement.conditional) {\n          const option = await this.getAgreementsOption(\n            agreement.checker,\n            agreement.defaultOption,\n          );\n          agreementsSpec.agreements[name] = agreement.options[option];\n        }\n      }),\n    );\n\n    return agreementsSpec;\n  }\n\n  private async updateAgreements(\n    sessionMetadata: SessionMetadata,\n    dtoAgreements: Map<string, boolean> = new Map(),\n    analyticsReason?: ToggleAnalyticsReasonType,\n  ): Promise<void> {\n    this.logger.debug('Updating application agreements.', sessionMetadata);\n    const oldAgreements =\n      await this.agreementRepository.getOrCreate(sessionMetadata);\n    const agreementsSpec = await this.getAgreementsSpecFromFile();\n\n    const newAgreements = {\n      ...oldAgreements,\n      version: agreementsSpec.version,\n      data: {\n        ...oldAgreements.data,\n        ...Object.fromEntries(dtoAgreements),\n      },\n    };\n    // Detect which agreements should be defined according to the settings specification\n    const diff = difference(\n      Object.keys(agreementsSpec.agreements),\n      Object.keys(newAgreements.data),\n    );\n    if (diff.length) {\n      const messages = diff.map(\n        (item: string) => `agreements.${item} should not be null or undefined`,\n      );\n      throw new AgreementIsNotDefinedException(messages);\n    }\n\n    await this.agreementRepository.update(sessionMetadata, newAgreements);\n\n    if (dtoAgreements.has('analytics')) {\n      this.analytics.sendAnalyticsAgreementChange(\n        sessionMetadata,\n        dtoAgreements,\n        new Map(Object.entries(oldAgreements.data || {})),\n        analyticsReason,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/constants/commands.ts",
    "content": "export enum SlowLogCommands {\n  SlowLog = 'slowlog',\n  Config = 'config',\n}\n\nexport enum SlowLogArguments {\n  Get = 'get',\n  Set = 'set',\n  Reset = 'reset',\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/dto/get-slow-logs.dto.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsInt, IsNotEmpty, IsOptional, Min } from 'class-validator';\nimport { Type } from 'class-transformer';\n\nexport class GetSlowLogsDto {\n  @ApiPropertyOptional({\n    description: 'Specifying the number of slow logs to fetch per node.',\n    type: Number,\n    minimum: -1,\n    default: 50,\n  })\n  @IsInt()\n  @Min(-1)\n  @Type(() => Number)\n  @IsNotEmpty()\n  @IsOptional()\n  count?: number = 50;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/dto/update-slow-log-config.dto.ts",
    "content": "import { SlowLogConfig } from 'src/modules/slow-log/models';\n\nexport class UpdateSlowLogConfigDto extends SlowLogConfig {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/models/index.ts",
    "content": "export * from './slow-log';\nexport * from './slow-log-config';\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/models/slow-log-config.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsInt, Min, NotEquals, ValidateIf } from 'class-validator';\n\nexport class SlowLogConfig {\n  @ApiPropertyOptional({\n    description: 'Max logs to store inside Redis slowlog',\n    example: 128,\n    type: Number,\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt()\n  @Min(0)\n  slowlogMaxLen?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Store logs with execution time greater than this value (in microseconds)',\n    example: 10000,\n    type: Number,\n  })\n  @NotEquals(null)\n  @ValidateIf((object, value) => value !== undefined)\n  @IsInt()\n  @Min(-1)\n  slowlogLogSlowerThan?: number;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/models/slow-log.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nexport class SlowLog {\n  @ApiProperty({\n    description: 'Unique slowlog Id calculated by Redis',\n    example: 12,\n    type: Number,\n  })\n  id: number;\n\n  @ApiProperty({\n    description: 'Time when command was executed',\n    example: 1652265051,\n    type: Number,\n  })\n  time: number;\n\n  @ApiProperty({\n    description: 'Time needed to execute this command in microseconds',\n    example: 57000,\n    type: Number,\n  })\n  durationUs: number;\n\n  @ApiProperty({\n    description: 'Command with args',\n    example: 'SET foo bar',\n    type: String,\n  })\n  args: string;\n\n  @ApiProperty({\n    description: 'Client that executed this command',\n    example: '127.17.0.1:46922',\n    type: String,\n  })\n  source: string;\n\n  @ApiPropertyOptional({\n    description: 'Client name if defined',\n    example: 'redisinsight-common-e25b587e',\n    type: String,\n  })\n  client?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/slow-log.analytics.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { TelemetryEvents } from 'src/constants';\nimport { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class SlowLogAnalytics extends TelemetryBaseService {\n  private events: Map<TelemetryEvents, Function> = new Map();\n\n  constructor(protected eventEmitter: EventEmitter2) {\n    super(eventEmitter);\n    this.events.set(\n      TelemetryEvents.SlowlogSetLogSlowerThan,\n      this.slowLogLogSlowerThanUpdated.bind(this),\n    );\n    this.events.set(\n      TelemetryEvents.SlowlogSetMaxLen,\n      this.slowLogMaxLenUpdated.bind(this),\n    );\n  }\n\n  updateSlowLogConfig(\n    sessionMetadata: SessionMetadata,\n    event: TelemetryEvents,\n    eventData: any,\n  ): void {\n    try {\n      this.sendEvent(sessionMetadata, event, eventData);\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  slowLogMaxLenUpdated(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    previousValue: number,\n    currentValue: number,\n  ): void {\n    this.updateSlowLogConfig(\n      sessionMetadata,\n      TelemetryEvents.SlowlogSetMaxLen,\n      {\n        databaseId,\n        previousValue,\n        currentValue,\n      },\n    );\n  }\n\n  slowLogLogSlowerThanUpdated(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    previousValue: number,\n    currentValue: number,\n  ): void {\n    this.updateSlowLogConfig(\n      sessionMetadata,\n      TelemetryEvents.SlowlogSetLogSlowerThan,\n      {\n        databaseId,\n        // convert microseconds to milliseconds\n        previousValueInMSeconds: previousValue / 1_000,\n        currentValueInMSeconds: currentValue / 1_000,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/slow-log.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Patch,\n  Query,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { SlowLogService } from 'src/modules/slow-log/slow-log.service';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { SlowLog, SlowLogConfig } from 'src/modules/slow-log/models';\nimport { UpdateSlowLogConfigDto } from 'src/modules/slow-log/dto/update-slow-log-config.dto';\nimport { GetSlowLogsDto } from 'src/modules/slow-log/dto/get-slow-logs.dto';\nimport { ClientMetadataParam } from 'src/common/decorators';\nimport { ClientMetadata } from 'src/common/models';\n\n@ApiTags('Slow Logs')\n@Controller('slow-logs')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class SlowLogController {\n  constructor(private service: SlowLogService) {}\n\n  @ApiEndpoint({\n    description: 'List of slow logs',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: SlowLog,\n        isArray: true,\n      },\n    ],\n  })\n  @Get('')\n  async getSlowLogs(\n    @ClientMetadataParam({\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n    @Query() getSlowLogsDto: GetSlowLogsDto,\n  ): Promise<any> {\n    return this.service.getSlowLogs(clientMetadata, getSlowLogsDto);\n  }\n\n  @ApiEndpoint({\n    description: 'Clear slow logs',\n    statusCode: 200,\n  })\n  @Delete('')\n  async resetSlowLogs(\n    @ClientMetadataParam({\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n  ): Promise<void> {\n    return this.service.reset(clientMetadata);\n  }\n\n  @ApiEndpoint({\n    description: 'Get slowlog config',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: SlowLogConfig,\n      },\n    ],\n  })\n  @Get('config')\n  async getConfig(\n    @ClientMetadataParam({\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n  ): Promise<SlowLogConfig> {\n    return this.service.getConfig(clientMetadata);\n  }\n\n  @ApiEndpoint({\n    description: 'Update slowlog config',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: SlowLogConfig,\n      },\n    ],\n  })\n  @Patch('config')\n  async updateConfig(\n    @ClientMetadataParam({\n      ignoreDbIndex: true,\n    })\n    clientMetadata: ClientMetadata,\n    @Body() dto: UpdateSlowLogConfigDto,\n  ): Promise<SlowLogConfig> {\n    return this.service.updateConfig(clientMetadata, dto);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/slow-log.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { SlowLogController } from 'src/modules/slow-log/slow-log.controller';\nimport { SlowLogService } from 'src/modules/slow-log/slow-log.service';\nimport { SlowLogAnalytics } from 'src/modules/slow-log/slow-log.analytics';\n\n@Module({\n  providers: [SlowLogService, SlowLogAnalytics],\n  controllers: [SlowLogController],\n})\nexport class SlowLogModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockRedisNoPermError,\n  MockType,\n  mockCommonClientMetadata,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n  mockClusterRedisClient,\n} from 'src/__mocks__';\nimport { SlowLogService } from 'src/modules/slow-log/slow-log.service';\nimport { BadRequestException, ForbiddenException } from '@nestjs/common';\nimport {\n  SlowLogArguments,\n  SlowLogCommands,\n} from 'src/modules/slow-log/constants/commands';\nimport { SlowLogAnalytics } from 'src/modules/slow-log/slow-log.analytics';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nconst getSlowLogDto = { count: 100 };\nconst mockSlowLog = {\n  id: 1,\n  time: 165234561,\n  durationUs: 100,\n  args: 'get foo',\n  source: '127.0.0.1:12399',\n  client: 'client-name',\n};\n\nconst mockLogReply = [\n  mockSlowLog.id,\n  mockSlowLog.time,\n  mockSlowLog.durationUs,\n  mockSlowLog.args.split(' '),\n  mockSlowLog.source,\n  mockSlowLog.client,\n];\nconst mockSlowLogConfig = {\n  slowlogMaxLen: 128,\n  slowlogLogSlowerThan: 10000,\n};\n\nconst mockSlowlogConfigReply = [\n  'slowlog-max-len',\n  mockSlowLogConfig.slowlogMaxLen,\n  'slowlog-log-slower-than',\n  mockSlowLogConfig.slowlogLogSlowerThan,\n];\n\nconst mockSlowLogReply = [mockLogReply, mockLogReply];\n\ndescribe('SlowLogService', () => {\n  const standaloneClient = mockStandaloneRedisClient;\n  const clusterClient = mockClusterRedisClient;\n  let service: SlowLogService;\n  let databaseClientFactory: MockType<DatabaseClientFactory>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        SlowLogService,\n        EventEmitter2,\n        SlowLogAnalytics,\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = await module.get(SlowLogService);\n    databaseClientFactory = await module.get(DatabaseClientFactory);\n\n    clusterClient.call.mockResolvedValue(mockSlowLogReply);\n    clusterClient.nodes.mockReturnValue([standaloneClient, standaloneClient]);\n    standaloneClient.call.mockResolvedValue(mockSlowLogReply);\n  });\n\n  describe('getSlowLogs', () => {\n    it('should return slowlogs for standalone', async () => {\n      const res = await service.getSlowLogs(\n        mockCommonClientMetadata,\n        getSlowLogDto,\n      );\n      expect(res).toEqual([mockSlowLog, mockSlowLog]);\n    });\n    it('should return slowlogs for standalone without active connection', async () => {\n      const res = await service.getSlowLogs(\n        mockCommonClientMetadata,\n        getSlowLogDto,\n      );\n      expect(res).toEqual([mockSlowLog, mockSlowLog]);\n    });\n    it('should return slowlogs cluster', async () => {\n      databaseClientFactory.getOrCreateClient.mockResolvedValueOnce(\n        clusterClient,\n      );\n      const res = await service.getSlowLogs(\n        mockCommonClientMetadata,\n        getSlowLogDto,\n      );\n      expect(res).toEqual([mockSlowLog, mockSlowLog, mockSlowLog, mockSlowLog]);\n    });\n    it('should proxy HttpException', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw new BadRequestException('error');\n      });\n      try {\n        await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n    it('should throw an Forbidden error when command execution failed', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw mockRedisNoPermError;\n      });\n\n      try {\n        await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('reset', () => {\n    it('should reset slowlogs for standalone', async () => {\n      await service.reset(mockCommonClientMetadata);\n      expect(standaloneClient.call).toHaveBeenCalledWith([\n        SlowLogCommands.SlowLog,\n        SlowLogArguments.Reset,\n      ]);\n    });\n    it('should reset slowlogs cluster', async () => {\n      databaseClientFactory.getOrCreateClient.mockResolvedValueOnce(\n        clusterClient,\n      );\n      await service.reset(mockCommonClientMetadata);\n      expect(standaloneClient.call).toHaveBeenCalledWith([\n        SlowLogCommands.SlowLog,\n        SlowLogArguments.Reset,\n      ]);\n    });\n    it('should proxy HttpException', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw new BadRequestException('error');\n      });\n\n      try {\n        await service.reset(mockCommonClientMetadata);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n    it('should throw an Forbidden error when command execution failed', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw mockRedisNoPermError;\n      });\n\n      try {\n        await service.reset(mockCommonClientMetadata);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('getConfig', () => {\n    it('should get slowlogs config', async () => {\n      standaloneClient.call.mockResolvedValueOnce(mockSlowlogConfigReply);\n\n      const res = await service.getConfig(mockCommonClientMetadata);\n      expect(res).toEqual(mockSlowLogConfig);\n    });\n    it('should get ONLY supported slowlogs config even if there some extra fields in resp', async () => {\n      standaloneClient.call.mockResolvedValueOnce([\n        ...mockSlowlogConfigReply,\n        'slowlog-extra',\n        12,\n      ]);\n\n      const res = await service.getConfig(mockCommonClientMetadata);\n      expect(res).toEqual(mockSlowLogConfig);\n    });\n    it('should proxy HttpException', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw new BadRequestException('error');\n      });\n\n      try {\n        await service.getConfig(mockCommonClientMetadata);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n    it('should throw an Forbidden error when command execution failed', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw mockRedisNoPermError;\n      });\n\n      try {\n        await service.getConfig(mockCommonClientMetadata);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n\n  describe('updateConfig', () => {\n    it('should update slowlogs config (1 field)', async () => {\n      standaloneClient.call.mockResolvedValueOnce(mockSlowlogConfigReply);\n      standaloneClient.call.mockResolvedValueOnce('OK');\n\n      const res = await service.updateConfig(mockCommonClientMetadata, {\n        slowlogMaxLen: 128,\n      });\n      expect(res).toEqual(mockSlowLogConfig);\n      expect(standaloneClient.call).toHaveBeenCalledTimes(2);\n    });\n    it('should update slowlogs config (2 fields)', async () => {\n      standaloneClient.call\n        .mockResolvedValueOnce(mockSlowlogConfigReply)\n        .mockResolvedValueOnce('OK')\n        .mockResolvedValueOnce('OK');\n\n      const res = await service.updateConfig(mockCommonClientMetadata, {\n        slowlogMaxLen: 128,\n        slowlogLogSlowerThan: 1,\n      });\n      expect(res).toEqual({ slowlogMaxLen: 128, slowlogLogSlowerThan: 1 });\n      expect(standaloneClient.call).toHaveBeenCalledTimes(3);\n    });\n    it('should throw an error for cluster', async () => {\n      databaseClientFactory.getOrCreateClient.mockResolvedValueOnce(\n        clusterClient,\n      );\n      databaseClientFactory.getOrCreateClient.mockResolvedValueOnce(\n        clusterClient,\n      );\n      clusterClient.call.mockResolvedValueOnce(mockSlowlogConfigReply);\n\n      try {\n        await service.updateConfig(mockCommonClientMetadata, {\n          slowlogMaxLen: 128,\n          slowlogLogSlowerThan: 1,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n    it('should proxy HttpException', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw new BadRequestException('error');\n      });\n\n      try {\n        await service.updateConfig(mockCommonClientMetadata, {\n          slowlogMaxLen: 1,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n    it('should throw an Forbidden error when command execution failed', async () => {\n      databaseClientFactory.getOrCreateClient.mockImplementationOnce(() => {\n        throw mockRedisNoPermError;\n      });\n\n      try {\n        await service.updateConfig(mockCommonClientMetadata, {\n          slowlogMaxLen: 1,\n        });\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(ForbiddenException);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/slow-log/slow-log.service.ts",
    "content": "import { concat } from 'lodash';\nimport {\n  BadRequestException,\n  HttpException,\n  Injectable,\n  Logger,\n} from '@nestjs/common';\nimport { SlowLog, SlowLogConfig } from 'src/modules/slow-log/models';\nimport {\n  SlowLogArguments,\n  SlowLogCommands,\n} from 'src/modules/slow-log/constants/commands';\nimport { catchAclError } from 'src/utils';\nimport { UpdateSlowLogConfigDto } from 'src/modules/slow-log/dto/update-slow-log-config.dto';\nimport { GetSlowLogsDto } from 'src/modules/slow-log/dto/get-slow-logs.dto';\nimport { SlowLogAnalytics } from 'src/modules/slow-log/slow-log.analytics';\nimport { ClientMetadata } from 'src/common/models';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  RedisClient,\n  RedisClientConnectionType,\n} from 'src/modules/redis/client';\n\n@Injectable()\nexport class SlowLogService {\n  private logger = new Logger('SlowLogService');\n\n  constructor(\n    private databaseClientFactory: DatabaseClientFactory,\n    private analyticsService: SlowLogAnalytics,\n  ) {}\n\n  /**\n   * Get slow logs for each node and return concatenated result\n   * @param clientMetadata\n   * @param dto\n   */\n  async getSlowLogs(clientMetadata: ClientMetadata, dto: GetSlowLogsDto) {\n    try {\n      this.logger.debug('Getting slow logs', clientMetadata);\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const nodes = await client.nodes();\n\n      return concat(\n        ...(await Promise.all(\n          nodes.map((node) => this.getNodeSlowLogs(node, dto)),\n        )),\n      );\n    } catch (e) {\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Get array of slow logs for particular node\n   * @param node\n   * @param dto\n   */\n  async getNodeSlowLogs(\n    node: RedisClient,\n    dto: GetSlowLogsDto,\n  ): Promise<SlowLog[]> {\n    const resp = (await node.call(\n      [SlowLogCommands.SlowLog, SlowLogArguments.Get, dto.count],\n      { replyEncoding: 'utf8' },\n    )) as string[][] | number[][];\n\n    return resp.map((log) => {\n      const [id, time, durationUs, args, source, client] = log;\n      return {\n        id,\n        time,\n        durationUs,\n        args: args.join(' '),\n        source,\n        client,\n      };\n    });\n  }\n\n  /**\n   * Clear slow logs in all nodes\n   * @param clientMetadata\n   */\n  async reset(clientMetadata: ClientMetadata): Promise<void> {\n    try {\n      this.logger.debug('Resetting slow logs', clientMetadata);\n\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const nodes = await client.nodes();\n\n      await Promise.all(\n        nodes.map((node) =>\n          node.call([SlowLogCommands.SlowLog, SlowLogArguments.Reset]),\n        ),\n      );\n    } catch (e) {\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Get current slowlog config to show for user\n   * @param clientMetadata\n   */\n  async getConfig(clientMetadata: ClientMetadata): Promise<SlowLogConfig> {\n    try {\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      const resp = convertArrayReplyToObject(\n        (await client.call(\n          [SlowLogCommands.Config, SlowLogArguments.Get, 'slowlog*'],\n          { replyEncoding: 'utf8' },\n        )) as string[],\n      );\n\n      return {\n        slowlogMaxLen: parseInt(resp['slowlog-max-len'], 10) || 0,\n        slowlogLogSlowerThan:\n          parseInt(resp['slowlog-log-slower-than'], 10) || 0,\n      };\n    } catch (e) {\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n\n  /**\n   * Update slowlog config\n   * @param clientMetadata\n   * @param dto\n   */\n  async updateConfig(\n    clientMetadata: ClientMetadata,\n    dto: UpdateSlowLogConfigDto,\n  ): Promise<SlowLogConfig> {\n    try {\n      const commands = [];\n      const config = await this.getConfig(clientMetadata);\n      const { slowlogLogSlowerThan, slowlogMaxLen } = config;\n\n      if (dto.slowlogLogSlowerThan !== undefined) {\n        commands.push({\n          command: SlowLogCommands.Config,\n          args: [\n            SlowLogArguments.Set,\n            'slowlog-log-slower-than',\n            dto.slowlogLogSlowerThan,\n          ],\n          analytics: () =>\n            this.analyticsService.slowLogLogSlowerThanUpdated(\n              clientMetadata.sessionMetadata,\n              clientMetadata.databaseId,\n              slowlogLogSlowerThan,\n              dto.slowlogLogSlowerThan,\n            ),\n        });\n\n        config.slowlogLogSlowerThan = dto.slowlogLogSlowerThan;\n      }\n\n      if (dto.slowlogMaxLen !== undefined) {\n        commands.push({\n          command: SlowLogCommands.Config,\n          args: [SlowLogArguments.Set, 'slowlog-max-len', dto.slowlogMaxLen],\n          analytics: () =>\n            this.analyticsService.slowLogMaxLenUpdated(\n              clientMetadata.sessionMetadata,\n              clientMetadata.databaseId,\n              slowlogMaxLen,\n              dto.slowlogMaxLen,\n            ),\n        });\n\n        config.slowlogMaxLen = dto.slowlogMaxLen;\n      }\n\n      if (commands.length) {\n        const client =\n          await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n        if (client.getConnectionType() === RedisClientConnectionType.CLUSTER) {\n          return Promise.reject(\n            new BadRequestException(\n              'Configuration slowlog for cluster is deprecated',\n            ),\n          );\n        }\n        await Promise.all(\n          commands.map((command) =>\n            client\n              .call([command.command, ...command.args])\n              .then(command.analytics),\n          ),\n        );\n      }\n\n      return config;\n    } catch (e) {\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw catchAclError(e);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/dto/create.basic-ssh-options.dto.ts",
    "content": "import { OmitType } from '@nestjs/swagger';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\n\nexport class CreateBasicSshOptionsDto extends OmitType(SshOptions, [\n  'privateKey',\n  'passphrase',\n  'id',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/dto/create.cert-ssh-options.dto.ts",
    "content": "import { OmitType } from '@nestjs/swagger';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\n\nexport class CreateCertSshOptionsDto extends OmitType(SshOptions, [\n  'password',\n  'id',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/dto/ssh-options.response.ts",
    "content": "import { ApiPropertyOptional, OmitType } from '@nestjs/swagger';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\nimport { Expose } from 'class-transformer';\nimport { HiddenField } from 'src/common/decorators/hidden-field.decorator';\n\nexport class SshOptionsResponse extends OmitType(SshOptions, [\n  'password',\n  'passphrase',\n  'privateKey',\n] as const) {\n  @ApiPropertyOptional({\n    description: 'The SSH password flag (true if password was set)',\n    type: Boolean,\n  })\n  @Expose()\n  @HiddenField(true)\n  password?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'The SSH passphrase flag (true if password was set)',\n    type: Boolean,\n  })\n  @Expose()\n  @HiddenField(true)\n  passphrase?: boolean;\n\n  @ApiPropertyOptional({\n    description: 'The SSH private key',\n    type: Boolean,\n  })\n  @Expose()\n  @HiddenField(true)\n  privateKey?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/dto/update.ssh-options.dto.ts",
    "content": "import { OmitType, PartialType } from '@nestjs/swagger';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\n\nexport class UpdateSshOptionsDto extends PartialType(\n  OmitType(SshOptions, ['id'] as const),\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/entities/ssh-options.entity.ts",
    "content": "import {\n  Column,\n  Entity,\n  JoinColumn,\n  OneToOne,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Entity('ssh_options')\nexport class SshOptionsEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  host: string;\n\n  @Expose()\n  @Column({ nullable: false })\n  port: number;\n\n  @Expose()\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  username: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  password: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  privateKey: string;\n\n  @Expose()\n  @Column({ nullable: true })\n  passphrase: string;\n\n  @OneToOne(() => DatabaseEntity, (database) => database.sshOptions, {\n    nullable: true,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn()\n  database: DatabaseEntity;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/exceptions/index.ts",
    "content": "export * from './unable-to-create-ssh-connection.exception';\nexport * from './unable-to-create-tunnel.exception';\nexport * from './tunnel-connection-lost.exception';\nexport * from './unable-to-create-local-server.exception';\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/exceptions/tunnel-connection-lost.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { sanitizeMessage } from '../utils';\n\nexport class TunnelConnectionLostException extends HttpException {\n  constructor(message = '') {\n    const prepend = 'Tunnel connection was lost.';\n    const sanitizedMessage = sanitizeMessage(message);\n    super(\n      {\n        message: `${prepend} ${sanitizedMessage}`,\n        name: 'TunnelConnectionLostException',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/exceptions/unable-to-create-local-server.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { sanitizeMessage } from '../utils';\n\nexport class UnableToCreateLocalServerException extends HttpException {\n  constructor(message = '') {\n    const prepend = 'Unable to create local server.';\n    const sanitizedMessage = sanitizeMessage(message);\n    super(\n      {\n        message: `${prepend} ${sanitizedMessage}`,\n        name: 'UnableToCreateLocalServerException',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/exceptions/unable-to-create-ssh-connection.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { sanitizeMessage } from '../utils';\n\nexport class UnableToCreateSshConnectionException extends HttpException {\n  constructor(message = '') {\n    const prepend = 'Unable to create ssh connection.';\n    const sanitizedMessage = sanitizeMessage(message);\n    super(\n      {\n        message: `${prepend} ${sanitizedMessage}`,\n        name: 'UnableToCreateSshConnectionException',\n        statusCode: 503,\n      },\n      503,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/exceptions/unable-to-create-tunnel.exception.ts",
    "content": "import { HttpException } from '@nestjs/common';\nimport { sanitizeMessage } from '../utils';\n\nexport class UnableToCreateTunnelException extends HttpException {\n  constructor(message = '') {\n    const prepend = 'Unable to create tunnel.';\n    const sanitizedMessage = sanitizeMessage(message);\n    super(\n      {\n        message: `${prepend} ${sanitizedMessage}`,\n        name: 'UnableToCreateTunnelException',\n        statusCode: 500,\n      },\n      500,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/models/ssh-options.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\nimport { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';\n\nexport class SshOptions {\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'The hostname of SSH server',\n    type: String,\n    default: 'localhost',\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsString({ always: true })\n  host: string;\n\n  @ApiProperty({\n    description: 'The port of SSH server',\n    type: Number,\n    default: 22,\n  })\n  @Expose()\n  @IsNotEmpty()\n  @IsInt({ always: true })\n  port: number;\n\n  @ApiPropertyOptional({\n    description: 'SSH username',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  username?: string;\n\n  @ApiPropertyOptional({\n    description: 'The SSH password',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  password?: string;\n\n  @ApiPropertyOptional({\n    description: 'The SSH private key',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  privateKey?: string;\n\n  @ApiPropertyOptional({\n    description: 'The SSH passphrase',\n    type: String,\n  })\n  @Expose()\n  @IsString({ always: true })\n  @IsNotEmpty()\n  @IsOptional()\n  passphrase?: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/models/ssh-tunnel.ts",
    "content": "import { AddressInfo } from 'net';\nimport { createTunnel } from 'tunnel-ssh';\nimport { Endpoint } from 'src/common/models';\n\nexport type SshTunnelServer = Awaited<ReturnType<typeof createTunnel>>[0];\nexport type SshTunnelClient = Awaited<ReturnType<typeof createTunnel>>[1];\n\nexport interface ISshTunnelOptions {\n  targetHost: string;\n  targetPort: number;\n}\n\nexport class SshTunnel {\n  public readonly serverAddress: Endpoint;\n\n  constructor(\n    private readonly server: SshTunnelServer,\n    private readonly client: SshTunnelClient,\n    public readonly options: ISshTunnelOptions,\n  ) {\n    const address = this.server?.address() as AddressInfo;\n    this.serverAddress = {\n      host: '127.0.0.1',\n      port: address?.port,\n    };\n  }\n\n  public close() {\n    this.server?.close?.(() => {\n      // ignore any error\n    });\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/ssh-tunnel.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { mockDatabaseWithSshBasic } from 'src/__mocks__';\nimport { SshTunnelProvider } from 'src/modules/ssh/ssh-tunnel.provider';\nimport { EventEmitter } from 'events';\nimport { UnableToCreateTunnelException } from 'src/modules/ssh/exceptions';\nimport * as tunnelSsh from 'tunnel-ssh';\n\njest.mock('tunnel-ssh', () => ({\n  ...(jest.requireActual('tunnel-ssh') as object),\n}));\n\ndescribe('SshTunnelProvider', () => {\n  let service: SshTunnelProvider;\n  let mockClient;\n  let mockServer;\n  let tunnelSshSpy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [SshTunnelProvider],\n    }).compile();\n\n    service = await module.get(SshTunnelProvider);\n\n    mockClient = new EventEmitter();\n    mockClient.connect = jest.fn();\n    mockServer = new EventEmitter();\n    mockServer.listen = jest.fn(() => mockServer.emit('listening'));\n    mockServer.address = jest\n      .fn()\n      .mockReturnValue({ address: '127.0.0.1', port: 50000 });\n    tunnelSshSpy = jest.spyOn(tunnelSsh, 'createTunnel');\n    tunnelSshSpy.mockImplementationOnce(() => [mockServer, mockClient]);\n  });\n\n  describe('createTunnel', () => {\n    it('should create tunnel', (done) => {\n      service\n        .createTunnel(\n          mockDatabaseWithSshBasic,\n          mockDatabaseWithSshBasic.sshOptions,\n        )\n        .then((tnl) => {\n          expect(tnl['client']).toEqual(mockClient);\n          expect(tnl['server']).toEqual(mockServer);\n          done();\n        })\n        .catch(done);\n    });\n    it('should fail due to server init error', (done) => {\n      tunnelSshSpy.mockReset().mockImplementationOnce(() => {\n        throw new Error('bb');\n      });\n\n      service\n        .createTunnel(\n          mockDatabaseWithSshBasic,\n          mockDatabaseWithSshBasic.sshOptions,\n        )\n        .catch((e) => {\n          expect(e).toBeInstanceOf(UnableToCreateTunnelException);\n          done();\n        });\n    });\n    it('should fail with error but not with unable to get property from \"undefined\"', (done) => {\n      tunnelSshSpy.mockReset().mockImplementationOnce(() => {\n        throw new Error('bb');\n      });\n\n      service.createTunnel(undefined, undefined).catch((e) => {\n        expect(e).toBeInstanceOf(UnableToCreateTunnelException);\n        done();\n      });\n    });\n    it('should fail due to createServer failed, error \"Cannot parse privateKey\" message', (done) => {\n      tunnelSshSpy.mockReset().mockImplementationOnce(() => {\n        throw new Error('Cannot parse privateKey: due to some reason');\n      });\n\n      service\n        .createTunnel(\n          mockDatabaseWithSshBasic,\n          mockDatabaseWithSshBasic.sshOptions,\n        )\n        .catch((e) => {\n          expect(e).toBeInstanceOf(UnableToCreateTunnelException);\n          expect(e.message).toEqual(\n            'Unable to create tunnel. Cannot parse privateKey',\n          );\n          done();\n        });\n    });\n\n    it('should fail due to createServer failed, error connect ECONNREFUSED', (done) => {\n      const sshClientErrorMessage = 'connect ECONNREFUSED 127.0.0.1:22222';\n      tunnelSshSpy.mockReset().mockImplementationOnce(() => {\n        throw new Error(sshClientErrorMessage);\n      });\n\n      const sanitizedMessage = 'connect ECONNREFUSED';\n      service\n        .createTunnel(\n          mockDatabaseWithSshBasic,\n          mockDatabaseWithSshBasic.sshOptions,\n        )\n        .catch((e) => {\n          expect(e).toBeInstanceOf(UnableToCreateTunnelException);\n          expect(e.message).toEqual(\n            `Unable to create tunnel. ${sanitizedMessage}`,\n          );\n          done();\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/ssh-tunnel.provider.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { SshTunnel } from 'src/modules/ssh/models/ssh-tunnel';\nimport { UnableToCreateTunnelException } from 'src/modules/ssh/exceptions';\nimport { Endpoint } from 'src/common/models';\nimport { SshOptions } from 'src/modules/ssh/models/ssh-options';\nimport { createTunnel } from 'tunnel-ssh';\n\n@Injectable()\nexport class SshTunnelProvider {\n  public async createTunnel(target: Endpoint, sshOptions: SshOptions) {\n    try {\n      const [server, client] = await createTunnel(\n        {\n          autoClose: true,\n        },\n        {\n          host: '127.0.0.1',\n        },\n        {\n          ...sshOptions,\n        },\n        {\n          dstAddr: target.host,\n          dstPort: target.port,\n        },\n      );\n\n      return new SshTunnel(server, client, {\n        targetHost: target.host,\n        targetPort: target.port,\n      });\n    } catch (e) {\n      if (e instanceof HttpException) {\n        throw e;\n      }\n\n      throw new UnableToCreateTunnelException(e.message);\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/ssh.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { SshTunnelProvider } from 'src/modules/ssh/ssh-tunnel.provider';\n\n@Module({\n  providers: [SshTunnelProvider],\n  exports: [SshTunnelProvider],\n})\nexport class SshModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/transformers/ssh-options.transformer.spec.ts",
    "content": "import { TypeHelpOptions } from 'class-transformer';\nimport { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';\nimport { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';\nimport { sshOptionsTransformer } from './ssh-options.transformer';\n\ndescribe('caCertTransformer', () => {\n  [\n    {\n      input: { object: { sshOptions: {} } } as unknown as TypeHelpOptions,\n      output: CreateBasicSshOptionsDto,\n    },\n    {\n      input: {\n        object: { sshOptions: { privateKey: 'asd' } },\n      } as unknown as TypeHelpOptions,\n      output: CreateCertSshOptionsDto,\n    },\n    {\n      input: {\n        object: { sshOptions: { privateKey: null } },\n      } as unknown as TypeHelpOptions,\n      output: CreateBasicSshOptionsDto,\n    },\n    {\n      input: { object: null } as unknown as TypeHelpOptions,\n      output: CreateBasicSshOptionsDto,\n    },\n    {\n      input: null,\n      output: CreateBasicSshOptionsDto,\n    },\n  ].forEach((tc) => {\n    it(`Should return ${tc.output} when input is: ${tc.input}`, () => {\n      expect(sshOptionsTransformer(tc.input)).toEqual(tc.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/transformers/ssh-options.transformer.ts",
    "content": "import { get } from 'lodash';\nimport { TypeHelpOptions } from 'class-transformer';\nimport { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';\nimport { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';\n\nexport const sshOptionsTransformer = (data: TypeHelpOptions) => {\n  if (get(data?.object, 'sshOptions.privateKey')) {\n    return CreateCertSshOptionsDto;\n  }\n  return CreateBasicSshOptionsDto;\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/utils/error-message.spec.ts",
    "content": "import { sanitizeMessage } from './error-message';\n\ndescribe('SSH exception error message helper', () => {\n  it('should return \"Cannot parse privateKey\" when the message contains \"Cannot parse privateKey\"', () => {\n    const message = 'Cannot parse privateKey some sensitive data';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('Cannot parse privateKey');\n  });\n\n  it('should extract core error message for known patterns like \"getaddrinfo ENOTFOUND\"', () => {\n    const message = 'getaddrinfo ENOTFOUND domain.com';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('getaddrinfo ENOTFOUND');\n  });\n\n  it('should extract core error message for known patterns like \"connect EHOSTDOWN\"', () => {\n    const message = 'connect EHOSTDOWN 127.0.0.1:22';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('connect EHOSTDOWN');\n  });\n\n  it('should remove IP addresses and ports from the message', () => {\n    const message = 'Error occurred at 192.168.1.1:8080';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('Error occurred at');\n  });\n\n  it('should remove hostnames and domains from the message', () => {\n    const message = 'Unable to resolve host example.com';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('Unable to resolve host');\n  });\n\n  it('should handle messages with multiple sensitive patterns', () => {\n    const message = 'connect ECONNREFUSED 192.168.1.1:22 example.com';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('connect ECONNREFUSED');\n  });\n\n  it('should return the original message if no sensitive patterns are found', () => {\n    const message = 'An unknown error occurred';\n    const result = sanitizeMessage(message);\n    expect(result).toBe('An unknown error occurred');\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/utils/error-message.ts",
    "content": "/**\n * Sanitizes an error message by removing sensitive data:\n *\n * - IP addresses (with optional ports)\n * - Hostnames\n * - Domains\n * - Private key-related content\n *\n * Examples:\n * - `\"getaddrinfo ENOTFOUND domain.com\"` → `\"getaddrinfo ENOTFOUND\"`\n * - `\"connect EHOSTDOWN 127.0.0.1:22\"` → `\"connect EHOSTDOWN\"`\n * - `\"Cannot parse privateKey ...\"` → `\"Cannot parse privateKey\"`\n *\n * @param {string} message - The raw error message.\n * @returns {string} A sanitized, safe-to-display message.\n */\nexport const sanitizeMessage = (message: string): string => {\n  if (message.includes('Cannot parse privateKey'))\n    return 'Cannot parse privateKey';\n\n  const coreError = message.match(/(getaddrinfo|connect|read|write)\\s+[A-Z_]+/);\n  if (coreError) return coreError[0];\n\n  // Remove known sensitive patterns from the message\n  const regexes = [\n    /(?:\\d{1,3}\\.){3}\\d{1,3}(?::\\d{1,5})?/g, // IP + port\n    /\\b[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)+/g, // Hostname/domain\n  ];\n\n  return regexes.reduce(\n    (sanitized, regex) => sanitized.replace(regex, '').trim(),\n    message,\n  );\n};\n"
  },
  {
    "path": "redisinsight/api/src/modules/ssh/utils/index.ts",
    "content": "export * from './error-message';\n"
  },
  {
    "path": "redisinsight/api/src/modules/statics-management/providers/auto-updated-statics.interface.ts",
    "content": "export interface IStaticsProviderOptions {\n  name: string;\n  defaultSourcePath: string;\n  destinationPath: string;\n  updateUrl: string;\n  zip: string;\n  buildInfo: string;\n  devMode?: boolean;\n  autoUpdate?: boolean;\n  initDefaults?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/statics-management/providers/auto-updated-statics.provider.spec.ts",
    "content": "import axios from 'axios';\nimport * as fs from 'fs-extra';\nimport config from 'src/utils/config';\nimport { AutoUpdatedStaticsProvider } from './auto-updated-statics.provider';\n\nconst PATH_CONFIG = config.get('dir_path');\nconst TUTORIALS = config.get('tutorials');\n\njest.mock('axios');\nconst mockedAxios = axios as jest.Mocked<typeof axios>;\n\njest.mock('fs-extra');\nconst mockedFs = fs as jest.Mocked<typeof fs>;\n\nconst mockedAdmZip = {\n  extractAllTo: jest.fn(),\n};\njest.mock('adm-zip', () => jest.fn().mockImplementation(() => mockedAdmZip));\n\ndescribe('AutoUpdatedStaticsProvider', () => {\n  let service: AutoUpdatedStaticsProvider;\n  let initDefaultsSpy: jest.SpyInstance;\n  let autoUpdateSpy: jest.SpyInstance;\n\n  beforeEach(async () => {\n    jest.mock('fs-extra', () => mockedFs);\n    jest.mock('axios', () => mockedAxios);\n    jest.mock('adm-zip', () =>\n      jest.fn().mockImplementation(() => mockedAdmZip),\n    );\n\n    service = new AutoUpdatedStaticsProvider({\n      name: 'TutorialsProvider',\n      destinationPath: PATH_CONFIG.tutorials,\n      defaultSourcePath: PATH_CONFIG.defaultTutorials,\n      updateUrl: TUTORIALS.updateUrl,\n      buildInfo: TUTORIALS.buildInfo,\n      zip: TUTORIALS.zip,\n      devMode: TUTORIALS.devMode,\n      autoUpdate: true,\n      initDefaults: true,\n    });\n  });\n\n  describe('onModuleInit', () => {\n    beforeEach(() => {\n      initDefaultsSpy = jest.spyOn(service, 'initDefaults');\n      autoUpdateSpy = jest.spyOn(service, 'autoUpdate');\n\n      initDefaultsSpy.mockResolvedValueOnce(undefined);\n      autoUpdateSpy.mockResolvedValueOnce(undefined);\n    });\n\n    it('should invoke autoUpdate and initDefaults', async () => {\n      await service.onModuleInit();\n\n      expect(initDefaultsSpy).toHaveBeenCalled();\n      expect(autoUpdateSpy).toHaveBeenCalled();\n    });\n    it('should invoke autoUpdate but not initDefaults', async () => {\n      service['options'].initDefaults = false;\n\n      await service.onModuleInit();\n\n      expect(initDefaultsSpy).not.toHaveBeenCalled();\n      expect(autoUpdateSpy).toHaveBeenCalled();\n    });\n    it('should not invoke autoUpdate but invoke initDefaults', async () => {\n      service['options'].autoUpdate = false;\n\n      await service.onModuleInit();\n\n      expect(initDefaultsSpy).toHaveBeenCalled();\n      expect(autoUpdateSpy).not.toHaveBeenCalled();\n    });\n    it('should not invoke autoUpdate and initDefaults', async () => {\n      service['options'].initDefaults = false;\n      service['options'].autoUpdate = false;\n\n      await service.onModuleInit();\n\n      expect(initDefaultsSpy).not.toHaveBeenCalled();\n      expect(autoUpdateSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('initDefaults', () => {\n    it('should not copy defaults when files already exists', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => true);\n\n      await service.initDefaults();\n\n      expect(mockedFs.ensureDir).not.toHaveBeenCalled();\n      expect(mockedFs.copy).not.toHaveBeenCalled();\n    });\n    it('should copy defaults when no files in home directory', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => false);\n\n      await service.initDefaults();\n\n      expect(mockedFs.ensureDir).toHaveBeenCalled();\n      expect(mockedFs.copy).toHaveBeenCalled();\n    });\n    it('should not fail when there is an error during copying default files', async () => {\n      mockedFs.pathExists.mockImplementationOnce(async () => false);\n      mockedFs.copy.mockImplementationOnce(async () => {\n        throw new Error();\n      });\n\n      await service.initDefaults();\n    });\n  });\n\n  describe('autoUpdate', () => {\n    it('should not try to update when there is nothing to update', async () => {\n      const isUpdatesAvailableSpy = jest.spyOn(service, 'isUpdatesAvailable');\n      const updateStaticFilesSpy = jest.spyOn(service, 'updateStaticFiles');\n      isUpdatesAvailableSpy.mockResolvedValueOnce(false);\n\n      await service.autoUpdate();\n\n      expect(updateStaticFilesSpy).not.toHaveBeenCalled();\n    });\n    it('should try to update', async () => {\n      const isUpdatesAvailableSpy = jest.spyOn(service, 'isUpdatesAvailable');\n      const updateStaticFilesSpy = jest.spyOn(service, 'updateStaticFiles');\n      isUpdatesAvailableSpy.mockResolvedValueOnce(true);\n      updateStaticFilesSpy.mockResolvedValueOnce();\n\n      await service.autoUpdate();\n\n      expect(updateStaticFilesSpy).toHaveBeenCalled();\n    });\n    it('should not throw and error when update failed', async () => {\n      const isUpdatesAvailableSpy = jest.spyOn(service, 'isUpdatesAvailable');\n      const updateStaticFilesSpy = jest.spyOn(service, 'updateStaticFiles');\n      isUpdatesAvailableSpy.mockResolvedValueOnce(true);\n      updateStaticFilesSpy.mockRejectedValueOnce(new Error());\n\n      await service.autoUpdate();\n\n      expect(updateStaticFilesSpy).toHaveBeenCalled();\n    });\n  });\n\n  describe('updateStaticFiles', () => {\n    it('should not process when no archive found', async () => {\n      const getLatestArchiveSpy = jest.spyOn(service, 'getLatestArchive');\n      getLatestArchiveSpy.mockResolvedValueOnce(null);\n\n      await service.updateStaticFiles();\n\n      expect(mockedFs.remove).not.toHaveBeenCalled();\n    });\n    it('should extract all files', async () => {\n      const getLatestArchiveSpy = jest.spyOn(service, 'getLatestArchive');\n      getLatestArchiveSpy.mockResolvedValueOnce(Buffer.from('asdasdsad'));\n      mockedAdmZip.extractAllTo.mockResolvedValueOnce(true);\n      mockedFs.writeFile.mockImplementationOnce(async () => true);\n\n      await service.updateStaticFiles();\n    });\n  });\n\n  describe('getLatestArchive', () => {\n    it('should return latest archive buffer', async () => {\n      const mockedArchiveBuffer = Buffer.alloc(10, 0);\n      mockedAxios.get.mockResolvedValueOnce({ data: mockedArchiveBuffer });\n\n      expect(await service.getLatestArchive()).toEqual(mockedArchiveBuffer);\n    });\n    it('should return null when error during downloading archive', async () => {\n      mockedAxios.get.mockRejectedValueOnce(new Error());\n\n      expect(await service.getLatestArchive()).toEqual(null);\n    });\n  });\n\n  describe('isUpdatesAvailable', () => {\n    let getCurrentBuildInfoSpy;\n    let getRemoteBuildInfoSpy;\n\n    beforeEach(() => {\n      getCurrentBuildInfoSpy = jest.spyOn(service, 'getCurrentBuildInfo');\n      getRemoteBuildInfoSpy = jest.spyOn(service, 'getRemoteBuildInfo');\n    });\n\n    it('should return true when current timestamp is less then remote', async () => {\n      getCurrentBuildInfoSpy.mockResolvedValueOnce({ timestamp: 1 });\n      getRemoteBuildInfoSpy.mockResolvedValueOnce({ timestamp: 2 });\n      expect(await service.isUpdatesAvailable()).toEqual(true);\n    });\n    it('should return true when no current timestamp but remote timestamp exists', async () => {\n      getCurrentBuildInfoSpy.mockResolvedValueOnce({});\n      getRemoteBuildInfoSpy.mockResolvedValueOnce({ timestamp: 2 });\n      expect(await service.isUpdatesAvailable()).toEqual(true);\n    });\n    it('should return false when no remote timestamp but has current', async () => {\n      getCurrentBuildInfoSpy.mockResolvedValueOnce({ timestamp: 2 });\n      getRemoteBuildInfoSpy.mockResolvedValueOnce({});\n      expect(await service.isUpdatesAvailable()).toEqual(false);\n    });\n    it('should return false when no remote and current timestamps', async () => {\n      getCurrentBuildInfoSpy.mockResolvedValueOnce({});\n      getRemoteBuildInfoSpy.mockResolvedValueOnce({});\n      expect(await service.isUpdatesAvailable()).toEqual(false);\n    });\n    it('should return false when remote is less then current', async () => {\n      getCurrentBuildInfoSpy.mockResolvedValueOnce({ timestamp: 2 });\n      getRemoteBuildInfoSpy.mockResolvedValueOnce({ timestamp: 1 });\n      expect(await service.isUpdatesAvailable()).toEqual(false);\n    });\n  });\n\n  describe('getRemoteBuildInfo', () => {\n    it('should return remote build info json', async () => {\n      const mockRemoteBuildInfo = { timestamp: 1 };\n      mockedAxios.get.mockResolvedValueOnce({\n        data: Buffer.from(JSON.stringify(mockRemoteBuildInfo)),\n      });\n      expect(await service.getRemoteBuildInfo()).toEqual(mockRemoteBuildInfo);\n    });\n    it('should return empty object on fail', async () => {\n      mockedAxios.get.mockRejectedValueOnce(new Error());\n      expect(await service.getRemoteBuildInfo()).toEqual({});\n    });\n  });\n\n  describe('getCurrentBuildInfo', () => {\n    it('should return current build info json', async () => {\n      const mockCurrentBuildInfo = { timestamp: 3 };\n      mockedFs.readFile.mockImplementationOnce(async () =>\n        Buffer.from(JSON.stringify(mockCurrentBuildInfo)),\n      );\n      expect(await service.getCurrentBuildInfo()).toEqual(mockCurrentBuildInfo);\n    });\n    it('should return empty object on fail', async () => {\n      mockedFs.readFile.mockImplementationOnce(async () => {\n        throw new Error();\n      });\n\n      expect(await service.getCurrentBuildInfo()).toEqual({});\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/statics-management/providers/auto-updated-statics.provider.ts",
    "content": "import { Injectable, Logger, OnModuleInit } from '@nestjs/common';\nimport * as fs from 'fs-extra';\nimport * as AdmZip from 'adm-zip';\nimport { URL } from 'url';\nimport { join } from 'path';\nimport { get } from 'lodash';\nimport { getFile } from 'src/utils';\n\nimport { IStaticsProviderOptions } from './auto-updated-statics.interface';\n\n@Injectable()\nexport class AutoUpdatedStaticsProvider implements OnModuleInit {\n  private readonly logger: Logger;\n\n  private readonly options: IStaticsProviderOptions;\n\n  constructor(options: IStaticsProviderOptions) {\n    this.logger = new Logger(options.name);\n    this.options = options;\n  }\n\n  /**\n   * Updates latest json on startup\n   */\n  async onModuleInit() {\n    if (this.options.initDefaults) {\n      // wait for populating default data (should take milliseconds)\n      await this.initDefaults().catch((e) =>\n        this.logger.warn('Unable to populate default data', e),\n      );\n    }\n\n    if (this.options.autoUpdate) {\n      // async operation to not wait for it and not block user in case when no internet connection\n      this.autoUpdate().catch();\n    }\n  }\n\n  /**\n   * Simply copy default files prepared during build to the home directory when no files there\n   */\n  async initDefaults() {\n    try {\n      if (\n        !(await fs.pathExists(\n          join(this.options.destinationPath, this.options.buildInfo),\n        ))\n      ) {\n        await fs.ensureDir(this.options.destinationPath);\n        await fs.copy(\n          this.options.defaultSourcePath,\n          this.options.destinationPath,\n          {\n            overwrite: true,\n          },\n        );\n      }\n    } catch (e) {\n      this.logger.error('Unable to create static files from default', e);\n    }\n  }\n\n  /**\n   * Update static files if needed\n   */\n  async autoUpdate() {\n    this.logger.debug('Checking for updates...');\n    if (!this.options.devMode && (await this.isUpdatesAvailable())) {\n      this.logger.debug('Updates available! Updating...');\n\n      try {\n        await this.updateStaticFiles();\n      } catch (e) {\n        this.logger.warn('Unable to update auto static files', e);\n      }\n    }\n  }\n\n  async updateStaticFiles(): Promise<void> {\n    const latestArchive = await this.getLatestArchive();\n\n    if (latestArchive) {\n      const zip = new AdmZip(latestArchive as Buffer);\n      await fs.remove(this.options.destinationPath);\n      await zip.extractAllTo(this.options.destinationPath, true);\n      await fs.writeFile(\n        join(this.options.destinationPath, this.options.buildInfo),\n        JSON.stringify(await this.getRemoteBuildInfo()),\n      );\n    }\n  }\n\n  /**\n   * Download archive from remote\n   */\n  async getLatestArchive() {\n    try {\n      return await getFile(\n        new URL(join(this.options.updateUrl, this.options.zip)).toString(),\n      );\n    } catch (e) {\n      this.logger.warn('Unable to get remote archive', e);\n      return null;\n    }\n  }\n\n  /**\n   * Compare current vs remote build timestamp to understand if update is available.\n   *\n   * Note: We decided to not use versioning (semver or similar)\n   */\n  async isUpdatesAvailable(): Promise<boolean> {\n    const currentBuildInfo = await this.getCurrentBuildInfo();\n    const remoteBuildInfo = await this.getRemoteBuildInfo();\n\n    return (\n      get(remoteBuildInfo, ['timestamp'], 0) >\n      get(currentBuildInfo, ['timestamp'], 0)\n    );\n  }\n\n  /**\n   * Get checksum for the remote latest version\n   */\n  async getRemoteBuildInfo(): Promise<Record<string, any>> {\n    try {\n      const buildInfoBuffer = await getFile(\n        new URL(\n          join(this.options.updateUrl, this.options.buildInfo),\n        ).toString(),\n      );\n      return JSON.parse(buildInfoBuffer.toString());\n    } catch (e) {\n      this.logger.warn('Unable to get remote build info', e);\n      return {};\n    }\n  }\n\n  /**\n   * Get checksum for the current version of statics\n   */\n  async getCurrentBuildInfo(): Promise<Record<string, any>> {\n    try {\n      return JSON.parse(\n        await fs.readFile(\n          join(this.options.destinationPath, this.options.buildInfo),\n          'utf8',\n        ),\n      );\n    } catch (e) {\n      this.logger.warn('Unable to get local checksum', e);\n      return {};\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/statics-management/statics-management.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { ServeStaticModule } from '@nestjs/serve-static';\nimport { join } from 'path';\nimport config, { Config } from 'src/utils/config';\nimport { Response } from 'express';\nimport { AutoUpdatedStaticsProvider } from './providers/auto-updated-statics.provider';\n\nconst SERVER_CONFIG = config.get('server') as Config['server'];\nconst PATH_CONFIG = config.get('dir_path') as Config['dir_path'];\nconst TUTORIALS_CONFIG = config.get('tutorials') as Config['tutorials'];\n\nconst CONTENT_CONFIG = config.get('content');\n\nconst setXFrameOptionsHeader = (res: Response) => {\n  res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n};\n\nconst downloadableStaticFiles = (res: Response) => {\n  if (res.req?.query?.download === 'true') {\n    res.setHeader('Content-Type', 'application/octet-stream');\n    res.setHeader('Content-Disposition', 'attachment;');\n    res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n  }\n};\n\n@Module({})\nexport class StaticsManagementModule {\n  static register({ autoUpdate, initDefaults }): DynamicModule {\n    return {\n      module: StaticsManagementModule,\n      imports: [\n        ServeStaticModule.forRoot({\n          serveRoot: SERVER_CONFIG.tutorialsUri,\n          rootPath: join(PATH_CONFIG.tutorials),\n          serveStaticOptions: {\n            fallthrough: false,\n            setHeaders: downloadableStaticFiles,\n          },\n        }),\n        ServeStaticModule.forRoot({\n          serveRoot: SERVER_CONFIG.customTutorialsUri,\n          rootPath: join(PATH_CONFIG.customTutorials),\n          serveStaticOptions: {\n            fallthrough: false,\n            setHeaders: downloadableStaticFiles,\n          },\n        }),\n        ServeStaticModule.forRoot({\n          serveRoot: SERVER_CONFIG.contentUri,\n          rootPath: join(PATH_CONFIG.content),\n          serveStaticOptions: {\n            fallthrough: false,\n            setHeaders: setXFrameOptionsHeader,\n          },\n        }),\n        ServeStaticModule.forRoot({\n          serveRoot: SERVER_CONFIG.defaultPluginsUri,\n          rootPath: join(PATH_CONFIG.defaultPlugins),\n          serveStaticOptions: {\n            fallthrough: false,\n            setHeaders: setXFrameOptionsHeader,\n          },\n        }),\n        ServeStaticModule.forRoot({\n          serveRoot: SERVER_CONFIG.customPluginsUri,\n          rootPath: join(PATH_CONFIG.customPlugins),\n          serveStaticOptions: {\n            fallthrough: false,\n            setHeaders: setXFrameOptionsHeader,\n          },\n        }),\n        ServeStaticModule.forRoot({\n          serveRoot: SERVER_CONFIG.staticUri,\n          rootPath: join(PATH_CONFIG.staticDir),\n          serveStaticOptions: {\n            fallthrough: false,\n            setHeaders: setXFrameOptionsHeader,\n          },\n        }),\n        ...(SERVER_CONFIG.staticContent\n          ? [\n              ServeStaticModule.forRoot({\n                rootPath: join(\n                  __dirname,\n                  '..',\n                  '..',\n                  '..',\n                  '..',\n                  '..',\n                  'ui',\n                  'dist',\n                ),\n                exclude: [\n                  '/api/{*splat}',\n                  `${SERVER_CONFIG.customPluginsUri}/{*splat}`,\n                  `${SERVER_CONFIG.staticUri}/{*splat}`,\n                ],\n                serveRoot: SERVER_CONFIG.proxyPath\n                  ? `/${SERVER_CONFIG.proxyPath}`\n                  : '',\n                serveStaticOptions: {\n                  index: false,\n                  setHeaders: setXFrameOptionsHeader,\n                },\n              }),\n            ]\n          : []),\n      ],\n      providers: [\n        {\n          provide: 'TutorialsProvider',\n          useFactory: () =>\n            new AutoUpdatedStaticsProvider({\n              name: 'TutorialsProvider',\n              destinationPath: PATH_CONFIG.tutorials,\n              defaultSourcePath: PATH_CONFIG.defaultTutorials,\n              autoUpdate,\n              initDefaults,\n              ...TUTORIALS_CONFIG,\n            }),\n        },\n        {\n          provide: 'ContentProvider',\n          useFactory: () =>\n            new AutoUpdatedStaticsProvider({\n              name: 'ContentProvider',\n              destinationPath: PATH_CONFIG.content,\n              defaultSourcePath: PATH_CONFIG.defaultContent,\n              autoUpdate,\n              initDefaults,\n              ...CONTENT_CONFIG,\n            }),\n        },\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/dto/create-tag.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString, Length, Matches } from 'class-validator';\n\nexport class CreateTagDto {\n  @ApiProperty({\n    description: 'Key of the tag.',\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  @Length(1, 64)\n  @Matches(/^[a-zA-Z0-9\\-_\\.@:+ ]+$/)\n  key: string;\n\n  @ApiProperty({\n    description: 'Value of the tag.',\n    type: String,\n  })\n  @IsNotEmpty()\n  @IsString()\n  @Length(1, 128)\n  @Matches(/^[a-zA-Z0-9\\-_\\.@:+ ]+$/)\n  value: string;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/dto/index.ts",
    "content": "export * from './create-tag.dto';\nexport * from './update-tag.dto';\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/dto/update-tag.dto.ts",
    "content": "import { PartialType } from '@nestjs/swagger';\n\nimport { CreateTagDto } from './create-tag.dto';\n\nexport class UpdateTagDto extends PartialType(CreateTagDto) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/entities/tag.entity.ts",
    "content": "import {\n  Column,\n  Entity,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  Unique,\n  ManyToMany,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\n\n@Entity('tag')\n@Unique(['key', 'value'])\nexport class TagEntity {\n  @Expose()\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Expose()\n  @Column()\n  key: string;\n\n  @Expose()\n  @Column()\n  value: string;\n\n  @Expose()\n  @CreateDateColumn()\n  createdAt: Date;\n\n  @Expose()\n  @UpdateDateColumn()\n  updatedAt: Date;\n\n  @Column({ nullable: true })\n  @Expose({ groups: ['security'] })\n  encryption: string;\n\n  @Expose()\n  @ManyToMany(() => DatabaseEntity, (database) => database.tags, {\n    onDelete: 'CASCADE',\n  })\n  databases: DatabaseEntity[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/models/tag.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class Tag {\n  @ApiProperty({\n    description: 'Tag id.',\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Key of the tag.',\n    type: String,\n  })\n  @Expose()\n  key: string;\n\n  @ApiProperty({\n    description: 'Value of the tag.',\n    type: String,\n  })\n  @Expose()\n  value: string;\n\n  @ApiProperty({\n    description: 'Creation date of the tag.',\n    type: String,\n    format: 'date-time',\n    example: '2025-03-05T08:54:53.322Z',\n  })\n  @Expose()\n  createdAt: Date;\n\n  @ApiProperty({\n    description: 'Last update date of the tag.',\n    type: String,\n    format: 'date-time',\n  })\n  @Expose()\n  updatedAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/repository/local.tag.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { LocalTagRepository } from './local.tag.repository';\nimport { TagEntity } from '../entities/tag.entity';\nimport { Tag } from '../models/tag';\nimport { classToClass } from 'src/utils';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { mockEncryptionService, MockType } from 'src/__mocks__';\n\nconst mockTagEntities = [\n  { id: '1', key: 'key1', value: 'value1' },\n  { id: '2', key: 'key2', value: 'value2' },\n].map((tag) => Object.assign(new TagEntity(), tag));\n\ndescribe('LocalTagRepository', () => {\n  let repository: LocalTagRepository;\n  let tagRepository: Repository<TagEntity>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalTagRepository,\n        {\n          provide: getRepositoryToken(TagEntity),\n          useClass: Repository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    repository = module.get<LocalTagRepository>(LocalTagRepository);\n    tagRepository = module.get<Repository<TagEntity>>(\n      getRepositoryToken(TagEntity),\n    );\n    encryptionService = await module.get(EncryptionService);\n\n    encryptionService.encrypt.mockImplementation(async (data) => ({\n      data,\n      encryption: 'KEYTAR',\n    }));\n    encryptionService.decrypt.mockImplementation(async (data) => data);\n  });\n\n  it('should be defined', () => {\n    expect(repository).toBeDefined();\n  });\n\n  describe('list', () => {\n    it('should return a list of tags', async () => {\n      jest.spyOn(tagRepository, 'find').mockResolvedValue(mockTagEntities);\n\n      const result = await repository.list();\n      expect(result).toEqual(\n        mockTagEntities.map((entity) => classToClass(Tag, entity)),\n      );\n    });\n  });\n\n  describe('get', () => {\n    it('should return a tag by id', async () => {\n      jest\n        .spyOn(tagRepository, 'findOneBy')\n        .mockResolvedValue(mockTagEntities[0]);\n\n      const result = await repository.get('1');\n      expect(result).toEqual(classToClass(Tag, mockTagEntities[0]));\n    });\n  });\n\n  describe('getByKeyValuePair', () => {\n    it('should return a tag by key-value pair', async () => {\n      jest\n        .spyOn(tagRepository, 'findOneBy')\n        .mockResolvedValue(mockTagEntities[0]);\n\n      const result = await repository.getByKeyValuePair('key1', 'value1');\n      expect(result).toEqual(classToClass(Tag, mockTagEntities[0]));\n    });\n  });\n\n  describe('create', () => {\n    it('should create a new tag', async () => {\n      const tag = { id: '1', key: 'key1', value: 'value1' } as Tag;\n      const tagEntity = classToClass(TagEntity, tag);\n      jest.spyOn(tagRepository, 'save').mockResolvedValue(tagEntity);\n\n      const result = await repository.create(tag);\n      expect(result).toEqual(classToClass(Tag, tagEntity));\n    });\n  });\n\n  describe('update', () => {\n    it('should update a tag', async () => {\n      const tag = { key: 'updatedKey', value: 'updatedValue' } as Partial<Tag>;\n      const tagEntity = classToClass(TagEntity, tag);\n      jest.spyOn(tagRepository, 'update').mockResolvedValue(undefined);\n\n      await repository.update('1', tag);\n      expect(tagRepository.update).toHaveBeenCalledWith(\n        '1',\n        expect.objectContaining({ ...tagEntity, encryption: 'KEYTAR' }),\n      );\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a tag', async () => {\n      jest.spyOn(tagRepository, 'delete').mockResolvedValue(undefined);\n\n      await repository.delete('1');\n      expect(tagRepository.delete).toHaveBeenCalledWith('1');\n    });\n  });\n\n  describe('getOrCreateByKeyValuePairs', () => {\n    it('should return existing tags or create new ones', async () => {\n      const keyValuePairs = [\n        { key: 'key1', value: 'value1' },\n        { key: 'key2', value: 'value2' },\n      ];\n      const tags = mockTagEntities.map((entity) => classToClass(Tag, entity));\n      jest\n        .spyOn(repository, 'getByKeyValuePair')\n        .mockImplementation(async (key, value) => {\n          return tags.find((tag) => tag.key === key && tag.value === value);\n        });\n      jest\n        .spyOn(repository, 'create')\n        .mockImplementation(async (tag) => classToClass(Tag, tag));\n\n      const result = await repository.getOrCreateByKeyValuePairs(keyValuePairs);\n      expect(result).toEqual(tags);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/repository/local.tag.repository.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { classToClass } from 'src/utils';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { TagEntity } from '../entities/tag.entity';\nimport { TagRepository } from './tag.repository';\nimport { Tag } from '../models/tag';\n\nexport const TAG_FIELDS_TO_ENCRYPT = ['key', 'value'];\n\n@Injectable()\nexport class LocalTagRepository implements TagRepository {\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(TagEntity)\n    private readonly repository: Repository<TagEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    this.modelEncryptor = new ModelEncryptor(\n      encryptionService,\n      TAG_FIELDS_TO_ENCRYPT,\n    );\n  }\n\n  async list(): Promise<Tag[]> {\n    const entities = await this.repository.find();\n    const decrypted = await this.modelEncryptor.decryptEntities(entities);\n\n    return decrypted.map((entity) => classToClass(Tag, entity));\n  }\n\n  async get(id: string): Promise<Tag> {\n    const entity = await this.repository.findOneBy({ id });\n    const decrypted = await this.modelEncryptor.decryptEntity(entity);\n\n    return classToClass(Tag, decrypted);\n  }\n\n  async getByKeyValuePair(key: string, value: string): Promise<Tag> {\n    const encrypted = await this.modelEncryptor.encryptEntity({\n      key,\n      value,\n    } as TagEntity);\n\n    const entity = await this.repository.findOneBy({\n      key: encrypted.key,\n      value: encrypted.value,\n    });\n\n    return classToClass(Tag, await this.modelEncryptor.decryptEntity(entity));\n  }\n\n  async create(tag: Tag): Promise<Tag> {\n    const entity = classToClass(TagEntity, tag);\n    const encrypted = await this.modelEncryptor.encryptEntity(entity);\n    const createdEntity = await this.repository.save(encrypted);\n\n    return classToClass(\n      Tag,\n      await this.modelEncryptor.decryptEntity(createdEntity),\n    );\n  }\n\n  async update(id: string, tag: Partial<Tag>): Promise<void> {\n    const entity = classToClass(TagEntity, tag);\n    const encrypted = await this.modelEncryptor.encryptEntity(entity);\n\n    await this.repository.update(id, encrypted);\n  }\n\n  async delete(id: string): Promise<void> {\n    await this.repository.delete(id);\n  }\n\n  public async getOrCreateByKeyValuePairs(\n    keyValuePairs: { key: string; value: string }[],\n  ): Promise<Tag[]> {\n    if (!keyValuePairs?.length) {\n      return [];\n    }\n\n    return await Promise.all(\n      keyValuePairs.map(async ({ key, value }) => {\n        const found = await this.getByKeyValuePair(key, value);\n\n        if (found) {\n          return found;\n        }\n\n        return await this.create({ key, value } as Tag);\n      }),\n    );\n  }\n\n  async cleanupUnusedTags(): Promise<void> {\n    await this.repository\n      .createQueryBuilder('tag')\n      .leftJoin('tag.databases', 'database')\n      .where((qb) => {\n        const subQuery = qb\n          .subQuery()\n          .select('tag.id')\n          .from('tag', 'tag')\n          .leftJoin('tag.databases', 'database')\n          .groupBy('tag.id')\n          .having('COUNT(database.id) = 0')\n          .getQuery();\n\n        return `tag.id IN ${subQuery}`;\n      })\n      .delete()\n      .execute();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/repository/tag.repository.ts",
    "content": "import { Tag } from '../models/tag';\n\nexport abstract class TagRepository {\n  /**\n   * List all tags.\n   * @returns {Promise<Tag[]>} A promise that resolves to an array of tags.\n   */\n  abstract list(): Promise<Tag[]>;\n\n  /**\n   * Get a tag by its ID.\n   * @param {string} id - The ID of the tag.\n   * @returns {Promise<Tag>} A promise that resolves to the tag.\n   */\n  abstract get(id: string): Promise<Tag>;\n\n  /**\n   * Get a tag by its key and value.\n   * @param {string} key - The key of the tag.\n   * @param {string} value - The value of the tag.\n   * @returns {Promise<Tag>} A promise that resolves to the tag.\n   */\n  abstract getByKeyValuePair(key: string, value: string): Promise<Tag>;\n\n  /**\n   * Create a new tag.\n   * @param {Tag} tag - The tag to create.\n   * @returns {Promise<Tag>} A promise that resolves to the created tag.\n   */\n  abstract create(tag: Tag): Promise<Tag>;\n\n  /**\n   * Update an existing tag.\n   * @param {string} id - The ID of the tag to update.\n   * @param {Partial<Tag>} tag - The updated tag data.\n   * @returns {Promise<void>} A promise that resolves when the tag is updated.\n   */\n  abstract update(id: string, tag: Partial<Tag>): Promise<void>;\n\n  /**\n   * Delete a tag by its ID.\n   * @param {string} id - The ID of the tag to delete.\n   * @returns {Promise<void>} A promise that resolves when the tag is deleted.\n   */\n  abstract delete(id: string): Promise<void>;\n\n  /**\n   * Get or create tags by key-value pairs.\n   * @param {Array<{ key: string; value: string }>} keyValuePairs - An array of key-value pairs.\n   * @returns {Promise<Tag[]>} A promise that resolves to an array of tags.\n   * */\n  public abstract getOrCreateByKeyValuePairs(\n    keyValuePairs: { key: string; value: string }[],\n  ): Promise<Tag[]>;\n\n  /**\n   * Cleanup unused tags.\n   * @returns {Promise<void>} A promise that resolves when the cleanup is complete.\n   */\n  abstract cleanupUnusedTags(): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/tag.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Patch,\n  Post,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { TagService } from './tag.service';\nimport { CreateTagDto, UpdateTagDto } from './dto';\nimport { Tag } from './models/tag';\nimport { ApiTags } from '@nestjs/swagger';\n\n@ApiTags('TAGS')\n@Controller('tags')\n@UsePipes(new ValidationPipe({ transform: true }))\nexport class TagController {\n  constructor(private readonly tagService: TagService) {}\n\n  @Get()\n  @ApiEndpoint({\n    description: 'Get tags list',\n    responses: [{ status: 200, isArray: true, type: Tag }],\n  })\n  async list(): Promise<Tag[]> {\n    return this.tagService.list();\n  }\n\n  @Get(':id')\n  @ApiEndpoint({\n    description: 'Get tag by id',\n    responses: [{ status: 200, type: Tag }],\n  })\n  async get(@Param('id') id: string): Promise<Tag> {\n    return this.tagService.get(id);\n  }\n\n  @Post()\n  @ApiEndpoint({\n    description: 'Create tag',\n    statusCode: 201,\n    responses: [{ status: 201, type: Tag }],\n  })\n  async create(@Body() createTagDto: CreateTagDto): Promise<Tag> {\n    return this.tagService.create(createTagDto);\n  }\n\n  @Patch(':id')\n  @ApiEndpoint({\n    description: 'Update tag',\n    responses: [{ status: 200, type: Tag }],\n  })\n  async update(\n    @Param('id') id: string,\n    @Body() updateTagDto: UpdateTagDto,\n  ): Promise<Tag> {\n    return this.tagService.update(id, updateTagDto);\n  }\n\n  @Delete(':id')\n  @ApiEndpoint({\n    description: 'Delete tag',\n    responses: [{ status: 200 }],\n  })\n  async delete(@Param('id') id: string): Promise<void> {\n    return this.tagService.delete(id);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/tag.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TagService } from './tag.service';\nimport { TagController } from './tag.controller';\nimport { TagRepository } from './repository/tag.repository';\nimport { LocalTagRepository } from './repository/local.tag.repository';\n\n@Module({\n  controllers: [TagController],\n  providers: [\n    TagService,\n    {\n      provide: TagRepository,\n      useClass: LocalTagRepository,\n    },\n  ],\n  exports: [TagService, TagRepository],\n})\nexport class TagModule {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/tag.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { TagService } from './tag.service';\nimport { TagRepository } from './repository/tag.repository';\nimport { Tag } from './models/tag';\nimport { NotFoundException, ConflictException } from '@nestjs/common';\nimport {\n  mockTags,\n  createTagDto,\n  mockTagsRepository,\n  updateTagDto,\n} from 'src/__mocks__';\n\ndescribe('TagService', () => {\n  let service: TagService;\n  let repository: TagRepository;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        TagService,\n        {\n          provide: TagRepository,\n          useFactory: mockTagsRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<TagService>(TagService);\n    repository = module.get<TagRepository>(TagRepository);\n  });\n\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  describe('create', () => {\n    it('should create a new tag', async () => {\n      const tag: Tag = {\n        ...mockTags[0],\n        ...createTagDto,\n      };\n      jest.spyOn(repository, 'create').mockResolvedValue(tag);\n\n      const result = await service.create(createTagDto);\n      expect(result).toEqual(tag);\n      expect(repository.create).toHaveBeenCalledWith(expect.any(Tag));\n    });\n\n    it('should throw ConflictException if tag already exists', async () => {\n      jest.spyOn(repository, 'create').mockRejectedValue({ code: '2067' });\n\n      await expect(service.create(createTagDto)).rejects.toThrow(\n        ConflictException,\n      );\n    });\n  });\n\n  describe('list', () => {\n    it('should return a list of tags', async () => {\n      jest.spyOn(repository, 'list').mockResolvedValue(mockTags);\n\n      const result = await service.list();\n      expect(result).toEqual(mockTags);\n    });\n  });\n\n  describe('get', () => {\n    it('should return a tag by id', async () => {\n      const tag: Tag = mockTags[0];\n      jest.spyOn(repository, 'get').mockResolvedValue(tag);\n\n      const result = await service.get('1');\n      expect(result).toEqual(tag);\n    });\n\n    it('should throw NotFoundException if tag not found', async () => {\n      jest.spyOn(repository, 'get').mockResolvedValue(null);\n\n      await expect(service.get('1')).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('getByKeyValuePair', () => {\n    it('should return a tag by key-value pair', async () => {\n      const tag: Tag = mockTags[0];\n      jest.spyOn(repository, 'getByKeyValuePair').mockResolvedValue(tag);\n\n      const result = await service.getByKeyValuePair(tag.key, tag.value);\n      expect(result).toEqual(tag);\n    });\n\n    it('should throw NotFoundException if tag not found', async () => {\n      jest.spyOn(repository, 'getByKeyValuePair').mockResolvedValue(null);\n\n      await expect(service.getByKeyValuePair('key1', 'value1')).rejects.toThrow(\n        NotFoundException,\n      );\n    });\n  });\n\n  describe('getOrCreateByKeyValuePairs', () => {\n    it('should return existing tags or create new ones', async () => {\n      const keyValuePairs = [\n        { key: 'key1', value: 'value1' },\n        { key: 'key2', value: 'value2' },\n      ];\n      const tags = [mockTags[0], mockTags[1]];\n      jest\n        .spyOn(repository, 'getOrCreateByKeyValuePairs')\n        .mockResolvedValue(tags);\n\n      const result = await service.getOrCreateByKeyValuePairs(keyValuePairs);\n      expect(result).toEqual(tags);\n    });\n  });\n\n  describe('update', () => {\n    it('should update a tag', async () => {\n      const tag: Tag = {\n        id: '1',\n        ...updateTagDto,\n        createdAt: new Date(),\n        updatedAt: new Date(),\n        databases: [],\n      } as Tag;\n      jest.spyOn(repository, 'update').mockResolvedValue(undefined);\n      jest.spyOn(repository, 'get').mockResolvedValue(tag);\n\n      const result = await service.update('1', updateTagDto);\n      expect(result).toEqual(tag);\n      expect(repository.update).toHaveBeenCalledWith('1', updateTagDto);\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a tag', async () => {\n      await service.delete('1');\n      expect(repository.delete).toHaveBeenCalledWith('1');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/tag/tag.service.ts",
    "content": "import {\n  Injectable,\n  Logger,\n  NotFoundException,\n  ConflictException,\n} from '@nestjs/common';\nimport { TagRepository } from './repository/tag.repository';\nimport { CreateTagDto, UpdateTagDto } from './dto';\nimport { Tag } from './models/tag';\nimport { classToClass } from 'src/utils';\n\n@Injectable()\nexport class TagService {\n  private logger = new Logger('RdiService');\n\n  constructor(private readonly tagRepository: TagRepository) {}\n\n  async create(createTagDto: CreateTagDto): Promise<Tag> {\n    const model = classToClass(Tag, createTagDto);\n    try {\n      const tag = await this.tagRepository.create(model);\n      this.logger.debug('Successfully created tag', tag);\n      return tag;\n    } catch (error) {\n      if (error.code === '2067') {\n        // Unique violation error code for SQLite (SQLITE_CONSTRAINT_UNIQUE)\n        throw new ConflictException(\n          `Tag with key ${createTagDto.key} and value ${createTagDto.value} already exists`,\n        );\n      }\n      throw error;\n    }\n  }\n\n  async list(): Promise<Tag[]> {\n    return this.tagRepository.list();\n  }\n\n  async get(id: string): Promise<Tag> {\n    const tag = await this.tagRepository.get(id);\n\n    if (!tag) {\n      throw new NotFoundException(`Tag with id ${id} not found`);\n    }\n\n    return tag;\n  }\n\n  async getByKeyValuePair(key: string, value: string): Promise<Tag> {\n    const tag = await this.tagRepository.getByKeyValuePair(key, value);\n\n    if (!tag) {\n      throw new NotFoundException(\n        `Tag with key ${key} and value ${value} not found`,\n      );\n    }\n\n    return tag;\n  }\n\n  async getOrCreateByKeyValuePairs(\n    keyValuePairs: CreateTagDto[],\n  ): Promise<Tag[]> {\n    return this.tagRepository.getOrCreateByKeyValuePairs(keyValuePairs);\n  }\n\n  async update(id: string, updateTagDto: UpdateTagDto): Promise<Tag> {\n    await this.tagRepository.update(id, updateTagDto);\n\n    this.logger.debug('Successfully updated tag', { id, updateTagDto });\n\n    return this.get(id);\n  }\n\n  async delete(id: string): Promise<void> {\n    await this.tagRepository.delete(id);\n\n    this.logger.debug('Successfully deleted tag', { id });\n  }\n\n  async cleanupUnusedTags(): Promise<void> {\n    await this.tagRepository.cleanupUnusedTags();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/decorators/workbench-client-metadata.decorator.ts",
    "content": "import { API_PARAM_DATABASE_ID } from 'src/common/constants';\nimport { createParamDecorator } from '@nestjs/common';\nimport { ClientContext } from 'src/common/models';\nimport { clientMetadataParamFactory } from 'src/common/decorators';\n\nexport const WorkbenchClientMetadata = (\n  databaseIdParam = API_PARAM_DATABASE_ID,\n) =>\n  createParamDecorator(clientMetadataParamFactory)({\n    context: ClientContext.Workbench,\n    databaseIdParam,\n  });\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { CommandExecution } from 'src/modules/workbench/models/command-execution';\n\nexport class CreateCommandExecutionDto extends PickType(CommandExecution, [\n  'command',\n  'mode',\n  'resultsMode',\n  'type',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts",
    "content": "import { ApiProperty, PickType } from '@nestjs/swagger';\nimport { IsArray, IsDefined, IsString, ArrayNotEmpty } from 'class-validator';\nimport { CommandExecution } from 'src/modules/workbench/models/command-execution';\nimport { Expose } from 'class-transformer';\n\nexport class CreateCommandExecutionsDto extends PickType(CommandExecution, [\n  'mode',\n  'resultsMode',\n  'type',\n] as const) {\n  @ApiProperty({\n    isArray: true,\n    type: String,\n    description: 'Redis commands',\n  })\n  @Expose()\n  @IsArray()\n  @ArrayNotEmpty()\n  @IsDefined()\n  @IsString({ each: true })\n  commands: string[];\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/dto/create-plugin-state.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotIn } from 'class-validator';\n\nexport class CreatePluginStateDto {\n  @ApiProperty({\n    type: String,\n    example: 'any',\n    description: 'State can be anything except \"undefined\"',\n  })\n  @IsNotIn([undefined], {\n    message: 'state should be defined',\n  })\n  state: any;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  Index,\n} from 'typeorm';\nimport { DatabaseEntity } from 'src/modules/database/entities/database.entity';\nimport { Expose } from 'class-transformer';\nimport { IsInt, Min } from 'class-validator';\nimport {\n  CommandExecutionType,\n  ResultsMode,\n  RunQueryMode,\n} from 'src/modules/workbench/models/command-execution';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('command_execution')\nexport class CommandExecutionEntity {\n  @PrimaryGeneratedColumn('uuid')\n  @Expose()\n  id: string;\n\n  @Column({ nullable: false })\n  @Expose()\n  databaseId: string;\n\n  @ManyToOne(() => DatabaseEntity, {\n    nullable: false,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn({ name: 'databaseId' })\n  @Expose()\n  database: DatabaseEntity;\n\n  @Column({ nullable: false, type: 'text' })\n  @Expose()\n  command: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  mode?: string = RunQueryMode.ASCII;\n\n  @Column({ nullable: false, type: 'text' })\n  @DataAsJsonString()\n  @Expose()\n  result: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  role?: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  resultsMode?: string = ResultsMode.Default;\n\n  @Column({ nullable: true })\n  @DataAsJsonString()\n  @Expose()\n  summary?: string;\n\n  @Column({ nullable: true })\n  @DataAsJsonString()\n  @Expose()\n  nodeOptions?: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @Column({ nullable: true })\n  @Expose()\n  executionTime?: number;\n\n  @Column({ nullable: true })\n  @Expose()\n  @IsInt()\n  @Min(0)\n  db?: number;\n\n  @Column({ nullable: false, default: CommandExecutionType.Workbench })\n  @Expose()\n  type?: string = CommandExecutionType.Workbench;\n\n  @CreateDateColumn()\n  @Index()\n  @Expose()\n  createdAt: Date;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/entities/plugin-state.entity.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  JoinColumn,\n  UpdateDateColumn,\n  PrimaryColumn,\n} from 'typeorm';\nimport { Expose } from 'class-transformer';\nimport { CommandExecutionEntity } from 'src/modules/workbench/entities/command-execution.entity';\nimport { DataAsJsonString } from 'src/common/decorators';\n\n@Entity('plugin_state')\nexport class PluginStateEntity {\n  @PrimaryColumn()\n  @Expose()\n  commandExecutionId: string;\n\n  @ManyToOne(() => CommandExecutionEntity, {\n    nullable: false,\n    onDelete: 'CASCADE',\n  })\n  @JoinColumn({ name: 'commandExecutionId' })\n  commandExecution: CommandExecutionEntity;\n\n  @PrimaryColumn()\n  @Expose()\n  visualizationId: string;\n\n  @Column({ nullable: false, type: 'text' })\n  @DataAsJsonString()\n  @Expose()\n  state: string;\n\n  @Column({ nullable: true })\n  encryption: string;\n\n  @CreateDateColumn()\n  @Expose()\n  createdAt: Date;\n\n  @UpdateDateColumn()\n  @Expose()\n  updatedAt: Date;\n\n  constructor(entity: Partial<PluginStateEntity>) {\n    Object.assign(this, entity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/models/command-execution-result.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { Expose } from 'class-transformer';\n\nexport class CommandExecutionResult {\n  @ApiProperty({\n    description: 'Redis CLI command execution status',\n    default: CommandExecutionStatus.Success,\n    enum: CommandExecutionStatus,\n  })\n  @Expose()\n  status: CommandExecutionStatus;\n\n  @ApiProperty({\n    type: String,\n    description: 'Redis response',\n  })\n  @Expose()\n  response: any;\n\n  @ApiProperty({\n    type: Boolean,\n    description:\n      'Flag showing if response was replaced with message notification about response size limit threshold',\n  })\n  @Expose()\n  sizeLimitExceeded?: boolean;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/models/command-execution.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport {\n  IsDefined,\n  IsEnum,\n  IsInt,\n  IsNotEmpty,\n  IsOptional,\n  IsString,\n  Min,\n} from 'class-validator';\nimport { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result';\nimport { Expose, Type } from 'class-transformer';\nimport { Default as DefaultDecorator } from 'src/common/decorators';\n\nexport enum RunQueryMode {\n  Raw = 'RAW',\n  ASCII = 'ASCII',\n}\n\nexport enum ResultsMode {\n  Default = 'DEFAULT',\n  GroupMode = 'GROUP_MODE',\n  Silent = 'SILENT',\n}\n\nexport enum CommandExecutionType {\n  Workbench = 'WORKBENCH',\n  Search = 'SEARCH',\n}\n\nexport class ResultsSummary {\n  @ApiProperty({\n    description: 'Total number of commands executed',\n    type: Number,\n  })\n  @IsDefined()\n  total: number;\n\n  @ApiProperty({\n    description: 'Total number of successful commands executed',\n    type: Number,\n  })\n  @IsDefined()\n  success: number;\n\n  @ApiProperty({\n    description: 'Total number of failed commands executed',\n    type: Number,\n  })\n  @IsDefined()\n  fail: number;\n}\n\nexport class CommandExecution {\n  @ApiProperty({\n    description: 'Command execution id',\n    type: String,\n  })\n  @Expose()\n  id: string;\n\n  @ApiProperty({\n    description: 'Database id',\n    type: String,\n  })\n  @Expose()\n  databaseId: string;\n\n  @ApiProperty({\n    description: 'Redis command',\n    type: String,\n  })\n  @IsString()\n  @IsNotEmpty()\n  @Expose()\n  command: string;\n\n  @ApiPropertyOptional({\n    description: 'Workbench mode',\n    default: RunQueryMode.ASCII,\n    enum: RunQueryMode,\n  })\n  @Expose()\n  @IsOptional()\n  @IsEnum(RunQueryMode, {\n    message: `mode must be a valid enum value. Valid values: ${Object.values(\n      RunQueryMode,\n    )}.`,\n  })\n  @DefaultDecorator(RunQueryMode.ASCII)\n  mode?: RunQueryMode;\n\n  @ApiPropertyOptional({\n    description: 'Workbench result mode',\n    default: ResultsMode.Default,\n    enum: ResultsMode,\n  })\n  @Expose()\n  @IsOptional()\n  @IsEnum(ResultsMode, {\n    message: `resultsMode must be a valid enum value. Valid values: ${Object.values(\n      ResultsMode,\n    )}.`,\n  })\n  @DefaultDecorator(ResultsMode.Default)\n  resultsMode?: ResultsMode;\n\n  @ApiPropertyOptional({\n    description: 'Workbench executions summary',\n    type: () => ResultsSummary,\n  })\n  @Expose()\n  summary?: ResultsSummary;\n\n  @ApiProperty({\n    description: 'Command execution result',\n    type: () => CommandExecutionResult,\n    isArray: true,\n  })\n  @Type(() => CommandExecutionResult)\n  @Expose()\n  result: CommandExecutionResult[];\n\n  @ApiPropertyOptional({\n    description: 'Result did not stored in db',\n    type: Boolean,\n  })\n  @Expose()\n  isNotStored?: boolean;\n\n  @ApiProperty({\n    description: 'Date of command execution',\n    type: Date,\n  })\n  @Expose()\n  createdAt: Date;\n\n  @ApiPropertyOptional({\n    description: 'Workbench command execution time',\n    type: Number,\n  })\n  @Expose()\n  executionTime?: number;\n\n  @ApiPropertyOptional({\n    description: 'Logical database number.',\n    type: Number,\n  })\n  @Expose()\n  @IsInt()\n  @Min(0)\n  @IsOptional()\n  db?: number;\n\n  @ApiPropertyOptional({\n    description:\n      'Command execution type. Used to distinguish between search and workbench',\n    default: CommandExecutionType.Workbench,\n    enum: CommandExecutionType,\n  })\n  @Expose()\n  @IsOptional()\n  @IsEnum(CommandExecutionType, {\n    message: `type must be a valid enum value. Valid values: ${Object.values(\n      CommandExecutionType,\n    )}.`,\n  })\n  @DefaultDecorator(CommandExecutionType.Workbench)\n  type?: CommandExecutionType;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/models/command-executions.filter.ts",
    "content": "import { PickType } from '@nestjs/swagger';\nimport { CommandExecution } from 'src/modules/workbench/models/command-execution';\n\nexport class CommandExecutionFilter extends PickType(CommandExecution, [\n  'type',\n] as const) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/models/plugin-command-execution.ts",
    "content": "import { CommandExecution } from 'src/modules/workbench/models/command-execution';\nimport { OmitType, PartialType } from '@nestjs/swagger';\n\nexport class PluginCommandExecution extends PartialType(\n  OmitType(CommandExecution, ['createdAt', 'id'] as const),\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/models/plugin-state.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Expose } from 'class-transformer';\n\nexport class PluginState {\n  @ApiProperty({\n    description: 'Plugin visualization id. Should be unique per all plugins',\n    type: String,\n  })\n  @Expose()\n  visualizationId: string;\n\n  @ApiProperty({\n    description: 'Command Execution id',\n    type: String,\n  })\n  @Expose()\n  commandExecutionId: string;\n\n  @ApiProperty({\n    type: String,\n    example: 'any',\n    description: 'Stored state',\n  })\n  @Expose()\n  state: any;\n\n  @ApiProperty({\n    description: 'Date of creation',\n    type: Date,\n  })\n  @Expose()\n  createdAt: Date;\n\n  @ApiProperty({\n    description: 'Date of updating',\n    type: Date,\n  })\n  @Expose()\n  updatedAt: Date;\n\n  constructor(partial: Partial<PluginState> = {}) {\n    Object.assign(this, partial);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/models/short-command-execution.ts",
    "content": "import { CommandExecution } from 'src/modules/workbench/models/command-execution';\nimport { OmitType, PartialType } from '@nestjs/swagger';\n\nexport class ShortCommandExecution extends PartialType(\n  OmitType(CommandExecution, ['result'] as const),\n) {}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/plugins.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Get,\n  Param,\n  Post,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto';\nimport { PluginsService } from 'src/modules/workbench/plugins.service';\nimport { PluginCommandExecution } from 'src/modules/workbench/models/plugin-command-execution';\nimport { CreatePluginStateDto } from 'src/modules/workbench/dto/create-plugin-state.dto';\nimport { PluginState } from 'src/modules/workbench/models/plugin-state';\nimport { ClientMetadata } from 'src/common/models';\nimport { WorkbenchClientMetadata } from 'src/modules/workbench/decorators/workbench-client-metadata.decorator';\n\n@ApiTags('Plugins')\n@UsePipes(new ValidationPipe({ transform: true }))\n@Controller('plugins')\nexport class PluginsController {\n  constructor(private service: PluginsService) {}\n\n  @ApiEndpoint({\n    description: 'Send Redis Command from the Workbench',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: PluginCommandExecution,\n      },\n    ],\n  })\n  @Post('/command-executions')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async sendCommand(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateCommandExecutionDto,\n  ): Promise<PluginCommandExecution> {\n    return this.service.sendCommand(clientMetadata, dto);\n  }\n\n  @ApiEndpoint({\n    description: 'Get Redis whitelist commands available for plugins',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'List of available commands',\n        type: [String],\n      },\n    ],\n  })\n  @Get('/commands')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async getPluginCommands(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n  ): Promise<string[]> {\n    return this.service.getWhitelistCommands(clientMetadata);\n  }\n\n  @ApiEndpoint({\n    description: 'Save plugin state for particular command execution',\n    statusCode: 201,\n  })\n  @Post('/:visualizationId/command-executions/:id/state')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async saveState(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Param('visualizationId') visualizationId: string,\n    @Param('id') commandExecutionId: string,\n    @Body() dto: CreatePluginStateDto,\n  ): Promise<void> {\n    await this.service.saveState(\n      clientMetadata,\n      visualizationId,\n      commandExecutionId,\n      dto,\n    );\n  }\n\n  @ApiEndpoint({\n    description: 'Get previously saved state',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        description: 'Plugin state',\n        type: () => PluginState,\n      },\n    ],\n  })\n  @Get('/:visualizationId/command-executions/:id/state')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async getState(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Param('visualizationId') visualizationId: string,\n    @Param('id') commandExecutionId: string,\n  ): Promise<PluginState> {\n    return this.service.getState(\n      clientMetadata,\n      visualizationId,\n      commandExecutionId,\n    );\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/plugins.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockClientMetadata,\n  mockCommandExecutionUnsupportedCommandResult,\n  mockCreateCommandExecutionDto,\n  mockDatabaseClientFactory,\n  mockPluginCommandExecution,\n  mockWhitelistCommandsResponse,\n  mockWorkbenchClientMetadata,\n  mockWorkbenchCommandsExecutor,\n} from 'src/__mocks__';\nimport { v4 as uuidv4 } from 'uuid';\nimport { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';\nimport { BadRequestException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { PluginsService } from 'src/modules/workbench/plugins.service';\nimport { PluginCommandsWhitelistProvider } from 'src/modules/workbench/providers/plugin-commands-whitelist.provider';\nimport { PluginStateRepository } from 'src/modules/workbench/repositories/plugin-state.repository';\nimport { PluginState } from 'src/modules/workbench/models/plugin-state';\nimport config from 'src/utils/config';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  CommandExecutionType,\n  ResultsMode,\n  RunQueryMode,\n} from 'src/modules/workbench/models/command-execution';\n\nconst PLUGINS_CONFIG = config.get('plugins');\n\nconst mockVisualizationId = 'pluginName_visualizationName';\nconst mockCommandExecutionId = uuidv4();\nconst mockState = {\n  some: 'object',\n};\n\nconst mockPluginState: PluginState = new PluginState({\n  visualizationId: mockVisualizationId,\n  commandExecutionId: mockCommandExecutionId,\n  state: mockState,\n  createdAt: new Date(),\n  updatedAt: new Date(),\n});\n\nconst mockPluginCommandsWhitelistProvider = () => ({\n  getWhitelistCommands: jest.fn(),\n});\n\nconst mockPluginStateProvider = () => ({\n  upsert: jest.fn(),\n  getOne: jest.fn(),\n});\n\ndescribe('PluginsService', () => {\n  let service: PluginsService;\n  let workbenchCommandsExecutor;\n  let pluginsCommandsWhitelistProvider;\n  let pluginStateProvider;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        PluginsService,\n        {\n          provide: WorkbenchCommandsExecutor,\n          useFactory: mockWorkbenchCommandsExecutor,\n        },\n        {\n          provide: PluginCommandsWhitelistProvider,\n          useFactory: mockPluginCommandsWhitelistProvider,\n        },\n        {\n          provide: PluginStateRepository,\n          useFactory: mockPluginStateProvider,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<PluginsService>(PluginsService);\n    workbenchCommandsExecutor = module.get<WorkbenchCommandsExecutor>(\n      WorkbenchCommandsExecutor,\n    );\n    pluginsCommandsWhitelistProvider =\n      module.get<PluginCommandsWhitelistProvider>(\n        PluginCommandsWhitelistProvider,\n      );\n    pluginStateProvider = module.get(PluginStateRepository);\n  });\n\n  describe('sendCommand', () => {\n    it('should successfully execute command', async () => {\n      pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(\n        mockWhitelistCommandsResponse,\n      );\n\n      const result = await service.sendCommand(\n        mockWorkbenchClientMetadata,\n        mockCreateCommandExecutionDto,\n      );\n\n      expect(result).toEqual(mockPluginCommandExecution);\n      expect(workbenchCommandsExecutor.sendCommand).toHaveBeenCalled();\n    });\n    it('should return status failed when unsupported command called', async () => {\n      const dto = {\n        command: 'subscribe',\n        mode: RunQueryMode.ASCII,\n      };\n\n      pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(\n        mockWhitelistCommandsResponse,\n      );\n\n      const result = await service.sendCommand(\n        mockWorkbenchClientMetadata,\n        dto,\n      );\n\n      expect(result).toEqual({\n        ...dto,\n        databaseId: mockWorkbenchClientMetadata.databaseId,\n        result: [mockCommandExecutionUnsupportedCommandResult],\n        resultsMode: ResultsMode.Default,\n        type: CommandExecutionType.Workbench,\n      });\n      expect(workbenchCommandsExecutor.sendCommand).not.toHaveBeenCalled();\n    });\n    it('should throw an error when command execution failed', async () => {\n      pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(\n        mockWhitelistCommandsResponse,\n      );\n      workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(\n        new BadRequestException('error'),\n      );\n\n      const dto = {\n        command: 'get foo',\n        mode: RunQueryMode.ASCII,\n      };\n\n      try {\n        await service.sendCommand(mockWorkbenchClientMetadata, dto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n  });\n  describe('getWhitelistCommands', () => {\n    it('should successfully return whitelisted commands', async () => {\n      pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(\n        mockWhitelistCommandsResponse,\n      );\n\n      const result = await service.getWhitelistCommands(\n        mockWorkbenchClientMetadata,\n      );\n\n      expect(result).toEqual(mockWhitelistCommandsResponse);\n    });\n  });\n  describe('saveState', () => {\n    it('should successfully save state', async () => {\n      pluginStateProvider.upsert.mockResolvedValueOnce(mockPluginState);\n\n      const dto = {\n        state: mockState,\n      };\n      const result = await service.saveState(\n        mockClientMetadata,\n        mockVisualizationId,\n        mockCommandExecutionId,\n        dto,\n      );\n\n      expect(result).toEqual(undefined);\n    });\n    it('should throw an error when state too large', async () => {\n      pluginStateProvider.upsert.mockResolvedValueOnce(mockPluginState);\n\n      try {\n        const dto = {\n          state: Buffer.alloc(PLUGINS_CONFIG.stateMaxSize + 1, 0),\n        };\n        await service.saveState(\n          mockClientMetadata,\n          mockVisualizationId,\n          mockCommandExecutionId,\n          dto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n        expect(e.message).toEqual(\n          ERROR_MESSAGES.PLUGIN_STATE_MAX_SIZE(PLUGINS_CONFIG.stateMaxSize),\n        );\n      }\n      expect(pluginStateProvider.upsert).not.toHaveBeenCalled();\n    });\n  });\n  describe('getState', () => {\n    it('should successfully get state', async () => {\n      pluginStateProvider.getOne.mockResolvedValueOnce(mockPluginState);\n\n      const result = await service.getState(\n        mockClientMetadata,\n        mockVisualizationId,\n        mockCommandExecutionId,\n      );\n\n      expect(result).toEqual(mockPluginState);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/plugins.service.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';\nimport { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto';\nimport { CommandNotSupportedError } from 'src/modules/cli/constants/errors';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { PluginCommandExecution } from 'src/modules/workbench/models/plugin-command-execution';\nimport { plainToInstance } from 'class-transformer';\nimport { PluginCommandsWhitelistProvider } from 'src/modules/workbench/providers/plugin-commands-whitelist.provider';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { CreatePluginStateDto } from 'src/modules/workbench/dto/create-plugin-state.dto';\nimport { PluginState } from 'src/modules/workbench/models/plugin-state';\nimport config from 'src/utils/config';\nimport { ClientMetadata } from 'src/common/models';\nimport { PluginStateRepository } from 'src/modules/workbench/repositories/plugin-state.repository';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\n\nconst PLUGINS_CONFIG = config.get('plugins');\n\n@Injectable()\nexport class PluginsService {\n  constructor(\n    private commandsExecutor: WorkbenchCommandsExecutor,\n    private pluginStateRepository: PluginStateRepository,\n    private whitelistProvider: PluginCommandsWhitelistProvider,\n    private databaseClientFactory: DatabaseClientFactory,\n  ) {}\n\n  /**\n   * Send redis command from workbench and save history\n   *\n   * @param clientMetadata\n   * @param dto\n   */\n  async sendCommand(\n    clientMetadata: ClientMetadata,\n    dto: CreateCommandExecutionDto,\n  ): Promise<PluginCommandExecution> {\n    try {\n      const client =\n        await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n      await this.checkWhitelistedCommands(clientMetadata, dto.command);\n\n      const result = await this.commandsExecutor.sendCommand(client, dto);\n\n      return plainToInstance(PluginCommandExecution, {\n        ...dto,\n        databaseId: clientMetadata.databaseId,\n        result,\n      });\n    } catch (error) {\n      if (error instanceof CommandNotSupportedError) {\n        return plainToInstance(PluginCommandExecution, {\n          ...dto,\n          databaseId: clientMetadata.databaseId,\n          result: [\n            {\n              response: error.message,\n              status: CommandExecutionStatus.Fail,\n            },\n          ],\n        });\n      }\n\n      throw error;\n    }\n  }\n\n  /**\n   * Get database white listed commands for plugins\n   * @param clientMetadata\n   */\n  async getWhitelistCommands(\n    clientMetadata: ClientMetadata,\n  ): Promise<string[]> {\n    const client =\n      await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n    return await this.whitelistProvider.getWhitelistCommands(client);\n  }\n\n  /**\n   * Save plugin state\n   *\n   * @param clientMetadata\n   * @param visualizationId\n   * @param commandExecutionId\n   * @param dto\n   */\n  async saveState(\n    clientMetadata: ClientMetadata,\n    visualizationId: string,\n    commandExecutionId: string,\n    dto: CreatePluginStateDto,\n  ): Promise<void> {\n    if (JSON.stringify(dto.state).length > PLUGINS_CONFIG.stateMaxSize) {\n      throw new BadRequestException(\n        ERROR_MESSAGES.PLUGIN_STATE_MAX_SIZE(PLUGINS_CONFIG.stateMaxSize),\n      );\n    }\n\n    await this.pluginStateRepository.upsert(clientMetadata.sessionMetadata, {\n      visualizationId,\n      commandExecutionId,\n      ...dto,\n    });\n  }\n\n  /**\n   * Get plugin state\n   *\n   * @param clientMetadata\n   * @param visualizationId\n   * @param commandExecutionId\n   */\n  async getState(\n    clientMetadata: ClientMetadata,\n    visualizationId: string,\n    commandExecutionId: string,\n  ): Promise<PluginState> {\n    return this.pluginStateRepository.getOne(\n      clientMetadata.sessionMetadata,\n      visualizationId,\n      commandExecutionId,\n    );\n  }\n\n  /**\n   * Check if command outside workbench commands black list\n   * @param clientMetadata\n   * @param commandLine\n   * @private\n   */\n  private async checkWhitelistedCommands(\n    clientMetadata: ClientMetadata,\n    commandLine: string,\n  ) {\n    const targetCommand = commandLine.toLowerCase();\n\n    const whitelist = await this.getWhitelistCommands(clientMetadata);\n\n    if (!whitelist.find((command) => targetCommand.startsWith(command))) {\n      throw new CommandNotSupportedError(\n        ERROR_MESSAGES.PLUGIN_COMMAND_NOT_SUPPORTED(\n          targetCommand.split(' ')[0].toUpperCase(),\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockRedisCommandReply,\n  mockWhitelistCommandsResponse,\n  mockStandaloneRedisClient,\n} from 'src/__mocks__';\nimport { PluginCommandsWhitelistProvider } from 'src/modules/workbench/providers/plugin-commands-whitelist.provider';\n\ndescribe('PluginCommandsWhitelistProvider', () => {\n  const client = mockStandaloneRedisClient;\n  let service;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [PluginCommandsWhitelistProvider],\n    }).compile();\n\n    service = await module.get<PluginCommandsWhitelistProvider>(\n      PluginCommandsWhitelistProvider,\n    );\n  });\n\n  describe('getWhitelistCommands', () => {\n    let calculateCommandsSpy;\n\n    beforeEach(() => {\n      calculateCommandsSpy = jest.spyOn(service, 'calculateWhiteListCommands');\n    });\n\n    it('should fetch commands when no cache and return from cache when possible', async () => {\n      calculateCommandsSpy.mockResolvedValueOnce(mockWhitelistCommandsResponse);\n\n      expect(await service.getWhitelistCommands(client)).toEqual(\n        mockWhitelistCommandsResponse,\n      );\n      expect(calculateCommandsSpy).toHaveBeenCalled();\n\n      calculateCommandsSpy.mockClear();\n\n      expect(await service.getWhitelistCommands(client)).toEqual(\n        mockWhitelistCommandsResponse,\n      );\n      expect(calculateCommandsSpy).not.toHaveBeenCalled();\n    });\n  });\n  describe('calculateWhiteListCommands', () => {\n    it('should return 2 readonly commands', async () => {\n      client.call.mockResolvedValueOnce(mockRedisCommandReply);\n      client.call.mockResolvedValueOnce([]);\n      client.call.mockResolvedValueOnce([]);\n\n      const result = await service.calculateWhiteListCommands(client);\n\n      expect(result).toEqual(mockWhitelistCommandsResponse);\n    });\n    it('should return 1 readonly commands excluded by dangerous filter', async () => {\n      client.call.mockResolvedValueOnce(mockRedisCommandReply);\n      client.call.mockResolvedValueOnce(['custom.command']);\n      client.call.mockResolvedValueOnce([]);\n\n      const result = await service.calculateWhiteListCommands(client);\n\n      expect(result).toEqual(['get']);\n    });\n    it('should return 1 readonly commands excluded by blocking filter', async () => {\n      client.call.mockResolvedValueOnce(mockRedisCommandReply);\n      client.call.mockResolvedValueOnce([]);\n      client.call.mockResolvedValueOnce(['custom.command']);\n\n      const result = await service.calculateWhiteListCommands(client);\n\n      expect(result).toEqual(['get']);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { filter, get, map } from 'lodash';\nimport {\n  pluginBlockingCommands,\n  pluginUnsupportedCommands,\n} from 'src/constants';\nimport { RedisClient } from 'src/modules/redis/client';\n\n@Injectable()\nexport class PluginCommandsWhitelistProvider {\n  private databasesCommands: Map<string, string[]> = new Map();\n\n  /**\n   * Get cached commands list or determine it and cache\n   * @param client\n   */\n  async getWhitelistCommands(client: RedisClient): Promise<string[]> {\n    return (\n      this.databasesCommands.get(client.clientMetadata.databaseId) ||\n      this.determineWhitelistCommandsForDatabase(client)\n    );\n  }\n\n  /**\n   * Get or create Workbench redis client, fetch commands and cache them\n   * @param client\n   */\n  async determineWhitelistCommandsForDatabase(\n    client: RedisClient,\n  ): Promise<string[]> {\n    // no need to define AppTool since it was set on RedisTool creation. todo: do not forget after refactoring;\n\n    const commands = await this.calculateWhiteListCommands(client);\n    this.databasesCommands.set(client.clientMetadata.databaseId, commands);\n\n    return commands;\n  }\n\n  /**\n   * Get whitelisted commands available for plugins for particular database\n   * Commands:\n   *  +Readonly\n   *  -Hardcoded unsupported commands\n   *  -Hardcoded blocking commands\n   *  -Redis dangerous\n   *  -Redis blocking\n   */\n  async calculateWhiteListCommands(client: RedisClient): Promise<string[]> {\n    let pluginWhiteListCommands = [];\n    const replyEncoding = 'utf8';\n    try {\n      const availableCommands = (await client.call(['command'], {\n        replyEncoding,\n      })) as string[][];\n      const readOnlyCommands = map(\n        filter(availableCommands, (command) =>\n          get(command, [2], []).includes('readonly'),\n        ),\n        (command) => command[0],\n      );\n\n      const blackListCommands = [\n        ...pluginUnsupportedCommands,\n        ...pluginBlockingCommands,\n      ];\n      try {\n        const dangerousCommands = (await client.call(\n          ['acl', 'cat', 'dangerous'],\n          { replyEncoding },\n        )) as string[];\n        blackListCommands.push(...dangerousCommands);\n      } catch (e) {\n        // ignore error as acl cat available since Redis 6.0\n      }\n\n      try {\n        const blockingCommands = (await client.call(\n          ['acl', 'cat', 'blocking'],\n          { replyEncoding },\n        )) as string[];\n        blackListCommands.push(...blockingCommands);\n      } catch (e) {\n        // ignore error as acl cat available since Redis 6.0\n      }\n\n      pluginWhiteListCommands = filter(\n        readOnlyCommands,\n        (command) => !blackListCommands.includes(command),\n      );\n    } catch (e) {\n      // ignore any error to not block main process of client creation\n    }\n\n    return pluginWhiteListCommands.map((cmd) => cmd.toLowerCase());\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { get } from 'lodash';\nimport {\n  mockDatabaseClientFactory,\n  mockFtInfoAnalyticsData,\n  mockRedisFtInfoReply,\n  mockSessionMetadata,\n  mockStandaloneRedisClient,\n  mockWorkbenchAnalyticsService,\n  mockWorkbenchClientMetadata,\n} from 'src/__mocks__';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { unknownCommand } from 'src/constants';\nimport { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';\nimport { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto';\nimport { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { ServiceUnavailableException } from '@nestjs/common';\nimport {\n  CommandNotSupportedError,\n  CommandParsingError,\n} from 'src/modules/cli/constants/errors';\nimport {\n  FormatterManager,\n  IFormatterStrategy,\n  FormatterTypes,\n} from 'src/common/transformers';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport {\n  CommandExecutionType,\n  RunQueryMode,\n} from 'src/modules/workbench/models/command-execution';\nimport { WorkbenchAnalytics } from 'src/modules/workbench/workbench.analytics';\n\nconst MOCK_ERROR_MESSAGE = 'Some error';\n\nconst mockSetCommand = 'set';\nconst mockGetEscapedKeyCommand = 'get \"\\\\\\\\key';\nconst mockCreateCommandExecutionDto: CreateCommandExecutionDto = {\n  command: `${mockSetCommand} foo bar`,\n  mode: RunQueryMode.ASCII,\n};\n\nconst mockCommandExecutionResult: CommandExecutionResult = {\n  response: 'OK',\n  status: CommandExecutionStatus.Success,\n};\n\nconst mockAnalyticsService = mockWorkbenchAnalyticsService();\n\ndescribe('WorkbenchCommandsExecutor', () => {\n  const client = mockStandaloneRedisClient;\n  let service: WorkbenchCommandsExecutor;\n  let utf8Formatter: IFormatterStrategy;\n  let asciiFormatter: IFormatterStrategy;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        WorkbenchCommandsExecutor,\n        {\n          provide: WorkbenchAnalytics,\n          useFactory: () => mockAnalyticsService,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<WorkbenchCommandsExecutor>(WorkbenchCommandsExecutor);\n\n    const formatterManager: FormatterManager = get(service, 'formatterManager');\n    utf8Formatter = formatterManager.getStrategy(FormatterTypes.UTF8);\n    asciiFormatter = formatterManager.getStrategy(FormatterTypes.ASCII);\n\n    client.sendCommand = jest.fn().mockResolvedValue(undefined);\n  });\n\n  describe('sendCommand', () => {\n    describe('sendCommandForStandalone', () => {\n      it('should successfully send ft.info', async () => {\n        client.sendCommand.mockResolvedValueOnce(mockRedisFtInfoReply);\n\n        const result = await service.sendCommand(client, {\n          command: 'ft.info idx',\n          mode: RunQueryMode.Raw,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: mockRedisFtInfoReply,\n            status: mockCommandExecutionResult.status,\n          },\n        ]);\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvents,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          [\n            {\n              response: mockRedisFtInfoReply,\n              status: CommandExecutionStatus.Success,\n            },\n          ],\n          {\n            command: 'ft.info',\n            rawMode: true,\n          },\n        );\n        expect(mockAnalyticsService.sendIndexInfoEvent).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          mockFtInfoAnalyticsData,\n        );\n      });\n      it('should successfully send command for standalone', async () => {\n        client.sendCommand.mockResolvedValueOnce('OK');\n\n        const result = await service.sendCommand(client, {\n          command: mockCreateCommandExecutionDto.command,\n          mode: RunQueryMode.ASCII,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: mockCommandExecutionResult.response,\n            status: mockCommandExecutionResult.status,\n          },\n        ]);\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvents,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          [\n            {\n              response: mockCommandExecutionResult.response,\n              status: CommandExecutionStatus.Success,\n            },\n          ],\n          {\n            command: mockSetCommand,\n            rawMode: false,\n          },\n        );\n      });\n      it('should return fail status in case of unsupported command error', async () => {\n        client.sendCommand.mockRejectedValueOnce(\n          new CommandNotSupportedError(MOCK_ERROR_MESSAGE),\n        );\n\n        const result = await service.sendCommand(client, {\n          command: mockCreateCommandExecutionDto.command,\n          mode: RunQueryMode.ASCII,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: MOCK_ERROR_MESSAGE,\n            status: CommandExecutionStatus.Fail,\n          },\n        ]);\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          {\n            response: MOCK_ERROR_MESSAGE,\n            error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE),\n            status: CommandExecutionStatus.Fail,\n          },\n          {\n            command: mockSetCommand,\n            rawMode: false,\n          },\n        );\n      });\n      it('should return fail status when replyError happened', async () => {\n        const replyError: Error = {\n          message: MOCK_ERROR_MESSAGE,\n          name: 'ReplyError',\n        };\n\n        client.sendCommand.mockRejectedValueOnce(replyError);\n\n        const result = await service.sendCommand(client, {\n          command: mockCreateCommandExecutionDto.command,\n          mode: RunQueryMode.ASCII,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: MOCK_ERROR_MESSAGE,\n            status: CommandExecutionStatus.Fail,\n          },\n        ]);\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          {\n            response: MOCK_ERROR_MESSAGE,\n            error: replyError,\n            status: CommandExecutionStatus.Fail,\n          },\n          {\n            command: mockSetCommand,\n            rawMode: false,\n          },\n        );\n      });\n      it('should successfully execute command and return ascii response', async () => {\n        const formatSpy = jest.spyOn(asciiFormatter, 'format');\n\n        client.sendCommand.mockResolvedValueOnce(\n          mockCommandExecutionResult.response,\n        );\n\n        const result = await service.sendCommand(client, {\n          command: mockCreateCommandExecutionDto.command,\n          mode: RunQueryMode.ASCII,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: mockCommandExecutionResult.response,\n            status: mockCommandExecutionResult.status,\n          },\n        ]);\n        expect(formatSpy).toHaveBeenCalled();\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvents,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          [\n            {\n              response: mockCommandExecutionResult.response,\n              status: CommandExecutionStatus.Success,\n            },\n          ],\n          {\n            command: mockSetCommand,\n            rawMode: false,\n          },\n        );\n      });\n      it('should successfully execute command and return raw response', async () => {\n        const formatSpy = jest.spyOn(utf8Formatter, 'format');\n\n        client.sendCommand.mockResolvedValueOnce(\n          mockCommandExecutionResult.response,\n        );\n\n        const result = await service.sendCommand(client, {\n          command: mockCreateCommandExecutionDto.command,\n          mode: RunQueryMode.Raw,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: mockCommandExecutionResult.response,\n            status: mockCommandExecutionResult.status,\n          },\n        ]);\n        expect(formatSpy).toHaveBeenCalled();\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvents,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          [\n            {\n              response: mockCommandExecutionResult.response,\n              status: CommandExecutionStatus.Success,\n            },\n          ],\n          {\n            command: mockSetCommand,\n            rawMode: true,\n          },\n        );\n      });\n      it('should return fail status when on unexpected error', async () => {\n        client.sendCommand.mockRejectedValueOnce(\n          new ServiceUnavailableException(MOCK_ERROR_MESSAGE),\n        );\n\n        const result = await service.sendCommand(client, {\n          command: mockCreateCommandExecutionDto.command,\n          mode: RunQueryMode.ASCII,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual([\n          {\n            response: MOCK_ERROR_MESSAGE,\n            status: CommandExecutionStatus.Fail,\n          },\n        ]);\n\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          {\n            response: MOCK_ERROR_MESSAGE,\n            error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE),\n            status: CommandExecutionStatus.Fail,\n          },\n          {\n            command: mockSetCommand,\n            rawMode: false,\n          },\n        );\n      });\n    });\n    describe('CommandParsingError', () => {\n      it('should return response with [CLI_UNTERMINATED_QUOTES] error for sendCommandForNodes', async () => {\n        const mockResult = [\n          {\n            response: ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n            status: CommandExecutionStatus.Fail,\n          },\n        ];\n\n        const result = await service.sendCommand(client, {\n          command: mockGetEscapedKeyCommand,\n          mode: RunQueryMode.ASCII,\n          type: CommandExecutionType.Workbench,\n        });\n\n        expect(result).toEqual(mockResult);\n        expect(\n          mockAnalyticsService.sendCommandExecutedEvent,\n        ).toHaveBeenCalledWith(\n          mockSessionMetadata,\n          mockWorkbenchClientMetadata.databaseId,\n          CommandExecutionType.Workbench,\n          {\n            response: ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n            error: new CommandParsingError(\n              ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n            ),\n            status: CommandExecutionStatus.Fail,\n          },\n          {\n            command: unknownCommand,\n            rawMode: false,\n          },\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts",
    "content": "import { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport {\n  checkHumanReadableCommands,\n  splitCliCommandLine,\n} from 'src/utils/cli-helper';\nimport {\n  CommandNotSupportedError,\n  CommandParsingError,\n  ClusterNodeNotFoundError,\n  WrongDatabaseTypeError,\n} from 'src/modules/cli/constants/errors';\nimport { unknownCommand } from 'src/constants';\nimport { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result';\nimport { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto';\nimport {\n  FormatterManager,\n  FormatterTypes,\n  ASCIIFormatterStrategy,\n  UTF8FormatterStrategy,\n} from 'src/common/transformers';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { getAnalyticsDataFromIndexInfo } from 'src/utils';\nimport { RunQueryMode } from 'src/modules/workbench/models/command-execution';\nimport { WorkbenchAnalytics } from 'src/modules/workbench/workbench.analytics';\n\n@Injectable()\nexport class WorkbenchCommandsExecutor {\n  private logger = new Logger('WorkbenchCommandsExecutor');\n\n  private formatterManager: FormatterManager;\n\n  constructor(private analyticsService: WorkbenchAnalytics) {\n    this.formatterManager = new FormatterManager();\n    this.formatterManager.addStrategy(\n      FormatterTypes.UTF8,\n      new UTF8FormatterStrategy(),\n    );\n    this.formatterManager.addStrategy(\n      FormatterTypes.ASCII,\n      new ASCIIFormatterStrategy(),\n    );\n  }\n\n  /**\n   * Entrypoint for any CommandExecution\n   * Will determine type of command (standalone, per node(s)) and format, and execute it\n   * Also sis a single place of analytics events invocation\n   * @param client\n   * @param dto\n   */\n  public async sendCommand(\n    client: RedisClient,\n    dto: CreateCommandExecutionDto,\n  ): Promise<CommandExecutionResult[]> {\n    this.logger.debug('Executing workbench command.');\n    let command = unknownCommand;\n    let commandArgs: string[] = [];\n\n    try {\n      const { command: commandLine, mode } = dto;\n      [command, ...commandArgs] = splitCliCommandLine(commandLine);\n\n      const formatter = this.getFormatter(mode);\n      const replyEncoding = checkHumanReadableCommands(\n        `${command} ${commandArgs[0]}`,\n      )\n        ? 'utf8'\n        : undefined;\n\n      const response = formatter.format(\n        await client.sendCommand([command, ...commandArgs], { replyEncoding }),\n      );\n      const result: CommandExecutionResult[] = [\n        { response, status: CommandExecutionStatus.Success },\n      ];\n\n      this.logger.debug('Succeed to execute workbench command.');\n      this.analyticsService.sendCommandExecutedEvents(\n        client.clientMetadata.sessionMetadata,\n        client.clientMetadata.databaseId,\n        dto.type,\n        result,\n        { command, rawMode: mode === RunQueryMode.Raw },\n      );\n\n      if (command.toLowerCase() === 'ft.info') {\n        this.analyticsService.sendIndexInfoEvent(\n          client.clientMetadata.sessionMetadata,\n          client.clientMetadata.databaseId,\n          dto.type,\n          getAnalyticsDataFromIndexInfo(response as string[]),\n        );\n      }\n\n      return result;\n    } catch (error) {\n      this.logger.error('Failed to execute workbench command.', error);\n\n      const errorResult = {\n        response: error.message,\n        status: CommandExecutionStatus.Fail,\n      };\n      this.analyticsService.sendCommandExecutedEvent(\n        client.clientMetadata.sessionMetadata,\n        client.clientMetadata.databaseId,\n        dto.type,\n        { ...errorResult, error },\n        { command, rawMode: dto.mode === RunQueryMode.Raw },\n      );\n\n      if (\n        error instanceof CommandParsingError ||\n        error instanceof CommandNotSupportedError ||\n        error.name === 'ReplyError'\n      ) {\n        return [errorResult];\n      }\n\n      if (\n        error instanceof WrongDatabaseTypeError ||\n        error instanceof ClusterNodeNotFoundError\n      ) {\n        throw new BadRequestException(error.message);\n      }\n\n      return [errorResult];\n    }\n  }\n\n  /**\n   * Get formatter strategy based on \"mode\"\n   * @param mode\n   * @private\n   */\n  private getFormatter(mode: RunQueryMode) {\n    switch (mode) {\n      case RunQueryMode.Raw:\n        return this.formatterManager.getStrategy(FormatterTypes.UTF8);\n      case RunQueryMode.ASCII:\n      default: {\n        return this.formatterManager.getStrategy(FormatterTypes.ASCII);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts",
    "content": "import { CommandExecution } from 'src/modules/workbench/models/command-execution';\nimport { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution';\nimport { SessionMetadata } from 'src/common/models';\nimport { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter';\n\nexport abstract class CommandExecutionRepository {\n  /**\n   * Create multiple entities\n   *\n   * @param sessionMetadata\n   * @param commandExecutions\n   */\n  abstract createMany(\n    sessionMetadata: SessionMetadata,\n    commandExecutions: Partial<CommandExecution>[],\n  ): Promise<CommandExecution[]>;\n\n  /**\n   * Fetch only needed fields to show in list to avoid huge decryption work\n   *\n   * @param sessionMetadata\n   * @param databaseId\n   * @param filter\n   */\n  abstract getList(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    filter: CommandExecutionFilter,\n  ): Promise<ShortCommandExecution[]>;\n\n  /**\n   * Get single command execution entity, decrypt and convert to model\n   *\n   * @param sessionMetadata\n   * @param databaseId\n   * @param id\n   */\n  abstract getOne(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<CommandExecution>;\n\n  /**\n   * Delete single item\n   *\n   * @param sessionMetadata\n   * @param databaseId\n   * @param id\n   */\n  abstract delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<void>;\n\n  /**\n   * Delete all items\n   *\n   * @param sessionMetadata\n   * @param databaseId\n   * @param filter\n   */\n  abstract deleteAll(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    filter: CommandExecutionFilter,\n  ): Promise<void>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockEncryptionService,\n  mockRepository,\n  MockType,\n  mockSessionMetadata,\n  mockCommandExecutionEntity,\n  mockCommandExecution,\n  mockCommendExecutionHugeResultPlaceholder,\n  mockCommendExecutionHugeResultPlaceholderEncrypted,\n  mockShortCommandExecutionEntity,\n  mockShortCommandExecution,\n  mockCommandExecutionFilter,\n} from 'src/__mocks__';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { NotFoundException } from '@nestjs/common';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { Repository } from 'typeorm';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { CommandExecutionEntity } from 'src/modules/workbench/entities/command-execution.entity';\nimport { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport config from 'src/utils/config';\nimport { LocalCommandExecutionRepository } from 'src/modules/workbench/repositories/local-command-execution.repository';\n\nconst WORKBENCH_CONFIG = config.get('workbench');\n\ndescribe('LocalCommandExecutionRepository', () => {\n  let service: LocalCommandExecutionRepository;\n  let repository: MockType<Repository<CommandExecutionEntity>>;\n  let encryptionService: MockType<EncryptionService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalCommandExecutionRepository,\n        {\n          provide: getRepositoryToken(CommandExecutionEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalCommandExecutionRepository);\n    repository = module.get(getRepositoryToken(CommandExecutionEntity));\n    encryptionService = module.get(EncryptionService);\n\n    when(encryptionService.encrypt)\n      .calledWith(mockCommandExecution.command)\n      .mockResolvedValue({\n        data: mockCommandExecutionEntity.command,\n        encryption: mockCommandExecutionEntity.encryption,\n      })\n      .calledWith(JSON.stringify(mockCommandExecution.result))\n      .mockResolvedValue({\n        data: mockCommandExecutionEntity.result,\n        encryption: mockCommandExecutionEntity.encryption,\n      })\n      .calledWith(JSON.stringify([mockCommendExecutionHugeResultPlaceholder]))\n      .mockResolvedValue({\n        data: mockCommendExecutionHugeResultPlaceholderEncrypted,\n        encryption: mockCommandExecutionEntity.encryption,\n      });\n\n    when(encryptionService.decrypt)\n      .calledWith(mockCommandExecutionEntity.command, expect.anything())\n      .mockResolvedValue(mockCommandExecution.command)\n      .calledWith(mockCommandExecutionEntity.result, expect.anything())\n      .mockResolvedValue(JSON.stringify(mockCommandExecution.result));\n\n    repository.save.mockReturnValue(mockCommandExecutionEntity);\n    repository.findOneBy.mockReturnValue(mockCommandExecutionEntity);\n  });\n\n  describe('create', () => {\n    let cleanupSpy: jest.SpyInstance;\n\n    beforeEach(() => {\n      cleanupSpy = jest.spyOn(service as any, 'cleanupDatabaseHistory');\n    });\n\n    it('should process new entity', async () => {\n      expect(\n        await service.createMany(mockSessionMetadata, [\n          {\n            ...mockCommandExecution,\n            id: undefined,\n            createdAt: undefined,\n          },\n        ]),\n      ).toEqual([mockCommandExecution]);\n      expect(repository.save).toHaveBeenCalledWith({\n        ...mockCommandExecutionEntity,\n        id: undefined,\n        createdAt: undefined,\n      });\n      expect(cleanupSpy).toBeCalledTimes(1);\n      expect(cleanupSpy).toHaveBeenCalledWith(mockCommandExecution.databaseId, {\n        type: mockCommandExecution.type,\n      });\n    });\n    it('should return full result even if size limit exceeded', async () => {\n      const executionResult = [\n        {\n          status: CommandExecutionStatus.Success,\n          response: `${Buffer.alloc(WORKBENCH_CONFIG.maxResultSize, 'a').toString()}`,\n        },\n      ];\n\n      expect(\n        await service.createMany(mockSessionMetadata, [\n          {\n            ...mockCommandExecution,\n            result: executionResult,\n          },\n        ]),\n      ).toEqual([\n        {\n          ...mockCommandExecution,\n          result: executionResult,\n          isNotStored: true, // double check that for such cases special flag returned\n        },\n      ]);\n      expect(repository.save).toHaveBeenCalledWith({\n        ...mockCommandExecutionEntity,\n        command: mockCommandExecutionEntity.command,\n        result: mockCommendExecutionHugeResultPlaceholderEncrypted,\n      });\n    });\n  });\n  describe('getList', () => {\n    it('should return list (2) of command execution', async () => {\n      repository\n        .createQueryBuilder()\n        .getMany.mockReturnValueOnce([\n          mockShortCommandExecutionEntity,\n          mockShortCommandExecutionEntity,\n        ]);\n\n      expect(\n        await service.getList(\n          mockSessionMetadata,\n          mockCommandExecutionEntity.databaseId,\n          mockCommandExecutionFilter,\n        ),\n      ).toEqual([mockShortCommandExecution, mockShortCommandExecution]);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        databaseId: mockCommandExecution.databaseId,\n        type: mockCommandExecutionFilter.type,\n      });\n    });\n    it('should return list (1) of command execution without failed decrypted item', async () => {\n      repository.createQueryBuilder().getMany.mockResolvedValueOnce([\n        mockShortCommandExecutionEntity,\n        {\n          ...mockShortCommandExecutionEntity,\n          command: 'something that can not be decrypted',\n        },\n      ]);\n      encryptionService.decrypt.mockResolvedValueOnce(\n        mockShortCommandExecution.command,\n      );\n      encryptionService.decrypt.mockRejectedValueOnce(\n        new KeytarDecryptionErrorException(),\n      );\n\n      expect(\n        await service.getList(\n          mockSessionMetadata,\n          mockCommandExecution.databaseId,\n          mockCommandExecutionFilter,\n        ),\n      ).toEqual([mockShortCommandExecution]);\n    });\n  });\n  describe('getOne', () => {\n    it('should return decrypted and transformed command execution', async () => {\n      expect(\n        await service.getOne(\n          mockSessionMetadata,\n          mockCommandExecution.databaseId,\n          mockCommandExecution.id,\n        ),\n      ).toEqual(mockCommandExecution);\n      expect(repository.findOneBy).toHaveBeenCalledWith({\n        id: mockCommandExecution.id,\n        databaseId: mockCommandExecution.databaseId,\n      });\n    });\n    it('should return null fields in case of decryption errors', async () => {\n      encryptionService.decrypt.mockReturnValueOnce(\n        mockCommandExecution.command,\n      );\n      encryptionService.decrypt.mockRejectedValueOnce(\n        new KeytarDecryptionErrorException(),\n      );\n\n      expect(\n        await service.getOne(\n          mockSessionMetadata,\n          mockCommandExecution.databaseId,\n          mockCommandExecution.id,\n        ),\n      ).toEqual({\n        ...mockCommandExecution,\n        result: null,\n      });\n    });\n    it('should return not found exception', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      await expect(\n        service.getOne(\n          mockSessionMetadata,\n          mockCommandExecution.databaseId,\n          mockCommandExecution.id,\n        ),\n      ).rejects.toEqual(\n        new NotFoundException(ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND),\n      );\n    });\n  });\n  describe('delete', () => {\n    it('Should not return anything on delete', async () => {\n      repository.delete.mockResolvedValueOnce(1);\n      expect(\n        await service.delete(\n          mockSessionMetadata,\n          mockCommandExecution.databaseId,\n          mockCommandExecution.id,\n        ),\n      ).toEqual(undefined);\n      expect(repository.delete).toHaveBeenCalledWith({\n        id: mockCommandExecution.id,\n        databaseId: mockCommandExecution.databaseId,\n      });\n    });\n  });\n  describe('deleteAll', () => {\n    it('Should not return anything on delete', async () => {\n      repository.delete.mockResolvedValueOnce(1);\n      expect(\n        await service.deleteAll(\n          mockSessionMetadata,\n          mockCommandExecution.databaseId,\n          mockCommandExecutionFilter,\n        ),\n      ).toEqual(undefined);\n      expect(repository.delete).toHaveBeenCalledWith({\n        databaseId: mockCommandExecution.databaseId,\n        type: mockCommandExecutionFilter.type,\n      });\n    });\n  });\n  describe('cleanupDatabaseHistory', () => {\n    it('Should should not return anything on cleanup', async () => {\n      repository\n        .createQueryBuilder()\n        .getRawMany.mockReturnValueOnce([\n          { id: mockCommandExecutionEntity.id },\n          { id: mockCommandExecutionEntity.id },\n        ]);\n\n      expect(\n        await service['cleanupDatabaseHistory'](\n          mockCommandExecution.databaseId,\n          mockCommandExecutionFilter,\n        ),\n      ).toEqual(undefined);\n      expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({\n        databaseId: mockCommandExecution.databaseId,\n        type: mockCommandExecutionFilter.type,\n      });\n      expect(repository.createQueryBuilder().whereInIds).toHaveBeenCalledWith([\n        mockCommandExecutionEntity.id,\n        mockCommandExecutionEntity.id,\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts",
    "content": "import { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { filter, isNull } from 'lodash';\nimport { plainToInstance } from 'class-transformer';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { CommandExecutionEntity } from 'src/modules/workbench/entities/command-execution.entity';\nimport { CommandExecution } from 'src/modules/workbench/models/command-execution';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { classToClass } from 'src/utils';\nimport { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository';\nimport config from 'src/utils/config';\nimport { SessionMetadata } from 'src/common/models';\nimport { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter';\n\nconst WORKBENCH_CONFIG = config.get('workbench');\n\n@Injectable()\nexport class LocalCommandExecutionRepository extends CommandExecutionRepository {\n  private logger = new Logger('LocalCommandExecutionRepository');\n\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(CommandExecutionEntity)\n    private readonly commandExecutionRepository: Repository<CommandExecutionEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(this.encryptionService, [\n      'command',\n      'result',\n    ]);\n  }\n\n  /**\n   * @inheritDoc\n   * ___\n   * Should encrypt command executions\n   * Should always throw and error in case when unable to encrypt for some reason\n   */\n  async createMany(\n    sessionMetadata: SessionMetadata,\n    commandExecutions: Partial<CommandExecution>[],\n  ): Promise<CommandExecution[]> {\n    // todo: limit by 30 max to insert\n    const response = await Promise.all(\n      commandExecutions.map(async (commandExecution, idx) => {\n        const entity = plainToInstance(\n          CommandExecutionEntity,\n          commandExecution,\n        );\n        let isNotStored: undefined | boolean;\n\n        // Do not store command execution result that exceeded limitation\n        if (\n          JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize\n        ) {\n          entity.result = JSON.stringify([\n            {\n              status: CommandExecutionStatus.Success,\n              response: ERROR_MESSAGES.WORKBENCH_RESPONSE_TOO_BIG(),\n              sizeLimitExceeded: true,\n            },\n          ]);\n          // Hack, do not store isNotStored. Send once to show warning\n          isNotStored = true;\n        }\n\n        return classToClass(CommandExecution, {\n          ...(await this.commandExecutionRepository.save(\n            await this.modelEncryptor.encryptEntity(entity),\n          )),\n          command: commandExecutions[idx].command, // avoid decryption\n          mode: commandExecutions[idx].mode,\n          // avoid decryption + show original response when it was huge\n          // also will return original response even if it wasn't stored\n          // so flag sizeLimitExceeded will be undefined\n          result: commandExecutions[idx].result,\n          summary: commandExecutions[idx].summary,\n          executionTime: commandExecutions[idx].executionTime,\n          isNotStored,\n        });\n      }),\n    );\n\n    // cleanup history and ignore error if any\n    try {\n      await this.cleanupDatabaseHistory(response[0].databaseId, {\n        type: commandExecutions[0].type,\n      });\n    } catch (e) {\n      this.logger.error(\n        'Error when trying to cleanup history after insert',\n        e,\n        sessionMetadata,\n      );\n    }\n\n    return response;\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getList(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    queryFilter: CommandExecutionFilter,\n  ): Promise<ShortCommandExecution[]> {\n    this.logger.debug('Getting command executions', sessionMetadata);\n    const entities = await this.commandExecutionRepository\n      .createQueryBuilder('e')\n      .where({ databaseId, type: queryFilter.type })\n      .select([\n        'e.id',\n        'e.command',\n        'e.databaseId',\n        'e.createdAt',\n        'e.encryption',\n        'e.mode',\n        'e.summary',\n        'e.resultsMode',\n        'e.executionTime',\n        'e.db',\n        'e.type',\n      ])\n      .orderBy('e.createdAt', 'DESC')\n      .limit(WORKBENCH_CONFIG.maxItemsPerDb)\n      .getMany();\n\n    this.logger.debug('Succeed to get command executions', sessionMetadata);\n\n    const decryptedEntities = await Promise.all(\n      entities.map<Promise<CommandExecutionEntity>>(async (entity) => {\n        try {\n          return await this.modelEncryptor.decryptEntity(entity);\n        } catch (e) {\n          return null;\n        }\n      }),\n    );\n\n    return filter(decryptedEntities, (entity) => !isNull(entity)).map(\n      (entity) => classToClass(ShortCommandExecution, entity),\n    );\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async getOne(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<CommandExecution> {\n    this.logger.debug('Getting command executions', sessionMetadata);\n\n    const entity = await this.commandExecutionRepository.findOneBy({\n      id,\n      databaseId,\n    });\n\n    if (!entity) {\n      this.logger.error(\n        `Command execution with id:${id} and databaseId:${databaseId} was not Found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND);\n    }\n\n    this.logger.debug(\n      `Succeed to get command execution ${id}`,\n      sessionMetadata,\n    );\n\n    const decryptedEntity = await this.modelEncryptor.decryptEntity(\n      entity,\n      true,\n    );\n\n    return classToClass(CommandExecution, decryptedEntity);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async delete(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    id: string,\n  ): Promise<void> {\n    this.logger.debug('Delete command execution', sessionMetadata);\n\n    await this.commandExecutionRepository.delete({ id, databaseId });\n\n    this.logger.debug('Command execution deleted', sessionMetadata);\n  }\n\n  /**\n   * @inheritDoc\n   */\n  async deleteAll(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    queryFilter: CommandExecutionFilter,\n  ): Promise<void> {\n    this.logger.debug('Delete all command executions', sessionMetadata);\n\n    await this.commandExecutionRepository.delete({\n      databaseId,\n      type: queryFilter.type,\n    });\n\n    this.logger.debug('Command executions deleted', sessionMetadata);\n  }\n\n  /**\n   * Clean history for particular database to fit N items limitation\n   * @param databaseId\n   * @param queryFilter\n   */\n  private async cleanupDatabaseHistory(\n    databaseId: string,\n    queryFilter: CommandExecutionFilter,\n  ): Promise<void> {\n    // todo: investigate why delete with sub-query doesn't works\n    const idsToDelete = (\n      await this.commandExecutionRepository\n        .createQueryBuilder()\n        .where({ databaseId, type: queryFilter.type })\n        .select('id')\n        .orderBy('createdAt', 'DESC')\n        .offset(WORKBENCH_CONFIG.maxItemsPerDb)\n        .getRawMany()\n    ).map((item) => item.id);\n\n    await this.commandExecutionRepository\n      .createQueryBuilder()\n      .delete()\n      .whereInIds(idsToDelete)\n      .execute();\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/repositories/local-plugin-state.repository.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  mockEncryptionService,\n  mockEncryptResult,\n  mockRepository,\n  mockSessionMetadata,\n  MockType,\n} from 'src/__mocks__';\nimport { v4 as uuidv4 } from 'uuid';\nimport { NotFoundException } from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { LocalPluginStateRepository } from 'src/modules/workbench/repositories/local-plugin-state.repository';\nimport { PluginState } from 'src/modules/workbench/models/plugin-state';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport { PluginStateEntity } from 'src/modules/workbench/entities/plugin-state.entity';\nimport { Repository } from 'typeorm';\nimport { KeytarDecryptionErrorException } from 'src/modules/encryption/exceptions';\n\nconst mockVisualizationId = 'pluginName_visualizationName';\nconst mockCommandExecutionId = uuidv4();\nconst mockState = {\n  some: 'object',\n};\n\nconst mockPluginStatePartial: Partial<PluginState> = new PluginState({\n  visualizationId: mockVisualizationId,\n  commandExecutionId: mockCommandExecutionId,\n  state: mockState,\n});\n\nconst mockPluginState: PluginState = new PluginState({\n  ...mockPluginStatePartial,\n  createdAt: new Date(),\n  updatedAt: new Date(),\n});\n\nconst mockPluginStateEntity = new PluginStateEntity({\n  ...mockPluginState,\n  state: mockEncryptResult.data,\n  encryption: 'KEYTAR',\n});\n\ndescribe('LocalPluginStateRepository', () => {\n  let service: LocalPluginStateRepository;\n  let repository: MockType<Repository<PluginStateEntity>>;\n  let encryptionService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        LocalPluginStateRepository,\n        {\n          provide: getRepositoryToken(PluginStateEntity),\n          useFactory: mockRepository,\n        },\n        {\n          provide: EncryptionService,\n          useFactory: mockEncryptionService,\n        },\n      ],\n    }).compile();\n\n    service = module.get(LocalPluginStateRepository);\n    repository = module.get(getRepositoryToken(PluginStateEntity));\n    encryptionService = module.get<EncryptionService>(EncryptionService);\n  });\n\n  describe('upsert', () => {\n    it('should process new entity', async () => {\n      repository.save.mockReturnValueOnce(mockPluginStateEntity);\n      encryptionService.encrypt.mockReturnValue(mockEncryptResult);\n\n      expect(\n        await service.upsert(mockSessionMetadata, mockPluginStatePartial),\n      ).toEqual(undefined);\n    });\n    it('should throw origin error when error is not a SQL constraint error', async () => {\n      const constraintError: any = new Error('any error');\n\n      repository.save.mockRejectedValueOnce(constraintError);\n      encryptionService.encrypt.mockReturnValue(mockEncryptResult);\n\n      try {\n        await service.upsert(mockSessionMetadata, mockPluginStatePartial);\n        fail();\n      } catch (e) {\n        expect(e).not.toBeInstanceOf(NotFoundException);\n        expect(e.message).not.toEqual(\n          ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND,\n        );\n      }\n    });\n    it('should throw not found error ON SQL constraint error', async () => {\n      const constraintError: any = new Error('FOREIGN_KEY error');\n      constraintError.code = 'SQLITE_CONSTRAINT';\n\n      repository.save.mockRejectedValueOnce(constraintError);\n      encryptionService.encrypt.mockReturnValue(mockEncryptResult);\n\n      try {\n        await service.upsert(mockSessionMetadata, mockPluginStatePartial);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND);\n      }\n    });\n  });\n  describe('getOne', () => {\n    it('should return decrypted and transformed state', async () => {\n      repository.findOneBy.mockResolvedValueOnce(mockPluginStateEntity);\n      encryptionService.decrypt.mockReturnValueOnce(\n        JSON.stringify(mockPluginState.state),\n      );\n\n      expect(\n        await service.getOne(\n          mockSessionMetadata,\n          mockVisualizationId,\n          mockCommandExecutionId,\n        ),\n      ).toEqual(mockPluginState);\n    });\n    it('should return null fields in case of decryption errors', async () => {\n      repository.findOneBy.mockResolvedValueOnce(mockPluginStateEntity);\n      encryptionService.decrypt.mockRejectedValueOnce(\n        new KeytarDecryptionErrorException(),\n      );\n\n      const result = await service.getOne(\n        mockSessionMetadata,\n        mockVisualizationId,\n        mockCommandExecutionId,\n      );\n\n      expect(result).toBeInstanceOf(PluginState);\n      expect(result).toEqual({\n        ...mockPluginState,\n        state: null,\n      });\n    });\n    it('should return not found exception', async () => {\n      repository.findOneBy.mockResolvedValueOnce(null);\n\n      try {\n        await service.getOne(\n          mockSessionMetadata,\n          mockVisualizationId,\n          mockCommandExecutionId,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(NotFoundException);\n        expect(e.message).toEqual(ERROR_MESSAGES.PLUGIN_STATE_NOT_FOUND);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/repositories/local-plugin-state.repository.ts",
    "content": "import { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { plainToInstance } from 'class-transformer';\nimport { EncryptionService } from 'src/modules/encryption/encryption.service';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { classToClass } from 'src/utils';\nimport { PluginStateEntity } from 'src/modules/workbench/entities/plugin-state.entity';\nimport { PluginState } from 'src/modules/workbench/models/plugin-state';\nimport { PluginStateRepository } from 'src/modules/workbench/repositories/plugin-state.repository';\nimport { ModelEncryptor } from 'src/modules/encryption/model.encryptor';\nimport { SessionMetadata } from 'src/common/models';\n\n@Injectable()\nexport class LocalPluginStateRepository extends PluginStateRepository {\n  private logger = new Logger('LocalPluginStateRepository');\n\n  private readonly modelEncryptor: ModelEncryptor;\n\n  constructor(\n    @InjectRepository(PluginStateEntity)\n    private readonly repository: Repository<PluginStateEntity>,\n    private readonly encryptionService: EncryptionService,\n  ) {\n    super();\n    this.modelEncryptor = new ModelEncryptor(encryptionService, ['state']);\n  }\n\n  /**\n   * Encrypt command execution and save entire entity\n   * Should always throw and error in case when unable to encrypt for some reason\n   * @param _\n   * @param pluginState\n   */\n  async upsert(\n    _: SessionMetadata,\n    pluginState: Partial<PluginState>,\n  ): Promise<void> {\n    const entity = plainToInstance(PluginStateEntity, pluginState);\n    try {\n      await this.repository.save(\n        await this.modelEncryptor.encryptEntity(entity),\n      );\n    } catch (e) {\n      if (e.code === 'SQLITE_CONSTRAINT') {\n        throw new NotFoundException(ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND);\n      }\n\n      throw e;\n    }\n  }\n\n  /**\n   * Get single command execution entity, decrypt and convert to model\n   *\n   * @param _\n   * @param visualizationId\n   * @param commandExecutionId\n   */\n  async getOne(\n    sessionMetadata: SessionMetadata,\n    visualizationId: string,\n    commandExecutionId: string,\n  ): Promise<PluginState> {\n    this.logger.debug('Getting plugin state', sessionMetadata);\n\n    const entity = await this.repository.findOneBy({\n      visualizationId,\n      commandExecutionId,\n    });\n\n    if (!entity) {\n      this.logger.error(\n        `Plugin state ${commandExecutionId}:${visualizationId} was not Found`,\n        sessionMetadata,\n      );\n      throw new NotFoundException(ERROR_MESSAGES.PLUGIN_STATE_NOT_FOUND);\n    }\n\n    this.logger.debug(\n      `Succeed to get plugin state ${commandExecutionId}:${visualizationId}`,\n      sessionMetadata,\n    );\n\n    const decryptedEntity = await this.modelEncryptor.decryptEntity(\n      entity,\n      true,\n    );\n\n    return classToClass(PluginState, decryptedEntity);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/repositories/plugin-state.repository.ts",
    "content": "import { PluginState } from 'src/modules/workbench/models/plugin-state';\nimport { SessionMetadata } from 'src/common/models';\n\nexport abstract class PluginStateRepository {\n  abstract upsert(\n    sessionMetadata: SessionMetadata,\n    pluginState: Partial<PluginState>,\n  ): Promise<void>;\n  abstract getOne(\n    sessionMetadata: SessionMetadata,\n    visualizationId: string,\n    commandExecutionId: string,\n  ): Promise<PluginState>;\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.spec.ts",
    "content": "import { getUnsupportedCommands } from './getUnsupportedCommands';\n\ndescribe('workbench unsupported commands', () => {\n  it('should return correct list', () => {\n    const expectedResult = [\n      'monitor',\n      'subscribe',\n      'psubscribe',\n      'ssubscribe',\n      'sync',\n      'psync',\n      'script debug',\n      'select',\n      'hello 3',\n    ];\n\n    expect(getUnsupportedCommands()).toEqual(expectedResult);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/utils/getUnsupportedCommands.ts",
    "content": "import config from 'src/utils/config';\n\nconst WORKBENCH_CONFIG = config.get('workbench');\n\nexport enum WorkbenchToolUnsupportedCommands {\n  Monitor = 'monitor',\n  Subscribe = 'subscribe',\n  PSubscribe = 'psubscribe',\n  SSubscribe = 'ssubscribe',\n  Sync = 'sync',\n  PSync = 'psync',\n  ScriptDebug = 'script debug',\n  Select = 'select',\n  Hello3 = 'hello 3',\n}\n\nexport const getUnsupportedCommands = (): string[] => [\n  ...Object.values(WorkbenchToolUnsupportedCommands),\n  ...WORKBENCH_CONFIG.unsupportedCommands,\n];\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/workbench.analytics.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { ServiceUnavailableException } from '@nestjs/common';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport {\n  mockRedisWrongTypeError,\n  mockDatabase,\n  MockType,\n  mockSessionMetadata,\n} from 'src/__mocks__';\nimport { CommandType, TelemetryEvents } from 'src/constants';\nimport { ReplyError } from 'src/models';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { CommandParsingError } from 'src/modules/cli/constants/errors';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { WorkbenchAnalytics } from './workbench.analytics';\nimport { CommandExecutionType } from './models/command-execution';\n\nconst redisReplyError: ReplyError = {\n  ...mockRedisWrongTypeError,\n  command: { name: 'sadd' },\n};\nconst instanceId = mockDatabase.id;\n\nconst mockCommandsService = {\n  getCommandsGroups: jest.fn(),\n};\n\ndescribe('WorkbenchAnalytics', () => {\n  let service: WorkbenchAnalytics;\n  let sendEventMethod: jest.SpyInstance<WorkbenchAnalytics, unknown[]>;\n  let sendFailedEventMethod: jest.SpyInstance<WorkbenchAnalytics, unknown[]>;\n  let commandsService: MockType<CommandsService>;\n\n  beforeEach(async () => {\n    jest.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        EventEmitter2,\n        {\n          provide: CommandsService,\n          useFactory: () => mockCommandsService,\n        },\n        WorkbenchAnalytics,\n      ],\n    }).compile();\n\n    service = module.get<WorkbenchAnalytics>(WorkbenchAnalytics);\n    sendEventMethod = jest.spyOn<WorkbenchAnalytics, any>(service, 'sendEvent');\n    sendFailedEventMethod = jest.spyOn<WorkbenchAnalytics, any>(\n      service,\n      'sendFailedEvent',\n    );\n\n    commandsService = module.get(CommandsService);\n    commandsService.getCommandsGroups.mockResolvedValue({\n      main: {\n        SET: {\n          summary: 'Set the string value of a key',\n          since: '1.0.0',\n          group: 'string',\n          complexity: 'O(1)',\n          acl_categories: ['@write', '@string', '@slow'],\n        },\n      },\n      redisbloom: {\n        'BF.RESERVE': {\n          summary: 'Creates a new Bloom Filter',\n          complexity: 'O(1)',\n          since: '1.0.0',\n          group: 'bf',\n        },\n      },\n      custommodule: {\n        'CUSTOM.COMMAND': {\n          summary: 'Creates a new Bloom Filter',\n          complexity: 'O(1)',\n          since: '1.0.0',\n        },\n      },\n    });\n  });\n\n  describe('sendIndexInfoEvent', () => {\n    it('should emit index info event for Workbench commands', async () => {\n      service.sendIndexInfoEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        {\n          any: 'fields',\n        },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchIndexInfoSubmitted,\n        {\n          databaseId: instanceId,\n          any: 'fields',\n        },\n      );\n    });\n    it('should emit index info event for Search commands', async () => {\n      service.sendIndexInfoEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Search,\n        {\n          any: 'fields',\n        },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SearchIndexInfoSubmitted,\n        {\n          databaseId: instanceId,\n          any: 'fields',\n        },\n      );\n    });\n    it('should not fail and should not emit when no data to send', async () => {\n      service.sendIndexInfoEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        null,\n      );\n\n      expect(sendEventMethod).not.toHaveBeenCalled();\n    });\n  });\n  describe('sendCommandExecutedEvents', () => {\n    it('should emit multiple Workbench events', async () => {\n      await service.sendCommandExecutedEvents(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        [\n          { response: 'OK', status: CommandExecutionStatus.Success },\n          { response: 'OK', status: CommandExecutionStatus.Success },\n        ],\n        { command: 'set' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledTimes(2);\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'set',\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit multiple Search events', async () => {\n      await service.sendCommandExecutedEvents(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Search,\n        [\n          { response: 'OK', status: CommandExecutionStatus.Success },\n          { response: 'OK', status: CommandExecutionStatus.Success },\n        ],\n        { command: 'set' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledTimes(2);\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SearchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'set',\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n  });\n  describe('sendCommandExecutedEvent', () => {\n    it('should emit WorkbenchCommandExecuted event', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        { response: 'OK', status: CommandExecutionStatus.Success },\n        { command: 'set' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'set',\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit event if failed to fetch commands groups', async () => {\n      commandsService.getCommandsGroups.mockRejectedValue(\n        new Error('some error'),\n      );\n\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        { response: 'OK', status: CommandExecutionStatus.Success },\n        { command: 'set' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'set',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandExecuted event (module with cap.)', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        { response: 'OK', status: CommandExecutionStatus.Success },\n        { command: 'bF.rEsErvE' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'bF.rEsErvE',\n          commandType: CommandType.Module,\n          moduleName: 'redisbloom',\n          capability: 'bf',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandExecuted event (module w\\\\o cap.)', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        { response: 'OK', status: CommandExecutionStatus.Success },\n        { command: 'CUSTOM.COMMAnd' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'CUSTOM.COMMAnd',\n          commandType: CommandType.Module,\n          moduleName: 'custommodule',\n          capability: 'n/a',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandExecuted event (custom module)', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        { response: 'OK', status: CommandExecutionStatus.Success },\n        { command: 'some.command' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'some.command',\n          commandType: CommandType.Module,\n          moduleName: 'custom',\n          capability: 'n/a',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandExecuted event without additional data', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        {\n          response: 'OK',\n          status: CommandExecutionStatus.Success,\n        },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandExecuted,\n        {\n          databaseId: instanceId,\n        },\n      );\n    });\n    it('should emit WorkbenchCommandError event', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        {\n          response: 'Error',\n          error: redisReplyError,\n          status: CommandExecutionStatus.Fail,\n        },\n        { command: 'set', data: 'Some data' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandErrorReceived,\n        {\n          databaseId: instanceId,\n          error: ReplyError.name,\n          command: 'set',\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n          data: 'Some data',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandError event without additional data', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        {\n          response: 'Error',\n          error: redisReplyError,\n          status: CommandExecutionStatus.Fail,\n        },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandErrorReceived,\n        {\n          databaseId: instanceId,\n          error: ReplyError.name,\n          command: 'sadd',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandError event for custom error', async () => {\n      const error: any = CommandParsingError;\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        {\n          response: 'Error',\n          status: CommandExecutionStatus.Fail,\n          error,\n        },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandErrorReceived,\n        {\n          databaseId: instanceId,\n          error: CommandParsingError.name,\n          command: undefined,\n        },\n      );\n    });\n    it('should emit WorkbenchCommandError event for HttpException', async () => {\n      const error = new ServiceUnavailableException();\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Workbench,\n        {\n          response: 'Error',\n          status: CommandExecutionStatus.Fail,\n          error,\n        },\n      );\n\n      expect(sendFailedEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandErrorReceived,\n        error,\n        { databaseId: instanceId },\n      );\n    });\n    it('should emit SearchCommandExecuted event', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Search,\n        { response: 'OK', status: CommandExecutionStatus.Success },\n        { command: 'set' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SearchCommandExecuted,\n        {\n          databaseId: instanceId,\n          command: 'set',\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n        },\n      );\n    });\n    it('should emit SearchCommandError event', async () => {\n      await service.sendCommandExecutedEvent(\n        mockSessionMetadata,\n        instanceId,\n        CommandExecutionType.Search,\n        {\n          response: 'Error',\n          error: redisReplyError,\n          status: CommandExecutionStatus.Fail,\n        },\n        { command: 'set', data: 'Some data' },\n      );\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.SearchCommandErrorReceived,\n        {\n          databaseId: instanceId,\n          error: ReplyError.name,\n          command: 'set',\n          commandType: CommandType.Core,\n          moduleName: 'n/a',\n          capability: 'string',\n          data: 'Some data',\n        },\n      );\n    });\n  });\n  describe('sendCommandDeletedEvent', () => {\n    it('should emit WorkbenchCommandDeleted event', () => {\n      service.sendCommandDeletedEvent(mockSessionMetadata, instanceId, {\n        command: 'info',\n      });\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandDeleted,\n        {\n          databaseId: instanceId,\n          command: 'info',\n        },\n      );\n    });\n    it('should emit WorkbenchCommandDeleted event without additional data', () => {\n      service.sendCommandDeletedEvent(mockSessionMetadata, instanceId);\n\n      expect(sendEventMethod).toHaveBeenCalledWith(\n        mockSessionMetadata,\n        TelemetryEvents.WorkbenchCommandDeleted,\n        {\n          databaseId: instanceId,\n        },\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/workbench.analytics.ts",
    "content": "import { HttpException, Injectable } from '@nestjs/common';\nimport { TelemetryEvents } from 'src/constants';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { RedisError, ReplyError } from 'src/models';\nimport { EventEmitter2 } from '@nestjs/event-emitter';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { CommandTelemetryBaseService } from 'src/modules/analytics/command.telemetry.base.service';\nimport { SessionMetadata } from 'src/common/models';\nimport { CommandExecutionType } from './models/command-execution';\n\nexport interface IExecResult {\n  response: any;\n  status: CommandExecutionStatus;\n  error?: RedisError | ReplyError | Error;\n}\n\n@Injectable()\nexport class WorkbenchAnalytics extends CommandTelemetryBaseService {\n  constructor(\n    protected eventEmitter: EventEmitter2,\n    protected readonly commandsService: CommandsService,\n  ) {\n    super(eventEmitter, commandsService);\n  }\n\n  sendIndexInfoEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    commandExecutionType: CommandExecutionType,\n    additionalData: object,\n  ): void {\n    if (!additionalData) {\n      return;\n    }\n\n    try {\n      const event =\n        commandExecutionType === CommandExecutionType.Search\n          ? TelemetryEvents.SearchIndexInfoSubmitted\n          : TelemetryEvents.WorkbenchIndexInfoSubmitted;\n\n      this.sendEvent(sessionMetadata, event, {\n        databaseId,\n        ...additionalData,\n      });\n    } catch (e) {\n      // ignore error\n    }\n  }\n\n  public async sendCommandExecutedEvents(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    commandExecutionType: CommandExecutionType,\n    results: IExecResult[],\n    additionalData: object = {},\n  ): Promise<void> {\n    try {\n      await Promise.all(\n        results.map((result) =>\n          this.sendCommandExecutedEvent(\n            sessionMetadata,\n            databaseId,\n            commandExecutionType,\n            result,\n            additionalData,\n          ),\n        ),\n      );\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  public async sendCommandExecutedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    commandExecutionType: CommandExecutionType,\n    result: IExecResult,\n    additionalData: object = {},\n  ): Promise<void> {\n    const { status } = result;\n    try {\n      if (status === CommandExecutionStatus.Success) {\n        const event =\n          commandExecutionType === CommandExecutionType.Search\n            ? TelemetryEvents.SearchCommandExecuted\n            : TelemetryEvents.WorkbenchCommandExecuted;\n\n        this.sendEvent(sessionMetadata, event, {\n          databaseId,\n          ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n          ...additionalData,\n        });\n      }\n      if (status === CommandExecutionStatus.Fail) {\n        this.sendCommandErrorEvent(\n          sessionMetadata,\n          databaseId,\n          result.error,\n          commandExecutionType,\n          {\n            ...(await this.getCommandAdditionalInfo(additionalData['command'])),\n            ...additionalData,\n          },\n        );\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n\n  sendCommandDeletedEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    additionalData: object = {},\n  ): void {\n    this.sendEvent(sessionMetadata, TelemetryEvents.WorkbenchCommandDeleted, {\n      databaseId,\n      ...additionalData,\n    });\n  }\n\n  private sendCommandErrorEvent(\n    sessionMetadata: SessionMetadata,\n    databaseId: string,\n    error: any,\n    commandExecutionType: CommandExecutionType,\n    additionalData: object = {},\n  ): void {\n    try {\n      const event =\n        commandExecutionType === CommandExecutionType.Search\n          ? TelemetryEvents.SearchCommandErrorReceived\n          : TelemetryEvents.WorkbenchCommandErrorReceived;\n\n      if (error instanceof HttpException) {\n        this.sendFailedEvent(sessionMetadata, event, error, {\n          databaseId,\n          ...additionalData,\n        });\n      } else {\n        this.sendEvent(sessionMetadata, event, {\n          databaseId,\n          error: error.name,\n          command: error?.command?.name,\n          ...additionalData,\n        });\n      }\n    } catch (e) {\n      // continue regardless of error\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/workbench.controller.ts",
    "content": "import {\n  Body,\n  ClassSerializerInterceptor,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Post,\n  Query,\n  UseInterceptors,\n  UsePipes,\n  ValidationPipe,\n} from '@nestjs/common';\nimport { ApiTags } from '@nestjs/swagger';\nimport { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';\nimport { ApiRedisParams } from 'src/decorators/api-redis-params.decorator';\nimport { WorkbenchService } from 'src/modules/workbench/workbench.service';\nimport { CommandExecution } from 'src/modules/workbench/models/command-execution';\nimport { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto';\nimport { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution';\nimport { ClientMetadata } from 'src/common/models';\nimport { WorkbenchClientMetadata } from 'src/modules/workbench/decorators/workbench-client-metadata.decorator';\nimport { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter';\n\n@ApiTags('Workbench')\n@UsePipes(new ValidationPipe({ transform: true }))\n@Controller('workbench/command-executions')\nexport class WorkbenchController {\n  constructor(private service: WorkbenchService) {}\n\n  @ApiEndpoint({\n    description: 'Send Redis Batch Commands from the Workbench',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: CommandExecution,\n      },\n    ],\n  })\n  @Post()\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async sendCommands(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Body() dto: CreateCommandExecutionsDto,\n  ): Promise<CommandExecution[]> {\n    return this.service.createCommandExecutions(clientMetadata, dto);\n  }\n\n  @ApiEndpoint({\n    description: 'List of command executions',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: ShortCommandExecution,\n        isArray: true,\n      },\n    ],\n  })\n  @Get()\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async listCommandExecutions(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Query() filter: CommandExecutionFilter,\n  ): Promise<ShortCommandExecution[]> {\n    return this.service.listCommandExecutions(clientMetadata, filter);\n  }\n\n  @ApiEndpoint({\n    description: 'Get command execution details',\n    statusCode: 200,\n    responses: [\n      {\n        status: 200,\n        type: CommandExecution,\n      },\n    ],\n  })\n  @Get('/:id')\n  @UseInterceptors(ClassSerializerInterceptor)\n  @ApiRedisParams()\n  async getCommandExecution(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Param('id') id: string,\n  ): Promise<CommandExecution> {\n    return this.service.getCommandExecution(clientMetadata, id);\n  }\n\n  @ApiEndpoint({\n    description: 'Delete command execution',\n    statusCode: 200,\n  })\n  @Delete('/:id')\n  @ApiRedisParams()\n  async deleteCommandExecution(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Param('id') id: string,\n  ): Promise<void> {\n    return this.service.deleteCommandExecution(clientMetadata, id);\n  }\n\n  @ApiEndpoint({\n    description: 'Delete command executions',\n    statusCode: 200,\n  })\n  @Delete()\n  @ApiRedisParams()\n  async deleteCommandExecutions(\n    @WorkbenchClientMetadata() clientMetadata: ClientMetadata,\n    @Body() filter: CommandExecutionFilter,\n  ): Promise<void> {\n    return this.service.deleteCommandExecutions(clientMetadata, filter);\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/workbench.module.ts",
    "content": "import { DynamicModule, Module, Type } from '@nestjs/common';\nimport { WorkbenchController } from 'src/modules/workbench/workbench.controller';\nimport { WorkbenchService } from 'src/modules/workbench/workbench.service';\nimport { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';\nimport { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository';\nimport { PluginStateRepository } from 'src/modules/workbench/repositories/plugin-state.repository';\nimport { CommandsModule } from 'src/modules/commands/commands.module';\nimport { CommandsService } from 'src/modules/commands/commands.service';\nimport { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider';\nimport { PluginsService } from 'src/modules/workbench/plugins.service';\nimport { PluginCommandsWhitelistProvider } from 'src/modules/workbench/providers/plugin-commands-whitelist.provider';\nimport { PluginsController } from 'src/modules/workbench/plugins.controller';\nimport { LocalPluginStateRepository } from 'src/modules/workbench/repositories/local-plugin-state.repository';\nimport { LocalCommandExecutionRepository } from 'src/modules/workbench/repositories/local-command-execution.repository';\nimport config from 'src/utils/config';\nimport { WorkbenchAnalytics } from 'src/modules/workbench/workbench.analytics';\n\nconst COMMANDS_CONFIGS = config.get('commands');\n\n@Module({})\nexport class WorkbenchModule {\n  static register(\n    commandExecutionRepository: Type<CommandExecutionRepository> = LocalCommandExecutionRepository,\n    pluginStateRepository: Type<PluginStateRepository> = LocalPluginStateRepository,\n  ): DynamicModule {\n    return {\n      module: WorkbenchModule,\n      imports: [CommandsModule],\n      controllers: [WorkbenchController, PluginsController],\n      providers: [\n        WorkbenchService,\n        WorkbenchCommandsExecutor,\n        {\n          provide: CommandExecutionRepository,\n          useClass: commandExecutionRepository,\n        },\n        {\n          provide: PluginStateRepository,\n          useClass: pluginStateRepository,\n        },\n        {\n          provide: CommandsService,\n          useFactory: () =>\n            new CommandsService(\n              COMMANDS_CONFIGS.map(\n                ({ name, url }) => new CommandsJsonProvider(name, url),\n              ),\n            ),\n        },\n        PluginsService,\n        PluginCommandsWhitelistProvider,\n        WorkbenchAnalytics,\n      ],\n    };\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/workbench.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { when } from 'jest-when';\nimport {\n  mockCommandExecution,\n  mockCommandExecutionFilter,\n  mockCommandExecutionRepository,\n  mockCommandExecutionSuccessResult,\n  mockCreateCommandExecutionDto,\n  mockDatabaseClientFactory,\n  mockStandaloneRedisClient,\n  MockType,\n  mockWorkbenchAnalyticsService,\n  mockWorkbenchClientMetadata,\n  mockWorkbenchCommandsExecutor,\n} from 'src/__mocks__';\nimport { WorkbenchService } from 'src/modules/workbench/workbench.service';\nimport { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';\nimport { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository';\nimport {\n  ResultsMode,\n  RunQueryMode,\n} from 'src/modules/workbench/models/command-execution';\nimport { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport {\n  BadRequestException,\n  InternalServerErrorException,\n} from '@nestjs/common';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { WorkbenchAnalytics } from 'src/modules/workbench/workbench.analytics';\n\nconst mockCommands = ['set 1 1', 'get 1'];\n\nconst mockCreateCommandExecutionDtoWithGroupMode: CreateCommandExecutionsDto = {\n  commands: mockCommands,\n  mode: RunQueryMode.ASCII,\n  resultsMode: ResultsMode.GroupMode,\n};\n\nconst mockCreateCommandExecutionDtoWithSilentMode: CreateCommandExecutionsDto =\n  {\n    commands: mockCommands,\n    mode: RunQueryMode.ASCII,\n    resultsMode: ResultsMode.Silent,\n  };\n\nconst mockCreateCommandExecutionsDto: CreateCommandExecutionsDto = {\n  commands: [\n    mockCreateCommandExecutionDto.command,\n    mockCreateCommandExecutionDto.command,\n  ],\n  ...mockCreateCommandExecutionDto,\n};\n\nconst mockCommandExecutionResults: CommandExecutionResult[] = [\n  mockCommandExecutionSuccessResult,\n];\n\nconst mockSendCommandResultSuccess = { response: '1', status: 'success' };\nconst mockSendCommandResultFail = { response: 'error', status: 'fail' };\n\nconst mockCommandExecutionWithGroupMode = {\n  mode: 'ASCII',\n  commands: mockCommands,\n  resultsMode: 'GROUP_MODE',\n  databaseId: 'd05043d0 - 0d12- 4ce1-9ca3 - 30c6d7e391ea',\n  summary: { total: 2, success: 1, fail: 1 },\n  command: 'set 1 1\\r\\nget 1',\n  result: [\n    {\n      status: 'success',\n      response: [\n        { response: 'OK', status: 'success', command: 'set 1 1' },\n        { response: 'error', status: 'fail', command: 'get 1' },\n      ],\n    },\n  ],\n};\n\nconst mockCommandExecutionWithSilentMode = {\n  mode: 'ASCII',\n  commands: mockCommands,\n  resultsMode: 'GROUP_MODE',\n  databaseId: 'd05043d0 - 0d12- 4ce1-9ca3 - 30c6d7e391ea',\n  summary: { total: 2, success: 1, fail: 1 },\n  command: 'set 1 1\\r\\nget 1',\n  result: [\n    {\n      status: 'success',\n      response: [{ response: 'error', status: 'fail', command: 'get 1' }],\n    },\n  ],\n};\n\ndescribe('WorkbenchService', () => {\n  let service: WorkbenchService;\n  let workbenchCommandsExecutor: MockType<WorkbenchCommandsExecutor>;\n  let commandExecutionRepository: MockType<CommandExecutionRepository>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        WorkbenchService,\n        {\n          provide: WorkbenchAnalytics,\n          useFactory: mockWorkbenchAnalyticsService,\n        },\n        {\n          provide: WorkbenchCommandsExecutor,\n          useFactory: mockWorkbenchCommandsExecutor,\n        },\n        {\n          provide: CommandExecutionRepository,\n          useFactory: mockCommandExecutionRepository,\n        },\n        {\n          provide: DatabaseClientFactory,\n          useFactory: mockDatabaseClientFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get(WorkbenchService);\n    workbenchCommandsExecutor = module.get(WorkbenchCommandsExecutor);\n    commandExecutionRepository = module.get(CommandExecutionRepository);\n  });\n\n  describe('createCommandExecution', () => {\n    it('should successfully execute command and save it', async () => {\n      const result = await service.createCommandExecution(\n        mockStandaloneRedisClient,\n        mockCreateCommandExecutionDto,\n      );\n      expect(result).toEqual({\n        ...mockCommandExecution,\n        executionTime: result.executionTime,\n        id: undefined, // result was not saved yet\n        createdAt: undefined, // result was not saved yet\n      });\n    });\n    it('should save db index', async () => {\n      const db = 2;\n      mockStandaloneRedisClient.getCurrentDbIndex.mockResolvedValueOnce(db);\n      const result = await service.createCommandExecution(\n        mockStandaloneRedisClient,\n        mockCreateCommandExecutionDto,\n      );\n      expect(result).toEqual({\n        ...mockCommandExecution,\n        executionTime: result.executionTime,\n        id: undefined, // result was not saved yet\n        createdAt: undefined, // result was not saved yet\n        db,\n      });\n    });\n    it('should save result as unsupported command message', async () => {\n      mockStandaloneRedisClient.getCurrentDbIndex = jest\n        .fn()\n        .mockResolvedValueOnce(0);\n      workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(\n        mockCommandExecutionResults,\n      );\n\n      const dto = {\n        ...mockCommandExecutionResults,\n        command: 'subscribe',\n        mode: RunQueryMode.ASCII,\n      };\n\n      expect(\n        await service.createCommandExecution(mockStandaloneRedisClient, dto),\n      ).toEqual({\n        ...dto,\n        db: 0,\n        databaseId: mockWorkbenchClientMetadata.databaseId,\n        result: [\n          {\n            response: ERROR_MESSAGES.WORKBENCH_COMMAND_NOT_SUPPORTED(\n              dto.command.toUpperCase(),\n            ),\n            status: CommandExecutionStatus.Fail,\n          },\n        ],\n      });\n    });\n    it('should throw an error when command execution failed', async () => {\n      workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(\n        new BadRequestException('error'),\n      );\n\n      const dto = {\n        ...mockCommandExecutionResults,\n        command: 'scan 0',\n        mode: RunQueryMode.ASCII,\n      };\n\n      try {\n        await service.createCommandExecution(mockStandaloneRedisClient, dto);\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n  });\n  describe('createCommandExecutions', () => {\n    it('should successfully execute commands and save them', async () => {\n      workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce([\n        mockCommandExecutionResults,\n        mockCommandExecutionResults,\n      ]);\n      commandExecutionRepository.createMany.mockResolvedValueOnce([\n        mockCommandExecution,\n        mockCommandExecution,\n      ]);\n\n      const result = await service.createCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCreateCommandExecutionsDto,\n      );\n\n      expect(result).toEqual([mockCommandExecution, mockCommandExecution]);\n    });\n    it('should successfully execute commands and save in group mode view', async () => {\n      when(workbenchCommandsExecutor.sendCommand)\n        .calledWith(mockStandaloneRedisClient, expect.anything())\n        .mockResolvedValue([mockSendCommandResultSuccess]);\n\n      commandExecutionRepository.createMany.mockResolvedValueOnce([\n        mockCommandExecutionWithGroupMode,\n      ]);\n\n      const result = await service.createCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCreateCommandExecutionDtoWithGroupMode,\n      );\n\n      expect(result).toEqual([mockCommandExecutionWithGroupMode]);\n    });\n    it('should successfully execute commands and save in silent mode view', async () => {\n      when(workbenchCommandsExecutor.sendCommand)\n        .calledWith(mockStandaloneRedisClient, expect.anything())\n        .mockResolvedValue([mockSendCommandResultSuccess]);\n\n      commandExecutionRepository.createMany.mockResolvedValueOnce([\n        mockCommandExecutionWithSilentMode,\n      ]);\n\n      const result = await service.createCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCreateCommandExecutionDtoWithSilentMode,\n      );\n\n      expect(result).toEqual([mockCommandExecutionWithSilentMode]);\n    });\n\n    it('should successfully execute commands with error and save summary', async () => {\n      when(workbenchCommandsExecutor.sendCommand)\n        .calledWith(mockStandaloneRedisClient, {\n          ...mockCreateCommandExecutionDtoWithGroupMode,\n          command: mockCommands[0],\n        })\n        .mockResolvedValue([mockSendCommandResultSuccess]);\n\n      when(workbenchCommandsExecutor.sendCommand)\n        .calledWith(mockStandaloneRedisClient, {\n          ...mockCreateCommandExecutionDtoWithGroupMode,\n          command: mockCommands[1],\n        })\n        .mockResolvedValue([mockSendCommandResultFail]);\n\n      commandExecutionRepository.createMany.mockResolvedValueOnce([\n        mockCommandExecutionWithGroupMode,\n      ]);\n\n      const result = await service.createCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCreateCommandExecutionDtoWithGroupMode,\n      );\n\n      expect(result).toEqual([mockCommandExecutionWithGroupMode]);\n    });\n\n    it('should successfully execute commands with error and save summary in silent mode view', async () => {\n      when(workbenchCommandsExecutor.sendCommand)\n        .calledWith(mockStandaloneRedisClient, {\n          ...mockCreateCommandExecutionDtoWithSilentMode,\n          command: mockCommands[0],\n        })\n        .mockResolvedValue([mockSendCommandResultSuccess]);\n\n      when(workbenchCommandsExecutor.sendCommand)\n        .calledWith(mockStandaloneRedisClient, {\n          ...mockCreateCommandExecutionDtoWithSilentMode,\n          command: mockCommands[1],\n        })\n        .mockResolvedValue([mockSendCommandResultFail]);\n\n      commandExecutionRepository.createMany.mockResolvedValueOnce([\n        mockCommandExecutionWithSilentMode,\n      ]);\n\n      const result = await service.createCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCreateCommandExecutionDtoWithSilentMode,\n      );\n\n      expect(result).toEqual([mockCommandExecutionWithSilentMode]);\n    });\n\n    it('should throw an error when command execution failed', async () => {\n      workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(\n        new BadRequestException('error'),\n      );\n\n      try {\n        await service.createCommandExecutions(\n          mockWorkbenchClientMetadata,\n          mockCreateCommandExecutionsDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(BadRequestException);\n      }\n    });\n    it('should throw an error from command execution provider (create)', async () => {\n      workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce([\n        mockCommandExecutionResults,\n      ]);\n      commandExecutionRepository.createMany.mockRejectedValueOnce(\n        new InternalServerErrorException('db error'),\n      );\n\n      try {\n        await service.createCommandExecutions(\n          mockWorkbenchClientMetadata,\n          mockCreateCommandExecutionsDto,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n  describe('listCommandExecutions', () => {\n    it('should return list of command executions', async () => {\n      commandExecutionRepository.getList.mockResolvedValueOnce([\n        mockCommandExecution,\n        mockCommandExecution,\n      ]);\n\n      const result = await service.listCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCommandExecutionFilter,\n      );\n\n      expect(result).toEqual([mockCommandExecution, mockCommandExecution]);\n    });\n    it('should throw an error from command execution provider (getList)', async () => {\n      commandExecutionRepository.getList.mockRejectedValueOnce(\n        new InternalServerErrorException(),\n      );\n\n      try {\n        await service.listCommandExecutions(\n          mockWorkbenchClientMetadata,\n          mockCommandExecutionFilter,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n  describe('getCommandExecution', () => {\n    it('should return full command executions', async () => {\n      commandExecutionRepository.getOne.mockResolvedValueOnce(\n        mockCommandExecution,\n      );\n\n      const result = await service.getCommandExecution(\n        mockWorkbenchClientMetadata,\n        mockCommandExecution.id,\n      );\n\n      expect(result).toEqual(mockCommandExecution);\n    });\n    it('should throw an error from command execution provider (getOne)', async () => {\n      commandExecutionRepository.getOne.mockRejectedValueOnce(\n        new InternalServerErrorException(),\n      );\n\n      try {\n        await service.getCommandExecution(\n          mockWorkbenchClientMetadata,\n          mockCommandExecution.id,\n        );\n        fail();\n      } catch (e) {\n        expect(e).toBeInstanceOf(InternalServerErrorException);\n      }\n    });\n  });\n  describe('deleteCommandExecution', () => {\n    it('should not return anything on delete', async () => {\n      commandExecutionRepository.delete.mockResolvedValueOnce('some response');\n\n      const result = await service.deleteCommandExecution(\n        mockWorkbenchClientMetadata,\n        mockCommandExecution.id,\n      );\n\n      expect(result).toEqual(undefined);\n    });\n  });\n  describe('deleteCommandExecutions', () => {\n    it('should not return anything on delete', async () => {\n      commandExecutionRepository.deleteAll.mockResolvedValueOnce(\n        'some response',\n      );\n\n      const result = await service.deleteCommandExecutions(\n        mockWorkbenchClientMetadata,\n        mockCommandExecutionFilter,\n      );\n\n      expect(result).toEqual(undefined);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/modules/workbench/workbench.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { omit } from 'lodash';\nimport { ClientMetadata } from 'src/common/models';\nimport { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';\nimport {\n  CommandExecution,\n  ResultsMode,\n} from 'src/modules/workbench/models/command-execution';\nimport { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto';\nimport { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto';\nimport {\n  getBlockingCommands,\n  multilineCommandToOneLine,\n} from 'src/utils/cli-helper';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution';\nimport { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';\nimport { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository';\nimport { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter';\nimport { WorkbenchAnalytics } from 'src/modules/workbench/workbench.analytics';\nimport { getUnsupportedCommands } from './utils/getUnsupportedCommands';\n\n@Injectable()\nexport class WorkbenchService {\n  constructor(\n    private readonly databaseClientFactory: DatabaseClientFactory,\n    private commandsExecutor: WorkbenchCommandsExecutor,\n    private commandExecutionRepository: CommandExecutionRepository,\n    private analyticsService: WorkbenchAnalytics,\n  ) {}\n\n  /**\n   * Send redis command from workbench and save history\n   *\n   * @param client\n   * @param dto\n   */\n  async createCommandExecution(\n    client: RedisClient,\n    dto: CreateCommandExecutionDto,\n  ): Promise<Partial<CommandExecution>> {\n    const commandExecution: Partial<CommandExecution> = {\n      ...omit(dto, 'commands'),\n      db: await client.getCurrentDbIndex(),\n      databaseId: client.clientMetadata.databaseId,\n    };\n\n    const command = multilineCommandToOneLine(dto.command);\n    const deprecatedCommand = this.findCommandInBlackList(command);\n    if (deprecatedCommand) {\n      commandExecution.result = [\n        {\n          response: ERROR_MESSAGES.WORKBENCH_COMMAND_NOT_SUPPORTED(\n            deprecatedCommand.toUpperCase(),\n          ),\n          status: CommandExecutionStatus.Fail,\n        },\n      ];\n    } else {\n      const startCommandExecutionTime = process.hrtime.bigint();\n      commandExecution.result = await this.commandsExecutor.sendCommand(\n        client,\n        { ...dto, command },\n      );\n      const endCommandExecutionTime = process.hrtime.bigint();\n      commandExecution.executionTime = Math.round(\n        Number(endCommandExecutionTime - startCommandExecutionTime) / 1000,\n      );\n    }\n\n    return commandExecution;\n  }\n\n  /**\n   * Send redis command from workbench and save history\n   *\n   * @param client\n   * @param dto\n   * @param commands\n   * @param onlyErrorResponse\n   */\n  async createCommandsExecution(\n    client: RedisClient,\n    dto: Partial<CreateCommandExecutionDto>,\n    commands: string[],\n    onlyErrorResponse: boolean = false,\n  ): Promise<Partial<CommandExecution>> {\n    const commandExecution: Partial<CommandExecution> = {\n      ...dto,\n      db: await client.getCurrentDbIndex(),\n      databaseId: client.clientMetadata.databaseId,\n    };\n\n    const startCommandExecutionTime = process.hrtime.bigint();\n\n    const executionResults = await Promise.all(\n      commands.map(async (singleCommand) => {\n        const command = multilineCommandToOneLine(singleCommand);\n        const deprecatedCommand = this.findCommandInBlackList(command);\n        if (deprecatedCommand) {\n          return {\n            command,\n            response: ERROR_MESSAGES.WORKBENCH_COMMAND_NOT_SUPPORTED(\n              deprecatedCommand.toUpperCase(),\n            ),\n            status: CommandExecutionStatus.Fail,\n          };\n        }\n        const result = await this.commandsExecutor.sendCommand(client, {\n          ...dto,\n          command,\n        });\n        return { ...result[0], command };\n      }),\n    );\n\n    const executionTimeInNanoseconds =\n      process.hrtime.bigint() - startCommandExecutionTime;\n\n    if (Number(executionTimeInNanoseconds) !== 0) {\n      commandExecution.executionTime = Math.round(\n        Number(executionTimeInNanoseconds) / 1000,\n      );\n    }\n\n    const successCommands = executionResults.filter(\n      (command) => command.status === CommandExecutionStatus.Success,\n    );\n    const failedCommands = executionResults.filter(\n      (command) => command.status === CommandExecutionStatus.Fail,\n    );\n\n    commandExecution.summary = {\n      total: executionResults.length,\n      success: successCommands.length,\n      fail: failedCommands.length,\n    };\n\n    commandExecution.command = commands.join('\\r\\n');\n    commandExecution.result = [\n      {\n        status: CommandExecutionStatus.Success,\n        response: onlyErrorResponse ? failedCommands : executionResults,\n      },\n    ];\n\n    return commandExecution;\n  }\n\n  /**\n   * Send redis command from workbench and save history\n   *\n   * @param clientMetadata\n   * @param dto\n   */\n  async createCommandExecutions(\n    clientMetadata: ClientMetadata,\n    dto: CreateCommandExecutionsDto,\n  ): Promise<CommandExecution[]> {\n    // todo: handle concurrent client creation on RedisModule side\n    // temporary workaround. Just create client before any command execution precess\n    const client: RedisClient =\n      await this.databaseClientFactory.getOrCreateClient(clientMetadata);\n\n    if (\n      dto.resultsMode === ResultsMode.GroupMode ||\n      dto.resultsMode === ResultsMode.Silent\n    ) {\n      return this.commandExecutionRepository.createMany(\n        clientMetadata.sessionMetadata,\n        [\n          await this.createCommandsExecution(\n            client,\n            dto,\n            dto.commands,\n            dto.resultsMode === ResultsMode.Silent,\n          ),\n        ],\n      );\n    }\n    // todo: rework to support pipeline\n    // prepare and execute commands\n    const commandExecutions = await Promise.all(\n      dto.commands.map(\n        async (command) =>\n          await this.createCommandExecution(client, { ...dto, command }),\n      ),\n    );\n\n    // save history\n    // todo: rework\n    return this.commandExecutionRepository.createMany(\n      clientMetadata.sessionMetadata,\n      commandExecutions,\n    );\n  }\n\n  /**\n   * Get list command execution history per instance (last 30 items)\n   *\n   * @param clientMetadata\n   * @param filter\n   */\n  async listCommandExecutions(\n    clientMetadata: ClientMetadata,\n    filter: CommandExecutionFilter,\n  ): Promise<ShortCommandExecution[]> {\n    return this.commandExecutionRepository.getList(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      filter,\n    );\n  }\n\n  /**\n   * Get command execution details\n   *\n   * @param clientMetadata\n   * @param id\n   */\n  async getCommandExecution(\n    clientMetadata: ClientMetadata,\n    id: string,\n  ): Promise<CommandExecution> {\n    return this.commandExecutionRepository.getOne(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      id,\n    );\n  }\n\n  /**\n   * Delete command execution by id and databaseId\n   *\n   * @param clientMetadata\n   * @param id\n   */\n  async deleteCommandExecution(\n    clientMetadata: ClientMetadata,\n    id: string,\n  ): Promise<void> {\n    await this.commandExecutionRepository.delete(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      id,\n    );\n    this.analyticsService.sendCommandDeletedEvent(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n    );\n  }\n\n  /**\n   * Delete command executions by databaseId\n   *\n   * @param clientMetadata\n   * @param filter\n   */\n  async deleteCommandExecutions(\n    clientMetadata: ClientMetadata,\n    filter: CommandExecutionFilter,\n  ): Promise<void> {\n    await this.commandExecutionRepository.deleteAll(\n      clientMetadata.sessionMetadata,\n      clientMetadata.databaseId,\n      filter,\n    );\n  }\n\n  /**\n   * Check if workbench allows such command\n   * @param commandLine\n   * @private\n   */\n  private findCommandInBlackList(commandLine: string): string {\n    const targetCommand = commandLine.toLowerCase();\n    return getUnsupportedCommands()\n      .concat(getBlockingCommands())\n      .find((command) => targetCommand.startsWith(command));\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/src/utils/analytics-helper.spec.ts",
    "content": "import {\n  calculateRedisHitRatio,\n  getRangeForNumber,\n  getAnalyticsDataFromIndexInfo,\n} from 'src/utils/analytics-helper';\nimport {\n  mockFtInfoAnalyticsData,\n  mockRedisFtInfoReply,\n  replyToBuffer,\n} from 'src/__mocks__';\n\n/* eslint-disable sonarjs/no-duplicate-string */\nconst getRangeForNumberTests = [\n  { input: null, output: undefined },\n  { input: undefined, output: undefined },\n  { input: 0, output: '0 - 500 000' },\n  { input: 100, output: '0 - 500 000' },\n  { input: 500000, output: '0 - 500 000' },\n  { input: 500001, output: '500 001 - 1 000 000' },\n  { input: 600000, output: '500 001 - 1 000 000' },\n  { input: 1000000, output: '500 001 - 1 000 000' },\n  { input: 1000001, output: '1 000 001 - 10 000 000' },\n  { input: 2000000, output: '1 000 001 - 10 000 000' },\n  { input: 10000000, output: '1 000 001 - 10 000 000' },\n  { input: 10000001, output: '10 000 001 - 50 000 000' },\n  { input: 20000000, output: '10 000 001 - 50 000 000' },\n  { input: 50000000, output: '10 000 001 - 50 000 000' },\n  { input: 50000001, output: '50 000 001 - 100 000 000' },\n  { input: 60000000, output: '50 000 001 - 100 000 000' },\n  { input: 100000000, output: '50 000 001 - 100 000 000' },\n  { input: 100000001, output: '100 000 001 - 1 000 000 000' },\n  { input: 200000000, output: '100 000 001 - 1 000 000 000' },\n  { input: 1000000000, output: '100 000 001 - 1 000 000 000' },\n  { input: 1000000001, output: '1 000 000 001 +' },\n  { input: 2000000000, output: '1 000 000 001 +' },\n];\n/* eslint-enable sonarjs/no-duplicate-string */\nconst calculateRedisHitRatioTests = [\n  { input: { hits: null, misses: null }, output: undefined },\n  { input: { hits: undefined, misses: undefined }, output: undefined },\n  { input: { hits: 1, misses: undefined }, output: undefined },\n  { input: { hits: undefined, misses: 1 }, output: undefined },\n  { input: { hits: null, misses: 1 }, output: undefined },\n  { input: { hits: 1, misses: null }, output: undefined },\n  { input: { hits: NaN, misses: NaN }, output: undefined },\n  { input: { hits: NaN, misses: NaN }, output: undefined },\n  { input: { hits: NaN, misses: 'string' }, output: undefined },\n  { input: { hits: 'string', misses: 'string' }, output: undefined },\n  { input: { hits: 2, misses: 2 }, output: 0.5 },\n  { input: { hits: 1, misses: 2 }, output: 0.3333333333333333 },\n  { input: { hits: 62409, misses: 0 }, output: 1 },\n  { input: { hits: 62409, misses: 109669 }, output: 0.3626785527493346 },\n  { input: { hits: '62409', misses: '109669' }, output: 0.3626785527493346 },\n  { input: { hits: '62409', misses: 109669 }, output: 0.3626785527493346 },\n  { input: { hits: '0', misses: 109669 }, output: 1 },\n  { input: { hits: 0, misses: 109669 }, output: 1 },\n];\n\ndescribe('getRangeForNumber', () => {\n  getRangeForNumberTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n      const result = getRangeForNumber(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\ndescribe('calculateRedisHitRatio', () => {\n  calculateRedisHitRatioTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${JSON.stringify(\n      test.input,\n    )} `, async () => {\n      const result = calculateRedisHitRatio(test.input.hits, test.input.misses);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\ndescribe('getAnalyticsDataFromIndexInfo', () => {\n  it('should return proper analytics data', () => {\n    expect(\n      getAnalyticsDataFromIndexInfo(mockRedisFtInfoReply as string[]),\n    ).toEqual(mockFtInfoAnalyticsData);\n  });\n  it('should return proper analytics data when buffers received', () => {\n    expect(\n      getAnalyticsDataFromIndexInfo(\n        replyToBuffer(mockRedisFtInfoReply) as string[],\n      ),\n    ).toEqual(mockFtInfoAnalyticsData);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/analytics-helper.ts",
    "content": "import { includes, isNil, map } from 'lodash';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils/reply.util';\n\nexport const TOTAL_KEYS_BREAKPOINTS = [\n  500000, 1000000, 10000000, 50000000, 100000000, 1000000000,\n];\n\nexport const SCAN_THRESHOLD_BREAKPOINTS = [5000, 10000, 50000, 100000, 1000000];\n\nexport const BULK_ACTIONS_BREAKPOINTS = [5000, 10000, 50000, 100000, 1000000];\n\nconst numberWithSpaces = (x: number): string =>\n  x.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ' ');\n\nexport const getRangeForNumber = (\n  value: number,\n  breakpoints: number[] = TOTAL_KEYS_BREAKPOINTS,\n): string => {\n  if (isNil(value)) {\n    return undefined;\n  }\n  const index = breakpoints.findIndex(\n    (threshold: number) => value <= threshold,\n  );\n  if (index === 0) {\n    return `0 - ${numberWithSpaces(breakpoints[0])}`;\n  }\n  if (index === -1) {\n    const lastItem = breakpoints[breakpoints.length - 1];\n    return `${numberWithSpaces(lastItem + 1)} +`;\n  }\n  return `${numberWithSpaces(\n    breakpoints[index - 1] + 1,\n  )} - ${numberWithSpaces(breakpoints[index])}`;\n};\n\nexport const calculateRedisHitRatio = (\n  keyspaceHits: string | number,\n  keyspaceMisses: string | number,\n): number => {\n  try {\n    if (isNil(keyspaceHits) || isNil(keyspaceMisses)) {\n      return undefined;\n    }\n    const keyspaceHitsValue = +keyspaceHits;\n    const keyspaceMissesValue = +keyspaceMisses;\n    if (keyspaceHitsValue === 0) {\n      return 1;\n    }\n    const result =\n      keyspaceHitsValue / (keyspaceHitsValue + keyspaceMissesValue);\n    return Number.isNaN(result) ? undefined : result;\n  } catch (error) {\n    return undefined;\n  }\n};\n\nexport const getIsPipelineEnable = (size: number): boolean => size > 1;\n\nexport const getAnalyticsDataFromIndexInfo = (reply: string[]): object => {\n  const analyticsData = {};\n\n  try {\n    const replyInfo = convertArrayReplyToObject(reply, { utf: true });\n    const definition = convertArrayReplyToObject(replyInfo.index_definition, {\n      utf: true,\n    });\n\n    analyticsData['key_type'] = definition?.key_type;\n    analyticsData['default_score'] = definition?.default_score;\n    analyticsData['num_docs'] = replyInfo?.num_docs;\n    analyticsData['max_doc_id'] = replyInfo?.max_doc_id;\n    analyticsData['num_terms'] = replyInfo?.num_terms;\n    analyticsData['num_records'] = replyInfo?.num_records;\n    analyticsData['total_indexing_time'] = replyInfo?.total_indexing_time;\n    analyticsData['number_of_uses'] = replyInfo?.number_of_uses;\n    analyticsData['cleaning'] = replyInfo?.cleaning;\n\n    if (replyInfo.dialect_stats) {\n      analyticsData['dialect_stats'] = convertArrayReplyToObject(\n        replyInfo.dialect_stats,\n        { utf: true },\n      );\n    }\n\n    analyticsData['attributes'] = map(replyInfo?.attributes, (attr) => {\n      const attrArray = map(attr, (str) => str.toString().toLowerCase());\n      const attrObject = convertArrayReplyToObject(attr, { utf: true });\n\n      return {\n        type: attrObject?.['type'],\n        weight: attrObject?.['weight'] || undefined,\n        phonetic: attrObject?.['phonetic'] || undefined,\n        sortable: includes(attrArray, 'sortable') || undefined,\n        nostem: includes(attrArray, 'nostem') || undefined,\n        unf: includes(attrArray, 'unf') || undefined,\n        noindex: includes(attrArray, 'noindex') || undefined,\n        casesensitive: includes(attrArray, 'casesensitive') || undefined,\n      };\n    });\n\n    return analyticsData;\n  } catch (e) {\n    // ignore errors\n    return null;\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/base.helper.spec.ts",
    "content": "import { isJson, numberWithSpaces } from 'src/utils/base.helper';\n\nconst numberWithSpacesTests = [\n  { input: 0, output: '0' },\n  { input: 10, output: '10' },\n  { input: 100, output: '100' },\n  { input: 1000, output: '1 000' },\n  { input: 1000.001, output: '1 000.001' },\n  { input: 5500, output: '5 500' },\n  { input: 1000000, output: '1 000 000' },\n  { input: 1233543234543243, output: '1 233 543 234 543 243' },\n  { input: NaN, output: 'NaN' },\n];\n\ndescribe('numberWithSpaces', () => {\n  numberWithSpacesTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n      const result = numberWithSpaces(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\nconst isJsonTests = [\n  { input: {}, output: true },\n  { input: { foo: 'bar' }, output: true },\n  { input: [], output: true },\n  { input: [123, '123'], output: true },\n  { input: '[]', output: true },\n  { input: '[123, \"123\"]', output: true },\n  { input: '{}', output: true },\n  { input: '{\"foo\": \"bar\"}', output: true },\n  { input: null, output: false },\n  { input: '', output: false },\n  { input: '123', output: false },\n  { input: 0, output: false },\n  { input: 123, output: false },\n  { input: Infinity, output: false },\n  { input: NaN, output: false },\n  { input: undefined, output: false },\n];\n\ndescribe('isJson', () => {\n  isJsonTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n      const result = isJson(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/base.helper.ts",
    "content": "import { camelCase, isNumber, mapKeys, sortBy } from 'lodash';\n\nexport const sortByNumberField = <T>(items: T[], field: string): T[] =>\n  sortBy(items, (o) => (o && isNumber(o[field]) ? o[field] : -Infinity));\n\nexport const numberWithSpaces = (number: number = 0) =>\n  number.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ' ');\n\nexport const isJson = (item: any): boolean => {\n  let value = typeof item !== 'string' ? JSON.stringify(item) : item;\n  try {\n    value = JSON.parse(value);\n  } catch (e) {\n    return false;\n  }\n\n  return typeof value === 'object' && value !== null;\n};\n\nexport const convertKeysToCamelCase = (data: any): any => {\n  if (Array.isArray(data)) {\n    return data.map(convertKeysToCamelCase);\n  }\n\n  if (data !== null && data.constructor === Object) {\n    return mapKeys(\n      Object.fromEntries(\n        Object.entries(data).map(([key, value]) => [\n          camelCase(key),\n          convertKeysToCamelCase(value),\n        ]),\n      ),\n      (_value, key) => camelCase(key),\n    );\n  }\n\n  return data;\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/big-string.spec.ts",
    "content": "import * as bigStringUtil from 'src/utils/big-string';\nimport config, { Config } from 'src/utils/config';\n\nconst REDIS_CLIENTS_CONFIG = config.get(\n  'redis_clients',\n) as Config['redis_clients'];\nconst BIG_STRING_PREFIX = REDIS_CLIENTS_CONFIG.truncatedStringPrefix;\n\ndescribe('bigStringUtil', () => {\n  describe('isTruncatingEnabled', () => {\n    it.each([\n      { input: { maxStringSize: NaN }, output: false },\n      { input: { maxStringSize: 0 }, output: false },\n      { input: { maxStringSize: -1 }, output: false },\n      { input: { maxStringSize: 1 }, output: true },\n    ])('%j', async ({ input, output }) => {\n      expect(bigStringUtil.isTruncatingEnabled(input as any)).toEqual(output);\n    });\n  });\n\n  describe('isTruncatedString', () => {\n    let isTruncatingEnabledSpy: jest.SpyInstance;\n\n    beforeEach(async () => {\n      isTruncatingEnabledSpy = jest.spyOn(bigStringUtil, 'isTruncatingEnabled');\n      isTruncatingEnabledSpy.mockReturnValue(true);\n    });\n\n    it.each([\n      { input: 'some string', output: false },\n      { input: Buffer.from('some string'), output: false },\n      { input: `${BIG_STRING_PREFIX} some string`, output: true },\n      { input: Buffer.from(`${BIG_STRING_PREFIX} some string`), output: true },\n      { input: null, output: false },\n      { input: '', output: false },\n      { input: Buffer.from(''), output: false },\n    ])('%j', async ({ input, output }) => {\n      expect(bigStringUtil.isTruncatedString(input)).toEqual(output);\n    });\n\n    it('should return false when truncating is disabled', async () => {\n      isTruncatingEnabledSpy.mockReturnValueOnce(false);\n      expect(\n        bigStringUtil.isTruncatedString(`${BIG_STRING_PREFIX} some string`),\n      ).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/big-string.ts",
    "content": "import { RedisString } from 'src/common/constants';\nimport { isString } from 'lodash';\nimport config, { Config } from 'src/utils/config';\n\nconst REDIS_CLIENTS_CONFIG = config.get(\n  'redis_clients',\n) as Config['redis_clients'];\nconst BIG_STRING_PREFIX = REDIS_CLIENTS_CONFIG.truncatedStringPrefix;\nconst BIG_STRING_PREFIX_BUFFER = Buffer.from(BIG_STRING_PREFIX);\n\n/**\n * Checks weather truncating functionality enabled based on global clients configuration\n * @param clientsConf\n */\nexport const isTruncatingEnabled = (clientsConf: Config['redis_clients']) =>\n  clientsConf?.maxStringSize ? clientsConf.maxStringSize > 0 : false;\n\nconst bufferStartsWith = (value: Buffer, subBuffer: Buffer) => {\n  if (subBuffer.length > value.length) {\n    return false;\n  }\n\n  return subBuffer.every((v, i) => v === value[i]);\n};\n\nexport const isTruncatedString = (value: RedisString): boolean => {\n  if (!isTruncatingEnabled(REDIS_CLIENTS_CONFIG)) {\n    return false;\n  }\n\n  try {\n    if (value instanceof Buffer) {\n      return bufferStartsWith(value, BIG_STRING_PREFIX_BUFFER);\n    }\n\n    if (isString(value) && value.startsWith(BIG_STRING_PREFIX)) {\n      return true;\n    }\n  } catch (e) {\n    // ignore error\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/catch-redis-errors.spec.ts",
    "content": "import {\n  RedisConnectionAuthUnsupportedException,\n  RedisConnectionClusterNodesUnavailableException,\n  RedisConnectionFailedException,\n  RedisConnectionIncorrectCertificateException,\n  RedisConnectionSentinelMasterRequiredException,\n  RedisConnectionTimeoutException,\n  RedisConnectionUnauthorizedException,\n  RedisConnectionUnavailableException,\n} from 'src/modules/redis/exceptions/connection';\nimport {\n  catchRedisSearchError,\n  getRedisConnectionException,\n} from 'src/utils/catch-redis-errors';\nimport { ReplyError } from 'src/models';\nimport { CertificatesErrorCodes, RedisErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  BadRequestException,\n  ForbiddenException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\n\ndescribe('catch-redis-errors', () => {\n  describe('getRedisConnectionExceptions', () => {\n    const database = { host: '127.0.0.1', port: 6379 };\n\n    const urlPlaceholder = `${database.host}:${database.port}`;\n\n    it.each([\n      {\n        error: new BadRequestException(),\n        output: new BadRequestException(),\n      },\n      {\n        error: new Error('unknown error'),\n        output: new RedisConnectionFailedException('unknown error'),\n      },\n      {\n        error: new Error(RedisErrorCodes.SentinelParamsRequired),\n        output: new RedisConnectionSentinelMasterRequiredException(),\n      },\n      {\n        error: new Error(RedisErrorCodes.Timeout),\n        output: new RedisConnectionTimeoutException(),\n      },\n      {\n        error: new Error('connection timed out'),\n        output: new RedisConnectionTimeoutException(),\n      },\n      {\n        error: new Error(RedisErrorCodes.InvalidPassword),\n        output: new RedisConnectionUnauthorizedException(),\n      },\n      {\n        error: new Error(RedisErrorCodes.AuthRequired),\n        output: new RedisConnectionUnauthorizedException(),\n      },\n      {\n        error: new Error('ERR invalid password'),\n        output: new RedisConnectionUnauthorizedException(),\n      },\n      {\n        error: new Error(\"ERR unknown command 'auth'\"),\n        output: new RedisConnectionAuthUnsupportedException(),\n      },\n      {\n        error: new Error(RedisErrorCodes.ClusterAllFailedError),\n        output: new RedisConnectionClusterNodesUnavailableException(),\n      },\n      {\n        error: new Error(RedisErrorCodes.ConnectionRefused),\n        output: new RedisConnectionUnavailableException(\n          ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder),\n        ),\n      },\n      {\n        error: new Error(RedisErrorCodes.ConnectionNotFound),\n        output: new RedisConnectionUnavailableException(\n          ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder),\n        ),\n      },\n      {\n        error: new Error(RedisErrorCodes.DNSTimeoutError),\n        output: new RedisConnectionUnavailableException(\n          ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder),\n        ),\n      },\n      {\n        error: {\n          message: 'some message',\n          code: RedisErrorCodes.ConnectionReset,\n        },\n        output: new RedisConnectionUnavailableException(\n          ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder),\n        ),\n      },\n      {\n        error: {\n          message: 'some message',\n          code: CertificatesErrorCodes.IncorrectCertificates,\n        },\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: {\n          message: 'some message',\n          code: CertificatesErrorCodes.DepthZeroSelfSignedCert,\n        },\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: {\n          message: 'some message',\n          code: CertificatesErrorCodes.SelfSignedCertInChain,\n        },\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: {\n          message: 'some message',\n          code: CertificatesErrorCodes.OSSLError,\n        },\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: new Error('SSL error'),\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: new Error(CertificatesErrorCodes.OSSLError),\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: new Error(CertificatesErrorCodes.IncorrectCertificates),\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n      {\n        error: new Error('ERR unencrypted connection is prohibited'),\n        output: new RedisConnectionIncorrectCertificateException(\n          ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder),\n        ),\n      },\n    ])('should handle %j', ({ error, output }) => {\n      expect(\n        getRedisConnectionException(error as ReplyError, database),\n      ).toEqual(output);\n    });\n  });\n\n  describe('catchRedisSearchError', () => {\n    it('should throw BadRequestException for Invalid JSONPath error', () => {\n      const error = {\n        name: 'ReplyError',\n        message:\n          \"Invalid JSONPath 'embedding' in attribute 'embedding' in index 'test_chatbot'\",\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(BadRequestException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw BadRequestException for Bad arguments error', () => {\n      const error = {\n        name: 'ReplyError',\n        message: 'Bad arguments for VECTOR field',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(BadRequestException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw BadRequestException for duplicate field error', () => {\n      // Actual Redis error: \"Duplicate field in schema - title\"\n      const error = {\n        name: 'ReplyError',\n        message: 'Duplicate field in schema - title',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(BadRequestException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw BadRequestException for missing mandatory parameter error', () => {\n      // Actual Redis error: \"Missing mandatory parameter: cannot create FLAT index without specifying DIM argument\"\n      const error = {\n        name: 'ReplyError',\n        message:\n          'Missing mandatory parameter: cannot create FLAT index without specifying DIM argument',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(BadRequestException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw BadRequestException for wrong number of arguments error', () => {\n      // Actual Redis error: \"ERR wrong number of arguments for 'FT.CREATE' command\"\n      const error = {\n        name: 'ReplyError',\n        message: \"ERR wrong number of arguments for 'FT.CREATE' command\",\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(BadRequestException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw NotFoundException for unknown index error', () => {\n      const error = {\n        name: 'ReplyError',\n        message: 'Unknown index name',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(NotFoundException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw NotFoundException for no such index error', () => {\n      const error = {\n        name: 'ReplyError',\n        message: 'idx: no such index',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(NotFoundException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw ForbiddenException for NOPERM error', () => {\n      const error = {\n        name: 'ReplyError',\n        message: 'NOPERM this user has no permissions',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(ForbiddenException);\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw InternalServerErrorException for unknown server error', () => {\n      const error = {\n        name: 'ReplyError',\n        message: 'Some unexpected server error',\n      } as ReplyError;\n\n      expect(() => catchRedisSearchError(error)).toThrow(\n        InternalServerErrorException,\n      );\n      expect(() => catchRedisSearchError(error)).toThrow(error.message);\n    });\n\n    it('should throw BadRequestException for RedisearchLimit error', () => {\n      const error = {\n        name: 'ReplyError',\n        message: RedisErrorCodes.RedisearchLimit,\n      } as ReplyError;\n\n      expect(() =>\n        catchRedisSearchError(error, { searchLimit: 10000 }),\n      ).toThrow(BadRequestException);\n    });\n\n    it('should re-throw HttpException if already an HttpException', () => {\n      const error = new BadRequestException('Already a bad request');\n\n      expect(() =>\n        catchRedisSearchError(error as unknown as ReplyError),\n      ).toThrow(BadRequestException);\n      expect(() =>\n        catchRedisSearchError(error as unknown as ReplyError),\n      ).toThrow('Already a bad request');\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/catch-redis-errors.ts",
    "content": "import {\n  BadRequestException,\n  ForbiddenException,\n  HttpException,\n  InternalServerErrorException,\n  NotFoundException,\n} from '@nestjs/common';\nimport { ReplyError } from 'src/models';\nimport {\n  RedisErrorCodes,\n  RedisearchErrorCodes,\n  CertificatesErrorCodes,\n} from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { RedisClientCommandReply } from 'src/modules/redis/client';\nimport {\n  RedisConnectionAuthUnsupportedException,\n  RedisConnectionClusterNodesUnavailableException,\n  RedisConnectionFailedException,\n  RedisConnectionTimeoutException,\n  RedisConnectionUnauthorizedException,\n  RedisConnectionUnavailableException,\n  RedisConnectionSentinelMasterRequiredException,\n  RedisConnectionIncorrectCertificateException,\n} from 'src/modules/redis/exceptions/connection';\n\nexport const isCertError = (error: ReplyError): boolean => {\n  try {\n    const errorCodesArray: string[] = Object.values(CertificatesErrorCodes);\n    return (\n      errorCodesArray.includes(error.code) ||\n      error.code?.includes(CertificatesErrorCodes.OSSLError) ||\n      error.message.includes('SSL') ||\n      error.message.includes(CertificatesErrorCodes.OSSLError) ||\n      error.message.includes(CertificatesErrorCodes.IncorrectCertificates) ||\n      error.message.includes('ERR unencrypted connection is prohibited')\n    );\n  } catch (e) {\n    return false;\n  }\n};\n\nexport const getRedisConnectionException = (\n  error: ReplyError,\n  connectionOptions: { host: string; port: number },\n  errorPlaceholder: string = '',\n): HttpException => {\n  const { host, port } = connectionOptions;\n\n  if (error instanceof HttpException) {\n    return error;\n  }\n\n  if (error?.message) {\n    if (error.message.includes(RedisErrorCodes.SentinelParamsRequired)) {\n      return new RedisConnectionSentinelMasterRequiredException(undefined, {\n        cause: error,\n      });\n    }\n\n    if (\n      error.message.includes(RedisErrorCodes.Timeout) ||\n      error.message.includes('timed out')\n    ) {\n      return new RedisConnectionTimeoutException(undefined, { cause: error });\n    }\n\n    if (\n      error.message.includes(RedisErrorCodes.InvalidPassword) ||\n      error.message.includes(RedisErrorCodes.AuthRequired) ||\n      error.message === 'ERR invalid password'\n    ) {\n      return new RedisConnectionUnauthorizedException(undefined, {\n        cause: error,\n      });\n    }\n\n    if (error.message === \"ERR unknown command 'auth'\") {\n      return new RedisConnectionAuthUnsupportedException(undefined, {\n        cause: error,\n      });\n    }\n\n    if (error.message.includes(RedisErrorCodes.ClusterAllFailedError)) {\n      return new RedisConnectionClusterNodesUnavailableException(undefined, {\n        cause: error,\n      });\n    }\n\n    if (\n      error.message.includes(RedisErrorCodes.ConnectionRefused) ||\n      error.message.includes(RedisErrorCodes.ConnectionNotFound) ||\n      error.message.includes(RedisErrorCodes.DNSTimeoutError) ||\n      error?.code === RedisErrorCodes.ConnectionReset\n    ) {\n      return new RedisConnectionUnavailableException(\n        ERROR_MESSAGES.INCORRECT_DATABASE_URL(\n          errorPlaceholder || `${host}:${port}`,\n        ),\n        { cause: error },\n      );\n    }\n\n    if (isCertError(error)) {\n      return new RedisConnectionIncorrectCertificateException(\n        ERROR_MESSAGES.INCORRECT_CERTIFICATES(\n          errorPlaceholder || `${host}:${port}`,\n        ),\n        { cause: error },\n      );\n    }\n  }\n\n  return new RedisConnectionFailedException(error?.message, { cause: error });\n};\n\nexport const catchRedisConnectionError = (\n  error: ReplyError,\n  connectionOptions: { host: string; port: number },\n  errorPlaceholder: string = '',\n): HttpException => {\n  throw getRedisConnectionException(error, connectionOptions, errorPlaceholder);\n};\n\nexport const catchAclError = (error: ReplyError): HttpException => {\n  // todo: Move to other place after refactoring\n  if (error instanceof HttpException) {\n    throw error;\n  }\n\n  if (error?.message?.includes(RedisErrorCodes.NoPermission)) {\n    throw new ForbiddenException(error.message);\n  }\n  if (error?.previousErrors?.length) {\n    const noPermError: ReplyError = error.previousErrors.find((errorItem) =>\n      errorItem?.message?.includes(RedisErrorCodes.NoPermission),\n    );\n\n    if (noPermError) {\n      throw new ForbiddenException(noPermError.message);\n    }\n  }\n  throw new InternalServerErrorException(error.message);\n};\n\nexport const catchTransactionError = (\n  transactionError: ReplyError | null,\n  transactionResults: [ReplyError, any][],\n): void => {\n  if (transactionError) {\n    throw transactionError;\n  }\n  const previousErrors = transactionResults\n    .map((item: [ReplyError, any]) => item[0])\n    .filter((item) => !!item);\n  if (previousErrors.length) {\n    throw previousErrors[0];\n  }\n};\n\nexport const catchMultiTransactionError = (\n  transactionResults: [Error, RedisClientCommandReply][],\n): void => {\n  transactionResults.forEach(([err]) => {\n    if (err) throw err;\n  });\n};\n\nconst REDISEARCH_CLIENT_ERROR_PATTERNS = [\n  RedisearchErrorCodes.Invalid,\n  RedisearchErrorCodes.BadArguments,\n  RedisearchErrorCodes.Duplicate,\n  RedisearchErrorCodes.Missing,\n  RedisearchErrorCodes.WrongNumberOfArguments,\n];\n\nexport const catchRedisSearchError = (\n  error: ReplyError,\n  options?: { searchLimit?: number },\n): HttpException => {\n  if (error instanceof HttpException) {\n    throw error;\n  }\n\n  if (error.message?.includes(RedisErrorCodes.RedisearchLimit)) {\n    throw new BadRequestException(\n      ERROR_MESSAGES.INCREASE_MINIMUM_LIMIT(options?.searchLimit),\n    );\n  }\n\n  if (\n    error.message?.toLowerCase()?.includes('unknown index') ||\n    error.message?.toLowerCase()?.includes('no such index')\n  ) {\n    throw new NotFoundException(error.message);\n  }\n\n  // Check for client-side errors (invalid input, bad arguments, etc.)\n  // These should return 400 Bad Request, not 500 Internal Server Error\n  if (\n    REDISEARCH_CLIENT_ERROR_PATTERNS.some((pattern) =>\n      error.message?.startsWith(pattern),\n    )\n  ) {\n    throw new BadRequestException(error.message);\n  }\n\n  throw catchAclError(error);\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/class-transformer.ts",
    "content": "import {\n  ClassTransformOptions,\n  instanceToPlain,\n  plainToInstance,\n} from 'class-transformer';\nimport { ClassConstructor } from 'class-transformer/types/interfaces';\n\nexport function classToClass<T, V>(\n  targetClass: ClassConstructor<T>,\n  classInstance: V,\n  options?: ClassTransformOptions,\n): T {\n  const defaultOptions: ClassTransformOptions = {\n    excludeExtraneousValues: true,\n    groups: ['security'],\n  };\n\n  const transformOptions = {\n    ...defaultOptions,\n    ...options,\n  };\n\n  return plainToInstance(\n    targetClass,\n    instanceToPlain(classInstance, transformOptions),\n    transformOptions,\n  );\n}\n\nexport const cloneClassInstance = <V>(entity: V): V =>\n  classToClass(entity.constructor as ClassConstructor<V>, entity);\n"
  },
  {
    "path": "redisinsight/api/src/utils/cli-helper.spec.ts",
    "content": "/* eslint-disable @typescript-eslint/quotes */\nimport { randomBytes } from 'crypto';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  CommandParsingError,\n  RedirectionParsingError,\n} from 'src/modules/cli/constants/errors';\nimport {\n  mockRedisAskError,\n  mockRedisMovedError,\n  mockRedisNoPermError,\n  mockRedisWrongTypeError,\n} from 'src/__mocks__';\nimport {\n  checkHumanReadableCommands,\n  splitCliCommandLine,\n  getBlockingCommands,\n  checkRedirectionError,\n  parseRedirectionError,\n  getRedisPipelineSummary,\n  getASCIISafeStringFromBuffer,\n  getBufferFromSafeASCIIString,\n  getUTF8FromRedisString,\n} from 'src/utils/cli-helper';\n\ndescribe('Cli helper', () => {\n  describe('splitCliCommandLine', () => {\n    [\n      {\n        input: 'memory usage key',\n        output: ['memory', 'usage', 'key'],\n      },\n      {\n        input: 'set test \"—\"',\n        output: ['set', 'test', '—'],\n      },\n      {\n        input: \"set test '—'\",\n        output: ['set', 'test', '—'],\n      },\n      {\n        input: 'info',\n        output: ['info'],\n      },\n      {\n        input: 'get \"key name\"',\n        output: ['get', 'key name'],\n      },\n      {\n        input: `get \"key ' name\"`,\n        output: ['get', `key ' name`],\n      },\n      {\n        input: `get \"key \\\\\" name\"`,\n        output: ['get', `key \" name`],\n      },\n      {\n        input: \"get 'key name'\",\n        output: ['get', 'key name'],\n      },\n      {\n        input: `s\"et\" ~\\\\'\\\\nk\"k \"ey' 1`,\n        output: ['set', `~\\\\\\\\nk\"k \"ey`, '1'],\n      },\n      {\n        input: 'set key \"\\\\a\\\\b\\\\t\\\\n\\\\r\"',\n        output: ['set', 'key', `\\u0007\\u0008\\u0009\\n\\r`],\n      },\n      {\n        input: 'set key \"\\\\xac\\\\xed\"',\n        output: ['set', 'key', Buffer.from([172, 237])],\n      },\n      {\n        input: `ACL SETUSER t on nopass ~'\\\\x00' &* +@all`,\n        output: [\n          'ACL',\n          'SETUSER',\n          't',\n          'on',\n          'nopass',\n          '~\\\\x00',\n          '&*',\n          '+@all',\n        ],\n      },\n    ].forEach((tc) => {\n      it(`should return ${JSON.stringify(tc.output)} for command ${tc.input}`, async () => {\n        expect(splitCliCommandLine(tc.input)).toEqual(tc.output);\n      });\n    });\n    it('should throw [CLI_INVALID_QUOTES_CLOSING] error for command with double quotes', () => {\n      const input = 'get \"key\"a';\n\n      try {\n        splitCliCommandLine(input);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(CommandParsingError);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.CLI_INVALID_QUOTES_CLOSING(),\n        );\n      }\n    });\n    it('should throw [CLI_UNTERMINATED_QUOTES] error for command with double quotes', () => {\n      const input = 'get \"\\\\\\\\key';\n\n      try {\n        splitCliCommandLine(input);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(CommandParsingError);\n        expect(err.message).toEqual(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES());\n      }\n    });\n    it('should throw [CLI_INVALID_QUOTES_CLOSING] error for command with single quotes', () => {\n      const input = \"get 'key'a\";\n\n      try {\n        splitCliCommandLine(input);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(CommandParsingError);\n        expect(err.message).toEqual(\n          ERROR_MESSAGES.CLI_INVALID_QUOTES_CLOSING(),\n        );\n      }\n    });\n    it('should throw [CLI_UNTERMINATED_QUOTES] error for command with single quotes', () => {\n      const input = \"get 'key\";\n\n      try {\n        splitCliCommandLine(input);\n        fail();\n      } catch (err) {\n        expect(err).toBeInstanceOf(CommandParsingError);\n        expect(err.message).toEqual(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES());\n      }\n    });\n  });\n\n  describe('checkHumanReadableCommands', () => {\n    const tests = [\n      { input: 'info', output: true },\n      { input: 'info server', output: true },\n      { input: 'lolwut', output: true },\n      { input: 'LOLWUT', output: true },\n      { input: 'debug hstats', output: true },\n      { input: 'debug hstats-key', output: true },\n      { input: 'DEBUG HSTATS-KEY', output: true },\n      { input: 'memory doctor', output: true },\n      { input: 'memory malloc-stats', output: true },\n      { input: 'cluster nodes', output: true },\n      { input: 'cluster info', output: true },\n      { input: 'client list', output: true },\n      { input: 'latency graph', output: true },\n      { input: 'latency doctor', output: true },\n      { input: 'proxy info', output: true },\n      { input: 'PROXY INFO', output: true },\n      { input: 'get key', output: false },\n      { input: 'debug object', output: false },\n      { input: 'DEBUG OBJECT', output: false },\n      { input: 'client kill', output: false },\n      { input: 'scan 0 COUNT 15 MATCH *', output: false },\n    ];\n    tests.forEach((test) => {\n      it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n        const result = checkHumanReadableCommands(test.input);\n\n        expect(result).toEqual(test.output);\n      });\n    });\n  });\n\n  describe('getBlockingCommands', () => {\n    it('should return fixed predefined list of blocking commands', () => {\n      expect(getBlockingCommands()).toEqual([\n        'blpop',\n        'brpop',\n        'blmove',\n        'brpoplpush',\n        'bzpopmin',\n        'bzpopmax',\n        'xread',\n        'xreadgroup',\n      ]);\n    });\n  });\n\n  describe('checkRedirectionError', () => {\n    const tests: Record<string, any>[] = [\n      { input: mockRedisAskError, output: true },\n      { input: mockRedisMovedError, output: true },\n      { input: mockRedisNoPermError, output: false },\n      { input: mockRedisWrongTypeError, output: false },\n      { input: 'info', output: false },\n      { input: undefined, output: false },\n      { input: false, output: false },\n      { input: null, output: false },\n      { input: {}, output: false },\n    ];\n    tests.forEach((test) => {\n      it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n        expect(checkRedirectionError(test.input)).toEqual(test.output);\n      });\n    });\n  });\n\n  describe('parseRedirectionError', () => {\n    it('should get slot and address from MOVED error', () => {\n      const result = parseRedirectionError(mockRedisMovedError);\n\n      expect(result).toEqual({\n        slot: '7008',\n        address: '127.0.0.1:7002',\n      });\n    });\n    it('should get slot and address from ASK error', () => {\n      const result = parseRedirectionError({\n        ...mockRedisAskError,\n        message: 'ASK 7008 redis.cloud.redislabs.com:17182',\n      });\n\n      expect(result).toEqual({\n        slot: '7008',\n        address: 'redis.cloud.redislabs.com:17182',\n      });\n    });\n    it('should throw exception for wrong node address', () => {\n      const redirectionError = {\n        ...mockRedisAskError,\n        message: 'ASK 7008 redis.cloud.redislabs.com/test',\n      };\n      expect(() => parseRedirectionError(redirectionError)).toThrow(\n        RedirectionParsingError,\n      );\n    });\n    it('should throw exception for incorrect redirection message format', () => {\n      const redirectionError = {\n        ...mockRedisAskError,\n        message: 'ASK redis.cloud.redislabs.com:17182 7008',\n      };\n      expect(() => parseRedirectionError(redirectionError)).toThrow(\n        RedirectionParsingError,\n      );\n    });\n    it('should throw exception', () => {\n      const input: any = 'ASK redis.cloud.redislabs.com:17182 7008';\n      expect(() => parseRedirectionError(input)).toThrow(\n        RedirectionParsingError,\n      );\n    });\n  });\n\n  describe('getRedisPipelineSummary', () => {\n    const pipeline = Array(50).fill(['get', 'foo']);\n    const tests: Record<string, any>[] = [\n      {\n        input: { pipeline, limit: undefined },\n        output: {\n          length: pipeline.length,\n          summary: JSON.stringify([...Array(5).fill('get'), '...']),\n        },\n      },\n      {\n        input: { pipeline, limit: 10 },\n        output: {\n          length: pipeline.length,\n          summary: JSON.stringify([...Array(10).fill('get'), '...']),\n        },\n      },\n      {\n        input: { pipeline, limit: 1000 },\n        output: {\n          length: pipeline.length,\n          summary: JSON.stringify([...Array(50).fill('get')]),\n        },\n      },\n      {\n        input: { pipeline: {}, limit: 1000 },\n        output: {\n          length: 0,\n          summary: '[]',\n        },\n      },\n      {\n        input: { pipeline, limit: -10 },\n        output: {\n          length: pipeline.length,\n          summary: JSON.stringify(['...']),\n        },\n      },\n    ];\n    tests.forEach((test) => {\n      it(`should be output: ${JSON.stringify(test.output)} for input: ${JSON.stringify(test.input)} `, async () => {\n        expect(\n          getRedisPipelineSummary(test.input.pipeline, test.input.limit),\n        ).toEqual(test.output);\n      });\n    });\n  });\n\n  describe('getASCIISafeStringFromBuffer', () => {\n    const tests: Record<string, any>[] = [\n      {\n        buffer: Buffer.from([0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65]),\n        string: 'simple',\n        unicode: 'simple',\n      },\n      {\n        buffer: Buffer.from([\n          0x45, 0x75, 0x72, 0x6f, 0x20, 0x2d, 0x20, 0xe2, 0x82, 0xac,\n        ]),\n        string: 'Euro - \\\\xe2\\\\x82\\\\xac',\n        unicode: 'Euro - €',\n      },\n      {\n        buffer: Buffer.from([\n          0xe2,\n          0x82,\n          0xac, // €\n          0x20,\n          0x21,\n          0x3d,\n          0x20, // _!=_\n          0x5c,\n          0x65,\n          0x32, // \\e2\n          0x5c,\n          0x78,\n          0x7a,\n          0x73, // \\xzs\n          0x5c,\n          0x30,\n          0x32, // \\02\n        ]),\n        string: '\\\\xe2\\\\x82\\\\xac != \\\\\\\\e2\\\\\\\\xzs\\\\\\\\02',\n        unicode: '€ != \\\\e2\\\\xzs\\\\02',\n      },\n      {\n        buffer: Buffer.from([\n          0x02,\n          0x00,\n          0x00,\n          0x00, // special symbols\n          0x7a,\n          0x69,\n          0x70,\n          0x63,\n          0x6f,\n          0x64,\n          0x65, // zipcode\n        ]),\n        string: '\\\\x02\\\\x00\\\\x00\\\\x00zipcode',\n        unicode: '\\x02\\x00\\x00\\x00zipcode',\n      },\n    ];\n    tests.forEach((test) => {\n      it(`should convert ${test.unicode} to buffer and to ASCII string representation`, async () => {\n        const str = getASCIISafeStringFromBuffer(test.buffer);\n        const buf = getBufferFromSafeASCIIString(test.string);\n\n        expect(test.string).toEqual(str);\n        expect(test.buffer).toEqual(buf);\n        expect(test.unicode).toEqual(buf.toString());\n      });\n    });\n\n    it('test json data structure', () => {\n      const json = { test: 'test' };\n      const jsonString = JSON.stringify(json);\n      const jsonBuffer = Buffer.from(jsonString);\n\n      const str = getASCIISafeStringFromBuffer(jsonBuffer);\n      const buf = getBufferFromSafeASCIIString(str);\n\n      expect(jsonBuffer).toEqual(buf);\n      // getASCIISafeStringFromBuffer is analogue of JSON.stringify without leading \" for serialized json data\n      expect(JSON.stringify(jsonString)).toEqual(`\"${str}\"`);\n    });\n\n    it('test huge string timings', () => {\n      const buf = randomBytes(1024 * 1024);\n\n      let startTime = Date.now();\n      const str = getASCIISafeStringFromBuffer(buf);\n      console.log('To ASCII string took: ', Date.now() - startTime);\n      expect(Date.now() - startTime).toBeLessThan(5000); // usually takes ~1s\n\n      startTime = Date.now();\n      getBufferFromSafeASCIIString(str);\n      console.log('Back to Buffer took: ', Date.now() - startTime);\n      // todo: investigate how to optimize this\n      expect(Date.now() - startTime).toBeLessThan(15000); // usually takes ~0.7s\n    });\n  });\n\n  describe('getUTF8FromRedisString', () => {\n    const tests = [\n      { input: Buffer.from('abc'), output: 'abc' },\n      { input: Buffer.from('123'), output: '123' },\n      { input: Buffer.from('ntoheuthao u2312'), output: 'ntoheuthao u2312' },\n      {\n        input: Buffer.from('q;tkoeh uoaecr342 \"\"ueo!@#'),\n        output: 'q;tkoeh uoaecr342 \"\"ueo!@#',\n      },\n      {\n        input: Buffer.from('\\\\x02\\\\x00\\\\x00\\\\x00zipcode'),\n        output: '\\\\x02\\\\x00\\\\x00\\\\x00zipcode',\n      },\n      {\n        input: Buffer.from('€ != \\\\e2\\\\xzs\\\\02'),\n        output: '€ != \\\\e2\\\\xzs\\\\02',\n      },\n    ];\n    tests.forEach(({ input, output }) => {\n      it(`should be output: ${output} for input: ${input} `, async () => {\n        const result = getUTF8FromRedisString(input);\n\n        expect(result).toEqual(output);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/cli-helper.ts",
    "content": "import { take, isEmpty } from 'lodash';\nimport config from 'src/utils/config';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport {\n  CommandParsingError,\n  RedirectionParsingError,\n} from 'src/modules/cli/constants/errors';\nimport { ReplyError } from 'src/models';\nimport { IRedirectionInfo } from 'src/modules/cli/services/cli-business/output-formatter/output-formatter.interface';\nimport { IS_NON_PRINTABLE_ASCII_CHARACTER } from 'src/constants';\n\nconst LOGGER_CONFIG = config.get('logger');\nconst BLANK_LINE_REGEX = /^\\s*\\n/gm;\n\nexport enum CliToolBlockingCommands {\n  BLPop = 'blpop',\n  BRPop = 'brpop',\n  BLMove = 'blmove',\n  BRPopLPush = 'brpoplpush',\n  BZPopMin = 'bzpopmin',\n  BZPopMax = 'bzpopmax',\n  XRead = 'xread',\n  XReadGroup = 'xreadgroup',\n}\n\nexport enum CliToolHumanReadableCommands {\n  Info = 'info',\n  Lolwut = 'lolwut',\n  DebugHStats = 'debug hstats',\n  DebugHStatsKey = 'debug hstats-key',\n  MemoryDoctor = 'memory doctor',\n  MemoryMallocStats = 'memory malloc-stats',\n  ClusterNodes = 'cluster nodes',\n  ClusterInfo = 'cluster info',\n  ClientList = 'client list',\n  LatencyGraph = 'latency graph',\n  LatencyDoctor = 'latency doctor',\n  ProxyInfo = 'proxy info',\n}\n\nfunction isHex(str: string) {\n  return /^[A-F0-9]{1,2}$/i.test(str);\n}\n\nfunction getSpecChar(str: string): string {\n  let char;\n  switch (str) {\n    case 'a':\n      char = String.fromCharCode(7);\n      break;\n    case 'b':\n      char = String.fromCharCode(8);\n      break;\n    case 't':\n      char = String.fromCharCode(9);\n      break;\n    case 'n':\n      char = String.fromCharCode(10);\n      break;\n    case 'r':\n      char = String.fromCharCode(13);\n      break;\n    default:\n      char = str;\n  }\n  return char;\n}\n\nexport const convertToStringIfPossible = (data: any) => {\n  if (data instanceof Buffer) {\n    const str = data.toString();\n    if (Buffer.compare(data, Buffer.from(str)) === 0) {\n      return str;\n    }\n  }\n\n  return data;\n};\n\n// todo: review/rewrite this function. Pay attention on handling data inside '' vs \"\"\n// todo: rethink implementation. set key {value} where {value} is string ~500KB take ~15s\nexport const splitCliCommandLine = (line: string): string[] => {\n  // Splits a command line into a list of arguments.\n  // Ported from sdssplitargs() function in sds.c from Redis source code.\n  // This is the function redis-cli uses to parse command lines.\n  let i = 0;\n  let currentArg: any = '';\n  const args = [];\n  while (i < line.length) {\n    /* skip blanks */\n    while (line[i] === ' ') i += 1;\n    let inq = false; /* set to True if we are in \"quotes\" */\n    let insq = false; /* set to True if we are in 'single quotes' */\n    let done = false;\n    while (!done) {\n      if (inq) {\n        // Handle double quotes\n        if (i >= line.length) {\n          // unterminated quotes\n          throw new CommandParsingError(\n            ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n          );\n        } else if (\n          line[i] === '\\\\' &&\n          line[i + 1] === 'x' &&\n          isHex(`${line[i + 2]}${line[i + 3]}`)\n        ) {\n          const charCode = parseInt(`0x${line[i + 2]}${line[i + 3]}`, 16);\n          currentArg = Buffer.concat([\n            currentArg,\n            Buffer.alloc(1, charCode, 'binary'),\n          ]);\n          i += 3;\n        } else if (line[i] === '\\\\' && i < line.length) {\n          // Handle special characters\n          i += 1;\n          const c = getSpecChar(line[i]);\n          currentArg = Buffer.concat([\n            currentArg,\n            Buffer.alloc(1, c, 'binary'),\n          ]);\n        } else if (line[i] === '\"') {\n          // closing quote must be followed by a space or nothing at all.\n          if (i + 1 < line.length && line[i + 1] !== ' ') {\n            throw new CommandParsingError(\n              ERROR_MESSAGES.CLI_INVALID_QUOTES_CLOSING(),\n            );\n          }\n          done = true;\n        } else {\n          currentArg = Buffer.concat([\n            currentArg,\n            Buffer.from(line[i], 'utf8'),\n          ]);\n        }\n      } else if (insq) {\n        // Handle single quotes\n        if (i >= line.length) {\n          // unterminated quotes\n          throw new CommandParsingError(\n            ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(),\n          );\n        } else if (line[i] === '\\\\' && line[i + 1] === \"'\") {\n          i += 1;\n          currentArg += \"'\";\n        } else if (line[i] === \"'\") {\n          // closing quote must be followed by a space or nothing at all.\n          if (i + 1 < line.length && line[i + 1] !== ' ') {\n            throw new CommandParsingError(\n              ERROR_MESSAGES.CLI_INVALID_QUOTES_CLOSING(),\n            );\n          }\n          done = true;\n        } else {\n          currentArg = `${currentArg}${line[i]}`;\n        }\n      } else if (i >= line.length) {\n        done = true;\n      } else if ([' ', '\\n', '\\r', '\\t', '\\0'].includes(line[i])) {\n        done = true;\n      } else if (line[i] === '\"') {\n        currentArg = Buffer.from(currentArg);\n        inq = true;\n      } else if (line[i] === \"'\") {\n        insq = true;\n      } else {\n        currentArg = `${currentArg || ''}${line[i]}`;\n      }\n      if (i < line.length) i += 1;\n    }\n    args.push(convertToStringIfPossible(currentArg));\n    currentArg = '';\n  }\n\n  return args;\n};\n\nexport const getBlockingCommands = (): string[] =>\n  Object.values(CliToolBlockingCommands);\n\nexport function decimalToHexString(d: number, padding: number = 2): string {\n  const hex = Number(d).toString(16);\n  return '0'.repeat(padding).substr(0, padding - hex.length) + hex;\n}\n\nexport function checkHumanReadableCommands(commandLine: string): boolean {\n  // The list of command got from cliSendCommand() function in redis-cli.c from Redis source code.\n  return !!Object.values(CliToolHumanReadableCommands).find((command) =>\n    commandLine.toLowerCase().startsWith(command),\n  );\n}\n\nexport function checkRedirectionError(error: ReplyError): boolean {\n  try {\n    return error.message.startsWith('MOVED') || error.message.startsWith('ASK');\n  } catch (e) {\n    return false;\n  }\n}\n\nexport function parseRedirectionError(error: ReplyError): IRedirectionInfo {\n  try {\n    const [, slot, address] = error.message.split(' ');\n    const { port } = new URL(`redis://${address}`);\n    if (!port) {\n      throw new Error();\n    }\n    return { slot, address };\n  } catch (e) {\n    throw new RedirectionParsingError();\n  }\n}\n\ninterface IPipelineSummary {\n  summary: string;\n  length: number;\n}\n\nexport function getRedisPipelineSummary(\n  pipeline: Array<[toolCommand: any, ...args: Array<string | number | Buffer>]>,\n  limit: number = LOGGER_CONFIG.pipelineSummaryLimit,\n): IPipelineSummary {\n  const result: IPipelineSummary = {\n    summary: '[]',\n    length: 0,\n  };\n  try {\n    const commands = pipeline.reduce((prev, cur) => [...prev, cur[0]], []);\n    result.length = commands.length;\n    result.summary =\n      commands.length > limit\n        ? JSON.stringify([...take(commands, limit), '...'])\n        : JSON.stringify(commands);\n  } catch (e) {\n    // continue regardless of error\n  }\n  return result;\n}\n\nexport const multilineCommandToOneLine = (text: string = '') =>\n  text\n    .split(/(\\r\\n|\\n|\\r)+\\s+/gm)\n    .filter((line: string) => !(BLANK_LINE_REGEX.test(line) || isEmpty(line)))\n    .join(' ');\n\n/**\n * Produces an escaped string representation of a byte string.\n * Ported from sdscatrepr() function in sds.c from Redis source code.\n * This is the function redis-cli uses to escape strings for output.\n * @param reply\n */\nexport const getASCIISafeStringFromBuffer = (reply: Buffer): string => {\n  let result = '';\n  reply.forEach((byte: number) => {\n    const char = Buffer.from([byte]).toString();\n    if (IS_NON_PRINTABLE_ASCII_CHARACTER.test(char)) {\n      result += `\\\\x${decimalToHexString(byte)}`;\n    } else {\n      switch (char) {\n        case '\\u0007': // Bell character\n          result += '\\\\a';\n          break;\n        case '\"':\n          result += '\\\\\"';\n          break;\n        case '\\\\':\n          result += '\\\\\\\\';\n          break;\n        case '\\b':\n          result += '\\\\b';\n          break;\n        case '\\t':\n          result += '\\\\t';\n          break;\n        case '\\n':\n          result += '\\\\n';\n          break;\n        case '\\r':\n          result += '\\\\r';\n          break;\n        default:\n          result += char;\n      }\n    }\n  });\n  return result;\n};\n\nexport const getUTF8FromBuffer = (reply: Buffer): string =>\n  reply.toString('utf8');\n\nexport const getUTF8FromRedisString = (value: any) => {\n  if (value instanceof Buffer) {\n    return value.toString('utf8');\n  }\n\n  return value;\n};\n\n/**\n * Generates a Buffer from escaped string representation\n * An opposite for getASCIISafeStringFromBuffer\n * ANY CHANGES SHOULD BE TOO IN THE SAME FUNCTION IN THE CLIENTS-LIST PLUGIN\n * @param str\n */\nexport const getBufferFromSafeASCIIString = (str: string): Buffer => {\n  const bytes = [];\n\n  for (let i = 0; i < str.length; i += 1) {\n    if (str[i] === '\\\\') {\n      if (str[i + 1] === 'x') {\n        const hexString = str.substr(i + 2, 2);\n        if (isHex(hexString)) {\n          bytes.push(Buffer.from(hexString, 'hex'));\n          i += 3;\n          // eslint-disable-next-line no-continue\n          continue;\n        }\n      }\n\n      if (['a', '\"', '\\\\', 'b', 't', 'n', 'r'].includes(str[i + 1])) {\n        switch (str[i + 1]) {\n          case 'a':\n            bytes.push(Buffer.from('\\u0007'));\n            break;\n          case 'b':\n            bytes.push(Buffer.from('\\b'));\n            break;\n          case 't':\n            bytes.push(Buffer.from('\\t'));\n            break;\n          case 'n':\n            bytes.push(Buffer.from('\\n'));\n            break;\n          case 'r':\n            bytes.push(Buffer.from('\\r'));\n            break;\n          default:\n            bytes.push(Buffer.from(str[i + 1]));\n        }\n\n        i += 1;\n        // eslint-disable-next-line no-continue\n        continue;\n      }\n    }\n\n    bytes.push(Buffer.from(str[i]));\n  }\n\n  return Buffer.concat(bytes);\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/config.spec.ts",
    "content": "import { Config } from 'src/utils/config';\nimport { merge } from 'lodash';\nimport defaultConfig from '../../config/default';\nimport devConfig from '../../config/development';\nimport testConfig from '../../config/test';\nimport stageConfig from '../../config/staging';\nimport prodConfig from '../../config/production';\nimport stackConfig from '../../config/stack';\n\ndescribe('Config util', () => {\n  const OLD_ENV = process.env;\n\n  describe('get', () => {\n    beforeEach(() => {\n      // Clears the cache\n      jest.resetModules();\n      // Make a copy\n      process.env = { ...OLD_ENV };\n    });\n\n    afterAll(() => {\n      // Restore old environment\n      process.env = OLD_ENV;\n    });\n\n    it('should return dev server config', () => {\n      process.env.NODE_ENV = 'development';\n      // eslint-disable-next-line global-require\n      const { get } = require('./config');\n\n      const result = get('server') as Config['server'];\n\n      expect(result).toEqual({\n        ...defaultConfig.server,\n        ...devConfig.server,\n      });\n    });\n\n    it('should return test server config', () => {\n      process.env.NODE_ENV = 'test';\n      // eslint-disable-next-line global-require\n      const { get } = require('./config');\n\n      const result = get('server') as Config['server'];\n\n      expect(result).toEqual({\n        ...defaultConfig.server,\n        ...testConfig.server,\n      });\n    });\n\n    it('should return stack server config', () => {\n      process.env.RI_BUILD_TYPE = 'REDIS_STACK';\n      process.env.NODE_ENV = 'staging';\n      // eslint-disable-next-line global-require\n      const { get } = require('./config');\n\n      const result = get('server') as Config['server'];\n\n      expect(result).toEqual({\n        ...defaultConfig.server,\n        ...stageConfig.server,\n        ...stackConfig.server,\n        buildType: 'REDIS_STACK',\n      });\n    });\n\n    it('should return stage server config', () => {\n      process.env.NODE_ENV = 'staging';\n      // eslint-disable-next-line global-require\n      const { get } = require('./config');\n\n      const result = get('server') as Config['server'];\n\n      expect(result).toEqual({\n        ...defaultConfig.server,\n        ...stageConfig.server,\n      });\n    });\n\n    it('should return prod server config', () => {\n      process.env.NODE_ENV = 'production';\n      // eslint-disable-next-line global-require\n      const { get } = require('./config');\n\n      const result = get('server') as Config['server'];\n\n      expect(result).toEqual({\n        ...defaultConfig.server,\n        ...prodConfig.server,\n      });\n    });\n\n    it('should return entire prod server config', () => {\n      process.env.NODE_ENV = 'production';\n      // eslint-disable-next-line global-require\n      const { get } = require('./config');\n\n      const result = get() as Config['server'];\n\n      expect(result).toEqual(merge({ ...defaultConfig }, { ...prodConfig }));\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/config.ts",
    "content": "import { merge, cloneDeep } from 'lodash';\nimport defaultConfig from '../../config/default';\nimport development from '../../config/development';\nimport staging from '../../config/staging';\nimport test from '../../config/test';\nimport production from '../../config/production';\nimport stack from '../../config/stack';\n\nconst config = cloneDeep(defaultConfig);\n\nlet envConfig;\nswitch (process.env.NODE_ENV) {\n  case 'staging':\n    envConfig = staging;\n    break;\n  case 'production':\n    envConfig = production;\n    break;\n  case 'test':\n    envConfig = test;\n    break;\n  default:\n    envConfig = development;\n    break;\n}\n\nlet buildTypeConfig;\n// eslint-disable-next-line sonarjs/no-small-switch\nswitch (process.env.RI_BUILD_TYPE) {\n  case 'REDIS_STACK':\n    buildTypeConfig = stack;\n    break;\n  default:\n    buildTypeConfig = {};\n    break;\n}\n\nmerge(config, envConfig, buildTypeConfig);\n\nexport type Config = typeof config;\nexport type KeyOfConfig = keyof typeof config;\nexport const get: (key?: KeyOfConfig) => Config | any = (key?: KeyOfConfig) =>\n  key ? config[key] : config;\n\nexport default {\n  get,\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/converter.spec.ts",
    "content": "import {\n  convertIntToSemanticVersion,\n  convertStringToNumber,\n  convertAnyStringToPositiveInteger,\n} from './converter';\n\nconst convertIntToSemanticVersionTests: Record<string, any>[] = [\n  { input: 1, output: '0.0.1' },\n  { input: 10, output: '0.0.10' },\n  { input: 100, output: '0.1.0' },\n  { input: 1000, output: '0.10.0' },\n  { input: 10000, output: '1.0.0' },\n  { input: 100000, output: '10.0.0' },\n  { input: 1000000, output: '100.0.0' },\n  { input: 10410, output: '1.4.10' },\n  { input: 10008, output: '1.0.8' },\n  { input: 20407, output: '2.4.7' },\n  { input: 20011, output: '2.0.11' },\n  { input: 20206, output: '2.2.6' },\n  { input: 0, output: undefined },\n  { input: 'string', output: undefined },\n];\n\ndescribe('convertIntToSemanticVersionTests', () => {\n  convertIntToSemanticVersionTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${JSON.stringify(test.input)}`, () => {\n      const result = convertIntToSemanticVersion(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\nconst convertStringToNumberTests: Record<string, any>[] = [\n  { input: ['1'], output: 1 },\n  { input: [1], output: 1 },\n  { input: [{ some: 'obj' }], output: undefined },\n  { input: [{ some: 'obj' }, 11], output: 11 },\n  { input: ['asd45', 11], output: 11 },\n  { input: ['123.123', 11], output: 123.123 },\n  { input: [undefined, 11], output: 11 },\n];\n\ndescribe('convertStringToNumber', () => {\n  convertStringToNumberTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${JSON.stringify(test.input)}`, () => {\n      const result = convertStringToNumber.call(this, ...test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\nconst convertAnyStringToPositiveIntegerTests = [\n  [undefined, -1],\n  [null, -1],\n  [123123, -1],\n  [[], -1],\n  [{}, -1],\n  [{ length: 12 }, -1],\n  ['', -1],\n  ['1', 49],\n  ['4f5daa5e-6139-4e95-8e7c-3283287f4218', 1347108680],\n  [\n    new Array(1000).fill('4f5daa5e-6139-4e95-8e7c-3283287f4218').join(),\n    229890988,\n  ],\n] as [string, number][];\ndescribe('convertAnyStringToPositiveInteger', () => {\n  it.each(convertAnyStringToPositiveIntegerTests)(\n    'for input: %s, should return: %s',\n    (input, result) => {\n      expect(convertAnyStringToPositiveInteger(input)).toEqual(result);\n    },\n  );\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/converter.ts",
    "content": "import { isInteger, isString, isNumber, isNaN } from 'lodash';\n\nexport const convertIntToSemanticVersion = (input: number): string => {\n  const separator = '.';\n  try {\n    if (isInteger(input) && input > 0) {\n      // Pad input with optional zero symbols\n      const version = String(input).padStart(6, '0');\n      const patch = parseInt(version.slice(-2), 10);\n      const minor = parseInt(version.slice(-4, -2), 10);\n      const major = parseInt(version.slice(0, -4), 10);\n      return [major, minor, patch].join(separator);\n    }\n    return undefined;\n  } catch (e) {\n    return undefined;\n  }\n};\n\nexport const convertStringToNumber = (\n  value: any,\n  defaultValue?: number,\n): number => {\n  if (isNumber(value)) {\n    return value;\n  }\n\n  if (!isString(value) || !value) {\n    return defaultValue;\n  }\n\n  const num = parseFloat(value);\n\n  if (isNaN(num)) {\n    return defaultValue;\n  }\n\n  return num;\n};\n\nexport const convertAnyStringToPositiveInteger = (str: string) => {\n  if (!isString(str) || !str.length) {\n    return -1;\n  }\n\n  let hash = 0;\n  for (let i = 0; i < str.length; i += 1) {\n    const char = str.charCodeAt(i);\n    // eslint-disable-next-line no-bitwise\n    hash = (hash << 5) - hash + char;\n    // eslint-disable-next-line no-bitwise\n    hash |= 0;\n  }\n  return Math.abs(hash);\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/createHttpOptions.ts",
    "content": "import { readFile } from 'fs/promises';\nimport { Config } from '.';\n\nexport const createHttpOptions = async (serverConfig: Config['server']) => {\n  const { tlsKey, tlsCert } = serverConfig;\n\n  try {\n    const [key, cert] = await Promise.all([\n      readFile(tlsKey, { encoding: 'utf-8' }),\n      readFile(tlsCert, { encoding: 'utf-8' }),\n    ]);\n    return {\n      key,\n      cert,\n    };\n  } catch (e) {\n    /* if this throws, it could mean\n        1. the tlsKey and tlsCert provided by the config were actually certificates in PEM format, not file paths or\n        2. there were issues reading the files (wrong path, permissions, etc.)\n\n       nothing to do in this case except assume PEM format and let the calling code throw if that doesn't work either */\n  }\n\n  // for docker and perhaps other environments, multi-line env vars are problematic, so if there are escaped new-lines\n  // in the key or cert, replace them with proper newlines\n  const key = tlsKey.replace(/\\\\n/g, '\\n');\n  const cert = tlsCert.replace(/\\\\n/g, '\\n');\n\n  return {\n    key,\n    cert,\n  };\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/feature-version-filter.helper.spec.ts",
    "content": "// Import the function to test\nimport { FeatureConfigFilterCondition } from 'src/modules/feature/model/features-config';\nimport { filterVersion } from './feature-version-filter.helper';\n\ndescribe('filterVersion', () => {\n  it('should return true for Eq condition when versions are equal', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Eq, '1.0.0', '1.0.0'),\n    ).toBe(true);\n  });\n\n  it('should return false for Eq condition when versions are not equal', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Eq, '1.0.1', '1.0.0'),\n    ).toBe(false);\n  });\n\n  it('should return false for Neq condition when versions are equal', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Neq, '1.0.0', '1.0.0'),\n    ).toBe(false);\n  });\n\n  it('should return true for Neq condition when versions are not equal', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Neq, '1.0.1', '1.0.0'),\n    ).toBe(true);\n  });\n\n  it('should return true for Gt condition when first version is greater', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Gt, '1.0.1', '1.0.0'),\n    ).toBe(true);\n  });\n\n  it('should return false for Gt condition when first version is not greater', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Gt, '1.0.0', '1.0.1'),\n    ).toBe(false);\n  });\n\n  it('should return true for Gte condition when first version is greater', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Gte, '1.0.1', '1.0.0'),\n    ).toBe(true);\n  });\n\n  it('should return true for Gte condition when versions are equal', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Gte, '1.0.0', '1.0.0'),\n    ).toBe(true);\n  });\n\n  it('should return false for Gte condition when first version is less', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Gte, '1.0.0', '1.0.1'),\n    ).toBe(false);\n  });\n\n  it('should return true for Lt condition when first version is less', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Lt, '1.0.0', '1.0.1'),\n    ).toBe(true);\n  });\n\n  it('should return false for Lt condition when first version is not less', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Lt, '1.0.1', '1.0.0'),\n    ).toBe(false);\n  });\n\n  it('should return true for Lte condition when first version is less', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Lte, '1.0.0', '1.0.1'),\n    ).toBe(true);\n  });\n\n  it('should return true for Lte condition when versions are equal', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Lte, '1.0.0', '1.0.0'),\n    ).toBe(true);\n  });\n\n  it('should return false for Lte condition when first version is greater', () => {\n    expect(\n      filterVersion(FeatureConfigFilterCondition.Lte, '1.0.1', '1.0.0'),\n    ).toBe(false);\n  });\n\n  it('should return false for unknown condition', () => {\n    expect(\n      filterVersion(\n        'UnknownCondition' as FeatureConfigFilterCondition,\n        '1.0.0',\n        '1.0.0',\n      ),\n    ).toBe(false);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/feature-version-filter.helper.ts",
    "content": "import { FeatureConfigFilterCondition } from 'src/modules/feature/model/features-config';\nimport * as semverCompare from 'node-version-compare';\n\nexport const filterVersion = (\n  cond: FeatureConfigFilterCondition,\n  value: string,\n  filterValue: string,\n) => {\n  const compareRes = semverCompare(value, filterValue);\n  switch (cond) {\n    case FeatureConfigFilterCondition.Eq:\n      return compareRes === 0;\n    case FeatureConfigFilterCondition.Neq:\n      return compareRes !== 0;\n    case FeatureConfigFilterCondition.Gt:\n      return compareRes > 0;\n    case FeatureConfigFilterCondition.Gte:\n      return compareRes >= 0;\n    case FeatureConfigFilterCondition.Lt:\n      return compareRes < 0;\n    case FeatureConfigFilterCondition.Lte:\n      return compareRes <= 0;\n    default:\n      return false;\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/file-helper.ts",
    "content": "import axios from 'axios';\nimport * as fs from 'fs-extra';\nimport * as AdmZip from 'adm-zip';\nimport { join } from 'path';\n\n/**\n * Download file\n */\nexport const getFile = async (url: string): Promise<ArrayBuffer> => {\n  const { data } = await axios.get(url, {\n    responseType: 'arraybuffer',\n  });\n\n  return data;\n};\n\n/**\n * Purge folder and extract archive\n */\nexport const updateFolderFromArchive = async (\n  path: string,\n  data: any,\n): Promise<void> => {\n  await fs.remove(path);\n\n  await fs.ensureDir(path);\n\n  const zip = new AdmZip(data);\n  zip.extractAllTo(path, true);\n};\n\n/**\n * Exract file in folder\n */\nexport const updateFile = async (\n  path: string,\n  fileName: string,\n  data: any,\n): Promise<void> => {\n  await fs.ensureDir(path);\n\n  const buildInfoPath = join(path, fileName);\n\n  await fs.writeFile(buildInfoPath, data);\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/glob-pattern-helper.spec.ts",
    "content": "import { unescapeGlob, isRedisGlob } from 'src/utils/glob-pattern-helper';\n\nconst unescapeGlobTests = [\n  { input: 'h?llo', output: 'h?llo' },\n  { input: 'h\\\\?llo', output: 'h?llo' },\n  { input: '\\\\!hello', output: '!hello' },\n  { input: '\\\\*hello', output: '*hello' },\n  { input: 'hello\\\\*', output: 'hello*' },\n  { input: 'h\\\\(a|e\\\\)llo', output: 'h(a|e)llo' },\n  { input: 'h\\\\[a-e\\\\]llo', output: 'h[a-e]llo' },\n  { input: 'h\\\\[^a\\\\]llo', output: 'h[^a]llo' },\n  { input: 'h\\\\[a-e\\\\]llo\\\\\\\\:foo', output: 'h[a-e]llo\\\\:foo' },\n  { input: 'h\\\\{a,e\\\\}llo', output: 'h{a,e}llo' },\n  { input: 'h\\\\{a,e}llo', output: 'h{a,e}llo' },\n  { input: 'h\\\\[a-e\\\\]llo\\\\\\\\\\\\*', output: 'h[a-e]llo\\\\*' },\n  { input: 'h\\\\?(a)llo', output: 'h?(a)llo' },\n  { input: 'hello/\\\\!\\\\(a\\\\)llo', output: 'hello/!(a)llo' },\n  { input: 'hello/\\\\+(a)llo', output: 'hello/+(a)llo' },\n  { input: 'hello/\\\\@(a)llo', output: 'hello/@(a)llo' },\n  { input: 'hello/\\\\*(a)llo', output: 'hello/*(a)llo' },\n  { input: 'hello/\\\\?(a)llo', output: 'hello/?(a)llo' },\n];\n\ndescribe('unescapeGlob', () => {\n  unescapeGlobTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n      const result = unescapeGlob(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n\ndescribe('isRedisGlob', () => {\n  const testCases: [string, boolean][] = [\n    ['?ello', true],\n    ['??llo', true],\n    ['\\\\?\\\\?llo', false],\n    ['\\\\??llo', true],\n    ['?\\\\?llo', true],\n    ['\\\\\\\\\\\\\\\\?\\\\?llo', true],\n    ['h?llo', true],\n    ['h\\\\?llo', false],\n    ['h\\\\\\\\?llo', true],\n    ['h\\\\\\\\\\\\?llo', false],\n    ['h????llo', true],\n    ['h\\\\????llo', true],\n    ['h????ll?o', true],\n    ['h*llo', true],\n    ['h**llo', true],\n    ['h***ll*o', true],\n    ['\\\\*ello', false],\n    ['\\\\\\\\*ello', true],\n    ['\\\\\\\\\\\\*ello', false],\n    ['h[ae]llo', true],\n    ['h[^e]llo', true],\n    ['h[a-b]llo', true],\n    ['h[a-b\\\\]llo', false],\n    ['h[a-b\\\\\\\\]llo', true],\n    ['h\\\\[a-b\\\\\\\\]llo', false],\n    ['h\\\\\\\\[a-b\\\\\\\\]llo', true],\n    ['h\\\\\\\\[a-b\\\\]llo', false],\n  ];\n\n  testCases.forEach((tc) => {\n    it(`should return ${tc[1]} for input: ${tc[0]}`, () => {\n      expect(isRedisGlob(tc[0])).toEqual(tc[1]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/glob-pattern-helper.ts",
    "content": "const GLOB_SPEC_CHAR = ['!', '*', '?', '[', ']', '(', ')', '{', '}'];\nconst EXT_GLOB_SPEC_CHAR = ['@', '+'];\n\nexport const unescapeGlob = (value: string): string => {\n  let result = value;\n\n  [...GLOB_SPEC_CHAR, ...EXT_GLOB_SPEC_CHAR].forEach((char: string) => {\n    const regex = new RegExp('\\\\'.repeat(3) + char, 'g');\n    result = result.replace(regex, char);\n  });\n\n  return result.replace(/\\\\{2}/g, '\\\\');\n};\n\nconst REDIS_GLOB_SPEC_CHAR = ['?', '*', '[', ']'];\nexport const unescapeRedisGlob = (value: string): string => {\n  let result = value;\n\n  REDIS_GLOB_SPEC_CHAR.forEach((char: string) => {\n    const regex = new RegExp('\\\\'.repeat(3) + char, 'g');\n    result = result.replace(regex, char);\n  });\n\n  return result.replace(/\\\\{2}/g, '\\\\');\n};\n\n/**\n * Determines if any character on specific position is escaped\n * @param str\n * @param pos\n */\nexport const isEscaped = (str: string, pos: number) => {\n  let currPos = pos;\n  while (currPos > 0 && str[currPos - 1] === '\\\\') {\n    currPos -= 1;\n  }\n\n  const escCount = pos - currPos;\n\n  return escCount && escCount % 2 > 0;\n};\n\n/**\n * Find first position of unescaped char\n * @param char\n * @param str\n * @param startPosition\n */\nconst findUnescapedCharPosition = (\n  char: string,\n  str: string,\n  startPosition = 0,\n) => {\n  let pos = str.indexOf(char, startPosition);\n  while (pos >= 0) {\n    if (!isEscaped(str, pos)) {\n      return pos;\n    }\n\n    pos = str.indexOf(char, pos + 1);\n  }\n\n  return pos;\n};\n\n/**\n * Check if string has at least one unescaped char or sequence of unescaped chars in proper order\n * Supported only 1-char and 2-chars conditions for now\n * @param char\n * @param str\n * @param startPosition\n */\nconst hasUnescapedChar = (char: string, str: string, startPosition = 0) => {\n  if (char.length === 1) {\n    return findUnescapedCharPosition(char, str, startPosition) >= 0;\n  }\n\n  if (char.length === 2) {\n    const firstCharPos = findUnescapedCharPosition(char[0], str, startPosition);\n    if (firstCharPos >= 0) {\n      return findUnescapedCharPosition(char[1], str, firstCharPos) >= 0;\n    }\n  }\n\n  return false;\n};\n\nexport const isRedisGlob = (str: string) =>\n  hasUnescapedChar('?', str) ||\n  hasUnescapedChar('*', str) ||\n  hasUnescapedChar('[]', str);\n"
  },
  {
    "path": "redisinsight/api/src/utils/hosting-provider-helper.spec.ts",
    "content": "import { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { mockStandaloneRedisClient } from 'src/__mocks__';\nimport { getHostingProvider } from './hosting-provider-helper';\n\nconst getHostingProviderTests = [\n  { input: '127.0.0.1', output: HostingProvider.UNKNOWN_LOCALHOST },\n  { input: '0.0.0.0', output: HostingProvider.UNKNOWN_LOCALHOST },\n  { input: 'localhost', output: HostingProvider.UNKNOWN_LOCALHOST },\n  { input: '172.18.0.2', output: HostingProvider.UNKNOWN_LOCALHOST },\n  { input: '176.87.56.244', output: HostingProvider.UNKNOWN },\n  { input: '192.12.56.244', output: HostingProvider.UNKNOWN },\n  { input: '255.255.56.244', output: HostingProvider.UNKNOWN },\n  { input: 'redis', output: HostingProvider.UNKNOWN },\n  { input: 'demo-redislabs.rlrcp.com', output: HostingProvider.REDIS_CLOUD },\n  { input: 'memorydb.aws.com', output: HostingProvider.AWS_MEMORYDB },\n  {\n    input: 'redis-16781.c273.us-east-1-2.ec2.cloud.redislabs.com',\n    output: HostingProvider.REDIS_CLOUD,\n  },\n  {\n    input: 'redis-16781.c273.us-east-1-2.ec2.cloud.redis-cloud.com',\n    output: HostingProvider.REDIS_CLOUD,\n  },\n  {\n    input: 'redis-16781.c273.us-east-1-2.ec2.cloud.rlrcp.com',\n    output: HostingProvider.REDIS_CLOUD,\n  },\n  {\n    input: 'askubuntu.mki5tz.0001.use1.cache.amazonaws.com',\n    output: HostingProvider.AWS_ELASTICACHE,\n  },\n  {\n    input: 'contoso5.redis.cache.windows.net',\n    output: HostingProvider.AZURE_CACHE,\n  },\n  {\n    input: 'contoso5.redisenterprise.cache.azure.net',\n    output: HostingProvider.AZURE_CACHE_REDIS_ENTERPRISE,\n  },\n  { input: 'demo-redis-provider.unknown.com', output: HostingProvider.UNKNOWN },\n  {\n    input: 'localhost',\n    hello: ['server', 'redis'],\n    info: '# Server\\r\\n' + 'executable:/opt/redis/bin/redis-server',\n    output: HostingProvider.REDIS_COMMUNITY_EDITION,\n  },\n  {\n    input: 'localhost',\n    hello: ['server', 'redis'],\n    info: '# Server\\r\\n' + 'executable:/data/redis-server',\n    output: HostingProvider.REDIS_COMMUNITY_EDITION,\n  },\n  {\n    input: 'localhost',\n    hello: ['server', 'redis'],\n    info: '# Server\\r\\n' + 'executable:/opt/redis-stack/bin/redis-server',\n    output: HostingProvider.REDIS_STACK,\n  },\n  {\n    input: 'localhost',\n    hello: [\n      'server',\n      'redis',\n      'modules',\n      [['name', 'search', 'path', '/enterprise-managed']],\n    ],\n    info: '# Server\\r\\n' + 'redis_version: 7.2.0',\n    output: HostingProvider.OTHER_REDIS_MANAGED,\n  },\n  {\n    input: 'localhost',\n    hello: [\n      'server',\n      'redis',\n      'modules',\n      [['name', 'search', 'path', 'google']],\n    ],\n    output: HostingProvider.MEMORYSTORE,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'server_name:valkey',\n    output: HostingProvider.VALKEY,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'dragonfly_version:df-7.0.0',\n    output: HostingProvider.DRAGONFLY,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'garnet_version:gr-7.0.0',\n    output: HostingProvider.GARNET,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'kvrocks_version:kv-7.0.0',\n    output: HostingProvider.KVROCKS,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'redict_version:rd-7.0.0',\n    output: HostingProvider.REDICT,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'upstash_version:up-7.0.0',\n    output: HostingProvider.UPSTASH,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'ElastiCache:sometinhg',\n    output: HostingProvider.AWS_ELASTICACHE,\n  },\n  {\n    input: 'localhost',\n    info: '# Server\\r\\n' + 'MemoryDB:sometinhg',\n    output: HostingProvider.AWS_MEMORYDB,\n  },\n  {\n    input: 'localhost',\n    info: '#KeyDb\\r\\n' + 'some:data',\n    output: HostingProvider.KEYDB,\n  },\n];\n\ndescribe('getHostingProvider', () => {\n  beforeEach(() => {\n    mockStandaloneRedisClient.sendCommand.mockReset();\n  });\n\n  getHostingProviderTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} `, async () => {\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce(test.hello);\n      mockStandaloneRedisClient.sendCommand.mockResolvedValueOnce(test.info);\n\n      const result = await getHostingProvider(\n        mockStandaloneRedisClient,\n        test.input,\n      );\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/hosting-provider-helper.ts",
    "content": "import { IP_ADDRESS_REGEX, PRIVATE_IP_ADDRESS_REGEX } from 'src/constants';\nimport { HostingProvider } from 'src/modules/database/entities/database.entity';\nimport { RedisClient } from 'src/modules/redis/client';\nimport { convertRedisInfoReplyToObject } from 'src/utils/redis-reply-converter';\n\nconst PROVIDER_HOST_REGEX = {\n  RLCP: /\\.rlrcp\\.com$/,\n  REDISLABS: /\\.redislabs\\.com$/,\n  REDISCLOUD: /\\.redis-cloud\\.com$/,\n  CACHE_AMAZONAWS: /cache\\.amazonaws\\.com$/,\n  CACHE_WINDOWS: /cache\\.windows\\.net$/,\n  RE_CACHE_AZURE: /redisenterprise\\.cache\\.azure\\.net$/,\n};\n\n// Because we do not bind potentially dangerous logic to this.\n// We define a hosting provider for telemetry only.\nexport const getHostingProvider = async (\n  client: RedisClient,\n  databaseHost: string,\n): Promise<HostingProvider> => {\n  try {\n    const host = databaseHost.toLowerCase();\n\n    // Tries to detect the hosting provider from the hostname.\n    if (\n      PROVIDER_HOST_REGEX.RLCP.test(host) ||\n      PROVIDER_HOST_REGEX.REDISLABS.test(host) ||\n      PROVIDER_HOST_REGEX.REDISCLOUD.test(host)\n    ) {\n      return HostingProvider.REDIS_CLOUD;\n    }\n    if (PROVIDER_HOST_REGEX.CACHE_AMAZONAWS.test(host)) {\n      return HostingProvider.AWS_ELASTICACHE;\n    }\n    if (host.includes('memorydb')) {\n      return HostingProvider.AWS_MEMORYDB;\n    }\n    if (PROVIDER_HOST_REGEX.CACHE_WINDOWS.test(host)) {\n      return HostingProvider.AZURE_CACHE;\n    }\n    if (PROVIDER_HOST_REGEX.RE_CACHE_AZURE.test(host)) {\n      return HostingProvider.AZURE_CACHE_REDIS_ENTERPRISE;\n    }\n\n    try {\n      const hello = JSON.stringify(\n        (await client.sendCommand(['hello'], {\n          replyEncoding: 'utf8',\n        })) as string[],\n      ).toLowerCase();\n\n      if (hello.includes('/enterprise-managed')) {\n        return HostingProvider.OTHER_REDIS_MANAGED;\n      }\n\n      if (hello.includes('google')) {\n        return HostingProvider.MEMORYSTORE;\n      }\n    } catch (e) {\n      // ignore errors\n    }\n\n    try {\n      const info = (\n        (await client.sendCommand(['info'], {\n          replyEncoding: 'utf8',\n        })) as string\n      ).toLowerCase();\n\n      if (info.includes('elasticache')) {\n        return HostingProvider.AWS_ELASTICACHE;\n      }\n\n      if (info.includes('memorydb')) {\n        return HostingProvider.AWS_MEMORYDB;\n      }\n\n      if (info.includes('keydb')) {\n        return HostingProvider.KEYDB;\n      }\n\n      if (info.includes('valkey')) {\n        return HostingProvider.VALKEY;\n      }\n\n      if (info.includes('dragonfly_version')) {\n        return HostingProvider.DRAGONFLY;\n      }\n\n      if (info.includes('garnet_version')) {\n        return HostingProvider.GARNET;\n      }\n\n      if (info.includes('kvrocks_version')) {\n        return HostingProvider.KVROCKS;\n      }\n\n      if (info.includes('redict_version')) {\n        return HostingProvider.REDICT;\n      }\n\n      if (info.includes('upstash_version')) {\n        return HostingProvider.UPSTASH;\n      }\n\n      const infoObj = convertRedisInfoReplyToObject(info);\n\n      if (infoObj.server.executable.includes('redis-server')) {\n        if (infoObj.server.executable.includes('redis-stack')) {\n          return HostingProvider.REDIS_STACK;\n        }\n        return HostingProvider.REDIS_COMMUNITY_EDITION;\n      }\n    } catch (e) {\n      // ignore error\n    }\n\n    if (host === '0.0.0.0' || host === 'localhost' || host === '127.0.0.1') {\n      return HostingProvider.UNKNOWN_LOCALHOST;\n    }\n\n    // todo: investigate weather we need this\n    if (IP_ADDRESS_REGEX.test(host) && PRIVATE_IP_ADDRESS_REGEX.test(host)) {\n      return HostingProvider.UNKNOWN_LOCALHOST;\n    }\n  } catch (e) {\n    // ignore any error\n  }\n\n  return HostingProvider.UNKNOWN;\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/index.ts",
    "content": "export * from './config';\nexport * from './converter';\nexport * from './glob-pattern-helper';\nexport * from './catch-redis-errors';\nexport * from './redis-reply-converter';\nexport * from './hosting-provider-helper';\nexport * from './analytics-helper';\nexport * from './class-transformer';\nexport * from './file-helper';\nexport * from './recommendation-helper';\nexport * from './path';\nexport * from './big-string';\n"
  },
  {
    "path": "redisinsight/api/src/utils/logsFormatter.spec.ts",
    "content": "import { BadRequestException, NotFoundException } from '@nestjs/common';\nimport { CloudOauthMisconfigurationException } from 'src/modules/cloud/auth/exceptions';\nimport { AxiosError, AxiosHeaders } from 'axios';\nimport { mockSessionMetadata } from 'src/__mocks__';\nimport {\n  ClientContext,\n  ClientMetadata,\n  SessionMetadata,\n} from 'src/common/models';\nimport {\n  getOriginalErrorCause,\n  logDataToPlain,\n  sanitizeError,\n  sanitizeErrors,\n} from './logsFormatter';\n\nconst stringCause = 'string cause';\nconst objectCause = { object: 'cause' };\nconst simpleError = new Error('Original error');\nsimpleError['some'] = 'field';\nconst errorWithStringCause = new NotFoundException('Not found', {\n  cause: stringCause,\n});\nconst errorWithObjectCause = new NotFoundException('Not found', {\n  cause: objectCause,\n});\nconst errorWithCause = new NotFoundException('Not found', {\n  cause: simpleError,\n});\nconst errorWithCauseDepth2 = new BadRequestException('Bad req', {\n  cause: errorWithCause,\n});\nconst errorWithCauseDepth3 = new CloudOauthMisconfigurationException(\n  'Misconfigured',\n  { cause: errorWithCauseDepth2 },\n);\nconst errorWithObjectCauseDepth = new CloudOauthMisconfigurationException(\n  'Misconfigured',\n  { cause: errorWithObjectCause },\n);\nconst axiosError = new AxiosError(\n  'Request failed with status code 404',\n  'NOT_FOUND',\n  {\n    method: 'get',\n    url: '/test-endpoint',\n    headers: AxiosHeaders.prototype,\n    data: null,\n  },\n  null,\n  {\n    status: 404,\n    statusText: 'Not Found',\n    headers: {},\n    config: {\n      headers: AxiosHeaders.prototype,\n    },\n    data: {\n      message: 'Resource not found',\n    },\n  },\n);\n\nconst mockExtendedClientMetadata = Object.assign(new ClientMetadata(), {\n  databaseId: 'sdb-id',\n  context: ClientContext.Browser,\n  sessionMetadata: Object.assign(new SessionMetadata(), {\n    ...mockSessionMetadata,\n    data: {\n      some: 'data',\n    },\n    requestMetadata: {\n      some: 'meta',\n    },\n  }),\n});\n\nconst mockExtendedSessionMetadata = Object.assign(new SessionMetadata(), {\n  ...mockSessionMetadata,\n  data: {\n    some: 'data 2',\n  },\n  requestMetadata: {\n    some: 'meta 2',\n  },\n});\n\nconst mockLogData: any = {\n  sessionMetadata: mockSessionMetadata,\n  error: errorWithCauseDepth3,\n  data: [\n    errorWithCauseDepth2,\n    {\n      any: [\n        'other',\n        {\n          possible: 'data',\n          with: [\n            'nested',\n            'structure',\n            errorWithCause,\n            {\n              error: simpleError,\n            },\n          ],\n        },\n      ],\n    },\n  ],\n};\nmockLogData.data.push({ circular: mockLogData.data });\n\nconst mockUnsafeLog: any = {\n  clientMetadata: mockExtendedClientMetadata,\n  error: errorWithCauseDepth3,\n  data: [\n    errorWithCauseDepth2,\n    {\n      any: [\n        'other',\n        {\n          possible: 'data',\n          with: [\n            'nested',\n            'structure',\n            errorWithCause,\n            {\n              error: simpleError,\n            },\n          ],\n        },\n        mockExtendedSessionMetadata,\n      ],\n    },\n  ],\n};\nmockUnsafeLog.data.push(mockExtendedSessionMetadata);\nmockUnsafeLog.data[1].any[1].circular = mockExtendedClientMetadata;\nmockUnsafeLog.data.push(mockUnsafeLog.data);\n\ndescribe('logsFormatter', () => {\n  describe('getOriginalErrorCause', () => {\n    it('should return last cause in the chain', () => {\n      expect(getOriginalErrorCause(errorWithCauseDepth3)).toEqual(simpleError);\n    });\n\n    it('should return simple error if it is last in the chain and only the one in cause', () => {\n      expect(getOriginalErrorCause({ cause: simpleError })).toEqual(\n        simpleError,\n      );\n    });\n\n    it('should return string as cause', () => {\n      expect(getOriginalErrorCause(errorWithStringCause)).toEqual(stringCause);\n    });\n\n    it('should return object as cause', () => {\n      expect(getOriginalErrorCause(errorWithObjectCause)).toEqual(objectCause);\n    });\n\n    it('should not fail if input is not specified', () => {\n      expect(getOriginalErrorCause(undefined)).toEqual(undefined);\n    });\n  });\n\n  describe('sanitizeError', () => {\n    it('should sanitize simple error and return only message', () => {\n      expect(sanitizeError(simpleError, { omitSensitiveData: true })).toEqual({\n        type: 'Error',\n        message: simpleError.message,\n      });\n    });\n\n    it('should sanitize simple error and return message (with stack)', () => {\n      expect(sanitizeError(simpleError)).toEqual({\n        type: 'Error',\n        message: simpleError.message,\n        stack: simpleError.stack,\n      });\n    });\n\n    it('should return sanitized object with a single original cause for nested errors', () => {\n      expect(\n        sanitizeError(errorWithCauseDepth3, { omitSensitiveData: true }),\n      ).toEqual({\n        type: 'CloudOauthMisconfigurationException',\n        message: errorWithCauseDepth3.message,\n        cause: {\n          type: 'Error',\n          message: simpleError.message,\n        },\n      });\n    });\n\n    it('should return sanitized object with a single original cause for nested errors', () => {\n      expect(\n        sanitizeError(errorWithObjectCauseDepth, { omitSensitiveData: true }),\n      ).toEqual({\n        type: 'CloudOauthMisconfigurationException',\n        message: errorWithObjectCauseDepth.message,\n        cause: objectCause,\n      });\n    });\n\n    it('should return sanitized object with a single original cause for nested errors (with stack)', () => {\n      expect(sanitizeError(errorWithCauseDepth3)).toEqual({\n        type: 'CloudOauthMisconfigurationException',\n        message: errorWithCauseDepth3.message,\n        stack: errorWithCauseDepth3.stack,\n        cause: {\n          type: 'Error',\n          message: simpleError.message,\n          stack: simpleError.stack,\n        },\n      });\n    });\n\n    it('should sanitize axios error and return only message when sensitive data is omitted', () => {\n      expect(sanitizeError(axiosError, { omitSensitiveData: true })).toEqual({\n        type: 'AxiosError',\n        message: axiosError.message,\n      });\n    });\n  });\n\n  describe('sanitizeErrors', () => {\n    it('should sanitize all errors and replace circular dependencies', () => {\n      expect(sanitizeErrors(mockLogData, { omitSensitiveData: true })).toEqual({\n        sessionMetadata: mockSessionMetadata,\n        error: {\n          type: 'CloudOauthMisconfigurationException',\n          message: 'Misconfigured',\n          cause: {\n            message: 'Original error',\n            type: 'Error',\n          },\n        },\n        data: [\n          {\n            type: 'BadRequestException',\n            message: 'Bad req',\n            cause: {\n              type: 'Error',\n              message: 'Original error',\n            },\n          },\n          {\n            any: [\n              'other',\n              {\n                possible: 'data',\n                with: [\n                  'nested',\n                  'structure',\n                  {\n                    cause: {\n                      message: 'Original error',\n                      type: 'Error',\n                    },\n                    message: 'Not found',\n                    type: 'NotFoundException',\n                  },\n                  {\n                    error: {\n                      message: 'Original error',\n                      type: 'Error',\n                    },\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            circular: '[Circular]',\n          },\n        ],\n      });\n    });\n  });\n\n  describe('logDataToPlain', () => {\n    it('should sanitize all errors and replace circular dependencies after safeTransform of the data', () => {\n      const result: any = logDataToPlain(mockUnsafeLog);\n\n      // should return error instances untouched\n      expect(result.error).toBeInstanceOf(CloudOauthMisconfigurationException);\n      expect(result.data[0]).toBeInstanceOf(BadRequestException);\n      expect(result.data[1].any[1].with[2]).toBeInstanceOf(NotFoundException);\n      expect(result.data[1].any[1].with[3].error).toBeInstanceOf(Error);\n\n      // should sanitize sessionMetadata instances and convert them to plain objects\n      expect(result).toEqual({\n        clientMetadata: {\n          ...mockExtendedClientMetadata,\n          sessionMetadata: {\n            ...mockExtendedClientMetadata.sessionMetadata,\n            requestMetadata: undefined,\n          },\n        },\n        error: errorWithCauseDepth3,\n        data: [\n          errorWithCauseDepth2,\n          {\n            any: [\n              'other',\n              {\n                circular: '[Circular]',\n                possible: 'data',\n                with: [\n                  'nested',\n                  'structure',\n                  errorWithCause,\n                  {\n                    error: simpleError,\n                  },\n                ],\n              },\n              {\n                ...mockExtendedSessionMetadata,\n                requestMetadata: undefined,\n              },\n            ],\n          },\n          '[Circular]',\n          '[Circular]',\n        ],\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/logsFormatter.ts",
    "content": "import { format } from 'winston';\nimport { isArray, isObject, isPlainObject, omit } from 'lodash';\nimport { inspect } from 'util';\nimport config, { Config } from 'src/utils/config';\nimport { instanceToPlain } from 'class-transformer';\n\nconst LOGGER_CONFIG = config.get('logger') as Config['logger'];\n\ntype SanitizeOptions = {\n  omitSensitiveData?: boolean;\n};\n\ntype SanitizedError = {\n  type: string;\n  message: string;\n  stack?: string;\n  cause?: unknown;\n};\n\nexport const getOriginalErrorCause = (cause: unknown): unknown => {\n  if (cause) {\n    return getOriginalErrorCause(cause['cause']) || cause;\n  }\n};\n\nexport const sanitizeError = (\n  error?: Error,\n  opts: SanitizeOptions = {},\n): SanitizedError | undefined => {\n  if (!error) return undefined;\n\n  let cause = getOriginalErrorCause(error['cause']);\n\n  if (cause instanceof Error) {\n    cause = sanitizeError(cause, opts);\n  }\n\n  return {\n    type: error.constructor?.name ?? 'UnknownError',\n    message: String(error.message ?? 'Unknown error'),\n    stack: opts.omitSensitiveData ? undefined : error.stack,\n    cause,\n  };\n};\n\nexport const sanitizeErrors = <T>(\n  obj: T,\n  opts: SanitizeOptions = {},\n  seen = new WeakMap<any, any>(),\n): T => {\n  if (obj instanceof Error) {\n    return sanitizeError(obj, opts) as unknown as T;\n  }\n\n  if (obj === null || typeof obj !== 'object') return obj;\n\n  if (seen.has(obj)) {\n    return '[Circular]' as unknown as T;\n  }\n\n  const clone: any = Array.isArray(obj) ? [] : {};\n  seen.set(obj, clone);\n\n  Object.keys(obj).forEach((key) => {\n    clone[key] = sanitizeErrors(obj[key], opts, seen);\n  });\n\n  return clone;\n};\n\nexport const prepareLogsData = format((info, opts: SanitizeOptions = {}) => {\n  return sanitizeErrors(info, opts);\n});\n\nexport const prettyFileFormat = format.printf((info) => {\n  const separator = ' | ';\n  const timestamp = new Date().toLocaleString();\n  const { level, context, message } = info;\n\n  const logData = [\n    timestamp,\n    `${level}`.toUpperCase(),\n    context,\n    message,\n    inspect(omit(info, ['timestamp', 'level', 'context', 'message', 'stack']), {\n      depth: LOGGER_CONFIG.logDepthLevel,\n    }),\n  ];\n\n  return logData.join(separator);\n});\n\nconst MAX_DEPTH = 10;\nexport const logDataToPlain = (\n  value: any,\n  seen = new WeakSet(),\n  depth = 0,\n): any => {\n  if (depth > MAX_DEPTH) return '[MaxDepthExceeded]';\n\n  if (value === null || typeof value !== 'object' || value instanceof Error) {\n    return value;\n  }\n\n  if (isArray(value)) {\n    if (seen.has(value)) return '[Circular]';\n    seen.add(value);\n    return value.map((val) => logDataToPlain(val, seen, depth + 1));\n  }\n\n  if (isObject(value)) {\n    if (seen.has(value)) return '[Circular]';\n    seen.add(value);\n\n    if (!isPlainObject(value)) {\n      return instanceToPlain(value);\n    }\n\n    const plain = {};\n    Object.keys(value).forEach((key) => {\n      if (Object.prototype.hasOwnProperty.call(value, key)) {\n        plain[key] = logDataToPlain(value[key], seen, depth + 1);\n      }\n    });\n\n    return plain;\n  }\n\n  return value;\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/path.spec.ts",
    "content": "import { winPathToNormalPath } from 'src/utils';\n\nconst winPathToNormalPathTests: Record<string, any>[] = [\n  { input: '\\\\dir/file.js', output: '/dir/file.js' },\n  { input: '/dir/file.js', output: '/dir/file.js' },\n  { input: 'file.js', output: 'file.js' },\n  { input: '\\\\file.js', output: '/file.js' },\n  { input: '\\\\dir\\\\file.js', output: '/dir/file.js' },\n  { input: 'dir/file.js', output: 'dir/file.js' },\n];\n\ndescribe('winPathToNormalPath', () => {\n  winPathToNormalPathTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${JSON.stringify(test.input)}`, () => {\n      const result = winPathToNormalPath(test.input);\n\n      expect(result).toEqual(test.output);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/path.ts",
    "content": "export const winPathToNormalPath = (path: string) => path.replace(/\\\\/g, '/');\n"
  },
  {
    "path": "redisinsight/api/src/utils/promise-with-timeout.spec.ts",
    "content": "import { withTimeout } from 'src/utils/promise-with-timeout';\n\nconst timeoutException = new Error('Timeout exception');\n\ndescribe('promiseWithTimeout', () => {\n  it('should throw timeout exception', async () => {\n    const promise = new Promise((resolve) => {\n      setTimeout(() => resolve('ok'), 2000);\n    });\n\n    try {\n      await withTimeout(promise, 1000, timeoutException);\n    } catch (error) {\n      expect(error).toBe(timeoutException);\n    }\n  });\n  it('should resolve promise', async () => {\n    const promise = new Promise((resolve) => {\n      setTimeout(() => resolve('ok'), 500);\n    });\n\n    const result = await withTimeout(promise, 1000, timeoutException);\n\n    expect(result).toEqual('ok');\n  });\n  it('should reject promise', async () => {\n    const promiseException = new Error('Promise exception');\n    const promise = new Promise((resolve, reject) => {\n      setTimeout(() => reject(promiseException), 500);\n    });\n\n    try {\n      await withTimeout(promise, 1000, timeoutException);\n    } catch (error) {\n      expect(error).toBe(promiseException);\n    }\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/promise-with-timeout.ts",
    "content": "export const withTimeout = (\n  promise: Promise<any>,\n  delay: number,\n  error: Error,\n): Promise<any> => {\n  let timer = null;\n\n  return Promise.race([\n    new Promise((resolve, reject) => {\n      timer = setTimeout(reject, delay, error);\n      return timer;\n    }),\n    promise.then((value) => {\n      clearTimeout(timer);\n      return value;\n    }),\n  ]);\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/recommendation-helper.spec.ts",
    "content": "import {\n  AdditionalSearchModuleName,\n  AdditionalRedisModuleName,\n} from 'src/constants';\nimport {\n  isRedisearchModule,\n  sortRecommendations,\n  checkTimestamp,\n  checkKeyspaceNotification,\n} from './recommendation-helper';\n\nconst nameToModule = (name: string) => ({ name });\n\nconst getOutputForRedisearchAvailable: any[] = [\n  [['1', 'json'].map(nameToModule), false],\n  [['1', 'uoeuoeu ueaooe'].map(nameToModule), false],\n  [['1', 'json', AdditionalRedisModuleName.RediSearch].map(nameToModule), true],\n  [\n    ['1', 'json', AdditionalSearchModuleName.SearchLight].map(nameToModule),\n    true,\n  ],\n  [['1', 'json', AdditionalSearchModuleName.FT].map(nameToModule), true],\n  [['1', 'json', AdditionalSearchModuleName.FTL].map(nameToModule), true],\n];\n\nconst sortRecommendationsTests = [\n  {\n    input: [],\n    expected: [],\n  },\n  {\n    input: [\n      { name: 'luaScript' },\n      { name: 'bigSets' },\n      { name: 'searchIndexes' },\n    ],\n    expected: [\n      { name: 'searchIndexes' },\n      { name: 'bigSets' },\n      { name: 'luaScript' },\n    ],\n  },\n  {\n    input: [{ name: 'luaScript' }, { name: 'bigSets' }, { name: 'searchJSON' }],\n    expected: [\n      { name: 'searchJSON' },\n      { name: 'bigSets' },\n      { name: 'luaScript' },\n    ],\n  },\n  {\n    input: [\n      { name: 'luaScript' },\n      { name: 'bigSets' },\n      { name: 'searchIndexes' },\n      { name: 'searchJSON' },\n      { name: 'useSmallerKeys' },\n      { name: 'RTS' },\n    ],\n    expected: [\n      { name: 'searchJSON' },\n      { name: 'searchIndexes' },\n      { name: 'RTS' },\n      { name: 'bigSets' },\n      { name: 'luaScript' },\n      { name: 'useSmallerKeys' },\n    ],\n  },\n];\n\nconst checkTimestampTests = [\n  { input: '1234567891', expected: true },\n  { input: '1234567891234', expected: true },\n  { input: '1234567891234567', expected: true },\n  { input: '1234567891234567891', expected: true },\n  { input: '1234567891.2', expected: true },\n  { input: '1234567891:12', expected: true },\n  { input: '1234567891a12', expected: true },\n  { input: '10-10-2020', expected: true },\n  { input: '1', expected: false },\n  { input: '123', expected: false },\n  { input: '12345678911', expected: false },\n  { input: '12345678912345', expected: false },\n  { input: '12345678912345678', expected: false },\n  { input: '1234567891.2.2', expected: false },\n  { input: '1234567891asd', expected: false },\n  { input: '-1234567891', expected: false },\n  { input: 'inf', expected: false },\n  { input: '-inf', expected: false },\n];\n\nconst checkKeyspaceNotificationTests = [\n  { input: '', expected: false },\n  { input: 'fdKx', expected: true },\n  { input: 'lsE', expected: true },\n  { input: 'fdkx', expected: false },\n  { input: 'lse', expected: false },\n  { input: 'KfdE', expected: true },\n  { input: '1', expected: false },\n  { input: 'K', expected: true },\n  { input: 'E', expected: true },\n];\n\ndescribe('Recommendation helper', () => {\n  describe('isRedisearchModule', () => {\n    it.each(getOutputForRedisearchAvailable)(\n      'for input: %s (reply), should be output: %s',\n      (reply, expected) => {\n        const result = isRedisearchModule(reply);\n        expect(result).toBe(expected);\n      },\n    );\n  });\n\n  describe('sortRecommendations', () => {\n    test.each(sortRecommendationsTests)('%j', ({ input, expected }) => {\n      const result = sortRecommendations(input);\n      expect(result).toEqual(expected);\n    });\n  });\n\n  describe('checkTimestamp', () => {\n    test.each(checkTimestampTests)('%j', ({ input, expected }) => {\n      expect(checkTimestamp(input)).toEqual(expected);\n    });\n  });\n\n  describe('checkKeyspaceNotification', () => {\n    test.each(checkKeyspaceNotificationTests)('%j', ({ input, expected }) => {\n      expect(checkKeyspaceNotification(input)).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/recommendation-helper.ts",
    "content": "import { sortBy } from 'lodash';\nimport { isValid } from 'date-fns';\n\nimport {\n  REDISEARCH_MODULES,\n  REDIS_STACK,\n  RECOMMENDATION_NAMES,\n  IS_TIMESTAMP,\n  IS_INTEGER_NUMBER_REGEX,\n  IS_NUMBER_REGEX,\n} from 'src/constants';\n\nimport { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';\n\nexport const isRedisearchModule = (modules: AdditionalRedisModule[]): boolean =>\n  modules?.some(({ name }) =>\n    REDISEARCH_MODULES.some((search) => name === search),\n  );\n\nexport const sortRecommendations = (recommendations: any[]) =>\n  sortBy(recommendations, [\n    ({ name }) => name !== RECOMMENDATION_NAMES.SEARCH_JSON,\n    ({ name }) => name !== RECOMMENDATION_NAMES.SEARCH_INDEXES,\n    ({ name }) => !REDIS_STACK.includes(name),\n    ({ name }) => name,\n  ]);\n\nexport const checkTimestamp = (value: string): boolean => {\n  try {\n    if (!IS_NUMBER_REGEX.test(value) && isValid(new Date(value))) {\n      return true;\n    }\n    const integerPart = parseInt(value, 10);\n    if (!IS_TIMESTAMP.test(integerPart.toString())) {\n      return false;\n    }\n    if (integerPart.toString().length === value.length) {\n      return true;\n    }\n    // check part after separator\n    const subPart = value.replace(integerPart.toString(), '');\n    return IS_INTEGER_NUMBER_REGEX.test(subPart.substring(1, subPart.length));\n  } catch (err) {\n    // ignore errors\n    return false;\n  }\n};\n\n// https://redis.io/docs/manual/keyspace-notifications/\nexport const checkKeyspaceNotification = (reply: string): boolean =>\n  reply.indexOf('K') > -1 || reply.indexOf('E') > -1;\n"
  },
  {
    "path": "redisinsight/api/src/utils/redis-modules-summary.spec.ts",
    "content": "import { getRedisModulesSummary } from 'src/utils/redis-modules-summary';\n\nconst DEFAULT_SUMMARY = Object.freeze({\n  RediSearch: { loaded: false },\n  RedisAI: { loaded: false },\n  RedisGraph: { loaded: false },\n  RedisGears: { loaded: false },\n  RedisBloom: { loaded: false },\n  RedisJSON: { loaded: false },\n  RedisTimeSeries: { loaded: false },\n  customModules: [],\n});\n\nconst getRedisModulesSummaryTests = [\n  {\n    input: [{ name: 'ai', version: 20000 }],\n    expected: {\n      ...DEFAULT_SUMMARY,\n      RedisAI: { loaded: true, version: 20000 },\n      customModules: [],\n    },\n  },\n  {\n    input: [{ name: 'search', version: 10000 }],\n    expected: {\n      ...DEFAULT_SUMMARY,\n      RediSearch: { loaded: true, version: 10000 },\n    },\n  },\n  {\n    input: [\n      { name: 'bf', version: 1000 },\n      { name: 'rediSQL', version: 1 },\n    ],\n    expected: {\n      ...DEFAULT_SUMMARY,\n      RedisBloom: { loaded: true, version: 1000 },\n      customModules: [{ name: 'rediSQL', version: 1 }],\n    },\n  },\n  {\n    input: [{ name: 'ReJSON' }],\n    expected: { ...DEFAULT_SUMMARY, RedisJSON: { loaded: true } },\n  },\n  {\n    input: [\n      { name: 'ai', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'graph', version: 20000, semanticVersion: '2.0.0' },\n      { name: 'rg', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'bf' },\n      { name: 'ReJSON', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'search', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'timeseries', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'redisgears_2', version: 10000, semanticVersion: '1.0.0' },\n    ],\n    expected: {\n      RedisAI: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RedisGraph: { loaded: true, version: 20000, semanticVersion: '2.0.0' },\n      RedisGears: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RedisBloom: { loaded: true },\n      RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RedisTimeSeries: {\n        loaded: true,\n        version: 10000,\n        semanticVersion: '1.0.0',\n      },\n      customModules: [\n        {\n          name: 'redisgears_2',\n          semanticVersion: '1.0.0',\n          version: 10000,\n        },\n      ],\n    },\n  },\n  { input: [], expected: DEFAULT_SUMMARY },\n  { input: {}, expected: DEFAULT_SUMMARY },\n  { input: undefined, expected: DEFAULT_SUMMARY },\n  { input: null, expected: DEFAULT_SUMMARY },\n  { input: 1, expected: DEFAULT_SUMMARY },\n];\n\ndescribe('getRedisModulesSummary', () => {\n  test.each(getRedisModulesSummaryTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getRedisModulesSummary(input);\n    expect(result).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/redis-modules-summary.ts",
    "content": "import { cloneDeep } from 'lodash';\nimport {\n  AdditionalRedisModuleName,\n  SUPPORTED_REDIS_MODULES,\n  REDISEARCH_MODULES,\n} from 'src/constants';\nimport { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';\n\ninterface IModuleSummary {\n  loaded: boolean;\n  version?: number;\n  semanticVersion?: number;\n}\ninterface IRedisModulesSummary\n  extends Record<keyof typeof AdditionalRedisModuleName, IModuleSummary> {\n  customModules: AdditionalRedisModule[];\n}\nexport const DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze({\n  RediSearch: { loaded: false },\n  RedisAI: { loaded: false },\n  RedisGraph: { loaded: false },\n  RedisGears: { loaded: false },\n  RedisBloom: { loaded: false },\n  RedisJSON: { loaded: false },\n  RedisTimeSeries: { loaded: false },\n  customModules: [],\n});\n\nexport const isRedisearchAvailable = (\n  modules: AdditionalRedisModule[],\n): boolean =>\n  modules?.some(({ name }) =>\n    REDISEARCH_MODULES.some((search) => name === search),\n  );\n\nconst getEnumKeyBValue = (myEnum: any, enumValue: number | string): string => {\n  const keys = Object.keys(myEnum);\n  const index = keys.findIndex((x) => myEnum[x] === enumValue);\n  return index > -1 ? keys[index] : '';\n};\n\nconst getModuleSummaryToSent = (module: AdditionalRedisModule) => ({\n  loaded: true,\n  version: module.version,\n  semanticVersion: module.semanticVersion,\n});\n\n// same function as in FE\nexport const getRedisModulesSummary = (\n  modules: AdditionalRedisModule[] = [],\n): IRedisModulesSummary => {\n  const summary = cloneDeep(DEFAULT_SUMMARY);\n  try {\n    modules.forEach((module) => {\n      if (SUPPORTED_REDIS_MODULES[module.name]) {\n        const moduleName = getEnumKeyBValue(\n          AdditionalRedisModuleName,\n          module.name,\n        );\n        summary[moduleName] = getModuleSummaryToSent(module);\n        return;\n      }\n\n      if (isRedisearchAvailable([module])) {\n        const redisearchName = getEnumKeyBValue(\n          AdditionalRedisModuleName,\n          AdditionalRedisModuleName.RediSearch,\n        );\n        summary[redisearchName] = getModuleSummaryToSent(module);\n        return;\n      }\n\n      summary.customModules.push(module);\n    });\n  } catch (e) {\n    // continue regardless of error\n  }\n  return summary;\n};\n"
  },
  {
    "path": "redisinsight/api/src/utils/redis-reply-converter.spec.ts",
    "content": "import { mockStandaloneRedisInfoReply } from 'src/__mocks__';\nimport { convertRedisInfoReplyToObject } from './redis-reply-converter';\n\nconst mockRedisServerInfoDto = {\n  redis_version: '6.0.5',\n  redis_mode: 'standalone',\n  os: 'Linux 4.15.0-1087-gcp x86_64',\n  arch_bits: '64',\n  tcp_port: '11113',\n  uptime_in_seconds: '1000',\n};\n\nconst mockStandaloneRedisInfoDto = {\n  server: mockRedisServerInfoDto,\n  clients: {\n    connected_clients: '1',\n    client_longest_output_list: '0',\n    client_biggest_input_buf: '0',\n    blocked_clients: '0',\n  },\n  memory: {\n    used_memory: '1000000',\n    used_memory_human: '1M',\n    used_memory_rss: '1000000',\n    used_memory_peak: '1000000',\n    used_memory_peak_human: '1M',\n    used_memory_lua: '37888',\n    mem_fragmentation_ratio: '1',\n    mem_allocator: 'jemalloc-5.1.0',\n  },\n  cluster: {\n    cluster_enabled: '0',\n  },\n  keyspace: {\n    db0: 'keys=1,expires=0,avg_ttl=0',\n  },\n  stats: {\n    keyspace_hits: '1000',\n    keyspace_misses: '0',\n  },\n  replication: {\n    role: 'master',\n    connected_slaves: '0',\n    master_repl_offset: '0',\n    repl_backlog_active: '0',\n    repl_backlog_size: '1000',\n    repl_backlog_first_byte_offset: '0',\n    repl_backlog_histlen: '0',\n  },\n};\n\nconst mockIncorrectString = '$6\\r\\nfoobar\\r\\n';\n\ndescribe('convertRedisReplyInfoToObject', () => {\n  it('should return object in a defined format', async () => {\n    const result = convertRedisInfoReplyToObject(mockStandaloneRedisInfoReply);\n\n    expect(result).toEqual(mockStandaloneRedisInfoDto);\n  });\n  it('should return empty object when incorrect string passed', async () => {\n    const result = convertRedisInfoReplyToObject(mockIncorrectString);\n\n    expect(result).toEqual({});\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/utils/redis-reply-converter.ts",
    "content": "import { convertMultilineReplyToObject } from 'src/modules/redis/utils';\n\nexport const convertRedisInfoReplyToObject = (info: string): any => {\n  try {\n    const result = {};\n    const sections = info.match(/(?<=#\\s+).*?(?=[\\n,\\r])/g);\n    const values = info.split(/#.*?[\\n,\\r]/g);\n    values.shift();\n    sections.forEach((section: string, index: number) => {\n      result[section.toLowerCase()] = convertMultilineReplyToObject(\n        values[index].trim(),\n      );\n    });\n    return result;\n  } catch (e) {\n    return {};\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/src/validators/index.ts",
    "content": "export * from './serializedJson.validator';\n"
  },
  {
    "path": "redisinsight/api/src/validators/isObjectWithValues.validator.ts",
    "content": "import {\n  isObject,\n  registerDecorator,\n  ValidationArguments,\n  ValidationOptions,\n} from 'class-validator';\n\nexport function IsObjectWithValues(\n  valueValidators: ((value: unknown) => boolean)[],\n  validationOptions?: ValidationOptions,\n) {\n  return (object: unknown, propertyName: string) => {\n    registerDecorator({\n      name: 'IsObjectWithValues',\n      target: (object as any).constructor,\n      propertyName,\n      options: validationOptions,\n\n      validator: {\n        validate(data: unknown) {\n          if (!isObject(data)) return false;\n\n          for (const value of Object.values(data)) {\n            const isInvalidValue = valueValidators.some(\n              (validator) => !validator(value),\n            );\n            if (isInvalidValue) {\n              return false;\n            }\n          }\n\n          return true;\n        },\n        defaultMessage(validationArguments?: ValidationArguments): string {\n          return `${validationArguments.property} should be a valid object with proper values`;\n        },\n      },\n    });\n  };\n}\n"
  },
  {
    "path": "redisinsight/api/src/validators/serializedJson.validator.spec.ts",
    "content": "import { SerializedJsonValidator } from 'src/validators/serializedJson.validator';\n\nconst validator = new SerializedJsonValidator();\n\nconst toValidate = [\n  {\n    name: 'Boolean',\n    value: true,\n  },\n  {\n    name: 'Null',\n    value: null,\n  },\n  {\n    name: 'Number',\n    value: 12,\n  },\n  {\n    name: 'String',\n    value: 'some string',\n  },\n  {\n    name: 'Empty String',\n    value: '',\n  },\n  {\n    name: 'Object',\n    value: { some: 'object', width: ['diff', 'types', 0, 1, null] },\n  },\n  {\n    name: 'Array',\n    value: ['diff', 'types', 0, 1, null, { some: 'obj' }],\n  },\n];\n\ndescribe('SerializedJsonValidator', () => {\n  toValidate.forEach((testCase) => {\n    it(`return true when serialized (${testCase.name})`, () => {\n      expect(validator.validate(JSON.stringify(testCase.value))).toEqual(true);\n    });\n  });\n\n  toValidate.forEach((testCase) => {\n    switch (testCase.name) {\n      case 'Boolean':\n      case 'Number':\n      case 'Null':\n        it(`return true when not serializes (${testCase.name})`, () => {\n          expect(validator.validate(testCase.value)).toEqual(true);\n        });\n        break;\n      default:\n        it(`return false when not serializes (${testCase.name})`, () => {\n          expect(validator.validate(testCase.value)).toEqual(false);\n        });\n    }\n  });\n\n  it('should return particular message by default', () => {\n    expect(validator.defaultMessage({ property: 'path' })).toEqual(\n      'path should be a correct serialized json string',\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/src/validators/serializedJson.validator.ts",
    "content": "import {\n  ValidatorConstraint,\n  ValidatorConstraintInterface,\n} from 'class-validator';\n\n@ValidatorConstraint({ name: 'serialized-json', async: false })\nexport class SerializedJsonValidator implements ValidatorConstraintInterface {\n  validate(data: any): boolean {\n    try {\n      JSON.parse(data);\n    } catch {\n      return false;\n    }\n    return true;\n  }\n\n  defaultMessage(data): string {\n    return `${data.property} should be a correct serialized json string`;\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/stubs/cpu-features/index.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "redisinsight/api/stubs/cpu-features/package.json",
    "content": "{\n  \"name\": \"cpu-features\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\"\n}\n"
  },
  {
    "path": "redisinsight/api/test/README.md",
    "content": "# Integration Tests\n\n## How to Run\n\n### Prerequisites\n\n- **First Time Running Tests?**  \n  If you're running the tests for the first time, please review the [Short Explanation and High-Level Overview](#short-explanation-and-high-level-overview).\n\n- **Running a Subset of Tests**  \n  You can run a specific subset of tests by modifying the `spec` glob in `/redisinsight/api/test/api/.mocharc.yml`. This allows you to execute only the tests you need.\n\n### Steps to Run Tests\n\n1. **Run the Desired Environment**  \n   It's recommended to use Docker for this, as it provides better control over versioning and switching between environments.\n\n1. **Execute the Tests**  \n   From the root directory of the repository, run the following command:\n\n   ```bash\n   yarn test:api:integration\n   ```\n\n#### Example\n\nLet’s walk through an example where you need to run tests related to `string`.\n\n1. **Modify the Test Subset**  \n   Update `/redisinsight/api/test/api/.mocharc.yml` to include only the `string` tests:\n\n   ```yaml\n   spec:\n     - 'test/**/string/**/*.test.ts'\n   ```\n\n1. **Run the Environment in Docker**  \n   For this example, let’s assume you need to run the tests against the `OSS-ST-6` environment. To do this:\n\n   - Navigate to the environment directory:\n\n     ```bash\n     cd /redisinsight/api/test/test-runs/oss-st-6\n     ```\n\n   - Start Docker:\n     ```bash\n     docker-compose up\n     ```\n\n1. **Run the Tests**  \n   Finally, execute the tests from the root directory:\n\n   ```bash\n   yarn test:api:integration\n   ```\n\n---\n\n## Short Explanation and High-Level Overview\n\nThe integration tests follow a basic structure:\n\n- **Redis Environment**: Tests are executed against various Redis environments such as Redis Community Edition (Redis OSS), Redis Cluster, Redis Stack, etc.\n- **Test Suites**: A collection of test scenarios are designed to evaluate different Redis features.\n\n> **Note:** Keep in mind that specific Redis environments might require additional test suites. For example, Redis Stack includes `RediSearch`, which is not available in Redis OSS, meaning tests designed for `RediSearch` will fail if executed in a Redis OSS environment.\n\n---\n\n### Project Structure\n\n#### Tests\n\nThe `/redisinsight/api/test/api` directory contains all the test scenarios for the integration tests.\n\n#### Environments\n\nThe `/redisinsight/api/test/test-runs` directory houses the Redis Test Environments (RTEs). Some of these environments have more complex setups, such as TLS with authentication, and thus include all necessary components (e.g., keys, certificates).\n\n> **Note:** You may encounter some code related to GitHub Actions workflows, as these tests are also automatically run through CI/CD.\n"
  },
  {
    "path": "redisinsight/api/test/api/.mocharc.yml",
    "content": "spec:\n  - test/**/*.test.ts\nrequire: 'test/api/api.deps.init.ts'\nproject: ./test/api/api.tsconfig.json\nretries: 2\ntimeout: 60000\nexit: true\n"
  },
  {
    "path": "redisinsight/api/test/api/_init/WS-notifications-global-sync.test.ts",
    "content": "import { describe, it, expect, _ } from '../deps';\nimport {\n  createNotExistingNotifications,\n  getRepository,\n  repositories,\n} from '../../helpers/local-db';\nimport { Socket } from 'socket.io-client';\nimport { getSocket } from '../../helpers/server';\nimport { constants } from '../../helpers/constants';\n\nconst getClient = async (): Promise<Socket> => {\n  return getSocket('');\n};\n\nlet repo;\ndescribe('WS sync', () => {\n  beforeEach(async () => {\n    repo = await getRepository(repositories.NOTIFICATION);\n    await createNotExistingNotifications(true);\n  });\n\n  it('Should sync notifications and remove not existing in json from local db', async () => {\n    const oldNotifications = await repo\n      .createQueryBuilder()\n      .where({ type: 'global' })\n      .getMany();\n    expect(\n      _.find(oldNotifications, {\n        timestamp: constants.TEST_NOTIFICATION_NE_1.timestamp,\n      }),\n    ).to.not.eq(undefined);\n    expect(\n      _.find(oldNotifications, {\n        timestamp: constants.TEST_NOTIFICATION_NE_2.timestamp,\n      }),\n    ).to.not.eq(undefined);\n    expect(\n      _.find(oldNotifications, {\n        timestamp: constants.TEST_NOTIFICATION_NE_3.timestamp,\n      }),\n    ).to.not.eq(undefined);\n\n    // Initialize sync by connecting\n    const client = await getClient();\n\n    // todo: check states of notifications\n\n    const notificationsAlert: any = await new Promise((res) => {\n      client.on('notification', res);\n    });\n\n    expect(notificationsAlert.notifications.length).to.eq(3);\n    expect(notificationsAlert.totalUnread).to.eq(3);\n\n    const newNotifications = await repo\n      .createQueryBuilder()\n      .where({ type: 'global' })\n      .getMany();\n    expect(\n      _.find(newNotifications, {\n        timestamp: constants.TEST_NOTIFICATION_NE_1.timestamp,\n      }),\n    ).to.eq(undefined);\n    expect(\n      _.find(newNotifications, {\n        timestamp: constants.TEST_NOTIFICATION_NE_2.timestamp,\n      }),\n    ).to.eq(undefined);\n    expect(\n      _.find(newNotifications, {\n        timestamp: constants.TEST_NOTIFICATION_NE_3.timestamp,\n      }),\n    ).to.eq(undefined);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ai/assistant/DELETE-ai-assistant-chats-id.test.ts",
    "content": "import { describe, deps, getMainCheckFn, serverConfig } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport { mockAiChatId } from 'src/__mocks__';\n\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = (id: string = mockAiChatId) =>\n  request(server).delete(`/ai/assistant/chats/${id}`);\n\nconst aiAssistantNock = nock(serverConfig.get('ai').convAiApiUrl)\n  .post('/reset')\n  .reply(200);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /ai/assistant/chats/:id', () => {\n  [\n    {\n      name: 'Should reset chat by id',\n      responseBody: {},\n    },\n    {\n      name: 'Should return Unauthorized error',\n      before: () => {\n        aiAssistantNock.post('/reset').reply(401, {\n          message: 'Custom unauthorized message',\n        });\n      },\n      statusCode: 401,\n      responseBody: {\n        statusCode: 401,\n        error: 'ConvAiUnauthorized',\n        message: 'Request failed with status code 401',\n        errorCode: 11301,\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ai/assistant/GET-ai-assistant-chats-id.test.ts",
    "content": "import { describe, deps, Joi, getMainCheckFn, serverConfig } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport {\n  mockAiChat,\n  mockAiChatId,\n  mockAiHistoryApiResponse,\n} from 'src/__mocks__';\n\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = (id: string = mockAiChatId) =>\n  request(server).get(`/ai/assistant/chats/${id}`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    messages: Joi.array().items(\n      Joi.object({\n        type: Joi.string().allow('HumanMessage', 'AiMessage').required(),\n        content: Joi.string().required(),\n      }),\n    ),\n  })\n  .required();\n\nconst aiAssistantNock = nock(serverConfig.get('ai').convAiApiUrl)\n  .get('/history')\n  .reply(200, JSON.stringify(mockAiHistoryApiResponse));\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /ai/assistant/chats/:id', () => {\n  [\n    {\n      name: 'Should return history with items',\n      responseSchema,\n      responseBody: mockAiChat,\n    },\n    {\n      name: 'Should return empty history and not fail',\n      before: () => {\n        aiAssistantNock.get('/history').reply(200, JSON.stringify([]));\n      },\n      responseSchema,\n      responseBody: {\n        ...mockAiChat,\n        messages: [],\n      },\n    },\n    {\n      name: 'Should return Unauthorized error',\n      before: () => {\n        aiAssistantNock.get('/history').reply(401, {\n          message: 'Custom unauthorized message',\n        });\n      },\n      statusCode: 401,\n      responseBody: {\n        statusCode: 401,\n        error: 'ConvAiUnauthorized',\n        message: 'Request failed with status code 401',\n        errorCode: 11301,\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ai/assistant/POST-ai-assistant-chats-id-messages.test.ts",
    "content": "import { describe, deps, getMainCheckFn, serverConfig } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport {\n  mockAiChatId,\n  mockAiMessage1Response,\n  mockHumanMessage1Response,\n  mockSendAiChatMessageDto,\n} from 'src/__mocks__';\n\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = (id: string = mockAiChatId) =>\n  request(server).post(`/ai/assistant/chats/${id}/messages`);\n\nconst aiAssistantNock = nock(serverConfig.get('ai').convAiApiUrl)\n  .post('/chat')\n  .query(true)\n  .reply(200, mockAiMessage1Response.content);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /ai/assistant/chats/:id/messages', () => {\n  [\n    {\n      name: 'Should respond with text',\n      data: mockSendAiChatMessageDto,\n      // todo: find a way to check response\n      // responseBody: mockAiMessage1Response.content,\n    },\n    {\n      name: 'Should return Unauthorized error',\n      before: () => {\n        aiAssistantNock.post('/chat').query(true).reply(401, {\n          message: 'Custom unauthorized message',\n        });\n      },\n      data: {\n        content: mockHumanMessage1Response.content,\n      },\n      statusCode: 401,\n      responseBody: {\n        statusCode: 401,\n        error: 'ConvAiUnauthorized',\n        message: 'Request failed with status code 401',\n        errorCode: 11301,\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ai/assistant/POST-ai-assistant-chats.test.ts",
    "content": "import { describe, deps, Joi, getMainCheckFn, serverConfig } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport { mockAiChatId } from 'src/__mocks__';\n\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).post('/ai/assistant/chats');\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst aiAssistantNock = nock(serverConfig.get('ai').convAiApiUrl)\n  .post('/auth')\n  .reply(200, { convai_session_id: mockAiChatId });\n\ndescribe('POST /ai/assistant/chats', () => {\n  [\n    {\n      name: 'Should return new chat id',\n      responseSchema,\n      responseBody: {\n        id: mockAiChatId,\n      },\n    },\n    {\n      name: 'Should return Unauthorized error',\n      before: () => {\n        aiAssistantNock.post('/auth').reply(401, {\n          message: 'Custom unauthorized message',\n        });\n      },\n      statusCode: 401,\n      responseBody: {\n        statusCode: 401,\n        error: 'ConvAiUnauthorized',\n        message: 'Request failed with status code 401',\n        errorCode: 11301,\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/analytics/POST-analytics-send-event.test.ts",
    "content": "import {\n  describe,\n  deps,\n  Joi,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  _,\n} from '../deps';\nconst { server, request, constants } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).post('/analytics/send-event');\n\n// input data schema\nconst dataSchema = Joi.object({\n  event: Joi.string().required(),\n  eventData: Joi.object().allow(null),\n}).strict();\n\nconst validInputData = {\n  event: constants.TEST_ANALYTICS_EVENT,\n  eventData: constants.TEST_ANALYTICS_EVENT_DATA,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /analytics/send-event', () => {\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should send telemetry event',\n          data: {\n            event: constants.TEST_ANALYTICS_EVENT,\n            eventData: constants.TEST_ANALYTICS_EVENT_DATA,\n          },\n          statusCode: 204,\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/analytics/POST-analytics-send-page.test.ts",
    "content": "import {\n  describe,\n  deps,\n  Joi,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  _,\n} from '../deps';\nconst { server, request, constants } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).post('/analytics/send-page');\n\n// input data schema\nconst dataSchema = Joi.object({\n  event: Joi.string().required(),\n  eventData: Joi.object().allow(null),\n}).strict();\n\nconst validInputData = {\n  event: constants.TEST_ANALYTICS_PAGE,\n  eventData: constants.TEST_ANALYTICS_EVENT_DATA,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /analytics/send-page', () => {\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should send telemetry page',\n          data: {\n            event: constants.TEST_ANALYTICS_PAGE,\n            eventData: constants.TEST_ANALYTICS_EVENT_DATA,\n          },\n          statusCode: 204,\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/analytics/analytics.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  serverConfig,\n} from '../deps';\nconst { analytics } = deps;\n\ndescribe('Analytics', () => {\n  requirements('rte.serverType=local');\n\n  it('APPLICATION_STARTED', () => {\n    if (serverConfig.get('server').buildType !== 'ELECTRON') {\n      return;\n    }\n\n    const appStarted = analytics.findEvent({\n      event: 'APPLICATION_STARTED',\n    });\n\n    const appFirstStarted = analytics.findEvent({\n      event: 'APPLICATION_FIRST_START',\n    });\n\n    const found = appStarted || appFirstStarted;\n\n    if (!found) {\n      expect.fail(\n        'APPLICATION_STARTED or APPLICATION_FIRST_START events were not found',\n      );\n    }\n\n    expect(found?.properties).to.have.all.keys(\n      'appVersion',\n      'osPlatform',\n      'buildType',\n      'controlNumber',\n      'controlGroup',\n      'port',\n    );\n    expect(found?.properties?.appVersion).to.be.a('string');\n    expect(found?.properties?.osPlatform).to.be.a('string');\n    expect(found?.properties?.buildType).to.be.a('string');\n    expect(found?.properties?.port).to.be.a('number');\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/api.deps.init.ts",
    "content": "import { depsInit } from './deps';\n\n/**\n * Mocha hooks\n * Initiate dependencies before all tests\n */\nexport const mochaHooks = async () => {\n  await depsInit();\n};\n"
  },
  {
    "path": "redisinsight/api/test/api/api.tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"es2017\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"incremental\": true,\n    \"paths\": {\n      \"src/*\": [\"../../src/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/test/api/browser-history/DELETE-browser-histories.test.ts",
    "content": "import { BrowserHistoryMode } from 'src/common/constants';\nimport {\n  Joi,\n  expect,\n  describe,\n  before,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\n\nconst { request, server, localDb, constants } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(`/${constants.API.DATABASES}/${instanceId}/history`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  ids: Joi.array().items(Joi.any()).required(),\n}).strict();\n\nconst validInputData = {\n  ids: [constants.getRandomString()],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`DELETE /databases/:instanceId/history`, () => {\n  before(async () => {\n    await localDb.createDatabaseInstances();\n\n    await localDb.generateBrowserHistory(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n        mode: BrowserHistoryMode.Pattern,\n      },\n      10,\n      true,\n    );\n  });\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should remove multiple browser history items by ids',\n        data: {\n          ids: [\n            constants.TEST_BROWSER_HISTORY_ID_1,\n            constants.TEST_BROWSER_HISTORY_ID_2,\n          ],\n        },\n        query: {\n          mode: BrowserHistoryMode.Pattern,\n        },\n        responseBody: {\n          affected: 2,\n        },\n        before: async () => {\n          expect(\n            await localDb.getBrowserHistoryById(\n              constants.TEST_BROWSER_HISTORY_ID_1,\n            ),\n          ).to.be.an('object');\n          expect(\n            await localDb.getBrowserHistoryById(\n              constants.TEST_BROWSER_HISTORY_ID_2,\n            ),\n          ).to.be.an('object');\n        },\n        after: async () => {\n          expect(\n            await localDb.getBrowserHistoryById(\n              constants.TEST_BROWSER_HISTORY_ID_1,\n            ),\n          ).to.eql(null);\n          expect(\n            await localDb.getBrowserHistoryById(\n              constants.TEST_BROWSER_HISTORY_ID_2,\n            ),\n          ).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/browser-history/DELETE-browser-history-id.test.ts",
    "content": "import { BrowserHistoryMode } from 'src/common/constants';\nimport { expect, describe, before, deps, getMainCheckFn } from '../deps';\n\nconst { request, server, localDb, constants } = deps;\n\n// endpoint to test\nconst endpoint = (id) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/history/${id}`,\n  );\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`DELETE /databases/:instanceId/history/:id`, () => {\n  before(async () => {\n    await localDb.createDatabaseInstances();\n\n    await localDb.generateBrowserHistory(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n        mode: BrowserHistoryMode.Redisearch,\n      },\n      10,\n      true,\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should remove single browser history item',\n        endpoint: () => endpoint(constants.TEST_BROWSER_HISTORY_ID_2),\n        query: {\n          mode: BrowserHistoryMode.Redisearch,\n        },\n        before: async () => {\n          expect(\n            await localDb.getBrowserHistoryById(\n              constants.TEST_BROWSER_HISTORY_ID_2,\n            ),\n          ).to.be.an('object');\n        },\n        after: async () => {\n          expect(\n            await localDb.getBrowserHistoryById(\n              constants.TEST_BROWSER_HISTORY_ID_2,\n            ),\n          ).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/browser-history/GET-browser-histories.test.ts",
    "content": "import {\n  describe,\n  deps,\n  before,\n  getMainCheckFn,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nimport { Joi } from '../../helpers/test';\nimport { BrowserHistoryMode } from 'src/common/constants';\nconst { localDb, request, server, constants } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${instanceId}/history`);\n\n// input query schema\nconst querySchema = Joi.object({\n  mode: Joi.string().valid('pattern', 'redisearch').messages({\n    'any.only': 'mode must be one of the following values: pattern, redisearch',\n  }),\n}).strict();\n\nconst validInputData = {\n  mode: 'pattern',\n};\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object({\n      id: Joi.string().required(),\n      mode: Joi.string().valid('pattern', 'redisearch').required(),\n      filter: Joi.object({\n        type: Joi.string().allow(null),\n        match: Joi.string().required(),\n        count: Joi.number().integer().required(),\n      }).required(),\n    }),\n  )\n  .required()\n  .max(5)\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /databases/:instanceId/history`, () => {\n  before(async () => {\n    await localDb.createDatabaseInstances();\n\n    await localDb.generateBrowserHistory(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n        mode: BrowserHistoryMode.Pattern,\n      },\n      10,\n      true,\n    );\n\n    await localDb.generateBrowserHistory(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n        mode: BrowserHistoryMode.Redisearch,\n      },\n      10,\n      true,\n    );\n  });\n\n  [\n    {\n      name: 'Should get browser history list',\n      responseSchema,\n    },\n  ].map(mainCheckFn);\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(querySchema, validInputData, 'query').map(\n      validateInvalidDataTestCase(endpoint, querySchema, 'query'),\n    );\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-default_data.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n  fsExtra,\n} from '../deps';\nimport { path } from '../../helpers/test';\nconst { rte, request, server, constants } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${id}/bulk-actions/import/default-data`,\n  );\n\nconst connectEndpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${id}/connect`);\n\ndescribe('POST /databases/:id/bulk-actions/import/default-data', () => {\n  requirements(\n    '!rte.sharedData',\n    '!rte.bigData',\n    'rte.serverType=local',\n    'rte.modules.rejson',\n  );\n\n  beforeEach(async () => await rte.data.truncate());\n\n  describe('Common', function () {\n    it('should import default data', async () => {\n      await fsExtra.ensureDir(constants.TEST_DATA_DIR);\n\n      expect(await rte.client.get('string')).to.eq(null);\n      expect(await rte.client.get('json')).to.eq(null);\n      expect(await rte.client.get('should_not_exists')).to.eq(null);\n\n      // create manifest with data\n      const manifest = {\n        files: [\n          {\n            path: 'common',\n          },\n          {\n            path: 'json',\n            modules: ['rejson'],\n          },\n          {\n            path: 'notexistingmodule',\n            modules: ['not existing'],\n          },\n        ],\n      };\n\n      await fsExtra.writeFile(\n        path.join(constants.TEST_DATA_DIR, 'manifest.json'),\n        JSON.stringify(manifest),\n      );\n      await fsExtra.writeFile(\n        path.join(constants.TEST_DATA_DIR, 'common'),\n        'set string string',\n      );\n      await fsExtra.writeFile(\n        path.join(constants.TEST_DATA_DIR, 'json'),\n        'set json json',\n      );\n      await fsExtra.writeFile(\n        path.join(constants.TEST_DATA_DIR, 'notexistingmodule'),\n        'set should_not_exists value',\n      );\n\n      // connect to database\n      await validateApiCall({\n        endpoint: connectEndpoint,\n      });\n\n      // main check\n      await validateApiCall({\n        endpoint,\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 2, succeed: 2, failed: 0, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({}) => {\n          expect(await rte.client.get('string')).to.eq('string');\n          expect(await rte.client.get('json')).to.eq('json');\n          expect(await rte.client.get('should_not_exists')).to.eq(null);\n        },\n      });\n    });\n    it('should return BadRequest when path does not exists', async () => {\n      await fsExtra.remove(path.join(constants.TEST_DATA_DIR, 'manifest.json'));\n\n      await validateApiCall({\n        endpoint,\n        statusCode: 500,\n        responseBody: {\n          statusCode: 500,\n          message: 'Unable to import default data',\n          error: 'Internal Server Error',\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import-tutorial_data.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n} from '../deps';\nimport { AdmZip, path } from '../../helpers/test';\nconst { rte, request, server, constants } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${id}/bulk-actions/import/tutorial-data`,\n  );\n\nconst creatCustomTutorialsEndpoint = () =>\n  request(server).post(`/custom-tutorials`);\n\nconst getZipArchive = () => {\n  const zipArchive = new AdmZip();\n\n  zipArchive.addFile('info.md', Buffer.from('# info.md', 'utf8'));\n  zipArchive.addFile(\n    '_data/data.txt',\n    Buffer.from(`set ${constants.TEST_STRING_KEY_1} bulkimport`, 'utf8'),\n  );\n\n  return zipArchive;\n};\n\ndescribe('POST /databases/:id/bulk-actions/import/tutorial-data', () => {\n  requirements('!rte.sharedData', '!rte.bigData', 'rte.serverType=local');\n\n  beforeEach(async () => await rte.data.truncate());\n\n  describe('Common', function () {\n    let tutorialId;\n    it('should import data', async () => {\n      expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.not.eq(\n        'bulkimport',\n      );\n\n      // create tutorial\n      const zip = getZipArchive();\n      await validateApiCall({\n        endpoint: creatCustomTutorialsEndpoint,\n        attach: ['file', zip.toBuffer(), 'a.zip'],\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          tutorialId = body.id;\n        },\n      });\n\n      await validateApiCall({\n        endpoint,\n        data: {\n          path: path.join('/custom-tutorials', tutorialId, '_data/data.txt'),\n        },\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 1, succeed: 1, failed: 0, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eq(\n            'bulkimport',\n          );\n        },\n      });\n    });\n    it('should import data with static path', async () => {\n      expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.not.eq(\n        'bulkimport',\n      );\n\n      // create tutorial\n      const zip = getZipArchive();\n      await validateApiCall({\n        endpoint: creatCustomTutorialsEndpoint,\n        attach: ['file', zip.toBuffer(), 'a.zip'],\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          tutorialId = body.id;\n        },\n      });\n\n      await validateApiCall({\n        endpoint,\n        data: {\n          path: path.join(\n            '/static/custom-tutorials',\n            tutorialId,\n            '_data/data.txt',\n          ),\n        },\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 1, succeed: 1, failed: 0, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eq(\n            'bulkimport',\n          );\n        },\n      });\n    });\n    it('should return BadRequest when path does not exists', async () => {\n      await validateApiCall({\n        endpoint,\n        data: {\n          path: path.join(\n            '/custom-tutorials',\n            tutorialId,\n            '../../../../../_data/data.txt',\n          ),\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          message: 'Data file was not found',\n          error: 'Bad Request',\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/bulk-actions/POST-databases-id-bulk_actions-import.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n} from '../deps';\nconst { rte, request, server, constants } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${id}/bulk-actions/import`);\n\ndescribe('POST /databases/:id/bulk-actions/import', () => {\n  requirements('!rte.sharedData', '!rte.bigData');\n  beforeEach(async () => await rte.data.truncate());\n\n  describe('Common', function () {\n    it('Should not import anything', async () => {\n      await validateApiCall({\n        endpoint,\n        attach: ['file', Buffer.from('baddata'), 'file.json'],\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 1, succeed: 0, failed: 1, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: ({ body }) => {\n          expect(body.duration).to.gt(0);\n        },\n      });\n    });\n    it('Should import 100 strings', async () => {\n      await validateApiCall({\n        endpoint,\n        attach: [\n          'file',\n          Buffer.from(\n            new Array(100)\n              .fill(100)\n              .map((_v, idx) => `SET key${idx} value${idx}`)\n              .join('\\n'),\n          ),\n          'any_filename_and_ext',\n        ],\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 100, succeed: 100, failed: 0, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get('key0')).to.eq('value0');\n          expect(await rte.client.get('key99')).to.eq('value99');\n          expect(await rte.client.get('key100')).to.eq(null);\n        },\n      });\n    });\n    it('Should import 10K strings', async () => {\n      await validateApiCall({\n        endpoint,\n        attach: [\n          'file',\n          Buffer.from(\n            new Array(10_000)\n              .fill(1)\n              .map((_v, idx) => `SET key${idx} value${idx}`)\n              .join('\\n'),\n          ),\n          'any_filename_and_ext',\n        ],\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: {\n            processed: 10_000,\n            succeed: 10_000,\n            failed: 0,\n            errors: [],\n          },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get('key0')).to.eq('value0');\n          expect(await rte.client.get('key9999')).to.eq('value9999');\n          expect(await rte.client.get('key10000')).to.eq(null);\n        },\n      });\n    });\n    it('Should import 50 out of 100 keys strings', async () => {\n      await validateApiCall({\n        endpoint,\n        attach: [\n          'file',\n          Buffer.from(\n            new Array(25)\n              .fill(1)\n              .map((_v, idx) =>\n                [\n                  `SET key${idx}_1 value${idx}_1`,\n                  `SET \"key${idx}_2\" \"value${idx}_2 \\\\xE2\\\\x82\\\\xAC\"`,\n                  `SET no-value`,\n                  JSON.stringify({ something: 'bad' }),\n                ].join('\\n'),\n              )\n              .join('\\n'),\n          ),\n          'any_filename_and_ext',\n        ],\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 100, succeed: 50, failed: 50, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get('key0_1')).to.eq('value0_1');\n          expect(await rte.client.get('key0_2')).to.eq('value0_2 €');\n          expect(await rte.client.get('key0_3')).to.eq(null);\n          expect(await rte.client.get('key0_4')).to.eq(null);\n\n          expect(await rte.client.get('key24_1')).to.eq('value24_1');\n          expect(await rte.client.get('key24_2')).to.eq('value24_2 €');\n          expect(await rte.client.get('key24_3')).to.eq(null);\n          expect(await rte.client.get('key24_4')).to.eq(null);\n\n          expect(await rte.client.get('key25_0')).to.eq(null);\n        },\n      });\n    });\n    it('Should ignore blank lines', async () => {\n      await validateApiCall({\n        endpoint,\n        attach: [\n          'file',\n          Buffer.from(`\n            \\n\n            \\n\n            SET key0 value0\n            \\n\n                  \\n\n            SET key1 value1\n            \\n\n            \\n\n          `),\n          'any_filename_and_ext',\n        ],\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: { processed: 2, succeed: 2, failed: 0, errors: [] },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get('key0')).to.eq('value0');\n          expect(await rte.client.get('key1')).to.eq('value1');\n        },\n      });\n    });\n    it('Should import 100K strings', async () => {\n      const b = Buffer.from(\n        new Array(100_000)\n          .fill(1)\n          .map((_v, idx) => `SET key${idx} value${idx}`)\n          .join('\\n'),\n      );\n\n      require('fs').writeFileSync('_data', b);\n      await validateApiCall({\n        endpoint,\n        attach: ['file', b, 'any_filename_and_ext'],\n        responseBody: {\n          id: 'empty',\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'upload',\n          summary: {\n            processed: 100_000,\n            succeed: 100_000,\n            failed: 0,\n            errors: [],\n          },\n          progress: null,\n          filter: null,\n          status: 'completed',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.duration).to.gt(0);\n\n          expect(await rte.client.get('key0')).to.eq('value0');\n          expect(await rte.client.get('key99999')).to.eq('value99999');\n          expect(await rte.client.get('key100000')).to.eq(null);\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/certificate/constants.ts",
    "content": "import { Joi } from '../../helpers/test';\n\nexport const caCertSchema = Joi.object().keys({\n  id: Joi.string().required(),\n  name: Joi.string().required(),\n  certificate: Joi.string(),\n  isPreSetup: Joi.boolean().allow(null),\n});\n\nexport const clientCertSchema = Joi.object().keys({\n  id: Joi.string().required(),\n  name: Joi.string().required(),\n  certificate: Joi.string(),\n  key: Joi.string(),\n  isPreSetup: Joi.boolean().allow(null),\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cli/POST-databases-id-cli-uuid-send_cluster_command.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  Joi,\n  _,\n  deps,\n  validateApiCall,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  uuid = constants.TEST_CLI_UUID_1,\n) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/cli/${uuid}/send-cluster-command`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  command: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  outputFormat: Joi.string().allow(null).valid('TEXT', 'RAW'),\n}).strict();\n\nconst validInputData = {\n  command: 'set foo bar',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    response: Joi.string().required(),\n    status: Joi.string().required(),\n  })\n  .required();\n\nconst responseRawSchema = Joi.object()\n  .keys({\n    response: Joi.any().required(),\n    status: Joi.string().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/cli/:uuid/send-cluster-command', () => {\n  requirements('rte.type=CLUSTER');\n\n  before(rte.data.truncate);\n  // Create Redis client for CLI\n  before(\n    async () =>\n      await request(server).patch(\n        `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n      ),\n  );\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should create string',\n        data: {\n          command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n          outputFormat: 'TEXT',\n        },\n        responseSchema,\n        before: async () => {\n          expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n            0,\n          );\n        },\n        after: async () => {\n          expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n            constants.TEST_STRING_VALUE_1,\n          );\n        },\n      },\n      {\n        name: 'Should get string',\n        data: {\n          command: `get ${constants.TEST_STRING_KEY_1}`,\n          outputFormat: 'TEXT',\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.response).to.have.string(constants.TEST_STRING_VALUE_1);\n        },\n      },\n      {\n        name: 'Should remove string',\n        data: {\n          command: `del ${constants.TEST_STRING_KEY_1}`,\n          outputFormat: 'TEXT',\n        },\n        responseSchema,\n        after: async () => {\n          expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n            0,\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Single Node', () => {\n    describe('String', () => {\n      [\n        {\n          name: 'Should create string',\n          data: {\n            command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n        },\n        {\n          name: 'Should get string',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(constants.TEST_STRING_VALUE_1);\n          },\n        },\n        {\n          name: 'Should remove string',\n          data: {\n            command: `del ${constants.TEST_STRING_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Raw output', () => {\n      [\n        {\n          name: 'Should return a string type response',\n          data: {\n            command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n            outputFormat: 'RAW',\n          },\n          responseRawSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.eql('OK');\n          },\n        },\n        {\n          name: 'Should return a number type response',\n          data: {\n            command: `del ${constants.TEST_STRING_KEY_1}`,\n            outputFormat: 'RAW',\n          },\n          responseRawSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.be.a('number');\n          },\n        },\n        {\n          name: 'Should return an array type response',\n          data: {\n            command: `lrange ${constants.TEST_LIST_KEY_1} 0 100`,\n            outputFormat: 'RAW',\n          },\n          responseRawSchema,\n          before: async () => {\n            await rte.client.lpush(\n              constants.TEST_LIST_KEY_1,\n              constants.TEST_LIST_ELEMENT_1,\n              constants.TEST_LIST_ELEMENT_2,\n            );\n          },\n          after: async () => {\n            await rte.client.del(constants.TEST_LIST_KEY_1);\n          },\n          checkFn: ({ body }) => {\n            expect(body.response).to.eql([\n              constants.TEST_LIST_ELEMENT_2,\n              constants.TEST_LIST_ELEMENT_1,\n            ]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  // Skip 'Commands redirection' and 'Client' tests because tested functionalities were removed\n  xdescribe('Commands redirection', () => {\n    const nodes = rte.env.nodes;\n    _.map(nodes, (node) => ({\n      name: `Should create string with redirection if needed (${node.host}:${node.port})`,\n      data: {\n        command: `set ${constants.TEST_STRING_KEY_1} ${node.host}`,\n        outputFormat: 'TEXT',\n        nodeOptions: {\n          host: node.host,\n          port: node.port,\n          enableRedirection: true,\n        },\n      },\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(\n          body[0].response === '\"OK\"' ||\n            body[0].response.toLowerCase().includes('redirected'),\n        ).to.eql(true);\n      },\n      after: async () => {\n        expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n          node.host,\n        );\n      },\n    })).map(mainCheckFn);\n  });\n  xdescribe('Client', () => {\n    [\n      {\n        name: 'Should throw ClientNotFoundError',\n        data: {\n          command: 'info',\n          outputFormat: 'TEXT',\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Client not found or it has been disconnected.',\n          name: 'ClientNotFoundError',\n        },\n        before: async function () {\n          await request(server).delete(\n            `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n          );\n        },\n        after: async function () {\n          await request(server).patch(\n            `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cli/POST-databases-id-cli-uuid-send_command.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  Joi,\n  _,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  requirements,\n  serverConfig,\n} from '../deps';\nimport { ServerService } from 'src/modules/server/server.service';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\nconst { server, request, constants, rte, analytics } = deps;\n\n// endpoint to test\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  uuid = constants.TEST_CLI_UUID_1,\n) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/cli/${uuid}/send-command`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  command: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  outputFormat: Joi.string().allow(null).valid('TEXT', 'RAW'),\n}).strict();\n\nconst validInputData = {\n  command: 'set foo bar',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    response: Joi.string().required(),\n    status: Joi.string().required(),\n  })\n  .required();\n\nconst responseRawSchema = Joi.object()\n  .keys({\n    response: Joi.any().required(),\n    status: Joi.string().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/cli/:uuid/send-command', () => {\n  requirements('rte.type=STANDALONE');\n\n  before(rte.data.truncate);\n  // Create Redis client for CLI\n  beforeEach(\n    async () =>\n      await request(server).patch(\n        `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n      ),\n  );\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    describe('Analytics', () => {\n      requirements('rte.serverType=local');\n      const key = constants.getRandomString();\n      [\n        {\n          name: 'Should create string and send analytics event for it',\n          data: {\n            command: `set ${key} ${constants.TEST_STRING_VALUE_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(key)).to.eql(0);\n          },\n          after: async () => {\n            expect(await rte.client.get(key)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            );\n            await analytics.waitForEvent({\n              event: 'CLI_COMMAND_EXECUTED',\n              properties: {\n                databaseId: constants.TEST_INSTANCE_ID,\n                commandType: 'core',\n                moduleName: 'n/a',\n                capability: 'string',\n                command: 'SET',\n                outputFormat: 'TEXT',\n                buildType: ServerService.getAppType(\n                  serverConfig.get('server').buildType,\n                ),\n              },\n            });\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('String', () => {\n      [\n        {\n          name: 'Should create string',\n          data: {\n            command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n        },\n        {\n          name: 'Should get string',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(constants.TEST_STRING_VALUE_1);\n          },\n        },\n        {\n          name: 'Should remove string',\n          data: {\n            command: `del ${constants.TEST_STRING_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('List', () => {\n      [\n        {\n          name: 'Should create list',\n          data: {\n            command: `lpush ${constants.TEST_LIST_KEY_1} ${constants.TEST_LIST_ELEMENT_1} ${constants.TEST_LIST_ELEMENT_2}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_LIST_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.client.lrange(constants.TEST_LIST_KEY_1, 0, 100),\n            ).to.eql([\n              constants.TEST_LIST_ELEMENT_2,\n              constants.TEST_LIST_ELEMENT_1,\n            ]);\n          },\n        },\n        {\n          name: 'Should get list',\n          data: {\n            command: `lrange ${constants.TEST_LIST_KEY_1} 0 100`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(\n              `1) \"${constants.TEST_LIST_ELEMENT_2}\"`,\n            );\n            expect(body.response).to.have.string(\n              `2) \"${constants.TEST_LIST_ELEMENT_1}\"`,\n            );\n          },\n        },\n        {\n          name: 'Should remove list',\n          data: {\n            command: `del ${constants.TEST_LIST_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_LIST_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Set', () => {\n      [\n        {\n          name: 'Should create set',\n          data: {\n            command: `sadd ${constants.TEST_SET_KEY_1} ${constants.TEST_SET_MEMBER_1} ${constants.TEST_SET_MEMBER_2}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_SET_KEY_1)).to.eql(0);\n          },\n          after: async () => {\n            const [cursor, set] = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n            );\n            expect(cursor).to.eql('0');\n            expect(set.length).to.eql(2);\n            expect(set.join()).to.include(constants.TEST_SET_MEMBER_1);\n            expect(set.join()).to.include(constants.TEST_SET_MEMBER_2);\n          },\n        },\n        {\n          name: 'Should get set',\n          data: {\n            command: `sscan ${constants.TEST_SET_KEY_1} 0 count 100`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(constants.TEST_SET_MEMBER_2);\n            expect(body.response).to.have.string(constants.TEST_SET_MEMBER_1);\n          },\n        },\n        {\n          name: 'Should remove list',\n          data: {\n            command: `del ${constants.TEST_SET_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_SET_KEY_1)).to.eql(0);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('ZSet', () => {\n      [\n        {\n          name: 'Should create zset',\n          data: {\n            command: `zadd ${constants.TEST_ZSET_KEY_1} 1 ${constants.TEST_ZSET_MEMBER_1} 2 ${constants.TEST_ZSET_MEMBER_2}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_ZSET_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 100),\n            ).to.deep.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n            ]);\n          },\n        },\n        {\n          name: 'Should get zset',\n          data: {\n            command: `zrange ${constants.TEST_ZSET_KEY_1} 0 100`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(\n              `1) \"${constants.TEST_ZSET_MEMBER_1}\"`,\n            );\n            expect(body.response).to.have.string(\n              `2) \"${constants.TEST_ZSET_MEMBER_2}\"`,\n            );\n          },\n        },\n        {\n          name: 'Should remove zset',\n          data: {\n            command: `del ${constants.TEST_ZSET_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_ZSET_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Hash', () => {\n      [\n        {\n          name: 'Should create hash',\n          data: {\n            command: `hset ${constants.TEST_HASH_KEY_1} ${constants.TEST_HASH_FIELD_1_NAME} ${constants.TEST_HASH_FIELD_1_VALUE}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_HASH_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              convertArrayReplyToObject(\n                await rte.client.hgetall(constants.TEST_HASH_KEY_1),\n              ),\n            ).to.deep.eql({\n              [constants.TEST_HASH_FIELD_1_NAME]:\n                constants.TEST_HASH_FIELD_1_VALUE,\n            });\n          },\n        },\n        {\n          name: 'Should get hash',\n          data: {\n            command: `hgetall ${constants.TEST_HASH_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(\n              `1) \"${constants.TEST_HASH_FIELD_1_NAME}\"`,\n            );\n            expect(body.response).to.have.string(\n              `2) \"${constants.TEST_HASH_FIELD_1_VALUE}\"`,\n            );\n          },\n        },\n        {\n          name: 'Should remove hash',\n          data: {\n            command: `del ${constants.TEST_HASH_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_HASH_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('ReJSON-RL', () => {\n      requirements('rte.modules.rejson');\n      [\n        {\n          name: 'Should create json',\n          data: {\n            command: `json.set ${constants.TEST_REJSON_KEY_1} . \"{\\\\\"field\\\\\":\\\\\"value\\\\\"}\"`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_REJSON_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_1,\n                '.',\n              ),\n            ).to.eql('{\"field\":\"value\"}');\n          },\n        },\n        {\n          name: 'Should get json',\n          data: {\n            command: `json.get ${constants.TEST_REJSON_KEY_1} .field`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(`value`);\n            expect(body.response).to.have.string(`\\\\\"`);\n          },\n        },\n        {\n          name: 'Should remove json',\n          data: {\n            command: `json.del ${constants.TEST_REJSON_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_REJSON_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('TSDB-TYPE', () => {\n      requirements('rte.modules.timeseries');\n      [\n        {\n          name: 'Should create ts',\n          data: {\n            command: `ts.create ${constants.TEST_TS_KEY_1} ${constants.TEST_TS_VALUE_1} ${constants.TEST_TS_VALUE_2}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_TS_KEY_1)).to.eql(0);\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_TS_KEY_1)).to.eql(1);\n          },\n        },\n        {\n          name: 'Should add to ts',\n          data: {\n            command: `ts.add ${constants.TEST_TS_KEY_1} ${constants.TEST_TS_TIMESTAMP_1} ${constants.TEST_TS_VALUE_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(\n              await rte.data.executeCommand('ts.get', constants.TEST_TS_KEY_1),\n            ).to.eql([\n              constants.TEST_TS_TIMESTAMP_1,\n              constants.TEST_TS_VALUE_1.toString(),\n            ]);\n          },\n        },\n        {\n          name: 'Should get ts',\n          data: {\n            command: `ts.get ${constants.TEST_TS_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(`2) \"10\"`);\n          },\n        },\n        {\n          name: 'Should remove ts',\n          data: {\n            command: `del ${constants.TEST_TS_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_TS_KEY_1)).to.eql(0);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Graph', () => {\n      requirements('rte.modules.graph');\n      [\n        {\n          name: 'Should create graph',\n          data: {\n            command: `graph.query ${constants.TEST_GRAPH_KEY_1} \"CREATE (n1)\"`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(`1) \"Nodes created: 1\"`);\n          },\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_GRAPH_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_GRAPH_KEY_1)).to.eql(\n              1,\n            );\n          },\n        },\n        {\n          name: 'Should get graph',\n          data: {\n            command: `graph.query ${constants.TEST_GRAPH_KEY_1} \"MATCH (n1) RETURN n1\"`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(`1) \"n1\"`);\n          },\n        },\n        {\n          name: 'Should remove graph',\n          data: {\n            command: `del ${constants.TEST_GRAPH_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_GRAPH_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('RediSearch v2', () => {\n      describe('Hash', () => {\n        requirements('rte.modules.search', 'rte.modules.search.version>=20000');\n        [\n          {\n            name: 'Should create index',\n            data: {\n              command: `ft.create ${constants.TEST_SEARCH_HASH_INDEX_1} ON HASH\n              PREFIX 1 ${constants.TEST_SEARCH_HASH_KEY_PREFIX_1} NOOFFSETS SCHEMA title TEXT WEIGHT 5.0`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string('\"OK\"');\n            },\n            before: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n            after: async () => {\n              expect(await rte.client.call(`ft._list`)).to.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n          },\n          {\n            name: 'Should return the list of all existing indexes.',\n            data: {\n              command: `ft._list`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.response).to.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n          },\n          {\n            name: 'Should return index info',\n            data: {\n              outputFormat: 'RAW',\n              command: `ft.info ${constants.TEST_SEARCH_HASH_INDEX_1}`,\n            },\n            responseRawSchema,\n            checkFn: ({ body }) => {\n              expect(body.response[0]).to.eql('index_name');\n              expect(body.response[1]).to.eql(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n              expect(body.response[2]).to.eql('index_options');\n              expect(body.response[3]).to.eql(['NOOFFSETS']);\n              expect(body.response[4]).to.eql('index_definition');\n              expect(_.take(body.response[5], 4)).to.eql([\n                'key_type',\n                'HASH',\n                'prefixes',\n                [constants.TEST_SEARCH_HASH_KEY_PREFIX_1],\n              ]);\n              // redisearch return attributes in the current build.\n              // todo: confirm that there were breaking changes in the new redisearch release\n              // expect(body.response[6]).to.eql('fields');\n              // expect(body.response[7]).to.deep.include( [ 'title', 'type', 'TEXT', 'WEIGHT', '5' ]);\n            },\n          },\n          {\n            name: 'Should find documents',\n            data: {\n              command: `ft.search ${constants.TEST_SEARCH_HASH_INDEX_1} \"hello world\"`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            before: async () => {\n              for (let i = 0; i < 10; i++) {\n                await rte.client.hset(\n                  `${constants.TEST_SEARCH_HASH_KEY_PREFIX_1}${i}`,\n                  'title',\n                  `hello world ${i}`,\n                );\n              }\n            },\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string(`1) 10`);\n            },\n          },\n          {\n            name: 'Should aggregate documents by uniq @title',\n            data: {\n              command: `ft.aggregate ${constants.TEST_SEARCH_HASH_INDEX_1} * GROUPBY 1 @title`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string(`1) 10`);\n            },\n          },\n          {\n            name: 'Should remove index',\n            data: {\n              command: `ft.dropindex ${constants.TEST_SEARCH_HASH_INDEX_1} DD`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            after: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n          },\n        ].map(mainCheckFn);\n      });\n      describe('JSON', () => {\n        requirements(\n          'rte.modules.search',\n          'rte.modules.rejson',\n          'rte.modules.search.version>=20200',\n          'rte.modules.rejson>=20000',\n        );\n        [\n          {\n            name: 'Should create index',\n            data: {\n              command: `ft.create ${constants.TEST_SEARCH_JSON_INDEX_1} ON JSON\n              PREFIX 1 ${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}\n              NOOFFSETS SCHEMA $.user.name AS name TEXT`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string('\"OK\"');\n            },\n            before: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n            },\n            after: async () => {\n              expect(await rte.client.call(`ft._list`)).to.include(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n            },\n          },\n          {\n            name: 'Should return index info',\n            data: {\n              outputFormat: 'RAW',\n              command: `ft.info ${constants.TEST_SEARCH_JSON_INDEX_1}`,\n            },\n            responseRawSchema,\n            checkFn: ({ body }) => {\n              expect(body.response[0]).to.eql('index_name');\n              expect(body.response[1]).to.eql(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n              expect(body.response[2]).to.eql('index_options');\n              expect(body.response[3]).to.eql(['NOOFFSETS']);\n              expect(body.response[4]).to.eql('index_definition');\n              expect(_.take(body.response[5], 4)).to.eql([\n                'key_type',\n                'JSON',\n                'prefixes',\n                [constants.TEST_SEARCH_JSON_KEY_PREFIX_1],\n              ]);\n              // expect(body.response[6]).to.eql('fields');\n              // expect(body.response[7]).to.deep.include( [ 'name', 'type', 'TEXT', 'WEIGHT', '1' ]);\n            },\n          },\n          {\n            name: 'Should find documents',\n            data: {\n              command: `ft.search ${constants.TEST_SEARCH_JSON_INDEX_1} \"@name:(John)\"`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            before: async () => {\n              for (let i = 0; i < 10; i++) {\n                await rte.client.call('json.set', [\n                  `${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}${i}`,\n                  '$',\n                  `{\"user\":{\"name\":\"John Smith${i}\"}}`,\n                ]);\n              }\n            },\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string(`1) 10`);\n            },\n          },\n          {\n            name: 'Should aggregate documents by uniq @name',\n            data: {\n              command: `ft.aggregate ${constants.TEST_SEARCH_JSON_INDEX_1} * GROUPBY 1 @name`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string(`1) 10`);\n            },\n          },\n          {\n            name: 'Should remove index',\n            data: {\n              command: `ft.dropindex ${constants.TEST_SEARCH_JSON_INDEX_1} DD`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            after: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n    describe('RediSearch v1', () => {\n      describe('Hash', () => {\n        requirements('rte.modules.ft', 'rte.modules.ft.version>=10615');\n        [\n          {\n            name: 'Should create index',\n            data: {\n              command: `ft.create ${constants.TEST_SEARCH_HASH_INDEX_1} NOOFFSETS SCHEMA title TEXT WEIGHT 5.0`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string('\"OK\"');\n            },\n            before: async () => {\n              let errorMessage = '';\n              try {\n                await rte.client.call('ft.info', [\n                  constants.TEST_SEARCH_HASH_INDEX_1,\n                ]);\n              } catch ({ message }) {\n                errorMessage = message;\n              }\n              expect(errorMessage).to.eql('Unknown Index name');\n            },\n            after: async () => {\n              expect(\n                await rte.client.call('ft.info', [\n                  constants.TEST_SEARCH_HASH_INDEX_1,\n                ]),\n              ).to.include(constants.TEST_SEARCH_HASH_INDEX_1);\n            },\n          },\n          {\n            name: 'Should return index info',\n            data: {\n              outputFormat: 'RAW',\n              command: `ft.info ${constants.TEST_SEARCH_HASH_INDEX_1}`,\n            },\n            responseRawSchema,\n            checkFn: ({ body }) => {\n              expect(body.response[0]).to.eql('index_name');\n              expect(body.response[1]).to.eql(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n              expect(body.response[2]).to.eql('index_options');\n              expect(body.response[3]).to.eql(['NOOFFSETS']);\n              expect(body.response[4]).to.eql('fields');\n              expect(body.response[5]).to.deep.include([\n                'title',\n                'type',\n                'TEXT',\n                'WEIGHT',\n                '5',\n              ]);\n            },\n          },\n          {\n            name: 'Should find documents',\n            data: {\n              command: `ft.search ${constants.TEST_SEARCH_HASH_INDEX_1} \"hello world\"`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            before: async () => {\n              for (let i = 0; i < 10; i++) {\n                await rte.client.call('ft.add', [\n                  constants.TEST_SEARCH_HASH_INDEX_1,\n                  `${constants.TEST_SEARCH_HASH_KEY_PREFIX_1}${i}`,\n                  '1.0',\n                  'FIELDS',\n                  'title',\n                  'hello world',\n                ]);\n              }\n            },\n            checkFn: ({ body }) => {\n              expect(body.response).to.have.string(`1) 10`);\n            },\n          },\n          {\n            name: 'Should remove index',\n            data: {\n              command: `ft.drop ${constants.TEST_SEARCH_HASH_INDEX_1}`,\n              outputFormat: 'TEXT',\n            },\n            responseSchema,\n            after: async () => {\n              let errorMessage = '';\n              try {\n                await rte.client.call('ft.info', [\n                  constants.TEST_SEARCH_HASH_INDEX_1,\n                ]);\n              } catch ({ message }) {\n                errorMessage = message;\n              }\n              expect(errorMessage).to.eql('Unknown Index name');\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n    describe('Stream', () => {\n      requirements('rte.version>=5.0');\n      [\n        {\n          name: 'Should create stream',\n          data: {\n            command: `xadd ${constants.TEST_STREAM_KEY_1} * ${constants.TEST_STREAM_DATA_1} ${constants.TEST_STREAM_DATA_2}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_STREAM_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STREAM_KEY_1)).to.eql(\n              1,\n            );\n          },\n        },\n        {\n          name: 'Should get stream',\n          data: {\n            command: `xrange ${constants.TEST_STREAM_KEY_1} - +`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.have.string(\n              `1) \"${constants.TEST_STREAM_DATA_1}\"`,\n            );\n            expect(body.response).to.have.string(\n              `2) \"${constants.TEST_STREAM_DATA_2}\"`,\n            );\n          },\n        },\n        {\n          name: 'Should remove stream',\n          data: {\n            command: `del ${constants.TEST_STREAM_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STREAM_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Bad commands', () => {\n      [\n        {\n          name: 'Should return error if invalid command sent',\n          data: {\n            command: `setx ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include('ERR unknown command');\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (monitor)',\n          data: {\n            command: `monitor`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (subscribe)',\n          data: {\n            command: `subscribe`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (psubscribe)',\n          data: {\n            command: `psubscribe`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (sync)',\n          data: {\n            command: `sync`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (psync)',\n          data: {\n            command: `psync`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (script debug)',\n          data: {\n            command: `script debug`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (hello 3)',\n          data: {\n            command: `hello 3`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.status).to.eql('fail');\n            expect(body.response).to.include(\n              'command is not supported by the Redis Insight CLI',\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Blocking commands', () => {\n      [\n        {\n          name: 'Should use blocking command (unblock by cli command)',\n          data: {\n            command: `blpop ${constants.TEST_LIST_KEY_2} 0`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async function () {\n            // unblock command after 1 sec\n            setTimeout(async () => {\n              const clients = (await rte.client.client('list')).split('\\n');\n              const currentClient = clients.filter(\n                (client) => client.toLowerCase().indexOf('cmd=blpop') > -1,\n              );\n              expect(currentClient.length).to.eql(1);\n\n              const blockedClientId = currentClient[0].match(/^id=(\\d+)/)[1];\n              await rte.client.client('unblock', blockedClientId);\n            }, 5000);\n          },\n        },\n        {\n          name: 'Should use blocking command (unblock by adding element)',\n          data: {\n            command: `blpop ${constants.TEST_LIST_KEY_2} 0`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async function () {\n            // unblock command after 1 sec\n            setTimeout(async () => {\n              await rte.client.lpush(constants.TEST_LIST_KEY_2, 'element');\n            }, 5000);\n          },\n        },\n        {\n          name: 'Should use blocking command (unblock by removing client through API)',\n          data: {\n            command: `blpop ${constants.TEST_LIST_KEY_2} 0`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async function () {\n            // unblock command after 1 sec\n            setTimeout(async () => {\n              await request(server).delete(\n                `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n              );\n            }, 1000);\n          },\n        },\n        {\n          name: 'Should remove list',\n          data: {\n            command: `del ${constants.TEST_LIST_KEY_1}`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          before: async () => {\n            await request(server).patch(\n              `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_LIST_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Human readable commands', () => {\n      [\n        {\n          name: 'Should return server info in correct text format',\n          data: {\n            command: `info server`,\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.response).to.include('# Server\\r\\n');\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('Raw output', () => {\n    [\n      {\n        name: 'Should return a string type response',\n        data: {\n          command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        checkFn: ({ body }) => {\n          expect(body.response).to.eql('OK');\n        },\n      },\n      {\n        name: 'Should return a number type response',\n        data: {\n          command: `del ${constants.TEST_STRING_KEY_1}`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        checkFn: ({ body }) => {\n          expect(body.response).to.be.a('number');\n        },\n      },\n      {\n        name: 'Should return an array type response',\n        data: {\n          command: `lrange ${constants.TEST_LIST_KEY_1} 0 100`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        before: async () => {\n          await rte.client.lpush(\n            constants.TEST_LIST_KEY_1,\n            constants.TEST_LIST_ELEMENT_1,\n            constants.TEST_LIST_ELEMENT_2,\n          );\n        },\n        after: async () => {\n          await rte.client.del(constants.TEST_LIST_KEY_1);\n        },\n        checkFn: ({ body }) => {\n          expect(body.response).to.eql([\n            constants.TEST_LIST_ELEMENT_2,\n            constants.TEST_LIST_ELEMENT_1,\n          ]);\n        },\n      },\n      {\n        name: 'Should return an object type response',\n        data: {\n          command: `hgetall ${constants.TEST_HASH_KEY_1}`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        before: async () => {\n          await rte.client.hset(constants.TEST_HASH_KEY_1, [\n            constants.TEST_HASH_FIELD_1_NAME,\n            constants.TEST_HASH_FIELD_1_VALUE,\n          ]);\n        },\n        after: async () => {\n          await rte.client.del(constants.TEST_HASH_KEY_1);\n        },\n        checkFn: ({ body }) => {\n          expect([\n            // TODO: investigate the difference between getting a hash\n            // result from ioredis\n            {\n              [constants.TEST_HASH_FIELD_1_NAME]:\n                constants.TEST_HASH_FIELD_1_VALUE,\n            },\n            // result from node-redis\n            [\n              constants.TEST_HASH_FIELD_1_NAME,\n              constants.TEST_HASH_FIELD_1_VALUE,\n            ],\n          ]).to.deep.contain(body.response);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  // Skip 'Cluster' tests because tested functionalities were removed\n  xdescribe('Client', () => {\n    [\n      {\n        name: 'Should throw ClientNotFoundError',\n        data: {\n          command: `info`,\n          outputFormat: 'TEXT',\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Client not found or it has been disconnected.',\n          name: 'ClientNotFoundError',\n        },\n        before: async function () {\n          await request(server).delete(\n            `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n          );\n        },\n        after: async function () {\n          await request(server).patch(\n            `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n\ndescribe('POST /databases/:instanceId/cli/:uuid/send-command (MULTI)', () => {\n  requirements('rte.type=STANDALONE');\n\n  before(rte.data.truncate);\n  // Create Redis client for CLI\n  before(\n    async () =>\n      await request(server).patch(\n        `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/cli/${constants.TEST_CLI_UUID_1}`,\n      ),\n  );\n\n  describe('Raw output', () => {\n    [\n      {\n        name: 'Should start transaction',\n        data: {\n          command: `multi`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        checkFn: ({ body }) => {\n          expect(body.response).to.eq('OK');\n        },\n      },\n      {\n        name: 'Should create string',\n        data: {\n          command: `set ${constants.TEST_STRING_KEY_1} bar`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        checkFn: ({ body }) => {\n          expect(body.response).to.eq('QUEUED');\n        },\n      },\n      {\n        name: 'Should create string',\n        data: {\n          command: `incr ${constants.TEST_STRING_KEY_1}`,\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        checkFn: ({ body }) => {\n          expect(body.response).to.eq('QUEUED');\n        },\n      },\n      {\n        name: 'Should create string',\n        data: {\n          command: 'exec',\n          outputFormat: 'RAW',\n        },\n        responseRawSchema,\n        checkFn: ({ body }) => {\n          expect([\n            // TODO: investigate the difference between errors\n            // result from ioredis\n            ['OK', 'ReplyError: ERR value is not an integer or out of range'],\n            // result from node-redis\n            ['OK', 'Error: ERR value is not an integer or out of range'],\n          ]).to.deep.contain(body.response);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cli/POST-databases-id-cli.test.ts",
    "content": "import {\n  describe,\n  it,\n  before,\n  Joi,\n  deps,\n  validateApiCall,\n  requirements,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/cli`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    uuid: Joi.string().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('POST /databases/:id/cli', () => {\n  requirements('rte.type=STANDALONE');\n\n  before(rte.data.truncate);\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should create new cli client',\n        statusCode: 201,\n        responseSchema,\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/autodiscovery/GET-cloud-autodiscovery-account.test.ts",
    "content": "import {\n  describe,\n  deps,\n  requirements,\n  Joi,\n  nock,\n  getMainCheckFn,\n  serverConfig,\n} from '../../deps';\nimport { mockCloudAccountInfo, mockCloudCapiAccount } from 'src/__mocks__';\nimport { CustomErrorCodes } from 'src/constants';\nconst { request, server, constants } = deps;\n\nconst endpoint = () => request(server).get(`/cloud/autodiscovery/account`);\n\nconst headers = {\n  'x-cloud-api-key': constants.TEST_CLOUD_API_KEY,\n  'x-cloud-api-secret': constants.TEST_CLOUD_API_SECRET_KEY,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    accountId: Joi.number().required(),\n    accountName: Joi.string().required(),\n    ownerName: Joi.string().required(),\n    ownerEmail: Joi.string().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst nockScope = nock(serverConfig.get('cloud').capiUrl);\n\ndescribe('GET /cloud/autodiscovery/account', () => {\n  requirements('rte.serverType=local');\n\n  describe('Common', () => {\n    [\n      {\n        before: () => {\n          nockScope.get('/').reply(200, { account: mockCloudCapiAccount });\n        },\n        name: 'Should get account info',\n        headers,\n        responseSchema,\n        responseBody: mockCloudAccountInfo,\n      },\n      {\n        before: () => {\n          nockScope.get('/').reply(403, {\n            response: {\n              status: 403,\n              data: { message: 'Unauthorized for this action' },\n            },\n          });\n        },\n        name: 'Should throw Forbidden error when api returned 403 error',\n        headers,\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'CloudApiForbidden',\n          errorCode: CustomErrorCodes.CloudApiForbidden,\n        },\n      },\n      {\n        before: () => {\n          nockScope.get('/').reply(401, {\n            response: {\n              status: 401,\n              data: '',\n            },\n          });\n        },\n        name: 'Should throw Unauthorized error when api returns 401 error',\n        headers,\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'CloudCapiUnauthorized',\n          errorCode: CustomErrorCodes.CloudCapiUnauthorized,\n        },\n      },\n      {\n        name: 'Should throw Unauthorized error when api key or secret was not provided',\n        headers: {},\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'Unauthorized',\n          message: 'Required authentication credentials were not provided',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/autodiscovery/GET-cloud-autodiscovery-subscriptions.test.ts",
    "content": "import {\n  describe,\n  deps,\n  requirements,\n  Joi,\n  getMainCheckFn,\n  serverConfig,\n} from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport {\n  mockCloudCapiSubscription,\n  mockCloudCapiSubscriptionFixed,\n  mockCloudSubscription,\n  mockCloudSubscriptionFixed,\n} from 'src/__mocks__';\nimport { CustomErrorCodes } from 'src/constants';\n\nconst { request, server, constants } = deps;\n\nconst endpoint = () =>\n  request(server).get(`/cloud/autodiscovery/subscriptions`);\n\nconst headers = {\n  'x-cloud-api-key': constants.TEST_CLOUD_API_KEY,\n  'x-cloud-api-secret': constants.TEST_CLOUD_API_SECRET_KEY,\n};\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.number().required(),\n      name: Joi.string().required(),\n      numberOfDatabases: Joi.number().required(),\n      status: Joi.string().required(),\n      provider: Joi.string(),\n      region: Joi.string(),\n      type: Joi.string(),\n      price: Joi.number().integer(),\n      free: Joi.boolean(),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst nockScope = nock(serverConfig.get('cloud').capiUrl);\n\ndescribe('GET /cloud/autodiscovery/subscriptions', () => {\n  requirements('rte.serverType=local');\n\n  describe('Common', () => {\n    [\n      {\n        before: () => {\n          nockScope\n            .get('/fixed/subscriptions')\n            .reply(200, { subscriptions: [mockCloudCapiSubscriptionFixed] })\n            .get('/subscriptions')\n            .reply(200, { subscriptions: [mockCloudCapiSubscription] });\n        },\n        headers,\n        name: 'Should get subscriptions list',\n        responseSchema,\n        responseBody: [mockCloudSubscriptionFixed, mockCloudSubscription],\n      },\n      {\n        before: () => {\n          nockScope\n            .get('/fixed/subscriptions')\n            .reply(200, { subscriptions: [mockCloudCapiSubscription] })\n            .get('/subscriptions')\n            .reply(403, {\n              response: {\n                status: 403,\n                data: { message: 'Unauthorized for this action' },\n              },\n            });\n        },\n        headers,\n        name: 'Should throw Forbidden error when api returned 403 error',\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'CloudApiForbidden',\n          errorCode: CustomErrorCodes.CloudApiForbidden,\n        },\n      },\n      {\n        before: () => {\n          nockScope\n            .get('/fixed/subscriptions')\n            .reply(401, {\n              response: {\n                status: 401,\n                data: '',\n              },\n            })\n            .get('/subscriptions')\n            .reply(200, { subscriptions: [mockCloudCapiSubscription] });\n        },\n        name: 'Should throw Forbidden error when api returned 401',\n        headers,\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'CloudCapiUnauthorized',\n          errorCode: CustomErrorCodes.CloudCapiUnauthorized,\n        },\n      },\n      {\n        name: 'Should throw Unauthorized error when api key or secret was not provided',\n        headers: {},\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'Unauthorized',\n          message: 'Required authentication credentials were not provided',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/autodiscovery/GET-cloud-me-autodiscovery-account.test.ts",
    "content": "import {\n  describe,\n  deps,\n  requirements,\n  Joi,\n  nock,\n  getMainCheckFn,\n} from '../../deps';\nimport { mockCloudAccountInfo, mockCloudCapiAccount } from 'src/__mocks__';\nimport {\n  initApiCapiKeysEnsureNockScope,\n  initSMCapiNockScope,\n} from '../constants';\nconst { request, server } = deps;\n\nconst endpoint = () => request(server).get(`/cloud/me/autodiscovery/account`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    accountId: Joi.number().required(),\n    accountName: Joi.string().required(),\n    ownerName: Joi.string().required(),\n    ownerEmail: Joi.string().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /cloud/me/autodiscovery/account', () => {\n  requirements('rte.serverType=local');\n\n  beforeEach(async () => {\n    nock.cleanAll();\n    initApiCapiKeysEnsureNockScope();\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should get account info',\n        before: () => {\n          initSMCapiNockScope()\n            .get('/')\n            .reply(200, { account: mockCloudCapiAccount });\n        },\n        responseSchema,\n        responseBody: mockCloudAccountInfo,\n      },\n      // {\n      //   before: () => {\n      //     nockScope.get('/')\n      //       .reply(403, {\n      //         response: {\n      //           status: 403,\n      //           data: { message: 'Unauthorized for this action' },\n      //         }\n      //       });\n      //   },\n      //   name: 'Should throw Forbidden error when api returned 403 error',\n      //   statusCode: 403,\n      //   responseBody: {\n      //     statusCode: 403,\n      //     error: 'CloudApiForbidden',\n      //     errorCode: CustomErrorCodes.CloudApiForbidden,\n      //   },\n      // },\n      // {\n      //   before: () => {\n      //     nockScope.get('/')\n      //       .reply(401, {\n      //         response: {\n      //           status: 401,\n      //           data: '',\n      //         }\n      //       });\n      //   },\n      //   name: 'Should throw Unauthorized error when api returns 401 error',\n      //   statusCode: 401,\n      //   responseBody: {\n      //     statusCode: 401,\n      //     error: 'CloudApiUnauthorized',\n      //     errorCode: CustomErrorCodes.CloudApiUnauthorized,\n      //   },\n      // },\n      // {\n      //   name: 'Should throw Unauthorized error when api key or secret was not provided',\n      //   headers: {},\n      //   statusCode: 401,\n      //   responseBody: {\n      //     statusCode: 401,\n      //     error: 'Unauthorized',\n      //     message: 'Required authentication credentials were not provided',\n      //   },\n      // },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/autodiscovery/POST-cloud-autodiscovery-databases.test.ts",
    "content": "import {\n  describe,\n  deps,\n  expect,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  Joi,\n  getMainCheckFn,\n  serverConfig,\n} from '../../deps';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\n\nimport { nock } from '../../../helpers/test';\nimport {\n  mockCloudCapiDatabase,\n  mockCloudCapiDatabaseTags,\n  mockCloudCapiDatabaseFixed,\n  mockCloudDatabase,\n  mockCloudDatabaseFixed,\n  mockImportCloudDatabaseDto,\n  mockImportCloudDatabaseDtoFixed,\n} from 'src/__mocks__';\nimport { CustomErrorCodes } from 'src/constants';\n\nconst { request, server, constants } = deps;\n\nconst endpoint = () => request(server).post(`/cloud/autodiscovery/databases`);\n\nconst dataSchema = Joi.object({\n  databases: Joi.array()\n    .items({\n      databaseId: Joi.number().allow(true).required().label('.databaseId'),\n      subscriptionId: Joi.number()\n        .allow(true)\n        .required()\n        .label('.subscriptionId'),\n      subscriptionType: Joi.string()\n        .valid('fixed', 'flexible')\n        .required()\n        .label('subscriptionType'),\n    })\n    .required()\n    .messages({\n      'any.required': '{#label} should not be empty',\n      'array.sparse': '{#label} must be either object or array',\n      'array.base': 'property {#label} must be either object or array',\n    }),\n}).strict();\n\nconst validInputData = {\n  databases: [\n    {\n      databaseId: 1,\n      subscriptionId: constants.TEST_CLOUD_SUBSCRIPTION_ID,\n      subscriptionType: 'fixed',\n    },\n  ],\n};\n\nconst headers = {\n  'x-cloud-api-key': constants.TEST_CLOUD_API_KEY,\n  'x-cloud-api-secret': constants.TEST_CLOUD_API_SECRET_KEY,\n};\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      subscriptionId: Joi.number().required(),\n      subscriptionType: Joi.string().valid('fixed', 'flexible').required(),\n      databaseId: Joi.number().required(),\n      free: Joi.boolean(),\n      status: Joi.string().valid('fail', 'success').required(),\n      message: Joi.string().required(),\n      databaseDetails: Joi.object().required(),\n      tags: Joi.array()\n        .items(\n          Joi.object().keys({\n            key: Joi.string().required(),\n            value: Joi.string().required(),\n            createdAt: Joi.string().isoDate(),\n            updatedAt: Joi.string().isoDate(),\n            links: Joi.array().items(Joi.string().required()),\n          }),\n        )\n        .allow(null),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst nockScope = nock(serverConfig.get('cloud').capiUrl);\n\ndescribe('POST /cloud/autodiscovery/databases', () => {\n  requirements('rte.serverType=local');\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData, 'data', {\n      headers,\n    }).map(validateInvalidDataTestCase(endpoint, dataSchema));\n  });\n\n  describe('Common mocked to localhost', () => {\n    requirements('rte.type=STANDALONE', '!rte.pass', '!rte.tls');\n    [\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockImportCloudDatabaseDto.subscriptionId}/databases/${mockImportCloudDatabaseDto.databaseId}`,\n            )\n            .reply(200, {\n              ...mockCloudCapiDatabase,\n              publicEndpoint: `${constants.TEST_REDIS_HOST}:${constants.TEST_REDIS_PORT}`,\n            })\n            .get(\n              `/fixed/subscriptions/${mockImportCloudDatabaseDtoFixed.subscriptionId}/databases/${mockImportCloudDatabaseDtoFixed.databaseId}`,\n            )\n            .reply(200, {\n              ...mockCloudCapiDatabaseFixed,\n              publicEndpoint: `${constants.TEST_REDIS_HOST}:${constants.TEST_REDIS_PORT}`,\n            })\n            .get(\n              `/subscriptions/${mockImportCloudDatabaseDtoFixed.subscriptionId}/databases/${mockImportCloudDatabaseDtoFixed.databaseId}/tags`,\n            )\n            .reply(200, {\n              tags: mockCloudCapiDatabaseTags,\n            });\n        },\n        name: 'Should add 2 databases',\n        data: {\n          databases: [\n            mockImportCloudDatabaseDto,\n            mockImportCloudDatabaseDtoFixed,\n          ],\n        },\n        headers,\n        responseSchema,\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          expect(body).to.deepEqualIgnoreUndefined([\n            {\n              ...mockImportCloudDatabaseDto,\n              message: 'Added',\n              status: 'success',\n              databaseDetails: {\n                ...mockCloudDatabase,\n                publicEndpoint: `${constants.TEST_REDIS_HOST}:${constants.TEST_REDIS_PORT}`,\n                tags: mockCloudCapiDatabaseTags,\n              },\n            },\n            {\n              ...mockImportCloudDatabaseDtoFixed,\n              message: 'Added',\n              status: 'success',\n              databaseDetails: {\n                ...mockCloudDatabaseFixed,\n                publicEndpoint: `${constants.TEST_REDIS_HOST}:${constants.TEST_REDIS_PORT}`,\n              },\n            },\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Common fails', async () => {\n    [\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/fixed/subscriptions/${mockImportCloudDatabaseDtoFixed.subscriptionId}/databases/${mockImportCloudDatabaseDtoFixed.databaseId}`,\n            )\n            .reply(403, { message: 'Unauthorized for this action' });\n        },\n        name: 'Should throw Forbidden error when api returns 403',\n        headers,\n        data: {\n          databases: [mockImportCloudDatabaseDtoFixed],\n        },\n        responseBody: [\n          {\n            ...mockImportCloudDatabaseDtoFixed,\n            status: 'fail',\n            message: 'Request failed with status code 403',\n            error: {\n              statusCode: 403,\n              error: 'CloudApiForbidden',\n              message: 'Request failed with status code 403',\n              errorCode: CustomErrorCodes.CloudApiForbidden,\n            },\n          },\n        ],\n      },\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockImportCloudDatabaseDto.subscriptionId}/databases/${mockImportCloudDatabaseDto.databaseId}`,\n            )\n            .reply(401, { message: ERROR_MESSAGES.UNAUTHORIZED });\n        },\n        name: 'Should throw Forbidden error when api returns 401',\n        headers,\n        data: {\n          databases: [mockImportCloudDatabaseDto],\n        },\n        responseBody: [\n          {\n            ...mockImportCloudDatabaseDto,\n            status: 'fail',\n            message: 'Request failed with status code 401',\n            error: {\n              statusCode: 401,\n              error: 'CloudCapiUnauthorized',\n              errorCode: CustomErrorCodes.CloudCapiUnauthorized,\n              message: 'Request failed with status code 401',\n            },\n          },\n        ],\n      },\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockImportCloudDatabaseDto.subscriptionId}/databases/${mockImportCloudDatabaseDto.databaseId}`,\n            )\n            .reply(404, { message: ERROR_MESSAGES.NOT_FOUND });\n        },\n        name: 'Should throw Not Found error when subscription id is not found',\n        headers,\n        data: {\n          databases: [mockImportCloudDatabaseDto],\n        },\n        responseBody: [\n          {\n            ...mockImportCloudDatabaseDto,\n            status: 'fail',\n            message: 'Request failed with status code 404',\n            error: {\n              statusCode: 404,\n              error: 'CloudApiNotFound',\n              message: 'Request failed with status code 404',\n              errorCode: CustomErrorCodes.CloudApiNotFound,\n            },\n          },\n        ],\n      },\n      {\n        name: 'Should throw Unauthorized error when api key or secret was not provided',\n        headers: {},\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'Unauthorized',\n          message: 'Required authentication credentials were not provided',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/autodiscovery/POST-cloud-autodiscovery-get_databases.test.ts",
    "content": "import {\n  describe,\n  deps,\n  expect,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  Joi,\n  getMainCheckFn,\n  serverConfig,\n} from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport {\n  mockCloudCapiSubscriptionDatabases,\n  mockCloudCapiSubscriptionDatabasesFixed,\n  mockCloudDatabaseFromList,\n  mockCloudDatabaseFromListFixed,\n  mockGetCloudSubscriptionDatabasesDto,\n  mockGetCloudSubscriptionDatabasesDtoFixed,\n} from 'src/__mocks__';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\n\nconst { request, server, constants } = deps;\n\nconst endpoint = () =>\n  request(server).post(`/cloud/autodiscovery/get-databases`);\n\nconst dataSchema = Joi.object({\n  subscriptions: Joi.array()\n    .items({\n      subscriptionId: Joi.number()\n        .allow(true)\n        .required()\n        .label('.subscriptionId'), // todo: review transform rules\n      subscriptionType: Joi.string()\n        .valid('fixed', 'flexible')\n        .required()\n        .label('subscriptionType'),\n    })\n    .required()\n    .messages({\n      'any.required': '{#label} should not be empty',\n      'array.sparse': '{#label} must be either object or array',\n      'array.base': 'property {#label} must be either object or array',\n    }),\n}).strict();\n\nconst validInputData = {\n  subscriptions: [\n    {\n      subscriptionId: constants.TEST_CLOUD_SUBSCRIPTION_ID,\n      subscriptionType: 'fixed',\n    },\n  ],\n};\n\nconst headers = {\n  'x-cloud-api-key': constants.TEST_CLOUD_API_KEY,\n  'x-cloud-api-secret': constants.TEST_CLOUD_API_SECRET_KEY,\n};\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      subscriptionId: Joi.number().required(),\n      subscriptionType: Joi.string().valid('fixed', 'flexible').required(),\n      databaseId: Joi.number().required(),\n      name: Joi.string().required(),\n      publicEndpoint: Joi.string().required(),\n      status: Joi.string().required(),\n      sslClientAuthentication: Joi.boolean().required(),\n      modules: Joi.array().required(),\n      options: Joi.object().required(),\n      tags: Joi.array()\n        .items(\n          Joi.object().keys({\n            key: Joi.string().required(),\n            value: Joi.string().required(),\n          }),\n        )\n        .allow(null),\n      cloudDetails: Joi.object()\n        .keys({\n          cloudId: Joi.number().required(),\n          subscriptionType: Joi.string().valid('fixed', 'flexible').required(),\n          planMemoryLimit: Joi.number(),\n          memoryLimitMeasurementUnit: Joi.string(),\n          subscriptionId: Joi.number().integer(),\n          free: Joi.boolean().required(),\n        })\n        .required(),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst nockScope = nock(serverConfig.get('cloud').capiUrl);\n\ndescribe('POST /cloud/autodiscovery/get-databases', () => {\n  requirements('rte.serverType=local');\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData, 'data', {\n      headers,\n    }).map(validateInvalidDataTestCase(endpoint, dataSchema));\n  });\n\n  describe('Common', async () => {\n    [\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockGetCloudSubscriptionDatabasesDto.subscriptionId}/databases`,\n            )\n            .reply(200, mockCloudCapiSubscriptionDatabases);\n        },\n        name: 'Should get databases list inside subscription',\n        data: {\n          subscriptions: [mockGetCloudSubscriptionDatabasesDto],\n        },\n        headers,\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body).to.deepEqualIgnoreUndefined([mockCloudDatabaseFromList]);\n        },\n      },\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/fixed/subscriptions/${mockGetCloudSubscriptionDatabasesDtoFixed.subscriptionId}/databases`,\n            )\n            .reply(200, mockCloudCapiSubscriptionDatabasesFixed);\n        },\n        name: 'Should get databases list inside fixed subscription',\n        data: {\n          subscriptions: [mockGetCloudSubscriptionDatabasesDtoFixed],\n        },\n        headers,\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body).to.deepEqualIgnoreUndefined([\n            mockCloudDatabaseFromListFixed,\n          ]);\n        },\n      },\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockGetCloudSubscriptionDatabasesDto.subscriptionId}/databases`,\n            )\n            .reply(403, {\n              message: 'Unauthorized for this action',\n            });\n        },\n        name: 'Should throw Forbidden error when api returns 403',\n        headers,\n        data: {\n          subscriptions: [mockGetCloudSubscriptionDatabasesDto],\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'CloudApiForbidden',\n          message: 'Request failed with status code 403',\n          errorCode: CustomErrorCodes.CloudApiForbidden,\n        },\n      },\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockGetCloudSubscriptionDatabasesDto.subscriptionId}/databases`,\n            )\n            .reply(401, {\n              message: ERROR_MESSAGES.UNAUTHORIZED,\n            });\n        },\n        name: 'Should throw Forbidden error when api returns 401',\n        headers,\n        data: {\n          subscriptions: [mockGetCloudSubscriptionDatabasesDto],\n        },\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'CloudCapiUnauthorized',\n          errorCode: CustomErrorCodes.CloudCapiUnauthorized,\n          message: 'Request failed with status code 401',\n        },\n      },\n      {\n        before: () => {\n          nockScope\n            .get(\n              `/subscriptions/${mockGetCloudSubscriptionDatabasesDto.subscriptionId}/databases`,\n            )\n            .reply(404, {\n              message: ERROR_MESSAGES.NOT_FOUND,\n              data: 'Subscription is not found',\n            });\n        },\n        name: 'Should throw Not Found error when subscription id is not found',\n        headers,\n        data: {\n          subscriptions: [mockGetCloudSubscriptionDatabasesDto],\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'CloudApiNotFound',\n          message: 'Request failed with status code 404',\n          errorCode: CustomErrorCodes.CloudApiNotFound,\n        },\n      },\n      {\n        name: 'Should throw Unauthorized error when api key or secret was not provided',\n        headers: {},\n        statusCode: 401,\n        responseBody: {\n          statusCode: 401,\n          error: 'Unauthorized',\n          message: 'Required authentication credentials were not provided',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/constants.ts",
    "content": "import { nock, serverConfig } from '../../helpers/test';\nimport {\n  mockCloudApiAccount,\n  mockCloudApiCapiKey,\n  mockCloudApiCsrfToken,\n  mockCloudApiUser,\n} from 'src/__mocks__';\n\nexport const initSMCapiNockScope = () => {\n  return nock(serverConfig.get('cloud').capiUrl);\n};\n\nexport const initSMApiNockScope = () => {\n  return nock(serverConfig.get('cloud').apiUrl);\n};\n\nexport const initApiLoginNockScope = (\n  apiNockScope = initSMApiNockScope(),\n  persist = true,\n) => {\n  return apiNockScope\n    .persist(persist)\n    .post('/login')\n    .query(true)\n    .reply(200, {}, { 'set-cookie': 'JSESSIONID=jsessionid' })\n    .get('/csrf')\n    .reply(200, { csrfToken: mockCloudApiCsrfToken });\n};\n\nexport const initApiUserProfileNockScope = (\n  apiNockScope = initSMApiNockScope(),\n  persist = true,\n) => {\n  return initApiLoginNockScope(apiNockScope, persist)\n    .get('/users/me')\n    .reply(200, mockCloudApiUser)\n    .get('/accounts')\n    .reply(200, { accounts: [mockCloudApiAccount] });\n};\n\nexport const initApiCapiKeysEnsureNockScope = (\n  apiNockScope = initSMApiNockScope(),\n) => {\n  return initApiUserProfileNockScope(apiNockScope, true)\n    .get('/accounts/cloud-api/cloudApiKeys')\n    .reply(200, { cloudApiKeys: [] })\n    .post('/accounts/cloud-api/cloudApiKeys')\n    .reply(200, { cloudApiKey: mockCloudApiCapiKey });\n};\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/user/GET-cloud-me.test.ts",
    "content": "import {\n  describe,\n  deps,\n  requirements,\n  Joi,\n  getMainCheckFn,\n  expect,\n} from './../../deps';\nimport { mockCloudUserSafe } from 'src/__mocks__';\nimport { initApiUserProfileNockScope } from '../constants';\n\nconst { request, server } = deps;\n\nconst endpoint = () => request(server).get(`/cloud/me`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.number().required(),\n    name: Joi.string().required(),\n    currentAccountId: Joi.number().required(),\n    accounts: Joi.array()\n      .items(\n        Joi.object().keys({\n          id: Joi.number().required(),\n          name: Joi.string().required(),\n        }),\n      )\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ninitApiUserProfileNockScope();\n\ndescribe('GET /cloud/me', () => {\n  requirements('rte.serverType=local');\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should get user profile',\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body).to.deepEqualIgnoreUndefined(mockCloudUserSafe);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cloud/user/PUT-cloud-me-accounts-id-current.test.ts",
    "content": "import {\n  mockCapiUnauthorizedError,\n  mockCloudApiBadRequestExceptionResponse,\n  mockCloudApiUnauthorizedExceptionResponse,\n  mockCloudUserAccount,\n  mockCloudUserSafe,\n  mockSmApiBadRequestError,\n} from 'src/__mocks__';\nimport {\n  describe,\n  deps,\n  requirements,\n  Joi,\n  getMainCheckFn,\n  expect,\n  nock,\n} from '../../deps';\nimport { initApiUserProfileNockScope, initSMApiNockScope } from '../constants';\n\nconst { request, server } = deps;\n\nconst endpoint = (account = mockCloudUserAccount.id) =>\n  request(server).put(`/cloud/me/accounts/${account}/current`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.number().required(),\n    name: Joi.string().required(),\n    currentAccountId: Joi.number().required(),\n    accounts: Joi.array()\n      .items(\n        Joi.object().keys({\n          id: Joi.number().required(),\n          name: Joi.string().required(),\n        }),\n      )\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PUT /cloud/me/accounts/:id/current', () => {\n  requirements('rte.serverType=local');\n\n  beforeEach(async () => {\n    nock.cleanAll();\n    initApiUserProfileNockScope();\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should switch account',\n        before: () => {\n          initSMApiNockScope()\n            .post(`/accounts/setcurrent/${mockCloudUserAccount.id}`)\n            .reply(200, {});\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body).to.deepEqualIgnoreUndefined(mockCloudUserSafe);\n        },\n      },\n      {\n        name: 'Should switch account from 2nd attempt',\n        before: () => {\n          initSMApiNockScope()\n            .post(`/accounts/setcurrent/${mockCloudUserAccount.id}`)\n            .reply(401, mockCapiUnauthorizedError)\n            .post(`/accounts/setcurrent/${mockCloudUserAccount.id}`)\n            .reply(200, {});\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body).to.deepEqualIgnoreUndefined(mockCloudUserSafe);\n        },\n      },\n      {\n        name: 'Should throw 401 error',\n        before: () => {\n          initSMApiNockScope()\n            .post(`/accounts/setcurrent/${mockCloudUserAccount.id}`)\n            .reply(401, { message: mockCapiUnauthorizedError.message })\n            .post(`/accounts/setcurrent/${mockCloudUserAccount.id}`)\n            .reply(401, { message: mockCapiUnauthorizedError.message });\n        },\n        statusCode: 401,\n        checkFn: ({ body }) => {\n          expect(body).to.deep.eq(mockCloudApiUnauthorizedExceptionResponse);\n        },\n      },\n      {\n        name: 'Should throw 400 error without retry',\n        before: () => {\n          initSMApiNockScope()\n            .post(`/accounts/setcurrent/${mockCloudUserAccount.id}`)\n            .reply(400, { message: mockSmApiBadRequestError.message });\n        },\n        statusCode: 400,\n        checkFn: ({ body }) => {\n          expect(body).to.deep.eq(mockCloudApiBadRequestExceptionResponse);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/cluster-monitor/GET-databases-id-cluster_details.test.ts",
    "content": "import {\n  describe,\n  deps,\n  before,\n  expect,\n  requirements,\n  getMainCheckFn,\n} from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { localDb, request, server, constants, rte } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/cluster-details`,\n  );\n\nconst nodeSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    version: Joi.string().required(),\n    mode: Joi.string().required(),\n    host: Joi.string().required(),\n    port: Joi.number(),\n    role: Joi.string().required(),\n    slots: Joi.array().items(Joi.string()).required(),\n    health: Joi.string().required(),\n    totalKeys: Joi.number().allow(null),\n    usedMemory: Joi.number().allow(null),\n    opsPerSecond: Joi.number().allow(null),\n    connectionsReceived: Joi.number().allow(null),\n    connectedClients: Joi.number().allow(null),\n    commandsProcessed: Joi.number().allow(null),\n    networkInKbps: Joi.number().allow(null),\n    networkOutKbps: Joi.number().allow(null),\n    cacheHitRatio: Joi.number().allow(null),\n    replicationOffset: Joi.number().allow(null),\n    uptimeSec: Joi.number().allow(null),\n    replicas: Joi.array().items(this),\n  })\n  .required();\n\nconst responseSchema = Joi.object()\n  .keys({\n    user: Joi.string(),\n    version: Joi.string().required(),\n    mode: Joi.string().required(),\n    state: Joi.string().required(),\n    slotsAssigned: Joi.number().allow(null),\n    slotsOk: Joi.number().allow(null),\n    slotsPFail: Joi.number().allow(null),\n    slotsFail: Joi.number().allow(null),\n    slotsUnassigned: Joi.number().allow(null),\n    statsMessagesSent: Joi.number().allow(null),\n    statsMessagesReceived: Joi.number().allow(null),\n    currentEpoch: Joi.number().allow(null),\n    myEpoch: Joi.number().allow(null),\n    size: Joi.number().allow(null),\n    knownNodes: Joi.number().allow(null),\n    uptimeSec: Joi.number().allow(null),\n    nodes: Joi.array().items(nodeSchema).min(0).required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:id/cluster-details', () => {\n  before(localDb.createDatabaseInstances);\n\n  describe('Common', () => {\n    [\n      {\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_4),\n        name: 'Should not connect to a database due to misconfiguration',\n        statusCode: 424,\n        responseBody: {\n          statusCode: 424,\n          error: 'RedisConnectionUnavailableException',\n          errorCode: 10904,\n        },\n      },\n      {\n        name: 'Should return NotFound error if instance id does not exists',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Invalid database instance id.',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Any non-cluster', () => {\n    requirements('rte.type<>CLUSTER');\n    [\n      {\n        name: 'Should return BadRequest for non-cluster databases',\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n          message: 'Current database is not in a cluster mode',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Cluster', () => {\n    requirements('rte.type=CLUSTER');\n    [\n      {\n        name: 'Should get cluster details',\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.version).to.eql(rte.env.version);\n        },\n      },\n    ].map(mainCheckFn);\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should return details in positive case',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.version).to.eql(rte.env.version);\n          },\n        },\n        {\n          before: () => rte.data.setAclUserRules('~* +@all -cluster'),\n          name: 'Should throw error if no permissions for \"cluster\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n        },\n        {\n          before: () => rte.data.setAclUserRules('~* +@all -info'),\n          name: 'Should not throw error if no permissions for \"info\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.state).to.eql('ok');\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/commands/GET-commands.test.ts",
    "content": "import { expect, describe, it, deps, Joi, validateApiCall } from '../deps';\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/commands');\n\nconst responseSchema = Joi.object().required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /commands', () => {\n  [\n    {\n      name: 'Should return merged config',\n      statusCode: 200,\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body['GET']).to.be.an('object');\n        expect(body['FT.CREATE']).to.be.an('object');\n        expect(body['JSON.GET']).to.be.an('object');\n        expect(body['RG.PYEXECUTE']).to.be.an('object');\n        expect(body['BF.RESERVE']).to.be.an('object');\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  deps,\n  validateApiCall,\n  AdmZip,\n  fsExtra,\n  path,\n  serverConfig,\n  requirements,\n  before,\n  nock,\n  _,\n} from '../deps';\nconst { server, request, localDb } = deps;\n\n// create endpoint\nconst creatEndpoint = () => request(server).post(`/custom-tutorials`);\nconst manifestEndpoint = () =>\n  request(server).get(`/custom-tutorials/manifest`);\nconst deleteEndpoint = (id: string) => () =>\n  request(server).delete(`/custom-tutorials/${id}`);\n\nconst customTutorialsFolder = serverConfig.get('dir_path').customTutorials;\nconst staticsFolder = serverConfig.get('dir_path').staticDir;\n\nconst getZipArchive = () => {\n  const zipArchive = new AdmZip();\n\n  zipArchive.addFile('info.md', Buffer.from('# info.md', 'utf8'));\n  zipArchive.addFile('info.json', Buffer.from('# info.json', 'utf8'));\n  zipArchive.addFile('info.tar', Buffer.from('# info.tar', 'utf8'));\n  zipArchive.addFile('_info.tar', Buffer.from('# info.tar', 'utf8'));\n  zipArchive.addFile('folder/file.md', Buffer.from('# folder/file.md', 'utf8'));\n  zipArchive.addFile(\n    '.folder/file.md',\n    Buffer.from('# .folder/file.md', 'utf8'),\n  );\n  zipArchive.addFile(\n    '.folder/file2.md',\n    Buffer.from('# .folder/file2.md', 'utf8'),\n  );\n  zipArchive.addFile(\n    '_folder/file.md',\n    Buffer.from('# _folder/file.md', 'utf8'),\n  );\n  zipArchive.addFile(\n    '__MACOSX/file.md',\n    Buffer.from('# __MACOSX/file.md', 'utf8'),\n  );\n\n  return zipArchive;\n};\n\nconst checkFilesUnarchivedFiles = (zip: AdmZip, tutorialFolder = '/') => {\n  zip.getEntries().forEach((entry) => {\n    expect(\n      fsExtra.existsSync(\n        path.join(customTutorialsFolder, tutorialFolder, entry.entryName),\n      ),\n    ).eq(!entry.entryName.startsWith('__MACOSX'));\n  });\n};\n\nconst autoGeneratedManifest = {\n  children: [\n    {\n      id: 'folder',\n      type: 'group',\n      label: 'folder',\n      children: [\n        {\n          id: 'file.md',\n          type: 'internal-link',\n          label: 'file',\n          args: { path: '/folder/file.md' },\n        },\n      ],\n    },\n    {\n      id: 'info.md',\n      type: 'internal-link',\n      label: 'info',\n      args: { path: '/info.md' },\n    },\n  ],\n};\n\nconst testManifest = {\n  id: 'id',\n  type: 'group',\n  label: 'my tutorial',\n  children: [\n    {\n      id: 'main-page',\n      type: 'internal-link',\n      label: 'INFO',\n      args: { path: '/info.md' },\n    },\n    {\n      id: 'some-file',\n      type: 'internal-link',\n      label: 'FILE',\n      args: { path: '/folder/file.md' },\n    },\n  ],\n};\n\nconst globalManifest = {\n  id: 'custom-tutorials',\n  label: 'My tutorials',\n  type: 'group',\n  _actions: ['create'],\n  args: {\n    initialIsOpen: false,\n    withBorder: true,\n  },\n  children: [],\n};\n\nconst nockScope = nock('https://github.com/somerepo');\n\ndescribe('POST /custom-tutorials', () => {\n  requirements('rte.serverType=local');\n\n  before(async () => {\n    await fsExtra.remove(customTutorialsFolder);\n    await (\n      await localDb.getRepository(localDb.repositories.CUSTOM_TUTORIAL)\n    ).clear();\n  });\n\n  describe('Common', () => {\n    it('should import tutorial from file and generate _manifest.json', async () => {\n      const zip = getZipArchive();\n      zip.writeZip(path.join(staticsFolder, 'test_no_manifest.zip'));\n\n      // create tutorial\n      await validateApiCall({\n        endpoint: creatEndpoint,\n        attach: ['file', zip.toBuffer(), 'a.zip'],\n        statusCode: 201,\n        checkFn: async ({ body }) => {\n          const tutorialRootManifest = {\n            ...autoGeneratedManifest,\n            type: 'group',\n            id: body.id,\n            label: 'a',\n            _actions: ['delete'],\n            _path: `/${body.id}`,\n          };\n\n          globalManifest.children = [tutorialRootManifest].concat(\n            globalManifest.children,\n          );\n\n          expect(body).deep.eq(tutorialRootManifest);\n          checkFilesUnarchivedFiles(zip, body?._path);\n          expect(\n            JSON.parse(\n              await fsExtra.readFile(\n                path.join(customTutorialsFolder, body._path, '_manifest.json'),\n                'utf8',\n              ),\n            ),\n          ).deep.eq(_.omit(body, ['_actions', '_path', 'id', 'label', 'type']));\n          expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(1);\n        },\n      });\n\n      // global manifest\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body).deep.eq(globalManifest);\n        },\n      });\n    });\n\n    it('should import tutorial from file with manifest', async () => {\n      const zip = getZipArchive();\n      zip.addFile(\n        'manifest.json',\n        Buffer.from(JSON.stringify(testManifest), 'utf8'),\n      );\n      zip.writeZip(path.join(staticsFolder, 'test.zip'));\n\n      await validateApiCall({\n        endpoint: creatEndpoint,\n        attach: ['file', zip.toBuffer(), 'a.zip'],\n        statusCode: 201,\n        checkFn: async ({ body }) => {\n          const tutorialRootManifest = {\n            ...testManifest,\n            type: 'group',\n            id: body.id,\n            _actions: ['delete'],\n            _path: `/${body.id}`,\n          };\n\n          globalManifest.children = [tutorialRootManifest].concat(\n            globalManifest.children,\n          );\n\n          expect(body).deep.eq(tutorialRootManifest);\n          checkFilesUnarchivedFiles(zip, body?._path);\n          expect({\n            ...JSON.parse(\n              await fsExtra.readFile(\n                path.join(customTutorialsFolder, body._path, 'manifest.json'),\n                'utf8',\n              ),\n            ),\n            id: body.id,\n          }).deep.eq({\n            ..._.omit(body, ['_actions', '_path']),\n          });\n          expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(2);\n        },\n      });\n\n      // global manifest\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body).deep.eq(globalManifest);\n        },\n      });\n    });\n\n    it('should import tutorial from the external link with manifest', async () => {\n      const zip = new AdmZip(path.join(staticsFolder, 'test.zip'));\n      const file = await fsExtra.readFile(path.join(staticsFolder, 'test.zip'));\n      nockScope.get('/test.zip').reply(200, file);\n      const link = `https://github.com/somerepo/test.zip`;\n\n      await validateApiCall({\n        endpoint: creatEndpoint,\n        fields: [['link', link]],\n        statusCode: 201,\n        checkFn: async ({ body }) => {\n          const tutorialRootManifest = {\n            ...testManifest,\n            type: 'group',\n            id: body.id,\n            _actions: ['delete', 'sync'],\n            _path: `/${body.id}`,\n          };\n\n          globalManifest.children = [tutorialRootManifest].concat(\n            globalManifest.children,\n          );\n\n          expect(body).deep.eq(tutorialRootManifest);\n          checkFilesUnarchivedFiles(zip, body?._path);\n          expect({\n            ...JSON.parse(\n              await fsExtra.readFile(\n                path.join(customTutorialsFolder, body._path, 'manifest.json'),\n                'utf8',\n              ),\n            ),\n            id: body.id,\n          }).deep.eq({\n            ..._.omit(body, ['_actions', '_path']),\n          });\n          expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(3);\n        },\n      });\n\n      // global manifest\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body).deep.eq(globalManifest);\n        },\n      });\n    });\n\n    it('should delete tutorial', async () => {\n      expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(3);\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body.children.length).eq(3);\n        },\n      });\n\n      const toDelete = globalManifest.children.shift();\n      await validateApiCall({\n        endpoint: deleteEndpoint(toDelete.id),\n      });\n\n      expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(2);\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body.children.length).eq(2);\n          expect(body).deep.eq(globalManifest);\n        },\n      });\n    });\n\n    it('should delete tutorial and not fail even if folder does not exist', async () => {\n      expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(2);\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body.children.length).eq(2);\n        },\n      });\n\n      const toDelete = globalManifest.children.shift();\n\n      await fsExtra.remove(path.join(customTutorialsFolder, toDelete.id));\n      expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(1);\n\n      await validateApiCall({\n        endpoint: deleteEndpoint(toDelete.id),\n      });\n\n      expect((await fsExtra.readdir(customTutorialsFolder)).length).eq(1);\n      await validateApiCall({\n        endpoint: manifestEndpoint,\n        checkFn: async ({ body }) => {\n          expect(body.children.length).eq(1);\n          expect(body).deep.eq(globalManifest);\n        },\n      });\n    });\n\n    it('should fail when trying to delete not existing tutorial', async () => {\n      await validateApiCall({\n        endpoint: deleteEndpoint('not existing'),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Custom Tutorial was not found.',\n          error: 'Not Found',\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/DELETE-databases-id.test.ts",
    "content": "import { expect, describe, before, deps, getMainCheckFn } from '../deps';\n\nconst { request, server, localDb, constants } = deps;\n\nconst endpoint = (id) =>\n  request(server).delete(`/${constants.API.DATABASES}/${id}`);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`DELETE /databases/:id`, () => {\n  before(async () => await localDb.createDatabaseInstances());\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should remove single database',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n        before: async () => {\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2),\n          ).to.be.an('object');\n        },\n        after: async () => {\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2),\n          ).to.eql(null);\n        },\n      },\n      {\n        name: 'Should return Not Found Error',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n        before: async () => {\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2),\n          ).to.eql(null);\n        },\n      },\n      {\n        name: 'Should remove unused tags along with database',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_7),\n        before: async () => {\n          await localDb.createInstancesWithTags();\n\n          const instance = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_7,\n          );\n          const tags = await localDb.getAllTags();\n\n          expect(instance).to.be.an('object');\n          expect(tags.length).to.eq(constants.TEST_TAGS.length);\n        },\n        after: async () => {\n          const instance = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_7,\n          );\n          const tags = await localDb.getAllTags();\n\n          expect(instance).to.eql(null);\n          expect(tags.length).to.eq(constants.TEST_TAGS.length - 1);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/DELETE-databases.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  before,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\n\nconst { request, server, localDb, constants } = deps;\n\nconst endpoint = () => request(server).delete(`/${constants.API.DATABASES}`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  ids: Joi.array().items(Joi.any()).required(),\n}).strict();\n\nconst validInputData = {\n  ids: [constants.getRandomString()],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`DELETE /databases`, () => {\n  before(async () => await localDb.createDatabaseInstances());\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should remove multiple databases by ids',\n        data: {\n          ids: [constants.TEST_INSTANCE_ID_2, constants.TEST_INSTANCE_ID_3],\n        },\n        responseBody: {\n          affected: 2,\n        },\n        before: async () => {\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2),\n          ).to.be.an('object');\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3),\n          ).to.be.an('object');\n        },\n        after: async () => {\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2),\n          ).to.eql(null);\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3),\n          ).to.eql(null);\n        },\n      },\n      {\n        name: 'Should return affected 0 since no databases found',\n        data: {\n          ids: [constants.TEST_INSTANCE_ID_2, constants.TEST_INSTANCE_ID_3],\n        },\n        responseBody: {\n          affected: 0,\n        },\n        before: async () => {\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2),\n          ).to.eql(null);\n          expect(\n            await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3),\n          ).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/GET-databases-id-connect.test.ts",
    "content": "import { describe, deps, before, getMainCheckFn } from '../deps';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${id}/connect`);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /databases/:id/connect`, () => {\n  before(async () => await localDb.createDatabaseInstances());\n\n  [\n    {\n      name: 'Should connect to a database',\n      statusCode: 200,\n    },\n    {\n      endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n      name: 'Should not connect to a database due to misconfiguration',\n      statusCode: 424,\n      responseBody: {\n        statusCode: 424,\n        error: 'RedisConnectionUnavailableException',\n        errorCode: 10904,\n      },\n    },\n    {\n      name: 'Should return NotFound error if instance id does not exists',\n      endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n      statusCode: 404,\n      responseBody: {\n        statusCode: 404,\n        error: 'Not Found',\n        message: 'Invalid database instance id.',\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/GET-databases-id-info.test.ts",
    "content": "import {\n  describe,\n  deps,\n  before,\n  expect,\n  getMainCheckFn,\n  requirements,\n} from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { localDb, request, server, constants, rte } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${id}/info`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    version: Joi.string().required(),\n    databases: Joi.number().integer(),\n    role: Joi.string(),\n    totalKeys: Joi.number().integer().required(),\n    usedMemory: Joi.number().integer().required(),\n    connectedClients: Joi.number().integer(),\n    uptimeInSeconds: Joi.number().integer(),\n    hitRatio: Joi.number(),\n    cashedScripts: Joi.number(),\n    server: Joi.object(),\n    nodes: Joi.array().items(\n      Joi.object().keys({\n        version: Joi.string().required(),\n        databases: Joi.number().integer().required(),\n        role: Joi.string().required(),\n        totalKeys: Joi.number().integer().required(),\n        usedMemory: Joi.number().integer().required(),\n        connectedClients: Joi.number().integer().required(),\n        uptimeInSeconds: Joi.number().integer().required(),\n        hitRatio: Joi.number().required(),\n        cashedScripts: Joi.number(),\n        server: Joi.object().required(),\n        stats: Joi.object().keys({\n          instantaneous_ops_per_sec: Joi.string(),\n          instantaneous_input_kbps: Joi.string(),\n          instantaneous_output_kbps: Joi.string(),\n          uptime_in_days: Joi.string(),\n          maxmemory_policy: Joi.string(),\n          numberOfKeysRange: Joi.string(),\n        }),\n      }),\n    ),\n    stats: Joi.object().keys({\n      instantaneous_ops_per_sec: Joi.string(),\n      instantaneous_input_kbps: Joi.string(),\n      instantaneous_output_kbps: Joi.string(),\n      uptime_in_days: Joi.string(),\n      maxmemory_policy: Joi.string(),\n      numberOfKeysRange: Joi.string(),\n    }),\n  })\n  .required()\n  .strict();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /databases/:id/info`, () => {\n  before(localDb.createDatabaseInstances);\n\n  [\n    {\n      name: 'Should get database info',\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body.version).to.eql(rte.env.version);\n      },\n    },\n    {\n      endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n      name: 'Should not get info due to misconfiguration',\n      statusCode: 424,\n      responseBody: {\n        statusCode: 424,\n        error: 'RedisConnectionUnavailableException',\n        errorCode: 10904,\n      },\n    },\n    {\n      name: 'Should return NotFound error if instance id does not exists',\n      endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n      statusCode: 404,\n      responseBody: {\n        statusCode: 404,\n        error: 'Not Found',\n        message: 'Invalid database instance id.',\n      },\n    },\n  ].map(mainCheckFn);\n\n  describe('ACL', () => {\n    requirements(\n      'rte.acl',\n      'rte.type=STANDALONE',\n      '!rte.re',\n      '!rte.sharedData',\n    );\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should return 1 for empty databases',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        before: () => rte.data.setAclUserRules('~* +@all -config'),\n        responseBody: {\n          databases: 1,\n          // ...other fields\n        },\n        statusCode: 200,\n      },\n      {\n        name: 'Should return 1 for database with keys created for db0 only',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        before: async () => {\n          await rte.data.setAclUserRules('~* +@all -config');\n          await rte.data.generateStrings();\n        },\n        responseBody: {\n          databases: 1,\n          // ...other fields\n        },\n        statusCode: 200,\n      },\n      {\n        name: 'Should return > 1 databases since data persists there',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        before: async () => {\n          await rte.data.setAclUserRules('~* +@all -config');\n\n          // generate data in > 0 logical database\n          await rte.data.executeCommand(\n            'select',\n            `${constants.TEST_REDIS_DB_INDEX}`,\n          );\n          await rte.data.executeCommand('set', 'some', 'key');\n          await rte.data.executeCommand('select', '0');\n        },\n        responseBody: {\n          databases: constants.TEST_REDIS_DB_INDEX + 1,\n          // ...other fields\n        },\n        statusCode: 200,\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/GET-databases-id-overview.test.ts",
    "content": "import {\n  describe,\n  deps,\n  before,\n  expect,\n  requirements,\n  getMainCheckFn,\n} from '../deps';\nimport { Joi } from '../../helpers/test';\nimport { parseClusterNodesResponse } from '../../helpers/utils';\nconst { localDb, request, server, constants, rte } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${id}/overview`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    version: Joi.string().required(),\n    serverName: Joi.string().allow(null),\n    totalKeys: Joi.number().integer().allow(null),\n    totalKeysPerDb: Joi.object().allow(null),\n    usedMemory: Joi.number().integer().allow(null),\n    connectedClients: Joi.number().allow(null),\n    opsPerSecond: Joi.number().allow(null),\n    networkInKbps: Joi.number().allow(null),\n    networkOutKbps: Joi.number().integer().allow(null),\n    cpuUsagePercentage: Joi.number().allow(null),\n    maxCpuUsagePercentage: Joi.number().allow(null),\n  })\n  .required()\n  .strict();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /${constants.API.DATABASES}/:id/overview`, () => {\n  before(localDb.createDatabaseInstances);\n\n  [\n    {\n      name: 'Should get database overview',\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body.version).to.eql(rte.env.version);\n      },\n    },\n    {\n      endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n      name: 'Should not connect to a database due to misconfiguration',\n      statusCode: 424,\n      responseBody: {\n        statusCode: 424,\n        error: 'RedisConnectionUnavailableException',\n        errorCode: 10904,\n      },\n    },\n    {\n      name: 'Should return NotFound error if instance id does not exists',\n      endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n      statusCode: 404,\n      responseBody: {\n        statusCode: 404,\n        error: 'Not Found',\n        message: 'Invalid database instance id.',\n      },\n    },\n  ].map(mainCheckFn);\n\n  describe('Enterprise', () => {\n    requirements('rte.re');\n\n    [\n      {\n        name: 'Should get database overview except CPU',\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.version).to.eql(rte.env.version);\n          expect(body.cpuUsagePercentage).to.eql(undefined);\n          expect(body.totalKeys).to.not.eql(undefined);\n          expect(body.totalKeysPerDb).to.eql(undefined);\n          expect(body.connectedClients).to.not.eql(undefined);\n          expect(body.opsPerSecond).to.not.eql(undefined);\n          expect(body.networkInKbps).to.not.eql(undefined);\n          expect(body.networkOutKbps).to.not.eql(undefined);\n          expect(body.usedMemory).to.not.eql(undefined);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Cluster', () => {\n    requirements('rte.type=CLUSTER');\n\n    [\n      {\n        name: 'Should include maxCpuUsagePercentage for cluster based on primary nodes',\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.version).to.eql(rte.env.version);\n          expect(body).to.have.property('maxCpuUsagePercentage');\n\n          // Get the actual number of primary nodes from the cluster\n          const clusterNodesResponse = await rte.client.cluster('NODES');\n          const clusterNodes = parseClusterNodesResponse(clusterNodesResponse);\n          const primaryNodesCount = clusterNodes.filter(\n            (node) => !node.replicaOf || node.replicaOf === '-',\n          ).length;\n          const expectedMaxCpu = primaryNodesCount * 100;\n\n          expect(body.maxCpuUsagePercentage).to.eql(expectedMaxCpu);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/GET-databases.test.ts",
    "content": "import { describe, expect, deps, before, _, getMainCheckFn } from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { localDb, request, server, constants, rte } = deps;\n\nconst endpoint = () => request(server).get(`/${constants.API.DATABASES}`);\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      host: Joi.string().required(),\n      port: Joi.number().integer().required(),\n      db: Joi.number().integer().allow(null).required(),\n      name: Joi.string().required(),\n      provider: Joi.string().allow(null).required(),\n      new: Joi.boolean().allow(null).required(),\n      timeout: Joi.number().integer().allow(null),\n      compressor: Joi.string()\n        .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n        .allow(null),\n      connectionType: Joi.string()\n        .valid('STANDALONE', 'SENTINEL', 'CLUSTER', 'NOT CONNECTED')\n        .required(),\n      lastConnection: Joi.string().isoDate().allow(null).required(),\n      createdAt: Joi.string().isoDate(),\n      version: Joi.string().allow(null).required(),\n      modules: Joi.array()\n        .items(\n          Joi.object().keys({\n            name: Joi.string().required(),\n            version: Joi.number().integer().required(),\n            semanticVersion: Joi.string(),\n          }),\n        )\n        .min(0)\n        .required(),\n      cloudDetails: Joi.object()\n        .keys({\n          cloudId: Joi.number().integer().required(),\n          subscriptionType: Joi.string().valid('flexible', 'fixed').required(),\n          planMemoryLimit: Joi.number().integer().allow(null),\n          memoryLimitMeasurementUnit: Joi.string().allow(null),\n          free: Joi.boolean().allow(null),\n        })\n        .allow(null),\n      tags: Joi.array()\n        .items(\n          Joi.object().keys({\n            id: Joi.string().required(),\n            key: Joi.string().required(),\n            value: Joi.string().required(),\n            createdAt: Joi.string().isoDate(),\n            updatedAt: Joi.string().isoDate(),\n          }),\n        )\n        .allow(null),\n      isPreSetup: Joi.boolean().allow(null),\n    }),\n  )\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /databases`, () => {\n  before(async () => {\n    await localDb.createDatabaseInstances();\n    // initializing modules list when ran as standalone test\n    await request(server).get(\n      `/databases/${constants.TEST_INSTANCE_ID}/connect`,\n    );\n  });\n\n  [\n    {\n      name: 'Should get instances list',\n      responseSchema,\n      checkFn: ({ body }) => {\n        const instance = _.find(body, { id: constants.TEST_INSTANCE_ID });\n        _.map(rte.env.modules, (module, name) => {\n          expect(\n            _.find(\n              instance.modules,\n              (module) => module.name.toLowerCase() === name,\n            ).version,\n          ).to.eql(module.version);\n        });\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/PATCH-databases-id.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  requirements,\n  getMainCheckFn,\n  _,\n  it,\n  validateApiCall,\n  after,\n} from '../deps';\nimport { Joi } from '../../helpers/test';\nimport { databaseSchema } from './constants';\n\nconst { request, server, localDb, constants, rte } = deps;\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(`/${constants.API.DATABASES}/${id}`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  name: Joi.string().max(500),\n  host: Joi.string(),\n  port: Joi.number().integer(),\n  db: Joi.number().integer().allow(null),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  timeout: Joi.number().integer().allow(null),\n  compressor: Joi.string()\n    .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n    .allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n  ssh: Joi.boolean().allow(null),\n  sshOptions: Joi.object({\n    host: Joi.string().allow(null),\n    port: Joi.number().allow(null),\n    username: Joi.string().allow(null),\n    password: Joi.string().allow(null),\n    privateKey: Joi.string().allow(null),\n    passphrase: Joi.string().allow(null),\n  }).allow(null),\n  caCert: Joi.object({\n    name: Joi.string(),\n    certificate: Joi.string(),\n  }).allow(null),\n  clientCert: Joi.object({\n    name: Joi.string(),\n    certificate: Joi.string(),\n    key: Joi.string(),\n  }).allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst baseDatabaseData = {\n  name: 'someName',\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  username: constants.TEST_REDIS_USER || undefined,\n  password: constants.TEST_REDIS_PASSWORD || undefined,\n};\n\nconst responseSchema = databaseSchema\n  .keys({\n    isPreSetup: Joi.boolean().allow(null),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet oldDatabase;\nlet newDatabase;\ndescribe(`PATCH /databases/:id`, () => {\n  beforeEach(async () => await localDb.createDatabaseInstances());\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n\n    [\n      {\n        name: 'should deprecate to pass both cert id and other cert fields',\n        data: {\n          ...validInputData,\n          caCert: {\n            id: 'id',\n            name: 'ca',\n            certificate: 'ca_certificate',\n          },\n          clientCert: {\n            id: 'id',\n            name: 'client',\n            certificate: 'client_cert',\n            key: 'client_key',\n          },\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n        checkFn: ({ body }) => {\n          expect(body.message).to.contain(\n            'caCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'caCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property key should not exist',\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Common', () => {\n    const newName = constants.getRandomString();\n\n    [\n      {\n        name: 'Should change name (only) for existing database',\n        data: {\n          name: newName,\n        },\n        responseSchema,\n        before: async () => {\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID,\n          );\n          expect(oldDatabase.name).to.not.eq(newName);\n        },\n        responseBody: {\n          name: newName,\n        },\n        after: async () => {\n          newDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID,\n          );\n          expect(newDatabase.name).to.eq(newName);\n        },\n      },\n      {\n        name: 'Should update database without test connections if updated fields does not affect connection details',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_5),\n        data: {\n          name: newName,\n          timeout: 45_000,\n        },\n        responseSchema,\n        before: async () => {\n          await localDb.createIncorrectDatabaseInstances();\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_5,\n          );\n          expect(oldDatabase.name).to.not.eq(newName);\n          // check connection\n          await validateApiCall({\n            endpoint: () =>\n              request(server).get(\n                `/${constants.API.DATABASES}/${oldDatabase.id}/connect`,\n              ),\n            statusCode: 424,\n          });\n        },\n        responseBody: {\n          name: newName,\n          timeout: 45_000,\n        },\n        after: async () => {\n          newDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_5,\n          );\n          expect(newDatabase.name).to.eq(newName);\n          expect(newDatabase.timeout).to.eq(45_000);\n        },\n      },\n      {\n        name: 'Should return 424 error if incorrect connection data provided',\n        data: {\n          name: 'new name',\n          port: 1111,\n          ssh: false,\n        },\n        statusCode: 424,\n        responseBody: {\n          statusCode: 424,\n          message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`,\n          error: 'RedisConnectionUnavailableException',\n          errorCode: 10904,\n        },\n        after: async () => {\n          // check that instance wasn't changed\n          const newDb = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID,\n          );\n          expect(newDb.name).to.not.eql('new name');\n          expect(newDb.port).to.eql(constants.TEST_REDIS_PORT);\n        },\n      },\n      {\n        name: 'Should return Not Found Error',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: {\n          name: 'new name',\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('TAGS', () => {\n    const newTagsDto1 = [\n      {\n        key: constants.TEST_TAGS[1].key,\n        value: constants.TEST_TAGS[1].value,\n      },\n      {\n        key: 'newKey',\n        value: 'newValue',\n      },\n    ];\n    const newTagsDto2 = [\n      {\n        key: constants.TEST_TAGS[1].key,\n        value: constants.TEST_TAGS[1].value,\n      },\n      {\n        key: constants.TEST_TAGS[2].key,\n        value: constants.TEST_TAGS[2].value,\n      },\n    ];\n    const newTagsDto3 = [];\n    const newTagsDto4 = [\n      {\n        key: 'duplicateKey',\n        value: 'value1',\n      },\n      {\n        key: 'duplicateKey',\n        value: 'value2',\n      },\n    ];\n\n    const expectTagsCount = async (count: number) => {\n      const tags = await localDb.getAllTags();\n      expect(tags.length).to.eq(count);\n    };\n    const extractTagPairs = (tags: any[]) =>\n      tags\n        .map(({ key, value }) => ({ key, value }))\n        .sort((a, b) => a.key.localeCompare(b.key));\n\n    [\n      {\n        name: 'Should update database with new tags and remove unused ones (+1,-2)',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_6),\n        data: {\n          tags: newTagsDto1,\n        },\n        responseSchema,\n        before: async () => {\n          await localDb.createInstancesWithTags();\n\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_6,\n          );\n          const tagPairs = extractTagPairs(oldDatabase.tags);\n\n          expect(tagPairs).to.not.eql(newTagsDto1);\n          await expectTagsCount(constants.TEST_TAGS.length);\n        },\n        responseBody: {\n          tags: newTagsDto1,\n        },\n        after: async () => {\n          newDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_6,\n          );\n          const tagPairs = extractTagPairs(newDatabase.tags);\n\n          expect(tagPairs).to.eql(newTagsDto1);\n          await expectTagsCount(constants.TEST_TAGS.length - 1);\n        },\n      },\n      {\n        name: 'Should update database with new tags and remove unused ones (+0,+0)',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_7),\n        data: {\n          tags: newTagsDto2,\n        },\n        responseSchema,\n        before: async () => {\n          await localDb.createInstancesWithTags();\n\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_7,\n          );\n          const tagPairs = extractTagPairs(oldDatabase.tags);\n\n          expect(tagPairs).to.not.eql(newTagsDto2);\n          await expectTagsCount(constants.TEST_TAGS.length);\n        },\n        responseBody: {\n          tags: newTagsDto2,\n        },\n        after: async () => {\n          newDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_7,\n          );\n          const tagPairs = extractTagPairs(newDatabase.tags);\n\n          expect(tagPairs).to.eql(newTagsDto2);\n          await expectTagsCount(constants.TEST_TAGS.length);\n        },\n      },\n      {\n        name: 'Should update database with new tags and remove unused ones (+0,-2)',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_6),\n        data: {\n          tags: newTagsDto3,\n        },\n        responseSchema,\n        before: async () => {\n          await localDb.createInstancesWithTags();\n\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_6,\n          );\n          const tagPairs = extractTagPairs(oldDatabase.tags);\n\n          expect(tagPairs).to.not.eql(newTagsDto3);\n          await expectTagsCount(constants.TEST_TAGS.length);\n        },\n        responseBody: {\n          tags: newTagsDto3,\n        },\n        after: async () => {\n          newDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_6,\n          );\n          const tagPairs = extractTagPairs(newDatabase.tags);\n\n          expect(tagPairs).to.eql(newTagsDto3);\n          await expectTagsCount(constants.TEST_TAGS.length - 2);\n        },\n      },\n      {\n        name: 'Should not allow adding duplicated tags by key to a database',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ID_6),\n        data: {\n          tags: newTagsDto4,\n        },\n        before: async () => {\n          await localDb.createInstancesWithTags();\n\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID_6,\n          );\n          const tagPairs = extractTagPairs(oldDatabase.tags);\n\n          expect(tagPairs).to.not.eql(newTagsDto4);\n          await expectTagsCount(constants.TEST_TAGS.length);\n        },\n        statusCode: 400,\n        responseBody: {\n          message: ['Tags must not contain duplicates by key.'],\n          error: 'Bad Request',\n          statusCode: 400,\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('STANDALONE', () => {\n    requirements('rte.type=STANDALONE', '!rte.ssh');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n\n      [\n        {\n          name: 'Should change host and port and recalculate data such as (provider, modules, etc...)',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n          },\n          responseSchema,\n          before: async () => {\n            oldDatabase = await localDb.getInstanceById(\n              constants.TEST_INSTANCE_ID_3,\n            );\n            expect(oldDatabase.name).to.eq(constants.TEST_INSTANCE_NAME_3);\n            expect(oldDatabase.modules).to.eq('[]');\n            expect(oldDatabase.host).to.not.eq(constants.TEST_REDIS_HOST);\n            expect(oldDatabase.port).to.not.eq(constants.TEST_REDIS_PORT);\n          },\n          responseBody: {\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            timeout: constants.TEST_REDIS_TIMEOUT,\n            compressor: constants.TEST_REDIS_COMPRESSOR,\n            username: null,\n            connectionType: constants.STANDALONE,\n            tls: false,\n            verifyServerCert: false,\n            tlsServername: null,\n          },\n          after: async () => {\n            newDatabase = await localDb.getInstanceById(\n              constants.TEST_INSTANCE_ID_3,\n            );\n            expect(newDatabase).to.contain({\n              ..._.omit(oldDatabase, [\n                'modules',\n                'provider',\n                'lastConnection',\n                'new',\n                'timeout',\n                'compressor',\n                'version',\n                'createdAt',\n                'tags',\n              ]),\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n            });\n          },\n        },\n      ].map(mainCheckFn);\n\n      describe('Enterprise', () => {\n        requirements('rte.re');\n        it('Should throw an error if db index specified', async () => {\n          await validateApiCall({\n            endpoint,\n            statusCode: 424,\n            data: {\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          });\n        });\n      });\n      describe('Oss', () => {\n        requirements('!rte.re');\n        it('Update standalone with particular db index', async () => {\n          let addedId;\n          const cliUuid = constants.getRandomString();\n          const browserKeyName = constants.getRandomString();\n          const cliKeyName = constants.getRandomString();\n\n          await validateApiCall({\n            endpoint,\n            data: {\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n            responseSchema,\n            responseBody: {\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n            checkFn: ({ body }) => {\n              addedId = body.id;\n            },\n          });\n\n          // Create string using Browser API to particular db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${addedId}/string`,\n              ),\n            statusCode: 201,\n            data: {\n              keyName: browserKeyName,\n              value: 'somevalue',\n            },\n          });\n\n          // Create client for CLI\n          await validateApiCall({\n            endpoint: () =>\n              request(server).patch(\n                `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}`,\n              ),\n            statusCode: 200,\n          });\n\n          // Create string using CLI API to 0 db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}/send-command`,\n              ),\n            statusCode: 200,\n            data: {\n              command: `set ${cliKeyName} somevalue`,\n            },\n          });\n\n          // check data created by db index\n          await rte.data.executeCommand(\n            'select',\n            `${constants.TEST_REDIS_DB_INDEX}`,\n          );\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(1);\n\n          // check data created by db index\n          await rte.data.executeCommand('select', '0');\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(0);\n\n          // switch back to db index 0\n          await validateApiCall({\n            endpoint,\n            data: {\n              db: 0,\n            },\n            responseSchema,\n            responseBody: {\n              db: 0,\n            },\n            checkFn: ({ body }) => {\n              addedId = body.id;\n            },\n          });\n        });\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Update standalone with password', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            username: null,\n            password: true,\n            connectionType: constants.STANDALONE,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('update standalone instance using tls without CA verify', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: false,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Update standalone instance using tls and verify and create CA certificate (new)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should throw an error without CA cert when cert validation enabled', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: null,\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error with invalid CA cert', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: dbName,\n              certificate: 'invalid',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      let existingCACertId,\n        existingClientCertId,\n        existingCACertName,\n        existingClientCertName;\n\n      beforeEach(localDb.initAgreements);\n      after(localDb.initAgreements);\n\n      // should be first test to not break other tests\n      it('Update standalone instance and verify users certs (new certificates !do not encrypt)', async () => {\n        await localDb.setAgreements({\n          encryption: false,\n        });\n\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        const newClientCertName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA);\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT);\n            expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY);\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Update standalone instance and verify users certs (new certificates)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = (existingCACertName = constants.getRandomString());\n        const newClientCertName = (existingClientCertName =\n          constants.getRandomString());\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        const { body } = await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_CERT),\n            );\n            expect(clientPair.key).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_KEY),\n            );\n          },\n        });\n\n        // remember certificates ids\n        existingCACertId = body.caCert.id;\n        existingClientCertId = body.clientCert.id;\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should update standalone instance with existing certificates', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: existingCACertId,\n            },\n            clientCert: {\n              id: existingClientCertId,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.name).to.be.a('string');\n            expect(body.caCert.id).to.eq(existingCACertId);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.deep.eq(existingClientCertId);\n            expect(body.clientCert.name).to.be.a('string');\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n          },\n        });\n      });\n      it('Should throw an error if try to create client certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: existingClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            error: 'Bad Request',\n            message: 'This client certificate name is already in use.',\n            statusCode: 400,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error if try to create ca certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newClientName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: existingCACertName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            error: 'Bad Request',\n            message: 'This ca certificate name is already in use.',\n            statusCode: 400,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n  });\n  describe('CLUSTER', () => {\n    requirements('rte.type=CLUSTER');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      it('Update instance without pass', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: dbName,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n          },\n        });\n      });\n      it('Should throw an error if db index specified', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            db: constants.TEST_REDIS_DB_INDEX,\n          },\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Should create instance without CA tls', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: dbName,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n          },\n        });\n      });\n      it('Should create instance tls and create new CA cert', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: constants.getRandomString(),\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.CLUSTER,\n            nodes: rte.env.nodes,\n            tls: true,\n            verifyServerCert: true,\n          },\n        });\n      });\n      it('Should throw an error without CA cert', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            caCert: null,\n          },\n          statusCode: 424,\n          responseBody: {\n            error: 'RedisConnectionIncorrectCertificateException',\n            statusCode: 424,\n            errorCode: 10907,\n          },\n        });\n      });\n      it('Should throw an error without invalid cert', async () => {\n        const newClientName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            clientCert: {\n              name: newClientName,\n              certificate: '-----BEGIN CERTIFICATE REQUEST-----dasdas',\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n          },\n        });\n      });\n    });\n  });\n\n  describe('STANDALONE SSH', () => {\n    requirements('rte.type=STANDALONE', 'rte.ssh');\n    it('Should not update database with incorrect sshOptions', async () => {\n      await validateApiCall({\n        endpoint,\n        data: {\n          sshOptions: {\n            passphrase: 'incorrect passphrase',\n          },\n        },\n        statusCode: 500,\n        responseBody: {\n          error: 'Bad Request',\n          statusCode: 500,\n        },\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      it('Should update database with partial sshOptions', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            sshOptions: {\n              username: constants.TEST_SSH_USER,\n              password: constants.TEST_SSH_PASSWORD,\n            },\n          },\n        });\n      });\n\n      it('Should update standalone instance with existing certificates + ssh (pk)', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: constants.TEST_CA_ID,\n            },\n            clientCert: {\n              id: constants.TEST_USER_CERT_ID,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY,\n            },\n          },\n        });\n      });\n\n      it('Should test standalone instance with existing certificates + ssh (pkp)', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n        await validateApiCall({\n          endpoint,\n          data: {\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: constants.TEST_CA_ID,\n            },\n            clientCert: {\n              id: constants.TEST_USER_CERT_ID,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY_P,\n              passphrase: constants.TEST_SSH_PASSPHRASE,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n    });\n  });\n\n  describe('SENTINEL', () => {\n    describe('PASS', function () {\n      requirements('rte.type=SENTINEL', '!rte.tls', 'rte.pass');\n      it('Should update database without full sentinel master information', async () => {\n        const dbName = constants.getRandomString();\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            sentinelMaster: {\n              password: constants.TEST_SENTINEL_MASTER_PASS || null,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n\n      it('Should throw Unauthorized error', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            sentinelMaster: {\n              password: 'incorrect password',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Failed to authenticate, please check the username or password.',\n            error: 'RedisConnectionUnauthorizedException',\n          },\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/POST-databases-clone-id.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  requirements,\n  getMainCheckFn,\n  _,\n  it,\n  validateApiCall,\n  after,\n} from '../deps';\nimport { databaseSchema } from './constants';\nimport { Joi } from '../../helpers/test';\n\nconst { request, server, localDb, constants, rte } = deps;\n\nconst baseDatabaseData = {\n  name: 'someName',\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  username: constants.TEST_REDIS_USER || undefined,\n  password: constants.TEST_REDIS_PASSWORD || undefined,\n};\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/clone/${id}`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  name: Joi.string().max(500),\n  host: Joi.string(),\n  port: Joi.number().integer(),\n  db: Joi.number().integer().allow(null),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  timeout: Joi.number().integer().allow(null),\n  compressor: Joi.string()\n    .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n    .allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n}).strict();\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst responseSchema = databaseSchema\n  .keys({\n    isPreSetup: Joi.boolean().allow(null),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet oldDatabase;\nlet newDatabase;\ndescribe(`POST /databases/clone/:id`, () => {\n  beforeEach(async () => await localDb.createDatabaseInstances());\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n\n    [\n      {\n        name: 'should deprecate to pass both cert id and other cert fields',\n        data: {\n          ...validInputData,\n          caCert: {\n            id: 'id',\n            name: 'ca',\n            certificate: 'ca_certificate',\n          },\n          clientCert: {\n            id: 'id',\n            name: 'client',\n            certificate: 'client_cert',\n            key: 'client_key',\n          },\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n        checkFn: ({ body }) => {\n          expect(body.message).to.contain(\n            'caCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'caCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property key should not exist',\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 424 error if incorrect connection data provided',\n        data: {\n          name: 'new name',\n          port: 1111,\n          ssh: false,\n        },\n        statusCode: 424,\n        responseBody: {\n          statusCode: 424,\n          message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`,\n          error: 'RedisConnectionUnavailableException',\n          errorCode: 10904,\n        },\n        after: async () => {\n          expect(await localDb.getInstanceByName('new name')).to.eq(null);\n        },\n      },\n      {\n        name: 'Should return Not Found Error',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: {\n          name: 'new name',\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('STANDALONE', () => {\n    requirements('rte.type=STANDALONE', '!rte.ssh');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n\n      [\n        {\n          name: 'Should create new db with host and port and recalculate data such as (provider, modules, etc...)',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: 'some name',\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n          },\n          responseSchema,\n          before: async () => {\n            oldDatabase = await localDb.getInstanceByName(\n              constants.TEST_INSTANCE_NAME_3,\n            );\n            expect(oldDatabase.name).to.eq(constants.TEST_INSTANCE_NAME_3);\n          },\n          responseBody: {\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            timeout: constants.TEST_REDIS_TIMEOUT,\n            compressor: constants.TEST_REDIS_COMPRESSOR,\n            username: null,\n            connectionType: constants.STANDALONE,\n            tls: false,\n            verifyServerCert: false,\n            tlsServername: null,\n          },\n          after: async () => {\n            newDatabase = await localDb.getInstanceByName('some name');\n            expect(newDatabase).to.contain({\n              ..._.omit(oldDatabase, [\n                'id',\n                'modules',\n                'name',\n                'provider',\n                'lastConnection',\n                'new',\n                'timeout',\n                'compressor',\n                'version',\n                'createdAt',\n                'tags',\n              ]),\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              isPreSetup: false,\n            });\n            expect(newDatabase.name).to.not.eq(oldDatabase.name);\n            expect(newDatabase.name).to.eq('some name');\n          },\n        },\n      ].map(mainCheckFn);\n\n      describe('Enterprise', () => {\n        requirements('rte.re');\n        it('Should throw an error if db index specified', async () => {\n          await validateApiCall({\n            endpoint,\n            statusCode: 424,\n            data: {\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          });\n        });\n      });\n      describe('Oss', () => {\n        requirements('!rte.re');\n        it('Update standalone with particular db index', async () => {\n          let addedId;\n          const dbName = constants.getRandomString();\n          const cliUuid = constants.getRandomString();\n          const browserKeyName = constants.getRandomString();\n          const cliKeyName = constants.getRandomString();\n\n          await validateApiCall({\n            endpoint,\n            data: {\n              name: dbName,\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n            responseSchema,\n            responseBody: {\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n            checkFn: ({ body }) => {\n              addedId = body.id;\n            },\n          });\n\n          // Create string using Browser API to particular db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${addedId}/string`,\n              ),\n            statusCode: 201,\n            data: {\n              keyName: browserKeyName,\n              value: 'somevalue',\n            },\n          });\n\n          // Create client for CLI\n          await validateApiCall({\n            endpoint: () =>\n              request(server).patch(\n                `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}`,\n              ),\n            statusCode: 200,\n          });\n\n          // Create string using CLI API to 0 db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}/send-command`,\n              ),\n            statusCode: 200,\n            data: {\n              command: `set ${cliKeyName} somevalue`,\n            },\n          });\n\n          // check data created by db index\n          await rte.data.executeCommand(\n            'select',\n            `${constants.TEST_REDIS_DB_INDEX}`,\n          );\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(1);\n\n          // check data created by db index\n          await rte.data.executeCommand('select', '0');\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(0);\n\n          // switch back to db index 0\n          await validateApiCall({\n            endpoint,\n            data: {\n              db: 0,\n            },\n            responseSchema,\n            responseBody: {\n              db: 0,\n            },\n            checkFn: ({ body }) => {\n              addedId = body.id;\n            },\n          });\n        });\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Clone standalone with password', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            username: null,\n            password: true,\n            connectionType: constants.STANDALONE,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('update standalone instance using tls without CA verify', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: false,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Update standalone instance using tls and verify and create CA certificate (new)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should throw an error without CA cert when cert validation enabled', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: null,\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error with invalid CA cert', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: dbName,\n              certificate: 'invalid',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      let existingCACertId,\n        existingClientCertId,\n        existingCACertName,\n        existingClientCertName;\n\n      beforeEach(localDb.initAgreements);\n      after(localDb.initAgreements);\n\n      // should be first test to not break other tests\n      it('Update standalone instance and verify users certs (new certificates !do not encrypt)', async () => {\n        await localDb.setAgreements({\n          encryption: false,\n        });\n\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        const newClientCertName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA);\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT);\n            expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY);\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Update standalone instance and verify users certs (new certificates)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = (existingCACertName = constants.getRandomString());\n        const newClientCertName = (existingClientCertName =\n          constants.getRandomString());\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        const { body } = await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_CERT),\n            );\n            expect(clientPair.key).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_KEY),\n            );\n          },\n        });\n\n        // remember certificates ids\n        existingCACertId = body.caCert.id;\n        existingClientCertId = body.clientCert.id;\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should update standalone instance with existing certificates', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: existingCACertId,\n            },\n            clientCert: {\n              id: existingClientCertId,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.name).to.be.a('string');\n            expect(body.caCert.id).to.eq(existingCACertId);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.deep.eq(existingClientCertId);\n            expect(body.clientCert.name).to.be.a('string');\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n          },\n        });\n      });\n      it('Should throw an error if try to create client certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 400,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: existingClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseBody: {\n            error: 'Bad Request',\n            message: 'This client certificate name is already in use.',\n            statusCode: 400,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error if try to create ca certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newClientName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 400,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: existingCACertName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseBody: {\n            error: 'Bad Request',\n            message: 'This ca certificate name is already in use.',\n            statusCode: 400,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n  });\n  describe('CLUSTER', () => {\n    requirements('rte.type=CLUSTER');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      it('Update instance without pass', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: dbName,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n          },\n        });\n      });\n      it('Should throw an error if db index specified', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            db: constants.TEST_REDIS_DB_INDEX,\n          },\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Should create instance tls and create new CA cert', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: constants.getRandomString(),\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.CLUSTER,\n            nodes: rte.env.nodes,\n            tls: true,\n            verifyServerCert: true,\n          },\n        });\n      });\n      it('Should throw an error without CA cert', async () => {\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            caCert: null,\n          },\n          statusCode: 424,\n        });\n      });\n      it('Should throw an error without invalid cert', async () => {\n        const newClientName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            clientCert: {\n              name: newClientName,\n              certificate: '-----BEGIN CERTIFICATE REQUEST-----dasdas',\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          statusCode: 400,\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/POST-databases-export.test.ts",
    "content": "import { describe, expect, deps, before, _, getMainCheckFn } from '../deps';\nimport { Joi, requirements } from '../../helpers/test';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = () =>\n  request(server).post(`/${constants.API.DATABASES}/export`);\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      host: Joi.string().required(),\n      port: Joi.number().integer().required(),\n      db: Joi.number().integer().allow(null).required(),\n      name: Joi.string().required(),\n      username: Joi.string().allow(null).required(),\n      password: Joi.string().allow(null),\n      provider: Joi.string().required(),\n      tls: Joi.boolean().allow(null).required(),\n      tlsServername: Joi.string().allow(null).required(),\n      nameFromProvider: Joi.string().allow(null).required(),\n      connectionType: Joi.string()\n        .valid('STANDALONE', 'SENTINEL', 'CLUSTER', 'NOT CONNECTED')\n        .required(),\n      lastConnection: Joi.string().isoDate().allow(null).required(),\n      modules: Joi.array()\n        .items(\n          Joi.object().keys({\n            name: Joi.string().required(),\n            version: Joi.number().integer().required(),\n            semanticVersion: Joi.string(),\n          }),\n        )\n        .min(0)\n        .required(),\n      verifyServerCert: Joi.boolean().allow(null),\n      sentinelMaster: Joi.object({\n        name: Joi.string().required(),\n        username: Joi.string(),\n        password: Joi.string(),\n      }).allow(null),\n      ssh: Joi.boolean().allow(null),\n      forceStandalone: Joi.boolean().allow(null),\n      sshOptions: Joi.object({\n        id: Joi.string(),\n        host: Joi.string().required(),\n        port: Joi.number().required(),\n        username: Joi.string().required(),\n        password: Joi.string().allow(null),\n        privateKey: Joi.string().allow(null),\n        passphrase: Joi.string().allow(null),\n      }).allow(null),\n      caCert: Joi.object({\n        id: Joi.string().required(),\n        name: Joi.string().required(),\n        certificate: Joi.string().required(),\n        isPreSetup: Joi.boolean().allow(null),\n      }).allow(null),\n      clientCert: Joi.object({\n        id: Joi.string().required(),\n        name: Joi.string().required(),\n        certificate: Joi.string().required(),\n        key: Joi.string(),\n        isPreSetup: Joi.boolean().allow(null),\n      }).allow(null),\n      compressor: Joi.string()\n        .valid(\n          'NONE',\n          'GZIP',\n          'ZSTD',\n          'LZ4',\n          'SNAPPY',\n          'Brotli',\n          'PHPGZCompress',\n        )\n        .required(),\n      tags: Joi.array()\n        .items(\n          Joi.object().keys({\n            key: Joi.string().required(),\n            value: Joi.string().required(),\n          }),\n        )\n        .allow(null),\n      providerDetails: Joi.object({\n        provider: Joi.string().required(),\n        authType: Joi.string().required(),\n        azureAccountId: Joi.string(),\n      }).allow(null),\n      isPreSetup: Joi.boolean().allow(null),\n    }),\n  )\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`POST /databases/export`, () => {\n  before(async () => {\n    await localDb.createDatabaseInstances();\n    // initializing modules list when ran as standalone test\n    await request(server).get(\n      `/databases/${constants.TEST_INSTANCE_ACL_ID}/connect`,\n    );\n  });\n  describe('STANDALONE', function () {\n    requirements('rte.type=STANDALONE');\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth', '!rte.ssh');\n      [\n        {\n          name: 'Should return list of databases by ids without secrets',\n          data: {\n            ids: [constants.TEST_INSTANCE_ACL_ID],\n            withSecrets: false,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.length).to.eq(1);\n            expect(body[0]).to.not.have.property('password');\n            expect(body[0].clientCert).to.not.have.property('key');\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ACL_ID);\n            expect(body[0].name).to.eq(constants.TEST_INSTANCE_ACL_NAME);\n            expect(body[0].username).to.eq(constants.TEST_INSTANCE_ACL_USER);\n          },\n        },\n        {\n          name: 'Should return list of databases by ids with secrets',\n          data: {\n            ids: [constants.TEST_INSTANCE_ACL_ID],\n            withSecrets: true,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.length).to.eq(1);\n            expect(body[0]).to.have.property('password');\n            expect(body[0].password).to.eq(constants.TEST_INSTANCE_ACL_PASS);\n            expect(body[0].clientCert).to.have.property('key');\n            expect(body[0].clientCert.key).to.have.eq(\n              constants.TEST_USER_TLS_KEY,\n            );\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ACL_ID);\n            expect(body[0].username).to.eq(constants.TEST_INSTANCE_ACL_USER);\n            expect(body[0].compressor).to.eq(constants.TEST_REDIS_COMPRESSOR);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n  describe('SENTINEL', function () {\n    describe('TLS AUTH', function () {\n      requirements('rte.type=SENTINEL', 'rte.tlsAuth');\n      [\n        {\n          name: 'Should return list of databases by ids without secrets',\n          data: {\n            ids: [constants.TEST_INSTANCE_ACL_ID],\n            withSecrets: false,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.length).to.eq(1);\n            expect(body[0]).to.not.have.property('password');\n            expect(body[0].sentinelMaster).to.not.have.property('password');\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ACL_ID);\n            expect(body[0].name).to.eq(constants.TEST_INSTANCE_ACL_NAME);\n            expect(body[0].sentinelMaster.name).to.eq(\n              constants.TEST_SENTINEL_MASTER_GROUP,\n            );\n            expect(body[0].sentinelMaster).to.have.property('username');\n            expect(body[0].sentinelMaster.username).to.be.a('string');\n          },\n        },\n        {\n          name: 'Should return list of databases by ids with secrets',\n          data: {\n            ids: [constants.TEST_INSTANCE_ACL_ID],\n            withSecrets: true,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0]).to.have.property('password');\n            expect(body[0].sentinelMaster).to.have.property('password');\n            expect(body[0].sentinelMaster.password).to.be.a('string');\n            expect(body[0].sentinelMaster.password).to.not.eq('');\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ACL_ID);\n            expect(body[0].password).to.eq(constants.TEST_REDIS_PASSWORD);\n            expect(body[0].sentinelMaster.name).to.eq(\n              constants.TEST_SENTINEL_MASTER_GROUP,\n            );\n            expect(body[0].sentinelMaster).to.have.property('username');\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n  describe('STANDALONE SSH', function () {\n    requirements('rte.type=STANDALONE', 'rte.ssh');\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n      [\n        {\n          name: 'Should return list of databases by ids without secrets',\n          data: {\n            ids: [constants.TEST_INSTANCE_ACL_ID],\n            withSecrets: false,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.length).to.eq(1);\n            expect(body[0]).to.not.have.property('password');\n            // todo: fixed test but need to review implementation\n            // sshOptions.private key field exists but value is <null>\n            // expect(body[0].sshOptions).to.not.have.property('privateKey');\n            expect(body[0].sshOptions?.privateKey).to.eq(undefined);\n            expect(body[0].clientCert).to.not.have.property('key');\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ACL_ID);\n            expect(body[0].name).to.eq(constants.TEST_INSTANCE_ACL_NAME);\n            expect(body[0].username).to.eq(constants.TEST_INSTANCE_ACL_USER);\n          },\n        },\n        {\n          name: 'Should return list of databases by ids with secrets',\n          data: {\n            ids: [constants.TEST_INSTANCE_ACL_ID],\n            withSecrets: true,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.length).to.eq(1);\n            expect(body[0]).to.have.property('password');\n            expect(body[0].password).to.eq(constants.TEST_INSTANCE_ACL_PASS);\n            expect(body[0].sshOptions).to.have.property('privateKey');\n            expect(body[0].sshOptions.privateKey).to.have.eq(\n              constants.TEST_SSH_PRIVATE_KEY,\n            );\n            expect(body[0].clientCert).to.have.property('key');\n            expect(body[0].clientCert.key).to.have.eq(\n              constants.TEST_USER_TLS_KEY,\n            );\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ACL_ID);\n            expect(body[0].username).to.eq(constants.TEST_INSTANCE_ACL_USER);\n          },\n        },\n        {\n          name: 'Should return list of databases by ids with secrets (ssh privateKey along with passphrase)',\n          data: {\n            ids: [constants.TEST_INSTANCE_ID],\n            withSecrets: true,\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.length).to.eq(1);\n            expect(body[0].sshOptions.privateKey).to.eq(\n              constants.TEST_SSH_PRIVATE_KEY_P,\n            );\n            expect(body[0].sshOptions.passphrase).to.eq(\n              constants.TEST_SSH_PASSPHRASE,\n            );\n            expect(body[0].clientCert).to.have.property('key');\n            expect(body[0].clientCert.key).to.have.eq(\n              constants.TEST_USER_TLS_KEY,\n            );\n            expect(body[0].id).to.eq(constants.TEST_INSTANCE_ID);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/POST-databases-test-id.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  requirements,\n  getMainCheckFn,\n  _,\n  it,\n  validateApiCall,\n  after,\n} from '../deps';\nimport { Joi } from '../../helpers/test';\n\nconst { request, server, localDb, constants } = deps;\n\nconst baseDatabaseData = {\n  name: 'someName',\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  username: constants.TEST_REDIS_USER || undefined,\n  password: constants.TEST_REDIS_PASSWORD || undefined,\n};\n\nconst endpoint = (id = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/test/${id}`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  name: Joi.string().max(500),\n  host: Joi.string(),\n  port: Joi.number().integer(),\n  db: Joi.number().integer().allow(null),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  timeout: Joi.number().integer().allow(null),\n  compressor: Joi.string()\n    .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n    .allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n  ssh: Joi.boolean().allow(null),\n  sshOptions: Joi.object({\n    // todo why allow null\n    host: Joi.string().allow(null),\n    port: Joi.number().allow(null),\n    username: Joi.string().allow(null),\n    password: Joi.string().allow(null),\n    privateKey: Joi.string().allow(null),\n    passphrase: Joi.string().allow(null),\n  }).allow(null),\n  caCert: Joi.object({\n    name: Joi.string(),\n    certificate: Joi.string(),\n  }).allow(null),\n  clientCert: Joi.object({\n    name: Joi.string(),\n    certificate: Joi.string(),\n    key: Joi.string(),\n  }).allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet oldDatabase;\nlet newDatabase;\ndescribe(`POST /databases/test/:id`, () => {\n  beforeEach(async () => await localDb.createDatabaseInstances());\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n\n    [\n      {\n        name: 'should deprecate to pass both cert id and other cert fields',\n        data: {\n          ...validInputData,\n          caCert: {\n            id: 'id',\n            name: 'ca',\n            certificate: 'ca_certificate',\n          },\n          clientCert: {\n            id: 'id',\n            name: 'client',\n            certificate: 'client_cert',\n            key: 'client_key',\n          },\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n        checkFn: ({ body }) => {\n          expect(body.message).to.contain(\n            'caCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'caCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property key should not exist',\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Common', () => {\n    const newName = constants.getRandomString();\n\n    [\n      {\n        name: 'Should not change exist database name',\n        data: {\n          name: newName,\n        },\n        before: async () => {\n          oldDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID,\n          );\n          expect(oldDatabase.name).to.not.eq(newName);\n        },\n        after: async () => {\n          newDatabase = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID,\n          );\n          expect(newDatabase.name).to.not.eq(newName);\n        },\n      },\n      {\n        name: 'Should return 424 error if incorrect connection data provided',\n        data: {\n          name: 'new name',\n          port: 1111,\n          ssh: false,\n        },\n        statusCode: 424,\n        responseBody: {\n          statusCode: 424,\n          message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`,\n          error: 'RedisConnectionUnavailableException',\n          errorCode: 10904,\n        },\n        after: async () => {\n          // check that instance wasn't changed\n          const newDb = await localDb.getInstanceById(\n            constants.TEST_INSTANCE_ID,\n          );\n          expect(newDb.name).to.not.eql('new name');\n          expect(newDb.port).to.eql(constants.TEST_REDIS_PORT);\n        },\n      },\n      {\n        name: 'Should return Not Found Error',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: {\n          name: 'new name',\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('STANDALONE', () => {\n    requirements('rte.type=STANDALONE', '!rte.ssh');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n\n      [\n        {\n          name: 'Should not change host and port and recalculate data such as (provider, modules, etc...)',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n          },\n          before: async () => {\n            oldDatabase = await localDb.getInstanceById(\n              constants.TEST_INSTANCE_ID_3,\n            );\n            expect(oldDatabase.name).to.eq(constants.TEST_INSTANCE_NAME_3);\n            expect(oldDatabase.modules).to.eq('[]');\n            expect(oldDatabase.host).to.not.eq(constants.TEST_REDIS_HOST);\n            expect(oldDatabase.port).to.not.eq(constants.TEST_REDIS_PORT);\n          },\n          after: async () => {\n            newDatabase = await localDb.getInstanceById(\n              constants.TEST_INSTANCE_ID_3,\n            );\n            expect(newDatabase.name).to.eq(constants.TEST_INSTANCE_NAME_3);\n            expect(newDatabase.modules).to.eq('[]');\n            expect(newDatabase.host).to.not.eq(constants.TEST_REDIS_HOST);\n            expect(newDatabase.port).to.not.eq(constants.TEST_REDIS_PORT);\n          },\n        },\n      ].map(mainCheckFn);\n\n      describe('Enterprise', () => {\n        requirements('rte.re');\n        it('Should throw an error if db index specified', async () => {\n          await validateApiCall({\n            endpoint,\n            statusCode: 424,\n            data: {\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          });\n        });\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Should not update standalone with password', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Should not update standalone instance using tls without CA verify', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should not update standalone instance using tls and verify and create CA certificate (new)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error without CA cert when cert validation enabled', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: null,\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error with invalid CA cert', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: dbName,\n              certificate: 'invalid',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      let existingCACertId,\n        existingClientCertId,\n        existingCACertName,\n        existingClientCertName;\n\n      beforeEach(localDb.initAgreements);\n      after(localDb.initAgreements);\n\n      // should be first test to not break other tests\n      it('Should not update standalone instance and verify users certs (new certificates !do not encrypt)', async () => {\n        await localDb.setAgreements({\n          encryption: false,\n        });\n\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        const newClientCertName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Update standalone instance and verify users certs (new certificates)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = (existingCACertName = constants.getRandomString());\n        const newClientCertName = (existingClientCertName =\n          constants.getRandomString());\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should not update standalone instance with existing certificates', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 400,\n          data: {\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: existingCACertId,\n            },\n            clientCert: {\n              id: existingClientCertId,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should not throw an error if try to create client certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 200,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: existingClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should not throw an error if try to create ca certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newClientName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 200,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: existingCACertName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n  });\n  describe('CLUSTER', () => {\n    requirements('rte.type=CLUSTER');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      it('Should not update instance without pass', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 200,\n          data: {\n            name: dbName,\n          },\n        });\n      });\n      it('Should throw an error if db index specified', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            db: constants.TEST_REDIS_DB_INDEX,\n          },\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Should not create instance without CA tls', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n        });\n      });\n      it('Should not create instance tls and create new CA cert', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          statusCode: 424,\n          data: {\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: constants.getRandomString(),\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n        });\n      });\n      it('Should throw an error without CA cert', async () => {\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            caCert: null,\n          },\n          statusCode: 424,\n        });\n      });\n      it('Should throw an error without invalid cert', async () => {\n        const newClientName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3),\n          data: {\n            clientCert: {\n              name: newClientName,\n              certificate: '-----BEGIN CERTIFICATE REQUEST-----dasdas',\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          statusCode: 400,\n        });\n      });\n    });\n  });\n  describe('SENTINEL', () => {\n    describe('PASS', function () {\n      requirements('rte.type=SENTINEL', '!rte.tls', 'rte.pass');\n      it('Should test connection without full sentinel master information', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            sentinelMaster: {\n              password: constants.TEST_SENTINEL_MASTER_PASS || null,\n            },\n          },\n        });\n      });\n\n      it('Should throw Unauthorized error', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            sentinelMaster: {\n              password: 'incorrect password',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Failed to authenticate, please check the username or password.',\n            error: 'RedisConnectionUnauthorizedException',\n          },\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/POST-databases-test.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n  after,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nconst { request, server, localDb, constants } = deps;\n\nconst endpoint = () => request(server).post(`/${constants.API.DATABASES}/test`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  name: Joi.string().max(500).required(),\n  host: Joi.string().required(),\n  port: Joi.number().integer().required(),\n  db: Joi.number().integer().allow(null),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  timeout: Joi.number().integer().allow(null),\n  compressor: Joi.string()\n    .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n    .allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n  sentinelMaster: Joi.object({\n    name: Joi.string().required(),\n    username: Joi.string(),\n    password: Joi.string(),\n  }).allow(null),\n  ssh: Joi.boolean().allow(null),\n  sshOptions: Joi.object({\n    host: Joi.string().required(),\n    port: Joi.number().required(),\n    username: Joi.string().required(),\n    password: Joi.string().allow(null),\n    privateKey: Joi.string().allow(null),\n    passphrase: Joi.string().allow(null),\n  }).allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst baseDatabaseData = {\n  name: 'someName',\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  username: constants.TEST_REDIS_USER || undefined,\n  password: constants.TEST_REDIS_PASSWORD || undefined,\n};\n\nconst baseSentinelData = {\n  name: constants.TEST_SENTINEL_MASTER_GROUP,\n  username: constants.TEST_SENTINEL_MASTER_USER || null,\n  password: constants.TEST_SENTINEL_MASTER_PASS || null,\n};\n\nlet dbName;\nlet newCaName;\nlet newClientCertName;\ndescribe('POST /databases/test', () => {\n  beforeEach(async () => {\n    dbName = constants.getRandomString();\n    newCaName = constants.getRandomString();\n    newClientCertName = constants.getRandomString();\n  });\n\n  afterEach(async () => {\n    expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n    expect(\n      await (\n        await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n      ).findOneBy({ name: newCaName }),\n    ).to.eql(null);\n    expect(\n      await (\n        await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)\n      ).findOneBy({ name: newCaName }),\n    ).to.eql(null);\n  });\n\n  describe('Validation', function () {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n  describe('STANDALONE', () => {\n    requirements('rte.type=STANDALONE', '!rte.ssh');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      it('Test standalone without pass and tls', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n          },\n        });\n      });\n      describe('Enterprise', () => {\n        requirements('rte.re');\n        it('Should throw an error if db index specified', async () => {\n          await validateApiCall({\n            endpoint,\n            statusCode: 424,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          });\n        });\n      });\n      describe('Oss', () => {\n        requirements('!rte.re');\n        it('Test standalone with particular db index', async () => {\n          await validateApiCall({\n            endpoint,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          });\n        });\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Test standalone with password', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n          },\n        });\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Test standalone instance using tls without CA verify', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n        });\n      });\n      it('Test standalone instance using tls and verify BUT NOT create CA certificate (new)', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n        });\n      });\n      it('Should throw an error without CA cert when cert validation enabled', async () => {\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n      });\n      it('Should throw an error with invalid CA cert', async () => {\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: dbName,\n              certificate: 'invalid',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      after(localDb.initAgreements);\n\n      it('Test standalone instance and verify users certs (new certificates)', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n        });\n      });\n      it('Should test standalone instance with existing certificates', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: constants.TEST_CA_ID,\n            },\n            clientCert: {\n              id: constants.TEST_USER_CERT_ID,\n            },\n          },\n        });\n      });\n    });\n  });\n  describe('STANDALONE SSH', () => {\n    requirements('rte.type=STANDALONE', 'rte.ssh');\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      after(localDb.initAgreements);\n\n      it('Test standalone instance and verify users certs + ssh (basic)', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              password: constants.TEST_SSH_PASSWORD,\n            },\n          },\n        });\n      });\n      it('Should test standalone instance with existing certificates + ssh (pk)', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: constants.TEST_CA_ID,\n            },\n            clientCert: {\n              id: constants.TEST_USER_CERT_ID,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY,\n            },\n          },\n        });\n      });\n      it('Should test standalone instance with existing certificates + ssh (pkp)', async () => {\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: constants.TEST_CA_ID,\n            },\n            clientCert: {\n              id: constants.TEST_USER_CERT_ID,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY_P,\n              passphrase: constants.TEST_SSH_PASSPHRASE,\n            },\n          },\n        });\n      });\n    });\n  });\n  describe('CLUSTER', () => {\n    requirements('rte.type=CLUSTER');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      it('Test instance without pass', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n          },\n        });\n      });\n      it('Should throw an error if db index specified', async () => {\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            db: constants.TEST_REDIS_DB_INDEX,\n          },\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Should test instance without CA tls', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n        });\n      });\n      it('Should test instance tls BUT NOT create new CA cert', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n        });\n      });\n      // todo: Should throw an error without CA cert when cert validation enabled\n      // todo: Should throw an error with invalid CA cert\n    });\n  });\n  describe('SENTINEL', () => {\n    requirements('rte.type=SENTINEL', '!rte.tls');\n    it('Should !!!NOT throw an Invalid Data error for sentinel (without sentinelMaster provided)', async () => {\n      await validateApiCall({\n        endpoint,\n        data: {\n          name: constants.getRandomString(),\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n          password: constants.TEST_REDIS_PASSWORD,\n        },\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Test sentinel with password', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n            sentinelMaster: {\n              ...baseSentinelData,\n            },\n          },\n        });\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/POST-databases.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n  after,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  serverConfig,\n  before,\n} from '../deps';\nimport { databaseSchema } from './constants';\nimport { ServerService } from 'src/modules/server/server.service';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { CustomErrorCodes } from 'src/constants';\nconst { rte, request, server, localDb, constants, analytics } = deps;\n\nconst endpoint = () => request(server).post(`/${constants.API.DATABASES}`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  name: Joi.string().max(500).required(),\n  host: Joi.string().required(),\n  port: Joi.number().integer().required(),\n  db: Joi.number().integer().allow(null),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  timeout: Joi.number().integer().allow(null),\n  compressor: Joi.string()\n    .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n    .allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n  sentinelMaster: Joi.object({\n    name: Joi.string().required(),\n    username: Joi.string(),\n    password: Joi.string(),\n  }).allow(null),\n  ssh: Joi.boolean().allow(null),\n  sshOptions: Joi.object({\n    host: Joi.string().required(),\n    port: Joi.number().required(),\n    username: Joi.string().required(),\n    password: Joi.string().allow(null),\n    privateKey: Joi.string().allow(null),\n    passphrase: Joi.string().allow(null),\n  }).allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst baseDatabaseData = {\n  name: 'someName',\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  timeout: constants.TEST_REDIS_TIMEOUT,\n  compressor: constants.TEST_REDIS_COMPRESSOR,\n  username: constants.TEST_REDIS_USER || undefined,\n  password: constants.TEST_REDIS_PASSWORD || undefined,\n};\n\nconst baseSentinelData = {\n  name: constants.TEST_SENTINEL_MASTER_GROUP,\n  username: constants.TEST_SENTINEL_MASTER_USER || null,\n  password: constants.TEST_SENTINEL_MASTER_PASS || null,\n};\n\nconst responseSchema = databaseSchema\n  .keys({\n    isPreSetup: Joi.boolean().allow(null),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases', () => {\n  let existingCACertId,\n    existingClientCertId,\n    existingCACertName,\n    existingClientCertName;\n\n  describe('Validation', function () {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n\n    [\n      {\n        name: 'should deprecate to pass both cert id and other cert fields',\n        data: {\n          ...validInputData,\n          caCert: {\n            id: 'id',\n            name: 'ca',\n            certificate: 'ca_certificate',\n          },\n          clientCert: {\n            id: 'id',\n            name: 'client',\n            certificate: 'client_cert',\n            key: 'client_key',\n          },\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n        checkFn: ({ body }) => {\n          expect(body.message).to.contain(\n            'caCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'caCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property key should not exist',\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('STANDALONE', () => {\n    requirements('rte.type=STANDALONE', '!rte.ssh');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      describe('Analytics', () => {\n        requirements('rte.serverType=local');\n\n        // todo: investigate why fails\n        xit('Create standalone without pass and tls, and send analytics event for it', async () => {\n          const dbName = constants.getRandomString();\n\n          await validateApiCall({\n            endpoint,\n            statusCode: 201,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n            },\n            responseSchema,\n            responseBody: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              username: null,\n              connectionType: constants.STANDALONE,\n              new: true,\n            },\n            checkFn: async ({ body }) => {\n              // todo: find a way to test rest of the fields\n              await analytics.waitForEvent({\n                event: 'CONFIG_DATABASES_DATABASE_ADDED',\n                properties: {\n                  databaseId: body.id,\n                  connectionType: body.connectionType,\n                  provider: body.provider,\n                  useTLS: 'disabled',\n                  verifyTLSCertificate: 'disabled',\n                  useTLSAuthClients: 'disabled',\n                  useSNI: 'disabled',\n                  useSSH: 'disabled',\n                  version: rte.env.version,\n                  // numberOfKeys: 8,\n                  // numberOfKeysRange: '0 - 500 000',\n                  // totalMemory: 881632,\n                  // numberedDatabases: 16,\n                  // numberOfModules: 0,\n                  timeout: body.timeout / 1000,\n                  // RediSearch: { loaded: false },\n                  // RedisAI: { loaded: false },\n                  // RedisGraph: { loaded: false },\n                  // RedisGears: { loaded: false },\n                  // RedisBloom: { loaded: false },\n                  // RedisJSON: { loaded: false },\n                  // RedisTimeSeries: { loaded: false },\n                  // customModules: [],\n                  buildType: ServerService.getAppType(\n                    serverConfig.get('server').buildType,\n                  ),\n                },\n              });\n            },\n          });\n        });\n      });\n      it('Create standalone without pass and tls', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            username: null,\n            connectionType: constants.STANDALONE,\n            new: true,\n          },\n        });\n      });\n      describe('Enterprise', () => {\n        requirements('rte.re');\n        it('Should throw an error if db index specified', async () => {\n          const dbName = constants.getRandomString();\n\n          await validateApiCall({\n            endpoint,\n            statusCode: 424,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          });\n        });\n      });\n      describe('Oss', () => {\n        requirements('!rte.re');\n        it('Create standalone with particular db index', async () => {\n          let addedId;\n          const dbName = constants.getRandomString();\n          const cliUuid = constants.getRandomString();\n          const browserKeyName = constants.getRandomString();\n          const cliKeyName = constants.getRandomString();\n\n          await validateApiCall({\n            endpoint,\n            statusCode: 201,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n            responseSchema,\n            responseBody: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              db: constants.TEST_REDIS_DB_INDEX,\n              username: null,\n              connectionType: constants.STANDALONE,\n              new: true,\n            },\n            checkFn: ({ body }) => {\n              addedId = body.id;\n            },\n          });\n\n          // Create string using Browser API to particular db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${addedId}/string`,\n              ),\n            statusCode: 201,\n            data: {\n              keyName: browserKeyName,\n              value: 'somevalue',\n            },\n          });\n\n          // Create client for CLI\n          await validateApiCall({\n            endpoint: () =>\n              request(server).patch(\n                `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}`,\n              ),\n            statusCode: 200,\n          });\n\n          // Create string using CLI API to 0 db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}/send-command`,\n              ),\n            statusCode: 200,\n            data: {\n              command: `set ${cliKeyName} somevalue`,\n            },\n          });\n\n          // check data created by db index\n          await rte.data.executeCommand(\n            'select',\n            `${constants.TEST_REDIS_DB_INDEX}`,\n          );\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(1);\n\n          // check data created by db index\n          await rte.data.executeCommand('select', '0');\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(0);\n        });\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Create standalone with password', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            username: null,\n            password: true,\n            connectionType: constants.STANDALONE,\n            new: true,\n          },\n        });\n\n        const db = await localDb.getInstanceByName(dbName);\n        expect(db).to.be.an('object');\n        expect(db.new).to.eql(true);\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n    describe('Cloud details', function () {\n      before(async () => {\n        await localDb.initLocalDb(deps.rte, deps.server);\n      });\n      describe('Cloud details without pass and TLS', function () {\n        requirements('!rte.tls');\n        it('Should throw an error if request with cloudDetails and the same connection already exists', async () => {\n          const dbName = constants.getRandomString();\n          // preconditions\n          expect(\n            await localDb.getInstanceById(constants.TEST_INSTANCE_ID),\n          ).to.be.an('object');\n\n          await validateApiCall({\n            endpoint,\n            statusCode: 409,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              username: constants.TEST_REDIS_USER,\n              password: constants.TEST_REDIS_PASSWORD,\n              cloudDetails: {\n                cloudId: constants.TEST_CLOUD_ID,\n                subscriptionType: constants.TEST_CLOUD_SUBSCRIPTION_TYPE,\n              },\n            },\n            responseBody: {\n              message: ERROR_MESSAGES.DATABASE_ALREADY_EXISTS,\n              statusCode: 409,\n              error: 'DatabaseAlreadyExists',\n              errorCode: CustomErrorCodes.DatabaseAlreadyExists,\n            },\n            checkFn: async ({ body }) => {\n              const database = await (\n                await localDb.getRepository(localDb.repositories.DATABASE)\n              ).findOneBy({ host: constants.TEST_REDIS_HOST });\n\n              expect(body.resource.databaseId).to.eq(database.id);\n            },\n          });\n        });\n      });\n      describe('Cloud details with pass and TLS', function () {\n        requirements('rte.tlsAuth');\n        it('Should throw an error if request with cloudDetails and the same connection already exists', async () => {\n          const dbName = constants.getRandomString();\n          const newClientCertName = constants.getRandomString();\n          const newCaName = constants.getRandomString();\n          // preconditions\n          expect(\n            await localDb.getInstanceById(constants.TEST_INSTANCE_ID),\n          ).to.be.an('object');\n\n          await validateApiCall({\n            endpoint,\n            statusCode: 409,\n            data: {\n              name: dbName,\n              host: constants.TEST_REDIS_HOST,\n              port: constants.TEST_REDIS_PORT,\n              username: constants.TEST_REDIS_USER,\n              password: constants.TEST_REDIS_PASSWORD,\n              tls: true,\n              cloudDetails: {\n                cloudId: constants.TEST_CLOUD_ID,\n                subscriptionType: constants.TEST_CLOUD_SUBSCRIPTION_TYPE,\n              },\n              caCert: {\n                name: newCaName,\n                certificate: constants.TEST_REDIS_TLS_CA,\n              },\n              clientCert: {\n                name: newClientCertName,\n                certificate: constants.TEST_USER_TLS_CERT,\n                key: constants.TEST_USER_TLS_KEY,\n              },\n            },\n            responseBody: {\n              message: ERROR_MESSAGES.DATABASE_ALREADY_EXISTS,\n              statusCode: 409,\n              error: 'DatabaseAlreadyExists',\n              errorCode: CustomErrorCodes.DatabaseAlreadyExists,\n            },\n            checkFn: async ({ body }) => {\n              const database = await (\n                await localDb.getRepository(localDb.repositories.DATABASE)\n              ).findOneBy({ host: constants.TEST_REDIS_HOST });\n\n              expect(body.resource.databaseId).to.eq(database.id);\n            },\n          });\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Create standalone instance using tls without CA verify', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: false,\n            new: true,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Create standalone instance using tls and verify and create CA certificate (new)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n            new: true,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should throw an error without CA cert when cert validation enabled', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error with invalid CA cert', async () => {\n        const dbName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: dbName,\n              certificate: 'invalid',\n            },\n          },\n          responseBody: {\n            statusCode: 424,\n            message:\n              'Could not connect to redis:6379, please check the CA or Client certificate.',\n            error: 'RedisConnectionIncorrectCertificateException',\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      after(localDb.initAgreements);\n\n      it('Create standalone instance and verify users certs (new certificates)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = (existingCACertName = constants.getRandomString());\n        const newClientCertName = (existingClientCertName =\n          constants.getRandomString());\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        const { body } = await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_CERT),\n            );\n            expect(clientPair.key).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_KEY),\n            );\n          },\n        });\n\n        // remember certificates ids\n        existingCACertId = body.caCert.id;\n        existingClientCertId = body.clientCert.id;\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should create standalone instance with existing certificates', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: existingCACertId,\n            },\n            clientCert: {\n              id: existingClientCertId,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.name).to.be.a('string');\n            expect(body.caCert.id).to.eq(existingCACertId);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.deep.eq(existingClientCertId);\n            expect(body.clientCert.name).to.be.a('string');\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should throw an error if try to create client certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 400,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: existingClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseBody: {\n            error: 'Bad Request',\n            message: 'This client certificate name is already in use.',\n            statusCode: 400,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Should throw an error if try to create ca certificate with existing name', async () => {\n        const dbName = constants.getRandomString();\n        const newClientName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 400,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: existingCACertName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseBody: {\n            error: 'Bad Request',\n            message: 'This ca certificate name is already in use.',\n            statusCode: 400,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n      });\n      it('Create standalone instance and verify users certs (new certificates !do not encrypt)', async () => {\n        await localDb.setAgreements({\n          encryption: false,\n        });\n\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        const newClientCertName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA);\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT);\n            expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY);\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n    });\n  });\n  describe('STANDALONE SSH', () => {\n    requirements('rte.type=STANDALONE', 'rte.ssh');\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n\n      let existingCACertId, existingClientCertId;\n\n      after(localDb.initAgreements);\n\n      it('Create standalone instance and verify users certs + ssh (basic)', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        const newClientCertName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        const { body } = await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              password: constants.TEST_SSH_PASSWORD,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              // hide security fields (password, sshOptions.password, sshOptions.passphrase)\n              password: true,\n            },\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_CERT),\n            );\n            expect(clientPair.key).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_KEY),\n            );\n\n            const sshOptions: any = await (\n              await localDb.getRepository(\n                localDb.repositories.SSH_OPTIONS_REPOSITORY,\n              )\n            ).findOneBy({ id: body.sshOptions.id });\n\n            expect(sshOptions.username).to.eql(\n              localDb.encryptData(constants.TEST_SSH_USER),\n            );\n            expect(sshOptions.password).to.eql(\n              localDb.encryptData(constants.TEST_SSH_PASSWORD),\n            );\n          },\n        });\n\n        // remember certificates ids\n        existingCACertId = body.caCert.id;\n        existingClientCertId = body.clientCert.id;\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should create standalone instance with existing certificates + ssh (pk)', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: existingCACertId,\n            },\n            clientCert: {\n              id: existingClientCertId,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY,\n            },\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.name).to.be.a('string');\n            expect(body.caCert.id).to.eq(existingCACertId);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.deep.eq(existingClientCertId);\n            expect(body.clientCert.name).to.be.a('string');\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const sshOptions: any = await (\n              await localDb.getRepository(\n                localDb.repositories.SSH_OPTIONS_REPOSITORY,\n              )\n            ).findOneBy({ id: body.sshOptions.id });\n\n            expect(sshOptions.username).to.eql(\n              localDb.encryptData(constants.TEST_SSH_USER),\n            );\n            expect(sshOptions.privateKey).to.eql(\n              localDb.encryptData(constants.TEST_SSH_PRIVATE_KEY),\n            );\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Should create standalone instance with existing certificates + ssh (pkp)', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              id: existingCACertId,\n            },\n            clientCert: {\n              id: existingClientCertId,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: constants.TEST_SSH_PRIVATE_KEY_P,\n              passphrase: constants.TEST_SSH_PASSPHRASE,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              privateKey: true,\n              passphrase: true,\n            },\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.name).to.be.a('string');\n            expect(body.caCert.id).to.eq(existingCACertId);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.deep.eq(existingClientCertId);\n            expect(body.clientCert.name).to.be.a('string');\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const sshOptions: any = await (\n              await localDb.getRepository(\n                localDb.repositories.SSH_OPTIONS_REPOSITORY,\n              )\n            ).findOneBy({ id: body.sshOptions.id });\n\n            expect(sshOptions.username).to.eql(\n              localDb.encryptData(constants.TEST_SSH_USER),\n            );\n            expect(sshOptions.privateKey).to.eql(\n              localDb.encryptData(constants.TEST_SSH_PRIVATE_KEY_P),\n            );\n            expect(sshOptions.passphrase).to.eql(\n              localDb.encryptData(constants.TEST_SSH_PASSPHRASE),\n            );\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      it('Create standalone instance and verify users certs (new certificates !do not encrypt) + ssh (basic)', async () => {\n        await localDb.setAgreements({\n          encryption: false,\n        });\n\n        const dbName = constants.getRandomString();\n        const newCaName = constants.getRandomString();\n        const newClientCertName = constants.getRandomString();\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              password: constants.TEST_SSH_PASSWORD,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.STANDALONE,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n            ssh: true,\n            sshOptions: {\n              host: constants.TEST_SSH_HOST,\n              port: constants.TEST_SSH_PORT,\n              username: constants.TEST_SSH_USER,\n              // hide security fields (password, sshOptions.password, sshOptions.passphrase)\n              password: true,\n            },\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA);\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT);\n            expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY);\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n    });\n  });\n  describe('CLUSTER', () => {\n    requirements('rte.type=CLUSTER');\n    describe('NO AUTH', function () {\n      requirements('!rte.tls', '!rte.pass');\n      it('Create instance without pass', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.CLUSTER,\n            nodes: rte.env.nodes,\n          },\n        });\n      });\n      it('Should throw an error if db index specified', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 424,\n          data: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            db: constants.TEST_REDIS_DB_INDEX,\n          },\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Should create instance without CA tls', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: false,\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            connectionType: constants.CLUSTER,\n            tls: true,\n            nodes: rte.env.nodes,\n            verifyServerCert: false,\n          },\n        });\n      });\n      it('Should create instance tls and create new CA cert', async () => {\n        const dbName = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            tls: true,\n            verifyServerCert: true,\n            caCert: {\n              name: constants.getRandomString(),\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            port: constants.TEST_REDIS_PORT,\n            connectionType: constants.CLUSTER,\n            nodes: rte.env.nodes,\n            tls: true,\n            verifyServerCert: true,\n          },\n        });\n      });\n      // todo: Should throw an error without CA cert when cert validation enabled\n      // todo: Should throw an error with invalid CA cert\n    });\n  });\n  describe('SENTINEL', () => {\n    requirements('rte.type=SENTINEL');\n    describe('COMMON', function () {\n      requirements('!rte.tls');\n      it('Should always throw an Invalid Data error for sentinel', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            name: constants.getRandomString(),\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n          },\n          statusCode: 424,\n          responseBody: {\n            statusCode: 424,\n            error: 'RedisConnectionSentinelMasterRequiredException',\n            message: 'Sentinel master name must be specified.',\n          },\n        });\n      });\n    });\n    describe('PASS', function () {\n      requirements('!rte.tls', 'rte.pass');\n      it('Create sentinel with password (different sentinel and master passwords)', async () => {\n        const dbName = constants.getRandomString();\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n            sentinelMaster: {\n              ...baseSentinelData,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            username: null,\n            password: true,\n            connectionType: constants.SENTINEL,\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n      it('Create sentinel with tls pk', async () => {\n        const dbName = constants.getRandomString();\n        const newCaName = (existingCACertName = constants.getRandomString());\n        const newClientCertName = (existingClientCertName =\n          constants.getRandomString());\n\n        // preconditions\n        expect(await localDb.getInstanceByName(dbName)).to.eql(null);\n\n        await validateApiCall({\n          endpoint,\n          statusCode: 201,\n          data: {\n            ...baseDatabaseData,\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            password: constants.TEST_REDIS_PASSWORD,\n            tls: true,\n            verifyServerCert: true,\n            tlsServername: null,\n            caCert: {\n              name: newCaName,\n              certificate: constants.TEST_REDIS_TLS_CA,\n            },\n            clientCert: {\n              name: newClientCertName,\n              certificate: constants.TEST_USER_TLS_CERT,\n              key: constants.TEST_USER_TLS_KEY,\n            },\n            sentinelMaster: {\n              ...baseSentinelData,\n            },\n          },\n          responseSchema,\n          responseBody: {\n            name: dbName,\n            host: constants.TEST_REDIS_HOST,\n            port: constants.TEST_REDIS_PORT,\n            username: null,\n            password: true,\n            connectionType: constants.SENTINEL,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.caCert.id).to.be.a('string');\n            expect(body.caCert.name).to.eq(newCaName);\n            expect(body.caCert.certificate).to.be.undefined;\n\n            expect(body.clientCert.id).to.be.a('string');\n            expect(body.clientCert.name).to.deep.eq(newClientCertName);\n            expect(body.clientCert.certificate).to.be.undefined;\n            expect(body.clientCert.key).to.be.undefined;\n\n            const ca: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.caCert.id });\n\n            expect(ca.certificate).to.eql(\n              localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n            );\n\n            const clientPair: any = await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).findOneBy({ id: body.clientCert.id });\n\n            expect(clientPair.certificate).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_CERT),\n            );\n            expect(clientPair.key).to.eql(\n              localDb.encryptData(constants.TEST_USER_TLS_KEY),\n            );\n          },\n        });\n\n        expect(await localDb.getInstanceByName(dbName)).to.be.an('object');\n      });\n      // todo: cover connection error for incorrect username/password\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database/constants.ts",
    "content": "import { Joi } from '../../helpers/test';\nimport { caCertSchema, clientCertSchema } from '../certificate/constants';\n\nconst providers = [\n  'REDIS_SOFTWARE',\n  'REDIS_CLOUD',\n  'REDIS_STACK',\n  'OTHER_REDIS_MANAGED',\n  'AZURE_CACHE',\n  'AZURE_CACHE_REDIS_ENTERPRISE',\n  'REDIS_COMMUNITY_EDITION',\n  'AWS_ELASTICACHE',\n  'AWS_MEMORYDB',\n  'VALKEY',\n  'MEMORYSTORE',\n  'DRAGONFLY',\n  'KEYDB',\n  'GARNET',\n  'KVROCKS',\n  'REDICT',\n  'UPSTASH',\n  'UNKNOWN_LOCALHOST',\n  'UNKNOWN',\n];\n\nexport const databaseSchema = Joi.object().keys({\n  id: Joi.string().required(),\n  name: Joi.string().required(),\n  host: Joi.string().required(),\n  port: Joi.number().integer().required(),\n  db: Joi.number().integer().allow(null),\n  connectionType: Joi.string()\n    .valid('STANDALONE', 'CLUSTER', 'SENTINEL')\n    .required(),\n  username: Joi.string().allow(null),\n  password: Joi.boolean().allow(null),\n  timeout: Joi.number().integer().allow(null),\n  compressor: Joi.string()\n    .valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY')\n    .required(),\n  nameFromProvider: Joi.string().allow(null),\n  lastConnection: Joi.string().isoDate().allow(null),\n  createdAt: Joi.string().isoDate(),\n  provider: Joi.string().valid(...providers),\n  new: Joi.boolean().allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n  caCert: caCertSchema.strict(true).allow(null),\n  clientCert: clientCertSchema.strict(true).allow(null),\n  keyNameFormat: Joi.string().valid('Unicode', 'HEX').allow(null),\n  sentinelMaster: Joi.object({\n    name: Joi.string().required(),\n    username: Joi.string().allow(null),\n    password: Joi.boolean().allow(null),\n  }).allow(null),\n  nodes: Joi.array()\n    .items({\n      host: Joi.string().required(),\n      port: Joi.number().integer().required(),\n    })\n    .allow(null),\n  modules: Joi.array()\n    .items({\n      name: Joi.string().required(),\n      version: Joi.number().integer(),\n      semanticVersion: Joi.string(),\n    })\n    .allow(null),\n  ssh: Joi.boolean().allow(null),\n  forceStandalone: Joi.boolean().allow(null),\n  sshOptions: Joi.object({\n    id: Joi.string().allow(null),\n    host: Joi.string().required(),\n    port: Joi.number().required(),\n    username: Joi.string().required(),\n    password: Joi.boolean().allow(null),\n    privateKey: Joi.boolean().allow(null),\n    passphrase: Joi.boolean().allow(null),\n  }).allow(null),\n  version: Joi.string().allow(null),\n  cloudDetails: Joi.object()\n    .keys({\n      cloudId: Joi.number().required(),\n      subscriptionType: Joi.string().valid('fixed', 'flexible').required(),\n      planMemoryLimit: Joi.number(),\n      memoryLimitMeasurementUnit: Joi.string(),\n    })\n    .allow(null),\n  providerDetails: Joi.object()\n    .keys({\n      provider: Joi.string().required(),\n      authType: Joi.string().required(),\n      azureAccountId: Joi.string(),\n    })\n    .allow(null),\n  tags: Joi.array()\n    .items(\n      Joi.object().keys({\n        id: Joi.string().required(),\n        key: Joi.string().required(),\n        value: Joi.string().required(),\n        createdAt: Joi.string().isoDate(),\n        updatedAt: Joi.string().isoDate(),\n      }),\n    )\n    .allow(null),\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts",
    "content": "import { describe, deps, before, expect, getMainCheckFn } from '../deps';\nimport { analysisSchema } from './constants';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = constants.TEST_DATABASE_ANALYSIS_ID_1,\n) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/analysis/${id}`,\n  );\n\nconst responseSchema = analysisSchema;\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:id/analysis/:id', () => {\n  before(\n    async () =>\n      await localDb.generateNDatabaseAnalysis(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          id: constants.TEST_DATABASE_ANALYSIS_ID_1,\n          createdAt: constants.TEST_DATABASE_ANALYSIS_CREATED_AT_1,\n        },\n        1,\n        true,\n      ),\n  );\n\n  [\n    {\n      name: 'Should get database analysis',\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body).to.deep.eq({\n          id: constants.TEST_DATABASE_ANALYSIS_ID_1,\n          databaseId: constants.TEST_INSTANCE_ID,\n          db: constants.TEST_DATABASE_ANALYSIS_DB_1,\n          createdAt:\n            constants.TEST_DATABASE_ANALYSIS_CREATED_AT_1.toISOString(),\n          delimiter: constants.TEST_DATABASE_ANALYSIS_DELIMITER_1,\n          filter: constants.TEST_DATABASE_ANALYSIS_FILTER_1,\n          progress: constants.TEST_DATABASE_ANALYSIS_PROGRESS_1,\n          totalKeys: constants.TEST_DATABASE_ANALYSIS_TOTAL_KEYS_1,\n          totalMemory: constants.TEST_DATABASE_ANALYSIS_TOTAL_MEMORY_1,\n          topKeysNsp: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_NSP_1],\n          topMemoryNsp: [constants.TEST_DATABASE_ANALYSIS_TOP_MEMORY_NSP_1],\n          topKeysLength: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1],\n          topKeysMemory: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1],\n          expirationGroups: [\n            constants.TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1,\n          ],\n          recommendations: [\n            constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION,\n          ],\n        });\n      },\n    },\n    {\n      name: 'Should return NotFound error if analysis does not exists',\n      endpoint: () =>\n        endpoint(\n          constants.TEST_NOT_EXISTED_INSTANCE_ID,\n          constants.TEST_NOT_EXISTED_INSTANCE_ID,\n        ),\n      statusCode: 404,\n      responseBody: {\n        statusCode: 404,\n        error: 'Not Found',\n        message: 'Database analysis was not found.',\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-analysis/GET-databases-id-analysis.test.ts",
    "content": "import { describe, deps, before, getMainCheckFn } from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${instanceId}/analysis`);\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object({\n      createdAt: Joi.date().required(),\n      id: Joi.string().required(),\n      db: Joi.number().integer().allow(null),\n    }),\n  )\n  .required()\n  .max(5);\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:id/analysis', () => {\n  before(\n    async () =>\n      await localDb.generateNDatabaseAnalysis(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n        },\n        30,\n        true,\n      ),\n  );\n\n  [\n    {\n      name: 'Should get list of database analyses',\n      responseSchema,\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  before,\n  getMainCheckFn,\n  Joi,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nimport { analysisSchema } from './constants';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = constants.TEST_DATABASE_ANALYSIS_ID_1,\n) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/analysis/${id}`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  name: Joi.string(),\n  vote: Joi.string(),\n}).strict();\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  vote: constants.getRandomString(),\n};\n\nconst responseSchema = analysisSchema;\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/analysis/:id', () => {\n  before(\n    async () =>\n      await localDb.generateNDatabaseAnalysis(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          id: constants.TEST_DATABASE_ANALYSIS_ID_1,\n          createdAt: constants.TEST_DATABASE_ANALYSIS_CREATED_AT_1,\n        },\n        1,\n        true,\n      ),\n  );\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('recommendations', () => {\n    describe('recommendation vote', () => {\n      [\n        {\n          name: 'Should add vote for RTS recommendation',\n          data: {\n            name: 'luaScript',\n            vote: 'useful',\n          },\n          statusCode: 200,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              constants.TEST_LUA_SCRIPT_VOTE_RECOMMENDATION,\n            ]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  before,\n  getMainCheckFn,\n  requirements,\n} from '../deps';\nimport { analysisSchema } from './constants';\nconst { localDb, request, server, constants, rte } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/analysis`);\n\nconst responseSchema = analysisSchema;\nconst mainCheckFn = getMainCheckFn(endpoint);\nlet repository;\nlet recommendationRepository;\n\ndescribe('POST /databases/:instanceId/analysis', () => {\n  // todo: skip for RE for now since scan 0 count 10000 might return cursor and 0 keys multiple times\n  requirements('!rte.re');\n\n  before(async () => {\n    repository = await localDb.getRepository(\n      localDb.repositories.DATABASE_ANALYSIS,\n    );\n    recommendationRepository = await localDb.getRepository(\n      localDb.repositories.DATABASE_RECOMMENDATION,\n    );\n\n    await localDb.generateNDatabaseAnalysis(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n      },\n      30,\n      true,\n    );\n\n    await rte.data.generateKeys(true);\n  });\n\n  [\n    {\n      name: 'Should create new database analysis and clean history up to 5',\n      data: {\n        delimiter: '-',\n      },\n      statusCode: 201,\n      responseSchema,\n      before: async () => {\n        expect(await repository.count()).to.eq(30);\n      },\n      checkFn: async ({ body }) => {\n        expect(body.totalKeys.total).to.gt(0);\n        expect(body.totalMemory.total).to.gt(0);\n        expect(body.topKeysNsp.length).to.gt(0);\n        expect(body.topMemoryNsp.length).to.gt(0);\n        expect(body.topKeysLength.length).to.gt(0);\n        expect(body.topKeysMemory.length).to.gt(0);\n        expect(body.expirationGroups.length).to.gt(0);\n        expect(body.db).to.gte(0);\n      },\n      after: async () => {\n        expect(await repository.count()).to.eq(5);\n      },\n    },\n    {\n      name: 'Should create new database analysis w/o namespaces',\n      data: {\n        delimiter: 'somestrangedelimiter',\n      },\n      statusCode: 201,\n      responseSchema,\n      checkFn: async ({ body }) => {\n        expect(body.totalKeys.total).to.gt(0);\n        expect(body.totalMemory.total).to.gt(0);\n        expect(body.topKeysNsp.length).to.eq(0);\n        expect(body.topMemoryNsp.length).to.eq(0);\n        expect(body.topKeysLength.length).to.gt(0);\n        expect(body.topKeysMemory.length).to.gt(0);\n        expect(body.expirationGroups.length).to.gt(0);\n        expect(body.db).to.gte(0);\n      },\n      after: async () => {\n        expect(await repository.count()).to.eq(5);\n      },\n    },\n    {\n      name: 'Should create new database analysis with applied filter',\n      data: {\n        delimiter: '-',\n        filter: {\n          match: constants.TEST_STRING_KEY_1,\n          count: 10_000_000,\n        },\n      },\n      statusCode: 201,\n      responseSchema,\n      checkFn: async ({ body }) => {\n        expect(body.delimiter).to.eq('-');\n        expect(body.progress.total).to.gt(0);\n        expect(body.progress.scanned).to.gte(10_000_000);\n        expect(body.progress.processed).to.eq(1);\n        expect(body.filter).to.deep.eq({\n          match: constants.TEST_STRING_KEY_1,\n          count: 10_000_000,\n        });\n        expect(body.totalKeys).to.deep.eq({\n          total: 1,\n          types: [\n            {\n              type: 'string',\n              total: 1,\n            },\n          ],\n        });\n        expect(body.totalMemory.total).to.gt(0);\n        expect(body.totalMemory.types.length).to.eq(1);\n        expect(body.totalMemory.types[0].total).to.gt(0);\n        expect(body.totalMemory.types[0].type).to.eq('string');\n\n        expect(body.topKeysNsp.length).to.eq(1);\n        expect(\n          constants.TEST_STRING_KEY_1.indexOf(body.topKeysNsp[0].nsp),\n        ).to.eq(0);\n        expect(body.topKeysNsp[0].keys).to.eq(1);\n        expect(body.topKeysNsp[0].memory).to.gt(0);\n        expect(body.topKeysNsp[0].types.length).to.eq(1);\n        expect(body.topKeysNsp[0].types[0].type).to.eq('string');\n        expect(body.topKeysNsp[0].types[0].memory).to.gt(0);\n        expect(body.topKeysNsp[0].types[0].keys).to.eq(1);\n\n        expect(body.topMemoryNsp.length).to.eq(1);\n        expect(\n          constants.TEST_STRING_KEY_1.indexOf(body.topMemoryNsp[0].nsp),\n        ).to.eq(0);\n        expect(body.topMemoryNsp[0].keys).to.eq(1);\n        expect(body.topMemoryNsp[0].memory).to.gt(0);\n        expect(body.topMemoryNsp[0].types.length).to.eq(1);\n        expect(body.topMemoryNsp[0].types[0].type).to.eq('string');\n        expect(body.topMemoryNsp[0].types[0].memory).to.gt(0);\n        expect(body.topMemoryNsp[0].types[0].keys).to.eq(1);\n\n        expect(body.topKeysMemory.length).to.eq(1);\n        expect(body.topKeysMemory[0].name).to.eq(constants.TEST_STRING_KEY_1);\n        expect(body.topKeysMemory[0].type).to.eq('string');\n        expect(body.topKeysMemory[0].ttl).to.eq(-1);\n        expect(body.topKeysMemory[0].memory).to.gt(0);\n        expect(body.topKeysMemory[0].length).to.gt(0);\n\n        expect(body.topKeysLength.length).to.eq(1);\n        expect(body.topKeysLength[0].name).to.eq(constants.TEST_STRING_KEY_1);\n        expect(body.topKeysLength[0].type).to.eq('string');\n        expect(body.topKeysLength[0].ttl).to.eq(-1);\n        expect(body.topKeysLength[0].memory).to.gt(0);\n        expect(body.topKeysLength[0].length).to.gt(0);\n\n        expect(body.expirationGroups.length).to.eq(8);\n        for (let i = 1; i < 8; i++) {\n          expect(body.expirationGroups[i].label).to.be.a('string');\n          expect(body.expirationGroups[i].total).to.eq(0);\n          expect(body.expirationGroups[i].threshold).to.gt(0);\n        }\n        expect(body.expirationGroups[0].label).to.eq('No Expiry');\n        expect(body.expirationGroups[0].total).to.gt(0);\n        expect(body.expirationGroups[0].threshold).to.eq(0);\n        expect(body.db).to.eq(0);\n      },\n      after: async () => {\n        expect(await repository.count()).to.eq(5);\n      },\n    },\n  ].map(mainCheckFn);\n\n  describe('recommendations', () => {\n    requirements('!rte.bigData');\n\n    beforeEach(async () => {\n      await rte.data.truncate();\n    });\n\n    describe('useSmallerKeys recommendation', () => {\n      // generate 1M keys take a lot of time\n      requirements('!rte.type=CLUSTER');\n\n      [\n        {\n          name: 'Should create new database analysis with useSmallerKeys recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            await rte.data.truncate();\n            const KEYS_NUMBER = 1_000_006;\n            await rte.data.generateNKeys(KEYS_NUMBER, false);\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION,\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('combineSmallStringsToHashes recommendation', () => {\n      // can not generate strings only in one node\n      requirements('!rte.type=CLUSTER');\n\n      [\n        {\n          name: 'Should create new database analysis with combineSmallStringsToHashes recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            await rte.data.generateHugeNumberOfTinyStringKeys(10);\n          },\n          checkFn: async ({ body }) => {\n            // can not predict keys order, params.keys is random\n            const recommendationNames = body.recommendations.map(\n              (rec) => rec.name,\n            );\n            expect(recommendationNames).to.include.deep.members([\n              constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION.name,\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('redisVersion recommendation', () => {\n      // todo find solution for redis pass\n      requirements('rte.version <= 6', '!rte.pass');\n      [\n        {\n          name: 'Should create new database analysis with redisVersion recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              constants.TEST_REDIS_VERSION_RECOMMENDATION,\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('recommendations with ReJSON', () => {\n      requirements('rte.modules.rejson');\n      [\n        {\n          name: 'Should create new database analysis with searchJSON recommendation',\n          data: {\n            delimiter: '-',\n          },\n          before: async () => {\n            const NUMBERS_REJSONS = 1;\n            await rte.data.generateNReJSONs(NUMBERS_REJSONS, true);\n          },\n          statusCode: 201,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_SEARCH_JSON_RECOMMENDATION,\n                params: {\n                  keys: [\n                    {\n                      data: [\n                        ...Buffer.from(`${constants.TEST_RUN_ID}_rejson_key_0`),\n                      ],\n                      type: 'Buffer',\n                    },\n                  ],\n                },\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n        {\n          name: 'Should create new database analysis with searchIndexes recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            const jsonValue = JSON.stringify(constants.TEST_REJSON_VALUE_1);\n            await rte.data.sendCommand('ZADD', [\n              constants.TEST_ZSET_KEY_1,\n              constants.TEST_ZSET_MEMBER_1_SCORE,\n              constants.TEST_ZSET_MEMBER_1,\n            ]);\n            await rte.data.sendCommand('json.set', [\n              constants.TEST_ZSET_MEMBER_1,\n              '.',\n              jsonValue,\n            ]);\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_SEARCH_INDEXES_RECOMMENDATION,\n                params: {\n                  keys: [\n                    {\n                      data: [...Buffer.from(constants.TEST_ZSET_KEY_1)],\n                      type: 'Buffer',\n                    },\n                  ],\n                },\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n        {\n          name: 'Should create new database analysis with searchHash recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            for (let index = 0; index <= 51; index++) {\n              await rte.data.sendCommand('HSET', [\n                constants.TEST_HASH_KEY_1 + index,\n                constants.TEST_HASH_FIELD_1_NAME,\n                constants.TEST_HASH_FIELD_1_VALUE,\n                constants.TEST_HASH_FIELD_2_NAME,\n                constants.TEST_HASH_FIELD_2_VALUE,\n                constants.TEST_HASH_FIELD_3_NAME,\n                constants.TEST_HASH_FIELD_3_VALUE,\n              ]);\n            }\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_SEARCH_HASH_RECOMMENDATION,\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('searchIndexes recommendation', () => {\n      requirements('!rte.pass');\n      [\n        {\n          name: 'Should create new database analysis with searchIndexes recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            await rte.data.sendCommand('ZADD', [\n              constants.TEST_ZSET_KEY_1,\n              constants.TEST_ZSET_MEMBER_1_SCORE,\n              constants.TEST_ZSET_MEMBER_1,\n            ]);\n            await rte.data.sendCommand('HSET', [\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_HASH_FIELD_1_NAME,\n              constants.TEST_HASH_FIELD_1_VALUE,\n            ]);\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_SEARCH_INDEXES_RECOMMENDATION,\n                params: {\n                  keys: [\n                    {\n                      data: [...Buffer.from(constants.TEST_ZSET_KEY_1)],\n                      type: 'Buffer',\n                    },\n                  ],\n                },\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('searchHash recommendation', () => {\n      requirements('!rte.pass');\n      [\n        {\n          name: 'Should create new database analysis with searchHash recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            for (let index = 0; index <= 51; index++) {\n              await rte.data.sendCommand('HSET', [\n                constants.TEST_HASH_KEY_1 + index,\n                constants.TEST_HASH_FIELD_1_NAME,\n                constants.TEST_HASH_FIELD_1_VALUE,\n                constants.TEST_HASH_FIELD_2_NAME,\n                constants.TEST_HASH_FIELD_2_VALUE,\n                constants.TEST_HASH_FIELD_3_NAME,\n                constants.TEST_HASH_FIELD_3_VALUE,\n              ]);\n            }\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_SEARCH_HASH_RECOMMENDATION,\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('RTS recommendation', () => {\n      requirements('!rte.pass');\n      [\n        {\n          name: 'Should create new database analysis with RTS recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            await rte.data.sendCommand('ZADD', [\n              constants.TEST_ZSET_KEY_2,\n              constants.TEST_ZSET_TIMESTAMP_SCORE,\n              constants.TEST_ZSET_MEMBER_1,\n            ]);\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_RTS_RECOMMENDATION,\n                params: {\n                  keys: [\n                    {\n                      data: [...Buffer.from(constants.TEST_ZSET_KEY_2)],\n                      type: 'Buffer',\n                    },\n                  ],\n                },\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n        {\n          name: 'Should create new database analysis with RTS recommendation',\n          data: {\n            delimiter: '-',\n          },\n          statusCode: 201,\n          responseSchema,\n          before: async () => {\n            await rte.data.sendCommand('ZADD', [\n              constants.TEST_ZSET_KEY_3,\n              constants.TEST_ZSET_MEMBER_1_SCORE,\n              constants.TEST_ZSET_TIMESTAMP_MEMBER,\n            ]);\n          },\n          checkFn: async ({ body }) => {\n            expect(body.recommendations).to.include.deep.members([\n              {\n                ...constants.TEST_RTS_RECOMMENDATION,\n                params: {\n                  keys: [\n                    {\n                      data: [...Buffer.from(constants.TEST_ZSET_KEY_3)],\n                      type: 'Buffer',\n                    },\n                  ],\n                },\n              },\n            ]);\n          },\n          after: async () => {\n            expect(await repository.count()).to.eq(5);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('sync recommendations', () => {\n      [\n        {\n          name: 'Should create new recommendation in repository',\n          data: {\n            delimiter: '-',\n          },\n          before: async () => {\n            await recommendationRepository.clear();\n\n            const entities: any = await recommendationRepository.findBy({\n              name: constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION.name,\n            });\n            expect(entities.length).to.eq(0);\n\n            const NUMBERS_OF_LIST_ELEMENTS = 1001;\n            await rte.data.generateHugeElementsForListKey(\n              NUMBERS_OF_LIST_ELEMENTS,\n              true,\n            );\n          },\n          statusCode: 201,\n          responseSchema,\n          after: async () => {\n            // wait when recommendation will be saved\n            setTimeout(async () => {\n              const entities: any = await recommendationRepository.findBy({\n                name: constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION.name,\n                params: {\n                  keys: [\n                    {\n                      data: [...Buffer.from(constants.TEST_LIST_KEY_1)],\n                      type: 'Buffer',\n                    },\n                  ],\n                },\n              });\n              expect(entities.length).to.eq(1);\n            }, 5000);\n          },\n        },\n        {\n          name: 'Should not create duplicate recommendation',\n          data: {\n            delimiter: '-',\n          },\n          before: async () => {\n            const entities: any = await recommendationRepository.findBy({\n              name: constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION.name,\n            });\n            expect(entities.length).to.eq(1);\n\n            const NUMBERS_OF_LIST_ELEMENTS = 1001;\n            await rte.data.generateHugeElementsForListKey(\n              NUMBERS_OF_LIST_ELEMENTS,\n              true,\n            );\n          },\n          statusCode: 201,\n          responseSchema,\n          after: async () => {\n            const entities: any = await recommendationRepository.findBy({\n              name: constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION.name,\n            });\n            expect(entities.length).to.eq(1);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    [\n      {\n        name: 'Should create new database analysis with increaseSetMaxIntsetEntries recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const NUMBERS_OF_SET_MEMBERS = 513;\n          await rte.data.generateHugeNumberOfMembersForSetKey(\n            NUMBERS_OF_SET_MEMBERS,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            {\n              ...constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION,\n              params: {\n                keys: [\n                  {\n                    data: [...Buffer.from(constants.TEST_SET_KEY_1)],\n                    type: 'Buffer',\n                  },\n                ],\n              },\n            },\n          ]);\n        },\n        after: async () => {\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n      {\n        name: 'Should create new database analysis with hashHashtableToZiplist recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const NUMBERS_OF_HASH_FIELDS = 513;\n          await rte.data.generateHugeNumberOfFieldsForHashKey(\n            NUMBERS_OF_HASH_FIELDS,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            {\n              ...constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION,\n              params: {\n                keys: [\n                  {\n                    data: [...Buffer.from(constants.TEST_HASH_KEY_1)],\n                    type: 'Buffer',\n                  },\n                ],\n              },\n            },\n          ]);\n        },\n        after: async () => {\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n      {\n        name: 'Should create new database analysis with compressionForList recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const NUMBERS_OF_LIST_ELEMENTS = 1001;\n          await rte.data.generateHugeElementsForListKey(\n            NUMBERS_OF_LIST_ELEMENTS,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            {\n              ...constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION,\n              params: {\n                keys: [\n                  {\n                    data: [...Buffer.from(constants.TEST_LIST_KEY_1)],\n                    type: 'Buffer',\n                  },\n                ],\n              },\n            },\n          ]);\n        },\n        after: async () => {\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n      {\n        name: 'Should create new database analysis with bigStrings recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const BIG_STRING_MEMORY = 100_001;\n          const bigStringValue = Buffer.alloc(\n            BIG_STRING_MEMORY,\n            'a',\n          ).toString();\n\n          await rte.data.sendCommand('set', [\n            constants.TEST_STRING_KEY_1,\n            bigStringValue,\n          ]);\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            {\n              ...constants.TEST_BIG_STRINGS_RECOMMENDATION,\n              params: {\n                keys: [\n                  {\n                    data: [...Buffer.from(constants.TEST_STRING_KEY_1)],\n                    type: 'Buffer',\n                  },\n                ],\n              },\n            },\n          ]);\n        },\n        after: async () => {\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n      {\n        name: 'Should create new database analysis with zSetHashtableToZiplist recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const NUMBERS_OF_ZSET_MEMBERS = 129;\n          await rte.data.generateHugeMembersForSortedListKey(\n            NUMBERS_OF_ZSET_MEMBERS,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            {\n              ...constants.TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION,\n              params: {\n                keys: [\n                  {\n                    data: [...Buffer.from(constants.TEST_ZSET_KEY_1)],\n                    type: 'Buffer',\n                  },\n                ],\n              },\n            },\n          ]);\n        },\n        after: async () => {\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n      {\n        name: 'Should create new database analysis with bigSets recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const NUMBERS_OF_SET_MEMBERS = 1_001;\n          await rte.data.generateHugeNumberOfMembersForSetKey(\n            NUMBERS_OF_SET_MEMBERS,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            {\n              ...constants.TEST_BIG_SETS_RECOMMENDATION,\n              params: {\n                keys: [\n                  {\n                    data: [...Buffer.from(constants.TEST_SET_KEY_1)],\n                    type: 'Buffer',\n                  },\n                ],\n              },\n            },\n          ]);\n        },\n        after: async () => {\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n      {\n        name: 'Should create new database analysis with luaScript recommendation',\n        data: {\n          delimiter: '-',\n        },\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          await rte.data.generateNCachedScripts(11, true);\n        },\n        checkFn: async ({ body }) => {\n          expect(body.recommendations).to.include.deep.members([\n            constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION,\n          ]);\n        },\n        after: async () => {\n          await rte.data.sendCommand('script', ['flush']);\n          expect(await repository.count()).to.eq(5);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-analysis/constants.ts",
    "content": "import { Joi } from '../../helpers/test';\n\nexport const typedRecommendationSchema = Joi.object({\n  name: Joi.string().required(),\n  vote: Joi.string(),\n  params: Joi.any(),\n});\n\nexport const typedTotalSchema = Joi.object({\n  total: Joi.number().integer().required(),\n  types: Joi.array().items(\n    Joi.object({\n      type: Joi.string().required(),\n      total: Joi.number().integer().required(),\n    }),\n  ),\n});\n\nexport const nspSummarySchema = Joi.object({\n  nsp: Joi.string().required(),\n  memory: Joi.number().integer().required(),\n  keys: Joi.number().integer().required(),\n  types: Joi.array().items(\n    Joi.object({\n      type: Joi.string().required(),\n      memory: Joi.number().integer().required(),\n      keys: Joi.number().integer().required(),\n    }),\n  ),\n});\n\nexport const keySchema = Joi.object({\n  name: Joi.string().required(),\n  type: Joi.string().required(),\n  memory: Joi.number().integer().required(),\n  length: Joi.number().integer().required(),\n  ttl: Joi.number().integer().required(),\n});\n\nexport const sumGroupSchema = Joi.object({\n  label: Joi.string().required(),\n  total: Joi.number().integer().required(),\n  threshold: Joi.number().integer().required(),\n});\n\nexport const analysisSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    databaseId: Joi.string().required(),\n    delimiter: Joi.string().required(),\n    createdAt: Joi.date().required(),\n    filter: Joi.object({\n      type: Joi.string().allow(null),\n      match: Joi.string().required(),\n      count: Joi.number().integer().required(),\n    }).required(),\n    progress: Joi.object({\n      total: Joi.number().integer().required(),\n      scanned: Joi.number().integer().required(),\n      processed: Joi.number().integer().required(),\n    }).required(),\n    totalKeys: typedTotalSchema.required(),\n    totalMemory: typedTotalSchema.required(),\n    db: Joi.number().integer().allow(null),\n    topKeysNsp: Joi.array().items(nspSummarySchema).required().max(15),\n    topMemoryNsp: Joi.array().items(nspSummarySchema).required().max(15),\n    topKeysLength: Joi.array().items(keySchema).required().max(15),\n    topKeysMemory: Joi.array().items(keySchema).required().max(15),\n    expirationGroups: Joi.array().items(sumGroupSchema).required(),\n    recommendations: Joi.array().items(typedRecommendationSchema).required(),\n  })\n  .required();\n"
  },
  {
    "path": "redisinsight/api/test/api/database-discovery/pre-setup-databases.tests.ts",
    "content": "import {\n  path,\n  describe,\n  deps,\n  Joi,\n  expect,\n  after,\n  before,\n  getMainCheckFn,\n  fsExtra,\n} from '../deps';\nimport {\n  cleanupPreSetupDatabases,\n  getRepository,\n  initSettings,\n  repositories,\n  resetSettings,\n} from '../../helpers/local-db';\nimport { Repository } from 'typeorm';\nimport {\n  cleanupTestEnvs,\n  mockDatabaseToImportFromEnvsInput,\n  mockDatabaseToImportFromEnvsPrepared,\n  mockDatabaseToImportFromFileInput,\n  mockDatabaseToImportFromFilePrepared,\n  mockDatabaseToImportWithCertsFromEnvsInput,\n  mockDatabaseToImportWithCertsFromEnvsPrepared,\n  mockDatabaseToImportWithCertsFromFileInput,\n  mockDatabaseToImportWithCertsFromFilePrepared,\n} from 'src/__mocks__';\nimport { classToClass } from 'src/utils';\nimport { Database } from 'src/modules/database/models/database';\n\nconst { server, request, constants } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).patch('/settings');\n\nconst responseSchema = Joi.object()\n  .keys({\n    theme: Joi.string().allow(null).required(),\n    scanThreshold: Joi.number().required(),\n    batchSize: Joi.number().required(),\n    dateFormat: Joi.string().allow(null),\n    timezone: Joi.string().allow(null),\n    agreements: Joi.object()\n      .keys({\n        version: Joi.string().required(),\n        eula: Joi.bool().required(),\n        encryption: Joi.bool(),\n      })\n      .pattern(/./, Joi.boolean())\n      .allow(null)\n      .required(),\n  })\n  .required();\n\nconst validInputData = {\n  theme: 'DARK',\n  scanThreshold: 100000,\n  batchSize: 5,\n  dateFormat: null,\n  timezone: null,\n  agreements: {\n    eula: true,\n    analytics: false,\n    encryption: false,\n    notifications: false,\n  },\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet databaseRepository: Repository<any>;\n\ndescribe('Databases discovery', () => {\n  let databases = [];\n  let preSetupDatabases = [];\n\n  before(async () => {\n    databaseRepository = await getRepository(repositories.DATABASE);\n    await cleanupPreSetupDatabases();\n  });\n\n  beforeEach(async () => {\n    const allDatabases = await databaseRepository.find();\n    databases = allDatabases.filter((db) => !db.isPreSetup);\n    preSetupDatabases = allDatabases.filter((db) => db.isPreSetup);\n  });\n\n  afterEach(() => {\n    cleanupTestEnvs();\n    // remove pre setup databases file\n    try {\n      fsExtra.unlinkSync(constants.TEST_PRE_SETUP_DATABASES_PATH);\n    } catch (e) {\n      // ignore error\n    }\n  });\n\n  after(initSettings);\n\n  describe('settings', () => {\n    [\n      {\n        name: 'Should accept eula and discover env databases',\n        before: async () => {\n          await resetSettings();\n          // no pre setup database before accept eula\n          expect(preSetupDatabases.length).deep.eq(0);\n\n          // simple db\n          process.env.RI_REDIS_HOST = mockDatabaseToImportFromEnvsInput.host;\n          process.env.RI_REDIS_PORT = `${mockDatabaseToImportFromEnvsInput.port}`;\n          process.env.RI_REDIS_ALIAS = mockDatabaseToImportFromEnvsInput.name;\n\n          // with base64 certs\n          process.env.RI_REDIS_HOST_1 =\n            mockDatabaseToImportWithCertsFromEnvsInput.host;\n          process.env.RI_REDIS_PORT_1 = `${mockDatabaseToImportWithCertsFromEnvsInput.port}`;\n          process.env.RI_REDIS_ALIAS_1 =\n            mockDatabaseToImportWithCertsFromEnvsInput.name;\n          process.env.RI_REDIS_TLS_1 = 'true';\n          process.env.RI_REDIS_TLS_CA_BASE64_1 = Buffer.from(\n            mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n            'utf8',\n          ).toString('base64');\n          process.env.RI_REDIS_TLS_CERT_BASE64_1 = Buffer.from(\n            mockDatabaseToImportWithCertsFromEnvsInput.clientCert.certificate,\n            'utf8',\n          ).toString('base64');\n          process.env.RI_REDIS_TLS_KEY_BASE64_1 = Buffer.from(\n            mockDatabaseToImportWithCertsFromEnvsInput.clientCert.key,\n            'utf8',\n          ).toString('base64');\n        },\n        data: validInputData,\n        responseSchema,\n        checkFn: async () => {\n          const allDatabases = await databaseRepository.find();\n          // no other databases affected during discovery\n          expect(allDatabases.filter((db) => !db.isPreSetup)).deep.eq(\n            databases,\n          );\n\n          // verify discovered databases\n          const preSetupDatabases = allDatabases\n            .filter((db) => db.isPreSetup)\n            .map((db) => classToClass(Database, db));\n          expect(preSetupDatabases.length).to.eq(2);\n          expect(preSetupDatabases[0]).to.deep.include({\n            ...mockDatabaseToImportFromEnvsPrepared,\n          });\n          expect(preSetupDatabases[1]).to.deep.include({\n            ...mockDatabaseToImportWithCertsFromEnvsPrepared,\n          });\n        },\n      },\n      {\n        name: 'Should remove existing pre setup databases when no any config for them was provided',\n        before: async () => {\n          await resetSettings();\n          // 2 previously added databases\n          expect(preSetupDatabases.length).deep.eq(2);\n        },\n        data: validInputData,\n        responseSchema,\n        checkFn: async () => {\n          const allDatabases = await databaseRepository.find();\n          // no other databases affected during discovery\n          expect(allDatabases.filter((db) => !db.isPreSetup)).deep.eq(\n            databases,\n          );\n\n          // verify there are no pre setup databases\n          expect(allDatabases.filter((db) => db.isPreSetup).length).to.eq(0);\n        },\n      },\n      {\n        name: 'Should accept eula and discover env databases (with certs from file)',\n        before: async () => {\n          await resetSettings();\n          // no pre setup database before accept eula\n          expect(preSetupDatabases.length).deep.eq(0);\n\n          // simple db with password\n          process.env.RI_REDIS_HOST = mockDatabaseToImportFromEnvsInput.host;\n          process.env.RI_REDIS_PORT = `${mockDatabaseToImportFromEnvsInput.port}`;\n          process.env.RI_REDIS_ALIAS = mockDatabaseToImportFromEnvsInput.name;\n          process.env.RI_REDIS_USERNAME = 'admin';\n          process.env.RI_REDIS_PASSWORD = 'pass';\n\n          // prepare certs\n          const caPath = path.join(constants.TEST_DATA_DIR, 'ca.crt');\n          await fsExtra.writeFile(\n            caPath,\n            Buffer.from(\n              mockDatabaseToImportWithCertsFromEnvsInput.caCert.certificate,\n              'utf8',\n            ),\n          );\n\n          const certificatePath = path.join(\n            constants.TEST_DATA_DIR,\n            'user.crt',\n          );\n          await fsExtra.writeFile(\n            certificatePath,\n            Buffer.from(\n              mockDatabaseToImportWithCertsFromEnvsInput.clientCert.certificate,\n              'utf8',\n            ),\n          );\n\n          const keyPath = path.join(constants.TEST_DATA_DIR, 'user.key');\n          await fsExtra.writeFile(\n            keyPath,\n            Buffer.from(\n              mockDatabaseToImportWithCertsFromEnvsInput.clientCert.key,\n              'utf8',\n            ),\n          );\n\n          // with path certs\n          process.env.RI_REDIS_HOST_1 =\n            mockDatabaseToImportWithCertsFromEnvsInput.host;\n          process.env.RI_REDIS_PORT_1 = `${mockDatabaseToImportWithCertsFromEnvsInput.port}`;\n          process.env.RI_REDIS_ALIAS_1 =\n            mockDatabaseToImportWithCertsFromEnvsInput.name;\n          process.env.RI_REDIS_TLS_1 = 'true';\n          process.env.RI_REDIS_TLS_CA_PATH_1 = caPath;\n          process.env.RI_REDIS_TLS_CERT_PATH_1 = certificatePath;\n          process.env.RI_REDIS_TLS_KEY_PATH_1 = keyPath;\n        },\n        data: validInputData,\n        responseSchema,\n        checkFn: async () => {\n          const allDatabases = await databaseRepository.find();\n\n          // no other databases affected during discovery\n          expect(allDatabases.filter((db) => !db.isPreSetup)).deep.eq(\n            databases,\n          );\n\n          // verify discovered databases\n          const preSetupDatabases = allDatabases\n            .filter((db) => db.isPreSetup)\n            .map((db) => classToClass(Database, db));\n          expect(preSetupDatabases.length).to.eq(2);\n          expect(preSetupDatabases[0]).to.deep.include({\n            ...mockDatabaseToImportFromEnvsPrepared,\n            password: 'pass',\n            username: 'admin',\n          });\n          expect(preSetupDatabases[1]).to.deep.include({\n            ...mockDatabaseToImportWithCertsFromEnvsPrepared,\n          });\n        },\n      },\n      {\n        name: 'Should not run database discovery (remove existing pre setup dbs) since we had eula before',\n        before: async () => {\n          // 2 previously added databases\n          expect(preSetupDatabases.length).deep.eq(2);\n        },\n        data: validInputData,\n        responseSchema,\n        checkFn: async () => {\n          const allDatabases = await databaseRepository.find();\n          // no other databases affected during discovery\n          expect(allDatabases.filter((db) => !db.isPreSetup)).deep.eq(\n            databases,\n          );\n\n          // verify there are no pre setup databases\n          expect(allDatabases.filter((db) => db.isPreSetup).length).to.eq(2);\n        },\n      },\n      {\n        name: 'Should accept eula and discover databases from json and remove discovered databases from envs',\n        before: async () => {\n          await resetSettings();\n          // 2 pre setup database before accept eula\n          expect(preSetupDatabases.length).deep.eq(2);\n\n          // prepare databases json\n          const databasesFilePath = path.join(\n            constants.TEST_PRE_SETUP_DATABASES_PATH,\n          );\n          await fsExtra.writeFile(\n            databasesFilePath,\n            JSON.stringify([\n              mockDatabaseToImportFromFileInput,\n              mockDatabaseToImportWithCertsFromFileInput,\n            ]),\n          );\n        },\n        data: validInputData,\n        responseSchema,\n        checkFn: async () => {\n          const allDatabases = await databaseRepository.find();\n\n          // no other databases affected during discovery\n          expect(allDatabases.filter((db) => !db.isPreSetup)).deep.eq(\n            databases,\n          );\n\n          // verify discovered databases\n          const preSetupDatabases = allDatabases\n            .filter((db) => db.isPreSetup)\n            .map((db) => classToClass(Database, db));\n          expect(preSetupDatabases.length).to.eq(2);\n          expect(preSetupDatabases[0]).to.deep.include({\n            ...mockDatabaseToImportFromFilePrepared,\n          });\n          expect(preSetupDatabases[1]).to.deep.include({\n            ...mockDatabaseToImportWithCertsFromFilePrepared,\n          });\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-import/POST-databases-import.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n  getMainCheckFn,\n  generateInvalidDataArray,\n  _,\n} from '../deps';\nimport { randomBytes } from 'crypto';\nimport { cloneDeep, set } from 'lodash';\nconst { rte, request, server, localDb, constants } = deps;\n\nconst endpoint = () =>\n  request(server).post(`/${constants.API.DATABASES}/import`);\n\n// input data schema\nconst databaseSchema = Joi.object({\n  name: Joi.string().allow(null, ''),\n  host: Joi.string().required(),\n  port: Joi.number().integer().allow(true).required(),\n  db: Joi.number().integer().allow(null, ''),\n  username: Joi.string().allow(null, ''),\n  password: Joi.string().allow(null, ''),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst baseDatabaseData = {\n  name: 'someName',\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  username: constants.TEST_REDIS_USER || '',\n  password: constants.TEST_REDIS_PASSWORD || '',\n  compressor: constants.TEST_REDIS_COMPRESSOR,\n};\n\nconst baseTls = {\n  tls: constants.TEST_REDIS_TLS_CA ? true : undefined,\n  caCert: constants.TEST_REDIS_TLS_CA\n    ? {\n        name: constants.TEST_CA_NAME,\n        certificate: constants.TEST_REDIS_TLS_CA,\n      }\n    : undefined,\n  clientCert: constants.TEST_USER_TLS_CERT\n    ? {\n        name: constants.TEST_CLIENT_CERT_NAME,\n        certificate: constants.TEST_USER_TLS_CERT,\n        key: constants.TEST_USER_TLS_KEY,\n      }\n    : undefined,\n};\n\nconst baseSentinelData = {\n  sentinelMaster:\n    constants.TEST_RTE_TYPE === 'SENTINEL'\n      ? {\n          name: constants.TEST_SENTINEL_MASTER_GROUP,\n          username: constants.TEST_SENTINEL_MASTER_USER || null,\n          password: constants.TEST_SENTINEL_MASTER_PASS || null,\n        }\n      : undefined,\n};\n\nconst sshBasicData = {\n  ssh: true,\n  sshOptions: {\n    host: constants.TEST_SSH_HOST,\n    port: constants.TEST_SSH_PORT,\n    username: constants.TEST_SSH_USER,\n    password: constants.TEST_SSH_PASSWORD,\n  },\n};\n\nconst sshPKData = {\n  ...sshBasicData,\n  sshOptions: {\n    ...sshBasicData.sshOptions,\n    password: undefined,\n    privateKey: constants.TEST_SSH_PRIVATE_KEY,\n  },\n};\n\nconst sshPKPData = {\n  ...sshBasicData,\n  sshOptions: {\n    ...sshPKData.sshOptions,\n    privateKey: constants.TEST_SSH_PRIVATE_KEY_P,\n    passphrase: constants.TEST_SSH_PASSPHRASE,\n  },\n};\n\nconst importDatabaseFormat0 = {\n  ...baseDatabaseData,\n  ...baseTls,\n  ...baseSentinelData,\n  connectionType: 'STANDALONE',\n  verifyServerCert: true,\n};\n\nconst baseSentinelDataFormat1 = {\n  sentinelOptions: baseSentinelData.sentinelMaster\n    ? {\n        sentinelPassword: baseSentinelData.sentinelMaster.password,\n        name: baseSentinelData.sentinelMaster.name,\n      }\n    : undefined,\n};\n\nconst sshBasicDataFormat1 = {\n  sshHost: constants.TEST_SSH_HOST,\n  sshPort: constants.TEST_SSH_PORT,\n  sshUser: constants.TEST_SSH_USER,\n  sshPassword: constants.TEST_SSH_PASSWORD,\n};\n\nconst sshPKDataFormat1 = {\n  ...sshBasicDataFormat1,\n  sshPassword: undefined,\n  sshKeyFile: constants.TEST_SSH_PRIVATE_KEY,\n};\n\nconst sshPKPDataFormat1 = {\n  ...sshPKDataFormat1,\n  sshKeyFile: constants.TEST_SSH_PRIVATE_KEY_P,\n  sshKeyPassphrase: constants.TEST_SSH_PASSPHRASE,\n};\n\nconst importDatabaseFormat1 = {\n  id: '1393c216-3fd0-4ad5-8412-209a8e8ec77c',\n  name: baseDatabaseData.name,\n  type: 'standalone',\n  keyPrefix: null,\n  host: baseDatabaseData.host,\n  port: baseDatabaseData.port,\n  username: baseDatabaseData.username,\n  password: baseDatabaseData.password,\n  db: 0,\n  ssl: !!baseTls.tls,\n  caCert: baseTls.caCert ? constants.TEST_CA_CERT_PATH : null,\n  certificate: baseTls.clientCert ? constants.TEST_CLIENT_CERT_PATH : null,\n  keyFile: baseTls.clientCert ? constants.TEST_CLIENT_KEY_PATH : null,\n  ...baseSentinelDataFormat1,\n};\n\nconst baseSentinelDataFormat2 = {\n  sentinelOptions: baseSentinelData.sentinelMaster\n    ? {\n        masterName: baseSentinelData.sentinelMaster.name,\n        nodePassword: baseSentinelData.sentinelMaster.password,\n      }\n    : undefined,\n};\n\nconst sshBasicDataFormat2 = {\n  ssh: true,\n  sshOptions: {\n    host: constants.TEST_SSH_HOST,\n    port: constants.TEST_SSH_PORT,\n    username: constants.TEST_SSH_USER,\n    password: constants.TEST_SSH_PASSWORD,\n  },\n};\n\nconst sshPKDataFormat2 = {\n  ...sshBasicDataFormat2,\n  sshOptions: {\n    ...sshBasicDataFormat2.sshOptions,\n    password: undefined,\n    privatekey: constants.TEST_SSH_PRIVATE_KEY,\n  },\n};\n\nconst sshPKPDataFormat2 = {\n  ...sshBasicDataFormat2,\n  sshOptions: {\n    ...sshBasicDataFormat2.sshOptions,\n    privatekey: constants.TEST_SSH_PRIVATE_KEY_P,\n    passphrase: constants.TEST_SSH_PASSPHRASE,\n  },\n};\n\nconst importDatabaseFormat2 = {\n  host: baseDatabaseData.host,\n  port: `${baseDatabaseData.port}`,\n  auth: baseDatabaseData.password,\n  username: baseDatabaseData.username,\n  connectionName: baseDatabaseData.name,\n  cluster: false,\n  sslOptions: baseTls.caCert\n    ? {\n        key: baseTls.clientCert ? constants.TEST_CLIENT_KEY_PATH : undefined,\n        cert: baseTls.clientCert ? constants.TEST_CLIENT_CERT_PATH : undefined,\n        ca: baseTls.caCert ? constants.TEST_CA_CERT_PATH : undefined,\n      }\n    : undefined,\n  ...baseSentinelDataFormat2,\n};\n\nconst sshBasicDataFormat3 = {\n  ssh_host: constants.TEST_SSH_HOST,\n  ssh_port: constants.TEST_SSH_PORT,\n  ssh_user: constants.TEST_SSH_USER,\n  ssh_password: constants.TEST_SSH_PASSWORD,\n};\n\nconst sshPKDataFormat3 = {\n  ...sshBasicDataFormat3,\n  ssh_password: undefined,\n  ssh_private_key_path: constants.TEST_SSH_PRIVATE_KEY,\n};\n\nconst sshPKPDataFormat3 = {\n  ...sshPKDataFormat3,\n  ssh_private_key_path: constants.TEST_SSH_PRIVATE_KEY_P,\n  ssh_password: constants.TEST_SSH_PASSPHRASE,\n};\n\nconst importDatabaseFormat3 = {\n  name: baseDatabaseData.name,\n  host: baseDatabaseData.host,\n  port: baseDatabaseData.port,\n  auth: baseDatabaseData.password,\n  username: baseDatabaseData.username,\n  ssl: !!baseTls.tls,\n  ssl_ca_cert_path: baseTls.caCert ? constants.TEST_CA_CERT_PATH : undefined,\n  ssl_local_cert_path: baseTls.clientCert\n    ? constants.TEST_CLIENT_CERT_PATH\n    : undefined,\n  ssl_private_key_path: baseTls.clientCert\n    ? constants.TEST_CLIENT_KEY_PATH\n    : undefined,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst checkConnection = async (databaseId: string, statusCode = 200) => {\n  await validateApiCall({\n    endpoint: () =>\n      request(server).get(`/${constants.API.DATABASES}/${databaseId}/connect`),\n    statusCode,\n  });\n};\n\nconst checkDataManagement = async (databaseId: string) => {\n  await validateApiCall({\n    endpoint: () =>\n      request(server).post(\n        `/${constants.API.DATABASES}/${databaseId}/workbench/command-executions`,\n      ),\n    data: {\n      commands: ['set string value'],\n    },\n    checkFn: ({ body }) => {\n      expect(body[0].result).to.deep.eq([\n        {\n          status: 'success',\n          response: 'OK',\n        },\n      ]);\n    },\n  });\n};\n\nconst validateImportedDatabase = async (\n  name: string,\n  initType: string,\n  detectedType: string,\n  dataCheck = true,\n) => {\n  let database = await localDb.getInstanceByName(name);\n  expect(database.connectionType).to.eq(initType);\n  expect(database.new).to.eq(true);\n\n  await checkConnection(database.id);\n  database = await localDb.getInstanceByName(name);\n\n  expect(database.connectionType).to.eq(detectedType);\n  expect(database.new).to.eq(false);\n\n  if (dataCheck) {\n    await checkDataManagement(database.id);\n  }\n};\n\nconst validatePartialImportedDatabase = async (\n  name: string,\n  initType: string,\n  detectedType: string,\n  statusCode = 424,\n) => {\n  let database = await localDb.getInstanceByName(name);\n  expect(database.connectionType).to.eq(initType);\n  expect(database.new).to.eq(true);\n\n  await checkConnection(database.id, statusCode);\n  database = await localDb.getInstanceByName(name);\n\n  expect(database.connectionType).to.eq(detectedType);\n  expect(database.new).to.eq(true);\n};\n\nlet name;\n\ndescribe('POST /databases/import', () => {\n  beforeEach(() => {\n    name = constants.getRandomString();\n  });\n  describe('Validation', function () {\n    generateInvalidDataArray(databaseSchema)\n      .map(({ path, value }) => {\n        const database = path?.length\n          ? set(cloneDeep(validInputData), path, value)\n          : value;\n        return {\n          name: `Should not import when database: ${path.join('.')} = \"${value}\"`,\n          attach: [\n            'file',\n            Buffer.from(JSON.stringify([database])),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [],\n          },\n          checkFn: ({ body }) => {\n            expect(body.fail.length).to.eq(1);\n            expect(body.fail[0].status).to.eq('fail');\n            expect(body.fail[0].index).to.eq(0);\n            expect(body.fail[0].errors.length).to.eq(1);\n            expect(body.fail[0].errors[0].message).to.be.a('string');\n            expect(body.fail[0].errors[0].statusCode).to.eq(400);\n            expect(body.fail[0].errors[0].error).to.eq('Bad Request');\n            if (body.fail[0].host) {\n              expect(body.fail[0].host).to.be.a('string');\n            }\n            if (body.fail[0].port) {\n              expect(body.fail[0].port).to.be.a('number');\n            }\n          },\n        };\n      })\n      .map(async (testCase) => {\n        it(testCase.name, async () => {\n          await validateApiCall({\n            endpoint,\n            ...testCase,\n          });\n        });\n      });\n\n    [\n      {\n        name: 'Should fail due to file was not provided',\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          message: 'No import file provided',\n          error: 'No Database Import File Provided',\n        },\n      },\n      {\n        name: 'Should fail due to file size (>10mb)',\n        statusCode: 400,\n        attach: ['file', randomBytes(11 * 1024 * 1024), 'filename.json'],\n        responseBody: {\n          statusCode: 400,\n          message: 'Import file is too big. Maximum 10mb allowed',\n          error: 'Invalid Database Import File',\n        },\n      },\n      {\n        name: 'Should fail to incorrect file format',\n        statusCode: 400,\n        attach: ['file', randomBytes(10), 'filename.json'],\n        responseBody: {\n          statusCode: 400,\n          message: 'Unable to parse filename.json',\n          error: 'Unable To Parse Database Import File',\n        },\n      },\n      {\n        name: 'Should truncate error message',\n        statusCode: 400,\n        attach: ['file', randomBytes(10), new Array(10_000).fill(1).join('')],\n        responseBody: {\n          statusCode: 400,\n          message: `Unable to parse ${new Array(50).fill(1).join('')}...`,\n          error: 'Unable To Parse Database Import File',\n        },\n      },\n      {\n        name: 'Should return 0/0 imported if mandatory field was not defined (host)',\n        statusCode: 400,\n        attach: ['file', randomBytes(10), new Array(10_000).fill(1).join('')],\n        responseBody: {\n          statusCode: 400,\n          message: `Unable to parse ${new Array(50).fill(1).join('')}...`,\n          error: 'Unable To Parse Database Import File',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Certificates', () => {\n    describe('CA', () => {\n      it('Should create only 1 certificate', async () => {\n        const caCertName = constants.getRandomString();\n\n        const caCerts = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify(\n                new Array(10).fill(1).map(() => {\n                  return {\n                    ...baseDatabaseData,\n                    tls: true,\n                    caCert: {\n                      name: caCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----caCert`,\n                    },\n                    name,\n                  };\n                }),\n              ),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 10,\n            success: new Array(10).fill(1).map((_v, index) => ({\n              index,\n              status: 'success',\n              host: importDatabaseFormat0.host,\n              port: importDatabaseFormat0.port,\n            })),\n            partial: [],\n            fail: [],\n          },\n        });\n\n        const caCerts2 = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const diff = _.differenceWith(caCerts2, caCerts, _.isEqual);\n\n        expect(diff.length).to.eq(1);\n        expect(diff[0].name).to.eq(caCertName);\n\n        // import more\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify(\n                new Array(10).fill(1).map(() => {\n                  return {\n                    ...baseDatabaseData,\n                    tls: true,\n                    caCert: {\n                      name: caCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----caCert`,\n                    },\n                    name,\n                  };\n                }),\n              ),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 10,\n            success: new Array(10).fill(1).map((_v, index) => ({\n              index,\n              status: 'success',\n              host: importDatabaseFormat0.host,\n              port: importDatabaseFormat0.port,\n            })),\n            partial: [],\n            fail: [],\n          },\n        });\n\n        const caCerts3 = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const diff2 = _.differenceWith(caCerts3, caCerts2, _.isEqual);\n\n        expect(diff2.length).to.eq(0);\n      });\n      it('Should create multiple certs with name prefixes', async () => {\n        const caCertName = constants.getRandomString();\n\n        const caCerts = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify(\n                new Array(10).fill(1).map((c, idx) => {\n                  return {\n                    ...baseDatabaseData,\n                    tls: true,\n                    caCert: {\n                      name: caCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----caCert_${idx}`,\n                    },\n                    name,\n                  };\n                }),\n              ),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 10,\n            success: new Array(10).fill(1).map((_v, index) => ({\n              index,\n              status: 'success',\n              host: importDatabaseFormat0.host,\n              port: importDatabaseFormat0.port,\n            })),\n            partial: [],\n            fail: [],\n          },\n        });\n\n        const caCerts2 = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const diff = _.differenceWith(caCerts2, caCerts, _.isEqual);\n\n        expect(diff.length).to.eq(10);\n        expect(diff[0].name).to.eq(caCertName);\n        expect(diff[1].name).to.eq(`1_${caCertName}`);\n        expect(diff[2].name).to.eq(`2_${caCertName}`);\n        expect(diff[3].name).to.eq(`3_${caCertName}`);\n        expect(diff[9].name).to.eq(`9_${caCertName}`);\n      });\n    });\n    describe('CLIENT', () => {\n      it('Should create only 1 certificate', async () => {\n        const caCertName = constants.getRandomString();\n        const clientCertName = constants.getRandomString();\n\n        const caCerts = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const clientCerts = await (\n          await localDb.getRepository(\n            localDb.repositories.CLIENT_CERT_REPOSITORY,\n          )\n        ).find();\n\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify(\n                new Array(10).fill(1).map(() => {\n                  return {\n                    ...baseDatabaseData,\n                    tls: true,\n                    caCert: {\n                      name: caCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----caCert__`,\n                    },\n                    clientCert: {\n                      name: clientCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----clientCert__`,\n                      key: `-----BEGIN PRIVATE KEY-----clientKey__`,\n                    },\n                    name,\n                  };\n                }),\n              ),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 10,\n            success: new Array(10).fill(1).map((_v, index) => ({\n              index,\n              status: 'success',\n              host: importDatabaseFormat0.host,\n              port: importDatabaseFormat0.port,\n            })),\n            partial: [],\n            fail: [],\n          },\n        });\n\n        const caCerts2 = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const diff = _.differenceWith(caCerts2, caCerts, _.isEqual);\n        expect(diff.length).to.eq(1);\n        expect(diff[0].name).to.eq(caCertName);\n\n        const clientCerts2 = await (\n          await localDb.getRepository(\n            localDb.repositories.CLIENT_CERT_REPOSITORY,\n          )\n        ).find();\n        const clientDiff = _.differenceWith(\n          clientCerts2,\n          clientCerts,\n          _.isEqual,\n        );\n        expect(clientDiff.length).to.eq(1);\n        expect(clientDiff[0].name).to.eq(clientCertName);\n\n        // import more\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify(\n                new Array(10).fill(1).map(() => {\n                  return {\n                    ...baseDatabaseData,\n                    tls: true,\n                    caCert: {\n                      name: caCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----caCert__`,\n                    },\n                    clientCert: {\n                      name: clientCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----clientCert__`,\n                      key: `-----BEGIN PRIVATE KEY-----clientKey__`,\n                    },\n                    name,\n                  };\n                }),\n              ),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 10,\n            success: new Array(10).fill(1).map((_v, index) => ({\n              index,\n              status: 'success',\n              host: importDatabaseFormat0.host,\n              port: importDatabaseFormat0.port,\n            })),\n            partial: [],\n            fail: [],\n          },\n        });\n\n        const caCerts3 = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const diff2 = _.differenceWith(caCerts3, caCerts2, _.isEqual);\n        expect(diff2.length).to.eq(0);\n\n        const clientCerts3 = await (\n          await localDb.getRepository(\n            localDb.repositories.CLIENT_CERT_REPOSITORY,\n          )\n        ).find();\n        const clientDiff2 = _.differenceWith(\n          clientCerts3,\n          clientCerts2,\n          _.isEqual,\n        );\n        expect(clientDiff2.length).to.eq(0);\n      });\n      it('Should create multiple certs with name prefixes', async () => {\n        const caCertName = constants.getRandomString();\n        const clientCertName = constants.getRandomString();\n\n        const caCerts = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const clientCerts = await (\n          await localDb.getRepository(\n            localDb.repositories.CLIENT_CERT_REPOSITORY,\n          )\n        ).find();\n\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify(\n                new Array(10).fill(1).map((c, idx) => {\n                  return {\n                    ...baseDatabaseData,\n                    tls: true,\n                    caCert: {\n                      name: caCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----caCert__${idx}`,\n                    },\n                    clientCert: {\n                      name: clientCertName,\n                      certificate: `-----BEGIN CERTIFICATE-----clientCert__${idx}`,\n                      key: `-----BEGIN PRIVATE KEY-----clientKey__${idx}`,\n                    },\n                    name,\n                  };\n                }),\n              ),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 10,\n            success: new Array(10).fill(1).map((_v, index) => ({\n              index,\n              status: 'success',\n              host: importDatabaseFormat0.host,\n              port: importDatabaseFormat0.port,\n            })),\n            partial: [],\n            fail: [],\n          },\n        });\n\n        const caCerts2 = await (\n          await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n        ).find();\n        const diff = _.differenceWith(caCerts2, caCerts, _.isEqual);\n        expect(diff.length).to.eq(10);\n        expect(diff[0].name).to.eq(caCertName);\n        expect(diff[1].name).to.eq(`1_${caCertName}`);\n        expect(diff[2].name).to.eq(`2_${caCertName}`);\n        expect(diff[3].name).to.eq(`3_${caCertName}`);\n        expect(diff[9].name).to.eq(`9_${caCertName}`);\n\n        const clientCerts2 = await (\n          await localDb.getRepository(\n            localDb.repositories.CLIENT_CERT_REPOSITORY,\n          )\n        ).find();\n        const clientDiff = _.differenceWith(\n          clientCerts2,\n          clientCerts,\n          _.isEqual,\n        );\n        expect(clientDiff.length).to.eq(10);\n        expect(clientDiff[0].name).to.eq(clientCertName);\n        expect(clientDiff[1].name).to.eq(`1_${clientCertName}`);\n        expect(clientDiff[2].name).to.eq(`2_${clientCertName}`);\n        expect(clientDiff[3].name).to.eq(`3_${clientCertName}`);\n        expect(clientDiff[9].name).to.eq(`9_${clientCertName}`);\n      });\n    });\n  });\n  describe('STANDALONE', () => {\n    requirements('rte.type=STANDALONE', '!rte.ssh');\n    describe('NO TLS', function () {\n      requirements('!rte.tls');\n      it('Import standalone (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone (format 3)', async () => {\n        const name = constants.getRandomString();\n\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      describe('Oss', () => {\n        requirements('!rte.re');\n        it('Import standalone with particular db index (format 1)', async () => {\n          const name = constants.getRandomString();\n          const cliUuid = constants.getRandomString();\n          const browserKeyName = constants.getRandomString();\n          const cliKeyName = constants.getRandomString();\n\n          await validateApiCall({\n            endpoint,\n            attach: [\n              'file',\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat0,\n                    name,\n                    db: constants.TEST_REDIS_DB_INDEX,\n                  },\n                ]),\n              ),\n              'file.json',\n            ],\n            responseBody: {\n              total: 1,\n              success: [\n                {\n                  index: 0,\n                  status: 'success',\n                  host: importDatabaseFormat0.host,\n                  port: importDatabaseFormat0.port,\n                },\n              ],\n              partial: [],\n              fail: [],\n            },\n          });\n\n          // check connection\n          const database = await localDb.getInstanceByName(name);\n          await validateApiCall({\n            endpoint: () =>\n              request(server).get(\n                `/${constants.API.DATABASES}/${database.id}/connect`,\n              ),\n            statusCode: 200,\n          });\n\n          // Create string using Browser API to particular db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${database.id}/string`,\n              ),\n            statusCode: 201,\n            data: {\n              keyName: browserKeyName,\n              value: 'somevalue',\n            },\n          });\n\n          // Create client for CLI\n          await validateApiCall({\n            endpoint: () =>\n              request(server).patch(\n                `/${constants.API.DATABASES}/${database.id}/cli/${cliUuid}`,\n              ),\n            statusCode: 200,\n          });\n\n          // Create string using CLI API to 0 db index\n          await validateApiCall({\n            endpoint: () =>\n              request(server).post(\n                `/${constants.API.DATABASES}/${database.id}/cli/${cliUuid}/send-command`,\n              ),\n            statusCode: 200,\n            data: {\n              command: `set ${cliKeyName} somevalue`,\n            },\n          });\n\n          // check data created by db index\n          await rte.data.executeCommand(\n            'select',\n            `${constants.TEST_REDIS_DB_INDEX}`,\n          );\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(1);\n\n          // check data created by db index\n          await rte.data.executeCommand('select', '0');\n          expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0);\n          expect(\n            await rte.data.executeCommand('exists', browserKeyName),\n          ).to.eql(0);\n        });\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Import standalone with CA tls (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA tls partial with wrong body (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  caCert: {\n                    ...importDatabaseFormat0.caCert,\n                    certificate: 'bad body',\n                  },\n                  name,\n                },\n              ]),\n            ),\n            'file',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [\n              {\n                index: 0,\n                status: 'partial',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n                errors: [\n                  {\n                    message: 'Invalid CA body',\n                    statusCode: 400,\n                    error: 'Invalid Ca Certificate Body',\n                  },\n                ],\n              },\n            ],\n            fail: [],\n          },\n        });\n\n        await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA tls partial with no ca name (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  caCert: {\n                    ...importDatabaseFormat0.caCert,\n                    name: undefined,\n                  },\n                  name,\n                },\n              ]),\n            ),\n            'file',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [\n              {\n                index: 0,\n                status: 'partial',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n                errors: [\n                  {\n                    message: 'Certificate name is not defined',\n                    statusCode: 400,\n                    error: 'Invalid Certificate Name',\n                  },\n                ],\n              },\n            ],\n            fail: [],\n          },\n        });\n\n        await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA tls (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA tls partial with no ca file (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  caCert: 'not-existing-path',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [\n              {\n                index: 0,\n                status: 'partial',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n                errors: [\n                  {\n                    message: 'Invalid CA body',\n                    statusCode: 400,\n                    error: 'Invalid Ca Certificate Body',\n                  },\n                ],\n              },\n            ],\n            fail: [],\n          },\n        });\n\n        await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA tls (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA tls (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n      it('Import standalone with CA + CLIENT tls (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls partial with wrong bodies (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  caCert: {\n                    ...importDatabaseFormat0.caCert,\n                    certificate: 'bad body',\n                  },\n                  clientCert: {\n                    ...importDatabaseFormat0.clientCert,\n                    certificate: 'bad body',\n                  },\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [\n              {\n                index: 0,\n                status: 'partial',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n                errors: [\n                  {\n                    message: 'Invalid CA body',\n                    statusCode: 400,\n                    error: 'Invalid Ca Certificate Body',\n                  },\n                  {\n                    message: 'Invalid certificate body',\n                    statusCode: 400,\n                    error: 'Invalid Client Certificate Body',\n                  },\n                ],\n              },\n            ],\n            fail: [],\n          },\n        });\n\n        await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls partial with no cert name (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  caCert: {\n                    ...importDatabaseFormat0.caCert,\n                    certificate: 'bad body',\n                  },\n                  clientCert: {\n                    ...importDatabaseFormat0.clientCert,\n                    name: undefined,\n                  },\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [\n              {\n                index: 0,\n                status: 'partial',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n                errors: [\n                  {\n                    message: 'Invalid CA body',\n                    statusCode: 400,\n                    error: 'Invalid Ca Certificate Body',\n                  },\n                  {\n                    message: 'Certificate name is not defined',\n                    statusCode: 400,\n                    error: 'Invalid Certificate Name',\n                  },\n                ],\n              },\n            ],\n            fail: [],\n          },\n        });\n\n        await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls partial with wrong key (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  clientCert: {\n                    ...importDatabaseFormat0.clientCert,\n                    key: 'bad path',\n                  },\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [],\n            partial: [\n              {\n                index: 0,\n                status: 'partial',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n                errors: [\n                  {\n                    message: 'Invalid private key',\n                    statusCode: 400,\n                    error: 'Invalid Client Private Key',\n                  },\n                ],\n              },\n            ],\n            fail: [],\n          },\n        });\n\n        await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n    });\n  });\n  describe('STANDALONE SSH', () => {\n    requirements('rte.type=STANDALONE', 'rte.ssh');\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n      it('Import standalone with CA + CLIENT tls + ssh basic (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  ...sshBasicData,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PK (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  ...sshPKData,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PKP (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  ...sshPKPData,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh basic (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  ...sshBasicDataFormat1,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PK (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  ...sshPKDataFormat1,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PKP (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  ...sshPKPDataFormat1,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh basic (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    ...sshBasicDataFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PK (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    ...sshPKDataFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PKP (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    ...sshPKPDataFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh basic (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  ...sshBasicDataFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PK (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  ...sshPKDataFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n      it('Import standalone with CA + CLIENT tls + ssh PKP (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  ...sshPKPDataFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE');\n      });\n    });\n  });\n  describe('CLUSTER', () => {\n    requirements('rte.type=CLUSTER');\n    describe('NO TLS', function () {\n      requirements('!rte.tls');\n      it('Import cluster (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  connectionType: 'CLUSTER',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER');\n      });\n      it('Import cluster (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  type: 'cluster',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER');\n      });\n      it('Import cluster (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                    cluster: true,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER');\n      });\n      it('Import cluster auto discovered (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                    cluster: false,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'CLUSTER');\n      });\n      it('Import cluster (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'CLUSTER');\n      });\n    });\n    describe('TLS CA', function () {\n      requirements('rte.tls', '!rte.tlsAuth');\n      it('Import cluster with CA tls (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  connectionType: 'CLUSTER',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER');\n      });\n      it('Import cluster with CA tls (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  type: 'cluster',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER');\n      });\n      it('Import cluster with CA tls (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                    cluster: true,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER');\n      });\n      it('Import cluster with CA tls (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'NOT CONNECTED', 'CLUSTER');\n      });\n    });\n  });\n  describe('SENTINEL', () => {\n    requirements('rte.type=SENTINEL');\n    describe('NO TLS', function () {\n      requirements('!rte.tls');\n      it('Import sentinel (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  connectionType: 'SENTINEL',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL');\n      });\n      it('Import sentinel (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  type: 'sentinel',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL');\n      });\n      it('Import sentinel (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL');\n      });\n      // Note: disable this test since this export format does not support different passwords\n      // for sentinel and for the redis itself\n      xit('Import sentinel (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        // should determine connection type as standalone since we don't have sentinel auto discovery\n        await validateImportedDatabase(\n          name,\n          'NOT CONNECTED',\n          'STANDALONE',\n          false,\n        );\n      });\n    });\n    describe('TLS AUTH', function () {\n      requirements('rte.tls', 'rte.tlsAuth');\n      it('Import sentinel with CA + CLIENT tls (format 0)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat0,\n                  connectionType: 'SENTINEL',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat0.host,\n                port: importDatabaseFormat0.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL');\n      });\n      it('Import sentinel with CA + CLIENT tls (format 1)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat1,\n                  type: 'sentinel',\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat1.host,\n                port: importDatabaseFormat1.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL');\n      });\n      it('Import sentinel with CA + CLIENT tls (format 2)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              Buffer.from(\n                JSON.stringify([\n                  {\n                    ...importDatabaseFormat2,\n                    name,\n                  },\n                ]),\n              ).toString('base64'),\n            ),\n            'file.ano',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat2.host,\n                port: parseInt(importDatabaseFormat2.port, 10),\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL');\n      });\n      it('Import sentinel with CA + CLIENT tls (format 3)', async () => {\n        await validateApiCall({\n          endpoint,\n          attach: [\n            'file',\n            Buffer.from(\n              JSON.stringify([\n                {\n                  ...importDatabaseFormat3,\n                  name,\n                },\n              ]),\n            ),\n            'file.json',\n          ],\n          responseBody: {\n            total: 1,\n            success: [\n              {\n                index: 0,\n                status: 'success',\n                host: importDatabaseFormat3.host,\n                port: importDatabaseFormat3.port,\n              },\n            ],\n            partial: [],\n            fail: [],\n          },\n        });\n\n        // should determine connection type as standalone since we don't have sentinel auto discovery\n        await validateImportedDatabase(\n          name,\n          'NOT CONNECTED',\n          'STANDALONE',\n          false,\n        );\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-recommendations/DELETE-databases-id-recommendations.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  before,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nimport { getRepository, repositories } from '../../helpers/local-db';\n\nconst { request, server, localDb, constants } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/recommendations`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  ids: Joi.array().items(Joi.any()).required(),\n}).strict();\n\nconst validInputData = {\n  ids: [constants.getRandomString()],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet repo;\ndescribe(`DELETE /databases/:id/recommendations`, () => {\n  before(async () => {\n    repo = await getRepository(repositories.DATABASE_RECOMMENDATION);\n    await localDb.generateDatabaseRecommendations(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n      },\n      true,\n    );\n  });\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should remove multiple recommendations by ids',\n        data: {\n          ids: [\n            constants.TEST_RECOMMENDATION_ID_1,\n            constants.TEST_RECOMMENDATION_ID_3,\n          ],\n        },\n        responseBody: {\n          affected: 2,\n        },\n        before: async () => {\n          expect(\n            await repo\n              .createQueryBuilder()\n              .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n              .getOne(),\n          ).to.be.an('object');\n          expect(\n            await repo\n              .createQueryBuilder()\n              .where({ id: constants.TEST_RECOMMENDATION_ID_3 })\n              .getOne(),\n          ).to.be.an('object');\n        },\n        after: async () => {\n          expect(\n            await repo\n              .createQueryBuilder()\n              .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n              .getOne(),\n          ).to.eql(null);\n          expect(\n            await repo\n              .createQueryBuilder()\n              .where({ id: constants.TEST_RECOMMENDATION_ID_3 })\n              .getOne(),\n          ).to.eql(null);\n        },\n      },\n      {\n        name: 'Should return affected 0 since no recommendations found',\n        data: {\n          ids: [\n            constants.TEST_RECOMMENDATION_ID_1,\n            constants.TEST_RECOMMENDATION_ID_3,\n          ],\n        },\n        responseBody: {\n          affected: 0,\n        },\n        before: async () => {\n          expect(\n            await repo\n              .createQueryBuilder()\n              .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n              .getOne(),\n          ).to.eql(null);\n          expect(\n            await repo\n              .createQueryBuilder()\n              .where({ id: constants.TEST_RECOMMENDATION_ID_3 })\n              .getOne(),\n          ).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-recommendations/GET-databases-id-recommendations.test.ts",
    "content": "import { describe, deps, before, getMainCheckFn } from '../deps';\nimport { recommendationsSchema } from './constants';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/recommendations`,\n  );\n\nconst responseSchema = recommendationsSchema;\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:id/recommendations', () => {\n  before(\n    async () =>\n      await localDb.generateDatabaseRecommendations(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n        },\n        true,\n      ),\n  );\n\n  [\n    {\n      name: 'Should get list of database recommendations',\n      responseSchema,\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-recommendations/PATCH-databases-id-recommendations-id.test.ts",
    "content": "import { describe, deps, expect, getMainCheckFn } from '../deps';\nimport { getRepository, repositories } from '../../helpers/local-db';\nimport { recommendationSchema } from './constants';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = constants.TEST_RECOMMENDATION_ID_1,\n) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/recommendations/${id}`,\n  );\n\nconst responseSchema = recommendationSchema;\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet repo;\ndescribe('PATCH /recommendations/:id', () => {\n  beforeEach(async () => {\n    repo = await getRepository(repositories.DATABASE_RECOMMENDATION);\n    await localDb.generateDatabaseRecommendations(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n      },\n      true,\n    );\n  });\n\n  describe('Recommendation vote', () => {\n    [\n      {\n        name: 'Should put the vote to the recommendation',\n        data: {\n          vote: constants.TEST_RECOMMENDATION_VOTE,\n        },\n        statusCode: 200,\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eq(constants.TEST_RECOMMENDATION_ID_1);\n          expect(body.vote).to.eq(constants.TEST_RECOMMENDATION_VOTE);\n          expect(body.read).to.eq(false);\n          expect(body.name).to.eq(constants.TEST_RECOMMENDATION_NAME_1);\n        },\n        before: async () => {\n          const recommendation = await repo\n            .createQueryBuilder()\n            .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n            .getOne();\n          expect(recommendation.vote).to.eq(null);\n        },\n        after: async () => {\n          const recommendation = await repo\n            .createQueryBuilder()\n            .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n            .getOne();\n          expect(recommendation.vote).to.eq(constants.TEST_RECOMMENDATION_VOTE);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Recommendation hide', () => {\n    [\n      {\n        name: 'Should put the is hide to the recommendation',\n        data: {\n          hide: constants.TEST_RECOMMENDATION_HIDE,\n        },\n        statusCode: 200,\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eq(constants.TEST_RECOMMENDATION_ID_1);\n          expect(body.hide).to.eq(constants.TEST_RECOMMENDATION_HIDE);\n          expect(body.read).to.eq(false);\n          expect(body.name).to.eq(constants.TEST_RECOMMENDATION_NAME_1);\n        },\n        before: async () => {\n          const recommendation = await repo\n            .createQueryBuilder()\n            .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n            .getOne();\n          expect(recommendation.hide).to.eq(false);\n        },\n        after: async () => {\n          const recommendation = await repo\n            .createQueryBuilder()\n            .where({ id: constants.TEST_RECOMMENDATION_ID_1 })\n            .getOne();\n          expect(recommendation.hide).to.eq(constants.TEST_RECOMMENDATION_HIDE);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-recommendations/PATCH-databases-id-recommendations-read.test.ts",
    "content": "import { describe, deps, expect, getMainCheckFn } from '../deps';\nimport { getRepository, repositories } from '../../helpers/local-db';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/recommendations/read`,\n  );\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet repo;\ndescribe('PATCH /recommendations/read', () => {\n  beforeEach(async () => {\n    repo = await getRepository(repositories.DATABASE_RECOMMENDATION);\n    await localDb.generateDatabaseRecommendations(\n      {\n        databaseId: constants.TEST_INSTANCE_ID,\n      },\n      true,\n    );\n  });\n\n  [\n    {\n      name: 'Should set all recommendations into read state',\n      before: async () => {\n        const recommendations = await repo.createQueryBuilder().getMany();\n        expect(\n          recommendations.filter((recommendation) => {\n            return recommendation.read === false;\n          }).length,\n        ).to.gte(1);\n      },\n      after: async () => {\n        const recommendations = await repo.createQueryBuilder().getMany();\n        expect(\n          recommendations.filter((recommendation) => {\n            return recommendation.read === false;\n          }).length,\n        ).to.eq(0);\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-recommendations/WS-new-recommendations.test.ts",
    "content": "import {\n  describe,\n  it,\n  expect,\n  _,\n  before,\n  deps,\n  validateApiCall,\n  requirements,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\nimport {\n  enableAllDbFeatures,\n  getRepository,\n  repositories,\n} from '../../helpers/local-db';\nimport { Socket } from 'socket.io-client';\nimport { getSocket } from '../../helpers/server';\n\nconst getClient = async (): Promise<Socket> => {\n  return getSocket('');\n};\n\nlet repo;\ndescribe('WS new recommendations', () => {\n  requirements('!rte.sharedData', 'rte.modules.search');\n\n  beforeEach(async () => {\n    repo = await getRepository(repositories.DATABASE_RECOMMENDATION);\n    await repo.clear();\n  });\n\n  before(async () => {\n    await rte.data.truncate();\n    await enableAllDbFeatures();\n  });\n\n  it('Should notify about new big set recommendations', async () => {\n    // generate big set\n    const NUMBERS_OF_SET_MEMBERS = 1_001;\n    await rte.data.generateHugeNumberOfMembersForSetKey(\n      NUMBERS_OF_SET_MEMBERS,\n      true,\n    );\n\n    // Initialize sync by connecting\n    const client = await getClient();\n\n    const recommendationsResponse: any = await new Promise((res) => {\n      client.on(`recommendation`, res);\n\n      validateApiCall({\n        endpoint: () =>\n          request(server).post(\n            `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/keys/get-info`,\n          ),\n        data: {\n          keyName: constants.TEST_SET_KEY_1,\n        },\n      });\n    });\n\n    expect(recommendationsResponse.recommendations.length).to.eq(1);\n    expect(recommendationsResponse.recommendations[0].name).to.eq('bigSets');\n    expect(recommendationsResponse.recommendations[0].databaseId).to.eq(\n      constants.TEST_INSTANCE_ID,\n    );\n    expect(recommendationsResponse.recommendations[0].read).to.eq(false);\n    expect(recommendationsResponse.recommendations[0].disabled).to.eq(false);\n    expect(recommendationsResponse.totalUnread).to.eq(1);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/database-recommendations/constants.ts",
    "content": "import { Joi, JoiRedisString } from '../../helpers/test';\n\nexport const recommendationSchema = Joi.object({\n  read: Joi.boolean().required(),\n  id: Joi.string().required(),\n  name: Joi.string().required(),\n  disabled: Joi.boolean(),\n  hide: Joi.boolean(),\n  vote: Joi.string().valid('very useful', 'useful', 'not useful').allow(null),\n  createdAt: Joi.date(),\n  databaseId: Joi.string(),\n  params: Joi.object({\n    keys: Joi.array().items(JoiRedisString),\n  }).allow(null),\n});\n\nexport const recommendationsSchema = Joi.object({\n  recommendations: Joi.array().items(recommendationSchema),\n  totalUnread: Joi.number().required(),\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/deps.ts",
    "content": "import { getAnalytics } from '../helpers/analytics';\nexport { createAnalytics } from '../helpers/analytics';\n\nexport * from '../helpers/test';\nimport * as request from 'supertest';\nimport * as chai from 'chai';\nimport * as localDb from '../helpers/local-db';\nimport { constants } from '../helpers/constants';\nimport { getServer, getSocket } from '../helpers/server';\nimport { initRemoteServer } from '../helpers/remote-server';\nimport { testEnv } from '../helpers/test';\nimport * as redis from '../helpers/redis';\nimport { initCloudDatabase } from '../helpers/cloud';\n\n// Just dummy jest module implementation to be able to use common mocked models in UTests and ITests\nconst dummyJest = (factory: Function) => {\n  if (!factory) {\n    const dummyMock = () => {};\n\n    dummyMock.mockReturnThis = dummyJest;\n    dummyMock.mockReturnValue = dummyJest;\n    dummyMock.mockResolvedValue = dummyJest;\n    dummyMock.mockImplementation = dummyJest;\n\n    return dummyMock;\n  }\n\n  if (typeof factory !== 'function') {\n    return () => factory;\n  }\n\n  return factory;\n};\n\nglobal['jest'] = {\n  // @ts-ignore\n  fn: dummyJest,\n};\n\nglobal['expect'] = {\n  any: () => {},\n};\n\n/**\n * Initialize dependencies\n */\nexport async function depsInit() {\n  // create cloud subscription if needed\n  if (constants.TEST_CLOUD_RTE) {\n    await initCloudDatabase();\n  }\n\n  // initialize analytics module\n  deps.analytics = await getAnalytics();\n\n  await initRemoteServer();\n\n  // initializing backend server\n  deps.server = await getServer();\n\n  // initializing Redis Test Environment\n  deps.rte = await redis.initRTE();\n\n  testEnv.rte = deps.rte.env;\n\n  if (typeof deps.server === 'string') {\n    testEnv.rte.serverType = 'docker';\n  } else {\n    testEnv.rte.serverType = 'local';\n  }\n\n  // initializing local database\n  await localDb.initLocalDb(deps.rte, deps.server);\n}\n\nexport const deps = {\n  localDb,\n  constants,\n  request,\n  expect: chai.expect,\n  server: null,\n  analytics: null,\n  getSocket,\n  rte: null,\n  testEnv,\n};\n"
  },
  {
    "path": "redisinsight/api/test/api/enterprise/POST-redis-enterprise-cluster-get_dbs.test.ts",
    "content": "import { describe, it, deps, validateApiCall, requirements } from '../deps';\nconst { request, server, constants } = deps;\n\nconst endpoint = () =>\n  request(server).post(`/redis-enterprise/cluster/get-databases`);\n\n//todo: add response\n//{\n//     uid: 1,\n//     name: 'testdb',\n//     dnsName: 'redis-12010.cluster.local',\n//     address: '192.168.16.2',\n//     port: 12010,\n//     status: 'active',\n//     tls: false,\n//     modules: [],\n//     options: {\n//       enabledDataPersistence: false,\n//       persistencePolicy: 'none',\n//       enabledRedisFlash: false,\n//       enabledReplication: false,\n//       enabledBackup: false,\n//       enabledActiveActive: false,\n//       enabledClustering: true,\n//       isReplicaDestination: false,\n//       isReplicaSource: false\n//     }\n//   }\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('POST /redis-enterprise/cluster/get-databases', () => {\n  requirements('rte.re');\n\n  [\n    {\n      name: 'Should connect to a database',\n      data: {\n        host: constants.TEST_RE_HOST,\n        port: constants.TEST_RE_PORT,\n        password: constants.TEST_RE_PASS,\n        username: constants.TEST_RE_USER,\n        uids: [1],\n      },\n    },\n    {\n      name: 'Should return error if incorrect re credentials passed',\n      data: {\n        host: constants.TEST_RE_HOST,\n        port: constants.TEST_RE_PORT,\n        password: constants.TEST_RE_PASS + 1,\n        username: constants.TEST_RE_USER + 1,\n        uids: [1],\n      },\n      // todo: why 403 when it should be 401???\n      statusCode: 403,\n      responseBody: {\n        statusCode: 403,\n        error: 'Forbidden',\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/feature/GET-features.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  getMainCheckFn,\n  fsExtra,\n  before,\n  after,\n} from '../deps';\nimport { constants } from '../../helpers/constants';\nimport * as defaultConfig from '../../../config/features-config.json';\nimport {\n  getRepository,\n  initSettings,\n  repositories,\n} from '../../helpers/local-db';\nconst { getSocket, server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/features');\nconst syncEndpoint = () => request(server).post('/features/sync');\nconst updateSettings = (data) => request(server).patch('/settings').send(data);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst waitForFlags = async (flags: any, action?: Function) => {\n  const client = await getSocket('');\n\n  await new Promise((res, rej) => {\n    try {\n      action?.()?.catch(rej);\n    } catch (e) {\n      rej(e);\n    }\n\n    client.once('features', (data) => {\n      expect(flags.features).to.deep.eq(data.features);\n      res(true);\n    });\n    setTimeout(() => {\n      rej(new Error('no flags received in 10s'));\n    }, 10000);\n  });\n};\n\nlet featureConfigRepository;\nlet featureRepository;\ndescribe('GET /features', () => {\n  after(initSettings);\n\n  before(async () => {\n    await initSettings();\n    featureConfigRepository = await getRepository(repositories.FEATURES_CONFIG);\n    featureRepository = await getRepository(repositories.FEATURE);\n  });\n\n  [\n    {\n      name: 'Should return false flag since no range was defined',\n      before: async () => {\n        await fsExtra\n          .writeFile(\n            constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH,\n            JSON.stringify({\n              version: defaultConfig.version + 1,\n              features: {\n                insightsRecommendations: {\n                  perc: [],\n                  flag: true,\n                },\n                cloudSso: {\n                  perc: [[0, 100]],\n                  flag: true,\n                },\n              },\n            }),\n          )\n          .catch(console.error);\n\n        // remove all configs\n        await featureConfigRepository.deleteAll();\n        await featureRepository.deleteAll();\n        await waitForFlags(\n          {\n            features: {\n              insightsRecommendations: {\n                flag: false,\n                name: 'insightsRecommendations',\n              },\n              cloudSso: {\n                flag: true,\n                name: 'cloudSso',\n              },\n              databaseManagement: {\n                flag: true,\n                name: 'databaseManagement',\n              },\n            },\n          },\n          syncEndpoint,\n        );\n      },\n      statusCode: 200,\n      checkFn: async ({ body }) => {\n        const [config] = await featureConfigRepository.find();\n\n        expect(body.features).to.deep.eq({\n          insightsRecommendations: {\n            flag: false,\n            name: 'insightsRecommendations',\n          },\n          cloudSso: {\n            flag: true,\n            name: 'cloudSso',\n          },\n          databaseManagement: {\n            flag: true,\n            name: 'databaseManagement',\n          },\n        });\n        expect(body.controlNumber).to.eq(config.controlNumber);\n        expect(body.controlGroup).to.be.a('string');\n      },\n    },\n    {\n      name: 'Should return true since controlNumber is inside range',\n      before: async () => {\n        const [config, empty] = await featureConfigRepository.find();\n        expect(empty).to.eq(undefined);\n\n        await fsExtra\n          .writeFile(\n            constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH,\n            JSON.stringify({\n              version: defaultConfig.version + 2,\n              features: {\n                insightsRecommendations: {\n                  perc: [[config.controlNumber - 1, config.controlNumber + 1]],\n                  flag: true,\n                },\n                cloudSso: {\n                  perc: [[0, 100]],\n                  flag: true,\n                },\n              },\n            }),\n          )\n          .catch(console.error);\n\n        // remove all configs\n\n        await waitForFlags(\n          {\n            features: {\n              insightsRecommendations: {\n                flag: true,\n                name: 'insightsRecommendations',\n              },\n              cloudSso: {\n                flag: true,\n                name: 'cloudSso',\n              },\n              databaseManagement: {\n                flag: true,\n                name: 'databaseManagement',\n              },\n            },\n          },\n          syncEndpoint,\n        );\n      },\n      statusCode: 200,\n      responseBody: {\n        features: {\n          insightsRecommendations: {\n            flag: true,\n            name: 'insightsRecommendations',\n          },\n          cloudSso: {\n            flag: true,\n            name: 'cloudSso',\n          },\n          databaseManagement: {\n            flag: true,\n            name: 'databaseManagement',\n          },\n        },\n      },\n    },\n    {\n      name: 'Should return true since controlNumber is inside range and filters are match (analytics=true)',\n      before: async () => {\n        const [config, empty] = await featureConfigRepository.find();\n        expect(empty).to.eq(undefined);\n\n        await fsExtra\n          .writeFile(\n            constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH,\n            JSON.stringify({\n              version: JSON.parse(config.data).version + 1,\n              features: {\n                insightsRecommendations: {\n                  perc: [[config.controlNumber - 1, config.controlNumber + 1]],\n                  flag: true,\n                  filters: [\n                    {\n                      name: 'agreements.analytics',\n                      value: true,\n                      cond: 'eq',\n                    },\n                  ],\n                },\n                cloudSso: {\n                  perc: [[0, 100]],\n                  flag: true,\n                },\n              },\n            }),\n          )\n          .catch(console.error);\n\n        await waitForFlags(\n          {\n            features: {\n              insightsRecommendations: {\n                flag: true,\n                name: 'insightsRecommendations',\n              },\n              cloudSso: {\n                flag: true,\n                name: 'cloudSso',\n              },\n              databaseManagement: {\n                flag: true,\n                name: 'databaseManagement',\n              },\n            },\n          },\n          syncEndpoint,\n        );\n      },\n      statusCode: 200,\n      responseBody: {\n        features: {\n          insightsRecommendations: {\n            flag: true,\n            name: 'insightsRecommendations',\n          },\n          cloudSso: {\n            flag: true,\n            name: 'cloudSso',\n          },\n          databaseManagement: {\n            flag: true,\n            name: 'databaseManagement',\n          },\n        },\n      },\n    },\n    {\n      name: 'Should return false since analytics disabled (triggered by settings change)',\n      before: async () => {\n        await new Promise((res, rej) => {\n          waitForFlags({\n            features: {\n              insightsRecommendations: {\n                flag: false,\n                name: 'insightsRecommendations',\n              },\n              cloudSso: {\n                flag: true,\n                name: 'cloudSso',\n              },\n              databaseManagement: {\n                flag: true,\n                name: 'databaseManagement',\n              },\n            },\n          })\n            .then(res)\n            .catch(rej);\n\n          updateSettings({\n            agreements: {\n              analytics: false,\n            },\n          }).catch(rej);\n        });\n      },\n      statusCode: 200,\n      responseBody: {\n        features: {\n          insightsRecommendations: {\n            flag: false,\n            name: 'insightsRecommendations',\n          },\n          cloudSso: {\n            flag: true,\n            name: 'cloudSso',\n          },\n          databaseManagement: {\n            flag: true,\n            name: 'databaseManagement',\n          },\n        },\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/feature/POST-features-sync.test.ts",
    "content": "import {\n  expect,\n  before,\n  describe,\n  deps,\n  fsExtra,\n  getMainCheckFn,\n  sleep,\n} from '../deps';\nimport { constants } from '../../helpers/constants';\nimport * as defaultConfig from '../../../config/features-config.json';\nimport { getRepository, repositories } from '../../helpers/local-db';\n\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).post('/features/sync');\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nlet featureConfigRepository;\ndescribe('POST /features/sync', () => {\n  before(async () => {\n    featureConfigRepository = await getRepository(repositories.FEATURES_CONFIG);\n    await getRepository(repositories.FEATURE);\n  });\n\n  [\n    {\n      name: 'Should sync with default config when db:null and remote:fail',\n      before: async () => {\n        // remove remote config so BE will get an error during fetching\n        await fsExtra\n          .remove(constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH)\n          .catch(console.error);\n        // remove all configs\n        await featureConfigRepository.deleteAll();\n\n        const [config] = await featureConfigRepository.find();\n        expect(config).to.eq(undefined);\n      },\n      statusCode: 200,\n      checkFn: async () => {\n        const [config, empty] = await featureConfigRepository.find();\n\n        expect(empty).to.eq(undefined);\n        expect(config.controlNumber).to.gte(0).lt(100);\n        expect(config.data).to.eq(JSON.stringify(defaultConfig));\n      },\n    },\n    {\n      name: 'Should sync with default config when db:version < default.version and remote:fail',\n      before: async () => {\n        await fsExtra\n          .remove(constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH)\n          .catch(console.error);\n        await featureConfigRepository.updateAll({\n          data: JSON.stringify({\n            ...defaultConfig,\n            version: defaultConfig.version - 0.1,\n          }),\n        });\n\n        const [config, empty] = await featureConfigRepository.find();\n\n        expect(empty).to.eq(undefined);\n        expect(config.data).to.eq(\n          JSON.stringify({\n            ...defaultConfig,\n            version: defaultConfig.version - 0.1,\n          }),\n        );\n      },\n      statusCode: 200,\n      checkFn: async () => {\n        const [config, empty] = await featureConfigRepository.find();\n\n        expect(empty).to.eq(undefined);\n        expect(config.controlNumber).to.gte(0).lt(100);\n        expect(config.data).to.eq(JSON.stringify(defaultConfig));\n      },\n    },\n    {\n      name: 'Should sync with remote config when db:null and remote:version > default.version',\n      before: async () => {\n        await fsExtra\n          .writeFile(\n            constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH,\n            JSON.stringify({\n              ...defaultConfig,\n              version: defaultConfig.version + 3.33,\n            }),\n          )\n          .catch(console.error);\n\n        // remove all configs\n        await featureConfigRepository.deleteAll();\n\n        const [config] = await featureConfigRepository.find();\n\n        expect(config).to.eq(undefined);\n\n        // flaky test. wait for a while\n        await sleep(1000);\n      },\n      statusCode: 200,\n      checkFn: async () => {\n        const [config, empty] = await featureConfigRepository.find();\n\n        expect(empty).to.eq(undefined);\n        expect(config.controlNumber).to.gte(0).lt(100);\n        expect(config.data).to.eq(\n          JSON.stringify({\n            ...defaultConfig,\n            version: defaultConfig.version + 3.33,\n          }),\n        );\n      },\n    },\n    {\n      name: 'Should sync with remote config when db:version < default and remote:version > default',\n      before: async () => {\n        await fsExtra\n          .writeFile(\n            constants.TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH,\n            JSON.stringify({\n              ...defaultConfig,\n              version: defaultConfig.version + 1.11,\n            }),\n          )\n          .catch(console.error);\n        // remove all configs\n        await featureConfigRepository.updateAll({\n          data: JSON.stringify({\n            ...defaultConfig,\n            version: defaultConfig.version - 0.1,\n          }),\n        });\n\n        const [config, empty] = await featureConfigRepository.find();\n\n        expect(empty).to.eq(undefined);\n        expect(config.data).to.eq(\n          JSON.stringify({\n            ...defaultConfig,\n            version: defaultConfig.version - 0.1,\n          }),\n        );\n      },\n      statusCode: 200,\n      checkFn: async () => {\n        const [config, empty] = await featureConfigRepository.find();\n\n        expect(empty).to.eq(undefined);\n        expect(config.controlNumber).to.gte(0).lt(100);\n        expect(config.data).to.eq(\n          JSON.stringify({\n            ...defaultConfig,\n            version: defaultConfig.version + 1.11,\n          }),\n        );\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/hash/DELETE-databases-id-hash-fields.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\nimport * as Joi from 'joi';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/hash/fields`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  fields: Joi.array().items(Joi.any()).required(), // todo: investigate BE validation\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  fields: [constants.getRandomString()],\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/hash/fields', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should remove hash field from buff',\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_BUF_OBJ_1,\n          fields: [constants.TEST_HASH_FIELD_BIN_BUF_OBJ_1],\n        },\n        responseBody: {\n          affected: 1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_HASH_KEY_BIN_BUFFER_1),\n          ).to.eql(0);\n        },\n      },\n      {\n        name: 'Should remove hash field from ascii',\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_ASCII_1,\n          fields: [constants.TEST_HASH_FIELD_BIN_ASCII_1],\n        },\n        responseBody: {\n          affected: 1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_HASH_KEY_BIN_BUFFER_1),\n          ).to.eql(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should ignore not existing field',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            fields: [constants.getRandomString()],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 0,\n          },\n          after: async () => {\n            const fields = convertArrayReplyToObject(\n              await rte.client.hgetall(constants.TEST_HASH_KEY_2),\n            );\n            new Array(3000).fill(0).map((_, i) => {\n              expect(fields[`field_${i + 1}`]).to.eql(`value_${i + 1}`);\n            });\n          },\n        },\n        {\n          name: 'Should remove 1 field',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            fields: ['field_3000'],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          after: async () => {\n            const fields = convertArrayReplyToObject(\n              await rte.client.hgetall(constants.TEST_HASH_KEY_2),\n            );\n            new Array(2999).fill(0).map((_, i) => {\n              expect(fields[`field_${i + 1}`]).to.eql(`value_${i + 1}`);\n            });\n          },\n        },\n        {\n          name: 'Should remove multiple fields',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            fields: ['field_2999', 'field_2998', 'field_1', 'field_2'],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 4,\n          },\n          after: async () => {\n            const fields = convertArrayReplyToObject(\n              await rte.client.hgetall(constants.TEST_HASH_KEY_2),\n            );\n            new Array(2995).fill(0).map((_, i) => {\n              expect(fields[`field_${i + 3}`]).to.eql(`value_${i + 3}`);\n            });\n          },\n        },\n        {\n          name: 'Should remove all fields and the key',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            fields: [\n              ...new Array(2995).fill(0).map((_, i) => `field_${i + 3}`),\n            ],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 2995,\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_HASH_KEY_2)).to.eql(\n              0,\n            );\n          },\n        },\n        {\n          name: 'Should return BadRequest error if try to modify incorrect data type',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should not delete member',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [constants.getRandomString()],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 0,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"hdel\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -hdel'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/hash/POST-databases-id-hash-get_fields.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\nimport * as Joi from 'joi';\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/hash/get-fields`,\n  );\n\n// input data schema // todo: review BE for transform true -> 1\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  cursor: Joi.number().integer().min(0).allow(true).required().messages({\n    'any.required': 'cursor should not be empty',\n  }),\n  count: Joi.number().integer().min(1).allow(true, null).messages({\n    'any.required': 'count should not be empty',\n  }),\n  match: Joi.string().allow(null),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  cursor: 0,\n  count: 1,\n  match: constants.getRandomString(),\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: Joi.string().required(),\n    total: Joi.number().integer().required(),\n    fields: Joi.array().items(\n      Joi.object().keys({\n        field: Joi.string().required(),\n        value: Joi.string().required(),\n      }),\n    ),\n    nextCursor: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/hash/get-fields', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should find by buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_BUF_OBJ_1,\n          cursor: 0,\n          count: 15,\n          match: constants.TEST_HASH_FIELD_BIN_UTF8_1.slice(0, -10) + '*',\n        },\n        responseBody: {\n          keyName: constants.TEST_HASH_KEY_BIN_UTF8_1,\n          total: 1,\n          nextCursor: 0,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_UTF8_1,\n              value: constants.TEST_HASH_VALUE_BIN_UTF8_1,\n            },\n          ],\n        },\n      },\n      {\n        name: 'Should find by buff (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_BUF_OBJ_1,\n          cursor: 0,\n          count: 15,\n          match: constants.TEST_HASH_FIELD_BIN_UTF8_1.slice(0, -10) + '*',\n        },\n        responseBody: {\n          keyName: constants.TEST_HASH_KEY_BIN_BUF_OBJ_1,\n          total: 1,\n          nextCursor: 0,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_BUF_OBJ_1,\n              value: constants.TEST_HASH_VALUE_BIN_BUF_OBJ_1,\n            },\n          ],\n        },\n      },\n      {\n        name: 'Should find by ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_ASCII_1,\n          cursor: 0,\n          count: 15,\n          match: constants.TEST_HASH_FIELD_BIN_ASCII_1.slice(0, -20) + '*',\n        },\n        responseBody: {\n          keyName: constants.TEST_HASH_KEY_BIN_ASCII_1,\n          total: 1,\n          nextCursor: 0,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_ASCII_1,\n              value: constants.TEST_HASH_VALUE_BIN_ASCII_1,\n            },\n          ],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should find by exact match',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'field_9',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_HASH_KEY_2);\n            expect(body.total).to.eql(3000);\n            expect(body.fields.length).to.eql(1);\n            expect(body.fields[0].field).to.eql('field_9');\n            expect(body.fields[0].value).to.eql('value_9');\n          },\n        },\n        {\n          name: 'Should not find any field',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'field_9asd*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_HASH_KEY_2);\n            expect(body.total).to.eql(3000);\n            expect(body.fields.length).to.eql(0);\n          },\n        },\n        {\n          name: 'Should query 15 fields',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            cursor: 0,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_HASH_KEY_2);\n            expect(body.total).to.eql(3000);\n            expect(body.fields.length).to.gte(15);\n            expect(body.fields.length).to.lt(3000);\n          },\n        },\n        {\n          name: 'Should query by * in the end',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'field_219*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_HASH_KEY_2);\n            expect(body.total).to.eql(3000);\n            expect(body.fields.length).to.eq(11);\n          },\n        },\n        {\n          name: 'Should query by * in the beginning',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: '*eld_9',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_HASH_KEY_2);\n            expect(body.total).to.eql(3000);\n            expect(body.fields.length).to.eq(1);\n            expect(body.fields[0].field).to.eql('field_9');\n            expect(body.fields[0].value).to.eql('value_9');\n          },\n        },\n        {\n          name: 'Should query by * in the middle',\n          data: {\n            keyName: constants.TEST_HASH_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'f*eld_9',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_HASH_KEY_2);\n            expect(body.total).to.eql(3000);\n            expect(body.fields.length).to.eq(1);\n            expect(body.fields[0].field).to.eql('field_9');\n            expect(body.fields[0].value).to.eql('value_9');\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            cursor: 0,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            cursor: 0,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n\n      describe('Search in huge number of fields', () => {\n        requirements('rte.bigData');\n        // number of hash fields inside existing data (1M fields)\n        const NUMBER_OF_FIELDS = 1_000_000;\n\n        [\n          {\n            name: 'Should find exact one key',\n            data: {\n              keyName: constants.TEST_HASH_HUGE_KEY,\n              cursor: 0,\n              count: 15,\n              match: constants.TEST_HASH_HUGE_KEY_FIELD,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.keyName).to.eql(constants.TEST_HASH_HUGE_KEY);\n              expect(body.total).to.eql(NUMBER_OF_FIELDS);\n              expect(body.fields.length).to.eq(1);\n              expect(body.fields[0].field).to.eql(\n                constants.TEST_HASH_HUGE_KEY_FIELD,\n              );\n              expect(body.fields[0].value).to.eql(\n                constants.TEST_HASH_HUGE_KEY_VALUE,\n              );\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should not delete member',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            cursor: 0,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"hlen\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            cursor: 0,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -hlen'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"hget\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            cursor: 0,\n            match: 'asd',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -hget'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"hscan\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            cursor: 0,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -hscan'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/hash/POST-databases-id-hash.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\nimport * as Joi from 'joi';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/hash`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  fields: Joi.array()\n    .items(\n      Joi.object().keys({\n        field: Joi.string().allow('').label('.field'),\n        value: Joi.string().allow('').label('.value'),\n      }),\n    )\n    .required()\n    .messages({\n      'array.sparse': 'fields must be either object or array',\n      'array.base': 'property {#label} must be either object or array',\n    }),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_HASH_KEY_1,\n  fields: [\n    {\n      field: constants.TEST_HASH_FIELD_1_NAME,\n      value: constants.TEST_HASH_FIELD_1_VALUE,\n    },\n  ],\n  expire: constants.TEST_HASH_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst createCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      }\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n        expect(\n          convertArrayReplyToObject(\n            await rte.client.hgetall(testCase.data.keyName),\n          ),\n        ).to.eql({\n          [testCase.data.fields[0].field]: testCase.data.fields[0].value,\n        });\n        if (testCase.data.expire) {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.gte(\n            testCase.data.expire - 5,\n          );\n        } else {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.eql(-1);\n        }\n      }\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/hash', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should hash from buff',\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_BUF_OBJ_1,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_BUF_OBJ_1,\n              value: constants.TEST_HASH_VALUE_BIN_BUF_OBJ_1,\n            },\n          ],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_HASH_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'hscan',\n              [constants.TEST_HASH_KEY_BIN_BUFFER_1, 0],\n              null,\n            ),\n          ).to.deep.eq([\n            Buffer.from('0'),\n            [\n              constants.TEST_HASH_FIELD_BIN_BUFFER_1,\n              constants.TEST_HASH_VALUE_BIN_BUFFER_1,\n            ],\n          ]);\n        },\n      },\n      {\n        name: 'Should hash from ascii',\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_ASCII_1,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_ASCII_1,\n              value: constants.TEST_HASH_VALUE_BIN_ASCII_1,\n            },\n          ],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_HASH_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'hscan',\n              [constants.TEST_HASH_KEY_BIN_BUFFER_1, 0],\n              null,\n            ),\n          ).to.deep.eq([\n            Buffer.from('0'),\n            [\n              constants.TEST_HASH_FIELD_BIN_BUFFER_1,\n              constants.TEST_HASH_VALUE_BIN_BUFFER_1,\n            ],\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should create item with empty value',\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [\n              {\n                field: '',\n                value: '',\n              },\n            ],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with key ttl',\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n            expire: constants.TEST_HASH_EXPIRE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create regular item',\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.TEST_HASH_FIELD_1_NAME,\n                value: constants.TEST_HASH_FIELD_1_VALUE,\n              },\n            ],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should return conflict error if key already exists',\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n            message: 'This key name is already in use.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              convertArrayReplyToObject(\n                await rte.client.hgetall(constants.TEST_HASH_KEY_1),\n              ),\n            ).to.deep.eql({\n              [constants.TEST_HASH_FIELD_1_NAME]:\n                constants.TEST_HASH_FIELD_1_VALUE,\n            }),\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(createCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"hset\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -hset'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(createCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/hash/PUT-databases-id-hash.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\nimport * as Joi from 'joi';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).put(`/${constants.API.DATABASES}/${instanceId}/hash`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  fields: Joi.array()\n    .items(\n      Joi.object().keys({\n        field: Joi.string().allow('').label('.field'),\n        value: Joi.string().allow('').label('.value'),\n      }),\n    )\n    .required()\n    .messages({\n      'array.sparse': 'fields must be either object or array',\n      'array.base': 'property {#label} must be either object or array',\n    }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  fields: [\n    {\n      field: constants.TEST_HASH_FIELD_1_NAME,\n      value: constants.TEST_HASH_FIELD_1_VALUE,\n    },\n  ],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PUT /databases/:instanceId/hash', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should edit hash from buff',\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_BUF_OBJ_1,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_BUF_OBJ_1,\n              value: constants.TEST_STRING_VALUE_BIN_BUF_OBJ_1,\n            },\n          ],\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_HASH_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'hscan',\n              [constants.TEST_HASH_KEY_BIN_BUFFER_1, 0],\n              null,\n            ),\n          ).to.deep.eq([\n            Buffer.from('0'),\n            [\n              constants.TEST_HASH_FIELD_BIN_BUFFER_1,\n              constants.TEST_STRING_VALUE_BIN_BUFFER_1,\n            ],\n          ]);\n        },\n      },\n      {\n        name: 'Should edit hash from ascii',\n        data: {\n          keyName: constants.TEST_HASH_KEY_BIN_ASCII_1,\n          fields: [\n            {\n              field: constants.TEST_HASH_FIELD_BIN_ASCII_1,\n              value: constants.TEST_STRING_VALUE_BIN_ASCII_1,\n            },\n          ],\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_HASH_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'hscan',\n              [constants.TEST_HASH_KEY_BIN_BUFFER_1, 0],\n              null,\n            ),\n          ).to.deep.eq([\n            Buffer.from('0'),\n            [\n              constants.TEST_HASH_FIELD_BIN_BUFFER_1,\n              constants.TEST_STRING_VALUE_BIN_BUFFER_1,\n            ],\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should add new field and edit existing value',\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.TEST_HASH_FIELD_1_NAME,\n                value: '',\n              },\n              {\n                field: 'new_field',\n                value: 'new_value',\n              },\n            ],\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              convertArrayReplyToObject(\n                await rte.client.hgetall(constants.TEST_HASH_KEY_1),\n              ),\n            ).to.eql({\n              [constants.TEST_HASH_FIELD_1_NAME]: '',\n              [constants.TEST_HASH_FIELD_2_NAME]:\n                constants.TEST_HASH_FIELD_2_VALUE,\n              ['new_field']: 'new_value',\n            });\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"hset\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -hset'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            fields: [\n              {\n                field: constants.getRandomString(),\n                value: constants.getRandomString(),\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/info/GET-health.test.ts",
    "content": "import { expect, describe, deps, Joi, getMainCheckFn } from '../deps';\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/health');\n\nconst responseSchema = Joi.object()\n  .keys({\n    status: Joi.string().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /health', () => {\n  [\n    {\n      name: 'Should return server health',\n      statusCode: 200,\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body.status).to.eql('up');\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/info/GET-info-cli-blocking-commands.test.ts",
    "content": "import { describe, it, deps, Joi, validateApiCall } from '../deps';\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/info/cli-blocking-commands');\n\nconst responseSchema = Joi.array().items(Joi.string());\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /info/cli-blocking-commands', () => {\n  [\n    {\n      name: 'Should return array with blocking Redis commands',\n      statusCode: 200,\n      responseSchema,\n      responseBody: [\n        'blpop',\n        'brpop',\n        'blmove',\n        'brpoplpush',\n        'bzpopmin',\n        'bzpopmax',\n        'xread',\n        'xreadgroup',\n      ],\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/info/GET-info-cli-unsupported-commands.test.ts",
    "content": "import { describe, it, deps, Joi, validateApiCall } from '../deps';\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/info/cli-unsupported-commands');\n\nconst responseSchema = Joi.array().items(Joi.string());\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /info/cli-unsupported-commands', () => {\n  [\n    {\n      name: 'Should return array with unsupported commands for CLI tool',\n      statusCode: 200,\n      responseSchema,\n      responseBody: [\n        'monitor',\n        'subscribe',\n        'psubscribe',\n        'ssubscribe',\n        'sync',\n        'psync',\n        'script debug',\n        'hello 3',\n      ],\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/info/GET-info.test.ts",
    "content": "import { expect, describe, it, deps, Joi, validateApiCall } from '../deps';\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/info');\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    createDateTime: Joi.date().required(),\n    appVersion: Joi.string().required(),\n    osPlatform: Joi.string().required(),\n    buildType: Joi.string()\n      .valid('ELECTRON', 'DOCKER_ON_PREMISE', 'REDIS_STACK')\n      .required(),\n    appType: Joi.string()\n      .valid('ELECTRON', 'DOCKER', 'REDIS_STACK_WEB', 'UNKNOWN')\n      .required(),\n    encryptionStrategies: Joi.array().items(Joi.string()),\n    sessionId: Joi.number().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /info', () => {\n  [\n    {\n      name: 'Should return server info',\n      statusCode: 200,\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body.osPlatform).to.eql(process.platform);\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/keys/DELETE-databases-id-keys.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(`/${constants.API.DATABASES}/${instanceId}/keys`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyNames: Joi.array().items(Joi.string().allow('')).required().messages({\n    'string.base': 'each value in keyNames must be a string',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyNames: [constants.TEST_LIST_KEY_1],\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst deleteCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    if (testCase.before) {\n      await testCase.before();\n    } else if (testCase.statusCode < 300) {\n      testCase.data.keyNames.map(async (keyName) => {\n        expect(await rte.client.exists(keyName)).to.eql(1);\n      });\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      testCase.data.keyNames.map(async (keyName) => {\n        expect(await rte.client.exists(keyName)).to.eql(0);\n      });\n    }\n  });\n};\n\ndescribe('DELETE /databases/:instanceId/keys', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should process ascii input',\n        data: {\n          keyNames: [constants.TEST_STRING_KEY_BIN_ASCII_1],\n        },\n        responseSchema,\n        responseBody: {\n          affected: 1,\n        },\n      },\n      {\n        name: 'Should process buffer input',\n        data: {\n          keyNames: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1],\n        },\n        responseSchema,\n        responseBody: {\n          affected: 1,\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyNames: [constants.TEST_STRING_KEY_BIN_UTF8_1],\n        },\n        statusCode: 404,\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Rest', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    // todo: investigate BE validation pipe with transform:true flag. Seems like works incorrect\n    xdescribe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n    describe('Common', () => {\n      [\n        {\n          name: 'Should remove string',\n          data: {\n            keyNames: [constants.TEST_STRING_KEY_1],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n        },\n        {\n          name: 'Should remove list',\n          data: {\n            keyNames: [constants.TEST_LIST_KEY_1],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n        },\n        {\n          name: 'Should remove set',\n          data: {\n            keyNames: [constants.TEST_SET_KEY_1],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n        },\n        {\n          name: 'Should remove zset',\n          data: {\n            keyNames: [constants.TEST_ZSET_KEY_1],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n        },\n        {\n          name: 'Should remove hash',\n          data: {\n            keyNames: [constants.TEST_HASH_KEY_1],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n        },\n        {\n          name: 'Should remove multiple keys',\n          data: {\n            keyNames: [\n              constants.TEST_STRING_KEY_1,\n              constants.TEST_LIST_KEY_1,\n              constants.TEST_SET_KEY_1,\n              constants.TEST_ZSET_KEY_1,\n              constants.TEST_HASH_KEY_1,\n            ],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 5,\n          },\n          before: async function () {\n            // generate already deleted keys again\n            await rte.data.generateKeys(true);\n            this.data.keyNames.map(async (keyName) => {\n              expect(await rte.client.exists(keyName)).to.eql(1);\n            });\n          },\n        },\n        {\n          name: 'Should return NotFound error for not existing error',\n          data: {\n            keyNames: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          // todo: investigate error payload. Seems that missed fields and wrong message\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n      ].map(deleteCheckFn);\n\n      describe('ReJSON-RL', () => {\n        requirements('rte.modules.rejson');\n        [\n          {\n            name: 'Should remove ReJSON',\n            data: {\n              keyNames: [constants.TEST_REJSON_KEY_1],\n            },\n            responseSchema,\n            responseBody: {\n              affected: 1,\n            },\n          },\n        ].map(deleteCheckFn);\n      });\n    });\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should remove key',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyNames: [constants.TEST_STRING_KEY_1],\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"del\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyNames: [constants.TEST_STRING_KEY_1],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -del'),\n        },\n      ].map(deleteCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/keys/PATCH-databases-id-keys-name.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(`/${constants.API.DATABASES}/${instanceId}/keys/name`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  newKeyName: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  newKeyName: constants.getRandomString(),\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst renameCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n      expect(await rte.client.exists(testCase.data.newKeyName)).to.eql(0);\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      expect(await rte.client.exists(testCase.data.newKeyName)).to.eql(1);\n    }\n  });\n};\n\ndescribe('PATCH /databases/:instanceId/keys/name', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateKeys(true));\n\n    [\n      {\n        name: 'Should rename utf8 to buf and return utf8 (by default)',\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n          newKeyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.keyName).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1);\n        },\n      },\n      {\n        name: 'Should rename buf to utf8 and return utf8',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          newKeyName: constants.TEST_STRING_KEY_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.keyName).to.eq(constants.TEST_STRING_KEY_1);\n        },\n      },\n      {\n        name: 'Should rename utf8 to ascii and return buffer',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n          newKeyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.keyName).to.deep.eq(\n            constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          );\n        },\n      },\n      {\n        name: 'Should rename ASCII to utf8 and return utf8',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n          newKeyName: constants.TEST_STRING_KEY_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.keyName).to.eq(constants.TEST_STRING_KEY_1);\n        },\n      },\n      {\n        name: 'Should rename utf8 to buf and return ascii',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n          newKeyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.keyName).to.eq(constants.TEST_STRING_KEY_BIN_ASCII_1);\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n          newKeyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n        },\n        statusCode: 404,\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Rest', () => {\n    before(async () => await rte.data.generateKeys(true));\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n    describe('Common', () => {\n      [\n        {\n          name: 'Should rename string',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            newKeyName:\n              constants.getRandomString() + constants.CLUSTER_HASH_SLOT,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should rename list',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            newKeyName:\n              constants.getRandomString() + constants.CLUSTER_HASH_SLOT,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should rename set',\n          data: {\n            keyName: constants.TEST_SET_KEY_1,\n            newKeyName:\n              constants.getRandomString() + constants.CLUSTER_HASH_SLOT,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should rename zset',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            newKeyName:\n              constants.getRandomString() + constants.CLUSTER_HASH_SLOT,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should rename hash',\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            newKeyName:\n              constants.getRandomString() + constants.CLUSTER_HASH_SLOT,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should return NotFound error for not existing error',\n          data: {\n            keyName: constants.getRandomString(),\n            newKeyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n          before: async function () {\n            expect(await rte.client.exists(this.data.keyName)).to.eql(0);\n            expect(await rte.client.exists(this.data.newKeyName)).to.eql(0);\n          },\n          after: async function () {\n            expect(await rte.client.exists(this.data.keyName)).to.eql(0);\n            expect(await rte.client.exists(this.data.newKeyName)).to.eql(0);\n          },\n        },\n      ].map(renameCheckFn);\n\n      describe('ReJSON-RL', () => {\n        requirements('rte.modules.rejson');\n        [\n          {\n            name: 'Should rename ReJSON',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_1,\n              newKeyName: constants.getRandomString(),\n            },\n          },\n        ].map(renameCheckFn);\n      });\n    });\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => await rte.data.generateKeys(true));\n\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should rename key',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            newKeyName:\n              constants.getRandomString() + constants.CLUSTER_HASH_SLOT,\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            newKeyName: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n          after: async function () {\n            expect(await rte.client.exists(this.data.keyName)).to.eql(1);\n            expect(await rte.client.exists(this.data.newKeyName)).to.eql(0);\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"renamenx\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            newKeyName: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -renamenx'),\n          after: async function () {\n            expect(await rte.client.exists(this.data.keyName)).to.eql(1);\n            expect(await rte.client.exists(this.data.newKeyName)).to.eql(0);\n          },\n        },\n      ].map(renameCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/keys/PATCH-databases-id-keys-ttl.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(`/${constants.API.DATABASES}/${instanceId}/keys/ttl`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  ttl: Joi.number().integer().max(2147483647).required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  ttl: 12,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    ttl: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/keys/ttl', () => {\n  before(async () => await rte.data.generateKeys(true));\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(rte.data.generateBinKeys);\n\n    [\n      {\n        name: 'Should process ascii input',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n          ttl: 300,\n        },\n        responseSchema,\n        after: async () => {\n          expect(\n            await rte.client.ttl(constants.TEST_STRING_KEY_BIN_BUFFER_1),\n          ).to.gte(300 - 5);\n        },\n      },\n      {\n        name: 'Should process buffer input',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          ttl: 600,\n        },\n        responseSchema,\n        after: async () => {\n          expect(\n            await rte.client.ttl(constants.TEST_STRING_KEY_BIN_BUFFER_1),\n          ).to.gte(600 - 5);\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n          ttl: 600,\n        },\n        statusCode: 404,\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should set ttl for key',\n        data: {\n          keyName: constants.TEST_STRING_KEY_2,\n          ttl: 300,\n        },\n        responseSchema,\n        after: async () => {\n          expect(await rte.client.ttl(constants.TEST_STRING_KEY_2)).to.gte(\n            300 - 5,\n          );\n        },\n      },\n      {\n        name: 'Should remove ttl for key',\n        data: {\n          keyName: constants.TEST_STRING_KEY_2,\n          ttl: -1,\n        },\n        responseSchema,\n        after: async () => {\n          expect(await rte.client.ttl(constants.TEST_STRING_KEY_2)).to.eql(-1);\n        },\n      },\n      {\n        name: 'Should return NotFound error for not existing key error',\n        data: {\n          keyName: constants.getRandomString(),\n          ttl: 12,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Key with this name does not exist.',\n        },\n      },\n    ].map(mainCheckFn);\n\n    describe('ReJSON-RL', () => {\n      requirements('rte.modules.rejson');\n      [\n        {\n          name: 'Should set ttl for ReJSON',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            ttl: 3,\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => await rte.data.generateKeys(true));\n\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should set ttl for key',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n          ttl: 10,\n        },\n        after: async () => {\n          expect(await rte.client.ttl(constants.TEST_STRING_KEY_1)).to.eql(10);\n        },\n      },\n      {\n        name: 'Should throw error if no permissions for \"persist\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n          ttl: -1,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -persist'),\n      },\n      {\n        name: 'Should throw error if no permissions for \"expire\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_LIST_KEY_1,\n          ttl: 30,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -expire'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/keys/POST-databases-id-keys-get_info.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/keys/get-info`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_LIST_KEY_1,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    name: JoiRedisString.required(),\n    type: Joi.string().required(),\n    ttl: Joi.number().integer().allow(null).optional(),\n    size: Joi.number().integer().allow(null).optional(),\n    length: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('POST /databases/:instanceId/keys/get-info', () => {\n  before(async () => await rte.data.generateKeys(true));\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(rte.data.generateBinKeys);\n\n    [\n      {\n        name: 'Should return string info in utf8 (default)',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.name).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1);\n        },\n      },\n      {\n        name: 'Should return string info in utf8',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.name).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1);\n        },\n      },\n      {\n        name: 'Should return string info in ASCII',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.name).to.eq(constants.TEST_STRING_KEY_BIN_ASCII_1);\n        },\n      },\n      {\n        name: 'Should return string info in Buffer',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.name).to.deep.eq(constants.TEST_STRING_KEY_BIN_BUF_OBJ_1);\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n        },\n        statusCode: 404,\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return string info',\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n        },\n        responseSchema,\n        responseBody: {\n          name: constants.TEST_STRING_KEY_1,\n          type: constants.TEST_STRING_TYPE,\n          ttl: -1,\n          length: constants.TEST_STRING_VALUE_1.length,\n        },\n      },\n      {\n        name: 'Should return list info',\n        data: {\n          keyName: constants.TEST_LIST_KEY_1,\n        },\n        responseSchema,\n        responseBody: {\n          name: constants.TEST_LIST_KEY_1,\n          type: constants.TEST_LIST_TYPE,\n          ttl: -1,\n          length: 2,\n        },\n      },\n      {\n        name: 'Should return set info',\n        data: {\n          keyName: constants.TEST_SET_KEY_1,\n        },\n        responseSchema,\n        responseBody: {\n          name: constants.TEST_SET_KEY_1,\n          type: constants.TEST_SET_TYPE,\n          ttl: -1,\n          length: 1,\n        },\n      },\n      {\n        name: 'Should return zset info',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_1,\n        },\n        responseSchema,\n        responseBody: {\n          name: constants.TEST_ZSET_KEY_1,\n          type: constants.TEST_ZSET_TYPE,\n          ttl: -1,\n          length: 2,\n        },\n      },\n      {\n        name: 'Should return hash info',\n        data: {\n          keyName: constants.TEST_HASH_KEY_1,\n        },\n        responseSchema,\n        responseBody: {\n          name: constants.TEST_HASH_KEY_1,\n          type: constants.TEST_HASH_TYPE,\n          ttl: -1,\n          length: 2,\n        },\n      },\n      {\n        name: 'Should return NotFound error for not existing error',\n        data: {\n          keyName: constants.getRandomString(),\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Key with this name does not exist.',\n        },\n      },\n    ].map(mainCheckFn);\n\n    describe('ReJSON-RL', () => {\n      requirements('rte.modules.rejson');\n      [\n        {\n          name: 'Should return ReJSON info',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n          },\n          responseSchema,\n          responseBody: {\n            name: constants.TEST_REJSON_KEY_1,\n            type: constants.TEST_REJSON_TYPE,\n            ttl: -1,\n            length: 1,\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    const mainACLCheckFn = async (testCase) => {\n      it(testCase.name, async () => {\n        if (testCase.before) {\n          await testCase.before();\n        }\n\n        await validateApiCall({\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          ...testCase,\n          checkFn: ({ body }) => {\n            expect(body.ttl).to.be.oneOf([null, undefined]);\n            expect(body.length).to.be.oneOf([null, undefined]);\n            expect(body.size).to.be.oneOf([null, undefined]);\n          },\n        });\n      });\n    };\n\n    [\n      {\n        name: 'Should return key info',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n        },\n        statusCode: 200,\n      },\n      {\n        name: 'Should throw error if no permissions for \"type\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -type'),\n      },\n    ].map(mainCheckFn);\n\n    [\n      {\n        name: 'Should return empty fields if no permission for (ttl, memory, strlen)',\n        data: {\n          keyName: constants.TEST_STRING_KEY_1,\n        },\n        responseBody: {\n          name: constants.TEST_STRING_KEY_1,\n          type: constants.TEST_STRING_TYPE,\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -strlen'),\n      },\n      {\n        name: 'Should return empty fields if no permission for (ttl, memory, llen)',\n        data: {\n          keyName: constants.TEST_LIST_KEY_1,\n        },\n        responseBody: {\n          name: constants.TEST_LIST_KEY_1,\n          type: constants.TEST_LIST_TYPE,\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -llen'),\n      },\n      {\n        name: 'Should return empty fields if no permission for (ttl, memory, scard)',\n        data: {\n          keyName: constants.TEST_SET_KEY_1,\n        },\n        responseBody: {\n          name: constants.TEST_SET_KEY_1,\n          type: constants.TEST_SET_TYPE,\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -scard'),\n      },\n      {\n        name: 'Should return empty fields if no permission for (ttl, memory, zcard)',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_1,\n        },\n        responseBody: {\n          name: constants.TEST_ZSET_KEY_1,\n          type: constants.TEST_ZSET_TYPE,\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -zcard'),\n      },\n      {\n        name: 'Should return empty fields if no permission for (ttl, memory, zcard)',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_1,\n        },\n        responseBody: {\n          name: constants.TEST_ZSET_KEY_1,\n          type: constants.TEST_ZSET_TYPE,\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -zcard'),\n      },\n      {\n        name: 'Should return empty fields if no permission for (ttl, memory, usage, hlen)',\n        data: {\n          keyName: constants.TEST_HASH_KEY_1,\n        },\n        responseBody: {\n          name: constants.TEST_HASH_KEY_1,\n          type: constants.TEST_HASH_TYPE,\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -hlen'),\n      },\n    ].map(mainACLCheckFn);\n    //json.type\n    describe('ReJSON-RL', () => {\n      requirements('rte.modules.rejson');\n\n      [\n        {\n          name: 'Should return empty fields if no permission for (ttl, memory, json.type)',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n          },\n          responseBody: {\n            name: constants.TEST_REJSON_KEY_1,\n            type: constants.TEST_REJSON_TYPE,\n          },\n          before: () =>\n            rte.data.setAclUserRules('~* +@all -ttl -memory -json.type'),\n        },\n      ].map(mainACLCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/keys/POST-databases-id-keys-get_infos.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  validateApiCall,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/keys/get-metadata`,\n  );\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      name: JoiRedisString.required(),\n      type: Joi.string().required(),\n      ttl: Joi.number().integer().allow(null).optional(),\n      size: Joi.number().integer().allow(null).optional(),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('POST /databases/:instanceId/keys/get-metadata', () => {\n  before(async () => await rte.data.generateKeys(true));\n\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(rte.data.generateBinKeys);\n\n    [\n      {\n        name: 'Should not return size if includeSize is false',\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUFFER_1],\n          includeSize: false,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].size).to.eql(undefined);\n        },\n      },\n      {\n        name: 'Should return size if includeSize is true',\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUFFER_1],\n          includeSize: true,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].size).to.be.a('number');\n        },\n      },\n      {\n        name: 'Should not return ttl if includeTTL is false',\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUFFER_1],\n          includeTTL: false,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].ttl).to.eql(undefined);\n        },\n      },\n      {\n        name: 'Should return ttl if includeTTL is true',\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUFFER_1],\n          includeTTL: true,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].ttl).to.be.a('number');\n        },\n      },\n      {\n        name: 'Should return string info in utf8 (default)',\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1],\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].name).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1);\n        },\n      },\n      {\n        name: 'Should return string info in utf8',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1],\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].name).to.eq(constants.TEST_STRING_KEY_BIN_UTF8_1);\n        },\n      },\n      {\n        name: 'Should return string info in ASCII',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_ASCII_1],\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].name).to.eq(constants.TEST_STRING_KEY_BIN_ASCII_1);\n        },\n      },\n      {\n        name: 'Should return string info in Buffer',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1],\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].name).to.deep.eq(\n            constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          );\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_UTF8_1],\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].name).to.deep.eq(constants.TEST_STRING_KEY_BIN_UTF8_1);\n          expect(body[0].ttl).to.be.oneOf([-2, undefined]);\n          expect(body[0].size).to.be.oneOf([null, undefined]);\n          expect(body[0].type).to.deep.eq('none');\n        },\n      },\n      {\n        name: 'Should return string info in Buffer and Type',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keys: [constants.TEST_STRING_KEY_BIN_BUF_OBJ_1],\n          type: constants.TEST_LIST_TYPE,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body[0].name).to.deep.eq(\n            constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          );\n          expect(body[0].type).to.deep.eq(constants.TEST_LIST_TYPE);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should return key info',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keys: [constants.TEST_STRING_KEY_1],\n        },\n      },\n      {\n        name: 'Should not throw error if no acl permissions',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        before: () => rte.data.setAclUserRules('~* +@all -ttl -memory -type'),\n        data: {\n          keys: [constants.TEST_STRING_KEY_1],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/keys/POST-databases-id-keys.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  _,\n  requirements,\n  validateApiCall,\n  JoiRedisString,\n} from '../deps';\nimport { initSettings } from '../../helpers/local-db';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/keys`);\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object()\n      .keys({\n        total: Joi.number().integer().required(),\n        scanned: Joi.number().integer().required(),\n        cursor: Joi.number().integer().required(),\n        host: Joi.string(),\n        port: Joi.number().integer(),\n        keys: Joi.array()\n          .items(\n            Joi.object().keys({\n              name: JoiRedisString.required(),\n              type: Joi.string(),\n              ttl: Joi.number().integer(),\n              size: Joi.number().allow(null), // todo: fix size pipeline for cluster\n            }),\n          )\n          .required(),\n      })\n      .required(),\n  )\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\nconst isKeyInResponse = (body, keyName) =>\n  _.find(body, (nodeKeys) =>\n    _.find(nodeKeys.keys, (key) => _.isEqual(key.name, keyName)),\n  );\n\ndescribe('POST /databases/:id/keys', () => {\n  // todo: add query validation\n  xdescribe('Validation', () => {});\n\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should return all keys in utf-8 (by default)',\n        data: {\n          count: 10_000,\n          cursor: '0',\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(\n            isKeyInResponse(body, constants.TEST_STRING_KEY_BIN_UTF8_1),\n          ).to.not.eq(undefined);\n        },\n      },\n      {\n        name: 'Should return all keys in utf-8',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          count: 10_000,\n          cursor: '0',\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(\n            isKeyInResponse(body, constants.TEST_STRING_KEY_BIN_UTF8_1),\n          ).to.not.eq(undefined);\n        },\n      },\n      {\n        name: 'Should return all keys in ascii',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          count: 10_000,\n          cursor: '0',\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(\n            isKeyInResponse(body, constants.TEST_STRING_KEY_BIN_ASCII_1),\n          ).to.not.eq(undefined);\n        },\n      },\n      {\n        name: 'Should return all keys in buffer',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          count: 10_000,\n          cursor: '0',\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(\n            isKeyInResponse(body, constants.TEST_STRING_KEY_BIN_BUF_OBJ_1),\n          ).to.not.eq(undefined);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Sandbox rte', () => {\n    requirements('!rte.sharedData');\n    const KEYS_NUMBER = 1500; // 300 per each base type\n    before(async () => await rte.data.generateNKeys(KEYS_NUMBER, true));\n\n    describe('Search (standalone + cluster)', () => {\n      [\n        {\n          name: 'Should find key by exact name',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_1`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.eq(1);\n            expect(result.keys[0].name).to.eq(\n              `${constants.TEST_RUN_ID}_str_key_1`,\n            );\n          },\n        },\n        {\n          name: 'Should not find key by exact name',\n          data: {\n            cursor: '0',\n            match: 'not_exist_key',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).gte(KEYS_NUMBER);\n            expect(result.keys.length).to.eq(0);\n          },\n        },\n        {\n          name: 'Should prevent full scan in one request',\n          data: {\n            count: 100,\n            cursor: '0',\n            match: 'not_exist_key*',\n            scanThreshold: 500,\n          },\n          responseSchema,\n          after: async () => await initSettings(),\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned)\n              .to.gte(500)\n              .lte((500 + 100) * result.numberOfShards);\n            expect(result.keys.length).to.eql(0);\n          },\n        },\n        {\n          name: 'Should search by with * in the end',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_11*`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(11);\n            result.keys.map(({ name }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_11`),\n              ).to.eql(0);\n            });\n          },\n        },\n        {\n          name: 'Should search by with * in the beginning',\n          data: {\n            cursor: '0',\n            match: '*_key_111',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(5);\n            result.keys.map(({ name }) => {\n              expect(name.indexOf('_key_111')).to.eql(name.length - 8);\n            });\n          },\n        },\n        {\n          name: 'Should search by with * in the middle',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_*_111`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.eq(1);\n            expect(result.keys[0].name).to.eq(\n              `${constants.TEST_RUN_ID}_str_key_111`,\n            );\n          },\n        },\n        {\n          name: 'Should search by with ? in the end',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_10?`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(10);\n            result.keys.map(({ name, type, ttl, size }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`),\n              ).to.eql(0);\n              expect(type).to.be.a('string');\n              expect(ttl).to.be.a('number');\n              expect(size).to.be.a('number');\n            });\n          },\n        },\n        {\n          name: 'Should search by with ? in the end (without keys info)',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_10?`,\n            keysInfo: 'false',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(10);\n            result.keys.map(({ name, type, ttl, size }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`),\n              ).to.eql(0);\n              expect(type).to.eql(undefined);\n              expect(ttl).to.eql(undefined);\n              expect(size).to.eql(undefined);\n            });\n          },\n        },\n        {\n          name: 'Should search by with [a-b] glob pattern',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_10[0-5]`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(1).lte(6);\n            result.keys.map(({ name }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`),\n              ).to.eql(0);\n            });\n          },\n        },\n        {\n          name: 'Should search by with [a,b,c] glob pattern',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_10[0,1,2]`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(1).lte(3);\n            result.keys.map(({ name }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`),\n              ).to.eql(0);\n            });\n          },\n        },\n        {\n          name: 'Should search by with [abc] glob pattern',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_10[012]`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(1).lte(3);\n            result.keys.map(({ name }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`),\n              ).to.eql(0);\n            });\n          },\n        },\n        {\n          name: 'Should search by with [^a] glob pattern',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_str_key_10[^0]`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(9);\n            result.keys.map(({ name }) => {\n              expect(\n                name.indexOf(`${constants.TEST_RUN_ID}_str_key_10`),\n              ).to.eql(0);\n            });\n          },\n        },\n        {\n          name: 'Should search by with combined glob patterns',\n          data: {\n            cursor: '0',\n            match: `${constants.TEST_RUN_ID}_s?r_*_[1][0-5][^0]`,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(KEYS_NUMBER);\n            expect(result.keys.length).to.gte(54);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Standalone', () => {\n      requirements('rte.type=STANDALONE');\n\n      [\n        {\n          name: 'Should scan all types',\n          data: {\n            cursor: '0',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].total).to.eql(KEYS_NUMBER);\n            expect(body[0].scanned).to.eql(200);\n            expect(body[0].cursor).to.not.eql(0);\n            expect(body[0].keys.length).to.gte(200);\n          },\n        },\n        {\n          name: 'Should scan by provided count value',\n          data: {\n            count: 500,\n            cursor: '0',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.gte(500).lte(510);\n            expect(result.keys.length).to.gte(500).lte(510);\n          },\n        },\n      ].map(mainCheckFn);\n\n      it('Should scan entire database', async () => {\n        const keys = [];\n        let cursor = null;\n        let scanned = 0;\n\n        while (cursor !== 0) {\n          await validateApiCall({\n            endpoint,\n            data: {\n              cursor: cursor || 0,\n              count: 99,\n            },\n            checkFn: ({ body }) => {\n              cursor = body[0].cursor;\n              scanned += body[0].scanned;\n              keys.push(...body[0].keys);\n            },\n          });\n        }\n\n        expect(keys.length).to.be.gte(KEYS_NUMBER);\n        expect(keys.length).to.be.lt(KEYS_NUMBER + 5); // redis returns each key at least once\n        expect(cursor).to.eql(0);\n        expect(scanned).to.be.gte(KEYS_NUMBER);\n        expect(scanned).to.be.lt(KEYS_NUMBER + 99);\n      });\n\n      describe('Filter by type', () => {\n        requirements('rte.version>=6.0');\n\n        [\n          {\n            name: 'Should filter by type (string)',\n            data: {\n              cursor: '0',\n              type: 'string',\n              count: 200,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].total).to.eql(KEYS_NUMBER);\n              expect(body[0].scanned).to.gte(200);\n              expect(body[0].scanned).to.lte(KEYS_NUMBER);\n              expect(body[0].scanned % 200).to.lte(0);\n              expect(body[0].cursor).to.not.eql(0);\n              expect(body[0].keys.length).to.gte(200);\n              expect(body[0].keys.length).to.lt(300);\n              body[0].keys.map((key) =>\n                expect(key.name).to.have.string('str_key_'),\n              );\n              body[0].keys.map((key) => expect(key.type).to.eql('string'));\n            },\n          },\n          {\n            name: 'Should filter by type (list)',\n            data: {\n              cursor: '0',\n              type: 'list',\n              count: 200,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].total).to.eql(KEYS_NUMBER);\n              expect(body[0].scanned).to.gte(200);\n              expect(body[0].scanned).to.lte(KEYS_NUMBER);\n              expect(body[0].scanned % 200).to.lte(0);\n              expect(body[0].cursor).to.not.eql(0);\n              expect(body[0].keys.length).to.gte(200);\n              expect(body[0].keys.length).to.lt(300);\n              body[0].keys.map((key) =>\n                expect(key.name).to.have.string('list_key_'),\n              );\n              body[0].keys.map((key) => expect(key.type).to.eql('list'));\n            },\n          },\n          {\n            name: 'Should filter by type (set)',\n            data: {\n              cursor: '0',\n              type: 'set',\n              count: 200,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].total).to.eql(KEYS_NUMBER);\n              expect(body[0].scanned).to.gte(200);\n              expect(body[0].scanned).to.lte(KEYS_NUMBER);\n              expect(body[0].scanned % 200).to.lte(0);\n              expect(body[0].cursor).to.not.eql(0);\n              expect(body[0].keys.length).to.gte(200);\n              expect(body[0].keys.length).to.lt(300);\n              body[0].keys.map((key) =>\n                expect(key.name).to.have.string('set_key_'),\n              );\n              body[0].keys.map((key) => expect(key.type).to.eql('set'));\n            },\n          },\n          {\n            name: 'Should filter by type (zset)',\n            data: {\n              cursor: '0',\n              type: 'zset',\n              count: 200,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].total).to.eql(KEYS_NUMBER);\n              expect(body[0].scanned).to.gte(200);\n              expect(body[0].scanned).to.lte(KEYS_NUMBER);\n              expect(body[0].scanned % 200).to.lte(0);\n              expect(body[0].cursor).to.not.eql(0);\n              expect(body[0].keys.length).to.gte(200);\n              expect(body[0].keys.length).to.lt(300);\n              body[0].keys.map((key) =>\n                expect(key.name).to.have.string('zset_key_'),\n              );\n              body[0].keys.map((key) => expect(key.type).to.eql('zset'));\n            },\n          },\n          {\n            name: 'Should filter by type (hash)',\n            data: {\n              cursor: '0',\n              type: 'hash',\n              count: 200,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].total).to.eql(KEYS_NUMBER);\n              expect(body[0].scanned).to.gte(200);\n              expect(body[0].scanned).to.lte(KEYS_NUMBER);\n              expect(body[0].scanned % 200).to.lte(0);\n              expect(body[0].cursor).to.not.eql(0);\n              expect(body[0].keys.length).to.gte(200);\n              expect(body[0].keys.length).to.lt(300);\n              body[0].keys.map((key) =>\n                expect(key.name).to.have.string('hash_key_'),\n              );\n              body[0].keys.map((key) => expect(key.type).to.eql('hash'));\n            },\n          },\n        ].map(mainCheckFn);\n\n        describe('REJSON-RL', () => {\n          requirements('rte.modules.rejson');\n          before(async () => await rte.data.generateNReJSONs(300, false));\n\n          [\n            {\n              name: 'Should filter by type (ReJSON-RL)',\n              data: {\n                cursor: '0',\n                type: 'ReJSON-RL',\n                count: 200,\n              },\n              responseSchema,\n              checkFn: ({ body }) => {\n                expect(body[0].total).to.gte(KEYS_NUMBER);\n                expect(body[0].scanned).to.gte(200);\n                expect(body[0].scanned % 200).to.lte(0);\n                expect(body[0].cursor).to.not.eql(0);\n                expect(body[0].keys.length).to.gte(200);\n                expect(body[0].keys.length).to.lt(300);\n                body[0].keys.map((key) =>\n                  expect(key.name).to.have.string('rejson_key_'),\n                );\n                body[0].keys.map((key) => expect(key.type).to.eql('ReJSON-RL'));\n              },\n            },\n          ].map(mainCheckFn);\n        });\n        describe('TSDB-TYPE', () => {\n          requirements('rte.modules.timeseries');\n          before(async () => await rte.data.generateNTimeSeries(300, false));\n\n          [\n            {\n              name: 'Should filter by type (timeseries)',\n              data: {\n                cursor: '0',\n                type: 'TSDB-TYPE',\n                count: 200,\n              },\n              responseSchema,\n              checkFn: ({ body }) => {\n                expect(body[0].total).to.gte(KEYS_NUMBER);\n                expect(body[0].scanned).to.gte(200);\n                expect(body[0].scanned % 200).to.lte(0);\n                expect(body[0].cursor).to.not.eql(0);\n                expect(body[0].keys.length).to.gte(200);\n                expect(body[0].keys.length).to.lt(300);\n                body[0].keys.map((key) =>\n                  expect(key.name).to.have.string('ts_key_'),\n                );\n                body[0].keys.map((key) => expect(key.type).to.eql('TSDB-TYPE'));\n              },\n            },\n          ].map(mainCheckFn);\n        });\n        describe('Stream', () => {\n          requirements('rte.version>=5.0');\n          before(async () => await rte.data.generateNStreams(300, false));\n\n          [\n            {\n              name: 'Should filter by type (stream)',\n              data: {\n                cursor: '0',\n                type: 'stream',\n                count: 200,\n              },\n              responseSchema,\n              checkFn: ({ body }) => {\n                expect(body[0].total).to.gte(KEYS_NUMBER);\n                expect(body[0].scanned).to.gte(200);\n                expect(body[0].scanned % 200).to.lte(0);\n                expect(body[0].cursor).to.not.eql(0);\n                expect(body[0].keys.length).to.gte(200);\n                expect(body[0].keys.length).to.lt(300);\n                body[0].keys.map((key) =>\n                  expect(key.name).to.have.string('st_key_'),\n                );\n                body[0].keys.map((key) => expect(key.type).to.eql('stream'));\n              },\n            },\n          ].map(mainCheckFn);\n        });\n        describe('Graph', () => {\n          requirements('rte.modules.graph');\n          before(async () => await rte.data.generateNGraphs(300, false));\n\n          [\n            {\n              name: 'Should filter by type (stream)',\n              data: {\n                cursor: '0',\n                type: 'graphdata',\n                count: 200,\n              },\n              responseSchema,\n              checkFn: ({ body }) => {\n                expect(body[0].total).to.gte(KEYS_NUMBER);\n                expect(body[0].scanned).to.gte(200);\n                expect(body[0].scanned % 200).to.lte(0);\n                expect(body[0].cursor).to.not.eql(0);\n                expect(body[0].keys.length).to.gte(200);\n                expect(body[0].keys.length).to.lt(300);\n                body[0].keys.map((key) =>\n                  expect(key.name).to.have.string('graph_key_'),\n                );\n                body[0].keys.map((key) => expect(key.type).to.eql('graphdata'));\n              },\n            },\n          ].map(mainCheckFn);\n        });\n      });\n    });\n    describe('Cluster', () => {\n      requirements('rte.type=CLUSTER');\n\n      [\n        {\n          name: 'Should scan all types',\n          data: {\n            cursor: '0',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n              expect(shard.scanned).to.eql(200);\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned).to.eql(200 * result.numberOfShards);\n            expect(result.keys.length).to.gte(200 * result.numberOfShards);\n          },\n        },\n        {\n          name: 'Should scan by provided count value',\n          data: {\n            count: 300,\n            cursor: '0',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const result = {\n              total: 0,\n              scanned: 0,\n              keys: [],\n              numberOfShards: 0,\n            };\n\n            body.map((shard) => {\n              result.total += shard.total;\n              result.scanned += shard.scanned;\n              result.keys.push(...shard.keys);\n              result.numberOfShards++;\n            });\n            expect(result.total).to.eql(KEYS_NUMBER);\n            expect(result.scanned)\n              .to.gte(300 * result.numberOfShards)\n              .lte(310 * result.numberOfShards);\n            expect(result.keys.length)\n              .to.gte(300 * result.numberOfShards)\n              .lte(310 * result.numberOfShards);\n          },\n        },\n      ].map(mainCheckFn);\n\n      it('Should scan entire database', async () => {\n        const keys = [];\n        let scanned = 0;\n        let cursor = ['0'];\n        while (cursor.length > 0) {\n          await validateApiCall({\n            endpoint,\n            data: {\n              cursor: cursor.join('||'),\n              count: 99,\n            },\n            checkFn: ({ body }) => {\n              cursor = [];\n              body.map((shard) => {\n                if (shard.cursor !== 0) {\n                  cursor.push(`${shard.host}:${shard.port}@${shard.cursor}`);\n                }\n                scanned += shard.scanned;\n                keys.push(...shard.keys);\n              });\n            },\n          });\n        }\n\n        expect(keys.length).to.be.gte(KEYS_NUMBER);\n        expect(cursor).to.eql([]);\n        expect(scanned).to.be.gte(KEYS_NUMBER);\n      });\n\n      describe('Filter by type', () => {\n        requirements('rte.version>=6.0');\n        [\n          {\n            name: 'Should filter by type (string)',\n            data: {\n              cursor: '0',\n              type: 'string',\n              count: 200,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              const result = {\n                total: 0,\n                scanned: 0,\n                keys: [],\n                numberOfShards: 0,\n              };\n\n              body.map((shard) => {\n                result.total += shard.total;\n                result.scanned += shard.scanned;\n                result.keys.push(...shard.keys);\n                result.numberOfShards++;\n                expect(shard.scanned).to.gte(200);\n                expect(shard.scanned).to.lte(KEYS_NUMBER);\n              });\n              expect(result.total).to.eql(KEYS_NUMBER);\n              expect(result.scanned).to.gte(200 * result.numberOfShards);\n              expect(result.keys.length).to.gte(200);\n              result.keys.map((key) => {\n                expect(key.name).to.have.string('str_key_');\n                expect(key.type).to.eql('string');\n                expect(key.size).to.be.a('number');\n                expect(key.ttl).to.be.a('number');\n              });\n            },\n          },\n        ].map(mainCheckFn);\n      });\n      describe('Filter by type (w/o keys info, with sended type)', () => {\n        requirements('rte.version>=6.0');\n        [\n          {\n            name: 'Should filter by type (string)',\n            data: {\n              cursor: '0',\n              type: 'string',\n              count: 200,\n              keysInfo: false,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              const result = {\n                total: 0,\n                scanned: 0,\n                keys: [],\n                numberOfShards: 0,\n              };\n\n              body.map((shard) => {\n                result.total += shard.total;\n                result.scanned += shard.scanned;\n                result.keys.push(...shard.keys);\n                result.numberOfShards++;\n                expect(shard.scanned).to.gte(200);\n                expect(shard.scanned).to.lte(KEYS_NUMBER);\n              });\n              expect(result.total).to.eql(KEYS_NUMBER);\n              expect(result.scanned).to.gte(200 * result.numberOfShards);\n              expect(result.keys.length).to.gte(200);\n              result.keys.map((key) => {\n                expect(key.name).to.have.string('str_key_');\n                expect(key.ttl).to.eq(undefined);\n                expect(key.size).to.eq(undefined);\n                expect(key.type).to.eq('string');\n              });\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n    describe('non-ASCII keyName', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'check keyname with non-ASCII symbols should be properly listed',\n          data: {\n            cursor: '0',\n            count: 200,\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            const [stringNonASCIIKey] = _.filter(\n              body.map((nodeResult) =>\n                nodeResult.keys.find(\n                  (key) => key.name === constants.TEST_STRING_KEY_ASCII_UNICODE,\n                ),\n              ),\n              (array) => !!array,\n            );\n\n            expect(stringNonASCIIKey.name).to.eq(\n              constants.TEST_STRING_KEY_ASCII_UNICODE,\n            );\n            expect(stringNonASCIIKey.type).to.eq(constants.TEST_STRING_TYPE);\n            expect(stringNonASCIIKey.ttl).to.eq(-1);\n            expect(stringNonASCIIKey.size).to.gt(\n              constants.TEST_STRING_KEY_ASCII_BUFFER.length,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n  describe('Big data', () => {\n    describe('Exact search on huge keys number', () => {\n      requirements('rte.bigData');\n      // keys inside existing data (~3.6M) but we will check for at least 10M to have a possibility to change\n      // keys number at some point\n      const NUMBER_OF_KEYS = 3_000_000;\n      const key = 'user:15001:string';\n\n      [\n        {\n          name: 'Should scan all types',\n          data: {\n            cursor: '0',\n            match: key,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].total).to.gte(NUMBER_OF_KEYS);\n            expect(body[0].scanned).to.gte(NUMBER_OF_KEYS);\n            expect(body[0].cursor).to.eql(0);\n            expect(body[0].keys.length).to.eql(1);\n            expect(body[0].keys[0].name).to.eql(key);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => await rte.data.generateKeys(true));\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should remove key',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          cursor: '0',\n        },\n        statusCode: 200,\n      },\n      {\n        name: 'Should throw error if no permissions for \"scan\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          cursor: '0',\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -scan'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/list/DELETE-databases-id-list-elements.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/list/elements`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  destination: Joi.string().required().valid('HEAD', 'TAIL'),\n  count: Joi.number().integer().min(1).allow(true), // todo: investigate/fix BE payload transform function\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  destination: 'TAIL',\n  count: 2,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    elements: Joi.array().items(JoiRedisString).required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:id/list/elements', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should remove element by buffer (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          destination: 'TAIL',\n          count: 1,\n        },\n        responseSchema,\n        responseBody: {\n          elements: [constants.TEST_LIST_ELEMENT_BIN_UTF8_1],\n        },\n      },\n      {\n        name: 'Should remove element by buffer (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          destination: 'TAIL',\n          count: 1,\n        },\n        responseSchema,\n        responseBody: {\n          elements: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],\n        },\n      },\n      {\n        name: 'Should remove element by buffer (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          destination: 'TAIL',\n          count: 1,\n        },\n        responseSchema,\n        responseBody: {\n          elements: [constants.TEST_LIST_ELEMENT_BIN_ASCII_1],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      describe('Only one element for redis < 6.2', () => {\n        requirements('rte.version<6.2');\n        before(async () => await rte.data.generateKeys(true));\n\n        [\n          {\n            name: 'Should delete 1 element from the tail',\n            data: {\n              keyName: constants.TEST_LIST_KEY_2,\n              destination: 'TAIL',\n              count: 1,\n            },\n            responseSchema,\n            responseBody: {\n              elements: ['element_100'],\n            },\n            after: async () => {\n              const elements = await rte.client.lrange(\n                constants.TEST_LIST_KEY_2,\n                0,\n                1000,\n              );\n              expect(elements.length).to.eql(99);\n              expect(elements[0]).to.eql('element_1');\n              expect(elements[98]).to.eql('element_99');\n            },\n          },\n          {\n            name: 'Should delete 1 element from the head',\n            data: {\n              keyName: constants.TEST_LIST_KEY_2,\n              destination: 'HEAD',\n              count: 1,\n            },\n            responseSchema,\n            responseBody: {\n              elements: ['element_1'],\n            },\n            after: async () => {\n              const elements = await rte.client.lrange(\n                constants.TEST_LIST_KEY_2,\n                0,\n                1000,\n              );\n              expect(elements.length).to.eql(98);\n              expect(elements[0]).to.eql('element_2');\n              expect(elements[97]).to.eql('element_99');\n            },\n          },\n          {\n            name: 'Should return NotFound error if instance id does not exists',\n            endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n            data: {\n              keyName: constants.TEST_LIST_KEY_1,\n              destination: 'TAIL',\n              count: 1,\n            },\n            statusCode: 404,\n            responseBody: {\n              statusCode: 404,\n              error: 'Not Found',\n              message: 'Invalid database instance id.',\n            },\n          },\n        ].map(mainCheckFn);\n      });\n      describe('Multiple elements for redis >= 6.2', () => {\n        requirements('rte.version>=6.2');\n        before(async () => await rte.data.generateKeys(true));\n\n        [\n          {\n            name: 'Should delete 2 element from the tail',\n            data: {\n              keyName: constants.TEST_LIST_KEY_2,\n              destination: 'TAIL',\n              count: 2,\n            },\n            responseSchema,\n            responseBody: {\n              elements: ['element_100', 'element_99'],\n            },\n            after: async () => {\n              const elements = await rte.client.lrange(\n                constants.TEST_LIST_KEY_2,\n                0,\n                1000,\n              );\n              expect(elements.length).to.eql(98);\n              expect(elements[0]).to.eql('element_1');\n              expect(elements[97]).to.eql('element_98');\n            },\n          },\n          {\n            name: 'Should delete 10 elements from the head',\n            data: {\n              keyName: constants.TEST_LIST_KEY_2,\n              destination: 'HEAD',\n              count: 10,\n            },\n            responseBody: {\n              elements: new Array(10)\n                .fill(0)\n                .map((item, i) => `element_${i + 1}`),\n            },\n            responseSchema,\n            after: async () => {\n              const elements = await rte.client.lrange(\n                constants.TEST_LIST_KEY_2,\n                0,\n                1000,\n              );\n              expect(elements.length).to.eql(88);\n              expect(elements[0]).to.eql('element_11');\n              expect(elements[87]).to.eql('element_98');\n            },\n          },\n          {\n            name: 'Should delete all elements and key',\n            data: {\n              keyName: constants.TEST_LIST_KEY_2,\n              destination: 'HEAD',\n              count: 88,\n            },\n            responseBody: {\n              elements: new Array(88)\n                .fill(0)\n                .map((item, i) => `element_${i + 11}`),\n            },\n            responseSchema,\n            before: async () => {\n              expect(await rte.client.exists(constants.TEST_LIST_KEY_2)).to.eql(\n                1,\n              );\n            },\n            after: async () => {\n              expect(await rte.client.exists(constants.TEST_LIST_KEY_2)).to.eql(\n                0,\n              );\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            destination: 'TAIL',\n            count: 1,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"lpop\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            destination: 'HEAD',\n            count: 1,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -lpop'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"rpop\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            destination: 'TAIL',\n            count: 1,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -rpop'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/list/PATCH-databases-id-list.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(`/${constants.API.DATABASES}/${instanceId}/list`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  element: Joi.string().required(),\n  index: Joi.number().integer(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  element: constants.TEST_LIST_ELEMENT_1,\n  index: 0,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/list', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should update element from buffer (response utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          element: constants.TEST_LIST_ELEMENT_BIN_UTF8_1,\n          index: 0,\n        },\n        responseBody: {\n          index: 0,\n          element: constants.TEST_LIST_ELEMENT_BIN_UTF8_1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([\n            Buffer.from(constants.TEST_LIST_ELEMENT_BIN_UTF8_1, 'utf8'),\n          ]);\n        },\n      },\n      {\n        name: 'Should update element from buffer (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          element: constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1,\n          index: 0,\n        },\n        responseBody: {\n          index: 0,\n          element: constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([constants.TEST_LIST_ELEMENT_BIN_BUFFER_1]);\n        },\n      },\n      {\n        name: 'Should update element from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          element: constants.TEST_LIST_ELEMENT_BIN_ASCII_1,\n          index: 0,\n        },\n        responseBody: {\n          index: 0,\n          element: constants.TEST_LIST_ELEMENT_BIN_ASCII_1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([constants.TEST_LIST_ELEMENT_BIN_BUFFER_1]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should modify item with empty value on position 0',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            element: '',\n            index: 0,\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              await rte.client.lrange(constants.TEST_LIST_KEY_1, 0, 100),\n            ).to.eql(['', constants.TEST_LIST_ELEMENT_2]);\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            element: constants.getRandomString(),\n            index: 0,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            element: constants.getRandomString(),\n            index: 0,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return BadRequest error if index is out of range',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            element: constants.getRandomString(),\n            index: 999,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            element: constants.getRandomString(),\n            index: 0,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            element: constants.TEST_LIST_ELEMENT_1,\n            index: 0,\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"lset\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            element: constants.getRandomString(),\n            index: 0,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -lset'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            element: constants.getRandomString(),\n            index: 0,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/list/POST-databases-id-list-get_elements-index.test.ts",
    "content": "import {\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID, index = 0) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/list/get-elements/${index}`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    value: JoiRedisString.allow('').required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/list/get-elements/:index', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should get element by buffer (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_UTF8_1,\n          value: constants.TEST_LIST_ELEMENT_BIN_UTF8_1,\n        },\n      },\n      {\n        name: 'Should get element by buffer (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          value: constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1,\n        },\n      },\n      {\n        name: 'Should get element by ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          value: constants.TEST_LIST_ELEMENT_BIN_ASCII_1,\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should select key from position 0 (by default)',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            value: 'element_1',\n          },\n        },\n        {\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID, 0),\n          name: 'Should select key from position 0',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            value: 'element_1',\n          },\n        },\n        {\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID, 1),\n          name: 'Should select key from position 1',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            value: 'element_2',\n          },\n        },\n        {\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID, 99),\n          name: 'Should select key from position 99',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            value: 'element_100',\n          },\n        },\n        {\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID, -1),\n          name: 'Should select key from position -1',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            value: 'element_100',\n          },\n        },\n        {\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID, -2),\n          name: 'Should select key from position -2',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            value: 'element_99',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n\n      describe('Search in huge number of elements', () => {\n        requirements('rte.bigData');\n        [\n          {\n            endpoint: () =>\n              endpoint(\n                constants.TEST_INSTANCE_ID,\n                constants.TEST_LIST_HUGE_INDEX,\n              ),\n            name: 'Should get element from particular position',\n            data: {\n              keyName: constants.TEST_LIST_HUGE_KEY,\n            },\n            responseSchema,\n            responseBody: {\n              keyName: constants.TEST_LIST_HUGE_KEY,\n              value: constants.TEST_LIST_HUGE_ELEMENT,\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            offset: 0,\n            count: 1000,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"lindex\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            offset: 0,\n            count: 1000,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -lindex'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/list/POST-databases-id-list-get_elements.test.ts",
    "content": "import {\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/list/get-elements`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  offset: Joi.number().integer().min(0).allow(true), // todo: investigate/fix BE payload transform function\n  count: Joi.number().integer().min(1).allow(true), // todo: investigate/fix BE payload transform function\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  offset: 0,\n  count: 20,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    total: Joi.number().integer().required(),\n    elements: Joi.array().items(JoiRedisString).required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/list/get-elements', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should query all keys by buffer (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          offset: 0,\n          count: 1000,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_UTF8_1,\n          total: 1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_UTF8_1],\n        },\n      },\n      {\n        name: 'Should query all keys by buffer (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          offset: 0,\n          count: 1000,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          total: 1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],\n        },\n      },\n      {\n        name: 'Should query all keys by ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          offset: 0,\n          count: 1000,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          total: 1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_ASCII_1],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should select all keys',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            offset: 0,\n            count: 1000,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            total: 100,\n            elements: new Array(100)\n              .fill(0)\n              .map((item, i) => `element_${i + 1}`),\n          },\n        },\n        {\n          name: 'Should select last 50 keys',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            offset: 50,\n            count: 1000,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            total: 100,\n            elements: new Array(50)\n              .fill(0)\n              .map((item, i) => `element_${i + 51}`),\n          },\n        },\n        {\n          name: 'Should select first 50 keys',\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            offset: 0,\n            count: 50,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_2,\n            total: 100,\n            elements: new Array(50)\n              .fill(0)\n              .map((item, i) => `element_${i + 1}`),\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            offset: 0,\n            count: 1000,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            offset: 0,\n            count: 1000,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            offset: 0,\n            count: 1000,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"llen\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            offset: 0,\n            count: 1000,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -llen'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"lrange\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            offset: 0,\n            count: 1000,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -lrange'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/list/POST-databases-id-list.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nimport { ListElementDestination } from 'src/modules/browser/list/dto';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/list`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  elements: Joi.array()\n    .items(\n      Joi.custom((value, helpers) => {\n        if (typeof value === 'string' || Buffer.isBuffer(value)) {\n          return value;\n        }\n        return helpers.error('any.invalid');\n      }).messages({\n        'any.invalid': 'elements must be a string or a Buffer',\n      }),\n    )\n    .required(),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_LIST_KEY_1,\n  elements: [constants.TEST_LIST_ELEMENT_1],\n  expire: constants.TEST_LIST_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst createCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      }\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n        expect(await rte.client.lrange(testCase.data.keyName, 0, 100)).to.eql(\n          testCase.data.elements,\n        );\n        if (testCase.data.expire) {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.gte(\n            testCase.data.expire - 5,\n          );\n        } else {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.eql(-1);\n        }\n      }\n    }\n  });\n};\n\ndescribe('POST /databases/:databases/list', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should create list from buff',\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_LIST_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([constants.TEST_LIST_ELEMENT_BIN_BUFFER_1]);\n        },\n      },\n      {\n        name: 'Should create list from ascii',\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_ASCII_1],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_LIST_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([constants.TEST_LIST_ELEMENT_BIN_BUFFER_1]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should create item with empty value',\n          data: {\n            keyName: constants.getRandomString(),\n            elements: [''],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with key ttl',\n          data: {\n            keyName: constants.getRandomString(),\n            elements: [constants.getRandomString()],\n            expire: constants.TEST_STRING_EXPIRE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create regular item',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.TEST_LIST_ELEMENT_1],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should return conflict error if key already exists',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n            message: 'This key name is already in use.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.lrange(constants.TEST_LIST_KEY_1, 0, 10),\n            ).to.eql([constants.TEST_LIST_ELEMENT_1]),\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.lrange(constants.TEST_LIST_KEY_1, 0, 10),\n            ).to.eql([constants.TEST_LIST_ELEMENT_1]),\n        },\n      ].map(createCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            elements: [constants.TEST_LIST_ELEMENT_1],\n            destination: ListElementDestination.Head,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"lpush\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            elements: [constants.getRandomString()],\n            destination: ListElementDestination.Head,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -lpush'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            elements: [constants.getRandomString()],\n            destination: ListElementDestination.Head,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(createCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/list/PUT-databases-id-list.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).put(`/${constants.API.DATABASES}/${instanceId}/list`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  elements: Joi.array()\n    .items(\n      Joi.custom((value, helpers) => {\n        if (typeof value === 'string' || Buffer.isBuffer(value)) {\n          return value;\n        }\n        return helpers.error('any.invalid');\n      }).messages({\n        'any.invalid': 'elements must be a string or a Buffer',\n      }),\n    )\n    .required(),\n  destination: Joi.string().valid('HEAD', 'TAIL').default('TAIL'),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  elements: [constants.getRandomString()],\n  destination: 'TAIL',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: Joi.string().required(),\n    total: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PUT /databases/:instanceId/list', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should insert element from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],\n        },\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_UTF8_1,\n          total: 2,\n        },\n        after: async () => {\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n          ]);\n        },\n      },\n      {\n        name: 'Should insert element from buffer (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],\n        },\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          total: 3,\n        },\n        after: async () => {\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n          ]);\n        },\n      },\n      {\n        name: 'Should insert element from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          elements: [constants.TEST_LIST_ELEMENT_BIN_ASCII_1],\n        },\n        responseBody: {\n          keyName: constants.TEST_LIST_KEY_BIN_ASCII_1,\n          total: 4,\n        },\n        after: async () => {\n          expect(\n            await rte.client.lrangeBuffer(\n              constants.TEST_LIST_KEY_BIN_BUFFER_1,\n              0,\n              100,\n            ),\n          ).to.deep.eq([\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should insert 1 element to the tail (by default)',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_1,\n            total: 3,\n          },\n          after: async function () {\n            const elements = await rte.client.lrange(\n              constants.TEST_LIST_KEY_1,\n              0,\n              1000,\n            );\n            expect(elements[2]).to.eql(this.data.elements[0]);\n          },\n        },\n        {\n          name: 'Should insert 1 element to the tail',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n            destination: 'TAIL',\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_1,\n            total: 4,\n          },\n          after: async function () {\n            const elements = await rte.client.lrange(\n              constants.TEST_LIST_KEY_1,\n              0,\n              1000,\n            );\n            expect(elements[3]).to.eql(this.data.elements[0]);\n          },\n        },\n        {\n          name: 'Should insert 1 element to the head',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n            destination: 'HEAD',\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_LIST_KEY_1,\n            total: 5,\n          },\n          after: async function () {\n            const elements = await rte.client.lrange(\n              constants.TEST_LIST_KEY_1,\n              0,\n              1000,\n            );\n            expect(elements[0]).to.eql(this.data.elements[0]);\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            elements: [constants.getRandomString()],\n            destination: 'HEAD',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n            destination: 'HEAD',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n            destination: 'TAIL',\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"lpushx\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n            destination: 'HEAD',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -lpushx'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"rpushx\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            elements: [constants.getRandomString()],\n            destination: 'TAIL',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -rpushx'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/notifications/GET-notifications.test.ts",
    "content": "import { describe, it, deps, expect, validateApiCall } from '../deps';\nimport { Joi } from '../../helpers/test';\nimport { createDefaultNotifications } from '../../helpers/local-db';\nimport { constants } from '../../helpers/constants';\nconst { request, server } = deps;\n\nconst endpoint = () => request(server).get(`/notifications`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    totalUnread: Joi.number().integer().min(0).required(),\n    notifications: Joi.array()\n      .items(\n        Joi.object().keys({\n          title: Joi.string().required(),\n          category: Joi.string().allow(null),\n          categoryColor: Joi.string().allow(null),\n          body: Joi.string().required(),\n          timestamp: Joi.number().integer().required(),\n          read: Joi.boolean().required(),\n          type: Joi.string().valid('global').required(),\n        }),\n      )\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /notifications', () => {\n  beforeEach(async () => {\n    await createDefaultNotifications(true);\n  });\n\n  [\n    {\n      name: 'Should get ordered notifications list',\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          totalUnread: 2,\n          notifications: [\n            constants.TEST_NOTIFICATION_3,\n            constants.TEST_NOTIFICATION_2,\n            constants.TEST_NOTIFICATION_1,\n          ],\n        });\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/notifications/PATCH-notifications.test.ts",
    "content": "import { describe, it, deps, expect, validateApiCall } from '../deps';\nimport {\n  createDefaultNotifications,\n  getRepository,\n  repositories,\n} from '../../helpers/local-db';\nconst { request, server } = deps;\n\nconst endpoint = () => request(server).patch(`/notifications/read`);\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\nlet repo;\ndescribe('PATCH /notifications/read', () => {\n  beforeEach(async () => {\n    repo = await getRepository(repositories.NOTIFICATION);\n    await createDefaultNotifications(true);\n  });\n\n  [\n    {\n      name: 'Should set all notifications into read state',\n      before: async () => {\n        const notifications = await repo.createQueryBuilder().getMany();\n        expect(\n          notifications.filter((notification) => {\n            return notification.read === false;\n          }).length,\n        ).to.gte(2);\n      },\n      after: async () => {\n        const notifications = await repo.createQueryBuilder().getMany();\n        expect(\n          notifications.filter((notification) => {\n            return notification.read === false;\n          }).length,\n        ).to.eq(0);\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/notifications/notifications.json",
    "content": "{\n  \"notifications\": [\n    {\n      \"title\": \"Title-1\",\n      \"body\": \"Body-1\",\n      \"category\": \"release\",\n      \"categoryColor\": \"#d2b04c\",\n      \"timestamp\": 1656054100\n    },\n    {\n      \"title\": \"Title-3\",\n      \"body\": \"Body-3\",\n      \"category\": \"news\",\n      \"categoryColor\": \"#f18f49\",\n      \"timestamp\": 1656054300\n    },\n    {\n      \"title\": \"Title-4\",\n      \"body\": \"Body-4\",\n      \"timestamp\": 1656054400\n    }\n  ]\n}\n"
  },
  {
    "path": "redisinsight/api/test/api/plugins/GET-databases-id-plugins-commands.test.ts",
    "content": "import { describe, it, deps, validateApiCall, before, expect } from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/plugins/commands`,\n  );\n\nconst responseSchema = Joi.array().items(Joi.string()).required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /databases/:instanceId/plugins/commands', () => {\n  before(localDb.createDatabaseInstances);\n\n  [\n    {\n      name: 'Should get plugin commands whitelist',\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body).to.include('get');\n        expect(body).to.not.include('role');\n        expect(body).to.not.include('xread');\n      },\n    },\n    {\n      endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n      name: 'Should not connect to a database due to misconfiguration',\n      statusCode: 424,\n      responseBody: {\n        statusCode: 424,\n        error: 'RedisConnectionUnavailableException',\n        errorCode: 10904,\n      },\n    },\n    {\n      name: 'Should return NotFound error if instance id does not exists',\n      endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n      statusCode: 404,\n      responseBody: {\n        statusCode: 404,\n        error: 'Not Found',\n        message: 'Invalid database instance id.',\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/plugins/GET-databases-id-plugins-id-command_executions-id-state.test.ts",
    "content": "import { expect, describe, it, Joi, _, deps, validateApiCall } from '../deps';\nconst { server, request, constants, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  visualizationId = constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n  id = constants.TEST_COMMAND_EXECUTION_ID_1,\n) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/plugins/${visualizationId}/command-executions/${id}/state`,\n  );\n\nconst responseSchema = Joi.object()\n  .keys({\n    commandExecutionId: Joi.string().required(),\n    visualizationId: Joi.string().required(),\n    state: Joi.any().required(),\n    createdAt: Joi.date().required(),\n    updatedAt: Joi.date().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/plugins/:vId/command-executions/:id/state', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n          ),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Plugin state was not found.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should get string',\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.commandExecutionId).to.eql(\n            constants.TEST_COMMAND_EXECUTION_ID_1,\n          );\n          expect(body.visualizationId).to.eql(\n            constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n          );\n          expect(body.state).to.eql('some state');\n        },\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            1,\n            true,\n          );\n\n          await localDb.generatePluginState(\n            {\n              commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1,\n              visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n            },\n            true,\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/plugins/GET-plugins.test.ts",
    "content": "import { describe, it, deps, validateApiCall } from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { request, server } = deps;\n\nconst endpoint = () => request(server).get(`/plugins`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    static: Joi.string().required(),\n    plugins: Joi.array()\n      .items(\n        Joi.object().keys({\n          internal: Joi.boolean(),\n          name: Joi.string().required(),\n          baseUrl: Joi.string().required(),\n          main: Joi.string().required(),\n          styles: Joi.string(),\n          visualizations: Joi.array()\n            .items(\n              Joi.object()\n                .keys({\n                  id: Joi.string().required(),\n                  name: Joi.string().required(),\n                  activationMethod: Joi.string().required(),\n                  matchCommands: Joi.array()\n                    .items(Joi.string().required())\n                    .required(),\n                  default: Joi.boolean(),\n                  iconDark: Joi.string(),\n                  iconLight: Joi.string(),\n                })\n                .required(),\n            )\n            .required(),\n        }),\n      )\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /plugins', () => {\n  [\n    {\n      name: 'Should get plugin commands whitelist',\n      responseSchema,\n      checkFn: ({ body }) => {\n        console.log('body', body);\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/plugins/POST-databases-id-plugins-command_executions.test.ts",
    "content": "import {\n  expect,\n  before,\n  describe,\n  it,\n  Joi,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  requirements,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/plugins/command-executions`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  command: Joi.string().required(),\n  mode: Joi.string().valid('RAW', 'ASCII').allow(null),\n  resultsMode: Joi.string()\n    .valid('DEFAULT', 'GROUP_MODE', 'SILENT')\n    .allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict();\n\nconst validInputData = {\n  command: 'set foo bar',\n  mode: 'ASCII',\n  resultsMode: 'DEFAULT',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    databaseId: Joi.string().required(),\n    command: Joi.string().required(),\n    result: Joi.array().items(\n      Joi.object({\n        response: Joi.any().required(),\n        status: Joi.string().required(),\n      }),\n    ),\n    mode: Joi.string().required(),\n    resultsMode: Joi.string().required(),\n    type: Joi.string().valid('WORKBENCH', 'SEARCH').required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/plugins/command-executions', () => {\n  before(rte.data.truncate);\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    describe('String', () => {\n      const bigStringValue = Buffer.alloc(1023 * 1024, 'a').toString();\n\n      [\n        {\n          name: 'Should return 404 not found when incorrect instance',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          statusCode: 404,\n          data: {\n            command: 'get foo',\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          responseBody: {\n            statusCode: 404,\n            message: 'Invalid database instance id.',\n            error: 'Not Found',\n          },\n        },\n        {\n          name: 'Should get string',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.command).to.eql(`get ${constants.TEST_STRING_KEY_1}`);\n            expect(body.result.length).to.eql(1);\n            expect(body.result[0].response).to.eql(bigStringValue);\n            expect(body.result[0].status).to.eql('success');\n          },\n          before: async () => {\n            expect(\n              await rte.client.set(constants.TEST_STRING_KEY_1, bigStringValue),\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('RediSearch', () => {\n      requirements('rte.modules.search');\n      [\n        {\n          name: 'Should support ft.info command (whitelist case insensitive check)',\n          data: {\n            command: `ft.info ${constants.TEST_STRING_KEY_1}`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          responseSchema,\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Bad commands', () => {\n      [\n        {\n          name: 'Should return error if try to run unsupported command (monitor)',\n          data: {\n            command: `monitor`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (subscribe)',\n          data: {\n            command: `subscribe`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (psubscribe)',\n          data: {\n            command: `psubscribe`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (sync)',\n          data: {\n            command: `sync`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (psync)',\n          data: {\n            command: `psync`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (script debug)',\n          data: {\n            command: `script debug`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run blocking command',\n          data: {\n            command: `blpop key`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n        {\n          name: 'Should return error if try to run not readonly command',\n          data: {\n            command: `set string_key value`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n        },\n      ].map((testCase) =>\n        mainCheckFn({\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.result.length).to.eql(1);\n            expect(body.result[0].status).to.eql('fail');\n            expect(body.result[0].response).to.include(\n              'command is not allowed by the Redis Insight Plugins',\n            );\n          },\n          ...testCase,\n        }),\n      );\n    });\n    describe('History items limit', () => {\n      it('Number of history items should be less then 30', async () => {\n        const repo = await localDb.getRepository(\n          localDb.repositories.COMMAND_EXECUTION,\n        );\n        await localDb.generateNCommandExecutions(\n          {\n            databaseId: constants.TEST_INSTANCE_ID,\n            createdAt: new Date(Date.now() - 1000),\n          },\n          30,\n          true,\n        );\n\n        for (let i = 0; i < 40; i++) {\n          await validateApiCall({\n            endpoint,\n            data: {\n              command: `get ${constants.TEST_STRING_KEY_2}`,\n              mode: 'ASCII',\n              resultsMode: 'DEFAULT',\n            },\n            responseSchema,\n            checkFn: async ({ body }) => {\n              expect(body.result.length).to.eql(1);\n\n              // @ts-expect-error\n              const count = await repo.count({\n                databaseId: constants.TEST_INSTANCE_ID,\n              });\n              expect(count).to.lte(30);\n\n              // check that the last execution command was not deleted\n              // await repo.findOneOrFail({ id: body.id }); // sometimes localDb is not in sync. investigate\n            },\n          });\n        }\n      });\n    });\n  });\n  // Skip 'Standalone + Sentinel' and 'Cluster' tests because tested functionalities were removed\n  xdescribe('Standalone + Sentinel', () => {\n    requirements('!rte.type=CLUSTER');\n\n    describe('Incorrect requests for redis client type', () => {\n      [\n        {\n          name: 'Should return error if try to execute command for role for standalone database',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            role: 'ALL',\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n            message: 'Wrong database type.',\n          },\n        },\n        {\n          name: 'Should return error if try to execute command for particular node for standalone database',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n            nodeOptions: {\n              host: 'localhost',\n              port: 6379,\n              enableRedirection: true,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n            message: 'Wrong database type.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n  xdescribe('Cluster', () => {\n    requirements('rte.type=CLUSTER');\n    requirements('!rte.re');\n\n    let database;\n    let nodes;\n\n    before(async () => {\n      database = await (\n        await localDb.getRepository(localDb.repositories.DATABASE)\n      ).findOneBy({\n        id: constants.TEST_INSTANCE_ID,\n      });\n      nodes = JSON.parse(database.nodes);\n    });\n\n    describe('Commands using role', () => {\n      [\n        {\n          name: 'Get command with role=ALL',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            role: 'ALL',\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          responseSchema,\n          before: async () => {\n            await rte.client.set(\n              constants.TEST_STRING_KEY_1,\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n          checkFn: async ({ body }) => {\n            const result = body.result;\n\n            expect(result.length).to.eql(nodes.length);\n            expect(body.role).to.eql('ALL');\n            expect(body.nodeOptions).to.eql(undefined);\n\n            const resultSummary = {\n              moved: 0,\n              succeed: 0,\n            };\n\n            result.forEach((nodeResult) => {\n              const node = nodes.find((node) => {\n                return (\n                  nodeResult.node.host === node.host &&\n                  nodeResult.node.port === node.port\n                );\n              });\n\n              if (!node) {\n                fail(\n                  `Unexpected node detected: ${JSON.stringify(nodeResult.node)}`,\n                );\n              }\n\n              switch (nodeResult.status) {\n                case 'fail':\n                  expect(nodeResult.response).to.have.string('MOVED');\n                  resultSummary.moved++;\n                  break;\n                case 'success':\n                  expect(nodeResult.response).to.eql(\n                    constants.TEST_STRING_VALUE_1,\n                  );\n                  resultSummary.succeed++;\n                  break;\n                default:\n                  fail(`Unexpected node result status: ${nodeResult.status}`);\n              }\n            });\n\n            expect(resultSummary.moved).to.gt(0);\n            expect(resultSummary.succeed).to.gt(0);\n            expect(resultSummary.moved + resultSummary.succeed).to.eq(\n              nodes.length,\n            );\n          },\n        },\n        {\n          name: 'Get command with role=MASTER',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            role: 'MASTER',\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            const result = body.result;\n\n            expect(result.length).to.lte(nodes.length);\n\n            const resultSummary = {\n              moved: 0,\n              succeed: 0,\n            };\n\n            result.forEach((nodeResult) => {\n              const node = nodes.find((node) => {\n                return (\n                  nodeResult.node.host === node.host &&\n                  nodeResult.node.port === node.port\n                );\n              });\n\n              if (!node) {\n                fail(\n                  `Unexpected node detected: ${JSON.stringify(nodeResult.node)}`,\n                );\n              }\n\n              switch (nodeResult.status) {\n                case 'fail':\n                  expect(nodeResult.response).to.have.string('MOVED');\n                  resultSummary.moved++;\n                  break;\n                case 'success':\n                  expect(nodeResult.response).to.eql(\n                    constants.TEST_STRING_VALUE_1,\n                  );\n                  resultSummary.succeed++;\n                  break;\n                default:\n                  fail(`Unexpected node result status: ${nodeResult.status}`);\n              }\n            });\n\n            expect(resultSummary.moved).to.gt(0);\n            expect(resultSummary.succeed).to.gt(0);\n            expect(resultSummary.moved + resultSummary.succeed).to.lte(\n              nodes.length,\n            );\n          },\n        },\n        {\n          name: 'Get command with role=SLAVE should return all failed responses',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            role: 'SLAVE',\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            const result = body.result;\n\n            expect(result.length).to.lte(nodes.length);\n\n            const resultSummary = {\n              moved: 0,\n              succeed: 0,\n            };\n\n            result.forEach((nodeResult) => {\n              const node = nodes.find((node) => {\n                return (\n                  nodeResult.node.host === node.host &&\n                  nodeResult.node.port === node.port\n                );\n              });\n\n              if (!node) {\n                fail(\n                  `Unexpected node detected: ${JSON.stringify(nodeResult.node)}`,\n                );\n              }\n\n              switch (nodeResult.status) {\n                case 'fail':\n                  expect(nodeResult.response).to.have.string('MOVED');\n                  resultSummary.moved++;\n                  break;\n                case 'success':\n                  expect(nodeResult.response).to.eql(\n                    constants.TEST_STRING_VALUE_1,\n                  );\n                  resultSummary.succeed++;\n                  break;\n                default:\n                  fail(`Unexpected node result status: ${nodeResult.status}`);\n              }\n            });\n\n            expect(resultSummary.moved).to.gte(0);\n            expect(resultSummary.succeed).to.gte(0);\n            expect(resultSummary.moved + resultSummary.succeed).to.lte(\n              nodes.length,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Commands using nodeOptions', () => {\n      [\n        {\n          name: 'Incorrect node should return an error',\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n            nodeOptions: {\n              host: 'unreachable',\n              port: 6380,\n              enableRedirection: true,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            message: 'Node unreachable:6380 not exist in OSS Cluster.',\n            error: 'Bad Request',\n          },\n          before: async () => {\n            await rte.client.set(\n              constants.TEST_STRING_KEY_1,\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n\n      it('Should auto redirect and never fail', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            command: `get ${constants.TEST_STRING_KEY_1}`,\n            mode: 'ASCII',\n            resultsMode: 'DEFAULT',\n            nodeOptions: {\n              ...nodes[0],\n              enableRedirection: true,\n            },\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.result.length).to.eql(1);\n            expect(body.nodeOptions).to.eql({\n              ...nodes[0],\n              enableRedirection: true,\n            });\n            expect(body.result[0].status).to.eql('success');\n            expect(body.result[0].response).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/plugins/POST-databases-id-plugins-id-command_executions-id-state.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  Joi,\n  _,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n} from '../deps';\nconst { server, request, constants, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  visualizationId = constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n  id = constants.TEST_COMMAND_EXECUTION_ID_1,\n) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/plugins/${visualizationId}/command-executions/${id}/state`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  state: Joi.any().required(),\n})\n  .messages({\n    'any.required': '{#label} should be defined',\n  })\n  .strict();\n\nconst validInputData = {\n  state: {\n    some: 'state',\n    here: true,\n  },\n};\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/plugins/:vId/command-executions/:id/state', () => {\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n          ),\n        statusCode: 404,\n        data: {\n          state: 'some state',\n        },\n        responseBody: {\n          statusCode: 404,\n          message: 'Command execution was not found.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should set string',\n        data: {\n          state: 'some state',\n        },\n        statusCode: 201,\n        checkFn: async ({ body }) => {\n          expect(body).to.eql({});\n          const entity: any = await (\n            await localDb.getRepository(localDb.repositories.PLUGIN_STATE)\n          ).findOneBy({\n            commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1,\n            visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n          });\n\n          expect(entity.state).to.eql(\n            localDb.encryptData(JSON.stringify('some state')),\n          );\n        },\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            1,\n          );\n        },\n      },\n      {\n        name: 'Should set empty string',\n        data: {\n          state: '',\n        },\n        statusCode: 201,\n        checkFn: async ({ body }) => {\n          expect(body).to.eql({});\n          const entity: any = await (\n            await localDb.getRepository(localDb.repositories.PLUGIN_STATE)\n          ).findOneBy({\n            commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1,\n            visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n          });\n\n          expect(entity.state).to.eql(localDb.encryptData(JSON.stringify('')));\n        },\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            1,\n          );\n        },\n      },\n      {\n        name: 'Should set null state',\n        data: {\n          state: null,\n        },\n        statusCode: 201,\n        checkFn: async ({ body }) => {\n          expect(body).to.eql({});\n          const entity: any = await (\n            await localDb.getRepository(localDb.repositories.PLUGIN_STATE)\n          ).findOneBy({\n            commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1,\n            visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1,\n          });\n\n          expect(entity.state).to.eql(\n            localDb.encryptData(JSON.stringify(null)),\n          );\n        },\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            1,\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/pub-sub/POST-databases-id-pub-sub-messages.test.ts",
    "content": "import {\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/pub-sub/messages`,\n  );\n\nconst dataSchema = Joi.object({\n  channel: Joi.string().allow('').required(),\n  message: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  channel: constants.TEST_PUB_SUB_CHANNEL_1,\n  message: constants.TEST_PUB_SUB_MESSAGE_1,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().integer().required().min(0),\n  })\n  .required()\n  .strict();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/pub-sub/messages', () => {\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should send message',\n        data: {\n          ...validInputData,\n        },\n        responseSchema,\n        statusCode: 201,\n      },\n      {\n        name: 'Should return NotFound error if instance id does not exists',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Invalid database instance id.',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n\n    before(async () => await rte.data.generateKeys(true));\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should publish method',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        statusCode: 201,\n        data: {\n          ...validInputData,\n        },\n      },\n      {\n        name: 'Should throw error if no permissions for \"publish\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -publish'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/query-library/DELETE-databases-id-query_library-id.test.ts",
    "content": "import { expect, describe, deps, getMainCheckFn } from '../deps';\n\nconst { server, request, constants, localDb } = deps;\n\nconst TEST_QUERY_LIBRARY_ID = 'ql-test-delete-id';\n\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = TEST_QUERY_LIBRARY_ID,\n) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/query-library/${id}`,\n  );\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/query-library/:id', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            TEST_QUERY_LIBRARY_ID,\n          ),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 404 when item does not exist',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_INSTANCE_ID,\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n          ),\n        statusCode: 404,\n      },\n      {\n        name: 'Should delete a query library item',\n        before: async () => {\n          await localDb.generateNQueryLibraryItems(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: TEST_QUERY_LIBRARY_ID,\n              indexName: 'idx:bikes_vss',\n            },\n            1,\n            true,\n          );\n        },\n        after: async () => {\n          const entity = await (\n            await localDb.getRepository(localDb.repositories.QUERY_LIBRARY)\n          ).findOneBy({ id: TEST_QUERY_LIBRARY_ID });\n          expect(entity).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/query-library/GET-databases-id-query_library-id.test.ts",
    "content": "import { expect, describe, Joi, deps, getMainCheckFn } from '../deps';\n\nconst { server, request, constants, localDb } = deps;\n\nconst TEST_QUERY_LIBRARY_ID = 'ql-test-get-one-id';\n\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = TEST_QUERY_LIBRARY_ID,\n) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/query-library/${id}`,\n  );\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    databaseId: Joi.string().required(),\n    indexName: Joi.string().required(),\n    type: Joi.string().valid('SAMPLE', 'SAVED').required(),\n    name: Joi.string().required(),\n    description: Joi.string().allow(null).optional(),\n    query: Joi.string().required(),\n    createdAt: Joi.date().required(),\n    updatedAt: Joi.date().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:instanceId/query-library/:id', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            TEST_QUERY_LIBRARY_ID,\n          ),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 404 when item does not exist',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_INSTANCE_ID,\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n          ),\n        statusCode: 404,\n      },\n      {\n        name: 'Should return a single query library item',\n        responseSchema,\n        before: async () => {\n          await localDb.generateNQueryLibraryItems(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: TEST_QUERY_LIBRARY_ID,\n              indexName: 'idx:bikes_vss',\n              name: 'Get one test query',\n            },\n            1,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eql(TEST_QUERY_LIBRARY_ID);\n          expect(body.name).to.eql('Get one test query');\n          expect(body.databaseId).to.eql(constants.TEST_INSTANCE_ID);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/query-library/GET-databases-id-query_library.test.ts",
    "content": "import { expect, describe, before, Joi, deps, getMainCheckFn } from '../deps';\n\nconst { server, request, constants, localDb } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/query-library`,\n  );\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      databaseId: Joi.string().required(),\n      indexName: Joi.string().required(),\n      type: Joi.string().valid('SAMPLE', 'SAVED').required(),\n      name: Joi.string().required(),\n      description: Joi.string().allow(null).optional(),\n      query: Joi.string().required(),\n      createdAt: Joi.date().required(),\n      updatedAt: Joi.date().required(),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:instanceId/query-library', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return empty array when no items exist',\n        query: { indexName: 'idx:bikes_vss' },\n        responseSchema,\n        before: async () => {\n          const rep = await localDb.getRepository(\n            localDb.repositories.QUERY_LIBRARY,\n          );\n          await rep.clear();\n        },\n        checkFn: async ({ body }) => {\n          expect(body).to.eql([]);\n        },\n      },\n      {\n        name: 'Should return items for the database',\n        query: { indexName: 'idx:bikes_vss' },\n        responseSchema,\n        before: async () => {\n          await localDb.generateNQueryLibraryItems(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              indexName: 'idx:bikes_vss',\n            },\n            3,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length(3);\n          body.forEach((item) => {\n            expect(item.databaseId).to.eql(constants.TEST_INSTANCE_ID);\n          });\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Filter by indexName', () => {\n    before(async () => {\n      const rep = await localDb.getRepository(\n        localDb.repositories.QUERY_LIBRARY,\n      );\n      await rep.clear();\n      await localDb.generateNQueryLibraryItems(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          indexName: 'idx:bikes_vss',\n        },\n        3,\n      );\n      await localDb.generateNQueryLibraryItems(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          indexName: 'idx:movies_vss',\n        },\n        2,\n      );\n    });\n\n    [\n      {\n        name: 'Should return only items matching indexName filter',\n        query: { indexName: 'idx:bikes_vss' },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length(3);\n          body.forEach((item) => {\n            expect(item.indexName).to.eql('idx:bikes_vss');\n          });\n        },\n      },\n      {\n        name: 'Should return items for another index',\n        query: { indexName: 'idx:movies_vss' },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length(2);\n          body.forEach((item) => {\n            expect(item.indexName).to.eql('idx:movies_vss');\n          });\n        },\n      },\n      {\n        name: 'Should return 400 when indexName is not provided',\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Search filter', () => {\n    before(async () => {\n      const rep = await localDb.getRepository(\n        localDb.repositories.QUERY_LIBRARY,\n      );\n      await rep.clear();\n      await localDb.generateNQueryLibraryItems(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          indexName: 'idx:bikes_vss',\n          name: 'Vector similarity search',\n        },\n        1,\n      );\n      await localDb.generateNQueryLibraryItems(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          indexName: 'idx:bikes_vss',\n          name: 'Count all documents',\n        },\n        1,\n      );\n    });\n\n    [\n      {\n        name: 'Should filter by search term matching name',\n        query: { indexName: 'idx:bikes_vss', search: 'vector' },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length(1);\n          expect(body[0].name).to.eql('Vector similarity search');\n        },\n      },\n      {\n        name: 'Should return empty list when search does not match',\n        query: { indexName: 'idx:bikes_vss', search: 'nonexistent' },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/query-library/PATCH-databases-id-query_library-id.test.ts",
    "content": "import { expect, describe, Joi, deps, getMainCheckFn } from '../deps';\n\nconst { server, request, constants, localDb } = deps;\n\nconst TEST_QUERY_LIBRARY_ID = 'ql-test-update-id';\n\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = TEST_QUERY_LIBRARY_ID,\n) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/query-library/${id}`,\n  );\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    databaseId: Joi.string().required(),\n    indexName: Joi.string().required(),\n    type: Joi.string().valid('SAMPLE', 'SAVED').required(),\n    name: Joi.string().required(),\n    description: Joi.string().allow(null).optional(),\n    query: Joi.string().required(),\n    createdAt: Joi.date().required(),\n    updatedAt: Joi.date().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/query-library/:id', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            TEST_QUERY_LIBRARY_ID,\n          ),\n        data: { name: 'Updated name' },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 404 when item does not exist',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_INSTANCE_ID,\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n          ),\n        data: { name: 'Updated name' },\n        statusCode: 404,\n      },\n      {\n        name: 'Should update name of a query library item',\n        data: { name: 'Updated query name' },\n        responseSchema,\n        before: async () => {\n          await localDb.generateNQueryLibraryItems(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: TEST_QUERY_LIBRARY_ID,\n              indexName: 'idx:bikes_vss',\n              name: 'Original name',\n            },\n            1,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eql(TEST_QUERY_LIBRARY_ID);\n          expect(body.name).to.eql('Updated query name');\n        },\n      },\n      {\n        name: 'Should update query string',\n        data: { query: 'FT.SEARCH idx:bikes_vss \"@brand:Trek\"' },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eql(TEST_QUERY_LIBRARY_ID);\n          expect(body.query).to.eql('FT.SEARCH idx:bikes_vss \"@brand:Trek\"');\n          expect(body.name).to.eql('Updated query name');\n        },\n      },\n      {\n        name: 'Should update description',\n        data: { description: 'New description' },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eql(TEST_QUERY_LIBRARY_ID);\n          expect(body.description).to.eql('New description');\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/query-library/POST-databases-id-query_library-seed.test.ts",
    "content": "import { expect, describe, before, Joi, deps, getMainCheckFn } from '../deps';\nimport { seedQueryLibraryItemDtoFactory } from 'src/modules/query-library/__tests__/query-library.factory';\n\nconst { server, request, constants, localDb } = deps;\n\nconst indexName = 'idx:bikes_vss';\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/query-library/seed`,\n  );\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      databaseId: Joi.string().required(),\n      indexName: Joi.string().required(),\n      type: Joi.string().valid('SAMPLE', 'SAVED').required(),\n      name: Joi.string().required(),\n      description: Joi.string().allow(null).optional(),\n      query: Joi.string().required(),\n      createdAt: Joi.date().required(),\n      updatedAt: Joi.date().required(),\n    }),\n  )\n  .required();\n\nconst seedItems = seedQueryLibraryItemDtoFactory.buildList(2, { indexName });\nconst seedData = { items: seedItems };\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/query-library/seed', () => {\n  before(async () => {\n    const rep = await localDb.getRepository(localDb.repositories.QUERY_LIBRARY);\n    await rep.clear();\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: seedData,\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should seed sample queries',\n        data: seedData,\n        statusCode: 201,\n        responseSchema,\n        before: async () => {\n          const rep = await localDb.getRepository(\n            localDb.repositories.QUERY_LIBRARY,\n          );\n          await rep.clear();\n        },\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length(2);\n          body.forEach((item) => {\n            expect(item.type).to.eql('SAMPLE');\n            expect(item.databaseId).to.eql(constants.TEST_INSTANCE_ID);\n            expect(item.indexName).to.eql(indexName);\n          });\n          expect(body[0].name).to.eql(seedItems[0].name);\n          expect(body[1].name).to.eql(seedItems[1].name);\n        },\n      },\n      {\n        name: 'Should skip seeding when samples already exist and return existing items',\n        data: seedData,\n        statusCode: 201,\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body).to.have.length.greaterThan(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/query-library/POST-databases-id-query_library.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  Joi,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\n\nconst { server, request, constants, localDb } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/query-library`,\n  );\n\nconst dataSchema = Joi.object({\n  indexName: Joi.string().required().messages({\n    'string.base': 'indexName must be a string',\n    'any.required': 'indexName should not be empty',\n  }),\n  name: Joi.string().required().messages({\n    'string.base': 'name must be a string',\n    'any.required': 'name should not be empty',\n  }),\n  query: Joi.string().required().messages({\n    'string.base': 'query must be a string',\n    'any.required': 'query should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  indexName: 'idx:bikes_vss',\n  name: 'Find all bikes',\n  query: 'FT.SEARCH idx:bikes_vss \"*\"',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    databaseId: Joi.string().required(),\n    indexName: Joi.string().required(),\n    type: Joi.string().valid('SAMPLE', 'SAVED').required(),\n    name: Joi.string().required(),\n    description: Joi.string().allow(null).optional(),\n    query: Joi.string().required(),\n    createdAt: Joi.date().required(),\n    updatedAt: Joi.date().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/query-library', () => {\n  before(async () => {\n    const rep = await localDb.getRepository(localDb.repositories.QUERY_LIBRARY);\n    await rep.clear();\n  });\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: validInputData,\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should create a saved query',\n        data: validInputData,\n        statusCode: 201,\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.indexName).to.eql(validInputData.indexName);\n          expect(body.name).to.eql(validInputData.name);\n          expect(body.query).to.eql(validInputData.query);\n          expect(body.type).to.eql('SAVED');\n          expect(body.databaseId).to.eql(constants.TEST_INSTANCE_ID);\n\n          const entity: any = await (\n            await localDb.getRepository(localDb.repositories.QUERY_LIBRARY)\n          ).findOneBy({ id: body.id });\n\n          expect(entity).to.not.eql(null);\n          expect(entity.encryption).to.eql(constants.TEST_ENCRYPTION_STRATEGY);\n        },\n      },\n      {\n        name: 'Should always set type to SAVED regardless of input',\n        data: {\n          ...validInputData,\n          name: 'Another query',\n          type: 'SAMPLE',\n        },\n        statusCode: 201,\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.type).to.eql('SAVED');\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/DELETE-rdi.test.ts",
    "content": "import { describe, expect, deps, getMainCheckFn } from '../deps';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTestId';\nconst testRdiId2 = 'someTestId_2';\n\nconst endpoint = () => request(server).delete(`/${constants.API.RDI}/`);\n\nconst validInputData = {\n  ids: [testRdiId, testRdiId2],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /rdi', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should throw error if ids are empty',\n        data: { ids: [] },\n        statusCode: 500,\n      },\n      {\n        name: 'Should delete multiple rdis by ids',\n        data: validInputData,\n        statusCode: 200,\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiId }, 1);\n          await localDb.generateRdis({ id: testRdiId2 }, 1);\n          const rdi1 = await localDb.getRdiById(testRdiId);\n          const rdi2 = await localDb.getRdiById(testRdiId2);\n          expect(rdi1.id).to.eql(testRdiId);\n          expect(rdi2.id).to.eql(testRdiId2);\n        },\n        after: async () => {\n          expect(await localDb.getRdiById(testRdiId)).to.eql(null);\n          expect(await localDb.getRdiById(testRdiId2)).to.eql(null);\n        },\n      },\n      {\n        name: 'Should not throw error even if id does not exist',\n        data: { ids: ['Not_existed'] },\n        statusCode: 200,\n        before: async () => {\n          expect(await localDb.getRdiById('Not_existed')).to.eql(null);\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/GET-rdi-id-connect.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../deps';\nimport { nock } from '../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\n\nconst endpoint = (id) =>\n  request(server).get(`/${constants.API.RDI}/${id || testRdiId}/connect`);\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/connect', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db',\n      statusCode: 200,\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/GET-rdi-id.test.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport { describe, expect, deps, getMainCheckFn } from '../deps';\nimport { Joi } from '../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = uuidv4();\nconst notExistedRdiId = 'not-existed-rdi-id';\n\nconst endpoint = (rdiId) =>\n  request(server).get(`/${constants.API.RDI}/${rdiId || testRdiId}`);\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    url: Joi.string().required(),\n    name: Joi.string().max(500).required(),\n    username: Joi.string().required(),\n    password: Joi.string().required(),\n    lastConnection: Joi.string().isoDate().required(),\n    version: Joi.string().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id', () => {\n  [\n    {\n      name: 'Should return rdi data by id',\n      responseSchema,\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body.id).to.eql(testRdiId);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId }, 1);\n      },\n    },\n    {\n      name: 'Should throw error if no rdi found in a db',\n      statusCode: 404,\n      endpoint: () => endpoint(notExistedRdiId),\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          message: `RDI with id ${notExistedRdiId} was not found`,\n          statusCode: 404,\n          error: 'RdiNotFound',\n          errorCode: 11405,\n        });\n      },\n      before: async () => {\n        await (await localDb.getRepository(localDb.repositories.RDI)).clear();\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/GET-rdi.test.ts",
    "content": "import { describe, expect, deps, getMainCheckFn } from '../deps';\nimport { Joi } from '../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst endpoint = () => request(server).get(`/${constants.API.RDI}`);\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      url: Joi.string().required(),\n      name: Joi.string().max(500).required(),\n      username: Joi.string().required(),\n      lastConnection: Joi.string().isoDate().required(),\n      version: Joi.string().required(),\n    }),\n  )\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi', () => {\n  [\n    {\n      name: 'Should return empty array if no rdis yet',\n      responseSchema,\n      checkFn: ({ body }) => {\n        expect(body).to.eql([]);\n      },\n      before: async () => {\n        await (await localDb.getRepository(localDb.repositories.RDI)).clear();\n        await request(server).get('/rdi');\n      },\n    },\n    {\n      name: 'Should get rdis list',\n      responseSchema,\n      before: async () => {\n        await localDb.generateRdis({}, 2);\n        await request(server).get('/rdi');\n      },\n      checkFn: ({ body }) => {\n        expect(body.length).to.eql(2);\n        expect(body[0].name).to.eql('Rdi');\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/PATCH-rdi-id.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport {\n  describe,\n  expect,\n  deps,\n  getMainCheckFn,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nimport { Joi, nock } from '../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTestId';\nconst testRdiUrl = 'http://rdilocal.test';\nconst testRdiName = 'Test Rdi Name';\nconst testRdiBase = { id: testRdiId, name: testRdiName, url: testRdiUrl };\n\nconst endpoint = (id) =>\n  request(server).patch(`/${constants.API.RDI}/${id || testRdiId}`);\n\nconst dataSchema = Joi.object()\n  .keys({\n    name: Joi.string().max(500).allow(null),\n    username: Joi.string().allow(null),\n    password: Joi.string().allow(null),\n  })\n  .messages({ 'any.required': '{#label} should not be empty' })\n  .strict(true);\n\nconst validInputData = {\n  name: 'Updated Rdi',\n  username: 'rdiUsername Updated',\n  password: constants.TEST_KEYTAR_PASSWORD,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    url: Joi.string().required(),\n    name: Joi.string().max(500).required(),\n    username: Joi.string().required(),\n    password: Joi.string().required(),\n    lastConnection: Joi.string().isoDate().required(),\n    version: Joi.string().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\nconst loginNock = nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true);\n\ndescribe('PATCH /rdi/:id', () => {\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).forEach(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n  describe('Common', () => {\n    [\n      {\n        name: 'Should update rdi name',\n        responseSchema,\n        data: { name: validInputData.name },\n        statusCode: 200,\n        checkFn: ({ body }) => {\n          expect(body.name).to.eql(validInputData.name);\n          expect(body.id).to.eql(testRdiId);\n        },\n        before: async () => {\n          await localDb.generateRdis(testRdiBase, 1);\n          loginNock.reply(200, {\n            access_token: mockedAccessToken,\n          });\n        },\n      },\n      {\n        name: 'Should update rdi username',\n        responseSchema,\n        data: { username: validInputData.username },\n        statusCode: 200,\n        checkFn: ({ body }) => {\n          expect(body.name).to.eql(testRdiName);\n          expect(body.username).to.eql(validInputData.username);\n        },\n        before: async () => {\n          await localDb.generateRdis(testRdiBase, 1);\n          loginNock.reply(200, {\n            access_token: mockedAccessToken,\n          });\n        },\n      },\n      {\n        name: 'Should throw error if rdiClient was not connected',\n        statusCode: 401,\n        data: validInputData,\n        responseBody: {\n          message: 'Unauthorized',\n          statusCode: 401,\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n        },\n        before: () => {\n          loginNock.reply(401, {\n            message: 'Unauthorized',\n            detail: 'Unauthorized',\n          });\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/POST-rdi.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport {\n  describe,\n  expect,\n  deps,\n  getMainCheckFn,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nimport { Joi, nock } from '../../helpers/test';\n\nconst { request, server, constants } = deps;\n\nconst endpoint = () => request(server).post(`/${constants.API.RDI}`);\n\nconst dataSchema = Joi.object()\n  .keys({\n    url: Joi.string().required(),\n    name: Joi.string().max(500).required(),\n    username: Joi.string().allow(null),\n    password: Joi.string().allow(null),\n  })\n  .messages({ 'any.required': '{#label} should not be empty' })\n  .strict(true);\n\nconst validInputData = {\n  url: 'http://testRDI.test/',\n  name: 'Created Rdi',\n  username: 'rdiUsername',\n  password: constants.TEST_KEYTAR_PASSWORD,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    url: Joi.string().required(),\n    name: Joi.string().max(500).required(),\n    username: Joi.string().required(),\n    password: Joi.string().required(),\n    lastConnection: Joi.string().isoDate().required(),\n    version: Joi.string().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi', () => {\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).forEach(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n  describe('Common', () => {\n    [\n      {\n        name: 'Should create rdi from valid data',\n        responseSchema,\n        data: validInputData,\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          expect(body.name).to.eql(validInputData.name);\n        },\n        before: () => {\n          nock(validInputData.url)\n            .post(`/${RdiUrl.Login}`)\n            .query(true)\n            .reply(200, {\n              access_token: mockedAccessToken,\n            });\n          nock(validInputData.url)\n            .get(`/${RdiUrl.GetPipelineStatus}`)\n            .query(true)\n            .reply(200, {\n              version: '2.17',\n            });\n        },\n      },\n      {\n        name: 'Should throw error if rdiClient was not connected',\n        statusCode: 401,\n        data: validInputData,\n        before: () => {\n          nock(validInputData.url)\n            .post(`/${RdiUrl.Login}`)\n            .query(true)\n            .reply(401, {\n              message: 'Unauthorized',\n              detail: 'Unauthorized',\n            });\n        },\n        responseBody: {\n          message: 'Unauthorized',\n          statusCode: 401,\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline-config-template-pipelineType-dbType.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock, Joi } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_config_template_pipelineType';\nconst testPipelineType = 'someType';\nconst testDBType = 'someDBType';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst endpoint = (id: string, type: string = '', dbType: string = '') =>\n  request(server).get(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/config/template/${type || testPipelineType}/${dbType || testDBType}`,\n  );\n\nconst mockResponseSuccess = {\n  template: 'Some template',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    template: Joi.string().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/config/template/:pipelineType/:dbType', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and client getConfigTemplate succeeds',\n      responseSchema,\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(mockResponseSuccess);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetConfigTemplate}/${testPipelineType}/${testDBType}`)\n          .query(true)\n          .reply(200, mockResponseSuccess);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client getConfigTemplate will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetConfigTemplate}/${testPipelineType}/${testDBType}`)\n          .query(true)\n          .reply(401, {\n            message: 'Request failed with status code 401',\n            detail: 'Unauthorized',\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline-job-functions.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_job_functions';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst endpoint = (id: string) =>\n  request(server).get(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/job-functions`,\n  );\n\nconst mockResponseSuccess = {\n  jobFunctions: 'some functions',\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/job-functions', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and client GetJobFunctions succeeds',\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(mockResponseSuccess);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`${RdiUrl.JobFunctions}`)\n          .query(true)\n          .reply(200, mockResponseSuccess);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client getJobFunctions will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl).get(`${RdiUrl.JobFunctions}`).query(true).reply(401, {\n          message: 'Request failed with status code 401',\n          detail: 'Unauthorized',\n        });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline-job-template-pipelineType.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock, Joi } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_job_template_pipelineType';\nconst testPipelineType = 'someType';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst endpoint = (id: string, type: string = '') =>\n  request(server).get(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/job/template/${type || testPipelineType}`,\n  );\n\nconst mockResponseSuccess = {\n  template: 'Some template',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    template: Joi.string().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/job/template/:pipelineType', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and client getJobTemplate succeeds',\n      responseSchema,\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(mockResponseSuccess);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetJobTemplate}/${testPipelineType}`)\n          .query(true)\n          .reply(200, mockResponseSuccess);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client getJobTemplate will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetJobTemplate}/${testPipelineType}`)\n          .query(true)\n          .reply(401, {\n            message: 'Request failed with status code 401',\n            detail: 'Unauthorized',\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline-schema.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { nock } from '../../../helpers/test';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport {\n  mockRdiConfigSchema,\n  mockRdiJobsSchema,\n  mockRdiSchema,\n} from 'src/__mocks__';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_schema';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst endpoint = (id: string) =>\n  request(server).get(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/schema`,\n  );\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/schema', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db',\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(mockRdiSchema);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetConfigSchema}`)\n          .query(true)\n          .reply(200, mockRdiConfigSchema);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetJobsSchema}`)\n          .query(true)\n          .reply(200, mockRdiJobsSchema);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client getSchema will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetConfigSchema}`)\n          .query(true)\n          .reply(200, mockRdiConfigSchema);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetJobsSchema}`)\n          .query(true)\n          .reply(401, {\n            message: 'Request failed with status code 401',\n            detail: 'Unauthorized',\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline-status.test.ts",
    "content": "import { RdiUrl, RdiUrlV2 } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiIdV1 = 'someTEST_pipeline_status_v1';\nconst testRdiIdV2 = 'someTEST_pipeline_status_v2';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrlV1 = 'http://rdilocalv1.test';\nconst testRdiUrlV2 = 'http://rdilocalv2.test';\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst endpoint = (id: string) =>\n  request(server).get(`/${constants.API.RDI}/${id}/pipeline/status`);\n\n// V1 API response structure\nconst mockV1ApiResponse = {\n  components: {\n    'collector-source': {\n      status: 'running',\n      connected: true,\n      version: '1.0.0',\n    },\n    processor: { status: 'running', version: '1.0.0' },\n  },\n  pipelines: {\n    default: {\n      status: 'ready',\n      state: 'cdc',\n      tasks: [],\n    },\n  },\n};\n\n// Expected transformed response for V1\nconst mockV1ResponseSuccess = {\n  status: 'ready',\n  state: 'cdc',\n};\n\n// V2 API response structure\nconst mockV2InfoResponse = {\n  version: '2.0.0',\n};\n\nconst mockV2PipelinesResponse = [\n  {\n    name: 'test-pipeline',\n    active: true,\n    status: 'started',\n  },\n];\n\nconst mockV2StatusResponse = {\n  status: 'started',\n  errors: [],\n  components: [\n    {\n      name: 'processor',\n      type: 'stream-processor',\n      version: '2.0.0',\n      status: 'running',\n      errors: [],\n    },\n  ],\n};\n\n// Expected transformed response for V2\nconst mockV2ResponseSuccess = {\n  status: 'started',\n  errors: [],\n  components: [\n    {\n      name: 'processor',\n      type: 'stream-processor',\n      version: '2.0.0',\n      status: 'running',\n      errors: [],\n    },\n  ],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/status', () => {\n  describe('V1 API', () => {\n    [\n      {\n        name: 'Should be success if rdi with :id is in db and client GetPipelineStatus succeeds (V1)',\n        endpoint: () => endpoint(testRdiIdV1),\n        statusCode: 200,\n        checkFn: ({ body }) => {\n          expect(body).to.eql(mockV1ResponseSuccess);\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiIdV1, url: testRdiUrlV1 }, 1);\n          // Mock V2 info endpoint to return 404 (fall back to V1)\n          nock(testRdiUrlV1).get(`/${RdiUrlV2.GetInfo}`).query(true).reply(404);\n          nock(testRdiUrlV1).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n          nock(testRdiUrlV1)\n            .get(`/${RdiUrl.GetPipelineStatus}`)\n            .query(true)\n            .reply(200, mockV1ApiResponse);\n        },\n      },\n      {\n        name: 'Should throw notFoundError if rdi with id in params does not exist',\n        endpoint: () => endpoint(notExistedRdiId),\n        statusCode: 404,\n        checkFn: ({ body }) => {\n          expect(body).to.eql({\n            error: 'Not Found',\n            message: 'Invalid rdi instance id.',\n            statusCode: 404,\n          });\n        },\n        before: async () => {\n          expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n        },\n      },\n      {\n        name: 'Should throw error if client getPipelineStatus will not succeed (V1)',\n        endpoint: () => endpoint(testRdiIdV1),\n        statusCode: 401,\n        checkFn: ({ body }) => {\n          expect(body).to.eql({\n            error: 'RdiUnauthorized',\n            errorCode: 11402,\n            message: 'Unauthorized',\n            statusCode: 401,\n          });\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiIdV1, url: testRdiUrlV1 }, 1);\n          // Mock V2 info endpoint to return 404 (fall back to V1)\n          nock(testRdiUrlV1).get(`/${RdiUrlV2.GetInfo}`).query(true).reply(404);\n          nock(testRdiUrlV1).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n          nock(testRdiUrlV1)\n            .get(`/${RdiUrl.GetPipelineStatus}`)\n            .query(true)\n            .reply(401, {\n              message: 'Request failed with status code 401',\n              detail: 'Unauthorized',\n            });\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n\n  describe('V2 API', () => {\n    [\n      {\n        name: 'Should be success if rdi with :id is in db and client GetPipelineStatus succeeds (V2)',\n        endpoint: () => endpoint(testRdiIdV2),\n        statusCode: 200,\n        checkFn: ({ body }) => {\n          expect(body).to.eql(mockV2ResponseSuccess);\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiIdV2, url: testRdiUrlV2 }, 1);\n          // Mock V2 info endpoint to return success (use V2 client)\n          nock(testRdiUrlV2)\n            .get(`/${RdiUrlV2.GetInfo}`)\n            .query(true)\n            .reply(200, mockV2InfoResponse);\n          nock(testRdiUrlV2).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n          // Mock V2 pipelines endpoint for selectPipeline()\n          nock(testRdiUrlV2)\n            .get(`/${RdiUrlV2.GetPipelines}`)\n            .query(true)\n            .reply(200, mockV2PipelinesResponse);\n          // Mock V2 pipeline status endpoint\n          nock(testRdiUrlV2)\n            .get(`/${RdiUrlV2.GetPipelineStatus('test-pipeline')}`)\n            .query(true)\n            .reply(200, mockV2StatusResponse);\n        },\n      },\n      {\n        name: 'Should throw error if client getPipelineStatus will not succeed (V2)',\n        endpoint: () => endpoint(testRdiIdV2),\n        statusCode: 401,\n        checkFn: ({ body }) => {\n          expect(body).to.eql({\n            error: 'RdiUnauthorized',\n            errorCode: 11402,\n            message: 'Unauthorized',\n            statusCode: 401,\n          });\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiIdV2, url: testRdiUrlV2 }, 1);\n          // Mock V2 info endpoint to return success (use V2 client)\n          nock(testRdiUrlV2)\n            .get(`/${RdiUrlV2.GetInfo}`)\n            .query(true)\n            .reply(200, mockV2InfoResponse);\n          nock(testRdiUrlV2).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n          // Mock V2 pipelines endpoint for selectPipeline()\n          nock(testRdiUrlV2)\n            .get(`/${RdiUrlV2.GetPipelines}`)\n            .query(true)\n            .reply(200, mockV2PipelinesResponse);\n          // Mock V2 pipeline status endpoint with error\n          nock(testRdiUrlV2)\n            .get(`/${RdiUrlV2.GetPipelineStatus('test-pipeline')}`)\n            .query(true)\n            .reply(401, {\n              message: 'Request failed with status code 401',\n              detail: 'Unauthorized',\n            });\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline-strategies.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_strategies';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst endpoint = (id: string) =>\n  request(server).get(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/strategies`,\n  );\n\nconst mockResponseSuccess = {\n  strategies: {},\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/strategies', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and client GetStrategies succeeds',\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(mockResponseSuccess);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetStrategies}`)\n          .query(true)\n          .reply(200, mockResponseSuccess);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client getStrategies will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetStrategies}`)\n          .query(true)\n          .reply(401, {\n            message: 'Request failed with status code 401',\n            detail: 'Unauthorized',\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/GET-rdi-id-pipeline.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { RdiPipeline } from 'src/modules/rdi/models';\nimport { nock, Joi } from '../../../helpers/test';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\n\nconst endpoint = (id: string) =>\n  request(server).get(`/${constants.API.RDI}/${id || testRdiId}/pipeline`);\n\nconst job1 = {\n  name: 'job1',\n  source: {\n    redis: {},\n  },\n  transform: [],\n  output: [],\n};\nconst job2 = {\n  name: 'job2',\n  source: {\n    redis: {},\n  },\n  transform: [],\n  output: [],\n};\n\nconst mockResponseSuccess = {\n  targets: {\n    target: {},\n  },\n  jobs: [job1, job2],\n  sources: { psql: {} },\n  processors: {},\n};\n\nconst expectedPipeline: RdiPipeline = Object.assign(new RdiPipeline(), {\n  config: {\n    targets: {\n      target: {},\n    },\n    sources: { psql: {} },\n  },\n  jobs: {\n    [job1.name]: (({ name: _name, ...job }) => job)(job1),\n    [job2.name]: (({ name: _name, ...job }) => job)(job2),\n  },\n});\n\nconst responseSchema = Joi.object()\n  .keys({\n    jobs: Joi.object().required(),\n    config: Joi.object().required(),\n  })\n  .required()\n  .strict(true);\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/pipeline/', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db',\n      responseSchema,\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(expectedPipeline);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetPipeline}`)\n          .query(true)\n          .reply(200, mockResponseSuccess);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client getPipeline will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl).get(`/${RdiUrl.GetPipeline}`).query(true).reply(401, {\n          message: 'Request failed with status code 401',\n          detail: 'Unauthorized',\n        });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/POST-rdi-id-pipeline-deploy.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_deploy';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst errorMessage = 'Authorization failed';\n\nconst endpoint = (id) =>\n  request(server).post(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/deploy`,\n  );\n\nconst validInputData = {\n  config: {},\n  jobs: {},\n};\n\nconst mockResponse = {\n  action_id: 'some_action_id_123',\n};\nconst mockActionResponse = {\n  status: 'completed',\n  data: 'Some successful data',\n  error: null,\n};\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi/:id/pipeline/deploy', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and all client deploy and action calls are success',\n      data: validInputData,\n      statusCode: 201,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({});\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.Deploy}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, mockActionResponse);\n      },\n    },\n    {\n      name: 'Should be success if rdi with :id is in db and all client deploy and action calls are success',\n      data: validInputData,\n      statusCode: 400,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiPipelineDeployFailed',\n          errorCode: 11401,\n          errors: [null],\n          message: 'Failed to deploy pipeline',\n          statusCode: 400,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.Deploy}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, {\n            status: 'failed',\n            data: null,\n            error: 'Error with deploy',\n          });\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      data: validInputData,\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client deploy will not succeed',\n      data: validInputData,\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl).post(`/${RdiUrl.Deploy}`).query(true).reply(401, {\n          message: errorMessage,\n          detail: errorMessage,\n        });\n      },\n    },\n    {\n      name: 'Should throw error if client Action will not succeed',\n      data: validInputData,\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.Deploy}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(401, {\n            message: 'Authorization 2 failed',\n            detail: errorMessage,\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/POST-rdi-id-pipeline-dry-run-job.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { RdiDyRunJobStatus } from 'src/modules/rdi/models';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { Joi, nock } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_dry_run_job';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\n\nconst endpoint = (id) =>\n  request(server).post(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/dry-run-job`,\n  );\n\nconst validInputData = {\n  input_data: { some: 'data' },\n  job: { name: 'job1' },\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    transformations: Joi.object().keys({\n      status: Joi.string().allow('success', 'failed').required(),\n      data: Joi.object()\n        .keys({\n          connections: Joi.any(),\n          dataStreams: Joi.any(),\n          processingPerformance: Joi.any(),\n          rdiPipelineStatus: Joi.any(),\n          clients: Joi.any(),\n        })\n        .optional(),\n      error: Joi.string().optional(),\n    }),\n    commands: Joi.object().keys({\n      status: Joi.string()\n        .allow(RdiDyRunJobStatus.Success, RdiDyRunJobStatus.Fail)\n        .required(),\n      data: Joi.any().optional(),\n      error: Joi.any().optional(),\n    }),\n  })\n  .required()\n  .strict(true);\n\nconst mockResponse = {\n  transformations: {\n    status: RdiDyRunJobStatus.Success,\n  },\n  commands: {\n    status: RdiDyRunJobStatus.Success,\n  },\n};\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi/:id/pipeline/dry-run-job', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db',\n      responseSchema,\n      data: validInputData,\n      statusCode: 201,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(mockResponse);\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.DryRunJob}`)\n          .query(true)\n          .reply(200, mockResponse);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      data: validInputData,\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client dryRunJob will not succeed',\n      data: validInputData,\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: 'Unauthorized',\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl).post(`/${RdiUrl.DryRunJob}`).query(true).reply(401, {\n          message: 'Request failed with status code 401',\n          detail: 'Unauthorized',\n        });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/POST-rdi-id-pipeline-reset.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { mockedAccessToken } from 'src/__mocks__';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_reset';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst errorMessage = 'Authorization failed';\n\nconst endpoint = (id) =>\n  request(server).post(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/reset`,\n  );\n\nconst mockResponse = {\n  action_id: 'some_action_id_123',\n};\nconst mockActionResponse = {\n  status: 'completed',\n  data: 'Some successful data',\n  error: null,\n};\nconst mockErrorMessage = 'Error when resetting a pipeline';\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi/:id/pipeline/reset', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and all client resetPipeline and action calls are success',\n      statusCode: 201,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({});\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.ResetPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, mockActionResponse);\n      },\n    },\n    {\n      name: 'Should throw an error if rdi is ok but the reset pipeline Action call responds success data with failed status',\n      statusCode: 400,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiResetPipelineFailed',\n          errorCode: CustomErrorCodes.RdiResetPipelineFailure,\n          errors: [null],\n          message: ERROR_MESSAGES.RDI_RESET_PIPELINE_FAILURE,\n          statusCode: 400,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.ResetPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, {\n            status: 'failed',\n            data: null,\n            error: mockErrorMessage,\n          });\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client resetPipeline will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.ResetPipeline}`)\n          .query(true)\n          .reply(401, {\n            message: errorMessage,\n            detail: errorMessage,\n          });\n      },\n    },\n    {\n      name: 'Should throw error if client authorization will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.ResetPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(401, {\n            message: 'Authorization 2 failed',\n            detail: errorMessage,\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/POST-rdi-id-pipeline-start.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { mockedAccessToken } from 'src/__mocks__';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_start';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst errorMessage = 'Authorization failed';\n\nconst endpoint = (id) =>\n  request(server).post(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/start`,\n  );\n\nconst mockResponse = {\n  action_id: 'some_action_id_123',\n};\nconst mockActionResponse = {\n  status: 'completed',\n  data: 'Some successful data',\n  error: null,\n};\nconst mockErrorMessage = 'Error when starting a pipeline';\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi/:id/pipeline/start', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and all client startPipeline and action calls are success',\n      statusCode: 201,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({});\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StartPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, mockActionResponse);\n      },\n    },\n    {\n      name: 'Should throw an error if rdi is ok but the start pipeline Action call responds success data with failed status',\n      statusCode: 400,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiStartPipelineFailed',\n          errorCode: CustomErrorCodes.RdiStartPipelineFailure,\n          errors: [null],\n          message: ERROR_MESSAGES.RDI_START_PIPELINE_FAILURE,\n          statusCode: 400,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StartPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, {\n            status: 'failed',\n            data: null,\n            error: mockErrorMessage,\n          });\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client startPipeline will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StartPipeline}`)\n          .query(true)\n          .reply(401, {\n            message: errorMessage,\n            detail: errorMessage,\n          });\n      },\n    },\n    {\n      name: 'Should throw error if client authorization will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StartPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(401, {\n            message: 'Authorization 2 failed',\n            detail: errorMessage,\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/POST-rdi-id-pipeline-stop.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\nimport { nock } from '../../../helpers/test';\nimport { CustomErrorCodes } from 'src/constants';\nimport ERROR_MESSAGES from 'src/constants/error-messages';\nimport { mockedAccessToken } from 'src/__mocks__';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_stop';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\nconst errorMessage = 'Authorization failed';\n\nconst endpoint = (id) =>\n  request(server).post(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/stop`,\n  );\n\nconst mockResponse = {\n  action_id: 'some_action_id_123',\n};\nconst mockActionResponse = {\n  status: 'completed',\n  data: 'Some successful data',\n  error: null,\n};\nconst mockErrorMessage = 'Error when stopping a pipeline';\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi/:id/pipeline/stop', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and all client stopPipeline and action calls are success',\n      statusCode: 201,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({});\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StopPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, mockActionResponse);\n      },\n    },\n    {\n      name: 'Should throw an error if rdi is ok but the stop pipeline Action call responds success data with failed status',\n      statusCode: 400,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiStopPipelineFailed',\n          errorCode: CustomErrorCodes.RdiStopPipelineFailure,\n          errors: [null],\n          message: ERROR_MESSAGES.RDI_STOP_PIPELINE_FAILURE,\n          statusCode: 400,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StopPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(200, {\n            status: 'failed',\n            data: null,\n            error: mockErrorMessage,\n          });\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should throw error if client stopPipeline will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StopPipeline}`)\n          .query(true)\n          .reply(401, {\n            message: errorMessage,\n            detail: errorMessage,\n          });\n      },\n    },\n    {\n      name: 'Should throw error if client authorization will not succeed',\n      statusCode: 401,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'RdiUnauthorized',\n          errorCode: 11402,\n          message: errorMessage,\n          statusCode: 401,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n          access_token: mockedAccessToken,\n        });\n        nock(testRdiUrl)\n          .post(`/${RdiUrl.StopPipeline}`)\n          .query(true)\n          .reply(200, mockResponse);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.Action}/${mockResponse.action_id}`)\n          .query(true)\n          .reply(401, {\n            message: 'Authorization 2 failed',\n            detail: errorMessage,\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/pipeline/POST-rdi-id-pipeline-test-connections.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { RdiTestConnectionStatus } from 'src/modules/rdi/dto';\nimport {\n  describe,\n  expect,\n  deps,\n  getMainCheckFn,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../../deps';\nimport { Joi, nock } from '../../../helpers/test';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_pipeline_test_connections';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\n\nconst endpoint = (id) =>\n  request(server).post(\n    `/${constants.API.RDI}/${id || testRdiId}/pipeline/test-connections`,\n  );\n\nconst dataSchema = Joi.object().optional().strict(true);\n\nconst validInputData = {\n  sources: {},\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    sources: Joi.object()\n      .pattern(\n        Joi.string(),\n        Joi.object({\n          connected: Joi.boolean().required(),\n          error: Joi.string().allow(null).optional(),\n        }),\n      )\n      .required(),\n\n    targets: Joi.object()\n      .keys({\n        target: Joi.object()\n          .keys({\n            status: Joi.string()\n              .valid(\n                RdiTestConnectionStatus.Success,\n                RdiTestConnectionStatus.Fail,\n              )\n              .required(),\n            error: Joi.object()\n              .keys({\n                code: Joi.string(),\n                message: Joi.string(),\n              })\n              .optional(),\n          })\n          .required(),\n      })\n      .required(),\n  })\n  .strict(true);\n\nconst validMockResponses = {\n  sources: {},\n  targets: {\n    target: {\n      status: RdiTestConnectionStatus.Success,\n    },\n  },\n};\n\nconst failedMockResponses = {\n  sources: {},\n  targets: {\n    target: {\n      status: RdiTestConnectionStatus.Fail,\n      error: {\n        code: 'some_code',\n        message: 'Some Error during connection',\n      },\n    },\n  },\n};\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /rdi/:id/pipeline/test-connections', () => {\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).forEach(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n  describe('Common', () => {\n    [\n      {\n        name: 'Should be success if rdi with :id is in db and client testConnections returns with success status',\n        responseSchema,\n        data: validInputData,\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          expect(body).to.eql(validMockResponses);\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n          nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n\n          nock(testRdiUrl)\n            .post(`/${RdiUrl.TestTargetsConnections}`)\n            .query(true)\n            .reply(200, { targets: validMockResponses.targets });\n        },\n      },\n      {\n        name: 'Should be success even if client testConnection returns with failed connection status',\n        responseSchema,\n        data: validInputData,\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          expect(body).to.eql(failedMockResponses);\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n          nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n\n          nock(testRdiUrl)\n            .post(`/${RdiUrl.TestTargetsConnections}`)\n            .query(true)\n            .reply(200, { targets: failedMockResponses.targets });\n        },\n      },\n      {\n        name: 'Should be success even if client testConnection returns with some failed connection status',\n        responseSchema,\n        data: validInputData,\n        statusCode: 201,\n        checkFn: ({ body }) => {\n          expect(body).to.eql({\n            sources: validMockResponses.sources,\n            targets: failedMockResponses.targets,\n          });\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n          nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n\n          nock(testRdiUrl)\n            .post(`/${RdiUrl.TestTargetsConnections}`)\n            .query(true)\n            .reply(200, { targets: failedMockResponses.targets });\n        },\n      },\n      {\n        name: 'Should throw notFoundError if rdi with id in params does not exist',\n        endpoint: () => endpoint(notExistedRdiId),\n        data: validInputData,\n        statusCode: 404,\n        checkFn: ({ body }) => {\n          expect(body).to.eql({\n            error: 'Not Found',\n            message: 'Invalid rdi instance id.',\n            statusCode: 404,\n          });\n        },\n        before: async () => {\n          expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n        },\n      },\n      {\n        name: 'Should throw error if client testConnection will not succeed',\n        data: validInputData,\n        statusCode: 401,\n        checkFn: ({ body }) => {\n          expect(body).to.eql({\n            error: 'RdiUnauthorized',\n            errorCode: 11402,\n            message: 'Unauthorized',\n            statusCode: 401,\n          });\n        },\n        before: async () => {\n          await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n          nock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n            access_token: mockedAccessToken,\n          });\n          nock(testRdiUrl)\n            .post(`/${RdiUrl.TestTargetsConnections}`)\n            .query(true)\n            .reply(401, {\n              message: 'Request failed with status code 401',\n              detail: 'Unauthorized',\n            });\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rdi/statistics/GET-rdi-id-statistics.test.ts",
    "content": "import { RdiUrl } from 'src/modules/rdi/constants';\nimport { sign } from 'jsonwebtoken';\nimport { Joi, nock } from '../../../helpers/test';\nimport { describe, expect, deps, getMainCheckFn } from '../../deps';\n\nconst { localDb, request, server, constants } = deps;\n\nconst testRdiId = 'someTEST_statistics';\nconst notExistedRdiId = 'notExisted';\nconst testRdiUrl = 'http://rdilocal.test';\n\nconst endpoint = (id: string) => {\n  return request(server).get(\n    `/${constants.API.RDI}/${id || testRdiId}/statistics`,\n  );\n};\n\n// Mock response in the OLD format (what RDI API returns)\nconst mockRdiApiResponse = {\n  rdi_pipeline_status: {\n    rdi_version: '1.0.0',\n    address: 'redis://localhost:6379',\n    run_status: 'running',\n    sync_mode: 'streaming',\n  },\n  processing_performance: {\n    total_batches: 100,\n    batch_size_avg: 1.5,\n  },\n  connections: {\n    target1: {\n      status: 'connected',\n      type: 'redis',\n      host: 'localhost',\n      port: 6379,\n      database: 'db1',\n      user: 'user1',\n    },\n  },\n  data_streams: {\n    totals: { total: 300, last_arrival: '' },\n    streams: {\n      stream1: { total: 100, last_arrival: '2024-01-15T10:30:00Z' },\n      stream2: { total: 200, last_arrival: '2024-01-16T11:45:00Z' },\n    },\n  },\n  clients: {\n    client1: { id: 'c1', addr: '127.0.0.1' },\n  },\n};\n\n// Expected response in the NEW format (what our API returns after transformation)\nconst expectedTransformedResponse = {\n  sections: [\n    {\n      name: 'General info',\n      view: 'info',\n      data: [\n        { label: 'RDI version', value: '1.0.0' },\n        { label: 'RDI database address', value: 'redis://localhost:6379' },\n        { label: 'Run status', value: 'running' },\n        { label: 'Sync mode', value: 'streaming' },\n      ],\n    },\n    {\n      name: 'Processing performance information',\n      view: 'blocks',\n      data: [\n        { label: 'Total batches', value: 100, units: 'Total' },\n        { label: 'Batch size average', value: 1.5, units: 'MB' },\n      ],\n    },\n    {\n      name: 'Target Connections',\n      view: 'table',\n      columns: [\n        { id: 'status', header: 'Status', type: 'status' },\n        { id: 'name', header: 'Name' },\n        { id: 'type', header: 'Type' },\n        { id: 'host_port', header: 'Host:port' },\n        { id: 'database', header: 'Database' },\n        { id: 'user', header: 'Username' },\n      ],\n      data: [\n        {\n          status: 'connected',\n          name: 'target1',\n          type: 'redis',\n          host_port: 'localhost:6379',\n          database: 'db1',\n          user: 'user1',\n        },\n      ],\n    },\n    {\n      name: 'Data Streams',\n      view: 'table',\n      columns: [\n        { id: 'name', header: 'Name' },\n        { id: 'total', header: 'Total' },\n        { id: 'last_arrival', header: 'Last arrival', type: 'date' },\n      ],\n      data: [\n        { name: 'stream1', total: 100, last_arrival: '2024-01-15T10:30:00Z' },\n        { name: 'stream2', total: 200, last_arrival: '2024-01-16T11:45:00Z' },\n      ],\n      footer: { name: 'Total', total: 300, last_arrival: '' },\n    },\n    {\n      name: 'Clients',\n      view: 'table',\n      columns: [\n        { id: 'id', header: 'ID' },\n        { id: 'addr', header: 'ADDR' },\n      ],\n      data: [{ id: 'c1', addr: '127.0.0.1' }],\n    },\n  ],\n};\n\nconst statisticsColumnSchema = Joi.object().keys({\n  id: Joi.string().required(),\n  header: Joi.string().required(),\n  type: Joi.string().valid('status', 'date').optional(),\n});\n\nconst statisticsTableSectionSchema = Joi.object().keys({\n  name: Joi.string().required(),\n  view: Joi.string().valid('table').required(),\n  columns: Joi.array().items(statisticsColumnSchema).required(),\n  data: Joi.array().items(Joi.object()).required(),\n  footer: Joi.object().optional(),\n});\n\nconst statisticsBlockItemSchema = Joi.object().keys({\n  label: Joi.string().required(),\n  value: Joi.number().required(),\n  units: Joi.string().required(),\n});\n\nconst statisticsBlocksSectionSchema = Joi.object().keys({\n  name: Joi.string().required(),\n  view: Joi.string().valid('blocks').required(),\n  data: Joi.array().items(statisticsBlockItemSchema).required(),\n});\n\nconst statisticsInfoItemSchema = Joi.object().keys({\n  label: Joi.string().required(),\n  value: Joi.string().required(),\n});\n\nconst statisticsInfoSectionSchema = Joi.object().keys({\n  name: Joi.string().required(),\n  view: Joi.string().valid('info').required(),\n  data: Joi.array().items(statisticsInfoItemSchema).required(),\n});\n\nconst statisticsSectionSchema = Joi.alternatives().try(\n  statisticsTableSectionSchema,\n  statisticsBlocksSectionSchema,\n  statisticsInfoSectionSchema,\n);\n\nconst responseSchema = Joi.object()\n  .keys({\n    status: Joi.string().valid('success', 'failed').required(),\n    data: Joi.object()\n      .keys({\n        sections: Joi.array().items(statisticsSectionSchema).required(),\n      })\n      .optional(),\n    error: Joi.string().optional(),\n  })\n  .required()\n  .strict(true);\n\nconst mockedAccessToken = sign(\n  { exp: Math.trunc(Date.now() / 1000) + 3600 },\n  'test',\n);\nnock(testRdiUrl).post(`/${RdiUrl.Login}`).query(true).reply(200, {\n  access_token: mockedAccessToken,\n});\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /rdi/:id/statistics/', () => {\n  [\n    {\n      name: 'Should be success if rdi with :id is in db and client GetStatistics succeeds',\n      responseSchema,\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          status: 'success',\n          data: expectedTransformedResponse,\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetStatistics}`)\n          .query(true)\n          .reply(200, mockRdiApiResponse);\n      },\n    },\n    {\n      name: 'Should throw notFoundError if rdi with id in params does not exist',\n      endpoint: () => endpoint(notExistedRdiId),\n      statusCode: 404,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          error: 'Not Found',\n          message: 'Invalid rdi instance id.',\n          statusCode: 404,\n        });\n      },\n      before: async () => {\n        expect(await localDb.getRdiById(notExistedRdiId)).to.eql(null);\n      },\n    },\n    {\n      name: 'Should not throw error even if client GetStatistics will not succeed',\n      responseSchema,\n      statusCode: 200,\n      checkFn: ({ body }) => {\n        expect(body).to.eql({\n          status: 'failed',\n          error: 'Request failed with status code 401',\n        });\n      },\n      before: async () => {\n        await localDb.generateRdis({ id: testRdiId, url: testRdiUrl }, 1);\n        nock(testRdiUrl)\n          .get(`/${RdiUrl.GetStatistics}`)\n          .query(true)\n          .reply(401, {\n            message: 'Request failed with status code 401',\n          });\n      },\n    },\n  ].forEach(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/redisearch/DELETE-databases-id-redisearch.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\n\nconst { server, request, constants, rte } = deps;\n\n// API endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/redisearch`,\n  );\n\n// Input data schema\nconst dataSchema = Joi.object({\n  index: Joi.string().required(),\n}).strict();\n\nconst validInputData = {\n  index: constants.TEST_SEARCH_HASH_INDEX_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:id/redisearch', () => {\n  requirements('!rte.bigData', 'rte.modules.search');\n\n  before(async () => {\n    await rte.data.generateRedisearchIndexes(true);\n  });\n\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).forEach(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => rte.data.generateRedisearchIndexes(true));\n\n      [\n        {\n          name: 'Should delete index',\n          data: validInputData,\n          statusCode: 204,\n          before: async () => {\n            // Verify index exists before deletion\n            expect(await rte.client.call('FT._LIST')).to.include(\n              constants.TEST_SEARCH_HASH_INDEX_1,\n            );\n          },\n          after: async () => {\n            // Verify index is deleted after deletion\n            expect(await rte.client.call('FT._LIST')).to.not.include(\n              constants.TEST_SEARCH_HASH_INDEX_1,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('RediSearch version < 2.10.X', () => {\n      requirements('rte.modules.search.version<21000');\n      before(async () => rte.data.generateRedisearchIndexes(true));\n\n      [\n        {\n          name: 'Should return 404 if index does not exist',\n          data: { index: 'non-existing-index' },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            message: 'Unknown Index name',\n            error: 'Not Found',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('RediSearch version >= 2.10.X', () => {\n      requirements('rte.modules.search.version>=21000');\n      before(async () => rte.data.generateRedisearchIndexes(true));\n\n      [\n        {\n          name: 'Should return 404 if index does not exist',\n          data: { index: 'non-existing-index' },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            message: 'non-existing-index: no such index',\n            error: 'Not Found',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => {\n        await rte.data.generateRedisearchIndexes(true);\n        await rte.data.setAclUserRules('~* +@all');\n      });\n\n      [\n        {\n          name: 'Should delete regular index',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: validInputData,\n          statusCode: 204,\n        },\n        {\n          name: 'Should throw error if no permissions for \"FT.DROPINDEX\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            index: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => {\n            // Remove permission for \"FT.DROPINDEX\" command\n            return rte.data.setAclUserRules('~* +@all -FT.DROPINDEX');\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/redisearch/GET-databases-id-redisearch.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  requirements,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${instanceId}/redisearch`);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:id/redisearch', () => {\n  requirements('!rte.bigData', 'rte.modules.search');\n\n  describe('Common', () => {\n    before(async () => rte.data.generateRedisearchIndexes(true));\n\n    [\n      {\n        name: 'Should get index list',\n        checkFn: async ({ body }) => {\n          expect(body.indexes.length).to.eq(2);\n          expect(body.indexes).to.include(\n            constants.TEST_SEARCH_HASH_INDEX_1,\n            constants.TEST_SEARCH_HASH_INDEX_2,\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should get index list',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n      },\n      {\n        name: 'Should throw error if no permissions for \"ft._list\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ft._list'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\n\nconst { server, request, constants, rte, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/redisearch/info`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  index: Joi.string().required(),\n}).strict();\n\nconst validInputData = {\n  index: constants.TEST_SEARCH_HASH_INDEX_1,\n};\n\nconst BASE_RESPONSE_SCHEMA = {\n  index_name: Joi.string().required(),\n  index_options: Joi.object({}),\n  index_definition: {\n    key_type: Joi.string(),\n    prefixes: Joi.array(),\n    default_score: Joi.string(),\n  },\n  attributes: Joi.array().items({\n    identifier: Joi.string(),\n    attribute: Joi.string(),\n    type: Joi.string(),\n    WEIGHT: Joi.string(),\n    SORTABLE: Joi.string(),\n    NOINDEX: Joi.string(),\n    CASESENSITIVE: Joi.string(),\n    UNF: Joi.string(),\n    NOSTEM: Joi.string(),\n    SEPARATOR: Joi.string(),\n  }),\n  inverted_sz_mb: Joi.string(),\n  vector_index_sz_mb: Joi.string(),\n  offset_vectors_sz_mb: Joi.string(),\n  doc_table_size_mb: Joi.string(),\n  sortable_values_size_mb: Joi.string(),\n  tag_overhead_sz_mb: Joi.string(),\n  text_overhead_sz_mb: Joi.string(),\n  total_index_memory_sz_mb: Joi.string(),\n  key_table_size_mb: Joi.string(),\n  geoshapes_sz_mb: Joi.string(),\n  records_per_doc_avg: Joi.string(),\n  bytes_per_record_avg: Joi.string(),\n  offsets_per_term_avg: Joi.string(),\n  offset_bits_per_record_avg: Joi.string(),\n  total_indexing_time: Joi.string(),\n  percent_indexed: Joi.string(),\n  number_of_uses: Joi.number(),\n  cleaning: Joi.number(),\n  gc_stats: Joi.object(),\n  cursor_stats: Joi.object(),\n  dialect_stats: Joi.object(),\n  'Index Errors': Joi.object(),\n  'field statistics': Joi.array().items({\n    identifier: Joi.string(),\n    attribute: Joi.string(),\n    'Index Errors': Joi.object(),\n  }),\n};\n\nconst EXPECTED_SCHEMA_V1 = Joi.object({\n  ...BASE_RESPONSE_SCHEMA,\n  num_docs: Joi.string(),\n  max_doc_id: Joi.string(),\n  num_terms: Joi.string(),\n  num_records: Joi.string(),\n  total_inverted_index_blocks: Joi.string(),\n  hash_indexing_failures: Joi.string(),\n  indexing: Joi.string(),\n  index_definition: Joi.object(BASE_RESPONSE_SCHEMA.index_definition),\n})\n  .required()\n  .strict();\n\nconst EXPECTED_SCHEMA_V2 = Joi.object({\n  ...BASE_RESPONSE_SCHEMA,\n  num_docs: Joi.number(),\n  max_doc_id: Joi.number(),\n  num_terms: Joi.number(),\n  num_records: Joi.number(),\n  total_inverted_index_blocks: Joi.number(),\n  hash_indexing_failures: Joi.number(),\n  indexing: Joi.number(),\n  index_definition: Joi.object({\n    ...BASE_RESPONSE_SCHEMA.index_definition,\n    indexes_all: Joi.string(),\n  }),\n})\n  .required()\n  .strict();\n\nconst INVALID_INDEX_ERROR_MESSAGE_V1: string = 'Unknown Index name';\nconst INVALID_INDEX_ERROR_MESSAGE_V2: string = 'Unknown index name';\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:id/redisearch/info', () => {\n  requirements('!rte.bigData', 'rte.modules.search');\n  before(async () => {\n    await rte.data.generateRedisearchIndexes(true);\n    await localDb.createTestDbInstance(\n      rte,\n      {},\n      { id: constants.TEST_INSTANCE_ID_2 },\n    );\n  });\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).forEach(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common, redisearch version < 2.8.X', () => {\n    requirements('rte.modules.search.version<20800');\n    [\n      {\n        name: 'Should get info index',\n        data: validInputData,\n        responseSchema: EXPECTED_SCHEMA_V1,\n        checkFn: async ({ body }) => {\n          expect(body.index_name).to.eq(constants.TEST_SEARCH_HASH_INDEX_1);\n          expect(body.index_definition?.key_type).to.eq('HASH');\n        },\n      },\n      {\n        name: 'Should throw error if non-existent index provided',\n        data: {\n          index: 'Invalid index',\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: INVALID_INDEX_ERROR_MESSAGE_V1,\n          error: 'Not Found',\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n\n  describe('Common, 2.8.X <= redisearch version  < 2.10.X', () => {\n    requirements(\n      () =>\n        20800 <= rte.env.modules.search.version &&\n        rte.env.modules.search.version < 21000,\n    );\n    [\n      {\n        name: 'Should get info index',\n        data: validInputData,\n        responseSchema: EXPECTED_SCHEMA_V1,\n        checkFn: async ({ body }) => {\n          expect(body.index_name).to.eq(constants.TEST_SEARCH_HASH_INDEX_1);\n          expect(body.index_definition?.key_type).to.eq('HASH');\n        },\n      },\n      {\n        name: 'Should throw error if non-existent index provided',\n        data: {\n          index: 'Invalid index',\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: INVALID_INDEX_ERROR_MESSAGE_V2,\n          error: 'Not Found',\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n\n  describe('Common, redisearch version >= 2.10.X', () => {\n    requirements('rte.modules.search.version>=21000');\n    [\n      {\n        name: 'Should get info index',\n        data: validInputData,\n        responseSchema: EXPECTED_SCHEMA_V2,\n        checkFn: async ({ body }) => {\n          expect(body.index_name).to.eq(constants.TEST_SEARCH_HASH_INDEX_1);\n          expect(body.index_definition?.key_type).to.eq('HASH');\n        },\n      },\n      {\n        name: 'Should throw error if non-existent index provided',\n        data: {\n          index: 'Invalid index',\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid index: no such index',\n          error: 'Not Found',\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-key-indexes.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\n\nconst { server, request, constants, rte, localDb } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/redisearch/key-indexes`,\n  );\n\nconst dataSchema = Joi.object({\n  key: Joi.string().required(),\n}).strict();\n\nconst validInputData = {\n  key: `${constants.TEST_SEARCH_HASH_KEY_PREFIX_1}1`,\n};\n\nconst INDEX_SUMMARY_SCHEMA = Joi.object({\n  name: Joi.string().required(),\n  prefixes: Joi.array().items(Joi.string().allow('')).required(),\n  key_type: Joi.string().required(),\n}).required();\n\nconst RESPONSE_SCHEMA = Joi.object({\n  indexes: Joi.array().items(INDEX_SUMMARY_SCHEMA).required(),\n})\n  .required()\n  .strict();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:id/redisearch/key-indexes', () => {\n  requirements('!rte.bigData', 'rte.modules.search');\n  before(async () => {\n    await rte.data.generateRedisearchIndexes(true);\n    await localDb.createTestDbInstance(\n      rte,\n      {},\n      { id: constants.TEST_INSTANCE_ID_2 },\n    );\n  });\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).forEach(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return matching indexes for a key that matches a prefix',\n        data: validInputData,\n        responseSchema: RESPONSE_SCHEMA,\n        checkFn: async ({ body }) => {\n          expect(body.indexes.length).to.be.gte(1);\n          const names = body.indexes.map((idx) => idx.name);\n          expect(names).to.include(constants.TEST_SEARCH_HASH_INDEX_1);\n        },\n      },\n      {\n        name: 'Should still return indexes with no prefix for an unrelated key',\n        data: {\n          key: 'nonexistent_prefix_zzz:1',\n        },\n        responseSchema: RESPONSE_SCHEMA,\n        checkFn: async ({ body }) => {\n          expect(body.indexes).to.be.an('array');\n        },\n      },\n      {\n        name: 'Should return indexes array with correct structure',\n        data: validInputData,\n        responseSchema: RESPONSE_SCHEMA,\n        checkFn: async ({ body }) => {\n          if (body.indexes.length > 0) {\n            const idx = body.indexes[0];\n            expect(idx).to.have.property('name');\n            expect(idx).to.have.property('prefixes');\n            expect(idx).to.have.property('key_type');\n            expect(idx.prefixes).to.be.an('array');\n          }\n        },\n      },\n    ].forEach(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should get key indexes',\n        data: validInputData,\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n      },\n      {\n        name: 'Should throw error if no permissions for \"ft._list\" command',\n        data: validInputData,\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -ft._list'),\n      },\n    ].forEach(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-search.test.ts",
    "content": "import { numberWithSpaces } from 'src/utils/base.helper';\nimport {\n  expect,\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/redisearch/search`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  index: Joi.string().allow('').required(),\n  query: Joi.string().allow('').required(),\n  limit: Joi.number().integer(),\n  offset: Joi.number().integer(),\n}).strict();\n\nconst validInputData = {\n  index: constants.TEST_SEARCH_HASH_INDEX_1,\n  query: '*',\n  limit: 10,\n  offset: 0,\n};\n\nconst responseSchema = Joi.object({\n  cursor: Joi.number().integer().required(),\n  scanned: Joi.number().integer().required(),\n  total: Joi.number().integer().required(),\n  maxResults: Joi.number().integer().allow(null).required(),\n  keys: Joi.array()\n    .items(\n      Joi.object({\n        name: JoiRedisString.required(),\n        type: Joi.string(),\n      }),\n    )\n    .required(),\n})\n  .required()\n  .strict(true);\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:id/redisearch/search', () => {\n  requirements('!rte.bigData', 'rte.modules.search');\n  before(async () => {\n    await rte.data.generateRedisearchIndexes(true);\n    await localDb.createTestDbInstance(\n      rte,\n      {},\n      { id: constants.TEST_INSTANCE_ID_2 },\n    );\n  });\n\n  describe('Main', () => {\n    before(() => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '10000'));\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should search data (limit 10)',\n          data: validInputData,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.keys.length).to.eq(10);\n            expect(body.cursor).to.eq(10);\n            expect(body.scanned).to.eq(10);\n            expect(body.total).to.eq(2000);\n            expect(body.maxResults).to.gte(10000);\n          },\n        },\n        {\n          name: 'Should search 100 entries (continue from previous 10)',\n          data: {\n            ...validInputData,\n            offset: 10,\n            limit: 100,\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.keys.length).to.eq(100);\n            expect(body.cursor).to.eq(110);\n            expect(body.scanned).to.eq(110);\n            expect(body.total).to.eq(2000);\n            expect(body.maxResults).to.gte(10000);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('maxSearchResults', () => {\n      [\n        {\n          name: 'Should modify limit to not exceed available search limitation',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...validInputData,\n            offset: 0,\n            limit: 10,\n          },\n          checkFn: async ({ body }) => {\n            expect(body.keys.length).to.eq(1);\n            expect(body.cursor).to.eq(10);\n            expect(body.scanned).to.eq(1);\n            expect(body.total).to.eq(2000);\n            expect(body.maxResults).to.gte(1);\n          },\n          before: async () => {\n            await rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1');\n          },\n        },\n        {\n          name: 'Should return custom error message if MAXSEARCHRESULTS less than request.limit',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2),\n          data: {\n            ...validInputData,\n            offset: 10,\n            limit: 10,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n            message: `Set MAXSEARCHRESULTS to at least ${numberWithSpaces(validInputData.limit)}.`,\n          },\n          before: async () => {\n            await rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1');\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => {\n        await rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '10000');\n        await rte.data.setAclUserRules('~* +@all');\n      });\n\n      [\n        {\n          name: 'Should return response with maxResults = null if no permissions for \"ft.config\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: validInputData,\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.keys.length).to.eq(10);\n            expect(body.cursor).to.eq(10);\n            expect(body.scanned).to.eq(10);\n            expect(body.total).to.eq(2000);\n            expect(body.maxResults).to.eq(null);\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -ft.config'),\n        },\n        {\n          name: 'Should search',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: validInputData,\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"ft.search\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            index: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -ft.search'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/redisearch/POST-databases-id-redisearch.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n  _,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/redisearch`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  index: Joi.string().allow('').required(),\n  type: Joi.string().valid('hash', 'json').required(),\n  // prefixes: Joi.array().items(Joi.string()).allow(null),\n  // fields: Joi.array().items(Joi.object({\n  //   name: Joi.string().required(),\n  //   type: Joi.string().valid('text', 'tag', 'numeric', 'geo', 'vector').required(),\n  // }).required()),\n}).strict();\n\nconst validInputData = {\n  index: constants.TEST_SEARCH_HASH_INDEX_1,\n  type: constants.TEST_SEARCH_HASH_TYPE,\n  prefixes: ['*'],\n  fields: [\n    {\n      name: '*',\n      type: 'text',\n    },\n  ],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:id/redisearch', () => {\n  requirements('!rte.bigData', 'rte.modules.search');\n\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(rte.data.truncate);\n\n      [\n        {\n          name: 'Should create index',\n          data: {\n            ...validInputData,\n            index: constants.TEST_SEARCH_HASH_INDEX_1,\n          },\n          statusCode: 201,\n          before: async () => {\n            await validateApiCall({\n              endpoint: () =>\n                request(server).get(\n                  `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/redisearch`,\n                ),\n              checkFn: ({ body }) => {\n                expect(body.indexes.length).to.eq(0);\n              },\n            });\n          },\n          after: async () => {\n            await validateApiCall({\n              endpoint: () =>\n                request(server).get(\n                  `/${constants.API.DATABASES}/${constants.TEST_INSTANCE_ID}/redisearch`,\n                ),\n              checkFn: ({ body }) => {\n                expect(body.indexes.length).to.eq(1);\n                expect(body.indexes).to.include(\n                  constants.TEST_SEARCH_HASH_INDEX_1,\n                );\n              },\n            });\n          },\n        },\n        {\n          name: 'Should throw Conflict Error if such index name already exists',\n          data: {\n            ...validInputData,\n            index: constants.TEST_SEARCH_HASH_INDEX_1,\n          },\n          statusCode: 201,\n          after: async () => {\n            await validateApiCall({\n              endpoint,\n              statusCode: 409,\n              data: {\n                ...validInputData,\n                index: constants.TEST_SEARCH_HASH_INDEX_1,\n              },\n              responseBody: {\n                statusCode: 409,\n                message: 'This index name is already in use.',\n                error: 'Conflict',\n              },\n            });\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('Client Errors (400 Bad Request)', () => {\n      beforeEach(rte.data.truncate);\n\n      [\n        {\n          name: 'Should return 400 for Invalid JSONPath error (JSON type with invalid path)',\n          data: {\n            index: constants.getRandomString(),\n            type: 'json',\n            prefixes: ['test:'],\n            fields: [\n              {\n                name: 'embedding', // Missing $ prefix for JSONPath\n                type: 'vector',\n                algorithm: 'FLAT',\n                dim: 3,\n                distanceMetric: 'L2',\n              },\n            ],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n          checkFn: ({ body }) => {\n            expect(body.message).to.satisfy((msg: string) =>\n              msg.startsWith('Invalid'),\n            );\n          },\n        },\n        {\n          name: 'Should return 400 for Invalid field type error (VECTOR without DIM)',\n          data: {\n            index: constants.getRandomString(),\n            type: 'hash',\n            prefixes: ['test:'],\n            fields: [\n              {\n                name: 'embedding',\n                type: 'vector',\n                algorithm: 'FLAT',\n                // Missing 'dim' which is mandatory for VECTOR\n                distanceMetric: 'L2',\n              },\n            ],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n          checkFn: ({ body }) => {\n            expect(body.message).to.satisfy((msg: string) =>\n              msg.startsWith('Invalid'),\n            );\n          },\n        },\n        {\n          name: 'Should return 400 for Invalid field type error (VECTOR with invalid algorithm)',\n          data: {\n            index: constants.getRandomString(),\n            type: 'hash',\n            prefixes: ['test:'],\n            fields: [\n              {\n                name: 'embedding',\n                type: 'vector',\n                algorithm: 'INVALID_ALGO', // Invalid algorithm\n                dim: 3,\n                distanceMetric: 'L2',\n              },\n            ],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n          checkFn: ({ body }) => {\n            expect(body.message).to.satisfy((msg: string) =>\n              msg.startsWith('Invalid'),\n            );\n          },\n        },\n        {\n          name: 'Should return 400 for Duplicate field error',\n          data: {\n            index: constants.getRandomString(),\n            type: 'hash',\n            prefixes: ['test:'],\n            fields: [\n              {\n                name: 'field1',\n                type: 'text',\n              },\n              {\n                name: 'field1', // Duplicate field name\n                type: 'text',\n              },\n            ],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n          checkFn: ({ body }) => {\n            expect(body.message).to.satisfy((msg: string) =>\n              msg.startsWith('Duplicate'),\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular index',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            index: constants.getRandomString(),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"ft.info\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            index: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -ft.info'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"ft.create\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            index: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -ft.create'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rejson-rl/DELETE-databases-id-rejson_rl.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  _,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(`/${constants.API.DATABASES}/${instanceId}/rejson-rl`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  path: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  path: '$',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/rejson-rl', () => {\n  requirements('rte.modules.rejson');\n\n  before(async () => await rte.data.generateKeys(true));\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    describe('re-json v1', () => {\n      requirements('rte.modules.rejson.version<20000');\n\n      [\n        {\n          name: 'Should delete element from nested object by path (from buf)',\n          data: {\n            keyName: {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_REJSON_KEY_3)],\n            },\n            path: '.object.field',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          before: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '.',\n              ),\n            );\n            expect(json.object).to.have.property('field');\n          },\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '.',\n              ),\n            );\n            expect(json).to.deep.eql(\n              _.omit(constants.TEST_REJSON_VALUE_3, 'object.field'),\n            );\n          },\n        },\n        {\n          name: 'Should delete element from array by path',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.array[1]',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          before: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '.',\n              ),\n            );\n            expect(json.array.length).to.eql(3);\n          },\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '.',\n              ),\n            );\n            expect(json.array.length).to.eql(2);\n          },\n        },\n        {\n          name: 'Should not affect json if not existing path',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.not_existing_path',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 0,\n          },\n        },\n        {\n          name: 'Should delete entire json and remove the key',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_REJSON_KEY_3)).to.eql(\n              0,\n            );\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.getRandomString()),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_1,\n                '.',\n              ),\n            );\n            expect(json).to.deep.eql(constants.TEST_REJSON_VALUE_1);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('re-json v2', () => {\n      requirements('rte.modules.rejson.version>=20000');\n\n      [\n        {\n          name: 'Should delete element from nested object by path (from buf)',\n          data: {\n            keyName: {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_REJSON_KEY_3)],\n            },\n            path: '$.object.field',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          before: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '$',\n              ),\n            );\n            expect(json[0].object).to.have.property('field');\n          },\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '$',\n              ),\n            );\n            expect(json[0]).to.deep.eql(\n              _.omit(constants.TEST_REJSON_VALUE_3, 'object.field'),\n            );\n          },\n        },\n        {\n          name: 'Should delete element from array by path',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '$.array[1]',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          before: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '$',\n              ),\n            );\n            expect(json[0].array.length).to.eql(3);\n          },\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_3,\n                '$',\n              ),\n            );\n            expect(json[0].array.length).to.eql(2);\n          },\n        },\n        {\n          name: 'Should not affect json if not existing path',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.not_existing_path',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 0,\n          },\n        },\n        {\n          name: 'Should delete entire json and remove the key',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '$',\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_REJSON_KEY_3)).to.eql(\n              0,\n            );\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.getRandomString()),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_1,\n                '$',\n              ),\n            );\n            expect(json[0]).to.deep.eql(constants.TEST_REJSON_VALUE_1);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should create regular item',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_REJSON_KEY_1,\n          path: '.n',\n        },\n        responseSchema,\n      },\n      {\n        name: 'Should throw error if no permissions for \"json.del\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_REJSON_KEY_1,\n          path: '.n',\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -json.del'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rejson-rl/PATCH-databases-id-rejson_rl-arrappend.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/rejson-rl/arrappend`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  data: Joi.array()\n    .items(\n      Joi.string()\n        .required()\n        .messages({\n          'any.required': '{#label} should be a correct serialized json string',\n        })\n        .label('data'),\n    )\n    .required()\n    .messages({\n      'any.required': '{#label} must be an array',\n      'array.sparse': 'each value in data must be a string',\n    }),\n  path: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  data: [JSON.stringify(constants.getRandomString())],\n  path: '$',\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/rejson-rl/arrappend', () => {\n  requirements('rte.modules.rejson');\n\n  before(async () => await rte.data.generateKeys(true));\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    describe('re-json v1', () => {\n      requirements('rte.modules.rejson.version<20000');\n      [\n        {\n          name: 'Should append array (from buf)',\n          data: {\n            keyName: {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_REJSON_KEY_2)],\n            },\n            data: [JSON.stringify([1, 2])],\n            path: '.',\n          },\n          statusCode: 200,\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_2,\n                '.',\n              ),\n            );\n            expect(json).to.eql([...constants.TEST_REJSON_VALUE_2, [1, 2]]);\n          },\n        },\n        {\n          name: 'Should append multiple items into array.array',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_2,\n            data: [JSON.stringify(null), JSON.stringify('somestring')],\n            path: '[1]',\n          },\n          statusCode: 200,\n          before: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_2,\n                '[1]',\n              ),\n            );\n            expect(json).to.eql([1, 2]);\n          },\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_2,\n                '.',\n              ),\n            );\n            expect(json).to.eql([\n              ...constants.TEST_REJSON_VALUE_2,\n              [1, 2, null, 'somestring'],\n            ]);\n          },\n        },\n        {\n          name: 'Should return BadRequest if try to append to not array item',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_2,\n            data: [JSON.stringify(constants.getRandomString())],\n            path: '[1][1]',\n          },\n          // todo: handle error to return 400 instead of 500 (BE)\n          statusCode: 500,\n          responseBody: {\n            statusCode: 500,\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_2,\n            data: JSON.stringify(constants.getRandomString()),\n            path: '.',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('re-json v2', () => {\n      requirements('rte.modules.rejson.version>=20000');\n      [\n        {\n          name: 'Should append array (from buf)',\n          data: {\n            keyName: {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_REJSON_KEY_2)],\n            },\n            data: [JSON.stringify([1, 2])],\n            path: '.',\n          },\n          statusCode: 200,\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_2,\n                '$',\n              ),\n            );\n            expect(json[0]).to.eql([...constants.TEST_REJSON_VALUE_2, [1, 2]]);\n          },\n        },\n        {\n          name: 'Should append multiple items into array.array',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_2,\n            data: [JSON.stringify(null), JSON.stringify('somestring')],\n            path: '$[1]',\n          },\n          statusCode: 200,\n          before: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_2,\n                '$[1]',\n              ),\n            );\n            expect(json[0]).to.eql([1, 2]);\n          },\n          after: async () => {\n            const json = JSON.parse(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_2,\n                '$',\n              ),\n            );\n            expect(json[0]).to.eql([\n              ...constants.TEST_REJSON_VALUE_2,\n              [1, 2, null, 'somestring'],\n            ]);\n          },\n        },\n        {\n          name: 'Should return BadRequest if try to append to not array item',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_2,\n            data: [JSON.stringify(constants.getRandomString())],\n            path: '$[1][1]',\n          },\n          // todo: handle error to return 400 instead of 500 (BE)\n          statusCode: 500,\n          responseBody: {\n            statusCode: 500,\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_2,\n            data: JSON.stringify(constants.getRandomString()),\n            path: '$',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should modify json',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_REJSON_KEY_2,\n          data: [JSON.stringify([1, 2])],\n          path: '.',\n        },\n        statusCode: 200,\n      },\n      {\n        name: 'Should throw error if no permissions for \"json.arrappend\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_REJSON_KEY_2,\n          data: [JSON.stringify(constants.getRandomString())],\n          path: '.',\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -json.arrappend'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rejson-rl/PATCH-databases-id-rejson_rl-set.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/rejson-rl/set`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  data: Joi.string().required().messages({\n    'any.required': '{#label} should be a correct serialized json string',\n  }),\n  path: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  data: JSON.stringify(constants.TEST_REJSON_VALUE_1),\n  path: '$',\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/rejson-rl/set', () => {\n  requirements('rte.modules.rejson');\n\n  before(async () => await rte.data.generateKeys(true));\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    describe('re-json v1', () => {\n      requirements('rte.modules.rejson.version<20000');\n      [\n        {\n          name: 'Should modify item with empty value (from buff)',\n          data: {\n            keyName: {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_REJSON_KEY_1)],\n            },\n            data: JSON.stringify(''),\n            path: 'test',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '.',\n                ),\n              ),\n            ).to.eql({ test: '' });\n          },\n        },\n        {\n          name: 'Should modify item with null value',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(null),\n            path: 'test',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '.',\n                ),\n              ),\n            ).to.eql({ test: null });\n          },\n        },\n        {\n          name: 'Should modify item with array in the root',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify([1, 2]),\n            path: '.',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '.',\n                ),\n              ),\n            ).to.eql([1, 2]);\n          },\n        },\n        {\n          name: 'Should modify item with object in the root',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify({ test: 'test' }),\n            path: '$',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_1,\n                '.',\n              ),\n            ).to.eql(JSON.stringify({ test: 'test' }));\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.getRandomString()),\n            path: '.',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('re-json v2', () => {\n      requirements('rte.modules.rejson.version>=20000');\n      [\n        {\n          name: 'Should modify item with empty value (from buff)',\n          data: {\n            keyName: {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_REJSON_KEY_1)],\n            },\n            data: JSON.stringify(''),\n            path: 'test',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '$',\n                ),\n              )[0],\n            ).to.eql({ test: '' });\n          },\n        },\n        {\n          name: 'Should modify item with null value',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(null),\n            path: 'test',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '$',\n                ),\n              )[0],\n            ).to.eql({ test: null });\n          },\n        },\n        {\n          name: 'Should modify item with array in the root',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify([1, 2]),\n            path: '$',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '$',\n                ),\n              )[0],\n            ).to.eql([1, 2]);\n          },\n        },\n        {\n          name: 'Should modify item with object in the root',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify({ test: 'test' }),\n            path: '$',\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_1,\n                '.',\n              ),\n            ).to.eql(JSON.stringify({ test: 'test' }));\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.getRandomString()),\n            path: '.',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should modify json',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_REJSON_KEY_1,\n          data: JSON.stringify([1, 2]),\n          path: '.',\n        },\n        statusCode: 200,\n        after: async () => {\n          expect(\n            await rte.data.executeCommand(\n              'json.get',\n              constants.TEST_REJSON_KEY_1,\n              '.',\n            ),\n          ).to.eql(JSON.stringify([1, 2]));\n        },\n      },\n      {\n        name: 'Should throw error if no permissions for \"json.set\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          keyName: constants.TEST_REJSON_KEY_1,\n          data: JSON.stringify(constants.getRandomString()),\n          path: '.',\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -json.set'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts",
    "content": "import {\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/rejson-rl/get`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  path: Joi.string(),\n  forceRetrieve: Joi.boolean(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  path: '$',\n  forceRetrieve: false,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    downloaded: Joi.boolean().required(),\n    path: Joi.string().required(),\n    type: Joi.string(),\n    data: Joi.any(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/rejson-rl/get', () => {\n  requirements('rte.modules.rejson');\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Modes', () => {\n      describe('re-json v1', () => {\n        requirements('rte.modules.rejson.version<20000');\n\n        [\n          {\n            name: 'Should force get entire json from buff',\n            data: {\n              keyName: {\n                type: 'Buffer',\n                data: [...Buffer.from(constants.TEST_REJSON_KEY_3)],\n              },\n              path: '.',\n              forceRetrieve: true,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '.',\n              data: JSON.stringify(constants.TEST_REJSON_VALUE_3),\n            },\n          },\n        ].map(mainCheckFn);\n      });\n      describe('re-json v2', () => {\n        requirements('rte.modules.rejson.version>=20000');\n\n        [\n          {\n            name: 'Should force get entire json from buff',\n            data: {\n              keyName: {\n                type: 'Buffer',\n                data: [...Buffer.from(constants.TEST_REJSON_KEY_3)],\n              },\n              path: '$',\n              forceRetrieve: true,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '$',\n              data: JSON.stringify(constants.TEST_REJSON_VALUE_3),\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('Common', () => {\n      describe('re-json v1', () => {\n        requirements('rte.modules.rejson.version<20000');\n        [\n          {\n            name: 'Should force get entire json',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_3,\n              path: '.',\n              forceRetrieve: true,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '.',\n              data: JSON.stringify(constants.TEST_REJSON_VALUE_3),\n            },\n          },\n          {\n            name: 'Should get nested object',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_3,\n              path: '.object.field',\n              forceRetrieve: false,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '.object.field',\n              data: `\"${'value'}\"`,\n            },\n          },\n          {\n            name: 'Should get nested array value (downloaded true due to size)',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_3,\n              path: '[\"array\"][1]',\n              forceRetrieve: false,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '[\"array\"][1]',\n              data: String(2),\n            },\n          },\n          {\n            name: 'Should return NotFound error if instance id does not exists',\n            endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n            data: {\n              keyName: constants.TEST_REJSON_KEY_1,\n              path: '[\"object\"][\"some\"]',\n              forceRetrieve: false,\n            },\n            statusCode: 404,\n            responseBody: {\n              statusCode: 404,\n              error: 'Not Found',\n              message: 'Invalid database instance id.',\n            },\n          },\n        ].map(mainCheckFn);\n      });\n\n      describe('re-json v2', () => {\n        requirements('rte.modules.rejson.version>=20000');\n        [\n          {\n            name: 'Should force get entire json',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_3,\n              path: '$',\n              forceRetrieve: true,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '$',\n              data: JSON.stringify(constants.TEST_REJSON_VALUE_3),\n            },\n          },\n          {\n            name: 'Should get nested object',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_3,\n              path: '$.object.field',\n              forceRetrieve: false,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '$.object.field',\n              data: `\"${'value'}\"`,\n            },\n          },\n          {\n            name: 'Should get nested array value (downloaded true due to size)',\n            data: {\n              keyName: constants.TEST_REJSON_KEY_3,\n              path: '$[\"array\"][1]',\n              forceRetrieve: false,\n            },\n            responseSchema,\n            responseBody: {\n              downloaded: true,\n              path: '$[\"array\"][1]',\n              data: String(2),\n            },\n          },\n          {\n            name: 'Should return NotFound error if instance id does not exists',\n            endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n            data: {\n              keyName: constants.TEST_REJSON_KEY_1,\n              path: '$[\"object\"][\"some\"]',\n              forceRetrieve: false,\n            },\n            statusCode: 404,\n            responseBody: {\n              statusCode: 404,\n              error: 'Not Found',\n              message: 'Invalid database instance id.',\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('Large key value', () => {\n      // todo: do not forget to remove module version check after fixing MEMORY USAGE issue in RedisJSON v2.0.0\n      requirements('rte.modules.rejson.version<20000');\n      [\n        {\n          name: 'Should get json with calculated cardinality',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          responseSchema,\n          responseBody: {\n            downloaded: false,\n            path: '.',\n            type: 'object',\n            data: [\n              {\n                type: 'array',\n                key: 'array',\n                path: '[\"array\"]',\n                cardinality: 3,\n              },\n              {\n                type: 'object',\n                key: 'object',\n                path: '[\"object\"]',\n                cardinality: 2,\n              },\n            ],\n          },\n        },\n        {\n          name: 'Should get safe large string from the object', // todo: do not forget to implement partially string download for JSON\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '[\"object\"][\"some\"]',\n            forceRetrieve: false,\n          },\n          responseSchema,\n          responseBody: {\n            downloaded: false,\n            path: '[\"object\"][\"some\"]',\n            data: `\"${constants.TEST_REJSON_VALUE_3.object.some}\"`, // full value right now\n            type: 'string',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      // todo: do not forget to remove rte.modules.rejson check after fixing MEMORY USAGE issue in RedisJSON v2.0.0\n      requirements('rte.acl', 'rte.modules.rejson.version<20000');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            path: '.',\n            forceRetrieve: false,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.get\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            path: '.',\n            forceRetrieve: true,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.get'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.get\" command (another)',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            path: '.',\n            forceRetrieve: false,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.get'),\n        },\n        {\n          name: 'Should return regular item if no permissions for \"json.debug\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          responseSchema,\n          responseBody: {\n            downloaded: true,\n            path: '.',\n            data: constants.TEST_REJSON_VALUE_3,\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.debug'),\n        },\n        {\n          name: 'Should get full json if no permissions for \"json.debug\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          responseSchema,\n          responseBody: {\n            downloaded: true,\n            path: '.',\n            data: constants.TEST_REJSON_VALUE_3,\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.debug'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.objkeys\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.objkeys'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.type\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.type'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.objlen\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.objlen'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.arrlen\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_3,\n            path: '.',\n            forceRetrieve: false,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.arrlen'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/rejson-rl`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  data: Joi.string().required().messages({\n    'any.required': '{#label} should be a correct serialized json string',\n  }),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_SET_KEY_1,\n  data: JSON.stringify(constants.TEST_REJSON_VALUE_1),\n  expire: constants.TEST_SET_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst createCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      }\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n        expect(\n          JSON.parse(\n            await rte.data.executeCommand(\n              'json.get',\n              testCase.data.keyName,\n              '.',\n            ),\n          ),\n        ).to.deep.eql(JSON.parse(testCase.data.data));\n        if (testCase.data.expire) {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.gte(\n            testCase.data.expire - 5,\n          );\n        } else {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.eql(-1);\n        }\n      }\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/rejson-rl', () => {\n  requirements('rte.modules.rejson');\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should create json from buff',\n        data: {\n          keyName: constants.TEST_REJSON_KEY_BIN_BUF_OBJ_1,\n          data: JSON.stringify(constants.TEST_REJSON_VALUE_1),\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_REJSON_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            JSON.parse(\n              await rte.data.sendCommand('json.get', [\n                constants.TEST_REJSON_KEY_BIN_BUFFER_1,\n                '.',\n              ]),\n            ),\n          ).to.deep.eql(constants.TEST_REJSON_VALUE_1);\n        },\n      },\n      {\n        name: 'Should create json from ascii',\n        data: {\n          keyName: constants.TEST_REJSON_KEY_BIN_ASCII_1,\n          data: JSON.stringify(constants.TEST_REJSON_VALUE_1),\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_REJSON_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            JSON.parse(\n              await rte.data.sendCommand('json.get', [\n                constants.TEST_REJSON_KEY_BIN_BUFFER_1,\n                '.',\n              ]),\n            ),\n          ).to.deep.eql(constants.TEST_REJSON_VALUE_1);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should create item with empty value',\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify(''),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with null',\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify(null),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with boolean',\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify(true),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with array',\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify([1, 2, 3, 'somestring']),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with object',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.TEST_REJSON_VALUE_1),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with key ttl',\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify(constants.getRandomString()),\n            expire: constants.TEST_REJSON_EXPIRE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should return conflict error if key already exists',\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.getRandomString()),\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n            message: 'This key name is already in use.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '.',\n                ),\n              ),\n            ).to.deep.eql(constants.TEST_REJSON_VALUE_1);\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_REJSON_KEY_1,\n            data: JSON.stringify(constants.getRandomString()),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            expect(\n              JSON.parse(\n                await rte.data.executeCommand(\n                  'json.get',\n                  constants.TEST_REJSON_KEY_1,\n                  '.',\n                ),\n              ),\n            ).to.deep.eql(constants.TEST_REJSON_VALUE_1);\n          },\n        },\n      ].map(createCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify(constants.getRandomString()),\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"json.set\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            data: JSON.stringify(constants.getRandomString()),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -json.set'),\n        },\n      ].map(createCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/reporters.json",
    "content": "{\n  \"reporterEnabled\": \"spec,@mochajs/json-file-reporter,mocha-junit-reporter\",\n  \"mochajsJsonFileReporterReporterOptions\": {\n    \"output\": \"coverage/test-run-result.json\"\n  },\n  \"mochaJunitReporterReporterOptions\": {\n    \"mochaFile\": \"coverage/test-run-result.xml\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/api/test/api/sentinel/POST-redis_sentinel-databases.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  after,\n  it,\n  deps,\n  requirements,\n  validateApiCall,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { rte, request, server, constants, localDb } = deps;\n\nconst endpoint = () => request(server).post('/redis-sentinel/databases');\n\n// input data schema\nconst dataSchema = Joi.object({\n  host: Joi.string().required(),\n  port: Joi.number().integer().required(),\n  db: Joi.number().integer().allow(null),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst baseSentinelData = {\n  alias: constants.getRandomString(),\n  name: constants.TEST_SENTINEL_MASTER_GROUP,\n  username: constants.TEST_SENTINEL_MASTER_USER || null,\n  password: constants.TEST_SENTINEL_MASTER_PASS || null,\n};\n\nconst baseDatabaseData = {\n  host: constants.TEST_REDIS_HOST,\n  port: constants.TEST_REDIS_PORT,\n  username: constants.TEST_REDIS_USER || undefined,\n  password: constants.TEST_REDIS_PASSWORD || undefined,\n  masters: [baseSentinelData],\n};\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object({\n      id: Joi.string().required(),\n      name: Joi.string().required(),\n      status: Joi.string().required(),\n      message: Joi.string().required(),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /redis-sentinel/databases', () => {\n  requirements('rte.type=SENTINEL');\n  after(localDb.initAgreements);\n\n  describe('Validation', function () {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n\n    [\n      {\n        name: 'should deprecate to pass both cert id and other cert fields',\n        data: {\n          ...validInputData,\n          caCert: {\n            id: 'id',\n            name: 'ca',\n            certificate: 'ca_certificate',\n          },\n          clientCert: {\n            id: 'id',\n            name: 'client',\n            certificate: 'client_cert',\n            key: 'client_key',\n          },\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n        checkFn: ({ body }) => {\n          expect(body.message).to.contain(\n            'caCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'caCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property name should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property certificate should not exist',\n          );\n          expect(body.message).to.contain(\n            'clientCert.property key should not exist',\n          );\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  // todo: cover connection error for incorrect host + port [describe('common')]\n  describe('NO TLS', () => {\n    requirements('!rte.tls');\n    it('Create sentinel database', async () => {\n      const dbName = constants.getRandomString();\n\n      await validateApiCall({\n        endpoint,\n        statusCode: 201,\n        data: {\n          ...baseDatabaseData,\n          masters: [\n            {\n              ...baseSentinelData,\n              alias: dbName,\n            },\n          ],\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(1);\n          expect(body[0].name).to.eql(constants.TEST_SENTINEL_MASTER_GROUP);\n          expect(body[0].status).to.eql('success');\n          expect(body[0].message).to.eql('Added');\n\n          const db: any = await (\n            await localDb.getRepository(localDb.repositories.DATABASE)\n          ).findOneBy({\n            id: body[0].id,\n          });\n\n          expect(db.password).to.eql(\n            localDb.encryptData(constants.TEST_REDIS_PASSWORD),\n          );\n          expect(db.sentinelMasterPassword).to.eql(\n            localDb.encryptData(constants.TEST_SENTINEL_MASTER_PASS),\n          );\n        },\n      });\n    });\n    it('Create sentinel database with particular db index', async () => {\n      let addedId;\n      const dbName = constants.getRandomString();\n      const cliUuid = constants.getRandomString();\n      const browserKeyName = constants.getRandomString();\n      const cliKeyName = constants.getRandomString();\n\n      await validateApiCall({\n        endpoint,\n        statusCode: 201,\n        data: {\n          ...baseDatabaseData,\n          masters: [\n            {\n              ...baseSentinelData,\n              alias: dbName,\n              db: constants.TEST_REDIS_DB_INDEX,\n            },\n          ],\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.length).to.eql(1);\n          addedId = body[0].id;\n          expect(body[0].name).to.eql(constants.TEST_SENTINEL_MASTER_GROUP);\n          expect(body[0].status).to.eql('success');\n          expect(body[0].message).to.eql('Added');\n        },\n      });\n\n      // Create string using Browser API to particular db index\n      await validateApiCall({\n        endpoint: () =>\n          request(server).post(`/${constants.API.DATABASES}/${addedId}/string`),\n        statusCode: 201,\n        data: {\n          keyName: browserKeyName,\n          value: 'somevalue',\n        },\n      });\n\n      // Create client for CLI\n      await validateApiCall({\n        endpoint: () =>\n          request(server).patch(\n            `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}`,\n          ),\n        statusCode: 200,\n      });\n\n      // Create string using CLI API to 0 db index\n      await validateApiCall({\n        endpoint: () =>\n          request(server).post(\n            `/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}/send-command`,\n          ),\n        statusCode: 200,\n        data: {\n          command: `set ${cliKeyName} somevalue`,\n        },\n      });\n\n      // check data created by db index\n      await rte.data.executeCommand(\n        'select',\n        `${constants.TEST_REDIS_DB_INDEX}`,\n      );\n      expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1);\n      expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(1);\n\n      // check data created by db index\n      await rte.data.executeCommand('select', '0');\n      expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0);\n      expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(0);\n    });\n  });\n  describe('TLS AUTH', () => {\n    requirements('rte.tlsAuth');\n\n    it('Create sentinel database (tls)', async () => {\n      const dbName = constants.getRandomString();\n      const newCaName = constants.getRandomString();\n      const newClientCertName = constants.getRandomString();\n\n      await validateApiCall({\n        endpoint,\n        statusCode: 201,\n        data: {\n          ...baseDatabaseData,\n          tls: true,\n          verifyServerCert: true,\n          caCert: {\n            name: newCaName,\n            certificate: constants.TEST_REDIS_TLS_CA,\n          },\n          clientCert: {\n            name: newClientCertName,\n            certificate: constants.TEST_USER_TLS_CERT,\n            key: constants.TEST_USER_TLS_KEY,\n          },\n          masters: [\n            {\n              ...baseSentinelData,\n              alias: dbName,\n            },\n          ],\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(1);\n          expect(body[0].name).to.eql(constants.TEST_SENTINEL_MASTER_GROUP);\n          expect(body[0].status).to.eql('success');\n          expect(body[0].message).to.eql('Added');\n\n          const db: any = await (\n            await localDb.getRepository(localDb.repositories.DATABASE)\n          ).findOneBy({\n            id: body[0].id,\n          });\n\n          expect(db.password).to.eql(\n            localDb.encryptData(constants.TEST_REDIS_PASSWORD),\n          );\n          expect(db.sentinelMasterPassword).to.eql(\n            localDb.encryptData(constants.TEST_SENTINEL_MASTER_PASS),\n          );\n          expect(db.caCert.certificate).to.eql(\n            localDb.encryptData(constants.TEST_REDIS_TLS_CA),\n          );\n          expect(db.clientCert.certificate).to.eql(\n            localDb.encryptData(constants.TEST_USER_TLS_CERT),\n          );\n          expect(db.clientCert.key).to.eql(\n            localDb.encryptData(constants.TEST_USER_TLS_KEY),\n          );\n        },\n      });\n    });\n    it('Create sentinel database with plain auth data', async () => {\n      await localDb.setAgreements({\n        encryption: false,\n      });\n\n      const dbName = constants.getRandomString();\n      const newCaName = constants.getRandomString();\n      const newClientCertName = constants.getRandomString();\n\n      await validateApiCall({\n        endpoint,\n        statusCode: 201,\n        data: {\n          ...baseDatabaseData,\n          tls: true,\n          verifyServerCert: true,\n          caCert: {\n            name: newCaName,\n            certificate: constants.TEST_REDIS_TLS_CA,\n          },\n          clientCert: {\n            name: newClientCertName,\n            certificate: constants.TEST_USER_TLS_CERT,\n            key: constants.TEST_USER_TLS_KEY,\n          },\n          masters: [\n            {\n              ...baseSentinelData,\n              alias: dbName,\n            },\n          ],\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(1);\n          expect(body[0].name).to.eql(constants.TEST_SENTINEL_MASTER_GROUP);\n          expect(body[0].status).to.eql('success');\n          expect(body[0].message).to.eql('Added');\n\n          const db: any = await (\n            await localDb.getRepository(localDb.repositories.DATABASE)\n          ).findOneBy({\n            id: body[0].id,\n          });\n\n          expect(db.password).to.eql(constants.TEST_REDIS_PASSWORD);\n          expect(db.sentinelMasterPassword).to.eql(\n            constants.TEST_SENTINEL_MASTER_PASS,\n          );\n          expect(db.caCert.certificate).to.eql(constants.TEST_REDIS_TLS_CA);\n          expect(db.clientCert.certificate).to.eql(\n            constants.TEST_USER_TLS_CERT,\n          );\n          expect(db.clientCert.key).to.eql(constants.TEST_USER_TLS_KEY);\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/sentinel/POST-redis_sentinel-get_databases.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  after,\n  deps,\n  requirements,\n  getMainCheckFn,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  _,\n} from '../deps';\nconst { request, server, constants, localDb } = deps;\n\nconst endpoint = () => request(server).post('/redis-sentinel/get-databases');\nconst mainCheckFn = getMainCheckFn(endpoint);\n\n// input data schema\nconst dataSchema = Joi.object({\n  host: Joi.string().required(),\n  port: Joi.number().integer().required(),\n  username: Joi.string().allow(null),\n  password: Joi.string().allow(null),\n  tls: Joi.boolean().allow(null),\n  tlsServername: Joi.string().allow(null),\n  verifyServerCert: Joi.boolean().allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict(true);\n\nconst validInputData = {\n  name: constants.getRandomString(),\n  host: constants.getRandomString(),\n  port: 111,\n};\n\nconst responseSchema = Joi.array().items(\n  Joi.object().keys({\n    host: Joi.string().required(),\n    port: Joi.number().required(),\n    name: Joi.string().required(),\n    status: Joi.string().required(),\n    numberOfSlaves: Joi.number().required(),\n    nodes: Joi.array()\n      .items(\n        Joi.object({\n          host: Joi.string().required(),\n          port: Joi.number().required(),\n        }),\n      )\n      .required(),\n  }),\n);\n\ndescribe('POST /redis-sentinel/get-databases', () => {\n  requirements('rte.type=SENTINEL');\n  after(localDb.initAgreements);\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('NO TLS', () => {\n    requirements('!rte.tls');\n\n    [\n      {\n        name: 'Get list of master groups',\n        data: {\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n          username: constants.TEST_REDIS_USER,\n          password: constants.TEST_REDIS_PASSWORD,\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.length).to.gte(1);\n          const sentinelMaster = _.find(\n            body,\n            ({ name }) => name === constants.TEST_SENTINEL_MASTER_GROUP,\n          );\n          // since there is no other sentinel for current RTE\n          expect(sentinelMaster.nodes).to.eql([]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('TLS AUTH', () => {\n    requirements('rte.tlsAuth');\n    let caCerts = 0;\n    let clientCerts = 0;\n\n    [\n      {\n        name: \"Get list of master groups but shouldn't create certs on this step (current implementation)\",\n        data: {\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n          username: constants.TEST_REDIS_USER,\n          password: constants.TEST_REDIS_PASSWORD,\n          tls: true,\n          verifyServerCert: true,\n          caCert: {\n            name: constants.getRandomString(),\n            certificate: constants.TEST_REDIS_TLS_CA,\n          },\n          clientCert: {\n            name: constants.getRandomString(),\n            certificate: constants.TEST_USER_TLS_CERT,\n            key: constants.TEST_USER_TLS_KEY,\n          },\n        },\n        responseSchema,\n        before: async () => {\n          caCerts = await (\n            await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)\n          ).count({});\n          clientCerts = await (\n            await localDb.getRepository(\n              localDb.repositories.CLIENT_CERT_REPOSITORY,\n            )\n          ).count({});\n        },\n        after: async () => {\n          expect(caCerts).to.eq(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.CA_CERT_REPOSITORY,\n              )\n            ).count({}),\n          );\n          expect(clientCerts).to.eq(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.CLIENT_CERT_REPOSITORY,\n              )\n            ).count({}),\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.length).to.gte(1);\n          const sentinelMaster = _.find(\n            body,\n            ({ name }) => name === constants.TEST_SENTINEL_MASTER_GROUP,\n          );\n          expect(sentinelMaster.nodes).to.eql([]); // no other sentinels for this master group\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/set/DELETE-databases-id-set-members.test.ts",
    "content": "import {\n  expect,\n  describe,\n  _,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/set/members`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  members: Joi.array().items(Joi.any()).required(), // todo: look at BE validation rules for string members\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  members: [constants.getRandomString()],\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/set/members', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should remove member from buff',\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_BUF_OBJ_1,\n          members: [constants.TEST_SET_MEMBER_BIN_BUF_OBJ_1],\n        },\n        responseBody: {\n          affected: 1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_SET_KEY_BIN_BUFFER_1),\n          ).to.eql(0);\n        },\n      },\n      {\n        name: 'Should add member from ascii',\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_ASCII_1,\n          members: [constants.TEST_SET_MEMBER_BIN_ASCII_1],\n        },\n        responseBody: {\n          affected: 1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_SET_KEY_BIN_BUFFER_1),\n          ).to.eql(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should delete single member',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: ['member_1'],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          after: async () => {\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_2,\n              0,\n              'count',\n              1000,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1].length).to.eql(99);\n          },\n        },\n        {\n          name: 'Should delete multiple members',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: ['member_2', 'member_3', 'member_4'],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 3,\n          },\n          after: async () => {\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_2,\n              0,\n              'count',\n              1000,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1].length).to.eql(96);\n          },\n        },\n        {\n          name: 'Should not delete any member if incorrect member passed',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 0,\n          },\n          after: async () => {\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_2,\n              0,\n              'count',\n              1000,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1].length).to.eql(96);\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n              'count',\n              100,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1]).to.eql([constants.TEST_SET_MEMBER_1]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should delete member',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 0,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"srem\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -srem'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/set/POST-databases-id-set-get_members.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n  _,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/set/get-members`,\n  );\n\n// input data schema // todo: review BE for transform true -> 1\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  cursor: Joi.number().integer().min(0).allow(true).required().messages({\n    'any.required': 'cursor should not be empty',\n  }),\n  count: Joi.number().integer().min(1).allow(true, null).messages({\n    'any.required': 'count should not be empty',\n  }),\n  match: Joi.string().allow(null),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  cursor: 0,\n  count: 1,\n  match: constants.getRandomString(),\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    total: Joi.number().integer().required(),\n    members: Joi.array().items(JoiRedisString),\n    nextCursor: Joi.number().integer().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/set/get-members', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should query members from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_BUF_OBJ_1,\n          cursor: 0,\n          count: 15,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_SET_KEY_BIN_UTF8_1,\n          total: 1,\n          members: [constants.TEST_SET_MEMBER_BIN_UTF8_1],\n        },\n      },\n      {\n        name: 'Should query members from buff (return buffer)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_BUF_OBJ_1,\n          cursor: 0,\n          count: 15,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_SET_KEY_BIN_BUF_OBJ_1,\n          total: 1,\n          members: [constants.TEST_SET_MEMBER_BIN_BUF_OBJ_1],\n        },\n      },\n      {\n        name: 'Should query members from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_ASCII_1,\n          cursor: 0,\n          count: 15,\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_SET_KEY_BIN_ASCII_1,\n          total: 1,\n          members: [constants.TEST_SET_MEMBER_BIN_ASCII_1],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should find by exact match',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'member_9',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_SET_KEY_2);\n            expect(body.total).to.eql(100);\n            expect(body.members.length).to.eql(1);\n          },\n        },\n        {\n          name: 'Should not find any member',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'notExistin*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_SET_KEY_2);\n            expect(body.total).to.eql(100);\n            expect(body.members.length).to.eql(0);\n          },\n        },\n        {\n          name: 'Should query 15 members',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            count: 15,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_SET_KEY_2);\n            expect(body.total).to.eql(100);\n            expect(body.members.length).to.gte(15);\n            expect(body.members.length).to.lt(100);\n          },\n        },\n        {\n          name: 'Should query by * in the end',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'member_9*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_SET_KEY_2);\n            expect(body.total).to.eql(100);\n            expect(body.members.length).to.eql(11);\n          },\n        },\n        {\n          name: 'Should query by * in the beginning',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: '*ber_9',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_SET_KEY_2);\n            expect(body.total).to.eql(100);\n            expect(body.members.length).to.eql(1);\n          },\n        },\n        {\n          name: 'Should query by * in the middle',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'membe*_9',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_SET_KEY_2);\n            expect(body.total).to.eql(100);\n            expect(body.members.length).to.eql(1);\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            cursor: 0,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n              'count',\n              100,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1]).to.eql([constants.TEST_SET_MEMBER_1]);\n          },\n        },\n      ].map(mainCheckFn);\n\n      it('Should scan entire set', async () => {\n        const members = [];\n        let cursor = null;\n\n        while (cursor !== 0) {\n          await validateApiCall({\n            endpoint,\n            data: {\n              keyName: constants.TEST_SET_KEY_2,\n              cursor: cursor || 0,\n            },\n            checkFn: ({ body }) => {\n              cursor = body.nextCursor;\n              members.push(...body.members);\n            },\n          });\n        }\n\n        expect(members.length).to.be.gte(100);\n        expect(cursor).to.eql(0);\n      });\n\n      describe('Search in huge number of elements', () => {\n        const ELEMENTS_NUMBER = 1_000_000;\n\n        requirements('rte.bigData');\n        [\n          {\n            name: 'Should get element using exists without full scan',\n            data: {\n              keyName: constants.TEST_SET_HUGE_KEY,\n              cursor: 0,\n              match: constants.TEST_SET_HUGE_ELEMENT,\n            },\n            responseSchema,\n            responseBody: {\n              keyName: constants.TEST_SET_HUGE_KEY,\n              total: ELEMENTS_NUMBER,\n              members: [constants.TEST_SET_HUGE_ELEMENT],\n              nextCursor: 0,\n            },\n          },\n          {\n            name: 'Should get elements with possibility to continue iterating',\n            data: {\n              keyName: constants.TEST_SET_HUGE_KEY,\n              cursor: 0,\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.keyName).to.eql(constants.TEST_SET_HUGE_KEY);\n              expect(body.total).to.eql(ELEMENTS_NUMBER);\n              expect(body.nextCursor).to.not.eql(0);\n              expect(body.members.length).to.gte(200);\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should add member',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"scard\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -scard'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"sismember\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n            match: 'asd',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -sismember'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"sscan\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            cursor: 0,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -sscan'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/set/POST-databases-id-set.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/set`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  members: Joi.array().items(Joi.string().allow(null)).required().messages({\n    'string.base': 'members must be a string or a Buffer',\n  }),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_SET_KEY_1,\n  members: [constants.TEST_SET_MEMBER_1],\n  expire: constants.TEST_SET_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst createCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      }\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n        const scanResult = await rte.client.sscan(\n          testCase.data.keyName,\n          0,\n          'count',\n          100,\n        );\n        expect(scanResult[0]).to.eql('0'); // full scan completed\n        expect(scanResult[1]).to.eql(testCase.data.members);\n        if (testCase.data.expire) {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.gte(\n            testCase.data.expire - 5,\n          );\n        } else {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.eql(-1);\n        }\n      }\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/set', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should create set from buff',\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_BUF_OBJ_1,\n          members: [constants.TEST_SET_MEMBER_BIN_BUF_OBJ_1],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_SET_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'sscan',\n              [constants.TEST_SET_KEY_BIN_BUFFER_1, 0, 'count', 100],\n              null,\n            ),\n          ).to.deep.eq([\n            Buffer.from('0'),\n            [constants.TEST_SET_MEMBER_BIN_BUFFER_1],\n          ]);\n        },\n      },\n      {\n        name: 'Should create set from ascii',\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_ASCII_1,\n          members: [constants.TEST_SET_MEMBER_BIN_ASCII_1],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_SET_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'sscan',\n              [constants.TEST_SET_KEY_BIN_BUFFER_1, 0, 'count', 100],\n              null,\n            ),\n          ).to.deep.eq([\n            Buffer.from('0'),\n            [constants.TEST_SET_MEMBER_BIN_BUFFER_1],\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should create item with empty value',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [''],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with key ttl',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n            expire: constants.TEST_SET_EXPIRE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create regular item',\n          data: {\n            keyName: constants.TEST_SET_KEY_1,\n            members: [constants.TEST_SET_MEMBER_1],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should return conflict error if key already exists',\n          data: {\n            keyName: constants.TEST_SET_KEY_1,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n            message: 'This key name is already in use.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n              'count',\n              100,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1]).to.eql([constants.TEST_SET_MEMBER_1]);\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n              'count',\n              100,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1]).to.eql([constants.TEST_SET_MEMBER_1]);\n          },\n        },\n      ].map(createCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"sadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -sadd'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(createCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/set/PUT-databases-id-set.test.ts",
    "content": "import {\n  expect,\n  describe,\n  _,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).put(`/${constants.API.DATABASES}/${instanceId}/set`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  members: Joi.array().items(Joi.string()).required().messages({\n    'string.base': 'members must be a string or a Buffer',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  members: [constants.getRandomString()],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PUT /databases/:instanceId/set', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should add member from buff',\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_BUF_OBJ_1,\n          members: [constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1],\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_SET_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          const [cursor, members] = await rte.data.sendCommand(\n            'sscan',\n            [constants.TEST_SET_KEY_BIN_BUFFER_1, 0, 'count', 100],\n            null,\n          );\n          expect(cursor).to.deep.eq(Buffer.from('0'));\n          expect(\n            _.find(members, constants.TEST_LIST_ELEMENT_BIN_BUFFER_1),\n          ).to.not.eq(undefined);\n          expect(\n            _.find(members, constants.TEST_SET_MEMBER_BIN_BUFFER_1),\n          ).to.not.eq(undefined);\n        },\n      },\n      {\n        name: 'Should add member from ascii',\n        data: {\n          keyName: constants.TEST_SET_KEY_BIN_ASCII_1,\n          members: [constants.TEST_LIST_ELEMENT_BIN_ASCII_1],\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_SET_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          const [cursor, members] = await rte.data.sendCommand(\n            'sscan',\n            [constants.TEST_SET_KEY_BIN_BUFFER_1, 0, 'count', 100],\n            null,\n          );\n          expect(cursor).to.deep.eq(Buffer.from('0'));\n          expect(\n            _.find(members, constants.TEST_LIST_ELEMENT_BIN_BUFFER_1),\n          ).to.not.eq(undefined);\n          expect(\n            _.find(members, constants.TEST_SET_MEMBER_BIN_BUFFER_1),\n          ).to.not.eq(undefined);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', function () {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      before(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should not modify set as such member already exists',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: ['member_1'],\n          },\n          after: async () => {\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_2,\n              0,\n              'count',\n              1000,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1].length).to.eql(100);\n          },\n        },\n        {\n          name: 'Should add single member',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          after: async () => {\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_2,\n              0,\n              'count',\n              1000,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1].length).to.eql(101);\n          },\n        },\n        {\n          name: 'Should add multiple members',\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [\n              constants.getRandomString(),\n              constants.getRandomString(),\n              constants.getRandomString(),\n              constants.getRandomString(),\n            ],\n          },\n          after: async () => {\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_2,\n              0,\n              'count',\n              1000,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1].length).to.eql(105);\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () => {\n            // check that value was not overwritten\n            const scanResult = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n              'count',\n              100,\n            );\n            expect(scanResult[0]).to.eql('0'); // full scan completed\n            expect(scanResult[1]).to.eql([constants.TEST_SET_MEMBER_1]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should add member',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"sadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_SET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -sadd'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/settings/GET-settings-agreements-spec.test.ts",
    "content": "import { describe, it, deps, Joi, expect, validateApiCall } from '../deps';\nimport { constants } from '../../helpers/constants';\nconst { server, request } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/settings/agreements/spec');\n\nconst agreementItemSchema = Joi.object().keys({\n  defaultValue: Joi.bool().required(),\n  required: Joi.bool().required(),\n  disabled: Joi.bool().required(),\n  displayInSetting: Joi.bool().required(),\n  editable: Joi.bool().required(),\n  since: Joi.string().required(),\n  title: Joi.string().required(),\n  label: Joi.string().required(),\n  category: Joi.string().optional(),\n  description: Joi.string().optional(),\n  requiredText: Joi.string().optional(),\n  linkToPrivacyPolicy: Joi.boolean().required(),\n});\n\nconst responseSchema = Joi.object()\n  .keys({\n    version: Joi.string().required(),\n    agreements: Joi.object()\n      .keys({\n        eula: agreementItemSchema.required(),\n        analytics: agreementItemSchema.required(),\n        encryption: agreementItemSchema.required(),\n        notifications: agreementItemSchema.required(),\n      })\n      .pattern(/./, agreementItemSchema)\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('GET /settings/agreements/spec', () => {\n  [\n    {\n      name: 'Should return valid JSON',\n      statusCode: 200,\n      responseSchema,\n      checkFn: ({ body }) => {\n        const encryptionAgreements = body.agreements.encryption;\n        expect(encryptionAgreements.since).to.eql('1.0.3');\n        expect(encryptionAgreements.defaultValue).to.eql(\n          constants.TEST_ENCRYPTION_STRATEGY === 'KEYTAR',\n        );\n      },\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/settings/GET-settings.test.ts",
    "content": "import {\n  describe,\n  it,\n  deps,\n  Joi,\n  validateApiCall,\n  expect,\n  after,\n} from '../deps';\nimport {\n  applyEulaAgreement,\n  initSettings,\n  resetSettings,\n} from '../../helpers/local-db';\nconst { server, request, constants } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).get('/settings');\n\nconst responseSchema = Joi.object()\n  .keys({\n    theme: Joi.string().allow(null).required(),\n    scanThreshold: Joi.number().required(),\n    batchSize: Joi.number().required(),\n    dateFormat: Joi.string().allow(null),\n    timezone: Joi.string().allow(null),\n    acceptTermsAndConditionsOverwritten: Joi.bool().required(),\n    agreements: Joi.object()\n      .keys({\n        version: Joi.string().required(),\n        eula: Joi.bool().required(),\n        encryption: Joi.bool(),\n      })\n      .allow(null)\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('GET /settings', () => {\n  after(initSettings);\n\n  [\n    {\n      name: 'Should return default settings',\n      statusCode: 200,\n      responseSchema,\n      before: resetSettings,\n      checkFn: ({ body }) => {\n        expect(body).to.eql(constants.APP_DEFAULT_SETTINGS);\n      },\n    },\n    {\n      name: 'Should return settings with applied EULA agreement',\n      statusCode: 200,\n      responseSchema,\n      before: applyEulaAgreement,\n      checkFn: ({ body }) => {\n        expect(body)\n          .to.have.nested.property('agreements.eula')\n          .that.deep.equals(true);\n        expect(body)\n          .to.have.nested.property('agreements.encryption')\n          .that.deep.equals(true);\n      },\n      after: async () => initSettings(),\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/settings/PATCH-settings.test.ts",
    "content": "import * as AGREEMENTS_SPEC from 'src/constants/agreements-spec.json';\nimport {\n  describe,\n  it,\n  deps,\n  Joi,\n  validateApiCall,\n  expect,\n  after,\n  before,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nimport { initSettings, resetSettings } from '../../helpers/local-db';\nconst { server, request, constants } = deps;\n\n// endpoint to test\nconst endpoint = () => request(server).patch('/settings');\n\nconst responseSchema = Joi.object()\n  .keys({\n    theme: Joi.string().allow(null).required(),\n    scanThreshold: Joi.number().required(),\n    batchSize: Joi.number().required(),\n    dateFormat: Joi.string().allow(null),\n    timezone: Joi.string().allow(null),\n    acceptTermsAndConditionsOverwritten: Joi.bool().required(),\n    agreements: Joi.object()\n      .keys({\n        version: Joi.string().required(),\n        eula: Joi.bool().required(),\n        encryption: Joi.bool(),\n      })\n      .pattern(/./, Joi.boolean())\n      .allow(null)\n      .required(),\n  })\n  .required();\n\n// input data schema\nconst dataSchema = Joi.object({\n  theme: Joi.string().allow(null).optional(),\n  scanThreshold: Joi.number().allow(null).min(500).optional(),\n  dateFormat: Joi.string().allow(null),\n  timezone: Joi.string().allow(null),\n  agreements: Joi.object()\n    .keys({\n      eula: Joi.boolean().label('.eula').optional(),\n      encryption: Joi.boolean().label('.encryption').optional(),\n    })\n    .allow(null)\n    .optional()\n    .messages({\n      'boolean.base': 'each value in agreements must be a boolean value',\n      'object.base': 'agreements must be an instance of Map',\n    }),\n}).strict();\n\nconst validInputData = {\n  theme: 'DARK',\n  scanThreshold: 100000,\n  batchSize: 5,\n  dateFormat: null,\n  timezone: null,\n  agreements: {\n    eula: true,\n    analytics: false,\n    encryption: false,\n    notifications: false,\n  },\n};\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n  });\n};\n\ndescribe('PATCH /settings', () => {\n  after(initSettings);\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('settings', () => {\n    before(resetSettings);\n\n    return [\n      {\n        name: 'Should update only scanThreshold value',\n        statusCode: 200,\n        data: { scanThreshold: 10000000, batchSize: 5 },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body).to.include({\n            ...constants.APP_DEFAULT_SETTINGS,\n            scanThreshold: 10000000,\n            batchSize: 5,\n          });\n        },\n      },\n      {\n        name: 'Should update settings and agreements',\n        statusCode: 200,\n        data: validInputData,\n        responseSchema,\n        checkFn: ({ body }) => {\n          const { agreements, ...settings } = validInputData;\n\n          expect(body).to.include(settings);\n          expect(body.agreements).to.include(agreements);\n        },\n      },\n      {\n        name: 'Should set default settings',\n        statusCode: 200,\n        data: { scanThreshold: null, theme: null, batchSize: null },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const { agreements: _agreements, ...defaultSettings } =\n            constants.APP_DEFAULT_SETTINGS;\n\n          expect(body).to.include(defaultSettings);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('agreements', () => {\n    before(resetSettings);\n\n    const allAcceptedAgreements = {};\n    Object.keys(AGREEMENTS_SPEC.agreements).forEach(\n      (agreement) => (allAcceptedAgreements[agreement] = true),\n    );\n    return [\n      {\n        name: 'Should throw [Bad Request] if some agreements are missed in dto',\n        data: {\n          agreements: {\n            analytics: true,\n          },\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n      },\n      {\n        name: 'Should accept all agreements defined in specification',\n        statusCode: 200,\n        data: { agreements: allAcceptedAgreements },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const { agreements: _agreements, ...defaultSettings } =\n            constants.APP_DEFAULT_SETTINGS;\n\n          expect(body).to.include(defaultSettings);\n          expect(body.agreements).to.eql({\n            version: AGREEMENTS_SPEC.version,\n            ...allAcceptedAgreements,\n          });\n        },\n      },\n      {\n        name: 'Should reject analytics agreement',\n        statusCode: 200,\n        data: { agreements: { analytics: false } },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const { agreements: _agreements, ...defaultSettings } =\n            constants.APP_DEFAULT_SETTINGS;\n\n          expect(body).to.include(defaultSettings);\n          expect(body.agreements).to.eql({\n            version: AGREEMENTS_SPEC.version,\n            ...allAcceptedAgreements,\n            analytics: false,\n          });\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/slowlog/DELETE-databases-id-slow_logs.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  deps,\n  validateApiCall,\n  after,\n  requirements,\n  before,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(`/${constants.API.DATABASES}/${instanceId}/slow-logs`);\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('DELETE /databases/:instanceId/slow-logs', () => {\n  describe('Common', () => {\n    beforeEach(async () => {\n      await rte.data.executeCommandAll('config', [\n        'set',\n        'slowlog-log-slower-than',\n        0,\n      ]);\n      await rte.client.get(constants.TEST_STRING_KEY_1);\n    });\n\n    after(async () => {\n      await rte.data.executeCommandAll('config', [\n        'set',\n        'slowlog-log-slower-than',\n        10000,\n      ]);\n    });\n\n    [\n      {\n        name: 'Check that slowlog cleaned up',\n        before: async () => {\n          await rte.data.executeCommandAll('config', [\n            'set',\n            'slowlog-log-slower-than',\n            10000000000,\n          ]);\n          expect((await rte.client.call('slowlog', 'get')).length).to.gt(0);\n        },\n        after: async () => {\n          expect((await rte.client.call('slowlog', 'get')).length).to.eq(0);\n        },\n      },\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should reset slowlog',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n      },\n      {\n        name: 'Should throw error if no permissions for \"slowlog\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -slowlog'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/slowlog/GET-databases-id-slow_logs-config.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  Joi,\n  deps,\n  validateApiCall,\n  requirements,\n  before,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/slow-logs/config`,\n  );\n\nconst responseSchema = Joi.object()\n  .keys({\n    slowlogMaxLen: Joi.number().required(),\n    slowlogLogSlowerThan: Joi.number().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('GET /databases/:instanceId/slow-logs/config', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should get slowlog config',\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.slowlogMaxLen).to.gte(0);\n          expect(body.slowlogLogSlowerThan).to.gte(0);\n        },\n      },\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should get slowlog config',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n      },\n      {\n        name: 'Should throw error if no permissions for \"config\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -config'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/slowlog/GET-databases-id-slow_logs.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  Joi,\n  deps,\n  validateApiCall,\n  after,\n  requirements,\n  before,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(`/${constants.API.DATABASES}/${instanceId}/slow-logs`);\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.number().required(),\n      time: Joi.number().required(),\n      durationUs: Joi.number().required(),\n      args: Joi.string().required(),\n      source: Joi.string().allow(''),\n      client: Joi.string().allow(''),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('GET /databases/:instanceId/slow-logs', () => {\n  describe('Common', () => {\n    beforeEach(async () => {\n      await rte.data.executeCommandAll('config', [\n        'set',\n        'slowlog-log-slower-than',\n        0,\n      ]);\n      await rte.data.executeCommandAll('slowlog', ['reset']);\n    });\n\n    after(async () => {\n      await rte.data.executeCommandAll('config', [\n        'set',\n        'slowlog-log-slower-than',\n        10000,\n      ]);\n    });\n\n    [\n      {\n        name: 'Should return 0 array when slowlog-log-slower-than is a huge value',\n        responseSchema,\n        before: async () => {\n          await rte.data.executeCommandAll('config', [\n            'set',\n            'slowlog-log-slower-than',\n            1000000000,\n          ]);\n          await rte.data.executeCommandAll('slowlog', ['reset']);\n        },\n        checkFn: async ({ body }) => {\n          expect(body).to.eql([]);\n        },\n      },\n      {\n        name: 'Should return 1 + slave nodes array when slowlog-log-slower-than is a huge value',\n        responseSchema,\n        query: {\n          count: 1,\n        },\n        before: async () => {\n          await rte.data.executeCommandAll('config', [\n            'set',\n            'slowlog-log-slower-than',\n            0,\n          ]);\n          await rte.data.executeCommandAll('slowlog', ['reset']);\n        },\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(\n            rte.client.nodes ? rte.client.nodes().length : 1,\n          );\n        },\n      },\n      {\n        name: 'Should get slow logs including \"set\" command inside',\n        responseSchema,\n        before: async () => {\n          await rte.client.set(\n            constants.TEST_STRING_KEY_1,\n            constants.GENERATE_BIG_TEST_STRING_VALUE(0.1),\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.length).to.gt(0);\n          const stringSlowLog = body.find((log) =>\n            log.args.startsWith(`set ${constants.TEST_STRING_KEY_1}`),\n          );\n          expect(stringSlowLog.durationUs).to.gt(0);\n        },\n      },\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should fetch slowlog',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n      },\n      {\n        name: 'Should throw error if no permissions for \"slowlog\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -slowlog'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/slowlog/PATCH-databases-id-slow_logs-config.test.ts",
    "content": "import {\n  describe,\n  it,\n  Joi,\n  deps,\n  validateApiCall,\n  after,\n  requirements,\n  before,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/slow-logs/config`,\n  );\n\nconst dataSchema = Joi.object({\n  slowlogMaxLen: Joi.number().min(0).messages({\n    'array.sparse': 'entries must be either object or array',\n    'array.base': 'property {#label} must be either object or array',\n  }),\n  slowlogLogSlowerThan: Joi.number().min(-1),\n}).strict();\n\nconst validInputData = {\n  slowlogMaxLen: 128,\n  slowlogLogSlowerThan: 10000,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    slowlogMaxLen: Joi.number().required(),\n    slowlogLogSlowerThan: Joi.number().required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('PATCH /databases/:instanceId/slow-logs/config', () => {\n  before(async () => {\n    await rte.data.executeCommand('config', [\n      'set',\n      'slowlog-log-slower-than',\n      10000,\n    ]);\n    await rte.data.executeCommand('config', ['set', 'slowlog-max-len', 128]);\n  });\n\n  after(async () => {\n    await rte.data.executeCommand('config', [\n      'set',\n      'slowlog-log-slower-than',\n      10000,\n    ]);\n    await rte.data.executeCommand('config', ['set', 'slowlog-max-len', 128]);\n  });\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Standalone', () => {\n    requirements('rte.type=STANDALONE');\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should NOT change anything',\n          responseSchema,\n          responseBody: {\n            slowlogMaxLen: 128,\n            slowlogLogSlowerThan: 10000,\n          },\n        },\n        {\n          name: 'Should change only slowlog-max-len',\n          data: {\n            slowlogMaxLen: 100,\n          },\n          responseSchema,\n          responseBody: {\n            slowlogMaxLen: 100,\n            slowlogLogSlowerThan: 10000,\n          },\n        },\n        {\n          name: 'Should change only slowlog-log-slower-than',\n          data: {\n            slowlogLogSlowerThan: 100,\n          },\n          responseSchema,\n          responseBody: {\n            slowlogMaxLen: 100,\n            slowlogLogSlowerThan: 100,\n          },\n        },\n        {\n          name: 'Should change both',\n          data: {\n            slowlogMaxLen: 128,\n            slowlogLogSlowerThan: 10000,\n          },\n          responseSchema,\n          responseBody: {\n            slowlogMaxLen: 128,\n            slowlogLogSlowerThan: 10000,\n          },\n        },\n        {\n          name: 'Should return 404 not found when incorrect instance',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            message: 'Invalid database instance id.',\n            error: 'Not Found',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should get slowlog config',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        },\n        {\n          name: 'Should throw error if no permissions for \"config\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -config'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('Cluster', () => {\n    requirements('rte.type=CLUSTER');\n\n    [\n      {\n        name: 'Should return 400 since there is no way to modify cluster config at the moment',\n        data: {\n          slowlogMaxLen: 1,\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          message: 'Configuration slowlog for cluster is deprecated',\n          error: 'Bad Request',\n        },\n      },\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/DELETE-databases-id-streams-consumer_groups-consumers.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups/consumers`,\n  );\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  groupName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  consumerNames: Joi.array()\n    .items(\n      Joi.string().required().label('consumerNames').messages({\n        'any.required': '{#label} should not be empty',\n      }),\n    )\n    .min(1)\n    .required()\n    .messages({\n      'array.sparse': 'each value in consumerNames should not be empty',\n    }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  groupName: constants.TEST_STREAM_GROUP_1,\n  consumerNames: [constants.TEST_STREAM_GROUP_1],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/streams/consumer-groups/consumers', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(async () => {\n      await rte.data.generateBinKeys(true);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n        constants.TEST_STREAM_CONSUMER_BIN_BUFFER_1,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n        constants.TEST_STREAM_ID_1,\n      ]);\n    });\n\n    [\n      {\n        name: 'Should remove stream consumer from buff',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n          consumerNames: [constants.TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1],\n        },\n        before: async () => {\n          const consumers = await rte.data.sendCommand('xinfo', [\n            'consumers',\n            constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n            constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n          ]);\n          expect(consumers.length).to.eq(1);\n        },\n        after: async () => {\n          const consumers = await rte.data.sendCommand('xinfo', [\n            'consumers',\n            constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n            constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n          ]);\n          expect(consumers.length).to.eq(0);\n        },\n      },\n      {\n        name: 'Should remove stream consumer from ascii',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_ASCII_1,\n          consumerNames: [constants.TEST_STREAM_CONSUMER_BIN_ASCII_1],\n        },\n        before: async () => {\n          const consumers = await rte.data.sendCommand('xinfo', [\n            'consumers',\n            constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n            constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n          ]);\n          expect(consumers.length).to.eq(1);\n        },\n        after: async () => {\n          const consumers = await rte.data.sendCommand('xinfo', [\n            'consumers',\n            constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n            constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n          ]);\n          expect(consumers.length).to.eq(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => {\n        await rte.data.sendCommand('xreadgroup', [\n          'GROUP',\n          constants.TEST_STREAM_GROUP_1,\n          constants.TEST_STREAM_CONSUMER_1,\n          'STREAMS',\n          constants.TEST_STREAM_KEY_1,\n          constants.TEST_STREAM_ID_1,\n        ]);\n        await rte.data.sendCommand('xreadgroup', [\n          'GROUP',\n          constants.TEST_STREAM_GROUP_1,\n          constants.TEST_STREAM_CONSUMER_2,\n          'STREAMS',\n          constants.TEST_STREAM_KEY_1,\n          constants.TEST_STREAM_ID_2,\n        ]);\n      });\n\n      [\n        {\n          name: 'Should remove single consumer',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerNames: [constants.TEST_STREAM_CONSUMER_1],\n          },\n          before: async () => {\n            const consumers = await rte.data.sendCommand('xinfo', [\n              'consumers',\n              constants.TEST_STREAM_KEY_1,\n              constants.TEST_STREAM_GROUP_1,\n            ]);\n            expect(consumers.length).to.eq(2);\n          },\n          after: async () => {\n            const consumers = await rte.data.sendCommand('xinfo', [\n              'consumers',\n              constants.TEST_STREAM_KEY_1,\n              constants.TEST_STREAM_GROUP_1,\n            ]);\n            expect(consumers.length).to.eq(1);\n          },\n        },\n        {\n          name: 'Should remove multiple consumers',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerNames: [\n              constants.TEST_STREAM_CONSUMER_1,\n              constants.TEST_STREAM_CONSUMER_2,\n            ],\n          },\n          before: async () => {\n            const consumers = await rte.data.sendCommand('xinfo', [\n              'consumers',\n              constants.TEST_STREAM_KEY_1,\n              constants.TEST_STREAM_GROUP_1,\n            ]);\n            expect(consumers.length).to.eq(2);\n          },\n          after: async () => {\n            const consumers = await rte.data.sendCommand('xinfo', [\n              'consumers',\n              constants.TEST_STREAM_KEY_1,\n              constants.TEST_STREAM_GROUP_1,\n            ]);\n            expect(consumers.length).to.eq(0);\n          },\n        },\n        {\n          name: 'Should remove single consumers and skip not existing consumers',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerNames: [\n              constants.TEST_STREAM_CONSUMER_1,\n              constants.getRandomString(),\n              constants.getRandomString(),\n            ],\n          },\n          before: async () => {\n            const consumers = await rte.data.sendCommand('xinfo', [\n              'consumers',\n              constants.TEST_STREAM_KEY_1,\n              constants.TEST_STREAM_GROUP_1,\n            ]);\n            expect(consumers.length).to.eq(2);\n          },\n          after: async () => {\n            const consumers = await rte.data.sendCommand('xinfo', [\n              'consumers',\n              constants.TEST_STREAM_KEY_1,\n              constants.TEST_STREAM_GROUP_1,\n            ]);\n            expect(consumers.length).to.eq(1);\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if group does not exists',\n          data: {\n            ...validInputData,\n            groupName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Consumer Group with such name was not found.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xgroup)\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xgroup'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/DELETE-databases-id-streams-consumer_groups.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups`,\n  );\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  consumerGroups: Joi.array()\n    .items(\n      Joi.string().label('consumerGroups').required().messages({\n        'any.required': '{#label} should not be empty',\n      }),\n    )\n    .required()\n    .min(1)\n    .messages({\n      'array.sparse': 'consumerGroups must be a string or a Buffer',\n    }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  consumerGroups: [constants.TEST_STREAM_GROUP_1],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/streams/consumer-groups', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should delete consumer group from buff',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          consumerGroups: [constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1],\n        },\n        after: async () => {\n          const groups = await rte.data.sendCommand('xinfo', [\n            'groups',\n            constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n          ]);\n          expect(groups.length).to.eq(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => await rte.data.generateKeys(true));\n\n      [\n        {\n          name: 'Should delete consumer group',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            consumerGroups: [constants.TEST_STREAM_GROUP_1],\n          },\n          before: async () => {\n            const groups = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(groups.length).to.eq(2);\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(groups.length).to.eq(1);\n          },\n        },\n        {\n          name: 'Should delete multiple consumer group',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            consumerGroups: [\n              constants.TEST_STREAM_GROUP_1,\n              constants.TEST_STREAM_GROUP_2,\n            ],\n          },\n          before: async () => {\n            const groups = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(groups.length).to.eq(2);\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(groups.length).to.eq(0);\n          },\n        },\n        {\n          name: 'Should delete single consumer group and ignore not existing',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            consumerGroups: [\n              constants.TEST_STREAM_GROUP_1,\n              constants.getRandomString(),\n              constants.getRandomString(),\n            ],\n          },\n          before: async () => {\n            const groups = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(groups.length).to.eq(2);\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(groups.length).to.eq(1);\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should remove consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xgroup\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xgroup'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/PATCH-databases-id-streams-consumer_groups.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups`,\n  );\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  name: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  lastDeliveredId: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  name: constants.TEST_STREAM_GROUP_1,\n  lastDeliveredId: '$',\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/streams/consumer-groups', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    describe('Redis version < 7', () => {\n      requirements('rte.version<7.0');\n      [\n        {\n          name: 'Should update consumer group lastDeliveredId from buff',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n            name: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n            lastDeliveredId: constants.TEST_STREAM_ID_2,\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            expect(groups).to.deep.eq([\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_2),\n              ],\n            ]);\n          },\n        },\n        {\n          name: 'Should update consumer group lastDeliveredId from ascii',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n            name: constants.TEST_STREAM_GROUP_BIN_ASCII_1,\n            lastDeliveredId: constants.TEST_STREAM_ID_2,\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            expect(groups).to.deep.eq([\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_2),\n              ],\n            ]);\n          },\n        },\n      ].forEach(mainCheckFn);\n    });\n    describe('Redis version >= 7', () => {\n      requirements('rte.version>=7.0');\n      [\n        {\n          name: 'Should update consumer group lastDeliveredId from buff',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n            name: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n            lastDeliveredId: constants.TEST_STREAM_ID_2,\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            expect(groups).to.deep.eq([\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_2),\n                Buffer.from('entries-read'),\n                null,\n                Buffer.from('lag'),\n                1,\n              ],\n            ]);\n          },\n        },\n        {\n          name: 'Should update consumer group lastDeliveredId from ascii',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n            name: constants.TEST_STREAM_GROUP_BIN_ASCII_1,\n            lastDeliveredId: constants.TEST_STREAM_ID_2,\n          },\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            expect(groups).to.deep.eq([\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_2),\n                Buffer.from('entries-read'),\n                null,\n                Buffer.from('lag'),\n                1,\n              ],\n            ]);\n          },\n        },\n      ].forEach(mainCheckFn);\n    });\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => {\n        await rte.client.del(constants.TEST_STREAM_KEY_2);\n        await rte.client.xadd(constants.TEST_STREAM_KEY_2, '*', 'f', 'v');\n      });\n\n      [\n        {\n          name: 'Should update lastDeliveredId',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            name: constants.TEST_STREAM_GROUP_1,\n            lastDeliveredId: constants.TEST_STREAM_ID_2,\n          },\n          before: async () => {\n            const [group] = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(group[7]).to.eq(constants.TEST_STREAM_ID_1);\n          },\n          after: async () => {\n            const [group] = await rte.data.sendCommand('xinfo', [\n              'groups',\n              constants.TEST_STREAM_KEY_1,\n            ]);\n            expect(group[7]).to.eq(constants.TEST_STREAM_ID_2);\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if group does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STREAM_KEY_2,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Consumer Group with such name was not found.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xgroup\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xgroup'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-consumer_groups-consumers-get.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups/consumers/get`,\n  );\n\nconst consumerSchema = Joi.object()\n  .keys({\n    name: JoiRedisString.required(),\n    idle: Joi.number().required(),\n    pending: Joi.number().required(),\n  })\n  .strict();\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  groupName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  groupName: constants.TEST_STREAM_GROUP_1,\n};\n\nconst responseSchema = Joi.array().items(consumerSchema).min(0).required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/consumer-groups/consumers/get', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(async () => {\n      await rte.data.generateBinKeys(true);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n        constants.TEST_STREAM_CONSUMER_BIN_BUFFER_1,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n        constants.TEST_STREAM_ID_1,\n      ]);\n    });\n\n    [\n      {\n        name: 'Should get stream consumer from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const [consumer] = body;\n          expect(consumer.name).to.eq(\n            constants.TEST_STREAM_CONSUMER_BIN_UTF8_1,\n          );\n          expect(consumer.pending).to.eq(0);\n          expect(consumer.idle).to.gte(0);\n        },\n      },\n      {\n        name: 'Should get stream consumer from buff (return buff)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const [consumer] = body;\n          expect(consumer.name).to.deep.eq(\n            constants.TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1,\n          );\n          expect(consumer.pending).to.eq(0);\n          expect(consumer.idle).to.gte(0);\n        },\n      },\n      {\n        name: 'Should get stream consumer from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_ASCII_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const [consumer] = body;\n          expect(consumer.name).to.deep.eq(\n            constants.TEST_STREAM_CONSUMER_BIN_ASCII_1,\n          );\n          expect(consumer.pending).to.eq(0);\n          expect(consumer.idle).to.gte(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => {\n        await rte.data.sendCommand('xreadgroup', [\n          'GROUP',\n          constants.TEST_STREAM_GROUP_1,\n          constants.TEST_STREAM_CONSUMER_1,\n          'STREAMS',\n          constants.TEST_STREAM_KEY_1,\n          constants.TEST_STREAM_ID_1,\n        ]);\n      });\n\n      [\n        {\n          name: 'Should return empty array when no consumers',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_2,\n          },\n          responseSchema,\n          responseBody: [],\n        },\n        {\n          name: 'Should return consumers list',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const [consumer] = body;\n            expect(consumer.name).to.eq(constants.TEST_STREAM_CONSUMER_1);\n            expect(consumer.pending).to.eq(0);\n            expect(consumer.idle).to.gte(0);\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if group does not exists',\n          data: {\n            ...validInputData,\n            groupName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Consumer Group with such name was not found.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xinfo\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xinfo'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-consumer_groups-consumers-pending_messages-ack.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups/consumers/pending-messages/ack`,\n  );\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  groupName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  entries: Joi.array()\n    .items(\n      Joi.string().required().label('entries').messages({\n        'any.required': '{#label} should not be empty',\n      }),\n    )\n    .required()\n    .min(1)\n    .messages({\n      'array.sparse': 'each value in entries should not be empty',\n    }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  groupName: constants.TEST_STREAM_GROUP_1,\n  entries: [constants.TEST_STREAM_ID_1],\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().required().min(0),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/consumer-groups/consumers/pending-messages/ack', () => {\n  requirements('!rte.crdt');\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    beforeEach(async () => {\n      await rte.data.generateStrings(true);\n      await rte.data.generateStreamsWithoutStrictMode();\n    });\n\n    beforeEach(async () => {\n      await rte.data.sendCommand('xadd', [\n        constants.TEST_STREAM_KEY_1,\n        constants.TEST_STREAM_ID_3,\n        constants.TEST_STREAM_FIELD_1,\n        constants.TEST_STREAM_VALUE_1,\n      ]);\n      await rte.data.sendCommand('xadd', [\n        constants.TEST_STREAM_KEY_1,\n        constants.TEST_STREAM_ID_4,\n        constants.TEST_STREAM_FIELD_1,\n        constants.TEST_STREAM_VALUE_1,\n      ]);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_1,\n        constants.TEST_STREAM_CONSUMER_1,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_1,\n        '>',\n      ]);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_1,\n        constants.TEST_STREAM_CONSUMER_2,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_1,\n        '>',\n      ]);\n    });\n\n    [\n      {\n        name: 'Should ack single entry (from buf)',\n        data: {\n          keyName: {\n            type: 'Buffer',\n            data: [...Buffer.from(constants.TEST_STREAM_KEY_1)],\n          },\n          groupName: {\n            type: 'Buffer',\n            data: [...Buffer.from(constants.TEST_STREAM_GROUP_1)],\n          },\n          entries: [\n            {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_STREAM_ID_3)],\n            },\n          ],\n        },\n        responseSchema,\n        responseBody: { affected: 1 },\n        before: async () => {\n          const pendingMessages = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n          ]);\n          expect(pendingMessages.length).to.eql(2);\n        },\n        after: async () => {\n          const pendingMessages = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n          ]);\n          expect(pendingMessages.length).to.eql(1);\n        },\n      },\n      {\n        name: 'Should ack single entry and ignore not existing',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_1,\n          groupName: constants.TEST_STREAM_GROUP_1,\n          entries: [constants.TEST_STREAM_ID_3, '9999-98', '9999-99'],\n        },\n        responseSchema,\n        responseBody: { affected: 1 },\n        before: async () => {\n          const pendingMessages = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n          ]);\n          expect(pendingMessages.length).to.eql(2);\n        },\n        after: async () => {\n          const pendingMessages = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n          ]);\n          expect(pendingMessages.length).to.eql(1);\n        },\n      },\n      {\n        name: 'Should return affected:0 if group does not exists',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_1,\n          groupName: constants.getRandomString(),\n          entries: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4],\n        },\n        responseSchema,\n        responseBody: { affected: 0 },\n        before: async () => {\n          const pendingMessages = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n          ]);\n          expect(pendingMessages.length).to.eql(2);\n        },\n        after: async () => {\n          const pendingMessages = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n          ]);\n          expect(pendingMessages.length).to.eql(2);\n        },\n      },\n      {\n        name: 'Should return BadRequest error if key has another type',\n        data: {\n          ...validInputData,\n          keyName: constants.TEST_STRING_KEY_1,\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n      },\n      {\n        name: 'Should return NotFound error if key does not exists',\n        data: {\n          ...validInputData,\n          keyName: constants.getRandomString(),\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Key with this name does not exist.',\n        },\n      },\n      {\n        name: 'Should return NotFound error if instance id does not exists',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Invalid database instance id.',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n\n    before(async () => await rte.data.generateKeys(true));\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should create consumer group',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n      },\n      {\n        name: 'Should throw error if no permissions for \"exists\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -exists'),\n      },\n      {\n        name: 'Should throw error if no permissions for \"xack\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -xack'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-consumer_groups-consumers-pending_messages-claim.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups/consumers/pending-messages/claim`,\n  );\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  groupName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  consumerName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  entries: Joi.array()\n    .items(\n      Joi.string().required().label('entries').messages({\n        'any.required': '{#label} should not be empty',\n      }),\n    )\n    .required()\n    .min(1)\n    .messages({\n      'array.sparse': 'each value in entries should not be empty',\n    }),\n  minIdleTime: Joi.number().integer().min(0),\n  time: Joi.number().integer(),\n  retryCount: Joi.number().integer().min(0),\n  force: Joi.boolean(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  groupName: constants.TEST_STREAM_GROUP_1,\n  consumerName: constants.TEST_STREAM_GROUP_1,\n  minIdleTime: 0,\n  entries: [constants.TEST_STREAM_ID_1],\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.array().items(Joi.string()).required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/consumer-groups/consumers/pending-messages/claim', () => {\n  requirements('!rte.crdt');\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    beforeEach(async () => {\n      await rte.data.generateStrings(true);\n      await rte.data.generateStreamsWithoutStrictMode();\n    });\n\n    beforeEach(async () => {\n      await rte.data.sendCommand('xadd', [\n        constants.TEST_STREAM_KEY_1,\n        constants.TEST_STREAM_ID_3,\n        constants.TEST_STREAM_FIELD_1,\n        constants.TEST_STREAM_VALUE_1,\n      ]);\n      await rte.data.sendCommand('xadd', [\n        constants.TEST_STREAM_KEY_1,\n        constants.TEST_STREAM_ID_4,\n        constants.TEST_STREAM_FIELD_1,\n        constants.TEST_STREAM_VALUE_1,\n      ]);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_1,\n        constants.TEST_STREAM_CONSUMER_1,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_1,\n        '>',\n      ]);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_1,\n        constants.TEST_STREAM_CONSUMER_2,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_1,\n        '>',\n      ]);\n    });\n\n    [\n      {\n        name: 'Should claim single entry',\n        data: {\n          keyName: {\n            type: 'Buffer',\n            data: [...Buffer.from(constants.TEST_STREAM_KEY_1)],\n          },\n          groupName: {\n            type: 'Buffer',\n            data: [...Buffer.from(constants.TEST_STREAM_GROUP_1)],\n          },\n          consumerName: {\n            type: 'Buffer',\n            data: [...Buffer.from(constants.TEST_STREAM_CONSUMER_2)],\n          },\n          entries: [\n            {\n              type: 'Buffer',\n              data: [...Buffer.from(constants.TEST_STREAM_ID_3)],\n            },\n          ],\n          force: true,\n        },\n        responseSchema,\n        responseBody: { affected: [constants.TEST_STREAM_ID_3] },\n        before: async () => {\n          const consumerOneEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_1,\n          ]);\n          expect(consumerOneEntries.length).to.eql(2);\n          const consumerTwoEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_2,\n          ]);\n          expect(consumerTwoEntries.length).to.eql(0);\n        },\n        after: async () => {\n          const consumerOneEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_1,\n          ]);\n          expect(consumerOneEntries.length).to.eql(1);\n          const consumerTwoEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_2,\n          ]);\n          expect(consumerTwoEntries.length).to.eql(1);\n        },\n      },\n      {\n        name: 'Should claim multiple entries',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_1,\n          groupName: constants.TEST_STREAM_GROUP_1,\n          consumerName: constants.TEST_STREAM_CONSUMER_2,\n          entries: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4],\n          minIdleTime: 0,\n          idle: 0,\n          retryCount: 1,\n        },\n        responseSchema,\n        responseBody: {\n          affected: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4],\n        },\n        before: async () => {\n          const consumerOneEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_1,\n          ]);\n          expect(consumerOneEntries.length).to.eql(2);\n          const consumerTwoEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_2,\n          ]);\n          expect(consumerTwoEntries.length).to.eql(0);\n        },\n        after: async () => {\n          const consumerOneEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_1,\n          ]);\n          expect(consumerOneEntries.length).to.eql(0);\n          const consumerTwoEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_2,\n          ]);\n          expect(consumerTwoEntries.length).to.eql(2);\n        },\n      },\n      {\n        name: 'Should claim multiple entries out of known consumer',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_1,\n          groupName: constants.TEST_STREAM_GROUP_1,\n          consumerName: constants.getRandomString(),\n          entries: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4],\n          minIdleTime: 0,\n          time: 0,\n          retryCount: 1,\n        },\n        responseSchema,\n        responseBody: {\n          affected: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4],\n        },\n        before: async () => {\n          const consumerOneEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_1,\n          ]);\n          expect(consumerOneEntries.length).to.eql(2);\n          const consumerTwoEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_2,\n          ]);\n          expect(consumerTwoEntries.length).to.eql(0);\n        },\n        after: async () => {\n          const consumerOneEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_1,\n          ]);\n          expect(consumerOneEntries.length).to.eql(0);\n          const consumerTwoEntries = await rte.data.sendCommand('xpending', [\n            constants.TEST_STREAM_KEY_1,\n            constants.TEST_STREAM_GROUP_1,\n            '-',\n            '+',\n            100,\n            constants.TEST_STREAM_CONSUMER_2,\n          ]);\n          expect(consumerTwoEntries.length).to.eql(0);\n        },\n      },\n      {\n        name: 'Should return BadRequest error if key has another type',\n        data: {\n          ...validInputData,\n          keyName: constants.TEST_STRING_KEY_1,\n        },\n        statusCode: 400,\n        responseBody: {\n          statusCode: 400,\n          error: 'Bad Request',\n        },\n      },\n      {\n        name: 'Should return NotFound error if group does not exists',\n        data: {\n          ...validInputData,\n          groupName: constants.getRandomString(),\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Consumer Group with such name was not found.',\n        },\n      },\n      {\n        name: 'Should return NotFound error if key does not exists',\n        data: {\n          ...validInputData,\n          keyName: constants.getRandomString(),\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Key with this name does not exist.',\n        },\n      },\n      {\n        name: 'Should return NotFound error if instance id does not exists',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          error: 'Not Found',\n          message: 'Invalid database instance id.',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n\n    before(async () => await rte.data.generateKeys(true));\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    [\n      {\n        name: 'Should create consumer group',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n      },\n      {\n        name: 'Should throw error if no permissions for \"exists\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -exists'),\n      },\n      {\n        name: 'Should throw error if no permissions for \"xclaim\" command',\n        endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n        data: {\n          ...validInputData,\n        },\n        statusCode: 403,\n        responseBody: {\n          statusCode: 403,\n          error: 'Forbidden',\n        },\n        before: () => rte.data.setAclUserRules('~* +@all -xclaim'),\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-consumer_groups-consumers-pending_messages-get.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups/consumers/pending-messages/get`,\n  );\n\nconst pendingMessageSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    consumerName: JoiRedisString.required(),\n    idle: Joi.number().required(),\n    delivered: Joi.number().required(),\n  })\n  .strict();\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  groupName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  consumerName: Joi.string().required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  count: Joi.number(),\n  start: Joi.string(),\n  end: Joi.string(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  groupName: constants.TEST_STREAM_GROUP_1,\n  consumerName: constants.TEST_STREAM_CONSUMER_1,\n};\n\nconst responseSchema = Joi.array().items(pendingMessageSchema).required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/consumer-groups/consumers/pending-messages/get', () => {\n  requirements('!rte.crdt');\n\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(async () => {\n      await rte.data.generateBinKeys(true);\n      await rte.data.sendCommand('xreadgroup', [\n        'GROUP',\n        constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n        constants.TEST_STREAM_CONSUMER_BIN_BUFFER_1,\n        'STREAMS',\n        constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n        '>',\n      ]);\n    });\n\n    [\n      {\n        name: 'Should get pending messages from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n          consumerName: constants.TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1,\n          count: 1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const [message] = body;\n          expect(body.length).to.eql(1);\n          expect(message.id).to.be.a('string');\n          expect(message.consumerName).to.eq(\n            constants.TEST_STREAM_CONSUMER_BIN_UTF8_1,\n          );\n          expect(message.idle).to.gte(0);\n          expect(message.delivered).to.eq(1);\n        },\n      },\n      {\n        name: 'Should get pending messages from buff (return buff)',\n        query: {\n          encoding: 'buff',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n          consumerName: constants.TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1,\n          count: 1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const [message] = body;\n          expect(body.length).to.eql(1);\n          expect(message.id).to.be.a('string');\n          expect(message.consumerName).to.deep.eq(\n            constants.TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1,\n          );\n          expect(message.idle).to.gte(0);\n          expect(message.delivered).to.eq(1);\n        },\n      },\n      {\n        name: 'Should get pending messages from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          groupName: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n          consumerName: constants.TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1,\n          count: 1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          const [message] = body;\n          expect(body.length).to.eql(1);\n          expect(message.id).to.be.a('string');\n          expect(message.consumerName).to.deep.eq(\n            constants.TEST_STREAM_CONSUMER_BIN_ASCII_1,\n          );\n          expect(message.idle).to.gte(0);\n          expect(message.delivered).to.eq(1);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => {\n        await rte.data.generateStrings(true);\n        await rte.data.generateStreamsWithoutStrictMode();\n      });\n\n      beforeEach(async () => {\n        await rte.data.sendCommand('xadd', [\n          constants.TEST_STREAM_KEY_1,\n          constants.TEST_STREAM_ID_3,\n          constants.TEST_STREAM_FIELD_1,\n          constants.TEST_STREAM_VALUE_1,\n        ]);\n        await rte.data.sendCommand('xadd', [\n          constants.TEST_STREAM_KEY_1,\n          constants.TEST_STREAM_ID_4,\n          constants.TEST_STREAM_FIELD_1,\n          constants.TEST_STREAM_VALUE_1,\n        ]);\n        await rte.data.sendCommand('xreadgroup', [\n          'GROUP',\n          constants.TEST_STREAM_GROUP_1,\n          constants.TEST_STREAM_CONSUMER_1,\n          'STREAMS',\n          constants.TEST_STREAM_KEY_1,\n          '>',\n        ]);\n        await rte.data.sendCommand('xreadgroup', [\n          'GROUP',\n          constants.TEST_STREAM_GROUP_1,\n          constants.TEST_STREAM_CONSUMER_2,\n          'STREAMS',\n          constants.TEST_STREAM_KEY_1,\n          '>',\n        ]);\n      });\n\n      [\n        {\n          name: 'Should return empty array when no pending messages',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_2,\n            consumerName: constants.TEST_STREAM_CONSUMER_2,\n          },\n          responseSchema,\n          responseBody: [],\n        },\n        {\n          name: 'Should return pending messages list with only 1 message',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerName: constants.TEST_STREAM_CONSUMER_1,\n            count: 1,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            const [message] = body;\n            expect(body.length).to.eql(1);\n            expect(message.id).to.eq(constants.TEST_STREAM_ID_3);\n            expect(message.consumerName).to.eq(\n              constants.TEST_STREAM_CONSUMER_1,\n            );\n            expect(message.idle).to.gte(0);\n            expect(message.delivered).to.eq(1);\n          },\n        },\n        {\n          name: 'Should return pending messages list (2 messages)',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerName: constants.TEST_STREAM_CONSUMER_1,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.length).to.eql(2);\n          },\n        },\n        {\n          name: 'Should return pending messages list (0 messages) filtered by end',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerName: constants.TEST_STREAM_CONSUMER_1,\n            start: '-',\n            end: '99-0',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.length).to.eql(0);\n          },\n        },\n        {\n          name: 'Should return pending messages list (1 messages) filtered by end',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerName: constants.TEST_STREAM_CONSUMER_1,\n            start: '-',\n            end: '300-0',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.length).to.eql(1);\n          },\n        },\n        {\n          name: 'Should return pending messages list (0 messages) filtered by start',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerName: constants.TEST_STREAM_CONSUMER_1,\n            start: '999-0',\n            end: '+',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.length).to.eql(0);\n          },\n        },\n        {\n          name: 'Should return pending messages list (1 messages) filtered by start',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            groupName: constants.TEST_STREAM_GROUP_1,\n            consumerName: constants.TEST_STREAM_CONSUMER_1,\n            start: '400-0',\n            end: '+',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.length).to.eql(1);\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if group does not exists',\n          data: {\n            ...validInputData,\n            groupName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Consumer Group with such name was not found.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xpending\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xpending'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-consumer_groups-get.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups/get`,\n  );\n\nconst consumerGroupSchema = Joi.object().keys({\n  name: JoiRedisString.required(),\n  consumers: Joi.number().required(),\n  pending: Joi.number().required(),\n  lastDeliveredId: Joi.string().required(),\n  smallestPendingId: Joi.string().allow(null).required(),\n  greatestPendingId: Joi.string().allow(null).required(),\n});\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n};\n\nconst responseSchema = Joi.array().items(consumerGroupSchema).min(0).required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/consumer-groups/get', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should get consumer groups from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.length).to.eq(1);\n          expect(body[0].name).to.eq(constants.TEST_STREAM_GROUP_BIN_UTF8_1);\n          expect(body[0].consumers).to.eq(0);\n          expect(body[0].pending).to.eq(0);\n          expect(body[0].lastDeliveredId).to.eq(constants.TEST_STREAM_ID_1);\n          expect(body[0].smallestPendingId).to.eq(null);\n          expect(body[0].greatestPendingId).to.eq(null);\n        },\n      },\n      {\n        name: 'Should get consumer groups from buff (return buff)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.length).to.eq(1);\n          expect(body[0].name).to.deep.eq(\n            constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n          );\n          expect(body[0].consumers).to.eq(0);\n          expect(body[0].pending).to.eq(0);\n          expect(body[0].lastDeliveredId).to.eq(constants.TEST_STREAM_ID_1);\n          expect(body[0].smallestPendingId).to.eq(null);\n          expect(body[0].greatestPendingId).to.eq(null);\n        },\n      },\n      {\n        name: 'Should get consumer groups from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: ({ body }) => {\n          expect(body.length).to.eq(1);\n          expect(body[0].name).to.eq(constants.TEST_STREAM_GROUP_BIN_ASCII_1);\n          expect(body[0].consumers).to.eq(0);\n          expect(body[0].pending).to.eq(0);\n          expect(body[0].lastDeliveredId).to.eq(constants.TEST_STREAM_ID_1);\n          expect(body[0].smallestPendingId).to.eq(null);\n          expect(body[0].greatestPendingId).to.eq(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should return empty array when no consumer groups',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_2,\n          },\n          responseSchema,\n          responseBody: [],\n        },\n        {\n          name: 'Should return groups list',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.length).to.eq(2);\n            expect(body[0].name).to.eq(constants.TEST_STREAM_GROUP_1);\n            expect(body[1].name).to.eq(constants.TEST_STREAM_GROUP_2);\n            expect(body[1].consumers).to.eq(0);\n            expect(body[1].pending).to.eq(0);\n            expect(body[1].lastDeliveredId).to.eq(constants.TEST_STREAM_ID_1);\n            expect(body[1].smallestPendingId).to.eq(null);\n            expect(body[1].greatestPendingId).to.eq(null);\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xpending\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xpending'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xinfo\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xinfo'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-consumer_groups.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/consumer-groups`,\n  );\n\nconst consumerGroupSchema = Joi.object().keys({\n  name: Joi.string().label('consumerGroups.0.name').required(),\n  lastDeliveredId: Joi.string()\n    .label('consumerGroups.0.lastDeliveredId')\n    .required(),\n});\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  consumerGroups: Joi.array().items(consumerGroupSchema).required().messages({\n    'array.sparse': 'entries must be either object or array',\n    'array.base': 'property {#label} must be either object or array',\n    'any.required': '{#label} should not be empty',\n  }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  consumerGroups: [\n    {\n      name: 'group-1',\n      lastDeliveredId: '$',\n    },\n  ],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/consumer-groups', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(async () => {\n      await rte.data.generateBinKeys(true);\n      await rte.data.sendCommand('xgroup', [\n        'destroy',\n        constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n        constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n      ]);\n    });\n\n    describe('Redis version < 7', () => {\n      requirements('rte.version<7.0');\n      [\n        {\n          name: 'Should create consumer group from buff',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n            consumerGroups: [\n              {\n                name: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n                lastDeliveredId: constants.TEST_STREAM_ID_1,\n              },\n            ],\n          },\n          statusCode: 201,\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            const expected = [\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_1),\n              ],\n            ];\n\n            expect(groups).to.deep.eq(expected);\n          },\n        },\n        {\n          name: 'Should create consumer group from ascii',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n            consumerGroups: [\n              {\n                name: constants.TEST_STREAM_GROUP_BIN_ASCII_1,\n                lastDeliveredId: constants.TEST_STREAM_ID_1,\n              },\n            ],\n          },\n          statusCode: 201,\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            expect(groups).to.deep.eq([\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_1),\n              ],\n            ]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('Redis version >= 7', () => {\n      requirements('rte.version>=7.0');\n      [\n        {\n          name: 'Should create consumer group from buff',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n            consumerGroups: [\n              {\n                name: constants.TEST_STREAM_GROUP_BIN_BUF_OBJ_1,\n                lastDeliveredId: constants.TEST_STREAM_ID_1,\n              },\n            ],\n          },\n          statusCode: 201,\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            const expected = [\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_1),\n                Buffer.from('entries-read'),\n                null,\n                Buffer.from('lag'),\n                1,\n              ],\n            ];\n\n            expect(groups).to.deep.eq(expected);\n          },\n        },\n        {\n          name: 'Should create consumer group from ascii',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n            consumerGroups: [\n              {\n                name: constants.TEST_STREAM_GROUP_BIN_ASCII_1,\n                lastDeliveredId: constants.TEST_STREAM_ID_1,\n              },\n            ],\n          },\n          statusCode: 201,\n          after: async () => {\n            const groups = await rte.data.sendCommand(\n              'xinfo',\n              ['groups', constants.TEST_STREAM_KEY_BIN_BUFFER_1],\n              null,\n            );\n            expect(groups).to.deep.eq([\n              [\n                Buffer.from('name'),\n                constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n                Buffer.from('consumers'),\n                0,\n                Buffer.from('pending'),\n                0,\n                Buffer.from('last-delivered-id'),\n                Buffer.from(constants.TEST_STREAM_ID_1),\n                Buffer.from('entries-read'),\n                null,\n                Buffer.from('lag'),\n                1,\n              ],\n            ]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => {\n        await rte.client.del(constants.TEST_STREAM_KEY_2);\n        await rte.client.xadd(constants.TEST_STREAM_KEY_2, '*', 'f', 'v');\n      });\n\n      describe('Redis version < 7', () => {\n        requirements('rte.version<7.0');\n        [\n          {\n            name: 'Should create single consumer group',\n            data: {\n              keyName: constants.TEST_STREAM_KEY_2,\n              consumerGroups: [\n                {\n                  name: constants.TEST_STREAM_GROUP_1,\n                  lastDeliveredId: constants.TEST_STREAM_ID_1,\n                },\n              ],\n            },\n            before: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups.length).to.eq(0);\n            },\n            statusCode: 201,\n            after: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups).to.deep.eq([\n                [\n                  'name',\n                  constants.TEST_STREAM_GROUP_1,\n                  'consumers',\n                  0,\n                  'pending',\n                  0,\n                  'last-delivered-id',\n                  constants.TEST_STREAM_ID_1,\n                ],\n              ]);\n            },\n          },\n          {\n            name: 'Should create multiple consumer groups',\n            data: {\n              keyName: constants.TEST_STREAM_KEY_2,\n              consumerGroups: [\n                {\n                  name: constants.TEST_STREAM_GROUP_1,\n                  lastDeliveredId: constants.TEST_STREAM_ID_1,\n                },\n                {\n                  name: constants.TEST_STREAM_GROUP_2,\n                  lastDeliveredId: constants.TEST_STREAM_ID_1,\n                },\n              ],\n            },\n            before: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups.length).to.eq(0);\n            },\n            statusCode: 201,\n            after: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups).to.deep.eq([\n                [\n                  'name',\n                  constants.TEST_STREAM_GROUP_1,\n                  'consumers',\n                  0,\n                  'pending',\n                  0,\n                  'last-delivered-id',\n                  constants.TEST_STREAM_ID_1,\n                ],\n                [\n                  'name',\n                  constants.TEST_STREAM_GROUP_2,\n                  'consumers',\n                  0,\n                  'pending',\n                  0,\n                  'last-delivered-id',\n                  constants.TEST_STREAM_ID_1,\n                ],\n              ]);\n            },\n          },\n        ].forEach(mainCheckFn);\n      });\n\n      describe('Redis version >= 7', () => {\n        requirements('rte.version>=7.0');\n        [\n          {\n            name: 'Should create single consumer group',\n            data: {\n              keyName: constants.TEST_STREAM_KEY_2,\n              consumerGroups: [\n                {\n                  name: constants.TEST_STREAM_GROUP_1,\n                  lastDeliveredId: constants.TEST_STREAM_ID_1,\n                },\n              ],\n            },\n            before: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups.length).to.eq(0);\n            },\n            statusCode: 201,\n            after: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups).to.deep.eq([\n                [\n                  'name',\n                  constants.TEST_STREAM_GROUP_1,\n                  'consumers',\n                  0,\n                  'pending',\n                  0,\n                  'last-delivered-id',\n                  constants.TEST_STREAM_ID_1,\n                  'entries-read',\n                  null,\n                  'lag',\n                  1,\n                ],\n              ]);\n            },\n          },\n          {\n            name: 'Should create multiple consumer groups',\n            data: {\n              keyName: constants.TEST_STREAM_KEY_2,\n              consumerGroups: [\n                {\n                  name: constants.TEST_STREAM_GROUP_1,\n                  lastDeliveredId: constants.TEST_STREAM_ID_1,\n                },\n                {\n                  name: constants.TEST_STREAM_GROUP_2,\n                  lastDeliveredId: constants.TEST_STREAM_ID_1,\n                },\n              ],\n            },\n            before: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups.length).to.eq(0);\n            },\n            statusCode: 201,\n            after: async () => {\n              const groups = await rte.data.sendCommand('xinfo', [\n                'groups',\n                constants.TEST_STREAM_KEY_2,\n              ]);\n              expect(groups).to.deep.eq([\n                [\n                  'name',\n                  constants.TEST_STREAM_GROUP_1,\n                  'consumers',\n                  0,\n                  'pending',\n                  0,\n                  'last-delivered-id',\n                  constants.TEST_STREAM_ID_1,\n                  'entries-read',\n                  null,\n                  'lag',\n                  1,\n                ],\n                [\n                  'name',\n                  constants.TEST_STREAM_GROUP_2,\n                  'consumers',\n                  0,\n                  'pending',\n                  0,\n                  'last-delivered-id',\n                  constants.TEST_STREAM_ID_1,\n                  'entries-read',\n                  null,\n                  'lag',\n                  1,\n                ],\n              ]);\n            },\n          },\n        ].forEach(mainCheckFn);\n      });\n\n      [\n        {\n          name: 'Should return 409 Conflict error when group exists',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            consumerGroups: [\n              {\n                name: constants.TEST_STREAM_GROUP_1,\n                lastDeliveredId: constants.TEST_STREAM_ID_1,\n              },\n            ],\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n          },\n        },\n        {\n          name: 'Should return BadRequest error if key has another type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n\n      before(async () => await rte.data.generateKeys(true));\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create consumer group',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          statusCode: 201,\n          data: {\n            ...validInputData,\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xgroup\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xgroup'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-entries-get.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/entries/get`,\n  );\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  start: Joi.string(),\n  end: Joi.string(),\n  count: Joi.number().integer().min(1).allow(true),\n  sortOrder: Joi.string().valid('DESC', 'ASC'),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  start: '-',\n  end: '+',\n  count: 15,\n  sortOrder: 'DESC',\n};\n\nconst entrySchema = Joi.object().keys({\n  id: Joi.string().required(),\n  fields: Joi.array().required(),\n});\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    total: Joi.number().integer().required(),\n    lastGeneratedId: Joi.string().required(),\n    firstEntry: entrySchema.required(),\n    lastEntry: entrySchema.required(),\n    entries: Joi.array().items(entrySchema.required()).required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/entries/get', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(async () => await rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should query entries from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.keyName).to.eql(constants.TEST_STREAM_KEY_BIN_UTF8_1);\n          expect(body.total).to.eql(1);\n          expect(body.entries.length).to.eql(1);\n          expect(body.entries[0].fields).to.deep.eq([\n            {\n              name: constants.TEST_STREAM_FIELD_BIN_UTF8_1,\n              value: constants.TEST_STREAM_VALUE_BIN_UTF8_1,\n            },\n          ]);\n        },\n      },\n      {\n        name: 'Should query entries from buff (return buff)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.keyName).to.eql(constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1);\n          expect(body.total).to.eql(1);\n          expect(body.entries.length).to.eql(1);\n          expect(body.entries[0].fields).to.deep.eq([\n            {\n              name: constants.TEST_STREAM_FIELD_BIN_BUF_OBJ_1,\n              value: constants.TEST_STREAM_VALUE_BIN_BUF_OBJ_1,\n            },\n          ]);\n        },\n      },\n      {\n        name: 'Should query entries from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.keyName).to.eql(constants.TEST_STREAM_KEY_BIN_ASCII_1);\n          expect(body.total).to.eql(1);\n          expect(body.entries.length).to.eql(1);\n          expect(body.entries[0].fields).to.deep.eq([\n            {\n              name: constants.TEST_STREAM_FIELD_BIN_ASCII_1,\n              value: constants.TEST_STREAM_VALUE_BIN_ASCII_1,\n            },\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n    before(async () => await rte.data.generateHugeStream(10000, false));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      let offsetEntryId;\n      [\n        {\n          name: 'Should query 500 entries in the DESC order by default',\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            offsetEntryId = body.entries[99].id;\n            expect(body.keyName).to.eql(constants.TEST_STREAM_HUGE_KEY);\n            expect(body.total).to.eql(10000);\n            expect(body.entries.length).to.eql(500);\n            body.entries.forEach((entry, i) => {\n              expect(entry.id).to.be.a('string');\n              expect(entry.fields).to.eql([\n                { name: `f_${9999 - i}`, value: `v_${9999 - i}` },\n              ]);\n            });\n          },\n        },\n        {\n          name: 'Should query 10 entries in the DESC order starting from 100th entry',\n          data: () => ({\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            start: '-',\n            end: offsetEntryId,\n            count: 10,\n          }),\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_STREAM_HUGE_KEY);\n            expect(body.total).to.eql(10000);\n            expect(body.entries.length).to.eql(10);\n            body.entries.forEach((entry, i) => {\n              expect(entry.id).to.be.a('string');\n              expect(entry.fields).to.eql([\n                { name: `f_${9900 - i}`, value: `v_${9900 - i}` },\n              ]);\n            });\n          },\n        },\n        {\n          name: 'Should query 500 entries in the ASC order',\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            sortOrder: 'ASC',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            offsetEntryId = body.entries[99].id;\n            expect(body.keyName).to.eql(constants.TEST_STREAM_HUGE_KEY);\n            expect(body.total).to.eql(10000);\n            expect(body.entries.length).to.eql(500);\n            body.entries.forEach((entry, i) => {\n              expect(entry.id).to.be.a('string');\n              expect(entry.fields).to.eql([\n                { name: `f_${i}`, value: `v_${i}` },\n              ]);\n            });\n          },\n        },\n        {\n          name: 'Should query 10 entries in the ASC order starting from 100th entry',\n          data: () => ({\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            start: offsetEntryId,\n            end: '+',\n            count: 10,\n            sortOrder: 'ASC',\n          }),\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_STREAM_HUGE_KEY);\n            expect(body.total).to.eql(10000);\n            expect(body.entries.length).to.eql(10);\n            body.entries.forEach((entry, i) => {\n              expect(entry.id).to.be.a('string');\n              expect(entry.fields).to.eql([\n                { name: `f_${99 + i}`, value: `v_${99 + i}` },\n              ]);\n            });\n          },\n        },\n        {\n          name: 'Should return BadRequest when try to work with non-stream type',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return bad request',\n          data: {\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            offset: 45,\n            count: 45,\n            sortOrder: 'ASC',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should remove all members and key',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            offset: 0,\n            count: 15,\n            sortOrder: 'ASC',\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"xinfo\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            offset: 0,\n            count: 15,\n            sortOrder: 'ASC',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xinfo'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xrange\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            offset: 0,\n            count: 15,\n            sortOrder: 'ASC',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xrange'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xrevrange\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STREAM_HUGE_KEY,\n            offset: 0,\n            count: 15,\n            sortOrder: 'DESC',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xrevrange'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams-entries.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  beforeEach,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/streams/entries`,\n  );\n\nconst entryFieldSchema = Joi.object().keys({\n  name: Joi.string().label('entries.0.fields.0.name').required(),\n  value: Joi.string().label('entries.0.fields.0.value').required(),\n});\n\nconst entrySchema = Joi.object().keys({\n  id: Joi.string().label('entries.0.id').required(),\n  fields: Joi.array()\n    .label('entries.0.fields')\n    .items(entryFieldSchema)\n    .required()\n    .messages({\n      'array.base': '{#label} must be an array',\n    }),\n});\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  entries: Joi.array().items(entrySchema).required().messages({\n    'array.sparse': 'entries must be either object or array',\n    'array.base': '{#label} must be either object or array',\n  }),\n}).strict();\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: Joi.string().required(),\n    entries: Joi.array().items(Joi.string()).required(),\n  })\n  .required();\n\nconst validInputData = {\n  keyName: constants.TEST_STREAM_KEY_1,\n  entries: [\n    {\n      id: '*',\n      fields: [\n        {\n          name: constants.TEST_STREAM_FIELD_1,\n          value: constants.TEST_STREAM_VALUE_1,\n        },\n        {\n          name: constants.TEST_STREAM_FIELD_2,\n          value: constants.TEST_STREAM_VALUE_2,\n        },\n      ],\n    },\n  ],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams/entries', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(async () => await rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should add entries to stream from buff',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          entries: [\n            {\n              id: '*',\n              fields: [\n                {\n                  name: constants.TEST_STREAM_FIELD_BIN_BUF_OBJ_1,\n                  value: constants.TEST_STREAM_VALUE_BIN_BUF_OBJ_1,\n                },\n              ],\n            },\n          ],\n        },\n        responseSchema,\n        after: async () => {\n          expect(\n            await rte.client.xlen(constants.TEST_STREAM_KEY_BIN_BUFFER_1),\n          ).to.eq(2);\n          const [entry] = await rte.data.sendCommand(\n            'xrevrange',\n            [constants.TEST_STREAM_KEY_BIN_BUFFER_1, '+', '-', 'COUNT', 1],\n            null,\n          );\n          expect(entry[1]).to.eql([\n            constants.TEST_STREAM_FIELD_BIN_BUFFER_1,\n            constants.TEST_STREAM_VALUE_BIN_BUFFER_1,\n          ]);\n        },\n      },\n      {\n        name: 'Should add entries to stream from ascii',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n          entries: [\n            {\n              id: '*',\n              fields: [\n                {\n                  name: constants.TEST_STREAM_FIELD_BIN_ASCII_1,\n                  value: constants.TEST_STREAM_VALUE_BIN_ASCII_1,\n                },\n              ],\n            },\n          ],\n        },\n        responseSchema,\n        after: async () => {\n          expect(\n            await rte.client.xlen(constants.TEST_STREAM_KEY_BIN_BUFFER_1),\n          ).to.eq(2);\n          const [entry] = await rte.data.sendCommand(\n            'xrevrange',\n            [constants.TEST_STREAM_KEY_BIN_BUFFER_1, '+', '-', 'COUNT', 1],\n            null,\n          );\n          expect(entry[1]).to.eql([\n            constants.TEST_STREAM_FIELD_BIN_BUFFER_1,\n            constants.TEST_STREAM_VALUE_BIN_BUFFER_1,\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should add entry',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            entries: [\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_FIELD_1,\n                    value: constants.TEST_STREAM_FIELD_1,\n                  },\n                ],\n              },\n            ],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.xlen(constants.TEST_STREAM_KEY_1)).to.eq(2);\n            const [entry] = await rte.client.xrevrange(\n              constants.TEST_STREAM_KEY_1,\n              '+',\n              '-',\n              'COUNT',\n              1,\n            );\n            expect(entry[1]).to.eql([\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_FIELD_1,\n            ]);\n          },\n        },\n        {\n          name: 'Should add multiple entries and multiple fields',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            entries: [\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_FIELD_1,\n                    value: constants.TEST_STREAM_FIELD_1,\n                  },\n                  {\n                    name: constants.TEST_STREAM_FIELD_2,\n                    value: constants.TEST_STREAM_FIELD_2,\n                  },\n                ],\n              },\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_VALUE_1,\n                    value: constants.TEST_STREAM_VALUE_1,\n                  },\n                  {\n                    name: constants.TEST_STREAM_VALUE_2,\n                    value: constants.TEST_STREAM_VALUE_2,\n                  },\n                ],\n              },\n            ],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.xlen(constants.TEST_STREAM_KEY_1)).to.eq(4);\n            const [entry1, entry2] = await rte.client.xrevrange(\n              constants.TEST_STREAM_KEY_1,\n              '+',\n              '-',\n              'COUNT',\n              2,\n            );\n            expect(entry1[1]).to.eql([\n              constants.TEST_STREAM_VALUE_1,\n              constants.TEST_STREAM_VALUE_1,\n              constants.TEST_STREAM_VALUE_2,\n              constants.TEST_STREAM_VALUE_2,\n            ]);\n            expect(entry2[1]).to.eql([\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_FIELD_2,\n              constants.TEST_STREAM_FIELD_2,\n            ]);\n          },\n        },\n        {\n          name: 'Should return BadRequest when try to work with non-stream type',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return BadRequest when id specified is less then the latest one',\n          data: {\n            ...validInputData,\n            entries: [\n              {\n                ...validInputData.entries[0],\n                id: '100',\n              },\n            ],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should add entries',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xadd'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/stream/POST-databases-id-streams.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/streams`);\n\nconst entryFieldSchema = Joi.object().keys({\n  name: Joi.string().label('entries.0.fields.0.name').required(),\n  value: Joi.string().label('entries.0.fields.0.value').required(),\n});\n\nconst entrySchema = Joi.object().keys({\n  id: Joi.string().label('entries.0.id').required(),\n  fields: Joi.array()\n    .label('entries.0.fields')\n    .items(entryFieldSchema)\n    .required()\n    .messages({\n      'array.base': '{#label} must be an array',\n    }),\n});\n\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  entries: Joi.array().items(entrySchema).required().messages({\n    'array.sparse': 'entries must be either object or array',\n    'array.base': '{#label} must be either object or array',\n  }),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  entries: [\n    {\n      id: constants.TEST_STREAM_ID_1,\n      fields: [\n        {\n          name: constants.TEST_STREAM_FIELD_1,\n          value: constants.TEST_STREAM_VALUE_1,\n        },\n        {\n          name: constants.TEST_STREAM_FIELD_2,\n          value: constants.TEST_STREAM_VALUE_2,\n        },\n      ],\n    },\n  ],\n  expire: constants.TEST_STREAM_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/streams', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should create stream from buff',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_BUF_OBJ_1,\n          entries: [\n            {\n              id: '*',\n              fields: [\n                {\n                  name: constants.TEST_STREAM_FIELD_BIN_BUF_OBJ_1,\n                  value: constants.TEST_STREAM_VALUE_BIN_BUF_OBJ_1,\n                },\n              ],\n            },\n          ],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_STREAM_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          const entries = await rte.data.sendCommand(\n            'xrange',\n            [constants.TEST_STREAM_KEY_BIN_BUFFER_1, '-', '+'],\n            null,\n          );\n          expect(entries[0][1]).to.eql([\n            constants.TEST_STREAM_FIELD_BIN_BUFFER_1,\n            constants.TEST_STREAM_VALUE_BIN_BUFFER_1,\n          ]);\n        },\n      },\n      {\n        name: 'Should create stream from ascii',\n        data: {\n          keyName: constants.TEST_STREAM_KEY_BIN_ASCII_1,\n          entries: [\n            {\n              id: '*',\n              fields: [\n                {\n                  name: constants.TEST_STREAM_FIELD_BIN_ASCII_1,\n                  value: constants.TEST_STREAM_VALUE_BIN_ASCII_1,\n                },\n              ],\n            },\n          ],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_STREAM_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          const entries = await rte.data.sendCommand(\n            'xrange',\n            [constants.TEST_STREAM_KEY_BIN_BUFFER_1, '-', '+'],\n            null,\n          );\n          expect(entries[0][1]).to.eql([\n            constants.TEST_STREAM_FIELD_BIN_BUFFER_1,\n            constants.TEST_STREAM_VALUE_BIN_BUFFER_1,\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      beforeEach(async () => {\n        await rte.client.del(constants.TEST_STREAM_KEY_1);\n      });\n\n      [\n        {\n          name: 'Should create stream with single entry and single field',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            entries: [\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_FIELD_1,\n                    value: constants.TEST_STREAM_VALUE_1,\n                  },\n                ],\n              },\n            ],\n          },\n          statusCode: 201,\n          after: async () => {\n            const entries = await rte.client.xrange(\n              constants.TEST_STREAM_KEY_1,\n              '-',\n              '+',\n            );\n            expect(entries[0][1]).to.eql([\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_VALUE_1,\n            ]);\n          },\n        },\n        {\n          name: 'Should create stream with ttl',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            entries: [\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_FIELD_1,\n                    value: constants.TEST_STREAM_VALUE_1,\n                  },\n                ],\n              },\n            ],\n            expire: constants.TEST_STREAM_EXPIRE_1,\n          },\n          statusCode: 201,\n          after: async () => {\n            const ttl = await rte.client.ttl(constants.TEST_STREAM_KEY_1);\n            expect(ttl).to.lte(constants.TEST_STREAM_EXPIRE_1);\n            expect(ttl).to.gt(0);\n\n            const entries = await rte.client.xrange(\n              constants.TEST_STREAM_KEY_1,\n              '-',\n              '+',\n            );\n            expect(entries[0][1]).to.eql([\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_VALUE_1,\n            ]);\n          },\n        },\n        {\n          name: 'Should create stream with multiple entries and multiple fields',\n          data: {\n            keyName: constants.TEST_STREAM_KEY_1,\n            entries: [\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_FIELD_1,\n                    value: constants.TEST_STREAM_VALUE_1,\n                  },\n                  {\n                    name: constants.TEST_STREAM_FIELD_2,\n                    value: constants.TEST_STREAM_VALUE_2,\n                  },\n                ],\n              },\n              {\n                id: '*',\n                fields: [\n                  {\n                    name: constants.TEST_STREAM_FIELD_1,\n                    value: constants.TEST_STREAM_VALUE_1,\n                  },\n                  {\n                    name: constants.TEST_STREAM_FIELD_2,\n                    value: constants.TEST_STREAM_VALUE_2,\n                  },\n                ],\n              },\n            ],\n          },\n          statusCode: 201,\n          after: async () => {\n            const entries = await rte.client.xrange(\n              constants.TEST_STREAM_KEY_1,\n              '-',\n              '+',\n            );\n            expect(entries[0][1]).to.eql([\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_VALUE_1,\n              constants.TEST_STREAM_FIELD_2,\n              constants.TEST_STREAM_VALUE_2,\n            ]);\n            expect(entries[1][1]).to.eql([\n              constants.TEST_STREAM_FIELD_1,\n              constants.TEST_STREAM_VALUE_1,\n              constants.TEST_STREAM_FIELD_2,\n              constants.TEST_STREAM_VALUE_2,\n            ]);\n          },\n        },\n        {\n          name: 'Should return Conflict error when trying to create key with existing key name',\n          data: {\n            ...validInputData,\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            ...validInputData,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create stream',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          statusCode: 201,\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"xadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            ...validInputData,\n            keyName: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -xadd'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/string/POST-databases-id-string-download_value.test.ts",
    "content": "import {\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/string/download-value`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STRING_KEY_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/string/download-value', () => {\n  describe('Main', () => {\n    before(() => rte.data.generateBinKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should download value',\n          data: {\n            keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          },\n          responseHeaders: {\n            'content-type': 'application/octet-stream',\n            'content-disposition': 'attachment;filename=\"string_value\"',\n            'access-control-expose-headers': 'Content-Disposition',\n          },\n          responseBody: constants.TEST_STRING_VALUE_BIN_BUFFER_1,\n        },\n        {\n          name: 'Should return an error when incorrect type',\n          data: {\n            keyName: constants.TEST_LIST_KEY_BIN_BUF_OBJ_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should download value',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          },\n          responseHeaders: {\n            'content-type': 'application/octet-stream',\n            'content-disposition': 'attachment;filename=\"string_value\"',\n            'access-control-expose-headers': 'Content-Disposition',\n          },\n          responseBody: constants.TEST_STRING_VALUE_BIN_BUFFER_1,\n        },\n        {\n          name: 'Should throw error if no permissions for \"set\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n            value: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -get'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/string/POST-databases-id-string-get_value.test.ts",
    "content": "import {\n  describe,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/string/get-value`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STRING_KEY_1,\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    value: JoiRedisString.required(),\n  })\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/string/get-value', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should return value in utf8 (by default)',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseBody: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n          value: constants.TEST_STRING_VALUE_BIN_UTF8_1,\n        },\n      },\n      {\n        name: 'Should return value in utf8',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseBody: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n          value: constants.TEST_STRING_VALUE_BIN_UTF8_1,\n        },\n      },\n      {\n        name: 'Should return value in ascii',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n        },\n        responseBody: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n          value: constants.TEST_STRING_VALUE_BIN_ASCII_1,\n        },\n      },\n      {\n        name: 'Should return value in buffer',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n        },\n        responseBody: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          value: constants.TEST_STRING_VALUE_BIN_BUF_OBJ_1,\n        },\n      },\n      {\n        name: 'Should return part of value in buffer (only \"end\")',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          end: constants.TEST_STRING_KEY_END,\n        },\n        responseBody: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          value: constants.TEST_STRING_PARTIAL_VALUE_BIN_BUF_OBJ_1,\n        },\n      },\n      {\n        name: 'Should return part of value in buffer (\"start\" & \"end\")',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          start: constants.TEST_STRING_KEY_START_2,\n          end: constants.TEST_STRING_KEY_END,\n        },\n        responseBody: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          value: constants.TEST_STRING_PARTIAL_VALUE_BIN_BUF_OBJ_2,\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n        },\n        statusCode: 404,\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Main', () => {\n    before(() => rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should get value',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.TEST_STRING_VALUE_1,\n          },\n        },\n        {\n          name: 'Should get part of value',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            end: constants.TEST_STRING_KEY_END,\n          },\n          responseBody: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.TEST_STRING_VALUE_1.slice(\n              constants.TEST_STRING_KEY_START_1,\n              constants.TEST_STRING_KEY_END + 1,\n            ),\n          },\n        },\n        {\n          name: 'Should return an error when incorrect type',\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n            // message: 'WRONGTYPE Operation against a key holding the wrong kind of value',\n          },\n        },\n        {\n          name: 'Should return an error when incorrect end of string',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            end: 0,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return an error when start of string greater than end',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            start: 10,\n            end: 9,\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should get value',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"set\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -get'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/string/POST-databases-id-string.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/string`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  value: Joi.string().required(),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STRING_KEY_1,\n  value: constants.TEST_STRING_VALUE_1,\n  expire: constants.TEST_STRING_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst createCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      }\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n        expect(await rte.client.get(testCase.data.keyName)).to.eql(\n          testCase.data.value,\n        );\n        if (testCase.data.expire) {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.gte(\n            testCase.data.expire - 5,\n          );\n        } else {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.eql(-1);\n        }\n      }\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/string', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should create string from buff',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          value: constants.TEST_STRING_VALUE_BIN_BUF_OBJ_1,\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.getBuffer(constants.TEST_STRING_KEY_BIN_BUFFER_1),\n          ).to.eql(constants.TEST_STRING_VALUE_BIN_BUFFER_1);\n        },\n      },\n      {\n        name: 'Should create string from ascii',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n          value: constants.TEST_STRING_VALUE_BIN_ASCII_1,\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.getBuffer(constants.TEST_STRING_KEY_BIN_BUFFER_1),\n          ).to.eql(constants.TEST_STRING_VALUE_BIN_BUFFER_1);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should create item with empty value',\n          data: {\n            keyName: constants.getRandomString(),\n            value: '',\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with key ttl',\n          data: {\n            keyName: constants.getRandomString(),\n            value: constants.getRandomString(),\n            expire: constants.TEST_STRING_EXPIRE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create regular item',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.TEST_STRING_VALUE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should return conflict error if key already exists',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.getRandomString(),\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n            message: 'This key name is already in use.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            ),\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            ),\n        },\n      ].map(createCheckFn);\n    });\n\n    describe('Big values', () => {\n      requirements('rte.onPremise');\n      before(rte.data.truncate);\n\n      [\n        {\n          name: 'Should create 110MB string',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.GENERATE_BIG_TEST_STRING_VALUE(10),\n          },\n          statusCode: 201,\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            value: constants.TEST_STRING_VALUE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"set\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            value: constants.getRandomString(),\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -set'),\n        },\n      ].map(createCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/string/PUT-databases-id-string.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  Joi,\n  deps,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).put(`/${constants.API.DATABASES}/${instanceId}/string`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  value: Joi.string().required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_STRING_KEY_1,\n  value: constants.TEST_STRING_VALUE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst updateCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test execution\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      expect(await rte.client.get(testCase.data.keyName)).to.eql(\n        testCase.data.value,\n      );\n    }\n  });\n};\n\ndescribe('PUT /databases/:instanceId/string', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(rte.data.generateBinKeys);\n\n    [\n      {\n        name: 'Should update string from buff',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_BUF_OBJ_1,\n          value: constants.TEST_STRING_VALUE_BIN_BUF_OBJ_1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.getBuffer(constants.TEST_STRING_KEY_BIN_BUFFER_1),\n          ).to.eql(constants.TEST_STRING_VALUE_BIN_BUFFER_1);\n        },\n      },\n      {\n        name: 'Should update string from ascii',\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_ASCII_1,\n          value: constants.TEST_STRING_VALUE_BIN_ASCII_1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.getBuffer(constants.TEST_STRING_KEY_BIN_BUFFER_1),\n          ).to.eql(constants.TEST_STRING_VALUE_BIN_BUFFER_1);\n        },\n      },\n      {\n        name: 'Should return error when send unicode with unprintable chars',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_STRING_KEY_BIN_UTF8_1,\n          value: constants.TEST_STRING_KEY_BIN_ASCII_1,\n        },\n        statusCode: 404,\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            ),\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            value: constants.getRandomString(),\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n          },\n          after: () => {},\n        },\n        {\n          name: 'Should edit existing value',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: '',\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should edit existing value and do not edit ttl',\n          data: {\n            keyName: constants.TEST_STRING_KEY_2,\n            value: '',\n          },\n          statusCode: 200,\n          after: async function () {\n            expect(await rte.client.get(constants.TEST_STRING_KEY_2)).to.eql(\n              '',\n            );\n            expect(await rte.client.ttl(constants.TEST_STRING_KEY_2))\n              .to.lte(constants.TEST_STRING_EXPIRE_2)\n              .gte(-1);\n          },\n        },\n        {\n          name: 'Should edit existing value for different key type',\n          data: {\n            keyName: constants.TEST_HASH_KEY_1,\n            value: '',\n          },\n          statusCode: 200,\n        },\n      ].map(updateCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: constants.TEST_STRING_VALUE_1,\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"set\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: '',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -set'),\n          after: async () =>\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            ),\n        },\n        {\n          name: 'Should throw error if no permissions for \"ttl\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            value: '',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -ttl'),\n          after: async () =>\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            ),\n        },\n        {\n          name: 'Should throw error if no permissions for \"expire\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_STRING_KEY_2,\n            value: '',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -expire'),\n          // todo: Implement transaction for set + expire commands on BE. As if no ACL rules for \"expire\" key will be edited but ttl will be not set\n        },\n      ].map(updateCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/triggered-functions/DELETE-databases-id-library.test.ts",
    "content": "import {\n  Joi,\n  expect,\n  describe,\n  before,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  requirements,\n} from '../deps';\n\nconst { request, server, constants, rte } = deps;\n\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/triggered-functions/library`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  libraryName: Joi.string().required(),\n}).strict();\n\nconst validInputData = {\n  libraryName: constants.getRandomString(),\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`DELETE /databases/:id/triggered-functions/library`, () => {\n  requirements('rte.modules.redisgears_2');\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    before(async () => {\n      await rte.data.generateTriggeredFunctionsLibrary();\n    });\n\n    [\n      {\n        name: 'Should remove library by library name',\n        data: {\n          libraryName: constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME,\n        },\n        before: async () => {\n          const libraries = await rte.data.sendCommand('TFUNCTION', [\n            'LIST',\n            'LIBRARY',\n            constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME,\n          ]);\n          expect(libraries.length).to.eq(1);\n        },\n        after: async () => {\n          const libraries = await rte.data.sendCommand('TFUNCTION', [\n            'LIST',\n            'LIBRARY',\n            constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME,\n          ]);\n          expect(libraries.length).to.eq(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/triggered-functions/GET-databases-id-functions.test.ts",
    "content": "import { describe, deps, requirements, _, getMainCheckFn } from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { request, server, constants } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/triggered-functions/functions`,\n  );\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object({\n      type: Joi.string()\n        .valid(\n          'functions',\n          'cluster_functions',\n          'keyspace_triggers',\n          'stream_triggers',\n        )\n        .required(),\n      name: Joi.string().required(),\n      library: Joi.string(),\n      success: Joi.number(),\n      fail: Joi.number(),\n      total: Joi.number(),\n      flags: Joi.array().items(Joi.string()),\n      isAsync: Joi.boolean(),\n      description: Joi.string(),\n      lastError: Joi.string(),\n      lastExecutionTime: Joi.number(),\n      totalExecutionTime: Joi.number(),\n      prefix: Joi.string(),\n      trim: Joi.boolean(),\n      window: Joi.number(),\n    }),\n  )\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /databases/:instanceId/history`, () => {\n  requirements('rte.modules.redisgears_2');\n\n  [\n    {\n      name: 'Should get triggered functions libraries',\n      responseSchema,\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/triggered-functions/GET-databases-id-libraries.test.ts",
    "content": "import { describe, deps, requirements, _, getMainCheckFn } from '../deps';\nimport { Joi } from '../../helpers/test';\nconst { request, server, constants } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/triggered-functions/libraries`,\n  );\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object({\n      name: Joi.string().required(),\n      user: Joi.string().required(),\n      totalFunctions: Joi.number().required(),\n      pendingJobs: Joi.number().required(),\n    }),\n  )\n  .required()\n  .strict(true);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe(`GET /databases/:instanceId/history`, () => {\n  requirements('rte.modules.redisgears_2');\n\n  [\n    {\n      name: 'Should get triggered functions libraries',\n      responseSchema,\n    },\n  ].map(mainCheckFn);\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/triggered-functions/POST-databases-id-library.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  _,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/triggered-functions/library`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  code: Joi.string().required(),\n  configuration: Joi.string().allow(null),\n}).strict();\n\nconst validInputData = {\n  code: constants.TEST_TRIGGERED_FUNCTIONS_CODE,\n  configuration: constants.TEST_TRIGGERED_FUNCTIONS_CONFIGURATION,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/triggered-functions/library', () => {\n  requirements('rte.modules.redisgears_2');\n\n  describe('Main', () => {\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should upload library',\n          data: {\n            code: constants.TEST_TRIGGERED_FUNCTIONS_CODE,\n            configuration: constants.TEST_TRIGGERED_FUNCTIONS_CONFIGURATION,\n          },\n          statusCode: 201,\n          before: async () => {\n            // Triggered and functions did not have ability to remove all libraries\n            try {\n              await rte.data.sendCommand('TFUNCTION', [\n                'DELETE',\n                constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME,\n              ]);\n            } catch (err) {\n              // ignore\n            }\n            const libraries = await rte.data.sendCommand('TFUNCTION', [\n              'LIST',\n              'LIBRARY',\n              constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME,\n            ]);\n            expect(libraries.length).to.eq(0);\n          },\n          after: async () => {\n            const libraries = await rte.data.sendCommand('TFUNCTION', [\n              'LIST',\n              'LIBRARY',\n              constants.TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME,\n            ]);\n            expect(libraries.length).to.eq(1);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions-id.test.ts",
    "content": "import { expect, describe, deps, getMainCheckFn } from '../deps';\nconst { server, request, constants, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = constants.TEST_COMMAND_EXECUTION_ID_1,\n) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/workbench/command-executions/${id}`,\n  );\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/workbench/command-executions/:commandExecutionId', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            constants.TEST_COMMAND_EXECUTION_ID_1,\n          ),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 0 array when no history items yet',\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            1,\n          );\n        },\n        after: async () => {\n          expect(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({ id: constants.TEST_COMMAND_EXECUTION_ID_1 }),\n          ).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts",
    "content": "import {\n  expect,\n  describe,\n  deps,\n  getMainCheckFn,\n  Joi,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n} from '../deps';\nconst { server, request, constants, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/workbench/command-executions`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  type: Joi.string().valid('WORKBENCH', 'SEARCH').allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict();\n\nconst validInputData = {\n  type: 'WORKBENCH',\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/workbench/command-executions', () => {\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 0 array when no history items yet',\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            2,\n          );\n        },\n        after: async () => {\n          expect(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).count({}),\n          ).to.eq(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Filter', () => {\n    beforeEach(async () => {\n      await localDb.generateNCommandExecutions(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'WORKBENCH',\n        },\n        20,\n        true,\n      );\n      await localDb.generateNCommandExecutions(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'SEARCH',\n        },\n        10,\n        false,\n      );\n    });\n\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return remove only WORKBENCH items (by default)',\n        after: async () => {\n          expect(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).count({}),\n          ).to.eq(10);\n        },\n      },\n      {\n        name: 'Should return remove only WORKBENCH items',\n        data: {\n          type: 'WORKBENCH',\n        },\n        after: async () => {\n          expect(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).count({}),\n          ).to.eq(10);\n        },\n      },\n      {\n        name: 'Should return remove only SEARCH items',\n        data: {\n          type: 'SEARCH',\n        },\n        after: async () => {\n          expect(\n            await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).count({}),\n          ).to.eq(20);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions-id.test.ts",
    "content": "import { expect, describe, it, Joi, deps, validateApiCall } from '../deps';\nconst { server, request, constants, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (\n  instanceId = constants.TEST_INSTANCE_ID,\n  id = constants.TEST_COMMAND_EXECUTION_ID_1,\n) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/workbench/command-executions/${id}`,\n  );\n\nconst responseSchema = Joi.object()\n  .keys({\n    id: Joi.string().required(),\n    databaseId: Joi.string().required(),\n    command: Joi.string().required().allow(null),\n    result: Joi.array()\n      .items(\n        Joi.object({\n          response: Joi.any().required(),\n          status: Joi.string().required(),\n        }),\n      )\n      .allow(null),\n    mode: Joi.string().required(),\n    summary: Joi.string().allow(null),\n    resultsMode: Joi.string().allow(null),\n    executionTime: Joi.number().required(),\n    db: Joi.number().integer().allow(null),\n    createdAt: Joi.date().required(),\n    type: Joi.string().valid('WORKBENCH', 'SEARCH').required(),\n  })\n  .required();\n\nconst mainCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\ndescribe('GET /databases/:instanceId/workbench/command-executions/:commandExecutionId', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () =>\n          endpoint(\n            constants.TEST_NOT_EXISTED_INSTANCE_ID,\n            constants.TEST_COMMAND_EXECUTION_ID_1,\n          ),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 404 not found when incorrect command execution id',\n        endpoint: () =>\n          endpoint(constants.TEST_INSTANCE_ID, constants.TEST_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Command execution was not found.',\n          error: 'Not Found',\n        },\n      },\n      {\n        name: 'Should return 0 array when no history items yet',\n        responseSchema,\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n            },\n            100,\n            true,\n          );\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n            },\n            1,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eql(constants.TEST_COMMAND_EXECUTION_ID_1);\n        },\n      },\n      {\n        name: 'Should return null in the command and result when unable to decrypt',\n        responseSchema,\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              id: constants.TEST_COMMAND_EXECUTION_ID_1,\n              command: 'badencryption',\n              result: 'badencryption',\n              encryption: 'KEYTAR',\n            },\n            1,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.id).to.eql(constants.TEST_COMMAND_EXECUTION_ID_1);\n          expect(body.command).to.eql(null);\n          expect(body.result).to.eql(null);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions.test.ts",
    "content": "import { expect, describe, before, Joi, deps, getMainCheckFn } from '../deps';\nconst { server, request, constants, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).get(\n    `/${constants.API.DATABASES}/${instanceId}/workbench/command-executions`,\n  );\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      databaseId: Joi.string().required(),\n      command: Joi.string().required(),\n      role: Joi.string().allow(null),\n      mode: Joi.string().required(),\n      summary: Joi.string().allow(null),\n      resultsMode: Joi.string().allow(null),\n      executionTime: Joi.number().required(),\n      nodeOptions: Joi.object()\n        .keys({\n          host: Joi.string().required(),\n          port: Joi.number().required(),\n          enableRedirection: Joi.boolean().required(),\n        })\n        .allow(null),\n      db: Joi.number().integer().allow(null),\n      createdAt: Joi.date().required(),\n      type: Joi.string().valid('WORKBENCH', 'SEARCH').required(),\n    }),\n  )\n  .required()\n  .max(30);\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('GET /databases/:instanceId/workbench/command-executions', () => {\n  describe('Common', () => {\n    [\n      {\n        name: 'Should return 0 array when no history items yet',\n        responseSchema,\n        before: async () => {\n          await (\n            await localDb.getRepository(localDb.repositories.COMMAND_EXECUTION)\n          ).clear();\n        },\n        checkFn: async ({ body }) => {\n          expect(body).to.eql([]);\n        },\n      },\n      {\n        name: 'Should get only 30 items',\n        responseSchema,\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n            },\n            100,\n            true,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(30);\n          for (let i = 0; i < 30; i++) {\n            expect(body[i].command).to.eql('set foo bar');\n          }\n        },\n      },\n      {\n        name: 'Should return only 10 items that we are able to decrypt',\n        responseSchema,\n        before: async () => {\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n            },\n            10,\n            true,\n          );\n          await localDb.generateNCommandExecutions(\n            {\n              databaseId: constants.TEST_INSTANCE_ID,\n              command: 'invalidaencrypted',\n              encryption: 'KEYTAR',\n            },\n            10,\n          );\n        },\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(10);\n\n          for (let i = 0; i < 10; i++) {\n            expect(body[i].command).to.eql('set foo bar');\n          }\n        },\n      },\n      {\n        name: 'Should return 404 not found when incorrect instance',\n        endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n        statusCode: 404,\n        responseBody: {\n          statusCode: 404,\n          message: 'Invalid database instance id.',\n          error: 'Not Found',\n        },\n      },\n    ].map(mainCheckFn);\n  });\n  describe('Filter', () => {\n    before(async () => {\n      await localDb.generateNCommandExecutions(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'WORKBENCH',\n        },\n        20,\n        true,\n      );\n      await localDb.generateNCommandExecutions(\n        {\n          databaseId: constants.TEST_INSTANCE_ID,\n          type: 'SEARCH',\n        },\n        10,\n        false,\n      );\n    });\n\n    [\n      {\n        name: 'Should get only 20 items (workbench by default)',\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(20);\n          for (let i = 0; i < 20; i++) {\n            expect(body[i].command).to.eql('set foo bar');\n            expect(body[i].type).to.eql('WORKBENCH');\n          }\n        },\n      },\n      {\n        name: 'Should get only 20 items filtered by type (WORKBENCH)',\n        query: {\n          type: 'WORKBENCH',\n        },\n        responseSchema,\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(20);\n          for (let i = 0; i < 20; i++) {\n            expect(body[i].command).to.eql('set foo bar');\n            expect(body[i].type).to.eql('WORKBENCH');\n          }\n        },\n      },\n      {\n        name: 'Should get only 10 items filtered by type (SEARCH)',\n        responseSchema,\n        query: {\n          type: 'SEARCH',\n        },\n        checkFn: async ({ body }) => {\n          expect(body.length).to.eql(10);\n          for (let i = 0; i < 10; i++) {\n            expect(body[i].command).to.eql('set foo bar');\n            expect(body[i].type).to.eql('SEARCH');\n          }\n        },\n      },\n    ].map(mainCheckFn);\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts",
    "content": "import {\n  expect,\n  before,\n  describe,\n  it,\n  Joi,\n  _,\n  deps,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  requirements,\n  getMainCheckFn,\n} from '../deps';\nimport { convertArrayReplyToObject } from 'src/modules/redis/utils';\nconst { server, request, constants, rte, localDb } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/workbench/command-executions`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  commands: Joi.array().items(Joi.string().allow('')).required().messages({\n    'string.base': 'each value in commands must be a string',\n  }),\n  mode: Joi.string().valid('RAW', 'ASCII').allow(null),\n  resultsMode: Joi.string().valid('DEFAULT', 'GROUP_MODE').allow(null),\n  type: Joi.string().valid('WORKBENCH', 'SEARCH').allow(null),\n})\n  .messages({\n    'any.required': '{#label} should not be empty',\n  })\n  .strict();\n\nconst validInputData = {\n  commands: ['set foo bar'],\n  mode: 'RAW',\n  resultsMode: 'DEFAULT',\n};\n\nconst responseSchema = Joi.array()\n  .items(\n    Joi.object().keys({\n      id: Joi.string().required(),\n      databaseId: Joi.string().required(),\n      command: Joi.string().required(),\n      mode: Joi.string().allow(null),\n      resultsMode: Joi.string().allow(null),\n      result: Joi.array().items(\n        Joi.object({\n          response: Joi.any().required(),\n          status: Joi.string().required(),\n        }),\n      ),\n      createdAt: Joi.date().required(),\n      executionTime: Joi.number().integer(),\n      db: Joi.number().integer().allow(null),\n      isNotStored: Joi.boolean(),\n      summary: Joi.object({\n        total: Joi.number(),\n        success: Joi.number(),\n        fail: Joi.number(),\n      }),\n      type: Joi.string().valid('WORKBENCH', 'SEARCH').required(),\n    }),\n  )\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/workbench/command-executions', () => {\n  before(rte.data.truncate);\n\n  describe('Validation', () => {\n    generateInvalidDataTestCases(dataSchema, validInputData).map(\n      validateInvalidDataTestCase(endpoint, dataSchema),\n    );\n  });\n\n  describe('Common', () => {\n    describe('String', () => {\n      const bigStringValue = Buffer.alloc(10, 'a').toString();\n\n      [\n        {\n          name: 'Should return 404 not found when incorrect instance',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            message: 'Invalid database instance id.',\n            error: 'Not Found',\n          },\n        },\n        {\n          name: 'Should get string',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].command).to.eql(\n              `get ${constants.TEST_STRING_KEY_1}`,\n            );\n            expect(body[0].executionTime).to.be.a('number');\n            expect(body[0].db).to.be.eql(0);\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].response).to.eql(bigStringValue);\n            expect(body[0].result[0].status).to.eql('success');\n\n            const entity: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            expect(entity.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(body[0].command).to.eql(localDb.decryptData(entity.command));\n            expect(body[0].result).to.eql(\n              JSON.parse(localDb.decryptData(entity.result)),\n            );\n          },\n          before: async () => {\n            expect(\n              await rte.client.set(constants.TEST_STRING_KEY_1, bigStringValue),\n            );\n          },\n        },\n        {\n          name: 'Should remove string',\n          data: {\n            commands: [`del ${constants.TEST_STRING_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Big String', () => {\n      const bigStringValue = Buffer.alloc(1024 * 1024, 'a').toString();\n\n      [\n        {\n          name: 'Should create string',\n          data: {\n            commands: [`set ${constants.TEST_STRING_KEY_1} ${bigStringValue}`],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.get(constants.TEST_STRING_KEY_1)).to.eql(\n              bigStringValue,\n            );\n          },\n        },\n        {\n          name: 'Should get string',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].command).to.eql(\n              `get ${constants.TEST_STRING_KEY_1}`,\n            );\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].response).to.eql(bigStringValue);\n            expect(body[0].result[0].status).to.eql('success');\n\n            const entity: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            expect(entity.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(localDb.encryptData(body[0].command)).to.eql(entity.command);\n            expect(\n              localDb.encryptData(\n                JSON.stringify([\n                  {\n                    status: 'success',\n                    response:\n                      'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.',\n                    sizeLimitExceeded: true,\n                  },\n                ]),\n              ),\n            ).to.eql(entity.result);\n          },\n        },\n        {\n          name: 'Should remove string',\n          data: {\n            commands: [`del ${constants.TEST_STRING_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STRING_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('List', () => {\n      [\n        {\n          name: 'Should create list',\n          data: {\n            commands: [\n              `lpush ${constants.TEST_LIST_KEY_1} ${constants.TEST_LIST_ELEMENT_1} ${constants.TEST_LIST_ELEMENT_2}`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_LIST_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.client.lrange(constants.TEST_LIST_KEY_1, 0, 100),\n            ).to.eql([\n              constants.TEST_LIST_ELEMENT_2,\n              constants.TEST_LIST_ELEMENT_1,\n            ]);\n          },\n        },\n        {\n          name: 'Should get list',\n          data: {\n            commands: [`lrange ${constants.TEST_LIST_KEY_1} 0 100`],\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].result).to.eql([\n              {\n                status: 'success',\n                response: [\n                  constants.TEST_LIST_ELEMENT_2,\n                  constants.TEST_LIST_ELEMENT_1,\n                ],\n              },\n            ]);\n\n            const entity: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            expect(entity.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(body[0].command).to.eql(localDb.decryptData(entity.command));\n            expect(body[0].result).to.eql(\n              JSON.parse(localDb.decryptData(entity.result)),\n            );\n          },\n        },\n        {\n          name: 'Should remove list',\n          data: {\n            commands: [`del ${constants.TEST_LIST_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_LIST_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Set', () => {\n      [\n        {\n          name: 'Should create set',\n          data: {\n            commands: [\n              `sadd ${constants.TEST_SET_KEY_1} ${constants.TEST_SET_MEMBER_1} ${constants.TEST_SET_MEMBER_2}`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_SET_KEY_1)).to.eql(0);\n          },\n          after: async () => {\n            const [cursor, set] = await rte.client.sscan(\n              constants.TEST_SET_KEY_1,\n              0,\n            );\n            expect(cursor).to.eql('0');\n            expect(set.length).to.eql(2);\n            expect(set.join()).to.include(constants.TEST_SET_MEMBER_1);\n            expect(set.join()).to.include(constants.TEST_SET_MEMBER_2);\n          },\n        },\n        {\n          name: 'Should get set',\n          data: {\n            commands: [`sscan ${constants.TEST_SET_KEY_1} 0 count 100`],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0]).to.eql('0'); // 0 cursor\n            // Check for members. No order guaranteed\n            expect(body[0].result[0].response[1]).to.include(\n              constants.TEST_SET_MEMBER_1,\n            );\n            expect(body[0].result[0].response[1]).to.include(\n              constants.TEST_SET_MEMBER_2,\n            );\n          },\n        },\n        {\n          name: 'Should get set (multiline)',\n          data: {\n            commands: [\n              `sscan\\n    ${constants.TEST_SET_KEY_1} 0\\n    count\\n    100`,\n            ],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].command).to.eql(\n              `sscan\\n    ${constants.TEST_SET_KEY_1} 0\\n    count\\n    100`,\n            );\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0]).to.eql('0'); // 0 cursor\n            // Check for members. No order guaranteed\n            expect(body[0].result[0].response[1]).to.include(\n              constants.TEST_SET_MEMBER_1,\n            );\n            expect(body[0].result[0].response[1]).to.include(\n              constants.TEST_SET_MEMBER_2,\n            );\n          },\n        },\n        {\n          name: 'Should remove list',\n          data: {\n            commands: [`del ${constants.TEST_SET_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_SET_KEY_1)).to.eql(0);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('ZSet', () => {\n      [\n        {\n          name: 'Should create zset',\n          data: {\n            commands: [\n              `zadd ${constants.TEST_ZSET_KEY_1} 1 ${constants.TEST_ZSET_MEMBER_1} 2 ${constants.TEST_ZSET_MEMBER_2}`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_ZSET_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 100),\n            ).to.deep.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n            ]);\n          },\n        },\n        {\n          name: 'Should get zset',\n          data: {\n            commands: [`zrange ${constants.TEST_ZSET_KEY_1} 0 100`],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result).to.eql([\n              {\n                status: 'success',\n                response: [\n                  constants.TEST_ZSET_MEMBER_1,\n                  constants.TEST_ZSET_MEMBER_2,\n                ],\n              },\n            ]);\n          },\n        },\n        {\n          name: 'Should remove zset',\n          data: {\n            commands: [`del ${constants.TEST_ZSET_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_ZSET_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Hash', () => {\n      [\n        {\n          name: 'Should create hash',\n          data: {\n            commands: [\n              `hset ${constants.TEST_HASH_KEY_1} ${constants.TEST_HASH_FIELD_1_NAME} ${constants.TEST_HASH_FIELD_1_VALUE}`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_HASH_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              convertArrayReplyToObject(\n                await rte.client.hgetall(constants.TEST_HASH_KEY_1),\n              ),\n            ).to.deep.eql({\n              [constants.TEST_HASH_FIELD_1_NAME]:\n                constants.TEST_HASH_FIELD_1_VALUE,\n            });\n          },\n        },\n        {\n          name: 'Should get hash',\n          data: {\n            commands: [`hgetall ${constants.TEST_HASH_KEY_1}`],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect([\n              // TODO: investigate the difference between getting a hash\n              // result from ioredis\n              [\n                {\n                  response: {\n                    [constants.TEST_HASH_FIELD_1_NAME]:\n                      constants.TEST_HASH_FIELD_1_VALUE,\n                  },\n                  status: 'success',\n                },\n              ],\n              // result from node-redis\n              [\n                {\n                  response: [\n                    constants.TEST_HASH_FIELD_1_NAME,\n                    constants.TEST_HASH_FIELD_1_VALUE,\n                  ],\n                  status: 'success',\n                },\n              ],\n            ]).to.deep.contain(body[0].result);\n          },\n        },\n        {\n          name: 'Should remove hash',\n          data: {\n            commands: [`del ${constants.TEST_HASH_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_HASH_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('ReJSON-RL', () => {\n      requirements('rte.modules.rejson');\n      [\n        {\n          name: 'Should create json',\n          data: {\n            commands: [\n              `json.set ${constants.TEST_REJSON_KEY_1} . \"{\\\\\"field\\\\\":\\\\\"value\\\\\"}\"`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_REJSON_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.data.executeCommand(\n                'json.get',\n                constants.TEST_REJSON_KEY_1,\n                '.',\n              ),\n            ).to.eql('{\"field\":\"value\"}');\n          },\n        },\n        {\n          name: 'Should get json',\n          data: {\n            commands: [`json.get ${constants.TEST_REJSON_KEY_1} .field`],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result).to.eql([\n              {\n                status: 'success',\n                response: '\\\\\"value\\\\\"',\n              },\n            ]);\n          },\n        },\n        {\n          name: 'Should remove json',\n          data: {\n            commands: [`json.del ${constants.TEST_REJSON_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_REJSON_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('TSDB-TYPE', () => {\n      requirements('rte.modules.timeseries');\n      [\n        {\n          name: 'Should create ts',\n          data: {\n            commands: [\n              `ts.create ${constants.TEST_TS_KEY_1} ${constants.TEST_TS_VALUE_1} ${constants.TEST_TS_VALUE_2}`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_TS_KEY_1)).to.eql(0);\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_TS_KEY_1)).to.eql(1);\n          },\n        },\n        {\n          name: 'Should add to ts',\n          data: {\n            commands: [\n              `ts.add ${constants.TEST_TS_KEY_1} ${constants.TEST_TS_TIMESTAMP_1} ${constants.TEST_TS_VALUE_1}`,\n            ],\n          },\n          responseSchema,\n          after: async () => {\n            expect(\n              await rte.data.executeCommand('ts.get', constants.TEST_TS_KEY_1),\n            ).to.eql([\n              constants.TEST_TS_TIMESTAMP_1,\n              constants.TEST_TS_VALUE_1.toString(),\n            ]);\n          },\n        },\n        {\n          name: 'Should get ts',\n          data: {\n            commands: [`ts.get ${constants.TEST_TS_KEY_1}`],\n            outputFormat: 'TEXT',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result).to.eql([\n              {\n                status: 'success',\n                response: [\n                  constants.TEST_TS_TIMESTAMP_1,\n                  constants.TEST_TS_VALUE_1.toString(),\n                ],\n              },\n            ]);\n          },\n        },\n        {\n          name: 'Should remove ts',\n          data: {\n            commands: [`del ${constants.TEST_TS_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_TS_KEY_1)).to.eql(0);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Graph', () => {\n      requirements('rte.modules.graph');\n      [\n        {\n          name: 'Should create graph',\n          data: {\n            commands: [\n              `graph.query ${constants.TEST_GRAPH_KEY_1} \"CREATE (n1)\"`,\n            ],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0]).to.include(\n              'Nodes created: 1',\n            );\n          },\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_GRAPH_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_GRAPH_KEY_1)).to.eql(\n              1,\n            );\n          },\n        },\n        {\n          name: 'Should get graph',\n          data: {\n            commands: [\n              `graph.query ${constants.TEST_GRAPH_KEY_1} \"MATCH (n1) RETURN n1\"`,\n            ],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0]).to.eql(['n1']);\n            expect(body[0].result[0].response[1]).to.be.an('array');\n            expect(body[0].result[0].response[2][0]).to.eql(\n              'Cached execution: 0',\n            );\n            expect(body[0].result[0].response[2][1]).to.have.string(\n              'Query internal execution time:',\n            );\n          },\n        },\n        {\n          name: 'Should remove graph',\n          data: {\n            commands: [`del ${constants.TEST_GRAPH_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_GRAPH_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('RediSearch v2', () => {\n      describe('Hash', () => {\n        requirements('rte.modules.search', 'rte.modules.search.version>=20000');\n        [\n          {\n            name: 'Should create index',\n            data: {\n              commands: [\n                `ft.create ${constants.TEST_SEARCH_HASH_INDEX_1} ON HASH\n              PREFIX 1 ${constants.TEST_SEARCH_HASH_KEY_PREFIX_1} NOOFFSETS SCHEMA title TEXT WEIGHT 5.0`,\n              ],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].result).to.eql([\n                {\n                  status: 'success',\n                  response: 'OK',\n                },\n              ]);\n            },\n            before: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n            after: async () => {\n              expect(await rte.client.call(`ft._list`)).to.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n          },\n          {\n            name: 'Should return the list of all existing indexes.',\n            data: {\n              commands: [`ft._list`],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].result).to.eql([\n                {\n                  status: 'success',\n                  response: [constants.TEST_SEARCH_HASH_INDEX_1],\n                },\n              ]);\n            },\n          },\n          {\n            name: 'Should return index info',\n            data: {\n              commands: [`ft.info ${constants.TEST_SEARCH_HASH_INDEX_1}`],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              const response = body[0].result[0].response;\n\n              expect(body[0].result.length).to.eql(1);\n              expect(body[0].result[0].status).to.eql('success');\n              expect(response[0]).to.eql('index_name');\n              expect(response[1]).to.eql(constants.TEST_SEARCH_HASH_INDEX_1);\n              expect(response[2]).to.eql('index_options');\n              expect(response[3]).to.eql(['NOOFFSETS']);\n              expect(response[4]).to.eql('index_definition');\n              expect(_.take(response[5], 4)).to.eql([\n                'key_type',\n                'HASH',\n                'prefixes',\n                [constants.TEST_SEARCH_HASH_KEY_PREFIX_1],\n              ]);\n            },\n          },\n          {\n            name: 'Should find documents',\n            data: {\n              commands: [\n                `ft.search ${constants.TEST_SEARCH_HASH_INDEX_1} \"hello world\"`,\n              ],\n            },\n            responseSchema,\n            before: async () => {\n              for (let i = 0; i < 10; i++) {\n                await rte.client.hset(\n                  `${constants.TEST_SEARCH_HASH_KEY_PREFIX_1}${i}`,\n                  'title',\n                  `hello world ${i}`,\n                );\n              }\n            },\n            checkFn: ({ body }) => {\n              const response: any[] = [10];\n\n              for (let i = 0; i < 10; i++) {\n                response.push(\n                  `${constants.TEST_SEARCH_HASH_KEY_PREFIX_1}${i}`,\n                  ['title', `hello world ${i}`],\n                );\n              }\n\n              expect(body[0].result).to.eql([\n                {\n                  status: 'success',\n                  response,\n                },\n              ]);\n            },\n          },\n          {\n            name: 'Should aggregate documents by uniq @title',\n            data: {\n              commands: [\n                `ft.aggregate ${constants.TEST_SEARCH_HASH_INDEX_1} * GROUPBY 1 @title`,\n              ],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              const response = body[0].result[0].response;\n\n              expect(body[0].result.length).to.eql(1);\n              expect(body[0].result[0].status).to.eql('success');\n              expect(response[0]).to.eql(10);\n              expect(response).to.deep.include(['title', 'hello world 1']);\n            },\n          },\n          {\n            name: 'Should remove index',\n            data: {\n              commands: [\n                `ft.dropindex ${constants.TEST_SEARCH_HASH_INDEX_1} DD`,\n              ],\n            },\n            responseSchema,\n            after: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_HASH_INDEX_1,\n              );\n            },\n          },\n        ].map(mainCheckFn);\n      });\n      describe('JSON', () => {\n        requirements(\n          'rte.modules.search',\n          'rte.modules.rejson',\n          'rte.modules.search.version>=20200',\n          'rte.modules.rejson>=20000',\n        );\n        [\n          {\n            name: 'Should create index',\n            data: {\n              commands: [\n                `ft.create ${constants.TEST_SEARCH_JSON_INDEX_1} ON JSON\n              PREFIX 1 ${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}\n              NOOFFSETS SCHEMA $.user.name AS name TEXT`,\n              ],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body[0].result).to.eql([\n                {\n                  status: 'success',\n                  response: 'OK',\n                },\n              ]);\n            },\n            before: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n            },\n            after: async () => {\n              expect(await rte.client.call(`ft._list`)).to.include(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n            },\n          },\n          {\n            name: 'Should return index info',\n            data: {\n              commands: [`ft.info ${constants.TEST_SEARCH_JSON_INDEX_1}`],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              const response = body[0].result[0].response;\n\n              expect(body[0].result.length).to.eql(1);\n              expect(body[0].result[0].status).to.eql('success');\n              expect(response[0]).to.eql('index_name');\n              expect(response[1]).to.eql(constants.TEST_SEARCH_JSON_INDEX_1);\n              expect(response[2]).to.eql('index_options');\n              expect(response[3]).to.eql(['NOOFFSETS']);\n              expect(response[4]).to.eql('index_definition');\n              expect(_.take(response[5], 4)).to.eql([\n                'key_type',\n                'JSON',\n                'prefixes',\n                [constants.TEST_SEARCH_JSON_KEY_PREFIX_1],\n              ]);\n            },\n          },\n          {\n            name: 'Should find documents',\n            data: {\n              commands: [\n                `ft.search ${constants.TEST_SEARCH_JSON_INDEX_1} \"@name:(John)\"`,\n              ],\n            },\n            responseSchema,\n            before: async () => {\n              for (let i = 0; i < 10; i++) {\n                await rte.client.call('json.set', [\n                  `${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}${i}`,\n                  '$',\n                  `{\"user\":{\"name\":\"John Smith${i}\"}}`,\n                ]);\n              }\n            },\n            checkFn: ({ body }) => {\n              const response: any[] = [10];\n\n              for (let i = 0; i < 10; i++) {\n                response.push(\n                  `${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}${i}`,\n                  ['$', `{\\\\\"user\\\\\":{\\\\\"name\\\\\":\\\\\"John Smith${i}\\\\\"}}`],\n                );\n              }\n\n              expect(body[0].result).to.eql([\n                {\n                  status: 'success',\n                  response,\n                },\n              ]);\n            },\n          },\n          {\n            name: 'Should aggregate documents by uniq @name',\n            data: {\n              commands: [\n                `ft.aggregate ${constants.TEST_SEARCH_JSON_INDEX_1} * GROUPBY 1 @name`,\n              ],\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              const response = body[0].result[0].response;\n\n              expect(body[0].result.length).to.eql(1);\n              expect(body[0].result[0].status).to.eql('success');\n              expect(response[0]).to.eql(10);\n              expect(response).to.deep.include(['name', 'John Smith0']);\n            },\n          },\n          {\n            name: 'Should remove index',\n            data: {\n              commands: [\n                `ft.dropindex ${constants.TEST_SEARCH_JSON_INDEX_1} DD`,\n              ],\n            },\n            responseSchema,\n            after: async () => {\n              expect(await rte.client.call('ft._list')).to.not.include(\n                constants.TEST_SEARCH_JSON_INDEX_1,\n              );\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n    describe('Stream', () => {\n      requirements('rte.version>=5.0');\n      [\n        {\n          name: 'Should create stream',\n          data: {\n            commands: [\n              `xadd ${constants.TEST_STREAM_KEY_1} * ${constants.TEST_STREAM_DATA_1} ${constants.TEST_STREAM_DATA_2}`,\n            ],\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_STREAM_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STREAM_KEY_1)).to.eql(\n              1,\n            );\n          },\n        },\n        {\n          name: 'Should get stream',\n          data: {\n            commands: [`xrange ${constants.TEST_STREAM_KEY_1} - +`],\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0][0]).to.have.string('-');\n            expect(body[0].result[0].response[0][1]).to.eql([\n              constants.TEST_STREAM_DATA_1,\n              constants.TEST_STREAM_DATA_2,\n            ]);\n          },\n        },\n        {\n          name: 'Should remove stream',\n          data: {\n            commands: [`del ${constants.TEST_STREAM_KEY_1}`],\n          },\n          responseSchema,\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_STREAM_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Bad commands', () => {\n      [\n        {\n          name: 'Should return error if invalid command sent',\n          data: {\n            commands: [\n              `setx ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`,\n            ],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].status).to.eql('fail');\n            expect(body[0].result[0].response).to.include(\n              'ERR unknown command',\n            );\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (monitor)',\n          data: {\n            commands: [`monitor`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (subscribe)',\n          data: {\n            commands: [`subscribe`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (psubscribe)',\n          data: {\n            commands: [`psubscribe`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (sync)',\n          data: {\n            commands: [`sync`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (psync)',\n          data: {\n            commands: [`psync`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (script debug)',\n          data: {\n            commands: [`script debug`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run unsupported command (hello 3)',\n          data: {\n            commands: [`hello 3`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n        {\n          name: 'Should return error if try to run blocking command',\n          data: {\n            commands: [`blpop key`],\n          },\n          checkFn: ({ body }) => {\n            expect(body[0].executionTime).to.eql(undefined);\n          },\n        },\n      ].map((testCase) =>\n        mainCheckFn({\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].result[0].status).to.eql('fail');\n            expect(body[0].result[0].response).to.include(\n              'command is not supported by the Redis Insight Workbench',\n            );\n          },\n          ...testCase,\n        }),\n      );\n    });\n    describe('History items limit', () => {\n      it('Number of history items should be less then 30', async () => {\n        const repo = await localDb.getRepository(\n          localDb.repositories.COMMAND_EXECUTION,\n        );\n        await localDb.generateNCommandExecutions(\n          {\n            databaseId: constants.TEST_INSTANCE_ID,\n            createdAt: new Date(Date.now() - 1000),\n          },\n          30,\n          true,\n        );\n\n        for (let i = 0; i < 40; i++) {\n          await validateApiCall({\n            endpoint,\n            data: {\n              commands: [`get ${constants.TEST_STRING_KEY_1}`],\n            },\n            responseSchema,\n            checkFn: async ({ body }) => {\n              expect(body[0].result.length).to.eql(1);\n\n              const count = await repo.countBy({\n                databaseId: constants.TEST_INSTANCE_ID,\n              });\n              expect(count).to.lte(30);\n\n              // check that the last execution command was not deleted\n              // await repo.findOneOrFail({ id: body.id }); // sometimes localDb is not in sync. investigate\n            },\n          });\n        }\n      });\n    });\n  });\n  // Skip 'Standalone + Sentinel' and 'Cluster' tests because tested functionalities were removed\n  xdescribe('Standalone + Sentinel', () => {\n    requirements('!rte.type=CLUSTER');\n\n    describe('Incorrect requests for redis client type', () => {\n      [\n        {\n          name: 'Should return error if try to execute command for role for standalone database',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n            message: 'Wrong database type.',\n          },\n        },\n        {\n          name: 'Should return error if try to execute command for particular node for standalone database',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n            message: 'Wrong database type.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n  xdescribe('Cluster', () => {\n    // requirements('rte.type=CLUSTER');\n    requirements('!rte.re');\n\n    let database;\n    let nodes;\n\n    before(async () => {\n      database = await (\n        await localDb.getRepository(localDb.repositories.DATABASE)\n      ).findOneBy({\n        id: constants.TEST_INSTANCE_ID,\n      });\n      nodes = JSON.parse(database.nodes);\n    });\n\n    describe('Commands using role', () => {\n      [\n        {\n          name: 'Get command with role=ALL',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n            role: 'ALL',\n          },\n          responseSchema,\n          before: async () => {\n            await rte.client.set(\n              constants.TEST_STRING_KEY_1,\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n          checkFn: async ({ body }) => {\n            const result = body[0].result;\n\n            expect(result.length).to.eql(nodes.length);\n            expect(body[0].role).to.eql('ALL');\n            expect(body[0].nodeOptions).to.eql(undefined);\n\n            const resultSummary = {\n              moved: 0,\n              succeed: 0,\n            };\n\n            result.forEach((nodeResult) => {\n              const node = nodes.find((node) => {\n                return (\n                  nodeResult.node.host === node.host &&\n                  nodeResult.node.port === node.port\n                );\n              });\n\n              if (!node) {\n                fail(\n                  `Unexpected node detected: ${JSON.stringify(nodeResult.node)}`,\n                );\n              }\n\n              switch (nodeResult.status) {\n                case 'fail':\n                  expect(nodeResult.response).to.have.string('MOVED');\n                  resultSummary.moved++;\n                  break;\n                case 'success':\n                  expect(nodeResult.response).to.eql(\n                    constants.TEST_STRING_VALUE_1,\n                  );\n                  resultSummary.succeed++;\n                  break;\n                default:\n                  fail(`Unexpected node result status: ${nodeResult.status}`);\n              }\n            });\n\n            expect(resultSummary.moved).to.gt(0);\n            expect(resultSummary.succeed).to.gt(0);\n            expect(resultSummary.moved + resultSummary.succeed).to.eq(\n              nodes.length,\n            );\n          },\n        },\n        {\n          name: 'Get command with role=MASTER',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n            role: 'MASTER',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            const result = body[0].result;\n\n            expect(result.length).to.lte(nodes.length);\n\n            const resultSummary = {\n              moved: 0,\n              succeed: 0,\n            };\n\n            result.forEach((nodeResult) => {\n              const node = nodes.find((node) => {\n                return (\n                  nodeResult.node.host === node.host &&\n                  nodeResult.node.port === node.port\n                );\n              });\n\n              if (!node) {\n                fail(\n                  `Unexpected node detected: ${JSON.stringify(nodeResult.node)}`,\n                );\n              }\n\n              switch (nodeResult.status) {\n                case 'fail':\n                  expect(nodeResult.response).to.have.string('MOVED');\n                  resultSummary.moved++;\n                  break;\n                case 'success':\n                  expect(nodeResult.response).to.eql(\n                    constants.TEST_STRING_VALUE_1,\n                  );\n                  resultSummary.succeed++;\n                  break;\n                default:\n                  fail(`Unexpected node result status: ${nodeResult.status}`);\n              }\n            });\n\n            expect(resultSummary.moved).to.gt(0);\n            expect(resultSummary.succeed).to.gt(0);\n            expect(resultSummary.moved + resultSummary.succeed).to.lte(\n              nodes.length,\n            );\n          },\n        },\n        {\n          name: 'Set command with role=SLAVE should return all failed responses',\n          data: {\n            commands: [\n              `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_KEY_1}`,\n            ],\n            role: 'SLAVE',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            const result = body[0].result;\n\n            expect(result.length).to.lte(nodes.length);\n\n            const resultSummary = {\n              moved: 0,\n              succeed: 0,\n            };\n\n            result.forEach((nodeResult) => {\n              const node = nodes.find((node) => {\n                return (\n                  nodeResult.node.host === node.host &&\n                  nodeResult.node.port === node.port\n                );\n              });\n\n              if (!node) {\n                fail(\n                  `Unexpected node detected: ${JSON.stringify(nodeResult.node)}`,\n                );\n              }\n\n              switch (nodeResult.status) {\n                case 'fail':\n                  expect(nodeResult.response).to.have.string('MOVED');\n                  resultSummary.moved++;\n                  break;\n                case 'success':\n                  expect(nodeResult.response).to.eql(\n                    constants.TEST_STRING_VALUE_1,\n                  );\n                  resultSummary.succeed++;\n                  break;\n                default:\n                  fail(`Unexpected node result status: ${nodeResult.status}`);\n              }\n            });\n\n            expect(resultSummary.moved).to.gte(0);\n            expect(resultSummary.succeed).to.eq(0);\n            expect(resultSummary.moved + resultSummary.succeed).to.lte(\n              nodes.length,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Commands using nodeOptions', () => {\n      [\n        {\n          name: 'Incorrect node should return an error',\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n            nodeOptions: {\n              host: 'unreachable',\n              port: 6380,\n              enableRedirection: true,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            message: 'Node unreachable:6380 not exist in OSS Cluster.',\n            error: 'Bad Request',\n          },\n          before: async () => {\n            await rte.client.set(\n              constants.TEST_STRING_KEY_1,\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n        },\n      ].map(mainCheckFn);\n\n      it('Should auto redirect and never fail', async () => {\n        await validateApiCall({\n          endpoint,\n          data: {\n            commands: [`get ${constants.TEST_STRING_KEY_1}`],\n            nodeOptions: {\n              ...nodes[0],\n              enableRedirection: true,\n            },\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].result.length).to.eql(1);\n            expect(body[0].role).to.eql(null);\n            expect(body[0].nodeOptions).to.eql({\n              ...nodes[0],\n              enableRedirection: true,\n            });\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response).to.eql(\n              constants.TEST_STRING_VALUE_1,\n            );\n          },\n        });\n      });\n    });\n  });\n  describe('Execution time', () => {\n    describe('pipeline', () => {\n      [\n        {\n          name: 'Pipeline with two commands',\n          data: {\n            commands: [`lrange ${constants.TEST_LIST_KEY_1} 0 100`, 'info'],\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].executionTime).to.be.a('number');\n            expect(body[1].executionTime).to.be.a('number');\n\n            const entity_1: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            const entity_2: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[1].id,\n            });\n\n            expect(entity_1.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(entity_2.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(body[0].executionTime).to.eql(entity_1.executionTime);\n            expect(body[1].executionTime).to.eql(entity_2.executionTime);\n            expect(body[0].db).to.eql(entity_1.db);\n            expect(body[1].db).to.eql(entity_2.db);\n            expect(localDb.encryptData(body[0].command)).to.eql(\n              entity_1.command,\n            );\n            expect(localDb.encryptData(body[1].command)).to.eql(\n              entity_2.command,\n            );\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[1].result[0].status).to.eql('success');\n          },\n        },\n        {\n          name: 'Pipeline with one wrong command',\n          data: {\n            commands: [`lrange ${constants.TEST_LIST_KEY_1} 0 100`, 'dasd'],\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            expect(body[0].executionTime).to.be.a('number');\n            expect(body[1].executionTime).to.be.a('number');\n\n            const entity_1: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            const entity_2: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[1].id,\n            });\n\n            expect(entity_1.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(entity_2.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(body[0].executionTime).to.eql(entity_1.executionTime);\n            expect(body[1].executionTime).to.eql(entity_2.executionTime);\n            expect(body[0].db).to.eql(entity_1.db);\n            expect(body[1].db).to.eql(entity_2.db);\n            expect(localDb.encryptData(body[0].command)).to.eql(\n              entity_1.command,\n            );\n            expect(localDb.encryptData(body[1].command)).to.eql(\n              entity_2.command,\n            );\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[1].result[0].status).to.eql('fail');\n          },\n        },\n      ].map(mainCheckFn);\n    });\n    describe('Group mode', () => {\n      [\n        {\n          name: 'Group mode with two commands',\n          data: {\n            commands: [\n              `lpush ${constants.TEST_LIST_KEY_1} ${constants.TEST_LIST_ELEMENT_1} ${constants.TEST_LIST_ELEMENT_2}`,\n              `lrange ${constants.TEST_LIST_KEY_1} 0 100`,\n            ],\n            resultsMode: 'GROUP_MODE',\n          },\n          responseSchema,\n          before: async () => {\n            expect(await rte.client.exists(constants.TEST_LIST_KEY_1)).to.eql(\n              0,\n            );\n          },\n          after: async () => {\n            expect(\n              await rte.client.lrange(constants.TEST_LIST_KEY_1, 0, 100),\n            ).to.eql([\n              constants.TEST_LIST_ELEMENT_2,\n              constants.TEST_LIST_ELEMENT_1,\n            ]);\n          },\n          checkFn: async ({ body }) => {\n            expect(body[0].executionTime).to.be.a('number');\n\n            const entity: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            expect(entity.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(body[0].executionTime).to.eql(entity.executionTime);\n            expect(body[0].db).to.eql(entity.db);\n            // group mode should always return success\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0].status).to.eql('success');\n            expect(body[0].result[0].response[1]).to.eql({\n              status: 'success',\n              response: [\n                constants.TEST_LIST_ELEMENT_2,\n                constants.TEST_LIST_ELEMENT_1,\n              ],\n              command: `lrange ${constants.TEST_LIST_KEY_1} 0 100`,\n            });\n            expect(body[0].summary).to.eql({ total: 2, success: 2, fail: 0 });\n            expect(localDb.encryptData(body[0].command)).to.eql(entity.command);\n            expect(localDb.encryptData(JSON.stringify(body[0].result))).to.eql(\n              entity.result,\n            );\n          },\n        },\n        {\n          name: 'Group mode with one wrong command',\n          data: {\n            commands: [`lrange ${constants.TEST_LIST_KEY_1} 0 100`, 'dasd'],\n            resultsMode: 'GROUP_MODE',\n          },\n          responseSchema,\n          checkFn: async ({ body }) => {\n            const entity: any = await (\n              await localDb.getRepository(\n                localDb.repositories.COMMAND_EXECUTION,\n              )\n            ).findOneBy({\n              id: body[0].id,\n            });\n\n            expect(body[0].executionTime).to.be.a('number');\n            expect(entity.encryption).to.eql(\n              constants.TEST_ENCRYPTION_STRATEGY,\n            );\n            expect(body[0].executionTime).to.eql(entity.executionTime);\n            expect(localDb.encryptData(body[0].command)).to.eql(entity.command);\n            expect(body[0].summary).to.eql({ total: 2, success: 1, fail: 1 });\n            // group mode should always return success\n            expect(body[0].result[0].status).to.eql('success');\n            expect(body[0].result[0].response[0].status).to.eql('success');\n            expect(body[0].result[0].response[1].status).to.eql('fail');\n            expect(localDb.encryptData(body[0].command)).to.eql(entity.command);\n            expect(body[0].result[0].response[0].response).to.eql([\n              constants.TEST_LIST_ELEMENT_2,\n              constants.TEST_LIST_ELEMENT_1,\n            ]);\n            expect(body[0].result[0].response[1].response).to.include(\n              'ERR unknown command',\n            );\n            expect(localDb.encryptData(JSON.stringify(body[0].result))).to.eql(\n              entity.result,\n            );\n            expect(body[0].db).to.be.a('number');\n            expect(body[0].db).to.eql(entity.db);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ws/bulk-actions/bulk-actions-create.test.ts",
    "content": "import { describe, it, deps, expect } from '../../deps';\nimport { Socket } from 'socket.io-client';\nconst { getSocket, constants, rte } = deps;\n\nconst getClient = async (): Promise<Socket> => {\n  return getSocket('bulk-actions');\n};\n\nconst createDto = {\n  databaseId: constants.TEST_INSTANCE_ID,\n  id: '1',\n  type: 'delete',\n  filter: {\n    match: `${constants.TEST_RUN_ID}*`,\n  },\n};\n\nlet client;\n\ndescribe('bulk-actions', function () {\n  this.timeout(20000);\n  beforeEach(async () => {\n    client = await getClient();\n    await rte.data.generateKeys(true);\n  });\n\n  afterEach(async () => {\n    client.close();\n  });\n\n  describe('Connection edge cases', () => {\n    it('should not crash on 100 the same concurrent bulk-actions create events', async () => {\n      let errors = 0;\n      let created = 0;\n      await Promise.all(\n        new Array(10).fill(1).map(\n          () =>\n            new Promise((res, rej) => {\n              client.emit('create', createDto, (ack) => {\n                if (ack.status === 'error') {\n                  errors += 1;\n                } else {\n                  created += 1;\n                  expect(ack.id).to.eq(createDto.id);\n                  expect(ack.type).to.eq(createDto.type);\n                  expect(['running', 'ready'].includes(ack.status)).to.eq(true);\n                  expect(ack.filter.match).to.eq(createDto.filter.match);\n                  expect(ack.filter.type).to.eq(null);\n                  expect(ack.progress.total).to.gt(0);\n                  expect(ack.progress.scanned).to.gte(0);\n                  expect(ack.summary.processed).to.gte(0);\n                  expect(ack.summary.succeed).to.gte(0);\n                  expect(ack.summary.failed).to.eq(0);\n                  expect(ack.summary.errors).to.deep.eq([]);\n                }\n                res(ack);\n              });\n              client.on('exception', rej);\n            }),\n        ),\n      );\n\n      expect(errors).to.eq(9);\n      expect(created).to.eq(1);\n    });\n  });\n  describe('abort', () => {\n    it('should abort just started bulk action', (done) => {\n      client.emit('create', createDto, (ack) => {\n        if (ack.status === 'error') {\n          fail(ack.message);\n        }\n        client.emit('abort', { id: createDto.id }, (ack) => {\n          expect(ack.id).to.eq(createDto.id);\n          expect(ack.type).to.eq(createDto.type);\n          expect(['aborted', 'completed'].includes(ack.status)).to.eq(true);\n          expect(ack.filter.match).to.eq(createDto.filter.match);\n          expect(ack.filter.type).to.eq(null);\n          expect(ack.progress.total).to.gt(0);\n          expect(ack.progress.scanned).to.gte(0);\n          expect(ack.summary.processed).to.gte(0);\n          expect(ack.summary.succeed).to.gte(0);\n          expect(ack.summary.failed).to.eq(0);\n          expect(ack.summary.errors).to.deep.eq([]);\n          done();\n        });\n      });\n    });\n  });\n  describe('get', () => {\n    it('should get just started bulk action', (done) => {\n      client.emit('create', createDto, (ack) => {\n        if (ack.status === 'error') {\n          fail(ack.message);\n        }\n        client.emit('get', { id: createDto.id }, (ack) => {\n          expect(ack.id).to.eq(createDto.id);\n          expect(ack.type).to.eq(createDto.type);\n          expect(ack.filter.match).to.eq(createDto.filter.match);\n          expect(ack.filter.type).to.eq(null);\n          expect(ack.progress.total).to.gt(0);\n          expect(ack.progress.scanned).to.gte(0);\n          expect(ack.summary.processed).to.gte(0);\n          expect(ack.summary.succeed).to.gte(0);\n          expect(ack.summary.failed).to.eq(0);\n          expect(ack.summary.errors).to.deep.eq([]);\n          done();\n        });\n      });\n    });\n  });\n  describe('overview', () => {\n    it('should receive overview', async () => {\n      client.emit('create', createDto, (ack) => {\n        if (ack.status === 'error') {\n          fail(ack.message);\n        }\n      });\n\n      const overview: any = await new Promise((res, rej) => {\n        client.on('overview', (overview) => {\n          res(overview);\n        });\n\n        setTimeout(() => {\n          rej(new Error('Timedout'));\n        }, 3000);\n      });\n\n      expect(overview.id).to.eq(createDto.id);\n      expect(overview.type).to.eq(createDto.type);\n      expect(overview.filter.match).to.eq(createDto.filter.match);\n      expect(overview.filter.type).to.eq(null);\n      expect(overview.progress.total).to.gt(0);\n      expect(overview.progress.scanned).to.gte(0);\n      expect(overview.summary.processed).to.gte(0);\n      expect(overview.summary.succeed).to.gte(0);\n      expect(overview.summary.failed).to.eq(0);\n      expect(overview.summary.errors).to.deep.eq([]);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ws/monitor/monitor.test.ts",
    "content": "import {\n  describe,\n  it,\n  before,\n  deps,\n  expect,\n  requirements,\n  _,\n} from '../../deps';\nimport { Socket } from 'socket.io-client';\nconst { getSocket, constants, rte } = deps;\n\nconst getMonitorClient = async (instanceId): Promise<Socket> => {\n  return getSocket('monitor', {\n    query: { instanceId },\n  });\n};\n\ndescribe('monitor', function () {\n  this.timeout(4000);\n\n  describe('Connection edge cases', () => {\n    it('should not crash on 100 concurrent monitor connections to the same db', async () => {\n      const client = await getMonitorClient(constants.TEST_INSTANCE_ID);\n      await Promise.all(\n        new Array(10).fill(1).map(\n          () =>\n            new Promise((res, rej) => {\n              client.emit(\n                'monitor',\n                { logFileId: constants.getRandomString() },\n                (ack) => {\n                  expect(ack).to.eql({ status: 'ok' });\n                  res(ack);\n                },\n              );\n              client.on('exception', rej);\n            }),\n        ),\n      );\n    });\n  });\n\n  describe('Client creation', () => {\n    it('Should successfully create a client', async () => {\n      const client = await getMonitorClient(constants.TEST_INSTANCE_ID);\n      expect(client instanceof Socket).to.eql(true);\n      await client.close();\n    });\n    it('Should successfully create a client even when incorrect instanceId provided', async () => {\n      const client = await getMonitorClient(\n        constants.TEST_NOT_EXISTED_INSTANCE_ID,\n      );\n      expect(client instanceof Socket).to.eql(true);\n    });\n  });\n\n  describe('Emit monitor', () => {\n    it('Should successfully emit monitor event', async () => {\n      const client = await getMonitorClient(constants.TEST_INSTANCE_ID);\n      await new Promise((resolve) => {\n        client.emit('monitor', (ack) => {\n          expect(ack).to.eql({ status: 'ok' });\n          resolve(ack);\n        });\n      });\n    });\n    it('Should return Not Found acknowledge on monitor event with incorrect instanceId', async () => {\n      try {\n        const client = await getMonitorClient(\n          constants.TEST_NOT_EXISTED_INSTANCE_ID,\n        );\n        await new Promise((resolve, reject) => {\n          client.emit('monitor', () => {\n            reject('Should fail');\n          });\n          client.on('exception', reject);\n        });\n      } catch (e) {\n        expect(e.status).to.eql(404);\n        expect(e.message).to.eql('Invalid database instance id.');\n      }\n    });\n  });\n\n  describe('On monitorData', () => {\n    it('Should receive particular log', async () => {\n      const client = await getMonitorClient(constants.TEST_INSTANCE_ID);\n      await new Promise((resolve) => {\n        client.emit('monitor', async (ack) => {\n          expect(ack).to.eql({ status: 'ok' });\n\n          client.on('monitorData', (data) => {\n            expect(data).to.be.an('array');\n            data.forEach((log) => {\n              if (_.isEqual(log.args, ['scan', '2'])) {\n                resolve(log);\n              }\n            });\n          });\n\n          await rte.data.executeCommand('scan', '2');\n        });\n      });\n    });\n    it('Should receive bunch of logs for many clients', async () => {\n      const clients = await Promise.all(\n        new Array(3)\n          .fill(1)\n          .map(() => getMonitorClient(constants.TEST_INSTANCE_ID)),\n      );\n\n      const counts = {};\n\n      await Promise.all(\n        clients.map(\n          (client) =>\n            new Promise((resolve, reject) => {\n              counts[client.id] = {\n                numberOfTargetLogs: 0,\n              };\n\n              client.on('exception', reject);\n              client.on('monitorData', (data) => {\n                expect(data).to.be.an('array');\n                data.forEach((log) => {\n                  if (_.isEqual(log.args, ['scan', '2'])) {\n                    counts[client.id].numberOfTargetLogs += 1;\n                  }\n                });\n              });\n              client.emit('monitor', (ack) => {\n                expect(ack).to.eql({ status: 'ok' });\n                resolve(ack);\n              });\n            }),\n        ),\n      );\n\n      await Promise.all(\n        new Array(100).fill(1).map(() => rte.data.executeCommand('scan', '2')),\n      );\n\n      // Wait for a while\n      await new Promise((resolve) => setTimeout(resolve, 2000));\n\n      _.map(counts).forEach((count) => {\n        // @ts-ignore\n        expect(count.numberOfTargetLogs).to.gte(100);\n      });\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    it('should connect if monitor permitted', async () => {\n      const client = await getMonitorClient(constants.TEST_INSTANCE_ACL_ID);\n\n      expect(client instanceof Socket).to.eql(true);\n\n      await new Promise((resolve) => {\n        client.emit('monitor', (ack) => {\n          expect(ack).to.eql({ status: 'ok' });\n          resolve(ack);\n        });\n      });\n      client.close();\n    });\n\n    it('should throw an error on connect without permissions', async () => {\n      await rte.data.setAclUserRules('~* +@all -monitor');\n\n      const client = await getMonitorClient(constants.TEST_INSTANCE_ACL_ID);\n\n      expect(client instanceof Socket).to.eql(true);\n\n      try {\n        await new Promise((resolve, reject) => {\n          client.emit('monitor', (ack) => {\n            expect(ack).to.eql({ status: 'ok' });\n            reject(new Error('should throw NOPERM Error'));\n          });\n          client.on('exception', reject);\n        });\n      } catch (e) {\n        expect(e.status).to.eql(403);\n        expect(e.message).to.have.string('NOPERM');\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/ws/pub-sub/pub-sub.test.ts",
    "content": "import {\n  describe,\n  it,\n  before,\n  deps,\n  expect,\n  requirements,\n  _,\n  sleep,\n} from '../../deps';\nimport { Socket } from 'socket.io-client';\nconst { getSocket, constants, rte } = deps;\n\nconst getClient = async (instanceId): Promise<Socket> => {\n  return getSocket('pub-sub', {\n    query: { instanceId },\n  });\n};\n\nconst subscription = {\n  channel: 'channel-a',\n  type: 's',\n};\n\nconst subscriptionB = {\n  channel: 'channel-b',\n  type: 's',\n};\n\nconst pSubscription = {\n  channel: '*',\n  type: 'p',\n};\n\nlet client;\n\ndescribe('pub-sub', function () {\n  this.timeout(10000);\n  beforeEach(async () => {\n    client = await getClient(constants.TEST_INSTANCE_ID);\n  });\n\n  afterEach(async () => {\n    client.close();\n  });\n\n  describe('Connection edge cases', () => {\n    it('should not crash on 100 concurrent pub-sub connections to the same db', async () => {\n      await Promise.all(\n        new Array(10).fill(1).map(\n          () =>\n            new Promise((res, rej) => {\n              client.emit(\n                'subscribe',\n                { subscriptions: [pSubscription, subscription] },\n                (ack) => {\n                  expect(ack).to.eql({ status: 'ok' });\n                  res(ack);\n                },\n              );\n              client.on('exception', rej);\n            }),\n        ),\n      );\n    });\n  });\n\n  describe('Client creation', () => {\n    it('Should successfully create a client', async () => {\n      expect(client instanceof Socket).to.eql(true);\n    });\n    it('Should successfully create a client even when incorrect instanceId provided', async () => {\n      const client = await getClient(constants.TEST_NOT_EXISTED_INSTANCE_ID);\n      expect(client instanceof Socket).to.eql(true);\n      await client.close();\n    });\n  });\n\n  describe('subscribe', () => {\n    it('Should successfully subscribe', async () => {\n      await new Promise((resolve) => {\n        client.emit('subscribe', { subscriptions: [pSubscription] }, (ack) => {\n          expect(ack).to.eql({ status: 'ok' });\n          resolve(ack);\n        });\n      });\n    });\n    it('Should return Not Found acknowledge when incorrect instanceId', async () => {\n      const client = await getClient(constants.TEST_NOT_EXISTED_INSTANCE_ID);\n      await new Promise((resolve, reject) => {\n        client.emit('subscribe', { subscriptions: [pSubscription] }, (ack) => {\n          try {\n            expect(ack.status).to.eql('error');\n            expect(ack.error.status).to.eql(404);\n            expect(ack.error.message).to.eql('Invalid database instance id.');\n            expect(ack.error.name).to.eql('NotFoundException');\n            resolve(null);\n          } catch (e) {\n            reject(e);\n          }\n        });\n      });\n    });\n  });\n\n  describe('on message', () => {\n    it('Should receive message on particular channel only', async () => {\n      await new Promise((resolve, reject) => {\n        client.emit(\n          'subscribe',\n          { subscriptions: [subscription, subscriptionB] },\n          async (ack) => {\n            expect(ack).to.eql({ status: 'ok' });\n\n            client.on('s:channel-a', (data) => {\n              expect(data.count).to.be.eql(1);\n              expect(data.messages.length).to.be.eql(1);\n              const [message] = data.messages;\n              expect(message.channel).to.eq('channel-a');\n              expect(message.message).to.eq('message-a');\n              expect(message.time).to.be.a('number');\n              resolve(null);\n            });\n\n            client.on('s:channel-b', () => {\n              reject(\n                new Error('Should not receive message-a in this listener-b'),\n              );\n            });\n\n            await rte.data.sendCommand('publish', ['channel-c', 'message-c']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n          },\n        );\n      });\n    });\n    describe('on message [unstable test]', () => {\n      requirements('!rte.tls'); // tls works slower. skip test to not add additional wait time. todo: rewrite test\n      requirements('rte.type<>SENTINEL'); // sentinel too\n\n      it('Should receive bunch of logs for many subscriptions', async () => {\n        const messages = {\n          'channel-a': [],\n          'channel-b': [],\n          '*': [],\n        };\n\n        client.on('s:channel-a', (data) =>\n          messages['channel-a'].push(...data.messages),\n        );\n        client.on('s:channel-b', (data) =>\n          messages['channel-b'].push(...data.messages),\n        );\n        client.on('p:*', (data) => messages['*'].push(...data.messages));\n\n        await new Promise((resolve) => {\n          client.emit(\n            'subscribe',\n            { subscriptions: [subscription, subscriptionB, pSubscription] },\n            (ack) => {\n              expect(ack).to.eql({ status: 'ok' });\n\n              client.on('s:channel-b', resolve);\n\n              rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n              rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n              rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n              rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n              rte.data.sendCommand('publish', ['channel-b', 'message-b']);\n            },\n          );\n        });\n\n        await sleep(3000);\n\n        expect(messages['channel-a'].length).to.eql(4);\n        messages['channel-a'].forEach((message) => {\n          expect(message.channel).to.eql('channel-a');\n        });\n        expect(messages['channel-b'].length).to.eql(1);\n        expect(messages['*'].length).to.eql(5);\n      });\n    });\n  });\n\n  describe('unsubscribe', () => {\n    it('Should still receive messages on subscriptions left', async () => {\n      const messages = {\n        'channel-a': [],\n        'channel-b': [],\n        '*': [],\n      };\n\n      client.on('s:channel-a', (data) =>\n        messages['channel-a'].push(...data.messages),\n      );\n      client.on('s:channel-b', (data) =>\n        messages['channel-b'].push(...data.messages),\n      );\n      client.on('p:*', (data) => messages['*'].push(...data.messages));\n\n      await new Promise((resolve) => {\n        client.emit(\n          'subscribe',\n          { subscriptions: [subscription, subscriptionB, pSubscription] },\n          async (ack) => {\n            expect(ack).to.eql({ status: 'ok' });\n\n            client.on('s:channel-b', resolve);\n\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-b', 'message-b']);\n          },\n        );\n      });\n\n      await new Promise((resolve) => {\n        client.emit(\n          'unsubscribe',\n          { subscriptions: [subscription, pSubscription] },\n          async (ack) => {\n            expect(ack).to.eql({ status: 'ok' });\n\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-b', 'message-b']);\n\n            client.on('s:channel-b', resolve);\n          },\n        );\n      });\n\n      expect(messages['channel-a'].length).to.eql(4);\n      messages['channel-a'].forEach((message) => {\n        expect(message.channel).to.eql('channel-a');\n      });\n      expect(messages['channel-b'].length).to.eql(2);\n      expect(messages['*'].length).to.eql(5);\n    });\n\n    it('Should receive bunch of messages when subscribed only', async () => {\n      const messages = {\n        'channel-a': [],\n        'channel-b': [],\n        '*': [],\n      };\n\n      client.on('s:channel-a', (data) =>\n        messages['channel-a'].push(...data.messages),\n      );\n      client.on('s:channel-b', (data) =>\n        messages['channel-b'].push(...data.messages),\n      );\n      client.on('p:*', (data) => messages['*'].push(...data.messages));\n\n      await new Promise((resolve) => {\n        client.emit(\n          'subscribe',\n          { subscriptions: [subscription, subscriptionB, pSubscription] },\n          async (ack) => {\n            expect(ack).to.eql({ status: 'ok' });\n\n            client.on('s:channel-b', resolve);\n\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-b', 'message-b']);\n          },\n        );\n      });\n\n      await new Promise((resolve) => {\n        client.emit(\n          'unsubscribe',\n          { subscriptions: [subscription, subscriptionB, pSubscription] },\n          async (ack) => {\n            expect(ack).to.eql({ status: 'ok' });\n\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-a', 'message-a']);\n            await rte.data.sendCommand('publish', ['channel-b', 'message-b']);\n\n            resolve(null);\n          },\n        );\n      });\n\n      expect(messages['channel-a'].length).to.eql(4);\n      messages['channel-a'].forEach((message) => {\n        expect(message.channel).to.eql('channel-a');\n      });\n      expect(messages['channel-b'].length).to.eql(1);\n      expect(messages['*'].length).to.eql(5);\n    });\n  });\n\n  describe('ACL', () => {\n    requirements('rte.acl');\n    // todo: investigate cluster behaviour. tmp disabled ACL checks for cluster databases\n    requirements('rte.type<>CLUSTER');\n    before(async () => rte.data.setAclUserRules('~* +@all'));\n\n    it('should throw an error on connect without permissions (subscribe)', async () => {\n      await rte.data.setAclUserRules('~* +@all -subscribe');\n\n      const client = await getClient(constants.TEST_INSTANCE_ACL_ID);\n\n      expect(client instanceof Socket).to.eql(true);\n\n      await new Promise((resolve) => {\n        client.emit('subscribe', { subscriptions: [subscription] }, (ack) => {\n          expect(ack.status).to.eql('error');\n          expect(ack.error.status).to.eql(403);\n          expect(ack.error.message).to.have.string('NOPERM');\n          resolve(null);\n        });\n      });\n    });\n\n    it('should throw an error on connect without permissions (psubscribe)', async () => {\n      await rte.data.setAclUserRules('~* +@all -psubscribe');\n\n      const client = await getClient(constants.TEST_INSTANCE_ACL_ID);\n\n      expect(client instanceof Socket).to.eql(true);\n\n      await new Promise((resolve) => {\n        client.emit('subscribe', { subscriptions: [pSubscription] }, (ack) => {\n          expect(ack.status).to.eql('error');\n          expect(ack.error.status).to.eql(403);\n          expect(ack.error.message).to.have.string('NOPERM');\n          resolve(null);\n        });\n      });\n    });\n\n    it('should throw an error on connect without permissions (unsubscribe)', async () => {\n      await rte.data.setAclUserRules('~* +@all -unsubscribe');\n\n      const client = await getClient(constants.TEST_INSTANCE_ACL_ID);\n\n      expect(client instanceof Socket).to.eql(true);\n\n      await new Promise((resolve) => {\n        client.emit('subscribe', { subscriptions: [subscription] }, (ack) => {\n          expect(ack).to.deep.eql({ status: 'ok' });\n          client.emit(\n            'unsubscribe',\n            { subscriptions: [subscription] },\n            (ack) => {\n              expect(ack.status).to.eql('error');\n              expect(ack.error.status).to.eql(403);\n              expect(ack.error.message).to.have.string('NOPERM');\n              resolve(null);\n            },\n          );\n        });\n      });\n    });\n\n    it('should throw an error on connect without permissions (punsubscribe)', async () => {\n      await rte.data.setAclUserRules('~* +@all -punsubscribe');\n\n      const client = await getClient(constants.TEST_INSTANCE_ACL_ID);\n\n      expect(client instanceof Socket).to.eql(true);\n\n      await new Promise((resolve) => {\n        client.emit('subscribe', { subscriptions: [pSubscription] }, (ack) => {\n          expect(ack).to.deep.eql({ status: 'ok' });\n          client.emit(\n            'unsubscribe',\n            { subscriptions: [pSubscription] },\n            (ack) => {\n              expect(ack.status).to.eql('error');\n              expect(ack.error.status).to.eql(403);\n              expect(ack.error.message).to.have.string('NOPERM');\n              resolve(null);\n            },\n          );\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/z-set/DELETE-databases-id-zSet-members.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).delete(\n    `/${constants.API.DATABASES}/${instanceId}/zSet/members`,\n  );\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  members: Joi.array().items(Joi.any()).required(), // todo: investigate BE validation rules\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  members: [constants.getRandomString()],\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    affected: Joi.number().required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('DELETE /databases/:instanceId/zSet/members', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should remove member from buff',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          members: [constants.TEST_ZSET_MEMBER_BIN_BUF_OBJ_1],\n        },\n        responseBody: {\n          affected: 1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_ZSET_KEY_BIN_BUFFER_1),\n          ).to.eql(0);\n        },\n      },\n      {\n        name: 'Should add member from ascii',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          members: [constants.TEST_ZSET_MEMBER_BIN_ASCII_1],\n        },\n        responseBody: {\n          affected: 1,\n        },\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_ZSET_KEY_BIN_BUFFER_1),\n          ).to.eql(0);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n            ]),\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [constants.getRandomString()],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return BadRequest error if try to modify incorrect data type',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should remove single member',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            members: ['member_1'],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 1,\n          },\n          after: async () => {\n            const members = await rte.client.zrange(\n              constants.TEST_ZSET_KEY_2,\n              0,\n              1000,\n            );\n            expect(members.length).to.eql(99);\n          },\n        },\n        {\n          name: 'Should remove multiple member',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            members: ['member_2', 'member_3', 'member_4', 'member_100'],\n          },\n          responseSchema,\n          responseBody: {\n            affected: 4,\n          },\n          after: async () => {\n            const members = await rte.client.zrange(\n              constants.TEST_ZSET_KEY_2,\n              0,\n              1000,\n            );\n            expect(members.length).to.eql(95);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should remove all members and key',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n            ],\n          },\n          responseBody: {\n            affected: 2,\n          },\n          after: async () => {\n            expect(await rte.client.exists(constants.TEST_ZSET_KEY_1)).to.eql(\n              0,\n            );\n          },\n        },\n        {\n          name: 'Should throw error if no permissions for \"zrem\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zrem'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            members: [constants.getRandomString()],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/z-set/PATCH-databases-id-zSet.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).patch(`/${constants.API.DATABASES}/${instanceId}/zSet`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  member: Joi.object()\n    .keys({\n      name: Joi.string().required(),\n      // todo: allow(true) - is incorrect but will be transformed to number by BE. Investigate/fix it\n      score: Joi.number().required().allow('inf', '-inf').label('.score'),\n    })\n    .messages({\n      'number.base': '{#lavel} must be a string or a number',\n    }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_ZSET_KEY_1,\n  member: {\n    name: constants.TEST_ZSET_MEMBER_1,\n    score: constants.TEST_ZSET_MEMBER_1_SCORE,\n  },\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PATCH /databases/:instanceId/zSet', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should modify member from buff',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          member: {\n            name: constants.TEST_ZSET_MEMBER_BIN_BUF_OBJ_1,\n            score: 3,\n          },\n        },\n        after: async () => {\n          expect(\n            await rte.data.sendCommand(\n              'zrange',\n              [constants.TEST_ZSET_KEY_BIN_BUFFER_1, 0, 10],\n              null,\n            ),\n          ).to.deep.eq([constants.TEST_ZSET_MEMBER_BIN_BUFFER_1]);\n        },\n      },\n      {\n        name: 'Should modify member from ascii',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          member: {\n            name: constants.TEST_ZSET_MEMBER_BIN_ASCII_1,\n            score: 3,\n          },\n        },\n        after: async () => {\n          expect(\n            await rte.data.sendCommand(\n              'zrange',\n              [constants.TEST_ZSET_KEY_BIN_BUFFER_1, 0, 10],\n              null,\n            ),\n          ).to.deep.eq([constants.TEST_ZSET_MEMBER_BIN_BUFFER_1]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            member: {\n              name: constants.getRandomString(),\n              score: 0,\n            },\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n            ]),\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            member: {\n              name: constants.getRandomString(),\n              score: 0,\n            },\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if try to modify incorrect data type',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            member: {\n              name: constants.getRandomString(),\n              score: 0,\n            },\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should modify member with empty value',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            member: {\n              name: constants.TEST_ZSET_MEMBER_1,\n              score: 1,\n            },\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.deep.eql([\n              constants.TEST_ZSET_MEMBER_2,\n              constants.TEST_ZSET_MEMBER_1,\n            ]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            member: {\n              name: constants.TEST_ZSET_MEMBER_1,\n              score: 0.1,\n            },\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"zadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            member: {\n              name: constants.getRandomString(),\n              score: 0,\n            },\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zadd'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            member: {\n              name: constants.getRandomString(),\n              score: 0,\n            },\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/z-set/POST-databases-id-zSet-get_members.test.ts",
    "content": "import {\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(\n    `/${constants.API.DATABASES}/${instanceId}/zSet/get-members`,\n  );\n\n// input data schema todo: investigate BE validation\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  offset: Joi.number().integer().min(0).allow(true).required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  count: Joi.number().integer().min(1).allow(true).required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  sortOrder: Joi.string().valid('DESC', 'ASC'),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  offset: 0,\n  count: 15,\n  sortOrder: 'DESC',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    total: Joi.number().integer().required(),\n    members: Joi.array()\n      .items(\n        Joi.object().keys({\n          name: JoiRedisString.required(),\n          score: Joi.number().required().allow('inf', '-inf'),\n        }),\n      )\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/zSet/get-members', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should query members from buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          offset: 0,\n          count: 15,\n          sortOrder: 'DESC',\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_ZSET_KEY_BIN_UTF8_1,\n          total: 1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_UTF8_1,\n              score: constants.TEST_ZSET_MEMBER_1_SCORE,\n            },\n          ],\n        },\n      },\n      {\n        name: 'Should query members from buff (return buff)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          offset: 0,\n          count: 15,\n          sortOrder: 'DESC',\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          total: 1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_BUF_OBJ_1,\n              score: constants.TEST_ZSET_MEMBER_1_SCORE,\n            },\n          ],\n        },\n      },\n      {\n        name: 'Should query members from ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          offset: 0,\n          count: 15,\n          sortOrder: 'DESC',\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          total: 1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_ASCII_1,\n              score: constants.TEST_ZSET_MEMBER_1_SCORE,\n            },\n          ],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should query 15 members sorted DESC',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            offset: 0,\n            count: 15,\n            sortOrder: 'DESC',\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            total: 100,\n            members: new Array(15).fill(0).map((item, i) => {\n              return {\n                name: `member_${100 - i}`,\n                score: 100 - i,\n              };\n            }),\n          },\n        },\n        {\n          name: 'Should query 45 members sorted ASC',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            offset: 0,\n            count: 45,\n            sortOrder: 'ASC',\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            total: 100,\n            members: new Array(45).fill(0).map((item, i) => {\n              return {\n                name: `member_${i + 1}`,\n                score: i + 1,\n              };\n            }),\n          },\n        },\n        {\n          name: 'Should query next 45 members sorted ASC',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            offset: 45,\n            count: 45,\n            sortOrder: 'ASC',\n          },\n          responseSchema,\n          responseBody: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            total: 100,\n            members: new Array(45).fill(0).map((item, i) => {\n              return {\n                name: `member_${i + 45 + 1}`,\n                score: i + 45 + 1,\n              };\n            }),\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            offset: 45,\n            count: 45,\n            sortOrder: 'ASC',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            offset: 45,\n            count: 45,\n            sortOrder: 'ASC',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should remove all members and key',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            offset: 0,\n            count: 15,\n            sortOrder: 'ASC',\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"zcard\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            offset: 0,\n            count: 15,\n            sortOrder: 'ASC',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zcard'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"zrange\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            offset: 0,\n            count: 15,\n            sortOrder: 'ASC',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zrange'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"zrevrange\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            offset: 0,\n            count: 15,\n            sortOrder: 'DESC',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zrevrange'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/z-set/POST-databases-id-zSet-search.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n  JoiRedisString,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/zSet/search`);\n\n// input data schema todo: investigate BE validation\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  cursor: Joi.number().integer().min(0).allow(true).required().messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  count: Joi.number().integer().min(1).allow(true, null).messages({\n    'any.required': '{#label} should not be empty',\n  }),\n  match: Joi.string().required(),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.getRandomString(),\n  cursor: 0,\n  count: 15,\n  match: '*',\n};\n\nconst responseSchema = Joi.object()\n  .keys({\n    keyName: JoiRedisString.required(),\n    total: Joi.number().integer().required(),\n    nextCursor: Joi.number().integer().required(),\n    members: Joi.array()\n      .items(\n        Joi.object().keys({\n          name: JoiRedisString.required(),\n          score: Joi.number().required().allow('inf', '-inf'),\n        }),\n      )\n      .required(),\n  })\n  .required();\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('POST /databases/:instanceId/zSet/search', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    before(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should find by buff (return utf8)',\n        query: {\n          encoding: 'utf8',\n        },\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          cursor: 0,\n          count: 15,\n          match: constants.TEST_ZSET_KEY_BIN_UTF8_1.slice(0, -20) + '*',\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_ZSET_KEY_BIN_UTF8_1,\n          total: 1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_UTF8_1,\n              score: constants.TEST_ZSET_MEMBER_1_SCORE,\n            },\n          ],\n        },\n      },\n      {\n        name: 'Should find by buff (return buff)',\n        query: {\n          encoding: 'buffer',\n        },\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          cursor: 0,\n          count: 15,\n          match: constants.TEST_ZSET_KEY_BIN_UTF8_1.slice(0, -20) + '*',\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          total: 1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_BUF_OBJ_1,\n              score: constants.TEST_ZSET_MEMBER_1_SCORE,\n            },\n          ],\n        },\n      },\n      {\n        name: 'Should find by ascii (return ascii)',\n        query: {\n          encoding: 'ascii',\n        },\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          cursor: 0,\n          count: 15,\n          match: constants.TEST_ZSET_KEY_BIN_ASCII_1.slice(0, -30) + '*',\n        },\n        responseSchema,\n        responseBody: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          total: 1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_ASCII_1,\n              score: constants.TEST_ZSET_MEMBER_1_SCORE,\n            },\n          ],\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should find by exact match',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_3,\n            cursor: 0,\n            count: 15,\n            match: 'member_2555',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_ZSET_KEY_3);\n            expect(body.total).to.eql(3000);\n            expect(body.members.length).to.eq(1);\n            expect(body.members[0].name).to.eq('member_2555');\n            expect(body.members[0].score).to.eq(2555);\n          },\n        },\n        {\n          name: 'Should not find any member',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_3,\n            cursor: 0,\n            count: 15,\n            match: 'notExis*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_ZSET_KEY_3);\n            expect(body.total).to.eql(3000);\n            expect(body.members.length).to.eq(0);\n          },\n        },\n        {\n          name: 'Should query 15 members',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_3,\n            cursor: 0,\n            count: 15,\n            match: '*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_ZSET_KEY_3);\n            expect(body.total).to.eql(3000);\n            expect(body.members.length).to.gte(15);\n            expect(body.members.length).to.lt(3000);\n          },\n        },\n        {\n          name: 'Should query members with * in the end',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_3,\n            cursor: 0,\n            count: 15,\n            match: 'member_215*',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_ZSET_KEY_3);\n            expect(body.total).to.eql(3000);\n            expect(body.members.length).to.eq(11);\n          },\n        },\n        {\n          name: 'Should query members with * in the beginning',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_3,\n            cursor: 0,\n            count: 15,\n            match: '*r_2155',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_ZSET_KEY_3);\n            expect(body.total).to.eql(3000);\n            expect(body.members.length).to.eq(1);\n            expect(body.members[0].name).to.eq('member_2155');\n            expect(body.members[0].score).to.eq(2155);\n          },\n        },\n        {\n          name: 'Should query members with * in the middle',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_3,\n            cursor: 0,\n            count: 15,\n            match: 'mem*r_2155',\n          },\n          responseSchema,\n          checkFn: ({ body }) => {\n            expect(body.keyName).to.eql(constants.TEST_ZSET_KEY_3);\n            expect(body.total).to.eql(3000);\n            expect(body.members.length).to.eq(1);\n            expect(body.members[0].name).to.eq('member_2155');\n            expect(body.members[0].score).to.eq(2155);\n          },\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            cursor: 0,\n            count: 15,\n            match: '*',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            cursor: 0,\n            count: 15,\n            match: '*',\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n        },\n      ].map(mainCheckFn);\n\n      describe('Search in huge number of elements', () => {\n        const ELEMENTS_NUMBER = 1_000_000;\n\n        requirements('rte.bigData');\n        [\n          {\n            name: 'Should get member using \"exists\" cmd without full scan',\n            data: {\n              keyName: constants.TEST_ZSET_HUGE_KEY,\n              cursor: 0,\n              match: constants.TEST_ZSET_HUGE_MEMBER,\n            },\n            responseSchema,\n            responseBody: {\n              keyName: constants.TEST_ZSET_HUGE_KEY,\n              total: ELEMENTS_NUMBER,\n              members: [\n                {\n                  name: constants.TEST_ZSET_HUGE_MEMBER,\n                  score: constants.TEST_ZSET_HUGE_SCORE,\n                },\n              ],\n              nextCursor: 0,\n            },\n          },\n          {\n            name: 'Should get elements with possibility to continue iterating',\n            data: {\n              keyName: constants.TEST_ZSET_HUGE_KEY,\n              cursor: 0,\n              match: '*',\n            },\n            responseSchema,\n            checkFn: ({ body }) => {\n              expect(body.keyName).to.eql(constants.TEST_ZSET_HUGE_KEY);\n              expect(body.total).to.eql(ELEMENTS_NUMBER);\n              expect(body.nextCursor).to.not.eql(0);\n              expect(body.members.length).to.gte(200);\n            },\n          },\n        ].map(mainCheckFn);\n      });\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should remove all members and key',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            cursor: 0,\n            count: 15,\n            match: '*',\n          },\n          responseSchema,\n        },\n        {\n          name: 'Should throw error if no permissions for \"zcard\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: '*',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zcard'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"zscan\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: '*',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zscan'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"zscore\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_2,\n            cursor: 0,\n            count: 15,\n            match: 'member_1',\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zscore'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/z-set/POST-databases-id-zSet.test.ts",
    "content": "import {\n  expect,\n  describe,\n  it,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  validateApiCall,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).post(`/${constants.API.DATABASES}/${instanceId}/zSet`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  members: Joi.array()\n    .items(\n      Joi.object().keys({\n        name: Joi.string().required().label('.name'),\n        // todo: allow(true) - is incorrect but will be transformed to number by BE. Investigate/fix it\n        score: Joi.number().required().allow(true).label('.score'),\n      }),\n    )\n    .messages({\n      'number.base': '{#lavel} must be a string or a number',\n      'array.sparse': 'members must be either object or array',\n      'array.base': 'property {#label} must be either object or array',\n    }),\n  expire: Joi.number().integer().allow(null).min(1).max(2147483647),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_ZSET_KEY_1,\n  members: [\n    {\n      name: constants.TEST_ZSET_MEMBER_1,\n      score: constants.TEST_ZSET_MEMBER_1_SCORE,\n    },\n  ],\n  expire: constants.TEST_ZSET_EXPIRE_1,\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\nconst createCheckFn = async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(0);\n      }\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    } else {\n      if (testCase.statusCode === 201) {\n        expect(await rte.client.exists(testCase.data.keyName)).to.eql(1);\n        expect(await rte.client.zrange(testCase.data.keyName, 0, 10)).to.eql([\n          testCase.data.members[0].name,\n        ]);\n        if (testCase.data.expire) {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.gte(\n            testCase.data.expire - 5,\n          );\n        } else {\n          expect(await rte.client.ttl(testCase.data.keyName)).to.eql(-1);\n        }\n      }\n    }\n  });\n};\n\ndescribe('POST /databases/:instanceId/zSet', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(rte.data.truncate);\n\n    [\n      {\n        name: 'Should create zset from buff',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_BUF_OBJ_1,\n              score: 0,\n            },\n          ],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_ZSET_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'zrange',\n              [constants.TEST_ZSET_KEY_BIN_BUFFER_1, 0, 10],\n              null,\n            ),\n          ).to.deep.eq([constants.TEST_ZSET_MEMBER_BIN_BUFFER_1]);\n        },\n      },\n      {\n        name: 'Should create zset from ascii',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          members: [\n            {\n              name: constants.TEST_ZSET_MEMBER_BIN_ASCII_1,\n              score: 0,\n            },\n          ],\n        },\n        statusCode: 201,\n        after: async () => {\n          expect(\n            await rte.client.exists(constants.TEST_ZSET_KEY_BIN_BUFFER_1),\n          ).to.eql(1);\n          expect(\n            await rte.data.sendCommand(\n              'zrange',\n              [constants.TEST_ZSET_KEY_BIN_BUFFER_1, 0, 10],\n              null,\n            ),\n          ).to.deep.eq([constants.TEST_ZSET_MEMBER_BIN_BUFFER_1]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(rte.data.truncate);\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should create item with empty value',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: '',\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create item with key ttl',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n            expire: constants.TEST_ZSET_EXPIRE_1,\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should create regular item',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: constants.TEST_ZSET_MEMBER_1,\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should return conflict error if key already exists',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 409,\n          responseBody: {\n            statusCode: 409,\n            error: 'Conflict',\n            message: 'This key name is already in use.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.eql([constants.TEST_ZSET_MEMBER_1]),\n        },\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_LIST_KEY_1,\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.eql([constants.TEST_ZSET_MEMBER_1]),\n        },\n      ].map(createCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 201,\n        },\n        {\n          name: 'Should throw error if no permissions for \"zadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zadd'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(createCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/api/z-set/PUT-databases-id-zSet.test.ts",
    "content": "import {\n  expect,\n  describe,\n  before,\n  deps,\n  Joi,\n  requirements,\n  generateInvalidDataTestCases,\n  validateInvalidDataTestCase,\n  getMainCheckFn,\n} from '../deps';\nconst { server, request, constants, rte } = deps;\n\n// endpoint to test\nconst endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>\n  request(server).put(`/${constants.API.DATABASES}/${instanceId}/zSet`);\n\n// input data schema\nconst dataSchema = Joi.object({\n  keyName: Joi.string().allow('').required(),\n  members: Joi.array()\n    .items(\n      Joi.object().keys({\n        name: Joi.string().required().label('.name'),\n        score: Joi.number().required().allow('inf', '-inf').label('.score'),\n      }),\n    )\n    .messages({\n      'number.base': '{#lavel} must be a string or a number',\n      'array.sparse': 'members must be either object or array',\n      'array.base': 'property {#label} must be either object or array',\n    }),\n}).strict();\n\nconst validInputData = {\n  keyName: constants.TEST_ZSET_KEY_1,\n  members: [\n    {\n      name: constants.TEST_ZSET_MEMBER_1,\n      score: constants.TEST_ZSET_MEMBER_1_SCORE,\n    },\n  ],\n};\n\nconst mainCheckFn = getMainCheckFn(endpoint);\n\ndescribe('PUT /databases/:instanceId/zSet', () => {\n  describe('Modes', () => {\n    requirements('!rte.bigData');\n    beforeEach(() => rte.data.generateBinKeys(true));\n\n    [\n      {\n        name: 'Should add member from buff',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_BUF_OBJ_1,\n          members: [\n            {\n              name: constants.TEST_LIST_ELEMENT_BIN_BUF_OBJ_1,\n              score: 1,\n            },\n          ],\n        },\n        after: async () => {\n          expect(\n            await rte.data.sendCommand(\n              'zrange',\n              [constants.TEST_ZSET_KEY_BIN_BUFFER_1, 0, 10],\n              null,\n            ),\n          ).to.deep.eq([\n            constants.TEST_ZSET_MEMBER_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n          ]);\n        },\n      },\n      {\n        name: 'Should add member from ascii',\n        data: {\n          keyName: constants.TEST_ZSET_KEY_BIN_ASCII_1,\n          members: [\n            {\n              name: constants.TEST_LIST_ELEMENT_BIN_ASCII_1,\n              score: 1,\n            },\n          ],\n        },\n        after: async () => {\n          expect(\n            await rte.data.sendCommand(\n              'zrange',\n              [constants.TEST_ZSET_KEY_BIN_BUFFER_1, 0, 10],\n              null,\n            ),\n          ).to.deep.eq([\n            constants.TEST_ZSET_MEMBER_BIN_BUFFER_1,\n            constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n          ]);\n        },\n      },\n    ].map(mainCheckFn);\n  });\n\n  describe('Main', () => {\n    before(async () => await rte.data.generateKeys(true));\n\n    describe('Validation', () => {\n      generateInvalidDataTestCases(dataSchema, validInputData).map(\n        validateInvalidDataTestCase(endpoint, dataSchema),\n      );\n    });\n\n    describe('Common', () => {\n      [\n        {\n          name: 'Should return NotFound error if instance id does not exists',\n          endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Invalid database instance id.',\n          },\n          after: async () =>\n            // check that value was not overwritten\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n            ]),\n        },\n        {\n          name: 'Should return NotFound error if key does not exists',\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 404,\n          responseBody: {\n            statusCode: 404,\n            error: 'Not Found',\n            message: 'Key with this name does not exist.',\n          },\n        },\n        {\n          name: 'Should return BadRequest error if try to modify incorrect data type',\n          data: {\n            keyName: constants.TEST_STRING_KEY_1,\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 400,\n          responseBody: {\n            statusCode: 400,\n            error: 'Bad Request',\n          },\n        },\n        {\n          name: 'Should add member with empty value',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: '',\n                score: 1,\n              },\n            ],\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.deep.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n              '',\n            ]);\n          },\n        },\n        {\n          name: 'Should add few members',\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: '2',\n                score: 2,\n              },\n              {\n                name: '3',\n                score: 3,\n              },\n            ],\n          },\n          statusCode: 200,\n          after: async () => {\n            expect(\n              await rte.client.zrange(constants.TEST_ZSET_KEY_1, 0, 10),\n            ).to.deep.eql([\n              constants.TEST_ZSET_MEMBER_1,\n              constants.TEST_ZSET_MEMBER_2,\n              '',\n              '2',\n              '3',\n            ]);\n          },\n        },\n      ].map(mainCheckFn);\n    });\n\n    describe('ACL', () => {\n      requirements('rte.acl');\n      before(async () => rte.data.setAclUserRules('~* +@all'));\n\n      [\n        {\n          name: 'Should create regular item',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 200,\n        },\n        {\n          name: 'Should throw error if no permissions for \"zadd\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.TEST_ZSET_KEY_1,\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -zadd'),\n        },\n        {\n          name: 'Should throw error if no permissions for \"exists\" command',\n          endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),\n          data: {\n            keyName: constants.getRandomString(),\n            members: [\n              {\n                name: constants.getRandomString(),\n                score: 0,\n              },\n            ],\n          },\n          statusCode: 403,\n          responseBody: {\n            statusCode: 403,\n            error: 'Forbidden',\n          },\n          before: () => rte.data.setAclUserRules('~* +@all -exists'),\n        },\n      ].map(mainCheckFn);\n    });\n  });\n});\n"
  },
  {
    "path": "redisinsight/api/test/helpers/analytics.ts",
    "content": "import { EventEmitter } from 'events';\nimport * as nock from 'nock';\nimport * as _ from 'lodash';\nimport { isMatch } from 'lodash';\n\nlet analytics;\n\nexport class Analytics extends EventEmitter {\n  public messages = [];\n\n  constructor() {\n    super();\n    const scope = nock('https://api.segment.io')\n      .post('/v1/batch', (body) => {\n        const batchMessages = body?.batch || [];\n        this.messages = this.messages.concat(batchMessages);\n        this.emit('batch', batchMessages);\n        return true;\n      })\n      .reply(200, {});\n\n    scope.persist();\n  }\n\n  public findEvent(event: any, messages = this.messages) {\n    return _.find(messages, (message) => {\n      return isMatch(message, event);\n    });\n  }\n\n  public async waitForEvent(event) {\n    await new Promise((res, rej) => {\n      this.once('batch', (batch) => {\n        const exists = this.findEvent(event, batch);\n\n        if (!exists) {\n          rej(\n            new Error(\n              `Unable to find event:\\n${JSON.stringify(event)}\\nin the events batch:\\n${JSON.stringify(batch)}`,\n            ),\n          );\n        }\n\n        res(exists);\n      });\n\n      setTimeout(\n        () =>\n          rej(new Error(`No event ${JSON.stringify(event)} received in 10s`)),\n        10000,\n      );\n    });\n  }\n}\n\nexport const getAnalytics = () => {\n  return analytics || createAnalytics();\n};\n\nexport const createAnalytics = () => {\n  return new Analytics();\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/cloud.ts",
    "content": "import { constants } from './constants';\nimport * as request from 'supertest';\nimport * as _ from 'lodash';\n\nexport const initCloudDatabase = async () => {\n  let subscription = await getSubscriptionByName(\n    constants.TEST_CLOUD_SUBSCRIPTION_NAME,\n  );\n  let startTime;\n  let ttlThreshold;\n\n  // create subscription with database\n  if (!subscription) {\n    const paymentMethodId = await getPaymentMethod();\n\n    if (!paymentMethodId) {\n      throw new Error(\"Cloud Account isn't configured well\");\n    }\n\n    await createSubscription({\n      name: constants.TEST_CLOUD_SUBSCRIPTION_NAME,\n      paymentMethodId: paymentMethodId,\n      cloudProviders: [\n        {\n          regions: [\n            {\n              region: 'us-east-1',\n              networking: {\n                deploymentCIDR: '10.0.0.0/24',\n              },\n            },\n          ],\n        },\n      ],\n      databases: [\n        {\n          name: constants.TEST_CLOUD_DATABASE_NAME,\n          memoryLimitInGb: 1,\n        },\n      ],\n    });\n\n    ttlThreshold = 5 * 60 * 1000; // 5 min to wait for pending or active status\n    startTime = Date.now();\n    while (\n      (!subscription || !['pending', 'active'].includes(subscription.status)) &&\n      Date.now() - startTime < ttlThreshold\n    ) {\n      subscription = await new Promise((resolve) => {\n        setTimeout(\n          async () => {\n            const subscription = await getSubscriptionByName(\n              constants.TEST_CLOUD_SUBSCRIPTION_NAME,\n            );\n            console.log(\n              `Waiting for pending or active subscriptions ${(Date.now() - startTime) / 1000}s: `,\n            );\n            resolve(subscription);\n          },\n          +(Date.now() - startTime > 1000) * 20000,\n        ); // execute each 20 sec\n      });\n    }\n  }\n\n  constants.TEST_CLOUD_SUBSCRIPTION_ID = subscription.id;\n\n  switch (subscription.status) {\n    case 'pending':\n      ttlThreshold = 20 * 60 * 1000; // !!! 20 min to wait for active status\n      startTime = Date.now();\n      while (\n        subscription.status !== 'active' &&\n        Date.now() - startTime < ttlThreshold\n      ) {\n        subscription = await new Promise((resolve) => {\n          setTimeout(\n            async () => {\n              const subscription = await getSubscriptionByName(\n                constants.TEST_CLOUD_SUBSCRIPTION_NAME,\n              );\n              console.log(\n                `Waiting for active subscriptions ${(Date.now() - startTime) / 1000}s: `,\n              );\n              resolve(subscription);\n            },\n            +(Date.now() - startTime > 1000) * 20000,\n          ); // execute each 20 sec\n        });\n      }\n      if (subscription.status !== 'active') {\n        throw new Error(\n          'Timeout exceeded when waiting for subscription \"active\" status',\n        );\n      }\n    case 'active':\n      let database = await getDatabaseByName(\n        constants.TEST_CLOUD_SUBSCRIPTION_ID,\n        constants.TEST_CLOUD_DATABASE_NAME,\n      );\n\n      if (!database) {\n        throw new Error('Error when fetching database');\n      }\n\n      startTime = Date.now();\n      ttlThreshold = 5 * 60 * 1000; // !!! 5 min to wait for database public endpoint\n      while (\n        !database.publicEndpoint &&\n        Date.now() - startTime < ttlThreshold\n      ) {\n        database = await new Promise((resolve) => {\n          setTimeout(\n            async () => {\n              const database = await getDatabaseByName(\n                constants.TEST_CLOUD_SUBSCRIPTION_ID,\n                constants.TEST_CLOUD_DATABASE_NAME,\n              );\n              console.log(\n                `Waiting for database public endpoint ${(Date.now() - startTime) / 1000}s: `,\n              );\n              resolve(database);\n            },\n            +(Date.now() - startTime > 1000) * 5000,\n          ); // execute each 5 sec\n        });\n      }\n\n      const [host, port] = database.publicEndpoint.split(':');\n      constants.TEST_REDIS_HOST = host;\n      constants.TEST_REDIS_PORT = +port;\n      constants.TEST_REDIS_PASSWORD = database.security.password;\n      break;\n    default:\n      throw new Error(`Unexpected subscription status: ${subscription.status}`);\n  }\n};\n\nconst getSubscriptionByName = async (name) => {\n  const { body } = await request(constants.TEST_CLOUD_API)\n    .get('/subscriptions')\n    .set('x-api-key', constants.TEST_CLOUD_API_KEY)\n    .set('x-api-secret-key', constants.TEST_CLOUD_API_SECRET_KEY)\n    .expect(200);\n\n  return _.find(body.subscriptions, { name });\n};\n\nconst getDatabaseByName = async (subscriptionId, databaseName) => {\n  const { body } = await request(constants.TEST_CLOUD_API)\n    .get(`/subscriptions/${subscriptionId}/databases`)\n    .set('x-api-key', constants.TEST_CLOUD_API_KEY)\n    .set('x-api-secret-key', constants.TEST_CLOUD_API_SECRET_KEY)\n    .expect(200);\n\n  const subscription = _.find(body.subscription, { subscriptionId });\n\n  if (!subscription) {\n    throw new Error(`There is no subscription with such id`);\n  }\n\n  const database = _.find(subscription.databases, { name: databaseName });\n  if (!database) {\n    throw new Error(\n      `There is no database with name ${databaseName} in subscription ${subscriptionId}`,\n    );\n  }\n  const { body: fullDatabaseInfo } = await request(constants.TEST_CLOUD_API)\n    .get(`/subscriptions/${subscriptionId}/databases/${database.databaseId}`)\n    .set('x-api-key', constants.TEST_CLOUD_API_KEY)\n    .set('x-api-secret-key', constants.TEST_CLOUD_API_SECRET_KEY)\n    .expect(200);\n\n  return fullDatabaseInfo;\n};\n\nconst getPaymentMethod = async () => {\n  const { body } = await request(constants.TEST_CLOUD_API)\n    .get('/payment-methods')\n    .set('x-api-key', constants.TEST_CLOUD_API_KEY)\n    .set('x-api-secret-key', constants.TEST_CLOUD_API_SECRET_KEY)\n    .expect(200);\n\n  return body.paymentMethods.length ? body.paymentMethods[0].id : null;\n};\n\nconst createSubscription = async (data) => {\n  return request(constants.TEST_CLOUD_API)\n    .post('/subscriptions')\n    .send(data)\n    .set('x-api-key', constants.TEST_CLOUD_API_KEY)\n    .set('x-api-secret-key', constants.TEST_CLOUD_API_SECRET_KEY)\n    .expect(202);\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/constants.ts",
    "content": "import { v4 as uuidv4 } from 'uuid';\nimport * as path from 'path';\nimport { randomBytes } from 'crypto';\nimport {\n  getASCIISafeStringFromBuffer,\n  getBufferFromSafeASCIIString,\n} from 'src/utils/cli-helper';\nimport { RECOMMENDATION_NAMES, TelemetryEvents } from 'src/constants';\nimport { Compressor } from 'src/modules/database/entities/database.entity';\nimport { Vote } from 'src/modules/database-recommendation/models';\nimport { CloudSubscriptionType } from 'src/modules/cloud/subscription/models';\nimport { TagEntity } from 'src/modules/tag/entities/tag.entity';\n\nconst API = {\n  DATABASES: 'databases',\n  RDI: 'rdi',\n};\n\nconst TEST_RUN_ID = `=${uuidv4()}`;\nconst KEY_TTL = 100;\nconst CLUSTER_HASH_SLOT = '{slot1}';\nconst APP_DEFAULT_SETTINGS = {\n  scanThreshold: 10000,\n  batchSize: 5,\n  theme: null,\n  dateFormat: null,\n  timezone: null,\n  agreements: null,\n  acceptTermsAndConditionsOverwritten: false,\n};\nconst TEST_LIBRARY_NAME = 'lib';\nconst TEST_ANALYTICS_PAGE = 'Settings';\n\nconst unprintableBuf = Buffer.concat([\n  Buffer.from('acedae', 'hex'),\n  Buffer.from(CLUSTER_HASH_SLOT),\n]);\n\nconst CERTS_FOLDER = process.env.CERTS_FOLDER || './coverage';\nconst TEST_PRE_SETUP_DATABASES_PATH =\n  process.env.RI_PRE_SETUP_DATABASES_PATH || './databases.json';\n\nconst TEST_RUN_DIR = '.test_run';\nconst TEST_ENCRYPTION_STRATEGY = 'KEYTAR';\n\nconst TEST_TAGS: TagEntity[] = [\n  Object.assign(new TagEntity(), {\n    id: uuidv4(),\n    key: 'environment',\n    value: 'production',\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    encryption: TEST_ENCRYPTION_STRATEGY,\n  }),\n  Object.assign(new TagEntity(), {\n    id: uuidv4(),\n    key: 'environment',\n    value: 'staging',\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    encryption: TEST_ENCRYPTION_STRATEGY,\n  }),\n  Object.assign(new TagEntity(), {\n    id: uuidv4(),\n    key: 'size',\n    value: 'large',\n    createdAt: new Date(),\n    updatedAt: new Date(),\n    encryption: TEST_ENCRYPTION_STRATEGY,\n  }),\n];\n\nexport const constants = {\n  // api\n  API,\n  // common\n  TEST_RUN_ID,\n  TEST_RUN_NAME: process.env.TEST_RUN_NAME || '',\n  KEY_TTL,\n  CLUSTER_HASH_SLOT,\n  TEST_TAGS,\n  getRandomString: () => `${TEST_RUN_ID}_${uuidv4()}_${CLUSTER_HASH_SLOT}`,\n  generateRandomKey: () => `${TEST_RUN_ID}_${uuidv4()}_${CLUSTER_HASH_SLOT}`,\n  APP_DEFAULT_SETTINGS,\n  TEST_KEYTAR_PASSWORD:\n    process.env.RI_SECRET_STORAGE_PASSWORD || 'somepassword',\n  TEST_INCORRECT_PASSWORD: 'incorrect',\n  TEST_ENCRYPTION_STRATEGY,\n  TEST_AGREEMENTS_VERSION: '1.0.3',\n  TEST_REMOTE_STATIC_PATH: './remote',\n  TEST_REMOTE_STATIC_URI: '/remote',\n  TEST_FEATURE_FLAG_REMOTE_CONFIG_PATH: './remote/features-config.json',\n\n  // local database\n  TEST_LOCAL_DB_FILE_PATH:\n    process.env.TEST_LOCAL_DB_FILE_PATH || './redisinsight.db',\n  TEST_NOT_EXISTED_INSTANCE_ID: uuidv4(),\n  TEST_INSTANCE_ID: uuidv4(),\n  TEST_INSTANCE_NAME: uuidv4(),\n  TEST_INSTANCE_ACL_ID: uuidv4(),\n  TEST_INSTANCE_ACL_NAME: uuidv4(),\n  TEST_INSTANCE_ACL_USER: uuidv4(),\n  TEST_INSTANCE_ACL_PASS: uuidv4(),\n  TEST_NEW_INSTANCE_NAME: uuidv4(),\n  TEST_CLI_UUID_1: uuidv4(),\n  TEST_INSTANCE_ID_2: uuidv4(),\n  TEST_INSTANCE_NAME_2: uuidv4(),\n  TEST_INSTANCE_HOST_2: uuidv4(),\n  TEST_INSTANCE_ID_3: uuidv4(),\n  TEST_INSTANCE_NAME_3: uuidv4(),\n  TEST_INSTANCE_HOST_3: uuidv4(),\n  TEST_INSTANCE_ID_4: uuidv4(),\n  TEST_INSTANCE_NAME_4: uuidv4(),\n  TEST_INSTANCE_ID_5: uuidv4(),\n  TEST_INSTANCE_NAME_5: uuidv4(),\n  TEST_INSTANCE_HOST_5: uuidv4(),\n  TEST_INSTANCE_ID_6: uuidv4(),\n  TEST_INSTANCE_NAME_6: uuidv4(),\n  TEST_INSTANCE_HOST_6: uuidv4(),\n  TEST_INSTANCE_ID_7: uuidv4(),\n  TEST_INSTANCE_NAME_7: uuidv4(),\n  TEST_INSTANCE_HOST_7: uuidv4(),\n  TEST_INSTANCE_PORT_5: 5555,\n  TEST_INSTANCE_HOST_4: '127.0.0.1',\n  TEST_INSTANCE_PORT_4: 3333,\n\n  // redis client\n  TEST_REDIS_HOST: process.env.TEST_REDIS_HOST || 'localhost',\n  TEST_REDIS_PORT: parseInt(process.env.TEST_REDIS_PORT) || 6379,\n  TEST_REDIS_TIMEOUT: 30_000,\n  TEST_REDIS_COMPRESSOR: Compressor.NONE,\n  TEST_REDIS_DB_INDEX: 7,\n  TEST_REDIS_USER: process.env.TEST_REDIS_USER,\n  TEST_REDIS_PASSWORD: process.env.TEST_REDIS_PASSWORD,\n  TEST_REDIS_TLS_CA: process.env.TEST_REDIS_TLS_CA,\n  TEST_USER_TLS_CERT: process.env.TEST_USER_TLS_CERT,\n  TEST_USER_TLS_KEY: process.env.TEST_USER_TLS_KEY,\n\n  TEST_RTE_ON_PREMISE: process.env.TEST_RTE_ON_PREMISE\n    ? process.env.TEST_RTE_ON_PREMISE === 'true'\n    : true,\n  TEST_RTE_SHARED_DATA: process.env.TEST_RTE_SHARED_DATA\n    ? process.env.TEST_RTE_SHARED_DATA === 'true'\n    : false,\n  TEST_RTE_BIG_DATA: process.env.TEST_RTE_BIG_DATA\n    ? process.env.TEST_RTE_BIG_DATA === 'true'\n    : false,\n  TEST_RTE_CRDT: process.env.TEST_RTE_CRDT\n    ? process.env.TEST_RTE_CRDT === 'true'\n    : false,\n  TEST_RTE_TYPE: process.env.TEST_RTE_DISCOVERY_TYPE || 'STANDALONE',\n  TEST_RTE_HOST: process.env.TEST_RTE_DISCOVERY_HOST,\n  TEST_RTE_PORT: process.env.TEST_RTE_DISCOVERY_PORT,\n  TEST_RTE_USER: process.env.TEST_RTE_DISCOVERY_USER,\n  TEST_RTE_PASSWORD: process.env.TEST_RTE_DISCOVERY_PASSWORD,\n\n  // sentinel\n  TEST_SENTINEL_MASTER_GROUP:\n    process.env.TEST_SENTINEL_MASTER_GROUP || 'primary1',\n  TEST_SENTINEL_MASTER_USER: process.env.TEST_SENTINEL_MASTER_USER,\n  TEST_SENTINEL_MASTER_PASS: process.env.TEST_SENTINEL_MASTER_PASS,\n\n  // re\n  TEST_RE_HOST: process.env.TEST_RE_HOST || 'localhost',\n  TEST_RE_PORT: parseInt(process.env.TEST_RE_PORT) || 9443,\n  TEST_RE_USER: process.env.TEST_RE_USER,\n  TEST_RE_PASS: process.env.TEST_RE_PASS,\n\n  // cloud\n  TEST_CLOUD_RTE: process.env.TEST_CLOUD_RTE,\n  TEST_CLOUD_API:\n    process.env.REDIS_CLOUD_URL ||\n    process.env.TEST_CLOUD_API ||\n    'https://api.qa.redislabs.com/v1',\n  TEST_CLOUD_API_KEY: process.env.TEST_CLOUD_API_KEY || 'TEST_CLOUD_API_KEY',\n  TEST_CLOUD_API_SECRET_KEY:\n    process.env.TEST_CLOUD_API_SECRET_KEY || 'TEST_CLOUD_API_SECRET_KEY',\n  TEST_CLOUD_SUBSCRIPTION_NAME:\n    process.env.TEST_CLOUD_SUBSCRIPTION_NAME || 'ITests',\n  TEST_CLOUD_SUBSCRIPTION_ID: process.env.TEST_CLOUD_SUBSCRIPTION_ID || 1,\n  TEST_CLOUD_DATABASE_NAME: process.env.TEST_CLOUD_DATABASE_NAME || 'ITests-db',\n  TEST_CLOUD_ID: process.env.TEST_CLOUD_ID || 111,\n  TEST_CLOUD_SUBSCRIPTION_TYPE:\n    process.env.TEST_CLOUD_SUBSCRIPTION_TYPE || CloudSubscriptionType.Fixed,\n\n  STANDALONE: 'STANDALONE',\n  CLUSTER: 'CLUSTER',\n  SENTINEL: 'SENTINEL',\n\n  // ssh\n  TEST_SSH_HOST: process.env.TEST_SSH_HOST,\n  TEST_SSH_PORT: process.env.TEST_SSH_PORT\n    ? parseInt(process.env.TEST_SSH_PORT, 10)\n    : 22,\n  TEST_SSH_USER: process.env.TEST_SSH_USER,\n  TEST_SSH_PASSWORD: process.env.TEST_SSH_PASSWORD,\n  TEST_SSH_PRIVATE_KEY: process.env.TEST_SSH_PRIVATE_KEY,\n  TEST_SSH_PRIVATE_KEY_P: process.env.TEST_SSH_PRIVATE_KEY_P,\n  TEST_SSH_PASSPHRASE: process.env.TEST_SSH_PASSPHRASE,\n\n  // certificates\n  TEST_USER_CERT_ID: uuidv4(),\n  TEST_USER_CERT_NAME: uuidv4(),\n  TEST_USER_CERT_FILENAME: 'user.crt',\n  TEST_USER_CERT_KEY_FILENAME: 'user.key',\n\n  TEST_CLIENT_CERT_NAME: 'client certificate',\n  TEST_CLIENT_CERT:\n    '-----BEGIN CERTIFICATE-----\\nMIIFJTCCAw0CFCnZUPMfcoAU/VJYA6Qf4cZIJp4iMA0GCSqGSIb3DQEBCwUAMEUx\\nCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl\\ncm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjEwNjIyMTcwMDQ2WhcNMzUwMzAxMTcw\\nMDQ2WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE\\nCgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3Qw\\nggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDfRqe8YhMbWSlcbpGcIxXf\\ncxYt9IYa1oAhW/KJ7iHwjldy82ht6mdYIvhqSxo8Xo9AUpMl9LT3mZv1aCup8G4u\\nS5DXdYNV5KuTTP8zx5pcw0GVKLKB7THOOFV8Fzyx8dQAA24Z7Bz9aRRAeQsm2+tN\\nQHL6D71uVPt9D07Tu2GGjivFhT/gHn1VBFbrpGEF+Z5dQbh7fd1j3kBpEtSrMrTh\\nQfYVWtpdRW6JvsdG/Y07fkFCWEHbgVGqnVjJEc38ieCImDFK6vR+Q1bFqtvkr1zw\\nKx6X6Hol32LeI2TJ+cPtHak8L53cKJyoIe3xu/9uIqhqGL+GUqBGLNsGYVR9RgfF\\nwndk3/2ZeRodxKKsjaIMBlLLgmZkXoO6+hmyE3RNZv1fmgrTbjs1XTlxpi+byVs1\\nuqHFBKLt2NclAOIXf8IGt9+5cPSOenMEW3pUUUb8yXKUgBKfEU8HO38tbLDpY0hW\\n3mS/hIiTzcr5kD1jgoJ17SKZXKgOd0dhilN263YZnpcy0zFTeLNyTBAopte84Mmh\\nRoMFVM2r6449P7sbm1YvyUNTGwgwsFpr2eJNcKk8laW4uSelvSLxlm6e4VtQ0FEX\\n+7igpL6Mxdu3BUqhJrcoeNzz4AvbwZWie9IbSaIz+FeB1lMXDAt1kis+QCNc1S6a\\n4Ulsl2HAApT8u3Fdfs+c3QIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQClCb7TfzWz\\nSMT/6Y9cB4phR/FFQxqumNaE1ER1hLvU1wiGX85KwpAQOpIOS3J8pDYmTIiD2Zl4\\n/EoHr//OsfYQQ05LT7pR5qPHyz+pxp/OH35k+LIaPtG3E2PrkjHffG5udRhGAxtr\\nP3pampp0NaoFDtVNSjj73jedxhXKQVIPypK34yGOa67ISbDOzWQEoHCUwFPsNIp0\\nd3uq8WDb0Er9sX2pCheuHYtxs6jgNaXOmJT8VkAKwaKnpUejfFiA/YPtsGgkSlEX\\nhv1G0jmMquhPrMBq/pQrqvnA69dPi009L8m+aO1q1w5HQhH67JPjYSVR8A+OWfJ+\\n2oMRO6UP2Ryf3G4STL1sL7GF9rWWsKtQH8T4ESZ3WfHcw7NjuEt2ngH2bplAei/e\\nxCVDWgNfAWYQqbJhqv4NMEaIh5L0GkTTCnjxsEq2ByFlka+hQqz+6PQW/gQYScgT\\n2+D0DQW7RP1pvheLwYABLDkx1y0eXBOmcthxVn9GyjZOmZFreRIBhlHp2lN6wUiR\\n9Qj7UvwJ7Jhocc2mNwNxEmFLRoKku+1uoc/n3b/chaq0WadlDohWmE7hiKA25AG+\\nRj54Ou5G78qbWstZPR/sAXQGtUkgXuAkh+RcP8OcfpImUryZ/4cIoSzgAIE+NYX6\\nif8fVyASWrgcBKk5RUyFCZKMkJceV9ZGFg==\\n-----END CERTIFICATE-----\\n',\n  TEST_CLIENT_KEY:\n    '-----BEGIN RSA PRIVATE KEY-----\\nMIIJKgIBAAKCAgEAuBRkoLY666J54zx2BFquG1drk+Scpw7/4VA/4wEF/RYN8vjU\\n8jancCxK0lWWoIj+JdK2UXxJF9bjbmArnMyZm7EN1MKrbPeRIYCeH57ZWFTdMYHL\\nZRBY2Frc7dYpe6+ow2Wyu1oGQVnu6fCGPDh9oYqD1ULC0KBzD/GtLGDoqxjiC+/y\\nsW6T++XrZ4QGAfhewu7BXApMmE6q8EpJvB2g167IVdytaXIkIl5CWayS1Uva2wDH\\nLcq+UkORlOEPH7cZej/du5+8vnpwpbIvBR+DJw9Y9q0sQSxyYsX36CW4fd+l3ClX\\nDqD6MuiRQntpy8N73K1c3glALnBwWmQ9K3dQgDwcXL6mk9Pz5kJsnft3i2FMEyLb\\nYx8j8dlem4CzFd1DT8p4WOVttg1iIQYdjggPAKUio1hevZB/BI2EwB0/U1IZ657W\\n/krfwPoOPaWfC7i58RHcbKG14oHJSL0CzK4F+bfCSkvz1f1DrcMqkryU5in4x/1L\\nG9r78eenuy96s9qhpKaeBKOLgREqZoLnqsWiqoVePb7bnSISW/VKGiXen9AlIWL4\\nfOWJTs4PW0JLp9OLwxxbwVEkZkNsilWH/F0ueUZBZYYJVohh7tp2JABaeioxj+0V\\ngwoFgQDbmpJvB/XkdG6Eg6J3cTnbHR7sOjFvSpCmUnjnhakZT5RRQVvYSvkCAwEA\\nAQKCAgBRfRWe34ztyxtSMO29t7bje6uv6MBAZC96OuBNSaKxCxZZvTXnk7JDwhfN\\nTP5FSt/XNpRnNjHVT9eWgRRNcXV+qr6ItTTWJDInNpzJOrTUmZzh0aeMsdPi0zaC\\nQxBSJMz80wRwU8X5ICrXfRavigJzhLIfslIzsRO+tyoGP1BAjd9jkXFKgr0YAgxX\\n4uYV8TFh8fe/GwAVXJ3nibtif2s4j7M3710FFPZSEJAmynKl4dKcqJeD+gCOwkKs\\nOYVMcO3iZGtwJ6KSX/mGIH8YMX8Jx42GhdrVbyuj9idsqWYmst7lu5dCbpjT+Ih1\\nedS30239nvFBia7T4AqcuUsq9sK3gbFJKGADWDEeJiRP66ITQRXMQDJ/F1UXwWWa\\n0zOFF+UYAAf42JKDyhMjc9RPtRLBZJFsJ9wzhbyRl+UO23DTjZBs5OsOp6VVOhce\\nSFVxG9tSLIVWSuZLKa0u+R7Bh5zUYaX2nYO3fNqueFoQdMm8EA31OiBVCPdVEvqf\\nl/n0IaxN0G1zZMOmLoaUu0j1PwjkI8qZ4D1X/aDK1zYaMGAy8PdY0swtwRt/vC9F\\nAfSwY4eSPgHEeyJH0cLJkUCrQCSNMLyCXhErMlesoNNdGAxFIho0OzsVVEZAt0zd\\nP1NzsufcSKtsHEa+hM0YW80ST4GtyPkTKsRXtNDwMg3U4A+0AQKCAQEA2h+Wx7xN\\n694m73VCoCkGTa08PRaZjIdztYpMpe7Lj8EjtrKVBYIHBj3JLmXtp1jXMhSsPWLr\\nHrqQhy33/rX95JJBu5nxvok6wAISET3CEtxNUXe8GbN+QG6ry4TIcRH8aqqpFDOM\\nb9V3ZFEbTOo6S4l9Thoqa+la2NK4WGbDufPVlI2unluEKVTHyak0Tf2Qu2SzIL8A\\nqIpKB3MjweE9IGHtM0tgIdJjEQA8VS5Ai0pE8iohh5J2kIjEqPRMfwb9F+41tNlH\\nuAkj2sZge2GFz3qYk8x5bKk37xH3ogCRQtxt8x5rlPZWpf6/LvEm04PtRPXuJZ0u\\nm+voqrE8mVBtmQKCAQEA2AtuYZGKxEp+a/gde9TvkkCWiSnqfMwCQ+ax9TfGenS5\\ntYh75uEpkcsIDE9d8GD4tkShyMbZPUglxR/WVsz4HsACXLoR/sRSWNuQkMhEltN0\\n4VQcbaNkgKvktuvealueKEIWlKoKyqZEmmyJaW9wBt2mc9hku81kdXCAXTksjqgY\\ncaayi9BYg5+EqKzmAIDA33sh+douv9VvJSPJ93g4f3UvOdUiAyFCbXnYN9Iq46nP\\nuCc5qzoGHHi54o9FRHp0H0YioQxl5IvE42EkqJLnc1JaTixWUWhiFJjsbFlCcNsQ\\n0aoIB/kVrOyXvnK+r/Jge/LILathqk7sjRjYaWdkYQKCAQEArYc4A0rxitY/j31w\\nNc6tbxqEs+zI153jFegitlfVplX3PZ+xIqKhR/vbk4gPm3T4LqV3qZaKivXNiV2u\\nz/qlNDSPCtqcEgNGs/5xtTm2rh6JfGiPQrsjk8r37X+Dn0C52XpP7Pxdm5Lt2ucT\\nmws0uWd2Qq5aVWNenOR3OAz5ZXRw1DArXVxdNix2jR6JuAokHJEuWLzbnzn1Txvw\\ntIumf56ogIhUwFOJ8LqJRRL40leRpj6SUjLZFH9aRTelq+E5dNJT875wah8LYT80\\n/rNFKxzTSbIAX8v37cATi9R7u/91kVcAK5AWuxSBsKy1QMzR9Gzaux3jOLRjc3hx\\nR19O8QKCAQEAyBKuAkVakTW7phl8lHU59+NAhX3/3drALkmyfDlO4ZC/etIOjF3w\\ntUelCGFnyXjEW2drvBgKjqoF8GvvfysKjM+cYGsgxyLgb9HGK46LlnH1R8cxHIe4\\nR0Do6k29CBoYeYfaiYp/u/QGjEv/ZVkCEhmqUJYRk6o+YlPxTGPqU6JwILAToU8s\\n6ZgMrniP999EvrG1YUEhEh6Cc46VN0xqZf8L4S7z9JoUfnXcOrWzamqUJyKMUXnG\\ntw9Gdf3gU+5jI6M75pEou2KEz13jKQoCtdWKM+LzfSiBzDlimWSAFyuIg+JG1bti\\ny2W/kWuKFD8OAztvDnwsUiANCQ39PH+3gQKCAQEAqv5ig8A75OCtFwnOXadiI7xu\\nOzZezpmgzwLxQTdLzkcoSZ6oSgpDs9123i6j2hzriIzp0DvoyYo9qC7KWSP4iP6b\\nTi1gGJOADTehZ/DhLI7p6pCwi7YAWD/D6BhssmcKvdVDNjK1kqxJQetbI1XSEv2B\\nnabfcN+yYd0T0HB0gEA8qrtxQF4lkpZNtAjUnPpMSzel9VKEisGm5UIAVTIk1Gbc\\ndXQFkuq7T7DVQtYxkz9ZOqbZB0yMLKYpFXnUQ0z5OpYDgtp7Zs6r7CtTR2YROIQ0\\nbFVfR3CPbk4Qj+QBZvIjoeiUJwZUab0JWRxn5BsoKAeHJ1BZtN7KsKMHiLPlgg==\\n-----END RSA PRIVATE KEY-----\\n',\n\n  TEST_CA_ID: uuidv4(),\n  TEST_CA_NAME: 'ca certificate',\n  TEST_CA_FILENAME: 'redisCA.crt',\n  TEST_CA_CERT:\n    '-----BEGIN CERTIFICATE-----\\nMIIFazCCA1OgAwIBAgIUavmmz7/4r2muhE1He1u/6S1jLXEwDQYJKoZIhvcNAQEL\\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA2MjIxNjMwNTJaFw00ODEx\\nMDcxNjMwNTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB\\nAQUAA4ICDwAwggIKAoICAQDGUx5tjluzcotvXlr4XCqAbdO+ehD/djqzb3KxB4p4\\nUNE5tqS4TykvbisASOnNj/643J0BSYKEZwNBjy/oAWmc3cVMq30WnWRENGRUKyr+\\nqhgjR0OGMHxpAU8DiDgsJAuvvh86SU0xIo6PUWyO38XNIOGt05s61My9fW+Allai\\n5/jj6knBej42cRY7B9hUgHfko9NXE5oUVFKE+dpH9IiMUGBm7SDi1ysB1vIMQhcT\\n8ugQHdwXAiQfhDODNuDG48z6OprhGgHN5lYNFd3oFlweoFaqE0psFRh9bR5AuqES\\nubxEFqMVwEjyJa8BgObRBwdHoipZt1FLDeKTP5/MGUm5n/2X+pcAi4Q7+9i+aVz5\\ngFiCz6ndOFEj3X4CXcHHLVzI8ukQ3wQiDFXnomLOcFcuAJ9t+MisUOwts/Nvmqa0\\n+copNgXu2N8K01G77HX1qbJ0uyF6pupw2EWW0yJXkoSeOeaFegHPMx6y3RUx1adl\\nKu9vQ8JDodK4OwHfQcSBgj8aKA7huBnclgpBmM6B1czC6pw7DN6orLOlsx6cUusP\\n4mELM2CNNYLUQuxhghTO8lAQTgvvth5MNSpxA6x/gKFGmLN9XUJIZweQQymeY137\\n8elXS2yuoSyppisB+HDvp6MbegN1ldzhI0AjdUj9NDiiO5sDk+XscKA8tsZz/MgW\\nMQIDAQABo1MwUTAdBgNVHQ4EFgQU0CzAfHYx+Tr/axoAsurYNR/t2RMwHwYDVR0j\\nBBgwFoAU0CzAfHYx+Tr/axoAsurYNR/t2RMwDwYDVR0TAQH/BAUwAwEB/zANBgkq\\nhkiG9w0BAQsFAAOCAgEAd6Fqt+Ji1DV/7XA6e5QeCjqhrPxsXaUorbNSy2a4U59y\\nRj5lmI8RUPBt6AtSLWpeZ5JU2NQpK+4YfbopSPnVtc8Xipta1VmSr2grjT0n4cjY\\nXkMHV4bwaHBhr1OI2REcBOiwNP2QzXK7uFa75nZUyQSC0C3Qi5EJri2+a6xMsuF5\\nE8a9eyIvst1ESXJ9IJITc8e/eYFtpGw7WRClcm1UblwqYpO9sW9fFuZDpuBC0UH1\\nGXolRnFYN8PstjxmXHtrjHGcmOY+t1yFnyxOgZ01rmaFt+JEFbPOmgN17wcAidrV\\nAuXKWal9zrtlJc1J8GPHPpBTlZ+Qq5TlPI7Z3Boj9FCZdl3JEWUZGP7TPjxCWLoH\\n2/wJppE7w2bQcnidQngZhf2PN5RNQASUa2QBae7rkztReJ6A/xMWXAOfgkj13IbS\\nPIDZnBQYp5DKAxL9PRB/javL57/fUtYAxxzZK4xbvwY/lygv3+NetPqRHnx/IVBj\\nuEal2rpdwyFcoJ3DODbh9eh6tWJB4wR8QyYm3ATF1VV+x6XX5u5t5Z4IUt8WJkgn\\nHGzepJVYxzJMzjlyjqF1IG9e1da8c4DdRgmOn3R55G5BWQR3i6J+RAQY/O1S3VKA\\n0FDYT/EDZRbtXWwStSWUIPxNZt62vNGgwzprQow9OfJHRuOzlzIiK2BqnixboOs=\\n-----END CERTIFICATE-----\\n',\n  CERTS_FOLDER,\n  TEST_PRE_SETUP_DATABASES_PATH,\n  TEST_CA_CERT_PATH: path.join(CERTS_FOLDER, 'ca.crt'),\n  TEST_CLIENT_CERT_PATH: path.join(CERTS_FOLDER, 'client.crt'),\n  TEST_CLIENT_KEY_PATH: path.join(CERTS_FOLDER, 'client.key'),\n\n  // Redis Strings\n  TEST_STRING_TYPE: 'string',\n  TEST_STRING_KEY_1: TEST_RUN_ID + '_string_1' + CLUSTER_HASH_SLOT,\n  TEST_STRING_VALUE_1: TEST_RUN_ID + '_value_1',\n  GENERATE_BIG_TEST_STRING_VALUE: (sizeInMB = 1) =>\n    randomBytes(sizeInMB * 1024 * 1024).toString(),\n  TEST_STRING_EXPIRE_1: KEY_TTL,\n  TEST_STRING_KEY_2: TEST_RUN_ID + '_string_2' + CLUSTER_HASH_SLOT,\n  TEST_STRING_VALUE_2: TEST_RUN_ID + '_value_2',\n  TEST_STRING_EXPIRE_2: KEY_TTL,\n  TEST_STRING_KEY_ASCII: getASCIISafeStringFromBuffer(\n    getBufferFromSafeASCIIString(\n      TEST_RUN_ID + '_str_ascii_€' + CLUSTER_HASH_SLOT,\n    ),\n  ),\n  TEST_STRING_KEY_ASCII_BUFFER: getBufferFromSafeASCIIString(\n    TEST_RUN_ID + '_str_ascii_€' + CLUSTER_HASH_SLOT,\n  ),\n  TEST_STRING_KEY_ASCII_UNICODE:\n    TEST_RUN_ID + '_str_ascii_€' + CLUSTER_HASH_SLOT,\n  TEST_STRING_KEY_ASCII_VALUE: TEST_RUN_ID + '_value_ascii',\n  TEST_STRING_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('strk'),\n    unprintableBuf,\n  ]),\n  TEST_STRING_VALUE_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('strv'),\n    unprintableBuf,\n  ]),\n  TEST_STRING_KEY_START_1: 0,\n  TEST_STRING_KEY_START_2: 2,\n  TEST_STRING_KEY_END: 9,\n  get TEST_STRING_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_STRING_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_STRING_VALUE_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_STRING_VALUE_BIN_BUFFER_1] };\n  },\n  get TEST_STRING_PARTIAL_VALUE_BIN_BUF_OBJ_1() {\n    return {\n      type: 'Buffer',\n      data: [\n        ...this.TEST_STRING_VALUE_BIN_BUFFER_1.slice(\n          this.TEST_STRING_KEY_START_1,\n          this.TEST_STRING_KEY_END + 1,\n        ),\n      ],\n    };\n  },\n  get TEST_STRING_PARTIAL_VALUE_BIN_BUF_OBJ_2() {\n    return {\n      type: 'Buffer',\n      data: [\n        ...this.TEST_STRING_VALUE_BIN_BUFFER_1.slice(\n          this.TEST_STRING_KEY_START_2,\n          this.TEST_STRING_KEY_END + 1,\n        ),\n      ],\n    };\n  },\n  get TEST_STRING_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STRING_KEY_BIN_BUFFER_1);\n  },\n  get TEST_STRING_KEY_BIN_UTF8_1() {\n    return this.TEST_STRING_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n  get TEST_STRING_VALUE_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STRING_VALUE_BIN_BUFFER_1);\n  },\n  get TEST_STRING_VALUE_BIN_UTF8_1() {\n    return this.TEST_STRING_VALUE_BIN_BUFFER_1.toString('utf-8');\n  },\n\n  // Redis List\n  TEST_LIST_TYPE: 'list',\n  TEST_LIST_KEY_1: TEST_RUN_ID + '_list_1' + CLUSTER_HASH_SLOT,\n  TEST_LIST_ELEMENT_1: TEST_RUN_ID + '_list_el_1',\n  TEST_LIST_ELEMENT_2: TEST_RUN_ID + '_list_el_2',\n  TEST_LIST_EXPIRE_1: KEY_TTL,\n  TEST_LIST_KEY_2: TEST_RUN_ID + '_list_2' + CLUSTER_HASH_SLOT,\n  TEST_LIST_HUGE_KEY: 'big list 1M',\n  TEST_LIST_HUGE_INDEX: 678900,\n  TEST_LIST_HUGE_ELEMENT: ' 321099',\n  TEST_LIST_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('listk'),\n    unprintableBuf,\n  ]),\n  get TEST_LIST_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_LIST_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_LIST_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_LIST_KEY_BIN_BUFFER_1);\n  },\n  get TEST_LIST_KEY_BIN_UTF8_1() {\n    return this.TEST_LIST_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_LIST_ELEMENT_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('liste'),\n    unprintableBuf,\n  ]),\n  get TEST_LIST_ELEMENT_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_LIST_ELEMENT_BIN_BUFFER_1);\n  },\n  get TEST_LIST_ELEMENT_BIN_UTF8_1() {\n    return this.TEST_LIST_ELEMENT_BIN_BUFFER_1.toString('utf-8');\n  },\n  get TEST_LIST_ELEMENT_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_LIST_ELEMENT_BIN_BUFFER_1] };\n  },\n\n  // Redis Set\n  TEST_SET_TYPE: 'set',\n  TEST_SET_KEY_1: TEST_RUN_ID + '_set_1' + CLUSTER_HASH_SLOT,\n  TEST_SET_MEMBER_1: TEST_RUN_ID + '_set_mem_1',\n  TEST_SET_MEMBER_2: TEST_RUN_ID + '_set_mem_2',\n  TEST_SET_EXPIRE_1: KEY_TTL,\n  TEST_SET_KEY_2: TEST_RUN_ID + '_set_2' + CLUSTER_HASH_SLOT,\n  TEST_SET_HUGE_KEY: 'big set 1M',\n  TEST_SET_HUGE_ELEMENT: ' 356897',\n  TEST_SET_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('setk'),\n    unprintableBuf,\n  ]),\n  get TEST_SET_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_SET_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_SET_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_SET_KEY_BIN_BUFFER_1);\n  },\n  get TEST_SET_KEY_BIN_UTF8_1() {\n    return this.TEST_SET_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_SET_MEMBER_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('setm'),\n    unprintableBuf,\n  ]),\n  get TEST_SET_MEMBER_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_SET_MEMBER_BIN_BUFFER_1);\n  },\n  get TEST_SET_MEMBER_BIN_UTF8_1() {\n    return this.TEST_SET_MEMBER_BIN_BUFFER_1.toString('utf-8');\n  },\n  get TEST_SET_MEMBER_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_SET_MEMBER_BIN_BUFFER_1] };\n  },\n\n  // Redis ZSet\n  TEST_ZSET_TYPE: 'zset',\n  TEST_ZSET_KEY_1: TEST_RUN_ID + '_zset_1' + CLUSTER_HASH_SLOT,\n  TEST_ZSET_MEMBER_1: TEST_RUN_ID + '_zset_mem_1',\n  TEST_ZSET_MEMBER_1_SCORE: 0,\n  TEST_ZSET_MEMBER_2: TEST_RUN_ID + '_zset_mem_2',\n  TEST_ZSET_MEMBER_2_SCORE: 0.1,\n  TEST_ZSET_EXPIRE_1: KEY_TTL,\n  TEST_ZSET_KEY_2: TEST_RUN_ID + '_zset_2' + CLUSTER_HASH_SLOT,\n  TEST_ZSET_KEY_3: TEST_RUN_ID + '_zset_3' + CLUSTER_HASH_SLOT,\n  TEST_ZSET_HUGE_KEY: 'big zset 1M',\n  TEST_ZSET_HUGE_MEMBER: ' 356897',\n  TEST_ZSET_HUGE_SCORE: 356897,\n  TEST_ZSET_TIMESTAMP_KEY: TEST_RUN_ID + '_zset_timestamp' + CLUSTER_HASH_SLOT,\n  TEST_ZSET_TIMESTAMP_MEMBER: '1234567891',\n  TEST_ZSET_TIMESTAMP_SCORE: 1234567891,\n  TEST_ZSET_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('zsetk'),\n    unprintableBuf,\n  ]),\n  get TEST_ZSET_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_ZSET_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_ZSET_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_ZSET_KEY_BIN_BUFFER_1);\n  },\n  get TEST_ZSET_KEY_BIN_UTF8_1() {\n    return this.TEST_ZSET_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_ZSET_MEMBER_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('zsetm'),\n    unprintableBuf,\n  ]),\n  get TEST_ZSET_MEMBER_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_ZSET_MEMBER_BIN_BUFFER_1);\n  },\n  get TEST_ZSET_MEMBER_BIN_UTF8_1() {\n    return this.TEST_ZSET_MEMBER_BIN_BUFFER_1.toString('utf-8');\n  },\n  get TEST_ZSET_MEMBER_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_ZSET_MEMBER_BIN_BUFFER_1] };\n  },\n\n  // Redis Hash\n  TEST_HASH_TYPE: 'hash',\n  TEST_HASH_KEY_1: TEST_RUN_ID + '_hash_1' + CLUSTER_HASH_SLOT,\n  TEST_HASH_FIELD_1_NAME: TEST_RUN_ID + '_hash_f_1_name',\n  TEST_HASH_FIELD_1_VALUE: TEST_RUN_ID + '_hash_f_1_val',\n  TEST_HASH_FIELD_2_NAME: TEST_RUN_ID + '_hash_f_2_name',\n  TEST_HASH_FIELD_2_VALUE: TEST_RUN_ID + '_hash_f_2_val',\n  TEST_HASH_FIELD_3_NAME: TEST_RUN_ID + '_hash_f_3_name',\n  TEST_HASH_FIELD_3_VALUE: TEST_RUN_ID + '_hash_f_3_val',\n  TEST_HASH_EXPIRE_1: KEY_TTL,\n  TEST_HASH_KEY_2: TEST_RUN_ID + '_hash_2' + CLUSTER_HASH_SLOT,\n  TEST_HASH_HUGE_KEY: 'big hash 1M',\n  TEST_HASH_HUGE_KEY_FIELD: 'key678900',\n  TEST_HASH_HUGE_KEY_VALUE: ' 678900',\n  TEST_HASH_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('hashk'),\n    unprintableBuf,\n  ]),\n  get TEST_HASH_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_HASH_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_HASH_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_HASH_KEY_BIN_BUFFER_1);\n  },\n  get TEST_HASH_KEY_BIN_UTF8_1() {\n    return this.TEST_HASH_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_HASH_FIELD_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('hashf'),\n    unprintableBuf,\n  ]),\n  get TEST_HASH_FIELD_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_HASH_FIELD_BIN_BUFFER_1] };\n  },\n  get TEST_HASH_FIELD_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_HASH_FIELD_BIN_BUFFER_1);\n  },\n  get TEST_HASH_FIELD_BIN_UTF8_1() {\n    return this.TEST_HASH_FIELD_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_HASH_VALUE_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('hashv'),\n    unprintableBuf,\n  ]),\n  get TEST_HASH_VALUE_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_HASH_VALUE_BIN_BUFFER_1] };\n  },\n  get TEST_HASH_VALUE_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_HASH_VALUE_BIN_BUFFER_1);\n  },\n  get TEST_HASH_VALUE_BIN_UTF8_1() {\n    return this.TEST_HASH_VALUE_BIN_BUFFER_1.toString('utf-8');\n  },\n\n  // Redis Stream\n  TEST_STREAM_TYPE: 'stream',\n  TEST_STREAM_KEY_1: TEST_RUN_ID + '_stream_1' + CLUSTER_HASH_SLOT,\n  TEST_STREAM_KEY_2: TEST_RUN_ID + '_stream_2' + CLUSTER_HASH_SLOT,\n  TEST_STREAM_DATA_1: TEST_RUN_ID + '_stream_data_1',\n  TEST_STREAM_DATA_2: TEST_RUN_ID + '_stream_data_2',\n  TEST_STREAM_ID_1: '100-0',\n  TEST_STREAM_FIELD_1: TEST_RUN_ID + '_stream_field_1',\n  TEST_STREAM_VALUE_1: TEST_RUN_ID + '_stream_value_1',\n  TEST_STREAM_ID_2: '200-0',\n  TEST_STREAM_ID_3: '300-0',\n  TEST_STREAM_ID_4: '400-0',\n  TEST_STREAM_FIELD_2: TEST_RUN_ID + '_stream_field_2',\n  TEST_STREAM_VALUE_2: TEST_RUN_ID + '_stream_value_2',\n  TEST_STREAM_EXPIRE_1: KEY_TTL,\n  TEST_STREAM_HUGE_KEY: TEST_RUN_ID + '_stream_huge' + CLUSTER_HASH_SLOT,\n  TEST_STREAM_GROUP_1: TEST_RUN_ID + '_stream_group_1',\n  TEST_STREAM_CONSUMER_1: TEST_RUN_ID + '_stream_consumer_1',\n  TEST_STREAM_GROUP_2: TEST_RUN_ID + '_stream_group_2',\n  TEST_STREAM_CONSUMER_2: TEST_RUN_ID + '_stream_consumer_2',\n  TEST_STREAM_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('streamk'),\n    unprintableBuf,\n  ]),\n  get TEST_STREAM_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_STREAM_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_STREAM_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STREAM_KEY_BIN_BUFFER_1);\n  },\n  get TEST_STREAM_KEY_BIN_UTF8_1() {\n    return this.TEST_STREAM_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_STREAM_FIELD_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('streamf'),\n    unprintableBuf,\n  ]),\n  get TEST_STREAM_FIELD_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_STREAM_FIELD_BIN_BUFFER_1] };\n  },\n  get TEST_STREAM_FIELD_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STREAM_FIELD_BIN_BUFFER_1);\n  },\n  get TEST_STREAM_FIELD_BIN_UTF8_1() {\n    return this.TEST_STREAM_FIELD_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_STREAM_VALUE_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('streamv'),\n    unprintableBuf,\n  ]),\n  get TEST_STREAM_VALUE_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_STREAM_VALUE_BIN_BUFFER_1] };\n  },\n  get TEST_STREAM_VALUE_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STREAM_VALUE_BIN_BUFFER_1);\n  },\n  get TEST_STREAM_VALUE_BIN_UTF8_1() {\n    return this.TEST_STREAM_VALUE_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_STREAM_GROUP_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('streamg'),\n    unprintableBuf,\n  ]),\n  get TEST_STREAM_GROUP_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_STREAM_GROUP_BIN_BUFFER_1] };\n  },\n  get TEST_STREAM_GROUP_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STREAM_GROUP_BIN_BUFFER_1);\n  },\n  get TEST_STREAM_GROUP_BIN_UTF8_1() {\n    return this.TEST_STREAM_GROUP_BIN_BUFFER_1.toString('utf-8');\n  },\n  TEST_STREAM_CONSUMER_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('streamc'),\n    unprintableBuf,\n  ]),\n  get TEST_STREAM_CONSUMER_BIN_BUF_OBJ_1() {\n    return {\n      type: 'Buffer',\n      data: [...this.TEST_STREAM_CONSUMER_BIN_BUFFER_1],\n    };\n  },\n  get TEST_STREAM_CONSUMER_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_STREAM_CONSUMER_BIN_BUFFER_1);\n  },\n  get TEST_STREAM_CONSUMER_BIN_UTF8_1() {\n    return this.TEST_STREAM_CONSUMER_BIN_BUFFER_1.toString('utf-8');\n  },\n\n  // ReJSON-RL\n  TEST_REJSON_TYPE: 'ReJSON-RL',\n  TEST_REJSON_KEY_1: TEST_RUN_ID + '_rejson_1' + CLUSTER_HASH_SLOT,\n  TEST_REJSON_VALUE_1: { test: 'value' },\n  TEST_REJSON_EXPIRE_1: KEY_TTL,\n  TEST_REJSON_KEY_2: TEST_RUN_ID + '_rejson_2' + CLUSTER_HASH_SLOT,\n  TEST_REJSON_VALUE_2: [{ obj: 1 }],\n  TEST_REJSON_EXPIRE_2: KEY_TTL,\n  TEST_REJSON_KEY_3: TEST_RUN_ID + '_rejson_3' + CLUSTER_HASH_SLOT,\n  TEST_REJSON_VALUE_3: {\n    array: [{ obj: 1 }, 2, 3],\n    object: { some: randomBytes(1024).toString('hex'), field: 'value' },\n  },\n  TEST_REJSON_EXPIRE_3: KEY_TTL,\n  TEST_REJSON_KEY_BIN_BUFFER_1: Buffer.concat([\n    Buffer.from(TEST_RUN_ID),\n    Buffer.from('setk'),\n    unprintableBuf,\n  ]),\n  get TEST_REJSON_KEY_BIN_BUF_OBJ_1() {\n    return { type: 'Buffer', data: [...this.TEST_REJSON_KEY_BIN_BUFFER_1] };\n  },\n  get TEST_REJSON_KEY_BIN_ASCII_1() {\n    return getASCIISafeStringFromBuffer(this.TEST_REJSON_KEY_BIN_BUFFER_1);\n  },\n  get TEST_REJSON_KEY_BIN_UTF8_1() {\n    return this.TEST_REJSON_KEY_BIN_BUFFER_1.toString('utf-8');\n  },\n\n  // TSDB-TYPE\n  TEST_TS_TYPE: 'TSDB-TYPE',\n  TEST_TS_KEY_1: TEST_RUN_ID + '_ts_1' + CLUSTER_HASH_SLOT,\n  TEST_TS_TIMESTAMP_1: 1627537290803,\n  TEST_TS_VALUE_1: 10,\n  TEST_TS_TIMESTAMP_2: 1627537290804,\n  TEST_TS_VALUE_2: 20,\n\n  // Graph\n  TEST_GRAPH_TYPE: 'graphdata',\n  TEST_GRAPH_KEY_1: TEST_RUN_ID + '_graph_1' + CLUSTER_HASH_SLOT,\n  TEST_GRAPH_NODE_1: TEST_RUN_ID + 'n1',\n  TEST_GRAPH_NODE_2: TEST_RUN_ID + 'n2',\n\n  // RediSearch\n  TEST_SEARCH_HASH_TYPE: 'hash',\n  TEST_SEARCH_HASH_INDEX_1:\n    TEST_RUN_ID + '_hash_search_idx_1' + CLUSTER_HASH_SLOT,\n  TEST_SEARCH_HASH_KEY_PREFIX_1: TEST_RUN_ID + '_hash_search:',\n  TEST_SEARCH_HASH_INDEX_2:\n    TEST_RUN_ID + '_hash_search_idx_2' + CLUSTER_HASH_SLOT,\n  TEST_SEARCH_HASH_KEY_PREFIX_2: TEST_RUN_ID + '_hash_search:',\n  TEST_SEARCH_JSON_INDEX_1:\n    TEST_RUN_ID + '_json_search_idx_1' + CLUSTER_HASH_SLOT,\n  TEST_SEARCH_JSON_KEY_PREFIX_1: TEST_RUN_ID + '_json_search:',\n\n  // Command Executions\n  TEST_COMMAND_EXECUTION_ID_1: uuidv4(),\n\n  // Plugins\n  TEST_PLUGIN_VISUALIZATION_ID_1: uuidv4(),\n\n  // Pub/Sub\n  TEST_PUB_SUB_CHANNEL_1: 'channel-a',\n  TEST_PUB_SUB_CHANNEL_2: 'channel-b',\n  TEST_PUB_SUB_CHANNEL_3: 'channel-c',\n  TEST_PUB_SUB_P_CHANNEL_1: '*',\n  TEST_PUB_SUB_MESSAGE_1: 'message-a',\n  TEST_PUB_SUB_MESSAGE_2: 'message-b',\n  TEST_PUB_SUB_MESSAGE_3: 'message-c',\n\n  // Notifications\n  TEST_NOTIFICATION_1: {\n    timestamp: 1656054100,\n    title: 'Title-1',\n    category: 'Release',\n    categoryColor: '#ea14fd',\n    body: 'Body-1',\n    read: false,\n    type: 'global',\n  },\n  TEST_NOTIFICATION_2: {\n    timestamp: 1656054200,\n    title: 'Title-2',\n    category: 'News',\n    categoryColor: null,\n    body: 'Body-2',\n    read: false,\n    type: 'global',\n  },\n  TEST_NOTIFICATION_3: {\n    timestamp: 1656054300,\n    title: 'Title-3',\n    category: null,\n    categoryColor: null,\n    body: 'Body-3',\n    read: true,\n    type: 'global',\n  },\n  TEST_NOTIFICATION_NE_1: {\n    timestamp: 1656054101,\n    title: 'Title-1',\n    body: 'Body-1',\n    read: false,\n    type: 'global',\n  },\n  TEST_NOTIFICATION_NE_2: {\n    timestamp: 1656054201,\n    title: 'Title-2',\n    body: 'Body-2',\n    read: false,\n    type: 'global',\n  },\n  TEST_NOTIFICATION_NE_3: {\n    timestamp: 1656054303,\n    title: 'Title-3',\n    body: 'Body-3',\n    read: true,\n    type: 'global',\n  },\n\n  // Database Analysis\n  TEST_DATABASE_ANALYSIS_ID_1: uuidv4(),\n  TEST_DATABASE_ANALYSIS_CREATED_AT_1: new Date(),\n  TEST_DATABASE_ANALYSIS_DELIMITER_1: ':',\n  TEST_DATABASE_ANALYSIS_DB_1: 2,\n  TEST_DATABASE_ANALYSIS_FILTER_1: {\n    type: null,\n    match: '*',\n    count: 10_000,\n  },\n  TEST_DATABASE_ANALYSIS_PROGRESS_1: {\n    total: 1_000_000,\n    scanned: 10_000,\n    processed: 10_000,\n  },\n  TEST_DATABASE_ANALYSIS_TOTAL_KEYS_1: {\n    total: 10_000,\n    types: [\n      {\n        type: 'string',\n        total: 50_000,\n      },\n      {\n        type: 'list',\n        total: 50_000,\n      },\n    ],\n  },\n  TEST_DATABASE_ANALYSIS_TOTAL_MEMORY_1: {\n    total: 10_000_000,\n    types: [\n      {\n        type: 'string',\n        total: 5_000_000,\n      },\n      {\n        type: 'list',\n        total: 5_000_000,\n      },\n    ],\n  },\n  TEST_DATABASE_ANALYSIS_TOP_KEYS_NSP_1: {\n    nsp: 'Namespace',\n    memory: 10_000_000,\n    keys: 10_000_000,\n    types: [\n      {\n        type: 'string',\n        keys: 5_000,\n        memory: 5_000_000,\n      },\n      {\n        type: 'list',\n        keys: 5_000,\n        memory: 5_000_000,\n      },\n    ],\n  },\n  TEST_DATABASE_ANALYSIS_TOP_MEMORY_NSP_1: {\n    nsp: 'Namespace',\n    memory: 10_000_000,\n    keys: 10_000_000,\n    types: [\n      {\n        type: 'string',\n        keys: 5_000,\n        memory: 5_000_000,\n      },\n      {\n        type: 'list',\n        keys: 5_000,\n        memory: 5_000_000,\n      },\n    ],\n  },\n  TEST_DATABASE_ANALYSIS_TOP_KEYS_1: {\n    name: 'Key Name',\n    type: 'string',\n    memory: 1_000,\n    length: 1_000,\n    ttl: -1,\n  },\n  TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1: {\n    label: '1-4 Hrs',\n    total: 10_000_000,\n    threshold: 4 * 60 * 60 * 1000,\n  },\n\n  // recommendations\n  TEST_RECOMMENDATIONS_DATABASE_ID: uuidv4(),\n  TEST_RECOMMENDATION_ID_1: uuidv4(),\n  TEST_RECOMMENDATION_ID_2: uuidv4(),\n  TEST_RECOMMENDATION_ID_3: uuidv4(),\n\n  TEST_RECOMMENDATION_VOTE: Vote.Like,\n  TEST_RECOMMENDATION_HIDE: true,\n\n  TEST_RECOMMENDATION_NAME_1: RECOMMENDATION_NAMES.BIG_SETS,\n  TEST_RECOMMENDATION_NAME_2: RECOMMENDATION_NAMES.BIG_STRINGS,\n  TEST_RECOMMENDATION_NAME_3: RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,\n\n  TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.LUA_SCRIPT,\n  },\n  TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.BIG_HASHES,\n  },\n  TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.USE_SMALLER_KEYS,\n  },\n  TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES,\n  },\n  TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES,\n  },\n  TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST,\n  },\n  TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES,\n  },\n  TEST_COMPRESSION_FOR_LIST_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST,\n  },\n  TEST_BIG_STRINGS_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.BIG_STRINGS,\n  },\n\n  TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST,\n  },\n\n  TEST_BIG_SETS_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.BIG_SETS,\n  },\n\n  TEST_SET_PASSWORD_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.SET_PASSWORD,\n  },\n\n  TEST_RTS_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.RTS,\n  },\n\n  TEST_REDIS_VERSION_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.REDIS_VERSION,\n  },\n\n  TEST_SEARCH_JSON_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.SEARCH_JSON,\n  },\n\n  TEST_SEARCH_INDEXES_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.SEARCH_INDEXES,\n  },\n\n  TEST_SEARCH_HASH_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.SEARCH_HASH,\n  },\n\n  TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: {\n    name: RECOMMENDATION_NAMES.LUA_SCRIPT,\n    vote: 'useful',\n  },\n  TEST_BROWSER_HISTORY_DATABASE_ID: uuidv4(),\n  TEST_BROWSER_HISTORY_ID_1: uuidv4(),\n  TEST_BROWSER_HISTORY_ID_2: uuidv4(),\n  TEST_BROWSER_HISTORY_ID_3: uuidv4(),\n  TEST_BROWSER_HISTORY_FILTER_1: {\n    type: null,\n    match: 'hi',\n  },\n  TEST_BROWSER_HISTORY_FILTER_2: {\n    type: null,\n    match: 'hi',\n  },\n  TEST_TRIGGERED_FUNCTIONS_LIBRARY_NAME: TEST_LIBRARY_NAME,\n  TEST_TRIGGERED_FUNCTIONS_CODE: `#!js api_version=1.0 name=${TEST_LIBRARY_NAME}\\n redis.registerFunction('foo', ()=>{return 'bar'})`,\n  TEST_TRIGGERED_FUNCTIONS_CONFIGURATION: '{}',\n\n  TEST_ANALYTICS_EVENT: TelemetryEvents.RedisInstanceAdded,\n  TEST_ANALYTICS_EVENT_DATA: { length: 5 },\n  TEST_ANALYTICS_PAGE,\n\n  TEST_DATA_DIR: process.env.TEST_DATA_DIR || `${TEST_RUN_DIR}/data`,\n  // etc...\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/data/redis.ts",
    "content": "import { get } from 'lodash';\nimport { constants } from '../constants';\nimport * as _ from 'lodash';\nimport * as IORedis from 'ioredis';\nimport { sleep } from '../test';\nimport { convertRedisInfoReplyToObject } from 'src/utils';\nimport { convertMultilineReplyToObject } from 'src/modules/redis/utils';\n\nexport const initDataHelper = (rte) => {\n  const client = rte.client;\n\n  const sendCommand = async (\n    command: string,\n    args?: (Buffer | string)[],\n    replyEncoding: BufferEncoding = 'utf8',\n  ): Promise<any> => {\n    return client.sendCommand(\n      new IORedis.Command(command, args, {\n        replyEncoding,\n      }),\n    );\n  };\n\n  const executeCommand = async (...args: string[]): Promise<any> => {\n    return client.nodes\n      ? Promise.all(\n          client.nodes('master').map(async (node) => {\n            try {\n              return node.call(...args);\n            } catch (e) {\n              return null;\n            }\n          }),\n        )\n      : client.call(args.shift(), ...args);\n  };\n\n  const waitForInfoSync = async () => {\n    let totalKeys = 0;\n    let dbSize = -1;\n\n    while (true) {\n      const currentDbIndex = get(client, ['options', 'db'], 0);\n\n      dbSize = await sendCommand('dbsize');\n\n      const info = convertRedisInfoReplyToObject(\n        await sendCommand('info', ['keyspace']),\n      );\n      const dbInfo = get(info, 'keyspace', {});\n      if (dbInfo[`db${currentDbIndex}`]) {\n        const { keys } = convertMultilineReplyToObject(\n          dbInfo[`db${currentDbIndex}`],\n          ',',\n          '=',\n        );\n        totalKeys = parseInt(keys, 10);\n      }\n\n      if (dbSize === totalKeys) {\n        break;\n      }\n\n      await sleep(200);\n    }\n  };\n\n  const executeCommandAll = async (...args: string[]): Promise<any> => {\n    return client.nodes\n      ? Promise.all(\n          client.nodes().map(async (node) => {\n            try {\n              return node.call(...args);\n            } catch (e) {\n              return null;\n            }\n          }),\n        )\n      : client.call(args.shift(), ...args);\n  };\n\n  const setAclUserRules = async (rules: string): Promise<any> => {\n    const command = `ACL SETUSER ${constants.TEST_INSTANCE_ACL_USER} reset on ${rules} >${constants.TEST_INSTANCE_ACL_PASS}`;\n\n    return executeCommandAll(...command.split(' '));\n  };\n\n  const flushTestRunData = async (node) => {\n    if (!constants.TEST_RTE_SHARED_DATA) {\n      return node.flushall();\n    }\n\n    // 5M count looks like \"too much\" but each test run should generate even less then 100 keys\n    // we want to not wait for a long time when run tests on huge databases (currently ~4M keys)\n    const count = constants.TEST_RTE_BIG_DATA ? 5_000_000 : 10_000;\n    let cursor = null;\n    let keys = [];\n    while (cursor !== '0') {\n      [cursor, keys] = await node.sendCommand(\n        new IORedis.Command('scan', [\n          cursor,\n          'count',\n          count,\n          'match',\n          `${constants.TEST_RUN_ID}*`,\n        ]),\n      );\n      cursor = cursor.toString();\n      if (keys.length) {\n        await Promise.all(\n          keys.map((key) =>\n            node.sendCommand(new IORedis.Command('del', [key])),\n          ),\n        );\n      }\n    }\n  };\n\n  const truncate = async () => {\n    return client.nodes\n      ? Promise.all(\n          client.nodes('master').map(async (node) => {\n            try {\n              return flushTestRunData(node);\n            } catch (e) {\n              return null;\n            }\n          }),\n        )\n      : flushTestRunData(client);\n  };\n\n  // bin data\n  const generateBinKeys = async (clean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    // string\n    await client.set(\n      constants.TEST_STRING_KEY_BIN_BUFFER_1,\n      constants.TEST_STRING_VALUE_BIN_BUFFER_1,\n    );\n\n    // list\n    await client.lpush(\n      constants.TEST_LIST_KEY_BIN_BUFFER_1,\n      constants.TEST_LIST_ELEMENT_BIN_BUFFER_1,\n    );\n\n    // set\n    await client.sadd(\n      constants.TEST_SET_KEY_BIN_BUFFER_1,\n      constants.TEST_SET_MEMBER_BIN_BUFFER_1,\n    );\n\n    // zset\n    await client.zadd(\n      constants.TEST_ZSET_KEY_BIN_BUFFER_1,\n      constants.TEST_ZSET_MEMBER_1_SCORE,\n      constants.TEST_ZSET_MEMBER_BIN_BUFFER_1,\n    );\n\n    // hash\n    await client.hset(\n      constants.TEST_HASH_KEY_BIN_BUFFER_1,\n      constants.TEST_HASH_FIELD_BIN_BUFFER_1,\n      constants.TEST_HASH_VALUE_BIN_BUFFER_1,\n    );\n\n    // stream\n    await client.xadd(\n      constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n      '*',\n      constants.TEST_STREAM_FIELD_BIN_BUFFER_1,\n      constants.TEST_STREAM_VALUE_BIN_BUFFER_1,\n    );\n    await sendCommand('xgroup', [\n      'create',\n      constants.TEST_STREAM_KEY_BIN_BUFFER_1,\n      constants.TEST_STREAM_GROUP_BIN_BUFFER_1,\n      constants.TEST_STREAM_ID_1,\n    ]);\n\n    await waitForInfoSync();\n  };\n\n  // keys\n  const generateKeys = async (clean: boolean) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await generateStrings();\n    await generateLists();\n    await generateSets();\n    await generateZSets();\n    await generateHashes();\n    await generateReJSONs();\n    await generateStreams();\n\n    await waitForInfoSync();\n  };\n\n  const insertKeysBasedOnEnv = async (\n    pipeline,\n    forcePipeline: boolean = false,\n  ) => {\n    const builtInCommand = client.getBuiltinCommands().includes(pipeline[0][0]);\n    if (!forcePipeline && (!builtInCommand || rte.env.type === 'CLUSTER')) {\n      for (const command of pipeline) {\n        try {\n          await executeCommand(...command); // todo: implement performant way to insert keys for Cluster nodes\n        } catch (e) {\n          if (!e.message.includes('MOVED') && !e.message.includes('ASK')) {\n            throw e;\n          }\n        }\n      }\n    } else {\n      await client.pipeline(pipeline).exec();\n    }\n  };\n\n  const generateAnyKeys = async (\n    types: Array<any>,\n    number: number = 15000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const numberPerType = Math.floor(number / types.length);\n\n    for (let i = 0; i < types.length; i++) {\n      await insertKeysBasedOnEnv(types[i].create(numberPerType));\n    }\n  };\n\n  // Strings\n  const generateString = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.set(\n      constants.TEST_STRING_KEY_1,\n      constants.TEST_STRING_VALUE_1,\n    );\n  };\n\n  const generateStrings = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.set(\n      constants.TEST_STRING_KEY_1,\n      constants.TEST_STRING_VALUE_1,\n    );\n    await client.set(\n      constants.TEST_STRING_KEY_2,\n      constants.TEST_STRING_VALUE_2,\n      'EX',\n      constants.TEST_STRING_EXPIRE_2,\n    );\n    await client.set(\n      constants.TEST_STRING_KEY_ASCII_BUFFER,\n      constants.TEST_STRING_KEY_ASCII_VALUE,\n    );\n  };\n\n  // List\n  const generateLists = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.lpush(\n      constants.TEST_LIST_KEY_1,\n      constants.TEST_LIST_ELEMENT_2,\n      constants.TEST_LIST_ELEMENT_1,\n    );\n    await client.rpush(\n      constants.TEST_LIST_KEY_2,\n      ...new Array(100).fill(0).map((item, i) => `element_${i + 1}`),\n    );\n  };\n\n  const generateHugeElementsForListKey = async (\n    number: number = 100000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const batchSize = 10000;\n    let inserted = 0;\n    do {\n      const pipeline = [];\n      const limit = inserted + batchSize;\n      for (inserted; inserted < limit && inserted < number; inserted++) {\n        pipeline.push(['lpush', constants.TEST_LIST_KEY_1, inserted]);\n      }\n\n      await insertKeysBasedOnEnv(pipeline, true);\n    } while (inserted < number);\n  };\n\n  // Set\n  const generateSets = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.sadd(constants.TEST_SET_KEY_1, constants.TEST_SET_MEMBER_1);\n    await client.sadd(\n      constants.TEST_SET_KEY_2,\n      ...new Array(100).fill(0).map((item, i) => `member_${i + 1}`),\n    );\n  };\n\n  // ZSet\n  const generateZSets = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.zadd(\n      constants.TEST_ZSET_KEY_1,\n      constants.TEST_ZSET_MEMBER_1_SCORE,\n      constants.TEST_ZSET_MEMBER_1,\n      constants.TEST_ZSET_MEMBER_2_SCORE,\n      constants.TEST_ZSET_MEMBER_2,\n    );\n\n    await client.zadd(\n      constants.TEST_ZSET_KEY_2,\n      ...(() => {\n        const toInsert = [];\n        new Array(100).fill(0).map((item, i) => {\n          toInsert.push(i + 1, `member_${i + 1}`);\n        });\n        return toInsert;\n      })(),\n    );\n    await client.zadd(\n      constants.TEST_ZSET_KEY_3,\n      ...(() => {\n        const toInsert = [];\n        new Array(3000).fill(0).map((item, i) => {\n          toInsert.push(i + 1, `member_${i + 1}`);\n        });\n        return toInsert;\n      })(),\n    );\n  };\n\n  const generateHugeMembersForSortedListKey = async (\n    number: number = 100000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const batchSize = 10000;\n    let inserted = 0;\n    do {\n      const pipeline = [];\n      const limit = inserted + batchSize;\n      for (inserted; inserted < limit && inserted < number; inserted++) {\n        pipeline.push(['zadd', constants.TEST_ZSET_KEY_1, inserted, inserted]);\n      }\n\n      await insertKeysBasedOnEnv(pipeline, true);\n    } while (inserted < number);\n  };\n\n  // Hash\n  const generateHashes = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.hset(\n      constants.TEST_HASH_KEY_1,\n      constants.TEST_HASH_FIELD_1_NAME,\n      constants.TEST_HASH_FIELD_1_VALUE,\n      constants.TEST_HASH_FIELD_2_NAME,\n      constants.TEST_HASH_FIELD_2_VALUE,\n    );\n    await client.hset(\n      constants.TEST_HASH_KEY_2,\n      ...(() => {\n        const toInsert = [];\n        new Array(3000).fill(0).map((item, i) => {\n          toInsert.push(`field_${i + 1}`, `value_${i + 1}`);\n        });\n        return toInsert;\n      })(),\n    );\n  };\n\n  // ReJSON-RL\n  const generateReJSONs = async (clean: boolean = false) => {\n    if (!get(rte, ['env', 'modules', 'rejson'])) {\n      return;\n    }\n\n    if (clean) {\n      await truncate();\n    }\n\n    await executeCommand(\n      'json.set',\n      constants.TEST_REJSON_KEY_1,\n      '.',\n      JSON.stringify(constants.TEST_REJSON_VALUE_1),\n    );\n    await executeCommand(\n      'json.set',\n      constants.TEST_REJSON_KEY_2,\n      '.',\n      JSON.stringify(constants.TEST_REJSON_VALUE_2),\n    );\n    await executeCommand(\n      'json.set',\n      constants.TEST_REJSON_KEY_3,\n      '.',\n      JSON.stringify(constants.TEST_REJSON_VALUE_3),\n    );\n  };\n\n  // Streams\n  const generateStreams = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.xadd(\n      constants.TEST_STREAM_KEY_1,\n      '*',\n      constants.TEST_STREAM_FIELD_1,\n      constants.TEST_STREAM_VALUE_1,\n    );\n    await sendCommand('xgroup', [\n      'create',\n      constants.TEST_STREAM_KEY_1,\n      constants.TEST_STREAM_GROUP_1,\n      constants.TEST_STREAM_ID_1,\n    ]);\n    await sendCommand('xgroup', [\n      'create',\n      constants.TEST_STREAM_KEY_1,\n      constants.TEST_STREAM_GROUP_2,\n      constants.TEST_STREAM_ID_1,\n    ]);\n    await client.xadd(\n      constants.TEST_STREAM_KEY_2,\n      '*',\n      constants.TEST_STREAM_FIELD_1,\n      constants.TEST_STREAM_VALUE_1,\n    );\n  };\n\n  const generateStreamsWithoutStrictMode = async (clean: boolean = false) => {\n    if (clean) {\n      await truncate();\n    }\n\n    await client.xadd(\n      constants.TEST_STREAM_KEY_1,\n      constants.TEST_STREAM_ID_1,\n      constants.TEST_STREAM_FIELD_1,\n      constants.TEST_STREAM_VALUE_1,\n    );\n    await sendCommand('xgroup', [\n      'create',\n      constants.TEST_STREAM_KEY_1,\n      constants.TEST_STREAM_GROUP_1,\n      constants.TEST_STREAM_ID_1,\n    ]);\n    await sendCommand('xgroup', [\n      'create',\n      constants.TEST_STREAM_KEY_1,\n      constants.TEST_STREAM_GROUP_2,\n      constants.TEST_STREAM_ID_1,\n    ]);\n    await client.xadd(\n      constants.TEST_STREAM_KEY_2,\n      constants.TEST_STREAM_ID_1,\n      constants.TEST_STREAM_FIELD_1,\n      constants.TEST_STREAM_VALUE_1,\n    );\n  };\n\n  const generateHugeStream = async (\n    number: number = 100000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const batchSize = 10000;\n    let inserted = 0;\n    do {\n      const pipeline = [];\n      const limit = inserted + batchSize;\n      for (inserted; inserted < limit && inserted < number; inserted++) {\n        pipeline.push([\n          'xadd',\n          `${constants.TEST_STREAM_HUGE_KEY}`,\n          '*',\n          `f_${inserted}`,\n          `v_${inserted}`,\n        ]);\n      }\n\n      await insertKeysBasedOnEnv(pipeline);\n    } while (inserted < number);\n  };\n\n  const generateHugeNumberOfFieldsForHashKey = async (\n    number: number = 100000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const batchSize = 10000;\n    let inserted = 0;\n    do {\n      const pipeline = [];\n      const limit = inserted + batchSize;\n      for (inserted; inserted < limit && inserted < number; inserted++) {\n        pipeline.push([\n          'hset',\n          constants.TEST_HASH_KEY_1,\n          `f_${inserted}`,\n          'v',\n        ]);\n      }\n\n      await insertKeysBasedOnEnv(pipeline, true);\n    } while (inserted < number);\n  };\n\n  const generateHugeNumberOfMembersForSetKey = async (\n    number: number = 100000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const batchSize = 10000;\n    let inserted = 0;\n    do {\n      const pipeline = [];\n      const limit = inserted + batchSize;\n      for (inserted; inserted < limit && inserted < number; inserted++) {\n        pipeline.push(['sadd', constants.TEST_SET_KEY_1, inserted]);\n      }\n\n      await insertKeysBasedOnEnv(pipeline, true);\n    } while (inserted < number);\n  };\n\n  const generateHugeNumberOfTinyStringKeys = async (\n    number: number = 100000,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const batchSize = 10000;\n    let inserted = 0;\n    do {\n      const pipeline = [];\n      const limit = inserted + batchSize;\n      for (inserted; inserted < limit && inserted < number; inserted++) {\n        pipeline.push(['set', `${constants.TEST_RUN_ID}_${inserted}`, 'v']);\n      }\n\n      await insertKeysBasedOnEnv(pipeline);\n    } while (inserted < number);\n  };\n\n  const generateNKeys = async (number: number = 15000, clean: boolean) => {\n    await generateAnyKeys(\n      [\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'set',\n              `${constants.TEST_RUN_ID}_str_key_${i}`,\n              `str_val_${i}`,\n            ]),\n        }, // string\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'lpush',\n              `${constants.TEST_RUN_ID}_list_key_${i}`,\n              `list_val_${i}`,\n            ]),\n        }, // list\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'sadd',\n              `${constants.TEST_RUN_ID}_set_key_${i}`,\n              `set_val_${i}`,\n            ]),\n        }, // set\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'zadd',\n              `${constants.TEST_RUN_ID}_zset_key_${i}`,\n              0,\n              `zset_val_${i}`,\n            ]),\n        }, // zset\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'hset',\n              `${constants.TEST_RUN_ID}_hash_key_${i}`,\n              `field`,\n              `hash_val_${i}`,\n            ]),\n        }, // hash\n      ],\n      number,\n      clean,\n    );\n\n    await waitForInfoSync();\n  };\n\n  const generateRedisearchIndexes = async (clean: boolean) => {\n    await generateNKeys(10_000, clean);\n\n    await sendCommand('ft.create', [\n      constants.TEST_SEARCH_HASH_INDEX_1,\n      'on',\n      'hash',\n      'schema',\n      'field',\n      'text',\n    ]);\n    await sendCommand('ft.create', [\n      constants.TEST_SEARCH_HASH_INDEX_2,\n      'on',\n      'hash',\n      'schema',\n      '*',\n      'text',\n    ]);\n\n    // Indexes creation needs some additional time to complete, which usually is around 500ms\n    await waitIndexingToComplete([\n      constants.TEST_SEARCH_HASH_INDEX_1,\n      constants.TEST_SEARCH_HASH_INDEX_2,\n    ]);\n  };\n\n  /**\n   * Checks periodically (`retryLimit` times, every `retryInterval` milliseconds) if the creation of the `hashIndexes` has completed.\n   *\n   * @param {string[]} indexes - string array containing the hashes of the indexes.\n   * @param {number} [retryLimit=3] - the maximum number of iterations. Defaults to 3.\n   * @param {number} [retryInterval=300] - the time wait between iterations, in milliseconds. Defaults to 300.\n   */\n  const waitIndexingToComplete = async (\n    hashIndexes: string[],\n    retryLimit = 3,\n    retryInterval = 300,\n  ) => {\n    let indexesCompleted = new Array(hashIndexes.length).fill(false);\n    for (let retryCounter = 0; retryCounter < retryLimit; retryCounter++) {\n      await new Promise((resolve) => setTimeout(resolve, retryInterval));\n\n      for (let i = 0; i < hashIndexes.length; i++) {\n        // ft.info command returns an array which contains data that shows wether the indexing is in progress\n        // it looks something like this: [\"index_name\", \"the_index_name\", ... \"indexing\", 0, ...]\n        const indexInfo = await sendCommand('ft.info', [hashIndexes[i]]);\n\n        // searching for the \"indexing\" property index, so we can reach it's value using index + 1 in the info array\n        const indexingPropertyIndex = indexInfo.indexOf('indexing');\n        const indexingValue = indexInfo[indexingPropertyIndex + 1]; // 1 - in progress, 0 - completed\n        indexesCompleted[i] = !indexingValue;\n      }\n\n      if (!indexesCompleted.includes(false)) {\n        break;\n      }\n    }\n\n    if (indexesCompleted.includes(false)) {\n      console.error('Indexing has not yet completed');\n    }\n  };\n\n  const generateNReJSONs = async (number: number = 300, clean: boolean) => {\n    const jsonValue = JSON.stringify(constants.TEST_REJSON_VALUE_1);\n    await generateAnyKeys(\n      [\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'json.set',\n              `${constants.TEST_RUN_ID}_rejson_key_${i}`,\n              '.',\n              jsonValue,\n            ]),\n        },\n      ],\n      number,\n      clean,\n    );\n  };\n\n  const generateNTimeSeries = async (number: number = 300, clean: boolean) => {\n    await generateAnyKeys(\n      [\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'ts.create',\n              `${constants.TEST_RUN_ID}_ts_key_${i}`,\n              `ts_val_${i}`,\n            ]),\n        },\n      ],\n      number,\n      clean,\n    );\n  };\n\n  const generateNStreams = async (number: number = 300, clean: boolean) => {\n    await generateAnyKeys(\n      [\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'xadd',\n              `${constants.TEST_RUN_ID}_st_key_${i}`,\n              `*`,\n              `st_field_${i}`,\n              `st_val_${i}`,\n            ]),\n        },\n      ],\n      number,\n      clean,\n    );\n  };\n\n  const generateNGraphs = async (number: number = 300, clean: boolean) => {\n    await generateAnyKeys(\n      [\n        {\n          create: (n) =>\n            _.map(new Array(n), (v, i) => [\n              'graph.query',\n              `${constants.TEST_RUN_ID}_graph_key_${i}`,\n              `CREATE (n_${i})`,\n            ]),\n        },\n      ],\n      number,\n      clean,\n    );\n  };\n\n  const getClientNodes = () => {\n    if (client.nodes) {\n      return client.nodes();\n    } else {\n      return [client];\n    }\n  };\n\n  // scripts\n  const generateNCachedScripts = async (\n    number: number = 10,\n    clean: boolean,\n  ) => {\n    if (clean) {\n      await truncate();\n    }\n\n    const pipeline = [];\n    for (let i = 0; i < number; i++) {\n      pipeline.push(['eval', `return ${i}`, '0']);\n    }\n    await insertKeysBasedOnEnv(pipeline);\n  };\n\n  const setRedisearchConfig = async (\n    rule: string,\n    value: string,\n  ): Promise<any> => {\n    const command = `FT.CONFIG SET ${rule} ${value}`;\n\n    return executeCommand(...command.split(' '));\n  };\n\n  const generateTriggeredFunctionsLibrary = async (\n    clean: boolean = true,\n  ): Promise<any> => {\n    if (clean) {\n      await truncate();\n    }\n\n    await sendCommand('TFUNCTION', [\n      'LOAD',\n      constants.TEST_TRIGGERED_FUNCTIONS_CODE,\n      constants.TEST_TRIGGERED_FUNCTIONS_CONFIGURATION,\n    ]);\n  };\n\n  return {\n    sendCommand,\n    executeCommand,\n    executeCommandAll,\n    setAclUserRules,\n    truncate,\n    generateBinKeys,\n    generateKeys,\n    generateHugeNumberOfFieldsForHashKey,\n    generateHugeNumberOfTinyStringKeys,\n    generateHugeElementsForListKey,\n    generateHugeMembersForSortedListKey,\n    generateHugeStream,\n    generateNKeys,\n    generateRedisearchIndexes,\n    generateNReJSONs,\n    generateNTimeSeries,\n    generateString,\n    generateStrings,\n    generateStreams,\n    generateStreamsWithoutStrictMode,\n    generateNStreams,\n    generateNGraphs,\n    generateNCachedScripts,\n    generateHugeNumberOfMembersForSetKey,\n    getClientNodes,\n    generateTriggeredFunctionsLibrary,\n    setRedisearchConfig,\n  };\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/local-db.ts",
    "content": "import { Connection, createConnection, getConnectionManager } from 'typeorm';\nimport { v4 as uuidv4 } from 'uuid';\nimport { constants } from './constants';\nimport { createCipheriv, createDecipheriv, createHash } from 'crypto';\nimport { TagEntity } from 'src/modules/tag/entities/tag.entity';\nimport { queryLibraryEntityFactory } from 'src/modules/query-library/__tests__/query-library.factory';\n\nexport const repositories = {\n  DATABASE: 'DatabaseEntity',\n  TAG: 'TagEntity',\n  CA_CERT_REPOSITORY: 'CaCertificateEntity',\n  CLIENT_CERT_REPOSITORY: 'ClientCertificateEntity',\n  SSH_OPTIONS_REPOSITORY: 'SshOptionsEntity',\n  AGREEMENTS: 'AgreementsEntity',\n  COMMAND_EXECUTION: 'CommandExecutionEntity',\n  PLUGIN_STATE: 'PluginStateEntity',\n  SETTINGS: 'SettingsEntity',\n  NOTIFICATION: 'NotificationEntity',\n  DATABASE_ANALYSIS: 'DatabaseAnalysisEntity',\n  DATABASE_RECOMMENDATION: 'DatabaseRecommendationEntity',\n  BROWSER_HISTORY: 'BrowserHistoryEntity',\n  CUSTOM_TUTORIAL: 'CustomTutorialEntity',\n  FEATURES_CONFIG: 'FeaturesConfigEntity',\n  FEATURE: 'FeatureEntity',\n  CLOUD_DATABASE_DETAILS: 'CloudDatabaseDetailsEntity',\n  RDI: 'RdiEntity',\n  QUERY_LIBRARY: 'QueryLibraryEntity',\n};\n\nlet localDbConnection;\nconst getDBConnection = async (): Promise<Connection> => {\n  if (!localDbConnection) {\n    const dbFile = constants.TEST_LOCAL_DB_FILE_PATH;\n    localDbConnection = await createConnection({\n      name: 'integrationtests',\n      type: 'sqlite',\n      database: dbFile,\n      entities: [`./../**/*.entity.ts`],\n      synchronize: false,\n      migrationsRun: false,\n    }).catch((err) => {\n      if (err.name === 'AlreadyHasActiveConnectionError') {\n        return getConnectionManager().get('default');\n      }\n      throw err;\n    });\n  }\n\n  return localDbConnection;\n};\n\nexport const getRepository = async (repository: string) => {\n  return (await getDBConnection()).getRepository(repository);\n};\n\nexport const encryptData = (data) => {\n  if (!data) {\n    return null;\n  }\n\n  if (constants.TEST_ENCRYPTION_STRATEGY === 'KEYTAR') {\n    let cipherKey = createHash('sha256')\n      .update(constants.TEST_KEYTAR_PASSWORD, 'utf8') // lgtm[js/insufficient-password-hash]\n      .digest();\n    const cipher = createCipheriv(\n      'aes-256-cbc',\n      cipherKey,\n      Buffer.alloc(16, 0),\n    );\n    let encrypted = cipher.update(data, 'utf8', 'hex');\n    encrypted += cipher.final('hex');\n\n    return encrypted;\n  }\n\n  return data;\n};\n\nexport const decryptData = (data) => {\n  if (!data) {\n    return null;\n  }\n\n  if (constants.TEST_ENCRYPTION_STRATEGY === 'KEYTAR') {\n    let cipherKey = createHash('sha256')\n      .update(constants.TEST_KEYTAR_PASSWORD, 'utf8') // lgtm[js/insufficient-password-hash]\n      .digest();\n\n    const decipher = createDecipheriv(\n      'aes-256-cbc',\n      cipherKey,\n      Buffer.alloc(16, 0),\n    );\n    let decrypted = decipher.update(data, 'hex', 'utf8');\n    decrypted += decipher.final('utf8');\n\n    return decrypted;\n  }\n\n  return data;\n};\n\nexport const generateNCommandExecutions = async (\n  partial: Record<string, any>,\n  number: number,\n  truncate: boolean = false,\n) => {\n  const result = [];\n  const rep = await getRepository(repositories.COMMAND_EXECUTION);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  for (let i = 0; i < number; i++) {\n    result.push(\n      await rep.save({\n        id: uuidv4(),\n        command: encryptData('set foo bar'),\n        result: encryptData(\n          JSON.stringify([\n            {\n              status: 'success',\n              response: `\"OK_${i}\"`,\n            },\n          ]),\n        ),\n        nodeOptions: null,\n        role: null,\n        mode: 'ASCII',\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n        executionTime: Math.round(Math.random() * 10000),\n        createdAt: new Date(),\n        ...partial,\n      }),\n    );\n  }\n\n  return result;\n};\n\nexport const generateNDatabaseAnalysis = async (\n  partial: Record<string, any>,\n  number: number,\n  truncate: boolean = false,\n) => {\n  const result = [];\n  const rep = await getRepository(repositories.DATABASE_ANALYSIS);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  for (let i = 0; i < number; i++) {\n    result.push(\n      await rep.save({\n        id: uuidv4(),\n        databaseId: uuidv4(),\n        db: constants.TEST_DATABASE_ANALYSIS_DB_1,\n        delimiter: constants.TEST_DATABASE_ANALYSIS_DELIMITER_1,\n        filter: encryptData(\n          JSON.stringify(constants.TEST_DATABASE_ANALYSIS_FILTER_1),\n        ),\n        progress: encryptData(\n          JSON.stringify(constants.TEST_DATABASE_ANALYSIS_PROGRESS_1),\n        ),\n        totalKeys: encryptData(\n          JSON.stringify(constants.TEST_DATABASE_ANALYSIS_TOTAL_KEYS_1),\n        ),\n        totalMemory: encryptData(\n          JSON.stringify(constants.TEST_DATABASE_ANALYSIS_TOTAL_MEMORY_1),\n        ),\n        topKeysNsp: encryptData(\n          JSON.stringify([\n            {\n              ...constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_NSP_1,\n              nsp: Buffer.from(\n                constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_NSP_1.nsp,\n              ),\n            },\n          ]),\n        ),\n        topMemoryNsp: encryptData(\n          JSON.stringify([\n            {\n              ...constants.TEST_DATABASE_ANALYSIS_TOP_MEMORY_NSP_1,\n              nsp: Buffer.from(\n                constants.TEST_DATABASE_ANALYSIS_TOP_MEMORY_NSP_1.nsp,\n              ),\n            },\n          ]),\n        ),\n        topKeysLength: encryptData(\n          JSON.stringify([\n            {\n              ...constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1,\n              name: Buffer.from(\n                constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1.name,\n              ),\n            },\n          ]),\n        ),\n        topKeysMemory: encryptData(\n          JSON.stringify([\n            {\n              ...constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1,\n              name: Buffer.from(\n                constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1.name,\n              ),\n            },\n          ]),\n        ),\n        expirationGroups: encryptData(\n          JSON.stringify([constants.TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1]),\n        ),\n        recommendations: encryptData(\n          JSON.stringify([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]),\n        ),\n        createdAt: new Date(),\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n        ...partial,\n      }),\n    );\n  }\n\n  return result;\n};\n\nexport const generatePluginState = async (\n  partial: Record<string, any>,\n  truncate: boolean = false,\n) => {\n  const rep = await getRepository(repositories.PLUGIN_STATE);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  return rep.save({\n    id: uuidv4(),\n    state: encryptData(JSON.stringify('some state')),\n    encryption: constants.TEST_ENCRYPTION_STRATEGY,\n    createdAt: new Date(),\n    ...partial,\n  });\n};\n\nexport const generateRdis = async (\n  partial: Record<string, any>,\n  number: number = 2,\n  truncate: boolean = false,\n) => {\n  const result = [];\n  const rep = await getRepository(repositories.RDI);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  for (let i = 0; i < number; i++) {\n    result.push(\n      await rep.save({\n        id: uuidv4(),\n        url: 'http://localhost:4000',\n        name: 'Rdi',\n        username: 'Rdi Username',\n        password: encryptData(constants.TEST_KEYTAR_PASSWORD),\n        lastConnection: new Date(),\n        version: '1.2',\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n        ...partial,\n      }),\n    );\n  }\n\n  return result;\n};\n\nexport const getRdiById = async (id: string) => {\n  const rep = await getRepository(repositories.RDI);\n  return rep.findOneBy({ id });\n};\n\nexport const generateBrowserHistory = async (\n  partial: Record<string, any>,\n  number: number,\n  truncate: boolean = false,\n) => {\n  const result = [];\n  const rep = await getRepository(repositories.BROWSER_HISTORY);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  result.push(\n    await rep.save({\n      id: constants.TEST_BROWSER_HISTORY_ID_1,\n      databaseId: constants.TEST_BROWSER_HISTORY_DATABASE_ID,\n      filter: encryptData(\n        JSON.stringify(constants.TEST_BROWSER_HISTORY_FILTER_1),\n      ),\n      createdAt: new Date(),\n      encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      ...partial,\n    }),\n  );\n\n  result.push(\n    await rep.save({\n      id: constants.TEST_BROWSER_HISTORY_ID_2,\n      databaseId: constants.TEST_BROWSER_HISTORY_DATABASE_ID,\n      filter: encryptData(\n        JSON.stringify(constants.TEST_BROWSER_HISTORY_FILTER_2),\n      ),\n      createdAt: new Date(),\n      encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      ...partial,\n    }),\n  );\n\n  for (let i = result.length; i < number; i++) {\n    result.push(\n      await rep.save({\n        id: uuidv4(),\n        databaseId: uuidv4(),\n        filter: encryptData(\n          JSON.stringify(constants.TEST_BROWSER_HISTORY_FILTER_1),\n        ),\n        createdAt: new Date(),\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n        ...partial,\n      }),\n    );\n  }\n\n  return result;\n};\n\nexport const generateDatabaseRecommendations = async (\n  partial: Record<string, any>,\n  truncate: boolean = false,\n) => {\n  const result = [];\n  const rep = await getRepository(repositories.DATABASE_RECOMMENDATION);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  result.push(\n    await rep.save({\n      id: constants.TEST_RECOMMENDATION_ID_1,\n      databaseId: constants.TEST_RECOMMENDATIONS_DATABASE_ID,\n      name: constants.TEST_RECOMMENDATION_NAME_1,\n      createdAt: new Date(),\n      read: false,\n      vote: null,\n      ...partial,\n    }),\n  );\n\n  result.push(\n    await rep.save({\n      id: constants.TEST_RECOMMENDATION_ID_2,\n      databaseId: constants.TEST_RECOMMENDATIONS_DATABASE_ID,\n      name: constants.TEST_RECOMMENDATION_NAME_2,\n      createdAt: new Date(),\n      read: false,\n      vote: null,\n      ...partial,\n    }),\n  );\n\n  result.push(\n    await rep.save({\n      id: constants.TEST_RECOMMENDATION_ID_3,\n      databaseId: constants.TEST_RECOMMENDATIONS_DATABASE_ID,\n      name: constants.TEST_RECOMMENDATION_NAME_3,\n      createdAt: new Date(),\n      read: false,\n      db: 3,\n      vote: null,\n      ...partial,\n    }),\n  );\n\n  return result;\n};\n\nconst createCACertificate = async (certificate) => {\n  const rep = await getRepository(repositories.CA_CERT_REPOSITORY);\n  return rep.save(certificate);\n};\n\nconst createClientCertificate = async (certificate) => {\n  const rep = await getRepository(repositories.CLIENT_CERT_REPOSITORY);\n  return rep.save(certificate);\n};\n\n/**\n * Remove all pre setup databases and certificates\n */\nexport const cleanupPreSetupDatabases = async () => {\n  const databaseRepository = await getRepository(repositories.DATABASE);\n  await databaseRepository\n    .createQueryBuilder()\n    .delete()\n    .where({ isPreSetup: true })\n    .execute();\n\n  const caCertificateRepository = await getRepository(\n    repositories.CA_CERT_REPOSITORY,\n  );\n  await caCertificateRepository\n    .createQueryBuilder()\n    .delete()\n    .where({ isPreSetup: true })\n    .execute();\n\n  const clientCertificateRepository = await getRepository(\n    repositories.CLIENT_CERT_REPOSITORY,\n  );\n  await clientCertificateRepository\n    .createQueryBuilder()\n    .delete()\n    .where({ isPreSetup: true })\n    .execute();\n};\n\nexport const createTestDbInstance = async (\n  rte,\n  server,\n  data: any = {},\n): Promise<void> => {\n  const rep = await getRepository(repositories.DATABASE);\n\n  const instance: any = {\n    id: constants.TEST_INSTANCE_ID,\n    name: constants.TEST_INSTANCE_NAME,\n    host: constants.TEST_REDIS_HOST,\n    port: constants.TEST_REDIS_PORT,\n    username: constants.TEST_REDIS_USER,\n    password: encryptData(constants.TEST_REDIS_PASSWORD),\n    encryption: constants.TEST_ENCRYPTION_STRATEGY,\n    tls: false,\n    verifyServerCert: false,\n    connectionType: rte.env.type,\n  };\n\n  if (rte.env.type === constants.CLUSTER) {\n    instance.nodes = JSON.stringify(rte.env.nodes);\n  }\n\n  if (rte.env.type === constants.SENTINEL) {\n    instance.nodes = JSON.stringify([\n      {\n        host: constants.TEST_REDIS_HOST,\n        port: constants.TEST_REDIS_PORT,\n      },\n    ]);\n    instance.sentinelMasterName = constants.TEST_SENTINEL_MASTER_GROUP;\n    instance.sentinelMasterUsername = constants.TEST_SENTINEL_MASTER_USER;\n    instance.sentinelMasterPassword = encryptData(\n      constants.TEST_SENTINEL_MASTER_PASS,\n    );\n  }\n\n  if (constants.TEST_REDIS_TLS_CA) {\n    instance.tls = true;\n    instance.verifyServerCert = true;\n    instance.caCert = await createCACertificate({\n      id: constants.TEST_CA_ID,\n      name: constants.TEST_CA_NAME,\n      encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      certificate: encryptData(constants.TEST_REDIS_TLS_CA),\n    });\n\n    if (constants.TEST_USER_TLS_CERT && constants.TEST_USER_TLS_CERT) {\n      instance.clientCert = await createClientCertificate({\n        id: constants.TEST_USER_CERT_ID,\n        name: constants.TEST_USER_CERT_NAME,\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n        certificate: encryptData(constants.TEST_USER_TLS_CERT),\n        key: encryptData(constants.TEST_USER_TLS_KEY),\n      });\n    }\n  }\n\n  if (constants.TEST_SSH_USER) {\n    instance.ssh = true;\n    instance.sshOptions = {\n      host: constants.TEST_SSH_HOST,\n      port: constants.TEST_SSH_PORT,\n      encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      username: encryptData(constants.TEST_SSH_USER),\n      privateKey: encryptData(constants.TEST_SSH_PRIVATE_KEY_P),\n      passphrase: encryptData(constants.TEST_SSH_PASSPHRASE),\n    };\n  }\n  await rep.save({ ...instance, ...data });\n};\n\nexport const createDatabaseInstances = async () => {\n  const rep = await getRepository(repositories.DATABASE);\n  const instances = [\n    {\n      id: constants.TEST_INSTANCE_ID_2,\n      name: constants.TEST_INSTANCE_NAME_2,\n      host: constants.TEST_INSTANCE_HOST_2,\n      db: constants.TEST_REDIS_DB_INDEX,\n      timeout: 30000,\n    },\n    {\n      id: constants.TEST_INSTANCE_ID_3,\n      name: constants.TEST_INSTANCE_NAME_3,\n      host: constants.TEST_INSTANCE_HOST_3,\n      timeout: 30000,\n    },\n    {\n      id: constants.TEST_INSTANCE_ID_4,\n      name: constants.TEST_INSTANCE_NAME_4,\n      host: constants.TEST_INSTANCE_HOST_4,\n      port: constants.TEST_INSTANCE_PORT_4,\n      timeout: 30000,\n    },\n  ];\n\n  for (let instance of instances) {\n    // await rep.remove(instance);\n    await rep.save({\n      tls: false,\n      verifyServerCert: false,\n      host: 'localhost',\n      port: 3679,\n      connectionType: 'STANDALONE',\n      ...instance,\n      modules: '[]',\n      version: '7.0',\n    });\n  }\n};\n\nconst encryptTags = (tags: TagEntity[]) =>\n  tags.map(\n    ({ key, value, ...rest }) =>\n      ({\n        key: encryptData(key),\n        value: encryptData(value),\n        ...rest,\n      }) as TagEntity,\n  );\n\nconst decryptTags = (tags: TagEntity[]) =>\n  tags.map(\n    ({ key, value, ...rest }) =>\n      ({\n        key: decryptData(key),\n        value: decryptData(value),\n        ...rest,\n      }) as TagEntity,\n  );\n\nexport const getAllTags = async () => {\n  const rep = await getRepository(repositories.TAG);\n  const tags = (await rep.find()) as TagEntity[];\n  const decryptedTags = decryptTags(tags);\n\n  return decryptedTags;\n};\n\nexport const initTags = async () => {\n  const rep = await getRepository(repositories.TAG);\n\n  await rep.createQueryBuilder().delete().execute();\n};\n\nexport const createInstancesWithTags = async () => {\n  await initTags();\n\n  const rep = await getRepository(repositories.DATABASE);\n  const instances = [\n    {\n      id: constants.TEST_INSTANCE_ID_6,\n      name: constants.TEST_INSTANCE_NAME_6,\n      host: constants.TEST_INSTANCE_HOST_6,\n      db: constants.TEST_REDIS_DB_INDEX,\n      tags: encryptTags([constants.TEST_TAGS[0], constants.TEST_TAGS[2]]),\n      timeout: 30000,\n    },\n    {\n      id: constants.TEST_INSTANCE_ID_7,\n      name: constants.TEST_INSTANCE_NAME_7,\n      host: constants.TEST_INSTANCE_HOST_7,\n      tags: encryptTags([constants.TEST_TAGS[1]]),\n      timeout: 30000,\n    },\n  ];\n\n  for (let instance of instances) {\n    await rep.save({\n      tls: false,\n      verifyServerCert: false,\n      host: 'localhost',\n      port: 3679,\n      connectionType: 'STANDALONE',\n      ...instance,\n      modules: '[]',\n      version: '7.0',\n    });\n  }\n};\n\nexport const createIncorrectDatabaseInstances = async () => {\n  const rep = await getRepository(repositories.DATABASE);\n\n  await rep.save({\n    tls: false,\n    verifyServerCert: false,\n    host: constants.TEST_INSTANCE_HOST_5,\n    port: constants.TEST_INSTANCE_PORT_5,\n    connectionType: 'STANDALONE',\n    id: constants.TEST_INSTANCE_ID_5,\n    name: constants.TEST_INSTANCE_ID_5,\n    password: constants.TEST_INCORRECT_PASSWORD,\n    modules: '[]',\n    version: '7.0',\n    timeout: 30000,\n  });\n};\n\nexport const createAclInstance = async (rte, _server): Promise<void> => {\n  const rep = await getRepository(repositories.DATABASE);\n  const instance: any = {\n    id: constants.TEST_INSTANCE_ACL_ID,\n    name: constants.TEST_INSTANCE_ACL_NAME,\n    host: constants.TEST_REDIS_HOST,\n    port: constants.TEST_REDIS_PORT,\n    username: constants.TEST_INSTANCE_ACL_USER,\n    password: encryptData(constants.TEST_INSTANCE_ACL_PASS),\n    encryption: constants.TEST_ENCRYPTION_STRATEGY,\n    tls: false,\n    verifyServerCert: false,\n    connectionType: rte.env.type,\n    timeout: 30000,\n  };\n\n  if (rte.env.type === constants.CLUSTER) {\n    instance.nodes = JSON.stringify(rte.env.nodes);\n  }\n\n  if (rte.env.type === constants.SENTINEL) {\n    instance.nodes = JSON.stringify([\n      {\n        host: constants.TEST_REDIS_HOST,\n        port: constants.TEST_REDIS_PORT,\n      },\n    ]);\n    instance.username = constants.TEST_REDIS_USER;\n    instance.password = encryptData(constants.TEST_REDIS_PASSWORD);\n    instance.sentinelMasterName = constants.TEST_SENTINEL_MASTER_GROUP;\n    instance.sentinelMasterUsername = constants.TEST_INSTANCE_ACL_USER;\n    instance.sentinelMasterPassword = encryptData(\n      constants.TEST_INSTANCE_ACL_PASS,\n    );\n  }\n\n  if (constants.TEST_REDIS_TLS_CA) {\n    instance.tls = true;\n    instance.verifyServerCert = true;\n    instance.caCert = await createCACertificate({\n      id: constants.TEST_CA_ID,\n      name: constants.TEST_CA_NAME,\n      encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      certificate: encryptData(constants.TEST_REDIS_TLS_CA),\n    });\n\n    if (constants.TEST_USER_TLS_CERT && constants.TEST_USER_TLS_CERT) {\n      instance.clientCert = await createClientCertificate({\n        id: constants.TEST_USER_CERT_ID,\n        name: constants.TEST_USER_CERT_NAME,\n        certFilename: constants.TEST_USER_CERT_FILENAME,\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n        certificate: encryptData(constants.TEST_USER_TLS_CERT),\n        key: encryptData(constants.TEST_USER_TLS_KEY),\n      });\n    }\n  }\n\n  if (constants.TEST_SSH_USER) {\n    instance.ssh = true;\n    instance.sshOptions = {\n      host: constants.TEST_SSH_HOST,\n      port: constants.TEST_SSH_PORT,\n      encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      username: encryptData(constants.TEST_SSH_USER),\n      privateKey: encryptData(constants.TEST_SSH_PRIVATE_KEY),\n    };\n  }\n\n  await rep.save(instance);\n};\n\nexport const getInstanceByName = async (name: string) => {\n  const rep = await getRepository(repositories.DATABASE);\n  return rep.findOneBy({ name });\n};\n\nexport const getInstanceById = async (id: string) => {\n  const rep = await getRepository(repositories.DATABASE);\n  const instance = await rep.findOneBy({ id });\n\n  if (instance?.tags) {\n    instance.tags = decryptTags(instance.tags);\n  }\n\n  return instance;\n};\n\nexport const getBrowserHistoryById = async (id: string) => {\n  const rep = await getRepository(repositories.BROWSER_HISTORY);\n  return rep.findOneBy({ id });\n};\n\nexport const applyEulaAgreement = async () => {\n  const rep = await getRepository(repositories.AGREEMENTS);\n  const agreements: any = await rep.findOneBy({});\n  agreements.version = '1.0.0';\n  agreements.data = JSON.stringify({ eula: true, encryption: true });\n\n  await rep.save(agreements);\n};\n\nexport const setAgreements = async (agreements = {}) => {\n  const defaultAgreements = { eula: true, encryption: true };\n\n  const rep = await getRepository(repositories.AGREEMENTS);\n  const entity: any = await rep.findOneBy({});\n\n  entity.version = '1.0.0';\n  entity.data = JSON.stringify({ ...defaultAgreements, ...agreements });\n\n  await rep.save(entity);\n};\n\nconst resetAgreements = async () => {\n  const rep = await getRepository(repositories.AGREEMENTS);\n  const agreements: any = await rep.findOneBy({});\n  agreements.version = null;\n  agreements.data = null;\n\n  await rep.save(agreements);\n};\n\nexport const initAgreements = async () => {\n  const rep = await getRepository(repositories.AGREEMENTS);\n  const agreements: any = await rep.findOneBy({});\n  agreements.version = constants.TEST_AGREEMENTS_VERSION;\n  agreements.data = JSON.stringify({\n    eula: true,\n    encryption: constants.TEST_ENCRYPTION_STRATEGY === 'KEYTAR',\n    analytics: true,\n    notifications: true,\n  });\n\n  await rep.save(agreements);\n};\n\nexport const resetSettings = async () => {\n  await resetAgreements();\n  const rep = await getRepository(repositories.SETTINGS);\n  const settings: any = await rep.findOneBy({});\n  settings.data = null;\n\n  await rep.save(settings);\n};\n\nexport const enableAllDbFeatures = async () => {\n  const rep = await getRepository(repositories.FEATURE);\n  await rep.deleteAll();\n  await rep.insert([{ name: 'insightsRecommendations', flag: true }]);\n};\n\nexport const initSettings = async () => {\n  await initAgreements();\n  const rep = await getRepository(repositories.SETTINGS);\n  const settings: any = await rep.findOneBy({});\n  settings.data = null;\n\n  await rep.save(settings);\n};\n\nexport const setAppSettings = async (data: object) => {\n  const rep = await getRepository(repositories.SETTINGS);\n  const settings: any = await rep.findOneBy({});\n  settings.data = JSON.stringify({\n    ...JSON.parse(settings.data),\n    ...data,\n  });\n  await rep.save(settings);\n};\n\nconst truncateAll = async () => {\n  await (await getRepository(repositories.QUERY_LIBRARY)).clear();\n  await (await getRepository(repositories.TAG)).clear();\n  await (await getRepository(repositories.DATABASE)).clear();\n  await (await getRepository(repositories.FEATURE)).clear();\n  await (await getRepository(repositories.FEATURES_CONFIG)).clear();\n  await (await getRepository(repositories.CA_CERT_REPOSITORY)).clear();\n  await (await getRepository(repositories.CLIENT_CERT_REPOSITORY)).clear();\n  await (await getRepository(repositories.CUSTOM_TUTORIAL)).clear();\n  await await resetSettings();\n};\n\nexport const initLocalDb = async (rte, server) => {\n  await truncateAll();\n  await createTestDbInstance(rte, server);\n  await initAgreements();\n  if (rte.env.acl) {\n    await createAclInstance(rte, server);\n  }\n};\n\nexport const createNotifications = async (\n  notifications: object[],\n  truncate: boolean,\n) => {\n  const rep = await getRepository(repositories.NOTIFICATION);\n\n  if (truncate) {\n    await rep.createQueryBuilder().delete().execute();\n  }\n\n  await rep.insert(notifications);\n};\n\nexport const createDefaultNotifications = async (truncate: boolean = false) => {\n  const notifications = [\n    constants.TEST_NOTIFICATION_1,\n    constants.TEST_NOTIFICATION_2,\n    constants.TEST_NOTIFICATION_3,\n  ];\n\n  await createNotifications(notifications, truncate);\n};\n\nexport const createNotExistingNotifications = async (\n  truncate: boolean = false,\n) => {\n  const notifications = [\n    constants.TEST_NOTIFICATION_NE_1,\n    constants.TEST_NOTIFICATION_NE_2,\n    constants.TEST_NOTIFICATION_NE_3,\n  ];\n\n  await createNotifications(notifications, truncate);\n};\n\nexport const generateNQueryLibraryItems = async (\n  partial: Record<string, any>,\n  number: number,\n  truncate: boolean = false,\n) => {\n  const result = [];\n  const rep = await getRepository(repositories.QUERY_LIBRARY);\n\n  if (truncate) {\n    await rep.clear();\n  }\n\n  for (let i = 0; i < number; i++) {\n    const entity = queryLibraryEntityFactory.build(partial);\n\n    result.push(\n      await rep.save({\n        ...entity,\n        name: encryptData(entity.name),\n        description: encryptData(entity.description),\n        query: encryptData(entity.query),\n        encryption: constants.TEST_ENCRYPTION_STRATEGY,\n      }),\n    );\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/redis.ts",
    "content": "import Redis, * as IORedis from 'ioredis';\nimport * as semverCompare from 'node-version-compare';\nimport * as fs from 'fs';\nimport { Server, createServer } from 'net';\nimport { Client } from 'ssh2';\nimport { constants } from './constants';\nimport { parseReplToObject, parseClusterNodesResponse } from './utils';\nimport { initDataHelper } from './data/redis';\nimport {\n  UnableToCreateLocalServerException,\n  UnableToCreateSshConnectionException,\n} from 'src/modules/ssh/exceptions';\n\n/**\n * Connect to redis in standalone mode and return client\n * @param options\n */\nexport const connectToStandalone = async (\n  options: IORedis.RedisOptions,\n): Promise<IORedis.Redis> => {\n  return await new Promise((resolve, reject) => {\n    const client = new Redis(options);\n\n    client.on('error', (e: Error) => {\n      console.error('Unable to connect in standalone mode', e);\n      reject(e);\n    });\n    client.on('ready', () => {\n      resolve(client);\n    });\n  });\n};\n\n/**\n * Connect to redis in cluster mode and return client\n * @param nodes\n * @param redisOptions\n */\nexport const connectToRedisCluster = async (\n  nodes: any[],\n  redisOptions: IORedis.RedisOptions,\n): Promise<IORedis.Cluster> => {\n  return await new Promise((resolve, reject) => {\n    const client = new Redis.Cluster(nodes, { redisOptions });\n\n    client.on('error', (e: Error): void => {\n      console.error('Unable to connect in cluster mode', e);\n      reject(e);\n    });\n    client.on('ready', async () => {\n      resolve(client);\n    });\n  });\n};\n\n/**\n * Connect to redis in sentinel mode and return client\n * @param redisOptions\n */\nexport const connectToRedisSentinel = async (\n  redisOptions: IORedis.RedisOptions,\n): Promise<IORedis.Redis> => {\n  return await new Promise((resolve, reject) => {\n    const client = new Redis(redisOptions);\n\n    client.on('error', (e: Error): void => {\n      console.error('Unable to connect in sentinel mode', e);\n      reject(e);\n    });\n    client.on('ready', async () => {\n      resolve(client);\n    });\n  });\n};\n\n/**\n * Automatically determines connection mode and returns client\n * @param connectionOptions\n */\nconst getClient = async (\n  connectionOptions: IORedis.RedisOptions,\n): Promise<Record<string, any>> => {\n  let standaloneClient = await connectToStandalone(connectionOptions);\n  const info: any = {\n    type: constants.STANDALONE,\n  };\n\n  // check for cluster\n  try {\n    const clusterInfo = parseReplToObject(\n      await standaloneClient.cluster('INFO'),\n    );\n    if (clusterInfo.cluster_state === 'ok') {\n      const nodes = parseClusterNodesResponse(\n        // https://github.com/luin/ioredis/issues/1572\n        // @ts-expect-error\n        await standaloneClient.cluster('NODES'),\n      )\n        .filter((node) => node.linkState === 'connected')\n        .map(({ host, port }) => {\n          return { host, port };\n        });\n      if (nodes.length > 0) {\n        info.type = constants.CLUSTER;\n        return {\n          client: await connectToRedisCluster(nodes, connectionOptions),\n          info,\n        };\n      }\n    }\n  } catch (e) {}\n\n  // check for sentinel\n  try {\n    const masterGroups = (await standaloneClient.call('sentinel', [\n      'masters',\n    ])) as [];\n    if (!masterGroups?.length) {\n      throw new Error('Invalid sentinel configuration');\n    }\n    info.type = constants.SENTINEL;\n    const sentinelOptions = {\n      ...connectionOptions,\n      sentinels: [\n        {\n          host: constants.TEST_REDIS_HOST,\n          port: constants.TEST_REDIS_PORT,\n        },\n      ],\n      name: constants.TEST_SENTINEL_MASTER_GROUP,\n      sentinelUsername: constants.TEST_REDIS_USER,\n      sentinelPassword: constants.TEST_REDIS_PASSWORD,\n      username: constants.TEST_SENTINEL_MASTER_USER,\n      password: constants.TEST_SENTINEL_MASTER_PASS,\n      connectionName: connectionOptions.connectionName,\n      sentinelTLS: connectionOptions.tls,\n      enableTLSForSentinelMode: !!connectionOptions.tls,\n    };\n    return {\n      client: await connectToRedisSentinel(sentinelOptions),\n      info,\n    };\n  } catch (e) {}\n\n  return { client: standaloneClient, info };\n};\n\nconst initTunnel = async () => {\n  const server = (await new Promise((resolve, reject) => {\n    try {\n      const server = createServer();\n\n      server.on('listening', () => resolve(server));\n      server.on('error', (e) => {\n        reject(new UnableToCreateLocalServerException(e.message));\n      });\n\n      server.listen({\n        host: '127.0.0.1',\n        port: 44444,\n      });\n    } catch (e) {\n      reject(e);\n    }\n  })) as Server;\n\n  const client = (await new Promise((resolve, reject) => {\n    const conn = new Client();\n    conn.on('ready', () => resolve(conn));\n    conn.on('error', (e) => {\n      reject(new UnableToCreateSshConnectionException(e.message));\n    });\n    conn.connect({\n      host: constants.TEST_SSH_HOST,\n      port: constants.TEST_SSH_PORT,\n      username: constants.TEST_SSH_USER,\n      password: constants.TEST_SSH_PASSWORD || undefined,\n    });\n  })) as Client;\n\n  server.on('connection', (connection) => {\n    client.forwardOut(\n      '127.0.0.1',\n      44444,\n      constants.TEST_REDIS_HOST,\n      constants.TEST_REDIS_PORT,\n      (e, stream) => {\n        if (e) {\n          console.error(e);\n          client.emit('error', e);\n        } else {\n          return connection.pipe(stream).pipe(connection);\n        }\n      },\n    );\n\n    connection.on('error', (e) => {\n      client.emit('error', e);\n    });\n  });\n};\n\nlet rte;\n/**\n * Create test Redis client and determine environment settings\n */\nexport const initRTE = async () => {\n  if (!rte) {\n    const options: IORedis.RedisOptions = {\n      host: constants.TEST_REDIS_HOST,\n      port: constants.TEST_REDIS_PORT,\n      username: constants.TEST_REDIS_USER,\n      password: constants.TEST_REDIS_PASSWORD,\n      showFriendlyErrorStack: true,\n      connectionName: constants.TEST_RUN_ID,\n    };\n\n    if (constants.TEST_REDIS_TLS_CA) {\n      if (!constants.TEST_USER_TLS_CERT || !constants.TEST_USER_TLS_CERT) {\n        options.tls = {\n          rejectUnauthorized: true,\n          checkServerIdentity: () => undefined,\n          ca: [constants.TEST_REDIS_TLS_CA],\n        };\n      } else {\n        options.tls = {\n          rejectUnauthorized: true,\n          checkServerIdentity: () => undefined,\n          ca: [constants.TEST_REDIS_TLS_CA],\n          cert: constants.TEST_USER_TLS_CERT,\n          key: constants.TEST_USER_TLS_KEY,\n        };\n      }\n    }\n\n    if (constants.TEST_SSH_USER) {\n      await initTunnel();\n      options.host = '127.0.0.1';\n      options.port = 44444;\n    }\n\n    rte = await getClient(options);\n  }\n\n  const info = parseReplToObject(await rte.client.info());\n\n  rte.env = {\n    name: constants.TEST_RUN_NAME,\n    version: info['redis_version'],\n    mode: info['redis_mode'],\n    type: rte.info.type,\n    onPremise: constants.TEST_RTE_ON_PREMISE,\n    // ACL commands are blocked in the Redis Enterprise and Cloud\n    acl:\n      !constants.TEST_CLOUD_RTE &&\n      !constants.TEST_RE_USER &&\n      semverCompare(info['redis_version'], '6') >= 0,\n    pass: !!constants.TEST_REDIS_PASSWORD,\n    tls: !!constants.TEST_REDIS_TLS_CA,\n    tlsAuth: !!constants.TEST_USER_TLS_KEY && !!constants.TEST_USER_TLS_CERT,\n    modules: await determineModulesInstalled(rte.client),\n    re: !!constants.TEST_RE_USER,\n    ssh: !!constants.TEST_SSH_USER,\n    cloud: !!constants.TEST_CLOUD_RTE,\n    sharedData: constants.TEST_RTE_SHARED_DATA,\n    bigData: constants.TEST_RTE_BIG_DATA,\n    crdt: constants.TEST_RTE_CRDT,\n    nodes: [],\n  };\n\n  if (rte.env.type === constants.CLUSTER) {\n    rte.env.nodes = rte.client.nodes('all').map(({ options }) => {\n      return { host: options.host, port: options.port };\n    });\n  }\n\n  rte.data = await initDataHelper(rte);\n\n  // generate cert files\n  if (rte.env.tls) {\n    fs.writeFileSync(constants.TEST_CA_CERT_PATH, constants.TEST_REDIS_TLS_CA);\n  }\n  if (rte.env.tlsAuth) {\n    fs.writeFileSync(\n      constants.TEST_CLIENT_CERT_PATH,\n      constants.TEST_USER_TLS_CERT,\n    );\n    fs.writeFileSync(\n      constants.TEST_CLIENT_KEY_PATH,\n      constants.TEST_USER_TLS_KEY,\n    );\n  }\n\n  return rte;\n};\n\nconst determineModulesInstalled = async (client) => {\n  const modules = {};\n  try {\n    (await client.call('module', 'list')).map((module) => {\n      modules[module[1].toLowerCase()] = { version: module[3] || -1 };\n    });\n  } catch (e) {\n    console.error('Error when try to indicate modules installed: ', e);\n  }\n\n  return modules;\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/remote-server.ts",
    "content": "import * as express from 'express';\nimport * as fs from 'fs-extra';\nimport { constants } from './constants';\n/**\n * Initiate remote server to fetch various static data like notificaitons or features configs\n */\nexport const initRemoteServer = async () => {\n  await fs.ensureDir(constants.TEST_REMOTE_STATIC_PATH);\n\n  const app = express();\n  app.use(\n    constants.TEST_REMOTE_STATIC_URI,\n    express.static(constants.TEST_REMOTE_STATIC_PATH, { etag: false }),\n  );\n  await app.listen(5551);\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/server.ts",
    "content": "import * as qs from 'qs';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { AppModule } from 'src/app.module';\nimport * as bodyParser from 'body-parser';\nimport { constants } from './constants';\nimport { connect, Socket } from 'socket.io-client';\nimport * as express from 'express';\nimport { serverConfig } from './test';\nimport { SessionMetadataAdapter } from 'src/modules/auth/session-metadata/adapters/session-metadata.adapter';\nimport * as process from 'process';\nimport { sign } from 'jsonwebtoken';\n\n/**\n * TEST_BE_SERVER - url to already running API that we want to test\n * When not defined We will up and run local server\n */\nexport let server = process.env.TEST_BE_SERVER;\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]\nprocess.env.MOCK_AKEY = sign({ exp: Date.now() + 360_000 }, 'test');\nprocess.env.MOCK_RKEY = 'rk_asdasdasd';\nprocess.env.MOCK_IDP_TYPE = 'google';\n\nexport let baseUrl = server;\n\n/**\n * Initiate server if needed (only once)\n */\nexport const getServer = async () => {\n  try {\n    const keytar = require('keytar');\n    let keytarPassword = await keytar.getPassword('redisinsight', 'app');\n    if (!keytarPassword) {\n      await keytar.setPassword(\n        'redisinsight',\n        'app',\n        constants.TEST_KEYTAR_PASSWORD,\n      );\n    } else {\n      constants.TEST_KEYTAR_PASSWORD = keytarPassword;\n    }\n  } catch (e) {\n    constants.TEST_ENCRYPTION_STRATEGY = 'PLAIN';\n  }\n\n  if (!server) {\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    const app = moduleFixture.createNestApplication();\n    // set qs as parser to support nested objects in the query string\n    app.set('query parser', qs.parse);\n    app.use(bodyParser.json({ limit: '512mb' }));\n    app.use(bodyParser.urlencoded({ limit: '512mb', extended: true }));\n    app.use('/static', express.static(serverConfig.get('dir_path').staticDir));\n    app.useWebSocketAdapter(new SessionMetadataAdapter(app));\n\n    await app.init();\n    server = await app.getHttpServer();\n\n    await app.listen(0, '0.0.0.0');\n    baseUrl = await app.getUrl();\n  }\n\n  return server;\n};\n\nexport const getBaseURL = (): string => baseUrl;\n\nexport const getSocket = async (\n  namespace: string,\n  options = {},\n): Promise<Socket> => {\n  return new Promise((resolve, reject) => {\n    const base = new URL(baseUrl);\n    const client = connect(\n      `ws${base.protocol === 'https:' ? 's' : ''}://${base.host}/${namespace}`,\n      {\n        forceNew: true,\n        rejectUnauthorized: false,\n        ...options,\n      },\n    );\n    client.on('connect_error', reject);\n    client.on('connect', () => resolve(client));\n  });\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/test/conditionalIgnore.ts",
    "content": "import { before } from 'mocha';\nimport { get, has } from 'lodash';\nimport * as semverCompare from 'node-version-compare';\nimport { testEnv } from '../test';\n\n/**\n * Function to run tests by condition\n * Used inside \"describe\" function only\n * note: add support \"it\" if needed\n * @param conditions\n */\nexport const requirements = function (...conditions) {\n  before(function () {\n    for (let cond of conditions) {\n      switch (typeof cond) {\n        case 'function':\n          if (!cond()) {\n            this.skip();\n          }\n          break;\n        case 'string':\n          if (!processConditionString(cond)) {\n            this.skip();\n          }\n          break;\n        default:\n          throw new Error(`Unsupported condition type ${cond}`);\n      }\n    }\n  });\n};\n\nconst cmdReg = /^([?!\\w\\.]+)(\\s?[=<>]+)?(\\s?[\\w\\.]+)?$/;\nconst processConditionString = (condition: string): boolean => {\n  if (!cmdReg.test(condition)) {\n    throw new Error('Unsupported condition structure');\n  }\n\n  const args = condition.match(cmdReg).filter((val) => val !== undefined);\n\n  switch (args.length) {\n    case 2:\n      return checkBooleanCondition(\n        args[1].replace(/^!+|!+$/, ''),\n        args[1][0] === '!',\n      );\n    case 4:\n      return checkStringCondition(\n        args[1].replace(/^!+|!+$/, ''),\n        args[2].trim(),\n        args[3].trim(),\n        args[1][0] === '!',\n      );\n    default:\n      throw new Error('Unsupported condition structure');\n  }\n};\n\nconst checkBooleanCondition = (path: string, inverse = false): boolean => {\n  const check = !!get(testEnv, path);\n  return inverse ? !check : check;\n};\n\nconst checkStringCondition = (\n  path: string,\n  expression: string,\n  targetValue: string,\n  inverse = false,\n): boolean => {\n  if (!has(testEnv, path)) {\n    throw new Error(`Test environment does not has such path: ${path}`);\n  }\n\n  const inputValue = get(testEnv, path);\n  const isSemver = path.indexOf('version') > -1;\n  let check: boolean;\n  switch (expression) {\n    case '=':\n    case '==':\n    case '===':\n      check = compareValues(inputValue, targetValue, isSemver) === 0;\n      break;\n    case '>':\n      check = compareValues(inputValue, targetValue, isSemver) === 1;\n      break;\n    case '>=':\n      check = compareValues(inputValue, targetValue, isSemver) >= 0;\n      break;\n    case '<':\n      check = compareValues(inputValue, targetValue, isSemver) === -1;\n      break;\n    case '<=':\n      check = compareValues(inputValue, targetValue, isSemver) <= 0;\n      break;\n  }\n  return inverse ? !check : check;\n};\n\nconst compareValues = (\n  inputValue: string,\n  targetValue: string,\n  semver: boolean = false,\n): number => {\n  if (semver) return semverCompare(inputValue, targetValue);\n  if (inputValue == targetValue) return 0;\n  if (inputValue > targetValue) return 1;\n  if (inputValue < targetValue) return -1;\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/test/dataGenerator.ts",
    "content": "/**\n * Generates invalid data based on Joi schema\n *\n * @param schema\n * @param path\n * @param cases\n */\nexport const generateInvalidDataArray = (schema, path = [], cases = []) => {\n  if (schema._flags?.presence === 'required') {\n    cases.push({ path, value: undefined });\n  }\n\n  const allowedValues = [];\n  if (schema._valids?._values?.size) {\n    schema._valids._values.forEach((value) => allowedValues.push(value));\n  }\n\n  switch (schema.type) {\n    case 'object':\n      // if nested object\n      if (path?.length) {\n        if (!allowedValues.some((allowed) => allowed === null)) {\n          cases.push({ path, value: null });\n        }\n        cases.push({ path, value: 'somestring' });\n        cases.push({ path, value: 100 });\n        cases.push({ path, value: 100.12 });\n        cases.push({ path, value: true });\n      }\n\n      const keys = schema._ids._byKey;\n      if (keys.size) {\n        keys.forEach((key) => {\n          generateInvalidDataArray(key.schema, [...path, key.id], cases);\n        });\n      }\n      break;\n    case 'array':\n      // if nested array\n      if (path?.length) {\n        if (!allowedValues.some((allowed) => allowed === null)) {\n          cases.push({ path, value: null });\n        }\n        cases.push({ path, value: 'somestring' });\n        cases.push({ path, value: 100 });\n        cases.push({ path, value: 100.12 });\n        cases.push({ path, value: true });\n        // cases.push({ path, value: { some: 'object' } });\n      }\n\n      const items = schema.$_terms.items;\n      if (items.length) {\n        items.forEach((item) => {\n          generateInvalidDataArray(item, [...path, 0], cases);\n        });\n      }\n      break;\n    case 'string':\n      [null, 100, 100.12, true, { some: 'object' }, ['some', 'array']].map(\n        (value) => {\n          if (!allowedValues.some((allowed) => allowed === value)) {\n            cases.push({ path, value });\n          }\n        },\n      );\n\n      // check for additional rules\n      if (schema._singleRules?.size) {\n        schema._singleRules.forEach((rule) => {\n          switch (rule.name) {\n            case 'min':\n              cases.push({ path, value: 'a'.repeat(rule.args.limit - 1) });\n              break;\n            case 'max':\n              cases.push({ path, value: 'a'.repeat(rule.args.limit + 1) });\n              break;\n            default:\n              throw new Error(\n                `Unsupported rule ${rule.name}. Need to implement...`,\n              );\n          }\n        });\n      }\n      break;\n    case 'number':\n      [null, 'stringvalue', true, { some: 'object' }, ['some', 'array']].map(\n        (value) => {\n          if (!allowedValues.some((allowed) => allowed === value)) {\n            cases.push({ path, value });\n          }\n        },\n      );\n\n      // check for additional rules\n      if (schema._singleRules?.size) {\n        schema._singleRules.forEach((rule) => {\n          switch (rule.name) {\n            case 'integer':\n              cases.push({ path, value: 11.11 });\n              break;\n            case 'min':\n              cases.push({ path, value: rule.args.limit - 1 });\n              break;\n            case 'max':\n              cases.push({ path, value: rule.args.limit + 1 });\n              break;\n            default:\n              throw new Error(\n                `Unsupported rule ${rule.name}. Need to implement...`,\n              );\n          }\n        });\n      }\n      break;\n    case 'boolean':\n      [\n        null,\n        'stringvalue',\n        100,\n        100.12,\n        { some: 'object' },\n        ['some', 'array'],\n      ].map((value) => {\n        if (!allowedValues.some((allowed) => allowed === value)) {\n          cases.push({ path, value });\n        }\n      });\n      break;\n    case 'any':\n      // ignore \"any\" type\n      break;\n    default:\n      throw new Error(\n        `Data generation doesn't support ${schema.type}. Need to implement...`,\n      );\n  }\n\n  return cases;\n};\n"
  },
  {
    "path": "redisinsight/api/test/helpers/test.ts",
    "content": "import { describe, it, before, after, beforeEach } from 'mocha';\nimport * as util from 'util';\nimport * as _ from 'lodash';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport * as fsExtra from 'fs-extra';\nimport * as chai from 'chai';\nimport chaiDeepEqualIgnoreUndefined from 'chai-deep-equal-ignore-undefined';\nimport * as nock from 'nock';\nimport * as Joi from 'joi';\nimport * as AdmZip from 'adm-zip';\nimport * as diff from 'object-diff';\nimport axios from 'axios';\nimport { cloneDeep, isMatch, isObject, set, isArray } from 'lodash';\nimport { generateInvalidDataArray } from './test/dataGenerator';\nimport serverConfig from 'src/utils/config';\n\nchai.use(chaiDeepEqualIgnoreUndefined);\n\nexport { _, path, fs, fsExtra, AdmZip, serverConfig, axios, nock };\nexport const expect = chai.expect;\nexport const testEnv: Record<any, any> = {};\nexport { Joi, describe, it, before, after, beforeEach };\n\nexport * from './test/conditionalIgnore';\nexport * from './test/dataGenerator';\n\ninterface ITestCaseInput {\n  endpoint: Function; // function that returns prepared supertest with url\n  data?: any;\n  attach?: any[];\n  headers?: Record<string, string>;\n  fields?: [string, string][];\n  query?: any;\n  statusCode?: number;\n  responseSchema?: Joi.AnySchema;\n  responseBody?: any;\n  responseHeaders?: object;\n  checkFn?: Function;\n  preconditionFn?: Function;\n  postCheckFn?: Function;\n}\n\n/**\n * Common validation function\n * @param ITestCaseInput\n */\nexport const validateApiCall = async function ({\n  endpoint,\n  data,\n  headers,\n  attach,\n  fields,\n  query,\n  statusCode = 200,\n  responseSchema,\n  responseBody,\n  responseHeaders,\n  checkFn,\n}: ITestCaseInput): Promise<any> {\n  const request = endpoint();\n\n  // data to send with POST, PUT etc\n  if (data) {\n    request.send(typeof data === 'function' ? data() : data);\n  }\n\n  if (headers) {\n    request.set(headers);\n  }\n\n  if (attach) {\n    request.attach(...attach);\n  }\n\n  if (fields?.length) {\n    fields.forEach((field) => {\n      request.field(...field);\n    });\n  }\n\n  // data to send with url query string\n  if (query) {\n    request.query(query);\n  }\n\n  const response = await request;\n\n  // custom function to check conditions\n  if (checkFn) {\n    await checkFn(response);\n  }\n\n  // check response body (not deep strict)\n  if (responseBody) {\n    checkResponseBody(response.body, responseBody);\n  }\n\n  expect(response.res.statusCode).to.eq(statusCode);\n\n  // validate response headers if passed\n  if (responseHeaders) {\n    expect(response.res.headers).to.include(responseHeaders);\n  }\n\n  // validate response schema if passed\n  if (responseSchema) {\n    Joi.assert(response.body, responseSchema);\n  }\n\n  return response;\n};\n\n/**\n * Checks if values from \"expected\" persist in body\n * Can receive more fields from API (\"body\") but will check values from \"expected\" only\n *\n * @param body\n * @param expected\n */\nexport const checkResponseBody = (body, expected) => {\n  try {\n    if (isArray(expected)) {\n      return expect(body).to.deep.eq(expected);\n    }\n\n    if (isObject(expected)) {\n      return expect(isMatch(body, expected)).to.eql(true);\n    }\n    // todo: improve to support array, arrays of objects etc.\n    expect(expected).to.eql(body);\n  } catch (e) {\n    const errorMessage =\n      'Response does not includes expected value(s)' +\n      '\\nExpect:\\n' +\n      util.inspect(body, { depth: null }) +\n      '\\nTo include:\\n' +\n      util.inspect(expected, { depth: null }) +\n      '\\nDiff:\\n' +\n      util.inspect(diff(body, expected), { depth: null });\n\n    throw new Error(errorMessage);\n  }\n};\n\nconst defaultValidationErrorMessages = {\n  'any.required': '{#label} should not be null or undefined',\n  'any.only': '{#label} must be a valid enum value',\n  'array.base': '{#label} must be an array',\n  'string.base': `{#label} must be a string`,\n  'string.empty': `{#label} should not be null or undefined`,\n  'number.base': `{#label} must be an integer number`,\n  'number.integer': `{#label} must be an integer number`,\n  'number.min': `{#label} must not be less than {#min}`,\n  'number.max': `{#label} must not be greater than {#max}`,\n  'string.min': `{#label} must be longer than or equal to {#limit} characters`,\n  'string.max': `{#label} must be shorter than or equal to {#limit} characters`,\n  'object.base': `must be either object or array`,\n};\n\n/**\n * Common test case for input data validation\n *\n * @param endpoint\n * @param schema\n * @param target\n */\nexport const validateInvalidDataTestCase = (\n  endpoint,\n  schema,\n  target = 'data',\n) => {\n  return (testCase) => {\n    it(testCase.name, async () => {\n      await validateApiCall({\n        endpoint,\n        statusCode: 400,\n        checkFn: badRequestCheckFn(schema, testCase[target]),\n        ...testCase,\n      });\n    });\n  };\n};\n\n/**\n * Custom check for API response for validation error\n * @param schema\n * @param data\n */\nconst badRequestCheckFn = (schema, data) => {\n  return ({ body }) => {\n    expect(body.statusCode).to.eql(400);\n    expect(body.error).to.eql('Bad Request');\n\n    // check expected error messages using validation schema\n    const { error } = schema.validate(data, {\n      abortEarly: true,\n      errors: { wrap: { label: false } },\n      messages: defaultValidationErrorMessages,\n    });\n    error.details.map(({ message }) => {\n      expect(body.message.join()).to.have.string(message);\n    });\n  };\n};\n\n/**\n * Generates input data for validation test case based on Joi schema\n *\n * @param schema\n * @param validData\n * @param target\n * @param extra\n */\nexport const generateInvalidDataTestCases = (\n  schema,\n  validData,\n  target = 'data',\n  extra: any = {},\n) => {\n  return generateInvalidDataArray(schema).map(({ path, value }) => {\n    return {\n      name: `Validation error when ${target}: ${path.join('.')} = \"${value}\"`,\n      [target]: path?.length ? set(cloneDeep(validData), path, value) : value,\n      ...extra,\n    };\n  });\n};\n\nexport const getMainCheckFn = (endpoint) => async (testCase) => {\n  it(testCase.name, async () => {\n    // additional checks before test run\n    if (testCase.before) {\n      await testCase.before();\n    }\n\n    await validateApiCall({\n      endpoint,\n      ...testCase,\n    });\n\n    // additional checks after test pass\n    if (testCase.after) {\n      await testCase.after();\n    }\n  });\n};\n\nexport const sleep = (ms: number) =>\n  new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const JoiRedisString = Joi.alternatives().try(\n  Joi.string(),\n  Joi.object().keys({\n    type: Joi.string().valid('Buffer').required(),\n    data: Joi.array().items(Joi.number()).required(),\n  }),\n);\n"
  },
  {
    "path": "redisinsight/api/test/helpers/utils.ts",
    "content": "/**\n * Parses Redis REPL info responses to object\n * @param data\n */\nexport const parseReplToObject = (data: string): Record<string, any> => {\n  try {\n    const obj = {};\n\n    data.split('\\r\\n').map((line) => {\n      if (!line) return;\n\n      const fields = line.match(/^(.+):(.+)$/);\n      fields ? (obj[fields[1]] = fields[2]) : null;\n    });\n\n    return obj;\n  } catch (e) {\n    console.error('Error when trying to parse REPL object response', e);\n    return {};\n  }\n};\n\n/**\n * Parses Redis REPL cluster nodes command response\n * @param data\n */\nexport const parseClusterNodesResponse = (\n  data: string,\n): Record<string, any>[] => {\n  try {\n    const nodes = [];\n\n    data.split('\\n').map((line) => {\n      if (!line) return;\n\n      const fields = line.split(' ');\n      const [id, endpoint, , master, , , , linkState, slot] = fields;\n      nodes.push({\n        id,\n        host: endpoint.split(':')[0],\n        port: parseInt(endpoint.split(':')[1].split('@')[0], 10),\n        replicaOf: master !== '-' ? master : undefined,\n        linkState,\n        slot,\n      });\n    });\n\n    return nodes;\n  } catch (e) {\n    console.error('Error when trying to parse REPL array response', e);\n    return [];\n  }\n};\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/cloud-st/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./cloud-st/.env\n    environment:\n      TEST_CLOUD_API_KEY: ${TEST_CLOUD_API_KEY}\n      TEST_CLOUD_API_SECRET_KEY: ${TEST_CLOUD_API_SECRET_KEY}\n  redis:\n    image: node:20.14-alpine\n    entrypoint: ['echo', 'Dummy Service']\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/docker.build.env",
    "content": "COV_FOLDER=./test/test-runs/coverage\nID=defaultid\nRTE=defaultrte\nAPP_IMAGE=redisinsight:amd64\nTEST_BE_SERVER=http://app:5540/api\nRI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json\nCERTS_FOLDER=/root/.redisinsight-v2.0\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/docker.build.yml",
    "content": "# Base compose file that includes all BE, RTE builds\nversion: '3.4'\n\nx-constants:\n  - &apiRoot ./../../\n\nservices:\n  test:\n    cap_add:\n      - ipc_lock\n    build:\n      context: *apiRoot\n      dockerfile: ./test/test-runs/test.Dockerfile\n    tty: true\n    volumes:\n      - shared-data:/usr/src/app/test/test-runs/coverage\n      - shared-data:/root/.redisinsight-v2.0\n      - shared-data:/data\n    depends_on:\n      - redis\n      - app\n    environment:\n      RI_REDIS_CLIENTS_FORCE_STRATEGY: ${RI_REDIS_CLIENTS_FORCE_STRATEGY}\n      CERTS_FOLDER: '/root/.redisinsight-v2.0'\n      RI_PRE_SETUP_DATABASES_PATH: '/root/.redisinsight-v2.0/databases.json'\n      TEST_REDIS_HOST: 'redis'\n      RI_DB_SYNC: 'true'\n      TEST_BE_SERVER: ${TEST_BE_SERVER}\n      TEST_LOCAL_DB_FILE_PATH: '/root/.redisinsight-v2.0/redisinsight.db'\n      RI_SECRET_STORAGE_PASSWORD: 'somepassword'\n  app:\n    cap_add:\n      - ipc_lock\n    image: ${APP_IMAGE}\n    depends_on:\n      - redis\n    volumes:\n      - shared-data:/root/.redisinsight-v2.0\n      - shared-data:/data\n    environment:\n      RI_REDIS_CLIENTS_FORCE_STRATEGY: ${RI_REDIS_CLIENTS_FORCE_STRATEGY}\n      CERTS_FOLDER: '/root/.redisinsight-v2.0'\n      RI_PRE_SETUP_DATABASES_PATH: '/root/.redisinsight-v2.0/databases.json'\n      RI_DB_SYNC: 'true'\n      RI_DB_MIGRATIONS: 'false'\n      RI_APP_FOLDER_NAME: '.redisinsight-v2.0'\n      RI_SECRET_STORAGE_PASSWORD: 'somepassword'\n      RI_NOTIFICATION_UPDATE_URL: 'https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json'\n      RI_FEATURES_CONFIG_URL: 'http://test:5551/remote/features-config.json'\n\nnetworks:\n  default:\n    name: ${ID}\n\nvolumes:\n  shared-data:\n    driver: local\n    driver_opts:\n      type: none\n      device: ../../${COV_FOLDER}\n      o: bind\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/gears-clu/docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  redis:\n    image: redislabs/redisgears:edge\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/local.build.env",
    "content": "COV_FOLDER=./coverage\nID=defaultid\nRTE=defaultrte\nRI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json\nCERTS_FOLDER=/root/.redisinsight-v2.0\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/local.build.yml",
    "content": "# Base compose file that includes all BE, RTE builds\nversion: '3.4'\n\nx-constants:\n  - &apiRoot ./../../\n\nservices:\n  test:\n    cap_add:\n      - ipc_lock\n    build:\n      context: *apiRoot\n      dockerfile: ./test/test-runs/test.Dockerfile\n    tty: true\n    volumes:\n      - ${COV_FOLDER}:/usr/src/app/coverage\n      - ${COV_FOLDER}:/root/.redisinsight-v2.0\n    depends_on:\n      - redis\n    environment:\n      RI_REDIS_CLIENTS_FORCE_STRATEGY: ${RI_REDIS_CLIENTS_FORCE_STRATEGY}\n      CERTS_FOLDER: '/root/.redisinsight-v2.0'\n      RI_PRE_SETUP_DATABASES_PATH: '/root/.redisinsight-v2.0/databases.json'\n      TEST_REDIS_HOST: 'redis'\n      NODE_ENV: 'test'\n      RI_REQUEST_TIMEOUT: '25000'\n      RI_DATA_DIR: '/usr/src/app/coverage/data'\n      TEST_DATA_DIR: '/usr/src/app/coverage/data'\n\n  # dummy service to prevent docker validation errors\n  app:\n    image: node:20.14-alpine\n\nnetworks:\n  default:\n    name: ${ID}\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/mods-preview/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    environment:\n      - 'TEST_RUN_NAME=MODS_PREVIEW'\n  redis:\n    image: redislabs/redismod:preview\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu/Dockerfile",
    "content": "FROM bitnamilegacy/redis-cluster:6.2.6\n\nENV ALLOW_EMPTY_PASSWORD yes\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-clu/.env\n    environment:\n      TEST_REDIS_HOST: 'r1'\n\n  redis:\n    build: &build ./oss-clu\n    environment:\n      - &nodes 'REDIS_NODES=r1 r2 r3 s1 s2 s3 s4 s5 s6'\n      - 'REDIS_CLUSTER_REPLICAS=2'\n      - 'REDIS_CLUSTER_CREATOR=yes'\n    depends_on: [r1, r2, r3, s1, s2, s3, s4, s5, s6]\n\n  r1:\n    build: *build\n    environment: [*nodes]\n  r2:\n    build: *build\n    environment: [*nodes]\n  r3:\n    build: *build\n    environment: [*nodes]\n  s1:\n    build: *build\n    environment: [*nodes]\n  s2:\n    build: *build\n    environment: [*nodes]\n  s3:\n    build: *build\n    environment: [*nodes]\n  s4:\n    build: *build\n    environment: [*nodes]\n  s5:\n    build: *build\n    environment: [*nodes]\n  s6:\n    build: *build\n    environment: [*nodes]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu-tls/Dockerfile",
    "content": "FROM bitnamilegacy/redis-cluster:6.0.8\n\nENV ALLOW_EMPTY_PASSWORD yes\n\n# TLS options\nENV REDIS_TLS_ENABLED yes\nENV REDIS_TLS_PORT 6379\nENV REDIS_TLS_CERT_FILE /opt/bitnami/redis/certs/redis.crt\nENV REDIS_TLS_KEY_FILE /opt/bitnami/redis/certs/redis.key\nENV REDIS_TLS_CA_FILE /opt/bitnami/redis/certs/redisCA.crt\nENV REDIS_TLS_AUTH_CLIENTS no\n\nCOPY --chown=1001 ./certs /opt/bitnami/redis/certs/\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu-tls/certs/redis.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhEwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI3WhcNMzExMDI2MTMyNzI3WjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAM4osMW/hBlde+/E20wP9X+zJ0AMD6OtfhqQ5brC\ngbVs9mPccZ/8R0fj83YtZfnoEodMZ/7yUTeCCPMdprAezMU1KBf9EpZmTdWhpO3e\nkESHcQdsKkqGtyjYF7dDahTmKt4a4aHlPH0DJLltB5HlbVabkzlo+3S8QaNwH5lY\nyJTQIqiqVzs9oRLT76nZuJjsym0dNXE42rO3KCniI6kvJDmUzBD8Wc94iDExfy7q\nqHyV7b2DCp1w7XP4yrQAFQ6kiVqNcfTTAO4MHNP54V2nZLPdOsUD5BsYY8hu0HDc\n/PisZ9ZMcw7LMfpUd3dfA9zefQXPQsWJK20ZCNmdIFtwvIFUpYu/FEF3FWO83zeI\nXkVZiuCOnoFvp8JIDvXgzXUBWzvYmxLqVSZAuabqU5pKRswDPLGlZTkHbuP2DiXD\nLD5AsTnICpzYkUeERSZKf2qc/nTUk04W/7FUT/75ItVzZvu90mPJlmArB0j4zdAG\nKwKo8v/cF1hA1YznhibxcUAA/Q/O3Y6CPQ7C3NeaGKcycgUxWoEY3Leno40ukijd\nR0MvsaY7V0/up37fkPtH9rcCkZOGVT5Q4Ww9gVO3yseXVkxbJyzHV1tuwg6yY9wO\nLOU2Bbmazkjlkb8a5OyQol2zZNJ0L3lvRWTildtGUTsBkeqI6HAedTTHJkhjHh/P\nP0+TAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nAKn+aH60zdd4ItMOhgd4BIok/u6S4NF0oe4kDwC/EU5C0hbuT6gLAn7ArHMeadue\nOuSnhqIPfxNR9WCes9OU1O00WeCuWCE4XfNMMFN5iDFfLxO4Oy5Adt0T74hWbcy6\nh28TdcjrkJEr7HR59G5tQ8TW5gVB4a0WXDw0ob9DSxbFKZU1uZm9L/+MgB/SNCHL\nGZSKt75Z/M10b9BTC3OG9swsoWvXEjR2ICiwzk+LxVf5K38faDyBrNJVglrpEUZz\ngP60kL73qK0y1/i35UuP0yIJIy48XnDsSByN7eBVsNTGMW3CFLKWA4RVfnEHNUff\nvsLHXZFYsUIPnPc5jksFwb/wKAe9JbCrgQPhBYaIYkRGiYt64C48r3boIIVoz9+1\n9Nq0Ik06fCzlI9APq2nzEiVeB7mDyZ692neu32QM6zRkYor+W8uI21YnRJWlOx7+\nx2GIh2EZnEYNvbpbvk/fV5AqkYOu9auRAkcKfME7dJ3Gwndl0YBOjE2DMTv6vIjS\ndVuGXQCvlzkRAnPMh5MR5/bSUKVvBryXs9ecAMgoVXBVB+4tGWct5ziL+8qyNtgA\nWJ2EWj3xtLlMwwQmLjRsCrZjL4liLJG8Yn8Ehfq1rRJREH2O8uYKCO1fdhuI0Y5S\niBPfqJi6QBHj7i01K9OpNUB7l+xAFLA3cBsegcm2GPoL\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu-tls/certs/redis.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDOKLDFv4QZXXvv\nxNtMD/V/sydADA+jrX4akOW6woG1bPZj3HGf/EdH4/N2LWX56BKHTGf+8lE3ggjz\nHaawHszFNSgX/RKWZk3VoaTt3pBEh3EHbCpKhrco2Be3Q2oU5ireGuGh5Tx9AyS5\nbQeR5W1Wm5M5aPt0vEGjcB+ZWMiU0CKoqlc7PaES0++p2biY7MptHTVxONqztygp\n4iOpLyQ5lMwQ/FnPeIgxMX8u6qh8le29gwqdcO1z+Mq0ABUOpIlajXH00wDuDBzT\n+eFdp2Sz3TrFA+QbGGPIbtBw3Pz4rGfWTHMOyzH6VHd3XwPc3n0Fz0LFiSttGQjZ\nnSBbcLyBVKWLvxRBdxVjvN83iF5FWYrgjp6Bb6fCSA714M11AVs72JsS6lUmQLmm\n6lOaSkbMAzyxpWU5B27j9g4lwyw+QLE5yAqc2JFHhEUmSn9qnP501JNOFv+xVE/+\n+SLVc2b7vdJjyZZgKwdI+M3QBisCqPL/3BdYQNWM54Ym8XFAAP0Pzt2Ogj0OwtzX\nmhinMnIFMVqBGNy3p6ONLpIo3UdDL7GmO1dP7qd+35D7R/a3ApGThlU+UOFsPYFT\nt8rHl1ZMWycsx1dbbsIOsmPcDizlNgW5ms5I5ZG/GuTskKJds2TSdC95b0Vk4pXb\nRlE7AZHqiOhwHnU0xyZIYx4fzz9PkwIDAQABAoICAHyc/+0oDHNAnK+bsGrTorNj\n2S/Pmox3TChGuXYgKEM/79cA4vWvim6cDQe7/U4Hx1tdBeeHFSyWP06k96Kxm1kA\n/pExedDLWfTt1kGqLE4gCGRSL2YI9CGOLRerei3TysmiOgygAeYWxlYG33KC2Ypm\nU6F6IbS4LnzaQ19v2R6KiMim3j+CyyAUV2O1pO1bBCjcZPdhRGEpLu/SL3gOdLkR\nhiAmSSstUjVaE+SKFvnnrmLFGN996SoWkoAnJJNLRXMk2GMCQCejzrEa8+ymSCqo\naOO5rGHsZjQ7N2dhTNALdmCEqW+hxz3nXKcdGbqiCbQ/Sb8ZYNR7M2xGm85p4Ka9\n0UK4cOM1VJPwz8hotSKAUmXnpbu73CsZi5HyzwZkk4FpcaYCYrCbGVmm1cIKEKI7\n8SN/oqgFdj4Ha9cemnu+RecQZouK+wPWtcILd2kstJl52TV952fVOrnXQDo6XCXB\nfbs9IYN1hB6N79xv4L7Jj53hSRMeSNf70Ejkh1FXPOvmvFT12wy5JQdBBR5nnb4a\nGEsMpGVe1k3bxjK7K263tLSH0UZ8dMgdSx1E4D1hT1K/gEwTSMOJ0E1R0M6SJmF2\n6TnZ0MbJWx6PbICmyZrd2agfTQrq6CgY1fWLGbQrtnwXtsUR7PiHarydXfs3V8g1\nxHnK1bItOBBMOMcWV93hAoIBAQD2xMceUfBUN0TeWrJ4izD3lUYxmWqjTg7kPcjJ\n0VN8v3txGAcyPmbuz5AEzvdFjTispoZNB9QOrmsUVWTQDE5otnei9kHWzqJWYHg4\nUSuUuAh8OJGCiepo8zHT3qHDNhKGtOAp5LC8TaOznUFr35zCBCOsvQfRUKrv5IOc\nvCFjO07Xly8+M3qK7/UswRQ6480VlE2t1p+VNaORHdTDg2tes3/9owuiNmR/sPT8\nnIoe01LS7qmZoiB1vracaLcBf1Iwd7RvKg7mgFJzmowZUYxyX2YGK5qZ1h74In2X\n55+qQnNW0RwPijopTv711pMhMKWl8i3ilcCfoeBXz8zCwFfbAoIBAQDV3wHAO7ic\nMYP/Bm5jgIwlix1eOWY/yB+VqdYn2GmC49vTEIlIVlFRq0vZE06iUxs87BIV08zO\n4w/iKXd7ktkuhphiEuU2yXA3LQPHpbSOW43RONbf4glFU/DlP/P6fiybbWj6+f7L\n7Zbvtz5AW03Y4ZpagJTqOgVdJ0MdLnh9vZj6okGGV1fidKtG6hr7Z/oLhnl9aAZK\n4vrvBZ//qz99vEVByiV5kRaJDulu+diBy4n6iBjzjHA5a9e7lY3sUBw3DMgb7kYs\nJJPkCPdSxCYq4Ef3z/Eao0tyUuCzyznfCMGJA1gBdTpwDNDCTaXqkjR5nvsdE5k0\nIVQgFPtcOPCpAoIBABujNlni+3OzLPdqWQq/LCDOiyoK8LKRj4FomhBgbWVPXNfx\nxPyPmJ+uh4bCV1dm1a4giHIgKlPqnPuOBNh4SF/Z79REmGMiiXP7IfvMu4DQi8K9\n4y4nnCVc93uvN5bRe4mywFhw0IqGd4sqVaVrSfdA124FTdbXng14HnVzbJncjpv+\nxr/ErDjbXy5AAbAGy3VbQsfxfbYMZ+Fc4fNzyJa2q+MQW8EzLlZOz2Frdty09lXB\nfSVDzzbgwTsLT1PPmrjq7z50C28teA6ShJZhV8WHgbm3MH2CSb2ov0BAJNXA04Ip\nsWbcKF9wBYYrHhddh2/qi9EQzJ4UVzf+ggRd3nkCggEAWcjyWjp4KRJcgJ65jwoz\nS7uYS6s7MsGYCOOw5S9kNC/mZDhH+ddK8kdAY1RIqbrL74qHmSQ+kgge7epMn9Mp\nW+/jXyDhm1t7wZ4jPRhisXTcF56ODpU9IR65PfTYPyvjHCkVbm+vOPt4ZxB9kNUD\n3G3xt9bNLXvILrBB66lLqjYDWAzwBy751Tb3hKDZTPv8rAP7Uttt8NhTUi8BWXsR\n/34fcRwlGWEAne9lrlIzQ2IofcXO+8fUgTa17ak+WJvVDINQKvGgAf4lHBFrixKP\nl2ZqsC1a4bz1+nuym6hQlkJ9xUBjHNGTA+FNbpTcd5qDbx9/+lf09D6dq45DbBb3\naQKCAQBrnFYocTm/fIeKo1n1kyF2ULkd6k984ztH8UyluXleSS1ShFFoo/x3vz35\nfsZNUggRnSe7/OBGZYquF/1roVULI1hKh4tbEmW4SWNeTFvwXKdRe6T7NnWSZtS/\nKtamA3lT2wtoEVOvfMo8M0hoFuRWdT2M0i+LKZQdRsq18XPLqdHt1kkSNcnPDERm\n4gLQ8zXTf2fHrtZmyM8fc0GuTVwprPFeJkLtSPehkeXSTgb6rpyelX9NBUILwRgP\nnw0+cbjFDFKaLnIrMFoVAAn/8DcnbbSt1TZhgNsMxY+GHWPBYW8SUi5nBmQQtmA7\nn3ju44acIPvJ9sWuZruVlWZGFaHm\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu-tls/certs/redisCA.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-clu-tls/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-clu-tls/.env\n    environment:\n      TEST_REDIS_HOST: 'r1'\n\n  redis:\n    build: &build ./oss-clu-tls\n    environment:\n      - 'REDIS_NODES=r1 r2 r3'\n      - 'REDIS_CLUSTER_REPLICAS=0'\n      - 'REDIS_CLUSTER_CREATOR=yes'\n    depends_on:\n      - r1\n      - r2\n      - r3\n\n  r1:\n    build: *build\n    environment:\n      - 'REDIS_NODES=r1 r2 r3'\n  r2:\n    build: *build\n    environment:\n      - 'REDIS_NODES=r1 r2 r3'\n  r3:\n    build: *build\n    environment:\n      - 'REDIS_NODES=r1 r2 r3'\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/Dockerfile",
    "content": "FROM redis:6.2.6-alpine\nCOPY redis.conf users.acl /etc/redis/\nENTRYPOINT [ \"redis-server\", \"/etc/redis/redis.conf\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-sent/.env\n  redis:\n    build:\n      context: &build ./oss-sent\n      dockerfile: sentinel.Dockerfile\n    links:\n      - p1:p1\n      - p2:p2\n    depends_on:\n      - p1\n      - s1_1\n      - s1_2\n      - p2\n      - s2_1\n      - s2_2\n  p1:\n    build: *build\n  s1_1:\n    build: *build\n    command: --slaveof p1 6379 --masterauth defaultpass\n  s1_2:\n    build: *build\n    command: --slaveof p1 6379 --masterauth defaultpass\n  p2:\n    build: *build\n  s2_1:\n    build: *build\n    command: --slaveof p2 6379 --masterauth defaultpass\n  s2_2:\n    build: *build\n    command: --slaveof p2 6379 --masterauth defaultpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Note that option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all available network interfaces on the host machine.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only on the\n# IPv4 loopback interface address (this means Redis will only be able to\n# accept client connections from the same host that it is running on).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT OUT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# bind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\n# protected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need a high backlog in order\n# to avoid slow clients connection issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Force network equipment in the middle to consider the connection to be\n#    alive.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# TLS/SSL #####################################\n\n# By default, TLS/SSL is disabled. To enable it, the \"tls-port\" configuration\n# directive can be used to define TLS-listening ports. To enable TLS on the\n# default port, use:\n#\n# port 0\n# tls-port 6379\n\n# Configure a X.509 certificate and private key to use for authenticating the\n# server to connected clients, masters or cluster peers.  These files should be\n# PEM formatted.\n#\n#tls-cert-file /etc/redis/redis.crt\n#tls-key-file /etc/redis/redis.key\n\n# Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange:\n#\n# tls-dh-params-file redis.dh\n\n# Configure a CA certificate(s) bundle or directory to authenticate TLS/SSL\n# clients and peers.  Redis requires an explicit configuration of at least one\n# of these, and will not implicitly use the system wide configuration.\n#\n#tls-ca-cert-file /etc/redis/ca.crt\n# tls-ca-cert-dir /etc/ssl/certs\n\n# By default, clients (including replica servers) on a TLS port are required\n# to authenticate using valid client side certificates.\n#\n# If \"no\" is specified, client certificates are not required and not accepted.\n# If \"optional\" is specified, client certificates are accepted and must be\n# valid if provided, but are not required.\n#\n#tls-auth-clients yes\n# tls-auth-clients optional\n\n# By default, a Redis replica does not attempt to establish a TLS connection\n# with its master.\n#\n# Use the following directive to enable TLS on replication links.\n#\n#tls-replication yes\n\n# By default, the Redis Cluster bus uses a plain TCP connection. To enable\n# TLS for the bus protocol, use the following directive:\n#\n# tls-cluster yes\n\n# Explicitly specify TLS versions to support. Allowed values are case insensitive\n# and include \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\" (OpenSSL >= 1.1.1) or\n# any combination. To enable only TLSv1.2 and TLSv1.3, use:\n#\n# tls-protocols \"TLSv1.2 TLSv1.3\"\n\n# Configure allowed ciphers.  See the ciphers(1ssl) manpage for more information\n# about the syntax of this string.\n#\n# Note: this configuration applies only to <= TLSv1.2.\n#\n# tls-ciphers DEFAULT:!MEDIUM\n\n# Configure allowed TLSv1.3 ciphersuites.  See the ciphers(1ssl) manpage for more\n# information about the syntax of this string, and specifically for TLSv1.3\n# ciphersuites.\n#\n# tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256\n\n# When choosing a cipher, use the server's preference instead of the client\n# preference. By default, the server follows the client's preference.\n#\n# tls-prefer-server-ciphers yes\n\n# By default, TLS session caching is enabled to allow faster and less expensive\n# reconnections by clients that support it. Use the following directive to disable\n# caching.\n#\n# tls-session-caching no\n\n# Change the default number of TLS sessions cached. A zero value sets the cache\n# to unlimited size. The default size is 20480.\n#\n# tls-session-cache-size 5000\n\n# Change the default timeout of cached TLS sessions. The default timeout is 300\n# seconds.\n#\n# tls-session-cache-timeout 60\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#                        requires \"expect stop\" in your upstart job config\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile /var/run/redis_6379.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\nlogfile \"\"\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behavior will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# By default compression is enabled as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# Remove RDB files used by replication in instances without persistence\n# enabled. By default this option is disabled, however there are environments\n# where for regulations or other security concerns, RDB files persisted on\n# disk by masters in order to feed replicas, or stored on disk by replicas\n# in order to load them for the initial synchronization, should be deleted\n# ASAP. Note that this option ONLY WORKS in instances that have both AOF\n# and RDB persistence disabled, otherwise is completely ignored.\n#\n# An alternative (and sometimes better) way to obtain the same effect is\n# to use diskless replication on both master and replicas instances. However\n# in the case of replicas, diskless is not always an option.\nrdb-del-sync-files no\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir ./\n\n################################# REPLICATION #################################\n\n# Master-Replica replication. Use replicaof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n#   +------------------+      +---------------+\n#   |      Master      | ---> |    Replica    |\n#   | (receive writes) |      |  (exact copy) |\n#   +------------------+      +---------------+\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of replicas.\n# 2) Redis replicas are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition replicas automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# replicaof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the replica to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the replica request.\n#\n# masterauth <master-password>\n#\n# However this is not enough if you are using Redis ACLs (for Redis version\n# 6 or greater), and the default user is not capable of running the PSYNC\n# command and/or other commands needed for replication. In this case it's\n# better to configure a special user to use with replication, and specify the\n# masteruser configuration as such:\n#\n# masteruser <username>\n#\n# When masteruser is specified, the replica will authenticate against its\n# master using the new AUTH form: AUTH <username> <password>.\n\n# When a replica loses its connection with the master, or when the replication\n# is still in progress, the replica can act in two different ways:\n#\n# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) If replica-serve-stale-data is set to 'no' the replica will reply with\n#    an error \"SYNC with master in progress\" to all commands except:\n#    INFO, REPLICAOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG, SUBSCRIBE,\n#    UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB, COMMAND, POST,\n#    HOST and LATENCY.\n#\nreplica-serve-stale-data yes\n\n# You can configure a replica instance to accept writes or not. Writing against\n# a replica instance may be useful to store some ephemeral data (because data\n# written on a replica will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default replicas are read-only.\n#\n# Note: read only replicas are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only replica exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only replicas using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nreplica-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# New replicas and reconnecting replicas that are not able to continue the\n# replication process just receiving differences, need to do what is called a\n# \"full synchronization\". An RDB file is transmitted from the master to the\n# replicas.\n#\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the replicas incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to replica sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more replicas\n# can be queued and served with the RDB file as soon as the current child\n# producing the RDB file finishes its work. With diskless replication instead\n# once the transfer starts, new replicas arriving will be queued and a new\n# transfer will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple\n# replicas will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the replicas.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new replicas arriving, that will be queued for the next RDB transfer, so the\n# server waits a delay in order to let more replicas arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# -----------------------------------------------------------------------------\n# WARNING: RDB diskless load is experimental. Since in this setup the replica\n# does not immediately store an RDB on disk, it may cause data loss during\n# failovers. RDB diskless load + Redis modules not handling I/O reads may also\n# cause Redis to abort in case of I/O errors during the initial synchronization\n# stage with the master. Use only if your do what you are doing.\n# -----------------------------------------------------------------------------\n#\n# Replica can load the RDB it reads from the replication link directly from the\n# socket, or store the RDB to a file and read that file after it was completely\n# received from the master.\n#\n# In many cases the disk is slower than the network, and storing and loading\n# the RDB file may increase replication time (and even increase the master's\n# Copy on Write memory and salve buffers).\n# However, parsing the RDB file directly from the socket may mean that we have\n# to flush the contents of the current database before the full rdb was\n# received. For this reason we have the following options:\n#\n# \"disabled\"    - Don't use diskless load (store the rdb file to the disk first)\n# \"on-empty-db\" - Use diskless load only when it is completely safe.\n# \"swapdb\"      - Keep a copy of the current db contents in RAM while parsing\n#                 the data directly from the socket. note that this requires\n#                 sufficient memory, if you don't have it, you risk an OOM kill.\nrepl-diskless-load disabled\n\n# Replicas send PINGs to server in a predefined interval. It's possible to\n# change this interval with the repl_ping_replica_period option. The default\n# value is 10 seconds.\n#\n# repl-ping-replica-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of replica.\n# 2) Master timeout from the point of view of replicas (data, pings).\n# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-replica-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the replica. The default\n# value is 60 seconds.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the replica socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to replicas. But this can add a delay for\n# the data to appear on the replica side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the replica side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and replicas are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# replica data when replicas are disconnected for some time, so that when a\n# replica wants to reconnect again, often a full resync is not needed, but a\n# partial resync is enough, just passing the portion of data the replica\n# missed while disconnected.\n#\n# The bigger the replication backlog, the longer the replica can endure the\n# disconnect and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated if there is at least one replica connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no connected replicas for some time, the backlog will be\n# freed. The following option configures the amount of seconds that need to\n# elapse, starting from the time the last replica disconnected, for the backlog\n# buffer to be freed.\n#\n# Note that replicas never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with other replicas: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The replica priority is an integer number published by Redis in the INFO\n# output. It is used by Redis Sentinel in order to select a replica to promote\n# into a master if the master is no longer working correctly.\n#\n# A replica with a low priority number is considered better for promotion, so\n# for instance if there are three replicas with priority 10, 100, 25 Sentinel\n# will pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the replica as not able to perform the\n# role of master, so a replica with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nreplica-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N replicas connected, having a lag less or equal than M seconds.\n#\n# The N replicas need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the replica, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough replicas\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 replicas with a lag <= 10 seconds use:\n#\n# min-replicas-to-write 3\n# min-replicas-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-replicas-to-write is set to 0 (feature disabled) and\n# min-replicas-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# replicas in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover replica instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP address and port normally reported by a replica is\n# obtained in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the replica to connect with the master.\n#\n#   Port: The port is communicated by the replica during the replication\n#   handshake, and is normally the port that the replica is using to\n#   listen for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the replica may actually be reachable via different IP and port\n# pairs. The following two options can be used by a replica in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# replica-announce-ip 5.5.5.5\n# replica-announce-port 1234\n\n############################### KEYS TRACKING #################################\n\n# Redis implements server assisted support for client side caching of values.\n# This is implemented using an invalidation table that remembers, using\n# 16 millions of slots, what clients may have certain subsets of keys. In turn\n# this is used in order to send invalidation messages to clients. Please\n# check this page to understand more about the feature:\n#\n#   https://redis.io/topics/client-side-caching\n#\n# When tracking is enabled for a client, all the read only queries are assumed\n# to be cached: this will force Redis to store information in the invalidation\n# table. When keys are modified, such information is flushed away, and\n# invalidation messages are sent to the clients. However if the workload is\n# heavily dominated by reads, Redis could use more and more memory in order\n# to track the keys fetched by many clients.\n#\n# For this reason it is possible to configure a maximum fill value for the\n# invalidation table. By default it is set to 1M of keys, and once this limit\n# is reached, Redis will start to evict keys in the invalidation table\n# even if they were not modified, just to reclaim memory: this will in turn\n# force the clients to invalidate the cached values. Basically the table\n# maximum size is a trade off between the memory you want to spend server\n# side to track information about who cached what, and the ability of clients\n# to retain cached objects in memory.\n#\n# If you set the value to 0, it means there are no limits, and Redis will\n# retain as many keys as needed in the invalidation table.\n# In the \"stats\" INFO section, you can find information about the number of\n# keys in the invalidation table at every given moment.\n#\n# Note: when key tracking is used in broadcasting mode, no memory is used\n# in the server side so this setting is useless.\n#\n# tracking-table-max-keys 1000000\n\n################################## SECURITY ###################################\n\n# Warning: since Redis is pretty fast, an outside user can try up to\n# 1 million passwords per second against a modern box. This means that you\n# should use very strong passwords, otherwise they will be very easy to break.\n# Note that because the password is really a shared secret between the client\n# and the server, and should not be memorized by any human, the password\n# can be easily a long string from /dev/urandom or whatever, so by using a\n# long and unguessable password no brute force attack will be possible.\n\n# Redis ACL users are defined in the following format:\n#\n#   user <username> ... acl rules ...\n#\n# For example:\n#\n#   user worker +@list +@connection ~jobs:* on >ffa9203c493aa99\n#\n# The special username \"default\" is used for new connections. If this user\n# has the \"nopass\" rule, then new connections will be immediately authenticated\n# as the \"default\" user without the need of any password provided via the\n# AUTH command. Otherwise if the \"default\" user is not flagged with \"nopass\"\n# the connections will start in not authenticated state, and will require\n# AUTH (or the HELLO command AUTH option) in order to be authenticated and\n# start to work.\n#\n# The ACL rules that describe what a user can do are the following:\n#\n#  on           Enable the user: it is possible to authenticate as this user.\n#  off          Disable the user: it's no longer possible to authenticate\n#               with this user, however the already authenticated connections\n#               will still work.\n#  +<command>   Allow the execution of that command\n#  -<command>   Disallow the execution of that command\n#  +@<category> Allow the execution of all the commands in such category\n#               with valid categories are like @admin, @set, @sortedset, ...\n#               and so forth, see the full list in the server.c file where\n#               the Redis command table is described and defined.\n#               The special category @all means all the commands, but currently\n#               present in the server, and that will be loaded in the future\n#               via modules.\n#  +<command>|subcommand    Allow a specific subcommand of an otherwise\n#                           disabled command. Note that this form is not\n#                           allowed as negative like -DEBUG|SEGFAULT, but\n#                           only additive starting with \"+\".\n#  allcommands  Alias for +@all. Note that it implies the ability to execute\n#               all the future commands loaded via the modules system.\n#  nocommands   Alias for -@all.\n#  ~<pattern>   Add a pattern of keys that can be mentioned as part of\n#               commands. For instance ~* allows all the keys. The pattern\n#               is a glob-style pattern like the one of KEYS.\n#               It is possible to specify multiple patterns.\n#  allkeys      Alias for ~*\n#  resetkeys    Flush the list of allowed keys patterns.\n#  ><password>  Add this password to the list of valid password for the user.\n#               For example >mypass will add \"mypass\" to the list.\n#               This directive clears the \"nopass\" flag (see later).\n#  <<password>  Remove this password from the list of valid passwords.\n#  nopass       All the set passwords of the user are removed, and the user\n#               is flagged as requiring no password: it means that every\n#               password will work against this user. If this directive is\n#               used for the default user, every new connection will be\n#               immediately authenticated with the default user without\n#               any explicit AUTH command required. Note that the \"resetpass\"\n#               directive will clear this condition.\n#  resetpass    Flush the list of allowed passwords. Moreover removes the\n#               \"nopass\" status. After \"resetpass\" the user has no associated\n#               passwords and there is no way to authenticate without adding\n#               some password (or setting it as \"nopass\" later).\n#  reset        Performs the following actions: resetpass, resetkeys, off,\n#               -@all. The user returns to the same state it has immediately\n#               after its creation.\n#\n# ACL rules can be specified in any order: for instance you can start with\n# passwords, then flags, or key patterns. However note that the additive\n# and subtractive rules will CHANGE MEANING depending on the ordering.\n# For instance see the following example:\n#\n#   user alice on +@all -DEBUG ~* >somepassword\n#\n# This will allow \"alice\" to use all the commands with the exception of the\n# DEBUG command, since +@all added all the commands to the set of the commands\n# alice can use, and later DEBUG was removed. However if we invert the order\n# of two ACL rules the result will be different:\n#\n#   user alice on -DEBUG +@all ~* >somepassword\n#\n# Now DEBUG was removed when alice had yet no commands in the set of allowed\n# commands, later all the commands are added, so the user will be able to\n# execute everything.\n#\n# Basically ACL rules are processed left-to-right.\n#\n# For more information about ACL configuration please refer to\n# the Redis web site at https://redis.io/topics/acl\n\n# ACL LOG\n#\n# The ACL Log tracks failed commands and authentication events associated\n# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked\n# by ACLs. The ACL Log is stored in memory. You can reclaim memory with\n# ACL LOG RESET. Define the maximum entry length of the ACL Log below.\nacllog-max-len 128\n\n# Using an external ACL file\n#\n# Instead of configuring users here in this file, it is possible to use\n# a stand-alone file just listing users. The two methods cannot be mixed:\n# if you configure users here and at the same time you activate the external\n# ACL file, the server will refuse to start.\n#\n# The format of the external ACL user file is exactly the same as the\n# format that is used inside redis.conf to describe users.\n#\n# aclfile /etc/redis/users.acl\n\naclfile /etc/redis/users.acl\n\n# IMPORTANT NOTE: starting with Redis 6 \"requirepass\" is just a compatibility\n# layer on top of the new ACL system. The option effect will be just setting\n# the password for the default user. Clients will still authenticate using\n# AUTH <password> as usually, or more explicitly with AUTH default <password>\n# if they follow the new protocol: both will work.\n#\nrequirepass somepass\n\n# Command renaming (DEPRECATED).\n#\n# ------------------------------------------------------------------------\n# WARNING: avoid using this option if possible. Instead use ACLs to remove\n# commands from the default user, and put them only in some admin user you\n# create for administrative purposes.\n# ------------------------------------------------------------------------\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to replicas may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# IMPORTANT: When Redis Cluster is used, the max number of connections is also\n# shared with the cluster bus: every node in the cluster will use two\n# connections, one incoming and another outgoing. It is important to size the\n# limit accordingly in case of very large clusters.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have replicas attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the replicas are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of replicas is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have replicas attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for replica\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select one from the following behaviors:\n#\n# volatile-lru -> Evict using approximated LRU, only keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key having an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. By default Redis will check five keys and pick the one that was\n# used least recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n# Starting from Redis 5, by default a replica will ignore its maxmemory setting\n# (unless it is promoted to master after a failover or manually). It means\n# that the eviction of keys will be just handled by the master, sending the\n# DEL commands to the replica as keys evict in the master side.\n#\n# This behavior ensures that masters and replicas stay consistent, and is usually\n# what you want, however if your replica is writable, or you want the replica\n# to have a different memory setting, and you are sure all the writes performed\n# to the replica are idempotent, then you may change this default (but be sure\n# to understand what you are doing).\n#\n# Note that since the replica by default does not evict, it may end using more\n# memory than the one set via maxmemory (there are certain buffers that may\n# be larger on the replica, or data structures may sometimes take more memory\n# and so forth). So make sure you monitor your replicas and make sure they\n# have enough memory to never hit a real out-of-memory condition before the\n# master hits the configured maxmemory setting.\n#\n# replica-ignore-maxmemory yes\n\n# Redis reclaims expired keys in two ways: upon access when those keys are\n# found to be expired, and also in background, in what is called the\n# \"active expire key\". The key space is slowly and interactively scanned\n# looking for expired keys to reclaim, so that it is possible to free memory\n# of keys that are expired and will never be accessed again in a short time.\n#\n# The default effort of the expire cycle will try to avoid having more than\n# ten percent of expired keys still in memory, and will try to avoid consuming\n# more than 25% of total memory and to add latency to the system. However\n# it is possible to increase the expire \"effort\" that is normally set to\n# \"1\", to a greater value, up to the value \"10\". At its maximum value the\n# system will use more CPU, longer cycles (and technically may introduce\n# more latency), and will tolerate less already expired keys still present\n# in the system. It's a tradeoff between memory, CPU and latency.\n#\n# active-expire-effort 1\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a replica performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives.\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nreplica-lazy-flush no\n\n# It is also possible, for the case when to replace the user code DEL calls\n# with UNLINK calls is not easy, to modify the default behavior of the DEL\n# command to act exactly like UNLINK, using the following configuration\n# directive:\n\nlazyfree-lazy-user-del no\n\n################################ THREADED I/O #################################\n\n# Redis is mostly single threaded, however there are certain threaded\n# operations such as UNLINK, slow I/O accesses and other things that are\n# performed on side threads.\n#\n# Now it is also possible to handle Redis clients socket reads and writes\n# in different I/O threads. Since especially writing is so slow, normally\n# Redis users use pipelining in order to speed up the Redis performances per\n# core, and spawn multiple instances in order to scale more. Using I/O\n# threads it is possible to easily speedup two times Redis without resorting\n# to pipelining nor sharding of the instance.\n#\n# By default threading is disabled, we suggest enabling it only in machines\n# that have at least 4 or more cores, leaving at least one spare core.\n# Using more than 8 threads is unlikely to help much. We also recommend using\n# threaded I/O only if you actually have performance problems, with Redis\n# instances being able to use a quite big percentage of CPU time, otherwise\n# there is no point in using this feature.\n#\n# So for instance if you have a four cores boxes, try to use 2 or 3 I/O\n# threads, if you have a 8 cores, try to use 6 threads. In order to\n# enable I/O threads use the following configuration directive:\n#\n# io-threads 4\n#\n# Setting io-threads to 1 will just use the main thread as usual.\n# When I/O threads are enabled, we only use threads for writes, that is\n# to thread the write(2) syscall and transfer the client buffers to the\n# socket. However it is also possible to enable threading of reads and\n# protocol parsing using the following configuration directive, by setting\n# it to yes:\n#\n# io-threads-do-reads no\n#\n# Usually threading reads doesn't help much.\n#\n# NOTE 1: This configuration directive cannot be changed at runtime via\n# CONFIG SET. Aso this feature currently does not work when SSL is\n# enabled.\n#\n# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make\n# sure you also run the benchmark itself in threaded mode, using the\n# --threads option to match the number of Redis threads, otherwise you'll not\n# be able to notice the improvements.\n\n############################ KERNEL OOM CONTROL ##############################\n\n# On Linux, it is possible to hint the kernel OOM killer on what processes\n# should be killed first when out of memory.\n#\n# Enabling this feature makes Redis actively control the oom_score_adj value\n# for all its processes, depending on their role. The default scores will\n# attempt to have background child processes killed before all others, and\n# replicas killed before masters.\n#\n# Redis supports three options:\n#\n# no:       Don't make changes to oom-score-adj (default).\n# yes:      Alias to \"relative\" see below.\n# absolute: Values in oom-score-adj-values are written as is to the kernel.\n# relative: Values are used relative to the initial value of oom_score_adj when\n#           the server starts and are then clamped to a range of -1000 to 1000.\n#           Because typically the initial value is 0, they will often match the\n#           absolute values.\noom-score-adj no\n\n# When oom-score-adj is used, this directive controls the specific values used\n# for master, replica and background child processes. Values range -2000 to\n# 2000 (higher means more likely to be killed).\n#\n# Unprivileged processes (not root, and without CAP_SYS_RESOURCE capabilities)\n# can freely increase their value, but not decrease it below its initial\n# settings. This means that setting oom-score-adj to \"relative\" and setting the\n# oom-score-adj-values to positive values will always succeed.\noom-score-adj-values 0 200 800\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading, Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, then continues loading the AOF\n# tail.\naof-use-rdb-preamble yes\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet call any write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are a multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A replica of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a replica to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple replicas able to failover, they exchange messages\n#    in order to try to give an advantage to the replica with the best\n#    replication offset (more data from the master processed).\n#    Replicas will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single replica computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the replica will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a replica will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * cluster-replica-validity-factor) + repl-ping-replica-period\n#\n# So for example if node-timeout is 30 seconds, and the cluster-replica-validity-factor\n# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the\n# replica will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large cluster-replica-validity-factor may allow replicas with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a replica at all.\n#\n# For maximum availability, it is possible to set the cluster-replica-validity-factor\n# to a value of 0, which means, that replicas will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-replica-validity-factor 10\n\n# Cluster replicas are able to migrate to orphaned masters, that are masters\n# that are left without working replicas. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working replicas.\n#\n# Replicas migrate to orphaned masters only if there are still at least a\n# given number of other working replicas for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a replica\n# will migrate only if there is at least 1 other working replica for its master\n# and so forth. It usually reflects the number of replicas you want for every\n# master in your cluster.\n#\n# Default is 1 (replicas migrate only if their masters remain with at least\n# one replica). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least a hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents replicas from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-replica-no-failover no\n\n# This option, when set to yes, allows nodes to serve read traffic while the\n# the cluster is in a down state, as long as it believes it owns the slots.\n#\n# This is useful for two cases.  The first case is for when an application\n# doesn't require consistency of data during node failures or network partitions.\n# One example of this is a cache, where as long as the node has the data it\n# should be able to serve it.\n#\n# The second use case is for configurations that don't meet the recommended\n# three shards but want to enable cluster mode and scale later. A\n# master outage in a 1 or 2 shard configuration causes a read/write outage to the\n# entire cluster without this option set, with it set there is only a write outage.\n# Without a quorum of masters, slot ownership will not change automatically.\n#\n# cluster-allow-reads-when-down no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instructs the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usual.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  t     Stream commands\n#  m     Key-miss events (Note: It is not included in the 'A' class)\n#  A     Alias for g$lshzxet, so that the \"AKE\" string means all the events\n#        (Except key-miss events which are excluded from 'A' due to their\n#         unique nature).\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### GOPHER SERVER #################################\n\n# Redis contains an implementation of the Gopher protocol, as specified in\n# the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt).\n#\n# The Gopher protocol was very popular in the late '90s. It is an alternative\n# to the web, and the implementation both server and client side is so simple\n# that the Redis server has just 100 lines of code in order to implement this\n# support.\n#\n# What do you do with Gopher nowadays? Well Gopher never *really* died, and\n# lately there is a movement in order for the Gopher more hierarchical content\n# composed of just plain text documents to be resurrected. Some want a simpler\n# internet, others believe that the mainstream internet became too much\n# controlled, and it's cool to create an alternative space for people that\n# want a bit of fresh air.\n#\n# Anyway for the 10nth birthday of the Redis, we gave it the Gopher protocol\n# as a gift.\n#\n# --- HOW IT WORKS? ---\n#\n# The Redis Gopher support uses the inline protocol of Redis, and specifically\n# two kind of inline requests that were anyway illegal: an empty request\n# or any request that starts with \"/\" (there are no Redis commands starting\n# with such a slash). Normal RESP2/RESP3 requests are completely out of the\n# path of the Gopher protocol implementation and are served as usual as well.\n#\n# If you open a connection to Redis when Gopher is enabled and send it\n# a string like \"/foo\", if there is a key named \"/foo\" it is served via the\n# Gopher protocol.\n#\n# In order to create a real Gopher \"hole\" (the name of a Gopher site in Gopher\n# talking), you likely need a script like the following:\n#\n#   https://github.com/antirez/gopher2redis\n#\n# --- SECURITY WARNING ---\n#\n# If you plan to put Redis on the internet in a publicly accessible address\n# to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance.\n# Once a password is set:\n#\n#   1. The Gopher server (when enabled, not by default) will still serve\n#      content via Gopher.\n#   2. However other commands cannot be called before the client will\n#      authenticate.\n#\n# So use the 'requirepass' option to protect your instance.\n#\n# Note that Gopher is not currently supported when 'io-threads-do-reads'\n# is enabled.\n#\n# To enable Gopher support, uncomment the following line and set the option\n# from no (the default) to yes.\n#\n# gopher-enabled no\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Streams macro node max size / items. The stream data structure is a radix\n# tree of big nodes that encode multiple items inside. Using this configuration\n# it is possible to configure how big a single node can be in bytes, and the\n# maximum number of items it may contain before switching to a new node when\n# appending new stream entries. If any of the following settings are set to\n# zero, the limit is ignored, so for instance it is possible to set just a\n# max entires limit by setting max-bytes to 0 and max-entries to the desired\n# value.\nstream-node-max-bytes 4096\nstream-node-max-entries 100\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# replica  -> replica clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and replica clients, since\n# subscribers and replicas receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit replica 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here, but must be 1mb or greater\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# Normally it is useful to have an HZ value which is proportional to the\n# number of clients connected. This is useful in order, for instance, to\n# avoid too many clients are processed for each background task invocation\n# in order to avoid latency spikes.\n#\n# Since the default HZ value by default is conservatively set to 10, Redis\n# offers, and enables by default, the ability to use an adaptive HZ value\n# which will temporarily raise when there are many connected clients.\n#\n# When dynamic HZ is enabled, the actual configured HZ will be used\n# as a baseline, but multiples of the configured HZ value will be actually\n# used as needed once more clients are connected. In this way an idle\n# instance will use very little CPU time while a busy instance will be\n# more responsive.\ndynamic-hz yes\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# When redis saves RDB file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\nrdb-save-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in a \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag no\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage, to be used when the lower\n# threshold is reached\n# active-defrag-cycle-min 1\n\n# Maximal effort for defrag in CPU percentage, to be used when the upper\n# threshold is reached\n# active-defrag-cycle-max 25\n\n# Maximum number of set/hash/zset/list fields that will be processed from\n# the main dictionary scan\n# active-defrag-max-scan-fields 1000\n\n# Jemalloc background thread for purging will be enabled by default\njemalloc-bg-thread yes\n\n# It is possible to pin different threads and processes of Redis to specific\n# CPUs in your system, in order to maximize the performances of the server.\n# This is useful both in order to pin different Redis threads in different\n# CPUs, but also in order to make sure that multiple Redis instances running\n# in the same host will be pinned to different CPUs.\n#\n# Normally you can do this using the \"taskset\" command, however it is also\n# possible to this via Redis configuration directly, both in Linux and FreeBSD.\n#\n# You can pin the server/IO threads, bio threads, aof rewrite child process, and\n# the bgsave child process. The syntax to specify the cpu list is the same as\n# the taskset command:\n#\n# Set redis server/io threads to cpu affinity 0,2,4,6:\n# server_cpulist 0-7:2\n#\n# Set bio threads to cpu affinity 1,3:\n# bio_cpulist 1,3\n#\n# Set aof rewrite child process to cpu affinity 8,9,10,11:\n# aof_rewrite_cpulist 8-11\n#\n# Set bgsave child process to cpu affinity 1,10,11\n# bgsave_cpulist 1,10-11\n\n# In some cases redis will emit warnings and even refuse to start if it detects\n# that the system is in bad state, it is possible to suppress these warnings\n# by setting the following config which takes a space delimited list of warnings\n# to suppress\n#\n# ignore-warnings ARM64-COW-BUG\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/sentinel.Dockerfile",
    "content": "FROM redis:6.2.6-alpine\nCOPY sentinel.conf sentinel.users.acl /etc/redis/\nENTRYPOINT [ \"redis-server\", \"/etc/redis/sentinel.conf\", \"--sentinel\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/sentinel.conf",
    "content": "port 0\nport 26379\naclfile /etc/redis/sentinel.users.acl\n\ndir /tmp\nsentinel resolve-hostnames yes\n\nsentinel monitor primary_group_1 p1 6379 2\nsentinel down-after-milliseconds primary_group_1 5000\nsentinel parallel-syncs primary_group_1 1\nsentinel failover-timeout primary_group_1 10000\nsentinel auth-pass primary_group_1 defaultpass\n\nsentinel monitor primary_group_2 p2 6379 2\nsentinel down-after-milliseconds primary_group_2 5000\nsentinel parallel-syncs primary_group_2 1\nsentinel failover-timeout primary_group_2 10000\nsentinel auth-pass primary_group_2 defaultpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/sentinel.users.acl",
    "content": "user default on +@all ~* >sentinelpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent/users.acl",
    "content": "user default on +@all ~* >defaultpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/Dockerfile",
    "content": "FROM redis:6.2.6-alpine\nCOPY redis.conf users.acl certs/* /etc/redis/\nENTRYPOINT [ \"redis-server\", \"/etc/redis/redis.conf\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/certs/ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/certs/redis.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhEwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI3WhcNMzExMDI2MTMyNzI3WjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAM4osMW/hBlde+/E20wP9X+zJ0AMD6OtfhqQ5brC\ngbVs9mPccZ/8R0fj83YtZfnoEodMZ/7yUTeCCPMdprAezMU1KBf9EpZmTdWhpO3e\nkESHcQdsKkqGtyjYF7dDahTmKt4a4aHlPH0DJLltB5HlbVabkzlo+3S8QaNwH5lY\nyJTQIqiqVzs9oRLT76nZuJjsym0dNXE42rO3KCniI6kvJDmUzBD8Wc94iDExfy7q\nqHyV7b2DCp1w7XP4yrQAFQ6kiVqNcfTTAO4MHNP54V2nZLPdOsUD5BsYY8hu0HDc\n/PisZ9ZMcw7LMfpUd3dfA9zefQXPQsWJK20ZCNmdIFtwvIFUpYu/FEF3FWO83zeI\nXkVZiuCOnoFvp8JIDvXgzXUBWzvYmxLqVSZAuabqU5pKRswDPLGlZTkHbuP2DiXD\nLD5AsTnICpzYkUeERSZKf2qc/nTUk04W/7FUT/75ItVzZvu90mPJlmArB0j4zdAG\nKwKo8v/cF1hA1YznhibxcUAA/Q/O3Y6CPQ7C3NeaGKcycgUxWoEY3Leno40ukijd\nR0MvsaY7V0/up37fkPtH9rcCkZOGVT5Q4Ww9gVO3yseXVkxbJyzHV1tuwg6yY9wO\nLOU2Bbmazkjlkb8a5OyQol2zZNJ0L3lvRWTildtGUTsBkeqI6HAedTTHJkhjHh/P\nP0+TAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nAKn+aH60zdd4ItMOhgd4BIok/u6S4NF0oe4kDwC/EU5C0hbuT6gLAn7ArHMeadue\nOuSnhqIPfxNR9WCes9OU1O00WeCuWCE4XfNMMFN5iDFfLxO4Oy5Adt0T74hWbcy6\nh28TdcjrkJEr7HR59G5tQ8TW5gVB4a0WXDw0ob9DSxbFKZU1uZm9L/+MgB/SNCHL\nGZSKt75Z/M10b9BTC3OG9swsoWvXEjR2ICiwzk+LxVf5K38faDyBrNJVglrpEUZz\ngP60kL73qK0y1/i35UuP0yIJIy48XnDsSByN7eBVsNTGMW3CFLKWA4RVfnEHNUff\nvsLHXZFYsUIPnPc5jksFwb/wKAe9JbCrgQPhBYaIYkRGiYt64C48r3boIIVoz9+1\n9Nq0Ik06fCzlI9APq2nzEiVeB7mDyZ692neu32QM6zRkYor+W8uI21YnRJWlOx7+\nx2GIh2EZnEYNvbpbvk/fV5AqkYOu9auRAkcKfME7dJ3Gwndl0YBOjE2DMTv6vIjS\ndVuGXQCvlzkRAnPMh5MR5/bSUKVvBryXs9ecAMgoVXBVB+4tGWct5ziL+8qyNtgA\nWJ2EWj3xtLlMwwQmLjRsCrZjL4liLJG8Yn8Ehfq1rRJREH2O8uYKCO1fdhuI0Y5S\niBPfqJi6QBHj7i01K9OpNUB7l+xAFLA3cBsegcm2GPoL\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/certs/redis.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDOKLDFv4QZXXvv\nxNtMD/V/sydADA+jrX4akOW6woG1bPZj3HGf/EdH4/N2LWX56BKHTGf+8lE3ggjz\nHaawHszFNSgX/RKWZk3VoaTt3pBEh3EHbCpKhrco2Be3Q2oU5ireGuGh5Tx9AyS5\nbQeR5W1Wm5M5aPt0vEGjcB+ZWMiU0CKoqlc7PaES0++p2biY7MptHTVxONqztygp\n4iOpLyQ5lMwQ/FnPeIgxMX8u6qh8le29gwqdcO1z+Mq0ABUOpIlajXH00wDuDBzT\n+eFdp2Sz3TrFA+QbGGPIbtBw3Pz4rGfWTHMOyzH6VHd3XwPc3n0Fz0LFiSttGQjZ\nnSBbcLyBVKWLvxRBdxVjvN83iF5FWYrgjp6Bb6fCSA714M11AVs72JsS6lUmQLmm\n6lOaSkbMAzyxpWU5B27j9g4lwyw+QLE5yAqc2JFHhEUmSn9qnP501JNOFv+xVE/+\n+SLVc2b7vdJjyZZgKwdI+M3QBisCqPL/3BdYQNWM54Ym8XFAAP0Pzt2Ogj0OwtzX\nmhinMnIFMVqBGNy3p6ONLpIo3UdDL7GmO1dP7qd+35D7R/a3ApGThlU+UOFsPYFT\nt8rHl1ZMWycsx1dbbsIOsmPcDizlNgW5ms5I5ZG/GuTskKJds2TSdC95b0Vk4pXb\nRlE7AZHqiOhwHnU0xyZIYx4fzz9PkwIDAQABAoICAHyc/+0oDHNAnK+bsGrTorNj\n2S/Pmox3TChGuXYgKEM/79cA4vWvim6cDQe7/U4Hx1tdBeeHFSyWP06k96Kxm1kA\n/pExedDLWfTt1kGqLE4gCGRSL2YI9CGOLRerei3TysmiOgygAeYWxlYG33KC2Ypm\nU6F6IbS4LnzaQ19v2R6KiMim3j+CyyAUV2O1pO1bBCjcZPdhRGEpLu/SL3gOdLkR\nhiAmSSstUjVaE+SKFvnnrmLFGN996SoWkoAnJJNLRXMk2GMCQCejzrEa8+ymSCqo\naOO5rGHsZjQ7N2dhTNALdmCEqW+hxz3nXKcdGbqiCbQ/Sb8ZYNR7M2xGm85p4Ka9\n0UK4cOM1VJPwz8hotSKAUmXnpbu73CsZi5HyzwZkk4FpcaYCYrCbGVmm1cIKEKI7\n8SN/oqgFdj4Ha9cemnu+RecQZouK+wPWtcILd2kstJl52TV952fVOrnXQDo6XCXB\nfbs9IYN1hB6N79xv4L7Jj53hSRMeSNf70Ejkh1FXPOvmvFT12wy5JQdBBR5nnb4a\nGEsMpGVe1k3bxjK7K263tLSH0UZ8dMgdSx1E4D1hT1K/gEwTSMOJ0E1R0M6SJmF2\n6TnZ0MbJWx6PbICmyZrd2agfTQrq6CgY1fWLGbQrtnwXtsUR7PiHarydXfs3V8g1\nxHnK1bItOBBMOMcWV93hAoIBAQD2xMceUfBUN0TeWrJ4izD3lUYxmWqjTg7kPcjJ\n0VN8v3txGAcyPmbuz5AEzvdFjTispoZNB9QOrmsUVWTQDE5otnei9kHWzqJWYHg4\nUSuUuAh8OJGCiepo8zHT3qHDNhKGtOAp5LC8TaOznUFr35zCBCOsvQfRUKrv5IOc\nvCFjO07Xly8+M3qK7/UswRQ6480VlE2t1p+VNaORHdTDg2tes3/9owuiNmR/sPT8\nnIoe01LS7qmZoiB1vracaLcBf1Iwd7RvKg7mgFJzmowZUYxyX2YGK5qZ1h74In2X\n55+qQnNW0RwPijopTv711pMhMKWl8i3ilcCfoeBXz8zCwFfbAoIBAQDV3wHAO7ic\nMYP/Bm5jgIwlix1eOWY/yB+VqdYn2GmC49vTEIlIVlFRq0vZE06iUxs87BIV08zO\n4w/iKXd7ktkuhphiEuU2yXA3LQPHpbSOW43RONbf4glFU/DlP/P6fiybbWj6+f7L\n7Zbvtz5AW03Y4ZpagJTqOgVdJ0MdLnh9vZj6okGGV1fidKtG6hr7Z/oLhnl9aAZK\n4vrvBZ//qz99vEVByiV5kRaJDulu+diBy4n6iBjzjHA5a9e7lY3sUBw3DMgb7kYs\nJJPkCPdSxCYq4Ef3z/Eao0tyUuCzyznfCMGJA1gBdTpwDNDCTaXqkjR5nvsdE5k0\nIVQgFPtcOPCpAoIBABujNlni+3OzLPdqWQq/LCDOiyoK8LKRj4FomhBgbWVPXNfx\nxPyPmJ+uh4bCV1dm1a4giHIgKlPqnPuOBNh4SF/Z79REmGMiiXP7IfvMu4DQi8K9\n4y4nnCVc93uvN5bRe4mywFhw0IqGd4sqVaVrSfdA124FTdbXng14HnVzbJncjpv+\nxr/ErDjbXy5AAbAGy3VbQsfxfbYMZ+Fc4fNzyJa2q+MQW8EzLlZOz2Frdty09lXB\nfSVDzzbgwTsLT1PPmrjq7z50C28teA6ShJZhV8WHgbm3MH2CSb2ov0BAJNXA04Ip\nsWbcKF9wBYYrHhddh2/qi9EQzJ4UVzf+ggRd3nkCggEAWcjyWjp4KRJcgJ65jwoz\nS7uYS6s7MsGYCOOw5S9kNC/mZDhH+ddK8kdAY1RIqbrL74qHmSQ+kgge7epMn9Mp\nW+/jXyDhm1t7wZ4jPRhisXTcF56ODpU9IR65PfTYPyvjHCkVbm+vOPt4ZxB9kNUD\n3G3xt9bNLXvILrBB66lLqjYDWAzwBy751Tb3hKDZTPv8rAP7Uttt8NhTUi8BWXsR\n/34fcRwlGWEAne9lrlIzQ2IofcXO+8fUgTa17ak+WJvVDINQKvGgAf4lHBFrixKP\nl2ZqsC1a4bz1+nuym6hQlkJ9xUBjHNGTA+FNbpTcd5qDbx9/+lf09D6dq45DbBb3\naQKCAQBrnFYocTm/fIeKo1n1kyF2ULkd6k984ztH8UyluXleSS1ShFFoo/x3vz35\nfsZNUggRnSe7/OBGZYquF/1roVULI1hKh4tbEmW4SWNeTFvwXKdRe6T7NnWSZtS/\nKtamA3lT2wtoEVOvfMo8M0hoFuRWdT2M0i+LKZQdRsq18XPLqdHt1kkSNcnPDERm\n4gLQ8zXTf2fHrtZmyM8fc0GuTVwprPFeJkLtSPehkeXSTgb6rpyelX9NBUILwRgP\nnw0+cbjFDFKaLnIrMFoVAAn/8DcnbbSt1TZhgNsMxY+GHWPBYW8SUi5nBmQQtmA7\nn3ju44acIPvJ9sWuZruVlWZGFaHm\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-sent-tls-auth/.env\n  redis:\n    build:\n      context: &build ./oss-sent-tls-auth\n      dockerfile: sentinel.Dockerfile\n    links:\n      - p1:p1\n      - p2:p2\n    depends_on:\n      - p1\n      - s1_1\n      - s1_2\n      - p2\n      - s2_1\n      - s2_2\n  p1:\n    build: *build\n  s1_1:\n    build: *build\n    command: --slaveof p1 6379 --masterauth defaultpass\n  s1_2:\n    build: *build\n    command: --slaveof p1 6379 --masterauth defaultpass\n  p2:\n    build: *build\n  s2_1:\n    build: *build\n    command: --slaveof p2 6379 --masterauth defaultpass\n  s2_2:\n    build: *build\n    command: --slaveof p2 6379 --masterauth defaultpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Note that option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\n# loadmodule /path/to/my_module.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all available network interfaces on the host machine.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only on the\n# IPv4 loopback interface address (this means Redis will only be able to\n# accept client connections from the same host that it is running on).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT OUT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# bind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\n# protected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 0\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need a high backlog in order\n# to avoid slow clients connection issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Force network equipment in the middle to consider the connection to be\n#    alive.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# TLS/SSL #####################################\n\n# By default, TLS/SSL is disabled. To enable it, the \"tls-port\" configuration\n# directive can be used to define TLS-listening ports. To enable TLS on the\n# default port, use:\n#\n# port 0\ntls-port 6379\n\n# Configure a X.509 certificate and private key to use for authenticating the\n# server to connected clients, masters or cluster peers.  These files should be\n# PEM formatted.\n#\ntls-cert-file /etc/redis/redis.crt\ntls-key-file /etc/redis/redis.key\n\n# Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange:\n#\n# tls-dh-params-file redis.dh\n\n# Configure a CA certificate(s) bundle or directory to authenticate TLS/SSL\n# clients and peers.  Redis requires an explicit configuration of at least one\n# of these, and will not implicitly use the system wide configuration.\n#\ntls-ca-cert-file /etc/redis/ca.crt\n# tls-ca-cert-dir /etc/ssl/certs\n\n# By default, clients (including replica servers) on a TLS port are required\n# to authenticate using valid client side certificates.\n#\n# If \"no\" is specified, client certificates are not required and not accepted.\n# If \"optional\" is specified, client certificates are accepted and must be\n# valid if provided, but are not required.\n#\ntls-auth-clients yes\n# tls-auth-clients optional\n\n# By default, a Redis replica does not attempt to establish a TLS connection\n# with its master.\n#\n# Use the following directive to enable TLS on replication links.\n#\ntls-replication yes\n\n# By default, the Redis Cluster bus uses a plain TCP connection. To enable\n# TLS for the bus protocol, use the following directive:\n#\n# tls-cluster yes\n\n# Explicitly specify TLS versions to support. Allowed values are case insensitive\n# and include \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\" (OpenSSL >= 1.1.1) or\n# any combination. To enable only TLSv1.2 and TLSv1.3, use:\n#\n# tls-protocols \"TLSv1.2 TLSv1.3\"\n\n# Configure allowed ciphers.  See the ciphers(1ssl) manpage for more information\n# about the syntax of this string.\n#\n# Note: this configuration applies only to <= TLSv1.2.\n#\n# tls-ciphers DEFAULT:!MEDIUM\n\n# Configure allowed TLSv1.3 ciphersuites.  See the ciphers(1ssl) manpage for more\n# information about the syntax of this string, and specifically for TLSv1.3\n# ciphersuites.\n#\n# tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256\n\n# When choosing a cipher, use the server's preference instead of the client\n# preference. By default, the server follows the client's preference.\n#\n# tls-prefer-server-ciphers yes\n\n# By default, TLS session caching is enabled to allow faster and less expensive\n# reconnections by clients that support it. Use the following directive to disable\n# caching.\n#\n# tls-session-caching no\n\n# Change the default number of TLS sessions cached. A zero value sets the cache\n# to unlimited size. The default size is 20480.\n#\n# tls-session-cache-size 5000\n\n# Change the default timeout of cached TLS sessions. The default timeout is 300\n# seconds.\n#\n# tls-session-cache-timeout 60\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#                        requires \"expect stop\" in your upstart job config\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile /var/run/redis_6379.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\nlogfile \"\"\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behavior will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# By default compression is enabled as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# Remove RDB files used by replication in instances without persistence\n# enabled. By default this option is disabled, however there are environments\n# where for regulations or other security concerns, RDB files persisted on\n# disk by masters in order to feed replicas, or stored on disk by replicas\n# in order to load them for the initial synchronization, should be deleted\n# ASAP. Note that this option ONLY WORKS in instances that have both AOF\n# and RDB persistence disabled, otherwise is completely ignored.\n#\n# An alternative (and sometimes better) way to obtain the same effect is\n# to use diskless replication on both master and replicas instances. However\n# in the case of replicas, diskless is not always an option.\nrdb-del-sync-files no\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir ./\n\n################################# REPLICATION #################################\n\n# Master-Replica replication. Use replicaof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n#   +------------------+      +---------------+\n#   |      Master      | ---> |    Replica    |\n#   | (receive writes) |      |  (exact copy) |\n#   +------------------+      +---------------+\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of replicas.\n# 2) Redis replicas are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition replicas automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# replicaof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the replica to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the replica request.\n#\n# masterauth <master-password>\n#\n# However this is not enough if you are using Redis ACLs (for Redis version\n# 6 or greater), and the default user is not capable of running the PSYNC\n# command and/or other commands needed for replication. In this case it's\n# better to configure a special user to use with replication, and specify the\n# masteruser configuration as such:\n#\n# masteruser <username>\n#\n# When masteruser is specified, the replica will authenticate against its\n# master using the new AUTH form: AUTH <username> <password>.\n\n# When a replica loses its connection with the master, or when the replication\n# is still in progress, the replica can act in two different ways:\n#\n# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) If replica-serve-stale-data is set to 'no' the replica will reply with\n#    an error \"SYNC with master in progress\" to all commands except:\n#    INFO, REPLICAOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG, SUBSCRIBE,\n#    UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB, COMMAND, POST,\n#    HOST and LATENCY.\n#\nreplica-serve-stale-data yes\n\n# You can configure a replica instance to accept writes or not. Writing against\n# a replica instance may be useful to store some ephemeral data (because data\n# written on a replica will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default replicas are read-only.\n#\n# Note: read only replicas are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only replica exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only replicas using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nreplica-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# New replicas and reconnecting replicas that are not able to continue the\n# replication process just receiving differences, need to do what is called a\n# \"full synchronization\". An RDB file is transmitted from the master to the\n# replicas.\n#\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the replicas incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to replica sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more replicas\n# can be queued and served with the RDB file as soon as the current child\n# producing the RDB file finishes its work. With diskless replication instead\n# once the transfer starts, new replicas arriving will be queued and a new\n# transfer will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple\n# replicas will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the replicas.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new replicas arriving, that will be queued for the next RDB transfer, so the\n# server waits a delay in order to let more replicas arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# -----------------------------------------------------------------------------\n# WARNING: RDB diskless load is experimental. Since in this setup the replica\n# does not immediately store an RDB on disk, it may cause data loss during\n# failovers. RDB diskless load + Redis modules not handling I/O reads may also\n# cause Redis to abort in case of I/O errors during the initial synchronization\n# stage with the master. Use only if your do what you are doing.\n# -----------------------------------------------------------------------------\n#\n# Replica can load the RDB it reads from the replication link directly from the\n# socket, or store the RDB to a file and read that file after it was completely\n# received from the master.\n#\n# In many cases the disk is slower than the network, and storing and loading\n# the RDB file may increase replication time (and even increase the master's\n# Copy on Write memory and salve buffers).\n# However, parsing the RDB file directly from the socket may mean that we have\n# to flush the contents of the current database before the full rdb was\n# received. For this reason we have the following options:\n#\n# \"disabled\"    - Don't use diskless load (store the rdb file to the disk first)\n# \"on-empty-db\" - Use diskless load only when it is completely safe.\n# \"swapdb\"      - Keep a copy of the current db contents in RAM while parsing\n#                 the data directly from the socket. note that this requires\n#                 sufficient memory, if you don't have it, you risk an OOM kill.\nrepl-diskless-load disabled\n\n# Replicas send PINGs to server in a predefined interval. It's possible to\n# change this interval with the repl_ping_replica_period option. The default\n# value is 10 seconds.\n#\n# repl-ping-replica-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of replica.\n# 2) Master timeout from the point of view of replicas (data, pings).\n# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-replica-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the replica. The default\n# value is 60 seconds.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the replica socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to replicas. But this can add a delay for\n# the data to appear on the replica side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the replica side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and replicas are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# replica data when replicas are disconnected for some time, so that when a\n# replica wants to reconnect again, often a full resync is not needed, but a\n# partial resync is enough, just passing the portion of data the replica\n# missed while disconnected.\n#\n# The bigger the replication backlog, the longer the replica can endure the\n# disconnect and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated if there is at least one replica connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no connected replicas for some time, the backlog will be\n# freed. The following option configures the amount of seconds that need to\n# elapse, starting from the time the last replica disconnected, for the backlog\n# buffer to be freed.\n#\n# Note that replicas never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with other replicas: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The replica priority is an integer number published by Redis in the INFO\n# output. It is used by Redis Sentinel in order to select a replica to promote\n# into a master if the master is no longer working correctly.\n#\n# A replica with a low priority number is considered better for promotion, so\n# for instance if there are three replicas with priority 10, 100, 25 Sentinel\n# will pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the replica as not able to perform the\n# role of master, so a replica with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nreplica-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N replicas connected, having a lag less or equal than M seconds.\n#\n# The N replicas need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the replica, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough replicas\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 replicas with a lag <= 10 seconds use:\n#\n# min-replicas-to-write 3\n# min-replicas-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-replicas-to-write is set to 0 (feature disabled) and\n# min-replicas-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# replicas in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover replica instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP address and port normally reported by a replica is\n# obtained in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the replica to connect with the master.\n#\n#   Port: The port is communicated by the replica during the replication\n#   handshake, and is normally the port that the replica is using to\n#   listen for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the replica may actually be reachable via different IP and port\n# pairs. The following two options can be used by a replica in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# replica-announce-ip 5.5.5.5\n# replica-announce-port 1234\n\n############################### KEYS TRACKING #################################\n\n# Redis implements server assisted support for client side caching of values.\n# This is implemented using an invalidation table that remembers, using\n# 16 millions of slots, what clients may have certain subsets of keys. In turn\n# this is used in order to send invalidation messages to clients. Please\n# check this page to understand more about the feature:\n#\n#   https://redis.io/topics/client-side-caching\n#\n# When tracking is enabled for a client, all the read only queries are assumed\n# to be cached: this will force Redis to store information in the invalidation\n# table. When keys are modified, such information is flushed away, and\n# invalidation messages are sent to the clients. However if the workload is\n# heavily dominated by reads, Redis could use more and more memory in order\n# to track the keys fetched by many clients.\n#\n# For this reason it is possible to configure a maximum fill value for the\n# invalidation table. By default it is set to 1M of keys, and once this limit\n# is reached, Redis will start to evict keys in the invalidation table\n# even if they were not modified, just to reclaim memory: this will in turn\n# force the clients to invalidate the cached values. Basically the table\n# maximum size is a trade off between the memory you want to spend server\n# side to track information about who cached what, and the ability of clients\n# to retain cached objects in memory.\n#\n# If you set the value to 0, it means there are no limits, and Redis will\n# retain as many keys as needed in the invalidation table.\n# In the \"stats\" INFO section, you can find information about the number of\n# keys in the invalidation table at every given moment.\n#\n# Note: when key tracking is used in broadcasting mode, no memory is used\n# in the server side so this setting is useless.\n#\n# tracking-table-max-keys 1000000\n\n################################## SECURITY ###################################\n\n# Warning: since Redis is pretty fast, an outside user can try up to\n# 1 million passwords per second against a modern box. This means that you\n# should use very strong passwords, otherwise they will be very easy to break.\n# Note that because the password is really a shared secret between the client\n# and the server, and should not be memorized by any human, the password\n# can be easily a long string from /dev/urandom or whatever, so by using a\n# long and unguessable password no brute force attack will be possible.\n\n# Redis ACL users are defined in the following format:\n#\n#   user <username> ... acl rules ...\n#\n# For example:\n#\n#   user worker +@list +@connection ~jobs:* on >ffa9203c493aa99\n#\n# The special username \"default\" is used for new connections. If this user\n# has the \"nopass\" rule, then new connections will be immediately authenticated\n# as the \"default\" user without the need of any password provided via the\n# AUTH command. Otherwise if the \"default\" user is not flagged with \"nopass\"\n# the connections will start in not authenticated state, and will require\n# AUTH (or the HELLO command AUTH option) in order to be authenticated and\n# start to work.\n#\n# The ACL rules that describe what a user can do are the following:\n#\n#  on           Enable the user: it is possible to authenticate as this user.\n#  off          Disable the user: it's no longer possible to authenticate\n#               with this user, however the already authenticated connections\n#               will still work.\n#  +<command>   Allow the execution of that command\n#  -<command>   Disallow the execution of that command\n#  +@<category> Allow the execution of all the commands in such category\n#               with valid categories are like @admin, @set, @sortedset, ...\n#               and so forth, see the full list in the server.c file where\n#               the Redis command table is described and defined.\n#               The special category @all means all the commands, but currently\n#               present in the server, and that will be loaded in the future\n#               via modules.\n#  +<command>|subcommand    Allow a specific subcommand of an otherwise\n#                           disabled command. Note that this form is not\n#                           allowed as negative like -DEBUG|SEGFAULT, but\n#                           only additive starting with \"+\".\n#  allcommands  Alias for +@all. Note that it implies the ability to execute\n#               all the future commands loaded via the modules system.\n#  nocommands   Alias for -@all.\n#  ~<pattern>   Add a pattern of keys that can be mentioned as part of\n#               commands. For instance ~* allows all the keys. The pattern\n#               is a glob-style pattern like the one of KEYS.\n#               It is possible to specify multiple patterns.\n#  allkeys      Alias for ~*\n#  resetkeys    Flush the list of allowed keys patterns.\n#  ><password>  Add this password to the list of valid password for the user.\n#               For example >mypass will add \"mypass\" to the list.\n#               This directive clears the \"nopass\" flag (see later).\n#  <<password>  Remove this password from the list of valid passwords.\n#  nopass       All the set passwords of the user are removed, and the user\n#               is flagged as requiring no password: it means that every\n#               password will work against this user. If this directive is\n#               used for the default user, every new connection will be\n#               immediately authenticated with the default user without\n#               any explicit AUTH command required. Note that the \"resetpass\"\n#               directive will clear this condition.\n#  resetpass    Flush the list of allowed passwords. Moreover removes the\n#               \"nopass\" status. After \"resetpass\" the user has no associated\n#               passwords and there is no way to authenticate without adding\n#               some password (or setting it as \"nopass\" later).\n#  reset        Performs the following actions: resetpass, resetkeys, off,\n#               -@all. The user returns to the same state it has immediately\n#               after its creation.\n#\n# ACL rules can be specified in any order: for instance you can start with\n# passwords, then flags, or key patterns. However note that the additive\n# and subtractive rules will CHANGE MEANING depending on the ordering.\n# For instance see the following example:\n#\n#   user alice on +@all -DEBUG ~* >somepassword\n#\n# This will allow \"alice\" to use all the commands with the exception of the\n# DEBUG command, since +@all added all the commands to the set of the commands\n# alice can use, and later DEBUG was removed. However if we invert the order\n# of two ACL rules the result will be different:\n#\n#   user alice on -DEBUG +@all ~* >somepassword\n#\n# Now DEBUG was removed when alice had yet no commands in the set of allowed\n# commands, later all the commands are added, so the user will be able to\n# execute everything.\n#\n# Basically ACL rules are processed left-to-right.\n#\n# For more information about ACL configuration please refer to\n# the Redis web site at https://redis.io/topics/acl\n\n# ACL LOG\n#\n# The ACL Log tracks failed commands and authentication events associated\n# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked\n# by ACLs. The ACL Log is stored in memory. You can reclaim memory with\n# ACL LOG RESET. Define the maximum entry length of the ACL Log below.\nacllog-max-len 128\n\n# Using an external ACL file\n#\n# Instead of configuring users here in this file, it is possible to use\n# a stand-alone file just listing users. The two methods cannot be mixed:\n# if you configure users here and at the same time you activate the external\n# ACL file, the server will refuse to start.\n#\n# The format of the external ACL user file is exactly the same as the\n# format that is used inside redis.conf to describe users.\n#\n# aclfile /etc/redis/users.acl\n\naclfile /etc/redis/users.acl\n\n# IMPORTANT NOTE: starting with Redis 6 \"requirepass\" is just a compatibility\n# layer on top of the new ACL system. The option effect will be just setting\n# the password for the default user. Clients will still authenticate using\n# AUTH <password> as usually, or more explicitly with AUTH default <password>\n# if they follow the new protocol: both will work.\n#\nrequirepass somepass\n\n# Command renaming (DEPRECATED).\n#\n# ------------------------------------------------------------------------\n# WARNING: avoid using this option if possible. Instead use ACLs to remove\n# commands from the default user, and put them only in some admin user you\n# create for administrative purposes.\n# ------------------------------------------------------------------------\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to replicas may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# IMPORTANT: When Redis Cluster is used, the max number of connections is also\n# shared with the cluster bus: every node in the cluster will use two\n# connections, one incoming and another outgoing. It is important to size the\n# limit accordingly in case of very large clusters.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have replicas attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the replicas are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of replicas is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have replicas attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for replica\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select one from the following behaviors:\n#\n# volatile-lru -> Evict using approximated LRU, only keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key having an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. By default Redis will check five keys and pick the one that was\n# used least recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n# Starting from Redis 5, by default a replica will ignore its maxmemory setting\n# (unless it is promoted to master after a failover or manually). It means\n# that the eviction of keys will be just handled by the master, sending the\n# DEL commands to the replica as keys evict in the master side.\n#\n# This behavior ensures that masters and replicas stay consistent, and is usually\n# what you want, however if your replica is writable, or you want the replica\n# to have a different memory setting, and you are sure all the writes performed\n# to the replica are idempotent, then you may change this default (but be sure\n# to understand what you are doing).\n#\n# Note that since the replica by default does not evict, it may end using more\n# memory than the one set via maxmemory (there are certain buffers that may\n# be larger on the replica, or data structures may sometimes take more memory\n# and so forth). So make sure you monitor your replicas and make sure they\n# have enough memory to never hit a real out-of-memory condition before the\n# master hits the configured maxmemory setting.\n#\n# replica-ignore-maxmemory yes\n\n# Redis reclaims expired keys in two ways: upon access when those keys are\n# found to be expired, and also in background, in what is called the\n# \"active expire key\". The key space is slowly and interactively scanned\n# looking for expired keys to reclaim, so that it is possible to free memory\n# of keys that are expired and will never be accessed again in a short time.\n#\n# The default effort of the expire cycle will try to avoid having more than\n# ten percent of expired keys still in memory, and will try to avoid consuming\n# more than 25% of total memory and to add latency to the system. However\n# it is possible to increase the expire \"effort\" that is normally set to\n# \"1\", to a greater value, up to the value \"10\". At its maximum value the\n# system will use more CPU, longer cycles (and technically may introduce\n# more latency), and will tolerate less already expired keys still present\n# in the system. It's a tradeoff between memory, CPU and latency.\n#\n# active-expire-effort 1\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a replica performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives.\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nreplica-lazy-flush no\n\n# It is also possible, for the case when to replace the user code DEL calls\n# with UNLINK calls is not easy, to modify the default behavior of the DEL\n# command to act exactly like UNLINK, using the following configuration\n# directive:\n\nlazyfree-lazy-user-del no\n\n################################ THREADED I/O #################################\n\n# Redis is mostly single threaded, however there are certain threaded\n# operations such as UNLINK, slow I/O accesses and other things that are\n# performed on side threads.\n#\n# Now it is also possible to handle Redis clients socket reads and writes\n# in different I/O threads. Since especially writing is so slow, normally\n# Redis users use pipelining in order to speed up the Redis performances per\n# core, and spawn multiple instances in order to scale more. Using I/O\n# threads it is possible to easily speedup two times Redis without resorting\n# to pipelining nor sharding of the instance.\n#\n# By default threading is disabled, we suggest enabling it only in machines\n# that have at least 4 or more cores, leaving at least one spare core.\n# Using more than 8 threads is unlikely to help much. We also recommend using\n# threaded I/O only if you actually have performance problems, with Redis\n# instances being able to use a quite big percentage of CPU time, otherwise\n# there is no point in using this feature.\n#\n# So for instance if you have a four cores boxes, try to use 2 or 3 I/O\n# threads, if you have a 8 cores, try to use 6 threads. In order to\n# enable I/O threads use the following configuration directive:\n#\n# io-threads 4\n#\n# Setting io-threads to 1 will just use the main thread as usual.\n# When I/O threads are enabled, we only use threads for writes, that is\n# to thread the write(2) syscall and transfer the client buffers to the\n# socket. However it is also possible to enable threading of reads and\n# protocol parsing using the following configuration directive, by setting\n# it to yes:\n#\n# io-threads-do-reads no\n#\n# Usually threading reads doesn't help much.\n#\n# NOTE 1: This configuration directive cannot be changed at runtime via\n# CONFIG SET. Aso this feature currently does not work when SSL is\n# enabled.\n#\n# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make\n# sure you also run the benchmark itself in threaded mode, using the\n# --threads option to match the number of Redis threads, otherwise you'll not\n# be able to notice the improvements.\n\n############################ KERNEL OOM CONTROL ##############################\n\n# On Linux, it is possible to hint the kernel OOM killer on what processes\n# should be killed first when out of memory.\n#\n# Enabling this feature makes Redis actively control the oom_score_adj value\n# for all its processes, depending on their role. The default scores will\n# attempt to have background child processes killed before all others, and\n# replicas killed before masters.\n#\n# Redis supports three options:\n#\n# no:       Don't make changes to oom-score-adj (default).\n# yes:      Alias to \"relative\" see below.\n# absolute: Values in oom-score-adj-values are written as is to the kernel.\n# relative: Values are used relative to the initial value of oom_score_adj when\n#           the server starts and are then clamped to a range of -1000 to 1000.\n#           Because typically the initial value is 0, they will often match the\n#           absolute values.\noom-score-adj no\n\n# When oom-score-adj is used, this directive controls the specific values used\n# for master, replica and background child processes. Values range -2000 to\n# 2000 (higher means more likely to be killed).\n#\n# Unprivileged processes (not root, and without CAP_SYS_RESOURCE capabilities)\n# can freely increase their value, but not decrease it below its initial\n# settings. This means that setting oom-score-adj to \"relative\" and setting the\n# oom-score-adj-values to positive values will always succeed.\noom-score-adj-values 0 200 800\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading, Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, then continues loading the AOF\n# tail.\naof-use-rdb-preamble yes\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet call any write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\n# cluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file nodes-6379.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are a multiple of the node timeout.\n#\n# cluster-node-timeout 15000\n\n# A replica of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a replica to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple replicas able to failover, they exchange messages\n#    in order to try to give an advantage to the replica with the best\n#    replication offset (more data from the master processed).\n#    Replicas will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single replica computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the replica will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a replica will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * cluster-replica-validity-factor) + repl-ping-replica-period\n#\n# So for example if node-timeout is 30 seconds, and the cluster-replica-validity-factor\n# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the\n# replica will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large cluster-replica-validity-factor may allow replicas with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a replica at all.\n#\n# For maximum availability, it is possible to set the cluster-replica-validity-factor\n# to a value of 0, which means, that replicas will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-replica-validity-factor 10\n\n# Cluster replicas are able to migrate to orphaned masters, that are masters\n# that are left without working replicas. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working replicas.\n#\n# Replicas migrate to orphaned masters only if there are still at least a\n# given number of other working replicas for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a replica\n# will migrate only if there is at least 1 other working replica for its master\n# and so forth. It usually reflects the number of replicas you want for every\n# master in your cluster.\n#\n# Default is 1 (replicas migrate only if their masters remain with at least\n# one replica). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least a hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents replicas from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-replica-no-failover no\n\n# This option, when set to yes, allows nodes to serve read traffic while the\n# the cluster is in a down state, as long as it believes it owns the slots.\n#\n# This is useful for two cases.  The first case is for when an application\n# doesn't require consistency of data during node failures or network partitions.\n# One example of this is a cache, where as long as the node has the data it\n# should be able to serve it.\n#\n# The second use case is for configurations that don't meet the recommended\n# three shards but want to enable cluster mode and scale later. A\n# master outage in a 1 or 2 shard configuration causes a read/write outage to the\n# entire cluster without this option set, with it set there is only a write outage.\n# Without a quorum of masters, slot ownership will not change automatically.\n#\n# cluster-allow-reads-when-down no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instructs the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usual.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  t     Stream commands\n#  m     Key-miss events (Note: It is not included in the 'A' class)\n#  A     Alias for g$lshzxet, so that the \"AKE\" string means all the events\n#        (Except key-miss events which are excluded from 'A' due to their\n#         unique nature).\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### GOPHER SERVER #################################\n\n# Redis contains an implementation of the Gopher protocol, as specified in\n# the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt).\n#\n# The Gopher protocol was very popular in the late '90s. It is an alternative\n# to the web, and the implementation both server and client side is so simple\n# that the Redis server has just 100 lines of code in order to implement this\n# support.\n#\n# What do you do with Gopher nowadays? Well Gopher never *really* died, and\n# lately there is a movement in order for the Gopher more hierarchical content\n# composed of just plain text documents to be resurrected. Some want a simpler\n# internet, others believe that the mainstream internet became too much\n# controlled, and it's cool to create an alternative space for people that\n# want a bit of fresh air.\n#\n# Anyway for the 10nth birthday of the Redis, we gave it the Gopher protocol\n# as a gift.\n#\n# --- HOW IT WORKS? ---\n#\n# The Redis Gopher support uses the inline protocol of Redis, and specifically\n# two kind of inline requests that were anyway illegal: an empty request\n# or any request that starts with \"/\" (there are no Redis commands starting\n# with such a slash). Normal RESP2/RESP3 requests are completely out of the\n# path of the Gopher protocol implementation and are served as usual as well.\n#\n# If you open a connection to Redis when Gopher is enabled and send it\n# a string like \"/foo\", if there is a key named \"/foo\" it is served via the\n# Gopher protocol.\n#\n# In order to create a real Gopher \"hole\" (the name of a Gopher site in Gopher\n# talking), you likely need a script like the following:\n#\n#   https://github.com/antirez/gopher2redis\n#\n# --- SECURITY WARNING ---\n#\n# If you plan to put Redis on the internet in a publicly accessible address\n# to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance.\n# Once a password is set:\n#\n#   1. The Gopher server (when enabled, not by default) will still serve\n#      content via Gopher.\n#   2. However other commands cannot be called before the client will\n#      authenticate.\n#\n# So use the 'requirepass' option to protect your instance.\n#\n# Note that Gopher is not currently supported when 'io-threads-do-reads'\n# is enabled.\n#\n# To enable Gopher support, uncomment the following line and set the option\n# from no (the default) to yes.\n#\n# gopher-enabled no\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Streams macro node max size / items. The stream data structure is a radix\n# tree of big nodes that encode multiple items inside. Using this configuration\n# it is possible to configure how big a single node can be in bytes, and the\n# maximum number of items it may contain before switching to a new node when\n# appending new stream entries. If any of the following settings are set to\n# zero, the limit is ignored, so for instance it is possible to set just a\n# max entires limit by setting max-bytes to 0 and max-entries to the desired\n# value.\nstream-node-max-bytes 4096\nstream-node-max-entries 100\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# replica  -> replica clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and replica clients, since\n# subscribers and replicas receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit replica 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here, but must be 1mb or greater\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# Normally it is useful to have an HZ value which is proportional to the\n# number of clients connected. This is useful in order, for instance, to\n# avoid too many clients are processed for each background task invocation\n# in order to avoid latency spikes.\n#\n# Since the default HZ value by default is conservatively set to 10, Redis\n# offers, and enables by default, the ability to use an adaptive HZ value\n# which will temporarily raise when there are many connected clients.\n#\n# When dynamic HZ is enabled, the actual configured HZ will be used\n# as a baseline, but multiples of the configured HZ value will be actually\n# used as needed once more clients are connected. In this way an idle\n# instance will use very little CPU time while a busy instance will be\n# more responsive.\ndynamic-hz yes\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# When redis saves RDB file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\nrdb-save-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in a \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag no\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage, to be used when the lower\n# threshold is reached\n# active-defrag-cycle-min 1\n\n# Maximal effort for defrag in CPU percentage, to be used when the upper\n# threshold is reached\n# active-defrag-cycle-max 25\n\n# Maximum number of set/hash/zset/list fields that will be processed from\n# the main dictionary scan\n# active-defrag-max-scan-fields 1000\n\n# Jemalloc background thread for purging will be enabled by default\njemalloc-bg-thread yes\n\n# It is possible to pin different threads and processes of Redis to specific\n# CPUs in your system, in order to maximize the performances of the server.\n# This is useful both in order to pin different Redis threads in different\n# CPUs, but also in order to make sure that multiple Redis instances running\n# in the same host will be pinned to different CPUs.\n#\n# Normally you can do this using the \"taskset\" command, however it is also\n# possible to this via Redis configuration directly, both in Linux and FreeBSD.\n#\n# You can pin the server/IO threads, bio threads, aof rewrite child process, and\n# the bgsave child process. The syntax to specify the cpu list is the same as\n# the taskset command:\n#\n# Set redis server/io threads to cpu affinity 0,2,4,6:\n# server_cpulist 0-7:2\n#\n# Set bio threads to cpu affinity 1,3:\n# bio_cpulist 1,3\n#\n# Set aof rewrite child process to cpu affinity 8,9,10,11:\n# aof_rewrite_cpulist 8-11\n#\n# Set bgsave child process to cpu affinity 1,10,11\n# bgsave_cpulist 1,10-11\n\n# In some cases redis will emit warnings and even refuse to start if it detects\n# that the system is in bad state, it is possible to suppress these warnings\n# by setting the following config which takes a space delimited list of warnings\n# to suppress\n#\n# ignore-warnings ARM64-COW-BUG\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/sentinel.Dockerfile",
    "content": "FROM redis:6.2.6-alpine\nCOPY sentinel.conf users.acl certs/* /etc/redis/\nENTRYPOINT [ \"redis-server\", \"/etc/redis/sentinel.conf\", \"--sentinel\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/sentinel.conf",
    "content": "port 0\ntls-auth-clients no\ntls-replication yes\ntls-port 26379\ntls-cert-file /etc/redis/redis.crt\ntls-key-file /etc/redis/redis.key\ntls-ca-cert-file /etc/redis/ca.crt\naclfile /etc/redis/users.acl\n\ndir /tmp\nsentinel resolve-hostnames yes\n\nsentinel monitor primary_group_1 p1 6379 2\nsentinel down-after-milliseconds primary_group_1 5000\nsentinel parallel-syncs primary_group_1 1\nsentinel failover-timeout primary_group_1 10000\nsentinel auth-pass primary_group_1 defaultpass\n\nsentinel monitor primary_group_2 p2 6379 2\nsentinel down-after-milliseconds primary_group_2 5000\nsentinel parallel-syncs primary_group_2 1\nsentinel failover-timeout primary_group_2 10000\nsentinel auth-pass primary_group_2 defaultpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-sent-tls-auth/users.acl",
    "content": "user default on +@all ~* >defaultpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-5/Dockerfile",
    "content": "FROM --platform=linux/amd64 redislabs/redisearch:1.6.15 as redisearch\nFROM --platform=linux/amd64 redislabs/rejson:1.0.8 as rejson\n\nFROM --platform=linux/amd64 redis:5\n\n# Install RediSearch 1.6.15\nCOPY --from=redisearch /usr/lib/redis/modules/ /usr/lib/redis/modules/\n\n# Install RedisJSON 1.0.8\nCOPY --from=rejson /usr/lib/redis/modules/ /usr/lib/redis/modules/\n\nCOPY redis.conf /etc/redis.conf\n\nENTRYPOINT [ \"redis-server\", \"/etc/redis.conf\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-5/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  redis:\n    build: ./oss-st-5\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-5/redis.conf",
    "content": "port 6379\n\nloadmodule /usr/lib/redis/modules/redisearch.so\nloadmodule /usr/lib/redis/modules/rejson.so\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-5-pass/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-st-5-pass/.env\n  redis:\n    image: redis:5\n    command: redis-server --requirepass testpass\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  redis:\n    image: redis:6.2.14-alpine\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls/Dockerfile",
    "content": "FROM bitnamilegacy/redis:6.2.4\n\nENV ALLOW_EMPTY_PASSWORD yes\n\n# TLS options\nENV REDIS_TLS_ENABLED yes\nENV REDIS_TLS_PORT 6379\nENV REDIS_TLS_CERT_FILE /opt/bitnami/redis/certs/redis.crt\nENV REDIS_TLS_KEY_FILE /opt/bitnami/redis/certs/redis.key\nENV REDIS_TLS_CA_FILE /opt/bitnami/redis/certs/redisCA.crt\nENV REDIS_TLS_AUTH_CLIENTS no\n\nCOPY --chown=1001 ./certs /opt/bitnami/redis/certs/\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls/certs/redis.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhEwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI3WhcNMzExMDI2MTMyNzI3WjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAM4osMW/hBlde+/E20wP9X+zJ0AMD6OtfhqQ5brC\ngbVs9mPccZ/8R0fj83YtZfnoEodMZ/7yUTeCCPMdprAezMU1KBf9EpZmTdWhpO3e\nkESHcQdsKkqGtyjYF7dDahTmKt4a4aHlPH0DJLltB5HlbVabkzlo+3S8QaNwH5lY\nyJTQIqiqVzs9oRLT76nZuJjsym0dNXE42rO3KCniI6kvJDmUzBD8Wc94iDExfy7q\nqHyV7b2DCp1w7XP4yrQAFQ6kiVqNcfTTAO4MHNP54V2nZLPdOsUD5BsYY8hu0HDc\n/PisZ9ZMcw7LMfpUd3dfA9zefQXPQsWJK20ZCNmdIFtwvIFUpYu/FEF3FWO83zeI\nXkVZiuCOnoFvp8JIDvXgzXUBWzvYmxLqVSZAuabqU5pKRswDPLGlZTkHbuP2DiXD\nLD5AsTnICpzYkUeERSZKf2qc/nTUk04W/7FUT/75ItVzZvu90mPJlmArB0j4zdAG\nKwKo8v/cF1hA1YznhibxcUAA/Q/O3Y6CPQ7C3NeaGKcycgUxWoEY3Leno40ukijd\nR0MvsaY7V0/up37fkPtH9rcCkZOGVT5Q4Ww9gVO3yseXVkxbJyzHV1tuwg6yY9wO\nLOU2Bbmazkjlkb8a5OyQol2zZNJ0L3lvRWTildtGUTsBkeqI6HAedTTHJkhjHh/P\nP0+TAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nAKn+aH60zdd4ItMOhgd4BIok/u6S4NF0oe4kDwC/EU5C0hbuT6gLAn7ArHMeadue\nOuSnhqIPfxNR9WCes9OU1O00WeCuWCE4XfNMMFN5iDFfLxO4Oy5Adt0T74hWbcy6\nh28TdcjrkJEr7HR59G5tQ8TW5gVB4a0WXDw0ob9DSxbFKZU1uZm9L/+MgB/SNCHL\nGZSKt75Z/M10b9BTC3OG9swsoWvXEjR2ICiwzk+LxVf5K38faDyBrNJVglrpEUZz\ngP60kL73qK0y1/i35UuP0yIJIy48XnDsSByN7eBVsNTGMW3CFLKWA4RVfnEHNUff\nvsLHXZFYsUIPnPc5jksFwb/wKAe9JbCrgQPhBYaIYkRGiYt64C48r3boIIVoz9+1\n9Nq0Ik06fCzlI9APq2nzEiVeB7mDyZ692neu32QM6zRkYor+W8uI21YnRJWlOx7+\nx2GIh2EZnEYNvbpbvk/fV5AqkYOu9auRAkcKfME7dJ3Gwndl0YBOjE2DMTv6vIjS\ndVuGXQCvlzkRAnPMh5MR5/bSUKVvBryXs9ecAMgoVXBVB+4tGWct5ziL+8qyNtgA\nWJ2EWj3xtLlMwwQmLjRsCrZjL4liLJG8Yn8Ehfq1rRJREH2O8uYKCO1fdhuI0Y5S\niBPfqJi6QBHj7i01K9OpNUB7l+xAFLA3cBsegcm2GPoL\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls/certs/redis.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDOKLDFv4QZXXvv\nxNtMD/V/sydADA+jrX4akOW6woG1bPZj3HGf/EdH4/N2LWX56BKHTGf+8lE3ggjz\nHaawHszFNSgX/RKWZk3VoaTt3pBEh3EHbCpKhrco2Be3Q2oU5ireGuGh5Tx9AyS5\nbQeR5W1Wm5M5aPt0vEGjcB+ZWMiU0CKoqlc7PaES0++p2biY7MptHTVxONqztygp\n4iOpLyQ5lMwQ/FnPeIgxMX8u6qh8le29gwqdcO1z+Mq0ABUOpIlajXH00wDuDBzT\n+eFdp2Sz3TrFA+QbGGPIbtBw3Pz4rGfWTHMOyzH6VHd3XwPc3n0Fz0LFiSttGQjZ\nnSBbcLyBVKWLvxRBdxVjvN83iF5FWYrgjp6Bb6fCSA714M11AVs72JsS6lUmQLmm\n6lOaSkbMAzyxpWU5B27j9g4lwyw+QLE5yAqc2JFHhEUmSn9qnP501JNOFv+xVE/+\n+SLVc2b7vdJjyZZgKwdI+M3QBisCqPL/3BdYQNWM54Ym8XFAAP0Pzt2Ogj0OwtzX\nmhinMnIFMVqBGNy3p6ONLpIo3UdDL7GmO1dP7qd+35D7R/a3ApGThlU+UOFsPYFT\nt8rHl1ZMWycsx1dbbsIOsmPcDizlNgW5ms5I5ZG/GuTskKJds2TSdC95b0Vk4pXb\nRlE7AZHqiOhwHnU0xyZIYx4fzz9PkwIDAQABAoICAHyc/+0oDHNAnK+bsGrTorNj\n2S/Pmox3TChGuXYgKEM/79cA4vWvim6cDQe7/U4Hx1tdBeeHFSyWP06k96Kxm1kA\n/pExedDLWfTt1kGqLE4gCGRSL2YI9CGOLRerei3TysmiOgygAeYWxlYG33KC2Ypm\nU6F6IbS4LnzaQ19v2R6KiMim3j+CyyAUV2O1pO1bBCjcZPdhRGEpLu/SL3gOdLkR\nhiAmSSstUjVaE+SKFvnnrmLFGN996SoWkoAnJJNLRXMk2GMCQCejzrEa8+ymSCqo\naOO5rGHsZjQ7N2dhTNALdmCEqW+hxz3nXKcdGbqiCbQ/Sb8ZYNR7M2xGm85p4Ka9\n0UK4cOM1VJPwz8hotSKAUmXnpbu73CsZi5HyzwZkk4FpcaYCYrCbGVmm1cIKEKI7\n8SN/oqgFdj4Ha9cemnu+RecQZouK+wPWtcILd2kstJl52TV952fVOrnXQDo6XCXB\nfbs9IYN1hB6N79xv4L7Jj53hSRMeSNf70Ejkh1FXPOvmvFT12wy5JQdBBR5nnb4a\nGEsMpGVe1k3bxjK7K263tLSH0UZ8dMgdSx1E4D1hT1K/gEwTSMOJ0E1R0M6SJmF2\n6TnZ0MbJWx6PbICmyZrd2agfTQrq6CgY1fWLGbQrtnwXtsUR7PiHarydXfs3V8g1\nxHnK1bItOBBMOMcWV93hAoIBAQD2xMceUfBUN0TeWrJ4izD3lUYxmWqjTg7kPcjJ\n0VN8v3txGAcyPmbuz5AEzvdFjTispoZNB9QOrmsUVWTQDE5otnei9kHWzqJWYHg4\nUSuUuAh8OJGCiepo8zHT3qHDNhKGtOAp5LC8TaOznUFr35zCBCOsvQfRUKrv5IOc\nvCFjO07Xly8+M3qK7/UswRQ6480VlE2t1p+VNaORHdTDg2tes3/9owuiNmR/sPT8\nnIoe01LS7qmZoiB1vracaLcBf1Iwd7RvKg7mgFJzmowZUYxyX2YGK5qZ1h74In2X\n55+qQnNW0RwPijopTv711pMhMKWl8i3ilcCfoeBXz8zCwFfbAoIBAQDV3wHAO7ic\nMYP/Bm5jgIwlix1eOWY/yB+VqdYn2GmC49vTEIlIVlFRq0vZE06iUxs87BIV08zO\n4w/iKXd7ktkuhphiEuU2yXA3LQPHpbSOW43RONbf4glFU/DlP/P6fiybbWj6+f7L\n7Zbvtz5AW03Y4ZpagJTqOgVdJ0MdLnh9vZj6okGGV1fidKtG6hr7Z/oLhnl9aAZK\n4vrvBZ//qz99vEVByiV5kRaJDulu+diBy4n6iBjzjHA5a9e7lY3sUBw3DMgb7kYs\nJJPkCPdSxCYq4Ef3z/Eao0tyUuCzyznfCMGJA1gBdTpwDNDCTaXqkjR5nvsdE5k0\nIVQgFPtcOPCpAoIBABujNlni+3OzLPdqWQq/LCDOiyoK8LKRj4FomhBgbWVPXNfx\nxPyPmJ+uh4bCV1dm1a4giHIgKlPqnPuOBNh4SF/Z79REmGMiiXP7IfvMu4DQi8K9\n4y4nnCVc93uvN5bRe4mywFhw0IqGd4sqVaVrSfdA124FTdbXng14HnVzbJncjpv+\nxr/ErDjbXy5AAbAGy3VbQsfxfbYMZ+Fc4fNzyJa2q+MQW8EzLlZOz2Frdty09lXB\nfSVDzzbgwTsLT1PPmrjq7z50C28teA6ShJZhV8WHgbm3MH2CSb2ov0BAJNXA04Ip\nsWbcKF9wBYYrHhddh2/qi9EQzJ4UVzf+ggRd3nkCggEAWcjyWjp4KRJcgJ65jwoz\nS7uYS6s7MsGYCOOw5S9kNC/mZDhH+ddK8kdAY1RIqbrL74qHmSQ+kgge7epMn9Mp\nW+/jXyDhm1t7wZ4jPRhisXTcF56ODpU9IR65PfTYPyvjHCkVbm+vOPt4ZxB9kNUD\n3G3xt9bNLXvILrBB66lLqjYDWAzwBy751Tb3hKDZTPv8rAP7Uttt8NhTUi8BWXsR\n/34fcRwlGWEAne9lrlIzQ2IofcXO+8fUgTa17ak+WJvVDINQKvGgAf4lHBFrixKP\nl2ZqsC1a4bz1+nuym6hQlkJ9xUBjHNGTA+FNbpTcd5qDbx9/+lf09D6dq45DbBb3\naQKCAQBrnFYocTm/fIeKo1n1kyF2ULkd6k984ztH8UyluXleSS1ShFFoo/x3vz35\nfsZNUggRnSe7/OBGZYquF/1roVULI1hKh4tbEmW4SWNeTFvwXKdRe6T7NnWSZtS/\nKtamA3lT2wtoEVOvfMo8M0hoFuRWdT2M0i+LKZQdRsq18XPLqdHt1kkSNcnPDERm\n4gLQ8zXTf2fHrtZmyM8fc0GuTVwprPFeJkLtSPehkeXSTgb6rpyelX9NBUILwRgP\nnw0+cbjFDFKaLnIrMFoVAAn/8DcnbbSt1TZhgNsMxY+GHWPBYW8SUi5nBmQQtmA7\nn3ju44acIPvJ9sWuZruVlWZGFaHm\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls/certs/redisCA.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-st-6-tls/.env\n  redis:\n    build:\n      context: ./oss-st-6-tls\n      dockerfile: Dockerfile\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/Dockerfile",
    "content": "FROM bitnamilegacy/redis:6.0.8\n\nENV ALLOW_EMPTY_PASSWORD yes\n\n# TLS options\nENV REDIS_TLS_ENABLED yes\nENV REDIS_TLS_PORT 6379\nENV REDIS_TLS_CERT_FILE /opt/bitnami/redis/certs/redis.crt\nENV REDIS_TLS_KEY_FILE /opt/bitnami/redis/certs/redis.key\nENV REDIS_TLS_CA_FILE /opt/bitnami/redis/certs/redisCA.crt\nENV REDIS_TLS_AUTH_CLIENTS yes\n\nCOPY --chown=1001 ./certs /opt/bitnami/redis/certs/\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/certs/redis.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhEwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI3WhcNMzExMDI2MTMyNzI3WjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAM4osMW/hBlde+/E20wP9X+zJ0AMD6OtfhqQ5brC\ngbVs9mPccZ/8R0fj83YtZfnoEodMZ/7yUTeCCPMdprAezMU1KBf9EpZmTdWhpO3e\nkESHcQdsKkqGtyjYF7dDahTmKt4a4aHlPH0DJLltB5HlbVabkzlo+3S8QaNwH5lY\nyJTQIqiqVzs9oRLT76nZuJjsym0dNXE42rO3KCniI6kvJDmUzBD8Wc94iDExfy7q\nqHyV7b2DCp1w7XP4yrQAFQ6kiVqNcfTTAO4MHNP54V2nZLPdOsUD5BsYY8hu0HDc\n/PisZ9ZMcw7LMfpUd3dfA9zefQXPQsWJK20ZCNmdIFtwvIFUpYu/FEF3FWO83zeI\nXkVZiuCOnoFvp8JIDvXgzXUBWzvYmxLqVSZAuabqU5pKRswDPLGlZTkHbuP2DiXD\nLD5AsTnICpzYkUeERSZKf2qc/nTUk04W/7FUT/75ItVzZvu90mPJlmArB0j4zdAG\nKwKo8v/cF1hA1YznhibxcUAA/Q/O3Y6CPQ7C3NeaGKcycgUxWoEY3Leno40ukijd\nR0MvsaY7V0/up37fkPtH9rcCkZOGVT5Q4Ww9gVO3yseXVkxbJyzHV1tuwg6yY9wO\nLOU2Bbmazkjlkb8a5OyQol2zZNJ0L3lvRWTildtGUTsBkeqI6HAedTTHJkhjHh/P\nP0+TAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nAKn+aH60zdd4ItMOhgd4BIok/u6S4NF0oe4kDwC/EU5C0hbuT6gLAn7ArHMeadue\nOuSnhqIPfxNR9WCes9OU1O00WeCuWCE4XfNMMFN5iDFfLxO4Oy5Adt0T74hWbcy6\nh28TdcjrkJEr7HR59G5tQ8TW5gVB4a0WXDw0ob9DSxbFKZU1uZm9L/+MgB/SNCHL\nGZSKt75Z/M10b9BTC3OG9swsoWvXEjR2ICiwzk+LxVf5K38faDyBrNJVglrpEUZz\ngP60kL73qK0y1/i35UuP0yIJIy48XnDsSByN7eBVsNTGMW3CFLKWA4RVfnEHNUff\nvsLHXZFYsUIPnPc5jksFwb/wKAe9JbCrgQPhBYaIYkRGiYt64C48r3boIIVoz9+1\n9Nq0Ik06fCzlI9APq2nzEiVeB7mDyZ692neu32QM6zRkYor+W8uI21YnRJWlOx7+\nx2GIh2EZnEYNvbpbvk/fV5AqkYOu9auRAkcKfME7dJ3Gwndl0YBOjE2DMTv6vIjS\ndVuGXQCvlzkRAnPMh5MR5/bSUKVvBryXs9ecAMgoVXBVB+4tGWct5ziL+8qyNtgA\nWJ2EWj3xtLlMwwQmLjRsCrZjL4liLJG8Yn8Ehfq1rRJREH2O8uYKCO1fdhuI0Y5S\niBPfqJi6QBHj7i01K9OpNUB7l+xAFLA3cBsegcm2GPoL\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/certs/redis.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDOKLDFv4QZXXvv\nxNtMD/V/sydADA+jrX4akOW6woG1bPZj3HGf/EdH4/N2LWX56BKHTGf+8lE3ggjz\nHaawHszFNSgX/RKWZk3VoaTt3pBEh3EHbCpKhrco2Be3Q2oU5ireGuGh5Tx9AyS5\nbQeR5W1Wm5M5aPt0vEGjcB+ZWMiU0CKoqlc7PaES0++p2biY7MptHTVxONqztygp\n4iOpLyQ5lMwQ/FnPeIgxMX8u6qh8le29gwqdcO1z+Mq0ABUOpIlajXH00wDuDBzT\n+eFdp2Sz3TrFA+QbGGPIbtBw3Pz4rGfWTHMOyzH6VHd3XwPc3n0Fz0LFiSttGQjZ\nnSBbcLyBVKWLvxRBdxVjvN83iF5FWYrgjp6Bb6fCSA714M11AVs72JsS6lUmQLmm\n6lOaSkbMAzyxpWU5B27j9g4lwyw+QLE5yAqc2JFHhEUmSn9qnP501JNOFv+xVE/+\n+SLVc2b7vdJjyZZgKwdI+M3QBisCqPL/3BdYQNWM54Ym8XFAAP0Pzt2Ogj0OwtzX\nmhinMnIFMVqBGNy3p6ONLpIo3UdDL7GmO1dP7qd+35D7R/a3ApGThlU+UOFsPYFT\nt8rHl1ZMWycsx1dbbsIOsmPcDizlNgW5ms5I5ZG/GuTskKJds2TSdC95b0Vk4pXb\nRlE7AZHqiOhwHnU0xyZIYx4fzz9PkwIDAQABAoICAHyc/+0oDHNAnK+bsGrTorNj\n2S/Pmox3TChGuXYgKEM/79cA4vWvim6cDQe7/U4Hx1tdBeeHFSyWP06k96Kxm1kA\n/pExedDLWfTt1kGqLE4gCGRSL2YI9CGOLRerei3TysmiOgygAeYWxlYG33KC2Ypm\nU6F6IbS4LnzaQ19v2R6KiMim3j+CyyAUV2O1pO1bBCjcZPdhRGEpLu/SL3gOdLkR\nhiAmSSstUjVaE+SKFvnnrmLFGN996SoWkoAnJJNLRXMk2GMCQCejzrEa8+ymSCqo\naOO5rGHsZjQ7N2dhTNALdmCEqW+hxz3nXKcdGbqiCbQ/Sb8ZYNR7M2xGm85p4Ka9\n0UK4cOM1VJPwz8hotSKAUmXnpbu73CsZi5HyzwZkk4FpcaYCYrCbGVmm1cIKEKI7\n8SN/oqgFdj4Ha9cemnu+RecQZouK+wPWtcILd2kstJl52TV952fVOrnXQDo6XCXB\nfbs9IYN1hB6N79xv4L7Jj53hSRMeSNf70Ejkh1FXPOvmvFT12wy5JQdBBR5nnb4a\nGEsMpGVe1k3bxjK7K263tLSH0UZ8dMgdSx1E4D1hT1K/gEwTSMOJ0E1R0M6SJmF2\n6TnZ0MbJWx6PbICmyZrd2agfTQrq6CgY1fWLGbQrtnwXtsUR7PiHarydXfs3V8g1\nxHnK1bItOBBMOMcWV93hAoIBAQD2xMceUfBUN0TeWrJ4izD3lUYxmWqjTg7kPcjJ\n0VN8v3txGAcyPmbuz5AEzvdFjTispoZNB9QOrmsUVWTQDE5otnei9kHWzqJWYHg4\nUSuUuAh8OJGCiepo8zHT3qHDNhKGtOAp5LC8TaOznUFr35zCBCOsvQfRUKrv5IOc\nvCFjO07Xly8+M3qK7/UswRQ6480VlE2t1p+VNaORHdTDg2tes3/9owuiNmR/sPT8\nnIoe01LS7qmZoiB1vracaLcBf1Iwd7RvKg7mgFJzmowZUYxyX2YGK5qZ1h74In2X\n55+qQnNW0RwPijopTv711pMhMKWl8i3ilcCfoeBXz8zCwFfbAoIBAQDV3wHAO7ic\nMYP/Bm5jgIwlix1eOWY/yB+VqdYn2GmC49vTEIlIVlFRq0vZE06iUxs87BIV08zO\n4w/iKXd7ktkuhphiEuU2yXA3LQPHpbSOW43RONbf4glFU/DlP/P6fiybbWj6+f7L\n7Zbvtz5AW03Y4ZpagJTqOgVdJ0MdLnh9vZj6okGGV1fidKtG6hr7Z/oLhnl9aAZK\n4vrvBZ//qz99vEVByiV5kRaJDulu+diBy4n6iBjzjHA5a9e7lY3sUBw3DMgb7kYs\nJJPkCPdSxCYq4Ef3z/Eao0tyUuCzyznfCMGJA1gBdTpwDNDCTaXqkjR5nvsdE5k0\nIVQgFPtcOPCpAoIBABujNlni+3OzLPdqWQq/LCDOiyoK8LKRj4FomhBgbWVPXNfx\nxPyPmJ+uh4bCV1dm1a4giHIgKlPqnPuOBNh4SF/Z79REmGMiiXP7IfvMu4DQi8K9\n4y4nnCVc93uvN5bRe4mywFhw0IqGd4sqVaVrSfdA124FTdbXng14HnVzbJncjpv+\nxr/ErDjbXy5AAbAGy3VbQsfxfbYMZ+Fc4fNzyJa2q+MQW8EzLlZOz2Frdty09lXB\nfSVDzzbgwTsLT1PPmrjq7z50C28teA6ShJZhV8WHgbm3MH2CSb2ov0BAJNXA04Ip\nsWbcKF9wBYYrHhddh2/qi9EQzJ4UVzf+ggRd3nkCggEAWcjyWjp4KRJcgJ65jwoz\nS7uYS6s7MsGYCOOw5S9kNC/mZDhH+ddK8kdAY1RIqbrL74qHmSQ+kgge7epMn9Mp\nW+/jXyDhm1t7wZ4jPRhisXTcF56ODpU9IR65PfTYPyvjHCkVbm+vOPt4ZxB9kNUD\n3G3xt9bNLXvILrBB66lLqjYDWAzwBy751Tb3hKDZTPv8rAP7Uttt8NhTUi8BWXsR\n/34fcRwlGWEAne9lrlIzQ2IofcXO+8fUgTa17ak+WJvVDINQKvGgAf4lHBFrixKP\nl2ZqsC1a4bz1+nuym6hQlkJ9xUBjHNGTA+FNbpTcd5qDbx9/+lf09D6dq45DbBb3\naQKCAQBrnFYocTm/fIeKo1n1kyF2ULkd6k984ztH8UyluXleSS1ShFFoo/x3vz35\nfsZNUggRnSe7/OBGZYquF/1roVULI1hKh4tbEmW4SWNeTFvwXKdRe6T7NnWSZtS/\nKtamA3lT2wtoEVOvfMo8M0hoFuRWdT2M0i+LKZQdRsq18XPLqdHt1kkSNcnPDERm\n4gLQ8zXTf2fHrtZmyM8fc0GuTVwprPFeJkLtSPehkeXSTgb6rpyelX9NBUILwRgP\nnw0+cbjFDFKaLnIrMFoVAAn/8DcnbbSt1TZhgNsMxY+GHWPBYW8SUi5nBmQQtmA7\nn3ju44acIPvJ9sWuZruVlWZGFaHm\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/certs/redisCA.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/certs/user.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhIwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTQz\nNTAzWhcNMzExMDI2MTQzNTAzWjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAKOod8jpFXqjtNvl0FgIkg0fSZbzvh7jbI7TEUVQ\nmyeZxjmB3fZh5f6dxM7TZ048CUOeUeq3lemDqay+Moku0rL4PsFNe8z1C1zHuhf9\n4Qw/f7rMBIZ73L4Y/7cPWfjZbeme06+D7HMBZGTWGHZCWrqZQOwA3hKBjC3VY/a5\nz6oP78+w18WDpnavGwXwgCd1yTOwz3tVJUOcJdjGv3iwrHABcGVfxUEKTabP+p6V\nHA/+w4AlCloS57GQCh0RWCXMyfekv6MGBaqQa6GtOK5ScLJ1YSlJ6PRoK2N+shbw\nL/kQGlilgYBVGOQgNKd94+PwJgOCy72S7p9yF3ZTBB4/51Bwl7IV74Om/GmqzJMx\nxY9/PPaxKlOkP+dW41/IrcDULdh0jAfe9rKdFf9/9NWA37S68pKFpzRuRrpLqIwm\nBPtHvtLnTbhgmS/O1Rwmxqs8r+VA6D8+/drAor/KAcCwgRiYLvhvl4ABoqj4toEK\njCXAR/jeoLAb8HDBzkot4hhJPjMhQMYX9/HfdK4YX359EkHdsO/+R6+ImXb68DS5\nzh0028ktMM+KEhWSffSmU3imZOrH1/TQfSxfzuTHvyd0HXAHvzx+w1VWNK4fqU8O\ntDbMt1GAaatrfrqwP4qTjzLEqtlJLIjg4qgzpYCRUvgVdxyeii9o7IeYT8I6Penf\nQpAJAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nABb+A9C1AqstP5d2HXS/pIef1BNYAV8A/D2+nUpEXiautmjRZBRNwzBX2ffEZWV7\nEMkvqrRwz2ZgOJ4BRzJiDIYoF8dOhd0lc/0PqoR0xVzjOFDqd0ZcPHAjaY3UoBE7\njQSQ6ccc1tY5peNLAWCvRO6V9yhdV/SKGhveXGl/24MK9juwArnAitekCWZJQifT\nCFOJX5UvifrT8s0v0AqkycaNpkMvl0BAl4DRDJ3+EwZmzfOdATawyXBVXHt1Gz+N\niskPJAJsIjEdFYTjDUzwRN3bHFbTRXt2v1U18YIvMjvxq8MlITEC2lEW+3Xu90d3\naE/N9mLNJCgmZ2CGywWoaJlUXix2LTo5kT5coVVx0HK0tg5EcBua05qM3xO9Rgxv\nHkCnm/jMeN4oQ5o7h+q7UQja8mg1bjCzlt+RxqoA1snjglra/h5I8TTEhvSfxEy7\nh5Wiwne/TH/e8fN1IYRDvv602MNSZnAEPyG3Hc5xQOSGNpoKOZG7tpU+mRYIvlPe\nJgA5WNZ83y25JqSxF3kQuk7vrLByzEByqV3j+jIAQiHu/qIXwXUpkoV3L6A18yx/\nTbpQasr/bRFZKe83WlNl2ASAVyubal8ocmA0ua24/RV0I0VOCEXiIkl+pZ6e5Qn4\nL6Tryy5NxaEpUAZ9yv3P75PfNVQ3+vGYi3BLuhZUf/Dd\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/certs/user.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCjqHfI6RV6o7Tb\n5dBYCJINH0mW874e42yO0xFFUJsnmcY5gd32YeX+ncTO02dOPAlDnlHqt5Xpg6ms\nvjKJLtKy+D7BTXvM9Qtcx7oX/eEMP3+6zASGe9y+GP+3D1n42W3pntOvg+xzAWRk\n1hh2Qlq6mUDsAN4SgYwt1WP2uc+qD+/PsNfFg6Z2rxsF8IAndckzsM97VSVDnCXY\nxr94sKxwAXBlX8VBCk2mz/qelRwP/sOAJQpaEuexkAodEVglzMn3pL+jBgWqkGuh\nrTiuUnCydWEpSej0aCtjfrIW8C/5EBpYpYGAVRjkIDSnfePj8CYDgsu9ku6fchd2\nUwQeP+dQcJeyFe+DpvxpqsyTMcWPfzz2sSpTpD/nVuNfyK3A1C3YdIwH3vaynRX/\nf/TVgN+0uvKShac0bka6S6iMJgT7R77S5024YJkvztUcJsarPK/lQOg/Pv3awKK/\nygHAsIEYmC74b5eAAaKo+LaBCowlwEf43qCwG/Bwwc5KLeIYST4zIUDGF/fx33Su\nGF9+fRJB3bDv/keviJl2+vA0uc4dNNvJLTDPihIVkn30plN4pmTqx9f00H0sX87k\nx78ndB1wB788fsNVVjSuH6lPDrQ2zLdRgGmra366sD+Kk48yxKrZSSyI4OKoM6WA\nkVL4FXccnoovaOyHmE/COj3p30KQCQIDAQABAoICADAiwPiq9dJYjD2RXrJF8w9B\nAJgRoP3cznVDx3SnvLrtE8yeUfbB3LADH3vl2iC8r8zfqCBtVv6T5zgTyTFoQDi7\no1mfvKYP/QORCz87QRIlKyB6GWqky8xt9eiV71SuPxHT0Vdyaf15j1nJTvCZm63+\nnYXMy4SN7fkdJoXPKTFP9q0TyqMhkbie0Efy8P6qOj+l5aDU7lzwdIFKE88fx9g5\n1CE9BfuXWDeUPJagLNzXhhEO0/iiTtt/Djp2e4LCtTTNlEAS6V+9kqq/FEjRnqwe\nsjE+t/ILIZfmD+OHSdTr05P3OhvQ671Na69H69uDKuslcV+U8/KZ0CTRTgjHqvUZ\neLNC8BZfAk8IZx637/rSlqPmxyS/j35vdslebTbWV2KM7jXPqSb9YokdoJ6M0NZX\nIYiMK2reVzjy2YvX1Nhp4Xn68il10XVS4P9tFxyNWdTclCbuSlTfgc27ercQMMgY\nfe7/8+A/QhV8tdly8W3HwTmvkmmWRSTMziI+zQzZmYYlAWb33rQYfMoHs4tEf2u2\nRf0Oso56X73sc3ncnOFm+s5iwTeUH6EgF3ephJX4nR3canmtpy40nbXUJ+tAuaAj\nuo56KNlPxIHKf96o2LGXGTrgbH39f0MebWOq/7YjtCg6sUbwuyyG3afLTHHuss13\n5bTJ5gD3rsiGUWjfY3oBAoIBAQDRR/BnDw501Hky32/Vhlt7Ef1iplru+Fh0yQj7\n2DQ+U+L1Ir4Q67ESH8qDnjkyLP1a8BDNOIEEGp5dBb+OHb/rwdb+RZ7OCIzFCQ/d\nWR7m0ucuPBQwytQb7iXa9w0umZwoeTXEGP8aGe+bSBIHv8/em26rkSx0A1rxr2/O\n1ho8xxgBmOxL3NSCnv56JUu/W0vFq/7OfWQ19SOvFahp3TeqR1gkHe76teWv11Pj\n+RdiIIdCOifWChZPEdgMZD4rl1cs9QQb+n+WkRt/mZgtTIRQIe+we+vIha7TW46X\n6A1DjSxV4WUSXvv10heYYpZkKzpNG9YOhRB3bvyDkRy11XZ5AoIBAQDIMUETtoa9\nEFNY+uieZwJCTWrrB1njLLRZS3eCAKsVegHD0txLG8H5VMkyZQErRe52zR9QXWU/\nU80tIO5BTbP3ME2AbjJvMwuiEe1lBKlVnn2JSGjbtzUMa1QBvDRmBEZkr8OneMN6\np2tX3L3Vw8Xm/97rjkAgo3gQkqyDf6VZ4xvH2Wo405yMywcoifMZXo/PN9fI5V8S\nfi3XjHrHzaY4cucbdaezVb4Zd0xwl+c6Ifw6+VtmRyfCEHk8yvSkoKWqdxtD0p3a\n3e8txYoI/YZltAICZ2vjZPv05Ts/VwWVzaxUArYiUH+k6J+6yCavKWesmeac0vLG\nyN07gpRPPsIRAoIBADIp+UDqxf9REsAT+L2I2BK27DKiR3eyhZlwuruLRnKOLv+t\nVTu/ExGSFzvXSERzrkMG+jAG1D4El2MaxqCtFtzO+Na4H2mpePydwHTBMPwJH6rg\nccKES7VqLx6+SyWZYmn9K9sWVseN4fYpn1DGNHBad3ueb7ZbO4hlEfrVLTLWUjXH\nzxQcGcA5liv3FqIGozH9mTUrr0KTwPrtyRGfGgGx2jnGBwuHYEf26D/j7Cv0Ohew\n0u2mO1S2pT/LI2/VderrzBFcyQpxO9MpIOXyymBe0hJOkeTdzlsRPivBTrSbeT4Y\nqd5ucByrQEahkwTtq6rh+jw+vwSx0MtElEotoZkCggEAB8ujNRlOdd5E4JokpMZu\nGBbbqvtGTMpY24FMzgsonlV57B4x5drW2taqXwP/36eBea7TIVYBs02YF8HIhVJ5\nR47h9bZU0G+0bEM2c1CTJ3pceRQQwT2JG0qyor6pa6+O7izJ+aOCOSx7yZgW7FQL\nSMt96r5HUP4MltifTx+RWMa3NjkJId1boz/kr3dvt/UutGsARBpqcVXogxQ9U7p2\nVoxi43bZaOpV1LgIifngTysznzhGjt0Gd1Ac6HkevapjyReKQEHbU8KApc+jaGY2\n7Y7s5RsR4HD2PrsOa5D/7q1roHnajcuErO9CCQvyNa/vEZGMoV61hXgc5UxYah2P\ngQKCAQEAkzISMmGPyQT7t6F/P2dFmrotAUU8gsEaWhrlkS0AuREXv1p14I1OnQhS\neWU7I9qSG4NfslRi5WUnowyawQKYibShtJ9/tOWMTaEELVTDtPAIu2y9kcquiG2j\no34vfpByz0w1vhmd/hwcPAvBFV+oaGN6lPz9Pv9MlNBLJoMhCPdr3aBJJuThT1Ka\nJQ/RT0XfU7XXSC74x7JwoKB4bobVHdON09yielC6w9wq9anqD18nrz/4wBwWDhDE\nKPxeXVpnIZfhukmWxkBY8NLAOFEenS3f6D4wzuOD25mPRSJQTngh7w9XkZYzDnOo\niwa43+YOKJx4Qh4SeXLBc/Udm1eMTA==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-st-6-tls-auth/.env\n  redis:\n    build:\n      context: ./oss-st-6-tls-auth\n      dockerfile: Dockerfile\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/Dockerfile",
    "content": "FROM bitnamilegacy/redis:6.0.8\n\nENV ALLOW_EMPTY_PASSWORD yes\n\n# TLS options\nENV REDIS_TLS_ENABLED yes\nENV REDIS_TLS_PORT 6379\nENV REDIS_TLS_CERT_FILE /opt/bitnami/redis/certs/redis.crt\nENV REDIS_TLS_KEY_FILE /opt/bitnami/redis/certs/redis.key\nENV REDIS_TLS_CA_FILE /opt/bitnami/redis/certs/redisCA.crt\nENV REDIS_TLS_AUTH_CLIENTS yes\n\nCOPY --chown=1001 ./certs /opt/bitnami/redis/certs/\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/certs/redis.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhEwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI3WhcNMzExMDI2MTMyNzI3WjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAM4osMW/hBlde+/E20wP9X+zJ0AMD6OtfhqQ5brC\ngbVs9mPccZ/8R0fj83YtZfnoEodMZ/7yUTeCCPMdprAezMU1KBf9EpZmTdWhpO3e\nkESHcQdsKkqGtyjYF7dDahTmKt4a4aHlPH0DJLltB5HlbVabkzlo+3S8QaNwH5lY\nyJTQIqiqVzs9oRLT76nZuJjsym0dNXE42rO3KCniI6kvJDmUzBD8Wc94iDExfy7q\nqHyV7b2DCp1w7XP4yrQAFQ6kiVqNcfTTAO4MHNP54V2nZLPdOsUD5BsYY8hu0HDc\n/PisZ9ZMcw7LMfpUd3dfA9zefQXPQsWJK20ZCNmdIFtwvIFUpYu/FEF3FWO83zeI\nXkVZiuCOnoFvp8JIDvXgzXUBWzvYmxLqVSZAuabqU5pKRswDPLGlZTkHbuP2DiXD\nLD5AsTnICpzYkUeERSZKf2qc/nTUk04W/7FUT/75ItVzZvu90mPJlmArB0j4zdAG\nKwKo8v/cF1hA1YznhibxcUAA/Q/O3Y6CPQ7C3NeaGKcycgUxWoEY3Leno40ukijd\nR0MvsaY7V0/up37fkPtH9rcCkZOGVT5Q4Ww9gVO3yseXVkxbJyzHV1tuwg6yY9wO\nLOU2Bbmazkjlkb8a5OyQol2zZNJ0L3lvRWTildtGUTsBkeqI6HAedTTHJkhjHh/P\nP0+TAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nAKn+aH60zdd4ItMOhgd4BIok/u6S4NF0oe4kDwC/EU5C0hbuT6gLAn7ArHMeadue\nOuSnhqIPfxNR9WCes9OU1O00WeCuWCE4XfNMMFN5iDFfLxO4Oy5Adt0T74hWbcy6\nh28TdcjrkJEr7HR59G5tQ8TW5gVB4a0WXDw0ob9DSxbFKZU1uZm9L/+MgB/SNCHL\nGZSKt75Z/M10b9BTC3OG9swsoWvXEjR2ICiwzk+LxVf5K38faDyBrNJVglrpEUZz\ngP60kL73qK0y1/i35UuP0yIJIy48XnDsSByN7eBVsNTGMW3CFLKWA4RVfnEHNUff\nvsLHXZFYsUIPnPc5jksFwb/wKAe9JbCrgQPhBYaIYkRGiYt64C48r3boIIVoz9+1\n9Nq0Ik06fCzlI9APq2nzEiVeB7mDyZ692neu32QM6zRkYor+W8uI21YnRJWlOx7+\nx2GIh2EZnEYNvbpbvk/fV5AqkYOu9auRAkcKfME7dJ3Gwndl0YBOjE2DMTv6vIjS\ndVuGXQCvlzkRAnPMh5MR5/bSUKVvBryXs9ecAMgoVXBVB+4tGWct5ziL+8qyNtgA\nWJ2EWj3xtLlMwwQmLjRsCrZjL4liLJG8Yn8Ehfq1rRJREH2O8uYKCO1fdhuI0Y5S\niBPfqJi6QBHj7i01K9OpNUB7l+xAFLA3cBsegcm2GPoL\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/certs/redis.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDOKLDFv4QZXXvv\nxNtMD/V/sydADA+jrX4akOW6woG1bPZj3HGf/EdH4/N2LWX56BKHTGf+8lE3ggjz\nHaawHszFNSgX/RKWZk3VoaTt3pBEh3EHbCpKhrco2Be3Q2oU5ireGuGh5Tx9AyS5\nbQeR5W1Wm5M5aPt0vEGjcB+ZWMiU0CKoqlc7PaES0++p2biY7MptHTVxONqztygp\n4iOpLyQ5lMwQ/FnPeIgxMX8u6qh8le29gwqdcO1z+Mq0ABUOpIlajXH00wDuDBzT\n+eFdp2Sz3TrFA+QbGGPIbtBw3Pz4rGfWTHMOyzH6VHd3XwPc3n0Fz0LFiSttGQjZ\nnSBbcLyBVKWLvxRBdxVjvN83iF5FWYrgjp6Bb6fCSA714M11AVs72JsS6lUmQLmm\n6lOaSkbMAzyxpWU5B27j9g4lwyw+QLE5yAqc2JFHhEUmSn9qnP501JNOFv+xVE/+\n+SLVc2b7vdJjyZZgKwdI+M3QBisCqPL/3BdYQNWM54Ym8XFAAP0Pzt2Ogj0OwtzX\nmhinMnIFMVqBGNy3p6ONLpIo3UdDL7GmO1dP7qd+35D7R/a3ApGThlU+UOFsPYFT\nt8rHl1ZMWycsx1dbbsIOsmPcDizlNgW5ms5I5ZG/GuTskKJds2TSdC95b0Vk4pXb\nRlE7AZHqiOhwHnU0xyZIYx4fzz9PkwIDAQABAoICAHyc/+0oDHNAnK+bsGrTorNj\n2S/Pmox3TChGuXYgKEM/79cA4vWvim6cDQe7/U4Hx1tdBeeHFSyWP06k96Kxm1kA\n/pExedDLWfTt1kGqLE4gCGRSL2YI9CGOLRerei3TysmiOgygAeYWxlYG33KC2Ypm\nU6F6IbS4LnzaQ19v2R6KiMim3j+CyyAUV2O1pO1bBCjcZPdhRGEpLu/SL3gOdLkR\nhiAmSSstUjVaE+SKFvnnrmLFGN996SoWkoAnJJNLRXMk2GMCQCejzrEa8+ymSCqo\naOO5rGHsZjQ7N2dhTNALdmCEqW+hxz3nXKcdGbqiCbQ/Sb8ZYNR7M2xGm85p4Ka9\n0UK4cOM1VJPwz8hotSKAUmXnpbu73CsZi5HyzwZkk4FpcaYCYrCbGVmm1cIKEKI7\n8SN/oqgFdj4Ha9cemnu+RecQZouK+wPWtcILd2kstJl52TV952fVOrnXQDo6XCXB\nfbs9IYN1hB6N79xv4L7Jj53hSRMeSNf70Ejkh1FXPOvmvFT12wy5JQdBBR5nnb4a\nGEsMpGVe1k3bxjK7K263tLSH0UZ8dMgdSx1E4D1hT1K/gEwTSMOJ0E1R0M6SJmF2\n6TnZ0MbJWx6PbICmyZrd2agfTQrq6CgY1fWLGbQrtnwXtsUR7PiHarydXfs3V8g1\nxHnK1bItOBBMOMcWV93hAoIBAQD2xMceUfBUN0TeWrJ4izD3lUYxmWqjTg7kPcjJ\n0VN8v3txGAcyPmbuz5AEzvdFjTispoZNB9QOrmsUVWTQDE5otnei9kHWzqJWYHg4\nUSuUuAh8OJGCiepo8zHT3qHDNhKGtOAp5LC8TaOznUFr35zCBCOsvQfRUKrv5IOc\nvCFjO07Xly8+M3qK7/UswRQ6480VlE2t1p+VNaORHdTDg2tes3/9owuiNmR/sPT8\nnIoe01LS7qmZoiB1vracaLcBf1Iwd7RvKg7mgFJzmowZUYxyX2YGK5qZ1h74In2X\n55+qQnNW0RwPijopTv711pMhMKWl8i3ilcCfoeBXz8zCwFfbAoIBAQDV3wHAO7ic\nMYP/Bm5jgIwlix1eOWY/yB+VqdYn2GmC49vTEIlIVlFRq0vZE06iUxs87BIV08zO\n4w/iKXd7ktkuhphiEuU2yXA3LQPHpbSOW43RONbf4glFU/DlP/P6fiybbWj6+f7L\n7Zbvtz5AW03Y4ZpagJTqOgVdJ0MdLnh9vZj6okGGV1fidKtG6hr7Z/oLhnl9aAZK\n4vrvBZ//qz99vEVByiV5kRaJDulu+diBy4n6iBjzjHA5a9e7lY3sUBw3DMgb7kYs\nJJPkCPdSxCYq4Ef3z/Eao0tyUuCzyznfCMGJA1gBdTpwDNDCTaXqkjR5nvsdE5k0\nIVQgFPtcOPCpAoIBABujNlni+3OzLPdqWQq/LCDOiyoK8LKRj4FomhBgbWVPXNfx\nxPyPmJ+uh4bCV1dm1a4giHIgKlPqnPuOBNh4SF/Z79REmGMiiXP7IfvMu4DQi8K9\n4y4nnCVc93uvN5bRe4mywFhw0IqGd4sqVaVrSfdA124FTdbXng14HnVzbJncjpv+\nxr/ErDjbXy5AAbAGy3VbQsfxfbYMZ+Fc4fNzyJa2q+MQW8EzLlZOz2Frdty09lXB\nfSVDzzbgwTsLT1PPmrjq7z50C28teA6ShJZhV8WHgbm3MH2CSb2ov0BAJNXA04Ip\nsWbcKF9wBYYrHhddh2/qi9EQzJ4UVzf+ggRd3nkCggEAWcjyWjp4KRJcgJ65jwoz\nS7uYS6s7MsGYCOOw5S9kNC/mZDhH+ddK8kdAY1RIqbrL74qHmSQ+kgge7epMn9Mp\nW+/jXyDhm1t7wZ4jPRhisXTcF56ODpU9IR65PfTYPyvjHCkVbm+vOPt4ZxB9kNUD\n3G3xt9bNLXvILrBB66lLqjYDWAzwBy751Tb3hKDZTPv8rAP7Uttt8NhTUi8BWXsR\n/34fcRwlGWEAne9lrlIzQ2IofcXO+8fUgTa17ak+WJvVDINQKvGgAf4lHBFrixKP\nl2ZqsC1a4bz1+nuym6hQlkJ9xUBjHNGTA+FNbpTcd5qDbx9/+lf09D6dq45DbBb3\naQKCAQBrnFYocTm/fIeKo1n1kyF2ULkd6k984ztH8UyluXleSS1ShFFoo/x3vz35\nfsZNUggRnSe7/OBGZYquF/1roVULI1hKh4tbEmW4SWNeTFvwXKdRe6T7NnWSZtS/\nKtamA3lT2wtoEVOvfMo8M0hoFuRWdT2M0i+LKZQdRsq18XPLqdHt1kkSNcnPDERm\n4gLQ8zXTf2fHrtZmyM8fc0GuTVwprPFeJkLtSPehkeXSTgb6rpyelX9NBUILwRgP\nnw0+cbjFDFKaLnIrMFoVAAn/8DcnbbSt1TZhgNsMxY+GHWPBYW8SUi5nBmQQtmA7\nn3ju44acIPvJ9sWuZruVlWZGFaHm\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/certs/redisCA.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/certs/user.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhIwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTQz\nNTAzWhcNMzExMDI2MTQzNTAzWjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAKOod8jpFXqjtNvl0FgIkg0fSZbzvh7jbI7TEUVQ\nmyeZxjmB3fZh5f6dxM7TZ048CUOeUeq3lemDqay+Moku0rL4PsFNe8z1C1zHuhf9\n4Qw/f7rMBIZ73L4Y/7cPWfjZbeme06+D7HMBZGTWGHZCWrqZQOwA3hKBjC3VY/a5\nz6oP78+w18WDpnavGwXwgCd1yTOwz3tVJUOcJdjGv3iwrHABcGVfxUEKTabP+p6V\nHA/+w4AlCloS57GQCh0RWCXMyfekv6MGBaqQa6GtOK5ScLJ1YSlJ6PRoK2N+shbw\nL/kQGlilgYBVGOQgNKd94+PwJgOCy72S7p9yF3ZTBB4/51Bwl7IV74Om/GmqzJMx\nxY9/PPaxKlOkP+dW41/IrcDULdh0jAfe9rKdFf9/9NWA37S68pKFpzRuRrpLqIwm\nBPtHvtLnTbhgmS/O1Rwmxqs8r+VA6D8+/drAor/KAcCwgRiYLvhvl4ABoqj4toEK\njCXAR/jeoLAb8HDBzkot4hhJPjMhQMYX9/HfdK4YX359EkHdsO/+R6+ImXb68DS5\nzh0028ktMM+KEhWSffSmU3imZOrH1/TQfSxfzuTHvyd0HXAHvzx+w1VWNK4fqU8O\ntDbMt1GAaatrfrqwP4qTjzLEqtlJLIjg4qgzpYCRUvgVdxyeii9o7IeYT8I6Penf\nQpAJAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nABb+A9C1AqstP5d2HXS/pIef1BNYAV8A/D2+nUpEXiautmjRZBRNwzBX2ffEZWV7\nEMkvqrRwz2ZgOJ4BRzJiDIYoF8dOhd0lc/0PqoR0xVzjOFDqd0ZcPHAjaY3UoBE7\njQSQ6ccc1tY5peNLAWCvRO6V9yhdV/SKGhveXGl/24MK9juwArnAitekCWZJQifT\nCFOJX5UvifrT8s0v0AqkycaNpkMvl0BAl4DRDJ3+EwZmzfOdATawyXBVXHt1Gz+N\niskPJAJsIjEdFYTjDUzwRN3bHFbTRXt2v1U18YIvMjvxq8MlITEC2lEW+3Xu90d3\naE/N9mLNJCgmZ2CGywWoaJlUXix2LTo5kT5coVVx0HK0tg5EcBua05qM3xO9Rgxv\nHkCnm/jMeN4oQ5o7h+q7UQja8mg1bjCzlt+RxqoA1snjglra/h5I8TTEhvSfxEy7\nh5Wiwne/TH/e8fN1IYRDvv602MNSZnAEPyG3Hc5xQOSGNpoKOZG7tpU+mRYIvlPe\nJgA5WNZ83y25JqSxF3kQuk7vrLByzEByqV3j+jIAQiHu/qIXwXUpkoV3L6A18yx/\nTbpQasr/bRFZKe83WlNl2ASAVyubal8ocmA0ua24/RV0I0VOCEXiIkl+pZ6e5Qn4\nL6Tryy5NxaEpUAZ9yv3P75PfNVQ3+vGYi3BLuhZUf/Dd\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/certs/user.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCjqHfI6RV6o7Tb\n5dBYCJINH0mW874e42yO0xFFUJsnmcY5gd32YeX+ncTO02dOPAlDnlHqt5Xpg6ms\nvjKJLtKy+D7BTXvM9Qtcx7oX/eEMP3+6zASGe9y+GP+3D1n42W3pntOvg+xzAWRk\n1hh2Qlq6mUDsAN4SgYwt1WP2uc+qD+/PsNfFg6Z2rxsF8IAndckzsM97VSVDnCXY\nxr94sKxwAXBlX8VBCk2mz/qelRwP/sOAJQpaEuexkAodEVglzMn3pL+jBgWqkGuh\nrTiuUnCydWEpSej0aCtjfrIW8C/5EBpYpYGAVRjkIDSnfePj8CYDgsu9ku6fchd2\nUwQeP+dQcJeyFe+DpvxpqsyTMcWPfzz2sSpTpD/nVuNfyK3A1C3YdIwH3vaynRX/\nf/TVgN+0uvKShac0bka6S6iMJgT7R77S5024YJkvztUcJsarPK/lQOg/Pv3awKK/\nygHAsIEYmC74b5eAAaKo+LaBCowlwEf43qCwG/Bwwc5KLeIYST4zIUDGF/fx33Su\nGF9+fRJB3bDv/keviJl2+vA0uc4dNNvJLTDPihIVkn30plN4pmTqx9f00H0sX87k\nx78ndB1wB788fsNVVjSuH6lPDrQ2zLdRgGmra366sD+Kk48yxKrZSSyI4OKoM6WA\nkVL4FXccnoovaOyHmE/COj3p30KQCQIDAQABAoICADAiwPiq9dJYjD2RXrJF8w9B\nAJgRoP3cznVDx3SnvLrtE8yeUfbB3LADH3vl2iC8r8zfqCBtVv6T5zgTyTFoQDi7\no1mfvKYP/QORCz87QRIlKyB6GWqky8xt9eiV71SuPxHT0Vdyaf15j1nJTvCZm63+\nnYXMy4SN7fkdJoXPKTFP9q0TyqMhkbie0Efy8P6qOj+l5aDU7lzwdIFKE88fx9g5\n1CE9BfuXWDeUPJagLNzXhhEO0/iiTtt/Djp2e4LCtTTNlEAS6V+9kqq/FEjRnqwe\nsjE+t/ILIZfmD+OHSdTr05P3OhvQ671Na69H69uDKuslcV+U8/KZ0CTRTgjHqvUZ\neLNC8BZfAk8IZx637/rSlqPmxyS/j35vdslebTbWV2KM7jXPqSb9YokdoJ6M0NZX\nIYiMK2reVzjy2YvX1Nhp4Xn68il10XVS4P9tFxyNWdTclCbuSlTfgc27ercQMMgY\nfe7/8+A/QhV8tdly8W3HwTmvkmmWRSTMziI+zQzZmYYlAWb33rQYfMoHs4tEf2u2\nRf0Oso56X73sc3ncnOFm+s5iwTeUH6EgF3ephJX4nR3canmtpy40nbXUJ+tAuaAj\nuo56KNlPxIHKf96o2LGXGTrgbH39f0MebWOq/7YjtCg6sUbwuyyG3afLTHHuss13\n5bTJ5gD3rsiGUWjfY3oBAoIBAQDRR/BnDw501Hky32/Vhlt7Ef1iplru+Fh0yQj7\n2DQ+U+L1Ir4Q67ESH8qDnjkyLP1a8BDNOIEEGp5dBb+OHb/rwdb+RZ7OCIzFCQ/d\nWR7m0ucuPBQwytQb7iXa9w0umZwoeTXEGP8aGe+bSBIHv8/em26rkSx0A1rxr2/O\n1ho8xxgBmOxL3NSCnv56JUu/W0vFq/7OfWQ19SOvFahp3TeqR1gkHe76teWv11Pj\n+RdiIIdCOifWChZPEdgMZD4rl1cs9QQb+n+WkRt/mZgtTIRQIe+we+vIha7TW46X\n6A1DjSxV4WUSXvv10heYYpZkKzpNG9YOhRB3bvyDkRy11XZ5AoIBAQDIMUETtoa9\nEFNY+uieZwJCTWrrB1njLLRZS3eCAKsVegHD0txLG8H5VMkyZQErRe52zR9QXWU/\nU80tIO5BTbP3ME2AbjJvMwuiEe1lBKlVnn2JSGjbtzUMa1QBvDRmBEZkr8OneMN6\np2tX3L3Vw8Xm/97rjkAgo3gQkqyDf6VZ4xvH2Wo405yMywcoifMZXo/PN9fI5V8S\nfi3XjHrHzaY4cucbdaezVb4Zd0xwl+c6Ifw6+VtmRyfCEHk8yvSkoKWqdxtD0p3a\n3e8txYoI/YZltAICZ2vjZPv05Ts/VwWVzaxUArYiUH+k6J+6yCavKWesmeac0vLG\nyN07gpRPPsIRAoIBADIp+UDqxf9REsAT+L2I2BK27DKiR3eyhZlwuruLRnKOLv+t\nVTu/ExGSFzvXSERzrkMG+jAG1D4El2MaxqCtFtzO+Na4H2mpePydwHTBMPwJH6rg\nccKES7VqLx6+SyWZYmn9K9sWVseN4fYpn1DGNHBad3ueb7ZbO4hlEfrVLTLWUjXH\nzxQcGcA5liv3FqIGozH9mTUrr0KTwPrtyRGfGgGx2jnGBwuHYEf26D/j7Cv0Ohew\n0u2mO1S2pT/LI2/VderrzBFcyQpxO9MpIOXyymBe0hJOkeTdzlsRPivBTrSbeT4Y\nqd5ucByrQEahkwTtq6rh+jw+vwSx0MtElEotoZkCggEAB8ujNRlOdd5E4JokpMZu\nGBbbqvtGTMpY24FMzgsonlV57B4x5drW2taqXwP/36eBea7TIVYBs02YF8HIhVJ5\nR47h9bZU0G+0bEM2c1CTJ3pceRQQwT2JG0qyor6pa6+O7izJ+aOCOSx7yZgW7FQL\nSMt96r5HUP4MltifTx+RWMa3NjkJId1boz/kr3dvt/UutGsARBpqcVXogxQ9U7p2\nVoxi43bZaOpV1LgIifngTysznzhGjt0Gd1Ac6HkevapjyReKQEHbU8KApc+jaGY2\n7Y7s5RsR4HD2PrsOa5D/7q1roHnajcuErO9CCQvyNa/vEZGMoV61hXgc5UxYah2P\ngQKCAQEAkzISMmGPyQT7t6F/P2dFmrotAUU8gsEaWhrlkS0AuREXv1p14I1OnQhS\neWU7I9qSG4NfslRi5WUnowyawQKYibShtJ9/tOWMTaEELVTDtPAIu2y9kcquiG2j\no34vfpByz0w1vhmd/hwcPAvBFV+oaGN6lPz9Pv9MlNBLJoMhCPdr3aBJJuThT1Ka\nJQ/RT0XfU7XXSC74x7JwoKB4bobVHdON09yielC6w9wq9anqD18nrz/4wBwWDhDE\nKPxeXVpnIZfhukmWxkBY8NLAOFEenS3f6D4wzuOD25mPRSJQTngh7w9XkZYzDnOo\niwa43+YOKJx4Qh4SeXLBc/Udm1eMTA==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  ssh:\n    image: lscr.io/linuxserver/openssh-server:9.7_p1-r4-ls172\n    environment:\n      - PASSWORD_ACCESS=true\n      - USER_PASSWORD=pass\n      - USER_NAME=u\n      - DOCKER_MODS=linuxserver/mods:openssh-server-ssh-tunnel\n      - PUBLIC_KEY_DIR=/keys/pub\n    volumes:\n      - ./oss-st-6-tls-auth-ssh/ssh/keys:/keys\n    expose:\n      - 2222\n    networks:\n      - private\n      - ssh\n  test:\n    env_file:\n      - ./oss-st-6-tls-auth-ssh/.env\n    command:\n      [\n        './wait-for-it.sh',\n        'ssh:2222',\n        '-s',\n        '-t',\n        '120',\n        '--',\n        'yarn',\n        'test:api:ci:cov',\n      ]\n    links:\n      - ssh:ssh\n    networks:\n      - ssh\n  redis:\n    build:\n      context: ./oss-st-6-tls-auth-ssh\n      dockerfile: Dockerfile\n    networks:\n      - private\n  app:\n    links:\n      - ssh:ssh\n    networks:\n      - ssh\n\nnetworks:\n  private:\n    name: 'oss-st-6-tls-auth-ssh-net-private'\n    internal: true\n  ssh:\n    name: 'oss-st-6-tls-auth-ssh-net'\n    internal: false\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/ssh/keys/pub/test.pub",
    "content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPXS0xkxY7o+MUNBJJnf6fKh6AFFpzB0YIfifHSSseXw\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/ssh/keys/pub/testp.pub",
    "content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEs/ewkUXl0+uDr7hxSM2vURqdRNFHm7+x05azzW/Yzu\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/ssh/keys/test",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8AAAAKBv1saEb9bG\nhAAAAAtzc2gtZWQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8A\nAAAEDyew1DnmWamAr0OrUM87FauJfFfea+pi8ctpKNnurNi/XS0xkxY7o+MUNBJJnf6fKh\n6AFFpzB0YIfifHSSseXwAAAAG3pvem9Aem96by1IUC1Qcm9Cb29rLTQ1MC1HNwEC\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-6-tls-auth-ssh/ssh/keys/testp",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBPcEHCGN\nDrMHhpQnPwc0XwAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEs/ewkUXl0+uDr7\nhxSM2vURqdRNFHm7+x05azzW/YzuAAAAoEhNzctHXM6YBV0z4zzvdniQ5cLwsv8TfMZp2G\nWUhZU05yugvKlRu1pml5q3XGSP5wYCF4vvi4BE563PMDKZWAqFFGtiTotEn+XuD/eP+P8H\nxdf91tV5kE+1yvVwxUNMcijHY0uYopnG2NN3bdjOH/4YmW0WLyDu10EoMZKVnrP0qBbOrR\nxKIy5lqa39SrAnUnGSoTEJsEWGLiIS2rBhkVc=\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-big/Dockerfile",
    "content": "FROM redislabs/redismod\n\nARG TEST_DB_DUMP\nADD $TEST_DB_DUMP /data/\n\nADD entrypoint.sh .\nRUN chmod +x entrypoint.sh\n\nENTRYPOINT [\"sh\", \"entrypoint.sh\", \"redis-server\"]\nCMD [\"--loadmodule\", \"/usr/lib/redis/modules/redisai.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisearch.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisgraph.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redistimeseries.so\", \"--loadmodule\", \"/usr/lib/redis/modules/rejson.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisbloom.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisgears.so\", \"Plugin\", \"/var/opt/redislabs/modules/rg/plugin/gears_python.so\"]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-big/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./oss-st-big/.env\n  redis:\n    build:\n      context: ./oss-st-big\n      dockerfile: Dockerfile\n      args:\n        TEST_DB_DUMP: $TEST_MEDIUM_DB_DUMP\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/oss-st-big/entrypoint.sh",
    "content": "#!/bin/sh\n\nif [ -e dump.tar.gz ]\nthen\n  echo 'Extracting .rdb file...'\n  tar -zxvf dump.tar.gz\nfi\n\nexec \"$@\"\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-clu/Dockerfile",
    "content": "FROM redislabs/redis:6.2.8-50\n\n## Set the env var to instruct RE to create a cluster on startup\nENV BOOTSTRAP_ACTION create_cluster\nENV BOOTSTRAP_CLUSTER_FQDN cluster.local\n\nCOPY entrypoint.sh db.json ./\n\nENTRYPOINT [ \"bash\", \"./entrypoint.sh\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-clu/db.json",
    "content": "{\n  \"name\": \"testdb\",\n  \"type\": \"redis\",\n  \"memory_size\": 1073741824,\n  \"port\": 12010,\n  \"sharding\": true,\n  \"shards_count\": 3,\n  \"proxy_policy\": \"all-master-shards\",\n  \"oss_cluster\": true,\n  \"shard_key_regex\": [\n    {\n      \"regex\": \".*\\\\{(?<tag>.*)\\\\}.*\"\n    },\n    {\n      \"regex\": \"(?<tag>.*)\"\n    }\n  ]\n}\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-clu/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./re-clu/.env\n    command:\n      [\n        './wait-for-it.sh',\n        'redis:12010',\n        '-s',\n        '-t',\n        '120',\n        '--',\n        'yarn',\n        'test:api:ci:cov',\n      ]\n  redis:\n    build: ./re-clu\n    cap_add:\n      - sys_resource\n    env_file:\n      - ./re-clu/.env\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-clu/entrypoint.sh",
    "content": "#! /bin/bash\n\nTEST_RE_USER=${TEST_RE_USER:-\"demo@redislabs.com\"}\nTEST_RE_PASS=${TEST_RE_PASS:-\"123456\"}\n\nset -e\n\n# enable job control\nset -m\n\n/opt/start.sh &\n\n# This command queries the REST API and outputs the status code\nCURL_CMD=\"curl --silent --fail --output /dev/null -i -w %{http_code} -u $TEST_RE_USER:$TEST_RE_PASS -k https://localhost:9443/v1/nodes\"\n\n# Wait to get 2 consecutive 200 responses from the REST API\nwhile true\ndo\n    echo yay $CURL_CMD\n    CURL_CMD_OUTPUT=$($CURL_CMD || true)\n    if [ $CURL_CMD_OUTPUT == \"200\" ]\n    then\n        echo \"Got 200 response, trying again in 5 seconds to verify...\"\n        sleep 5\n        if [ $($CURL_CMD || true) == \"200\" ]\n        then\n            echo \"Got 200 response after 5 seconds again, proceeding...\"\n            break\n        fi\n    else\n        echo \"Did not get 200 response, got $CURL_CMD_OUTPUT, trying again in 10 seconds...\"\n        sleep 10\n    fi\ndone\n\necho \"Creating databases...\"\n\ncurl -k -u \"$TEST_RE_USER:$TEST_RE_PASS\" --request POST --url \"https://localhost:9443/v1/bdbs\" --header 'content-type: application/json' --data-binary \"@db.json\"\n\n# now we bring the primary process back into the foreground\n# and leave it there\nfg\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-crdt/Dockerfile",
    "content": "FROM redislabs/redis:6.2.8-50\n\n## Set the env var to instruct RE to create a cluster on startup\nENV BOOTSTRAP_ACTION create_cluster\n\nCOPY entrypoint.sh .\n\nENTRYPOINT [ \"bash\", \"./entrypoint.sh\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-crdt/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./re-crdt/.env\n    command:\n      [\n        './wait-for-it.sh',\n        'redis:12000',\n        '-s',\n        '-t',\n        '120',\n        '--',\n        'yarn',\n        'test:api:ci:cov',\n      ]\n  redis:\n    build: ./re-crdt\n    environment:\n      - BOOTSTRAP_CLUSTER_FQDN=cluster1.local\n      - CREATE_DB=1\n    depends_on:\n      - redis-2\n    cap_add:\n      - sys_resource\n    env_file:\n      - ./re-crdt/.env\n  redis-2:\n    build: ./re-crdt\n    environment:\n      - BOOTSTRAP_CLUSTER_FQDN=cluster2.local\n    cap_add:\n      - sys_resource\n    env_file:\n      - ./re-crdt/.env\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-crdt/entrypoint.sh",
    "content": "#! /bin/bash\n\nTEST_RE_USER=${TEST_RE_USER:-\"demo@redislabs.com\"}\nTEST_RE_PASS=${TEST_RE_PASS:-\"123456\"}\n\nset -e\n\n# enable job control\nset -m\n\n/opt/start.sh &\n\nif [ $CREATE_DB ]\nthen\n# This command queries the REST API and outputs the status code\nCURL_CMD=\"curl --silent --fail --output /dev/null -i -w %{http_code} -u $TEST_RE_USER:$TEST_RE_PASS -k https://localhost:9443/v1/nodes\"\n\n# Wait to get 2 consecutive 200 responses from the REST API\nwhile true\ndo\n    echo yay $CURL_CMD\n    CURL_CMD_OUTPUT=$($CURL_CMD || true)\n    if [ $CURL_CMD_OUTPUT == \"200\" ]\n    then\n        echo \"Got 200 response, trying again in 5 seconds to verify...\"\n        sleep 5\n        if [ $($CURL_CMD || true) == \"200\" ]\n        then\n            echo \"Got 200 response after 5 seconds again, proceeding...\"\n            break\n        fi\n    else\n        echo \"Did not get 200 response, got $CURL_CMD_OUTPUT, trying again in 10 seconds...\"\n        sleep 10\n    fi\ndone\n\necho \"Creating databases...\"\n\n/opt/redislabs/bin/crdb-cli crdb create \\\n  --name testdb --memory-size 1024mb --port 12000 --replication false --shards-count 1 \\\n  --oss-cluster true --proxy-policy=all-nodes \\\n  --instance fqdn=cluster1.local,username=\"$TEST_RE_USER\",password=\"$TEST_RE_PASS\" \\\n  --instance fqdn=cluster2.local,username=\"$TEST_RE_USER\",password=\"$TEST_RE_PASS\"\n\nfi\n\n# now we bring the primary process back into the foreground\n# and leave it there\nfg\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-st/Dockerfile",
    "content": "FROM redislabs/redis:6.2.8-50\n\n## Set the env var to instruct RE to create a cluster on startup\nENV BOOTSTRAP_ACTION create_cluster\nENV BOOTSTRAP_CLUSTER_FQDN cluster.local\n\nUSER root\nRUN sed -i \"141s/username,/username, 'flash_enabled',/g\" bootstrap.py\nUSER redislabs\n\nCOPY entrypoint.sh db.json ./\n\nENTRYPOINT [ \"bash\", \"./entrypoint.sh\" ]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-st/db.json",
    "content": "{\n  \"name\": \"testdb\",\n  \"type\": \"redis\",\n  \"memory_size\": 1073741824,\n  \"port\": 12000\n}\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-st/docker-compose.yml",
    "content": "version: '3.4'\n\nservices:\n  test:\n    env_file:\n      - ./re-st/.env\n    command:\n      [\n        './wait-for-it.sh',\n        'redis:12000',\n        '-s',\n        '-t',\n        '120',\n        '--',\n        'yarn',\n        'test:api:ci:cov',\n      ]\n  redis:\n    build: ./re-st\n    cap_add:\n      - sys_resource\n    env_file:\n      - ./re-st/.env\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/re-st/entrypoint.sh",
    "content": "#! /bin/bash\n\nTEST_RE_USER=${TEST_RE_USER:-\"demo@redislabs.com\"}\nTEST_RE_PASS=${TEST_RE_PASS:-\"123456\"}\n\nset -e\n\n# enable job control\nset -m\n\n/opt/start.sh &\n\n# This command queries the REST API and outputs the status code\nCURL_CMD=\"curl --silent --fail --output /dev/null -i -w %{http_code} -u $TEST_RE_USER:$TEST_RE_PASS -k https://localhost:9443/v1/nodes\"\n\n# Wait to get 2 consecutive 200 responses from the REST API\nwhile true\ndo\n    echo yay $CURL_CMD\n    CURL_CMD_OUTPUT=$($CURL_CMD || true)\n    if [ $CURL_CMD_OUTPUT == \"200\" ]\n    then\n        echo \"Got 200 response, trying again in 5 seconds to verify...\"\n        sleep 5\n        if [ $($CURL_CMD || true) == \"200\" ]\n        then\n            echo \"Got 200 response after 5 seconds again, proceeding...\"\n            break\n        fi\n    else\n        echo \"Did not get 200 response, got $CURL_CMD_OUTPUT, trying again in 10 seconds...\"\n        sleep 10\n    fi\ndone\n\necho \"Creating databases...\"\n\ncurl -k -u \"$TEST_RE_USER:$TEST_RE_PASS\" --request POST --url \"https://localhost:9443/v1/bdbs\" --header 'content-type: application/json' --data-binary \"@db.json\"\n\n# now we bring the primary process back into the foreground\n# and leave it there\nfg\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/run-all.sh",
    "content": "#!/bin/bash\n\nBASEDIR=$(dirname $0)\n$BASEDIR/start-test-run.sh -r redis-5 |\n$BASEDIR/start-test-run.sh -r redis-6\n#echo \"All Test Runs were executed\"\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/start-test-run.sh",
    "content": "#!/bin/bash\n\nBASEDIR=$(dirname $0)\nBUILD=\"local\"\n\nhelpFunction()\n{\n  printf \"Some of the required parameters are empty\\n\\n\"\n  printf \"Usage: %s -r RTE [-t local]\\n\" \"$0\"\n  printf \" -r - (required) Redis Test Environment (RTE). Should match any service name from redis.docker-compose.yml\\n\"\n  printf \" -f - (optional) force rebuild api image\\n\"\n  printf \" -t - Backend build type.\n    \\t local - (default) run server using source code\n    \\t docker - run server on built docker container\n    \"\n  exit 1 # Exit script after printing help\n}\n\n# required params\nwhile getopts \"r:t:f\" opt\ndo\n   case \"$opt\" in\n      r ) RTE=\"$OPTARG\" ;;\n      t ) BUILD=\"$OPTARG\" ;;\n      f ) FORCE_REBUILD=true ;;\n      ? ) helpFunction ;; # Print helpFunction in case parameter is non-existent\n   esac\ndone\necho \"BUILD: ${BUILD}\"\n\n# Print helpFunction in case parameters are empty\nif [ -z \"$RTE\" ]\nthen\n   helpFunction\nfi\n\n# Unique ID for the test run\nID=$RTE-$(tr -dc a-z0-9 </dev/urandom | head -c 6)\n\n# Check if we need to run prestart script\nPRESTART=\"$BASEDIR/$RTE/prestart.sh\"\nif test -f \"$PRESTART\"; then\n    echo \"Running prestart.sh script...\"\n    ID=$ID ./$PRESTART``\nfi\n\necho \"Pulling RTE... ${RTE}\"\neval \"ID=$ID RTE=$RTE docker compose \\\n  -f $BASEDIR/$BUILD.build.yml \\\n  -f $BASEDIR/$RTE/docker-compose.yml \\\n  --env-file $BASEDIR/$BUILD.build.env pull redis\"\n\necho \"Building RTE... ${RTE}\"\neval \"ID=$ID RTE=$RTE docker compose \\\n  -f $BASEDIR/$BUILD.build.yml \\\n  -f $BASEDIR/$RTE/docker-compose.yml \\\n  --env-file $BASEDIR/$BUILD.build.env build --no-cache redis\"\n\nif [ \"$FORCE_REBUILD\" = true ]; then\n    echo \"Force rebuilding api and test containers\"\n    eval \"ID=$ID RTE=$RTE docker compose \\\n      -f $BASEDIR/$BUILD.build.yml \\\n      -f $BASEDIR/$RTE/docker-compose.yml \\\n      --env-file $BASEDIR/$BUILD.build.env build --no-cache test app\"\nfi\n\necho \"Test run is starting... ${RTE}\"\neval \"ID=$ID RTE=$RTE docker compose -p $ID \\\n  -f $BASEDIR/$BUILD.build.yml \\\n  -f $BASEDIR/$RTE/docker-compose.yml \\\n  --env-file $BASEDIR/$BUILD.build.env run --build --use-aliases test\"\n\n# Remember exit code for tests\nTEST_EXIT_CODE=$?\n\necho \"Stop all containers... ${RTE}\"\neval \"ID=$ID RTE=$RTE docker compose -p $ID \\\n  -f $BASEDIR/$BUILD.build.yml \\\n  -f $BASEDIR/$RTE/docker-compose.yml \\\n  --env-file $BASEDIR/$BUILD.build.env stop\"\n\necho \"Remove containers with anonymous volumes... ${RTE}\"\neval \"ID=$ID RTE=$RTE docker compose -p $ID \\\n  -f $BASEDIR/$BUILD.build.yml \\\n  -f $BASEDIR/$RTE/docker-compose.yml \\\n  --env-file $BASEDIR/$BUILD.build.env rm -v -f\"\n\necho \"Removing test run docker network...\"\neval \"docker network rm $ID || true\"\n\nexit $TEST_EXIT_CODE\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/test-docker-entry.sh",
    "content": "#!/bin/sh\n\n# Initializing system's secret storage\neval \"$(dbus-launch --sh-syntax)\"\n\nmkdir -p ~/.cache\nmkdir -p ~/.local/share/keyrings\n# fix \"Remote error from secret service:\n# org.freedesktop.Secret.Error.IsLocked: Cannot create an item in a locked collection\" issue\neval \"$(echo \"$GNOME_KEYRING_PASS\" | gnome-keyring-daemon --unlock)\"\nsleep 1\neval \"$(echo \"$GNOME_KEYRING_PASS\" | gnome-keyring-daemon --start)\"\n\nexec \"$@\"\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/test.Dockerfile",
    "content": "FROM node:22.12.0-alpine as test\n\nRUN apk update && apk add bash libsecret dbus-x11 gnome-keyring\nRUN dbus-uuidgen > /var/lib/dbus/machine-id\n\nWORKDIR /usr/src/app\n\nCOPY package.json yarn.lock ./\nCOPY stubs ./stubs\nRUN yarn install\nCOPY . .\n\nCOPY ./test/test-runs/test-docker-entry.sh ./test/test-runs/wait-for-it.sh ./\nRUN chmod +x test-docker-entry.sh\nRUN chmod +x wait-for-it.sh\n\nARG GNOME_KEYRING_PASS=\"somepass\"\nENV GNOME_KEYRING_PASS=${GNOME_KEYRING_PASS}\n\nENTRYPOINT [\"./test-docker-entry.sh\"]\nCMD [\"yarn\", \"test:api:ci:cov\"]\n"
  },
  {
    "path": "redisinsight/api/test/test-runs/wait-for-it.sh",
    "content": "#!/usr/bin/env bash\n# Use this script to test if a given TCP host/port are available\n\nWAITFORIT_cmdname=${0##*/}\n\nechoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo \"$@\" 1>&2; fi }\n\nusage()\n{\n    cat << USAGE >&2\nUsage:\n    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]\n    -h HOST | --host=HOST       Host or IP under test\n    -p PORT | --port=PORT       TCP port under test\n                                Alternatively, you specify the host and port as host:port\n    -s | --strict               Only execute subcommand if the test succeeds\n    -q | --quiet                Don't output any status messages\n    -t TIMEOUT | --timeout=TIMEOUT\n                                Timeout in seconds, zero for no timeout\n    -- COMMAND ARGS             Execute command with args after the test finishes\nUSAGE\n    exit 1\n}\n\nwait_for()\n{\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    else\n        echoerr \"$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout\"\n    fi\n    WAITFORIT_start_ts=$(date +%s)\n    while :\n    do\n        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then\n            nc -z $WAITFORIT_HOST $WAITFORIT_PORT\n            WAITFORIT_result=$?\n        else\n            (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1\n            WAITFORIT_result=$?\n        fi\n        if [[ $WAITFORIT_result -eq 0 ]]; then\n            WAITFORIT_end_ts=$(date +%s)\n            echoerr \"$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds\"\n            break\n        fi\n        sleep 1\n    done\n    return $WAITFORIT_result\n}\n\nwait_for_wrapper()\n{\n    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692\n    if [[ $WAITFORIT_QUIET -eq 1 ]]; then\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    else\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    fi\n    WAITFORIT_PID=$!\n    trap \"kill -INT -$WAITFORIT_PID\" INT\n    wait $WAITFORIT_PID\n    WAITFORIT_RESULT=$?\n    if [[ $WAITFORIT_RESULT -ne 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    fi\n    return $WAITFORIT_RESULT\n}\n\n# process arguments\nwhile [[ $# -gt 0 ]]\ndo\n    case \"$1\" in\n        *:* )\n        WAITFORIT_hostport=(${1//:/ })\n        WAITFORIT_HOST=${WAITFORIT_hostport[0]}\n        WAITFORIT_PORT=${WAITFORIT_hostport[1]}\n        shift 1\n        ;;\n        --child)\n        WAITFORIT_CHILD=1\n        shift 1\n        ;;\n        -q | --quiet)\n        WAITFORIT_QUIET=1\n        shift 1\n        ;;\n        -s | --strict)\n        WAITFORIT_STRICT=1\n        shift 1\n        ;;\n        -h)\n        WAITFORIT_HOST=\"$2\"\n        if [[ $WAITFORIT_HOST == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --host=*)\n        WAITFORIT_HOST=\"${1#*=}\"\n        shift 1\n        ;;\n        -p)\n        WAITFORIT_PORT=\"$2\"\n        if [[ $WAITFORIT_PORT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --port=*)\n        WAITFORIT_PORT=\"${1#*=}\"\n        shift 1\n        ;;\n        -t)\n        WAITFORIT_TIMEOUT=\"$2\"\n        if [[ $WAITFORIT_TIMEOUT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --timeout=*)\n        WAITFORIT_TIMEOUT=\"${1#*=}\"\n        shift 1\n        ;;\n        --)\n        shift\n        WAITFORIT_CLI=(\"$@\")\n        break\n        ;;\n        --help)\n        usage\n        ;;\n        *)\n        echoerr \"Unknown argument: $1\"\n        usage\n        ;;\n    esac\ndone\n\nif [[ \"$WAITFORIT_HOST\" == \"\" || \"$WAITFORIT_PORT\" == \"\" ]]; then\n    echoerr \"Error: you need to provide a host and port to test.\"\n    usage\nfi\n\nWAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}\nWAITFORIT_STRICT=${WAITFORIT_STRICT:-0}\nWAITFORIT_CHILD=${WAITFORIT_CHILD:-0}\nWAITFORIT_QUIET=${WAITFORIT_QUIET:-0}\n\n# Check to see if timeout is from busybox?\nWAITFORIT_TIMEOUT_PATH=$(type -p timeout)\nWAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)\n\nWAITFORIT_BUSYTIMEFLAG=\"\"\nif [[ $WAITFORIT_TIMEOUT_PATH =~ \"busybox\" ]]; then\n    WAITFORIT_ISBUSY=1\n    # Check if busybox timeout uses -t flag\n    # (recent Alpine versions don't support -t anymore)\n    if timeout &>/dev/stdout | grep -q -e '-t '; then\n        WAITFORIT_BUSYTIMEFLAG=\"-t\"\n    fi\nelse\n    WAITFORIT_ISBUSY=0\nfi\n\nif [[ $WAITFORIT_CHILD -gt 0 ]]; then\n    wait_for\n    WAITFORIT_RESULT=$?\n    exit $WAITFORIT_RESULT\nelse\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        wait_for_wrapper\n        WAITFORIT_RESULT=$?\n    else\n        wait_for\n        WAITFORIT_RESULT=$?\n    fi\nfi\n\nif [[ $WAITFORIT_CLI != \"\" ]]; then\n    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then\n        echoerr \"$WAITFORIT_cmdname: strict mode, refusing to execute subprocess\"\n        exit $WAITFORIT_RESULT\n    fi\n    exec \"${WAITFORIT_CLI[@]}\"\nelse\n    exit $WAITFORIT_RESULT\nfi\n"
  },
  {
    "path": "redisinsight/api/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\n    \"node_modules\",\n    \"test\",\n    \"dist\",\n    \"ui\",\n    \"**/ui/**.*\",\n    \"**/*spec.ts\",\n    \"src/__mocks__\"\n  ]\n}\n"
  },
  {
    "path": "redisinsight/api/tsconfig.build.prod.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"sourceMap\": false\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"test\",\n    \"dist\",\n    \"ui\",\n    \"**/ui/**.*\",\n    \"**/*spec.ts\",\n    \"src/__mocks__\"\n  ]\n}\n"
  },
  {
    "path": "redisinsight/api/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"es2019\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"incremental\": true,\n    \"paths\": {\n      \"src/*\": [\"src/*\"],\n      \"apiSrc/*\": [\"src/*\"],\n      \"tests/*\": [\"__tests__/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/app.ts",
    "content": "/* eslint global-require: off, no-console: off */\nimport { app, nativeTheme } from 'electron'\nimport log from 'electron-log'\nimport path from 'path'\nimport {\n  initElectronHandlers,\n  initLogging,\n  WindowType,\n  windowFactory,\n  AboutPanelOptions,\n  initAutoUpdateChecks,\n  installExtensions,\n  initTray,\n  initAutoUpdaterHandlers,\n  launchApiServer,\n  initCloudHandlers,\n  electronStore,\n} from 'desktopSrc/lib'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { configMain as config } from 'desktopSrc/config'\nimport {\n  deepLinkHandler,\n  deepLinkWindowHandler,\n} from 'desktopSrc/lib/app/deep-link.handlers'\nimport { ElectronStorageItem } from 'uiSrc/electron/constants'\n\nif (!config.isProduction) {\n  const sourceMapSupport = require('source-map-support')\n  sourceMapSupport.install()\n}\n\nlet deepLink: undefined | string\n\nconst init = async () => {\n  await launchApiServer()\n  initLogging()\n  initElectronHandlers()\n  initAutoUpdaterHandlers()\n  initTray()\n  initCloudHandlers()\n\n  nativeTheme.themeSource =\n    electronStore?.get(ElectronStorageItem.themeSource) || config.themeSource\n\n  app.setName(config.name)\n  app.setAppUserModelId(config.name)\n  app.setAboutPanelOptions(AboutPanelOptions)\n\n  await installExtensions()\n\n  try {\n    await app.whenReady()\n\n    deepLink = process.argv?.[1] || deepLink\n\n    // deep linking\n    // register our application to handle custom protocol\n    if (process.defaultApp) {\n      if (deepLink) {\n        app.setAsDefaultProtocolClient(config.schema, process.execPath, [\n          path.resolve(deepLink),\n        ])\n      }\n    } else {\n      app.setAsDefaultProtocolClient(config.schema)\n    }\n\n    const splashWindow = await windowFactory(WindowType.Splash)\n\n    let parsedDeepLink\n\n    if (deepLink) {\n      parsedDeepLink = await deepLinkHandler(deepLink)\n    }\n\n    await windowFactory(WindowType.Main, splashWindow, { parsedDeepLink })\n\n    if (process.env.RI_DISABLE_AUTO_UPGRADE !== 'true') {\n      initAutoUpdateChecks(\n        process.env.RI_MANUAL_UPGRADES_LINK || process.env.RI_UPGRADES_LINK,\n        parseInt(process.env.RI_AUTO_UPDATE_INTERVAL ?? '', 10) ||\n          84 * 3600 * 1000,\n      )\n    }\n  } catch (err) {\n    log.error(wrapErrorMessageSensitiveData(err as Error))\n  }\n}\n\n// deep link open (darwin)\n// if app is not ready then we store url and continue in init function\napp.on('open-url', async (event, url) => {\n  event.preventDefault()\n\n  deepLink = url\n  if (app.isReady()) {\n    await deepLinkWindowHandler(await deepLinkHandler(url))\n  }\n})\n\nexport default init\n"
  },
  {
    "path": "redisinsight/desktop/config.json",
    "content": "{\n  \"build\": \"release\",\n  \"defaultPort\": 5530,\n  \"host\": \"localhost\",\n  \"debug\": false,\n  \"themeSource\": \"system\",\n  \"appName\": \"Redis Insight\",\n  \"schema\": \"redisinsight\",\n  \"mainWindow\": {\n    \"show\": false,\n    \"width\": 1300,\n    \"height\": 860,\n    \"minHeight\": 680,\n    \"minWidth\": 960,\n    \"webPreferences\": {\n      \"contextIsolation\": true,\n      \"nodeIntegration\": false,\n      \"nodeIntegrationInWorker\": false,\n      \"webSecurity\": true,\n      \"spellcheck\": true,\n      \"allowRunningInsecureContent\": false,\n      \"scrollBounce\": true\n    }\n  },\n  \"splashWindow\": {\n    \"width\": 500,\n    \"height\": 200,\n    \"transparent\": true,\n    \"frame\": false,\n    \"resizable\": false,\n    \"alwaysOnTop\": true,\n    \"title\": \"Redis Insight\",\n    \"webPreferences\": {\n      \"nodeIntegration\": false,\n      \"contextIsolation\": true\n    }\n  },\n  \"crashReporter\": false,\n  \"updater\": {\n    \"downloadUrl\": \"\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/index.ts",
    "content": "import { app } from 'electron'\nimport init from './app'\n\nconst gotTheLock =\n  app.requestSingleInstanceLock() || process.platform === 'darwin'\n\n// deep link open (win)\nif (!gotTheLock) {\n  // eslint-disable-next-line no-console\n  console.log(\"Didn't get the lock. Quiting...\")\n  app.quit()\n} else {\n  init()\n}\n"
  },
  {
    "path": "redisinsight/desktop/package.json",
    "content": "{\n  \"name\": \"redisinsight\",\n  \"version\": \"1.0.0\",\n  \"main\": \"dist/index.js\",\n  \"scripts\": {\n    \"dev\": \"cross-env ELECTRON_DEV=true ELECTRON_ENABLE_LOGGING=true ELECTRON_DEBUG_LOGGING=true ELECTRON_ENABLE_STACK_DUMPING=true NODE_ENV=development yarn build && yarn build:preload && yarn build:renderer && electron . --enable-logging --inspect=5858\",\n    \"build\": \"cross-env ELECTRON_DEV=true ELECTRON_ENABLE_LOGGING=true ELECTRON_DEBUG_LOGGING=true ELECTRON_ENABLE_STACK_DUMPING=true NODE_ENV=development vite build --config vite.main.config.ts\",\n    \"build:preload\": \"cross-env ELECTRON_DEV=true ELECTRON_ENABLE_LOGGING=true ELECTRON_DEBUG_LOGGING=true ELECTRON_ENABLE_STACK_DUMPING=true NODE_ENV=development vite build --config vite.preload.config.ts\",\n    \"build:renderer\": \"cross-env ELECTRON_DEV=true ELECTRON_ENABLE_LOGGING=true ELECTRON_DEBUG_LOGGING=true ELECTRON_ENABLE_STACK_DUMPING=true NODE_ENV=development vite build --config vite.renderer.config.ts\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/preload.ts",
    "content": "import { contextBridge, ipcRenderer } from 'electron'\nimport { configRenderer as config } from 'desktopSrc/config/configRenderer'\nimport { IpcInvokeEvent, IpcOnEvent } from 'uiSrc/electron/constants'\nimport { WindowApp } from 'uiSrc/types'\n\nconst ipcHandler = {\n  invoke: (channel: IpcInvokeEvent, data?: any) => {\n    // whitelist channels\n    if (Object.values(IpcInvokeEvent).includes(channel)) {\n      return ipcRenderer.invoke(channel, data)\n    }\n\n    return new Error('channel is not allowed')\n  },\n}\n\ncontextBridge.exposeInMainWorld('app', {\n  // Send data from main to render\n  sendWindowId: (windowId: any) => {\n    ipcRenderer.on(IpcOnEvent.sendWindowId, windowId)\n  },\n  cloudOauthCallback: (connected: any) => {\n    ipcRenderer.on(IpcOnEvent.cloudOauthCallback, connected)\n  },\n  azureOauthCallback: (callback: any) => {\n    ipcRenderer.on(IpcOnEvent.azureOauthCallback, callback)\n  },\n  deepLinkAction: (parsedDeepLink: any) => {\n    ipcRenderer.on(IpcOnEvent.deepLinkAction, parsedDeepLink)\n  },\n  updateAvailable: (updateInfo: any) => {\n    ipcRenderer.on(IpcOnEvent.appUpdateAvailable, updateInfo)\n  },\n  ipc: ipcHandler,\n  config: {\n    apiPort: config.apiPort,\n  },\n} as WindowApp)\n\nexport type IPCHandler = typeof ipcHandler\n"
  },
  {
    "path": "redisinsight/desktop/splash.html",
    "content": "<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <style>\n      body {\n        margin: 0;\n        font:\n          normal normal normal 11px/14px 'Graphik',\n          sans-serif;\n      }\n\n      #app {\n        width: 100%;\n        height: 100%;\n        background-size: cover;\n        color: #ffffff;\n        background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhwAAADIBAMAAABR4CVfAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAbUExURQkaIwseKQ0jLRUyQBAoNBEqNhMtOg8mMf////WmRZkAAAABYktHRAiG3pV6AAAAB3RJTUUH6AQDCxwBTNO4SwAAAAFvck5UAc+id5oAACnHSURBVHja7V3Ne5u4uxU4NlsSpsnWNdNmC5aBbaa+027dcu9064Spu3VCW//79z3vK4Hkj8T9yvO7z7WeiccFWUgHJKSjcySlTuEUTuFnwvmxEYPJo6fDo694Efdpxs7PLsb7v393+MHfBlOlzvTeU7XWRYxPnaumVEGlgkYXhxKq6U+v951JtC7H+NSlSjWlGKtW6y6/bUY/G9BJ+nGq+fuNSsfqUu9P7onQMtCD6umYyR7IAMfwABybL02mrjfl5l41NeBIZpN6+RgcN3sv+3lTVyraNB8fVNreEByRvm+n9vQ800vK/qYuVKAf0koNVhnBQd9fz34cjuLpmIfgCO67f7rVhgp4BpCRcpPGBMd8rc6mBxIHHNfdv9xqkyyonCaj6euM4BjkcsTkKonVIKMU4lFOkKrB55zgGNL3Jv5ROML10XAEcVfwYBx0xQtwtv3gFTAoLByDBcHRmH+Z0KdDn7UH6aDooSc41Dw2cFznVOx0bbNNYRQniuFI1wOK2Y4HtxXBMVjSL7tCBQeAkeOcdRU4cDhF8u6xSYf/BzginTdUf6kVIPTDRv81pQqLZ4CO3Cr1QvflqE3hGY5RRnCU9K00Z/WsRd1ouTWZ67ymJojveIvkkLILR2vheFkRHPzNXieKzywcgClZD5bUxowTShzwSBy0IyG1PvaZGi35LB2/4bPvFbU1RbI+kzhzlE6OB1Wqu1tOpy8XBIHWccgRg2/6YZ6ppLomUNLqqpiqqy+AYzC9wLWCumsACI6wqyxRTnC4FXKjp68rdVa+pNZkVE7qRoUbPD3h7GVNpQpec44sHI25b+m4NHD4AXDMx4AIcMwpXrp2TjdvXlFDvtls7DN4ljOgzYdXdHvmd39obnfqNeXh3YZq7TdcvL17oeOgqK60vR7iLwmC+3kWbOYfARm1armiCPQ00meCtgOFpsSlMr3KLByTSZtbOILZFhz4saY7CsjoM+LWgf6GmRpxChd3Fo67CTeKAsd8PxzTySutLBzJ2Icjmkp1owfEPvQztCy4W+0an8ma25q1rSxURK7pyTJAIU1aiDlfGgikuPQluEWLEeXhVK7EcNDDKZXv1dLCQS+/2MJBgBAcwWr1r80j3Za/TYXCvbdwdG1t1MGh5ZUpcCRrwPHPajVWTqAXLdVVC8dgATi+rVZrp2pwErb24HIl4z5Y4nO0sG1NDweeoGEGUGylk/gCQQ+HkmqYh7l5s1Rc0Vao7eG8ryzn55y0wNGeExwh+g4m8KPScG9iJVWL4Qh0yc3xNzRFAsetpCNwDJaAo3E6HgxHfh6rDo5RBjjmWi96sPB92Pcm2phuJoo5zAZ8/wCXDwcKH6KO93AsLRycrx6OgurhvQeHuvqbHqs/5IEQOJxyNyq5l8qyBQel88DHLBwq/NLQFRvdvb2ThS0BCjzMCI6x05RKgTN82qY0yv2mdPCZLjOm1r7/TXJDb2UXjuRIOEZ74BAAUFlCC8c157u967Pow4HXH5piHw7EibmyNAaOaIyndDCL+5x7cIS5vGjne+BAtvkVO5MXbWrhWPJrNM37H4wyijzg4qHYZzcc34PjLOPKUu2rLD4ceLypzdNowwwc1FTRr2Iniz4cZ3lFzZaKfDjoflCr1jpNKSVCT13gpOPDoWbUDcuoTnlwSOvL3bCYMtykY3yvzS+R53YclE7u8ABxw2ea0sVotymdcVPqwBGh6fXgCL/NHvB1OqHfttNJiRdtSU9iMr3wb5iBw3TSCTPqPheT2sJBh+lzWODtQ8fbBt1winlWvEwzLx2Bw3TS6eWITjrF3/N0oGNe4f61NXfSG1MMpSfXJb0XqMp0iMy4/X5zrfH6pF5DoCevG2qu2ndUSzffZhSz5uMOHBTzTz01ENDggW6A5n4TtYkVf76fYjxFo4aAj+zCYYZwdGkewuUrOUnR+W0+R2NDnZVpzQ1ebo7swmGGcGO6HIZwxb62wwzhlhQZY77SPlfUO6QccvvfJd0UcvyWO1d4UegSY4oExUDMMbpht8qDg+JflQYCb6gWSCc93j7yeNgTh98a3vHz+OmEDsVxjx/67mUo7rO1G2c3u8fk7RRO4RRO4ekQvHS+M/d5/qPM4o+H41nVQ0zHLwr09pnSa3KKzkiI73TkGGaRQ62PjHgYBxnhHeQYnSA9HLcT0xx9FZ0fFW8NjoP6JrMCcOD7ml7bg/KoH9OQ+VfB0WRPR5VMtU7BjoZjVB0HR8YcB/VcG8t6UL6mwXE/VsP8V8FxFR8LRzj+ATjAQR4T8nAGIiCo2jjKw0L4jtsg24pmKqzpxcT2Y7B04Qj6mIH8/2LnehdOOucOHMo57rYO587P3EdW4uzCYS4f9xnijNZXLhxOj/OcI3VXzDF8CadBNbgR+gd/92phf/mJwFoa3pRaCrQyVQvyhnmoNO7hSJkLCMC2BpXOm4p5U3sdcFeNPfKau+1znX9CL1xzmzVW5vjovTOf0uppS3FeU0cbXX7ubt8o8KD0IFPnWtuphihDbtA9H3NzuKCzH7jvzfReEXVwGK50qMvLxei/9UZnIHJvt+AYZRHTP1uh5JGu8KZJhWHeF32HgXWE0YJWDhxvwZJSnGIa6Gv9VxNH5cvaPmYY6ZYKR3KFWZVcjXj4d/W1oGFesKnHQBizLYPmIe0INIpfq6v600TH0UZvMBhjAqy5TygdOvLgwzG/fwHe8O4FXavVTNImFSq1A4dwpc2HV81yNEvKV5U0n3xyYeGI8kfgoMLQf3UMyiDQMr3wjs87cGTgNSjOYBqAHkxj0Iv2FUVNEj13mGEpVZLhM6U2u+wqCwZwMttCb7WOQ6GYeArTJahKU1l4+EXJzrzKYuBY4XnGAJ9SpxE+Ez43fL6Hg7lSYUnxDCimbZKlScfAAUDC/Gy1cvie/um4RRUrmAkBa9Dxoy4cYxStkNQYDlQQM94FUpQ3HJkzA/GNi+LDwX/gPrpiplLFULaxBwdVqngfHA2OU4YRCzRXV+ndpwNnDau6pCegYoo48+FQJeAYau3Pqgkcwptqrr2B1ydx4KCGA+nIJ+ConXkQzGAuOPt0nI+sduAATKACe6IJ9NG84w0dONLy371wJPrTmNsUahFGSyefbtvBcCwdOLQwERxvauCY903pDhxUxamJKsGnbsHRZ2gPHBR/0+MGpudXwRFc19U+OPj4CLTQ+nE4Bi4cM47fnR2CT002OepB5MOxYp7uijkbtBjx40+HX1mU6snFedxw0cysyqv4QGUZe3DEUlm24QhuVMfH9nC0MY7XzIkG6omnI0N7IXAEbpHRjoBPxcQwumE+HDW3Z5QtSpw+MSNh4HjYC0fDTWnVNaXdnHtyX8jcTclsmLY8q9uULnmC2oFjIJPbO3CgcD4cIbOqyFvLOaQrGTik27YNB7OqBg4kLNws7sD0Wo8pznCWq7a6Lnw4krvrFV605/M1ZsbaZfCFOVFq8WPwpv2rTuBIppNP5bnAcVZM+h714A5cNh+JyslreqHOJnOqgUGxoTZ4U398oBZqIsxoBwfi1zdX9Ud+lDU90cGm/UzX1y+vmMDedLMV9Ft6qzVvrqhszIlGbcUn8aINwJWuXTiUfvNKTw0c6d2kY2plCFfxKIcalVsfDjr7Ai9X9HdY3BKa5pHhkEkmFw6KuajvBQ7qRPUsaaSBvxxhBjSodd4ukcaS+kRMV6boRLlwoBuWZK3m39I/MjtNfSkzUGk/Mkt0RVcc9lxpYprHAQ9K+8bSwHGpy6vSwBG6tLBLIO6SiS756J2N1d7wBEsqR3b51IPx91/lYMzdjB4iBNwUjqGFT+EUTuEUvjM8v67058N3cKjj70j19+tKvZD4U9U/EuS1nBzBd6UMmcsE+1Oh++H4zbpSP1r7q+CYPh3TwOHQasfA8Rt0pQcvVzyVoWPhOEY5KnA4xdsHh9Q6ifm7dKX288I5BJYyzN0MOQpXJYzGbovgsqEXDhzuWbePGDgn0ng7pnf18bPpSs3na7Q+c51wjxkty2DZZ2j0F7Okrc7fqnlaQK5+2UvGMPBL1+aI6Edfo2M+kg4++vrmeJi3fef9UheD9Ujf0uggkg5+azWmCyOJQLd9dIN/PJuulD7nGMK+pCHcdfP5BUBEAdJxD8fgn4c05xQaVb9p7tqFaNINWBnIARHAKA39KLjVOqarYBi5aTMlOlEVFncvOtqJUmjWYVve18uABoobcDeAI/1Ag0A5YkYxyR0etufRlQ75P3ClNBasx+BU1autx5UGW1QZE2iH6TyIoGHeD+IxJNcKR+oYisF2zNxqbCsL86KiK9VGdYqrT1kSZXhTqSxMCbeSO1zdwCEnn0dXKqxSKpmn/153tdiBI2OBZozf1nQvRktwG52PgG77jNmOAcvgRqwlnHtwCAOKYthfGXUpnQBv6sKx7q5un44bA8dz6Er5s5Xa67Z9LhxLnKqVfAKORERMEho8GpBGDpaiGkbhUg8OEVKGuQPHghWmht1x4BiBVfXgONOrDo7frSuVz1bY00NwLCwcjYXjoyOGa8EbopgdHOOfgENdvdU+HHTEwvHbdaX0iSnxmN+Bj8Ih83Wmstw4EekltFY4kjDxO1pbEa0DB44vPTiWMi+xA8eDxOGrz/jaMt54Hl0pZSsUVXDZJSTT1NtwJHjlGTigDZ3b06OM2l8caeMh60pZjerBYXSlDhzQodf74CgdOEoWt3Ob/Uy60sh8Tl7nig5ztsDvXeFVF7twUArfmrXAEejJq458DqclWiIcCVg/GupJ2rzH4IEnK6uN0ZW6cEBdWt9Z3jT9iJhfoTFtP1zAO5V8fEBf87peoMDPpytt+TRbAI1hgUc0qaMNFTjQhUsygQOEZ19feEZMDIKiH6X+4iAXxjSQdPi4CwerS4uBmU0beBpTHBrhVTBkxWrYzZGpZ9CVup/m+IFEj2Fb3e68l6ED8R/PuknzxJiewimcwi8MTzCgx7c4k7j7Gro/cxz33vfvzujxvx0fGY9eO3fy9skCXRl16UFdaWGmI3dDykxqyhqQmo16nnVD/4XhMrvv5aXL7nvlue+/I8jrPT2CH9314Lsk0C4cm6/0Jt98LTbrYFaKuvTiIOdbGFXYHjg+fqlz6lZBwVVTl7ZUI1aC2cvcwpIi7ntmPVLqutZGJ/bDcBzBj+568B+Fw8zmy1ildtSlh+BQfVVyqw2mrK1eq6aedgnnWtgLGuJ/pAuvrTYsXRIc3NH+YThcjenjcLhM6lNwiCCKNYZxlAeFJ3oR37rlL3uQcSS9i72LaAvHZaZKpnj6hQbUV3EuzmOMExpx37N5b94l8oSzPu5zsto5u48f7eFwY3pw8CHHuV/KmEbgYHVpZUhAgKqnDUQYUJSiS/6+oI4uIKHvPCFTdvp+D46rnNLlb/b0EA+VwMGCuZjd92IPtNljteiZoxZFfFm5Y8wd8zV3zIUfha2aVQ1D9uPn886nS/Fn1MVnXem2B3/tWcbEfW90pdJJR5+9g4PVpU6FDL6WH6i9EkUpfZYFDcwwaBtW50iTcFr2cASFheO8MnD4AcXT/NS0Mbvv/XunH6gdCTebjX0GmR+LhTGlYdsryEvu501sFKUb6EPAmxbMj5Z9Omk9Fl2p58FPP/xRGX7UxGT3vdGVttUVUptM4McUOFhd6rZPYYEWoWZ1IsbXlvCBaRW3Rr2wDWH6RphUhiMu98PxAfoeC8d8C45BbirXmX0NoBaXPKhveFA/jy0/KpUFeidEno9D8AYmLcSkx8nwo71x1PCSbmUxRDHrSqVeazOkZThYXTp11KWoNXesKGUZXNDBYZ/MPzs4DI8ncMwBR7BauXXcuu8tHMkYcPyzWsX9s8OZh/1V7t7MiHOhKKTPAfPutQeHaACRfcuciBnZ6kodOCw/6sABolh0peYpKM87qbxRl06HfcdDIrGiNLCTCoAj1Ks3ipnUrrLcy7hQ4EjGgENr/62RvEccC8dgATiaribbwb4z29rgHrOMbtHzo60Pxw1ejWHew8F287GVyjlwDAw/6sCRKUsUGzFd2Zcb04xXoi714WBFqQeHukBv5YrXK1Ie5gLHYFFuaftsRu3KLul4lPlNaSrMqNvNa2GxNnCMfxoOdf223AcH60r3wmHUpVtwiKJ05lSWP7i3kn7ui+rDMcrkRbsPDkxS04t2HOXyorV13qyq0S9CgdkNTPegeDdc7DFA2VtZ8n2VZQuOB+W/yQwcMg9rK4sHh1WX+nDMWVHaUINq4cDlMs9Z78MR5qVhPXfhoAIEpbjvleU7uXAZi2TdAcIoa8SJL8siMZm+1ZSKH9+FY2Sa0h04yn4ywoXD9ERLBw7ppIu6NK0mK5OngA4rZlLnC+ZTC+6GQ2N67t7FDg7TSadHqYRFo/VIcwPHmcY7iN33it33JnsB60pHnx3pdjQt7HF6ff5ZSvz3mMBgjanwo3TchYP50VvLjzoe/PbDBaBMP1otbPiN44iutJ1OOjhkCIeLirrUwBFJY0idrhnTobcFN3gZjmz55QQOM4SLoYehkVzV7oHDDOHgvsf3WXe3WG860m4DzIRmwh2wSy1T4syPtpJPxHwh09QOJ5/o4qyyulLfgy8tqu3XjOSdIWPWSB8YLexRl+7KNY8Z5h+jCj3Gre+xrY+m9ng6hzK9SxSfwimcwin8R4bf4bL/jpjOQgNjhzj1FiB4+XQ6h4WjR+Mgvd8jNKbGOURD7i4cvjr3PffyingBv5fX8Bq+Eeq6yfI3EkZlwkYTUMQ8NQnryjA//KJ1k65+GRyLY+Fw/fhPwLGXKw2Lzd865k5aHFWsHphjtUEPjpIp4nZ6peNwNgMcKfMdj4dR/qvgOMJlb+BwYz4OR8+VutWGxVBLI3HIZxjRFP3yXCrI4Rsv2PPGTEdYsbDIY8S9pTx7pSlb2PpI/clA/rGnU8SfoviMHTiUc3yPy/68h8ON6cERupfvVnfiI2HZa2cBh1WKRXlDcJzljtstyMIFw3GWS8ywgmLEsNwQVfJs/nvDgIZQmhI4ZyxpOu8zNL9EV51iPlA6OteVVYtyMEZQ0ZWy155SW1Qsj4ATl8Hn48mDu7IxnPitvqT62/nx1wpd8pLnZboLpPoVvg9Nhx0SBOFKR5zaXHc0tg8HOAJ8G3Z3ZRGMGY6IOQ6Qw1i/0gzwKx5iJncXJbvsqdJ91W+wZsgIUqci6OGo/2dS83ofZR7qa/2g2UdvbxKNHMOKFaVLaEPnzLAWlbr6OuPhFobl4sFP6n5uBfFX6qqp/tQss4Effw44NMtjWFoj4WoOJ77SD694OIcl1YQrre8vkdof3SDKryyyhOP2M4xiseWcHt6wgmTTg4OGUeLZpl+CH4W98C/7OwvHAmcxmYanrFRz9tHbgtFwHu3RGvxjypbRmgbZVVdZMFAzHvzefWqd+O0N4DRHMdDDunxTv7IkcOIzYyrq0oWxyhYmVmAnKMJico0HRuAYLPfCUU4mzY2F4yzbfjpuTN2lfFBZg04h58IRK5l2OIPkslAtcxt2EmDEbzRhwDBk/4SqE/hwiAcfjVrRp8lVLFaXHhzWgOvCQaNdZkyFN00sHE1s2ihLQ4emigkcAGSUjVarW+WEgKcmLBxBBTiGqw6OM726N6Sh3/a5cPBvc/MJOLThnREipjAKjifkzExSd+AQ0zmKax9sHFlZjsaBQ7XisnfhoBsuK5WyZnBg4UjLf3Hmuq8sM6F/BY6gAhyJ9lf/CZgitnCoEnCgdTBwQGYZi7r0O+AoWUVqIvL8g8BR/jwcwZfaCr49OEAOxR4c7LsPWt0RlXZmROBQ5XZT2hfLNqWqJjimNtMEB0+Rmew8Cod49g0ceApie7pBGU1lURBkl8K5upWFq4cLB9iycg8c4bo3i3hwLO1L1laW4AZXDHsOexuOuXnR7oED2RsBjvRFbi5e8pKdDVPSDatLTf7FBbMNBwsyLRx4m3Qdw/aCVZ1oiVMuDLUUw62mVDz4DhypKFN34MC93AcHM6PxyGlK8aPGm6HdgiNJlyhEugcOZbphavA/uZl2aj9cN0uIKhvYFehVGn4tuAJwVdrQy89WB4GjnV6XxYXAMaom7JvgMPgb94B1pfLJfvwYvD21wZv63b3x4LtwUMy5Xl8371BW6jkTmpv55wdq6l6+QqlKa52nFy078XmlUqhL9cJwpc39ld93FThMJ5366PTmbz5cl/vgwAIEWD97VPACBIqnnZOloQtZSzo0snvWDbuNpcCBeV9t4AhqR9AyQqfIHBF1qdbLOSQMsoUEaE/24LtwsBM/a8yiznPqsA3F15+a1bK1hds48cV9z2uX9lzp7R44zBCO7o2Y/Df74JAhXIW5azuEc0WVR7nsnePBoU760S77g5zrMbzp4zEPZfrQ5U+60lM4hVP4peE5PfhH7+rknX2u8Fs9+Du7Ovm60rnd1Wnc7eq0ltXAWBWWH7OoKULUvZ1/CRy/zYPf7er07qHTlb7sdaV1Tn0K2dUplF2dalAP0HrNsFDpMYuaIgw+/VI4fpsHX3SlRmWxqytNxinbeWnMbnZ1+u+M4BBtmBotkvi4MqSd9feHEOCr8Mdv9OAbOPxdnXxd6SBOjZlvzBsejQfvK6xCuORfDteDbTgOrE6qhy4c5/3Jc6Fkx/sRwMnn8uDvwrGrKz2L/7BwmK1IFtTG8P4cMGeNhzads6UwRtaPj1bmHatRE9aE9nCEU7bcw4l/M/pb/wnjfQNamAMG9TTaFV0pFfjZPPg9HF1l2dGVmkdkCRLAbFSzqAHHzt1EcemXwpI20JVGbTnh9pZpYweOsjL7PDWLQZUWr2iodtft7QTXK1XXlnWlaYXX5LPt7aSsrtTZ1WkfHO6uTgsC4zAczJLyyqMxRKkAMlrb8wYO6GfH4BkIjuVZHkwBQTfnggUNb3i2JTPzLM+2t5Pa2dVpS1dq4HB3dVowION+VycPDl61lO3EN2CEhgv3vIGDt73iBRcprSgLpqBfuvN4xOKwK7x6vr2dGI733q5OW7pSA8ets6vTYpQBjn5XJw8OZknNPk+DxdZ5A8cUpnOW6Bo4IlnN1FxsQXcRerlwapj0Z/LgCxzr7qaoXV2pySFHRvOJXQOjXLjV7vF24WCW1OzzdDwczuqkPAW3B47f7sHfB8e2rtSFw7xoF8FMXrR2V6e+uHUc3LAsecmv0cfhWPqVpROARjmm7oUNdyrLb/fg74NjW1fqwmG6YQv097gb5sERsVAfBeDV7pGWgUMmsbfhYCe+gQP5HCy7TOOrvF+4IM+6t5PAYTrpO7pSFw6zqxM9ni0vcNPNfSo5O0npmdLMkjbw3V+1nzltvGjDzbfKVgeBg1czvTNwtB8m/RUbXlV4OtFGV/qsezsJHM6uTr6u1IHDDOEWFBkuwtJvO+jIVLphS8OVplZFmvOsdtdYGjgSXV4VBo7IXd4aA0kW0qpn9uAfCkdpT+P9v9rlTX9sddI9RPEpnMIpnMLPhh9rU46nMn+HxvQJi/H46RR2daXWcPJ9OEivJzjCTL8rqgynByOzVTDbe0FWjvILNcacY4KFvjsWYzugE769W1XgLD1wWFcqcAx/OxzLY+FwJbfOBWebt5q6W5iUHs148T5ehfAROLYobqMr3XwrNuvDulKBw13Q83g4jliddI/G9Ak4oi6mW214oay1WScsh7WUWY/1YTi2VzMVXanR//i6UqcDlri/chWiaue4/GrswOGedTPvXMZT0nSG1a1Ddi3SPjM4HPX+doaDR6mAI8OuDXZNOVvaPnNBbPdecXMlulILh6MrDRvZC05fLgPxSHVcKR1fXXaGeF67gzvmsv7ojH/7kDPHsZAlpwxXml71JIaoS5sSa5S23G3vNabKaEkliLrUMKCv9SxZB47GlDr+HW/iw9GYdebsshIj/VEXZtd7fF5nrge/hyPq4Oh0pUwRUiRsh5SpzfzzJjZcacLHm0+TLomG8wE//g0P3pbMrebqmkZMY8OMiq40/cfZ1amalJVZoxQ7OVmNaVC+bJdmbycJV/XsWnhNaH0f2prqNhujtQwCX3R3BnCkNxaONBY4usrRaghgW3bl13cvisz14PdwjLrK0ulKmR8dZRBtoxE3bQd+xMfh/+7W8mj5O3Z4ylFPo6loTLvKwv520ZVm/Q4gWOKu4jVKG08YFeXdQhk2pDSA5U2aG9nnaS37lWO6TwQ9/1g4qgmYbgPH4MaHg3nTTGhB3ucp8z34nBTrSi0cna5UPNW3qEYDDw7z18iO2HKVJTIsUwuoGJ/tE+fAIcQPtGH2PqwkrZqXa3XgMM5lDw4apI55kQpmwFIDhyqscvSthUOL4kfgGGWA43K1WvQZPVuwPI5XLbRPgfXgcxHNshgCR6crNZyYlXD3cITCmTf9LlCIUPPCwATGzBYl9OEQIWUa93DgSNUtV93DQc3Jh3gHDuvEZ2GdhcNoTP/sJoSHU1GOChxRDjhShwHFoyI7PPEaDhYO48GXp0NSEDg6Xen3wBGCDeRirhwy8YfhCDDjuwcOduJ7cFBjQ3Vnrjui0mpEBY6g8ptSAwfv8MSbwFs4jAefi2iQNXBYXanIZm/xi0OVpX9PlXgZuZWlOlRZHDjKrrL4cERjbhF24DAUkVNZApZlRo5y1IdDleZF68EhOzwNM6eyPPRL32zBYXWlTBGOeBmC3aZ0mPtwNEOZPCYg5/yp1XZTanSlDhxGXboDB9z01R44xImPotmmFFleeX7/LTjmyYJ3tPfg4F3vb3ADu6a07Fn7LTisrpQpQnp9QHhZscZZdVwpH3fhSNIls6r1Ajsz0dXT6aQsUFxWjsIXL4pSFw5Wl47NGqUhdnKKNvXHe6xg+hqkNo5IuK6hLoVmtOJ9npo1dnJijemfft9V4DCddOqjz7mTbtsO477nHZ64YJnvwXfgkE66qysVlpTaakzJnsm66j1X6sExQP9IumGiH4UVByslaVkiDeuG8nEXDjj324VdwplniMRRYIQuTTeAqWVyyYhbdJGsI9GYequW9nCYIVxGCfIQ7tJk1Mjxedd7RKJumOfBd+CQIZynKz1kQH/csK4O/GqXHHgm5eihXe+Pp39PXOkpnMIp/AeHX8uMfgd76vrunZ/t+u7Pj6BSf959Hx3vvpd+RccKKO7Y7wQ2mmf7Eogc3/1QfPdDx9gxqJLc8d3n7LsfZWaC8kwP9l3Nx/bn3fcWjptj4XCFo4fg2L+8aeT47rGcE0yb/S4talCmxndP/c67P+C7rwCH6MRGejBTT4TBz7vvDRxHcKgGDtd9fwiO/b77yBJCGTpplfHdY9QjUfPhQjZmtb77aYnBn6w2GOTRss+KpB87l8BH+tKF46I/ebz7Psq3Uzjovp/uxKycSId8930jgEvZUe0wL5VsddMN6oIsWpudiCPx3U/n/CmMaR4aOG56931uVyfddd+/ZcY06tz30233Pfq8zJ527vv3lZpzZ/9sy33f4TEX933iuu9xZy45JpvFUn2Jawl7SscxgcBVj1c2pV5957v34Zgb333vrr6hh0rgQPGxGAEEdKL7obOBeYwWnfv+Wtz3GBbuuu8/YTiXVn/Aff8nu+9nk/rGlov9kqPCuO/rzLjvr3lP3HADcUnvvrc5HLFU5qqB7vPqSyHu+zHGOL37/qr+F4sCsPRFtKFqw6u3Nfe8TuOLTqiHgkEDInDAd7+7PjHvw8KaU+gEB7Ltux94S6M2NrpSqm8spNxx3zNLqjvLaL3tvh917nt+JrGyadVVFpY+HXTfr1n0JpUFQla473NbWehnYLnN0pQsieIKUxmze2C5z2hm2FOGA777PXAUE7qrFo5hthcO8TSztJKzcsB9bxa7sHBA82Nb40HnvpdVKD9ZQ7EDh2xC47rvzdbexn3fw2Hd9wLHWI3GssOTtRIzHKVlT+3uLZE4MQ0cwwxwXK5WSw8OHklbOMIccGx583lLo5dHuO8j5s0iC8ch931pixL4cPR2c/sru94HF8uBQ83Lz3EHB77xDk9r5sYsHKllT7vKUhn2lOEIpoAj0b6vJSgQx8KhZoBj4HvzF7LJ0dPu+y04ttz3jee+D34KDuO+d+AQ3/3YgUN89/Ped28fe4EDgkC/KbXZEq6E4WikskQ+HEe670WlPHQrS2xPNyijuBdQ7P8y83UuHLKflwtHX1l8OMK11JceDtnhSXZ1EjiCm23f/RYcDb1oM7W13LsU0r5oVXvJczFDH46GGdLVU+77yHXf1/vc92M0pS3TvM1OU9q7722qree+7+Gw7vsejqHnuwccUb69MOwWHC11tGbbcoh+0QF0w1TyLZMieXCk1n3f7Lrv7XShwNHeXZczA8dg6rjvk7e4E8yeeu77SNz3zbt7ozd14RjAfT/u3feY1px/9N33VzV894Ge/DnjVUjZdw86V7/c8t0LHKaTTn30Ofvui31wzO+uWaI7KjNeUMCHo1OU9u577ndoZxkigaN339f73ffcfDzlvrfFYPf90rrvazHKm1lsVAL48Y3vXrhS13d/uaXZN3CYIZz47l8wS7oLB3Oo4ZQNhPT9zdbL9v+A+/5Xsae7Fz6tUXoKp3AKvy2cPPhdlk4e/G04ntWDf3/y4J88+BaOkwf/5ME/efBPHvz/sx78Tip78uCfPPgnD/4WHMuTB9+B4+TBX7twnDz4mQfH/ysP/hHc58mDfwqncAq/N3gyS+Y+j5FZPlfm4l8f89HS8XsnoL+I1VnsqX1aZmlC/SO7lB8XJAv7PfhbmeBPd+q5PRiXNWT7S4fZzrVs5BzMClat0Pc1VKelOioExe+GY78Hfy8czsj5CTj2l05vvirZb2UZVI1lPej60yBXR4Vh/rvh6D34T8Hhuuwfh8OhgsfOKYDE1vOzPKjamIb8hfAdt0G2lUpPJiLE9mOwdOEI+piB/P9iJwk5G+9UdNe5f+7A4f7W5TgvnEvWOzE9OHw/fu7FbBwqmOEQp3NQDW6EAcHfPTRSEj4xMSm8KbUUaGWqFhQOb7ybxj0cKXMBAdjWoNJ5UxktqYTk2ihBb/9Vn+ZVa59X7FxMg8XX3JEX7rPV+VujK+08+HT87Lbp5ylSXdHAX29ooDASGYR4yM44pu5ZmJH+4Pvxc967lb8v4PTvqWAPDgilwt06UvLwUHjTpIK1/Yu+w2YWEUqnlQPHW7CkUJROA32t/2riqHSc+HMoSmlotyqUfqPvapMLjFA0drdvcyX73YNtbeyuTuzBl+PD4sPrbg9lik/pgx8dhzRQfEBMXGp+/2IKjrbb4DdsKTnwFNCnY6GBXF19RTGTDyyYuWi6WS/AsbBwYNefg3DQ0CvhDdvPeFqEpxfe8XkHjgy8BsUZTCGHgjDuxvFL5oZDHcKD28Z2qC9bSSWc7CA3YstId5UFxlGZZxlWvQCKHfrczmFcX9uUmKf45FeWEZwJvFrHmneHMzpjROFYwWuTExr1ToyRlOAAIGF+tlo5fE//dNyiBhayOgA1Nh0/6sIxRkUrJDWGA1MIKwvHwnCogQ8HNVVD2dq9ZYr4ddzGkmoPB34rYls7I2NmZwiLkQ+H2efJhSNTEd/zUWY3rQk9P/63Dg6MaC0cqgQcQ3fw28MhvKnIo7y97Fw4IJZlCaWFo3a2YRYZFJ5HHw76B5j92EzBKasxdOGQjd8NIWivhb+BJOLAkYjL3oWDaqMZ8ncLVACOkcSMnMrCuz1NDRzzvindgYPqG/2qZO2pD0ev39sDx6bfpBtzFHvh4OUctuFofhQOZk/3wNH58Ts4WE6LRVNip6jKyPgx/bxh63nkw7Hi61zxE4sWI3786fAri+pVZgLHbmWhstaxKZ5UFpFre5UFfzcuHDhS74EjuHEoawcOkMMBTyd0leWB89T85d15KVCCCcbBZ+mG+XDUPCND2aBk25hJSgPHw144Gm5Kq64ptbp1gYNS2GpK6dVS2vcLeoA1fhXuNqV17MIB/77eB0clUGzDAfl8smBXvoWj5DzFfUQDB7Y3GlM6sEy11XXhw5HcXa/woj2fr1lLugy+MCfK01vqeqM7KbbAwYrSc4FDdnJy4RjMJv/osQfHaMqjpMlrLHCDObphMfnWrEVXaj34fNyBgxnW95YfreWVzPs8vblCqdp3tuNp/Pjth4n48anPsflq/Pg+R2vgkCFcxWY5kIY+HHT2Bd6CmA9mcUtomkeGo3E6PAIHxVzU9wKHaEldONCda5ceHAF3ilLugImidK5zLGE1F9mn8eCvPTgwDT7I7QImqRbbH9aUEDtg0r0QBvJuEPf9Ge/w1Pnxt6QYpiCPb29k+sfHa08fZUmf0JJ+x073B9I5fqf7EzP6ZPhfdo2iV9Ih7EgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjQtMDQtMDNUMTE6MjY6MzIrMDA6MDCcZqtdAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI0LTA0LTAzVDExOjI2OjMyKzAwOjAw7TsT4QAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNC0wNC0wM1QxMToyODowMSswMDowMBuAH/MAAAAASUVORK5CYII=');\n      }\n\n      #Group_4 {\n        fill: #ffffff;\n      }\n\n      .copyright {\n        color: #b5b6c0;\n        font-size: 11px;\n      }\n\n      .container {\n        width: 400px;\n        text-align: center;\n        margin: 0 auto;\n      }\n\n      .logo {\n        display: flex;\n        padding-top: 73px;\n        margin-left: 106px;\n        padding-bottom: 28px;\n        width: 177px;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"app\">\n      <div class=\"container\">\n        <div class=\"logo\">\n          <svg\n            viewBox=\"0 0 172 64\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <g clip-path=\"url(#clip0_96_147)\">\n              <path\n                d=\"M140.162 12.4835C141.779 9.63636 144.425 6.57025 145.38 5.62121C149.79 7.44628 153.905 11.1694 153.317 12.1185C151.627 14.8926 149.055 18.0317 148.099 18.9807C143.69 17.1556 139.575 13.5055 140.162 12.4835ZM171.249 25.0399C170.734 27.522 167.648 30.2961 166.325 30.7342C165.223 28.3981 163.973 27.011 162.797 27.011C161.328 27.011 161.254 28.0331 161.254 29.3471C161.254 31.6832 162.944 36.7934 162.944 42.1226C162.944 47.9628 158.829 52.27 152.509 52.27C146.721 52.27 143.523 48.5014 142.097 42.4807C138.316 49.2103 132.788 52.27 128.551 52.27C121.927 52.27 120.368 47.4064 120.525 42.4733C117.863 47.146 112.74 52.27 107.827 52.27C102.811 52.27 101.039 47.9332 101.446 42.8827C98.4405 48.4432 93.0044 52.27 87.7639 52.27C82.0772 52.27 79.2624 47.7824 80.1733 42.22C76.3476 46.8911 69.2259 52.27 61.8219 52.27C53.3797 52.27 49.7049 47.7478 49.2693 42.0811C45.1947 48.5683 39.7026 52.489 33.1607 52.489C23.7171 52.489 20.3393 44.147 19.8478 37.3234C16.3479 41.9791 12.413 46.8094 7.58606 52.197C7.07163 52.708 6.63068 53 6.11625 53C4.42598 53 0.971936 45.5537 0.751465 42.7796C2.71173 39.7579 18.7228 22.5935 25.396 15.2013C20.8879 16.5526 16.24 19.2475 10.3787 23.4339C9.34982 24.1639 6.4837 17.5207 6.55719 12.4105C13.3183 7.44628 23.6069 4.30716 31.9113 4.30716C43.5228 4.30716 50.2104 10.7314 50.2104 19.6377C50.2104 27.084 43.9637 35.2603 34.8509 35.5523C30.1126 35.6746 27.0758 33.0324 25.5225 29.7696C25.7081 34.8158 28.3493 41.0275 35.4389 41.0275C43.6698 41.0275 47.3443 35.7714 53.5175 28.1061C58.2208 22.3388 63.6591 17.2286 71.5961 17.2286C76.4464 17.2286 79.7535 20.2218 79.7535 24.7479C79.7535 30.2231 73.2863 37.8154 64.247 37.8154C62.7032 37.8154 61.2959 37.6134 60.108 37.214C60.0781 37.4442 60.0581 37.6706 60.0581 37.8884C60.0581 40.4435 61.0135 41.9766 65.2024 41.9766C71.3756 41.9766 77.1813 38.3264 84.2364 29.7851C91.1445 21.3898 96.3623 17.7397 101.874 17.7397C105.595 17.7397 108.419 19.7428 109.663 23.1164C117.05 12.5182 123.317 5.00901 128.624 0C133.842 2.19008 137.59 6.49724 136.561 7.37328C132.666 10.8774 119.659 24.9669 114.514 33.3623C113.192 35.5523 111.942 37.9614 111.942 39.1295C111.942 40.2245 112.604 40.5895 113.339 40.5895C118.189 40.5895 129.727 24.9669 136.267 18.1047C140.383 19.7837 144.572 23.3609 143.543 24.6019C138.105 31.0262 133.989 36.2824 133.989 39.2755C133.989 40.0785 134.283 40.5895 135.386 40.5895C137.443 40.5895 139.354 38.7645 142.514 34.8953C143.176 34.0923 143.984 34.0923 144.498 35.3333C145.895 38.6915 147.952 40.5165 149.569 40.5165C151.48 40.5165 152.435 38.8375 152.435 36.2824C152.435 33.2163 151.774 29.2741 151.774 27.522C151.774 21.6088 156.183 18.1777 161.695 18.1777C165.811 18.1777 169.485 20.1488 171.249 25.0399ZM35.3941 14.3913C32.919 18.234 30.6152 21.8236 28.2879 25.3243C29.5527 26.0299 31.1522 26.573 33.2342 26.573C37.1291 26.573 41.3916 24.4559 41.3916 20.1488C41.3916 17.5349 39.7584 15.1251 35.3941 14.3913ZM61.226 33.9955C61.9983 34.2919 62.9051 34.4573 63.8796 34.4573C69.0974 34.4573 72.6249 30.5152 72.6249 27.8871C72.6249 26.719 71.89 25.916 70.7142 25.916C67.7657 25.916 63.3185 30.0262 61.226 33.9955ZM104.961 28.5441C104.961 27.084 104.152 26.208 102.829 26.208C98.4935 26.208 91.9529 34.3843 91.9529 38.4725C91.9529 39.7865 92.6878 40.6625 94.231 40.6625C99.0079 40.6625 104.961 32.0482 104.961 28.5441Z\"\n                fill=\"#FF4438\"\n              />\n            </g>\n            <defs>\n              <clipPath id=\"clip0_96_147\">\n                <rect\n                  width=\"170.497\"\n                  height=\"64\"\n                  fill=\"white\"\n                  transform=\"translate(0.751465)\"\n                />\n              </clipPath>\n            </defs>\n          </svg>\n        </div>\n        <span id=\"text\" class=\"text\"></span>\n        <span id=\"copyright\" class=\"copyright\"></span>\n      </div>\n    </div>\n\n    <script>\n      const bootstrap = async () => {\n        const appVersion =\n          (await window.app.ipc.invoke('app:get:version')) || '';\n        const copyrightEl = document.getElementById('copyright') || null;\n\n        if (copyrightEl) {\n          copyrightEl.innerHTML = `Redis Insight ${appVersion}&nbsp;&nbsp;© ${new Date().getFullYear() || '2023'} Redis Ltd.`;\n        }\n      };\n\n      bootstrap();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/desktop/src/config/configMain.ts",
    "content": "import { app } from 'electron'\nimport path from 'path'\nimport { getAssetPath } from 'desktopSrc/utils'\nimport configInit from '../../config.json'\nimport pkg from '../../../package.json'\n\nconst config: any = configInit\n\n// Merge in some details from package.json\nconfig.defaultPort =\n  process.env.NODE_ENV === 'development' ? 5540 : config.defaultPort\nconfig.name = pkg.appName\nconfig.description = pkg.description\nconfig.version = pkg.version\nconfig.author = pkg.author\nconfig.isDevelopment = process.env.NODE_ENV === 'development'\nconfig.isProduction = process.env.NODE_ENV === 'production'\nconfig.appPort = process.env.RI_APP_PORT || configInit.defaultPort\nconfig.appType = process.env.RI_APP_TYPE || 'ELECTRON'\nconfig.isEnterprise = config.appType === 'ELECTRON_ENTERPRISE'\nconfig.getApiPort = () => process.env.RI_APP_PORT || configInit.defaultPort\nconfig.tcpLocalAuthPort = process.env.TCP_LOCAL_AUTH_PORT\n  ? parseInt(process.env.TCP_LOCAL_AUTH_PORT, 10)\n  : 5541\n\nconfig.icon = getAssetPath('icon.png')\n\nconfig.preloadPath = app.isPackaged\n  ? path.join(__dirname, 'preload.js')\n  : path.join(__dirname, '../../../dist/preload.js')\n\nexport const configMain = config\n"
  },
  {
    "path": "redisinsight/desktop/src/config/configRenderer.ts",
    "content": "import configInit from '../../config.json'\nimport pkg from '../../../package.json'\n\nconst config: any = configInit\n\n// Merge in some details from package.json\nconfig.defaultPort =\n  process.env.NODE_ENV === 'development' ? 5540 : config.defaultPort\nconfig.name = pkg.appName\nconfig.description = pkg.description\nconfig.version = pkg.version\nconfig.author = pkg.author\nconfig.isDevelopment = process.env.NODE_ENV === 'development'\nconfig.isProduction = process.env.NODE_ENV === 'production'\nconfig.apiPort = process.env.RI_APP_PORT || configInit.defaultPort\n\nexport const configRenderer = config\n"
  },
  {
    "path": "redisinsight/desktop/src/config/index.ts",
    "content": "export * from './configMain'\nexport * from './configRenderer'\n"
  },
  {
    "path": "redisinsight/desktop/src/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta\n      http-equiv=\"Content-Security-Policy\"\n      content=\"script-src 'self' 'unsafe-inline';\"\n    />\n    <title>RedisInsight</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"../dist/index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts",
    "content": "import { app } from 'electron'\nimport path from 'path'\nimport { configMain as config } from 'desktopSrc/config'\n\nconst ICON_PATH = app.isPackaged\n  ? path.join(process.resourcesPath, 'resources', 'icon.png')\n  : path.join(__dirname, '../resources', 'icon.png')\n\nconst appVersionPrefix = config.isEnterprise ? 'Enterprise - ' : ''\nconst appVersion = app.getVersion() || '3.2.0'\nconst appVersionSuffix = !config.isProduction\n  ? `-dev-${process.getCreationTime()}`\n  : ''\n\nexport const AboutPanelOptions = {\n  applicationName: 'Redis Insight',\n  applicationVersion: `${appVersionPrefix}${appVersion}${appVersionSuffix}`,\n  copyright: `Copyright © ${new Date().getFullYear()} Redis Ltd.`,\n  iconPath: ICON_PATH,\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/app/app.handlers.ts",
    "content": "import { app } from 'electron'\nimport log from 'electron-log'\nimport { getBackendGracefulShutdown } from 'desktopSrc/lib'\nimport {\n  deepLinkHandler,\n  deepLinkWindowHandler,\n} from 'desktopSrc/lib/app/deep-link.handlers'\nimport { showOrCreateWindow } from 'desktopSrc/utils'\n\nexport const initAppHandlers = () => {\n  app.on('activate', async () => {\n    // On macOS it's common to re-create a window in the app when the\n    // dock icon is clicked and there are no other windows open.\n    await showOrCreateWindow()\n  })\n\n  app.on(\n    'certificate-error',\n    (event, _webContents, _url, _error, _certificate, callback) => {\n      // Skip error due to self-signed certificate\n      event.preventDefault()\n      callback(true)\n    },\n  )\n\n  app.on('window-all-closed', () => {\n    log.info('window-all-closed')\n    // Respect the OSX convention of having the application in memory even\n    // after all windows have been closed\n    if (process.platform !== 'darwin') {\n      app.quit()\n    }\n  })\n\n  app.on('continue-activity-error', (event, type, error) => {\n    log.info('event', event)\n    log.info('type', type)\n    log.info('error', error)\n    // Respect the OSX convention of having the application in memory even\n    // after all windows have been closed\n    if (process.platform !== 'darwin') {\n      app.quit()\n    }\n  })\n\n  app.on('quit', () => {\n    try {\n      getBackendGracefulShutdown?.()\n    } catch (e) {\n      // ignore any error\n    }\n  })\n\n  // deep link open (win + linux)\n  app.on('second-instance', async (_event, commandLine) => {\n    await deepLinkWindowHandler(await deepLinkHandler(commandLine?.pop()))\n  })\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/app/deep-link.handlers.ts",
    "content": "import log from 'electron-log'\nimport { parse } from 'url'\nimport {\n  azureDeepLinkHandler,\n  cloudDeepLinkHandler,\n  focusWindow,\n  getWindows,\n  windowFactory,\n  WindowType,\n} from 'desktopSrc/lib'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { IpcOnEvent } from 'uiSrc/electron/constants'\n\nexport interface IParsedDeepLink {\n  initialPage?: string\n  target?: string\n  from?: string\n}\n\nexport const deepLinkHandler = async (\n  from?: string,\n): Promise<undefined | IParsedDeepLink> => {\n  if (from) {\n    try {\n      const url = parse(from, true)\n      switch (url?.hostname) {\n        case 'azure':\n          await azureDeepLinkHandler(url)\n          break\n        case 'cloud':\n          await cloudDeepLinkHandler(url)\n          break\n        default:\n          return {\n            from,\n            target: url.query?.target || '_self',\n            initialPage: url.query?.initialPage,\n          } as IParsedDeepLink\n      }\n    } catch (e) {\n      log.error(wrapErrorMessageSensitiveData(e as Error))\n    }\n  }\n\n  return undefined\n}\n\nexport const deepLinkWindowHandler = async (\n  parsedDeepLink?: IParsedDeepLink,\n) => {\n  // tbd: implement mechanism to find current window\n  const [currentWindow] = getWindows().values()\n\n  if (parsedDeepLink) {\n    if (parsedDeepLink?.target === '_blank') {\n      await windowFactory(WindowType.Main, null, { parsedDeepLink })\n    } else if (currentWindow) {\n      currentWindow?.show()\n      currentWindow?.webContents.send(IpcOnEvent.deepLinkAction, parsedDeepLink)\n      focusWindow(currentWindow)\n    } else {\n      await windowFactory(WindowType.Main, null, { parsedDeepLink })\n    }\n  } else if (currentWindow) {\n    focusWindow(currentWindow)\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/app/dialog.handlers.ts",
    "content": "import { dialog } from 'electron'\nimport log from 'electron-log'\n\nexport const initDialogHandlers = () => {\n  dialog.showErrorBox = (title: string, content: string) => {\n    log.error('Dialog shows error:', `\\n${title}\\n${content}`)\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/app/index.ts",
    "content": "import { initWindowIPCHandlers } from 'desktopSrc/lib/window'\nimport { initAppHandlers } from './app.handlers'\nimport { initDialogHandlers } from './dialog.handlers'\nimport { initIPCHandlers } from './ipc.handlers'\n\nexport const initElectronHandlers = () => {\n  initAppHandlers()\n  initIPCHandlers()\n  initDialogHandlers()\n\n  initWindowIPCHandlers()\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/app/ipc.handlers.ts",
    "content": "import { app, ipcMain, nativeTheme } from 'electron'\nimport { electronStore } from 'desktopSrc/lib'\nimport { ElectronStorageItem, IpcInvokeEvent } from 'uiSrc/electron/constants'\n\nexport const initIPCHandlers = () => {\n  ipcMain.handle(IpcInvokeEvent.getAppVersion, () => app?.getVersion())\n\n  ipcMain.handle(IpcInvokeEvent.getStoreValue, (_event, key) =>\n    electronStore?.get(key),\n  )\n\n  ipcMain.handle(IpcInvokeEvent.deleteStoreValue, (_event, key) =>\n    electronStore?.delete(key),\n  )\n\n  ipcMain.handle(IpcInvokeEvent.themeChange, (_event, theme: string) => {\n    const themeSource = theme.toLowerCase() as typeof nativeTheme.themeSource\n\n    nativeTheme.themeSource = themeSource\n    electronStore?.set(ElectronStorageItem.themeSource, themeSource)\n  })\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/auth/auth.factory.ts",
    "content": "import { AuthStrategy } from './auth.interface'\nimport { TcpAuthStrategy } from './tcp.auth.strategy'\nimport { ServiceAuthStrategy } from './service.auth.strategy'\n\nexport const createAuthStrategy = (beApp?: any): AuthStrategy => {\n  if (process.env.USE_TCP_CLOUD_AUTH === 'true') {\n    return TcpAuthStrategy.getInstance()\n  }\n  return ServiceAuthStrategy.getInstance(beApp)\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/auth/auth.interface.ts",
    "content": "export interface AuthStrategy {\n  initialize(): Promise<void>\n  shutdown(): Promise<void>\n  getAuthUrl(options: any): Promise<{ url: string }>\n  handleCallback(query: any): Promise<any>\n  getBackendApp?(): any\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/auth/service.auth.strategy.ts",
    "content": "import log from 'electron-log'\nimport { AuthStrategy } from './auth.interface'\nimport { CloudAuthService } from '../../../../api/dist/src/modules/cloud/auth/cloud-auth.service'\nimport { CloudAuthModule } from '../../../../api/dist/src/modules/cloud/auth/cloud-auth.module'\n\nexport class ServiceAuthStrategy implements AuthStrategy {\n  private static instance: ServiceAuthStrategy\n\n  private cloudAuthService!: CloudAuthService\n\n  private initialized = false\n\n  private beApp: any\n\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  private constructor() {}\n\n  public static getInstance(beApp?: any): ServiceAuthStrategy {\n    if (!ServiceAuthStrategy.instance) {\n      ServiceAuthStrategy.instance = new ServiceAuthStrategy()\n    }\n    if (beApp) {\n      ServiceAuthStrategy.instance.beApp = beApp\n    }\n    return ServiceAuthStrategy.instance\n  }\n\n  async initialize(): Promise<void> {\n    if (this.initialized) {\n      log.info('[Service Auth] Already initialized')\n      return\n    }\n\n    log.info('[Service Auth] Initializing service auth')\n    try {\n      if (!this.beApp) {\n        throw new Error('[Service Auth] Backend app not provided')\n      }\n\n      this.cloudAuthService = this.beApp\n        .select(CloudAuthModule)\n        .get(CloudAuthService)\n      this.initialized = true\n      log.info('[Service Auth] Service auth initialized')\n    } catch (err) {\n      log.error('[Service Auth] Initialization failed:', err)\n      throw err\n    }\n  }\n\n  async getAuthUrl(options: any): Promise<{ url: string }> {\n    log.info('[Service Auth] Getting auth URL')\n    const url = await this.cloudAuthService.getAuthorizationUrl(\n      options.sessionMetadata,\n      options.authOptions,\n    )\n    log.info('[Service Auth] Auth URL obtained')\n    return { url }\n  }\n\n  async handleCallback(query: any): Promise<any> {\n    log.info('[Service Auth] Handling callback')\n    if (this.cloudAuthService.isRequestInProgress(query)) {\n      log.info('[Service Auth] Request already in progress, skipping')\n      return { status: 'succeed' }\n    }\n    const result = await this.cloudAuthService.handleCallback(query)\n    log.info('[Service Auth] Callback handled')\n    return result\n  }\n\n  async shutdown(): Promise<void> {\n    log.info('[Service Auth] Shutting down service auth')\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/auth/tcp.auth.strategy.ts",
    "content": "import log from 'electron-log'\nimport { Socket } from 'net'\nimport { AuthStrategy } from './auth.interface'\n\nexport class TcpAuthStrategy implements AuthStrategy {\n  private static instance: TcpAuthStrategy\n\n  private initialized = false\n\n  private readonly port = parseInt(\n    process.env.TCP_LOCAL_CLOUD_AUTH_PORT || '5542',\n    10,\n  )\n\n  // eslint-disable-next-line @typescript-eslint/no-empty-function\n  private constructor() {}\n\n  public static getInstance(): TcpAuthStrategy {\n    if (!TcpAuthStrategy.instance) {\n      TcpAuthStrategy.instance = new TcpAuthStrategy()\n    }\n    return TcpAuthStrategy.instance\n  }\n\n  async initialize(): Promise<void> {\n    if (this.initialized) {\n      return\n    }\n\n    log.info('[TCP Auth] Initializing TCP auth strategy')\n\n    this.initialized = true\n  }\n\n  // Add this method to handle window auth requests\n  async checkWindowAuth(windowId: string): Promise<boolean> {\n    log.info('[TCP Auth] Checking window auth via TCP:', windowId)\n    const result = await this.sendTcpRequest('checkWindowAuth', { windowId })\n    return result.isAuthorized\n  }\n\n  private async sendTcpRequest(action: string, options: any): Promise<any> {\n    return new Promise((resolve, reject) => {\n      const client = new Socket()\n\n      client.connect(this.port, 'localhost', () => {\n        client.write(JSON.stringify({ action, options }))\n      })\n\n      client.on('data', (data) => {\n        try {\n          const response = JSON.parse(data.toString())\n          resolve(response)\n        } catch (err) {\n          reject(err)\n        }\n        client.end()\n      })\n      client.on('error', (err) => {\n        reject(err)\n      })\n    })\n  }\n\n  async getAuthUrl(options: any): Promise<{ url: string }> {\n    log.info('[TCP Auth] Getting auth URL')\n    const result = await this.sendTcpRequest('getAuthUrl', options)\n    log.info('[TCP Auth] Auth URL obtained')\n    return result\n  }\n\n  async handleCallback(query: any): Promise<any> {\n    log.info('[TCP Auth] Handling callback')\n    const result = await this.sendTcpRequest('handleCallback', { query })\n    log.info('[TCP Auth] Callback handled')\n    return result\n  }\n\n  async shutdown(): Promise<void> {\n    log.info('[TCP Auth] Shutting down TCP auth server')\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/azure/azure-auth.service.provider.ts",
    "content": "import log from 'electron-log'\nimport { AzureAuthService } from '../../../../api/dist/src/modules/azure/auth/azure-auth.service'\nimport { AzureModule } from '../../../../api/dist/src/modules/azure/azure.module'\n\nlet azureAuthService: AzureAuthService | null = null\nlet beApp: any = null\n\n/**\n * Initialize the Azure auth service provider with the backend app instance.\n * This should be called after the NestJS app is bootstrapped.\n */\nexport const initAzureAuthServiceProvider = (app: any): void => {\n  beApp = app\n  azureAuthService = null // Reset cached service when app is re-initialized\n  log.debug('[Azure Auth] Service provider initialized with backend app')\n}\n\n/**\n * Get the AzureAuthService instance from the backend app.\n */\nexport const getAzureAuthService = (): AzureAuthService | null => {\n  if (azureAuthService) {\n    return azureAuthService\n  }\n\n  if (!beApp) {\n    log.warn('[Azure Auth] Backend app not initialized')\n    return null\n  }\n\n  try {\n    azureAuthService = beApp.select(AzureModule).get(AzureAuthService)\n    log.debug('[Azure Auth] Service obtained from backend app')\n    return azureAuthService\n  } catch (err) {\n    log.error('[Azure Auth] Failed to get service from backend app:', err)\n    return null\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/azure/azure-oauth-errors.spec.ts",
    "content": "import { mapKnownAzureAdError } from './azure-oauth-errors'\n\ndescribe('mapKnownAzureAdError', () => {\n  describe('known Microsoft AADSTS errors', () => {\n    it('should return user-friendly message with error code for AADSTS650057', () => {\n      const microsoftError =\n        'AADSTS650057: Invalid resource. The client has requested access to a resource which is not listed. Trace ID: abc123'\n\n      const result = mapKnownAzureAdError(microsoftError)\n\n      expect(result).toBe(\n        'Azure authentication failed. The application is not properly configured for Azure Redis access. Please contact your administrator. (AADSTS650057)',\n      )\n    })\n\n    it('should return user-friendly message with error code for AADSTS65004', () => {\n      const microsoftError =\n        'AADSTS65004: User declined to consent to access the app.'\n\n      const result = mapKnownAzureAdError(microsoftError)\n\n      expect(result).toBe(\n        'Azure authentication was cancelled or access was denied. (AADSTS65004)',\n      )\n    })\n\n    it('should correctly match AADSTS700016 instead of AADSTS70001', () => {\n      const microsoftError =\n        'AADSTS700016: Application with identifier was not found in the directory.'\n\n      const result = mapKnownAzureAdError(microsoftError)\n\n      expect(result).toBe(\n        'Azure authentication failed. The application is not available in your directory. Please contact your administrator. (AADSTS700016)',\n      )\n    })\n\n    it('should correctly match AADSTS70001 when it is the exact error', () => {\n      const microsoftError =\n        'AADSTS70001: Application is not registered in the tenant.'\n\n      const result = mapKnownAzureAdError(microsoftError)\n\n      expect(result).toBe(\n        'Azure authentication failed. The application is not registered. Please contact your administrator. (AADSTS70001)',\n      )\n    })\n  })\n\n  describe('non-Microsoft errors', () => {\n    it('should return original error unchanged for non-AADSTS errors', () => {\n      const customError = 'Connection timeout while authenticating'\n\n      const result = mapKnownAzureAdError(customError)\n\n      expect(result).toBe(customError)\n    })\n\n    it('should return original error for generic OAuth errors', () => {\n      const genericError = 'access_denied: User cancelled the request'\n\n      const result = mapKnownAzureAdError(genericError)\n\n      expect(result).toBe(genericError)\n    })\n  })\n\n  describe('edge cases', () => {\n    it('should return default message for undefined input with no error fallback', () => {\n      const result = mapKnownAzureAdError(undefined)\n\n      expect(result).toBe('Azure authentication failed. Please try again.')\n    })\n\n    it('should return error code when errorDescription is undefined but error is provided', () => {\n      const result = mapKnownAzureAdError(undefined, 'access_denied')\n\n      expect(result).toBe('access_denied')\n    })\n\n    it('should handle array input and use first element', () => {\n      const errorArray = ['AADSTS650057: Invalid resource', 'Secondary error']\n\n      const result = mapKnownAzureAdError(errorArray)\n\n      expect(result).toBe(\n        'Azure authentication failed. The application is not properly configured for Azure Redis access. Please contact your administrator. (AADSTS650057)',\n      )\n    })\n\n    it('should handle array error fallback and use first element', () => {\n      const result = mapKnownAzureAdError(undefined, [\n        'access_denied',\n        'secondary',\n      ])\n\n      expect(result).toBe('access_denied')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/azure/azure-oauth-errors.ts",
    "content": "/**\n * User-friendly messages for known Azure AD error codes.\n * Sorted by error code length (longest first) to ensure correct matching.\n * @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes\n */\nconst AZURE_AD_ERROR_MESSAGES: [string, string][] = [\n  // Application not found in directory (must be before AADSTS70001 due to substring match)\n  [\n    'AADSTS700016',\n    'Azure authentication failed. The application is not available in your directory. Please contact your administrator.',\n  ],\n  // Invalid resource - app registration misconfiguration\n  [\n    'AADSTS650057',\n    'Azure authentication failed. The application is not properly configured for Azure Redis access. Please contact your administrator.',\n  ],\n  // Invalid scope - requested scope not configured\n  [\n    'AADSTS70011',\n    'Azure authentication failed. The requested permissions are not configured. Please contact your administrator.',\n  ],\n  // Application not found (invalid client_id)\n  [\n    'AADSTS70001',\n    'Azure authentication failed. The application is not registered. Please contact your administrator.',\n  ],\n  // User consent required\n  [\n    'AADSTS65001',\n    'Azure authentication requires consent. Please grant the required permissions and try again.',\n  ],\n  // User denied consent\n  ['AADSTS65004', 'Azure authentication was cancelled or access was denied.'],\n  // Invalid reply URL\n  [\n    'AADSTS50011',\n    'Azure authentication failed. Invalid redirect configuration. Please contact your administrator.',\n  ],\n  // MFA required\n  [\n    'AADSTS50076',\n    'Multi-factor authentication is required. Please complete MFA and try again.',\n  ],\n]\n\n/**\n * Maps known Azure AD (AADSTS) error codes to user-friendly messages.\n * Returns the original error description unchanged for non-Microsoft errors.\n *\n * @param errorDescription - The error_description from Azure OAuth\n * @param error - The error code from Azure OAuth (fallback when description is absent)\n */\nexport const mapKnownAzureAdError = (\n  errorDescription: string | string[] | undefined,\n  error?: string | string[],\n): string => {\n  const description = Array.isArray(errorDescription)\n    ? errorDescription[0]\n    : errorDescription\n\n  if (!description) {\n    // Fallback to error code if errorDescription is absent\n    const errorCode = Array.isArray(error) ? error[0] : error\n    return errorCode || 'Azure authentication failed. Please try again.'\n  }\n\n  for (const [errorCode, userFriendlyMessage] of AZURE_AD_ERROR_MESSAGES) {\n    if (description.includes(errorCode)) {\n      return `${userFriendlyMessage} (${errorCode})`\n    }\n  }\n\n  return description\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/azure/deep-link.handlers.ts",
    "content": "import log from 'electron-log'\nimport axios from 'axios'\nimport { UrlWithParsedQuery } from 'url'\nimport { configMain as config } from 'desktopSrc/config'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { getWindows } from 'desktopSrc/lib/window/browserWindow'\nimport { IpcOnEvent } from 'uiSrc/electron/constants'\nimport {\n  AzureAuthStatus,\n  AZURE_OAUTH_REDIRECT_PATH,\n} from 'apiSrc/modules/azure/constants'\nimport { getAzureAuthService } from './azure-auth.service.provider'\nimport { mapKnownAzureAdError } from './azure-oauth-errors'\n\n// Extract pathname from redirect URI (e.g., '/oauth/callback' from 'redisinsight://azure/oauth/callback')\nconst AZURE_OAUTH_CALLBACK_PATH = new URL(AZURE_OAUTH_REDIRECT_PATH).pathname\n\n/**\n * Handle callback via direct service call (production mode).\n * In production, the API is embedded in the Electron app.\n */\nconst handleCallbackViaService = async (code: string, state: string) => {\n  const azureAuthService = getAzureAuthService()\n  if (!azureAuthService) {\n    throw new Error('Azure auth service not initialized')\n  }\n  return azureAuthService.handleCallback(code, state)\n}\n\n/**\n * Handle callback via HTTP request (development mode).\n * In development, the API runs as a separate process.\n */\nconst handleCallbackViaHttp = async (code: string, state: string) => {\n  const apiBase = `http://localhost:${config.getApiPort()}/api`\n  const response = await axios.get(`${apiBase}/azure/auth/callback`, {\n    params: { code, state },\n  })\n  return response.data\n}\n\nconst azureOauthCallback = async (url: UrlWithParsedQuery) => {\n  const [currentWindow] = getWindows().values()\n\n  try {\n    const {\n      code,\n      state,\n      error,\n      error_description: errorDescription,\n    } = url.query\n\n    // Handle OAuth errors from Azure\n    if (error) {\n      log.error('Azure OAuth error:', error, errorDescription)\n      const errorMessage = mapKnownAzureAdError(errorDescription, error)\n      currentWindow?.webContents.send(IpcOnEvent.azureOauthCallback, {\n        status: AzureAuthStatus.Failed,\n        error: errorMessage,\n      })\n      return\n    }\n\n    if (!code || !state) {\n      log.error('Azure OAuth callback missing code or state')\n      currentWindow?.webContents.send(IpcOnEvent.azureOauthCallback, {\n        status: AzureAuthStatus.Failed,\n        error: 'Missing authorization code or state',\n      })\n      return\n    }\n\n    // Use direct service call in production, HTTP in development\n    let result\n    if (config.isDevelopment) {\n      log.debug('Using HTTP callback handler (development mode)')\n      result = await handleCallbackViaHttp(code as string, state as string)\n    } else {\n      log.debug('Using service callback handler (production mode)')\n      result = await handleCallbackViaService(code as string, state as string)\n    }\n\n    currentWindow?.webContents.send(IpcOnEvent.azureOauthCallback, {\n      status: result.status,\n      account: result.account,\n    })\n    currentWindow?.focus()\n  } catch (e) {\n    log.error(\n      'Azure OAuth callback error:',\n      wrapErrorMessageSensitiveData(e as Error),\n    )\n    currentWindow?.webContents.send(IpcOnEvent.azureOauthCallback, {\n      status: AzureAuthStatus.Failed,\n      error: (e as Error).message,\n    })\n  }\n}\n\nexport const azureDeepLinkHandler = async (url: UrlWithParsedQuery) => {\n  switch (url?.pathname) {\n    case AZURE_OAUTH_CALLBACK_PATH:\n      await azureOauthCallback(url)\n      break\n    default:\n      log.warn('Unknown Azure deep link pathname', url?.pathname)\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/azure/index.ts",
    "content": "export * from './deep-link.handlers'\nexport * from './azure-auth.service.provider'\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/cloud/cloud-oauth.handlers.ts",
    "content": "import { ipcMain, WebContents } from 'electron'\nimport log from 'electron-log'\nimport open from 'open'\nimport { UrlWithParsedQuery } from 'url'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\n\nimport { IpcOnEvent, IpcInvokeEvent } from 'uiSrc/electron/constants'\n\nimport { CloudOauthUnexpectedErrorException } from 'apiSrc/modules/cloud/auth/exceptions'\nimport {\n  CloudAuthRequestOptions,\n  CloudAuthResponse,\n  CloudAuthStatus,\n} from 'apiSrc/modules/cloud/auth/models'\nimport { DEFAULT_SESSION_ID, DEFAULT_USER_ID } from 'apiSrc/common/constants'\nimport { createAuthStrategy } from '../auth/auth.factory'\nimport { getWindows } from '../window/browserWindow'\n\nconst authStrategy = createAuthStrategy()\n\nexport const getOauthIpcErrorResponse = (\n  error: any,\n): { status: CloudAuthStatus.Failed; error: {} } => {\n  let errorResponse = new CloudOauthUnexpectedErrorException().getResponse()\n\n  if (error?.getResponse) {\n    errorResponse = error.getResponse()\n  } else if (error instanceof Error) {\n    errorResponse = new CloudOauthUnexpectedErrorException(\n      error.message,\n    ).getResponse()\n  }\n\n  return {\n    status: CloudAuthStatus.Failed,\n    error: errorResponse,\n  }\n}\n\nexport const getTokenCallbackFunction =\n  (webContents: WebContents) => (response: CloudAuthResponse) => {\n    webContents.send(IpcOnEvent.cloudOauthCallback, response)\n    webContents.focus()\n  }\n\nexport const initCloudOauthHandlers = () => {\n  ipcMain.handle(\n    IpcInvokeEvent.cloudOauth,\n    async (event, options: CloudAuthRequestOptions) => {\n      try {\n        await authStrategy.initialize()\n        const { url } = await authStrategy.getAuthUrl({\n          sessionMetadata: {\n            sessionId: DEFAULT_SESSION_ID,\n            userId: DEFAULT_USER_ID,\n          },\n          authOptions: {\n            ...options,\n            callback: getTokenCallbackFunction(event?.sender as WebContents),\n          },\n        })\n\n        await open(url)\n\n        return {\n          status: CloudAuthStatus.Succeed,\n        }\n      } catch (e) {\n        log.error(wrapErrorMessageSensitiveData(e as Error))\n        const error = getOauthIpcErrorResponse(e)\n        const [currentWindow] = getWindows().values()\n        currentWindow?.webContents.send(IpcOnEvent.cloudOauthCallback, error)\n        return error\n      }\n    },\n  )\n}\n\nexport const cloudOauthCallback = async (url: UrlWithParsedQuery) => {\n  try {\n    const result = await authStrategy.handleCallback(url.query)\n\n    if (result.status === CloudAuthStatus.Failed) {\n      const [currentWindow] = getWindows().values()\n      currentWindow?.webContents.send(IpcOnEvent.cloudOauthCallback, result)\n    }\n  } catch (e) {\n    log.error(wrapErrorMessageSensitiveData(e as Error))\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/cloud/deep-link.handlers.ts",
    "content": "import log from 'electron-log'\nimport { UrlWithParsedQuery } from 'url'\nimport { cloudOauthCallback } from 'desktopSrc/lib/cloud/cloud-oauth.handlers'\n\nexport const cloudDeepLinkHandler = async (url: UrlWithParsedQuery) => {\n  switch (url?.pathname) {\n    case '/oauth/callback':\n      await cloudOauthCallback(url)\n      break\n    default:\n      log.warn('Unknown cloud deep link pathname', url?.pathname)\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/cloud/index.ts",
    "content": "import { initCloudOauthHandlers } from 'desktopSrc/lib/cloud/cloud-oauth.handlers'\n\nexport * from './deep-link.handlers'\n\nexport const initCloudHandlers = () => {\n  initCloudOauthHandlers()\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/extensions/extensions.ts",
    "content": "/* eslint-disable no-console */\nimport installExtension, {\n  REACT_DEVELOPER_TOOLS,\n  REDUX_DEVTOOLS,\n} from 'electron-devtools-installer'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { configMain as config } from 'desktopSrc/config'\n\nexport const installExtensions = async () => {\n  if (config.isProduction) {\n    return Promise.resolve()\n  }\n\n  const extensions = [REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]\n  const forceDownload = !!process.env.UPGRADE_EXTENSIONS\n\n  return installExtension(extensions, {\n    forceDownload,\n    loadExtensionOptions: { allowFileAccess: true },\n  })\n    .then((name) => console.log(`Added Extension:  ${name}`))\n    .catch((err) =>\n      console.log(\n        'An error occurred: ',\n        wrapErrorMessageSensitiveData(err).toString(),\n      ),\n    )\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/index.ts",
    "content": "export * from './aboutPanel/aboutPanel'\nexport * from './tray/tray'\nexport * from './menu/menu'\nexport * from './updater/updater'\nexport * from './updater/updater.handlers'\nexport * from './extensions/extensions'\nexport * from './tray/trayManager'\nexport * from './window/browserWindow'\nexport * from './logging/logging'\nexport * from './app'\nexport * from './server/server'\nexport * from './store/store'\nexport * from './azure'\nexport * from './cloud'\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/logging/logging.ts",
    "content": "import log from 'electron-log'\nimport { configMain as config } from 'desktopSrc/config'\n\nexport const initLogging = () => {\n  if (!config.isProduction) {\n    log.transports.file.getFile().clear()\n  }\n\n  log.info('App starting.....')\n}\n\nexport const logStoreStatus = (text: string) => {\n  log.info(text)\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/menu/menu.ts",
    "content": "import {\n  app,\n  Menu,\n  shell,\n  BrowserWindow,\n  MenuItemConstructorOptions,\n  MenuItem,\n} from 'electron'\n// eslint-disable-next-line import/no-cycle\nimport {\n  getDisplayAppInTrayValue,\n  updateDisplayAppInTray,\n  WindowType,\n  windowFactory,\n  electronStore,\n} from 'desktopSrc/lib'\nimport { ElectronStorageItem } from 'uiSrc/electron/constants'\n\ninterface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {\n  selector?: string\n  submenu?: DarwinMenuItemConstructorOptions[] | Menu\n}\n\nexport const STEP_ZOOM_FACTOR = 0.2\n\nexport class MenuBuilder {\n  public mainWindow: BrowserWindow\n\n  constructor(mainWindow: BrowserWindow) {\n    this.mainWindow = mainWindow\n  }\n\n  buildMenu(): Menu {\n    const template =\n      process.platform === 'darwin'\n        ? this.buildDarwinTemplate()\n        : this.buildDefaultTemplate()\n\n    const menu = Menu.buildFromTemplate(\n      template as MenuItemConstructorOptions[],\n    )\n    Menu.setApplicationMenu(menu)\n\n    return menu\n  }\n\n  getZoomFactor(isZoomIn: boolean = false): number {\n    const correctZoomFactor = isZoomIn ? STEP_ZOOM_FACTOR : -STEP_ZOOM_FACTOR\n    const zoomFactor =\n      (this.mainWindow?.webContents.getZoomFactor() * 100 +\n        correctZoomFactor * 100) /\n      100\n    return zoomFactor\n  }\n\n  setZoomFactor(zoomFactor: number): void {\n    electronStore?.set(ElectronStorageItem.zoomFactor, zoomFactor)\n    this.mainWindow.webContents.setZoomFactor(zoomFactor)\n  }\n\n  buildDarwinTemplate(): MenuItemConstructorOptions[] {\n    const subMenuApp: DarwinMenuItemConstructorOptions = {\n      label: app.name,\n      submenu: [\n        {\n          label: `About ${app.name}`,\n          selector: 'orderFrontStandardAboutPanel:',\n        },\n        { type: 'separator' },\n        {\n          label: `Hide ${app.name}`,\n          accelerator: 'Command+H',\n          selector: 'hide:',\n        },\n        {\n          label: 'Hide Others',\n          accelerator: 'Command+Shift+H',\n          selector: 'hideOtherApplications:',\n        },\n        { label: 'Show All', selector: 'unhideAllApplications:' },\n        { type: 'separator' },\n        {\n          label: 'Quit',\n          accelerator: 'Command+Q',\n          click: () => {\n            app.quit()\n          },\n        },\n      ],\n    }\n    const subMenuEdit: DarwinMenuItemConstructorOptions = {\n      label: 'Edit',\n      submenu: [\n        { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },\n        { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },\n        { type: 'separator' },\n        { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },\n        { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },\n        { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },\n        {\n          label: 'Select All',\n          accelerator: 'Command+A',\n          selector: 'selectAll:',\n        },\n      ],\n    }\n    const subMenuView: MenuItemConstructorOptions = {\n      label: 'View',\n      submenu: [\n        {\n          label: 'Reload',\n          accelerator: 'Command+R',\n          click: () => {\n            this.mainWindow.webContents.reload()\n          },\n        },\n        { type: 'separator' },\n        {\n          label: 'Toggle Full Screen',\n          accelerator: 'Ctrl+Command+F',\n          click: () => {\n            this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen())\n          },\n        },\n        {\n          label: 'Toggle Developer Tools',\n          accelerator: 'Alt+Command+I',\n          click: () => {\n            this.mainWindow.webContents.toggleDevTools()\n          },\n        },\n        { type: 'separator' },\n        {\n          label: 'Reset Zoom',\n          accelerator: 'CmdOrCtrl+0',\n          click: () => {\n            const zoomFactor = 1\n            this.setZoomFactor(zoomFactor)\n          },\n        },\n        {\n          label: 'Zoom In',\n          accelerator: 'CmdOrCtrl+=',\n          click: () => {\n            const zoomFactor = this.getZoomFactor(true)\n            this.setZoomFactor(zoomFactor)\n          },\n        },\n        {\n          label: 'Zoom Out',\n          accelerator: 'CmdOrCtrl+-',\n          click: () => {\n            const zoomFactor = this.getZoomFactor()\n            this.setZoomFactor(zoomFactor)\n          },\n        },\n      ],\n    }\n    const subMenuWindow: DarwinMenuItemConstructorOptions = {\n      label: 'Window',\n      submenu: [\n        {\n          label: 'New Window',\n          accelerator: 'Command+N',\n          click: () => {\n            windowFactory(WindowType.Main)\n          },\n        },\n        {\n          label: 'Minimize',\n          accelerator: 'Command+M',\n          selector: 'performMiniaturize:',\n        },\n        {\n          label: 'Close',\n          accelerator: 'Command+W',\n          click: () => {\n            this.mainWindow.close()\n          },\n        },\n        {\n          type: 'separator',\n        },\n        {\n          label: 'Show in Menu Bar',\n          type: 'checkbox',\n          checked: getDisplayAppInTrayValue(),\n          click: (menuItem: MenuItem) => {\n            updateDisplayAppInTray(menuItem.checked)\n          },\n        },\n        // { type: 'separator' },\n        // { label: 'Bring All to Front', selector: 'arrangeInFront:' },\n      ],\n    }\n    const subMenuHelp: MenuItemConstructorOptions = {\n      label: 'Help',\n      submenu: [\n        {\n          label: 'License Terms',\n          click() {\n            shell.openExternal(\n              'https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE',\n            )\n          },\n        },\n        {\n          label: 'Submit a Bug or Idea',\n          click() {\n            shell.openExternal(\n              'https://github.com/RedisInsight/RedisInsight/issues',\n            )\n          },\n        },\n        {\n          label: 'Learn More',\n          click() {\n            shell.openExternal(\n              'https://redis.io/docs/latest/develop/tools/insight/?utm_source=redisinsight&utm_medium=main&utm_campaign=learn_more',\n            )\n          },\n        },\n      ],\n    }\n\n    return [subMenuApp, subMenuEdit, subMenuWindow, subMenuView, subMenuHelp]\n  }\n\n  buildDefaultTemplate() {\n    const templateDefault = [\n      {\n        label: '&Window',\n        submenu: [\n          {\n            label: 'New Window',\n            accelerator: 'Ctrl+N',\n            click: () => {\n              windowFactory(WindowType.Main)\n            },\n          },\n          {\n            label: '&Close',\n            accelerator: 'Ctrl+W',\n            click: () => {\n              this.mainWindow.close()\n            },\n          },\n          // type separator cannot be invisible\n          {\n            label: '',\n            type: process.platform !== 'linux' ? 'separator' : 'normal',\n            visible: false,\n          },\n          {\n            label: 'Display On System Tray',\n            type: 'checkbox',\n            visible: process.platform !== 'linux',\n            checked: getDisplayAppInTrayValue(),\n            click: (menuItem: MenuItem) => {\n              updateDisplayAppInTray(menuItem.checked)\n            },\n          },\n        ],\n      },\n      {\n        label: '&View',\n        submenu: [\n          {\n            label: '&Reload',\n            accelerator: 'Ctrl+R',\n            click: () => {\n              this.mainWindow.webContents.reload()\n            },\n          },\n          { type: 'separator' },\n          {\n            label: 'Toggle &Full Screen',\n            accelerator: 'F11',\n            click: () => {\n              this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen())\n              // on Linux menubar is hidden on full screen mode\n              this.mainWindow.setMenuBarVisibility(true)\n            },\n          },\n          {\n            label: 'Toggle &Developer Tools',\n            accelerator: 'Ctrl+Shift+I',\n            click: () => {\n              this.mainWindow.webContents.toggleDevTools()\n            },\n          },\n          { type: 'separator' },\n          {\n            label: 'Reset &Zoom',\n            accelerator: 'Ctrl+0',\n            click: () => {\n              const zoomFactor = 1\n              this.setZoomFactor(zoomFactor)\n            },\n          },\n          {\n            label: 'Zoom &In',\n            accelerator: 'Ctrl+=',\n            click: () => {\n              const zoomFactor = this.getZoomFactor(true)\n              this.setZoomFactor(zoomFactor)\n            },\n          },\n          {\n            label: 'Zoom &Out',\n            accelerator: 'Ctrl+-',\n            click: () => {\n              const zoomFactor = this.getZoomFactor()\n              this.setZoomFactor(zoomFactor)\n            },\n          },\n        ],\n      },\n      {\n        label: 'Help',\n        submenu: [\n          {\n            label: 'License Terms',\n            click() {\n              shell.openExternal(\n                'https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE',\n              )\n            },\n          },\n          {\n            label: 'Submit a Bug or Idea',\n            click() {\n              shell.openExternal(\n                'https://github.com/RedisInsight/RedisInsight/issues',\n              )\n            },\n          },\n          {\n            label: 'Learn More',\n            click() {\n              shell.openExternal(\n                'https://redis.io/docs/latest/develop/tools/insight/?utm_source=redisinsight&utm_medium=main&utm_campaign=learn_more',\n              )\n            },\n          },\n          { type: 'separator' },\n          {\n            label: `About ${app.name}`,\n            click: () => {\n              app.showAboutPanel()\n            },\n          },\n        ],\n      },\n    ]\n\n    return templateDefault\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/server/server.ts",
    "content": "import log from 'electron-log'\nimport getPort, { portNumbers } from 'get-port'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { configMain as config } from 'desktopSrc/config'\nimport { createAuthStrategy } from 'desktopSrc/lib/auth/auth.factory'\nimport { AuthStrategy } from 'desktopSrc/lib/auth/auth.interface'\nimport { initAzureAuthServiceProvider } from 'desktopSrc/lib/azure'\nimport { AbstractWindowAuthStrategy } from 'apiSrc/modules/auth/window-auth/strategies/abstract.window.auth.strategy'\n// eslint-disable-next-line import/extensions -- api/dist doesn't exist in CI\nimport { WindowAuthModule } from '../../../../api/dist/src/modules/auth/window-auth/window-auth.module'\n// eslint-disable-next-line import/extensions -- api/dist doesn't exist in CI\nimport { WindowAuthService } from '../../../../api/dist/src/modules/auth/window-auth/window-auth.service'\n// eslint-disable-next-line import/extensions -- api/dist doesn't exist in CI\nimport server from '../../../../api/dist/src/main'\nimport { getWindows } from '../window'\n\nconst port = config?.defaultPort\nlet gracefulShutdown: Function\nlet beApp: any\n\nexport class ElectronWindowAuthStrategy extends AbstractWindowAuthStrategy {\n  async isAuthorized(id: string): Promise<boolean> {\n    return getWindows()?.has(id)\n  }\n}\n\n// Create auth strategy after beApp is initialized\nlet authStrategy: AuthStrategy\n\nexport const launchApiServer = async () => {\n  try {\n    log.info('[Server] Launching API server')\n\n    if (!config.isDevelopment) {\n      // Production code\n      const detectPortConst = await getPort({\n        port: portNumbers(port, port + 1_000),\n      })\n      process.env.RI_APP_PORT = detectPortConst?.toString()\n\n      if (process.env.APPIMAGE) {\n        process.env.BUILD_PACKAGE = 'appimage'\n      }\n      log.info(\n        '[Server] Starting production server with port:',\n        detectPortConst,\n      )\n      log.info('[Server] Environment:', process.env.NODE_ENV)\n\n      const { gracefulShutdown: gracefulShutdownFn, app: apiApp } =\n        await server(detectPortConst)\n      gracefulShutdown = gracefulShutdownFn\n      beApp = apiApp\n\n      // Get the WindowAuthService directly from the app\n      const winAuthService = beApp\n        ?.select?.(WindowAuthModule)\n        .get?.(WindowAuthService)\n      winAuthService.setStrategy(new ElectronWindowAuthStrategy())\n\n      // Pass the service instance to the auth strategy\n      authStrategy = createAuthStrategy(apiApp)\n      await authStrategy.initialize()\n\n      initAzureAuthServiceProvider(apiApp)\n\n      log.info('[Server] Production server initialized')\n    } else {\n      authStrategy = createAuthStrategy()\n      await authStrategy.initialize()\n    }\n  } catch (_err) {\n    const error = _err as Error\n    log.error(\n      '[Server] Catch server error:',\n      wrapErrorMessageSensitiveData(error),\n    )\n    log.error('[Server] Server initialization error:', error)\n    log.error('[Server] Error stack:', error.stack)\n    throw error\n  }\n}\n\nexport const getBackendGracefulShutdown = () => {\n  log.info('[Server] Initiating graceful shutdown')\n  return gracefulShutdown?.()\n}\n\nexport const getBackendApp = () => {\n  log.info('[Server] Getting backend app')\n  return beApp\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/store/store.ts",
    "content": "import ElectronStore from 'electron-store'\nimport log from 'electron-log'\n\nimport { ElectronStorageItem } from 'uiSrc/electron/constants'\n\nclass ElectronStoreService {\n  private storage: ElectronStore\n\n  constructor() {\n    this.storage = new ElectronStore()\n  }\n\n  get(itemName: ElectronStorageItem) {\n    let item\n    try {\n      item = this.storage.get<ElectronStorageItem>(itemName)\n    } catch (error) {\n      log.error(`get from electron store error: ${error}`)\n    }\n\n    return item ?? null\n  }\n\n  getAllItems() {\n    return this.storage.store\n  }\n\n  set(itemName: ElectronStorageItem, item: any) {\n    try {\n      this.storage.set<ElectronStorageItem>(itemName, item)\n    } catch (error) {\n      log.error(`set to electron store error: ${error}`)\n    }\n  }\n\n  delete(itemName: ElectronStorageItem) {\n    try {\n      this.storage.delete<ElectronStorageItem>(itemName)\n    } catch (error) {\n      log.error(`delete from electron store error: ${error}`)\n    }\n  }\n}\n\nexport const electronStore = new ElectronStoreService()\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/tray/tray.ts",
    "content": "import {\n  app,\n  Menu,\n  shell,\n  Tray,\n  nativeImage,\n  MenuItemConstructorOptions,\n} from 'electron'\nimport path from 'path'\nimport { getWindows } from 'desktopSrc/lib'\nimport { showOrCreateWindow } from 'desktopSrc/utils'\n// eslint-disable-next-line import/no-cycle\nimport { setToQuiting } from './trayManager'\n\nexport class TrayBuilder {\n  public tray: Tray\n\n  constructor() {\n    // eslint-disable-next-line operator-linebreak\n    const iconName =\n      process.platform === 'darwin'\n        ? 'icon-tray-white.png'\n        : 'icon-tray-colored.png'\n    const iconPath = `${!app.isPackaged ? '../' : ''}../../../resources/`\n    const iconFullPath = path.join(__dirname, iconPath, iconName)\n    const icon = nativeImage.createFromPath(iconFullPath)\n    const iconTray = icon.resize({ height: 16, width: 16 })\n\n    this.tray = new Tray(iconTray)\n  }\n\n  buildOpenAppSubMenu() {\n    if (getWindows()?.size > 1) {\n      return {\n        label: 'Open Redis Insight',\n        type: 'submenu',\n        submenu: [\n          {\n            label: 'All',\n            click: () => {\n              this.openApp()\n            },\n          },\n          {\n            type: 'separator',\n          },\n          ...[...getWindows().values()].map((window) => ({\n            label: window.webContents.getTitle(),\n            click: () => {\n              window.show()\n            },\n          })),\n        ],\n      }\n    }\n\n    return {\n      label: 'Open Redis Insight',\n      click: () => {\n        this.openApp()\n      },\n    }\n  }\n\n  buildContextMenu() {\n    const contextMenu = Menu.buildFromTemplate([\n      this.buildOpenAppSubMenu(),\n      { type: 'separator' },\n      {\n        label: 'About',\n        click: () => {\n          this.openApp()\n\n          app.showAboutPanel()\n        },\n      },\n      {\n        label: 'Learn More',\n        click() {\n          shell.openExternal(\n            'https://redis.io/docs/latest/develop/tools/insight/?utm_source=redisinsight&utm_medium=main&utm_campaign=learn_more',\n          )\n        },\n      },\n      { type: 'separator' },\n      {\n        label: 'Quit',\n        click: () => {\n          setToQuiting()\n          app.quit()\n        },\n      },\n    ] as MenuItemConstructorOptions[])\n\n    this.tray.setContextMenu(contextMenu)\n  }\n\n  buildTray() {\n    this.tray.setToolTip(app.name)\n    this.buildContextMenu()\n\n    if (process.platform !== 'darwin') {\n      this.tray.on('click', () => {\n        this.openApp()\n      })\n    }\n\n    return this.tray\n  }\n\n  updateTooltip(name: string) {\n    this.tray.setToolTip(name)\n  }\n\n  private async openApp() {\n    await showOrCreateWindow()\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/tray/trayManager.ts",
    "content": "import { BrowserWindow, Tray } from 'electron'\nimport { electronStore } from 'desktopSrc/lib'\nimport { ElectronStorageItem } from 'uiSrc/electron/constants'\n// eslint-disable-next-line import/no-cycle\nimport { TrayBuilder } from './tray'\n\nlet tray: TrayBuilder\nlet trayInstance: Tray\n\nexport const updateDisplayAppInTray = (value: boolean) => {\n  electronStore?.set(ElectronStorageItem.isDisplayAppInTray, value)\n  if (!value) {\n    trayInstance?.destroy()\n    return\n  }\n  tray = new TrayBuilder()\n  trayInstance = tray.buildTray()\n\n  const currentWindow = BrowserWindow.getFocusedWindow()\n  if (currentWindow) {\n    tray.updateTooltip(currentWindow.webContents.getTitle())\n  }\n}\n\nexport const getDisplayAppInTrayValue = (): boolean => {\n  if (process.platform === 'linux') {\n    return false\n  }\n  return !!electronStore?.get(ElectronStorageItem.isDisplayAppInTray)\n}\n\nexport const initTray = () => {\n  if (getDisplayAppInTrayValue()) {\n    tray = new TrayBuilder()\n    trayInstance = tray.buildTray()\n  }\n}\n\nexport const updateTray = (title: string = '') => {\n  if (!trayInstance?.isDestroyed()) {\n    tray?.buildContextMenu()\n    tray?.updateTooltip(title)\n  }\n}\n\nlet IS_QUITING = false\nexport const getIsQuiting = () => IS_QUITING\nexport const setToQuiting = () => {\n  IS_QUITING = true\n}\n\nexport const getTray = () => tray\nexport const getTrayInstance = () => trayInstance\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/updater/updater.handlers.ts",
    "content": "import { app } from 'electron'\nimport { autoUpdater, UpdateDownloadedEvent } from 'electron-updater'\nimport log from 'electron-log'\n\nimport { electronStore, updateDownloaded } from 'desktopSrc/lib'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { ElectronStorageItem } from 'uiSrc/electron/constants'\n\nexport const initAutoUpdaterHandlers = () => {\n  autoUpdater.on('checking-for-update', () => {\n    log.info('Checking for update...')\n  })\n  autoUpdater.on('update-available', () => {\n    log.info('Update available.')\n    electronStore?.set(ElectronStorageItem.isUpdateAvailable, true)\n  })\n  autoUpdater.on('update-not-available', () => {\n    log.info('Update not available.')\n    electronStore?.set(ElectronStorageItem.isUpdateAvailable, false)\n  })\n  autoUpdater.on('error', (err: Error) => {\n    log.info(`Error in auto-updater. ${wrapErrorMessageSensitiveData(err)}`)\n  })\n  autoUpdater.on('download-progress', (progressObj: any) => {\n    let logMessage = `Download speed: ${progressObj.bytesPerSecond}`\n    logMessage += ` - Downloaded ${progressObj.percent}%`\n    logMessage += ` (${progressObj.transferred}/${progressObj.total})`\n    log.info(logMessage)\n  })\n  autoUpdater.on('update-downloaded', (info: UpdateDownloadedEvent) => {\n    log.info('Update downloaded')\n    log.info('releaseNotes', info.releaseNotes)\n    log.info('releaseDate', info.releaseDate)\n    log.info('releaseName', info.releaseName)\n    log.info('version', info.version)\n    log.info('files', info.files)\n\n    // set updateDownloaded to electron storage for Telemetry send event APPLICATION_UPDATED\n    electronStore?.set(ElectronStorageItem.updateDownloaded, true)\n    electronStore?.set(ElectronStorageItem.updateDownloadedForTelemetry, true)\n    electronStore?.set(\n      ElectronStorageItem.updateDownloadedVersion,\n      info.version,\n    )\n    electronStore?.set(\n      ElectronStorageItem.updatePreviousVersion,\n      app.getVersion(),\n    )\n\n    updateDownloaded(info)\n  })\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/updater/updater.ts",
    "content": "import log from 'electron-log'\nimport { UpdateDownloadedEvent, autoUpdater } from 'electron-updater'\nimport { wrapErrorMessageSensitiveData } from 'desktopSrc/utils'\nimport { getWindows } from 'desktopSrc/lib/window'\nimport { IpcOnEvent } from 'uiSrc/electron/constants'\n\nexport const updateDownloaded = (updateInfo: UpdateDownloadedEvent) => {\n  setTimeout(() => {\n    const [currentWindow] = getWindows().values()\n\n    currentWindow?.webContents.send(IpcOnEvent.appUpdateAvailable, updateInfo)\n  }, 60 * 1_000) // 1 min\n}\n\nexport const checkForUpdate = async (url: string = '') => {\n  if (!url || process.mas) {\n    return\n  }\n\n  log.info('AppUpdater initialization')\n  log.transports.file.level = 'info'\n\n  try {\n    autoUpdater.setFeedURL({\n      provider: 'generic',\n      url,\n    })\n  } catch (_err) {\n    const error = _err as Error\n    log.error(wrapErrorMessageSensitiveData(error))\n  }\n\n  autoUpdater.autoDownload = true\n  autoUpdater.autoInstallOnAppQuit = true\n\n  const res = await autoUpdater.checkForUpdates()\n\n  if (res?.downloadPromise) {\n    await res.downloadPromise\n  }\n}\n\nexport const initAutoUpdateChecks = (url = '', interval = 84 * 3600 * 1000) => {\n  checkForUpdate(url)\n    .catch((e) => log.error(wrapErrorMessageSensitiveData(e)))\n    .finally(() => {\n      setTimeout(() => initAutoUpdateChecks(url, interval), interval)\n    })\n}\n\nexport const quitAndInstallUpdate = () => {\n  autoUpdater.quitAndInstall(true, true)\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/window/browserWindow.ts",
    "content": "import contextMenu from 'electron-context-menu'\nimport { BrowserWindow, Rectangle } from 'electron'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport { IParsedDeepLink } from 'desktopSrc/lib/app/deep-link.handlers'\nimport { configMain as config } from 'desktopSrc/config'\nimport { electronStore, updateTray } from 'desktopSrc/lib'\nimport { resolveHtmlPath, getFittedBounds } from 'desktopSrc/utils'\nimport { ElectronStorageItem } from 'uiSrc/electron/constants'\nimport { initWindowHandlers } from './window.handlers'\n\nexport const windows = new Map<string, BrowserWindow>()\nexport const getWindows = () => windows\nexport const focusWindow = (win: BrowserWindow) => {\n  if (win.isMinimized()) win.restore()\n  win.focus()\n}\nexport const NEW_WINDOW_OFFSET = 24\n\nexport enum WindowType {\n  Splash = 'splash',\n  Main = 'main',\n}\n\nexport interface ICreateWindow {\n  prevWindow: BrowserWindow | null\n  htmlFileName: string\n  id?: string\n  windowType: WindowType\n  options: any\n}\n\nexport const createWindow = async ({\n  prevWindow = null,\n  htmlFileName = '',\n  windowType = WindowType.Main,\n  id = uuidv4(),\n  options = {},\n}: ICreateWindow) => {\n  let x\n  let y\n  let { width, height } = options\n\n  const currentWindow = BrowserWindow.getFocusedWindow()\n  const isNewMainWindow =\n    currentWindow && currentWindow?.getTitle() !== config.splashWindow.title\n\n  if (isNewMainWindow) {\n    const [currentWindowX, currentWindowY] = currentWindow.getPosition()\n    const [currentWindowWidth, currentWindowHeight] = currentWindow?.getSize()\n    x = currentWindowX + NEW_WINDOW_OFFSET\n    y = currentWindowY + NEW_WINDOW_OFFSET\n    width = currentWindowWidth\n    height = currentWindowHeight\n  }\n\n  const newWindow: BrowserWindow | null = new BrowserWindow({\n    ...options,\n    x,\n    y,\n    width,\n    height,\n    webPreferences: {\n      ...options.webPreferences,\n      preload: options.preloadPath,\n    },\n  })\n\n  if (windowType !== WindowType.Main) {\n    await newWindow.loadURL(resolveHtmlPath(htmlFileName))\n    return newWindow\n  }\n\n  const savedBounds = electronStore?.get(ElectronStorageItem.bounds)\n  if (!isNewMainWindow && savedBounds) {\n    const bounds = getFittedBounds(savedBounds as Rectangle)\n    if (bounds) {\n      newWindow.setBounds(bounds)\n    }\n  }\n\n  if (config.isDevelopment) {\n    newWindow.loadURL('http://localhost:8080')\n  } else {\n    newWindow.loadURL(resolveHtmlPath(htmlFileName, options?.parsedDeepLink))\n  }\n\n  initWindowHandlers(newWindow, prevWindow, windows, id)\n\n  contextMenu({ window: newWindow, showInspectElement: true })\n\n  windows.set(id, newWindow)\n\n  updateTray(newWindow.webContents.getTitle())\n\n  return newWindow\n}\n\nexport const windowFactory = async (\n  windowType: WindowType,\n  prevWindow: BrowserWindow | null = null,\n  options?: { parsedDeepLink?: IParsedDeepLink },\n): Promise<BrowserWindow> => {\n  switch (windowType) {\n    case WindowType.Splash:\n      return createWindow({\n        prevWindow,\n        htmlFileName: config.isDevelopment\n          ? '../../../splash.html'\n          : 'splash.html',\n        windowType,\n        options: {\n          ...config.splashWindow,\n          preloadPath: config.preloadPath,\n        },\n      })\n    case WindowType.Main:\n      return createWindow({\n        prevWindow,\n        htmlFileName: config.isDevelopment ? '../src/index.html' : 'index.html',\n        windowType,\n        options: {\n          ...options,\n          ...config.mainWindow,\n          preloadPath: config.preloadPath,\n        },\n      })\n\n    default:\n      break\n  }\n\n  throw Error(`${windowType} is not supported`)\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/window/index.ts",
    "content": "export * from './browserWindow'\nexport * from './window.handlers'\n"
  },
  {
    "path": "redisinsight/desktop/src/lib/window/window.handlers.ts",
    "content": "import { BrowserWindow, app, shell, ipcMain } from 'electron'\nimport {\n  MenuBuilder,\n  getDisplayAppInTrayValue,\n  getIsQuiting,\n  getTray,\n  getTrayInstance,\n  electronStore,\n  windowFactory,\n  WindowType,\n  quitAndInstallUpdate,\n} from 'desktopSrc/lib'\nimport {\n  IpcInvokeEvent,\n  ElectronStorageItem,\n  IpcOnEvent,\n} from 'uiSrc/electron/constants'\n\nexport const initWindowHandlers = (\n  newWindow: BrowserWindow,\n  splash: BrowserWindow | null = null,\n  windows: Map<string, BrowserWindow>,\n  id: string,\n) => {\n  const tray = getTray()\n  const trayInstance = getTrayInstance()\n\n  newWindow.webContents.on('did-finish-load', () => {\n    if (!newWindow) {\n      throw new Error('\"newWindow\" is not defined')\n    }\n\n    // set up windowId to preload.js\n    newWindow.webContents.send(IpcOnEvent.sendWindowId, id)\n\n    const zoomFactor =\n      (electronStore?.get(ElectronStorageItem.zoomFactor) as number) ?? null\n    if (zoomFactor) {\n      newWindow?.webContents.setZoomFactor(zoomFactor)\n    }\n\n    if (!trayInstance?.isDestroyed()) {\n      tray?.updateTooltip(newWindow.webContents.getTitle())\n    }\n\n    if (process.env.START_MINIMIZED) {\n      newWindow.minimize()\n    } else {\n      newWindow?.show()\n      newWindow?.focus()\n      splash?.destroy()\n    }\n  })\n\n  newWindow.on('page-title-updated', () => {\n    if (newWindow && !trayInstance?.isDestroyed()) {\n      tray?.updateTooltip(newWindow.webContents.getTitle())\n      tray?.buildContextMenu()\n    }\n  })\n\n  newWindow.on('close', (event) => {\n    electronStore?.set(ElectronStorageItem.bounds, newWindow.getNormalBounds())\n\n    if (!getIsQuiting() && getDisplayAppInTrayValue() && windows.size === 1) {\n      event.preventDefault()\n      newWindow?.hide()\n      app.dock?.hide()\n    }\n  })\n\n  newWindow.on('closed', () => {\n    if (newWindow && id) {\n      windows.delete(id)\n      // newWindow = null\n    }\n\n    if (!trayInstance?.isDestroyed()) {\n      tray?.buildContextMenu()\n    }\n  })\n\n  newWindow.on('focus', () => {\n    if (newWindow) {\n      const menuBuilder = new MenuBuilder(newWindow)\n      menuBuilder.buildMenu()\n\n      if (!trayInstance?.isDestroyed()) {\n        tray?.updateTooltip(newWindow.webContents.getTitle())\n      }\n    }\n  })\n\n  // Open urls in the user's browser\n  newWindow.webContents.on('new-window', (event, url) => {\n    event.preventDefault()\n    shell.openExternal(url)\n  })\n\n  newWindow.webContents.setWindowOpenHandler((edata: any) => {\n    shell.openExternal(edata.url)\n    return { action: 'deny' }\n  })\n}\n\nexport const initWindowIPCHandlers = () => {\n  ipcMain.handle(IpcInvokeEvent.windowOpen, async (_event, { location }) => {\n    await windowFactory(WindowType.Main, null, {\n      parsedDeepLink: { initialPage: location },\n    })\n  })\n  ipcMain.handle(IpcInvokeEvent.appRestart, async () => {\n    quitAndInstallUpdate()\n  })\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/utils/getAssetPath.ts",
    "content": "import { app } from 'electron'\nimport path from 'path'\n\nexport const getAssetPath = (...paths: string[]): string => {\n  const RESOURCES_PATH = app.isPackaged\n    ? path.join(process.resourcesPath, 'resources')\n    : path.join(__dirname, '../../resources')\n\n  return path.join(RESOURCES_PATH, ...paths)\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/utils/index.ts",
    "content": "export * from './resolveHtmlPath'\nexport * from './wrapErrorSensitiveData'\nexport * from './getAssetPath'\nexport * from './showOrCreateWindow'\nexport * from './window-size'\n"
  },
  {
    "path": "redisinsight/desktop/src/utils/resolveHtmlPath.ts",
    "content": "/* eslint import/prefer-default-export: off */\nimport path from 'path'\nimport { IParsedDeepLink } from 'desktopSrc/lib/app/deep-link.handlers'\n\nexport const resolveHtmlPath = (\n  htmlFileName: string,\n  parsedDeepLink?: IParsedDeepLink,\n) => {\n  let resolved = `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}#/`\n\n  if (parsedDeepLink) {\n    try {\n      if (parsedDeepLink.initialPage) {\n        const initialPage = parsedDeepLink.initialPage.slice(\n          +parsedDeepLink.initialPage.startsWith('/'),\n        )\n        resolved += initialPage\n      }\n\n      const queryParameters = new URLSearchParams([\n        ['from', parsedDeepLink.from || ''],\n        ['target', parsedDeepLink.target || ''],\n      ])\n\n      resolved += `${resolved.indexOf('?') !== -1 ? '&' : '?'}${queryParameters.toString()}`\n    } catch (e) {\n      // todo: log error\n    }\n  }\n\n  return resolved\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/utils/showOrCreateWindow.ts",
    "content": "import { BrowserWindow, app } from 'electron'\nimport { WindowType, getWindows, windowFactory } from 'desktopSrc/lib'\n\nexport const showOrCreateWindow = async () => {\n  const windows = getWindows()\n  if (windows?.size) {\n    windows?.forEach((window: BrowserWindow) => window.show())\n    app.dock?.show()\n  }\n\n  if (!windows?.size) {\n    await windowFactory(WindowType.Main)\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/utils/window-size.ts",
    "content": "import { Rectangle, screen } from 'electron'\n\nexport const getFittedBounds = (bounds: Rectangle): Rectangle | null => {\n  try {\n    const options: any = {}\n    const area = screen.getDisplayMatching(bounds).workArea\n\n    if (\n      bounds.x >= area.x &&\n      bounds.y >= area.y &&\n      bounds.x + bounds.width <= area.x + area.width &&\n      bounds.y + bounds.height <= area.y + area.height\n    ) {\n      options.x = bounds.x\n      options.y = bounds.y\n    } else return null\n\n    // If the saved size is still valid, use it.\n    if (bounds.width <= area.width) {\n      options.width = bounds.width\n    } else return null\n\n    if (bounds.height <= area.height) {\n      options.height = bounds.height\n    } else return null\n\n    return options as Rectangle\n  } catch {\n    return null\n  }\n}\n"
  },
  {
    "path": "redisinsight/desktop/src/utils/wrapErrorSensitiveData.ts",
    "content": "/* eslint import/prefer-default-export: off */\n// Replacing sensitive data inside error message\n// todo: split main.ts file and make proper structure\nexport const wrapErrorMessageSensitiveData = (e: Error) => {\n  const regexp = /(\\/[^\\s]*\\/)|(\\\\[^\\s]*\\\\)/gi\n  e.message = e.message.replace(regexp, (_match, unixPath, winPath): string => {\n    if (unixPath) {\n      return '*****/'\n    }\n    if (winPath) {\n      return '*****\\\\'\n    }\n\n    return _match\n  })\n\n  return e\n}\n"
  },
  {
    "path": "redisinsight/desktop/views/cloud_outh_callback/callback.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Redis Insight</title>\n    <link rel=\"icon\" href=\"favicon.png\" />\n    <link rel=\"stylesheet\" href=\"styles.css\" />\n  </head>\n  <body>\n    <div class=\"container\">\n      <div class=\"content\">\n        <header class=\"header\">\n          <div class=\"logo\">\n            <svg\n              viewBox=\"0 0 172 64\"\n              fill=\"none\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n            >\n              <g clip-path=\"url(#clip0_96_147)\">\n                <path\n                  d=\"M140.162 12.4835C141.779 9.63636 144.425 6.57025 145.38 5.62121C149.79 7.44628 153.905 11.1694 153.317 12.1185C151.627 14.8926 149.055 18.0317 148.099 18.9807C143.69 17.1556 139.575 13.5055 140.162 12.4835ZM171.249 25.0399C170.734 27.522 167.648 30.2961 166.325 30.7342C165.223 28.3981 163.973 27.011 162.797 27.011C161.328 27.011 161.254 28.0331 161.254 29.3471C161.254 31.6832 162.944 36.7934 162.944 42.1226C162.944 47.9628 158.829 52.27 152.509 52.27C146.721 52.27 143.523 48.5014 142.097 42.4807C138.316 49.2103 132.788 52.27 128.551 52.27C121.927 52.27 120.368 47.4064 120.525 42.4733C117.863 47.146 112.74 52.27 107.827 52.27C102.811 52.27 101.039 47.9332 101.446 42.8827C98.4405 48.4432 93.0044 52.27 87.7639 52.27C82.0772 52.27 79.2624 47.7824 80.1733 42.22C76.3476 46.8911 69.2259 52.27 61.8219 52.27C53.3797 52.27 49.7049 47.7478 49.2693 42.0811C45.1947 48.5683 39.7026 52.489 33.1607 52.489C23.7171 52.489 20.3393 44.147 19.8478 37.3234C16.3479 41.9791 12.413 46.8094 7.58606 52.197C7.07163 52.708 6.63068 53 6.11625 53C4.42598 53 0.971936 45.5537 0.751465 42.7796C2.71173 39.7579 18.7228 22.5935 25.396 15.2013C20.8879 16.5526 16.24 19.2475 10.3787 23.4339C9.34982 24.1639 6.4837 17.5207 6.55719 12.4105C13.3183 7.44628 23.6069 4.30716 31.9113 4.30716C43.5228 4.30716 50.2104 10.7314 50.2104 19.6377C50.2104 27.084 43.9637 35.2603 34.8509 35.5523C30.1126 35.6746 27.0758 33.0324 25.5225 29.7696C25.7081 34.8158 28.3493 41.0275 35.4389 41.0275C43.6698 41.0275 47.3443 35.7714 53.5175 28.1061C58.2208 22.3388 63.6591 17.2286 71.5961 17.2286C76.4464 17.2286 79.7535 20.2218 79.7535 24.7479C79.7535 30.2231 73.2863 37.8154 64.247 37.8154C62.7032 37.8154 61.2959 37.6134 60.108 37.214C60.0781 37.4442 60.0581 37.6706 60.0581 37.8884C60.0581 40.4435 61.0135 41.9766 65.2024 41.9766C71.3756 41.9766 77.1813 38.3264 84.2364 29.7851C91.1445 21.3898 96.3623 17.7397 101.874 17.7397C105.595 17.7397 108.419 19.7428 109.663 23.1164C117.05 12.5182 123.317 5.00901 128.624 0C133.842 2.19008 137.59 6.49724 136.561 7.37328C132.666 10.8774 119.659 24.9669 114.514 33.3623C113.192 35.5523 111.942 37.9614 111.942 39.1295C111.942 40.2245 112.604 40.5895 113.339 40.5895C118.189 40.5895 129.727 24.9669 136.267 18.1047C140.383 19.7837 144.572 23.3609 143.543 24.6019C138.105 31.0262 133.989 36.2824 133.989 39.2755C133.989 40.0785 134.283 40.5895 135.386 40.5895C137.443 40.5895 139.354 38.7645 142.514 34.8953C143.176 34.0923 143.984 34.0923 144.498 35.3333C145.895 38.6915 147.952 40.5165 149.569 40.5165C151.48 40.5165 152.435 38.8375 152.435 36.2824C152.435 33.2163 151.774 29.2741 151.774 27.522C151.774 21.6088 156.183 18.1777 161.695 18.1777C165.811 18.1777 169.485 20.1488 171.249 25.0399ZM35.3941 14.3913C32.919 18.234 30.6152 21.8236 28.2879 25.3243C29.5527 26.0299 31.1522 26.573 33.2342 26.573C37.1291 26.573 41.3916 24.4559 41.3916 20.1488C41.3916 17.5349 39.7584 15.1251 35.3941 14.3913ZM61.226 33.9955C61.9983 34.2919 62.9051 34.4573 63.8796 34.4573C69.0974 34.4573 72.6249 30.5152 72.6249 27.8871C72.6249 26.719 71.89 25.916 70.7142 25.916C67.7657 25.916 63.3185 30.0262 61.226 33.9955ZM104.961 28.5441C104.961 27.084 104.152 26.208 102.829 26.208C98.4935 26.208 91.9529 34.3843 91.9529 38.4725C91.9529 39.7865 92.6878 40.6625 94.231 40.6625C99.0079 40.6625 104.961 32.0482 104.961 28.5441Z\"\n                  fill=\"#FF4438\"\n                />\n              </g>\n              <defs>\n                <clipPath id=\"clip0_96_147\">\n                  <rect\n                    width=\"170.497\"\n                    height=\"64\"\n                    fill=\"white\"\n                    transform=\"translate(0.751465)\"\n                  />\n                </clipPath>\n              </defs>\n            </svg>\n          </div>\n        </header>\n        <section class=\"section\">\n          <h1 class=\"title\">Thank you</h1>\n          <h2 class=\"subTitle\">\n            To complete the authentication, click \"Open Redis Insight\"\n          </h2>\n          <div class=\"text\">\n            Click open Redis Insight below, if you don’t see the dialog.\n          </div>\n          <button id=\"open-app\" class=\"button\">Open Redis Insight</button>\n          <div class=\"text\">\n            In case you are not redirected, check that your package supports\n            deep linking and try again.\n            <br />\n            <div style=\"margin-top: 4px\"></div>\n            If the issue persists, manually add your database from\n            <a\n              class=\"link\"\n              href=\"https://cloud.redis.io/#/databases?utm_source=redisinsight&utm_medium=main&utm_campaign=thank_you_page\"\n              target=\"_blank\"\n              >Redis Cloud</a\n            >.\n          </div>\n        </section>\n      </div>\n    </div>\n    <script src=\"./index.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/desktop/views/cloud_outh_callback/index.js",
    "content": "const protocol = 'redisinsight://';\nconst callbackUrl = 'cloud/oauth/callback';\n\nconst openAppButton = document.querySelector('#open-app');\n// this script is used to open the app from the callback url\n// it is also hosted, so changes here won't impact the app\nconst openApp = (forceOpen) => {\n  try {\n    const currentUrl = new URL(window.location.href);\n    const redirectUrl = protocol + callbackUrl + currentUrl.search;\n    const isOpened = window.location.hash === '#success';\n\n    if (forceOpen || !isOpened) {\n      window.location.href = redirectUrl.toString();\n    }\n\n    window.location.hash = '#success';\n  } catch (_e) {\n    //\n  }\n};\n\n// handlers\nopenAppButton.addEventListener('click', () => openApp(true));\n\nopenApp();\n"
  },
  {
    "path": "redisinsight/desktop/views/cloud_outh_callback/styles.css",
    "content": "@font-face {\n  font-family: 'Graphik';\n  src: url('fonts/Graphik-Regular.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 500;\n  src: url('fonts/Graphik-Medium.woff2') format('woff2');\n}\n\nbody {\n  margin: 0;\n  font:\n    normal normal normal 11px/14px 'Graphik',\n    sans-serif;\n}\n\n.container {\n  height: 100vh;\n  width: 100vw;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n}\n\n.content {\n  width: 540px;\n  min-height: 357px;\n}\n\n.header {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 150px;\n  border-radius: 16px 16px 0 0;\n  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhwAAADIBAMAAABR4CVfAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAbUExURQkaIwseKQ0jLRUyQBAoNBEqNhMtOg8mMf////WmRZkAAAABYktHRAiG3pV6AAAAB3RJTUUH6AQDCxwBTNO4SwAAAAFvck5UAc+id5oAACnHSURBVHja7V3Ne5u4uxU4NlsSpsnWNdNmC5aBbaa+027dcu9064Spu3VCW//79z3vK4Hkj8T9yvO7z7WeiccFWUgHJKSjcySlTuEUTuFnwvmxEYPJo6fDo694Efdpxs7PLsb7v393+MHfBlOlzvTeU7XWRYxPnaumVEGlgkYXhxKq6U+v951JtC7H+NSlSjWlGKtW6y6/bUY/G9BJ+nGq+fuNSsfqUu9P7onQMtCD6umYyR7IAMfwABybL02mrjfl5l41NeBIZpN6+RgcN3sv+3lTVyraNB8fVNreEByRvm+n9vQ800vK/qYuVKAf0koNVhnBQd9fz34cjuLpmIfgCO67f7rVhgp4BpCRcpPGBMd8rc6mBxIHHNfdv9xqkyyonCaj6euM4BjkcsTkKonVIKMU4lFOkKrB55zgGNL3Jv5ROML10XAEcVfwYBx0xQtwtv3gFTAoLByDBcHRmH+Z0KdDn7UH6aDooSc41Dw2cFznVOx0bbNNYRQniuFI1wOK2Y4HtxXBMVjSL7tCBQeAkeOcdRU4cDhF8u6xSYf/BzginTdUf6kVIPTDRv81pQqLZ4CO3Cr1QvflqE3hGY5RRnCU9K00Z/WsRd1ouTWZ67ymJojveIvkkLILR2vheFkRHPzNXieKzywcgClZD5bUxowTShzwSBy0IyG1PvaZGi35LB2/4bPvFbU1RbI+kzhzlE6OB1Wqu1tOpy8XBIHWccgRg2/6YZ6ppLomUNLqqpiqqy+AYzC9wLWCumsACI6wqyxRTnC4FXKjp68rdVa+pNZkVE7qRoUbPD3h7GVNpQpec44sHI25b+m4NHD4AXDMx4AIcMwpXrp2TjdvXlFDvtls7DN4ljOgzYdXdHvmd39obnfqNeXh3YZq7TdcvL17oeOgqK60vR7iLwmC+3kWbOYfARm1armiCPQ00meCtgOFpsSlMr3KLByTSZtbOILZFhz4saY7CsjoM+LWgf6GmRpxChd3Fo67CTeKAsd8PxzTySutLBzJ2Icjmkp1owfEPvQztCy4W+0an8ma25q1rSxURK7pyTJAIU1aiDlfGgikuPQluEWLEeXhVK7EcNDDKZXv1dLCQS+/2MJBgBAcwWr1r80j3Za/TYXCvbdwdG1t1MGh5ZUpcCRrwPHPajVWTqAXLdVVC8dgATi+rVZrp2pwErb24HIl4z5Y4nO0sG1NDweeoGEGUGylk/gCQQ+HkmqYh7l5s1Rc0Vao7eG8ryzn55y0wNGeExwh+g4m8KPScG9iJVWL4Qh0yc3xNzRFAsetpCNwDJaAo3E6HgxHfh6rDo5RBjjmWi96sPB92Pcm2phuJoo5zAZ8/wCXDwcKH6KO93AsLRycrx6OgurhvQeHuvqbHqs/5IEQOJxyNyq5l8qyBQel88DHLBwq/NLQFRvdvb2ThS0BCjzMCI6x05RKgTN82qY0yv2mdPCZLjOm1r7/TXJDb2UXjuRIOEZ74BAAUFlCC8c157u967Pow4HXH5piHw7EibmyNAaOaIyndDCL+5x7cIS5vGjne+BAtvkVO5MXbWrhWPJrNM37H4wyijzg4qHYZzcc34PjLOPKUu2rLD4ceLypzdNowwwc1FTRr2Iniz4cZ3lFzZaKfDjoflCr1jpNKSVCT13gpOPDoWbUDcuoTnlwSOvL3bCYMtykY3yvzS+R53YclE7u8ABxw2ea0sVotymdcVPqwBGh6fXgCL/NHvB1OqHfttNJiRdtSU9iMr3wb5iBw3TSCTPqPheT2sJBh+lzWODtQ8fbBt1winlWvEwzLx2Bw3TS6eWITjrF3/N0oGNe4f61NXfSG1MMpSfXJb0XqMp0iMy4/X5zrfH6pF5DoCevG2qu2ndUSzffZhSz5uMOHBTzTz01ENDggW6A5n4TtYkVf76fYjxFo4aAj+zCYYZwdGkewuUrOUnR+W0+R2NDnZVpzQ1ebo7swmGGcGO6HIZwxb62wwzhlhQZY77SPlfUO6QccvvfJd0UcvyWO1d4UegSY4oExUDMMbpht8qDg+JflQYCb6gWSCc93j7yeNgTh98a3vHz+OmEDsVxjx/67mUo7rO1G2c3u8fk7RRO4RRO4ekQvHS+M/d5/qPM4o+H41nVQ0zHLwr09pnSa3KKzkiI73TkGGaRQ62PjHgYBxnhHeQYnSA9HLcT0xx9FZ0fFW8NjoP6JrMCcOD7ml7bg/KoH9OQ+VfB0WRPR5VMtU7BjoZjVB0HR8YcB/VcG8t6UL6mwXE/VsP8V8FxFR8LRzj+ATjAQR4T8nAGIiCo2jjKw0L4jtsg24pmKqzpxcT2Y7B04Qj6mIH8/2LnehdOOucOHMo57rYO587P3EdW4uzCYS4f9xnijNZXLhxOj/OcI3VXzDF8CadBNbgR+gd/92phf/mJwFoa3pRaCrQyVQvyhnmoNO7hSJkLCMC2BpXOm4p5U3sdcFeNPfKau+1znX9CL1xzmzVW5vjovTOf0uppS3FeU0cbXX7ubt8o8KD0IFPnWtuphihDbtA9H3NzuKCzH7jvzfReEXVwGK50qMvLxei/9UZnIHJvt+AYZRHTP1uh5JGu8KZJhWHeF32HgXWE0YJWDhxvwZJSnGIa6Gv9VxNH5cvaPmYY6ZYKR3KFWZVcjXj4d/W1oGFesKnHQBizLYPmIe0INIpfq6v600TH0UZvMBhjAqy5TygdOvLgwzG/fwHe8O4FXavVTNImFSq1A4dwpc2HV81yNEvKV5U0n3xyYeGI8kfgoMLQf3UMyiDQMr3wjs87cGTgNSjOYBqAHkxj0Iv2FUVNEj13mGEpVZLhM6U2u+wqCwZwMttCb7WOQ6GYeArTJahKU1l4+EXJzrzKYuBY4XnGAJ9SpxE+Ez43fL6Hg7lSYUnxDCimbZKlScfAAUDC/Gy1cvie/um4RRUrmAkBa9Dxoy4cYxStkNQYDlQQM94FUpQ3HJkzA/GNi+LDwX/gPrpiplLFULaxBwdVqngfHA2OU4YRCzRXV+ndpwNnDau6pCegYoo48+FQJeAYau3Pqgkcwptqrr2B1ydx4KCGA+nIJ+ConXkQzGAuOPt0nI+sduAATKACe6IJ9NG84w0dONLy371wJPrTmNsUahFGSyefbtvBcCwdOLQwERxvauCY903pDhxUxamJKsGnbsHRZ2gPHBR/0+MGpudXwRFc19U+OPj4CLTQ+nE4Bi4cM47fnR2CT002OepB5MOxYp7uijkbtBjx40+HX1mU6snFedxw0cysyqv4QGUZe3DEUlm24QhuVMfH9nC0MY7XzIkG6omnI0N7IXAEbpHRjoBPxcQwumE+HDW3Z5QtSpw+MSNh4HjYC0fDTWnVNaXdnHtyX8jcTclsmLY8q9uULnmC2oFjIJPbO3CgcD4cIbOqyFvLOaQrGTik27YNB7OqBg4kLNws7sD0Wo8pznCWq7a6Lnw4krvrFV605/M1ZsbaZfCFOVFq8WPwpv2rTuBIppNP5bnAcVZM+h714A5cNh+JyslreqHOJnOqgUGxoTZ4U398oBZqIsxoBwfi1zdX9Ud+lDU90cGm/UzX1y+vmMDedLMV9Ft6qzVvrqhszIlGbcUn8aINwJWuXTiUfvNKTw0c6d2kY2plCFfxKIcalVsfDjr7Ai9X9HdY3BKa5pHhkEkmFw6KuajvBQ7qRPUsaaSBvxxhBjSodd4ukcaS+kRMV6boRLlwoBuWZK3m39I/MjtNfSkzUGk/Mkt0RVcc9lxpYprHAQ9K+8bSwHGpy6vSwBG6tLBLIO6SiS756J2N1d7wBEsqR3b51IPx91/lYMzdjB4iBNwUjqGFT+EUTuEUvjM8v67058N3cKjj70j19+tKvZD4U9U/EuS1nBzBd6UMmcsE+1Oh++H4zbpSP1r7q+CYPh3TwOHQasfA8Rt0pQcvVzyVoWPhOEY5KnA4xdsHh9Q6ifm7dKX288I5BJYyzN0MOQpXJYzGbovgsqEXDhzuWbePGDgn0ng7pnf18bPpSs3na7Q+c51wjxkty2DZZ2j0F7Okrc7fqnlaQK5+2UvGMPBL1+aI6Edfo2M+kg4++vrmeJi3fef9UheD9Ujf0uggkg5+azWmCyOJQLd9dIN/PJuulD7nGMK+pCHcdfP5BUBEAdJxD8fgn4c05xQaVb9p7tqFaNINWBnIARHAKA39KLjVOqarYBi5aTMlOlEVFncvOtqJUmjWYVve18uABoobcDeAI/1Ag0A5YkYxyR0etufRlQ75P3ClNBasx+BU1autx5UGW1QZE2iH6TyIoGHeD+IxJNcKR+oYisF2zNxqbCsL86KiK9VGdYqrT1kSZXhTqSxMCbeSO1zdwCEnn0dXKqxSKpmn/153tdiBI2OBZozf1nQvRktwG52PgG77jNmOAcvgRqwlnHtwCAOKYthfGXUpnQBv6sKx7q5un44bA8dz6Er5s5Xa67Z9LhxLnKqVfAKORERMEho8GpBGDpaiGkbhUg8OEVKGuQPHghWmht1x4BiBVfXgONOrDo7frSuVz1bY00NwLCwcjYXjoyOGa8EbopgdHOOfgENdvdU+HHTEwvHbdaX0iSnxmN+Bj8Ih83Wmstw4EekltFY4kjDxO1pbEa0DB44vPTiWMi+xA8eDxOGrz/jaMt54Hl0pZSsUVXDZJSTT1NtwJHjlGTigDZ3b06OM2l8caeMh60pZjerBYXSlDhzQodf74CgdOEoWt3Ob/Uy60sh8Tl7nig5ztsDvXeFVF7twUArfmrXAEejJq458DqclWiIcCVg/GupJ2rzH4IEnK6uN0ZW6cEBdWt9Z3jT9iJhfoTFtP1zAO5V8fEBf87peoMDPpytt+TRbAI1hgUc0qaMNFTjQhUsygQOEZ19feEZMDIKiH6X+4iAXxjSQdPi4CwerS4uBmU0beBpTHBrhVTBkxWrYzZGpZ9CVup/m+IFEj2Fb3e68l6ED8R/PuknzxJiewimcwi8MTzCgx7c4k7j7Gro/cxz33vfvzujxvx0fGY9eO3fy9skCXRl16UFdaWGmI3dDykxqyhqQmo16nnVD/4XhMrvv5aXL7nvlue+/I8jrPT2CH9314Lsk0C4cm6/0Jt98LTbrYFaKuvTiIOdbGFXYHjg+fqlz6lZBwVVTl7ZUI1aC2cvcwpIi7ntmPVLqutZGJ/bDcBzBj+568B+Fw8zmy1ildtSlh+BQfVVyqw2mrK1eq6aedgnnWtgLGuJ/pAuvrTYsXRIc3NH+YThcjenjcLhM6lNwiCCKNYZxlAeFJ3oR37rlL3uQcSS9i72LaAvHZaZKpnj6hQbUV3EuzmOMExpx37N5b94l8oSzPu5zsto5u48f7eFwY3pw8CHHuV/KmEbgYHVpZUhAgKqnDUQYUJSiS/6+oI4uIKHvPCFTdvp+D46rnNLlb/b0EA+VwMGCuZjd92IPtNljteiZoxZFfFm5Y8wd8zV3zIUfha2aVQ1D9uPn886nS/Fn1MVnXem2B3/tWcbEfW90pdJJR5+9g4PVpU6FDL6WH6i9EkUpfZYFDcwwaBtW50iTcFr2cASFheO8MnD4AcXT/NS0Mbvv/XunH6gdCTebjX0GmR+LhTGlYdsryEvu501sFKUb6EPAmxbMj5Z9Omk9Fl2p58FPP/xRGX7UxGT3vdGVttUVUptM4McUOFhd6rZPYYEWoWZ1IsbXlvCBaRW3Rr2wDWH6RphUhiMu98PxAfoeC8d8C45BbirXmX0NoBaXPKhveFA/jy0/KpUFeidEno9D8AYmLcSkx8nwo71x1PCSbmUxRDHrSqVeazOkZThYXTp11KWoNXesKGUZXNDBYZ/MPzs4DI8ncMwBR7BauXXcuu8tHMkYcPyzWsX9s8OZh/1V7t7MiHOhKKTPAfPutQeHaACRfcuciBnZ6kodOCw/6sABolh0peYpKM87qbxRl06HfcdDIrGiNLCTCoAj1Ks3ipnUrrLcy7hQ4EjGgENr/62RvEccC8dgATiaribbwb4z29rgHrOMbtHzo60Pxw1ejWHew8F287GVyjlwDAw/6sCRKUsUGzFd2Zcb04xXoi714WBFqQeHukBv5YrXK1Ie5gLHYFFuaftsRu3KLul4lPlNaSrMqNvNa2GxNnCMfxoOdf223AcH60r3wmHUpVtwiKJ05lSWP7i3kn7ui+rDMcrkRbsPDkxS04t2HOXyorV13qyq0S9CgdkNTPegeDdc7DFA2VtZ8n2VZQuOB+W/yQwcMg9rK4sHh1WX+nDMWVHaUINq4cDlMs9Z78MR5qVhPXfhoAIEpbjvleU7uXAZi2TdAcIoa8SJL8siMZm+1ZSKH9+FY2Sa0h04yn4ywoXD9ERLBw7ppIu6NK0mK5OngA4rZlLnC+ZTC+6GQ2N67t7FDg7TSadHqYRFo/VIcwPHmcY7iN33it33JnsB60pHnx3pdjQt7HF6ff5ZSvz3mMBgjanwo3TchYP50VvLjzoe/PbDBaBMP1otbPiN44iutJ1OOjhkCIeLirrUwBFJY0idrhnTobcFN3gZjmz55QQOM4SLoYehkVzV7oHDDOHgvsf3WXe3WG860m4DzIRmwh2wSy1T4syPtpJPxHwh09QOJ5/o4qyyulLfgy8tqu3XjOSdIWPWSB8YLexRl+7KNY8Z5h+jCj3Gre+xrY+m9ng6hzK9SxSfwimcwin8R4bf4bL/jpjOQgNjhzj1FiB4+XQ6h4WjR+Mgvd8jNKbGOURD7i4cvjr3PffyingBv5fX8Bq+Eeq6yfI3EkZlwkYTUMQ8NQnryjA//KJ1k65+GRyLY+Fw/fhPwLGXKw2Lzd865k5aHFWsHphjtUEPjpIp4nZ6peNwNgMcKfMdj4dR/qvgOMJlb+BwYz4OR8+VutWGxVBLI3HIZxjRFP3yXCrI4Rsv2PPGTEdYsbDIY8S9pTx7pSlb2PpI/clA/rGnU8SfoviMHTiUc3yPy/68h8ON6cERupfvVnfiI2HZa2cBh1WKRXlDcJzljtstyMIFw3GWS8ywgmLEsNwQVfJs/nvDgIZQmhI4ZyxpOu8zNL9EV51iPlA6OteVVYtyMEZQ0ZWy155SW1Qsj4ATl8Hn48mDu7IxnPitvqT62/nx1wpd8pLnZboLpPoVvg9Nhx0SBOFKR5zaXHc0tg8HOAJ8G3Z3ZRGMGY6IOQ6Qw1i/0gzwKx5iJncXJbvsqdJ91W+wZsgIUqci6OGo/2dS83ofZR7qa/2g2UdvbxKNHMOKFaVLaEPnzLAWlbr6OuPhFobl4sFP6n5uBfFX6qqp/tQss4Effw44NMtjWFoj4WoOJ77SD694OIcl1YQrre8vkdof3SDKryyyhOP2M4xiseWcHt6wgmTTg4OGUeLZpl+CH4W98C/7OwvHAmcxmYanrFRz9tHbgtFwHu3RGvxjypbRmgbZVVdZMFAzHvzefWqd+O0N4DRHMdDDunxTv7IkcOIzYyrq0oWxyhYmVmAnKMJico0HRuAYLPfCUU4mzY2F4yzbfjpuTN2lfFBZg04h58IRK5l2OIPkslAtcxt2EmDEbzRhwDBk/4SqE/hwiAcfjVrRp8lVLFaXHhzWgOvCQaNdZkyFN00sHE1s2ihLQ4emigkcAGSUjVarW+WEgKcmLBxBBTiGqw6OM726N6Sh3/a5cPBvc/MJOLThnREipjAKjifkzExSd+AQ0zmKax9sHFlZjsaBQ7XisnfhoBsuK5WyZnBg4UjLf3Hmuq8sM6F/BY6gAhyJ9lf/CZgitnCoEnCgdTBwQGYZi7r0O+AoWUVqIvL8g8BR/jwcwZfaCr49OEAOxR4c7LsPWt0RlXZmROBQ5XZT2hfLNqWqJjimNtMEB0+Rmew8Cod49g0ceApie7pBGU1lURBkl8K5upWFq4cLB9iycg8c4bo3i3hwLO1L1laW4AZXDHsOexuOuXnR7oED2RsBjvRFbi5e8pKdDVPSDatLTf7FBbMNBwsyLRx4m3Qdw/aCVZ1oiVMuDLUUw62mVDz4DhypKFN34MC93AcHM6PxyGlK8aPGm6HdgiNJlyhEugcOZbphavA/uZl2aj9cN0uIKhvYFehVGn4tuAJwVdrQy89WB4GjnV6XxYXAMaom7JvgMPgb94B1pfLJfvwYvD21wZv63b3x4LtwUMy5Xl8371BW6jkTmpv55wdq6l6+QqlKa52nFy078XmlUqhL9cJwpc39ld93FThMJ5366PTmbz5cl/vgwAIEWD97VPACBIqnnZOloQtZSzo0snvWDbuNpcCBeV9t4AhqR9AyQqfIHBF1qdbLOSQMsoUEaE/24LtwsBM/a8yiznPqsA3F15+a1bK1hds48cV9z2uX9lzp7R44zBCO7o2Y/Df74JAhXIW5azuEc0WVR7nsnePBoU760S77g5zrMbzp4zEPZfrQ5U+60lM4hVP4peE5PfhH7+rknX2u8Fs9+Du7Ovm60rnd1Wnc7eq0ltXAWBWWH7OoKULUvZ1/CRy/zYPf7er07qHTlb7sdaV1Tn0K2dUplF2dalAP0HrNsFDpMYuaIgw+/VI4fpsHX3SlRmWxqytNxinbeWnMbnZ1+u+M4BBtmBotkvi4MqSd9feHEOCr8Mdv9OAbOPxdnXxd6SBOjZlvzBsejQfvK6xCuORfDteDbTgOrE6qhy4c5/3Jc6Fkx/sRwMnn8uDvwrGrKz2L/7BwmK1IFtTG8P4cMGeNhzads6UwRtaPj1bmHatRE9aE9nCEU7bcw4l/M/pb/wnjfQNamAMG9TTaFV0pFfjZPPg9HF1l2dGVmkdkCRLAbFSzqAHHzt1EcemXwpI20JVGbTnh9pZpYweOsjL7PDWLQZUWr2iodtft7QTXK1XXlnWlaYXX5LPt7aSsrtTZ1WkfHO6uTgsC4zAczJLyyqMxRKkAMlrb8wYO6GfH4BkIjuVZHkwBQTfnggUNb3i2JTPzLM+2t5Pa2dVpS1dq4HB3dVowION+VycPDl61lO3EN2CEhgv3vIGDt73iBRcprSgLpqBfuvN4xOKwK7x6vr2dGI733q5OW7pSA8ets6vTYpQBjn5XJw8OZknNPk+DxdZ5A8cUpnOW6Bo4IlnN1FxsQXcRerlwapj0Z/LgCxzr7qaoXV2pySFHRvOJXQOjXLjV7vF24WCW1OzzdDwczuqkPAW3B47f7sHfB8e2rtSFw7xoF8FMXrR2V6e+uHUc3LAsecmv0cfhWPqVpROARjmm7oUNdyrLb/fg74NjW1fqwmG6YQv097gb5sERsVAfBeDV7pGWgUMmsbfhYCe+gQP5HCy7TOOrvF+4IM+6t5PAYTrpO7pSFw6zqxM9ni0vcNPNfSo5O0npmdLMkjbw3V+1nzltvGjDzbfKVgeBg1czvTNwtB8m/RUbXlV4OtFGV/qsezsJHM6uTr6u1IHDDOEWFBkuwtJvO+jIVLphS8OVplZFmvOsdtdYGjgSXV4VBo7IXd4aA0kW0qpn9uAfCkdpT+P9v9rlTX9sddI9RPEpnMIpnMLPhh9rU46nMn+HxvQJi/H46RR2daXWcPJ9OEivJzjCTL8rqgynByOzVTDbe0FWjvILNcacY4KFvjsWYzugE769W1XgLD1wWFcqcAx/OxzLY+FwJbfOBWebt5q6W5iUHs148T5ehfAROLYobqMr3XwrNuvDulKBw13Q83g4jliddI/G9Ak4oi6mW214oay1WScsh7WUWY/1YTi2VzMVXanR//i6UqcDlri/chWiaue4/GrswOGedTPvXMZT0nSG1a1Ddi3SPjM4HPX+doaDR6mAI8OuDXZNOVvaPnNBbPdecXMlulILh6MrDRvZC05fLgPxSHVcKR1fXXaGeF67gzvmsv7ojH/7kDPHsZAlpwxXml71JIaoS5sSa5S23G3vNabKaEkliLrUMKCv9SxZB47GlDr+HW/iw9GYdebsshIj/VEXZtd7fF5nrge/hyPq4Oh0pUwRUiRsh5SpzfzzJjZcacLHm0+TLomG8wE//g0P3pbMrebqmkZMY8OMiq40/cfZ1amalJVZoxQ7OVmNaVC+bJdmbycJV/XsWnhNaH0f2prqNhujtQwCX3R3BnCkNxaONBY4usrRaghgW3bl13cvisz14PdwjLrK0ulKmR8dZRBtoxE3bQd+xMfh/+7W8mj5O3Z4ylFPo6loTLvKwv520ZVm/Q4gWOKu4jVKG08YFeXdQhk2pDSA5U2aG9nnaS37lWO6TwQ9/1g4qgmYbgPH4MaHg3nTTGhB3ucp8z34nBTrSi0cna5UPNW3qEYDDw7z18iO2HKVJTIsUwuoGJ/tE+fAIcQPtGH2PqwkrZqXa3XgMM5lDw4apI55kQpmwFIDhyqscvSthUOL4kfgGGWA43K1WvQZPVuwPI5XLbRPgfXgcxHNshgCR6crNZyYlXD3cITCmTf9LlCIUPPCwATGzBYl9OEQIWUa93DgSNUtV93DQc3Jh3gHDuvEZ2GdhcNoTP/sJoSHU1GOChxRDjhShwHFoyI7PPEaDhYO48GXp0NSEDg6Xen3wBGCDeRirhwy8YfhCDDjuwcOduJ7cFBjQ3Vnrjui0mpEBY6g8ptSAwfv8MSbwFs4jAefi2iQNXBYXanIZm/xi0OVpX9PlXgZuZWlOlRZHDjKrrL4cERjbhF24DAUkVNZApZlRo5y1IdDleZF68EhOzwNM6eyPPRL32zBYXWlTBGOeBmC3aZ0mPtwNEOZPCYg5/yp1XZTanSlDhxGXboDB9z01R44xImPotmmFFleeX7/LTjmyYJ3tPfg4F3vb3ADu6a07Fn7LTisrpQpQnp9QHhZscZZdVwpH3fhSNIls6r1Ajsz0dXT6aQsUFxWjsIXL4pSFw5Wl47NGqUhdnKKNvXHe6xg+hqkNo5IuK6hLoVmtOJ9npo1dnJijemfft9V4DCddOqjz7mTbtsO477nHZ64YJnvwXfgkE66qysVlpTaakzJnsm66j1X6sExQP9IumGiH4UVByslaVkiDeuG8nEXDjj324VdwplniMRRYIQuTTeAqWVyyYhbdJGsI9GYequW9nCYIVxGCfIQ7tJk1Mjxedd7RKJumOfBd+CQIZynKz1kQH/csK4O/GqXHHgm5eihXe+Pp39PXOkpnMIp/AeHX8uMfgd76vrunZ/t+u7Pj6BSf959Hx3vvpd+RccKKO7Y7wQ2mmf7Eogc3/1QfPdDx9gxqJLc8d3n7LsfZWaC8kwP9l3Nx/bn3fcWjptj4XCFo4fg2L+8aeT47rGcE0yb/S4talCmxndP/c67P+C7rwCH6MRGejBTT4TBz7vvDRxHcKgGDtd9fwiO/b77yBJCGTpplfHdY9QjUfPhQjZmtb77aYnBn6w2GOTRss+KpB87l8BH+tKF46I/ebz7Psq3Uzjovp/uxKycSId8930jgEvZUe0wL5VsddMN6oIsWpudiCPx3U/n/CmMaR4aOG56931uVyfddd+/ZcY06tz30233Pfq8zJ527vv3lZpzZ/9sy33f4TEX933iuu9xZy45JpvFUn2Jawl7SscxgcBVj1c2pV5957v34Zgb333vrr6hh0rgQPGxGAEEdKL7obOBeYwWnfv+Wtz3GBbuuu8/YTiXVn/Aff8nu+9nk/rGlov9kqPCuO/rzLjvr3lP3HADcUnvvrc5HLFU5qqB7vPqSyHu+zHGOL37/qr+F4sCsPRFtKFqw6u3Nfe8TuOLTqiHgkEDInDAd7+7PjHvw8KaU+gEB7Ltux94S6M2NrpSqm8spNxx3zNLqjvLaL3tvh917nt+JrGyadVVFpY+HXTfr1n0JpUFQla473NbWehnYLnN0pQsieIKUxmze2C5z2hm2FOGA777PXAUE7qrFo5hthcO8TSztJKzcsB9bxa7sHBA82Nb40HnvpdVKD9ZQ7EDh2xC47rvzdbexn3fw2Hd9wLHWI3GssOTtRIzHKVlT+3uLZE4MQ0cwwxwXK5WSw8OHklbOMIccGx583lLo5dHuO8j5s0iC8ch931pixL4cPR2c/sru94HF8uBQ83Lz3EHB77xDk9r5sYsHKllT7vKUhn2lOEIpoAj0b6vJSgQx8KhZoBj4HvzF7LJ0dPu+y04ttz3jee+D34KDuO+d+AQ3/3YgUN89/Ped28fe4EDgkC/KbXZEq6E4WikskQ+HEe670WlPHQrS2xPNyijuBdQ7P8y83UuHLKflwtHX1l8OMK11JceDtnhSXZ1EjiCm23f/RYcDb1oM7W13LsU0r5oVXvJczFDH46GGdLVU+77yHXf1/vc92M0pS3TvM1OU9q7722qree+7+Gw7vsejqHnuwccUb69MOwWHC11tGbbcoh+0QF0w1TyLZMieXCk1n3f7Lrv7XShwNHeXZczA8dg6rjvk7e4E8yeeu77SNz3zbt7ozd14RjAfT/u3feY1px/9N33VzV894Ge/DnjVUjZdw86V7/c8t0LHKaTTn30Ofvui31wzO+uWaI7KjNeUMCHo1OU9u577ndoZxkigaN339f73ffcfDzlvrfFYPf90rrvazHKm1lsVAL48Y3vXrhS13d/uaXZN3CYIZz47l8wS7oLB3Oo4ZQNhPT9zdbL9v+A+/5Xsae7Fz6tUXoKp3AKvy2cPPhdlk4e/G04ntWDf3/y4J88+BaOkwf/5ME/efBPHvz/sx78Tip78uCfPPgnD/4WHMuTB9+B4+TBX7twnDz4mQfH/ysP/hHc58mDfwqncAq/N3gyS+Y+j5FZPlfm4l8f89HS8XsnoL+I1VnsqX1aZmlC/SO7lB8XJAv7PfhbmeBPd+q5PRiXNWT7S4fZzrVs5BzMClat0Pc1VKelOioExe+GY78Hfy8czsj5CTj2l05vvirZb2UZVI1lPej60yBXR4Vh/rvh6D34T8Hhuuwfh8OhgsfOKYDE1vOzPKjamIb8hfAdt0G2lUpPJiLE9mOwdOEI+piB/P9iJwk5G+9UdNe5f+7A4f7W5TgvnEvWOzE9OHw/fu7FbBwqmOEQp3NQDW6EAcHfPTRSEj4xMSm8KbUUaGWqFhQOb7ybxj0cKXMBAdjWoNJ5UxktqYTk2ihBb/9Vn+ZVa59X7FxMg8XX3JEX7rPV+VujK+08+HT87Lbp5ylSXdHAX29ooDASGYR4yM44pu5ZmJH+4Pvxc967lb8v4PTvqWAPDgilwt06UvLwUHjTpIK1/Yu+w2YWEUqnlQPHW7CkUJROA32t/2riqHSc+HMoSmlotyqUfqPvapMLjFA0drdvcyX73YNtbeyuTuzBl+PD4sPrbg9lik/pgx8dhzRQfEBMXGp+/2IKjrbb4DdsKTnwFNCnY6GBXF19RTGTDyyYuWi6WS/AsbBwYNefg3DQ0CvhDdvPeFqEpxfe8XkHjgy8BsUZTCGHgjDuxvFL5oZDHcKD28Z2qC9bSSWc7CA3YstId5UFxlGZZxlWvQCKHfrczmFcX9uUmKf45FeWEZwJvFrHmneHMzpjROFYwWuTExr1ToyRlOAAIGF+tlo5fE//dNyiBhayOgA1Nh0/6sIxRkUrJDWGA1MIKwvHwnCogQ8HNVVD2dq9ZYr4ddzGkmoPB34rYls7I2NmZwiLkQ+H2efJhSNTEd/zUWY3rQk9P/63Dg6MaC0cqgQcQ3fw28MhvKnIo7y97Fw4IJZlCaWFo3a2YRYZFJ5HHw76B5j92EzBKasxdOGQjd8NIWivhb+BJOLAkYjL3oWDaqMZ8ncLVACOkcSMnMrCuz1NDRzzvindgYPqG/2qZO2pD0ev39sDx6bfpBtzFHvh4OUctuFofhQOZk/3wNH58Ts4WE6LRVNip6jKyPgx/bxh63nkw7Hi61zxE4sWI3786fAri+pVZgLHbmWhstaxKZ5UFpFre5UFfzcuHDhS74EjuHEoawcOkMMBTyd0leWB89T85d15KVCCCcbBZ+mG+XDUPCND2aBk25hJSgPHw144Gm5Kq64ptbp1gYNS2GpK6dVS2vcLeoA1fhXuNqV17MIB/77eB0clUGzDAfl8smBXvoWj5DzFfUQDB7Y3GlM6sEy11XXhw5HcXa/woj2fr1lLugy+MCfK01vqeqM7KbbAwYrSc4FDdnJy4RjMJv/osQfHaMqjpMlrLHCDObphMfnWrEVXaj34fNyBgxnW95YfreWVzPs8vblCqdp3tuNp/Pjth4n48anPsflq/Pg+R2vgkCFcxWY5kIY+HHT2Bd6CmA9mcUtomkeGo3E6PAIHxVzU9wKHaEldONCda5ceHAF3ilLugImidK5zLGE1F9mn8eCvPTgwDT7I7QImqRbbH9aUEDtg0r0QBvJuEPf9Ge/w1Pnxt6QYpiCPb29k+sfHa08fZUmf0JJ+x073B9I5fqf7EzP6ZPhfdo2iV9Ih7EgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjQtMDQtMDNUMTE6MjY6MzIrMDA6MDCcZqtdAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI0LTA0LTAzVDExOjI2OjMyKzAwOjAw7TsT4QAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNC0wNC0wM1QxMToyODowMSswMDowMBuAH/MAAAAASUVORK5CYII=');\n}\n\n.logo {\n  width: 170px;\n}\n\n.section {\n  display: flex;\n  text-align: center;\n  align-items: center;\n  flex-direction: column;\n  line-height: normal;\n  color: #000;\n  padding: 50px 24px;\n  border-radius: 0 0 16px 16px;\n  box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);\n}\n\n.title {\n  font-size: 28px;\n  font-weight: 500;\n  margin: 0;\n}\n\n.subTitle {\n  font-size: 16px;\n  padding-top: 20px;\n  font-weight: 400;\n  margin: 0;\n}\n\n.text {\n  font-size: 12px;\n  font-weight: 400;\n  margin-top: 20px;\n  color: #527298;\n}\n\n.link {\n  color: #3163d8;\n}\n\n.button {\n  display: flex;\n  padding: 6px 12px;\n  margin-top: 16px;\n  min-height: 38px;\n\n  justify-content: center;\n  align-items: center;\n\n  border-radius: 4px;\n  background: #465282;\n  box-shadow: 4px 4px 20px 0px rgba(0, 0, 0, 0.2);\n\n  color: #fff;\n  font-size: 14px;\n  font-weight: 400;\n\n  border: 0;\n  outline: 0;\n\n  cursor: pointer;\n}\n"
  },
  {
    "path": "redisinsight/desktop/vite.main.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport { builtinModules } from 'module'\nimport path from 'path'\n\nconst apiDistPath = path.resolve(__dirname, '../api/dist/src')\n\nexport default defineConfig({\n  plugins: [\n    {\n      name: 'resolve-imports',\n      enforce: 'pre',\n      resolveId(source) {\n        if (source.startsWith('desktopSrc/')) {\n          const relativePath = source.replace('desktopSrc/', '')\n          return path.join(__dirname, 'src', relativePath)\n        }\n        if (source.startsWith('apiSrc/') || source.includes('api/dist/src/')) {\n          const modulePath = source.includes('apiSrc/')\n            ? source.replace('apiSrc/', '')\n            : source.split('api/dist/src/')[1]\n\n          return {\n            id: path.join(apiDistPath, modulePath),\n            external: 'absolute',\n          }\n        }\n        return null\n      },\n    },\n  ],\n  build: {\n    emptyOutDir: false,\n    outDir: 'dist',\n    lib: {\n      entry: path.resolve(__dirname, 'index.ts'),\n      formats: ['cjs'],\n      fileName: () => 'index.js',\n    },\n    rollupOptions: {\n      external: [\n        'electron',\n        'ts-node',\n        ...builtinModules,\n        ...builtinModules.map((m) => `node:${m}`),\n        /^@nestjs\\/.*/,\n        /^src\\//,\n        (id) => id.startsWith(apiDistPath),\n      ],\n      output: {\n        format: 'cjs',\n        entryFileNames: '[name].js',\n        interop: 'auto',\n        preserveModules: true,\n        preserveModulesRoot: path.resolve(__dirname),\n      },\n    },\n  },\n  resolve: {\n    alias: {\n      desktopSrc: path.resolve(__dirname, 'src'),\n      uiSrc: path.resolve(__dirname, '../ui/src'),\n    },\n  },\n})\n"
  },
  {
    "path": "redisinsight/desktop/vite.preload.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport path from 'path'\n\nexport default defineConfig({\n  build: {\n    emptyOutDir: false,\n    outDir: '../desktop/dist',\n    lib: {\n      entry: path.join(__dirname, '../desktop/preload.ts'),\n      formats: ['cjs'],\n      fileName: () => 'preload.js',\n    },\n    rollupOptions: {\n      external: ['electron'],\n    },\n  },\n  resolve: {\n    alias: {\n      desktopSrc: path.resolve(__dirname, 'src'),\n      uiSrc: path.resolve(__dirname, '../ui/src'),\n    },\n  },\n})\n"
  },
  {
    "path": "redisinsight/desktop/vite.renderer.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport path from 'path'\n\nexport default defineConfig({\n  plugins: [react()],\n  base: './',\n  build: {\n    emptyOutDir: false,\n    outDir: 'dist/renderer',\n    rollupOptions: {\n      input: {\n        index: path.join(__dirname, './src/index.html'),\n      },\n      output: {\n        format: 'es',\n        chunkFileNames: '[name].[hash].js',\n        entryFileNames: '[name].[hash].js',\n      },\n    },\n    commonjsOptions: {\n      include: [/node_modules/],\n      transformMixedEsModules: true,\n    },\n  },\n  resolve: {\n    alias: {\n      uiSrc: path.resolve(__dirname, '../ui/src'),\n      apiSrc: path.resolve(__dirname, '../api/src'),\n    },\n  },\n  optimizeDeps: {\n    include: ['electron'],\n  },\n})\n"
  },
  {
    "path": "redisinsight/package.json",
    "content": "{\n  \"name\": \"redisinsight\",\n  \"appName\": \"Redis Insight\",\n  \"productName\": \"RedisInsight\",\n  \"private\": true,\n  \"version\": \"3.2.0\",\n  \"description\": \"Redis Insight\",\n  \"main\": \"./dist/main/main.js\",\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"scripts\": {\n    \"postinstall\": \"npx patch-package\"\n  },\n  \"resolutions\": {\n    \"**/semver\": \"^7.5.2\",\n    \"sqlite3/**/tar\": \"^6.2.1\",\n    \"**/cpu-features\": \"file:./api/stubs/cpu-features\"\n  },\n  \"dependencies\": {\n    \"keytar\": \"^7.9.0\",\n    \"sqlite3\": \"5.1.7\",\n    \"tunnel-ssh\": \"^5.1.2\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/patches/sqlite3+5.1.7.patch",
    "content": "diff --git a/node_modules/sqlite3/package.json b/node_modules/sqlite3/package.json\nindex ab413ff..a54b830 100644\n--- a/node_modules/sqlite3/package.json\n+++ b/node_modules/sqlite3/package.json\n@@ -9,7 +9,6 @@\n   },\n   \"binary\": {\n     \"napi_versions\": [\n-      3,\n       6\n     ]\n   },\n"
  },
  {
    "path": "redisinsight/ui/.eslintignore",
    "content": "dist\n"
  },
  {
    "path": "redisinsight/ui/README.md",
    "content": ""
  },
  {
    "path": "redisinsight/ui/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" dir=\"ltr\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"\n    />\n    <title>Redis Insight</title>\n    <link rel=\"icon\" href=\"/favicon-180x180.png\" sizes=\"180x180\" />\n    <link rel=\"icon\" href=\"/favicon-48x48.ico\" />\n    <link rel=\"icon\" href=\"/favicon-32x32.png\" sizes=\"32x32\" />\n    <link rel=\"icon\" href=\"/favicon-16x16.png\" sizes=\"16x16\" />\n    <script>\n      var RIPROXYPATH = '__RIPROXYPATH__';\n      // for vite RIPROXYPATH is a \"base\" field in the config\n      // in the vite base must starts with '/', for app we should remove the first '/'\n      window.__RI_PROXY_PATH__ = RIPROXYPATH.startsWith('/')\n        ? RIPROXYPATH.slice(1)\n        : RIPROXYPATH;\n\n      // If the RIPROXYPATH is not set, then we need to set it to undefined\n      if (RIPROXYPATH.startsWith('__') || RIPROXYPATH.endsWith('__')) {\n        window.__RI_PROXY_PATH__ = undefined;\n      }\n    </script>\n\n    <style>\n      #root {\n        min-height: 100vh;\n        display: flex;\n        flex-direction: column;\n      }\n      #page-placeholder {\n        display: flex;\n        background-color: black;\n        width: 100vw;\n        height: 100vh;\n        position: fixed;\n        justify-content: center;\n        align-items: center;\n        top: 0;\n        z-index: 100;\n      }\n\n      #page-placeholder__icon {\n        width: 28px;\n        height: 28px;\n        animation: nonstop-jump 1s ease-in-out infinite;\n\n        background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjgiIGhlaWdodD0iMjgiIHZpZXdCb3g9IjAgMCAyOCAyOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI2LjM4MjggMTUuNDY3MkMyNC40NTQyIDE3Ljg5NzIgMjIuMzcxMyAyMC42NzQzIDE4LjIwNTYgMjAuNjc0M0MxNC40ODQ3IDIwLjY3NDMgMTMuMDk4NSAxNy4zOTIzIDEzLjAwMSAxNC43MjYxQzEzLjgxNjMgMTYuNDUgMTUuNDEwMiAxNy44NDYxIDE3Ljg5NzEgMTcuNzgxNUMyMi42Nzk5IDE3LjYyNzIgMjUuOTU4NSAxMy4zMDcyIDI1Ljk1ODUgOS4zNzI5MkMyNS45NTg1IDQuNjY3MjIgMjIuNDQ4NSAxLjI3Mjk1IDE2LjM1NDIgMS4yNzI5NUMxMS45OTU3IDEuMjcyOTUgNi41OTU2OSAyLjkzMTUxIDMuMDQ3MTMgNS41NTQzNkMzLjAwODU2IDguMjU0MzUgNC41MTI4NCAxMS43NjQzIDUuMDUyODQgMTEuMzc4NkM4LjEyOTIgOS4xNjY3MyAxMC41Njg2IDcuNzQyODcgMTIuOTM0NyA3LjAyODkyQzkuNDMyMjYgMTAuOTM0NiAxLjAyODg4IDIwLjAwMzUgMCAyMS42QzAuMTE1NzE0IDIzLjA2NTcgMS45Mjg1NiAyNyAyLjgxNTcgMjdDMy4wODU3IDI3IDMuMzE3MTMgMjYuODQ1NyAzLjU4NzEzIDI2LjU3NTdDNi4xMjA1NiAyMy43MjkyIDguMTg1NzggMjEuMTc3MSAxMC4wMjI3IDE4LjcxNzJDMTAuMjgwNyAyMi4zMjI1IDEyLjA1MzUgMjYuNzMgMTcuMDA5OSAyNi43M0MyMS40NDU2IDI2LjczIDI1Ljg0MjggMjMuNTI4NiAyNy44NDg1IDE2LjMxNThDMjguMDc5OSAxNS40Mjg2IDI2Ljk5OTkgMTQuNzM0MyAyNi4zODI4IDE1LjQ2NzJaTTIxLjMyOTkgOS42NDI5MkMyMS4zMjk5IDExLjkxODYgMTkuMDkyOCAxMy4wMzcyIDE3LjA0ODUgMTMuMDM3MkMxNS45NTU4IDEzLjAzNzIgMTUuMTE2NCAxMi43NTAzIDE0LjQ1MjUgMTIuMzc3NEMxNS42NzQgMTAuNTI3OCAxNi44ODMxIDguNjMxMjUgMTguMTgyMiA2LjYwMDk1QzIwLjQ3MjggNi45ODg2NSAyMS4zMjk5IDguMjYxODcgMjEuMzI5OSA5LjY0MjkyWiIgZmlsbD0iI0ZGNDQzOCIvPgo8L3N2Zz4K');\n      }\n\n      @keyframes nonstop-jump {\n        0% {\n          transform: translateY(0px);\n        }\n        50% {\n          transform: translateY(6px);\n        }\n        100% {\n          transform: translateY(0px);\n        }\n      }\n    </style>\n\n    <% if(isDev){ %>\n    <style>\n      body #page-placeholder {\n        display: none;\n      }\n    </style>\n    <% } %>\n  </head>\n  <body style=\"margin: 0\">\n    <div id=\"root\"></div>\n    <div id=\"page-placeholder\">\n      <div id=\"page-placeholder__icon\"></div>\n    </div>\n    <script type=\"module\" src=\"/%RI_INDEX_NAME%\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/ui/index.tsx",
    "content": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from 'uiSrc/App'\nimport Router from 'uiSrc/Router'\nimport { listenPluginsEvents } from 'uiSrc/plugins/pluginEvents'\nimport { migrateLocalStorageData } from 'uiSrc/services'\nimport 'uiSrc/styles/base/_fonts.scss'\nimport 'uiSrc/styles/main.scss'\n\nmigrateLocalStorageData()\nlistenPluginsEvents()\n\nconst rootEl = document.getElementById('root')\nconst root = createRoot(rootEl!)\nroot.render(\n  <Router>\n    <App />\n  </Router>,\n)\n"
  },
  {
    "path": "redisinsight/ui/indexElectron.tsx",
    "content": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport AppElectron from 'uiSrc/electron/AppElectron'\nimport { listenPluginsEvents } from 'uiSrc/plugins/pluginEvents'\nimport { migrateLocalStorageData } from 'uiSrc/services'\nimport 'uiSrc/styles/base/_fonts.scss'\nimport 'uiSrc/styles/main.scss'\n\nwindow.app.sendWindowId((_e: any, windowId: string = '') => {\n  window.windowId = windowId || window.windowId\n\n  migrateLocalStorageData()\n  listenPluginsEvents()\n\n  const rootEl = document.getElementById('root')\n  const root = createRoot(rootEl!)\n  root.render(<AppElectron />)\n})\n"
  },
  {
    "path": "redisinsight/ui/package.json",
    "content": "{\n  \"name\": \"redisinsight\",\n  \"appName\": \"Redis Insight\",\n  \"productName\": \"RedisInsight\",\n  \"version\": \"2.66.0\",\n  \"description\": \"Redis Insight\",\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"scripts\": {\n    \"dev\": \"vite dev\",\n    \"build\": \"vite build\",\n    \"stats\": \"NODE_OPTIONS=--max_old_space_size=8192 npx vite-bundle-visualizer --open -o ./dist-stats.html --sourcemap\"\n  },\n  \"resolutions\": {\n    \"**/form-data\": \"^4.0.4\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/App.scss",
    "content": ".main-container {\n  display: flex;\n  height: 100vh;\n  .main {\n    flex: 1;\n    padding: 0;\n  }\n}\n\ninput[type=\"number\"]::-webkit-outer-spin-button,\ninput[type=\"number\"]::-webkit-inner-spin-button {\n  -webkit-appearance: none;\n  margin: 0;\n}\n\ninput[type=\"number\"] {\n  -moz-appearance: textfield;\n}\n\n.euiScreenReaderOnly {\n  // fix additional scroll\n  display: none;\n  // position: absolute !important;\n}\n\n.euiGlobalToastList {\n  bottom: 68px !important;\n}\n\n.euiToast {\n  background-color: var(--euiColorLightestShade) !important;\n\n  &--success {\n    background-color: var(--euiToastBackgroundColor) !important;\n  }\n}\n\n// Fix for resolve conflict Elasti Tooltips and package 'custom-electron-titlebar'\n.electron.euiBody-hasPortalContent {\n  position: initial !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/App.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport App from './App'\n\ndescribe('App', () => {\n  it('should render', () => {\n    expect(render(<App />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/App.tsx",
    "content": "import React, { ReactElement, useEffect } from 'react'\nimport { Provider, useSelector } from 'react-redux'\n\nimport { Route, Switch } from 'react-router-dom'\nimport { store } from 'uiSrc/slices/store'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { removePagePlaceholder } from 'uiSrc/utils'\nimport MonacoLanguages from 'uiSrc/components/monaco-laguages'\nimport AppInit from 'uiSrc/components/init/AppInit'\nimport { Page, PageBody } from 'uiSrc/components/base/layout/page'\nimport { useSystemThemeListener } from 'uiSrc/services/hooks/useSystemThemeListener'\nimport { Pages, Theme } from './constants'\nimport { themeService } from './services'\nimport {\n  Config,\n  GlobalSubscriptions,\n  NavigationMenu,\n  Notifications,\n  ShortcutsFlyout,\n} from './components'\nimport { ThemeProvider } from './contexts/themeContext'\nimport MainComponent from './components/main/MainComponent'\nimport MonacoEnvironmentInitializer from './components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer'\nimport GlobalDialogs from './components/global-dialogs'\nimport GlobalAzureAuth, {\n  AzureAuthCallbackPage,\n} from './components/global-azure-auth'\nimport NotFoundErrorPage from './pages/not-found-error/NotFoundErrorPage'\n\nimport themeDark from './styles/themes/dark_theme/darkTheme.scss?inline'\nimport themeLight from './styles/themes/light_theme/lightTheme.scss?inline'\n\nimport './styles/elastic.css'\nimport './App.scss'\n\nthemeService.registerTheme(Theme.Dark, themeDark)\nthemeService.registerTheme(Theme.Light, themeLight)\n\nconst AppWrapper = ({ children }: { children?: ReactElement[] }) => (\n  <Provider store={store}>\n    <ThemeProvider>\n      <AppInit>\n        <App>{children}</App>\n      </AppInit>\n    </ThemeProvider>\n  </Provider>\n)\nconst App = ({ children }: { children?: ReactElement[] }) => {\n  const { loading: serverLoading } = useSelector(appInfoSelector)\n  useEffect(() => {\n    if (!serverLoading) {\n      removePagePlaceholder()\n    }\n  }, [serverLoading])\n  useSystemThemeListener()\n  return (\n    <div className=\"main-container\">\n      <MonacoEnvironmentInitializer />\n      <Switch>\n        <Route exact path={Pages.notFound} component={NotFoundErrorPage} />\n        <Route\n          exact\n          path=\"/azure-auth-callback\"\n          component={AzureAuthCallbackPage}\n        />\n        <Route\n          path=\"*\"\n          render={() => (\n            <>\n              <Page className=\"main\">\n                <GlobalDialogs />\n                <GlobalSubscriptions />\n                <NavigationMenu />\n                <PageBody component=\"main\">\n                  <MainComponent />\n                </PageBody>\n              </Page>\n              <Notifications />\n              <Config />\n              <ShortcutsFlyout />\n              <MonacoLanguages />\n              <GlobalAzureAuth />\n              {children}\n            </>\n          )}\n        />\n      </Switch>\n    </div>\n  )\n}\nexport default AppWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/Router.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport Router from './Router'\n\ndescribe('Router', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <Router>\n          <div>test</div>\n        </Router>,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/Router.tsx",
    "content": "import React from 'react'\nimport { Router as ReactRouter } from 'react-router-dom'\nimport { createBrowserHistory } from 'history'\n\ninterface Props {\n  children: React.ReactElement\n}\n\nconst RIPROXYPATH = window.__RI_PROXY_PATH__ || ''\n\nlet MOUNT_PATH = '/'\n\nif (RIPROXYPATH !== '') {\n  MOUNT_PATH = RIPROXYPATH\n}\n\nconst history = createBrowserHistory({ basename: MOUNT_PATH })\n\nexport const navigate = (path: string) => {\n  if (window.location.hash) {\n    // Electron (HashRouter)\n    window.location.hash = `#${path}`\n  } else {\n    // Web (BrowserRouter)\n    history.push(path)\n  }\n}\n\nconst Router = ({ children }: Props) => (\n  <ReactRouter history={history}>{children}</ReactRouter>\n)\n\nexport default Router\n"
  },
  {
    "path": "redisinsight/ui/src/RouterElectron.tsx",
    "content": "import React from 'react'\nimport { HashRouter } from 'react-router-dom'\n\ninterface Props {\n  children: React.ReactElement\n}\n\nconst Router = ({ children }: Props) => <HashRouter>{children}</HashRouter>\n\nexport default Router\n"
  },
  {
    "path": "redisinsight/ui/src/assets/assets.d.ts",
    "content": "declare module '*.svg' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.ico' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.png' {\n  const content: string\n  export default content\n}\n\ndeclare module '*.jpg' {\n  const content: string\n  export default content\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/ContentEditable.tsx",
    "content": "import React from 'react'\nimport ReactContentEditable, { Props } from 'react-contenteditable'\n\nconst useRefCallback = <T extends any[]>(\n  value: ((...args: T) => void) | undefined,\n  deps?: React.DependencyList,\n): ((...args: T) => void) => {\n  const ref = React.useRef(value)\n\n  React.useEffect(\n    () => {\n      ref.current = value\n    },\n    deps ?? [value],\n  )\n\n  return React.useCallback((...args: T) => {\n    ref.current?.(...args)\n  }, [])\n}\n\n// remove line break and encode angular brackets\nexport const parsePastedText = (text: string = '') =>\n  text.replace(/\\n/gi, '').replace(/</gi, '<').replace(/>/gi, '>')\n\nexport const parseContentEditableChangeHtml = (text: string = '') =>\n  text.replace(/&nbsp;/gi, ' ')\n\nexport const parseMultilineContentEditableChangeHtml = (text: string = '') =>\n  parseContentEditableChangeHtml(text).replace(/<br>/gi, ' ')\n\nexport const parseContentEditableHtml = (text: string = '') =>\n  text\n    .replace(/&nbsp;/gi, ' ')\n    .replace(/&lt;/gi, '<')\n    .replace(/&gt;/gi, '>')\n    .replace(/&amp;/gi, '&')\n\nconst onPaste = (e: React.ClipboardEvent) => {\n  e.preventDefault()\n\n  const clipboardData =\n    e.clipboardData || window.clipboardData || e.originalEvent.clipboardData\n  const text = clipboardData.getData('text/plain') as string\n\n  document.execCommand('insertText', false, parsePastedText(text))\n}\n\nexport default function ContentEditable({\n  ref: _ref,\n  onChange,\n  onInput,\n  onBlur,\n  onKeyPress,\n  onKeyDown,\n  onMouseUp,\n  ...props\n}: Props) {\n  const onChangeRef = useRefCallback(onChange)\n  const onInputRef = useRefCallback(onInput)\n  const onBlurRef = useRefCallback(onBlur)\n  const onKeyPressRef = useRefCallback(onKeyPress)\n  const onKeyDownRef = useRefCallback(onKeyDown)\n  const onMouseUpRef = useRefCallback(onMouseUp)\n\n  return (\n    <ReactContentEditable\n      {...props}\n      onPaste={onPaste}\n      onChange={onChangeRef}\n      onInput={onInputRef}\n      onBlur={onBlurRef}\n      onKeyPress={onKeyPressRef}\n      onKeyDown={onKeyDownRef}\n      onMouseUp={onMouseUpRef}\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MonacoEnvironmentInitializer from './MonacoEnvironmentInitializer'\n\ndescribe('MonacoEnvironmentInitializer', () => {\n  it('should render', () => {\n    expect(render(<MonacoEnvironmentInitializer />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer.tsx",
    "content": "import { useEffect } from 'react'\n\nimport EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'\nimport JSONWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'\n// https://github.com/remcohaszing/monaco-yaml?tab=readme-ov-file#why-doesnt-it-work-with-vite\nimport YamlWorker from './yaml.worker?worker'\n\nconst MonacoEnvironmentInitializer = () => {\n  useEffect(() => {\n    window.MonacoEnvironment = {\n      getWorker: (_workerId, label) => {\n        switch (label) {\n          case 'editorWorkerService':\n            return new EditorWorker()\n          case 'yaml':\n            return new YamlWorker()\n          case 'json':\n            return new JSONWorker()\n          default:\n            throw new Error(`MonacoWorker: Unknown label ${label}`)\n        }\n      },\n    }\n  }, [])\n\n  return null\n}\n\nexport default MonacoEnvironmentInitializer\n"
  },
  {
    "path": "redisinsight/ui/src/components/MonacoEnvironmentInitializer/yaml.worker.js",
    "content": "import 'monaco-yaml/yaml.worker';\n"
  },
  {
    "path": "redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport AnalyticsTabs from './AnalyticsTabs'\n\nconst mockedStandaloneConnection = ConnectionType.Standalone\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    connectionType: mockedStandaloneConnection,\n  }),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\ndescribe('AnalyticsTabs', () => {\n  it('should render', () => {\n    expect(render(<AnalyticsTabs />)).toBeTruthy()\n  })\n\n  it('should call History push with /database-analysis path when click on DatabaseAnalysis tab', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<AnalyticsTabs />)\n\n    fireEvent.mouseDown(screen.getByText('Database Analysis'))\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith(\n      '/instanceId/analytics/database-analysis',\n    )\n  })\n  it('should call History push with /slowlog path when click on SlowLog tab', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<AnalyticsTabs />)\n\n    fireEvent.mouseDown(screen.getByText('Slow Log'))\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('/instanceId/analytics/slowlog')\n  })\n\n  it('should render cluster details tab when connectionType is Cluster', async () => {\n    const mockConnectionType = ConnectionType.Cluster\n    ;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({\n      connectionType: mockConnectionType,\n    })\n\n    render(<AnalyticsTabs />)\n\n    expect(screen.getByText('Overview')).toBeInTheDocument()\n  })\n\n  it('should not render cluster details tab when connectionType is not Cluster', async () => {\n    const { queryByText } = render(<AnalyticsTabs />)\n\n    expect(queryByText('Overview')).not.toBeInTheDocument()\n  })\n\n  it('should call History push with /cluster-details path when click on SlowLog tab ', async () => {\n    const mockConnectionType = ConnectionType.Cluster\n    ;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({\n      connectionType: mockConnectionType,\n    })\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<AnalyticsTabs />)\n\n    fireEvent.mouseDown(screen.getByText('Slow Log'))\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('/instanceId/analytics/slowlog')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport AnalyticsTabs from './index'\n\nconst meta = {\n  component: AnalyticsTabs,\n} satisfies Meta<typeof AnalyticsTabs>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.tsx",
    "content": "import React, { useEffect, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams, useHistory } from 'react-router-dom'\n\nimport { Pages } from 'uiSrc/constants'\nimport { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport {\n  analyticsSettingsSelector,\n  setAnalyticsViewTab,\n} from 'uiSrc/slices/analytics/settings'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\n\nimport {\n  appFeatureOnboardingSelector,\n  setOnboardNextStep,\n} from 'uiSrc/slices/app/features'\nimport { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding'\nimport { OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { useConnectionType } from 'uiSrc/components/hooks/useConnectionType'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { Text } from 'uiSrc/components/base/text'\n\nconst AnalyticsTabs = () => {\n  const { viewTab } = useSelector(analyticsSettingsSelector)\n  const connectionType = useConnectionType()\n  const { currentStep } = useSelector(appFeatureOnboardingSelector)\n  const history = useHistory()\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (\n      connectionType !== ConnectionType.Cluster &&\n      currentStep === OnboardingSteps.AnalyticsOverview\n    ) {\n      dispatch(setOnboardNextStep())\n    }\n  }, [])\n\n  const tabs: TabInfo[] = useMemo(() => {\n    const visibleTabs: TabInfo[] = [\n      {\n        value: AnalyticsViewTab.DatabaseAnalysis,\n        content: null,\n        label: renderOnboardingTourWithChild(\n          <Text>Database Analysis</Text>,\n          {\n            options: ONBOARDING_FEATURES?.ANALYTICS_DATABASE_ANALYSIS,\n            anchorPosition: 'downLeft',\n          },\n          viewTab === AnalyticsViewTab.DatabaseAnalysis,\n          AnalyticsViewTab.DatabaseAnalysis,\n        ),\n      },\n      {\n        value: AnalyticsViewTab.SlowLog,\n        content: null,\n        label: renderOnboardingTourWithChild(\n          <Text>Slow Log</Text>,\n          {\n            options: ONBOARDING_FEATURES?.ANALYTICS_SLOW_LOG,\n            anchorPosition: 'downLeft',\n          },\n          viewTab === AnalyticsViewTab.SlowLog,\n          AnalyticsViewTab.SlowLog,\n        ),\n      },\n    ]\n\n    if (connectionType === ConnectionType.Cluster) {\n      visibleTabs.unshift({\n        value: AnalyticsViewTab.ClusterDetails,\n        content: null,\n        label: renderOnboardingTourWithChild(\n          <Text>Overview</Text>,\n          {\n            options: ONBOARDING_FEATURES?.ANALYTICS_OVERVIEW,\n            anchorPosition: 'downLeft',\n          },\n          viewTab === AnalyticsViewTab.ClusterDetails,\n          AnalyticsViewTab.ClusterDetails,\n        ),\n      })\n    }\n\n    return visibleTabs\n  }, [viewTab, connectionType])\n\n  const handleTabChange = (id: string) => {\n    if (viewTab === id) return\n\n    if (id === AnalyticsViewTab.ClusterDetails) {\n      history.push(Pages.clusterDetails(instanceId))\n    }\n    if (id === AnalyticsViewTab.SlowLog) {\n      history.push(Pages.slowLog(instanceId))\n    }\n    if (id === AnalyticsViewTab.DatabaseAnalysis) {\n      history.push(Pages.databaseAnalysis(instanceId))\n    }\n    dispatch(setAnalyticsViewTab(id as AnalyticsViewTab))\n  }\n\n  return (\n    <Tabs\n      tabs={tabs}\n      value={viewTab}\n      onChange={handleTabChange}\n      data-testid=\"analytics-tabs\"\n    />\n  )\n}\n\nexport default AnalyticsTabs\n"
  },
  {
    "path": "redisinsight/ui/src/components/analytics-tabs/index.ts",
    "content": "import AnalyticsTabs from './AnalyticsTabs'\n\nexport default AnalyticsTabs\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-discover/EmptyState.tsx",
    "content": "import React from 'react'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport type { EmptyStateProps } from './EmptyState.types'\n\nexport const EmptyState = ({ message }: EmptyStateProps) => (\n  <Col centered full>\n    <FlexItem padding={13}>\n      <Text size=\"L\">{message}</Text>\n    </FlexItem>\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-discover/EmptyState.types.ts",
    "content": "export interface EmptyStateProps {\n  message: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-discover/Header.tsx",
    "content": "import React, { ReactNode } from 'react'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { ChevronLeftIcon } from 'uiSrc/components/base/icons'\nimport {\n  PageSubTitle,\n  PageTitle,\n  SearchContainer,\n  SearchForm,\n} from 'uiSrc/components/auto-discover/index'\nimport { SearchInput } from 'uiSrc/components/base/inputs'\n\ntype HeaderProps = {\n  title: ReactNode\n  subTitle?: ReactNode\n  onBack: () => void\n  onQueryChange: (query: string) => void\n  backButtonText?: string\n}\nexport const Header = ({\n  title,\n  subTitle,\n  onBack,\n  onQueryChange,\n  backButtonText = 'Add databases',\n}: HeaderProps) => {\n  return (\n    <Row align=\"center\" justify=\"between\" grow={false}>\n      <Col align=\"start\" justify=\"start\" gap=\"m\">\n        <EmptyButton\n          icon={ChevronLeftIcon}\n          onClick={onBack}\n          data-testid=\"btn-back-adding\"\n        >\n          {backButtonText}\n        </EmptyButton>\n        <PageTitle color=\"primary\" data-testid=\"title\">\n          {title}\n        </PageTitle>\n        {subTitle && (\n          <FlexItem grow>\n            <PageSubTitle>{subTitle}</PageSubTitle>\n          </FlexItem>\n        )}\n      </Col>\n      <Row justify=\"end\" gap=\"s\" grow={false}>\n        <SearchContainer>\n          <SearchForm>\n            <SearchInput\n              placeholder=\"Search...\"\n              onChange={onQueryChange}\n              aria-label=\"Search\"\n              data-testid=\"search\"\n            />\n          </SearchForm>\n        </SearchContainer>\n      </Row>\n    </Row>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-discover/index.ts",
    "content": "export * from './styles'\n\nexport { Header } from './Header'\nexport { EmptyState } from './EmptyState'\nexport type { EmptyStateProps } from './EmptyState.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-discover/styles.ts",
    "content": "import styled from 'styled-components'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { CopyButton } from 'uiSrc/components/copy-button'\n\nexport const PageTitle = styled(Title).attrs({\n  size: 'L',\n})`\n  padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n`\nexport const PageSubTitle = styled(Text).attrs({\n  size: 'S',\n  component: 'span',\n})`\n  padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n`\nexport const SearchContainer = styled(FlexItem)`\n  max-width: 100%;\n  padding-top: ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n`\nexport const SearchForm = styled(FormField)`\n  width: 266px;\n`\nexport const Footer = styled(FlexItem).attrs<{\n  grow?: boolean | number\n  padding?: React.ComponentProps<typeof FlexItem>['padding']\n}>(({ grow, padding }) => ({\n  grow: grow ?? false,\n  padding: padding ?? 6,\n}))`\n  border-top: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral400};\n`\n\nexport const DatabaseContainer = styled(Col)`\n  position: relative;\n  padding: ${({ theme }: { theme: Theme }) =>\n    `${theme.core.space.space250} ${theme.core.space.space200} 0 ${theme.core.space.space200}`};\n  @media only screen and (min-width: 768px) {\n    padding: ${({ theme }: { theme: Theme }) =>\n      `${theme.core.space.space400} ${theme.core.space.space200} 0 ${theme.core.space.space400}`};\n    max-width: calc(100vw - 95px);\n  }\n`\n\nexport const DatabaseWrapper = styled.div`\n  height: auto;\n  scrollbar-width: thin;\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space010};\n  position: relative;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral100};\n  overflow: hidden;\n`\nexport const SelectAllCheckbox = styled(Checkbox)`\n  & svg {\n    margin: 0 !important;\n  }\n`\nexport const CellText = styled(Text).attrs({\n  size: 'M',\n  component: 'span',\n})`\n  max-width: 100%;\n  display: inline-block;\n  width: auto;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n`\n\nexport const CopyPublicEndpointText = styled(CellText)`\n  vertical-align: top;\n`\n\nexport const StatusColumnText = styled(CellText)`\n  text-transform: capitalize;\n`\n\nexport const CopyBtnWrapper = styled(CopyButton)`\n  margin-left: 15px;\n  position: absolute;\n  right: 0;\n  top: 0;\n  opacity: 0;\n  transition: opacity 0.25s ease-in-out;\n  height: 100%;\n`\n\nexport const CopyTextContainer = styled.div`\n  height: 24px;\n  line-height: 24px;\n  width: auto;\n  max-width: 100%;\n  padding-right: 34px;\n  position: relative;\n\n  &:hover ${CopyBtnWrapper} {\n    opacity: 1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-refresh/AutoRefresh.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  userEvent,\n  fireEvent,\n  screen,\n  render,\n  act,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\nimport { localStorageService } from 'uiSrc/services'\nimport AutoRefresh, { Props } from './AutoRefresh'\nimport { DEFAULT_REFRESH_RATE } from './utils'\n\nconst mockedProps = mock<Props>()\n\nconst INLINE_ITEM_EDITOR = 'inline-item-editor'\n\ndescribe('AutoRefresh', () => {\n  beforeEach(() => {\n    // Clear any stored refresh rate before each test\n    jest.clearAllMocks()\n    jest.spyOn(localStorageService, 'get').mockImplementation(() => null)\n  })\n  it('should render', () => {\n    expect(render(<AutoRefresh {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('prop \"displayText = true\" should show Refresh text', () => {\n    const { queryByTestId } = render(\n      <AutoRefresh {...instance(mockedProps)} displayText />,\n    )\n\n    expect(queryByTestId('refresh-message-label')).toBeInTheDocument()\n  })\n\n  it('prop \"displayText = false\" should hide Refresh text', () => {\n    const { queryByTestId } = render(\n      <AutoRefresh {...instance(mockedProps)} displayText={false} />,\n    )\n\n    expect(queryByTestId('refresh-message-label')).not.toBeInTheDocument()\n  })\n\n  it('prop \"displayLastRefresh = true\" should show refresh time message', () => {\n    const { queryByTestId } = render(\n      <AutoRefresh {...instance(mockedProps)} displayLastRefresh />,\n    )\n\n    expect(queryByTestId('refresh-message')).toBeInTheDocument()\n  })\n\n  it('prop \"displayLastRefresh = false\" should hide refresh time message', () => {\n    const { queryByTestId } = render(\n      <AutoRefresh {...instance(mockedProps)} displayLastRefresh={false} />,\n    )\n\n    expect(queryByTestId('refresh-message')).not.toBeInTheDocument()\n  })\n\n  it('should call onRefresh', () => {\n    const onRefresh = jest.fn()\n    render(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} />)\n\n    fireEvent.click(screen.getByTestId('refresh-btn'))\n    expect(onRefresh).toHaveBeenCalled()\n  })\n\n  it('refresh text should contain \"Last refresh\" time with disabled auto-refresh', async () => {\n    render(<AutoRefresh {...instance(mockedProps)} displayText />)\n\n    expect(screen.getByTestId('refresh-message-label')).toHaveTextContent(\n      /Last refresh:/i,\n    )\n    expect(screen.getByTestId('refresh-message')).toHaveTextContent('now')\n  })\n\n  it('refresh text should contain \"Auto-refresh\" time with enabled auto-refresh', async () => {\n    render(<AutoRefresh {...instance(mockedProps)} displayText />)\n\n    await userEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n    await waitForRiPopoverVisible()\n    await userEvent.click(screen.getByTestId('auto-refresh-switch'))\n\n    expect(screen.getByTestId('refresh-message-label')).toHaveTextContent(\n      /Auto refresh:/i,\n    )\n    expect(screen.getByTestId('refresh-message')).toHaveTextContent(\n      DEFAULT_REFRESH_RATE,\n    )\n  })\n\n  it('should locate refresh message label when testid is provided', () => {\n    render(\n      <AutoRefresh {...instance(mockedProps)} displayText testid=\"testid\" />,\n    )\n\n    expect(\n      screen.getByTestId('testid-refresh-message-label'),\n    ).toBeInTheDocument()\n  })\n\n  it('should locate refresh message when testid is provided', () => {\n    render(\n      <AutoRefresh {...instance(mockedProps)} displayText testid=\"testid\" />,\n    )\n\n    expect(screen.getByTestId('testid-refresh-message')).toBeInTheDocument()\n  })\n\n  it('should locate refresh button when testid is provided', () => {\n    render(<AutoRefresh {...instance(mockedProps)} testid=\"testid\" />)\n\n    expect(screen.getByTestId('testid-refresh-btn')).toBeInTheDocument()\n  })\n\n  it('should locate auto-refresh config button when testid is provided', () => {\n    render(<AutoRefresh {...instance(mockedProps)} testid=\"testid\" />)\n\n    expect(\n      screen.getByTestId('testid-auto-refresh-config-btn'),\n    ).toBeInTheDocument()\n  })\n\n  it('should locate auto-refresh switch when testid is provided', () => {\n    render(<AutoRefresh {...instance(mockedProps)} testid=\"testid\" />)\n\n    fireEvent.click(screen.getByTestId('testid-auto-refresh-config-btn'))\n    expect(screen.getByTestId('testid-auto-refresh-switch')).toBeInTheDocument()\n  })\n\n  it('should locate refresh rate when testid is provided', () => {\n    render(<AutoRefresh {...instance(mockedProps)} testid=\"testid\" />)\n\n    fireEvent.click(screen.getByTestId('testid-auto-refresh-config-btn'))\n    expect(screen.getByTestId('testid-refresh-rate')).toBeInTheDocument()\n  })\n\n  it('should locate auto-refresh rate input when testid is provided', () => {\n    render(<AutoRefresh {...instance(mockedProps)} testid=\"testid\" />)\n\n    fireEvent.click(screen.getByTestId('testid-auto-refresh-config-btn'))\n    fireEvent.click(screen.getByTestId('testid-refresh-rate'))\n    expect(\n      screen.getByTestId('testid-auto-refresh-rate-input'),\n    ).toBeInTheDocument()\n  })\n\n  describe('AutoRefresh Config', () => {\n    it('Auto refresh config should render', () => {\n      const { queryByTestId } = render(\n        <AutoRefresh {...instance(mockedProps)} />,\n      )\n\n      fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n      expect(queryByTestId('auto-refresh-switch')).toBeInTheDocument()\n    })\n\n    it('should call onRefresh after enable auto-refresh and set 1 sec', async () => {\n      const onRefresh = jest.fn()\n      render(<AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} />)\n\n      await userEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n      await waitForRiPopoverVisible()\n      await userEvent.click(screen.getByTestId('auto-refresh-switch'))\n      fireEvent.click(screen.getByTestId('refresh-rate'))\n\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: '1' },\n      })\n      expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('1')\n\n      await userEvent.click(screen.getByTestId(/apply-btn/))\n      // screen.getByTestId(/apply-btn/).click()\n\n      await act(async () => {\n        await new Promise((r) => setTimeout(r, 1300))\n      })\n      expect(onRefresh).toHaveBeenCalledTimes(1)\n\n      await act(async () => {\n        await new Promise((r) => setTimeout(r, 1300))\n      })\n      expect(onRefresh).toHaveBeenCalledTimes(2)\n\n      await act(async () => {\n        await new Promise((r) => setTimeout(r, 1300))\n      })\n      expect(onRefresh).toHaveBeenCalledTimes(3)\n    })\n\n    it('should respect minimumRefreshRate when setting refresh rate', async () => {\n      const onChangeAutoRefreshRate = jest.fn()\n      const minimumRefreshRate = 6\n      render(\n        <AutoRefresh\n          {...instance(mockedProps)}\n          minimumRefreshRate={minimumRefreshRate}\n          onChangeAutoRefreshRate={onChangeAutoRefreshRate}\n        />,\n      )\n\n      fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n      fireEvent.click(screen.getByTestId('refresh-rate'))\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: (minimumRefreshRate / 2).toString() },\n      })\n      screen.getByTestId(/apply-btn/).click()\n      expect(onChangeAutoRefreshRate).toHaveBeenLastCalledWith(\n        false,\n        minimumRefreshRate.toString(),\n      )\n    })\n\n    it('should allow valid refresh rates above minimumRefreshRate', async () => {\n      const onChangeAutoRefreshRate = jest.fn()\n      const minimumRefreshRate = 6\n      render(\n        <AutoRefresh\n          {...instance(mockedProps)}\n          minimumRefreshRate={minimumRefreshRate}\n          onChangeAutoRefreshRate={onChangeAutoRefreshRate}\n        />,\n      )\n\n      fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n      fireEvent.click(screen.getByTestId('refresh-rate'))\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: (minimumRefreshRate * 2).toString() },\n      })\n      screen.getByTestId(/apply-btn/).click()\n\n      expect(onChangeAutoRefreshRate).toHaveBeenLastCalledWith(\n        false,\n        (minimumRefreshRate * 2).toString(),\n      )\n    })\n\n    it('should use defaultRefreshRate when provided', () => {\n      const customDefaultRate = '30'\n      render(\n        <AutoRefresh\n          {...instance(mockedProps)}\n          defaultRefreshRate={customDefaultRate}\n        />,\n      )\n\n      fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n      expect(screen.getByTestId('refresh-rate')).toHaveTextContent(\n        `${customDefaultRate} s`,\n      )\n    })\n\n    it('should use DEFAULT_REFRESH_RATE when defaultRefreshRate is not provided', () => {\n      render(<AutoRefresh {...instance(mockedProps)} />)\n\n      // Open config and check default value\n      fireEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n      expect(screen.getByTestId('refresh-rate')).toHaveTextContent(\n        `${DEFAULT_REFRESH_RATE} s`,\n      )\n    })\n  })\n\n  it('should NOT call onRefresh with disabled state', async () => {\n    const onRefresh = jest.fn()\n    const { rerender } = render(\n      <AutoRefresh {...instance(mockedProps)} onRefresh={onRefresh} />,\n    )\n\n    await userEvent.click(screen.getByTestId('auto-refresh-config-btn'))\n    await waitForRiPopoverVisible()\n    await userEvent.click(screen.getByTestId('auto-refresh-switch'))\n    fireEvent.click(screen.getByTestId('refresh-rate'))\n    fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n      target: { value: '1' },\n    })\n\n    expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('1')\n\n    screen.getByTestId(/apply-btn/).click()\n\n    await act(async () => {\n      rerender(\n        <AutoRefresh\n          {...instance(mockedProps)}\n          onRefresh={onRefresh}\n          disabled\n        />,\n      )\n    })\n\n    await act(async () => {\n      await new Promise((r) => setTimeout(r, 1300))\n    })\n    expect(onRefresh).toHaveBeenCalledTimes(0)\n\n    await act(async () => {\n      await new Promise((r) => setTimeout(r, 1300))\n    })\n    expect(onRefresh).toHaveBeenCalledTimes(0)\n\n    await act(async () => {\n      rerender(\n        <AutoRefresh\n          {...instance(mockedProps)}\n          onRefresh={onRefresh}\n          disabled={false}\n        />,\n      )\n    })\n\n    await act(async () => {\n      await new Promise((r) => setTimeout(r, 1300))\n    })\n    expect(onRefresh).toHaveBeenCalledTimes(1)\n  })\n\n  it('refresh tooltip text should contain disabled refresh button reason message when button disabled', async () => {\n    const tooltipText = 'some-disabled-message'\n    render(\n      <AutoRefresh\n        {...instance(mockedProps)}\n        disabled\n        disabledRefreshButtonMessage={tooltipText}\n      />,\n    )\n\n    fireEvent.focus(screen.getByTestId('refresh-btn'))\n    await screen.findByTestId('refresh-tooltip')\n    expect(screen.getByTestId('refresh-tooltip')).toHaveTextContent(\n      new RegExp(`^${tooltipText}`),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-refresh/AutoRefresh.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport React from 'react'\nimport AutoRefresh from './index'\nimport {\n  DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL,\n  DATABASE_OVERVIEW_REFRESH_INTERVAL,\n} from 'uiSrc/constants'\nimport { StyledContainer } from '../../../../../.storybook/helpers/styles'\n\nconst meta = {\n  component: AutoRefresh,\n  parameters: {\n    layout: 'centered',\n  },\n  decorators: [\n    (Story) => {\n      return (\n        <StyledContainer>\n          <Story />\n        </StyledContainer>\n      )\n    },\n  ],\n} satisfies Meta<typeof AutoRefresh>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const AutoRefreshDefault: Story = {\n  args: {\n    postfix: 'default',\n    loading: false,\n    onRefresh: () => {},\n    onRefreshClicked: () => {},\n    onEnableAutoRefresh: () => {},\n    enableAutoRefreshDefault: true,\n    defaultRefreshRate: DATABASE_OVERVIEW_REFRESH_INTERVAL,\n    minimumRefreshRate: parseInt(DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL),\n    lastRefreshTime: Date.now(),\n  },\n}\nexport const AutoRefreshDatabaseOverview: Story = {\n  args: {\n    displayText: false,\n    displayLastRefresh: false,\n    iconSize: 'S',\n    loading: false,\n    enableAutoRefreshDefault: true,\n    lastRefreshTime: 100,\n    containerClassName: '',\n    postfix: 'overview',\n    testid: 'auto-refresh-overview',\n    defaultRefreshRate: DATABASE_OVERVIEW_REFRESH_INTERVAL,\n    minimumRefreshRate: parseInt(DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL),\n    onRefresh: () => {},\n    onRefreshClicked: () => {},\n    onEnableAutoRefresh: () => {},\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-refresh/AutoRefresh.tsx",
    "content": "import React, { HTMLAttributes, useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport styled from 'styled-components'\nimport { ChevronDownIcon, ResetIcon } from 'uiSrc/components/base/icons'\nimport {\n  errorValidateRefreshRateNumber,\n  MIN_REFRESH_RATE,\n  Nullable,\n  validateRefreshRateNumber,\n} from 'uiSrc/utils'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor'\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport {\n  DEFAULT_REFRESH_RATE,\n  DURATION_FIRST_REFRESH_TIME,\n  getTextByRefreshTime,\n  MINUTE,\n  NOW,\n} from './utils'\n\nimport styles from './styles.module.scss'\n\nconst AutoRefreshInterval = styled(ColorText)<\n  HTMLAttributes<HTMLSpanElement> & {\n    enableAutoRefresh: boolean\n    disabled?: boolean\n  }\n>`\n  color: ${({ disabled, enableAutoRefresh, theme }) =>\n    !disabled && enableAutoRefresh\n      ? theme.semantic.color.text.primary400\n      : 'inherit'};\n  opacity: ${({ disabled }) => (disabled ? '0.5' : 'inherit')};\n`\n\nconst AutoRefreshButton = styled(IconButton)<{\n  enableAutoRefresh: boolean\n  disabled?: boolean\n}>`\n  color: ${({ theme, disabled, enableAutoRefresh }) =>\n    !disabled && enableAutoRefresh\n      ? theme.semantic.color.text.primary400\n      : 'inherit'};\n`\n\nconst AutoRefreshConfigButton = styled(IconButton)<{\n  isPopoverOpen: boolean\n}>`\n    svg {\n      width: 10px;\n      height: 10px;\n    }\n\n    background-color: ${({ theme, isPopoverOpen }) =>\n      isPopoverOpen\n        ? theme.semantic.color.background.neutral100\n        : 'transparent'};\n  }\n`\n\nexport interface Props {\n  postfix: string\n  loading: boolean\n  displayText?: boolean\n  displayLastRefresh?: boolean\n  lastRefreshTime: Nullable<number>\n  testid?: string\n  containerClassName?: string\n  turnOffAutoRefresh?: boolean\n  onRefresh: (forceRefresh?: boolean) => void\n  onRefreshClicked?: () => void\n  onEnableAutoRefresh?: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  onChangeAutoRefreshRate?: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  minimumRefreshRate?: number\n  defaultRefreshRate?: string\n  iconSize?: 'S' | 'M' | 'L'\n  disabled?: boolean\n  disabledRefreshButtonMessage?: string\n  enableAutoRefreshDefault?: boolean\n}\n\nconst TIMEOUT_TO_UPDATE_REFRESH_TIME = 1_000 * MINUTE // once a minute\n\nconst AutoRefresh = ({\n  postfix,\n  loading,\n  displayText = true,\n  displayLastRefresh = true,\n  lastRefreshTime,\n  containerClassName = '',\n  testid = '',\n  turnOffAutoRefresh,\n  onRefresh,\n  onRefreshClicked,\n  onEnableAutoRefresh,\n  onChangeAutoRefreshRate,\n  iconSize = 'M',\n  disabled,\n  disabledRefreshButtonMessage,\n  minimumRefreshRate,\n  defaultRefreshRate,\n  enableAutoRefreshDefault = false,\n}: Props) => {\n  let intervalText: NodeJS.Timeout\n  let intervalRefresh: NodeJS.Timeout\n\n  const [refreshMessage, setRefreshMessage] = useState(NOW)\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const [refreshRate, setRefreshRate] = useState<string>(\n    defaultRefreshRate || '',\n  )\n  const [refreshRateMessage, setRefreshRateMessage] = useState<string>('')\n  const [enableAutoRefresh, setEnableAutoRefresh] = useState(\n    enableAutoRefreshDefault,\n  )\n  const [editingRate, setEditingRate] = useState(false)\n\n  const onButtonClick = () =>\n    setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen)\n  const closePopover = () => {\n    setEnableAutoRefresh(enableAutoRefresh)\n    setIsPopoverOpen(false)\n  }\n\n  useEffect(() => {\n    const refreshRateStorage =\n      localStorageService.get(BrowserStorageItem.autoRefreshRate + postfix) ||\n      defaultRefreshRate ||\n      DEFAULT_REFRESH_RATE\n\n    setRefreshRate(refreshRateStorage)\n  }, [postfix])\n\n  useEffect(() => {\n    if (turnOffAutoRefresh && enableAutoRefresh) {\n      setEnableAutoRefresh(false)\n      clearInterval(intervalRefresh)\n    }\n  }, [turnOffAutoRefresh])\n\n  // update refresh label text\n  useEffect(() => {\n    const delta = getLastRefreshDelta(lastRefreshTime)\n    updateLastRefresh()\n\n    intervalText = setInterval(\n      () => {\n        if (document.hidden) return\n\n        updateLastRefresh()\n      },\n      delta < DURATION_FIRST_REFRESH_TIME\n        ? DURATION_FIRST_REFRESH_TIME\n        : TIMEOUT_TO_UPDATE_REFRESH_TIME,\n    )\n    return () => clearInterval(intervalText)\n  }, [lastRefreshTime])\n\n  // refresh interval\n  useEffect(() => {\n    updateLastRefresh()\n    if (enableAutoRefresh && !loading && !disabled) {\n      intervalRefresh = setInterval(() => {\n        if (document.hidden) return\n\n        handleRefresh()\n      }, +refreshRate * 1_000)\n    } else {\n      clearInterval(intervalRefresh)\n    }\n\n    if (enableAutoRefresh) {\n      updateAutoRefreshText(refreshRate)\n    }\n\n    return () => clearInterval(intervalRefresh)\n  }, [enableAutoRefresh, refreshRate, loading, disabled, lastRefreshTime])\n\n  const getLastRefreshDelta = (time: Nullable<number>) =>\n    (Date.now() - (time || 0)) / 1_000\n\n  const getDataTestid = (suffix: string) =>\n    testid ? `${testid}-${suffix}` : suffix\n\n  const updateLastRefresh = () => {\n    const delta = getLastRefreshDelta(lastRefreshTime)\n    const text = getTextByRefreshTime(delta, lastRefreshTime ?? 0)\n    lastRefreshTime && setRefreshMessage(text)\n  }\n\n  const updateAutoRefreshText = (refreshRate: string) => {\n    enableAutoRefresh &&\n      setRefreshRateMessage(\n        // more than 1 minute\n        +refreshRate > MINUTE\n          ? `${Math.floor(+refreshRate / MINUTE)} min`\n          : `${refreshRate} s`,\n      )\n  }\n\n  const handleApplyAutoRefreshRate = (initValue: string) => {\n    const minRefreshRate = minimumRefreshRate || MIN_REFRESH_RATE\n    const value = +initValue >= minRefreshRate ? initValue : `${minRefreshRate}`\n    setRefreshRate(value)\n    setEditingRate(false)\n    localStorageService.set(BrowserStorageItem.autoRefreshRate + postfix, value)\n    onChangeAutoRefreshRate?.(enableAutoRefresh, value)\n  }\n\n  const handleDeclineAutoRefreshRate = () => {\n    setEditingRate(false)\n  }\n\n  const handleRefresh = (forceRefresh = false) => {\n    onRefresh(forceRefresh)\n  }\n\n  const handleRefreshClick = () => {\n    handleRefresh(true)\n    onRefreshClicked?.()\n  }\n\n  const onChangeEnableAutoRefresh = (value: boolean) => {\n    setEnableAutoRefresh(value)\n\n    onEnableAutoRefresh?.(value, refreshRate)\n  }\n\n  return (\n    <Row\n      align=\"center\"\n      gap=\"m\"\n      className={cx(containerClassName, {\n        [styles.enable]: !disabled && enableAutoRefresh,\n      })}\n      data-testid={getDataTestid('auto-refresh-container')}\n      // TODO: fix properly\n      style={{ lineHeight: 1 }}\n    >\n      {displayText && (\n        <FlexItem>\n          <ColorText\n            size=\"s\"\n            component=\"span\"\n            data-testid={getDataTestid('refresh-message-label')}\n          >\n            {enableAutoRefresh ? 'Auto refresh:' : 'Last refresh:'}\n          </ColorText>\n        </FlexItem>\n      )}\n      {displayLastRefresh && (\n        <FlexItem>\n          <AutoRefreshInterval\n            disabled={disabled}\n            enableAutoRefresh={enableAutoRefresh}\n            className={cx('refresh-message-time')}\n            data-testid={getDataTestid('refresh-message')}\n            component=\"span\"\n            size=\"s\"\n          >\n            {` ${enableAutoRefresh ? refreshRateMessage : refreshMessage}`}\n          </AutoRefreshInterval>\n        </FlexItem>\n      )}\n      <FlexItem>\n        <Row align=\"center\" gap=\"none\">\n          <FlexItem>\n            <RiTooltip\n              title={!disabled && 'Last Refresh'}\n              className={styles.tooltip}\n              position=\"top\"\n              content={disabled ? disabledRefreshButtonMessage : refreshMessage}\n              data-testid={getDataTestid('refresh-tooltip')}\n            >\n              <AutoRefreshButton\n                enableAutoRefresh={enableAutoRefresh}\n                size={iconSize}\n                icon={ResetIcon}\n                disabled={loading || disabled}\n                onClick={handleRefreshClick}\n                onMouseEnter={updateLastRefresh}\n                className={cx('auto-refresh-btn')}\n                aria-labelledby={getDataTestid('refresh-btn')?.replaceAll?.(\n                  '-',\n                  ' ',\n                )}\n                data-testid={getDataTestid('refresh-btn')}\n              />\n            </RiTooltip>\n          </FlexItem>\n          <FlexItem>\n            <RiPopover\n              ownFocus={false}\n              anchorPosition=\"downCenter\"\n              isOpen={isPopoverOpen}\n              anchorClassName={styles.anchorWrapper}\n              panelClassName={cx(styles.popoverWrapper, {\n                [styles.popoverWrapperEditing]: editingRate,\n              })}\n              closePopover={closePopover}\n              button={\n                <AutoRefreshConfigButton\n                  isPopoverOpen={isPopoverOpen}\n                  disabled={disabled}\n                  size=\"XS\"\n                  icon={ChevronDownIcon}\n                  aria-label=\"Auto-refresh config popover\"\n                  className={cx(styles.anchorBtn, {\n                    [styles.anchorBtnOpen]: isPopoverOpen,\n                  })}\n                  onClick={onButtonClick}\n                  data-testid={getDataTestid('auto-refresh-config-btn')}\n                />\n              }\n            >\n              <SwitchInput\n                title=\"Auto Refresh\"\n                checked={enableAutoRefresh}\n                onCheckedChange={onChangeEnableAutoRefresh}\n                className={styles.switchOption}\n                data-testid={getDataTestid('auto-refresh-switch')}\n              />\n              <div className={styles.inputContainer}>\n                <div className={styles.inputLabel}>Refresh rate:</div>\n                {!editingRate && (\n                  <ColorText\n                    className={styles.refreshRateText}\n                    onClick={() => setEditingRate(true)}\n                    data-testid={getDataTestid('refresh-rate')}\n                  >\n                    {`${refreshRate} s`}\n                    <div className={styles.refreshRatePencil}>\n                      <RiIcon type=\"EditIcon\" />\n                    </div>\n                  </ColorText>\n                )}\n                {editingRate && (\n                  <>\n                    <div\n                      className={styles.input}\n                      data-testid={getDataTestid('auto-refresh-rate-input')}\n                    >\n                      <InlineItemEditor\n                        initialValue={refreshRate}\n                        fieldName=\"refreshRate\"\n                        placeholder={DEFAULT_REFRESH_RATE}\n                        isLoading={loading}\n                        validation={validateRefreshRateNumber}\n                        disableByValidation={errorValidateRefreshRateNumber}\n                        onDecline={() => handleDeclineAutoRefreshRate()}\n                        onApply={(value) => handleApplyAutoRefreshRate(value)}\n                      />\n                    </div>\n                    <ColorText>{' s'}</ColorText>\n                  </>\n                )}\n              </div>\n            </RiPopover>\n          </FlexItem>\n        </Row>\n      </FlexItem>\n    </Row>\n  )\n}\n\nexport default React.memo(AutoRefresh)\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-refresh/index.ts",
    "content": "import AutoRefresh from './AutoRefresh'\n\nexport * from './AutoRefresh'\n\nexport default AutoRefresh\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-refresh/styles.module.scss",
    "content": ".summary {\n  color: var(--euiColorMediumShade) !important;\n  vertical-align: middle;\n  font: normal normal normal 12px/18px \"Graphik\", sans-serif !important;\n  letter-spacing: -0.12px;\n}\n\n.tooltip {\n  max-width: 372px !important;\n}\n\n.switch {\n  padding-bottom: 16px;\n}\n\n.popoverWrapper {\n  width: 240px;\n  height: 114px;\n  padding: 28px 18px !important;\n\n  .input {\n    display: inline-block;\n    width: 80px;\n\n    input {\n      height: 30px !important;\n      border-radius: 0 !important;\n    }\n  }\n}\n.popoverWrapperEditing {\n  height: 140px;\n}\n\n.inputContainer {\n  height: 30px;\n  line-height: 30px;\n}\n\n.inputLabel {\n  display: inline-block;\n  width: 80px;\n  font-size: 13px;\n  color: var(--euiTextSubduedColor) !important;\n}\n\n.switchOption {\n  padding-bottom: 16px;\n}\n\n.refreshRatePencil {\n  display: none;\n  right: 61px;\n  margin-bottom: 3px;\n  width: 26px !important;\n  height: 28px !important;\n  padding: 5px;\n  position: absolute;\n\n  svg {\n    margin-top: -16px !important;\n  }\n}\n\n.refreshRateText {\n  display: inline-block;\n  width: 80px;\n  height: 30px;\n  cursor: pointer;\n  padding-left: 5px;\n\n  &:hover {\n    border-color: var(--controlsBorderColor);\n\n    .refreshRatePencil {\n      display: inline-block !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/auto-refresh/utils.ts",
    "content": "import { truncateNumberToFirstUnit } from 'uiSrc/utils'\n\nexport const NOW = 'now'\nexport const MINUTE = 60\nexport const DURATION_FIRST_REFRESH_TIME = 5\nexport const DEFAULT_REFRESH_RATE = '5.0'\n\nexport const getTextByRefreshTime = (\n  delta: number,\n  lastRefreshTime: number,\n) => {\n  let text = ''\n\n  if (delta > MINUTE) {\n    text = truncateNumberToFirstUnit(\n      (Date.now() - (lastRefreshTime || 0)) / 1_000,\n    )\n  }\n  if (delta < MINUTE) {\n    text = '< 1 min'\n  }\n  if (delta < DURATION_FIRST_REFRESH_TIME) {\n    text = NOW\n  }\n\n  return text\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/code-editor/CodeEditor.styles.ts",
    "content": "import { createGlobalStyle } from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\n/**\n * Generate CSS variables for Monaco editor based on the theme.\n *\n * Rebrand themes (light-2/dark-2) use the secondary scale for\n * backgrounds (secondary900/950) and borders (secondary700/800) instead\n * of the neutral palette. These neutral-based tokens will need\n * secondary-scale overrides once the rebrand themes are adopted.\n *\n * It is either this approach, or defining a separate theme color config for each theme.\n */\nconst monacoStyle = (theme: Theme) => {\n  const { name, semantic, core } = theme\n  const colors = semantic.color\n  const fontSize = core.font.fontSize\n  let bgColor = colors.background.neutral100\n  let bgWidgetColor = colors.background.neutral200\n  let borderWidgetColor = colors.border.neutral400\n  let hoverBgWidgetColor = colors.background.neutral300\n  let hoverBorderWidgetColor = colors.border.neutral500\n  let titleWidgetColor = colors.text.neutral700\n  let shortcutWidgetColor = colors.text.neutral700\n\n  // Rebrand dark theme uses the secondary scale\n  if (name === 'dark-2') {\n    bgColor = colors.background.secondary950\n    bgWidgetColor = colors.background.secondary900\n    borderWidgetColor = colors.border.secondary700\n    hoverBgWidgetColor = colors.background.secondary950\n    hoverBorderWidgetColor = colors.border.secondary800\n    titleWidgetColor = colors.text.secondary300\n    shortcutWidgetColor = colors.text.secondary400\n  }\n  return `\n    /* Colors */\n    --monaco-docs-link-color: ${colors.text.secondary300};\n    --monaco-widget-bg: ${bgWidgetColor};\n    --monaco-widget-border: ${borderWidgetColor};\n    --monaco-widget-hover-bg: ${hoverBgWidgetColor};\n    --monaco-widget-hover-border: ${hoverBorderWidgetColor};\n    --monaco-widget-title-color: ${titleWidgetColor};\n    --monaco-widget-shortcut-color: ${shortcutWidgetColor};\n    --monaco-color-submit: ${colors.icon.success500};\n    --monaco-color-bg: ${bgColor};\n    --monaco-color-params: ${colors.text.discovery200};\n    \n    /* Font sizes */\n    --monaco-font-size-s: ${fontSize.s13};\n    --monaco-font-size-m: ${fontSize.s16};\n    --monaco-font-size-l: ${fontSize.s18};\n    `\n}\n\n/**\n * Global styles for Monaco editor overrides.\n * Rendered inside CodeEditor so they are only injected when a Monaco\n * instance is mounted. styled-components deduplicates automatically\n * when multiple CodeEditors are on screen at the same time.\n */\nexport const MonacoGlobalStyles = createGlobalStyle<{ theme: Theme }>`\n  :root {\n    ${({ theme }) => monacoStyle(theme)}\n\n    /* Sizes */\n    --monaco-size-xs: 5px;\n    --monaco-size-s: 10px;\n    --monaco-size-m: 12px;\n    --monaco-size-l: 14px;\n    --monaco-size-xl: 16px;\n    --monaco-widget-height: 30px;\n    --monaco-widget-width: 220px;\n\n    /* Opacity */\n    --monaco-opacity-muted: 0.5;\n  }\n\n  /* Editor background */\n  .monaco-editor,\n  .monaco-editor .margin,\n  .monaco-editor .minimap-decorations-layer,\n  .monaco-editor-background {\n    background-color: var(--monaco-color-bg) !important;\n  }\n\n  /* Markdown docs inside hover/suggestion widgets */\n  .monaco-editor .markdown-docs {\n    h1,\n    h2,\n    h3 {\n      font-weight: bold;\n      margin: var(--monaco-size-m) 0;\n      font-size: var(--monaco-font-size-s);\n    }\n\n    h2 {\n      margin: var(--monaco-size-l) 0;\n      font-size: var(--monaco-font-size-m);\n    }\n\n    h1 {\n      margin: var(--monaco-size-xl) 0;\n      font-size: var(--monaco-font-size-l);\n    }\n\n    a {\n      color: var(--monaco-docs-link-color) !important;\n      text-decoration: underline !important;\n\n      &:hover {\n        text-decoration: none !important;\n      }\n    }\n  }\n\n  .monaco-editor .accessibilityHelpWidget {\n    overflow: auto;\n  }\n\n  .monaco-editor .suggest-widget {\n    max-width: 100% !important;\n    overflow: hidden;\n  }\n\n  /* Params line decoration */\n  .monaco-params-line {\n    color: var(--monaco-color-params) !important;\n  }\n\n  /* Run command glyph margin icon */\n  .monaco-glyph-run-command {\n    color: var(--monaco-color-submit);\n    margin-left: var(--monaco-size-s);\n    opacity: var(--monaco-opacity-muted) !important;\n\n    &::before {\n      content: \"\";\n      width: var(--monaco-size-xl);\n      height: var(--monaco-size-xl);\n      mask-image: url(\"uiSrc/assets/img/play_icon.svg\");\n      -webkit-mask-image: url(\"uiSrc/assets/img/play_icon.svg\");\n      background-color: var(--monaco-color-submit);\n      background-size: contain;\n      font-size: var(--monaco-size-xl);\n      font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n    }\n  }\n\n  /* DSL syntax content widget */\n  .monaco-widget {\n    display: flex !important;\n    align-items: center;\n    justify-content: space-around;\n    background: var(--monaco-widget-bg);\n    border: 1px solid var(--monaco-widget-border);\n    width: var(--monaco-widget-width);\n    height: var(--monaco-widget-height);\n    padding: var(--monaco-size-xs);\n    font-size: var(--monaco-size-m);\n    cursor: pointer;\n\n    &:hover {\n      background: var(--monaco-widget-hover-bg);\n      border-color: var(--monaco-widget-hover-border);\n    }\n  }\n\n  .monaco-widget__title {\n    color: var(--monaco-widget-title-color);\n  }\n\n  .monaco-widget__shortcut {\n    color: var(--monaco-widget-shortcut-color);\n  }\n\n  .monaco-widget * {\n    background: transparent;\n  }\n\n  /* Editor bounder overlay */\n  .editorBounder {\n    position: absolute;\n    top: var(--monaco-size-m);\n    bottom: 0;\n    left: 0;\n    right: 0;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/code-editor/CodeEditor.tsx",
    "content": "import React, { useContext } from 'react'\nimport ReactMonacoEditor from 'react-monaco-editor'\n\nimport { Theme } from 'uiSrc/constants'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\n\nimport { CodeEditorProps } from './CodeEditor.types'\nimport { MonacoGlobalStyles } from './CodeEditor.styles'\n\n/**\n * Thin wrapper around Monaco editor.\n * Provides an abstraction point for potential future editor changes.\n * Automatically handles theme from context.\n * Injects global Monaco styles (deduplicated by styled-components).\n */\nexport const CodeEditor = (props: CodeEditorProps) => {\n  const { theme } = useContext(ThemeContext)\n  const monacoTheme = props.theme ?? (theme === Theme.Dark ? 'dark' : 'light')\n\n  return (\n    <>\n      <MonacoGlobalStyles />\n      <ReactMonacoEditor {...props} theme={monacoTheme} />\n    </>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/code-editor/CodeEditor.types.ts",
    "content": "import { MonacoEditorProps } from 'react-monaco-editor'\n\n/**\n * CodeEditor props - extends Monaco's MonacoEditorProps.\n * This is the abstraction point: add custom props here if needed.\n */\nexport interface CodeEditorProps extends MonacoEditorProps {\n  /**\n   * Monaco editor theme. If not provided, automatically derived from ThemeContext.\n   * Pass explicitly to override the context-based theme.\n   */\n  theme?: string | null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/code-editor/index.ts",
    "content": "export { CodeEditor } from './CodeEditor'\nexport type { CodeEditorProps } from './CodeEditor.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/accordion/RiAccordion.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\n\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport { RiAccordion } from './RiAccordion'\nimport { RiAccordionProps } from './RiAccordion.types'\n\nconst accordionBody = faker.lorem.sentence()\nconst label = faker.lorem.words(2)\n\nconst expectBodyHidden = (content: string) => {\n  const body = screen.queryByText(content)\n  if (body) {\n    expect(body).not.toBeVisible()\n    return\n  }\n  expect(body).not.toBeInTheDocument()\n}\n\ndescribe('RiAccordion', () => {\n  const defaultProps: RiAccordionProps = {\n    id: faker.string.uuid(),\n    label,\n    actionButtonText: faker.lorem.words(2),\n  }\n\n  const renderComponent = (\n    propsOverride?: Partial<RiAccordionProps>,\n    body: React.ReactNode = accordionBody,\n  ) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(<RiAccordion {...props}>{body}</RiAccordion>)\n  }\n\n  describe('Default', () => {\n    it('Should render the label', () => {\n      renderComponent()\n\n      expect(screen.getByText(label)).toBeInTheDocument()\n    })\n\n    it('Should toggle open and closed when collapsible', async () => {\n      renderComponent({ collapsible: true, defaultOpen: false })\n\n      expectBodyHidden(accordionBody)\n\n      await userEvent.click(screen.getByText(label))\n      expect(screen.getByText(accordionBody)).toBeVisible()\n\n      await userEvent.click(screen.getByText(label))\n      expectBodyHidden(accordionBody)\n    })\n\n    it('Should keep content visible when not collapsible', async () => {\n      renderComponent({ collapsible: false })\n\n      expect(screen.getByText(accordionBody)).toBeVisible()\n\n      await userEvent.click(screen.getByText(label))\n      expect(screen.getByText(accordionBody)).toBeVisible()\n    })\n\n    it('Should call onAction when action button clicked', async () => {\n      const onAction = jest.fn()\n      const actionButtonText = faker.lorem.words(3)\n\n      renderComponent({ onAction, actionButtonText })\n\n      await userEvent.click(screen.getByText(actionButtonText))\n\n      expect(onAction).toHaveBeenCalledTimes(1)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/accordion/RiAccordion.tsx",
    "content": "import React, { isValidElement } from 'react'\nimport { Section } from '@redis-ui/components'\nimport { RiAccordionActionsProps, RiAccordionProps } from './RiAccordion.types'\n\nconst RiAccordionLabel = ({ label }: Pick<RiAccordionProps, 'label'>) => {\n  if (!label) {\n    return null\n  }\n  if (typeof label === 'string') {\n    return <Section.Header.Label label={label} />\n  }\n  // Ensure we always return a valid JSX element by wrapping non-JSX values\n  return isValidElement(label) ? label : <>{label}</>\n}\n\nconst RiAccordionActions = ({\n  actionButtonText,\n  actions,\n  onAction,\n}: RiAccordionActionsProps) => (\n  <Section.Header.Group>\n    <Section.Header.ActionButton onClick={onAction}>\n      {actionButtonText}\n    </Section.Header.ActionButton>\n    {actions}\n    <Section.Header.CollapseButton />\n  </Section.Header.Group>\n)\n\nexport const RiAccordion = ({\n  id,\n  content,\n  label,\n  onAction,\n  actionButtonText,\n  children,\n  actions,\n  collapsible = true,\n  ...rest\n}: RiAccordionProps) => (\n  <Section.Compose\n    id={`ri-accordion-${id}`}\n    data-testid={`ri-accordion-${id}`}\n    {...rest}\n    collapsible={collapsible}\n  >\n    <Section.Header.Compose\n      id={`ri-accordion-${id}`}\n      data-testid={`ri-accordion-header-${id}`}\n    >\n      <RiAccordionLabel\n        label={label}\n        data-testid={`ri-accordion-label-${id}`}\n      />\n      <RiAccordionActions\n        actions={actions}\n        onAction={onAction}\n        actionButtonText={actionButtonText}\n        data-testid={`ri-accordion-actions-${id}`}\n      />\n    </Section.Header.Compose>\n    <Section.Body data-testid={`ri-accordion-body-${id}`}>\n      {children ?? content}\n    </Section.Body>\n  </Section.Compose>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/accordion/RiAccordion.types.ts",
    "content": "import { ComponentProps, ReactNode } from 'react'\nimport { Section, SectionProps } from '@redis-ui/components'\n\nexport type RiAccordionProps = Omit<ComponentProps<typeof Section>, 'label'> & {\n  label: ReactNode\n  actions?: ReactNode\n  collapsible?: SectionProps['collapsible']\n  actionButtonText?: ReactNode\n  content?: SectionProps['content']\n  children?: SectionProps['content']\n  onAction?: () => void\n}\n\nexport type RiAccordionActionsProps = Pick<\n  RiAccordionProps,\n  'actionButtonText' | 'actions' | 'onAction'\n>\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/badge/RiBadge.tsx",
    "content": "import React from 'react'\nimport { Badge, BadgeVariants } from '@redis-ui/components'\n\ntype RiBadgeProps = Omit<React.ComponentProps<typeof Badge>, 'label'> & {\n  children?: React.ReactNode\n  label?: React.ReactNode\n}\nexport const RiBadge = ({ children, label, ...rest }: RiBadgeProps) => {\n  let internalLabel: React.ReactNode = label\n  if (children && !internalLabel) {\n    internalLabel = children\n  }\n  // Redis-UI badge accepts `string` as label, however in implementation it just renders it out, so any valid node will work\n  return <Badge {...rest} label={internalLabel as string} />\n}\n\nexport type { BadgeVariants }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/banner/index.ts",
    "content": "import { Banner } from '@redis-ui/components'\n\nexport { Banner }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/call-out/CallOut.tsx",
    "content": "import React from 'react'\nimport { Banner } from '@redis-ui/components'\n\nexport type CallOutProps = Omit<React.ComponentProps<typeof Banner>, 'show'> & {\n  children: React.ReactNode\n}\n\nexport const CallOut = ({ children, ...rest }: CallOutProps) => (\n  <Banner\n    {...rest}\n    show\n    showIcon={false}\n    layoutVariant=\"banner\"\n    message={children}\n  />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/collapsible-nav-group/RICollapsibleNavGroup.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport cx from 'classnames'\nimport {\n  RiAccordion,\n  RiAccordionProps,\n} from 'uiSrc/components/base/display/accordion/RiAccordion'\n\nexport type RICollapsibleNavGroupProps = Omit<\n  RiAccordionProps,\n  'collapsible' | 'content' | 'defaultOpen' | 'title' | 'label'\n> & {\n  title: ReactNode\n  children: ReactNode\n  isCollapsible?: boolean\n  className?: string\n  initialIsOpen?: boolean\n  onToggle?: (isOpen: boolean) => void\n  forceState?: 'open' | 'closed'\n}\nexport const RICollapsibleNavGroup = ({\n  children,\n  title,\n  isCollapsible = true,\n  className,\n  initialIsOpen,\n  onToggle,\n  forceState,\n  open,\n  ...rest\n}: RICollapsibleNavGroupProps) => (\n  <RiAccordion\n    {...rest}\n    collapsible={isCollapsible}\n    className={cx(className, 'RI-collapsible-nav-group')}\n    defaultOpen={initialIsOpen}\n    open={forceState === 'open' || open}\n    label={title}\n    onOpenChange={onToggle}\n  >\n    <div className=\"RI-collapsible-nav-group-content\">{children}</div>\n  </RiAccordion>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/image/RiImage.tsx",
    "content": "import React from 'react'\nimport { RiImageProps, StyledImage } from './image.styles'\n\nconst RiImage = ({ $size, src, alt, ...rest }: RiImageProps) => (\n  <StyledImage src={src} alt={alt} $size={$size} {...rest} />\n)\n\nexport default RiImage\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/image/image.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled, { css } from 'styled-components'\n\nexport const SIZES = [\n  'xs',\n  's',\n  'm',\n  'l',\n  'xl',\n  'original',\n  'fullWidth',\n] as const\n\nexport const imageSizeStyles = {\n  xs: css`\n    width: 50px;\n  `,\n  s: css`\n    width: 100px;\n  `,\n  m: css`\n    width: 200px;\n  `,\n  l: css`\n    width: 360px;\n  `,\n  xl: css`\n    width: 600px;\n  `,\n  original: css`\n    width: auto;\n  `,\n  fullWidth: css`\n    width: 100%;\n  `,\n}\n\nexport type RiImageSize = (typeof SIZES)[number]\n\nexport interface RiImageProps extends HTMLAttributes<HTMLImageElement> {\n  $size?: RiImageSize\n  src: string\n  alt: string\n}\n\nexport const StyledImage = styled.img<RiImageProps>`\n  ${({ $size = 'original' }) => imageSizeStyles[$size]}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/index.ts",
    "content": "import Loader from './loader/Loader'\nimport ProgressBarLoader from './progress-bar/ProgressBarLoader'\nimport RiImage from './image/RiImage'\nimport RiLoadingLogo from './loading-logo/RiLoadingLogo'\nimport { Modal } from './modal'\nimport { Banner } from './banner'\n\nexport { Loader, ProgressBarLoader, RiImage, RiLoadingLogo, Modal, Banner }\n\nexport { RICollapsibleNavGroup } from './collapsible-nav-group/RICollapsibleNavGroup'\n\nexport type { RICollapsibleNavGroupProps } from './collapsible-nav-group/RICollapsibleNavGroup'\nexport * from './section'\n\nexport * from './call-out/CallOut'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/loader/Loader.tsx",
    "content": "import React, { ComponentProps } from 'react'\nimport { Loader as RedisLoader } from '@redis-ui/components'\nimport { useTheme } from '@redis-ui/styles'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport type RedisLoaderProps = ComponentProps<typeof RedisLoader>\n\nconst convertSizeToPx = (tShirtSize: string, space: Theme['core']['space']) => {\n  switch (tShirtSize.toLowerCase()) {\n    case 's':\n      return space.space050\n    case 'm':\n      return space.space100\n    case 'l':\n      return space.space250\n    case 'xl':\n      return space.space300\n    default:\n      return space.space100\n  }\n}\n\nconst Loader = ({ size, ...rest }: RedisLoaderProps) => {\n  const theme = useTheme()\n  const { space } = theme.core\n  const sizeInPx = size ? convertSizeToPx(size, space) : space.space100\n  return <RedisLoader size={sizeInPx} {...rest} />\n}\n\nexport default Loader\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/loading-logo/RiLoadingLogo.tsx",
    "content": "import React, { HTMLAttributes } from 'react'\nimport styled, { keyframes } from 'styled-components'\n\nconst bounce = keyframes`\n  0%, 100% {\n    transform: translateY(0);\n  }\n\n  50% {\n    transform: translateY(-15px);\n  }\n`\n\nexport const SIZES = ['M', 'L', 'XL', 'XXL'] as const\n\nexport type RiLoadingLogoSize = (typeof SIZES)[number]\n\nexport interface RiLoadingLogoProps extends HTMLAttributes<HTMLImageElement> {\n  src: string\n  $size?: RiLoadingLogoSize\n  $bounceSpeed?: number\n  alt?: string\n}\n\nconst Wrapper = styled.div`\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n`\n\nconst BouncingLogo = styled.img<RiLoadingLogoProps>`\n  width: ${({ theme, $size = 'XL' }) =>\n    theme.components.iconButton.sizes[$size].width};\n  animation: ${bounce} ${({ $bounceSpeed }) => $bounceSpeed}s ease-in-out\n    infinite;\n`\n\nconst RiLoadingLogo = ({\n  src,\n  $size = 'XL',\n  $bounceSpeed = 1,\n  alt = 'Loading logo',\n}: RiLoadingLogoProps) => (\n  <Wrapper>\n    <BouncingLogo\n      src={src}\n      $size={$size}\n      $bounceSpeed={$bounceSpeed}\n      alt={alt}\n    />\n  </Wrapper>\n)\n\nexport default RiLoadingLogo\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/modal/index.ts",
    "content": "export { Modal } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/progress-bar/ProgressBarLoader.tsx",
    "content": "import React from 'react'\nimport {\n  LoaderBar,\n  ProgressBarLoaderProps,\n  LoaderContainer,\n} from './progress-bar-loader.styles'\n\nconst ProgressBarLoader = ({\n  className,\n  style,\n  color,\n  ...rest\n}: ProgressBarLoaderProps) => (\n  <LoaderContainer className={className} style={style} {...rest}>\n    <LoaderBar $color={color} />\n  </LoaderContainer>\n)\n\nexport default ProgressBarLoader\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/progress-bar/progress-bar-loader.styles.ts",
    "content": "import { Theme, theme } from '@redis-ui/styles'\nimport { ReactNode } from 'react'\nimport styled, { css, keyframes } from 'styled-components'\n\nexport type EuiColorNames =\n  | 'inherit'\n  | 'default'\n  | 'primary'\n  | 'danger'\n  | 'warning'\n  | 'success'\n\ninterface LoaderBarProps {\n  color?: string\n}\n\nexport type ColorType = EuiColorNames | (string & {})\ntype ThemeColors = typeof theme.semantic.color\n\nexport const getBarBackgroundColor = (\n  themeColors: ThemeColors,\n  color?: ColorType,\n) => {\n  if (!color) {\n    return themeColors.background.primary300\n  }\n\n  const barBackgroundColors: Record<ColorType, string> = {\n    inherit: 'inherit',\n    default: themeColors.background.primary300,\n    primary: themeColors.background.primary300,\n    danger: themeColors.background.danger600,\n    warning: themeColors.background.attention600,\n    success: themeColors.background.success600,\n  }\n\n  return barBackgroundColors[color] ?? color\n}\n\nexport interface MapProps extends LoaderBarProps {\n  $color?: ColorType\n  theme: Theme\n}\n\nexport const getColorBackgroundStyles = ({ $color, theme }: MapProps) => {\n  const colors = theme.semantic.color\n\n  const getColorValue = (color?: ColorType) =>\n    getBarBackgroundColor(colors, color)\n\n  return css`\n    background-color: ${getColorValue($color)};\n  `\n}\n\nconst loading = keyframes`\n  0% {\n    transform: scaleX(1) translateX(-100%);\n  }\n  100% {\n    transform: scaleX(1) translateX(100%);\n  }\n`\n\ninterface LoaderContainerProps {\n  children?: ReactNode\n  style?: React.CSSProperties\n  className?: string\n  absolute?: boolean\n}\n\nexport const LoaderContainer = styled.div<LoaderContainerProps>`\n  position: ${({ absolute }) => (absolute ? 'absolute' : 'relative')};\n  width: 100%;\n  height: 3px;\n  overflow: hidden;\n  border-radius: 2px;\n`\n\nexport const LoaderBar = styled.div<MapProps>`\n  ${({ $color, theme }) => getColorBackgroundStyles({ $color, theme })};\n\n  position: absolute;\n  height: 100%;\n  width: 100%;\n  border-radius: 2px;\n\n  animation: ${loading} 1s ease-in-out infinite;\n`\n\nexport type ProgressBarLoaderProps = LoaderContainerProps & LoaderBarProps\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/section/index.ts",
    "content": "export { Section } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/toast/RiToast.tsx",
    "content": "import React from 'react'\nimport {\n  Toast,\n  toast,\n  ToastContentParams,\n  ToastOptions,\n  ToastVariant,\n} from '@redis-ui/components'\nimport { ToastOptions as RcToastOptions } from 'react-toastify'\n\nimport { CommonProps } from 'uiSrc/components/base/theme/types'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { ColorType } from 'uiSrc/components/base/text/text.styles'\n\ntype RiToastProps = React.ComponentProps<typeof Toast>\nexport const RiToast = (props: RiToastProps) => <Toast {...props} />\n\nexport type RiToastType = ToastContentParams &\n  CommonProps & {\n    onClose?: VoidFunction\n  }\nexport const riToast = (\n  { onClose, message, ...content }: RiToastType,\n  options?: ToastOptions | undefined,\n) => {\n  const toastContent: ToastContentParams = {\n    ...content,\n  }\n\n  if (typeof message === 'string') {\n    let color: ColorType = options?.variant\n    if (color === 'informative') {\n      color = 'subdued'\n    }\n    toastContent.message = (\n      <Text size=\"M\" variant=\"semiBold\" component=\"span\">\n        <ColorText color={color} component=\"span\">\n          {message}\n        </ColorText>\n      </Text>\n    )\n  } else {\n    toastContent.message = message\n  }\n\n  const toastOptions: ToastOptions & RcToastOptions = {\n    ...options,\n    delay: 100,\n    closeOnClick: false,\n    onClose,\n  }\n  return toast(<RiToast {...toastContent} />, toastOptions)\n}\nriToast.Variant = toast.Variant\nriToast.Position = toast.Position\nriToast.dismiss = toast.dismiss\nriToast.isActive = toast.isActive\n\nexport type { ToastVariant }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/toast/RiToaster.tsx",
    "content": "import React from 'react'\nimport { toast, Toaster } from '@redis-ui/components'\n\ntype RiToasterProps = React.ComponentProps<typeof Toaster>\nconst DEFAULT_LIFETIME = 6000\n\nexport const RiToaster = (props: RiToasterProps) => (\n  <Toaster\n    position={toast.Position.BottomRight}\n    newestOnTop\n    pauseOnHover\n    autoClose={DEFAULT_LIFETIME}\n    {...props}\n  />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/toast/index.ts",
    "content": "export { RiToaster } from './RiToaster'\nexport { RiToast, riToast } from './RiToast'\nexport type { ToastVariant } from './RiToast'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/tour/TourStep.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { Popover } from '@redis-ui/components'\nimport {\n  PopoverPlacementMapType,\n  TourStepProps,\n} from 'uiSrc/components/base/display/tour/types'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\n\nconst popoverPlacementMap: PopoverPlacementMapType = {\n  upCenter: {\n    placement: 'top',\n    align: 'center',\n  },\n  upLeft: {\n    placement: 'top',\n    align: 'start',\n  },\n  upRight: {\n    placement: 'top',\n    align: 'end',\n  },\n  downCenter: {\n    placement: 'bottom',\n    align: 'center',\n  },\n  downLeft: {\n    placement: 'bottom',\n    align: 'start',\n  },\n  downRight: {\n    placement: 'bottom',\n    align: 'end',\n  },\n  leftCenter: {\n    placement: 'left',\n    align: 'center',\n  },\n  leftUp: {\n    placement: 'left',\n    align: 'start',\n  },\n  leftDown: {\n    placement: 'left',\n    align: 'end',\n  },\n  rightCenter: {\n    placement: 'right',\n    align: 'center',\n  },\n  rightUp: {\n    placement: 'right',\n    align: 'start',\n  },\n  rightDown: {\n    placement: 'right',\n    align: 'end',\n  },\n}\n\nexport const TourStep = ({\n  open,\n  content,\n  title,\n  placement = 'rightUp',\n  className = '',\n  children,\n  minWidth = 300,\n  maxWidth,\n  offset = 5,\n  ...rest\n}: TourStepProps) => {\n  const [isVisible, setIsVisible] = useState(open)\n  const id = useGenerateId()\n  const titleId = `${id}-title`\n\n  useEffect(() => {\n    setIsVisible(open)\n  }, [open])\n\n  if (!isVisible) {\n    return null\n  }\n  const place = popoverPlacementMap[placement]\n  const popoverContent = (\n    <Popover.Card.Compose style={{ minWidth, maxWidth }}>\n      <Popover.Card.Header.Compose id={titleId}>\n        {title}\n      </Popover.Card.Header.Compose>\n      <Popover.Card.Body.Compose>{content}</Popover.Card.Body.Compose>\n    </Popover.Card.Compose>\n  )\n  return (\n    <Popover\n      className={className}\n      open={isVisible}\n      placement={place.placement}\n      align={place.align}\n      sideOffset={offset}\n      alignOffset={-10}\n      content={popoverContent}\n      id={id}\n      aria-labelledby={titleId}\n      {...rest}\n      withButton\n      persistent\n    >\n      {children}\n    </Popover>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/display/tour/types.ts",
    "content": "import React, { ReactNode } from 'react'\nimport { Popover } from '@redis-ui/components'\n\nexport type TourStepProps = {\n  /**\n   * Contents of the tour step popover\n   */\n  content: ReactNode\n  /**\n   * Step will display if set to `true`\n   */\n  open?: boolean\n  /**\n   * The title text that appears atop each step in the tour.\n   */\n  title?: ReactNode\n  placement?:\n    | 'upCenter'\n    | 'upLeft'\n    | 'upRight'\n    | 'downCenter'\n    | 'downLeft'\n    | 'downRight'\n    | 'leftCenter'\n    | 'leftUp'\n    | 'leftDown'\n    | 'rightCenter'\n    | 'rightUp'\n    | 'rightDown'\n  className?: string\n  children?: ReactNode\n  minWidth?: number | string\n  maxWidth?: number | string\n  offset?: number\n}\ntype PopoverTypes = React.ComponentProps<typeof Popover>\n\nexport type PopoverPlacementMapType = Record<\n  NonNullable<TourStepProps['placement']>,\n  {\n    placement: PopoverTypes['placement']\n    align: PopoverTypes['align']\n  }\n>\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/FormField.tsx",
    "content": "import React, { ComponentProps } from 'react'\nimport {\n  FormField as RedisFormField,\n  TooltipProvider,\n  LabelProps,\n} from '@redis-ui/components'\n\nexport type RiInfoIconProps = LabelProps['infoIconProps']\n\nexport type RedisFormFieldProps = ComponentProps<typeof RedisFormField> & {\n  infoIconProps?: RiInfoIconProps\n}\n\nexport function FormField(props: RedisFormFieldProps) {\n  // eslint-disable-next-line react/destructuring-assignment\n  if (props.infoIconProps) {\n    return (\n      <TooltipProvider>\n        <RedisFormField {...props} />\n      </TooltipProvider>\n    )\n  }\n  return <RedisFormField {...props} />\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/button-group/ButtonGroup.tsx",
    "content": "import { ButtonGroup, ButtonGroupProps } from '@redis-ui/components'\n\nexport { ButtonGroup }\n\nexport type { ButtonGroupProps }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/ActionIconButton.stories.tsx",
    "content": "// Action Icon Button Stories\nimport { fn } from 'storybook/test'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { ActionIconButton } from './ActionIconButton'\nimport { ActiveActiveIcon } from 'uiSrc/components/base/icons'\n\nconst actionIconMeta = {\n  component: ActionIconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof ActionIconButton>\n\nexport default actionIconMeta\ntype ActionIconStory = StoryObj<typeof actionIconMeta>\n\nexport const ActionIconDefault: ActionIconStory = {\n  args: {\n    icon: ActiveActiveIcon,\n    'aria-label': 'More actions',\n  },\n}\n\nexport const ActionIconSmall: ActionIconStory = {\n  args: {\n    icon: ActiveActiveIcon,\n    size: 'S',\n    'aria-label': 'Filter',\n  },\n}\n\nexport const ActionIconDisabled: ActionIconStory = {\n  args: {\n    icon: ActiveActiveIcon,\n    disabled: true,\n    'aria-label': 'Search',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/ActionIconButton.tsx",
    "content": "import React from 'react'\nimport { ActionIconButton as RedisUiActionIconButton } from '@redis-ui/components'\n\nexport type ButtonProps = React.ComponentProps<typeof RedisUiActionIconButton>\nexport const ActionIconButton = (props: ButtonProps) => (\n  <RedisUiActionIconButton {...props} />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/Button.stories.tsx",
    "content": "// Base Button Stories\nimport { fn } from 'storybook/test'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { Button } from './index'\n\nconst baseMeta = {\n  component: Button,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof Button>\n\nexport default baseMeta\ntype BaseStory = StoryObj<typeof baseMeta>\n\nexport const Default: BaseStory = {\n  args: {\n    children: 'Base Button',\n  },\n}\n\nexport const Small: BaseStory = {\n  args: {\n    children: 'Small Button',\n    size: 'small',\n  },\n}\n\nexport const Medium: BaseStory = {\n  args: {\n    children: 'Medium Button',\n    size: 'medium',\n  },\n}\n\nexport const Large: BaseStory = {\n  args: {\n    children: 'Large Button',\n    size: 'large',\n  },\n}\n\nexport const Disabled: BaseStory = {\n  args: {\n    children: 'Disabled Button',\n    disabled: true,\n  },\n}\n\nexport const Loading: BaseStory = {\n  args: {\n    children: 'Loading Button',\n    loading: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/Button.tsx",
    "content": "import { Button } from '@redis-ui/components'\nimport React from 'react'\nimport { LoaderLargeIcon } from 'uiSrc/components/base/icons'\nimport { BaseButtonProps } from 'uiSrc/components/base/forms/buttons/button.styles'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport styled from 'styled-components'\n\ntype ButtonSize = 'small' | 'medium' | 'large'\ntype SizeKey = 'small' | 's' | 'medium' | 'm' | 'large' | 'l'\n\nconst buttonSizeMap: Record<SizeKey, ButtonSize> = {\n  small: 'small',\n  s: 'small',\n  medium: 'medium',\n  m: 'medium',\n  large: 'large',\n  l: 'large',\n}\nexport const BaseButton = ({\n  children,\n  icon,\n  iconSide = 'left',\n  loading,\n  size = 'medium',\n  ...props\n}: BaseButtonProps) => {\n  let btnSize: ButtonSize = 'medium'\n\n  if (size in buttonSizeMap) {\n    btnSize = buttonSizeMap[size]\n  }\n\n  return (\n    <Button {...props} size={btnSize} disabled={props.disabled || loading}>\n      <ButtonIcon\n        buttonSide=\"left\"\n        icon={icon}\n        iconSide={iconSide}\n        loading={loading}\n      />\n      {children}\n      <ButtonIcon\n        buttonSide=\"right\"\n        icon={icon}\n        iconSide={iconSide}\n        loading={loading}\n      />\n    </Button>\n  )\n}\n\nexport type ButtonIconProps = Pick<\n  BaseButtonProps,\n  'icon' | 'iconSide' | 'loading'\n> & {\n  buttonSide: 'left' | 'right'\n  size?: 'small' | 'large' | 'medium'\n}\nexport const IconSizes = {\n  small: '16px',\n  medium: '20px',\n  large: '24px',\n}\nconst Wrapper = styled.div`\n  svg {\n    display: block;\n  }\n`\nexport const ButtonIcon = ({\n  buttonSide,\n  icon,\n  iconSide,\n  loading,\n  size,\n}: ButtonIconProps) => {\n  // if iconSide is not the same as side of the button, don't render\n  if (iconSide !== buttonSide) {\n    return null\n  }\n  let renderIcon = icon\n  if (loading) {\n    renderIcon = LoaderLargeIcon\n  }\n  if (!renderIcon) {\n    return null\n  }\n  let iconSize: string | undefined\n  if (size) {\n    iconSize = IconSizes[size]\n  }\n  const spacer = <Spacer size=\"s\" direction=\"horizontal\" />\n  return (\n    <Wrapper>\n      {buttonSide === 'right' && spacer}\n      <Button.Icon icon={renderIcon} customSize={iconSize} />\n      {buttonSide === 'left' && spacer}\n    </Wrapper>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/DestructiveButton.stories.tsx",
    "content": "// Destructive Button Stories\nimport { fn } from 'storybook/test'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { DestructiveButton } from './DestructiveButton'\n\nconst destructiveMeta = {\n  component: DestructiveButton,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof DestructiveButton>\nexport default destructiveMeta\n\ntype DestructiveStory = StoryObj<typeof destructiveMeta>\n\nexport const DestructiveDefault: DestructiveStory = {\n  args: {\n    children: 'Delete',\n  },\n}\n\nexport const DestructiveSmall: DestructiveStory = {\n  args: {\n    children: 'Remove',\n    size: 'small',\n  },\n}\n\nexport const DestructiveDisabled: DestructiveStory = {\n  args: {\n    children: 'Cannot Delete',\n    disabled: true,\n  },\n}\n\nexport const DestructiveLoading: DestructiveStory = {\n  args: {\n    children: 'Deleting...',\n    loading: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/DestructiveButton.tsx",
    "content": "import React from 'react'\nimport { ButtonProps } from 'uiSrc/components/base/forms/buttons/button.styles'\nimport { BaseButton } from 'uiSrc/components/base/forms/buttons/Button'\n\nexport const DestructiveButton = (props: ButtonProps) => (\n  <BaseButton {...props} variant=\"destructive\" />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/EmptyButton.stories.tsx",
    "content": "// Empty Button Stories\nimport { EmptyButton } from './EmptyButton'\nimport { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nconst emptyMeta = {\n  component: EmptyButton,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof EmptyButton>\nexport default emptyMeta\n\ntype EmptyStory = StoryObj<typeof emptyMeta>\n\nexport const EmptyDefault: EmptyStory = {\n  args: {\n    children: 'Empty Button',\n  },\n}\n\nexport const EmptySmall: EmptyStory = {\n  args: {\n    children: 'Small Empty',\n    size: 'small',\n  },\n}\n\nexport const EmptyMedium: EmptyStory = {\n  args: {\n    children: 'Medium Empty',\n    size: 'medium',\n  },\n}\n\nexport const EmptyLarge: EmptyStory = {\n  args: {\n    children: 'Large Empty',\n    size: 'large',\n  },\n}\n\nexport const EmptyDisabled: EmptyStory = {\n  args: {\n    children: 'Disabled Empty',\n    disabled: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/EmptyButton.tsx",
    "content": "import React from 'react'\nimport { TextButton } from '@redis-ui/components'\nimport { ButtonIcon } from 'uiSrc/components/base/forms/buttons/Button'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nimport { EmptyButtonProps } from './EmptyButton.types'\n\nexport const EmptyButton = React.forwardRef<\n  HTMLButtonElement,\n  EmptyButtonProps\n>(\n  (\n    {\n      children,\n      icon,\n      iconSide = 'left',\n      loading,\n      size = 'small',\n      justify = 'center',\n      ...rest\n    },\n    ref,\n  ) => (\n    <TextButton ref={ref} {...rest}>\n      {icon ? (\n        <Row justify={justify} gap=\"m\" align=\"center\">\n          <ButtonIcon\n            buttonSide=\"left\"\n            icon={icon}\n            iconSide={iconSide}\n            loading={loading}\n            size={size}\n          />\n          {children}\n          <ButtonIcon\n            buttonSide=\"right\"\n            icon={icon}\n            iconSide={iconSide}\n            loading={loading}\n            size={size}\n          />\n        </Row>\n      ) : (\n        children\n      )}\n    </TextButton>\n  ),\n)\n\nEmptyButton.displayName = 'EmptyButton'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/EmptyButton.types.ts",
    "content": "import { TextButton } from '@redis-ui/components'\nimport { IconType } from 'uiSrc/components/base/icons'\nimport { FlexProps } from 'uiSrc/components/base/layout/flex/flex.styles'\n\nexport type EmptyButtonProps = React.ComponentProps<typeof TextButton> & {\n  icon?: IconType\n  iconSide?: 'left' | 'right'\n  loading?: boolean\n  size?: 'small' | 'large' | 'medium'\n  justify?: FlexProps['justify']\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/IconButton.stories.tsx",
    "content": "// Icon Button Stories\nimport { fn } from 'storybook/test'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { IconButton } from './IconButton'\n\nconst iconMeta = {\n  component: IconButton,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof IconButton>\nexport default iconMeta\ntype IconStory = StoryObj<typeof iconMeta>\n\nexport const IconDefault: IconStory = {\n  args: {\n    icon: 'ActiveActiveIcon',\n    'aria-label': 'Add',\n  },\n}\n\nexport const IconSmall: IconStory = {\n  args: {\n    icon: 'EditIcon',\n    size: 'S',\n    'aria-label': 'Edit',\n  },\n}\n\nexport const IconMedium: IconStory = {\n  args: {\n    icon: 'DeleteIcon',\n    size: 'M',\n    'aria-label': 'Delete',\n  },\n}\n\nexport const IconLarge: IconStory = {\n  args: {\n    icon: 'InfoIcon',\n    size: 'L',\n    'aria-label': 'Settings',\n  },\n}\n\nexport const IconDisabled: IconStory = {\n  args: {\n    icon: 'ActiveActiveIcon',\n    disabled: true,\n    'aria-label': 'Close',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/IconButton.tsx",
    "content": "import React from 'react'\nimport { IconButton as RedisUiIconButton } from '@redis-ui/components'\nimport * as Icons from 'uiSrc/components/base/icons/iconRegistry'\nimport { AllIconsType } from 'uiSrc/components/base/icons'\n\nexport type ButtonProps = React.ComponentProps<typeof RedisUiIconButton>\n\nexport type IconType = ButtonProps['icon']\nexport type IconButtonProps = Omit<ButtonProps, 'icon'> & {\n  icon: IconType | string\n}\nexport const IconButton = ({\n  icon,\n  size: _size,\n  ...props\n}: IconButtonProps) => {\n  let buttonIcon: IconType\n  if (typeof icon === 'string') {\n    buttonIcon = Icons[icon as AllIconsType]\n  } else {\n    buttonIcon = icon\n  }\n  return <RedisUiIconButton icon={buttonIcon} {...props} />\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/PrimaryButton.stories.tsx",
    "content": "// Primary Button Stories\nimport { fn } from 'storybook/test'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { PrimaryButton } from './PrimaryButton'\n\nconst primaryMeta = {\n  component: PrimaryButton,\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n  parameters: {\n    layout: 'centered',\n  },\n} satisfies Meta<typeof PrimaryButton>\n\nexport default primaryMeta\ntype PrimaryStory = StoryObj<typeof primaryMeta>\n\nexport const PrimaryDefault: PrimaryStory = {\n  args: {\n    children: 'Primary Button',\n  },\n}\n\nexport const PrimarySmall: PrimaryStory = {\n  args: {\n    children: 'Small Primary',\n    size: 'small',\n  },\n}\n\nexport const PrimaryLarge: PrimaryStory = {\n  args: {\n    children: 'Large Primary',\n    size: 'large',\n  },\n}\n\nexport const PrimaryDisabled: PrimaryStory = {\n  args: {\n    children: 'Disabled Primary',\n    disabled: true,\n  },\n}\n\nexport const PrimaryLoading: PrimaryStory = {\n  args: {\n    children: 'Loading Primary',\n    loading: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/PrimaryButton.tsx",
    "content": "import React from 'react'\nimport { BaseButton } from 'uiSrc/components/base/forms/buttons/Button'\nimport { ButtonProps } from 'uiSrc/components/base/forms/buttons/button.styles'\n\nexport const PrimaryButton = (props: ButtonProps) => (\n  <BaseButton {...props} variant=\"primary\" />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/SecondaryButton.stories.tsx",
    "content": "// Secondary Button Stories\nimport { fn } from 'storybook/test'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { SecondaryButton } from './SecondaryButton'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\n\nconst secondaryMeta = {\n  component: SecondaryButton,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof SecondaryButton>\nexport default secondaryMeta\ntype SecondaryStory = StoryObj<typeof secondaryMeta>\n\nexport const SecondaryDefault: SecondaryStory = {\n  args: {\n    children: 'Secondary Button',\n  },\n}\n\nexport const SecondaryFilled: SecondaryStory = {\n  args: {\n    children: 'Secondary Filled',\n    filled: true,\n  },\n}\n\nexport const SecondaryInverted: SecondaryStory = {\n  args: {\n    children: 'Secondary Inverted',\n    inverted: true,\n  },\n}\n\nexport const SecondarySmall: SecondaryStory = {\n  args: {\n    children: 'Small Secondary',\n    size: 'small',\n  },\n}\n\nexport const SecondaryDisabled: SecondaryStory = {\n  args: {\n    children: 'Disabled Secondary',\n    disabled: true,\n  },\n}\n\nexport const SecondaryLoading: SecondaryStory = {\n  args: {\n    children: 'Loading Secondary',\n    loading: true,\n  },\n}\n\nexport const SecondaryIcon: SecondaryStory = {\n  args: {\n    children: 'Secondary Icon Default',\n    icon: InfoIcon,\n  },\n}\n\nexport const SecondaryIconRight: SecondaryStory = {\n  args: {\n    children: 'Secondary Icon Right',\n    icon: InfoIcon,\n    iconSide: 'right',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/SecondaryButton.tsx",
    "content": "import React from 'react'\nimport {\n  BaseButtonProps,\n  SecondaryButtonProps,\n} from 'uiSrc/components/base/forms/buttons/button.styles'\nimport { BaseButton } from 'uiSrc/components/base/forms/buttons/Button'\n\nexport const SecondaryButton = ({\n  filled = false,\n  inverted,\n  ...props\n}: SecondaryButtonProps) => {\n  let variant: BaseButtonProps['variant'] = 'secondary-fill'\n\n  if (filled === false) {\n    variant = 'secondary-ghost'\n  }\n  if (inverted === true) {\n    variant = 'secondary-invert'\n  }\n  return <BaseButton {...props} variant={variant} />\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/ToggleButton.stories.tsx",
    "content": "// Toggle Button Stories\nimport { ToggleButton } from './ToggleButton'\nimport { fn } from 'storybook/test'\nimport { Meta, StoryObj } from '@storybook/react-vite'\n\nconst toggleMeta = {\n  component: ToggleButton,\n  parameters: {\n    layout: 'centered',\n  },\n  tags: ['autodocs'],\n  args: { onClick: fn() },\n} satisfies Meta<typeof ToggleButton>\n\nexport default toggleMeta\n\ntype ToggleStory = StoryObj<typeof toggleMeta>\n\nexport const ToggleOff: ToggleStory = {\n  args: {\n    children: 'Toggle Off',\n    pressed: false,\n    'aria-label': 'Toggle feature',\n  },\n}\n\nexport const ToggleOn: ToggleStory = {\n  args: {\n    children: 'Toggle On',\n    pressed: true,\n    'aria-label': 'Toggle feature',\n  },\n}\n\nexport const ToggleDisabled: ToggleStory = {\n  args: {\n    children: 'Toggle Disabled',\n    pressed: false,\n    disabled: true,\n    'aria-label': 'Toggle feature',\n  },\n}\n\nexport const ToggleDisabledOn: ToggleStory = {\n  args: {\n    children: 'Toggle Disabled On',\n    pressed: true,\n    disabled: true,\n    'aria-label': 'Toggle feature',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/ToggleButton.tsx",
    "content": "export { ToggleButton } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/button.styles.ts",
    "content": "import React from 'react'\nimport { Button } from '@redis-ui/components'\nimport { IconType } from 'uiSrc/components/base/icons'\n\ntype RedisUiButtonProps = React.ComponentProps<typeof Button>\nexport type BaseButtonProps = Omit<RedisUiButtonProps, 'size'> & {\n  icon?: IconType\n  iconSide?: 'left' | 'right'\n  loading?: boolean\n  size?: RedisUiButtonProps['size'] | 's' | 'm' | 'l'\n}\nexport type ButtonProps = Omit<BaseButtonProps, 'variant'>\nexport type SecondaryButtonProps = ButtonProps & {\n  filled?: boolean\n  inverted?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/buttons/index.ts",
    "content": "export { ActionIconButton } from 'uiSrc/components/base/forms/buttons/ActionIconButton'\nexport { BaseButton as Button } from 'uiSrc/components/base/forms/buttons/Button'\nexport { DestructiveButton } from 'uiSrc/components/base/forms/buttons/DestructiveButton'\nexport { EmptyButton } from 'uiSrc/components/base/forms/buttons/EmptyButton'\nexport type { EmptyButtonProps } from 'uiSrc/components/base/forms/buttons/EmptyButton.types'\nexport { IconButton } from 'uiSrc/components/base/forms/buttons/IconButton'\nexport { PrimaryButton } from 'uiSrc/components/base/forms/buttons/PrimaryButton'\nexport { SecondaryButton } from 'uiSrc/components/base/forms/buttons/SecondaryButton'\nexport { ToggleButton } from 'uiSrc/components/base/forms/buttons/ToggleButton'\n\nexport type { IconType } from 'uiSrc/components/base/forms/buttons/IconButton'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/checkbox/Checkbox.test.tsx",
    "content": "import React from 'react'\nimport { fireEvent } from '@testing-library/react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { Checkbox } from './Checkbox'\n\ndescribe('Checkbox', () => {\n  it('Should render checkbox', () => {\n    render(<Checkbox label=\"Checkbox Label\" />)\n\n    expect(screen.getByText('Checkbox Label')).toBeInTheDocument()\n  })\n\n  describe('Checkbox states', () => {\n    it('Should render disabled checkbox when disabled prop is passed', () => {\n      render(<Checkbox id=\"id1\" label=\"Checkbox Label\" disabled />)\n\n      expect(screen.getByRole('checkbox')).toBeDisabled()\n    })\n    it('Should render un-checked checkbox when checked prop is passed as false', () => {\n      render(<Checkbox id=\"id1\" label=\"Checkbox Label\" checked={false} />)\n\n      const checkbox = screen.getByRole('checkbox')\n      expect(checkbox).toHaveAttribute('aria-checked', 'false')\n    })\n    it('Should render checked checkbox when checked prop is passed as true', () => {\n      render(<Checkbox id=\"id1\" label=\"Checkbox Label\" checked />)\n\n      const checkbox = screen.getByRole('checkbox')\n      expect(checkbox).toHaveAttribute('aria-checked', 'true')\n    })\n    it('Should render indeterminate checkbox when checked prop is passed as indeterminate', () => {\n      render(\n        <Checkbox id=\"id1\" label=\"Checkbox Label\" checked=\"indeterminate\" />,\n      )\n\n      const checkbox = screen.getByRole('checkbox')\n      expect(checkbox).toHaveValue('on')\n      expect(screen.getByLabelText('Minus')).toBeInTheDocument()\n    })\n  })\n\n  describe('change handlers', () => {\n    it('Should call handlers when checkbox is clicked with thruthy values', () => {\n      const onChange = jest.fn()\n      const onCheckedChange = jest.fn()\n      render(\n        <Checkbox\n          id=\"id1\"\n          label=\"Checkbox Label\"\n          onChange={onChange}\n          onCheckedChange={onCheckedChange}\n        />,\n      )\n      const checkbox = screen.getByRole('checkbox')\n      fireEvent.click(checkbox)\n      expect(onChange).toHaveBeenCalled()\n      expect(onChange).toHaveBeenCalledWith({\n        target: {\n          checked: true,\n          type: 'checkbox',\n          name: undefined,\n          id: 'id1',\n        },\n      })\n      expect(onCheckedChange).toHaveBeenCalled()\n      expect(onCheckedChange).toHaveBeenCalledWith(true)\n    })\n    it('Should call handlers when checkbox is clicked with falsy values', () => {\n      const onChange = jest.fn()\n      const onCheckedChange = jest.fn()\n      render(\n        <Checkbox\n          id=\"id1\"\n          label=\"Checkbox Label\"\n          onChange={onChange}\n          onCheckedChange={onCheckedChange}\n          checked\n        />,\n      )\n      const checkbox = screen.getByRole('checkbox')\n      fireEvent.click(checkbox)\n      expect(onChange).toHaveBeenCalled()\n      expect(onChange).toHaveBeenCalledWith({\n        target: {\n          checked: false,\n          type: 'checkbox',\n          name: undefined,\n          id: 'id1',\n        },\n      })\n      expect(onCheckedChange).toHaveBeenCalled()\n      expect(onCheckedChange).toHaveBeenCalledWith(false)\n    })\n    it('Should change state when clicked', () => {\n      render(<Checkbox id=\"id1\" label=\"Checkbox Label\" defaultChecked />)\n      const checkbox = screen.getByRole('checkbox')\n      expect(checkbox).toHaveAttribute('aria-checked', 'true')\n      fireEvent.click(checkbox)\n      expect(checkbox).toHaveAttribute('aria-checked', 'false')\n      fireEvent.click(checkbox)\n      expect(checkbox).toHaveAttribute('aria-checked', 'true')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/checkbox/Checkbox.tsx",
    "content": "import React, { ChangeEvent } from 'react'\nimport {\n  Checkbox as RedisUiCheckbox,\n  CheckedType,\n  Typography,\n} from '@redis-ui/components'\nimport { BodyProps } from 'uiSrc/components/base/text/text.styles'\n\nexport type CheckboxProps = Omit<\n  React.ComponentProps<typeof RedisUiCheckbox>,\n  'onChange'\n> & {\n  onCheckedChange?: (checked: CheckedType) => void\n  onChange?: (e: ChangeEvent<HTMLInputElement>) => void\n  name?: string\n  id?: string\n  labelSize?: BodyProps['size']\n}\n\ntype CheckboxLabelProps = Omit<\n  React.ComponentProps<typeof Typography.Body>,\n  'children' | 'component'\n> & {\n  children: React.ReactNode\n}\n\nconst CheckboxLabel = ({ children, ...rest }: CheckboxLabelProps) => {\n  if (typeof children !== 'string') {\n    return <>{children}</>\n  }\n  return (\n    <Typography.Body {...rest} component=\"span\">\n      {children}\n    </Typography.Body>\n  )\n}\n\nexport const Checkbox = ({\n  onChange,\n  onCheckedChange,\n  id,\n  label,\n  labelSize = 'S',\n  ...rest\n}: CheckboxProps) => {\n  /**\n   * Handles the change event for a checkbox input and notifies the relevant handlers.\n   *\n   * This is added to provide compatibility with the `onChange` handler expected by the Formik library.\n   * Constructs a synthetic event object designed to mimic a React checkbox change event.\n   * Updates the `checked` status and passes the constructed event to the `onChange` handler\n   * if provided. Additionally, invokes the `onCheckedChange` handler with the new `checked` state\n   * if it is defined.\n   *\n   * @param {CheckedType} checked - The new checked state of the checkbox. It is expected to\n   *        be a boolean-like value where `true` indicates checked and any other value\n   *        indicates unchecked.\n   */\n  const handleCheckedChange = (checked: CheckedType) => {\n    const syntheticEvent = {\n      target: {\n        checked: checked === true,\n        type: 'checkbox',\n        name: rest.name,\n        id,\n      },\n    } as React.ChangeEvent<HTMLInputElement>\n    onChange?.(syntheticEvent)\n    onCheckedChange?.(checked)\n  }\n\n  return (\n    <RedisUiCheckbox\n      {...rest}\n      id={id}\n      onCheckedChange={handleCheckedChange}\n      label={<CheckboxLabel size={labelSize}>{label}</CheckboxLabel>}\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/combo-box/AutoTag.spec.tsx",
    "content": "import { getTagFromValue } from 'uiSrc/components/base/forms/combo-box/AutoTag'\n\nconst defaultDelimiter = ' '\ndescribe('AutoTag', () => {\n  describe('getTagFromValue', () => {\n    it('should return null on empty string', () => {\n      const result = getTagFromValue('', defaultDelimiter)\n      expect(result).toBeNull()\n    })\n    it.each([\n      ['', defaultDelimiter],\n      ['a', defaultDelimiter],\n      [' ', defaultDelimiter],\n      ['abcd', defaultDelimiter],\n    ])(\n      'should return null on single character string where delimiter is not present: `%s`, `%s`',\n      (value, delimiter) => {\n        const result = getTagFromValue(value, delimiter)\n        expect(result).toBeNull()\n      },\n    )\n    it.each([\n      ['a,', ',', 'a'],\n      [' ,', ',', ' '],\n      ['abcd ', defaultDelimiter, 'abcd'],\n      ['abcd dcba', defaultDelimiter, 'abcd'],\n    ])(\n      'should return correct value on value + delimiter string + whatever: `%s`, `%s` -> `%s`',\n      (value, delimiter, expected) => {\n        const result = getTagFromValue(value, delimiter)\n        expect(result).toEqual(expected)\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/combo-box/AutoTag.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { Chip, FormField, Input } from '@redis-ui/components'\nimport cn from 'classnames'\nimport styled from 'styled-components'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { CommonProps, Theme } from 'uiSrc/components/base/theme/types'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { truncateText } from 'uiSrc/utils'\n\nconst StyledWrapper = styled(Row)`\n  position: relative;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral600};\n  border-radius: 0.4rem;\n  padding: ${({ theme }: { theme: Theme }) =>\n    `${theme.core.space.space000} ${theme.core.space.space050}`};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nconst StyledInput = styled(Input)`\n  flex: 1;\n  min-width: 27px;\n`\n\nexport type AutoTagOption<T = string | number | string[] | undefined> = {\n  label: string\n  key?: string\n  value?: T\n}\nexport type AutoTagProps = Omit<\n  React.ComponentProps<typeof FormField>,\n  'children' | 'onChange'\n> &\n  CommonProps & {\n    isClearable?: boolean\n    placeholder?: string\n    delimiter?: string\n    selectedOptions?: AutoTagOption[]\n    onCreateOption?: (value: string, options?: AutoTagOption[]) => void\n    onChange?: (value: AutoTagOption[]) => void\n    onInputChange?: (value: string) => void\n    size?: 'S' | 'M'\n    full?: boolean\n  }\n\nexport function getTagFromValue(value: string, delimiter: string) {\n  const delimiterFirstIndex = value.indexOf(delimiter)\n  if (delimiterFirstIndex > -1) {\n    const firstValue = value.slice(0, delimiterFirstIndex)\n    if (firstValue !== '') {\n      return firstValue\n    }\n  }\n  return null\n}\n\nexport function filterOptions(\n  selection: AutoTagOption[],\n  value?: AutoTagOption['value'],\n  label?: AutoTagOption['label'],\n) {\n  // remove option from selection\n  return selection.filter((option) => {\n    if (value) return option.value !== value\n    if (label) return option.label !== label\n    return false\n  })\n}\n\nconst ClearButton = ({\n  onClick,\n  shouldRender,\n}: {\n  onClick: () => void\n  shouldRender: boolean\n}) => {\n  if (!shouldRender) {\n    return null\n  }\n  return (\n    <IconButton\n      data-test-subj=\"autoTagClearButton\"\n      title=\"Clear\"\n      style={{\n        position: 'absolute',\n        right: '4px',\n        top: 'calc(50% - 10px)',\n      }}\n      icon={CancelSlimIcon}\n      onClick={onClick}\n    />\n  )\n}\n\nexport const AutoTag = ({\n  className,\n  isClearable = false,\n  placeholder,\n  selectedOptions,\n  onCreateOption,\n  delimiter = '',\n  onChange,\n  onInputChange,\n  style,\n  size = 'S',\n  full = false,\n  ...rest\n}: AutoTagProps) => {\n  const [selection, setSelection] = useState<AutoTagOption[]>([])\n  useEffect(() => {\n    if (selectedOptions) {\n      setSelection(selectedOptions)\n    }\n  }, [selectedOptions])\n  const [tag, setTag] = useState('')\n  const createOption = (value: string) => {\n    // create a new option\n    const newOption = {\n      label: value,\n      value,\n    }\n    // add the new option to options\n    setTag('')\n    onInputChange?.('')\n    const newSelection = [...selection, newOption]\n    setSelection(newSelection)\n    // add the new option to selection\n    onCreateOption?.(value, newSelection)\n  }\n  const handleInputChange = (value: string) => {\n    const tag = getTagFromValue(value, delimiter)\n    if (tag !== null) {\n      createOption(tag)\n      return\n    }\n    setTag(value)\n    onInputChange?.(value)\n  }\n  const handleEnter: React.KeyboardEventHandler<HTMLInputElement> = (e) => {\n    // todo: replace when keys constants are in scope\n    if (e.key === 'Enter') {\n      const tag = (e.target as HTMLInputElement).value.trim()\n      if (tag === null || tag.length === 0) {\n        return\n      }\n      createOption(tag)\n    }\n  }\n\n  function getPlaceholder() {\n    return selectedOptions?.length && selectedOptions.length > 0\n      ? undefined\n      : placeholder\n  }\n\n  return (\n    <FormField\n      {...rest}\n      className={cn('RI-combo-box', className)}\n      style={{\n        ...style,\n        columnGap: '0.5rem',\n        width: full ? '100%' : undefined,\n      }}\n    >\n      <StyledWrapper\n        justify=\"start\"\n        wrap\n        gap=\"s\"\n        className=\"RI-auto-tag__container\"\n        full\n      >\n        <Row\n          gap=\"s\"\n          className=\"RI-auto-tag__selection\"\n          wrap\n          justify=\"start\"\n          grow\n          align=\"center\"\n          data-test-subj=\"autoTagWrapper\"\n        >\n          {selection?.map(({ value, label }, idx) => {\n            const key = `${label}-${value}-${idx}`\n            const text = String(label || value || '')\n            return (\n              <Chip\n                data-test-subj=\"autoTagChip\"\n                size={size}\n                key={key}\n                text={truncateText(text, 20)}\n                title={text}\n                onClose={() => {\n                  // remove option from selection\n                  const newSelection = filterOptions(selection, value, label)\n                  setSelection(newSelection)\n                  // call onChange\n                  onChange?.(newSelection)\n                }}\n              />\n            )\n          })}\n          <StyledInput\n            variant=\"underline\"\n            placeholder={getPlaceholder()}\n            onChange={handleInputChange}\n            onKeyDown={handleEnter}\n            value={tag}\n            data-test-subj=\"autoTagInput\"\n          />\n          <ClearButton\n            onClick={() => {\n              setTag('')\n              setSelection([])\n              // call onChange\n              onChange?.([])\n            }}\n            shouldRender={\n              isClearable && (tag.length > 0 || selection.length > 0)\n            }\n          />\n        </Row>\n      </StyledWrapper>\n    </FormField>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/fieldset/FormFieldset.spec.tsx",
    "content": "/* eslint-disable jsx-a11y/label-has-associated-control */\nimport React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { FormFieldset, FormFieldsetProps } from './FormFieldset'\n\nconst defaultProps: FormFieldsetProps = {\n  children: <div data-testid=\"fieldset-content\">Test content</div>,\n}\n\ndescribe('FormFieldset', () => {\n  it('should render', () => {\n    expect(render(<FormFieldset {...defaultProps} />)).toBeTruthy()\n  })\n\n  it('should render children', () => {\n    render(<FormFieldset {...defaultProps} />)\n\n    expect(screen.getByTestId('fieldset-content')).toBeInTheDocument()\n    expect(screen.getByText('Test content')).toBeInTheDocument()\n  })\n\n  it('should render as fieldset element', () => {\n    render(<FormFieldset {...defaultProps} />)\n\n    const fieldset = screen.getByRole('group')\n    expect(fieldset.tagName).toBe('FIELDSET')\n  })\n\n  it('should render without legend when legend prop is not provided', () => {\n    render(<FormFieldset {...defaultProps} />)\n\n    expect(screen.queryByRole('legend')).not.toBeInTheDocument()\n  })\n\n  it('should render legend when legend prop is provided', () => {\n    render(\n      <FormFieldset {...defaultProps} legend={{ children: 'Test Legend' }} />,\n    )\n\n    expect(screen.getByText('Test Legend')).toBeInTheDocument()\n  })\n\n  it('should render legend with custom content', () => {\n    const legendContent = (\n      <span data-testid=\"custom-legend\">Custom Legend Content</span>\n    )\n\n    render(\n      <FormFieldset {...defaultProps} legend={{ children: legendContent }} />,\n    )\n\n    expect(screen.getByTestId('custom-legend')).toBeInTheDocument()\n    expect(screen.getByText('Custom Legend Content')).toBeInTheDocument()\n  })\n\n  it('should not render legend when display is hidden', () => {\n    render(\n      <FormFieldset\n        {...defaultProps}\n        legend={{\n          children: 'Hidden Legend',\n          display: 'hidden',\n        }}\n      />,\n    )\n\n    expect(screen.queryByText('Hidden Legend')).not.toBeInTheDocument()\n  })\n\n  it('should render legend when display is visible', () => {\n    render(\n      <FormFieldset\n        {...defaultProps}\n        legend={{\n          children: 'Visible Legend',\n          display: 'visible',\n        }}\n      />,\n    )\n\n    expect(screen.getByText('Visible Legend')).toBeInTheDocument()\n  })\n\n  it('should render legend when display is not specified (defaults to visible)', () => {\n    render(\n      <FormFieldset\n        {...defaultProps}\n        legend={{ children: 'Default Legend' }}\n      />,\n    )\n\n    expect(screen.getByText('Default Legend')).toBeInTheDocument()\n  })\n\n  it('should pass through HTML attributes to fieldset element', () => {\n    render(\n      <FormFieldset\n        {...defaultProps}\n        data-testid=\"custom-fieldset\"\n        className=\"custom-class\"\n        id=\"custom-id\"\n      />,\n    )\n\n    const fieldset = screen.getByTestId('custom-fieldset')\n    expect(fieldset).toHaveClass('custom-class')\n    expect(fieldset).toHaveAttribute('id', 'custom-id')\n  })\n\n  it('should pass through HTML attributes to legend element', () => {\n    render(\n      <FormFieldset\n        {...defaultProps}\n        legend={{\n          children: 'Legend with attributes',\n          // @ts-ignore\n          'data-testid': 'custom-legend',\n          className: 'legend-class',\n          id: 'legend-id',\n        }}\n      />,\n    )\n\n    const legend = screen.getByTestId('custom-legend')\n    expect(legend).toHaveClass('legend-class')\n    expect(legend).toHaveAttribute('id', 'legend-id')\n  })\n\n  it('should handle multiple children', () => {\n    render(\n      <FormFieldset>\n        <div data-testid=\"child-1\">Child 1</div>\n        <div data-testid=\"child-2\">Child 2</div>\n        <input data-testid=\"input-field\" type=\"text\" />\n      </FormFieldset>,\n    )\n\n    expect(screen.getByTestId('child-1')).toBeInTheDocument()\n    expect(screen.getByTestId('child-2')).toBeInTheDocument()\n    expect(screen.getByTestId('input-field')).toBeInTheDocument()\n  })\n\n  it('should handle form elements as children', () => {\n    render(\n      <FormFieldset legend={{ children: 'Form Fields' }}>\n        <label htmlFor=\"name\">Name:</label>\n        <input id=\"name\" type=\"text\" data-testid=\"name-input\" />\n        <label htmlFor=\"email\">Email:</label>\n        <input id=\"email\" type=\"email\" data-testid=\"email-input\" />\n      </FormFieldset>,\n    )\n\n    expect(screen.getByText('Form Fields')).toBeInTheDocument()\n    expect(screen.getByLabelText('Name:')).toBeInTheDocument()\n    expect(screen.getByLabelText('Email:')).toBeInTheDocument()\n    expect(screen.getByTestId('name-input')).toBeInTheDocument()\n    expect(screen.getByTestId('email-input')).toBeInTheDocument()\n  })\n\n  it('should handle empty children', () => {\n    render(<FormFieldset />)\n\n    const fieldset = screen.getByRole('group')\n    expect(fieldset).toBeInTheDocument()\n    expect(fieldset).toBeEmptyDOMElement()\n  })\n\n  it('should handle null children', () => {\n    render(<FormFieldset>{null}</FormFieldset>)\n\n    const fieldset = screen.getByRole('group')\n    expect(fieldset).toBeInTheDocument()\n  })\n\n  it('should handle undefined children', () => {\n    render(<FormFieldset>{undefined}</FormFieldset>)\n\n    const fieldset = screen.getByRole('group')\n    expect(fieldset).toBeInTheDocument()\n  })\n\n  it('should handle complex legend with multiple elements', () => {\n    const complexLegend = (\n      <div>\n        <strong>Important:</strong>\n        <span> Please fill all required fields</span>\n      </div>\n    )\n\n    render(\n      <FormFieldset {...defaultProps} legend={{ children: complexLegend }} />,\n    )\n\n    expect(screen.getByText('Important:')).toBeInTheDocument()\n    expect(\n      screen.getByText('Please fill all required fields'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/fieldset/FormFieldset.styles.ts",
    "content": "/* eslint-disable sonarjs/no-nested-template-literals */\nimport { HTMLAttributes } from 'react'\nimport styled, { css } from 'styled-components'\nimport { Theme } from '@redis-ui/styles'\n\nexport type StyledFieldsetProps = HTMLAttributes<HTMLFieldSetElement>\n\nexport const StyledFieldset = styled.fieldset`\n  border: none;\n  margin: 0;\n  padding: 0;\n  min-width: 0;\n`\n\nexport interface StyledLegendProps extends HTMLAttributes<HTMLLegendElement> {\n  display?: 'visible' | 'hidden'\n}\n\nexport const StyledLegend = styled.legend<StyledLegendProps>`\n  ${({ theme }: { theme: Theme } & StyledLegendProps) => css`\n    margin-bottom: ${theme.core.space.space100};\n  `}\n  padding: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/fieldset/FormFieldset.tsx",
    "content": "import React from 'react'\n\nimport {\n  StyledFieldset,\n  StyledFieldsetProps,\n  StyledLegend,\n  StyledLegendProps,\n} from './FormFieldset.styles'\n\nexport interface FormFieldsetProps extends StyledFieldsetProps {\n  legend?: StyledLegendProps\n}\n\nexport const FormFieldset = ({\n  legend,\n  children,\n  ...props\n}: FormFieldsetProps) => (\n  <StyledFieldset {...props}>\n    {legend && legend.display !== 'hidden' && <StyledLegend {...legend} />}\n    {children}\n  </StyledFieldset>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/fieldset/index.ts",
    "content": "export * from './FormFieldset'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/file-picker/RiFilePicker.tsx",
    "content": "import React, { InputHTMLAttributes, ReactNode, useRef, useState } from 'react'\nimport cx from 'classnames'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\nimport { Loader } from 'uiSrc/components/base/display'\nimport {\n  EmptyButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport {\n  FilePickerInput,\n  FilePickerPrompt,\n  FilePickerPromptText,\n  FilePickerWrapper,\n} from 'uiSrc/components/base/forms/file-picker/styles'\nimport { CommonProps } from 'uiSrc/components/base/theme/types'\nimport ProgressBarLoader from 'uiSrc/components/base/display/progress-bar/ProgressBarLoader'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nexport type RiFilePickerProps = CommonProps &\n  Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {\n    id?: string\n    name?: string\n    className?: string\n    /**\n     * The content that appears in the dropzone if no file is attached\n     * @default 'Select or drag and drop a file'\n     */\n    initialPromptText?: ReactNode\n    /**\n     * Use as a callback to access the HTML FileList API\n     */\n    onChange?: (files: FileList | null) => void\n    /**\n     * Size or type of display;\n     * `default` for normal height, similar to other controls;\n     * `large` for taller size\n     * @default large\n     */\n    display?: 'default' | 'large'\n    isInvalid?: boolean\n    isLoading?: boolean\n    disabled?: boolean\n  }\n\nexport const RiFilePicker = ({\n  initialPromptText = <span>Select or drag and drop a file</span>,\n  onChange,\n  disabled,\n  id,\n  name,\n  className,\n  isInvalid,\n  isLoading,\n  display,\n  ...props\n}: RiFilePickerProps) => {\n  const [promptText, setPromptText] = useState<string | null>(null)\n\n  const [isHoveringDrop, setIsHoveringDrop] = useState(false)\n  const fileInput = useRef<HTMLInputElement | null>(null)\n  const generatedId: string = useGenerateId()\n  const handleChange = () => {\n    if (!fileInput.current) return\n\n    if (fileInput.current.files && fileInput.current.files.length > 1) {\n      setPromptText(`${fileInput.current.files.length} files selected`)\n    } else if (\n      fileInput.current.files &&\n      fileInput.current.files.length === 0\n    ) {\n      setPromptText(null)\n    } else {\n      setPromptText(fileInput.current.value.split('\\\\').pop()!)\n    }\n\n    onChange?.(fileInput.current.files)\n  }\n  const removeFiles = (e?: React.MouseEvent<HTMLButtonElement>) => {\n    if (e) {\n      e.stopPropagation()\n      e.preventDefault()\n    }\n\n    if (!fileInput.current) return\n\n    fileInput.current.value = ''\n    handleChange()\n  }\n\n  const showDrop = () => {\n    if (!disabled) {\n      setIsHoveringDrop(true)\n    }\n  }\n\n  const hideDrop = () => {\n    setIsHoveringDrop(false)\n  }\n\n  const promptId = `${id || generatedId}-filePicker__prompt`\n\n  const isOverridingInitialPrompt = promptText != null\n\n  const normalFormControl = display === 'default'\n\n  const classes = cx(\n    'RI-File-Picker',\n    {\n      'RI-File-Picker-isDroppingFile': isHoveringDrop,\n      'RI-File-Picker-isInvalid': isInvalid,\n      'RI-File-Picker-isLoading': isLoading,\n      'RI-File-Picker-hasFiles': isOverridingInitialPrompt,\n    },\n    className,\n  )\n  const compressed = display === 'default'\n\n  let clearButton: ReactNode\n  if (isLoading && normalFormControl) {\n    // Override clear button with loading spinner if it is in loading state\n    clearButton = (\n      <Loader\n        className=\"RI-File-Picker__loadingSpinner\"\n        size={compressed ? 's' : 'm'}\n      />\n    )\n  } else if (isOverridingInitialPrompt && !disabled) {\n    if (normalFormControl) {\n      clearButton = (\n        <SecondaryButton\n          aria-label=\"Remove selected files\"\n          className=\"RI-File-Picker__clearButton\"\n          onClick={removeFiles}\n          size={compressed ? 's' : 'm'}\n        />\n      )\n    } else {\n      clearButton = (\n        <EmptyButton\n          aria-label=\"Remove selected files\"\n          className=\"RI-File-Picker__clearButton\"\n          size=\"small\"\n          onClick={removeFiles}\n        >\n          <ColorText color=\"default\">Remove</ColorText>\n        </EmptyButton>\n      )\n    }\n  } else {\n    clearButton = null\n  }\n\n  const loader = !normalFormControl && isLoading && (\n    <ProgressBarLoader color=\"accent\" />\n  )\n  return (\n    <FilePickerWrapper className={classes} $large={display === 'large'}>\n      <FilePickerInput\n        type=\"file\"\n        id={id}\n        name={name}\n        className=\"RI-File-Picker__input\"\n        onChange={handleChange}\n        ref={fileInput}\n        onDragOver={showDrop}\n        onDragLeave={hideDrop}\n        onDrop={hideDrop}\n        disabled={disabled}\n        aria-describedby={promptId}\n        {...props}\n      />\n      <FilePickerPrompt\n        className=\"RI-File-Picker__prompt\"\n        id={promptId}\n        $large={display === 'large'}\n      >\n        <RiIcon\n          className=\"RI-File-Picker__icon\"\n          color={\n            isInvalid ? 'danger500' : disabled ? 'neutral500' : 'primary500'\n          }\n          type={isInvalid ? 'ToastDangerIcon' : 'UploadIcon'}\n          size={normalFormControl ? 'L' : 'XL'}\n          aria-hidden=\"true\"\n        />\n        <FilePickerPromptText size=\"s\" className=\"RI-File-Picker__promptText\">\n          {promptText || initialPromptText}\n        </FilePickerPromptText>\n        {clearButton}\n        {loader}\n      </FilePickerPrompt>\n    </FilePickerWrapper>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/file-picker/styles.tsx",
    "content": "/* eslint-disable sonarjs/no-nested-template-literals */\nimport styled, { css } from 'styled-components'\nimport React, { forwardRef, InputHTMLAttributes } from 'react'\nimport { Text } from 'uiSrc/components/base/text'\n\ntype FilePickerWrapperProps = InputHTMLAttributes<HTMLDivElement> & {\n  $large?: boolean\n}\nexport const FilePickerPromptText = styled(Text)``\n\nconst largeWrapper = css`\n  border-radius: 0;\n  overflow: hidden;\n  height: auto;\n`\nconst defaultWrapper = css`\n  height: 40px;\n`\nexport const FilePickerWrapper = styled.div<FilePickerWrapperProps>`\n  width: 100%;\n  position: relative;\n  ${({ $large }) => ($large ? largeWrapper : defaultWrapper)}\n  &:hover {\n    ${FilePickerPromptText} {\n      text-decoration: underline;\n      font-weight: 600;\n    }\n    svg {\n      scale: 1.2;\n    }\n  }\n`\n\n// Create a base component that forwards refs\nconst FilePickerInputBase = forwardRef<\n  HTMLInputElement,\n  InputHTMLAttributes<HTMLInputElement>\n>((props, ref) => <input ref={ref} {...props} />)\n\n// Style the forwarded ref component\nexport const FilePickerInput = styled(FilePickerInputBase)`\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  opacity: 0;\n  overflow: hidden;\n  &:hover {\n    cursor: pointer;\n  }\n`\nconst promptLarge = css`\n  min-height: ${({ theme }) => theme.core.space.space800};\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: ${({ theme }) => theme.core.space.space150};\n`\nconst promptDefault = css`\n  height: 140px;\n`\n\nconst promptPadding = css<FilePickerWrapperProps>`\n  padding: ${({ theme, $large }) => {\n    const { space100, space400, space250 } = theme.core.space\n    return $large\n      ? `0 ${space250}`\n      : `${space100} ${space100} ${space100} ${space400}`\n  }};\n`\nexport const FilePickerPrompt = styled.div<FilePickerWrapperProps>`\n  pointer-events: none;\n  border-radius: ${({ theme }) => theme.core.space.space050};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  ${promptPadding}\n\n  ${({ $large }) => ($large ? promptLarge : promptDefault)}\n  /* Ensure inner buttons are clickable above the FilePickerInput */\n  button {\n    pointer-events: auto;\n    z-index: 1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/radio-group/RadioGroup.tsx",
    "content": "import { RadioGroup } from '@redis-ui/components'\n\nexport { RadioGroup as RiRadioGroup } from '@redis-ui/components'\n\nexport const RiRadioGroupRoot = RadioGroup.Compose\nexport const RiRadioGroupItemRoot = RadioGroup.Item.Compose\nexport const RiRadioGroupItemLabel = RadioGroup.Item.Label\nexport const RiRadioGroupItemIndicator = RadioGroup.Item.Indicator\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/select/RISelectWithActions.spec.tsx",
    "content": "import React from 'react'\nimport { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { SelectOption } from '@redis-ui/components'\nimport { RISelectWithActions, SelectOptionActions } from './RISelectWithActions'\n\n// Factory for creating select options\nconst selectOptionFactory = Factory.define<SelectOption & SelectOptionActions>(\n  () => ({\n    value: faker.string.uuid(),\n    label: faker.word.words(2),\n  }),\n)\n\ndescribe('RISelectWithActions', () => {\n  describe('Actions rendering - 0 to 3 actions support', () => {\n    it('should render option with no actions (0 actions)', () => {\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'No actions',\n        }),\n      ]\n\n      const { container } = render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(container).toBeInTheDocument()\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n\n    it('should render option with single action (1 action)', () => {\n      const actionClick = jest.fn()\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Single action',\n          actions: (\n            <button data-testid=\"delete-action\" onClick={actionClick}>\n              Delete\n            </button>\n          ),\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n\n    it('should render option with two actions (2 actions)', () => {\n      const editClick = jest.fn()\n      const deleteClick = jest.fn()\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Two actions',\n          actions: (\n            <>\n              <button data-testid=\"edit-action\" onClick={editClick}>\n                Edit\n              </button>\n              <button data-testid=\"delete-action\" onClick={deleteClick}>\n                Delete\n              </button>\n            </>\n          ),\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n\n    it('should render option with three actions (3 actions)', () => {\n      const viewClick = jest.fn()\n      const editClick = jest.fn()\n      const deleteClick = jest.fn()\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Three actions',\n          actions: (\n            <>\n              <button data-testid=\"view-action\" onClick={viewClick}>\n                View\n              </button>\n              <button data-testid=\"edit-action\" onClick={editClick}>\n                Edit\n              </button>\n              <button data-testid=\"delete-action\" onClick={deleteClick}>\n                Delete\n              </button>\n            </>\n          ),\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n  })\n\n  describe('CustomOptionWithAction component behavior', () => {\n    it('should wrap actions in div with event stopPropagation handlers', () => {\n      const actionClick = jest.fn()\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Option 1',\n          actions: (\n            <button data-testid=\"test-action\" onClick={actionClick}>\n              Action\n            </button>\n          ),\n        }),\n      ]\n\n      // The CustomOptionWithAction component wraps actions in a div\n      // with onPointerDown, onPointerUp, and onClick stopPropagation handlers\n      // This ensures clicking actions won't close the select dropdown\n      const { container } = render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(container).toBeInTheDocument()\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n\n    it('should use custom optionComponent prop for rendering options', () => {\n      const options = selectOptionFactory.buildList(2, {\n        actions: <button>Action</button>,\n      })\n\n      // The component passes CustomOptionWithAction as optionComponent\n      // to Select.Content.OptionList\n      const { container } = render(\n        <RISelectWithActions\n          options={options}\n          value={options[0].value}\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(container.querySelector('[role=\"combobox\"]')).toBeInTheDocument()\n    })\n  })\n\n  describe('Actions with Popover support', () => {\n    it('should render action wrapped in popover component', () => {\n      const deleteClick = jest.fn()\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Option with popover',\n          actions: (\n            <div data-testid=\"popover-wrapper\">\n              <button data-testid=\"trigger-btn\">Delete</button>\n              <div data-testid=\"popover-content\">\n                <p>Are you sure?</p>\n                <button onClick={deleteClick}>Confirm</button>\n              </div>\n            </div>\n          ),\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n\n    it('should support complex popover structures in actions', () => {\n      const TestPopoverAction = () => {\n        return (\n          <div data-testid=\"complex-popover\">\n            <button data-testid=\"popover-trigger\">More Options</button>\n            <div data-testid=\"popover-menu\">\n              <button>Edit</button>\n              <button>Duplicate</button>\n              <button>Delete</button>\n            </div>\n          </div>\n        )\n      }\n\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Option with complex popover',\n          actions: <TestPopoverAction />,\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n  })\n\n  describe('Mixed options with different action configurations', () => {\n    it('should render multiple options with 0, 1, 2, and 3 actions', () => {\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'No actions',\n        }),\n        selectOptionFactory.build({\n          value: '2',\n          label: 'One action',\n          actions: <button>Action 1</button>,\n        }),\n        selectOptionFactory.build({\n          value: '3',\n          label: 'Two actions',\n          actions: (\n            <>\n              <button>Action 2</button>\n              <button>Action 3</button>\n            </>\n          ),\n        }),\n        selectOptionFactory.build({\n          value: '4',\n          label: 'Three actions',\n          actions: (\n            <>\n              <button>Action 4</button>\n              <button>Action 5</button>\n              <button>Action 6</button>\n            </>\n          ),\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n      expect(screen.getByText('No actions')).toBeInTheDocument()\n    })\n\n    it('should support actions with different types (buttons, icons, popovers)', () => {\n      const options = [\n        selectOptionFactory.build({\n          value: '1',\n          label: 'Button action',\n          actions: <button>Delete</button>,\n        }),\n        selectOptionFactory.build({\n          value: '2',\n          label: 'Icon action',\n          actions: <span data-testid=\"icon-action\">🗑️</span>,\n        }),\n        selectOptionFactory.build({\n          value: '3',\n          label: 'Popover action',\n          actions: (\n            <div data-testid=\"popover\">\n              <button>...</button>\n            </div>\n          ),\n        }),\n      ]\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value=\"1\"\n          onChange={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n  })\n\n  describe('Component structure and props', () => {\n    it('should use Select.Compose as root component', () => {\n      const options = selectOptionFactory.buildList(2)\n      const { container } = render(\n        <RISelectWithActions\n          options={options}\n          value={options[0].value}\n          onChange={jest.fn()}\n        />,\n      )\n\n      // Verify the select structure is rendered\n      expect(container.querySelector('[role=\"combobox\"]')).toBeInTheDocument()\n    })\n\n    it('should pass all props to Select.Compose', () => {\n      const onChange = jest.fn()\n      const options = selectOptionFactory.buildList(2)\n\n      render(\n        <RISelectWithActions\n          options={options}\n          value={options[0].value}\n          onChange={onChange}\n        />,\n      )\n\n      expect(screen.getByRole('combobox')).toBeInTheDocument()\n    })\n\n    it('should render Select.Trigger inside Select.Compose', () => {\n      const options = selectOptionFactory.buildList(2)\n      render(\n        <RISelectWithActions\n          options={options}\n          value={options[0].value}\n          onChange={jest.fn()}\n        />,\n      )\n\n      const trigger = screen.getByRole('combobox')\n      expect(trigger).toBeInTheDocument()\n      expect(trigger).toHaveAttribute('type', 'button')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/select/RISelectWithActions.tsx",
    "content": "import React from 'react'\nimport { Select, SelectOption, SelectProps } from '@redis-ui/components'\n\nexport type SelectOptionActions = {\n  actions?: JSX.Element\n}\n\nexport type Props = SelectProps & {\n  options: (SelectOption & SelectOptionActions)[]\n  'data-testid'?: string\n}\n\nconst CustomOptionWithAction = ({ option, content, ...restProps }: any) => {\n  return (\n    <Select.Option.Compose option={option} {...restProps}>\n      <Select.Option.Content>{content}</Select.Option.Content>\n      {option.actions && (\n        <div\n          onPointerDown={(e) => e.stopPropagation()}\n          onPointerUp={(e) => e.stopPropagation()}\n          onClick={(e) => e.stopPropagation()}\n        >\n          {option.actions}\n        </div>\n      )}\n      <Select.Option.Indicator />\n    </Select.Option.Compose>\n  )\n}\n\nexport const RISelectWithActions = (props: Props) => {\n  const { 'data-testid': dataTestId, ...restProps } = props\n\n  return (\n    <Select.Compose {...restProps}>\n      <Select.Trigger data-testid={dataTestId} />\n      <Select.Content.Compose>\n        <Select.Content.OptionList optionComponent={CustomOptionWithAction} />\n      </Select.Content.Compose>\n    </Select.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/forms/select/RiSelect.tsx",
    "content": "// Import the original type but don't re-export it\nimport type { SelectOption, SelectValueRender } from '@redis-ui/components'\nimport React from 'react'\n\nexport { Select as RiSelect } from '@redis-ui/components'\nexport type {\n  SelectOption,\n  SelectValueRender,\n  SelectValueRenderParams,\n} from '@redis-ui/components'\n\n// Define our extended type\nexport type RiSelectOption = SelectOption & {\n  'data-test-subj'?: string\n  dropdownDisplay?: string | JSX.Element | null\n  inputDisplay?: string | JSX.Element | null\n}\n\nexport const defaultValueRender: SelectValueRender<RiSelectOption> = ({\n  option,\n  isOptionValue,\n}) => {\n  if (!option.inputDisplay) {\n    return option.label ?? option.value\n  }\n\n  if (isOptionValue) {\n    // render dropdown list item\n    if (option.dropdownDisplay && typeof option.dropdownDisplay !== 'string') {\n      // allow for custom dropdown display element\n      return option.dropdownDisplay\n    }\n    return (\n      <span\n        data-test-subj={option['data-test-subj']}\n        data-testid={option['data-test-subj']}\n      >\n        {option.dropdownDisplay ?? option.inputDisplay}\n      </span>\n    )\n  }\n  // allow for custom input display element\n  if (typeof option.inputDisplay !== 'string') {\n    return option.inputDisplay\n  }\n  return (\n    <span\n      data-test-subj={`${option['data-test-subj']}-prompt`}\n      data-testid={`${option['data-test-subj']}-prompt`}\n    >\n      {option.inputDisplay}\n    </span>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/icons/Icon.tsx",
    "content": "import React from 'react'\nimport { useTheme } from '@redis-ui/styles'\nimport cx from 'classnames'\nimport { IconSizeType } from '@redis-ui/icons'\nimport { MonochromeIconProps } from 'uiSrc/components/base/icons'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\ntype BaseIconProps = Omit<MonochromeIconProps, 'color' | 'size'> & {\n  icon: React.ComponentType<any>\n  color?:\n    | keyof Theme['semantic']['color']['icon']\n    | 'currentColor'\n    | (string & {})\n  size?: IconSizeType | null\n  isSvg?: boolean\n  style?: React.CSSProperties\n}\n\nconst sizesMap = {\n  XS: 8,\n  S: 12,\n  M: 16,\n  L: 20,\n  XL: 24,\n}\n\n/**\n * Type guard function to check if a color is a valid icon color in the theme\n * @param theme The current theme object\n * @param color The color string to check\n * @returns A boolean indicating if the color is valid and a type predicate\n */\nfunction isValidIconColor(\n  theme: Theme,\n  color: string | number | symbol,\n): color is keyof typeof theme.semantic.color.icon {\n  return color in theme.semantic.color.icon\n}\n\nexport const Icon = ({\n  icon: IconComponent,\n  isSvg = false,\n  customSize,\n  customColor,\n  color = 'primary600',\n  size,\n  className,\n  style = {},\n  ...rest\n}: BaseIconProps) => {\n  let sizeValue: number | string | undefined\n  if (size && sizesMap[size]) {\n    sizeValue = sizesMap[size]\n  } else if (typeof size === 'undefined') {\n    sizeValue = 'L'\n  }\n  if (customSize) {\n    sizeValue = customSize\n  }\n  const theme = useTheme()\n  let colorValue = customColor\n  if (!colorValue && isValidIconColor(theme, color)) {\n    colorValue = theme.semantic.color.icon[color]\n  } else if (color === 'currentColor') {\n    colorValue = 'currentColor'\n  }\n\n  const svgProps = {\n    color: colorValue,\n    width: sizeValue,\n    height: sizeValue,\n    ...rest,\n  }\n\n  const props = isSvg\n    ? svgProps\n    : { color, customColor, size, customSize, ...rest }\n\n  return (\n    <IconComponent\n      {...props}\n      style={{ ...style, verticalAlign: 'middle' }}\n      className={cx(className, 'RI-Icon')}\n    />\n  )\n}\n\nexport type IconProps = Omit<BaseIconProps, 'icon'>\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/icons/RiIcon.tsx",
    "content": "import React, { ImgHTMLAttributes, SVGProps } from 'react'\nimport cx from 'classnames'\nimport { IconProps } from './Icon'\nimport * as Icons from './iconRegistry'\n\nexport type AllIconsType = keyof typeof Icons\n\nexport type IconComponentProps = Omit<IconProps, 'icon' | 'size'> &\n  Omit<SVGProps<SVGSVGElement>, 'color' | 'size'> & {\n    type: AllIconsType\n    size?:\n      | IconProps['size']\n      | 'm'\n      | 's'\n      | 'xs'\n      | 'l'\n      | 'xl'\n      | 'xxl'\n      | 'original'\n  }\n\nexport const RiIcon = ({ type, size, ...props }: IconComponentProps) => {\n  const IconType = Icons[type]\n  if (!IconType) {\n    console.warn(`Icon type \"${type}\" not found, rendering as image`)\n    // TODO - 17.06.25 - Replace with icon\n    //  There are a few cases where type is just imported image asset. In most cases, it seems\n    //  that the image is an svg in the plugins folder - http://localhost:5540/static/plugins/redisearch/./dist/table_view_icon_light.svg\n    //  we can either just scratch the plugins and move assets in to the main project, or look into dynamically loading as icons in runtime\n    return (\n      <img\n        {...(props as ImgHTMLAttributes<HTMLImageElement>)}\n        alt={props.title ? props.title : ''}\n        src={type}\n        className={cx(type, props.className)}\n        style={props.style}\n      />\n    )\n  }\n  let iconSize: IconProps['size']\n\n  switch (size?.toLowerCase()) {\n    case 'm':\n      iconSize = 'M'\n      break\n    case 's':\n      iconSize = 'S'\n      break\n    case 'xs':\n      iconSize = 'XS'\n      break\n    case 'xl':\n    case 'xxl':\n      iconSize = 'XL'\n      break\n    case 'original':\n      iconSize = null\n      break\n    case 'l':\n    default:\n      iconSize = 'L'\n  }\n  // @ts-ignore\n  return <IconType {...props} size={iconSize} />\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/icons/iconRegistry.tsx",
    "content": "import React, { useContext } from 'react'\n\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { Theme } from 'uiSrc/constants'\n\n// Import all custom SVG assets\nimport AlarmSvg from 'uiSrc/assets/img/alarm.svg?react'\nimport BanIconSvg from 'uiSrc/assets/img/monitor/ban.svg?react'\nimport BulkUploadSvg from 'uiSrc/assets/img/icons/bulk-upload.svg?react'\nimport ChampagneSvg from 'uiSrc/assets/img/icons/champagne.svg?react'\nimport CloudLinkSvg from 'uiSrc/assets/img/oauth/cloud_link.svg?react'\nimport CloudSvg from 'uiSrc/assets/img/oauth/cloud.svg?react'\nimport ConnectionSvg from 'uiSrc/assets/img/icons/connection.svg?react'\nimport CopilotSvg from 'uiSrc/assets/img/icons/copilot.svg?react'\nimport DefaultPluginDarkSvg from 'uiSrc/assets/img/workbench/default_view_dark.svg?react'\nimport DefaultPluginLightSvg from 'uiSrc/assets/img/workbench/default_view_light.svg?react'\nimport DislikeSvg from 'uiSrc/assets/img/icons/dislike.svg?react'\nimport ExtendSvg from 'uiSrc/assets/img/icons/extend.svg?react'\nimport GithubHelpCenterSVG from 'uiSrc/assets/img/github.svg?react'\nimport GroupModeSvg from 'uiSrc/assets/img/icons/group_mode.svg?react'\nimport KeyboardShortcutsSvg from 'uiSrc/assets/img/icons/keyboard-shortcuts.svg?react'\nimport LikeSvg from 'uiSrc/assets/img/icons/like.svg?react'\nimport MessageInfoSvg from 'uiSrc/assets/img/icons/help_illus.svg?react'\nimport MinusInCircleSvg from 'uiSrc/assets/img/icons/minus_in_circle.svg?react'\nimport NoRecommendationsDarkSvg from 'uiSrc/assets/img/icons/recommendations_dark.svg?react'\nimport NoRecommendationsLightSvg from 'uiSrc/assets/img/icons/recommendations_light.svg?react'\nimport NotSubscribedIconDarkSvg from 'uiSrc/assets/img/pub-sub/not-subscribed.svg?react'\nimport NotSubscribedIconLightSvg from 'uiSrc/assets/img/pub-sub/not-subscribed-lt.svg?react'\nimport PetardSvg from 'uiSrc/assets/img/icons/petard.svg?react'\nimport PlayFilledSvg from 'uiSrc/assets/img/icons/play-filled.svg?react'\nimport PlaySvg from 'uiSrc/assets/img/icons/play.svg?react'\nimport PlusInCircleSvg from 'uiSrc/assets/img/icons/plus_in_circle.svg?react'\nimport ProfilerSvg from 'uiSrc/assets/img/icons/profiler.svg?react'\nimport RawModeSvg from 'uiSrc/assets/img/icons/raw_mode.svg?react'\nimport RedisDbBlueSvg from 'uiSrc/assets/img/icons/redis_db_blue.svg?react'\nimport RedisLogoFullSvg from 'uiSrc/assets/img/logo.svg?react'\nimport RedisLogoSvg from 'uiSrc/assets/img/logo_small.svg?react'\nimport ResetSvg from 'uiSrc/assets/img/rdi/reset.svg?react'\nimport ShrinkSvg from 'uiSrc/assets/img/icons/shrink.svg?react'\nimport SilentModeSvg from 'uiSrc/assets/img/icons/silent_mode.svg?react'\nimport SnoozeSvg from 'uiSrc/assets/img/icons/snooze.svg?react'\nimport StarsSvg from 'uiSrc/assets/img/icons/stars.svg?react'\nimport StopIconSvg from 'uiSrc/assets/img/rdi/stopFilled.svg?react'\nimport SubscribedIconDarkSvg from 'uiSrc/assets/img/pub-sub/subscribed.svg?react'\nimport SubscribedIconLightSvg from 'uiSrc/assets/img/pub-sub/subscribed-lt.svg?react'\nimport SurveySvg from 'uiSrc/assets/img/survey_icon.svg?react'\nimport TextViewIconDarkSvg from 'uiSrc/assets/img/workbench/text_view_dark.svg?react'\nimport TextViewIconLightSvg from 'uiSrc/assets/img/workbench/text_view_light.svg?react'\nimport ThreeDotsSvg from 'uiSrc/assets/img/icons/three_dots.svg?react'\nimport TriggerIcon from 'uiSrc/assets/img/bulb.svg?react'\nimport UserInCircleSvg from 'uiSrc/assets/img/icons/user_in_circle.svg?react'\nimport UserSvg from 'uiSrc/assets/img/icons/user.svg?react'\nimport VersionSvg from 'uiSrc/assets/img/icons/version.svg?react'\nimport VisTagCloudSvg from 'uiSrc/assets/img/workbench/vis_tag_cloud.svg?react'\nimport BikeSvg from 'uiSrc/assets/img/icons/bike.svg?react'\nimport PopcornSvg from 'uiSrc/assets/img/icons/popcorn.svg?react'\n\n// Import guides icons\nimport ProbabilisticDataSvg from 'uiSrc/assets/img/guides/probabilistic-data.svg?react'\nimport JSONSvg from 'uiSrc/assets/img/guides/json.svg?react'\nimport VectorSimilaritySvg from 'uiSrc/assets/img/guides/vector-similarity.svg?react'\n\n// Import metrics icons\nimport KeyDarkSvg from 'uiSrc/assets/img/overview/key_dark.svg?react'\nimport KeyTipSvg from 'uiSrc/assets/img/overview/key_tip.svg?react'\nimport KeyLightSvg from 'uiSrc/assets/img/overview/key_light.svg?react'\nimport MemoryDarkSvg from 'uiSrc/assets/img/overview/memory_dark.svg?react'\nimport MemoryLightSvg from 'uiSrc/assets/img/overview/memory_light.svg?react'\nimport MemoryTipSvg from 'uiSrc/assets/img/overview/memory_tip.svg?react'\nimport MeasureLightSvg from 'uiSrc/assets/img/overview/measure_light.svg?react'\nimport MeasureDarkSvg from 'uiSrc/assets/img/overview/measure_dark.svg?react'\nimport MeasureTipSvg from 'uiSrc/assets/img/overview/measure_tip.svg?react'\nimport TimeLightSvg from 'uiSrc/assets/img/overview/time_light.svg?react'\nimport TimeDarkSvg from 'uiSrc/assets/img/overview/time_dark.svg?react'\nimport TimeTipSvg from 'uiSrc/assets/img/overview/time_tip.svg?react'\nimport UserDarkSvg from 'uiSrc/assets/img/overview/user_dark.svg?react'\nimport UserLightSvg from 'uiSrc/assets/img/overview/user_light.svg?react'\nimport UserTipSvg from 'uiSrc/assets/img/overview/user_tip.svg?react'\nimport InputTipSvg from 'uiSrc/assets/img/overview/input_tip.svg?react'\nimport InputLightSvg from 'uiSrc/assets/img/overview/input_light.svg?react'\nimport InputDarkSvg from 'uiSrc/assets/img/overview/input_dark.svg?react'\nimport KeyIconBaseSvg from 'uiSrc/assets/img/overview/key.svg?react'\nimport MemoryIconBaseSvg from 'uiSrc/assets/img/overview/memory.svg?react'\nimport MeasureIconBaseSvg from 'uiSrc/assets/img/overview/measure.svg?react'\nimport TimeIconBaseSvg from 'uiSrc/assets/img/overview/time.svg?react'\nimport UserIconBaseSvg from 'uiSrc/assets/img/overview/user.svg?react'\nimport InputIconBaseSvg from 'uiSrc/assets/img/overview/input.svg?react'\nimport OutputTipSvg from 'uiSrc/assets/img/overview/output_tip.svg?react'\nimport OutputLightSvg from 'uiSrc/assets/img/overview/output_light.svg?react'\nimport OutputDarkSvg from 'uiSrc/assets/img/overview/output_dark.svg?react'\nimport OutputIconBaseSvg from 'uiSrc/assets/img/overview/output.svg?react'\n\n// Import modules icons\nimport RediStackDarkLogoSvg from 'uiSrc/assets/img/modules/redistack/RedisStackLogoDark.svg?react'\nimport RediStackDarkMinSvg from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg?react'\nimport RediStackLightLogoSvg from 'uiSrc/assets/img/modules/redistack/RedisStackLogoLight.svg?react'\nimport RediStackLightMinLight from 'uiSrc/assets/img/modules/redistack/RediStackLight-min.svg?react'\nimport RedisAIDark from 'uiSrc/assets/img/modules/RedisAIDark.svg?react'\nimport RedisAILight from 'uiSrc/assets/img/modules/RedisAILight.svg?react'\nimport RedisBloomDark from 'uiSrc/assets/img/modules/RedisBloomDark.svg?react'\nimport RedisBloomLight from 'uiSrc/assets/img/modules/RedisBloomLight.svg?react'\nimport RedisGears2Dark from 'uiSrc/assets/img/modules/RedisGears2Dark.svg?react'\nimport RedisGears2Light from 'uiSrc/assets/img/modules/RedisGears2Light.svg?react'\nimport RedisGears2Svg from 'uiSrc/assets/img/modules/RedisGears2.svg?react'\nimport RedisGearsDark from 'uiSrc/assets/img/modules/RedisGearsDark.svg?react'\nimport RedisGearsLight from 'uiSrc/assets/img/modules/RedisGearsLight.svg?react'\nimport RedisGraphDark from 'uiSrc/assets/img/modules/RedisGraphDark.svg?react'\nimport RedisGraphLight from 'uiSrc/assets/img/modules/RedisGraphLight.svg?react'\nimport RedisJSONDark from 'uiSrc/assets/img/modules/RedisJSONDark.svg?react'\nimport RedisJSONLight from 'uiSrc/assets/img/modules/RedisJSONLight.svg?react'\nimport RedisSearchDark from 'uiSrc/assets/img/modules/RedisSearchDark.svg?react'\nimport RedisSearchLight from 'uiSrc/assets/img/modules/RedisSearchLight.svg?react'\nimport RedisTimeSeriesDark from 'uiSrc/assets/img/modules/RedisTimeSeriesDark.svg?react'\nimport RedisTimeSeriesLight from 'uiSrc/assets/img/modules/RedisTimeSeriesLight.svg?react'\nimport UnknownDark from 'uiSrc/assets/img/modules/UnknownDark.svg?react'\nimport UnknownLight from 'uiSrc/assets/img/modules/UnknownLight.svg?react'\nimport UnknownSvg from 'uiSrc/assets/img/modules/Unknown.svg?react'\nimport FormatterSvg from 'uiSrc/assets/img/icons/formatter.svg?react'\nimport VectorSetSvg from 'uiSrc/assets/img/icons/vector_sets.svg?react'\n\n// Import options icons\nimport ActiveActiveDark from 'uiSrc/assets/img/options/Active-ActiveDark.svg?react'\nimport ActiveActiveLight from 'uiSrc/assets/img/options/Active-ActiveLight.svg?react'\nimport RedisOnFlashDark from 'uiSrc/assets/img/options/RedisOnFlashDark.svg?react'\nimport RedisOnFlashLight from 'uiSrc/assets/img/options/RedisOnFlashLight.svg?react'\n\n// Import sidebar icons\nimport BrowserSvg from 'uiSrc/assets/img/sidebar/browser.svg?react'\nimport PipelineManagementActiveSvg from 'uiSrc/assets/img/sidebar/pipeline_active.svg?react'\nimport PipelineManagementSvg from 'uiSrc/assets/img/sidebar/pipeline.svg?react'\nimport PipelineStatisticsSvg from 'uiSrc/assets/img/sidebar/pipeline_statistics.svg?react'\nimport PubSubSvg from 'uiSrc/assets/img/sidebar/pubsub.svg?react'\nimport SlowLogSvg from 'uiSrc/assets/img/sidebar/slowlog.svg?react'\n\nimport ShieldSvg from 'uiSrc/assets/img/shield.svg?react'\nimport RedisSoftwareSvg from 'uiSrc/assets/img/redis-software.svg?react'\n\nimport { Icon, IconProps } from './Icon'\n\n// Helper function to create icon component\nconst createIconComponent =\n  (\n    SvgComponentLight: React.ComponentType<IconProps>,\n    SvgComponentDark: React.ComponentType<IconProps> = SvgComponentLight,\n  ) =>\n  (props: IconProps) => {\n    const { theme } = useContext(ThemeContext)\n    const icon = theme === Theme.Light ? SvgComponentLight : SvgComponentDark\n\n    return <Icon icon={icon} {...props} isSvg />\n  }\n\n// Re-export all library icons from @redis-ui/icons\nexport * from '@redis-ui/icons'\n\n// Export multicolor library icons\nexport {\n  LoaderLargeIcon,\n  AzureIcon,\n  Awss3Icon,\n  GooglecloudIcon,\n  GoogleSigninIcon,\n  SsoIcon,\n  GithubIcon,\n  RedisLogoDarkMinIcon,\n} from '@redis-ui/icons/multicolor'\n\n// Common icons\nexport const AlarmIcon = createIconComponent(AlarmSvg)\nexport const BannedIcon = createIconComponent(BanIconSvg)\nexport const BulkUploadIcon = createIconComponent(BulkUploadSvg)\nexport const ChampagneIcon = createIconComponent(ChampagneSvg)\nexport const CloudIcon = createIconComponent(CloudSvg)\nexport const CloudLinkIcon = createIconComponent(CloudLinkSvg)\nexport const ConnectionIcon = createIconComponent(ConnectionSvg)\nexport const CopilotIcon = createIconComponent(CopilotSvg)\nexport const DefaultPluginDarkIcon = createIconComponent(DefaultPluginDarkSvg)\nexport const DefaultPluginLightIcon = createIconComponent(DefaultPluginLightSvg)\nexport const DislikeIcon = createIconComponent(DislikeSvg)\nexport const ExtendIcon = createIconComponent(ExtendSvg)\nexport const GithubHelpCenterIcon = createIconComponent(GithubHelpCenterSVG)\nexport const GroupModeIcon = createIconComponent(GroupModeSvg)\nexport const KeyboardShortcutsIcon = createIconComponent(KeyboardShortcutsSvg)\nexport const LikeIcon = createIconComponent(LikeSvg)\nexport const MessageInfoIcon = createIconComponent(MessageInfoSvg)\nexport const MinusInCircleIcon = createIconComponent(MinusInCircleSvg)\nexport const NoRecommendationsDarkIcon = createIconComponent(\n  NoRecommendationsDarkSvg,\n)\nexport const NoRecommendationsLightIcon = createIconComponent(\n  NoRecommendationsLightSvg,\n)\nexport const NotSubscribedDarkIcon = createIconComponent(\n  NotSubscribedIconDarkSvg,\n)\nexport const NotSubscribedLightIcon = createIconComponent(\n  NotSubscribedIconLightSvg,\n)\nexport const PetardIcon = createIconComponent(PetardSvg)\nexport const PlayFilledIcon = createIconComponent(PlayFilledSvg)\nexport const PlayIcon = createIconComponent(PlaySvg)\nexport const PlusInCircleIcon = createIconComponent(PlusInCircleSvg)\nexport const ProfilerIcon = createIconComponent(ProfilerSvg)\nexport const RawModeIcon = createIconComponent(RawModeSvg)\nexport const RedisDbBlueIcon = createIconComponent(RedisDbBlueSvg)\nexport const RedisLogo = createIconComponent(RedisLogoSvg)\nexport const RedisLogoFullIcon = createIconComponent(RedisLogoFullSvg)\nexport const RiResetIcon = createIconComponent(ResetSvg)\nexport const RiStarsIcon = createIconComponent(StarsSvg)\nexport const RiStopIcon = createIconComponent(StopIconSvg)\nexport const RiUserIcon = createIconComponent(UserSvg)\nexport const ShrinkIcon = createIconComponent(ShrinkSvg)\nexport const SilentModeIcon = createIconComponent(SilentModeSvg)\nexport const SnoozeIcon = createIconComponent(SnoozeSvg)\nexport const SubscribedDarkIcon = createIconComponent(SubscribedIconDarkSvg)\nexport const SubscribedLightIcon = createIconComponent(SubscribedIconLightSvg)\nexport const SurveyIcon = createIconComponent(SurveySvg)\nexport const TextViewIconDarkIcon = createIconComponent(TextViewIconDarkSvg)\nexport const TextViewIconLightIcon = createIconComponent(TextViewIconLightSvg)\nexport const ThreeDotsIcon = createIconComponent(ThreeDotsSvg)\nexport const Trigger = createIconComponent(TriggerIcon)\nexport const UserInCircle = createIconComponent(UserInCircleSvg)\nexport const VersionIcon = createIconComponent(VersionSvg)\nexport const VisTagCloudIcon = createIconComponent(VisTagCloudSvg)\nexport const BikeIcon = createIconComponent(BikeSvg)\nexport const PopcornIcon = createIconComponent(PopcornSvg)\n\n// Guides icons\nexport const ProbabilisticDataIcon = createIconComponent(ProbabilisticDataSvg)\nexport const JSONIcon = createIconComponent(JSONSvg)\nexport const VectorSimilarityIcon = createIconComponent(VectorSimilaritySvg)\n\n// Metrics icons\nexport const KeyDarkIcon = createIconComponent(KeyDarkSvg)\nexport const KeyTipIcon = createIconComponent(KeyTipSvg)\nexport const KeyLightIcon = createIconComponent(KeyLightSvg)\nexport const MemoryDarkIcon = createIconComponent(MemoryDarkSvg)\nexport const MemoryLightIcon = createIconComponent(MemoryLightSvg)\nexport const MemoryTipIcon = createIconComponent(MemoryTipSvg)\nexport const MeasureLightIcon = createIconComponent(MeasureLightSvg)\nexport const MeasureDarkIcon = createIconComponent(MeasureDarkSvg)\nexport const MeasureTipIcon = createIconComponent(MeasureTipSvg)\nexport const TimeLightIcon = createIconComponent(TimeLightSvg)\nexport const TimeDarkIcon = createIconComponent(TimeDarkSvg)\nexport const TimeTipIcon = createIconComponent(TimeTipSvg)\nexport const UserDarkIcon = createIconComponent(UserDarkSvg)\nexport const UserLightIcon = createIconComponent(UserLightSvg)\nexport const UserTipIcon = createIconComponent(UserTipSvg)\nexport const InputTipIcon = createIconComponent(InputTipSvg)\nexport const InputLightIcon = createIconComponent(InputLightSvg)\nexport const InputDarkIcon = createIconComponent(InputDarkSvg)\nexport const KeyIconIcon = createIconComponent(KeyIconBaseSvg)\nexport const MemoryIconIcon = createIconComponent(MemoryIconBaseSvg)\nexport const MeasureIconIcon = createIconComponent(MeasureIconBaseSvg)\nexport const TimeIconIcon = createIconComponent(TimeIconBaseSvg)\nexport const UserIconIcon = createIconComponent(UserIconBaseSvg)\nexport const InputIconIcon = createIconComponent(InputIconBaseSvg)\nexport const OutputTipIcon = createIconComponent(OutputTipSvg)\nexport const OutputLightIcon = createIconComponent(OutputLightSvg)\nexport const OutputDarkIcon = createIconComponent(OutputDarkSvg)\nexport const OutputIconIcon = createIconComponent(OutputIconBaseSvg)\n\n// Modules icons\nexport const FormatterIcon = createIconComponent(FormatterSvg)\nexport const RedisAIDarkIcon = createIconComponent(RedisAIDark)\nexport const RedisAILightIcon = createIconComponent(RedisAILight)\nexport const RedisBloomDarkIcon = createIconComponent(RedisBloomDark)\nexport const RedisBloomLightIcon = createIconComponent(RedisBloomLight)\nexport const RedisGears2DarkIcon = createIconComponent(RedisGears2Dark)\nexport const RedisGears2LightIcon = createIconComponent(RedisGears2Light)\nexport const RedisGears2Icon = createIconComponent(RedisGears2Svg)\nexport const RedisGearsDarkIcon = createIconComponent(RedisGearsDark)\nexport const RedisGearsLightIcon = createIconComponent(RedisGearsLight)\nexport const RedisGraphDarkIcon = createIconComponent(RedisGraphDark)\nexport const RedisGraphLightIcon = createIconComponent(RedisGraphLight)\nexport const RedisJSONDarkIcon = createIconComponent(RedisJSONDark)\nexport const RedisJSONLightIcon = createIconComponent(RedisJSONLight)\nexport const RedisSearchDarkIcon = createIconComponent(RedisSearchDark)\nexport const RedisSearchLightIcon = createIconComponent(RedisSearchLight)\nexport const RediStackDarkLogoIcon = createIconComponent(RediStackDarkLogoSvg)\nexport const RediStackLightLogoIcon = createIconComponent(RediStackLightLogoSvg)\nexport const RediStackMinIcon = createIconComponent(\n  RediStackLightMinLight,\n  RediStackDarkMinSvg,\n)\nexport const RedisTimeSeriesDarkIcon = createIconComponent(RedisTimeSeriesDark)\nexport const RedisTimeSeriesLightIcon =\n  createIconComponent(RedisTimeSeriesLight)\nexport const UnknownDarkIcon = createIconComponent(UnknownDark)\nexport const UnknownLightIcon = createIconComponent(UnknownLight)\nexport const UnknownModuleIcon = createIconComponent(UnknownSvg)\nexport const VectorSetIcon = createIconComponent(VectorSetSvg)\n\n// Options icons\nexport const ActiveActiveDarkIcon = createIconComponent(ActiveActiveDark)\nexport const ActiveActiveLightIcon = createIconComponent(ActiveActiveLight)\nexport const RedisOnFlashDarkIcon = createIconComponent(RedisOnFlashDark)\nexport const RedisOnFlashLightIcon = createIconComponent(RedisOnFlashLight)\n\n// Sidebar icons\nexport const BrowserIcon = createIconComponent(BrowserSvg)\nexport const PipelineManagementActiveIcon = createIconComponent(\n  PipelineManagementActiveSvg,\n)\nexport const PipelineManagementIcon = createIconComponent(PipelineManagementSvg)\n\nexport const PipelineStatisticsIcon = createIconComponent(PipelineStatisticsSvg)\nexport const PubSubIcon = createIconComponent(PubSubSvg)\nexport const SlowLogIcon = createIconComponent(SlowLogSvg)\n\nexport const ShieldIcon = createIconComponent(ShieldSvg)\nexport const RedisSoftwareIcon = createIconComponent(RedisSoftwareSvg)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/icons/index.ts",
    "content": "// Core icon system exports\nexport * from './Icon'\n// New centralized icon system\nexport * from './RiIcon'\n// Export all individual icons from the registry\nexport * from './iconRegistry'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/index.ts",
    "content": "import { HorizontalRule, LoadingContent } from './layout'\n\nexport { HorizontalRule, LoadingContent }\n\nexport * from './code-editor'\nexport * from './tooltip'\nexport * from './popover'\n\nexport { RiFilePicker } from './forms/file-picker/RiFilePicker'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/ComposedInput.tsx",
    "content": "import React, { ComponentProps } from 'react'\n\nimport { Input } from '@redis-ui/components'\n\nexport type ComposedInputProps = ComponentProps<typeof Input> & {\n  before?: JSX.Element\n  after?: JSX.Element\n}\n\nexport default function ComposedInput(props: ComposedInputProps) {\n  const { before, after, placeholder, ...inputProps } = props\n  return (\n    <Input.Compose {...inputProps}>\n      {before}\n      <Input.Tag placeholder={placeholder} />\n      {after}\n    </Input.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/NumericInput.tsx",
    "content": "import { NumericInput } from '@redis-ui/components'\n\nexport default NumericInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/PasswordInput.tsx",
    "content": "import React, { ComponentProps } from 'react'\n\nimport { PasswordInput as RedisPasswordInput } from '@redis-ui/components'\n\nexport type RedisPasswordInputProps = ComponentProps<typeof RedisPasswordInput>\n\nexport default function PasswordInput(props: RedisPasswordInputProps) {\n  return <RedisPasswordInput showExposureToggle=\"never\" {...props} />\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/SearchInput.tsx",
    "content": "import { SearchInput } from '@redis-ui/components'\n\nexport default SearchInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/SwitchInput.spec.tsx",
    "content": "import React from 'react'\nimport userEvent from '@testing-library/user-event'\nimport { render } from '@testing-library/react'\n\nimport SwitchInput from './SwitchInput'\n\ndescribe('SwitchInput', () => {\n  it('should render with default props', () => {\n    const { container } = render(<SwitchInput title=\"On\" />)\n\n    expect(container.firstChild).toHaveTextContent('On')\n  })\n\n  it('should render with titleOff when provided', () => {\n    const { container } = render(\n      <SwitchInput title=\"On\" titleOff=\"Off\" checked={false} />,\n    )\n\n    expect(container.firstChild).toHaveTextContent('Off')\n  })\n\n  it('should fall back to title when titleOff is not provided', () => {\n    const { container } = render(<SwitchInput title=\"On\" checked={false} />)\n\n    expect(container.firstChild).toHaveTextContent('On')\n  })\n\n  it('should call onCheckedChange when toggled', async () => {\n    const onCheckedChange = jest.fn()\n    const { getByRole, container } = render(\n      <SwitchInput title=\"On\" onCheckedChange={onCheckedChange} />,\n    )\n\n    expect(container.firstChild).toHaveTextContent('On')\n\n    const switchElement = getByRole('switch')\n    await userEvent.click(switchElement)\n\n    expect(onCheckedChange).toHaveBeenCalledWith(true)\n    expect(container.firstChild).toHaveTextContent('On')\n  })\n\n  it('should apply custom styles', () => {\n    const { container } = render(\n      <SwitchInput title=\"On\" style={{ backgroundColor: 'red' }} />,\n    )\n    expect(container.firstChild).toHaveStyle('background-color: red')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/SwitchInput.tsx",
    "content": "import React from 'react'\n\nimport { Switch } from '@redis-ui/components'\n\ntype SwitchInputProps = Omit<React.ComponentProps<typeof Switch>, 'titleOn'>\n\nconst SwitchInput = ({\n  style,\n  title,\n  titleOff,\n  ...props\n}: SwitchInputProps) => (\n  <Switch\n    {...props}\n    titleOn={title}\n    titleOff={titleOff !== undefined ? titleOff : title}\n    style={{\n      alignItems: 'center',\n      ...style,\n    }}\n  />\n)\n\nexport default SwitchInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/TextArea.ts",
    "content": "import { TextArea } from '@redis-ui/components'\n\nexport default TextArea\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/TextInput.tsx",
    "content": "import React, { ComponentProps, forwardRef } from 'react'\n\nimport { Input as RedisInput, TooltipProvider } from '@redis-ui/components'\n\nexport type RedisInputProps = ComponentProps<typeof RedisInput>\n\nconst TextInput = forwardRef<\n  React.ElementRef<typeof RedisInput>,\n  RedisInputProps\n>((props, ref) => {\n  // eslint-disable-next-line react/destructuring-assignment\n  if (props.error) {\n    return (\n      <TooltipProvider>\n        <RedisInput ref={ref} {...props} />\n      </TooltipProvider>\n    )\n  }\n  return <RedisInput ref={ref} {...props} />\n})\n\nexport default TextInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/inputs/index.ts",
    "content": "export { default as PasswordInput } from './PasswordInput'\nexport { default as SearchInput } from './SearchInput'\nexport { default as NumericInput } from './NumericInput'\nexport { default as SwitchInput } from './SwitchInput'\nexport { default as TextArea } from './TextArea'\nexport { default as TextInput } from './TextInput'\nexport { default as ComposedInput } from './ComposedInput'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/card/index.tsx",
    "content": "export { Card } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/drawer/index.ts",
    "content": "import { Drawer } from '@redis-ui/components'\n\nconst DrawerHeader = Drawer.Header\nconst DrawerBody = Drawer.Body\nconst DrawerFooter = Drawer.Footer\n\nexport { Drawer, DrawerHeader, DrawerBody, DrawerFooter }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/empty-prompt/RiEmptyPrompt.tsx",
    "content": "import React, { HTMLAttributes } from 'react'\nimport styled from 'styled-components'\nimport { useTheme } from '@redis-ui/styles'\nimport { Spacer } from '../spacer'\n\ninterface RiEmptyPromptProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {\n  body?: React.ReactNode\n  title?: React.ReactNode\n  icon?: React.ReactNode\n}\n\nconst StyledEmptyPrompt = styled.div`\n  max-width: 36em;\n  text-align: center;\n  padding: 24px;\n  margin: auto;\n`\n\nconst RiEmptyPrompt = ({ body, title, icon, ...rest }: RiEmptyPromptProps) => {\n  const theme = useTheme()\n\n  return (\n    <StyledEmptyPrompt {...rest}>\n      {icon}\n      {title && (\n        <>\n          <Spacer size={theme.core.space.space100} />\n          {title}\n        </>\n      )}\n      {body && (\n        <>\n          <Spacer size={theme.core.space.space100} />\n          {body}\n        </>\n      )}\n    </StyledEmptyPrompt>\n  )\n}\n\nexport default RiEmptyPrompt\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/flex/flex.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { alignValues, dirValues, gapSizes, justifyValues } from './flex.styles'\nimport { Col, FlexGroup as Flex, FlexItem, Grid, Row } from './flex'\n\nconst gapStyles = {\n  none: '',\n  xs: '0.2rem',\n  s: '0.4rem',\n  m: '0.8rem',\n  l: '1.2rem',\n  xl: '2rem',\n  xxl: '2.4rem',\n}\ndescribe('Flex Components', () => {\n  it('should render', () => {\n    expect(render(<FlexItem />)).toBeTruthy()\n    expect(\n      render(\n        <Flex>\n          <span>Child</span>\n        </Flex>,\n      ),\n    ).toBeTruthy()\n    expect(\n      render(\n        <Row>\n          <span>Child</span>\n        </Row>,\n      ),\n    ).toBeTruthy()\n    expect(\n      render(\n        <Col>\n          <span>Child</span>\n        </Col>,\n      ),\n    ).toBeTruthy()\n    expect(\n      render(\n        <Grid>\n          <span>Child</span>\n        </Grid>,\n      ),\n    ).toBeTruthy()\n  })\n\n  describe('Flex', () => {\n    it('should render with default classes', () => {\n      const { container } = render(\n        <Row>\n          <span>Child</span>\n        </Row>,\n      )\n      expect(container).toBeTruthy()\n      expect(container.firstChild).toHaveClass('RI-flex-row', 'RI-flex-group')\n      expect(container.firstChild).toHaveStyle('flex-direction: row')\n    })\n\n    describe('Col', () => {\n      it('should render', () => {\n        const { container } = render(\n          <Col>\n            <span>Child</span>\n          </Col>,\n        )\n        expect(container.firstChild).toHaveClass('RI-flex-col', 'RI-flex-group')\n        expect(container.firstChild).toHaveStyle('flex-direction: column')\n      })\n    })\n\n    describe('Props', () => {\n      describe('gap', () => {\n        gapSizes.forEach((value) => {\n          it(`should render gap ${value}`, () => {\n            const { container } = render(\n              <Flex gap={value}>\n                <span>Child</span>\n              </Flex>,\n            )\n            expect(container.firstChild).toHaveClass('RI-flex-group')\n            if (value !== 'none') {\n              expect(container.firstChild).toHaveStyle(\n                `gap: ${gapStyles[value]}`,\n              )\n            } else {\n              expect(container.firstChild).not.toHaveStyle('')\n            }\n          })\n        })\n      })\n      describe('align', () => {\n        alignValues.forEach((value) => {\n          it(`should render ${value} align`, () => {\n            const { container } = render(\n              <Flex align={value}>\n                <span>Child</span>\n              </Flex>,\n            )\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-group',\n              // flex[`align-${value}`],\n            )\n          })\n        })\n      })\n\n      describe('justify', () => {\n        justifyValues.forEach((value) => {\n          it(`should render ${value} justify`, () => {\n            const { container } = render(\n              <Flex justify={value}>\n                <span>Child</span>\n              </Flex>,\n            )\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-group',\n              // flex[`justify-${value}`],\n            )\n          })\n        })\n      })\n\n      describe('dir', () => {\n        dirValues.forEach((value) => {\n          it(`should render ${value} dir`, () => {\n            const { container } = render(\n              <Flex direction={value}>\n                <span>Child</span>\n              </Flex>,\n            )\n\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-group',\n              // flex[`flex-${value}`],\n            )\n          })\n        })\n      })\n\n      describe('wrap', () => {\n        ;[true, false].forEach((value) => {\n          test(`${value} is rendered`, () => {\n            const { container } = render(\n              <Flex wrap={value}>\n                <span>Child</span>\n              </Flex>,\n            )\n\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-group',\n              // value ? flex['flex-wrap'] : '',\n            )\n          })\n        })\n      })\n      describe('responsive', () => {\n        ;[true, false].forEach((value) => {\n          it(`should render ${value} responsive`, () => {\n            const { container } = render(\n              <Flex responsive={value}>\n                <span>Child</span>\n              </Flex>,\n            )\n\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-group',\n              // value ? flex['flex-responsive'] : '',\n            )\n          })\n        })\n      })\n    })\n  })\n\n  describe('FlexItem', () => {\n    describe('inline', () => {\n      it('should render div as default', () => {\n        const { getByText, container } = render(\n          <FlexItem>\n            <span>Child</span>\n          </FlexItem>,\n        )\n        expect(container.firstChild?.nodeName).toEqual('DIV')\n\n        expect(getByText('Child')).toBeInTheDocument()\n      })\n    })\n\n    describe('grow', () => {\n      describe('falsy values', () => {\n        const VALUES = [0, false, null] as const\n\n        VALUES.forEach((value) => {\n          it(`${value} should generate a flex-grow of 0`, () => {\n            const { container } = render(<FlexItem grow={value} />)\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-item',\n              // value ? flex['flex-responsive'] : '',\n            )\n            // assertClassName(flex['flexItem-grow-0'])\n          })\n        })\n      })\n\n      describe('default values', () => {\n        const VALUES = [true, undefined] as const\n\n        VALUES.forEach((value) => {\n          test(`${value} generates a flex-grow of 1`, () => {\n            const { container } = render(<FlexItem grow={value} />)\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-item',\n              // value ? flex['flex-responsive'] : '',\n            )\n            // assertClassName(flex['flexItem-grow-0'])\n          })\n        })\n      })\n\n      describe('numeric values', () => {\n        const VALUES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const\n\n        VALUES.forEach((value) => {\n          test(`${value} generates a flex-grow of ${value}`, () => {\n            const { container } = render(<FlexItem grow={value} />)\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-item',\n              // value ? flex['flex-responsive'] : '',\n            )\n            // assertClassName(flex[`flexItem-grow-${value}`])\n          })\n        })\n      })\n    })\n  })\n\n  describe('Grid', () => {\n    it('should render', () => {\n      expect(\n        render(\n          <Grid>\n            <h2>My Child</h2>\n          </Grid>,\n        ),\n      ).toBeTruthy()\n    })\n    describe('props', () => {\n      describe('gap', () => {\n        gapSizes.forEach((value) => {\n          it(`should render ${value} gap`, () => {\n            const { getByText, container } = render(\n              <Grid gap={value}>\n                <h2>My Child</h2>\n              </Grid>,\n            )\n            const grid = container.firstChild\n            expect(grid).toHaveClass('RI-flex-grid')\n            if (value !== 'none') {\n              expect(grid).toHaveStyle(`gap: ${gapStyles[value]}`)\n            } else {\n              expect(grid).not.toHaveStyle('')\n            }\n            expect(getByText('My Child')).toBeInTheDocument()\n          })\n        })\n      })\n\n      describe('columns', () => {\n        ;([1, 2, 3, 4] as const).forEach((value) => {\n          it(`should render ${value} columns`, () => {\n            const { container } = render(\n              <Grid columns={value}>\n                <h2>My Child</h2>\n              </Grid>,\n            )\n            expect(container.firstChild).toHaveClass(\n              'RI-flex-grid',\n              // flex[`grid-columns-${value}`],\n            )\n          })\n        })\n      })\n\n      describe('responsive', () => {\n        it('should render when responsive is false', () => {\n          const { container } = render(\n            <Grid responsive={false}>\n              <h2>My Child</h2>\n            </Grid>,\n          )\n          expect(container.firstChild).toHaveClass('RI-flex-grid')\n          // expect(container.firstChild).not.toHaveClass(flex.gridResponsive)\n        })\n        it('should have class grid-responsive when responsive is true', () => {\n          const { container } = render(\n            <Grid responsive>\n              <h2>My Child</h2>\n            </Grid>,\n          )\n          expect(container.firstChild).toHaveClass(\n            'RI-flex-grid',\n            // flex['grid-responsive'],\n          )\n        })\n      })\n\n      describe('centered', () => {\n        it('should render when centered is false', () => {\n          const { container } = render(\n            <Grid centered={false}>\n              <h2>My Child</h2>\n            </Grid>,\n          )\n          expect(container.firstChild).toHaveClass('RI-flex-grid')\n          // expect(container.firstChild).not.toHaveClass(flex.gridCentered)\n        })\n        it('should have class grid-centered when responsive is true', () => {\n          const { container } = render(\n            <Grid centered>\n              <h2>My Child</h2>\n            </Grid>,\n          )\n          expect(container.firstChild).toHaveClass(\n            'RI-flex-grid',\n            // flex['grid-centered'],\n          )\n        })\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/flex/flex.styles.ts",
    "content": "import React, { HTMLAttributes, PropsWithChildren, ReactNode } from 'react'\n\nimport styled, { css } from 'styled-components'\nimport { CommonProps, Theme } from 'uiSrc/components/base/theme/types'\n\nexport const gapSizes = ['none', 'xs', 's', 'm', 'l', 'xl', 'xxl'] as const\nexport type GapSizeType = (typeof gapSizes)[number]\nexport const columnCount = [1, 2, 3, 4] as const\nexport type ColumnCountType = (typeof columnCount)[number]\n\nexport type GridProps = HTMLAttributes<HTMLDivElement> & {\n  children: ReactNode\n  columns?: ColumnCountType\n  className?: string\n  gap?: GapSizeType\n  centered?: boolean\n  responsive?: boolean\n}\n\nconst flexGridStyles = {\n  columns: {\n    1: 'repeat(1, max-content)',\n    2: 'repeat(2, max-content)',\n    3: 'repeat(3, max-content)',\n    4: 'repeat(4, max-content)',\n  },\n  responsive: css`\n    @media screen and (max-width: 767px) {\n      grid-template-columns: repeat(1, 1fr);\n      grid-auto-flow: row;\n    }\n  `,\n  centered: css`\n    place-content: center;\n  `,\n}\n\nexport const StyledGrid = styled.div<GridProps>`\n  display: grid;\n  grid-template-columns: ${({ columns = 1 }) =>\n    flexGridStyles.columns[columns] ?? flexGridStyles.columns['1']};\n  gap: ${({ gap = 'none' }) => flexGroupStyles.gapSizes[gap] ?? '0'};\n  ${({ centered = false }) => (centered ? flexGroupStyles.centered : '')}\n  ${({ responsive = false }) => (responsive ? flexGridStyles.responsive : '')}\n`\n\nexport const alignValues = [\n  'center',\n  'stretch',\n  'baseline',\n  'start',\n  'end',\n] as const\nexport const justifyValues = [\n  'center',\n  'start',\n  'end',\n  'between',\n  'around',\n  'evenly',\n] as const\nexport const dirValues = [\n  'row',\n  'rowReverse',\n  'column',\n  'columnReverse',\n] as const\n\nconst flexGroupStyles = {\n  wrap: css`\n    flex-wrap: wrap;\n  `,\n  centered: css`\n    justify-content: center;\n    align-items: center;\n  `,\n  gapSizes: {\n    none: css``,\n    xs: css`\n      ${({ theme }: { theme: Theme }) => theme.core.space.space025};\n    `,\n    s: css`\n      ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n    `,\n    m: css`\n      ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n    `,\n    l: css`\n      ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n    `,\n    xl: css`\n      ${({ theme }: { theme: Theme }) => theme.core.space.space250};\n    `,\n    xxl: css`\n      ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n    `,\n  },\n  justify: {\n    center: 'center',\n    start: 'flex-start',\n    end: 'flex-end',\n    between: 'space-between',\n    around: 'space-around',\n    evenly: 'space-evenly',\n  },\n  align: {\n    center: 'center',\n    stretch: 'stretch',\n    baseline: 'baseline',\n    start: 'flex-start',\n    end: 'flex-end',\n  },\n  direction: {\n    row: 'row',\n    rowReverse: 'row-reverse',\n    column: 'column',\n    columnReverse: 'column-reverse',\n  },\n  responsive: css`\n    @media screen and (max-width: 767px) {\n      flex-wrap: wrap;\n    }\n  `,\n}\n\nexport type FlexProps = PropsWithChildren &\n  CommonProps &\n  React.HTMLAttributes<HTMLDivElement> & {\n    gap?: GapSizeType\n    align?: (typeof alignValues)[number]\n    direction?: (typeof dirValues)[number]\n    justify?: (typeof justifyValues)[number]\n    centered?: boolean\n    responsive?: boolean\n    wrap?: boolean\n    grow?: boolean\n    full?: boolean\n  }\n\ntype StyledFlexProps = Omit<\n  FlexProps,\n  | 'grow'\n  | 'full'\n  | 'gap'\n  | 'align'\n  | 'direction'\n  | 'justify'\n  | 'centered'\n  | 'responsive'\n  | 'wrap'\n> & {\n  $grow?: boolean\n  $gap?: GapSizeType\n  $align?: FlexProps['align']\n  $direction?: FlexProps['direction']\n  $justify?: FlexProps['justify']\n  $centered?: boolean\n  $responsive?: boolean\n  $wrap?: boolean\n  $full?: boolean\n}\nexport const StyledFlex = styled.div<StyledFlexProps>`\n  display: flex;\n  flex-grow: ${({ $grow = true }) => ($grow ? 1 : 0)};\n  gap: ${({ $gap = 'none' }) => flexGroupStyles.gapSizes[$gap] ?? '0'};\n  align-items: ${({ $align = 'stretch' }) =>\n    flexGroupStyles.align[$align] ?? 'stretch'};\n  flex-direction: ${({ $direction = 'row' }) =>\n    flexGroupStyles.direction[$direction] ?? 'row'};\n  justify-content: ${({ $justify = 'start' }) =>\n    flexGroupStyles.justify[$justify] ?? 'flex-start'};\n  ${({ $centered = false }) => ($centered ? flexGroupStyles.centered : '')}\n  ${({ $responsive = false }) =>\n    $responsive ? flexGroupStyles.responsive : ''}\n  ${({ $wrap = false }) => ($wrap ? flexGroupStyles.wrap : '')}\n  ${({ $full = false, $direction = 'row' }) =>\n    $full\n      ? $direction === 'row' || $direction === 'rowReverse'\n        ? 'width: 100%' // if it is row make it full width\n        : 'height: 100%;' // else, make it full height\n      : ''}\n`\nexport const flexItemStyles = {\n  growZero: css`\n    flex-grow: 0;\n    flex-basis: auto;\n  `,\n  grow: css`\n    flex-basis: 0;\n    min-width: 0;\n  `,\n  growSizes: {\n    '1': css`\n      flex-grow: 1;\n    `,\n    '2': css`\n      flex-grow: 2;\n    `,\n    '3': css`\n      flex-grow: 3;\n    `,\n    '4': css`\n      flex-grow: 4;\n    `,\n    '5': css`\n      flex-grow: 5;\n    `,\n    '6': css`\n      flex-grow: 6;\n    `,\n    '7': css`\n      flex-grow: 7;\n    `,\n    '8': css`\n      flex-grow: 8;\n    `,\n    '9': css`\n      flex-grow: 9;\n    `,\n    '10': css`\n      flex-grow: 10;\n    `,\n  },\n  padding: {\n    0: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space000};\n    `,\n    1: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space010};\n    `,\n    2: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space025};\n    `,\n    3: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n    `,\n    4: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n    `,\n    5: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n    `,\n    6: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n    `,\n    7: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space250};\n    `,\n    8: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n    `,\n    9: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space400};\n    `,\n    10: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space500};\n    `,\n    11: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space550};\n    `,\n    12: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space600};\n    `,\n    13: css`\n      padding: ${({ theme }: { theme: Theme }) => theme.core.space.space800};\n    `,\n  },\n}\n\nexport const VALID_GROW_VALUES = [\n  null,\n  undefined,\n  true,\n  false,\n  0,\n  1,\n  2,\n  3,\n  4,\n  5,\n  6,\n  7,\n  8,\n  9,\n  10,\n] as const\n\nexport type PaddingType =\n  | keyof typeof flexItemStyles.padding\n  | null\n  | undefined\n  | true\n  | false\n\nexport type FlexItemProps = React.HTMLAttributes<HTMLDivElement> &\n  PropsWithChildren &\n  CommonProps & {\n    grow?: (typeof VALID_GROW_VALUES)[number]\n    $direction?: (typeof dirValues)[number]\n    $padding?: PaddingType\n    $gap?: GapSizeType\n  }\n\nexport const StyledFlexItem = styled.div<FlexItemProps>`\n  display: flex;\n  gap: ${({ $gap = 'none' }) => ($gap ? flexGroupStyles.gapSizes[$gap] : '')};\n  flex-direction: ${({ $direction = 'column' }) =>\n    flexGroupStyles.direction[$direction] ?? 'column'};\n  ${({ grow }) => {\n    if (!grow) {\n      return flexItemStyles.growZero\n    }\n    const result = [flexItemStyles.grow]\n    if (typeof grow === 'number') {\n      result.push(flexItemStyles.growSizes[grow])\n    } else {\n      result.push(flexItemStyles.growSizes['1'])\n    }\n    return result.join('\\n')\n  }}\n  ${({ $padding }) => {\n    if ($padding === null || $padding === undefined || $padding === false) {\n      return ''\n    }\n    if ($padding === true) {\n      return flexItemStyles.padding['4'] // Default padding (space100)\n    }\n    if (flexItemStyles.padding[$padding] !== undefined) {\n      return flexItemStyles.padding[$padding]\n    }\n    return ''\n  }}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/flex/flex.tsx",
    "content": "import React from 'react'\nimport classNames from 'classnames'\nimport {\n  dirValues,\n  FlexItemProps,\n  FlexProps,\n  GridProps,\n  PaddingType,\n  StyledFlex,\n  StyledFlexItem,\n  StyledGrid,\n} from 'uiSrc/components/base/layout/flex/flex.styles'\n\nexport const Grid = ({ children, className, ...rest }: GridProps) => {\n  const classes = classNames('RI-flex-grid', className)\n  return (\n    <StyledGrid {...rest} className={classes}>\n      {children}\n    </StyledGrid>\n  )\n}\n\n/**\n * Flex Group Component\n *\n * A flexbox container that can be used to lay out other flex items.\n * All properties are passed directly to the underlying `div`.\n *\n * @remarks\n * This is more or less direct reimplementation of `EuiFlexGroup`\n *\n * @example\n * <FlexGroup>\n *   <FlexItem grow={2}>\n *     Left\n *   </FlexItem>\n *   <FlexItem grow={3}>\n *     Right\n *   </FlexItem>\n * </FlexGroup>\n */\nexport const FlexGroup = ({\n  children,\n  className,\n  grow,\n  justify,\n  gap,\n  wrap,\n  full,\n  align,\n  direction,\n  responsive,\n  centered,\n  ...rest\n}: FlexProps) => {\n  const classes = classNames('RI-flex-group', className)\n  return (\n    <StyledFlex\n      {...rest}\n      className={classes}\n      $grow={grow}\n      $justify={justify}\n      $gap={gap}\n      $wrap={wrap}\n      $full={full}\n      $align={align}\n      $direction={direction}\n      $responsive={responsive}\n      $centered={centered}\n    >\n      {children}\n    </StyledFlex>\n  )\n}\n\n/**\n * Column Component\n *\n * A Column component is a special type of FlexGroup that is meant to be used when you\n * want to lay out a group of items in a vertical column. It is functionally equivalent to\n * using a FlexGroup with a direction of 'column', but includes some additional conveniences.\n *\n * This is the preferred API of a component that is not meant to be distributed but widely used in our project\n *\n * @example\n * <Col>\n *   <FlexItem grow={2}>\n *     Top\n *   </FlexItem>\n *   <FlexItem grow={3}>\n *     Bottom\n *   </FlexItem>\n * </Col>\n */\nexport const Col = ({\n  className,\n  reverse,\n  contentCentered,\n  align,\n  justify,\n  ...rest\n}: Omit<FlexProps, 'direction'> & {\n  reverse?: boolean\n  contentCentered?: boolean\n}) => {\n  const classes = classNames('RI-flex-col', className)\n  return (\n    <FlexGroup\n      {...rest}\n      align={contentCentered ? 'center' : align}\n      justify={contentCentered ? 'center' : justify}\n      className={classes}\n      direction={reverse ? 'columnReverse' : 'column'}\n    />\n  )\n}\n\nexport const Row = ({\n  className,\n  reverse,\n  ...rest\n}: Omit<FlexProps, 'direction'> & {\n  reverse?: boolean\n}) => {\n  const classes = classNames('RI-flex-row', className)\n  return (\n    <FlexGroup\n      {...rest}\n      className={classes}\n      direction={reverse ? 'rowReverse' : 'row'}\n    />\n  )\n}\n\n/**\n * Flex item component\n *\n * This represents a more or less direct implementation of `EuiFlexItem`\n *\n * @remarks\n * This component is useful when you want to create a flex item that can\n * grow or shrink based on the available space.\n *\n * @example\n * <FlexItem grow={2}>\n *   <div>Content</div>\n * </FlexItem>\n */\nexport const FlexItem = ({\n  children,\n  className,\n  grow = false,\n  padding,\n  direction,\n  ...rest\n}: Omit<FlexItemProps, '$padding' | '$direction'> & {\n  padding?: PaddingType\n  direction?: (typeof dirValues)[number]\n}) => {\n  const classes = classNames('RI-flex-item', className)\n  return (\n    <StyledFlexItem\n      {...rest}\n      grow={grow}\n      $padding={padding}\n      $direction={direction}\n      className={classes}\n    >\n      {children}\n    </StyledFlexItem>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/flex/index.ts",
    "content": "export { FlexGroup, FlexGroup as Flex, FlexItem, Col, Row, Grid } from './flex'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/flex.module.scss",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-rule/HorizontalRule.spec.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport HorizontalRule from './HorizontalRule'\n\ndescribe('HorizontalRule', () => {\n  it('should render with default props', () => {\n    const { container } = render(<HorizontalRule />)\n    expect(container).toBeTruthy()\n    expect(container.firstChild).toHaveStyle('width: 100%')\n  })\n\n  it('should render with set size and margin', () => {\n    const { container } = render(<HorizontalRule size=\"half\" margin=\"xs\" />)\n    expect(container).toBeTruthy()\n    expect(container.firstChild).toHaveStyle('width: 50%')\n    expect(container.firstChild).toHaveStyle('margin-inline: auto')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-rule/HorizontalRule.tsx",
    "content": "import classNames from 'classnames'\nimport React from 'react'\n\nimport {\n  HorizontalRuleProps,\n  StyledHorizontalRule,\n} from './horizontal-rule.styles'\n\nconst HorizontalRule = ({\n  className,\n  size = 'full',\n  margin = 'l',\n  color,\n  colorVariable,\n  ...rest\n}: HorizontalRuleProps) => {\n  const classes = classNames('RI-horizontal-rule', className)\n\n  return (\n    <StyledHorizontalRule\n      size={size}\n      margin={margin}\n      className={classes}\n      color={color}\n      colorVariable={colorVariable}\n      {...rest}\n    />\n  )\n}\n\nexport default HorizontalRule\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-rule/horizontal-rule.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled, { css } from 'styled-components'\n\nexport const SIZES = ['full', 'half', 'quarter'] as const\nexport const MARGINS = ['none', 'xs', 's', 'm', 'l', 'xl', 'xxl'] as const\n\nexport type HorizontalRuleSize = (typeof SIZES)[number]\nexport type HorizontalRuleMargin = (typeof MARGINS)[number]\n\nconst horizontalRuleStyles = {\n  size: {\n    full: css`\n      width: 100%;\n    `,\n    half: css`\n      width: 50%;\n      margin-inline: auto;\n    `,\n    quarter: css`\n      width: 25%;\n      margin-inline: auto;\n    `,\n  },\n  margin: {\n    none: '',\n    xs: css`\n      margin-block: var(--size-xs);\n    `,\n    s: css`\n      margin-block: var(--size-s);\n    `,\n    m: css`\n      margin-block: var(--size-m);\n    `,\n    l: css`\n      margin-block: var(--size-l);\n    `,\n    xl: css`\n      margin-block: var(--size-xl);\n    `,\n    xxl: css`\n      margin-block: var(--size-xxl);\n    `,\n  },\n}\n\nexport interface HorizontalRuleProps extends HTMLAttributes<HTMLHRElement> {\n  size?: HorizontalRuleSize\n  margin?: HorizontalRuleMargin\n  color?: string\n  colorVariable?: string\n}\n\nexport const StyledHorizontalRule = styled.hr<\n  Omit<HorizontalRuleProps, 'size' | 'margin'> & {\n    size?: HorizontalRuleSize\n    margin?: HorizontalRuleMargin\n    color?: string\n    colorVariable?: string\n  }\n>`\n  ${({ size = 'full' }) => horizontalRuleStyles.size[size]}\n  ${({ margin = 'l' }) => horizontalRuleStyles.margin[margin]}\n\n  /* Reset the default styles */\n  border: none;\n\n  /* If the component is inside a flex box */\n  flex-shrink: 0;\n  flex-grow: 0;\n\n  background-color: ${({ color, colorVariable }) =>\n    color || colorVariable\n      ? (color ?? `var(--${colorVariable})`)\n      : 'var(--hrBackgroundColor)'};\n  height: 1px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-spacer/HorizontalSpacer.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { HorizontalSpacer } from './horizontal-spacer'\n\ndescribe('HorizontalSpacer', () => {\n  it('should render with different sizes correctly', () => {\n    const sizes = ['xs', 's', 'm', 'l', 'xl', 'xxl'] as const\n\n    sizes.forEach((size) => {\n      const { container } = render(<HorizontalSpacer size={size} />)\n      const spacer = container.querySelector(\n        '.RI-horizontal-spacer',\n      ) as HTMLElement\n\n      if (size === 'xl') {\n        expect(spacer).toHaveStyle('width: calc(var(--base) * 2.25)')\n      } else {\n        expect(spacer).toHaveStyle(`width: var(--size-${size})`)\n      }\n    })\n  })\n\n  it('should render children when provided', () => {\n    const { getByText } = render(\n      <HorizontalSpacer size=\"s\">\n        <span>Test content</span>\n      </HorizontalSpacer>,\n    )\n    const content = getByText('Test content')\n    expect(content).toBeInTheDocument()\n    expect(content.parentElement).toHaveStyle('width: var(--size-s)')\n  })\n\n  it('should apply custom className', () => {\n    const { container } = render(<HorizontalSpacer className=\"custom-class\" />)\n    const spacer = container.querySelector(\n      '.RI-horizontal-spacer',\n    ) as HTMLElement\n\n    expect(spacer).toHaveClass('RI-horizontal-spacer')\n    expect(spacer).toHaveClass('custom-class')\n  })\n\n  it('should pass through custom props', () => {\n    const { container } = render(\n      <HorizontalSpacer data-testid=\"my-spacer\" id=\"spacer-id\" />,\n    )\n    const spacer = container.querySelector(\n      '.RI-horizontal-spacer',\n    ) as HTMLElement\n\n    expect(spacer).toHaveAttribute('data-testid', 'my-spacer')\n    expect(spacer).toHaveAttribute('id', 'spacer-id')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-spacer/horizontal-spacer.styles.ts",
    "content": "import { HTMLAttributes, ReactNode } from 'react'\nimport styled from 'styled-components'\nimport { CommonProps } from 'uiSrc/components/base/theme/types'\n\nexport const HorizontalSpacerSizes = ['xs', 's', 'm', 'l', 'xl', 'xxl'] as const\nexport type HorizontalSpacerSize = (typeof HorizontalSpacerSizes)[number]\nexport type HorizontalSpacerProps = CommonProps &\n  HTMLAttributes<HTMLDivElement> & {\n    children?: ReactNode\n    size?: HorizontalSpacerSize\n  }\n\nexport const horizontalSpacerStyles = {\n  xs: 'var(--size-xs)',\n  s: 'var(--size-s)',\n  m: 'var(--size-m)',\n  l: 'var(--size-l)',\n  xl: 'calc(var(--base) * 2.25)',\n  xxl: 'var(--size-xxl)',\n}\n\nexport const StyledHorizontalSpacer = styled.div<HorizontalSpacerProps>`\n  flex-shrink: 0;\n  width: ${({ size = 'l' }) => horizontalSpacerStyles[size]};\n  display: inline-block;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-spacer/horizontal-spacer.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport {\n  HorizontalSpacerProps,\n  StyledHorizontalSpacer,\n} from './horizontal-spacer.styles'\n\nexport const HorizontalSpacer = ({\n  className,\n  children,\n  ...rest\n}: HorizontalSpacerProps) => (\n  <StyledHorizontalSpacer\n    {...rest}\n    className={cx('RI-horizontal-spacer', className)}\n  >\n    {children}\n  </StyledHorizontalSpacer>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/horizontal-spacer/index.ts",
    "content": "export { HorizontalSpacer } from './horizontal-spacer'\nexport type {\n  HorizontalSpacerSize,\n  HorizontalSpacerProps,\n} from './horizontal-spacer.styles'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/index.ts",
    "content": "import HorizontalRule from './horizontal-rule/HorizontalRule'\nimport LoadingContent from './loading-content/LoadingContent'\nimport ResizableContainer from './resize/container/ResizableContainer'\nimport ResizablePanel from './resize/panel/ResizablePanel'\nimport ResizablePanelHandle from './resize/handle/ResizablePanelHandle'\nimport RiEmptyPrompt from './empty-prompt/RiEmptyPrompt'\n\nexport * from './card'\nexport * from './horizontal-spacer'\nexport * from './spacer'\nexport * from './stepper'\nexport {\n  HorizontalRule,\n  LoadingContent,\n  ResizablePanel,\n  ResizableContainer,\n  ResizablePanelHandle,\n  RiEmptyPrompt,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/list/Group.tsx",
    "content": "import React from 'react'\nimport classNames from 'classnames'\nimport {\n  ListClassNames,\n  ListGroupProps,\n  MAX_FORM_WIDTH,\n  StyledGroup,\n} from 'uiSrc/components/base/layout/list/list.styles'\n\nconst Group = ({\n  children,\n  className,\n  style,\n  maxWidth = true,\n  gap,\n  flush,\n  ...rest\n}: ListGroupProps) => {\n  let newStyle = style\n\n  if (maxWidth) {\n    newStyle = {\n      ...newStyle,\n      maxWidth: maxWidth === true ? MAX_FORM_WIDTH : maxWidth,\n    }\n  }\n  const classes = classNames(ListClassNames.listGroup, className)\n  return (\n    <StyledGroup\n      {...rest}\n      className={classes}\n      style={newStyle}\n      $gap={gap}\n      $flush={flush}\n    >\n      {children}\n    </StyledGroup>\n  )\n}\n\nexport default Group\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/list/Item.tsx",
    "content": "import React, { ButtonHTMLAttributes, ReactElement } from 'react'\nimport cx from 'classnames'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { useInnerText } from 'uiSrc/components/base/utils/hooks/inner-text'\nimport {\n  ListClassNames,\n  ListGroupItemProps,\n  StyledItem,\n  StyledItemInnerButton,\n  StyledItemInnerSpan,\n  StyledLabel,\n} from './list.styles'\n\nconst Item = ({\n  size,\n  label,\n  isActive,\n  isDisabled,\n  className,\n  children: _children,\n  onClick,\n  iconType,\n  iconProps,\n  icon,\n  wrapText,\n  buttonRef,\n  color,\n  ...rest\n}: ListGroupItemProps) => {\n  const isClickable = !!onClick\n  let iconNode: ReactElement\n\n  if (iconType) {\n    iconNode = (\n      <RiIcon\n        color=\"currentColor\" // forces the icon to inherit its parent color\n        {...iconProps}\n        type={iconType}\n        className={cx('ListGroupItem__icon', iconProps?.className)}\n      />\n    )\n\n    if (icon) {\n      console.warn(\n        'Both `iconType` and `icon` were passed to EuiListGroupItem but only one can exist. The `iconType` was used.',\n      )\n    }\n  } else if (icon) {\n    iconNode = icon\n  } else {\n    iconNode = <></>\n  }\n  // Only add the label as the title attribute if it's possibly truncated\n  // Also ensure the value of the title attribute is a string\n  const [ref, innerText] = useInnerText()\n  const labelContent = !wrapText ? (\n    <StyledLabel\n      ref={ref}\n      className=\"label\"\n      title={typeof label === 'string' ? label : innerText}\n      wrapText={wrapText}\n    >\n      {label}\n    </StyledLabel>\n  ) : (\n    <StyledLabel className=\"label\" wrapText={wrapText}>\n      {label}\n    </StyledLabel>\n  )\n  let itemContent: ReactElement\n  if (isDisabled || onClick) {\n    itemContent = (\n      <StyledItemInnerButton\n        type=\"button\"\n        className={ListClassNames.listItemButton}\n        disabled={isDisabled}\n        onClick={onClick}\n        $isActive={isActive}\n        $isDisabled={isDisabled}\n        $isClickable={isClickable}\n        $size={size}\n        ref={buttonRef}\n        $color={color}\n        {...(rest as Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'color'>)}\n      >\n        {iconNode !== undefined && iconNode}\n        {labelContent}\n      </StyledItemInnerButton>\n    )\n  } else {\n    itemContent = (\n      <StyledItemInnerSpan\n        $isClickable={false}\n        $isActive={isActive}\n        $isDisabled={isDisabled}\n        className={ListClassNames.listItemText}\n        $color={color}\n        $size={size}\n        {...rest}\n      >\n        {iconNode !== undefined && iconNode}\n        {labelContent}\n      </StyledItemInnerSpan>\n    )\n  }\n\n  return (\n    <StyledItem\n      $size={size}\n      $isActive={isActive}\n      $isDisabled={isDisabled}\n      $color={color}\n      className={cx(ListClassNames.listItem, className, {\n        [ListClassNames.listItemActive]: isActive,\n        [ListClassNames.listItemDisabled]: isDisabled,\n      })}\n    >\n      {itemContent}\n    </StyledItem>\n  )\n}\n\nexport default Item\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/list/index.ts",
    "content": "import Group from './Group'\nimport Item from './Item'\n\nexport { Group, Item }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/list/list.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport {\n  AllHTMLAttributes,\n  ButtonHTMLAttributes,\n  CSSProperties,\n  HTMLAttributes,\n  MouseEventHandler,\n  ReactElement,\n  ReactNode,\n  Ref,\n} from 'react'\n\nimport { AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\nimport { IconProps } from 'uiSrc/components/base/icons'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const ListClassNames = {\n  listItem: 'RI-list-group-item',\n  listItemLabel: 'RI-list-group-item-label',\n  listItemButton: 'RI-list-group-item-button',\n  listItemText: 'RI-list-group-item-text',\n  listGroup: 'RI-list-group',\n  listItemActive: 'isActive',\n  listItemDisabled: 'isDisabled',\n}\n\nexport const MAX_FORM_WIDTH = 400\n\nexport type ListGroupProps = HTMLAttributes<HTMLUListElement> & {\n  className?: string\n  /**\n   * Remove container padding, stretching list items to the edges\n   * @default false\n   */\n  flush?: boolean\n\n  /**\n   * Spacing between list items\n   * @default s\n   */\n  gap?: keyof typeof listStyles.gap\n\n  /**\n   * Sets the max-width of the page.\n   * Set to `true` to use the default size,\n   * set to `false` to not restrict the width,\n   * or set to a number/string for a custom CSS width/measurement.\n   *\n   * @default true\n   */\n  maxWidth?: boolean | CSSProperties['maxWidth']\n}\n\nexport const listStyles = {\n  gap: {\n    none: 'gap: 0;',\n    s: css`\n      gap: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n    `,\n    m: css`\n      gap: ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n    `,\n  },\n  flush: css`\n    margin: 0;\n    padding: 0;\n    border: 0 none;\n\n    .${ListClassNames.listItem} {\n      border-radius: 0;\n    }\n  `,\n}\n\nexport const StyledGroup = styled.ul<\n  Omit<ListGroupProps, 'gap' | 'flush' | 'maxWidth'> & {\n    $gap?: keyof typeof listStyles.gap\n    $flush?: boolean\n  }\n>`\n  display: flex;\n  flex-direction: column;\n  ${({ $gap = 's' }) => listStyles.gap[$gap]};\n  ${({ $flush = false }) => $flush && listStyles.flush};\n`\n\nexport const SIZES = ['xs', 's', 'm', 'l'] as const\nexport type ListGroupItemSize = (typeof SIZES)[number]\n\nexport const COLORS = ['primary', 'text', 'subdued', 'ghost'] as const\nexport type ListGroupItemColor = (typeof COLORS)[number]\n\nexport type ListGroupItemProps = HTMLAttributes<HTMLLIElement> & {\n  /**\n   * Size of the label text\n   */\n  size?: ListGroupItemSize\n  /**\n   * By default, the item will get the color `text`.\n   * You can customize the color of the item by passing a color name.\n   */\n  color?: ListGroupItemColor\n\n  /**\n   * Content to be displayed in the list item\n   */\n  label: ReactNode\n\n  /**\n   * Apply styles indicating an item is active\n   */\n  isActive?: boolean\n\n  /**\n   * Apply styles indicating an item is disabled\n   */\n  isDisabled?: boolean\n\n  /**\n   * Adds `RiIcon` of `RiIcon.type`\n   */\n  iconType?: AllIconsType\n\n  /**\n   * Further extend the props applied to RiIcon\n   */\n  iconProps?: IconProps\n\n  /**\n   * Custom node to pass as the icon. Cannot be used in conjunction\n   * with `iconType` and `iconProps`.\n   */\n  icon?: ReactElement\n\n  /**\n   * Make the list item label a button.\n   * While permitted, `href` and `onClick` should not be used together in most cases and may create problems.\n   */\n  onClick?: MouseEventHandler<HTMLButtonElement>\n\n  /**\n   * Allow link text to wrap\n   */\n  wrapText?: boolean\n\n  /**\n   * Pass-through ref reference specifically for targeting\n   * instances where the item content is rendered as a `button`\n   */\n  buttonRef?: Ref<HTMLButtonElement>\n}\n\nconst listItemStyles = {\n  size: {\n    xs: css`\n      border-radius: var(--border-radius-small);\n    `,\n    s: css`\n      border-radius: var(--border-radius-small);\n    `,\n    m: css`\n      border-radius: var(--border-radius-medium);\n    `,\n    l: css`\n      border-radius: var(--border-radius-medium);\n    `,\n  },\n  active: {\n    primary: css`\n      background-color: var(--color-primary);\n    `,\n    text: css`\n      background-color: var(--color-subdued);\n    `,\n    subdued: css`\n      background-color: var(--color-subdued);\n    `,\n    ghost: css`\n      background-color: var(--color-ghost);\n    `,\n  },\n  clickable: {\n    primary: css`\n      &:hover,\n      &:focus-within {\n        background-color: var(--color-subdued);\n      }\n    `,\n    text: css`\n      &:hover,\n      &:focus-within {\n        background-color: var(--color-subdued);\n      }\n    `,\n    subdued: css`\n      &:hover,\n      &:focus-within {\n        background-color: var(--color-subdued);\n      }\n    `,\n    ghost: css`\n      &:hover,\n      &:focus-within {\n        background-color: var(--color-ghost);\n      }\n    `,\n  },\n}\n\nexport const StyledItem = styled.li<\n  Omit<\n    ListGroupItemProps,\n    'label' | 'color' | 'size' | 'isDisabled' | 'isActive'\n  > & {\n    $size?: ListGroupItemSize\n    $color?: ListGroupItemColor\n    $isDisabled?: boolean\n    $isActive?: boolean\n  }\n>`\n  padding: 0;\n  display: flex;\n  align-items: center;\n  position: relative;\n  transition: background-color 150ms;\n  ${({ $size = 'm' }) => listItemStyles.size[$size]};\n  ${({ $isActive = false, $color = 'text' }) =>\n    $isActive && listItemStyles.active[$color]};\n  ${({ onClick, $color = 'text' }) =>\n    onClick !== undefined && listItemStyles.clickable[$color]};\n`\n\nconst listItemInnerStyles = {\n  base: css`\n    padding: var(--size-xs) var(--size-s);\n    display: flex;\n    align-items: center;\n    flex-grow: 1;\n    max-inline-size: 100%;\n    overflow: hidden;\n    text-align: start;\n    font-weight: inherit;\n  `,\n  size: {\n    xs: css`\n      font-size: var(--font-size-xs);\n      font-weight: var(--font-weight-m);\n      letter-spacing: 0;\n      min-height: var(--size-l);\n    `,\n    s: css`\n      font-size: var(--font-size-s);\n      font-weight: var(--font-weight-m);\n      letter-spacing: 0;\n      min-height: var(--size-xl);\n    `,\n    m: css`\n      font-size: var(--font-size-m);\n      min-height: var(--size-xl);\n    `,\n    l: css`\n      font-size: var(--font-size-l);\n      min-height: var(--size-xxl);\n    `,\n  },\n  colors: {\n    // Colors\n    primary: css`\n      color: ${({ theme }: { theme: Theme }) =>\n        theme.components.typography.colors.primary};\n    `,\n    text: css`\n      color: ${({ theme }: { theme: Theme }) =>\n        theme.components.typography.colors.secondary};\n    `,\n    subdued: css`\n      color: ${({ theme }: { theme: Theme }) =>\n        theme.semantic.color.text.informative400};\n    `,\n    ghost: css`\n      color: ${({ theme }: { theme: Theme }) =>\n        theme.semantic.color.text.neutral600};\n    `,\n  },\n  variants: {\n    // Variants\n    isDisabled: css`\n      cursor: not-allowed;\n\n      &,\n      &:hover,\n      &:focus {\n        color: #ffffff;\n        background-color: transparent;\n        text-decoration: none;\n      }\n    `,\n    isActive: css``,\n    isClickable: css`\n      &:hover,\n      &:focus {\n        text-decoration: underline;\n      }\n    `,\n    externalIcon: css`\n      margin-left: var(--size-s);\n    `,\n  },\n}\n\ntype InnerProps = {\n  $size?: ListGroupItemSize\n  $color?: ListGroupItemColor\n  $isActive?: boolean\n  $isDisabled?: boolean\n  $isClickable?: boolean\n  ref?: React.Ref<HTMLButtonElement | HTMLSpanElement>\n}\n\nexport const StyledItemInnerButton = styled.button<\n  ButtonHTMLAttributes<HTMLButtonElement> & InnerProps\n>`\n  ${listItemInnerStyles.base}\n  ${({ $size = 'm' }) => listItemInnerStyles.size[$size]}\n    ${({ $isActive = false }) =>\n    $isActive && listItemInnerStyles.variants.isActive}\n    ${({ $isDisabled = false }) =>\n    $isDisabled && listItemInnerStyles.variants.isDisabled}\n    ${({ $isDisabled = false, $color = 'text' }) =>\n    !$isDisabled && listItemInnerStyles.colors[$color]}\n    ${({ $isDisabled = false, $isClickable = false }) =>\n    !$isDisabled && $isClickable && listItemInnerStyles.variants.isClickable}\n`\n\nexport const StyledItemInnerSpan = styled.span<\n  Omit<AllHTMLAttributes<HTMLSpanElement>, 'size'> & InnerProps\n>`\n  ${listItemInnerStyles.base}\n  ${({ $size = 'm' }) => listItemInnerStyles.size[$size]}\n    ${({ $isActive = false }) =>\n    $isActive && listItemInnerStyles.variants.isActive}\n    ${({ $isDisabled = false }) =>\n    $isDisabled && listItemInnerStyles.variants.isDisabled}\n    ${({ $isDisabled = false, $color = 'text' }) =>\n    !$isDisabled && listItemInnerStyles.colors[$color]}\n    ${({ $isDisabled = false, $isClickable = false }) =>\n    !$isDisabled && $isClickable && listItemInnerStyles.variants.isClickable}\n`\n\nconst listItemLabelStyles = {\n  truncate: css`\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n  `,\n  wrapText: css`\n    overflow-wrap: break-word !important; // makes sure the long string will wrap and not bust out of the container\n    word-break: break-word;\n  `,\n}\n\nexport const StyledLabel = styled.span<{\n  wrapText?: boolean\n  children: ReactNode\n  className?: string\n  title?: string | ReactNode\n  ref?: React.Ref<HTMLSpanElement>\n}>`\n  white-space: break-spaces;\n  ${({ wrapText }) =>\n    wrapText ? listItemLabelStyles.wrapText : listItemLabelStyles.truncate}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/loading-content/LoadingContent.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport LoadingContent from './LoadingContent'\n\ndescribe('LoadingContent', () => {\n  it('should render the component', () => {\n    const { container } = render(<LoadingContent />)\n    expect(container.firstChild).toHaveClass('RI-loading-content')\n  })\n\n  it('should render the default number of lines (3)', () => {\n    const { container } = render(<LoadingContent />)\n    const lines = container.querySelectorAll('.RI-loading-content > span')\n    expect(lines.length).toBe(3)\n  })\n\n  it('should render the correct number of lines when \"lines\" prop is passed', () => {\n    const { container } = render(<LoadingContent lines={5} />)\n    const lines = container.querySelectorAll('.RI-loading-content > span')\n    expect(lines.length).toBe(5)\n  })\n\n  it('should apply the custom className if provided', () => {\n    const { container } = render(<LoadingContent className=\"custom-class\" />)\n    expect(container.firstChild).toHaveClass('custom-class')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/loading-content/LoadingContent.tsx",
    "content": "import React from 'react'\nimport classNames from 'classnames'\n\nimport {\n  StyledLoadingContent,\n  LoadingContentProps,\n  SingleLine,\n  SingleLineBackground,\n} from './loading-content.styles'\n\nconst LoadingContent = ({\n  className,\n  lines = 3,\n  ...rest\n}: LoadingContentProps) => {\n  const classes = classNames('RI-loading-content', className)\n  const lineElements = []\n\n  for (let i = 0; i < lines; i++) {\n    lineElements.push(\n      <SingleLine key={i}>\n        <SingleLineBackground />\n      </SingleLine>,\n    )\n  }\n\n  return (\n    <StyledLoadingContent className={classes} {...rest}>\n      {lineElements}\n    </StyledLoadingContent>\n  )\n}\n\nexport default LoadingContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/loading-content/loading-content.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled, { keyframes } from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport type LineRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10\n\nexport interface LoadingContentProps extends HTMLAttributes<HTMLDivElement> {\n  lines?: LineRange\n}\n\nconst loadingAnimation = keyframes`\n  0% {\n    transform: translateX(-53%);\n  }\n\n  100% {\n    transform: translateX(0);\n  }\n`\n\nexport const StyledLoadingContent = styled.span<\n  React.HtmlHTMLAttributes<HTMLSpanElement>\n>`\n  display: block;\n  width: 100%;\n`\n\nexport const SingleLine = styled.span<\n  React.HtmlHTMLAttributes<HTMLSpanElement> & { theme: Theme }\n>`\n  display: block;\n  width: 100%;\n  height: ${({ theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }) => theme.core.space.space100};\n  border-radius: ${({ theme }) => theme.core.space.space050};\n  overflow: hidden;\n\n  &:last-child:not(:only-child) {\n    width: 75%;\n  }\n`\n\nexport const SingleLineBackground = styled.span<{ theme: Theme }>`\n  display: block;\n  width: 220%;\n  height: 100%;\n  background: linear-gradient(\n    137deg,\n    ${({ theme }) => theme.semantic.color.background.neutral200} 45%,\n    ${({ theme }) => theme.semantic.color.background.neutral300} 50%,\n    ${({ theme }) => theme.semantic.color.background.neutral200} 55%\n  );\n  animation: ${loadingAnimation} 1.5s ease-in-out infinite;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/menu/index.ts",
    "content": "import { Menu } from '@redis-ui/components'\n\nconst MenuContent = Menu.Content\nconst MenuTrigger = Menu.Trigger\nconst MenuItem = Menu.Content.Item\nconst MenuDropdownArrow = Menu.Content.DropdownArrow\n\nexport { Menu, MenuContent, MenuItem, MenuTrigger, MenuDropdownArrow }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/Page.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport {\n  PageClassNames,\n  PageProps,\n  restrictWidthSize,\n  StyledPage,\n} from 'uiSrc/components/base/layout/page/page.styles'\n\nconst Page = ({\n  className,\n  restrictWidth = false,\n  paddingSize = 'm',\n  grow = true,\n  direction = 'row',\n  style,\n  ...rest\n}: PageProps) => (\n  <StyledPage\n    {...rest}\n    className={cx(PageClassNames.page, className)}\n    $grow={grow}\n    $direction={direction}\n    $restrictWidth={restrictWidth}\n    $paddingSize={paddingSize}\n    style={restrictWidthSize(style, restrictWidth)}\n  />\n)\n\nexport default Page\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/PageBody.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport {\n  PageClassNames,\n  restrictWidthSize,\n} from 'uiSrc/components/base/layout/page/page.styles'\nimport {\n  ComponentTypes,\n  PageBodyProps,\n  StyledPageBody,\n} from 'uiSrc/components/base/layout/page/page-body.styles'\n\nconst PageBody = <T extends ComponentTypes = 'div'>({\n  component = 'div' as T,\n  className,\n  restrictWidth,\n  paddingSize,\n  style,\n  ...rest\n}: PageBodyProps<T>) => (\n  <StyledPageBody\n    as={component}\n    {...rest}\n    $restrictWidth={restrictWidth}\n    $paddingSize={paddingSize}\n    style={restrictWidthSize(style, restrictWidth)}\n    className={cx(PageClassNames.body, className)}\n  />\n)\n\nexport default PageBody\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/PageContentBody.tsx",
    "content": "import cx from 'classnames'\nimport React from 'react'\nimport {\n  PageClassNames,\n  PageContentBodyProps,\n  restrictWidthSize,\n  StyledPageContentBody,\n} from 'uiSrc/components/base/layout/page/page.styles'\n\nconst PageContentBody = ({\n  restrictWidth = false,\n  paddingSize = 'none',\n  style,\n  className,\n  ...rest\n}: PageContentBodyProps) => {\n  const classes = cx(PageClassNames.contentBody, className)\n\n  return (\n    <StyledPageContentBody\n      className={classes}\n      $paddingSize={paddingSize}\n      $restrictWidth={restrictWidth}\n      style={restrictWidthSize(style, restrictWidth)}\n      {...rest}\n    />\n  )\n}\n\nexport default PageContentBody\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/PageHeader.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { restrictWidthSize } from 'uiSrc/components/base/layout/page/page.styles'\nimport {\n  PageHeaderClassName,\n  PageHeaderProps,\n  StyledPageHeader,\n} from './page-heading.styles'\n\nconst PageHeader = ({\n  className,\n  style,\n  restrictWidth,\n  alignItems,\n  responsive = true,\n  bottomBorder,\n  paddingSize,\n  direction,\n  ...rest\n}: PageHeaderProps) => (\n  <StyledPageHeader\n    {...rest}\n    style={restrictWidthSize(style, restrictWidth)}\n    className={cx(className, PageHeaderClassName)}\n    $restrictWidth={restrictWidth}\n    $paddingSize={paddingSize}\n    $direction={direction}\n    $responsive={responsive}\n    $alignItems={alignItems}\n    $bottomBorder={bottomBorder}\n  />\n)\n\nexport default PageHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/index.ts",
    "content": "import Page from './Page'\nimport PageBody from './PageBody'\nimport PageHeader from './PageHeader'\nimport PageContentBody from './PageContentBody'\n\nexport { Page, PageBody, PageHeader, PageContentBody }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/page-body.spec.tsx",
    "content": "import React from 'react'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport { PADDING_SIZES } from './page.styles'\nimport PageBody from './PageBody'\n\ndescribe('PageBody', () => {\n  test('is rendered', () => {\n    const { container } = render(<PageBody />)\n\n    expect(container.firstChild).toBeTruthy()\n  })\n\n  describe('paddingSize', () => {\n    const sizes = {\n      none: '0',\n      s: '8px',\n      m: '16px',\n      l: '24px',\n    }\n    PADDING_SIZES.forEach((size) => {\n      it(`padding '${size}' is rendered`, () => {\n        const { container } = render(<PageBody paddingSize={size} />)\n        expect(container.firstChild).toHaveStyle(`padding: ${sizes[size]}`)\n      })\n    })\n  })\n\n  describe('restrict width', () => {\n    test('can be set to a default', () => {\n      const { container } = render(<PageBody restrictWidth />)\n\n      expect(container.firstChild).toHaveStyle('max-width: 1200px')\n    })\n\n    test('can be set to a custom number', () => {\n      const { container } = render(<PageBody restrictWidth={1024} />)\n\n      expect(container.firstChild).toHaveStyle('max-width: 1024px')\n    })\n\n    test('can be set to a custom value and measurement', () => {\n      const { container } = render(\n        <PageBody\n          restrictWidth=\"24rem\"\n          style={{\n            color: 'red ',\n          }}\n        />,\n      )\n\n      expect(container.firstChild).toHaveStyle('max-width: 24rem; color: red;')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/page-body.styles.ts",
    "content": "import { ComponentProps, ComponentType, PropsWithChildren } from 'react'\nimport styled from 'styled-components'\nimport {\n  PaddingSize,\n  pageStyles,\n} from 'uiSrc/components/base/layout/page/page.styles'\n\nexport type ComponentTypes = keyof JSX.IntrinsicElements | ComponentType<any>\nexport type PageBodyProps<T extends ComponentTypes = 'main'> =\n  PropsWithChildren &\n    ComponentProps<T> & {\n      className?: string\n      /**\n       * Sets the max-width of the page,\n       * set to `true` to use the default size of `1200px`,\n       * set to `false` to not restrict the width,\n       * set to a number for a custom width in px,\n       * set to a string for a custom width in custom measurement.\n       */\n      restrictWidth?: boolean | number | string\n      /**\n       * Sets the HTML element for `PageBody`.\n       */\n      component?: T\n      /**\n       * Adjusts the padding\n       */\n      paddingSize?: PaddingSize\n    }\n\ntype StyledPageBodyProps = Omit<\n  PageBodyProps,\n  'component' | 'paddingSize' | 'restrictWidth'\n> & {\n  $restrictWidth?: boolean | number | string\n  $paddingSize?: PaddingSize\n}\n\nexport const StyledPageBody = styled.main<StyledPageBodyProps>`\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  flex: 1 1 100%;\n  /* Make sure that inner flex layouts don't get larger than this container */\n  max-width: 100%;\n  min-width: 0;\n  ${({ $restrictWidth = false }) => $restrictWidth && pageStyles.restrictWidth}\n  ${({ $paddingSize = 'none' }) =>\n    $paddingSize && pageStyles.padding[$paddingSize]}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/page-heading.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled, { css } from 'styled-components'\nimport { PaddingSize } from 'uiSrc/components/base/layout/page/page.styles'\n\nexport const PageHeaderClassName = 'RI-page-header'\nexport const ALIGN_ITEMS = ['top', 'bottom', 'center', 'stretch'] as const\nexport type PageHeaderProps = HTMLAttributes<HTMLHeadingElement> & {\n  className?: string\n  /**\n   * Sets the max-width of the page,\n   * set to `true` to use the default size of `1200px`,\n   * set to `false` to not restrict the width,\n   * set to a number for a custom width in px,\n   * set to a string for a custom width in custom measurement.\n   */\n  restrictWidth?: boolean | number | string\n  /**\n   * Adjust the padding.\n   * When using this setting it's best to be consistent throughout all similar usages\n   */\n  paddingSize?: PaddingSize\n  /**\n   * Changes the `flex-direction` property.\n   * Flip to `column` when not including a sidebar.\n   */\n  direction?: 'row' | 'column'\n  /**\n   * Set to false if you don't want the children to stack at small screen sizes.\n   * Set to `reverse` to display the right side content first for the sack of hierarchy (like global time)\n   */\n  responsive?: boolean | 'reverse'\n  /**\n   * Vertical alignment of the left and right side content;\n   * Default is `middle` for custom content, but `top` for when `pageTitle` or `tabs` are included\n   */\n  alignItems?: (typeof ALIGN_ITEMS)[number]\n  /**\n   * defaults to false.\n   */\n  bottomBorder?: boolean\n}\n\nconst pageHeaderStyles = {\n  align: {\n    top: css`\n      align-items: flex-start;\n    `,\n    bottom: css`\n      align-items: flex-end;\n    `,\n    center: css`\n      align-items: center;\n    `,\n    stretch: css`\n      align-items: stretch;\n    `,\n  },\n  direction: {\n    row: css`\n      flex-direction: row;\n    `,\n    column: css`\n      flex-direction: column;\n    `,\n  },\n  border: css`\n    padding-bottom: 24px;\n    border-bottom: 1px solid #d3dae6;\n  `,\n  // Padding\n  padding: {\n    none: css`\n      padding: 0;\n    `,\n    // todo: use theme\n    s: css`\n      padding: 8px;\n    `,\n    m: css`\n      padding: 16px;\n    `,\n    l: css`\n      padding: 24px;\n    `,\n  },\n  responsive: css`\n    @media only screen and (min-width: 575px) and (max-width: 768px) {\n      flex-direction: column;\n    }\n  `,\n  responsiveReverse: css`\n    @media only screen and (min-width: 575px) and (max-width: 768px) {\n      flex-direction: column-reverse;\n    }\n  `,\n  restrictWidth: css`\n    margin-inline: auto;\n  `,\n}\ntype StyledPageHeaderProps = Omit<\n  PageHeaderProps,\n  | 'direction'\n  | 'responsive'\n  | 'alignItems'\n  | 'bottomBorder'\n  | 'paddingSize'\n  | 'restrictWidth'\n> & {\n  $restrictWidth?: boolean | number | string\n  $paddingSize?: PaddingSize\n  $direction?: 'row' | 'column'\n  $responsive?: boolean | 'reverse'\n  $alignItems?: (typeof ALIGN_ITEMS)[number]\n  $bottomBorder?: boolean\n}\nexport const StyledPageHeader = styled.header<StyledPageHeaderProps>`\n  width: 100%;\n  min-width: 0;\n  display: flex;\n  flex-shrink: 0;\n  justify-content: space-between;\n  ${({ $responsive = true }) => {\n    if (!$responsive) return ''\n    return $responsive === 'reverse'\n      ? pageHeaderStyles.responsiveReverse\n      : pageHeaderStyles.responsive\n  }}\n  ${({ $direction = 'row' }) => pageHeaderStyles.direction[$direction]}\n  ${({ $paddingSize = 'none' }) =>\n    $paddingSize && pageHeaderStyles.padding[$paddingSize]}\n  ${({ $alignItems = 'center' }) =>\n    $alignItems && pageHeaderStyles.align[$alignItems]}\n  ${({ $bottomBorder = false }) => $bottomBorder && pageHeaderStyles.border}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/page.spec.tsx",
    "content": "import React from 'react'\nimport { PADDING_SIZES } from 'uiSrc/components/base/layout/page/page.styles'\nimport Page from 'uiSrc/components/base/layout/page/Page'\nimport { render } from 'uiSrc/utils/test-utils'\n\ndescribe('RIPage', () => {\n  it('is rendered', () => {\n    const { container } = render(<Page />)\n\n    expect(container.firstChild).toBeTruthy()\n  })\n\n  describe('paddingSize', () => {\n    const sizes = {\n      none: '0',\n      s: '8px',\n      m: '16px',\n      l: '24px',\n    }\n    PADDING_SIZES.forEach((size) => {\n      it(`padding '${size}' is rendered`, () => {\n        const { container } = render(<Page paddingSize={size} />)\n        expect(container.firstChild).toHaveStyle(`padding: ${sizes[size]}`)\n      })\n    })\n  })\n\n  describe('grow', () => {\n    it(`grow 'true' gives flex-grow: 1`, () => {\n      const { container } = render(<Page grow />)\n\n      expect(container.firstChild).toHaveStyle('flex-grow: 1')\n    })\n    it(`grow 'false' does not render flex-grow`, () => {\n      const { container } = render(<Page grow={false} />)\n\n      expect(container.firstChild).not.toHaveStyle('flex-grow: 1')\n    })\n  })\n\n  describe('direction', () => {\n    it(`can be row`, () => {\n      const { container } = render(\n        <Page direction=\"row\" restrictWidth style={{ width: '1000px' }} />,\n      )\n\n      expect(container.firstChild).toHaveStyle('flex-direction: row')\n    })\n  })\n\n  describe('restrict width', () => {\n    it('can be set to a default', () => {\n      const { container } = render(<Page restrictWidth />)\n\n      expect(container.firstChild).toHaveStyle('max-width: 1200px')\n    })\n\n    it('can be set to a custom number', () => {\n      const { container } = render(<Page restrictWidth={1024} />)\n\n      expect(container.firstChild).toHaveStyle('max-width: 1024px')\n    })\n\n    it('can be set to a custom value and does not override custom style', () => {\n      const { container } = render(\n        <Page\n          restrictWidth=\"24rem\"\n          style={{\n            color: 'red ',\n          }}\n        />,\n      )\n\n      expect(container.firstChild).toHaveStyle('max-width: 24rem; color: red;')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/page/page.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { CSSProperties, HTMLAttributes } from 'react'\nimport { StyledPageHeader } from 'uiSrc/components/base/layout/page/page-heading.styles'\nimport { StyledPageBody } from 'uiSrc/components/base/layout/page/page-body.styles'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const PageClassNames = {\n  page: 'RI-page',\n  body: 'RI-page-body',\n  contentBody: 'RI-page-content-body',\n}\nexport const PADDING_SIZES = ['none', 's', 'm', 'l'] as const\nexport type PaddingSize = (typeof PADDING_SIZES)[number]\nexport const PAGE_MAX_WIDTH: CSSProperties['maxWidth'] = '1200px'\n\nexport type PageProps = HTMLAttributes<HTMLDivElement> & {\n  className?: string\n  /**\n   * Sets the max-width of the page,\n   * set to `true` to use the default size of `1200px`,\n   * set to `false` to not restrict the width,\n   * set to a number for a custom width in px,\n   * set to a string for a custom width in custom measurement.\n   */\n  restrictWidth?: boolean | number | string\n  /**\n   * Adjust the padding.\n   * When using this setting it's best to be consistent throughout all similar usages\n   */\n  paddingSize?: PaddingSize\n  /**\n   * Adds `flex-grow: 1` to the whole page for stretching to fit vertically.\n   * Must be wrapped inside a flexbox, preferably with `min-height: 100vh`\n   */\n  grow?: boolean\n  /**\n   * Changes the `flex-direction` property.\n   * Flip to `column` when not including a sidebar.\n   */\n  direction?: 'row' | 'column'\n}\n\n// Define the type for the padding object\ntype PaddingStyles = {\n  none: ReturnType<typeof css>\n  s: ReturnType<typeof css>\n  m: ReturnType<typeof css>\n  l: ReturnType<typeof css>\n}\n\n// Define the type for the pageStyles object\ntype PageStyles = {\n  grow: ReturnType<typeof css>\n  column: ReturnType<typeof css>\n  row: ReturnType<typeof css>\n  restrictWidth: ReturnType<typeof css>\n  padding: PaddingStyles\n}\n\nexport const pageStyles: PageStyles = {\n  // Grow\n  grow: css`\n    flex-grow: 1;\n  `,\n\n  // Direction\n  column: css`\n    flex-direction: column;\n  `,\n\n  row: css`\n    flex-direction: row;\n  `,\n\n  // Max widths\n  restrictWidth: css`\n    margin-inline: auto;\n  `,\n\n  // Padding\n  padding: {\n    none: css`\n      padding: 0;\n    `,\n    // todo: use theme\n    s: css`\n      padding: 8px;\n\n      ${StyledPageBody} {\n        & > ${StyledPageHeader} {\n          margin-bottom: 8px;\n        }\n      }\n    `,\n    m: css`\n      padding: 16px;\n      ${StyledPageBody} {\n        & > ${StyledPageHeader} {\n          margin-bottom: 16px;\n        }\n      }\n    `,\n    l: css`\n      padding: 24px;\n\n      ${StyledPageBody} {\n        & > ${StyledPageHeader} {\n          margin-bottom: 24px;\n        }\n      }\n    `,\n  },\n}\n\nexport const StyledPage = styled.div<\n  Omit<PageProps, 'grow' | 'direction' | 'restrictWidth' | 'paddingSize'> & {\n    $grow?: boolean\n    $direction?: 'row' | 'column'\n    $restrictWidth?: boolean | number | string\n    $paddingSize?: PaddingSize\n  }\n>`\n  display: flex;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral100};\n  /* Ensure Safari doesn't shrink height beyond contents */\n  flex-shrink: 0;\n  /* Ensure Firefox doesn't expand width beyond bounds */\n  max-width: 100%;\n  ${({ $grow = true }) => $grow && pageStyles.grow}\n  ${({ $restrictWidth = false }) => $restrictWidth && pageStyles.restrictWidth}\n  ${({ $direction = 'row' }) =>\n    $direction === 'column' ? pageStyles.column : pageStyles.row}\n  ${({ $paddingSize = 'none' }) =>\n    $paddingSize && pageStyles.padding[$paddingSize]}\n`\n\n/**\n * Returns a new style object with a maxWidth property set according to the\n * given `restrictWidth` argument.\n *\n * If `restrictWidth` is:\n * - `true`, sets `maxWidth` to `PAGE_MAX_WIDTH` (1200px).\n * - A string, sets `maxWidth` to that string.\n * - A number, sets `maxWidth` to that number followed by the string `'px'`.\n * - `false`, does nothing.\n *\n * If `style` is given, the new style object will have all of its properties,\n * and then the `maxWidth` property will be set according to the above rules.\n *\n * @param {React.CSSProperties} [style] The style object to modify.\n * @param {boolean | number | string} [restrictWidth] The value to set\n *   `maxWidth` to.\n * @return {React.CSSProperties} A new style object with the `maxWidth` property\n *   set according to the given `restrictWidth` argument.\n */\nexport function restrictWidthSize(\n  style: React.CSSProperties = {},\n  restrictWidth?: boolean | number | string,\n) {\n  const newStyle = { ...style }\n\n  if (restrictWidth === true) {\n    newStyle.maxWidth = PAGE_MAX_WIDTH\n  } else if (restrictWidth !== false) {\n    if (typeof restrictWidth === 'string') {\n      newStyle.maxWidth = restrictWidth\n    } else if (typeof restrictWidth === 'number') {\n      newStyle.maxWidth = `${restrictWidth}px`\n    }\n  }\n  return newStyle\n}\n\nexport type PageContentBodyProps = HTMLAttributes<HTMLDivElement> & {\n  className?: string\n  /**\n   * Sets the max-width of the page,\n   * set to `true` to use the default size of `1200px`,\n   * set to `false` to not restrict the width,\n   * set to a number for a custom width in px,\n   * set to a string for a custom width in custom measurement.\n   */\n  restrictWidth?: boolean | number | string\n  /**\n   * Adjust the padding.\n   * When using this setting it's best to be consistent throughout all similar usages\n   */\n  paddingSize?: PaddingSize\n}\ntype StyledPageContentBodyProps = Omit<\n  PageContentBodyProps,\n  'restrictWidth' | 'paddingSize'\n> & {\n  $restrictWidth?: boolean | number | string\n  $paddingSize?: PaddingSize\n}\nconst pageContentBodyStyles = {\n  restrictWidth: css`\n    margin-left: auto;\n    margin-right: auto;\n    width: 100%;\n  `,\n}\nexport const StyledPageContentBody = styled.div<StyledPageContentBodyProps>`\n  ${({ $restrictWidth = false }) =>\n    $restrictWidth && pageContentBodyStyles.restrictWidth}\n  ${({ $paddingSize = 'none' }) =>\n    $paddingSize && pageStyles.padding[$paddingSize]}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/profile-icon/ProfileIcon.tsx",
    "content": "export { ProfileIcon } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/resize/container/ResizableContainer.tsx",
    "content": "import React, { forwardRef } from 'react'\n\nimport {\n  ImperativePanelGroupHandle,\n  PanelGroup,\n  PanelGroupProps,\n} from 'react-resizable-panels'\n\nconst ResizableContainer = forwardRef<\n  ImperativePanelGroupHandle,\n  PanelGroupProps\n>((props, ref) => <PanelGroup ref={ref} {...props} />)\n\nexport default ResizableContainer\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/resize/handle/ResizablePanelHandle.tsx",
    "content": "import React from 'react'\nimport {\n  HandleContainer,\n  Line,\n  ResizablePanelHandleProps,\n  StyledPanelResizeHandle,\n} from './resizable-panel-handle.styles'\n\nconst ResizablePanelHandle = ({\n  className,\n  direction = 'vertical',\n  ...rest\n}: ResizablePanelHandleProps) => (\n  <StyledPanelResizeHandle\n    $direction={direction}\n    className={className}\n    {...rest}\n  >\n    <HandleContainer $direction={direction}>\n      <Line />\n      <Line />\n    </HandleContainer>\n  </StyledPanelResizeHandle>\n)\n\nexport default ResizablePanelHandle\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/resize/handle/resizable-panel-handle.styles.ts",
    "content": "import {\n  PanelResizeHandle,\n  PanelResizeHandleProps,\n} from 'react-resizable-panels'\nimport styled, { css } from 'styled-components'\n\nexport interface ResizablePanelHandleProps extends PanelResizeHandleProps {\n  direction?: 'horizontal' | 'vertical'\n}\n\nexport const StyledPanelResizeHandle = styled(PanelResizeHandle)<{\n  $direction: 'horizontal' | 'vertical'\n}>`\n  ${({ $direction }) =>\n    $direction === 'vertical'\n      ? css`\n          width: 16px;\n          height: 100%;\n          display: flex;\n          flex-direction: column;\n          justify-content: center;\n        `\n      : css`\n          height: 16px;\n          width: 100%;\n          display: flex;\n          flex-direction: row;\n          justify-content: center;\n        `}\n`\n\nexport const HandleContainer = styled.div<{\n  $direction: 'horizontal' | 'vertical'\n  children?: React.ReactNode\n}>`\n  width: 16px;\n  height: 16px;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  gap: 2px;\n\n  ${({ $direction }) =>\n    $direction === 'vertical' &&\n    css`\n      transform: rotate(90deg);\n    `}\n`\n\nexport const Line = styled.div`\n  width: 12px;\n  height: 1px;\n  background-color: #343741;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/resize/index.ts",
    "content": "export { ImperativePanelGroupHandle } from 'react-resizable-panels'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/resize/panel/ResizablePanel.tsx",
    "content": "import React from 'react'\n\nimport { Panel, PanelProps } from 'react-resizable-panels'\n\nconst ResizablePanel = (props: PanelProps) => <Panel {...props} />\n\nexport default ResizablePanel\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/sidebar/SideBarItemIcon.tsx",
    "content": "import React from 'react'\n\nimport { RiSideBarItemIconProps, StyledIcon } from './sidebar-item-icon.styles'\n\nexport const SideBarItemIcon = ({\n  centered,\n  ...props\n}: RiSideBarItemIconProps) => <StyledIcon {...props} $centered={centered} />\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/sidebar/index.ts",
    "content": "import { SideBar } from '@redis-ui/components'\nimport { SideBarItemIcon } from './SideBarItemIcon'\n\nconst SideBarHeader = SideBar.Header\nconst SideBarContainer = SideBar.ItemsContainer\nconst SideBarItem = SideBar.Item\nconst SideBarDivider = SideBar.Divider\nconst SideBarFooter = SideBar.Footer\n\nexport {\n  SideBar,\n  SideBarHeader,\n  SideBarContainer,\n  SideBarItem,\n  SideBarItemIcon,\n  SideBarDivider,\n  SideBarFooter,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/sidebar/sidebar-item-icon.styles.ts",
    "content": "import { SideBar } from '@redis-ui/components'\nimport styled from 'styled-components'\n\nexport type RiSideBarItemIconProps = Omit<\n  React.ComponentProps<typeof SideBar.Item.Icon>,\n  'width' | 'height'\n> & {\n  width?: string\n  height?: string\n  centered?: boolean\n}\n\nexport const StyledIcon = styled(SideBar.Item.Icon)<\n  RiSideBarItemIconProps & {\n    $centered?: boolean\n  }\n>`\n  ${({ width = 'inherit' }) => `\n    width: ${width};\n  `}\n  ${({ height = 'inherit' }) => `\n    height: ${height};\n  `}\n  ${({ $centered }) =>\n    $centered &&\n    `\n    justify-content: center;\n    align-items: center;\n  `}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/spacer/index.ts",
    "content": "export { Spacer } from './spacer'\nexport type { SpacerSize } from './spacer.styles'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/spacer/spacer.spec.tsx",
    "content": "import React from 'react'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Spacer } from './spacer'\nimport { SpacerProps } from './spacer.styles'\n\nconst sizeToValue = {\n  xs: '0.2rem',\n  s: '0.4rem',\n  m: '0.8rem',\n  l: '1.6rem',\n  xl: '2rem',\n  xxl: '2.4rem',\n} as const\n\ndescribe('Spacer', () => {\n  test('is rendered', () => {\n    const { container } = render(<Spacer />)\n\n    expect(container.firstChild).toBeTruthy()\n  })\n\n  describe('Size', () => {\n    Object.entries(sizeToValue).forEach(([size, value]) => {\n      it(`size '${size}' is rendered`, () => {\n        const { container } = render(\n          <Spacer size={size as SpacerProps['size']} />,\n        )\n        expect(container.firstChild).toHaveStyle(`height: ${value}`)\n      })\n    })\n  })\n\n  describe('Direction', () => {\n    it(`width is rendered for Horizontal direction`, () => {\n      const { container } = render(<Spacer />)\n      expect(container.firstChild).toHaveStyle(`height: 1.6rem`)\n      expect(container.firstChild).not.toHaveStyle(`width: 1.6rem`)\n    })\n    it(`width is rendered for explicit Horizontal direction`, () => {\n      const { container } = render(<Spacer direction=\"horizontal\" />)\n      expect(container.firstChild).toHaveStyle(`width: 1.6rem`)\n      expect(container.firstChild).not.toHaveStyle(`height: 1.6rem`)\n    })\n    it(`height is rendered for Vertical direction`, () => {\n      const { container } = render(<Spacer direction=\"vertical\" />)\n      expect(container.firstChild).toHaveStyle('height: 1.6rem')\n      expect(container.firstChild).not.toHaveStyle(`width: 1.6rem`)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/spacer/spacer.styles.ts",
    "content": "import { HTMLAttributes, ReactNode } from 'react'\nimport styled, { css } from 'styled-components'\nimport { CommonProps, Theme } from 'uiSrc/components/base/theme/types'\n\nexport type SpacerSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl'\n\n// Extract only the spaceXXX keys from the theme\nexport type ThemeSpacingKey = Extract<\n  keyof Theme['core']['space'],\n  `space${string}`\n>\n\nexport type SpacerProps = CommonProps &\n  HTMLAttributes<HTMLDivElement> & {\n    children?: ReactNode\n    size?: SpacerSize | ThemeSpacingKey | string\n    direction?: 'horizontal' | 'vertical'\n  }\n\nexport const spacerStyles: Record<SpacerSize, ReturnType<typeof css>> = {\n  xs: css`\n    ${({ theme }: { theme: Theme }) => theme.core.space.space025}\n  `,\n  s: css`\n    ${({ theme }: { theme: Theme }) => theme.core.space.space050}\n  `,\n  m: css`\n    ${({ theme }: { theme: Theme }) => theme.core.space.space100}\n  `,\n  l: css`\n    ${({ theme }: { theme: Theme }) => theme.core.space.space200}\n  `,\n  // @see redisinsight/ui/src/styles/base/_base.scss:124\n  xl: css`\n    ${({ theme }: { theme: Theme }) => theme.core.space.space250}\n  `,\n  xxl: css`\n    ${({ theme }: { theme: Theme }) => theme.core.space.space300}\n  `,\n}\n\nconst getSpacingValue = (\n  size: SpacerSize | ThemeSpacingKey | string,\n  theme: Theme,\n): string | ReturnType<typeof css> => {\n  // Check if it's a theme spacing key\n  if (size in theme.core.space) {\n    return theme.core.space[size as ThemeSpacingKey]\n  }\n\n  // Check if it's a legacy spacer size\n  if (size in spacerStyles) {\n    return spacerStyles[size as SpacerSize]\n  }\n\n  // Custom string value (e.g., '7.2rem', '72px')\n  return size\n}\n\ntype StyledSpacerType = Omit<SpacerProps, 'direction'> & {\n  $direction: SpacerProps['direction']\n}\n\nexport const StyledSpacer = styled.div<StyledSpacerType>`\n  flex-shrink: 0;\n  ${({ $direction = 'vertical', size = 'l', theme }) => {\n    const spacingValue = getSpacingValue(size, theme)\n\n    return $direction === 'horizontal'\n      ? css`\n          width: ${spacingValue};\n        `\n      : css`\n          height: ${spacingValue};\n        `\n  }}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/spacer/spacer.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { SpacerProps, StyledSpacer } from './spacer.styles'\n\n/**\n * A simple spacer component that can be used to add vertical spacing between\n * other components. The size of the spacer can be specified using the `size`\n * prop, which can be one of the following values:\n *   - Legacy sizes: 'xs' = 4px, 's' = 8px, 'm' = 12px, 'l' = 16px, 'xl' = 24px, 'xxl' = 32px\n *   - Theme spacing sizes: Any key from theme.semantic.core.space (e.g., 'space000', 'space010',\n *     'space025', 'space050', 'space100', 'space150', 'space200', 'space250', 'space300',\n *     'space400', 'space500', 'space550', 'space600', 'space800', etc.)\n *   - Custom CSS value: Any valid CSS size string (e.g., '7.2rem', '72px')\n *\n *   The theme spacing tokens are dynamically extracted from the theme, ensuring consistency\n *   and automatic updates when the theme changes.\n *\n *   The default value for `size` is 'l'.\n */\nexport const Spacer = ({\n  className,\n  children,\n  direction,\n  ...rest\n}: SpacerProps) => {\n  return (\n    <StyledSpacer\n      {...rest}\n      className={cx('RI-spacer', className)}\n      $direction={direction}\n    >\n      {children}\n    </StyledSpacer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/stepper/index.ts",
    "content": "export { Stepper } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/table/index.ts",
    "content": "export * from '@redis-ui/table'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/layout/tabs/index.ts",
    "content": "import { Tabs, TabInfo } from '@redis-ui/components'\n\nexport default Tabs\nexport type { TabInfo }\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/link/Link.tsx",
    "content": "import React from 'react'\nimport * as S from 'uiSrc/components/base/link/link.styles'\nimport { type RiLinkProps } from './link.types'\n\nexport const Link = ({ color, underline, ...props }: RiLinkProps) => (\n  <S.StyledLink {...props} $color={color} $underline={underline} />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/link/index.ts",
    "content": "export { type RiLinkProps } from './link.types'\nexport { Link } from './Link'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/link/link.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { Link as RedisUiLink } from '@redis-ui/components'\nimport { useTheme } from '@redis-ui/styles'\nimport { ColorType, MapProps } from './link.types'\n\nconst useColorTextStyles = ({ $color }: MapProps = {}) => {\n  const theme = useTheme()\n  const colors = theme.semantic.color\n  const textColors = theme.components.typography.colors\n\n  const getColorValue = (color?: ColorType) => {\n    if (!color) {\n      return textColors.primary\n    }\n    switch (color) {\n      case 'inherit':\n        return 'inherit'\n      case 'default':\n      case 'primary':\n        return textColors.primary\n      case 'text':\n        return textColors.secondary\n      case 'subdued':\n        return colors.text.informative400\n      case 'danger':\n        return colors.text.danger600\n      case 'ghost':\n        return colors.text.neutral600\n      case 'accent':\n        return colors.text.notice600\n      case 'warning':\n        return colors.text.attention600\n      case 'success':\n        return colors.text.success600\n      default:\n        return color // any supported color value e.g #fff\n    }\n  }\n\n  return css`\n    color: ${getColorValue($color)};\n  `\n}\n\nexport const StyledLink = styled(RedisUiLink)<MapProps>`\n  ${useColorTextStyles};\n  text-decoration: ${({ $underline }) =>\n    $underline ? 'underline' : 'none'} !important;\n  & > span {\n    text-decoration: ${({ $underline }) =>\n      $underline ? 'underline' : 'none'} !important;\n  }\n  &:hover {\n    text-decoration: underline !important;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/link/link.types.ts",
    "content": "import { LinkProps } from '@redis-ui/components'\n\n// TODO [DA]: Export the color functionality and use both for Link and Text\nexport type EuiColorNames =\n  | 'inherit'\n  | 'default'\n  | 'primary'\n  | 'text'\n  | 'subdued'\n  | 'danger'\n  | 'ghost'\n  | 'accent'\n  | 'warning'\n  | 'success'\n\nexport type ColorType = LinkProps['color'] | EuiColorNames | (string & {})\n\nexport type RiLinkProps = Omit<LinkProps, 'color'> & {\n  color?: ColorType\n  underline?: boolean\n}\n\nexport type MapProps = RiLinkProps & {\n  $color?: ColorType\n  $underline?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/navigation/breadcrumbs/RiBreadcrumbs.tsx",
    "content": "export { Breadcrumbs } from '@redis-ui/components'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/navigation/breadcrumbs/index.ts",
    "content": "export { Breadcrumbs } from './RiBreadcrumbs'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/popover/RiPopover.constants.ts",
    "content": "export const ANCHOR_POSITION_MAP = {\n  upCenter: {\n    placement: 'top',\n    align: 'center',\n  },\n  upLeft: {\n    placement: 'top',\n    align: 'start',\n  },\n  upRight: {\n    placement: 'top',\n    align: 'end',\n  },\n  downCenter: {\n    placement: 'bottom',\n    align: 'center',\n  },\n  downLeft: {\n    placement: 'bottom',\n    align: 'start',\n  },\n  downRight: {\n    placement: 'bottom',\n    align: 'end',\n  },\n  leftCenter: {\n    placement: 'left',\n    align: 'center',\n  },\n  leftUp: {\n    placement: 'left',\n    align: 'start',\n  },\n  leftDown: {\n    placement: 'left',\n    align: 'end',\n  },\n  rightCenter: {\n    placement: 'right',\n    align: 'center',\n  },\n  rightUp: {\n    placement: 'right',\n    align: 'start',\n  },\n  rightDown: {\n    placement: 'right',\n    align: 'end',\n  },\n} as const\n\nexport const PANEL_PADDING_SIZE_MAP = {\n  l: 24,\n  m: 18,\n  s: 8,\n  none: 0,\n} as const\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/popover/RiPopover.spec.tsx",
    "content": "import React from 'react'\nimport { render, waitForRiPopoverVisible, screen } from 'uiSrc/utils/test-utils'\nimport { RiPopover } from './RiPopover'\nimport { RiPopoverProps } from './RiPopover.types'\n\nconst TestButton = () => (\n  <button type=\"button\" data-testid=\"popover-trigger\">\n    Click me\n  </button>\n)\n\nconst renderPopover = (overrides: Partial<RiPopoverProps> = {}) => {\n  return render(\n    <RiPopover\n      button={<TestButton />}\n      isOpen={false}\n      closePopover={jest.fn()}\n      {...overrides}\n    >\n      <div data-testid=\"popover-content\">Popover content</div>\n    </RiPopover>,\n  )\n}\n\ndescribe('RiPopover', () => {\n  it('should render', () => {\n    expect(renderPopover()).toBeTruthy()\n  })\n\n  it('should render trigger button', () => {\n    renderPopover()\n\n    expect(screen.getByTestId('popover-trigger')).toBeInTheDocument()\n  })\n\n  it('should render popover content when isOpen is true', async () => {\n    renderPopover({ isOpen: true })\n\n    await waitForRiPopoverVisible()\n\n    expect(screen.getByTestId('popover-content')).toBeInTheDocument()\n  })\n\n  it('should not render popover content when isOpen is false', () => {\n    renderPopover({ isOpen: false })\n\n    expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument()\n  })\n\n  describe('button prop (legacy)', () => {\n    it('should wrap button in span by default', () => {\n      renderPopover()\n\n      const trigger = screen.getByTestId('popover-trigger')\n      const wrapper = trigger.parentElement\n\n      expect(wrapper?.tagName).toBe('SPAN')\n    })\n\n    it('should apply anchorClassName to wrapper span', () => {\n      renderPopover({ anchorClassName: 'custom-anchor-class' })\n\n      const trigger = screen.getByTestId('popover-trigger')\n      const wrapper = trigger.parentElement\n\n      expect(wrapper).toHaveClass('custom-anchor-class')\n    })\n  })\n\n  describe('trigger prop (new)', () => {\n    it('should use trigger when provided', () => {\n      renderPopover({\n        trigger: <button data-testid=\"new-trigger\">New Trigger</button>,\n      })\n\n      expect(screen.getByTestId('new-trigger')).toBeInTheDocument()\n      expect(screen.queryByTestId('popover-trigger')).not.toBeInTheDocument()\n    })\n\n    it('should wrap trigger in span by default (standalone=false)', () => {\n      renderPopover({\n        trigger: <button data-testid=\"new-trigger\">New Trigger</button>,\n      })\n\n      const trigger = screen.getByTestId('new-trigger')\n      const wrapper = trigger.parentElement\n\n      expect(wrapper?.tagName).toBe('SPAN')\n    })\n\n    it('should render trigger directly when standalone is true', () => {\n      renderPopover({\n        trigger: <div data-testid=\"standalone-trigger\">Standalone</div>,\n        standalone: true,\n      })\n\n      const trigger = screen.getByTestId('standalone-trigger')\n      const wrapper = trigger.parentElement\n\n      // Should not be wrapped in span\n      expect(wrapper?.tagName).not.toBe('SPAN')\n      expect(wrapper?.tagName).toBe('DIV')\n    })\n\n    it('should apply anchorClassName to wrapper when standalone is false', () => {\n      renderPopover({\n        trigger: <button data-testid=\"new-trigger\">New Trigger</button>,\n        anchorClassName: 'custom-anchor-class',\n      })\n\n      const trigger = screen.getByTestId('new-trigger')\n      const wrapper = trigger.parentElement\n\n      expect(wrapper).toHaveClass('custom-anchor-class')\n    })\n\n    it('should not apply anchorClassName when standalone is true', () => {\n      renderPopover({\n        trigger: <div data-testid=\"standalone-trigger\">Standalone</div>,\n        standalone: true,\n        anchorClassName: 'custom-anchor-class',\n      })\n\n      const trigger = screen.getByTestId('standalone-trigger')\n\n      // anchorClassName should not be applied since there's no wrapper\n      expect(trigger).not.toHaveClass('custom-anchor-class')\n    })\n  })\n\n  describe('prop conflicts and warnings', () => {\n    it('should warn when both button and trigger are provided', () => {\n      const consoleWarnSpy = jest.spyOn(console, 'warn')\n\n      renderPopover({ trigger: <button>Trigger</button> })\n\n      expect(consoleWarnSpy).toHaveBeenCalledWith(\n        \"[RiPopover]: Both 'button' and 'trigger' props are provided. Using 'trigger'. Please migrate to 'trigger' prop.\",\n      )\n    })\n\n    it('should warn when both panelClassName and className are provided', () => {\n      const consoleWarnSpy = jest.spyOn(console, 'warn')\n\n      renderPopover({\n        panelClassName: 'old-class',\n        className: 'new-class',\n      })\n\n      expect(consoleWarnSpy).toHaveBeenCalledWith(\n        \"[RiPopover]: Both 'panelClassName' and 'className' props are provided. Using 'className'. Please migrate to 'className' prop.\",\n      )\n    })\n  })\n\n  describe('className prop', () => {\n    it('should use className when provided', async () => {\n      renderPopover({\n        isOpen: true,\n        className: 'custom-class',\n      })\n\n      await waitForRiPopoverVisible()\n\n      const popover = screen.queryByRole('dialog')\n      expect(popover).toBeInTheDocument()\n      expect(popover).toHaveClass('custom-class')\n    })\n\n    it('should fall back to panelClassName when className is not provided', async () => {\n      renderPopover({\n        isOpen: true,\n        panelClassName: 'fallback-class',\n      })\n\n      await waitForRiPopoverVisible()\n\n      const popover = screen.queryByRole('dialog')\n      expect(popover).toBeInTheDocument()\n      expect(popover).toHaveClass('fallback-class')\n    })\n\n    it('should prefer className over panelClassName when both are provided', async () => {\n      renderPopover({\n        isOpen: true,\n        panelClassName: 'old-class',\n        className: 'new-class',\n      })\n\n      await waitForRiPopoverVisible()\n\n      const popover = screen.queryByRole('dialog')\n\n      expect(popover).toBeInTheDocument()\n      expect(popover).toHaveClass('new-class')\n      expect(popover).not.toHaveClass('old-class')\n    })\n  })\n\n  describe('panelPaddingSize', () => {\n    it('should apply padding style based on panelPaddingSize', async () => {\n      renderPopover({\n        isOpen: true,\n        panelPaddingSize: 'm',\n      })\n\n      await waitForRiPopoverVisible()\n\n      const popover = screen.queryByRole('dialog')\n\n      expect(popover).toBeInTheDocument()\n      expect(popover).toHaveStyle({ padding: '18px' })\n    })\n\n    it('should apply no padding when panelPaddingSize is none', async () => {\n      renderPopover({\n        isOpen: true,\n        panelPaddingSize: 'none',\n      })\n\n      await waitForRiPopoverVisible()\n\n      const popover = screen.queryByRole('dialog')\n\n      expect(popover).toBeInTheDocument()\n      expect(popover).toHaveStyle({ padding: '0px' })\n    })\n  })\n\n  describe('scalar trigger values', () => {\n    it('should wrap string trigger in span', () => {\n      const { container } = renderPopover({ trigger: 'String trigger' })\n\n      const text = screen.getByText('String trigger')\n      // The Popover component might wrap our span in a div, so check if span exists\n      const span = container.querySelector('span')\n      expect(span).toBeInTheDocument()\n      expect(span).toContainElement(text)\n    })\n\n    it('should wrap number trigger in span', () => {\n      const { container } = renderPopover({ trigger: 123 })\n\n      const text = screen.getByText('123')\n      // The Popover component might wrap our span in a div, so check if span exists\n      const span = container.querySelector('span')\n      expect(span).toBeInTheDocument()\n      expect(span).toContainElement(text)\n    })\n\n    it('should wrap scalar trigger in span when standalone is true', () => {\n      // When standalone is true and trigger is a scalar (string, number, etc.),\n      // we wrap it in a span because RadixPopover.Trigger with asChild requires a React element\n      const { container } = renderPopover({\n        trigger: 'String trigger',\n        standalone: true,\n      })\n\n      const text = screen.getByText('String trigger')\n      // Should be wrapped in a span (without anchorClassName)\n      const span = container.querySelector('span')\n      expect(span).toBeInTheDocument()\n      expect(span).toContainElement(text)\n      // The span should not have anchorClassName when standalone is true\n      expect(span).not.toHaveClass()\n    })\n  })\n\n  describe('backwards compatibility', () => {\n    it('should work with button prop only (legacy behavior)', () => {\n      renderPopover()\n\n      expect(screen.getByTestId('popover-trigger')).toBeInTheDocument()\n    })\n\n    it('should work with panelClassName prop only (legacy behavior)', async () => {\n      const { getByRole } = renderPopover({\n        isOpen: true,\n        panelClassName: 'legacy-class',\n      })\n\n      await waitForRiPopoverVisible()\n\n      const popover = getByRole('dialog')\n      expect(popover).toBeInTheDocument()\n      expect(popover).toHaveClass('legacy-class')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/popover/RiPopover.tsx",
    "content": "import React from 'react'\nimport { Popover } from '@redis-ui/components'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\nimport { type RiPopoverProps } from './RiPopover.types'\nimport {\n  ANCHOR_POSITION_MAP,\n  PANEL_PADDING_SIZE_MAP,\n} from './RiPopover.constants'\n\nexport const RiPopover = ({\n  isOpen,\n  closePopover,\n  children,\n  ownFocus,\n  button,\n  trigger,\n  anchorPosition,\n  panelPaddingSize,\n  anchorClassName,\n  panelClassName,\n  className,\n  maxWidth = '100%',\n  standalone = false,\n  persistent,\n  customOutsideDetector,\n  ...props\n}: RiPopoverProps) => {\n  // Warn if both button and trigger are provided\n  if (button !== undefined && trigger !== undefined) {\n    console.warn(\n      \"[RiPopover]: Both 'button' and 'trigger' props are provided. Using 'trigger'. Please migrate to 'trigger' prop.\",\n    )\n  }\n\n  // Warn if both panelClassName and className are provided\n  if (panelClassName !== undefined && className !== undefined) {\n    console.warn(\n      \"[RiPopover]: Both 'panelClassName' and 'className' props are provided. Using 'className'. Please migrate to 'className' prop.\",\n    )\n  }\n\n  // Determine which trigger to use\n  const activeTrigger = trigger ?? button\n\n  // Determine which className to use\n  const activeClassName = className ?? panelClassName\n\n  // Render trigger element\n  // If standalone is true, the trigger will be standalone and will not be wrapped in a span\n  // for this to work properly, either base trigger element is `div`, `span` etc. (base dom element)\n  // or a component that forwards ref\n  // However, if standalone is true and trigger is a scalar (string, number, etc.),\n  // we need to wrap it in a span because RadixPopover.Trigger with asChild requires a React element\n  let triggerElement: React.ReactNode\n\n  if (standalone) {\n    if (React.isValidElement(activeTrigger)) {\n      triggerElement = activeTrigger\n    } else {\n      // Wrap scalar values in span for asChild compatibility\n      triggerElement = <span>{activeTrigger}</span>\n    }\n  } else {\n    // Always wrap in span with anchorClassName for backwards compatibility\n    triggerElement = <span className={anchorClassName}>{activeTrigger}</span>\n  }\n\n  const placement =\n    anchorPosition && ANCHOR_POSITION_MAP[anchorPosition]?.placement\n  const align = anchorPosition && ANCHOR_POSITION_MAP[anchorPosition]?.align\n  // TODO: maybe use wrapped popover instead of inline style?!\n  const padding = panelPaddingSize && PANEL_PADDING_SIZE_MAP[panelPaddingSize]\n  return (\n    <Popover\n      {...props}\n      open={isOpen}\n      onClickOutside={customOutsideDetector ? undefined : closePopover}\n      onKeyDown={(event) => {\n        // Close on escape press\n        if (event.key === keys.ESCAPE) {\n          closePopover?.(event as any)\n        }\n      }}\n      persistent={persistent}\n      content={\n        children && customOutsideDetector ? (\n          <OutsideClickDetector\n            onOutsideClick={(event) => closePopover?.(event as any)}\n          >\n            {children as JSX.Element}\n          </OutsideClickDetector>\n        ) : (\n          children\n        )\n      }\n      // Props passed to the children wrapper:\n      className={activeClassName}\n      maxWidth={maxWidth}\n      style={{\n        padding,\n      }}\n      autoFocus={ownFocus}\n      placement={placement}\n      align={align}\n    >\n      {triggerElement}\n    </Popover>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/popover/RiPopover.types.ts",
    "content": "import { type PopoverProps } from '@redis-ui/components'\n\nimport { ReactNode } from 'react'\nimport {\n  ANCHOR_POSITION_MAP,\n  PANEL_PADDING_SIZE_MAP,\n} from './RiPopover.constants'\n\ntype PanelPaddingSize = keyof typeof PANEL_PADDING_SIZE_MAP\n\nexport type AnchorPosition = keyof typeof ANCHOR_POSITION_MAP\n\nexport type RiPopoverProps = Omit<\n  PopoverProps,\n  | 'open'\n  | 'onClickOutside'\n  | 'autoFocus'\n  | 'content'\n  | 'className'\n  | 'placement'\n  | 'align'\n> & {\n  isOpen?: PopoverProps['open']\n  closePopover?: PopoverProps['onClickOutside']\n  ownFocus?: PopoverProps['autoFocus']\n  /** @deprecated old prop for popover trigger element, use {@linkcode trigger} */\n  button?: PopoverProps['content']\n  /** preferred prop for popover trigger element (optional) */\n  trigger?: ReactNode\n  anchorPosition?: AnchorPosition\n  panelPaddingSize?: PanelPaddingSize\n  anchorClassName?: string\n  /** @deprecated - use {@linkcode className} - this is popover content wrapper class name */\n  panelClassName?: string\n  /** new preferred prop for popover content wrapper class name (optional) */\n  className?: string\n  'data-testid'?: string\n  /** if true, the trigger will be standalone and will not be wrapped in a span */\n  standalone?: boolean\n  customOutsideDetector?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/popover/index.ts",
    "content": "export * from './RiPopover'\nexport * from './RiPopover.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/shared/WindowControlGroup.tsx",
    "content": "import React from 'react'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon, MinusIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\n\ntype Props = {\n  onClose: () => void\n  onHide: () => void\n  id?: string\n  label?: string\n  closeContent?: string\n  hideContent?: string\n}\nexport const WindowControlGroup = ({\n  onClose,\n  onHide,\n  id,\n  label,\n  closeContent = 'Close',\n  hideContent = 'Minimize',\n}: Props) => (\n  <Row gap=\"m\" justify=\"end\">\n    <FlexItem>\n      <RiTooltip\n        content={hideContent}\n        position=\"top\"\n        anchorClassName=\"flex-row\"\n      >\n        <IconButton\n          size=\"S\"\n          icon={MinusIcon}\n          id={`hide-${id}`}\n          aria-label={`hide ${label || id || ''}`}\n          data-testid={`hide-${id}`}\n          onClick={onHide}\n        />\n      </RiTooltip>\n    </FlexItem>\n    <FlexItem>\n      <RiTooltip\n        content={closeContent}\n        position=\"top\"\n        anchorClassName=\"flex-row\"\n      >\n        <IconButton\n          size=\"S\"\n          icon={CancelSlimIcon}\n          id={`close-${id}`}\n          aria-label={`close ${label || id || ''}`}\n          data-testid={`close-${id}`}\n          onClick={onClose}\n        />\n      </RiTooltip>\n    </FlexItem>\n  </Row>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/ColorText.tsx",
    "content": "import React from 'react'\nimport cn from 'classnames'\nimport { ColorTextProps, mapSize, StyledColorText } from './text.styles'\n\nexport const ColorText = ({\n  color,\n  component = 'span',\n  className,\n  size,\n  ...rest\n}: ColorTextProps) => (\n  <StyledColorText\n    {...rest}\n    size={mapSize(size)}\n    component={component}\n    $color={color}\n    className={cn(className, { [`color__${color}`]: !!color }, 'RI-color-text')}\n  />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/HealthText.tsx",
    "content": "import React from 'react'\nimport { Typography } from '@redis-ui/components'\nimport cn from 'classnames'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { BodyProps, Indicator } from 'uiSrc/components/base/text/text.styles'\n\ntype ColorType = BodyProps['color'] | (string & {})\nexport type HealthProps = Omit<BodyProps, 'color'> & {\n  color?: ColorType\n}\n\nexport const HealthText = ({\n  color,\n  size = 'S',\n  className,\n  ...rest\n}: HealthProps) => (\n  <Row align=\"center\" gap=\"m\" justify=\"start\">\n    <Indicator\n      $color={color}\n      className={cn(`color__${color}`, 'RI-health-indicator')}\n    />\n    <Typography.Body\n      {...rest}\n      component=\"div\"\n      size={size}\n      className={cn(className, 'RI-health-text')}\n    />\n  </Row>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/MultilineEllipsisText.tsx",
    "content": "import styled from 'styled-components'\n\nimport { ColorText } from './ColorText'\nimport { theme } from '@redis-ui/styles'\n\nexport const lineHeightSizes = ['xs', 's', 'm', 'l'] as const\nexport type LineHeightType = (typeof lineHeightSizes)[number]\n\nexport const paddingBlockSizes = ['none', 'xs', 's', 'm'] as const\nexport type PaddingBlockType = (typeof paddingBlockSizes)[number]\n\nconst multiLineEllipsisStyles = {\n  lineHeight: {\n    xs: theme.core.space.space100,\n    s: theme.core.space.space150,\n    m: theme.core.space.space200,\n    l: theme.core.space.space250,\n  },\n  paddingBlock: {\n    none: theme.core.space.space000,\n    xs: theme.core.space.space010,\n    s: theme.core.space.space025,\n    m: theme.core.space.space050,\n  },\n}\n\nconst MultilineEllipsisText = styled(ColorText)<{\n  lineCount?: number\n  lineHeight?: LineHeightType\n  paddingBlock?: PaddingBlockType\n}>`\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: ${({ lineCount = 5 }) => lineCount};\n  line-height: ${({ lineHeight = 'l' }) =>\n    multiLineEllipsisStyles.lineHeight[lineHeight]};\n  text-overflow: ellipsis;\n  overflow: hidden;\n  padding-block: ${({ paddingBlock = 'none' }) =>\n    multiLineEllipsisStyles.paddingBlock[paddingBlock]};\n`\n\nexport default MultilineEllipsisText\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/Text.tsx",
    "content": "import React from 'react'\nimport cn from 'classnames'\nimport { mapSize, StyledText, TextProps } from './text.styles'\n\nexport const Text = ({\n  className,\n  color,\n  size,\n  textAlign,\n  ...rest\n}: TextProps) => {\n  return (\n    <StyledText\n      {...rest}\n      className={cn(className, 'RI-text')}\n      $color={color}\n      $align={textAlign}\n      size={mapSize(size)}\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/Title.tsx",
    "content": "import React from 'react'\nimport { Typography } from '@redis-ui/components'\n\nexport type TitleProps = React.ComponentProps<typeof Typography.Heading> & {}\nexport type TitleSize = TitleProps['size']\nexport const Title = (props: TitleProps) => <Typography.Heading {...props} />\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/index.ts",
    "content": "export { Text } from './Text'\nexport { ColorText } from './ColorText'\nexport { HealthText } from './HealthText'\nexport { Title } from './Title'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/text/text.styles.ts",
    "content": "import React, { HTMLAttributes } from 'react'\nimport { useTheme } from '@redis-ui/styles'\nimport { Typography } from '@redis-ui/components'\nimport styled, { css } from 'styled-components'\nimport { CommonProps } from 'uiSrc/components/base/theme/types'\n\nexport type BodyProps = React.ComponentProps<typeof Typography.Body>\n\nexport type EuiColorNames =\n  | 'default'\n  | 'subdued'\n  | 'danger'\n  | 'ghost'\n  | 'accent'\n  | 'warning'\n  | 'success'\n\nexport type ColorType = BodyProps['color'] | EuiColorNames | (string & {})\nexport interface MapProps extends HTMLAttributes<HTMLElement> {\n  $color?: ColorType\n  $align?: 'left' | 'center' | 'right'\n}\ntype BodySizesLowerCaseType = 'm' | 's' | 'xs'\ntype TextSizeType = BodyProps['size'] | BodySizesLowerCaseType\n\nexport type ColorTextProps = Omit<BodyProps, 'color' | 'size' | 'component'> & {\n  color?: ColorType\n  size?: TextSizeType\n  component?: 'div' | 'span'\n}\n\nexport type TextProps = Omit<BodyProps, 'color' | 'size'> &\n  CommonProps & {\n    color?: ColorType\n    size?: TextSizeType\n    textAlign?: 'left' | 'center' | 'right'\n  }\n\nexport const useColorTextStyles = ({ $color }: MapProps = {}) => {\n  const theme = useTheme()\n  const colors = theme.semantic.color\n  // @ts-ignore\n  const typographyColors = theme.components.typography.colors as Record<\n    'primary' | 'secondary',\n    string\n  >\n  const getColorValue = (color?: ColorType) => {\n    if (!color) {\n      return 'inherit'\n    }\n    switch (color) {\n      case 'default':\n      case 'primary':\n        return typographyColors?.primary || colors.text.neutral800\n      case 'secondary':\n        return typographyColors?.secondary || colors.text.neutral700\n      case 'subdued':\n        return colors.text.informative400\n      case 'danger':\n        return colors.text.danger600\n      case 'ghost':\n        return colors.text.neutral600\n      case 'accent':\n        return colors.text.notice600\n      case 'warning':\n        return colors.text.attention600\n      case 'success':\n        return colors.text.success600\n      default:\n        return color // any supported color value e.g #fff\n    }\n  }\n\n  return css`\n    color: ${getColorValue($color)};\n  `\n}\n\nconst getAlignValue = (align?: MapProps['$align']) => {\n  switch (align) {\n    case 'left':\n      return 'text-align: left'\n    case 'center':\n      return 'text-align: center'\n    case 'right':\n      return 'text-align: right'\n    default:\n      return ''\n  }\n}\n\nexport const StyledColorText = styled(Typography.Body)<MapProps>`\n  ${useColorTextStyles}\n`\n\nexport const StyledText = styled(Typography.Body)<MapProps>`\n  ${useColorTextStyles};\n  ${({ $align }) => getAlignValue($align)};\n`\n\nconst useStatusColorStyles = ({ $color }: MapProps = {}) => {\n  const theme = useTheme()\n  const colors = theme.semantic.color\n\n  const getColorValue = (color?: ColorType) => {\n    switch (color) {\n      case 'informative':\n        return colors.text.informative400\n      case 'danger':\n        return colors.text.danger500\n      case 'warning':\n        return colors.text.attention500\n      case 'success':\n        return colors.text.success500\n      default:\n        return color // any supported color value e.g #fff\n    }\n  }\n\n  return css`\n    background-color: ${getColorValue($color)};\n  `\n}\n\nexport const Indicator = styled.div<\n  {\n    $color: ColorType\n  } & CommonProps\n>`\n  width: 0.8rem;\n  height: 0.8rem;\n  border-radius: 50%;\n  ${useStatusColorStyles};\n`\n\nexport const mapSize = (size: TextSizeType): BodyProps['size'] => {\n  if (size === 'm') {\n    return 'M'\n  } else if (size === 's') {\n    return 'S'\n  } else if (size === 'xs') {\n    return 'XS'\n  }\n  return size\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/theme/index.ts",
    "content": "// import { theme } from '@redis-ui/styles'\n// todo: after integration with redis-ui, override the theme here\n\nexport const theme = {\n  light: 'light',\n  dark: 'dark',\n  semantic: {\n    core: {\n      space: {\n        base: 'var(--base)', // 16px\n        xxs: 'var(--size-xxs)',\n        xs: 'var(--size-xs)',\n        s: 'var(--size-s)',\n        m: 'var(--size-m)',\n        l: 'var(--size-l)',\n        xl: 'var(--size-xl)',\n        xxl: 'var(--size-xxl)',\n        xxxl: 'var(--size-xxxl)',\n        xxxxl: 'var(--size-xxxxl)',\n        space000: '0',\n        space010: '0.1rem',\n        space025: '0.2rem',\n        space050: '0.4rem',\n        space100: '0.8rem',\n        space150: '1.2rem',\n        space200: '1.6rem',\n        space250: '2rem',\n        space300: '2.4rem',\n        space400: '3.2rem',\n        space500: '4rem',\n        space550: '4.4rem',\n        space600: '4.8rem',\n        space800: '6.4rem',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/theme/types.ts",
    "content": "import { useTheme } from '@redis-ui/styles'\n\nexport type CommonProps = {\n  className?: string\n  'aria-label'?: string\n  'data-testid'?: string\n}\n\nexport type Theme = ReturnType<typeof useTheme>\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/tooltip/HoverContent.tsx",
    "content": "import React from 'react'\n\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Title } from 'uiSrc/components/base/text'\n\ninterface RiTooltipContentProps {\n  title?: React.ReactNode\n  content: React.ReactNode\n}\n\nexport const HoverContent = ({ title, content }: RiTooltipContentProps) => {\n  return (\n    <Col gap=\"s\">\n      {typeof title === 'string' ? <Title size=\"XS\">{title}</Title> : title}\n      {content}\n    </Col>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/tooltip/RITooltip.tsx",
    "content": "import React from 'react'\n\nimport { TooltipProvider, Tooltip, TooltipProps } from '@redis-ui/components'\nimport { HoverContent } from './HoverContent'\nimport styled from 'styled-components'\n\nexport interface RiTooltipProps\n  extends Omit<TooltipProps, 'placement' | 'openDelayDuration'> {\n  title?: React.ReactNode\n  position?: TooltipProps['placement']\n  delay?: TooltipProps['openDelayDuration']\n  anchorClassName?: string\n}\n\nconst StyledTooltip = styled(Tooltip)`\n  word-break: break-word;\n`\n\nexport const RiTooltip = ({\n  children,\n  title,\n  content,\n  position,\n  delay = 250,\n  anchorClassName,\n  ...props\n}: RiTooltipProps) => (\n  <TooltipProvider>\n    <StyledTooltip\n      {...props}\n      content={\n        (content || title) && <HoverContent title={title} content={content} />\n      }\n      placement={position}\n      openDelayDuration={delay}\n    >\n      <span className={anchorClassName}>{children}</span>\n    </StyledTooltip>\n  </TooltipProvider>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/tooltip/RiTooltip.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, screen, act } from '@testing-library/react'\nimport { render, waitForRiTooltipVisible } from 'uiSrc/utils/test-utils'\nimport { RiTooltip, RiTooltipProps } from './RITooltip'\nimport { HoverContent } from './HoverContent'\n\nconst TestButton = () => (\n  <button type=\"button\" data-testid=\"tooltip-trigger\">\n    Hover me\n  </button>\n)\n\nconst defaultProps: RiTooltipProps = {\n  children: <TestButton />,\n  content: 'Test tooltip content',\n}\n\ndescribe('RiTooltip', () => {\n  it('should render', () => {\n    expect(render(<RiTooltip {...defaultProps} />)).toBeTruthy()\n  })\n\n  it('should render children', () => {\n    render(<RiTooltip {...defaultProps} />)\n\n    expect(screen.getByTestId('tooltip-trigger')).toBeInTheDocument()\n  })\n\n  it('should render tooltip content on focus', async () => {\n    render(<RiTooltip {...defaultProps} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getAllByText('Test tooltip content')[0]).toBeInTheDocument()\n  })\n\n  it('should render tooltip with title and content', async () => {\n    render(\n      <RiTooltip {...defaultProps} title=\"Test Title\" content=\"Test content\" />,\n    )\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getAllByText('Test Title')[0]).toBeInTheDocument()\n    expect(screen.getAllByText('Test content')[0]).toBeInTheDocument()\n  })\n\n  it('should render tooltip with only content when title is not provided', async () => {\n    render(<RiTooltip {...defaultProps} content=\"Only content\" />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getAllByText('Only content')[0]).toBeInTheDocument()\n    expect(screen.queryByRole('heading')).not.toBeInTheDocument()\n  })\n\n  it('should not render tooltip when content and title are not provided', async () => {\n    render(\n      <RiTooltip>\n        <TestButton />\n      </RiTooltip>,\n    )\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n\n    // Wait a bit to ensure tooltip doesn't appear\n    await new Promise((resolve) => setTimeout(resolve, 100))\n\n    expect(screen.queryByText('Test Title')).not.toBeInTheDocument()\n  })\n\n  it('should apply anchorClassName to the wrapper span', () => {\n    render(\n      <RiTooltip {...defaultProps} anchorClassName=\"custom-anchor-class\" />,\n    )\n\n    const wrapper = screen.getAllByTestId('tooltip-trigger')[0].parentElement\n    expect(wrapper).toHaveClass('custom-anchor-class')\n  })\n\n  it('should render with React node as title', async () => {\n    const titleNode = <span data-testid=\"custom-title\">Custom Title Node</span>\n\n    render(\n      <RiTooltip {...defaultProps} title={titleNode} content=\"Test content\" />,\n    )\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getAllByTestId('custom-title')[0]).toBeInTheDocument()\n    expect(screen.getAllByText('Test content')[0]).toBeInTheDocument()\n  })\n\n  it('should render with React node as content', async () => {\n    const contentNode = (\n      <div data-testid=\"tooltip-custom-content\">\n        <p>Custom content with HTML</p>\n        <TestButton />\n      </div>\n    )\n\n    render(<RiTooltip {...defaultProps} content={contentNode} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(\n      screen.getAllByTestId('tooltip-custom-content')[0],\n    ).toBeInTheDocument()\n    expect(\n      screen.getAllByText('Custom content with HTML')[0],\n    ).toBeInTheDocument()\n    expect(\n      screen.getAllByRole('button', { name: 'Hover me' })[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should pass through additional props to underlying Tooltip component', async () => {\n    render(\n      <RiTooltip\n        {...defaultProps}\n        position=\"top\"\n        delay={100}\n        data-testid=\"custom-tooltip\"\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n    await waitForRiTooltipVisible()\n\n    // The tooltip should be rendered (testing that props are passed through)\n    expect(screen.getAllByText('Test tooltip content')[0]).toBeInTheDocument()\n  })\n\n  it('should handle empty string content', async () => {\n    render(<RiTooltip {...defaultProps} content=\"\" />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n\n    // Wait a bit to ensure tooltip doesn't appear\n    await new Promise((resolve) => setTimeout(resolve, 100))\n\n    // Should not render tooltip for empty content\n    expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()\n  })\n\n  it('should handle null content', async () => {\n    render(<RiTooltip {...defaultProps} content={null} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n\n    // Wait a bit to ensure tooltip doesn't appear\n    await new Promise((resolve) => setTimeout(resolve, 100))\n\n    // Should not render tooltip for null content\n    expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()\n  })\n\n  it('should handle undefined content', async () => {\n    render(<RiTooltip {...defaultProps} content={undefined} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-trigger'))\n    })\n\n    // Wait a bit to ensure tooltip doesn't appear\n    await new Promise((resolve) => setTimeout(resolve, 100))\n\n    // Should not render tooltip for undefined content\n    expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()\n  })\n})\n\ndescribe('HoverContent', () => {\n  it('should render only content when title is falsy', () => {\n    render(<HoverContent title={null} content=\"Test content\" />)\n\n    expect(screen.getByText('Test content')).toBeInTheDocument()\n    expect(screen.queryByRole('heading')).not.toBeInTheDocument()\n  })\n\n  it('should render only content when title is undefined', () => {\n    render(<HoverContent title={undefined} content=\"Test content\" />)\n\n    expect(screen.getByText('Test content')).toBeInTheDocument()\n    expect(screen.queryByRole('heading')).not.toBeInTheDocument()\n  })\n\n  it('should render title as Title component when title is a plain string', () => {\n    render(<HoverContent title=\"Plain Title\" content=\"Test content\" />)\n\n    const titleElement = screen.getByText('Plain Title')\n    expect(titleElement).toBeInTheDocument()\n    expect(titleElement.tagName).toBe('DIV')\n    expect(screen.getByText('Test content')).toBeInTheDocument()\n  })\n\n  it('should render custom node as-is when title is a React node', () => {\n    const customTitle = (\n      <div data-testid=\"custom-title-node\">\n        <strong>Custom</strong> Title\n      </div>\n    )\n\n    render(<HoverContent title={customTitle} content=\"Test content\" />)\n\n    const customNode = screen.getByTestId('custom-title-node')\n    expect(customNode).toBeInTheDocument()\n    expect(screen.getByText('Custom')).toBeInTheDocument()\n    expect(screen.getByText('Test content')).toBeInTheDocument()\n\n    // Verify the custom node is NOT wrapped in a Title component\n    // Title component would create a heading element (h1-h6)\n    expect(customNode.parentElement?.tagName).not.toMatch(/^H[1-6]$/)\n\n    // Verify the custom node structure is preserved exactly\n    expect(customNode.tagName).toBe('DIV')\n    expect(customNode.querySelector('strong')).toBeInTheDocument()\n  })\n\n  it('should render content with React node', () => {\n    const customContent = (\n      <div data-testid=\"custom-content-node\">\n        <p>Paragraph 1</p>\n        <p>Paragraph 2</p>\n      </div>\n    )\n\n    render(<HoverContent title=\"Title\" content={customContent} />)\n\n    expect(screen.getByText('Title')).toBeInTheDocument()\n    expect(screen.getByTestId('custom-content-node')).toBeInTheDocument()\n    expect(screen.getByText('Paragraph 1')).toBeInTheDocument()\n    expect(screen.getByText('Paragraph 2')).toBeInTheDocument()\n  })\n\n  it('should use Col component with gap=\"s\"', () => {\n    const { container } = render(\n      <HoverContent title=\"Title\" content=\"Content\" />,\n    )\n\n    // Col component should be present as the wrapper\n    const colElement = container.firstChild\n    expect(colElement).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/tooltip/index.tsx",
    "content": "export * from './RITooltip'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/FocusTrap.tsx",
    "content": "import React, { CSSProperties, useEffect, useState } from 'react'\nimport { FocusOn } from 'react-focus-on'\nimport { ReactFocusOnProps } from 'react-focus-on/dist/es5/types'\nimport { RemoveScrollBar } from 'react-remove-scroll-bar'\n\nexport type FocusTarget = HTMLElement | string | (() => HTMLElement)\nconst findElementBySelectorOrRef = (elementTarget?: FocusTarget) => {\n  let node = elementTarget instanceof HTMLElement ? elementTarget : null\n  if (typeof elementTarget === 'string') {\n    node = document.querySelector(elementTarget as string)\n  } else if (typeof elementTarget === 'function') {\n    node = (elementTarget as () => HTMLElement)()\n  }\n  return node\n}\n\nexport type FocusTrapProps = Omit<\n  ReactFocusOnProps,\n  // Inverted `disabled` prop used instead\n  | 'enabled'\n  // Props that differ from react-focus-on's default settings\n  | 'gapMode'\n  | 'crossFrame'\n  | 'scrollLock'\n  | 'noIsolation'\n  | 'returnFocus'\n> & {\n  className?: string\n  style?: CSSProperties\n  /**\n   * @default false\n   */\n  disabled?: boolean\n  /**\n   * Whether `onClickOutside` should be called on mouseup instead of mousedown.\n   * This flag can be used to prevent conflicts with outside toggle buttons by delaying the closing click callback.\n   */\n  closeOnMouseup?: boolean\n  /**\n   * Clicking outside the trap area will disable the trap\n   * @default false\n   */\n  clickOutsideDisables?: boolean\n  /**\n   * Reference to element that will get focus when the trap is initiated\n   */\n  initialFocus?: FocusTarget\n  /**\n   * if `scrollLock` is set to true, the body's scrollbar width will be preserved on lock\n   * via the `gapMode` CSS property. Depending on your custom CSS, you may prefer to use\n   * `margin` instead of `padding`.\n   * @default padding\n   */\n  gapMode?: 'padding' | 'margin'\n  /**\n   * Configures focus trapping between iframes.\n   * By default, FocusTrap allows focus to leave iframes and move to elements outside of it.\n   * Set to `true` if you want focus to remain trapped within the iframe.\n   * @default false\n   */\n  crossFrame?: ReactFocusOnProps['crossFrame']\n  /**\n   * @default false\n   */\n  scrollLock?: ReactFocusOnProps['scrollLock']\n  /**\n   * @default true\n   */\n  noIsolation?: ReactFocusOnProps['noIsolation']\n  /**\n   * @default true\n   */\n  returnFocus?: ReactFocusOnProps['returnFocus']\n}\n\n// Programmatically sets focus on a nested DOM node; optional\nconst setInitialFocus = (initialFocus?: FocusTarget) => {\n  if (!initialFocus) {\n    return\n  }\n  const node = findElementBySelectorOrRef(initialFocus)\n  if (!node) {\n    return\n  }\n  // `data-autofocus` is part of the 'react-focus-on' API\n  node.setAttribute('data-autofocus', 'true')\n}\n\nconst removeMouseupListener = (\n  onMouseupListener: (e: MouseEvent | TouchEvent) => void,\n) => {\n  document.removeEventListener('mouseup', onMouseupListener)\n  document.removeEventListener('touchend', onMouseupListener)\n}\n\nconst addMouseupListener = (\n  onMouseupListener: (e: MouseEvent | TouchEvent) => void,\n) => {\n  document.addEventListener('mouseup', onMouseupListener)\n  document.addEventListener('touchend', onMouseupListener)\n}\n\nconst defaultProps = {\n  clickOutsideDisables: false,\n  disabled: false,\n  returnFocus: true,\n  noIsolation: true,\n  scrollLock: false,\n  crossFrame: false,\n  gapMode: 'padding',\n} as const\n\nexport const FocusTrap = ({\n  children,\n  clickOutsideDisables = defaultProps.clickOutsideDisables,\n  closeOnMouseup,\n  crossFrame = defaultProps.crossFrame,\n  disabled = defaultProps.disabled,\n  gapMode = defaultProps.gapMode,\n  initialFocus,\n  noIsolation = defaultProps.noIsolation,\n  onClickOutside,\n  returnFocus = defaultProps.returnFocus,\n  scrollLock = defaultProps.scrollLock,\n  ...rest\n}: FocusTrapProps) => {\n  const [hasBeenDisabledByClick, setHasBeenDisabledByClick] = useState(disabled)\n\n  const onMouseupOutside = (e: MouseEvent | TouchEvent) => {\n    // Timeout gives precedence to the consumer to initiate close if it has toggle behavior.\n    // Otherwise, this event may occur first and the consumer toggle will reopen the flyout.\n    setTimeout(() => onClickOutside?.(e))\n  }\n  useEffect(() => {\n    setInitialFocus(initialFocus)\n    return () => {\n      removeMouseupListener(onMouseupOutside)\n    }\n  }, [])\n\n  useEffect(() => {\n    if (hasBeenDisabledByClick && disabled === false) {\n      setHasBeenDisabledByClick(false)\n    }\n  }, [disabled])\n\n  const handleOutsideClick: ReactFocusOnProps['onClickOutside'] = (event) => {\n    if (clickOutsideDisables) {\n      setHasBeenDisabledByClick(true)\n    }\n\n    if (onClickOutside) {\n      closeOnMouseup\n        ? addMouseupListener(onMouseupOutside)\n        : onClickOutside(event)\n    }\n  }\n\n  const isDisabled = disabled || hasBeenDisabledByClick\n  const focusOnProps = {\n    returnFocus,\n    noIsolation,\n    crossFrame,\n    enabled: !isDisabled,\n    ...rest,\n    onClickOutside: handleOutsideClick,\n    /**\n     * `scrollLock` should always be unset on FocusOn, as it can prevent scrolling on\n     * portals (i.e. popovers, comboboxes, dropdown menus, etc.) within modals & flyouts\n     * @see https://github.com/theKashey/react-focus-on/issues/49\n     */\n    scrollLock: false,\n  }\n  return (\n    <FocusOn {...focusOnProps}>\n      {children}\n      {!isDisabled && scrollLock && <RemoveScrollBar gapMode={gapMode} />}\n    </FocusOn>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/OutsideClickDetector.tsx",
    "content": "import {\n  Children,\n  cloneElement,\n  EventHandler,\n  MouseEvent as ReactMouseEvent,\n  ReactElement,\n  useCallback,\n  useEffect,\n  useRef,\n} from 'react'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\n\nexport interface RIEvent extends Event {\n  riGeneratedBy: string[]\n}\n\nexport interface OutsideClickDetectorProps {\n  /**\n   * ReactNode to render as this component's content\n   */\n  children: ReactElement\n  onOutsideClick: (event: Event) => void\n  isDisabled?: boolean\n  onMouseDown?: (event: ReactMouseEvent) => void\n  onMouseUp?: (event: ReactMouseEvent) => void\n  onTouchStart?: (event: ReactMouseEvent) => void\n  onTouchEnd?: (event: ReactMouseEvent) => void\n}\n\n// We are working with the assumption that a click event is\n// equivalent to a sequential, compound press and release of\n// the pointing device (mouse, finger, stylus, etc.).\n// A click event's target can be imprecise, as the value will be\n// the closest common ancestor of the press (mousedown, touchstart)\n// and release (mouseup, touchend) events (often <body />) if\n// the target of each event differs.\n// We need the actual event targets to make the correct decisions\n// about user intention. So, consider the down/start and up/end\n// items below as the deconstruction of a click event.\nexport const OutsideClickDetector = ({\n  children,\n  onOutsideClick,\n  isDisabled,\n  onMouseDown,\n  onMouseUp,\n  onTouchStart,\n  onTouchEnd,\n}: OutsideClickDetectorProps) => {\n  const genId = useGenerateId()\n  // the id is used to identify which EuiOutsideClickDetector\n  // is the source of a click event; as the click event bubbles\n  // up and reaches the click detector's child component the\n  // id value is stamped on the event. This id is inspected\n  // in the document's click handler, and if the id doesn't\n  // exist or doesn't match this detector's id, then trigger\n  // the outsideClick callback.\n  //\n  // Taking this approach instead of checking if the event's\n  // target element exists in this component's DOM subtree is\n  // necessary for handling clicks originating from children\n  // rendered through React's portals (EuiPortal). The id tracking\n  // works because React guarantees the event bubbles through the\n  // virtual DOM and executes EuiClickDetector's onClick handler,\n  // stamping the id even though the event originates outside\n  // this component's reified DOM tree.\n  const id = useRef(genId)\n\n  const capturedDownIds = useRef<string[]>([])\n  const onClickOutside: EventHandler<any> = useCallback(\n    (e: Event) => {\n      if (isDisabled) {\n        capturedDownIds.current = []\n        return\n      }\n\n      const event = e as unknown as RIEvent\n\n      if (\n        (event.riGeneratedBy && event.riGeneratedBy.includes(id.current)) ||\n        capturedDownIds.current.includes(id.current)\n      ) {\n        capturedDownIds.current = []\n        return\n      }\n      capturedDownIds.current = []\n      onOutsideClick(event)\n    },\n    [isDisabled, onOutsideClick],\n  )\n  useEffect(() => {\n    document.addEventListener('mouseup', onClickOutside)\n    document.addEventListener('touchend', onClickOutside)\n\n    return () => {\n      document.removeEventListener('mouseup', onClickOutside)\n      document.removeEventListener('touchend', onClickOutside)\n    }\n  }, [onClickOutside])\n\n  const onChildClick = (\n    event: ReactMouseEvent,\n    cb: (event: ReactMouseEvent) => void,\n  ) => {\n    // to support nested click detectors, build an array\n    // of detector ids that have been encountered;\n    if (\n      Object.prototype.hasOwnProperty.call(event.nativeEvent, 'riGeneratedBy')\n    ) {\n      ;(event.nativeEvent as unknown as RIEvent).riGeneratedBy.push(id.current)\n    } else {\n      ;(event.nativeEvent as unknown as RIEvent).riGeneratedBy = [id.current]\n    }\n    if (cb) cb(event)\n  }\n\n  const onChildMouseDown = (event: ReactMouseEvent) => {\n    onChildClick(event, (e) => {\n      const nativeEvent = e.nativeEvent as unknown as RIEvent\n      capturedDownIds.current = nativeEvent.riGeneratedBy\n      if (onMouseDown) onMouseDown(e)\n      if (onTouchStart) onTouchStart(e)\n    })\n  }\n\n  const onChildMouseUp = (event: ReactMouseEvent) => {\n    onChildClick(event, (e) => {\n      if (onMouseUp) onMouseUp(e)\n      if (onTouchEnd) onTouchEnd(e)\n    })\n  }\n  const props = {\n    ...children.props,\n    ...{\n      onMouseDown: onChildMouseDown,\n      onTouchStart: onChildMouseDown,\n      onMouseUp: onChildMouseUp,\n      onTouchEnd: onChildMouseUp,\n    },\n  }\n\n  const child = Children.only(children)\n  return cloneElement(child, props)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/ShowHide.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport {\n  Breakpoints,\n  HideFor,\n  ShowFor,\n} from 'uiSrc/components/base/utils/ShowHide'\n\ndescribe('ShowHide', () => {\n  beforeAll(() => {\n    // @ts-ignore innerWidth might be read only, but we can still override it for the sake of testing\n    window.innerWidth = 670\n  })\n  afterAll(() => 1024) // reset to jsdom's default\n  describe('HideFor', () => {\n    it('should render', () => {\n      expect(\n        render(\n          <HideFor sizes={['s']}>\n            <span>Child</span>\n          </HideFor>,\n        ),\n      ).toBeTruthy()\n    })\n\n    it('hides for matching breakpoints', () => {\n      render(\n        <HideFor sizes={['s']}>\n          <span>Child</span>\n        </HideFor>,\n      )\n\n      expect(screen.queryByText('Child')).not.toBeInTheDocument()\n    })\n\n    Breakpoints.forEach((size) => {\n      it(`${size} is rendered`, () => {\n        render(\n          <HideFor sizes={[size]}>\n            <span>Child</span>\n          </HideFor>,\n        )\n\n        const child = screen.queryByText('Child')\n        if (size === 's') {\n          expect(child).not.toBeInTheDocument()\n          return\n        }\n        expect(child).toBeInTheDocument()\n      })\n    })\n\n    it('renders for multiple breakpoints', () => {\n      render(\n        <HideFor sizes={['m', 'l']}>\n          <span>Child</span>\n        </HideFor>,\n      )\n\n      expect(screen.getByText('Child')).toBeInTheDocument()\n    })\n\n    it('renders for \"none\"', () => {\n      render(\n        <HideFor sizes=\"none\">\n          <span>Child</span>\n        </HideFor>,\n      )\n\n      expect(screen.queryByText('Child')).toBeInTheDocument()\n    })\n\n    test('never renders for \"all\"', () => {\n      render(\n        <HideFor sizes=\"all\">\n          <span>Child</span>\n        </HideFor>,\n      )\n\n      expect(screen.queryByText('Child')).not.toBeInTheDocument()\n    })\n  })\n\n  describe('ShowFor', () => {\n    it('should render', () => {\n      expect(\n        render(\n          <ShowFor sizes={['s']}>\n            <span>Child</span>\n          </ShowFor>,\n        ),\n      ).toBeTruthy()\n    })\n\n    it('shows for matching breakpoints', () => {\n      render(\n        <ShowFor sizes={['s']}>\n          <span>Child</span>\n        </ShowFor>,\n      )\n\n      expect(screen.queryByText('Child')).toBeInTheDocument()\n    })\n\n    Breakpoints.forEach((size) => {\n      it(`${size} is rendered`, () => {\n        render(\n          <ShowFor sizes={[size]}>\n            <span>Child</span>\n          </ShowFor>,\n        )\n\n        const child = screen.queryByText('Child')\n        if (size === 's') {\n          expect(child).toBeInTheDocument()\n          return\n        }\n        expect(child).not.toBeInTheDocument()\n      })\n    })\n\n    it('renders for multiple breakpoints', () => {\n      render(\n        <ShowFor sizes={['s', 'xs']}>\n          <span>Child</span>\n        </ShowFor>,\n      )\n\n      expect(screen.getByText('Child')).toBeInTheDocument()\n    })\n\n    it('never renders for \"none\"', () => {\n      render(\n        <ShowFor sizes=\"none\">\n          <span>Child</span>\n        </ShowFor>,\n      )\n\n      expect(screen.queryByText('Child')).not.toBeInTheDocument()\n    })\n\n    test('renders for \"all\"', () => {\n      render(\n        <ShowFor sizes=\"all\">\n          <span>Child</span>\n        </ShowFor>,\n      )\n\n      expect(screen.queryByText('Child')).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/ShowHide.tsx",
    "content": "import throttle from 'lodash/throttle'\nimport React, { ReactNode, useEffect, useState } from 'react'\n\nexport const Breakpoints = ['xs', 's', 'm', 'l', 'xl'] as const\nexport type BreakpointKey = (typeof Breakpoints)[number]\ntype CurrentBreakpoint = BreakpointKey | undefined\n\n// Explicitly list out each key so we can document default values\n// via JSDoc (which is available to devs in IDE via intellisense)\nexport type BreakpointsType = Record<BreakpointKey, number>\n\nexport const breakpoints: BreakpointsType = {\n  xl: 1200,\n  l: 992,\n  m: 768,\n  s: 575,\n  xs: 0,\n}\n\nexport interface ShowHideForProps {\n  /**\n   * Required otherwise nothing ever gets returned\n   */\n  children: ReactNode\n  /**\n   * List of all the responsive sizes to hide the children for.\n   * Array of #BreakpointKey\n   */\n  sizes: BreakpointKey[] | 'all' | 'none'\n}\n\nexport const HideFor = ({ children, sizes }: ShowHideForProps) => {\n  const currentBreakpoint = useCurrentBreakpoint()\n  const isWithinBreakpointSizes =\n    currentBreakpoint && sizes.includes(currentBreakpoint)\n\n  if (sizes === 'all' || isWithinBreakpointSizes) {\n    return null\n  }\n  return <>{children}</>\n}\n\nexport const ShowFor = ({ children, sizes }: ShowHideForProps) => {\n  const currentBreakpoint = useCurrentBreakpoint()\n  const isWithinBreakpointSizes =\n    currentBreakpoint && sizes.includes(currentBreakpoint)\n\n  if (sizes === 'all' || isWithinBreakpointSizes) {\n    return <>{children}</>\n  }\n  return null\n}\n\n// Find the breakpoint (key) whose value is <= windowWidth starting with the largest first\nconst getBreakpoint = (width: number) => {\n  const breakpointKeys = Object.keys(breakpoints) as BreakpointKey[]\n  const sortedBreakpoints = breakpointKeys.sort(\n    (a, b) => breakpoints[b] - breakpoints[a],\n  )\n  return sortedBreakpoints.find(\n    (key) => breakpoints[key as BreakpointKey] <= width,\n  )\n}\n\n/**\n * Returns the current breakpoint based on window width.\n */\nexport const useCurrentBreakpoint = () => {\n  const [currentBreakpoint, setCurrentBreakpoint] = useState<CurrentBreakpoint>(\n    typeof window !== 'undefined'\n      ? getBreakpoint(window.innerWidth)\n      : undefined,\n  )\n\n  useEffect(() => {\n    const onWindowResize = throttle(() => {\n      setCurrentBreakpoint(getBreakpoint(window.innerWidth))\n    }, 50)\n\n    window.addEventListener('resize', onWindowResize)\n\n    return () => window.removeEventListener('resize', onWindowResize)\n  }, [])\n\n  return currentBreakpoint\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/WindowEvent.spec.tsx",
    "content": "import React from 'react'\nimport { render, fireEvent } from 'uiSrc/utils/test-utils'\nimport { WindowEvent } from './WindowEvent'\n\ndescribe('WindowEvent', () => {\n  let windowAddCount = 0\n  let windowRemoveCount = 0\n  let windowAddEventListener: typeof window.addEventListener\n  let windowRemoveEventListener: typeof window.removeEventListener\n\n  beforeAll(() => {\n    windowAddEventListener = window.addEventListener\n    windowRemoveEventListener = window.removeEventListener\n    // React 16 and 17 register a bunch of error listeners which we don't need to capture\n    window.addEventListener = jest.fn((event: string) => {\n      if (event !== 'error') {\n        windowAddCount++\n      }\n    })\n    window.removeEventListener = jest.fn((event: string) => {\n      if (event !== 'error') {\n        windowRemoveCount++\n      }\n    })\n  })\n\n  beforeEach(() => {\n    // Reset counts\n    windowAddCount = 0\n    windowRemoveCount = 0\n  })\n\n  afterAll(() => {\n    jest.restoreAllMocks()\n  })\n\n  it('should attach handler to window event on mount', () => {\n    const handler = () => null\n    render(<WindowEvent event=\"click\" handler={handler} />)\n    expect(window.addEventListener).toHaveBeenCalledWith('click', handler)\n    expect(windowAddCount).toEqual(1)\n  })\n\n  it('should remove handler on unmount', () => {\n    const handler = () => null\n    const { unmount } = render(<WindowEvent event=\"click\" handler={handler} />)\n    unmount()\n    expect(window.removeEventListener).toHaveBeenCalledWith('click', handler)\n    expect(windowRemoveCount).toEqual(1)\n  })\n\n  it('should remove and re-attach handler to window event on update', () => {\n    const handler1 = () => null\n    const handler2 = () => null\n    const { rerender } = render(\n      <WindowEvent event=\"click\" handler={handler1} />,\n    )\n\n    expect(window.addEventListener).toHaveBeenCalledWith('click', handler1)\n\n    rerender(<WindowEvent event=\"keydown\" handler={handler2} />)\n\n    expect(window.removeEventListener).toHaveBeenCalledWith('click', handler1)\n    expect(window.addEventListener).toHaveBeenCalledWith('keydown', handler2)\n  })\n\n  it('should not remove or re-attach handler if update is irrelevant', () => {\n    const handler = () => null\n    const { rerender } = render(<WindowEvent event=\"click\" handler={handler} />)\n    expect(windowAddCount).toEqual(1)\n\n    rerender(\n      <WindowEvent event=\"click\" handler={handler} data-test-subj=\"whatever\" />,\n    )\n    expect(windowAddCount).toEqual(1)\n    expect(windowRemoveCount).toEqual(0)\n  })\n\n  it('should call handler on window event', () => {\n    window.addEventListener = windowAddEventListener\n    window.removeEventListener = windowRemoveEventListener\n    const handler = jest.fn()\n    render(<WindowEvent event=\"click\" handler={handler} />)\n    fireEvent.click(window)\n    expect(handler).toHaveBeenCalled()\n    expect(handler).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/WindowEvent.tsx",
    "content": "import { useEffect } from 'react'\n\ntype EventNames = keyof WindowEventMap\n\ninterface WindowEventProps<Ev extends EventNames> {\n  event: Ev\n  handler: (ev: WindowEventMap[Ev]) => any\n}\n\n/**\n * Adds an event listener to the window object and cleans it up when the component\n * is unmounted.\n *\n * @example\n *\n * ```tsx\n * useWindowEvent('resize', handleResize)\n * ```\n */\nexport function useWindowEvent<EName extends EventNames>(\n  event: WindowEventProps<EName>['event'],\n  handler: WindowEventProps<EName>['handler'],\n) {\n  useEffect(() => {\n    window.addEventListener(event, handler)\n    return () => {\n      window.removeEventListener(event, handler)\n    }\n  }, [event, handler])\n}\n\n/**\n * A component that adds an event listener to the window object and cleans it up when it is unmounted.\n *\n * Added for convenience, replacing EuiWindowEvent, but for future uses, the useWindowEvent hook is recommended.\n * @example\n *\n * ```tsx\n * <WindowEvent event=\"resize\" handler={handleResize} />\n * ```\n */\nexport function WindowEvent<EName extends EventNames>({\n  event,\n  handler,\n}: WindowEventProps<EName>) {\n  useWindowEvent(event, handler)\n  return null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/hooks/generate-id.ts",
    "content": "import { useMemo, useId } from 'react'\nimport { v1 as uuidV1 } from 'uuid'\n\n/**\n * Generates a memoized ID that remains static until component unmount.\n * This prevents IDs from being re-randomized on every component update.\n * @param prefix Optional prefix to prepend to the generated ID\n * @param suffix Optional suffix to append to the generated ID\n * @param conditionalId Optional conditional ID to use instead of a randomly generated ID. Typically used by components where IDs can be passed in as custom props\n */\nexport const useGenerateId = (\n  prefix = '',\n  suffix = '',\n  conditionalId?: string,\n) => {\n  let id: string\n  if (useId) {\n    // eslint-disable-next-line react-hooks/rules-of-hooks\n    id = useId()\n  } else {\n    id = htmlIdGenerator(prefix)(suffix)\n  }\n\n  return useMemo(\n    () => conditionalId || `${prefix}${id}${suffix}`,\n    [id, prefix, suffix, conditionalId],\n  )\n}\n\n/**\n * This function returns a function to generate ids.\n * This can be used to generate unique, but predictable ids to pair labels\n * with their inputs. It takes an optional prefix as a parameter. If you don't\n * specify it, it generates a random id prefix. If you specify a custom prefix\n * it should begin with an letter to be HTML4 compliant.\n */\nexport function htmlIdGenerator(idPrefix: string = '') {\n  const staticUuid = uuidV1()\n  return (idSuffix: string = '') => {\n    const prefix = `${idPrefix}${idPrefix !== '' ? '_' : 'i'}`\n    const suffix = idSuffix ? `_${idSuffix}` : ''\n    return `${prefix}${suffix ? staticUuid : uuidV1()}${suffix}`\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/hooks/inner-text.ts",
    "content": "import { useCallback, useEffect, useState } from 'react'\n\ntype RefT = HTMLElement | Element | undefined | null\n\n/**\n * `useInnerText` is a hook that provides the text content of the DOM node referenced by `ref`.\n *\n * When `ref` changes, the hook will update the `innerText` value by reading the `ref`'s `innerText` property.\n * If `ref` is null or does not have an `innerText` property, the hook will return `null`.\n *\n * @example\n * const MyComponent = () => {\n *   const [ref, innerText] = useInnerText('default value')\n *\n *   return (\n *     <div ref={ref}>\n *       {innerText}\n *     </div>\n *   )\n * }\n *\n * @param innerTextFallback Value to return if `ref` is null or does not have an `innerText` property.\n * @returns A tuple containing a function to update the `ref` and the current `innerText` value.\n */\nexport function useInnerText(\n  innerTextFallback?: string,\n): [(node: RefT) => void, string | undefined] {\n  const [ref, setRef] = useState<RefT>(null)\n  const [innerText, setInnerText] = useState(innerTextFallback)\n\n  const updateInnerText = useCallback(\n    (node: RefT) => {\n      if (!node) return\n      setInnerText(\n        // Check for `innerText` implementation rather than a simple OR check\n        // because in real cases the result of `innerText` could correctly be `null`\n        // while the result of `textContent` could correctly be non-`null` due to\n        // differing reliance on browser layout calculations.\n        // We prefer the result of `innerText`, if available.\n        'innerText' in node\n          ? node.innerText\n          : node.textContent || innerTextFallback,\n      )\n    },\n    [innerTextFallback],\n  )\n\n  useEffect(() => {\n    const observer = new MutationObserver((mutationsList) => {\n      if (mutationsList.length) updateInnerText(ref)\n    })\n\n    if (ref) {\n      updateInnerText(ref)\n      observer.observe(ref, {\n        characterData: true,\n        subtree: true,\n        childList: true,\n      })\n    }\n    return () => {\n      observer.disconnect()\n    }\n  }, [ref, updateInnerText])\n\n  return [setRef, innerText]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/index.ts",
    "content": "export { OutsideClickDetector } from './OutsideClickDetector'\nexport { RIResizeObserver } from './resize-observer/ResizeObserver'\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/outsideClickDetector.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render } from 'uiSrc/utils/test-utils'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils/OutsideClickDetector'\n\ndescribe('OutsideClickDetector', () => {\n  it('is rendered', () => {\n    const { container } = render(\n      <OutsideClickDetector onOutsideClick={() => {}}>\n        <div />\n      </OutsideClickDetector>,\n    )\n    expect(container.firstChild).toBeTruthy()\n  })\n\n  describe('behavior', () => {\n    test('nested detectors', async () => {\n      const unrelatedDetector = jest.fn()\n      const parentDetector = jest.fn()\n      const childDetector = jest.fn()\n\n      const { findByTestId } = render(\n        <div role=\"button\" tabIndex={0}>\n          <div>\n            <OutsideClickDetector onOutsideClick={parentDetector}>\n              <div>\n                <OutsideClickDetector onOutsideClick={childDetector}>\n                  <div data-testid=\"target1\" />\n                </OutsideClickDetector>\n              </div>\n            </OutsideClickDetector>\n          </div>\n\n          <OutsideClickDetector onOutsideClick={unrelatedDetector}>\n            <div data-testid=\"target2\" />\n          </OutsideClickDetector>\n        </div>,\n      )\n      const target2 = await findByTestId('target2')\n      fireEvent.mouseDown(target2)\n      fireEvent.mouseUp(target2)\n\n      expect(unrelatedDetector).toHaveBeenCalledTimes(0)\n      expect(childDetector).toHaveBeenCalledTimes(1)\n      expect(parentDetector).toHaveBeenCalledTimes(1)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/pluginsThemeContext.tsx",
    "content": "import React, { useLayoutEffect, useState } from 'react'\nimport { ThemeProvider as StyledThemeProvider } from 'styled-components'\nimport { CommonStyles, themesDefault } from '@redis-ui/styles'\nimport 'modern-normalize/modern-normalize.css'\nimport '@redis-ui/styles/normalized-styles.css'\nimport '@redis-ui/styles/fonts.css'\nimport { GlobalStyles } from 'uiSrc/styles/globalStyles'\n\ninterface Props {\n  children: React.ReactNode\n}\n\nconst { light: themeLight, dark: themeDark } = themesDefault\nexport const defaultState = {\n  theme: themeLight,\n}\n\nexport const PluginsThemeContext = React.createContext(defaultState)\n\nexport const ThemeProvider = ({ children }: Props) => {\n  const [theme, setTheme] = useState(defaultState.theme)\n  // use useEffect to get body class, if it is equal to 'theme_DARK', set theme to\n  // themeDark, if it is equal to 'theme_LIGHT', set theme to themeLight\n  useLayoutEffect(() => {\n    const bodyClass = document.body.classList.value\n    if (bodyClass === 'theme_DARK' && theme !== themeDark) {\n      setTheme(themeDark)\n    } else if (bodyClass === 'theme_LIGHT' && theme !== themeLight) {\n      setTheme(themeLight)\n    }\n  }, [theme])\n  return (\n    <PluginsThemeContext.Provider value={{ theme }}>\n      <StyledThemeProvider theme={theme}>\n        <CommonStyles />\n        <GlobalStyles />\n        {children}\n      </StyledThemeProvider>\n    </PluginsThemeContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/base/utils/resize-observer/ResizeObserver.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\n\ninterface RIResizeObserverProps {\n  children: (ref: React.Ref<HTMLDivElement>) => React.ReactNode\n  onResize: (dimensions: { height: number; width: number }) => void\n}\n\nexport const RIResizeObserver: React.FC<RIResizeObserverProps> = ({\n  children,\n  onResize,\n}) => {\n  const containerRef = useRef<HTMLDivElement | null>(null)\n\n  useEffect(() => {\n    const element = containerRef.current\n    if (element) {\n      const observer = new window.ResizeObserver(([entry]) => {\n        const { width, height } = entry.contentRect\n        onResize({ width, height })\n      })\n\n      observer.observe(element)\n\n      return () => {\n        observer.disconnect()\n      }\n    }\n  }, [onResize, containerRef.current])\n\n  return <>{children(containerRef)}</>\n}\n\nexport default RIResizeObserver\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/BottomGroupComponents.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  resetCliHelperSettings,\n  resetCliSettings,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { resetOutputLoading } from 'uiSrc/slices/cli/cli-output'\n\nimport BottomGroupComponents from './BottomGroupComponents'\n\njest.mock('uiSrc/slices/cli/cli-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/cli-settings'),\n  cliSettingsSelector: jest.fn().mockReturnValue({\n    isShowCli: true,\n    isShowHelper: true,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst commandHelperId = 'command-helper'\nconst cliId = 'cli'\n\ndescribe('BottomGroupComponents', () => {\n  it('should render', () => {\n    expect(render(<BottomGroupComponents />)).toBeTruthy()\n  })\n\n  it('should render Cli when isShowCli truthy', () => {\n    render(<BottomGroupComponents />)\n    expect(screen.getByTestId(cliId)).toBeInTheDocument()\n  })\n\n  it('should render Command Helper when isShowHelper truthy', () => {\n    render(<BottomGroupComponents />)\n    expect(screen.getByTestId(commandHelperId)).toBeInTheDocument()\n  })\n\n  it('should not to close command helper after closing cli', () => {\n    render(<BottomGroupComponents />)\n    fireEvent.click(screen.getByTestId('close-cli'))\n    const expectedActions = [resetCliSettings(), resetOutputLoading()]\n    expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions))\n\n    expect(screen.getByTestId('command-helper')).toBeInTheDocument()\n  })\n\n  it('should not to close cli after closing command-helper', () => {\n    render(<BottomGroupComponents />)\n    fireEvent.click(screen.getByTestId('close-command-helper'))\n\n    const expectedActions = [resetCliHelperSettings()]\n    expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions))\n\n    expect(screen.getByTestId('cli')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/BottomGroupComponents.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport React, { useEffect } from 'react'\nimport { useDispatch } from 'react-redux'\n\nimport BottomGroupComponents from './BottomGroupComponents'\nimport {\n  openCliHelper,\n  openCli,\n  resetCliHelperSettings,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { showMonitor } from 'uiSrc/slices/cli/monitor'\n\nimport { StyledContainer } from '../../../../../.storybook/helpers/styles'\n\nconst meta = {\n  component: BottomGroupComponents,\n} satisfies Meta<typeof BottomGroupComponents>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const CLI: Story = {\n  decorators: [\n    (Story) => {\n      const dispatch = useDispatch()\n\n      useEffect(() => {\n        dispatch(resetCliHelperSettings())\n        dispatch(openCli())\n      }, [dispatch])\n\n      return (\n        <StyledContainer>\n          <Story />\n        </StyledContainer>\n      )\n    },\n  ],\n}\nexport const CLIHelper: Story = {\n  decorators: [\n    (Story) => {\n      const dispatch = useDispatch()\n\n      useEffect(() => {\n        dispatch(resetCliHelperSettings())\n        dispatch(openCliHelper())\n      }, [dispatch])\n\n      return (\n        <StyledContainer>\n          <Story />\n        </StyledContainer>\n      )\n    },\n  ],\n}\n\nexport const Monitor: Story = {\n  decorators: [\n    (Story) => {\n      const dispatch = useDispatch()\n\n      useEffect(() => {\n        dispatch(resetCliHelperSettings())\n        dispatch(showMonitor())\n      }, [dispatch])\n\n      return (\n        <StyledContainer>\n          <Story />\n        </StyledContainer>\n      )\n    },\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/BottomGroupComponents.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport styled from 'styled-components'\n\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport CliWrapper from 'uiSrc/components/cli/CliWrapper'\nimport CommandHelperWrapper from 'uiSrc/components/command-helper/CommandHelperWrapper'\nimport { MonitorWrapper } from 'uiSrc/components'\nimport { monitorSelector } from 'uiSrc/slices/cli/monitor'\nimport BottomGroupMinimized from './components/bottom-group-minimized/BottomGroupMinimized'\n\nimport styles from './styles.module.scss'\n\nconst GroupComponentsWrapper = styled.div`\n  height: 100%;\n  padding: 0 16px;\n`\n\nconst GroupComponents = styled.div`\n  display: flex;\n  flex-grow: 1;\n  height: calc(100% - 26px);\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`\n\nconst BottomGroupComponents = () => {\n  const { isShowCli, isShowHelper } = useSelector(cliSettingsSelector)\n  const { isShowMonitor } = useSelector(monitorSelector)\n\n  return (\n    <GroupComponentsWrapper>\n      <GroupComponents>\n        {isShowCli && <CliWrapper />}\n        {isShowHelper && (\n          <div\n            className={cx(styles.helperWrapper, {\n              [styles.fullWidth]: !isShowCli,\n            })}\n          >\n            <CommandHelperWrapper />\n          </div>\n        )}\n        {isShowMonitor && (\n          <div\n            className={cx(styles.monitorWrapper, {\n              [styles.fullWidth]: !isShowCli,\n            })}\n          >\n            <MonitorWrapper />\n          </div>\n        )}\n      </GroupComponents>\n      <BottomGroupMinimized />\n    </GroupComponentsWrapper>\n  )\n}\n\nexport default BottomGroupComponents\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\n\nimport { toggleCli, toggleCliHelper } from 'uiSrc/slices/cli/cli-settings'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport BottomGroupMinimized from './BottomGroupMinimized'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('BottomGroupMinimized', () => {\n  it('should render', () => {\n    expect(render(<BottomGroupMinimized />)).toBeTruthy()\n  })\n\n  it('should \"toggleCli\" action be called after click \"expand-cli\" button', () => {\n    render(<BottomGroupMinimized />)\n    fireEvent.click(screen.getByTestId('expand-cli'))\n\n    const expectedActions = [toggleCli()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should \"toggleCliHelper\" action be called after click \"expand-command-helper\" button', () => {\n    render(<BottomGroupMinimized />)\n    fireEvent.click(screen.getByTestId('expand-command-helper'))\n\n    const expectedActions = [toggleCliHelper()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n  it('should show \"Profiler\" and \"user-survey-link\" when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<BottomGroupMinimized />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('expand-monitor')).toBeInTheDocument()\n    expect(screen.queryByTestId('user-survey-link')).toBeInTheDocument()\n  })\n\n  it('should hide \"Profiler\" and \"user-survey-link\" when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<BottomGroupMinimized />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('expand-monitor')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('user-survey-link')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport BottomGroupMinimized from './BottomGroupMinimized'\n\nconst meta = {\n  component: BottomGroupMinimized,\n} satisfies Meta<typeof BottomGroupMinimized>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport {\n  clearSearchingCommand,\n  cliSettingsSelector,\n  setCliEnteringCommand,\n  toggleCli,\n  toggleCliHelper,\n  toggleHideCliHelper,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  monitorSelector,\n  toggleHideMonitor,\n  toggleMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport FeatureFlagComponent from 'uiSrc/components/feature-flag-component'\nimport { FeatureFlags } from 'uiSrc/constants'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { HideFor, ShowFor } from 'uiSrc/components/base/utils/ShowHide'\nimport {\n  CliIcon,\n  DocumentationIcon,\n  ProfilerIcon,\n} from 'uiSrc/components/base/icons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from '../../styles.module.scss'\nimport {\n  ComponentBadge,\n  ContainerMinimized,\n} from './ButtonGroupMinimized.styles'\n\nconst BottomGroupMinimized = () => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { isShowCli, cliClientUuid, isShowHelper, isMinimizedHelper } =\n    useSelector(cliSettingsSelector)\n  const { isShowMonitor, isMinimizedMonitor } = useSelector(monitorSelector)\n  const dispatch = useDispatch()\n\n  useEffect(\n    () => () => {\n      dispatch(clearSearchingCommand())\n      dispatch(setCliEnteringCommand())\n    },\n    [],\n  )\n\n  const handleExpandCli = () => {\n    sendEventTelemetry({\n      event: isShowCli\n        ? TelemetryEvent.CLI_MINIMIZED\n        : TelemetryEvent.CLI_OPENED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    dispatch(toggleCli())\n  }\n\n  const handleExpandHelper = () => {\n    sendEventTelemetry({\n      event: isShowHelper\n        ? TelemetryEvent.COMMAND_HELPER_MINIMIZED\n        : TelemetryEvent.COMMAND_HELPER_OPENED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    isMinimizedHelper && dispatch(toggleHideCliHelper())\n    dispatch(toggleCliHelper())\n  }\n\n  const handleExpandMonitor = () => {\n    sendEventTelemetry({\n      event: isShowMonitor\n        ? TelemetryEvent.PROFILER_MINIMIZED\n        : TelemetryEvent.PROFILER_OPENED,\n      eventData: { databaseId: instanceId },\n    })\n    isMinimizedMonitor && dispatch(toggleHideMonitor())\n    dispatch(toggleMonitor())\n  }\n\n  const onClickSurvey = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.USER_SURVEY_LINK_CLICKED,\n    })\n  }\n\n  return (\n    <ContainerMinimized>\n      <Row align=\"center\" responsive={false} style={{ height: '100%' }} gap=\"s\">\n        <FlexItem onClick={handleExpandCli} data-testid=\"expand-cli\">\n          <ComponentBadge\n            withIcon\n            icon={CliIcon}\n            label={\n              <Text size=\"S\" variant=\"semiBold\" component=\"span\">\n                CLI\n              </Text>\n            }\n            isActive={isShowCli || !!cliClientUuid}\n          />\n        </FlexItem>\n\n        <FlexItem\n          onClick={handleExpandHelper}\n          data-testid=\"expand-command-helper\"\n        >\n          <ComponentBadge\n            withIcon\n            icon={DocumentationIcon}\n            label={\n              <Text size=\"S\" variant=\"semiBold\" component=\"span\">\n                Command Helper\n              </Text>\n            }\n            isActive={isShowHelper || isMinimizedHelper}\n          />\n        </FlexItem>\n        <FeatureFlagComponent name={FeatureFlags.envDependent}>\n          <FlexItem onClick={handleExpandMonitor} data-testid=\"expand-monitor\">\n            <ComponentBadge\n              withIcon\n              icon={ProfilerIcon}\n              label={\n                <Text size=\"S\" variant=\"semiBold\" component=\"span\">\n                  Profiler\n                </Text>\n              }\n              isActive={isShowMonitor || isMinimizedMonitor}\n            />\n          </FlexItem>\n        </FeatureFlagComponent>\n      </Row>\n      <FeatureFlagComponent name={FeatureFlags.envDependent}>\n        <a\n          className={styles.surveyLink}\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          href={EXTERNAL_LINKS.userSurvey}\n          onClick={onClickSurvey}\n          data-testid=\"user-survey-link\"\n        >\n          <RiIcon type=\"SurveyIcon\" className={styles.surveyIcon} />\n          <HideFor sizes={['xs', 's']}>\n            <span>Let us know what you think</span>\n          </HideFor>\n          <ShowFor sizes={['xs', 's']}>\n            <span>Survey</span>\n          </ShowFor>\n        </a>\n      </FeatureFlagComponent>\n    </ContainerMinimized>\n  )\n}\n\nexport default BottomGroupMinimized\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/ButtonGroupMinimized.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\n\nexport const ComponentBadge = styled(RiBadge)<{ isActive?: boolean }>`\n  height: 18px !important;\n  border: none !important;\n  cursor: pointer;\n  user-select: none;\n\n  &[title] {\n    pointer-events: none;\n  }\n\n  background-color: transparent !important;\n\n  ${({ isActive, theme }) => {\n    // TODO: try to replace with semantic colors once the palette is bigger.\n    const bgColorActive =\n      theme.name === 'dark'\n        ? theme.semantic.color.background.primary400\n        : theme.semantic.color.background.primary400\n    const bgColorHover =\n      theme.name === 'dark'\n        ? theme.semantic.color.background.primary500\n        : theme.semantic.color.background.primary300\n\n    const textColorActiveHover = theme.semantic.color.text.primary50\n\n    return `\n    ${isActive ? `background-color: ${bgColorActive} !important;` : ''}\n    ${isActive ? `color: ${textColorActiveHover} !important;` : ''}\n    &:hover {\n      background-color: ${bgColorHover} !important;\n      color: ${textColorActiveHover} !important;\n    }\n  `\n  }}\n`\n\nexport const ContainerMinimized = styled.div`\n  display: flex;\n  align-items: center;\n  padding-left: ${({ theme }) => theme.core.space.space050};\n  height: 26px;\n  line-height: 26px;\n  border-left: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-right: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/index.ts",
    "content": "import BottomGroupMinimized from './BottomGroupMinimized'\n\nexport default BottomGroupMinimized\n"
  },
  {
    "path": "redisinsight/ui/src/components/bottom-group-components/styles.module.scss",
    "content": ".surveyLink {\n  display: flex;\n  align-items: center;\n  height: 100%;\n  padding: 0 12px;\n  color: var(--htmlColor) !important;\n  font: normal normal normal 12px/18px Graphik, sans-serif;\n  &:hover {\n    background-color: var(--euiColorSecondary);\n    color: var(--euiColorPrimaryText) !important;\n  }\n}\n.surveyIcon {\n  margin-right: 8px;\n  width: 18px;\n  height: 18px;\n}\n\n.helperWrapper {\n  width: 100%;\n  max-width: 360px;\n  min-width: 230px;\n  &.fullWidth {\n    max-width: 100%;\n  }\n}\n\n.monitorWrapper {\n  width: 100%;\n  max-width: 628px;\n  min-width: 230px;\n  &.fullWidth {\n    max-width: 100%;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/KeysBrowser.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { KeysBrowser } from './KeysBrowser'\n\ndescribe('KeysBrowser', () => {\n  describe('Compose', () => {\n    it('should render children', () => {\n      render(\n        <KeysBrowser.Compose>\n          <span>content</span>\n        </KeysBrowser.Compose>,\n      )\n      expect(screen.getByText('content')).toBeInTheDocument()\n    })\n\n    it('should use default data-testid', () => {\n      render(\n        <KeysBrowser.Compose>\n          <span>content</span>\n        </KeysBrowser.Compose>,\n      )\n      expect(screen.getByTestId('keys-browser')).toBeInTheDocument()\n    })\n\n    it('should accept custom data-testid', () => {\n      render(\n        <KeysBrowser.Compose data-testid=\"custom-root\">\n          <span>content</span>\n        </KeysBrowser.Compose>,\n      )\n      expect(screen.getByTestId('custom-root')).toBeInTheDocument()\n    })\n\n    it('should apply className', () => {\n      render(\n        <KeysBrowser.Compose className=\"my-class\">\n          <span>content</span>\n        </KeysBrowser.Compose>,\n      )\n      expect(screen.getByTestId('keys-browser')).toHaveClass('my-class')\n    })\n  })\n\n  describe('Header', () => {\n    it('should render children', () => {\n      render(\n        <KeysBrowser.Header>\n          <span>header content</span>\n        </KeysBrowser.Header>,\n      )\n      expect(screen.getByText('header content')).toBeInTheDocument()\n    })\n\n    it('should use default data-testid', () => {\n      render(\n        <KeysBrowser.Header>\n          <span>header</span>\n        </KeysBrowser.Header>,\n      )\n      expect(screen.getByTestId('keys-browser-header')).toBeInTheDocument()\n    })\n\n    it('should accept custom data-testid', () => {\n      render(\n        <KeysBrowser.Header data-testid=\"custom-header\">\n          <span>header</span>\n        </KeysBrowser.Header>,\n      )\n      expect(screen.getByTestId('custom-header')).toBeInTheDocument()\n    })\n\n    it('should apply className', () => {\n      render(\n        <KeysBrowser.Header className=\"header-class\">\n          <span>header</span>\n        </KeysBrowser.Header>,\n      )\n      expect(screen.getByTestId('keys-browser-header')).toHaveClass(\n        'header-class',\n      )\n    })\n  })\n\n  describe('Content', () => {\n    it('should render children', () => {\n      render(\n        <KeysBrowser.Content>\n          <span>main content</span>\n        </KeysBrowser.Content>,\n      )\n      expect(screen.getByText('main content')).toBeInTheDocument()\n    })\n\n    it('should use default data-testid', () => {\n      render(\n        <KeysBrowser.Content>\n          <span>content</span>\n        </KeysBrowser.Content>,\n      )\n      expect(screen.getByTestId('keys-browser-content')).toBeInTheDocument()\n    })\n\n    it('should accept custom data-testid', () => {\n      render(\n        <KeysBrowser.Content data-testid=\"custom-content\">\n          <span>content</span>\n        </KeysBrowser.Content>,\n      )\n      expect(screen.getByTestId('custom-content')).toBeInTheDocument()\n    })\n\n    it('should apply className', () => {\n      render(\n        <KeysBrowser.Content className=\"content-class\">\n          <span>content</span>\n        </KeysBrowser.Content>,\n      )\n      expect(screen.getByTestId('keys-browser-content')).toHaveClass(\n        'content-class',\n      )\n    })\n  })\n\n  describe('Footer', () => {\n    it('should render children', () => {\n      render(\n        <KeysBrowser.Footer>\n          <span>footer content</span>\n        </KeysBrowser.Footer>,\n      )\n      expect(screen.getByText('footer content')).toBeInTheDocument()\n    })\n\n    it('should use default data-testid', () => {\n      render(\n        <KeysBrowser.Footer>\n          <span>footer</span>\n        </KeysBrowser.Footer>,\n      )\n      expect(screen.getByTestId('keys-browser-footer')).toBeInTheDocument()\n    })\n\n    it('should accept custom data-testid', () => {\n      render(\n        <KeysBrowser.Footer data-testid=\"custom-footer\">\n          <span>footer</span>\n        </KeysBrowser.Footer>,\n      )\n      expect(screen.getByTestId('custom-footer')).toBeInTheDocument()\n    })\n\n    it('should apply className', () => {\n      render(\n        <KeysBrowser.Footer className=\"footer-class\">\n          <span>footer</span>\n        </KeysBrowser.Footer>,\n      )\n      expect(screen.getByTestId('keys-browser-footer')).toHaveClass(\n        'footer-class',\n      )\n    })\n  })\n\n  describe('composed layout', () => {\n    it('should render all slots together', () => {\n      render(\n        <KeysBrowser.Compose>\n          <KeysBrowser.Header>\n            <span>header</span>\n          </KeysBrowser.Header>\n          <KeysBrowser.Content>\n            <span>content</span>\n          </KeysBrowser.Content>\n          <KeysBrowser.Footer>\n            <span>footer</span>\n          </KeysBrowser.Footer>\n        </KeysBrowser.Compose>,\n      )\n\n      expect(screen.getByTestId('keys-browser')).toBeInTheDocument()\n      expect(screen.getByTestId('keys-browser-header')).toBeInTheDocument()\n      expect(screen.getByTestId('keys-browser-content')).toBeInTheDocument()\n      expect(screen.getByTestId('keys-browser-footer')).toBeInTheDocument()\n\n      expect(screen.getByText('header')).toBeInTheDocument()\n      expect(screen.getByText('content')).toBeInTheDocument()\n      expect(screen.getByText('footer')).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/KeysBrowser.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Root = styled(Col)`\n  height: 100%;\n`\n\nexport const HeaderContainer = styled(Row)`\n  width: 100%;\n  padding: ${({ theme }) =>\n    `${theme.core.space.space050} ${theme.core.space.space150}`};\n  flex-shrink: 0;\n  position: relative;\n`\n\nexport const ContentContainer = styled(Col)`\n  overflow: hidden;\n`\n\nexport const FooterContainer = styled(Row)`\n  flex-shrink: 0;\n  padding: ${({ theme }) =>\n    `${theme.core.space.space050} ${theme.core.space.space150}`};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/KeysBrowser.tsx",
    "content": "import React from 'react'\n\nimport { KeysBrowserSlotProps } from './KeysBrowser.types'\nimport * as S from './KeysBrowser.styles'\n\nconst KeysBrowserCompose = ({\n  children,\n  className,\n  'data-testid': testId,\n}: KeysBrowserSlotProps) => (\n  <S.Root className={className} data-testid={testId ?? 'keys-browser'}>\n    {children}\n  </S.Root>\n)\n\nconst KeysBrowserHeader = ({\n  children,\n  className,\n  'data-testid': testId,\n}: KeysBrowserSlotProps) => (\n  <S.HeaderContainer\n    grow={false}\n    className={className}\n    data-testid={testId ?? 'keys-browser-header'}\n  >\n    {children}\n  </S.HeaderContainer>\n)\n\nconst KeysBrowserContent = ({\n  children,\n  className,\n  'data-testid': testId,\n}: KeysBrowserSlotProps) => (\n  <S.ContentContainer\n    className={className}\n    data-testid={testId ?? 'keys-browser-content'}\n  >\n    {children}\n  </S.ContentContainer>\n)\n\nconst KeysBrowserFooter = ({\n  children,\n  className,\n  'data-testid': testId,\n}: KeysBrowserSlotProps) => (\n  <S.FooterContainer\n    align=\"center\"\n    grow={false}\n    className={className}\n    data-testid={testId ?? 'keys-browser-footer'}\n  >\n    {children}\n  </S.FooterContainer>\n)\n\nexport const KeysBrowser = {\n  Compose: KeysBrowserCompose,\n  Header: KeysBrowserHeader,\n  Content: KeysBrowserContent,\n  Footer: KeysBrowserFooter,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/KeysBrowser.types.ts",
    "content": "import React from 'react'\n\nexport interface KeysBrowserSlotProps {\n  children?: React.ReactNode\n  className?: string\n  'data-testid'?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/columns-menu/ColumnsMenu.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { ToggleButton } from 'uiSrc/components/base/forms/buttons'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\n\nexport const ColumnsButton = styled(ToggleButton)`\n  padding: ${({ theme }) => theme.core.space.space050};\n  border-color: transparent;\n  box-shadow: none;\n`\n\nexport const StyledCheckbox = styled(Checkbox)`\n  white-space: nowrap;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/columns-menu/ColumnsMenu.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { BrowserColumns } from 'uiSrc/constants'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport * as S from './ColumnsMenu.styles'\n\nexport interface ColumnsMenuProps {\n  shownColumns: BrowserColumns[]\n  onToggleColumn: (checked: boolean, column: BrowserColumns) => void\n}\n\nconst ColumnsMenu = ({ shownColumns, onToggleColumn }: ColumnsMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false)\n\n  const toggleVisibility = () => setIsOpen(!isOpen)\n\n  return (\n    <RiPopover\n      ownFocus={false}\n      anchorPosition=\"downLeft\"\n      isOpen={isOpen}\n      closePopover={() => setIsOpen(false)}\n      button={\n        <S.ColumnsButton\n          onPressedChange={toggleVisibility}\n          data-testid=\"btn-columns-actions\"\n          aria-label=\"columns\"\n          pressed={isOpen}\n        >\n          <RiIcon size=\"m\" type=\"ColumnsIcon\" />\n          <Text size=\"s\">Columns</Text>\n        </S.ColumnsButton>\n      }\n    >\n      <Col gap=\"m\">\n        <FlexItem>\n          <Row align=\"center\" gap=\"m\">\n            <FlexItem grow>\n              <S.StyledCheckbox\n                id=\"show-key-size\"\n                name=\"show-key-size\"\n                label=\"Key size\"\n                checked={shownColumns.includes(BrowserColumns.Size)}\n                onChange={(e) =>\n                  onToggleColumn(e.target.checked, BrowserColumns.Size)\n                }\n                data-testid=\"show-key-size\"\n              />\n            </FlexItem>\n            <FlexItem>\n              <RiTooltip\n                content=\"Hide the key size to avoid performance issues when working with large keys.\"\n                position=\"top\"\n                anchorClassName=\"flex-row\"\n              >\n                <RiIcon\n                  type=\"InfoIcon\"\n                  size=\"m\"\n                  style={{ cursor: 'pointer' }}\n                  data-testid=\"key-size-info-icon\"\n                />\n              </RiTooltip>\n            </FlexItem>\n          </Row>\n        </FlexItem>\n        <FlexItem>\n          <Checkbox\n            id=\"show-ttl\"\n            name=\"show-ttl\"\n            label=\"TTL\"\n            checked={shownColumns.includes(BrowserColumns.TTL)}\n            onChange={(e) =>\n              onToggleColumn(e.target.checked, BrowserColumns.TTL)\n            }\n            data-testid=\"show-ttl\"\n          />\n        </FlexItem>\n      </Col>\n    </RiPopover>\n  )\n}\n\nexport default React.memo(ColumnsMenu)\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/columns-menu/index.ts",
    "content": "export { default as ColumnsMenu } from './ColumnsMenu'\nexport type { ColumnsMenuProps } from './ColumnsMenu'\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/hooks/index.ts",
    "content": "export { useResponsiveColumns } from './useResponsiveColumns'\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/hooks/useResponsiveColumns.ts",
    "content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { BrowserColumns } from 'uiSrc/constants'\n\n/**\n * Responsive column hiding breakpoints (in pixels):\n * - >= 500px: show all user-enabled columns\n * - 400-500px: auto-hide Size column\n * - 300-400px: auto-hide both Size and TTL columns\n * - ~300px (minimum): show only Type + Key name\n */\nconst BREAKPOINT_HIDE_SIZE = 500\nconst BREAKPOINT_HIDE_TTL = 400\n\nconst getEffectiveColumns = (\n  shownColumns: BrowserColumns[],\n  width: number,\n): BrowserColumns[] => {\n  if (width >= BREAKPOINT_HIDE_SIZE) {\n    return shownColumns\n  }\n\n  if (width >= BREAKPOINT_HIDE_TTL) {\n    return shownColumns.filter((col) => col !== BrowserColumns.Size)\n  }\n\n  return shownColumns.filter(\n    (col) => col !== BrowserColumns.Size && col !== BrowserColumns.TTL,\n  )\n}\n\nexport const useResponsiveColumns = (\n  shownColumns: BrowserColumns[],\n): {\n  effectiveColumns: BrowserColumns[]\n  containerRef: React.RefObject<HTMLDivElement>\n} => {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const [containerWidth, setContainerWidth] = useState<number>(Infinity)\n\n  const handleResize = useCallback((entries: ResizeObserverEntry[]) => {\n    const entry = entries[0]\n    if (entry) {\n      setContainerWidth(entry.contentRect.width)\n    }\n  }, [])\n\n  useEffect(() => {\n    const element = containerRef.current\n    if (!element) return undefined\n\n    const observer = new ResizeObserver(handleResize)\n    observer.observe(element)\n\n    return () => {\n      observer.disconnect()\n    }\n  }, [handleResize])\n\n  // Derive a discrete breakpoint level so the memoized result only changes\n  // when crossing a threshold, not on every pixel of resize.\n  const breakpointLevel =\n    containerWidth >= BREAKPOINT_HIDE_SIZE\n      ? 2\n      : containerWidth >= BREAKPOINT_HIDE_TTL\n        ? 1\n        : 0\n\n  const effectiveColumns = useMemo(\n    () => getEffectiveColumns(shownColumns, containerWidth),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [shownColumns, breakpointLevel],\n  )\n\n  return { effectiveColumns, containerRef }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/index.ts",
    "content": "export { KeysBrowser } from './KeysBrowser'\nexport type { KeysBrowserSlotProps } from './KeysBrowser.types'\nexport { ViewSwitch } from './view-switch'\nexport type { ViewSwitchProps } from './view-switch'\nexport { ColumnsMenu } from './columns-menu'\nexport type { ColumnsMenuProps } from './columns-menu'\nexport { useResponsiveColumns } from './hooks'\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/view-switch/ViewSwitch.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { ButtonGroup } from 'uiSrc/components/base/forms/button-group/ButtonGroup'\n\nexport const SwitchButton = styled(ButtonGroup.Button)`\n  width: ${({ theme }) => theme.core.space.space300} !important;\n  min-width: ${({ theme }) => theme.core.space.space300} !important;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/view-switch/ViewSwitch.tsx",
    "content": "import React from 'react'\n\nimport { EqualIcon, FoldersIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport { OnboardingTour } from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { ButtonGroup } from 'uiSrc/components/base/forms/button-group/ButtonGroup'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\n\nimport { ISwitchType, ViewSwitchProps } from './ViewSwitch.types'\nimport * as S from './ViewSwitch.styles'\n\nconst ViewSwitch = ({\n  viewType,\n  isTreeViewDisabled = false,\n  onChange,\n}: ViewSwitchProps) => {\n  const viewTypes: ISwitchType[] = [\n    {\n      type: KeyViewType.Browser,\n      tooltipText: 'List View',\n      ariaLabel: 'List view button',\n      dataTestId: 'view-type-browser-btn',\n      getIconType: () => EqualIcon,\n    },\n    {\n      type: KeyViewType.Tree,\n      tooltipText: isTreeViewDisabled\n        ? 'Tree View is unavailable when the HEX key name format is selected.'\n        : 'Tree View',\n      ariaLabel: 'Tree view button',\n      dataTestId: 'view-type-list-btn',\n      disabled: isTreeViewDisabled,\n      getIconType: () => FoldersIcon,\n    },\n  ]\n\n  return (\n    <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_TREE_VIEW}>\n      <ButtonGroup data-testid=\"view-type-switcher\">\n        {viewTypes.map((view) => (\n          <RiTooltip\n            content={view.tooltipText}\n            position=\"top\"\n            key={view.tooltipText}\n          >\n            <S.SwitchButton\n              aria-label={view.ariaLabel}\n              onClick={() => onChange(view.type)}\n              isSelected={viewType === view.type}\n              data-testid={view.dataTestId}\n              disabled={view.disabled || false}\n            >\n              <ButtonGroup.Icon icon={view.getIconType()} />\n            </S.SwitchButton>\n          </RiTooltip>\n        ))}\n      </ButtonGroup>\n    </OnboardingTour>\n  )\n}\n\nexport default React.memo(ViewSwitch)\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/view-switch/ViewSwitch.types.ts",
    "content": "import { IconType } from 'uiSrc/components/base/icons'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\n\nexport interface ISwitchType {\n  tooltipText: string\n  type: KeyViewType\n  disabled?: boolean\n  ariaLabel: string\n  dataTestId: string\n  getIconType: () => IconType\n}\n\nexport interface ViewSwitchProps {\n  viewType: KeyViewType\n  isTreeViewDisabled?: boolean\n  onChange: (type: KeyViewType) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/browser/view-switch/index.ts",
    "content": "export { default as ViewSwitch } from './ViewSwitch'\nexport type { ViewSwitchProps } from './ViewSwitch.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.spec.tsx",
    "content": "/* eslint-disable sonarjs/no-duplicate-string */\nimport { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport MockedSocket from 'socket.io-mock'\nimport socketIO from 'socket.io-client'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport {\n  BulkActionsServerEvent,\n  BulkActionsStatus,\n  BulkActionsType,\n  FeatureFlags,\n  SocketEvent,\n} from 'uiSrc/constants'\nimport {\n  bulkActionsDeleteSelector,\n  bulkActionsSelector,\n  disconnectBulkDeleteAction,\n  setBulkActionConnected,\n  setBulkDeleteLoading,\n  setDeleteOverviewStatus,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { GlobalSubscriptions } from 'uiSrc/components'\nimport * as ioHooks from 'uiSrc/services/hooks/useIoConnection'\nimport { getSocketApiUrl } from 'uiSrc/utils'\nimport BulkActionsConfig from './BulkActionsConfig'\n\nlet store: typeof mockedStore\nlet socket: typeof MockedSocket\nlet useIoConnectionSpy: jest.SpyInstance\n\nbeforeEach(() => {\n  cleanup()\n  socket = new MockedSocket()\n  socketIO.mockReturnValue(socket)\n  useIoConnectionSpy = jest.spyOn(ioHooks, 'useIoConnection')\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('socket.io-client')\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  bulkActionsSelector: jest.fn().mockReturnValue({\n    isConnected: false,\n  }),\n  bulkActionsDeleteSelector: jest.fn().mockReturnValue({\n    isActionTriggered: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '1',\n  }),\n}))\n\nconst deletingMock = [\n  {\n    id: '123',\n    databaseId: '1',\n    db: 1,\n    type: BulkActionsType.Unlink,\n    filter: {\n      type: null,\n      match: '*',\n    },\n  },\n]\n\ndescribe('BulkActionsConfig', () => {\n  it('should render', () => {\n    expect(render(<BulkActionsConfig />)).toBeTruthy()\n  })\n\n  it('should connect socket', () => {\n    const bulkActionsDeleteSelectorMock = jest.fn().mockReturnValue({\n      isActionTriggered: true,\n    })\n    bulkActionsDeleteSelector.mockImplementation(bulkActionsDeleteSelectorMock)\n\n    render(<BulkActionsConfig />)\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    const afterRenderActions = [\n      setBulkActionConnected(true),\n      setBulkDeleteLoading(true),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n    expect(useIoConnectionSpy).toHaveBeenCalledWith(\n      getSocketApiUrl('bulk-actions'),\n      { query: { instanceId: '1' }, token: '' },\n    )\n  })\n\n  it('should not connect socket', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    const { unmount } = render(<GlobalSubscriptions />, {\n      store: mockStore(initialStoreState),\n    })\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    expect(store.getActions()).toEqual([])\n\n    unmount()\n  })\n\n  it('should not connect socket', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    const { unmount } = render(<GlobalSubscriptions />, {\n      store: mockStore(initialStoreState),\n    })\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    expect(store.getActions()).toEqual([])\n\n    unmount()\n  })\n\n  it('should emit Create a delete type', () => {\n    const bulkActionsDeleteSelectorMock = jest.fn().mockReturnValue({\n      isActionTriggered: true,\n    })\n    bulkActionsDeleteSelector.mockImplementation(bulkActionsDeleteSelectorMock)\n\n    render(<BulkActionsConfig />)\n\n    socket.on(BulkActionsServerEvent.Create, (data: any) => {\n      expect(data).toEqual(deletingMock)\n    })\n\n    socket.socketClient.emit(SocketEvent.Connect)\n    socket.socketClient.emit(BulkActionsServerEvent.Create, deletingMock)\n  })\n\n  it('should catch disconnect', () => {\n    const bulkActionsSelectorMock = jest.fn().mockReturnValue({\n      isConnected: true,\n    })\n    const bulkActionsDeleteSelectorMock = jest.fn().mockReturnValue({\n      isActionTriggered: true,\n    })\n    bulkActionsSelector.mockImplementation(bulkActionsSelectorMock)\n    bulkActionsDeleteSelector.mockImplementation(bulkActionsDeleteSelectorMock)\n\n    const { unmount } = render(<BulkActionsConfig retryDelay={0} />)\n\n    socket.socketClient.emit(SocketEvent.Connect)\n    socket.socketClient.emit(SocketEvent.Disconnect)\n\n    const afterRenderActions = [\n      setBulkActionConnected(true),\n      setBulkDeleteLoading(true),\n      setDeleteOverviewStatus(BulkActionsStatus.Disconnected),\n      disconnectBulkDeleteAction(),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Socket } from 'socket.io-client'\n\nimport {\n  bulkActionsDeleteSelector,\n  bulkActionsSelector,\n  disconnectBulkDeleteAction,\n  setBulkActionConnected,\n  setBulkDeleteLoading,\n  setDeleteOverview,\n  setDeleteOverviewStatus,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { deleteKeysByPattern } from 'uiSrc/slices/browser/keys'\nimport { getSocketApiUrl, Nullable, triggerDownloadFromUrl } from 'uiSrc/utils'\nimport { sessionStorageService } from 'uiSrc/services'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { isProcessingBulkAction } from 'uiSrc/pages/browser/components/bulk-actions/utils'\nimport {\n  BrowserStorageItem,\n  BulkActionsServerEvent,\n  BulkActionsStatus,\n  BulkActionsType,\n  SocketEvent,\n} from 'uiSrc/constants'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { appCsrfSelector } from 'uiSrc/slices/app/csrf'\nimport { useIoConnection } from 'uiSrc/services/hooks/useIoConnection'\nimport { getBaseUrl } from 'uiSrc/services/apiService'\n\nconst BulkActionsConfig = () => {\n  const { id: instanceId = '', db } = useSelector(connectedInstanceSelector)\n  const { isConnected } = useSelector(bulkActionsSelector)\n  const {\n    isActionTriggered: isDeleteTriggered,\n    generateReport,\n    filter,\n    search,\n  } = useSelector(bulkActionsDeleteSelector)\n  const { token } = useSelector(appCsrfSelector)\n  const socketRef = useRef<Nullable<Socket>>(null)\n  const connectIo = useIoConnection(getSocketApiUrl('bulk-actions'), {\n    token,\n    query: { instanceId },\n  })\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!isDeleteTriggered || !instanceId || socketRef.current?.connected) {\n      return\n    }\n\n    let retryTimer: NodeJS.Timer\n    socketRef.current = connectIo()\n\n    socketRef.current.on(SocketEvent.Connect, () => {\n      clearTimeout(retryTimer)\n      dispatch(setBulkActionConnected(true))\n      emitBulkDelete(`${Date.now()}`)\n    })\n\n    // Catch connect error\n    socketRef.current?.on(SocketEvent.ConnectionError, () => {})\n\n    // Catch disconnect\n    socketRef.current?.on(SocketEvent.Disconnect, () => {\n      dispatch(setDeleteOverviewStatus(BulkActionsStatus.Disconnected))\n      handleDisconnect()\n    })\n  }, [instanceId, isDeleteTriggered])\n\n  useEffect(() => {\n    if (!socketRef.current?.connected) {\n      return\n    }\n    const id =\n      sessionStorageService.get(BrowserStorageItem.bulkActionDeleteId) ?? ''\n    if (!id) return\n\n    if (!isDeleteTriggered) {\n      abortBulkDelete(id)\n      return\n    }\n\n    emitBulkDelete(id)\n  }, [isDeleteTriggered])\n\n  const emitBulkDelete = (id: string) => {\n    dispatch(setBulkDeleteLoading(true))\n    sessionStorageService.set(BrowserStorageItem.bulkActionDeleteId, id)\n\n    // Register overview listener BEFORE emitting Create to avoid missing early events\n    // Server may start sending Overview events immediately after receiving Create\n    socketRef.current?.off(BulkActionsServerEvent.Overview)\n    socketRef.current?.on(BulkActionsServerEvent.Overview, (payload: any) => {\n      dispatch(setBulkDeleteLoading(isProcessingBulkAction(payload.status)))\n      dispatch(setDeleteOverview(payload))\n\n      if (payload.status === BulkActionsStatus.Failed) {\n        dispatch(disconnectBulkDeleteAction())\n      }\n\n      // Remove deleted keys from local state when bulk delete completes\n      // Use payload.filter values (what server actually used) to avoid race conditions\n      // if user changed search/filter while delete was running\n      if (payload.status === BulkActionsStatus.Completed) {\n        const deletedCount = payload.summary?.succeed || 0\n        const pattern = payload.filter?.match\n        // Only do local filtering for specific patterns, not for '*' (all keys)\n        if (pattern && pattern !== '*') {\n          dispatch(\n            deleteKeysByPattern({\n              pattern,\n              deletedCount,\n              filterType: payload.filter?.type || null,\n            }),\n          )\n        }\n      }\n    })\n\n    socketRef.current?.emit(\n      BulkActionsServerEvent.Create,\n      {\n        id,\n        databaseId: instanceId,\n        db: db || 0,\n        type: BulkActionsType.Unlink,\n        filter: {\n          type: filter,\n          match: search || '*',\n        },\n        generateReport,\n      },\n      onBulkDeleting,\n    )\n  }\n\n  const abortBulkDelete = (id: string) => {\n    dispatch(setBulkDeleteLoading(true))\n    socketRef.current?.emit(\n      BulkActionsServerEvent.Abort,\n      { id: `${id}` },\n      onBulkDeleteAborted,\n    )\n  }\n\n  const onBulkDeleting = (data: any) => {\n    if (data.status === BulkActionsServerEvent.Error) {\n      dispatch(disconnectBulkDeleteAction())\n      dispatch(addErrorNotification({ response: { data: data.error } }))\n    }\n\n    // Trigger download if report URL is provided\n    if ('downloadUrl' in data && data.downloadUrl) {\n      triggerDownloadFromUrl(`${getBaseUrl()}${data.downloadUrl}`)\n    }\n  }\n\n  const onBulkDeleteAborted = (data: any) => {\n    dispatch(setBulkDeleteLoading(false))\n    sessionStorageService.set(BrowserStorageItem.bulkActionDeleteId, '')\n    dispatch(setDeleteOverview(data))\n    handleDisconnect()\n  }\n\n  useEffect(() => {\n    if (!isConnected && socketRef.current?.connected) {\n      handleDisconnect()\n    }\n  }, [isConnected])\n\n  const handleDisconnect = () => {\n    dispatch(disconnectBulkDeleteAction())\n    socketRef.current?.removeAllListeners()\n    socketRef.current?.disconnect()\n  }\n\n  return null\n}\n\nexport default BulkActionsConfig\n"
  },
  {
    "path": "redisinsight/ui/src/components/bulk-actions-config/index.ts",
    "content": "import BulkActionsConfig from './BulkActionsConfig'\n\nexport default BulkActionsConfig\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/bar-chart/BarChart.spec.tsx",
    "content": "import { last, min as minBy, reject } from 'lodash'\nimport React from 'react'\nimport { render, screen, fireEvent, waitFor } from 'uiSrc/utils/test-utils'\n\nimport BarChart, { BarChartData, BarChartDataType } from './BarChart'\n\nconst mockData: BarChartData[] = [\n  { x: 1, y: 0, xlabel: '', ylabel: '' },\n  { x: 5, y: 0.1, xlabel: '', ylabel: '' },\n  { x: 10, y: 20, xlabel: '', ylabel: '' },\n  { x: 2, y: 30, xlabel: '', ylabel: '' },\n  { x: 30, y: 40, xlabel: '', ylabel: '' },\n  { x: 15, y: 50000, xlabel: '', ylabel: '' },\n]\n\ndescribe('BarChart', () => {\n  it('should render with empty data', () => {\n    expect(render(<BarChart data={[]} />)).toBeTruthy()\n  })\n\n  it('should render with data', () => {\n    expect(render(<BarChart data={mockData} />)).toBeTruthy()\n  })\n\n  it('should not render area with empty data', () => {\n    const { container } = render(<BarChart data={[]} name=\"test\" />)\n    expect(container).toBeEmptyDOMElement()\n  })\n\n  it('should render svg', () => {\n    render(<BarChart data={mockData} name=\"test\" />)\n    expect(screen.getByTestId('bar-test')).toBeInTheDocument()\n  })\n\n  it('should render bars', () => {\n    render(<BarChart data={mockData} />)\n    mockData.forEach(({ x, y }) => {\n      expect(screen.getByTestId(`bar-${x}-${y}`)).toBeInTheDocument()\n    })\n  })\n\n  it('should render smallest bar with min height', () => {\n    const minBarHeight = 5\n    const smallestBar = minBy(\n      reject([...mockData], ({ y }) => !y),\n      ({ y }) => y,\n    ) ?? { x: 0, y: 0 }\n\n    render(<BarChart data={mockData} minBarHeight={minBarHeight} />)\n    expect(\n      screen.getByTestId(`bar-${smallestBar.x}-${smallestBar.y}`),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByTestId(`bar-${smallestBar.x}-${smallestBar.y}`),\n    ).toHaveAttribute('height', `${minBarHeight}`)\n  })\n\n  it('should render tooltip and content inside', async () => {\n    render(<BarChart data={mockData} name=\"test\" />)\n\n    await waitFor(\n      () => {\n        fireEvent.mouseMove(screen.getByTestId('bar-15-50000'))\n      },\n      { timeout: 210 },\n    ) // Account for long delay on tooltips\n\n    expect(screen.getByTestId('bar-tooltip')).toBeInTheDocument()\n    expect(screen.getByTestId('bar-tooltip')).toHaveTextContent('50000')\n  })\n\n  it('when dataType=\"Bytes\" max value should be rounded by metric', async () => {\n    const lastDataValue = last(mockData)\n    const { queryByTestId } = render(\n      <BarChart\n        data={mockData}\n        name=\"test\"\n        dataType={BarChartDataType.Bytes}\n      />,\n    )\n\n    expect(queryByTestId(`ytick-${lastDataValue?.y}-4`)).not.toBeInTheDocument()\n    expect(queryByTestId('ytick-51200-8')).toBeInTheDocument()\n    expect(queryByTestId('ytick-51200-8')).toHaveTextContent('51200')\n  })\n\n  it('when dataType!=\"Bytes\" max value should be rounded by default', async () => {\n    const lastDataValue = last(mockData)\n    const { queryByTestId } = render(<BarChart data={mockData} name=\"test\" />)\n\n    expect(queryByTestId('ytick-51200-8')).not.toBeInTheDocument()\n    expect(queryByTestId(`ytick-${lastDataValue?.y}-8`)).toBeInTheDocument()\n    expect(queryByTestId(`ytick-${lastDataValue?.y}-8`)).toHaveTextContent(\n      `${lastDataValue?.y}`,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/bar-chart/BarChart.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport BarChart, { BarChartDataType } from './BarChart'\nimport { formatBytes } from 'uiSrc/utils'\n\nconst barChartMeta: Meta<typeof BarChart> = {\n  component: BarChart,\n  decorators: [\n    (Story) => (\n      <div style={{ padding: '20px' }}>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport default barChartMeta\n\ntype Story = StoryObj<typeof barChartMeta>\n\nexport const Default: Story = {\n  args: {\n    width: 600,\n    height: 300,\n    name: 'default',\n    data: [\n      { x: 1, y: 100, xlabel: 'A', ylabel: '' },\n      { x: 2, y: 200, xlabel: 'B', ylabel: '' },\n      { x: 3, y: 150, xlabel: 'C', ylabel: '' },\n      { x: 4, y: 300, xlabel: 'D', ylabel: '' },\n      { x: 5, y: 250, xlabel: 'E', ylabel: '' },\n    ],\n  },\n}\n\nexport const BytesDataType: Story = {\n  args: {\n    width: 700,\n    height: 350,\n    name: 'memory-usage',\n    dataType: BarChartDataType.Bytes,\n    data: [\n      { x: 3600, y: 1024 * 512, xlabel: '<1 hr', ylabel: '' },\n      { x: 14400, y: 1024 * 1024 * 2, xlabel: '1-4 Hrs', ylabel: '' },\n      { x: 43200, y: 1024 * 1024 * 5, xlabel: '4-12 Hrs', ylabel: '' },\n      { x: 86400, y: 1024 * 1024 * 10, xlabel: '12-24 Hrs', ylabel: '' },\n      { x: 604800, y: 1024 * 1024 * 3, xlabel: '1-7 Days', ylabel: '' },\n      { x: 2592000, y: 1024 * 1024, xlabel: '>7 Days', ylabel: '' },\n    ],\n    tooltipValidation: (val) => formatBytes(val, 3) as string,\n    leftAxiosValidation: (val, i) => (i % 2 ? '' : formatBytes(val, 1)),\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/bar-chart/BarChart.styles.ts",
    "content": "import styled, { createGlobalStyle } from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport React from 'react'\n\nexport const Wrapper = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  margin: 0 auto;\n`\n\nexport const StyledSVG = styled.svg<\n  React.SVGProps<SVGSVGElement> & {\n    ref?: React.Ref<SVGSVGElement>\n    theme: Theme\n  }\n>`\n  --bar-fill: ${({ theme }) => theme.semantic.color.background.notice500};\n  --bar-stroke: ${({ theme }) =>\n    theme.semantic.color.background.informative700};\n\n  width: 100%;\n  height: 100%;\n  /* D3-created bar elements */\n  .bar-chart-bar {\n    fill: rgb(from var(--bar-fill) r g b / 0.1);\n    stroke: var(--bar-stroke);\n    stroke-width: 1.5px;\n  }\n\n  /* D3-created scatter point elements */\n  .bar-chart-scatter-points {\n    fill: var(--bar-stroke);\n    cursor: pointer;\n  }\n\n  /* D3-created dashed line elements */\n  .bar-chart-dashed-line {\n    stroke: ${({ theme }) => theme.semantic.color.text.neutral800};\n    stroke-width: 1px;\n    stroke-dasharray: 5, 3;\n  }\n\n  /* D3-created tick lines */\n  .tick line {\n    stroke: ${({ theme }) => theme.semantic.color.text.neutral800};\n    opacity: 0.1;\n  }\n\n  /* D3-created domain */\n  .domain {\n    opacity: 0;\n  }\n\n  /* D3-created text elements */\n  text {\n    color: ${({ theme }) => theme.semantic.color.text.neutral800};\n  }\n`\n\n// Tooltip is appended to body by D3, so needs global styles\nexport const TooltipGlobalStyles = createGlobalStyle<{ theme: Theme }>`\n  .bar-chart-tooltip {\n    position: fixed;\n    min-width: 50px;\n    background: ${({ theme }) => theme.semantic.color.background.neutral600};\n    color: ${({ theme }) => theme.semantic.color.text.primary600} !important;\n    z-index: 10;\n    border-radius: ${({ theme }) => theme.core.space.space100};\n    pointer-events: none;\n    font-weight: 400;\n    font-size: 12px !important;\n    box-shadow: 0 3px 15px rgba(0, 0, 0, 0.2) !important;\n    bottom: 0;\n    height: 36px;\n    min-height: 36px;\n    padding: ${({ theme }) => theme.core.space.space100};\n    line-height: 16px;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/bar-chart/BarChart.tsx",
    "content": "import * as d3 from 'd3'\nimport React, { useEffect, useRef } from 'react'\nimport cx from 'classnames'\nimport { curryRight, flow, toNumber } from 'lodash'\n\nimport { formatBytes, toBytes } from 'uiSrc/utils'\nimport { Wrapper, StyledSVG, TooltipGlobalStyles } from './BarChart.styles'\n\nexport interface BarChartData {\n  y: number\n  x: number\n  xlabel: string\n  ylabel: string\n}\n\ninterface IDatum extends BarChartData {\n  index: number\n}\n\nexport enum BarChartDataType {\n  Bytes = 'bytes',\n}\n\nexport interface IProps {\n  name?: string\n  data?: BarChartData[]\n  dataType?: BarChartDataType\n  barWidth?: number\n  minBarHeight?: number\n  width?: number\n  height?: number\n  yCountTicks?: number\n  divideLastColumn?: boolean\n  multiplierGrid?: number\n  classNames?: {\n    bar?: string\n    dashedLine?: string\n    tooltip?: string\n    scatterPoints?: string\n  }\n  tooltipValidation?: (val: any, index: number) => string\n  leftAxiosValidation?: (val: any, index: number) => any\n  bottomAxiosValidation?: (val: any, index: number) => any\n}\n\nexport const DEFAULT_MULTIPLIER_GRID = 5\nexport const DEFAULT_Y_TICKS = 8\nexport const DEFAULT_BAR_WIDTH = 40\nexport const MIN_BAR_HEIGHT = 3\nlet cleanedData: IDatum[] = []\n\nconst BarChart = (props: IProps) => {\n  const {\n    data = [],\n    name,\n    width: propWidth = 0,\n    height: propHeight = 0,\n    barWidth = DEFAULT_BAR_WIDTH,\n    yCountTicks = DEFAULT_Y_TICKS,\n    minBarHeight = MIN_BAR_HEIGHT,\n    dataType,\n    classNames,\n    divideLastColumn,\n    multiplierGrid = DEFAULT_MULTIPLIER_GRID,\n    tooltipValidation = (val) => val,\n    leftAxiosValidation = (val) => val,\n    bottomAxiosValidation = (val) => val,\n  } = props\n\n  const margin = { top: 10, right: 0, bottom: 32, left: 60 }\n  const width = propWidth - margin.left - margin.right\n  const height = propHeight - margin.top - margin.bottom\n\n  const svgRef = useRef<SVGSVGElement>(null)\n\n  const getRoundedYMaxValue = (number: number): number => {\n    const numLen = number.toString().length\n    const dividerValue = toNumber(`1${'0'.repeat(numLen - 1)}`)\n\n    return Math.ceil(number / dividerValue) * dividerValue\n  }\n\n  useEffect(() => {\n    if (data.length === 0) {\n      return undefined\n    }\n\n    const tooltip = d3\n      .select('body')\n      .append('div')\n      .attr('class', cx('bar-chart-tooltip', classNames?.tooltip || ''))\n      .style('opacity', 0)\n\n    d3.select(svgRef.current).select('g').remove()\n\n    // append the svg object to the body of the page\n    const svg = d3\n      .select(svgRef.current)\n      .attr('data-testid', `bar-${name}`)\n      .attr('width', width + margin.left + margin.right)\n      .attr('height', height + margin.top + margin.bottom + 30)\n      .append('g')\n      .attr('transform', `translate(${margin.left},${margin.top})`)\n\n    const tempData = [...data]\n\n    tempData.push({ x: 0, y: 0, xlabel: '', ylabel: '' })\n    cleanedData = tempData.map((datum, index) => ({\n      index,\n      xlabel: `${datum?.xlabel || ''}`,\n      ylabel: `${datum?.ylabel || ''}`,\n      y: datum.y || 0,\n      x: datum.x || 0,\n    }))\n\n    // Add X axis\n    const xAxis = d3\n      .scaleLinear()\n      .domain(d3.extent(cleanedData, (d) => d.index) as [number, number])\n      .range([0, width])\n\n    let maxY = d3.max(cleanedData, (d) => d.y) || yCountTicks\n\n    if (dataType === BarChartDataType.Bytes) {\n      const curriedTyBytes = curryRight(toBytes)\n      const [maxYFormatted, type] = formatBytes(maxY, 1, true)\n\n      maxY = flow(\n        toNumber,\n        Math.ceil,\n        getRoundedYMaxValue,\n        curriedTyBytes(`${type}`),\n      )(maxYFormatted)\n    }\n\n    // Add Y axis\n    const yAxis = d3\n      .scaleLinear()\n      .domain([0, maxY || 0])\n      .range([height, 0])\n\n    // divider for last column\n    if (divideLastColumn) {\n      svg\n        .append('line')\n        .attr('class', cx('bar-chart-dashed-line', classNames?.dashedLine))\n        .attr('x1', xAxis(cleanedData.length - 2.3))\n        .attr('x2', xAxis(cleanedData.length - 2.3))\n        .attr('y1', 0)\n        .attr('y2', height)\n    }\n\n    // squared background for Y axis\n    svg.append('g').call(\n      d3\n        .axisLeft(yAxis)\n        .tickSize(\n          -width + (2 * width) / (cleanedData.length * multiplierGrid) + 6,\n        )\n        .tickValues([...d3.range(0, maxY, maxY / yCountTicks), maxY])\n        .tickFormat((d, i) => leftAxiosValidation(d, i))\n        .ticks(cleanedData.length * multiplierGrid)\n        .tickPadding(10),\n    )\n\n    const yTicks = d3.selectAll('.tick')\n    yTicks.attr('data-testid', (d, i) => `ytick-${d}-${i}`)\n\n    // squared background for X axis\n    svg\n      .append('g')\n      .attr('transform', `translate(0,${height})`)\n      .call(\n        d3\n          .axisBottom(xAxis)\n          .ticks(cleanedData.length * multiplierGrid)\n          .tickFormat((d, i) => bottomAxiosValidation(d, i))\n          .tickSize(-height)\n          .tickPadding(22),\n      )\n\n    // TODO: hide last 2 columns of background grid\n    const allTicks = d3.selectAll('.tick')\n    allTicks.attr('opacity', (_a, i) =>\n      i === allTicks.size() - 1 || i === allTicks.size() - 2 ? 0 : 1,\n    )\n\n    // moving X axios labels under the center of Bar\n    svg.selectAll('text').attr('x', barWidth / 2)\n\n    // roll back all changes for Y axios labels\n    yTicks.attr('opacity', '1')\n    yTicks.selectAll('text').attr('x', -10)\n\n    // bars\n    svg\n      .selectAll('.bar')\n      .data(cleanedData)\n      .enter()\n      .append('rect')\n      .attr('class', cx('bar-chart-bar', classNames?.bar))\n      .attr('x', (d) => xAxis(d.index))\n      .attr('width', barWidth)\n      // set minimal height for Bar\n      .attr('y', (d) =>\n        d.y && height - yAxis(d.y) < minBarHeight\n          ? height - minBarHeight\n          : yAxis(d.y),\n      )\n      .attr('height', (d) => {\n        const initialHeight = height - yAxis(d.y)\n        return initialHeight && initialHeight < minBarHeight\n          ? minBarHeight\n          : initialHeight\n      })\n      .attr('data-testid', (d) => `bar-${d.x}-${d.y}`)\n      .on('mouseenter mousemove', (event, d) => {\n        tooltip.style('opacity', 1)\n        tooltip\n          .html(tooltipValidation(d.y, d.index))\n          .style('left', `${event.pageX + 16}px`)\n          .style('top', `${event.pageY + 16}px`)\n          .attr('data-testid', 'bar-tooltip')\n      })\n      .on('mouseleave', () => {\n        tooltip.style('opacity', 0)\n      })\n\n    return () => {\n      tooltip.remove()\n    }\n  }, [data, width, height])\n\n  if (!data.length) {\n    return null\n  }\n\n  return (\n    <>\n      <TooltipGlobalStyles />\n      <Wrapper style={{ width: propWidth, height: propHeight }}>\n        <StyledSVG ref={svgRef} />\n      </Wrapper>\n    </>\n  )\n}\n\nexport default BarChart\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/bar-chart/index.ts",
    "content": "import BarChart from './BarChart'\n\nexport * from './BarChart'\n\nexport default BarChart\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx",
    "content": "import { sumBy } from 'lodash'\nimport React from 'react'\nimport { getPercentage } from 'uiSrc/utils/numbers'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport DonutChart, { ChartData } from './DonutChart'\n\nconst mockData: ChartData[] = [\n  { value: 1, name: 'A', color: [0, 0, 0] },\n  { value: 5, name: 'B', color: [10, 10, 10] },\n  { value: 10, name: 'C', color: [20, 20, 20] },\n  { value: 2, name: 'D', color: [30, 30, 30] },\n  { value: 30, name: 'E', color: [40, 40, 40] },\n  { value: 15, name: 'F', color: [50, 50, 50] },\n]\n\nconst sum = sumBy(mockData, 'value')\n\ndescribe('DonutChart', () => {\n  it('should render with empty data', () => {\n    expect(render(<DonutChart data={[]} />)).toBeTruthy()\n  })\n\n  it('should render with data', () => {\n    expect(render(<DonutChart data={mockData} />)).toBeTruthy()\n  })\n\n  it('should not render donut with empty data', () => {\n    const { container } = render(<DonutChart data={[]} name=\"test\" />)\n    expect(container).toBeEmptyDOMElement()\n  })\n\n  it('should not render donut with 0 values', () => {\n    const mockData: ChartData[] = [\n      { value: 0, name: 'A', color: [0, 0, 0] },\n      { value: 0, name: 'B', color: [10, 10, 10] },\n    ]\n    const { container } = render(<DonutChart data={mockData} name=\"test\" />)\n    expect(container).toBeEmptyDOMElement()\n  })\n\n  it('should render svg', () => {\n    render(<DonutChart data={mockData} name=\"test\" />)\n    expect(screen.getByTestId('donut-test')).toBeInTheDocument()\n  })\n\n  it('should render arcs and labels', () => {\n    render(<DonutChart data={mockData} />)\n    mockData.forEach(({ value, name }) => {\n      expect(screen.getByTestId(`arc-${name}-${value}`)).toBeInTheDocument()\n      expect(screen.getByTestId(`label-${name}-${value}`)).toBeInTheDocument()\n    })\n  })\n\n  it('should not render arcs and labels with 0 value', () => {\n    const mockData: ChartData[] = [\n      { value: 0, name: 'A', color: [0, 0, 0] },\n      { value: 10, name: 'B', color: [10, 10, 10] },\n    ]\n    render(<DonutChart data={mockData} />)\n    expect(screen.queryByTestId('arc-A-0')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('label-A-0')).not.toBeInTheDocument()\n  })\n\n  it('should do not render label value if value less than 5%', () => {\n    render(<DonutChart data={mockData} config={{ percentToShowLabel: 5 }} />)\n    expect(screen.getByTestId('label-A-1')).toHaveTextContent('')\n  })\n\n  it('should render label value if value more than 5%', () => {\n    render(<DonutChart data={mockData} config={{ percentToShowLabel: 5 }} />)\n    expect(screen.getByTestId('label-E-30')).toHaveTextContent('E: 30')\n  })\n\n  it('should render label value without title', () => {\n    render(\n      <DonutChart\n        data={mockData}\n        config={{ percentToShowLabel: 5 }}\n        hideLabelTitle\n      />,\n    )\n    expect(screen.getByTestId('label-E-30').textContent).toBe('30')\n  })\n\n  it('should call render tooltip and label methods', () => {\n    const renderLabel = jest.fn()\n    const renderTooltip = jest.fn()\n    render(\n      <DonutChart\n        data={mockData}\n        renderLabel={renderLabel}\n        renderTooltip={renderTooltip}\n        config={{ percentToShowLabel: 0 }}\n      />,\n    )\n    expect(renderLabel).toBeCalledTimes(mockData.length)\n\n    fireEvent.mouseEnter(screen.getByTestId('arc-A-1'))\n    expect(renderTooltip).toBeCalledWith(mockData[0])\n  })\n\n  it('should render provided tooltip', () => {\n    const renderTooltip = () => <span data-testid=\"label\" />\n\n    render(<DonutChart data={mockData} renderTooltip={renderTooltip} />)\n\n    fireEvent.mouseEnter(screen.getByTestId('arc-A-1'))\n    expect(screen.getByTestId('label')).toBeInTheDocument()\n  })\n\n  it('should set tooltip as visible on hover and hidden on leave', () => {\n    render(<DonutChart data={mockData} />)\n\n    fireEvent.mouseEnter(screen.getByTestId('arc-A-1'))\n    expect(screen.getByTestId('chart-value-tooltip')).toBeVisible()\n\n    fireEvent.mouseLeave(screen.getByTestId('arc-A-1'))\n    expect(screen.getByTestId('chart-value-tooltip')).not.toBeVisible()\n  })\n\n  it('should display values with percentage', () => {\n    render(\n      <DonutChart\n        data={mockData}\n        labelAs=\"percentage\"\n        config={{ percentToShowLabel: 0 }}\n      />,\n    )\n\n    mockData.forEach(({ value, name }) => {\n      expect(screen.getByTestId(`label-${name}-${value}`)).toHaveTextContent(\n        `: ${getPercentage(value, sum)}%`,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/donut-chart/DonutChart.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport DonutChart from './index'\n\nconst donutChartMeta: Meta<typeof DonutChart> = {\n  component: DonutChart,\n  args: {\n    width: 400,\n    height: 200,\n    name: 'test',\n    data: [\n      { value: 1, name: 'A', color: [231, 76, 60] }, // Red\n      { value: 5, name: 'B', color: [52, 152, 219] }, // Blue\n      { value: 10, name: 'C', color: [46, 204, 113] }, // Green\n      { value: 2, name: 'D', color: [241, 196, 15] }, // Yellow\n      { value: 30, name: 'E', color: [155, 89, 182] }, // Purple\n      { value: 15, name: 'F', color: [230, 126, 34] }, // Orange\n    ],\n  },\n}\n\nexport default donutChartMeta\n\ntype Story = StoryObj<typeof donutChartMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/donut-chart/DonutChart.styles.ts",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Wrapper = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  position: relative;\n`\n\nexport const InnerTextContainer = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n`\n\nexport const Tooltip = styled.div<\n  React.HTMLAttributes<HTMLDivElement> & {\n    ref?: React.Ref<HTMLDivElement>\n    theme: Theme\n  }\n>`\n  position: fixed;\n  background: ${({ theme }) => theme.semantic.color.background.neutral500};\n  color: ${({ theme }) => theme.semantic.color.text.primary600};\n  padding: ${({ theme }) => theme.core.space.space100};\n  visibility: hidden;\n  border-radius: ${({ theme }) => theme.core.space.space050};\n  z-index: 100;\n`\n\n// SVG elements styled for d3 manipulation\n// Using data attributes for styling since d3 dynamically creates these elements\nexport const StyledSVG = styled.svg<\n  React.SVGProps<SVGSVGElement> & {\n    ref?: React.Ref<SVGSVGElement>\n    theme: Theme\n  }\n>`\n  & .donut-arc {\n    stroke: ${({ theme }) => theme.semantic.color.border.neutral200};\n    stroke-width: 2px;\n    cursor: pointer;\n  }\n\n  & .donut-label {\n    fill: ${({ theme }) => theme.semantic.color.text.primary600};\n    font-size: 12px;\n    font-weight: bold;\n    letter-spacing: -0.12px !important;\n\n    .donut-label-value {\n      font-weight: normal;\n    }\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx",
    "content": "import cx from 'classnames'\nimport * as d3 from 'd3'\nimport { isString, sumBy } from 'lodash'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { flushSync } from 'react-dom'\nimport { Nullable, truncateNumberToRange } from 'uiSrc/utils'\nimport { rgb, RGBColor } from 'uiSrc/utils/colors'\nimport { getPercentage } from 'uiSrc/utils/numbers'\n\nimport {\n  Wrapper,\n  InnerTextContainer,\n  Tooltip,\n  StyledSVG,\n} from './DonutChart.styles'\n\nexport interface ChartData {\n  value: number\n  name: string\n  color: RGBColor | string\n  meta?: {\n    [key: string]: any\n  }\n}\n\nexport interface IProps {\n  name?: string\n  data: ChartData[]\n  width?: number\n  height?: number\n  title?: React.ReactElement | string\n  config?: {\n    percentToShowLabel?: number\n    arcWidth?: number\n    margin?: number\n    radius?: number\n  }\n  classNames?: {\n    chart?: string\n    arc?: string\n    arcLabel?: string\n    arcLabelValue?: string\n    tooltip?: string\n  }\n  renderLabel?: (data: ChartData) => string\n  renderTooltip?: (data: ChartData) => React.ReactElement | string\n  labelAs?: 'value' | 'percentage'\n  hideLabelTitle?: boolean\n}\n\nconst ANIMATION_DURATION_MS = 100\n\nconst DonutChart = (props: IProps) => {\n  const {\n    name = '',\n    data,\n    width = 380,\n    height = 300,\n    title,\n    config,\n    classNames,\n    labelAs = 'value',\n    renderLabel,\n    renderTooltip,\n    hideLabelTitle = false,\n  } = props\n\n  const margin = config?.margin || 98\n  const radius = config?.radius || width / 2 - margin\n  const arcWidth = config?.arcWidth || 8\n  const percentToShowLabel = config?.percentToShowLabel ?? 5\n\n  const [hoveredData, setHoveredData] = useState<Nullable<ChartData>>(null)\n  const svgRef = useRef<SVGSVGElement>(null)\n  const tooltipRef = useRef<HTMLDivElement>(null)\n  const sum = sumBy(data, 'value')\n\n  const arc = d3\n    .arc<d3.PieArcDatum<ChartData>>()\n    .outerRadius(radius)\n    .innerRadius(radius - arcWidth)\n\n  const arcHover = d3\n    .arc<d3.PieArcDatum<ChartData>>()\n    .outerRadius(radius + 4)\n    .innerRadius(radius - arcWidth)\n\n  const onMouseEnterSlice = (e: MouseEvent, d: d3.PieArcDatum<ChartData>) => {\n    d3.select<SVGPathElement, d3.PieArcDatum<ChartData>>(\n      e.target as SVGPathElement,\n    )\n      .transition()\n      .duration(ANIMATION_DURATION_MS)\n      .attr('d', arcHover)\n\n    if (!tooltipRef.current) {\n      return\n    }\n\n    // calculate position after tooltip rendering (do update as synchronous operation)\n    if (e.type === 'mouseenter') {\n      flushSync(() => {\n        setHoveredData(d.data)\n      })\n    }\n\n    tooltipRef.current.style.top = `${e.pageY + 15}px`\n    tooltipRef.current.style.left =\n      window.innerWidth < tooltipRef.current.scrollWidth + e.pageX + 20\n        ? `${e.pageX - tooltipRef.current.scrollWidth - 15}px`\n        : `${e.pageX + 15}px`\n    tooltipRef.current.style.visibility = 'visible'\n  }\n\n  const onMouseLeaveSlice = (e: MouseEvent) => {\n    d3.select<SVGPathElement, d3.PieArcDatum<ChartData>>(\n      e.target as SVGPathElement,\n    )\n      .transition()\n      .duration(ANIMATION_DURATION_MS)\n      .attr('d', arc)\n\n    if (tooltipRef.current) {\n      tooltipRef.current.style.visibility = 'hidden'\n      setHoveredData(null)\n    }\n  }\n\n  const isShowLabel = (d: d3.PieArcDatum<ChartData>) =>\n    percentToShowLabel > 0\n      ? d.endAngle - d.startAngle > (Math.PI * 2) / (100 / percentToShowLabel)\n      : true\n\n  const getLabelPosition = (d: d3.PieArcDatum<ChartData>) => {\n    const [x, y] = arc.centroid(d)\n    const h = Math.sqrt(x * x + y * y)\n    return `translate(${(x / h) * (radius + 12)}, ${((y + 4) / h) * (radius + 12)})`\n  }\n\n  useEffect(() => {\n    d3.select(svgRef.current)\n      .attr('width', width)\n      .attr('height', height)\n      .select('g')\n      .attr('transform', `translate(${width / 2},${height / 2})`)\n  }, [height, width])\n\n  useEffect(() => {\n    const pie = d3\n      .pie<ChartData>()\n      .value((d: ChartData) => d.value)\n      .sort(null)\n    const dataReady = pie(data.filter((d) => d.value !== 0))\n\n    d3.select(svgRef.current).select('g').remove()\n\n    const svgElement = svgRef.current\n    const existingClasses = svgElement?.getAttribute('class') || ''\n\n    const svg = d3\n      .select(svgRef.current)\n      .attr('width', width)\n      .attr('height', height)\n      .attr('data-testid', `donut-svg-${name}`)\n      .attr('class', cx(existingClasses, classNames?.chart))\n      .append('g')\n      .attr('transform', `translate(${width / 2},${height / 2})`)\n\n    // add arcs\n    svg\n      .selectAll()\n      .data(dataReady)\n      .enter()\n      .append('path')\n      .attr('data-testid', (d) => `arc-${d.data.name}-${d.data.value}`)\n      .attr('d', arc)\n      .attr('fill', (d) =>\n        isString(d.data.color) ? d.data.color : rgb(d.data.color),\n      )\n      .attr('class', cx('donut-arc', classNames?.arc))\n      .on('mouseenter mousemove', onMouseEnterSlice)\n      .on('mouseleave', onMouseLeaveSlice)\n\n    // add labels\n    svg\n      .selectAll()\n      .data(dataReady)\n      .enter()\n      .append('text')\n      .attr('class', cx('donut-label', classNames?.arcLabel))\n      .attr('transform', getLabelPosition)\n      .text((d) =>\n        isShowLabel(d) && !hideLabelTitle ? `${d.data.name}: ` : '',\n      )\n      .attr('data-testid', (d) => `label-${d.data.name}-${d.data.value}`)\n      .style('text-anchor', (d) =>\n        (d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start',\n      )\n      .on('mouseenter mousemove', onMouseEnterSlice)\n      .on('mouseleave', onMouseLeaveSlice)\n      .append('tspan')\n      .text((d) => {\n        if (!isShowLabel(d)) {\n          return ''\n        }\n\n        if (renderLabel) {\n          return renderLabel(d.data)\n        }\n\n        if (labelAs === 'percentage') {\n          return `${getPercentage(d.value, sum)}%`\n        }\n\n        return truncateNumberToRange(d.value)\n      })\n      .attr('class', cx('donut-label-value', classNames?.arcLabelValue))\n  }, [data, hideLabelTitle])\n\n  if (!data.length || sum === 0) {\n    return null\n  }\n\n  return (\n    <Wrapper data-testid={`donut-${name}`}>\n      <StyledSVG ref={svgRef} />\n      <Tooltip\n        className={cx(classNames?.tooltip)}\n        data-testid=\"chart-value-tooltip\"\n        ref={tooltipRef}\n      >\n        {renderTooltip && hoveredData\n          ? renderTooltip(hoveredData)\n          : hoveredData?.value || ''}\n      </Tooltip>\n      {title && <InnerTextContainer>{title}</InnerTextContainer>}\n    </Wrapper>\n  )\n}\n\nexport default DonutChart\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/donut-chart/index.ts",
    "content": "import DonutChart from './DonutChart'\n\nexport default DonutChart\n"
  },
  {
    "path": "redisinsight/ui/src/components/charts/index.ts",
    "content": "import DonutChart from './donut-chart'\nimport BarChart from './bar-chart'\n\nexport { DonutChart, BarChart }\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/Cli/Cli.spec.tsx",
    "content": "import React from 'react'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\nimport { fireEvent, render } from 'uiSrc/utils/test-utils'\nimport CLI from './Cli'\n\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\ndescribe('CLI', () => {\n  it('should render', () => {\n    expect(render(<CLI />)).toBeTruthy()\n  })\n  it('on \"Esc\" key should focus to \"close-cli\" button', () => {\n    const { getByTestId } = render(<CLI />)\n\n    fireEvent.keyDown(getByTestId('cli-command'), { key: keys.ESCAPE })\n\n    expect(getByTestId('close-cli')).toHaveFocus()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/Cli/Cli.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport Cli from './Cli'\n\nconst cliMeta = {\n  component: Cli,\n} satisfies Meta<typeof Cli>\n\nexport default cliMeta\n\ntype Story = StoryObj<typeof cliMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/Cli/Cli.tsx",
    "content": "import React from 'react'\n\nimport CliHeader from 'uiSrc/components/cli/components/cli-header'\nimport CliBodyWrapper from 'uiSrc/components/cli/components/cli-body'\nimport styles from './styles.module.scss'\n\nconst CLI = () => (\n  <div className={styles.container} data-testid=\"cli\">\n    <div className={styles.main}>\n      <CliHeader />\n      <CliBodyWrapper />\n    </div>\n  </div>\n)\n\nexport default CLI\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/Cli/index.ts",
    "content": "import Cli from './Cli'\n\nexport default Cli\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/Cli/styles.module.scss",
    "content": ".container {\n  height: 100%;\n  width: 100%;\n  min-width: 230px;\n}\n\n.main {\n  @include eui.scrollBar;\n  box-sizing: border-box;\n  height: 100%;\n  width: 100%;\n  position: relative;\n  background-color: var(--browserTableRowEven);\n  border-left: 1px solid var(--euiColorLightShade);\n  border-right: 1px solid var(--euiColorLightShade);\n  border-top: 1px solid var(--euiColorLightShade);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/CliWrapper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { InitOutputText } from 'uiSrc/components/messages/cli-output/cliOutput'\nimport { concatToOutput } from 'uiSrc/slices/cli/cli-output'\nimport { setCliEnteringCommand } from 'uiSrc/slices/cli/cli-settings'\nimport {\n  cleanup,\n  clearStoreActions,\n  mockedStore,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport CliWrapper from './CliWrapper'\n\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\nlet mathRandom: jest.SpyInstance<number>\nconst random = 0.91911\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  const { MOCK_COMMANDS_SPEC, MOCK_COMMANDS_ARRAY } =\n    jest.requireActual('uiSrc/constants')\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      spec: MOCK_COMMANDS_SPEC,\n      commandsArray: Object.keys(MOCK_COMMANDS_ARRAY).sort(),\n    }),\n  }\n})\n\ndescribe('CliWrapper', () => {\n  beforeAll(() => {\n    mathRandom = jest.spyOn(Math, 'random').mockImplementation(() => random)\n  })\n\n  afterAll(() => {\n    mathRandom.mockRestore()\n  })\n\n  it('should render', () => {\n    expect(render(<CliWrapper />)).toBeTruthy()\n  })\n  it('Actions should be called after component will unmount', () => {\n    const { unmount } = render(<CliWrapper />)\n\n    unmount()\n\n    const handleWorkbenchClick = () => {}\n\n    const expectedActions = [\n      concatToOutput(InitOutputText('', 0, 0, true, handleWorkbenchClick)),\n      setCliEnteringCommand(),\n    ]\n    expect(clearStoreActions(store.getActions().slice(-2))).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/CliWrapper.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliWrapper from './CliWrapper'\n\nconst cliWrapperMeta = {\n  component: CliWrapper,\n} satisfies Meta<typeof CliWrapper>\n\nexport default cliWrapperMeta\n\ntype Story = StoryObj<typeof cliWrapperMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/CliWrapper.tsx",
    "content": "import React from 'react'\n\nimport Cli from './Cli'\n\nconst CliWrapper = () => <Cli />\n\nexport default CliWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.spec.tsx",
    "content": "import { cloneDeep, last } from 'lodash'\nimport React from 'react'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { clearOutput, updateCliHistoryStorage } from 'uiSrc/utils/cliHelper'\nimport { MOCK_COMMANDS_ARRAY } from 'uiSrc/constants'\nimport CliBody, { Props } from './CliBody'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nconst commandHistory = ['info', 'hello', 'keys *', 'clear']\nconst commandsArr = MOCK_COMMANDS_ARRAY\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\nconst cliOutputPath = 'uiSrc/slices/cli/cli-output'\nconst cliCommand = 'cli-command'\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock(cliOutputPath, () => {\n  const defaultState = jest.requireActual(cliOutputPath).initialState\n  return {\n    ...jest.requireActual(cliOutputPath),\n    setOutputInitialState: jest.fn,\n    outputSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      commandHistory,\n    }),\n  }\n})\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  const { MOCK_COMMANDS_SPEC, MOCK_COMMANDS_ARRAY } =\n    jest.requireActual('uiSrc/constants')\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      spec: MOCK_COMMANDS_SPEC,\n      commandsArray: MOCK_COMMANDS_ARRAY,\n    }),\n  }\n})\n\njest.mock('uiSrc/utils/cliHelper', () => ({\n  ...jest.requireActual('uiSrc/utils/cliHelper'),\n  updateCliHistoryStorage: jest.fn(),\n  clearOutput: jest.fn(),\n}))\n\ndescribe('CliBody', () => {\n  it('should render', () => {\n    expect(render(<CliBody {...instance(mockedProps)} />)).toBeTruthy()\n  })\n  it('Input should render without error', () => {\n    render(<CliBody {...instance(mockedProps)} />)\n\n    const cliInput = screen.queryByTestId(cliCommand)\n\n    expect(cliInput).toBeInTheDocument()\n  })\n\n  it('Input should not render with error', () => {\n    render(<CliBody {...instance(mockedProps)} error=\"error\" />)\n\n    const cliInput = screen.queryByTestId(cliCommand)\n\n    expect(cliInput).toBeNull()\n  })\n\n  describe('CLI input special commands', () => {\n    it('\"clear\" command should call \"setOutputInitialState\"', () => {\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n\n      const command = 'clear'\n\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command={command}\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'Enter',\n      })\n\n      expect(clearOutput).toBeCalled()\n      expect(updateCliHistoryStorage).toBeCalledWith(\n        command,\n        expect.any(Function),\n      )\n\n      expect(setCommandMock).toBeCalledWith('')\n      expect(onSubmitMock).not.toBeCalled()\n    })\n  })\n\n  describe('CLI input keyboard cases', () => {\n    it('\"Enter\" keydown should call \"onSubmit\"', () => {\n      const command = 'info'\n      const onSubmitMock = jest.fn()\n\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command={command}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'Enter',\n      })\n\n      expect(updateCliHistoryStorage).toBeCalledWith(\n        command,\n        expect.any(Function),\n      )\n      expect(onSubmitMock).toBeCalled()\n    })\n\n    it('\"Ctrl+l\" hot key for Windows OS should call \"setOutputInitialState\"', () => {\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command=\"clear\"\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'l',\n        ctrlKey: true,\n      })\n\n      expect(clearOutput).toBeCalled()\n\n      expect(setCommandMock).toBeCalledWith('')\n      expect(onSubmitMock).not.toBeCalled()\n    })\n\n    it('\"Command+k\" hot key for MacOS should call \"setOutputInitialState\"', () => {\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command=\"clear\"\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'k',\n        metaKey: true,\n      })\n\n      expect(clearOutput).toBeCalled()\n\n      expect(setCommandMock).toBeCalledWith('')\n      expect(onSubmitMock).not.toBeCalled()\n    })\n\n    it('\"ArrowUp\" should call \"setCommand\" with commands from history', () => {\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command=\"clear\"\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'ArrowUp',\n      })\n\n      expect(setCommandMock).toBeCalledWith(commandHistory[0])\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'ArrowUp',\n      })\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'ArrowUp',\n      })\n\n      expect(setCommandMock).toBeCalledWith(commandHistory[2])\n\n      expect(onSubmitMock).not.toBeCalled()\n    })\n\n    it('\"ArrowDown\" should call \"setCommand\" with commands from history', () => {\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command=\"clear\"\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      for (let index = 0; index < 3; index++) {\n        fireEvent.keyDown(screen.getByTestId(cliCommand), {\n          key: 'ArrowUp',\n        })\n      }\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: 'ArrowDown',\n      })\n\n      expect(setCommandMock).toBeCalledWith(commandHistory[2])\n\n      for (let index = 0; index < 3; index++) {\n        fireEvent.keyDown(screen.getByTestId(cliCommand), {\n          key: 'ArrowDown',\n        })\n      }\n\n      expect(setCommandMock).toBeCalledWith('')\n      expect(setCommandMock).toBeCalledTimes(6)\n\n      for (let index = 0; index < 2; index++) {\n        fireEvent.keyDown(screen.getByTestId(cliCommand), {\n          key: 'ArrowDown',\n        })\n      }\n\n      expect(setCommandMock).toBeCalledTimes(6)\n\n      expect(onSubmitMock).not.toBeCalled()\n    })\n\n    it('\"Tab\" with command=\"\" should setCommand first command from constants/commands ', () => {\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command=\"\"\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: keys.TAB,\n      })\n\n      expect(setCommandMock).toBeCalledWith(commandsArr[0])\n\n      expect(onSubmitMock).not.toBeCalled()\n    })\n\n    // eslint-disable-next-line max-len\n    it('\"Tab\" with command=\"g\" should setCommand first command starts with \"g\" from constants/commands ', () => {\n      const command = 'g'\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command={command}\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: keys.TAB,\n      })\n\n      expect(setCommandMock).toBeCalledWith(\n        commandsArr.filter((cmd: string) =>\n          cmd.startsWith(command.toUpperCase()),\n        )[0],\n      )\n\n      expect(onSubmitMock).not.toBeCalled()\n    })\n\n    // eslint-disable-next-line max-len\n    it('\"Shift+Tab\" with command=\"g\" should setCommand last command starts with \"g\" from constants/commands ', () => {\n      const command = 'g'\n      const onSubmitMock = jest.fn()\n      const setCommandMock = jest.fn()\n      render(\n        <CliBody\n          {...instance(mockedProps)}\n          command={command}\n          setCommand={setCommandMock}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: keys.TAB,\n        shiftKey: true,\n      })\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: keys.TAB,\n      })\n\n      fireEvent.keyDown(screen.getByTestId(cliCommand), {\n        key: keys.TAB,\n        shiftKey: true,\n      })\n\n      expect(setCommandMock).toBeCalledWith(\n        last(\n          commandsArr.filter((cmd: string) =>\n            cmd.startsWith(command.toUpperCase()),\n          ),\n        ),\n      )\n\n      expect(onSubmitMock).not.toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliBody from './CliBody'\n\nconst cliBodyMeta = {\n  component: CliBody,\n} satisfies Meta<typeof CliBody>\n\nexport default cliBodyMeta\n\ntype Story = StoryObj<typeof cliBodyMeta>\n\nexport const Default: Story = {\n  args: {\n    data: [],\n    command: '',\n    error: '',\n    setCommand: () => {},\n    onSubmit: () => {},\n  },\n}\n\nexport const WithInput: Story = {\n  args: {\n    data: ['test\\n', 'test2\\n', 'test3\\n'],\n    command: 'INFO',\n    error: '',\n    setCommand: () => {},\n    onSubmit: () => {},\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBody/CliBody.tsx",
    "content": "import React, { Ref, useEffect, useRef, useState } from 'react'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Nullable, scrollIntoView } from 'uiSrc/utils'\nimport { isModifiedEvent } from 'uiSrc/services'\nimport { ClearCommand } from 'uiSrc/constants/cliOutput'\nimport { outputSelector } from 'uiSrc/slices/cli/cli-output'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport CliInputWrapper from 'uiSrc/components/cli/components/cli-input'\nimport { clearOutput, updateCliHistoryStorage } from 'uiSrc/utils/cliHelper'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  data: (string | JSX.Element)[]\n  command: string\n  error: string\n  setCommand: (command: string) => void\n  onSubmit: () => void\n}\n\nconst commandTabPosInit = 0\nconst commandHistoryPosInit = -1\nconst TIME_FOR_DOUBLE_CLICK = 300\n\nconst CliBody = (props: Props) => {\n  const { data, command = '', error, setCommand, onSubmit } = props\n\n  const [inputEl, setInputEl] = useState<Nullable<HTMLSpanElement>>(null)\n  const [commandHistory, setCommandHistory] = useState<string[]>([])\n  const [commandHistoryPos, setCommandHistoryPos] = useState<number>(\n    commandHistoryPosInit,\n  )\n  const [commandTabPos, setCommandTabPos] = useState<number>(commandTabPosInit)\n  const [wordsTyped, setWordsTyped] = useState<number>(0)\n  const [matchingCmds, setMatchingCmds] = useState<string[]>([])\n  const { loading: settingsLoading } = useSelector(cliSettingsSelector)\n  const { loading, commandHistory: commandHistoryStore } =\n    useSelector(outputSelector)\n  const { commandsArray } = useSelector(appRedisCommandsSelector)\n\n  const timerClickRef = useRef<NodeJS.Timeout>()\n  const scrollDivRef: Ref<HTMLDivElement> = useRef(null)\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    inputEl?.focus()\n    scrollIntoView(scrollDivRef?.current, {\n      behavior: 'smooth',\n      block: 'nearest',\n      inline: 'end',\n    })\n  }, [command, data, inputEl, scrollDivRef])\n\n  useEffect(() => {\n    setCommandHistory(commandHistoryStore)\n  }, [commandHistoryStore])\n\n  useEffect(() => {\n    if (command) {\n      setWordsTyped(\n        command.trim().match(/(?:'[^']*'|[^\\s'\"]|\"[^\"]*\"|\\[[^\\]]*\\])+/g)\n          ?.length ?? wordsTyped,\n      )\n    }\n  }, [command])\n\n  const onClearOutput = (event: React.KeyboardEvent<HTMLSpanElement>) => {\n    event.preventDefault()\n\n    clearOutput(dispatch)\n    setCommand('')\n  }\n\n  const onKeyDownEnter = (\n    commandLine: string,\n    event: React.KeyboardEvent<HTMLSpanElement>,\n  ) => {\n    event.preventDefault()\n\n    setWordsTyped(0)\n    setCommandHistoryPos(commandHistoryPosInit)\n    updateCliHistoryStorage(commandLine, dispatch)\n\n    if (commandLine === ClearCommand) {\n      onClearOutput(event)\n      return\n    }\n\n    onSubmit()\n  }\n\n  const onKeyDownArrowUp = (event: React.KeyboardEvent<HTMLSpanElement>) => {\n    event.preventDefault()\n    const newPos = commandHistoryPos + 1\n    if (newPos >= commandHistory.length) {\n      return\n    }\n\n    setCommandFromHistory(newPos)\n  }\n\n  const onKeyDownArrowDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {\n    const newPos = commandHistoryPos - 1\n\n    if (commandHistoryPos === commandHistoryPosInit) {\n      event.preventDefault()\n      return\n    }\n\n    setCommandFromHistory(newPos)\n  }\n\n  const onKeyDownTab = (\n    event: React.KeyboardEvent<HTMLSpanElement>,\n    commandLine: string,\n  ) => {\n    event.preventDefault()\n\n    const nextPos =\n      commandTabPos === matchingCmds.length - 1\n        ? commandTabPosInit\n        : commandTabPos + 1\n    let matchingCmdsCurrent = matchingCmds\n\n    if (commandTabPos === commandTabPosInit) {\n      matchingCmdsCurrent = updateMatchingCmds(commandLine)\n    }\n\n    if (matchingCmdsCurrent.length > 1) {\n      setCommand(matchingCmdsCurrent[nextPos])\n      setCommandTabPos(nextPos)\n    }\n  }\n\n  const onKeyDownShiftTab = (event: React.KeyboardEvent<HTMLSpanElement>) => {\n    event.preventDefault()\n\n    let matchingCmdsCurrent = matchingCmds\n\n    if (commandTabPos === commandTabPosInit) {\n      matchingCmdsCurrent = updateMatchingCmds(command)\n    }\n\n    const nextPos = commandTabPos\n      ? commandTabPos - 1\n      : matchingCmdsCurrent.length - 1\n\n    if (!matchingCmdsCurrent.length) {\n      return\n    }\n\n    if (matchingCmdsCurrent.length > 1) {\n      setCommand(matchingCmdsCurrent[nextPos])\n      setCommandTabPos(nextPos)\n    }\n  }\n\n  const onKeyEsc = () => {\n    document.getElementById('close-cli')?.focus()\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {\n    const commandLine = command?.trim()\n\n    const isModifierKey = isModifiedEvent(event)\n\n    if (event.shiftKey && event.key === keys.TAB)\n      return onKeyDownShiftTab(event)\n    if (event.key === keys.TAB) return onKeyDownTab(event, commandLine)\n\n    // reset command tab position\n    if (!event.shiftKey || (event.shiftKey && event.key !== 'Shift')) {\n      setCommandTabPos(commandTabPosInit)\n    }\n\n    if (event.key === keys.ENTER) return onKeyDownEnter(commandLine, event)\n    if (event.key === keys.ARROW_UP && !isModifierKey)\n      return onKeyDownArrowUp(event)\n    if (event.key === keys.ARROW_DOWN && !isModifierKey)\n      return onKeyDownArrowDown(event)\n    if (event.key === keys.ESCAPE) return onKeyEsc()\n\n    if (\n      (event.metaKey && event.key === 'k') ||\n      (event.ctrlKey && event.key === 'l')\n    ) {\n      onClearOutput(event)\n    }\n    return undefined\n  }\n\n  const updateMatchingCmds = (command: string = '') => {\n    const matchingCmdsCurrent = [\n      command,\n      ...commandsArray.filter((cmd: string) =>\n        cmd.startsWith(command.toUpperCase()),\n      ),\n    ]\n\n    setMatchingCmds(matchingCmdsCurrent)\n\n    return matchingCmdsCurrent\n  }\n\n  const setCommandFromHistory = (newPos: number) => {\n    const newCommand = commandHistory[newPos] ?? ''\n\n    setCommand(newCommand)\n    setCommandHistoryPos(newPos)\n\n    setTimeout(() => {\n      inputEl?.focus()\n    })\n  }\n\n  const onMouseUpOutput = () => {\n    if (timerClickRef.current) {\n      clearTimeout(timerClickRef.current)\n      timerClickRef.current = undefined\n      return\n    }\n\n    if (window.getSelection()?.toString()) {\n      return\n    }\n\n    timerClickRef.current = setTimeout(() => {\n      const isInputFocused = document.activeElement === inputEl\n\n      if (!window.getSelection()?.toString() && !isInputFocused) {\n        inputEl?.focus()\n        document.execCommand('selectAll', false)\n        document.getSelection()?.collapseToEnd()\n        timerClickRef.current = undefined\n      }\n    }, TIME_FOR_DOUBLE_CLICK)\n  }\n\n  return (\n    <div\n      className=\"cli-container\"\n      onMouseUp={onMouseUpOutput}\n      onKeyDown={() => {}}\n      role=\"textbox\"\n      tabIndex={0}\n    >\n      <Row justify=\"between\" style={{ height: '100%' }}>\n        <FlexItem grow>\n          <div className={styles.output}>{data}</div>\n          {!error && !(loading || settingsLoading) ? (\n            <span\n              style={{\n                paddingBottom: 5,\n                paddingTop: 17,\n              }}\n            >\n              <CliInputWrapper\n                command={command}\n                setCommand={setCommand}\n                setInputEl={setInputEl}\n                onKeyDown={onKeyDown}\n                wordsTyped={wordsTyped}\n              />\n            </span>\n          ) : (\n            !error && <span>Executing command...</span>\n          )}\n          <div ref={scrollDivRef} />\n        </FlexItem>\n      </Row>\n    </div>\n  )\n}\n\nexport default CliBody\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBody/index.ts",
    "content": "import CliBody from './CliBody'\n\nexport default CliBody\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBody/styles.module.scss",
    "content": ".section {\n  position: absolute;\n  width: 100%;\n  height: calc(100% - 34px);\n  display: flex;\n}\n\n.title {\n  padding-left: 18px;\n}\n\n.output {\n  white-space: pre-wrap;\n}\n\n.input {\n  padding-bottom: 7px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, first } from 'lodash'\nimport { useSelector } from 'react-redux'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  clearStoreActions,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendCliClusterCommandAction } from 'uiSrc/slices/cli/cli-output'\nimport { processCliClient } from 'uiSrc/slices/cli/cli-settings'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { processUnsupportedCommand } from 'uiSrc/utils/cliOutputActions'\n\nimport CliBodyWrapper from './CliBodyWrapper'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '123',\n    connectionType: 'STANDALONE',\n    db: 0,\n  }),\n}))\n\njest.mock('uiSrc/slices/cli/cli-output', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/cli-output'),\n  sendCliClusterCommandAction: jest.fn(),\n  processUnsupportedCommand: jest.fn(),\n  updateCliCommandHistory: jest.fn,\n  concatToOutput: () => jest.fn(),\n}))\n\njest.mock('uiSrc/utils/cliHelper', () => ({\n  ...jest.requireActual('uiSrc/utils/cliHelper'),\n  updateCliHistoryStorage: jest.fn(),\n  clearOutput: jest.fn(),\n  cliParseTextResponse: jest.fn(),\n  cliParseTextResponseWithOffset: jest.fn(),\n}))\n\nconst unsupportedCommands = ['sync', 'subscription']\nconst cliCommandTestId = 'cli-command'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\ndescribe('CliBodyWrapper', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: any) => any) =>\n        callback({\n          ...state,\n          cli: {\n            ...state.cli,\n            settings: { ...state.cli.settings, loading: false },\n          },\n        }),\n    )\n  })\n  it('should render and call process cli client', () => {\n    const expectedActions = [processCliClient()]\n\n    expect(render(<CliBodyWrapper />)).toBeTruthy()\n    expect(\n      clearStoreActions(store.getActions().slice(0, expectedActions.length)),\n    ).toEqual(clearStoreActions(expectedActions))\n  })\n\n  // It's not possible to simulate events on contenteditable with testing-react-library,\n  // or any testing library that uses js - dom, because of a limitation on js - dom itself.\n  // https://github.com/testing-library/dom-testing-library/pull/235\n  it.skip('\"onSubmit\" should check unsupported commands', () => {\n    const processUnsupportedCommandMock = jest.fn()\n\n    ;(processUnsupportedCommand as jest.Mock).mockImplementation(\n      () => processUnsupportedCommandMock,\n    )\n\n    render(<CliBodyWrapper />)\n\n    // Act\n    fireEvent.change(screen.getByTestId(cliCommandTestId), {\n      target: { value: first(unsupportedCommands) },\n    })\n\n    // Act\n    fireEvent.keyDown(screen.getByTestId(cliCommandTestId), {\n      key: 'Enter',\n    })\n\n    expect(processUnsupportedCommandMock).toBeCalled()\n  })\n\n  it('\"onSubmit\" for Cluster connection should call \"sendCliClusterCommandAction\"', () => {\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      id: '123',\n      connectionType: 'CLUSTER',\n      db: 0,\n    }))\n\n    const sendCliClusterActionMock = jest.fn()\n\n    ;(sendCliClusterCommandAction as jest.Mock).mockImplementation(\n      () => sendCliClusterActionMock,\n    )\n\n    render(<CliBodyWrapper />)\n\n    // Act\n    fireEvent.keyDown(screen.getByTestId(cliCommandTestId), {\n      key: 'Enter',\n    })\n\n    expect(sendCliClusterActionMock).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliBodyWrapper from './CliBodyWrapper'\n\nconst cliBodyWrapperMeta = {\n  component: CliBodyWrapper,\n} satisfies Meta<typeof CliBodyWrapper>\n\nexport default cliBodyWrapperMeta\n\ntype Story = StoryObj<typeof cliBodyWrapperMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx",
    "content": "import { decode } from 'html-entities'\nimport React, { useEffect, useState } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { useHistory, useParams } from 'react-router-dom'\n\nimport {\n  cliSettingsSelector,\n  createCliClientAction,\n  setCliEnteringCommand,\n  clearSearchingCommand,\n  toggleCli,\n} from 'uiSrc/slices/cli/cli-settings'\nimport {\n  concatToOutput,\n  outputSelector,\n  sendCliCommandAction,\n  sendCliClusterCommandAction,\n} from 'uiSrc/slices/cli/cli-output'\nimport {\n  CommandMonitor,\n  CommandPSubscribe,\n  CommandSubscribe,\n  CommandHello3,\n  Pages,\n} from 'uiSrc/constants'\nimport { getCommandRepeat, isRepeatCountCorrect } from 'uiSrc/utils'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  checkUnsupportedCommand,\n  clearOutput,\n  cliCommandOutput,\n} from 'uiSrc/utils/cliHelper'\nimport {\n  cliTexts,\n  ConnectionSuccessOutputText,\n  InitOutputText,\n} from 'uiSrc/components/messages/cli-output/cliOutput'\nimport { showMonitor } from 'uiSrc/slices/cli/monitor'\nimport {\n  cliCommandError,\n  processUnrepeatableNumber,\n  processUnsupportedCommand,\n} from 'uiSrc/utils/cliOutputActions'\nimport CliBody from './CliBody'\n\nimport styles from './CliBody/styles.module.scss'\n\nconst CliBodyWrapper = () => {\n  const [command, setCommand] = useState('')\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { data = [] } = useSelector(outputSelector)\n  const {\n    errorClient: error,\n    unsupportedCommands,\n    isEnteringCommand,\n    isSearching,\n    matchedCommand,\n    cliClientUuid,\n  } = useSelector(cliSettingsSelector)\n  const { connectionType, host, port, db } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { db: currentDbIndex } = useSelector(outputSelector)\n\n  useEffect(() => {\n    if (!cliClientUuid) {\n      dispatch(\n        createCliClientAction(\n          instanceId,\n          () => {\n            dispatch(concatToOutput(ConnectionSuccessOutputText))\n          },\n          (errorMessage: string) => {\n            dispatch(concatToOutput(cliTexts.CLI_ERROR_MESSAGE(errorMessage)))\n          },\n        ),\n      )\n      dispatch(\n        concatToOutput(\n          InitOutputText(host, port, db, !data.length, handleWorkbenchClick),\n        ),\n      )\n    }\n  }, [])\n\n  useEffect(() => {\n    if (!isEnteringCommand) {\n      dispatch(setCliEnteringCommand())\n    }\n    if (isSearching && matchedCommand) {\n      dispatch(clearSearchingCommand())\n    }\n  }, [command])\n\n  const handleClearOutput = () => {\n    clearOutput(dispatch)\n  }\n\n  const handleWorkbenchClick = () => {\n    dispatch(toggleCli())\n    history.push(Pages.workbench(instanceId))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLI_WORKBENCH_LINK_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const refHotkeys = useHotkeys<HTMLDivElement>(\n    'command+k,ctrl+l',\n    handleClearOutput,\n  )\n\n  const handleSubmit = () => {\n    const [commandLine, countRepeat] = getCommandRepeat(\n      decode(command).trim() || '',\n    )\n    const unsupportedCommand = checkUnsupportedCommand(\n      unsupportedCommands,\n      commandLine,\n    )\n    dispatch(concatToOutput(cliCommandOutput(decode(command), currentDbIndex)))\n\n    if (!isRepeatCountCorrect(countRepeat)) {\n      processUnrepeatableNumber(commandLine, resetCommand)\n      return\n    }\n\n    // Flow if MONITOR command was executed\n    if (checkUnsupportedCommand([CommandMonitor.toLowerCase()], commandLine)) {\n      dispatch(\n        concatToOutput([\n          cliTexts.MONITOR_COMMAND(() => {\n            dispatch(showMonitor())\n          }),\n          '\\n',\n        ]),\n      )\n      resetCommand()\n      return\n    }\n\n    // Flow if PSUBSCRIBE command was executed\n    if (\n      checkUnsupportedCommand([CommandPSubscribe.toLowerCase()], commandLine)\n    ) {\n      dispatch(\n        concatToOutput(\n          cliTexts.PSUBSCRIBE_COMMAND_CLI(Pages.pubSub(instanceId)),\n        ),\n      )\n      resetCommand()\n      return\n    }\n\n    // Flow if SUBSCRIBE command was executed\n    if (\n      checkUnsupportedCommand([CommandSubscribe.toLowerCase()], commandLine)\n    ) {\n      dispatch(\n        concatToOutput([\n          cliTexts.SUBSCRIBE_COMMAND_CLI(Pages.pubSub(instanceId)),\n          '\\n',\n        ]),\n      )\n      resetCommand()\n      return\n    }\n\n    // Flow if HELLO 3 command was executed\n    if (checkUnsupportedCommand([CommandHello3.toLowerCase()], commandLine)) {\n      dispatch(concatToOutput(cliTexts.HELLO3_COMMAND_CLI()))\n      resetCommand()\n      return\n    }\n\n    if (unsupportedCommand) {\n      processUnsupportedCommand(commandLine, unsupportedCommand, resetCommand)\n      return\n    }\n\n    for (let i = 0; i < countRepeat; i++) {\n      sendCommand(commandLine)\n    }\n  }\n\n  const sendCommand = (command: string) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLI_COMMAND_SUBMITTED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    if (connectionType !== ConnectionType.Cluster) {\n      dispatch(\n        sendCliCommandAction(command, resetCommand, (error) =>\n          cliCommandError(error, command),\n        ),\n      )\n      return\n    }\n\n    dispatch(\n      sendCliClusterCommandAction(command, resetCommand, (error) =>\n        cliCommandError(error, command),\n      ),\n    )\n  }\n\n  const resetCommand = () => {\n    setCommand('')\n  }\n\n  return (\n    <section ref={refHotkeys} className={styles.section}>\n      <CliBody\n        data={data}\n        command={command}\n        error={error}\n        setCommand={setCommand}\n        onSubmit={handleSubmit}\n      />\n    </section>\n  )\n}\n\nexport default CliBodyWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-body/index.ts",
    "content": "import CliBodyWrapper from './CliBodyWrapper'\n\nexport default CliBodyWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-header/CliHeader.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport {\n  cleanup,\n  fireEvent,\n  sessionStorageMock,\n  mockedStore,\n  render,\n  screen,\n  act,\n} from 'uiSrc/utils/test-utils'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  processCliClient,\n  resetCliSettings,\n  toggleCli,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { sessionStorageService } from 'uiSrc/services'\nimport { resetOutputLoading } from 'uiSrc/slices/cli/cli-output'\nimport CliHeader from './CliHeader'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    host: 'localhost',\n    port: 6379,\n  }),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('CliHeader', () => {\n  it('should render', () => {\n    expect(render(<CliHeader />)).toBeTruthy()\n  })\n\n  it('should \"resetCliSettings\" action be called after click \"close-cli\" button', () => {\n    render(<CliHeader />)\n    fireEvent.click(screen.getByTestId('close-cli'))\n\n    const expectedActions = [resetCliSettings(), resetOutputLoading()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should \"resetCliSettings\" action be called after click \"close-cli\" button', async () => {\n    const mockUuid = 'test-uuid'\n    sessionStorageMock.getItem = jest.fn().mockReturnValue(mockUuid)\n\n    render(<CliHeader />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('close-cli'))\n    })\n\n    const expectedActions = [resetCliSettings(), resetOutputLoading()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should \"toggleCli\" action be called after click \"hide-cli\" button', () => {\n    render(<CliHeader />)\n    fireEvent.click(screen.getByTestId('hide-cli'))\n\n    const expectedActions = [toggleCli()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should \"toggleCli\" action be called after click \"hide-cli\" button', async () => {\n    const mockUuid = 'test-uuid'\n    sessionStorageMock.getItem = jest.fn().mockReturnValue(mockUuid)\n\n    render(<CliHeader />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('hide-cli'))\n    })\n\n    const expectedActions = [toggleCli()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n\nit('should \"processCliClient\" action be called after close cli with mocked sessionStorage item ', async () => {\n  const mockUuid = 'test-uuid'\n  sessionStorageService.get = jest.fn().mockReturnValue(mockUuid)\n\n  render(<CliHeader />)\n\n  await act(() => {\n    fireEvent.click(screen.getByTestId('close-cli'))\n  })\n\n  expect(sessionStorageService.get).toBeCalledWith(\n    BrowserStorageItem.cliClientUuid,\n  )\n\n  const expectedActions = [\n    processCliClient(),\n    resetCliSettings(),\n    resetOutputLoading(),\n  ]\n  expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n    expectedActions,\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-header/CliHeader.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliHeader from './CliHeader'\n\nconst cliHeaderMeta = {\n  component: CliHeader,\n} satisfies Meta<typeof CliHeader>\n\nexport default cliHeaderMeta\n\ntype Story = StoryObj<typeof cliHeaderMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-header/CliHeader.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  toggleCli,\n  resetCliSettings,\n  deleteCliClientAction,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { sessionStorageService } from 'uiSrc/services'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { resetOutputLoading } from 'uiSrc/slices/cli/cli-output'\nimport { OnboardingTour } from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { WindowControlGroup } from 'uiSrc/components/base/shared/WindowControlGroup'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nconst CliHeader = () => {\n  const dispatch = useDispatch()\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const removeCliClient = () => {\n    const cliClientUuid =\n      sessionStorageService.get(BrowserStorageItem.cliClientUuid) ?? ''\n\n    cliClientUuid && dispatch(deleteCliClientAction(instanceId, cliClientUuid))\n  }\n\n  useEffect(() => {\n    window.addEventListener('beforeunload', removeCliClient, false)\n    return () => {\n      window.removeEventListener('beforeunload', removeCliClient, false)\n    }\n  }, [])\n\n  const handleCloseCli = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLI_CLOSED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    removeCliClient()\n    dispatch(resetCliSettings())\n    dispatch(resetOutputLoading())\n  }\n\n  const handleHideCli = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLI_MINIMIZED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    dispatch(toggleCli())\n  }\n\n  return (\n    <div className={styles.container} id=\"cli-header\">\n      <Row justify=\"between\" align=\"center\" style={{ height: '100%' }}>\n        <FlexItem className={styles.title} direction=\"row\">\n          <RiIcon type=\"CliIcon\" size=\"M\" />\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.BROWSER_CLI}\n            anchorPosition=\"upLeft\"\n            panelClassName={styles.cliOnboardPanel}\n          >\n            <Text>CLI</Text>\n          </OnboardingTour>\n        </FlexItem>\n        <FlexItem grow />\n        <WindowControlGroup\n          onClose={handleCloseCli}\n          onHide={handleHideCli}\n          id=\"cli\"\n        />\n      </Row>\n    </div>\n  )\n}\n\nexport default CliHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-header/index.ts",
    "content": "import CliHeader from './CliHeader'\n\nexport default CliHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-header/styles.module.scss",
    "content": ".container {\n  height: 34px;\n  line-height: 34px;\n  width: 100%;\n  overflow: hidden;\n  background-color: var(--euiPageBackgroundColor);\n\n  padding: 0 10px 0 16px;\n  z-index: 10;\n}\n\n.icon {\n  margin-left: 5px;\n}\n\n.endpointContainer {\n  cursor: default;\n  font: normal normal normal 12px/15px Graphik, sans-serif !important;\n  letter-spacing: -0.12px;\n  max-width: 210px;\n  display: inline-flex;\n  padding-right: 10px;\n}\n\n.endpoint {\n  font: normal normal normal 12px/15px Graphik, sans-serif !important;\n  letter-spacing: -0.12px;\n  display: inline-block !important;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  padding-left: 5px;\n}\n\n.title {\n  align-items: center;\n  :global {\n    .euiIcon {\n      color: var(--euiColorPrimary);\n      margin-right: 8px;\n    }\n  }\n}\n\n.cliOnboardPanel {\n  margin-top: -4px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliAutocomplete/CliAutocomplete.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  setMatchedCommand,\n  clearSearchingCommand,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport CliAutocomplete, { Props } from './CliAutocomplete'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst CliAutocompleteTestId = 'cli-command-autocomplete'\nconst scanCommand = 'scan'\nconst scanArgs = [\n  {\n    name: 'cursor',\n    type: 'integer',\n  },\n  {\n    token: 'MATCH',\n    name: 'pattern',\n    type: 'pattern',\n    optional: true,\n  },\n  {\n    token: 'COUNT',\n    name: 'count',\n    type: 'integer',\n    optional: true,\n  },\n  {\n    token: 'TYPE',\n    name: 'type',\n    type: 'string',\n    optional: true,\n  },\n]\n\ndescribe('CliAutocomplete', () => {\n  it('should render', () => {\n    expect(render(<CliAutocomplete {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('Autocomplete should not be in the Document with empty array of arguments prop ', () => {\n    const command = 'clear'\n\n    const { queryByTestId } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        commandName={command}\n        arguments={[]}\n      />,\n    )\n\n    const autocompleteComponent = queryByTestId(CliAutocompleteTestId)\n\n    expect(autocompleteComponent).not.toBeInTheDocument()\n  })\n\n  it('Autocomplete should be in Document with \"scan\" command ', () => {\n    const { queryByTestId } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        commandName={scanCommand}\n        arguments={scanArgs}\n      />,\n    )\n\n    const autocompleteComponent = queryByTestId(CliAutocompleteTestId)\n\n    expect(autocompleteComponent).toBeInTheDocument()\n  })\n\n  it('should \"setMatchedCommand\" & \"clearSearchingCommand\" action be called after unmount with empty string', () => {\n    const { unmount } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        commandName={scanCommand}\n        arguments={scanArgs}\n      />,\n    )\n\n    unmount()\n\n    const expectedActions = [setMatchedCommand(''), clearSearchingCommand()]\n    expect(store.getActions().slice(-2)).toEqual(expectedActions)\n  })\n\n  it('Autocomplete should be only with optional args for \"scan\" command with filled in required args (new realization)', () => {\n    const autocompleteOptionalText = '[MATCH pattern] [COUNT count] [TYPE type]'\n    const { queryByTestId } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        provider=\"main\"\n        commandName={scanCommand}\n        arguments={scanArgs}\n        wordsTyped={2}\n      />,\n    )\n\n    const autocompleteComponent = queryByTestId(CliAutocompleteTestId)\n\n    expect(autocompleteOptionalText).toEqual(autocompleteComponent?.textContent)\n  })\n\n  it('Autocomplete should be only with optional args for \"scan\" command with filled in required args (old realization)', () => {\n    const autocompleteOptionalText = '[pattern] [count] [type]'\n    const { queryByTestId } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        provider=\"someprovider\"\n        commandName={scanCommand}\n        arguments={scanArgs}\n        wordsTyped={2}\n      />,\n    )\n\n    const autocompleteComponent = queryByTestId(CliAutocompleteTestId)\n\n    expect(autocompleteOptionalText).toEqual(autocompleteComponent?.textContent)\n  })\n\n  it('Autocomplete should be only with optional args for \"scan\" command with filled in required args (old realization)', () => {\n    const autocompleteOptionalText = '[pattern] [count] [type]'\n    const { queryByTestId } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        commandName={scanCommand}\n        arguments={scanArgs}\n        wordsTyped={2}\n      />,\n    )\n\n    const autocompleteComponent = queryByTestId(CliAutocompleteTestId)\n\n    expect(autocompleteOptionalText).toEqual(autocompleteComponent?.textContent)\n  })\n\n  it('Autocomplete should be only with optional args for \"scan\" command with filled in required args and several optional args', () => {\n    const autocompleteOptionalText = '[MATCH pattern] [COUNT count] [TYPE type]'\n    const { queryByTestId } = render(\n      <CliAutocomplete\n        {...instance(mockedProps)}\n        provider=\"main\"\n        commandName={scanCommand}\n        arguments={scanArgs}\n        wordsTyped={10}\n      />,\n    )\n\n    const autocompleteComponent = queryByTestId(CliAutocompleteTestId)\n\n    expect(autocompleteOptionalText).toEqual(autocompleteComponent?.textContent)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliAutocomplete/CliAutocomplete.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliAutocomplete from './index'\n\nconst cliAutocompleteMeta = {\n  component: CliAutocomplete,\n  args: {\n    commandName: 'scan',\n    provider: 'redis',\n    arguments: [\n      {\n        name: 'cursor',\n        type: 'integer',\n      },\n      {\n        token: 'MATCH',\n        name: 'pattern',\n        type: 'pattern',\n        optional: true,\n      },\n      {\n        token: 'COUNT',\n        name: 'count',\n        type: 'integer',\n        optional: true,\n      },\n      {\n        token: 'TYPE',\n        name: 'type',\n        type: 'string',\n        optional: true,\n      },\n    ],\n  },\n} satisfies Meta<typeof CliAutocomplete>\n\nexport default cliAutocompleteMeta\n\ntype Story = StoryObj<typeof cliAutocompleteMeta>\n\nexport const Default: Story = {\n  args: {\n    wordsTyped: 5,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliAutocomplete/CliAutocomplete.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { findIndex } from 'lodash'\nimport { useDispatch } from 'react-redux'\n\nimport { ICommandArg } from 'uiSrc/constants'\nimport { generateArgsNames } from 'uiSrc/utils'\nimport {\n  setMatchedCommand,\n  clearSearchingCommand,\n} from 'uiSrc/slices/cli/cli-settings'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  provider: string\n  commandName: string\n  wordsTyped: number\n  arguments?: ICommandArg[]\n}\n\nconst CliAutocomplete = (props: Props) => {\n  const {\n    commandName = '',\n    provider = '',\n    arguments: args = [],\n    wordsTyped,\n  } = props\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(setMatchedCommand(commandName))\n    dispatch(clearSearchingCommand())\n  }, [commandName])\n\n  useEffect(\n    () => () => {\n      dispatch(setMatchedCommand(''))\n      dispatch(clearSearchingCommand())\n    },\n    [],\n  )\n\n  let argsList: any[] | string = []\n  let untypedArgs: any[] | string = []\n\n  const getUntypedArgs = () => {\n    const firstOptionalArgIndex = findIndex(argsList, (arg: string = '') =>\n      arg.toString().includes('['),\n    )\n\n    const isOnlyOptionalLeft =\n      wordsTyped - commandName.split(' ').length >= firstOptionalArgIndex &&\n      firstOptionalArgIndex > -1\n\n    if (isOnlyOptionalLeft) {\n      return firstOptionalArgIndex\n    }\n\n    return wordsTyped - commandName.split(' ').length\n  }\n\n  if (args.length) {\n    argsList = generateArgsNames(provider, args)\n\n    untypedArgs = argsList.slice(getUntypedArgs()).join(' ')\n    argsList = argsList.join(' ')\n  }\n\n  return (\n    <>\n      {!!args.length && argsList && untypedArgs && (\n        <span\n          className={styles.container}\n          data-testid=\"cli-command-autocomplete\"\n        >\n          <span className={styles.params}>{untypedArgs}</span>\n        </span>\n      )}\n    </>\n  )\n}\n\nexport default CliAutocomplete\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliAutocomplete/index.ts",
    "content": "import CliAutocomplete from './CliAutocomplete'\n\nexport default CliAutocomplete\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliAutocomplete/styles.module.scss",
    "content": ".container {\n  font: normal normal normal 13px/15px Inconsolata !important;\n  background-color: var(--tableDarkestBorderColor);\n  opacity: 0.8;\n  margin-left: 1px;\n}\n\n.params {\n  padding: 0 5px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInput/CliInput.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\nimport CliInput, { Props } from './CliInput'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('CliInput', () => {\n  it('should render', () => {\n    expect(render(<CliInput {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render db index if it is greater than 0', () => {\n    const { queryByTestId } = render(\n      <CliInput {...instance(mockedProps)} dbIndex={1} />,\n    )\n    const dbIndexEl = queryByTestId('cli-db-index')\n\n    expect(dbIndexEl).toBeInTheDocument()\n    expect(dbIndexEl).toHaveTextContent('[db1]')\n  })\n\n  it('should not render db index if it is 0', () => {\n    const { queryByTestId } = render(\n      <CliInput {...instance(mockedProps)} dbIndex={0} />,\n    )\n    const dbIndexEl = queryByTestId('cli-db-index')\n\n    expect(dbIndexEl).not.toBeInTheDocument()\n  })\n\n  // It's not possible to simulate events on contenteditable with testing-react-library,\n  // or any testing library that uses js - dom, because of a limitation on js - dom itself.\n  // https://github.com/testing-library/dom-testing-library/pull/235\n  it.skip('\"onChange\" should be called', async () => {\n    const command = 'keys *'\n    const setCommandMock = jest.fn()\n\n    render(<CliInput {...instance(mockedProps)} setCommand={setCommandMock} />)\n\n    const cliInput = screen.getByTestId('cli-command')\n\n    fireEvent.blur(cliInput, { target: { innerHTML: command } })\n\n    expect(setCommandMock).toBeCalledTimes(command.length)\n  })\n\n  it('onMouseUp should be called', async () => {\n    const setCommandMock = jest.fn()\n\n    render(<CliInput {...instance(mockedProps)} setCommand={setCommandMock} />)\n\n    const cliInput = screen.getByTestId('cli-command')\n\n    fireEvent.mouseUp(cliInput)\n\n    expect(setCommandMock).not.toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInput/CliInput.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliInput from './index'\n\nconst cliInputMeta = {\n  component: CliInput,\n  args: {\n    command: 'SCAN',\n    setInputEl: () => {},\n    setCommand: () => {},\n    onKeyDown: () => {},\n    dbIndex: 0,\n  },\n} satisfies Meta<typeof CliInput>\n\nexport default cliInputMeta\n\ntype Story = StoryObj<typeof cliInputMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInput/CliInput.tsx",
    "content": "import React from 'react'\nimport { ContentEditableEvent } from 'react-contenteditable'\n\nimport { ContentEditable } from 'uiSrc/components'\nimport { parseContentEditableChangeHtml } from 'uiSrc/components/ContentEditable'\nimport { getDbIndex } from 'uiSrc/utils'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  command: string\n  setInputEl: Function\n  setCommand: (command: string) => void\n  onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => void\n  dbIndex: number\n}\n\nconst CliInput = (props: Props) => {\n  const { command = '', setInputEl, setCommand, onKeyDown, dbIndex = 0 } = props\n\n  const onMouseUp = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {\n    event.stopPropagation()\n  }\n\n  const onChange = (e: ContentEditableEvent) => {\n    setCommand(parseContentEditableChangeHtml(e.target.value ?? ''))\n  }\n\n  return (\n    <>\n      <span>\n        {dbIndex !== 0 && (\n          <span data-testid=\"cli-db-index\">{`${getDbIndex(dbIndex)} `}</span>\n        )}\n        &gt;&nbsp;\n      </span>\n      <ContentEditable\n        tagName=\"span\"\n        html={command}\n        id={styles.command}\n        spellCheck={false}\n        data-testid=\"cli-command\"\n        innerRef={setInputEl}\n        onKeyDown={onKeyDown}\n        onMouseUp={onMouseUp}\n        onChange={onChange}\n      />\n    </>\n  )\n}\n\nexport default CliInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInput/index.ts",
    "content": "import CliInput from './CliInput'\n\nexport default CliInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInput/styles.module.scss",
    "content": "#command {\n  font: normal normal bold 14px/15px Inconsolata !important;\n  color: var(--textColorShade);\n  caret-color: var(--euiColorFullShade);\n  min-width: 5px;\n  display: inline;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport CliInputWrapper, { Props } from './CliInputWrapper'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst autocompleteTestId = 'cli-command-autocomplete'\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  const { MOCK_COMMANDS_SPEC, MOCK_COMMANDS_ARRAY } =\n    jest.requireActual('uiSrc/constants')\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      spec: MOCK_COMMANDS_SPEC,\n      commandsArray: Object.keys(MOCK_COMMANDS_ARRAY).sort(),\n    }),\n  }\n})\n\ndescribe('CliInputWrapper', () => {\n  it('should render', () => {\n    expect(render(<CliInputWrapper {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('\"get\" command (with args) should render CliAutocomplete', () => {\n    const setCommandMock = jest.fn()\n\n    const command = 'get'\n\n    render(\n      <CliInputWrapper\n        {...instance(mockedProps)}\n        command={command}\n        setCommand={setCommandMock}\n      />,\n    )\n\n    expect(screen.getByTestId(autocompleteTestId)).toBeInTheDocument()\n  })\n\n  it('\"client info\" command (without args) should not render CliAutocomplete', () => {\n    const setCommandMock = jest.fn()\n\n    const command = 'client info'\n\n    const { queryByTestId } = render(\n      <CliInputWrapper\n        {...instance(mockedProps)}\n        command={command}\n        setCommand={setCommandMock}\n      />,\n    )\n\n    expect(queryByTestId(autocompleteTestId)).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CliInputWrapper from './CliInputWrapper'\n\nconst cliInputWrapperMeta = {\n  component: CliInputWrapper,\n  args: {\n    command: 'SCAN',\n    wordsTyped: 1,\n    setInputEl: () => {},\n    setCommand: () => {},\n    onKeyDown: () => {},\n  },\n} satisfies Meta<typeof CliInputWrapper>\n\nexport default cliInputWrapperMeta\n\ntype Story = StoryObj<typeof cliInputWrapperMeta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/CliInputWrapper.tsx",
    "content": "import { isUndefined } from 'lodash'\nimport React from 'react'\nimport { useSelector } from 'react-redux'\nimport { getCommandRepeat } from 'uiSrc/utils'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport { outputSelector } from 'uiSrc/slices/cli/cli-output'\nimport { CommandProvider } from 'uiSrc/constants'\nimport CliAutocomplete from './CliAutocomplete'\n\nimport CliInput from './CliInput'\n\nexport interface Props {\n  command: string\n  wordsTyped: number\n  setInputEl: Function\n  setCommand: (command: string) => void\n  onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => void\n}\n\nconst CliInputWrapper = (props: Props) => {\n  const { command = '', wordsTyped, setInputEl, setCommand, onKeyDown } = props\n  const { spec: ALL_REDIS_COMMANDS } = useSelector(appRedisCommandsSelector)\n  const { db } = useSelector(outputSelector)\n\n  const [commandLine, repeatCommand] = getCommandRepeat(command || '')\n  const [firstCommand, secondCommand] = commandLine.split(' ')\n  const firstCommandMatch = firstCommand.toUpperCase()\n  const secondCommandMatch = `${firstCommandMatch} ${secondCommand ? secondCommand.toUpperCase() : null}`\n\n  const matchedCmd =\n    ALL_REDIS_COMMANDS[secondCommandMatch] ||\n    ALL_REDIS_COMMANDS[firstCommandMatch]\n  const provider = matchedCmd?.provider || CommandProvider.Unknown\n  const commandName = !isUndefined(ALL_REDIS_COMMANDS[secondCommandMatch])\n    ? `${firstCommand} ${secondCommand}`\n    : firstCommand\n\n  return (\n    <>\n      <CliInput\n        command={command}\n        setInputEl={setInputEl}\n        setCommand={setCommand}\n        onKeyDown={onKeyDown}\n        dbIndex={db}\n      />\n      {matchedCmd && (\n        <CliAutocomplete\n          provider={provider}\n          commandName={commandName}\n          wordsTyped={repeatCommand === 1 ? wordsTyped : wordsTyped - 1}\n          {...matchedCmd}\n        />\n      )}\n    </>\n  )\n}\n\nexport default CliInputWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/cli/components/cli-input/index.ts",
    "content": "import CliInputWrapper from './CliInputWrapper'\n\nexport default CliInputWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/code-block/CodeBlock.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport CodeBlock from './CodeBlock'\n\nconst originalClipboard = { ...global.navigator.clipboard }\ndescribe('CodeBlock', () => {\n  beforeEach(() => {\n    // @ts-ignore\n    global.navigator.clipboard = {\n      writeText: jest.fn(),\n    }\n  })\n\n  afterEach(() => {\n    jest.resetAllMocks()\n    // @ts-ignore\n    global.navigator.clipboard = originalClipboard\n  })\n\n  it('should render', () => {\n    expect(render(<CodeBlock>text</CodeBlock>)).toBeTruthy()\n  })\n\n  it('should render proper content', () => {\n    render(<CodeBlock data-testid=\"code\">text</CodeBlock>)\n    expect(screen.getByTestId('code')).toHaveTextContent('text')\n  })\n\n  it('should not render copy button by default', () => {\n    render(<CodeBlock data-testid=\"code\">text</CodeBlock>)\n    expect(screen.queryByTestId('copy-code-btn')).not.toBeInTheDocument()\n  })\n\n  it('should copy proper text', () => {\n    render(\n      <CodeBlock data-testid=\"code\" isCopyable>\n        text\n      </CodeBlock>,\n    )\n    fireEvent.click(screen.getByTestId('copy-code-btn'))\n    expect(navigator.clipboard.writeText).toHaveBeenCalledWith('text')\n  })\n\n  it('should copy proper text when children is ReactNode', () => {\n    render(\n      <CodeBlock data-testid=\"code\" isCopyable>\n        <span>text2</span>\n      </CodeBlock>,\n    )\n    fireEvent.click(screen.getByTestId('copy-code-btn'))\n    expect(navigator.clipboard.writeText).toHaveBeenCalledWith('text2')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/code-block/CodeBlock.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport CodeBlock from './index'\n\nconst codeBlockMeta: Meta<typeof CodeBlock> = {\n  component: CodeBlock,\n}\n\nexport default codeBlockMeta\n\ntype Story = StoryObj<typeof codeBlockMeta>\n\nexport const Default: Story = {\n  args: {\n    children: 'console.log(\"Hello, World!\");',\n  },\n}\n\nexport const WithCopyButton: Story = {\n  args: {\n    children: 'redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001',\n    isCopyable: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/code-block/CodeBlock.tsx",
    "content": "import React, { HTMLAttributes, useMemo } from 'react'\nimport cx from 'classnames'\n\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { useInnerText } from 'uiSrc/components/base/utils/hooks/inner-text'\nimport styles from './styles.module.scss'\n\nexport interface Props extends HTMLAttributes<HTMLPreElement> {\n  children: React.ReactNode\n  className?: string\n  isCopyable?: boolean\n}\n\nconst CodeBlock = (props: Props) => {\n  const { isCopyable, className, children, ...rest } = props\n  const [innerTextRef, innerTextString] = useInnerText('')\n\n  const innerText = useMemo(\n    () => innerTextString?.replace(/[\\r\\n?]{2}|\\n\\n/g, '\\n') || '',\n    [innerTextString],\n  )\n\n  return (\n    <div className={cx(styles.wrapper, { [styles.isCopyable]: isCopyable })}>\n      <pre className={cx(styles.pre, className)} ref={innerTextRef} {...rest}>\n        {children}\n      </pre>\n      {isCopyable && (\n        <span className={styles.copyBtn}>\n          <CopyButton\n            copy={innerText}\n            withTooltip={false}\n            data-testid=\"copy-code\"\n            aria-label=\"copy code\"\n          />\n        </span>\n      )}\n    </div>\n  )\n}\n\nexport default CodeBlock\n"
  },
  {
    "path": "redisinsight/ui/src/components/code-block/index.ts",
    "content": "import CodeBlock from './CodeBlock'\n\nexport default CodeBlock\n"
  },
  {
    "path": "redisinsight/ui/src/components/code-block/styles.module.scss",
    "content": ".wrapper {\n  position: relative;\n\n  &.isCopyable {\n    .pre {\n      padding: 8px 30px 8px 16px !important;\n    }\n  }\n\n  .pre {\n    padding: 8px 16px !important;\n  }\n\n  .copyBtn {\n    position: absolute;\n    top: 4px;\n    right: 4px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/column-header/ColumnHeader.spec.tsx",
    "content": "import React from 'react'\nimport { act } from '@testing-library/react'\nimport {\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { ColumnHeader, ColumnHeaderProps } from './ColumnHeader'\n\nconst defaultProps: ColumnHeaderProps = {\n  label: 'Test label',\n  tooltip: 'Tooltip content',\n}\n\nconst renderComponent = (props: Partial<ColumnHeaderProps> = {}) =>\n  render(<ColumnHeader {...defaultProps} {...props} />)\n\ndescribe('ColumnHeader', () => {\n  it('should render the label', () => {\n    renderComponent({ label: 'Index name' })\n\n    const label = screen.getByText('Index name')\n    expect(label).toBeInTheDocument()\n  })\n\n  it('should render with tooltip content', () => {\n    const TooltipContent = () => <div>Custom tooltip</div>\n    renderComponent({ label: 'Test label', tooltip: <TooltipContent /> })\n\n    const label = screen.getByText('Test label')\n    expect(label).toBeInTheDocument()\n  })\n\n  it('should show tooltip content when info icon is focused', async () => {\n    renderComponent({\n      label: 'Index prefix',\n      tooltip: 'Keys matching this prefix are automatically indexed.',\n    })\n\n    const header = screen.getByText('Index prefix')\n    const infoIcon = header.parentElement?.querySelector('svg') as Element\n\n    await act(async () => {\n      fireEvent.focus(infoIcon)\n    })\n    await waitForRiTooltipVisible()\n\n    const tooltipContent = screen.getAllByText(\n      'Keys matching this prefix are automatically indexed.',\n    )[0]\n    expect(tooltipContent).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/column-header/ColumnHeader.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\n\nexport interface ColumnHeaderProps {\n  label: ReactNode\n  tooltip: ReactNode\n}\n\nexport const ColumnHeader = ({ label, tooltip }: ColumnHeaderProps) => (\n  <Row gap=\"xs\" align=\"center\">\n    {label}\n    <RiTooltip content={tooltip}>\n      <FlexItem>\n        <InfoIcon />\n      </FlexItem>\n    </RiTooltip>\n  </Row>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/column-header/index.ts",
    "content": "export { ColumnHeader } from './ColumnHeader'\nexport type { ColumnHeaderProps } from './ColumnHeader'\n"
  },
  {
    "path": "redisinsight/ui/src/components/columns-config/ColumnsConfigPopover.spec.tsx",
    "content": "import React from 'react'\nimport { cleanup, render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport ColumnsConfigPopover from 'uiSrc/components/columns-config/ColumnsConfigPopover'\n\n// Simple test columns enum/union\nenum TestCol {\n  A = 'a',\n  B = 'b',\n  C = 'c',\n}\n\nconst columnsMap = new Map<TestCol, string>([\n  [TestCol.A, 'Column A'],\n  [TestCol.B, 'Column B'],\n  [TestCol.C, 'Column C'],\n])\n\ndescribe('ColumnsConfigPopover', () => {\n  beforeEach(() => cleanup())\n\n  const openPopover = async () => {\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n    return popover\n  }\n\n  it('renders button and shows checkboxes with correct checked state', async () => {\n    render(\n      <ColumnsConfigPopover<TestCol>\n        columnsMap={columnsMap}\n        shownColumns={[TestCol.A, TestCol.B]}\n        onChange={jest.fn()}\n      />,\n    )\n\n    // Default label and button test id\n    expect(screen.getByTestId('btn-columns-config')).toBeInTheDocument()\n    expect(screen.getByText('Columns')).toBeInTheDocument()\n\n    await openPopover()\n\n    // Checked\n    expect(screen.getByTestId('show-a')).toBeChecked()\n    expect(screen.getByTestId('show-b')).toBeChecked()\n    // Not checked\n    expect(screen.getByTestId('show-c')).not.toBeChecked()\n  })\n\n  it('calls onChange with hidden diff when unchecking a checked column', async () => {\n    const onChange = jest.fn()\n\n    render(\n      <ColumnsConfigPopover<TestCol>\n        columnsMap={columnsMap}\n        shownColumns={[TestCol.A, TestCol.B]}\n        onChange={onChange}\n      />,\n    )\n\n    await openPopover()\n\n    fireEvent.click(screen.getByTestId('show-a'))\n\n    expect(onChange).toHaveBeenCalledTimes(1)\n    expect(onChange).toHaveBeenCalledWith([TestCol.B], {\n      shown: [],\n      hidden: [TestCol.A],\n    })\n  })\n\n  it('calls onChange with shown diff when checking a hidden column', async () => {\n    const onChange = jest.fn()\n\n    render(\n      <ColumnsConfigPopover<TestCol>\n        columnsMap={columnsMap}\n        shownColumns={[TestCol.A]}\n        onChange={onChange}\n      />,\n    )\n\n    await openPopover()\n\n    fireEvent.click(screen.getByTestId('show-b'))\n\n    expect(onChange).toHaveBeenCalledTimes(1)\n    expect(onChange).toHaveBeenCalledWith([TestCol.A, TestCol.B], {\n      shown: [TestCol.B],\n      hidden: [],\n    })\n  })\n\n  it('prevents hiding the last remaining column (disabled and no onChange)', async () => {\n    const onChange = jest.fn()\n\n    render(\n      <ColumnsConfigPopover<TestCol>\n        columnsMap={columnsMap}\n        shownColumns={[TestCol.A]}\n        onChange={onChange}\n      />,\n    )\n\n    await openPopover()\n\n    const lastCheckbox = screen.getByTestId('show-a') as HTMLInputElement\n    expect(lastCheckbox).toBeDisabled()\n\n    fireEvent.click(lastCheckbox)\n    expect(onChange).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/columns-config/ColumnsConfigPopover.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { RiPopover } from 'uiSrc/components/base'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { ColumnsIcon } from 'uiSrc/components/base/icons'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\ninterface ColumnsConfigPopoverProps<T extends string = string> {\n  columnsMap: Map<T, string>\n  shownColumns: T[]\n  onChange: (nextShownColumns: T[], diff: { shown: T[]; hidden: T[] }) => void\n  buttonLabel?: React.ReactNode\n  buttonTestId?: string\n  popoverTestId?: string\n}\n\nfunction ColumnsConfigPopover<T extends string = string>({\n  columnsMap,\n  shownColumns,\n  onChange,\n  buttonLabel = 'Columns',\n  buttonTestId = 'btn-columns-config',\n  popoverTestId = 'columns-config-popover',\n}: ColumnsConfigPopoverProps<T>) {\n  const [isOpen, setIsOpen] = useState(false)\n\n  const toggle = () => setIsOpen((v) => !v)\n\n  const handleToggle = (checked: boolean, col: T) => {\n    // prevent hiding the last remaining column\n    if (!checked && shownColumns.length === 1 && shownColumns.includes(col)) {\n      return\n    }\n\n    const next = checked\n      ? [...shownColumns, col]\n      : shownColumns.filter((c) => c !== col)\n\n    onChange(next, {\n      shown: checked ? [col] : ([] as T[]),\n      hidden: checked ? ([] as T[]) : [col],\n    })\n  }\n\n  return (\n    <RiPopover\n      ownFocus={false}\n      anchorPosition=\"downLeft\"\n      isOpen={isOpen}\n      closePopover={() => setIsOpen(false)}\n      data-testid={popoverTestId}\n      button={\n        <EmptyButton\n          icon={ColumnsIcon}\n          onClick={toggle}\n          data-testid={buttonTestId}\n          aria-label=\"columns\"\n        >\n          {buttonLabel}\n        </EmptyButton>\n      }\n    >\n      <Col gap=\"m\">\n        {Array.from(columnsMap.entries()).map(([field, name]) => (\n          <Checkbox\n            key={`show-${field}`}\n            id={`show-${field}`}\n            name={`show-${field}`}\n            label={name}\n            checked={shownColumns.includes(field)}\n            disabled={shownColumns.includes(field) && shownColumns.length === 1}\n            onChange={(e) => handleToggle(e.target.checked, field)}\n            data-testid={`show-${field}`}\n          />\n        ))}\n      </Col>\n    </RiPopover>\n  )\n}\n\nexport default ColumnsConfigPopover\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelper/CommandHelper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport CommandHelper, { Props } from './CommandHelper'\n\nconst mockedProps = mock<Props>()\nlet store: typeof mockedStore\n\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  const { MOCK_COMMANDS_SPEC, MOCK_COMMANDS_ARRAY } =\n    jest.requireActual('uiSrc/constants')\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      spec: MOCK_COMMANDS_SPEC,\n      commandsArray: MOCK_COMMANDS_ARRAY,\n    }),\n  }\n})\n\nconst commandLine = 'get'\nconst mockedSearchedCommands = ['HSET', 'SET']\n\ndescribe('CliHelper', () => {\n  it('should render', () => {\n    expect(render(<CommandHelper {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('Cli Helper should be in the Document', () => {\n    render(<CommandHelper {...instance(mockedProps)} />)\n\n    const cliHelper = screen.queryByTestId('cli-helper')\n\n    expect(cliHelper).toBeInTheDocument()\n  })\n\n  it('Default text component should be in the Document by default', () => {\n    render(<CommandHelper {...instance(mockedProps)} />)\n\n    const cliHelperDefault = screen.queryByTestId('cli-helper-default')\n\n    expect(cliHelperDefault).toBeInTheDocument()\n  })\n\n  it('Default text component should not be in the Document when Command is matched', () => {\n    const { queryByTestId } = render(\n      <CommandHelper {...instance(mockedProps)} commandLine={commandLine} />,\n    )\n\n    const cliHelperDefault = queryByTestId('cli-helper-default')\n\n    expect(cliHelperDefault).not.toBeInTheDocument()\n  })\n\n  it('Cli Helper search should be in the Document', () => {\n    render(<CommandHelper {...instance(mockedProps)} />)\n\n    const cliHelperSearch = screen.queryByTestId('cli-helper-search')\n\n    expect(cliHelperSearch).toBeInTheDocument()\n  })\n\n  it('Title text component should be in the Document when Command is matched', () => {\n    const { queryByTestId } = render(\n      <CommandHelper {...instance(mockedProps)} commandLine={commandLine} />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-title')\n\n    expect(cliHelperTitle).toBeInTheDocument()\n  })\n\n  it('Summary text component should be in the Document when Command is matched and summary exists', () => {\n    const { queryByTestId } = render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        commandLine={commandLine}\n        summary=\"summary\"\n      />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-summary')\n\n    expect(cliHelperTitle).toBeInTheDocument()\n  })\n\n  it('Complexity badge text component should be in the Document when Command is matched and complexity exists', () => {\n    const { queryByTestId } = render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        commandLine={commandLine}\n        complexityShort=\"O(N)\"\n      />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-complexity-short')\n\n    expect(cliHelperTitle).toBeInTheDocument()\n  })\n\n  it('Complexity text component should be in the Document when Command is matched and complexity exists', () => {\n    const { queryByTestId } = render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        commandLine={commandLine}\n        complexity=\"O(N) bla bla\"\n      />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-complexity')\n\n    expect(cliHelperTitle).toBeInTheDocument()\n  })\n\n  it('Complexity text component should not be in the Document when Command is matched and complexity exists and ComplexityShort detected', () => {\n    const { queryByTestId } = render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        commandLine={commandLine}\n        complexity=\"O(N)\"\n        complexityShort=\"O(N)\"\n      />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-complexity')\n\n    expect(cliHelperTitle).not.toBeInTheDocument()\n  })\n\n  it('Since text component should be in the Document when Command is matched and since exists', () => {\n    const { queryByTestId } = render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        commandLine={commandLine}\n        since=\"2.0\"\n      />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-since')\n\n    expect(cliHelperTitle).toBeInTheDocument()\n  })\n\n  it('Arguments component should be in the Document when Command is matched and argList exists', () => {\n    // eslint-disable-next-line react/no-array-index-key\n    const argList = ['key', 'field'].map((field, i) => (\n      <div key={i}>{field}</div>\n    ))\n    const { queryByTestId } = render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        commandLine={commandLine}\n        argList={argList}\n      />,\n    )\n\n    const cliHelperTitle = queryByTestId('cli-helper-arguments')\n\n    expect(cliHelperTitle).toBeInTheDocument()\n  })\n\n  it('Search results should be in the Document when Command is matched', () => {\n    render(\n      <CommandHelper\n        {...instance(mockedProps)}\n        isSearching\n        searchedCommands={mockedSearchedCommands}\n      />,\n    )\n    const cliHelperSearchResultsTitle = screen.queryAllByTestId(\n      /cli-helper-output-title/,\n    )\n\n    expect(cliHelperSearchResultsTitle).toHaveLength(2)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelper/CommandHelper.tsx",
    "content": "import React, { ReactElement } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { CommandGroup } from 'uiSrc/constants'\nimport { goBackFromCommand } from 'uiSrc/slices/cli/cli-settings'\nimport { getDocUrlForCommand } from 'uiSrc/utils'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport CHCommandInfo from '../components/command-helper-info'\nimport CHSearchWrapper from '../components/command-helper-search'\nimport CHSearchOutput from '../components/command-helper-search-output'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  commandLine: string\n  isSearching: boolean\n  searchedCommands: string[]\n  argString: string\n  argList: ReactElement[]\n  summary: string\n  group: CommandGroup | string\n  complexity: string\n  complexityShort: string\n  since: string\n}\n\nconst CommandHelper = (props: Props) => {\n  const {\n    commandLine = '',\n    isSearching = false,\n    searchedCommands = [],\n    argString = '',\n    argList = [],\n    summary = '',\n    group = CommandGroup.Generic,\n    complexity = '',\n    complexityShort = '',\n    since = '',\n  } = props\n\n  const dispatch = useDispatch()\n  const handleBackClick = () => dispatch(goBackFromCommand())\n\n  const readMore = (commandName = '') => {\n    const docUrl = getDocUrlForCommand(commandName)\n    return (\n      <Link\n        href={docUrl}\n        target=\"_blank\"\n        data-testid=\"read-more\"\n        size=\"S\"\n        variant=\"inline\"\n        color=\"primary\"\n      >\n        Read more\n      </Link>\n    )\n  }\n\n  return (\n    <div className={styles.container} data-testid=\"cli-helper\">\n      <div className={styles.searchWrapper}>\n        <CHSearchWrapper />\n      </div>\n      {isSearching && (\n        <div className={styles.outputWrapper}>\n          <CHSearchOutput searchedCommands={searchedCommands} />\n        </div>\n      )}\n      {!isSearching && (\n        <div className={styles.outputWrapper}>\n          {commandLine && (\n            <div style={{ width: '100%' }}>\n              <CHCommandInfo\n                args={argString}\n                group={group}\n                complexity={complexityShort}\n                onBackClick={handleBackClick}\n              />\n              {summary && (\n                <Text\n                  className={styles.summary}\n                  data-testid=\"cli-helper-summary\"\n                >\n                  <span style={{ paddingRight: 5 }}>{summary}</span>{' '}\n                  {readMore(commandLine)}\n                </Text>\n              )}\n              {!!argList.length && (\n                <div\n                  className={styles.field}\n                  data-testid=\"cli-helper-arguments\"\n                >\n                  <Text color=\"primary\" className={styles.fieldTitle}>\n                    Arguments:\n                  </Text>\n                  {argList}\n                </div>\n              )}\n              {since && (\n                <div className={styles.field} data-testid=\"cli-helper-since\">\n                  <Text color=\"primary\" className={styles.fieldTitle}>\n                    Since:\n                  </Text>\n                  {since}\n                </div>\n              )}\n              {!complexityShort && complexity && (\n                <div\n                  className={styles.field}\n                  data-testid=\"cli-helper-complexity\"\n                >\n                  <Text color=\"primary\" className={styles.fieldTitle}>\n                    Complexity:\n                  </Text>\n                  {complexity}\n                </div>\n              )}\n            </div>\n          )}\n          {!commandLine && (\n            <Text\n              color=\"primary\"\n              className={styles.defaultScreen}\n              data-testid=\"cli-helper-default\"\n            >\n              Enter any command in CLI or use search to see detailed\n              information.\n            </Text>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default CommandHelper\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelper/index.ts",
    "content": "import CommandHelper from './CommandHelper'\n\nexport default CommandHelper\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelper/styles.module.scss",
    "content": ".container {\n  height: calc(100% - 34px);\n  position: relative;\n  width: 100%;\n\n  background-color: var(--browserTableRowEven);\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--euiTextSubduedColor) !important;\n  border-top: 1px solid var(--euiColorLightShade);\n\n  z-index: 10;\n  overflow: hidden;\n}\n\n.searchWrapper {\n  padding: 10px 10px 0 10px;\n  background-color: var(--browserTableRowEven);\n}\n\n.outputWrapper {\n  @include eui.scrollBar;\n  display: flex;\n  flex: 1;\n  padding: 0 10px 10px 10px;\n\n  width: 100%;\n  min-width: 230px;\n  overflow: auto;\n  height: 100%;\n  max-height: calc(100% - 64px);\n}\n\n.defaultScreen {\n  text-align: center;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-grow: 1;\n  line-height: 21px;\n}\n\n.summary {\n  font:\n    normal normal normal 13px/18px Graphik,\n    sans-serif !important;\n  letter-spacing: -0.13px !important;\n  padding: 10px 0 5px;\n}\n\n.field {\n  padding-top: 12px;\n  font:\n    normal normal normal 13px/17px Graphik,\n    sans-serif !important;\n  letter-spacing: -0.13px;\n}\n\n.fieldTitle {\n  font:\n    normal normal 500 14px/17px Graphik,\n    sans-serif !important;\n  letter-spacing: -0.14px;\n  color: var(--euiTextSubduedColorHover);\n  padding-bottom: 3px;\n}\n\n.arg {\n  padding: 3px 10px;\n  margin: 0 -10px;\n  &:nth-child(2n) {\n    background-color: var(--euiColorEmptyShade);\n  }\n}\n.badge {\n  background-color: var(--badgeBackgroundColor) !important;\n  margin-right: 18px;\n  min-width: 68px;\n  text-align: center !important;\n}\n\n.commandHelperWrapper {\n  height: 100%;\n  border-top: 1px solid var(--euiColorLightShade);\n  border-left: 1px solid var(--euiColorLightShade);\n  border-right: 1px solid var(--euiColorLightShade);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelperHeader/CommandHelperHeader.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  resetCliHelperSettings,\n  toggleCliHelper,\n  toggleHideCliHelper,\n} from 'uiSrc/slices/cli/cli-settings'\nimport CommandHelperHeader from './CommandHelperHeader'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('CommandHelperHeader', () => {\n  it('should render', () => {\n    expect(render(<CommandHelperHeader />)).toBeTruthy()\n  })\n\n  it('should \"resetCliHelperSettings\" action be called after click \"close-command-helper\" button', () => {\n    render(<CommandHelperHeader />)\n    fireEvent.click(screen.getByTestId('close-command-helper'))\n\n    const expectedActions = [resetCliHelperSettings()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should \"toggleCliHelper\" action be called after click \"hide-command-helper\" button', () => {\n    render(<CommandHelperHeader />)\n    fireEvent.click(screen.getByTestId('hide-command-helper'))\n\n    const expectedActions = [toggleCliHelper(), toggleHideCliHelper()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelperHeader/CommandHelperHeader.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  resetCliHelperSettings,\n  toggleCliHelper,\n  toggleHideCliHelper,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { OnboardingTour } from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { WindowControlGroup } from 'uiSrc/components/base/shared/WindowControlGroup'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nconst CommandHelperHeader = () => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  const handleCloseHelper = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.COMMAND_HELPER_CLOSED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    dispatch(resetCliHelperSettings())\n  }\n\n  const handleHideHelper = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.COMMAND_HELPER_MINIMIZED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    dispatch(toggleCliHelper())\n    dispatch(toggleHideCliHelper())\n  }\n\n  return (\n    <div className={styles.container} id=\"command-helper-header\">\n      <Row justify=\"between\" align=\"center\" style={{ height: '100%' }}>\n        <FlexItem className={styles.title}>\n          <RiIcon type=\"DocumentationIcon\" size=\"L\" />\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.BROWSER_COMMAND_HELPER}\n            anchorPosition=\"upLeft\"\n            panelClassName={styles.helperOnboardPanel}\n          >\n            <Text>Command Helper</Text>\n          </OnboardingTour>\n        </FlexItem>\n        <FlexItem grow />\n        <WindowControlGroup\n          onClose={handleCloseHelper}\n          onHide={handleHideHelper}\n          id=\"command-helper\"\n          label=\"Command Helper\"\n        />\n      </Row>\n    </div>\n  )\n}\n\nexport default CommandHelperHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelperHeader/index.ts",
    "content": "import CommandHelperHeader from './CommandHelperHeader'\n\nexport default CommandHelperHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelperHeader/styles.module.scss",
    "content": ".container {\n  height: 34px;\n  line-height: 34px;\n  width: 100%;\n  overflow: hidden;\n  background-color: var(--euiPageBackgroundColor);\n\n  padding: 0 10px 0 16px;\n  z-index: 10;\n}\n\n.icon {\n  margin-left: 5px;\n}\n\n.title {\n  display: flex;\n  flex-direction: row !important;\n  align-items: center;\n  :global {\n    .euiIcon {\n      color: var(--euiColorPrimary);\n      margin-right: 8px;\n    }\n  }\n}\n\n.helperOnboardPanel {\n  margin-top: -4px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelperWrapper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport { ICommands, MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport CommandHelperWrapper from './CommandHelperWrapper'\n\nconst ALL_REDIS_COMMANDS: ICommands = MOCK_COMMANDS_SPEC\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\nconst cliHelperTestId = 'cli-helper'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/cli/cli-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/cli-settings'),\n  cliSettingsSelector: jest.fn().mockReturnValue({\n    matchedCommand: '',\n    isSearching: false,\n    isEnteringCommand: false,\n    searchedCommand: '',\n    searchingCommand: '',\n  }),\n}))\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  const { MOCK_COMMANDS_SPEC, MOCK_COMMANDS_ARRAY } =\n    jest.requireActual('uiSrc/constants')\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      spec: MOCK_COMMANDS_SPEC,\n      commandsArray: MOCK_COMMANDS_ARRAY,\n    }),\n  }\n})\n\ninterface IMockedCommands {\n  matchedCommand: string\n  argStr?: string\n  argListText?: string\n  complexityShort?: string\n}\n\nconst mockedCommands: IMockedCommands[] = [\n  {\n    matchedCommand: 'xgroup',\n    argStr: 'XGROUP',\n    argListText: '',\n  },\n  {\n    matchedCommand: 'hset',\n    argStr: 'HSET key field value [field value ...]',\n    argListText: 'Arguments:RequiredkeyMultiplefield value',\n  },\n  {\n    matchedCommand: 'acl setuser',\n    argStr: 'ACL SETUSER username [rule [rule ...]]',\n    argListText: 'Arguments:RequiredusernameMultiple[rule]',\n  },\n  {\n    matchedCommand: 'bitfield',\n    argStr:\n      'BITFIELD key [GET encoding offset | [OVERFLOW WRAP | SAT | FAIL] SET encoding offset value | INCRBY encoding offset increment [GET encoding offset | [OVERFLOW WRAP | SAT | FAIL] SET encoding offset value | INCRBY encoding offset increment ...]]',\n    argListText:\n      'Arguments:RequiredkeyMultiple[GET encoding offset | [OVERFLOW WRAP | SAT | FAIL] SET encoding offset value | INCRBY encoding offset increment]',\n  },\n  {\n    matchedCommand: 'client kill',\n    argStr:\n      'CLIENT KILL ip:port | [ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] [[ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] ...]',\n    argListText:\n      'Arguments:Requiredip:port | [ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] [[ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] ...]',\n  },\n  {\n    matchedCommand: 'geoadd',\n    argStr:\n      'GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]',\n    argListText:\n      'Arguments:RequiredkeyOptional[NX | XX]Optional[CH]Multiplelongitude latitude member',\n  },\n  {\n    matchedCommand: 'zadd',\n    argStr:\n      'ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]',\n    argListText:\n      'Arguments:RequiredkeyOptional[NX | XX]Optional[GT | LT]Optional[CH]Optional[INCR]Multiplescore member',\n  },\n]\n\ndescribe('CliBodyWrapper', () => {\n  it('should render', () => {\n    expect(render(<CommandHelperWrapper />)).toBeTruthy()\n  })\n\n  it('Title should be rendered according mocked data', () => {\n    const titleArgsId = 'cli-helper-title-args'\n\n    mockedCommands.forEach(({ matchedCommand, argStr = '' }) => {\n      cliSettingsSelector.mockImplementation(() => ({\n        matchedCommand,\n        isEnteringCommand: true,\n      }))\n\n      const { unmount } = render(<CommandHelperWrapper />)\n\n      expect(screen.getByTestId(cliHelperTestId)).toBeInTheDocument()\n      expect(screen.getByTestId(titleArgsId)).toHaveTextContent(argStr)\n\n      unmount()\n    })\n  })\n\n  it('Arguments list text should be rendered according mocked data', () => {\n    const argsId = 'cli-helper-arguments'\n\n    mockedCommands.forEach(({ matchedCommand, argListText = '' }) => {\n      cliSettingsSelector.mockImplementation(() => ({\n        matchedCommand,\n        isEnteringCommand: true,\n      }))\n\n      const { unmount } = render(<CommandHelperWrapper />)\n\n      if (argListText) {\n        expect(screen.getByTestId(cliHelperTestId)).toBeInTheDocument()\n        expect(screen.getByTestId(argsId)).toHaveTextContent(argListText)\n      }\n\n      unmount()\n    })\n  })\n\n  it('Since should be rendered according mocked data', () => {\n    const sinceId = 'cli-helper-since'\n\n    mockedCommands.forEach(({ matchedCommand = '' }) => {\n      const since = ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.since\n\n      cliSettingsSelector.mockImplementation(() => ({\n        matchedCommand,\n        isEnteringCommand: true,\n      }))\n\n      const { unmount } = render(<CommandHelperWrapper />)\n\n      expect(screen.getByTestId(cliHelperTestId)).toBeInTheDocument()\n      expect(screen.getByTestId(sinceId)).toHaveTextContent(since)\n\n      unmount()\n    })\n  })\n\n  it('Complexity should be rendered according mocked data', () => {\n    const complexityId = 'cli-helper-complexity'\n\n    mockedCommands.forEach(({ matchedCommand = '' }) => {\n      const complexity =\n        ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.complexity\n\n      cliSettingsSelector.mockImplementation(() => ({\n        matchedCommand,\n        isEnteringCommand: true,\n      }))\n\n      const { unmount } = render(<CommandHelperWrapper />)\n\n      expect(screen.getByTestId(cliHelperTestId)).toBeInTheDocument()\n\n      if (complexity) {\n        expect(screen.getByTestId(complexityId)).toBeInTheDocument()\n        expect(screen.getByTestId(complexityId)).toHaveTextContent(complexity)\n      }\n\n      unmount()\n    })\n  })\n\n  it('should render search results', () => {\n    mockedCommands.forEach(({ matchedCommand }) => {\n      cliSettingsSelector.mockImplementation(() => ({\n        searchingCommand: matchedCommand,\n        searchedCommand: '',\n        isSearching: true,\n      }))\n      const { unmount } = render(<CommandHelperWrapper />)\n      expect(\n        screen.getByTestId(\n          `cli-helper-output-title-${matchedCommand.toUpperCase()}`,\n        ),\n      ).toBeInTheDocument()\n      unmount()\n    })\n  })\n\n  it('should render default message when matched command is deprecated', () => {\n    const sinceId = 'cli-helper-since'\n    const cliHelperDefaultId = 'cli-helper-default'\n\n    cliSettingsSelector.mockImplementation(() => ({\n      matchedCommand: 'GRAPH.CONFIG SET',\n      isEnteringCommand: true,\n    }))\n\n    const { unmount, queryByTestId } = render(<CommandHelperWrapper />)\n\n    expect(queryByTestId(cliHelperDefaultId)).toBeInTheDocument()\n    expect(queryByTestId(sinceId)).not.toBeInTheDocument()\n\n    unmount()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/CommandHelperWrapper.tsx",
    "content": "import React, { ReactElement, useEffect, useMemo } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport cn from 'classnames'\n\nimport { CommandGroup, ICommand, ICommandArgGenerated } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport {\n  generateArgs,\n  generateArgsNames,\n  getComplexityShortNotation,\n  removeDeprecatedModuleCommands,\n  checkDeprecatedModuleCommand,\n} from 'uiSrc/utils'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\n\nimport CommandHelper from './CommandHelper'\nimport CommandHelperHeader from './CommandHelperHeader'\n\nimport styles from './CommandHelper/styles.module.scss'\n\nconst CommandHelperWrapper = () => {\n  const {\n    matchedCommand = '',\n    searchedCommand = '',\n    isSearching,\n    isEnteringCommand,\n    searchingCommand,\n    searchingCommandFilter,\n  } = useSelector(cliSettingsSelector)\n  const { spec: ALL_REDIS_COMMANDS, commandsArray } = useSelector(\n    appRedisCommandsSelector,\n  )\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const lastMatchedCommand =\n    isEnteringCommand &&\n    matchedCommand &&\n    !checkDeprecatedModuleCommand(matchedCommand)\n      ? matchedCommand\n      : searchedCommand\n\n  const KEYS_OF_COMMANDS = useMemo(\n    () => removeDeprecatedModuleCommands(commandsArray),\n    [commandsArray],\n  )\n  let searchedCommands: string[] = []\n\n  useEffect(() => {\n    if (!isSearching && isEnteringCommand && matchedCommand) {\n      sendEventTelemetry({\n        event: TelemetryEvent.COMMAND_HELPER_INFO_DISPLAYED_FOR_CLI_INPUT,\n        eventData: {\n          databaseId: instanceId,\n          command: matchedCommand,\n        },\n      })\n    }\n  }, [isSearching, isEnteringCommand, matchedCommand])\n\n  const {\n    arguments: args = [],\n    summary = '',\n    group = CommandGroup.Generic,\n    complexity = '',\n    since = '',\n    provider,\n  }: ICommand = ALL_REDIS_COMMANDS[lastMatchedCommand.toUpperCase()] ?? {}\n\n  if (isSearching) {\n    searchedCommands = KEYS_OF_COMMANDS.filter((command) => {\n      const isSuitableForFilter = searchingCommandFilter\n        ? ALL_REDIS_COMMANDS[command].group === searchingCommandFilter\n        : true\n      return (\n        isSuitableForFilter &&\n        command.toLowerCase().indexOf(searchingCommand.toLowerCase()) > -1\n      )\n    })\n  }\n\n  const generatedArgs = generateArgs(provider, args)\n  const complexityShort = getComplexityShortNotation(complexity)\n  const argString = [\n    lastMatchedCommand.toUpperCase(),\n    ...generateArgsNames(provider, args),\n  ].join(' ')\n\n  const generateArgData = (\n    arg: ICommandArgGenerated,\n    i: number,\n  ): ReactElement => {\n    const type = arg.multiple\n      ? 'Multiple'\n      : arg.optional\n        ? 'Optional'\n        : 'Required'\n    return (\n      <Row justify=\"between\" align=\"center\" className={styles.arg} key={i}>\n        <FlexItem>\n          <RiBadge\n            variant=\"light\"\n            className={cn(styles.badge, 'text-capitalize')}\n            label={type}\n          />\n        </FlexItem>\n        <FlexItem grow>{arg.generatedName}</FlexItem>\n      </Row>\n    )\n  }\n\n  return (\n    <div className={styles.commandHelperWrapper} data-testid=\"command-helper\">\n      <CommandHelperHeader />\n      <CommandHelper\n        commandLine={lastMatchedCommand}\n        searchedCommands={searchedCommands}\n        isSearching={isSearching}\n        argString={argString}\n        summary={summary}\n        group={group}\n        since={since}\n        complexity={complexity}\n        complexityShort={complexityShort}\n        argList={generatedArgs.map((obj, i) => generateArgData(obj, i))}\n      />\n    </div>\n  )\n}\n\nexport default React.memo(CommandHelperWrapper)\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-info/CHCommandInfo.tsx",
    "content": "import React from 'react'\n\nimport { GroupBadge } from 'uiSrc/components'\nimport { CommandGroup } from 'uiSrc/constants'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { ArrowLeftIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nimport styles from './styles.module.scss'\nimport { HorizontalSpacer } from 'uiSrc/components/base/layout'\n\nexport interface Props {\n  args: string\n  group: CommandGroup | string\n  complexity: string\n  onBackClick: () => void\n}\n\nconst CHCommandInfo = (props: Props) => {\n  const {\n    args = '',\n    group = CommandGroup.Generic,\n    complexity = '',\n    onBackClick,\n  } = props\n\n  return (\n    <Row\n      align=\"center\"\n      className={styles.container}\n      data-testid=\"cli-helper-title\"\n    >\n      <IconButton\n        icon={ArrowLeftIcon}\n        onClick={onBackClick}\n        data-testid=\"cli-helper-back-to-list-btn\"\n        style={{ marginRight: '4px' }}\n      />\n      <GroupBadge type={group} />\n      <HorizontalSpacer size=\"s\" />\n      <Text\n        data-testid=\"cli-helper-title-args\"\n        variant=\"semiBold\"\n        color=\"primary\"\n      >\n        {args}\n      </Text>\n      {complexity && (\n        <RiBadge\n          label={complexity}\n          variant=\"light\"\n          className={styles.badge}\n          data-testid=\"cli-helper-complexity-short\"\n        />\n      )}\n    </Row>\n  )\n}\n\nexport default CHCommandInfo\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-info/index.ts",
    "content": "import CHCommandInfo from './CHCommandInfo'\n\nexport default CHCommandInfo\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-info/styles.module.scss",
    "content": ".container {\n  font: normal normal 500 14px/21px Graphik, sans-serif !important;\n}\n\n.badge {\n  background-color: var(--badgeBackgroundColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchFilter/CHSearchFilter.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport { GROUP_TYPES_DISPLAY } from 'uiSrc/constants'\nimport CHSearchFilter from './CHSearchFilter'\n\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\nconst commandGroupsMock = ['list', 'hash', 'set']\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      commandGroups: commandGroupsMock,\n    }),\n  }\n})\n\ndescribe('CHSearchFilter', () => {\n  it('should render', () => {\n    expect(render(<CHSearchFilter submitFilter={jest.fn()} />)).toBeTruthy()\n  })\n\n  it('should call submitFilter after choose options', async () => {\n    const submitFilter = jest.fn()\n    render(<CHSearchFilter submitFilter={submitFilter} />)\n    const testGroup = commandGroupsMock[0]\n    const dropdownButton = screen.getByTestId('select-filter-group-type')\n    await userEvent.click(dropdownButton)\n\n    await userEvent.click(\n      (await screen.findByText((GROUP_TYPES_DISPLAY as any)[testGroup])) ||\n        document,\n    )\n\n    expect(submitFilter).toHaveBeenCalledWith(testGroup)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchFilter/CHSearchFilter.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { useSelector } from 'react-redux'\n\nimport { GROUP_TYPES_DISPLAY } from 'uiSrc/constants'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  submitFilter: (type: string) => void\n  isLoading?: boolean\n}\n\nconst CHSearchFilter = ({ submitFilter, isLoading }: Props) => {\n  const { commandGroups = [] } = useSelector(appRedisCommandsSelector)\n  const { isEnteringCommand, matchedCommand, searchingCommandFilter } =\n    useSelector(cliSettingsSelector)\n\n  const [typeSelected, setTypeSelected] = useState<string>(\n    searchingCommandFilter,\n  )\n\n  useEffect(() => {\n    if (isEnteringCommand && matchedCommand) {\n      setTypeSelected('')\n    }\n  }, [isEnteringCommand])\n\n  useEffect(() => {\n    matchedCommand && setTypeSelected('')\n  }, [matchedCommand])\n\n  const groupOptions = [...commandGroups].sort().map((group: string) => ({\n    text: (GROUP_TYPES_DISPLAY as any)[group] || group.replace(/_/g, ' '),\n    value: group,\n  }))\n\n  const options = groupOptions.map((item) => {\n    const { value, text } = item\n    return {\n      label: text,\n      value,\n      inputDisplay: (\n        <Text\n          data-test-subj={`filter-option-group-type-${value}`}\n          className={cx(styles.selectedType, 'text-capitalize')}\n          size=\"s\"\n        >\n          {text}\n        </Text>\n      ),\n      dropdownDisplay: (\n        <Text\n          data-test-subj={`filter-option-group-type-${value}`}\n          className=\"text-capitalize\"\n        >\n          {text}\n        </Text>\n      ),\n    }\n  })\n\n  const onChangeType = (initValue: string) => {\n    const value = typeSelected === initValue ? '' : initValue\n    setTypeSelected(value)\n    submitFilter(value)\n  }\n\n  return (\n    <div className={styles.container}>\n      <RiSelect\n        loading={isLoading}\n        disabled={isLoading}\n        options={options}\n        allowReset\n        placeholder={\n          <Row role=\"presentation\">\n            <RiIcon\n              type=\"FilterIcon\"\n              data-testid=\"filter-option--group-type-default\"\n              className={styles.controlsIcon}\n            />\n          </Row>\n        }\n        value={typeSelected}\n        data-testid=\"select-filter-group-type\"\n        onChange={(value: string) => onChangeType(value)}\n        valueRender={({ option, isOptionValue }) => {\n          if (isOptionValue) {\n            return option.inputDisplay\n          }\n          return option.dropdownDisplay\n        }}\n      />\n    </div>\n  )\n}\n\nexport default CHSearchFilter\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchFilter/index.ts",
    "content": "import CHSearchFilter from './CHSearchFilter'\n\nexport default CHSearchFilter\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchFilter/styles.module.scss",
    "content": ".container {\n  height: 36px;\n}\n\n.filterKeyType {\n  line-height: 16px !important;\n  padding: 4px !important;\n}\n\n.controlsIcon {\n  cursor: pointer;\n  height: 20px !important;\n  width: 20px !important;\n  \n  &:global(svg) {\n    color: var(--inputTextColor) !important;\n  }\n}\n\n.selectedType {\n  max-width: 74px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  height: 36px;\n  font-weight: 500 !important;\n  line-height: 36px !important;\n}\n\n.allTypes {\n  position: absolute;\n  top: 5px;\n  display: flex;\n  align-items: center;\n\n  width: 106px;\n  height: 36px;\n  padding-left: 12px;\n\n  cursor: pointer;\n  z-index: 5;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchInput/CHSearchInput.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport CHSearchInput from './CHSearchInput'\n\ndescribe('CHSearchInput', () => {\n  it('should render', () => {\n    expect(render(<CHSearchInput submitSearch={jest.fn()} />)).toBeTruthy()\n  })\n\n  it('should call submitSearch with after typing', () => {\n    const submitSearch = jest.fn()\n    render(<CHSearchInput submitSearch={submitSearch} />)\n    fireEvent.change(screen.getByTestId('cli-helper-search'), {\n      target: { value: 'set' },\n    })\n    expect(submitSearch).toBeCalledWith('set')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchInput/CHSearchInput.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\n\nimport { SearchInput } from 'uiSrc/components/base/inputs'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  submitSearch: (searchValue: string) => void\n  isLoading?: boolean\n}\n\nconst CHSearchInput = ({ submitSearch, isLoading = false }: Props) => {\n  const {\n    isEnteringCommand,\n    searchingCommand = '',\n    matchedCommand = '',\n  } = useSelector(cliSettingsSelector)\n  const [searchValue, setSearchValue] = useState<string>(\n    matchedCommand || searchingCommand,\n  )\n\n  useEffect(() => {\n    if (isEnteringCommand && matchedCommand) {\n      setSearchValue('')\n    }\n  }, [isEnteringCommand])\n\n  useEffect(() => {\n    matchedCommand && setSearchValue('')\n  }, [matchedCommand])\n\n  const onChangeSearch = (value: string) => {\n    setSearchValue(value)\n    submitSearch(value)\n  }\n\n  return (\n    <div className={styles.container}>\n      <SearchInput\n        loading={isLoading}\n        disabled={isLoading}\n        name=\"search-command\"\n        placeholder=\"Search for a command\"\n        autoComplete=\"off\"\n        value={searchValue}\n        onChange={onChangeSearch}\n        data-testid=\"cli-helper-search\"\n      />\n    </div>\n  )\n}\n\nexport default CHSearchInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchInput/index.ts",
    "content": "import CHSearchInput from './CHSearchInput'\n\nexport default CHSearchInput\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchInput/styles.module.scss",
    "content": ".container {\n  flex: 1;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\nimport {\n  clearSearchingCommand,\n  setSearchingCommand,\n  setCliEnteringCommand,\n} from 'uiSrc/slices/cli/cli-settings'\nimport CHSearchWrapper from './CHSearchWrapper'\n\nlet store: typeof mockedStore\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\ndescribe('CHSearchInput', () => {\n  it('should render', () => {\n    expect(render(<CHSearchWrapper />)).toBeTruthy()\n  })\n\n  it('should call search action after typing', () => {\n    render(<CHSearchWrapper />)\n    fireEvent.change(screen.getByTestId('cli-helper-search'), {\n      target: { value: 'set' },\n    })\n    const expectedActions = [setSearchingCommand('set')]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call clear search action after clear input', () => {\n    render(<CHSearchWrapper />)\n    const searchInput = screen.getByTestId('cli-helper-search')\n    fireEvent.change(searchInput, { target: { value: 'set' } })\n    fireEvent.change(searchInput, { target: { value: '' } })\n    const expectedActions = [\n      setSearchingCommand('set'),\n      clearSearchingCommand(),\n      setCliEnteringCommand(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/CHSearchWrapper.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  clearSearchingCommand,\n  setSearchingCommand,\n  setSearchingCommandFilter,\n  setCliEnteringCommand,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\n\nimport CHSearchInput from './CHSearchInput'\nimport CHSearchFilter from './CHSearchFilter'\n\nimport styles from './styles.module.scss'\n\nconst CHSearchWrapper = () => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { loading } = useSelector(appRedisCommandsSelector)\n  const [filterType, setFilterType] = useState<string>('')\n  const [searchValue, setSearchValue] = useState<string>('')\n  const dispatch = useDispatch()\n\n  const onChangeSearch = (value: string) => {\n    setSearchValue(value)\n\n    if (value === '' && !filterType) {\n      dispatch(clearSearchingCommand())\n      dispatch(setCliEnteringCommand())\n      return\n    }\n    dispatch(setSearchingCommand(value))\n  }\n\n  const onChangeFilter = (type: string) => {\n    setFilterType(type)\n\n    if (type) {\n      sendEventTelemetry({\n        event: TelemetryEvent.COMMAND_HELPER_COMMAND_FILTERED,\n        eventData: {\n          databaseId: instanceId,\n          group: type,\n        },\n      })\n    }\n\n    if (searchValue === '' && !type) {\n      dispatch(clearSearchingCommand())\n      dispatch(setCliEnteringCommand())\n      return\n    }\n    dispatch(setSearchingCommandFilter(type))\n  }\n\n  return (\n    <div className={styles.searchWrapper}>\n      <CHSearchFilter isLoading={loading} submitFilter={onChangeFilter} />\n      <CHSearchInput isLoading={loading} submitSearch={onChangeSearch} />\n    </div>\n  )\n}\n\nexport default CHSearchWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/index.ts",
    "content": "import CHSearchWrapper from './CHSearchWrapper'\n\nexport default CHSearchWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search/styles.module.scss",
    "content": ".searchWrapper {\n  margin-bottom: 16px;\n  position: relative;\n  display: flex;\n  gap: 6px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search-output/CHSearchOutput.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { useParams } from 'react-router-dom'\nimport styled from 'styled-components'\n\nimport { generateArgsNames } from 'uiSrc/utils'\nimport { setSearchedCommand } from 'uiSrc/slices/cli/cli-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  searchedCommands: string[]\n}\n\nconst UnderlineReverseLink = styled(Link)`\n  text-decoration: underline !important;\n\n  &:hover {\n    text-decoration: none !important;\n  }\n`\n\nconst CHSearchOutput = ({ searchedCommands }: Props) => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n  const { spec: ALL_REDIS_COMMANDS } = useSelector(appRedisCommandsSelector)\n\n  const handleClickCommand = (\n    e: React.MouseEvent<HTMLAnchorElement>,\n    command: string,\n  ) => {\n    e.preventDefault()\n    sendEventTelemetry({\n      event: TelemetryEvent.COMMAND_HELPER_COMMAND_OPENED,\n      eventData: {\n        databaseId: instanceId,\n        command,\n      },\n    })\n    dispatch(setSearchedCommand(command))\n  }\n\n  const renderDescription = (command: string) => {\n    const args = ALL_REDIS_COMMANDS[command].arguments || []\n    if (args.length) {\n      const argString = generateArgsNames(\n        ALL_REDIS_COMMANDS[command]?.provider,\n        args,\n      ).join(' ')\n      return (\n        <Text\n          size=\"s\"\n          className={styles.description}\n          data-testid={`cli-helper-output-args-${command}`}\n        >\n          {argString}\n        </Text>\n      )\n    }\n    return (\n      <Text\n        size=\"s\"\n        color=\"primary\"\n        className={cx(styles.description, styles.summary)}\n        data-testid={`cli-helper-output-summary-${command}`}\n      >\n        {ALL_REDIS_COMMANDS[command].summary}\n      </Text>\n    )\n  }\n\n  return (\n    <>\n      {searchedCommands.length > 0 && (\n        <div style={{ width: '100%' }}>\n          {searchedCommands.map((command: string) => (\n            <Row gap=\"m\" key={command} align=\"center\">\n              <FlexItem style={{ flexShrink: 0 }}>\n                <Text\n                  key={command}\n                  size=\"s\"\n                  data-testid={`cli-helper-output-title-${command}`}\n                  onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                    handleClickCommand(e, command)\n                  }}\n                >\n                  <UnderlineReverseLink href=\"#\" color=\"text\" variant=\"inline\">\n                    {command}\n                  </UnderlineReverseLink>\n                </Text>\n              </FlexItem>\n              <FlexItem style={{ flexDirection: 'row', overflow: 'hidden' }}>\n                <Text color=\"text\" size=\"s\">\n                  {renderDescription(command)}\n                </Text>\n              </FlexItem>\n            </Row>\n          ))}\n        </div>\n      )}\n      {searchedCommands.length === 0 && (\n        <div className={styles.defaultScreen}>\n          <Text data-testid=\"search-cmds-no-results\">No results found.</Text>\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default CHSearchOutput\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search-output/CliSearchOutput.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\nimport { setSearchedCommand } from 'uiSrc/slices/cli/cli-settings'\n\nimport CHSearchOutput from './CHSearchOutput'\n\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ninterface IMockedCommands {\n  matchedCommand: string\n  argStr?: string\n  summary?: string\n}\n\nconst mockedCommands: IMockedCommands[] = [\n  {\n    matchedCommand: 'HSET',\n    argStr: 'key field value [field value ...]',\n  },\n  {\n    matchedCommand: 'GEOADD',\n    argStr:\n      'key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]',\n  },\n  {\n    matchedCommand: 'ZADD',\n    argStr:\n      'key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]',\n  },\n  {\n    matchedCommand: 'RESET',\n    summary: 'Reset the connection',\n  },\n]\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  const { MOCK_COMMANDS_SPEC, MOCK_COMMANDS_ARRAY } =\n    jest.requireActual('uiSrc/constants')\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      spec: MOCK_COMMANDS_SPEC,\n      commandsArray: MOCK_COMMANDS_ARRAY,\n    }),\n  }\n})\n\ndescribe('CHSearchOutput', () => {\n  it('should render', () => {\n    expect(render(<CHSearchOutput searchedCommands={[]} />)).toBeTruthy()\n  })\n\n  it('should render no results', () => {\n    render(<CHSearchOutput searchedCommands={[]} />)\n    expect(screen.getByTestId('search-cmds-no-results')).toBeInTheDocument()\n  })\n\n  it('should render searched commands results', () => {\n    const searchedCommands = mockedCommands.map(\n      (command) => command.matchedCommand,\n    )\n    render(<CHSearchOutput searchedCommands={searchedCommands} />)\n    searchedCommands.forEach((command) => {\n      expect(\n        screen.getByTestId(`cli-helper-output-title-${command}`),\n      ).toBeInTheDocument()\n    })\n  })\n\n  it('should render searched commands results with proper args or summary', () => {\n    const searchedCommands = mockedCommands.map(\n      (command) => command.matchedCommand,\n    )\n    render(<CHSearchOutput searchedCommands={searchedCommands} />)\n    mockedCommands.forEach((command) => {\n      if (command.argStr) {\n        expect(\n          screen.getByTestId(\n            `cli-helper-output-args-${command.matchedCommand}`,\n          ),\n        ).toHaveTextContent(command.argStr || '')\n      } else {\n        expect(\n          screen.getByTestId(\n            `cli-helper-output-summary-${command.matchedCommand}`,\n          ),\n        ).toHaveTextContent(command.summary || '')\n      }\n    })\n  })\n\n  it('should call setSearchedCommand after click any command', () => {\n    const searchedCommands = mockedCommands.map(\n      (command) => command.matchedCommand,\n    )\n    const anySearchCommand = searchedCommands[0]\n    render(<CHSearchOutput searchedCommands={searchedCommands} />)\n    fireEvent.click(\n      screen.getByTestId(`cli-helper-output-title-${anySearchCommand}`),\n    )\n    expect(store.getActions()).toEqual([setSearchedCommand(anySearchCommand)])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search-output/index.ts",
    "content": "import CHSearchOutput from './CHSearchOutput'\n\nexport default CHSearchOutput\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/components/command-helper-search-output/styles.module.scss",
    "content": ".defaultScreen {\n  text-align: center;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-grow: 1;\n  line-height: 21px;\n}\n\n.description, .description div {\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n\n.summary, .summary div {\n  color: var(--inputPlaceholderColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/command-helper/index.ts",
    "content": "import CommandHelperWrapper from './CommandHelperWrapper'\n\nexport default CommandHelperWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/config/Config.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport { waitFor } from '@testing-library/react'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { localStorageService } from 'uiSrc/services'\nimport {\n  setFeaturesToHighlight,\n  setOnboarding,\n} from 'uiSrc/slices/app/features'\nimport { getNotifications } from 'uiSrc/slices/app/notifications'\nimport {\n  render,\n  mockedStore,\n  cleanup,\n  MOCKED_HIGHLIGHTING_FEATURES,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  getUserConfigSettings,\n  getUserSettingsSpec,\n  setSettingsPopupState,\n  userSettingsSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport {\n  appServerInfoSelector,\n  getServerInfo,\n  setServerLoaded,\n} from 'uiSrc/slices/app/info'\nimport { processCliClient } from 'uiSrc/slices/cli/cli-settings'\nimport { getRedisCommands } from 'uiSrc/slices/app/redis-commands'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { getWBTutorials } from 'uiSrc/slices/workbench/wb-tutorials'\nimport { getContentRecommendations } from 'uiSrc/slices/recommendations/recommendations'\nimport { getGuideLinks } from 'uiSrc/slices/content/guide-links'\nimport { getWBCustomTutorials } from 'uiSrc/slices/workbench/wb-custom-tutorials'\nimport { setCapability } from 'uiSrc/slices/app/context'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport Config from './Config'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    config: {\n      agreements: {},\n    },\n    spec: {\n      agreements: {},\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/app/info', () => ({\n  ...jest.requireActual('uiSrc/slices/app/info'),\n  appServerInfoSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\nconst onboardingTotalSteps = Object.keys(ONBOARDING_FEATURES)?.length\n\ndescribe('Config', () => {\n  it('should render with spec call', async () => {\n    set(\n      store,\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<Config />)\n    const afterRenderActions = [\n      setCapability(),\n      getServerInfo(),\n      getNotifications(),\n      getWBCustomTutorials(),\n      processCliClient(),\n      getRedisCommands(),\n      getContentRecommendations(),\n      getGuideLinks(),\n      getWBTutorials(),\n      getUserConfigSettings(),\n      setSettingsPopupState(false),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n    await waitFor(() =>\n      expect(store.getActions()).toContainEqual(getUserSettingsSpec()),\n    )\n  })\n\n  it('should render w/o settings spec call', async () => {\n    set(\n      store,\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<Config />)\n\n    const afterRenderActions = [\n      setCapability(),\n      getServerInfo(),\n      getNotifications(),\n      getWBCustomTutorials(),\n      processCliClient(),\n      getRedisCommands(),\n      getContentRecommendations(),\n      getGuideLinks(),\n      getWBTutorials(),\n      getUserConfigSettings(),\n      setSettingsPopupState(false),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n    await waitFor(() =>\n      expect(store.getActions()).not.toContainEqual(getUserSettingsSpec()),\n    )\n  })\n\n  it('should render expected actions when envDependent feature is off', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    const mockedStore = mockStore(initialStoreState)\n\n    render(<Config />, { store: mockedStore })\n    const afterRenderActions = [\n      setCapability(),\n      setServerLoaded(),\n      processCliClient(),\n      getRedisCommands(),\n      getContentRecommendations(),\n      getGuideLinks(),\n      getWBTutorials(),\n      getUserConfigSettings(),\n    ]\n    expect(mockedStore.getActions()).toEqual([...afterRenderActions])\n  })\n\n  it('should call the list of actions', () => {\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: {},\n      },\n      spec: {\n        agreements: {\n          eula: {\n            defaultValue: false,\n            required: true,\n            editable: false,\n            since: '1.0.0',\n            title: 'EULA: Redis Insight License Terms',\n            label: 'Label',\n          },\n        },\n      },\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    render(<Config />)\n    const afterRenderActions = [\n      setCapability(),\n      getServerInfo(),\n      getNotifications(),\n      getWBCustomTutorials(),\n      processCliClient(),\n      getRedisCommands(),\n      getContentRecommendations(),\n      getGuideLinks(),\n      getWBTutorials(),\n      getUserConfigSettings(),\n      setSettingsPopupState(true),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n  })\n\n  it('should call updateHighlightingFeatures for new user with empty features', () => {\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: null,\n      },\n    })\n    const appServerInfoSelectorMock = jest.fn().mockReturnValue({\n      buildType: BuildType.Electron,\n      appVersion: '2.0.0',\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    appServerInfoSelector.mockImplementation(appServerInfoSelectorMock)\n\n    render(<Config />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setFeaturesToHighlight({ version: '2.0.0', features: [] }),\n      ]),\n    )\n  })\n\n  it('should call updateHighlightingFeatures for existing user with proper data', () => {\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: {},\n      },\n    })\n    const appServerInfoSelectorMock = jest.fn().mockReturnValue({\n      buildType: BuildType.Electron,\n      appVersion: '2.0.0',\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    appServerInfoSelector.mockImplementation(appServerInfoSelectorMock)\n\n    render(<Config />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setFeaturesToHighlight({\n          version: '2.0.0',\n          features: MOCKED_HIGHLIGHTING_FEATURES,\n        }),\n      ]),\n    )\n  })\n\n  it('should call updateHighlightingFeatures for existing user with proper data with features from LS', () => {\n    localStorageService.get = jest\n      .fn()\n      .mockReturnValue({ version: '2.0.0', features: ['importDatabases'] })\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: {},\n      },\n    })\n    const appServerInfoSelectorMock = jest.fn().mockReturnValue({\n      buildType: BuildType.Electron,\n      appVersion: '2.0.0',\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    appServerInfoSelector.mockImplementation(appServerInfoSelectorMock)\n\n    render(<Config />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setFeaturesToHighlight({\n          version: '2.0.0',\n          features: ['importDatabases'],\n        }),\n      ]),\n    )\n  })\n\n  it('should call updateHighlightingFeatures for existing user with proper data with features from LS for different version', () => {\n    localStorageService.get = jest\n      .fn()\n      .mockReturnValue({ version: '2.0.0', features: ['importDatabases'] })\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: {},\n      },\n    })\n    const appServerInfoSelectorMock = jest.fn().mockReturnValue({\n      buildType: BuildType.Electron,\n      appVersion: '2.0.12',\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    appServerInfoSelector.mockImplementation(appServerInfoSelectorMock)\n\n    render(<Config />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setFeaturesToHighlight({\n          version: '2.0.12',\n          features: MOCKED_HIGHLIGHTING_FEATURES,\n        }),\n      ]),\n    )\n  })\n\n  it('should call setOnboarding for new user', () => {\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: null,\n      },\n    })\n    const appServerInfoSelectorMock = jest.fn().mockReturnValue({\n      buildType: BuildType.Electron,\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    appServerInfoSelector.mockImplementation(appServerInfoSelectorMock)\n\n    render(<Config />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setOnboarding({ currentStep: 0, totalSteps: onboardingTotalSteps }),\n      ]),\n    )\n  })\n\n  it('should call setOnboarding for existing user with not completed process', () => {\n    localStorageService.get = jest.fn().mockReturnValue(5)\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        agreements: {},\n      },\n    })\n    const appServerInfoSelectorMock = jest.fn().mockReturnValue({\n      buildType: BuildType.Electron,\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n    appServerInfoSelector.mockImplementation(appServerInfoSelectorMock)\n\n    render(<Config />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setOnboarding({ currentStep: 5, totalSteps: onboardingTotalSteps }),\n      ]),\n    )\n  })\n\n  it('should not show consent popup when acceptTermsAndConditionsOverwritten is true, regardless of consent differences', () => {\n    const userSettingsSelectorMock = jest.fn().mockReturnValue({\n      config: {\n        acceptTermsAndConditionsOverwritten: true,\n        agreements: {}, // Empty agreements - would normally cause a popup\n      },\n      spec: {\n        agreements: {\n          eula: {\n            defaultValue: false,\n            required: true,\n            editable: false,\n            since: '1.0.0',\n            title: 'EULA: Redis Insight License Terms',\n            label: 'Label',\n          },\n        },\n      },\n    })\n    userSettingsSelector.mockImplementation(userSettingsSelectorMock)\n\n    render(<Config />)\n\n    // Check that setSettingsPopupState is called with false\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([setSettingsPopupState(false)]),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/config/Config.tsx",
    "content": "import { useContext, useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useLocation } from 'react-router-dom'\nimport { isNumber } from 'lodash'\nimport { BrowserStorageItem, FeatureFlags } from 'uiSrc/constants'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting'\nimport { localStorageService, setObjectStorage } from 'uiSrc/services'\n\nimport {\n  appFeatureFlagsFeaturesSelector,\n  setFeaturesToHighlight,\n  setOnboarding,\n} from 'uiSrc/slices/app/features'\nimport { fetchNotificationsAction } from 'uiSrc/slices/app/notifications'\n\nimport {\n  fetchUserConfigSettings,\n  fetchUserSettingsSpec,\n  userSettingsSelector,\n  setSettingsPopupState,\n} from 'uiSrc/slices/user/user-settings'\nimport {\n  fetchServerInfo,\n  appServerInfoSelector,\n  setServerLoaded,\n} from 'uiSrc/slices/app/info'\n\nimport { isDifferentConsentsExists } from 'uiSrc/utils'\nimport { fetchUnsupportedCliCommandsAction } from 'uiSrc/slices/cli/cli-settings'\nimport { fetchRedisCommandsInfo } from 'uiSrc/slices/app/redis-commands'\nimport { fetchTutorials } from 'uiSrc/slices/workbench/wb-tutorials'\nimport { fetchCustomTutorials } from 'uiSrc/slices/workbench/wb-custom-tutorials'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { fetchContentRecommendations } from 'uiSrc/slices/recommendations/recommendations'\nimport { fetchGuideLinksAction } from 'uiSrc/slices/content/guide-links'\nimport { setCapability, setDbConfig } from 'uiSrc/slices/app/context'\n\nimport { fetchProfile } from 'uiSrc/slices/oauth/cloud'\nimport { fetchDBSettings } from 'uiSrc/slices/app/db-settings'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { DatabaseSettingsData } from 'uiSrc/slices/interfaces'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\n\nconst SETTINGS_PAGE_PATH = '/settings'\nconst Config = () => {\n  const serverInfo = useSelector(appServerInfoSelector)\n  const { id } = useSelector(connectedInstanceSelector)\n  const { config, spec } = useSelector(userSettingsSelector)\n  const {\n    [FeatureFlags.cloudSso]: cloudSsoFeature,\n    [FeatureFlags.envDependent]: envDependentFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n  const { changeTheme } = useContext(ThemeContext)\n  const { pathname } = useLocation()\n\n  const dispatch = useDispatch()\n  useEffect(() => {\n    dispatch(\n      setCapability(localStorageService?.get(BrowserStorageItem.capability)),\n    )\n    if (envDependentFeature?.flag) {\n      dispatch(fetchServerInfo())\n      dispatch(fetchNotificationsAction())\n      dispatch(fetchCustomTutorials())\n    } else {\n      dispatch(setServerLoaded())\n    }\n\n    dispatch(fetchUnsupportedCliCommandsAction())\n    dispatch(fetchRedisCommandsInfo())\n    dispatch(fetchContentRecommendations())\n    dispatch(fetchGuideLinksAction())\n\n    // get tutorials\n    dispatch(fetchTutorials())\n\n    // fetch config settings, after that take spec\n    if (pathname !== SETTINGS_PAGE_PATH) {\n      dispatch(\n        fetchUserConfigSettings(() => {\n          if (envDependentFeature?.flag) {\n            dispatch(fetchUserSettingsSpec())\n          }\n        }),\n      )\n    }\n  }, [])\n\n  useEffect(() => {\n    if (id) {\n      // fetch db settings and store them in local storage\n      dispatch(\n        fetchDBSettings(\n          id,\n          (payload: { id: string; data: DatabaseSettingsData }) => {\n            // set DB Config Storage\n            setObjectStorage(\n              BrowserStorageItem.dbConfig + payload.id,\n              payload.data,\n            )\n            // Update Redux state with the fetched config\n            dispatch(setDbConfig(payload.data))\n          },\n        ),\n      )\n    }\n  }, [id])\n\n  useEffect(() => {\n    if (config) {\n      checkAndSetTheme()\n    }\n  }, [config])\n\n  useEffect(() => {\n    if (config && spec && envDependentFeature?.flag) {\n      checkSettingsToShowPopup()\n    }\n  }, [spec])\n\n  useEffect(() => {\n    if (cloudSsoFeature?.flag) {\n      dispatch(fetchProfile())\n    }\n  }, [cloudSsoFeature?.flag])\n\n  useEffect(() => {\n    featuresHighlight()\n    onboardUsers()\n  }, [serverInfo, config])\n\n  const featuresHighlight = () => {\n    if (serverInfo?.buildType === BuildType.Electron && config) {\n      // new user, set all features as viewed\n      if (!config.agreements) {\n        updateHighlightingFeatures({\n          version: serverInfo.appVersion,\n          features: [],\n        })\n        return\n      }\n\n      const userFeatures = localStorageService.get(\n        BrowserStorageItem.featuresHighlighting,\n      )\n\n      // existing user with the same version of app, get not viewed features from LS\n      if (userFeatures?.version === serverInfo.appVersion) {\n        dispatch(setFeaturesToHighlight(userFeatures))\n        return\n      }\n\n      // existing user, no any new features viewed (after application update e.g.)\n      updateHighlightingFeatures({\n        version: serverInfo.appVersion,\n        features: Object.keys(BUILD_FEATURES),\n      })\n    }\n  }\n\n  const updateHighlightingFeatures = (data: {\n    version: string\n    features: string[]\n  }) => {\n    dispatch(setFeaturesToHighlight(data))\n    localStorageService.set(BrowserStorageItem.featuresHighlighting, data)\n  }\n\n  const onboardUsers = () => {\n    if (config) {\n      const totalSteps = Object.keys(ONBOARDING_FEATURES).length\n      const userCurrentStep = localStorageService.get(\n        BrowserStorageItem.onboardingStep,\n      )\n\n      // start onboarding for new electron users\n      if (serverInfo?.buildType === BuildType.Electron && !config.agreements) {\n        dispatch(\n          setOnboarding({\n            currentStep: 0,\n            totalSteps,\n          }),\n        )\n\n        return\n      }\n\n      // continue onboarding for all users\n      if (isNumber(userCurrentStep)) {\n        dispatch(\n          setOnboarding({\n            currentStep: userCurrentStep,\n            totalSteps,\n          }),\n        )\n      }\n    }\n  }\n\n  const checkSettingsToShowPopup = () => {\n    const specConsents = spec?.agreements\n    const appliedConsents = config?.agreements\n    dispatch(\n      setSettingsPopupState(\n        config?.acceptTermsAndConditionsOverwritten\n          ? false\n          : isDifferentConsentsExists(specConsents, appliedConsents),\n      ),\n    )\n  }\n\n  const checkAndSetTheme = () => {\n    const theme = config?.theme\n    if (theme && localStorageService.get(BrowserStorageItem.theme) !== theme)\n      changeTheme(theme)\n  }\n\n  return null\n}\n\nexport default Config\n"
  },
  {
    "path": "redisinsight/ui/src/components/config/index.ts",
    "content": "import Config from './Config'\n\nexport default Config\n"
  },
  {
    "path": "redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport ConfirmationPopover, {\n  ConfirmationPopoverProps,\n} from './ConfirmationPopover'\n\nconst mockClosePopover = jest.fn()\nconst mockButtonClick = jest.fn()\n\nconst defaultProps: ConfirmationPopoverProps = {\n  isOpen: true,\n  closePopover: mockClosePopover,\n  button: <button data-testid=\"trigger-button\">Trigger</button>,\n  confirmButton: (\n    <button data-testid=\"confirm-button\" onClick={mockButtonClick}>\n      Confirm\n    </button>\n  ),\n}\n\nconst renderConfirmationPopover = (\n  props: Partial<ConfirmationPopoverProps> = {},\n) => {\n  return render(<ConfirmationPopover {...defaultProps} {...props} />)\n}\n\ndescribe('ConfirmationPopover', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  describe('Basic Rendering', () => {\n    it('should render with required props only', () => {\n      renderConfirmationPopover()\n\n      expect(screen.getByTestId('trigger-button')).toBeInTheDocument()\n      expect(screen.getByTestId('confirm-button')).toBeInTheDocument()\n    })\n\n    it('should render the confirm button', () => {\n      renderConfirmationPopover()\n\n      const confirmButton = screen.getByTestId('confirm-button')\n      expect(confirmButton).toBeInTheDocument()\n      expect(confirmButton).toHaveTextContent('Confirm')\n    })\n\n    it('should not render title when not provided', () => {\n      renderConfirmationPopover()\n\n      expect(screen.queryByRole('heading')).not.toBeInTheDocument()\n    })\n\n    it('should not render message when not provided', () => {\n      renderConfirmationPopover()\n\n      expect(screen.queryByText(/message/i)).not.toBeInTheDocument()\n    })\n  })\n\n  describe('Optional Props', () => {\n    it('should render title when provided', () => {\n      const title = 'Confirmation Required'\n      renderConfirmationPopover({ title })\n\n      expect(screen.getByText(title)).toBeInTheDocument()\n    })\n\n    it('should render message when provided', () => {\n      const message = 'Are you sure you want to proceed?'\n      renderConfirmationPopover({ message })\n\n      expect(screen.getByText(message)).toBeInTheDocument()\n    })\n\n    it('should render both title and message when provided', () => {\n      const title = 'Delete Item'\n      const message = 'This action cannot be undone.'\n      renderConfirmationPopover({ title, message })\n\n      expect(screen.getByText(title)).toBeInTheDocument()\n      expect(screen.getByText(message)).toBeInTheDocument()\n    })\n\n    it('should render with custom confirm button', () => {\n      const customConfirmButton = (\n        <button data-testid=\"custom-confirm\" className=\"custom-class\">\n          Delete Forever\n        </button>\n      )\n      renderConfirmationPopover({ confirmButton: customConfirmButton })\n\n      const customButton = screen.getByTestId('custom-confirm')\n      expect(customButton).toBeInTheDocument()\n      expect(customButton).toHaveTextContent('Delete Forever')\n      expect(customButton).toHaveClass('custom-class')\n    })\n  })\n\n  describe('RiPopover Integration', () => {\n    it('should pass through isOpen prop to RiPopover', () => {\n      const { rerender } = renderConfirmationPopover({ isOpen: false })\n\n      expect(screen.queryByTestId('confirm-button')).not.toBeInTheDocument()\n      rerender(<ConfirmationPopover {...defaultProps} isOpen={true} />)\n      expect(screen.getByTestId('confirm-button')).toBeInTheDocument()\n    })\n\n    it('should pass through closePopover prop to RiPopover', () => {\n      renderConfirmationPopover()\n\n      fireEvent.click(document.body)\n    })\n\n    it('should pass through button prop to RiPopover', () => {\n      const customButton = (\n        <button data-testid=\"custom-trigger\">Custom Trigger</button>\n      )\n      renderConfirmationPopover({ button: customButton })\n\n      expect(screen.getByTestId('custom-trigger')).toBeInTheDocument()\n      expect(screen.getByTestId('custom-trigger')).toHaveTextContent(\n        'Custom Trigger',\n      )\n    })\n\n    it('should pass through additional RiPopover props', () => {\n      const additionalProps = {\n        anchorPosition: 'rightCenter' as const,\n        panelPaddingSize: 'l' as const,\n        'data-testid': 'custom-popover',\n      }\n\n      renderConfirmationPopover(additionalProps)\n\n      expect(screen.getByTestId('custom-popover')).toBeInTheDocument()\n    })\n  })\n\n  describe('User Interactions', () => {\n    it('should handle confirm button click', () => {\n      renderConfirmationPopover()\n\n      const confirmButton = screen.getByTestId('confirm-button')\n      fireEvent.click(confirmButton)\n\n      expect(mockButtonClick).toHaveBeenCalledTimes(1)\n    })\n\n    it('should handle trigger button interaction', () => {\n      const triggerClickMock = jest.fn()\n      const customTrigger = (\n        <button data-testid=\"trigger-button\" onClick={triggerClickMock}>\n          Trigger\n        </button>\n      )\n\n      renderConfirmationPopover({ button: customTrigger })\n\n      const triggerButton = screen.getByTestId('trigger-button')\n      fireEvent.click(triggerButton)\n\n      expect(triggerClickMock).toHaveBeenCalledTimes(1)\n    })\n\n    it('should not interfere with confirm button when disabled', () => {\n      const disabledConfirmButton = (\n        <button data-testid=\"confirm-button\" disabled onClick={mockButtonClick}>\n          Confirm\n        </button>\n      )\n\n      renderConfirmationPopover({ confirmButton: disabledConfirmButton })\n\n      const confirmButton = screen.getByTestId('confirm-button')\n      expect(confirmButton).toBeDisabled()\n\n      fireEvent.click(confirmButton)\n      expect(mockButtonClick).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('Layout and Structure', () => {\n    it('should have correct structure with title and message', () => {\n      const title = 'Confirm Action'\n      const message = 'This is a test message'\n      renderConfirmationPopover({ title, message })\n\n      const titleElement = screen.getByText(title)\n      const messageElement = screen.getByText(message)\n      const confirmButton = screen.getByTestId('confirm-button')\n\n      expect(titleElement).toBeInTheDocument()\n      expect(messageElement).toBeInTheDocument()\n      expect(confirmButton).toBeInTheDocument()\n\n      const allElements = screen.getAllByText(\n        /Confirm Action|This is a test message|Confirm/,\n      )\n      expect(allElements[0]).toBe(titleElement)\n      expect(allElements[1]).toBe(messageElement)\n    })\n\n    it('should maintain proper spacing between elements', () => {\n      const title = 'Test Title'\n      const message = 'Test Message'\n      renderConfirmationPopover({ title, message })\n\n      expect(screen.getByText(title)).toBeInTheDocument()\n      expect(screen.getByText(message)).toBeInTheDocument()\n      expect(screen.getByTestId('confirm-button')).toBeInTheDocument()\n    })\n\n    it('should handle long text content gracefully', () => {\n      const longTitle =\n        'This is a very long title that might wrap to multiple lines'\n      const longMessage =\n        'This is a very long message that should break properly with word-break styling'\n\n      renderConfirmationPopover({\n        title: longTitle,\n        message: longMessage,\n      })\n\n      expect(screen.getByText(longTitle)).toBeInTheDocument()\n      expect(screen.getByText(longMessage)).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/confirmation-popover/ConfirmationPopover.tsx",
    "content": "import React from 'react'\nimport { RiPopover, RiPopoverProps } from 'uiSrc/components'\nimport styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text, Title } from 'uiSrc/components/base/text'\n\nconst PopoverContentWrapper = styled(Col)`\n  word-break: break-word;\n  max-width: 300px;\n`\n\nexport interface ConfirmationPopoverProps\n  extends Omit<RiPopoverProps, 'children' | 'title'> {\n  title?: JSX.Element | string\n  message?: JSX.Element | string\n  appendInfo?: JSX.Element | string | null\n  confirmButton: React.ReactNode\n}\n\nconst ConfirmationPopover = (props: ConfirmationPopoverProps) => {\n  const { title, message, confirmButton, appendInfo, ...rest } = props\n\n  return (\n    <RiPopover {...rest}>\n      <PopoverContentWrapper gap=\"l\" data-testid=\"confirm-popover\">\n        {title && <Title size=\"S\">{title}</Title>}\n        {message && <Text size=\"m\">{message}</Text>}\n        {appendInfo}\n        <Row justify=\"end\">{confirmButton}</Row>\n      </PopoverContentWrapper>\n    </RiPopover>\n  )\n}\n\nexport default ConfirmationPopover\n"
  },
  {
    "path": "redisinsight/ui/src/components/confirmation-popover/index.ts",
    "content": "import ConfirmationPopover from './ConfirmationPopover'\n\nexport default ConfirmationPopover\n"
  },
  {
    "path": "redisinsight/ui/src/components/connectivity-error/ConnectivityError.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, screen, within } from '@testing-library/react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport ConnectivityError, {\n  ConnectivityErrorProps,\n} from 'uiSrc/components/connectivity-error/ConnectivityError'\n\nconst defaultProps: ConnectivityErrorProps = {\n  isLoading: false,\n  onRetry: undefined,\n  error: 'Test error',\n}\n\nconst expectErrorMessage = (error: string) => {\n  const errorElement = screen.getByTestId('connectivity-error-message')\n  expect(within(errorElement).getByText(error)).toBeInTheDocument()\n}\n\ndescribe('Connectivity error component', () => {\n  it('should show error message without retry button', () => {\n    render(<ConnectivityError {...defaultProps} />)\n    expect(screen.queryByRole('button')).not.toBeInTheDocument()\n    expectErrorMessage('Test error')\n    expect(screen.queryByTestId('suspense-loader')).not.toBeInTheDocument()\n  })\n\n  it('should show retry button if onRetry is provided', async () => {\n    const onRetry = jest.fn()\n\n    render(<ConnectivityError {...defaultProps} onRetry={onRetry} />)\n    const retryButton = await screen.findByRole('button', { name: 'Retry' })\n    expect(retryButton).toBeInTheDocument()\n    expectErrorMessage('Test error')\n\n    fireEvent.click(retryButton)\n    expect(onRetry).toHaveBeenCalledTimes(1)\n  })\n\n  it('should show loading when isLoading = true', async () => {\n    render(<ConnectivityError {...defaultProps} isLoading />)\n    expect(screen.getByTestId('suspense-loader')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/connectivity-error/ConnectivityError.tsx",
    "content": "import React from 'react'\nimport SuspenseLoader from 'uiSrc/components/main-router/components/SuspenseLoader'\n\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\n\nexport type ConnectivityErrorProps = {\n  onRetry?: () => void\n  isLoading: boolean\n  error?: string | null\n}\n\nconst ConnectivityError = ({\n  isLoading,\n  error,\n  onRetry,\n}: ConnectivityErrorProps) => (\n  <Col centered>\n    {isLoading && <SuspenseLoader />}\n    <Col centered gap=\"xl\">\n      <FlexItem data-testid=\"connectivity-error-message\">{error}</FlexItem>\n      {onRetry && (\n        <FlexItem>\n          <PrimaryButton onClick={onRetry}>Retry</PrimaryButton>\n        </FlexItem>\n      )}\n    </Col>\n  </Col>\n)\n\nexport default ConnectivityError\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport ConsentOption from './ConsentOption'\nimport { IConsent } from '../ConsentsSettings'\n\nconst mockConsent: IConsent = {\n  agreementName: 'analytics',\n  title: 'Analytics',\n  label: 'Share usage data',\n  description: 'Help us improve Redis Insight by sharing usage data.',\n  required: false,\n  editable: true,\n  disabled: false,\n  defaultValue: false,\n  displayInSetting: true,\n  since: '1.0.0',\n  linkToPrivacyPolicy: false,\n}\n\nconst mockOnChangeAgreement = jest.fn()\n\nconst defaultProps = {\n  consent: mockConsent,\n  onChangeAgreement: mockOnChangeAgreement,\n  checked: false,\n}\n\ndescribe('ConsentOption', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(render(<ConsentOption {...defaultProps} />)).toBeTruthy()\n  })\n\n  it('should render switch with correct test id', () => {\n    render(<ConsentOption {...defaultProps} />)\n    expect(screen.getByTestId('switch-option-analytics')).toBeInTheDocument()\n  })\n\n  it('should call onChangeAgreement when switch is clicked', async () => {\n    render(<ConsentOption {...defaultProps} />)\n\n    await userEvent.click(screen.getByTestId('switch-option-analytics'))\n\n    expect(mockOnChangeAgreement).toHaveBeenCalledWith(true, 'analytics')\n  })\n\n  it('should render description without privacy policy link when linkToPrivacyPolicy is false', () => {\n    const consentWithDescription = {\n      ...mockConsent,\n      description: 'Help us improve Redis Insight by sharing usage data.',\n      linkToPrivacyPolicy: false,\n    }\n\n    render(<ConsentOption {...defaultProps} consent={consentWithDescription} />)\n\n    expect(\n      screen.getByText('Help us improve Redis Insight by sharing usage data.'),\n    ).toBeInTheDocument()\n    expect(screen.queryByText('Privacy Policy')).not.toBeInTheDocument()\n  })\n\n  it('should render description with privacy policy link when linkToPrivacyPolicy is true', () => {\n    const consentWithPrivacyLink = {\n      ...mockConsent,\n      description: 'Help us improve Redis Insight by sharing usage data.',\n      linkToPrivacyPolicy: true,\n    }\n\n    render(<ConsentOption {...defaultProps} consent={consentWithPrivacyLink} />)\n\n    // Verify that the Privacy Policy link is rendered\n    expect(screen.getByText('Privacy Policy')).toBeInTheDocument()\n\n    const privacyPolicyLink = screen.getByText('Privacy Policy')\n    expect(privacyPolicyLink.closest('a')).toHaveAttribute(\n      'href',\n      'https://redis.io/legal/privacy-policy/?utm_source=redisinsight&utm_medium=app&utm_campaign=telemetry',\n    )\n  })\n\n  it('should render description with privacy policy link on settings page when linkToPrivacyPolicy is true', () => {\n    const consentWithPrivacyLink = {\n      ...mockConsent,\n      description: 'Help us improve Redis Insight by sharing usage data.',\n      linkToPrivacyPolicy: true,\n    }\n\n    render(\n      <ConsentOption\n        {...defaultProps}\n        consent={consentWithPrivacyLink}\n        isSettingsPage\n      />,\n    )\n\n    // Verify that the Privacy Policy link is rendered\n    expect(screen.getByText('Privacy Policy')).toBeInTheDocument()\n  })\n\n  it('should not render privacy policy link on settings page when linkToPrivacyPolicy is false', () => {\n    const consentWithoutPrivacyLink = {\n      ...mockConsent,\n      description: 'Help us improve Redis Insight by sharing usage data.',\n      linkToPrivacyPolicy: false,\n    }\n\n    render(\n      <ConsentOption\n        {...defaultProps}\n        consent={consentWithoutPrivacyLink}\n        isSettingsPage\n      />,\n    )\n\n    expect(\n      screen.getByText('Help us improve Redis Insight by sharing usage data.'),\n    ).toBeInTheDocument()\n    expect(screen.queryByText('Privacy Policy')).not.toBeInTheDocument()\n  })\n\n  it('should render disabled switch when consent is disabled', () => {\n    const disabledConsent = {\n      ...mockConsent,\n      disabled: true,\n    }\n\n    render(<ConsentOption {...defaultProps} consent={disabledConsent} />)\n\n    const switchElement = screen.getByTestId('switch-option-analytics')\n    expect(switchElement).toBeDisabled()\n  })\n\n  it('should render checked switch when checked prop is true', () => {\n    render(<ConsentOption {...defaultProps} checked />)\n\n    const switchElement = screen.getByTestId('switch-option-analytics')\n    expect(switchElement).toBeChecked()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.tsx",
    "content": "import React from 'react'\nimport parse from 'html-react-parser'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\n\nimport { ItemDescription } from './components'\nimport { IConsent } from '../ConsentsSettings'\n\ninterface Props {\n  consent: IConsent\n  onChangeAgreement: (checked: boolean, name: string) => void\n  checked: boolean\n  isSettingsPage?: boolean\n  withoutSpacer?: boolean\n}\n\nconst ConsentOption = (props: Props) => {\n  const {\n    consent,\n    onChangeAgreement,\n    checked,\n    isSettingsPage = false,\n    withoutSpacer = false,\n  } = props\n\n  return (\n    <FlexItem key={consent.agreementName} grow>\n      {isSettingsPage && consent.description && (\n        <>\n          <Spacer size=\"s\" />\n          <Text size=\"M\" color=\"primary\">\n            <ItemDescription\n              description={consent.description}\n              withLink={consent.linkToPrivacyPolicy}\n            />\n          </Text>\n          <Spacer size=\"m\" />\n        </>\n      )}\n      <Row gap=\"m\">\n        <FlexItem>\n          <Spacer size=\"xs\" />\n          <SwitchInput\n            checked={checked}\n            onCheckedChange={(checked) =>\n              onChangeAgreement(checked, consent.agreementName)\n            }\n            data-testid={`switch-option-${consent.agreementName}`}\n            disabled={consent?.disabled}\n          />\n        </FlexItem>\n        <FlexItem>\n          <Text size=\"M\" color=\"primary\">\n            {parse(consent.label)}\n          </Text>\n          {!isSettingsPage && consent.description && (\n            <>\n              <Spacer size=\"xs\" />\n              <Text size=\"s\" color=\"secondary\">\n                <ItemDescription\n                  description={consent.description}\n                  withLink={consent.linkToPrivacyPolicy}\n                />\n              </Text>\n            </>\n          )}\n        </FlexItem>\n      </Row>\n      {!withoutSpacer && <Spacer />}\n    </FlexItem>\n  )\n}\n\nexport default ConsentOption\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentOption/components/ItemDescription.tsx",
    "content": "import parse from 'html-react-parser'\nimport React from 'react'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { EXTERNAL_LINKS, UTM_MEDIUMS } from 'uiSrc/constants/links'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\n\ninterface ItemDescriptionProps {\n  description: string\n  withLink: boolean\n}\n\nexport const ItemDescription = ({\n  description,\n  withLink,\n}: ItemDescriptionProps) => (\n  <>\n    {description && parse(description)}\n    {withLink && (\n      <>\n        <Link\n          variant=\"inline\"\n          target=\"_blank\"\n          color=\"secondary\"\n          size=\"S\"\n          href={getUtmExternalLink(EXTERNAL_LINKS.legalPrivacyPolicy, {\n            medium: UTM_MEDIUMS.App,\n            campaign: 'telemetry',\n          })}\n        >\n          Privacy Policy\n        </Link>\n        .\n      </>\n    )}\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentOption/components/index.tsx",
    "content": "import { ItemDescription } from './ItemDescription'\n\nexport { ItemDescription }\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentOption/index.ts",
    "content": "import ConsentOption from './ConsentOption'\n\nexport default ConsentOption\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsNotifications/ConsentsNotifications.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  userEvent,\n  screen,\n  mockedStore,\n  cleanup,\n  clearStoreActions,\n} from 'uiSrc/utils/test-utils'\nimport { updateUserConfigSettings } from 'uiSrc/slices/user/user-settings'\nimport ConsentsNotifications from './ConsentsNotifications'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\nconst COMMON_CONSENT_CONTENT = {\n  defaultValue: false,\n  required: false,\n  editable: true,\n  disabled: false,\n  displayInSetting: true,\n  since: '1.0.0',\n  title: 'Title',\n  label: '<a>Text</a>',\n}\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    isShowConceptsPopup: true,\n    config: {\n      agreements: {\n        eula: true,\n        version: '1.0.1',\n      },\n    },\n    spec: {\n      version: '1.0.0',\n      agreements: {\n        eula: {\n          ...COMMON_CONSENT_CONTENT,\n          editable: false,\n          displayInSetting: false,\n          required: true,\n        },\n        eulaNew: {\n          ...COMMON_CONSENT_CONTENT,\n          editable: false,\n          displayInSetting: false,\n          required: true,\n        },\n        analytics: {\n          ...COMMON_CONSENT_CONTENT,\n          category: 'privacy',\n        },\n        notifications: {\n          ...COMMON_CONSENT_CONTENT,\n          category: 'notifications',\n        },\n        disabledConsent: {\n          ...COMMON_CONSENT_CONTENT,\n          disabled: true,\n        },\n      },\n    },\n  }),\n}))\n\ndescribe('ConsentsNotifications', () => {\n  it('should render', () => {\n    expect(render(<ConsentsNotifications />)).toBeTruthy()\n  })\n\n  it('should render proper elements', () => {\n    render(<ConsentsNotifications />)\n    expect(screen.getAllByTestId(/switch-option/)).toHaveLength(1)\n  })\n\n  describe('update settings', () => {\n    it('option change should call \"updateUserConfigSettingsAction\"', async () => {\n      render(<ConsentsNotifications />)\n\n      const elements = screen.getAllByTestId(/switch-option/)\n      await Promise.all(elements.map((el) => userEvent.click(el)))\n\n      const expectedActions = [{}].fill(updateUserConfigSettings(), 0)\n      expect(\n        clearStoreActions(store.getActions().slice(0, expectedActions.length)),\n      ).toEqual(clearStoreActions(expectedActions))\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsNotifications/ConsentsNotifications.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useFormik } from 'formik'\nimport { has } from 'lodash'\n\nimport { compareConsents } from 'uiSrc/utils'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport ConsentOption from '../ConsentOption'\nimport { IConsent, ConsentCategories } from '../ConsentsSettings'\n\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  onSubmitted?: () => void\n}\n\nconst ConsentsNotifications = () => {\n  const [consents, setConsents] = useState<IConsent[]>([])\n  const [notificationConsents, setNotificationConsents] = useState<IConsent[]>(\n    [],\n  )\n  const [initialValues, setInitialValues] = useState<any>({})\n\n  const { config, spec } = useSelector(userSettingsSelector)\n\n  const dispatch = useDispatch()\n\n  const formik = useFormik({\n    initialValues,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      submitForm(values)\n    },\n  })\n\n  useEffect(() => {\n    if (spec && config) {\n      setConsents(compareConsents(spec?.agreements, config?.agreements, true))\n    }\n  }, [spec, config])\n\n  useEffect(() => {\n    setNotificationConsents(\n      consents.filter(\n        (consent: IConsent) =>\n          !consent.required &&\n          consent.category === ConsentCategories.Notifications &&\n          consent.displayInSetting,\n      ),\n    )\n    if (consents.length) {\n      const values = consents.reduce(\n        (acc: any, cur: IConsent) => ({\n          ...acc,\n          [cur.agreementName]: cur.defaultValue,\n        }),\n        {},\n      )\n\n      if (config) {\n        Object.keys(values).forEach((value) => {\n          if (has(config.agreements, value)) {\n            values[value] = config?.agreements?.[value]\n          }\n        })\n      }\n      setInitialValues(values)\n    }\n  }, [consents])\n\n  const onChangeAgreement = (checked: boolean, name: string) => {\n    formik.setFieldValue(name, checked)\n    formik.submitForm()\n    sendEventTelemetry({\n      event: checked\n        ? TelemetryEvent.SETTINGS_NOTIFICATION_MESSAGES_ENABLED\n        : TelemetryEvent.SETTINGS_NOTIFICATION_MESSAGES_DISABLED,\n    })\n  }\n\n  const submitForm = (values: any) => {\n    dispatch(updateUserConfigSettingsAction({ agreements: values }))\n  }\n\n  return (\n    <form onSubmit={formik.handleSubmit} data-testid=\"consents-settings-form\">\n      <div className={styles.consentsWrapper}>\n        <Title size=\"XS\">Notifications</Title>\n        {notificationConsents.map((consent: IConsent) => (\n          <ConsentOption\n            consent={consent}\n            checked={formik.values[consent.agreementName] ?? false}\n            onChangeAgreement={onChangeAgreement}\n            isSettingsPage\n            key={consent.agreementName}\n          />\n        ))}\n      </div>\n    </form>\n  )\n}\n\nexport default ConsentsNotifications\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsNotifications/index.ts",
    "content": "import ConsentsNotifications from './ConsentsNotifications'\n\nexport default ConsentsNotifications\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/ConsentsPrivacy.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  userEvent,\n  screen,\n  mockedStore,\n  cleanup,\n  clearStoreActions,\n} from 'uiSrc/utils/test-utils'\nimport { updateUserConfigSettings } from 'uiSrc/slices/user/user-settings'\nimport ConsentsPrivacy from './ConsentsPrivacy'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\nconst COMMON_CONSENT_CONTENT = {\n  defaultValue: false,\n  required: false,\n  editable: true,\n  disabled: false,\n  displayInSetting: true,\n  since: '1.0.0',\n  title: 'Title',\n  label: '<a>Text</a>',\n}\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    isShowConceptsPopup: true,\n    config: {\n      agreements: {\n        eula: true,\n        version: '1.0.1',\n      },\n    },\n    spec: {\n      version: '1.0.0',\n      agreements: {\n        eula: {\n          ...COMMON_CONSENT_CONTENT,\n          editable: false,\n          displayInSetting: false,\n          required: true,\n        },\n        eulaNew: {\n          ...COMMON_CONSENT_CONTENT,\n          editable: false,\n          displayInSetting: false,\n          required: true,\n        },\n        analytics: {\n          ...COMMON_CONSENT_CONTENT,\n          category: 'privacy',\n        },\n        notifications: {\n          ...COMMON_CONSENT_CONTENT,\n          category: 'notifications',\n        },\n        disabledConsent: {\n          ...COMMON_CONSENT_CONTENT,\n          disabled: true,\n        },\n      },\n    },\n  }),\n}))\n\ndescribe('ConsentsPrivacy', () => {\n  it('should render', () => {\n    expect(render(<ConsentsPrivacy />)).toBeTruthy()\n  })\n\n  it('should render proper elements', () => {\n    render(<ConsentsPrivacy />)\n    expect(screen.getAllByTestId(/switch-option/)).toHaveLength(1)\n  })\n\n  describe('update settings', () => {\n    it('option change should call \"updateUserConfigSettingsAction\"', async () => {\n      render(<ConsentsPrivacy />)\n\n      const elements = screen.getAllByTestId(/switch-option/)\n      await Promise.all(elements.map((el) => userEvent.click(el)))\n\n      const expectedActions = [updateUserConfigSettings()]\n      expect(\n        clearStoreActions(store.getActions().slice(0, expectedActions.length)),\n      ).toEqual(clearStoreActions(expectedActions))\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/ConsentsPrivacy.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useFormik } from 'formik'\nimport { has } from 'lodash'\n\nimport { compareConsents } from 'uiSrc/utils'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport ConsentOption from '../ConsentOption'\nimport { ConsentCategories, IConsent } from '../ConsentsSettings'\n\nimport styles from '../styles.module.scss'\n\nconst ConsentsPrivacy = () => {\n  const [consents, setConsents] = useState<IConsent[]>([])\n  const [privacyConsents, setPrivacyConsents] = useState<IConsent[]>([])\n  const [initialValues, setInitialValues] = useState<any>({})\n\n  const { config, spec } = useSelector(userSettingsSelector)\n\n  const dispatch = useDispatch()\n\n  const formik = useFormik({\n    initialValues,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      submitForm(values)\n    },\n  })\n\n  useEffect(() => {\n    if (spec && config) {\n      setConsents(compareConsents(spec?.agreements, config?.agreements, true))\n    }\n  }, [spec, config])\n\n  useEffect(() => {\n    setPrivacyConsents(\n      consents.filter(\n        (consent: IConsent) =>\n          !consent.required &&\n          consent.category === ConsentCategories.Privacy &&\n          consent.displayInSetting,\n      ),\n    )\n    if (consents.length) {\n      const values = consents.reduce(\n        (acc: any, cur: IConsent) => ({\n          ...acc,\n          [cur.agreementName]: cur.defaultValue,\n        }),\n        {},\n      )\n\n      if (config) {\n        Object.keys(values).forEach((value) => {\n          if (has(config.agreements, value)) {\n            values[value] = config?.agreements?.[value]\n          }\n        })\n      }\n      setInitialValues(values)\n    }\n  }, [consents])\n\n  const onChangeAgreement = (checked: boolean, name: string) => {\n    formik.setFieldValue(name, checked)\n    formik.submitForm()\n  }\n\n  const submitForm = (values: any) => {\n    dispatch(updateUserConfigSettingsAction({ agreements: values }))\n  }\n\n  return (\n    <form onSubmit={formik.handleSubmit} data-testid=\"consents-settings-form\">\n      <div className={styles.consentsWrapper}>\n        <Text size=\"M\" color=\"primary\">\n          To optimize your experience, Redis Insight uses third-party tools.\n        </Text>\n        <Spacer size=\"m\" />\n        <Title size=\"M\">Usage Data</Title>\n        <Spacer size=\"m\" />\n        {privacyConsents.map((consent: IConsent) => (\n          <ConsentOption\n            consent={consent}\n            checked={formik.values[consent.agreementName] ?? false}\n            onChangeAgreement={onChangeAgreement}\n            isSettingsPage\n            key={consent.agreementName}\n          />\n        ))}\n      </div>\n    </form>\n  )\n}\n\nexport default ConsentsPrivacy\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/index.ts",
    "content": "import ConsentsPrivacy from './ConsentsPrivacy'\n\nexport default ConsentsPrivacy\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsSettings.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  userEvent,\n  render,\n  screen,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\nimport ConsentsSettings from './ConsentsSettings'\n\nconst BTN_SUBMIT = 'btn-submit'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\nconst COMMON_CONSENT_CONTENT = {\n  defaultValue: false,\n  required: false,\n  editable: true,\n  disabled: false,\n  displayInSetting: true,\n  since: '1.0.0',\n  title: 'Title',\n  label: '<a>Text</a>',\n}\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    isShowConceptsPopup: true,\n    config: {\n      agreements: {\n        eula: true,\n        version: '1.0.1',\n      },\n    },\n    spec: {\n      version: '1.0.0',\n      agreements: {\n        eula: {\n          ...COMMON_CONSENT_CONTENT,\n          editable: false,\n          displayInSetting: false,\n          required: true,\n        },\n        eulaNew: {\n          ...COMMON_CONSENT_CONTENT,\n          editable: false,\n          displayInSetting: false,\n          required: true,\n        },\n        analytics: {\n          ...COMMON_CONSENT_CONTENT,\n          category: 'privacy',\n        },\n        notifications: {\n          ...COMMON_CONSENT_CONTENT,\n          category: 'notifications',\n        },\n        disabledConsent: {\n          ...COMMON_CONSENT_CONTENT,\n          disabled: true,\n        },\n      },\n    },\n  }),\n}))\n\ndescribe('ConsentsSettings', () => {\n  it('should render', () => {\n    expect(render(<ConsentsSettings />)).toBeTruthy()\n  })\n\n  it('should render proper elements', () => {\n    render(<ConsentsSettings />)\n    expect(screen.getAllByTestId(/switch-option/)).toHaveLength(4)\n  })\n\n  it('should be disabled submit button with required options with false value', () => {\n    render(<ConsentsSettings />)\n    expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled()\n  })\n\n  it('should be able to submit with required options with true value', async () => {\n    render(<ConsentsSettings />)\n    const elements = screen.getAllByTestId(/switch-option/)\n    await Promise.all(elements.map((el) => userEvent.click(el)))\n    expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsSettings.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { FormikErrors, useFormik } from 'formik'\nimport { isEmpty, forEach } from 'lodash'\nimport cx from 'classnames'\n\nimport { HorizontalRule, RiTooltip } from 'uiSrc/components'\nimport { compareConsents } from 'uiSrc/utils'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport ConsentOption from './ConsentOption'\n\nimport styles from './styles.module.scss'\nimport { StyledContainer } from './styles'\n\ninterface Values {\n  [key: string]: string\n}\n\nexport interface IConsent {\n  defaultValue: boolean\n  displayInSetting: boolean\n  required: boolean\n  editable: boolean\n  disabled: boolean\n  linkToPrivacyPolicy: boolean\n  category?: string\n  since: string\n  title: string\n  label: string\n  agreementName: string\n  description?: string\n}\n\nexport enum ConsentCategories {\n  Notifications = 'notifications',\n  Privacy = 'privacy',\n}\n\nexport interface Props {\n  onSubmitted?: () => void\n}\n\nconst ConsentsSettings = ({ onSubmitted }: Props) => {\n  const [consents, setConsents] = useState<IConsent[]>([])\n  const [privacyConsents, setPrivacyConsents] = useState<IConsent[]>([])\n  const [notificationConsents, setNotificationConsents] = useState<IConsent[]>(\n    [],\n  )\n  const [requiredConsents, setRequiredConsents] = useState<IConsent[]>([])\n  const [initialValues, setInitialValues] = useState<any>({})\n  const [errors, setErrors] = useState<FormikErrors<Values>>({})\n  const [isRecommended, setIsRecommended] = useState<boolean>(false)\n  const [valuesBuffer, setValuesBuffer] = useState<Values>({})\n\n  const { config, spec } = useSelector(userSettingsSelector)\n\n  const dispatch = useDispatch()\n\n  const submitIsDisabled = () => !isEmpty(errors)\n\n  const validate = (values: any) => {\n    const errs: FormikErrors<any> = {}\n    requiredConsents.forEach((consent) => {\n      if (!values[consent.agreementName]) {\n        errs[consent.agreementName] = consent.agreementName\n      }\n    })\n    setErrors(errs)\n    return errs\n  }\n\n  const selectAll = (checked: boolean) => {\n    setIsRecommended(checked)\n\n    if (checked) {\n      const newBufferValues: Values = {}\n      consents.forEach((consent) => {\n        if (!consent.required && !consent.disabled) {\n          newBufferValues[consent.agreementName] =\n            formik.values[consent.agreementName]\n          formik.setFieldValue(consent.agreementName, true)\n        }\n        setValuesBuffer(newBufferValues)\n      })\n    } else {\n      consents.forEach((consent) => {\n        if (!consent.required && !consent.disabled) {\n          formik.setFieldValue(\n            consent.agreementName,\n            valuesBuffer[consent.agreementName],\n          )\n        }\n      })\n    }\n  }\n\n  const formik = useFormik({\n    initialValues,\n    validate,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      submitForm(values)\n    },\n  })\n\n  useEffect(() => {\n    if (spec && config) {\n      setConsents(compareConsents(spec?.agreements, config?.agreements))\n    }\n  }, [spec, config])\n\n  useEffect(() => {\n    if (!isRecommended) {\n      setValuesBuffer({})\n    }\n  }, [isRecommended])\n\n  useEffect(() => {\n    setRequiredConsents(\n      consents.filter((consent: IConsent) => consent.required),\n    )\n    setPrivacyConsents(\n      consents.filter(\n        (consent: IConsent) =>\n          !consent.required && consent.category === ConsentCategories.Privacy,\n      ),\n    )\n    setNotificationConsents(\n      consents.filter(\n        (consent: IConsent) =>\n          !consent.required &&\n          consent.category === ConsentCategories.Notifications,\n      ),\n    )\n    if (consents.length) {\n      const values = consents.reduce(\n        (acc: any, cur: IConsent) => ({\n          ...acc,\n          [cur.agreementName]: cur.defaultValue,\n        }),\n        {},\n      )\n\n      setInitialValues(values)\n    }\n  }, [consents])\n\n  useEffect(() => {\n    formik.validateForm(initialValues)\n  }, [requiredConsents])\n\n  useEffect(() => {\n    setIsRecommended(checkIsRecommended())\n  }, [formik.values])\n\n  const checkIsRecommended = () => {\n    let recommended = true\n    forEach(privacyConsents, (consent) => {\n      if (!formik.values[consent?.agreementName] && !consent.disabled) {\n        recommended = false\n        return false\n      }\n      return true\n    })\n\n    forEach(notificationConsents, (consent) => {\n      if (!formik.values[consent?.agreementName] && !consent.disabled) {\n        recommended = false\n        return false\n      }\n      return true\n    })\n\n    return recommended\n  }\n\n  const onChangeAgreement = (checked: boolean, name: string) => {\n    formik.setFieldValue(name, checked)\n  }\n\n  const submitForm = (values: any) => {\n    if (submitIsDisabled()) {\n      return\n    }\n    // have only one switcher in notificationConsents\n    if (notificationConsents.length) {\n      sendEventTelemetry({\n        event: values[notificationConsents[0]?.agreementName]\n          ? TelemetryEvent.SETTINGS_NOTIFICATION_MESSAGES_ENABLED\n          : TelemetryEvent.SETTINGS_NOTIFICATION_MESSAGES_DISABLED,\n      })\n    }\n    const settings: Record<string, any> = { agreements: values }\n    if (values.analytics) {\n      settings.analyticsReason = 'user'\n    }\n    dispatch(updateUserConfigSettingsAction(settings, onSubmitted))\n  }\n\n  return (\n    <form onSubmit={formik.handleSubmit} data-testid=\"consents-settings-form\">\n      <div className={styles.consentsWrapper}>\n        <Spacer size=\"m\" />\n        {consents.length > 1 && (\n          <>\n            <FlexItem>\n              <Row gap=\"m\">\n                <FlexItem>\n                  <Spacer size=\"xs\" />\n                  <SwitchInput\n                    checked={isRecommended}\n                    onCheckedChange={selectAll}\n                    data-testid=\"switch-option-recommended\"\n                  />\n                </FlexItem>\n                <FlexItem>\n                  <Text color=\"primary\">Use recommended settings</Text>\n                  <Spacer size=\"xs\" />\n                  <Text size=\"s\" color=\"secondary\">\n                    Select to activate all listed options.\n                  </Text>\n                </FlexItem>\n              </Row>\n            </FlexItem>\n            <HorizontalRule\n              margin=\"m\"\n              className={cx({\n                [styles.pluginWarningHR]: !!requiredConsents.length,\n              })}\n            />\n          </>\n        )}\n\n        {!!privacyConsents.length && (\n          <>\n            <Spacer size=\"l\" />\n            <Title size=\"M\" color=\"primary\">\n              Privacy settings\n            </Title>\n            <Spacer size=\"xs\" />\n            <Text size=\"s\" color=\"secondary\">\n              To optimize your experience, Redis Insight uses third-party tools.\n            </Text>\n            <Spacer size=\"m\" />\n          </>\n        )}\n        <StyledContainer>\n          {privacyConsents.map((consent: IConsent) => (\n            <ConsentOption\n              consent={consent}\n              checked={formik.values[consent.agreementName] ?? false}\n              onChangeAgreement={onChangeAgreement}\n              key={consent.agreementName}\n              withoutSpacer\n            />\n          ))}\n        </StyledContainer>\n\n        {!!notificationConsents.length && (\n          <>\n            <Spacer size=\"m\" />\n            <Title size=\"M\" color=\"primary\">\n              Notifications\n            </Title>\n            <Spacer size=\"m\" />\n          </>\n        )}\n        <StyledContainer>\n          {notificationConsents.map((consent: IConsent) => (\n            <ConsentOption\n              consent={consent}\n              checked={formik.values[consent.agreementName] ?? false}\n              onChangeAgreement={onChangeAgreement}\n              key={consent.agreementName}\n              withoutSpacer\n            />\n          ))}\n        </StyledContainer>\n      </div>\n      {requiredConsents.length ? (\n        <>\n          <Spacer size=\"l\" />\n          <Text color=\"secondary\" size=\"s\">\n            Use of Redis Insight is governed by your signed agreement with\n            Redis, or, if none, by the{' '}\n            <Link\n              variant=\"inline\"\n              size=\"S\"\n              color=\"secondary\"\n              target=\"_blank\"\n              href=\"https://redis.io/software-subscription-agreement/?utm_source=redisinsight&utm_medium=app&utm_campaign=EULA\"\n            >\n              Redis Enterprise Software Subscription Agreement\n            </Link>\n            . If no agreement applies, use is subject to the{' '}\n            <Link\n              variant=\"inline\"\n              size=\"S\"\n              color=\"secondary\"\n              target=\"_blank\"\n              href=\"https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE\"\n            >\n              Server Side Public License\n            </Link>\n          </Text>\n          <Spacer size=\"m\" />\n        </>\n      ) : (\n        <Spacer />\n      )}\n\n      <Row align=\"center\" justify=\"between\" responsive={false}>\n        <FlexItem>\n          {requiredConsents.map((consent: IConsent) => (\n            <ConsentOption\n              consent={consent}\n              checked={formik.values[consent.agreementName] ?? false}\n              onChangeAgreement={onChangeAgreement}\n              withoutSpacer\n              key={consent.agreementName}\n            />\n          ))}\n        </FlexItem>\n      </Row>\n      <Spacer />\n      <Row justify=\"end\">\n        <FlexItem>\n          <RiTooltip\n            position=\"top\"\n            anchorClassName=\"euiToolTip__btn-disabled\"\n            content={\n              submitIsDisabled() ? (\n                <span>\n                  {Object.values(errors).map((err) => [\n                    spec?.agreements[err as string]?.requiredText,\n                    <br key={err} />,\n                  ])}\n                </span>\n              ) : null\n            }\n          >\n            <PrimaryButton\n              className=\"btn-add\"\n              type=\"submit\"\n              onClick={() => {}}\n              disabled={submitIsDisabled()}\n              icon={submitIsDisabled() ? InfoIcon : undefined}\n              data-testid=\"btn-submit\"\n            >\n              Submit\n            </PrimaryButton>\n          </RiTooltip>\n        </FlexItem>\n      </Row>\n    </form>\n  )\n}\n\nexport default ConsentsSettings\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsSettingsPopup/ConsentsSettingsPopup.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport ConsentsSettingsPopup from './ConsentsSettingsPopup'\n\ndescribe('ConsentsSettingsPopup', () => {\n  it('should render', () => {\n    expect(render(<ConsentsSettingsPopup />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/ConsentsSettingsPopup/ConsentsSettingsPopup.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport { BuildType } from 'uiSrc/constants/env'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { Pages } from 'uiSrc/constants'\nimport { ConsentsSettings } from 'uiSrc/components'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Modal } from 'uiSrc/components/base/display'\nimport styles from '../styles.module.scss'\n\nconst ConsentsSettingsPopup = () => {\n  const history = useHistory()\n  const { server } = useSelector(appInfoSelector)\n\n  const handleSubmitted = () => {\n    if (\n      server &&\n      server.buildType === BuildType.RedisStack &&\n      server?.fixedDatabaseId\n    ) {\n      history.push(Pages.browser(server.fixedDatabaseId))\n    }\n  }\n\n  useEffect(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CONSENT_MENU_VIEWED,\n    })\n  }, [])\n\n  return (\n    <Modal\n      open\n      persistent\n      width=\"600px\"\n      className={styles.consentsPopup}\n      data-testid=\"consents-settings-popup\"\n      title={\n        <Row justify=\"between\">\n          <FlexItem>\n            <Title size=\"XL\" variant=\"semiBold\" color=\"primary\">\n              EULA and Privacy settings\n            </Title>\n          </FlexItem>\n          <FlexItem>\n            <RiIcon className={styles.redisIcon} type=\"RedisLogoFullIcon\" />\n          </FlexItem>\n        </Row>\n      }\n      content={<ConsentsSettings onSubmitted={handleSubmitted} />}\n    />\n  )\n}\n\nexport default ConsentsSettingsPopup\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/index.ts",
    "content": "import ConsentsSettings from './ConsentsSettings'\nimport ConsentsSettingsPopup from './ConsentsSettingsPopup/ConsentsSettingsPopup'\nimport ConsentsPrivacy from './ConsentsPrivacy'\nimport ConsentsNotifications from './ConsentsNotifications'\n\nexport {\n  ConsentsSettings,\n  ConsentsSettingsPopup,\n  ConsentsPrivacy,\n  ConsentsNotifications,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/styles.module.scss",
    "content": ".redisIcon {\n  width: 128px;\n  height: 34px;\n}\n\n.consentsPopup {\n  max-width: 94vw;\n  max-height: calc(100vh - 60px) !important;\n  height: auto;\n}\n\n.modalHeader {\n  padding: 30px 42px 12px !important;\n  padding-bottom: 4px;\n}\n\n.modalBody :global(.euiModalBody__overflow) {\n  padding: 0 42px 30px !important;\n}\n\n.consentsWrapper {\n  @include eui.scrollBar;\n  overflow-x: hidden;\n  overflow-y: auto;\n  max-height: calc(100vh - 290px);\n}\n\n.pluginWarningHR {\n  margin-bottom: 0 !important;\n}\n\n.requiredHR {\n  margin: 0 !important;\n}\n\n.overlay {\n  &.overlay_dark {\n    background-image: url('uiSrc/assets/img/welcome_bg_dark.png') !important;\n  }\n\n  &.overlay_light {\n    background-image: url('uiSrc/assets/img/welcome_bg_light.png') !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/consents-settings/styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from '../base/layout/flex'\n\nexport const StyledContainer = styled(FlexItem)`\n  background: ${({ theme }) => theme.semantic.color.background.neutral200};\n  padding: 16px;\n  border-radius: 8px;\n  gap: 16px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/CopyButton.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\n\nimport { render, fireEvent, screen, waitFor, act } from 'uiSrc/utils/test-utils'\n\nimport { CopyButton } from './CopyButton'\nimport { CopyButtonProps } from './CopyButton.types'\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  handleCopy: jest.fn(),\n}))\n\nimport { handleCopy } from 'uiSrc/utils'\n\nconst mockedHandleCopy = jest.mocked(handleCopy)\n\ndescribe('CopyButton', () => {\n  const defaultProps: CopyButtonProps = {\n    copy: faker.lorem.sentence(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<CopyButtonProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(<CopyButton {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    jest.useFakeTimers()\n  })\n\n  afterEach(() => {\n    jest.useRealTimers()\n  })\n\n  it('should render', () => {\n    expect(renderComponent()).toBeTruthy()\n  })\n\n  it('should render copy button with default test id', () => {\n    renderComponent()\n\n    expect(screen.getByTestId('copy-button-btn')).toBeInTheDocument()\n  })\n\n  it('should render copy button with custom test id', () => {\n    const customTestId = faker.string.alphanumeric(10)\n    renderComponent({ 'data-testid': customTestId })\n\n    expect(screen.getByTestId(`${customTestId}-btn`)).toBeInTheDocument()\n  })\n\n  it('should copy text to clipboard when clicked', () => {\n    const textToCopy = faker.lorem.sentence()\n    renderComponent({ copy: textToCopy })\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    expect(mockedHandleCopy).toHaveBeenCalledWith(textToCopy)\n  })\n\n  it('should call onCopy callback when clicked', async () => {\n    const onCopyMock = jest.fn()\n    renderComponent({ onCopy: onCopyMock })\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    await waitFor(() => {\n      expect(onCopyMock).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  it('should show success badge after copying', () => {\n    renderComponent()\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    expect(screen.getByTestId('copy-button-badge')).toBeInTheDocument()\n    expect(screen.getByText('Copied')).toBeInTheDocument()\n  })\n\n  it('should show custom success label after copying', () => {\n    const customLabel = faker.lorem.word()\n    renderComponent({ successLabel: customLabel })\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    expect(screen.getByText(customLabel)).toBeInTheDocument()\n  })\n\n  it('should hide button when showing success badge', () => {\n    renderComponent()\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    expect(screen.queryByTestId('copy-button-btn')).not.toBeInTheDocument()\n    expect(screen.getByTestId('copy-button-badge')).toBeInTheDocument()\n  })\n\n  it('should reset to button state after resetDuration', async () => {\n    const resetDuration = 1000\n    renderComponent({ resetDuration })\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    expect(screen.getByTestId('copy-button-badge')).toBeInTheDocument()\n\n    act(() => {\n      jest.advanceTimersByTime(resetDuration)\n    })\n\n    await waitFor(() => {\n      expect(screen.getByTestId('copy-button-btn')).toBeInTheDocument()\n      expect(screen.queryByTestId('copy-button-badge')).not.toBeInTheDocument()\n    })\n  })\n\n  it('should not copy when button is disabled', () => {\n    renderComponent({ disabled: true })\n\n    const button = screen.getByTestId('copy-button-btn')\n    expect(button).toBeDisabled()\n  })\n\n  it('should stop event propagation when clicked', () => {\n    const parentClickHandler = jest.fn()\n    render(\n      <div onClick={parentClickHandler}>\n        <CopyButton {...defaultProps} />\n      </div>,\n    )\n\n    fireEvent.click(screen.getByTestId('copy-button-btn'))\n\n    expect(parentClickHandler).not.toHaveBeenCalled()\n  })\n\n  it('should use custom aria-label when provided', () => {\n    const customAriaLabel = faker.lorem.words(2)\n    renderComponent({ 'aria-label': customAriaLabel })\n\n    const button = screen.getByTestId('copy-button-btn')\n    expect(button).toHaveAttribute('aria-label', customAriaLabel)\n  })\n\n  it('should use default aria-label when not provided', () => {\n    renderComponent()\n\n    const button = screen.getByTestId('copy-button-btn')\n    expect(button).toHaveAttribute('aria-label', 'Copy')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/CopyButton.styles.ts",
    "content": "import styled, { keyframes } from 'styled-components'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nconst fadeOut = keyframes`\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n`\n\n/**\n * Props for StyledCopiedBadge component\n * @member $fadeOutDuration - Duration of the fade-out animation in milliseconds\n */\ninterface StyledCopiedBadgeProps {\n  /** Duration of the fade-out animation in milliseconds */\n  $fadeOutDuration: number\n  /** Whether the badge is empty */\n  $isEmpty: boolean\n}\n\nconst isEmptyStyles = `\n  padding: 0;\n  gap: 0;\n`\n\nexport const StyledCopiedBadge = styled(RiBadge)<StyledCopiedBadgeProps>`\n  border-color: transparent;\n  background-color: transparent;\n  animation: ${fadeOut} ${({ $fadeOutDuration }) => $fadeOutDuration}ms\n    ease-in-out forwards;\n  ${({ $isEmpty }) => $isEmpty && isEmptyStyles}\n`\n\nexport const StyledTooltipContainer = styled(Row)`\n  // aiming at the tooltip trigger container, try to make its layout more consistent with the button\n  & > span[data-state] {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/CopyButton.tsx",
    "content": "import React, { useEffect, useState } from 'react'\n\nimport { handleCopy as handleCopyUtil } from 'uiSrc/utils'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CopyIcon } from 'uiSrc/components/base/icons'\n\nimport { ButtonWithTooltip } from './components'\nimport { StyledCopiedBadge, StyledTooltipContainer } from './CopyButton.styles'\nimport { CopyButtonProps } from './CopyButton.types'\n\nconst DEFAULT_TOOLTIP_CONTENT = 'Copy'\n\nexport const CopyButton = ({\n  onCopy,\n  id,\n  copy = '',\n  successLabel = 'Copied',\n  fadeOutDuration = 2500,\n  resetDuration = 2500,\n  'data-testid': dataTestId = 'copy-button',\n  'aria-label': ariaLabel,\n  withTooltip = true,\n  tooltipConfig,\n  className,\n  disabled = false,\n}: CopyButtonProps) => {\n  const [isCopied, setIsCopied] = useState(false)\n\n  const buttonAriaLabel = ariaLabel ?? DEFAULT_TOOLTIP_CONTENT\n\n  useEffect(() => {\n    if (!isCopied) {\n      return\n    }\n    const timeout = setTimeout(() => {\n      setIsCopied(false)\n    }, resetDuration)\n\n    return () => clearTimeout(timeout)\n  }, [isCopied, resetDuration])\n\n  const handleCopyClick = async (event: React.MouseEvent) => {\n    event.stopPropagation()\n\n    handleCopyUtil(copy)\n\n    setIsCopied(true)\n\n    if (onCopy) {\n      await onCopy(event)\n    }\n  }\n\n  const button = (\n    <IconButton\n      icon={CopyIcon}\n      id={id}\n      aria-label={buttonAriaLabel}\n      onClick={handleCopyClick}\n      disabled={disabled}\n      data-testid={`${dataTestId}-btn`}\n    />\n  )\n\n  return (\n    <StyledTooltipContainer\n      grow={false}\n      align=\"center\"\n      className={className}\n      justify=\"center\"\n    >\n      {!isCopied && (\n        <ButtonWithTooltip\n          button={button}\n          withTooltip={withTooltip}\n          tooltipConfig={tooltipConfig}\n        />\n      )}\n      {isCopied && (\n        <StyledCopiedBadge\n          label={successLabel}\n          withIcon\n          variant=\"success\"\n          $fadeOutDuration={fadeOutDuration}\n          $isEmpty={!successLabel}\n          data-testid={`${dataTestId}-badge`}\n        />\n      )}\n    </StyledTooltipContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/CopyButton.types.ts",
    "content": "import { RiTooltipProps } from 'uiSrc/components'\n\nexport interface CopyButtonProps {\n  /** Text to copy to clipboard */\n  copy?: string\n  /** Optional callback called after copy action and state update */\n  onCopy?: (...args: any[]) => void | Promise<void>\n  /** Optional ID for the copy button */\n  id?: string\n  /** Label text for the success badge */\n  successLabel?: string\n  /** Duration of the fade-out animation in milliseconds */\n  fadeOutDuration?: number\n  /** Duration before resetting the copied state in milliseconds */\n  resetDuration?: number\n  /** Test ID for the component */\n  'data-testid'?: string\n  'aria-label'?: string\n  /** Whether to show tooltip on hover (default: true) */\n  withTooltip?: boolean\n  /** Tooltip configuration options */\n  tooltipConfig?: Omit<RiTooltipProps, 'children'>\n  /** Class name for the component, can override with Styled Components */\n  className?: string\n  /** Whether the button is disabled */\n  disabled?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/components/ButtonWithTooltip/ButtonWithTooltip.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\n\nimport { ButtonWithTooltipProps } from './ButtonWithTooltip.types'\n\nconst DEFAULT_TOOLTIP_CONTENT = 'Copy'\n\nexport const ButtonWithTooltip = ({\n  button,\n  withTooltip,\n  tooltipConfig,\n}: ButtonWithTooltipProps) => {\n  if (withTooltip) {\n    return (\n      <RiTooltip\n        position=\"right\"\n        content={DEFAULT_TOOLTIP_CONTENT}\n        {...tooltipConfig}\n      >\n        {button}\n      </RiTooltip>\n    )\n  }\n\n  return <>{button}</>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/components/ButtonWithTooltip/ButtonWithTooltip.types.ts",
    "content": "import { CopyButtonProps } from '../../CopyButton.types'\n\nexport interface ButtonWithTooltipProps {\n  button: React.ReactNode\n  withTooltip: boolean\n  tooltipConfig: CopyButtonProps['tooltipConfig']\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/components/ButtonWithTooltip/index.ts",
    "content": "export { ButtonWithTooltip } from './ButtonWithTooltip'\nexport type { ButtonWithTooltipProps } from './ButtonWithTooltip.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/components/index.ts",
    "content": "export { ButtonWithTooltip } from './ButtonWithTooltip'\nexport type { ButtonWithTooltipProps } from './ButtonWithTooltip'\n"
  },
  {
    "path": "redisinsight/ui/src/components/copy-button/index.ts",
    "content": "export { CopyButton } from './CopyButton'\nexport type { CopyButtonProps } from './CopyButton.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/css.d.ts",
    "content": "declare module '*.scss' {\n  const content: { [className: string]: string }\n  export default content\n}\ndeclare module '*.scss?inline' {\n  const content: { [className: string]: string }\n  export default content\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/DatabaseListModules.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  RedisDefaultModules,\n  DATABASE_LIST_MODULES_TEXT,\n} from 'uiSrc/slices/interfaces'\nimport { fireEvent, render } from 'uiSrc/utils/test-utils'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport { DatabaseListModules } from './DatabaseListModules'\nimport { DatabaseListModulesProps } from './DatabaseListModules.types'\n\nconst mockedProps = mock<DatabaseListModulesProps>()\n\nconst modulesMock: AdditionalRedisModule[] = [\n  { name: RedisDefaultModules.AI },\n  { name: RedisDefaultModules.Bloom },\n  { name: RedisDefaultModules.Gears },\n  { name: RedisDefaultModules.Graph },\n  { name: RedisDefaultModules.ReJSON },\n  { name: RedisDefaultModules.Search },\n  { name: RedisDefaultModules.TimeSeries },\n]\n\ndescribe('DatabaseListModules', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <DatabaseListModules\n          {...instance(mockedProps)}\n          modules={modulesMock}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should copy module name to clipboard when clicked', async () => {\n    const writeTextMock = jest.fn()\n    Object.assign(navigator, {\n      clipboard: { writeText: writeTextMock },\n    })\n\n    const { queryByTestId } = render(\n      <DatabaseListModules {...instance(mockedProps)} modules={modulesMock} />,\n    )\n\n    const term = DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.Search]\n    const module = queryByTestId(`${term}_module`)\n    expect(module).toBeInTheDocument()\n\n    fireEvent.click(module!)\n\n    expect(writeTextMock).toHaveBeenCalledWith(term)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/DatabaseListModules.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { DatabaseListModules } from './DatabaseListModules'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport type { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\n\nconst meta: Meta<typeof DatabaseListModules> = {\n  component: DatabaseListModules,\n  tags: ['autodocs'],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nconst commonModules: AdditionalRedisModule[] = [\n  { name: RedisDefaultModules.Search },\n  { name: RedisDefaultModules.ReJSON },\n  { name: RedisDefaultModules.TimeSeries },\n]\n\nconst allModules: AdditionalRedisModule[] = [\n  { name: RedisDefaultModules.AI },\n  { name: RedisDefaultModules.Bloom },\n  { name: RedisDefaultModules.Gears },\n  { name: RedisDefaultModules.RedisGears },\n  { name: RedisDefaultModules.RedisGears2 },\n  { name: RedisDefaultModules.Graph },\n  { name: RedisDefaultModules.ReJSON },\n  { name: RedisDefaultModules.Search },\n  { name: RedisDefaultModules.SearchLight },\n  { name: RedisDefaultModules.TimeSeries },\n  { name: RedisDefaultModules.FT },\n  { name: RedisDefaultModules.FTL },\n  { name: RedisDefaultModules.VectorSet },\n]\nconst withVectorSet: AdditionalRedisModule[] = [\n  {\n    name: RedisDefaultModules.Bloom,\n    version: 80200,\n    semanticVersion: '8.2.0',\n  },\n  {\n    name: RedisDefaultModules.VectorSet,\n    version: 1,\n    semanticVersion: '0.0.1',\n  },\n  {\n    name: RedisDefaultModules.Search,\n    version: 80201,\n    semanticVersion: '8.2.1',\n  },\n  {\n    name: RedisDefaultModules.TimeSeries,\n    version: 80200,\n    semanticVersion: '8.2.0',\n  },\n  {\n    name: RedisDefaultModules.ReJSON,\n    version: 80200,\n    semanticVersion: '8.2.0',\n  },\n]\n\nconst customModules: AdditionalRedisModule[] = [\n  { name: 'CustomModule1' },\n  { name: 'AnotherCustomModule', semanticVersion: '1.0.0' },\n  { name: RedisDefaultModules.Search },\n]\n\nexport const AllModules: Story = {\n  args: {\n    modules: allModules,\n  },\n}\n\nexport const WithVectorSet: Story = {\n  args: {\n    modules: withVectorSet,\n  },\n}\n\nexport const SingleModule: Story = {\n  args: {\n    modules: [{ name: RedisDefaultModules.Search }],\n  },\n}\n\nexport const CommonModules: Story = {\n  args: {\n    modules: commonModules,\n  },\n}\n\nexport const WithCustomModules: Story = {\n  args: {\n    modules: customModules,\n  },\n}\n\nexport const InCircle: Story = {\n  args: {\n    modules: commonModules,\n    inCircle: true,\n  },\n}\n\nexport const Highlighted: Story = {\n  args: {\n    modules: commonModules,\n    highlight: true,\n  },\n}\n\nexport const InCircleAndHighlighted: Story = {\n  args: {\n    modules: commonModules,\n    inCircle: true,\n    highlight: true,\n  },\n}\n\nexport const WithCustomContent: Story = {\n  args: {\n    modules: commonModules,\n    content: <span>Custom Content</span>,\n  },\n}\n\nexport const WithTooltipTitle: Story = {\n  args: {\n    modules: commonModules,\n    tooltipTitle: 'Database Capabilities',\n  },\n}\n\nexport const WithoutStyles: Story = {\n  args: {\n    modules: commonModules,\n    withoutStyles: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/DatabaseListModules.styles.ts",
    "content": "import React from 'react'\nimport styled, { css } from 'styled-components'\n\nexport interface StyledContainerProps {\n  $unstyled?: boolean\n  $highlight?: boolean\n  $inCircle?: boolean\n  children?: React.ReactNode\n}\n\nconst highlightStyles = css`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral300};\n  border-radius: ${({ theme }) => theme.core.space.space150};\n`\n\nconst inCircleStyles = css`\n  height: ${({ theme }) => theme.core.space.space400};\n`\n\nexport const StyledContainer = styled.div<StyledContainerProps>`\n  ${({ $unstyled, $highlight, $inCircle }) =>\n    !$unstyled &&\n    css`\n      height: ${({ theme }) => theme.core.space.space300};\n      line-height: ${({ theme }) => theme.core.space.space250};\n      display: inline-block;\n      width: auto;\n      padding-left: ${({ theme }) => theme.core.space.space050};\n      padding-right: ${({ theme }) => theme.core.space.space050};\n\n      ${$highlight && highlightStyles}\n\n      ${$inCircle && inCircleStyles}\n    `}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport {\n  IDatabaseModule,\n  sortModules,\n  transformModule,\n} from 'uiSrc/utils/modules'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport { DatabaseModulesList, DatabaseModuleContent } from './components'\nimport { DatabaseListModulesProps } from './DatabaseListModules.types'\nimport { StyledContainer } from './DatabaseListModules.styles'\n\nexport type { DatabaseListModulesProps }\n\nimport styles from './styles.module.scss'\n\nexport const DatabaseListModules = React.memo(\n  (props: DatabaseListModulesProps) => {\n    const {\n      content,\n      modules,\n      inCircle,\n      highlight,\n      tooltipTitle,\n      maxViewModules,\n      withoutStyles,\n    } = props\n\n    const { newModules, contentItems } = useMemo(() => {\n      const mainModules: IDatabaseModule[] = []\n\n      const newModules: IDatabaseModule[] = sortModules(\n        modules?.map((module) => {\n          const transformed = transformModule(module)\n          mainModules.push({\n            icon: transformed.icon,\n            content: transformed.content,\n            abbreviation: transformed.abbreviation,\n            moduleName: transformed.moduleName,\n          })\n          return transformed\n        }),\n      )\n      // set count of hidden modules if maxViewModules is provided\n      let finalModules = newModules\n      if (maxViewModules && newModules.length > maxViewModules + 1) {\n        const hiddenCount = newModules.length - maxViewModules\n        finalModules = [\n          ...newModules.slice(0, maxViewModules),\n          {\n            icon: null,\n            content: '',\n            moduleName: '',\n            abbreviation: `+${hiddenCount}`,\n          },\n        ]\n      }\n      const contentItems = sortModules(mainModules)\n\n      return { newModules: finalModules, contentItems }\n    }, [modules, maxViewModules])\n\n    return (\n      <StyledContainer\n        $unstyled={withoutStyles}\n        $highlight={highlight}\n        $inCircle={inCircle}\n      >\n        {inCircle ? (\n          <DatabaseModulesList\n            modules={newModules}\n            contentItems={contentItems}\n            inCircle={inCircle}\n            anchorClassName={styles.anchorModuleTooltip}\n          />\n        ) : (\n          <RiTooltip\n            position=\"bottom\"\n            title={tooltipTitle ?? undefined}\n            content={<DatabaseModuleContent modules={contentItems} />}\n            data-testid=\"modules-tooltip\"\n          >\n            <>\n              {content ?? (\n                <DatabaseModulesList\n                  modules={newModules}\n                  contentItems={contentItems}\n                  inCircle={inCircle}\n                />\n              )}\n            </>\n          </RiTooltip>\n        )}\n      </StyledContainer>\n    )\n  },\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/DatabaseListModules.types.ts",
    "content": "import type { ReactNode } from 'react'\nimport type { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\n\nexport interface DatabaseListModulesProps {\n  content?: ReactNode\n  modules: AdditionalRedisModule[]\n  inCircle?: boolean\n  highlight?: boolean\n  maxViewModules?: number\n  tooltipTitle?: ReactNode\n  withoutStyles?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleContent/DatabaseModuleContent.tsx",
    "content": "import React from 'react'\n\nimport { DatabaseModuleContentItem } from 'uiSrc/components/database-list-modules/components'\nimport { DatabaseModuleContentProps } from './DatabaseModuleContent.types'\n\nexport const DatabaseModuleContent = ({\n  modules,\n}: DatabaseModuleContentProps) => {\n  return (\n    <>\n      {modules.map(({ icon, content, abbreviation }) => (\n        <DatabaseModuleContentItem\n          key={content || abbreviation}\n          icon={icon}\n          content={content}\n          abbreviation={abbreviation}\n        />\n      ))}\n    </>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleContent/DatabaseModuleContent.types.ts",
    "content": "import type { IDatabaseModule } from 'uiSrc/utils'\n\nexport interface DatabaseModuleContentProps {\n  modules: IDatabaseModule[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleContentItem/DatabaseModuleContentItem.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nexport const StyledContentItemRow = styled(Row)`\n  &:not(:last-of-type) {\n    padding-bottom: ${({ theme }) => theme.core.space.space150};\n  }\n`\n\nexport const StyledAbbreviationText = styled(ColorText)`\n  margin-right: ${({ theme }) => theme.core.space.space100};\n  vertical-align: text-top;\n`\n\nexport const StyledContentText = styled(ColorText)`\n  vertical-align: text-top;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleContentItem/DatabaseModuleContentItem.tsx",
    "content": "import React from 'react'\n\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nimport { DatabaseModuleContentItemProps } from './DatabaseModuleContentItem.types'\nimport {\n  StyledAbbreviationText,\n  StyledContentItemRow,\n  StyledContentText,\n} from './DatabaseModuleContentItem.styles'\n\nexport const DatabaseModuleContentItem = ({\n  icon,\n  content,\n  abbreviation = '',\n}: DatabaseModuleContentItemProps) => {\n  const hasIcon = !!icon\n  const hasContent = !!content\n  const hasAbbreviation = !!abbreviation\n\n  return (\n    <StyledContentItemRow align=\"center\" gap=\"m\">\n      {hasIcon && <RiIcon type={icon} size=\"M\" />}\n      {!hasIcon && hasAbbreviation && (\n        <StyledAbbreviationText>{abbreviation}</StyledAbbreviationText>\n      )}\n      {hasContent && <StyledContentText>{content}</StyledContentText>}\n    </StyledContentItemRow>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleContentItem/DatabaseModuleContentItem.types.ts",
    "content": "import type { AllIconsType } from 'uiSrc/components/base/icons'\n\nexport interface DatabaseModuleContentItemProps {\n  icon?: AllIconsType\n  content?: string\n  abbreviation?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleItem/DatabaseModuleItem.styles.ts",
    "content": "import styled, { css } from 'styled-components'\n\nimport {\n  IconButton,\n  type IconButtonProps,\n} from 'uiSrc/components/base/forms/buttons/IconButton'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport type { ColorTextProps } from 'uiSrc/components/base/text/text.styles'\n\ntype StyledIconButtonProps = IconButtonProps & { $inCircle?: boolean }\n\ntype StyledColorTextProps = ColorTextProps & { $inCircle?: boolean }\n\nconst inCircleStyles = css`\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral300};\n  border: none;\n  width: ${({ theme }) => theme.core.space.space400};\n  max-width: ${({ theme }) => theme.core.space.space400};\n  height: ${({ theme }) => theme.core.space.space400};\n  border-radius: 50%;\n  padding: ${({ theme }) => theme.core.space.space050};\n  margin-right: 0;\n\n  &:hover,\n  &:focus,\n  &:focus-within {\n    background-color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.background.neutral200};\n  }\n`\n\nexport const StyledIconButton = styled(IconButton)<StyledIconButtonProps>`\n  margin-right: ${({ theme }) => theme.core.space.space050};\n\n  img {\n    width: ${({ theme }) => theme.core.space.space200};\n    max-width: ${({ theme }) => theme.core.space.space200};\n    height: ${({ theme }) => theme.core.space.space200};\n  }\n\n  ${({ $inCircle }) => $inCircle && inCircleStyles}\n`\n\nexport const StyledColorText = styled(ColorText)<StyledColorTextProps>`\n  margin-right: ${({ theme }) => theme.core.space.space050};\n  vertical-align: text-top;\n\n  ${({ $inCircle }) => $inCircle && inCircleStyles}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleItem/DatabaseModuleItem.tsx",
    "content": "import React from 'react'\n\nimport { DatabaseModuleItemProps } from './DatabaseModuleItem.types'\nimport { StyledIconButton, StyledColorText } from './DatabaseModuleItem.styles'\n\nexport const DatabaseModuleItem = ({\n  abbreviation = '',\n  icon,\n  content = '',\n  inCircle,\n  onCopy,\n}: DatabaseModuleItemProps) => {\n  const handleCopy = () => {\n    onCopy?.(content)\n  }\n\n  return (\n    <span>\n      {icon ? (\n        <StyledIconButton\n          icon={icon}\n          $inCircle={inCircle}\n          onClick={handleCopy}\n          data-testid={`${content}_module`}\n          aria-labelledby={`${content}_module`}\n        />\n      ) : (\n        <StyledColorText\n          $inCircle={inCircle}\n          onClick={handleCopy}\n          data-testid={`${content}_module`}\n          aria-labelledby={`${content}_module`}\n        >\n          {abbreviation}\n        </StyledColorText>\n      )}\n    </span>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModuleItem/DatabaseModuleItem.types.ts",
    "content": "import type { AllIconsType } from 'uiSrc/components/base/icons'\n\nexport interface DatabaseModuleItemProps {\n  abbreviation?: string\n  icon?: AllIconsType | null\n  content?: string\n  inCircle?: boolean\n  onCopy?: (text: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModulesList/DatabaseModulesList.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { handleCopy } from 'uiSrc/utils'\n\nimport { DatabaseModuleContentItem } from 'uiSrc/components/database-list-modules/components'\nimport { DatabaseModuleItem } from '../DatabaseModuleItem/DatabaseModuleItem'\nimport { DatabaseModulesListProps } from './DatabaseModulesList.types'\n\nexport const DatabaseModulesList = ({\n  modules,\n  contentItems,\n  inCircle,\n  anchorClassName,\n}: DatabaseModulesListProps) => {\n  return (\n    <>\n      {modules.map(({ icon, content, abbreviation, moduleName }, i) => {\n        const contentItem = contentItems[i]\n        return !inCircle ? (\n          <DatabaseModuleItem\n            key={moduleName || abbreviation || content}\n            abbreviation={abbreviation}\n            icon={icon}\n            content={content}\n            inCircle={inCircle}\n            onCopy={handleCopy}\n          />\n        ) : (\n          <RiTooltip\n            position=\"bottom\"\n            content={\n              <DatabaseModuleContentItem\n                key={contentItem.content || contentItem.abbreviation}\n                {...contentItem}\n              />\n            }\n            anchorClassName={anchorClassName}\n            key={moduleName}\n          >\n            <DatabaseModuleItem\n              abbreviation={abbreviation}\n              icon={icon}\n              content={content}\n              inCircle={inCircle}\n              onCopy={handleCopy}\n            />\n          </RiTooltip>\n        )\n      })}\n    </>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/DatabaseModulesList/DatabaseModulesList.types.ts",
    "content": "import type { IDatabaseModule } from 'uiSrc/utils/modules'\n\nexport interface DatabaseModulesListProps {\n  modules: IDatabaseModule[]\n  contentItems: IDatabaseModule[]\n  inCircle?: boolean\n  anchorClassName?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/components/index.ts",
    "content": "export { DatabaseModuleContent } from './DatabaseModuleContent/DatabaseModuleContent'\nexport type { DatabaseModuleContentProps } from './DatabaseModuleContent/DatabaseModuleContent.types'\n\nexport { DatabaseModuleContentItem } from './DatabaseModuleContentItem/DatabaseModuleContentItem'\nexport type { DatabaseModuleContentItemProps } from './DatabaseModuleContentItem/DatabaseModuleContentItem.types'\n\nexport { DatabaseModuleItem } from './DatabaseModuleItem/DatabaseModuleItem'\nexport type { DatabaseModuleItemProps } from './DatabaseModuleItem/DatabaseModuleItem.types'\n\nexport { DatabaseModulesList } from './DatabaseModulesList/DatabaseModulesList'\nexport type { DatabaseModulesListProps } from './DatabaseModulesList/DatabaseModulesList.types'\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-modules/styles.module.scss",
    "content": ".anchorModuleTooltip {\n  margin-right: 18px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-options/DatabaseListOptions.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport DatabaseListOptions from './DatabaseListOptions'\n\nconst optionsMock: Partial<any> = {\n  enabledDataPersistence: true,\n  persistencePolicy: 'aof-every-write',\n  enabledRedisFlash: false,\n  enabledReplication: false,\n  enabledBackup: false,\n  enabledActiveActive: false,\n  enabledClustering: false,\n  isReplicaDestination: false,\n  isReplicaSource: false,\n}\n\ndescribe('DatabaseListOptions', () => {\n  it('should render', () => {\n    expect(render(<DatabaseListOptions options={optionsMock} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-options/DatabaseListOptions.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport DatabaseListOptions from './DatabaseListOptions'\n\nconst meta: Meta<typeof DatabaseListOptions> = {\n  component: DatabaseListOptions,\n  args: {\n    options: {\n      enabledDataPersistence: false,\n      persistencePolicy: 'none',\n      enabledRedisFlash: false,\n      enabledReplication: false,\n      enabledBackup: false,\n      enabledActiveActive: false,\n      enabledClustering: false,\n      isReplicaDestination: false,\n      isReplicaSource: false,\n    },\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const WithDataPersistence: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledDataPersistence: true,\n      persistencePolicy: 'aof-every-write',\n    },\n  },\n}\n\nexport const WithRedisFlash: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledRedisFlash: true,\n    },\n  },\n}\n\nexport const WithReplication: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledReplication: true,\n    },\n  },\n}\n\nexport const WithBackup: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledBackup: true,\n    },\n  },\n}\n\nexport const WithActiveActive: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledActiveActive: true,\n    },\n  },\n}\n\nexport const WithClustering: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledClustering: true,\n    },\n  },\n}\n\nexport const AsReplicaSource: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      isReplicaSource: true,\n    },\n  },\n}\n\nexport const AsReplicaDestination: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      isReplicaDestination: true,\n    },\n  },\n}\n\nexport const WithAllOptions: Story = {\n  args: {\n    options: {\n      ...meta.args!.options,\n      enabledDataPersistence: true,\n      persistencePolicy: 'aof-every-write',\n      enabledRedisFlash: true,\n      enabledReplication: true,\n      enabledBackup: true,\n      enabledActiveActive: true,\n      enabledClustering: true,\n      isReplicaSource: true,\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-options/DatabaseListOptions.styles.ts",
    "content": "import styled from 'styled-components'\nimport type { HTMLAttributes } from 'react'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const DatabaseListOptionsContainer = styled.div`\n  padding-left: 7px;\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n`\n\nexport type ValidOptionIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7\n\ntype OptionColor = { bg: string; fg?: string }\ntype ThemePalette = Record<ValidOptionIndex, OptionColor>\n\ntype OptionsIconProps = {\n  theme: Theme\n  $icon: ValidOptionIndex\n}\n\nconst getIconStyle = ({ $icon, theme }: OptionsIconProps) => {\n  // todo: move to theme\n  const customFgDarkColor = theme.semantic.color.text.primary50\n  // const customFgDarkColor = '#202020'\n  const customFgLightColor = theme.semantic.color.text.primary700\n  // const customFgLightColor = #1A3091\n  const optionColors: { dark: ThemePalette; light: ThemePalette } = {\n    dark: {\n      0: { bg: '#293152' },\n      1: { bg: '#323e6c' },\n      2: { bg: '#465282' },\n      3: { bg: '#606c98' },\n      4: { bg: '#737fa8' },\n      5: { bg: '#8f99bc', fg: customFgDarkColor },\n      6: { bg: '#adb5d3', fg: customFgDarkColor },\n      7: { bg: '#cdd4ea', fg: customFgDarkColor },\n    },\n    light: {\n      0: { bg: '#587AB2' },\n      1: { bg: '#6A8BC1' },\n      2: { bg: '#97B4E3', fg: customFgLightColor },\n      3: { bg: '#ADC5ED', fg: customFgLightColor },\n      4: { bg: '#C6D8F7', fg: customFgLightColor },\n      5: { bg: '#DEEAFF', fg: customFgLightColor },\n      6: { bg: '#EAF1FF', fg: customFgLightColor },\n      7: { bg: '#EFF4FF', fg: customFgLightColor },\n    },\n  }\n  const themeColors =\n    theme.name === 'dark' ? optionColors.dark : optionColors.light\n  return `\n    background-color: ${themeColors[$icon].bg};\n    ${themeColors[$icon].fg ? `color: ${themeColors[$icon].fg};` : ''}\n`\n}\n\nexport const OptionsIcon = styled.div<\n  HTMLAttributes<HTMLDivElement> & OptionsIconProps\n>`\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: ${({ theme }) => theme.core.space.space300};\n  height: ${({ theme }) => theme.core.space.space300};\n  border-radius: 100%;\n  cursor: pointer;\n  line-height: ${({ theme }) => theme.core.space.space300};\n  text-align: center;\n  text-transform: uppercase;\n  margin-left: -${({ theme }) => theme.core.space.space050};\n  color: #fff;\n\n  &:hover {\n    transform: translateY(-1px);\n  }\n  &:active {\n    transform: translateY(1px);\n  }\n  ${getIconStyle};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-options/DatabaseListOptions.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport { PersistencePolicy } from 'uiSrc/slices/interfaces'\nimport { DatabaseListOptionsContainer } from './DatabaseListOptions.styles'\nimport { Tooltip } from './components/Tooltip'\nimport { OPTIONS_CONTENT, OptionKey } from './constants'\n\ninterface Props {\n  options: Partial<any>\n}\n\nconst DatabaseListOptions = ({ options }: Props) => {\n  const optionsRender = useMemo(() => {\n    const sortedOptions = Object.entries(options).sort(([option]) => {\n      if (OPTIONS_CONTENT[option as OptionKey]?.icon === undefined) {\n        return -1\n      }\n      return 0\n    })\n    return sortedOptions.map(([option, value]: any, index: number) => {\n      if (value && value !== PersistencePolicy.none) {\n        return (\n          <Tooltip\n            key={`${option + index}`}\n            icon={OPTIONS_CONTENT[option as OptionKey]?.icon}\n            content={OPTIONS_CONTENT[option as OptionKey]?.text}\n            value={value}\n            index={index}\n          />\n        )\n      }\n      return null\n    })\n  }, [options])\n\n  return (\n    <DatabaseListOptionsContainer>{optionsRender}</DatabaseListOptionsContainer>\n  )\n}\n\nexport default DatabaseListOptions\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-options/components/Tooltip.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { PersistencePolicy } from 'uiSrc/slices/interfaces'\nimport { IconButton, IconType } from 'uiSrc/components/base/forms/buttons'\nimport { handleCopy } from 'uiSrc/utils'\nimport { OptionsIcon, ValidOptionIndex } from '../DatabaseListOptions.styles'\n\n/**\n * Type guard to check if value is a valid PersistencePolicy key\n */\nfunction isPersistencePolicyKey(\n  value: unknown,\n): value is keyof typeof PersistencePolicy {\n  return typeof value === 'string' && value in PersistencePolicy\n}\n\ninterface ITooltipProps {\n  content: string\n  index: number\n  value: unknown\n  icon?: IconType\n}\nexport const Tooltip = ({\n  content: contentProp,\n  icon,\n  value,\n  index,\n}: ITooltipProps) => {\n  if (!contentProp) {\n    return null\n  }\n  return (\n    <RiTooltip\n      content={\n        isPersistencePolicyKey(value)\n          ? `Persistence: ${PersistencePolicy[value]}`\n          : contentProp\n      }\n      position=\"top\"\n    >\n      {icon ? (\n        <IconButton\n          icon={icon}\n          onClick={() => handleCopy(contentProp)}\n          aria-label={`${contentProp}_module`}\n        />\n      ) : (\n        <OptionsIcon\n          $icon={index as ValidOptionIndex}\n          aria-label={contentProp}\n          onClick={() => handleCopy(contentProp)}\n          onKeyDown={() => ({})}\n          role=\"presentation\"\n        >\n          {contentProp.match(/\\b(\\w)/g)?.join('')}\n        </OptionsIcon>\n      )}\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-list-options/constants.ts",
    "content": "import {\n  AddRedisClusterDatabaseOptions,\n  DATABASE_LIST_OPTIONS_TEXT,\n} from 'uiSrc/slices/interfaces'\nimport { ActiveActiveIcon, RedisonFlashIcon } from 'uiSrc/components/base/icons'\nimport { IconType } from 'uiSrc/components/base/forms/buttons'\n\nexport type OptionKey = AddRedisClusterDatabaseOptions\n\ntype OptionsContent = Record<OptionKey, OptionContent>\n\ntype OptionContent = {\n  icon?: IconType\n  text: string\n}\n\nexport const OPTIONS_CONTENT: OptionsContent = {\n  [AddRedisClusterDatabaseOptions.ActiveActive]: {\n    icon: ActiveActiveIcon,\n    text: DATABASE_LIST_OPTIONS_TEXT[\n      AddRedisClusterDatabaseOptions.ActiveActive\n    ],\n  },\n  [AddRedisClusterDatabaseOptions.Backup]: {\n    text: DATABASE_LIST_OPTIONS_TEXT[AddRedisClusterDatabaseOptions.Backup],\n  },\n\n  [AddRedisClusterDatabaseOptions.Clustering]: {\n    text: DATABASE_LIST_OPTIONS_TEXT[AddRedisClusterDatabaseOptions.Clustering],\n  },\n  [AddRedisClusterDatabaseOptions.PersistencePolicy]: {\n    text: DATABASE_LIST_OPTIONS_TEXT[\n      AddRedisClusterDatabaseOptions.PersistencePolicy\n    ],\n  },\n  [AddRedisClusterDatabaseOptions.Flash]: {\n    icon: RedisonFlashIcon,\n    text: DATABASE_LIST_OPTIONS_TEXT[AddRedisClusterDatabaseOptions.Flash],\n  },\n  [AddRedisClusterDatabaseOptions.Replication]: {\n    text: DATABASE_LIST_OPTIONS_TEXT[\n      AddRedisClusterDatabaseOptions.Replication\n    ],\n  },\n  [AddRedisClusterDatabaseOptions.ReplicaDestination]: {\n    text: DATABASE_LIST_OPTIONS_TEXT[\n      AddRedisClusterDatabaseOptions.ReplicaDestination\n    ],\n  },\n  [AddRedisClusterDatabaseOptions.ReplicaSource]: {\n    text: DATABASE_LIST_OPTIONS_TEXT[\n      AddRedisClusterDatabaseOptions.ReplicaSource\n    ],\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/DatabaseOverview.spec.tsx",
    "content": "import React from 'react'\nimport { truncateNumberToRange } from 'uiSrc/utils'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport DatabaseOverview from './DatabaseOverview'\nimport { useDatabaseOverview } from './hooks/useDatabaseOverview'\nimport { IMetric } from './components/OverviewMetrics'\nimport styles from './styles.module.scss'\n\n// Mock the useDatabaseOverview hook\njest.mock('./hooks/useDatabaseOverview')\n\n// Mock the config\njest.mock('uiSrc/config', () => ({\n  getConfig: jest.fn(() => {\n    const { getConfig: actualGetConfig } = jest.requireActual('uiSrc/config')\n    const actualConfig = actualGetConfig()\n    return {\n      ...actualConfig,\n      app: {\n        ...actualConfig.app,\n        returnUrlBase: 'https://redis.cloud.io/#',\n      },\n    }\n  }),\n}))\n\n// Create a default mock implementation for the hook\nconst defaultMockHook = () => ({\n  metrics: mockMetrics,\n  connectivityError: null,\n  lastRefreshTime: Date.now(),\n  subscriptionType: undefined,\n  subscriptionId: undefined,\n  isBdbPackages: undefined,\n  usedMemoryPercent: undefined,\n  handleEnableAutoRefresh: jest.fn(),\n  handleRefresh: jest.fn(),\n})\n\ndescribe('DatabaseOverview', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n    // Set the default mock implementation\n    ;(useDatabaseOverview as jest.Mock).mockReturnValue(defaultMockHook())\n  })\n\n  it('should render with metrics', () => {\n    const { getByTestId, queryByRole } = render(<DatabaseOverview />)\n\n    expect(getByTestId('overview-cpu')).toBeInTheDocument()\n    expect(getByTestId('overview-commands-sec')).toBeInTheDocument()\n    expect(getByTestId('overview-total-memory')).toHaveTextContent(\n      '13 / 30 (43%)',\n    )\n    expect(getByTestId('overview-total-keys')).toBeInTheDocument()\n    expect(getByTestId('overview-connected-clients')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade-ri-db-button')).not.toBeInTheDocument()\n    expect(\n      queryByRole('button', { name: 'Upgrade plan' }),\n    ).not.toBeInTheDocument()\n    expect(\n      getByTestId('auto-refresh-overview-auto-refresh-container'),\n    ).toBeInTheDocument()\n  })\n\n  it('should show upgrade button and memory usage percentage if cloud plan limit is present', () => {\n    // Mock the hook with subscription data\n    const mockWithSubscription = {\n      ...defaultMockHook(),\n      metrics: mockMetrics.map((m) => {\n        if (m.id === 'overview-total-memory') {\n          return {\n            ...m,\n            content: (\n              <span>\n                45 MB / <strong>75 MB</strong> (60.3%)\n              </span>\n            ),\n          }\n        }\n        return m\n      }),\n      subscriptionType: 'fixed',\n      subscriptionId: 123,\n      usedMemoryPercent: 60.3,\n    }\n    ;(useDatabaseOverview as jest.Mock).mockReturnValue(mockWithSubscription)\n\n    const { getByTestId, getByRole } = render(<DatabaseOverview />)\n\n    expect(getByTestId('overview-total-memory')).toHaveTextContent(\n      '45 MB / 75 MB (60.3%)',\n    )\n    expect(getByRole('button', { name: 'Upgrade plan' })).toBeInTheDocument()\n  })\n\n  it('should not show upgrade button for flexible subscriptions', () => {\n    // Mock the hook with flexible subscription data\n    const mockWithFlexibleSubscription = {\n      ...defaultMockHook(),\n      metrics: mockMetrics.map((m) => {\n        if (m.id === 'overview-total-memory') {\n          return {\n            ...m,\n            content: (\n              <span>\n                45 MB / <strong>75 MB</strong> (60.3%)\n              </span>\n            ),\n          }\n        }\n        return m\n      }),\n      subscriptionType: 'flexible',\n      subscriptionId: 123,\n      usedMemoryPercent: 60.3,\n    }\n    ;(useDatabaseOverview as jest.Mock).mockReturnValue(\n      mockWithFlexibleSubscription,\n    )\n\n    const { getByTestId, queryByRole } = render(<DatabaseOverview />)\n\n    expect(getByTestId('overview-total-memory')).toHaveTextContent(\n      '45 MB / 75 MB (60.3%)',\n    )\n    expect(\n      queryByRole('button', { name: 'Upgrade plan' }),\n    ).not.toBeInTheDocument()\n  })\n\n  test.each([\n    ['https://redis.cloud.io/#/subscription/123/change-plan', false],\n    ['https://redis.cloud.io/#/databases/upgrade/123', true],\n  ])('should redirect to %s when isBdbPackages = %s', (url, isBdbPackages) => {\n    const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null)\n\n    // Mock the hook with subscription data and isBdbPackages flag\n    const mockWithBdbPackages = {\n      ...defaultMockHook(),\n      metrics: mockMetrics.map((m) => {\n        if (m.id === 'overview-total-memory') {\n          return {\n            ...m,\n            content: (\n              <span>\n                45 MB / <strong>75 MB</strong> (60.3%)\n              </span>\n            ),\n          }\n        }\n        return m\n      }),\n      subscriptionType: 'fixed',\n      subscriptionId: 123,\n      isBdbPackages,\n      usedMemoryPercent: 60.3,\n    }\n    ;(useDatabaseOverview as jest.Mock).mockReturnValue(mockWithBdbPackages)\n\n    const { getByTestId, getByRole } = render(<DatabaseOverview />)\n\n    expect(getByTestId('overview-total-memory')).toHaveTextContent(\n      '45 MB / 75 MB (60.3%)',\n    )\n    const upgradeBtn = getByRole('button', { name: 'Upgrade plan' })\n    expect(upgradeBtn).toBeInTheDocument()\n\n    fireEvent.click(upgradeBtn)\n    expect(openSpy).toHaveBeenCalledTimes(1)\n    expect(openSpy).toHaveBeenCalledWith(url, '_blank')\n  })\n\n  it('should show connectivity error when present', () => {\n    // Mock the hook with connectivity error\n    const mockWithError = {\n      ...defaultMockHook(),\n      connectivityError: 'Connection error',\n    }\n    ;(useDatabaseOverview as jest.Mock).mockReturnValue(mockWithError)\n\n    const { getByTestId } = render(<DatabaseOverview />)\n\n    // Check that the warning icon is displayed\n    expect(getByTestId('connectivityError')).toBeInTheDocument()\n  })\n\n  it('should call handleRefresh when auto-refresh is triggered', () => {\n    const mockHandleRefresh = jest.fn()\n    ;(useDatabaseOverview as jest.Mock).mockReturnValue({\n      ...defaultMockHook(),\n      handleRefresh: mockHandleRefresh,\n    })\n\n    const { getByTestId } = render(<DatabaseOverview />)\n\n    // Find and click the refresh button\n    const refreshButton = getByTestId('auto-refresh-overview-refresh-btn')\n    fireEvent.click(refreshButton)\n\n    expect(mockHandleRefresh).toHaveBeenCalled()\n  })\n})\n\n// Create mock metrics for testing\nconst mockMetrics: IMetric[] = [\n  {\n    id: 'overview-cpu',\n    title: 'CPU',\n    value: 5,\n    loading: 5 === null,\n    unavailableText: 'CPU is not available',\n    icon: 'TimeLightIcon',\n    className: styles.cpuWrapper,\n    content: '5 %',\n    tooltip: {\n      title: 'CPU',\n      icon: 'TimeLightIcon',\n      content: (\n        <>\n          <b>5</b>\n          &nbsp;%\n        </>\n      ),\n    },\n  },\n  {\n    id: 'overview-total-memory',\n    value: 13,\n    unavailableText: 'Total Memory is not available',\n    title: 'Total Memory',\n    tooltip: {\n      title: 'Total Memory',\n      icon: 'MemoryLightIcon',\n      content: '13 / 30 (43%)',\n    },\n    icon: 'MemoryLightIcon',\n    content: (\n      <span>\n        13 / <strong>30</strong> (43%)\n      </span>\n    ),\n  },\n  {\n    id: 'overview-total-keys',\n    value: 5000,\n    unavailableText: 'Total Keys are not available',\n    title: 'Total Keys',\n    icon: 'KeyLightIcon',\n    content: truncateNumberToRange(5000),\n    tooltip: {\n      icon: 'KeyLightIcon',\n      content: <b>5 000</b>,\n      title: 'Total Keys',\n    },\n    children: [\n      {\n        id: 'total-keys-tip',\n        value: 5000,\n        unavailableText: 'Total Keys are not available',\n        title: 'Total Keys',\n        tooltip: {\n          title: 'Total Keys',\n          content: <b>5 000</b>,\n        },\n        content: <b>5 000</b>,\n      },\n      {\n        id: 'overview-db-total-keys',\n        title: 'Keys',\n        value: 3000,\n        content: (\n          <>\n            <span\n              style={{\n                fontWeight: 200,\n                paddingRight: 1,\n              }}\n            >\n              db0:\n            </span>\n            <b>3 000</b>\n          </>\n        ),\n      },\n    ],\n  },\n  {\n    id: 'overview-connected-clients',\n    value: 3,\n    unavailableText: 'Connected Clients are not available',\n    title: 'Connected Clients',\n    tooltip: {\n      title: 'Connected Clients',\n      content: <b>3</b>,\n      icon: 'UserLightIcon',\n    },\n    icon: 'UserLightIcon',\n    content: 3,\n  },\n  {\n    id: 'overview-commands-sec',\n    icon: 'MeasureLightIcon',\n    content: 5,\n    value: 5,\n    unavailableText: 'Commands/s are not available',\n    title: 'Commands/s',\n    tooltip: {\n      icon: 'MeasureLightIcon',\n      content: 5,\n    },\n    className: styles.opsPerSecItem,\n    children: [\n      {\n        id: 'commands-per-sec-tip',\n        title: 'Commands/s',\n        icon: 'MeasureLightIcon',\n        value: 5,\n        content: 5,\n        unavailableText: 'Commands/s are not available',\n      },\n    ],\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/DatabaseOverview.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { getConfig } from 'uiSrc/config'\n\nimport {\n  DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL,\n  DATABASE_OVERVIEW_REFRESH_INTERVAL,\n} from 'uiSrc/constants/browser'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport MetricItem, {\n  OverviewItem,\n} from 'uiSrc/components/database-overview/components/OverviewMetrics/MetricItem'\nimport { useDatabaseOverview } from 'uiSrc/components/database-overview/hooks/useDatabaseOverview'\n\nimport { IMetric } from 'uiSrc/components/database-overview/components/OverviewMetrics'\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport AutoRefresh from '../auto-refresh'\nimport styles from './styles.module.scss'\n\nconst riConfig = getConfig()\n\nconst DatabaseOverview = () => {\n  const {\n    connectivityError,\n    metrics,\n    subscriptionId,\n    subscriptionType,\n    usedMemoryPercent,\n    isBdbPackages,\n    lastRefreshTime,\n    handleEnableAutoRefresh,\n    handleRefresh,\n  } = useDatabaseOverview()\n\n  return (\n    <Row className={styles.container}>\n      <FlexItem grow key=\"overview\">\n        <Row\n          className={cx('flex-row', styles.itemContainer, styles.overview)}\n          align=\"center\"\n        >\n          {connectivityError && (\n            <MetricItem\n              id=\"connectivityError\"\n              tooltipContent={connectivityError}\n              content={\n                <RiIcon size=\"m\" type=\"ToastInfoIcon\" color=\"danger500\" />\n              }\n            />\n          )}\n          {metrics?.length! > 0 && (\n            <>\n              {subscriptionId && subscriptionType === 'fixed' && (\n                <OverviewItem\n                  id=\"upgrade-ri-db-button\"\n                  className={styles.upgradeBtnItem}\n                  style={{ borderRight: 'none' }}\n                >\n                  <SecondaryButton\n                    filled={!!usedMemoryPercent && usedMemoryPercent >= 75}\n                    className={cx(styles.upgradeBtn)}\n                    style={{ fontWeight: '400' }}\n                    onClick={() => {\n                      const upgradeUrl = isBdbPackages\n                        ? `${riConfig.app.returnUrlBase}/databases/upgrade/${subscriptionId}`\n                        : `${riConfig.app.returnUrlBase}/subscription/${subscriptionId}/change-plan`\n                      window.open(upgradeUrl, '_blank')\n                    }}\n                    data-testid=\"upgrade-ri-db-button\"\n                  >\n                    Upgrade plan\n                  </SecondaryButton>\n                </OverviewItem>\n              )}\n              {metrics?.map((overviewItem) => (\n                <MetricItem\n                  key={overviewItem.id}\n                  {...overviewItem}\n                  tooltipContent={getTooltipContent(overviewItem)}\n                />\n              ))}\n              <OverviewItem\n                className={styles.autoRefresh}\n                data-testid=\"overview-auto-refresh\"\n                id=\"overview-auto-refresh\"\n              >\n                <FlexItem className={styles.overviewItemContent}>\n                  <AutoRefresh\n                    displayText={false}\n                    displayLastRefresh={false}\n                    iconSize=\"S\"\n                    loading={false}\n                    enableAutoRefreshDefault\n                    lastRefreshTime={lastRefreshTime}\n                    containerClassName=\"\"\n                    postfix=\"overview\"\n                    testid=\"auto-refresh-overview\"\n                    defaultRefreshRate={DATABASE_OVERVIEW_REFRESH_INTERVAL}\n                    minimumRefreshRate={parseInt(\n                      DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL,\n                    )}\n                    onRefresh={handleRefresh}\n                    onEnableAutoRefresh={handleEnableAutoRefresh}\n                  />\n                </FlexItem>\n              </OverviewItem>\n            </>\n          )}\n        </Row>\n      </FlexItem>\n    </Row>\n  )\n}\n\nconst getTooltipContent = (metric: IMetric) => {\n  if (!metric.children?.length) {\n    return (\n      <Row>\n        <span>{metric.tooltip?.content}</span>\n        &nbsp;\n        <span>{metric.tooltip?.title}</span>\n      </Row>\n    )\n  }\n  return metric.children\n    .filter((item) => item.value !== undefined)\n    .map((tooltipItem) => (\n      <Row\n        className={styles.commandsPerSecTip}\n        key={tooltipItem.id}\n        align=\"center\"\n      >\n        {tooltipItem.icon && (\n          <FlexItem>\n            <RiIcon\n              className={styles.moreInfoOverviewIcon}\n              size=\"m\"\n              type={tooltipItem.icon}\n            />\n          </FlexItem>\n        )}\n        <FlexItem className={styles.moreInfoOverviewContent} direction=\"row\">\n          {tooltipItem.content}\n        </FlexItem>\n        <FlexItem className={styles.moreInfoOverviewTitle}>\n          {tooltipItem.title}\n        </FlexItem>\n      </Row>\n    ))\n}\n\nexport default DatabaseOverview\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/components/OverviewMetrics/MetricItem.tsx",
    "content": "import React, { CSSProperties, ReactNode } from 'react'\nimport cx from 'classnames'\nimport styles from 'uiSrc/components/database-overview/styles.module.scss'\nimport { IMetric } from 'uiSrc/components/database-overview/components/OverviewMetrics/OverviewMetrics'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface OverviewItemProps {\n  children: ReactNode\n  className?: string\n  id?: string\n  style?: CSSProperties\n}\nexport const OverviewItem = ({\n  children,\n  className,\n  id,\n  style,\n}: OverviewItemProps) => (\n  <FlexItem\n    className={cx(styles.overviewItem, className)}\n    key={id}\n    data-test-subj={id}\n    data-testid={id}\n    style={style}\n  >\n    {children}\n  </FlexItem>\n)\n\nconst MetricItem = (\n  props: Partial<IMetric> & {\n    tooltipContent?: ReactNode\n    style?: CSSProperties\n  },\n) => {\n  const { className = '', content, icon, id, tooltipContent, style } = props\n  return (\n    <OverviewItem id={id} className={className} style={style}>\n      <RiTooltip\n        position=\"bottom\"\n        className={styles.tooltip}\n        content={tooltipContent}\n      >\n        <Row gap=\"none\" responsive={false} align=\"center\" justify=\"center\">\n          {icon && (\n            <FlexItem className={styles.icon}>\n              <RiIcon size=\"m\" type={icon} className={styles.icon} />\n            </FlexItem>\n          )}\n          <FlexItem className={styles.overviewItemContent}>{content}</FlexItem>\n        </Row>\n      </RiTooltip>\n    </OverviewItem>\n  )\n}\n\nexport default MetricItem\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/components/OverviewMetrics/OverviewMetrics.spec.tsx",
    "content": "import { getCpuDisplayValue, getCpuTooltipValue } from './OverviewMetrics'\n\ndescribe('getCpuDisplayValue', () => {\n  it('should return formatted display value for valid CPU percentage', () => {\n    expect(getCpuDisplayValue(50.123)).toBe('50.12 %')\n    expect(getCpuDisplayValue(100)).toBe('100 %')\n    expect(getCpuDisplayValue(250.567)).toBe('250.57 %')\n  })\n\n  it('should return null when CPU percentage is null', () => {\n    expect(getCpuDisplayValue(null)).toBeNull()\n  })\n})\n\ndescribe('getCpuTooltipValue', () => {\n  describe('when maxCpuUsagePercentage is available and > 100%', () => {\n    it('should return \"current% / max%\" format', () => {\n      expect(getCpuTooltipValue(150.1234, 400)).toBe('150.1234% / 400%')\n      expect(getCpuTooltipValue(250.5678, 400)).toBe('250.5678% / 400%')\n      expect(getCpuTooltipValue(100, 200)).toBe('100% / 200%')\n    })\n\n    it('should return single number when max is exactly 100%', () => {\n      expect(getCpuTooltipValue(50, 100)).toBe('50%')\n    })\n  })\n\n  describe('when maxCpuUsagePercentage is not available or <= 100%', () => {\n    it('should return just \"current%\" format', () => {\n      expect(getCpuTooltipValue(50.1234)).toBe('50.1234%')\n      expect(getCpuTooltipValue(100.5678)).toBe('100.5678%')\n      expect(getCpuTooltipValue(50.1234, null)).toBe('50.1234%')\n      expect(getCpuTooltipValue(50.1234, undefined)).toBe('50.1234%')\n      expect(getCpuTooltipValue(50.1234, 100)).toBe('50.1234%')\n    })\n  })\n\n  describe('when cpuUsagePercentage is null', () => {\n    it('should return null', () => {\n      expect(getCpuTooltipValue(null)).toBeNull()\n      expect(getCpuTooltipValue(null, 400)).toBeNull()\n      expect(getCpuTooltipValue(null, null)).toBeNull()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/components/OverviewMetrics/OverviewMetrics.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport { isArray, isUndefined, toNumber } from 'lodash'\n\nimport {\n  formatBytes,\n  Nullable,\n  toBytes,\n  truncateNumberToRange,\n  truncatePercentage,\n} from 'uiSrc/utils'\nimport { Theme } from 'uiSrc/constants'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\n\nimport { AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\nimport { Loader } from 'uiSrc/components/base/display'\n\nimport styles from './styles.module.scss'\n\ninterface Props {\n  theme: string\n  db?: number\n  items: {\n    version: string\n    usedMemory?: Nullable<number>\n    usedMemoryPercent?: Nullable<number>\n    totalKeys?: Nullable<number>\n    connectedClients?: Nullable<number>\n    opsPerSecond?: Nullable<number>\n    networkInKbps?: Nullable<number>\n    networkOutKbps?: Nullable<number>\n    cpuUsagePercentage?: Nullable<number>\n    maxCpuUsagePercentage?: Nullable<number>\n    totalKeysPerDb?: Nullable<{ [key: string]: number }>\n    cloudDetails?: {\n      cloudId: number\n      subscriptionId: number\n      subscriptionType: 'fixed' | 'flexible'\n      planMemoryLimit: number\n      memoryLimitMeasurementUnit: string\n    }\n  }\n}\n\nexport interface IMetric {\n  id: string\n  content: ReactNode\n  value: any\n  unavailableText?: string\n  title: string\n  tooltip?: {\n    title?: string\n    icon?: Nullable<AllIconsType>\n    content: ReactNode | string\n  }\n  loading?: boolean\n  groupId?: string\n  icon?: Nullable<AllIconsType>\n  className?: string\n  children?: Array<IMetric>\n}\n\nexport function getCpuDisplayValue(\n  cpuUsagePercentage: number | null,\n): string | null {\n  return cpuUsagePercentage !== null\n    ? `${truncatePercentage(cpuUsagePercentage, 2)} %`\n    : null\n}\n\nexport function getCpuTooltipValue(\n  cpuUsagePercentage: number | null,\n  maxCpuUsagePercentage?: number | null,\n): string | null {\n  const hasMaxCpu =\n    maxCpuUsagePercentage !== undefined &&\n    maxCpuUsagePercentage !== null &&\n    maxCpuUsagePercentage > 100\n\n  if (hasMaxCpu && cpuUsagePercentage !== null) {\n    return `${truncatePercentage(cpuUsagePercentage, 4)}% / ${maxCpuUsagePercentage}%`\n  }\n\n  if (cpuUsagePercentage !== null) {\n    return `${truncatePercentage(cpuUsagePercentage, 4)}%`\n  }\n\n  return null\n}\n\nfunction getCpuUsage(\n  cpuUsagePercentage: number | null,\n  theme: string,\n  maxCpuUsagePercentage?: number | null,\n): IMetric {\n  const displayValue = getCpuDisplayValue(cpuUsagePercentage)\n  const tooltipValue = getCpuTooltipValue(\n    cpuUsagePercentage,\n    maxCpuUsagePercentage,\n  )\n\n  return {\n    id: 'overview-cpu',\n    title: 'CPU',\n    value: cpuUsagePercentage,\n    loading: cpuUsagePercentage === null,\n    unavailableText: 'CPU is not available',\n    tooltip: {\n      title: 'CPU',\n      icon: theme === Theme.Dark ? 'TimeDarkIcon' : 'TimeLightIcon',\n      content:\n        cpuUsagePercentage === null ? (\n          'Calculating in progress'\n        ) : (\n          <>\n            <b>{tooltipValue}</b>\n          </>\n        ),\n    },\n    className: styles.cpuWrapper,\n    icon:\n      cpuUsagePercentage !== null\n        ? theme === Theme.Dark\n          ? 'TimeDarkIcon'\n          : 'TimeLightIcon'\n        : null,\n    content:\n      cpuUsagePercentage === null ? (\n        <>\n          <div className={styles.calculationWrapper}>\n            <Loader className={styles.spinner} size=\"m\" />\n            <span className={styles.calculation}>Calculating...</span>\n          </div>\n        </>\n      ) : (\n        displayValue\n      ),\n  }\n}\n\nfunction getOpsPerSecondItem(\n  theme: string,\n  opsPerSecond: number,\n  networkInKbps: number,\n  networkOutKbps: number,\n) {\n  // Ops per second with tooltip\n  const opsPerSecItem: any = {\n    id: 'overview-commands-sec',\n    icon: theme === Theme.Dark ? 'MeasureDarkIcon' : 'MeasureLightIcon',\n    content: opsPerSecond,\n    value: opsPerSecond,\n    unavailableText: 'Commands/s are not available',\n    title: 'Commands/s',\n    tooltip: {\n      icon: theme === Theme.Dark ? 'MeasureDarkIcon' : 'MeasureLightIcon',\n      content: opsPerSecond,\n    },\n    className: styles.opsPerSecItem,\n  }\n\n  // let [networkIn, networkInUnit] = formatBytes(\n  const networkInBytes = formatBytes((networkInKbps ?? 0) * 1000, 3, true, 1000)\n  const networkIn = networkInBytes[0]\n  const networkInUnit = networkInBytes[1]\n    ? `${networkInBytes[1].toString().toLowerCase()}/s`\n    : ''\n  // let [networkOut, networkOutUnit] = formatBytes(\n  const networkOutBytes = formatBytes(\n    (networkOutKbps ?? 0) * 1000,\n    3,\n    true,\n    1000,\n  )\n  const networkOutUnit = networkOutBytes[1]\n    ? `${networkOutBytes[1].toString().toLowerCase()}/s`\n    : ''\n  const networkOut = networkOutBytes[0]\n\n  const networkInItem = {\n    id: 'network-input',\n    groupId: opsPerSecItem.id,\n    title: 'Network Input',\n    icon: theme === Theme.Dark ? 'InputDarkIcon' : 'InputLightIcon',\n    value: networkIn,\n    content: (\n      <>\n        <b>{networkIn}</b>\n        &nbsp;{networkInUnit}\n      </>\n    ),\n    unavailableText: 'Network Input is not available',\n    tooltip: {\n      title: 'Network Input',\n      icon: theme === Theme.Dark ? 'InputDarkIcon' : 'InputLightIcon',\n      content: (\n        <>\n          <b>{networkIn}</b>\n          &nbsp;{networkInUnit}\n        </>\n      ),\n    },\n  }\n\n  const networkOutItem = {\n    id: 'network-output-tip',\n    groupId: opsPerSecItem.id,\n    title: 'Network Output',\n    icon: theme === Theme.Dark ? 'OutputDarkIcon' : 'OutputLightIcon',\n    value: networkOut,\n    content: (\n      <>\n        <b>{networkOut}</b>\n        &nbsp;{networkOutUnit}\n      </>\n    ),\n    unavailableText: 'Network Output is not available',\n    tooltip: {\n      title: 'Network Output',\n      icon: theme === Theme.Dark ? 'OutputDarkIcon' : 'OutputLightIcon',\n      content: (\n        <>\n          <b>{networkOut}</b>\n          &nbsp;{networkOutUnit}\n        </>\n      ),\n    },\n  }\n\n  if (!isUndefined(opsPerSecond)) {\n    opsPerSecItem.children = [\n      {\n        id: 'commands-per-sec-tip',\n        title: 'Commands/s',\n        icon: theme === Theme.Dark ? 'MeasureDarkIcon' : 'MeasureLightIcon',\n        value: opsPerSecond,\n        content: opsPerSecond,\n        unavailableText: 'Commands/s are not available',\n      },\n      networkInItem,\n      networkOutItem,\n    ]\n  }\n  return opsPerSecItem\n}\n\nfunction getUsedMemoryItem(\n  theme: string,\n  usedMemory: number,\n  planMemoryLimit: number,\n  usedMemoryPercent: number,\n  memoryLimitMeasurementUnit = 'MB',\n): IMetric {\n  const memoryUsed = formatBytes(usedMemory, 0)\n  const planMemory = planMemoryLimit\n    ? formatBytes(toBytes(planMemoryLimit, memoryLimitMeasurementUnit) || 0, 1)\n    : ''\n\n  const memoryContent = planMemoryLimit ? (\n    <span>\n      {memoryUsed} / <strong>{planMemory}</strong> ({usedMemoryPercent}%)\n    </span>\n  ) : (\n    memoryUsed\n  )\n  const memoryUsedTooltip = planMemory\n    ? ` / ${planMemory} (${usedMemoryPercent}%)`\n    : ''\n\n  const formattedUsedMemoryTooltip = formatBytes(usedMemory || 0, 3, true)\n  return {\n    id: 'overview-total-memory',\n    value: usedMemory,\n    unavailableText: 'Total Memory is not available',\n    title: 'Total Memory',\n    tooltip: {\n      title: 'Total Memory',\n      icon: theme === Theme.Dark ? 'MemoryDarkIcon' : 'MemoryLightIcon',\n      content: isArray(formattedUsedMemoryTooltip) ? (\n        <>\n          <b>{formattedUsedMemoryTooltip[0]}</b>\n          &nbsp;\n          {formattedUsedMemoryTooltip[1]}\n          {memoryUsedTooltip}\n        </>\n      ) : (\n        `${formattedUsedMemoryTooltip}${memoryUsedTooltip}`\n      ),\n    },\n    icon: theme === Theme.Dark ? 'MemoryDarkIcon' : 'MemoryLightIcon',\n    content: memoryContent,\n  }\n}\n\nfunction getTotalKeysItem(\n  theme: string,\n  totalKeys = 0,\n  db = 0,\n  dbKeysCount?: number,\n) {\n  const totalKeysItem: any = {\n    id: 'overview-total-keys',\n    value: totalKeys,\n    unavailableText: 'Total Keys are not available',\n    title: 'Total Keys',\n    tooltip: {\n      title: 'Total Keys',\n      content: <b>{numberWithSpaces(totalKeys)}</b>,\n      icon: theme === Theme.Dark ? 'KeyDarkIcon' : 'KeyLightIcon',\n    },\n    icon: theme === Theme.Dark ? 'KeyDarkIcon' : 'KeyLightIcon',\n    content: truncateNumberToRange(totalKeys),\n  }\n\n  // keys in the logical database\n  if (!isUndefined(dbKeysCount) && dbKeysCount < toNumber(totalKeys)) {\n    totalKeysItem.children = [\n      {\n        id: 'total-keys-tip',\n        value: totalKeys,\n        unavailableText: 'Total Keys are not available',\n        title: 'Total Keys',\n        tooltip: {\n          title: 'Total Keys',\n          content: <b>{numberWithSpaces(totalKeys)}</b>,\n        },\n        content: <b>{numberWithSpaces(totalKeys)}</b>,\n      },\n      {\n        id: 'overview-db-total-keys',\n        title: 'Keys',\n        value: dbKeysCount,\n        content: (\n          <>\n            <span\n              style={{\n                fontWeight: 200,\n                paddingRight: 1,\n              }}\n            >\n              db{db || 0}:\n            </span>\n            &nbsp;\n            <b>{numberWithSpaces(dbKeysCount || 0)}</b>\n          </>\n        ),\n      },\n    ]\n  }\n  return totalKeysItem\n}\n\nconst getConnectedClient = (connectedClients: number = 0) =>\n  Number.isInteger(connectedClients)\n    ? connectedClients\n    : `~${Math.round(connectedClients)}`\n\nfunction getConnectedClientItem(theme: string, connectedClients = 0): IMetric {\n  const connectedClientsCount = getConnectedClient(connectedClients)\n  const icon = theme === Theme.Dark ? 'UserDarkIcon' : 'UserLightIcon'\n  return {\n    id: 'overview-connected-clients',\n    value: connectedClients,\n    unavailableText: 'Connected Clients are not available',\n    title: 'Connected Clients',\n    tooltip: {\n      title: 'Connected Clients',\n      content: <b>{connectedClientsCount}</b>,\n      icon,\n    },\n    icon,\n    content: connectedClientsCount,\n  }\n}\n\nexport const getOverviewMetrics = ({\n  theme,\n  items,\n  db = 0,\n}: Props): Array<IMetric> => {\n  const {\n    usedMemory,\n    usedMemoryPercent,\n    totalKeys,\n    connectedClients,\n    cpuUsagePercentage,\n    maxCpuUsagePercentage,\n    opsPerSecond,\n    networkInKbps,\n    networkOutKbps,\n    totalKeysPerDb = {},\n    cloudDetails,\n  } = items\n\n  const availableItems: Array<IMetric> = []\n\n  // CPU\n  if (!isUndefined(cpuUsagePercentage)) {\n    availableItems.push(\n      getCpuUsage(cpuUsagePercentage, theme, maxCpuUsagePercentage),\n    )\n  }\n\n  if (!isUndefined(opsPerSecond)) {\n    availableItems.push(\n      getOpsPerSecondItem(\n        theme,\n        opsPerSecond ?? 0,\n        networkInKbps ?? 0,\n        networkOutKbps ?? 0,\n      ),\n    )\n  }\n\n  // Used memory\n  if (!isUndefined(usedMemory)) {\n    availableItems.push(\n      getUsedMemoryItem(\n        theme,\n        usedMemory ?? 0,\n        cloudDetails?.planMemoryLimit ?? 0,\n        usedMemoryPercent ?? 0,\n        cloudDetails?.memoryLimitMeasurementUnit,\n      ),\n    )\n  }\n\n  // Total keys\n  const totalKeysItem = getTotalKeysItem(\n    theme,\n    totalKeys ?? 0,\n    db,\n    totalKeysPerDb?.[`db${db || 0}`],\n  )\n\n  if (!isUndefined(totalKeys)) {\n    availableItems.push(totalKeysItem)\n  }\n\n  // Connected clients\n  if (!isUndefined(connectedClients)) {\n    availableItems.push(getConnectedClientItem(theme, connectedClients ?? 0))\n  }\n\n  return availableItems\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/components/OverviewMetrics/index.ts",
    "content": "export * from './OverviewMetrics'\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/components/OverviewMetrics/styles.module.scss",
    "content": ".calculationWrapper {\n  display: flex;\n  align-items: center;\n  min-width: 96px;\n}\n\n.cpuWrapper {\n  min-width: 96px;\n\n  @media only screen and (max-width: 992px) {\n    display: none !important;\n  }\n}\n\n.calculation {\n  font-size: 13px;\n  font-weight: 500;\n  margin-left: 8px;\n}\n\n.spinner {\n  width: 18px !important;\n  height: 18px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/components/icons.ts",
    "content": "import KeyDarkIcon from 'uiSrc/assets/img/overview/key_dark.svg?react'\nimport KeyTipIcon from 'uiSrc/assets/img/overview/key_tip.svg?react'\nimport KeyLightIcon from 'uiSrc/assets/img/overview/key_light.svg?react'\nimport KeyIconSvg from 'uiSrc/assets/img/overview/key.svg?react'\n\nimport MemoryDarkIcon from 'uiSrc/assets/img/overview/memory_dark.svg?react'\nimport MemoryLightIcon from 'uiSrc/assets/img/overview/memory_light.svg?react'\nimport MemoryTipIcon from 'uiSrc/assets/img/overview/memory_tip.svg?react'\nimport MemoryIconSvg from 'uiSrc/assets/img/overview/memory.svg?react'\n\nimport MeasureLightIcon from 'uiSrc/assets/img/overview/measure_light.svg?react'\nimport MeasureDarkIcon from 'uiSrc/assets/img/overview/measure_dark.svg?react'\nimport MeasureTipIcon from 'uiSrc/assets/img/overview/measure_tip.svg?react'\nimport MeasureIconSvg from 'uiSrc/assets/img/overview/measure.svg?react'\n\nimport TimeLightIcon from 'uiSrc/assets/img/overview/time_light.svg?react'\nimport TimeDarkIcon from 'uiSrc/assets/img/overview/time_dark.svg?react'\nimport TimeTipIcon from 'uiSrc/assets/img/overview/time_tip.svg?react'\nimport TimeIconSvg from 'uiSrc/assets/img/overview/time.svg?react'\n\nimport UserDarkIcon from 'uiSrc/assets/img/overview/user_dark.svg?react'\nimport UserLightIcon from 'uiSrc/assets/img/overview/user_light.svg?react'\nimport UserTipIcon from 'uiSrc/assets/img/overview/user_tip.svg?react'\nimport UserIconSvg from 'uiSrc/assets/img/overview/user.svg?react'\n\nimport InputTipIcon from 'uiSrc/assets/img/overview/input_tip.svg?react'\nimport InputLightIcon from 'uiSrc/assets/img/overview/input_light.svg?react'\nimport InputDarkIcon from 'uiSrc/assets/img/overview/input_dark.svg?react'\nimport InputIconSvg from 'uiSrc/assets/img/overview/input.svg?react'\n\nimport OutputTipIcon from 'uiSrc/assets/img/overview/output_tip.svg?react'\nimport OutputLightIcon from 'uiSrc/assets/img/overview/output_light.svg?react'\nimport OutputDarkIcon from 'uiSrc/assets/img/overview/output_dark.svg?react'\nimport OutputIconSvg from 'uiSrc/assets/img/overview/output.svg?react'\n\nexport {\n  KeyDarkIcon,\n  KeyTipIcon,\n  KeyLightIcon,\n  MemoryDarkIcon,\n  MemoryLightIcon,\n  MemoryTipIcon,\n  MeasureLightIcon,\n  MeasureDarkIcon,\n  MeasureTipIcon,\n  TimeLightIcon,\n  TimeDarkIcon,\n  TimeTipIcon,\n  UserDarkIcon,\n  UserLightIcon,\n  UserTipIcon,\n  InputTipIcon,\n  InputLightIcon,\n  InputDarkIcon,\n  OutputTipIcon,\n  OutputLightIcon,\n  OutputDarkIcon,\n  KeyIconSvg,\n  MemoryIconSvg,\n  MeasureIconSvg,\n  TimeIconSvg,\n  UserIconSvg,\n  InputIconSvg,\n  OutputIconSvg,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/hooks/useDatabaseOverview.spec.ts",
    "content": "import { cloneDeep, set } from 'lodash'\nimport {\n  act,\n  initialStateDefault,\n  mockStore,\n  renderHook,\n} from 'uiSrc/utils/test-utils'\nimport { getDatabaseConfigInfo } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { useDatabaseOverview } from './useDatabaseOverview'\n\n// Mock the telemetry function\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\n// Mock the getOverviewMetrics function\njest.mock(\n  'uiSrc/components/database-overview/components/OverviewMetrics',\n  () => ({\n    getOverviewMetrics: jest.fn().mockReturnValue([\n      { id: 'cpu', title: 'CPU' },\n      { id: 'memory', title: 'Memory' },\n      { id: 'keys', title: 'Keys' },\n    ]),\n  }),\n)\n\nconst mockInstanceId = 'test-instance-id'\nconst mockDb = 0\n\nconst overviewData = {\n  version: '6.0.0',\n  usedMemory: 45.2 * 1024 * 1024, // 45.2 MB\n  usedMemoryPercent: null,\n  totalKeys: 5000,\n  connectedClients: 1,\n  cpuUsagePercentage: 0.23,\n  networkInKbps: 3,\n  networkOutKbps: 5,\n  opsPerSecond: 10,\n  cloudDetails: undefined,\n}\n\nconst initialState = set(\n  cloneDeep(initialStateDefault),\n  'connections.instances.instanceOverview',\n  overviewData,\n)\n\n// Set connected instance\nset(initialState, 'connections.instances.connectedInstance', {\n  id: mockInstanceId,\n  db: mockDb,\n})\n\nlet mockedStore: ReturnType<typeof mockStore>\nlet mockDate: Date\ntype HookReturnType = ReturnType<typeof useDatabaseOverview>\n\nconst renderHelper = (store: typeof mockedStore) => {\n  const { result } = renderHook(() => useDatabaseOverview(), {\n    store,\n  })\n\n  return result.current as HookReturnType\n}\n\ndescribe('useDatabaseOverview', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockedStore = mockStore(initialState)\n\n    // Set up fake timers\n    jest.useFakeTimers()\n    mockDate = new Date('2024-11-22T12:00:00Z')\n    jest.setSystemTime(mockDate)\n  })\n\n  afterEach(() => {\n    jest.useRealTimers()\n  })\n\n  it('should return metrics and other data', () => {\n    const data = renderHelper(mockedStore)\n    expect(data.metrics).toEqual([\n      { id: 'cpu', title: 'CPU' },\n      { id: 'memory', title: 'Memory' },\n      { id: 'keys', title: 'Keys' },\n    ])\n    expect(data.connectivityError).toBeUndefined()\n    expect(data.lastRefreshTime).not.toBeUndefined()\n    expect(data.subscriptionType).toBeUndefined()\n    expect(data.subscriptionId).toBeUndefined()\n    expect(data.isBdbPackages).toBeUndefined()\n    expect(data.usedMemoryPercent).toBeUndefined()\n    expect(typeof data.handleRefresh).toBe('function')\n    expect(typeof data.handleEnableAutoRefresh).toBe('function')\n  })\n\n  it('should set lastRefreshTime to current timestamp on mount', () => {\n    const data = renderHelper(mockedStore)\n    expect(data.lastRefreshTime).toBe(mockDate.getTime())\n  })\n\n  it('should not dispatch getDatabaseConfigInfoAction when connectivity error exists', () => {\n    const stateWithError = set(\n      cloneDeep(initialState),\n      'app.connectivity.error',\n      'Network error',\n    )\n    mockedStore = mockStore(stateWithError)\n\n    renderHook(() => useDatabaseOverview(), {\n      store: mockedStore,\n    })\n\n    const actions = mockedStore.getActions()\n    expect(actions).not.toContainEqual(getDatabaseConfigInfo())\n  })\n\n  it('should calculate usedMemoryPercent when cloud plan limit is present', () => {\n    const stateWithCloudDetails = set(\n      cloneDeep(initialState),\n      'connections.instances.instanceOverview.cloudDetails',\n      {\n        cloudId: 123,\n        subscriptionId: 456,\n        subscriptionType: 'fixed',\n        planMemoryLimit: 75,\n        memoryLimitMeasurementUnit: 'MB',\n      },\n    )\n    mockedStore = mockStore(stateWithCloudDetails)\n\n    const data = renderHelper(mockedStore)\n\n    // 45.2 MB / 75 MB ≈ 60.3%\n    expect(data.usedMemoryPercent).toBeCloseTo(60.3, 1)\n    expect(data.subscriptionType).toBe('fixed')\n    expect(data.subscriptionId).toBe(456)\n  })\n\n  it('should handle refresh correctly', () => {\n    const data = renderHelper(mockedStore)\n\n    // Clear previous actions\n    mockedStore.clearActions()\n\n    act(() => {\n      data.handleRefresh()\n    })\n\n    const actions = mockedStore.getActions()\n    expect(actions).toContainEqual(getDatabaseConfigInfo())\n  })\n\n  it('should send telemetry event when auto-refresh is enabled', () => {\n    const data = renderHelper(mockedStore)\n    act(() => {\n      data.handleEnableAutoRefresh(true, '30')\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.OVERVIEW_AUTO_REFRESH_ENABLED,\n      eventData: {\n        databaseId: mockInstanceId,\n        refreshRate: 30,\n      },\n    })\n  })\n\n  it('should send telemetry event when auto-refresh is disabled', () => {\n    const data = renderHelper(mockedStore)\n    act(() => {\n      data.handleEnableAutoRefresh(false, '30')\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.OVERVIEW_AUTO_REFRESH_DISABLED,\n      eventData: {\n        databaseId: mockInstanceId,\n        refreshRate: 30,\n      },\n    })\n  })\n\n  it('should handle flexible subscription type correctly', () => {\n    const stateWithFlexibleSubscription = set(\n      cloneDeep(initialState),\n      'connections.instances.instanceOverview.cloudDetails',\n      {\n        cloudId: 123,\n        subscriptionId: 456,\n        subscriptionType: 'flexible',\n        planMemoryLimit: 75,\n        memoryLimitMeasurementUnit: 'MB',\n      },\n    )\n    mockedStore = mockStore(stateWithFlexibleSubscription)\n    const data = renderHelper(mockedStore)\n\n    expect(data.subscriptionType).toBe('flexible')\n  })\n\n  it('should handle BDB packages flag correctly', () => {\n    const stateWithBdbPackages = set(\n      cloneDeep(initialState),\n      'connections.instances.instanceOverview.cloudDetails',\n      {\n        cloudId: 123,\n        subscriptionId: 456,\n        subscriptionType: 'fixed',\n        planMemoryLimit: 75,\n        memoryLimitMeasurementUnit: 'MB',\n        isBdbPackages: true,\n      },\n    )\n    mockedStore = mockStore(stateWithBdbPackages)\n    const data = renderHelper(mockedStore)\n\n    expect(data.isBdbPackages).toBe(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/hooks/useDatabaseOverview.ts",
    "content": "import { useContext, useEffect, useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport {\n  connectedInstanceOverviewSelector,\n  connectedInstanceSelector,\n  getDatabaseConfigInfoAction,\n} from 'uiSrc/slices/instances/instances'\nimport {\n  appConnectivityError,\n  setConnectivityError,\n} from 'uiSrc/slices/app/connectivity'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Nullable, toBytes, truncatePercentage } from 'uiSrc/utils'\nimport { getOverviewMetrics } from 'uiSrc/components/database-overview/components/OverviewMetrics'\n\nfunction getUsedMemoryPercent(\n  planMemoryLimit: number | undefined,\n  usedMemory: Nullable<number> | undefined,\n  memoryLimitMeasurementUnit = 'MB',\n) {\n  if (!planMemoryLimit) {\n    return undefined\n  }\n  return parseFloat(\n    `${truncatePercentage(((usedMemory || 0) / toBytes(planMemoryLimit, memoryLimitMeasurementUnit)) * 100, 1)}`,\n  )\n}\n\nexport const useDatabaseOverview = () => {\n  const { theme } = useContext(ThemeContext)\n  const dispatch = useDispatch()\n  const [lastRefreshTime, setLastRefreshTime] = useState<number | null>(null)\n  const { id: connectedInstanceId = '', db } = useSelector(\n    connectedInstanceSelector,\n  )\n  const connectivityError = useSelector(appConnectivityError)\n\n  const overview = useSelector(connectedInstanceOverviewSelector)\n  const {\n    usedMemory,\n    cloudDetails: {\n      subscriptionType,\n      subscriptionId,\n      planMemoryLimit,\n      memoryLimitMeasurementUnit,\n      isBdbPackages,\n    } = {},\n  } = overview\n\n  const loadData = () => {\n    if (connectedInstanceId) {\n      dispatch(\n        getDatabaseConfigInfoAction(connectedInstanceId, () => {\n          // Clear connectivity error on successful API call.\n          // Always dispatch to avoid stale closure issues - dispatching null\n          // when there's no error is a safe no-op.\n          dispatch(setConnectivityError(null))\n        }),\n      )\n      setLastRefreshTime(Date.now())\n    }\n  }\n\n  useEffect(() => {\n    if (!connectivityError) {\n      setLastRefreshTime(Date.now())\n    }\n  }, [connectivityError])\n\n  const handleEnableAutoRefresh = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) =>\n    sendEventTelemetry({\n      event: enableAutoRefresh\n        ? TelemetryEvent.OVERVIEW_AUTO_REFRESH_ENABLED\n        : TelemetryEvent.OVERVIEW_AUTO_REFRESH_DISABLED,\n      eventData: {\n        databaseId: connectedInstanceId,\n        refreshRate: +refreshRate,\n      },\n    })\n\n  const handleRefresh = () => {\n    loadData()\n  }\n  const usedMemoryPercent = getUsedMemoryPercent(\n    planMemoryLimit,\n    usedMemory,\n    memoryLimitMeasurementUnit,\n  )\n\n  const metrics = useMemo(() => {\n    const overviewItems = {\n      ...overview,\n      usedMemoryPercent,\n    }\n    return getOverviewMetrics({\n      theme,\n      items: overviewItems,\n      db,\n    })\n  }, [theme, overview, db, usedMemoryPercent])\n  return {\n    metrics,\n    connectivityError,\n    lastRefreshTime,\n    subscriptionType,\n    subscriptionId,\n    isBdbPackages,\n    usedMemoryPercent,\n    handleEnableAutoRefresh,\n    handleRefresh,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/database-overview/styles.module.scss",
    "content": ".container {\n  margin: 0;\n\n  @media only screen and (max-width: 1124px) {\n    .modules {\n      margin-left: 0;\n      border-left: 0;\n      padding-left: 12px;\n    }\n\n    .overview {\n      border-right: 0;\n    }\n  }\n}\n\n.itemContainer {\n  height: 42px;\n  border-radius: 8px;\n  align-items: center;\n  justify-content: center;\n  margin-left: 6px;\n}\n\n.modules {\n  padding-left: 0;\n  padding-right: 0;\n}\n\n.overviewItem {\n  min-width: 58px;\n\n  padding: 0 14px;\n  color: var(--euiTextSubduedColor);\n\n  &:not(:last-child) {\n    border-right: 1px solid var(--separatorColor);\n  }\n}\n\n.icon {\n  color: var(--euiColorMediumShade);\n  margin-right: 6px;\n  width: auto !important;\n  height: 14px !important;\n  max-width: 18px;\n}\n\n.overviewItemContent {\n  font-size: 14px;\n  font-weight: 400;\n}\n\n.tooltip {\n  max-width: 372px;\n}\n\n.commandsPerSecTip {\n  margin-bottom: 8px;\n\n  &:last-child {\n    margin-bottom: 0;\n  }\n\n  .moreInfoOverviewIcon {\n    margin-right: 8px;\n    width: auto !important;\n    max-width: 20px;\n    height: 18px !important;\n  }\n\n  .moreInfoOverviewContent {\n    flex-direction: row;\n    margin-right: 6px;\n    font-size: 12px;\n  }\n\n  .moreInfoOverviewTitle {\n    margin-right: 6px;\n    font-size: 12px;\n    font-weight: 200;\n  }\n}\n\n.RediStackLogoWrapper {\n  padding: 0 6px;\n  cursor: pointer;\n  transition: transform 250ms ease-in-out;\n\n  &:hover {\n    transform: translateY(-1px);\n  }\n\n  .redistackLogoIcon {\n    width: 24px;\n    height: 24px;\n\n    @media only screen and (min-width: 1124px) {\n      display: none;\n    }\n  }\n\n  .redistackIcon {\n    display: none;\n    width: 114px;\n    height: 18px;\n\n    @media only screen and (min-width: 1124px) {\n      display: block;\n    }\n  }\n}\n\n.tooltipLogo {\n  width: 88px !important;\n  height: 18px !important;\n\n  @media only screen and (min-width: 1124px) {\n    display: none !important;\n  }\n}\n\n.autoRefresh {\n  padding-top: 0;\n  padding-bottom: 0;\n  margin-left: -2px; /* adjust for whitespace in the icon */\n\n  :global(.popover-without-top-tail) {\n    margin-top: 0;\n  }\n}\n\n.upgradeBtnItem {\n  border-right: none;\n  height: 28px;\n}\n\n.upgradeBtn {\n  padding: 0;\n  margin-top: 0;\n\n  :global(.euiButton__text) {\n    font: normal normal 400 14px/18px Graphik, sans-serif !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/divider/Divider.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport Divider, { DividerProps } from './Divider'\n\nconst mockedProps = mock<DividerProps>()\n\ndescribe('Divider', () => {\n  it('should render', () => {\n    expect(render(<Divider {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/divider/Divider.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled from 'styled-components'\n\nimport { StyledDividerProps } from './Divider.types'\n\nconst dividerStyles = {\n  orientation: {\n    horizontal: 'width: 100%; height: 1px;',\n    vertical: 'width: 1px; height: 100%;',\n  },\n  variant: {\n    fullWidth: {\n      horizontal: '',\n      vertical: '',\n    },\n    half: {\n      horizontal: 'width: 50%;',\n      vertical: 'height: 50%;',\n    },\n  },\n}\n\nexport const DividerWrapper = styled.div<HTMLAttributes<HTMLDivElement>>`\n  display: flex;\n  align-items: center;\n  justify-content: center;\n`\n\nexport const Divider = styled.hr<StyledDividerProps>`\n  border: none;\n  background-color: ${({\n    theme,\n    $color = theme.semantic.color.background.neutral500,\n  }) => $color};\n\n  ${({ $orientation = 'horizontal' }) =>\n    dividerStyles.orientation[$orientation]}\n  ${({ $variant = 'fullWidth', $orientation = 'horizontal' }) =>\n    dividerStyles.variant[$variant][$orientation]}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/divider/Divider.tsx",
    "content": "import React from 'react'\n\nimport { DividerProps } from './Divider.types'\nimport * as S from './Divider.styles'\n\nconst Divider = ({\n  orientation,\n  variant,\n  color,\n  className: _className,\n  ...props\n}: DividerProps) => (\n  <S.DividerWrapper {...props}>\n    <S.Divider $variant={variant} $orientation={orientation} $color={color} />\n  </S.DividerWrapper>\n)\n\nexport default Divider\n"
  },
  {
    "path": "redisinsight/ui/src/components/divider/Divider.types.ts",
    "content": "import { HTMLAttributes } from 'react'\n\nexport type DividerVariant = 'fullWidth' | 'half'\nexport type DividerOrientation = 'horizontal' | 'vertical'\n\nexport interface DividerProps {\n  orientation?: DividerOrientation\n  variant?: DividerVariant\n  color?: string\n  className?: string\n}\n\nexport interface StyledDividerProps extends HTMLAttributes<HTMLHRElement> {\n  $color?: string\n  $orientation?: DividerOrientation\n  $variant?: DividerVariant\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/explore-guides/ExploreGuides.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { MOCK_EXPLORE_GUIDES } from 'uiSrc/constants/mocks/mock-explore-guides'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { findTutorialPath } from 'uiSrc/utils'\n\nimport ExploreGuides from './ExploreGuides'\n\njest.mock('uiSrc/slices/content/guide-links', () => ({\n  ...jest.requireActual('uiSrc/slices/content/guide-links'),\n  guideLinksSelector: jest.fn().mockReturnValue({\n    data: MOCK_EXPLORE_GUIDES,\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  findTutorialPath: jest.fn(),\n}))\n\ndescribe('ExploreGuides', () => {\n  it('should render', () => {\n    expect(render(<ExploreGuides />)).toBeTruthy()\n  })\n\n  it('should render guides', () => {\n    render(<ExploreGuides />)\n\n    MOCK_EXPLORE_GUIDES.forEach(({ tutorialId, icon }) => {\n      expect(\n        screen.getByTestId(`guide-button-${tutorialId}`),\n      ).toBeInTheDocument()\n      expect(screen.getByTestId(`guide-icon-${icon}`)).toBeInTheDocument()\n    })\n  })\n\n  it('should call proper history push after click on guide', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    render(<ExploreGuides />)\n\n    fireEvent.click(screen.getByTestId('guide-button-sq-intro'))\n\n    expect(pushMock).toHaveBeenCalledWith({\n      search: 'path=tutorials/path',\n    })\n  })\n\n  it('should call proper history push after click on guide with tutorial', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    render(<ExploreGuides />)\n\n    fireEvent.click(screen.getByTestId('guide-button-ds-json-intro'))\n\n    expect(pushMock).toHaveBeenCalledWith({\n      search: 'path=tutorials/path',\n    })\n  })\n\n  it('should call proper telemetry event after click on guide', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<ExploreGuides />)\n\n    fireEvent.click(screen.getByTestId('guide-button-sq-intro'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        tutorialId: 'sq-intro',\n        provider: 'REDIS_CLOUD',\n        source: 'empty browser',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/explore-guides/ExploreGuides.styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\nimport { Title } from 'uiSrc/components/base/text/Title'\n\nexport const Guides = styled(FlexGroup)`\n  max-width: 414px;\n`\n\nexport const CenteredTitle = styled(Title)`\n  text-align: center;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/explore-guides/ExploreGuides.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { guideLinksSelector } from 'uiSrc/slices/content/guide-links'\n\nimport GUIDE_ICONS from 'uiSrc/components/explore-guides/icons'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nimport { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels'\nimport { findTutorialPath } from 'uiSrc/utils'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport * as S from './ExploreGuides.styles'\n\nconst ExploreGuides = () => {\n  const { data } = useSelector(guideLinksSelector)\n  const { provider } = useSelector(connectedInstanceSelector)\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const handleLinkClick = (tutorialId: string) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      eventData: {\n        databaseId: instanceId,\n        tutorialId,\n        provider,\n        source: 'empty browser',\n      },\n    })\n\n    const tutorialPath = findTutorialPath({ id: tutorialId ?? '' })\n    dispatch(openTutorialByPath(tutorialPath ?? '', history))\n  }\n\n  return (\n    <div data-testid=\"explore-guides\">\n      <S.CenteredTitle color=\"primary\" size=\"S\">\n        Here&apos;s a good starting point\n      </S.CenteredTitle>\n      <Spacer size=\"s\" />\n      <Text color=\"primary\" textAlign=\"center\">\n        Explore the amazing world of Redis with our interactive guides\n      </Text>\n      <Spacer size=\"xl\" />\n      {!!data.length && (\n        <S.Guides gap=\"l\" wrap justify=\"center\">\n          {data.map(({ title, tutorialId, icon }) => (\n            <SecondaryButton\n              key={tutorialId}\n              inverted\n              tabIndex={0}\n              onClick={() => handleLinkClick(tutorialId)}\n              data-testid={`guide-button-${tutorialId}`}\n            >\n              {icon in GUIDE_ICONS && (\n                <RiIcon\n                  type={GUIDE_ICONS[icon]}\n                  data-testid={`guide-icon-${icon}`}\n                  color=\"inherit\"\n                />\n              )}\n              {title}\n            </SecondaryButton>\n          ))}\n        </S.Guides>\n      )}\n    </div>\n  )\n}\n\nexport default ExploreGuides\n"
  },
  {
    "path": "redisinsight/ui/src/components/explore-guides/icons.ts",
    "content": "import { AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\n\nconst GUIDE_ICONS: Record<string, AllIconsType> = {\n  search: 'QuerySearchIcon',\n  json: 'JsonIcon',\n  'probabilistic-data-structures': 'ProbabilisticIcon',\n  'time-series': 'TimeSeriesIcon',\n  'vector-similarity-search': 'VectorSimilarityIcon',\n}\n\nexport default GUIDE_ICONS\n"
  },
  {
    "path": "redisinsight/ui/src/components/explore-guides/index.ts",
    "content": "import ExploreGuides from './ExploreGuides'\n\nexport default ExploreGuides\n"
  },
  {
    "path": "redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { FeatureFlags } from 'uiSrc/constants'\n\nimport FeatureFlagComponent from './FeatureFlagComponent'\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    name: {\n      flag: false,\n    },\n    otherName: {\n      flag: false,\n    },\n  }),\n}))\n\nconst InnerComponent = () => <span data-testid=\"inner-component\" />\nconst OtherwiseComponent = () => <span data-testid=\"otherwise-component\" />\ndescribe('FeatureFlagComponent', () => {\n  describe('Single feature', () => {\n    it('should not render component by default', () => {\n      render(\n        <FeatureFlagComponent name={'name' as FeatureFlags}>\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(screen.queryByTestId('inner-component')).not.toBeInTheDocument()\n    })\n\n    it('should render component', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        name: {\n          flag: true,\n        },\n      })\n\n      render(\n        <FeatureFlagComponent name={'name' as FeatureFlags}>\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(screen.getByTestId('inner-component')).toBeInTheDocument()\n    })\n\n    it('should render otherwise component if the feature flag not enabled', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        name: {\n          flag: false,\n        },\n      })\n\n      const { queryByTestId } = render(\n        <FeatureFlagComponent\n          name={'name' as FeatureFlags}\n          otherwise={<OtherwiseComponent />}\n        >\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(queryByTestId('inner-component')).not.toBeInTheDocument()\n      expect(queryByTestId('otherwise-component')).toBeInTheDocument()\n    })\n  })\n\n  describe('Multiple features', () => {\n    it('should not render component if any feature flag is disabled', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        name: {\n          flag: true,\n        },\n        otherName: {\n          flag: false,\n        },\n      })\n\n      const { queryByTestId } = render(\n        <FeatureFlagComponent\n          name={['name' as FeatureFlags, 'otherName' as FeatureFlags]}\n        >\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(queryByTestId('inner-component')).not.toBeInTheDocument()\n    })\n\n    it('should use enabledByDefault=true for unmatched features', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({})\n\n      const { queryByTestId } = render(\n        <FeatureFlagComponent\n          name={['name' as FeatureFlags, 'otherName' as FeatureFlags]}\n          enabledByDefault\n        >\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(queryByTestId('inner-component')).toBeInTheDocument()\n    })\n\n    it('should use enabledByDefault=false for unmatched features', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({})\n\n      const { queryByTestId } = render(\n        <FeatureFlagComponent\n          name={['name' as FeatureFlags, 'otherName' as FeatureFlags]}\n          enabledByDefault={false}\n        >\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(queryByTestId('inner-component')).not.toBeInTheDocument()\n    })\n\n    it('should render component if all feature flags are enabled', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        name: {\n          flag: true,\n        },\n        otherName: {\n          flag: true,\n        },\n      })\n\n      const { queryByTestId } = render(\n        <FeatureFlagComponent\n          name={['name' as FeatureFlags, 'otherName' as FeatureFlags]}\n        >\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(queryByTestId('inner-component')).toBeInTheDocument()\n    })\n\n    it('should render otherwise component if any feature flag is not enabled', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        name: {\n          flag: true,\n        },\n        otherName: {\n          flag: false,\n        },\n      })\n\n      const { queryByTestId } = render(\n        <FeatureFlagComponent\n          name={['name' as FeatureFlags, 'otherName' as FeatureFlags]}\n          otherwise={<OtherwiseComponent />}\n        >\n          <InnerComponent />\n        </FeatureFlagComponent>,\n      )\n\n      expect(queryByTestId('inner-component')).not.toBeInTheDocument()\n      expect(queryByTestId('otherwise-component')).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/feature-flag-component/FeatureFlagComponent.tsx",
    "content": "import React from 'react'\nimport { isArray } from 'lodash'\nimport { useSelector } from 'react-redux'\nimport { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\n\nexport interface Props {\n  name: FeatureFlags | FeatureFlags[]\n  children?: JSX.Element | JSX.Element[]\n  otherwise?: React.ReactElement\n  enabledByDefault?: boolean\n}\n\nconst FeatureFlagComponent = (props: Props) => {\n  const { children, name, otherwise, enabledByDefault } = props\n  const features = useSelector(appFeatureFlagsFeaturesSelector)\n\n  const nameArray = isArray(name) ? name : [name]\n  const matchingFeatures = nameArray.map(\n    (feature) => features?.[feature] || { flag: enabledByDefault },\n  )\n  const allFlagsEnabled = matchingFeatures.every((feature) => feature.flag)\n\n  if (!allFlagsEnabled) {\n    return otherwise ?? null\n  }\n\n  if (!children) {\n    return null\n  }\n\n  return <>{children}</>\n}\n\nexport default FeatureFlagComponent\n"
  },
  {
    "path": "redisinsight/ui/src/components/feature-flag-component/index.ts",
    "content": "import FeatureFlagComponent from './FeatureFlagComponent'\n\nexport default FeatureFlagComponent\n"
  },
  {
    "path": "redisinsight/ui/src/components/field-message/FieldMessage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport FieldMessage, { Props } from './FieldMessage'\n\nconst mockedProps = mock<Props>()\n\ndescribe('FieldMessage', () => {\n  it('should render', () => {\n    const message = 'Error Message'\n    expect(\n      render(<FieldMessage {...instance(mockedProps)}>{message}</FieldMessage>),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/field-message/FieldMessage.tsx",
    "content": "import React, { Ref, useEffect, useRef } from 'react'\nimport cx from 'classnames'\n\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { scrollIntoView } from 'uiSrc/utils'\nimport { AllIconsType, RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\ntype Colors =\n  | 'default'\n  | 'secondary'\n  | 'accent'\n  | 'warning'\n  | 'danger'\n  | 'subdued'\n  | 'ghost'\nexport interface Props {\n  children: React.ReactElement | string\n  color?: Colors\n  scrollViewOnAppear?: boolean\n  icon?: AllIconsType\n  testID?: string\n}\n\nconst FieldMessage = ({\n  children,\n  color,\n  testID,\n  icon,\n  scrollViewOnAppear,\n}: Props) => {\n  const divRef: Ref<HTMLDivElement> = useRef(null)\n\n  useEffect(() => {\n    // componentDidMount\n    if (scrollViewOnAppear) {\n      scrollIntoView(divRef?.current, {\n        behavior: 'smooth',\n        block: 'nearest',\n        inline: 'end',\n      })\n    }\n  }, [])\n\n  return (\n    <div ref={divRef} className={cx(styles.container)}>\n      {icon && (\n        <RiIcon\n          className={cx(styles.icon)}\n          type={icon}\n          color={color || 'danger'}\n        />\n      )}\n      <ColorText\n        className={cx(styles.message)}\n        data-testid={testID}\n        color={color || 'danger'}\n      >\n        {children}\n      </ColorText>\n    </div>\n  )\n}\n\nexport default FieldMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/field-message/styles.module.scss",
    "content": ".container {\n  padding: 4px 0;\n  display: flex;\n  flex: 1;\n}\n.icon {\n  margin-right: 4px;\n}\n.message {\n  font: normal normal normal 12px/16px Graphik, sans-serif;\n  letter-spacing: -0.12px\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/form-dialog/FooterDatabaseForm.ts",
    "content": "import styled from 'styled-components'\n\nexport const FooterDatabaseForm = styled.div.attrs({\n  id: 'footerDatabaseForm',\n})`\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/form-dialog/FormDialog.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport FormDialog from './FormDialog'\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\ndescribe('FormDialog', () => {\n  it('should render', () => {\n    render(\n      <FormDialog\n        isOpen\n        onClose={jest.fn()}\n        header={<div data-testid=\"header\" />}\n        footer={<div data-testid=\"footer\" />}\n      >\n        <div data-testid=\"body\" />\n      </FormDialog>,\n    )\n\n    // comment out until the modal header issue is fixed\n    // expect(screen.getByTestId('header')).toBeInTheDocument()\n    expect(screen.getByTestId('footer')).toBeInTheDocument()\n    expect(screen.getByTestId('body')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/form-dialog/FormDialog.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display/modal'\n\nexport const StyledFormDialogContent = styled(Modal.Content.Compose)`\n  width: 900px;\n  height: 700px;\n\n  max-width: calc(100vw - 120px);\n  max-height: calc(100vh - 120px);\n`\n\nexport const StyledFormDialogContentBody = styled(Modal.Content.Body)`\n  flex: 1;\n  min-height: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/form-dialog/FormDialog.tsx",
    "content": "import React from 'react'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { Modal } from 'uiSrc/components/base/display'\nimport {\n  StyledFormDialogContent,\n  StyledFormDialogContentBody,\n} from './FormDialog.styles'\n\nexport interface Props {\n  isOpen: boolean\n  onClose: () => void\n  header: Nullable<React.ReactNode>\n  footer?: Nullable<React.ReactNode>\n  children: Nullable<React.ReactNode>\n  className?: string\n}\n\nconst FormDialog = (props: Props) => {\n  const { isOpen, onClose, header, footer, children, className = '' } = props\n\n  if (!isOpen) return null\n\n  return (\n    <Modal.Compose open={isOpen}>\n      <StyledFormDialogContent\n        persistent\n        className={className}\n        onCancel={onClose}\n      >\n        <Modal.Content.Close icon={CancelIcon} onClick={onClose} />\n        <Modal.Content.Header.Compose>\n          <Modal.Content.Header.Title>{header}</Modal.Content.Header.Title>\n        </Modal.Content.Header.Compose>\n        <StyledFormDialogContentBody content={children} />\n        <Modal.Content.Footer.Compose>{footer}</Modal.Content.Footer.Compose>\n      </StyledFormDialogContent>\n    </Modal.Compose>\n  )\n}\n\nexport default FormDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/form-dialog/index.ts",
    "content": "import FormDialog from './FormDialog'\n\nexport default FormDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/formated-date/FormatedDate.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'\nimport { formatTimestamp } from 'uiSrc/utils'\nimport FormatedDate, { Props } from './FormatedDate'\n\nconst mockedProps = mock<Props>()\nconst mockedDate = new Date('2012-12-12').getTime()\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsConfigSelector: jest.fn().mockReturnValue({\n    dateFormat: 'y',\n    timezone: 'UTC',\n  }),\n}))\n\ndescribe('FormatedDate', () => {\n  it('should render', () => {\n    expect(\n      render(<FormatedDate {...instance(mockedProps)} date={mockedDate} />),\n    ).toBeTruthy()\n  })\n\n  it('should formatDate relying on user config settings', () => {\n    render(<FormatedDate {...instance(mockedProps)} date={mockedDate} />)\n    expect(screen.getByText('2012')).toBeTruthy()\n  })\n\n  it('should formatDate relying on default settings if no settings saved yet', () => {\n    ;(userSettingsConfigSelector as jest.Mock).mockImplementation(() => ({\n      dateFormat: null,\n      timezone: null,\n    }))\n    render(<FormatedDate {...instance(mockedProps)} date={mockedDate} />)\n    expect(screen.getByText(formatTimestamp(mockedDate))).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/formated-date/FormatedDate.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { DATETIME_FORMATTER_DEFAULT, TimezoneOption } from 'uiSrc/constants'\nimport { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'\nimport { formatTimestamp } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  date: Date | string | number\n}\n\nconst FormatedDate = ({ date }: Props) => {\n  const config = useSelector(userSettingsConfigSelector)\n  const dateFormat = config?.dateFormat || DATETIME_FORMATTER_DEFAULT\n  const timezone = config?.timezone || TimezoneOption.Local\n\n  if (!date) return null\n\n  const formatedDate = formatTimestamp(date, dateFormat, timezone)\n\n  return (\n    <RiTooltip anchorClassName={styles.text} content={formatedDate}>\n      <span>{formatedDate}</span>\n    </RiTooltip>\n  )\n}\n\nexport default FormatedDate\n"
  },
  {
    "path": "redisinsight/ui/src/components/formated-date/index.ts",
    "content": "import FormatedDate from './FormatedDate'\n\nexport { FormatedDate }\n\nexport default FormatedDate\n"
  },
  {
    "path": "redisinsight/ui/src/components/formated-date/styles.module.scss",
    "content": ".text {\n  display: inline-block;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  overflow: hidden;\n  position: relative;\n  max-width: 100%;\n  vertical-align: middle;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/full-screen/FullScreen.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, FullScreen } from './FullScreen'\n\nconst mockedProps = mock<Props>()\n\ndescribe('FullScreen', () => {\n  it('should render', () => {\n    expect(render(<FullScreen {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/full-screen/FullScreen.tsx",
    "content": "import React from 'react'\nimport { ExtendIcon, ShrinkIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiTooltip } from 'uiSrc/components'\n\nexport interface Props {\n  isFullScreen: boolean\n  onToggleFullScreen: () => void\n  anchorClassName?: string\n  btnTestId?: string\n}\n\nconst FullScreen = ({\n  isFullScreen,\n  onToggleFullScreen,\n  anchorClassName = '',\n  btnTestId = 'toggle-full-screen',\n}: Props) => (\n  <RiTooltip\n    content={isFullScreen ? 'Exit Full Screen' : 'Full Screen'}\n    position=\"left\"\n    anchorClassName={anchorClassName}\n  >\n    <IconButton\n      icon={isFullScreen ? ShrinkIcon : ExtendIcon}\n      color=\"primary\"\n      aria-label=\"Open full screen\"\n      onClick={onToggleFullScreen}\n      data-testid={btnTestId}\n    />\n  </RiTooltip>\n)\n\nexport { FullScreen }\n"
  },
  {
    "path": "redisinsight/ui/src/components/full-screen/index.ts",
    "content": "export { FullScreen } from './FullScreen'\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-azure-auth/AzureAuthCallbackPage.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { AzureAuthStatus } from 'apiSrc/modules/azure/constants'\n\nimport AzureAuthCallbackPage from './AzureAuthCallbackPage'\n\ndescribe('AzureAuthCallbackPage', () => {\n  const mockClose = jest.fn()\n  const originalLocation = window.location\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    window.close = mockClose\n    localStorage.clear()\n  })\n\n  afterEach(() => {\n    // Restore original location\n    Object.defineProperty(window, 'location', {\n      value: originalLocation,\n      writable: true,\n    })\n  })\n\n  const setLocationWithResult = (result: object) => {\n    const encodedResult = encodeURIComponent(\n      btoa(unescape(encodeURIComponent(JSON.stringify(result)))),\n    )\n    Object.defineProperty(window, 'location', {\n      value: {\n        ...originalLocation,\n        href: `http://localhost/azure-auth-callback?result=${encodedResult}`,\n        search: `?result=${encodedResult}`,\n      },\n      writable: true,\n    })\n  }\n\n  it('should render returning message with valid result', () => {\n    setLocationWithResult({\n      status: AzureAuthStatus.Succeed,\n      account: { id: 'test-id', username: 'test@example.com' },\n    })\n\n    const { getByText } = render(<AzureAuthCallbackPage />)\n\n    expect(getByText('Returning to RedisInsight...')).toBeInTheDocument()\n    expect(\n      getByText('This window will close automatically'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render error message when result parameter is missing', () => {\n    Object.defineProperty(window, 'location', {\n      value: {\n        ...originalLocation,\n        href: 'http://localhost/azure-auth-callback',\n        search: '',\n      },\n      writable: true,\n    })\n\n    const { getByText } = render(<AzureAuthCallbackPage />)\n\n    expect(getByText('✕ Something went wrong')).toBeInTheDocument()\n    expect(\n      getByText('This window will close automatically...'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render error message when result is invalid base64', () => {\n    Object.defineProperty(window, 'location', {\n      value: {\n        ...originalLocation,\n        href: 'http://localhost/azure-auth-callback?result=invalid-base64!!!',\n        search: '?result=invalid-base64!!!',\n      },\n      writable: true,\n    })\n\n    const { getByText } = render(<AzureAuthCallbackPage />)\n\n    expect(getByText('✕ Something went wrong')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-azure-auth/AzureAuthCallbackPage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const PageWrapper = styled(Col)`\n  height: 100vh;\n  width: 100vw;\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nexport const ContentWrapper = styled.div`\n  text-align: center;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-azure-auth/AzureAuthCallbackPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { Title, Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  AZURE_OAUTH_STORAGE_KEY,\n  AzureAuthStatus,\n} from 'apiSrc/modules/azure/constants'\nimport * as S from './AzureAuthCallbackPage.styles'\n\n/**\n * Minimal page component for the Azure OAuth callback route.\n * This runs in the popup window when redirected from the API.\n * It extracts the result from URL, stores it in localStorage, and closes.\n */\nconst AzureAuthCallbackPage = () => {\n  const [state, setState] = useState<AzureAuthStatus>(\n    AzureAuthStatus.Processing,\n  )\n\n  useEffect(() => {\n    const url = new URL(window.location.href)\n    const encodedResult = url.searchParams.get('result')\n\n    if (!encodedResult) {\n      // No result parameter - store error and show error state\n      localStorage.setItem(\n        AZURE_OAUTH_STORAGE_KEY,\n        JSON.stringify({\n          timestamp: Date.now(),\n          result: {\n            status: AzureAuthStatus.Failed,\n            error: 'Missing authentication result',\n          },\n        }),\n      )\n      setState(AzureAuthStatus.Failed)\n      setTimeout(() => window.close(), 2000)\n      return\n    }\n\n    try {\n      // Use decodeURIComponent + escape to handle non-ASCII characters (e.g., international names)\n      // This is the inverse of btoa(unescape(encodeURIComponent(...))) used in the callback template\n      const result = JSON.parse(\n        decodeURIComponent(escape(atob(decodeURIComponent(encodedResult)))),\n      )\n\n      // Store in localStorage for the main window to pick up\n      localStorage.setItem(\n        AZURE_OAUTH_STORAGE_KEY,\n        JSON.stringify({\n          timestamp: Date.now(),\n          result,\n        }),\n      )\n\n      setState(AzureAuthStatus.Succeed)\n      // Close this popup window\n      setTimeout(() => window.close(), 500)\n    } catch {\n      // Failed to parse result - store error for main window and show error state\n      localStorage.setItem(\n        AZURE_OAUTH_STORAGE_KEY,\n        JSON.stringify({\n          timestamp: Date.now(),\n          result: {\n            status: AzureAuthStatus.Failed,\n            error: 'Failed to process authentication response',\n          },\n        }),\n      )\n      setState(AzureAuthStatus.Failed)\n      setTimeout(() => window.close(), 2000)\n    }\n  }, [])\n\n  if (state === AzureAuthStatus.Processing) {\n    return (\n      <S.PageWrapper contentCentered grow={false}>\n        <S.ContentWrapper>\n          <Title size=\"L\">Processing...</Title>\n          <Spacer size=\"s\" />\n          <Text size=\"M\" color=\"subdued\">\n            Please wait\n          </Text>\n        </S.ContentWrapper>\n      </S.PageWrapper>\n    )\n  }\n\n  if (state === AzureAuthStatus.Failed) {\n    return (\n      <S.PageWrapper contentCentered grow={false}>\n        <S.ContentWrapper>\n          <Title size=\"L\" color=\"danger\">\n            ✕ Something went wrong\n          </Title>\n          <Spacer size=\"s\" />\n          <Text size=\"M\" color=\"subdued\">\n            This window will close automatically...\n          </Text>\n        </S.ContentWrapper>\n      </S.PageWrapper>\n    )\n  }\n\n  // Successfully received and relayed the OAuth result to the main application\n  // (regardless of whether the OAuth flow itself succeeded or failed)\n  return (\n    <S.PageWrapper contentCentered grow={false}>\n      <S.ContentWrapper>\n        <Title size=\"L\">Returning to RedisInsight...</Title>\n        <Spacer size=\"s\" />\n        <Text size=\"M\" color=\"subdued\">\n          This window will close automatically\n        </Text>\n      </S.ContentWrapper>\n    </S.PageWrapper>\n  )\n}\n\nexport default AzureAuthCallbackPage\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-azure-auth/GlobalAzureAuth.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\n\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  createMockedStore,\n  localStorageMock,\n} from 'uiSrc/utils/test-utils'\nimport { AzureAuthStatus } from 'apiSrc/modules/azure/constants'\nimport {\n  azureOAuthCallbackSuccess,\n  azureOAuthCallbackFailure,\n  setAzureLoginSource,\n} from 'uiSrc/slices/oauth/azure'\nimport { resetDataAzure } from 'uiSrc/slices/instances/azure'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nimport GlobalAzureAuth from './GlobalAzureAuth'\n\nconst AZURE_OAUTH_STORAGE_KEY = 'ri_azure_oauth_result'\n\n// Mock config to simulate non-Electron environment\njest.mock('uiSrc/config', () => ({\n  getConfig: jest.fn().mockReturnValue({\n    app: {\n      type: 'WEB',\n      env: 'production',\n    },\n  }),\n}))\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n  jest.clearAllMocks()\n})\n\nconst renderGlobalAzureAuth = () => render(<GlobalAzureAuth />, { store })\n\ndescribe('GlobalAzureAuth', () => {\n  it('should render without crashing', () => {\n    const { container } = renderGlobalAzureAuth()\n    // GlobalAzureAuth returns null, so container should be empty\n    expect(container.firstChild).toBeNull()\n  })\n\n  describe('localStorage polling', () => {\n    it('should set up interval on mount', () => {\n      const setIntervalSpy = jest.spyOn(window, 'setInterval')\n\n      renderGlobalAzureAuth()\n\n      expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 500)\n\n      setIntervalSpy.mockRestore()\n    })\n\n    it('should clean up interval on unmount', () => {\n      const clearIntervalSpy = jest.spyOn(window, 'clearInterval')\n\n      const { unmount } = renderGlobalAzureAuth()\n      unmount()\n\n      expect(clearIntervalSpy).toHaveBeenCalled()\n\n      clearIntervalSpy.mockRestore()\n    })\n\n    it('should dispatch success actions when valid result found in localStorage', () => {\n      const mockMsalAccount = {\n        id: faker.string.uuid(),\n        username: faker.internet.email(),\n        name: faker.person.fullName(),\n      }\n\n      const storedValue = JSON.stringify({\n        timestamp: Date.now(),\n        result: {\n          status: AzureAuthStatus.Succeed,\n          account: mockMsalAccount,\n        },\n      })\n\n      // Mock localStorage.getItem to return the stored value once, then null\n      localStorageMock.getItem.mockReturnValueOnce(storedValue)\n\n      renderGlobalAzureAuth()\n\n      const actions = store.getActions()\n      expect(actions).toContainEqual(resetDataAzure())\n      expect(actions).toContainEqual(azureOAuthCallbackSuccess(mockMsalAccount))\n      expect(actions).toContainEqual(setAzureLoginSource(null))\n\n      // Verify localStorage.removeItem was called\n      expect(localStorageMock.removeItem).toHaveBeenCalledWith(\n        AZURE_OAUTH_STORAGE_KEY,\n      )\n    })\n\n    it('should dispatch failure actions when error result found in localStorage', () => {\n      const errorMessage = faker.lorem.sentence()\n\n      const storedValue = JSON.stringify({\n        timestamp: Date.now(),\n        result: {\n          status: AzureAuthStatus.Failed,\n          error: errorMessage,\n        },\n      })\n\n      // Mock localStorage.getItem to return the stored value once\n      localStorageMock.getItem.mockReturnValueOnce(storedValue)\n\n      renderGlobalAzureAuth()\n\n      const actions = store.getActions()\n      // handleAzureOAuthFailure dispatches azureOAuthCallbackFailure first\n      expect(actions).toContainEqual(azureOAuthCallbackFailure(errorMessage))\n      // Then addErrorNotification is dispatched\n      expect(\n        actions.some(\n          (a: { type: string }) =>\n            a.type === addErrorNotification({} as any).type,\n        ),\n      ).toBe(true)\n\n      // Verify localStorage.removeItem was called\n      expect(localStorageMock.removeItem).toHaveBeenCalledWith(\n        AZURE_OAUTH_STORAGE_KEY,\n      )\n    })\n\n    it('should ignore stale results in localStorage', () => {\n      const mockMsalAccount = {\n        id: faker.string.uuid(),\n        username: faker.internet.email(),\n        name: faker.person.fullName(),\n      }\n\n      const storedValue = JSON.stringify({\n        timestamp: Date.now() - 60000, // 60 seconds ago (stale)\n        result: {\n          status: AzureAuthStatus.Succeed,\n          account: mockMsalAccount,\n        },\n      })\n\n      // Mock localStorage.getItem to return the stale value\n      localStorageMock.getItem.mockReturnValueOnce(storedValue)\n\n      renderGlobalAzureAuth()\n\n      const actions = store.getActions()\n      expect(actions.length).toBe(0)\n\n      // Stale result should be cleared\n      expect(localStorageMock.removeItem).toHaveBeenCalledWith(\n        AZURE_OAUTH_STORAGE_KEY,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-azure-auth/GlobalAzureAuth.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { getConfig } from 'uiSrc/config'\n\nimport {\n  azureAuthSourceSelector,\n  handleAzureOAuthSuccess,\n  handleAzureOAuthFailure,\n  setAzureLoginSource,\n} from 'uiSrc/slices/oauth/azure'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { AppDispatch } from 'uiSrc/slices/store'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  AzureAuthStatus,\n  AZURE_OAUTH_STORAGE_KEY,\n} from 'apiSrc/modules/azure/constants'\nconst STORAGE_POLL_INTERVAL = 500 // ms\nconst STORAGE_RESULT_MAX_AGE = 30000 // 30 seconds\n\ninterface AzureOAuthCallbackPayload {\n  status: string\n  account?: {\n    id: string\n    username: string\n    name?: string\n  }\n  error?: string\n}\n\nconst riConfig = getConfig()\nconst isElectron = riConfig.app.type === 'ELECTRON'\n\n/**\n * Global Azure Auth handler for web flow.\n * Polls localStorage for OAuth results from the popup window.\n * This component should be mounted in the main App for non-Electron builds.\n */\nconst GlobalAzureAuth = () => {\n  const dispatch = useDispatch<AppDispatch>()\n  const history = useHistory()\n  const source = useSelector(azureAuthSourceSelector)\n  const sourceRef = useRef(source)\n  sourceRef.current = source\n\n  // Process the OAuth callback payload\n  const processCallbackPayload = (payload: AzureOAuthCallbackPayload) => {\n    const { status, account, error } = payload\n\n    if (status === AzureAuthStatus.Succeed && account) {\n      const azureAccount = {\n        id: account.id,\n        username: account.username,\n        name: account.name,\n      }\n      const currentSource = sourceRef.current\n      dispatch(handleAzureOAuthSuccess(azureAccount))\n      dispatch(setAzureLoginSource(null))\n\n      // Only redirect to autodiscovery flow if login was initiated from there\n      if (currentSource === AzureLoginSource.Autodiscovery) {\n        history.push(Pages.azureSubscriptions)\n      }\n\n      // Show success notification only for token refresh\n      if (currentSource === AzureLoginSource.TokenRefresh) {\n        dispatch(\n          addMessageNotification({\n            title: 'Signed in to Azure',\n            message: 'You can now connect to your Azure database.',\n          }),\n        )\n      }\n      return\n    }\n\n    // Handle failure\n    const errorMessage = error || 'Azure authentication failed'\n    dispatch(handleAzureOAuthFailure(errorMessage))\n    dispatch(\n      addErrorNotification({\n        response: {\n          data: {\n            message: errorMessage,\n          },\n        },\n        persistent: true,\n      } as any),\n    )\n  }\n\n  useEffect(() => {\n    // Skip in Electron - uses IPC callbacks via ConfigAzureAuth\n    if (isElectron) {\n      return undefined\n    }\n\n    // Poll localStorage for OAuth results from popup\n    // Note: The popup window uses AzureAuthCallbackPage (routed at /azure-auth-callback)\n    // to store the result in localStorage, then this component picks it up.\n    const checkLocalStorage = () => {\n      try {\n        const stored = localStorage.getItem(AZURE_OAUTH_STORAGE_KEY)\n        if (!stored) return\n\n        const data = JSON.parse(stored)\n        const age = Date.now() - data.timestamp\n\n        // Only process recent results\n        if (age < STORAGE_RESULT_MAX_AGE && data.result) {\n          // Clear immediately to prevent duplicate processing\n          localStorage.removeItem(AZURE_OAUTH_STORAGE_KEY)\n          processCallbackPayload(data.result)\n        } else if (age >= STORAGE_RESULT_MAX_AGE) {\n          // Clean up stale results\n          localStorage.removeItem(AZURE_OAUTH_STORAGE_KEY)\n        }\n      } catch {\n        // Remove corrupt data to prevent infinite polling errors\n        localStorage.removeItem(AZURE_OAUTH_STORAGE_KEY)\n      }\n    }\n\n    const pollInterval = setInterval(checkLocalStorage, STORAGE_POLL_INTERVAL)\n\n    // Check immediately in case result is already there\n    checkLocalStorage()\n\n    return () => {\n      clearInterval(pollInterval)\n    }\n  }, [dispatch, history])\n\n  return null\n}\n\nexport default GlobalAzureAuth\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-azure-auth/index.ts",
    "content": "import GlobalAzureAuth from './GlobalAzureAuth'\nimport AzureAuthCallbackPage from './AzureAuthCallbackPage'\n\nexport { GlobalAzureAuth, AzureAuthCallbackPage }\nexport default GlobalAzureAuth\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-dialogs/GlobalDialogs.tsx",
    "content": "import React from 'react'\nimport {\n  FeatureFlagComponent,\n  OAuthSelectAccountDialog,\n  OAuthSelectPlan,\n  OAuthSsoDialog,\n} from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\n\nconst GlobalDialogs = () => (\n  <>\n    <FeatureFlagComponent name={FeatureFlags.cloudSso}>\n      <OAuthSelectAccountDialog />\n      <OAuthSelectPlan />\n      <OAuthSsoDialog />\n    </FeatureFlagComponent>\n  </>\n)\n\nexport default GlobalDialogs\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-dialogs/index.ts",
    "content": "import GlobalDialogs from './GlobalDialogs'\n\nexport default GlobalDialogs\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-subscriptions/CommonAppSubscription/CommonAppSubscription.spec.tsx",
    "content": "/* eslint-disable sonarjs/no-duplicate-string */\nimport { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport MockedSocket from 'socket.io-mock'\nimport socketIO from 'socket.io-client'\nimport { NotificationEvent } from 'uiSrc/constants/notifications'\nimport {\n  setLastReceivedNotification,\n  setNewNotificationReceived,\n} from 'uiSrc/slices/app/notifications'\nimport { setIsConnected } from 'uiSrc/slices/app/socket-connection'\nimport { NotificationType } from 'uiSrc/slices/interfaces'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags, SocketEvent } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RecommendationsSocketEvents } from 'uiSrc/constants/recommendations'\nimport { addUnreadRecommendations } from 'uiSrc/slices/recommendations/recommendations'\n\nimport { GlobalSubscriptions } from 'uiSrc/components'\nimport { NotificationsDto } from 'apiSrc/modules/notification/dto'\nimport CommonAppSubscription from './CommonAppSubscription'\n\nlet store: typeof mockedStore\nlet socket: typeof MockedSocket\nbeforeEach(() => {\n  cleanup()\n  socket = new MockedSocket()\n  socketIO.mockReturnValue(socket)\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('socket.io-client')\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: jest.fn().mockReturnValue(''),\n    connectionType: 'STANDALONE',\n    db: 0,\n  }),\n}))\n\ndescribe('CommonAppSubscription', () => {\n  it('should render', () => {\n    expect(render(<CommonAppSubscription />)).toBeTruthy()\n  })\n\n  it('should connect socket', () => {\n    const { unmount } = render(<CommonAppSubscription />)\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    const afterRenderActions = [setIsConnected(true)]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n\n  it('should not connect socket', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    const { unmount } = render(<GlobalSubscriptions />, {\n      store: mockStore(initialStoreState),\n    })\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    expect(store.getActions()).toEqual([])\n\n    unmount()\n  })\n\n  it('should set notifications', () => {\n    const { unmount } = render(<CommonAppSubscription />)\n\n    const mockData: NotificationsDto = {\n      notifications: [\n        {\n          type: NotificationType.Global,\n          timestamp: 123123125,\n          title: 'string',\n          body: 'string',\n          read: false,\n        },\n      ],\n      totalUnread: 1,\n    }\n\n    socket.on(NotificationEvent.Notification, (data: NotificationsDto) => {\n      expect(data).toEqual(mockData)\n    })\n\n    socket.socketClient.emit(NotificationEvent.Notification, mockData)\n\n    const afterRenderActions = [\n      setNewNotificationReceived(mockData as NotificationsDto),\n      setLastReceivedNotification(null),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n\n  it('should call proper actions after emit recommendation', async () => {\n    ;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({\n      id: '123',\n      connectionType: 'STANDALONE',\n      db: 0,\n    })\n\n    const { unmount } = render(<CommonAppSubscription />)\n    const mockData = {\n      totalUnread: 10,\n      recommendations: [{ databaseId: '123' }],\n    }\n    const mockData2 = {\n      totalUnread: 20,\n      recommendations: [{ databaseId: '123' }],\n    }\n\n    socket.socketClient.emit(\n      RecommendationsSocketEvents.Recommendation,\n      mockData,\n    )\n    socket.socketClient.emit(\n      RecommendationsSocketEvents.Recommendation,\n      mockData2,\n    )\n\n    const afterRenderActions = [\n      addUnreadRecommendations({\n        totalUnread: 10,\n        recommendations: [{ databaseId: '123' }],\n      }),\n      addUnreadRecommendations({\n        totalUnread: 20,\n        recommendations: [{ databaseId: '123' }],\n      }),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n\n  it('should ignore recommendations from non-connected instances', async () => {\n    ;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({\n      id: '456',\n      connectionType: 'STANDALONE',\n      db: 0,\n    })\n\n    const { unmount } = render(<CommonAppSubscription />)\n    const mockData = {\n      totalUnread: 10,\n      recommendations: [{ databaseId: '123' }],\n    }\n    const mockData2 = {\n      totalUnread: 20,\n      recommendations: [{ databaseId: '123' }],\n    }\n\n    socket.socketClient.emit(\n      RecommendationsSocketEvents.Recommendation,\n      mockData,\n    )\n    socket.socketClient.emit(\n      RecommendationsSocketEvents.Recommendation,\n      mockData2,\n    )\n\n    expect(store.getActions()).toEqual([])\n\n    unmount()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-subscriptions/CommonAppSubscription/CommonAppSubscription.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Socket } from 'socket.io-client'\n\nimport {\n  CloudJobEvents,\n  SocketEvent,\n  SocketFeaturesEvent,\n} from 'uiSrc/constants'\nimport { NotificationEvent } from 'uiSrc/constants/notifications'\nimport { setNewNotificationAction } from 'uiSrc/slices/app/notifications'\nimport { setIsConnected } from 'uiSrc/slices/app/socket-connection'\nimport { getSocketApiUrl, Nullable } from 'uiSrc/utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { addUnreadRecommendations } from 'uiSrc/slices/recommendations/recommendations'\nimport { RecommendationsSocketEvents } from 'uiSrc/constants/recommendations'\nimport { getFeatureFlagsSuccess } from 'uiSrc/slices/app/features'\nimport { oauthCloudJobSelector, setJob } from 'uiSrc/slices/oauth/cloud'\nimport { CloudJobName } from 'uiSrc/electron/constants'\nimport { appCsrfSelector } from 'uiSrc/slices/app/csrf'\nimport { useIoConnection } from 'uiSrc/services/hooks/useIoConnection'\nimport { CloudJobInfo } from 'apiSrc/modules/cloud/job/models'\n\nconst CommonAppSubscription = () => {\n  const { id: jobId = '' } = useSelector(oauthCloudJobSelector) ?? {}\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { token } = useSelector(appCsrfSelector)\n  const socketRef = useRef<Nullable<Socket>>(null)\n  const connectIo = useIoConnection(getSocketApiUrl(), {\n    forceNew: false,\n    token,\n    reconnection: true,\n  })\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (socketRef.current?.connected) {\n      return\n    }\n\n    socketRef.current = connectIo()\n\n    socketRef.current.on(SocketEvent.Connect, () => {\n      dispatch(setIsConnected(true))\n    })\n\n    socketRef.current.on(NotificationEvent.Notification, (data) => {\n      dispatch(setNewNotificationAction(data))\n    })\n\n    socketRef.current.on(SocketFeaturesEvent.Features, (data) => {\n      dispatch(getFeatureFlagsSuccess(data))\n\n      // or\n      // dispatch(fetchFeatureFlags())\n    })\n\n    socketRef.current.on(CloudJobEvents.Monitor, (data: CloudJobInfo) => {\n      const jobName = data.name as unknown\n\n      if (\n        jobName === CloudJobName.CreateFreeDatabase ||\n        jobName === CloudJobName.CreateFreeSubscriptionAndDatabase ||\n        jobName === CloudJobName.ImportFreeDatabase\n      ) {\n        dispatch(setJob(data))\n      }\n    })\n\n    // Catch disconnect\n    socketRef.current?.on(SocketEvent.Disconnect, () => {\n      unSubscribeFromRecommendations()\n    })\n\n    emitCloudJobMonitor(jobId)\n  }, [])\n\n  useEffect(() => {\n    emitCloudJobMonitor(jobId)\n  }, [jobId])\n\n  const unSubscribeFromRecommendations = () => {\n    const subscription = RecommendationsSocketEvents.Recommendation\n    const isListenerExist = !!socketRef.current?.listeners(subscription).length\n\n    if (isListenerExist) {\n      socketRef.current?.removeListener(subscription)\n    }\n  }\n\n  useEffect(() => {\n    if (!instanceId) return\n\n    unSubscribeFromRecommendations()\n\n    socketRef.current?.on(\n      RecommendationsSocketEvents.Recommendation,\n      (data) => {\n        const databaseId = data.recommendations[0]?.databaseId as string\n        if (databaseId === instanceId) {\n          dispatch(addUnreadRecommendations(data))\n        }\n      },\n    )\n  }, [instanceId])\n\n  const emitCloudJobMonitor = (jobId: string) => {\n    if (!jobId) return\n\n    socketRef.current?.emit(CloudJobEvents.Monitor, { jobId })\n  }\n\n  return null\n}\n\nexport default CommonAppSubscription\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-subscriptions/CommonAppSubscription/index.ts",
    "content": "import CommonAppSubscription from './CommonAppSubscription'\n\nexport default CommonAppSubscription\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-subscriptions/GlobalSubscriptions.tsx",
    "content": "import React from 'react'\nimport {\n  BulkActionsConfig,\n  FeatureFlagComponent,\n  MonitorConfig,\n  OAuthJobs,\n  PubSubConfig,\n} from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport CommonAppSubscription from './CommonAppSubscription'\n\nconst GlobalSubscriptions = () => (\n  <>\n    <FeatureFlagComponent name={FeatureFlags.envDependent} enabledByDefault>\n      <CommonAppSubscription />\n    </FeatureFlagComponent>\n    <MonitorConfig />\n    <PubSubConfig />\n    <FeatureFlagComponent name={FeatureFlags.envDependent}>\n      <BulkActionsConfig />\n    </FeatureFlagComponent>\n    <FeatureFlagComponent name={FeatureFlags.cloudSso}>\n      <OAuthJobs />\n    </FeatureFlagComponent>\n  </>\n)\n\nexport default GlobalSubscriptions\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-subscriptions/index.ts",
    "content": "import GlobalSubscriptions from './GlobalSubscriptions'\n\nexport default GlobalSubscriptions\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-url-handler/GlobalUrlHandler.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  act,\n  cleanup,\n  createMockedStore,\n  mockedStore,\n  render,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  appRedirectionSelector,\n  setFromUrl,\n  setUrlDbConnection,\n  setUrlHandlingInitialState,\n  setUrlProperties,\n} from 'uiSrc/slices/app/url-handling'\nimport { userSettingsSelector } from 'uiSrc/slices/user/user-settings'\nimport { addInfiniteNotification } from 'uiSrc/slices/app/notifications'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { Pages } from 'uiSrc/constants'\nimport { ADD_NEW, ADD_NEW_CA_CERT } from 'uiSrc/pages/home/constants'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\nimport { changeSidePanel } from 'uiSrc/slices/panels/sidePanels'\nimport { SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { setOnboarding } from 'uiSrc/slices/app/features'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport GlobalUrlHandler from './GlobalUrlHandler'\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    config: null,\n    isShowConsents: null,\n  }),\n}))\n\njest.mock('uiSrc/slices/app/url-handling', () => ({\n  ...jest.requireActual('uiSrc/slices/app/url-handling'),\n  appRedirectionSelector: jest.fn().mockReturnValue({\n    fromUrl: '',\n  }),\n}))\n\njest.mock('uiSrc/utils/routing', () => ({\n  ...jest.requireActual('uiSrc/slices/app/url-handling'),\n  getRedirectionPage: jest.fn().mockImplementation((page) => page),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n})\n\nconst fromUrl =\n  'redisinsight://databases/connect?redisUrl=redis://default:password@localhost:6379&databaseAlias=My Name&redirect=workbench?guidePath=/quick-guides/document/introduction.md&cloudBdbId=1232&subscriptionType=fixed&planMemoryLimit=30&memoryLimitMeasurementUnit=mb&free=true&target=_blank'\n\nconst renderGlobalUrlHandler = () => render(<GlobalUrlHandler />, { store })\n\ndescribe('GlobalUrlHandler', () => {\n  beforeEach(() => {\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ search: '', pathname: '' })\n  })\n\n  it('should render', () => {\n    expect(renderGlobalUrlHandler()).toBeTruthy()\n  })\n\n  it('should not call any actions by default', () => {\n    renderGlobalUrlHandler()\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should store fromUrl to the state and clear search', () => {\n    const search = `?from=${encodeURIComponent(fromUrl)}`\n\n    const replaceMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ replace: replaceMock })\n    reactRouterDom.useLocation = jest.fn().mockReturnValueOnce({ search })\n\n    renderGlobalUrlHandler()\n    expect(store.getActions()).toEqual([\n      setFromUrl(decodeURIComponent(fromUrl)),\n    ])\n\n    expect(replaceMock).toBeCalledWith({ search: '' })\n  })\n\n  it('should call proper actions to open page', async () => {\n    const fromUrl = 'redisinsight://open?redirect=/integrate'\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({ fromUrl })\n\n    await act(async () => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(fromUrl)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n\n    expect(store.getActions()).toEqual([\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n    ])\n\n    expect(pushMock).toBeCalledWith(Pages.rdi)\n  })\n\n  it('should call proper actions to open page and copilot, onboarding', async () => {\n    const fromUrl =\n      'redisinsight://open?redirect=/integrate&copilot=true&onboarding=true'\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({ fromUrl })\n\n    await act(async () => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(fromUrl)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n\n    const onboardingTotalSteps = Object.keys(ONBOARDING_FEATURES)?.length\n    expect(store.getActions()).toEqual([\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      changeSidePanel(SidePanels.AiAssistant),\n      setOnboarding({ currentStep: 0, totalSteps: onboardingTotalSteps }),\n    ])\n\n    expect(pushMock).toBeCalledWith(Pages.rdi)\n  })\n\n  it('should call proper actions only after consents popup is accepted', async () => {\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({\n      fromUrl,\n    })\n\n    renderGlobalUrlHandler()\n\n    const actionUrl = new URL(fromUrl)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n    urlProperties.cloudId = urlProperties.cloudBdbId\n    delete urlProperties.cloudBdbId\n\n    expect(store.getActions()).toEqual([\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      setUrlHandlingInitialState(),\n      addInfiniteNotification(INFINITE_MESSAGES.AUTO_CREATING_DATABASE()),\n    ])\n  })\n\n  it('should call proper actions only after consents popup is accepted and open form to add db without name', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n\n    const url = fromUrl.replace('default', '')\n\n    ;(appRedirectionSelector as jest.Mock).mockReturnValue({\n      fromUrl: url,\n    })\n\n    await act(() => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(url)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n    urlProperties.cloudId = urlProperties.cloudBdbId\n    delete urlProperties.cloudBdbId\n\n    const expectedActions = [\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      setUrlDbConnection({\n        action: UrlHandlingActions.Connect,\n        dbConnection: {\n          host: 'localhost',\n          name: 'My Name',\n          password: 'password',\n          port: 6379,\n          tls: false,\n          username: '',\n          caCert: undefined,\n          clientCert: undefined,\n        },\n      }),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n\n    expect(pushMock).toBeCalledWith(Pages.home)\n  })\n\n  it('should call proper actions only after consents popup is accepted and open form to add db with caCert', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n\n    const url = `${fromUrl}&requiredCaCert=true`\n\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({\n      fromUrl: url,\n    })\n\n    await act(() => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(url)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n    urlProperties.cloudId = urlProperties.cloudBdbId\n    delete urlProperties.cloudBdbId\n\n    const expectedActions = [\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      setUrlDbConnection({\n        action: UrlHandlingActions.Connect,\n        dbConnection: {\n          host: 'localhost',\n          name: 'My Name',\n          password: 'password',\n          port: 6379,\n          tls: true,\n          caCert: { id: ADD_NEW_CA_CERT },\n          username: 'default',\n        },\n      }),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n    expect(pushMock).toBeCalledWith(Pages.home)\n  })\n\n  it('should call proper actions only after consents popup is accepted and open form to add db with client cert', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n\n    const url = `${fromUrl}&requiredClientCert=true`\n\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({\n      fromUrl: url,\n    })\n\n    await act(() => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(url)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n    urlProperties.cloudId = urlProperties.cloudBdbId\n    delete urlProperties.cloudBdbId\n\n    const expectedActions = [\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      setUrlDbConnection({\n        action: UrlHandlingActions.Connect,\n        dbConnection: {\n          host: 'localhost',\n          name: 'My Name',\n          password: 'password',\n          port: 6379,\n          tls: true,\n          caCert: undefined,\n          clientCert: { id: ADD_NEW },\n          username: 'default',\n        },\n      }),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n    expect(pushMock).toBeCalledWith(Pages.home)\n  })\n\n  it('should call proper actions only after consents popup is accepted and open form to add db with tls certs', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n\n    const url = `${fromUrl}&requiredCaCert=true&requiredClientCert=true`\n\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({\n      fromUrl: url,\n    })\n\n    await act(() => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(url)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n    urlProperties.cloudId = urlProperties.cloudBdbId\n    delete urlProperties.cloudBdbId\n\n    const expectedActions = [\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      setUrlDbConnection({\n        action: UrlHandlingActions.Connect,\n        dbConnection: {\n          host: 'localhost',\n          name: 'My Name',\n          password: 'password',\n          port: 6379,\n          tls: true,\n          caCert: { id: ADD_NEW_CA_CERT },\n          clientCert: { id: ADD_NEW },\n          username: 'default',\n        },\n      }),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n    expect(pushMock).toBeCalledWith(Pages.home)\n  })\n\n  it('should call proper actions only after consents popup is accepted and open form to add db with tls', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    ;(userSettingsSelector as jest.Mock).mockReturnValueOnce({\n      config: {},\n      isShowConsents: false,\n    })\n\n    const url = `${fromUrl}&requiredTls=true`\n\n    ;(appRedirectionSelector as jest.Mock).mockReturnValueOnce({\n      fromUrl: url,\n    })\n\n    await act(() => {\n      renderGlobalUrlHandler()\n    })\n\n    const actionUrl = new URL(url)\n    const fromParams = new URLSearchParams(actionUrl.search)\n    // @ts-ignore\n    const urlProperties = Object.fromEntries(fromParams) || {}\n    urlProperties.cloudId = urlProperties.cloudBdbId\n    delete urlProperties.cloudBdbId\n\n    const expectedActions = [\n      setUrlProperties(urlProperties),\n      setFromUrl(null),\n      setUrlDbConnection({\n        action: UrlHandlingActions.Connect,\n        dbConnection: {\n          host: 'localhost',\n          name: 'My Name',\n          password: 'password',\n          port: 6379,\n          tls: true,\n          caCert: undefined,\n          clientCert: undefined,\n          username: 'default',\n        },\n      }),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n    expect(pushMock).toBeCalledWith(Pages.home)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-url-handler/GlobalUrlHandler.tsx",
    "content": "import { useHistory, useLocation } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useEffect } from 'react'\nimport { isNull, isNumber, every, values, pick, some } from 'lodash'\nimport { Pages } from 'uiSrc/constants'\nimport { ADD_NEW_CA_CERT, ADD_NEW } from 'uiSrc/pages/home/constants'\nimport {\n  appRedirectionSelector,\n  setFromUrl,\n  setReturnUrl,\n  setUrlDbConnection,\n  setUrlHandlingInitialState,\n  setUrlProperties,\n} from 'uiSrc/slices/app/url-handling'\nimport { userSettingsSelector } from 'uiSrc/slices/user/user-settings'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\nimport { autoCreateAndConnectToInstanceAction } from 'uiSrc/slices/instances/instances'\nimport { getRedirectionPage } from 'uiSrc/utils/routing'\nimport {\n  Nullable,\n  transformQueryParamsObject,\n  parseRedisUrl,\n  Maybe,\n} from 'uiSrc/utils'\nimport { changeSidePanel } from 'uiSrc/slices/panels/sidePanels'\nimport { SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { setOnboarding } from 'uiSrc/slices/app/features'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { localStorageService } from 'uiSrc/services'\nimport { AppStorageItem } from 'uiSrc/constants/storage'\n\nconst GlobalUrlHandler = () => {\n  const { fromUrl } = useSelector(appRedirectionSelector)\n  const { isShowConceptsPopup: isShowConsents, config } =\n    useSelector(userSettingsSelector)\n  const { search } = useLocation()\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const location = useLocation()\n\n  useEffect(() => {\n    // start handling only after closing consent popup\n    // including updated consents & from scratch\n    if (!fromUrl || isNull(isShowConsents) || isShowConsents || !config) return\n\n    try {\n      const actionUrl = new URL(fromUrl)\n      const fromParams = new URLSearchParams(actionUrl.search)\n      const pathname = actionUrl.hostname + actionUrl.pathname\n      const action = pathname?.replace(/^(\\/\\/?)|\\/$/g, '')\n\n      // @ts-ignore\n      const urlProperties = Object.fromEntries(fromParams) || {}\n\n      // rename cloudBdbId to cloudId\n      if (action === UrlHandlingActions.Connect) {\n        urlProperties.cloudId = urlProperties.cloudBdbId\n        delete urlProperties.cloudBdbId\n      }\n\n      dispatch(setUrlProperties(urlProperties))\n      dispatch(setFromUrl(null))\n\n      const transformedProperties = transformQueryParamsObject(urlProperties)\n      handleCommonProperties(transformedProperties)\n\n      if (action === UrlHandlingActions.Connect)\n        connectToDatabase(urlProperties)\n      if (action === UrlHandlingActions.Open) openPage(transformedProperties)\n    } catch (_e) {\n      //\n    }\n  }, [fromUrl, config, isShowConsents])\n\n  useEffect(() => {\n    try {\n      const params = new URLSearchParams(search)\n      const from = params.get('from')\n      const returnUrl = params.get('returnUrl')\n\n      if (from) {\n        dispatch(setFromUrl(from))\n        history.replace({\n          search: '',\n        })\n      }\n      if (returnUrl) {\n        localStorageService.set(AppStorageItem.returnUrl, returnUrl)\n        dispatch(setReturnUrl(returnUrl))\n        history.push(location.pathname)\n      }\n    } catch {\n      // do nothing\n    }\n  }, [search])\n\n  const redirectToPage = (\n    id: Maybe<string>,\n    redirectPage: Nullable<string>,\n    currentPathname?: string,\n  ) => {\n    if (redirectPage) {\n      const pageToRedirect = getRedirectionPage(\n        redirectPage,\n        id || undefined,\n        currentPathname,\n      )\n\n      if (pageToRedirect) {\n        history.push(pageToRedirect)\n        return\n      }\n    }\n\n    history.push(id ? Pages.browser(id) : Pages.home)\n  }\n\n  const connectToDatabase = (properties: Record<string, any>) => {\n    try {\n      const {\n        redisUrl,\n        databaseAlias,\n        redirect,\n        requiredTls,\n        requiredCaCert,\n        requiredClientCert,\n      } = properties\n\n      const cloudDetails = transformQueryParamsObject(\n        pick(properties, [\n          'cloudId',\n          'subscriptionType',\n          'planMemoryLimit',\n          'memoryLimitMeasurementUnit',\n          'free',\n        ]),\n      )\n\n      const url = parseRedisUrl(redisUrl)\n\n      if (!url) return\n\n      const obligatoryForAutoConnectFields = {\n        host: url.host,\n        port: url.port || 6379,\n        username: url.username,\n        password: url.password,\n      }\n\n      const tlsFields = {\n        requiredTls,\n        requiredCaCert,\n        requiredClientCert,\n      }\n\n      const isAllObligatoryProvided = every(\n        values(obligatoryForAutoConnectFields),\n        (value) => value || isNumber(value),\n      )\n      const isTlsProvided = some(values(tlsFields), (value) => value === 'true')\n\n      const db = {\n        ...obligatoryForAutoConnectFields,\n        name: databaseAlias || url.hostname || url.host,\n      } as any\n\n      if (isAllObligatoryProvided && !isTlsProvided) {\n        if (cloudDetails?.cloudId) {\n          db.cloudDetails = cloudDetails\n        }\n        dispatch(setUrlHandlingInitialState())\n        dispatch(\n          autoCreateAndConnectToInstanceAction(db, (id) =>\n            redirectToPage(id, redirect),\n          ),\n        )\n\n        return\n      }\n\n      dispatch(\n        setUrlDbConnection({\n          action: UrlHandlingActions.Connect,\n          dbConnection: {\n            ...db,\n            // set tls with new cert option\n            tls: isTlsProvided,\n            caCert:\n              requiredCaCert === 'true' ? { id: ADD_NEW_CA_CERT } : undefined,\n            clientCert:\n              requiredClientCert === 'true' ? { id: ADD_NEW } : undefined,\n          },\n        }),\n      )\n\n      history.push(Pages.home)\n    } catch (e) {\n      //\n    }\n  }\n\n  const handleCommonProperties = (properties: Record<string, any>) => {\n    if (properties.copilot) {\n      dispatch(changeSidePanel(SidePanels.AiAssistant))\n    }\n\n    if (properties.onboarding) {\n      const totalSteps = Object.keys(ONBOARDING_FEATURES || {}).length\n      dispatch(setOnboarding({ currentStep: 0, totalSteps }))\n    }\n  }\n\n  const openPage = (properties: Record<string, any>) => {\n    redirectToPage(undefined, properties.redirect || '/_', location.pathname)\n  }\n\n  return null\n}\n\nexport default GlobalUrlHandler\n"
  },
  {
    "path": "redisinsight/ui/src/components/global-url-handler/index.ts",
    "content": "import GlobalUrlHandler from './GlobalUrlHandler'\n\nexport default GlobalUrlHandler\n"
  },
  {
    "path": "redisinsight/ui/src/components/group-badge/GroupBadge.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { CommandGroup, KeyTypes } from 'uiSrc/constants'\nimport GroupBadge from './GroupBadge'\n\nconst meta: Meta<typeof GroupBadge> = {\n  component: GroupBadge,\n  args: {\n    type: KeyTypes.String,\n  },\n  decorators: [\n    (Story) => (\n      <div style={{ padding: '20px' }}>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const KeyTypeBadges: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>\n      <GroupBadge type={KeyTypes.String} />\n      <GroupBadge type={KeyTypes.Hash} />\n      <GroupBadge type={KeyTypes.List} />\n      <GroupBadge type={KeyTypes.Set} />\n      <GroupBadge type={KeyTypes.ZSet} />\n      <GroupBadge type={KeyTypes.Stream} />\n      <GroupBadge type={KeyTypes.JSON} />\n    </div>\n  ),\n}\n\nexport const CommandGroupBadges: Story = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>\n      <GroupBadge type={CommandGroup.Generic} />\n      <GroupBadge type={CommandGroup.Bitmap} />\n      <GroupBadge type={CommandGroup.Cluster} />\n      <GroupBadge type={CommandGroup.Connection} />\n      <GroupBadge type={CommandGroup.Geo} />\n      <GroupBadge type={CommandGroup.PubSub} />\n      <GroupBadge type={CommandGroup.Scripting} />\n      <GroupBadge type={CommandGroup.Transactions} />\n      <GroupBadge type={CommandGroup.Server} />\n      <GroupBadge type={CommandGroup.SortedSet} />\n      <GroupBadge type={CommandGroup.HyperLogLog} />\n    </div>\n  ),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/group-badge/GroupBadge.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\n\ninterface StyledGroupBadgeProps {\n  $color: string\n  $withDeleteBtn?: boolean\n  $compressed?: boolean\n}\nexport const DeleteButton = styled(IconButton)`\n  margin-left: ${({ theme }) => theme.core.space.space050};\n  width: ${({ theme }) => theme.core.space.space150};\n  height: ${({ theme }) => theme.core.space.space150};\n  color: #ffffff;\n  &:hover {\n    // disable default hover appearance\n    appearance: none;\n    background-color: transparent;\n    text-decoration: none;\n    color: #ffffff;\n  }\n  & svg {\n    width: 10px;\n    height: 10px;\n  }\n`\n\nconst compressedStyle = css`\n  padding-left: 0;\n  padding-right: 0;\n\n  ${DeleteButton} {\n    margin-left: 0;\n  }\n`\nexport const StyledGroupBadge = styled(RiBadge)<StyledGroupBadgeProps>`\n  min-width: ${({ theme }) => theme.core.space.space150};\n  & > p {\n    display: flex;\n    align-items: center;\n  }\n  ${({ $color }) =>\n    `\n      background-color: ${$color};\n    `}\n\n  ${({ $withDeleteBtn }) => $withDeleteBtn && 'padding-right: 0 !important;'}\n  ${({ $compressed }) => $compressed && compressedStyle}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/group-badge/GroupBadge.tsx",
    "content": "import React from 'react'\n\nimport { CommandGroup, GROUP_TYPES_COLORS, KeyTypes } from 'uiSrc/constants'\nimport { getGroupTypeDisplay } from 'uiSrc/utils'\n\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { DeleteButton, StyledGroupBadge } from './GroupBadge.styles'\n\nexport interface Props {\n  type: KeyTypes | CommandGroup | string\n  name?: string\n  className?: string\n  compressed?: boolean\n  onDelete?: (type: string) => void\n}\n\n// TODO: Replace GroupBadge with RiChip component and implement new custom colors for different key types as part of the Redis UI migration.\nconst GroupBadge = ({\n  type,\n  name = '',\n  className = '',\n  onDelete,\n  compressed,\n}: Props) => {\n  // @ts-ignore\n  const backgroundColor = GROUP_TYPES_COLORS[type] ?? 'var(--defaultTypeColor)'\n  return (\n    <StyledGroupBadge\n      $withDeleteBtn={!!onDelete}\n      $compressed={!!compressed}\n      variant=\"light\"\n      $color={backgroundColor}\n      className={className}\n      title={undefined}\n      data-testid={`badge-${type}_${name}`}\n    >\n      {!compressed && (\n        <Text\n          color=\"#ffffff\"\n          className=\"text-uppercase\"\n          variant=\"semiBold\"\n          size=\"S\"\n          component=\"span\"\n        >\n          {getGroupTypeDisplay(type)}\n        </Text>\n      )}\n      {onDelete && (\n        <DeleteButton\n          size=\"XS\"\n          icon={CancelSlimIcon}\n          aria-label=\"Delete\"\n          onClick={() => onDelete(type)}\n          data-testid={`${type}-delete-btn`}\n        />\n      )}\n    </StyledGroupBadge>\n  )\n}\n\nexport default GroupBadge\n"
  },
  {
    "path": "redisinsight/ui/src/components/group-badge/index.ts",
    "content": "import GroupBadge from './GroupBadge'\n\nexport default GroupBadge\n"
  },
  {
    "path": "redisinsight/ui/src/components/group-badge/styles.module.scss",
    "content": ".badgeWrapper {\n\n  :global(.euiBadge__text) {\n    display: flex;\n    align-items: center;\n  }\n}\n\n.deleteIcon {\n  margin-left: 4px;\n  width: 16px !important;\n  height: 18px !important;\n  color: var(--htmlColor) !important;\n\n  :global(.euiIcon) {\n    width: 10px !important;\n    height: 10px !important;\n  }\n}\n\n.withDeleteBtn {\n  padding-right: 0 !important;\n}\n\n.compressed {\n  padding-left: 0 !important;\n  padding-right: 0 !important;\n\n  .deleteIcon {\n    margin-left: 0;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx",
    "content": "import { fireEvent } from '@testing-library/react'\nimport React from 'react'\nimport {\n  act,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport HighlightedFeature from './HighlightedFeature'\n\nconst Content = () => <div data-testid=\"some-feature\" />\ndescribe('HighlightedFeature', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <HighlightedFeature>\n          <Content />\n        </HighlightedFeature>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render content', () => {\n    render(\n      <HighlightedFeature>\n        <Content />\n      </HighlightedFeature>,\n    )\n\n    expect(screen.getByTestId('some-feature')).toBeInTheDocument()\n  })\n\n  it('should render dot highlighting', () => {\n    render(\n      <HighlightedFeature type=\"plain\" isHighlight>\n        <Content />\n      </HighlightedFeature>,\n    )\n\n    expect(screen.getByTestId('some-feature')).toBeInTheDocument()\n    expect(screen.getByTestId('dot-highlighting')).toBeInTheDocument()\n  })\n\n  it('should render badge highlighting', async () => {\n    render(\n      <HighlightedFeature\n        type=\"tooltip-badge\"\n        content=\"content\"\n        title=\"title\"\n        isHighlight\n      >\n        <Content />\n      </HighlightedFeature>,\n    )\n\n    expect(screen.getByTestId('some-feature')).toBeInTheDocument()\n    expect(screen.getByTestId('badge-highlighting')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-badge-highlighting-inner'))\n    })\n\n    await waitForRiTooltipVisible()\n\n    expect(\n      screen.queryByTestId('tooltip-badge-highlighting'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('tooltip-badge-highlighting'),\n    ).toHaveTextContent('title')\n    expect(\n      screen.queryByTestId('tooltip-badge-highlighting'),\n    ).toHaveTextContent('content')\n  })\n\n  it('should not render highlighting', () => {\n    render(\n      <HighlightedFeature type=\"plain\" isHighlight={false}>\n        <Content />\n      </HighlightedFeature>,\n    )\n\n    expect(screen.getByTestId('some-feature')).toBeInTheDocument()\n    expect(screen.queryByTestId('dot-highlighting')).not.toBeInTheDocument()\n  })\n\n  it('should render tooltip highlighting', async () => {\n    render(\n      <HighlightedFeature\n        type=\"tooltip\"\n        content=\"content\"\n        title=\"title\"\n        isHighlight\n      >\n        <Content />\n      </HighlightedFeature>,\n    )\n\n    expect(screen.getByTestId('some-feature')).toBeInTheDocument()\n    expect(screen.getByTestId('dot-highlighting')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('tooltip-highlighting-inner'))\n    })\n\n    await waitForRiTooltipVisible()\n\n    expect(screen.queryByTestId('tooltip-highlighting')).toBeInTheDocument()\n    expect(screen.queryByTestId('tooltip-highlighting')).toHaveTextContent(\n      'title',\n    )\n    expect(screen.queryByTestId('tooltip-highlighting')).toHaveTextContent(\n      'content',\n    )\n  })\n\n  it('should call onClick', () => {\n    const onClick = jest.fn()\n    render(\n      <HighlightedFeature\n        type=\"plain\"\n        onClick={onClick}\n        isHighlight\n        dataTestPostfix=\"feature\"\n      >\n        <Content />\n      </HighlightedFeature>,\n    )\n\n    fireEvent.click(screen.getByTestId('feature-highlighted-feature'))\n\n    expect(onClick).toBeCalled()\n  })\n\n  it('should not render second tooltip', async () => {\n    render(\n      <HighlightedFeature\n        type=\"tooltip\"\n        content=\"content\"\n        title=\"title\"\n        isHighlight\n        hideFirstChild\n      >\n        <RiTooltip title=\"PrevTooltipTitle\" data-testid=\"no-render-tooltip\">\n          <Content />\n        </RiTooltip>\n      </HighlightedFeature>,\n    )\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('some-feature'))\n    })\n\n    await waitForRiTooltipVisible()\n\n    expect(screen.queryByTestId('tooltip-highlighting')).toBeInTheDocument()\n    expect(screen.queryByTestId('no-render-tooltip')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx",
    "content": "import { isString } from 'lodash'\nimport { ToolTipPositions } from '@elastic/eui/src/components/tool_tip/tool_tip'\nimport cx from 'classnames'\nimport React from 'react'\nimport { FeaturesHighlightingType } from 'uiSrc/constants/featuresHighlighting'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  isHighlight?: boolean\n  children: React.ReactElement<any, any> | string\n  title?: string | React.ReactElement\n  content?: string | React.ReactElement\n  type?: FeaturesHighlightingType\n  transformOnHover?: boolean\n  onClick?: () => void\n  wrapperClassName?: string\n  dotClassName?: string\n  tooltipPosition?: ToolTipPositions\n  hideFirstChild?: boolean\n  dataTestPostfix?: string\n}\nconst HighlightedFeature = (props: Props) => {\n  const {\n    isHighlight,\n    children,\n    title,\n    content,\n    type = 'plain',\n    transformOnHover,\n    onClick,\n    wrapperClassName,\n    dotClassName,\n    tooltipPosition = 'bottom',\n    hideFirstChild,\n    dataTestPostfix = '',\n  } = props\n\n  const innerContent =\n    hideFirstChild && !isString(children) ? children.props.children : children\n\n  const BadgeHighlighting = () => (\n    <>\n      {innerContent}\n      <RiBadge\n        label=\"New!\"\n        className={styles.badge}\n        data-testid=\"badge-highlighting\"\n      />\n    </>\n  )\n\n  const DotHighlighting = () => (\n    <>\n      {innerContent}\n      <span\n        className={cx(styles.dot, dotClassName)}\n        data-testid=\"dot-highlighting\"\n      />\n    </>\n  )\n\n  const TooltipHighlighting = () => (\n    <RiTooltip\n      title={title}\n      content={content}\n      position={tooltipPosition}\n      data-testid=\"tooltip-highlighting\"\n    >\n      <div data-testid=\"tooltip-highlighting-inner\">\n        <DotHighlighting />\n      </div>\n    </RiTooltip>\n  )\n\n  const TooltipBadgeHighlighting = () => (\n    <RiTooltip\n      title={title}\n      content={content}\n      position={tooltipPosition}\n      data-testid=\"tooltip-badge-highlighting\"\n    >\n      <div\n        className={styles.badgeContainer}\n        data-testid=\"tooltip-badge-highlighting-inner\"\n      >\n        <BadgeHighlighting />\n      </div>\n    </RiTooltip>\n  )\n\n  if (type === 'dialog') {\n    return !isHighlight ? null : <>{children}</>\n  }\n\n  if (!isHighlight) return <>{children}</>\n\n  return (\n    <div\n      className={cx(styles.wrapper, wrapperClassName, {\n        'transform-on-hover': transformOnHover,\n      })}\n      onClick={() => onClick?.()}\n      role=\"presentation\"\n      data-testid={`feature-highlighted-${dataTestPostfix}`}\n    >\n      {type === 'plain' && <DotHighlighting />}\n      {type === 'tooltip' && <TooltipHighlighting />}\n      {type === 'popover' && <DotHighlighting />}\n      {type === 'tooltip-badge' && <TooltipBadgeHighlighting />}\n    </div>\n  )\n}\n\nexport default HighlightedFeature\n"
  },
  {
    "path": "redisinsight/ui/src/components/hightlighted-feature/styles.module.scss",
    "content": ".wrapper {\n  position: relative;\n\n  &:global(.transform-on-hover) {\n    &:hover {\n      .dot {\n        transform: translateY(-1px);\n      }\n    }\n  }\n\n  .dot {\n    position: absolute;\n    background: var(--highlightDotColor);\n    top: -4px;\n    right: -4px;\n    z-index: 1;\n\n    width: 12px;\n    height: 12px;\n    border-radius: 50%;\n\n    transition: transform 250ms ease-in-out;\n  }\n\n  .badgeContainer {\n    display: flex;\n    align-items: center;\n  }\n\n  .badge {\n    font-size: 8px !important;\n    line-height: 12px !important;\n    background-color: var(--recommendationLiveBorderColor) !important;\n    border: 1px solid var(--triggerIconActiveColor) !important;\n    color: #FFF7EA !important;\n    border-radius: 2px !important;\n    padding: 0 4px;\n    margin-left: 8px;\n\n    transition: transform 250ms ease-in-out;\n    pointer-events: none;\n\n    :global(.euiBadge__content) {\n      min-height: 12px !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/home-tabs/HomeTabs.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  cleanup,\n  mockedStore,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\n\nimport { Pages } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport HomeTabs from './HomeTabs'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    redisDataIntegration: {\n      flag: true,\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('HomeTabs', () => {\n  it('should render', () => {\n    expect(render(<HomeTabs />)).toBeTruthy()\n  })\n\n  it('should show database instances tab active', async () => {\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.home })\n\n    render(<HomeTabs />)\n\n    const tabs = await screen.findAllByRole('tab')\n\n    const databasesTab = tabs.find((tab) =>\n      tab.getAttribute('id')?.endsWith('trigger-databases'),\n    )\n\n    expect(databasesTab).toHaveAttribute('data-state', 'active')\n  })\n\n  it('should show rdi instances tab active', async () => {\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdi })\n\n    render(<HomeTabs />)\n\n    const tabs = await screen.findAllByRole('tab')\n\n    const rdiTab = tabs.find((tab) =>\n      tab.getAttribute('id')?.endsWith('trigger-rdi-instances'),\n    )\n\n    expect(rdiTab).toHaveAttribute('data-state', 'active')\n  })\n\n  it('should call proper history push', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.home })\n\n    render(<HomeTabs />)\n\n    fireEvent.mouseDown(screen.getByText('Redis Data Integration'))\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.rdi)\n  })\n\n  it('should send proper telemetry', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.home })\n\n    render(<HomeTabs />)\n\n    fireEvent.mouseDown(screen.getByText('Redis Data Integration'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSTANCES_TAB_CHANGED,\n      eventData: {\n        tab: 'Redis Data Integration',\n      },\n    })\n  })\n\n  it('should not render rdi tab', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      rdi: {\n        flag: false,\n      },\n    })\n\n    render(<HomeTabs />)\n\n    expect(screen.queryByText('Redis Data Integration')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/home-tabs/HomeTabs.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { useHistory, useLocation } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport Tabs from 'uiSrc/components/base/layout/tabs'\nimport { tabs } from './constants'\n\nconst HomeTabs = () => {\n  const history = useHistory()\n  const { pathname } = useLocation()\n  const featureFlags = useSelector(appFeatureFlagsFeaturesSelector)\n\n  const filteredTabs = useMemo(\n    () =>\n      tabs.filter(\n        (tab) => !tab.featureFlag || featureFlags?.[tab.featureFlag]?.flag,\n      ),\n    [featureFlags],\n  )\n\n  const activeTab =\n    filteredTabs.find((tab) => tab.path.startsWith(pathname)) ?? filteredTabs[0]\n\n  const onSelectedTabChanged = (newValue: string) => {\n    const tab =\n      filteredTabs.find((tab) => tab.value === newValue) ?? filteredTabs[0]\n\n    sendEventTelemetry({\n      event: TelemetryEvent.INSTANCES_TAB_CHANGED,\n      eventData: {\n        tab: tab.label,\n      },\n    })\n\n    history.push(tab.path)\n  }\n\n  return (\n    <Tabs\n      tabs={filteredTabs}\n      value={activeTab.value}\n      onChange={onSelectedTabChanged}\n      data-testid=\"home-tabs\"\n    />\n  )\n}\n\nexport default React.memo(HomeTabs)\n"
  },
  {
    "path": "redisinsight/ui/src/components/home-tabs/constants.ts",
    "content": "import { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { TabInfo } from 'uiSrc/components/base/layout/tabs'\n\ntype HomeTab = TabInfo & {\n  path: string\n  featureFlag?: FeatureFlags\n}\n\nconst tabs: HomeTab[] = [\n  {\n    value: 'databases',\n    label: 'Redis Databases',\n    content: null,\n    path: Pages.home,\n  },\n  {\n    value: 'rdi-instances',\n    label: 'Redis Data Integration',\n    content: null,\n    path: Pages.rdi,\n    featureFlag: FeatureFlags.rdi,\n  },\n]\n\nexport { tabs }\n"
  },
  {
    "path": "redisinsight/ui/src/components/home-tabs/index.ts",
    "content": "import HomeTabs from './HomeTabs'\n\nexport default HomeTabs\n"
  },
  {
    "path": "redisinsight/ui/src/components/hooks/useAzureAuth.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport { cleanup, mockedStore, renderHook, act } from 'uiSrc/utils/test-utils'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport {\n  azureAuthSelector,\n  AzureOAuthPrompt,\n  AzureOAuthRedirectType,\n  initiateAzureLoginAction,\n} from 'uiSrc/slices/oauth/azure'\nimport { AzureAccountFactory } from 'uiSrc/mocks/factories/cloud/AzureAccount.factory'\n\nimport { useAzureAuth } from './useAzureAuth'\n\n// Mock config to simulate Electron environment\njest.mock('uiSrc/config', () => ({\n  getConfig: jest.fn().mockReturnValue({\n    app: {\n      type: 'ELECTRON',\n      env: 'production',\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/oauth/azure', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/azure'),\n  azureAuthSelector: jest.fn().mockReturnValue({\n    loading: false,\n    account: null,\n    error: '',\n  }),\n  initiateAzureLoginAction: jest.fn().mockReturnValue({ type: 'mock-action' }),\n}))\n\nconst mockedAzureAuthSelector = azureAuthSelector as jest.Mock\nconst mockedInitiateAzureLoginAction = initiateAzureLoginAction as jest.Mock\n\nlet store: typeof mockedStore\n\nconst mockAccount = AzureAccountFactory.build()\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  jest.clearAllMocks()\n  mockedAzureAuthSelector.mockReturnValue({\n    loading: false,\n    account: null,\n    error: '',\n  })\n})\n\ndescribe('useAzureAuth', () => {\n  it('should return loading, account, and error from selector', () => {\n    mockedAzureAuthSelector.mockReturnValue({\n      loading: true,\n      account: mockAccount,\n      error: 'test error',\n    })\n\n    const { result } = renderHook(() => useAzureAuth())\n\n    expect(result.current.loading).toBe(true)\n    expect(result.current.account).toEqual(mockAccount)\n    expect(result.current.error).toBe('test error')\n  })\n\n  describe('initiateLogin', () => {\n    it('should dispatch initiateAzureLoginAction with default source', () => {\n      const { result } = renderHook(() => useAzureAuth())\n\n      act(() => {\n        result.current.initiateLogin()\n      })\n\n      expect(mockedInitiateAzureLoginAction).toHaveBeenCalledWith({\n        source: AzureLoginSource.Autodiscovery,\n        onSuccess: expect.any(Function),\n        prompt: AzureOAuthPrompt.SelectAccount,\n        redirectType: AzureOAuthRedirectType.Deeplink,\n      })\n    })\n\n    it('should dispatch initiateAzureLoginAction with provided source', () => {\n      const { result } = renderHook(() => useAzureAuth())\n\n      act(() => {\n        result.current.initiateLogin(AzureLoginSource.TokenRefresh)\n      })\n\n      expect(mockedInitiateAzureLoginAction).toHaveBeenCalledWith({\n        source: AzureLoginSource.TokenRefresh,\n        onSuccess: expect.any(Function),\n        prompt: AzureOAuthPrompt.SelectAccount,\n        redirectType: AzureOAuthRedirectType.Deeplink,\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/hooks/useAzureAuth.ts",
    "content": "import { useCallback, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { getConfig } from 'uiSrc/config'\n\nimport {\n  azureAuthSelector,\n  AzureOAuthPrompt,\n  AzureOAuthRedirectType,\n  initiateAzureLoginAction,\n} from 'uiSrc/slices/oauth/azure'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport { AppDispatch } from 'uiSrc/slices/store'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nconst riConfig = getConfig()\nconst isElectron = riConfig.app.type === 'ELECTRON'\n\nconst AZURE_LOCALHOST_ERROR_MESSAGE =\n  'Azure authentication requires accessing RedisInsight via localhost. ' +\n  'Please use http://localhost:PORT instead of IP addresses or custom domains.'\n\n// Popup window dimensions for OAuth\nconst POPUP_WIDTH = 500\nconst POPUP_HEIGHT = 700\n\nexport const useAzureAuth = () => {\n  const dispatch = useDispatch<AppDispatch>()\n  const { loading, account, error } = useSelector(azureAuthSelector)\n  const popupRef = useRef<Window | null>(null)\n\n  const openAuthUrl = useCallback((url: string) => {\n    if (isElectron) {\n      // Electron: open in system browser, deeplink will handle callback\n      window.open(url, '_blank')\n    } else {\n      // Web: open popup window, localStorage polling will handle callback\n      const left = window.screenX + (window.innerWidth - POPUP_WIDTH) / 2\n      const top = window.screenY + (window.innerHeight - POPUP_HEIGHT) / 2\n      const features = `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=yes`\n\n      // Close any existing popup\n      if (popupRef.current && !popupRef.current.closed) {\n        popupRef.current.close()\n      }\n\n      popupRef.current = window.open(url, 'azureOAuthPopup', features)\n    }\n  }, [])\n\n  const initiateLogin = useCallback(\n    (source: AzureLoginSource = AzureLoginSource.Autodiscovery) => {\n      // In web mode, Azure OAuth only works when accessed via localhost\n      // due to Azure's redirect URI restrictions for public client apps\n      if (!isElectron && window.location.hostname !== 'localhost') {\n        dispatch(\n          addErrorNotification({\n            response: {\n              data: {\n                message: AZURE_LOCALHOST_ERROR_MESSAGE,\n              },\n            },\n          } as any),\n        )\n        return\n      }\n\n      const redirectType = isElectron\n        ? AzureOAuthRedirectType.Deeplink\n        : AzureOAuthRedirectType.Web\n\n      dispatch(\n        initiateAzureLoginAction({\n          source,\n          onSuccess: openAuthUrl,\n          prompt: AzureOAuthPrompt.SelectAccount,\n          redirectType,\n        }),\n      )\n    },\n    [dispatch, openAuthUrl],\n  )\n\n  return {\n    loading,\n    account,\n    error,\n    initiateLogin,\n  }\n}\n\nexport default useAzureAuth\n"
  },
  {
    "path": "redisinsight/ui/src/components/hooks/useConnectionType.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { ConnectionType, Instance } from 'uiSrc/slices/interfaces'\nimport { cleanup, mockedStore, renderHook } from 'uiSrc/utils/test-utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { useConnectionType } from 'uiSrc/components/hooks/useConnectionType'\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    connectedInstance: {},\n  }),\n}))\n\nlet store: typeof mockedStore\ntype ConnType = 'cluster' | 'standalone' | 'sentinel'\nlet instances: Record<ConnType, Instance>\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  instances = {\n    standalone: {\n      id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff',\n      host: 'localhost',\n      port: 6379,\n      name: 'localhost',\n      connectionType: ConnectionType.Standalone,\n      modules: [],\n      version: '6.2.6',\n    },\n    cluster: {\n      id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4',\n      host: 'localhost',\n      port: 12000,\n      name: 'oea123123',\n      connectionType: ConnectionType.Cluster,\n      modules: [],\n      version: '6.2.6',\n    },\n    sentinel: {\n      id: 'b83a3932-e95f-4f09-9d8a-55079f400186',\n      version: '6.2.6',\n      host: 'localhost',\n      port: 5005,\n      connectionType: ConnectionType.Sentinel,\n      modules: [],\n    },\n  }\n})\n\ndescribe('useConnectionType', () => {\n  it.each([\n    [ConnectionType.Cluster, 'cluster' as ConnType],\n    [ConnectionType.Standalone, 'standalone' as ConnType],\n    [ConnectionType.Sentinel, 'sentinel' as ConnType],\n  ])(\n    'should return %i when forceStandalone is undefined',\n    async (expected, type) => {\n      const instance = { ...instances[type] }\n      ;(connectedInstanceSelector as jest.Mock).mockReturnValue(instance)\n\n      const { result } = renderHook(useConnectionType)\n      const connectionType = result.current\n      expect(connectionType).toEqual(expected)\n    },\n  )\n\n  it.each([\n    [ConnectionType.Cluster, 'cluster' as ConnType],\n    [ConnectionType.Standalone, 'standalone' as ConnType],\n    [ConnectionType.Sentinel, 'sentinel' as ConnType],\n  ])(\n    'should return %i when forceStandalone is false',\n    async (expected, type) => {\n      const instance = {\n        ...instances[type],\n        forceStandalone: false,\n      }\n      ;(connectedInstanceSelector as jest.Mock).mockReturnValue(instance)\n\n      const { result } = renderHook(useConnectionType)\n      const connectionType = result.current\n      expect(connectionType).toEqual(expected)\n    },\n  )\n  it.each([\n    [ConnectionType.Standalone, 'cluster' as ConnType],\n    [ConnectionType.Standalone, 'standalone' as ConnType],\n    [ConnectionType.Sentinel, 'sentinel' as ConnType],\n  ])('should return STANDALONE ', async (expected, type) => {\n    const instance = {\n      ...instances[type],\n      forceStandalone: true,\n    }\n    ;(connectedInstanceSelector as jest.Mock).mockReturnValue(instance)\n\n    const { result } = renderHook(useConnectionType)\n    const connectionType = result.current\n    expect(connectionType).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/hooks/useConnectionType.ts",
    "content": "import { useSelector } from 'react-redux'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nexport const useConnectionType = () => {\n  const { connectionType, forceStandalone } = useSelector(\n    connectedInstanceSelector,\n  )\n\n  if (forceStandalone && connectionType !== ConnectionType.Sentinel) {\n    return ConnectionType.Standalone\n  }\n  return connectionType\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/import-file-modal/ImportFileModal.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport ImportFileModal, { Props } from './ImportFileModal'\n\nconst mockProps: Props<Object> = {\n  onClose: jest.fn(),\n  onFileChange: jest.fn(),\n  onSubmit: jest.fn(),\n  title: 'title',\n  submitResults: <div>submitResults</div>,\n  loading: false,\n  data: null,\n  error: '',\n  errorMessage: '',\n  invalidMessage: '',\n  isInvalid: false,\n  isSubmitDisabled: false,\n}\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\ndescribe('ImportFileModal', () => {\n  it('should render', () => {\n    expect(render(<ImportFileModal {...mockProps} />)).toBeTruthy()\n  })\n\n  it('should call onClose', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    fireEvent.click(screen.getByTestId('cancel-btn'))\n\n    expect(mockProps.onClose).toBeCalled()\n  })\n\n  it('should call onFileChange', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n      target: { files: ['file'] },\n    })\n\n    expect(mockProps.onFileChange).toBeCalled()\n  })\n\n  it('should call onSubmit', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    fireEvent.click(screen.getByTestId('submit-btn'))\n\n    expect(mockProps.onSubmit).toBeCalled()\n  })\n\n  // Skipping until the title issue in the modal is fixed\n  it.skip('should show title before submit', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    expect(screen.getByTestId('import-file-modal-title')).toHaveTextContent(\n      'title',\n    )\n  })\n\n  // Skipping until the title issue in the modal is fixed\n  it.skip('should show custom results title after submit', () => {\n    render(\n      <ImportFileModal {...mockProps} data={{}} resultsTitle=\"resultsTitle\" />,\n    )\n\n    expect(screen.getByTestId('import-file-modal-title')).toHaveTextContent(\n      'resultsTitle',\n    )\n  })\n\n  // Skipping until the title issue in the modal is fixed\n  it.skip('should show default results title after submit', () => {\n    render(<ImportFileModal {...mockProps} data={{}} />)\n\n    expect(screen.getByTestId('import-file-modal-title')).toHaveTextContent(\n      'Import Results',\n    )\n  })\n\n  it('should show submit results after submit', () => {\n    render(<ImportFileModal {...mockProps} data={{}} />)\n\n    expect(screen.getByText('submitResults')).toBeTruthy()\n  })\n\n  it('should render loading indicator', () => {\n    render(<ImportFileModal {...mockProps} loading />)\n    expect(screen.getByTestId('file-loading-indicator')).toBeInTheDocument()\n  })\n\n  it('should render error message', () => {\n    render(<ImportFileModal {...mockProps} error=\"error\" />)\n\n    expect(screen.getByTestId('result-failed')).toHaveTextContent('error')\n  })\n\n  it('should render invalid message', () => {\n    render(\n      <ImportFileModal\n        {...mockProps}\n        isInvalid\n        invalidMessage=\"invalidMessage\"\n      />,\n    )\n\n    expect(screen.getByTestId('input-file-error-msg')).toHaveTextContent(\n      'invalidMessage',\n    )\n  })\n\n  it('submit btn should be disabled without file', () => {\n    render(<ImportFileModal {...mockProps} isSubmitDisabled />)\n\n    expect(screen.getByTestId('submit-btn')).toBeDisabled()\n  })\n\n  it('submit btn should be enabled with file', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n      target: { files: ['file'] },\n    })\n\n    expect(screen.getByTestId('submit-btn')).not.toBeDisabled()\n  })\n\n  it('should render submit button with custom text', () => {\n    render(<ImportFileModal {...mockProps} submitBtnText=\"custom text\" />)\n\n    expect(screen.getByTestId('submit-btn')).toHaveTextContent('custom text')\n  })\n\n  it('should render submit button with default text', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    expect(screen.getByTestId('submit-btn')).toHaveTextContent('Import')\n  })\n\n  it('should render warning message', () => {\n    render(\n      <ImportFileModal\n        {...mockProps}\n        warning={<div data-testid=\"warning\">warning</div>}\n      />,\n    )\n\n    expect(screen.getByTestId('warning')).toHaveTextContent('warning')\n  })\n\n  it('should contain the upload warning text', () => {\n    render(<ImportFileModal {...mockProps} />)\n\n    expect(\n      screen.getByText(\n        'Use files only from trusted authors to avoid automatic execution of malicious code.',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/import-file-modal/ImportFileModal.tsx",
    "content": "import React from 'react'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { RiFilePicker, UploadWarning } from 'uiSrc/components'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { Loader, Modal } from 'uiSrc/components/base/display'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport styles from './styles.module.scss'\n\nexport interface Props<T> {\n  onClose: () => void\n  onFileChange: (files: FileList | null) => void\n  onSubmit: () => void\n  title: string\n  resultsTitle?: string\n  submitResults: JSX.Element\n  loading: boolean\n  data: Nullable<T>\n  warning?: JSX.Element | null\n  error?: string\n  errorMessage?: string\n  invalidMessage?: string\n  isInvalid: boolean\n  isSubmitDisabled: boolean\n  submitBtnText?: string\n  acceptedFileExtension?: string\n}\n\nconst ImportFileModal = <T,>({\n  onClose,\n  onFileChange,\n  onSubmit,\n  title,\n  resultsTitle,\n  submitResults,\n  loading,\n  data,\n  warning,\n  error,\n  errorMessage,\n  invalidMessage,\n  isInvalid,\n  isSubmitDisabled,\n  submitBtnText,\n  acceptedFileExtension,\n}: Props<T>) => {\n  const isShowForm = !loading && !data && !error\n  return (\n    <Modal.Compose open>\n      <Modal.Content.Compose persistent>\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={onClose}\n          data-testid=\"import-file-modal-close-btn\"\n        />\n\n        <Modal.Content.Header.Compose>\n          <Modal.Content.Header.Title data-testid=\"import-file-modal-title\">\n            {!data && !error ? title : resultsTitle || 'Import Results'}\n          </Modal.Content.Header.Title>\n        </Modal.Content.Header.Compose>\n\n        <Modal.Content.Body\n          width=\"610px\"\n          content={\n            <Col gap=\"l\">\n              {warning && <FlexItem>{warning}</FlexItem>}\n              <FlexItem>\n                {isShowForm && (\n                  <>\n                    <RiFilePicker\n                      id=\"import-file-modal-filepicker\"\n                      initialPromptText=\"Select or drag and drop a file\"\n                      className={styles.fileDrop}\n                      isInvalid={isInvalid}\n                      onChange={onFileChange}\n                      display=\"large\"\n                      accept={acceptedFileExtension}\n                      data-testid=\"import-file-modal-filepicker\"\n                      aria-label=\"Select or drag and drop file\"\n                    />\n                    {isInvalid && (\n                      <ColorText\n                        color=\"danger\"\n                        className={styles.errorFileMsg}\n                        data-testid=\"input-file-error-msg\"\n                      >\n                        {invalidMessage}\n                      </ColorText>\n                    )}\n                  </>\n                )}\n                {loading && (\n                  <div\n                    className={styles.loading}\n                    data-testid=\"file-loading-indicator\"\n                  >\n                    <Loader size=\"xl\" />\n                    <Text color=\"subdued\" style={{ marginTop: 12 }}>\n                      Uploading...\n                    </Text>\n                  </div>\n                )}\n                {error && (\n                  <div className={styles.result} data-testid=\"result-failed\">\n                    <RiIcon\n                      type=\"ToastCancelIcon\"\n                      size=\"xxl\"\n                      color=\"danger500\"\n                    />\n                    <Text style={{ marginTop: 16 }}>{errorMessage}</Text>\n                    <Text>{error}</Text>\n                  </div>\n                )}\n              </FlexItem>\n              {isShowForm && (\n                <FlexItem grow>\n                  <UploadWarning />\n                </FlexItem>\n              )}\n            </Col>\n          }\n        />\n\n        {data && (\n          <Modal.Content.Body\n            content={submitResults}\n            data-testid=\"result-succeeded\"\n          />\n        )}\n        <Modal.Content.Footer.Compose>\n          <Modal.Content.Footer.Group>\n            {isShowForm && (\n              <Row gap=\"m\" justify=\"end\">\n                <SecondaryButton\n                  size=\"l\"\n                  onClick={onClose}\n                  data-testid=\"cancel-btn\"\n                >\n                  Cancel\n                </SecondaryButton>\n                <PrimaryButton\n                  size=\"l\"\n                  onClick={onSubmit}\n                  disabled={isSubmitDisabled}\n                  data-testid=\"submit-btn\"\n                >\n                  {submitBtnText || 'Import'}\n                </PrimaryButton>\n              </Row>\n            )}\n            {data && <PrimaryButton onClick={onClose}>OK</PrimaryButton>}\n          </Modal.Content.Footer.Group>\n        </Modal.Content.Footer.Compose>\n      </Modal.Content.Compose>\n    </Modal.Compose>\n  )\n}\n\nexport default ImportFileModal\n"
  },
  {
    "path": "redisinsight/ui/src/components/import-file-modal/index.ts",
    "content": "import ImportFileModal from './ImportFileModal'\n\nexport default ImportFileModal\n"
  },
  {
    "path": "redisinsight/ui/src/components/import-file-modal/styles.module.scss",
    "content": ".result {\n   height: fit-content;\n   overflow: hidden;\n}\n\n.errorFileMsg {\n  margin-top: 10px;\n  font-size: 12px;\n}\n\n.fileDrop {\n  width: 300px;\n  margin: auto;\n\n  :global {\n    .RI-File-Picker__showDrop .RI-File-Picker__prompt, .RI-File-Picker__input:focus + .RI-File-Picker__prompt {\n      background-color: var(--euiColorEmptyShade);\n    }\n\n    .RI-File-Picker__prompt {\n      background-color: var(--euiColorEmptyShade);\n      height: 140px;\n      border-radius: 4px;\n      box-shadow: none;\n      border: 1px dashed var(--controlsBorderColor);\n      color: var(--htmlColor);\n    }\n\n    .RI-File-Picker {\n      width: 400px;\n    }\n\n    .RI-File-Picker__clearButton {\n      margin-top: 4px;\n    }\n  }\n}\n\n.loading, .result {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  margin-top: 20px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/index.ts",
    "content": "import NavigationMenu from './navigation-menu/NavigationMenu'\nimport AppNavigation from './navigation-menu/app-navigation/AppNavigation'\nimport PageHeader from './page-header/PageHeader'\nimport GroupBadge from './group-badge/GroupBadge'\nimport Notifications from './notifications/Notifications'\nimport { DatabaseListModules } from './database-list-modules/DatabaseListModules'\nimport DatabaseListOptions from './database-list-options/DatabaseListOptions'\nimport DatabaseOverview from './database-overview/DatabaseOverview'\nimport InputFieldSentinel from './input-field-sentinel/InputFieldSentinel'\nimport ContentEditable from './ContentEditable'\nimport Config from './config'\nimport SettingItem from './settings-item/SettingItem'\nimport {\n  ConsentsSettings,\n  ConsentsSettingsPopup,\n  ConsentsPrivacy,\n  ConsentsNotifications,\n} from './consents-settings'\nimport KeyboardShortcut from './keyboard-shortcut/KeyboardShortcut'\nimport ShortcutsFlyout from './shortcuts-flyout/ShortcutsFlyout'\nimport MonitorConfig from './monitor-config'\nimport PubSubConfig from './pub-sub-config'\nimport GlobalSubscriptions from './global-subscriptions'\nimport MonitorWrapper from './monitor'\nimport PagePlaceholder from './page-placeholder'\nimport BulkActionsConfig from './bulk-actions-config'\nimport OnboardingTour from './onboarding-tour'\nimport CodeBlock from './code-block'\nimport FeatureFlagComponent from './feature-flag-component'\nimport AutoRefresh from './auto-refresh'\nimport ConfirmationPopover from './confirmation-popover'\nimport { ModuleNotLoaded, FeatureNotAvailable } from './messages'\nimport RdiInstanceHeader from './rdi-instance-header'\nimport {\n  RecommendationBody,\n  RecommendationBadges,\n  RecommendationBadgesLegend,\n  RecommendationCopyComponent,\n  RecommendationVoting,\n} from './recommendation'\nimport { FormatedDate } from './formated-date'\nimport { UploadWarning } from './upload-warning'\nimport FormDialog from './form-dialog'\n\nexport { FullScreen } from './full-screen'\n\nexport * from './oauth'\nexport * from './base'\n\nexport {\n  NavigationMenu,\n  AppNavigation,\n  PageHeader,\n  GroupBadge,\n  Notifications,\n  DatabaseListModules,\n  DatabaseListOptions,\n  DatabaseOverview,\n  InputFieldSentinel,\n  Config,\n  ContentEditable,\n  ConsentsSettings,\n  ConsentsSettingsPopup,\n  ConsentsPrivacy,\n  ConsentsNotifications,\n  SettingItem,\n  KeyboardShortcut,\n  MonitorConfig,\n  PubSubConfig,\n  GlobalSubscriptions,\n  MonitorWrapper,\n  ShortcutsFlyout,\n  PagePlaceholder,\n  BulkActionsConfig,\n  OnboardingTour,\n  CodeBlock,\n  RecommendationVoting,\n  RecommendationCopyComponent,\n  FeatureFlagComponent,\n  ModuleNotLoaded,\n  FeatureNotAvailable,\n  AutoRefresh,\n  ConfirmationPopover,\n  RdiInstanceHeader,\n  RecommendationBody,\n  RecommendationBadges,\n  RecommendationBadgesLegend,\n  FormatedDate,\n  UploadWarning,\n  FormDialog,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/init/AppInit.spec.tsx",
    "content": "import React from 'react'\nimport { screen } from '@testing-library/react'\nimport { cloneDeep, set } from 'lodash'\nimport { initialStateDefault, mockStore, render } from 'uiSrc/utils/test-utils'\nimport {\n  appInitSelector,\n  STATUS_FAIL,\n  STATUS_INITIAL,\n  STATUS_LOADING,\n  STATUS_SUCCESS,\n  initializeAppAction,\n} from 'uiSrc/slices/app/init'\nimport AppInit from 'uiSrc/components/init/AppInit'\n\njest.mock('uiSrc/slices/app/init', () => ({\n  ...jest.requireActual('uiSrc/slices/app/init'),\n  reducers: {\n    ...jest.requireActual('uiSrc/slices/app/init').reducers,\n  },\n  appInitSelector: jest\n    .fn()\n    .mockReturnValue(jest.requireActual('uiSrc/slices/app/init').initialState),\n  initializeAppAction: jest.fn(),\n}))\n\nconst initialState = set(cloneDeep(initialStateDefault), 'app.init', {\n  status: STATUS_INITIAL,\n  error: 'Test error',\n})\nconst mockedStore = mockStore(initialState)\n\nbeforeEach(() => {\n  jest.resetAllMocks()\n  mockedStore.clearActions()\n})\n\ndescribe('App Init', () => {\n  it('should call initializeAppAction when status is \"initial', () => {\n    ;(appInitSelector as jest.Mock).mockReturnValue({\n      ...initialState,\n      status: STATUS_INITIAL,\n      error: undefined,\n    })\n    const initializeAppActionMock = jest.fn()\n    ;(initializeAppAction as jest.Mock).mockImplementation(\n      () => initializeAppActionMock,\n    )\n    const onSuccess = jest.fn()\n    const onFail = jest.fn()\n    render(\n      <AppInit onSuccess={onSuccess} onFail={onFail}>\n        <div>children</div>\n      </AppInit>,\n      {\n        store: mockedStore,\n      },\n    )\n    expect(initializeAppAction).toHaveBeenCalled()\n  })\n\n  it('should render page placeholder when status is \"loading\"', () => {\n    ;(appInitSelector as jest.Mock).mockReturnValue({\n      ...initialState,\n      status: STATUS_LOADING,\n    })\n    const initializeAppActionMock = jest.fn()\n    ;(initializeAppAction as jest.Mock).mockImplementation(\n      () => initializeAppActionMock,\n    )\n    render(\n      <AppInit>\n        <div>children</div>\n      </AppInit>,\n      {\n        store: mockedStore,\n      },\n    )\n    expect(screen.getByTestId('suspense-loader')).toBeInTheDocument()\n  })\n\n  it('should render error page with generic error message, when status is \"fail\"', () => {\n    ;(appInitSelector as jest.Mock).mockReturnValue({\n      ...initialState,\n      status: STATUS_FAIL,\n    })\n    const initializeAppActionMock = jest.fn()\n    ;(initializeAppAction as jest.Mock).mockImplementation(\n      () => initializeAppActionMock,\n    )\n    render(\n      <AppInit>\n        <div>children</div>\n      </AppInit>,\n      {\n        store: mockedStore,\n      },\n    )\n    expect(screen.getByTestId('connectivity-error-message')).toBeInTheDocument()\n    expect(\n      screen.getByText(\n        'An unexpected server error has occurred. Please retry the request.',\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should render children when status is \"success\"', () => {\n    ;(appInitSelector as jest.Mock).mockReturnValue({\n      ...initialState,\n      status: STATUS_SUCCESS,\n    })\n    const initializeAppActionMock = jest.fn()\n    ;(initializeAppAction as jest.Mock).mockImplementation(\n      () => initializeAppActionMock,\n    )\n    render(\n      <AppInit>\n        <div>children</div>\n      </AppInit>,\n      {\n        store: mockedStore,\n      },\n    )\n    expect(screen.getByText('children')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/init/AppInit.tsx",
    "content": "import React, { ReactElement, useCallback, useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  appInitSelector,\n  initializeAppAction,\n  STATUS_FAIL,\n  STATUS_SUCCESS,\n} from 'uiSrc/slices/app/init'\nimport { removePagePlaceholder } from 'uiSrc/utils'\nimport ConnectivityError from 'uiSrc/components/connectivity-error/ConnectivityError'\nimport SuspenseLoader from 'uiSrc/components/main-router/components/SuspenseLoader'\n\ntype Props = {\n  children: ReactElement\n  onSuccess?: () => void\n  onFail?: () => void\n}\n\nconst AppInit = ({ children, onSuccess, onFail }: Props) => {\n  const dispatch = useDispatch()\n  const { status } = useSelector(appInitSelector)\n\n  const initApp = useCallback(\n    () => dispatch(initializeAppAction(onSuccess, onFail)),\n    [onSuccess, onFail],\n  )\n\n  useEffect(() => {\n    initApp()\n  }, [])\n\n  if (status === STATUS_FAIL) {\n    removePagePlaceholder()\n    return (\n      <ConnectivityError\n        isLoading={false}\n        onRetry={initApp}\n        error=\"An unexpected server error has occurred. Please retry the request.\"\n      />\n    )\n  }\n\n  return status === STATUS_SUCCESS ? children : <SuspenseLoader />\n}\n\nexport default AppInit\n"
  },
  {
    "path": "redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport { validateScoreNumber } from 'uiSrc/utils'\nimport InlineItemEditor, { Props } from './InlineItemEditor'\n\nconst mockedProps = mock<Props>()\nexport const INLINE_ITEM_EDITOR = 'inline-item-editor'\n\ndescribe('InlineItemEditor', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <InlineItemEditor {...instance(mockedProps)} onDecline={jest.fn()} />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should change value properly', () => {\n    render(\n      <InlineItemEditor {...instance(mockedProps)} onDecline={jest.fn()} />,\n    )\n    fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n      target: { value: 'val' },\n    })\n    expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('val')\n  })\n\n  it('should change value properly with validation', () => {\n    render(\n      <InlineItemEditor\n        {...instance(mockedProps)}\n        onDecline={jest.fn()}\n        validation={validateScoreNumber}\n      />,\n    )\n    fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n      target: { value: 'val123' },\n    })\n    expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue(\n      validateScoreNumber('val123'),\n    )\n  })\n\n  it('should be focused properly', () => {\n    render(\n      <InlineItemEditor {...instance(mockedProps)} onDecline={jest.fn()} />,\n    )\n    expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveFocus()\n  })\n\n  describe('approveByValidation', () => {\n    it('should not render popover after click on Apply btn if approveByValidation return \"true\" in the props and onApply should be called', () => {\n      const approveByValidationMock = jest.fn().mockReturnValue(true)\n      const onApplyMock = jest.fn().mockReturnValue(false)\n      const { queryByTestId } = render(\n        <InlineItemEditor\n          {...instance(mockedProps)}\n          onApply={onApplyMock}\n          onDecline={jest.fn()}\n          approveByValidation={approveByValidationMock}\n        />,\n      )\n\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: 'val123' },\n      })\n\n      fireEvent.click(screen.getByTestId(/apply-btn/))\n      expect(queryByTestId('approve-popover')).not.toBeInTheDocument()\n      expect(onApplyMock).toBeCalled()\n    })\n\n    it('should render popover after click on Apply btn if approveByValidation return \"false\" in the props and onApply should not be called ', () => {\n      const approveByValidationMock = jest.fn().mockReturnValue(false)\n      const onApplyMock = jest.fn().mockReturnValue(false)\n      const { queryByTestId } = render(\n        <InlineItemEditor\n          {...instance(mockedProps)}\n          onApply={onApplyMock}\n          onDecline={jest.fn()}\n          approveByValidation={approveByValidationMock}\n        />,\n      )\n\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: 'val123' },\n      })\n\n      fireEvent.click(screen.getByTestId(/apply-btn/))\n      expect(queryByTestId('confirm-popover')).toBeInTheDocument()\n      expect(onApplyMock).not.toBeCalled()\n    })\n\n    it('should render popover after click on Apply btn if approveByValidation return \"false\" in the props and onApply should be called after click on Save btn', () => {\n      const approveByValidationMock = jest.fn().mockReturnValue(false)\n      const onApplyMock = jest.fn().mockReturnValue(false)\n      const { queryByTestId } = render(\n        <InlineItemEditor\n          {...instance(mockedProps)}\n          onApply={onApplyMock}\n          onDecline={jest.fn()}\n          approveByValidation={approveByValidationMock}\n        />,\n      )\n\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: 'val123' },\n      })\n\n      fireEvent.click(screen.getByTestId(/apply-btn/))\n      expect(queryByTestId('confirm-popover')).toBeInTheDocument()\n      expect(onApplyMock).not.toBeCalled()\n\n      fireEvent.click(screen.getByTestId(/save-btn/))\n      expect(onApplyMock).toBeCalled()\n    })\n\n    it('should not call onApply if form is invalid', () => {\n      const onApplyMock = jest.fn().mockReturnValue(false)\n      render(\n        <InlineItemEditor\n          {...instance(mockedProps)}\n          isLoading\n          onApply={onApplyMock}\n          onDecline={jest.fn()}\n        />,\n      )\n\n      expect(screen.getByTestId('apply-btn')).toBeDisabled()\n\n      fireEvent.submit(screen.getByTestId(INLINE_ITEM_EDITOR))\n\n      expect(onApplyMock).not.toBeCalled()\n    })\n\n    it('should call onApply if form is valid', () => {\n      const onApplyMock = jest.fn().mockReturnValue(false)\n      render(\n        <InlineItemEditor\n          {...instance(mockedProps)}\n          onApply={onApplyMock}\n          onDecline={jest.fn()}\n        />,\n      )\n\n      fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n        target: { value: 'val123' },\n      })\n\n      expect(screen.getByTestId('apply-btn')).not.toBeDisabled()\n\n      fireEvent.submit(screen.getByTestId(INLINE_ITEM_EDITOR))\n\n      expect(onApplyMock).toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.styles.tsx",
    "content": "import React from 'react'\nimport styled, { css } from 'styled-components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { Props } from 'uiSrc/components/inline-item-editor/InlineItemEditor'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon, CheckThinIcon } from 'uiSrc/components/base/icons'\nimport { TextInput } from '../base/inputs'\n\ninterface ContainerProps {\n  className?: string\n  children?: React.ReactNode\n}\n\nconst RefStyledContainer = React.forwardRef(\n  (\n    { className, children }: ContainerProps,\n    ref?: React.Ref<HTMLDivElement>,\n  ) => (\n    <div className={className} ref={ref}>\n      {children}\n    </div>\n  ),\n)\n\nexport const StyledContainer = styled(RefStyledContainer)`\n  max-width: 100%;\n\n  & .euiFormControlLayout {\n    max-width: 100% !important;\n  }\n\n  & .tooltip {\n    display: inline-block;\n  }\n`\n\nexport const IIEContainer = React.forwardRef<\n  HTMLDivElement,\n  {\n    children?: React.ReactNode\n  }\n>(({ children, ...rest }, ref) => (\n  <StyledContainer ref={ref} {...rest}>\n    {children}\n  </StyledContainer>\n))\n\ntype ActionsContainerProps = React.ComponentProps<typeof Row> & {\n  $position?: Props['controlsPosition']\n  $design?: Props['controlsDesign']\n  $width?: string\n  $height?: string\n}\n\nexport const DeclineButton = styled(IconButton).attrs({\n  icon: CancelSlimIcon,\n  'aria-label': 'Cancel editing',\n})`\n  &:hover {\n    color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.text.danger500};\n  }\n`\n\nexport const ApplyButton = styled(IconButton).attrs({\n  icon: CheckThinIcon,\n  color: 'primary',\n  'aria-label': 'Apply',\n})`\n  vertical-align: top;\n  &:hover:not([class*='isDisabled']) {\n    color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.text.neutral500};\n  }\n`\n\nconst positions = {\n  bottom: css`\n    top: 100%;\n    right: 0;\n    border-radius: 0 0 10px 10px;\n    box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n  `,\n  top: css`\n    bottom: 100%;\n    right: 0;\n    border-radius: 10px 10px 0 0;\n    box-shadow: 0 -3px 3px var(--controlsBoxShadowColor);\n  `,\n  right: css`\n    top: 0;\n    left: 100%;\n    height: 100%;\n    border-radius: 0 10px 10px 0;\n    box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n    align-items: center;\n  `,\n  left: css`\n    top: 0;\n    right: 100%;\n    border-radius: 10px 0 0 10px;\n    box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n  `,\n  inside: css`\n    top: calc(100% - 35px);\n    right: 7px;\n    border-radius: 0 10px 10px 0;\n    box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n  `,\n}\n\nconst designs = {\n  default: css``,\n  separate: css`\n    border-radius: 0;\n    box-shadow: none;\n    background-color: inherit !important;\n    width: 60px;\n    z-index: 4;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    .popoverWrapper,\n    ${DeclineButton}, ${ApplyButton} {\n      height: 24px !important;\n      width: 24px !important;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      flex-shrink: 0;\n    }\n\n    svg {\n      width: 18px !important;\n      height: 18px !important;\n    }\n  `,\n}\n\nexport const ActionsWrapper = styled(FlexItem)<{\n  $size?: { width: string; height: string }\n}>`\n  width: ${({ $size }) => $size?.width ?? '24px'} !important;\n  height: ${({ $size }) => $size?.height ?? '24px'} !important;\n`\n\nexport const ActionsContainer = styled(Row)<ActionsContainerProps>`\n  position: absolute;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.primary200};\n  width: ${({ $width }) => $width || '80px'};\n  height: ${({ $height }) => $height || '33px'};\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n  align-items: center;\n  z-index: 3;\n  ${({ $position }) => positions[$position || 'inside']}\n  ${({ $design }) => designs[$design || 'default']}\n`\n\nexport const StyledTextInput = styled(TextInput)<{\n  $width?: string\n  $height?: string\n}>`\n  width: ${({ $width }) => $width || 'auto'};\n  height: ${({ $height }) => $height || 'auto'};\n  max-height: ${({ $height }) => $height || 'auto'};\n  min-height: ${({ $height }) => $height || 'auto'};\n  padding: 0;\n\n  // Target the actual input element inside\n  input {\n    width: 100%;\n    height: ${({ $height }) => $height || 'auto'};\n    padding: 0 5px;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx",
    "content": "import React, { Ref, useEffect, useRef, useState } from 'react'\nimport cx from 'classnames'\n\nimport { useTheme } from '@redis-ui/styles'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { WindowEvent } from 'uiSrc/components/base/utils/WindowEvent'\nimport { FocusTrap } from 'uiSrc/components/base/utils/FocusTrap'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\nimport { DestructiveButton } from 'uiSrc/components/base/forms/buttons'\nimport ConfirmationPopover from 'uiSrc/components/confirmation-popover'\n\nimport {\n  ActionsContainer,\n  ActionsWrapper,\n  ApplyButton,\n  DeclineButton,\n  IIEContainer,\n  StyledTextInput,\n} from './InlineItemEditor.styles'\n\nimport styles from './styles.module.scss'\n\ntype Positions = 'top' | 'bottom' | 'left' | 'right' | 'inside'\ntype Design = 'default' | 'separate'\ntype InputVariant = 'outline' | 'underline'\n\nexport interface Props {\n  onDecline: (event?: React.MouseEvent<HTMLElement>) => void\n  onApply: (value: string, event: React.MouseEvent) => void\n  onChange?: (value: string) => void\n  fieldName?: string\n  initialValue?: string\n  placeholder?: string\n  controlsPosition?: Positions\n  controlsDesign?: Design\n  maxLength?: number\n  expandable?: boolean\n  isLoading?: boolean\n  isDisabled?: boolean\n  isInvalid?: boolean\n  disableEmpty?: boolean\n  disableByValidation?: (value: string) => boolean\n  children?: React.ReactElement\n  validation?: (value: string) => string\n  getError?: (\n    value: string,\n  ) => { title: string; content: string | React.ReactNode } | undefined\n  declineOnUnmount?: boolean\n  iconSize?: 'S' | 'M' | 'L'\n  viewChildrenMode?: boolean\n  autoComplete?: string\n  controlsClassName?: string\n  disabledTooltipText?: { title: string; content: string | React.ReactNode }\n  preventOutsideClick?: boolean\n  disableFocusTrap?: boolean\n  approveByValidation?: (value: string) => boolean\n  approveText?: { title: string; text: string }\n  textFiledClassName?: string\n  variant?: InputVariant\n  styles?: {\n    inputContainer?: {\n      width?: string\n      height?: string\n    }\n    input?: {\n      width?: string\n      height?: string\n    }\n    actionsContainer?: {\n      width?: string\n      height?: string\n    }\n  }\n}\n\nconst InlineItemEditor = (props: Props) => {\n  const {\n    initialValue = '',\n    placeholder = '',\n    controlsPosition = 'bottom',\n    controlsDesign = 'default',\n    onDecline,\n    onApply,\n    onChange,\n    fieldName,\n    maxLength,\n    children,\n    expandable,\n    isLoading,\n    disableEmpty,\n    disableByValidation,\n    validation,\n    getError,\n    declineOnUnmount = true,\n    viewChildrenMode,\n    iconSize,\n    isDisabled,\n    autoComplete = 'off',\n    controlsClassName,\n    disabledTooltipText,\n    preventOutsideClick = false,\n    disableFocusTrap = false,\n    approveByValidation,\n    approveText,\n    textFiledClassName,\n    variant,\n    styles: customStyles,\n  } = props\n  const containerEl: Ref<HTMLDivElement> = useRef(null)\n  const [value, setValue] = useState<string>(initialValue)\n  const [isError, setIsError] = useState<boolean>(false)\n  const [isShowApprovePopover, setIsShowApprovePopover] = useState(false)\n  const theme = useTheme()\n\n  const size = theme.components.iconButton.sizes[iconSize ?? 'M']\n\n  const inputRef: Ref<HTMLInputElement> = useRef(null)\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        declineOnUnmount && onDecline()\n      },\n    [],\n  )\n\n  useEffect(() => {\n    setTimeout(() => {\n      inputRef?.current?.focus()\n      inputRef?.current?.select()\n    }, 100)\n  }, [])\n\n  const handleChangeValue = (value: string) => {\n    let newValue = value\n\n    if (validation) {\n      newValue = validation(newValue)\n    }\n    if (disableByValidation) {\n      setIsError(disableByValidation(newValue))\n    }\n\n    setValue(newValue)\n    onChange?.(newValue)\n  }\n\n  const handleClickOutside = (event: any) => {\n    if (preventOutsideClick) return\n    if (!containerEl?.current?.contains(event.target)) {\n      if (!isLoading) {\n        onDecline(event)\n      } else {\n        event.stopPropagation()\n        event.preventDefault()\n      }\n    }\n  }\n\n  const handleOnEsc = (e: KeyboardEvent) => {\n    if (e.key === keys.ESCAPE) {\n      e.stopPropagation()\n      onDecline()\n    }\n  }\n\n  const handleApplyClick = (event: React.MouseEvent<HTMLElement>) => {\n    if (approveByValidation && !approveByValidation?.(value)) {\n      setIsShowApprovePopover(true)\n    } else {\n      handleFormSubmit(event)\n    }\n  }\n\n  const handleFormSubmit = (event: React.MouseEvent<HTMLElement>): void => {\n    event.preventDefault()\n    event.stopPropagation()\n    if (!isDisabledApply()) {\n      onApply(value, event)\n    }\n  }\n\n  const isDisabledApply = (): boolean =>\n    !!(isLoading || isError || isDisabled || (disableEmpty && !value.length))\n\n  const ApplyBtn = (\n    <RiTooltip\n      anchorClassName={cx(styles.tooltip, 'tooltip')}\n      position=\"bottom\"\n      title={\n        (isDisabled && disabledTooltipText?.title) ||\n        (getError && getError?.(value)?.title)\n      }\n      content={\n        (isDisabled && disabledTooltipText?.content) ||\n        (getError && getError?.(value)?.content)\n      }\n      data-testid=\"apply-tooltip\"\n    >\n      <ApplyButton\n        size={iconSize ?? 'M'}\n        disabled={isDisabledApply()}\n        onClick={handleApplyClick}\n        data-testid=\"apply-btn\"\n      />\n    </RiTooltip>\n  )\n\n  return (\n    <>\n      {viewChildrenMode ? (\n        children\n      ) : (\n        <OutsideClickDetector\n          onOutsideClick={handleClickOutside}\n          isDisabled={isShowApprovePopover}\n        >\n          <IIEContainer ref={containerEl}>\n            <WindowEvent event=\"keydown\" handler={handleOnEsc} />\n            <FocusTrap disabled={disableFocusTrap}>\n              <form\n                className=\"relative\"\n                onSubmit={(e: unknown) =>\n                  handleFormSubmit(e as React.MouseEvent<HTMLElement>)\n                }\n                style={{\n                  ...customStyles?.inputContainer,\n                }}\n              >\n                <FlexItem grow>\n                  {children || (\n                    <>\n                      <StyledTextInput\n                        $width={customStyles?.input?.width}\n                        $height={customStyles?.input?.height}\n                        name={fieldName}\n                        id={fieldName}\n                        className={cx(styles.field, textFiledClassName)}\n                        maxLength={maxLength || undefined}\n                        placeholder={placeholder}\n                        value={value}\n                        onChange={handleChangeValue}\n                        loading={isLoading}\n                        data-testid=\"inline-item-editor\"\n                        autoComplete={autoComplete}\n                        variant={variant}\n                        ref={inputRef}\n                      />\n                      {expandable && (\n                        <p className={styles.keyHiddenText}>{value}</p>\n                      )}\n                    </>\n                  )}\n                </FlexItem>\n                <ActionsContainer\n                  justify=\"around\"\n                  gap=\"m\"\n                  $position={controlsPosition}\n                  $design={controlsDesign}\n                  $width={customStyles?.actionsContainer?.width}\n                  $height={customStyles?.actionsContainer?.height}\n                  grow={false}\n                  className={cx(\n                    'inlineItemEditor__controls',\n                    styles.controls,\n                    controlsClassName,\n                  )}\n                >\n                  <ActionsWrapper $size={size}>\n                    <DeclineButton\n                      onClick={onDecline}\n                      disabled={isLoading}\n                      data-testid=\"cancel-btn\"\n                    />\n                  </ActionsWrapper>\n                  {!approveByValidation && (\n                    <ActionsWrapper $size={size}>{ApplyBtn}</ActionsWrapper>\n                  )}\n                  {approveByValidation && (\n                    <ActionsWrapper $size={size}>\n                      <ConfirmationPopover\n                        anchorPosition=\"leftCenter\"\n                        isOpen={isShowApprovePopover}\n                        closePopover={() => setIsShowApprovePopover(false)}\n                        anchorClassName={cx(\n                          styles.popoverAnchor,\n                          'popoverAnchor',\n                        )}\n                        panelClassName={cx(styles.popoverPanel)}\n                        button={ApplyBtn}\n                        title={approveText?.title}\n                        message={approveText?.text}\n                        confirmButton={\n                          <DestructiveButton\n                            aria-label=\"Save\"\n                            size=\"small\"\n                            className={cx(styles.btn, styles.saveBtn)}\n                            disabled={isDisabledApply()}\n                            onClick={handleFormSubmit}\n                            data-testid=\"save-btn\"\n                          >\n                            Save\n                          </DestructiveButton>\n                        }\n                      />\n                    </ActionsWrapper>\n                  )}\n                </ActionsContainer>\n              </form>\n            </FocusTrap>\n          </IIEContainer>\n        </OutsideClickDetector>\n      )}\n    </>\n  )\n}\n\nexport default InlineItemEditor\n"
  },
  {
    "path": "redisinsight/ui/src/components/inline-item-editor/index.ts",
    "content": "import InlineItemEditor from './InlineItemEditor'\n\nexport default InlineItemEditor\n"
  },
  {
    "path": "redisinsight/ui/src/components/inline-item-editor/styles.module.scss",
    "content": ".container {\n  max-width: 100%;\n\n  :global(.euiFormControlLayout) {\n    max-width: 100% !important;\n  }\n}\n\n.controls {\n  .tooltip,\n  .popoverWrapper {\n    width: 50% !important;\n    height: 100% !important;\n  }\n}\n\n.applyBtn {\n  height: 100% !important;\n  width: 100% !important;\n}\n\n.controlsBottom {\n  top: 100%;\n  right: 0;\n  border-radius: 0 0 10px 10px;\n  box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n}\n\n.controlsTop {\n  bottom: 100%;\n  right: 0;\n  border-radius: 10px 10px 0 0;\n  box-shadow: 0 -3px 3px var(--controlsBoxShadowColor);\n}\n\n.controlsRight {\n  top: 0;\n  left: 100%;\n  border-radius: 0 10px 10px 0;\n  box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n}\n\n.controlsLeft {\n  top: 0;\n  right: 100%;\n  border-radius: 10px 0 0 10px;\n  box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n}\n\n.controlsInside {\n  top: calc(100% - 35px);\n  right: 7px;\n  border-radius: 0 10px 10px 0;\n  box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n}\n\n.controlsSeparate {\n  border-radius: 0;\n  box-shadow: none;\n  background-color: inherit !important;\n  text-align: right;\n  width: 60px;\n  z-index: 4;\n\n  .btn,\n  .popoverWrapper {\n    margin: 6px 3px;\n    height: 24px !important;\n    width: 24px !important;\n  }\n\n  .btn:hover {\n    background-color: var(--hoverInListColorDarken) !important;\n  }\n\n  .applyBtn {\n    margin-top: 0;\n  }\n\n  svg {\n    width: 18px !important;\n    height: 18px !important;\n  }\n}\n\n.declineBtn:hover {\n  color: var(--euiColorColorDanger) !important;\n}\n\n.applyBtn:hover:not([class*=\"isDisabled\"]) {\n  color: var(--euiColorPrimary) !important;\n}\n\n.keyHiddenText {\n  display: inline-block;\n  visibility: hidden;\n  height: 1px;\n  overflow: hidden;\n  max-width: 100%;\n  margin-right: 80px;\n  word-break: break-all;\n}\n\n.popoverAnchor,\n.popoverWrapper .tooltip {\n  width: 100% !important;\n  height: 100% !important;\n}\n\n.popoverPanel:global(.euiPanel--paddingMedium) {\n  width: 296px !important;\n  padding: 24px 30px !important;\n  border: 1px solid var(--euiColorPrimary) !important;\n  background-color: var(--browserTableRowEven) !important;\n\n  :global(.euiPopover__panelArrow:after) {\n    border-left-color: var(--browserTableRowEven) !important;\n  }\n  :global(.euiPopover__panelArrow:before) {\n    border-left-color: var(--euiColorPrimary) !important;\n  }\n}\n\n.popover {\n  word-wrap: break-word;\n\n  h4 b {\n    font-size: 14px !important;\n    color: var(--htmlColor) !important;\n  }\n}\n.approveText {\n  font-size: 13px !important;\n  letter-spacing: -0.13px;\n}\n\n.popoverFooter {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 12px;\n\n  .saveBtn {\n    height: 36px !important;\n    width: 86px !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/input-field-sentinel/InputFieldSentinel.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport InputFieldSentinel, {\n  Props,\n  SentinelInputFieldType,\n} from './InputFieldSentinel'\n\nconst mockedProps = mock<Props>()\n\nconst inputTextTestId = 'sentinel-input'\nconst inputPasswordTestId = 'sentinel-input-password'\nconst inputNumberTestId = 'sentinel-input-number'\n\ndescribe('InputFieldSentinel', () => {\n  it('should render simple fieldText', () => {\n    expect(\n      render(<InputFieldSentinel {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should change simple fieldText properly', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Text}\n      />,\n    )\n    fireEvent.change(screen.getByTestId(inputTextTestId), {\n      target: { value: 'val' },\n    })\n    expect(screen.getByTestId(inputTextTestId)).toHaveValue('val')\n  })\n\n  it('should render Password field', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Password}\n      />,\n    )\n    expect(screen.getByTestId(inputPasswordTestId)).toBeInTheDocument()\n  })\n\n  it('should change Password field properly', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Password}\n      />,\n    )\n    fireEvent.change(screen.getByTestId(inputPasswordTestId), {\n      target: { value: 'val' },\n    })\n    expect(screen.getByTestId(inputPasswordTestId)).toHaveValue('val')\n  })\n\n  it('should render Number field', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Number}\n      />,\n    )\n    expect(screen.getByTestId(inputNumberTestId)).toBeInTheDocument()\n  })\n\n  it('should default to 0 when Number field properly is set to string with letters and Number field was not previously set', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Number}\n      />,\n    )\n    fireEvent.change(screen.getByTestId(inputNumberTestId), {\n      target: { value: 'val13' },\n    })\n    expect(screen.getByTestId(inputNumberTestId)).toHaveValue('0')\n  })\n\n  it('should default to previous value when Number field properly is set to string with letters', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Number}\n      />,\n    )\n    fireEvent.change(screen.getByTestId(inputNumberTestId), {\n      target: { value: '1' },\n    })\n    expect(screen.getByTestId(inputNumberTestId)).toHaveValue('1')\n\n    fireEvent.change(screen.getByTestId(inputNumberTestId), {\n      target: { value: 'val13' },\n    })\n    expect(screen.getByTestId(inputNumberTestId)).toHaveValue('1')\n  })\n\n  it('should set Number field properly when is set to string with numbers only', () => {\n    render(\n      <InputFieldSentinel\n        {...instance(mockedProps)}\n        inputType={SentinelInputFieldType.Number}\n      />,\n    )\n    fireEvent.change(screen.getByTestId(inputNumberTestId), {\n      target: { value: '13' },\n    })\n    expect(screen.getByTestId(inputNumberTestId)).toHaveValue('13')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/input-field-sentinel/InputFieldSentinel.tsx",
    "content": "import React, { useState } from 'react'\nimport styled from 'styled-components'\nimport { omit } from 'lodash'\n\nimport { useDebouncedEffect } from 'uiSrc/services'\nimport {\n  NumericInput,\n  PasswordInput,\n  TextInput,\n} from 'uiSrc/components/base/inputs'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nexport enum SentinelInputFieldType {\n  Text = 'text',\n  Password = 'password',\n  Number = 'number',\n}\n\nexport interface Props {\n  name?: string\n  value?: string\n  placeholder?: string\n  inputType?: SentinelInputFieldType\n  isText?: boolean\n  isNumber?: boolean\n  maxLength?: number\n  min?: number\n  max?: number\n  isInvalid?: boolean\n  disabled?: boolean\n  className?: string\n  append?: React.ReactElement\n  onChangedInput: (name: string, value: string) => void\n}\nconst InputInvalidIcon = styled(RiIcon).attrs({\n  color: 'danger500',\n  type: 'ToastDangerIcon',\n})`\n  position: absolute;\n  top: calc(50% - 9px);\n  right: 10px;\n`\n\nconst InputFieldSentinel = (props: Props) => {\n  const {\n    name = '',\n    value: valueProp = '',\n    inputType = SentinelInputFieldType.Text,\n    isInvalid: isInvalidProp = false,\n    onChangedInput,\n  } = props\n\n  const clearProp = omit(props, 'inputType')\n\n  const [value, setValue] = useState(valueProp)\n  const [isInvalid, setIsInvalid] = useState(isInvalidProp)\n\n  const handleChange = (value: string) => {\n    setValue(value)\n    isInvalid && setIsInvalid(false)\n  }\n\n  useDebouncedEffect(() => onChangedInput(name, value), 200, [value])\n\n  return (\n    <>\n      {inputType === SentinelInputFieldType.Text && (\n        <TextInput\n          {...clearProp}\n          value={value}\n          onChange={handleChange}\n          data-testid=\"sentinel-input\"\n        />\n      )}\n      {inputType === SentinelInputFieldType.Password && (\n        <PasswordInput\n          {...clearProp}\n          value={value}\n          onChange={(value) => handleChange(value)}\n          data-testid=\"sentinel-input-password\"\n        />\n      )}\n      {inputType === SentinelInputFieldType.Number && (\n        <NumericInput\n          {...clearProp}\n          autoValidate\n          value={Number(value)}\n          onChange={(value) => handleChange(value ? value.toString() : '')}\n          data-testid=\"sentinel-input-number\"\n        />\n      )}\n      {isInvalid && <InputInvalidIcon />}\n    </>\n  )\n}\n\nexport default InputFieldSentinel\n"
  },
  {
    "path": "redisinsight/ui/src/components/input-field-sentinel/styles.module.scss",
    "content": ".inputInvalidIcon {\n  position: absolute;\n  top: calc(50% - 9px);\n  right: 10px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/InstanceHeader.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  act,\n  fireEvent,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n  screen,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport {\n  checkDatabaseIndex,\n  connectedInstanceInfoSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { appContextDbIndex } from 'uiSrc/slices/app/context'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport InstanceHeader, { Props } from './InstanceHeader'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceInfoSelector: jest.fn().mockReturnValue({\n    databases: 16,\n  }),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    username: 'username',\n    id: 'instanceId',\n    loading: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextDbIndex: jest.fn().mockReturnValue({\n    disabled: false,\n  }),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\ndescribe('InstanceHeader', () => {\n  it('should render', () => {\n    expect(render(<InstanceHeader {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render change index button with databases = 1', () => {\n    ;(connectedInstanceInfoSelector as jest.Mock).mockReturnValueOnce({\n      databases: 1,\n    })\n\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    expect(screen.queryByTestId('change-index-btn')).not.toBeInTheDocument()\n  })\n\n  it('should render change index button', () => {\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('change-index-btn')).toBeInTheDocument()\n  })\n\n  it('should render change index input after click on the button', () => {\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('change-index-btn'))\n\n    expect(screen.getByTestId('change-index-input')).toBeInTheDocument()\n  })\n\n  it('should call proper actions after changing database index', () => {\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('change-index-btn'))\n\n    fireEvent.change(screen.getByTestId('change-index-input'), {\n      target: { value: 3 },\n    })\n\n    expect(screen.getByTestId('change-index-input')).toHaveValue('3')\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    // check if store actions contain proper action: {type: \"instances/checkDatabaseIndex\"}\n    expect(store.getActions()).toContainEqual(\n      expect.objectContaining(checkDatabaseIndex()),\n    )\n  })\n\n  it('should be disabled db index button with loading state', () => {\n    ;(connectedInstanceSelector as jest.Mock).mockReturnValueOnce({\n      loading: true,\n    })\n\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('change-index-btn')).toBeDisabled()\n  })\n\n  it('should be disabled db index button with disabled state', () => {\n    ;(appContextDbIndex as jest.Mock).mockReturnValueOnce({\n      disabled: true,\n    })\n\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('change-index-btn')).toBeDisabled()\n  })\n\n  it('should call history push with proper path', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<InstanceHeader {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('my-redis-db-btn'))\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('/')\n  })\n\n  it('should show env dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<InstanceHeader {...instance(mockedProps)} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('my-redis-db-btn')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('admin-console-breadcrumb-btn'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should not show env dependent items button when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    set(initialStoreState, 'user.cloudProfile', { data: {} })\n\n    render(<InstanceHeader {...instance(mockedProps)} />, {\n      store: mockStore(initialStoreState),\n    })\n\n    expect(screen.queryByTestId('my-redis-db-btn')).not.toBeInTheDocument()\n  })\n\n  it('should show cloud user profile if env dependant flag is off and cloud profile is present', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    set(initialStoreState, 'user.cloudProfile', {\n      data: {\n        id: 123,\n        name: 'John Smith',\n        currentAccountId: '40',\n        accounts: [{ id: '40', name: 'Test account' }],\n      },\n    })\n\n    render(<InstanceHeader {...instance(mockedProps)} />, {\n      store: mockStore(initialStoreState),\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n    await waitFor(() => {\n      expect(\n        screen.queryByTestId('user-profile-popover-content'),\n      ).toBeInTheDocument()\n    })\n\n    expect(screen.queryByTestId('cloud-user-profile-badge')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('profile-import-cloud-databases'),\n    ).not.toBeInTheDocument()\n    expect(screen.queryByTestId('profile-logout')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('cloud-admin-console-link')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('profile-account-40-selected'),\n    ).toHaveTextContent('Test account #40')\n  })\n\n  it('should show sso user profile if env dependant flag and cloud-sso features are on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: true },\n    )\n    set(initialStoreState, 'oauth.cloud.user', {\n      data: {\n        id: 123,\n        name: 'John Smith',\n        currentAccountId: '40',\n        accounts: [{ id: '40', name: 'Test account' }],\n      },\n    })\n\n    render(<InstanceHeader {...instance(mockedProps)} />, {\n      store: mockStore(initialStoreState),\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n    await waitFor(() => {\n      expect(\n        screen.queryByTestId('user-profile-popover-content'),\n      ).toBeInTheDocument()\n    })\n\n    expect(screen.queryByTestId('oauth-user-profile-badge')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('profile-import-cloud-databases'),\n    ).toBeInTheDocument()\n    expect(screen.queryByTestId('profile-logout')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('cloud-admin-console-link'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('profile-account-40-selected'),\n    ).toHaveTextContent('Test account #40')\n  })\n\n  it('should not show sso user profile if cloud ads feature is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudAds}`,\n      { flag: false },\n    )\n\n    render(<InstanceHeader {...instance(mockedProps)} />, {\n      store: mockStore(initialStoreState),\n    })\n\n    expect(\n      screen.queryByTestId('oauth-user-profile-badge'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('cloud-user-profile-badge'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/InstanceHeader.stories.tsx",
    "content": "import React, { useEffect } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useDispatch } from 'react-redux'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { ConnectionType, Instance } from 'uiSrc/slices/interfaces'\n\nimport InstanceHeader from './InstanceHeader'\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\nimport {\n  getDatabaseConfigInfoSuccess,\n  setConnectedInfoInstanceSuccess,\n  setConnectedInstanceSuccess,\n} from 'uiSrc/slices/instances/instances'\nimport { getServerInfoSuccess } from 'uiSrc/slices/app/info'\nimport { setDbIndexState } from 'uiSrc/slices/app/context'\nimport { fn } from 'storybook/test'\n\ninterface InstanceHeaderArgs {\n  instance?: Partial<Instance>\n  databases?: number\n  buildType?: BuildType | null\n  dbIndexDisabled?: boolean\n  returnUrl?: string | null\n  onChangeDbIndex?: (index: number) => void\n}\n\nconst StorePopulator = ({ args }: { args: InstanceHeaderArgs }) => {\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    // Build instance from factory with optional overrides\n    const instance = DBInstanceFactory.build({\n      db: 0,\n      loading: false,\n      ...args.instance,\n    })\n\n    dispatch(setConnectedInstanceSuccess(instance))\n\n    // Set instance info (includes databases count)\n    dispatch(\n      setConnectedInfoInstanceSuccess({\n        version: instance.version ?? '7.0.0',\n        server: {},\n        databases: args.databases ?? 1,\n      } as any),\n    )\n\n    // Set instance overview (includes version)\n    dispatch(\n      getDatabaseConfigInfoSuccess({\n        version: instance.version ?? '7.0.0',\n      } as any),\n    )\n\n    // Set server info (buildType)\n    if (args.buildType) {\n      dispatch(\n        getServerInfoSuccess({\n          buildType: args.buildType,\n        } as any),\n      )\n    }\n\n    // Set db index disabled state\n    if (args.dbIndexDisabled !== undefined) {\n      dispatch(setDbIndexState(args.dbIndexDisabled))\n    }\n  }, [dispatch, args])\n\n  return null\n}\n\nconst meta: Meta<typeof InstanceHeader> = {\n  component: InstanceHeader,\n  decorators: [\n    (Story, context) => {\n      // storeArgs is a custom parameter (not built-in) used to pass data for Redux store setup\n      const storeArgs =\n        (context.parameters?.storeArgs as InstanceHeaderArgs) || {}\n\n      return (\n        <>\n          <StorePopulator args={storeArgs} />\n          <Story />\n        </>\n      )\n    },\n  ],\n  args: {\n    onChangeDbIndex: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n\nexport const WithMultipleDatabases: Story = {\n  parameters: {\n    storeArgs: {\n      databases: 16,\n      instance: {\n        db: 5,\n      },\n    } as InstanceHeaderArgs,\n  },\n}\n\nexport const WithLongDatabaseName: Story = {\n  parameters: {\n    storeArgs: {\n      instance: {\n        name: 'Very Long Database Name That Should Truncate With Ellipsis',\n      },\n    } as InstanceHeaderArgs,\n  },\n}\n\nexport const RedisStack: Story = {\n  parameters: {\n    storeArgs: {\n      buildType: BuildType.RedisStack,\n      instance: {\n        name: 'Redis Stack Instance',\n        modules: [\n          { name: 'search', version: 20400, semanticVersion: '2.4.0' },\n          { name: 'json', version: 20000, semanticVersion: '2.0.0' },\n        ],\n      },\n    } as InstanceHeaderArgs,\n  },\n}\n\nexport const WithReturnUrl: Story = {\n  parameters: {\n    storeArgs: {\n      returnUrl: '/some/path',\n    } as InstanceHeaderArgs,\n  },\n}\n\nexport const Loading: Story = {\n  parameters: {\n    storeArgs: {\n      instance: {\n        loading: true,\n      },\n    } as InstanceHeaderArgs,\n  },\n}\n\nexport const DbIndexDisabled: Story = {\n  parameters: {\n    storeArgs: {\n      databases: 16,\n      instance: {\n        db: 3,\n      },\n      dbIndexDisabled: true,\n    } as InstanceHeaderArgs,\n  },\n}\n\nexport const ClusterConnection: Story = {\n  parameters: {\n    storeArgs: {\n      instance: {\n        connectionType: ConnectionType.Cluster,\n        name: 'Redis Cluster',\n        host: 'cluster.example.com',\n        port: 7000,\n      },\n    } as InstanceHeaderArgs,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/InstanceHeader.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport cx from 'classnames'\nimport { useTheme } from '@redis-ui/styles'\n\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { selectOnFocus } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport {\n  checkDatabaseIndexAction,\n  connectedInstanceInfoSelector,\n  connectedInstanceOverviewSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport {\n  appContextDbIndex,\n  clearBrowserKeyListData,\n  setBrowserSelectedKey,\n} from 'uiSrc/slices/app/context'\n\nimport {\n  DatabaseOverview,\n  FeatureFlagComponent,\n  RiTooltip,\n} from 'uiSrc/components'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor'\nimport { CopilotTrigger, InsightsTrigger } from 'uiSrc/components/triggers'\nimport ShortInstanceInfo from 'uiSrc/components/instance-header/components/ShortInstanceInfo'\n\nimport { resetKeyInfo } from 'uiSrc/slices/browser/keys'\n\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { isAnyFeatureEnabled } from 'uiSrc/utils/features'\nimport { getConfig } from 'uiSrc/config'\nimport { appReturnUrlSelector } from 'uiSrc/slices/app/url-handling'\nimport UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { NumericInput } from 'uiSrc/components/base/inputs'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport InstancesNavigationPopover from './components/instances-navigation-popover'\nimport styles from './styles.module.scss'\n\nconst riConfig = getConfig()\nconst { returnUrlBase, returnUrlLabel, returnUrlTooltip } = riConfig.app\n\nexport interface Props {\n  onChangeDbIndex?: (index: number) => void\n}\n\nconst InstanceHeader = ({ onChangeDbIndex }: Props) => {\n  const theme = useTheme()\n  const {\n    name = '',\n    host = '',\n    port = '',\n    username,\n    connectionType = ConnectionType.Standalone,\n    db = 0,\n    id,\n    loading: instanceLoading,\n    modules = [],\n  } = useSelector(connectedInstanceSelector)\n  const { version } = useSelector(connectedInstanceOverviewSelector)\n  const { server } = useSelector(appInfoSelector)\n  const { disabled: isDbIndexDisabled } = useSelector(appContextDbIndex)\n  const { databases = 0 } = useSelector(connectedInstanceInfoSelector)\n  const returnUrl = useSelector(appReturnUrlSelector)\n  const {\n    [FeatureFlags.databaseChat]: databaseChatFeature,\n    [FeatureFlags.documentationChat]: documentationChatFeature,\n    [FeatureFlags.envDependent]: envDependentFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n  const isAnyChatAvailable = isAnyFeatureEnabled([\n    databaseChatFeature,\n    documentationChatFeature,\n  ])\n\n  const history = useHistory()\n  const [dbIndex, setDbIndex] = useState<string>(String(db || 0))\n  const [isDbIndexEditing, setIsDbIndexEditing] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setDbIndex(String(db || 0))\n  }, [db])\n\n  const isRedisStack = server?.buildType === BuildType.RedisStack\n\n  const goHome = () => {\n    history.push(Pages.home)\n  }\n\n  const goToReturnUrl = () => {\n    document.location = `${returnUrlBase}${returnUrl}`\n  }\n\n  const handleChangeDbIndex = () => {\n    setIsDbIndexEditing(false)\n\n    if (db === +dbIndex) return\n\n    dispatch(\n      checkDatabaseIndexAction(\n        id,\n        +dbIndex,\n        () => {\n          dispatch(clearBrowserKeyListData())\n          onChangeDbIndex?.(+dbIndex)\n          dispatch(resetKeyInfo())\n          dispatch(setBrowserSelectedKey(null))\n\n          sendEventTelemetry({\n            event: TelemetryEvent.BROWSER_DATABASE_INDEX_CHANGED,\n            eventData: {\n              databaseId: id,\n              prevIndex: db,\n              nextIndex: +dbIndex,\n            },\n          })\n        },\n        () => setDbIndex(String(db)),\n      ),\n    )\n  }\n\n  return (\n    <div\n      className={cx(styles.container)}\n      style={{\n        borderBottom: theme.components.sideBar.collapsed.borderRight,\n      }}\n    >\n      <Row\n        responsive\n        align=\"center\"\n        justify=\"between\"\n        style={{ height: '100%' }}\n      >\n        <FlexItem style={{ overflow: 'hidden' }} grow={false}>\n          <div\n            className={styles.breadcrumbsContainer}\n            data-testid=\"breadcrumbs-container\"\n          >\n            <div>\n              <FeatureFlagComponent name={FeatureFlags.envDependent}>\n                <RiTooltip\n                  position=\"bottom\"\n                  content={\n                    server?.buildType === BuildType.RedisStack\n                      ? 'Edit database'\n                      : 'Redis Databases'\n                  }\n                >\n                  <Link\n                    color=\"subdued\"\n                    underline\n                    variant=\"inline\"\n                    aria-label={\n                      server?.buildType === BuildType.RedisStack\n                        ? 'Edit database'\n                        : 'Redis Databases'\n                    }\n                    data-testid=\"my-redis-db-btn\"\n                    onClick={goHome}\n                  >\n                    Databases\n                  </Link>\n                </RiTooltip>\n              </FeatureFlagComponent>\n            </div>\n            <div style={{ flex: 1, overflow: 'hidden' }}>\n              <div style={{ maxWidth: '100%' }}>\n                <Row align=\"center\">\n                  <FeatureFlagComponent name={FeatureFlags.envDependent}>\n                    <FlexItem>\n                      <Text className={styles.divider}>/</Text>\n                    </FlexItem>\n                  </FeatureFlagComponent>\n                  {returnUrlBase && returnUrl && (\n                    <FeatureFlagComponent\n                      name={FeatureFlags.envDependent}\n                      otherwise={\n                        <FlexItem\n                          style={{ padding: '4px 24px 4px 0' }}\n                          data-testid=\"return-to-sm-item\"\n                        >\n                          <RiTooltip\n                            position=\"bottom\"\n                            content={returnUrlTooltip || returnUrlLabel}\n                          >\n                            <Text\n                              className={styles.breadCrumbLink}\n                              aria-label={returnUrlTooltip || returnUrlLabel}\n                              onClick={goToReturnUrl}\n                              onKeyDown={goToReturnUrl}\n                            >\n                              &#60; {returnUrlLabel}\n                            </Text>\n                          </RiTooltip>\n                        </FlexItem>\n                      }\n                    />\n                  )}\n                  <FlexItem grow style={{ overflow: 'hidden' }}>\n                    {isRedisStack || !envDependentFeature?.flag ? (\n                      <b className={styles.dbName}>{name}</b>\n                    ) : (\n                      <InstancesNavigationPopover name={name} />\n                    )}\n                  </FlexItem>\n                  {databases > 1 && (\n                    <FlexItem style={{ paddingLeft: 12 }}>\n                      <div\n                        style={{\n                          display: 'flex',\n                          alignItems: 'center',\n                        }}\n                      >\n                        {isDbIndexEditing ? (\n                          <div style={{ marginRight: 48 }}>\n                            <InlineItemEditor\n                              controlsPosition=\"right\"\n                              onApply={handleChangeDbIndex}\n                              onDecline={() => setIsDbIndexEditing(false)}\n                              viewChildrenMode={false}\n                              controlsClassName={styles.controls}\n                            >\n                              <NumericInput\n                                autoSize\n                                autoValidate\n                                min={0}\n                                onFocus={selectOnFocus}\n                                onChange={(value) =>\n                                  setDbIndex(value ? value.toString() : '')\n                                }\n                                value={Number(dbIndex)}\n                                placeholder=\"Database Index\"\n                                className={styles.dbIndexInput}\n                                data-testid=\"change-index-input\"\n                              />\n                            </InlineItemEditor>\n                          </div>\n                        ) : (\n                          <EmptyButton\n                            icon={EditIcon}\n                            iconSide=\"right\"\n                            onClick={() => setIsDbIndexEditing(true)}\n                            className={styles.buttonDbIndex}\n                            disabled={isDbIndexDisabled || instanceLoading}\n                            data-testid=\"change-index-btn\"\n                          >\n                            <span\n                              style={{\n                                fontSize: 14,\n                                marginBottom: '-2px',\n                              }}\n                            >\n                              db{db || 0}\n                            </span>\n                          </EmptyButton>\n                        )}\n                      </div>\n                    </FlexItem>\n                  )}\n                  <FlexItem style={{ paddingLeft: 6 }}>\n                    <RiTooltip\n                      position=\"right\"\n                      anchorClassName={styles.tooltipAnchor}\n                      className={styles.tooltip}\n                      content={\n                        <ShortInstanceInfo\n                          info={{\n                            name,\n                            host,\n                            port,\n                            user: username,\n                            connectionType,\n                            version,\n                            dbIndex: db,\n                          }}\n                          modules={modules}\n                          databases={databases}\n                        />\n                      }\n                    >\n                      <RiIcon\n                        className={styles.infoIcon}\n                        type=\"InfoIcon\"\n                        size=\"l\"\n                        style={{ cursor: 'pointer' }}\n                        data-testid=\"db-info-icon\"\n                      />\n                    </RiTooltip>\n                  </FlexItem>\n                </Row>\n              </div>\n            </div>\n          </div>\n        </FlexItem>\n\n        <FlexItem style={{ textAlign: 'center' }}>\n          <DatabaseOverview />\n        </FlexItem>\n\n        <FlexItem>\n          <Row align=\"center\" justify=\"end\">\n            {isAnyChatAvailable && (\n              <FlexItem style={{ marginLeft: 12 }}>\n                <CopilotTrigger />\n              </FlexItem>\n            )}\n\n            <FlexItem style={{ marginLeft: 12 }}>\n              <InsightsTrigger />\n            </FlexItem>\n\n            <UserProfile />\n          </Row>\n        </FlexItem>\n      </Row>\n    </div>\n  )\n}\n\nexport default InstanceHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/ShortInstanceInfo.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport { getModule, truncateText } from 'uiSrc/utils'\nimport { DATABASE_LIST_MODULES_TEXT } from 'uiSrc/slices/interfaces'\nimport ShortInstanceInfo, { Props } from './ShortInstanceInfo'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('ShortInstanceInfo', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <ShortInstanceInfo\n          info={{ ...instance(mockedProps) }}\n          modules={[]}\n          databases={2}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render database modules', () => {\n    const modules = [\n      { name: 'redisgears' },\n      { name: 'redisearch', version: '123.23' },\n    ]\n    render(\n      <ShortInstanceInfo\n        info={{ ...instance(mockedProps) }}\n        modules={modules}\n        databases={2}\n      />,\n    )\n\n    modules.forEach(({ name, version }) => {\n      expect(screen.getByTestId(`module_${name}`)).toBeInTheDocument()\n      expect(screen.getByTestId(`module_${name}`)).toHaveTextContent(\n        `${truncateText(getModule(name)?.name || DATABASE_LIST_MODULES_TEXT[name] || name, 50)}${version ? ` v.${version}` : ''}`,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/ShortInstanceInfo.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const DbIndexInfoWrapper = styled(Row)`\n  background: var(\n    --tooltipLightBgColor\n  ); // TODO: use theme color when designs are available\n  border-radius: 8px;\n  padding: 8px 16px;\n`\n\nexport const SeparatorLine = styled.div`\n  width: 100%;\n  height: 1px;\n  background-color: var(\n    --euiToastSuccessBorderColor\n  ); // TODO: use theme color when designs are available\n`\n\nexport const WordBreakWrapper = styled.div`\n  word-break: break-word;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/ShortInstanceInfo.tsx",
    "content": "import React, { useContext } from 'react'\nimport { capitalize } from 'lodash'\n\nimport {\n  CONNECTION_TYPE_DISPLAY,\n  ConnectionType,\n  DATABASE_LIST_MODULES_TEXT,\n  RedisDefaultModules,\n} from 'uiSrc/slices/interfaces'\nimport { getModule, Nullable, truncateText } from 'uiSrc/utils'\n\nimport { DEFAULT_MODULES_INFO } from 'uiSrc/constants/modules'\nimport { Theme } from 'uiSrc/constants'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { AllIconsType, RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport MessageInfoSvg from 'uiSrc/assets/img/icons/help_illus.svg'\nimport {\n  DbIndexInfoWrapper,\n  SeparatorLine,\n  WordBreakWrapper,\n} from './ShortInstanceInfo.styles'\n\nexport interface Props {\n  info: {\n    name: string\n    host: string\n    port: string | number\n    connectionType: ConnectionType\n    version: string\n    dbIndex: number\n    user?: Nullable<string>\n  }\n  databases: number\n  modules: AdditionalRedisModule[]\n}\nconst ShortInstanceInfo = ({ info, databases, modules }: Props) => {\n  const { name, host, port, connectionType, version, user } = info\n  const { theme } = useContext(ThemeContext)\n\n  const getIcon = (name: string) => {\n    const icon: AllIconsType =\n      DEFAULT_MODULES_INFO[name as RedisDefaultModules]?.icon\n    if (icon) {\n      return icon\n    }\n\n    return theme === Theme.Dark ? 'UnknownDarkIcon' : 'UnknownLightIcon'\n  }\n\n  return (\n    <Col gap=\"l\" data-testid=\"db-info-tooltip\">\n      <Col gap=\"m\">\n        <Col gap=\"m\">\n          <Text color=\"primary\" size=\"L\" component=\"div\" variant=\"semiBold\">\n            {name}\n          </Text>\n          <Text color=\"primary\" size=\"s\">\n            {host}:{port}\n          </Text>\n        </Col>\n        {databases > 1 && (\n          <DbIndexInfoWrapper align=\"center\" gap=\"l\">\n            <Col>\n              <RiImage src={MessageInfoSvg} alt=\"Database Info\" $size=\"xs\" />\n            </Col>\n            <Col gap=\"xs\">\n              <Text size=\"m\">Logical databases</Text>\n              <Text color=\"secondary\" size=\"s\">\n                <WordBreakWrapper>\n                  Select logical databases to work with in Browser, Workbench,\n                  and Database Analysis.\n                </WordBreakWrapper>\n              </Text>\n            </Col>\n          </DbIndexInfoWrapper>\n        )}\n        <Row align=\"center\" gap=\"l\">\n          <Row align=\"center\" grow={false}>\n            <RiIcon type=\"ConnectionIcon\" size=\"M\" />\n            <span>\n              {connectionType\n                ? CONNECTION_TYPE_DISPLAY[connectionType]\n                : capitalize(connectionType)}\n            </span>\n          </Row>\n          <Row align=\"center\" grow={false}>\n            <RiIcon type=\"VersionIcon\" size=\"M\" />\n            <span>{version}</span>\n          </Row>\n          <Row align=\"center\" grow={false}>\n            <RiIcon type=\"UserIcon\" size=\"S\" />\n            <span>{user || 'Default'}</span>\n          </Row>\n        </Row>\n      </Col>\n      {!!modules?.length && (\n        <>\n          <SeparatorLine />\n          <Text color=\"primary\" size=\"L\" component=\"div\" variant=\"semiBold\">\n            Database modules\n          </Text>\n          <Col gap=\"s\">\n            {modules?.map(\n              ({ name = '', semanticVersion = '', version = '' }) => (\n                <Row\n                  gap=\"s\"\n                  align=\"center\"\n                  key={name}\n                  data-testid={`module_${name}`}\n                >\n                  <RiIcon type={getIcon(name)} size=\"M\" />\n                  <Text size=\"S\" color=\"secondary\">\n                    {truncateText(\n                      getModule(name)?.name ||\n                        DATABASE_LIST_MODULES_TEXT[\n                          name as RedisDefaultModules\n                        ] ||\n                        name,\n                      50,\n                    )}{' '}\n                    v.\n                    {semanticVersion || version}\n                  </Text>\n                </Row>\n              ),\n            )}\n          </Col>\n        </>\n      )}\n    </Col>\n  )\n}\n\nexport default ShortInstanceInfo\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/InstancesNavigationPopover.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { act } from 'react-dom/test-utils'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { instancesSelector } from 'uiSrc/slices/rdi/instances'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport InstancesNavigationPopover, {\n  InstancesTabs,\n} from './InstancesNavigationPopover'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockRdis = [\n  {\n    id: 'rdiDB_1',\n    name: 'RdiDB_1',\n  },\n  {\n    id: 'rdiDB_2',\n    name: 'RdiDB_2',\n  },\n]\n\nconst mockDbs = [\n  {\n    id: 'db_1',\n    name: 'DB_1',\n  },\n  {\n    id: 'db_2',\n    name: 'DB_2',\n  },\n]\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  instancesSelector: jest.fn().mockReturnValue({\n    data: mockRdis,\n    connectedInstance: {\n      id: 'rdiDB_1',\n      name: 'RdiDB_1',\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  instancesSelector: jest.fn().mockReturnValue({\n    data: mockDbs,\n    connectedInstance: {\n      id: 'db_1',\n      name: 'DB_1',\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('InstancesNavigationPopover', () => {\n  it('should render', () => {\n    expect(render(<InstancesNavigationPopover name=\"db\" />)).toBeTruthy()\n  })\n\n  it('should open popover on click', () => {\n    render(<InstancesNavigationPopover name=\"db\" />)\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))\n    })\n\n    expect(screen.getByTestId('instances-tabs-testId')).toBeInTheDocument()\n  })\n\n  it('should filter instances list', () => {\n    ;(instancesSelector as jest.Mock).mockReturnValue({\n      data: mockRdis,\n    })\n    render(<InstancesNavigationPopover name=\"db\" />)\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))\n    })\n\n    const searchInput = screen.getByTestId('instances-nav-popover-search')\n\n    expect(screen.getByText('RdiDB_2')).toBeInTheDocument()\n\n    fireEvent.change(searchInput, { target: { value: '_1' } })\n\n    expect(screen.getByText('RdiDB_1')).toBeInTheDocument()\n    expect(screen.queryAllByText('RdiDB_2')).toHaveLength(0)\n  })\n\n  it('should change tabs on tabs click', () => {\n    render(<InstancesNavigationPopover name=\"db\" />)\n\n    fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))\n    expect(\n      screen.getByText(`${InstancesTabs.Databases} (0)`),\n    ).toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText(`${InstancesTabs.RDI} (2)`))\n    expect(screen.getByText('Redis Data Integration page')).toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText(`${InstancesTabs.Databases} (0)`))\n    expect(screen.getByText('Redis Databases page')).toBeInTheDocument()\n  })\n\n  it('should send event telemetry', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<InstancesNavigationPopover name=\"db\" />)\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('nav-instance-popover-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.NAVIGATION_PANEL_OPENED,\n      eventData: {\n        databaseId: 'instanceId',\n        numOfRdiDbs: 2,\n        numOfRedisDbs: 0,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/InstancesNavigationPopover.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const ButtonWrapper = styled(Row)`\n  cursor: pointer;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/InstancesNavigationPopover.tsx",
    "content": "import React, { useEffect, useState, useMemo } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { instancesSelector as rdiInstancesSelector } from 'uiSrc/slices/rdi/instances'\nimport { instancesSelector as dbInstancesSelector } from 'uiSrc/slices/instances/instances'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { BrowserStorageItem, DEFAULT_SORT, Pages } from 'uiSrc/constants'\nimport Search from 'uiSrc/assets/img/Search.svg'\nimport { Instance, RdiInstance } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { localStorageService } from 'uiSrc/services'\nimport { filterAndSort } from 'uiSrc/utils'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text } from 'uiSrc/components/base/text'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { RiPopover } from 'uiSrc/components/base'\nimport InstancesList from './components/instances-list'\nimport { ChevronDownIcon } from 'uiSrc/components/base/icons'\nimport { ButtonWrapper } from './InstancesNavigationPopover.styles'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  name: string\n}\n\nexport enum InstancesTabs {\n  Databases = 'Databases',\n  RDI = 'Redis Data Integration',\n}\n\nconst InstancesNavigationPopover = ({ name }: Props) => {\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const [searchFilter, setSearchFilter] = useState('')\n  const [filteredDbInstances, setFilteredDbInstances] = useState<Instance[]>([])\n  const [filteredRdiInstances, setFilteredRdiInstances] = useState<\n    RdiInstance[]\n  >([])\n\n  const { instanceId, rdiInstanceId } = useParams<{\n    instanceId: string\n    rdiInstanceId: string\n  }>()\n  const [selectedTab, setSelectedTab] = useState(\n    rdiInstanceId ? InstancesTabs.RDI : InstancesTabs.Databases,\n  )\n\n  const { data: rdiInstances } = useSelector(rdiInstancesSelector)\n  const { data: dbInstances } = useSelector(dbInstancesSelector)\n  const history = useHistory()\n\n  useEffect(() => {\n    const dbSort =\n      localStorageService.get(BrowserStorageItem.instancesSorting) ??\n      DEFAULT_SORT\n\n    const dbFiltered = filterAndSort(dbInstances, searchFilter, dbSort)\n\n    const rdiSort =\n      localStorageService.get(BrowserStorageItem.rdiInstancesSorting) ??\n      DEFAULT_SORT\n\n    const rdiFiltered = filterAndSort(rdiInstances, searchFilter, rdiSort)\n    setFilteredDbInstances(dbFiltered)\n    setFilteredRdiInstances(rdiFiltered)\n  }, [dbInstances, rdiInstances, searchFilter])\n\n  const handleSearch = (value: string) => {\n    setSearchFilter(value)\n  }\n\n  const showPopover = () => {\n    if (!isPopoverOpen) {\n      sendEventTelemetry({\n        event: TelemetryEvent.NAVIGATION_PANEL_OPENED,\n        eventData: {\n          databaseId: instanceId || rdiInstanceId,\n          numOfRedisDbs: dbInstances?.length || 0,\n          numOfRdiDbs: rdiInstances?.length || 0,\n        },\n      })\n    }\n    setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen)\n  }\n\n  const btnLabel =\n    selectedTab === InstancesTabs.Databases\n      ? 'Redis Databases page'\n      : 'Redis Data Integration page'\n\n  const goHome = () => {\n    history.push(\n      selectedTab === InstancesTabs.Databases ? Pages.home : Pages.rdi,\n    )\n  }\n\n  const tabs: TabInfo[] = useMemo(\n    () => [\n      {\n        label: `${InstancesTabs.Databases} (${dbInstances?.length || 0})`,\n        value: InstancesTabs.Databases,\n        content: null,\n      },\n      {\n        label: `${InstancesTabs.RDI} (${rdiInstances?.length || 0})`,\n        value: InstancesTabs.RDI,\n        content: null,\n      },\n    ],\n    [dbInstances, rdiInstances],\n  )\n\n  return (\n    <RiPopover\n      ownFocus\n      anchorPosition=\"downLeft\"\n      panelPaddingSize=\"none\"\n      isOpen={isPopoverOpen}\n      closePopover={() => showPopover()}\n      button={\n        <ButtonWrapper\n          align=\"center\"\n          gap=\"s\"\n          onClick={() => showPopover()}\n          data-testid=\"nav-instance-popover-btn\"\n        >\n          <Text color=\"primary\">{name}</Text>\n          <ChevronDownIcon size=\"S\" />\n        </ButtonWrapper>\n      }\n    >\n      <div className={styles.wrapper}>\n        <div className={styles.searchInputContainer}>\n          <TextInput\n            className={styles.searchInput}\n            icon={Search}\n            value={searchFilter}\n            onChange={handleSearch}\n            data-testid=\"instances-nav-popover-search\"\n          />\n        </div>\n        <div>\n          <div className={styles.tabsContainer}>\n            <Tabs\n              tabs={tabs}\n              value={selectedTab}\n              // @ts-expect-error type mismatch\n              onChange={setSelectedTab}\n              className={styles.tabs}\n              data-testid=\"instances-tabs-testId\"\n            />\n          </div>\n          <Spacer size=\"m\" />\n          <InstancesList\n            selectedTab={selectedTab}\n            filteredDbInstances={filteredDbInstances}\n            filteredRdiInstances={filteredRdiInstances}\n            onItemClick={showPopover}\n          />\n          <div>\n            <Spacer size=\"m\" />\n            <Divider />\n            <div className={styles.footerContainer}>\n              <Text className={styles.homePageLink} onClick={goHome}>\n                {btnLabel}\n              </Text>\n            </div>\n          </div>\n        </div>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default InstancesNavigationPopover\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/InstancesList.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { act } from 'react-dom/test-utils'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Instance, RdiInstance } from 'uiSrc/slices/interfaces'\nimport InstancesList, { InstancesListProps } from './InstancesList'\nimport { InstancesTabs } from '../../InstancesNavigationPopover'\n\nconst mockedProps = mock<InstancesListProps>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockRdis: RdiInstance[] = [\n  {\n    id: 'rdiDB_1',\n    name: 'RdiDB_1',\n    loading: false,\n    error: '',\n    url: '',\n  },\n  {\n    id: 'rdiDB_2',\n    name: 'RdiDB_2',\n    loading: false,\n    error: '',\n    url: '',\n  },\n]\n\nconst mockDbs: Instance[] = [\n  {\n    id: 'db_1',\n    name: 'DB_1',\n    host: 'localhost',\n    port: 6379,\n    modules: [],\n    version: '7.0.0',\n  },\n  {\n    id: 'db_2',\n    name: 'DB_2',\n    host: 'localhost',\n    port: 6379,\n    modules: [],\n    version: '7.0.0',\n  },\n]\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  instancesSelector: jest.fn().mockReturnValue({\n    data: mockRdis,\n    connectedInstance: {\n      id: 'rdiDB_1',\n      name: 'RdiDB_1',\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  instancesSelector: jest.fn().mockReturnValue({\n    data: mockDbs,\n    connectedInstance: {\n      id: 'db_1',\n      name: 'DB_1',\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('InstancesList', () => {\n  it('should render', () => {\n    expect(render(<InstancesList {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render database instances when selected tab is db', () => {\n    render(\n      <InstancesList\n        {...instance(mockedProps)}\n        selectedTab={InstancesTabs.Databases}\n        filteredDbInstances={mockDbs}\n      />,\n    )\n\n    expect(screen.getByText(mockDbs[0].name!)).toBeInTheDocument()\n  })\n\n  it('should render rdi instances when selected tab is rdi', () => {\n    render(\n      <InstancesList\n        {...instance(mockedProps)}\n        selectedTab={InstancesTabs.RDI}\n        filteredRdiInstances={mockRdis}\n      />,\n    )\n    expect(screen.getByText(mockRdis[0].name)).toBeInTheDocument()\n  })\n\n  it('should send event telemetry', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const { getByTestId } = render(\n      <InstancesList\n        {...instance(mockedProps)}\n        selectedTab={InstancesTabs.Databases}\n        filteredDbInstances={mockDbs}\n      />,\n    )\n\n    const listItem = getByTestId(`instance-item-${mockDbs[1].id}`)\n    await act(() => fireEvent.click(listItem))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE,\n      eventData: expect.any(Object),\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/InstancesList.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { checkConnectToRdiInstanceAction } from 'uiSrc/slices/rdi/instances'\nimport {\n  checkConnectToInstanceAction,\n  setConnectedInstanceId,\n} from 'uiSrc/slices/instances/instances'\nimport { Pages } from 'uiSrc/constants'\nimport { Instance, RdiInstance } from 'uiSrc/slices/interfaces'\nimport {\n  TelemetryEvent,\n  getRedisModulesSummary,\n  sendEventTelemetry,\n  getRedisInfoSummary,\n} from 'uiSrc/telemetry'\nimport { getDbIndex } from 'uiSrc/utils'\nimport {\n  Group as ListGroup,\n  Item as ListGroupItem,\n} from 'uiSrc/components/base/layout/list'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { InstancesTabs } from '../../InstancesNavigationPopover'\nimport styles from '../../styles.module.scss'\n\nexport interface InstancesListProps {\n  selectedTab: InstancesTabs\n  filteredDbInstances: Instance[]\n  filteredRdiInstances: RdiInstance[]\n  onItemClick: () => void\n}\n\nconst InstancesList = ({\n  selectedTab,\n  filteredDbInstances,\n  filteredRdiInstances,\n  onItemClick,\n}: InstancesListProps) => {\n  const [loading, setLoading] = useState<boolean>(false)\n  const [selected, setSelected] = useState<string>('')\n\n  const { instanceId, rdiInstanceId } = useParams<{\n    instanceId: string\n    rdiInstanceId: string\n  }>()\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const instances =\n    selectedTab === InstancesTabs.Databases\n      ? filteredDbInstances\n      : filteredRdiInstances\n\n  const connectToInstance = (id = '') => {\n    dispatch(setConnectedInstanceId(id))\n    setLoading(false)\n    onItemClick?.()\n    history.push(Pages.browser(id))\n  }\n\n  const goToInstance = async (instance: Instance) => {\n    if (instanceId === instance.id) {\n      // already connected so do nothing\n      return\n    }\n    setLoading(true)\n    const modulesSummary = getRedisModulesSummary(instance.modules)\n    const infoData = await getRedisInfoSummary(instance.id)\n    await sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE,\n      eventData: {\n        databaseId: instance.id,\n        source: 'navigation_panel',\n        provider: instance.provider,\n        ...modulesSummary,\n        ...infoData,\n      },\n    })\n    dispatch(\n      checkConnectToInstanceAction(\n        instance.id,\n        connectToInstance,\n        () => setLoading(false),\n        false,\n      ),\n    )\n  }\n\n  const goToRdiInstance = (instance: RdiInstance) => {\n    if (rdiInstanceId === instance.id) {\n      // already connected so do nothing\n      return\n    }\n    setLoading(true)\n    dispatch(\n      checkConnectToRdiInstanceAction(\n        instance.id,\n        (id: string) => {\n          setLoading(false)\n          onItemClick?.()\n          history.push(Pages.rdiPipelineConfig(id))\n        },\n        () => setLoading(false),\n      ),\n    )\n  }\n\n  const goToPage = (instance: Instance | RdiInstance) => {\n    if (selectedTab === InstancesTabs.Databases) {\n      goToInstance(instance as Instance)\n    } else {\n      goToRdiInstance(instance as RdiInstance)\n    }\n  }\n\n  const isInstanceActive = (id: string) => {\n    if (selectedTab === InstancesTabs.Databases) {\n      return id === instanceId\n    }\n    return id === rdiInstanceId\n  }\n\n  if (!instances?.length) {\n    const emptyMsg =\n      selectedTab === InstancesTabs.Databases\n        ? 'No databases'\n        : 'No RDI endpoints'\n    return <div className={styles.emptyMsg}>{emptyMsg}</div>\n  }\n\n  return (\n    <div className={styles.listContainer}>\n      <ListGroup flush maxWidth=\"none\" gap=\"none\">\n        {instances?.map((instance) => (\n          <ListGroupItem\n            color=\"subdued\"\n            className={styles.item}\n            isActive={isInstanceActive(instance.id)}\n            isDisabled={loading}\n            key={instance.id}\n            label={\n              <Text\n                style={{ display: 'flex', alignItems: 'center' }}\n                component=\"div\"\n              >\n                {loading && instance?.id === selected && (\n                  <Loader size=\"s\" className={styles.loading} />\n                )}\n                {instance.name} {getDbIndex(instance.db)}\n              </Text>\n            }\n            onClick={() => {\n              setSelected(instance.id)\n              goToPage(instance)\n            }}\n            data-testid={`instance-item-${instance.id}`}\n          />\n        ))}\n      </ListGroup>\n    </div>\n  )\n}\n\nexport default InstancesList\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/components/instances-list/index.tsx",
    "content": "import InstancesList from './InstancesList'\n\nexport default InstancesList\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/index.tsx",
    "content": "import InstancesNavigationPopover from './InstancesNavigationPopover'\n\nexport default InstancesNavigationPopover\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/instances-navigation-popover/styles.module.scss",
    "content": ".breadCrumbLink {\n  cursor: pointer;\n  text-decoration: underline;\n  max-width: 300px;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n\n  &:hover {\n    text-decoration: none;\n  }\n}\n\n.wrapper {\n  width: 450px;\n  font-size: 14px;\n  line-height: 16.8px;\n}\n\n.searchInputContainer {\n  padding: 16px;\n\n  .searchInput {\n    border-radius: 4px !important;\n  }\n}\n\n.tabsContainer {\n  padding: 0 16px;\n}\n\n.emptyMsg {\n  padding: 20px;\n  text-align: center;\n  font-size: 14px !important;\n  line-height: 16.8px !important;\n  color: var(--euiTextSubduedColor) !important;\n}\n\n.listContainer {\n  @include eui.scrollBar;\n  max-height: 160px;\n  overflow-y: auto;\n  .item {\n    padding-left:10px !important;\n    font-size: 14px !important;\n    line-height: 16.8px !important;\n    color: var(--euiTextSubduedColor) !important;\n  }\n\n  .loading {\n    margin-right: 8px;\n    border-color: var(--separatorDropdownColor) !important;\n    border-top-color: var(--euiColorGhost) !important;\n  }\n}\n\n.listContainer {\n  :global(.euiListGroupItem-isActive), :global(.euiListGroupItem:hover),\n  :global(.RI-list-group-item.isActive), :global(.RI-list-group-item:hover) {\n    background-color: var(--hoverInListColorDarken) !important;\n    color: var(--euiTextSubduedColorHover) !important;\n    border-left-color: var(--externalLinkColor) !important;\n    border-left-width: 3px !important;\n    border-left-style: solid !important;\n    text-decoration: none !important;\n  }\n}\n\n.footerContainer {\n  padding: 12px 16px;\n  .homePageLink {\n    color: var(--euiTextSubduedColor) !important;\n    font-size: 14px !important;\n    cursor: pointer;\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/user-profile/CloudUserProfile.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { cloudUserProfileSelector } from 'uiSrc/slices/user/cloud-user-profile'\nimport UserProfileBadge from 'uiSrc/components/instance-header/components/user-profile/UserProfileBadge'\n\nexport const CloudUserProfile = () => {\n  const { data, error } = useSelector(cloudUserProfileSelector)\n  if (!data?.name) {\n    return null\n  }\n\n  return (\n    <UserProfileBadge\n      error={error}\n      data={data}\n      data-testid=\"cloud-user-profile-badge\"\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport { CloudUser } from 'src/modules/cloud/user/models'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport UserProfile from 'uiSrc/components/instance-header/components/user-profile/UserProfile'\n\nconst initialMockUser: CloudUser = {\n  id: 123,\n  name: 'John Smith',\n  currentAccountId: 45,\n  accounts: [\n    {\n      id: 45,\n      name: 'Account 1',\n    },\n    {\n      id: 46,\n      name: 'Account 2',\n    },\n  ],\n  data: {},\n}\n\ntype MockStoreStateProps = {\n  envDependent?: boolean\n  cloudSso?: boolean\n  cloudAds?: boolean\n  mockUser?: CloudUser\n}\n\nconst mockStoreStateWithFlags = ({\n  envDependent = true,\n  cloudSso = true,\n  cloudAds = true,\n  mockUser = initialMockUser,\n}: MockStoreStateProps = {}) => {\n  const keys = ['envDependent', 'cloudSso', 'cloudAds']\n  const values = [envDependent, cloudSso, cloudAds]\n\n  const initialStoreState = cloneDeep(initialStateDefault)\n\n  for (let i = 0; i < keys.length; i++) {\n    set(\n      initialStoreState,\n      `app.features.featureFlags.features.${FeatureFlags[keys[i] as keyof typeof FeatureFlags]}`,\n      { flag: values[i] },\n    )\n  }\n\n  set(initialStoreState, 'user.cloudProfile', {\n    error: '',\n    data: mockUser,\n  })\n  set(initialStoreState, 'oauth.cloud.user', {\n    error: '',\n    loading: false,\n    initialLoading: false,\n    data: mockUser,\n  })\n\n  return mockStore(initialStoreState)\n}\n\ndescribe('UserProfile', () => {\n  let store: typeof mockedStore\n  beforeEach(() => {\n    cleanup()\n    store = mockStoreStateWithFlags()\n  })\n\n  it('should render CloudUserProfile if envDependentFeature is disabled', () => {\n    store = mockStoreStateWithFlags({ envDependent: false })\n    const { getByTestId } = render(<UserProfile />, {\n      store,\n    })\n\n    expect(getByTestId('cloud-user-profile-badge')).toBeInTheDocument()\n  })\n\n  it('should not show cloud user profile badge profile name is empty', () => {\n    store = mockStoreStateWithFlags({\n      envDependent: false,\n      mockUser: {\n        ...initialMockUser,\n        name: '',\n      },\n    })\n    const { queryByTestId } = render(<UserProfile />, {\n      store,\n    })\n\n    expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument()\n  })\n\n  it('should render OAuthUserProfile if envDependentFeature is enabled', () => {\n    store = mockStoreStateWithFlags()\n    const { queryByTestId } = render(<UserProfile />, {\n      store,\n    })\n\n    expect(queryByTestId('oauth-user-profile-badge')).toBeInTheDocument()\n  })\n\n  it('should render nothing when envDependent=true, cloudAds=false, cloudSso=true', () => {\n    store = mockStoreStateWithFlags({\n      envDependent: true,\n      cloudAds: false,\n      cloudSso: true,\n    })\n    const { queryByTestId } = render(<UserProfile />, {\n      store,\n    })\n\n    expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument()\n    expect(queryByTestId('oauth-user-profile-badge')).not.toBeInTheDocument()\n  })\n\n  it('should render nothing when envDependent=true, cloudAds=true, cloudSso=false', () => {\n    store = mockStoreStateWithFlags({\n      envDependent: true,\n      cloudAds: true,\n      cloudSso: false,\n    })\n    const { queryByTestId } = render(<UserProfile />, {\n      store,\n    })\n\n    expect(queryByTestId('cloud-user-profile-badge')).not.toBeInTheDocument()\n    expect(queryByTestId('oauth-user-profile-badge')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/user-profile/UserProfile.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { OAuthUserProfile } from 'uiSrc/components'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { CloudUserProfile } from './CloudUserProfile'\n\nconst UserProfile = () => {\n  const {\n    [FeatureFlags.envDependent]: envDependentFeature,\n    [FeatureFlags.cloudAds]: cloudAds,\n    [FeatureFlags.cloudSso]: cloudSso,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n\n  if (!envDependentFeature?.flag) {\n    return (\n      <FlexItem style={{ marginLeft: 16 }}>\n        <CloudUserProfile />\n      </FlexItem>\n    )\n  }\n\n  if (cloudAds?.flag && cloudSso?.flag) {\n    return (\n      <FlexItem style={{ marginLeft: 16 }}>\n        <OAuthUserProfile source={OAuthSocialSource.UserProfile} />\n      </FlexItem>\n    )\n  }\n\n  return null\n}\n\nexport default UserProfile\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.spec.tsx",
    "content": "import React from 'react'\nimport { CloudUser } from 'src/modules/cloud/user/models'\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n  within,\n} from 'uiSrc/utils/test-utils'\nimport * as appFeaturesSlice from 'uiSrc/slices/app/features'\nimport UserProfileBadge, { UserProfileBadgeProps } from './UserProfileBadge'\n\nconst mockUser: CloudUser = {\n  id: 123,\n  name: 'John Smith',\n  currentAccountId: 45,\n  accounts: [\n    {\n      id: 45,\n      name: 'Account 1',\n    },\n    {\n      id: 46,\n      name: 'Account 2',\n    },\n  ],\n  data: {},\n}\n\nconst TEST_IDS = {\n  badge: 'test-user-profile-badge',\n  profileTitle: 'profile-title',\n  accountsList: 'user-profile-popover-accounts',\n  selectedAccountCheckmark: (accountId: number) =>\n    `user-profile-selected-account-${accountId}`,\n  selectingAccountSpinner: (accountId: number) =>\n    `user-profile-selecting-account-${accountId}`,\n  cloudAdminConsoleLink: 'cloud-admin-console-link',\n  importCloudDatabases: 'profile-import-cloud-databases',\n  logoutButton: 'profile-logout',\n  accountFullName: 'account-full-name',\n  cloudConsoleLink: 'cloud-console-link',\n}\n\njest.mock('uiSrc/config', () => ({\n  getConfig: jest.fn(() => {\n    const { getConfig: actualGetConfig } = jest.requireActual('uiSrc/config')\n    const actualConfig = actualGetConfig()\n    return {\n      ...actualConfig,\n      app: {\n        ...actualConfig.app,\n        smConsoleRedirect: 'https://foo.bar',\n      },\n    }\n  }),\n}))\n\nconst mockFeatureFlags = (envDependent = true) => {\n  jest\n    .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n    .mockReturnValue({\n      envDependent: {\n        flag: envDependent,\n      },\n    })\n}\n\ndescribe('UserProfileBadge', () => {\n  let handleClickSelectAccount: jest.Mock\n  let handleClickCloudAccount: jest.Mock\n  const selectingAccountId = undefined\n  let renderUserProfileBadge: (\n    props?: Partial<UserProfileBadgeProps>,\n  ) => ReturnType<typeof render>\n  let renderAndOpenUserProfileBadge: (\n    props?: Partial<UserProfileBadgeProps>,\n  ) => Promise<ReturnType<typeof render>>\n\n  beforeEach(() => {\n    mockFeatureFlags()\n    handleClickSelectAccount = jest.fn()\n    handleClickCloudAccount = jest.fn()\n\n    renderUserProfileBadge = (props?: Partial<UserProfileBadgeProps>) =>\n      render(\n        <UserProfileBadge\n          data={mockUser}\n          error={null}\n          handleClickSelectAccount={handleClickSelectAccount}\n          handleClickCloudAccount={handleClickCloudAccount}\n          selectingAccountId={selectingAccountId}\n          data-testid={TEST_IDS.badge}\n          {...props}\n        />,\n      )\n\n    renderAndOpenUserProfileBadge = async (\n      props?: Partial<UserProfileBadgeProps>,\n    ) => {\n      const resp = renderUserProfileBadge(props)\n\n      await act(async () => {\n        fireEvent.click(screen.getByTestId('user-profile-btn'))\n      })\n      await waitForRiPopoverVisible()\n\n      return resp\n    }\n  })\n\n  it('should show button with user initials if data is present', () => {\n    const { queryByRole } = renderUserProfileBadge()\n    expect(queryByRole('presentation')).toHaveTextContent('JS')\n  })\n\n  it('should not render anything if data is absent', () => {\n    const { container } = renderUserProfileBadge({ data: null })\n    expect(container).toBeEmptyDOMElement()\n  })\n\n  it('should not render anything if error is provided', () => {\n    const { container } = renderUserProfileBadge({ error: 'An error occurred' })\n    expect(container).toBeEmptyDOMElement()\n  })\n\n  it('should show expected header when envDependent flag is enabled', async () => {\n    const { getByTestId } = await renderAndOpenUserProfileBadge()\n    expect(getByTestId(TEST_IDS.profileTitle)).toHaveTextContent(\n      'Redis Cloud account',\n    )\n  })\n\n  it('should show expected header when envDependent flag is disabled', async () => {\n    mockFeatureFlags(false)\n    const { getByTestId } = await renderAndOpenUserProfileBadge()\n    expect(getByTestId(TEST_IDS.profileTitle)).toHaveTextContent('Account')\n  })\n\n  it('should show available accounts and selected account', async () => {\n    const { getByTestId } = await renderAndOpenUserProfileBadge()\n\n    // eslint-disable-next-line no-restricted-syntax\n    for (const account of mockUser.accounts ?? []) {\n      const testId = `profile-account-${account.id}${account.id === mockUser.currentAccountId ? '-selected' : ''}`\n      const accountElement = getByTestId(testId)\n      expect(accountElement).toHaveTextContent(account.name)\n\n      // checkbox icon for selected account\n      if (account.id === mockUser.currentAccountId) {\n        expect(\n          within(accountElement).queryByTestId(\n            TEST_IDS.selectedAccountCheckmark(account.id),\n          ),\n        ).toBeInTheDocument()\n      } else {\n        expect(\n          within(accountElement).queryByTestId(\n            TEST_IDS.selectedAccountCheckmark(account.id),\n          ),\n        ).not.toBeInTheDocument()\n      }\n\n      // click on account\n      // eslint-disable-next-line no-await-in-loop\n      await act(async () => {\n        fireEvent.click(accountElement)\n      })\n      expect(handleClickSelectAccount).toHaveBeenCalledTimes(1)\n      expect(handleClickSelectAccount).toHaveBeenCalledWith(account.id)\n      handleClickSelectAccount.mockReset()\n    }\n  })\n\n  it('should show spinner next to account when selectedAccountId is provided', async () => {\n    const selectingAccountId = 46\n    const { getByTestId } = await renderAndOpenUserProfileBadge({\n      selectingAccountId,\n    })\n\n    mockUser.accounts?.forEach((account) => {\n      const testId = `profile-account-${account.id}${account.id === mockUser.currentAccountId ? '-selected' : ''}`\n      const accountElement = getByTestId(testId)\n      expect(accountElement).toHaveTextContent(account.name)\n\n      // spinner for selecting account\n      if (account.id === selectingAccountId) {\n        expect(\n          within(accountElement).queryByTestId(\n            TEST_IDS.selectingAccountSpinner(account.id),\n          ),\n        ).toBeInTheDocument()\n      } else {\n        expect(\n          within(accountElement).queryByTestId(\n            TEST_IDS.selectingAccountSpinner(account.id),\n          ),\n        ).not.toBeInTheDocument()\n      }\n    })\n  })\n\n  it('should show expected links when envDependent flag is disabled', async () => {\n    mockFeatureFlags(false)\n    const { getByTestId, queryByTestId } = await renderAndOpenUserProfileBadge()\n\n    const link = getByTestId(TEST_IDS.cloudAdminConsoleLink)\n    expect(link).toHaveAttribute('href', 'https://foo.bar')\n    expect(link).toHaveTextContent('Back to Redis Cloud Admin console')\n\n    expect(queryByTestId(TEST_IDS.importCloudDatabases)).not.toBeInTheDocument()\n    expect(queryByTestId(TEST_IDS.logoutButton)).not.toBeInTheDocument()\n    expect(queryByTestId(TEST_IDS.accountFullName)).not.toBeInTheDocument()\n    expect(queryByTestId(TEST_IDS.cloudConsoleLink)).not.toBeInTheDocument()\n  })\n\n  it('should show expected links when envDependent flag is enabled', async () => {\n    mockFeatureFlags()\n    const { getByTestId, queryByTestId } = await renderAndOpenUserProfileBadge()\n\n    expect(\n      queryByTestId(TEST_IDS.cloudAdminConsoleLink),\n    ).not.toBeInTheDocument()\n    expect(getByTestId(TEST_IDS.importCloudDatabases)).toBeInTheDocument()\n    expect(getByTestId(TEST_IDS.logoutButton)).toBeInTheDocument()\n    expect(getByTestId(TEST_IDS.accountFullName)).toBeInTheDocument()\n    expect(getByTestId(TEST_IDS.cloudConsoleLink)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/components/user-profile/UserProfileBadge.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { logoutUserAction } from 'uiSrc/slices/oauth/cloud'\n\nimport { buildRedisInsightUrl, getUtmExternalLink } from 'uiSrc/utils/links'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  fetchSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { getConfig } from 'uiSrc/config'\nimport { CloudUser } from 'apiSrc/modules/cloud/user/models'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { DownloadIcon, SignoutIcon } from '@redis-ui/icons'\nimport { Menu } from 'uiSrc/components/base/layout/menu'\nimport { ProfileIcon } from 'uiSrc/components/base/layout/profile-icon/ProfileIcon'\nimport Loader from 'uiSrc/components/base/display/loader/Loader'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\n\nexport interface UserProfileBadgeProps {\n  'data-testid'?: string\n  error: Nullable<string>\n  data: Nullable<CloudUser>\n  handleClickSelectAccount?: (id: number) => void\n  handleClickCloudAccount?: () => void\n  selectingAccountId?: number\n}\n\nconst riConfig = getConfig()\n\nconst UserProfileBadge = (props: UserProfileBadgeProps) => {\n  const {\n    error,\n    data,\n    handleClickSelectAccount,\n    handleClickCloudAccount,\n    selectingAccountId,\n    'data-testid': dataTestId,\n  } = props\n\n  const connectedInstance = useSelector(connectedInstanceSelector)\n\n  const riDesktopLink = buildRedisInsightUrl(connectedInstance)\n\n  const [isProfileOpen, setIsProfileOpen] = useState(false)\n  const [isImportLoading, setIsImportLoading] = useState(false)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  if (!data || error) {\n    return null\n  }\n\n  const handleClickImport = () => {\n    if (isImportLoading) return\n\n    setIsImportLoading(true)\n    dispatch(setSSOFlow(OAuthSocialAction.Import))\n    dispatch(\n      fetchSubscriptionsRedisCloud(\n        null,\n        true,\n        () => {\n          history.push(Pages.redisCloudSubscriptions)\n          setIsImportLoading(false)\n        },\n        () => setIsImportLoading(false),\n      ),\n    )\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED,\n      eventData: {\n        source: OAuthSocialSource.UserProfile,\n      },\n    })\n  }\n\n  const handleClickLogout = () => {\n    setIsProfileOpen(false)\n    dispatch(\n      logoutUserAction(() => {\n        sendEventTelemetry({\n          event: TelemetryEvent.CLOUD_SIGN_OUT_CLICKED,\n        })\n      }),\n    )\n  }\n\n  const handleToggleProfile = () => {\n    if (!isProfileOpen) {\n      sendEventTelemetry({\n        event: TelemetryEvent.CLOUD_PROFILE_OPENED,\n      })\n    }\n    setIsProfileOpen((v) => !v)\n  }\n\n  const { accounts, currentAccountId, name } = data\n\n  return (\n    <OutsideClickDetector onOutsideClick={() => setIsProfileOpen(false)}>\n      <div data-testid={dataTestId}>\n        <Menu open={isProfileOpen}>\n          <Menu.Trigger\n            withButton\n            onClick={handleToggleProfile}\n            data-testid=\"user-profile-btn\"\n          >\n            <ProfileIcon\n              size=\"L\"\n              fullName={name}\n              role=\"presentation\"\n              style={{ cursor: 'pointer' }}\n            />\n          </Menu.Trigger>\n          <Menu.Content\n            data-testid=\"user-profile-popover-content\"\n            style={{ minWidth: 330 }}\n          >\n            <FeatureFlagComponent\n              name={FeatureFlags.envDependent}\n              otherwise={\n                <Menu.Content.Label\n                  text=\"Account\"\n                  data-testid=\"profile-title\"\n                />\n              }\n            >\n              <Menu.Content.Label\n                text=\"Redis Cloud account\"\n                data-testid=\"profile-title\"\n              />\n            </FeatureFlagComponent>\n            <Menu.Content.Separator />\n            <div data-testid=\"user-profile-popover-accounts\">\n              {accounts?.map(({ name, id }) => (\n                <Menu.Content.Item.Compose\n                  role=\"presentation\"\n                  key={id}\n                  selected={id === selectingAccountId}\n                  onClick={() => handleClickSelectAccount?.(id)}\n                  data-testid={`profile-account-${id}${id === currentAccountId ? '-selected' : ''}`}\n                >\n                  <Menu.Content.Item.Text>\n                    {`${name} #${id}`}\n                  </Menu.Content.Item.Text>\n                  {id === selectingAccountId && (\n                    <Loader\n                      size=\"m\"\n                      data-testid={`user-profile-selecting-account-${id}`}\n                    />\n                  )}\n                  {id === currentAccountId && (\n                    <Menu.Content.Item.Check\n                      data-testid={`user-profile-selected-account-${id}`}\n                    />\n                  )}\n                </Menu.Content.Item.Compose>\n              ))}\n            </div>\n            <Menu.Content.Separator />\n            <FeatureFlagComponent\n              name={FeatureFlags.envDependent}\n              otherwise={\n                <>\n                  <Menu.Content.Item.Compose>\n                    <Menu.Content.Item.Text>\n                      <Link\n                        external\n                        color=\"text\"\n                        href={riDesktopLink}\n                        data-testid=\"open-ri-desktop-link\"\n                        variant=\"inline\"\n                      >\n                        Open in Redis Insight Desktop version\n                      </Link>\n                    </Menu.Content.Item.Text>\n                  </Menu.Content.Item.Compose>\n                  <Menu.Content.Item.Compose>\n                    <Menu.Content.Item.Text>\n                      <Link\n                        external\n                        color=\"text\"\n                        variant=\"inline\"\n                        target=\"_blank\"\n                        href={riConfig.app.smConsoleRedirect}\n                        data-testid=\"cloud-admin-console-link\"\n                      >\n                        Back to Redis Cloud Admin console\n                      </Link>\n                    </Menu.Content.Item.Text>\n                  </Menu.Content.Item.Compose>\n                </>\n              }\n            >\n              <Menu.Content.Item\n                role=\"presentation\"\n                subHead=\"Import Cloud Databases\"\n                text=\"\"\n                icon={DownloadIcon}\n                onSelect={handleClickImport}\n                data-testid=\"profile-import-cloud-databases\"\n              />\n              <Menu.Content.Item.Compose>\n                <Menu.Content.Item.Text>\n                  <Link\n                    external\n                    color=\"text\"\n                    variant=\"inline\"\n                    target=\"_blank\"\n                    href={getUtmExternalLink(EXTERNAL_LINKS.cloudConsole, {\n                      campaign: 'cloud_account',\n                    })}\n                    onClick={handleClickCloudAccount}\n                    data-testid=\"cloud-console-link\"\n                  >\n                    Cloud Console:\n                    <span data-testid=\"account-full-name\"> {name}</span>\n                  </Link>\n                </Menu.Content.Item.Text>\n              </Menu.Content.Item.Compose>\n              <Menu.Content.Separator />\n              <Menu.Content.Item\n                role=\"presentation\"\n                subHead=\"Logout\"\n                text=\"\"\n                icon={SignoutIcon}\n                onClick={handleClickLogout}\n                data-testid=\"profile-logout\"\n              />\n            </FeatureFlagComponent>\n            <Menu.Content.DropdownArrow />\n          </Menu.Content>\n        </Menu>\n      </div>\n    </OutsideClickDetector>\n  )\n}\n\nexport default UserProfileBadge\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/index.ts",
    "content": "import InstanceHeader from './InstanceHeader'\n\nexport default InstanceHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/instance-header/styles.module.scss",
    "content": ".container {\n  padding: 0 16px;\n  height: 70px;\n}\n\n.breadcrumbsContainer {\n  height: 100%;\n  display: flex;\n  align-items: center;\n\n  & > div {\n    display: flex;\n  }\n\n  .breadCrumbLink {\n    color: var(--euiColorPrimary) !important;\n    font-size: 14px !important;\n    line-height: 20px;\n    font-weight: 400;\n    cursor: pointer;\n    text-decoration: underline;\n\n    &:hover {\n      text-decoration: none;\n    }\n  }\n}\n\n.tooltip {\n  max-width: 400px !important;\n}\n\n.tooltipAnchor {\n  max-width: 100%;\n  display: inline-flex;\n\n  .infoIcon {\n    transition: color ease .3s;\n  }\n\n  &:hover {\n    .infoIcon {\n      color: currentColor;\n    }\n  }\n}\n\n.dbName {\n  display: inline-block !important;\n  overflow: hidden;\n  font-size: 14px;\n  line-height: 20px;\n  font-weight: 400;\n  text-overflow: ellipsis;\n  max-width: 100%;\n  white-space: nowrap;\n}\n\n.infoIcon {\n  color: var(--euiColorMediumShade);\n}\n\n.buttonDbIndex {\n  height: 32px !important;\n}\n\n.controls {\n  height: 32px !important;\n}\n\n.dbIndexInput {\n  width: 60px !important;\n  height: 32px !important;\n  border-color: transparent !important;\n  border-bottom-right-radius: 0 !important;\n  border-top-right-radius: 0 !important;\n}\n\n.divider {\n  color: var(--euiTextSubduedColor);\n  font-size: 14px;\n  line-height: 20px;\n  font-weight: 400;\n  margin: 0 8px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/action-bar/ActionBar.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, screen, render } from 'uiSrc/utils/test-utils'\nimport ActionBar, { Props } from './ActionBar'\n\nconst mockedProps = mock<Props>()\n\ndescribe('ActionBar', () => {\n  it('should render', () => {\n    expect(render(<ActionBar {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call \"onCloseActionBar\"', () => {\n    const handleClick = jest.fn()\n\n    const renderer = render(\n      <ActionBar {...instance(mockedProps)} onCloseActionBar={handleClick} />,\n    )\n\n    expect(renderer).toBeTruthy()\n\n    fireEvent.click(screen.getByTestId('cancel-selecting'))\n    expect(handleClick).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/action-bar/ActionBar.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Row)`\n  position: fixed;\n  z-index: 1;\n  left: 50%;\n  transform: translate(-50%, -50%);\n\n  width: 462px;\n  height: 50px;\n  bottom: calc(9vh + 9px);\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral500};\n  border-radius: ${({ theme }) => theme.core.space.space250};\n  padding-left: ${({ theme }) => theme.core.space.space050};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/action-bar/ActionBar.tsx",
    "content": "import React from 'react'\n\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { Container } from './ActionBar.styles'\n\nexport interface Props {\n  width?: number\n  selectionCount: number\n  actions: (JSX.Element | null)[]\n  onCloseActionBar: () => void\n}\n\nconst ActionBar = ({ selectionCount, actions, onCloseActionBar }: Props) => (\n  <Container centered gap=\"l\">\n    <FlexItem>{`You selected: ${selectionCount} items`}</FlexItem>\n    {actions?.map((action, index) => (\n      <FlexItem key={`action-${index + 1}`}>{action}</FlexItem>\n    ))}\n    <FlexItem>\n      <IconButton\n        icon={CancelSlimIcon}\n        aria-label=\"Cancel selecting\"\n        onClick={() => onCloseActionBar()}\n        data-testid=\"cancel-selecting\"\n      />\n    </FlexItem>\n  </Container>\n)\n\nexport default ActionBar\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/delete-action/DeleteAction.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { INSTANCES_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport DeleteAction from './DeleteAction'\n\ndescribe('DeleteAction', () => {\n  it('should render', () => {\n    expect(\n      render(<DeleteAction subTitle=\"\" selection={[]} onDelete={jest.fn()} />),\n    ).toBeTruthy()\n  })\n\n  it('should display instances that are going to be deleted', () => {\n    const onDelete = jest.fn()\n    render(\n      <DeleteAction\n        subTitle=\"\"\n        selection={INSTANCES_MOCK}\n        onDelete={onDelete}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('delete-btn'))\n\n    expect(screen.getByText('localhost')).toBeInTheDocument()\n    expect(screen.getByText('oea123123')).toBeInTheDocument()\n    expect(screen.getByText('sentinel')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/delete-action/DeleteAction.tsx",
    "content": "import React, { useState } from 'react'\nimport { formatLongName } from 'uiSrc/utils'\n\nimport {\n  DestructiveButton,\n  PrimaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from '../styles.module.scss'\n\nexport interface Props<T> {\n  selection: T[]\n  onDelete: () => void\n  subTitle: string\n}\n\nconst DeleteAction = <T extends { id: string; name?: string }>(\n  props: Props<T>,\n) => {\n  const { selection, onDelete, subTitle } = props\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  const onButtonClick = () => {\n    setIsPopoverOpen((prevState) => !prevState)\n  }\n\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const deleteBtn = (\n    <PrimaryButton\n      size=\"small\"\n      onClick={onButtonClick}\n      icon={DeleteIcon}\n      className={styles.actionBtn}\n      data-testid=\"delete-btn\"\n    >\n      Delete\n    </PrimaryButton>\n  )\n\n  return (\n    <RiPopover\n      id=\"deletePopover\"\n      ownFocus\n      button={deleteBtn}\n      isOpen={isPopoverOpen}\n      closePopover={closePopover}\n      panelPaddingSize=\"l\"\n      data-testid=\"delete-popover\"\n    >\n      <Text size=\"m\" className={styles.popoverSubTitle}>\n        {subTitle}\n      </Text>\n      <div className={styles.boxSection}>\n        {selection.map((select) => (\n          <Row key={select.id} gap=\"s\" className={styles.nameList}>\n            <FlexItem>\n              <RiIcon type=\"CheckThinIcon\" />\n            </FlexItem>\n            <FlexItem grow className={styles.nameListText}>\n              <span>{formatLongName(select.name)}</span>\n            </FlexItem>\n          </Row>\n        ))}\n      </div>\n      <div className={styles.popoverFooter}>\n        <DestructiveButton\n          size=\"small\"\n          icon={DeleteIcon}\n          onClick={() => {\n            closePopover()\n            onDelete()\n          }}\n          className={styles.popoverDeleteBtn}\n          data-testid=\"delete-selected-dbs\"\n        >\n          Delete\n        </DestructiveButton>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default DeleteAction\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/export-action/ExportAction.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { INSTANCES_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport ExportAction from './ExportAction'\n\ndescribe('ExportAction', () => {\n  it('should render', () => {\n    expect(\n      render(<ExportAction subTitle=\"\" selection={[]} onExport={jest.fn()} />),\n    ).toBeTruthy()\n  })\n\n  it('should call onExport with proper data', () => {\n    const onExport = jest.fn()\n    render(\n      <ExportAction\n        subTitle=\"\"\n        selection={INSTANCES_MOCK}\n        onExport={onExport}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('export-btn'))\n    fireEvent.click(screen.getByTestId('export-selected-dbs'))\n\n    expect(onExport).toBeCalledWith(INSTANCES_MOCK, true)\n  })\n\n  it('should call onExport with proper data', () => {\n    const onExport = jest.fn()\n    render(\n      <ExportAction\n        subTitle=\"\"\n        selection={INSTANCES_MOCK}\n        onExport={onExport}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('export-btn'))\n    fireEvent.click(screen.getByTestId('export-passwords'))\n\n    fireEvent.click(screen.getByTestId('export-selected-dbs'))\n\n    expect(onExport).toBeCalledWith(INSTANCES_MOCK, false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/export-action/ExportAction.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { formatLongName } from 'uiSrc/utils'\n\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { ExportIcon } from 'uiSrc/components/base/icons'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from '../styles.module.scss'\n\nexport interface Props<T> {\n  selection: T[]\n  onExport: (instances: T[], withSecrets: boolean) => void\n  subTitle: string\n}\n\nconst ExportAction = <T extends { id: string; name?: string }>(\n  props: Props<T>,\n) => {\n  const { selection, onExport, subTitle } = props\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const [withSecrets, setWithSecrets] = useState(true)\n\n  const exportBtn = (\n    <PrimaryButton\n      onClick={() => setIsPopoverOpen((prevState) => !prevState)}\n      size=\"small\"\n      icon={ExportIcon}\n      className={styles.actionBtn}\n      data-testid=\"export-btn\"\n    >\n      Export\n    </PrimaryButton>\n  )\n\n  return (\n    <RiPopover\n      id=\"exportPopover\"\n      ownFocus\n      button={exportBtn}\n      isOpen={isPopoverOpen}\n      closePopover={() => setIsPopoverOpen(false)}\n      panelPaddingSize=\"l\"\n      data-testid=\"export-popover\"\n    >\n      <Text size=\"m\" className={styles.popoverSubTitle}>\n        {subTitle}\n      </Text>\n      <div className={styles.boxSection}>\n        {selection.map((select) => (\n          <Row key={select.id} gap=\"s\" className={styles.nameList}>\n            <FlexItem>\n              <RiIcon type=\"CheckThinIcon\" />\n            </FlexItem>\n            <FlexItem grow className={styles.nameListText}>\n              <span>{formatLongName(select.name)}</span>\n            </FlexItem>\n          </Row>\n        ))}\n      </div>\n      <FormField style={{ marginTop: 16 }}>\n        <Checkbox\n          id=\"export-passwords\"\n          name=\"export-passwords\"\n          label=\"Export passwords\"\n          checked={withSecrets}\n          onChange={(e) => setWithSecrets(e.target.checked)}\n          data-testid=\"export-passwords\"\n        />\n      </FormField>\n      <div className={styles.popoverFooter}>\n        <PrimaryButton\n          size=\"small\"\n          icon={ExportIcon}\n          onClick={() => {\n            setIsPopoverOpen(false)\n            onExport(selection, withSecrets)\n          }}\n          data-testid=\"export-selected-dbs\"\n        >\n          Export\n        </PrimaryButton>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default ExportAction\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/index.ts",
    "content": "import DeleteAction from './delete-action/DeleteAction'\nimport ExportAction from './export-action/ExportAction'\nimport ActionBar from './action-bar/ActionBar'\n\nexport { DeleteAction, ExportAction, ActionBar }\n"
  },
  {
    "path": "redisinsight/ui/src/components/item-list/components/styles.module.scss",
    "content": ".popoverSubTitle {\n  width: 372px;\n  color: var(--textColorShade) !important;\n  line-height: 1.3;\n  font-size: 14px;\n}\n\n.nameList:not(:last-child) {\n  padding-bottom: 7px;\n}\n\n.nameListText {\n  line-height: 22px;\n}\n\n.boxSection {\n  @include eui.scrollBar;\n\n  width: 400px;\n  max-height: 189px;\n  overflow-y: scroll;\n\n  padding: 13px 15px;\n  margin-top: 10px;\n\n  color: var(--textColorShade) !important;\n  background-color: var(--euiColorLightestShade) !important;\n\n  svg {\n    color: var(--euiTextColor) !important;\n    width: 22px !important;\n    height: 22px !important;\n  }\n}\n\n.popoverFooter {\n  text-align: right;\n  margin-top: 24px;\n\n  span {\n    font-size: 13px !important;\n  }\n}\n\n.noResults {\n  display: flex;\n\n  height: calc(100vh - 315px);\n  align-items: center;\n  flex-direction: column;\n  justify-content: center;\n\n  @media (min-width: 768px) and (max-width: 1100px) {\n    height: calc(100vh - 223px);\n  }\n\n  @media (min-width: 1101px) {\n    height: calc(100vh - 248px);\n  }\n}\n\n.actionBtn {\n  min-width: 93px !important;\n\n  &:focus {\n    text-decoration: none !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/JSONViewer.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport JSONViewer from './JSONViewer'\n\ndescribe('JSONViewer', () => {\n  it('should render proper json', () => {\n    const jsx = JSONViewer({ value: JSON.stringify({}) })\n    render(jsx.value as React.ReactElement)\n\n    expect(jsx.isValid).toBeTruthy()\n    expect(screen.queryByTestId('value-as-json')).toBeInTheDocument()\n  })\n\n  it('should not render invalid json', () => {\n    const jsx = JSONViewer({ value: 'zxc' })\n\n    expect(jsx.value).toEqual('zxc')\n    expect(jsx.isValid).toBeFalsy()\n  })\n\n  it('should parse json with \"constructor\" key without error', () => {\n    const value = JSON.stringify({\n      constructor: 'value',\n      nested: { constructor: 123 },\n    })\n    const jsx = JSONViewer({ value })\n\n    expect(jsx.isValid).toBeTruthy()\n  })\n\n  it('should parse json with \"__proto__\" key without error', () => {\n    const value = '{\"__proto__\": \"value\"}'\n    const jsx = JSONViewer({ value })\n\n    expect(jsx.isValid).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/JSONViewer.tsx",
    "content": "import cx from 'classnames'\nimport React from 'react'\nimport JSONBigInt from 'json-bigint'\n\nimport JsonPretty from 'uiSrc/components/json-viewer/components/json-pretty'\nimport { formatLongName } from 'uiSrc/utils'\nimport { TOOLTIP_CONTENT_MAX_LENGTH } from 'uiSrc/constants'\n\ninterface Props {\n  value: string\n  expanded?: boolean\n  space?: number\n  useNativeBigInt?: boolean\n  tooltip?: boolean\n}\n\nconst JSONViewer = (props: Props) => {\n  const {\n    value,\n    expanded = false,\n    space = 2,\n    useNativeBigInt = true,\n    tooltip = false,\n  } = props\n\n  try {\n    const className = cx('jsonViewer', {\n      'jsonViewer-collapsed': !expanded && !tooltip,\n    })\n    const data = JSONBigInt({\n      useNativeBigInt,\n      protoAction: 'preserve',\n      constructorAction: 'preserve',\n    }).parse(value)\n\n    if (tooltip && value?.length > TOOLTIP_CONTENT_MAX_LENGTH) {\n      return { value: formatLongName(value), isValid: true }\n    }\n\n    return {\n      value: (\n        <div className={className} data-testid=\"value-as-json\">\n          <JsonPretty data={data} space={space} />\n        </div>\n      ),\n      isValid: true,\n    }\n  } catch (e) {\n    return {\n      value,\n      isValid: false,\n    }\n  }\n}\n\nexport default JSONViewer\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-array/JsonArray.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport JsonArray from './JsonArray'\n\nconst mockArray = [123]\n\ndescribe('JsonArray', () => {\n  it('should render JsonArray', () => {\n    expect(render(<JsonArray data={mockArray} />)).toBeTruthy()\n  })\n\n  it('should render jsonObjectComponent', () => {\n    render(<JsonArray data={mockArray} gap={8} />)\n\n    expect(screen.getByTestId('json-array-component')).toHaveTextContent(\n      '[ 123 ]',\n    )\n  })\n\n  it('should render coma', () => {\n    render(<JsonArray data={mockArray} lastElement={false} />)\n\n    expect(screen.getByTestId('json-array-component')).toHaveTextContent(\n      '[ 123 ],',\n    )\n  })\n\n  it('should not render coma', () => {\n    render(<JsonArray data={mockArray} lastElement />)\n\n    expect(screen.getByTestId('json-array-component')).toHaveTextContent(\n      '[ 123 ]',\n    )\n  })\n\n  it('should not render empty space and line break', () => {\n    render(<JsonArray data={[]} lastElement />)\n\n    expect(screen.getByTestId('json-array-component')).toHaveTextContent('[', {\n      normalizeWhitespace: false,\n    })\n  })\n\n  it('should render empty space and line break', () => {\n    const renderedArray = '[\\n  123\\n]'\n    render(<JsonArray data={mockArray} lastElement />)\n\n    expect(screen.getByTestId('json-array-component')).toHaveTextContent(\n      renderedArray,\n      { normalizeWhitespace: false },\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-array/JsonArray.tsx",
    "content": "import React, { Fragment } from 'react'\n\nimport JsonPretty from 'uiSrc/components/json-viewer/components/json-pretty'\nimport { IJsonArrayProps } from 'uiSrc/components/json-viewer/interfaces'\n\nconst JsonArray = ({\n  data,\n  space = 2,\n  gap = 0,\n  lastElement = true,\n}: IJsonArrayProps) => (\n  <span data-testid=\"json-array-component\">\n    [{!!data.length && '\\n'}\n    {data.map((value, idx) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <Fragment key={idx}>\n        {!!space && Array.from({ length: space + gap }, () => ' ')}\n        <JsonPretty\n          data={value}\n          lastElement={idx === data.length - 1}\n          space={space}\n          gap={gap + space}\n        />\n      </Fragment>\n    ))}\n    {!!data.length && !!gap && Array.from({ length: gap }, () => ' ')}]\n    {!lastElement && ','}\n    {'\\n'}\n  </span>\n)\n\nexport default JsonArray\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-array/index.ts",
    "content": "import JsonArray from './JsonArray'\n\nexport default JsonArray\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-object/JsonObject.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport JsonObject from './JsonObject'\n\nconst mockJson = { value: JSON.stringify({}) }\n\ndescribe('JsonObject', () => {\n  it('should render jsonObjectComponent', () => {\n    expect(render(<JsonObject data={mockJson} />)).toBeTruthy()\n  })\n\n  it('should render jsonObjectComponent', () => {\n    render(<JsonObject data={mockJson} gap={8} />)\n\n    expect(screen.getByTestId('json-object-component')).toHaveTextContent(\n      '{ \"value\": \"{}\" }',\n    )\n  })\n\n  it('should render coma', () => {\n    render(<JsonObject data={mockJson} lastElement={false} />)\n\n    expect(screen.getByTestId('json-object-component')).toHaveTextContent(\n      '{ \"value\": \"{}\" },',\n    )\n  })\n\n  it('should not render coma', () => {\n    render(<JsonObject data={mockJson} lastElement />)\n\n    expect(screen.getByTestId('json-object-component')).toHaveTextContent(\n      '{ \"value\": \"{}\" }',\n    )\n  })\n\n  it('should not render empty space and line break', () => {\n    render(<JsonObject data={{}} lastElement />)\n\n    expect(screen.getByTestId('json-object-component')).toHaveTextContent(\n      '{}',\n      { normalizeWhitespace: false },\n    )\n  })\n\n  it('should render empty space and line break', () => {\n    const renderedObject = '{\\n  \"value\": \"{}\"\\n}'\n    render(<JsonObject data={mockJson} lastElement />)\n\n    expect(screen.getByTestId('json-object-component')).toHaveTextContent(\n      renderedObject,\n      { normalizeWhitespace: false },\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-object/JsonObject.tsx",
    "content": "import React, { Fragment } from 'react'\n\nimport JsonPretty from 'uiSrc/components/json-viewer/components/json-pretty'\nimport { IJsonObjectProps } from 'uiSrc/components/json-viewer/interfaces'\n\nconst JsonObject = ({\n  data,\n  space = 2,\n  gap = 0,\n  lastElement = true,\n}: IJsonObjectProps) => {\n  const keys = Object.keys(data)\n\n  return (\n    <span data-testid=\"json-object-component\">\n      {'{'}\n      {!!keys.length && '\\n'}\n      {keys.map((key, idx) => (\n        <Fragment key={key}>\n          {!!space && Array.from({ length: space + gap }, () => ' ')}\n          <span className=\"json-pretty__key\">{`\"${key}\"`}</span>\n          {': '}\n          <JsonPretty\n            data={data[key]}\n            lastElement={idx === keys.length - 1}\n            space={space}\n            gap={gap + space}\n          />\n        </Fragment>\n      ))}\n      {!!keys.length && !!gap && Array.from({ length: gap }, () => ' ')}\n      {'}'}\n      {!lastElement && ','}\n      {'\\n'}\n    </span>\n  )\n}\n\nexport default JsonObject\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-object/index.ts",
    "content": "import JsonObject from './JsonObject'\n\nexport default JsonObject\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-pretty/JsonPretty.spec.tsx",
    "content": "import React from 'react'\nimport JSONBigInt from 'json-bigint'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport JsonPretty from './JsonPretty'\n\ndescribe('JsonPretty', () => {\n  it('should render jsonObjectComponent', () => {\n    const json = { value: JSON.stringify({}) }\n    render(<JsonPretty data={json} />)\n\n    expect(screen.getByTestId('json-object-component')).toBeInTheDocument()\n  })\n\n  it('should render json array component', () => {\n    const json = ['123']\n    render(<JsonPretty data={json} />)\n\n    expect(screen.getByTestId('json-array-component')).toBeInTheDocument()\n  })\n\n  it('should render json primitive component', () => {\n    const json = null\n    render(<JsonPretty data={json} />)\n\n    expect(screen.getByTestId('json-primitive-component')).toBeInTheDocument()\n  })\n\n  it('should render json primitive component with big number', () => {\n    const json = JSONBigInt({ useNativeBigInt: true }).parse(\n      '1234567890123456789012345678901234567890',\n    )\n    render(<JsonPretty data={json} />)\n    expect(screen.getByTestId('json-primitive-component')).toBeInTheDocument()\n  })\n\n  it('should render json primitive component with big float', () => {\n    const json = JSONBigInt({ useNativeBigInt: false }).parse(\n      '1234567890123456789012345678901234567890.1234567890123456789012345678901234567890',\n    )\n    render(<JsonPretty data={json} />)\n    expect(screen.getByTestId('json-primitive-component')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-pretty/JsonPretty.tsx",
    "content": "import React from 'react'\n\nimport JsonPrimitive from 'uiSrc/components/json-viewer/components/json-primitive'\nimport JsonArray from 'uiSrc/components/json-viewer/components/json-array'\nimport JsonObject from 'uiSrc/components/json-viewer/components/json-object'\nimport { isArray, isObject } from 'uiSrc/components/json-viewer/utils'\nimport { IDefaultProps } from 'uiSrc/components/json-viewer/interfaces'\n\nconst JsonPretty = ({ data, ...props }: IDefaultProps) => {\n  if (data?._isBigNumber) {\n    return <JsonPrimitive data={data} {...props} />\n  }\n\n  if (isArray(data)) {\n    return <JsonArray data={data} {...props} />\n  }\n\n  if (isObject(data)) {\n    return <JsonObject data={data} {...props} />\n  }\n\n  return <JsonPrimitive data={data} {...props} />\n}\n\nexport default JsonPretty\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-pretty/index.ts",
    "content": "import JsonPretty from './JsonPretty'\n\nexport default JsonPretty\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-primitive/JsonPrimitive.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport JsonPrimitive from './JsonPrimitive'\n\ndescribe('JsonPrimitive', () => {\n  it('should render', () => {\n    expect(render(<JsonPrimitive data={null} />)).toBeTruthy()\n  })\n\n  it('should render proper text and value for null', () => {\n    render(<JsonPrimitive data={null} />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent('null')\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__null-value',\n    )\n  })\n\n  it('should render proper text and value for number', () => {\n    render(<JsonPrimitive data={1} />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent('1')\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__number-value',\n    )\n  })\n\n  it('should render proper text and value for boolean', () => {\n    render(<JsonPrimitive data={false} />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent(\n      'false',\n    )\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__boolean-value',\n    )\n  })\n\n  it('should render proper text and value for string', () => {\n    render(<JsonPrimitive data=\"123\" />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent(\n      '\"123\"',\n    )\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__string-value',\n    )\n  })\n\n  it('should render proper text and value for bigint', () => {\n    render(<JsonPrimitive data={BigInt(123)} />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent('123')\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__bigint-value',\n    )\n  })\n\n  it('should render proper text and value for other types', () => {\n    render(<JsonPrimitive data={new Map()} />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent(\n      '[object Map]',\n    )\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__other-value',\n    )\n  })\n\n  it('should render proper text and value for true', () => {\n    render(<JsonPrimitive data />)\n\n    expect(screen.getByTestId('json-primitive-value')).toHaveTextContent('true')\n    expect(screen.getByTestId('json-primitive-value')).toHaveClass(\n      'json-pretty__boolean-value',\n    )\n  })\n\n  it('should render coma', () => {\n    render(<JsonPrimitive data lastElement={false} />)\n\n    expect(screen.getByTestId('json-primitive-component')).toHaveTextContent(\n      'true,',\n    )\n  })\n\n  it('should not render coma', () => {\n    render(<JsonPrimitive data lastElement />)\n\n    expect(screen.getByTestId('json-primitive-component')).toHaveTextContent(\n      'true',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-primitive/JsonPrimitive.tsx",
    "content": "import React from 'react'\nimport { isString, isBoolean, isNull, isNumber } from 'lodash'\nimport { isBigInt } from 'uiSrc/components/json-viewer/utils'\nimport { IDefaultProps } from 'uiSrc/components/json-viewer/interfaces'\n\nconst JsonPrimitive = ({ data, lastElement = true }: IDefaultProps) => {\n  let stringValue = data\n  let valueStyle = 'json-pretty__other-value'\n\n  if (isNull(data)) {\n    stringValue = 'null'\n    valueStyle = 'json-pretty__null-value'\n  } else if (isString(data)) {\n    stringValue = `\"${data}\"`\n    valueStyle = 'json-pretty__string-value'\n  } else if (isBoolean(data)) {\n    stringValue = data ? 'true' : 'false'\n    valueStyle = 'json-pretty__boolean-value'\n  } else if (isNumber(data)) {\n    stringValue = data.toString()\n    valueStyle = 'json-pretty__number-value'\n  } else if (isBigInt(data)) {\n    stringValue = data.toString()\n    valueStyle = 'json-pretty__bigint-value'\n  } else {\n    stringValue = data.toString()\n  }\n  return (\n    <span data-testid=\"json-primitive-component\">\n      <span className={valueStyle} data-testid=\"json-primitive-value\">\n        {stringValue}\n      </span>\n      {!lastElement && ','}\n      {'\\n'}\n    </span>\n  )\n}\n\nexport default JsonPrimitive\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/components/json-primitive/index.ts",
    "content": "import JsonPrimitive from './JsonPrimitive'\n\nexport default JsonPrimitive\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/interfaces.ts",
    "content": "export interface IDefaultProps {\n  data: any\n  space?: number\n  gap?: number\n  lastElement?: boolean\n}\n\nexport interface IJsonArrayProps extends IDefaultProps {\n  data: Array<any>\n}\n\nexport interface IJsonObjectProps extends IDefaultProps {\n  data: Record<string, any>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/utils.spec.ts",
    "content": "import { isBigInt, isArray, isObject } from './utils'\n\nconst isBigIntTests = [\n  ['', false],\n  [true, false],\n  [false, false],\n  [0, false],\n  ['123', false],\n  [123, false],\n  [[], false],\n  [[123], false],\n  [{}, false],\n  [{ name: 'something' }, false],\n  [BigInt(1), true],\n  [Infinity, false],\n  [NaN, false],\n]\n\nconst isArrayTests = [\n  ['', false],\n  [true, false],\n  [false, false],\n  [0, false],\n  ['123', false],\n  [123, false],\n  [[], true],\n  [[123], true],\n  [{}, false],\n  [{ name: 'something' }, false],\n  [BigInt(1), false],\n  [Infinity, false],\n  [NaN, false],\n]\n\nconst isObjectTests = [\n  ['', false],\n  [true, false],\n  [false, false],\n  [0, false],\n  ['123', false],\n  [123, false],\n  [[], false],\n  [[123], false],\n  [{}, true],\n  [{ name: 'something' }, true],\n  [BigInt(1), false],\n  [Infinity, false],\n  [NaN, false],\n]\n\ndescribe('isBigInt', () => {\n  it.each(isBigIntTests)(\n    'for input: %s (name), should be output: %s',\n    (value, expected) => {\n      const result = isBigInt(value)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\ndescribe('isArray', () => {\n  it.each(isArrayTests)(\n    'for input: %s (name), should be output: %s',\n    (value, expected) => {\n      const result = isArray(value)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\ndescribe('isObject', () => {\n  it.each(isObjectTests)(\n    'for input: %s (name), should be output: %s',\n    (value, expected) => {\n      const result = isObject(value)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/json-viewer/utils.ts",
    "content": "export const isBigInt = (data: any) =>\n  typeof data === 'bigint' || data instanceof BigInt\nexport const isArray = (data: any) => Array.isArray(data)\nexport const isObject = (data: any) =>\n  typeof data === 'object' && data !== null && !Array.isArray(data)\n"
  },
  {
    "path": "redisinsight/ui/src/components/keyboard-shortcut/KeyboardShortcut.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport KeyboardShortcut, { Props } from './KeyboardShortcut'\n\nconst mockedProps = mock<Props>()\n\ndescribe('KeyboardShortcut', () => {\n  it('should render', () => {\n    expect(render(<KeyboardShortcut {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/keyboard-shortcut/KeyboardShortcut.tsx",
    "content": "import React from 'react'\nimport { isString } from 'lodash'\nimport cx from 'classnames'\n\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  items: (string | JSX.Element)[]\n  separator?: string\n  transparent?: boolean\n  badgeTextClassName?: string\n}\n\nconst KeyboardShortcut = (props: Props) => {\n  const {\n    items = [],\n    separator = '',\n    transparent = false,\n    badgeTextClassName = '',\n  } = props\n  return (\n    <div className={styles.container}>\n      {items.map((item: string | JSX.Element, index: number) => (\n        <div key={isString(item) ? item : item?.props?.children}>\n          {index !== 0 && <div className={styles.separator}>{separator}</div>}\n          <RiBadge\n            className={cx(styles.badge, badgeTextClassName, {\n              [styles.transparent]: transparent,\n            })}\n            label={item}\n          />\n        </div>\n      ))}\n    </div>\n  )\n}\nexport default KeyboardShortcut\n"
  },
  {
    "path": "redisinsight/ui/src/components/keyboard-shortcut/styles.module.scss",
    "content": ".container {\n  display: flex;\n  align-items: center;\n  & > div {\n    display: flex;\n    align-items: center;\n  }\n}\n\n.separator {\n  margin: 0 4px;\n}\n\n.badge {\n  background-color: var(--euiTooltipBackgroundColor) !important;\n  border: 1px solid var(--euiToastSuccessBtnColor) !important;\n}\n\n.transparent {\n  background-color: transparent !important;\n  border-color: var(--separatorColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/keys-summary/KeysSummary.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport KeysSummary, { Props } from './KeysSummary'\n\nconst mockedProps = mock<Props>()\n\ndescribe('KeysSummary', () => {\n  it('should render', () => {\n    expect(render(<KeysSummary {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should \"Scanning...\" be in the document until loading and totalItemsCount == 0 ', () => {\n    const { queryByTestId } = render(\n      <KeysSummary {...instance(mockedProps)} loading totalItemsCount={0} />,\n    )\n    expect(queryByTestId('scanning-text')).toBeInTheDocument()\n  })\n\n  it('should Keys summary be in the document meanwhile totalItemsCount != 0 ', () => {\n    const { queryByTestId } = render(\n      <KeysSummary {...instance(mockedProps)} totalItemsCount={2} />,\n    )\n    expect(queryByTestId('keys-summary')).toBeInTheDocument()\n  })\n\n  it('should Keys summary show proper text with count = 1', () => {\n    const { queryByTestId } = render(\n      <KeysSummary\n        {...instance(mockedProps)}\n        scanned={1}\n        items={[{}]}\n        totalItemsCount={1}\n      />,\n    )\n    expect(queryByTestId('keys-summary')).toHaveTextContent('Results: 1.')\n  })\n\n  it('should Keys summary show proper text with count > 1', () => {\n    const { queryByTestId } = render(\n      <KeysSummary\n        {...instance(mockedProps)}\n        scanned={2}\n        items={[{}, {}]}\n        totalItemsCount={2}\n      />,\n    )\n    expect(queryByTestId('keys-summary')).toHaveTextContent('Results: 2.')\n  })\n\n  it('should not render Scan more button if showScanMore = false ', () => {\n    const { queryByTestId } = render(\n      <KeysSummary {...instance(mockedProps)} showScanMore={false} />,\n    )\n    expect(queryByTestId('scan-more')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/keys-summary/KeysSummary.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { isNull } from 'lodash'\nimport { useSelector } from 'react-redux'\n\nimport { Text, ColorText } from 'uiSrc/components/base/text'\n\nimport { numberWithSpaces, nullableNumberWithSpaces } from 'uiSrc/utils/numbers'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\nimport { keysSelector } from 'uiSrc/slices/browser/keys'\nimport { KeyTreeSettings } from 'uiSrc/pages/browser/components/key-tree'\n\nimport ScanMore from '../scan-more'\nimport styles from './styles.module.scss'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  loading: boolean\n  items: any[]\n  showScanMore?: boolean\n  scanned?: number\n  totalItemsCount?: number\n  nextCursor?: string\n  scanMoreStyle?: {\n    [key: string]: string | number\n  }\n  loadMoreItems?: (config: any) => void\n}\n\nconst KeysSummary = (props: Props) => {\n  const {\n    items = [],\n    loading,\n    showScanMore = true,\n    scanned = 0,\n    totalItemsCount = 0,\n    scanMoreStyle,\n    loadMoreItems,\n    nextCursor,\n  } = props\n\n  const resultsLength = items.length\n  const scannedDisplay = resultsLength > scanned ? resultsLength : scanned\n  const notAccurateScanned =\n    totalItemsCount &&\n    scanned >= totalItemsCount &&\n    nextCursor &&\n    nextCursor !== '0'\n      ? '~'\n      : ''\n\n  const { viewType } = useSelector(keysSelector)\n\n  return (\n    <>\n      {(!!totalItemsCount || isNull(totalItemsCount)) && (\n        <Row align=\"center\" gap=\"l\" data-testid=\"keys-summary\">\n          {!!scanned && (\n            <FlexItem>\n              <Row gap=\"s\">\n                <ColorText size=\"s\" variant=\"semiBold\" component=\"span\">\n                  {'Results: '}\n                  <span data-testid=\"keys-number-of-results\">\n                    {numberWithSpaces(resultsLength)}\n                  </span>\n                  {'. '}\n                </ColorText>\n                <ColorText size=\"s\" color=\"secondary\" component=\"span\">\n                  {'Scanned '}\n                  <span data-testid=\"keys-number-of-scanned\">\n                    {notAccurateScanned}\n                    {numberWithSpaces(scannedDisplay)}\n                  </span>\n                  {' / '}\n                  <span data-testid=\"keys-total\">\n                    {nullableNumberWithSpaces(totalItemsCount)}\n                  </span>\n                  <span\n                    className={cx([\n                      styles.loading,\n                      { [styles.loadingShow]: loading },\n                    ])}\n                  />\n                </ColorText>\n              </Row>\n            </FlexItem>\n          )}\n          {!scanned && (\n            <FlexItem>\n              <Text size=\"s\" variant=\"semiBold\" component=\"span\">\n                {'Total: '}\n                {nullableNumberWithSpaces(totalItemsCount)}\n              </Text>\n            </FlexItem>\n          )}\n          {showScanMore && (\n            <FlexItem>\n              <ScanMore\n                withAlert\n                fill={false}\n                style={scanMoreStyle}\n                scanned={scanned}\n                totalItemsCount={totalItemsCount}\n                loading={loading}\n                loadMoreItems={loadMoreItems}\n                nextCursor={nextCursor}\n              />\n            </FlexItem>\n          )}\n          {viewType === KeyViewType.Tree && (\n            <FlexItem>\n              <KeyTreeSettings loading={loading} />\n            </FlexItem>\n          )}\n        </Row>\n      )}\n      {loading && !totalItemsCount && !isNull(totalItemsCount) && (\n        <Row align=\"center\">\n          <FlexItem>\n            <Text size=\"s\" data-testid=\"scanning-text\">\n              Scanning...\n            </Text>\n          </FlexItem>\n        </Row>\n      )}\n    </>\n  )\n}\n\nexport default KeysSummary\n"
  },
  {
    "path": "redisinsight/ui/src/components/keys-summary/index.ts",
    "content": "import KeysSummary from './KeysSummary'\n\nexport default KeysSummary\n"
  },
  {
    "path": "redisinsight/ui/src/components/keys-summary/styles.module.scss",
    "content": ".loading {\n  opacity: 0;\n}\n\n.loadingShow {\n  opacity: 1;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/main/MainComponent.tsx",
    "content": "import React from 'react'\n\nimport MainRouter from '../main-router/MainRouter'\n\nconst MainComponent = () => (\n  // here will be CLI for all pages\n  <MainRouter />\n)\n\nexport default MainComponent\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/MainRouter.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport { waitFor } from '@testing-library/react'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport Router from 'uiSrc/Router'\nimport { localStorageService } from 'uiSrc/services'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  appContextSelector,\n  setCurrentWorkspace,\n} from 'uiSrc/slices/app/context'\nimport { AppWorkspace } from 'uiSrc/slices/interfaces'\nimport MainRouter from './MainRouter'\nimport * as activityMonitor from './hooks/useActivityMonitor'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextSelector: jest.fn().mockReturnValue({\n    ...jest.requireActual('uiSrc/slices/app/context').appContextSelector,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('MainRouter', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <Router>\n          <MainRouter />\n        </Router>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should redirect to rdi page', () => {\n    localStorageService.get = jest.fn().mockReturnValue(Pages.rdi)\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.home })\n\n    render(\n      <Router>\n        <MainRouter />\n      </Router>,\n    )\n\n    expect(pushMock).toBeCalledWith(Pages.rdi)\n  })\n\n  it('should set RDI workspace', () => {\n    ;(appContextSelector as jest.Mock).mockReturnValueOnce({\n      workspace: AppWorkspace.Databases,\n    })\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdiPipelineConfig('1') })\n\n    render(\n      <Router>\n        <MainRouter />\n      </Router>,\n    )\n\n    expect(store.getActions()).toContainEqual(\n      setCurrentWorkspace(AppWorkspace.RDI),\n    )\n  })\n\n  it('should set Database workspace', () => {\n    ;(appContextSelector as jest.Mock).mockReturnValueOnce({\n      workspace: AppWorkspace.RDI,\n    })\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.analytics('1') })\n\n    render(\n      <Router>\n        <MainRouter />\n      </Router>,\n    )\n\n    expect(store.getActions()).toContainEqual(\n      setCurrentWorkspace(AppWorkspace.Databases),\n    )\n  })\n\n  it('starts activity monitor on mount and stops on unmount', async () => {\n    const useActivityMonitorSpy = jest.spyOn(\n      activityMonitor,\n      'useActivityMonitor',\n    )\n\n    render(\n      <Router>\n        <MainRouter />\n      </Router>,\n    )\n\n    await waitFor(() => {\n      expect(useActivityMonitorSpy).toHaveBeenCalledTimes(1)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/MainRouter.tsx",
    "content": "import React, { Suspense, useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  Redirect,\n  Route,\n  Switch,\n  useHistory,\n  useLocation,\n} from 'react-router-dom'\n\nimport extractRouter from 'uiSrc/hoc/extractRouter.hoc'\nimport { registerRouter } from 'uiSrc/services/routing'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { ConsentsSettingsPopup } from 'uiSrc/components'\nimport { userSettingsSelector } from 'uiSrc/slices/user/user-settings'\nimport GlobalUrlHandler from 'uiSrc/components/global-url-handler'\n\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem, Pages } from 'uiSrc/constants'\nimport {\n  appContextSelector,\n  setCurrentWorkspace,\n} from 'uiSrc/slices/app/context'\nimport { AppWorkspace } from 'uiSrc/slices/interfaces'\nimport SuspenseLoader from 'uiSrc/components/main-router/components/SuspenseLoader'\nimport RedisStackRoutes from './components/RedisStackRoutes'\nimport DEFAULT_ROUTES from './constants/defaultRoutes'\nimport { useActivityMonitor } from './hooks/useActivityMonitor'\n\nconst MainRouter = () => {\n  const { server } = useSelector(appInfoSelector)\n  const { isShowConceptsPopup: isShowConsents } =\n    useSelector(userSettingsSelector)\n  const { workspace } = useSelector(appContextSelector)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n  const { pathname } = useLocation()\n  useActivityMonitor()\n\n  const isRedisStack = server?.buildType === BuildType.RedisStack\n\n  useEffect(() => {\n    if (!isRedisStack) {\n      const isRdiPageHome =\n        localStorageService.get(BrowserStorageItem.homePage) === Pages.rdi\n      if (pathname === Pages.home && isRdiPageHome) {\n        history.push(Pages.rdi)\n      }\n    }\n  }, [])\n\n  useEffect(() => {\n    const isRdiWorkspace = pathname.startsWith(Pages.rdi)\n    const isWorkspaceMatch =\n      isRdiWorkspace === (workspace === AppWorkspace.Databases)\n\n    if (isWorkspaceMatch && !pathname.startsWith(Pages.settings)) {\n      const newWorkspace = isRdiWorkspace\n        ? AppWorkspace.RDI\n        : AppWorkspace.Databases\n      dispatch(setCurrentWorkspace(newWorkspace))\n    }\n\n    // before quit - save home page\n    window.addEventListener('beforeunload', beforeUnload)\n    return () => {\n      window.removeEventListener('beforeunload', beforeUnload)\n    }\n  }, [pathname, workspace])\n\n  const beforeUnload = () => {\n    localStorageService.set(\n      BrowserStorageItem.homePage,\n      workspace === AppWorkspace.RDI ? Pages.rdi : Pages.home,\n    )\n  }\n\n  return (\n    <>\n      {isShowConsents && <ConsentsSettingsPopup />}\n      {!isRedisStack && <GlobalUrlHandler />}\n      <Suspense fallback={<SuspenseLoader />}>\n        <Switch>\n          {isRedisStack ? (\n            <RedisStackRoutes databaseId={server?.fixedDatabaseId} />\n          ) : (\n            DEFAULT_ROUTES.map((route, i) => (\n              // eslint-disable-next-line react/no-array-index-key\n              <RouteWithSubRoutes key={i} {...route} />\n            ))\n          )}\n          <Route path=\"*\" render={() => <Redirect to={Pages.notFound} />} />\n        </Switch>\n      </Suspense>\n    </>\n  )\n}\n\nconst MainMount: any = extractRouter(registerRouter)(MainRouter)\n\nexport default MainMount\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/components/RedisStackRoutes.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport RedisStackRoutes, { Props } from './RedisStackRoutes'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('RedisStackRoutes', () => {\n  it('should render', () => {\n    expect(render(<RedisStackRoutes {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/components/RedisStackRoutes.tsx",
    "content": "import { useDispatch } from 'react-redux'\nimport React, { useEffect, useState } from 'react'\nimport { Switch, useHistory } from 'react-router-dom'\n\nimport {\n  checkConnectToInstanceAction,\n  resetConnectedInstance,\n  setConnectedInstanceId,\n} from 'uiSrc/slices/instances/instances'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { Pages } from 'uiSrc/constants'\nimport { PagePlaceholder } from 'uiSrc/components'\nimport ProtectedRoute from 'uiSrc/pages/redis-stack/components/protected-route/ProtectedRoute'\nimport ROUTES from '../constants/redisStackRoutes'\n\ninterface IProps {\n  databaseId?: string\n}\n\ninterface IConnectionState {\n  loading: boolean\n  ready: boolean\n}\n\nconst Router = ({ databaseId = '' }: IProps) => {\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const [connection, setConnection] = useState<IConnectionState>({\n    loading: true,\n    ready: false,\n  })\n\n  const handleSuccess = (id: string) => {\n    dispatch(setConnectedInstanceId(id))\n    setConnection({ loading: false, ready: false })\n    history.push(Pages.browser(databaseId))\n  }\n\n  const handleFail = () => {\n    dispatch(resetConnectedInstance())\n    setConnection({ loading: false, ready: false })\n  }\n\n  useEffect(() => {\n    dispatch(\n      checkConnectToInstanceAction(\n        databaseId,\n        () => handleSuccess(databaseId),\n        handleFail,\n      ),\n    )\n  }, [])\n\n  return connection.loading ? (\n    <PagePlaceholder />\n  ) : (\n    <Switch>\n      {ROUTES.map((route, i) =>\n        route.protected ? (\n          // eslint-disable-next-line react/no-array-index-key\n          <ProtectedRoute key={i}>\n            <RouteWithSubRoutes {...route} />\n          </ProtectedRoute>\n        ) : (\n          // eslint-disable-next-line react/no-array-index-key\n          <RouteWithSubRoutes key={i} {...route} />\n        ),\n      )}\n    </Switch>\n  )\n}\n\nexport default Router\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/components/SuspenseLoader.tsx",
    "content": "import React from 'react'\nimport { Loader } from 'uiSrc/components/base/display'\nimport styles from './loader.module.scss'\n\nconst SuspenseLoader = () => (\n  <div className={styles.cover} data-testid=\"suspense-loader\">\n    <Loader size=\"xl\" className={styles.loader} />\n  </div>\n)\n\nexport default SuspenseLoader\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/components/loader.module.scss",
    "content": ".cover {\n  position: absolute;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  z-index: 2;\n  opacity: 0.8;\n  background-color: var(--euiColorLightestShade);\n}\n\n.loader {\n  border-color:\n    var(--euiColorPrimary)\n    var(--separatorDropdownColor)\n    var(--separatorDropdownColor)\n    var(--separatorDropdownColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/config.ts",
    "content": "import { IRoute } from 'uiSrc/constants'\nimport { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\n\nexport const LAZY_LOAD = riConfig?.app?.lazyLoad\n\nexport const ROUTES_EXCLUDED_BY_ENV = riConfig?.app?.routesExcludedByEnv\n\nexport const getRouteIncludedByEnv = (routeDefinition: IRoute[]) => {\n  if (ROUTES_EXCLUDED_BY_ENV) {\n    return []\n  }\n\n  return routeDefinition\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/commonRoutes.ts",
    "content": "import { lazy } from 'react'\nimport { IRoute, Pages } from 'uiSrc/constants'\nimport SettingsPage from 'uiSrc/pages/settings'\nimport {\n  SentinelDatabasesPage,\n  SentinelDatabasesResultPage,\n  SentinelPage,\n} from 'uiSrc/pages/autodiscover-sentinel'\n\nimport { LAZY_LOAD } from '../config'\n\nconst LazySettingsPage = lazy(() => import('uiSrc/pages/settings'))\nconst LazySentinelDatabasesPage = lazy(\n  () => import('uiSrc/pages/autodiscover-sentinel/sentinel-databases'),\n)\nconst LazySentinelDatabasesResultPage = lazy(\n  () => import('uiSrc/pages/autodiscover-sentinel/sentinel-databases-result'),\n)\nconst LazySentinelPage = lazy(\n  () => import('uiSrc/pages/autodiscover-sentinel/sentinel'),\n)\n\nconst ROUTES: IRoute[] = [\n  {\n    path: Pages.settings,\n    component: LAZY_LOAD ? LazySettingsPage : SettingsPage,\n  },\n  {\n    path: Pages.sentinel,\n    component: LAZY_LOAD ? LazySentinelPage : SentinelPage,\n    routes: [\n      {\n        path: Pages.sentinelDatabases,\n        component: LAZY_LOAD\n          ? LazySentinelDatabasesPage\n          : SentinelDatabasesPage,\n      },\n      {\n        path: Pages.sentinelDatabasesResult,\n        component: LAZY_LOAD\n          ? LazySentinelDatabasesResultPage\n          : SentinelDatabasesResultPage,\n      },\n    ],\n  },\n]\n\nexport default ROUTES\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts",
    "content": "import { lazy } from 'react'\nimport { IRoute, FeatureFlags, PageNames, Pages } from 'uiSrc/constants'\nimport {\n  BrowserPage,\n  HomePage,\n  InstancePage,\n  RedisCloudDatabasesPage,\n  RedisCloudDatabasesResultPage,\n  RedisCloudPage,\n  RedisCloudSubscriptionsPage,\n  RedisClusterDatabasesPage,\n  AzurePage,\n  AzureSubscriptionsPage,\n  AzureDatabasesPage,\n} from 'uiSrc/pages'\nimport { VectorSearchPageRouter } from 'uiSrc/pages/vector-search'\nimport WorkbenchPage from 'uiSrc/pages/workbench'\nimport PubSubPage from 'uiSrc/pages/pub-sub'\nimport AnalyticsPage from 'uiSrc/pages/analytics'\nimport RdiPage from 'uiSrc/pages/rdi/home'\nimport RdiInstancePage from 'uiSrc/pages/rdi/instance'\nimport RdiStatisticsPage from 'uiSrc/pages/rdi/statistics'\nimport PipelineManagementPage from 'uiSrc/pages/rdi/pipeline-management'\nimport { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES } from './sub-routes'\nimport COMMON_ROUTES from './commonRoutes'\nimport { getRouteIncludedByEnv, LAZY_LOAD } from '../config'\nimport { VECTOR_SEARCH_ROUTES } from './sub-routes/vectorSearchRoutes'\n\nconst LazyBrowserPage = lazy(() => import('uiSrc/pages/browser'))\nconst LazyVectorSearchPageRouter = lazy(\n  () => import('uiSrc/pages/vector-search/VectorSearchPageRouter'),\n)\nconst LazyHomePage = lazy(() => import('uiSrc/pages/home'))\nconst LazyWorkbenchPage = lazy(() => import('uiSrc/pages/workbench'))\nconst LazyPubSubPage = lazy(() => import('uiSrc/pages/pub-sub'))\nconst LazyInstancePage = lazy(() => import('uiSrc/pages/instance'))\nconst LazyRedisCloudDatabasesPage = lazy(\n  () => import('uiSrc/pages/autodiscover-cloud/redis-cloud-databases'),\n)\nconst LazyRedisCloudDatabasesResultPage = lazy(\n  () => import('uiSrc/pages/autodiscover-cloud/redis-cloud-databases-result'),\n)\nconst LazyRedisCloudSubscriptionsPage = lazy(\n  () => import('uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions'),\n)\nconst LazyRedisClusterDatabasesPage = lazy(\n  () => import('uiSrc/pages/redis-cluster'),\n)\nconst LazyAnalyticsPage = lazy(() => import('uiSrc/pages/analytics'))\nconst LazyRedisCloudPage = lazy(\n  () => import('uiSrc/pages/autodiscover-cloud/redis-cloud'),\n)\nconst LazyAzurePage = lazy(() => import('uiSrc/pages/autodiscover-azure/azure'))\nconst LazyAzureSubscriptionsPage = lazy(\n  () => import('uiSrc/pages/autodiscover-azure/azure-subscriptions'),\n)\nconst LazyAzureDatabasesPage = lazy(\n  () => import('uiSrc/pages/autodiscover-azure/azure-databases'),\n)\nconst LazyRdiPage = lazy(() => import('uiSrc/pages/rdi/home'))\nconst LazyRdiInstancePage = lazy(() => import('uiSrc/pages/rdi/instance'))\nconst LazyRdiStatisticsPage = lazy(() => import('uiSrc/pages/rdi/statistics'))\nconst LazyPipelineManagementPage = lazy(\n  () => import('uiSrc/pages/rdi/pipeline-management'),\n)\n\nconst INSTANCE_ROUTES: IRoute[] = [\n  {\n    pageName: PageNames.browser,\n    path: Pages.browser(':instanceId'),\n    component: LAZY_LOAD ? LazyBrowserPage : BrowserPage,\n  },\n  // Vector search route - behind feature flag\n  {\n    pageName: PageNames.vectorSearch,\n    path: Pages.vectorSearch(':instanceId'),\n    component: LAZY_LOAD ? LazyVectorSearchPageRouter : VectorSearchPageRouter,\n    routes: VECTOR_SEARCH_ROUTES,\n    featureFlag: FeatureFlags.vectorSearchV2,\n  },\n  {\n    pageName: PageNames.workbench,\n    path: Pages.workbench(':instanceId'),\n    component: LAZY_LOAD ? LazyWorkbenchPage : WorkbenchPage,\n  },\n  {\n    pageName: PageNames.pubSub,\n    path: Pages.pubSub(':instanceId'),\n    component: LAZY_LOAD ? LazyPubSubPage : PubSubPage,\n    featureFlag: FeatureFlags.envDependent,\n  },\n  ...getRouteIncludedByEnv([\n    {\n      path: Pages.analytics(':instanceId'),\n      component: LAZY_LOAD ? LazyAnalyticsPage : AnalyticsPage,\n      routes: ANALYTICS_ROUTES,\n    },\n  ]),\n]\n\nconst RDI_INSTANCE_ROUTES: IRoute[] = getRouteIncludedByEnv([\n  {\n    path: Pages.rdiStatistics(':rdiInstanceId'),\n    component: LAZY_LOAD ? LazyRdiStatisticsPage : RdiStatisticsPage,\n  },\n  {\n    path: Pages.rdiPipelineManagement(':rdiInstanceId'),\n    component: LAZY_LOAD ? LazyPipelineManagementPage : PipelineManagementPage,\n    routes: RDI_PIPELINE_MANAGEMENT_ROUTES,\n  },\n])\n\nconst ROUTES: IRoute[] = [\n  ...getRouteIncludedByEnv([\n    {\n      path: Pages.home,\n      exact: true,\n      component: LAZY_LOAD ? LazyHomePage : HomePage,\n      isAvailableWithoutAgreements: true,\n      featureFlag: FeatureFlags.envDependent,\n    },\n    ...COMMON_ROUTES,\n    {\n      path: Pages.redisEnterpriseAutodiscovery,\n      component: LAZY_LOAD\n        ? LazyRedisClusterDatabasesPage\n        : RedisClusterDatabasesPage,\n    },\n    {\n      path: Pages.redisCloud,\n      component: LAZY_LOAD ? LazyRedisCloudPage : RedisCloudPage,\n      routes: [\n        {\n          path: Pages.redisCloudSubscriptions,\n          component: LAZY_LOAD\n            ? LazyRedisCloudSubscriptionsPage\n            : RedisCloudSubscriptionsPage,\n        },\n        {\n          path: Pages.redisCloudDatabases,\n          component: LAZY_LOAD\n            ? LazyRedisCloudDatabasesPage\n            : RedisCloudDatabasesPage,\n        },\n        {\n          path: Pages.redisCloudDatabasesResult,\n          component: LAZY_LOAD\n            ? LazyRedisCloudDatabasesResultPage\n            : RedisCloudDatabasesResultPage,\n        },\n      ],\n    },\n    {\n      path: Pages.azure,\n      component: LAZY_LOAD ? LazyAzurePage : AzurePage,\n      routes: [\n        {\n          path: Pages.azureSubscriptions,\n          component: LAZY_LOAD\n            ? LazyAzureSubscriptionsPage\n            : AzureSubscriptionsPage,\n        },\n        {\n          path: Pages.azureDatabases,\n          component: LAZY_LOAD ? LazyAzureDatabasesPage : AzureDatabasesPage,\n        },\n      ],\n    },\n    {\n      path: Pages.rdi,\n      component: LAZY_LOAD ? LazyRdiPage : RdiPage,\n      exact: true,\n      featureFlag: FeatureFlags.rdi,\n    },\n    {\n      path: Pages.rdiPipeline(':rdiInstanceId'),\n      component: LAZY_LOAD ? LazyRdiInstancePage : RdiInstancePage,\n      routes: RDI_INSTANCE_ROUTES,\n      featureFlag: FeatureFlags.rdi,\n    },\n  ]),\n  {\n    path: '/:instanceId',\n    component: LAZY_LOAD ? LazyInstancePage : InstancePage,\n    routes: INSTANCE_ROUTES,\n  },\n]\n\nexport default ROUTES\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts",
    "content": "import { PageNames, Pages, IRoute } from 'uiSrc/constants'\nimport { BrowserPage, InstancePage } from 'uiSrc/pages'\nimport WorkbenchPage from 'uiSrc/pages/workbench'\nimport SlowLogPage from 'uiSrc/pages/slow-log'\nimport PubSubPage from 'uiSrc/pages/pub-sub'\nimport EditConnection from 'uiSrc/pages/redis-stack/components/edit-connection'\nimport ClusterDetailsPage from 'uiSrc/pages/cluster-details'\nimport AnalyticsPage from 'uiSrc/pages/analytics'\nimport DatabaseAnalysisPage from 'uiSrc/pages/database-analysis'\nimport COMMON_ROUTES from './commonRoutes'\n\nconst ANALYTICS_ROUTES: IRoute[] = [\n  {\n    pageName: PageNames.slowLog,\n    protected: true,\n    path: Pages.slowLog(':instanceId'),\n    component: SlowLogPage,\n  },\n  {\n    pageName: PageNames.databaseAnalysis,\n    protected: true,\n    path: Pages.databaseAnalysis(':instanceId'),\n    component: DatabaseAnalysisPage,\n  },\n  {\n    pageName: PageNames.clusterDetails,\n    protected: true,\n    path: Pages.clusterDetails(':instanceId'),\n    component: ClusterDetailsPage,\n  },\n]\n\nconst INSTANCE_ROUTES: IRoute[] = [\n  {\n    pageName: PageNames.browser,\n    protected: true,\n    path: Pages.browser(':instanceId'),\n    component: BrowserPage,\n  },\n  {\n    pageName: PageNames.workbench,\n    protected: true,\n    path: Pages.workbench(':instanceId'),\n    component: WorkbenchPage,\n  },\n  {\n    pageName: PageNames.pubSub,\n    protected: true,\n    path: Pages.pubSub(':instanceId'),\n    component: PubSubPage,\n  },\n  {\n    path: Pages.analytics(':instanceId'),\n    protected: true,\n    component: AnalyticsPage,\n    routes: ANALYTICS_ROUTES,\n  },\n]\n\nconst ROUTES: IRoute[] = [\n  {\n    path: Pages.home,\n    exact: true,\n    component: EditConnection,\n  },\n  ...COMMON_ROUTES,\n  {\n    path: '/:instanceId',\n    protected: true,\n    component: InstancePage,\n    routes: INSTANCE_ROUTES,\n  },\n]\n\nexport default ROUTES\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/sub-routes/analyticsRoutes.ts",
    "content": "import { IRoute, PageNames, Pages } from 'uiSrc/constants'\nimport ClusterDetailsPage from 'uiSrc/pages/cluster-details'\nimport SlowLogPage from 'uiSrc/pages/slow-log'\nimport DatabaseAnalysisPage from 'uiSrc/pages/database-analysis'\n\nexport const ANALYTICS_ROUTES: IRoute[] = [\n  {\n    pageName: PageNames.slowLog,\n    path: Pages.slowLog(':instanceId'),\n    component: SlowLogPage,\n  },\n  {\n    pageName: PageNames.databaseAnalysis,\n    path: Pages.databaseAnalysis(':instanceId'),\n    component: DatabaseAnalysisPage,\n  },\n  {\n    pageName: PageNames.clusterDetails,\n    path: Pages.clusterDetails(':instanceId'),\n    component: ClusterDetailsPage,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts",
    "content": "import { ANALYTICS_ROUTES } from './analyticsRoutes'\nimport { RDI_PIPELINE_MANAGEMENT_ROUTES } from './rdiPipelineManagementRoutes'\n\nexport { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES }\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/sub-routes/rdiPipelineManagementRoutes.ts",
    "content": "import { IRoute, Pages } from 'uiSrc/constants'\nimport ConfigPage from 'uiSrc/pages/rdi/pipeline-management/pages/config'\nimport JobPage from 'uiSrc/pages/rdi/pipeline-management/pages/job'\n\nexport const RDI_PIPELINE_MANAGEMENT_ROUTES: IRoute[] = [\n  {\n    path: Pages.rdiPipelineConfig(':rdiInstanceId'),\n    component: ConfigPage,\n  },\n  {\n    path: Pages.rdiPipelineJobs(':rdiInstanceId', ':jobName'),\n    component: JobPage,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/constants/sub-routes/vectorSearchRoutes.ts",
    "content": "import { lazy } from 'react'\n\nimport { IRoute, PageNames, Pages } from 'uiSrc/constants'\nimport {\n  VectorSearchPage,\n  VectorSearchCreateIndexPage,\n  VectorSearchQueryPage,\n} from 'uiSrc/pages/vector-search'\nimport { LAZY_LOAD } from '../../config'\n\nconst LazyVectorSearchPage = lazy(\n  () => import('uiSrc/pages/vector-search/pages/VectorSearchPage'),\n)\nconst LazyVectorSearchCreateIndexPage = lazy(\n  () => import('uiSrc/pages/vector-search/pages/VectorSearchCreateIndexPage'),\n)\nconst LazyVectorSearchQueryPage = lazy(\n  () => import('uiSrc/pages/vector-search/pages/VectorSearchQueryPage'),\n)\n\nexport const VECTOR_SEARCH_ROUTES: IRoute[] = [\n  {\n    pageName: PageNames.vectorSearchCreateIndex,\n    path: Pages.vectorSearchCreateIndex(':instanceId'),\n    component: LAZY_LOAD\n      ? LazyVectorSearchCreateIndexPage\n      : VectorSearchCreateIndexPage,\n  },\n  {\n    pageName: PageNames.vectorSearchQuery,\n    path: Pages.vectorSearchQuery(':instanceId', ':indexName'),\n    component: LAZY_LOAD ? LazyVectorSearchQueryPage : VectorSearchQueryPage,\n  },\n  {\n    pageName: PageNames.vectorSearch,\n    path: Pages.vectorSearch(':instanceId'),\n    component: LAZY_LOAD ? LazyVectorSearchPage : VectorSearchPage,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/hooks/useActivityMonitor.spec.ts",
    "content": "import set from 'lodash/set'\nimport { renderHook } from '@testing-library/react-hooks'\nimport { getConfig } from 'uiSrc/config'\nimport { mockWindowLocation } from 'uiSrc/utils/test-utils'\nimport { useActivityMonitor } from './useActivityMonitor'\n\nconst addEventListenerSpy = jest.spyOn(window, 'addEventListener')\nconst removeEventListenerSpy = jest.spyOn(window, 'removeEventListener')\nconst addEventListenerProps = { passive: true, capture: true }\nconst removeEventListenerProps = { capture: true }\n\nconst riConfig = getConfig()\n\nconst mockConfig = (origin = 'http://localhost', throttleTimeout = 200) => {\n  set(riConfig, 'app.activityMonitorOrigin', origin)\n  set(riConfig, 'app.activityMonitorThrottleTimeout', throttleTimeout)\n}\n\nconst mockWindowOpener = (postMessage = jest.fn()) => {\n  global.window.opener = {\n    postMessage,\n  }\n}\n\njest.useFakeTimers()\n\nconst browserUrl = 'http://localhost/123/browser'\nconst logoutUrl = 'http://localhost/#/logout'\n\nlet setHrefMock: typeof jest.fn\nbeforeEach(() => {\n  jest.resetAllMocks()\n\n  const mockDate = new Date('2024-11-22T12:00:00Z')\n  jest.setSystemTime(mockDate)\n\n  mockConfig()\n  setHrefMock = mockWindowLocation(browserUrl)\n  mockWindowOpener()\n})\n\ndescribe('useActivityMonitor', () => {\n  it('should register event handlers on mount and unregister on unmount', () => {\n    const { unmount } = renderHook(useActivityMonitor)\n\n    // Verify mount behavior\n    expect(addEventListenerSpy).toHaveBeenCalledTimes(4)\n    expect(addEventListenerSpy).toHaveBeenNthCalledWith(\n      1,\n      'click',\n      expect.any(Function),\n      addEventListenerProps,\n    )\n    expect(addEventListenerSpy).toHaveBeenNthCalledWith(\n      2,\n      'keydown',\n      expect.any(Function),\n      addEventListenerProps,\n    )\n    expect(addEventListenerSpy).toHaveBeenNthCalledWith(\n      3,\n      'scroll',\n      expect.any(Function),\n      addEventListenerProps,\n    )\n    expect(addEventListenerSpy).toHaveBeenNthCalledWith(\n      4,\n      'touchstart',\n      expect.any(Function),\n      addEventListenerProps,\n    )\n\n    // Trigger unmount\n    unmount()\n\n    // Verify unmount behavior\n    expect(removeEventListenerSpy).toHaveBeenCalledTimes(4)\n    expect(removeEventListenerSpy).toHaveBeenNthCalledWith(\n      1,\n      'click',\n      expect.any(Function),\n      removeEventListenerProps,\n    )\n    expect(removeEventListenerSpy).toHaveBeenNthCalledWith(\n      2,\n      'keydown',\n      expect.any(Function),\n      removeEventListenerProps,\n    )\n    expect(removeEventListenerSpy).toHaveBeenNthCalledWith(\n      3,\n      'scroll',\n      expect.any(Function),\n      removeEventListenerProps,\n    )\n    expect(removeEventListenerSpy).toHaveBeenNthCalledWith(\n      4,\n      'touchstart',\n      expect.any(Function),\n      removeEventListenerProps,\n    )\n  })\n\n  it('should register event handlers even if window.opener is undefined', () => {\n    global.window.opener = undefined\n\n    const { unmount } = renderHook(useActivityMonitor)\n\n    // Verify mount behavior\n    expect(addEventListenerSpy).toHaveBeenCalledTimes(4)\n\n    // Trigger unmount\n    unmount()\n\n    // Verify unmount behavior\n    expect(removeEventListenerSpy).toHaveBeenCalledTimes(4)\n  })\n\n  it('should not register handlers if activityMonitorOrigin is not defined', () => {\n    mockConfig('')\n\n    const { unmount } = renderHook(useActivityMonitor)\n\n    // Verify mount behavior\n    expect(addEventListenerSpy).not.toHaveBeenCalled()\n\n    // Trigger unmount\n    unmount()\n\n    // Verify unmount behavior\n    expect(removeEventListenerSpy).not.toHaveBeenCalled()\n  })\n\n  it('should logout user after expected amount of inactivity', async () => {\n    renderHook(useActivityMonitor)\n    jest.advanceTimersByTime(1900 * 1000)\n    expect(setHrefMock).toHaveBeenCalledWith(logoutUrl)\n  })\n\n  it('should not logout user if hook unmounts', async () => {\n    const { unmount } = renderHook(useActivityMonitor)\n    jest.advanceTimersByTime(1700 * 1000)\n    expect(setHrefMock).not.toHaveBeenCalled()\n\n    unmount()\n\n    jest.advanceTimersByTime(1000 * 1000)\n    expect(setHrefMock).not.toHaveBeenCalled()\n  })\n\n  it('should keep user logged in if they stay active', async () => {\n    renderHook(useActivityMonitor)\n\n    const activityHandler = addEventListenerSpy.mock.calls[0]?.[1] as Function\n\n    // act\n    jest.advanceTimersByTime(1700 * 1000)\n    activityHandler()\n    jest.advanceTimersByTime(1700 * 1000)\n\n    // assert\n    expect(setHrefMock).not.toHaveBeenCalled()\n\n    // act\n    activityHandler()\n    jest.advanceTimersByTime(1700 * 1000)\n\n    // assert\n    expect(setHrefMock).not.toHaveBeenCalled()\n\n    // act\n    jest.advanceTimersByTime(1000 * 1000)\n\n    // assert\n    expect(setHrefMock).toHaveBeenCalledWith(logoutUrl)\n  })\n\n  it('should throttle events and call window.opener.postMessage', async () => {\n    const mockPostMessage = jest.fn()\n\n    mockWindowOpener(mockPostMessage)\n    renderHook(useActivityMonitor)\n\n    // act\n    const activityHandler = addEventListenerSpy.mock.calls[0]?.[1] as Function\n    activityHandler()\n    activityHandler()\n    activityHandler()\n    activityHandler()\n\n    jest.advanceTimersByTime(20_000)\n\n    // assert\n    expect(mockPostMessage).toHaveBeenCalledTimes(1)\n  })\n\n  it('should ignore errors from activity handler function', async () => {\n    const mockPostMessage = jest.fn(() => {\n      throw new Error('test')\n    })\n\n    mockWindowOpener(mockPostMessage)\n    renderHook(useActivityMonitor)\n\n    expect(addEventListenerSpy).toHaveBeenCalledTimes(4)\n\n    // simulate events\n    const activityHandler = addEventListenerSpy.mock.calls[0]?.[1] as Function\n    expect(() => activityHandler()).not.toThrow()\n  })\n\n  it('should ignore errors from window.addEventListener', () => {\n    jest.spyOn(window, 'addEventListener').mockImplementation(\n      jest.fn(() => {\n        throw new Error('Test')\n      }),\n    )\n\n    mockWindowOpener()\n\n    expect(() => renderHook(useActivityMonitor)).not.toThrow()\n    expect(addEventListenerSpy).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/hooks/useActivityMonitor.ts",
    "content": "import throttle from 'lodash/throttle'\nimport { useEffect } from 'react'\nimport { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\n\nconst throttleTimeout = riConfig.app.activityMonitorThrottleTimeout\nconst windowEvents = ['click', 'keydown', 'scroll', 'touchstart']\n\nconst SESSION_TIME_SECONDS = riConfig.app.sessionTtlSeconds\nconst SESSION_TIME_MS = SESSION_TIME_SECONDS * 1000\nconst CHECK_SESSION_INTERVAL_MS = 10000\n\nlet lastActivityTime: number\nlet checkInterval: ReturnType<typeof setTimeout> | null = null\n\nconst getIsMonitorEnabled = () => !!riConfig.app.activityMonitorOrigin\n\nconst onActivity = throttle(() => {\n  lastActivityTime = +new Date()\n\n  try {\n    // post event to parent window\n    window.opener?.postMessage(\n      { name: 'setLastActivity' },\n      riConfig.app.activityMonitorOrigin,\n    )\n  } catch {\n    // ignore errors\n  }\n}, throttleTimeout)\n\nexport const startActivityMonitor = () => {\n  lastActivityTime = +new Date()\n  try {\n    if (getIsMonitorEnabled()) {\n      checkInterval = setInterval(() => {\n        const now = +new Date()\n        if (now - lastActivityTime >= SESSION_TIME_MS) {\n          // expire session\n          window.location.href = `${riConfig.app.activityMonitorOrigin}/#/logout`\n        }\n      }, CHECK_SESSION_INTERVAL_MS)\n\n      windowEvents.forEach((event) => {\n        window.addEventListener(event, onActivity, {\n          passive: true,\n          capture: true,\n        })\n      })\n    }\n  } catch {\n    // ignore errors\n  }\n}\n\nexport const stopActivityMonitor = () => {\n  try {\n    if (getIsMonitorEnabled()) {\n      if (checkInterval) {\n        clearInterval(checkInterval)\n        checkInterval = null\n      }\n\n      windowEvents.forEach((event) => {\n        window.removeEventListener(event, onActivity, { capture: true })\n      })\n    }\n  } catch {\n    // ignore errors\n  }\n}\n\nexport const useActivityMonitor = () => {\n  useEffect(() => {\n    startActivityMonitor()\n\n    return () => {\n      stopActivityMonitor()\n    }\n  }, [])\n}\n\nexport default useActivityMonitor\n"
  },
  {
    "path": "redisinsight/ui/src/components/main-router/interfaces.ts",
    "content": "export interface RouteParams {\n  instanceId: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CloudLink/CloudLink.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport CloudLink from './CloudLink'\n\ndescribe('CloudLink', () => {\n  it('should render', () => {\n    expect(render(<CloudLink text=\"Link\" url=\"\" />)).toBeTruthy()\n  })\n\n  it('should render proper url', () => {\n    const url = 'https://site.com'\n    render(<CloudLink text=\"Link\" url={url} />)\n\n    expect(\n      screen.getByTestId('guide-free-database-link').getAttribute('href'),\n    ).toBe(url)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CloudLink/CloudLink.tsx",
    "content": "import React from 'react'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nexport interface Props {\n  url: string\n  text: string\n  source: OAuthSocialSource\n}\n\nconst CloudLink = (props: Props) => {\n  const { url, text, source = OAuthSocialSource.Tutorials } = props\n  return (\n    <OAuthSsoHandlerDialog>\n      {(ssoCloudHandlerClick) => (\n        <Link\n          color=\"text\"\n          onClick={(e) => {\n            ssoCloudHandlerClick(e, {\n              source,\n              action: OAuthSocialAction.Create,\n            })\n          }}\n          target=\"_blank\"\n          href={url}\n          data-testid=\"guide-free-database-link\"\n        >\n          {text}\n        </Link>\n      )}\n    </OAuthSsoHandlerDialog>\n  )\n}\n\nexport default CloudLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CloudLink/index.ts",
    "content": "import CloudLink from './CloudLink'\n\nexport default CloudLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/CodeButtonBlock.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport reactRouterDom from 'react-router-dom'\nimport { fireEvent, render, screen, act } from 'uiSrc/utils/test-utils'\nimport { Pages } from 'uiSrc/constants'\nimport { setDBConfigStorageField } from 'uiSrc/services'\nimport { ConfigDBStorageItem } from 'uiSrc/constants/storage'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport CodeButtonBlock, { Props } from './CodeButtonBlock'\n\nconst mockedProps = mock<Props>()\n\nconst simpleContent = 'info'\nconst label = 'btn'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  setDBConfigStorageField: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('CodeButtonBlock', () => {\n  it('should render', () => {\n    const component = render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        content={simpleContent}\n      />,\n    )\n    const { container } = component\n\n    expect(component).toBeTruthy()\n    expect(container).toHaveTextContent(label)\n    expect(container).toHaveTextContent(simpleContent)\n  })\n\n  it('should call onClick function', () => {\n    const onApply = jest.fn()\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        content={simpleContent}\n      />,\n    )\n    fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n\n    expect(onApply).toBeCalled()\n  })\n\n  it('should call onCopy function', () => {\n    const onCopy = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onCopy={onCopy}\n        onApply={jest.fn()}\n        content={simpleContent}\n      />,\n    )\n    fireEvent.click(screen.getByTestId(`copy-btn-${label}`))\n\n    expect(onCopy).toBeCalled()\n  })\n\n  it('should call onApply with provided params', () => {\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ pipeline: '10' }}\n        content={simpleContent}\n      />,\n    )\n    fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n\n    expect(onApply).toBeCalledWith({ pipeline: '10' }, expect.any(Function))\n  })\n\n  it('should not render run button with executable=false param', () => {\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ executable: 'false' }}\n        content={simpleContent}\n      />,\n    )\n\n    expect(screen.queryByTestId(`run-btn-${label}`)).not.toBeInTheDocument()\n  })\n\n  it('should not show confirmation popover with option', async () => {\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={jest.fn}\n        params={{ run_confirmation: 'true' }}\n        content={simpleContent}\n        isShowConfirmation={false}\n      />,\n    )\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n    })\n\n    expect(\n      screen.queryByTestId('tutorial-popover-apply-run'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should go to home page after click on change db', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ run_confirmation: 'true' }}\n        content={simpleContent}\n      />,\n    )\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n    })\n\n    fireEvent.click(screen.getByTestId('tutorial-popover-change-db'))\n\n    expect(pushMock).toBeCalledWith(Pages.home)\n  })\n\n  it('should set show confirmation to LS', async () => {\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ run_confirmation: 'true' }}\n        content={simpleContent}\n      />,\n    )\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('checkbox-show-again'))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('tutorial-popover-apply-run'))\n    })\n\n    expect(setDBConfigStorageField).toBeCalledWith(\n      'instanceId',\n      ConfigDBStorageItem.notShowConfirmationRunTutorial,\n      true,\n    )\n  })\n\n  it('should call proper telemetry on click change db', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ run_confirmation: 'true' }}\n        content={simpleContent}\n      />,\n    )\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('tutorial-popover-change-db'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_DATABASE_CHANGE_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n  })\n\n  it('should call popover with no module loaded', async () => {\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ run_confirmation: 'false' }}\n        content=\"ft.info\"\n      />,\n    )\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n    })\n\n    expect(screen.getByTestId('module-not-loaded-popover')).toBeInTheDocument()\n  })\n\n  it('should call not opened db popover without instanceId', async () => {\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: undefined })\n    const onApply = jest.fn()\n\n    render(\n      <CodeButtonBlock\n        {...instance(mockedProps)}\n        label={label}\n        onApply={onApply}\n        params={{ run_confirmation: 'false' }}\n        content={simpleContent}\n      />,\n    )\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`run-btn-${label}`))\n    })\n\n    expect(\n      screen.getByTestId('database-not-opened-popover'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/CodeButtonBlock.tsx",
    "content": "import cx from 'classnames'\nimport React, { useEffect, useState } from 'react'\nimport { monaco } from 'react-monaco-editor'\nimport parse from 'html-react-parser'\nimport { useParams } from 'react-router-dom'\nimport { find } from 'lodash'\nimport {\n  getCommandsForExecution,\n  getUnsupportedModulesFromQuery,\n  truncateText,\n  handleCopy as handleCopyUtil,\n} from 'uiSrc/utils'\nimport {\n  BooleanParams,\n  CodeButtonParams,\n  MonacoLanguage,\n} from 'uiSrc/constants'\n\nimport { CodeBlock } from 'uiSrc/components'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { getDBConfigStorageField } from 'uiSrc/services'\nimport { ConfigDBStorageItem } from 'uiSrc/constants/storage'\nimport {\n  ModuleNotLoadedMinimalized,\n  DatabaseNotOpened,\n} from 'uiSrc/components/messages'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { ButtonLang } from 'uiSrc/utils/formatters/markdown/remarkCode'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  PlayIcon,\n  CheckBoldIcon,\n  CopyIcon,\n  ToastCheckIcon,\n} from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\n\nimport { RunConfirmationPopover } from './components'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  content: string\n  onApply?: (params?: CodeButtonParams, onFinish?: () => void) => void\n  modules?: AdditionalRedisModule[]\n  onCopy?: () => void\n  label?: string\n  isLoading?: boolean\n  className?: string\n  params?: CodeButtonParams\n  isShowConfirmation?: boolean\n  lang?: string\n}\n\nconst FINISHED_COMMAND_INDICATOR_TIME_MS = 5_000\n\nconst CodeButtonBlock = (props: Props) => {\n  const {\n    lang,\n    onApply,\n    label,\n    // eslint-disable-next-line @typescript-eslint/naming-convention\n    className: _className,\n    params,\n    content,\n    onCopy,\n    modules = [],\n    isShowConfirmation = true,\n    ...rest\n  } = props\n\n  const [highlightedContent, setHighlightedContent] = useState('')\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const [isLoading, setIsLoading] = useState(false)\n  const [isRan, setIsRan] = useState(false)\n  const [isCopied, setIsCopied] = useState(false)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const isButtonHasConfirmation =\n    params?.run_confirmation === BooleanParams.true\n  const isRunButtonHidden = params?.executable === BooleanParams.false\n  const [notLoadedModule] = isRunButtonHidden\n    ? []\n    : getUnsupportedModulesFromQuery(modules, content)\n\n  useEffect(() => {\n    if (!lang) return\n\n    const languageId =\n      lang === ButtonLang.Redis\n        ? MonacoLanguage.Redis\n        : find(monaco.languages?.getLanguages(), ({ id }) => id === lang)?.id\n\n    if (languageId) {\n      monaco.editor.colorize(content.trim(), languageId, {}).then((data) => {\n        setHighlightedContent(data)\n      })\n    }\n  }, [])\n\n  const getIsShowConfirmation = () =>\n    isShowConfirmation &&\n    !getDBConfigStorageField(\n      instanceId,\n      ConfigDBStorageItem.notShowConfirmationRunTutorial,\n    )\n\n  const handleCopy = () => {\n    const query = getCommandsForExecution(content)?.join('\\n') || ''\n    handleCopyUtil(query)\n    setIsCopied(true)\n    onCopy?.()\n  }\n\n  useEffect(() => {\n    if (isCopied) {\n      const timeout = setTimeout(() => {\n        setIsCopied(false)\n      }, 1000)\n      return () => clearTimeout(timeout)\n    }\n    return undefined\n  }, [isCopied])\n\n  const runQuery = () => {\n    setIsLoading(true)\n    onApply?.(params, () => {\n      setIsLoading(false)\n      setIsRan(true)\n      setTimeout(() => setIsRan(false), FINISHED_COMMAND_INDICATOR_TIME_MS)\n    })\n  }\n\n  const handleRunClicked = () => {\n    if (\n      !instanceId ||\n      notLoadedModule ||\n      (getIsShowConfirmation() && isButtonHasConfirmation)\n    ) {\n      setIsPopoverOpen((v) => !v)\n      return\n    }\n\n    runQuery()\n  }\n\n  const handleApplyRun = () => {\n    handleClosePopover()\n    runQuery()\n  }\n\n  const handleClosePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const getPopoverMessage = (): React.ReactNode => {\n    if (!instanceId) {\n      return <DatabaseNotOpened />\n    }\n\n    if (notLoadedModule) {\n      return (\n        <ModuleNotLoadedMinimalized\n          moduleName={notLoadedModule}\n          source={OAuthSocialSource.Tutorials}\n          onClose={() => setIsPopoverOpen(false)}\n        />\n      )\n    }\n\n    return <RunConfirmationPopover onApply={handleApplyRun} />\n  }\n\n  return (\n    <div className={styles.wrapper}>\n      <Row align=\"center\">\n        <FlexItem grow>\n          {!!label && (\n            <Title\n              size=\"XS\"\n              className={styles.label}\n              data-testid=\"code-button-block-label\"\n            >\n              {truncateText(label, 86)}\n            </Title>\n          )}\n        </FlexItem>\n        <FlexItem className={styles.actions}>\n          <EmptyButton\n            onClick={handleCopy}\n            icon={isCopied ? ToastCheckIcon : CopyIcon}\n            disabled={isCopied}\n            size=\"small\"\n            className={cx(styles.actionBtn, styles.copyBtn)}\n            data-testid={`copy-btn-${label}`}\n          >\n            Copy\n          </EmptyButton>\n          {!isRunButtonHidden && (\n            <RiPopover\n              ownFocus\n              panelClassName={cx('popoverLikeTooltip', styles.popover)}\n              anchorClassName={styles.popoverAnchor}\n              anchorPosition=\"upLeft\"\n              isOpen={isPopoverOpen}\n              panelPaddingSize=\"m\"\n              closePopover={handleClosePopover}\n              button={\n                <RiTooltip\n                  content={\n                    isPopoverOpen\n                      ? undefined\n                      : 'Open Workbench in the left menu to see the command results.'\n                  }\n                  data-testid=\"run-btn-open-workbench-tooltip\"\n                >\n                  <EmptyButton\n                    onClick={handleRunClicked}\n                    icon={isRan ? CheckBoldIcon : PlayIcon}\n                    iconSide=\"right\"\n                    size=\"small\"\n                    disabled={isLoading || isRan}\n                    loading={isLoading}\n                    className={cx(styles.actionBtn, styles.runBtn)}\n                    {...rest}\n                    data-testid={`run-btn-${label}`}\n                  >\n                    Run\n                  </EmptyButton>\n                </RiTooltip>\n              }\n            >\n              {getPopoverMessage()}\n            </RiPopover>\n          )}\n        </FlexItem>\n      </Row>\n      <div className={styles.content} data-testid=\"code-button-block-content\">\n        <CodeBlock className={styles.code}>\n          {highlightedContent ? parse(highlightedContent) : content}\n        </CodeBlock>\n      </div>\n      <Spacer size=\"s\" />\n    </div>\n  )\n}\n\nexport default CodeButtonBlock\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/components/RunConfirmationPopover.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  initialStateDefault,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport RunConfirmationPopover from './RunConfirmationPopover'\n\ndescribe('RunConfirmationPopover', () => {\n  it('should render', () => {\n    expect(render(<RunConfirmationPopover onApply={jest.fn()} />)).toBeTruthy()\n  })\n\n  it('should hide \"Change Database\" button when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<RunConfirmationPopover onApply={jest.fn()} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByRole('button', { name: 'Change Database' }),\n    ).toBeInTheDocument()\n  })\n\n  it('should hide \"Change Database\" button when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<RunConfirmationPopover onApply={jest.fn()} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByRole('button', { name: 'Change Database' }),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/components/RunConfirmationPopover.tsx",
    "content": "import React, { useState } from 'react'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { setDBConfigStorageField } from 'uiSrc/services'\nimport { ConfigDBStorageItem } from 'uiSrc/constants/storage'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport styles from '../styles.module.scss'\n\ninterface Props {\n  onApply: () => void\n}\n\nconst RunConfirmationPopover = ({ onApply }: Props) => {\n  const [notShowAgain, setNotShowAgain] = useState(false)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const history = useHistory()\n\n  const handleChangeDatabase = () => {\n    history.push(Pages.home)\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_DATABASE_CHANGE_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const handleApply = () => {\n    if (notShowAgain) {\n      setDBConfigStorageField(\n        instanceId,\n        ConfigDBStorageItem.notShowConfirmationRunTutorial,\n        true,\n      )\n    }\n    onApply?.()\n  }\n\n  return (\n    <>\n      <Title size=\"XS\">Run commands</Title>\n      <Spacer size=\"s\" />\n      <Text size=\"s\">\n        This tutorial will change data in your database, are you sure you want\n        to run commands in this database?\n      </Text>\n      <Spacer size=\"s\" />\n      <Checkbox\n        id=\"showAgain\"\n        name=\"showAgain\"\n        label=\"Don't show again for this database\"\n        checked={notShowAgain}\n        className={styles.showAgainCheckBox}\n        onChange={(e) => setNotShowAgain(e.target.checked)}\n        data-testid=\"checkbox-show-again\"\n        aria-label=\"checkbox do not show agan\"\n      />\n      <div className={styles.popoverFooter}>\n        <Row gap=\"m\" justify=\"end\">\n          <FeatureFlagComponent name={FeatureFlags.envDependent}>\n            <SecondaryButton\n              size=\"s\"\n              className={styles.popoverBtn}\n              onClick={handleChangeDatabase}\n              data-testid=\"tutorial-popover-change-db\"\n            >\n              Change Database\n            </SecondaryButton>\n          </FeatureFlagComponent>\n          <PrimaryButton\n            size=\"s\"\n            className={styles.popoverBtn}\n            onClick={handleApply}\n            data-testid=\"tutorial-popover-apply-run\"\n          >\n            Run\n          </PrimaryButton>\n        </Row>\n      </div>\n    </>\n  )\n}\n\nexport default RunConfirmationPopover\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/components/index.ts",
    "content": "import RunConfirmationPopover from './RunConfirmationPopover'\n\nexport { RunConfirmationPopover }\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/index.ts",
    "content": "import CodeButtonBlock from './CodeButtonBlock'\n\nexport default CodeButtonBlock\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/CodeButtonBlock/styles.module.scss",
    "content": ".wrapper {\n  margin-top: 8px;\n}\n\n.content {\n  max-width: 100%;\n  max-height: 320px;\n  overflow: auto;\n  @include eui.scrollBar;\n\n  border: 1px solid var(--tableDarkestBorderColor);\n  border-radius: 4px;\n\n  font-size: 11px;\n  background: var(--browserTableRowEven);\n  word-wrap: break-word;\n}\n\n.code {\n  word-wrap: break-word;\n}\n\n.actions {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: row !important;\n}\n\n.anchorBtn {\n  display: inline-flex !important;\n}\n\n.actionBtn {\n  &:global(.euiButton) {\n    height: 20px !important;\n    min-width: auto !important;\n    margin-left: 4px;\n    background: none !important;\n    border: 0 !important;\n    box-shadow: none !important;\n\n    :global {\n      .euiButton__content {\n        padding: 0 4px !important;\n      }\n\n      .euiButton__text {\n        font-size: 11px !important;\n        line-height: 12px !important;\n        font-weight: 400 !important;\n      }\n\n      .euiButtonContent__icon {\n        width: 12px;\n        height: 12px;\n      }\n    }\n  }\n}\n\n.copyBtn {\n  &:global(.euiButton) {\n    color: var(--euiTextSubduedColor) !important;\n\n    :global(.euiButton__text) {\n      margin-inline-start: 4px !important;\n      color: var(--euiTextSubduedColor) !important;\n    }\n  }\n}\n\n.runBtn {\n  &:global(.euiButton) {\n    color: var(--buttonSuccessColor) !important;\n\n    :global(.euiButton__text) {\n      color: var(--buttonSuccessColor) !important;\n      margin-inline-end: 4px !important;\n    }\n  }\n\n  :global {\n    .euiLoadingSpinner {\n      width: 12px;\n      height: 12px;\n      border-width: 2px;\n      border-top-color: transparent !important;\n    }\n  }\n}\n\n.popover {\n  min-width: 372px !important;\n}\n\n.popoverAnchor {\n  display: inline-flex !important;\n}\n\n.popoverFooter {\n  margin-top: 12px;\n\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n\n  .showAgainCheckBox {\n    :global(.euiCheckbox__label) {\n      font-size: 12px !important;\n    }\n  }\n\n  .popoverBtn {\n    margin-left: 8px;\n    min-width: auto !important;\n\n    :global(.euiButton__text) {\n      font-size: 12px !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/Image/Image.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'\nimport Image, { Props } from './Image'\n\nconst mockedProps = mock<Props>()\n\ndescribe('Image', () => {\n  it('should render', () => {\n    expect(render(<Image {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render image with absolute path', () => {\n    const path = 'https://image.com/image.png'\n    const { container } = render(\n      <Image {...instance(mockedProps)} src={path} />,\n    )\n    expect(container.querySelector('img')?.getAttribute('src')).toEqual(path)\n  })\n\n  it('should render image with path to static folder', () => {\n    const path = 'static/image.png'\n    const { container } = render(\n      <Image {...instance(mockedProps)} src={path} />,\n    )\n    expect(container.querySelector('img')?.getAttribute('src')).toEqual(\n      `${RESOURCES_BASE_URL}${path}`,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/Image/Image.tsx",
    "content": "import React from 'react'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\nimport { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'\n\nexport interface Props {\n  src: string\n}\nconst Image = ({ src, ...rest }: Props) => {\n  const path: string = IS_ABSOLUTE_PATH.test(src || '')\n    ? src\n    : `${RESOURCES_BASE_URL}${src}`\n  return (\n    // eslint-disable-next-line jsx-a11y/alt-text\n    <img src={path} {...rest} />\n  )\n}\n\nexport default Image\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/Image/index.ts",
    "content": "import Image from './Image'\n\nexport default Image\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisInsightLink/RedisInsightLink.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport RedisInsightLink from './RedisInsightLink'\n\njest.mock('uiSrc/utils/routing', () => ({\n  ...jest.requireActual('uiSrc/utils/routing'),\n}))\n\nObject.defineProperty(window, 'location', {\n  value: {\n    origin: 'http://localhost',\n  },\n  writable: true,\n})\n\ndescribe('RedisInsightLink', () => {\n  it('should render', () => {\n    expect(render(<RedisInsightLink url=\"/\" text=\"label\" />)).toBeTruthy()\n  })\n\n  it('should call proper history push on click', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<RedisInsightLink url=\"/settings\" text=\"label\" />)\n\n    fireEvent.click(screen.getByTestId('redisinsight-link'))\n\n    expect(pushMock).toHaveBeenCalledWith('/settings')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisInsightLink/RedisInsightLink.tsx",
    "content": "import React, { useState } from 'react'\nimport { useHistory, useLocation, useParams } from 'react-router-dom'\nimport cx from 'classnames'\nimport { isNull } from 'lodash'\nimport { getRedirectionPage } from 'uiSrc/utils/routing'\nimport DatabaseNotOpened from 'uiSrc/components/messages/database-not-opened'\n\nimport { Link, RiLinkProps } from 'uiSrc/components/base/link'\nimport { RiPopover } from 'uiSrc/components/base'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  url: string\n  text: string\n  size?: RiLinkProps['size']\n}\n\nconst RedisInsightLink = (props: Props) => {\n  const { url, text, size } = props\n\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const history = useHistory()\n  const location = useLocation()\n\n  const handleLinkClick = (e: React.MouseEvent) => {\n    e.preventDefault()\n\n    const href = getRedirectionPage(url, instanceId, location.pathname)\n    if (href) {\n      history.push(href)\n      return\n    }\n\n    if (isNull(href)) {\n      setIsPopoverOpen(true)\n    }\n  }\n\n  return (\n    <RiPopover\n      ownFocus\n      panelClassName={cx('popoverLikeTooltip', styles.popover)}\n      anchorClassName={styles.popoverAnchor}\n      anchorPosition=\"upLeft\"\n      isOpen={isPopoverOpen}\n      panelPaddingSize=\"m\"\n      closePopover={() => setIsPopoverOpen(false)}\n      button={\n        <Link\n          variant=\"inline\"\n          size={size}\n          href=\"/\"\n          onClick={handleLinkClick}\n          data-testid=\"redisinsight-link\"\n        >\n          {text}\n        </Link>\n      }\n    >\n      <DatabaseNotOpened />\n    </RiPopover>\n  )\n}\n\nexport default RedisInsightLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisInsightLink/index.ts",
    "content": "import RedisInsightLink from './RedisInsightLink'\n\nexport default RedisInsightLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisInsightLink/styles.module.scss",
    "content": ".popover {\n  min-width: 372px !important;\n}\n\n.popoverAnchor {\n  display: inline-flex !important;\n  vertical-align: baseline !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisUploadButton/RedisUploadButton.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport { AxiosError } from 'axios'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  customTutorialsBulkUploadSelector,\n  uploadDataBulk,\n} from 'uiSrc/slices/workbench/wb-custom-tutorials'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { checkResourse } from 'uiSrc/services/resourcesService'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport RedisUploadButton, { Props } from './RedisUploadButton'\n\njest.mock('uiSrc/slices/workbench/wb-custom-tutorials', () => ({\n  ...jest.requireActual('uiSrc/slices/workbench/wb-custom-tutorials'),\n  customTutorialsBulkUploadSelector: jest.fn().mockReturnValue({\n    pathsInProgress: [],\n  }),\n}))\n\njest.mock('uiSrc/services/resourcesService', () => ({\n  ...jest.requireActual('uiSrc/services/resourcesService'),\n  checkResourse: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst props: Props = {\n  label: 'Label',\n  path: '/text',\n}\n\nconst error = {\n  response: {\n    data: {\n      message: 'File not found. Check if this file exists and try again.',\n    },\n  },\n} as AxiosError<any>\n\ndescribe('RedisUploadButton', () => {\n  beforeEach(() => {\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: 'instanceId' })\n  })\n\n  it('should render', () => {\n    expect(render(<RedisUploadButton {...props} />)).toBeTruthy()\n  })\n\n  it('should be disabled with loading state', () => {\n    ;(customTutorialsBulkUploadSelector as jest.Mock).mockReturnValueOnce({\n      pathsInProgress: [props.path],\n    })\n\n    render(<RedisUploadButton {...props} />)\n\n    expect(screen.getByTestId('upload-data-bulk-btn')).toBeDisabled()\n  })\n\n  it('should open warning popover and call proper actions after submit', () => {\n    render(<RedisUploadButton {...props} />)\n\n    fireEvent.click(screen.getByTestId('upload-data-bulk-btn'))\n\n    expect(screen.getByTestId('upload-data-bulk-tooltip')).toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('upload-data-bulk-apply-btn'))\n\n    const expectedActions = [uploadDataBulk(props.path)]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render no database poper', () => {\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: undefined })\n    render(<RedisUploadButton {...props} />)\n\n    fireEvent.click(screen.getByTestId('upload-data-bulk-btn'))\n\n    expect(\n      screen.getByTestId('database-not-opened-popover'),\n    ).toBeInTheDocument()\n  })\n\n  it('should show error when file is not exists', async () => {\n    const checkResourceMock = jest.fn().mockRejectedValue('')\n    ;(checkResourse as jest.Mock).mockImplementation(checkResourceMock)\n\n    render(<RedisUploadButton {...props} />)\n\n    fireEvent.click(screen.getByTestId('upload-data-bulk-btn'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('download-redis-upload-file'))\n    })\n\n    expect(checkResourceMock).toHaveBeenCalledWith('http://localhost:5001/text')\n    const expected = addErrorNotification(error)\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([expect.objectContaining(expected)]),\n    )\n  })\n\n  it('should call proper telemetry events', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(<RedisUploadButton {...props} />)\n\n    fireEvent.click(screen.getByTestId('upload-data-bulk-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_DATA_UPLOAD_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('download-redis-upload-file'))\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_DOWNLOAD_BULK_FILE_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    fireEvent.click(screen.getByTestId('upload-data-bulk-apply-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_DATA_UPLOAD_SUBMITTED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisUploadButton/RedisUploadButton.tsx",
    "content": "import { useDispatch, useSelector } from 'react-redux'\nimport React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { useParams } from 'react-router-dom'\nimport { AxiosError } from 'axios'\nimport { truncateText } from 'uiSrc/utils'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  customTutorialsBulkUploadSelector,\n  uploadDataBulkAction,\n} from 'uiSrc/slices/workbench/wb-custom-tutorials'\n\nimport DatabaseNotOpened from 'uiSrc/components/messages/database-not-opened'\n\nimport {\n  checkResourse,\n  getPathToResource,\n} from 'uiSrc/services/resourcesService'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { PlayFilledIcon, ContractsIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  label: string\n  path: string\n}\n\nconst RedisUploadButton = ({ label, path }: Props) => {\n  const { pathsInProgress } = useSelector(customTutorialsBulkUploadSelector)\n\n  const [isLoading, setIsLoading] = useState<boolean>(false)\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const urlToFile = getPathToResource(path)\n\n  useEffect(() => {\n    setIsLoading(pathsInProgress.includes(path))\n  }, [pathsInProgress])\n\n  const openPopover = () => {\n    if (!isPopoverOpen) {\n      sendEventTelemetry({\n        event: TelemetryEvent.EXPLORE_PANEL_DATA_UPLOAD_CLICKED,\n        eventData: {\n          databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        },\n      })\n    }\n\n    setIsPopoverOpen((v) => !v)\n  }\n\n  const uploadData = async () => {\n    setIsPopoverOpen(false)\n    dispatch(uploadDataBulkAction(instanceId, path))\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_DATA_UPLOAD_SUBMITTED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const handleDownload = async (e: React.MouseEvent) => {\n    e.preventDefault()\n\n    try {\n      await checkResourse(urlToFile)\n\n      const downloadAnchor = document.createElement('a')\n      downloadAnchor.setAttribute('href', `${urlToFile}?download=true`)\n      downloadAnchor.setAttribute('download', label)\n      downloadAnchor.click()\n    } catch {\n      const error = {\n        response: {\n          data: {\n            message: 'File not found. Check if this file exists and try again.',\n          },\n        },\n      } as AxiosError<any>\n      dispatch(addErrorNotification(error))\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_DOWNLOAD_BULK_FILE_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  return (\n    <div className={cx(styles.wrapper, 'mb-s mt-s')}>\n      <RiPopover\n        ownFocus\n        id=\"upload-data-bulk-btn\"\n        anchorPosition=\"downLeft\"\n        isOpen={isPopoverOpen}\n        closePopover={() => setIsPopoverOpen(false)}\n        panelClassName={cx('popoverLikeTooltip', styles.popover)}\n        anchorClassName={styles.popoverAnchor}\n        panelPaddingSize=\"none\"\n        button={\n          <SecondaryButton\n            loading={isLoading}\n            iconSide=\"right\"\n            icon={ContractsIcon}\n            size=\"s\"\n            className={styles.button}\n            onClick={openPopover}\n            color=\"secondary\"\n            data-testid=\"upload-data-bulk-btn\"\n          >\n            {truncateText(label, 86)}\n          </SecondaryButton>\n        }\n      >\n        {instanceId ? (\n          <Text\n            color=\"subdued\"\n            className={styles.containerPopover}\n            data-testid=\"upload-data-bulk-tooltip\"\n          >\n            <RiIcon type=\"ToastDangerIcon\" className={styles.popoverIcon} />\n            <div className={cx(styles.popoverItem, styles.popoverItemTitle)}>\n              Execute commands in bulk\n            </div>\n            <Spacer size=\"s\" />\n            <div className={styles.popoverItem}>\n              All commands from the file in your tutorial will be automatically\n              executed against your database. Avoid executing them in production\n              databases.\n            </div>\n            <Spacer size=\"m\" />\n            <div className={styles.popoverActions}>\n              <Link\n                onClick={handleDownload}\n                className={styles.link}\n                data-testid=\"download-redis-upload-file\"\n              >\n                Download file\n              </Link>\n              <PrimaryButton\n                size=\"s\"\n                icon={PlayFilledIcon}\n                iconSide=\"right\"\n                className={styles.uploadApproveBtn}\n                onClick={uploadData}\n                data-testid=\"upload-data-bulk-apply-btn\"\n              >\n                Execute\n              </PrimaryButton>\n            </div>\n          </Text>\n        ) : (\n          <DatabaseNotOpened />\n        )}\n      </RiPopover>\n    </div>\n  )\n}\n\nexport default RedisUploadButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisUploadButton/index.ts",
    "content": "import RedisUploadButton from './RedisUploadButton'\n\nexport default RedisUploadButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/RedisUploadButton/styles.module.scss",
    "content": ".wrapper {\n  width: 100%;\n  max-width: 480px;\n\n  :global(.euiPopover) {\n    width: 100%;\n  }\n\n  .button {\n    &[class*='euiButton--secondary']:not([class*='isDisabled']) {\n      &:hover {\n        background-color: var(--euiColorSecondary) !important;\n        border-color: var(--euiColorSecondary) !important;\n        color: var(--euiColorPrimaryText) !important;\n      }\n    }\n    &:global(.euiButton.euiButton-isDisabled) {\n      color: var(--buttonSecondaryDisabledTextColor) !important;\n    }\n\n    :global(.euiIcon) {\n      width: 14px;\n      height: 14px;\n    }\n  }\n}\n\n.popover {\n  min-width: 378px !important;\n}\n\n.panelPopover {\n  border-color: var(--euiColorPrimary) !important;\n  :global(.euiPopover__panelArrow--bottom:before) {\n    border-bottom-color: var(--euiColorPrimary) !important;\n  }\n\n  :global(.euiPopover__panelArrow--top:before) {\n    border-top-color: var(--euiColorPrimary) !important;\n  }\n}\n\n.popoverAnchor {\n  display: block;\n  width: 100%;\n}\n\n.popoverIcon {\n  position: absolute;\n  top: 14px;\n  left: 14px;\n\n  color: var(--euiColorWarningLight) !important;\n  width: 18px !important;\n  height: 18px !important;\n}\n\n.popoverItem {\n  font-size: 13px !important;\n  line-height: 18px !important;\n  padding-left: 30px;\n}\n\n.link {\n  color: var(--externalLinkColor) !important;\n}\n\n.popoverActions {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  padding-left: 30px;\n}\n\n.popoverItemTitle {\n  color: var(--htmlColor) !important;\n  font-size: 14px !important;\n  line-height: 24px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/markdown/index.ts",
    "content": "import Image from './Image'\nimport RedisUploadButton from './RedisUploadButton'\nimport CloudLink from './CloudLink'\nimport RedisInsightLink from './RedisInsightLink'\nimport CodeButtonBlock from './CodeButtonBlock'\n\nexport {\n  Image,\n  RedisUploadButton,\n  CloudLink,\n  RedisInsightLink,\n  CodeButtonBlock,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/message-bar/MessageBar.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, waitFor } from 'uiSrc/utils/test-utils'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport MessageBar, { Props } from './MessageBar'\n\nconst mockedProps = mock<Props>()\nconst TOASTIFY_ATTENTION_CLASS = '.Toastify__toast--attention'\nconst TOASTIFY_SUCCESS_CLASS = '.Toastify__toast--success'\n\nconst renderMessageBar = async (\n  children: React.ReactElement,\n  opened = true,\n  variant?: typeof riToast.Variant.Success | typeof riToast.Variant.Attention,\n) => {\n  const screen = render(\n    <MessageBar {...instance(mockedProps)} opened={opened} variant={variant}>\n      {children}\n    </MessageBar>,\n  )\n  if (!opened) {\n    return { ...screen }\n  }\n  await waitFor(() => expect(screen.queryByRole('alert')).toBeInTheDocument(), {\n    timeout: 1000,\n  })\n  return { ...screen }\n}\n\ndescribe('MessageBar', () => {\n  it('should render', () => {\n    expect(render(<MessageBar {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render children', async () => {\n    const { getByTestId, getByText } = await renderMessageBar(\n      <p data-testid=\"text\">lorem ipsum</p>,\n    )\n    expect(getByTestId('text')).toBeTruthy()\n    expect(getByText('lorem ipsum')).toBeInTheDocument()\n  })\n\n  it('should display toast with success variant by default', async () => {\n    const { getByRole, getByText } = await renderMessageBar(\n      <p data-testid=\"default-variant\">Default variant test</p>,\n    )\n\n    const toast = getByRole('alert')\n\n    expect(getByText('Default variant test')).toBeInTheDocument()\n    expect(toast.closest(TOASTIFY_SUCCESS_CLASS)).toBeInTheDocument()\n  })\n\n  it('should display toast with attention variant when specified', async () => {\n    const { getByRole } = await renderMessageBar(\n      <p data-testid=\"attention-variant\">Attention variant test</p>,\n      true,\n      riToast.Variant.Attention,\n    )\n\n    const toast = getByRole('alert')\n    expect(toast.closest(TOASTIFY_ATTENTION_CLASS)).toBeInTheDocument()\n  })\n\n  it('should not display toast when opened is false', async () => {\n    const { queryByTestId } = await renderMessageBar(\n      <p data-testid=\"closed-message\">Should not appear</p>,\n      false,\n    )\n\n    expect(queryByTestId('closed-message')).not.toBeInTheDocument()\n  })\n\n  it('should render complex children content in toast', async () => {\n    const { getByTestId, getByText } = await renderMessageBar(\n      <div data-testid=\"complex-content\">\n        <h3>Title</h3>\n        <p>Description text</p>\n        <button type=\"button\">Action</button>\n      </div>,\n    )\n\n    expect(getByTestId('complex-content')).toBeInTheDocument()\n    expect(getByText('Title')).toBeInTheDocument()\n    expect(getByText('Description text')).toBeInTheDocument()\n    expect(getByText('Action')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/message-bar/MessageBar.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral200};\n  border-radius: 20px;\n  padding: 0 25px 0 35px;\n  max-width: 80%;\n  min-height: 48px;\n  box-shadow: ${({ theme }) => theme.core.shadow.shadow700};\n`\n\nexport const ContainerWrapper = styled(Row)`\n  position: absolute;\n  min-width: 332px;\n  min-height: 48px;\n  bottom: 12px;\n  width: 100%;\n  z-index: 10;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/message-bar/MessageBar.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { riToast, RiToaster } from 'uiSrc/components/base/display/toast'\nimport { ONE_HOUR } from 'uiSrc/components/notifications/constants'\n\nexport interface Props {\n  children?: React.ReactElement\n  opened: boolean\n  variant?: typeof riToast.Variant.Success | typeof riToast.Variant.Attention\n}\n\nexport const MessageBar = ({\n  children,\n  opened,\n  variant = riToast.Variant.Success,\n}: Props) => {\n  useEffect(() => {\n    if (!opened) {\n      return\n    }\n\n    riToast(\n      {\n        message: children,\n      },\n      {\n        variant,\n        containerId: 'autodiscovery-message-bar',\n      },\n    )\n  }, [opened, variant])\n\n  return (\n    <RiToaster\n      data-testid=\"autodiscovery-message-bar\"\n      containerId=\"autodiscovery-message-bar\"\n      autoClose={ONE_HOUR}\n      position=\"top-center\"\n    />\n  )\n}\n\nexport default MessageBar\n"
  },
  {
    "path": "redisinsight/ui/src/components/message-bar/styles.module.scss",
    "content": ":global {\n  .euiPopoverTitle {\n    text-transform: none !important;\n  }\n\n  .euiButton {\n    min-width: 93px !important;\n\n    &--small {\n      min-width: 67px !important;\n    }\n\n    &:focus {\n      text-decoration: none !important;\n    }\n  }\n}\n\n.text {\n  font-size: 13px;\n  text-align: center;\n  color: var(--euiColorPrimaryText);\n  > div {\n    font-size: 13px;\n  }\n}\n.actions {\n  span,\n  svg {\n    font-size: 14px !important;\n  }\n\n  svg {\n    width: 14px;\n    height: 14px;\n  }\n}\n\n.cross svg {\n  width: 20px;\n  height: 20px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/cli-output/cliOutput.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { cliTexts } from './cliOutput'\n\ndescribe('cliTexts', () => {\n  describe('Feature flag dependend', () => {\n    describe('MONITOR_COMMAND', () => {\n      it('should render proper content with flag disabled', async () => {\n        const initialStoreState = set(\n          cloneDeep(initialStateDefault),\n          `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n          { flag: false },\n        )\n\n        const onClick = jest.fn()\n\n        render(cliTexts.MONITOR_COMMAND(onClick), {\n          store: mockStore(initialStoreState),\n        })\n\n        expect(\n          screen.getByTestId('user-profiler-link-disabled'),\n        ).toBeInTheDocument()\n      })\n\n      it('should render proper content with flag enabled', () => {\n        const initialStoreState = set(\n          cloneDeep(initialStateDefault),\n          `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n          { flag: true },\n        )\n\n        const onClick = jest.fn()\n\n        render(cliTexts.MONITOR_COMMAND(onClick), {\n          store: mockStore(initialStoreState),\n        })\n\n        fireEvent.click(screen.getByTestId('monitor-btn'))\n        expect(onClick).toBeCalled()\n      })\n    })\n\n    describe('SUBSCRIBE_COMMAND_CLI', () => {\n      it('should render proper content with flag disabled', async () => {\n        const initialStoreState = set(\n          cloneDeep(initialStateDefault),\n          `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n          { flag: false },\n        )\n\n        render(cliTexts.SUBSCRIBE_COMMAND_CLI(), {\n          store: mockStore(initialStoreState),\n        })\n\n        expect(\n          screen.getByTestId('user-pub-sub-link-disabled'),\n        ).toBeInTheDocument()\n        expect(\n          screen.queryByTestId('user-pub-sub-link'),\n        ).not.toBeInTheDocument()\n      })\n\n      it('should render proper content with flag enabled', () => {\n        const initialStoreState = set(\n          cloneDeep(initialStateDefault),\n          `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n          { flag: true },\n        )\n\n        render(cliTexts.SUBSCRIBE_COMMAND_CLI(), {\n          store: mockStore(initialStoreState),\n        })\n\n        expect(screen.getByTestId('user-pub-sub-link')).toBeInTheDocument()\n        expect(\n          screen.queryByTestId('user-pub-sub-link-disabled'),\n        ).not.toBeInTheDocument()\n      })\n    })\n\n    describe('PSUBSCRIBE_COMMAND_CLI', () => {\n      it('should render proper content with flag disabled', async () => {\n        const initialStoreState = set(\n          cloneDeep(initialStateDefault),\n          `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n          { flag: false },\n        )\n\n        render(cliTexts.PSUBSCRIBE_COMMAND(), {\n          store: mockStore(initialStoreState),\n        })\n\n        expect(\n          screen.getByTestId('user-pub-sub-link-disabled'),\n        ).toBeInTheDocument()\n        expect(\n          screen.queryByTestId('user-pub-sub-link'),\n        ).not.toBeInTheDocument()\n      })\n\n      it('should render proper content with flag enabled', () => {\n        const initialStoreState = set(\n          cloneDeep(initialStateDefault),\n          `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n          { flag: true },\n        )\n\n        render(cliTexts.PSUBSCRIBE_COMMAND(), {\n          store: mockStore(initialStoreState),\n        })\n\n        expect(screen.getByTestId('user-pub-sub-link')).toBeInTheDocument()\n        expect(\n          screen.queryByTestId('user-pub-sub-link-disabled'),\n        ).not.toBeInTheDocument()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/cli-output/cliOutput.tsx",
    "content": "import React, { Fragment } from 'react'\nimport { getRouterLinkProps } from 'uiSrc/services'\nimport { getDbIndex } from 'uiSrc/utils'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\n\nexport const InitOutputText = (\n  host: string = '',\n  port: number = 0,\n  dbIndex: number = 0,\n  emptyOutput: boolean,\n  onClick: () => void,\n) => [\n  <Fragment key={Math.random()}>\n    {emptyOutput && (\n      <span className=\"color-green\" key={Math.random()}>\n        {'Try '}\n        <Link\n          onClick={onClick}\n          className=\"color-green\"\n          style={{ fontSize: 'inherit', fontFamily: 'inherit' }}\n          data-test-subj=\"cli-workbench-page-btn\"\n        >\n          Workbench\n        </Link>\n        , our advanced CLI. Check out our Quick Guides to learn more about Redis\n        capabilities.\n      </span>\n    )}\n  </Fragment>,\n  '\\n\\n',\n  'Connecting...',\n  '\\n\\n',\n  'Pinging Redis server on ',\n  <ColorText\n    component=\"span\"\n    color=\"default\"\n    key={Math.random()}\n    className=\"font-inconsolata\"\n  >\n    {`${host}:${port}${getDbIndex(dbIndex)}`}\n  </ColorText>,\n]\n\nexport const ConnectionSuccessOutputText = [\n  '\\n',\n  'Connected.',\n  '\\n',\n  'Ready to execute commands.',\n  '\\n\\n',\n]\n\nconst unsupportedCommandTextCli =\n  ' is not supported by the Redis Insight CLI. The list of all unsupported commands: '\nconst unsupportedCommandTextWorkbench =\n  ' is not supported by the Workbench. The list of all unsupported commands: '\nexport const cliTexts = {\n  CLI_UNSUPPORTED_COMMANDS: (commandLine: string, commands: string) =>\n    commandLine + unsupportedCommandTextCli + commands,\n  WORKBENCH_UNSUPPORTED_COMMANDS: (commandLine: string, commands: string) =>\n    commandLine + unsupportedCommandTextWorkbench + commands,\n  REPEAT_COUNT_INVALID: 'Invalid repeat command option value',\n  CONNECTION_CLOSED:\n    'Client connection previously closed. Run the command after the connection is re-created.',\n  UNABLE_TO_DECRYPT:\n    'Unable to decrypt. Check the system keychain or re-run the command.',\n  PUB_SUB_NOT_SUPPORTED_ENV: (\n    <div\n      className=\"cli-output-response-fail\"\n      data-testid=\"user-pub-sub-link-disabled\"\n    >\n      PubSub not supported in this environment.\n    </div>\n  ),\n  USE_PSUBSCRIBE_COMMAND: (path: string = '') => (\n    <ColorText color=\"danger\" key={Date.now()} data-testid=\"user-pub-sub-link\">\n      {'Use '}\n      <Link\n        {...getRouterLinkProps(path)}\n        color=\"text\"\n        data-test-subj=\"pubsub-page-btn\"\n      >\n        Pub/Sub\n      </Link>\n      {' to see the messages published to all channels in your database.'}\n    </ColorText>\n  ),\n  PSUBSCRIBE_COMMAND: (path: string = '') => (\n    <FeatureFlagComponent\n      name={FeatureFlags.envDependent}\n      otherwise={cliTexts.PUB_SUB_NOT_SUPPORTED_ENV}\n    >\n      {cliTexts.USE_PSUBSCRIBE_COMMAND(path)}\n    </FeatureFlagComponent>\n  ),\n  PSUBSCRIBE_COMMAND_CLI: (path: string = '') => [\n    cliTexts.PSUBSCRIBE_COMMAND(path),\n    '\\n',\n  ],\n  MONITOR_NOT_SUPPORTED_ENV: (\n    <div\n      className=\"cli-output-response-fail\"\n      data-testid=\"user-profiler-link-disabled\"\n    >\n      Monitor not supported in this environment.\n    </div>\n  ),\n  USE_PROFILER_TOOL: (onClick: () => void) => (\n    <ColorText color=\"danger\" key={Date.now()}>\n      {'Use '}\n      <EmptyButton\n        onClick={onClick}\n        className=\"btnLikeLink\"\n        color=\"text\"\n        data-testid=\"monitor-btn\"\n      >\n        Profiler\n      </EmptyButton>\n      {' tool to see all the requests processed by the server.'}\n    </ColorText>\n  ),\n  MONITOR_COMMAND: (onClick: () => void) => (\n    <FeatureFlagComponent\n      name={FeatureFlags.envDependent}\n      otherwise={cliTexts.MONITOR_NOT_SUPPORTED_ENV}\n    >\n      {cliTexts.USE_PROFILER_TOOL(onClick)}\n    </FeatureFlagComponent>\n  ),\n  USE_PUB_SUB_TOOL: (path: string = '') => (\n    <ColorText color=\"danger\" key={Date.now()} data-testid=\"user-pub-sub-link\">\n      {'Use '}\n      <Link\n        {...getRouterLinkProps(path)}\n        color=\"text\"\n        data-test-subj=\"pubsub-page-btn\"\n      >\n        Pub/Sub\n      </Link>\n      {' tool to subscribe to channels.'}\n    </ColorText>\n  ),\n  SUBSCRIBE_COMMAND_CLI: (path: string = '') => (\n    <FeatureFlagComponent\n      name={FeatureFlags.envDependent}\n      otherwise={cliTexts.PUB_SUB_NOT_SUPPORTED_ENV}\n    >\n      {cliTexts.USE_PUB_SUB_TOOL(path)}\n    </FeatureFlagComponent>\n  ),\n  HELLO3_COMMAND: () => (\n    <ColorText color=\"danger\" key={Date.now()}>\n      {'Redis Insight does not support '}\n      <Link\n        href=\"https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md\"\n        className=\"btnLikeLink\"\n        color=\"text\"\n        target=\"_blank\"\n        data-test-subj=\"hello3-btn\"\n      >\n        RESP3\n      </Link>\n      {' at the moment, but we are working on it.'}\n    </ColorText>\n  ),\n  HELLO3_COMMAND_CLI: () => [cliTexts.HELLO3_COMMAND(), '\\n'],\n  CLI_ERROR_MESSAGE: (message: string) => [\n    '\\n',\n    <ColorText color=\"danger\" key={Date.now()}>\n      {message}\n    </ColorText>,\n    '\\n\\n',\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/database-not-opened/DatabaseNotOpened.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport DatabaseNotOpened from './DatabaseNotOpened'\n\ndescribe('DatabaseNotOpened', () => {\n  it('should render', () => {\n    expect(render(<DatabaseNotOpened />)).toBeTruthy()\n  })\n\n  it('should render links', () => {\n    render(<DatabaseNotOpened />)\n\n    expect(screen.getByTestId('tutorials-get-started-link')).toBeInTheDocument()\n    expect(screen.getByTestId('tutorials-docker-link')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/database-not-opened/DatabaseNotOpened.tsx",
    "content": "import React from 'react'\n\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links'\nimport TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport styles from './styles.module.scss'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { OAuthSsoHandlerDialog } from 'uiSrc/components/oauth'\n\nexport interface Props {\n  source?: OAuthSocialSource\n  onClose?: () => void\n}\n\nconst DatabaseNotOpened = (props: Props) => {\n  const { source = OAuthSocialSource.Tutorials, onClose } = props\n\n  return (\n    <div className={styles.wrapper} data-testid=\"database-not-opened-popover\">\n      <div>\n        <Title size=\"S\" className={styles.title}>\n          Open a database\n        </Title>\n        <Spacer size=\"s\" />\n        <Col>\n          <Text size=\"s\">\n            Open your Redis database, or create a new database to get started.\n          </Text>\n          <Spacer size=\"m\" />\n          <OAuthSsoHandlerDialog>\n            {(ssoCloudHandlerClick) => (\n              <Link\n                external\n                target=\"_blank\"\n                variant=\"inline\"\n                href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n                  campaign: UTM_CAMPAINGS[source] ?? source,\n                })}\n                onClick={(e: React.MouseEvent) => {\n                  ssoCloudHandlerClick(e, {\n                    source,\n                    action: OAuthSocialAction.Create,\n                  })\n                  onClose?.()\n                }}\n                data-testid=\"tutorials-get-started-link\"\n              >\n                Create a free Redis Cloud database\n              </Link>\n            )}\n          </OAuthSsoHandlerDialog>\n          <Spacer size=\"xs\" />\n          <Link\n            external\n            target=\"_blank\"\n            variant=\"inline\"\n            href={getUtmExternalLink(EXTERNAL_LINKS.docker, {\n              campaign: UTM_CAMPAINGS[source] ?? source,\n            })}\n            data-testid=\"tutorials-docker-link\"\n          >\n            Install using Docker\n          </Link>\n        </Col>\n      </div>\n      <img\n        src={TelescopeImg}\n        className={styles.img}\n        alt=\"telescope\"\n        loading=\"lazy\"\n      />\n    </div>\n  )\n}\n\nexport default DatabaseNotOpened\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/database-not-opened/index.ts",
    "content": "import DatabaseNotOpened from './DatabaseNotOpened'\n\nexport default DatabaseNotOpened\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/database-not-opened/styles.module.scss",
    "content": ".wrapper {\n  display: flex;\n\n  .title {\n    font-size: 14px;\n    font-weight: 500;\n    line-height: 17px;\n  }\n}\n\n\n.img {\n  width: 80px;\n  margin-left: 20px;\n  transform: scale(-1, 1);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/feature-not-available/FeatureNotAvailable.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport FeatureNotAvailable from './FeatureNotAvailable'\nimport { FILTER_NOT_AVAILABLE_CONTENT } from './constants'\n\ndescribe('FeatureNotAvailable', () => {\n  it('should render', () => {\n    expect(\n      render(<FeatureNotAvailable content={FILTER_NOT_AVAILABLE_CONTENT} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/feature-not-available/FeatureNotAvailable.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Col)`\n  padding: 40px 60px;\n  text-align: center;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/feature-not-available/FeatureNotAvailable.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { OAuthSocialAction } from 'uiSrc/slices/interfaces'\nimport {\n  FeatureFlagComponent,\n  OAuthConnectFreeDb,\n  OAuthSsoHandlerDialog,\n} from 'uiSrc/components'\nimport { freeInstancesSelector } from 'uiSrc/slices/instances/instances'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport {\n  EXTERNAL_LINKS,\n  UTM_CAMPAINGS,\n  UTM_MEDIUMS,\n} from 'uiSrc/constants/links'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { FeatureNotAvailableProps } from './FeatureNotAvailable.types'\nimport * as S from './FeatureNotAvailable.styles'\n\nconst FeatureNotAvailable = ({\n  onClose,\n  content,\n}: FeatureNotAvailableProps) => {\n  const freeInstances = useSelector(freeInstancesSelector) || []\n  const learnMoreUtm = {\n    medium: UTM_MEDIUMS.Main,\n    campaign: UTM_CAMPAINGS[content.oauthSource],\n  }\n\n  return (\n    <S.Container gap=\"l\" data-testid={content.testId}>\n      <RiIcon type=\"RedisDbBlueIcon\" size=\"original\" />\n      <Title size=\"L\" data-testid={`${content.testId}-title`}>\n        {content.title}\n      </Title>\n      <Text color=\"primary\" data-testid={`${content.testId}-description`}>\n        {content.description}\n      </Text>\n      {!!freeInstances.length && (\n        <>\n          <Text color=\"primary\">{content.freeInstanceText}</Text>\n          <OAuthConnectFreeDb\n            id={freeInstances[0].id}\n            source={content.oauthSource}\n            onSuccessClick={onClose}\n          />\n        </>\n      )}\n      {!freeInstances.length && (\n        <FeatureFlagComponent name={FeatureFlags.cloudAds}>\n          <Text color=\"primary\">{content.noInstanceText}</Text>\n          <Col align=\"center\" gap=\"m\">\n            <OAuthSsoHandlerDialog>\n              {(ssoCloudHandlerClick) => (\n                <PrimaryButton\n                  onClick={(e) => {\n                    ssoCloudHandlerClick(e, {\n                      source: content.oauthSource,\n                      action: OAuthSocialAction.Create,\n                    })\n                    onClose?.()\n                  }}\n                  data-testid={`${content.testId}-get-started-link`}\n                  size=\"m\"\n                >\n                  Get Started For Free\n                </PrimaryButton>\n              )}\n            </OAuthSsoHandlerDialog>\n            <Link\n              variant=\"inline\"\n              target=\"_blank\"\n              href={getUtmExternalLink(EXTERNAL_LINKS.redisStack, learnMoreUtm)}\n              data-testid={`${content.testId}-learn-more-link`}\n            >\n              Learn More\n            </Link>\n          </Col>\n        </FeatureFlagComponent>\n      )}\n    </S.Container>\n  )\n}\n\nexport default FeatureNotAvailable\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/feature-not-available/FeatureNotAvailable.types.ts",
    "content": "import { OAuthSocialSource } from 'uiSrc/slices/interfaces'\n\nexport interface FeatureNotAvailableContent {\n  testId: string\n  title: string\n  description: string\n  freeInstanceText: string\n  noInstanceText: string\n  oauthSource: OAuthSocialSource\n}\n\nexport interface FeatureNotAvailableProps {\n  onClose?: () => void\n  content: FeatureNotAvailableContent\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/feature-not-available/constants.ts",
    "content": "import { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { FeatureNotAvailableContent } from './FeatureNotAvailable.types'\n\nexport const FILTER_NOT_AVAILABLE_CONTENT: FeatureNotAvailableContent = {\n  testId: 'filter-not-available',\n  title: 'Upgrade your Redis database to version 6 or above',\n  description: 'Filtering by data type is supported in Redis 6 and above.',\n  freeInstanceText:\n    'Use your free all-in-one Redis Cloud database to start exploring these capabilities.',\n  noInstanceText:\n    'Create a free Redis Cloud database that supports filtering and extends the core capabilities of your Redis.',\n  oauthSource: OAuthSocialSource.BrowserFiltering,\n}\n\nexport const REDISEARCH_VERSION_REQUIRED_CONTENT: FeatureNotAvailableContent = {\n  testId: 'redisearch-version-required',\n  title: 'Redis Query Engine 2.0+ required',\n  description:\n    'This feature requires Redis Query Engine 2.0 or later (included with Redis 6+). ' +\n    'Older versions of the query engine are not compatible with the commands used here.',\n  freeInstanceText:\n    'Use your free all-in-one Redis Cloud database to start exploring these capabilities.',\n  noInstanceText:\n    'Create a free Redis Cloud database to start exploring these capabilities.',\n  oauthSource: OAuthSocialSource.BrowserSearch,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/feature-not-available/index.ts",
    "content": "import FeatureNotAvailable from './FeatureNotAvailable'\n\nexport default FeatureNotAvailable\nexport type {\n  FeatureNotAvailableContent,\n  FeatureNotAvailableProps,\n} from './FeatureNotAvailable.types'\nexport {\n  FILTER_NOT_AVAILABLE_CONTENT,\n  REDISEARCH_VERSION_REQUIRED_CONTENT,\n} from './constants'\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/index.ts",
    "content": "import ModuleNotLoaded from './module-not-loaded'\nimport ModuleNotLoadedMinimalized from './module-not-loaded-minimalized'\nimport FeatureNotAvailable from './feature-not-available'\nimport DatabaseNotOpened from './database-not-opened'\n\nexport {\n  ModuleNotLoaded,\n  ModuleNotLoadedMinimalized,\n  FeatureNotAvailable,\n  DatabaseNotOpened,\n}\nexport type { FeatureNotAvailableContent } from './feature-not-available'\nexport {\n  FILTER_NOT_AVAILABLE_CONTENT,\n  REDISEARCH_VERSION_REQUIRED_CONTENT,\n} from './feature-not-available'\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  act,\n  fireEvent,\n  mockFeatureFlags,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport * as utils from 'uiSrc/utils/modules'\nimport { Instance, RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport ModuleNotLoaded, { IProps } from './ModuleNotLoaded'\n\nconst props: IProps = {\n  moduleName: RedisDefaultModules.Search,\n  type: 'browser',\n  id: 'id',\n  onClose: jest.fn(),\n}\n\nconst mockUseHistory = { push: jest.fn() }\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\njest.mock(\n  'uiSrc/assets/img/icons/mobile_module_not_loaded.svg?react',\n  () => 'div',\n)\njest.mock('uiSrc/assets/img/icons/module_not_loaded.svg?react', () => 'div')\njest.mock('uiSrc/assets/img/telescope-dark.svg?react', () => 'div')\njest.mock('uiSrc/assets/img/icons/cheer.svg?react', () => 'div')\n\nconst mockGetDbWithModuleLoaded = (value?: boolean) => {\n  jest\n    .spyOn(utils, 'getDbWithModuleLoaded')\n    .mockImplementation(() => value as unknown as Instance)\n}\n\nconst TEST_IDS = {\n  ctaWrapper: 'module-not-loaded-cta-wrapper',\n}\n\ndescribe('ModuleNotLoaded', () => {\n  beforeEach(() => {\n    jest.resetAllMocks()\n    mockFeatureFlags()\n    mockGetDbWithModuleLoaded()\n  })\n\n  it('should render', () => {\n    expect(render(<ModuleNotLoaded {...props} />)).toBeTruthy()\n  })\n\n  it('should render free text when cloudAds feature is enabled and no free db exists', () => {\n    const { queryByText } = render(<ModuleNotLoaded {...props} />)\n    expect(\n      queryByText(\n        /Create a free all-in-one Redis Cloud database to start exploring these capabilities./,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should render free db text when cloudAds feature is enabled and free db exists', () => {\n    mockGetDbWithModuleLoaded(true)\n    const { queryByText } = render(<ModuleNotLoaded {...props} />)\n    expect(\n      queryByText(\n        /Use your free all-in-one Redis Cloud database to start exploring these capabilities./,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should render expected text when cloudAds feature is disabled', () => {\n    mockFeatureFlags({\n      cloudAds: {\n        flag: false,\n      },\n    })\n    mockGetDbWithModuleLoaded(true) // should not affect output\n    const { queryByText } = render(<ModuleNotLoaded {...props} />)\n    expect(\n      queryByText(/Open a database with Redis Query Engine/),\n    ).toBeInTheDocument()\n  })\n\n  it('should not show CTA button when envDependant feature is disabled', () => {\n    mockFeatureFlags({\n      envDependent: {\n        flag: false,\n      },\n    })\n    const { queryByTestId } = render(<ModuleNotLoaded {...props} />)\n    expect(queryByTestId(TEST_IDS.ctaWrapper)).toBeEmptyDOMElement()\n  })\n\n  it('should show \"Get Started For Free\" button when envDependant feature is enabled and cloudAds feature is enabled', () => {\n    const { queryByText, getByText } = render(<ModuleNotLoaded {...props} />)\n    expect(getByText(/Get Started For Free/)).toBeInTheDocument()\n    expect(queryByText(/Redis Databases page/)).not.toBeInTheDocument()\n  })\n\n  it('should show \"Redis Databases page\" button when envDependant feature is enabled and cloudAds feature is disabled', async () => {\n    mockFeatureFlags({\n      cloudAds: {\n        flag: false,\n      },\n    })\n    jest\n      .spyOn(reactRouterDom, 'useHistory')\n      .mockImplementation(() => mockUseHistory as unknown as any)\n\n    const { queryByText, getByText } = render(<ModuleNotLoaded {...props} />)\n    const databasesButton = getByText(/Redis Databases page/)\n    expect(databasesButton).toBeInTheDocument()\n    expect(queryByText(/Get Started For Free/)).not.toBeInTheDocument()\n\n    // click button\n    act(() => {\n      fireEvent.click(databasesButton)\n    })\n\n    // assert\n    expect(mockUseHistory.push).toHaveBeenCalledTimes(1)\n    expect(mockUseHistory.push).toHaveBeenCalledWith('/')\n  })\n\n  it('should show expected text when cloudAds feature is disabled', () => {\n    mockFeatureFlags({\n      cloudAds: {\n        flag: false,\n      },\n    })\n    const { getByText } = render(<ModuleNotLoaded {...props} />)\n    expect(\n      getByText(/Open a database with Redis Query Engine/),\n    ).toBeInTheDocument()\n  })\n\n  it('should show expected text when free db exists', () => {\n    mockGetDbWithModuleLoaded(true)\n    const { getByText } = render(<ModuleNotLoaded {...props} />)\n    expect(\n      getByText(\n        /Use your free all-in-one Redis Cloud database to start exploring these capabilities./,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should show expected text when free db does not exist', () => {\n    const { getByText } = render(<ModuleNotLoaded {...props} />)\n    expect(\n      getByText(\n        /Create a free all-in-one Redis Cloud database to start exploring these capabilities./,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should uppercase first letter of module name in title - time series', () => {\n    const { getByText } = render(\n      <ModuleNotLoaded\n        {...props}\n        moduleName={RedisDefaultModules.TimeSeries}\n      />,\n    )\n    expect(\n      getByText(\n        /Time series data structure is not available for this database/,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should uppercase first letter of module name in title - bloom', () => {\n    const { getByText } = render(\n      <ModuleNotLoaded {...props} moduleName={RedisDefaultModules.Bloom} />,\n    )\n    expect(\n      getByText(\n        /Probabilistic data structures are not available for this database/,\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { useSelector } from 'react-redux'\n\nimport MobileIcon from 'uiSrc/assets/img/icons/mobile_module_not_loaded.svg?react'\nimport DesktopIcon from 'uiSrc/assets/img/icons/module_not_loaded.svg?react'\nimport TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg?react'\nimport CheerIcon from 'uiSrc/assets/img/icons/cheer.svg?react'\nimport {\n  FeatureFlags,\n  MODULE_NOT_LOADED_CONTENT as CONTENT,\n  MODULE_TEXT_VIEW,\n} from 'uiSrc/constants'\nimport { OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { OAuthConnectFreeDb } from 'uiSrc/components'\nimport { freeInstancesSelector } from 'uiSrc/slices/instances/instances'\n\nimport { getDbWithModuleLoaded } from 'uiSrc/utils'\nimport { useCapability } from 'uiSrc/services'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport ModuleNotLoadedButton from './ModuleNotLoadedButton'\nimport styles from './styles.module.scss'\n\nexport const MODULE_OAUTH_SOURCE_MAP: {\n  [key in RedisDefaultModules]?: String\n} = {\n  [RedisDefaultModules.Bloom]: 'RedisBloom',\n  [RedisDefaultModules.ReJSON]: 'RedisJSON',\n  [RedisDefaultModules.Search]: 'RediSearch',\n  [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries',\n}\n\nexport interface IProps {\n  moduleName: RedisDefaultModules\n  id: string\n  onClose?: () => void\n  type?: 'workbench' | 'browser'\n}\n\nconst MIN_ELEMENT_WIDTH = 1210\nconst MAX_ELEMENT_WIDTH = 1440\n\nconst renderTitle = (width: number, moduleName?: string) => (\n  <Title size=\"M\" className={styles.title} data-testid=\"welcome-page-title\">\n    {`${moduleName?.substring(0, 1).toUpperCase()}${moduleName?.substring(1)} ${[MODULE_TEXT_VIEW.redisgears, MODULE_TEXT_VIEW.bf].includes(moduleName) ? 'are' : 'is'} not available `}\n    {width > MAX_ELEMENT_WIDTH && <br />}\n    for this database\n  </Title>\n)\n\nconst ListItem = ({ item }: { item: string }) => (\n  <li className={styles.listItem}>\n    <div className={styles.iconWrapper}>\n      <CheerIcon className={styles.listIcon} />\n    </div>\n    <ColorText className={styles.text}>{item}</ColorText>\n  </li>\n)\n\nconst ModuleNotLoaded = ({\n  moduleName,\n  id,\n  type = 'workbench',\n  onClose,\n}: IProps) => {\n  const [width, setWidth] = useState(0)\n  const freeInstances = useSelector(freeInstancesSelector) || []\n  const { [FeatureFlags.cloudAds]: cloudAdsFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const module = MODULE_OAUTH_SOURCE_MAP[moduleName]\n\n  const freeDbWithModule = getDbWithModuleLoaded(freeInstances, moduleName)\n  const source =\n    type === 'browser'\n      ? OAuthSocialSource.BrowserSearch\n      : OAuthSocialSource[module as keyof typeof OAuthSocialSource]\n\n  useCapability(source)\n\n  useEffect(() => {\n    const parentEl = document?.getElementById(id)\n    if (parentEl) {\n      setWidth(parentEl.offsetWidth)\n    }\n  })\n\n  const renderText = useCallback(\n    (moduleName?: string) => {\n      if (!cloudAdsFeature?.flag) {\n        return (\n          <Text className={cx(styles.text, styles.marginBottom)}>\n            Open a database with {moduleName}.\n          </Text>\n        )\n      }\n\n      return !freeDbWithModule ? (\n        <Text className={cx(styles.text, styles.marginBottom)}>\n          Create a free all-in-one Redis Cloud database to start exploring these\n          capabilities.\n        </Text>\n      ) : (\n        <Text\n          className={cx(styles.text, styles.marginBottom, styles.textFooter)}\n        >\n          Use your free all-in-one Redis Cloud database to start exploring these\n          capabilities.\n        </Text>\n      )\n    },\n    [freeDbWithModule],\n  )\n\n  return (\n    <div\n      className={cx(styles.container, {\n        [styles.fullScreen]: width > MAX_ELEMENT_WIDTH || type === 'browser',\n        [styles.modal]: type === 'browser',\n      })}\n    >\n      <div className={styles.flex}>\n        <div>\n          {type !== 'browser' &&\n            (width > MAX_ELEMENT_WIDTH ? (\n              <DesktopIcon className={styles.bigIcon} />\n            ) : (\n              <MobileIcon className={styles.icon} />\n            ))}\n          {type === 'browser' && (\n            <TelescopeImg className={styles.iconTelescope} />\n          )}\n        </div>\n        <div\n          className={styles.contentWrapper}\n          data-testid=\"module-not-loaded-content\"\n        >\n          {renderTitle(width, MODULE_TEXT_VIEW[moduleName])}\n          <Spacer size=\"l\" />\n          <Text className={styles.bigText}>\n            {CONTENT[moduleName]?.text.map((item: string) =>\n              width > MIN_ELEMENT_WIDTH ? (\n                <>\n                  {item}\n                  <br />\n                </>\n              ) : (\n                item\n              ),\n            )}\n          </Text>\n          <Spacer size=\"m\" />\n          <ul\n            className={cx(styles.list, {\n              [styles.bloomList]: moduleName === RedisDefaultModules.Bloom,\n            })}\n          >\n            {CONTENT[moduleName]?.improvements.map((item: string) => (\n              <ListItem key={item} item={item} />\n            ))}\n          </ul>\n          {!!CONTENT[moduleName]?.additionalText && (\n            <>\n              <Spacer size=\"l\" />\n              <Text\n                className={cx(\n                  styles.text,\n                  styles.additionalText,\n                  styles.marginBottom,\n                )}\n              >\n                {CONTENT[moduleName]?.additionalText.map((item: string) =>\n                  width > MIN_ELEMENT_WIDTH ? (\n                    <>\n                      {item}\n                      <br />\n                    </>\n                  ) : (\n                    item\n                  ),\n                )}\n              </Text>\n            </>\n          )}\n          {renderText(MODULE_TEXT_VIEW[moduleName])}\n        </div>\n      </div>\n      <div\n        className={styles.linksWrapper}\n        data-testid=\"module-not-loaded-cta-wrapper\"\n      >\n        {freeDbWithModule ? (\n          <OAuthConnectFreeDb source={source} id={freeDbWithModule.id} />\n        ) : (\n          <ModuleNotLoadedButton\n            moduleName={moduleName}\n            module={module}\n            type={type}\n            onClose={onClose}\n          />\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport default React.memo(ModuleNotLoaded)\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoadedButton.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { useHistory } from 'react-router-dom'\nimport {\n  FeatureFlags,\n  MODULE_NOT_LOADED_CONTENT as CONTENT,\n  Pages,\n} from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport styles from 'uiSrc/components/messages/module-not-loaded/styles.module.scss'\nimport { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links'\nimport {\n  OAuthSocialAction,\n  OAuthSocialSource,\n  RedisDefaultModules,\n} from 'uiSrc/slices/interfaces'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nexport interface IProps {\n  moduleName: RedisDefaultModules\n  module?: String\n  onClose?: () => void\n  type?: 'workbench' | 'browser'\n}\n\nconst ModuleNotLoadedButton = ({\n  moduleName,\n  type,\n  onClose,\n  module,\n}: IProps) => {\n  const history = useHistory()\n  const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const utmCampaign =\n    type === 'browser'\n      ? UTM_CAMPAINGS[OAuthSocialSource.BrowserSearch]\n      : UTM_CAMPAINGS[OAuthSocialSource.Workbench]\n\n  if (!envDependentFeature?.flag) {\n    return null\n  }\n\n  return (\n    <>\n      <Link\n        className={cx(styles.text, styles.link)}\n        target=\"_blank\"\n        href={getUtmExternalLink(CONTENT[moduleName]?.link, {\n          campaign: utmCampaign,\n        })}\n        data-testid=\"learn-more-link\"\n      >\n        Learn More\n      </Link>\n      <FeatureFlagComponent\n        name={FeatureFlags.cloudAds}\n        otherwise={\n          <Link\n            className={styles.link}\n            target=\"_blank\"\n            href=\"\"\n            onClick={(e) => {\n              e.preventDefault()\n              e.stopPropagation()\n\n              history.push(Pages.home)\n            }}\n            data-testid=\"get-started-link\"\n          >\n            <PrimaryButton size=\"s\" className={styles.btnLink}>\n              Redis Databases page\n            </PrimaryButton>\n          </Link>\n        }\n      >\n        <OAuthSsoHandlerDialog>\n          {(ssoCloudHandlerClick) => (\n            <Link\n              className={styles.link}\n              target=\"_blank\"\n              href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n                campaign: utmCampaign,\n              })}\n              onClick={(e) => {\n                ssoCloudHandlerClick(e, {\n                  source:\n                    type === 'browser'\n                      ? OAuthSocialSource.BrowserSearch\n                      : OAuthSocialSource[\n                          module as keyof typeof OAuthSocialSource\n                        ],\n                  action: OAuthSocialAction.Create,\n                })\n                onClose?.()\n              }}\n              data-testid=\"get-started-link\"\n            >\n              <PrimaryButton size=\"s\" className={styles.btnLink}>\n                Get Started For Free\n              </PrimaryButton>\n            </Link>\n          )}\n        </OAuthSsoHandlerDialog>\n      </FeatureFlagComponent>\n    </>\n  )\n}\n\nexport default ModuleNotLoadedButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded/index.ts",
    "content": "import ModuleNotLoaded from './ModuleNotLoaded'\n\nexport * from './ModuleNotLoaded'\n\nexport default ModuleNotLoaded\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded/styles.module.scss",
    "content": ".container {\n  padding: 41px 30px;\n\n  .title {\n    font-family: 'Graphik', sans-serif;\n    font-size: 32px;\n    font-weight: 600;\n    word-break: break-word;\n    margin-bottom: 20px;\n\n    &::first-letter {\n      text-transform: uppercase;\n    }\n  }\n\n  .linksWrapper .text,\n  .text {\n    font-family: 'Graphik', sans-serif;\n    font-size: 14px;\n    line-height: 17px;\n    word-break: break-word;\n    color: var(--wbTextColor) !important;\n    text-decoration: none !important;\n  }\n\n  .bigText {\n    font-family: 'Graphik', sans-serif;\n    font-size: 20px;\n    line-height: 24px;\n    word-break: break-word;\n    color: var(--wbTextColor);\n  }\n\n  .flex {\n    display: flex;\n    flex-direction: row-reverse;\n    align-items: center;\n    justify-content: space-between;\n\n    .icon {\n      width: 173px;\n      margin-left: 15px;\n    }\n\n    .bigIcon {\n      width: 317px;\n      margin-bottom: 42px;\n    }\n\n    .iconTelescope {\n      width: 250px;\n      margin-left: 15px;\n    }\n  }\n\n  .list.bloomList {\n    display: flex;\n    flex-wrap: wrap;\n\n    .listItem {\n      margin-right: 18px;\n    }\n  }\n\n  .listItem {\n    display: flex;\n\n    .text {\n      white-space: nowrap;\n    }\n\n    .iconWrapper {\n      display: flex;\n      flex: 0 0 16px;\n      align-items: center;\n      justify-content: center;\n      width: 16px;\n      height: 16px;\n      background: var(--wbActiveIconColor);\n      border-radius: 50%;\n      margin-right: 10px;\n    }\n\n    .listIcon {\n      width: 10px;\n      height: 10px;\n\n      path {\n        fill: var(--euiPageBackgroundColor);\n      }\n    }\n  }\n\n  .linksWrapper {\n    display: flex;\n    justify-content: flex-end;\n    align-items: center;\n    margin-top: 20px;\n\n    .link {\n      margin-right: 20px;\n      color: var(-wbTextColor);\n    }\n  }\n\n  .marginBottom {\n    margin-bottom: 20px;\n  }\n\n  &.fullScreen {\n    padding: 89px 77px;\n\n    .flex {\n      flex-direction: column;\n    }\n\n    .contentWrapper {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n    }\n\n    .title,\n    .text,\n    .bigText {\n      text-align: center;\n    }\n\n    .list {\n      display: flex;\n      justify-content: center;\n    }\n\n    .listItem {\n      margin-right: 15px;\n\n      &:last-child {\n        margin-right: 0;\n      }\n    }\n\n    .linksWrapper {\n      justify-content: center;\n      flex-direction: column-reverse;\n    }\n\n    .link {\n      margin-right: 0;\n\n      &:first-child {\n        margin-top: 13px;\n      }\n    }\n  }\n\n  &.modal {\n    padding: 30px;\n\n    .title {\n      padding-top: 42px;\n      font-size: 18px;\n      line-height: 1;\n    }\n\n    .bigText {\n      font-size: 14px;\n    }\n\n    .textFooter {\n      font-weight: 500;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.spec.tsx",
    "content": "import React from 'react'\nimport { mockFeatureFlags, render, screen } from 'uiSrc/utils/test-utils'\nimport { OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { freeInstancesSelector } from 'uiSrc/slices/instances/instances'\nimport ModuleNotLoadedMinimalized from './ModuleNotLoadedMinimalized'\n\nconst moduleName = RedisDefaultModules.Search\nconst source = OAuthSocialSource.Tutorials\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  freeInstancesSelector: jest.fn().mockReturnValue([\n    {\n      id: 'instanceId',\n    },\n  ]),\n}))\n\ndescribe('ModuleNotLoadedMinimalized', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <ModuleNotLoadedMinimalized moduleName={moduleName} source={source} />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render connect to instance body when free instance is added', () => {\n    ;(freeInstancesSelector as jest.Mock).mockReturnValue([\n      {\n        id: 'instanceId',\n        modules: [\n          {\n            name: moduleName,\n          },\n        ],\n      },\n    ])\n    render(\n      <ModuleNotLoadedMinimalized moduleName={moduleName} source={source} />,\n    )\n\n    expect(screen.getByTestId('connect-free-db-btn')).toBeInTheDocument()\n  })\n\n  it('should render add free db body when free instance is not added', () => {\n    ;(freeInstancesSelector as jest.Mock).mockReturnValue(null)\n\n    render(\n      <ModuleNotLoadedMinimalized moduleName={moduleName} source={source} />,\n    )\n\n    expect(screen.getByTestId('tutorials-get-started-link')).toBeInTheDocument()\n  })\n\n  it('should render expected text and \"Redis databases page\" button when cloudAds feature flag is disabled', () => {\n    mockFeatureFlags({\n      cloudAds: {\n        flag: false,\n      },\n    })\n\n    render(\n      <ModuleNotLoadedMinimalized moduleName={moduleName} source={source} />,\n    )\n\n    expect(\n      screen.queryByTestId('tutorials-get-started-link'),\n    ).not.toBeInTheDocument()\n    expect(screen.queryByTestId('connect-free-db-btn')).not.toBeInTheDocument()\n    expect(screen.getByText(/Redis Databases page/)).toBeInTheDocument()\n    expect(\n      screen.getByText(/Open a database with Redis Query Engine/),\n    ).toBeInTheDocument()\n  })\n\n  it('should render expected text when cloudAds feature flag is enabled', () => {\n    mockFeatureFlags({\n      cloudAds: {\n        flag: true,\n      },\n    })\n\n    render(\n      <ModuleNotLoadedMinimalized moduleName={moduleName} source={source} />,\n    )\n\n    expect(\n      screen.getByText(\n        /Create a free Redis Cloud database with search and query/,\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded-minimalized/ModuleNotLoadedMinimalized.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg'\nimport {\n  OAuthSocialAction,\n  OAuthSocialSource,\n  RedisDefaultModules,\n} from 'uiSrc/slices/interfaces'\nimport { freeInstancesSelector } from 'uiSrc/slices/instances/instances'\n\nimport {\n  FeatureFlagComponent,\n  OAuthConnectFreeDb,\n  OAuthSsoHandlerDialog,\n} from 'uiSrc/components'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links'\nimport {\n  getDbWithModuleLoaded,\n  getSourceTutorialByCapability,\n} from 'uiSrc/utils'\nimport { useCapability } from 'uiSrc/services'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport {\n  MODULE_CAPABILITY_TEXT_NOT_AVAILABLE,\n  MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE,\n} from './constants'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  moduleName: RedisDefaultModules\n  source: OAuthSocialSource\n  onClose?: () => void\n}\n\nconst ModuleNotLoadedMinimalized = (props: Props) => {\n  const history = useHistory()\n  const { [FeatureFlags.cloudAds]: cloudAdsFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n  const { moduleName, source, onClose } = props\n  const freeInstances = useSelector(freeInstancesSelector) || []\n\n  const sourceTutorial = getSourceTutorialByCapability(moduleName)\n  const moduleText = cloudAdsFeature?.flag\n    ? MODULE_CAPABILITY_TEXT_NOT_AVAILABLE[moduleName]\n    : MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE[moduleName]\n  const freeDbWithModule = getDbWithModuleLoaded(freeInstances, moduleName)\n\n  useCapability(sourceTutorial)\n\n  return (\n    <div className={styles.wrapper} data-testid=\"module-not-loaded-popover\">\n      <div>\n        <Title size=\"S\" className={styles.title}>\n          {moduleText?.title}\n        </Title>\n        <Spacer size=\"s\" />\n        <FeatureFlagComponent\n          name={FeatureFlags.cloudAds}\n          otherwise={\n            <>\n              <Text color=\"subdued\" size=\"s\">\n                {moduleText?.text}\n              </Text>\n              <Spacer size=\"s\" />\n              <PrimaryButton\n                size=\"s\"\n                className={styles.btnLink}\n                onClick={() => {\n                  history.push(Pages.home)\n                }}\n              >\n                Redis Databases page\n              </PrimaryButton>\n            </>\n          }\n        >\n          {!freeDbWithModule ? (\n            <>\n              <Text size=\"s\">{moduleText?.text}</Text>\n              <Spacer size=\"s\" />\n              <OAuthSsoHandlerDialog>\n                {(ssoCloudHandlerClick) => (\n                  <Link\n                    external\n                    target=\"_blank\"\n                    href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n                      campaign: UTM_CAMPAINGS[source] ?? source,\n                    })}\n                    onClick={(e: React.MouseEvent) => {\n                      ssoCloudHandlerClick(\n                        e,\n                        {\n                          source,\n                          action: OAuthSocialAction.Create,\n                        },\n                        `${moduleName}_${source}`,\n                      )\n                      onClose?.()\n                    }}\n                    data-testid=\"tutorials-get-started-link\"\n                  >\n                    Start with Cloud for free\n                  </Link>\n                )}\n              </OAuthSsoHandlerDialog>\n            </>\n          ) : (\n            <>\n              <Text size=\"s\">\n                Use your free all-in-one Redis Cloud database to start exploring\n                these capabilities.\n              </Text>\n              <Spacer size=\"s\" />\n              <OAuthConnectFreeDb\n                id={freeDbWithModule.id}\n                source={sourceTutorial}\n              />\n            </>\n          )}\n        </FeatureFlagComponent>\n      </div>\n      <img\n        src={TelescopeImg}\n        className={styles.img}\n        alt=\"telescope\"\n        loading=\"lazy\"\n      />\n    </div>\n  )\n}\n\nexport default ModuleNotLoadedMinimalized\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts",
    "content": "import { RedisDefaultModules } from 'uiSrc/slices/interfaces'\n\nexport const MODULE_CAPABILITY_TEXT_NOT_AVAILABLE: {\n  [key in RedisDefaultModules]?: {\n    title: string\n    text: string\n  }\n} = {\n  [RedisDefaultModules.Bloom]: {\n    title: 'Probabilistic data structures are not available',\n    text: 'Create a free Redis Cloud database with probabilistic data structures that extend the core capabilities of your Redis.',\n  },\n  [RedisDefaultModules.ReJSON]: {\n    title: 'JSON data structure is not available',\n    text: 'Create a free Redis Cloud database with JSON capability that extends the core capabilities of your Redis.',\n  },\n  [RedisDefaultModules.Search]: {\n    title: 'Redis Query Engine capability is not available',\n    text: 'Create a free Redis Cloud database with search and query features that extend the core capabilities of your Redis.',\n  },\n  [RedisDefaultModules.TimeSeries]: {\n    title: 'Time series data structure is not available',\n    text: 'Create a free Redis Cloud database with the time series data structure that extends the core capabilities of your Redis.',\n  },\n}\n\nexport const MODULE_CAPABILITY_TEXT_NOT_AVAILABLE_ENTERPRISE: {\n  [key in RedisDefaultModules]?: {\n    title: string\n    text: string\n  }\n} = {\n  [RedisDefaultModules.Bloom]: {\n    title: 'Probabilistic data structures are not available',\n    text: 'Open a database with probabilistic data structures.',\n  },\n  [RedisDefaultModules.ReJSON]: {\n    title: 'JSON data structure is not available',\n    text: 'Open a database with JSON.',\n  },\n  [RedisDefaultModules.Search]: {\n    title: 'Redis Query Engine capability is not available',\n    text: 'Open a database with Redis Query Engine.',\n  },\n  [RedisDefaultModules.TimeSeries]: {\n    title: 'Time series data structure is not available',\n    text: 'Open a database with time series data structure.',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded-minimalized/index.ts",
    "content": "import ModuleNotLoadedMinimalized from './ModuleNotLoadedMinimalized'\n\nexport default ModuleNotLoadedMinimalized\n"
  },
  {
    "path": "redisinsight/ui/src/components/messages/module-not-loaded-minimalized/styles.module.scss",
    "content": ".wrapper {\n  display: flex;\n\n  .title {\n    font-size: 14px;\n    font-weight: 500;\n    line-height: 17px;\n  }\n}\n\n\n.img {\n  width: 80px;\n  margin-left: 20px;\n  transform: scale(-1, 1);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/MonacoEditor.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MonacoEditor from './MonacoEditor'\n\ndescribe('MonacoEditor', () => {\n  it('should render', () => {\n    expect(\n      render(<MonacoEditor value=\"val\" onChange={jest.fn()} language=\"val\" />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/MonacoEditor.tsx",
    "content": "import React, { useContext, useEffect, useRef, useState } from 'react'\nimport ReactMonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor'\nimport cx from 'classnames'\nimport { merge } from 'lodash'\n\nimport { MonacoThemes, darkTheme, lightTheme } from 'uiSrc/constants/monaco'\nimport { Nullable, triggerUpdateCursorPosition } from 'uiSrc/utils'\nimport {\n  IEditorMount,\n  ISnippetController,\n} from 'uiSrc/pages/workbench/interfaces'\nimport { DSL, Theme } from 'uiSrc/constants'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { ActionIconButton } from 'uiSrc/components/base/forms/buttons'\nimport { MonacoGlobalStyles } from 'uiSrc/components/base/code-editor/CodeEditor.styles'\nimport DedicatedEditor from './components/dedicated-editor'\nimport styles from './styles.module.scss'\n\nexport interface CommonProps {\n  value: string\n  onChange?: (value: string) => void\n  onApply?: (event: React.MouseEvent, closeEditor: () => void) => void\n  onDecline?: (event?: React.MouseEvent<HTMLElement>) => void\n  disabled?: boolean\n  readOnly?: boolean\n  isEditable?: boolean\n  wrapperClassName?: string\n  editorWrapperClassName?: string\n  options?: monacoEditor.editor.IStandaloneEditorConstructionOptions\n  dedicatedEditorOptions?: monacoEditor.editor.IStandaloneEditorConstructionOptions\n  dedicatedEditorLanguages?: DSL[]\n  dedicatedEditorKeywords?: string[]\n  dedicatedEditorFunctions?: monacoEditor.languages.CompletionItem[]\n  onChangeLanguage?: (langId: DSL) => void\n  shouldOpenDedicatedEditor?: boolean\n  onOpenDedicatedEditor?: () => void\n  onSubmitDedicatedEditor?: (langId: DSL) => void\n  onCloseDedicatedEditor?: (langId: DSL) => void\n  'data-testid'?: string\n  fullHeight?: boolean\n}\n\nexport interface Props extends CommonProps {\n  onEditorDidMount?: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => void\n  onEditorWillMount?: (monaco: typeof monacoEditor) => void\n  className?: string\n  language: string\n}\nconst MonacoEditor = (props: Props) => {\n  const {\n    value,\n    onChange,\n    onApply,\n    onDecline,\n    onEditorDidMount,\n    onEditorWillMount,\n    onChangeLanguage,\n    disabled,\n    readOnly,\n    isEditable,\n    language,\n    wrapperClassName,\n    editorWrapperClassName,\n    className,\n    options = {},\n    dedicatedEditorOptions = {},\n    dedicatedEditorLanguages = [],\n    dedicatedEditorKeywords = [],\n    dedicatedEditorFunctions = [],\n    shouldOpenDedicatedEditor,\n    onOpenDedicatedEditor,\n    onSubmitDedicatedEditor,\n    onCloseDedicatedEditor,\n    'data-testid': dataTestId = 'monaco-editor',\n    fullHeight,\n  } = props\n\n  let contribution: Nullable<ISnippetController> = null\n  const [isEditing, setIsEditing] = useState(!readOnly && !disabled)\n  const [isDedicatedEditorOpen, setIsDedicatedEditorOpen] = useState(false)\n  const monacoObjects = useRef<Nullable<IEditorMount>>(null)\n  const input = useRef<HTMLDivElement>(null)\n\n  const { theme } = useContext(ThemeContext)\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        contribution?.dispose?.()\n      },\n    [],\n  )\n\n  useEffect(() => {\n    monacoObjects.current?.editor.updateOptions({\n      readOnly: !isEditing && (disabled || readOnly),\n    })\n  }, [disabled, readOnly, isEditing])\n\n  useEffect(() => {\n    if (shouldOpenDedicatedEditor) {\n      setIsDedicatedEditorOpen(true)\n      onOpenDedicatedEditor?.()\n    }\n  }, [shouldOpenDedicatedEditor])\n\n  const editorDidMount = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => {\n    monacoObjects.current = { editor, monaco }\n\n    // hack for exit from snippet mode after click Enter until no answer from monaco authors\n    // https://github.com/microsoft/monaco-editor/issues/2756\n    contribution =\n      editor.getContribution<ISnippetController>('snippetController2')\n\n    editor.onKeyDown(onKeyDownMonaco)\n\n    if (dedicatedEditorLanguages?.length) {\n      editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Space, () => {\n        onPressWidget()\n      })\n    }\n\n    onEditorDidMount?.(editor, monaco)\n  }\n\n  const editorWillMount = (monaco: typeof monacoEditor) => {\n    onEditorWillMount?.(monaco)\n  }\n\n  const onKeyDownMonaco = (e: monacoEditor.IKeyboardEvent) => {\n    // trigger parameter hints\n    if (\n      e.keyCode === monacoEditor.KeyCode.Enter ||\n      e.keyCode === monacoEditor.KeyCode.Space\n    ) {\n      onExitSnippetMode()\n    }\n  }\n\n  const onExitSnippetMode = () => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects?.current\n\n    if (contribution?.isInSnippet?.()) {\n      const { lineNumber = 0, column = 0 } = editor?.getPosition() ?? {}\n      editor.setSelection(\n        new monacoEditor.Selection(lineNumber, column, lineNumber, column),\n      )\n      contribution?.cancel?.()\n    }\n  }\n\n  const onPressWidget = () => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects?.current\n\n    setIsDedicatedEditorOpen(true)\n    onOpenDedicatedEditor?.()\n    editor.updateOptions({ readOnly: true })\n  }\n\n  const updateArgFromDedicatedEditor = (value: string, selectedLang: DSL) => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects?.current\n\n    const model = editor.getModel()\n    if (!model) return\n    const position = editor.getPosition()\n\n    editor.updateOptions({ readOnly: false })\n    editor.executeEdits(null, [\n      {\n        range: new monacoEditor.Range(\n          position?.lineNumber!,\n          position?.column!,\n          position?.lineNumber!,\n          position?.column! + value.length,\n        ),\n        text: value.replaceAll('\\n', ' '),\n      },\n    ])\n    setIsDedicatedEditorOpen(false)\n    triggerUpdateCursorPosition(editor)\n    onSubmitDedicatedEditor?.(selectedLang)\n  }\n\n  const onCancelDedicatedEditor = (selectedLang: DSL) => {\n    setIsDedicatedEditorOpen(false)\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects?.current\n\n    editor.updateOptions({ readOnly: false })\n    triggerUpdateCursorPosition(editor)\n    onCloseDedicatedEditor?.(selectedLang)\n  }\n\n  if (monacoEditor?.editor) {\n    monacoEditor.editor.defineTheme(MonacoThemes.Dark, darkTheme)\n    monacoEditor.editor.defineTheme(MonacoThemes.Light, lightTheme)\n  }\n\n  const monacoOptions: monacoEditor.editor.IStandaloneEditorConstructionOptions =\n    merge(\n      {\n        wordWrap: 'on',\n        automaticLayout: true,\n        formatOnPaste: false,\n        padding: { top: 10 },\n        suggest: {\n          preview: false,\n          showStatusBar: false,\n          showIcons: false,\n          showProperties: false,\n        },\n        quickSuggestions: false,\n        minimap: {\n          enabled: false,\n        },\n        overviewRulerLanes: 0,\n        hideCursorInOverviewRuler: true,\n        overviewRulerBorder: false,\n        lineNumbersMinChars: 4,\n      },\n      options,\n    )\n\n  const handleApply = (_value: string, event: React.MouseEvent) => {\n    onApply?.(event, () => setIsEditing(false))\n  }\n\n  const handleDecline = (event?: React.MouseEvent<HTMLElement>) => {\n    setIsEditing(false)\n    onDecline?.(event)\n  }\n\n  return (\n    <div\n      className={cx(styles.wrapper, wrapperClassName, {\n        disabled,\n        [styles.isEditing]: isEditing && readOnly,\n      })}\n      style={fullHeight ? { flex: '1 1 auto' } : undefined}\n    >\n      <MonacoGlobalStyles />\n      <InlineItemEditor\n        onApply={handleApply}\n        onDecline={handleDecline}\n        viewChildrenMode={!isEditing || !readOnly}\n        declineOnUnmount={false}\n        preventOutsideClick\n      >\n        <div\n          className={cx('inlineMonacoEditor', editorWrapperClassName)}\n          data-testid={`wrapper-${dataTestId}`}\n          ref={input}\n          style={fullHeight ? { height: '100%' } : undefined}\n        >\n          <ReactMonacoEditor\n            language={language}\n            theme={theme === Theme.Dark ? 'dark' : 'light'}\n            value={value ?? ''}\n            onChange={onChange}\n            options={monacoOptions}\n            className={cx(styles.editor, className, {\n              readMode: !isEditing && readOnly,\n            })}\n            editorDidMount={editorDidMount}\n            editorWillMount={editorWillMount}\n            data-testid={dataTestId}\n          />\n        </div>\n      </InlineItemEditor>\n      {isDedicatedEditorOpen && (\n        <DedicatedEditor\n          initialHeight={input?.current?.scrollHeight || 0}\n          langs={dedicatedEditorLanguages}\n          customOptions={dedicatedEditorOptions}\n          keywords={dedicatedEditorKeywords}\n          functions={dedicatedEditorFunctions}\n          onChangeLanguage={onChangeLanguage}\n          onSubmit={updateArgFromDedicatedEditor}\n          onCancel={onCancelDedicatedEditor}\n        />\n      )}\n      {isEditable && readOnly && !isEditing && (\n        <ActionIconButton\n          variant=\"secondary\"\n          onClick={() => setIsEditing(true)}\n          className={styles.editBtn}\n          data-testid=\"edit-monaco-value\"\n          icon={EditIcon}\n        />\n      )}\n    </div>\n  )\n}\n\nexport default MonacoEditor\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/dedicated-editor/DedicatedEditor.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { DSL } from 'uiSrc/constants'\nimport DedicatedEditor, { Props } from './DedicatedEditor'\n\nconst SELECT_LANGUAGES_TEST_ID = 'dedicated-editor-language-select'\n\nconst mockedProps = mock<Props>()\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('DedicatedEditor', () => {\n  it('should render', () => {\n    expect(render(<DedicatedEditor {...instance(mockedProps)} />)).toBeTruthy()\n  })\n  it('should not render select languages if langs.length < 2', () => {\n    const { queryByTestId } = render(\n      <DedicatedEditor\n        {...instance(mockedProps)}\n        langs={[DSL.sqliteFunctions]}\n      />,\n    )\n\n    expect(queryByTestId(SELECT_LANGUAGES_TEST_ID!)).not.toBeInTheDocument()\n  })\n  it('should render select languages if langs.length >= 2', () => {\n    const { queryByTestId } = render(\n      <DedicatedEditor\n        {...instance(mockedProps)}\n        langs={[DSL.sqliteFunctions, DSL.jmespath]}\n      />,\n    )\n\n    expect(queryByTestId(SELECT_LANGUAGES_TEST_ID!)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/dedicated-editor/DedicatedEditor.tsx",
    "content": "import React, { useContext, useEffect, useRef, useState } from 'react'\nimport styled from 'styled-components'\nimport { compact, findIndex, first, merge } from 'lodash'\nimport AutoSizer, { Size } from 'react-virtualized-auto-sizer'\nimport ReactMonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor'\nimport { Rnd } from 'react-rnd'\nimport cx from 'classnames'\n\nimport {\n  decoration,\n  getMonacoAction,\n  MonacoAction,\n  Nullable,\n  toModelDeltaDecoration,\n} from 'uiSrc/utils'\nimport {\n  DEDICATED_EDITOR_LANGUAGES,\n  DSL,\n  MonacoLanguage,\n  MonacoSyntaxLang,\n  Theme,\n} from 'uiSrc/constants'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon, CheckThinIcon } from 'uiSrc/components/base/icons'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { MonacoGlobalStyles } from 'uiSrc/components/base/code-editor/CodeEditor.styles'\nimport styles from './styles.module.scss'\n\nconst LangSelect = styled(RiSelect)`\n  appearance: none;\n  border: 0 none;\n  outline: none;\n  background-color: transparent;\n  max-width: 200px;\n  max-height: 26px;\n  &:active,\n  &:focus,\n  &:hover,\n  &[data-state='open'] {\n    background-color: transparent;\n  }\n`\n\nexport interface Props {\n  query?: string\n  langId?: DSL\n  langs?: DSL[]\n  onChangeLanguage?: (langId: DSL) => void\n  onSubmit: (query: string, langId: DSL) => void\n  onCancel: (langId: DSL) => void\n  initialHeight: number\n  customOptions?: monacoEditor.editor.IStandaloneEditorConstructionOptions\n  keywords?: string[]\n  functions?: monacoEditor.languages.CompletionItem[]\n}\n\n// paddings of main editor\nconst WRAPPER_PADDINGS_HEIGHT = 18\nconst BOTTOM_INDENT_PADDING = 6\n\nconst notCommandRegEx = /^\\s|\\/\\//\nlet decorationCollection: Nullable<monacoEditor.editor.IEditorDecorationsCollection> =\n  null\n\nconst DedicatedEditor = (props: Props) => {\n  const {\n    initialHeight,\n    query = '',\n    langId,\n    langs = [],\n    onChangeLanguage,\n    onCancel,\n    onSubmit,\n    customOptions = {},\n    keywords,\n    functions,\n  } = props\n\n  const [value, setValue] = useState<string>(query)\n  const [height, setHeight] = useState(initialHeight)\n  const [selectedLang, setSelectedLang] = useState(\n    DEDICATED_EDITOR_LANGUAGES[!langs.length ? langId! : first(langs)!],\n  )\n  const monacoObjects = useRef<Nullable<IEditorMount>>(null)\n  const rndRef = useRef<Nullable<any>>(null)\n\n  const { theme } = useContext(ThemeContext)\n\n  const optionsLangs = langs.map((lang) => ({\n    value: lang,\n    label: DEDICATED_EDITOR_LANGUAGES[lang]?.name,\n  }))\n\n  let disposeCompletionItemProvider = () => {}\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        disposeCompletionItemProvider()\n      },\n    [],\n  )\n\n  useEffect(() => {\n    if (height === 0) return\n\n    const rndHeight = rndRef.current?.resizableElement.current.offsetHeight || 0\n    const rndTop = rndRef.current?.draggable.state.y\n    if (height < rndTop + rndHeight + WRAPPER_PADDINGS_HEIGHT) {\n      rndRef?.current.updatePosition({\n        x: 0,\n        y: height - rndHeight - WRAPPER_PADDINGS_HEIGHT,\n      })\n    }\n  }, [height])\n\n  useEffect(() => {\n    if (!monacoObjects.current) return\n    const commands = value.split('\\n')\n    const { monaco } = monacoObjects.current\n\n    const newDecorations = compact(\n      commands.map((command, index) => {\n        if (!command || notCommandRegEx.test(command)) return null\n        const lineNumber = index + 1\n\n        return toModelDeltaDecoration(\n          decoration(\n            monaco,\n            `decoration_${lineNumber}`,\n            lineNumber,\n            1,\n            lineNumber,\n            1,\n          ),\n        )\n      }),\n    )\n\n    decorationCollection?.set(newDecorations)\n  }, [value])\n\n  const onResize = ({ height }: Size): void => {\n    setHeight(height!)\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === 'Escape') {\n      onCancel(selectedLang.id as DSL)\n    }\n  }\n\n  const handleSubmit = () => {\n    const { editor } = monacoObjects?.current || {}\n    const val = editor\n      ?.getValue()\n      .split('\\n')\n      .map((line: string, i: number) =>\n        i > 0 && !notCommandRegEx.test(line) ? `\\t${line}` : line,\n      )\n      .join('\\n')\n    onSubmit(val || '', selectedLang.id as DSL)\n  }\n\n  const editorDidMount = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => {\n    monacoObjects.current = {\n      editor,\n      monaco,\n    }\n\n    setTimeout(() => editor.focus(), 0)\n\n    setupMonacoLang(monaco, selectedLang)\n    editor.addAction(\n      getMonacoAction(MonacoAction.Submit, () => handleSubmit(), monaco),\n    )\n\n    decorationCollection = editor.createDecorationsCollection()\n  }\n\n  const setupMonacoLang = (\n    monaco: typeof monacoEditor,\n    selectedLang: MonacoSyntaxLang,\n  ) => {\n    const languages = monaco.languages.getLanguages()\n\n    if (!selectedLang) return\n\n    const isLangRegistered =\n      findIndex(languages, { id: selectedLang.language }) > -1\n    if (isLangRegistered) {\n      return\n    }\n    monaco.languages.register({ id: selectedLang.language })\n\n    monaco.languages.setLanguageConfiguration(\n      selectedLang.language,\n      selectedLang.config!,\n    )\n\n    disposeCompletionItemProvider =\n      monaco.languages.registerCompletionItemProvider(\n        selectedLang.language,\n        selectedLang.completionProvider?.(keywords, functions)!,\n      ).dispose\n\n    try {\n      monaco.languages.setMonarchTokensProvider(\n        selectedLang.language,\n        selectedLang.tokensProvider?.(keywords, functions)!,\n      )\n    } catch (exception) {\n      console.error(\n        `Monaco ${selectedLang.language} language setup error: `,\n        exception,\n      )\n    }\n  }\n\n  const onChangeLanguageSelect = (id: string) => {\n    const selectedLang = DEDICATED_EDITOR_LANGUAGES[id]\n    setSelectedLang(selectedLang)\n\n    setupMonacoLang(monacoObjects.current?.monaco!, selectedLang)\n\n    onChangeLanguage?.(id as DSL)\n  }\n\n  const options: monacoEditor.editor.IStandaloneEditorConstructionOptions =\n    merge(\n      {\n        tabCompletion: 'on',\n        wordWrap: 'on',\n        padding: { top: 10 },\n        automaticLayout: true,\n        formatOnPaste: false,\n        suggest: {\n          preview: false,\n          showStatusBar: false,\n          showIcons: true,\n        },\n        minimap: {\n          enabled: false,\n        },\n        overviewRulerLanes: 0,\n        hideCursorInOverviewRuler: true,\n        overviewRulerBorder: false,\n        lineNumbersMinChars: 4,\n      },\n      customOptions,\n    )\n\n  return (\n    <AutoSizer onResize={onResize}>\n      {() => (\n        <div className=\"editorBounder\">\n          <MonacoGlobalStyles />\n          <Rnd\n            ref={rndRef}\n            default={{\n              x: 0,\n              y: initialHeight * 0.4 - BOTTOM_INDENT_PADDING,\n              width: '100%',\n              height: '60%',\n            }}\n            minHeight=\"80px\"\n            enableResizing={{\n              top: true,\n              right: false,\n              bottom: true,\n              left: false,\n              topRight: false,\n              bottomRight: false,\n              bottomLeft: false,\n              topLeft: false,\n            }}\n            resizeHandleClasses={{\n              top: 't_resize-top',\n              bottom: 't_resize-bottom',\n            }}\n            dragAxis=\"y\"\n            bounds=\".editorBounder\"\n            dragHandleClassName=\"draggable-area\"\n            className={styles.rnd}\n            data-testid=\"draggable-area\"\n          >\n            <div\n              className={styles.container}\n              onKeyDown={handleKeyDown}\n              role=\"textbox\"\n              tabIndex={0}\n            >\n              <div className=\"draggable-area\" />\n              <div className={styles.input} data-testid=\"query-input-container\">\n                <ReactMonacoEditor\n                  language={selectedLang?.language || MonacoLanguage.Cypher}\n                  theme={theme === Theme.Dark ? 'dark' : 'light'}\n                  value={value}\n                  onChange={setValue}\n                  options={options}\n                  className={`${langId}-editor`}\n                  editorDidMount={editorDidMount}\n                />\n              </div>\n              <div className={cx(styles.actions)}>\n                {langs?.length < 2 && <span>{selectedLang?.name}</span>}\n                {langs?.length >= 2 && (\n                  <LangSelect\n                    name=\"dedicated-editor-language-select\"\n                    placeholder=\"Select language\"\n                    value={selectedLang.id}\n                    options={optionsLangs}\n                    onChange={onChangeLanguageSelect}\n                    data-testid=\"dedicated-editor-language-select\"\n                  />\n                )}\n                <div>\n                  <IconButton\n                    icon={CancelSlimIcon}\n                    aria-label=\"Cancel editing\"\n                    className={styles.declineBtn}\n                    onClick={() => onCancel(selectedLang.id as DSL)}\n                    data-testid=\"cancel-btn\"\n                  />\n                  <IconButton\n                    icon={CheckThinIcon}\n                    type=\"submit\"\n                    aria-label=\"Apply\"\n                    onClick={handleSubmit}\n                    className={styles.applyBtn}\n                    data-testid=\"apply-btn\"\n                  />\n                </div>\n              </div>\n            </div>\n          </Rnd>\n        </div>\n      )}\n    </AutoSizer>\n  )\n}\n\nexport default React.memo(DedicatedEditor)\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/dedicated-editor/index.ts",
    "content": "import DedicatedEditor from './DedicatedEditor'\n\nexport default DedicatedEditor\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/dedicated-editor/styles.module.scss",
    "content": ".rnd {\n  width: 100% !important;\n  z-index: 100;\n}\n.container {\n  height: 100%;\n  word-break: break-word;\n  text-align: left;\n  letter-spacing: 0;\n  background-color: var(--monacoBgColor);\n  color: var(--euiTextSubduedColor) !important;\n  border: 1px solid var(--euiColorPrimary);\n  border-radius: 4px;\n  padding-left: 6px;\n  padding-right: 6px;\n  box-shadow: 0 5px 15px var(--controlsBoxShadowColor);\n\n  .selectLanguage {\n    border: none !important;\n    background: none !important;\n    height: 24px;\n    margin-top: 18px;\n  }\n}\n\n.containerPlaceholder {\n  display: flex;\n  background-color: var(--monacoBgColor);\n  color: var(--euiTextSubduedColor) !important;\n  border: 1px solid var(--euiColorLightShade);\n  border-radius: 4px;\n  overflow: hidden;\n  > div {\n    border: 1px solid var(--euiColorLightShade);\n    background-color: var(--euiColorEmptyShade);\n    padding: 8px 20px;\n    width: 100%;\n  }\n}\n\n.input {\n  height: calc(100% - 46px);\n  width: 100%;\n}\n\n#script {\n  font: normal normal bold 14px/17px Inconsolata !important;\n  color: var(--textColorShade);\n  caret-color: var(--euiColorFullShade);\n  min-width: 5px;\n  display: inline;\n}\n\n:global(.draggable-area) {\n  height: 16px;\n  width: 100%;\n  cursor: grab;\n  background-color: var(--monacoBgColor);\n  border-radius: 4px 4px 0 0;\n}\n\n.actions {\n  height: 26px;\n  width: 100%;\n  display: flex;\n  align-items: center;\n  padding: 6px;\n  background-color: var(--monacoBgColor);\n  border-radius: 0 0 4px 4px;\n  justify-content: space-between;\n}\n\n.declineBtn:hover span {\n  color: var(--euiColorColorDanger) !important;\n}\n\n.applyBtn {\n  margin-left: 6px;\n  &:hover span {\n    color: var(--euiColorPrimary) !important;\n  }\n}\n\n.submitButton {\n  color: var(--rsSubmitBtn) !important;\n  width: 44px !important;\n  height: 44px !important;\n\n  svg {\n    width: 24px;\n    height: 24px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MonacoJson from './MonacoJson'\n\ndescribe('MonacoJson', () => {\n  it('should render', () => {\n    expect(render(<MonacoJson value=\"val\" onChange={jest.fn()} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-json/MonacoJson.tsx",
    "content": "import React from 'react'\nimport * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'\n\nimport { MonacoEditor } from 'uiSrc/components/monaco-editor'\nimport { CommonProps } from 'uiSrc/components/monaco-editor/MonacoEditor'\n\nconst MonacoJson = (props: CommonProps) => {\n  const editorDidMount = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => {\n    monaco.languages.json.jsonDefaults.setDiagnosticsOptions({\n      validate: true,\n      schemaValidation: 'error',\n      schemaRequest: 'error',\n      trailingCommas: 'error',\n    })\n    const messageContribution = editor.getContribution(\n      'editor.contrib.messageController',\n    )\n    editor.onDidAttemptReadOnlyEdit(() => messageContribution?.dispose())\n  }\n\n  return (\n    <MonacoEditor\n      {...props}\n      language=\"json\"\n      className=\"json-monaco-editor\"\n      onEditorDidMount={editorDidMount}\n    />\n  )\n}\n\nexport default MonacoJson\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-json/index.ts",
    "content": "import MonacoJson from './MonacoJson'\n\nexport default MonacoJson\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MonacoYaml from './MonacoYaml'\n\nconst monacoTestId = 'monaco-yaml'\n\ndescribe('MonacoYaml', () => {\n  it('should render', () => {\n    const { queryByTestId } = render(\n      <MonacoYaml\n        schema={{}}\n        value=\"val\"\n        onChange={jest.fn()}\n        data-testid={monacoTestId}\n      />,\n    )\n    expect(queryByTestId(monacoTestId)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/MonacoYaml.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { isNull } from 'lodash'\n\nimport { CommonProps } from 'uiSrc/components/monaco-editor/MonacoEditor'\nimport { MonacoEditor } from 'uiSrc/components/monaco-editor'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  DEFAULT_MONACO_FILE_MATCH,\n  DEFAULT_MONACO_YAML_URI,\n} from 'uiSrc/constants'\nimport { monacoYamlModel } from './monacoYamlModel'\n\nexport interface Props extends CommonProps {\n  schema: Nullable<object>\n  uri?: string\n  fileMatch?: string\n}\n\nconst MonacoYaml = (props: Props) => {\n  const {\n    schema,\n    uri = DEFAULT_MONACO_YAML_URI,\n    fileMatch = DEFAULT_MONACO_FILE_MATCH,\n    value,\n    onChange,\n    ...rest\n  } = props\n\n  useEffect(() => {\n    if (!isNull(schema)) {\n      monacoYamlModel?.update({\n        schemas: [\n          {\n            schema,\n            uri,\n            fileMatch: [fileMatch],\n          },\n        ],\n      })\n    }\n  }, [schema, uri, fileMatch])\n\n  const editorWillMount = () => {\n    monacoYamlModel?.update({\n      schemas: [\n        {\n          schema: schema || {},\n          uri,\n          fileMatch: [fileMatch],\n        },\n      ],\n    })\n  }\n\n  return (\n    <MonacoEditor\n      {...rest}\n      language=\"yaml\"\n      value={value}\n      onChange={onChange}\n      onEditorWillMount={editorWillMount}\n      options={{\n        hover: {\n          enabled: true,\n        },\n        stickyScroll: {\n          enabled: true,\n          defaultModel: 'indentationModel',\n        },\n        tabSize: 2,\n        insertSpaces: true,\n        renderWhitespace: 'boundary',\n        quickSuggestions: {\n          other: 'inline',\n          comments: true,\n          strings: true,\n        },\n        suggest: {\n          preview: true,\n          showStatusBar: true,\n          showIcons: true,\n          showProperties: true,\n        },\n      }}\n    />\n  )\n}\n\nexport default React.memo(MonacoYaml)\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/index.ts",
    "content": "import MonacoYaml from './MonacoYaml'\n\nexport default MonacoYaml\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/components/monaco-yaml/monacoYamlModel.ts",
    "content": "import { configureMonacoYaml } from 'monaco-yaml'\nimport * as monaco from 'monaco-editor'\n\nexport const monacoYamlModel = configureMonacoYaml(monaco)\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/index.ts",
    "content": "import MonacoEditor from './MonacoEditor'\nimport MonacoJson from './components/monaco-json'\nimport MonacoYaml from './components/monaco-yaml'\nimport DedicatedEditor from './components/dedicated-editor'\nimport useMonacoValidation from './useMonacoValidation'\n\nexport {\n  MonacoEditor,\n  MonacoJson,\n  MonacoYaml,\n  DedicatedEditor,\n  useMonacoValidation,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/styles.module.scss",
    "content": ".wrapper {\n  position: relative;\n  height: 200px;\n  max-width: 100% !important;\n  border: 1px solid var(--controlsBorderColor) !important;\n\n  :global(.inlineMonacoEditor) {\n    height: 192px;\n    width: 100%;\n    font-size: 14px;\n    line-height: 24px;\n    letter-spacing: -0.14px;\n    margin-bottom: 2px;\n  }\n\n  &:global(.disabled) {\n    pointer-events: none;\n    opacity: 0.5;\n  }\n\n  :global {\n    .monaco-editor, .monaco-editor .margin, .monaco-editor .minimap-decorations-layer, .monaco-editor-background {\n      background-color: var(--euiColorEmptyShade) !important;\n    }\n\n    .monaco-editor .hover-row.status-bar {\n      display: none;\n    }\n  }\n\n  .isEditing {\n    padding-bottom: 40px;\n  }\n\n  .editBtn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    position: absolute;\n    bottom: 16px;\n    right: 16px;\n\n    width: 30px !important;\n    min-width: 0 !important;\n    height: 30px !important;\n    border-radius: 100%;\n\n    opacity: 0.7;\n\n    &:hover {\n      opacity: 1;\n    }\n\n    :global {\n      .euiButton__content {\n        padding: 0 !important;\n      }\n\n      .euiButton__text {\n        line-height: 12px !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/useMonacoValidation.spec.tsx",
    "content": "import { renderHook, act } from '@testing-library/react-hooks'\nimport useMonacoValidation from './useMonacoValidation'\n\nconst monaco = {\n  editor: {\n    getModelMarkers: jest.fn(),\n    onDidChangeMarkers: jest.fn(),\n  },\n} as any\n\nconst fakeModel = { uri: { toString: () => 'mock-uri' } }\nconst mockEditor = {\n  getModel: () => fakeModel,\n  onDidChangeModelContent: jest.fn(),\n  onDidChangeModelDecorations: jest.fn(),\n} as any\n\nlet markerListener: ((uris: any[]) => void) | undefined\n\nbeforeEach(() => {\n  markerListener = undefined\n\n  monaco.editor.getModelMarkers.mockReset()\n  monaco.editor.onDidChangeMarkers.mockReset()\n  mockEditor.onDidChangeModelContent.mockReset()\n\n  monaco.editor.onDidChangeMarkers.mockImplementation((cb: any) => {\n    markerListener = cb\n    return { dispose: jest.fn() }\n  })\n\n  mockEditor.onDidChangeModelContent.mockImplementation((cb: any) => {\n    mockEditor._contentChangeCallback = cb\n    return { dispose: jest.fn() }\n  })\n\n  mockEditor.onDidChangeModelDecorations.mockImplementation((cb: any) => {\n    mockEditor._decorationsChangeCallback = cb\n    return { dispose: jest.fn() }\n  })\n})\n\nafterEach(() => {\n  jest.clearAllMocks()\n})\n\ndescribe('useMonacoValidation', () => {\n  it('returns valid = true when there are no markers', () => {\n    monaco.editor.getModelMarkers.mockReturnValue([])\n\n    const { result } = renderHook(() =>\n      useMonacoValidation({ current: mockEditor }, monaco),\n    )\n\n    act(() => {\n      mockEditor._contentChangeCallback()\n    })\n    act(() => {\n      markerListener?.([fakeModel.uri])\n    })\n\n    expect(result.current.isValid).toBe(true)\n    expect(result.current.isValidating).toBe(false)\n  })\n\n  it('returns valid = false when there is an error marker', () => {\n    monaco.editor.getModelMarkers.mockReturnValue([{ severity: 8 }])\n\n    const { result } = renderHook(() =>\n      useMonacoValidation({ current: mockEditor }, monaco),\n    )\n\n    act(() => {\n      mockEditor._contentChangeCallback()\n    })\n    act(() => {\n      markerListener?.([fakeModel.uri])\n    })\n\n    expect(result.current.isValid).toBe(false)\n    expect(result.current.isValidating).toBe(false)\n  })\n\n  it('returns isValidating = false if decorations change without marker changes', () => {\n    let decorationsListener: (() => void) | undefined\n\n    mockEditor.onDidChangeModelDecorations.mockImplementation((cb: any) => {\n      decorationsListener = cb\n      return { dispose: jest.fn() }\n    })\n\n    monaco.editor.getModelMarkers.mockReturnValue([])\n\n    const { result } = renderHook(() =>\n      useMonacoValidation({ current: mockEditor }, monaco),\n    )\n\n    act(() => {\n      mockEditor._contentChangeCallback()\n    })\n\n    expect(result.current.isValidating).toBe(true)\n\n    // Simulate formatting triggering decorations change\n    act(() => {\n      decorationsListener?.()\n    })\n\n    expect(result.current.isValidating).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-editor/useMonacoValidation.ts",
    "content": "import * as monacoLib from 'monaco-editor'\nimport { useEffect, useState } from 'react'\n\nconst errorSeverity = 8\n\nconst useMonacoValidation = (\n  editorRef: React.MutableRefObject<monacoLib.editor.IStandaloneCodeEditor | null>,\n  monacoInstance?: typeof monacoLib,\n) => {\n  const monaco = monacoInstance || monacoLib\n\n  // setIsValid is updated only when markers change.\n  // However, Monaco does not emit marker updates for every edit.\n  // For example, if you delete part of the code but the result is still valid JSON,\n  // Monaco might skip updating the markers.\n  // Example:\n  // Before: [{ \"key\": \"value\" }, { \"foo\": \"bar\" }]\n  // After deleting: , { \"foo\": \"bar\" } -> Monaco may not emit marker changes since it's still valid.\n  // That's why we initialize isValid = true.\n  // On the other hand, if the edit makes the JSON invalid:\n  // e.g., deleting just { \"foo\": \"bar\" } (but leaving the comma),\n  // Monaco will detect the syntax error and update the markers.\n  const [isValid, setIsValid] = useState(true)\n  const [isValidating, setIsValidating] = useState(false)\n\n  useEffect(() => {\n    if (!editorRef.current) return\n\n    const editor = editorRef.current\n    const model = editor.getModel()\n    if (!model) return\n\n    // Mark as \"validating\" when content changes\n    const contentChangeDisposable = editor.onDidChangeModelContent(() => {\n      setIsValidating(true)\n    })\n\n    // Update validation when markers change\n    // Listening to markers change event is debounced\n    const markerChangeDisposable = monaco.editor.onDidChangeMarkers((uris) => {\n      if (!editorRef.current) return\n      if (!uris.some((u) => u.toString() === model.uri.toString())) return\n\n      const markers = monaco.editor.getModelMarkers({ resource: model.uri })\n      const hasErrors = markers.some((m) => m.severity === errorSeverity)\n\n      setIsValid(!hasErrors)\n      setIsValidating(false)\n    })\n\n    // Catch formatting or silent model changes that don't touch markers\n    const decorationsDisposable = editor.onDidChangeModelDecorations(() => {\n      setIsValidating(false)\n    })\n\n    // eslint-disable-next-line consistent-return\n    return () => {\n      contentChangeDisposable.dispose()\n      markerChangeDisposable.dispose()\n      decorationsDisposable.dispose()\n    }\n  }, [editorRef, monaco])\n\n  return { isValid, isValidating }\n}\n\nexport default useMonacoValidation\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MonacoLanguages from './MonacoLanguages'\n\ndescribe('MonacoLanguages', () => {\n  it('should render', () => {\n    expect(render(<MonacoLanguages />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx",
    "content": "import { useContext, useEffect } from 'react'\nimport { useSelector } from 'react-redux'\nimport { monaco } from 'react-monaco-editor'\nimport { findIndex } from 'lodash'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport {\n  MonacoLanguage,\n  redisLanguageConfig,\n  Theme,\n  IRedisCommandTree,\n} from 'uiSrc/constants'\nimport { getRedisMonarchTokensProvider } from 'uiSrc/utils'\nimport { darkTheme, lightTheme, MonacoThemes } from 'uiSrc/constants/monaco'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\n\nimport { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensSubRedis'\nimport SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json'\nimport { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands'\nimport { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants'\n\nconst MonacoLanguages = () => {\n  const { theme } = useContext(ThemeContext)\n  const { commandsArray: REDIS_COMMANDS_ARRAY, spec: COMMANDS_SPEC } =\n    useSelector(appRedisCommandsSelector)\n\n  useEffect(() => {\n    if (monaco?.editor) {\n      monaco.editor.defineTheme(MonacoThemes.Dark, darkTheme)\n      monaco.editor.defineTheme(MonacoThemes.Light, lightTheme)\n    }\n  }, [])\n\n  useEffect(() => {\n    monaco.editor.setTheme(\n      theme === Theme.Dark ? MonacoThemes.Dark : MonacoThemes.Light,\n    )\n  }, [theme])\n\n  useEffect(() => {\n    if (!REDIS_COMMANDS_ARRAY.length) return\n\n    setupMonacoRedisLang()\n  }, [REDIS_COMMANDS_ARRAY])\n\n  const setupMonacoRedisLang = () => {\n    const languages = monaco.languages.getLanguages()\n    const isRedisLangRegistered =\n      findIndex(languages, { id: MonacoLanguage.Redis }) > -1\n    if (!isRedisLangRegistered) {\n      monaco.languages.register({ id: MonacoLanguage.Redis })\n      monaco.languages.register({ id: MonacoLanguage.RediSearch })\n    }\n\n    monaco.languages.setLanguageConfiguration(\n      MonacoLanguage.Redis,\n      redisLanguageConfig,\n    )\n    monaco.languages.setLanguageConfiguration(\n      MonacoLanguage.RediSearch,\n      redisLanguageConfig,\n    )\n    const REDIS_COMMANDS = mergeRedisCommandsSpecs(\n      COMMANDS_SPEC,\n      SEARCH_COMMANDS_SPEC,\n    ) as IRedisCommandTree[]\n    const REDIS_SEARCH_COMMANDS = REDIS_COMMANDS.filter(({ name }) =>\n      name?.startsWith(ModuleCommandPrefix.RediSearch),\n    )\n\n    try {\n      monaco.languages.setMonarchTokensProvider(\n        MonacoLanguage.RediSearch,\n        getRediSearchSubRedisMonarchTokensProvider(REDIS_SEARCH_COMMANDS),\n      )\n    } catch (exception) {\n      console.error('Monaco RediSearch language setup error: ', exception)\n    }\n\n    try {\n      monaco.languages.setMonarchTokensProvider(\n        MonacoLanguage.Redis,\n        getRedisMonarchTokensProvider(REDIS_COMMANDS),\n      )\n    } catch (exception) {\n      console.error('Monaco Redis language setup error: ', exception)\n    }\n  }\n\n  return null\n}\n\nexport default MonacoLanguages\n"
  },
  {
    "path": "redisinsight/ui/src/components/monaco-laguages/index.ts",
    "content": "import MonacoLanguages from './MonacoLanguages'\n\nexport default MonacoLanguages\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/Monitor/Monitor.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport Monitor, { Props } from './Monitor'\n\nconst mockedProps = mock<Props>()\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('Monitor', () => {\n  it('should render', () => {\n    expect(render(<Monitor {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('Monitor should be in the Document', () => {\n    render(<Monitor {...instance(mockedProps)} />)\n\n    const monitor = screen.queryByTestId('monitor')\n\n    expect(monitor).toBeInTheDocument()\n  })\n\n  it('Default text component should be in the Document by default', () => {\n    render(<Monitor {...instance(mockedProps)} items={[]} />)\n\n    const monitorDefault = screen.queryByTestId('monitor-not-started')\n\n    expect(monitorDefault).toBeInTheDocument()\n  })\n\n  it('Default text component should not be in the Document when some items exists', () => {\n    const items = [\n      {\n        time: '1',\n        args: ['test'],\n        source: '1',\n        database: 0,\n        shardOptions: { host: '127.0.0.1', port: 6379 },\n      },\n    ]\n    const { queryByTestId, unmount } = render(\n      <Monitor {...instance(mockedProps)} isStarted isRunning items={items} />,\n    )\n\n    const monitorDefault = queryByTestId('monitor-not-started')\n\n    expect(monitorDefault).not.toBeInTheDocument()\n    unmount()\n  })\n\n  it('Monitor should start after click on the start button', () => {\n    const handleRunMonitorMock = jest.fn()\n    render(\n      <Monitor\n        {...instance(mockedProps)}\n        handleRunMonitor={handleRunMonitorMock}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('start-monitor') ?? {})\n\n    expect(handleRunMonitorMock).toBeCalled()\n  })\n\n  it('should show warning banner', () => {\n    render(<Monitor {...instance(mockedProps)} />)\n    expect(screen.getByTestId('monitor-warning-message')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/Monitor/Monitor.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledImagePanel = styled(Col)`\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  padding: 48px;\n  max-width: 420px;\n  border-radius: 8px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/Monitor/Monitor.tsx",
    "content": "import React, { useState } from 'react'\nimport cx from 'classnames'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport { IMonitorDataPayload } from 'uiSrc/slices/interfaces'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { ColorText, Text, Title } from 'uiSrc/components/base/text'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport MonitorLog from '../MonitorLog'\nimport MonitorOutputList from '../MonitorOutputList'\n\nimport ProfilerImage from 'uiSrc/assets/img/profiler/magnifier.svg'\n\nimport styles from './styles.module.scss'\nimport { StyledImagePanel } from './Monitor.styles'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Banner } from 'uiSrc/components/base/display/banner'\nimport { RiImage } from 'uiSrc/components/base/display'\n\nexport interface Props {\n  items: IMonitorDataPayload[]\n  error: string\n  isStarted: boolean\n  isRunning: boolean\n  isPaused: boolean\n  isShowHelper: boolean\n  isSaveToFile: boolean\n  isShowCli: boolean\n  handleRunMonitor: (isSaveToLog?: boolean) => void\n}\n\nconst Monitor = (props: Props) => {\n  const {\n    items = [],\n    error = '',\n    isRunning = false,\n    isStarted = false,\n    isPaused = false,\n    isShowHelper = false,\n    isShowCli = false,\n    isSaveToFile = false,\n    handleRunMonitor = () => {},\n  } = props\n  const [saveLogValue, setSaveLogValue] = useState(isSaveToFile)\n\n  const MonitorNotStarted = () => (\n    <Row\n      align=\"center\"\n      style={{ margin: 48 }}\n      gap=\"xxl\"\n      data-testid=\"monitor-not-started\"\n    >\n      <StyledImagePanel align=\"center\">\n        <RiImage\n          src={ProfilerImage}\n          alt=\"Profiler\"\n          style={{ userSelect: 'none', pointerEvents: 'none' }}\n        />\n        <Spacer size=\"l\" />\n        <Text>\n          Get a deeper understanding of your database with real-time command,\n          key, and client statistics.\n        </Text>\n      </StyledImagePanel>\n\n      <Col gap=\"xl\">\n        <Title size=\"M\">Profiler</Title>\n        <Text>\n          Analyze every command sent to Redis in real time to debug issues and\n          optimize performance.\n        </Text>\n\n        <div>\n          <RiTooltip content=\"Enable real-time profiling of your Redis database.\">\n            <PrimaryButton\n              onClick={() => handleRunMonitor(saveLogValue)}\n              aria-label=\"start monitor\"\n              data-testid=\"start-monitor\"\n            >\n              Start Profiler\n            </PrimaryButton>\n          </RiTooltip>\n        </div>\n\n        <div data-testid=\"save-log-container\">\n          <RiTooltip\n            title=\"Allows you to download the generated log file after pausing the Profiler.\"\n            content=\"Profiler log is saved to a file on your local machine with no size limitation. The temporary log file will be automatically rewritten when the Profiler is reset.\"\n            data-testid=\"save-log-tooltip\"\n          >\n            <SwitchInput\n              title=\"Save Log\"\n              checked={saveLogValue}\n              onCheckedChange={setSaveLogValue}\n              data-testid=\"save-log-switch\"\n            />\n          </RiTooltip>\n        </div>\n\n        <Banner\n          variant=\"attention\"\n          showIcon\n          data-testid=\"monitor-warning-message\"\n          message=\"Running Profiler will decrease throughput, avoid running it in production databases.\"\n        />\n      </Col>\n    </Row>\n  )\n\n  const MonitorError = () => (\n    <div className={styles.startContainer} data-testid=\"monitor-error\">\n      <div className={cx(styles.startContent, styles.startContentError)}>\n        <Row>\n          <FlexItem>\n            <RiIcon\n              type=\"BannedIcon\"\n              size=\"m\"\n              color=\"danger\"\n              aria-label=\"no permissions icon\"\n            />\n          </FlexItem>\n          <FlexItem grow>\n            <ColorText\n              color=\"danger\"\n              style={{ paddingLeft: 4 }}\n              data-testid=\"monitor-error-message\"\n            >\n              {error}\n            </ColorText>\n          </FlexItem>\n        </Row>\n      </div>\n    </div>\n  )\n\n  return (\n    <>\n      <div\n        className={cx(styles.container, {\n          [styles.isRunning]: isRunning && !isPaused,\n        })}\n        data-testid=\"monitor\"\n      >\n        {error && !isRunning ? (\n          <MonitorError />\n        ) : (\n          <>\n            {!isStarted && <MonitorNotStarted />}\n            {!items?.length && isRunning && !isPaused && (\n              <div\n                data-testid=\"monitor-started\"\n                style={{ paddingTop: 10, paddingLeft: 12 }}\n              >\n                Profiler is started.\n              </div>\n            )}\n          </>\n        )}\n        {isStarted && (\n          <div className={styles.content}>\n            {!!items?.length && (\n              <AutoSizer>\n                {({ width, height }) => (\n                  <MonitorOutputList\n                    width={width || 0}\n                    height={height || 0}\n                    items={items}\n                    compressed={isShowCli || isShowHelper}\n                  />\n                )}\n              </AutoSizer>\n            )}\n          </div>\n        )}\n        {isStarted && isPaused && <MonitorLog />}\n      </div>\n    </>\n  )\n}\n\nexport default React.memo(Monitor)\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/Monitor/index.ts",
    "content": "import Monitor from './Monitor'\n\nexport default Monitor\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/Monitor/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n  height: calc(100% - 34px);\n  position: relative;\n  width: 100%;\n\n  display: flex;\n  flex-direction: column;\n\n  background-color: var(--browserTableRowEven);\n  text-align: left;\n  letter-spacing: 0;\n  white-space: pre-line;\n  color: var(--euiTextSubduedColor) !important;\n  border-top: 1px solid var(--euiColorLightShade);\n\n  font: normal normal normal 14px/17px Inconsolata;\n\n  z-index: 10;\n  overflow: auto;\n}\n\n.listWrapper {\n  @include eui.scrollBar;\n  flex: 1;\n\n  width: 100%;\n  height: 100%;\n  position: relative;\n  overflow: auto;\n}\n\n.content {\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  padding: 12px 4px 12px 12px;\n\n  //@include eui.scrollBar;\n  //\n  //width: 100%;\n  //height: 100%;\n  //position: relative;\n  //overflow: auto;\n  //\n  //display: flex;\n  //flex-direction: column;\n  //margin-bottom: 6px;\n  //\n  //&:first-child {\n  //  padding-top: 10px;\n  //}\n  //\n  //&:last-child {\n  //  padding-bottom: 10px;\n  //}\n}\n\n.startContainer {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n  justify-content: center;\n\n  height: 100%;\n  padding-left: 12px;\n}\n\n.startContent {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-grow: 1;\n  max-width: 264px;\n  flex-direction: column;\n\n  font:\n    normal normal normal 12px/18px Graphik,\n    sans-serif;\n}\n\n.monitorStoppedText {\n  padding-left: 12px;\n  padding-bottom: 4px;\n}\n\n\n.startContentError {\n  max-width: 298px;\n  padding-right: 12px;\n}\n\n.autoScrollContainer {\n  :global {\n    .react-auto-scroll__scroll-container {\n      @include eui.scrollBar;\n    }\n\n    .react-auto-scroll__option {\n      // display: none;\n    }\n  }\n}\n\n.table {\n  table {\n    background-color: initial !important;\n  }\n\n  thead {\n    display: none;\n  }\n\n  :global {\n    .euiTableCellContent {\n      padding: 0;\n    }\n    .euiTableRow {\n      td:first-child,\n      td:last-child {\n        border: none;\n      }\n\n      &:hover {\n        background-color: inherit !important;\n      }\n    }\n\n    .euiTableRowCell {\n      border: none;\n    }\n  }\n}\n\n.startTitle {\n  font-size: 13px;\n  letter-spacing: -0.13px;\n  padding: 4px 0 18px;\n}\n\n\n.itemCommand {\n  padding-right: 6px;\n}\n\n.itemCommandFirst {\n  color: #569cd6;\n}\n\n.itemArgs {\n  word-break: break-word;\n  padding-right: 10px;\n  color: var(--euiTextSubduedColorHover);\n}\n\n.itemArgs__compressed {\n  max-width: 330px;\n  width: max-content;\n  color: var(--euiTextSubduedColorHover);\n}\n\n.item {\n  display: block;\n\n  & > span {\n    padding-right: 5px;\n    word-break: break-word;\n  }\n}\n\n.itemTime {\n  width: 94px;\n  padding-right: 10px;\n}\n\n.monitorWrapper {\n  height: 100%;\n  white-space: pre-line;\n  border-top: 1px solid var(--euiColorLightShade);\n  border-left: 1px solid var(--euiColorLightShade);\n  border-right: 1px solid var(--euiColorLightShade);\n}\n\n.scrollDivRef {\n  padding-top: 10px;\n  margin-left: -12px;\n}\n\n.time {\n  color: var(--defaultGreenColor);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorHeader/MonitorHeader.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  monitorSelector,\n  resetMonitorItems,\n  setMonitorInitialState,\n  toggleHideMonitor,\n  toggleMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport MonitorHeader, { Props } from './MonitorHeader'\n\nconst mockedProps = mock<Props>()\nconst monitorPath = 'uiSrc/slices/cli/monitor'\nlet store: typeof mockedStore\n\njest.mock(monitorPath, () => {\n  const defaultState = jest.requireActual(monitorPath).initialState\n  return {\n    ...jest.requireActual(monitorPath),\n    monitorSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      isMinimizedMonitor: false,\n      isShowMonitor: true,\n    }),\n  }\n})\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('MonitorHeader', () => {\n  it('should render', () => {\n    expect(render(<MonitorHeader {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should \"setMonitorInitialState\" action be called after click \"close-monitor\" button', () => {\n    render(<MonitorHeader {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('close-monitor'))\n\n    const expectedActions = [setMonitorInitialState()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should \"toggleCliHelper\" action be called after click \"hide-monitor\" button', () => {\n    render(<MonitorHeader {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('hide-monitor'))\n\n    const expectedActions = [toggleMonitor(), toggleHideMonitor()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('Should call handleRunMonitor after click on the play button', () => {\n    const handleRunMonitor = jest.fn()\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isStarted: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n    render(<MonitorHeader handleRunMonitor={handleRunMonitor} />)\n\n    fireEvent.click(screen.getByTestId('toggle-run-monitor'))\n\n    expect(handleRunMonitor).toHaveBeenCalled()\n  })\n\n  it('Should clear Monitor items after click on the clear button', () => {\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isStarted: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n    render(<MonitorHeader {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('clear-monitor'))\n\n    const expectedActions = [resetMonitorItems()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorHeader/MonitorHeader.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  monitorSelector,\n  resetMonitorItems,\n  setMonitorInitialState,\n  toggleHideMonitor,\n  toggleMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OnboardingTour, RiTooltip } from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  PlayIcon,\n  PauseIcon,\n  DeleteIcon,\n  BannedIcon,\n} from 'uiSrc/components/base/icons'\nimport { WindowControlGroup } from 'uiSrc/components/base/shared/WindowControlGroup'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  handleRunMonitor: () => void\n}\n\nconst MonitorHeader = ({ handleRunMonitor }: Props) => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const {\n    isRunning,\n    isPaused,\n    isResumeLocked,\n    isStarted,\n    items = [],\n    error,\n    loadingPause,\n  } = useSelector(monitorSelector)\n  const isErrorShown = !!error && !isRunning\n  const disabledPause = isErrorShown || isResumeLocked || loadingPause\n  const dispatch = useDispatch()\n\n  const handleCloseMonitor = () => {\n    if (isRunning) {\n      sendEventTelemetry({\n        event: TelemetryEvent.PROFILER_STOPPED,\n        eventData: { databaseId: instanceId },\n      })\n    }\n    sendEventTelemetry({\n      event: TelemetryEvent.PROFILER_CLOSED,\n      eventData: { databaseId: instanceId },\n    })\n    dispatch(setMonitorInitialState())\n  }\n\n  const handleHideMonitor = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.PROFILER_MINIMIZED,\n      eventData: { databaseId: instanceId },\n    })\n\n    dispatch(toggleMonitor())\n    dispatch(toggleHideMonitor())\n  }\n\n  const handleClearMonitor = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.PROFILER_CLEARED,\n      eventData: { databaseId: instanceId },\n    })\n    dispatch(resetMonitorItems())\n  }\n\n  return (\n    <div className={styles.container} data-testid=\"monitor-header\">\n      <Row justify=\"between\" align=\"center\" style={{ height: '100%' }}>\n        <FlexItem className={styles.title}>\n          <RiIcon type=\"ProfilerIcon\" size=\"m\" />\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.BROWSER_PROFILER}\n            anchorPosition=\"upLeft\"\n            panelClassName={styles.profilerOnboardPanel}\n          >\n            <Text>Profiler</Text>\n          </OnboardingTour>\n        </FlexItem>\n        {isStarted && (\n          <FlexItem direction=\"row\" className={styles.actions}>\n            <RiTooltip\n              content={\n                isErrorShown || isResumeLocked\n                  ? ''\n                  : !isPaused\n                    ? 'Pause'\n                    : 'Resume'\n              }\n              anchorClassName=\"inline-flex\"\n            >\n              <IconButton\n                icon={\n                  isErrorShown || isResumeLocked\n                    ? BannedIcon\n                    : !isPaused\n                      ? PauseIcon\n                      : PlayIcon\n                }\n                onClick={() => handleRunMonitor()}\n                aria-label=\"start/stop monitor\"\n                data-testid=\"toggle-run-monitor\"\n                disabled={disabledPause}\n              />\n            </RiTooltip>\n            <RiTooltip\n              content={\n                !isStarted || !items.length ? '' : 'Clear Profiler Window'\n              }\n              anchorClassName={cx('inline-flex', {\n                transparent: !isStarted || !items.length,\n              })}\n            >\n              <IconButton\n                icon={DeleteIcon}\n                onClick={handleClearMonitor}\n                aria-label=\"clear profiler\"\n                data-testid=\"clear-monitor\"\n              />\n            </RiTooltip>\n          </FlexItem>\n        )}\n        <FlexItem grow />\n        <WindowControlGroup\n          onClose={handleCloseMonitor}\n          onHide={handleHideMonitor}\n          id=\"monitor\"\n        />\n      </Row>\n    </div>\n  )\n}\n\nexport default MonitorHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorHeader/index.ts",
    "content": "import MonitorHeader from './MonitorHeader'\n\nexport default MonitorHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorHeader/styles.module.scss",
    "content": ".container {\n  height: 34px;\n  line-height: 34px;\n  width: 100%;\n  overflow: hidden;\n  background-color: var(--euiPageBackgroundColor);\n\n  padding: 0 10px 0 16px;\n  z-index: 10;\n\n  .actions {\n    width: 82px;\n    height: 100%;\n    flex-direction: row;\n    align-items: center;\n    justify-content: space-evenly;\n    margin-left: 12px;\n\n    border-left: 1px solid var(--euiColorLightShade);\n    border-right: 1px solid var(--euiColorLightShade);\n  }\n\n  :global(.transparent button) {\n    cursor: default;\n  }\n}\n\n.icon {\n  margin-left: 5px;\n}\n\n.title {\n  display: flex;\n  flex-direction: row !important;\n  align-items: center;\n  :global {\n    .euiIcon {\n      color: var(--euiColorPrimary);\n      margin-right: 8px;\n    }\n  }\n}\n\n.profilerOnboardPanel {\n  margin-top: -4px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport {\n  monitorSelector,\n  resetProfiler,\n  stopMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { sendCliCommand } from 'uiSrc/slices/cli/cli-output'\nimport MonitorLog from './MonitorLog'\n\nlet store: typeof mockedStore\nconst mockURLrevokeObjectURL = 123123\n\njest.mock('file-saver', () => ({\n  ...jest.requireActual('file-saver'),\n  saveAs: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/cli/monitor', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/monitor'),\n  monitorSelector: jest.fn().mockReturnValue({\n    isSaveToFile: false,\n    logFileId: 'logFileId',\n    timestamp: {\n      start: 1,\n      paused: 2,\n      unPaused: 3,\n      duration: 123,\n    },\n  }),\n}))\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('MonitorLog', () => {\n  beforeAll(() => {\n    jest\n      .spyOn(URL, 'revokeObjectURL')\n      .mockImplementation(() => mockURLrevokeObjectURL)\n  })\n\n  it('should render', () => {\n    expect(render(<MonitorLog />)).toBeTruthy()\n  })\n\n  it('should call proper actions on click reset profiler', () => {\n    render(<MonitorLog />)\n    fireEvent.click(screen.getByTestId('reset-profiler-btn'))\n\n    const expectedActions = [stopMonitor(), resetProfiler()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call fetchMonitorLog after click on Download', async () => {\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isSaveToFile: true,\n      logFileId: 'logFileId',\n      timestamp: {\n        start: 1,\n        paused: 2,\n        unPaused: 3,\n        duration: 123,\n      },\n    })\n\n    ;(monitorSelector as jest.Mock).mockImplementation(monitorSelectorMock)\n\n    render(<MonitorLog />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('download-log-btn'))\n    })\n\n    const expectedActions = [sendCliCommand()]\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.tsx",
    "content": "import { format, formatDuration, intervalToDuration } from 'date-fns'\nimport React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport {\n  monitorSelector,\n  resetProfiler,\n  stopMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport { cutDurationText } from 'uiSrc/utils'\nimport { downloadFile } from 'uiSrc/utils/dom/downloadFile'\nimport { fetchMonitorLog } from 'uiSrc/slices/cli/cli-output'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RefreshIcon, DownloadIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nconst PADDINGS_OUTSIDE = 12\nconst MIDDLE_SCREEN_RESOLUTION = 460 - PADDINGS_OUTSIDE\nconst SMALL_SCREEN_RESOLUTION = 360 - PADDINGS_OUTSIDE\nconst DOWNLOAD_IFRAME_NAME = 'logFileDownloadIFrame'\n\nconst MonitorLog = () => {\n  const { timestamp, logFileId, isSaveToFile } = useSelector(monitorSelector)\n  const dispatch = useDispatch()\n\n  const duration = cutDurationText(\n    formatDuration(\n      intervalToDuration({\n        end: timestamp.duration,\n        start: 0,\n      }),\n    ),\n  )\n\n  const downloadBtnProps: any = {\n    target: DOWNLOAD_IFRAME_NAME,\n  }\n\n  const onResetProfiler = () => {\n    dispatch(stopMonitor())\n    dispatch(resetProfiler())\n  }\n\n  const getPaddingByWidth = (width: number): number => {\n    if (width < SMALL_SCREEN_RESOLUTION) return 6\n    if (width < MIDDLE_SCREEN_RESOLUTION) return 12\n    return 18\n  }\n\n  const handleDownloadLog = async (e: React.MouseEvent<HTMLAnchorElement>) => {\n    e.preventDefault()\n\n    dispatch(fetchMonitorLog(logFileId || '', downloadFile))\n  }\n\n  return (\n    <div className={styles.monitorLogWrapper}>\n      <iframe\n        title=\"downloadIframeTarget\"\n        name={DOWNLOAD_IFRAME_NAME}\n        style={{ display: 'none' }}\n      />\n      <AutoSizer disableHeight>\n        {({ width = 0 }) => (\n          <div\n            className={styles.container}\n            style={{\n              width,\n              paddingLeft: getPaddingByWidth(width),\n              paddingRight: getPaddingByWidth(width),\n            }}\n            data-testid=\"download-log-panel\"\n          >\n            <Text\n              size=\"xs\"\n              color=\"subdued\"\n              className={styles.time}\n              data-testid=\"profiler-running-time\"\n            >\n              <RiIcon size=\"s\" color=\"informative400\" type=\"TimeIconIcon\" />\n              {format(timestamp.start, 'hh:mm:ss')}\n              &nbsp;&#8211;&nbsp;\n              {format(timestamp.paused, 'hh:mm:ss')}\n              &nbsp;(\n              {duration}\n              {width > SMALL_SCREEN_RESOLUTION && ' Running time'})\n            </Text>\n            <Row className={styles.actions} justify=\"between\" align=\"center\">\n              <FlexItem>\n                {isSaveToFile && (\n                  <SecondaryButton\n                    size=\"small\"\n                    icon={DownloadIcon}\n                    className={styles.btn}\n                    data-testid=\"download-log-btn\"\n                    onClick={handleDownloadLog}\n                    {...downloadBtnProps}\n                  >\n                    {width > SMALL_SCREEN_RESOLUTION && ' Download '}\n                    Log\n                  </SecondaryButton>\n                )}\n              </FlexItem>\n              <FlexItem>\n                <PrimaryButton\n                  size=\"small\"\n                  onClick={onResetProfiler}\n                  icon={RefreshIcon}\n                  className={styles.btn}\n                  data-testid=\"reset-profiler-btn\"\n                >\n                  Reset\n                  {width > SMALL_SCREEN_RESOLUTION && ' Profiler'}\n                </PrimaryButton>\n              </FlexItem>\n            </Row>\n          </div>\n        )}\n      </AutoSizer>\n    </div>\n  )\n}\n\nexport default MonitorLog\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorLog/index.ts",
    "content": "import MonitorLog from './MonitorLog'\n\nexport default MonitorLog\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorLog/styles.module.scss",
    "content": ".monitorLogWrapper {\n  display: flex;\n  min-height: 72px;\n\n  margin: 10px 6px 6px 6px;\n  padding: 6px 0;\n\n  background: var(--euiColorLightestShade);\n  box-shadow: 0 3px 15px var(--controlsBoxShadowColor);\n  border-radius: 4px;\n\n  font-family: 'Graphik', sans-serif;\n  font-size: 14px;\n  letter-spacing: -0.14px;\n  overflow: hidden;\n\n  .time {\n    display: flex;\n    align-items: center;\n    :global(svg) {\n      margin-right: 6px;\n    }\n  }\n\n  .actions {\n    margin-top: 6px;\n  }\n\n  .btn {\n    height: 36px !important;\n    line-height: 36px !important;\n  }\n\n  .container {\n    display: flex;\n    flex: 1;\n    flex-direction: column;\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorOutputList/MonitorOutputList.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MonitorOutputList, { Props } from './MonitorOutputList'\n\nconst mockedProps = {\n  ...mock<Props>(),\n  height: 20,\n  width: 20,\n}\n\ndescribe('MonitorOutputList', () => {\n  it('should render', () => {\n    expect(render(<MonitorOutputList {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render items properly', () => {\n    const item = { time: '112', args: ['ttl'], source: '12', database: '0' }\n    const mockItems = [item, item]\n    const { container } = render(\n      <MonitorOutputList {...mockedProps} items={mockItems} />,\n    )\n    expect(container.getElementsByClassName('item').length).toBe(2)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorOutputList/MonitorOutputList.tsx",
    "content": "import React, { useCallback, useEffect, useRef } from 'react'\nimport cx from 'classnames'\nimport {\n  ListChildComponentProps,\n  ListOnScrollProps,\n  VariableSizeList as List,\n} from 'react-window'\n\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { DEFAULT_ERROR_MESSAGE, getFormatTime } from 'uiSrc/utils'\n\nimport styles from 'uiSrc/components/monitor/Monitor/styles.module.scss'\n\nexport interface Props {\n  compressed: boolean\n  items: any[]\n  width: number\n  height: number\n}\n\nconst PROTRUDING_OFFSET = 2\nconst MIDDLE_SCREEN_RESOLUTION = 460\nconst SMALL_SCREEN_RESOLUTION = 360\nconst MIN_ROW_HEIGHT = 17\n\nconst MonitorOutputList = (props: Props) => {\n  const { compressed, items = [], width = 0, height = 0 } = props\n\n  const autoScrollRef = useRef<boolean>(true)\n  const rowHeights = useRef<{ [key: number]: number }>({})\n  const outerRef = useRef<HTMLDivElement>(null)\n  const listRef = useRef<List>(null)\n  const hasMountedRef = useRef<boolean>(false)\n\n  useEffect(() => {\n    if (autoScrollRef.current) {\n      setTimeout(() => {\n        scrollToBottom()\n      }, 0)\n    }\n  }, [items])\n\n  const getRowHeight = (index: number) =>\n    rowHeights.current[index] > MIN_ROW_HEIGHT\n      ? rowHeights.current[index] + 2\n      : MIN_ROW_HEIGHT\n\n  const setRowHeight = (index: number, size: number) => {\n    listRef.current?.resetAfterIndex(0)\n    if (size > MIN_ROW_HEIGHT) {\n      rowHeights.current[index] = size\n      return\n    }\n\n    if (rowHeights.current[index]) {\n      delete rowHeights.current[index]\n    }\n  }\n\n  const scrollToBottom = () => {\n    listRef.current?.scrollToItem(items.length - 1, 'end')\n    requestAnimationFrame(() => {\n      listRef.current?.scrollToItem(items.length - 1, 'end')\n    })\n  }\n\n  const handleScroll = useCallback((e: ListOnScrollProps) => {\n    if (!hasMountedRef.current) {\n      hasMountedRef.current = true\n      return\n    }\n\n    if (!outerRef.current) {\n      return\n    }\n\n    if (\n      e.scrollOffset + outerRef.current.offsetHeight ===\n      outerRef.current.scrollHeight\n    ) {\n      autoScrollRef.current = true\n      return\n    }\n\n    if (!e.scrollUpdateWasRequested) {\n      autoScrollRef.current = false\n    }\n  }, [])\n\n  const getArgs = (args: string[]): JSX.Element => (\n    <span\n      className={cx(styles.itemArgs, {\n        [styles.itemArgs__compressed]: compressed,\n      })}\n    >\n      {args?.map((arg, i) => (\n        <span key={`${arg + i}`}>\n          {i === 0 && (\n            <span className={cx(styles.itemCommandFirst)}>{`\"${arg}\"`}</span>\n          )}\n          {i !== 0 && ` \"${arg}\"`}\n        </span>\n      ))}\n    </span>\n  )\n\n  const Row = ({ index, style }: ListChildComponentProps) => {\n    const {\n      time = '',\n      args = [],\n      database = '',\n      source = '',\n      isError,\n      message = '',\n    } = items[index]\n    const rowRef = useRef<HTMLDivElement>(null)\n\n    useEffect(() => {\n      if (!rowRef.current) return\n      setRowHeight(index, rowRef.current?.clientHeight)\n    }, [rowRef])\n\n    return (\n      <div style={style} className={styles.item} data-testid={`row-${index}`}>\n        {!isError && (\n          <div ref={rowRef}>\n            {width > MIDDLE_SCREEN_RESOLUTION && (\n              <span className={cx(styles.time)}>\n                {getFormatTime(time)}&nbsp;\n              </span>\n            )}\n            {width > SMALL_SCREEN_RESOLUTION && (\n              <span>{`[${database} ${source}] `}</span>\n            )}\n            <span>{getArgs(args)}</span>\n          </div>\n        )}\n        {isError && (\n          <ColorText color=\"danger\">\n            {message ?? DEFAULT_ERROR_MESSAGE}\n          </ColorText>\n        )}\n      </div>\n    )\n  }\n\n  return (\n    <List\n      height={height}\n      itemCount={items.length}\n      itemSize={getRowHeight}\n      ref={listRef}\n      width={width - PROTRUDING_OFFSET}\n      className={styles.listWrapper}\n      outerRef={outerRef}\n      onScroll={handleScroll}\n      overscanCount={30}\n    >\n      {Row}\n    </List>\n  )\n}\n\nexport default MonitorOutputList\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorOutputList/index.ts",
    "content": "import MonitorOutputList from './MonitorOutputList'\n\nexport default MonitorOutputList\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorWrapper.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport MonitorWrapper from './MonitorWrapper'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('MonitorWrapper', () => {\n  it('should render', () => {\n    expect(render(<MonitorWrapper />)).toBeTruthy()\n  })\n\n  it('MonitorWrapper should be in the Document', () => {\n    render(<MonitorWrapper />)\n\n    const monitorWrapper = screen.queryByTestId('monitor-container')\n\n    expect(monitorWrapper).toBeInTheDocument()\n  })\n\n  it('MonitorWrapper should be in the Document', () => {\n    render(<MonitorWrapper />)\n\n    const monitor = screen.queryByTestId('monitor')\n\n    expect(monitor).toBeInTheDocument()\n  })\n\n  it('MonitorHeader should be in the Document', () => {\n    render(<MonitorWrapper />)\n\n    const monitorHeader = screen.queryByTestId('monitor-header')\n\n    expect(monitorHeader).toBeInTheDocument()\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<MonitorWrapper />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('monitor')).toBeInTheDocument()\n    expect(screen.queryByTestId('monitor-header')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('monitor-not-supported'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<MonitorWrapper />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('monitor')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('monitor-header')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('monitor-not-supported')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/MonitorWrapper.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  monitorSelector,\n  startMonitor,\n  togglePauseMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport Monitor from './Monitor'\nimport MonitorHeader from './MonitorHeader'\n\nimport styles from './Monitor/styles.module.scss'\n\nconst MonitorWrapper = () => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { items, isStarted, isRunning, isPaused, isSaveToFile, error } =\n    useSelector(monitorSelector)\n  const { isShowCli, isShowHelper } = useSelector(cliSettingsSelector)\n\n  const dispatch = useDispatch()\n\n  const handleRunMonitor = () => {\n    sendEventTelemetry({\n      event: isPaused\n        ? TelemetryEvent.PROFILER_RESUMED\n        : TelemetryEvent.PROFILER_PAUSED,\n      eventData: { databaseId: instanceId },\n    })\n    dispatch(togglePauseMonitor())\n  }\n\n  const onRunMonitor = (isSaveToLog: boolean = false) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.PROFILER_STARTED,\n      eventData: {\n        databaseId: instanceId,\n        logSaving: isSaveToLog,\n      },\n    })\n    dispatch(startMonitor(isSaveToLog))\n  }\n\n  return (\n    <section className={styles.monitorWrapper} data-testid=\"monitor-container\">\n      <FeatureFlagComponent\n        name={FeatureFlags.envDependent}\n        otherwise={\n          <div\n            data-testid=\"monitor-not-supported\"\n            style={{ display: 'grid', placeContent: 'center', height: '100%' }}\n          >\n            <div className=\"cli-output-response-fail\">\n              Monitor not supported in this environment.\n            </div>\n          </div>\n        }\n      >\n        <MonitorHeader handleRunMonitor={handleRunMonitor} />\n        <Monitor\n          items={items}\n          error={error}\n          isStarted={isStarted}\n          isPaused={isPaused}\n          isRunning={isRunning}\n          isShowCli={isShowCli}\n          isSaveToFile={isSaveToFile}\n          isShowHelper={isShowHelper}\n          handleRunMonitor={onRunMonitor}\n        />\n      </FeatureFlagComponent>\n    </section>\n  )\n}\n\nexport default React.memo(MonitorWrapper)\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor/index.ts",
    "content": "import MonitorWrapper from './MonitorWrapper'\n\nexport default MonitorWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor-config/MonitorConfig.spec.tsx",
    "content": "/* eslint-disable sonarjs/no-duplicate-string */\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\nimport MockedSocket from 'socket.io-mock'\nimport socketIO from 'socket.io-client'\nimport {\n  monitorSelector,\n  setMonitorLoadingPause,\n  pauseMonitor,\n  setSocket,\n  stopMonitor,\n  lockResume,\n  setLogFileId,\n  setStartTimestamp,\n} from 'uiSrc/slices/cli/monitor'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { MonitorEvent, SocketEvent } from 'uiSrc/constants'\nimport * as ioHooks from 'uiSrc/services/hooks/useIoConnection'\nimport { getSocketApiUrl } from 'uiSrc/utils'\nimport MonitorConfig from './MonitorConfig'\n\nlet store: typeof mockedStore\nlet socket: typeof MockedSocket\nlet useIoConnectionSpy: jest.SpyInstance\n\nbeforeEach(() => {\n  cleanup()\n  socket = new MockedSocket()\n  socketIO.mockReturnValue(socket)\n  useIoConnectionSpy = jest.spyOn(ioHooks, 'useIoConnection')\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('socket.io-client')\n\njest.mock('uiSrc/slices/cli/monitor', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/monitor'),\n  monitorSelector: jest.fn().mockReturnValue({\n    isRunning: false,\n    isMinimizedMonitor: false,\n    isShowMonitor: true,\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '1',\n  }),\n}))\n\ndescribe('MonitorConfig', () => {\n  it('should render', () => {\n    expect(render(<MonitorConfig />)).toBeTruthy()\n  })\n\n  it('socket should be set to store', () => {\n    render(<MonitorConfig />)\n\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isRunning: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n\n    const { unmount } = render(<MonitorConfig />)\n    const afterRenderActions = [setSocket(socket), setMonitorLoadingPause(true)]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n    expect(useIoConnectionSpy).toHaveBeenCalledWith(\n      getSocketApiUrl('monitor'),\n      { query: { instanceId: '1' }, token: '' },\n    )\n\n    unmount()\n  })\n\n  it(`should emit ${MonitorEvent.Monitor} event`, () => {\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isRunning: true,\n      isSaveToFile: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n\n    const { unmount } = render(<MonitorConfig />)\n\n    socket.socketClient.on(MonitorEvent.Monitor, (data: any) => {\n      expect(data).toEqual({ logFileId: expect.any(String) })\n    })\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    const afterRenderActions = [\n      setSocket(socket),\n      setMonitorLoadingPause(true),\n      setLogFileId(expect.any(String)),\n      setStartTimestamp(expect.any(Number)),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n\n  it(`should not emit ${MonitorEvent.Monitor} event when paused`, () => {\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isRunning: true,\n      isPaused: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n\n    const { unmount } = render(<MonitorConfig />)\n    const mockedMonitorEvent = jest.fn()\n\n    socket.socketClient.on(MonitorEvent.Monitor, mockedMonitorEvent)\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    expect(mockedMonitorEvent).not.toBeCalled()\n\n    unmount()\n  })\n\n  it('monitor should catch Exception', () => {\n    const { unmount } = render(<MonitorConfig />)\n\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isRunning: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n\n    socket.on(MonitorEvent.Exception, (error: Error) => {\n      expect(error).toEqual({ message: 'test', name: 'error' })\n      // done()\n    })\n\n    socket.socketClient.emit(MonitorEvent.Exception, {\n      message: 'test',\n      name: 'error',\n    })\n\n    const afterRenderActions = [\n      setSocket(socket),\n      setMonitorLoadingPause(true),\n      pauseMonitor(),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n\n  it('monitor should catch connect_error', () => {\n    const { unmount } = render(<MonitorConfig />)\n\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isRunning: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n\n    socket.on(SocketEvent.ConnectionError, (error: Error) => {\n      expect(error).toEqual({ message: 'test', name: 'error' })\n    })\n\n    socket.socketClient.emit(SocketEvent.ConnectionError, {\n      message: 'test',\n      name: 'error',\n    })\n\n    const afterRenderActions = [setSocket(socket), setMonitorLoadingPause(true)]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n\n  it('monitor should catch disconnect', () => {\n    const { unmount } = render(<MonitorConfig retryDelay={0} />)\n\n    const monitorSelectorMock = jest.fn().mockReturnValue({\n      isRunning: true,\n    })\n    monitorSelector.mockImplementation(monitorSelectorMock)\n\n    socket.socketClient.emit(SocketEvent.Disconnect)\n\n    const afterRenderActions = [\n      setSocket(socket),\n      setMonitorLoadingPause(true),\n      pauseMonitor(),\n      stopMonitor(),\n      lockResume(),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor-config/MonitorConfig.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { debounce } from 'lodash'\nimport { Socket } from 'socket.io-client'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport {\n  concatMonitorItems,\n  lockResume,\n  monitorSelector,\n  pauseMonitor,\n  resetMonitorItems,\n  setError,\n  setLogFileId,\n  setMonitorLoadingPause,\n  setSocket,\n  setStartTimestamp,\n  stopMonitor,\n} from 'uiSrc/slices/cli/monitor'\nimport { getSocketApiUrl, Nullable } from 'uiSrc/utils'\nimport {\n  MonitorErrorMessages,\n  MonitorEvent,\n  SocketErrors,\n  SocketEvent,\n} from 'uiSrc/constants'\nimport { IMonitorDataPayload } from 'uiSrc/slices/interfaces'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { appCsrfSelector } from 'uiSrc/slices/app/csrf'\nimport { useIoConnection } from 'uiSrc/services/hooks/useIoConnection'\nimport { IMonitorData } from 'apiSrc/modules/profiler/interfaces/monitor-data.interface'\n\nimport ApiStatusCode from '../../constants/apiStatusCode'\n\ninterface IProps {\n  retryDelay?: number\n}\nconst MonitorConfig = ({ retryDelay = 15000 }: IProps) => {\n  const { id: instanceId = '' } = useSelector(connectedInstanceSelector)\n  const {\n    socket,\n    isRunning,\n    isPaused,\n    isSaveToFile,\n    isMinimizedMonitor,\n    isShowMonitor,\n  } = useSelector(monitorSelector)\n  const { token } = useSelector(appCsrfSelector)\n\n  const socketRef = useRef<Nullable<Socket>>(null)\n  const connectIo = useIoConnection(getSocketApiUrl('monitor'), {\n    token,\n    query: { instanceId },\n  })\n  const logFileIdRef = useRef<string>()\n  const timestampRef = useRef<number>()\n  const retryTimerRef = useRef<NodeJS.Timer>()\n  const payloadsRef = useRef<IMonitorDataPayload[]>([])\n\n  const dispatch = useDispatch()\n\n  const setNewItems = debounce(\n    (items, onSuccess?) => {\n      dispatch(concatMonitorItems(items))\n      onSuccess?.()\n    },\n    50,\n    {\n      maxWait: 150,\n    },\n  )\n\n  const getErrorMessage = (error: {\n    type: string\n    name: any\n    message: any\n  }): string => {\n    if (error?.type === SocketErrors.TransportError) {\n      return MonitorErrorMessages.LostConnection\n    }\n    return error?.name || error?.message\n  }\n\n  useEffect(() => {\n    if (!isRunning || !instanceId || socket?.connected) {\n      return\n    }\n\n    logFileIdRef.current = `_redis_${uuidv4()}`\n    timestampRef.current = Date.now()\n\n    // Create SocketIO connection to instance by instanceId\n    socketRef.current = connectIo()\n\n    dispatch(setSocket(socketRef.current))\n\n    const handleDisconnect = () => {\n      socketRef.current?.removeAllListeners()\n      dispatch(pauseMonitor())\n      dispatch(stopMonitor())\n      dispatch(lockResume())\n    }\n\n    // Catch exceptions\n    socketRef.current?.on(MonitorEvent.Exception, (payload) => {\n      if (payload.status === ApiStatusCode.Forbidden) {\n        handleDisconnect()\n        dispatch(setError(MonitorErrorMessages.NoPerm))\n        dispatch(resetMonitorItems())\n        return\n      }\n\n      payloadsRef.current.push({\n        isError: true,\n        time: `${Date.now()}`,\n        ...payload,\n      })\n      setNewItems(payloadsRef.current, () => {\n        payloadsRef.current.length = 0\n      })\n      dispatch(pauseMonitor())\n    })\n\n    // Catch disconnect\n    socketRef.current?.on(SocketEvent.Disconnect, () => {\n      if (retryDelay) {\n        retryTimerRef.current = setTimeout(handleDisconnect, retryDelay)\n      } else {\n        handleDisconnect()\n      }\n    })\n\n    // Catch connect error\n    socketRef.current?.on(SocketEvent.ConnectionError, (error) => {\n      payloadsRef.current.push({\n        isError: true,\n        time: `${Date.now()}`,\n        message: getErrorMessage(error),\n      })\n      setNewItems(payloadsRef.current, () => {\n        payloadsRef.current.length = 0\n      })\n    })\n  }, [instanceId, isRunning, isPaused])\n\n  useEffect(() => {\n    if (!isRunning) {\n      return\n    }\n\n    socketRef.current?.removeAllListeners(SocketEvent.Connect)\n    socketRef.current?.on(SocketEvent.Connect, () => {\n      // Trigger Monitor event\n      clearTimeout(retryTimerRef.current!)\n      dispatch(setLogFileId(logFileIdRef.current))\n      dispatch(setStartTimestamp(timestampRef.current))\n      if (!isPaused) {\n        subscribeMonitorEvents()\n      }\n    })\n  }, [isRunning, isPaused])\n\n  useEffect(() => {\n    if (!isRunning || isPaused || !socketRef.current?.connected) {\n      return\n    }\n\n    subscribeMonitorEvents()\n  }, [isRunning, isPaused])\n\n  useEffect(() => {\n    if (!isRunning) return\n\n    const pauseUnpause = async () => {\n      !isPaused &&\n        (await new Promise<void>((resolve) =>\n          socket?.emit(MonitorEvent.Monitor, () => resolve()),\n        ))\n      isPaused &&\n        (await new Promise<void>((resolve) =>\n          socket?.emit(MonitorEvent.Pause, () => resolve()),\n        ))\n      dispatch(setMonitorLoadingPause(false))\n    }\n    dispatch(setMonitorLoadingPause(true))\n    pauseUnpause().catch(console.error)\n  }, [isPaused, isRunning])\n\n  useEffect(() => {\n    if (!isRunning) {\n      socket?.emit(MonitorEvent.FlushLogs)\n      socket?.removeAllListeners()\n      socket?.disconnect()\n    }\n  }, [socket, isRunning, isShowMonitor, isMinimizedMonitor])\n\n  const subscribeMonitorEvents = () => {\n    socketRef.current?.removeAllListeners(MonitorEvent.MonitorData)\n    socketRef.current?.emit(\n      MonitorEvent.Monitor,\n      { logFileId: isSaveToFile ? logFileIdRef.current : null },\n      handleMonitorEvents,\n    )\n  }\n\n  const handleMonitorEvents = () => {\n    dispatch(setMonitorLoadingPause(false))\n    socketRef.current?.on(\n      MonitorEvent.MonitorData,\n      (payload: IMonitorData[]) => {\n        payloadsRef.current = payloadsRef.current.concat(payload)\n\n        // set batch of payloads and then clear batch\n        setNewItems(payloadsRef.current, () => {\n          payloadsRef.current.length = 0\n          // reset all timings after items were changed\n          setNewItems.cancel()\n        })\n      },\n    )\n  }\n\n  return null\n}\n\nexport default MonitorConfig\n"
  },
  {
    "path": "redisinsight/ui/src/components/monitor-config/index.ts",
    "content": "import MonitorConfig from './MonitorConfig'\n\nexport default MonitorConfig\n"
  },
  {
    "path": "redisinsight/ui/src/components/multi-search/MultiSearch.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { map } from 'lodash'\nimport { fireEvent, render, screen, act } from 'uiSrc/utils/test-utils'\n\nimport MultiSearch, { Props } from './MultiSearch'\n\nconst mockedProps = mock<Props>()\nconst searchInputId = 'search-key'\n\nconst suggestionOptions = [\n  { id: '1', option: 'List', value: 'first' },\n  { id: '2', option: 'Hash', value: 'second' },\n  { id: '3', option: 'String', value: '*' },\n  { id: '4', value: '**]' },\n]\n\ndescribe('MultiSearch', () => {\n  it('should render', () => {\n    expect(render(<MultiSearch {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should proper change input value', () => {\n    const onKeyDown = jest.fn()\n    const onChange = jest.fn()\n    const inputVal = '*[]zx'\n\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        onChange={onChange}\n        onKeyDown={onKeyDown}\n        data-testid={searchInputId}\n      />,\n    )\n\n    const searchInput = screen.getByTestId(searchInputId)\n    fireEvent.change(searchInput, { target: { value: inputVal } })\n    expect(searchInput).toHaveValue(inputVal)\n    expect(onChange).toBeCalledWith(inputVal)\n\n    fireEvent.keyDown(searchInput, { key: 'A', code: 'KeyA' })\n    fireEvent.keyDown(searchInput, { key: 'B', code: 'KeyB' })\n    expect(onKeyDown).toBeCalledTimes(2)\n  })\n\n  it('should show option', () => {\n    render(<MultiSearch {...instance(mockedProps)} options={['hash']} />)\n    expect(screen.getByTestId('badge-hash_')).toBeInTheDocument()\n  })\n\n  it('should delete option', () => {\n    const onChangeOptions = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        options={['hash']}\n        onChangeOptions={onChangeOptions}\n      />,\n    )\n\n    const deleteHashBtn = screen.getByTestId('hash-delete-btn')\n    fireEvent.click(deleteHashBtn)\n    expect(onChangeOptions).toBeCalledWith([])\n  })\n\n  it('should delete option with more then 1 option', () => {\n    const onChangeOptions = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        options={['hash', 'zset', 'ts']}\n        onChangeOptions={onChangeOptions}\n      />,\n    )\n\n    const deleteHashBtn = screen.getByTestId('hash-delete-btn')\n    fireEvent.click(deleteHashBtn)\n    expect(onChangeOptions).toBeCalledWith(['zset', 'ts'])\n  })\n\n  it('should not show reset filters btn with empty value', () => {\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        value=\"\"\n        data-testid={searchInputId}\n      />,\n    )\n    expect(screen.queryByTestId('reset-filter-btn')).not.toBeInTheDocument()\n  })\n\n  it('should show reset filters btn', () => {\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        value=\"val\"\n        data-testid={searchInputId}\n      />,\n    )\n    expect(screen.getByTestId('reset-filter-btn')).toBeInTheDocument()\n  })\n\n  it('should call onClear', () => {\n    const onClear = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        onClear={onClear}\n        value=\"val\"\n        options={['hash']}\n        data-testid={searchInputId}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('reset-filter-btn'))\n    expect(onClear).toBeCalled()\n  })\n\n  it('should call onSubmit', () => {\n    const onSubmit = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        onSubmit={onSubmit}\n        data-testid={searchInputId}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('search-btn'))\n    expect(onSubmit).toBeCalled()\n  })\n\n  it('should not render suggestions by default', () => {\n    render(\n      <MultiSearch {...instance(mockedProps)} data-testid={searchInputId} />,\n    )\n    expect(screen.queryByTestId('suggestions')).not.toBeInTheDocument()\n  })\n\n  it('should show suggestions after click on button with proper text', () => {\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete: jest.fn(),\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n\n    expect(screen.getByTestId('suggestions')).toBeInTheDocument()\n    suggestionOptions.forEach(({ id, option, value }) => {\n      expect(screen.getByTestId(`suggestion-item-${id}`)).toBeInTheDocument()\n      expect(screen.getByTestId(`suggestion-item-${id}`)).toHaveTextContent(\n        (option ?? '') + value,\n      )\n    })\n  })\n\n  it('should show suggestions after key down arrow down', async () => {\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete: jest.fn(),\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'ArrowDown' })\n    })\n\n    expect(screen.getByTestId('suggestions')).toBeInTheDocument()\n  })\n\n  it('should call onApply after press enter on suggestion', async () => {\n    const onApply = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply,\n          onDelete: jest.fn(),\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'ArrowDown' })\n    })\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'ArrowDown' })\n    })\n\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'Enter' })\n    })\n    expect(onApply).toBeCalledWith(suggestionOptions[0])\n  })\n\n  it('should call onKeyDown if suggestions not opened', async () => {\n    const onKeyDown = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        onKeyDown={onKeyDown}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete: jest.fn(),\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), {\n        key: 'A',\n        code: 'KeyA',\n      })\n    })\n    expect(onKeyDown).toBeCalledTimes(1)\n  })\n\n  it('should call onApply after click on suggestion', () => {\n    const onApply = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply,\n          onDelete: jest.fn(),\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n    fireEvent.click(screen.getByTestId('suggestion-item-2'))\n    expect(onApply).toBeCalledWith(suggestionOptions[1])\n  })\n\n  it('should call onDelete after click on delete suggestion', () => {\n    const onDelete = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete,\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n    fireEvent.click(screen.getByTestId('remove-suggestion-item-2'))\n    expect(onDelete).toBeCalledWith(['2'])\n  })\n\n  it('should call onDelete after click on delete all suggestion', () => {\n    const onDelete = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete,\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n    fireEvent.click(screen.getByTestId('clear-history-btn'))\n    expect(onDelete).toBeCalledWith(map(suggestionOptions, 'id'))\n  })\n\n  it('should call onDelete after press delete on suggestion', async () => {\n    const onDelete = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete,\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'ArrowDown' })\n    })\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'ArrowDown' })\n    })\n\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'Delete' })\n    })\n    expect(onDelete).toBeCalledWith(['1'])\n  })\n\n  it('should close suggestion on Esc', async () => {\n    const onDelete = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete,\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'ArrowDown' })\n    })\n\n    expect(screen.getByTestId('suggestions')).toBeInTheDocument()\n\n    await act(() => {\n      fireEvent.keyDown(screen.getByTestId(searchInputId), { key: 'Esc' })\n    })\n\n    expect(screen.queryByTestId('suggestions')).not.toBeInTheDocument()\n  })\n\n  it('should close suggestion on click search button', async () => {\n    const onDelete = jest.fn()\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        onSubmit={jest.fn()}\n        suggestions={{\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete,\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n    expect(screen.getByTestId('suggestions')).toBeInTheDocument()\n    fireEvent.click(screen.getByTestId('search-btn'))\n    expect(screen.queryByTestId('suggestions')).not.toBeInTheDocument()\n  })\n\n  it('should show loading wth loading suggestions state', () => {\n    render(\n      <MultiSearch\n        {...instance(mockedProps)}\n        suggestions={{\n          loading: true,\n          options: suggestionOptions,\n          onApply: jest.fn(),\n          onDelete: jest.fn(),\n          buttonTooltipTitle: 'text',\n        }}\n        data-testid={searchInputId}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n    expect(screen.getByTestId('progress-suggestions')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/multi-search/MultiSearch.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport {\n  ActionIconButton,\n  IconButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport GroupBadge from 'uiSrc/components/group-badge'\n\ninterface StyledMultiSearchProps extends HTMLAttributes<HTMLDivElement> {\n  $isFocused: boolean\n}\n\ninterface StyledSuggestionProps extends HTMLAttributes<HTMLLIElement> {\n  $isFocused?: boolean\n}\n\nexport const StyledMultiSearch = styled(Row)<StyledMultiSearchProps>`\n  border: 1px solid\n    ${({ theme, $isFocused }: { theme: Theme; $isFocused: boolean }) =>\n      $isFocused\n        ? theme.components.input.states.focused.borderColor\n        : theme.components.input.states.normal.borderColor};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.input.states.normal.bgColor};\n  border-radius: 4px;\n\n  position: relative;\n  flex: 1;\n  height: 100%;\n  padding: 5px 6px 5px 0;\n  background-repeat: no-repeat;\n  background-size: 0 100%;\n  transition:\n    box-shadow 150ms ease-in,\n    background-image 150ms ease-in,\n    background-size 150ms ease-in,\n    background-color 150ms ease-in;\n`\n/**\n * Auto-suggestions dropdown container\n * Replaces: .autoSuggestions\n */\nexport const StyledAutoSuggestions = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  background-color: ${({ theme }) => theme.components.select.dropdown.bgColor};\n  border: 1px solid\n    ${({ theme }) => theme.components.select.states.disabled.borderColor};\n  position: absolute;\n  top: calc(100% + ${({ theme }) => theme.core.space.space025});\n  left: 0;\n  width: 100%;\n  min-width: calc(\n    ${({ theme }) => theme.core.space.space550} * 4\n  ); // 176px instead of hardcoded 180px\n\n  border-radius: ${({ theme }) => theme.core.space.space050};\n  z-index: 1001;\n  padding: ${({ theme }) => theme.core.space.space050} 0 0;\n`\n\nexport const StyledMultiSearchWrapper = styled(Row)<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  flex: 1;\n  padding-bottom: 0;\n  height: 100%;\n  min-height: 36px;\n`\n\nexport const StyledClearHistory = styled.li<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  border-top: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral400};\n  padding: 8px 10px;\n  text-align: left;\n\n  display: flex;\n  align-items: center;\n\n  cursor: pointer;\n  user-select: none;\n\n  &:hover {\n    background: ${({ theme }) =>\n      theme.components.select.dropdown.option.states.highlighted.bgColor};\n  }\n`\n\nexport const StyledSearchInput = styled(TextInput)`\n  flex: 1;\n  background-color: transparent;\n  max-width: 100%;\n  border: none;\n  height: 100%;\n  padding: 0 6px 0 10px;\n  background-image: none;\n`\n\n/**\n * Remove button within suggestion item\n * Replaces: .suggestionRemoveBtn\n * Note: Defined before StyledSuggestion so it can be referenced in selectors\n */\nexport const StyledSuggestionRemoveBtn = styled(IconButton)`\n  flex-shrink: 0;\n  visibility: hidden;\n  pointer-events: none;\n`\n\n/**\n * Individual suggestion item in the dropdown\n * Replaces: .suggestion\n */\nexport const StyledSuggestion = styled.li<StyledSuggestionProps>`\n  display: flex;\n  align-items: center;\n  text-align: left;\n  padding: ${({ theme }) => theme.core.space.space050}\n    ${({ theme }) => theme.core.space.space100};\n  cursor: default;\n\n  ${({ $isFocused, theme }) =>\n    $isFocused &&\n    `background: ${theme.components.select.dropdown.option.states.highlighted.bgColor};`}\n\n  &:hover {\n    background: ${({ theme }) =>\n      theme.components.select.dropdown.option.states.highlighted.bgColor};\n  }\n\n  /* Show remove button on hover or when focused */\n  &:hover ${StyledSuggestionRemoveBtn} {\n    visibility: visible;\n    pointer-events: auto;\n  }\n\n  ${({ $isFocused }) =>\n    $isFocused &&\n    `& ${StyledSuggestionRemoveBtn} { \n    visibility: visible; \n    pointer-events: auto; \n    }`}\n`\n\n/**\n * GroupBadge wrapper within suggestion item\n * Replaces: .suggestionOption\n */\nexport const StyledSuggestionOption = styled(GroupBadge)`\n  margin-right: ${({ theme }) => theme.core.space.space100};\n`\n\n/**\n * Text content within a suggestion item\n * Replaces: .suggestionText\n */\nexport const StyledSuggestionText = styled.span<\n  HTMLAttributes<HTMLSpanElement>\n>`\n  text-overflow: ellipsis;\n  overflow: hidden;\n  white-space: nowrap;\n  flex-grow: 1;\n  line-height: 1.4;\n`\n/**\n * Clear/Reset filters button\n * Replaces: .clearButton\n */\nexport const StyledClearButton = styled(ActionIconButton)`\n  margin-left: ${({ theme }) => theme.core.space.space050};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/multi-search/MultiSearch.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { GroupBadge, RiTooltip } from 'uiSrc/components'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\nimport { Nullable } from 'uiSrc/utils'\n\nimport {\n  CancelSlimIcon,\n  SearchIcon,\n  SwitchIcon,\n} from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport {\n  StyledAutoSuggestions,\n  StyledClearButton,\n  StyledClearHistory,\n  StyledMultiSearch,\n  StyledMultiSearchWrapper,\n  StyledSearchInput,\n  StyledSuggestion,\n  StyledSuggestionOption,\n  StyledSuggestionRemoveBtn,\n  StyledSuggestionText,\n} from './MultiSearch.styles'\n\nimport { Text } from 'uiSrc/components/base/text'\n\ninterface MultiSearchSuggestion {\n  options: null | Array<{\n    id: string\n    option?: Nullable<string>\n    value: string\n  }>\n  buttonTooltipTitle: string\n  onApply: (suggestion: any) => void\n  onDelete: (ids: string[]) => void\n  loading?: boolean\n}\n\nexport interface Props {\n  value: string\n  options?: string[]\n  placeholder: string\n  disableSubmit?: boolean\n  onSubmit: () => void\n  onKeyDown?: (e: React.KeyboardEvent) => void\n  suggestions?: MultiSearchSuggestion\n  onChange: (value: string) => void\n  onChangeOptions?: (options: string[]) => void\n  onClear?: () => void\n  className?: string\n  compressed?: boolean\n  appendRight?: React.ReactNode\n  [key: string]: any\n}\n\nconst MultiSearch = (props: Props) => {\n  const {\n    value,\n    options = [],\n    suggestions,\n    placeholder,\n    disableSubmit,\n    onSubmit,\n    onChangeOptions,\n    onChange,\n    onKeyDown,\n    onClear = () => {},\n    className,\n    compressed,\n    appendRight,\n    ...rest\n  } = props\n  const [isInputFocus, setIsInputFocus] = useState<boolean>(false)\n  const [showAutoSuggestions, setShowAutoSuggestions] = useState<boolean>(false)\n  const [focusedItem, setFocusedItem] = useState<number>(-1)\n\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const { options: suggestionOptions = [] } = suggestions ?? {}\n\n  const isArrowUpOrDown = (key: string) =>\n    [keys.ARROW_DOWN, keys.ARROW_UP].includes(key)\n\n  useEffect(() => {\n    if (!suggestionOptions?.length) {\n      setFocusedItem(-1)\n      setShowAutoSuggestions(false)\n    }\n  }, [suggestionOptions])\n\n  const onDeleteOption = (option: string) => {\n    onChangeOptions?.(options.filter((item) => item !== option))\n  }\n\n  const exitAutoSuggestions = () => {\n    setFocusedItem(-1)\n    setShowAutoSuggestions(false)\n  }\n\n  const handleApplySuggestion = (index: number) => {\n    suggestions?.onApply?.(suggestionOptions?.[index] ?? null)\n    setFocusedItem(-1)\n    setShowAutoSuggestions(false)\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    e.stopPropagation()\n\n    if (!suggestionOptions?.length) {\n      onKeyDown?.(e)\n      return\n    }\n\n    const min = -1\n    const max = suggestionOptions.length - 1\n\n    if (!showAutoSuggestions && isArrowUpOrDown(e.key)) {\n      e.preventDefault()\n      setShowAutoSuggestions(true)\n      setFocusedItem(-1)\n      return\n    }\n\n    if (!showAutoSuggestions) {\n      onKeyDown?.(e)\n      return\n    }\n\n    if (isArrowUpOrDown(e.key)) {\n      e.preventDefault()\n      const diff = focusedItem + (keys.ARROW_DOWN === e.key ? 1 : -1)\n      setFocusedItem(diff > max ? min : diff < min ? max : diff)\n    }\n\n    if (e.key === 'Delete') {\n      focusedItem > -1 && e.preventDefault()\n      handleDeleteSuggestion(\n        focusedItem > -1 ? [suggestionOptions[focusedItem].id] : undefined,\n      )\n    }\n\n    if (keys.ESCAPE === e.key) setShowAutoSuggestions(false)\n    if (keys.TAB === e.key) exitAutoSuggestions()\n    if (keys.ENTER === e.key) handleApplySuggestion(focusedItem)\n  }\n\n  const handleDeleteSuggestion = (ids?: string[]) => {\n    inputRef.current?.focus()\n    if (ids) {\n      suggestions?.onDelete?.(ids)\n    }\n  }\n\n  const handleSubmit = () => {\n    exitAutoSuggestions()\n    onSubmit()\n  }\n\n  const SubmitBtn = () => (\n    <IconButton\n      icon={SearchIcon}\n      aria-label=\"Search\"\n      disabled={disableSubmit}\n      size=\"S\"\n      onClick={handleSubmit}\n      data-testid=\"search-btn\"\n    />\n  )\n\n  return (\n    <OutsideClickDetector onOutsideClick={exitAutoSuggestions}>\n      <StyledMultiSearchWrapper\n        align=\"center\"\n        className={className}\n        onKeyDown={handleKeyDown}\n        role=\"presentation\"\n        data-testid=\"multi-search\"\n      >\n        <StyledMultiSearch align=\"center\" gap=\"m\" $isFocused={isInputFocus}>\n          <div>\n            {options.map((option) => (\n              <GroupBadge\n                key={option}\n                type={option}\n                onDelete={onDeleteOption}\n                compressed={compressed}\n              />\n            ))}\n          </div>\n          <StyledSearchInput\n            placeholder={placeholder}\n            value={value}\n            onKeyDown={handleKeyDown}\n            onChange={onChange}\n            onFocus={() => setIsInputFocus(true)}\n            onBlur={() => setIsInputFocus(false)}\n            ref={inputRef}\n            {...rest}\n          />\n          {showAutoSuggestions && !!suggestionOptions?.length && (\n            <StyledAutoSuggestions\n              role=\"presentation\"\n              data-testid=\"suggestions\"\n            >\n              {suggestions?.loading && (\n                <ProgressBarLoader\n                  data-testid=\"progress-suggestions\"\n                  color=\"primary\"\n                />\n              )}\n              <ul role=\"listbox\">\n                {suggestionOptions?.map(\n                  ({ id, option, value }, index) =>\n                    value && (\n                      <StyledSuggestion\n                        key={id}\n                        $isFocused={focusedItem === index}\n                        onClick={() => handleApplySuggestion(index)}\n                        role=\"presentation\"\n                        data-testid={`suggestion-item-${id}`}\n                      >\n                        {option && (\n                          <StyledSuggestionOption\n                            type={option}\n                            compressed={compressed}\n                          />\n                        )}\n                        <StyledSuggestionText data-testid=\"suggestion-item-text\">\n                          {value}\n                        </StyledSuggestionText>\n                        <StyledSuggestionRemoveBtn\n                          icon={CancelSlimIcon}\n                          color=\"primary\"\n                          aria-label=\"Remove History Record\"\n                          onClick={(e: React.MouseEvent) => {\n                            e.stopPropagation()\n                            handleDeleteSuggestion([id])\n                          }}\n                          data-testid={`remove-suggestion-item-${id}`}\n                        />\n                      </StyledSuggestion>\n                    ),\n                )}\n              </ul>\n              <StyledClearHistory\n                role=\"presentation\"\n                onClick={() =>\n                  handleDeleteSuggestion(\n                    suggestionOptions?.map((item) => item.id),\n                  )\n                }\n                data-testid=\"clear-history-btn\"\n              >\n                <RiIcon type=\"EraserIcon\" style={{ marginRight: 6 }} />\n                <Text component=\"span\" size=\"m\">\n                  Clear history\n                </Text>\n              </StyledClearHistory>\n            </StyledAutoSuggestions>\n          )}\n          {(value || !!options.length) && (\n            <RiTooltip content=\"Reset Filters\" position=\"bottom\">\n              <StyledClearButton\n                icon={CancelSlimIcon}\n                size=\"XS\"\n                aria-label=\"Reset Filters\"\n                onClick={onClear}\n                data-testid=\"reset-filter-btn\"\n                variant=\"secondary\"\n              />\n            </RiTooltip>\n          )}\n          {!!suggestionOptions?.length && (\n            <RiTooltip\n              content={suggestions?.buttonTooltipTitle}\n              position=\"bottom\"\n            >\n              <IconButton\n                icon={SwitchIcon}\n                size=\"S\"\n                aria-label={suggestions?.buttonTooltipTitle}\n                onClick={() => {\n                  setShowAutoSuggestions((v) => !v)\n                  inputRef.current?.focus()\n                }}\n                data-testid=\"show-suggestions-btn\"\n              />\n            </RiTooltip>\n          )}\n          {appendRight}\n          {disableSubmit && (\n            <RiTooltip\n              position=\"top\"\n              content=\"Please choose index in order to preform the search\"\n            >\n              {SubmitBtn()}\n            </RiTooltip>\n          )}\n          {!disableSubmit && SubmitBtn()}\n        </StyledMultiSearch>\n      </StyledMultiSearchWrapper>\n    </OutsideClickDetector>\n  )\n}\n\nexport default MultiSearch\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport NavigationMenu from './NavigationMenu'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockAppInfoSelector = jest.requireActual('uiSrc/slices/app/info')\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextSelector: jest.fn().mockReturnValue({\n    workspace: 'database',\n  }),\n}))\n\njest.mock('uiSrc/slices/app/info', () => ({\n  ...jest.requireActual('uiSrc/slices/app/info'),\n  appInfoSelector: jest.fn().mockReturnValue({\n    server: {},\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '',\n  }),\n}))\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'mockRdiId',\n  }),\n}))\n\ndescribe('NavigationMenu', () => {\n  describe('without connectedInstance', () => {\n    it('should render', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.DockerOnPremise,\n        },\n      }))\n      expect(render(<NavigationMenu />)).toBeTruthy()\n    })\n\n    it(\"shouldn't render private routes\", () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.DockerOnPremise,\n        },\n      }))\n      render(<NavigationMenu />)\n\n      expect(screen.queryByTestId('browser-page-btn')).not.toBeInTheDocument()\n    })\n\n    it('should render help menu', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.RedisStack,\n        },\n      }))\n      render(<NavigationMenu />)\n\n      expect(screen.getByTestId('help-menu-button')).toBeTruthy()\n    })\n\n    it('should render help menu items with proper links', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.RedisStack,\n        },\n      }))\n      render(<NavigationMenu />)\n\n      fireEvent.click(screen.getByTestId('help-menu-button'))\n\n      const submitBugBtn = screen.getByTestId('submit-bug-btn')\n      expect(submitBugBtn).toBeInTheDocument()\n      expect(submitBugBtn?.getAttribute('href')).toEqual(\n        EXTERNAL_LINKS.githubIssues,\n      )\n\n      expect(screen.getByTestId('shortcuts-btn')).toBeInTheDocument()\n\n      const releaseNotesBtn = screen.getByTestId('release-notes-btn')\n      expect(releaseNotesBtn).toBeInTheDocument()\n      expect(releaseNotesBtn?.getAttribute('href')).toEqual(\n        EXTERNAL_LINKS.releaseNotes,\n      )\n    })\n  })\n\n  describe('with connectedInstance', () => {\n    beforeEach(() => {\n      ;(connectedInstanceSelector as jest.Mock).mockReturnValue({\n        id: '123',\n        connectionType: 'STANDALONE',\n        db: 0,\n      })\n    })\n\n    it('should render', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.DockerOnPremise,\n        },\n      }))\n      expect(render(<NavigationMenu />)).toBeTruthy()\n    })\n\n    it('should not render private routes with instanceId', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.DockerOnPremise,\n        },\n      }))\n      render(<NavigationMenu />)\n\n      expect(screen.queryByTestId('browser-page-btn')).not.toBeInTheDocument()\n      expect(screen.queryByTestId('workbench-page-btn')).not.toBeInTheDocument()\n    })\n\n    it('should render public routes', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.DockerOnPremise,\n        },\n      }))\n      render(<NavigationMenu />)\n\n      expect(screen.getByTestId('settings-page-btn')).toBeTruthy()\n    })\n\n    it('should render cloud link', () => {\n      const { container } = render(<NavigationMenu />)\n\n      const createCloudItem = container.querySelector(\n        '[data-testid=\"create-cloud-sidebar-item\"]',\n      )\n      expect(createCloudItem).toBeTruthy()\n    })\n\n    it('should render github btn with proper link', () => {\n      ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n        ...mockAppInfoSelector,\n        server: {\n          buildType: BuildType.DockerOnPremise,\n        },\n      }))\n      render(<NavigationMenu />)\n\n      const githubBtn = screen.getByTestId('github-repo-btn')\n      expect(githubBtn).toBeTruthy()\n      expect(githubBtn?.getAttribute('href')).toEqual(EXTERNAL_LINKS.githubRepo)\n    })\n  })\n\n  describe('feature flags tests', () => {\n    it('should show feature dependent items when feature flag is on', async () => {\n      const initialStoreState = set(\n        cloneDeep(initialStateDefault),\n        `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n        { flag: true },\n      )\n\n      render(<NavigationMenu />, {\n        store: mockStore(initialStoreState),\n      })\n      fireEvent.click(screen.getByTestId('help-menu-button'))\n\n      expect(screen.queryByTestId('notification-menu')).toBeInTheDocument()\n      expect(screen.queryByTestId('help-center')).toBeInTheDocument()\n      expect(\n        screen.queryByTestId('github-repo-divider-default'),\n      ).toBeInTheDocument()\n      expect(screen.queryByTestId('github-repo-icon')).toBeInTheDocument()\n    })\n\n    it('should hide feature dependent items when feature flag is off', async () => {\n      const initialStoreState = set(\n        cloneDeep(initialStateDefault),\n        `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n        { flag: false },\n      )\n\n      render(<NavigationMenu />, {\n        store: mockStore(initialStoreState),\n      })\n      expect(screen.queryByTestId('help-center')).not.toBeInTheDocument()\n      expect(screen.queryByTestId('github-repo-icon')).not.toBeInTheDocument()\n      expect(\n        screen.queryByTestId('github-repo-divider-default'),\n      ).not.toBeInTheDocument()\n      expect(screen.queryByTestId('notification-menu')).not.toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx",
    "content": "/* eslint-disable react/no-this-in-sfc */\nimport React from 'react'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\n\nimport { FeatureFlagComponent } from 'uiSrc/components'\n\nimport {\n  SideBar,\n  SideBarContainer,\n  SideBarDivider,\n  SideBarFooter,\n  SideBarItem,\n  SideBarItemIcon,\n} from 'uiSrc/components/base/layout/sidebar'\nimport { GithubIcon } from 'uiSrc/components/base/icons'\nimport { INavigations } from './navigation.types'\nimport CreateCloud from './components/create-cloud'\nimport HelpMenu from './components/help-menu/HelpMenu'\nimport NotificationMenu from './components/notifications-center'\n\nimport { RedisLogo } from './components/redis-logo/RedisLogo'\nimport { useNavigation } from './hooks/useNavigation'\nimport HighlightedFeature from '../hightlighted-feature/HighlightedFeature'\nimport styles from './styles.module.scss'\n\nconst NavigationMenu = () => {\n  const { isRdiWorkspace, publicRoutes, highlightedPages } = useNavigation()\n\n  const renderPublicNavItem = (nav: INavigations) => {\n    const fragment = (\n      <HighlightedFeature\n        key={nav.tooltipText}\n        isHighlight={!!highlightedPages[nav.pageName]?.length}\n        dotClassName={styles.highlightDot}\n        transformOnHover\n      >\n        <SideBarItem\n          tooltipProps={{ text: nav.tooltipText, placement: 'right' }}\n          onClick={nav.onClick}\n          isActive={nav.isActivePage}\n          className={styles.sideBarItem}\n        >\n          <SideBarItemIcon\n            icon={nav.iconType}\n            aria-label={nav.ariaLabel}\n            data-testid={nav.dataTestId}\n          />\n        </SideBarItem>\n      </HighlightedFeature>\n    )\n\n    return nav.featureFlag ? (\n      <FeatureFlagComponent\n        name={nav.featureFlag}\n        key={nav.tooltipText}\n        enabledByDefault\n      >\n        {fragment}\n      </FeatureFlagComponent>\n    ) : (\n      fragment\n    )\n  }\n\n  return (\n    <SideBar\n      isExpanded={false}\n      aria-label=\"Main navigation\"\n      data-testid=\"main-navigation-sidebar\"\n      className={styles.mainNavbar}\n    >\n      <SideBarContainer>\n        <RedisLogo isRdiWorkspace={isRdiWorkspace} />\n      </SideBarContainer>\n      <SideBarFooter className={styles.footer}>\n        <FeatureFlagComponent name={FeatureFlags.envDependent} enabledByDefault>\n          <CreateCloud />\n          <NotificationMenu />\n        </FeatureFlagComponent>\n        <FeatureFlagComponent name={FeatureFlags.envDependent} enabledByDefault>\n          <HelpMenu />\n        </FeatureFlagComponent>\n\n        {publicRoutes.map(renderPublicNavItem)}\n\n        <FeatureFlagComponent name={FeatureFlags.envDependent} enabledByDefault>\n          <SideBarDivider data-testid=\"github-repo-divider-default\" />\n          <SideBarFooter.Link\n            data-testid=\"github-repo-btn\"\n            href={EXTERNAL_LINKS.githubRepo}\n            target=\"_blank\"\n          >\n            <SideBarItem\n              className={styles.githubNavItem}\n              tooltipProps={{\n                text: 'Star us on GitHub',\n                placement: 'right',\n              }}\n            >\n              <SideBarItemIcon\n                icon={GithubIcon}\n                aria-label=\"github-repo-icon\"\n                data-testid=\"github-repo-icon\"\n              />\n            </SideBarItem>\n          </SideBarFooter.Link>\n        </FeatureFlagComponent>\n      </SideBarFooter>\n    </SideBar>\n  )\n}\n\nexport default NavigationMenu\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigation.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledAppNavigation = styled.div`\n  display: grid;\n  grid-template-columns: 1fr auto 1fr;\n  background: ${({ theme }) =>\n    theme.components.appBar.variants.default.bgColor};\n  color: ${({ theme }) => theme.components.appBar.variants.default.color};\n  height: 6rem;\n  z-index: ${({ theme }) => theme.core.zIndex.zIndex5};\n  box-shadow: ${({ theme }) => theme.components.appBar.boxShadow};\n  box-sizing: border-box;\n  align-items: center;\n`\ntype NavContainerProps = React.ComponentProps<typeof Row> & {\n  $borderLess?: boolean\n}\nexport const StyledAppNavigationContainer = styled(Row)<NavContainerProps>`\n  height: 100%;\n  width: auto;\n  &:first-child {\n    padding-inline-start: ${({ theme }) => theme.components.appBar.group.gap};\n  }\n  &:last-child {\n    padding-inline-end: ${({ theme }) => theme.components.appBar.group.gap};\n  }\n\n  border-bottom: ${({ theme, $borderLess }) =>\n      $borderLess ? '0' : theme.components.tabs.variants.default.tabsLine.size}\n    solid\n    ${({ theme }) => theme.components.tabs.variants.default.tabsLine.color};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigation.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  StyledAppNavigation,\n  StyledAppNavigationContainer,\n} from './AppNavigation.styles'\nimport FeatureFlagComponent from 'uiSrc/components/feature-flag-component/FeatureFlagComponent'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { OnboardingTourOptions } from 'uiSrc/components/onboarding-tour'\nimport NavigationTabTrigger from './AppNavigationTabTrigger'\nimport { INavigations } from 'uiSrc/components/navigation-menu/navigation.types'\n\ntype AppNavigationContainerProps = {\n  children?: ReactNode\n  borderLess?: boolean\n} & Pick<\n  React.ComponentProps<typeof Row>,\n  'gap' | 'justify' | 'align' | 'grow' | 'style'\n>\nconst AppNavigationContainer = ({\n  children,\n  borderLess,\n  gap = 'm',\n  justify,\n  align,\n  grow,\n  style,\n}: AppNavigationContainerProps) => (\n  <StyledAppNavigationContainer\n    grow={grow}\n    gap={gap}\n    justify={justify}\n    align={align}\n    $borderLess={borderLess}\n    full\n    style={style}\n  >\n    {children}\n  </StyledAppNavigationContainer>\n)\n\nexport type AppNavigationProps = {\n  routes: INavigations[]\n  actions?: ReactNode\n  onChange?: (tabValue: string) => void\n}\n\nconst AppNavigation = ({ actions, onChange, routes }: AppNavigationProps) => {\n  const activeTab = routes.find((route) => route.isActivePage)\n  const navTabs: (TabInfo & {\n    isActivePage: boolean\n    featureFlag?: FeatureFlags\n    onboard?: OnboardingTourOptions\n  })[] = routes.map((route) => ({\n    label: route.tooltipText,\n    content: '',\n    value: route.pageName,\n    isActivePage: route.isActivePage,\n    featureFlag: route.featureFlag,\n    onboard: route.onboard,\n  }))\n\n  return (\n    <StyledAppNavigation>\n      <AppNavigationContainer />\n      <AppNavigationContainer\n        borderLess\n        grow={false}\n        justify=\"center\"\n        align=\"end\"\n      >\n        <Tabs.Compose\n          value={activeTab?.pageName}\n          onChange={(tabValue) => {\n            const tabNavItem = routes.find(\n              (route) => route.pageName === tabValue,\n            )\n            if (tabNavItem) {\n              onChange?.(tabNavItem.pageName) // remove actions before navigation, displayed page, should set their own actions\n              tabNavItem.onClick()\n            }\n          }}\n        >\n          <Tabs.TabBar.Compose variant=\"default\">\n            {navTabs.map(\n              (\n                { value, label, disabled, featureFlag, onboard, isActivePage },\n                index,\n              ) => {\n                const key = `${value}-${index}`\n                if (featureFlag) {\n                  return (\n                    <FeatureFlagComponent\n                      name={featureFlag as FeatureFlags}\n                      key={key}\n                    >\n                      <NavigationTabTrigger\n                        value={value}\n                        label={label}\n                        disabled={disabled}\n                        onboard={onboard}\n                        isActivePage={isActivePage}\n                        tabKey={key}\n                      />\n                    </FeatureFlagComponent>\n                  )\n                }\n\n                return (\n                  <NavigationTabTrigger\n                    key={key}\n                    value={value}\n                    label={label}\n                    disabled={disabled}\n                    onboard={onboard}\n                    isActivePage={isActivePage}\n                    tabKey={key}\n                  />\n                )\n              },\n            )}\n          </Tabs.TabBar.Compose>\n        </Tabs.Compose>\n      </AppNavigationContainer>\n      <AppNavigationContainer justify=\"end\" align=\"center\">\n        {actions}\n      </AppNavigationContainer>\n    </StyledAppNavigation>\n  )\n}\n\nexport default AppNavigation\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/app-navigation/AppNavigationTabTrigger.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport Tabs from 'uiSrc/components/base/layout/tabs'\nimport { OnboardingTourOptions } from 'uiSrc/components/onboarding-tour'\nimport { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding'\n\ninterface NavigationTabTriggerProps {\n  value: string\n  label?: ReactNode | string\n  disabled?: boolean\n  onboard?: OnboardingTourOptions\n  isActivePage: boolean\n  tabKey: string\n}\n\nconst NavigationTabTrigger = ({\n  value,\n  label,\n  disabled,\n  onboard,\n  isActivePage,\n  tabKey,\n}: NavigationTabTriggerProps) =>\n  renderOnboardingTourWithChild(\n    <Tabs.TabBar.Trigger.Compose value={value} disabled={disabled} key={tabKey}>\n      <Tabs.TabBar.Trigger.Tab>{label ?? value}</Tabs.TabBar.Trigger.Tab>\n      <Tabs.TabBar.Trigger.Marker />\n    </Tabs.TabBar.Trigger.Compose>,\n    { options: onboard, anchorPosition: 'upCenter' },\n    isActivePage,\n    `ob-${label}`,\n  )\n\nexport default NavigationTabTrigger\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { cleanup, mockedStore, render, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { sendEventTelemetry } from 'uiSrc/telemetry'\nimport { HELP_LINKS } from 'uiSrc/pages/home/constants'\nimport * as appFeaturesSlice from 'uiSrc/slices/app/features'\nimport { SideBar } from 'uiSrc/components/base/layout/sidebar'\nimport CreateCloud from './CreateCloud'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockFeatureFlags = (cloudAds = true) => {\n  jest\n    .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n    .mockReturnValue({\n      cloudSso: {\n        flag: true,\n      },\n      cloudAds: {\n        flag: cloudAds,\n      },\n    })\n}\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  mockFeatureFlags()\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst sideBarWithCreateCloud = (\n  <SideBar isExpanded={false}>\n    <CreateCloud />\n  </SideBar>\n)\n\ndescribe('CreateCloud', () => {\n  it('should render', () => {\n    expect(render(sideBarWithCreateCloud)).toBeTruthy()\n  })\n\n  it('should call proper actions on click cloud button', () => {\n    const { container } = render(sideBarWithCreateCloud)\n    const createCloudItem = container.querySelector(\n      '[data-testid=\"create-cloud-sidebar-item\"]',\n    )\n\n    fireEvent.click(createCloudItem as Element)\n\n    expect(store.getActions()).toEqual([\n      setSSOFlow(OAuthSocialAction.Create),\n      setSocialDialogState(OAuthSocialSource.NavigationMenu),\n    ])\n  })\n\n  it('should call proper telemetry when sso is disabled', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      cloudSso: {\n        flag: false,\n      },\n      cloudAds: {\n        flag: true,\n      },\n    })\n    const { container } = render(sideBarWithCreateCloud)\n    const createCloudItem = container.querySelector(\n      '[data-testid=\"create-cloud-sidebar-item\"]',\n    )\n\n    fireEvent.click(createCloudItem as Element)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: HELP_LINKS.cloud.event,\n      eventData: {\n        source: OAuthSocialSource.NavigationMenu,\n      },\n    })\n  })\n\n  it('should not render if cloud ads feature flag is disabled', () => {\n    mockFeatureFlags(false)\n    const { container } = render(sideBarWithCreateCloud)\n    const createCloudItem = container.querySelector(\n      '[data-testid=\"create-cloud-db-link\"]',\n    )\n    expect(createCloudItem).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/create-cloud/CreateCloud.tsx",
    "content": "import React from 'react'\n\nimport { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport CloudIcon from 'uiSrc/assets/img/oauth/cloud_centered.svg?react'\n\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { sendEventTelemetry } from 'uiSrc/telemetry'\nimport { HELP_LINKS } from 'uiSrc/pages/home/constants'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { SideBarItem } from 'uiSrc/components/base/layout/sidebar'\nimport { SideBarItemIcon } from 'uiSrc/components/base/layout/sidebar/SideBarItemIcon'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nconst CreateCloud = () => {\n  const onCLickLink = (isSSOEnabled: boolean) => {\n    if (isSSOEnabled) return\n\n    sendEventTelemetry({\n      event: HELP_LINKS.cloud.event,\n      eventData: {\n        source: OAuthSocialSource.NavigationMenu,\n      },\n    })\n  }\n\n  return (\n    <FeatureFlagComponent name={FeatureFlags.cloudAds}>\n      <OAuthSsoHandlerDialog>\n        {(ssoCloudHandlerClick, isSSOEnabled) => (\n          <Link\n            href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n              campaign: 'navigation_menu',\n            })}\n            style={{ marginInline: 'auto', backgroundColor: 'transparent' }}\n            data-testid=\"create-cloud-db-link\"\n          >\n            <SideBarItem\n              tooltipProps={{\n                text: 'Create FREE Redis Cloud database',\n                placement: 'right',\n              }}\n              onClick={(e) => {\n                onCLickLink(isSSOEnabled)\n                ssoCloudHandlerClick(e, {\n                  source: OAuthSocialSource.NavigationMenu,\n                  action: OAuthSocialAction.Create,\n                })\n              }}\n              style={{ marginInline: 'auto' }}\n              data-testid=\"create-cloud-sidebar-item\"\n            >\n              <SideBarItemIcon\n                width=\"20px\"\n                height=\"20px\"\n                icon={CloudIcon}\n                aria-label=\"cloud-db-icon\"\n                data-testid=\"cloud-db-icon\"\n              />\n            </SideBarItem>\n          </Link>\n        )}\n      </OAuthSsoHandlerDialog>\n    </FeatureFlagComponent>\n  )\n}\n\nexport default CreateCloud\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/create-cloud/index.ts",
    "content": "import CreateCloud from './CreateCloud'\n\nexport default CreateCloud\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/help-menu/HelpMenu.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { setOnboarding } from 'uiSrc/slices/app/features'\n\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  setReleaseNotesViewed,\n  setShortcutsFlyoutState,\n} from 'uiSrc/slices/app/info'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { SideBar } from 'uiSrc/components/base/layout/sidebar'\nimport HelpMenu from './HelpMenu'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/info', () => ({\n  ...jest.requireActual('uiSrc/slices/app/info'),\n  appElectronInfoSelector: jest.fn().mockReturnValue({\n    isReleaseNotesViewed: false,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst sideBarWithHelpMenu = (\n  <SideBar isExpanded={false}>\n    <HelpMenu />\n  </SideBar>\n)\n\ndescribe('HelpMenu', () => {\n  it('should render', () => {\n    expect(render(sideBarWithHelpMenu)).toBeTruthy()\n  })\n\n  it('should call proper action after click on keyboard shortcuts', () => {\n    render(sideBarWithHelpMenu)\n\n    fireEvent.click(screen.getByTestId('help-menu-button'))\n    fireEvent.click(screen.getByTestId('shortcuts-btn'))\n\n    const expectedActions = [setShortcutsFlyoutState(true)]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper action after click on release notes', () => {\n    render(sideBarWithHelpMenu)\n\n    fireEvent.click(screen.getByTestId('help-menu-button'))\n    fireEvent.click(screen.getByTestId('release-notes-btn'))\n\n    const expectedActions = [setReleaseNotesViewed(true)]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper action after click on reset onboarding', () => {\n    render(sideBarWithHelpMenu)\n\n    fireEvent.click(screen.getByTestId('help-menu-button'))\n    fireEvent.click(screen.getByTestId('reset-onboarding-btn'))\n\n    const expectedActions = [\n      setOnboarding({\n        currentStep: 0,\n        totalSteps: Object.keys(ONBOARDING_FEATURES || {}).length,\n      }),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper telemetry after click reset onboarding', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(sideBarWithHelpMenu)\n\n    fireEvent.click(screen.getByTestId('help-menu-button'))\n    fireEvent.click(screen.getByTestId('reset-onboarding-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.ONBOARDING_TOUR_TRIGGERED,\n      eventData: {\n        databaseId: '-',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(sideBarWithHelpMenu, {\n      store: mockStore(initialStoreState),\n    })\n    fireEvent.click(screen.getByTestId('help-menu-button'))\n\n    expect(screen.queryByTestId('submit-bug-btn')).toBeInTheDocument()\n    expect(screen.queryByTestId('reset-onboarding-btn')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(sideBarWithHelpMenu, {\n      store: mockStore(initialStoreState),\n    })\n    fireEvent.click(screen.getByTestId('help-menu-button'))\n\n    expect(screen.queryByTestId('submit-bug-btn')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('reset-onboarding-btn')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/help-menu/HelpMenu.tsx",
    "content": "import cx from 'classnames'\nimport React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { ReleaseNotesSource } from 'uiSrc/constants/telemetry'\nimport {\n  appElectronInfoSelector,\n  setReleaseNotesViewed,\n  setShortcutsFlyoutState,\n} from 'uiSrc/slices/app/info'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { setOnboarding } from 'uiSrc/slices/app/features'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { SupportIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport {\n  SideBarItem,\n  SideBarItemIcon,\n} from 'uiSrc/components/base/layout/sidebar'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport navStyles from '../../styles.module.scss'\nimport styles from './styles.module.scss'\n\nconst HelpMenu = () => {\n  const { id: connectedInstanceId = '' } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { isReleaseNotesViewed } = useSelector(appElectronInfoSelector)\n  const [isHelpMenuActive, setIsHelpMenuActive] = useState(false)\n\n  const dispatch = useDispatch()\n\n  const onKeyboardShortcutClick = () => {\n    setIsHelpMenuActive(false)\n    dispatch(setShortcutsFlyoutState(true))\n  }\n\n  const onClickReleaseNotes = async () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RELEASE_NOTES_LINK_CLICKED,\n      eventData: {\n        source: ReleaseNotesSource.helpCenter,\n      },\n    })\n    if (isReleaseNotesViewed === false) {\n      dispatch(setReleaseNotesViewed(true))\n    }\n  }\n\n  const onResetOnboardingClick = () => {\n    const totalSteps = Object.keys(ONBOARDING_FEATURES || {}).length\n\n    dispatch(setOnboarding({ currentStep: 0, totalSteps }))\n    sendEventTelemetry({\n      event: TelemetryEvent.ONBOARDING_TOUR_TRIGGERED,\n      eventData: {\n        databaseId: connectedInstanceId || '-',\n      },\n    })\n  }\n\n  const HelpMenuButton = (\n    <SideBarItem\n      className={cx({\n        [navStyles.navigationButtonNotified]: true,\n      })}\n      onClick={() => setIsHelpMenuActive((value) => !value)}\n      tooltipProps={{ text: 'Help', placement: 'right' }}\n      isActive={isHelpMenuActive}\n    >\n      <SideBarItemIcon\n        icon={SupportIcon}\n        aria-label=\"Help Menu\"\n        data-testid=\"help-menu-button\"\n      />\n    </SideBarItem>\n  )\n\n  return (\n    <RiPopover\n      anchorPosition=\"rightUp\"\n      isOpen={isHelpMenuActive}\n      anchorClassName={styles.unsupportedInfo}\n      panelClassName={cx('popoverLikeTooltip', styles.popoverWrapper)}\n      closePopover={() => setIsHelpMenuActive(false)}\n      button={HelpMenuButton}\n    >\n      <div className={styles.popover} data-testid=\"help-center\">\n        <Title size=\"XS\" className={styles.helpMenuTitle}>\n          Help Center\n        </Title>\n        <Spacer size=\"l\" />\n        <Row\n          className={styles.helpMenuItems}\n          align=\"center\"\n          justify=\"between\"\n          gap=\"l\"\n        >\n          <FeatureFlagComponent name={FeatureFlags.envDependent}>\n            <FlexItem grow={2} className={styles.helpMenuItem}>\n              <Link\n                className={styles.helpMenuItemLink}\n                href={EXTERNAL_LINKS.githubIssues}\n                target=\"_blank\"\n                data-testid=\"submit-bug-btn\"\n              >\n                <RiIcon type=\"GithubIcon\" size=\"original\" />\n                <Spacer size=\"xs\" />\n                <Text\n                  size=\"xs\"\n                  textAlign=\"center\"\n                  className={styles.helpMenuText}\n                >\n                  Provide <br /> Feedback\n                </Text>\n              </Link>\n            </FlexItem>\n          </FeatureFlagComponent>\n\n          <FlexItem className={styles.helpMenuItemRow} grow={4}>\n            <Row className={styles.helpMenuItemLink} align=\"center\" gap=\"xs\">\n              <RiIcon type=\"KeyboardShortcutsIcon\" size=\"l\" />\n              <Text\n                size=\"xs\"\n                className={styles.helpMenuTextLink}\n                onClick={onKeyboardShortcutClick}\n                data-testid=\"shortcuts-btn\"\n              >\n                Keyboard Shortcuts\n              </Text>\n            </Row>\n\n            <Row className={styles.helpMenuItemLink} align=\"center\" gap=\"xs\">\n              <div\n                className={cx({\n                  [styles.helpMenuItemNotified]: isReleaseNotesViewed === false,\n                })}\n                style={{ display: 'flex' }}\n              >\n                <RiIcon type=\"DocumentationIcon\" size=\"l\" />\n              </div>\n              <Link\n                onClick={onClickReleaseNotes}\n                className={styles.helpMenuTextLink}\n                href={EXTERNAL_LINKS.releaseNotes}\n                target=\"_blank\"\n                data-testid=\"release-notes-btn\"\n              >\n                <Text size=\"xs\" className={styles.helpMenuTextLink}>\n                  Release Notes\n                </Text>\n              </Link>\n            </Row>\n\n            <FeatureFlagComponent name={FeatureFlags.envDependent}>\n              <Row className={styles.helpMenuItemLink} align=\"center\" gap=\"xs\">\n                <RiIcon type=\"LightBulbIcon\" size=\"l\" />\n                <Text\n                  size=\"xs\"\n                  className={styles.helpMenuTextLink}\n                  onClick={onResetOnboardingClick}\n                  data-testid=\"reset-onboarding-btn\"\n                >\n                  Reset Onboarding\n                </Text>\n              </Row>\n            </FeatureFlagComponent>\n          </FlexItem>\n        </Row>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default HelpMenu\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/help-menu/styles.module.scss",
    "content": ".popoverWrapper {\n  min-width: 312px !important;\n\n  /* Ensure popover arrow (rendered in portal) uses tooltip background as fill.\n     The Arrow component uses fill: currentColor, so set color on the panel or target the Arrow class. */\n  :global([class*='Composestyle__Arrow-RedisUI__']) {\n    color: var(--euiTooltipBackgroundColor) !important;\n  }\n}\n\n.popover {\n  padding: 6px 15px 12px;\n}\n\n.helpMenuTitle {\n  font-size: 18px !important;\n}\n\n.helpMenuItem {\n  align-items: center;\n  cursor: pointer;\n\n  :global(.euiButtonIcon),\n  :global(svg) {\n    color: var(--euiTooltipTextColor) !important;\n  }\n\n  .helpMenuItemLink {\n    text-decoration: none !important;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    transition: transform 0.3s ease;\n    color: var(--externalLinkColor) !important;\n\n    &:hover {\n      background: none !important;\n      transform: translateY(-1px);\n    }\n\n    &:global(.euiLink) {\n      text-decoration: none !important;\n      &:focus {\n        animation: none !important;\n      }\n    }\n  }\n}\n\n.helpMenuItemRow {\n  .helpMenuItemLink {\n    &:not(:last-child) {\n      margin-bottom: 16px;\n    }\n\n    .helpMenuTextLink {\n      font-size: 13px !important;\n      line-height: 16px !important;\n      text-decoration: none !important;\n      cursor: pointer;\n      padding: 0 !important;\n      color: var(--externalLinkColor) !important;\n\n      &:hover {\n        background: none !important;\n        color: var(--externalLinkColor) !important;\n        text-decoration: underline !important;\n      }\n    }\n  }\n}\n\n.helpMenuItemDisabled {\n  cursor: auto;\n  :global(svg),\n  div {\n    color: var(--buttonSecondaryDisabledTextColor) !important;\n  }\n}\n\n.helpMenuItemNotified {\n  position: relative;\n  &:before {\n    content: '';\n    position: absolute;\n    right: 6px;\n    top: -3px;\n    display: block;\n    width: 6px;\n    height: 6px;\n    background-color: var(--euiColorPrimary);\n    border-radius: 100%;\n  }\n}\n\n.helpMenuText {\n  font-size: 13px !important;\n  line-height: 1.35 !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/Notification/Notification.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { format } from 'date-fns'\nimport parse from 'html-react-parser'\n\nimport { NOTIFICATION_DATE_FORMAT } from 'uiSrc/constants/notifications'\nimport { IGlobalNotification } from 'uiSrc/slices/interfaces'\nimport { truncateText } from 'uiSrc/utils'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { TitleSize, Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\n\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  notification: IGlobalNotification\n  titleSize?: TitleSize\n}\n\nconst Notification = (props: Props) => {\n  const { notification, titleSize = 'XS' } = props\n\n  return (\n    <>\n      <Title\n        size={titleSize}\n        className={styles.notificationTitle}\n        data-testid=\"notification-title\"\n      >\n        {notification.title}\n      </Title>\n      <Spacer size=\"s\" />\n      <Text\n        component=\"div\"\n        size=\"s\"\n        className={cx('notificationHTMLBody', styles.notificationBody)}\n        data-testid=\"notification-body\"\n      >\n        {parse(notification.body)}\n      </Text>\n\n      <Row className={styles.notificationFooter} align=\"center\" justify=\"start\">\n        <FlexItem>\n          <Text size=\"xs\" data-testid=\"notification-date\">\n            {format(notification.timestamp * 1000, NOTIFICATION_DATE_FORMAT)}\n          </Text>\n        </FlexItem>\n        {notification.category && (\n          <FlexItem>\n            <RiBadge\n              variant=\"light\"\n              className={styles.category}\n              style={{ backgroundColor: notification.categoryColor ?? '#666' }}\n              data-testid=\"notification-category\"\n              label={truncateText(notification.category, 32)}\n            />\n          </FlexItem>\n        )}\n      </Row>\n    </>\n  )\n}\n\nexport default Notification\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/Notification/index.ts",
    "content": "import Notification from './Notification'\n\nexport default Notification\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationCenter.spec.tsx",
    "content": "import { within } from '@testing-library/react'\nimport { format } from 'date-fns'\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { NOTIFICATION_DATE_FORMAT } from 'uiSrc/constants/notifications'\nimport { getNotifications } from 'uiSrc/slices/app/notifications'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport NotificationCenter from './NotificationCenter'\n\nconst notificationsMock = [\n  {\n    type: 'global',\n    timestamp: 123123125,\n    title: 'string',\n    body: 'string',\n    read: false,\n  },\n  {\n    type: 'global',\n    timestamp: 123123124,\n    title: 'string1',\n    body: 'string1',\n    read: false,\n  },\n  {\n    type: 'global',\n    timestamp: 123123121,\n    title: 'string2',\n    body: 'string2',\n    read: true,\n  },\n]\n\njest.mock('uiSrc/slices/app/notifications', () => ({\n  ...jest.requireActual('uiSrc/slices/app/notifications'),\n  notificationCenterSelector: jest.fn().mockReturnValue({\n    isCenterOpen: true,\n    notifications: notificationsMock,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('NotificationCenter', () => {\n  it('should render', () => {\n    expect(render(<NotificationCenter />)).toBeTruthy()\n  })\n\n  it('should render notifications', async () => {\n    render(<NotificationCenter />)\n\n    expect(screen.getAllByTestId(/notification-item-/)).toHaveLength(\n      notificationsMock.length,\n    )\n  })\n\n  it('should dispatch get notification', async () => {\n    render(<NotificationCenter />)\n\n    const expectedActions = [getNotifications()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render proper unread notifications', async () => {\n    render(<NotificationCenter />)\n\n    expect(screen.getAllByTestId(/notification-item-unread/)).toHaveLength(\n      notificationsMock.filter((n) => !n.read).length,\n    )\n  })\n\n  it('should render proper read notifications', async () => {\n    render(<NotificationCenter />)\n\n    expect(screen.getAllByTestId(/notification-item-read/)).toHaveLength(\n      notificationsMock.filter((n) => n.read).length,\n    )\n  })\n\n  it('should render proper notification content', async () => {\n    render(<NotificationCenter />)\n\n    notificationsMock.forEach((notification) => {\n      const notificationContainer = screen.getByTestId(\n        new RegExp(`notification-item-(.*)_${notification.timestamp}`),\n      )\n\n      expect(\n        within(notificationContainer).getByTestId('notification-title'),\n      ).toHaveTextContent(notification.title)\n\n      expect(\n        within(notificationContainer).getByTestId('notification-date'),\n      ).toHaveTextContent(\n        format(notification.timestamp * 1000, NOTIFICATION_DATE_FORMAT),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationCenter.tsx",
    "content": "import cx from 'classnames'\nimport React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  fetchNotificationsAction,\n  notificationCenterSelector,\n  setIsCenterOpen,\n  unreadNotificationsAction,\n} from 'uiSrc/slices/app/notifications'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport Notification from './Notification'\n\nimport styles from './styles.module.scss'\n\nconst NotificationCenter = () => {\n  const { isCenterOpen, notifications } = useSelector(\n    notificationCenterSelector,\n  )\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (isCenterOpen) {\n      dispatch(\n        fetchNotificationsAction((totalUnread, length) => {\n          totalUnread && dispatch(unreadNotificationsAction())\n\n          sendEventTelemetry({\n            event: TelemetryEvent.NOTIFICATIONS_HISTORY_OPENED,\n            eventData: {\n              notifications: length,\n              unreadNotifications: totalUnread,\n            },\n          })\n        }),\n      )\n    }\n  }, [isCenterOpen])\n\n  const hasNotifications = !!notifications?.length\n\n  return (\n    <RiPopover\n      anchorPosition=\"rightUp\"\n      isOpen={isCenterOpen}\n      panelClassName={cx('popoverLikeTooltip', styles.popoverCenterWrapper)}\n      anchorClassName={styles.popoverAnchor}\n      closePopover={() => dispatch(setIsCenterOpen(false))}\n      button={<div className={styles.popoverAnchor} />}\n    >\n      <div\n        className={styles.popoverNotificationCenter}\n        data-testid=\"notification-center\"\n      >\n        <Title size=\"S\" className={styles.title}>\n          Notification Center\n        </Title>\n        {!hasNotifications && (\n          <div className={styles.noItemsText}>\n            <Text color=\"subdued\" data-testid=\"no-notifications-text\">\n              No notifications to display.\n            </Text>\n          </div>\n        )}\n        {hasNotifications && (\n          <div\n            className={styles.notificationsList}\n            data-testid=\"notifications-list\"\n          >\n            {notifications.map((notification) => (\n              <div\n                key={notification.timestamp}\n                className={cx(styles.notificationItem, {\n                  [styles.unread]: !notification.read,\n                })}\n                data-testid={`notification-item-${notification.read ? 'read' : 'unread'}_${notification.timestamp}`}\n              >\n                <Notification notification={notification} titleSize=\"XS\" />\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default NotificationCenter\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.spec.tsx",
    "content": "import { fireEvent } from '@testing-library/react'\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\nimport {\n  notificationCenterSelector,\n  setIsCenterOpen,\n} from 'uiSrc/slices/app/notifications'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport { SideBar } from 'uiSrc/components/base/layout/sidebar'\nimport NotificationMenu from './NotificationMenu'\n\njest.mock('uiSrc/slices/app/notifications', () => ({\n  ...jest.requireActual('uiSrc/slices/app/notifications'),\n  notificationCenterSelector: jest.fn().mockReturnValue({\n    notifications: [],\n    totalUnread: 1,\n    isCenterOpen: false,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst sideBarWithNotificationMenu = (\n  <SideBar isExpanded={false}>\n    <NotificationMenu />\n  </SideBar>\n)\n\ndescribe('NotificationMenu', () => {\n  it('should render', () => {\n    expect(render(sideBarWithNotificationMenu)).toBeTruthy()\n  })\n\n  it('should open notification center onClick icon', async () => {\n    render(sideBarWithNotificationMenu)\n\n    fireEvent.mouseDown(screen.getByTestId('notification-menu-button'))\n\n    const expectedActions = [setIsCenterOpen()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should show badge with count of unread messages', async () => {\n    render(sideBarWithNotificationMenu)\n\n    expect(screen.getByTestId('total-unread-badge')).toBeInTheDocument()\n    expect(screen.getByTestId('total-unread-badge')).toHaveTextContent('1')\n  })\n\n  it('should show badge with count 9+ of unread messages', async () => {\n    ;(notificationCenterSelector as jest.Mock).mockReturnValueOnce({\n      notifications: [],\n      totalUnread: 13,\n      isCenterOpen: false,\n    })\n    render(sideBarWithNotificationMenu)\n\n    expect(screen.getByTestId('total-unread-badge')).toHaveTextContent('9+')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  notificationCenterSelector,\n  setIsCenterOpen,\n} from 'uiSrc/slices/app/notifications'\nimport { NotificationsIcon } from 'uiSrc/components/base/icons'\nimport {\n  SideBarItem,\n  SideBarItemIcon,\n} from 'uiSrc/components/base/layout/sidebar'\nimport NotificationCenter from './NotificationCenter'\nimport PopoverNotification from './PopoverNotification'\n\nimport styles from './styles.module.scss'\n\nconst NavButton = () => {\n  const { isCenterOpen, totalUnread } = useSelector(notificationCenterSelector)\n\n  const dispatch = useDispatch()\n\n  const onClickIcon = () => {\n    dispatch(setIsCenterOpen())\n  }\n\n  const Btn = (\n    <SideBarItem\n      tooltipProps={{ text: 'Notification Center', placement: 'right' }}\n      onMouseDownCapture={onClickIcon}\n      isActive={isCenterOpen}\n    >\n      <SideBarItemIcon\n        icon={NotificationsIcon}\n        aria-label=\"Notification Menu\"\n        data-testid=\"notification-menu-button\"\n      />\n    </SideBarItem>\n  )\n\n  return (\n    <>\n      {Btn}\n      {totalUnread > 0 && !isCenterOpen && (\n        <div\n          className={styles.badgeUnreadCount}\n          data-testid=\"total-unread-badge\"\n        >\n          {totalUnread > 9 ? '9+' : totalUnread}\n        </div>\n      )}\n    </>\n  )\n}\n\nconst NotificationMenu = () => (\n  <div className={styles.wrapper} data-testid=\"notification-menu\">\n    <NavButton />\n    <NotificationCenter />\n    <PopoverNotification />\n  </div>\n)\n\nexport default NotificationMenu\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/PopoverNotification/PopoverNotification.spec.tsx",
    "content": "import { fireEvent } from '@testing-library/react'\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { setIsNotificationOpen } from 'uiSrc/slices/app/notifications'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport PopoverNotification from './PopoverNotification'\n\njest.mock('uiSrc/slices/app/notifications', () => ({\n  ...jest.requireActual('uiSrc/slices/app/notifications'),\n  notificationCenterSelector: jest.fn().mockReturnValue({\n    isNotificationOpen: true,\n    isCenterOpen: false,\n    lastReceivedNotification: {\n      timestamp: 123123125,\n      title: 'string',\n      body: 'string',\n      read: false,\n    },\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('PopoverNotification', () => {\n  it('should render', () => {\n    expect(render(<PopoverNotification />)).toBeTruthy()\n  })\n\n  it('should show notification message', async () => {\n    render(<PopoverNotification />)\n\n    expect(screen.getByTestId('notification-popover')).toBeInTheDocument()\n  })\n\n  it('should close notification after click close btn', async () => {\n    render(<PopoverNotification />)\n\n    fireEvent.click(screen.getByTestId('close-notification-btn'))\n\n    const expectedActions = [setIsNotificationOpen(false)]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/PopoverNotification/PopoverNotification.tsx",
    "content": "import cx from 'classnames'\nimport React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  notificationCenterSelector,\n  setIsCenterOpen,\n  setIsNotificationOpen,\n  unreadNotificationsAction,\n} from 'uiSrc/slices/app/notifications'\nimport { IGlobalNotification } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { RiPopover } from 'uiSrc/components/base'\nimport Notification from '../Notification'\n\nimport styles from '../styles.module.scss'\n\nconst CLOSE_NOTIFICATION_TIME = 6000\n\nconst PopoverNotification = () => {\n  const { isNotificationOpen, isCenterOpen, lastReceivedNotification } =\n    useSelector(notificationCenterSelector)\n  const [isHovering, setIsHovering] = useState(false)\n  const [isShowNotification, setIsShowNotification] =\n    useState(isNotificationOpen)\n\n  const timeOutRef = useRef<NodeJS.Timeout>()\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (isShowNotification && isCenterOpen) {\n      onCloseNotification()\n      return\n    }\n\n    // if notification center is opened - wait closing and show notification\n    if (isNotificationOpen && !isCenterOpen) {\n      setTimeout(() => {\n        setIsShowNotification(isNotificationOpen)\n      }, 300)\n    }\n  }, [isNotificationOpen, isCenterOpen])\n\n  useEffect(() => {\n    if (isShowNotification) {\n      if (isHovering) {\n        timeOutRef.current && clearTimeout(timeOutRef.current)\n        return\n      }\n\n      timeOutRef.current = setTimeout(\n        onCloseNotification,\n        CLOSE_NOTIFICATION_TIME,\n      )\n    }\n  }, [isShowNotification, isHovering])\n\n  const onCloseNotification = () => {\n    setIsShowNotification(false)\n    dispatch(setIsNotificationOpen(false))\n  }\n\n  const handleClickClose = (notification: IGlobalNotification) => {\n    onCloseNotification()\n    dispatch(\n      unreadNotificationsAction({\n        timestamp: notification.timestamp,\n        type: notification.type,\n      }),\n    )\n\n    sendEventTelemetry({\n      event: TelemetryEvent.NOTIFICATIONS_MESSAGE_CLOSED,\n      eventData: {\n        notificationID: lastReceivedNotification?.timestamp,\n      },\n    })\n  }\n\n  const onMouseUpPopover = () => {\n    if (!window.getSelection()?.toString()) {\n      dispatch(setIsCenterOpen())\n    }\n  }\n\n  return (\n    <>\n      {lastReceivedNotification && (\n        <RiPopover\n          anchorPosition=\"rightUp\"\n          isOpen={isShowNotification}\n          closePopover={() => {}}\n          anchorClassName={styles.popoverAnchor}\n          panelClassName={cx(\n            'popoverLikeTooltip',\n            styles.popoverNotificationTooltip,\n          )}\n          button={<div className={styles.popoverAnchor} />}\n          onMouseUp={onMouseUpPopover}\n        >\n          <div\n            onMouseEnter={() => setIsHovering(true)}\n            onMouseLeave={() => setIsHovering(false)}\n            className={styles.popoverNotification}\n            data-testid=\"notification-popover\"\n          >\n            <IconButton\n              icon={CancelSlimIcon}\n              aria-label=\"Close notification\"\n              className={styles.closeBtn}\n              onMouseUp={(e: React.MouseEvent) => e.stopPropagation()}\n              onClick={() => handleClickClose(lastReceivedNotification)}\n              data-testid=\"close-notification-btn\"\n            />\n            <Notification notification={lastReceivedNotification} />\n          </div>\n        </RiPopover>\n      )}\n    </>\n  )\n}\n\nexport default PopoverNotification\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/PopoverNotification/index.ts",
    "content": "import PopoverNotification from './PopoverNotification'\n\nexport default PopoverNotification\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/index.ts",
    "content": "import NotificationMenu from './NotificationMenu'\n\nexport default NotificationMenu\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/notifications-center/styles.module.scss",
    "content": ".wrapper {\n  position: relative;\n  display: flex;\n\n  .badgeUnreadCount {\n    position: absolute;\n    top: 8px;\n    right: 10px;\n    width: 16px;\n    height: 16px;\n    border-radius: 22px;\n    background: #8BA2FF;\n    text-align: center;\n    line-height: 15px;\n    font-size: 10px;\n    color: #000;\n  }\n}\n\n.popoverNotificationTooltip {\n  min-width: 420px !important;\n}\n\n.popoverCenterWrapper {\n  min-width: 420px !important;\n\n  .noItemsText {\n    min-height: 200px;\n    display: flex;\n    align-items: center;\n    justify-items: center;\n    align-self: center;\n  }\n}\n\n.popoverNotificationCenter {\n  display: flex;\n  flex-direction: column;\n  .title {\n    font-size: 18px;\n    font-weight: 500;\n    padding: 6px 15px 0;\n  }\n}\n\n.notificationsList {\n  margin-top: 18px;\n  max-height: 70vh;\n  @include eui.scrollBar;\n  overflow: auto;\n  padding: 0 15px 0;\n  margin-bottom: 12px;\n\n  .notificationItem {\n    position: relative;\n    &.unread {\n      &:before {\n        display: block;\n        content: '';\n        width: 8px;\n        height: 8px;\n        border-radius: 50%;\n        background-color: var(--euiColorPrimary);\n        position: absolute;\n        top: 8px;\n        left: -14px;\n      }\n    }\n  }\n\n  .notificationItem:not(:last-child) {\n    margin-bottom: 24px;\n  }\n\n  .notificationTitle {\n    margin-bottom: 6px;\n    font-weight: normal;\n    display: inline-block;\n  }\n}\n\n.notificationFooter {\n  margin-top: 6px;\n}\n\n.category {\n  margin-left: 12px;\n  border: 0;\n\n  color: #DFE5EF !important;\n\n  &:global(.euiBadge) {\n    background-color: #666;\n  }\n}\n\n.popoverNotification {\n  padding: 6px 15px;\n  position: relative;\n\n  .closeBtn {\n    position: absolute;\n    top: 8px;\n    right: 8px;\n  }\n\n  .notificationTitle {\n    display: block;\n    margin-right: 30px;\n    margin-bottom: 12px;\n    font:\n      normal normal 500 18px/24px Graphik,\n      sans-serif;\n  }\n\n  .notificationDate {\n    margin-top: 6px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/redis-logo/RedisLogo.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { SideBar } from 'uiSrc/components/base/layout/sidebar'\nimport { RedisLogo } from './RedisLogo'\n\nbeforeEach(() => {\n  cleanup()\n  jest.clearAllMocks()\n})\n\ndescribe('RedisLogo', () => {\n  it('should have link if envDependent feature is on', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n    render(\n      <SideBar isExpanded={false}>\n        <RedisLogo isRdiWorkspace={false} />\n      </SideBar>,\n      {\n        store: mockStore(initialStoreState),\n      },\n    )\n\n    expect(screen.getByTestId('redis-logo-link')).toBeInTheDocument()\n  })\n\n  it('should not have link if envDependent feature is off', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    render(\n      <SideBar isExpanded={false}>\n        <RedisLogo isRdiWorkspace={false} />\n      </SideBar>,\n      {\n        store: mockStore(initialStoreState),\n      },\n    )\n\n    expect(screen.queryByTestId('redis-logo-link')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/components/redis-logo/RedisLogo.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { BuildType } from 'uiSrc/constants/env'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  SideBarItem,\n  SideBarItemIcon,\n} from 'uiSrc/components/base/layout/sidebar'\nimport { getRouterLinkProps } from 'uiSrc/services'\nimport { Pages } from 'uiSrc/constants'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { RedisLogoDarkMinIcon } from 'uiSrc/components/base/icons'\nimport styled from 'styled-components'\n\ntype Props = {\n  isRdiWorkspace: boolean\n}\n\nconst RedisLogoIcon = styled.span`\n  height: 60px;\n  width: 100%;\n  @media only screen and (min-width: 768px) {\n    height: 72px;\n  }\n  svg {\n    width: 30px;\n    height: 34px;\n  }\n`\n\nexport const RedisLogo = ({ isRdiWorkspace }: Props) => {\n  const { envDependent } = useSelector(appFeatureFlagsFeaturesSelector)\n  const { server } = useSelector(appInfoSelector)\n\n  if (!envDependent?.flag) {\n    return (\n      <RedisLogoIcon>\n        <SideBarItemIcon\n          height=\"50px\"\n          width=\"50px\"\n          aria-label=\"Redis Insight Homepage\"\n          icon={RedisLogoDarkMinIcon}\n          centered\n        />\n      </RedisLogoIcon>\n    )\n  }\n\n  return (\n    <Link\n      {...getRouterLinkProps(isRdiWorkspace ? Pages.rdi : Pages.home)}\n      data-testid=\"redis-logo-link\"\n      style={{ backgroundColor: 'transparent' }}\n    >\n      <SideBarItem\n        tooltipProps={{\n          text:\n            server?.buildType === BuildType.RedisStack\n              ? 'Edit database'\n              : isRdiWorkspace\n                ? 'Redis Data Integration'\n                : 'Redis Databases',\n          placement: 'right',\n        }}\n        style={{ marginBlock: '2rem', marginInline: 'auto' }}\n      >\n        <SideBarItemIcon icon={RedisLogoDarkMinIcon} />\n      </SideBarItem>\n    </Link>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/hooks/useNavigation.ts",
    "content": "import { useHistory, useLocation } from 'react-router-dom'\nimport { last } from 'lodash'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { useEffect, useState } from 'react'\nimport { Props as HighlightedFeatureProps } from 'uiSrc/components/hightlighted-feature/HighlightedFeature'\nimport { ANALYTICS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes'\nimport {\n  appFeatureFlagsFeaturesSelector,\n  appFeaturePagesHighlightingSelector,\n  removeFeatureFromHighlighting,\n} from 'uiSrc/slices/app/features'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { connectedInstanceSelector as connectedRdiInstanceSelector } from 'uiSrc/slices/rdi/instances'\n\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting'\nimport { Pages, FeatureFlags, PageNames } from 'uiSrc/constants'\n\nimport { appContextSelector } from 'uiSrc/slices/app/context'\nimport { AppWorkspace } from 'uiSrc/slices/interfaces'\nimport {\n  BrowserIcon,\n  PipelineManagementIcon,\n  PipelineStatisticsIcon,\n  PubSubIcon,\n  SlowLogIcon,\n  WorkbenchIcon,\n  SettingsIcon,\n} from 'uiSrc/components/base/icons'\nimport { INavigations } from '../navigation.types'\n\nconst pubSubPath = `/${PageNames.pubSub}`\n\nexport function useNavigation() {\n  const history = useHistory()\n  const location = useLocation()\n  const dispatch = useDispatch()\n\n  const [activePage, setActivePage] = useState(Pages.home)\n\n  const { workspace } = useSelector(appContextSelector)\n\n  const { id: connectedInstanceId = '' } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { id: connectedRdiInstanceId = '' } = useSelector(\n    connectedRdiInstanceSelector,\n  )\n  const highlightedPages = useSelector(appFeaturePagesHighlightingSelector)\n  const { [FeatureFlags.vectorSearchV2]: vectorSearchFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const isRdiWorkspace = workspace === AppWorkspace.RDI\n\n  useEffect(() => {\n    setActivePage(`/${last(location.pathname.split('/'))}`)\n  }, [location])\n\n  const handleGoPage = (page: string) => history.push(page)\n\n  const isAnalyticsPath = (activePage: string) =>\n    !!ANALYTICS_ROUTES.find(\n      ({ path }) => `/${last(path.split('/'))}` === activePage,\n    )\n\n  const isPipelineManagementPath = () =>\n    location.pathname?.startsWith(\n      Pages.rdiPipelineManagement(connectedRdiInstanceId),\n    )\n\n  const isVectorSearchPath = () =>\n    location.pathname.split('/')[2] === PageNames.vectorSearch\n\n  const getAdditionPropsForHighlighting = (\n    pageName: string,\n  ): Omit<HighlightedFeatureProps, 'children'> => {\n    if (BUILD_FEATURES[pageName]?.asPageFeature) {\n      return {\n        hideFirstChild: true,\n        onClick: () => dispatch(removeFeatureFromHighlighting(pageName)),\n        ...BUILD_FEATURES[pageName],\n      }\n    }\n\n    return {}\n  }\n\n  const privateRoutes: INavigations[] = [\n    {\n      tooltipText: 'Browse',\n      pageName: PageNames.browser,\n      isActivePage: activePage === `/${PageNames.browser}`,\n      ariaLabel: 'Browser page button',\n      onClick: () => handleGoPage(Pages.browser(connectedInstanceId)),\n      dataTestId: 'browser-page-btn',\n      connectedInstanceId,\n      iconType: BrowserIcon,\n      onboard: ONBOARDING_FEATURES.BROWSER_PAGE,\n    },\n    vectorSearchFeature?.flag && {\n      tooltipText: 'Search',\n      pageName: PageNames.vectorSearch,\n      ariaLabel: 'Search',\n      onClick: () => handleGoPage(Pages.vectorSearch(connectedInstanceId)),\n      dataTestId: 'vector-search-page-btn',\n      connectedInstanceId,\n      isActivePage: isVectorSearchPath(),\n      iconType: SlowLogIcon,\n      onboard: ONBOARDING_FEATURES.VECTOR_SEARCH_PAGE,\n    },\n    {\n      tooltipText: 'Workbench',\n      pageName: PageNames.workbench,\n      ariaLabel: 'Workbench page button',\n      onClick: () => handleGoPage(Pages.workbench(connectedInstanceId)),\n      dataTestId: 'workbench-page-btn',\n      connectedInstanceId,\n      isActivePage: activePage === `/${PageNames.workbench}`,\n      iconType: WorkbenchIcon,\n      onboard: ONBOARDING_FEATURES.WORKBENCH_PAGE,\n    },\n    {\n      tooltipText: 'Analyze',\n      pageName: PageNames.analytics,\n      ariaLabel: 'Analyze page button',\n      onClick: () => handleGoPage(Pages.analytics(connectedInstanceId)),\n      dataTestId: 'analytics-page-btn',\n      connectedInstanceId,\n      isActivePage: isAnalyticsPath(activePage),\n      iconType: SlowLogIcon,\n      featureFlag: FeatureFlags.envDependent,\n    },\n    {\n      tooltipText: 'Pub/Sub',\n      pageName: PageNames.pubSub,\n      ariaLabel: 'Pub/Sub page button',\n      onClick: () => handleGoPage(Pages.pubSub(connectedInstanceId)),\n      dataTestId: 'pub-sub-page-btn',\n      connectedInstanceId,\n      isActivePage: activePage === pubSubPath,\n      iconType: PubSubIcon,\n      onboard: ONBOARDING_FEATURES.PUB_SUB_PAGE,\n      featureFlag: FeatureFlags.envDependent,\n    },\n  ].filter((tab) => !!tab) as INavigations[]\n\n  const privateRdiRoutes: INavigations[] = [\n    {\n      tooltipText: 'Pipeline',\n      pageName: PageNames.rdiPipelineManagement,\n      ariaLabel: 'Pipeline Management page button',\n      onClick: () =>\n        handleGoPage(Pages.rdiPipelineManagement(connectedRdiInstanceId)),\n      dataTestId: 'pipeline-management-page-btn',\n      isActivePage: isPipelineManagementPath(),\n      iconType: PipelineManagementIcon,\n    },\n    {\n      tooltipText: 'Analytics',\n      pageName: PageNames.rdiStatistics,\n      ariaLabel: 'Pipeline Status page button',\n      onClick: () => handleGoPage(Pages.rdiStatistics(connectedRdiInstanceId)),\n      dataTestId: 'pipeline-status-page-btn',\n      isActivePage: activePage === `/${PageNames.rdiStatistics}`,\n      iconType: PipelineStatisticsIcon,\n    },\n  ]\n\n  const publicRoutes: INavigations[] = [\n    {\n      tooltipText: 'Settings',\n      pageName: PageNames.settings,\n      ariaLabel: 'Settings page button',\n      onClick: () => handleGoPage(Pages.settings),\n      dataTestId: 'settings-page-btn',\n      isActivePage: activePage === Pages.settings,\n      iconType: SettingsIcon,\n      featureFlag: FeatureFlags.envDependent,\n    },\n  ]\n\n  return {\n    isRdiWorkspace,\n    privateRoutes,\n    privateRdiRoutes,\n    publicRoutes,\n    getAdditionPropsForHighlighting,\n    highlightedPages,\n    activePage,\n    setActivePage,\n    handleGoPage,\n    connectedInstanceId,\n    connectedRdiInstanceId,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/navigation.types.ts",
    "content": "import { IconType } from 'uiSrc/components/base/forms/buttons'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { OnboardingTourOptions } from '../onboarding-tour'\n\nexport interface INavigations {\n  isActivePage: boolean\n  isBeta?: boolean\n  pageName: string\n  tooltipText: string\n  ariaLabel: string\n  dataTestId: string\n  connectedInstanceId?: string\n  onClick: () => void\n  iconType: IconType\n  onboard?: OnboardingTourOptions\n  featureFlag?: FeatureFlags\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/navigation-menu/styles.module.scss",
    "content": ".mainNavbar {\n  display: flex;\n  justify-content: space-between;\n  flex-direction: column;\n}\n\n.navigationButtonWrapper {\n  position: relative;\n  \n  .betaLabel {\n    position: absolute;\n    bottom: -4px;\n    left: 50%;\n    transform: translateX(-50%) translateY(0);\n\n    font-size: 8px !important;\n    line-height: 12px !important;\n    background-color: var(--recommendationLiveBorderColor) !important;\n    border: 1px solid var(--triggerIconActiveColor) !important;\n    color: #FFF7EA !important;\n    border-radius: 2px !important;\n\n    transition: transform 250ms ease-in-out;\n    pointer-events: none;\n\n    :global([class*='RedisUI']) {\n      min-height: 12px !important;\n    }\n  }\n\n  &:hover {\n    .betaLabel {\n      transform: translateX(-50%) translateY(-1px);\n    }\n  }\n}\n\n.footer {\n  margin-bottom: 1rem;\n}\n\n.highlightDot {\n  top: 11px !important;\n  right: 11px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/Notifications.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport Notifications from './Notifications'\n\njest.mock('uiSrc/slices/app/notifications', () => ({\n  ...jest.requireActual('uiSrc/slices/app/notifications'),\n  messagesSelector: jest.fn().mockReturnValue([\n    {\n      id: '1',\n      title: 'Header text',\n      message: 'Body text',\n    },\n  ]),\n  errorsSelector: jest.fn().mockReturnValue([\n    {\n      id: '2',\n      message: 'Body text',\n    },\n  ]),\n  removeMessage: jest.fn,\n}))\n\ndescribe('Notifications', () => {\n  it('should render', () => {\n    expect(render(<Notifications />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/Notifications.stories.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport Notifications from './Notifications'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { CloudJobStep } from 'uiSrc/electron/constants'\nimport { useDispatch } from 'react-redux'\nimport {\n  addInfiniteNotification,\n  removeInfiniteNotification,\n  addErrorNotification,\n  IAddInstanceErrorPayload,\n} from 'uiSrc/slices/app/notifications'\nimport { InfiniteMessage } from 'uiSrc/slices/interfaces'\nimport { fn } from 'storybook/test'\n\nconst meta: Meta<typeof Notifications> = {\n  component: Notifications,\n  decorators: [\n    (Story) => {\n      useNotificationUpdates()\n\n      return <Story />\n    },\n  ],\n}\n/* Captured some logs of sequence of notifications for testing purposes, simulated here with setTimeout */\ntype SampleNotification =\n  | { ts: number; type: 'add'; nf: InfiniteMessage }\n  | { ts: number; type: 'rm'; nf: string }\n  | { ts: number; type: 'error'; error: IAddInstanceErrorPayload }\n\nconst sampleNotifications: SampleNotification[] = [\n  {\n    ts: 0,\n    type: 'add',\n    nf: INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n  },\n  { ts: 20, type: 'add', nf: INFINITE_MESSAGES.AUTHENTICATING() },\n  {\n    ts: 1500,\n    type: 'add',\n    nf: INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Subscription),\n  },\n  {\n    ts: 2900,\n    type: 'add',\n    nf: INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(fn(), fn()),\n  },\n  { ts: 1, type: 'rm', nf: InfiniteMessagesIds.oAuthProgress },\n  {\n    ts: 4000,\n    type: 'add',\n    nf: INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(fn(), fn()),\n  },\n  { ts: 2, type: 'rm', nf: InfiniteMessagesIds.oAuthProgress },\n  { ts: 9000, type: 'rm', nf: InfiniteMessagesIds.subscriptionExists },\n  { ts: 10000, type: 'rm', nf: InfiniteMessagesIds.subscriptionExists },\n  {\n    ts: 1,\n    type: 'add',\n    nf: INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n  },\n  { ts: 4000, type: 'rm', nf: InfiniteMessagesIds.subscriptionExists },\n  {\n    ts: 5000,\n    type: 'error',\n    error: {\n      message: 'Something went wrong',\n      response: {\n        data: {\n          message: 'An unexpected error occurred',\n          title: 'Error',\n        },\n        status: 500,\n        statusText: 'Internal Server Error',\n        headers: {},\n        config: {} as any,\n      },\n    } as IAddInstanceErrorPayload,\n  },\n  { ts: 13000, type: 'rm', nf: InfiniteMessagesIds.oAuthProgress },\n]\n\nconst useNotificationUpdates = () => {\n  const dispatch = useDispatch()\n  const timeoutRefs = useRef<NodeJS.Timeout[]>([])\n\n  useEffect(() => {\n    let cumulativeTime = 0\n\n    sampleNotifications.forEach((notification) => {\n      cumulativeTime += notification.ts\n\n      const timeoutId = setTimeout(() => {\n        if (notification.type === 'add') {\n          dispatch(addInfiniteNotification(notification.nf))\n        } else if (notification.type === 'rm') {\n          dispatch(removeInfiniteNotification(notification.nf))\n        } else if (notification.type === 'error') {\n          dispatch(addErrorNotification(notification.error))\n        }\n      }, cumulativeTime)\n\n      timeoutRefs.current.push(timeoutId)\n    })\n\n    return () => {\n      timeoutRefs.current.forEach((timeoutId) => {\n        clearTimeout(timeoutId)\n      })\n      timeoutRefs.current = []\n    }\n  }, [dispatch])\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/Notifications.tsx",
    "content": "import React from 'react'\nimport { RiToaster } from 'uiSrc/components/base/display/toast'\nimport { useErrorNotifications, useMessageNotifications } from './hooks'\nimport { useInfiniteNotifications } from './hooks/useInfiniteNotifications'\nimport { defaultContainerId } from './constants'\n\nconst Notifications = () => {\n  useErrorNotifications()\n  useMessageNotifications()\n  useInfiniteNotifications()\n  return <RiToaster containerId={defaultContainerId} />\n}\n\nexport default Notifications\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/azure-token-expired/AzureTokenExpiredErrorContent.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent, cleanup } from 'uiSrc/utils/test-utils'\n\nimport AzureTokenExpiredErrorContent from './AzureTokenExpiredErrorContent'\n\nconst mockInitiateLogin = jest.fn()\njest.mock('uiSrc/components/hooks/useAzureAuth', () => ({\n  useAzureAuth: () => ({\n    initiateLogin: mockInitiateLogin,\n    loading: false,\n  }),\n}))\n\ndescribe('AzureTokenExpiredErrorContent', () => {\n  beforeEach(() => {\n    cleanup()\n    jest.clearAllMocks()\n  })\n\n  it('should render error text and sign in button', () => {\n    render(<AzureTokenExpiredErrorContent text=\"Token has expired\" />)\n\n    expect(screen.getByText('Token has expired')).toBeInTheDocument()\n    expect(screen.getByTestId('azure-sign-in-btn')).toBeInTheDocument()\n    expect(screen.getByTestId('azure-sign-in-btn')).toHaveTextContent(\n      'Sign in to Azure',\n    )\n  })\n\n  it('should call initiateLogin and onClose when sign in button is clicked', () => {\n    const onClose = jest.fn()\n    render(\n      <AzureTokenExpiredErrorContent\n        text=\"Token has expired\"\n        onClose={onClose}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('azure-sign-in-btn'))\n\n    expect(mockInitiateLogin).toHaveBeenCalled()\n    expect(onClose).toHaveBeenCalled()\n  })\n\n  it('should work without onClose callback', () => {\n    render(<AzureTokenExpiredErrorContent text=\"Token has expired\" />)\n\n    // Should not throw when clicking without onClose\n    expect(() => {\n      fireEvent.click(screen.getByTestId('azure-sign-in-btn'))\n    }).not.toThrow()\n\n    expect(mockInitiateLogin).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/azure-token-expired/AzureTokenExpiredErrorContent.tsx",
    "content": "import React from 'react'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Button } from 'uiSrc/components/base/forms/buttons'\nimport { useAzureAuth } from 'uiSrc/components/hooks/useAzureAuth'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\n\nexport interface Props {\n  text: string | JSX.Element | JSX.Element[]\n  onClose?: () => void\n}\n\nconst AzureTokenExpiredErrorContent = ({ text, onClose = () => {} }: Props) => {\n  const { initiateLogin, loading } = useAzureAuth()\n\n  const handleSignIn = () => {\n    initiateLogin(AzureLoginSource.TokenRefresh)\n    onClose?.()\n  }\n\n  return (\n    <>\n      <ColorText color=\"informative\">{text}</ColorText>\n      <Spacer />\n      <Row justify=\"end\">\n        <FlexItem>\n          <Button\n            size=\"s\"\n            onClick={handleSignIn}\n            loading={loading}\n            data-testid=\"azure-sign-in-btn\"\n          >\n            Sign in to Azure\n          </Button>\n        </FlexItem>\n      </Row>\n    </>\n  )\n}\n\nexport default AzureTokenExpiredErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/azure-token-expired/index.ts",
    "content": "export { default as AzureTokenExpiredErrorContent } from './AzureTokenExpiredErrorContent'\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/cloud-capi-unauthorized/CloudCapiUnAuthorizedErrorContent.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  act,\n} from 'uiSrc/utils/test-utils'\n\nimport { removeCapiKey } from 'uiSrc/slices/oauth/cloud'\nimport { apiService } from 'uiSrc/services'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\n\nimport CloudCapiUnAuthorizedErrorContent, {\n  Props,\n} from './CloudCapiUnAuthorizedErrorContent'\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockedProps = mock<Props>()\ndescribe('CloudCapiUnAuthorizedErrorContent', () => {\n  it('should render', () => {\n    expect(\n      render(<CloudCapiUnAuthorizedErrorContent {...mockedProps} />),\n    ).toBeTruthy()\n  })\n\n  it('should сall proper action on delete', () => {\n    const onClose = jest.fn()\n    render(\n      <CloudCapiUnAuthorizedErrorContent {...mockedProps} onClose={onClose} />,\n    )\n\n    fireEvent.click(screen.getByTestId('remove-api-key-btn'))\n\n    expect(store.getActions()).toEqual([removeCapiKey()])\n    expect(onClose).toBeCalled()\n  })\n\n  it('should сall proper history push on go to settings', () => {\n    const pushMock = jest.fn()\n    const onClose = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    render(\n      <CloudCapiUnAuthorizedErrorContent {...mockedProps} onClose={onClose} />,\n    )\n\n    fireEvent.click(screen.getByTestId('go-to-settings-btn'))\n\n    expect(pushMock).toBeCalledWith('/settings#cloud')\n    expect(onClose).toBeCalled()\n  })\n\n  it('should сall proper telemetry on delete', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 })\n\n    render(\n      <CloudCapiUnAuthorizedErrorContent {...mockedProps} resourceId=\"123\" />,\n    )\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('remove-api-key-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_API_KEY_REMOVED,\n      eventData: {\n        source: OAuthSocialSource.ConfirmationMessage,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/cloud-capi-unauthorized/CloudCapiUnAuthorizedErrorContent.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { removeCapiKeyAction } from 'uiSrc/slices/oauth/cloud'\nimport { Pages } from 'uiSrc/constants'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  DestructiveButton,\n  EmptyButton,\n} from 'uiSrc/components/base/forms/buttons'\n\nexport interface Props {\n  resourceId: string\n  text: string | JSX.Element | JSX.Element[]\n  onClose?: () => void\n}\n\nconst CloudCapiUnAuthorizedErrorContent = ({\n  text,\n  onClose = () => {},\n  resourceId,\n}: Props) => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const handleRemoveCapi = () => {\n    dispatch(\n      removeCapiKeyAction({ id: resourceId, name: 'Api Key' }, () => {\n        sendEventTelemetry({\n          event: TelemetryEvent.CLOUD_API_KEY_REMOVED,\n          eventData: {\n            source: OAuthSocialSource.ConfirmationMessage,\n          },\n        })\n      }),\n    )\n    onClose?.()\n  }\n\n  const handleGoToSettings = () => {\n    history.push(`${Pages.settings}#cloud`)\n    onClose?.()\n  }\n\n  return (\n    <>\n      <ColorText color=\"danger\">{text}</ColorText>\n      <Spacer />\n      <Row justify=\"end\">\n        <FlexItem>\n          <EmptyButton\n            variant=\"destructive\"\n            size=\"small\"\n            onClick={handleGoToSettings}\n            className=\"toast-danger-btn euiBorderWidthThick\"\n            data-testid=\"go-to-settings-btn\"\n          >\n            Go to Settings\n          </EmptyButton>\n        </FlexItem>\n        <FlexItem>\n          <DestructiveButton\n            size=\"s\"\n            onClick={handleRemoveCapi}\n            className=\"toast-danger-btn\"\n            data-testid=\"remove-api-key-btn\"\n          >\n            Remove API key\n          </DestructiveButton>\n        </FlexItem>\n      </Row>\n    </>\n  )\n}\n\nexport default CloudCapiUnAuthorizedErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/cloud-capi-unauthorized/index.ts",
    "content": "import CloudCapiUnAuthorizedErrorContent from './CloudCapiUnAuthorizedErrorContent'\n\nexport default CloudCapiUnAuthorizedErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport DefaultErrorContent, { Props } from './DefaultErrorContent'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('DefaultErrorContent', () => {\n  it('should render', () => {\n    expect(\n      render(<DefaultErrorContent {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/default-error-content/DefaultErrorContent.tsx",
    "content": "import React from 'react'\n\nimport { ColorText } from 'uiSrc/components/base/text'\n\nexport interface Props {\n  text: string | JSX.Element | JSX.Element[]\n}\n// TODO: use i18n file for texts\nconst DefaultErrorContent = ({ text }: Props) => (\n  <ColorText color=\"danger\">{text}</ColorText>\n)\n\nexport default DefaultErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/default-error-content/index.ts",
    "content": "import DefaultErrorContent from './DefaultErrorContent'\n\nexport default DefaultErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/encryption-error-content/EncryptionErrorContent.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  fireEvent,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport EncryptionErrorContent from './EncryptionErrorContent'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('EncryptionErrorContent', () => {\n  it('should render', () => {\n    expect(render(<EncryptionErrorContent />)).toBeTruthy()\n  })\n\n  it('should call onClose', () => {\n    const onClose = jest.fn()\n    render(<EncryptionErrorContent onClose={onClose} />)\n    fireEvent.click(screen.getByTestId('toast-action-btn'))\n\n    expect(onClose).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/encryption-error-content/EncryptionErrorContent.tsx",
    "content": "import React from 'react'\nimport { matchPath, useHistory, useLocation } from 'react-router-dom'\nimport { useDispatch } from 'react-redux'\nimport { Pages } from 'uiSrc/constants'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { updateUserConfigSettingsAction } from 'uiSrc/slices/user/user-settings'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  DestructiveButton,\n  EmptyButton,\n} from 'uiSrc/components/base/forms/buttons'\n\nexport interface Props {\n  onClose?: () => void\n  instanceId?: string\n}\n\n// TODO: use i18n file for texts\nconst EncryptionErrorContent = (props: Props) => {\n  const { onClose, instanceId } = props\n  const { pathname } = useLocation()\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  // useParams() hook can't be used because the Notifications component is outside of the MainRouter\n  const getInstanceIdFromUrl = (): string => {\n    const path = '/:instanceId/(browser|workbench)/'\n    const match: any = matchPath(pathname, { path })\n    return match?.params?.instanceId\n  }\n\n  const disableEncryption = () => {\n    const iId = instanceId || getInstanceIdFromUrl()\n    dispatch(\n      updateUserConfigSettingsAction({ agreements: { encryption: false } }),\n    )\n    if (instanceId) {\n      history.push(Pages.homeEditInstance(iId))\n    }\n    if (onClose) {\n      onClose()\n    }\n  }\n  return (\n    <>\n      <ColorText color=\"danger\">\n        <b>Check the system keychain or disable encryption to proceed.</b>\n      </ColorText>\n      <Spacer />\n      <ColorText color=\"danger\" style={{ fontWeight: 300 }}>\n        Disabling encryption will result in storing sensitive information\n        locally in plain text. Re-enter database connection information to work\n        with databases.\n      </ColorText>\n      <Spacer />\n      <Row justify=\"end\" gap=\"m\">\n        <FlexItem>\n          <div>\n            <DestructiveButton\n              onClick={disableEncryption}\n              className=\"toast-danger-btn euiBorderWidthThick\"\n              data-testid=\"toast-action-btn\"\n            >\n              Disable Encryption\n            </DestructiveButton>\n          </div>\n        </FlexItem>\n        <FlexItem>\n          <EmptyButton\n            variant=\"destructive\"\n            onClick={onClose}\n            data-testid=\"toast-cancel-btn\"\n            className=\"toast-danger-btn\"\n          >\n            Cancel\n          </EmptyButton>\n        </FlexItem>\n      </Row>\n    </>\n  )\n}\nexport default EncryptionErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/encryption-error-content/index.ts",
    "content": "import EncryptionErrorContent from './EncryptionErrorContent'\n\nexport default EncryptionErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/index.ts",
    "content": "import DefaultErrorContent from './default-error-content'\nimport EncryptionErrorContent from './encryption-error-content'\nimport { INFINITE_MESSAGES, InfiniteMessagesIds } from './infinite-messages'\n\nexport {\n  EncryptionErrorContent,\n  DefaultErrorContent,\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen, act } from 'uiSrc/utils/test-utils'\n\nimport { OAuthProvider } from 'uiSrc/components/oauth/oauth-select-plan/constants'\nimport notificationsReducer, {\n  addInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { combineReducers, configureStore } from '@reduxjs/toolkit'\nimport { InfiniteMessage } from 'uiSrc/slices/interfaces'\nimport Notifications from '../../Notifications'\nimport { INFINITE_MESSAGES } from './InfiniteMessages'\n\nconst createTestStore = () =>\n  configureStore({\n    reducer: combineReducers({\n      app: combineReducers({ notifications: notificationsReducer }),\n    }),\n    middleware: (getDefaultMiddleware) =>\n      getDefaultMiddleware({ serializableCheck: false }),\n  })\n\nconst renderToast = async (notification: InfiniteMessage) => {\n  const store = createTestStore()\n\n  render(\n    <>\n      <Notifications />\n    </>,\n    { store },\n  )\n  await act(async () => store.dispatch(addInfiniteNotification(notification)))\n}\n\ndescribe('INFINITE_MESSAGES', () => {\n  describe('AUTHENTICATING', () => {\n    it('should render message', async () => {\n      await renderToast(INFINITE_MESSAGES.AUTHENTICATING())\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('Authenticating…')\n      const description = await screen.findByText(\n        'This may take several seconds, but it is totally worth it!',\n      )\n      const closeButton = await screen.findByRole('button', { name: /close/i })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n  })\n\n  describe('PENDING_CREATE_DB', () => {\n    it('should render message', async () => {\n      renderToast(INFINITE_MESSAGES.PENDING_CREATE_DB())\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('Processing Cloud API keys…')\n      const description = await screen.findByText(\n        /This may take several minutes, but it is totally worth it!\\s*You can continue working in Redis Insight, and we will notify you once done\\./,\n      )\n      const closeButton = await screen.findByRole('button', { name: /close/i })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n  })\n\n  describe('SUCCESS_CREATE_DB', () => {\n    it('should render message', async () => {\n      const onSuccess = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.SUCCESS_CREATE_DB({}, onSuccess))\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('Congratulations!')\n      const description = await screen.findByText(\n        /You can now use your Redis Cloud database/,\n      )\n      const manageDbLink = await screen.findByText('Manage DB')\n      const connectButton = await screen.findByRole('button', {\n        name: /Connect/,\n      })\n      const closeButton = await screen.findByRole('button', { name: /close/i })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(manageDbLink).toBeInTheDocument()\n      expect(connectButton).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n\n    it('should call onSuccess callback when clicking on the \"Connect\" button', async () => {\n      const onSuccess = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.SUCCESS_CREATE_DB({}, onSuccess))\n\n      const connectButton = await screen.findByRole('button', {\n        name: /Connect/,\n      })\n      expect(connectButton).toBeInTheDocument()\n\n      fireEvent.click(connectButton)\n\n      expect(onSuccess).toHaveBeenCalled()\n    })\n\n    it('should render plan details', async () => {\n      const planDetails = { region: 'us-us', provider: OAuthProvider.AWS }\n      const onSuccess = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.SUCCESS_CREATE_DB(planDetails, onSuccess))\n\n      const notificationDetailsPlan = await screen.findByTestId(\n        'notification-details-plan',\n      )\n      expect(notificationDetailsPlan).toBeInTheDocument()\n      expect(notificationDetailsPlan).toHaveTextContent('Free')\n\n      const notificationDetailsVendor = await screen.findByTestId(\n        'notification-details-vendor',\n      )\n      expect(notificationDetailsVendor).toBeInTheDocument()\n      expect(notificationDetailsVendor).toHaveTextContent('Amazon Web Services')\n\n      const notificationDetailsRegion = await screen.findByTestId(\n        'notification-details-region',\n      )\n      expect(notificationDetailsRegion).toBeInTheDocument()\n      expect(notificationDetailsRegion).toHaveTextContent('us-us')\n    })\n  })\n\n  describe('DATABASE_EXISTS', () => {\n    it('should render message', async () => {\n      const onSuccess = jest.fn()\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.DATABASE_EXISTS(onSuccess, onClose))\n\n      // Wait for the notification to appear\n      const title = await screen.findByText(\n        'You already have a free Redis Cloud subscription.',\n      )\n      const description = await screen.findByText(\n        'Do you want to import your existing database into Redis Insight?',\n      )\n      const importButton = await screen.findByRole('button', {\n        name: /Import/,\n      })\n      const closeButton = await screen.findByRole('button', { name: /close/i })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(importButton).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n\n    it('should call onSuccess callback when clicking on the \"Import\" button', async () => {\n      const onSuccess = jest.fn()\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.DATABASE_EXISTS(onSuccess, onClose))\n\n      const importButton = await screen.findByRole('button', { name: /Import/ })\n      expect(importButton).toBeInTheDocument()\n\n      fireEvent.click(importButton)\n\n      expect(onSuccess).toHaveBeenCalled()\n    })\n\n    it('should call onCancel callback when clicking on the \"X\" dismiss button', async () => {\n      const onSuccess = jest.fn()\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.DATABASE_EXISTS(onSuccess, onClose))\n\n      const closeButton = await screen.findByRole('button', { name: /Close/ })\n      expect(closeButton).toBeInTheDocument()\n\n      fireEvent.click(closeButton)\n\n      // Note: In the browser it works, but in the test env it doesn't\n      // expect(onClose).toHaveBeenCalled()\n    })\n  })\n\n  describe('DATABASE_IMPORT_FORBIDDEN', () => {\n    it('should render message', async () => {\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.DATABASE_IMPORT_FORBIDDEN(onClose))\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('Unable to import Cloud database.')\n      const description = await screen.findByText(\n        /Adding your Redis Cloud database to Redis Insight is disabled due to a setting restricting database connection management./,\n      )\n      const okButton = await screen.findByRole('button', {\n        name: /OK/,\n      })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(okButton).toBeInTheDocument()\n    })\n\n    it('should call onClose', async () => {\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.DATABASE_IMPORT_FORBIDDEN(onClose))\n\n      const okButton = await screen.findByRole('button', {\n        name: /OK/,\n      })\n      expect(okButton).toBeInTheDocument()\n\n      fireEvent.click(okButton)\n\n      expect(onClose).toHaveBeenCalled()\n    })\n  })\n\n  describe('SUBSCRIPTION_EXISTS', () => {\n    it('should render message', async () => {\n      const onSuccess = jest.fn()\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(onSuccess, onClose))\n\n      // Wait for the notification to appear\n      const title = await screen.findByText(\n        'Your subscription does not have a free Redis Cloud database.',\n      )\n      const description = await screen.findByText(\n        'Do you want to create a free database in your existing subscription?',\n      )\n      const createButton = await screen.findByRole('button', {\n        name: /Create/,\n      })\n      const closeButton = await screen.findByRole('button', { name: /Close/ })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(createButton).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n\n    it('should call onSuccess callback when clicking on the \"Create\" button', async () => {\n      const onSuccess = jest.fn()\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(onSuccess, onClose))\n\n      const createButton = await screen.findByRole('button', {\n        name: /Create/,\n      })\n      expect(createButton).toBeInTheDocument()\n\n      fireEvent.click(createButton)\n\n      expect(onSuccess).toHaveBeenCalled()\n    })\n\n    it('should call onCancel callback when clicking on the \"X\" dismiss button', async () => {\n      const onSuccess = jest.fn()\n      const onClose = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(onSuccess, onClose))\n\n      const closeButton = await screen.findByRole('button', {\n        name: /Close/,\n      })\n      expect(closeButton).toBeInTheDocument()\n\n      fireEvent.click(closeButton)\n\n      // Note: In the browser it works, but in the test env it doesn't\n      // expect(onClose).toHaveBeenCalled()\n    })\n  })\n\n  describe('AUTO_CREATING_DATABASE', () => {\n    it('should render message', async () => {\n      renderToast(INFINITE_MESSAGES.AUTO_CREATING_DATABASE())\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('Connecting to your database')\n      const description = await screen.findByText(\n        'This may take several minutes, but it is totally worth it!',\n      )\n      const closeButton = await screen.findByRole('button', { name: /Close/ })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n  })\n\n  describe('APP_UPDATE_AVAILABLE', () => {\n    it('should render message', async () => {\n      const version = '<version>'\n      const onSuccess = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.APP_UPDATE_AVAILABLE(version, onSuccess))\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('New version is now available')\n      const description = await screen.findByText(\n        /With Redis Insight <version> you have access to new useful features and optimizations\\.\\s*Restart Redis Insight to install updates\\./,\n      )\n      const restartButton = await screen.findByRole('button', {\n        name: /Restart/,\n      })\n      const closeButton = await screen.findByRole('button', { name: /close/i })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(restartButton).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n\n    it('should call onSuccess when clicking restart button', async () => {\n      const version = '<version>'\n      const onSuccess = jest.fn()\n\n      renderToast(INFINITE_MESSAGES.APP_UPDATE_AVAILABLE(version, onSuccess))\n\n      const restartButton = await screen.findByRole('button', {\n        name: /Restart/,\n      })\n      expect(restartButton).toBeInTheDocument()\n\n      fireEvent.click(restartButton)\n\n      expect(onSuccess).toHaveBeenCalled()\n    })\n  })\n\n  describe('SUCCESS_DEPLOY_PIPELINE', () => {\n    it('should render message', async () => {\n      renderToast(INFINITE_MESSAGES.SUCCESS_DEPLOY_PIPELINE())\n\n      // Wait for the notification to appear\n      const title = await screen.findByText('Congratulations!')\n      const description = await screen.findByText(\n        /Deployment completed successfully!\\s*Check out the pipeline statistics page\\./,\n      )\n      const closeButton = await screen.findByRole('button', { name: /close/i })\n\n      expect(title).toBeInTheDocument()\n      expect(description).toBeInTheDocument()\n      expect(closeButton).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/infinite-messages/InfiniteMessages.tsx",
    "content": "import React from 'react'\nimport { find } from 'lodash'\nimport { CloudJobName, CloudJobStep } from 'uiSrc/electron/constants'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { OAuthProviders } from 'uiSrc/components/oauth/oauth-select-plan/constants'\nimport { LoaderLargeIcon } from 'uiSrc/components/base/icons'\n\nimport { CloudSuccessResult, InfiniteMessage } from 'uiSrc/slices/interfaces'\n\nimport { Maybe } from 'uiSrc/utils'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  EXTERNAL_LINKS,\n  UTM_CAMPAINGS,\n  UTM_MEDIUMS,\n} from 'uiSrc/constants/links'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nimport styles from './styles.module.scss'\n\nexport enum InfiniteMessagesIds {\n  oAuthProgress = 'oAuthProgress',\n  oAuthSuccess = 'oAuthSuccess',\n  autoCreateDb = 'autoCreateDb',\n  databaseExists = 'databaseExists',\n  databaseImportForbidden = 'databaseImportForbidden',\n  subscriptionExists = 'subscriptionExists',\n  appUpdateAvailable = 'appUpdateAvailable',\n  pipelineDeploySuccess = 'pipelineDeploySuccess',\n}\n\nconst MANAGE_DB_LINK = getUtmExternalLink(EXTERNAL_LINKS.cloudConsole, {\n  campaign: UTM_CAMPAINGS.Main,\n  medium: UTM_MEDIUMS.Main,\n})\n\ninterface InfiniteMessagesType {\n  AUTHENTICATING: () => InfiniteMessage\n  PENDING_CREATE_DB: (step?: CloudJobStep) => InfiniteMessage\n  SUCCESS_CREATE_DB: (\n    details: Omit<CloudSuccessResult, 'resourceId'>,\n    onSuccess: () => void,\n    jobName: Maybe<CloudJobName>,\n  ) => InfiniteMessage\n  DATABASE_EXISTS: (\n    onSuccess?: () => void,\n    onClose?: () => void,\n  ) => InfiniteMessage\n  DATABASE_IMPORT_FORBIDDEN: (onClose?: () => void) => InfiniteMessage\n  SUBSCRIPTION_EXISTS: (\n    onSuccess?: () => void,\n    onClose?: () => void,\n  ) => InfiniteMessage\n  AUTO_CREATING_DATABASE: () => InfiniteMessage\n  APP_UPDATE_AVAILABLE: (\n    version: string,\n    onSuccess?: () => void,\n  ) => InfiniteMessage\n  SUCCESS_DEPLOY_PIPELINE: () => InfiniteMessage\n}\n\nexport const INFINITE_MESSAGES: InfiniteMessagesType = {\n  AUTHENTICATING: () => ({\n    id: InfiniteMessagesIds.oAuthProgress,\n    message: 'Authenticating…',\n    description: 'This may take several seconds, but it is totally worth it!',\n    customIcon: LoaderLargeIcon,\n  }),\n  PENDING_CREATE_DB: (step?: CloudJobStep) => ({\n    id: InfiniteMessagesIds.oAuthProgress,\n    customIcon: LoaderLargeIcon,\n    variation: step,\n    message: (\n      <>\n        {(step === CloudJobStep.Credentials || !step) &&\n          'Processing Cloud API keys…'}\n        {step === CloudJobStep.Subscription &&\n          'Processing Cloud subscriptions…'}\n        {step === CloudJobStep.Database &&\n          'Creating a free Redis Cloud database…'}\n        {step === CloudJobStep.Import &&\n          'Importing a free Redis Cloud database…'}\n      </>\n    ),\n    description: (\n      <>\n        This may take several minutes, but it is totally worth it!\n        <Spacer size=\"m\" />\n        You can continue working in Redis Insight, and we will notify you once\n        done.\n      </>\n    ),\n  }),\n  SUCCESS_CREATE_DB: (\n    details: Omit<CloudSuccessResult, 'resourceId'>,\n    onSuccess: () => void,\n    jobName: Maybe<CloudJobName>,\n  ) => {\n    const vendor = find(OAuthProviders, ({ id }) => id === details.provider)\n    const withFeed =\n      jobName &&\n      [\n        CloudJobName.CreateFreeDatabase,\n        CloudJobName.CreateFreeSubscriptionAndDatabase,\n      ].includes(jobName)\n    const text = `You can now use your Redis Cloud database${withFeed ? ' with pre-loaded sample data' : ''}.`\n\n    return {\n      id: InfiniteMessagesIds.oAuthSuccess,\n      message: 'Congratulations!',\n      variant: 'success',\n      description: (\n        <>\n          {text}\n          <Spacer size=\"m\" />\n          <Text variant=\"semiBold\" component=\"span\">\n            Notice:\n          </Text>{' '}\n          the database will be deleted after 15 days of inactivity.\n          {!!details && (\n            <>\n              <Spacer size=\"m\" />\n              <Divider />\n              <Spacer size=\"m\" />\n              <Row className={styles.detailsRow} justify=\"between\">\n                <FlexItem>\n                  <Text size=\"xs\">Plan</Text>\n                </FlexItem>\n                <FlexItem data-testid=\"notification-details-plan\">\n                  <Text size=\"xs\">Free</Text>\n                </FlexItem>\n              </Row>\n              <Row\n                className={styles.detailsRow}\n                justify=\"between\"\n                align=\"center\"\n              >\n                <FlexItem>\n                  <Text size=\"xs\">Cloud Vendor</Text>\n                </FlexItem>\n                <FlexItem\n                  className={styles.vendorLabel}\n                  data-testid=\"notification-details-vendor\"\n                  $gap=\"s\"\n                >\n                  {!!vendor?.icon && <RiIcon type={vendor?.icon} />}\n                  <Text size=\"xs\">{vendor?.label}</Text>\n                </FlexItem>\n              </Row>\n              <Row className={styles.detailsRow} justify=\"between\">\n                <FlexItem>\n                  <Text size=\"xs\">Region</Text>\n                </FlexItem>\n                <FlexItem data-testid=\"notification-details-region\">\n                  <Text size=\"xs\">{details.region}</Text>\n                </FlexItem>\n              </Row>\n            </>\n          )}\n          <Spacer size=\"m\" />\n          <Row justify=\"between\" align=\"center\">\n            <FlexItem>\n              <Link\n                external\n                target=\"_blank\"\n                href={MANAGE_DB_LINK}\n                variant=\"inline\"\n              >\n                Manage DB\n              </Link>\n            </FlexItem>\n            <FlexItem>\n              <PrimaryButton\n                size=\"small\"\n                onClick={() => onSuccess()}\n                data-testid=\"notification-connect-db\"\n              >\n                Connect\n              </PrimaryButton>\n            </FlexItem>\n          </Row>\n        </>\n      ),\n    }\n  },\n  DATABASE_EXISTS: (onSuccess?: () => void, onClose?: () => void) => ({\n    id: InfiniteMessagesIds.databaseExists,\n    message: 'You already have a free Redis Cloud subscription.',\n    description:\n      'Do you want to import your existing database into Redis Insight?',\n    actions: {\n      primary: { label: 'Import', onClick: () => onSuccess?.() },\n    },\n    onClose,\n  }),\n  DATABASE_IMPORT_FORBIDDEN: (onClose?: () => void) => ({\n    id: InfiniteMessagesIds.databaseImportForbidden,\n    message: 'Unable to import Cloud database.',\n    description: (\n      <>\n        Adding your Redis Cloud database to Redis Insight is disabled due to a\n        setting restricting database connection management.\n        <Spacer size=\"m\" />\n        Log in to{' '}\n        <Link\n          external\n          target=\"_blank\"\n          variant=\"inline\"\n          tabIndex={-1}\n          href={getUtmExternalLink(EXTERNAL_LINKS.cloudConsole, {\n            medium: UTM_MEDIUMS.Main,\n            campaign: 'disabled_db_management',\n          })}\n        >\n          Redis Cloud\n        </Link>{' '}\n        to check your database.\n      </>\n    ),\n    actions: {\n      primary: {\n        label: 'OK',\n        onClick: () => onClose?.(),\n      },\n    },\n    showCloseButton: false,\n  }),\n  SUBSCRIPTION_EXISTS: (onSuccess?: () => void, onClose?: () => void) => ({\n    id: InfiniteMessagesIds.subscriptionExists,\n    message: 'Your subscription does not have a free Redis Cloud database.',\n    description:\n      'Do you want to create a free database in your existing subscription?',\n    actions: {\n      primary: { label: 'Create', onClick: () => onSuccess?.() },\n    },\n    onClose,\n  }),\n  AUTO_CREATING_DATABASE: () => ({\n    id: InfiniteMessagesIds.autoCreateDb,\n    message: 'Connecting to your database',\n    description: 'This may take several minutes, but it is totally worth it!',\n    customIcon: LoaderLargeIcon,\n  }),\n  APP_UPDATE_AVAILABLE: (version: string, onSuccess?: () => void) => ({\n    id: InfiniteMessagesIds.appUpdateAvailable,\n    message: 'New version is now available',\n    description: (\n      <>\n        With Redis Insight {version} you have access to new useful features and\n        optimizations.\n        <Spacer size=\"m\" />\n        Restart Redis Insight to install updates.\n      </>\n    ),\n    actions: {\n      primary: { label: 'Restart', onClick: () => onSuccess?.() },\n    },\n  }),\n  SUCCESS_DEPLOY_PIPELINE: () => ({\n    id: InfiniteMessagesIds.pipelineDeploySuccess,\n    message: 'Congratulations!',\n    description: (\n      <>\n        Deployment completed successfully!\n        <br />\n        Check out the pipeline statistics page.\n      </>\n    ),\n    // TODO enable when statistics page will be available\n    // actions: {\n    //   primary: {\n    //     label: 'Statistics',\n    //     onClick: () => {},\n    //   }\n    // }\n  }),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/infinite-messages/index.ts",
    "content": "import { InfiniteMessagesIds, INFINITE_MESSAGES } from './InfiniteMessages'\n\nexport { InfiniteMessagesIds, INFINITE_MESSAGES }\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/infinite-messages/styles.module.scss",
    "content": ".detailsRow {\n  margin-bottom: 4px;\n}\n.vendorLabel {\n  flex-direction: row !important;\n  align-items: center !important;\n\n  :global(.euiIcon) {\n    margin-right: 4px;\n  }\n}\n\n.loading {\n  border-top-color: var(--euiColorGhost) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/persistent-error-content/PersistentErrorContent.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport PersistentErrorContent, { Props } from './PersistentErrorContent'\n\nconst mockWriteText = jest.fn()\nObject.assign(navigator, {\n  clipboard: {\n    writeText: mockWriteText,\n  },\n})\n\ndescribe('PersistentErrorContent', () => {\n  const defaultProps: Props = {\n    text: faker.lorem.sentence(),\n    onClose: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<Props>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<PersistentErrorContent {...props} />)\n  }\n\n  beforeEach(() => {\n    mockWriteText.mockClear()\n  })\n\n  it('should render error message', () => {\n    renderComponent()\n    expect(screen.getByText(defaultProps.text)).toBeInTheDocument()\n  })\n\n  it('should render copy button with \"Copy\" label', () => {\n    renderComponent()\n    const button = screen.getByTestId('copy-error-message-btn')\n    expect(button).toBeInTheDocument()\n    expect(button).toHaveTextContent('Copy')\n  })\n\n  it('should copy error message and show \"Copied\" when clicked', () => {\n    renderComponent()\n    const button = screen.getByTestId('copy-error-message-btn')\n    fireEvent.click(button)\n    expect(mockWriteText).toHaveBeenCalledWith(defaultProps.text)\n    expect(button).toHaveTextContent('Copied')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/persistent-error-content/PersistentErrorContent.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Button } from 'uiSrc/components/base/forms/buttons'\nimport { CopyIcon, ToastCheckIcon } from 'uiSrc/components/base/icons'\nimport { handleCopy } from 'uiSrc/utils'\n\nexport interface Props {\n  text: string\n}\n\nconst PersistentErrorContent = ({ text }: Props) => {\n  const [isCopied, setIsCopied] = useState(false)\n\n  const handleCopyClick = () => {\n    handleCopy(text)\n    setIsCopied(true)\n  }\n\n  useEffect(() => {\n    if (isCopied) {\n      const timeout = setTimeout(() => {\n        setIsCopied(false)\n      }, 2000)\n      return () => clearTimeout(timeout)\n    }\n    return undefined\n  }, [isCopied])\n\n  return (\n    <>\n      <ColorText color=\"danger\">{text}</ColorText>\n      <Spacer />\n      <Row justify=\"end\">\n        <FlexItem>\n          <Button\n            onClick={handleCopyClick}\n            icon={isCopied ? ToastCheckIcon : CopyIcon}\n            size=\"s\"\n            data-testid=\"copy-error-message-btn\"\n          >\n            {isCopied ? 'Copied' : 'Copy'}\n          </Button>\n        </FlexItem>\n      </Row>\n    </>\n  )\n}\n\nexport default PersistentErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/persistent-error-content/index.ts",
    "content": "export { default as PersistentErrorContent } from './PersistentErrorContent'\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/rdi-deploy-error-content/RdiDeployErrorContent.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport RdiDeployErrorContent from './RdiDeployErrorContent'\n\ndescribe('RdiDeployErrorContent', () => {\n  const mockMessage = 'Test error log content'\n\n  beforeEach(() => {\n    jest.spyOn(URL, 'createObjectURL').mockImplementation(() => 'mock-url')\n    jest.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})\n  })\n\n  afterEach(() => {\n    jest.restoreAllMocks()\n  })\n\n  it('renders the error message and download button', () => {\n    render(<RdiDeployErrorContent message={mockMessage} />)\n\n    expect(\n      screen.getByText('Review the error log for details.'),\n    ).toBeInTheDocument()\n    const downloadButton = screen.getByTestId('donwload-log-file-btn')\n    expect(downloadButton).toBeInTheDocument()\n    expect(downloadButton).toHaveAttribute('href', 'mock-url')\n    expect(downloadButton).toHaveAttribute('download', 'error-log.txt')\n  })\n\n  it('creates and revokes the object URL properly', () => {\n    const { unmount } = render(<RdiDeployErrorContent message={mockMessage} />)\n\n    expect(URL.createObjectURL).toHaveBeenCalled()\n    unmount()\n    expect(URL.revokeObjectURL).toHaveBeenCalledWith('mock-url')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/rdi-deploy-error-content/RdiDeployErrorContent.tsx",
    "content": "import React, { useEffect, useMemo } from 'react'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { DestructiveButton } from 'uiSrc/components/base/forms/buttons'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nexport interface Props {\n  message: string\n  // eslint-disable-next-line react/no-unused-prop-types\n  onClose?: () => void\n}\n\nconst RdiDeployErrorContent = (props: Props) => {\n  const { message } = props\n\n  const fileUrl = useMemo(() => {\n    const blob = new Blob([message], { type: 'text/plain' })\n    return URL.createObjectURL(blob)\n  }, [message])\n\n  useEffect(\n    () => () => {\n      URL.revokeObjectURL(fileUrl)\n    },\n    [fileUrl],\n  )\n\n  return (\n    <>\n      <ColorText color=\"danger\">\n        <Col>\n          <FlexItem>\n            <div>Review the error log for details.</div>\n            <Link\n              variant=\"inline\"\n              size=\"S\"\n              href={fileUrl}\n              download=\"error-log.txt\"\n              data-testid=\"donwload-log-file-btn\"\n              style={{ marginTop: '10px', paddingLeft: 0 }}\n            >\n              Download Error Log File\n            </Link>\n          </FlexItem>\n        </Col>\n      </ColorText>\n\n      <Spacer />\n      {/* // TODO remove display none when logs column will be available */}\n      <Row style={{ display: 'none' }} justify=\"end\">\n        <FlexItem>\n          <DestructiveButton\n            size=\"s\"\n            onClick={() => {}}\n            className=\"toast-danger-btn\"\n            data-testid=\"see-errors-btn\"\n          >\n            Remove API key\n          </DestructiveButton>\n        </FlexItem>\n      </Row>\n    </>\n  )\n}\n\nexport default RdiDeployErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/components/rdi-deploy-error-content/index.ts",
    "content": "import RdiDeployErrorContent from './RdiDeployErrorContent'\n\nexport default RdiDeployErrorContent\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/constants.ts",
    "content": "export const defaultContainerId = 'default'\n\nexport const IMContainerId = 'InfiniteMessages'\nexport const ONE_HOUR = 3_600_000\nexport const NotificationTextLengthThreshold = 400\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/error-messages.spec.tsx",
    "content": "import React from 'react'\nimport { handleDownloadButton } from 'uiSrc/utils'\nimport { NotificationTextLengthThreshold } from 'uiSrc/components/notifications/constants'\nimport ERROR_MESSAGES from './error-messages'\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  handleDownloadButton: jest.fn(),\n}))\n\ndescribe('ERROR_MESSAGES', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  describe('DEFAULT', () => {\n    it('should return error notification with correct data-testid', () => {\n      const result = ERROR_MESSAGES.DEFAULT('Error text')\n\n      expect(result['data-testid']).toBe('toast-error')\n    })\n\n    it('should return error notification with default title', () => {\n      const result = ERROR_MESSAGES.DEFAULT('Error text')\n\n      expect(result.message).toBe('Error')\n    })\n\n    it('should return error notification with custom title', () => {\n      const customTitle = 'Custom Error Title'\n      const result = ERROR_MESSAGES.DEFAULT('Error text', () => {}, customTitle)\n\n      expect(result.message).toBe(customTitle)\n    })\n\n    it('should have customIcon property', () => {\n      const result = ERROR_MESSAGES.DEFAULT('Error text')\n\n      expect(result.customIcon).toBeDefined()\n    })\n\n    it('should have description for short messages', () => {\n      const shortText = 'Short error message'\n      const result = ERROR_MESSAGES.DEFAULT(shortText)\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should not have description for long messages', () => {\n      const longText = 'a'.repeat(NotificationTextLengthThreshold + 1)\n      const result = ERROR_MESSAGES.DEFAULT(longText)\n\n      expect(result.description).toBeUndefined()\n    })\n\n    it('should have description for non-string text', () => {\n      const objectText = { error: 'some error' }\n      const result = ERROR_MESSAGES.DEFAULT(objectText)\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should have download button action for long messages', () => {\n      const longText = 'a'.repeat(NotificationTextLengthThreshold + 1)\n      const result = ERROR_MESSAGES.DEFAULT(longText)\n\n      expect(result.actions.secondary).toBeDefined()\n      expect(result.actions.secondary?.label).toBe('Download full log')\n      expect(result.actions.secondary?.closes).toBe(true)\n    })\n\n    it('should not have download button action for short messages', () => {\n      const shortText = 'Short error message'\n      const result = ERROR_MESSAGES.DEFAULT(shortText)\n\n      expect(result.actions.secondary).toBeUndefined()\n    })\n\n    it('should call handleDownloadButton when download action is clicked', () => {\n      const longText = 'a'.repeat(NotificationTextLengthThreshold + 1)\n      const onClose = jest.fn()\n      const result = ERROR_MESSAGES.DEFAULT(longText, onClose)\n\n      result.actions.secondary?.onClick?.()\n\n      expect(handleDownloadButton).toHaveBeenCalledWith(\n        longText,\n        'error-log.txt',\n        onClose,\n      )\n    })\n\n    it('should handle onClick when it is undefined', () => {\n      const longText = 'a'.repeat(NotificationTextLengthThreshold + 1)\n      const result = ERROR_MESSAGES.DEFAULT(longText)\n\n      expect(result.actions.secondary?.onClick).toBeDefined()\n    })\n  })\n\n  describe('ENCRYPTION', () => {\n    it('should return encryption error notification with correct data-testid', () => {\n      const result = ERROR_MESSAGES.ENCRYPTION()\n\n      expect(result['data-testid']).toBe('toast-error-encryption')\n    })\n\n    it('should return encryption error notification with correct message', () => {\n      const result = ERROR_MESSAGES.ENCRYPTION()\n\n      expect(result.message).toBe('Unable to decrypt')\n    })\n\n    it('should have customIcon property', () => {\n      const result = ERROR_MESSAGES.ENCRYPTION()\n\n      expect(result.customIcon).toBeDefined()\n    })\n\n    it('should have showCloseButton set to false', () => {\n      const result = ERROR_MESSAGES.ENCRYPTION()\n\n      expect(result.showCloseButton).toBe(false)\n    })\n\n    it('should have description defined', () => {\n      const result = ERROR_MESSAGES.ENCRYPTION()\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should work with instanceId parameter', () => {\n      const instanceId = 'test-instance-123'\n      const result = ERROR_MESSAGES.ENCRYPTION(() => {}, instanceId)\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should work with default parameters', () => {\n      const result = ERROR_MESSAGES.ENCRYPTION()\n\n      expect(result.description).toBeDefined()\n      expect(result.showCloseButton).toBe(false)\n    })\n  })\n\n  describe('CLOUD_CAPI_KEY_UNAUTHORIZED', () => {\n    it('should return cloud capi unauthorized error with correct data-testid', () => {\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message: 'Unauthorized' },\n        {},\n        () => {},\n      )\n\n      expect(result['data-testid']).toBe(\n        'toast-error-cloud-capi-key-unauthorized',\n      )\n    })\n\n    it('should have customIcon property', () => {\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message: 'Unauthorized' },\n        {},\n        () => {},\n      )\n\n      expect(result.customIcon).toBeDefined()\n    })\n\n    it('should have showCloseButton set to false', () => {\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message: 'Unauthorized' },\n        {},\n        () => {},\n      )\n\n      expect(result.showCloseButton).toBe(false)\n    })\n\n    it('should return error notification with title', () => {\n      const title = 'Unauthorized Error'\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message: 'Unauthorized', title },\n        {},\n        () => {},\n      )\n\n      expect(result.message).toBe(title)\n    })\n\n    it('should have description defined', () => {\n      const message = 'Unauthorized access'\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message },\n        {},\n        () => {},\n      )\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should work with resourceId in additionalInfo', () => {\n      const message = 'Unauthorized access'\n      const resourceId = 'resource-123'\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message },\n        { resourceId },\n        () => {},\n      )\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should handle JSX Element as message', () => {\n      const jsxMessage = <span>Custom JSX Error</span>\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message: jsxMessage },\n        {},\n        () => {},\n      )\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should work without title', () => {\n      const message = 'Unauthorized access'\n      const result = ERROR_MESSAGES.CLOUD_CAPI_KEY_UNAUTHORIZED(\n        { message },\n        {},\n        () => {},\n      )\n\n      expect(result.message).toBeUndefined()\n    })\n  })\n\n  describe('RDI_DEPLOY_PIPELINE', () => {\n    it('should return rdi deploy error with correct data-testid', () => {\n      const result = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE(\n        { message: 'Deploy failed' },\n        () => {},\n      )\n\n      expect(result['data-testid']).toBe('toast-error-deploy')\n    })\n\n    it('should have customIcon property', () => {\n      const result = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE(\n        { message: 'Deploy failed' },\n        () => {},\n      )\n\n      expect(result.customIcon).toBeDefined()\n    })\n\n    it('should return error notification with title', () => {\n      const title = 'Deployment Error'\n      const result = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE(\n        { title, message: 'Deploy failed' },\n        () => {},\n      )\n\n      expect(result.message).toBe(title)\n    })\n\n    it('should pass onClose callback', () => {\n      const onClose = jest.fn()\n      const result = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE(\n        { message: 'Deploy failed' },\n        onClose,\n      )\n\n      expect(result.onClose).toBe(onClose)\n    })\n\n    it('should have description defined', () => {\n      const message = 'Deploy failed'\n      const result = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE({ message }, () => {})\n\n      expect(result.description).toBeDefined()\n    })\n\n    it('should work without title', () => {\n      const message = 'Deploy failed'\n      const result = ERROR_MESSAGES.RDI_DEPLOY_PIPELINE({ message }, () => {})\n\n      expect(result.message).toBeUndefined()\n      expect(result.description).toBeDefined()\n    })\n  })\n\n  describe('PERSISTENT', () => {\n    it('should return persistent error notification with correct data-testid', () => {\n      const result = ERROR_MESSAGES.PERSISTENT(\n        { message: 'Error text' },\n        () => {},\n      )\n\n      expect(result['data-testid']).toBe('toast-error-persistent')\n    })\n\n    it('should pass onClose callback for proper toast cleanup', () => {\n      const onClose = jest.fn()\n      const result = ERROR_MESSAGES.PERSISTENT(\n        { message: 'Error text' },\n        onClose,\n      )\n\n      expect(result.onClose).toBe(onClose)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/error-messages.tsx",
    "content": "import React from 'react'\n\nimport { InfoIcon, ToastDangerIcon } from 'uiSrc/components/base/icons'\n\nimport RdiDeployErrorContent from './components/rdi-deploy-error-content'\nimport { EncryptionErrorContent, DefaultErrorContent } from './components'\nimport CloudCapiUnAuthorizedErrorContent from './components/cloud-capi-unauthorized'\nimport { AzureTokenExpiredErrorContent } from './components/azure-token-expired'\nimport { PersistentErrorContent } from './components/persistent-error-content'\nimport { NotificationTextLengthThreshold } from 'uiSrc/components/notifications/constants'\nimport { handleDownloadButton } from 'uiSrc/utils'\n\nexport default {\n  DEFAULT: (text: any, onClose = () => {}, title: string = 'Error') => {\n    const isSafeMessage =\n      text.length < NotificationTextLengthThreshold || typeof text !== 'string'\n\n    return {\n      'data-testid': 'toast-error',\n      customIcon: ToastDangerIcon,\n      message: title,\n      description: isSafeMessage ? (\n        <DefaultErrorContent text={text} />\n      ) : undefined,\n      actions: {\n        secondary: !isSafeMessage\n          ? {\n              label: 'Download full log',\n              closes: true,\n              onClick: () =>\n                handleDownloadButton(text, 'error-log.txt', onClose),\n            }\n          : undefined,\n      },\n    }\n  },\n  ENCRYPTION: (onClose = () => {}, instanceId = '') => ({\n    'data-testid': 'toast-error-encryption',\n    customIcon: InfoIcon,\n    message: 'Unable to decrypt',\n    description: (\n      <EncryptionErrorContent instanceId={instanceId} onClose={onClose} />\n    ),\n    showCloseButton: false,\n  }),\n  CLOUD_CAPI_KEY_UNAUTHORIZED: (\n    {\n      message,\n      title,\n    }: {\n      message: string | JSX.Element\n      title?: string\n    },\n    additionalInfo: Record<string, any>,\n    onClose: () => void,\n  ) => ({\n    'data-testid': 'toast-error-cloud-capi-key-unauthorized',\n    customIcon: ToastDangerIcon,\n    message: title,\n    showCloseButton: false,\n    description: (\n      <CloudCapiUnAuthorizedErrorContent\n        text={message}\n        resourceId={additionalInfo.resourceId}\n        onClose={onClose}\n      />\n    ),\n  }),\n  RDI_DEPLOY_PIPELINE: (\n    { title, message }: { title?: string; message: string },\n    onClose: () => void,\n  ) => ({\n    'data-testid': 'toast-error-deploy',\n    customIcon: ToastDangerIcon,\n    onClose,\n    message: title,\n    description: <RdiDeployErrorContent message={message} onClose={onClose} />,\n  }),\n  AZURE_TOKEN_EXPIRED: (\n    { message }: { message: string | JSX.Element },\n    onClose: () => void,\n  ) => ({\n    'data-testid': 'toast-info-azure-token-expired',\n    customIcon: InfoIcon,\n    showCloseButton: true,\n    onClose,\n    description: (\n      <AzureTokenExpiredErrorContent text={message} onClose={onClose} />\n    ),\n  }),\n  PERSISTENT: (\n    { message, title }: { message: string; title?: string },\n    onClose: () => void,\n  ) => ({\n    'data-testid': 'toast-error-persistent',\n    customIcon: ToastDangerIcon,\n    onClose,\n    message: title,\n    description: <PersistentErrorContent text={message} />,\n  }),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/hooks/index.ts",
    "content": "export { useErrorNotifications } from './useErrorNotifications'\nexport { useMessageNotifications } from './useMessageNotifications'\nexport { useInfiniteNotifications } from './useInfiniteNotifications'\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.spec.tsx",
    "content": "import React from 'react'\nimport { renderHook } from '@testing-library/react-hooks'\nimport { Provider } from 'react-redux'\nimport configureStore from 'redux-mock-store'\nimport { cleanup } from '@testing-library/react'\n\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { useErrorNotifications } from './useErrorNotifications'\nimport { initialStateDefault } from 'uiSrc/utils/test-utils'\n\njest.mock('uiSrc/components/base/display/toast', () => ({\n  riToast: Object.assign(\n    jest.fn(() => 'mock-toast-id'),\n    {\n      dismiss: jest.fn(),\n      isActive: jest.fn(() => false),\n      Variant: {\n        Danger: 'danger',\n        Informative: 'informative',\n      },\n    },\n  ),\n}))\n\nconst mockStore = configureStore()\n\nconst createWrapper =\n  (store: ReturnType<typeof mockStore>) =>\n  ({ children }: { children: React.ReactNode }) => (\n    <Provider store={store}>{children}</Provider>\n  )\n\nconst createAzureError = (id: string) => ({\n  id,\n  name: 'Error',\n  message: 'Azure Entra ID token expired',\n  additionalInfo: {\n    errorCode: CustomErrorCodes.AzureEntraIdTokenExpired,\n  },\n})\n\nconst createRegularError = (id: string, message: string) => ({\n  id,\n  name: 'Error',\n  message,\n})\n\nconst mockedRiToastIsActive = riToast.isActive as jest.Mock\n\ndescribe('useErrorNotifications', () => {\n  beforeEach(() => {\n    cleanup()\n    jest.clearAllMocks()\n    mockedRiToastIsActive.mockReturnValue(false)\n  })\n\n  describe('Azure token expired errors', () => {\n    it('should show only one toast for multiple Azure errors', () => {\n      // Mock isActive to return true after the first toast is shown\n      let toastShown = false\n      mockedRiToastIsActive.mockImplementation(() => {\n        const result = toastShown\n        toastShown = true\n        return result\n      })\n\n      const errors = [createAzureError('error-1'), createAzureError('error-2')]\n\n      const store = mockStore({\n        ...initialStateDefault,\n        app: {\n          ...initialStateDefault.app,\n          notifications: {\n            ...initialStateDefault.app.notifications,\n            errors,\n          },\n        },\n      })\n\n      renderHook(() => useErrorNotifications(), {\n        wrapper: createWrapper(store),\n      })\n\n      // Only one toast should be shown (first error shows, second skips)\n      expect(riToast).toHaveBeenCalledTimes(1)\n      expect(riToast).toHaveBeenCalledWith(\n        expect.anything(),\n        expect.objectContaining({\n          toastId: 'azure-token-expired',\n        }),\n      )\n    })\n\n    it('should not show duplicate toast if one is already active', () => {\n      mockedRiToastIsActive.mockReturnValue(true)\n\n      const errors = [createAzureError('error-1')]\n\n      const store = mockStore({\n        ...initialStateDefault,\n        app: {\n          ...initialStateDefault.app,\n          notifications: {\n            ...initialStateDefault.app.notifications,\n            errors,\n          },\n        },\n      })\n\n      renderHook(() => useErrorNotifications(), {\n        wrapper: createWrapper(store),\n      })\n\n      // Toast is already active, so riToast should not be called\n      expect(riToast).not.toHaveBeenCalled()\n    })\n\n    it('should remove all Azure error IDs from Redux when toast is dismissed', () => {\n      const errors = [createAzureError('error-1'), createAzureError('error-2')]\n\n      const store = mockStore({\n        ...initialStateDefault,\n        app: {\n          ...initialStateDefault.app,\n          notifications: {\n            ...initialStateDefault.app.notifications,\n            errors,\n          },\n        },\n      })\n\n      renderHook(() => useErrorNotifications(), {\n        wrapper: createWrapper(store),\n      })\n\n      // Get the onClose callback passed to the toast\n      const mockRiToast = riToast as unknown as jest.Mock\n      const toastCall = mockRiToast.mock.calls[0]\n      const toastConfig = toastCall[0]\n      const onClose = toastConfig.onClose\n\n      // Call the onClose callback (simulating user closing the toast)\n      onClose()\n\n      // Should dispatch removeMessage for both error IDs\n      const actions = store.getActions()\n      expect(actions).toContainEqual({\n        type: 'notifications/removeMessage',\n        payload: 'error-1',\n      })\n      expect(actions).toContainEqual({\n        type: 'notifications/removeMessage',\n        payload: 'error-2',\n      })\n\n      // Should dismiss the toast\n      expect(riToast.dismiss).toHaveBeenCalledWith('azure-token-expired')\n    })\n  })\n\n  describe('regular errors', () => {\n    it('should show toast for regular errors', () => {\n      const errors = [createRegularError('error-1', 'Something went wrong')]\n\n      const store = mockStore({\n        ...initialStateDefault,\n        app: {\n          ...initialStateDefault.app,\n          notifications: {\n            ...initialStateDefault.app.notifications,\n            errors,\n          },\n        },\n      })\n\n      renderHook(() => useErrorNotifications(), {\n        wrapper: createWrapper(store),\n      })\n\n      expect(riToast).toHaveBeenCalledWith(\n        expect.anything(),\n        expect.objectContaining({\n          toastId: 'error-1',\n          variant: 'danger',\n        }),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/hooks/useErrorNotifications.ts",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { IError } from 'uiSrc/slices/interfaces'\nimport { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors'\nimport errorMessages from 'uiSrc/components/notifications/error-messages'\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { errorsSelector, removeMessage } from 'uiSrc/slices/app/notifications'\nimport { defaultContainerId } from 'uiSrc/components/notifications/constants'\nimport { RiToastType } from 'uiSrc/components/base/display/toast/RiToast'\n\nconst DEFAULT_ERROR_TITLE = 'Error'\n\nconst AZURE_TOKEN_EXPIRED_TOAST_ID = 'azure-token-expired'\n\nexport const useErrorNotifications = () => {\n  const errorsData = useSelector(errorsSelector)\n  const dispatch = useDispatch()\n  const toastIdsRef = useRef(new Map<string, number | string>())\n  const azureErrorIdsRef = useRef(new Set<string>())\n\n  const removeToast = (id: string) => {\n    if (toastIdsRef.current.has(id)) {\n      riToast.dismiss(toastIdsRef.current.get(id))\n      toastIdsRef.current.delete(id)\n    }\n    dispatch(removeMessage(id))\n  }\n\n  const removeAzureToast = () => {\n    riToast.dismiss(AZURE_TOKEN_EXPIRED_TOAST_ID)\n    azureErrorIdsRef.current.forEach((errorId) => {\n      toastIdsRef.current.delete(errorId)\n      dispatch(removeMessage(errorId))\n    })\n    azureErrorIdsRef.current.clear()\n  }\n\n  const showErrorsToasts = (errors: IError[]) =>\n    errors.forEach(\n      ({\n        id = '',\n        message = DEFAULT_ERROR_MESSAGE,\n        instanceId = '',\n        name,\n        title = DEFAULT_ERROR_TITLE,\n        additionalInfo,\n        persistent,\n      }) => {\n        if (toastIdsRef.current.has(id)) {\n          removeToast(id)\n          return\n        }\n\n        let errorMessage: RiToastType\n        if (ApiEncryptionErrors.includes(name)) {\n          errorMessage = errorMessages.ENCRYPTION(\n            () => removeToast(id),\n            instanceId,\n          )\n        } else if (\n          additionalInfo?.errorCode ===\n          CustomErrorCodes.CloudCapiKeyUnauthorized\n        ) {\n          errorMessage = errorMessages.CLOUD_CAPI_KEY_UNAUTHORIZED(\n            { message, title },\n            additionalInfo,\n            () => removeToast(id),\n          )\n        } else if (\n          additionalInfo?.errorCode ===\n          CustomErrorCodes.RdiDeployPipelineFailure\n        ) {\n          errorMessage = errorMessages.RDI_DEPLOY_PIPELINE(\n            { title, message },\n            () => removeToast(id),\n          )\n        } else if (\n          additionalInfo?.errorCode ===\n          CustomErrorCodes.AzureEntraIdTokenExpired\n        ) {\n          // Track original error ID and use fixed toastId to prevent duplicate toasts\n          azureErrorIdsRef.current.add(id)\n          toastIdsRef.current.set(id, AZURE_TOKEN_EXPIRED_TOAST_ID)\n\n          // Only show toast if not already visible\n          if (!riToast.isActive(AZURE_TOKEN_EXPIRED_TOAST_ID)) {\n            errorMessage = errorMessages.AZURE_TOKEN_EXPIRED(\n              { message },\n              removeAzureToast,\n            )\n            riToast(errorMessage, {\n              variant: riToast.Variant.Informative,\n              toastId: AZURE_TOKEN_EXPIRED_TOAST_ID,\n              containerId: defaultContainerId,\n              autoClose: false,\n            })\n          }\n          return\n        } else if (persistent) {\n          errorMessage = errorMessages.PERSISTENT({ message, title }, () =>\n            removeToast(id),\n          )\n        } else {\n          errorMessage = errorMessages.DEFAULT(\n            message,\n            () => removeToast(id),\n            title,\n          )\n        }\n        const toastId: ReturnType<typeof riToast> = riToast(errorMessage, {\n          variant: riToast.Variant.Danger,\n          toastId: id,\n          containerId: defaultContainerId,\n          autoClose: persistent ? false : undefined,\n        })\n        toastIdsRef.current.set(id, toastId)\n      },\n    )\n\n  useEffect(() => {\n    showErrorsToasts(errorsData)\n  }, [errorsData])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/hooks/useInfiniteNotifications.ts",
    "content": "import { useEffect, useMemo, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { InfiniteMessage } from 'uiSrc/slices/interfaces'\nimport { infiniteNotificationsSelector } from 'uiSrc/slices/app/notifications'\nimport { showOAuthProgress } from 'uiSrc/slices/oauth/cloud'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { InfiniteMessagesIds } from '../components'\nimport { defaultContainerId, ONE_HOUR } from '../constants'\n\nconst DISPLAY_THROTTLE = 3_000 // 3 seconds - minimum time between displaying notifications\nconst AUTO_DISMISS_DELAY = 5_000 // 5 seconds - wait before auto-dismissing when no notifications remain\n\nconst showNotification = (notification: InfiniteMessage) => {\n  if (!notification) {\n    return\n  }\n\n  // Show latest notification\n  return riToast(notification, {\n    containerId: defaultContainerId,\n    autoClose: ONE_HOUR,\n  })\n}\n\nexport const useInfiniteNotifications = () => {\n  const notifications = useSelector(infiniteNotificationsSelector)\n  const dispatch = useDispatch()\n  const notificationsData = useMemo(() => {\n    return notifications.map(\n      ({\n        id,\n        message,\n        description,\n        actions,\n        className = '',\n        variant,\n        customIcon,\n        showCloseButton = true,\n        onClose: onCloseCallback,\n      }: InfiniteMessage) => {\n        return {\n          id,\n          message,\n          description,\n          actions,\n          className,\n          variant,\n          customIcon,\n          showCloseButton,\n          onClose: () => {\n            switch (id) {\n              case InfiniteMessagesIds.oAuthProgress:\n                dispatch(showOAuthProgress(false))\n                break\n              case InfiniteMessagesIds.databaseExists:\n                sendEventTelemetry({\n                  event:\n                    TelemetryEvent.CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED,\n                })\n                break\n              case InfiniteMessagesIds.subscriptionExists:\n                sendEventTelemetry({\n                  event:\n                    TelemetryEvent.CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED,\n                })\n                break\n              case InfiniteMessagesIds.appUpdateAvailable:\n                sendEventTelemetry({\n                  event: TelemetryEvent.UPDATE_NOTIFICATION_CLOSED,\n                })\n                break\n              default:\n                break\n            }\n            onCloseCallback?.()\n          },\n        }\n      },\n    )\n  }, [notifications])\n  const lastDisplayTimeRef = useRef<number>(0)\n  const currentToastRef = useRef<ReturnType<typeof riToast> | null>(null)\n  const pendingNotificationRef = useRef<InfiniteMessage | null>(null)\n  const displayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const autoDismissTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n    null,\n  )\n  const previousNotificationsLengthRef = useRef<number>(0)\n\n  const displayNotification = (notification: InfiniteMessage) => {\n    // Dismiss current notification if any\n    if (currentToastRef.current) {\n      riToast.dismiss(currentToastRef.current)\n      currentToastRef.current = null\n    }\n\n    // Show new notification\n    const toastId = showNotification(notification)\n    if (toastId) {\n      currentToastRef.current = toastId\n      lastDisplayTimeRef.current = Date.now()\n    }\n  }\n\n  const scheduleNextDisplay = () => {\n    if (pendingNotificationRef.current) {\n      const timeSinceLastDisplay = Date.now() - lastDisplayTimeRef.current\n      const delay = Math.max(0, DISPLAY_THROTTLE - timeSinceLastDisplay)\n\n      if (displayTimeoutRef.current) {\n        clearTimeout(displayTimeoutRef.current)\n      }\n\n      displayTimeoutRef.current = setTimeout(() => {\n        if (pendingNotificationRef.current) {\n          displayNotification(pendingNotificationRef.current)\n          pendingNotificationRef.current = null\n          displayTimeoutRef.current = null\n        }\n      }, delay)\n    }\n  }\n\n  const clearAutoDismiss = () => {\n    if (autoDismissTimeoutRef.current) {\n      clearTimeout(autoDismissTimeoutRef.current)\n      autoDismissTimeoutRef.current = null\n    }\n  }\n\n  const scheduleAutoDismiss = () => {\n    clearAutoDismiss()\n\n    autoDismissTimeoutRef.current = setTimeout(() => {\n      if (currentToastRef.current) {\n        riToast.dismiss(currentToastRef.current)\n        currentToastRef.current = null\n      }\n      autoDismissTimeoutRef.current = null\n    }, AUTO_DISMISS_DELAY)\n  }\n\n  useEffect(() => {\n    const previousLength = previousNotificationsLengthRef.current\n    const currentLength = notificationsData.length\n\n    // Check if notifications became empty (after a removal)\n    if (previousLength > 0 && currentLength === 0) {\n      // Notifications were removed, clear any pending notifications and schedule auto-dismiss\n      pendingNotificationRef.current = null\n      if (displayTimeoutRef.current) {\n        clearTimeout(displayTimeoutRef.current)\n        displayTimeoutRef.current = null\n      }\n      scheduleAutoDismiss()\n    } else if (currentLength > 0) {\n      // There are notifications, cancel auto-dismiss if scheduled\n      clearAutoDismiss()\n\n      // Get the latest notification (last in array) - use transformed data\n      const latestNotification = notificationsData[notificationsData.length - 1]\n\n      // Check if we should display immediately or defer\n      const timeSinceLastDisplay = Date.now() - lastDisplayTimeRef.current\n\n      if (timeSinceLastDisplay >= DISPLAY_THROTTLE) {\n        // Enough time has passed, display immediately\n        displayNotification(latestNotification)\n        pendingNotificationRef.current = null\n\n        // Clear any pending display timeout\n        if (displayTimeoutRef.current) {\n          clearTimeout(displayTimeoutRef.current)\n          displayTimeoutRef.current = null\n        }\n      } else {\n        // Not enough time has passed, defer the display\n        pendingNotificationRef.current = latestNotification\n        scheduleNextDisplay()\n      }\n    }\n\n    // Update previous length for next render\n    previousNotificationsLengthRef.current = currentLength\n\n    // Cleanup on unmount\n    return () => {\n      if (displayTimeoutRef.current) {\n        clearTimeout(displayTimeoutRef.current)\n      }\n      if (autoDismissTimeoutRef.current) {\n        clearTimeout(autoDismissTimeoutRef.current)\n      }\n    }\n  }, [notificationsData])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/hooks/useMessageNotifications.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { ToastVariant } from '@redis-ui/components'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { messagesSelector, removeMessage } from 'uiSrc/slices/app/notifications'\nimport { IMessage } from 'uiSrc/slices/interfaces'\nimport { setReleaseNotesViewed } from 'uiSrc/slices/app/info'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { defaultContainerId } from '../constants'\n\nconst getDescriptionText = (\n  text: string | JSX.Element | JSX.Element[],\n  variant: ToastVariant = 'success',\n) => <ColorText color={variant}>{text}</ColorText>\n\nexport const useMessageNotifications = () => {\n  const messagesData = useSelector(messagesSelector)\n\n  const dispatch = useDispatch()\n  const toastIdsRef = useRef(new Map<string, number | string>())\n  const removeToast = (id: string) => {\n    if (toastIdsRef.current.has(id)) {\n      riToast.dismiss(toastIdsRef.current.get(id))\n      toastIdsRef.current.delete(id)\n    }\n    dispatch(removeMessage(id))\n  }\n  const onSubmitNotification = (id: string, group?: string) => {\n    if (group === 'upgrade') {\n      dispatch(setReleaseNotesViewed(true))\n    }\n    dispatch(removeMessage(id))\n  }\n\n  const showToasts = (data: IMessage[]) =>\n    data.forEach(\n      ({\n        id = '',\n        title = '',\n        message = '',\n        variant,\n        showCloseButton = true,\n        actions,\n        className,\n        group,\n      }) => {\n        const handleClose = () => {\n          onSubmitNotification(id, group)\n          removeToast(id)\n        }\n        if (toastIdsRef.current.has(id)) {\n          removeToast(id)\n          return\n        }\n\n        const toastVariant = variant ?? riToast.Variant.Success\n        const toastId = riToast(\n          {\n            className,\n            message: title,\n            description: getDescriptionText(message, toastVariant),\n            actions: actions ?? {\n              primary: {\n                closes: true,\n                label: 'OK',\n                onClick: handleClose,\n              },\n            },\n            showCloseButton,\n          },\n          {\n            variant: toastVariant,\n            toastId: id,\n            containerId: defaultContainerId,\n          },\n        )\n        toastIdsRef.current.set(id, toastId)\n      },\n    )\n\n  useEffect(() => {\n    showToasts(messagesData)\n  }, [messagesData])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/index.ts",
    "content": "import Notifications from './Notifications'\n\nexport default Notifications\n"
  },
  {
    "path": "redisinsight/ui/src/components/notifications/success-messages.tsx",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport {\n  IBulkActionOverview,\n  RedisResponseBuffer,\n} from 'uiSrc/slices/interfaces'\nimport {\n  bufferToString,\n  formatLongName,\n  formatNameShort,\n  Maybe,\n  millisecondsFormat,\n} from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { getIndexDisplayName } from 'uiSrc/pages/vector-search/utils'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\nconst Li = styled.li<React.HTMLAttributes<HTMLLIElement>>`\n  padding-bottom: 10px;\n\n  &:first-of-type {\n    padding-top: 10px;\n  }\n`\n\nexport default {\n  ADDED_NEW_INSTANCE: (instanceName: string) => ({\n    title: 'Database has been added',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(instanceName)}\n        </Text>{' '}\n        has been added to Redis Insight.\n      </Text>\n    ),\n  }),\n  ADDED_NEW_RDI_INSTANCE: (instanceName: string) => ({\n    title: 'Instance has been added',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(instanceName)}\n        </Text>{' '}\n        has been added to RedisInsight.\n      </Text>\n    ),\n  }),\n  DELETE_INSTANCE: (instanceName: string) => ({\n    title: 'Database has been deleted',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(instanceName)}\n        </Text>{' '}\n        has been deleted from Redis Insight.\n      </Text>\n    ),\n  }),\n  DELETE_RDI_INSTANCE: (instanceName: string) => ({\n    title: 'Instance has been deleted',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(instanceName)}\n        </Text>{' '}\n        has been deleted from RedisInsight.\n      </Text>\n    ),\n  }),\n  DELETE_INSTANCES: (instanceNames: Maybe<string>[]) => {\n    const limitShowRemovedInstances = 10\n    return {\n      title: 'Databases have been deleted',\n      message: (\n        <>\n          <Text component=\"span\">\n            <Text variant=\"semiBold\" component=\"span\">\n              {instanceNames.length}\n            </Text>{' '}\n            databases have been deleted from Redis Insight:\n          </Text>\n          <ul style={{ marginBottom: 0 }}>\n            {instanceNames.slice(0, limitShowRemovedInstances).map((el, i) => (\n              // eslint-disable-next-line react/no-array-index-key\n              <Li key={i}>\n                <Text component=\"div\" size=\"S\">\n                  {formatNameShort(el)}\n                </Text>\n              </Li>\n            ))}\n            {instanceNames.length >= limitShowRemovedInstances && <li>...</li>}\n          </ul>\n        </>\n      ),\n    }\n  },\n  DELETE_RDI_INSTANCES: (instanceNames: Maybe<string>[]) => {\n    const limitShowRemovedInstances = 10\n    return {\n      title: 'Instances have been deleted',\n      message: (\n        <>\n          <Text component=\"span\">\n            <Text variant=\"semiBold\" component=\"span\">\n              {instanceNames.length}\n            </Text>{' '}\n            instances have been deleted from RedisInsight:\n          </Text>\n          <ul style={{ marginBottom: 0 }}>\n            {instanceNames.slice(0, limitShowRemovedInstances).map((el, i) => (\n              // eslint-disable-next-line react/no-array-index-key\n              <Li key={i}>\n                <Text component=\"div\" size=\"S\">\n                  {formatNameShort(el)}\n                </Text>\n              </Li>\n            ))}\n            {instanceNames.length >= limitShowRemovedInstances && <li>...</li>}\n          </ul>\n        </>\n      ),\n    }\n  },\n  ADDED_NEW_KEY: (keyName: RedisResponseBuffer) => ({\n    title: 'Key has been added',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(bufferToString(keyName))}\n        </Text>{' '}\n        has been added.\n      </Text>\n    ),\n  }),\n  DELETED_KEY: (keyName: RedisResponseBuffer) => ({\n    title: 'Key has been deleted',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(bufferToString(keyName))}\n        </Text>{' '}\n        has been deleted.\n      </Text>\n    ),\n  }),\n  REMOVED_KEY_VALUE: (\n    keyName: RedisResponseBuffer,\n    keyValue: RedisResponseBuffer,\n    valueType: string,\n  ) => ({\n    title: (\n      <>\n        <span>{valueType}</span> has been removed\n      </>\n    ),\n    message: (\n      <>\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(bufferToString(keyValue))}\n        </Text>{' '}\n        has been removed from &nbsp;\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(bufferToString(keyName))}\n        </Text>\n      </>\n    ),\n  }),\n  REMOVED_LIST_ELEMENTS: (\n    keyName: RedisResponseBuffer,\n    numberOfElements: number,\n    listOfElements: RedisResponseBuffer[],\n  ) => {\n    const limitShowRemovedElements = 10\n    return {\n      title: 'Elements have been removed',\n      message: (\n        <>\n          <span>\n            {`${numberOfElements} Element(s) removed from ${formatNameShort(bufferToString(keyName))}:`}\n          </span>\n          <ul style={{ marginBottom: 0 }}>\n            {listOfElements.slice(0, limitShowRemovedElements).map((el, i) => (\n              // eslint-disable-next-line react/no-array-index-key\n              <Li key={i}>\n                <Text component=\"div\" size=\"S\">\n                  {formatNameShort(bufferToString(el))}\n                </Text>\n              </Li>\n            ))}\n            {listOfElements.length >= limitShowRemovedElements && <li>...</li>}\n          </ul>\n        </>\n      ),\n    }\n  },\n  INSTALLED_NEW_UPDATE: (\n    updateDownloadedVersion: string,\n    onClickLink?: () => void,\n  ) => ({\n    title: 'Application updated',\n    message: (\n      <>\n        <span>{`Your application has been updated to ${updateDownloadedVersion}. Find more information in `}</span>\n        <a\n          href={EXTERNAL_LINKS.releaseNotes}\n          onClick={() => onClickLink?.()}\n          className=\"link-underline\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n        >\n          Release Notes.\n        </a>\n      </>\n    ),\n    group: 'upgrade',\n  }),\n  // only one message is being processed at the moment\n  MESSAGE_ACTION: (message: string, actionName: string) => ({\n    title: <>Message has been {actionName}</>,\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {message}\n        </Text>{' '}\n        has been successfully {actionName}.\n      </Text>\n    ),\n  }),\n  NO_CLAIMED_MESSAGES: () => ({\n    title: 'No messages claimed',\n    message: 'No messages exceed the minimum idle time.',\n  }),\n  CREATE_INDEX: () => ({\n    title: 'Index has been created',\n    message: 'Open the list of indexes to see it.',\n  }),\n  DELETE_INDEX: (indexName: string) => ({\n    title: 'Index has been deleted',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(getIndexDisplayName(indexName))}\n        </Text>{' '}\n        has been deleted.\n      </Text>\n    ),\n  }),\n  TEST_CONNECTION: () => ({\n    title: 'Connection is successful',\n  }),\n  UPLOAD_DATA_BULK: (data?: IBulkActionOverview, fileName?: string) => {\n    const { processed = 0, succeed = 0, failed = 0 } = data?.summary ?? {}\n    return {\n      title: (\n        <>\n          <Text component=\"span\" variant=\"semiBold\">\n            Action completed\n          </Text>\n          {fileName ? (\n            <>\n              <Spacer size=\"s\" />\n              <Text component=\"span\">Commands executed from file:</Text>\n              <Text component=\"span\">{formatLongName(fileName, 34, 5)}</Text>\n            </>\n          ) : null}\n          <Spacer size=\"m\" />\n        </>\n      ),\n      message: (\n        <Row align=\"start\" gap=\"xl\">\n          <FlexItem>\n            <Text>{numberWithSpaces(processed)}</Text>\n            <Text size=\"xs\">Commands Processed</Text>\n          </FlexItem>\n          <FlexItem>\n            <Text>{numberWithSpaces(succeed)}</Text>\n            <Text size=\"xs\">Success</Text>\n          </FlexItem>\n          <FlexItem>\n            <Text>{numberWithSpaces(failed)}</Text>\n            <Text size=\"xs\">Errors</Text>\n          </FlexItem>\n          <FlexItem>\n            <Text>\n              {millisecondsFormat(data?.duration || 0, 'H:mm:ss.SSS')}\n            </Text>\n            <Text size=\"xs\">Time Taken</Text>\n          </FlexItem>\n        </Row>\n      ),\n      className: 'dynamic',\n      actions: {}, // Make sure we don't show the default OK button\n    }\n  },\n  DELETE_LIBRARY: (libraryName: string) => ({\n    title: 'Library has been deleted',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(libraryName)}\n        </Text>{' '}\n        has been deleted.\n      </Text>\n    ),\n  }),\n  ADD_LIBRARY: (libraryName: string) => ({\n    title: 'Library has been added',\n    message: (\n      <Text component=\"span\">\n        <Text variant=\"semiBold\" component=\"span\">\n          {formatNameShort(libraryName)}\n        </Text>{' '}\n        has been added.\n      </Text>\n    ),\n  }),\n  REMOVED_ALL_CAPI_KEYS: () => ({\n    title: 'API keys have been removed',\n    message: 'All API keys have been removed from Redis Insight.',\n  }),\n  REMOVED_CAPI_KEY: (name: string) => ({\n    title: 'API Key has been removed',\n    message: `${formatNameShort(name)} has been removed from Redis Insight.`,\n  }),\n  DATABASE_ALREADY_EXISTS: () => ({\n    title: 'Database already exists',\n    message: 'No new database connections have been added.',\n  }),\n  SUCCESS_RESET_PIPELINE: () => ({\n    title: 'Pipeline has been reset',\n    message:\n      'The RDI pipeline has been reset, consider flushing the target Redis database.',\n  }),\n  SUCCESS_TAGS_UPDATED: () => ({\n    title: 'Tags updated successfully.',\n  }),\n  AZURE_AUTH_SUCCESS: (username: string) => ({\n    title: 'Azure authentication successful',\n    message: (\n      <Text component=\"span\">\n        Signed in as{' '}\n        <Text variant=\"semiBold\" component=\"span\">\n          {username}\n        </Text>\n      </Text>\n    ),\n  }),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/index.ts",
    "content": "import OAuthSsoDialog from './oauth-sso-dialog'\nimport OAuthSsoHandlerDialog from './oauth-sso-handler-dialog'\nimport OAuthSelectAccountDialog from './oauth-select-account-dialog/OAuthSelectAccountDialog'\nimport OAuthConnectFreeDb from './oauth-connect-free-db'\nimport OAuthJobs from './oauth-jobs'\nimport OAuthSelectPlan from './oauth-select-plan'\nimport OAuthUserProfile from './oauth-user-profile'\n\nexport {\n  OAuthSsoDialog,\n  OAuthSsoHandlerDialog,\n  OAuthSelectAccountDialog,\n  OAuthConnectFreeDb,\n  OAuthJobs,\n  OAuthSelectPlan,\n  OAuthUserProfile,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { cleanup, mockedStore, render, userEvent } from 'uiSrc/utils/test-utils'\nimport {\n  getRedisModulesSummary,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  freeInstancesSelector,\n  setDefaultInstance,\n  setDefaultInstanceSuccess,\n} from 'uiSrc/slices/instances/instances'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { setCapability } from 'uiSrc/slices/app/context'\nimport { MOCK_ADDITIONAL_INFO } from 'uiSrc/mocks/data/instances'\nimport OAuthConnectFreeDb from './OAuthConnectFreeDb'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    source: 'source',\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  freeInstancesSelector: jest.fn().mockReturnValue([\n    {\n      id: 'instanceId',\n    },\n  ]),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OAuthConnectFreeDb', () => {\n  it('should render if there is a free cloud db', () => {\n    const { queryByTestId } = render(<OAuthConnectFreeDb />)\n    expect(queryByTestId('connect-free-db-btn')).toBeInTheDocument()\n  })\n\n  it('should send telemetry after click on connect btn', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const { queryByTestId } = render(<OAuthConnectFreeDb id=\"providedId\" />)\n\n    await userEvent.click(\n      queryByTestId('connect-free-db-btn') as HTMLButtonElement,\n    )\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE,\n      eventData: {\n        databaseId: 'providedId',\n        provider: undefined,\n        source: OAuthSocialSource.ListOfDatabases,\n        ...getRedisModulesSummary(),\n        ...MOCK_ADDITIONAL_INFO,\n      },\n    })\n\n    const expectedActions = [\n      setCapability({\n        source: OAuthSocialSource.ListOfDatabases,\n        tutorialPopoverShown: false,\n      }),\n      setDefaultInstance(),\n      setDefaultInstanceSuccess(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should not render if there is no free cloud db', () => {\n    ;(freeInstancesSelector as jest.Mock).mockReturnValue(null)\n\n    const { queryByTestId } = render(<OAuthConnectFreeDb />)\n    expect(queryByTestId('connect-free-db-btn')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-connect-free-db/OAuthConnectFreeDb.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { useLocation } from 'react-router-dom'\nimport cx from 'classnames'\nimport {\n  TelemetryEvent,\n  getRedisModulesSummary,\n  sendEventTelemetry,\n  getRedisInfoSummary,\n} from 'uiSrc/telemetry'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport {\n  checkConnectToInstanceAction,\n  connectedInstanceSelector,\n  freeInstancesSelector,\n  instancesSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { openNewWindowDatabase } from 'uiSrc/utils'\nimport { Pages } from 'uiSrc/constants'\nimport { setCapability } from 'uiSrc/slices/app/context'\n\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport styles from './styles.module.scss'\n\ninterface Props {\n  id?: string\n  source?: OAuthSocialSource | string\n  onSuccessClick?: () => void\n  className?: string\n}\n\nconst OAuthConnectFreeDb = ({\n  id = '',\n  source = OAuthSocialSource.ListOfDatabases,\n  onSuccessClick,\n  className,\n}: Props) => {\n  const { loading } = useSelector(instancesSelector) ?? {}\n  const { modules, provider } = useSelector(connectedInstanceSelector) ?? {}\n  const [firstFreeInstance] = useSelector(freeInstancesSelector) ?? []\n\n  const targetDatabaseId = id || firstFreeInstance?.id\n\n  const dispatch = useDispatch()\n  const { search } = useLocation()\n\n  if (!targetDatabaseId) {\n    return null\n  }\n\n  const sendTelemetry = async () => {\n    const modulesSummary = getRedisModulesSummary(modules)\n    const infoData = await getRedisInfoSummary(targetDatabaseId)\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE,\n      eventData: {\n        databaseId: targetDatabaseId,\n        provider,\n        source,\n        ...modulesSummary,\n        ...infoData,\n      },\n    })\n  }\n\n  const connectToInstanceSuccess = () => {\n    onSuccessClick?.()\n\n    openNewWindowDatabase(Pages.browser(targetDatabaseId) + search)\n  }\n\n  const handleCheckConnectToInstance = async () => {\n    await sendTelemetry()\n    dispatch(setCapability({ source, tutorialPopoverShown: false }))\n    dispatch(\n      checkConnectToInstanceAction(\n        targetDatabaseId,\n        connectToInstanceSuccess,\n        () => {},\n        false,\n      ),\n    )\n  }\n\n  return (\n    <PrimaryButton\n      size=\"m\"\n      disabled={loading}\n      loading={loading}\n      onClick={handleCheckConnectToInstance}\n      className={cx(styles.btn, className)}\n      data-testid=\"connect-free-db-btn\"\n    >\n      Launch database\n    </PrimaryButton>\n  )\n}\n\nexport default OAuthConnectFreeDb\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-connect-free-db/index.ts",
    "content": "import OAuthConnectFreeDb from './OAuthConnectFreeDb'\n\nexport default OAuthConnectFreeDb\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-connect-free-db/styles.module.scss",
    "content": ".btn {\n  position: relative;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-jobs/OAuthJobs.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { useSelector } from 'react-redux'\nimport { AxiosError } from 'axios'\nimport {\n  cleanup,\n  clearStoreActions,\n  mockedStore,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport {\n  logoutUser,\n  oauthCloudJobSelector,\n  setJob,\n  setSocialDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport {\n  CloudJobStatus,\n  CloudJobName,\n  CloudJobStep,\n} from 'uiSrc/electron/constants'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { RootState } from 'uiSrc/slices/store'\nimport { loadInstances } from 'uiSrc/slices/instances/instances'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport OAuthJobs from './OAuthJobs'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudJobSelector: jest.fn().mockReturnValue({\n    status: '',\n    step: 'subscription',\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OAuthJobs', () => {\n  beforeEach(() => {\n    const state = store.getState() as RootState\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: any) => any) =>\n        callback({\n          ...state,\n        }),\n    )\n  })\n\n  it('should render', () => {\n    expect(render(<OAuthJobs />)).toBeTruthy()\n  })\n\n  it('should call addInfiniteNotification when status changed to \"running\"', async () => {\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Running,\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addInfiniteNotification(INFINITE_MESSAGES.PENDING_CREATE_DB),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should not call addInfiniteNotification the second time when status \"running\"', async () => {\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: '',\n    }))\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Running,\n    }))\n\n    rerender(<OAuthJobs />)\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Running,\n      id: '123',\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addInfiniteNotification(INFINITE_MESSAGES.PENDING_CREATE_DB),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call loadInstances and setJob when status changed to \"finished\" without error', async () => {\n    const resourceId = '123123'\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Finished,\n      step: CloudJobStep.Database,\n      result: { resourceId },\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Database),\n      ),\n      loadInstances(),\n      setJob({ id: '', name: CloudJobName.CreateFreeDatabase, status: '' }),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call loadInstances and setJob when status changed to \"finished\" with error', async () => {\n    const error = 'error'\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: '',\n    }))\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Failed,\n      error,\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addErrorNotification({ response: { data: error } } as AxiosError),\n      setSSOFlow(),\n      setSocialDialogState(null),\n      removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call addInfiniteNotification and removeInfiniteNotification when errorCode is 11_108', async () => {\n    const mockDatabaseId = '123'\n    const error = {\n      errorCode: CustomErrorCodes.CloudDatabaseAlreadyExistsFree,\n      resource: {\n        databaseId: mockDatabaseId,\n      },\n    }\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: '',\n    }))\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Failed,\n      error,\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addInfiniteNotification(INFINITE_MESSAGES.DATABASE_EXISTS()),\n      setSSOFlow(),\n      setSocialDialogState(null),\n      removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call addInfiniteNotification and removeInfiniteNotification when errorCode is 11_114', async () => {\n    const mockDatabaseId = '123'\n    const error = {\n      errorCode: CustomErrorCodes.CloudSubscriptionAlreadyExistsFree,\n      resource: {\n        databaseId: mockDatabaseId,\n      },\n    }\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: '',\n    }))\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Failed,\n      error,\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addInfiniteNotification(INFINITE_MESSAGES.DATABASE_EXISTS()),\n      setSSOFlow(),\n      setSocialDialogState(null),\n      removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call addInfiniteNotification and removeInfiniteNotification when errorCode is 11_115', async () => {\n    const error = {\n      errorCode: CustomErrorCodes.CloudDatabaseImportForbidden,\n    }\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: '',\n    }))\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Failed,\n      error,\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      addInfiniteNotification(INFINITE_MESSAGES.DATABASE_IMPORT_FORBIDDEN()),\n      setSSOFlow(),\n      setSocialDialogState(null),\n      removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call logoutUser when statusCode is 401', async () => {\n    const mockDatabaseId = '123'\n    const error = {\n      statusCode: 401,\n      errorCode: CustomErrorCodes.CloudSubscriptionAlreadyExistsFree,\n      resource: {\n        databaseId: mockDatabaseId,\n      },\n    }\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: '',\n    }))\n\n    const { rerender } = render(<OAuthJobs />)\n\n    ;(oauthCloudJobSelector as jest.Mock).mockImplementation(() => ({\n      status: CloudJobStatus.Failed,\n      error,\n    }))\n\n    rerender(<OAuthJobs />)\n\n    const expectedActions = [\n      logoutUser(),\n      setSSOFlow(),\n      addInfiniteNotification(INFINITE_MESSAGES.DATABASE_EXISTS()),\n      setSSOFlow(),\n      setSocialDialogState(null),\n      removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n    ]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-jobs/OAuthJobs.tsx",
    "content": "import { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { get } from 'lodash'\n\nimport {\n  CloudJobStatus,\n  CloudJobName,\n  CloudJobStep,\n} from 'uiSrc/electron/constants'\nimport { fetchInstancesAction } from 'uiSrc/slices/instances/instances'\nimport {\n  createFreeDbJob,\n  createFreeDbSuccess,\n  logoutUserAction,\n  oauthCloudJobSelector,\n  oauthCloudSelector,\n  setJob,\n  setSocialDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport { CloudImportDatabaseResources } from 'uiSrc/slices/interfaces/cloud'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { parseCustomError } from 'uiSrc/utils'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  ApiStatusCode,\n  BrowserStorageItem,\n  CustomErrorCodes,\n} from 'uiSrc/constants'\nimport { localStorageService } from 'uiSrc/services'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\n\nconst OAuthJobs = () => {\n  const {\n    status,\n    name: jobName,\n    error,\n    step,\n    result,\n  } = useSelector(oauthCloudJobSelector) ?? {}\n  const { showProgress } = useSelector(oauthCloudSelector)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  useEffect(() => {\n    switch (status) {\n      case CloudJobStatus.Running:\n        if (!showProgress) return\n\n        dispatch(\n          addInfiniteNotification(\n            INFINITE_MESSAGES.PENDING_CREATE_DB(step as CloudJobStep),\n          ),\n        )\n        break\n\n      case CloudJobStatus.Finished:\n        dispatch(\n          fetchInstancesAction(() =>\n            dispatch(createFreeDbSuccess(result, history, jobName)),\n          ),\n        )\n        dispatch(\n          setJob({\n            id: '',\n            name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n            status: '',\n          }),\n        )\n        localStorageService.remove(BrowserStorageItem.OAuthJobId)\n        break\n\n      case CloudJobStatus.Failed:\n        const errorCode = get(error, 'errorCode', 0) as CustomErrorCodes\n        const subscriptionId = get(error, 'resource.subscriptionId', 0)\n        const resources = get(\n          error,\n          'resource',\n          {},\n        ) as CloudImportDatabaseResources\n        const statusCode = get(error, 'statusCode', 0) as number\n\n        if (statusCode === ApiStatusCode.Unauthorized) {\n          dispatch(logoutUserAction())\n        }\n\n        // eslint-disable-next-line sonarjs/no-nested-switch\n        switch (errorCode) {\n          case CustomErrorCodes.CloudDatabaseAlreadyExistsFree:\n            dispatch(\n              addInfiniteNotification(\n                INFINITE_MESSAGES.DATABASE_EXISTS(\n                  () => importDatabase(resources),\n                  closeImportDatabase,\n                ),\n              ),\n            )\n\n            break\n\n          case CustomErrorCodes.CloudDatabaseImportForbidden:\n            sendEventTelemetry({\n              event: TelemetryEvent.CLOUD_IMPORT_DATABASE_FORBIDDEN,\n            })\n\n            dispatch(\n              addInfiniteNotification(\n                INFINITE_MESSAGES.DATABASE_IMPORT_FORBIDDEN(\n                  closeDatabaseImportForbidden,\n                ),\n              ),\n            )\n\n            break\n\n          case CustomErrorCodes.CloudSubscriptionAlreadyExistsFree:\n            dispatch(\n              addInfiniteNotification(\n                INFINITE_MESSAGES.SUBSCRIPTION_EXISTS(\n                  () => createFreeDatabase(subscriptionId),\n                  closeCreateFreeDatabase,\n                ),\n              ),\n            )\n            break\n\n          default:\n            const err = parseCustomError(error || '')\n            dispatch(addErrorNotification(err))\n            break\n        }\n\n        dispatch(setSSOFlow())\n        dispatch(setSocialDialogState(null))\n        dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress))\n        break\n\n      default:\n        break\n    }\n  }, [status, error, step, result, showProgress])\n\n  const importDatabase = (resources: CloudImportDatabaseResources) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_IMPORT_EXISTING_DATABASE,\n    })\n    dispatch(\n      createFreeDbJob({\n        name: CloudJobName.ImportFreeDatabase,\n        resources,\n        onSuccessAction: () => {\n          dispatch(\n            removeInfiniteNotification(InfiniteMessagesIds.databaseExists),\n          )\n          dispatch(\n            addInfiniteNotification(\n              INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n            ),\n          )\n        },\n      }),\n    )\n  }\n\n  const createFreeDatabase = (subscriptionId: number) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION,\n    })\n    dispatch(\n      createFreeDbJob({\n        name: CloudJobName.CreateFreeDatabase,\n        resources: { subscriptionId },\n        onSuccessAction: () => {\n          dispatch(\n            removeInfiniteNotification(InfiniteMessagesIds.subscriptionExists),\n          )\n          dispatch(\n            addInfiniteNotification(\n              INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n            ),\n          )\n        },\n      }),\n    )\n  }\n\n  const closeDatabaseImportForbidden = () => {\n    dispatch(setSSOFlow())\n    dispatch(\n      removeInfiniteNotification(InfiniteMessagesIds.databaseImportForbidden),\n    )\n  }\n\n  const closeImportDatabase = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED,\n    })\n    dispatch(setSSOFlow())\n    dispatch(removeInfiniteNotification(InfiniteMessagesIds.databaseExists))\n  }\n\n  const closeCreateFreeDatabase = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED,\n    })\n    dispatch(setSSOFlow())\n    dispatch(removeInfiniteNotification(InfiniteMessagesIds.subscriptionExists))\n  }\n\n  return null\n}\n\nexport default OAuthJobs\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-jobs/index.ts",
    "content": "import OAuthJobs from './OAuthJobs'\n\nexport default OAuthJobs\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-account-dialog/OAuthSelectAccountDialog.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  getUserInfo,\n  getUserInfoSuccess,\n  oauthCloudSelector,\n  oauthCloudUserDataSelector,\n  setSelectAccountDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport { apiService } from 'uiSrc/services'\nimport { loadSubscriptionsRedisCloud } from 'uiSrc/slices/instances/cloud'\nimport OAuthSelectAccountDialog from './OAuthSelectAccountDialog'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    source: 'source',\n    isOpenSelectAccountDialog: true,\n  }),\n  oauthCloudUserDataSelector: jest.fn().mockReturnValue({\n    id: 1,\n    accounts: [\n      { id: 1, name: 'name1' },\n      { id: 2, name: 'name2' },\n    ],\n  }),\n}))\njest.mock('uiSrc/slices/instances/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/cloud'),\n  cloudSelector: jest.fn().mockReturnValue({\n    ssoFlow: 'import',\n  }),\n}))\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OAuthSelectAccountDialog', () => {\n  it('should render', () => {\n    const { queryByTestId } = render(<OAuthSelectAccountDialog />)\n    expect(queryByTestId('oauth-select-account-dialog')).toBeInTheDocument()\n  })\n  it('should not render if account.length < 2', () => {\n    ;(oauthCloudUserDataSelector as jest.Mock).mockReturnValueOnce({\n      accounts: [{ id: 1, name: 'name1' }],\n    })\n    const { queryByTestId } = render(<OAuthSelectAccountDialog />)\n    expect(queryByTestId('oauth-select-account-dialog')).not.toBeInTheDocument()\n  })\n  it('should not render if isOpenSelectAccountDialog=false', () => {\n    ;(oauthCloudSelector as jest.Mock).mockReturnValueOnce({\n      isOpenSelectAccountDialog: false,\n    })\n    const { queryByTestId } = render(<OAuthSelectAccountDialog />)\n    expect(queryByTestId('oauth-select-account-dialog')).not.toBeInTheDocument()\n  })\n\n  it('should send telemetry after close modal', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const { queryByTestId } = render(<OAuthSelectAccountDialog />)\n\n    const closeEl = queryByTestId('oauth-select-account-dialog-close-btn')\n\n    fireEvent.click(closeEl as HTMLButtonElement)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_ACCOUNT_FORM_CLOSED,\n      eventData: {\n        accountsCount: 2,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('click on submit btn should call getUserInfo', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    apiService.put = jest.fn().mockResolvedValue({ status: 200 })\n\n    const { queryByTestId } = render(<OAuthSelectAccountDialog />)\n    const submitEl = queryByTestId('submit-oauth-select-account-dialog')\n\n    await act(() => {\n      fireEvent.click(submitEl as HTMLButtonElement)\n    })\n\n    const expectedActions = [\n      getUserInfo(),\n      getUserInfoSuccess(),\n      loadSubscriptionsRedisCloud(),\n      setSelectAccountDialogState(false),\n    ]\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_ACCOUNT_SELECTED,\n      eventData: {\n        accountsCount: 2,\n        action: 'import',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n  it('on error in activeAccount telemetry should be sent', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const { queryByTestId } = render(<OAuthSelectAccountDialog />)\n\n    const submitEl = queryByTestId('submit-oauth-select-account-dialog')\n\n    const errorMessage = 'error'\n    const responsePayload = {\n      response: {\n        status: 500,\n        data: { message: errorMessage },\n      },\n    }\n\n    apiService.put = jest.fn().mockRejectedValueOnce(responsePayload)\n\n    await act(() => {\n      fireEvent.click(submitEl as HTMLButtonElement)\n    })\n\n    expect(sendEventTelemetry).toBeCalledTimes(1)\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_ACCOUNT_FAILED,\n      eventData: {\n        error: errorMessage,\n        accountsCount: 2,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-account-dialog/OAuthSelectAccountDialog.tsx",
    "content": "import React, { useCallback } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useFormik } from 'formik'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  activateAccount,\n  createFreeDbJob,\n  fetchPlans,\n  oauthCloudPlanSelector,\n  oauthCloudSelector,\n  oauthCloudUserDataSelector,\n  oauthCloudUserSelector,\n  setSelectAccountDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  cloudSelector,\n  fetchSubscriptionsRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { CloudJobName, CloudJobStep } from 'uiSrc/electron/constants'\nimport { OAuthSocialAction } from 'uiSrc/slices/interfaces'\n\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport {\n  RiRadioGroupItemIndicator,\n  RiRadioGroupItemLabel,\n  RiRadioGroupItemRoot,\n  RiRadioGroupRoot,\n} from 'uiSrc/components/base/forms/radio-group/RadioGroup'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Modal } from 'uiSrc/components/base/display'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport styles from './styles.module.scss'\n\ninterface FormValues {\n  accountId: Nullable<string>\n}\n\nconst OAuthSelectAccountDialog = () => {\n  const { ssoFlow, isRecommendedSettings } = useSelector(cloudSelector)\n  const { accounts = [], currentAccountId } =\n    useSelector(oauthCloudUserDataSelector) ?? {}\n  const { isOpenSelectAccountDialog } = useSelector(oauthCloudSelector)\n  const { loading } = useSelector(oauthCloudUserSelector)\n  const { loading: plansLoadings } = useSelector(oauthCloudPlanSelector)\n\n  const isAutodiscoverySSO = ssoFlow === OAuthSocialAction.Import\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const initialValues = {\n    accountId: `${currentAccountId}`,\n  }\n\n  const formik = useFormik({\n    initialValues,\n    validateOnMount: true,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      onSubmit(values)\n    },\n  })\n\n  const onSubmit = ({ accountId }: FormValues) => {\n    dispatch(\n      activateAccount(\n        accountId || '',\n        onActivateAccountSuccess,\n        onActivateAccountFail,\n      ),\n    )\n  }\n\n  const onActivateAccountSuccess = useCallback(() => {\n    if (isAutodiscoverySSO) {\n      dispatch(\n        fetchSubscriptionsRedisCloud(\n          null,\n          true,\n          () => {\n            dispatch(\n              removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n            )\n            history.push(Pages.redisCloudSubscriptions)\n          },\n          () => {\n            dispatch(\n              removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n            )\n          },\n        ),\n      )\n      dispatch(setSelectAccountDialogState(false))\n    } else if (isRecommendedSettings) {\n      dispatch(\n        createFreeDbJob({\n          name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          resources: {\n            isRecommendedSettings,\n          },\n          onSuccessAction: () => {\n            dispatch(setSelectAccountDialogState(false))\n            dispatch(\n              addInfiniteNotification(\n                INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n              ),\n            )\n          },\n          onFailAction: () => {\n            dispatch(\n              removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n            )\n          },\n        }),\n      )\n    } else {\n      dispatch(fetchPlans())\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_ACCOUNT_SELECTED,\n      eventData: {\n        action: ssoFlow,\n        accountsCount: accounts.length,\n      },\n    })\n  }, [isAutodiscoverySSO, isRecommendedSettings, accounts])\n\n  const onActivateAccountFail = useCallback(\n    (error: string) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.CLOUD_SIGN_IN_ACCOUNT_FAILED,\n        eventData: {\n          error,\n          accountsCount: accounts.length,\n        },\n      })\n    },\n    [accounts],\n  )\n\n  const handleOnClose = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_ACCOUNT_FORM_CLOSED,\n      eventData: { accountsCount: accounts.length },\n    })\n\n    dispatch(setSelectAccountDialogState(false))\n  }, [accounts])\n\n  if (accounts.length < 2 || !isOpenSelectAccountDialog) return null\n\n  const handleChangeAccountIdFormat = (value: string) => {\n    formik.setFieldValue('accountId', value)\n  }\n\n  const radios = accounts.map(({ id, name = '' }) => ({\n    id: `${id}`,\n    label: (\n      <ColorText color=\"subdued\">\n        {name}\n        <ColorText color=\"accent\" style={{ paddingLeft: 6 }}>\n          {id}\n        </ColorText>\n      </ColorText>\n    ),\n  }))\n\n  return (\n    <Modal.Compose open>\n      <Modal.Content.Compose\n        className={styles.container}\n        data-testid=\"oauth-select-account-dialog\"\n      >\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={handleOnClose}\n          data-testid=\"oauth-select-account-dialog-close-btn\"\n        />\n        <Modal.Content.Header.Title>\n          Connect to Redis Cloud\n        </Modal.Content.Header.Title>\n        <Modal.Content.Body.Compose>\n          <section className={styles.content}>\n            <Text className={styles.subTitle}>\n              Select an account to connect to:\n            </Text>\n            <Spacer size=\"xl\" />\n            <RiRadioGroupRoot\n              value={formik.values.accountId ?? ''}\n              onChange={(id) => handleChangeAccountIdFormat(id)}\n            >\n              {radios.map(({ id, label }) => (\n                <RiRadioGroupItemRoot value={id} key={id}>\n                  <RiRadioGroupItemIndicator />\n                  <RiRadioGroupItemLabel>{label}</RiRadioGroupItemLabel>\n                </RiRadioGroupItemRoot>\n              ))}\n            </RiRadioGroupRoot>\n          </section>\n          <div className={styles.footer}>\n            <SecondaryButton\n              className={styles.button}\n              onClick={handleOnClose}\n              data-testid=\"close-oauth-select-account-dialog\"\n              aria-labelledby=\"close oauth select account dialog\"\n            >\n              Cancel\n            </SecondaryButton>\n            <PrimaryButton\n              disabled={loading || plansLoadings}\n              loading={loading || plansLoadings}\n              className={styles.button}\n              onClick={() => formik.handleSubmit()}\n              data-testid=\"submit-oauth-select-account-dialog\"\n              aria-labelledby=\"submit oauth select account dialog\"\n            >\n              Select account\n            </PrimaryButton>\n          </div>\n        </Modal.Content.Body.Compose>\n      </Modal.Content.Compose>\n    </Modal.Compose>\n  )\n}\n\nexport default OAuthSelectAccountDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-account-dialog/index.ts",
    "content": "import OAuthSelectAccountDialog from './OAuthSelectAccountDialog'\n\nexport default OAuthSelectAccountDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-account-dialog/styles.module.scss",
    "content": ".container {\n  width: 612px !important;\n  min-width: 612px !important;\n  padding: 40px;\n  background-color: var(--euiColorLightestShade);\n}\n\n.content {\n  @include eui.scrollBar;\n  overflow: auto;\n  max-height: 400px;\n}\n\n.title {\n  font-size: 28px;\n  font-style: normal;\n  font-weight: 500;\n  line-height: normal;\n  padding: 10px 0;\n}\n\n.subTitle {\n  color: var(--htmlColor) !important;\n}\n\n.radios {\n  padding-top: 24px;\n}\n\n.label {\n  color: var(--htmlColor) !important;\n  span {\n    padding-left: 6px;\n    color: var(--euiTextSubduedColor) !important;\n  }\n}\n\n.footer {\n  padding-top: 20px;\n  text-align: right;\n  padding-bottom: 4px;\n}\n\n.button {\n  margin-left: 8px;\n}\n\n:global {\n  .euiRadio {\n    padding-bottom: 4px;\n  }\n  .euiModal .euiModal__flex {\n    max-height: 100% !important;\n    border-radius: 8px;\n  }\n  .euiModalBody__overflow {\n    padding: 0 !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-plan/OAuthSelectPlan.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  within,\n} from 'uiSrc/utils/test-utils'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { addFreeDb, oauthCloudPlanSelector } from 'uiSrc/slices/oauth/cloud'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  MOCK_NO_TF_REGION,\n  MOCK_REGIONS,\n  MOCK_RS_PREVIEW_REGION,\n  MOCK_CUSTOM_REGIONS,\n} from 'uiSrc/constants/mocks/mock-sso'\nimport OAuthSelectPlan from './OAuthSelectPlan'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/oauth/cloud',\n  ).initialState\n  return {\n    ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n    oauthCloudSelector: jest.fn().mockReturnValue(defaultState),\n    oauthCloudPlanSelector: jest.fn().mockReturnValue({\n      isOpenDialog: true,\n      data: [],\n    }),\n  }\n})\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: true,\n      data: {\n        selectPlan: {\n          components: {\n            redisStackPreview: [\n              {\n                provider: 'AWS',\n                regions: ['us-east-2', 'ap-southeast-1', 'sa-east-1'],\n              },\n              {\n                provider: 'GCP',\n                regions: ['asia-northeast1', 'europe-west1', 'us-central1'],\n              },\n            ],\n          },\n        },\n      },\n    },\n  }),\n}))\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OAuthSelectPlan', () => {\n  beforeEach(() => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValue({\n      isOpenDialog: true,\n      data: MOCK_REGIONS,\n    })\n  })\n\n  it('should render', () => {\n    const { queryByTestId } = render(<OAuthSelectPlan />)\n    expect(queryByTestId('oauth-select-plan-dialog')).toBeInTheDocument()\n  })\n  it('should not render if isOpenDialog=false', () => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValueOnce({\n      isOpenDialog: false,\n    })\n    const { queryByTestId } = render(<OAuthSelectPlan />)\n    expect(queryByTestId('oauth-select-plan-dialog')).not.toBeInTheDocument()\n  })\n\n  it('should send telemetry after close modal', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const { queryByTestId } = render(<OAuthSelectPlan />)\n\n    const closeEl = queryByTestId('oauth-select-plan-dialog-close-btn')\n\n    fireEvent.click(closeEl as HTMLButtonElement)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_PROVIDER_FORM_CLOSED,\n    })\n  })\n\n  it('should call addFreeDb after click on submit', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const { queryByTestId } = render(<OAuthSelectPlan />)\n\n    const submitEl = queryByTestId('submit-oauth-select-plan-dialog')\n\n    fireEvent.click(submitEl as HTMLButtonElement)\n\n    const expectedActions = [addFreeDb()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('region with RS should be selected by default', async () => {\n    const container = render(<OAuthSelectPlan />)\n\n    const { queryByTestId } = within(\n      container.queryByTestId('select-oauth-region') as HTMLElement,\n    )\n    const tfIconEl = queryByTestId(/rs-text-/)\n\n    expect(tfIconEl).toBeInTheDocument()\n  })\n\n  it('Should display region with RS preview text', async () => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValue({\n      isOpenDialog: true,\n      data: [MOCK_RS_PREVIEW_REGION],\n    })\n\n    const container = render(<OAuthSelectPlan />)\n\n    const { queryByTestId } = within(\n      container.queryByTestId('select-oauth-region') as HTMLElement,\n    )\n    const rsTextEl = queryByTestId(/rs-text-/)\n\n    expect(rsTextEl).toBeInTheDocument()\n  })\n\n  it('should be selected first region by default', async () => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValue({\n      isOpenDialog: true,\n      data: MOCK_CUSTOM_REGIONS,\n    })\n\n    const container = render(<OAuthSelectPlan />)\n\n    const { queryByTestId } = within(\n      container.queryByTestId('select-oauth-region') as HTMLElement,\n    )\n    const selectedEl = queryByTestId('option-custom-1')\n\n    expect(selectedEl).toBeInTheDocument()\n  })\n\n  it('should be selected us-east-2 region by default', async () => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValue({\n      isOpenDialog: true,\n      data: [MOCK_NO_TF_REGION, MOCK_RS_PREVIEW_REGION, ...MOCK_CUSTOM_REGIONS],\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({})\n\n    const container = render(<OAuthSelectPlan />)\n\n    const { queryByTestId } = within(\n      container.queryByTestId('select-oauth-region') as HTMLElement,\n    )\n    const selectedEl = queryByTestId('option-us-east-2')\n\n    expect(selectedEl).toBeInTheDocument()\n  })\n\n  it('Should select region with RS preview text by default', async () => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValue({\n      isOpenDialog: true,\n      data: [MOCK_RS_PREVIEW_REGION, ...MOCK_CUSTOM_REGIONS],\n    })\n\n    const container = render(<OAuthSelectPlan />)\n\n    const { queryByTestId } = within(\n      container.queryByTestId('select-oauth-region') as HTMLElement,\n    )\n    const rsTextEl = queryByTestId(/rs-text-/)\n\n    expect(rsTextEl).toBeInTheDocument()\n  })\n\n  it('should display text if regions is no available on this venodor', async () => {\n    ;(oauthCloudPlanSelector as jest.Mock).mockReturnValue({\n      isOpenDialog: true,\n      data: [],\n    })\n\n    const { queryByTestId } = render(<OAuthSelectPlan />)\n    const selectDescriptionEl = queryByTestId(\n      'select-region-select-description',\n    )\n\n    expect(selectDescriptionEl).toBeInTheDocument()\n    expect(selectDescriptionEl).toHaveTextContent(\n      'No regions available, try another vendor.',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-plan/OAuthSelectPlan.styles.ts",
    "content": "import styled from 'styled-components'\nimport { BoxSelectionGroup } from '@redis-ui/components'\n\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\n\nexport const StyledModalContentBody = styled.section`\n  width: 575px !important;\n  min-width: 575px !important;\n  padding: 16px;\n  text-align: center;\n`\n\nexport const StyledSubTitle = styled(Text)`\n  padding: 0 40px;\n`\n\nexport const StyledProvidersSection = styled(FlexGroup)`\n  width: 100%;\n  padding: 30px 45px 22px;\n`\n\nexport const StyledProvidersSelectionGroup = styled(BoxSelectionGroup)`\n  min-height: 68px;\n\n  svg {\n    width: 28px;\n    height: initial;\n  }\n\n  p {\n    font-size: 1.2rem;\n  }\n`\n\nexport const StyledRegion = styled.section`\n  padding: 2px 45px;\n  text-align: left;\n`\n\nexport const StyledRegionName = styled(ColorText)`\n  padding-left: 4px;\n`\n\nexport const StyledRegionSelectDescription = styled(Text)`\n  padding-top: 10px;\n`\n\nexport const StyledFooter = styled.footer`\n  width: 100%;\n  padding: 32px 46px 0 46px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-plan/OAuthSelectPlan.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { toNumber, filter, get, find, first } from 'lodash'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  createFreeDbJob,\n  oauthCloudPlanSelector,\n  setIsOpenSelectPlanDialog,\n  setSocialDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { addInfiniteNotification } from 'uiSrc/slices/app/notifications'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { CloudJobName, CloudJobStep } from 'uiSrc/electron/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { Region } from 'uiSrc/slices/interfaces'\n\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { ColorText, Text, Title } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { Modal } from 'uiSrc/components/base/display'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { CloudSubscriptionPlanResponse } from 'apiSrc/modules/cloud/subscription/dto'\nimport { OAuthProvider, OAuthProviders } from './constants'\nimport {\n  StyledFooter,\n  StyledModalContentBody,\n  StyledProvidersSection,\n  StyledProvidersSelectionGroup,\n  StyledRegion,\n  StyledRegionName,\n  StyledRegionSelectDescription,\n  StyledSubTitle,\n} from './OAuthSelectPlan.styles'\nimport { BoxSelectionGroupBox, CountryFlag } from '@redis-ui/components'\n\nexport const DEFAULT_REGIONS = ['us-east-2', 'asia-northeast1']\nexport const DEFAULT_PROVIDER = OAuthProvider.AWS\n\nconst getProviderRegions = (regions: Region[], provider: OAuthProvider) =>\n  (find(regions, { provider }) || {}).regions || []\n\nconst oAuthProvidersBoxes: BoxSelectionGroupBox<OAuthProvider>[] =\n  OAuthProviders.map(({ id, label, icon }) => ({\n    value: id,\n    label,\n    icon: () => <RiIcon type={icon} size=\"XL\" />,\n  }))\n\nconst OAuthSelectPlan = () => {\n  const {\n    isOpenDialog,\n    data: plansInit = [],\n    loading,\n  } = useSelector(oauthCloudPlanSelector)\n  const { [FeatureFlags.cloudSso]: cloudSsoFeature = {} } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const rsRegions: Region[] = get(\n    cloudSsoFeature,\n    'data.selectPlan.components.redisStackPreview',\n    [],\n  )\n\n  const [plans, setPlans] = useState(plansInit || [])\n  const [planIdSelected, setPlanIdSelected] = useState('')\n  const [providerSelected, setProviderSelected] =\n    useState<OAuthProvider>(DEFAULT_PROVIDER)\n  const [rsProviderRegions, setRsProviderRegions] = useState(\n    getProviderRegions(rsRegions, providerSelected as OAuthProvider),\n  )\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setRsProviderRegions(\n      getProviderRegions(rsRegions, providerSelected as OAuthProvider),\n    )\n  }, [providerSelected, plansInit])\n\n  useEffect(() => {\n    if (!plansInit.length) {\n      return\n    }\n\n    const filteredPlans = filter(plansInit, {\n      provider: providerSelected,\n    }).sort(\n      (a, b) =>\n        (a?.details?.displayOrder || 0) - (b?.details?.displayOrder || 0),\n    )\n\n    const defaultPlan = filteredPlans.find(({ region = '' }) =>\n      DEFAULT_REGIONS.includes(region),\n    )\n    const rsPreviewPlan = filteredPlans.find(({ region = '' }) =>\n      rsProviderRegions?.includes(region),\n    )\n    const planId =\n      (\n        defaultPlan ||\n        rsPreviewPlan ||\n        first(filteredPlans) ||\n        {}\n      ).id?.toString() || ''\n\n    setPlans(filteredPlans)\n    setPlanIdSelected(planId)\n  }, [plansInit, providerSelected, rsProviderRegions])\n\n  const handleOnClose = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_PROVIDER_FORM_CLOSED,\n    })\n    setPlanIdSelected('')\n    setProviderSelected(DEFAULT_PROVIDER)\n    dispatch(setIsOpenSelectPlanDialog(false))\n    dispatch(setSocialDialogState(null))\n  }, [])\n\n  if (!isOpenDialog) return null\n\n  const getOptionDisplay = (item: CloudSubscriptionPlanResponse) => {\n    const {\n      region = '',\n      details: { countryName = '', cityName = '', flag = '' },\n      provider,\n    } = item\n    const rsProviderRegions: string[] =\n      find(rsRegions, { provider })?.regions || []\n\n    return (\n      <Row align=\"center\" gap=\"s\">\n        <CountryFlag countryCode={flag} />\n\n        <Text\n          color=\"primary\"\n          data-testid={`option-${region}`}\n          data-test-subj={`oauth-region-${region}`}\n        >\n          {`${countryName} (${cityName})`}\n        </Text>\n\n        <Text color=\"secondary\">\n          <StyledRegionName>{region}</StyledRegionName>\n          {rsProviderRegions?.includes(region) && (\n            <ColorText data-testid={`rs-text-${region}`}>(Redis 7.2)</ColorText>\n          )}\n        </Text>\n      </Row>\n    )\n  }\n\n  const regionOptions = plans.map((item) => {\n    const { id, region = '' } = item\n    return {\n      label: `${id}`,\n      value: `${id}`,\n      inputDisplay: getOptionDisplay(item),\n      dropdownDisplay: getOptionDisplay(item),\n      'data-test-subj': `oauth-region-${region}`,\n    }\n  })\n\n  const onChangeRegion = (region: string) => {\n    setPlanIdSelected(region)\n  }\n\n  const handleSubmit = () => {\n    dispatch(\n      createFreeDbJob({\n        name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n        resources: { planId: toNumber(planIdSelected) },\n        onSuccessAction: () => {\n          dispatch(setIsOpenSelectPlanDialog(false))\n          dispatch(\n            addInfiniteNotification(\n              INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n            ),\n          )\n        },\n      }),\n    )\n  }\n\n  return (\n    <Modal.Compose open>\n      <Modal.Content.Compose data-testid=\"oauth-select-plan-dialog\">\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={handleOnClose}\n          data-testid=\"oauth-select-plan-dialog-close-btn\"\n        />\n        <Modal.Content.Header.Title>\n          <Row justify=\"center\">\n            <Title>Choose a cloud vendor</Title>\n          </Row>\n        </Modal.Content.Header.Title>\n        <Modal.Content.Body.Compose width=\"fit-content\">\n          <StyledModalContentBody>\n            <StyledSubTitle color=\"default\">\n              Select a cloud vendor and region to complete the final step\n              towards your free Redis Cloud database. No credit card is\n              required.\n            </StyledSubTitle>\n\n            <StyledProvidersSection gap=\"m\" direction=\"column\" align=\"start\">\n              <Text color=\"primary\">Select cloud vendor</Text>\n              <StyledProvidersSelectionGroup\n                boxes={oAuthProvidersBoxes}\n                value={providerSelected}\n                onChange={(value: string) =>\n                  setProviderSelected(value as OAuthProvider)\n                }\n              />\n            </StyledProvidersSection>\n\n            <StyledRegion>\n              <Text color=\"secondary\">Region</Text>\n              <RiSelect\n                loading={loading}\n                disabled={loading || !regionOptions.length}\n                options={regionOptions}\n                value={planIdSelected}\n                data-testid=\"select-oauth-region\"\n                onChange={onChangeRegion}\n                valueRender={({ option, isOptionValue }) => {\n                  if (isOptionValue) {\n                    return option.inputDisplay\n                  }\n                  return option.dropdownDisplay\n                }}\n              />\n              {!regionOptions.length && (\n                <StyledRegionSelectDescription data-testid=\"select-region-select-description\">\n                  No regions available, try another vendor.\n                </StyledRegionSelectDescription>\n              )}\n            </StyledRegion>\n            <StyledFooter>\n              <Row justify=\"end\" gap=\"m\">\n                <SecondaryButton\n                  onClick={handleOnClose}\n                  data-testid=\"close-oauth-select-plan-dialog\"\n                  aria-labelledby=\"close oauth select plan dialog\"\n                >\n                  Cancel\n                </SecondaryButton>\n                <PrimaryButton\n                  disabled={loading || !planIdSelected}\n                  loading={loading}\n                  onClick={handleSubmit}\n                  data-testid=\"submit-oauth-select-plan-dialog\"\n                  aria-labelledby=\"submit oauth select plan dialog\"\n                >\n                  Create database\n                </PrimaryButton>\n              </Row>\n            </StyledFooter>\n          </StyledModalContentBody>\n        </Modal.Content.Body.Compose>\n      </Modal.Content.Compose>\n    </Modal.Compose>\n  )\n}\n\nexport default OAuthSelectPlan\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-plan/constants.ts",
    "content": "import { AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\n\nexport enum OAuthProvider {\n  AWS = 'AWS',\n  Azure = 'Azure',\n  Google = 'GCP',\n}\n\nexport const OAuthProviders: {\n  id: OAuthProvider\n  icon: AllIconsType\n  label: string\n}[] = [\n  {\n    id: OAuthProvider.AWS,\n    icon: 'Awss3Icon',\n    label: 'Amazon Web Services',\n  },\n  {\n    id: OAuthProvider.Google,\n    icon: 'GooglecloudIcon',\n    label: 'Google Cloud',\n  },\n  {\n    id: OAuthProvider.Azure,\n    icon: 'AzureIcon',\n    label: 'Microsoft Azure',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-select-plan/index.ts",
    "content": "import OAuthSelectPlan from './OAuthSelectPlan'\n\nexport default OAuthSelectPlan\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sign-in-button/OAuthSignInButton.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport OAuthSignInButton, { Props } from './OAuthSignInButton'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: true,\n    },\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OAuthSignInButton', () => {\n  it('should render', () => {\n    expect(render(<OAuthSignInButton {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call proper actions after click on sign in button', () => {\n    render(\n      <OAuthSignInButton {...mockedProps} source={OAuthSocialSource.Browser} />,\n    )\n\n    fireEvent.click(screen.getByTestId('cloud-sign-in-btn'))\n\n    expect(store.getActions()).toEqual([\n      setSSOFlow(OAuthSocialAction.SignIn),\n      setSocialDialogState(OAuthSocialSource.Browser),\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sign-in-button/OAuthSignInButton.tsx",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { OAuthSsoHandlerDialog } from 'uiSrc/components'\n\nimport RedisLogo from 'uiSrc/assets/img/logo_small.svg'\n\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport styles from './styles.module.scss'\n\nconst LogoWrapper = styled.div`\n  width: 15px;\n  height: 15px;\n`\n\nexport interface Props {\n  source: OAuthSocialSource\n}\n\nconst OAuthSignInButton = (props: Props) => {\n  const { source } = props\n\n  return (\n    <OAuthSsoHandlerDialog>\n      {(socialCloudHandlerClick) => (\n        <SecondaryButton\n          className={styles.btn}\n          size=\"s\"\n          onClick={(e: React.MouseEvent) =>\n            socialCloudHandlerClick(e, {\n              source,\n              action: OAuthSocialAction.SignIn,\n            })\n          }\n          data-testid=\"cloud-sign-in-btn\"\n        >\n          <LogoWrapper>\n            <RiImage\n              $size={'fullWidth'}\n              className={styles.logo}\n              src={RedisLogo}\n              alt=\"Redis logo\"\n            />\n          </LogoWrapper>\n          <span>Cloud sign in</span>\n        </SecondaryButton>\n      )}\n    </OAuthSsoHandlerDialog>\n  )\n}\n\nexport default OAuthSignInButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sign-in-button/index.ts",
    "content": "import OAuthSignInButton from './OAuthSignInButton'\n\nexport default OAuthSignInButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sign-in-button/styles.module.scss",
    "content": ".btn {\n  background-color: var(--euiColorEmptyShade);\n  border-color: var(--separatorColorLight) !important;\n  border-radius: 16px !important;\n\n  :global(.euiButton__text) {\n    display: flex;\n    align-items: center;\n\n    color: var(--euiTextSubduedColor);\n  }\n\n  &:hover, &:focus {\n    background-color: var(--euiColorEmptyShade) !important;\n  }\n\n  .logo {\n    width: 14px;\n    margin-right: 6px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/index.ts",
    "content": "import OAuthAutodiscovery from './oauth-autodiscovery'\nimport OAuthCreateDb from './oauth-create-db'\nimport OAuthSignIn from './oauth-sign-in'\n\nexport { OAuthAutodiscovery, OAuthCreateDb, OAuthSignIn }\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  OAuthSocialAction,\n  OAuthSocialSource,\n  OAuthStrategy,\n} from 'uiSrc/slices/interfaces'\nimport { IpcInvokeEvent } from 'uiSrc/electron/constants'\nimport {\n  oauthCloudUserSelector,\n  setOAuthCloudSource,\n  signIn,\n} from 'uiSrc/slices/oauth/cloud'\nimport {\n  loadSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport {\n  MOCK_OAUTH_SSO_EMAIL,\n  MOCK_OAUTH_USER_PROFILE,\n} from 'uiSrc/mocks/data/oauth'\nimport OAuthAutodiscovery from './OAuthAutodiscovery'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n  oauthCloudUserSelector: jest.fn().mockReturnValue({\n    data: null,\n  }),\n}))\n\nconst invokeMock = jest.fn()\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  window.app = {\n    ipc: { invoke: invokeMock },\n  } as any\n})\n\ndescribe('OAuthAutodiscovery', () => {\n  it('should render', () => {\n    expect(render(<OAuthAutodiscovery />)).toBeTruthy()\n  })\n\n  it('should render social buttons when not logged in', () => {\n    render(<OAuthAutodiscovery />)\n\n    expect(screen.queryByTestId('github-oauth')).toBeInTheDocument()\n    expect(screen.queryByTestId('google-oauth')).toBeInTheDocument()\n  })\n\n  it('should send telemetry after click on github btn', async () => {\n    render(<OAuthAutodiscovery />)\n\n    fireEvent.click(screen.queryByTestId('github-oauth') as HTMLButtonElement)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.GitHub,\n        action: OAuthSocialAction.Import,\n        source: OAuthSocialSource.Autodiscovery,\n      },\n    })\n\n    expect(invokeMock).toBeCalledTimes(1)\n    expect(invokeMock).toBeCalledWith(IpcInvokeEvent.cloudOauth, {\n      action: OAuthSocialAction.Import,\n      strategy: OAuthStrategy.GitHub,\n    })\n    invokeMock.mockRestore()\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Import),\n      setOAuthCloudSource(OAuthSocialSource.Autodiscovery),\n      signIn(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n\n    invokeMock.mockRestore()\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should send telemetry after click on sso btn', async () => {\n    render(<OAuthAutodiscovery />)\n\n    fireEvent.click(screen.queryByTestId('sso-oauth') as HTMLButtonElement)\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.SSO,\n        action: OAuthSocialAction.Import,\n        source: OAuthSocialSource.Autodiscovery,\n      },\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sso-email'), {\n        target: { value: MOCK_OAUTH_SSO_EMAIL },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED,\n      eventData: {\n        action: OAuthSocialAction.Import,\n      },\n    })\n\n    expect(invokeMock).toBeCalledTimes(1)\n    expect(invokeMock).toBeCalledWith(IpcInvokeEvent.cloudOauth, {\n      action: OAuthSocialAction.Import,\n      strategy: OAuthStrategy.SSO,\n      data: {\n        email: MOCK_OAUTH_SSO_EMAIL,\n      },\n    })\n    invokeMock.mockRestore()\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Import),\n      setOAuthCloudSource(OAuthSocialSource.Autodiscovery),\n      signIn(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n\n    invokeMock.mockRestore()\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should render discover button and call proper actions on click when logged in', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: MOCK_OAUTH_USER_PROFILE,\n    })\n\n    render(<OAuthAutodiscovery />)\n\n    fireEvent.click(screen.getByTestId('oauth-discover-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED,\n      eventData: {\n        source: 'autodiscovery',\n      },\n    })\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Import),\n      loadSubscriptionsRedisCloud(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const StyledDiscoverText = styled(Text)`\n  align-self: flex-start;\n`\n\nexport const StyledContainer = styled(Col)``\n\nexport const StyledCreateDbSection = styled(Row)`\n  width: 100%;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.core.space.space100};\n  padding-block: ${({ theme }) => theme.core.space.space100};\n  padding-inline: ${({ theme }) => theme.core.space.space200};\n`\n\nexport const StyledAgreementContainer = styled.div`\n  margin-top: ${({ theme }) => theme.core.space.space200};\n  width: 50%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/OAuthAutodiscovery.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { find } from 'lodash'\nimport { OAuthAgreement } from 'uiSrc/components/oauth/shared'\nimport {\n  oauthCloudUserSelector,\n  setOAuthCloudSource,\n} from 'uiSrc/slices/oauth/cloud'\nimport {\n  fetchSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { Pages } from 'uiSrc/constants'\nimport OAuthForm from 'uiSrc/components/oauth/shared/oauth-form'\n\nimport { OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { CloudIcon } from 'uiSrc/components/base/icons'\n\nimport {\n  StyledDiscoverText,\n  StyledContainer,\n  StyledCreateDbSection,\n  StyledAgreementContainer,\n} from './OAuthAutodiscovery.styles'\n\nexport interface Props {\n  inline?: boolean\n  source?: OAuthSocialSource\n  onClose?: () => void\n}\n\nconst OAuthAutodiscovery = (props: Props) => {\n  const { inline, source = OAuthSocialSource.Autodiscovery, onClose } = props\n  const { data } = useSelector(oauthCloudUserSelector)\n\n  const [isDiscoverDisabled, setIsDiscoverDisabled] = useState(false)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const handleClickDiscover = () => {\n    dispatch(setSSOFlow(OAuthSocialAction.Import))\n    setIsDiscoverDisabled(true)\n    dispatch(\n      fetchSubscriptionsRedisCloud(\n        null,\n        true,\n        () => {\n          history.push(Pages.redisCloudSubscriptions)\n          setIsDiscoverDisabled(false)\n        },\n        () => setIsDiscoverDisabled(false),\n      ),\n    )\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED,\n      eventData: {\n        source,\n      },\n    })\n  }\n\n  if (data) {\n    const { accounts, currentAccountId } = data\n    const currentAccountName = find(\n      accounts,\n      ({ id }) => id === currentAccountId,\n    )\n\n    return (\n      <StyledContainer data-testid=\"oauth-container-import\" gap=\"xl\">\n        <Text>\n          Use{' '}\n          <Text color=\"primary\" variant=\"semiBold\" component=\"span\">\n            {currentAccountName?.name} #{currentAccountId}\n          </Text>{' '}\n          account to auto-discover subscriptions and add your databases.\n        </Text>\n        <Row justify=\"center\">\n          <PrimaryButton\n            onClick={handleClickDiscover}\n            disabled={isDiscoverDisabled}\n            data-testid=\"oauth-discover-btn\"\n          >\n            Discover\n          </PrimaryButton>\n        </Row>\n      </StyledContainer>\n    )\n  }\n\n  const handleClickSso = (accountOption: string) => {\n    dispatch(setSSOFlow(OAuthSocialAction.Import))\n    dispatch(setOAuthCloudSource(source))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption,\n        action: OAuthSocialAction.Import,\n        source,\n      },\n    })\n  }\n\n  const CreateFreeDb = () => (\n    <StyledCreateDbSection justify=\"between\" align=\"center\">\n      <Row align=\"center\" gap=\"m\">\n        <CloudIcon size=\"L\" />\n        <Text color=\"primary\">Start FREE with Redis Cloud</Text>\n      </Row>\n      <OAuthSsoHandlerDialog>\n        {(ssoCloudHandlerClick) => (\n          <PrimaryButton\n            // todo: choose either href or on click\n            // href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, { campaign: '' })}\n            // target=\"_blank\"\n            onClick={(e: React.MouseEvent) => {\n              ssoCloudHandlerClick(e, {\n                source: OAuthSocialSource.DiscoveryForm,\n                action: OAuthSocialAction.Create,\n              })\n              onClose?.()\n            }}\n          >\n            Quick start\n          </PrimaryButton>\n        )}\n      </OAuthSsoHandlerDialog>\n    </StyledCreateDbSection>\n  )\n\n  return (\n    <StyledContainer\n      data-testid=\"oauth-container-import\"\n      align=\"center\"\n      gap=\"xl\"\n    >\n      <OAuthForm\n        inline={inline}\n        onClick={handleClickSso}\n        action={OAuthSocialAction.Import}\n      >\n        {(form: React.ReactNode) => (\n          <>\n            <StyledDiscoverText color=\"primary\">\n              Discover subscriptions and add your databases. A new Redis Cloud\n              account will be created for you if you don’t have one.\n            </StyledDiscoverText>\n\n            <CreateFreeDb />\n\n            <Text color=\"primary\">Get started with</Text>\n            <Title size=\"L\" color=\"primary\">\n              Redis Cloud account\n            </Title>\n\n            {form}\n\n            <StyledAgreementContainer>\n              <OAuthAgreement size=\"s\" />\n            </StyledAgreementContainer>\n          </>\n        )}\n      </OAuthForm>\n    </StyledContainer>\n  )\n}\n\nexport default OAuthAutodiscovery\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-autodiscovery/index.ts",
    "content": "import OAuthAutodiscovery from './OAuthAutodiscovery'\n\nexport default OAuthAutodiscovery\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-create-db/OAuthCreateDb.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  expectActionsToContain,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  addFreeDb,\n  getPlans,\n  oauthCloudUserSelector,\n  setSocialDialogState,\n  showOAuthProgress,\n  signIn,\n} from 'uiSrc/slices/oauth/cloud'\nimport {\n  setIsRecommendedSettingsSSO,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { CloudJobStep } from 'uiSrc/electron/constants'\nimport { addInfiniteNotification } from 'uiSrc/slices/app/notifications'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { OAuthSocialAction, OAuthStrategy } from 'uiSrc/slices/interfaces'\nimport { MOCK_OAUTH_SSO_EMAIL } from 'uiSrc/mocks/data/oauth'\nimport OAuthCreateDb from './OAuthCreateDb'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSsoRecommendedSettings: {\n      flag: true,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    source: 'source',\n  }),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n  oauthCloudUserSelector: jest.fn().mockReturnValue({\n    data: null,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OAuthCreateDb', () => {\n  it('should render proper components ', () => {\n    expect(render(<OAuthCreateDb />)).toBeTruthy()\n  })\n\n  it('should render proper components if user is not logged in', () => {\n    render(<OAuthCreateDb />)\n\n    expect(screen.getByTestId('oauth-advantages')).toBeInTheDocument()\n    expect(\n      screen.getByTestId('oauth-container-social-buttons'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('oauth-agreement-checkbox')).toBeInTheDocument()\n    expect(\n      screen.getByTestId('oauth-recommended-settings-checkbox'),\n    ).toBeInTheDocument()\n  })\n\n  it('should call proper actions after click on sso sign button', async () => {\n    render(<OAuthCreateDb />)\n\n    fireEvent.click(screen.getByTestId('sso-oauth'))\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.SSO,\n        action: OAuthSocialAction.Create,\n        cloudRecommendedSettings: 'enabled',\n      },\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sso-email'), {\n        target: { value: MOCK_OAUTH_SSO_EMAIL },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED,\n      eventData: {\n        action: OAuthSocialAction.Create,\n      },\n    })\n\n    const expectedActions = [setIsRecommendedSettingsSSO(true), signIn()]\n    expect(store.getActions()).toEqual(expectedActions)\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper actions after click on sign button', () => {\n    render(<OAuthCreateDb />)\n\n    fireEvent.click(screen.getByTestId('google-oauth'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.Google,\n        action: OAuthSocialAction.Create,\n        cloudRecommendedSettings: 'enabled',\n      },\n    })\n\n    const expectedActions = [setIsRecommendedSettingsSSO(true), signIn()]\n    expect(store.getActions()).toEqual(expectedActions)\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should render proper components if user is logged in', () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({ data: {} })\n    render(<OAuthCreateDb />)\n\n    expect(screen.getByTestId('oauth-advantages')).toBeInTheDocument()\n    expect(screen.getByTestId('oauth-create-db')).toBeInTheDocument()\n    expect(\n      screen.getByTestId('oauth-recommended-settings-checkbox'),\n    ).toBeInTheDocument()\n\n    expect(\n      screen.queryByTestId('oauth-agreement-checkbox'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('oauth-container-social-buttons'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should call proper actions after click create', () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({ data: {} })\n    render(<OAuthCreateDb />)\n\n    fireEvent.click(screen.getByTestId('oauth-create-db'))\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Create),\n      showOAuthProgress(true),\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n      setSocialDialogState(null),\n      addFreeDb(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper actions after click create without recommended settings', async () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({ data: {} })\n    render(<OAuthCreateDb />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('oauth-recommended-settings-checkbox'))\n    })\n\n    fireEvent.click(screen.getByTestId('oauth-create-db'))\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Create),\n      showOAuthProgress(true),\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n      setSocialDialogState(null),\n      getPlans(),\n    ]\n    expectActionsToContain(store.getActions(), expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-create-db/OAuthCreateDb.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  createFreeDbJob,\n  fetchPlans,\n  oauthCloudUserSelector,\n  setSocialDialogState,\n  showOAuthProgress,\n} from 'uiSrc/slices/oauth/cloud'\n\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { CloudJobName, CloudJobStep } from 'uiSrc/electron/constants'\nimport {\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport {\n  setIsRecommendedSettingsSSO,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { Nullable } from 'uiSrc/utils'\nimport OAuthForm from 'uiSrc/components/oauth/shared/oauth-form'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  OAuthAdvantages,\n  OAuthAgreement,\n  OAuthRecommendedSettings,\n} from '../../shared'\nimport styles from './styles.module.scss'\nimport { StyledAdvantagesContainerAbsolute } from '../../shared/styles'\n\nexport interface Props {\n  source?: Nullable<OAuthSocialSource>\n}\n\nconst OAuthCreateDb = (props: Props) => {\n  const { source } = props\n  const { data } = useSelector(oauthCloudUserSelector)\n  const {\n    [FeatureFlags.cloudSsoRecommendedSettings]: isRecommendedFeatureEnabled,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n\n  const [isRecommended, setIsRecommended] = useState(\n    isRecommendedFeatureEnabled?.flag ? true : undefined,\n  )\n\n  const dispatch = useDispatch()\n\n  const handleSocialButtonClick = (accountOption: string) => {\n    dispatch(setIsRecommendedSettingsSSO(isRecommended))\n    const cloudRecommendedSettings = !isRecommendedFeatureEnabled?.flag\n      ? 'not displayed'\n      : isRecommended\n        ? 'enabled'\n        : 'disabled'\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption,\n        action: OAuthSocialAction.Create,\n        cloudRecommendedSettings,\n        source,\n      },\n    })\n  }\n\n  const handleChangeRecommendedSettings = (value: boolean) => {\n    setIsRecommended(value)\n  }\n\n  const handleClickCreate = () => {\n    dispatch(setSSOFlow(OAuthSocialAction.Create))\n    dispatch(showOAuthProgress(true))\n    dispatch(\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n    )\n    dispatch(setSocialDialogState(null))\n\n    if (isRecommended) {\n      dispatch(\n        createFreeDbJob({\n          name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          resources: {\n            isRecommendedSettings: isRecommended,\n          },\n          onFailAction: () => {\n            dispatch(\n              removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n            )\n            dispatch(setSSOFlow(undefined))\n          },\n        }),\n      )\n\n      return\n    }\n\n    dispatch(fetchPlans())\n  }\n\n  return (\n    <div className={styles.container} data-testid=\"oauth-container-create-db\">\n      <Row>\n        <FlexItem grow className={styles.advantagesContainer}>\n          <StyledAdvantagesContainerAbsolute>\n            <OAuthAdvantages />\n          </StyledAdvantagesContainerAbsolute>\n        </FlexItem>\n        <FlexItem grow className={styles.socialContainer}>\n          {!data ? (\n            <OAuthForm\n              className={styles.socialButtons}\n              onClick={handleSocialButtonClick}\n              action={OAuthSocialAction.Create}\n            >\n              {(form: React.ReactNode) => (\n                <>\n                  <Col align=\"center\">\n                    <Text color=\"primary\" size=\"L\">\n                      Get started with\n                    </Text>\n                    <Title size=\"XL\" color=\"primary\" className={styles.title}>\n                      Free Redis Cloud database\n                    </Title>\n                  </Col>\n\n                  {form}\n                  <div>\n                    <OAuthRecommendedSettings\n                      value={isRecommended}\n                      onChange={handleChangeRecommendedSettings}\n                    />\n                    <OAuthAgreement />\n                  </div>\n                </>\n              )}\n            </OAuthForm>\n          ) : (\n            <>\n              <Col align=\"center\">\n                <Text color=\"primary\" size=\"L\">\n                  Get your\n                </Text>\n                <Title size=\"XL\" color=\"primary\" className={styles.title}>\n                  Free Redis Cloud database\n                </Title>\n              </Col>\n              <Spacer size=\"xl\" />\n              <Text textAlign=\"center\" color=\"primary\">\n                The database will be created automatically and can be changed\n                from Redis Cloud.\n              </Text>\n              <Spacer size=\"xl\" />\n              <OAuthRecommendedSettings\n                value={isRecommended}\n                onChange={handleChangeRecommendedSettings}\n              />\n              <Spacer />\n              <PrimaryButton\n                onClick={handleClickCreate}\n                data-testid=\"oauth-create-db\"\n              >\n                Create\n              </PrimaryButton>\n            </>\n          )}\n        </FlexItem>\n      </Row>\n    </div>\n  )\n}\n\nexport default OAuthCreateDb\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-create-db/index.ts",
    "content": "import OAuthCreateDb from './OAuthCreateDb'\n\nexport default OAuthCreateDb\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-create-db/styles.module.scss",
    "content": ".container {\n  height: 100%;\n  display: flex;\n\n  .advantagesContainer {\n    max-width: 320px;\n  }\n\n  .socialContainer {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n\n    padding: 108px 0px 40px 40px;\n\n    .title {\n      text-align: center;\n    }\n  }\n\n  .socialButtons {\n    margin: 40px 0 60px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-sign-in/OAuthSignIn.spec.tsx",
    "content": "import React from 'react'\n\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { MOCK_OAUTH_SSO_EMAIL } from 'uiSrc/mocks/data/oauth'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSocialAction, OAuthStrategy } from 'uiSrc/slices/interfaces'\nimport { IpcInvokeEvent } from 'uiSrc/electron/constants'\nimport { signIn } from 'uiSrc/slices/oauth/cloud'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport OAuthSignIn from './OAuthSignIn'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n  oauthCloudUserSelector: jest.fn().mockReturnValue({\n    data: null,\n  }),\n}))\n\nconst invokeMock = jest.fn()\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  window.app = {\n    ipc: { invoke: invokeMock },\n  } as any\n})\n\ndescribe('OAuthSignIn', () => {\n  it('should render', () => {\n    expect(render(<OAuthSignIn />)).toBeTruthy()\n  })\n\n  it('should render proper components for signIn', () => {\n    render(<OAuthSignIn />)\n\n    expect(screen.getByTestId('oauth-advantages')).toBeInTheDocument()\n    expect(\n      screen.getByTestId('oauth-container-social-buttons'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('oauth-agreement-checkbox')).toBeInTheDocument()\n  })\n\n  it('should send telemetry after click on sso btn', async () => {\n    render(<OAuthSignIn />)\n\n    fireEvent.click(screen.queryByTestId('sso-oauth') as HTMLButtonElement)\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.SSO,\n        action: OAuthSocialAction.SignIn,\n      },\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sso-email'), {\n        target: { value: MOCK_OAUTH_SSO_EMAIL },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED,\n      eventData: {\n        action: OAuthSocialAction.SignIn,\n      },\n    })\n\n    expect(invokeMock).toBeCalledTimes(1)\n    expect(invokeMock).toBeCalledWith(IpcInvokeEvent.cloudOauth, {\n      action: OAuthSocialAction.SignIn,\n      strategy: OAuthStrategy.SSO,\n      data: {\n        email: MOCK_OAUTH_SSO_EMAIL,\n      },\n    })\n    invokeMock.mockRestore()\n\n    const expectedActions = [setSSOFlow(OAuthSocialAction.SignIn), signIn()]\n    expect(store.getActions()).toEqual(expectedActions)\n\n    invokeMock.mockRestore()\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-sign-in/OAuthSignIn.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { OAuthAdvantages, OAuthAgreement } from 'uiSrc/components/oauth/shared'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { Nullable } from 'uiSrc/utils'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport OAuthForm from '../../shared/oauth-form/OAuthForm'\nimport styles from './styles.module.scss'\nimport { StyledAdvantagesContainerAbsolute } from '../../shared/styles'\n\nexport interface Props {\n  source?: Nullable<OAuthSocialSource>\n  action?: OAuthSocialAction\n}\n\nconst OAuthSignIn = (props: Props) => {\n  const { source, action = OAuthSocialAction.SignIn } = props\n\n  const dispatch = useDispatch()\n\n  const handleSocialButtonClick = (accountOption: string) => {\n    dispatch(setSSOFlow(action))\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption,\n        action,\n        source,\n      },\n    })\n  }\n\n  return (\n    <div className={styles.container} data-testid=\"oauth-container-signIn\">\n      <Row>\n        <FlexItem grow className={styles.advantagesContainer}>\n          <StyledAdvantagesContainerAbsolute>\n            <OAuthAdvantages />\n          </StyledAdvantagesContainerAbsolute>\n        </FlexItem>\n        <FlexItem grow className={styles.socialContainer}>\n          <OAuthForm\n            onClick={handleSocialButtonClick}\n            action={action}\n            className={styles.socialButtons}\n          >\n            {(form: React.ReactNode) => (\n              <>\n                <Text className={styles.subTitle}>Get started with</Text>\n                <Title size=\"XL\" className={styles.title}>\n                  Redis Cloud account\n                </Title>\n                {form}\n                <OAuthAgreement />\n              </>\n            )}\n          </OAuthForm>\n        </FlexItem>\n      </Row>\n    </div>\n  )\n}\n\nexport default OAuthSignIn\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-sign-in/index.ts",
    "content": "import OAuthSignIn from './OAuthSignIn'\n\nexport default OAuthSignIn\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso/oauth-sign-in/styles.module.scss",
    "content": ".container {\n  height: 100%;\n  display: flex;\n\n  .advantagesContainer {\n    max-width: 320px;\n    padding: 0;\n  }\n\n  .socialContainer {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n\n    padding: 108px 0px 40px 40px;\n\n    .subTitle {\n      font-size: 16px;\n    }\n\n    .title {\n      font-weight: bold;\n      text-align: center;\n    }\n  }\n\n  .socialButtons {\n    margin: 40px 0 60px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-dialog/OAuthSsoDialog.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { cloudSelector } from 'uiSrc/slices/instances/cloud'\nimport { OAuthSocialAction } from 'uiSrc/slices/interfaces'\nimport OAuthSsoDialog from './OAuthSsoDialog'\n\njest.mock('uiSrc/slices/instances/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/cloud'),\n  cloudSelector: jest.fn().mockReturnValue({\n    ssoFlow: '',\n  }),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    isOpenSocialDialog: true,\n    source: 'source',\n  }),\n}))\n\ndescribe('OAuthSsoDialog', () => {\n  it('should render', () => {\n    expect(render(<OAuthSsoDialog />)).toBeTruthy()\n  })\n\n  it('should render proper modal with ssoFlow = OAuthSocialAction.Create', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: OAuthSocialAction.Create,\n    })\n    render(<OAuthSsoDialog />)\n\n    expect(screen.getByTestId('oauth-container-create-db')).toBeInTheDocument()\n  })\n\n  it('should render proper modal with ssoFlow = OAuthSocialAction.Import', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: OAuthSocialAction.Import,\n    })\n    render(<OAuthSsoDialog />)\n\n    expect(screen.getByTestId('oauth-container-signIn')).toBeInTheDocument()\n  })\n\n  it('should render proper modal with ssoFlow = OAuthSocialAction.SignIn', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: OAuthSocialAction.SignIn,\n    })\n    render(<OAuthSsoDialog />)\n\n    expect(screen.getByTestId('oauth-container-signIn')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-dialog/OAuthSsoDialog.tsx",
    "content": "import React, { useCallback } from 'react'\n\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport cx from 'classnames'\nimport {\n  oauthCloudSelector,\n  setSocialDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport { OAuthSocialAction } from 'uiSrc/slices/interfaces'\nimport { cloudSelector } from 'uiSrc/slices/instances/cloud'\nimport { OAuthCreateDb, OAuthSignIn } from 'uiSrc/components/oauth/oauth-sso'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Modal } from 'uiSrc/components/base/display'\nimport styles from './styles.module.scss'\n\nconst OAuthSsoDialog = () => {\n  const { ssoFlow } = useSelector(cloudSelector)\n  const { isOpenSocialDialog, source } = useSelector(oauthCloudSelector)\n\n  const dispatch = useDispatch()\n\n  const handleClose = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_FORM_CLOSED,\n      eventData: {\n        action: ssoFlow,\n      },\n    })\n    dispatch(setSocialDialogState(null))\n  }, [ssoFlow])\n\n  if (!isOpenSocialDialog || !ssoFlow) {\n    return null\n  }\n\n  return (\n    <Modal\n      open\n      onCancel={handleClose}\n      className={cx(styles.modal, {\n        [styles.createDb]: ssoFlow === OAuthSocialAction.Create,\n        [styles.signIn]: ssoFlow === OAuthSocialAction.SignIn,\n        [styles.import]: ssoFlow === OAuthSocialAction.Import,\n      })}\n      data-testid=\"social-oauth-dialog\"\n      title={null}\n      content={\n        <>\n          {ssoFlow === OAuthSocialAction.Create && (\n            <OAuthCreateDb source={source} />\n          )}\n          {ssoFlow === OAuthSocialAction.SignIn && (\n            <OAuthSignIn source={source} />\n          )}\n          {ssoFlow === OAuthSocialAction.Import && (\n            <OAuthSignIn action={OAuthSocialAction.Import} source={source} />\n          )}\n        </>\n      }\n    />\n  )\n}\n\nexport default OAuthSsoDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-dialog/index.ts",
    "content": "import OAuthSsoDialog from './OAuthSsoDialog'\n\nexport default OAuthSsoDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-dialog/styles.module.scss",
    "content": ".modal {\n  background: var(--euiColorEmptyShade) !important;\n  min-width: 768px !important;\n  min-height: 472px !important;\n  padding: 0;\n\n  :global(.euiModalBody__overflow) {\n    mask-image: none !important;\n  }\n\n  &.createDb, &.import {\n    max-width: 768px !important;\n    min-height: 500px !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-handler-dialog/OAuthSsoHandlerDialog.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { Maybe } from 'uiSrc/utils'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\n\nimport OAuthSsoHandlerDialog from './OAuthSsoHandlerDialog'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudUserDataSelector: jest.fn().mockReturnValue(null),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: false,\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst childrenMock = (\n  onClick: (\n    e: React.MouseEvent,\n    {\n      source,\n      action,\n    }: { source: OAuthSocialSource; action?: Maybe<OAuthSocialAction> },\n  ) => void,\n  {\n    source,\n    action,\n  }: { source: OAuthSocialSource; action?: Maybe<OAuthSocialAction> },\n) => (\n  <div\n    onClick={(e) => onClick(e, { source, action })}\n    onKeyDown={() => {}}\n    data-testid=\"link\"\n    aria-label=\"link\"\n    role=\"link\"\n    tabIndex={0}\n  >\n    link\n  </div>\n)\n\nafterEach(() => {\n  ;(sendEventTelemetry as jest.Mock).mockRestore()\n})\n\ndescribe('OAuthSsoHandlerDialog', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <OAuthSsoHandlerDialog>\n          {(ssoCloudHandlerClick) =>\n            childrenMock(ssoCloudHandlerClick, {\n              source: OAuthSocialSource.BrowserContentMenu,\n            })\n          }\n        </OAuthSsoHandlerDialog>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it(`setSocialDialogState should not called if ${FeatureFlags.cloudSso} is not enabled`, () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(\n      <OAuthSsoHandlerDialog>\n        {(ssoCloudHandlerClick) =>\n          childrenMock(ssoCloudHandlerClick, {\n            source: OAuthSocialSource.BrowserContentMenu,\n          })\n        }\n      </OAuthSsoHandlerDialog>,\n    )\n\n    expect(sendEventTelemetry).not.toBeCalled()\n    expect(store.getActions()).toEqual([])\n  })\n\n  it(`setSocialDialogState should called if ${FeatureFlags.cloudSso} is enabled`, () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockImplementation(() => ({\n      [FeatureFlags.cloudSso]: { flag: true },\n    }))\n\n    render(\n      <OAuthSsoHandlerDialog>\n        {(ssoCloudHandlerClick) =>\n          childrenMock(ssoCloudHandlerClick, {\n            source: OAuthSocialSource.BrowserContentMenu,\n            action: OAuthSocialAction.Create,\n          })\n        }\n      </OAuthSsoHandlerDialog>,\n    )\n\n    fireEvent.click(screen.queryByTestId('link')!)\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Create),\n      setSocialDialogState(OAuthSocialSource.BrowserContentMenu),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_FREE_DATABASE_CLICKED,\n      eventData: {\n        source: OAuthSocialSource.BrowserContentMenu,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-handler-dialog/OAuthSsoHandlerDialog.tsx",
    "content": "import { useDispatch, useSelector } from 'react-redux'\nimport React from 'react'\n\nimport { useHistory } from 'react-router-dom'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  oauthCloudUserSelector,\n  setSocialDialogState,\n} from 'uiSrc/slices/oauth/cloud'\nimport {\n  fetchSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { Maybe } from 'uiSrc/utils'\n\nexport interface Props {\n  children: (\n    ssoCloudHandlerClick: (\n      e: React.MouseEvent,\n      {\n        source,\n        action,\n      }: { source: OAuthSocialSource; action?: Maybe<OAuthSocialAction> },\n      telemetrySource?: string,\n    ) => void,\n    isSSOEnabled: boolean,\n  ) => React.ReactElement\n}\n\nconst telemetryEventByAction = {\n  [OAuthSocialAction.SignIn]: TelemetryEvent.CLOUD_SIGN_IN_CLICKED,\n  [OAuthSocialAction.Create]: TelemetryEvent.CLOUD_FREE_DATABASE_CLICKED,\n  [OAuthSocialAction.Import]: TelemetryEvent.CLOUD_IMPORT_DATABASES_CLICKED,\n}\n\nconst OAuthSsoHandlerDialog = ({ children }: Props) => {\n  const { data } = useSelector(oauthCloudUserSelector)\n  const { [FeatureFlags.cloudSso]: feature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const ssoCloudHandlerClick = (\n    e: React.MouseEvent,\n    {\n      source,\n      action,\n    }: { source: OAuthSocialSource; action?: Maybe<OAuthSocialAction> },\n    telemetrySource?: string,\n  ) => {\n    const isCloudSsoEnabled = !!feature?.flag\n\n    if (!isCloudSsoEnabled) {\n      return\n    }\n    e?.preventDefault()\n\n    if (action === OAuthSocialAction.Import && data) {\n      // if user logged in - do not show dialog, just redirect to subscriptions page\n      dispatch(\n        fetchSubscriptionsRedisCloud(null, true, () => {\n          history.push(Pages.redisCloudSubscriptions)\n        }),\n      )\n\n      sendEventTelemetry({\n        event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED,\n        eventData: { source },\n      })\n\n      return\n    }\n\n    if (action) {\n      sendEventTelemetry({\n        event: telemetryEventByAction[action],\n        eventData: { source: telemetrySource || source },\n      })\n    }\n\n    dispatch(setSSOFlow(action))\n    dispatch(setSocialDialogState(source))\n  }\n\n  return children?.(ssoCloudHandlerClick, !!feature?.flag)\n}\n\nexport default OAuthSsoHandlerDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-sso-handler-dialog/index.ts",
    "content": "import OAuthSsoHandlerDialog from './OAuthSsoHandlerDialog'\n\nexport default OAuthSsoHandlerDialog\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport { fireEvent } from '@testing-library/react'\nimport {\n  act,\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n  mockedStoreFn,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  getUserInfo,\n  logoutUser,\n  oauthCloudUserSelector,\n  setInitialLoadingState,\n} from 'uiSrc/slices/oauth/cloud'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  loadSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { MOCK_OAUTH_USER_PROFILE } from 'uiSrc/mocks/data/oauth'\n\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport OAuthUserProfile, { Props } from './OAuthUserProfile'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudUserSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: null,\n    error: '',\n    initialLoading: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/app/info', () => ({\n  ...jest.requireActual('uiSrc/slices/app/info'),\n  appInfoSelector: jest.fn().mockReturnValue({\n    server: {},\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\n\ndescribe('OAuthUserProfile', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStoreFn())\n    store.clearActions()\n  })\n\n  it('should render', () => {\n    expect(render(<OAuthUserProfile {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render loading spinner initially', () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      data: null,\n      error: '',\n      initialLoading: true,\n    })\n    render(<OAuthUserProfile {...mockedProps} />)\n\n    expect(screen.getByTestId('oath-user-profile-spinner')).toBeInTheDocument()\n    expect(screen.queryByTestId('cloud-sign-in-btn')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('user-profile-btn')).not.toBeInTheDocument()\n  })\n\n  it('should render sign in button if no profile', () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      data: null,\n      error: 'Some error',\n    })\n    render(<OAuthUserProfile {...mockedProps} />)\n\n    expect(screen.getByTestId('cloud-sign-in-btn')).toBeInTheDocument()\n    expect(screen.queryByTestId('user-profile-btn')).not.toBeInTheDocument()\n  })\n\n  it('should not render sign in button if no profile and mas build', () => {\n    ;(appInfoSelector as jest.Mock).mockReturnValueOnce({\n      server: {\n        packageType: 'mas',\n      },\n    })\n    render(<OAuthUserProfile {...mockedProps} />)\n\n    expect(screen.queryByTestId('cloud-sign-in-btn')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('user-profile-btn')).not.toBeInTheDocument()\n  })\n\n  it('should render profile button', () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: {},\n    })\n    render(<OAuthUserProfile {...mockedProps} />)\n\n    expect(screen.getByTestId('user-profile-btn')).toBeInTheDocument()\n    expect(screen.queryByTestId('cloud-sign-in-btn')).not.toBeInTheDocument()\n  })\n\n  it('should render profile info', async () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: MOCK_OAUTH_USER_PROFILE,\n    })\n    render(<OAuthUserProfile {...mockedProps} />, { store })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n    await waitForRiPopoverVisible()\n\n    expect(screen.getByTestId('account-full-name')).toHaveTextContent(\n      'Bill Russell',\n    )\n    expect(screen.getByTestId('profile-account-1-selected')).toHaveTextContent(\n      'Bill R #1',\n    )\n    expect(screen.getByTestId('profile-account-2')).toHaveTextContent(\n      'Bill R 2 #2',\n    )\n  })\n\n  it('should call proper action and telemetry after click on import databases', async () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: MOCK_OAUTH_USER_PROFILE,\n    })\n    render(<OAuthUserProfile {...mockedProps} />, { store })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n    await waitForRiPopoverVisible()\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    fireEvent.click(screen.getByTestId('profile-import-cloud-databases'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_IMPORT_DATABASES_SUBMITTED,\n      eventData: {\n        source: OAuthSocialSource.UserProfile,\n      },\n    })\n\n    expect(store.getActions()).toEqual([\n      setInitialLoadingState(false),\n      setSSOFlow(OAuthSocialAction.Import),\n      loadSubscriptionsRedisCloud(),\n    ])\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper action and telemetry after click on account', async () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: MOCK_OAUTH_USER_PROFILE,\n    })\n    render(<OAuthUserProfile {...mockedProps} />, { store })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n    await waitForRiPopoverVisible()\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_PROFILE_OPENED,\n    })\n\n    fireEvent.click(screen.getByTestId('profile-account-2'))\n\n    expect(store.getActions()).toEqual([\n      setInitialLoadingState(false),\n      getUserInfo(),\n    ])\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper action and telemetry after click on cloud link', async () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: MOCK_OAUTH_USER_PROFILE,\n    })\n    render(<OAuthUserProfile {...mockedProps} />, { store })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n    await waitForRiPopoverVisible()\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    fireEvent.click(screen.getByTestId('cloud-console-link'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CLOUD_CONSOLE_CLICKED,\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper action after click on logout', async () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({\n      data: MOCK_OAUTH_USER_PROFILE,\n    })\n    render(<OAuthUserProfile {...mockedProps} />, { store })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('user-profile-btn'))\n    })\n\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('profile-logout'))\n\n    expect(store.getActions()).toEqual([\n      setInitialLoadingState(false),\n      logoutUser(),\n      setSSOFlow(),\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport OAuthSignInButton from 'uiSrc/components/oauth/oauth-sign-in-button'\nimport {\n  activateAccount,\n  oauthCloudUserSelector,\n  setInitialLoadingState,\n} from 'uiSrc/slices/oauth/cloud'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { PackageType } from 'uiSrc/constants/env'\nimport UserProfileBadge from 'uiSrc/components/instance-header/components/user-profile/UserProfileBadge'\nimport { Loader } from 'uiSrc/components/base/display'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  source: OAuthSocialSource\n}\n\nconst OAuthUserProfile = (props: Props) => {\n  const { source } = props\n  const [selectingAccountId, setSelectingAccountId] = useState<number>()\n  const { error, data, initialLoading } = useSelector(oauthCloudUserSelector)\n  const { server } = useSelector(appInfoSelector)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (data || error) {\n      dispatch(setInitialLoadingState(false))\n    }\n  }, [data, error])\n\n  if (!data) {\n    if (server?.packageType === PackageType.Mas) return null\n\n    if (initialLoading) {\n      return (\n        <div className={styles.loadingContainer}>\n          <Loader\n            className={cx('infiniteMessage__icon', styles.loading)}\n            size=\"l\"\n            data-testid=\"oath-user-profile-spinner\"\n          />\n        </div>\n      )\n    }\n\n    return (\n      <div className={styles.wrapper}>\n        <OAuthSignInButton source={source} />\n      </div>\n    )\n  }\n\n  const handleClickSelectAccount = (id: number) => {\n    if (selectingAccountId) return\n\n    setSelectingAccountId(id)\n    dispatch(\n      activateAccount(\n        `${id}`,\n        () => {\n          setSelectingAccountId(undefined)\n          sendEventTelemetry({\n            event: TelemetryEvent.CLOUD_ACCOUNT_SWITCHED,\n          })\n        },\n        () => {\n          setSelectingAccountId(undefined)\n        },\n      ),\n    )\n  }\n\n  const handleClickCloudAccount = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_CONSOLE_CLICKED,\n    })\n  }\n\n  return (\n    <UserProfileBadge\n      error={error}\n      data={data}\n      handleClickCloudAccount={handleClickCloudAccount}\n      handleClickSelectAccount={handleClickSelectAccount}\n      data-testid=\"oauth-user-profile-badge\"\n      selectingAccountId={selectingAccountId}\n    />\n  )\n}\n\nexport default OAuthUserProfile\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-user-profile/index.ts",
    "content": "import OAuthUserProfile from './OAuthUserProfile'\n\nexport default OAuthUserProfile\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/oauth-user-profile/styles.module.scss",
    "content": ".wrapper {\n  flex-grow: 1;\n\n  display: flex;\n  align-items: center;\n\n  .profileBtn {\n    width: 42px;\n    height: 42px;\n\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    border-radius: 100%;\n\n    background-color: var(--insightsTriggerBgColor);\n\n    cursor: pointer;\n  }\n}\n\n.loadingContainer {\n  display: flex;\n  align-items: center;\n\n  .loading {\n    border-top-color: var(--euiColorGhost) !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/index.ts",
    "content": "import OAuthAdvantages from './oauth-advantages'\nimport OAuthAgreement from './oauth-agreement'\nimport OAuthRecommendedSettings from './oauth-recommended-settings'\nimport OAuthSocialButtons from './oauth-social-buttons'\n\nexport {\n  OAuthAdvantages,\n  OAuthAgreement,\n  OAuthRecommendedSettings,\n  OAuthSocialButtons,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-advantages/OAuthAdvantages.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport OAuthAdvantages from './OAuthAdvantages'\n\ndescribe('OAuthAdvantages', () => {\n  it('should render', () => {\n    expect(render(<OAuthAdvantages />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-advantages/OAuthAdvantages.tsx",
    "content": "import React from 'react'\nimport RedisLogo from 'uiSrc/assets/img/logo.svg'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { OAUTH_ADVANTAGES_ITEMS } from './constants'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nimport styles from './styles.module.scss'\n\nconst OAuthAdvantages = () => (\n  <div className={styles.container} data-testid=\"oauth-advantages\">\n    <RiImage src={RedisLogo} alt=\"Redis logo\" $size=\"s\" />\n    <Title size=\"M\">Cloud</Title>\n    <Spacer size=\"space600\" />\n    <Col justify=\"between\" align=\"stretch\" grow={false} gap=\"m\">\n      {OAUTH_ADVANTAGES_ITEMS.map(({ title }) => (\n        <Text\n          component=\"div\"\n          className={styles.advantage}\n          key={title?.toString()}\n        >\n          <RiIcon type=\"CheckThinIcon\" className={styles.advantageIcon} />\n          <Text size=\"S\">{title}</Text>\n        </Text>\n      ))}\n    </Col>\n  </div>\n)\n\nexport default OAuthAdvantages\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-advantages/constants.ts",
    "content": "export const OAUTH_ADVANTAGES_ITEMS = [\n  {\n    title: 'Structured querying and full-text search',\n  },\n  {\n    title: 'Document Store with JSON native',\n  },\n  {\n    title: 'Scalable and fully managed',\n  },\n  {\n    title: 'Free database to get started immediately',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-advantages/index.ts",
    "content": "import OAuthAdvantages from './OAuthAdvantages'\n\nexport default OAuthAdvantages\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-advantages/styles.module.scss",
    "content": ".container {\n  flex-grow: 1;\n\n  display: flex;\n  flex-direction: column;\n\n  align-items: center;\n  justify-content: center;\n}\n\n.advantage {\n  display: flex;\n  margin-top: 12px;\n\n  align-items: center;\n}\n\n.advantageIcon {\n  margin-right: 6px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-agreement/OAuthAgreement.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport {\n  cleanup,\n  clearStoreActions,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { localStorageService } from 'uiSrc/services'\nimport { setAgreement } from 'uiSrc/slices/oauth/cloud'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport OAuthAgreement from './OAuthAgreement'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n  },\n}))\n\ndescribe('OAuthAgreement', () => {\n  it('should render', () => {\n    expect(render(<OAuthAgreement />)).toBeTruthy()\n    expect(screen.getByTestId('oauth-agreement-checkbox')).toBeChecked()\n  })\n\n  it('should call setAgreement and set value in local storage', () => {\n    localStorageService.set = jest.fn()\n\n    render(<OAuthAgreement />)\n\n    fireEvent.click(screen.getByTestId('oauth-agreement-checkbox'))\n\n    const expectedActions = [setAgreement(false)]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n\n    expect(localStorageService.set).toBeCalledWith(\n      BrowserStorageItem.OAuthAgreement,\n      false,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-agreement/OAuthAgreement.tsx",
    "content": "import React, { ChangeEvent } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport cx from 'classnames'\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  setAgreement,\n  oauthCloudPAgreementSelector,\n} from 'uiSrc/slices/oauth/cloud'\n\nimport { enableUserAnalyticsAction } from 'uiSrc/slices/user/user-settings'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  size?: 's' | 'm'\n}\n\nconst OAuthAgreement = (props: Props) => {\n  const { size = 'm' } = props\n  const agreement = useSelector(oauthCloudPAgreementSelector)\n\n  const dispatch = useDispatch()\n\n  const handleCheck = (e: ChangeEvent<HTMLInputElement>) => {\n    if (e.target.checked) {\n      dispatch(enableUserAnalyticsAction('oauth-agreement'))\n    }\n    dispatch(setAgreement(e.target.checked))\n    localStorageService.set(BrowserStorageItem.OAuthAgreement, e.target.checked)\n  }\n\n  return (\n    <div className={cx(styles.wrapper, { [styles.small]: size === 's' })}>\n      <Checkbox\n        id=\"ouath-agreement\"\n        name=\"agreement\"\n        label=\"By signing up, you acknowledge that you agree:\"\n        labelSize=\"M\"\n        checked={agreement}\n        onChange={handleCheck}\n        className={styles.agreement}\n        data-testid=\"oauth-agreement-checkbox\"\n      />\n      <ul className={styles.list}>\n        <li className={styles.listItem}>\n          <Text color=\"secondary\" size=\"s\">\n            {'to our '}\n            <Link\n              variant=\"inline\"\n              size=\"S\"\n              color=\"subdued\"\n              href=\"https://redis.io/legal/cloud-tos/?utm_source=redisinsight&utm_medium=main&utm_campaign=main\"\n              className={styles.link}\n              target=\"_blank\"\n              data-testid=\"ouath-agreements-cloud-terms-of-service\"\n            >\n              Cloud Terms of Service\n            </Link>\n            {' and '}\n            <Link\n              variant=\"inline\"\n              size=\"S\"\n              color=\"subdued\"\n              href=\"https://redis.io/legal/privacy-policy/?utm_source=redisinsight&utm_medium=main&utm_campaign=main\"\n              className={styles.link}\n              target=\"_blank\"\n              data-testid=\"oauth-agreement-privacy-policy\"\n            >\n              Privacy Policy\n            </Link>\n          </Text>\n        </li>\n        <li className={styles.listItem}>\n          <Text color=\"secondary\" size=\"s\">\n            that Redis Insight will generate Redis Cloud API account and user\n            keys, and store them locally on your machine\n          </Text>\n        </li>\n        <li className={styles.listItem}>\n          <Text color=\"secondary\" size=\"s\">\n            that usage data will be enabled to help us understand and improve\n            how Redis Insight features are used\n          </Text>\n        </li>\n      </ul>\n    </div>\n  )\n}\n\nexport default OAuthAgreement\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-agreement/index.ts",
    "content": "import OAuthAgreement from './OAuthAgreement'\n\nexport default OAuthAgreement\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-agreement/styles.module.scss",
    "content": ".wrapper {\n  .list {\n    list-style: initial;\n    padding-left: 24px;\n    margin-top: 4px;\n  }\n\n  &.small {\n    .list {\n      list-style: initial;\n      padding-left: 18px;\n      margin-top: 2px;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-form/OAuthForm.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  render,\n  cleanup,\n  mockedStore,\n  fireEvent,\n  screen,\n  act,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport { OAuthStrategy } from 'uiSrc/slices/interfaces'\nimport { MOCK_OAUTH_SSO_EMAIL } from 'uiSrc/mocks/data/oauth'\nimport OAuthForm from './OAuthForm'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    source: 'source',\n  }),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n}))\n\nconst onClick = jest.fn()\n\nlet store: typeof mockedStore\nconst invokeMock = jest.fn()\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  window.app = {\n    ipc: { invoke: invokeMock },\n  } as any\n})\n\ndescribe('OAuthForm', () => {\n  afterEach(() => {\n    onClick.mockRestore()\n  })\n\n  it('should render', () => {\n    expect(\n      render(<OAuthForm>{(children) => <>{children}</>}</OAuthForm>),\n    ).toBeTruthy()\n  })\n\n  it('should call proper actions after click on google', () => {\n    render(\n      <OAuthForm onClick={onClick}>{(children) => <>{children}</>}</OAuthForm>,\n    )\n\n    fireEvent.click(screen.getByTestId('google-oauth'))\n\n    expect(onClick).toBeCalledWith(OAuthStrategy.Google)\n  })\n\n  it('should call proper actions after click on sso', async () => {\n    render(\n      <OAuthForm onClick={onClick}>{(children) => <>{children}</>}</OAuthForm>,\n    )\n\n    fireEvent.click(screen.getByTestId('sso-oauth'))\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sso-email'), {\n        target: { value: MOCK_OAUTH_SSO_EMAIL },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED,\n      eventData: {},\n    })\n\n    expect(onClick).toBeCalledWith(OAuthStrategy.SSO)\n  })\n\n  it('should go back to main oauth form by clicking to back button', async () => {\n    render(\n      <OAuthForm onClick={onClick}>{(children) => <>{children}</>}</OAuthForm>,\n    )\n\n    fireEvent.click(screen.getByTestId('sso-oauth'))\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n    expect(screen.getByTestId('btn-back')).toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('btn-back'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_CANCELED,\n      eventData: {},\n    })\n\n    expect(screen.getByTestId('sso-oauth')).toBeInTheDocument()\n  })\n\n  it('should disable submit button id incorrect email provided', async () => {\n    render(\n      <OAuthForm onClick={onClick}>{(children) => <>{children}</>}</OAuthForm>,\n    )\n\n    fireEvent.click(screen.getByTestId('sso-oauth'))\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sso-email'), {\n        target: { value: 'bad-email' },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('btn-submit'))\n    })\n\n    await waitFor(() => screen.getByTestId('btn-submit-tooltip'))\n\n    expect(screen.getByTestId('btn-submit-tooltip')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-form/OAuthForm.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { signIn } from 'uiSrc/slices/oauth/cloud'\nimport { OAuthSocialAction, OAuthStrategy } from 'uiSrc/slices/interfaces'\nimport { ipcAuth } from 'uiSrc/electron/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { enableUserAnalyticsAction } from 'uiSrc/slices/user/user-settings'\nimport OAuthSsoForm from './components/oauth-sso-form'\nimport OAuthSocialButtons from '../oauth-social-buttons'\nimport { Props as OAuthSocialButtonsProps } from '../oauth-social-buttons/OAuthSocialButtons'\n\nexport interface Props extends OAuthSocialButtonsProps {\n  action: OAuthSocialAction\n  children: (form: React.ReactNode) => JSX.Element\n}\n\nconst OAuthForm = ({ children, action, onClick, ...rest }: Props) => {\n  const dispatch = useDispatch()\n\n  const [authStrategy, setAuthStrategy] = useState('')\n  const [disabled, setDisabled] = useState(false)\n\n  const initOAuthProcess = (\n    strategy: OAuthStrategy,\n    action: string,\n    data?: {},\n  ) => {\n    dispatch(signIn())\n    ipcAuth(strategy, action, data)\n  }\n\n  const onSocialButtonClick = (authStrategy: OAuthStrategy) => {\n    setDisabled(true)\n    setTimeout(() => {\n      setDisabled(false)\n    }, 1000)\n    dispatch(enableUserAnalyticsAction(authStrategy))\n    setAuthStrategy(authStrategy)\n    onClick?.(authStrategy)\n\n    switch (authStrategy) {\n      case OAuthStrategy.Google:\n      case OAuthStrategy.GitHub:\n        initOAuthProcess(authStrategy, action)\n        break\n      case OAuthStrategy.SSO:\n        // ignore. sso email form will be shown\n        break\n      default:\n        break\n    }\n  }\n\n  const onSsoBackButtonClick = () => {\n    setAuthStrategy('')\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_CANCELED,\n      eventData: {\n        action,\n      },\n    })\n  }\n\n  const onSsoLoginButtonClick = (data: {}) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED,\n      eventData: {\n        action,\n      },\n    })\n    initOAuthProcess(OAuthStrategy.SSO, action, data)\n  }\n\n  if (authStrategy === OAuthStrategy.SSO) {\n    return (\n      <OAuthSsoForm\n        onBack={onSsoBackButtonClick}\n        onSubmit={onSsoLoginButtonClick}\n      />\n    )\n  }\n\n  return children(\n    <OAuthSocialButtons\n      onClick={onSocialButtonClick}\n      {...rest}\n      disabled={disabled}\n    />,\n  )\n}\n\nexport default OAuthForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-form/components/oauth-sso-form/OAuthSsoForm.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledOAuthCOntainer = styled(Col)`\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-form/components/oauth-sso-form/OAuthSsoForm.tsx",
    "content": "import { isEmpty } from 'lodash'\nimport React, { useState } from 'react'\nimport { FormikErrors, useFormik } from 'formik'\nimport { validateEmail, validateField } from 'uiSrc/utils'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { StyledOAuthCOntainer } from './OAuthSsoForm.styles'\n\nexport interface Props {\n  onBack: () => void\n  onSubmit: (values: { email: string }) => any\n}\n\ninterface Values {\n  email: string\n}\n\nconst OAuthSsoForm = ({ onBack, onSubmit }: Props) => {\n  const [validationErrors, setValidationErrors] = useState<\n    FormikErrors<Values>\n  >({ email: '' })\n\n  const validate = (values: Values) => {\n    const errs: FormikErrors<Values> = {}\n\n    if (!values?.email || !validateEmail(values.email)) {\n      errs.email = 'Invalid email'\n    }\n\n    setValidationErrors(errs)\n\n    return errs\n  }\n\n  const formik = useFormik({\n    initialValues: {\n      email: '',\n    },\n    validate,\n    onSubmit,\n  })\n\n  const submitIsDisabled = () => !isEmpty(validationErrors)\n\n  const SubmitButton = ({\n    text,\n    disabled,\n  }: {\n    disabled: boolean\n    text: string\n  }) => (\n    <RiTooltip\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      data-testid=\"btn-submit-tooltip\"\n      content={\n        disabled ? (\n          <>\n            <p>Email must be in the format</p>\n            <p>email@example.com without spaces</p>\n          </>\n        ) : null\n      }\n    >\n      <PrimaryButton\n        size=\"s\"\n        type=\"submit\"\n        disabled={disabled}\n        icon={disabled ? InfoIcon : undefined}\n        data-testid=\"btn-submit\"\n      >\n        {text}\n      </PrimaryButton>\n    </RiTooltip>\n  )\n\n  return (\n    <StyledOAuthCOntainer gap=\"xxl\" data-testid=\"oauth-container-sso-form\">\n      <Title size=\"S\" color=\"primary\">\n        Single Sign-On\n      </Title>\n      <form onSubmit={formik.handleSubmit}>\n        <Row grow>\n          <FlexItem grow>\n            <FormField label=\"Email\">\n              <TextInput\n                name=\"email\"\n                id=\"sso-email\"\n                data-testid=\"sso-email\"\n                maxLength={200}\n                value={formik.values.email}\n                autoComplete=\"off\"\n                onChange={(value) => {\n                  formik.setFieldValue('email', validateField(value.trim()))\n                }}\n              />\n            </FormField>\n          </FlexItem>\n        </Row>\n        <Spacer />\n        <Row justify=\"end\" gap=\"m\">\n          <FlexItem>\n            <SecondaryButton\n              type=\"button\"\n              size=\"s\"\n              onClick={onBack}\n              data-testid=\"btn-back\"\n            >\n              Back\n            </SecondaryButton>\n          </FlexItem>\n          <FlexItem>\n            <SubmitButton text=\"Login\" disabled={submitIsDisabled()} />\n          </FlexItem>\n        </Row>\n      </form>\n    </StyledOAuthCOntainer>\n  )\n}\n\nexport default OAuthSsoForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-form/components/oauth-sso-form/index.ts",
    "content": "import OAuthSsoForm from './OAuthSsoForm'\n\nexport default OAuthSsoForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-form/index.ts",
    "content": "import OAuthForm from './OAuthForm'\n\nexport default OAuthForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-recommended-settings/OAuthRecommendedSettings.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport OAuthRecommendedSettings from './OAuthRecommendedSettings'\n\ndescribe('OAuthRecommendedSettings', () => {\n  it('should render', () => {\n    expect(\n      render(<OAuthRecommendedSettings value onChange={jest.fn} />),\n    ).toBeTruthy()\n  })\n\n  it('should call onChange after change value', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSsoRecommendedSettings}`,\n      { flag: true },\n    )\n    const onChange = jest.fn()\n    render(<OAuthRecommendedSettings value onChange={onChange} />, {\n      store: mockStore(initialStoreState),\n    })\n\n    fireEvent.click(screen.getByTestId('oauth-recommended-settings-checkbox'))\n\n    expect(onChange).toBeCalledWith(false)\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSsoRecommendedSettings}`,\n      { flag: true },\n    )\n\n    render(<OAuthRecommendedSettings value onChange={jest.fn} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByTestId('oauth-recommended-settings-checkbox'),\n    ).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSsoRecommendedSettings}`,\n      { flag: false },\n    )\n\n    render(<OAuthRecommendedSettings value onChange={jest.fn} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByTestId('oauth-recommended-settings-checkbox'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-recommended-settings/OAuthRecommendedSettings.tsx",
    "content": "import React from 'react'\nimport { FeatureFlagComponent, RiTooltip } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\n\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  value?: boolean\n  onChange: (value: boolean) => void\n}\n\nconst OAuthRecommendedSettings = (props: Props) => {\n  const { value, onChange } = props\n\n  return (\n    <FeatureFlagComponent name={FeatureFlags.cloudSsoRecommendedSettings}>\n      <Row align=\"start\">\n        <Checkbox\n          id=\"ouath-recommended-settings\"\n          name=\"recommended-settings\"\n          label=\"Use a pre-selected provider and region\"\n          labelSize=\"M\"\n          checked={value}\n          onChange={(e) => onChange(e.target.checked)}\n          data-testid=\"oauth-recommended-settings-checkbox\"\n        />\n        <RiTooltip\n          content={\n            <>\n              The database will be automatically created using a pre-selected\n              provider and region.\n              <br />\n              You can change it by signing in to Redis Cloud.\n            </>\n          }\n          position=\"top\"\n        >\n          <RiIcon type=\"InfoIcon\" size=\"l\" />\n        </RiTooltip>\n      </Row>\n      <Spacer size=\"s\" />\n    </FeatureFlagComponent>\n  )\n}\n\nexport default OAuthRecommendedSettings\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-recommended-settings/index.ts",
    "content": "import OAuthRecommendedSettings from './OAuthRecommendedSettings'\n\nexport default OAuthRecommendedSettings\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/OAuthSocialButtons.spec.tsx",
    "content": "import React from 'react'\nimport { render, fireEvent, screen } from 'uiSrc/utils/test-utils'\nimport { OAuthStrategy } from 'uiSrc/slices/interfaces'\nimport OAuthSocialButtons from './OAuthSocialButtons'\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    source: 'source',\n  }),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n}))\n\nconst onClick = jest.fn()\n\ndescribe('OAuthSocialButtons', () => {\n  it('should render', () => {\n    expect(render(<OAuthSocialButtons />)).toBeTruthy()\n  })\n\n  it('should call proper actions after click on google', () => {\n    render(<OAuthSocialButtons onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('google-oauth'))\n\n    expect(onClick).toBeCalledWith(OAuthStrategy.Google)\n  })\n\n  it('should call proper actions after click on github', () => {\n    render(<OAuthSocialButtons onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('github-oauth'))\n\n    expect(onClick).toBeCalledWith(OAuthStrategy.GitHub)\n  })\n\n  it('should call proper actions after click on sso', () => {\n    render(<OAuthSocialButtons onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('sso-oauth'))\n\n    expect(onClick).toBeCalledWith(OAuthStrategy.SSO)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/OAuthSocialButtons.styles.ts",
    "content": "import styled from 'styled-components'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\n\nexport const StyledSocialButton = styled(EmptyButton)<{ $inline?: boolean }>`\n  padding: 8px;\n  transition: transform 0.3s ease;\n\n  &:hover,\n  &:focus {\n    background: none;\n    transform: translateY(-1px);\n  }\n\n  ${({ $inline }) =>\n    $inline &&\n    `    \n    svg {\n        height: 20px;\n        width: 20px;\n    }\n  `}\n\n  svg {\n    height: 34px;\n    width: 34px;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/OAuthSocialButtons.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { oauthCloudPAgreementSelector } from 'uiSrc/slices/oauth/cloud'\nimport { OAuthStrategy } from 'uiSrc/slices/interfaces'\n\nimport { FlexGroup, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { AllIconsType, RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { StyledSocialButton } from './OAuthSocialButtons.styles'\n\nexport interface Props {\n  onClick: (authStrategy: OAuthStrategy) => void\n  className?: string\n  inline?: boolean\n  disabled?: boolean\n}\n\nconst socialLinks = [\n  {\n    text: 'Google',\n    icon: 'GoogleSigninIcon',\n    label: 'google-oauth',\n    strategy: OAuthStrategy.Google,\n  },\n  {\n    text: 'Github',\n    icon: 'GithubIcon',\n    label: 'github-oauth',\n    strategy: OAuthStrategy.GitHub,\n  },\n  {\n    text: 'SSO',\n    icon: 'SsoIcon',\n    label: 'sso-oauth',\n    strategy: OAuthStrategy.SSO,\n  },\n]\n\nconst OAuthSocialButtons = (props: Props) => {\n  const { onClick, className, inline, disabled } = props\n\n  const agreement = useSelector(oauthCloudPAgreementSelector)\n\n  return (\n    <Row\n      gap=\"l\"\n      align=\"center\"\n      justify=\"between\"\n      className={className}\n      data-testid=\"oauth-container-social-buttons\"\n    >\n      {socialLinks.map(({ strategy, text, icon, label }) => (\n        <RiTooltip\n          key={label}\n          position=\"top\"\n          anchorClassName={!agreement ? 'euiToolTip__btn-disabled' : ''}\n          content={agreement ? null : 'Acknowledge the agreement'}\n          data-testid={`${label}-tooltip`}\n        >\n          <StyledSocialButton\n            variant=\"primary-inline\"\n            disabled={!agreement || disabled}\n            $inline={inline}\n            onClick={() => {\n              onClick(strategy)\n            }}\n            data-testid={label}\n            aria-labelledby={label}\n          >\n            <FlexGroup\n              direction={inline ? 'row' : 'column'}\n              align=\"center\"\n              justify=\"center\"\n              gap=\"m\"\n            >\n              <RiIcon type={icon as AllIconsType} />\n              <Text color=\"primary\">{text}</Text>\n            </FlexGroup>\n          </StyledSocialButton>\n        </RiTooltip>\n      ))}\n    </Row>\n  )\n}\n\nexport default OAuthSocialButtons\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/oauth-social-buttons/index.ts",
    "content": "import OAuthSocialButtons from './OAuthSocialButtons'\n\nexport default OAuthSocialButtons\n"
  },
  {
    "path": "redisinsight/ui/src/components/oauth/shared/styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledAdvantagesContainerAbsolute = styled(FlexItem)`\n  background: ${({ theme }) => theme.semantic.color.background.neutral200};\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 100%;\n  padding: ${({ theme }) => theme.core.space.space400};\n  border-radius: 0.8rem;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent } from '@testing-library/react'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  cleanup,\n  clearStoreActions,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { OnboardingTour } from 'uiSrc/components'\nimport {\n  appFeatureFlagsFeaturesSelector,\n  appFeatureOnboardingSelector,\n  setOnboardNextStep,\n  setOnboardPrevStep,\n} from 'uiSrc/slices/app/features'\nimport { keysDataSelector } from 'uiSrc/slices/browser/keys'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport {\n  openCli,\n  openCliHelper,\n  resetCliHelperSettings,\n  resetCliSettings,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { setMonitorInitialState, showMonitor } from 'uiSrc/slices/cli/monitor'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport {\n  dbAnalysisSelector,\n  setDatabaseAnalysisViewTab,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport {\n  fetchRedisearchListAction,\n  loadList,\n} from 'uiSrc/slices/browser/redisearch'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { ONBOARDING_FEATURES } from './OnboardingFeatures'\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureOnboardingSelector: jest.fn().mockReturnValue({\n    currentStep: 0,\n    isActive: true,\n    totalSteps: 14,\n  }),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    databaseChat: {\n      flag: false,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  keysDataSelector: jest.fn().mockReturnValue({\n    total: 0,\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/redisearch'),\n  fetchRedisearchListAction: jest\n    .fn()\n    .mockImplementation(\n      jest.requireActual('uiSrc/slices/browser/redisearch')\n        .fetchRedisearchListAction,\n    ),\n}))\n\njest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'),\n  dbAnalysisSelector: jest.fn().mockReturnValue({\n    data: {\n      recommendations: [],\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\nconst getEventProperties = (action: string, step: OnboardingStepName) => ({\n  event: TelemetryEvent.ONBOARDING_TOUR_CLICKED,\n  eventData: {\n    action,\n    databaseId: '',\n    step,\n  },\n})\n\nconst checkAllTelemetryButtons = (\n  stepName: OnboardingStepName,\n  sendEventTelemetry: jest.Mock,\n) => {\n  fireEvent.click(screen.getByTestId('next-btn'))\n  expect(sendEventTelemetry).toBeCalledWith(\n    getEventProperties('next', stepName),\n  )\n  sendEventTelemetry.mockRestore()\n\n  fireEvent.click(screen.getByTestId('back-btn'))\n  expect(sendEventTelemetry).toBeCalledWith(\n    getEventProperties('back', stepName),\n  )\n  sendEventTelemetry.mockRestore()\n\n  fireEvent.click(screen.getByTestId('skip-tour-btn'))\n  expect(sendEventTelemetry).toBeCalledWith(\n    getEventProperties('closed', stepName),\n  )\n  sendEventTelemetry.mockRestore()\n}\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ONBOARDING_FEATURES', () => {\n  describe('BROWSER_PAGE', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserPage,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PAGE}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n    })\n\n    it('should render proper text without keys', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Add a key to your database using a dedicated form.',\n      )\n    })\n\n    it('should render proper text with keys', () => {\n      ;(keysDataSelector as jest.Mock).mockReturnValueOnce({\n        total: 10,\n      })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      expect(screen.getByTestId('step-content')).not.toHaveTextContent(\n        'Add a key to your database using a dedicated form.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(sendEventTelemetry).toBeCalledWith(\n        getEventProperties('next', OnboardingStepName.BrowserWithoutKeys),\n      )\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n      fireEvent.click(screen.getByTestId('skip-tour-btn'))\n      expect(sendEventTelemetry).toBeCalledWith(\n        getEventProperties('closed', OnboardingStepName.BrowserWithoutKeys),\n      )\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n  })\n\n  describe('BROWSER_TREE_VIEW', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserTreeView,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_TREE_VIEW}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Switch from List to Tree view to see keys grouped',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_TREE_VIEW}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserTreeView,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n  })\n\n  describe('BROWSER_FILTER_SEARCH', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserFilterSearch,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_FILTER_SEARCH}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Choose between filtering your data based on key name',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_FILTER_SEARCH}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserFilters,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_FILTER_SEARCH}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [\n        setOnboardNextStep(),\n        openCli(),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper actions with enabled chat', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        databaseChat: {\n          flag: true,\n        },\n      })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_FILTER_SEARCH}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [\n        changeSidePanel(SidePanels.AiAssistant),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('BROWSER_COPILOT', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserCopilot,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COPILOT}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Redis Copilot is an AI-powered companion that lets you learn about',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COPILOT}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserCopilot,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COPILOT}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [\n        openCli(),\n        changeSidePanel(null),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('BROWSER_CLI', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserCLI,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_CLI}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Use CLI to run Redis commands.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_CLI}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserCLI,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions on next', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_CLI}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [openCliHelper(), setOnboardNextStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper actions on back', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_CLI}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      const expectedActions = [setOnboardPrevStep(), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper actions on back when chat available', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        databaseChat: {\n          flag: true,\n        },\n      })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_CLI}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      const expectedActions = [\n        changeSidePanel(SidePanels.AiAssistant),\n        setOnboardPrevStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('BROWSER_COMMAND_HELPER', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserCommandHelper,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COMMAND_HELPER}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Command Helper lets you search and learn more about Redis commands',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COMMAND_HELPER}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserCommandHelper,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions on back', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COMMAND_HELPER}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      const expectedActions = [openCli(), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper actions on next', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_COMMAND_HELPER}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [showMonitor(), setOnboardNextStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('BROWSER_PROFILER', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserProfiler,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PROFILER}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Use Profiler to track commands sent against the Redis server in real-time.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PROFILER}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserProfiler,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions on back', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PROFILER}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      const expectedActions = [openCliHelper(), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should skip vector search step and navigate to workbench on next when vectorSearchV2 is off', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PROFILER}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      expect(pushMock).toHaveBeenCalledWith(Pages.workbench(''))\n\n      const expectedActions = [\n        resetCliSettings(),\n        resetCliHelperSettings(),\n        setMonitorInitialState(),\n        setOnboardNextStep(),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should navigate to vector search on next when vectorSearchV2 is on', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        databaseChat: { flag: false },\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_PROFILER}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      expect(pushMock).toHaveBeenCalledWith(Pages.vectorSearch(''))\n\n      const expectedActions = [\n        resetCliSettings(),\n        resetCliHelperSettings(),\n        setMonitorInitialState(),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('VECTOR_SEARCH_PAGE', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.VectorSearchPage,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.VECTOR_SEARCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'This is Search, where you can index your data and query it using Redis Query Engine.',\n      )\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Load sample data to create your first index and run sample queries to see results instantly.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.VECTOR_SEARCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.VectorSearchIntro,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should navigate to browser and show monitor on back', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.VECTOR_SEARCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      expect(pushMock).toHaveBeenCalledWith(Pages.browser(''))\n\n      const expectedActions = [showMonitor(), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions().slice(-2))).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should navigate to workbench on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.VECTOR_SEARCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      expect(pushMock).toHaveBeenCalledWith(Pages.workbench(''))\n    })\n  })\n\n  describe.skip('BROWSER_INSIGHTS', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.BrowserInsights,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_INSIGHTS}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Insights will help you optimize performance and memory usage',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_INSIGHTS}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.BrowserInsights,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions on back', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_INSIGHTS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      const expectedActions = [showMonitor(), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper actions on next', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_INSIGHTS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [setOnboardNextStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should properly push history on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_INSIGHTS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.workbench(''))\n    })\n  })\n\n  describe('WORKBENCH_PAGE', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.WorkbenchPage,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'This is Workbench, our advanced CLI for Redis commands.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.WorkbenchIntro,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions on mount', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n\n      const expectedActions = [loadList()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should render FT.INFO when there are indexes in database', () => {\n      const fetchRedisearchListActionMock = (\n        onSuccess?: (indexes: RedisResponseBuffer[]) => void,\n      ) =>\n        jest\n          .fn()\n          .mockImplementation(() => onSuccess?.([stringToBuffer('someIndex')]))\n\n      ;(fetchRedisearchListAction as jest.Mock).mockImplementation(\n        fetchRedisearchListActionMock,\n      )\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n\n      expect(screen.getByTestId('wb-onboarding-command')).toHaveTextContent(\n        'FT.INFO someIndex',\n      )\n    })\n\n    it('should render CLIENT LIST when there are no indexes in database', () => {\n      const fetchRedisearchListActionMock = (\n        onSuccess?: (indexes: RedisResponseBuffer[]) => void,\n      ) => jest.fn().mockImplementation(() => onSuccess?.([]))\n\n      ;(fetchRedisearchListAction as jest.Mock).mockImplementation(\n        fetchRedisearchListActionMock,\n      )\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n\n      expect(screen.getByTestId('wb-onboarding-command')).toHaveTextContent(\n        'CLIENT LIST',\n      )\n    })\n\n    it('should skip vector search step and navigate to browser on back when vectorSearchV2 is off', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      expect(pushMock).toHaveBeenCalledWith(Pages.browser(''))\n\n      const expectedActions = [\n        setOnboardPrevStep(),\n        showMonitor(),\n        setOnboardPrevStep(),\n      ]\n      expect(clearStoreActions(store.getActions().slice(-3))).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should navigate to vector search on back when vectorSearchV2 is on', () => {\n      ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n        databaseChat: { flag: false },\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      expect(pushMock).toHaveBeenCalledWith(Pages.vectorSearch(''))\n    })\n\n    it('should call proper actions on next', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.WORKBENCH_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [\n        changeSelectedTab(InsightsPanelTabs.Explore),\n        changeSidePanel(SidePanels.Insights),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions().slice(-3))).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('EXPLORE_REDIS', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.Tutorials,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_REDIS}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Learn more about how Redis can solve your use cases using interactive Tutorials.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_REDIS}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.ExploreTutorials,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should properly push history on back', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_REDIS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.workbench(''))\n    })\n\n    it('should call proper actions on back', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_REDIS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n\n      const expectedActions = [changeSidePanel(null), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions().slice(-2))).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper actions on next', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_REDIS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [\n        resetExplorePanelSearch(),\n        setExplorePanelIsPageOpen(false),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions().slice(-3))).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('WORKBENCH_CUSTOM_TUTORIALS', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.CustomTutorials,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.EXPLORE_CUSTOM_TUTORIALS}\n          >\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Share your Redis expertise with your team and the wider community by building custom Redis Insight tutorials.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_CUSTOM_TUTORIALS}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.ExploreCustomTutorials,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should call proper actions on next', () => {\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_CUSTOM_TUTORIALS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n\n      const expectedActions = [changeSidePanel(null), setOnboardNextStep()]\n      expect(clearStoreActions(store.getActions().slice(-2))).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should properly push history on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.EXPLORE_CUSTOM_TUTORIALS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.clusterDetails(''))\n    })\n  })\n\n  describe('ANALYTICS_OVERVIEW', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.AnalyticsOverview,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_OVERVIEW}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Investigate memory and key allocation in your cluster database and monitor',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_OVERVIEW}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.ClusterOverview,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should properly push history on back', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_OVERVIEW}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.workbench(''))\n    })\n\n    it('should properly push history on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_OVERVIEW}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.databaseAnalysis(''))\n    })\n  })\n\n  describe('ANALYTICS_DATABASE_ANALYSIS', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.AnalyticsDatabaseAnalysis,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.ANALYTICS_DATABASE_ANALYSIS}\n          >\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Use Database Analysis to get summary of your database and receive',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.ANALYTICS_DATABASE_ANALYSIS}\n        >\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.DatabaseAnalysisOverview,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should properly push history on back', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.ANALYTICS_DATABASE_ANALYSIS}\n        >\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.workbench(''))\n    })\n\n    it('should properly push history on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.ANALYTICS_DATABASE_ANALYSIS}\n        >\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.slowLog(''))\n    })\n\n    it('should call proper actions on next with recommendations', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n      ;(dbAnalysisSelector as jest.Mock).mockReturnValue({\n        data: {\n          recommendations: [{}],\n        },\n      })\n\n      render(\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.ANALYTICS_DATABASE_ANALYSIS}\n        >\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).not.toHaveBeenCalled()\n\n      const expectedActions = [\n        setDatabaseAnalysisViewTab(DatabaseAnalysisViewTab.Recommendations),\n        setOnboardNextStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('ANALYTICS_RECOMMENDATIONS', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.AnalyticsRecommendations,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.ANALYTICS_RECOMMENDATIONS}\n          >\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'See tips to optimize the memory usage, performance',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_RECOMMENDATIONS}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.DatabaseAnalysisRecommendations,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should properly push history on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_RECOMMENDATIONS}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.slowLog(''))\n    })\n  })\n\n  describe('ANALYTICS_SLOW_LOG', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.AnalyticsSlowLog,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_SLOW_LOG}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Check Slow Log to troubleshoot performance issues.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_SLOW_LOG}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.SlowLog,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should properly push history on back with recommendations', () => {\n      ;(dbAnalysisSelector as jest.Mock).mockReturnValueOnce({\n        data: {\n          recommendations: [{}],\n        },\n      })\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_SLOW_LOG}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.databaseAnalysis(''))\n\n      const expectedActions = [\n        setDatabaseAnalysisViewTab(DatabaseAnalysisViewTab.Recommendations),\n        setOnboardPrevStep(),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should properly actions on back', () => {\n      ;(dbAnalysisSelector as jest.Mock).mockReturnValueOnce({\n        data: {\n          recommendations: [],\n        },\n      })\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_SLOW_LOG}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.databaseAnalysis(''))\n\n      const expectedActions = [setOnboardPrevStep(), setOnboardPrevStep()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('should call proper history push on next ', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.ANALYTICS_SLOW_LOG}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.pubSub(''))\n    })\n  })\n\n  describe('PUB_SUB_PAGE', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.PubSubPage,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.PUB_SUB_PAGE}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Use Redis pub/sub to subscribe to channels and post messages to channels.',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.PUB_SUB_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      checkAllTelemetryButtons(\n        OnboardingStepName.PubSub,\n        sendEventTelemetry as jest.Mock,\n      )\n    })\n\n    it('should properly push history on back', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.PUB_SUB_PAGE}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.slowLog(''))\n    })\n  })\n\n  describe('FINISH', () => {\n    beforeEach(() => {\n      ;(appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n        currentStep: OnboardingSteps.Finish,\n        isActive: true,\n        totalSteps: Object.keys(ONBOARDING_FEATURES).length,\n      })\n    })\n\n    it('should render', () => {\n      expect(\n        render(\n          <OnboardingTour options={ONBOARDING_FEATURES.FINISH}>\n            <span />\n          </OnboardingTour>,\n        ),\n      ).toBeTruthy()\n      expect(screen.getByTestId('step-content')).toHaveTextContent(\n        'Take me back to Browser',\n      )\n    })\n\n    it('should call proper telemetry events', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.FINISH}>\n          <span />\n        </OnboardingTour>,\n      )\n\n      fireEvent.click(screen.getByTestId('back-btn'))\n      expect(sendEventTelemetry).toBeCalledWith(\n        getEventProperties('back', OnboardingStepName.Finish),\n      )\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n      fireEvent.click(screen.getByTestId('close-tour-btn'))\n      expect(sendEventTelemetry).toBeCalledWith(\n        getEventProperties('closed', OnboardingStepName.Finish),\n      )\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.ONBOARDING_TOUR_FINISHED,\n        eventData: {\n          databaseId: '',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(sendEventTelemetry).toBeCalledWith(\n        getEventProperties('next', OnboardingStepName.Finish),\n      )\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.ONBOARDING_TOUR_FINISHED,\n        eventData: {\n          databaseId: '',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n\n    it('should properly push history on next', () => {\n      const pushMock = jest.fn()\n      reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n      render(\n        <OnboardingTour options={ONBOARDING_FEATURES.FINISH}>\n          <span />\n        </OnboardingTour>,\n      )\n      fireEvent.click(screen.getByTestId('next-btn'))\n      expect(pushMock).toHaveBeenCalledWith(Pages.browser(''))\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-features/OnboardingFeatures.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { isString, partialRight } from 'lodash'\nimport { keysDataSelector } from 'uiSrc/slices/browser/keys'\nimport {\n  openCli,\n  openCliHelper,\n  resetCliHelperSettings,\n  resetCliSettings,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { setMonitorInitialState, showMonitor } from 'uiSrc/slices/cli/monitor'\nimport { Pages } from 'uiSrc/constants/pages'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  dbAnalysisSelector,\n  setDatabaseAnalysisViewTab,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport {\n  appFeatureFlagsFeaturesSelector,\n  incrementOnboardStepAction,\n  setOnboardNextStep,\n  setOnboardPrevStep,\n} from 'uiSrc/slices/app/features'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport OnboardingEmoji from 'uiSrc/assets/img/onboarding-emoji.svg?react'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'\n\nimport { fetchRedisearchListAction } from 'uiSrc/slices/browser/redisearch'\nimport { bufferToString, Nullable } from 'uiSrc/utils'\nimport { CodeBlock } from 'uiSrc/components'\n\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { isAnyFeatureEnabled } from 'uiSrc/utils/features'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport styles from './styles.module.scss'\n\nconst sendTelemetry = (databaseId: string, step: string, action: string) =>\n  sendEventTelemetry({\n    event: TelemetryEvent.ONBOARDING_TOUR_CLICKED,\n    eventData: {\n      databaseId,\n      step,\n      action,\n    },\n  })\n\ntype TelemetryArgs = [string, OnboardingStepName]\n\nconst sendBackTelemetryEvent = partialRight(sendTelemetry, 'back')\nconst sendNextTelemetryEvent = partialRight(sendTelemetry, 'next')\nconst sendClosedTelemetryEvent = partialRight(sendTelemetry, 'closed')\n\nconst ONBOARDING_FEATURES = {\n  BROWSER_PAGE: {\n    step: OnboardingSteps.BrowserPage,\n    title: 'Browser',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const { total } = useSelector(keysDataSelector)\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        total\n          ? OnboardingStepName.BrowserWithKeys\n          : OnboardingStepName.BrowserWithoutKeys,\n      ]\n\n      return {\n        content: total ? (\n          'This is Browser, where you can see the list of keys, filter them, perform bulk operations, and view the values.'\n        ) : (\n          <>\n            This is Browser, where you can see the list of keys in the plain\n            List or Tree view, filter them, perform bulk operations, and view\n            the values.\n            <Spacer size=\"xs\" />\n            Add a key to your database using a dedicated form.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onNext: () => sendNextTelemetryEvent(...telemetryArgs),\n      }\n    },\n  },\n  BROWSER_TREE_VIEW: {\n    step: OnboardingSteps.BrowserTreeView,\n    title: 'Tree view',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.BrowserTreeView,\n      ]\n\n      return {\n        content:\n          'Switch from List to Tree view to see keys grouped into folders based on their namespaces.',\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => sendBackTelemetryEvent(...telemetryArgs),\n        onNext: () => sendNextTelemetryEvent(...telemetryArgs),\n      }\n    },\n  },\n  BROWSER_FILTER_SEARCH: {\n    step: OnboardingSteps.BrowserFilterSearch,\n    title: 'Filter and search',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const {\n        [FeatureFlags.databaseChat]: databaseChatFeature,\n        [FeatureFlags.documentationChat]: documentationChatFeature,\n      } = useSelector(appFeatureFlagsFeaturesSelector)\n      const isAnyChatAvailable = isAnyFeatureEnabled([\n        databaseChatFeature,\n        documentationChatFeature,\n      ])\n\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.BrowserFilters,\n      ]\n\n      return {\n        content:\n          'Choose between filtering your data based on key name or pattern. Or perform full-text search across all your data.',\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => sendBackTelemetryEvent(...telemetryArgs),\n        onNext: () => {\n          if (isAnyChatAvailable) {\n            dispatch(changeSidePanel(SidePanels.AiAssistant))\n            sendNextTelemetryEvent(...telemetryArgs)\n            return\n          }\n\n          dispatch(setOnboardNextStep())\n          dispatch(openCli())\n\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  BROWSER_COPILOT: {\n    step: OnboardingSteps.BrowserCopilot,\n    title: 'Try Redis Copilot',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.BrowserCopilot,\n      ]\n\n      return {\n        content:\n          'Redis Copilot is an AI-powered companion that lets you learn about Redis and explore your data, in a conversational manner, while also providing context-aware assistance to build search queries.',\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => sendBackTelemetryEvent(...telemetryArgs),\n        onNext: () => {\n          dispatch(openCli())\n          dispatch(changeSidePanel(null))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  BROWSER_CLI: {\n    step: OnboardingSteps.BrowserCLI,\n    title: 'CLI',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const {\n        [FeatureFlags.databaseChat]: databaseChatFeature,\n        [FeatureFlags.documentationChat]: documentationChatFeature,\n      } = useSelector(appFeatureFlagsFeaturesSelector)\n      const isAnyChatAvailable = isAnyFeatureEnabled([\n        databaseChatFeature,\n        documentationChatFeature,\n      ])\n\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.BrowserCLI,\n      ]\n\n      return {\n        content: 'Use CLI to run Redis commands.',\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          if (isAnyChatAvailable) {\n            dispatch(changeSidePanel(SidePanels.AiAssistant))\n            sendNextTelemetryEvent(...telemetryArgs)\n            return\n          }\n\n          dispatch(setOnboardPrevStep())\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          dispatch(openCliHelper())\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  BROWSER_COMMAND_HELPER: {\n    step: OnboardingSteps.BrowserCommandHelper,\n    title: 'Command Helper',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.BrowserCommandHelper,\n      ]\n\n      return {\n        content: (\n          <>\n            Command Helper lets you search and learn more about Redis commands,\n            their syntax, and details.\n            <Spacer size=\"xs\" />\n            Run <b>PING</b> in CLI to see how it works.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          dispatch(openCli())\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          dispatch(showMonitor())\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  BROWSER_PROFILER: {\n    step: OnboardingSteps.BrowserProfiler,\n    title: 'Profiler',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const { [FeatureFlags.vectorSearchV2]: vectorSearchFeature } =\n        useSelector(appFeatureFlagsFeaturesSelector)\n\n      const dispatch = useDispatch()\n      const history = useHistory()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.BrowserProfiler,\n      ]\n\n      return {\n        content: (\n          <>\n            Use Profiler to track commands sent against the Redis server in\n            real-time.\n            <Spacer size=\"xs\" />\n            Select <b>Start Profiler</b> to stream back every command processed\n            by the Redis server. Save the log to download and investigate\n            commands.\n            <Spacer size=\"xs\" />\n            <i>Tip: Remember to stop Profiler to avoid throughput decrease.</i>\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          dispatch(openCliHelper())\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          dispatch(resetCliSettings())\n          dispatch(resetCliHelperSettings())\n          dispatch(setMonitorInitialState())\n\n          if (vectorSearchFeature?.flag) {\n            history.push(Pages.vectorSearch(connectedInstanceId))\n          } else {\n            dispatch(setOnboardNextStep())\n            history.push(Pages.workbench(connectedInstanceId))\n          }\n\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  VECTOR_SEARCH_PAGE: {\n    step: OnboardingSteps.VectorSearchPage,\n    title: 'Search',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n\n      const dispatch = useDispatch()\n      const history = useHistory()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.VectorSearchIntro,\n      ]\n\n      return {\n        content: (\n          <>\n            This is Search, where you can index your data and query it using\n            Redis Query Engine. Run full-text search, vector similarity, and\n            filtered queries right from the UI.\n            <Spacer size=\"xs\" />\n            Load sample data to create your first index and run sample queries\n            to see results instantly.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          history.push(Pages.browser(connectedInstanceId))\n          dispatch(showMonitor())\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          history.push(Pages.workbench(connectedInstanceId))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  WORKBENCH_PAGE: {\n    step: OnboardingSteps.WorkbenchPage,\n    title: 'Try Workbench!',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const { [FeatureFlags.vectorSearchV2]: vectorSearchFeature } =\n        useSelector(appFeatureFlagsFeaturesSelector)\n      const [firstIndex, setFirstIndex] = useState<Nullable<string>>(null)\n\n      const dispatch = useDispatch()\n      const history = useHistory()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.WorkbenchIntro,\n      ]\n\n      useEffect(() => {\n        dispatch(\n          fetchRedisearchListAction(\n            (indexes) => {\n              setFirstIndex(indexes?.length ? bufferToString(indexes[0]) : '')\n            },\n            () => setFirstIndex(''),\n            false,\n          ),\n        )\n      }, [])\n\n      return {\n        content: (\n          <>\n            This is Workbench, our advanced CLI for Redis commands.\n            <Spacer size=\"xs\" />\n            Take advantage of syntax highlighting, intelligent auto-complete,\n            and working with commands in editor mode.\n            <Spacer size=\"xs\" />\n            Workbench visualizes complex{' '}\n            <Link\n              href={EXTERNAL_LINKS.redisStack}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              variant=\"inline\"\n            >\n              Redis Stack\n            </Link>{' '}\n            data models such as documents, graphs, and time series. Or you{' '}\n            <Link\n              href=\"https://github.com/RedisInsight/Packages\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              variant=\"inline\"\n            >\n              can build your own visualization\n            </Link>\n            .\n            {isString(firstIndex) && (\n              <>\n                <Spacer size=\"s\" />\n                {firstIndex ? (\n                  <>\n                    Run this command to see information and statistics on your\n                    index:\n                    <Spacer size=\"xs\" />\n                    <CodeBlock\n                      isCopyable\n                      className={styles.pre}\n                      data-testid=\"wb-onboarding-command\"\n                    >\n                      FT.INFO {firstIndex}\n                    </CodeBlock>\n                  </>\n                ) : (\n                  <>\n                    Run this command to see information and statistics about\n                    client connections:\n                    <Spacer size=\"xs\" />\n                    <CodeBlock\n                      isCopyable\n                      className={styles.pre}\n                      data-testid=\"wb-onboarding-command\"\n                    >\n                      CLIENT LIST\n                    </CodeBlock>\n                  </>\n                )}\n              </>\n            )}\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          if (vectorSearchFeature?.flag) {\n            history.push(Pages.vectorSearch(connectedInstanceId))\n          } else {\n            history.push(Pages.browser(connectedInstanceId))\n            dispatch(setOnboardPrevStep())\n            dispatch(showMonitor())\n          }\n\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n          dispatch(changeSidePanel(SidePanels.Insights))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  EXPLORE_REDIS: {\n    step: OnboardingSteps.Tutorials,\n    title: 'Explore and learn more',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.ExploreTutorials,\n      ]\n\n      const history = useHistory()\n      const dispatch = useDispatch()\n\n      return {\n        content:\n          'Learn more about how Redis can solve your use cases using interactive Tutorials.',\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          history.push(Pages.workbench(connectedInstanceId))\n          dispatch(changeSidePanel(null))\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          dispatch(resetExplorePanelSearch())\n          dispatch(setExplorePanelIsPageOpen(false))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  EXPLORE_CUSTOM_TUTORIALS: {\n    step: OnboardingSteps.CustomTutorials,\n    title: 'Upload your tutorials',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const history = useHistory()\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.ExploreCustomTutorials,\n      ]\n\n      return {\n        content: (\n          <>\n            Share your Redis expertise with your team and the wider community by\n            building custom Redis Insight tutorials.\n            <Spacer size=\"xs\" />\n            Use our{' '}\n            <Link\n              href={EXTERNAL_LINKS.guidesRepo}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              variant=\"inline\"\n            >\n              instructions\n            </Link>{' '}\n            to describe your implementations of Redis for other users to follow\n            and interact with in the context of a connected Redis database\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => sendBackTelemetryEvent(...telemetryArgs),\n        onNext: () => {\n          dispatch(changeSidePanel(null))\n          history.push(Pages.clusterDetails(connectedInstanceId))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  ANALYTICS_OVERVIEW: {\n    step: OnboardingSteps.AnalyticsOverview,\n    title: 'Cluster Overview',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const history = useHistory()\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.ClusterOverview,\n      ]\n\n      return {\n        content: (\n          <>\n            Investigate memory and key allocation in your cluster database and\n            monitor database information per primary nodes.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n          dispatch(changeSidePanel(SidePanels.Insights))\n          history.push(Pages.workbench(connectedInstanceId))\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          history.push(Pages.databaseAnalysis(connectedInstanceId))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  ANALYTICS_DATABASE_ANALYSIS: {\n    step: OnboardingSteps.AnalyticsDatabaseAnalysis,\n    title: 'Database Analysis',\n    Inner: () => {\n      const { data } = useSelector(dbAnalysisSelector)\n      const { id: connectedInstanceId = '', connectionType } = useSelector(\n        connectedInstanceSelector,\n      )\n      const dispatch = useDispatch()\n      const history = useHistory()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.DatabaseAnalysisOverview,\n      ]\n\n      return {\n        content: (\n          <>\n            Use Database Analysis to get summary of your database and receive\n            tips to improve memory usage and performance.\n            <Spacer size=\"xs\" />\n            Run a new report to get an overview of the database and receive tips\n            to optimize your database usage.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          if (connectionType !== ConnectionType.Cluster) {\n            dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n            dispatch(changeSidePanel(SidePanels.Insights))\n            dispatch(setOnboardPrevStep())\n            history.push(Pages.workbench(connectedInstanceId))\n          }\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          if (!data?.recommendations?.length) {\n            dispatch(setOnboardNextStep())\n            history.push(Pages.slowLog(connectedInstanceId))\n          } else {\n            dispatch(\n              setDatabaseAnalysisViewTab(\n                DatabaseAnalysisViewTab.Recommendations,\n              ),\n            )\n          }\n\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  ANALYTICS_RECOMMENDATIONS: {\n    step: OnboardingSteps.AnalyticsRecommendations,\n    title: 'Database Tips',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const history = useHistory()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.DatabaseAnalysisRecommendations,\n      ]\n\n      return {\n        content:\n          'See tips to optimize the memory usage, performance and increase the security of your Redis database',\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => sendBackTelemetryEvent(...telemetryArgs),\n        onNext: () => {\n          history.push(Pages.slowLog(connectedInstanceId))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  ANALYTICS_SLOW_LOG: {\n    step: OnboardingSteps.AnalyticsSlowLog,\n    title: 'Slow Log',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const { data } = useSelector(dbAnalysisSelector)\n      const history = useHistory()\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.SlowLog,\n      ]\n\n      return {\n        content: (\n          <>\n            Check Slow Log to troubleshoot performance issues.\n            <Spacer size=\"xs\" />\n            See the list of slow logs in chronological order to debug and trace\n            your Redis database. Customize parameters to capture logs.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          history.push(Pages.databaseAnalysis(connectedInstanceId))\n\n          if (!data?.recommendations?.length) {\n            dispatch(setOnboardPrevStep())\n          } else {\n            dispatch(\n              setDatabaseAnalysisViewTab(\n                DatabaseAnalysisViewTab.Recommendations,\n              ),\n            )\n          }\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          history.push(Pages.pubSub(connectedInstanceId))\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  PUB_SUB_PAGE: {\n    step: OnboardingSteps.PubSubPage,\n    title: 'Pub/Sub',\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const history = useHistory()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.PubSub,\n      ]\n\n      return {\n        content: (\n          <>\n            Use Redis pub/sub to subscribe to channels and post messages to\n            channels.\n            <Spacer size=\"xs\" />\n            Subscribe to receive messages from all channels or enter a message\n            to post to a specified channel.\n          </>\n        ),\n        onSkip: () => sendClosedTelemetryEvent(...telemetryArgs),\n        onBack: () => {\n          history.push(Pages.slowLog(connectedInstanceId))\n          sendBackTelemetryEvent(...telemetryArgs)\n        },\n        onNext: () => {\n          sendNextTelemetryEvent(...telemetryArgs)\n        },\n      }\n    },\n  },\n  FINISH: {\n    step: OnboardingSteps.Finish,\n    title: (\n      <>\n        Great job!\n        <OnboardingEmoji\n          style={{ marginLeft: 4, marginTop: -4, width: 16, height: 16 }}\n        />\n      </>\n    ),\n    Inner: () => {\n      const { id: connectedInstanceId = '' } = useSelector(\n        connectedInstanceSelector,\n      )\n      const history = useHistory()\n      const dispatch = useDispatch()\n      const telemetryArgs: TelemetryArgs = [\n        connectedInstanceId,\n        OnboardingStepName.Finish,\n      ]\n\n      useEffect(() => {\n        const closeLastStep = async () => {\n          dispatch(\n            incrementOnboardStepAction(OnboardingSteps.Finish, 0, async () => {\n              await sendEventTelemetry({\n                event: TelemetryEvent.ONBOARDING_TOUR_FINISHED,\n                eventData: {\n                  databaseId: connectedInstanceId,\n                },\n              })\n            }),\n          )\n        }\n\n        window.addEventListener('beforeunload', closeLastStep)\n        return () => {\n          window.removeEventListener('beforeunload', closeLastStep)\n        }\n      }, [connectedInstanceId])\n\n      return {\n        content: (\n          <>\n            You are done!\n            <Spacer size=\"xs\" />\n            Take me back to Browser.\n          </>\n        ),\n        onSkip: () => {\n          sendClosedTelemetryEvent(...telemetryArgs)\n          sendEventTelemetry({\n            event: TelemetryEvent.ONBOARDING_TOUR_FINISHED,\n            eventData: {\n              databaseId: connectedInstanceId,\n            },\n          })\n        },\n        onBack: () => sendBackTelemetryEvent(...telemetryArgs),\n        onNext: () => {\n          history.push(Pages.browser(connectedInstanceId))\n          sendNextTelemetryEvent(...telemetryArgs)\n          sendEventTelemetry({\n            event: TelemetryEvent.ONBOARDING_TOUR_FINISHED,\n            eventData: {\n              databaseId: connectedInstanceId,\n            },\n          })\n        },\n      }\n    },\n  },\n}\n\nexport { ONBOARDING_FEATURES }\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-features/index.ts",
    "content": "export * from './OnboardingFeatures'\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-features/styles.module.scss",
    "content": ".pre {\n  background-color: var(--commandGroupBadgeColor) !important;\n  word-wrap: break-word;\n\n  max-height: 240px;\n  overflow-y: auto;\n  @include eui.scrollBar;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/OnboardingTour.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  setOnboardNextStep,\n  setOnboardPrevStep,\n  skipOnboarding,\n} from 'uiSrc/slices/app/features'\nimport OnboardingTour from './OnboardingTour'\n\nconst mockedOptions = {\n  step: 2,\n  title: 'Title',\n  Inner: () => ({\n    content: 'Content',\n  }),\n}\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OnboardingTour', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <OnboardingTour\n          options={mockedOptions}\n          currentStep={2}\n          totalSteps={3}\n          isActive\n        >\n          <span />\n        </OnboardingTour>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render title, content, skip, next, back buttons', () => {\n    render(\n      <OnboardingTour\n        options={mockedOptions}\n        currentStep={2}\n        totalSteps={3}\n        isActive\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    expect(screen.getByTestId('step-title')).toHaveTextContent('Title')\n    expect(screen.getByTestId('step-content')).toHaveTextContent('Content')\n    expect(screen.getByTestId('skip-tour-btn')).toBeInTheDocument()\n    expect(screen.getByTestId('back-btn')).toBeInTheDocument()\n    expect(screen.getByTestId('next-btn')).toBeInTheDocument()\n  })\n\n  it('should not render back button for first step', () => {\n    render(\n      <OnboardingTour\n        options={{\n          ...mockedOptions,\n          step: 1,\n        }}\n        currentStep={1}\n        totalSteps={3}\n        isActive\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    expect(screen.queryByTestId('back-btn')).not.toBeInTheDocument()\n  })\n\n  it('should call proper actions on back button', () => {\n    const onBack = jest.fn()\n\n    render(\n      <OnboardingTour\n        options={{\n          ...mockedOptions,\n          Inner: () => ({\n            content: '',\n            onBack,\n          }),\n        }}\n        currentStep={2}\n        totalSteps={3}\n        isActive\n        preventPropagation\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    fireEvent.click(screen.getByTestId('back-btn'))\n    expect(store.getActions()).toEqual([setOnboardPrevStep()])\n    expect(onBack).toHaveBeenCalled()\n  })\n\n  it('should call proper actions on next button', () => {\n    const onNext = jest.fn()\n\n    render(\n      <OnboardingTour\n        options={{\n          ...mockedOptions,\n          Inner: () => ({\n            content: '',\n            onNext,\n          }),\n        }}\n        currentStep={2}\n        totalSteps={3}\n        isActive\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    fireEvent.click(screen.getByTestId('next-btn'))\n    expect(store.getActions()).toEqual([setOnboardNextStep()])\n    expect(onNext).toHaveBeenCalled()\n  })\n\n  it('should call proper actions on skip button', () => {\n    const onSkip = jest.fn()\n\n    render(\n      <OnboardingTour\n        options={{\n          ...mockedOptions,\n          Inner: () => ({\n            content: '',\n            onSkip,\n          }),\n        }}\n        currentStep={2}\n        totalSteps={3}\n        isActive\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    fireEvent.click(screen.getByTestId('skip-tour-btn'))\n    expect(store.getActions()).toEqual([skipOnboarding()])\n    expect(onSkip).toHaveBeenCalled()\n  })\n\n  it('should not show onboarding if step !== currentStep', () => {\n    render(\n      <OnboardingTour\n        options={mockedOptions}\n        currentStep={1}\n        totalSteps={3}\n        isActive\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    expect(screen.queryByTestId('step-title')).not.toBeInTheDocument()\n  })\n\n  it('should not show onboarding if isActive = false', () => {\n    render(\n      <OnboardingTour\n        options={mockedOptions}\n        currentStep={2}\n        totalSteps={3}\n        isActive={false}\n      >\n        <span />\n      </OnboardingTour>,\n    )\n\n    expect(screen.queryByTestId('step-title')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/OnboardingTour.tsx",
    "content": "import React, { useEffect, useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\n\nimport {\n  appFeatureFlagsFeaturesSelector,\n  skipOnboarding,\n  setOnboardNextStep,\n  setOnboardPrevStep,\n} from 'uiSrc/slices/app/features'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport {\n  EmptyButton,\n  IconButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { TourStep } from 'uiSrc/components/base/display/tour/TourStep'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Props as OnboardingWrapperProps } from './OnboardingTourWrapper'\n\nimport styles from './styles.module.scss'\n\nexport interface Props extends OnboardingWrapperProps {\n  isActive: boolean\n  currentStep: number\n  totalSteps: number\n}\n\nconst OnboardingTour = (props: Props) => {\n  const {\n    options,\n    children,\n    anchorPosition = 'rightUp',\n    panelClassName,\n    anchorWrapperClassName,\n    isActive,\n    currentStep,\n    totalSteps,\n    preventPropagation,\n    fullSize,\n  } = props\n  const { step, title, Inner } = options\n  const {\n    content = '',\n    onBack = () => {},\n    onNext = () => {},\n    onSkip = () => {},\n  } = Inner ? Inner() : {}\n\n  const { [FeatureFlags.vectorSearchV2]: vectorSearchFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const [isOpen, setIsOpen] = useState(step === currentStep && isActive)\n  const isLastStep = currentStep === totalSteps\n\n  const { displayStep, displayTotalSteps } = useMemo(() => {\n    const skippedSteps = vectorSearchFeature?.flag ? 0 : 1\n    return {\n      displayStep:\n        currentStep > OnboardingSteps.VectorSearchPage\n          ? currentStep - skippedSteps\n          : currentStep,\n      displayTotalSteps: totalSteps - skippedSteps,\n    }\n  }, [currentStep, totalSteps, vectorSearchFeature?.flag])\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsOpen(step === currentStep && isActive)\n  }, [currentStep, isActive])\n\n  const handleClickBack = () => {\n    onBack?.()\n    dispatch(setOnboardPrevStep())\n  }\n\n  const handleClickNext = () => {\n    onNext?.()\n    dispatch(setOnboardNextStep())\n  }\n\n  const handleSkip = () => {\n    onSkip?.()\n    dispatch(skipOnboarding())\n  }\n\n  const handleWrapperClick = (e: React.MouseEvent) => {\n    if (preventPropagation) {\n      e.stopPropagation()\n    }\n  }\n\n  const Header = (\n    <Col className={styles.header}>\n      {!isLastStep ? (\n        <EmptyButton\n          onClick={handleSkip}\n          className={styles.skipTourBtn}\n          size=\"small\"\n          data-testid=\"skip-tour-btn\"\n        >\n          Skip tour\n        </EmptyButton>\n      ) : (\n        <IconButton\n          icon={CancelSlimIcon}\n          className={styles.skipTourBtn}\n          onClick={handleSkip}\n          size=\"S\"\n          aria-label=\"close-tour\"\n          data-testid=\"close-tour-btn\"\n        />\n      )}\n      <Title size=\"XS\" data-testid=\"step-title\">\n        {title}\n      </Title>\n    </Col>\n  )\n\n  const StepContent = (\n    <Col>\n      <div className={styles.content} data-testid=\"step-content\">\n        {content}\n      </div>\n      <Spacer />\n      <Row align=\"center\" justify=\"between\">\n        <ColorText data-testid=\"step-progress\">\n          {displayStep} of {displayTotalSteps}\n        </ColorText>\n        <Row grow={false} gap=\"m\">\n          {currentStep > 1 && (\n            <SecondaryButton\n              onClick={handleClickBack}\n              size=\"s\"\n              data-testid=\"back-btn\"\n            >\n              Back\n            </SecondaryButton>\n          )}\n          <PrimaryButton\n            onClick={handleClickNext}\n            size=\"s\"\n            data-testid=\"next-btn\"\n          >\n            {!isLastStep ? 'Next' : 'Take me back'}\n          </PrimaryButton>\n        </Row>\n      </Row>\n    </Col>\n  )\n\n  return (\n    <div\n      onClick={handleWrapperClick}\n      className={cx(styles.wrapper, anchorWrapperClassName, {\n        [styles.fullSize]: fullSize,\n      })}\n      role=\"presentation\"\n    >\n      <TourStep\n        content={StepContent}\n        open={isOpen}\n        minWidth={300}\n        maxWidth={360}\n        title={Header}\n        placement={anchorPosition}\n        className={cx(styles.popoverPanel, panelClassName, {\n          [styles.lastStep]: isLastStep,\n        })}\n        offset={5}\n        data-testid=\"onboarding-tour\"\n      >\n        {children}\n      </TourStep>\n    </div>\n  )\n}\n\nexport default OnboardingTour\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/OnboardingTourWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport * as featureSlice from 'uiSrc/slices/app/features'\nimport OnboardingTourWrapper from './OnboardingTourWrapper'\n\nconst mockedOptions = {\n  step: 2,\n  title: 'Title',\n  Inner: () => ({\n    content: 'Content',\n  }),\n}\n\njest.spyOn(featureSlice, 'appFeatureOnboardingSelector').mockReturnValue({\n  currentStep: 2,\n  isActive: true,\n  totalSteps: 10,\n})\n\ndescribe('OnboardingTourWrapper', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <OnboardingTourWrapper options={mockedOptions}>\n          <span />\n        </OnboardingTourWrapper>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render tour', () => {\n    render(\n      <OnboardingTourWrapper options={mockedOptions}>\n        <span />\n      </OnboardingTourWrapper>,\n    )\n\n    expect(screen.getByTestId('onboarding-tour')).toBeInTheDocument()\n  })\n\n  it('should not render tour with isActive = false', () => {\n    ;(featureSlice.appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n      currentStep: 2,\n      isActive: false,\n      totalSteps: 10,\n    })\n    render(\n      <OnboardingTourWrapper options={mockedOptions}>\n        <span data-testid=\"span\" />\n      </OnboardingTourWrapper>,\n    )\n\n    expect(screen.queryByTestId('onboarding-tour')).not.toBeInTheDocument()\n    expect(screen.getByTestId('span')).toBeInTheDocument()\n  })\n\n  it('should not render tour with isActive = true & different step', () => {\n    ;(featureSlice.appFeatureOnboardingSelector as jest.Mock).mockReturnValue({\n      currentStep: 3,\n      isActive: true,\n      totalSteps: 10,\n    })\n    render(\n      <OnboardingTourWrapper options={mockedOptions}>\n        <span data-testid=\"span\" />\n      </OnboardingTourWrapper>,\n    )\n\n    expect(screen.queryByTestId('onboarding-tour')).not.toBeInTheDocument()\n    expect(screen.getByTestId('span')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/OnboardingTourWrapper.tsx",
    "content": "import { useSelector } from 'react-redux'\nimport React, { useEffect, useState } from 'react'\nimport { PopoverAnchorPosition } from '@elastic/eui/src/components/popover/popover'\nimport { appFeatureOnboardingSelector } from 'uiSrc/slices/app/features'\n\nimport OnboardingTour from './OnboardingTour'\nimport { OnboardingTourOptions } from './interfaces'\n\nexport interface Props {\n  options: OnboardingTourOptions\n  children: React.ReactElement\n  anchorPosition?: PopoverAnchorPosition\n  panelClassName?: string\n  anchorWrapperClassName?: string\n  preventPropagation?: boolean\n  fullSize?: boolean\n  delay?: number\n  rerenderWithDelay?: any\n}\n\nconst OnboardingTourWrapper = (props: Props) => {\n  const { options, children, delay, rerenderWithDelay } = props\n  const { step } = options\n  const { currentStep, isActive, totalSteps } = useSelector(\n    appFeatureOnboardingSelector,\n  )\n  const [isDelayed, setIsDelayed] = useState(true)\n\n  const isCurrentStep = step === currentStep && isActive\n\n  useEffect(() => {\n    if (!isCurrentStep) return setIsDelayed(true)\n    if (!delay) return setIsDelayed(false)\n\n    setIsDelayed(true)\n    const timeId = setTimeout(() => setIsDelayed(false), delay)\n\n    return () => clearTimeout(timeId)\n  }, [isCurrentStep, delay, rerenderWithDelay])\n\n  // render tour only when it needed due to side effect calls\n  return !isDelayed && isCurrentStep ? (\n    <OnboardingTour\n      currentStep={currentStep}\n      totalSteps={totalSteps}\n      isActive={isActive}\n      {...props}\n    >\n      {children}\n    </OnboardingTour>\n  ) : (\n    children\n  )\n}\n\nexport default OnboardingTourWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/index.ts",
    "content": "import OnboardingTourWrapper from './OnboardingTourWrapper'\n\nexport * from './interfaces'\nexport default OnboardingTourWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/interfaces.ts",
    "content": "import React from 'react'\n\nexport interface OnboardingTourInner {\n  content: string | React.ReactElement\n  onSkip?: () => void\n  onBack?: () => void\n  onNext?: () => void\n}\n\nexport interface OnboardingTourOptions {\n  step: number\n  title?: string | React.ReactElement\n  Inner?: () => OnboardingTourInner\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/onboarding-tour/styles.module.scss",
    "content": ".wrapper {\n  &.fullSize {\n    width: 100%;\n    height: 100%;\n  }\n}\n\n.popoverPanel {\n  &.lastStep > span {\n    display: none;\n  }\n\n  .header {\n    .skipTourBtn {\n      display: flex;\n      align-self: flex-end;\n\n      font-size: 11px;\n      line-height: 14px;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-header/PageHeader.module.scss",
    "content": ".pageHeader {\n  display: flex;\n  align-items: center;\n\n  min-height: 56px;\n}\n\n.pageHeaderTop {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n  padding: 8px 16px;\n  min-height: 70px;\n}\n\n\n\n.logo {\n  transition: transform 0.1s linear;\n  padding-left: 6px;\n  margin-top: 14px;\n\n  &:hover {\n    transform: translateY(-1px) !important;\n  }\n\n  svg {\n    height: 50px;\n    width: 90px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-header/PageHeader.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\n\nimport reactRouterDom from 'react-router-dom'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { resetDataRedisCluster } from 'uiSrc/slices/instances/cluster'\nimport { resetDataRedisCloud } from 'uiSrc/slices/instances/cloud'\nimport { resetDataSentinel } from 'uiSrc/slices/instances/sentinel'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport PageHeader from './PageHeader'\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('PageHeader', () => {\n  it('should render', () => {\n    expect(render(<PageHeader title=\"Page\" />)).toBeTruthy()\n  })\n\n  it('should render proper components', () => {\n    render(<PageHeader title=\"Page\" subtitle=\"subtitle\" />)\n\n    expect(screen.getByTestId('page-title')).toHaveTextContent('Page')\n    expect(screen.getByTestId('page-subtitle')).toHaveTextContent('subtitle')\n  })\n\n  it('should call proper actions after click logo', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<PageHeader title=\"Page\" subtitle=\"subtitle\" />)\n\n    fireEvent.click(screen.getByTestId('redis-logo-home'))\n\n    expect(store.getActions()).toEqual([\n      resetDataRedisCluster(),\n      resetDataRedisCloud(),\n      resetDataSentinel(),\n    ])\n\n    expect(pushMock).toBeCalledWith('/')\n    pushMock.mockRestore()\n  })\n\n  it('should render custom component', () => {\n    render(\n      <PageHeader title=\"Page\" showInsights>\n        <div data-testid=\"custom-logo\" />\n      </PageHeader>,\n    )\n\n    expect(screen.getByTestId('custom-logo')).toBeInTheDocument()\n    expect(screen.queryByTestId('redis-logo-home')).not.toBeInTheDocument()\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: true },\n    )\n\n    render(<PageHeader title=\"Page\" subtitle=\"subtitle\" showInsights />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('o-auth-user-profile')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: false },\n    )\n\n    render(<PageHeader title=\"Page\" subtitle=\"subtitle\" showInsights />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('o-auth-user-profile')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-header/PageHeader.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport cx from 'classnames'\nimport { Pages, FeatureFlags } from 'uiSrc/constants'\nimport { resetDataRedisCloud } from 'uiSrc/slices/instances/cloud'\nimport { resetDataRedisCluster } from 'uiSrc/slices/instances/cluster'\nimport { resetDataSentinel } from 'uiSrc/slices/instances/sentinel'\n\nimport { CopilotTrigger, InsightsTrigger } from 'uiSrc/components/triggers'\nimport { FeatureFlagComponent, OAuthUserProfile } from 'uiSrc/components'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { isAnyFeatureEnabled } from 'uiSrc/utils/features'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { RedisLogoFullIcon } from 'uiSrc/components/base/icons'\nimport styles from './PageHeader.module.scss'\nimport { ColorText } from 'uiSrc/components/base/text'\n\ninterface Props {\n  title?: string\n  subtitle?: string\n  children?: React.ReactNode\n  showInsights?: boolean\n  className?: string\n}\n\nconst PageHeader = (props: Props) => {\n  const { title, subtitle, showInsights, children, className } = props\n\n  const {\n    [FeatureFlags.databaseChat]: databaseChatFeature,\n    [FeatureFlags.documentationChat]: documentationChatFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n  const isAnyChatAvailable = isAnyFeatureEnabled([\n    databaseChatFeature,\n    documentationChatFeature,\n  ])\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const resetConnections = () => {\n    dispatch(resetDataRedisCluster())\n    dispatch(resetDataRedisCloud())\n    dispatch(resetDataSentinel())\n  }\n\n  const goHome = () => {\n    resetConnections()\n    history.push(Pages.home)\n  }\n\n  return (\n    <div className={cx(styles.pageHeader, className)}>\n      <div className={styles.pageHeaderTop}>\n        <div>\n          {title && (\n            <Title size=\"L\" data-testid=\"page-title\">\n              <ColorText variant=\"semiBold\" data-testid=\"page-header-title\">\n                {title}\n              </ColorText>\n            </Title>\n          )}\n          {subtitle ? <span data-testid=\"page-subtitle\">{subtitle}</span> : ''}\n        </div>\n        {children ? <>{children}</> : ''}\n        {showInsights ? (\n          <Row style={{ flexGrow: 0 }} align=\"center\">\n            {isAnyChatAvailable && (\n              <FlexItem style={{ marginRight: 12 }}>\n                <CopilotTrigger />\n              </FlexItem>\n            )}\n            <FlexItem grow>\n              <InsightsTrigger source=\"home page\" />\n            </FlexItem>\n            <FeatureFlagComponent\n              name={[FeatureFlags.cloudSso, FeatureFlags.cloudAds]}\n            >\n              <FlexItem\n                grow\n                style={{ marginLeft: 16 }}\n                data-testid=\"o-auth-user-profile\"\n              >\n                <OAuthUserProfile source={OAuthSocialSource.UserProfile} />\n              </FlexItem>\n            </FeatureFlagComponent>\n          </Row>\n        ) : (\n          <div className={styles.pageHeaderLogo}>\n            <EmptyButton\n              aria-label=\"redisinsight\"\n              onClick={goHome}\n              onKeyDown={goHome}\n              className={styles.logo}\n              tabIndex={0}\n              icon={RedisLogoFullIcon}\n              data-testid=\"redis-logo-home\"\n            />\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nPageHeader.defaultProps = {\n  subtitle: null,\n  children: null,\n}\n\nexport default PageHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-header/components/index.ts",
    "content": "import InsightsPanel from './insights-panel'\n\nexport { InsightsPanel }\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-placeholder/PagePlaceholder.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport PagePlaceholder from './PagePlaceholder'\n\ndescribe('PagePlaceholder', () => {\n  it('should render', () => {\n    expect(render(<PagePlaceholder />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-placeholder/PagePlaceholder.tsx",
    "content": "import React from 'react'\n\nimport LogoIcon from 'uiSrc/assets/img/logo_small.svg'\nimport { getConfig } from 'uiSrc/config'\nimport { RiLoadingLogo } from 'uiSrc/components/base/display'\nimport { RiEmptyPrompt } from 'uiSrc/components/base/layout'\n\nconst riConfig = getConfig()\n\nconst PagePlaceholder = () => (\n  <>\n    {riConfig.app.env !== 'development' && (\n      <RiEmptyPrompt\n        data-testid=\"page-placeholder\"\n        icon={<RiLoadingLogo src={LogoIcon} $size=\"XXL\" />}\n      />\n    )}\n  </>\n)\n\nexport default PagePlaceholder\n"
  },
  {
    "path": "redisinsight/ui/src/components/page-placeholder/index.ts",
    "content": "import PagePlaceholder from './PagePlaceholder'\n\nexport default PagePlaceholder\n"
  },
  {
    "path": "redisinsight/ui/src/components/panel/index.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Panel = styled(Row)`\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/promo-link/PromoLink.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, screen, render } from 'uiSrc/utils/test-utils'\nimport PromoLink, { Props } from './PromoLink'\n\nconst mockedProps = mock<Props>()\nconst testId = 'promoLink'\n\ndescribe('PromoLink', () => {\n  it('should render', () => {\n    expect(render(<PromoLink {...instance(mockedProps)} />)).toBeTruthy()\n  })\n  it('should correctly render content', () => {\n    const title = 'Limited offer'\n    const description = 'Try Redis Cloud'\n\n    const { container } = render(\n      <PromoLink\n        {...instance(mockedProps)}\n        title={title}\n        description={description}\n        testId={testId}\n      />,\n    )\n\n    expect(container).toHaveTextContent(title)\n    expect(container).toHaveTextContent(description)\n  })\n  it('should call \"handleClick\"', () => {\n    const handleClick = jest.fn()\n\n    const renderer = render(\n      <PromoLink\n        {...instance(mockedProps)}\n        onClick={handleClick}\n        testId={testId}\n      />,\n    )\n\n    expect(renderer).toBeTruthy()\n\n    fireEvent.click(screen.getByTestId(testId))\n    expect(handleClick).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/promo-link/PromoLink.tsx",
    "content": "import React from 'react'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  title: string\n  url: string\n  description?: string\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n  testId?: string\n  styles?: any\n}\n\nconst PromoLink = (props: Props) => {\n  const { title, description, url, onClick, testId, styles: linkStyles } = props\n\n  return (\n    <a\n      className={styles.link}\n      href={url}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      onClick={onClick}\n      data-testid={testId}\n      style={{ ...linkStyles }}\n    >\n      <RiIcon type=\"CloudIcon\" size=\"m\" className={styles.cloudIcon} />\n      <ColorText color={linkStyles?.color} className={styles.title}>\n        {title}\n      </ColorText>\n      <ColorText color={linkStyles?.color} className={styles.description}>\n        {description}\n      </ColorText>\n    </a>\n  )\n}\n\nexport default PromoLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/promo-link/styles.module.scss",
    "content": ".icon {\n  display: block !important;\n  position: absolute;\n  right: 14px;\n  top: calc(50% - 11px);\n  color: var(--euiTextColor);\n  width: 20px;\n  height: 20px;\n}\n\n.link {\n  border: 1px solid var(--euiColorSecondary);\n  padding: 6px 16px;\n  letter-spacing: normal;\n  border-radius: 4px;\n  text-align: left;\n  position: relative;\n  width: 100%;\n  height: 42px;\n  background-size: cover;\n  background-position: center;\n\n  &::before {\n    content: \"\";\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n  }\n\n  &:hover {\n    transform: translateY(-1px);\n  }\n\n  .title {\n    font-size: 12px !important;\n    position: relative;\n    font-weight: 500;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    line-height: 1;\n    padding-left: 30px;\n  }\n\n  .description {\n    font-size: 10px !important;\n    padding-left: 30px;\n    position: relative;\n    padding-top: 4px;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    line-height: 1;\n    color: var(--euiTextSubduedColor) !important;\n  }\n\n  .icon {\n    position: absolute;\n    right: 14px;\n    top: calc(50% - 11px);\n    color: var(--euiTextColor);\n    width: 20px;\n    height: 20px;\n  }\n\n  .cloudIcon {\n    position: absolute;\n    width: 28px;\n    height: 20px;\n    top: 12px;\n    left: 16px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/pub-sub-config/PubSubConfig.spec.tsx",
    "content": "/* eslint-disable sonarjs/no-duplicate-string */\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\nimport MockedSocket from 'socket.io-mock'\nimport socketIO from 'socket.io-client'\nimport { PubSubEvent, SubscriptionType } from 'uiSrc/constants/pubSub'\nimport {\n  disconnectPubSub,\n  pubSubSelector,\n  setLoading,\n  setPubSubConnected,\n} from 'uiSrc/slices/pubsub/pubsub'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { SocketEvent } from 'uiSrc/constants'\nimport * as ioHooks from 'uiSrc/services/hooks/useIoConnection'\nimport { getSocketApiUrl } from 'uiSrc/utils'\nimport PubSubConfig from './PubSubConfig'\n\nlet store: typeof mockedStore\nlet socket: typeof MockedSocket\nlet useIoConnectionSpy: jest.SpyInstance\n\nbeforeEach(() => {\n  cleanup()\n  socket = new MockedSocket()\n  socketIO.mockReturnValue(socket)\n  useIoConnectionSpy = jest.spyOn(ioHooks, 'useIoConnection')\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('socket.io-client')\n\njest.mock('uiSrc/slices/pubsub/pubsub', () => ({\n  ...jest.requireActual('uiSrc/slices/pubsub/pubsub'),\n  pubSubSelector: jest.fn().mockReturnValue({\n    isConnected: false,\n    isSubscribed: false,\n    isSubscribeTriggered: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '1',\n  }),\n}))\n\nconst subscriptionsMock = [{ channel: 'p*', type: SubscriptionType.PSubscribe }]\n\ndescribe('PubSubConfig', () => {\n  it('should render', () => {\n    expect(render(<PubSubConfig />)).toBeTruthy()\n  })\n\n  it('should connect socket', () => {\n    const pubSubSelectorMock = jest.fn().mockReturnValue({\n      isSubscribeTriggered: true,\n    })\n    pubSubSelector.mockImplementation(pubSubSelectorMock)\n\n    render(<PubSubConfig />)\n\n    socket.socketClient.emit(SocketEvent.Connect)\n\n    const afterRenderActions = [setPubSubConnected(true), setLoading(true)]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n    expect(useIoConnectionSpy).toHaveBeenCalledWith(\n      getSocketApiUrl('pub-sub'),\n      { query: { instanceId: '1' }, token: '' },\n    )\n  })\n\n  it('should emit subscribe on channel', () => {\n    const pubSubSelectorMock = jest.fn().mockReturnValue({\n      isSubscribeTriggered: true,\n      subscriptions: subscriptionsMock,\n    })\n    pubSubSelector.mockImplementation(pubSubSelectorMock)\n\n    render(<PubSubConfig />)\n\n    socket.on(PubSubEvent.Subscribe, (data: any) => {\n      expect(data).toEqual(subscriptionsMock)\n    })\n\n    socket.socketClient.emit(SocketEvent.Connect)\n    socket.socketClient.emit(PubSubEvent.Subscribe, subscriptionsMock)\n  })\n\n  it('should catch disconnect', () => {\n    const pubSubSelectorMock = jest.fn().mockReturnValue({\n      isSubscribeTriggered: true,\n      isConnected: true,\n      isSubscribed: true,\n    })\n    pubSubSelector.mockImplementation(pubSubSelectorMock)\n\n    const { unmount } = render(<PubSubConfig retryDelay={0} />)\n\n    socket.socketClient.emit(SocketEvent.Connect)\n    socket.socketClient.emit(SocketEvent.Disconnect)\n\n    const afterRenderActions = [\n      setPubSubConnected(true),\n      setLoading(true),\n      disconnectPubSub(),\n    ]\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    unmount()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/pub-sub-config/PubSubConfig.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Socket } from 'socket.io-client'\n\nimport { SocketEvent } from 'uiSrc/constants'\nimport { PubSubEvent } from 'uiSrc/constants/pubSub'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { PubSubSubscription } from 'uiSrc/slices/interfaces/pubsub'\nimport {\n  concatPubSubMessages,\n  disconnectPubSub,\n  pubSubSelector,\n  setIsPubSubSubscribed,\n  setIsPubSubUnSubscribed,\n  setLoading,\n  setPubSubConnected,\n} from 'uiSrc/slices/pubsub/pubsub'\nimport { getSocketApiUrl, Nullable } from 'uiSrc/utils'\nimport { appCsrfSelector } from 'uiSrc/slices/app/csrf'\nimport { useIoConnection } from 'uiSrc/services/hooks/useIoConnection'\n\ninterface IProps {\n  retryDelay?: number\n}\n\nconst PubSubConfig = ({ retryDelay = 5000 }: IProps) => {\n  const { id: instanceId = '' } = useSelector(connectedInstanceSelector)\n  const { isSubscribeTriggered, isConnected, subscriptions } =\n    useSelector(pubSubSelector)\n  const { token } = useSelector(appCsrfSelector)\n  const socketRef = useRef<Nullable<Socket>>(null)\n  const connectIo = useIoConnection(getSocketApiUrl('pub-sub'), {\n    token,\n    query: { instanceId },\n  })\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!isSubscribeTriggered || !instanceId || socketRef.current?.connected) {\n      return\n    }\n    let retryTimer: NodeJS.Timer\n\n    socketRef.current = connectIo()\n\n    socketRef.current.on(SocketEvent.Connect, () => {\n      clearTimeout(retryTimer)\n      dispatch(setPubSubConnected(true))\n      subscribeForChannels()\n    })\n\n    // Catch connect error\n    socketRef.current?.on(SocketEvent.ConnectionError, () => {})\n\n    // Catch exceptions\n    socketRef.current?.on(PubSubEvent.Exception, () => {\n      handleDisconnect()\n    })\n\n    // Catch disconnect\n    socketRef.current?.on(SocketEvent.Disconnect, () => {\n      if (retryDelay) {\n        retryTimer = setTimeout(handleDisconnect, retryDelay)\n      } else {\n        handleDisconnect()\n      }\n    })\n  }, [instanceId, isSubscribeTriggered])\n\n  useEffect(() => {\n    if (!socketRef.current?.connected) {\n      return\n    }\n\n    if (!isSubscribeTriggered) {\n      unSubscribeFromChannels()\n      return\n    }\n\n    subscribeForChannels()\n  }, [isSubscribeTriggered])\n\n  const subscribeForChannels = () => {\n    dispatch(setLoading(true))\n    socketRef.current?.emit(\n      PubSubEvent.Subscribe,\n      { subscriptions },\n      onChannelsSubscribe,\n    )\n  }\n\n  const unSubscribeFromChannels = () => {\n    dispatch(setLoading(true))\n    socketRef.current?.emit(\n      PubSubEvent.Unsubscribe,\n      { subscriptions },\n      onChannelsUnSubscribe,\n    )\n  }\n\n  const onChannelsSubscribe = () => {\n    dispatch(setLoading(false))\n    dispatch(setIsPubSubSubscribed())\n    subscriptions.forEach(({ channel, type }: PubSubSubscription) => {\n      const subscription = `${type}:${channel}`\n      const isListenerExist =\n        !!socketRef.current?.listeners(subscription).length\n\n      if (!isListenerExist) {\n        socketRef.current?.on(subscription, (data) => {\n          dispatch(concatPubSubMessages(data))\n        })\n      }\n    })\n  }\n\n  const onChannelsUnSubscribe = () => {\n    dispatch(setIsPubSubUnSubscribed())\n    dispatch(setLoading(false))\n\n    subscriptions.forEach(({ channel, type }: PubSubSubscription) => {\n      socketRef.current?.removeListener(`${type}:${channel}`)\n    })\n  }\n\n  useEffect(() => {\n    if (!isConnected && socketRef.current?.connected) {\n      handleDisconnect()\n    }\n  }, [isConnected])\n\n  const handleDisconnect = () => {\n    dispatch(disconnectPubSub())\n    socketRef.current?.removeAllListeners()\n    socketRef.current?.disconnect()\n  }\n\n  return null\n}\n\nexport default PubSubConfig\n"
  },
  {
    "path": "redisinsight/ui/src/components/pub-sub-config/index.ts",
    "content": "import PubSubConfig from './PubSubConfig'\n\nexport default PubSubConfig\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/components/RunButton.tsx",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { PlayFilledIcon } from 'uiSrc/components/base/icons'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\n\nconst StyledEmptyButton = styled(EmptyButton)`\n  &:focus,\n  &:active {\n    outline: 0;\n  }\n\n  svg {\n    margin-top: 1px;\n    width: 14px;\n    height: 14px;\n    color: var(--rsSubmitBtn);\n  }\n`\n\nexport const RunButton = ({\n  isLoading,\n  onSubmit,\n}: {\n  isLoading?: boolean\n  onSubmit: () => void\n}) => {\n  return (\n    <StyledEmptyButton\n      onClick={() => {\n        onSubmit()\n      }}\n      loading={isLoading}\n      disabled={isLoading}\n      icon={PlayFilledIcon}\n      aria-label=\"submit\"\n      data-testid=\"btn-submit\"\n    >\n      Run\n    </StyledEmptyButton>\n  )\n}\n\nexport default RunButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/query-editor-context.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from '@testing-library/react'\n\nimport {\n  QueryEditorContextProvider,\n  useQueryEditorContext,\n} from './query-editor.context'\n\nconst mockContextValue = {\n  query: 'FT.SEARCH idx \"*\"',\n  setQuery: jest.fn(),\n  isLoading: false,\n  commands: [],\n  indexes: [],\n  onSubmit: jest.fn(),\n}\n\nconst TestComponent: React.FC = () => {\n  const { query, isLoading } = useQueryEditorContext()\n\n  return (\n    <div>\n      <p data-testid=\"query\">{query}</p>\n      <p data-testid=\"is-loading\">{String(isLoading)}</p>\n    </div>\n  )\n}\n\ndescribe('QueryEditorContext', () => {\n  it('provides default values', () => {\n    render(\n      <QueryEditorContextProvider value={mockContextValue}>\n        <TestComponent />\n      </QueryEditorContextProvider>,\n    )\n\n    expect(screen.getByTestId('query')).toHaveTextContent('FT.SEARCH idx \"*\"')\n    expect(screen.getByTestId('is-loading')).toHaveTextContent('false')\n  })\n\n  it('provides custom values', () => {\n    render(\n      <QueryEditorContextProvider\n        value={{ ...mockContextValue, query: 'PING', isLoading: true }}\n      >\n        <TestComponent />\n      </QueryEditorContextProvider>,\n    )\n\n    expect(screen.getByTestId('query')).toHaveTextContent('PING')\n    expect(screen.getByTestId('is-loading')).toHaveTextContent('true')\n  })\n\n  it('provides monacoObjects ref', () => {\n    const RefTestComponent: React.FC = () => {\n      const { monacoObjects } = useQueryEditorContext()\n      return <p data-testid=\"has-ref\">{String(monacoObjects !== undefined)}</p>\n    }\n\n    render(\n      <QueryEditorContextProvider value={mockContextValue}>\n        <RefTestComponent />\n      </QueryEditorContextProvider>,\n    )\n\n    expect(screen.getByTestId('has-ref')).toHaveTextContent('true')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/query-editor.context.tsx",
    "content": "import React, { createContext, useContext, useRef } from 'react'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nimport {\n  QueryEditorContextValue,\n  QueryEditorContextProviderProps,\n} from './query-editor.context.types'\n\nconst defaultContextValue: QueryEditorContextValue = {\n  monacoObjects: { current: null },\n  query: '',\n  setQuery: () => {},\n  isLoading: false,\n  commands: [],\n  indexes: [],\n  onSubmit: () => {},\n}\n\nconst QueryEditorContext =\n  createContext<QueryEditorContextValue>(defaultContextValue)\n\nexport const QueryEditorContextProvider = ({\n  children,\n  value,\n}: QueryEditorContextProviderProps) => {\n  const monacoObjects = useRef<Nullable<IEditorMount>>(null)\n\n  return (\n    <QueryEditorContext.Provider value={{ ...value, monacoObjects }}>\n      {children}\n    </QueryEditorContext.Provider>\n  )\n}\n\nexport const useQueryEditorContext = (): QueryEditorContextValue =>\n  useContext(QueryEditorContext)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/query-editor.context.types.ts",
    "content": "import React, { ReactNode } from 'react'\n\nimport { IRedisCommand } from 'uiSrc/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nexport interface QueryEditorContextValue {\n  // Editor instance\n  monacoObjects: React.MutableRefObject<Nullable<IEditorMount>>\n\n  // State\n  query: string\n  setQuery: (value: string) => void\n  isLoading: boolean\n\n  // Data\n  commands: IRedisCommand[]\n  indexes: RedisResponseBuffer[]\n  activeIndexName?: string\n\n  // Callbacks\n  onSubmit: (value?: string) => void\n}\n\nexport interface QueryEditorContextProviderProps {\n  children: ReactNode\n  value: Omit<QueryEditorContextValue, 'monacoObjects'>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/query-results-context.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from '@testing-library/react'\n\nimport {\n  QueryResultsProvider,\n  useQueryResultsContext,\n  QueryResultsTelemetry,\n} from './query-results.context'\n\nconst TestComponent: React.FC = () => {\n  const { telemetry } = useQueryResultsContext()\n\n  return (\n    <div>\n      <button\n        data-testid=\"copy-btn\"\n        onClick={() =>\n          telemetry.onCommandCopied?.({\n            command: 'GET key',\n            databaseId: 'db-1',\n          })\n        }\n      />\n    </div>\n  )\n}\n\ndescribe('QueryResultsContext', () => {\n  it('does not throw when no telemetry is provided', () => {\n    render(\n      <QueryResultsProvider>\n        <TestComponent />\n      </QueryResultsProvider>,\n    )\n\n    expect(() => fireEvent.click(screen.getByTestId('copy-btn'))).not.toThrow()\n  })\n\n  it('invokes the telemetry callback when provided', () => {\n    const telemetry: QueryResultsTelemetry = {\n      onCommandCopied: jest.fn(),\n    }\n\n    render(\n      <QueryResultsProvider telemetry={telemetry}>\n        <TestComponent />\n      </QueryResultsProvider>,\n    )\n\n    fireEvent.click(screen.getByTestId('copy-btn'))\n\n    expect(telemetry.onCommandCopied).toHaveBeenCalledWith({\n      command: 'GET key',\n      databaseId: 'db-1',\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/query-results.context.tsx",
    "content": "import React, { createContext, ReactNode, useContext, useMemo } from 'react'\n\nexport interface QueryResultsTelemetry {\n  onCommandCopied?: (params: { command: string; databaseId: string }) => void\n  onResultCleared?: (params: { command: string; databaseId: string }) => void\n  onResultCollapsed?: (params: { command: string; databaseId: string }) => void\n  onResultExpanded?: (params: { command: string; databaseId: string }) => void\n  onResultViewChanged?: (params: Record<string, unknown>) => void\n  onFullScreenToggled?: (params: { state: string; databaseId: string }) => void\n  onQueryReRun?: (params: { command: string; databaseId: string }) => void\n}\n\nexport interface QueryResultsContextValue {\n  telemetry: QueryResultsTelemetry\n}\n\nconst emptyTelemetry: QueryResultsTelemetry = {}\n\nconst defaultContextValue: QueryResultsContextValue = {\n  telemetry: emptyTelemetry,\n}\n\nconst QueryResultsContext =\n  createContext<QueryResultsContextValue>(defaultContextValue)\n\ninterface QueryResultsProviderProps {\n  children: ReactNode\n  telemetry?: QueryResultsTelemetry\n}\n\nexport const QueryResultsProvider: React.FC<QueryResultsProviderProps> = ({\n  children,\n  telemetry = emptyTelemetry,\n}) => {\n  const value = useMemo(() => ({ telemetry }), [telemetry])\n\n  return (\n    <QueryResultsContext.Provider value={value}>\n      {children}\n    </QueryResultsContext.Provider>\n  )\n}\n\nexport const useQueryResultsContext = (): QueryResultsContextValue =>\n  useContext(QueryResultsContext)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/view-mode-context.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from '@testing-library/react'\n\nimport {\n  useViewModeContext,\n  ViewMode,\n  ViewModeContextProvider,\n} from './view-mode.context'\n\n// Test component to consume the context\nconst TestComponent: React.FC = () => {\n  const { viewMode } = useViewModeContext()\n\n  return (\n    <div>\n      <p data-testid=\"view-mode\">Current View Mode: {viewMode}</p>\n    </div>\n  )\n}\n\ndescribe('ViewModeContext', () => {\n  it('provides the default view mode', () => {\n    render(\n      <ViewModeContextProvider>\n        <TestComponent />\n      </ViewModeContextProvider>,\n    )\n\n    expect(screen.getByTestId('view-mode')).toHaveTextContent(\n      `Current View Mode: ${ViewMode.Workbench}`,\n    )\n  })\n\n  it('uses the initial view mode if provided', () => {\n    render(\n      <ViewModeContextProvider viewMode={ViewMode.VectorSearch}>\n        <TestComponent />\n      </ViewModeContextProvider>,\n    )\n\n    expect(screen.getByTestId('view-mode')).toHaveTextContent(\n      `Current View Mode: ${ViewMode.VectorSearch}`,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/context/view-mode.context.tsx",
    "content": "import React, { createContext, ReactNode, useContext } from 'react'\n\nexport enum ViewMode {\n  Workbench = 'workbench',\n  VectorSearch = 'vector-search',\n}\n\ninterface ViewModeContextType {\n  viewMode: ViewMode\n}\n\nconst ViewModeContext = createContext<ViewModeContextType>({\n  viewMode: ViewMode.Workbench,\n})\n\n// Props for the provider\ninterface ViewModeContextProviderProps {\n  children: ReactNode\n  viewMode?: ViewMode\n}\n\nexport const ViewModeContextProvider: React.FC<\n  ViewModeContextProviderProps\n> = ({ children, viewMode = ViewMode.Workbench }) => {\n  return (\n    <ViewModeContext.Provider value={{ viewMode }}>\n      {children}\n    </ViewModeContext.Provider>\n  )\n}\n\nexport const useViewModeContext = (): ViewModeContextType =>\n  useContext(ViewModeContext)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/index.ts",
    "content": "export { useMonacoRedisEditor } from './useMonacoRedisEditor'\nexport { useRedisCompletions } from './useRedisCompletions'\nexport { useQueryDecorations } from './useQueryDecorations'\nexport { useCommandHistory } from './useCommandHistory'\nexport { useDslSyntax } from './useDslSyntax'\nexport { useQueryEditor } from './useQueryEditor'\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useCommandHistory.ts",
    "content": "import { useEffect, useRef } from 'react'\n\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\n\nimport { UseCommandHistoryProps } from './useCommandHistory.types'\n\n/**\n * Provides command history navigation via Up arrow key\n * when cursor is at position (1,1) and suggestions are not open.\n */\nexport const useCommandHistory = ({\n  monacoObjects,\n  historyItems,\n}: UseCommandHistoryProps) => {\n  const execHistoryRef = useRef<CommandExecutionUI[]>([])\n  const execHistoryPosRef = useRef<number>(0)\n\n  // Sync history items from external state\n  useEffect(() => {\n    execHistoryRef.current = historyItems\n    execHistoryPosRef.current = 0\n  }, [historyItems])\n\n  const onQuickHistoryAccess = () => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects.current\n\n    const position = editor.getPosition()\n    if (\n      position?.column !== 1 ||\n      position?.lineNumber !== 1 ||\n      // @ts-ignore\n      editor.getContribution('editor.contrib.suggestController')?.model?.state\n    )\n      return\n\n    if (execHistoryRef.current[execHistoryPosRef.current]) {\n      const command =\n        execHistoryRef.current[execHistoryPosRef.current].command || ''\n      editor.setValue(command)\n      execHistoryPosRef.current++\n    }\n  }\n\n  const resetHistoryPos = () => {\n    execHistoryPosRef.current = 0\n  }\n\n  const isHistoryScrolled = () =>\n    execHistoryPosRef.current >= execHistoryRef.current.length\n\n  return {\n    onQuickHistoryAccess,\n    resetHistoryPos,\n    isHistoryScrolled,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useCommandHistory.types.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nexport interface UseCommandHistoryProps {\n  monacoObjects: React.RefObject<Nullable<IEditorMount>>\n  historyItems: CommandExecutionUI[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useDslSyntax.ts",
    "content": "import { useEffect, useRef, useState } from 'react'\nimport { useParams } from 'react-router-dom'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { DSLNaming } from 'uiSrc/constants'\nimport {\n  createSyntaxWidget,\n  Nullable,\n  triggerUpdateCursorPosition,\n} from 'uiSrc/utils'\nimport { IMonacoQuery } from 'uiSrc/utils/monaco/monacoInterfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { findSuggestionsByQueryArgs } from 'uiSrc/pages/workbench/utils/query'\n\nimport { UseDslSyntaxProps, UseDslSyntaxReturn } from './useDslSyntax.types'\n\nconst SYNTAX_CONTEXT_ID = 'syntaxWidgetContext'\nconst SYNTAX_WIDGET_ID = 'syntax.content.widget'\nconst argInQuotesRegExp = /^['\"](.|[\\r\\n])*['\"]$/\n\n/**\n * Manages the DSL syntax widget for Cypher, JMESPath, SQLite expressions.\n * Opens a dedicated editor when user clicks the widget or presses Shift+Space.\n * Workbench-only feature -- not used in Vector Search editor.\n */\nexport const useDslSyntax = ({\n  monacoObjects,\n}: UseDslSyntaxProps): UseDslSyntaxReturn => {\n  const [isDedicatedEditorOpen, setIsDedicatedEditorOpen] = useState(false)\n  const isDedicatedEditorOpenRef = useRef<boolean>(false)\n  const isWidgetOpen = useRef(false)\n  const isWidgetEscaped = useRef(false)\n  const selectedArg = useRef('')\n  const syntaxCommand = useRef<any>(null)\n  const syntaxWidgetContextRef =\n    useRef<Nullable<monacoEditor.editor.IContextKey<boolean>>>(null)\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  useEffect(() => {\n    isDedicatedEditorOpenRef.current = isDedicatedEditorOpen\n  }, [isDedicatedEditorOpen])\n\n  const onTriggerContentWidget = (\n    position: Nullable<monacoEditor.Position>,\n    language: string = '',\n  ): monacoEditor.editor.IContentWidget => ({\n    getId: () => SYNTAX_WIDGET_ID,\n    getDomNode: () =>\n      createSyntaxWidget(`Use ${language} Editor`, 'Shift+Space'),\n    getPosition: () => ({\n      position,\n      preference: [monacoEditor.editor.ContentWidgetPositionPreference.BELOW],\n    }),\n  })\n\n  const hideSyntaxWidget = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n  ) => {\n    editor.removeContentWidget(onTriggerContentWidget(null))\n    syntaxWidgetContextRef.current?.set(false)\n    isWidgetOpen.current = false\n  }\n\n  const showSyntaxWidget = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    position: monacoEditor.Position,\n    language: string,\n  ) => {\n    editor.addContentWidget(onTriggerContentWidget(position, language))\n    isWidgetOpen.current = true\n    syntaxWidgetContextRef.current?.set(true)\n  }\n\n  const onPressWidget = () => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects.current\n\n    setIsDedicatedEditorOpen(true)\n    editor.updateOptions({ readOnly: true })\n    hideSyntaxWidget(editor)\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_OPENED,\n      eventData: {\n        databaseId: instanceId,\n        lang: syntaxCommand.current.lang,\n      },\n    })\n  }\n\n  const onCancelDedicatedEditor = () => {\n    setIsDedicatedEditorOpen(false)\n    isDedicatedEditorOpenRef.current = false\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects.current\n\n    editor.updateOptions({ readOnly: false })\n    triggerUpdateCursorPosition(editor)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_CANCELLED,\n      eventData: {\n        databaseId: instanceId,\n        lang: syntaxCommand.current.lang,\n      },\n    })\n  }\n\n  const updateArgFromDedicatedEditor = (value: string = '') => {\n    if (!syntaxCommand.current || !monacoObjects.current) return\n    const { editor } = monacoObjects.current\n\n    const model = editor.getModel()\n    if (!model) return\n\n    const wrapQuote = syntaxCommand.current.argToReplace[0]\n    const replaceCommand = syntaxCommand.current.fullQuery.replace(\n      syntaxCommand.current.argToReplace,\n      `${wrapQuote}${value}${wrapQuote}`,\n    )\n    editor.updateOptions({ readOnly: false })\n    editor.executeEdits(null, [\n      {\n        range: new monacoEditor.Range(\n          syntaxCommand.current.commandPosition.startLine,\n          0,\n          syntaxCommand.current.commandPosition.endLine,\n          model.getLineLength(syntaxCommand.current.commandPosition.endLine) +\n            1,\n        ),\n        text: replaceCommand,\n      },\n    ])\n    setIsDedicatedEditorOpen(false)\n    isDedicatedEditorOpenRef.current = false\n    triggerUpdateCursorPosition(editor)\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_SAVED,\n      eventData: {\n        databaseId: instanceId,\n        lang: syntaxCommand.current.lang,\n      },\n    })\n  }\n\n  /**\n   * Hide the DSL syntax widget if it is currently visible.\n   * Intended to be called *before* suggestion logic runs so that\n   * `isWidgetOpen.current` is already `false` during `handleSuggestions`.\n   */\n  const hideWidget = () => {\n    const { editor } = monacoObjects?.current || {}\n    if (editor && isWidgetOpen.current) {\n      hideSyntaxWidget(editor)\n    }\n  }\n\n  const handleDslSyntax = (\n    e: monacoEditor.editor.ICursorPositionChangedEvent,\n    command: Nullable<IMonacoQuery>,\n  ) => {\n    const { editor } = monacoObjects?.current || {}\n\n    if (!command?.info || !editor) {\n      isWidgetEscaped.current = false\n      return\n    }\n\n    const isContainsDSL = command.info?.arguments?.some((arg) => arg.dsl)\n    if (!isContainsDSL) {\n      isWidgetEscaped.current = false\n      return\n    }\n\n    const [beforeOffsetArgs, [currentOffsetArg]] = command.args\n    const foundArg = findSuggestionsByQueryArgs(\n      [{ ...command.info, token: command.name }],\n      beforeOffsetArgs,\n    )\n\n    const DSL = foundArg?.stopArg?.dsl\n    if (DSL && argInQuotesRegExp.test(currentOffsetArg)) {\n      if (isWidgetEscaped.current) return\n\n      const lang = DSLNaming[DSL] ?? null\n      lang && showSyntaxWidget(editor, e.position, lang)\n      selectedArg.current = currentOffsetArg\n      syntaxCommand.current = {\n        ...command,\n        lang: DSL,\n        argToReplace: currentOffsetArg,\n      }\n    } else {\n      isWidgetEscaped.current = false\n    }\n  }\n\n  /**\n   * Sets up DSL-specific editor commands (Shift+Space, Escape, mouse click on widget).\n   * Call this inside editorDidMount's onSetup callback.\n   */\n  const setupDslCommands = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => {\n    syntaxWidgetContextRef.current = editor.createContextKey(\n      SYNTAX_CONTEXT_ID,\n      false,\n    )\n\n    editor.addCommand(\n      monaco.KeyMod.Shift | monaco.KeyCode.Space,\n      () => {\n        onPressWidget()\n      },\n      SYNTAX_CONTEXT_ID,\n    )\n\n    editor.onMouseDown((e: monacoEditor.editor.IEditorMouseEvent) => {\n      if (\n        (e.target as monacoEditor.editor.IMouseTargetContentWidget)?.detail ===\n        SYNTAX_WIDGET_ID\n      ) {\n        onPressWidget()\n      }\n    })\n\n    editor.addCommand(\n      monaco.KeyCode.Escape,\n      () => {\n        hideSyntaxWidget(editor)\n        isWidgetEscaped.current = true\n      },\n      SYNTAX_CONTEXT_ID,\n    )\n  }\n\n  return {\n    isDedicatedEditorOpen,\n    isDedicatedEditorOpenRef,\n    isWidgetOpen,\n    selectedArg,\n    syntaxCommand,\n    setupDslCommands,\n    hideWidget,\n    handleDslSyntax,\n    onPressWidget,\n    onCancelDedicatedEditor,\n    updateArgFromDedicatedEditor,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useDslSyntax.types.ts",
    "content": "import { MutableRefObject, RefObject } from 'react'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { IMonacoQuery, Nullable } from 'uiSrc/utils'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nexport interface UseDslSyntaxProps {\n  monacoObjects: RefObject<Nullable<IEditorMount>>\n}\n\nexport interface UseDslSyntaxReturn {\n  isDedicatedEditorOpen: boolean\n  isDedicatedEditorOpenRef: MutableRefObject<boolean>\n  isWidgetOpen: MutableRefObject<boolean>\n  selectedArg: MutableRefObject<string>\n  syntaxCommand: MutableRefObject<Nullable<IMonacoQuery>>\n  setupDslCommands: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => void\n  /** Hide the DSL widget if visible. Call before suggestion logic. */\n  hideWidget: () => void\n  handleDslSyntax: (\n    e: monacoEditor.editor.ICursorPositionChangedEvent,\n    command: Nullable<IMonacoQuery>,\n  ) => void\n  onPressWidget: () => void\n  onCancelDedicatedEditor: () => void\n  updateArgFromDedicatedEditor: (value?: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useMonacoRedisEditor.ts",
    "content": "import { useEffect, useRef } from 'react'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport {\n  getMonacoAction,\n  MonacoAction,\n  Nullable,\n  triggerUpdateCursorPosition,\n} from 'uiSrc/utils'\nimport { ISnippetController } from 'uiSrc/pages/workbench/interfaces'\n\nimport {\n  UseMonacoRedisEditorProps,\n  UseMonacoRedisEditorReturn,\n} from './useMonacoRedisEditor.types'\n\n/**\n * Core editor lifecycle management:\n * - editorDidMount handler (stores refs, registers submit action, focus)\n * - snippet mode exit\n * - cursor position triggering\n */\nexport const useMonacoRedisEditor = ({\n  monacoObjects,\n  onSubmit,\n  onSetup,\n}: UseMonacoRedisEditorProps): UseMonacoRedisEditorReturn => {\n  const contributionRef = useRef<Nullable<ISnippetController>>(null)\n\n  // Dispose the snippet controller on unmount (mirrors original Query.tsx cleanup)\n  useEffect(\n    () => () => {\n      contributionRef.current?.dispose?.()\n    },\n    [],\n  )\n\n  const editorDidMount = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => {\n    monacoObjects.current = { editor, monaco }\n\n    // hack for exit from snippet mode after click Enter\n    // https://github.com/microsoft/monaco-editor/issues/2756\n    contributionRef.current =\n      editor.getContribution<ISnippetController>('snippetController2')\n\n    editor.focus()\n\n    // Register Ctrl+Enter submit action\n    editor.addAction(\n      getMonacoAction(\n        MonacoAction.Submit,\n        (ed) => onSubmit(ed.getValue()),\n        monaco,\n      ),\n    )\n\n    // Allow consumers to do additional setup\n    onSetup?.(editor, monaco)\n  }\n\n  const onExitSnippetMode = () => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects.current\n\n    if (contributionRef.current?.isInSnippet?.()) {\n      const { lineNumber = 0, column = 0 } = editor?.getPosition() ?? {}\n      editor.setSelection(\n        new monacoEditor.Selection(lineNumber, column, lineNumber, column),\n      )\n      contributionRef.current?.cancel?.()\n    }\n  }\n\n  return {\n    editorDidMount,\n    onExitSnippetMode,\n    triggerUpdateCursorPosition,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useMonacoRedisEditor.types.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nexport interface UseMonacoRedisEditorProps {\n  monacoObjects: React.MutableRefObject<Nullable<IEditorMount>>\n  onSubmit: (value?: string) => void\n  onSetup?: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => void\n}\n\nexport interface UseMonacoRedisEditorReturn {\n  editorDidMount: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => void\n  onExitSnippetMode: () => void\n  triggerUpdateCursorPosition: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n  ) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useQueryDecorations.ts",
    "content": "import { useEffect, useRef } from 'react'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\nimport { compact, first } from 'lodash'\n\nimport {\n  decoration,\n  isParamsLine,\n  Nullable,\n  toModelDeltaDecoration,\n} from 'uiSrc/utils'\n\nimport { UseQueryDecorationsProps } from './useQueryDecorations.types'\n\n/**\n * Manages multi-line command decorations and params line highlighting\n * in the Monaco editor.\n */\nexport const useQueryDecorations = ({\n  monacoObjects,\n  query,\n}: UseQueryDecorationsProps) => {\n  const decorationCollection =\n    useRef<Nullable<monacoEditor.editor.IEditorDecorationsCollection>>(null)\n\n  // Update decorations whenever query changes.\n  // Lazily initializes the decoration collection on first run after\n  // the editor mounts, avoiding the anti-pattern of using a ref's\n  // .current value as a useEffect dependency.\n  useEffect(() => {\n    if (!monacoObjects.current) return\n    const { editor } = monacoObjects.current\n\n    if (!decorationCollection.current) {\n      decorationCollection.current = editor.createDecorationsCollection()\n    }\n\n    const { monaco } = monacoObjects.current\n    const lines = query.split('\\n')\n    const firstLine = first(lines) ?? ''\n    const notCommandRegEx = /^[\\s|//]/\n\n    const newDecorations = compact(\n      lines.map((line, index) => {\n        if (\n          !line ||\n          notCommandRegEx.test(line) ||\n          (index === 0 && isParamsLine(line))\n        )\n          return null\n        const lineNumber = index + 1\n\n        return toModelDeltaDecoration(\n          decoration(\n            monaco,\n            `decoration_${lineNumber}`,\n            lineNumber,\n            1,\n            lineNumber,\n            1,\n          ),\n        )\n      }),\n    )\n\n    // highlight the first line with params\n    if (isParamsLine(firstLine)) {\n      newDecorations.push({\n        range: new monaco.Range(1, 1, 1, firstLine.indexOf(']') + 2),\n        options: { inlineClassName: 'monaco-params-line' },\n      })\n    }\n\n    decorationCollection.current.set(newDecorations)\n  }, [query])\n\n  return { decorationCollection }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useQueryDecorations.types.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nexport interface UseQueryDecorationsProps {\n  monacoObjects: React.RefObject<Nullable<IEditorMount>>\n  query: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useQueryEditor.ts",
    "content": "import { useEffect } from 'react'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { useQueryEditorContext } from '../context/query-editor.context'\nimport { useRedisCompletions } from './useRedisCompletions'\nimport { useMonacoRedisEditor } from './useMonacoRedisEditor'\nimport { useQueryDecorations } from './useQueryDecorations'\nimport {\n  UseQueryEditorOptions,\n  UseQueryEditorReturn,\n} from './useQueryEditor.types'\n\n/**\n * Shared editor lifecycle hook that composes useRedisCompletions,\n * useMonacoRedisEditor, and useQueryDecorations.\n *\n * Handles the common editor setup: language providers, key handlers\n * (parameter hints, snippet exit, escape suggestions), cursor change,\n * initial suggestions, decorations, and cleanup.\n *\n * Consumers can extend behaviour via optional callbacks (onSetup,\n * onKeyDown, onCursorChange, onQueryChange, onCleanup,\n * shouldTriggerParameterHints).\n */\nexport const useQueryEditor = (\n  options: UseQueryEditorOptions,\n): UseQueryEditorReturn => {\n  const {\n    onSubmit,\n    onSetup,\n    onKeyDown,\n    beforeCursorChange,\n    onCursorChange,\n    onQueryChange,\n    onCleanup,\n    shouldTriggerParameterHints,\n    isDedicatedEditorOpen,\n  } = options\n\n  const { monacoObjects, query, setQuery, commands, indexes, activeIndexName } =\n    useQueryEditorContext()\n\n  // Autocomplete & suggestions\n  const completions = useRedisCompletions({\n    monacoObjects,\n    commands,\n    indexes,\n    activeIndexName,\n  })\n\n  function handleEditorSetup(\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) {\n    // Register language providers\n    completions.setupProviders(monaco)\n\n    // Base key handler\n    editor.onKeyDown((e: monacoEditor.IKeyboardEvent) => {\n      // Trigger parameter hints on Tab / Enter / Space / Ctrl+Shift+Space\n      if (\n        e.keyCode === monacoEditor.KeyCode.Tab ||\n        e.keyCode === monacoEditor.KeyCode.Enter ||\n        (e.keyCode === monacoEditor.KeyCode.Space && e.ctrlKey && e.shiftKey) ||\n        (e.keyCode === monacoEditor.KeyCode.Space && !e.ctrlKey && !e.shiftKey)\n      ) {\n        const canTrigger = shouldTriggerParameterHints\n          ? shouldTriggerParameterHints()\n          : true\n        if (canTrigger) {\n          completions.onTriggerParameterHints()\n        }\n      }\n\n      // Workaround for Monaco issue #2756: exit snippet mode on Enter/Space\n      if (\n        e.keyCode === monacoEditor.KeyCode.Enter ||\n        e.keyCode === monacoEditor.KeyCode.Space\n      ) {\n        onExitSnippetMode()\n      }\n\n      // Dismiss suggestions on Escape so they don't reappear on next cursor change\n      if (\n        e.keyCode === monacoEditor.KeyCode.Escape &&\n        completions.isSuggestionsOpened()\n      ) {\n        completions.setEscapedSuggestions(true)\n      }\n\n      // Consumer-specific key handling\n      onKeyDown?.(e, { completions, onExitSnippetMode })\n    })\n\n    // Base cursor change handler\n    editor.onDidChangeCursorPosition(\n      (e: monacoEditor.editor.ICursorPositionChangedEvent) => {\n        // Allow consumers to clean up state (e.g. hide DSL widget)\n        // before suggestion logic runs.\n        beforeCursorChange?.()\n\n        const command = completions.handleCursorChange(\n          e,\n          isDedicatedEditorOpen?.() ?? false,\n        )\n        onCursorChange?.(e, command)\n      },\n    )\n\n    // Initial suggestions\n    completions.setSuggestionsData(completions.getSuggestions(editor).data)\n\n    // Consumer-specific setup\n    onSetup?.(editor, monaco, completions)\n  }\n\n  // Core editor lifecycle\n  const {\n    editorDidMount: baseEditorDidMount,\n    onExitSnippetMode,\n    triggerUpdateCursorPosition,\n  } = useMonacoRedisEditor({\n    monacoObjects,\n    onSubmit,\n    onSetup: handleEditorSetup,\n  })\n\n  // Decorations\n  useQueryDecorations({ monacoObjects, query })\n\n  // Cleanup on unmount\n  useEffect(\n    () => () => {\n      completions.disposeProviders()\n      onCleanup?.()\n    },\n    [],\n  )\n\n  const onChange = (value: string = '') => {\n    setQuery(value)\n    onQueryChange?.(value)\n  }\n\n  return {\n    editorDidMount: baseEditorDidMount,\n    onChange,\n    completions,\n    onExitSnippetMode,\n    triggerUpdateCursorPosition,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useQueryEditor.types.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { IMonacoQuery, Nullable } from 'uiSrc/utils'\nimport { UseRedisCompletionsReturn } from './useRedisCompletions.types'\n\nexport interface UseQueryEditorOptions {\n  /** Called when the user submits the query (Ctrl+Enter). */\n  onSubmit: (value?: string) => void\n\n  /**\n   * Called during editor setup after base setup completes.\n   * Use to register additional commands, listeners, or expose the editor ref.\n   */\n  onSetup?: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n    completions: UseRedisCompletionsReturn,\n  ) => void\n\n  /**\n   * Returns whether a dedicated editor is currently open.\n   * When true, cursor change handling is suppressed (suggestions skipped).\n   */\n  isDedicatedEditorOpen?: () => boolean\n\n  /**\n   * Additional key handler, called after base key handling.\n   * Base handling covers: parameter hints, snippet exit, escape suggestions.\n   */\n  onKeyDown?: (\n    e: monacoEditor.IKeyboardEvent,\n    helpers: {\n      completions: UseRedisCompletionsReturn\n      onExitSnippetMode: () => void\n    },\n  ) => void\n\n  /**\n   * Called before cursor-change suggestion logic runs.\n   * Use to hide widgets (e.g. DSL syntax widget) so that\n   * `handleSuggestions` sees consistent state.\n   */\n  beforeCursorChange?: () => void\n\n  /**\n   * Additional cursor change handler, called after base cursor handling.\n   * Receives the cursor event and the resolved command (if any).\n   */\n  onCursorChange?: (\n    e: monacoEditor.editor.ICursorPositionChangedEvent,\n    command: Nullable<IMonacoQuery>,\n  ) => void\n\n  /** Called on onChange after setQuery. */\n  onQueryChange?: (value: string) => void\n\n  /** Additional cleanup on unmount. */\n  onCleanup?: () => void\n\n  /**\n   * Guard for parameter hints.\n   * When provided and returns false, parameter hints are suppressed.\n   * Default: always trigger.\n   */\n  shouldTriggerParameterHints?: () => boolean\n}\n\nexport interface UseQueryEditorReturn {\n  editorDidMount: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    monaco: typeof monacoEditor,\n  ) => void\n  onChange: (value?: string) => void\n  completions: UseRedisCompletionsReturn\n  onExitSnippetMode: () => void\n  triggerUpdateCursorPosition: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n  ) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useRedisCompletions.ts",
    "content": "import { useEffect, useMemo, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { MonacoLanguage } from 'uiSrc/constants'\nimport {\n  Nullable,\n  actionTriggerParameterHints,\n  findCompleteQuery,\n} from 'uiSrc/utils'\nimport { IMonacoQuery } from 'uiSrc/utils/monaco/monacoInterfaces'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport {\n  getRange,\n  getRediSearchSignutureProvider,\n} from 'uiSrc/pages/workbench/utils/monaco'\nimport { CursorContext } from 'uiSrc/pages/workbench/types'\nimport {\n  asSuggestionsRef,\n  getCommandsSuggestions,\n  isIndexComplete,\n} from 'uiSrc/pages/workbench/utils/suggestions'\nimport {\n  COMMANDS_TO_GET_INDEX_INFO,\n  COMPOSITE_ARGS,\n  EmptySuggestionsIds,\n} from 'uiSrc/pages/workbench/constants'\nimport { useDebouncedEffect } from 'uiSrc/services'\nimport { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch'\nimport { findSuggestionsByArg } from 'uiSrc/pages/workbench/utils/searchSuggestions'\n\nimport {\n  UseRedisCompletionsProps,\n  UseRedisCompletionsReturn,\n} from './useRedisCompletions.types'\n\n/**\n * Manages the entire autocomplete, suggestion, and signature help system\n * for the Redis/RQE query editor.\n */\nexport const useRedisCompletions = ({\n  monacoObjects,\n  commands,\n  indexes,\n  activeIndexName,\n}: UseRedisCompletionsProps): UseRedisCompletionsReturn => {\n  const [selectedIndex, setSelectedIndex] = useState('')\n\n  const suggestionsRef = useRef<monacoEditor.languages.CompletionItem[]>([])\n  const helpWidgetRef = useRef<any>({ isOpen: false, data: {} })\n  const indexesRef = useRef<RedisResponseBuffer[]>([])\n  const attributesRef = useRef<any>([])\n  const isEscapedSuggestions = useRef<boolean>(false)\n\n  const disposeCompletionRef = useRef<() => void>(() => {})\n  const disposeSignatureRef = useRef<() => void>(() => {})\n\n  const { commandsArray: REDIS_COMMANDS_ARRAY, spec: REDIS_COMMANDS_SPEC } =\n    useSelector(appRedisCommandsSelector)\n\n  const dispatch = useDispatch()\n\n  const compositeTokens = useMemo(\n    () =>\n      commands\n        .filter((command) => command.token && command.token.includes(' '))\n        .map(({ token }) => token)\n        .concat(...COMPOSITE_ARGS),\n    [commands],\n  )\n\n  // Sync indexes ref\n  useEffect(() => {\n    indexesRef.current = indexes\n  }, [indexes])\n\n  // Debounced fetch of index info when selected index changes\n  useDebouncedEffect(\n    () => {\n      attributesRef.current = []\n      if (!isIndexComplete(selectedIndex)) return\n\n      const index = selectedIndex.replace(/^(['\"])(.*)\\1$/, '$2')\n      dispatch(\n        fetchRedisearchInfoAction(index, (data: any) => {\n          attributesRef.current = data?.attributes || []\n        }),\n      )\n    },\n    200,\n    [selectedIndex],\n  )\n\n  const setupProviders = (monaco: typeof monacoEditor) => {\n    disposeCompletionRef.current =\n      monaco.languages.registerCompletionItemProvider(MonacoLanguage.Redis, {\n        provideCompletionItems: (): monacoEditor.languages.CompletionList => ({\n          suggestions: suggestionsRef.current,\n        }),\n      }).dispose\n\n    disposeSignatureRef.current =\n      monaco.languages.registerSignatureHelpProvider(MonacoLanguage.Redis, {\n        provideSignatureHelp: (): any =>\n          getRediSearchSignutureProvider(helpWidgetRef?.current),\n      }).dispose\n  }\n\n  const disposeProviders = () => {\n    disposeCompletionRef.current()\n    disposeSignatureRef.current()\n  }\n\n  const onTriggerParameterHints = () => {\n    if (!monacoObjects.current) return\n\n    const { editor } = monacoObjects.current\n    const model = editor.getModel()\n    const { lineNumber = 0 } = editor.getPosition() ?? {}\n    const lineContent = model?.getLineContent(lineNumber)?.trim() ?? ''\n    const matchedCommand =\n      REDIS_COMMANDS_ARRAY.find((command) =>\n        lineContent?.trim().startsWith(command),\n      ) ?? ''\n    const isTriggerHints =\n      lineContent.split(' ').length < 2 + matchedCommand.split(' ').length\n\n    if (isTriggerHints) {\n      actionTriggerParameterHints(editor)\n    }\n  }\n\n  const isSuggestionsOpened = () => {\n    const { editor } = monacoObjects.current || {}\n    if (!editor) return false\n    const suggestController = editor.getContribution<any>(\n      'editor.contrib.suggestController',\n    )\n    return suggestController?.model?.state === 1\n  }\n\n  const triggerSuggestions = () => {\n    isEscapedSuggestions.current = false\n    const { editor } = monacoObjects.current || {}\n    setTimeout(() =>\n      editor?.trigger('', 'editor.action.triggerSuggest', { auto: false }),\n    )\n  }\n\n  const getSuggestions = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    command?: Nullable<IMonacoQuery>,\n  ): {\n    forceHide: boolean\n    forceShow: boolean\n    data: monacoEditor.languages.CompletionItem[]\n  } => {\n    const position = editor.getPosition()\n    const model = editor.getModel()\n\n    if (!position || !model) return asSuggestionsRef([])\n    const word = model.getWordUntilPosition(position)\n    const range = getRange(position, word)\n\n    if (position.column === 1) {\n      helpWidgetRef.current.isOpen = false\n      if (command?.info) return asSuggestionsRef([])\n      return asSuggestionsRef(\n        getCommandsSuggestions(commands, range),\n        false,\n        false,\n      )\n    }\n\n    if (!command?.info) {\n      return asSuggestionsRef(\n        getCommandsSuggestions(commands, range),\n        false,\n        false,\n      )\n    }\n\n    const { allArgs, args, cursor } = command\n    const [, [currentOffsetArg]] = args\n\n    if (COMMANDS_TO_GET_INDEX_INFO.some((name) => name === command.name)) {\n      setSelectedIndex(allArgs[1] || '')\n    } else {\n      setSelectedIndex('')\n    }\n\n    const cursorContext: CursorContext = {\n      ...cursor,\n      currentOffsetArg,\n      offset: command.commandCursorPosition,\n      range,\n    }\n    const { suggestions, helpWidget } = findSuggestionsByArg(\n      commands,\n      command,\n      cursorContext,\n      {\n        fields: attributesRef.current,\n        indexes: indexesRef.current,\n        activeIndexName,\n      },\n      isEscapedSuggestions.current,\n    )\n\n    if (helpWidget) {\n      const { isOpen, data } = helpWidget\n      helpWidgetRef.current = {\n        isOpen,\n        data: data || helpWidgetRef.current.data,\n      }\n    }\n\n    return suggestions\n  }\n\n  const handleSuggestions = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    command?: Nullable<IMonacoQuery>,\n  ) => {\n    const { data, forceHide, forceShow } = getSuggestions(editor, command)\n    suggestionsRef.current = data\n\n    // Prevent suggestions if editor is not focused or cursor is not set\n    if (!editor.hasTextFocus() || !editor.getPosition()) {\n      editor.trigger('', 'hideSuggestWidget', null)\n      return\n    }\n\n    if (!forceShow) {\n      editor.trigger('', 'editor.action.triggerParameterHints', '')\n      return\n    }\n\n    if (data.length) {\n      helpWidgetRef.current.isOpen = false\n      triggerSuggestions()\n      return\n    }\n\n    editor.trigger('', 'editor.action.triggerParameterHints', '')\n\n    if (forceHide) {\n      setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0)\n    } else {\n      helpWidgetRef.current.isOpen =\n        !isSuggestionsOpened() && helpWidgetRef.current.isOpen\n    }\n  }\n\n  /**\n   * Handles cursor change events for suggestion logic.\n   * Returns the parsed command if found (for DSL syntax and other consumers).\n   */\n  const handleCursorChange = (\n    e: monacoEditor.editor.ICursorPositionChangedEvent,\n    isDedicatedEditorOpen = false,\n  ): Nullable<IMonacoQuery> => {\n    if (!monacoObjects.current) return null\n    const { editor } = monacoObjects.current\n    const model = editor.getModel()\n\n    if (!model || isDedicatedEditorOpen) {\n      return null\n    }\n\n    const command = findCompleteQuery(\n      model,\n      e.position,\n      REDIS_COMMANDS_SPEC,\n      REDIS_COMMANDS_ARRAY,\n      compositeTokens as string[],\n    )\n\n    handleSuggestions(editor, command)\n    return command\n  }\n\n  const setupSuggestionWidgetListener = (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n  ) => {\n    const suggestionWidget = editor.getContribution<any>(\n      'editor.contrib.suggestController',\n    )\n    suggestionWidget?.onWillInsertSuggestItem(\n      ({ item }: Record<'item', any>) => {\n        if (item.completion.id === EmptySuggestionsIds.NoIndexes) {\n          helpWidgetRef.current.isOpen = true\n          editor.trigger('', 'hideSuggestWidget', null)\n          editor.trigger('', 'editor.action.triggerParameterHints', '')\n        }\n      },\n    )\n  }\n\n  const setSuggestionsData = (\n    data: monacoEditor.languages.CompletionItem[],\n  ) => {\n    suggestionsRef.current = data\n  }\n\n  const setEscapedSuggestions = (value: boolean) => {\n    isEscapedSuggestions.current = value\n  }\n\n  return {\n    selectedIndex,\n    helpWidgetRef: helpWidgetRef as React.MutableRefObject<{\n      isOpen: boolean\n      data: any\n    }>,\n    compositeTokens,\n    getSuggestions,\n    handleSuggestions,\n    onTriggerParameterHints,\n    triggerSuggestions,\n    isSuggestionsOpened,\n    setupProviders,\n    disposeProviders,\n    handleCursorChange,\n    setupSuggestionWidgetListener,\n    setSuggestionsData,\n    setEscapedSuggestions,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/hooks/useRedisCompletions.types.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { IRedisCommand } from 'uiSrc/constants'\nimport { IMonacoQuery, Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { IEditorMount } from 'uiSrc/pages/workbench/interfaces'\n\nexport interface UseRedisCompletionsProps {\n  monacoObjects: React.RefObject<Nullable<IEditorMount>>\n  commands: IRedisCommand[]\n  indexes: RedisResponseBuffer[]\n  activeIndexName?: string\n}\n\nexport interface UseRedisCompletionsReturn {\n  selectedIndex: string\n  helpWidgetRef: React.MutableRefObject<{ isOpen: boolean; data: any }>\n  compositeTokens: (string | undefined)[]\n  getSuggestions: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    command?: Nullable<IMonacoQuery>,\n  ) => {\n    forceHide: boolean\n    forceShow: boolean\n    data: monacoEditor.languages.CompletionItem[]\n  }\n  handleSuggestions: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    command?: Nullable<IMonacoQuery>,\n  ) => void\n  onTriggerParameterHints: () => void\n  triggerSuggestions: () => void\n  isSuggestionsOpened: () => boolean\n  setupProviders: (monaco: typeof monacoEditor) => void\n  disposeProviders: () => void\n  handleCursorChange: (\n    e: monacoEditor.editor.ICursorPositionChangedEvent,\n    isDedicatedEditorOpen?: boolean,\n  ) => Nullable<IMonacoQuery>\n  setupSuggestionWidgetListener: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n  ) => void\n  setSuggestionsData: (data: monacoEditor.languages.CompletionItem[]) => void\n  setEscapedSuggestions: (value: boolean) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/index.ts",
    "content": "import QueryCard from './query-card'\nimport QueryActions from './query-actions'\nimport QueryLiteActions from './query-lite-actions'\nimport QueryTutorials from './query-tutorials'\nimport { QueryResults } from './query-results'\n\nexport {\n  QueryCard,\n  QueryActions,\n  QueryLiteActions,\n  QueryTutorials,\n  QueryResults,\n}\n\nexport {\n  QueryEditorContextProvider,\n  useQueryEditorContext,\n} from './context/query-editor.context'\nexport type { QueryEditorContextValue } from './context/query-editor.context.types'\n\nexport {\n  useMonacoRedisEditor,\n  useRedisCompletions,\n  useQueryDecorations,\n  useCommandHistory,\n  useDslSyntax,\n  useQueryEditor,\n} from './hooks'\n\nexport { LoadingContainer } from './query.styles'\n\nexport {\n  QueryResultsProvider,\n  useQueryResultsContext,\n} from './context/query-results.context'\nexport type {\n  QueryResultsTelemetry,\n  QueryResultsContextValue,\n} from './context/query-results.context'\nexport type { QueryResultsProps } from './query-results'\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-actions/QueryActions.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport QueryActions, { Props } from './QueryActions'\n\nconst mockedProps = mock<Props>()\n\ndescribe('QueryActions', () => {\n  it('should render', () => {\n    expect(render(<QueryActions {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call props on click buttons', () => {\n    const onChangeMode = jest.fn()\n    const onChangeGroupMode = jest.fn()\n    const onSubmit = jest.fn()\n\n    render(\n      <QueryActions\n        {...mockedProps}\n        onChangeMode={onChangeMode}\n        onChangeGroupMode={onChangeGroupMode}\n        onSubmit={onSubmit}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('btn-change-mode'))\n    expect(onChangeMode).toHaveBeenCalled()\n\n    fireEvent.click(screen.getByTestId('btn-change-group-mode'))\n    expect(onChangeGroupMode).toHaveBeenCalled()\n\n    fireEvent.click(screen.getByTestId('btn-submit'))\n    expect(onSubmit).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-actions/QueryActions.styles.ts",
    "content": "import styled from 'styled-components'\nimport Divider from 'uiSrc/components/divider/Divider'\nexport const QADivider = styled(Divider)`\n  height: 20px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-actions/QueryActions.tsx",
    "content": "import React from 'react'\n\nimport { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces'\nimport { KEYBOARD_SHORTCUTS } from 'uiSrc/constants'\nimport { KeyboardShortcut, RiTooltip } from 'uiSrc/components'\nimport { isGroupMode } from 'uiSrc/utils'\n\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text } from 'uiSrc/components/base/text'\nimport RunButton from 'uiSrc/components/query/components/RunButton'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { QADivider } from 'uiSrc/components/query/query-actions/QueryActions.styles'\nimport { ToggleButton } from 'uiSrc/components/base/forms/buttons'\n\nexport interface Props {\n  onChangeMode?: () => void\n  onChangeGroupMode?: () => void\n  onSubmit: () => void\n  activeMode: RunQueryMode\n  resultsMode?: ResultsMode\n  isLoading?: boolean\n}\n\nconst QueryActions = (props: Props) => {\n  const {\n    isLoading,\n    activeMode,\n    resultsMode,\n    onChangeMode,\n    onChangeGroupMode,\n    onSubmit,\n  } = props\n  const KeyBoardTooltipContent = KEYBOARD_SHORTCUTS?.workbench?.runQuery && (\n    <>\n      <Text size=\"s\">{KEYBOARD_SHORTCUTS.workbench.runQuery?.label}:</Text>\n      <Spacer size=\"s\" />\n      <KeyboardShortcut\n        separator={KEYBOARD_SHORTCUTS?._separator}\n        items={KEYBOARD_SHORTCUTS.workbench.runQuery.keys}\n      />\n    </>\n  )\n\n  return (\n    <Row align=\"center\" justify=\"between\" gap=\"l\" grow={false}>\n      {onChangeMode && (\n        <RiTooltip\n          position=\"left\"\n          content=\"Enables the raw output mode\"\n          data-testid=\"change-mode-tooltip\"\n        >\n          <ToggleButton\n            onPressedChange={() => onChangeMode()}\n            disabled={isLoading}\n            pressed={activeMode === RunQueryMode.Raw}\n            data-testid=\"btn-change-mode\"\n          >\n            <RiIcon size=\"m\" type=\"RawModeIcon\" />\n            <Text size=\"s\">Raw mode</Text>\n          </ToggleButton>\n        </RiTooltip>\n      )}\n      {onChangeGroupMode && (\n        <RiTooltip\n          position=\"left\"\n          content={\n            <>\n              Groups the command results into a single window.\n              <br />\n              When grouped, the results can be visualized only in the text\n              format.\n            </>\n          }\n          data-testid=\"group-results-tooltip\"\n        >\n          <ToggleButton\n            onPressedChange={() => onChangeGroupMode()}\n            disabled={isLoading}\n            pressed={isGroupMode(resultsMode)}\n            data-testid=\"btn-change-group-mode\"\n          >\n            <RiIcon size=\"m\" type=\"GroupModeIcon\" />\n            <Text size=\"s\">Group results</Text>\n          </ToggleButton>\n        </RiTooltip>\n      )}\n      <QADivider orientation=\"vertical\" colorVariable=\"separatorColor\" />\n      <RiTooltip\n        position=\"left\"\n        content={\n          isLoading\n            ? 'Please wait while the commands are being executed…'\n            : KeyBoardTooltipContent\n        }\n        data-testid=\"run-query-tooltip\"\n      >\n        <RunButton isLoading={isLoading} onSubmit={onSubmit} />\n      </RiTooltip>\n    </Row>\n  )\n}\n\nexport default QueryActions\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-actions/index.ts",
    "content": "import QueryActions from './QueryActions'\n\nexport default QueryActions\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCard.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { ResultsMode } from 'uiSrc/slices/interfaces/workbench'\nimport { cleanup, fireEvent, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport QueryCard, { Props, getSummaryText } from './QueryCard'\nimport { QueryResultsProvider } from '../context/query-results.context'\n\nconst mockedProps = mock<Props>()\n\nconst mockResult = [\n  {\n    response: 'response',\n    status: 'success',\n  },\n]\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [],\n  }),\n}))\n\nconst renderQueryCardComponent = (props: Partial<Props> = {}) => {\n  return render(\n    <QueryResultsProvider telemetry={{}}>\n      <QueryCard {...instance(mockedProps)} {...props} />\n    </QueryResultsProvider>,\n    {\n      store,\n    },\n  )\n}\n\ndescribe('QueryCard', () => {\n  it('should render', () => {\n    const { container } = renderQueryCardComponent()\n    expect(container).toBeTruthy()\n  })\n\n  it('Cli result should not in the document before Expand', () => {\n    const cliResultTestId = 'query-cli-result'\n\n    const { queryByTestId } = renderQueryCardComponent()\n\n    const cliResultEl = queryByTestId(cliResultTestId)\n    expect(cliResultEl).not.toBeInTheDocument()\n  })\n\n  it('Cli result should in the document when \"isOpen = true\"', () => {\n    const cliResultTestId = 'query-cli-result'\n\n    const { queryByTestId } = renderQueryCardComponent({\n      isOpen: true,\n      result: mockResult,\n    })\n\n    const cliResultEl = queryByTestId(cliResultTestId)\n\n    expect(cliResultEl).toBeInTheDocument()\n  })\n\n  it('Cli result should not in the document when \"isOpen = false\"', () => {\n    const cliResultTestId = 'query-cli-result'\n\n    const { queryByTestId } = renderQueryCardComponent({\n      isOpen: false,\n      result: mockResult,\n    })\n\n    const cliResultEl = queryByTestId(cliResultTestId)\n\n    expect(cliResultEl).not.toBeInTheDocument()\n  })\n\n  it('Should be in the document when resultsMode === ResultsMode.GroupMode', () => {\n    const cliResultTestId = 'query-cli-result'\n\n    const { queryByTestId } = renderQueryCardComponent({\n      isOpen: false,\n      result: mockResult,\n      resultsMode: ResultsMode.GroupMode,\n    })\n\n    const cliResultEl = queryByTestId(cliResultTestId)\n\n    expect(cliResultEl).not.toBeInTheDocument()\n  })\n\n  it('Click on the header should call onToggleOpen', () => {\n    const cardHeaderTestId = 'query-card-open'\n    const mockId = '123'\n    const mockOnToggleOpen = jest.fn()\n\n    const { queryByTestId } = renderQueryCardComponent({\n      id: mockId,\n      result: mockResult,\n      onToggleOpen: mockOnToggleOpen,\n    })\n\n    const cardHeaderTestEl = queryByTestId(cardHeaderTestId)\n\n    fireEvent.click(cardHeaderTestEl)\n\n    expect(mockOnToggleOpen).toHaveBeenCalledWith(mockId, true)\n  })\n\n  it('Should return correct summary string', () => {\n    const summary = { total: 2, success: 1, fail: 1 }\n    const summaryText = '2 Command(s) - 1 success, 1 error(s)'\n\n    const summaryString = getSummaryText(summary)\n\n    expect(summaryString).toEqual(summaryText)\n  })\n\n  it('should render QueryCardCliResultWrapper when command is null', () => {\n    const { queryByTestId } = renderQueryCardComponent({\n      resultsMode: ResultsMode.GroupMode,\n      result: null,\n      isOpen: true,\n      command: null,\n    })\n\n    const queryCommonResultEl = queryByTestId('query-common-result-wrapper')\n    const queryCliResultEl = queryByTestId('query-cli-result-wrapper')\n\n    expect(queryCommonResultEl).toBeInTheDocument()\n    expect(queryCliResultEl).not.toBeInTheDocument()\n  })\n\n  it('should render QueryCardCliResult when result reached response size threshold', () => {\n    const { queryByTestId } = renderQueryCardComponent({\n      resultsMode: ResultsMode.GroupMode,\n      result: [\n        {\n          status: CommandExecutionStatus.Success,\n          response: 'Any message about size limit threshold exceeded',\n          sizeLimitExceeded: true,\n        },\n      ],\n      isOpen: true,\n      command: null,\n    })\n    const queryCliResultEl = queryByTestId('query-cli-result')\n\n    expect(queryCliResultEl).toBeInTheDocument()\n  })\n\n  it('should render properly result when it has pure number', () => {\n    const { getByTestId } = renderQueryCardComponent({\n      resultsMode: ResultsMode.GroupMode,\n      result: [\n        {\n          status: CommandExecutionStatus.Success,\n          response: 1,\n        },\n      ],\n      isOpen: true,\n      command: 'del key',\n    })\n    const queryCliResultEl = getByTestId('query-cli-result')\n\n    expect(queryCliResultEl.textContent).toBe('(integer) 1')\n  })\n\n  it('should render QueryCardCliResult when result reached response size threshold even w/o flag', () => {\n    const { queryByTestId } = renderQueryCardComponent({\n      resultsMode: ResultsMode.GroupMode,\n      result: [\n        {\n          status: CommandExecutionStatus.Success,\n          response:\n            'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.',\n        },\n      ],\n      isOpen: true,\n      command: null,\n    })\n    const queryCliResultEl = queryByTestId('query-cli-result')\n\n    expect(queryCliResultEl).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCard.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { useParams } from 'react-router-dom'\nimport { isNull } from 'lodash'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport {\n  DEFAULT_TEXT_VIEW_TYPE,\n  ProfileQueryType,\n  WBQueryType,\n} from 'uiSrc/pages/workbench/constants'\nimport {\n  ResultsMode,\n  ResultsSummary,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces/workbench'\nimport {\n  getVisualizationsByCommand,\n  getWBQueryType,\n  isGroupResults,\n  isSilentModeWithoutError,\n  Maybe,\n} from 'uiSrc/utils'\nimport { appPluginsSelector } from 'uiSrc/slices/app/plugins'\nimport {\n  CommandExecutionResult,\n  IPluginVisualization,\n} from 'uiSrc/slices/interfaces'\n\nimport QueryCardHeader from './QueryCardHeader'\nimport QueryCardCliResultWrapper from './QueryCardCliResultWrapper'\nimport QueryCardCliPlugin from './QueryCardCliPlugin'\nimport QueryCardCommonResult, {\n  CommonErrorResponse,\n} from './QueryCardCommonResult'\n\nimport styles from './styles.module.scss'\nimport { useQueryResultsContext } from '../context/query-results.context'\n\nexport interface Props {\n  id: string\n  command: string\n  isOpen: boolean\n  result: Maybe<CommandExecutionResult[]>\n  activeMode: RunQueryMode\n  mode?: RunQueryMode\n  activeResultsMode?: ResultsMode\n  resultsMode?: ResultsMode\n  emptyCommand?: boolean\n  summary?: ResultsSummary\n  createdAt?: Date\n  loading?: boolean\n  clearing?: boolean\n  isNotStored?: boolean\n  executionTime?: number\n  db?: number\n  onToggleOpen?: (id: string, isOpen: boolean) => void\n  onQueryDelete: () => void\n  onQueryReRun: () => void\n  onQueryProfile: (type: ProfileQueryType) => void\n}\n\nconst getDefaultPlugin = (views: IPluginVisualization[], query: string) =>\n  getVisualizationsByCommand(query, views).find((view) => view.default)\n    ?.uniqId || DEFAULT_TEXT_VIEW_TYPE.id\n\nexport const getSummaryText = (\n  summary?: ResultsSummary,\n  mode?: ResultsMode,\n) => {\n  if (summary) {\n    const { total, success, fail } = summary\n    const summaryText = `${total} Command(s) - ${success} success`\n    if (!isSilentModeWithoutError(mode, summary?.fail)) {\n      return `${summaryText}, ${fail} error(s)`\n    }\n    return summaryText\n  }\n  return summary\n}\n\nconst QueryCard = (props: Props) => {\n  const {\n    id,\n    command = '',\n    result,\n    activeMode,\n    mode,\n    activeResultsMode,\n    resultsMode,\n    summary,\n    isOpen,\n    createdAt,\n    onToggleOpen,\n    onQueryDelete,\n    onQueryProfile,\n    onQueryReRun,\n    loading,\n    clearing,\n    emptyCommand,\n    isNotStored,\n    executionTime,\n    db,\n  } = props\n\n  const { visualizations = [] } = useSelector(appPluginsSelector)\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const [isFullScreen, setIsFullScreen] = useState<boolean>(false)\n  const [queryType, setQueryType] = useState<WBQueryType>(\n    getWBQueryType(command, visualizations),\n  )\n  const [viewTypeSelected, setViewTypeSelected] =\n    useState<WBQueryType>(queryType)\n  const [message, setMessage] = useState<string>('')\n  const [selectedViewValue, setSelectedViewValue] = useState<string>(\n    getDefaultPlugin(visualizations, command || '') || queryType,\n  )\n\n  const { telemetry } = useQueryResultsContext()\n\n  useEffect(() => {\n    window.addEventListener('keydown', handleEscFullScreen)\n    return () => {\n      window.removeEventListener('keydown', handleEscFullScreen)\n    }\n  }, [isFullScreen])\n\n  const handleEscFullScreen = (event: KeyboardEvent) => {\n    if (event.key === keys.ESCAPE && isFullScreen) {\n      toggleFullScreen()\n    }\n  }\n\n  const toggleFullScreen = () => {\n    setIsFullScreen((isFull) => {\n      telemetry.onFullScreenToggled?.({\n        databaseId: instanceId,\n        state: isFull ? 'Close' : 'Open',\n      })\n\n      return !isFull\n    })\n  }\n\n  useEffect(() => {\n    setQueryType(getWBQueryType(command, visualizations))\n  }, [command])\n\n  useEffect(() => {\n    if (visualizations.length) {\n      const type = getWBQueryType(command, visualizations)\n      setQueryType(type)\n      setViewTypeSelected(type)\n      setSelectedViewValue(\n        getDefaultPlugin(visualizations, command) || queryType,\n      )\n    }\n  }, [visualizations])\n\n  const toggleOpen = () => {\n    if (isFullScreen || isSilentModeWithoutError(resultsMode, summary?.fail))\n      return\n\n    onToggleOpen?.(id, !isOpen)\n  }\n\n  const changeViewTypeSelected = (type: WBQueryType, value: string) => {\n    setViewTypeSelected(type)\n    setSelectedViewValue(value)\n  }\n\n  const commonError = CommonErrorResponse(id, command, result)\n\n  const isSizeLimitExceededResponse = (\n    result: Maybe<CommandExecutionResult[]>,\n  ) => {\n    const resultObj = result?.[0]\n    // response.includes - to be backward compatible with responses which don't include sizeLimitExceeded flag\n    return (\n      resultObj?.sizeLimitExceeded === true ||\n      resultObj?.response?.includes?.('Results have been deleted')\n    )\n  }\n\n  return (\n    <div\n      className={cx(styles.containerWrapper, {\n        fullscreen: isFullScreen,\n        [styles.isOpen]: isOpen,\n      })}\n      id={id}\n    >\n      <div\n        className={cx(styles.container)}\n        data-testid={`query-card-container-${id}`}\n        data-full-screen={isFullScreen}\n      >\n        <QueryCardHeader\n          isOpen={isOpen}\n          isFullScreen={isFullScreen}\n          query={command}\n          loading={loading}\n          clearing={clearing}\n          createdAt={createdAt}\n          message={message}\n          queryType={queryType}\n          selectedValue={selectedViewValue}\n          activeMode={activeMode}\n          mode={mode}\n          resultsMode={resultsMode}\n          activeResultsMode={activeResultsMode}\n          emptyCommand={emptyCommand}\n          summary={summary}\n          summaryText={getSummaryText(summary, resultsMode)}\n          executionTime={executionTime}\n          db={db}\n          toggleOpen={toggleOpen}\n          toggleFullScreen={toggleFullScreen}\n          setSelectedValue={changeViewTypeSelected}\n          onQueryDelete={onQueryDelete}\n          onQueryReRun={onQueryReRun}\n          onQueryProfile={onQueryProfile}\n        />\n        {isOpen && (\n          <>\n            {React.isValidElement(commonError) &&\n            (!isGroupResults(resultsMode) || isNull(command)) ? (\n              <QueryCardCommonResult loading={loading} result={commonError} />\n            ) : (\n              <>\n                {isSizeLimitExceededResponse(result) ? (\n                  <QueryCardCliResultWrapper\n                    loading={loading}\n                    query={command}\n                    resultsMode={resultsMode}\n                    result={result}\n                    isNotStored={isNotStored}\n                    isFullScreen={isFullScreen}\n                  />\n                ) : (\n                  <>\n                    {isGroupResults(resultsMode) && (\n                      <QueryCardCliResultWrapper\n                        loading={loading}\n                        query={command}\n                        db={db}\n                        resultsMode={resultsMode}\n                        result={result}\n                        isNotStored={isNotStored}\n                        isFullScreen={isFullScreen}\n                        data-testid=\"group-mode-card\"\n                      />\n                    )}\n                    {(resultsMode === ResultsMode.Default || !resultsMode) && (\n                      <>\n                        {viewTypeSelected === WBQueryType.Plugin && (\n                          <>\n                            {!loading && result !== undefined ? (\n                              <QueryCardCliPlugin\n                                id={selectedViewValue}\n                                result={result}\n                                query={command}\n                                mode={mode}\n                                setMessage={setMessage}\n                                commandId={id}\n                              />\n                            ) : (\n                              <div className={styles.loading}>\n                                <LoadingContent\n                                  lines={5}\n                                  data-testid=\"loading-content\"\n                                />\n                              </div>\n                            )}\n                          </>\n                        )}\n                        {viewTypeSelected === WBQueryType.Text && (\n                          <QueryCardCliResultWrapper\n                            loading={loading}\n                            query={command}\n                            resultsMode={resultsMode}\n                            result={result}\n                            isNotStored={isNotStored}\n                            isFullScreen={isFullScreen}\n                          />\n                        )}\n                      </>\n                    )}\n                  </>\n                )}\n              </>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport default React.memo(QueryCard)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport QueryCardCliDefaultResult, { Props } from './QueryCardCliDefaultResult'\n\nconst mockedProps = mock<Props>()\n\ndescribe('QueryCardCliDefaultResult', () => {\n  it('should render', () => {\n    expect(\n      render(<QueryCardCliDefaultResult {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport VirtualList from 'uiSrc/components/virtual-list'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  items: (string | JSX.Element)[]\n  isFullScreen?: boolean\n}\n\nexport const MIN_ROWS_COUNT = 11\nexport const MAX_CARD_HEIGHT = 210\n\nconst QueryCardCliDefaultResult = (props: Props) => {\n  const { items = [], isFullScreen } = props\n\n  return (\n    <div\n      className={cx(styles.container, 'query-card-output-response-success', {\n        fullscreen: isFullScreen,\n      })}\n      data-testid=\"query-cli-card-result\"\n    >\n      <VirtualList\n        items={items}\n        dynamicHeight={\n          !isFullScreen\n            ? { itemsCount: MIN_ROWS_COUNT, maxHeight: MAX_CARD_HEIGHT }\n            : undefined\n        }\n      />\n    </div>\n  )\n}\n\nexport default QueryCardCliDefaultResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliDefaultResult/index.ts",
    "content": "import QueryCardCliDefaultResult from './QueryCardCliDefaultResult'\n\nexport default QueryCardCliDefaultResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliDefaultResult/styles.module.scss",
    "content": ".container {\n  position: relative;\n  width: 100%;\n  display: flex;\n  flex-grow: 1;\n  height: 100%;\n  overflow: hidden !important;\n}\n\n.listContent {\n  @include eui.scrollBar;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport QueryCardCliGroupResult, { Props } from './QueryCardCliGroupResult'\n\nconst mockedProps = mock<Props>()\n\ndescribe('QueryCardCliGroupResult', () => {\n  it('should render', () => {\n    const mockResult = [\n      {\n        response: [\n          {\n            response: 'response',\n            status: 'success',\n          },\n        ],\n        status: 'success',\n      },\n    ]\n    expect(\n      render(\n        <QueryCardCliGroupResult\n          {...instance(mockedProps)}\n          result={mockResult}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('Should render result when result is undefined', () => {\n    expect(\n      render(<QueryCardCliGroupResult {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render error when command is psubscribe', () => {\n    const mockResult = [\n      {\n        response: [\n          {\n            id: 'id',\n            command: 'psubscribe',\n            response: 'response',\n            status: CommandExecutionStatus.Success,\n          },\n        ],\n      },\n    ]\n    const { container } = render(\n      <QueryCardCliGroupResult\n        {...instance(mockedProps)}\n        result={mockResult}\n      />,\n    )\n    const errorBtn = container.querySelector(\n      '[data-test-subj=\"pubsub-page-btn\"]',\n    )\n\n    expect(errorBtn).toBeInTheDocument()\n  })\n\n  it('should render (nil) when response is null', () => {\n    const mockResult = [\n      {\n        response: [\n          {\n            id: 'id',\n            command: 'psubscribe',\n            response: null,\n            status: CommandExecutionStatus.Success,\n          },\n        ],\n      },\n    ]\n    const { container } = render(\n      <QueryCardCliGroupResult\n        {...instance(mockedProps)}\n        result={mockResult}\n      />,\n    )\n    const errorBtn = container.querySelector(\n      '[data-test-subj=\"pubsub-page-btn\"]',\n    )\n\n    expect(errorBtn).not.toBeInTheDocument()\n    expect(screen.getByText('(nil)')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.tsx",
    "content": "import { flatten, isNull } from 'lodash'\nimport React from 'react'\n\nimport { CommandExecutionResult } from 'uiSrc/slices/interfaces'\nimport {\n  cliParseCommandsGroupResult,\n  wbSummaryCommand,\n  Maybe,\n} from 'uiSrc/utils'\nimport QueryCardCliDefaultResult from '../QueryCardCliDefaultResult'\nimport { CommonErrorResponse } from '../QueryCardCommonResult'\n\nexport interface Props {\n  result?: Maybe<CommandExecutionResult[]>\n  isFullScreen?: boolean\n  db?: number\n}\n\nconst QueryCardCliGroupResult = (props: Props) => {\n  const { result = [], isFullScreen, db } = props\n\n  return (\n    <div\n      data-testid=\"query-cli-default-result\"\n      className=\"query-card-output-response-success\"\n    >\n      <QueryCardCliDefaultResult\n        isFullScreen={isFullScreen}\n        items={flatten(\n          result?.[0]?.response.map((item: any) => {\n            const commonError = CommonErrorResponse(\n              item.id,\n              item.command,\n              item.response,\n            )\n            if (React.isValidElement(commonError) && !isNull(item.response)) {\n              return [wbSummaryCommand(item.command), commonError]\n            }\n            return flatten(cliParseCommandsGroupResult(item, db))\n          }),\n        )}\n      />\n    </div>\n  )\n}\n\nexport default QueryCardCliGroupResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliGroupResult/index.ts",
    "content": "import QueryCardCliGroupResult from './QueryCardCliGroupResult'\n\nexport default QueryCardCliGroupResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { PluginEvents } from 'uiSrc/plugins/pluginEvents'\nimport { pluginApi } from 'uiSrc/services/PluginAPI'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { formatToText, replaceEmptyValue } from 'uiSrc/utils'\nimport {\n  sendPluginCommandAction,\n  getPluginStateAction,\n  setPluginStateAction,\n} from 'uiSrc/slices/app/plugins'\nimport QueryCardCliPlugin, { Props } from './QueryCardCliPlugin'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/services/PluginAPI', () => ({\n  pluginApi: {\n    onEvent: jest.fn(),\n    sendEvent: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  formatToText: jest.fn(),\n  replaceEmptyValue: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [\n      {\n        id: '1',\n        uniqId: '1',\n        name: 'test',\n        plugin: '',\n        activationMethod: 'render',\n        matchCommands: ['*'],\n      },\n    ],\n  }),\n  sendPluginCommandAction: jest.fn(),\n  getPluginStateAction: jest.fn(),\n  setPluginStateAction: jest.fn(),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('QueryCardCliPlugin', () => {\n  it('should render', () => {\n    expect(\n      render(<QueryCardCliPlugin {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should subscribes on events', () => {\n    const onEventMock = jest.fn()\n\n    ;(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)\n\n    render(<QueryCardCliPlugin {...instance(mockedProps)} id=\"1\" />)\n\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.heightChanged,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.loaded,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.error,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.setHeaderText,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.executeRedisCommand,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.getState,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.setState,\n      expect.any(Function),\n    )\n    expect(onEventMock).toBeCalledWith(\n      expect.any(String),\n      PluginEvents.formatRedisReply,\n      expect.any(Function),\n    )\n  })\n\n  it('should subscribes and call sendPluginCommandAction', () => {\n    const mockedSendPluginCommandAction = jest\n      .fn()\n      .mockImplementation(() => jest.fn())\n    ;(sendPluginCommandAction as jest.Mock).mockImplementation(\n      mockedSendPluginCommandAction,\n    )\n\n    const onEventMock = jest\n      .fn()\n      .mockImplementation(\n        (_iframeId: string, event: string, callback: (data: any) => void) => {\n          if (event === PluginEvents.executeRedisCommand) {\n            callback({ command: 'info' })\n          }\n        },\n      )\n\n    ;(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)\n\n    render(<QueryCardCliPlugin {...instance(mockedProps)} id=\"1\" />)\n\n    expect(mockedSendPluginCommandAction).toBeCalledWith({\n      command: 'info',\n      onSuccessAction: expect.any(Function),\n      onFailAction: expect.any(Function),\n    })\n  })\n\n  it('should subscribes and call getPluginStateAction with proper data', () => {\n    const mockedGetPluginStateAction = jest\n      .fn()\n      .mockImplementation(() => jest.fn())\n    ;(getPluginStateAction as jest.Mock).mockImplementation(\n      mockedGetPluginStateAction,\n    )\n\n    const onEventMock = jest\n      .fn()\n      .mockImplementation(\n        (_iframeId: string, event: string, callback: (data: any) => void) => {\n          if (event === PluginEvents.getState) {\n            callback({ requestId: 5 })\n          }\n        },\n      )\n\n    ;(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)\n\n    render(\n      <QueryCardCliPlugin {...instance(mockedProps)} id=\"1\" commandId=\"100\" />,\n    )\n\n    expect(mockedGetPluginStateAction).toBeCalledWith({\n      commandId: '100',\n      onSuccessAction: expect.any(Function),\n      onFailAction: expect.any(Function),\n      visualizationId: '1',\n    })\n  })\n\n  it('should subscribes and call setPluginStateAction with proper data', () => {\n    const mockedSetPluginStateAction = jest\n      .fn()\n      .mockImplementation(() => jest.fn())\n    ;(setPluginStateAction as jest.Mock).mockImplementation(\n      mockedSetPluginStateAction,\n    )\n\n    const onEventMock = jest\n      .fn()\n      .mockImplementation(\n        (_iframeId: string, event: string, callback: (data: any) => void) => {\n          if (event === PluginEvents.setState) {\n            callback({ requestId: 5 })\n          }\n        },\n      )\n\n    ;(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)\n\n    render(\n      <QueryCardCliPlugin {...instance(mockedProps)} id=\"1\" commandId=\"200\" />,\n    )\n\n    expect(mockedSetPluginStateAction).toBeCalledWith({\n      commandId: '200',\n      onSuccessAction: expect.any(Function),\n      onFailAction: expect.any(Function),\n      visualizationId: '1',\n    })\n  })\n\n  it('should subscribes and call formatToText', () => {\n    const formatToTextMock = jest.fn()\n    const replaceEmptyValueMock = jest.fn()\n    ;(replaceEmptyValue as jest.Mock)\n      .mockImplementation(replaceEmptyValueMock)\n      .mockReturnValue([])\n    ;(formatToText as jest.Mock).mockImplementation(formatToTextMock)\n    const onEventMock = jest\n      .fn()\n      .mockImplementation(\n        (_iframeId: string, event: string, callback: (dat: any) => void) => {\n          if (event === PluginEvents.formatRedisReply) {\n            callback({\n              requestId: '1',\n              data: { response: [], command: 'info' },\n            })\n          }\n        },\n      )\n\n    ;(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)\n\n    render(<QueryCardCliPlugin {...instance(mockedProps)} id=\"1\" />)\n\n    expect(formatToTextMock).toBeCalledWith([], 'info')\n  })\n\n  it('should subscribes and call replaceEmptyValue', () => {\n    const replaceEmptyValueMock = jest.fn()\n    ;(replaceEmptyValue as jest.Mock).mockImplementation(replaceEmptyValueMock)\n    const onEventMock = jest\n      .fn()\n      .mockImplementation(\n        (_iframeId: string, event: string, callback: (dat: any) => void) => {\n          if (event === PluginEvents.formatRedisReply) {\n            callback({\n              requestId: '1',\n              data: { response: [], command: 'info' },\n            })\n          }\n        },\n      )\n\n    ;(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)\n\n    render(<QueryCardCliPlugin {...instance(mockedProps)} id=\"1\" />)\n\n    expect(replaceEmptyValueMock).toBeCalledWith([])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx",
    "content": "import React, { useContext, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { v4 as uuidv4 } from 'uuid'\nimport { pluginApi } from 'uiSrc/services/PluginAPI'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport {\n  getBaseApiUrl,\n  Nullable,\n  formatToText,\n  replaceEmptyValue,\n} from 'uiSrc/utils'\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { Theme } from 'uiSrc/constants'\nimport {\n  CommandExecutionResult,\n  IPluginVisualization,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport { PluginEvents } from 'uiSrc/plugins/pluginEvents'\nimport { prepareIframeHtml } from 'uiSrc/plugins/pluginImport'\nimport {\n  appPluginsSelector,\n  getPluginStateAction,\n  sendPluginCommandAction,\n  setPluginStateAction,\n} from 'uiSrc/slices/app/plugins'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { appServerInfoSelector } from 'uiSrc/slices/app/info'\n\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  result: CommandExecutionResult[]\n  query: any\n  id: string\n  setMessage: (text: string) => void\n  commandId: string\n  mode?: RunQueryMode\n}\n\nenum StylesNamePostfix {\n  Dark = '/dark_theme.css',\n  Light = '/light_theme.css',\n  Global = '/global_styles.css',\n}\n\nenum ActionTypes {\n  Resolve = 'resolve',\n  Reject = 'reject',\n}\n\nconst baseUrl = getBaseApiUrl()\n\nconst QueryCardCliPlugin = (props: Props) => {\n  const {\n    query,\n    id,\n    result,\n    setMessage,\n    commandId,\n    mode = RunQueryMode.Raw,\n  } = props\n  const { visualizations = [], staticPath } = useSelector(appPluginsSelector)\n  const { modules = [] } = useSelector(connectedInstanceSelector)\n  const serverInfo = useSelector(appServerInfoSelector)\n\n  const [currentView, setCurrentView] = useState<Nullable<any>>(null)\n  const [currentPlugin, setCurrentPlugin] = useState<Nullable<string>>(null)\n  const [isPluginLoaded, setIsPluginLoaded] = useState<boolean>(false)\n  const [error, setError] = useState<string>('')\n  const pluginIframeRef = useRef<Nullable<HTMLIFrameElement>>(null)\n  const prevPluginHeightRef = useRef<string>('0')\n  const generatedIframeNameRef = useRef<string>('')\n  const { theme } = useContext(ThemeContext)\n\n  const dispatch = useDispatch()\n\n  const sendMessageToPlugin = (data = {}) => {\n    const event: any = document.createEvent('Event')\n    event.initEvent('message', false, false)\n    event.data = data\n    event.origin = '*'\n    pluginIframeRef?.current?.contentWindow?.dispatchEvent(event)\n  }\n\n  const executeCommand = (method: string) => {\n    sendMessageToPlugin({\n      event: 'executeCommand',\n      method,\n      data: { command: query, data: result, mode },\n    })\n  }\n\n  const sendRedisCommand = ({\n    command = '',\n    requestId = '',\n  }: {\n    command: string\n    requestId: string\n  }) => {\n    const commonOptions = {\n      event: PluginEvents.executeRedisCommand,\n      requestId,\n    }\n    dispatch(\n      sendPluginCommandAction({\n        command,\n        onSuccessAction: (response) => {\n          sendMessageToPlugin({\n            ...commonOptions,\n            actionType: ActionTypes.Resolve,\n            data: response.result,\n          })\n        },\n        onFailAction: (error: any) => {\n          sendMessageToPlugin({\n            ...commonOptions,\n            actionType: ActionTypes.Reject,\n            data: error,\n          })\n        },\n      }),\n    )\n  }\n\n  const getPluginState = ({ requestId }: { requestId: string }) => {\n    const commonOptions = {\n      event: PluginEvents.getState,\n      requestId,\n    }\n    dispatch(\n      getPluginStateAction({\n        visualizationId: id,\n        commandId,\n        onSuccessAction: (response) => {\n          sendMessageToPlugin({\n            ...commonOptions,\n            actionType: ActionTypes.Resolve,\n            data: response?.state ?? null,\n          })\n        },\n        onFailAction: (error: any) => {\n          sendMessageToPlugin({\n            ...commonOptions,\n            actionType: ActionTypes.Reject,\n            data: error,\n          })\n        },\n      }),\n    )\n  }\n\n  const setPluginState = ({\n    requestId,\n    state,\n  }: {\n    requestId: string\n    state: any\n  }) => {\n    const commonOptions = {\n      event: PluginEvents.setState,\n      requestId,\n    }\n    dispatch(\n      setPluginStateAction({\n        visualizationId: id,\n        commandId,\n        pluginState: state,\n        onSuccessAction: () => {\n          sendMessageToPlugin({\n            ...commonOptions,\n            actionType: ActionTypes.Resolve,\n            data: state,\n          })\n        },\n        onFailAction: (error: any) => {\n          sendMessageToPlugin({\n            ...commonOptions,\n            actionType: ActionTypes.Reject,\n            data: error,\n          })\n        },\n      }),\n    )\n  }\n\n  const formatRedisResponse = ({\n    requestId,\n    data,\n  }: {\n    requestId: string\n    data: { response: any; command: string }\n  }) => {\n    try {\n      const reply = formatToText(\n        replaceEmptyValue(data?.response),\n        data.command,\n      )\n\n      sendMessageToPlugin({\n        event: PluginEvents.formatRedisReply,\n        requestId,\n        actionType: ActionTypes.Resolve,\n        data: reply,\n      })\n    } catch (e) {\n      sendMessageToPlugin({\n        event: PluginEvents.formatRedisReply,\n        requestId,\n        actionType: ActionTypes.Reject,\n        data: e,\n      })\n    }\n  }\n\n  useEffect(() => {\n    if (currentView === null) return\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.heightChanged,\n      (height: string) => {\n        if (pluginIframeRef?.current) {\n          pluginIframeRef.current.height = height || prevPluginHeightRef.current\n          prevPluginHeightRef.current = height\n        }\n      },\n    )\n\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.loaded,\n      () => {\n        setIsPluginLoaded(true)\n        setError('')\n        executeCommand(currentView.activationMethod)\n      },\n    )\n\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.error,\n      (error: string) => {\n        setIsPluginLoaded(true)\n        setError(error)\n      },\n    )\n\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.setHeaderText,\n      (text: string) => {\n        setMessage(text)\n      },\n    )\n\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.executeRedisCommand,\n      sendRedisCommand,\n    )\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.getState,\n      getPluginState,\n    )\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.setState,\n      setPluginState,\n    )\n    pluginApi.onEvent(\n      generatedIframeNameRef.current,\n      PluginEvents.formatRedisReply,\n      formatRedisResponse,\n    )\n  }, [currentView])\n\n  const renderPluginIframe = (config: any) => {\n    const html = prepareIframeHtml({\n      ...config,\n      bodyClass: theme === Theme.Dark ? 'theme_DARK' : 'theme_LIGHT',\n      modules,\n    })\n    // @ts-ignore\n    pluginIframeRef.current.srcdoc = html\n  }\n\n  const getGlobalStylesSrc = (): string =>\n    `${baseUrl}${staticPath}${StylesNamePostfix.Global}`\n\n  const getThemeSrc = (): string =>\n    `${baseUrl}${staticPath}${theme === Theme.Dark ? StylesNamePostfix.Dark : StylesNamePostfix.Light}`\n\n  const generateStylesSrc = (styles: string): string[] => {\n    const themeSrc = getThemeSrc()\n    const globalSrc = getGlobalStylesSrc()\n\n    return [globalSrc, themeSrc, `${baseUrl}${styles}`]\n  }\n\n  useEffect(() => {\n    const view = visualizations.find(\n      (visualization: IPluginVisualization) => visualization.uniqId === id,\n    )\n    if (view) {\n      generatedIframeNameRef.current = `${view.plugin.name}-${uuidv4()}`\n      setCurrentView(view)\n\n      const { plugin } = view\n      if (plugin?.name !== currentPlugin) {\n        renderPluginIframe({\n          baseUrl: `${baseUrl}${plugin.baseUrl}`,\n          scriptPath: plugin.scriptSrc,\n          scriptSrc: `${baseUrl}${plugin.scriptSrc}`,\n          stylesSrc: generateStylesSrc(plugin.stylesSrc),\n          iframeId: generatedIframeNameRef.current,\n          appVersion: serverInfo?.appVersion,\n        })\n        setCurrentPlugin(plugin?.name || null)\n        return\n      }\n      executeCommand(view.activationMethod)\n    }\n  }, [result, id])\n\n  return (\n    <div\n      className={cx(\n        'queryResultsContainer',\n        'pluginStyles',\n        styles.pluginWrapperResult,\n      )}\n    >\n      <div data-testid=\"query-plugin-result\">\n        <iframe\n          seamless\n          className={cx('pluginIframe', styles.pluginIframe, {\n            [styles.hidden]: !currentPlugin || !isPluginLoaded || !!error,\n          })}\n          title={id}\n          ref={pluginIframeRef}\n          src=\"about:blank\"\n          referrerPolicy=\"no-referrer\"\n          sandbox=\"allow-same-origin allow-scripts\"\n          data-testid=\"pluginIframe\"\n        />\n        {!!error && (\n          <div className={styles.container}>\n            <FlexItem grow className=\"query-card-output-response-fail\">\n              <span data-testid=\"query-card-no-module-output\">\n                <span className={styles.alertIconWrapper}>\n                  <RiIcon\n                    type=\"ToastDangerIcon\"\n                    color=\"danger600\"\n                    style={{ display: 'inline', marginRight: 10 }}\n                  />\n                </span>\n                <ColorText color=\"danger\">{error}</ColorText>\n              </span>\n            </FlexItem>\n          </div>\n        )}\n        {!isPluginLoaded && (\n          <div>\n            <LoadingContent lines={5} />\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport default React.memo(QueryCardCliPlugin)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/index.ts",
    "content": "import QueryCardCliPlugin from './QueryCardCliPlugin'\n\nexport default QueryCardCliPlugin\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n  flex: auto;\n  padding: 9px 20px;\n  overflow: auto;\n  white-space: pre-wrap;\n  word-break: break-all;\n  line-height: 17px;\n\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--textColorShade);\n\n  background-color: var(--euiPageBackgroundColor);\n\n  z-index: 6;\n}\n\n.loading {\n  // margin-top: 8px;\n  height: 17px;\n  max-width: 600px;\n\n  :global(.euiLoadingContent__singleLine) {\n    margin-bottom: 0 !important;\n  }\n}\n\n.pluginIframe {\n  width: 100%;\n}\n\n.hidden {\n  display: none;\n}\n\n.pluginWrapperResult {\n  max-height: 600px;\n  overflow: auto;\n  @include eui.scrollBar;\n}\n\n.alertIconWrapper {\n  height: 19px;\n  display: inline-flex;\n  align-items: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport { ResultsMode } from 'uiSrc/slices/interfaces/workbench'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport QueryCardCliResultWrapper, { Props } from './QueryCardCliResultWrapper'\nimport QueryCardCliDefaultResult, {\n  Props as QueryCardCliDefaultResultProps,\n} from '../QueryCardCliDefaultResult'\nimport QueryCardCliGroupResult, {\n  Props as QueryCardCliGroupResultProps,\n} from '../QueryCardCliGroupResult'\n\nconst mockedProps = mock<Props>()\nconst mockedQueryCardCliDefaultResultProps =\n  mock<QueryCardCliDefaultResultProps>()\nconst mockedQueryCardCliGroupResultProps = mock<QueryCardCliGroupResultProps>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('QueryCardCliResultWrapper', () => {\n  it('should render', () => {\n    const mockResult = [\n      {\n        response: 'response',\n        status: CommandExecutionStatus.Success,\n      },\n    ]\n    expect(\n      render(\n        <QueryCardCliResultWrapper\n          {...instance(mockedProps)}\n          result={mockResult}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('Result element should render with result prop', () => {\n    const mockResult = [\n      {\n        response: 'response',\n        status: CommandExecutionStatus.Success,\n      },\n    ]\n\n    render(\n      <QueryCardCliResultWrapper\n        {...instance(mockedProps)}\n        result={mockResult}\n      />,\n    )\n\n    expect(screen.queryByTestId('query-cli-result')).toBeInTheDocument()\n    expect(screen.queryByTestId('query-cli-result')).toHaveTextContent(\n      mockResult?.[0]?.response,\n    )\n  })\n\n  it('Result element should render (nil) result', () => {\n    const mockResult = [\n      {\n        response: '',\n        status: 'success',\n      },\n    ]\n\n    render(\n      <QueryCardCliResultWrapper\n        {...instance(mockedProps)}\n        result={mockResult}\n      />,\n    )\n\n    expect(screen.queryByTestId('query-cli-result')).toHaveTextContent('(nil)')\n  })\n\n  it('should render QueryCardCliDefaultResult', () => {\n    expect(\n      render(\n        <QueryCardCliDefaultResult\n          {...instance(mockedQueryCardCliDefaultResultProps)}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render QueryCardCliGroupResult', () => {\n    const mockResult = [\n      {\n        response: ['response'],\n        status: 'success',\n      },\n    ]\n\n    render(\n      <QueryCardCliResultWrapper\n        {...instance(mockedProps)}\n        resultsMode={ResultsMode.GroupMode}\n        result={mockResult}\n      />,\n    )\n\n    expect(\n      render(\n        <QueryCardCliGroupResult\n          {...instance(mockedQueryCardCliGroupResultProps)}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render QueryCardCliDefaultResult when result.response is not array', () => {\n    const mockResult = [\n      {\n        response: 'response',\n        status: CommandExecutionStatus.Success,\n      },\n    ]\n\n    render(\n      <QueryCardCliResultWrapper\n        {...instance(mockedProps)}\n        resultsMode={ResultsMode.GroupMode}\n        result={mockResult}\n      />,\n    )\n\n    expect(\n      render(\n        <QueryCardCliDefaultResult\n          {...instance(mockedQueryCardCliDefaultResultProps)}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('Should render loader', () => {\n    render(<QueryCardCliResultWrapper {...instance(mockedProps)} loading />)\n\n    expect(screen.queryByTestId('query-cli-loader')).toBeInTheDocument()\n  })\n\n  it('should render warning', () => {\n    const mockResult = [\n      {\n        response: 'response',\n        status: CommandExecutionStatus.Success,\n      },\n    ]\n    render(\n      <QueryCardCliResultWrapper\n        {...instance(mockedProps)}\n        result={mockResult}\n        isNotStored\n      />,\n    )\n\n    expect(screen.queryByTestId('query-cli-warning')).toBeInTheDocument()\n  })\n\n  it('Result element should render (nil) result', () => {\n    const mockResult = [\n      {\n        response: '',\n        status: CommandExecutionStatus.Success,\n      },\n    ]\n\n    const { queryByTestId } = render(\n      <QueryCardCliResultWrapper\n        {...instance(mockedProps)}\n        result={mockResult}\n      />,\n    )\n\n    const resultEl = queryByTestId('query-cli-card-result')\n\n    expect(resultEl).toHaveTextContent('(nil)')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { isArray } from 'lodash'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { CommandExecutionResult } from 'uiSrc/slices/interfaces'\nimport { ResultsMode } from 'uiSrc/slices/interfaces/workbench'\nimport {\n  cliParseTextResponse,\n  formatToText,\n  isGroupResults,\n  Maybe,\n  replaceEmptyValue,\n} from 'uiSrc/utils'\n\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport QueryCardCliDefaultResult from '../QueryCardCliDefaultResult'\nimport QueryCardCliGroupResult from '../QueryCardCliGroupResult'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  query: string\n  result: Maybe<CommandExecutionResult[]>\n  loading?: boolean\n  resultsMode?: ResultsMode\n  isNotStored?: boolean\n  isFullScreen?: boolean\n  db?: number\n}\n\nconst QueryCardCliResultWrapper = (props: Props) => {\n  const {\n    result = [],\n    query,\n    loading,\n    resultsMode,\n    isNotStored,\n    isFullScreen,\n    db,\n  } = props\n\n  return (\n    <div\n      data-testid=\"query-cli-result-wrapper\"\n      className={cx('queryResultsContainer', styles.container)}\n    >\n      {!loading && (\n        <div data-testid=\"query-cli-result\" className={cx(styles.content)}>\n          {isNotStored && (\n            <Text className={styles.alert} data-testid=\"query-cli-warning\">\n              <RiIcon type=\"ToastDangerIcon\" className={styles.alertIcon} />\n              The result is too big to be saved. It will be deleted after the\n              application is closed.\n            </Text>\n          )}\n          {isGroupResults(resultsMode) && isArray(result[0]?.response) ? (\n            <QueryCardCliGroupResult\n              result={result}\n              isFullScreen={isFullScreen}\n              db={db}\n            />\n          ) : (\n            <QueryCardCliDefaultResult\n              isFullScreen={isFullScreen}\n              items={\n                result[0]?.status === CommandExecutionStatus.Success\n                  ? formatToText(\n                      replaceEmptyValue(result[0]?.response),\n                      query,\n                    ).split('\\n')\n                  : [\n                      cliParseTextResponse(\n                        replaceEmptyValue(result[0]?.response),\n                        '',\n                        result[0]?.status,\n                      ),\n                    ]\n              }\n            />\n          )}\n        </div>\n      )}\n      {loading && (\n        <div className={styles.loading} data-testid=\"query-cli-loader\">\n          <LoadingContent lines={1} />\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default React.memo(QueryCardCliResultWrapper)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/index.ts",
    "content": "import QueryCardCliResultWrapper from './QueryCardCliResultWrapper'\n\nexport default QueryCardCliResultWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCliResultWrapper/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n  flex: auto;\n  padding: 9px 8px 9px 20px;\n  overflow: auto;\n  white-space: pre-wrap;\n  word-break: break-all;\n  position: relative;\n\n  font: normal normal normal 14px/17px Inconsolata;\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--textColorShade);\n\n  background-color: var(--wbRunResultsBg);\n\n  z-index: 6;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.loading {\n  height: 17px;\n  max-width: 600px;\n\n  :global(.euiLoadingContent__singleLine) {\n    margin-bottom: 0 !important;\n  }\n}\n\n.container .alert {\n  font-size: 14px !important;\n  line-height: 17px !important;\n  letter-spacing: -0.13px !important;\n  color: var(--euiColorWarningLight) !important;\n  margin-bottom: 4px;\n}\n\n.container .alertIcon {\n  margin-right: 6px;\n  margin-top: -3px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCommonResult/QueryCardCommonResult.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport QueryCardCommonResult, { Props } from './QueryCardCommonResult'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('QueryCardCliResult', () => {\n  it('should render', () => {\n    expect(\n      render(<QueryCardCommonResult {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n  it('should render (nil) result', () => {\n    const result = ''\n    const { queryByTestId } = render(\n      <QueryCardCommonResult {...instance(mockedProps)} result={result} />,\n    )\n\n    const resultEl = queryByTestId('query-common-result')\n\n    expect(resultEl).toHaveTextContent('(nil)')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCommonResult/QueryCardCommonResult.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  result: React.ReactElement | string\n  loading?: boolean\n}\n\nconst QueryCardCommonResult = (props: Props) => {\n  const { result, loading } = props\n\n  return (\n    <div\n      data-testid=\"query-common-result-wrapper\"\n      className={cx('queryResultsContainer', styles.container)}\n    >\n      {!loading && (\n        <div data-testid=\"query-common-result\">{result || '(nil)'}</div>\n      )}\n      {loading && (\n        <div className={styles.loading}>\n          <LoadingContent lines={1} />\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default QueryCardCommonResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport {\n  checkUnsupportedCommand,\n  checkUnsupportedModuleCommand,\n  cliParseTextResponse,\n  CliPrefix,\n  getCommandRepeat,\n  isRepeatCountCorrect,\n} from 'uiSrc/utils'\nimport { ModuleNotLoaded } from 'uiSrc/components'\nimport { cliTexts } from 'uiSrc/components/messages/cli-output/cliOutput'\nimport { SelectCommand } from 'uiSrc/constants/cliOutput'\nimport {\n  CommandMonitor,\n  CommandPSubscribe,\n  CommandSubscribe,\n  CommandHello3,\n  Pages,\n} from 'uiSrc/constants'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nimport { showMonitor } from 'uiSrc/slices/cli/monitor'\n\nconst CommonErrorResponse = (id: string, command = '', result?: any) => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { unsupportedCommands: cliUnsupportedCommands, blockingCommands } =\n    useSelector(cliSettingsSelector)\n  const { modules } = useSelector(connectedInstanceSelector)\n  const dispatch = useDispatch()\n  const unsupportedCommands = [\n    SelectCommand.toLowerCase(),\n    ...cliUnsupportedCommands,\n    ...blockingCommands,\n  ]\n  const [commandLine, countRepeat] = getCommandRepeat(command || '')\n\n  // Flow if MONITOR command was executed\n  if (checkUnsupportedCommand([CommandMonitor.toLowerCase()], commandLine)) {\n    return cliTexts.MONITOR_COMMAND(() => {\n      dispatch(showMonitor())\n    })\n  }\n  // Flow if SUBSCRIBE command was executed\n  if (checkUnsupportedCommand([CommandSubscribe.toLowerCase()], commandLine)) {\n    return cliTexts.SUBSCRIBE_COMMAND_CLI(Pages.pubSub(instanceId))\n  }\n  // Flow if PSUBSCRIBE command was executed\n  if (checkUnsupportedCommand([CommandPSubscribe.toLowerCase()], commandLine)) {\n    return cliTexts.PSUBSCRIBE_COMMAND(Pages.pubSub(instanceId))\n  }\n\n  // Flow if HELLO 3 command was executed\n  if (checkUnsupportedCommand([CommandHello3.toLowerCase()], commandLine)) {\n    return cliTexts.HELLO3_COMMAND()\n  }\n\n  const unsupportedCommand = checkUnsupportedCommand(\n    unsupportedCommands,\n    commandLine,\n  )\n\n  if (result === null) {\n    return cliParseTextResponse(\n      cliTexts.UNABLE_TO_DECRYPT,\n      '',\n      CommandExecutionStatus.Fail,\n      CliPrefix.QueryCard,\n    )\n  }\n\n  if (!isRepeatCountCorrect(countRepeat)) {\n    return cliParseTextResponse(\n      cliTexts.REPEAT_COUNT_INVALID,\n      commandLine,\n      CommandExecutionStatus.Fail,\n      CliPrefix.QueryCard,\n    )\n  }\n\n  if (unsupportedCommand) {\n    return cliParseTextResponse(\n      cliTexts.WORKBENCH_UNSUPPORTED_COMMANDS(\n        commandLine.slice(0, unsupportedCommand.length),\n        [...blockingCommands, ...unsupportedCommands].join(', '),\n      ),\n      commandLine,\n      CommandExecutionStatus.Fail,\n    )\n  }\n  const unsupportedModule = checkUnsupportedModuleCommand(modules, commandLine)\n\n  if (unsupportedModule) {\n    return <ModuleNotLoaded moduleName={unsupportedModule} id={id} />\n  }\n\n  return null\n}\n\nexport default CommonErrorResponse\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCommonResult/components/CommonErrorResponse/index.ts",
    "content": "import CommonErrorResponse from './CommonErrorResponse'\n\nexport default CommonErrorResponse\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCommonResult/index.ts",
    "content": "import QueryCardCommonResult from './QueryCardCommonResult'\nimport CommonErrorResponse from './components/CommonErrorResponse'\n\nexport { CommonErrorResponse }\n\nexport default QueryCardCommonResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardCommonResult/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n  flex: auto;\n  padding: 9px 20px;\n  overflow: auto;\n  white-space: pre-wrap;\n  word-break: break-all;\n\n  font: normal normal normal 14px/17px Inconsolata;\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--textColorShade);\n\n  background-color: var(--euiPageBackgroundColor);\n\n  z-index: 6;\n}\n\n.loading {\n  // margin-top: 8px;\n  height: 17px;\n  max-width: 600px;\n\n  :global(.euiLoadingContent__singleLine) {\n    margin-bottom: 0 !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  fireEvent,\n  act,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport QueryCardHeader, { Props } from './QueryCardHeader'\nimport {\n  QueryResultsProvider,\n  QueryResultsTelemetry,\n} from '../../context/query-results.context'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [\n      {\n        id: '1',\n        uniqId: '1',\n        name: 'test',\n        plugin: '',\n        activationMethod: 'render',\n        matchCommands: ['FT.SEARCH'],\n      },\n    ],\n  }),\n}))\n\nconst mockTelemetry: QueryResultsTelemetry = {\n  onCommandCopied: jest.fn(),\n  onResultCleared: jest.fn(),\n  onResultCollapsed: jest.fn(),\n  onResultExpanded: jest.fn(),\n  onResultViewChanged: jest.fn(),\n  onFullScreenToggled: jest.fn(),\n  onQueryReRun: jest.fn(),\n}\n\nconst renderQueryCardHeaderComponent = (\n  props: Props,\n  telemetry: QueryResultsTelemetry = mockTelemetry,\n) => {\n  return render(\n    <QueryResultsProvider telemetry={telemetry}>\n      <QueryCardHeader {...instance(mockedProps)} {...props} />\n    </QueryResultsProvider>,\n    {\n      store,\n    },\n  )\n}\n\ndescribe('QueryCardHeader', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(\n      renderQueryCardHeaderComponent({ ...instance(mockedProps) }),\n    ).toBeTruthy()\n  })\n\n  it('should render tooltip in milliseconds', async () => {\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      executionTime: 12345678910,\n    })\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('command-execution-time-icon'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('execution-time-tooltip')).toHaveTextContent(\n      '12 345 678.91 msec',\n    )\n  })\n\n  it('should render disabled copy button', async () => {\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      emptyCommand: true,\n    })\n\n    expect(screen.getByTestId('copy-command-btn')).toBeDisabled()\n  })\n\n  it('should call telemetry onCommandCopied after click on copy btn', async () => {\n    const command = 'info'\n\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      query: command,\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('copy-command-btn'))\n    })\n\n    expect(mockTelemetry.onCommandCopied).toHaveBeenCalledWith({\n      command,\n      databaseId: INSTANCE_ID_MOCK,\n    })\n  })\n\n  it('should call telemetry onResultCollapsed when clicking collapse button while open', async () => {\n    const command = 'info'\n    const mockToggleOpen = jest.fn()\n\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      query: command,\n      isOpen: true,\n      toggleOpen: mockToggleOpen,\n    })\n\n    const collapseButton = screen.getByTestId('query-card-open')\n    expect(collapseButton).toBeInTheDocument()\n\n    fireEvent.click(collapseButton)\n    expect(mockToggleOpen).toHaveBeenCalled()\n\n    expect(mockTelemetry.onResultCollapsed).toHaveBeenCalledWith({\n      command,\n      databaseId: INSTANCE_ID_MOCK,\n    })\n  })\n\n  it('should call telemetry onResultExpanded when clicking expand button while closed', async () => {\n    const command = 'info'\n    const mockToggleOpen = jest.fn()\n\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      query: command,\n      isOpen: false,\n      toggleOpen: mockToggleOpen,\n    })\n\n    const collapseButton = screen.getByTestId('query-card-open')\n    expect(collapseButton).toBeInTheDocument()\n\n    fireEvent.click(collapseButton)\n    expect(mockToggleOpen).toHaveBeenCalled()\n\n    expect(mockTelemetry.onResultExpanded).toHaveBeenCalledWith({\n      command,\n      databaseId: INSTANCE_ID_MOCK,\n    })\n  })\n\n  it('should call telemetry onResultCleared when clicking delete button', async () => {\n    const command = 'info'\n    const mockOnQueryDelete = jest.fn()\n\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      query: command,\n      isOpen: true,\n      onQueryDelete: mockOnQueryDelete,\n    })\n\n    const deleteButton = screen.getByTestId('delete-command')\n    expect(deleteButton).toBeInTheDocument()\n\n    fireEvent.click(deleteButton)\n    expect(mockOnQueryDelete).toHaveBeenCalled()\n\n    expect(mockTelemetry.onResultCleared).toHaveBeenCalledWith({\n      command,\n      databaseId: INSTANCE_ID_MOCK,\n    })\n  })\n\n  it('should call telemetry onQueryReRun when clicking re-run button', async () => {\n    const command = 'info'\n    const mockOnQueryReRun = jest.fn()\n\n    renderQueryCardHeaderComponent({\n      ...instance(mockedProps),\n      query: command,\n      isOpen: true,\n      onQueryReRun: mockOnQueryReRun,\n    })\n\n    const reRunButton = screen.getByTestId('re-run-command')\n    expect(reRunButton).toBeInTheDocument()\n\n    fireEvent.click(reRunButton)\n    expect(mockOnQueryReRun).toHaveBeenCalled()\n\n    expect(mockTelemetry.onQueryReRun).toHaveBeenCalledWith({\n      command,\n      databaseId: INSTANCE_ID_MOCK,\n    })\n  })\n\n  it('should not call telemetry callbacks when none are provided', async () => {\n    const command = 'info'\n    const mockOnQueryDelete = jest.fn()\n\n    renderQueryCardHeaderComponent(\n      {\n        ...instance(mockedProps),\n        query: command,\n        isOpen: true,\n        onQueryDelete: mockOnQueryDelete,\n      },\n      {},\n    )\n\n    const deleteButton = screen.getByTestId('delete-command')\n    fireEvent.click(deleteButton)\n\n    expect(mockOnQueryDelete).toHaveBeenCalled()\n    // Should not throw when telemetry callbacks are undefined\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.styles.ts",
    "content": "import styled from 'styled-components'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\n\nexport const ProfileSelect = styled(RiSelect)`\n  border: none;\n  background-color: inherit;\n  //color: var(--iconsDefaultColor);\n  width: 46px;\n  padding: inherit;\n\n  &.profiler {\n    min-width: 50px;\n  }\n\n  &.toggle-view {\n    min-width: 40px;\n  }\n\n  & ~ div {\n    right: 0;\n\n    svg {\n      width: 10px;\n      height: 10px;\n    }\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx",
    "content": "import React, { useContext } from 'react'\nimport cx from 'classnames'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { findIndex, isNumber } from 'lodash'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nimport {\n  ChevronDownIcon,\n  ChevronUpIcon,\n  DeleteIcon,\n  PlayIcon,\n} from 'uiSrc/components/base/icons'\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { Theme } from 'uiSrc/constants'\nimport {\n  getCommandNameFromQuery,\n  getVisualizationsByCommand,\n  isGroupMode,\n  isGroupResults,\n  isRawMode,\n  isSilentMode,\n  isSilentModeWithoutError,\n  truncateMilliseconds,\n  truncateText,\n  urlForAsset,\n} from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { appPluginsSelector } from 'uiSrc/slices/app/plugins'\nimport {\n  getProfileViewTypeOptions,\n  getViewTypeOptions,\n  isCommandAllowedForProfile,\n  ProfileQueryType,\n  WBQueryType,\n} from 'uiSrc/pages/workbench/constants'\nimport { IPluginVisualization } from 'uiSrc/slices/interfaces'\nimport {\n  ResultsMode,\n  ResultsSummary,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces/workbench'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport { FormatedDate, FullScreen, RiTooltip } from 'uiSrc/components'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport QueryCardTooltip from '../QueryCardTooltip'\n\nimport styles from './styles.module.scss'\nimport { useQueryResultsContext } from '../../context/query-results.context'\nimport { ProfileSelect } from './QueryCardHeader.styles'\n\nexport interface Props {\n  query: string\n  isOpen: boolean\n  isFullScreen: boolean\n  createdAt?: Date\n  message?: string\n  activeMode: RunQueryMode\n  mode?: RunQueryMode\n  resultsMode?: ResultsMode\n  activeResultsMode?: ResultsMode\n  summary?: ResultsSummary\n  summaryText?: string\n  selectedValue: string\n  loading?: boolean\n  clearing?: boolean\n  executionTime?: number\n  emptyCommand?: boolean\n  db?: number\n  toggleOpen: () => void\n  toggleFullScreen: () => void\n  setSelectedValue: (type: WBQueryType, value: string) => void\n  onQueryDelete: () => void\n  onQueryReRun: () => void\n  onQueryProfile: (type: ProfileQueryType) => void\n}\n\nconst getExecutionTimeString = (value: number): string => {\n  if (value < 1) {\n    return '0.001 msec'\n  }\n  return `${numberWithSpaces(parseFloat((value / 1000).toFixed(3)))} msec`\n}\n\nconst getTruncatedExecutionTimeString = (value: number): string => {\n  if (value < 1) {\n    return '0.001 msec'\n  }\n\n  return truncateMilliseconds(parseFloat((value / 1000).toFixed(3)))\n}\n\nconst QueryCardHeader = (props: Props) => {\n  const {\n    isOpen,\n    toggleOpen,\n    isFullScreen,\n    toggleFullScreen,\n    query = '',\n    loading,\n    clearing,\n    message,\n    createdAt,\n    mode,\n    resultsMode,\n    summary,\n    activeResultsMode,\n    summaryText,\n    activeMode,\n    selectedValue,\n    executionTime,\n    emptyCommand = false,\n    setSelectedValue,\n    onQueryDelete,\n    onQueryReRun,\n    onQueryProfile,\n    db,\n  } = props\n\n  const { visualizations = [] } = useSelector(appPluginsSelector)\n  const { spec: COMMANDS_SPEC } = useSelector(appRedisCommandsSelector)\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const { theme } = useContext(ThemeContext)\n  const { telemetry } = useQueryResultsContext()\n\n  const eventStop = (event: React.MouseEvent) => {\n    event.preventDefault()\n    event.stopPropagation()\n  }\n\n  const getCommandName = () =>\n    getCommandNameFromQuery(query, COMMANDS_SPEC) ?? ''\n\n  const handleCopy = () => {\n    telemetry.onCommandCopied?.({\n      command: getCommandName(),\n      databaseId: instanceId,\n    })\n  }\n\n  const onDropDownViewClick = (event: React.MouseEvent) => {\n    eventStop(event)\n  }\n\n  const onChangeView = (initValue: string) => {\n    if (selectedValue === initValue) return\n    const currentView = options.find(({ id }) => id === initValue)\n    const previousView = options.find(({ id }) => id === selectedValue)\n    const type = currentView.value\n    setSelectedValue(type as WBQueryType, initValue)\n    telemetry.onResultViewChanged?.({\n      databaseId: instanceId,\n      command: getCommandName(),\n      rawMode: isRawMode(activeMode),\n      group: isGroupMode(activeResultsMode),\n      previousView: previousView?.name,\n      isPreviousViewInternal: !!previousView?.internal,\n      currentView: currentView?.name,\n      isCurrentViewInternal: !!currentView?.internal,\n    })\n  }\n\n  const handleQueryDelete = (event: React.MouseEvent) => {\n    eventStop(event)\n    onQueryDelete()\n\n    telemetry.onResultCleared?.({\n      command: getCommandName(),\n      databaseId: instanceId,\n    })\n  }\n\n  const handleQueryReRun = (event: React.MouseEvent) => {\n    eventStop(event)\n    onQueryReRun()\n\n    telemetry.onQueryReRun?.({\n      command: getCommandName(),\n      databaseId: instanceId,\n    })\n  }\n\n  const handleToggleOpen = () => {\n    if (\n      !isFullScreen &&\n      !isSilentModeWithoutError(resultsMode, summary?.fail)\n    ) {\n      const telemetryParams = {\n        command: getCommandName(),\n        databaseId: instanceId,\n      }\n\n      if (isOpen) {\n        telemetry.onResultCollapsed?.(telemetryParams)\n      } else {\n        telemetry.onResultExpanded?.(telemetryParams)\n      }\n    }\n    toggleOpen()\n  }\n\n  const pluginsOptions = getVisualizationsByCommand(query, visualizations).map(\n    (visualization: IPluginVisualization) => ({\n      id: visualization.uniqId,\n      value: WBQueryType.Plugin,\n      name: `${visualization.id}__${visualization.name}`,\n      text: visualization.name,\n      iconDark:\n        visualization.plugin.internal && visualization.iconDark\n          ? urlForAsset(visualization.plugin.baseUrl, visualization.iconDark)\n          : 'DefaultPluginDarkIcon',\n      iconLight:\n        visualization.plugin.internal && visualization.iconLight\n          ? urlForAsset(visualization.plugin.baseUrl, visualization.iconLight)\n          : 'DefaultPluginLightIcon',\n      internal: visualization.plugin.internal,\n    }),\n  )\n\n  const options: any[] = getViewTypeOptions()\n  options.push(...pluginsOptions)\n  const modifiedOptions = options.map((item) => {\n    const { value, id, text, iconDark, iconLight } = item\n    return {\n      value: id ?? value,\n      label: id ?? value,\n      disabled: false,\n      inputDisplay: (\n        <RiTooltip\n          content={truncateText(text, 500)}\n          position=\"left\"\n          anchorClassName={styles.changeViewWrapper}\n        >\n          <RiIcon\n            type={theme === Theme.Dark ? iconDark : iconLight}\n            data-testid={`view-type-selected-${value}-${id}`}\n          />\n        </RiTooltip>\n      ),\n      dropdownDisplay: (\n        <div className={cx(styles.dropdownOption)}>\n          <RiIcon type={theme === Theme.Dark ? iconDark : iconLight} />\n          <span>{truncateText(text, 20)}</span>\n        </div>\n      ),\n      'data-test-subj': `view-type-option-${value}-${id}`,\n    }\n  })\n\n  const profileOptions = (getProfileViewTypeOptions() as any[]).map((item) => {\n    const { value, id, text } = item\n    return {\n      value: id ?? value,\n      label: id ?? value,\n      inputDisplay: (\n        <div\n          data-test-subj={`profile-type-option-${value}-${id}`}\n          className={cx(styles.dropdownOption, styles.dropdownProfileOption)}\n        >\n          <RiIcon\n            type=\"VisTagCloudIcon\"\n            data-testid={`view-type-selected-${value}-${id}`}\n          />\n        </div>\n      ),\n      dropdownDisplay: (\n        <div\n          data-test-subj={`profile-type-option-${value}-${id}`}\n          className={cx(styles.dropdownOption, styles.dropdownProfileOption)}\n        >\n          <span>{truncateText(text, 20)}</span>\n        </div>\n      ),\n      'data-test-subj': `profile-type-option-${value}-${id}`,\n    }\n  })\n\n  const canCommandProfile = isCommandAllowedForProfile(query)\n\n  const indexForSeparator = findIndex(\n    pluginsOptions,\n    (option) => !option.internal,\n  )\n  if (indexForSeparator > -1) {\n    modifiedOptions.splice(indexForSeparator + 1, 0, {\n      value: '',\n      disabled: true,\n      inputDisplay: <span className={styles.separator} />,\n      label: '',\n      dropdownDisplay: <span />,\n      'data-test-subj': '',\n    })\n  }\n\n  return (\n    <Row\n      onClick={handleToggleOpen}\n      tabIndex={0}\n      onKeyDown={() => {}}\n      className={cx(styles.container, 'query-card-header', {\n        [styles.isOpen]: isOpen,\n        [styles.notExpanded]: isSilentModeWithoutError(\n          resultsMode,\n          summary?.fail,\n        ),\n      })}\n      data-testid=\"query-card-open\"\n      role=\"button\"\n    >\n      <Row align=\"center\" gap=\"l\" full>\n        <FlexItem className={styles.titleWrapper} grow>\n          <div className=\"copy-btn-wrapper\">\n            <ColorText\n              color=\"primary\"\n              className={styles.title}\n              component=\"div\"\n              data-testid=\"query-card-command\"\n            >\n              <QueryCardTooltip\n                query={query}\n                summary={summaryText}\n                db={db}\n                resultsMode={resultsMode}\n              />\n            </ColorText>\n            <CopyButton\n              copy={query || ''}\n              onCopy={handleCopy}\n              aria-label=\"Copy query\"\n              disabled={emptyCommand}\n              withTooltip={false}\n              className={cx('copy-btn', styles.copyBtn)}\n              data-testid=\"copy-command\"\n            />\n          </div>\n        </FlexItem>\n        <FlexItem className={styles.controls}>\n          <Row align=\"center\" justify=\"end\" gap=\"l\">\n            <FlexItem\n              className={styles.time}\n              data-testid=\"command-execution-date-time\"\n            >\n              {!!createdAt && (\n                <ColorText component=\"div\" size=\"S\">\n                  <FormatedDate date={createdAt} />\n                </ColorText>\n              )}\n            </FlexItem>\n            <FlexItem className={styles.summaryTextWrapper}>\n              {!!message && !isOpen && (\n                <ColorText component=\"div\" size=\"S\">\n                  {truncateText(message, 13)}\n                </ColorText>\n              )}\n            </FlexItem>\n            <FlexItem\n              data-testid=\"command-execution-time\"\n              className={styles.executionTime}\n            >\n              {isNumber(executionTime) && (\n                <RiTooltip\n                  title=\"Processing Time\"\n                  content={getExecutionTimeString(executionTime)}\n                  position=\"left\"\n                  anchorClassName={styles.executionTime}\n                  data-testid=\"execution-time-tooltip\"\n                >\n                  <Row align=\"center\" gap=\"s\" grow={false}>\n                    <RiIcon\n                      size=\"M\"\n                      color=\"primary600\"\n                      type=\"UptimeIcon\"\n                      data-testid=\"command-execution-time-icon\"\n                    />\n                    <ColorText\n                      size=\"S\"\n                      color=\"default\"\n                      className={cx(styles.executionTimeValue)}\n                      data-testid=\"command-execution-time-value\"\n                    >\n                      {getTruncatedExecutionTimeString(executionTime)}\n                    </ColorText>\n                  </Row>\n                </RiTooltip>\n              )}\n            </FlexItem>\n            <Row align=\"center\" justify=\"end\" gap=\"s\" grow={false}>\n              <FlexItem\n                className={cx(styles.buttonIcon, styles.viewTypeIcon)}\n                onClick={onDropDownViewClick}\n              >\n                {isOpen && canCommandProfile && !summaryText && (\n                  <ProfileSelect\n                    placeholder={profileOptions[0].inputDisplay}\n                    onChange={(value: ProfileQueryType | string) =>\n                      onQueryProfile(value as ProfileQueryType)\n                    }\n                    className=\"profiler\"\n                    options={profileOptions}\n                    data-testid=\"run-profile-type\"\n                    valueRender={({ option, isOptionValue }) => {\n                      if (isOptionValue) {\n                        return option.dropdownDisplay as JSX.Element\n                      }\n                      return option.inputDisplay as JSX.Element\n                    }}\n                  />\n                )}\n              </FlexItem>\n              <FlexItem\n                className={cx(styles.buttonIcon, styles.viewTypeIcon)}\n                onClick={onDropDownViewClick}\n              >\n                {isOpen && options.length > 1 && !summaryText && (\n                  <ProfileSelect\n                    options={modifiedOptions}\n                    valueRender={({ option, isOptionValue }) => {\n                      if (isOptionValue) {\n                        return option.dropdownDisplay as JSX.Element\n                      }\n                      return option.inputDisplay as JSX.Element\n                    }}\n                    value={selectedValue}\n                    onChange={(value: string) => onChangeView(value)}\n                    className=\"toggle-view\"\n                    data-testid=\"select-view-type\"\n                  />\n                )}\n              </FlexItem>\n              <FlexItem\n                className={styles.buttonIcon}\n                onClick={onDropDownViewClick}\n              >\n                {(isOpen || isFullScreen) && (\n                  <FullScreen\n                    isFullScreen={isFullScreen}\n                    onToggleFullScreen={toggleFullScreen}\n                  />\n                )}\n              </FlexItem>\n              <FlexItem className={styles.buttonIcon}>\n                <RiTooltip content=\"Clear result\" position=\"left\">\n                  <IconButton\n                    disabled={loading || clearing}\n                    icon={DeleteIcon}\n                    aria-label=\"Delete command\"\n                    data-testid=\"delete-command\"\n                    onClick={handleQueryDelete}\n                  />\n                </RiTooltip>\n              </FlexItem>\n              {!isFullScreen && (\n                <FlexItem className={cx(styles.buttonIcon, styles.playIcon)}>\n                  <RiTooltip\n                    content=\"Run again\"\n                    position=\"left\"\n                    anchorClassName={cx(styles.buttonIcon, styles.playIcon)}\n                  >\n                    <IconButton\n                      disabled={emptyCommand}\n                      icon={PlayIcon}\n                      aria-label=\"Re-run command\"\n                      data-testid=\"re-run-command\"\n                      onClick={handleQueryReRun}\n                    />\n                  </RiTooltip>\n                </FlexItem>\n              )}\n              {!isFullScreen && (\n                <FlexItem className={styles.buttonIcon}>\n                  {!isSilentModeWithoutError(resultsMode, summary?.fail) && (\n                    <IconButton\n                      icon={isOpen ? ChevronUpIcon : ChevronDownIcon}\n                      aria-label=\"toggle collapse\"\n                      data-testid=\"toggle-collapse\"\n                    />\n                  )}\n                </FlexItem>\n              )}\n              <FlexItem className={styles.buttonIcon}>\n                {(isRawMode(mode) || isGroupResults(resultsMode)) && (\n                  <RiTooltip\n                    className={styles.tooltip}\n                    anchorClassName={styles.buttonIcon}\n                    content={\n                      <>\n                        {isGroupMode(resultsMode) && (\n                          <ColorText\n                            className={cx(styles.mode)}\n                            data-testid=\"group-mode-tooltip\"\n                          >\n                            <RiIcon type=\"GroupModeIcon\" />\n                          </ColorText>\n                        )}\n                        {isSilentMode(resultsMode) && (\n                          <ColorText\n                            className={cx(styles.mode)}\n                            data-testid=\"silent-mode-tooltip\"\n                          >\n                            <RiIcon type=\"SilentModeIcon\" />\n                          </ColorText>\n                        )}\n                        {isRawMode(mode) && (\n                          <ColorText\n                            className={cx(styles.mode)}\n                            data-testid=\"raw-mode-tooltip\"\n                          >\n                            -r\n                          </ColorText>\n                        )}\n                      </>\n                    }\n                    position=\"bottom\"\n                    data-testid=\"parameters-tooltip\"\n                  >\n                    <RiIcon\n                      color=\"subdued\"\n                      type=\"MoreactionsIcon\"\n                      data-testid=\"parameters-anchor\"\n                    />\n                  </RiTooltip>\n                )}\n              </FlexItem>\n            </Row>\n          </Row>\n        </FlexItem>\n      </Row>\n    </Row>\n  )\n}\n\nexport default QueryCardHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardHeader/index.ts",
    "content": "import QueryCardResult from './QueryCardHeader'\n\nexport default QueryCardResult\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardHeader/styles.module.scss",
    "content": "$breakpoint-s: 850px;\n$breakpoint-m: 1050px;\n$breakpoint-l: 1300px;\n$breakpoint-xl: 1650px;\n\n$marginIcon: 12px;\n\n.container {\n  height: 45px;\n  padding: 0 20px;\n  cursor: pointer;\n\n  :global(.copy-btn) {\n    margin-left: 5px;\n    @media (min-width: $breakpoint-m) {\n      margin-left: 10px;\n    }\n  }\n\n  &.notExpanded {\n    cursor: default;\n  }\n}\n\n\n.title {\n  display: inline-block;\n  font: normal normal normal 13px/17px Graphik, sans-serif !important;\n  letter-spacing: -0.13px;\n  overflow: hidden;\n\n  :global(.euiToolTipAnchor) {\n    display: inline-block;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n    position: relative;\n    max-width: 100%;\n    vertical-align: middle;\n  }\n}\n\n.time {\n  max-width: 134px;\n}\n\n.mode + .mode {\n  margin-left: 18px;\n}\n\n.timeText,\n.summaryText {\n  font: normal normal normal 12px/16px Graphik, sans-serif;\n  letter-spacing: -0.12px;\n  color: var(--euiColorMediumShade) !important;\n  text-align: left;\n}\n\n.summaryTextWrapper {\n  min-width: 86px;\n}\n\n.extraInfo {\n  width: 210px;\n  text-align: right;\n}\n\n.changeViewWrapper {\n  display: flex;\n}\n\n.tooltipIcon {\n  display: flex !important;\n  flex: 1;\n}\n\n.dropdownProfileIcon {\n  padding: inherit !important;\n  :global {\n    .euiSuperSelectControl.euiFormControlLayoutIcons {\n      display: none !important;\n    }\n  }\n}\n\n.dropdownProfileOption {\n  display: inherit !important;\n}\n\n.dropdownProfileItem {\n  :global {\n    .euiContextMenu__icon {\n      display: none !important;\n    }\n  }\n}\n\n.executionTime {\n  min-width: 13px;\n  width: 13px;\n\n  @media (min-width: $breakpoint-m) {\n    min-width: 92px;\n    width: 92px;\n  }\n}\n\n.executionTimeValue {\n  display: none;\n\n  @media (min-width: $breakpoint-m) {\n    display: initial;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    width: 100%;\n    white-space: nowrap;\n    cursor: pointer;\n  }\n}\n\n.alignCenter {\n  align-items: center;\n}\n\n.dropdownOption {\n  display: flex !important;\n  align-items: center;\n  position: relative;\n  padding: 0 0 3px 8px;\n\n  span {\n    font-size: 14px;\n    margin-left: 5px;\n    line-height: 20px;\n    overflow: hidden;\n    max-width: 200px;\n  }\n}\n\n.titleWrapper {\n  justify-content: center;\n  min-height: 24px;\n  overflow: hidden;\n\n  .copyBtn {\n    margin-top: 2px;\n    margin-left: 12px !important;\n  }\n}\n\n.controls {\n  flex-shrink: 0;\n}\n\n.dropdownOptionSeparator:after {\n  content: \"\";\n  display: block;\n  height: 0;\n  width: 100%;\n  border-bottom: 1px solid var(--separatorDropdownColor);\n  opacity: 0.5;\n  position: absolute;\n  bottom: -8.5px;\n}\n\n.changeView:global(.euiSuperSelectControl) {\n  border: none !important;\n  background-color: inherit !important;\n  color: var(--iconsDefaultColor) !important;\n  width: 46px;\n  padding-right: 0;\n\n  & ~ div {\n    right: 0px;\n    svg {\n      width: 10px !important;\n      height: 10px !important;\n    }\n  }\n}\n\n.changeViewItem {\n  overflow-wrap: break-word;\n  :global {\n    .euiContextMenuItem__text {\n      overflow: visible;\n    }\n    .euiContextMenu__icon {\n      margin-right: 0;\n    }\n  }\n  &:global(.euiContextMenuItem-isDisabled) {\n    padding-top: 0;\n    padding-bottom: 0;\n    min-height: 12px !important;\n    cursor: auto !important;\n    &:hover {\n      background: transparent !important;\n    }\n    :global(.euiContextMenu__icon) {\n      height: 0;\n    }\n  }\n}\n\n.separator {\n  height: 1px;\n  width: 100%;\n  background: var(--separatorDropdownColor);\n  display: block;\n  opacity: 0.5;\n}\n\n.buttonIcon {\n  padding: 0 4px;\n  min-width: 32px;\n  position: relative;\n  z-index: 2;\n}\n\n.container :global(.RI-flex-item).viewTypeIcon {\n  width: 54px;\n  min-width: 54px;\n  margin: 0 calc($marginIcon/3) !important;\n}\n\n.container :global(.RI-flex-item).playIcon {\n  margin-right: calc($marginIcon/3) !important;\n}\n\n.tooltipAnchor {\n  width: 16px;\n  margin-left: -4px;\n  cursor: pointer;\n}\n\n:global(.fullscreen) .tooltipAnchor {\n  margin-left: 0;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardTooltip/QueryCardTooltip.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { EMPTY_COMMAND } from 'uiSrc/constants'\nimport { render } from 'uiSrc/utils/test-utils'\nimport QueryCardTooltip, { Props } from './QueryCardTooltip'\n\nconst mockedProps = mock<Props>()\n\ndescribe('QueryCardTooltip', () => {\n  it('should render', () => {\n    expect(render(<QueryCardTooltip {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should show db index', () => {\n    const { queryByTestId } = render(\n      <QueryCardTooltip\n        {...instance(mockedProps)}\n        query={null}\n        summary={null}\n        db={2}\n      />,\n    )\n\n    expect(queryByTestId('query-card-tooltip-anchor')).toHaveTextContent(\n      '[db2]',\n    )\n  })\n\n  it(`should show ${EMPTY_COMMAND} if command=null and summary=`, () => {\n    const { queryByTestId } = render(\n      <QueryCardTooltip\n        {...instance(mockedProps)}\n        query={null}\n        summary={null}\n      />,\n    )\n\n    expect(queryByTestId('query-card-tooltip-anchor')).toHaveTextContent(\n      EMPTY_COMMAND,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardTooltip/QueryCardTooltip.tsx",
    "content": "import React from 'react'\nimport { take } from 'lodash'\nimport cx from 'classnames'\n\nimport { Nullable, getDbIndex, isGroupResults, truncateText } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components'\nimport { EMPTY_COMMAND } from 'uiSrc/constants'\nimport { ResultsMode } from 'uiSrc/slices/interfaces'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  query: Nullable<string>\n  summary?: Nullable<string>\n  maxLinesNumber?: number\n  resultsMode?: ResultsMode\n  db?: number\n}\n\ninterface IQueryLine {\n  index: number\n  value: string\n  isFolding?: boolean\n}\n\nconst QueryCardTooltip = (props: Props) => {\n  const {\n    query = '',\n    maxLinesNumber = 20,\n    summary = '',\n    resultsMode,\n    db,\n  } = props\n  const command = summary || query || EMPTY_COMMAND\n\n  let queryLines: IQueryLine[] = (query || EMPTY_COMMAND)\n    .split('\\n')\n    .map((query: string, i) => ({\n      value: truncateText(query, 497, '...'),\n      index: i,\n    }))\n\n  const isMultilineCommand = queryLines.length > 1\n  if (queryLines.length > maxLinesNumber) {\n    const lastItem = queryLines[queryLines.length - 1]\n    queryLines = take(queryLines, maxLinesNumber - 2)\n    queryLines.push({\n      index: queryLines.length,\n      value: ' ...',\n      isFolding: true,\n    })\n    queryLines.push(lastItem)\n  }\n\n  const contentItems = queryLines.map((item: IQueryLine) => {\n    const { value, index, isFolding } = item\n    const command = `${getDbIndex(db)} ${value}`\n    return !isMultilineCommand ? (\n      <span key={index}>{command}</span>\n    ) : (\n      <pre\n        key={index}\n        className={cx(styles.queryLine, styles.queryMultiLine, {\n          [styles.queryLineFolding]: isFolding,\n        })}\n      >\n        <div className={styles.queryLineNumber}>{`${index + 1}`}</div>\n        <span>{command}</span>\n      </pre>\n    )\n  })\n\n  return (\n    <RiTooltip\n      className={styles.tooltip}\n      anchorClassName={styles.tooltipAnchor}\n      content={contentItems}\n      position=\"bottom\"\n    >\n      <span\n        data-testid=\"query-card-tooltip-anchor\"\n        className={styles.tooltipAnchor}\n      >\n        {`${!isGroupResults(resultsMode) ? getDbIndex(db) : ''} ${command}`.trim()}\n      </span>\n    </RiTooltip>\n  )\n}\n\nexport default QueryCardTooltip\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardTooltip/index.ts",
    "content": "import QueryCardTooltip from './QueryCardTooltip'\n\nexport default QueryCardTooltip\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/QueryCardTooltip/styles.module.scss",
    "content": ".tooltip {\n  @media only screen and (min-width: 768px) {\n    max-width: 400px !important;\n  }\n}\n\n.queryLine {\n  max-width: 100%;\n  text-overflow: ellipsis;\n  overflow: hidden;\n  position: relative;\n}\n\n.queryMultiLine {\n  padding-left: 40px;\n}\n\n.queryLineNumber {\n  position: absolute;\n  display: flex;\n  left: 0;\n  justify-content: flex-end;\n  width: 26px;\n}\n\n.queryLineFolding > span,\n.queryLineNumber {\n  opacity: 0.4;\n}\n\n.tooltipAnchor {\n  cursor: pointer;\n  display: block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/index.ts",
    "content": "import QueryCard from './QueryCard'\n\nexport default QueryCard\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-card/styles.module.scss",
    "content": "$breakpoint-l: 1300px;\n$breakpoint-m: 1050px;\n\n.containerWrapper {\n  min-width: 662px;\n  @media (min-width: $breakpoint-m) {\n    min-width: 762px;\n  }\n  &:nth-of-type(even) {\n    background-color: var(--euiColorEmptyShade) !important;\n  }\n  &:nth-of-type(odd) {\n    background-color: var(--browserTableRowEven) !important;\n  }\n\n  &.isOpen + &.isOpen .container {\n    border-top-width: 0 !important;\n  }\n\n  &.isOpen .container {\n    border-color: var(--browserComponentActive);\n  }\n\n  &:global(.fullscreen) {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    z-index: 110;\n    background-color: var(--euiColorEmptyShade);\n\n    :global(.queryResultsContainer) {\n      max-height: calc(100% - 45px);\n    }\n\n    :global(.queryResultsContainer.pluginStyles) {\n      max-height: calc(100vh - 45px);\n    }\n\n    .container {\n      border-color: var(--tableLightestBorderColor);\n      display: flex;\n      flex-direction: column;\n      height: 100%;\n    }\n\n    &.isOpen .container {\n      border: none !important;\n    }\n  }\n}\n\n.container {\n  border: 1px solid var(--tableDarkestBorderColor);\n}\n\n:global(.query-card-output-response-success) {\n  @include eui.scrollBar;\n  display: block;\n  max-height: 210px;\n  overflow: auto;\n  white-space: pre-wrap;\n  word-break: break-all;\n\n  color: var(--cliOutputResponseColor) !important;\n}\n\n:global(.fullscreen .query-card-output-response-success) {\n  height: 100%;\n  max-height: calc(100vh - 65px);\n  display: flex;\n}\n\n:global(.query-card-output-response-fail) {\n  color: var(--cliOutputResponseFailColor) !important;\n\n  span {\n    vertical-align: text-top;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-lite-actions/QueryActions.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport QueryLiteActions from './QueryLiteActions'\n\ndescribe('QueryLiteActions', () => {\n  const onSubmit = jest.fn()\n  const onClear = jest.fn()\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render both buttons', () => {\n    render(<QueryLiteActions onSubmit={onSubmit} onClear={onClear} />)\n\n    expect(screen.getByTestId('btn-submit')).toBeInTheDocument()\n    expect(screen.getByTestId('btn-clear')).toBeInTheDocument()\n  })\n\n  it('should call onSubmit when Run button is clicked', () => {\n    render(<QueryLiteActions onSubmit={onSubmit} onClear={onClear} />)\n\n    fireEvent.click(screen.getByTestId('btn-submit'))\n    expect(onSubmit).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onClear when Clear button is clicked', () => {\n    render(<QueryLiteActions onSubmit={onSubmit} onClear={onClear} />)\n\n    fireEvent.click(screen.getByTestId('btn-clear'))\n    expect(onClear).toHaveBeenCalledTimes(1)\n  })\n\n  it('should disable buttons and show loading tooltip when isLoading is true', () => {\n    render(<QueryLiteActions onSubmit={onSubmit} onClear={onClear} isLoading />)\n\n    const submitBtn = screen.getByTestId('btn-submit') as HTMLButtonElement\n    const clearBtn = screen.getByTestId('btn-clear') as HTMLButtonElement\n\n    expect(submitBtn).toBeDisabled()\n    expect(clearBtn).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-lite-actions/QueryLiteActions.tsx",
    "content": "import React from 'react'\n\nimport { KEYBOARD_SHORTCUTS } from 'uiSrc/constants'\nimport { KeyboardShortcut, RiTooltip } from 'uiSrc/components'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport RunButton from 'uiSrc/components/query/components/RunButton'\n\nexport interface Props {\n  onSubmit: () => void\n  onClear: () => void\n  isLoading?: boolean\n}\n\nconst QueryLiteActions = (props: Props) => {\n  const { isLoading, onSubmit, onClear } = props\n  const KeyBoardTooltipContent = KEYBOARD_SHORTCUTS?.workbench?.runQuery && (\n    <>\n      <Text size=\"s\">{KEYBOARD_SHORTCUTS.workbench.runQuery?.label}:</Text>\n      <Spacer size=\"s\" />\n      <KeyboardShortcut\n        separator={KEYBOARD_SHORTCUTS?._separator}\n        items={KEYBOARD_SHORTCUTS.workbench.runQuery.keys}\n      />\n    </>\n  )\n\n  return (\n    <>\n      <RiTooltip\n        position=\"right\"\n        content={\n          isLoading\n            ? 'Please wait while the commands are being executed…'\n            : 'Clear query'\n        }\n        data-testid=\"clear-query-tooltip\"\n      >\n        <EmptyButton\n          onClick={onClear}\n          loading={isLoading}\n          disabled={isLoading}\n          aria-label=\"clear\"\n          data-testid=\"btn-clear\"\n        >\n          Clear\n        </EmptyButton>\n      </RiTooltip>\n\n      <RiTooltip\n        position=\"left\"\n        content={\n          isLoading\n            ? 'Please wait while the commands are being executed…'\n            : KeyBoardTooltipContent\n        }\n        data-testid=\"run-query-tooltip\"\n      >\n        <RunButton onSubmit={onSubmit} isLoading={isLoading} />\n      </RiTooltip>\n    </>\n  )\n}\n\nexport default QueryLiteActions\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-lite-actions/index.ts",
    "content": "import QueryLiteActions from './QueryLiteActions'\n\nexport default QueryLiteActions\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-results/QueryResults.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { RunQueryMode } from 'uiSrc/slices/interfaces/workbench'\nimport { commandExecutionUIFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\nimport { QueryResultsProvider } from '../context/query-results.context'\nimport QueryResults, { QueryResultsProps } from './QueryResults'\n\nconst mockedProps = mock<QueryResultsProps>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [],\n  }),\n}))\n\nconst mockItems = [\n  commandExecutionUIFactory.build({\n    id: '1',\n    command: 'SET key value',\n    isOpen: false,\n  }),\n  commandExecutionUIFactory.build({\n    id: '2',\n    command: 'GET key',\n    isOpen: true,\n  }),\n]\n\nconst renderQueryResults = (props: Partial<QueryResultsProps> = {}) => {\n  const defaultProps: QueryResultsProps = {\n    ...instance(mockedProps),\n    isResultsLoaded: true,\n    items: [],\n    clearing: false,\n    processing: false,\n    activeMode: RunQueryMode.ASCII,\n    scrollDivRef: React.createRef(),\n    onQueryReRun: jest.fn(),\n    onQueryDelete: jest.fn(),\n    onAllQueriesDelete: jest.fn(),\n    onQueryProfile: jest.fn(),\n    ...props,\n  }\n\n  return render(\n    <QueryResultsProvider telemetry={{}}>\n      <QueryResults {...defaultProps} />\n    </QueryResultsProvider>,\n    {\n      store,\n    },\n  )\n}\n\ndescribe('QueryResults', () => {\n  it('should render', () => {\n    expect(renderQueryResults()).toBeTruthy()\n  })\n\n  it('should render query-results container', () => {\n    renderQueryResults()\n    expect(screen.getByTestId('query-results')).toBeInTheDocument()\n  })\n\n  it('should render progress bar when results are not loaded', () => {\n    renderQueryResults({ isResultsLoaded: false })\n    expect(screen.getByTestId('progress-results-history')).toBeInTheDocument()\n  })\n\n  it('should not render progress bar when results are loaded', () => {\n    renderQueryResults({ isResultsLoaded: true })\n    expect(\n      screen.queryByTestId('progress-results-history'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render clear results button when items exist', () => {\n    renderQueryResults({ items: mockItems })\n    expect(screen.getByTestId('clear-history-btn')).toBeInTheDocument()\n  })\n\n  it('should not render clear results button when no items', () => {\n    renderQueryResults({ items: [] })\n    expect(screen.queryByTestId('clear-history-btn')).not.toBeInTheDocument()\n  })\n\n  it('should call onAllQueriesDelete when clear results button is clicked', () => {\n    const onAllQueriesDelete = jest.fn()\n    renderQueryResults({ items: mockItems, onAllQueriesDelete })\n\n    fireEvent.click(screen.getByTestId('clear-history-btn'))\n    expect(onAllQueriesDelete).toHaveBeenCalled()\n  })\n\n  it('should disable clear results button when clearing', () => {\n    renderQueryResults({ items: mockItems, clearing: true })\n    expect(screen.getByTestId('clear-history-btn')).toBeDisabled()\n  })\n\n  it('should disable clear results button when processing', () => {\n    renderQueryResults({ items: mockItems, processing: true })\n    expect(screen.getByTestId('clear-history-btn')).toBeDisabled()\n  })\n\n  it('should render query cards for each item', () => {\n    renderQueryResults({ items: mockItems })\n    expect(screen.getByTestId('query-card-container-1')).toBeInTheDocument()\n    expect(screen.getByTestId('query-card-container-2')).toBeInTheDocument()\n  })\n\n  it('should render no results placeholder when loaded with no items', () => {\n    renderQueryResults({\n      isResultsLoaded: true,\n      items: [],\n      noResultsPlaceholder: <div data-testid=\"no-results\">No Results</div>,\n    })\n\n    expect(screen.getByTestId('no-results')).toBeInTheDocument()\n  })\n\n  it('should not render no results placeholder when items exist', () => {\n    renderQueryResults({\n      isResultsLoaded: true,\n      items: mockItems,\n      noResultsPlaceholder: <div data-testid=\"no-results\">No Results</div>,\n    })\n\n    expect(screen.queryByTestId('no-results')).not.toBeInTheDocument()\n  })\n\n  it('should not render no results placeholder when results are not loaded', () => {\n    renderQueryResults({\n      isResultsLoaded: false,\n      items: [],\n      noResultsPlaceholder: <div data-testid=\"no-results\">No Results</div>,\n    })\n\n    expect(screen.queryByTestId('no-results')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-results/QueryResults.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row, FlexItem } from 'uiSrc/components/base/layout/flex'\n\n/* TODO: use theme when it supports theme.semantic.core.radius */\n// to replace var(--border-radius-medium)\nexport const Wrapper = styled(Col)`\n  flex: 1;\n  height: 100%;\n  width: 100%;\n  background-color: ${({ theme }) =>\n    theme.semantic?.color.background.neutral100};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: var(--border-radius-medium);\n  overflow: hidden;\n\n  position: relative;\n`\n\nexport const Container = styled(FlexItem)`\n  width: 100%;\n  overflow: auto;\n  color: ${({ theme }) => theme.semantic.color.text.neutral700};\n`\n\nexport const Header = styled(Row)`\n  height: 42px;\n  padding: 0 ${({ theme }) => theme.core.space.space150};\n\n  flex-shrink: 0;\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-results/QueryResults.tsx",
    "content": "import React from 'react'\n\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport { ProfileQueryType } from 'uiSrc/pages/workbench/constants'\nimport { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils/profile'\nimport { Nullable } from 'uiSrc/utils'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench'\n\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport QueryCard from '../query-card'\n\nimport * as S from './QueryResults.styles'\n\nexport interface QueryResultsProps {\n  isResultsLoaded: boolean\n  items: CommandExecutionUI[]\n  clearing: boolean\n  processing: boolean\n  activeMode: RunQueryMode\n  activeResultsMode?: ResultsMode\n  scrollDivRef: React.Ref<HTMLDivElement>\n  noResultsPlaceholder?: React.ReactNode\n  onToggleOpen?: (id: string, isOpen: boolean) => void\n  onQueryReRun: (\n    query: string,\n    commandId?: Nullable<string>,\n    executeParams?: CodeButtonParams,\n  ) => void\n  onQueryDelete: (commandId: string) => void\n  onAllQueriesDelete: () => void\n  onQueryProfile: (\n    query: string,\n    commandId?: Nullable<string>,\n    executeParams?: CodeButtonParams,\n  ) => void\n}\n\nconst QueryResults = (props: QueryResultsProps) => {\n  const {\n    isResultsLoaded,\n    items = [],\n    clearing,\n    processing,\n    activeMode,\n    activeResultsMode,\n    noResultsPlaceholder,\n    onToggleOpen,\n    onQueryReRun,\n    onQueryProfile,\n    onQueryDelete,\n    onAllQueriesDelete,\n    scrollDivRef,\n  } = props\n\n  const handleQueryProfile = (\n    profileType: ProfileQueryType,\n    commandExecution: {\n      command: string\n      mode?: RunQueryMode\n      resultsMode?: ResultsMode\n    },\n  ) => {\n    const { command, mode, resultsMode } = commandExecution\n    const profileQuery = generateProfileQueryForCommand(command, profileType)\n    if (profileQuery) {\n      onQueryProfile(profileQuery, null, {\n        mode,\n        results: resultsMode,\n        clearEditor: false,\n      })\n    }\n  }\n\n  return (\n    <S.Wrapper data-testid=\"query-results\">\n      {!isResultsLoaded && (\n        <ProgressBarLoader\n          color=\"primary\"\n          data-testid=\"progress-results-history\"\n        />\n      )}\n      {!!items?.length && (\n        <S.Header align=\"center\" justify=\"end\" grow={false}>\n          <EmptyButton\n            size=\"small\"\n            icon={DeleteIcon}\n            onClick={() => onAllQueriesDelete?.()}\n            disabled={clearing || processing}\n            data-testid=\"clear-history-btn\"\n          >\n            Clear Results\n          </EmptyButton>\n        </S.Header>\n      )}\n      <S.Container grow>\n        <div ref={scrollDivRef} />\n        {items?.length\n          ? items.map(\n              ({\n                command = '',\n                isOpen = false,\n                result = undefined,\n                summary = undefined,\n                id = '',\n                loading,\n                createdAt,\n                mode,\n                resultsMode,\n                emptyCommand,\n                isNotStored,\n                executionTime,\n                db,\n              }) => (\n                <QueryCard\n                  id={id}\n                  key={id}\n                  isOpen={isOpen}\n                  result={result}\n                  summary={summary}\n                  clearing={clearing}\n                  loading={loading}\n                  command={command}\n                  createdAt={createdAt}\n                  activeMode={activeMode}\n                  emptyCommand={emptyCommand}\n                  isNotStored={isNotStored}\n                  executionTime={executionTime}\n                  mode={mode}\n                  activeResultsMode={activeResultsMode}\n                  resultsMode={resultsMode}\n                  db={db}\n                  onToggleOpen={onToggleOpen}\n                  onQueryProfile={(profileType) =>\n                    handleQueryProfile(profileType, {\n                      command,\n                      mode,\n                      resultsMode,\n                    })\n                  }\n                  onQueryReRun={() =>\n                    onQueryReRun(command, null, {\n                      mode,\n                      results: resultsMode,\n                      clearEditor: false,\n                    })\n                  }\n                  onQueryDelete={() => onQueryDelete(id)}\n                  data-testid={`query-card-${id}`}\n                />\n              ),\n            )\n          : null}\n        {isResultsLoaded && !items.length && (noResultsPlaceholder ?? null)}\n      </S.Container>\n    </S.Wrapper>\n  )\n}\n\nexport default React.memo(QueryResults)\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-results/index.ts",
    "content": "import QueryResults from './QueryResults'\n\nexport { QueryResults }\nexport type { QueryResultsProps } from './QueryResults'\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { findTutorialPath } from 'uiSrc/utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { TutorialsIds } from 'uiSrc/constants'\nimport QueryTutorials from './QueryTutorials'\n\nconst mockedTutorials = [\n  {\n    id: TutorialsIds.IntroToSearch,\n    title: 'Intro to search',\n  },\n  {\n    id: TutorialsIds.BasicRedisUseCases,\n    title: 'Basic use cases',\n  },\n  {\n    id: TutorialsIds.IntroVectorSearch,\n    title: 'Intro to vector search',\n  },\n]\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  findTutorialPath: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('QueryTutorial', () => {\n  it('should render', () => {\n    expect(\n      render(<QueryTutorials tutorials={mockedTutorials} source=\"source\" />),\n    ).toBeTruthy()\n  })\n\n  it('should call proper history push after click on guide with tutorial', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    render(<QueryTutorials tutorials={mockedTutorials} source=\"source\" />)\n\n    fireEvent.click(screen.getByTestId('query-tutorials-link_sq-intro'))\n\n    expect(pushMock).toHaveBeenCalledWith({\n      search: 'path=tutorials/path',\n    })\n  })\n\n  it('should call proper telemetry event after click on guide', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    render(<QueryTutorials tutorials={mockedTutorials} source=\"source\" />)\n\n    fireEvent.click(screen.getByTestId('query-tutorials-link_sq-intro'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        path: 'path',\n        databaseId: 'instanceId',\n        source: 'source',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx",
    "content": "import React from 'react'\n\nimport { useDispatch } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport styled from 'styled-components'\nimport { findTutorialPath } from 'uiSrc/utils'\nimport { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels'\nimport { Text } from 'uiSrc/components/base/text'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  tutorials: Array<{\n    id: string\n    title: string\n  }>\n  source: string\n}\n\nconst QueryTutorialsButton = styled(EmptyButton)`\n  padding: 4px 8px;\n  background-color: var(--browserTableRowEven);\n\n  border-radius: 4px;\n  border: 1px solid var(--separatorColor);\n\n  color: var(--htmlColor) !important;\n  font-size: 12px;\n\n  &:not(:first-of-type) {\n    margin-left: 8px;\n  }\n\n  &:hover,\n  &:focus {\n    color: var(--htmlColor);\n    text-decoration: underline !important;\n    outline: none !important;\n    animation: none !important;\n  }\n`\n\nconst QueryTutorials = ({ tutorials, source }: Props) => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const handleClickTutorial = (id: string) => {\n    const tutorialPath = findTutorialPath({ id })\n    dispatch(openTutorialByPath(tutorialPath, history, true))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        path: tutorialPath,\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        source,\n      },\n    })\n  }\n\n  return (\n    <div className={styles.container}>\n      <Text className={styles.title}>Tutorials:</Text>\n      {tutorials.map(({ id, title }) => (\n        <QueryTutorialsButton\n          role=\"button\"\n          key={id}\n          className={styles.tutorialLink}\n          onClick={() => handleClickTutorial(id)}\n          data-testid={`query-tutorials-link_${id}`}\n        >\n          {title}\n        </QueryTutorialsButton>\n      ))}\n    </div>\n  )\n}\n\nexport default QueryTutorials\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-tutorials/index.ts",
    "content": "import QueryTutorials from './QueryTutorials'\n\nexport default QueryTutorials\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query-tutorials/styles.module.scss",
    "content": ".container {\n  display: flex;\n  align-items: center;\n\n  .title {\n    margin-right: 8px;\n  }\n\n  .tutorialLink {\n    padding: 4px 8px;\n\n    background-color: var(--browserTableRowEven);\n\n    border-radius: 4px;\n    border: 1px solid var(--separatorColor);\n\n    color: var(--htmlColor) !important;\n    text-decoration: none !important;\n    font-size: 12px;\n\n    &:not(:first-of-type) {\n      margin-left: 8px;\n    }\n\n    &:global(.euiLink) {\n      &:hover, &:focus {\n        color: var(--htmlColor);\n        text-decoration: underline !important;\n        outline: none !important;\n        animation: none !important;\n      }\n    }\n  }\n}\n\n@include global.insights-open(1280px) {\n  .title {\n    display: none\n  }\n}\n\n@include global.insights-open(1024px) {\n  .tutorialLink:last-of-type {\n    display: none;\n  }\n}\n\n\n"
  },
  {
    "path": "redisinsight/ui/src/components/query/query.styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const LoadingContainer = styled(FlexItem).attrs({\n  grow: true,\n  padding: 6,\n})`\n  align-items: center;\n  justify-content: center;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/range-filter/RangeFilter.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport RangeFilter, { Props } from './RangeFilter'\n\nconst mockedProps = mock<Props>()\n\nconst startRangeTestId = 'range-start-input'\nconst endRangeTestId = 'range-end-input'\nconst resetBtnTestId = 'range-filter-btn'\n\ndescribe('RangeFilter', () => {\n  it('should render', () => {\n    expect(render(<RangeFilter {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call handleChangeStart onChange start range thumb', () => {\n    const handleChangeStart = jest.fn()\n    render(\n      <RangeFilter\n        {...instance(mockedProps)}\n        handleChangeStart={handleChangeStart}\n        start={1}\n        end={1000}\n      />,\n    )\n    const startRangeInput = screen.getByTestId(startRangeTestId)\n\n    fireEvent.mouseUp(startRangeInput, { target: { value: 123 } })\n    expect(handleChangeStart).toBeCalledTimes(1)\n  })\n\n  it('should call handleChangeEnd onChange end range thumb', () => {\n    const handleChangeEnd = jest.fn()\n    render(\n      <RangeFilter\n        {...instance(mockedProps)}\n        handleChangeEnd={handleChangeEnd}\n        start={1}\n        end={100}\n      />,\n    )\n    const endRangeInput = screen.getByTestId(endRangeTestId)\n\n    fireEvent.mouseUp(endRangeInput, { target: { value: 15 } })\n    expect(handleChangeEnd).toBeCalledTimes(1)\n  })\n  it('should call handleResetFilter onClick reset button', () => {\n    const handleResetFilter = jest.fn()\n\n    render(\n      <RangeFilter\n        {...instance(mockedProps)}\n        handleResetFilter={handleResetFilter}\n        start={1}\n        end={100}\n        min={1}\n        max={120}\n      />,\n    )\n    const resetBtn = screen.getByTestId(resetBtnTestId)\n\n    fireEvent.click(resetBtn)\n\n    expect(handleResetFilter).toBeCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/range-filter/RangeFilter.tsx",
    "content": "import React, { useCallback, useState, useEffect, useRef } from 'react'\nimport cx from 'classnames'\nimport styled from 'styled-components'\n\nimport { FormatedDate } from '../formated-date'\nimport styles from './styles.module.scss'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nconst SliderRange = styled.div<\n  React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>\n>`\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.primary400};\n  height: 1px;\n  z-index: 2;\n  transform: translateY(2px);\n\n  &:before {\n    content: '';\n    width: 1px;\n    height: 6px;\n    position: absolute;\n    top: -5px;\n    left: -1px;\n    background-color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.background.primary400};\n  }\n\n  &:after {\n    content: '';\n    width: 1px;\n    height: 6px;\n    position: absolute;\n    right: -1px;\n    background-color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.background.primary400};\n  }\n`\n\nconst buttonString = 'Reset Filter'\n\nexport interface Props {\n  max: number\n  min: number\n  start: number\n  end: number\n  disabled?: boolean\n  handleChangeStart: (value: number, shouldSentEventTelemetry: boolean) => void\n  handleChangeEnd: (value: number, shouldSentEventTelemetry: boolean) => void\n  handleUpdateRangeMax: (value: number) => void\n  handleUpdateRangeMin: (value: number) => void\n  handleResetFilter: () => void\n}\n\nfunction usePrevious(value: any) {\n  const ref = useRef()\n  useEffect(() => {\n    ref.current = value\n  })\n  return ref.current\n}\n\nconst RangeFilter = (props: Props) => {\n  const {\n    max,\n    min,\n    start,\n    end,\n    disabled = false,\n    handleChangeStart,\n    handleChangeEnd,\n    handleUpdateRangeMax,\n    handleUpdateRangeMin,\n    handleResetFilter,\n  } = props\n\n  const [startVal, setStartVal] = useState(start)\n  const [endVal, setEndVal] = useState(end)\n\n  const getPercent = useCallback(\n    (value) => Math.round(((value - min) / (max - min)) * 100),\n    [min, max],\n  )\n\n  const minValRef = useRef<HTMLInputElement>(null)\n  const maxValRef = useRef<HTMLInputElement>(null)\n  const range = useRef<HTMLInputElement>(null)\n\n  const prevValue = usePrevious({ max, min }) ?? { max: 0, min: 0 }\n\n  const onChangeStart = useCallback(\n    ({ target: { value } }) => {\n      const newValue = Math.min(+value, endVal - 1)\n      setStartVal(newValue)\n    },\n    [endVal],\n  )\n\n  const onMouseUpStart = useCallback(({ target: { value } }) => {\n    handleChangeStart(value, true)\n  }, [])\n\n  const onMouseUpEnd = useCallback(({ target: { value } }) => {\n    handleChangeEnd(value, true)\n  }, [])\n\n  const onChangeEnd = useCallback(\n    ({ target: { value } }) => {\n      const newValue = Math.max(+value, startVal + 1)\n      setEndVal(newValue)\n    },\n    [startVal],\n  )\n\n  useEffect(() => {\n    if (maxValRef.current) {\n      const minPercent = getPercent(startVal)\n      const maxPercent = getPercent(+maxValRef.current.value)\n\n      if (range.current) {\n        range.current.style.left = `${minPercent}%`\n        range.current.style.width = `${maxPercent - minPercent}%`\n      }\n    }\n  }, [startVal, getPercent])\n\n  useEffect(() => {\n    if (minValRef.current) {\n      const minPercent = getPercent(+minValRef.current.value)\n      const maxPercent = getPercent(endVal)\n\n      if (range.current) {\n        range.current.style.width = `${maxPercent - minPercent}%`\n      }\n    }\n  }, [endVal, getPercent])\n\n  useEffect(() => {\n    setStartVal(start)\n  }, [start])\n\n  useEffect(() => {\n    setEndVal(end)\n  }, [end])\n\n  useEffect(() => {\n    if (prevValue.max !== max && end === prevValue.max) {\n      handleUpdateRangeMax(max)\n    }\n    if (prevValue.min !== min && start === prevValue.min) {\n      handleUpdateRangeMin(min)\n    }\n  }, [prevValue])\n\n  if (start === 0 && max !== 0 && end === 0 && min !== 0) {\n    return (\n      <div data-testid=\"mock-blank-range\" className={styles.rangeWrapper}>\n        <div className={cx(styles.sliderTrack, styles.mockRange)} />\n      </div>\n    )\n  }\n\n  if (start === end) {\n    return (\n      <div data-testid=\"mock-fill-range\" className={styles.rangeWrapper}>\n        <SliderRange className={cx(styles.sliderRange, styles.mockRange)}>\n          <div\n            className={styles.sliderLeftValue}\n            data-testid=\"range-left-timestamp\"\n          >\n            <FormatedDate date={start?.toString()} />\n          </div>\n          <div\n            className={styles.sliderRightValue}\n            data-testid=\"range-right-timestamp\"\n          >\n            <FormatedDate date={end?.toString()} />\n          </div>\n        </SliderRange>\n      </div>\n    )\n  }\n\n  return (\n    <>\n      <div className={styles.rangeWrapper} data-testid=\"range-bar\">\n        <input\n          type=\"range\"\n          min={min}\n          max={max}\n          value={startVal}\n          ref={minValRef}\n          disabled={disabled}\n          onChange={onChangeStart}\n          onMouseUp={onMouseUpStart}\n          className={cx(styles.thumb, styles.thumbZindex3)}\n          data-testid=\"range-start-input\"\n        />\n        <input\n          type=\"range\"\n          min={min}\n          max={max}\n          value={endVal}\n          ref={maxValRef}\n          disabled={disabled}\n          onChange={onChangeEnd}\n          onMouseUp={onMouseUpEnd}\n          className={cx(styles.thumb, styles.thumbZindex4)}\n          data-testid=\"range-end-input\"\n        />\n        <div className={styles.slider}>\n          <div className={styles.sliderTrack} />\n          <SliderRange\n            ref={range}\n            className={cx(styles.sliderRange, {\n              [styles.leftPosition]: max - startVal < (max - min) / 2,\n              [styles.disabled]: disabled,\n            })}\n          >\n            <div\n              className={cx(styles.sliderLeftValue, {\n                [styles.leftPosition]: max - startVal < (max - min) / 2,\n                [styles.disabled]: disabled,\n              })}\n            >\n              <FormatedDate date={startVal?.toString()} />\n            </div>\n            <div\n              className={cx(styles.sliderRightValue, {\n                [styles.rightPosition]: max - endVal > (max - min) / 2,\n                [styles.disabled]: disabled,\n              })}\n            >\n              <FormatedDate date={endVal?.toString()} />\n            </div>\n          </SliderRange>\n        </div>\n      </div>\n      {(start !== min || end !== max) && (\n        <button\n          data-testid=\"range-filter-btn\"\n          className={styles.resetButton}\n          type=\"button\"\n          onClick={handleResetFilter}\n        >\n          {buttonString}\n        </button>\n      )}\n    </>\n  )\n}\n\nexport default RangeFilter\n"
  },
  {
    "path": "redisinsight/ui/src/components/range-filter/index.ts",
    "content": "import RangeFilter from './RangeFilter'\n\nexport default RangeFilter\n"
  },
  {
    "path": "redisinsight/ui/src/components/range-filter/styles.module.scss",
    "content": ".rangeWrapper {\n  margin: 30px 30px 26px;\n  padding: 12px 0;\n}\n\n.resetButton {\n  position: absolute;\n  right: 30px;\n  top: 80px;\n  z-index: 10;\n  text-decoration: underline;\n  color: var(--euiTextSubduedColor);\n  &:hover,\n  &:focus {\n    color: var(--euiTextColor);\n  }\n  font: normal normal 500 13px/18px Graphik, sans-serif;\n}\n\n.slider {\n  position: relative;\n  width: 100%;\n}\n\n.sliderTrack,\n.sliderRange,\n.sliderLeftValue,\n.sliderRightValue {\n  position: absolute;\n}\n\n.sliderTrack {\n  background-color: var(--separatorColor);\n  width: 100%;\n  height: 1px;\n  margin-top: 2px;\n  z-index: 1;\n}\n\n.rangeWrapper:hover .sliderRange:not(.disabled) {\n  height: 5px;\n  transform: translateY(0px);\n\n  &:before {\n    width: 2px;\n    height: 12px;\n    top: -7px;\n  }\n\n  &:after {\n    width: 2px;\n    height: 12px;\n  }\n}\n\n.sliderLeftValue,\n.sliderRightValue {\n  width: max-content;\n  color: var(--euiColorMediumShade);\n  font: normal normal normal 12px/18px Graphik, sans-serif;\n  margin-top: 8px;\n}\n\n.sliderLeftValue {\n  left: 0;\n  margin-top: -25px;\n}\n\n.sliderLeftValue.leftPosition {\n  transform: translateX(-100%);\n}\n\n.sliderRightValue.rightPosition {\n  transform: translateX(100%);\n}\n\n.sliderRightValue {\n  right: -4px;\n}\n\n.mockRange {\n  left: 30px;\n  width: calc(100% - 56px);\n}\n\n.thumb,\n.thumb::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  -webkit-tap-highlight-color: transparent;\n}\n\n.thumb {\n  pointer-events: none;\n  position: absolute;\n  height: 0;\n  width: calc(100% - 60px);\n  outline: none;\n}\n\n.thumbZindex3 {\n  z-index: 3;\n}\n\n.thumbZindex4 {\n  z-index: 4;\n}\n\n.thumb::-moz-range-thumb {\n  width: 24px;\n  height: 24px;\n  border: none;\n  border-radius: 0;\n  cursor: ew-resize;\n  margin-top: 4px;\n  pointer-events: all;\n  position: relative;\n  background: transparent;\n}\n\n.thumb::-moz-range-thumb:disabled {\n  cursor: auto;\n}\n\n.thumbZindex3::-moz-range-thumb {\n  transform: translate(-18px, -4px);\n}\n\n.thumbZindex4::-moz-range-thumb {\n  transform: translate(-20px, 8px) rotate(180deg);\n}\n\ninput[type='range']::-webkit-slider-thumb {\n  width: 24px;\n  height: 24px;\n  border: none;\n  border-radius: 0;\n  cursor: ew-resize;\n  margin-top: 4px;\n  pointer-events: all;\n  position: relative;\n  background: transparent;\n}\n\ninput[type='range'][disabled]::-webkit-slider-thumb {\n  cursor: auto;\n}\n\ninput[type='range']:first-child::-webkit-slider-thumb {\n  transform: translate(-18px, -4px);\n}\n\ninput[type='range']:last-of-type::-webkit-slider-thumb {\n  transform: translate(-20px, 8px) rotate(180deg);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport reactRouterDom from 'react-router-dom'\n\nimport {\n  cleanup,\n  fireEvent,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport RdiInstanceHeader from './RdiInstanceHeader'\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    name: 'name',\n  }),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('RdiInstanceHeader', () => {\n  it('should render', () => {\n    expect(render(<RdiInstanceHeader />)).toBeTruthy()\n  })\n\n  it('should call history push with proper path', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<RdiInstanceHeader />)\n\n    fireEvent.click(screen.getByTestId('my-rdi-instances-btn'))\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('/integrate')\n  })\n\n  it('should render proper instance name', () => {\n    expect(render(<RdiInstanceHeader />)).toBeTruthy()\n\n    expect(screen.getByText('name')).toBeInTheDocument()\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: true },\n    )\n\n    render(<RdiInstanceHeader />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('o-auth-user-profile-rdi')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: false },\n    )\n\n    render(<RdiInstanceHeader />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByTestId('o-auth-user-profile-rdi'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/rdi-instance-header/RdiInstanceHeader.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport { CopilotTrigger, InsightsTrigger } from 'uiSrc/components/triggers'\nimport {\n  FeatureFlagComponent,\n  OAuthUserProfile,\n  RiTooltip,\n} from 'uiSrc/components'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { connectedInstanceSelector } from 'uiSrc/slices/rdi/instances'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { isAnyFeatureEnabled } from 'uiSrc/utils/features'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport InstancesNavigationPopover from '../instance-header/components/instances-navigation-popover'\nimport styles from './styles.module.scss'\n\nconst RdiInstanceHeader = () => {\n  const { name = '' } = useSelector(connectedInstanceSelector)\n  const {\n    [FeatureFlags.databaseChat]: databaseChatFeature,\n    [FeatureFlags.documentationChat]: documentationChatFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n  const isAnyChatAvailable = isAnyFeatureEnabled([\n    databaseChatFeature,\n    documentationChatFeature,\n  ])\n  const history = useHistory()\n\n  const goHome = () => {\n    history.push(Pages.rdi)\n  }\n\n  return (\n    <Row className={styles.container} align=\"center\">\n      <FlexItem style={{ overflow: 'hidden' }} grow>\n        <div\n          className={styles.breadcrumbsContainer}\n          data-testid=\"breadcrumbs-container\"\n        >\n          <div>\n            <RiTooltip position=\"bottom\" content=\"My RDI instances\">\n              <Link\n                color=\"subdued\"\n                underline\n                variant=\"inline\"\n                aria-label=\"My RDI instances\"\n                data-testid=\"my-rdi-instances-btn\"\n                onClick={goHome}\n              >\n                Data integration\n              </Link>\n            </RiTooltip>\n          </div>\n          <div style={{ flex: 1, overflow: 'hidden' }}>\n            <div style={{ maxWidth: '100%' }}>\n              <Row align=\"center\">\n                <FlexItem>\n                  <Text className={styles.divider}>/</Text>\n                </FlexItem>\n                <FlexItem grow style={{ overflow: 'hidden' }}>\n                  <InstancesNavigationPopover name={name} />\n                </FlexItem>\n              </Row>\n            </div>\n          </div>\n        </div>\n      </FlexItem>\n\n      {isAnyChatAvailable && (\n        <FlexItem style={{ marginRight: 12 }}>\n          <CopilotTrigger />\n        </FlexItem>\n      )}\n      <FlexItem style={{ marginLeft: 12 }}>\n        <InsightsTrigger />\n      </FlexItem>\n\n      <FeatureFlagComponent\n        name={[FeatureFlags.cloudSso, FeatureFlags.cloudAds]}\n      >\n        <FlexItem\n          style={{ marginLeft: 16 }}\n          data-testid=\"o-auth-user-profile-rdi\"\n        >\n          <OAuthUserProfile source={OAuthSocialSource.UserProfile} />\n        </FlexItem>\n      </FeatureFlagComponent>\n    </Row>\n  )\n}\n\nexport default RdiInstanceHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/rdi-instance-header/index.ts",
    "content": "import RdiInstanceHeader from './RdiInstanceHeader'\n\nexport default RdiInstanceHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/rdi-instance-header/styles.module.scss",
    "content": ".container {\n  padding: 16px;\n}\n\n.breadcrumbsContainer {\n  height: 100%;\n  display: flex;\n  align-items: center;\n\n  & > div {\n    display: flex;\n  }\n}\n\n.rdiName {\n  display: inline-block !important;\n  overflow: hidden;\n  font-size: 14px;\n  line-height: 20px;\n  font-weight: 400;\n  text-overflow: ellipsis;\n  max-width: 100%;\n  white-space: nowrap;\n}\n\n.divider {\n  color: var(--euiTextSubduedColor);\n  font-size: 14px;\n  line-height: 20px;\n  font-weight: 400;\n  margin: 0 8px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/badge-icon/BadgeIcon.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport BadgeIcon, { Props } from './BadgeIcon'\n\nconst mockedProps = mock<Props>()\n\nconst icon = <div />\n\ndescribe('BadgeIcon', () => {\n  it('should render', () => {\n    expect(\n      render(<BadgeIcon {...instance(mockedProps)} icon={icon} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/badge-icon/BadgeIcon.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  id: string\n  icon: React.ReactElement\n  name: string\n}\nconst BadgeIcon = ({ id, icon, name }: Props) => (\n  <FlexItem\n    key={id}\n    className={styles.badge}\n    data-testid={`recommendation-badge-${id}`}\n  >\n    <div data-testid={id} className={styles.badgeWrapper}>\n      <RiTooltip content={name} position=\"top\" anchorClassName=\"flex-row\">\n        {icon}\n      </RiTooltip>\n    </div>\n  </FlexItem>\n)\n\nexport default BadgeIcon\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/badge-icon/index.ts",
    "content": "import BadgeIcon from './BadgeIcon'\n\nexport default BadgeIcon\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/constants.tsx",
    "content": "import React from 'react'\nimport CodeIcon from 'uiSrc/assets/img/code-changes.svg?react'\nimport ConfigurationIcon from 'uiSrc/assets/img/configuration-changes.svg?react'\nimport UpgradeIcon from 'uiSrc/assets/img/upgrade.svg?react'\n\nimport styles from './styles.module.scss'\n\nexport const badgesContent = [\n  {\n    id: 'code_changes',\n    icon: <CodeIcon className={styles.badgeIcon} />,\n    name: 'Code Changes',\n  },\n  {\n    id: 'configuration_changes',\n    icon: <ConfigurationIcon className={styles.badgeIcon} />,\n    name: 'Configuration Changes',\n  },\n  {\n    id: 'upgrade',\n    icon: <UpgradeIcon className={styles.badgeIcon} />,\n    name: 'Upgrade',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/content-element/ContentElement.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport ContentElement, { Props } from './ContentElement'\n\nconst mockedProps = mock<Props>()\n\nconst mockTelemetryName = 'name'\n\ndescribe('ContentElement', () => {\n  it('should render', () => {\n    expect(render(<ContentElement {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render paragraph', () => {\n    const mockContent = {\n      type: 'paragraph',\n      value: 'paragraph',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`paragraph-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render span', () => {\n    const mockContent = {\n      type: 'span',\n      value: 'span',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`span-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render code', () => {\n    const mockContent = {\n      type: 'code',\n      value: 'code',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`code-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render spacer', () => {\n    const mockContent = {\n      type: 'spacer',\n      value: 'l',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`spacer-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render link', () => {\n    const mockContent = {\n      type: 'link',\n      value: 'link',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`link-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render code link', () => {\n    const mockContent = {\n      type: 'code-link',\n      value: 'link',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`code-link-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render link-sso', () => {\n    const mockContent = {\n      type: 'link-sso',\n      value: 'link-sso',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`link-sso-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render internal-link', () => {\n    const mockContent = {\n      type: 'internal-link',\n      value: {\n        path: '/some-path',\n        name: 'name',\n      },\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`internal-link-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render list', () => {\n    const mockContent = {\n      type: 'list',\n      value: [[]],\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(\n      screen.queryByTestId(`list-${mockTelemetryName}-0`),\n    ).toBeInTheDocument()\n  })\n\n  it('should render unknown', () => {\n    const mockContent = {\n      type: 'unknown',\n      value: 'unknown',\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(screen.getByText('unknown')).toBeInTheDocument()\n  })\n\n  it('should not failed when value is not the string', () => {\n    const mockContent = {\n      type: 'unknown',\n      value: { custom: 'value' },\n    }\n    render(\n      <ContentElement\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    expect(screen.getByText('*Unknown format*')).toBeInTheDocument()\n  })\n\n  it('click on link should call onClick', () => {\n    const onClickMock = jest.fn()\n    const mockContent = {\n      type: 'link',\n      value: 'link',\n    }\n\n    const { queryByTestId } = render(\n      <ContentElement\n        onLinkClick={onClickMock}\n        content={mockContent}\n        telemetryName={mockTelemetryName}\n        idx={0}\n      />,\n    )\n\n    fireEvent.click(queryByTestId(`link-${mockTelemetryName}-0`) as HTMLElement)\n\n    expect(onClickMock).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/content-element/ContentElement.tsx",
    "content": "import React from 'react'\nimport { isArray, isString } from 'lodash'\nimport cx from 'classnames'\nimport { OAuthSsoHandlerDialog, OAuthConnectFreeDb } from 'uiSrc/components'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { replaceVariables } from 'uiSrc/utils/recommendation'\nimport { IRecommendationContent } from 'uiSrc/slices/interfaces/recommendations'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { UTM_MEDIUMS } from 'uiSrc/constants/links'\nimport { Spacer, SpacerSize } from 'uiSrc/components/base/layout/spacer'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport InternalLink from '../internal-link'\nimport RecommendationBody from '../recommendation-body'\n\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  content: IRecommendationContent\n  telemetryName: string\n  params?: any\n  onLinkClick?: () => void\n  insights?: boolean\n  idx: number\n}\n\nconst ContentElement = (props: Props) => {\n  const {\n    content = {},\n    params,\n    onLinkClick,\n    telemetryName,\n    insights,\n    idx,\n  } = props\n  const { type, value, parameter } = content\n\n  const replacedValue = replaceVariables(value, parameter, params)\n\n  switch (type) {\n    case 'paragraph':\n      return (\n        <ColorText\n          size=\"M\"\n          data-testid={`paragraph-${telemetryName}-${idx}`}\n          key={`${telemetryName}-${idx}`}\n          component=\"div\"\n          className={cx(styles.text, { [styles.insights]: insights })}\n          color=\"primary\"\n        >\n          {value}\n        </ColorText>\n      )\n    case 'code':\n      return (\n        <ColorText\n          size=\"M\"\n          data-testid={`code-${telemetryName}-${idx}`}\n          className={cx(styles.code, { [styles.insights]: insights })}\n          key={`${telemetryName}-${idx}`}\n          color=\"primary\"\n        >\n          <code className={cx(styles.span, styles.text)}>{value}</code>\n        </ColorText>\n      )\n    case 'span':\n      return (\n        <ColorText\n          size=\"M\"\n          data-testid={`span-${telemetryName}-${idx}`}\n          key={`${telemetryName}-${idx}`}\n          color=\"primary\"\n          className={cx(styles.span, styles.text, {\n            [styles.insights]: insights,\n          })}\n        >\n          {value}\n        </ColorText>\n      )\n    case 'link':\n      return (\n        <Link\n          color=\"subdued\"\n          key={`${telemetryName}-${idx}`}\n          data-testid={`link-${telemetryName}-${idx}`}\n          target=\"_blank\"\n          variant=\"inline\"\n          size=\"M\"\n          href={getUtmExternalLink(value.href, {\n            medium: UTM_MEDIUMS.Recommendation,\n            campaign: telemetryName,\n          })}\n          onClick={() => onLinkClick?.()}\n        >\n          {value.name}\n        </Link>\n      )\n    case 'link-sso':\n      return (\n        <OAuthSsoHandlerDialog>\n          {(ssoCloudHandlerClick) => (\n            <Link\n              key={`${telemetryName}-${idx}`}\n              data-testid={`link-sso-${telemetryName}-${idx}`}\n              target=\"_blank\"\n              variant=\"inline\"\n              size=\"M\"\n              onClick={(e) => {\n                ssoCloudHandlerClick?.(e, {\n                  source: telemetryName as OAuthSocialSource,\n                  action: OAuthSocialAction.Create,\n                })\n              }}\n              href={getUtmExternalLink(value.href, {\n                medium: UTM_MEDIUMS.Recommendation,\n                campaign: telemetryName,\n              })}\n            >\n              {value.name}\n            </Link>\n          )}\n        </OAuthSsoHandlerDialog>\n      )\n    case 'connect-btn':\n      return <OAuthConnectFreeDb source={telemetryName as OAuthSocialSource} />\n    case 'code-link':\n      return (\n        <Link\n          key={`${telemetryName}-${idx}`}\n          data-testid={`code-link-${telemetryName}-${idx}`}\n          target=\"_blank\"\n          variant=\"inline\"\n          size=\"M\"\n          href={getUtmExternalLink(value.href, {\n            medium: UTM_MEDIUMS.Recommendation,\n            campaign: telemetryName,\n          })}\n        >\n          <ColorText\n            className={cx(styles.code, { [styles.insights]: insights })}\n            color=\"subdued\"\n          >\n            <code className={cx(styles.span, styles.text)}>{value.name}</code>\n          </ColorText>\n        </Link>\n      )\n    case 'spacer':\n      return (\n        <Spacer\n          data-testid={`spacer-${telemetryName}-${idx}`}\n          key={`${telemetryName}-${idx}`}\n          size={value as SpacerSize}\n        />\n      )\n    case 'list':\n      return (\n        <ul\n          className={styles.list}\n          data-testid={`list-${telemetryName}-${idx}`}\n          key={`${telemetryName}-${idx}`}\n        >\n          {isArray(value) &&\n            value.map((listElement: IRecommendationContent[], idx: number) => (\n              <li\n                className={cx(styles.listItem, { [styles.insights]: insights })}\n                // eslint-disable-next-line react/no-array-index-key\n                key={`list-item-${idx}`}\n              >\n                <RecommendationBody\n                  elements={listElement}\n                  params={params}\n                  telemetryName={telemetryName}\n                  onLinkClick={onLinkClick}\n                  insights={insights}\n                />\n              </li>\n            ))}\n        </ul>\n      )\n    case 'internal-link':\n      return (\n        <InternalLink\n          key={`${telemetryName}-${idx}`}\n          dataTestid={`internal-link-${telemetryName}-${idx}`}\n          path={replacedValue.path}\n          text={replacedValue.name}\n        />\n      )\n    default:\n      return isString(value) ? <>{value}</> : <b>*Unknown format*</b>\n  }\n}\n\nexport default ContentElement\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/content-element/index.ts",
    "content": "import ContentElement from './ContentElement'\n\nexport default ContentElement\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/index.ts",
    "content": "import RecommendationBadges from './recommendation-badges'\nimport RecommendationBadgesLegend from './recommendation-badges-legend'\nimport RecommendationBody from './recommendation-body'\nimport RecommendationVoting from './recommendation-voting'\nimport RecommendationCopyComponent from './recommendation-copy-component'\n\nexport {\n  RecommendationBody,\n  RecommendationBadges,\n  RecommendationBadgesLegend,\n  RecommendationVoting,\n  RecommendationCopyComponent,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/internal-link/InternalLink.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport InternalLink, { Props } from './InternalLink'\n\nconst mockedProps = mock<Props>()\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\ndescribe('InternalLink', () => {\n  it('should render', () => {\n    expect(render(<InternalLink {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call history push with proper path', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    const onClickMock = jest.fn()\n\n    render(\n      <InternalLink\n        {...instance(mockedProps)}\n        path=\"path\"\n        onClick={onClickMock}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('internal-link'))\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('path')\n    expect(onClickMock).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/internal-link/InternalLink.tsx",
    "content": "import React from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\n\nexport interface Props {\n  path: string\n  text: string\n  dataTestid?: string\n  onClick?: () => void\n}\n\nconst InternalLink = (props: Props) => {\n  const { path, text, onClick, dataTestid } = props\n\n  const history = useHistory()\n\n  const handleClick = () => {\n    // can replace parameters here if needed (instanceId or rdiInstanceId)\n    history.push(path)\n    onClick?.()\n  }\n  return (\n    <PrimaryButton\n      size=\"s\"\n      onClick={handleClick}\n      data-testid={dataTestid || 'internal-link'}\n    >\n      {text}\n    </PrimaryButton>\n  )\n}\n\nexport default InternalLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/internal-link/index.ts",
    "content": "import InternalLink from './InternalLink'\n\nexport default InternalLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-badges/RecommendationBadges.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport RecommendationBadges, { Props } from './RecommendationBadges'\n\nconst mockedProps = mock<Props>()\n\ndescribe('RecommendationBadges', () => {\n  it('should render', () => {\n    expect(\n      render(<RecommendationBadges {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render code changes badge', () => {\n    expect(\n      render(<RecommendationBadges badges={['code_changes']} />),\n    ).toBeTruthy()\n\n    expect(\n      screen.queryByTestId('recommendation-badge-code_changes'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('recommendation-badge-upgrade'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('recommendation-badge-configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render upgrade and configuration changes badges', () => {\n    expect(\n      render(\n        <RecommendationBadges badges={['upgrade', 'configuration_changes']} />,\n      ),\n    ).toBeTruthy()\n\n    expect(\n      screen.queryByTestId('recommendation-badge-code_changes'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('recommendation-badge-upgrade'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('recommendation-badge-configuration_changes'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-badges/RecommendationBadges.tsx",
    "content": "import React from 'react'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport BadgeIcon from '../badge-icon'\nimport { badgesContent } from '../constants'\n\nexport interface Props {\n  badges?: string[]\n}\n\nconst RecommendationBadges = ({ badges = [] }: Props) => (\n  <Row align=\"center\" justify=\"end\" gap=\"m\">\n    {badgesContent.map(\n      ({ id, name, icon }) =>\n        badges.includes(id) && (\n          <BadgeIcon key={id} id={id} icon={icon} name={name} />\n        ),\n    )}\n  </Row>\n)\n\nexport default RecommendationBadges\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-badges/index.ts",
    "content": "import RecommendationBadges from './RecommendationBadges'\n\nexport default RecommendationBadges\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-badges-legend/RecommendationBadgesLegend.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RecommendationBadgesLegend from './RecommendationBadgesLegend'\n\ndescribe('RecommendationBadgesLegend', () => {\n  it('should render', () => {\n    expect(render(<RecommendationBadgesLegend />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-badges-legend/RecommendationBadgesLegend.tsx",
    "content": "import React from 'react'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { badgesContent } from '../constants'\nimport styles from '../styles.module.scss'\n\nconst RecommendationBadgesLegend = () => (\n  <Row\n    data-testid=\"badges-legend\"\n    className={styles.badgesLegend}\n    justify=\"end\"\n  >\n    {badgesContent.map(({ id, icon, name }) => (\n      <FlexItem key={id} className={styles.badge}>\n        <div className={styles.badgeWrapper}>\n          {icon}\n          {name}\n        </div>\n      </FlexItem>\n    ))}\n  </Row>\n)\n\nexport default RecommendationBadgesLegend\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-badges-legend/index.ts",
    "content": "import RecommendationBadgesLegend from './RecommendationBadgesLegend'\n\nexport default RecommendationBadgesLegend\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-body/RecommendationBody.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { IRecommendationContent } from 'uiSrc/slices/interfaces/recommendations'\nimport RecommendationBody, { Props } from './RecommendationBody'\n\nconst mockedProps = mock<Props>()\n\nconst mockContent: IRecommendationContent[] = [\n  {\n    type: 'unknown',\n    value: 'unknown',\n  },\n]\n\nconst mockTelemetryName = 'name'\n\ndescribe('RecommendationBody', () => {\n  it('should render', () => {\n    expect(\n      render(<RecommendationBody {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render element', () => {\n    render(\n      <RecommendationBody\n        {...instance(mockedProps)}\n        elements={mockContent}\n        telemetryName={mockTelemetryName}\n      />,\n    )\n\n    expect(screen.getByText('unknown')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-body/RecommendationBody.tsx",
    "content": "import React from 'react'\nimport { IRecommendationContent } from 'uiSrc/slices/interfaces/recommendations'\nimport ContentElement from '../content-element'\n\nexport interface Props {\n  elements: IRecommendationContent[]\n  telemetryName: string\n  params?: any\n  onLinkClick?: () => void\n  insights?: boolean\n}\n\nconst RecommendationBody = ({ elements = [], ...rest }: Props) => (\n  <div>\n    {// eslint-disable-next-line react/no-array-index-key\n    elements?.map((item, idx) => (\n      <ContentElement key={idx} content={item} idx={idx} {...rest} />\n    ))}\n  </div>\n)\n\nexport default RecommendationBody\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-body/index.ts",
    "content": "import RecommendationBody from './RecommendationBody'\n\nexport default RecommendationBody\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-copy-component/RecommendationCopyComponent.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, fireEvent, act, screen } from 'uiSrc/utils/test-utils'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport RecommendationCopyComponent, {\n  IProps,\n} from './RecommendationCopyComponent'\n\nconst mockedProps = mock<IProps>()\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockProvider = 'PROVIDER'\n\nconst mockTelemetryEvent = 'recommendationName'\n\ndescribe('RecommendationCopyComponent', () => {\n  beforeEach(() => {\n    Object.assign(navigator, {\n      clipboard: { writeText: jest.fn() },\n    })\n  })\n\n  it('should render', () => {\n    expect(\n      render(<RecommendationCopyComponent {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('event telemetry INSIGHTS_RECOMMENDATION_KEY_COPIED should be call after click on copy btn', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(\n      <RecommendationCopyComponent\n        {...instance(mockedProps)}\n        live\n        provider={mockProvider}\n        telemetryEvent={mockTelemetryEvent}\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('copy-key-name-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_KEY_COPIED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        name: mockTelemetryEvent,\n        provider: mockProvider,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('event telemetry DATABASE_RECOMMENDATIONS_KEY_COPIED should be call after click on copy btn', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(\n      <RecommendationCopyComponent\n        {...instance(mockedProps)}\n        provider={mockProvider}\n        telemetryEvent={mockTelemetryEvent}\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('copy-key-name-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.DATABASE_TIPS_KEY_COPIED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        name: mockTelemetryEvent,\n        provider: mockProvider,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-copy-component/RecommendationCopyComponent.tsx",
    "content": "import React from 'react'\nimport { useParams } from 'react-router-dom'\nimport styled from 'styled-components'\n\nimport { bufferToString } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\nimport { HorizontalSpacer } from 'uiSrc/components/base/layout'\n\nconst StyledWrapper = styled.div`\n  margin-top: 15px;\n`\n\nconst StyledKeyNameWrapper = styled(FlexGroup)<{ $isDbAnalysis: boolean }>`\n  margin-top: 5px;\n  border: 1px dashed ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: 4px;\n  background-color: ${(props) =>\n    props.$isDbAnalysis\n      ? props.theme.semantic.color.background.neutral100\n      : props.theme.semantic.color.background.neutral400};\n`\n\nconst StyledKeyName = styled(Text)`\n  padding: ${({ theme }) => theme.core.space.space050};\n  font:\n    normal normal normal 13px/16px Graphik,\n    sans-serif !important;\n  height: 26px;\n`\n\nconst StyledText = styled(Text)`\n  font:\n    normal normal normal 13px/16px Graphik,\n    sans-serif !important;\n`\n\nexport interface IProps {\n  keyName: string\n  provider?: string\n  telemetryEvent: string\n  live?: boolean\n}\n\nconst RecommendationCopyComponent = ({\n  live = false,\n  keyName,\n  telemetryEvent,\n  provider,\n}: IProps) => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const formattedName = bufferToString(keyName)\n\n  const handleCopy = () => {\n    sendEventTelemetry({\n      event: live\n        ? TelemetryEvent.INSIGHTS_TIPS_KEY_COPIED\n        : TelemetryEvent.DATABASE_TIPS_KEY_COPIED,\n      eventData: {\n        databaseId: instanceId,\n        name: telemetryEvent,\n        provider,\n      },\n    })\n  }\n\n  return (\n    <StyledWrapper>\n      <StyledText>Example of a key that may be relevant:</StyledText>\n      <StyledKeyNameWrapper align=\"center\" $isDbAnalysis={!live}>\n        <StyledKeyName\n          className=\"truncateText\"\n          data-testid=\"recommendation-key-name\"\n          component=\"div\"\n        >\n          {formattedName}\n        </StyledKeyName>\n        <CopyButton\n          copy={formattedName}\n          onCopy={handleCopy}\n          withTooltip={false}\n          data-testid=\"copy-key-name\"\n          aria-label=\"copy key name\"\n        />\n        <HorizontalSpacer size=\"xs\" />\n      </StyledKeyNameWrapper>\n    </StyledWrapper>\n  )\n}\n\nexport default RecommendationCopyComponent\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-copy-component/index.ts",
    "content": "import RecommendationCopyComponent from './RecommendationCopyComponent'\n\nexport default RecommendationCopyComponent\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/RecommendationVoting.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'\n\nimport {\n  act,\n  cleanup,\n  mockedStore,\n  fireEvent,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport RecommendationVoting, { Props } from './RecommendationVoting'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsConfigSelector: jest.fn().mockReturnValue({\n    agreements: {\n      analytics: true,\n    },\n  }),\n}))\n\ndescribe('RecommendationVoting', () => {\n  it('should render', () => {\n    expect(\n      render(<RecommendationVoting {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render popover after click \"not-useful-vote-btn\"', async () => {\n    render(<RecommendationVoting {...instance(mockedProps)} />)\n\n    expect(\n      document.querySelector('[data-test-subj=\"github-repo-link\"]'),\n    ).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('not useful-vote-btn'))\n    await waitForRiPopoverVisible()\n\n    expect(\n      document.querySelector('[data-test-subj=\"github-repo-link\"]'),\n    ).toHaveAttribute(\n      'href',\n      'https://github.com/RedisInsight/RedisInsight/issues/new/choose',\n    )\n  })\n\n  it('should render proper popover and btn should be disabled\"', async () => {\n    ;(userSettingsConfigSelector as jest.Mock).mockImplementation(() => ({\n      agreements: {\n        analytics: false,\n      },\n    }))\n    render(<RecommendationVoting {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('not useful-vote-btn'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('not useful-vote-tooltip')).toHaveTextContent(\n      'Enable Analytics on the Settings page to vote for a tip',\n    )\n    expect(screen.getByTestId('not useful-vote-btn')).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/RecommendationVoting.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport { Nullable } from 'uiSrc/utils'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport VoteOption from './components/vote-option'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  vote?: Nullable<Vote>\n  name: string\n  id?: string\n  live?: boolean\n  containerClass?: string\n}\n\nconst RecommendationVoting = ({\n  vote,\n  name,\n  id = '',\n  live = false,\n  containerClass = '',\n}: Props) => {\n  const config = useSelector(userSettingsConfigSelector)\n  const [popover, setPopover] = useState<string>('')\n\n  return (\n    <Row\n      align=\"center\"\n      className={cx(styles.votingContainer, containerClass)}\n      gap={live ? 'none' : 'l'}\n      data-testid=\"recommendation-voting\"\n    >\n      <Text size=\"m\">Is this useful?</Text>\n      <div className=\"voteContent\">\n        {Object.values(Vote).map((option) => (\n          <VoteOption\n            key={option}\n            voteOption={option}\n            vote={vote}\n            popover={popover}\n            isAnalyticsEnable={config?.agreements?.analytics}\n            setPopover={setPopover}\n            name={name}\n            id={id}\n            live={live}\n          />\n        ))}\n      </div>\n    </Row>\n  )\n}\n\nexport default RecommendationVoting\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/components/vote-option/VoteOption.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport { setRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport {\n  cleanup,\n  mockedStore,\n  fireEvent,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport VoteOption, { Props } from './VoteOption'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsConfigSelector: jest.fn().mockReturnValue({\n    agreements: {\n      analytics: true,\n    },\n  }),\n}))\n\ndescribe('VoteOption', () => {\n  it('should render', () => {\n    expect(render(<VoteOption {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render proper text for Like vote', async () => {\n    render(\n      <VoteOption\n        {...instance(mockedProps)}\n        voteOption={Vote.Like}\n        popover={Vote.Like}\n      />,\n    )\n\n    expect(screen.getByTestId('common-text')).toHaveTextContent(\n      'Thank you for the feedback.',\n    )\n    expect(screen.getByTestId('custom-text')).toHaveTextContent(\n      'Share your ideas with us.',\n    )\n  })\n\n  it('should render proper text for Dislike vote', () => {\n    render(<VoteOption {...instance(mockedProps)} vote={Vote.Dislike} />)\n    expect(screen.getByTestId('common-text')).toHaveTextContent(\n      'Thank you for the feedback.',\n    )\n    expect(screen.getByTestId('custom-text')).toHaveTextContent(\n      'Tell us how we can improve.',\n    )\n  })\n\n  it('should call \"setRecommendationVote\" action be called after click \"useful-vote-btn\"', () => {\n    render(\n      <VoteOption\n        {...instance(mockedProps)}\n        isAnalyticsEnable\n        voteOption={Vote.Like}\n        vote={Vote.Like}\n        setPopover={() => {}}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('useful-vote-btn'))\n\n    const expectedActions = [setRecommendationVote()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call \"setRecommendationVote\" action be called after click \"not-useful-vote-btn\"', () => {\n    render(\n      <VoteOption\n        {...instance(mockedProps)}\n        isAnalyticsEnable\n        voteOption={Vote.Dislike}\n        vote={Vote.Dislike}\n        setPopover={() => {}}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('not useful-vote-btn'))\n\n    const expectedActions = [setRecommendationVote()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/components/vote-option/VoteOption.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport { putRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  recommendationsSelector,\n  updateLiveRecommendation,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { Nullable } from 'uiSrc/utils'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nimport { getVotedText, iconType, voteTooltip } from './utils'\nimport styles from './styles.module.scss'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nconst GitHubLink = styled(Link)`\n  padding: 4px 8px 4px 4px;\n\n  margin-top: 10px;\n  height: 22px !important;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.button.variants.primary.normal?.bgColor};\n  color: ${({ theme }: { theme: Theme }) =>\n    theme.components.button.variants.primary.normal?.textColor};\n  &:hover {\n    text-decoration: none !important;\n    background-color: ${({ theme }: { theme: Theme }) =>\n      theme.components.button.variants.primary.hover?.bgColor};\n    color: ${({ theme }: { theme: Theme }) =>\n      theme.components.button.variants.primary.normal?.textColor};\n  }\n\n  & > span {\n    display: flex;\n    gap: 4px;\n    align-items: center;\n  }\n`\n\nconst VotingIconButton = styled(IconButton)`\n  width: 28px !important;\n  height: 28px !important;\n  border-radius: 50%;\n`\n\nexport interface Props {\n  voteOption: Vote\n  vote?: Nullable<Vote>\n  popover: string\n  isAnalyticsEnable?: boolean\n  setPopover: (popover: string) => void\n  live?: boolean\n  id: string\n  name: string\n}\n\nconst VoteOption = (props: Props) => {\n  const {\n    voteOption,\n    vote,\n    popover,\n    isAnalyticsEnable,\n    setPopover,\n    live,\n    id,\n    name,\n  } = props\n\n  const dispatch = useDispatch()\n  const { id: instanceId = '', provider } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { content: recommendationsContent } = useSelector(\n    recommendationsSelector,\n  )\n\n  const onSuccessVoted = ({\n    vote,\n    name,\n  }: {\n    name: string\n    vote: Nullable<Vote>\n  }) => {\n    sendEventTelemetry({\n      event: live\n        ? TelemetryEvent.INSIGHTS_TIPS_VOTED\n        : TelemetryEvent.DATABASE_ANALYSIS_TIPS_VOTED,\n      eventData: {\n        databaseId: instanceId,\n        name: recommendationsContent[name]?.telemetryEvent ?? name,\n        vote,\n        provider,\n      },\n    })\n  }\n\n  const handleClick = (name: string) => {\n    setPopover(voteOption)\n\n    if (live) {\n      dispatch(\n        updateLiveRecommendation(id, { vote: voteOption }, onSuccessVoted),\n      )\n    } else {\n      dispatch(putRecommendationVote(name, voteOption, onSuccessVoted))\n    }\n  }\n\n  const getTooltipContent = (voteOption: Vote) =>\n    isAnalyticsEnable\n      ? voteTooltip[voteOption]\n      : 'Enable Analytics on the Settings page to vote for a tip'\n\n  return (\n    <RiPopover\n      anchorPosition=\"rightCenter\"\n      isOpen={popover === voteOption}\n      closePopover={() => setPopover('')}\n      anchorClassName={styles.popoverAnchor}\n      panelClassName={cx('popoverLikeTooltip', styles.popover)}\n      button={\n        <RiTooltip\n          content={getTooltipContent(voteOption)}\n          position=\"bottom\"\n          data-testid={`${voteOption}-vote-tooltip`}\n        >\n          <VotingIconButton\n            disabled={!isAnalyticsEnable}\n            icon={iconType[voteOption] ?? 'LikeIcon'}\n            className={cx('vote__btn', { selected: vote === voteOption })}\n            aria-label=\"vote useful\"\n            data-testid={`${voteOption}-vote-btn`}\n            onClick={() => handleClick(name)}\n          />\n        </RiTooltip>\n      }\n    >\n      <div\n        className={styles.popoverWrapper}\n        data-testid={`${name}-${voteOption}-popover`}\n      >\n        <Col align=\"end\">\n          <FlexItem>\n            <Row>\n              <FlexItem>\n                <RiIcon type=\"PetardIcon\" className={styles.petardIcon} />\n              </FlexItem>\n              <FlexItem grow>\n                <div>\n                  <Text className={styles.text} data-testid=\"common-text\">\n                    Thank you for the feedback.\n                  </Text>\n                  <Text className={styles.text} data-testid=\"custom-text\">\n                    {getVotedText(voteOption)}\n                  </Text>\n                </div>\n              </FlexItem>\n              <FlexItem>\n                <IconButton\n                  icon={CancelSlimIcon}\n                  id=\"close-voting-popover\"\n                  aria-label=\"close popover\"\n                  data-testid=\"close-popover\"\n                  className={styles.closeBtn}\n                  onClick={() => setPopover('')}\n                />\n              </FlexItem>\n            </Row>\n          </FlexItem>\n          <FlexItem grow>\n            <GitHubLink\n              data-testid=\"recommendation-feedback-btn\"\n              className={styles.link}\n              href={EXTERNAL_LINKS.recommendationFeedback}\n              target=\"_blank\"\n              data-test-subj=\"github-repo-link\"\n            >\n              <RiIcon\n                className={styles.githubIcon}\n                aria-label=\"redis insight github issues\"\n                type=\"GithubIcon\"\n                color=\"informative100\"\n                data-testid=\"github-repo-icon\"\n              />\n              To Github\n            </GitHubLink>\n          </FlexItem>\n        </Col>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default VoteOption\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/components/vote-option/index.ts",
    "content": "import VoteOption from './VoteOption'\n\nexport default VoteOption\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/components/vote-option/styles.module.scss",
    "content": ".popoverWrapper {\n  .petardIcon {\n    width: 20px;\n    height: 18px;\n    margin-right: 4px;\n  }\n  \n  .text {\n    font: normal normal normal 12px/14px Graphik, sans-serif !important;\n  }\n\n  .closeBtn {\n    width: 14px;\n    height: 14px;\n    margin-left: 4px;\n  }\n  \n  .feedbackBtn {\n    padding: 4px 8px 4px 4px;\n    margin-top: 10px;\n    height: 22px !important;\n  \n    :global(.euiButtonContent.euiButton__content) {\n      padding: 0;\n    }\n  \n    :global(.euiLink).link {\n      display: flex;\n      align-items: center;\n      color: var(--euiColorPrimaryText) !important;\n      text-decoration: none !important;\n      font: normal normal normal 12px/14px Graphik, sans-serif;\n    }\n  \n    .link .githubIcon {\n      margin-right: 2px;\n    }\n  }\n}\n\n:global(.euiPanel).popover {\n  max-width: none !important;\n  box-shadow: none !important;\n  padding: 16px !important;\n  color: var(--buttonSecondaryTextColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/components/vote-option/utils.ts",
    "content": "import { Vote } from 'uiSrc/constants/recommendations'\nimport { Nullable } from 'uiSrc/utils'\nimport { DislikeIcon, LikeIcon } from 'uiSrc/components/base/icons'\n\nexport const getVotedText = (vote: Nullable<Vote>) =>\n  vote === Vote.Like\n    ? 'Share your ideas with us.'\n    : 'Tell us how we can improve.'\n\nexport const voteTooltip = Object.freeze({\n  [Vote.Like]: 'Useful',\n  [Vote.Dislike]: 'Not Useful',\n})\n\nexport const iconType = {\n  [Vote.Like]: LikeIcon,\n  [Vote.Dislike]: DislikeIcon,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/index.ts",
    "content": "import RecommendationVoting from './RecommendationVoting'\n\nexport default RecommendationVoting\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/recommendation-voting/styles.module.scss",
    "content": ".votingContainer {\n  :global {\n    .voteContent {\n      margin-left: 10px;\n\n      svg {\n        width: 34px;\n        height: 34px;\n        fill: none;\n\n        path {\n          stroke: var(--buttonSecondaryTextColor);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/recommendation/styles.module.scss",
    "content": ".text {\n  font: normal normal normal 14px/24px Graphik, sans-serif !important;\n\n  &.insights {\n    color: var(--htmlColor) !important;\n  }\n}\n\n.span {\n  display: inline !important;\n}\n\n.list {\n  list-style: initial;\n  padding-left: 21px;\n  margin: 0 !important;\n\n  .listItem::marker {\n    color: var(--euiTextSubduedColor);\n  }\n\n  .listItem.insights::marker {\n    color: var(--htmlColor) !important;\n  }\n}\n\n.code {\n  background-color: var(--separatorColor);\n  border-radius: 4px;\n  padding: 3px 6px;\n\n  &.insights {\n    background-color: var(--euiColorLightestShade) !important;\n  }\n\n  code {\n    font-family: \"Roboto Mono\", Consolas, Menlo, Courier, monospace !important;\n  }\n}\n\n.badgesLegend {\n  margin: 0 22px 14px 0 !important;\n  padding-top: 20px;\n\n  .badgeWrapper {\n    margin-right: 0;\n  }\n\n  .badge {\n    margin: 0 0 0 24px;\n  }\n\n  .badgeIcon {\n    margin-right: 14px;\n  }\n}\n\n.badgeIcon {\n  fill: var(--badgeIconColor);\n}\n\n.badgeWrapper {\n  display: flex;\n  align-items: center;\n  margin-right: 24px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/scan-more/ScanMore.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, screen, render } from 'uiSrc/utils/test-utils'\nimport ScanMore, { Props } from './ScanMore'\n\nconst mockedProps = mock<Props>()\n\ndescribe('ScanMore', () => {\n  it('should render', () => {\n    expect(render(<ScanMore {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call \"loadMoreItems\"', () => {\n    const handleClick = jest.fn()\n\n    const renderer = render(\n      <ScanMore\n        {...instance(mockedProps)}\n        loadMoreItems={handleClick}\n        scanned={1}\n        totalItemsCount={2}\n      />,\n    )\n\n    expect(renderer).toBeTruthy()\n\n    fireEvent.click(screen.getByTestId('scan-more'))\n    expect(handleClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should show button when totalItemsCount < scanned and nextCursor is not zero', () => {\n    const { queryByTestId } = render(\n      <ScanMore\n        {...instance(mockedProps)}\n        scanned={2}\n        totalItemsCount={1}\n        nextCursor=\"123\"\n      />,\n    )\n\n    expect(queryByTestId('scan-more')).toBeInTheDocument()\n  })\n\n  it('should hide button when totalItemsCount < scanned and nextCursor is zero', () => {\n    const { queryByTestId } = render(\n      <ScanMore\n        {...instance(mockedProps)}\n        scanned={2}\n        totalItemsCount={1}\n        nextCursor=\"0\"\n      />,\n    )\n\n    expect(queryByTestId('scan-more')).not.toBeInTheDocument()\n  })\n  it('should button be shown when totalItemsCount > scanned ', () => {\n    const { queryByTestId } = render(\n      <ScanMore {...instance(mockedProps)} scanned={1} totalItemsCount={2} />,\n    )\n\n    expect(queryByTestId('scan-more')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/scan-more/ScanMore.tsx",
    "content": "import React from 'react'\nimport { isNull } from 'lodash'\nimport styled from 'styled-components'\n\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { RiTooltip } from 'uiSrc/components'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { TextButton } from '@redis-ui/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  withAlert?: boolean\n  fill?: boolean\n  loading: boolean\n  scanned?: number\n  totalItemsCount?: number\n  nextCursor?: string\n  style?: {\n    [key: string]: string | number\n  }\n  loadMoreItems?: (config: any) => void\n}\n\nconst WARNING_MESSAGE =\n  'Scanning additional keys may decrease performance and memory available.'\n\nconst ScanMoreButton = styled(TextButton)`\n  color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.text.primary400} !important;\n  line-height: inherit;\n`\n\nconst ScanMore = ({\n  withAlert = true,\n  scanned = 0,\n  totalItemsCount = 0,\n  loading,\n  loadMoreItems,\n  nextCursor,\n}: Props) => (\n  <>\n    {(scanned || isNull(totalItemsCount)) && nextCursor !== '0' && (\n      <ScanMoreButton\n        disabled={loading}\n        onClick={() =>\n          loadMoreItems?.({\n            stopIndex: SCAN_COUNT_DEFAULT - 1,\n            startIndex: 0,\n          })\n        }\n        data-testid=\"scan-more\"\n      >\n        <Text size=\"s\">Scan more</Text>\n        {withAlert && (\n          <RiTooltip\n            content={WARNING_MESSAGE}\n            position=\"top\"\n            anchorClassName={styles.anchor}\n          >\n            <RiIcon color=\"primary400\" size=\"m\" type=\"InfoIcon\" />\n          </RiTooltip>\n        )}\n      </ScanMoreButton>\n    )}\n  </>\n)\n\nexport default ScanMore\n"
  },
  {
    "path": "redisinsight/ui/src/components/scan-more/index.ts",
    "content": "import ScanMore from './ScanMore'\n\nexport default ScanMore\n"
  },
  {
    "path": "redisinsight/ui/src/components/scan-more/styles.module.scss",
    "content": ".anchor {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport SettingItem from './SettingItem'\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    config: {\n      scanThreshold: 10000,\n      batchSize: 5,\n    },\n  }),\n  updateUserConfigSettingsAction: () => jest.fn,\n}))\n\nconst mockedProps = {\n  initValue: '10000',\n  onApply: jest.fn(),\n  validation: jest.fn((x) => x),\n  title: 'Keys to Scan in List view',\n  summary:\n    'Sets the amount of keys to scan per one iteration. Filtering by pattern per a large number of keys may decrease performance.',\n  testid: 'keys-to-scan',\n  placeholder: '10 000',\n  label: 'Keys to Scan:',\n}\n\ndescribe('SettingItem', () => {\n  it('should render', () => {\n    expect(render(<SettingItem {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render keys to scan value', () => {\n    render(<SettingItem {...mockedProps} />)\n    expect(screen.getByTestId(/keys-to-scan-value/)).toHaveTextContent('10000')\n  })\n\n  it('should render keys to scan input after click value', () => {\n    render(<SettingItem {...mockedProps} />)\n    fireEvent.click(screen.getByTestId(/keys-to-scan-value/))\n    expect(screen.getByTestId(/keys-to-scan-input/)).toBeInTheDocument()\n  })\n\n  it('should change keys to scan input properly', () => {\n    render(<SettingItem {...mockedProps} />)\n    fireEvent.click(screen.getByTestId(/keys-to-scan-value/))\n    fireEvent.change(screen.getByTestId(/keys-to-scan-input/), {\n      target: { value: '6900' },\n    })\n    expect(screen.getByTestId(/keys-to-scan-input/)).toHaveValue('6900')\n  })\n\n  it('should properly apply changes', () => {\n    render(<SettingItem {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId(/keys-to-scan-value/))\n    fireEvent.change(screen.getByTestId(/keys-to-scan-input/), {\n      target: { value: '6900' },\n    })\n    fireEvent.click(screen.getByTestId(/apply-btn/))\n    expect(screen.getByTestId(/keys-to-scan-value/)).toHaveTextContent('6900')\n  })\n\n  it('should properly decline changes', async () => {\n    render(<SettingItem {...mockedProps} />)\n    fireEvent.click(screen.getByTestId(/keys-to-scan-value/))\n\n    fireEvent.change(screen.getByTestId(/keys-to-scan-input/), {\n      target: { value: '6900' },\n    })\n    fireEvent.click(screen.getByTestId(/cancel-btn/))\n    expect(screen.getByTestId(/keys-to-scan-value/)).toHaveTextContent('10000')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/settings-item/SettingItem.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\n\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { NumericInput } from 'uiSrc/components/base/inputs'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  initValue: string\n  testid: string\n  placeholder: string\n  label: string\n  title: string\n  summary: string | JSX.Element\n  onApply: (value: string) => void\n  validation: (value: string) => string\n}\n\nconst SettingItem = (props: Props) => {\n  const {\n    initValue,\n    title,\n    summary,\n    testid,\n    placeholder,\n    label,\n    onApply,\n    validation = (val: string) => val,\n  } = props\n\n  const [value, setValue] = useState<string>(initValue)\n  const [isEditing, setEditing] = useState<boolean>(false)\n  const [isHovering, setHovering] = useState<boolean>(false)\n\n  useEffect(() => {\n    setValue(initValue)\n  }, [initValue])\n\n  const handleApplyChanges = () => {\n    setEditing(false)\n    setHovering(false)\n\n    onApply(value)\n  }\n\n  const handleDeclineChanges = (event?: React.MouseEvent<HTMLElement>) => {\n    event?.stopPropagation()\n    setValue(initValue)\n    setEditing(false)\n    setHovering(false)\n  }\n\n  return (\n    <>\n      <Title component=\"h5\" size=\"S\">\n        {title}\n      </Title>\n      <Spacer size=\"s\" />\n      <Text size=\"M\">{summary}</Text>\n      <Spacer size=\"m\" />\n      <Row align=\"center\" justify=\"start\" gap=\"s\" className={styles.container}>\n        <FlexItem>\n          <Text size=\"M\" variant=\"semiBold\">\n            {label}\n          </Text>\n        </FlexItem>\n\n        <FlexItem\n          onMouseEnter={() => !isEditing && setHovering(true)}\n          onMouseLeave={() => !isEditing && setHovering(false)}\n          onClick={() => setEditing(true)}\n          style={{ width: '200px' }}\n        >\n          {isEditing || isHovering ? (\n            <InlineItemEditor\n              controlsPosition=\"right\"\n              viewChildrenMode={!isEditing}\n              onApply={handleApplyChanges}\n              onDecline={handleDeclineChanges}\n              declineOnUnmount={false}\n            >\n              <Row\n                align=\"center\"\n                justify=\"between\"\n                className={styles.inputHover}\n              >\n                <NumericInput\n                  autoValidate\n                  onChange={(value) =>\n                    isEditing &&\n                    setValue(validation(value ? value.toString() : ''))\n                  }\n                  value={Number(value)}\n                  placeholder={placeholder}\n                  aria-label={testid?.replaceAll?.('-', ' ')}\n                  className={cx(styles.input, {\n                    [styles.inputEditing]: isEditing,\n                  })}\n                  readOnly={!isEditing}\n                  data-testid={`${testid}-input`}\n                  style={{ width: '100%' }}\n                />\n                {!isEditing && <EditIcon />}\n              </Row>\n            </InlineItemEditor>\n          ) : (\n            <Text\n              variant=\"semiBold\"\n              className={styles.value}\n              data-testid={`${testid}-value`}\n            >\n              {value}\n            </Text>\n          )}\n        </FlexItem>\n      </Row>\n      <Spacer size=\"m\" />\n    </>\n  )\n}\n\nexport default SettingItem\n"
  },
  {
    "path": "redisinsight/ui/src/components/settings-item/styles.module.scss",
    "content": ".input {\n  height: 31px !important;\n  font-family: 'Graphik', sans-serif !important;\n  border-bottom-right-radius: 0 !important;\n  border-top-right-radius: 0 !important;\n}\n\n.inputEditing {\n  height: 32px !important;\n}\n\n.inputHover {\n  padding-left: 10px;\n\n  & > * {\n    line-height: 3.2rem !important;\n  }\n}\n\n.container {\n  height: 40px;\n\n  :global(.euiFormControlLayout--group.euiFormControlLayout--readOnly) {\n    border: 1px solid var(--controlsBorderColor);\n    cursor: auto;\n  }\n}\n\n.inputLabel {\n  font: normal normal normal 13px/18px Graphik, sans-serif !important;\n  font-weight: 500 !important;\n}\n\n.value {\n  padding: 0 9px;\n  line-height: 3.2rem !important;\n  height: 3.2rem !important;\n  min-width: 150px;\n}\n\n.title {\n  font-size: 16px !important;\n}\n\n.smallText {\n  font: normal normal normal 14px/24px Graphik, sans-serif !important;\n  letter-spacing: -0.14px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/shortcuts-flyout/ShortcutsFlyout.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport ShortcutsFlyout from './ShortcutsFlyout'\nimport { SHORTCUTS, ShortcutGroup } from './schema'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/components/base/layout/drawer', () => ({\n  ...jest.requireActual('uiSrc/components/base/layout/drawer'),\n  DrawerHeader: jest.fn().mockReturnValue(null),\n}))\n\nconst appInfoSlicesPath = 'uiSrc/slices/app/info'\n\njest.mock(appInfoSlicesPath, () => ({\n  ...jest.requireActual(appInfoSlicesPath),\n  appInfoSelector: jest.fn().mockReturnValue({\n    ...jest.requireActual(appInfoSlicesPath).appInfoSelector,\n    isShortcutsFlyoutOpen: true,\n  }),\n}))\n\ndescribe('ShortcutsFlyout', () => {\n  it('should render', () => {\n    expect(render(<ShortcutsFlyout />)).toBeTruthy()\n  })\n\n  it('should render groups', () => {\n    render(<ShortcutsFlyout />)\n\n    SHORTCUTS.forEach((group: ShortcutGroup) => {\n      expect(\n        document.querySelector(\n          `[data-test-subj=\"shortcuts-section-${group.name}\"]`,\n        ),\n      ).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/shortcuts-flyout/ShortcutsFlyout.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { appInfoSelector, setShortcutsFlyoutState } from 'uiSrc/slices/app/info'\nimport { KeyboardShortcut } from 'uiSrc/components'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  Drawer,\n  DrawerHeader,\n  DrawerBody,\n} from 'uiSrc/components/base/layout/drawer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table'\n\nimport { SHORTCUTS, ShortcutGroup, separator } from './schema'\n\nconst ShortcutsFlyout = () => {\n  const { isShortcutsFlyoutOpen, server } = useSelector(appInfoSelector)\n\n  const dispatch = useDispatch()\n\n  const tableColumns: ColumnDefinition<any>[] = [\n    {\n      header: 'Description',\n      id: 'description',\n      accessorKey: 'description',\n      enableSorting: false,\n    },\n    {\n      header: 'Shortcut',\n      id: 'keys',\n      accessorKey: 'keys',\n      enableSorting: false,\n      cell: ({\n        row: {\n          original: { keys },\n        },\n      }) => <KeyboardShortcut items={keys} separator={separator} transparent />,\n    },\n  ]\n\n  const ShortcutsTable = ({ name, items }: ShortcutGroup) => (\n    <div key={name} data-testid={`shortcuts-table-${name}`}>\n      <Title size=\"XS\" data-test-subj={`shortcuts-section-${name}`}>\n        {name}\n      </Title>\n      <Spacer size=\"m\" />\n      <Table columns={tableColumns} data={items} defaultSorting={[]} />\n      <Spacer size=\"xl\" />\n    </div>\n  )\n\n  return (\n    <Drawer\n      open={isShortcutsFlyoutOpen}\n      onOpenChange={(isOpen) => dispatch(setShortcutsFlyoutState(isOpen))}\n      data-test-subj=\"shortcuts-flyout\"\n      title=\"Shortcuts\"\n    >\n      <DrawerHeader title=\"Shortcuts\" />\n      <DrawerBody>\n        {SHORTCUTS.filter(\n          ({ excludeFor }) =>\n            !excludeFor || !excludeFor.includes(server?.buildType as BuildType),\n        ).map(ShortcutsTable)}\n      </DrawerBody>\n    </Drawer>\n  )\n}\n\nexport default ShortcutsFlyout\n"
  },
  {
    "path": "redisinsight/ui/src/components/shortcuts-flyout/schema.tsx",
    "content": "import { KEYBOARD_SHORTCUTS } from 'uiSrc/constants'\nimport { BuildType } from 'uiSrc/constants/env'\n\nexport interface Shortcut {\n  label?: string\n  description: string\n  keys: (string | JSX.Element)[]\n}\n\nexport interface ShortcutGroup {\n  name: string\n  items: Shortcut[]\n  excludeFor?: BuildType[]\n}\n\nexport const separator = KEYBOARD_SHORTCUTS._separator\n\nexport const SHORTCUTS: ShortcutGroup[] = [\n  {\n    name: 'Desktop application',\n    excludeFor: [BuildType.RedisStack, BuildType.DockerOnPremise],\n    items: [\n      KEYBOARD_SHORTCUTS.desktop.newWindow,\n      KEYBOARD_SHORTCUTS.desktop.reloadPage,\n    ],\n  },\n  {\n    name: 'CLI',\n    items: [\n      KEYBOARD_SHORTCUTS.cli.autocompleteNext,\n      KEYBOARD_SHORTCUTS.cli.autocompletePrev,\n      KEYBOARD_SHORTCUTS.cli.clearSearch,\n      KEYBOARD_SHORTCUTS.cli.prevCommand,\n      KEYBOARD_SHORTCUTS.cli.nextCommand,\n    ],\n  },\n  {\n    name: 'Workbench',\n    items: [\n      KEYBOARD_SHORTCUTS.workbench.runQuery,\n      KEYBOARD_SHORTCUTS.workbench.nextLine,\n      KEYBOARD_SHORTCUTS.workbench.listOfCommands,\n      KEYBOARD_SHORTCUTS.workbench.triggerHints,\n      KEYBOARD_SHORTCUTS.workbench.quickHistoryAccess,\n      KEYBOARD_SHORTCUTS.workbench.nonRedisEditor,\n    ],\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/SidePanels.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledSidePanel = styled(Col)<{ isFullScreen?: boolean }>`\n  height: 100%;\n  width: 100%;\n  padding-right: ${({ theme }) => theme.core.space.space200};\n\n  ${({ isFullScreen }) =>\n    isFullScreen &&\n    css`\n      position: absolute;\n      top: 0;\n      left: 0;\n      width: 100%;\n      padding-inline: ${({ theme }) => theme.core.space.space200};\n      z-index: 1001;\n    `}\n`\n\nexport const StyledInnerSidePanel = styled(Col)<{ isFullScreen?: boolean }>`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.core.space.space100};\n  padding: ${({ theme }) => theme.core.space.space100};\n  height: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/SidePanels.test.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  getRecommendations,\n  recommendationsSelector,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  insightsPanelSelector,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n  sidePanelsSelector,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Pages } from 'uiSrc/constants'\nimport { connectedInstanceCDSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  InsightsPanelTabs,\n  SidePanels as ISidePanels,\n} from 'uiSrc/slices/interfaces/insights'\nimport { getTutorialCapability } from 'uiSrc/utils'\nimport { isShowCapabilityTutorialPopover } from 'uiSrc/services'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\n\nimport SidePanels from './SidePanels'\n\nlet store: typeof mockedStore\n\nconst mockRecommendationsSelector = {\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  content: MOCK_RECOMMENDATIONS,\n}\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    insightsRecommendations: {\n      flag: true,\n    },\n    documentationChat: {\n      flag: true,\n    },\n    databaseChat: {\n      flag: true,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'instanceId',\n    connectionType: 'CLUSTER',\n    provider: 'REDIS_CLOUD',\n  }),\n  connectedInstanceCDSelector: jest.fn().mockReturnValue({\n    free: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextCapability: jest.fn().mockReturnValue({\n    source: 'workbench RediSearch',\n  }),\n}))\n\njest.mock('uiSrc/slices/recommendations/recommendations', () => ({\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  recommendationsSelector: jest.fn().mockReturnValue({\n    data: {\n      recommendations: [],\n      totalUnread: 0,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/panels/sidePanels', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/sidePanels'),\n  insightsPanelSelector: jest.fn().mockReturnValue({\n    tabSelected: 'explore',\n  }),\n  sidePanelsSelector: jest.fn().mockReturnValue({\n    openedPanel: null,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  getTutorialCapability: jest\n    .fn()\n    .mockReturnValue({ path: 'path', telemetryName: 'searchAndQuery' }),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  isShowCapabilityTutorialPopover: jest.fn(),\n}))\n\n/**\n * SidePanels tests\n *\n * @group component\n */\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('SidePanels', () => {\n  beforeEach(() => {\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: 'instanceId' })\n  })\n  it('should render', () => {\n    expect(render(<SidePanels />)).toBeTruthy()\n  })\n\n  it('should call proper actions when recommendations tab is Open after render', () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [{ name: 'name' }],\n        totalUnread: 1,\n      },\n    }))\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'tips',\n    })\n\n    render(<SidePanels />)\n\n    const expectedActions = [getRecommendations()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should not render recommendations count with totalUnread = 0', () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementationOnce(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [],\n        totalUnread: 0,\n      },\n    }))\n\n    render(<SidePanels />)\n\n    expect(\n      screen.queryByTestId('recommendations-unread-count'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should not render recommendations count without instanceId', () => {\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: undefined })\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'tips',\n    })\n    ;(recommendationsSelector as jest.Mock).mockImplementationOnce(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [],\n        totalUnread: 7,\n      },\n    }))\n\n    render(<SidePanels />)\n    expect(\n      screen.queryByTestId('recommendations-unread-count'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render recommendations count with totalUnread > 0', () => {\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'tips',\n    })\n    ;(recommendationsSelector as jest.Mock).mockImplementationOnce(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [],\n        totalUnread: 7,\n      },\n    }))\n\n    render(<SidePanels />)\n    expect(screen.getByText(/^Tips \\(7\\)$/)).toBeVisible()\n  })\n\n  it('should call proper telemetry events on close panel', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.pubSub('instanceId') })\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'tips',\n    })\n\n    render(<SidePanels />)\n\n    fireEvent.click(screen.getByTestId('close-insights-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_PANEL_CLOSED,\n      eventData: {\n        databaseId: 'instanceId',\n        provider: 'REDIS_CLOUD',\n        page: '/pub-sub',\n        tab: 'tips',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper telemetry events on change tab', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.pubSub('instanceId') })\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'tips',\n    })\n\n    render(<SidePanels />)\n\n    fireEvent.mouseDown(screen.getByText(/^Tutorials$/))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_PANEL_TAB_CHANGED,\n      eventData: {\n        databaseId: 'instanceId',\n        prevTab: 'tips',\n        currentTab: 'explore',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper telemetry events on fullscreen', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'recommendations',\n    })\n\n    render(<SidePanels />)\n\n    fireEvent.click(screen.getByTestId('fullScreen-insights-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_PANEL_FULL_SCREEN_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n        state: 'open',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should render copilot if any chat is available', () => {\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.AiAssistant,\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: true,\n      },\n      databaseChat: {\n        flag: false,\n      },\n    })\n\n    render(<SidePanels />)\n    expect(screen.getByTestId('redis-copilot')).toBeInTheDocument()\n  })\n\n  it('should not render copilot tab if not any chats available', () => {\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.Insights,\n    })\n    ;(insightsPanelSelector as jest.Mock).mockReturnValue({\n      tabSelected: 'recommendations',\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: false,\n      },\n      databaseChat: {\n        flag: false,\n      },\n    })\n\n    render(<SidePanels />)\n    expect(screen.queryByTestId('ai-assistant-tab')).not.toBeInTheDocument()\n  })\n\n  it('should close insights if no any chats available', () => {\n    ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n      openedPanel: ISidePanels.AiAssistant,\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: false,\n      },\n      databaseChat: {\n        flag: false,\n      },\n    })\n\n    render(<SidePanels />)\n    expect(store.getActions()).toEqual([changeSidePanel(null)])\n  })\n\n  describe('capability', () => {\n    beforeEach(() => {\n      ;(connectedInstanceCDSelector as jest.Mock).mockReturnValueOnce({\n        free: true,\n      })\n      ;(isShowCapabilityTutorialPopover as jest.Mock).mockImplementation(\n        () => true,\n      )\n    })\n    it('should call store actions', () => {\n      ;(sidePanelsSelector as jest.Mock).mockReturnValue({\n        openedPanel: ISidePanels.Insights,\n      })\n      ;(insightsPanelSelector as jest.Mock).mockReturnValue({ tabSelected: '' })\n      ;(getTutorialCapability as jest.Mock).mockImplementation(() => ({\n        tutorialPage: { args: { path: 'path' } },\n      }))\n      render(<SidePanels />)\n\n      const expectedActions = [\n        resetExplorePanelSearch(),\n        setExplorePanelIsPageOpen(false),\n        changeSelectedTab(InsightsPanelTabs.Explore),\n        changeSidePanel(ISidePanels.Insights),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n      ;(getTutorialCapability as jest.Mock).mockRestore()\n    })\n    it('should call resetExplorePanelSearch if capability was not found', () => {\n      render(<SidePanels />)\n\n      const expectedActions = [\n        resetExplorePanelSearch(),\n        setExplorePanelIsPageOpen(false),\n        changeSelectedTab(InsightsPanelTabs.Explore),\n        changeSidePanel(ISidePanels.Insights),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/SidePanels.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport cx from 'classnames'\n\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useLocation, useParams } from 'react-router-dom'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\n\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  insightsPanelSelector,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n  sidePanelsSelector,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  connectedInstanceCDSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { appContextCapability } from 'uiSrc/slices/app/context'\nimport { getTutorialCapability } from 'uiSrc/utils'\nimport { isShowCapabilityTutorialPopover } from 'uiSrc/services'\nimport { EAManifestFirstKey, FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { isAnyFeatureEnabled } from 'uiSrc/utils/features'\n\nimport { CopilotPanel, InsightsPanel } from './components'\n\nimport { StyledInnerSidePanel, StyledSidePanel } from './SidePanels.styles'\n\nexport interface Props {\n  panelClassName?: string\n}\n\nconst SidePanelsWrapper = (props: Props) => {\n  const { panelClassName } = props\n  const { openedPanel } = useSelector(sidePanelsSelector)\n  const { tabSelected } = useSelector(insightsPanelSelector)\n  const { provider } = useSelector(connectedInstanceSelector)\n  const { source: capabilitySource } = useSelector(appContextCapability)\n  const { free = false } = useSelector(connectedInstanceCDSelector) ?? {}\n  const {\n    [FeatureFlags.databaseChat]: databaseChatFeature,\n    [FeatureFlags.documentationChat]: documentationChatFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n  const isAnyChatAvailable = isAnyFeatureEnabled([\n    databaseChatFeature,\n    documentationChatFeature,\n  ])\n\n  const [isFullScreen, setIsFullScreen] = useState<boolean>(false)\n\n  const history = useHistory()\n  const { pathname } = useLocation()\n  const dispatch = useDispatch()\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const pathnameRef = useRef<string>(pathname)\n\n  const page = pathname.replace(instanceId, '').replace(/^\\//g, '')\n\n  useEffect(() => {\n    if (openedPanel === SidePanels.AiAssistant && !isAnyChatAvailable) {\n      dispatch(changeSidePanel(null))\n    }\n  }, [isAnyChatAvailable, tabSelected])\n\n  useEffect(() => {\n    window.addEventListener('keydown', handleEscFullScreen)\n    return () => {\n      window.removeEventListener('keydown', handleEscFullScreen)\n    }\n  }, [isFullScreen])\n\n  useEffect(() => {\n    if (isFullScreen && pathnameRef.current !== pathname) {\n      setIsFullScreen(false)\n    }\n\n    pathnameRef.current = pathname\n  }, [pathname, isFullScreen])\n\n  useEffect(() => {\n    if (!capabilitySource || !isShowCapabilityTutorialPopover(free)) {\n      return\n    }\n\n    const tutorialCapabilityPath =\n      getTutorialCapability(capabilitySource)?.path || ''\n\n    // set 'path' with the path to capability tutorial\n    if (tutorialCapabilityPath) {\n      const search = new URLSearchParams(window.location.search)\n      search.set(\n        'path',\n        `${EAManifestFirstKey.TUTORIALS}/${tutorialCapabilityPath}`,\n      )\n      history.push({ search: search.toString() })\n    } else {\n      // reset explore if tutorial is not found\n      dispatch(resetExplorePanelSearch())\n      dispatch(setExplorePanelIsPageOpen(false))\n    }\n\n    dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n    dispatch(changeSidePanel(SidePanels.Insights))\n  }, [capabilitySource, free])\n\n  const handleEscFullScreen = (event: KeyboardEvent) => {\n    if (event?.key === keys.ESCAPE && isFullScreen) {\n      handleFullScreen()\n    }\n  }\n\n  const handleClose = () => {\n    dispatch(changeSidePanel(null))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_PANEL_CLOSED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        provider,\n        page,\n        tab: tabSelected,\n      },\n    })\n  }\n\n  const handleFullScreen = () => {\n    setIsFullScreen((value) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.INSIGHTS_PANEL_FULL_SCREEN_CLICKED,\n        eventData: {\n          databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n          state: value ? 'exit' : 'open',\n        },\n      })\n\n      return !value\n    })\n  }\n\n  return (\n    <>\n      {!!openedPanel && (\n        <StyledSidePanel\n          isFullScreen={isFullScreen}\n          className={cx(panelClassName)}\n          data-testid={`side-panels-${openedPanel}`}\n        >\n          <StyledInnerSidePanel>\n            {openedPanel === SidePanels.AiAssistant && (\n              <CopilotPanel\n                isFullScreen={isFullScreen}\n                onToggleFullScreen={handleFullScreen}\n                onClose={handleClose}\n              />\n            )}\n            {openedPanel === SidePanels.Insights && (\n              <InsightsPanel\n                isFullScreen={isFullScreen}\n                onToggleFullScreen={handleFullScreen}\n                onClose={handleClose}\n              />\n            )}\n          </StyledInnerSidePanel>\n        </StyledSidePanel>\n      )}\n    </>\n  )\n}\n\nexport default SidePanelsWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/copilot-panel/CopilotPanel.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport CopilotPanel, { Props } from './CopilotPanel'\n\nconst mockedProps = mock<Props>()\n\ndescribe('CopilotPanel', () => {\n  it('should render', () => {\n    expect(render(<CopilotPanel {...mockedProps} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/copilot-panel/CopilotPanel.tsx",
    "content": "import React, { useCallback } from 'react'\nimport { Header } from 'uiSrc/components/side-panels/components'\nimport styles from 'uiSrc/components/side-panels/styles.module.scss'\nimport AiAssistant from 'uiSrc/components/side-panels/panels/ai-assistant'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { OnboardingTour } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  isFullScreen: boolean\n  onToggleFullScreen: () => void\n  onClose: () => void\n}\n\nconst CopilotPanel = (props: Props) => {\n  const { isFullScreen, onToggleFullScreen, onClose } = props\n\n  const CopilotHeader = useCallback(\n    () => (\n      <div className={styles.assistantHeader}>\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.BROWSER_COPILOT}\n          anchorPosition={isFullScreen ? 'rightUp' : 'leftUp'}\n          anchorWrapperClassName={styles.onboardingAnchorWrapper}\n          fullSize\n        >\n          <Row>\n            <Text size=\"L\" color=\"primary\">\n              Redis Copilot\n            </Text>\n          </Row>\n        </OnboardingTour>\n      </div>\n    ),\n    [isFullScreen],\n  )\n\n  return (\n    <>\n      <Header\n        isFullScreen={isFullScreen}\n        onToggleFullScreen={onToggleFullScreen}\n        onClose={onClose}\n        panelName=\"copilot\"\n      >\n        <CopilotHeader />\n      </Header>\n      <div className={styles.body}>\n        <AiAssistant />\n      </div>\n    </>\n  )\n}\n\nexport default CopilotPanel\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/copilot-panel/index.ts",
    "content": "import CopilotPanel from './CopilotPanel'\n\nexport default CopilotPanel\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/header/Header.tsx",
    "content": "import React from 'react'\n\nimport { FullScreen } from 'uiSrc/components'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  panelName?: string\n  isFullScreen: boolean\n  onToggleFullScreen: () => void\n  onClose: () => void\n  children: React.ReactNode\n}\n\nconst Header = (props: Props) => {\n  const {\n    panelName = '',\n    isFullScreen,\n    onToggleFullScreen,\n    onClose,\n    children,\n  } = props\n  return (\n    <div className={styles.header}>\n      {children}\n      <FullScreen\n        isFullScreen={isFullScreen}\n        onToggleFullScreen={onToggleFullScreen}\n        btnTestId={`fullScreen-${panelName}-btn`}\n      />\n      <IconButton\n        icon={CancelSlimIcon}\n        aria-label=\"close insights\"\n        className={styles.closeBtn}\n        onClick={onClose}\n        data-testid={`close-${panelName}-btn`}\n      />\n    </div>\n  )\n}\n\nexport default Header\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/header/index.ts",
    "content": "import Header from './Header'\n\nexport default Header\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/header/styles.module.scss",
    "content": ".header {\n  position: relative;\n\n  padding: 12px;\n\n  display: flex;\n  align-items: center;\n}\n\n.closeBtn {\n  margin-left: 4px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/index.ts",
    "content": "import Header from './header'\nimport InsightsPanel from './insights-panel'\nimport CopilotPanel from './copilot-panel'\n\nexport { Header, InsightsPanel, CopilotPanel }\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/insights-panel/InsightsPanel.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport InsightsPanel, { Props } from './InsightsPanel'\n\nconst mockedProps = mock<Props>()\n\ndescribe('CopilotPanel', () => {\n  it('should render', () => {\n    expect(render(<InsightsPanel {...mockedProps} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/insights-panel/InsightsPanel.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { Header } from 'uiSrc/components/side-panels/components'\nimport { InsightsPanelTabs } from 'uiSrc/slices/interfaces/insights'\nimport EnablementAreaWrapper from 'uiSrc/components/side-panels/panels/enablement-area'\nimport LiveTimeRecommendations from 'uiSrc/components/side-panels/panels/live-time-recommendations'\nimport {\n  changeSelectedTab,\n  insightsPanelSelector,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { OnboardingTour } from 'uiSrc/components'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport styles from 'uiSrc/components/side-panels/styles.module.scss'\n\nexport interface Props {\n  isFullScreen: boolean\n  onToggleFullScreen: () => void\n  onClose: () => void\n}\n\nconst InsightsPanel = (props: Props) => {\n  const { isFullScreen, onToggleFullScreen, onClose } = props\n  const { tabSelected } = useSelector(insightsPanelSelector)\n  const {\n    data: { totalUnread },\n  } = useSelector(recommendationsSelector)\n\n  const dispatch = useDispatch()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const handleChangeTab = (name: InsightsPanelTabs) => {\n    if (tabSelected === name) return\n\n    dispatch(changeSelectedTab(name))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_PANEL_TAB_CHANGED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        prevTab: tabSelected,\n        currentTab: name,\n      },\n    })\n  }\n\n  const tabs: TabInfo[] = useMemo(\n    () => [\n      {\n        label: (\n          <OnboardingTour\n            options={ONBOARDING_FEATURES.EXPLORE_REDIS}\n            anchorPosition={isFullScreen ? 'rightUp' : 'leftUp'}\n            anchorWrapperClassName={styles.onboardingAnchorWrapper}\n            fullSize\n          >\n            <span>Tutorials</span>\n          </OnboardingTour>\n        ),\n        value: InsightsPanelTabs.Explore,\n        content: null,\n      },\n      {\n        label: <span>Tips {totalUnread ? ` (${totalUnread})` : ''}</span>,\n        value: InsightsPanelTabs.Recommendations,\n        content: null,\n      },\n    ],\n    [tabSelected, totalUnread, isFullScreen],\n  )\n\n  const handleTabChange = (name: string) => {\n    if (tabSelected === name) return\n    handleChangeTab(name as InsightsPanelTabs)\n  }\n\n  return (\n    <>\n      <Header\n        isFullScreen={isFullScreen}\n        onToggleFullScreen={onToggleFullScreen}\n        onClose={onClose}\n        panelName=\"insights\"\n      >\n        <Row>\n          <Text size=\"L\" color=\"primary\">\n            Insights\n          </Text>\n        </Row>\n      </Header>\n      <Col className={styles.body}>\n        <Tabs\n          tabs={tabs}\n          value={tabSelected}\n          onChange={handleTabChange}\n          className={styles.tabs}\n          data-testid=\"insights-tabs\"\n        />\n        {tabSelected === InsightsPanelTabs.Explore && <EnablementAreaWrapper />}\n        {tabSelected === InsightsPanelTabs.Recommendations && (\n          <LiveTimeRecommendations />\n        )}\n      </Col>\n    </>\n  )\n}\n\nexport default InsightsPanel\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/components/insights-panel/index.ts",
    "content": "import InsightsPanel from './InsightsPanel'\n\nexport default InsightsPanel\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/index.ts",
    "content": "import SidePanels from './SidePanels'\n\nexport default SidePanels\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/AiAssistant.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud'\n\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport AiAssistant from './AiAssistant'\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: true,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudUserSelector: jest.fn().mockReturnValue({\n    data: null,\n  }),\n}))\n\ndescribe('AiAssistant', () => {\n  it('should render', () => {\n    expect(render(<AiAssistant />)).toBeTruthy()\n  })\n\n  it('should render welcome screen with feature flag and unauthorized user', () => {\n    render(<AiAssistant />)\n\n    expect(screen.getByTestId('copilot-welcome')).toBeInTheDocument()\n  })\n\n  it('should not render welcome screen with feature flag and authorized user', () => {\n    ;(oauthCloudUserSelector as jest.Mock).mockReturnValue({ data: {} })\n    render(<AiAssistant />)\n\n    expect(screen.queryByTestId('copilot-welcome')).not.toBeInTheDocument()\n    expect(screen.getByTestId('chat-wrapper')).toBeInTheDocument()\n  })\n\n  it('should not render welcome screen without feature flag', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      cloudSso: { flag: false },\n    })\n    render(<AiAssistant />)\n\n    expect(screen.queryByTestId('copilot-welcome')).not.toBeInTheDocument()\n    expect(screen.getByTestId('chat-wrapper')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/AiAssistant.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { clearExpertChatHistory } from 'uiSrc/slices/panels/aiAssistant'\nimport { WelcomeAiAssistant, ChatsWrapper } from './components'\nimport styles from './styles.module.scss'\n\nconst AiAssistant = () => {\n  const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector)\n  const { [FeatureFlags.cloudSso]: cloudSsoFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const currentAccountIdRef = useRef(userOAuthProfile?.id)\n  const isShowAuth = cloudSsoFeature?.flag && !userOAuthProfile\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    // user logout\n    if (currentAccountIdRef.current && !userOAuthProfile?.id) {\n      dispatch(clearExpertChatHistory())\n    }\n\n    currentAccountIdRef.current = userOAuthProfile?.id\n  }, [userOAuthProfile])\n\n  return (\n    <div className={styles.wrapper} data-testid=\"redis-copilot\">\n      {isShowAuth ? <WelcomeAiAssistant /> : <ChatsWrapper />}\n    </div>\n  )\n}\n\nexport default AiAssistant\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/assistance-chat/AssistanceChat.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  expectActionsToContain,\n  fireEvent,\n  mockedStore,\n  mockedStoreFn,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  aiAssistantChatSelector,\n  createAssistantChat,\n  getAssistantChatHistory,\n  removeAssistantChatHistory,\n  sendQuestion,\n  updateAssistantChatAgreements,\n} from 'uiSrc/slices/panels/aiAssistant'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\nimport AssistanceChat from './AssistanceChat'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/panels/aiAssistant', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/aiAssistant'),\n  aiAssistantChatSelector: jest.fn().mockReturnValue({\n    id: '',\n    messages: [],\n    agreements: true,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStoreFn())\n  store.clearActions()\n})\n\ndescribe('AssistanceChat', () => {\n  it('should render', () => {\n    expect(render(<AssistanceChat />, { store })).toBeTruthy()\n  })\n\n  it('should proper components render by default', () => {\n    render(<AssistanceChat />, { store })\n\n    expect(\n      screen.getByTestId('ai-general-restart-session-btn'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('ai-chat-empty-history')).toBeInTheDocument()\n    expect(screen.getByTestId('ai-submit-message-btn')).toBeInTheDocument()\n  })\n\n  it('should call proper actions by default', () => {\n    render(<AssistanceChat />, { store })\n\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should get history', () => {\n    ;(aiAssistantChatSelector as jest.Mock).mockReturnValue({\n      id: '1',\n      messages: [],\n      agreements: true,\n    })\n    render(<AssistanceChat />, { store })\n\n    expect(store.getActions()).toEqual([getAssistantChatHistory()])\n  })\n\n  it('should call action to create an id after submit first message', () => {\n    ;(aiAssistantChatSelector as jest.Mock).mockReturnValue({\n      id: '',\n      messages: [],\n      agreements: true,\n    })\n    render(<AssistanceChat />, { store })\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n\n    expect(store.getActions()).toEqual([createAssistantChat()])\n  })\n\n  it('should call action after submit message', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiAssistantChatSelector as jest.Mock).mockReturnValue({\n      id: '1',\n      messages: [],\n      agreements: true,\n    })\n    render(<AssistanceChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      sendQuestion(expect.objectContaining({ content: 'test' })),\n    ])\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.AI_CHAT_MESSAGE_SENT,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should show agreements', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiAssistantChatSelector as jest.Mock).mockReturnValue({\n      id: '1',\n      messages: [],\n      agreements: false,\n    })\n    render(<AssistanceChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n\n    await waitForRiPopoverVisible()\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('ai-accept-agreements'))\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      updateAssistantChatAgreements(true),\n      sendQuestion(expect.objectContaining({ content: 'test' })),\n    ])\n  })\n\n  it('should call action after click on restart session', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiAssistantChatSelector as jest.Mock).mockReturnValue({\n      id: '1',\n      messages: [{}],\n      agreements: true,\n    })\n\n    render(<AssistanceChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('ai-general-restart-session-btn'))\n\n    await waitForRiPopoverVisible()\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('ai-chat-restart-confirm'))\n    })\n\n    expectActionsToContain(store.getActions(), [\n      ...afterRenderActions,\n      removeAssistantChatHistory(),\n    ])\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.AI_CHAT_SESSION_RESTARTED,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/assistance-chat/AssistanceChat.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport {\n  aiAssistantChatSelector,\n  askAssistantChatbot,\n  createAssistantChatAction,\n  getAssistantChatHistoryAction,\n  removeAssistantChatAction,\n  removeAssistantChatHistorySuccess,\n  sendQuestion,\n  updateAssistantChatAgreements,\n} from 'uiSrc/slices/panels/aiAssistant'\nimport { getCommandsFromQuery, Nullable } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { AiChatMessage, AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\n\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\n\nimport { generateHumanMessage } from 'uiSrc/utils/transformers/chatbot'\n\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { EraserIcon } from 'uiSrc/components/base/icons'\nimport { ASSISTANCE_CHAT_AGREEMENTS } from '../texts'\nimport {\n  AssistanceChatInitialMessage,\n  ChatForm,\n  ChatHistory,\n  RestartChat,\n} from '../shared'\n\nimport styles from './styles.module.scss'\n\nconst AssistanceChat = () => {\n  const { id, messages, agreements, loading } = useSelector(\n    aiAssistantChatSelector,\n  )\n  const { modules, provider } = useSelector(connectedInstanceSelector)\n  const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(\n    appRedisCommandsSelector,\n  )\n\n  const [inProgressMessage, setinProgressMessage] =\n    useState<Nullable<AiChatMessage>>(null)\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!id || messages.length) return\n\n    dispatch(getAssistantChatHistoryAction(id))\n  }, [id])\n\n  const handleSubmit = useCallback(\n    (message: string) => {\n      if (!agreements) {\n        dispatch(updateAssistantChatAgreements(true))\n        sendEventTelemetry({\n          event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,\n          eventData: {\n            chat: AiChatType.Assistance,\n          },\n        })\n      }\n\n      if (!id) {\n        dispatch(\n          createAssistantChatAction(\n            (chatId) => sendChatMessage(chatId, message),\n            // if cannot create a chat - just put message with error\n            () => {\n              dispatch(\n                sendQuestion({\n                  ...generateHumanMessage(message),\n                  error: {\n                    statusCode: 500,\n                    errorCode: CustomErrorCodes.QueryAiInternalServerError,\n                  },\n                }),\n              )\n\n              sendEventTelemetry({\n                event: TelemetryEvent.AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED,\n                eventData: {\n                  chat: AiChatType.Assistance,\n                  errorCode: 500,\n                },\n              })\n            },\n          ),\n        )\n        return\n      }\n\n      sendChatMessage(id, message)\n    },\n    [id, agreements],\n  )\n\n  const sendChatMessage = (chatId: string, message: string) => {\n    dispatch(\n      askAssistantChatbot(chatId, message, {\n        onMessage: (message: AiChatMessage) => {\n          setinProgressMessage({ ...message })\n        },\n        onError: (errorCode: number) => {\n          sendEventTelemetry({\n            event: TelemetryEvent.AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED,\n            eventData: {\n              chat: AiChatType.Assistance,\n              errorCode,\n            },\n          })\n        },\n        onFinish: () => setinProgressMessage(null),\n      }),\n    )\n\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_MESSAGE_SENT,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n  }\n\n  const onClearSession = useCallback(() => {\n    if (!id) {\n      dispatch(removeAssistantChatHistorySuccess())\n      return\n    }\n\n    dispatch(removeAssistantChatAction(id))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_SESSION_RESTARTED,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n  }, [id])\n\n  const onRunCommand = useCallback(\n    (query: string) => {\n      const command = getCommandsFromQuery(query, REDIS_COMMANDS_ARRAY) || ''\n      sendEventTelemetry({\n        event: TelemetryEvent.AI_CHAT_BOT_COMMAND_RUN_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          chat: AiChatType.Assistance,\n          provider,\n          command,\n        },\n      })\n    },\n    [instanceId, provider],\n  )\n\n  const handleAgreementsDisplay = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,\n      eventData: {\n        chat: AiChatType.Assistance,\n      },\n    })\n  }, [])\n\n  return (\n    <div className={styles.wrapper} data-testid=\"ai-general-chat\">\n      <div className={styles.header}>\n        <span />\n        <RestartChat\n          button={\n            <EmptyButton\n              disabled={!!inProgressMessage || !messages?.length}\n              icon={EraserIcon}\n              size=\"small\"\n              className={styles.headerBtn}\n              data-testid=\"ai-general-restart-session-btn\"\n            />\n          }\n          onConfirm={onClearSession}\n        />\n      </div>\n      <div className={styles.chatHistory}>\n        <ChatHistory\n          autoScroll\n          isLoading={loading}\n          modules={modules}\n          initialMessage={AssistanceChatInitialMessage}\n          inProgressMessage={inProgressMessage}\n          history={messages}\n          onRunCommand={onRunCommand}\n          onRestart={onClearSession}\n        />\n      </div>\n      <div className={styles.chatForm}>\n        <ChatForm\n          onAgreementsDisplayed={handleAgreementsDisplay}\n          agreements={!agreements ? ASSISTANCE_CHAT_AGREEMENTS : undefined}\n          placeholder=\"Ask me about Redis\"\n          isDisabled={inProgressMessage?.content === ''}\n          onSubmit={handleSubmit}\n        />\n      </div>\n    </div>\n  )\n}\n\nexport default AssistanceChat\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/assistance-chat/index.ts",
    "content": "import AssistanceChat from './AssistanceChat'\n\nexport default AssistanceChat\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/assistance-chat/styles.module.scss",
    "content": ".wrapper {\n  height: 100%;\n  width: 100%;\n\n  display: flex;\n  flex-direction: column;\n\n  .header {\n    background-color: var(--browserTableRowEven);\n    flex-shrink: 0;\n\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n\n    padding: 6px 12px;\n  }\n\n  .chatHistory {\n    flex-grow: 1;\n    overflow: hidden;\n\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n  }\n\n  .chatForm {\n    flex-shrink: 0;\n    padding: 0 12px 12px;\n  }\n\n  .headerBtn {\n    width: 24px;\n\n    :global {\n      .euiButtonEmpty__text {\n        margin-inline-start: 0;\n      }\n    }\n\n    &:disabled {\n      opacity: 0.5;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/chats-wrapper/ChatsWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  act,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\n\nimport { aiChatSelector, setSelectedTab } from 'uiSrc/slices/panels/aiAssistant'\nimport { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport ChatsWrapper from './ChatsWrapper'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/panels/aiAssistant', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/aiAssistant'),\n  aiChatSelector: jest.fn().mockReturnValue({\n    activeTab: '',\n  }),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    documentationChat: {\n      flag: true,\n    },\n    databaseChat: {\n      flag: true,\n    },\n  }),\n}))\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ChatsWrapper', () => {\n  it('should render', () => {\n    expect(render(<ChatsWrapper />)).toBeTruthy()\n  })\n\n  it('should call proper dispatch after click on tab', () => {\n    render(<ChatsWrapper />)\n\n    fireEvent.mouseDown(screen.getByText('General'))\n\n    expect(store.getActions()).toEqual([setSelectedTab(AiChatType.Assistance)])\n  })\n\n  it('should call proper dispatch after click on tab', () => {\n    render(<ChatsWrapper />)\n\n    fireEvent.mouseDown(screen.getByText('My Data'))\n\n    expect(store.getActions()).toEqual([setSelectedTab(AiChatType.Query)])\n  })\n\n  it('should render general chat when tab is selected', () => {\n    ;(aiChatSelector as jest.Mock).mockReturnValue({\n      activeTab: AiChatType.Assistance,\n    })\n    render(<ChatsWrapper />)\n\n    fireEvent.mouseDown(screen.getByText('General'))\n\n    expect(screen.getByTestId('ai-general-chat')).toBeInTheDocument()\n  })\n\n  it('should render database chat when tab is selected', () => {\n    ;(aiChatSelector as jest.Mock).mockReturnValue({\n      activeTab: AiChatType.Query,\n    })\n    render(<ChatsWrapper />)\n\n    fireEvent.mouseDown(screen.getByText('General'))\n\n    expect(screen.getByTestId('ai-document-chat')).toBeInTheDocument()\n  })\n\n  it('shoud not render tabs if chats are disabled', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: false,\n      },\n      databaseChat: {\n        flag: false,\n      },\n    })\n\n    render(<ChatsWrapper />)\n\n    expect(screen.queryByTestId('ai-general-chat_tab')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('ai-database-chat_tab')).not.toBeInTheDocument()\n  })\n\n  it('shoud not render tabs if only 1 chat is available', () => {\n    ;(aiChatSelector as jest.Mock).mockReturnValue({\n      activeTab: AiChatType.Assistance,\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: true,\n      },\n      databaseChat: {\n        flag: false,\n      },\n    })\n\n    render(<ChatsWrapper />)\n\n    expect(screen.queryByTestId('ai-general-chat_tab')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('ai-database-chat_tab')).not.toBeInTheDocument()\n\n    expect(screen.getByTestId('ai-general-chat')).toBeInTheDocument()\n  })\n\n  it('shoud switch to another chat if current is not available', async () => {\n    ;(aiChatSelector as jest.Mock).mockReturnValue({\n      activeTab: AiChatType.Query,\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: true,\n      },\n      databaseChat: {\n        flag: false,\n      },\n    })\n\n    await act(async () => {\n      render(<ChatsWrapper />)\n    })\n\n    expect(store.getActions()).toEqual([setSelectedTab(AiChatType.Assistance)])\n  })\n\n  it('should call proper telemetry after open chat', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiChatSelector as jest.Mock).mockReturnValue({\n      activeTab: AiChatType.Query,\n    })\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      documentationChat: {\n        flag: true,\n      },\n      databaseChat: {\n        flag: true,\n      },\n    })\n    render(<ChatsWrapper />)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.AI_CHAT_OPENED,\n      eventData: {\n        chat: AiChatType.Query,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/chats-wrapper/ChatsWrapper.tsx",
    "content": "import React, { useEffect } from 'react'\n\nimport { useDispatch, useSelector } from 'react-redux'\nimport { filter } from 'lodash'\nimport { aiChatSelector, setSelectedTab } from 'uiSrc/slices/panels/aiAssistant'\nimport { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { Maybe } from 'uiSrc/utils'\nimport { FeatureFlagComponent } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport AssistanceChat from '../assistance-chat'\nimport ExpertChat from '../expert-chat'\n\nimport styles from './styles.module.scss'\nimport sidePanelStyles from 'uiSrc/components/side-panels/styles.module.scss'\n\ninterface ChatWithTabs {\n  feature: Maybe<FeatureFlagComponent>\n  tab: AiChatType\n}\n\nconst ChatsWrapper = () => {\n  const { activeTab } = useSelector(aiChatSelector)\n  const {\n    [FeatureFlags.documentationChat]: documentationChatFeature,\n    [FeatureFlags.databaseChat]: databaseChatFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n\n  const chats = filter<ChatWithTabs>(\n    [\n      {\n        feature: documentationChatFeature,\n        tab: AiChatType.Assistance,\n      },\n      {\n        feature: databaseChatFeature,\n        tab: AiChatType.Query,\n      },\n    ],\n    ({ feature }) => !!feature?.flag,\n  )\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!chats.length) return\n\n    if (\n      (activeTab === AiChatType.Assistance &&\n        !documentationChatFeature?.flag) ||\n      (activeTab === AiChatType.Query && !databaseChatFeature?.flag)\n    ) {\n      dispatch(setSelectedTab(chats[0].tab))\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_OPENED,\n      eventData: {\n        chat: activeTab,\n      },\n    })\n  }, [databaseChatFeature, databaseChatFeature, activeTab])\n\n  const tabs: TabInfo[] = [\n    {\n      label: <span>General</span>,\n      value: AiChatType.Assistance,\n      content: null,\n    },\n    {\n      label: <span>My Data</span>,\n      value: AiChatType.Query,\n      content: null,\n    },\n  ].filter(\n    (tab) =>\n      (tab.value === AiChatType.Assistance && documentationChatFeature?.flag) ||\n      (tab.value === AiChatType.Query && databaseChatFeature?.flag),\n  )\n\n  const selectTab = (tab: string) => {\n    dispatch(setSelectedTab(tab as AiChatType))\n  }\n\n  return (\n    <div className={styles.wrapper} data-testid=\"chat-wrapper\">\n      {chats.length > 1 && (\n        <Tabs\n          tabs={tabs}\n          className={sidePanelStyles.tabs}\n          value={activeTab}\n          onChange={selectTab}\n          data-testid=\"ai-tabs\"\n        />\n      )}\n      {chats.length > 0 && (\n        <div className={styles.chat}>\n          {activeTab === AiChatType.Assistance &&\n            documentationChatFeature?.flag && <AssistanceChat />}\n          {activeTab === AiChatType.Query && databaseChatFeature?.flag && (\n            <ExpertChat />\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default ChatsWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/chats-wrapper/index.ts",
    "content": "import ChatsWrapper from './ChatsWrapper'\n\nexport default ChatsWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/chats-wrapper/styles.module.scss",
    "content": ".wrapper {\n  width: 100%;\n  height: 100%;\n\n  display: flex;\n  flex-direction: column;\n\n  .chat {\n    flex-grow: 1;\n    overflow: hidden;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  mockedStoreFn,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  aiExpertChatSelector,\n  clearExpertChatHistory,\n  getExpertChatHistory,\n  getExpertChatHistorySuccess,\n  sendExpertQuestion,\n  updateExpertChatAgreements,\n} from 'uiSrc/slices/panels/aiAssistant'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\nimport { apiService } from 'uiSrc/services'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { loadList } from 'uiSrc/slices/browser/redisearch'\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  resetExplorePanelSearch,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport ExpertChat from './ExpertChat'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/panels/aiAssistant', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/aiAssistant'),\n  aiExpertChatSelector: jest.fn().mockReturnValue({\n    loading: false,\n    messages: [],\n    agreements: [],\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    modules: [],\n  }),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useParams: () => ({\n    instanceId: 'instanceId',\n  }),\n}))\n\nlet store: typeof mockedStore\n\ndescribe('ExpertChat', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStoreFn())\n    store.clearActions()\n  })\n\n  it('should render', () => {\n    expect(render(<ExpertChat />, { store })).toBeTruthy()\n  })\n\n  it('should proper components render by default', () => {\n    render(<ExpertChat />, { store })\n\n    expect(\n      screen.getByTestId('ai-expert-restart-session-btn'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('ai-chat-empty-history')).toBeInTheDocument()\n    expect(screen.getByTestId('ai-submit-message-btn')).toBeInTheDocument()\n  })\n\n  it('should show loading', () => {\n    ;(aiExpertChatSelector as jest.Mock).mockReturnValue({\n      loading: true,\n      messages: [],\n      agreements: [],\n    })\n    render(<ExpertChat />, { store })\n\n    expect(screen.getByTestId('ai-loading-spinner')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('ai-chat-empty-history'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should call proper actions by default', () => {\n    render(<ExpertChat />, { store })\n\n    expect(store.getActions()).toEqual([getExpertChatHistory()])\n  })\n\n  it('should call fetch indexes', () => {\n    ;(aiExpertChatSelector as jest.Mock).mockReturnValue({\n      loading: true,\n      messages: [],\n      agreements: [],\n    })\n    ;(connectedInstanceSelector as jest.Mock).mockImplementationOnce(() => ({\n      modules: [\n        { name: RedisDefaultModules.FT },\n        { name: RedisDefaultModules.ReJSON },\n      ],\n    }))\n\n    render(<ExpertChat />, { store })\n\n    expect(store.getActions()).toEqual([getExpertChatHistory(), loadList()])\n  })\n\n  it('should call action after submit message', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiExpertChatSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      messages: [],\n      agreements: ['instanceId'],\n    })\n    render(<ExpertChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      sendExpertQuestion(expect.objectContaining({ content: 'test' })),\n    ])\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.AI_CHAT_MESSAGE_SENT,\n      eventData: {\n        chat: AiChatType.Query,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should show agreements after click submit', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiExpertChatSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      messages: [],\n      agreements: [],\n    })\n    render(<ExpertChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n\n    await waitForRiPopoverVisible()\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,\n      eventData: {\n        chat: AiChatType.Query,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('ai-accept-agreements'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,\n      eventData: {\n        chat: AiChatType.Query,\n        databaseId: 'instanceId',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      updateExpertChatAgreements('instanceId'),\n      sendExpertQuestion(expect.objectContaining({ content: 'test' })),\n    ])\n  })\n\n  it('should call action after click on restart session', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 })\n    apiService.get = jest.fn().mockResolvedValueOnce({ status: 200, data: [] })\n    ;(aiExpertChatSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      messages: [{}],\n      agreements: ['instanceId'],\n    })\n\n    render(<ExpertChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('ai-expert-restart-session-btn'))\n\n    await waitForRiPopoverVisible()\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('ai-chat-restart-confirm'))\n    })\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      getExpertChatHistorySuccess([]),\n      clearExpertChatHistory(),\n    ])\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.AI_CHAT_SESSION_RESTARTED,\n      eventData: {\n        chat: AiChatType.Query,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it.skip('should call proper actions after click tutorial in the initial message', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(aiExpertChatSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      messages: [],\n      agreements: ['instanceId'],\n    })\n\n    render(<ExpertChat />, { store })\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('tutorial-initial-message-link'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      changeSelectedTab(InsightsPanelTabs.Explore),\n      changeSidePanel(SidePanels.Insights),\n      resetExplorePanelSearch(),\n    ])\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        databaseId: 'instanceId',\n        source: 'sample_data',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport {\n  aiExpertChatSelector,\n  askExpertChatbotAction,\n  getExpertChatHistoryAction,\n  removeExpertChatHistoryAction,\n  updateExpertChatAgreements,\n} from 'uiSrc/slices/panels/aiAssistant'\nimport {\n  findTutorialPath,\n  getCommandsFromQuery,\n  isRedisearchAvailable,\n  Nullable,\n} from 'uiSrc/utils'\nimport {\n  connectedInstanceSelector,\n  freeInstancesSelector,\n} from 'uiSrc/slices/instances/instances'\n\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { AiChatMessage, AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud'\nimport { fetchRedisearchListAction } from 'uiSrc/slices/browser/redisearch'\nimport TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg?react'\nimport { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels'\nimport { TutorialsIds } from 'uiSrc/constants'\nimport NoIndexesInitialMessage from './components/no-indexes-initial-message'\nimport ExpertChatHeader from './components/expert-chat-header'\n\nimport { EXPERT_CHAT_AGREEMENTS, EXPERT_CHAT_INITIAL_MESSAGE } from '../texts'\nimport { ChatForm, ChatHistory } from '../shared'\n\nimport styles from './styles.module.scss'\n\nconst ExpertChat = () => {\n  const { messages, agreements, loading } = useSelector(aiExpertChatSelector)\n  const {\n    name: connectedInstanceName,\n    modules,\n    provider,\n  } = useSelector(connectedInstanceSelector)\n  const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(\n    appRedisCommandsSelector,\n  )\n  const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector)\n  const freeInstances = useSelector(freeInstancesSelector) || []\n\n  const [isNoIndexes, setIsNoIndexes] = useState(false)\n  const [isLoading, setIsLoading] = useState(false)\n  const [inProgressMessage, setinProgressMessage] =\n    useState<Nullable<AiChatMessage>>(null)\n\n  const currentAccountIdRef = useRef(userOAuthProfile?.id)\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const isAgreementsAccepted =\n    agreements.includes(instanceId) || messages.length > 0\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  useEffect(() => {\n    if (!instanceId) {\n      return\n    }\n\n    // changed account\n    if (currentAccountIdRef.current !== userOAuthProfile?.id) {\n      currentAccountIdRef.current = userOAuthProfile?.id\n      dispatch(getExpertChatHistoryAction(instanceId))\n      return\n    }\n\n    dispatch(getExpertChatHistoryAction(instanceId))\n  }, [instanceId, userOAuthProfile])\n\n  useEffect(() => {\n    if (!instanceId) return\n    if (!isRedisearchAvailable(modules)) return\n    if (messages.length) return\n\n    getIndexes()\n  }, [instanceId, modules])\n\n  const getIndexes = () => {\n    setIsLoading(true)\n    dispatch(\n      fetchRedisearchListAction(\n        (indexes) => {\n          setIsLoading(false)\n          setIsNoIndexes(!indexes.length)\n        },\n        () => setIsLoading(false),\n        false,\n      ),\n    )\n  }\n\n  const handleSubmit = useCallback(\n    (message: string) => {\n      if (!isAgreementsAccepted) {\n        dispatch(updateExpertChatAgreements(instanceId))\n\n        sendEventTelemetry({\n          event: TelemetryEvent.AI_CHAT_BOT_TERMS_ACCEPTED,\n          eventData: {\n            databaseId: instanceId,\n            chat: AiChatType.Query,\n          },\n        })\n      }\n\n      dispatch(\n        askExpertChatbotAction(instanceId, message, {\n          onMessage: (message: AiChatMessage) =>\n            setinProgressMessage({ ...message }),\n          onError: (errorCode: number) => {\n            sendEventTelemetry({\n              event: TelemetryEvent.AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED,\n              eventData: {\n                chat: AiChatType.Query,\n                errorCode,\n              },\n            })\n          },\n          onFinish: () => setinProgressMessage(null),\n        }),\n      )\n\n      sendEventTelemetry({\n        event: TelemetryEvent.AI_CHAT_MESSAGE_SENT,\n        eventData: {\n          chat: AiChatType.Query,\n        },\n      })\n    },\n    [instanceId, isAgreementsAccepted],\n  )\n\n  const onRunCommand = useCallback(\n    (query: string) => {\n      const command = getCommandsFromQuery(query, REDIS_COMMANDS_ARRAY) || ''\n      sendEventTelemetry({\n        event: TelemetryEvent.AI_CHAT_BOT_COMMAND_RUN_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          chat: AiChatType.Query,\n          provider,\n          command,\n        },\n      })\n    },\n    [instanceId, provider],\n  )\n\n  const onClearSession = useCallback(() => {\n    dispatch(removeExpertChatHistoryAction(instanceId))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_SESSION_RESTARTED,\n      eventData: {\n        chat: AiChatType.Query,\n      },\n    })\n  }, [])\n\n  const handleAgreementsDisplay = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_BOT_TERMS_DISPLAYED,\n      eventData: {\n        chat: AiChatType.Query,\n      },\n    })\n  }, [])\n\n  const handleClickTutorial = () => {\n    const tutorialPath = findTutorialPath({ id: TutorialsIds.RedisUseCases })\n    dispatch(openTutorialByPath(tutorialPath, history, true))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        source: 'sample_data',\n      },\n    })\n  }\n\n  const getValidationMessage = () => {\n    if (!instanceId) {\n      return {\n        title: 'Open a database',\n        content:\n          'Open your Redis database with Redis Query Engine, or create a new database to get started.',\n      }\n    }\n\n    if (!isRedisearchAvailable(modules)) {\n      return {\n        title: 'Redis Query Engine capability is not available',\n        content: freeInstances?.length\n          ? 'Use your free all-in-one Redis Cloud database to start exploring these capabilities.'\n          : 'Create a free Redis Cloud database with Redis Query Engine capability that extends the core capabilities of open-source Redis.',\n        icon: <TelescopeImg className={styles.iconTelescope} />,\n      }\n    }\n\n    return undefined\n  }\n\n  return (\n    <div className={styles.wrapper} data-testid=\"ai-document-chat\">\n      <ExpertChatHeader\n        connectedInstanceName={connectedInstanceName}\n        databaseId={instanceId}\n        isClearDisabled={!messages?.length || !instanceId}\n        onRestart={onClearSession}\n      />\n      <div className={styles.chatHistory}>\n        <ChatHistory\n          autoScroll\n          isLoading={loading || isLoading}\n          modules={modules}\n          initialMessage={\n            isNoIndexes ? (\n              <NoIndexesInitialMessage\n                onClickTutorial={handleClickTutorial}\n                onSuccess={getIndexes}\n              />\n            ) : (\n              EXPERT_CHAT_INITIAL_MESSAGE\n            )\n          }\n          inProgressMessage={inProgressMessage}\n          history={messages}\n          onRunCommand={onRunCommand}\n          onRestart={onClearSession}\n        />\n      </div>\n      <div className={styles.chatForm}>\n        <ChatForm\n          onAgreementsDisplayed={handleAgreementsDisplay}\n          agreements={\n            !isAgreementsAccepted ? EXPERT_CHAT_AGREEMENTS : undefined\n          }\n          isDisabled={!instanceId || inProgressMessage?.content === ''}\n          validation={getValidationMessage()}\n          placeholder=\"Ask me to query your data (e.g. How many road bikes?)\"\n          onSubmit={handleSubmit}\n        />\n      </div>\n    </div>\n  )\n}\n\nexport default ExpertChat\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/expert-chat-header/ExpertChatHeader.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport ExpertChatHeader from './ExpertChatHeader'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ExpertChatHeader', () => {\n  it('should render', () => {\n    expect(render(<ExpertChatHeader databaseId=\"1\" />)).toBeTruthy()\n  })\n\n  it('should render disabled restart session button', () => {\n    render(<ExpertChatHeader databaseId=\"1\" isClearDisabled />)\n\n    expect(screen.getByTestId('ai-expert-restart-session-btn')).toBeDisabled()\n  })\n\n  it('should call proper actions after click on tutorial button', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    render(<ExpertChatHeader databaseId=\"1\" />)\n\n    fireEvent.click(screen.getByTestId('ai-expert-tutorial-btn'))\n\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('ai-expert-open-tutorials'))\n\n    expect(store.getActions()).toEqual([\n      resetExplorePanelSearch(),\n      setExplorePanelIsPageOpen(false),\n      changeSelectedTab(InsightsPanelTabs.Explore),\n      changeSidePanel(SidePanels.Insights),\n    ])\n\n    expect(pushMock).toHaveBeenCalledWith({ search: '' })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        databaseId: '1',\n        source: 'chatbot_tutorials_button',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/expert-chat-header/ExpertChatHeader.tsx",
    "content": "import React, { useState } from 'react'\nimport cx from 'classnames'\n\nimport { useDispatch } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { RestartChat } from 'uiSrc/components/side-panels/panels/ai-assistant/components/shared'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { EmptyButton, PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { EraserIcon, LightBulbIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  connectedInstanceName?: string\n  databaseId: string\n  isClearDisabled?: boolean\n  onRestart: () => void\n}\n\nconst ExpertChatHeader = (props: Props) => {\n  const { databaseId, connectedInstanceName, isClearDisabled, onRestart } =\n    props\n  const [isTutorialsPopoverOpen, setIsTutorialsPopoverOpen] = useState(false)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const handleOpenTutorials = () => {\n    setIsTutorialsPopoverOpen(false)\n\n    dispatch(resetExplorePanelSearch())\n    dispatch(setExplorePanelIsPageOpen(false))\n    history.push({ search: '' })\n\n    dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n    dispatch(changeSidePanel(SidePanels.Insights))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        databaseId: databaseId || TELEMETRY_EMPTY_VALUE,\n        source: 'chatbot_tutorials_button',\n      },\n    })\n  }\n\n  return (\n    <div className={styles.header}>\n      {connectedInstanceName ? (\n        <RiTooltip\n          content={connectedInstanceName}\n          anchorClassName={styles.dbName}\n        >\n          <Text size=\"xs\" className=\"truncateText\">\n            {connectedInstanceName}\n          </Text>\n        </RiTooltip>\n      ) : (\n        <span />\n      )}\n      <div className={styles.headerActions}>\n        <RiTooltip\n          content={\n            isTutorialsPopoverOpen\n              ? undefined\n              : 'Open relevant tutorials to learn more'\n          }\n          anchorClassName={styles.headerBtnAnchor}\n          position=\"bottom\"\n        >\n          <RiPopover\n            ownFocus\n            panelClassName={cx('popoverLikeTooltip', styles.popover)}\n            anchorClassName={styles.popoverAnchor}\n            anchorPosition=\"downLeft\"\n            isOpen={isTutorialsPopoverOpen}\n            panelPaddingSize=\"m\"\n            closePopover={() => setIsTutorialsPopoverOpen(false)}\n            button={\n              <EmptyButton\n                icon={LightBulbIcon}\n                size=\"small\"\n                onClick={() => setIsTutorialsPopoverOpen(true)}\n                className={cx(styles.headerBtn)}\n                data-testid=\"ai-expert-tutorial-btn\"\n              />\n            }\n          >\n            <>\n              <Text size=\"m\" color=\"primary\">\n                Open relevant tutorials to learn more about search and query.\n              </Text>\n              <Spacer size=\"l\" />\n              <Row justify=\"end\">\n                <PrimaryButton\n                  size=\"s\"\n                  onClick={handleOpenTutorials}\n                  className={styles.openTutorialsBtn}\n                  data-testid=\"ai-expert-open-tutorials\"\n                >\n                  Open tutorials\n                </PrimaryButton>\n              </Row>\n            </>\n          </RiPopover>\n        </RiTooltip>\n        <RestartChat\n          button={\n            <EmptyButton\n              disabled={isClearDisabled}\n              icon={EraserIcon}\n              size=\"small\"\n              className={styles.headerBtn}\n              data-testid=\"ai-expert-restart-session-btn\"\n            />\n          }\n          onConfirm={onRestart}\n        />\n      </div>\n    </div>\n  )\n}\n\nexport default ExpertChatHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/expert-chat-header/index.ts",
    "content": "import ExpertChatHeader from './ExpertChatHeader'\n\nexport default ExpertChatHeader\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/expert-chat-header/styles.module.scss",
    "content": ".header {\n  background-color: var(--browserTableRowEven);\n  flex-shrink: 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  padding: 6px 12px;\n\n  .dbName {\n    overflow: hidden;\n    margin-right: 16px;\n\n    :global(.euiText) {\n      font-weight: 500;\n    }\n  }\n\n  .headerActions {\n    display: flex;\n    align-items: center;\n  }\n\n  .headerBtn {\n    width: 24px;\n\n    :global {\n      .euiButtonEmpty__text {\n        margin-inline-start: 0;\n      }\n    }\n\n    &:disabled {\n      opacity: 0.5;\n    }\n  }\n\n  .headerBtnAnchor {\n    margin-left: 4px;\n  }\n}\n\n.popover {\n  min-width: 300px !important;\n}\n\n.openTutorialsBtn {\n  display: block !important;\n  margin-left: auto;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/no-indexes-initial-message/NoIndexesInitialMessage.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport NoIndexesInitialMessage from './NoIndexesInitialMessage'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('NoIndexesInitialMessage', () => {\n  it('should render', () => {\n    expect(render(<NoIndexesInitialMessage />)).toBeTruthy()\n  })\n\n  it('should render load sample data button', () => {\n    render(<NoIndexesInitialMessage />)\n\n    expect(screen.getByTestId('load-sample-data-btn')).toBeInTheDocument()\n  })\n\n  it('should call telemetry on init', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<NoIndexesInitialMessage />)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.AI_CHAT_BOT_NO_INDEXES_MESSAGE_DISPLAYED,\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/no-indexes-initial-message/NoIndexesInitialMessage.tsx",
    "content": "import React, { useEffect } from 'react'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport LoadSampleData from 'uiSrc/pages/browser/components/load-sample-data'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  onSuccess?: () => void\n  onClickTutorial: () => void\n}\n\nconst NoIndexesInitialMessage = (props: Props) => {\n  const { onSuccess, onClickTutorial } = props\n\n  useEffect(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AI_CHAT_BOT_NO_INDEXES_MESSAGE_DISPLAYED,\n    })\n  }, [])\n\n  return (\n    <div data-testid=\"no-indexes-chat-message\">\n      <Text size=\"xs\">Hi!</Text>\n      <Text size=\"xs\">\n        I am here to help you get started with data querying. I noticed that you\n        have no indexes created.\n      </Text>\n      <Spacer />\n      <Text size=\"xs\">\n        Would you like to load the sample data and indexes (from this{' '}\n        <Link\n          size=\"S\"\n          variant=\"inline\"\n          color=\"subdued\"\n          onClick={onClickTutorial}\n          data-testid=\"tutorial-initial-message-link\"\n        >\n          tutorial\n        </Link>\n        ) to see what Redis Copilot can help you do?\n      </Text>\n      <Spacer />\n      <LoadSampleData\n        anchorClassName={styles.anchorClassName}\n        onSuccess={onSuccess}\n      />\n      <Spacer />\n    </div>\n  )\n}\n\nexport default NoIndexesInitialMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/no-indexes-initial-message/index.ts",
    "content": "import NoIndexesInitialMessage from './NoIndexesInitialMessage'\n\nexport default NoIndexesInitialMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/components/no-indexes-initial-message/styles.module.scss",
    "content": ".anchorClassName {\n  :global {\n    .euiButton {\n      height: 28px !important;\n      line-height: 28px !important;\n\n      .euiButton__text {\n        font-size: 13px !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/index.ts",
    "content": "import ExpertChat from './ExpertChat'\n\nexport default ExpertChat\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/styles.module.scss",
    "content": ".wrapper {\n  height: 100%;\n  width: 100%;\n\n  display: flex;\n  flex-direction: column;\n\n  .chatHistory {\n    flex-grow: 1;\n    overflow: hidden;\n\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n  }\n\n  .chatForm {\n    flex-shrink: 0;\n    padding: 0 12px 12px;\n  }\n\n  .startSessionBtn {\n    width: 24px;\n\n    &:disabled {\n      opacity: 0.5;\n    }\n\n    :global {\n      .euiButtonEmpty__text {\n        font-size: 12px;\n        line-height: 14px;\n      }\n    }\n  }\n\n  :global {\n    .defaultLink {\n      color: var(--externalLinkColor) !important;\n    }\n  }\n}\n\n.iconTelescope {\n  width: 80px !important;\n  height: 60px !important;\n  margin-left: 12px;\n\n  transform: rotateY(180deg) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/index.ts",
    "content": "import WelcomeAiAssistant from './welcome-ai-assistant'\nimport ChatsWrapper from './chats-wrapper'\n\nexport { WelcomeAiAssistant, ChatsWrapper }\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-form/ChatForm.spec.tsx",
    "content": "import React from 'react'\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport ChatForm from './ChatForm'\n\ndescribe('ChatForm', () => {\n  it('should render', () => {\n    expect(render(<ChatForm onSubmit={jest.fn()} />)).toBeTruthy()\n  })\n\n  it('should submit value', () => {\n    const onSubmit = jest.fn()\n    render(<ChatForm onSubmit={onSubmit} />)\n\n    expect(screen.getByTestId('ai-submit-message-btn')).toBeDisabled()\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n\n    expect(onSubmit).toHaveBeenCalledWith('test')\n  })\n\n  it('should submit by enter', () => {\n    const onSubmit = jest.fn()\n    render(<ChatForm onSubmit={onSubmit} />)\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    fireEvent.keyDown(screen.getByTestId('ai-message-textarea'), {\n      key: 'Enter',\n    })\n\n    expect(onSubmit).toHaveBeenCalledWith('test')\n  })\n\n  it('should show agreements popover', async () => {\n    const onSubmit = jest.fn()\n    render(\n      <ChatForm\n        onSubmit={onSubmit}\n        agreements={<div data-testid=\"agreements\" />}\n      />,\n    )\n\n    act(() => {\n      fireEvent.change(screen.getByTestId('ai-message-textarea'), {\n        target: { value: 'test' },\n      })\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('ai-submit-message-btn'))\n    })\n    await waitForRiPopoverVisible()\n\n    expect(onSubmit).not.toHaveBeenCalled()\n\n    expect(screen.getByTestId('ai-submit-message-btn')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('ai-accept-agreements'))\n    })\n\n    expect(onSubmit).toHaveBeenCalledWith('test')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-form/ChatForm.tsx",
    "content": "import React, { Ref, useRef, useState } from 'react'\n\nimport cx from 'classnames'\nimport { isModifiedEvent } from 'uiSrc/services'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { SendIcon } from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { TextArea } from 'uiSrc/components/base/inputs'\nimport * as keys from 'uiSrc/constants/keys'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  validation?: {\n    title?: React.ReactNode\n    content?: React.ReactNode\n    icon?: React.ReactNode\n  }\n  agreements?: React.ReactNode\n  onAgreementsDisplayed?: () => void\n  isDisabled?: boolean\n  placeholder?: string\n  onSubmit: (value: string) => void\n}\n\nconst INDENT_TEXTAREA_SPACE = 2\n\nconst ChatForm = (props: Props) => {\n  const {\n    validation,\n    agreements,\n    onAgreementsDisplayed,\n    isDisabled,\n    placeholder,\n    onSubmit,\n  } = props\n  const [value, setValue] = useState('')\n  const [isAgreementsPopoverOpen, setIsAgreementsPopoverOpen] = useState(false)\n  const textAreaRef: Ref<HTMLTextAreaElement> = useRef(null)\n\n  const updateTextAreaHeight = (initialState = false) => {\n    if (!textAreaRef.current) return\n\n    textAreaRef.current.style.height = '0px'\n\n    if (initialState) return\n\n    textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight + INDENT_TEXTAREA_SPACE}px`\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (isModifiedEvent(e)) return\n\n    if (e.key === keys.ENTER) {\n      e.preventDefault()\n      handleSubmitMessage()\n    }\n  }\n\n  const handleChange = (value: string) => {\n    setValue(value)\n    updateTextAreaHeight()\n  }\n\n  const handleSubmitForm = (e: React.MouseEvent<HTMLFormElement>) => {\n    e?.preventDefault()\n    handleSubmitMessage()\n  }\n\n  const handleSubmitMessage = () => {\n    if (!value || isDisabled) return\n\n    if (agreements) {\n      setIsAgreementsPopoverOpen(true)\n      onAgreementsDisplayed?.()\n      return\n    }\n\n    submitMessage()\n  }\n\n  const submitMessage = () => {\n    setIsAgreementsPopoverOpen(false)\n\n    onSubmit?.(value)\n    setValue('')\n    updateTextAreaHeight(true)\n  }\n\n  return (\n    <div>\n      <RiTooltip\n        content={\n          validation ? (\n            <div className={styles.tooltipContent}>\n              <div>\n                {validation.title && (\n                  <>\n                    <Title size=\"S\">{validation.title}</Title>\n                    <Spacer size=\"s\" />\n                  </>\n                )}\n                {validation.content && (\n                  <Text size=\"m\">{validation.content}</Text>\n                )}\n              </div>\n              {validation.icon}\n            </div>\n          ) : undefined\n        }\n        className={styles.validationTooltip}\n      >\n        <form\n          className={cx(styles.wrapper, {\n            [styles.isFormDisabled]: validation,\n          })}\n          onSubmit={handleSubmitForm}\n          onKeyDown={handleKeyDown}\n          role=\"presentation\"\n        >\n          <TextArea\n            ref={textAreaRef}\n            placeholder={placeholder || 'Ask me about Redis'}\n            value={value}\n            onChange={handleChange}\n            disabled={!!validation}\n            data-testid=\"ai-message-textarea\"\n          />\n          <RiPopover\n            ownFocus\n            isOpen={isAgreementsPopoverOpen}\n            anchorPosition=\"downRight\"\n            closePopover={() => setIsAgreementsPopoverOpen(false)}\n            panelClassName={cx('popoverLikeTooltip', styles.popover)}\n            anchorClassName={styles.popoverAnchor}\n            button={\n              <PrimaryButton\n                size=\"s\"\n                disabled={!value.length || isDisabled}\n                className={styles.submitBtn}\n                icon={SendIcon}\n                type=\"submit\"\n                aria-label=\"submit\"\n                data-testid=\"ai-submit-message-btn\"\n              />\n            }\n          >\n            <>\n              {agreements}\n              <Spacer size=\"l\" />\n              <Row justify=\"end\">\n                <PrimaryButton\n                  size=\"s\"\n                  className={styles.agreementsAccept}\n                  onClick={submitMessage}\n                  onKeyDown={(e: React.KeyboardEvent) => e.stopPropagation()}\n                  type=\"button\"\n                  data-testid=\"ai-accept-agreements\"\n                >\n                  I accept\n                </PrimaryButton>\n              </Row>\n            </>\n          </RiPopover>\n        </form>\n      </RiTooltip>\n      <Spacer size=\"xs\" />\n      <Text textAlign=\"center\" size=\"xs\" className={styles.agreementText}>\n        Verify the accuracy of any information provided by Redis Copilot before\n        using it\n      </Text>\n    </div>\n  )\n}\n\nexport default React.memo(ChatForm)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-form/index.ts",
    "content": "import ChatForm from './ChatForm'\n\nexport default ChatForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-form/styles.module.scss",
    "content": ".wrapper {\n  position: relative;\n\n  &.isFormDisabled {\n    cursor: not-allowed;\n    pointer-events: none;\n  }\n\n  .submitBtn {\n    width: 24px !important;\n    height: 24px !important;\n    min-width: 24px !important;\n\n    :global {\n      .euiButton__content {\n        padding: 0;\n      }\n\n      .euiButton__text {\n        display: none;\n      }\n    }\n  }\n}\n\n.validationTooltip {\n  max-width: 340px !important;\n}\n\n.tooltipContent {\n  display: flex;\n  align-items: center;\n}\n\n.popoverAnchor {\n  position: absolute;\n  bottom: 6px;\n  right: 10px;\n  z-index: 1;\n\n  width: 24px !important;\n  height: 24px !important;\n  min-width: 24px !important;\n}\n\n.popover {\n  max-width: 324px !important;\n\n  .agreementsAccept {\n    display: block;\n    margin-left: auto;\n  }\n}\n\n.agreementText {\n  margin-top: 6px;\n  font-size: 10px !important;\n  line-height: 1.15 !important;\n  color: var(--euiColorMediumShade) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-history/ChatHistory.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport {\n  AiChatMessage,\n  AiChatMessageType,\n} from 'uiSrc/slices/interfaces/aiAssistant'\nimport ChatHistory, { Props } from './ChatHistory'\n\nconst mockedProps = mock<Props>()\n\nconst history: AiChatMessage[] = [\n  {\n    content: '1',\n    type: AiChatMessageType.HumanMessage,\n    id: '1',\n  },\n  {\n    content: '2',\n    type: AiChatMessageType.AIMessage,\n    id: '2',\n  },\n  {\n    content: '3',\n    type: AiChatMessageType.HumanMessage,\n    id: '3',\n  },\n  {\n    content: '4',\n    type: AiChatMessageType.AIMessage,\n    id: '4',\n  },\n]\n\ndescribe('ChatHistory', () => {\n  it('should render', () => {\n    expect(render(<ChatHistory {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render intial chat message', () => {\n    render(\n      <ChatHistory\n        {...mockedProps}\n        history={[]}\n        initialMessage={<div data-testid=\"initial-message\" />}\n      />,\n    )\n\n    expect(screen.getByTestId('initial-message')).toBeInTheDocument()\n  })\n\n  it('should render loading answer indicator', () => {\n    render(\n      <ChatHistory\n        {...mockedProps}\n        history={history}\n        inProgressMessage={{ content: '' } as any}\n      />,\n    )\n\n    expect(screen.getByTestId('ai-loading-answer')).toBeInTheDocument()\n  })\n\n  it('should render history', () => {\n    render(<ChatHistory {...mockedProps} history={history} />)\n\n    history.forEach(({ id, type }) => {\n      expect(screen.getByTestId(`ai-message-${type}_${id}`)).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-history/ChatHistory.styles.ts",
    "content": "import styled from 'styled-components'\nimport { AiChatMessageType } from 'uiSrc/slices/interfaces/aiAssistant'\n\nexport const HistoryWrapper = styled.div`\n  width: 100%;\n  height: 100%;\n`\n\nexport const HistoryContainer = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  overflow-y: auto;\n  padding: 8px 12px;\n\n  > :first-child {\n    margin-top: auto;\n  }\n`\n\nexport const MessageWrapper = styled.div<{\n  messageType: AiChatMessageType\n}>`\n  max-width: 90%;\n  margin: 8px 0;\n  align-self: ${({ messageType }) =>\n    messageType === AiChatMessageType.AIMessage ? 'flex-start' : 'flex-end'};\n`\n\nexport const MessageContainer = styled.div<{\n  messageType: AiChatMessageType\n  hasError?: boolean\n}>`\n  overflow-wrap: break-word;\n  padding: 8px 16px;\n  border-radius: 8px;\n  gap: 6px;\n\n  ${({ messageType, theme }) =>\n    messageType === AiChatMessageType.AIMessage &&\n    `\n    background-color: ${theme.components.button.variants.primary.disabled?.bgColor};\n  `}\n\n  ${({ messageType, theme }) =>\n    messageType === AiChatMessageType.HumanMessage &&\n    `\n    background-color: ${theme.components.button.variants['secondary-invert'].normal?.bgColor};\n    color: ${theme.components.button.variants['secondary-invert'].normal?.textColor};\n  `}\n  \n  ${({ hasError }) =>\n    hasError &&\n    `\n    opacity: .66;\n    display: flex;\n  `}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-history/ChatHistory.tsx",
    "content": "import React, {\n  MutableRefObject,\n  Ref,\n  useCallback,\n  useEffect,\n  useRef,\n} from 'react'\n\nimport { throttle } from 'lodash'\nimport {\n  AiChatMessage,\n  AiChatMessageType,\n} from 'uiSrc/slices/interfaces/aiAssistant'\nimport { Nullable, scrollIntoView } from 'uiSrc/utils'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\n\nimport LoadingMessage from '../loading-message'\nimport MarkdownMessage from '../markdown-message'\nimport ErrorMessage from '../error-message'\n\nimport {\n  HistoryContainer,\n  HistoryWrapper,\n  MessageContainer,\n  MessageWrapper,\n} from './ChatHistory.styles'\n\nexport interface Props {\n  autoScroll?: boolean\n  isLoading?: boolean\n  initialMessage: React.ReactNode\n  inProgressMessage?: Nullable<AiChatMessage>\n  modules?: AdditionalRedisModule[]\n  history: AiChatMessage[]\n  onMessageRendered?: () => void\n  onRunCommand?: (query: string) => void\n  onRestart: () => void\n}\n\nconst SCROLL_THROTTLE_MS = 200\n\nconst ChatHistory = (props: Props) => {\n  const {\n    autoScroll,\n    isLoading,\n    initialMessage,\n    inProgressMessage,\n    modules,\n    history = [],\n    onMessageRendered,\n    onRunCommand,\n    onRestart,\n  } = props\n\n  const scrollDivRef: Ref<HTMLDivElement> = useRef(null)\n  const listRef: Ref<HTMLDivElement> = useRef(null)\n  const observerRef: MutableRefObject<Nullable<MutationObserver>> = useRef(null)\n  const scrollBehavior = useRef<ScrollBehavior>('auto')\n\n  useEffect(() => {\n    if (!autoScroll) return undefined\n    if (!listRef.current) return undefined\n\n    scrollBehavior.current = inProgressMessage ? 'smooth' : 'auto'\n\n    if (!inProgressMessage) scrollToBottom()\n    if (inProgressMessage?.content === '') scrollToBottomThrottled()\n\n    if (!observerRef.current) {\n      const observerCallback: MutationCallback = (mutationsList) => {\n        // eslint-disable-next-line no-restricted-syntax\n        for (const mutation of mutationsList) {\n          if (mutation.type === 'childList') {\n            scrollBehavior.current === 'smooth'\n              ? scrollToBottomThrottled()\n              : scrollToBottom()\n            break\n          }\n        }\n      }\n\n      observerRef.current = new MutationObserver(observerCallback)\n    }\n\n    observerRef.current.observe(listRef.current, {\n      childList: true,\n      subtree: true,\n    })\n\n    return () => {\n      observerRef.current?.disconnect()\n    }\n  }, [autoScroll, inProgressMessage, history])\n\n  const scrollToBottom = (behavior: ScrollBehavior = 'auto') => {\n    requestAnimationFrame(() => {\n      scrollIntoView(scrollDivRef?.current, {\n        behavior,\n        block: 'start',\n        inline: 'start',\n      })\n    })\n  }\n  const scrollToBottomThrottled = throttle(\n    () => scrollToBottom('smooth'),\n    SCROLL_THROTTLE_MS,\n  )\n\n  const getMessage = useCallback(\n    (message?: Nullable<AiChatMessage>) => {\n      if (!message) return null\n\n      const { id, content, error, type: messageType } = message\n      if (!content) return null\n\n      return (\n        <React.Fragment key={id}>\n          <MessageWrapper as=\"div\" messageType={messageType}>\n            <MessageContainer\n              as=\"div\"\n              className=\"jsx-markdown\"\n              messageType={messageType}\n              hasError={!!error}\n              data-testid={`ai-message-${messageType}_${id}`}\n            >\n              {error && (\n                <RiIcon type=\"ToastDangerIcon\" size=\"M\" color=\"danger500\" />\n              )}\n              {messageType === AiChatMessageType.HumanMessage ? (\n                content\n              ) : (\n                <MarkdownMessage\n                  onRunCommand={onRunCommand}\n                  onMessageRendered={onMessageRendered}\n                  modules={modules}\n                >\n                  {content}\n                </MarkdownMessage>\n              )}\n            </MessageContainer>\n          </MessageWrapper>\n          <ErrorMessage error={error} onRestart={onRestart} />\n        </React.Fragment>\n      )\n    },\n    [modules],\n  )\n\n  if (isLoading) {\n    return (\n      <HistoryWrapper>\n        <Loader size=\"xl\" data-testid=\"ai-loading-spinner\" />\n      </HistoryWrapper>\n    )\n  }\n\n  if (history.length === 0) {\n    return (\n      <HistoryWrapper>\n        <HistoryContainer as=\"div\" data-testid=\"ai-chat-empty-history\">\n          <MessageWrapper as=\"div\" messageType={AiChatMessageType.AIMessage}>\n            <MessageContainer\n              as=\"div\"\n              messageType={AiChatMessageType.AIMessage}\n              data-testid=\"ai-message-initial-message\"\n            >\n              {initialMessage}\n            </MessageContainer>\n          </MessageWrapper>\n        </HistoryContainer>\n      </HistoryWrapper>\n    )\n  }\n\n  const { content } = inProgressMessage || {}\n\n  return (\n    <HistoryWrapper>\n      <HistoryContainer as=\"div\" ref={listRef} data-testid=\"ai-chat-history\">\n        {history.map(getMessage)}\n        {getMessage(inProgressMessage)}\n        {content === '' && (\n          <MessageWrapper as=\"div\" messageType={AiChatMessageType.AIMessage}>\n            <MessageContainer\n              as=\"div\"\n              messageType={AiChatMessageType.AIMessage}\n              data-testid=\"ai-loading-answer\"\n            >\n              <LoadingMessage />\n            </MessageContainer>\n          </MessageWrapper>\n        )}\n        <div ref={scrollDivRef} />\n      </HistoryContainer>\n    </HistoryWrapper>\n  )\n}\n\nexport default React.memo(ChatHistory)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-history/index.ts",
    "content": "import ChatHistory from './ChatHistory'\n\nexport default ChatHistory\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/chat-history/texts.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nexport const AssistanceChatInitialMessage = (\n  <>\n    <Text size=\"xs\">Hi!</Text>\n    <Text size=\"xs\">\n      Feel free to engage in a general conversation with me about Redis.\n    </Text>\n    <Text size=\"xs\">\n      Or switch to <b>My Data</b> tab to get assistance in the context of your\n      data.\n    </Text>\n    <Text size=\"xs\">\n      Type <b>/help</b> for more info.\n    </Text>\n    <Spacer />\n    <Text size=\"xs\">\n      With <span style={{ color: 'red' }}>&hearts;</span>, your Redis Copilot!\n    </Text>\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/error-message/ErrorMessage.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { AI_CHAT_ERRORS } from 'uiSrc/constants/apiErrors'\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport ErrorMessage from './ErrorMessage'\n\ndescribe('ErrorMessage', () => {\n  it('should render', () => {\n    expect(render(<ErrorMessage onRestart={jest.fn} />)).toBeTruthy()\n  })\n\n  it('should render error', () => {\n    const onRestart = jest.fn()\n    render(<ErrorMessage onRestart={onRestart} error={{ statusCode: 404 }} />)\n\n    expect(screen.getByTestId('ai-chat-error-message')).toHaveTextContent(\n      AI_CHAT_ERRORS.default(),\n    )\n    expect(screen.getByTestId('ai-chat-error-report-link')).toBeInTheDocument()\n\n    expect(\n      screen.getByTestId('ai-chat-error-restart-session-btn'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('ai-chat-error-report-link')).toBeInTheDocument()\n  })\n\n  it('should render rate limit error', () => {\n    const error = {\n      errorCode: CustomErrorCodes.QueryAiRateLimitRequest,\n      statusCode: 429,\n      details: {\n        limiterType: 'request',\n        limiterKind: 'user',\n        limiterSeconds: 100,\n      },\n    }\n    render(<ErrorMessage onRestart={jest.fn} error={error} />)\n\n    expect(screen.getByTestId('ai-chat-error-message')).toHaveTextContent(\n      'Exceeded rate limit. Try again in 1 minute.',\n    )\n\n    expect(\n      screen.queryByTestId('ai-chat-error-restart-session-btn'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('ai-chat-error-report-link'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render tokens limit error', () => {\n    const error = {\n      errorCode: CustomErrorCodes.QueryAiRateLimitMaxTokens,\n      statusCode: 413,\n      details: { tokenLimit: 20000, tokenCount: 575 },\n    }\n    render(<ErrorMessage onRestart={jest.fn} error={error} />)\n\n    expect(screen.getByTestId('ai-chat-error-message')).toHaveTextContent(\n      AI_CHAT_ERRORS.tokenLimit(),\n    )\n\n    expect(\n      screen.getByTestId('ai-chat-error-restart-session-btn'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('ai-chat-error-report-link'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should not render restart button with timeout error', () => {\n    const onRestart = jest.fn()\n    render(<ErrorMessage onRestart={onRestart} error={{ statusCode: 408 }} />)\n\n    expect(screen.getByTestId('ai-chat-error-message')).toHaveTextContent(\n      AI_CHAT_ERRORS.timeout(),\n    )\n    expect(screen.getByTestId('ai-chat-error-report-link')).toBeInTheDocument()\n\n    expect(\n      screen.queryByTestId('ai-chat-error-restart-session-btn'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/error-message/ErrorMessage.tsx",
    "content": "import React from 'react'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { AI_CHAT_ERRORS } from 'uiSrc/constants/apiErrors'\nimport ApiStatusCode from 'uiSrc/constants/apiStatusCode'\n\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport RestartChat from '../restart-chat'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  error?: {\n    statusCode: number\n    errorCode?: number\n    details?: Record<string, any>\n  }\n  onRestart: () => void\n}\n\nconst ERROR_CODES_WITHOUT_RESTART = [\n  CustomErrorCodes.CloudApiUnauthorized,\n  CustomErrorCodes.QueryAiInternalServerError,\n  CustomErrorCodes.QueryAiRateLimitRequest,\n  CustomErrorCodes.QueryAiRateLimitToken,\n]\n\nconst ERROR_CODES_WITHOUT_REPORT_ISSUE = [\n  CustomErrorCodes.QueryAiRateLimitRequest,\n  CustomErrorCodes.QueryAiRateLimitToken,\n  CustomErrorCodes.QueryAiRateLimitMaxTokens,\n]\n\nconst ErrorMessage = (props: Props) => {\n  const { error, onRestart } = props\n\n  const getErrorMessage = (error?: {\n    statusCode: number\n    errorCode?: number\n    details?: Record<string, any>\n  }): string => {\n    const { statusCode, errorCode, details } = error || {}\n\n    if (statusCode === ApiStatusCode.Timeout) return AI_CHAT_ERRORS.timeout()\n    if (errorCode === CustomErrorCodes.QueryAiInternalServerError)\n      return AI_CHAT_ERRORS.unexpected()\n    if (\n      errorCode === CustomErrorCodes.QueryAiRateLimitRequest ||\n      errorCode === CustomErrorCodes.QueryAiRateLimitToken\n    )\n      return AI_CHAT_ERRORS.rateLimit(details?.limiterSeconds)\n    if (errorCode === CustomErrorCodes.QueryAiRateLimitMaxTokens)\n      return AI_CHAT_ERRORS.tokenLimit()\n\n    return AI_CHAT_ERRORS.default()\n  }\n\n  if (!error) return null\n\n  const isShowRestart =\n    !(\n      error.errorCode && ERROR_CODES_WITHOUT_RESTART.includes(error.errorCode)\n    ) && error.statusCode !== ApiStatusCode.Timeout\n  const isShowReportIssue = !(\n    error.errorCode &&\n    ERROR_CODES_WITHOUT_REPORT_ISSUE.includes(error.errorCode)\n  )\n\n  return (\n    <>\n      <div className={styles.errorMessage} data-testid=\"ai-chat-error-message\">\n        {getErrorMessage(error)}\n        {isShowReportIssue && (\n          <>\n            {' '}\n            <a\n              className=\"link-underline\"\n              href={EXTERNAL_LINKS.githubIssues}\n              data-testid=\"ai-chat-error-report-link\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              report the issue\n            </a>\n          </>\n        )}\n      </div>\n      {isShowRestart && (\n        <RestartChat\n          anchorClassName={styles.restartSessionWrapper}\n          button={\n            <SecondaryButton\n              size=\"s\"\n              icon={DeleteIcon}\n              className={styles.restartSessionBtn}\n              data-testid=\"ai-chat-error-restart-session-btn\"\n            >\n              Restart session\n            </SecondaryButton>\n          }\n          onConfirm={onRestart}\n        />\n      )}\n    </>\n  )\n}\n\nexport default ErrorMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/error-message/index.ts",
    "content": "import ErrorMessage from './ErrorMessage'\n\nexport default ErrorMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/error-message/styles.module.scss",
    "content": ".errorMessage {\n  color: var(--euiColorMediumShade);\n  font-size: 11px;\n  font-style: italic;\n  max-width: 80%;\n  text-align: right;\n  align-self: flex-end;\n\n  :global(a) {\n    color: var(--euiColorMediumShade) !important;\n  }\n}\n\n.restartSessionWrapper {\n  display: flex !important;\n  justify-content: center;\n  margin-top: 6px;\n  margin-bottom: 4px;\n}\n\n.restartSessionBtn {\n  :global(.euiButton__text) {\n    font-size: 12px !important;\n    font-weight: 400 !important;\n  }\n\n  :global(.euiIcon) {\n    width: 12px;\n    height: 12px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/index.ts",
    "content": "import ChatHistory from './chat-history'\nimport ChatForm from './chat-form'\nimport RestartChat from './restart-chat'\n\nexport * from './chat-history/texts'\n\nexport { ChatHistory, ChatForm, RestartChat }\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/loading-message/LoadingMessage.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport LoadingMessage from './LoadingMessage'\n\ndescribe('LoadingMessage', () => {\n  it('should render', () => {\n    expect(render(<LoadingMessage />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/loading-message/LoadingMessage.tsx",
    "content": "import React from 'react'\n\nimport styles from './styles.module.scss'\n\nconst LoadingMessage = () => (\n  <div className={styles.loader}>\n    {/* eslint-disable-next-line react/no-array-index-key */}\n    {Array.from({ length: 3 }).map((_, i) => (\n      <div key={`dot_${i}`} className={styles.dot} />\n    ))}\n  </div>\n)\n\nexport default LoadingMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/loading-message/index.ts",
    "content": "import LoadingMessage from './LoadingMessage'\n\nexport default LoadingMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/loading-message/styles.module.scss",
    "content": "\n.loader {\n  position: relative;\n  padding: 4px 0;\n\n  .dot {\n    display: inline-block;\n    width: 8px;\n    height: 8px;\n    border-radius: 50%;\n    background: var(--euiTextSubduedColor);\n    animation: bounce 1s linear infinite;\n\n    &:not(:last-child) {\n      margin-right: 3px;\n    }\n\n    &:nth-child(2) {\n      animation-delay: .25s;\n    }\n\n    &:nth-child(3) {\n      animation-delay: .5s;\n    }\n\n  }\n}\n\n\n@keyframes bounce {\n  0%, 40%, 100% {\n    transform: initial;\n  }\n\n  20% {\n    transform: translateY(-4px);\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/MarkdownMessage.spec.tsx",
    "content": "import React from 'react'\nimport { render, act, screen } from 'uiSrc/utils/test-utils'\n\nimport MarkdownMessage from './MarkdownMessage'\n\ndescribe('MarkdownMessage', () => {\n  it('should render', () => {\n    expect(render(<MarkdownMessage>1</MarkdownMessage>)).toBeTruthy()\n  })\n\n  it('should render 2', async () => {\n    await act(() => {\n      render(<MarkdownMessage>1</MarkdownMessage>)\n    })\n\n    screen.debug(undefined, 100_000)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/MarkdownMessage.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport JsxParser from 'react-jsx-parser'\nimport MarkdownToJsxString from 'uiSrc/services/formatter/MarkdownToJsxString'\nimport { CloudLink } from 'uiSrc/components/markdown'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport { ChatExternalLink, CodeBlock } from './components'\n\nexport interface CodeProps {\n  children: string\n  lang: string\n}\n\nexport interface Props {\n  onRunCommand?: (query: string) => void\n  modules?: AdditionalRedisModule[]\n  children: string\n  onMessageRendered?: () => void\n}\n\nconst MarkdownMessage = (props: Props) => {\n  const { modules, children, onMessageRendered, onRunCommand } = props\n\n  const [content, setContent] = useState('')\n  const [parseAsIs, setParseAsIs] = useState(false)\n\n  const ChatCodeBlock = useCallback(\n    (codeProps: CodeProps) => (\n      <CodeBlock {...codeProps} modules={modules} onRunCommand={onRunCommand} />\n    ),\n    [modules],\n  )\n  const components: any = {\n    Code: ChatCodeBlock,\n    CloudLink,\n    Link: ChatExternalLink,\n  }\n\n  useEffect(() => {\n    const formatContent = async () => {\n      try {\n        const formated = await new MarkdownToJsxString().format({\n          data: children,\n          codeOptions: { allLangs: true },\n        })\n        setContent(formated)\n      } catch {\n        setParseAsIs(true)\n      }\n    }\n\n    formatContent()\n  }, [children])\n\n  useEffect(() => {\n    if (content) {\n      onMessageRendered?.()\n    }\n  }, [content])\n\n  if (parseAsIs) {\n    return <>{children}</>\n  }\n\n  return (\n    // @ts-ignore\n    <JsxParser\n      components={components}\n      blacklistedTags={['iframe', 'script']}\n      autoCloseVoidElements\n      jsx={content}\n      onError={() => setParseAsIs(true)}\n    />\n  )\n}\n\nexport default React.memo(MarkdownMessage)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/chat-external-link/ChatExternalLink.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ChatExternalLink from './ChatExternalLink'\n\ndescribe('ChatExternalLink', () => {\n  it('should render', () => {\n    expect(render(<ChatExternalLink href=\"\" />)).toBeTruthy()\n  })\n\n  it('should render proper link', () => {\n    render(<ChatExternalLink href=\"https://localhost\" />)\n\n    expect(screen.getByTestId('chat-external-link')).toHaveAttribute(\n      'href',\n      'https://localhost/?utm_source=redisinsight&utm_medium=app&utm_campaign=ai_assistant',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/chat-external-link/ChatExternalLink.tsx",
    "content": "import React from 'react'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { Link, LinkProps } from 'uiSrc/components/base/link/Link'\n\nconst ChatExternalLink = (props: LinkProps) => {\n  const { href } = props\n  return (\n    <Link\n      external\n      variant=\"inline\"\n      allowWrap={true}\n      {...props}\n      data-testid=\"chat-external-link\"\n      href={getUtmExternalLink(href || EXTERNAL_LINKS.redisIo, {\n        campaign: 'ai_assistant',\n      })}\n    />\n  )\n}\n\nexport default React.memo(ChatExternalLink)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/chat-external-link/index.ts",
    "content": "import ChatExternalLink from './ChatExternalLink'\n\nexport default ChatExternalLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  cleanup,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { ButtonLang } from 'uiSrc/utils/formatters/markdown/remarkCode'\n\nimport { sendWBCommand } from 'uiSrc/slices/workbench/wb-results'\nimport { setDbIndexState } from 'uiSrc/slices/app/context'\nimport CodeBlock from './CodeBlock'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('CodeBlock', () => {\n  it('should render', () => {\n    expect(render(<CodeBlock>1</CodeBlock>)).toBeTruthy()\n  })\n\n  it('should call proper action after click on run button', () => {\n    render(<CodeBlock lang={ButtonLang.Redis}>info</CodeBlock>)\n\n    fireEvent.click(screen.getByTestId('run-btn-'))\n\n    expect(store.getActions()).toEqual([\n      sendWBCommand({\n        commandId: expect.any(String),\n        commands: ['info'],\n      }),\n      setDbIndexState(true),\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results'\nimport { CodeButtonBlock } from 'uiSrc/components/markdown'\nimport { ButtonLang } from 'uiSrc/utils/formatters/markdown/remarkCode'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\n\nexport interface Props {\n  modules?: AdditionalRedisModule[]\n  children: string\n  lang?: string\n  onRunCommand?: (query: string) => void\n}\n\nconst CodeBlock = (props: Props) => {\n  const { children, lang, modules, onRunCommand } = props\n\n  const dispatch = useDispatch()\n\n  const handleApply = (params?: CodeButtonParams, onFinish?: () => void) => {\n    onRunCommand?.(children)\n    dispatch(\n      sendWbQueryAction(\n        children,\n        null,\n        params,\n        { afterAll: onFinish },\n        onFinish,\n      ),\n    )\n  }\n\n  return (\n    <CodeButtonBlock\n      label=\"\"\n      content={children}\n      isShowConfirmation\n      onApply={handleApply}\n      lang={lang}\n      params={{ executable: lang === ButtonLang.Redis ? 'true' : 'false' }}\n      modules={modules}\n    />\n  )\n}\n\nexport default React.memo(CodeBlock)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/index.ts",
    "content": "import CodeBlock from './CodeBlock'\n\nexport default CodeBlock\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/index.ts",
    "content": "import ChatExternalLink from './chat-external-link'\nimport CodeBlock from './code-block'\n\nexport { ChatExternalLink, CodeBlock }\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/index.ts",
    "content": "import MarkdownMessage from './MarkdownMessage'\n\nexport default MarkdownMessage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/restart-chat/RestartChat.spec.tsx",
    "content": "import React from 'react'\nimport {\n  fireEvent,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport RestartChat from './RestartChat'\n\nconst button = <button data-testid=\"anchor-btn\" />\n\ndescribe('RestartChat', () => {\n  it('should render', () => {\n    expect(\n      render(<RestartChat button={button} onConfirm={jest.fn()} />),\n    ).toBeTruthy()\n  })\n\n  it('should call onConfirm', async () => {\n    const onConfirm = jest.fn()\n    render(<RestartChat button={button} onConfirm={onConfirm} />)\n\n    fireEvent.click(screen.getByTestId('anchor-btn'))\n\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('ai-chat-restart-confirm'))\n\n    expect(onConfirm).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/restart-chat/RestartChat.tsx",
    "content": "import React, { useState } from 'react'\nimport cx from 'classnames'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  button: NonNullable<React.ReactElement>\n  onConfirm: () => void\n  anchorClassName?: string\n}\n\nconst RestartChat = (props: Props) => {\n  const { button, onConfirm, anchorClassName = '' } = props\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  const handleConfirm = () => {\n    setIsPopoverOpen(false)\n    onConfirm()\n  }\n\n  const onClickAnchor = () => {\n    setIsPopoverOpen(true)\n  }\n\n  const extendedButton = React.cloneElement(button, { onClick: onClickAnchor })\n\n  return (\n    <RiPopover\n      ownFocus\n      panelClassName={cx('popoverLikeTooltip', styles.popover)}\n      anchorClassName={cx(styles.popoverAnchor, anchorClassName)}\n      anchorPosition=\"downLeft\"\n      isOpen={isPopoverOpen}\n      panelPaddingSize=\"m\"\n      closePopover={() => setIsPopoverOpen(false)}\n      button={extendedButton}\n    >\n      <>\n        <Title size=\"S\" color=\"primary\">\n          Restart session\n        </Title>\n        <Spacer size=\"s\" />\n        <Text size=\"m\" color=\"primary\">\n          This will delete the current message history and initiate a new\n          session.\n        </Text>\n        <Spacer size=\"l\" />\n        <Row justify=\"end\">\n          <PrimaryButton\n            size=\"s\"\n            onClick={handleConfirm}\n            className={styles.confirmBtn}\n            data-testid=\"ai-chat-restart-confirm\"\n          >\n            Restart\n          </PrimaryButton>\n        </Row>\n      </>\n    </RiPopover>\n  )\n}\n\nexport default React.memo(RestartChat)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/restart-chat/index.ts",
    "content": "import RestartChat from './RestartChat'\n\nexport default RestartChat\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/restart-chat/styles.module.scss",
    "content": ".popover {\n  min-width: 300px !important;\n}\n\n.confirmBtn {\n  display: block !important;\n  margin-left: auto;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/texts.tsx",
    "content": "import React from 'react'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const ASSISTANCE_CHAT_AGREEMENTS = (\n  <>\n    <Text size=\"m\" color=\"primary\">\n      Redis Copilot is powered by OpenAI API and is designed for general\n      information only.\n    </Text>\n    <Spacer size=\"xs\" />\n    <Text size=\"m\" color=\"primary\">\n      Please do not input any personal data or confidential information.\n    </Text>\n    <Spacer size=\"xs\" />\n    <Text size=\"m\" color=\"primary\">\n      By accessing and/or using Redis Copilot, you acknowledge that you agree to\n      the{' '}\n      <Link\n        variant=\"inline\"\n        size=\"S\"\n        color=\"subdued\"\n        target=\"_blank\"\n        href=\"https://redis.io/legal/redis-copilot-terms-of-use/\"\n      >\n        REDIS COPILOT TERMS\n      </Link>{' '}\n      and{' '}\n      <Link\n        variant=\"inline\"\n        size=\"S\"\n        color=\"subdued\"\n        target=\"_blank\"\n        href=\"https://redis.com/legal/privacy-policy/\"\n      >\n        Privacy Policy\n      </Link>\n      .\n    </Text>\n  </>\n)\n\nexport const EXPERT_CHAT_AGREEMENTS = (\n  <>\n    <Text size=\"m\" color=\"primary\">\n      Redis Copilot is powered by OpenAI API.\n    </Text>\n    <Spacer size=\"xs\" />\n    <Text size=\"m\" color=\"primary\">\n      Please do not include any personal data (except as expressly required for\n      the use of Redis Copilot) or confidential information.\n    </Text>\n    <Text size=\"m\" color=\"primary\">\n      Redis Copilot needs access to the information in your database to provide\n      you context-aware assistance.\n    </Text>\n    <Spacer size=\"xs\" />\n    <Text size=\"m\" color=\"primary\">\n      By accepting these terms, you consent to the processing of any information\n      included in your database, and you agree to the{' '}\n      <Link\n        variant=\"inline\"\n        size=\"S\"\n        color=\"subdued\"\n        target=\"_blank\"\n        href=\"https://redis.io/legal/redis-copilot-terms-of-use/\"\n      >\n        REDIS COPILOT TERMS\n      </Link>{' '}\n      and{' '}\n      <Link\n        variant=\"inline\"\n        size=\"S\"\n        color=\"subdued\"\n        target=\"_blank\"\n        href=\"https://redis.com/legal/privacy-policy/\"\n      >\n        Privacy Policy\n      </Link>\n      .\n    </Text>\n  </>\n)\n\nexport const EXPERT_CHAT_INITIAL_MESSAGE = (\n  <>\n    <Text size=\"xs\">Hi!</Text>\n    <Text size=\"xs\">I am here to help you get started with data querying.</Text>\n    <Text size=\"xs\">\n      Type <b>/help</b> to get more info on what questions I can answer.\n    </Text>\n    <Spacer />\n    <Text size=\"xs\">\n      With <span style={{ color: 'red' }}>&hearts;</span>, your Redis Copilot!\n    </Text>\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/welcome-ai-assistant/WelcomeAiAssistant.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  act,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport {\n  OAuthSocialAction,\n  OAuthSocialSource,\n  OAuthStrategy,\n} from 'uiSrc/slices/interfaces'\nimport { setOAuthCloudSource, signIn } from 'uiSrc/slices/oauth/cloud'\nimport { MOCK_OAUTH_SSO_EMAIL } from 'uiSrc/mocks/data/oauth'\nimport WelcomeAiAssistant from './WelcomeAiAssistant'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCloudSelector: jest.fn().mockReturnValue({\n    source: 'source',\n  }),\n  oauthCloudPAgreementSelector: jest.fn().mockReturnValue(true),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('WelcomeAiAssistant', () => {\n  it('should render', () => {\n    expect(render(<WelcomeAiAssistant />)).toBeTruthy()\n  })\n\n  it('should render sign in form', () => {\n    render(<WelcomeAiAssistant />)\n\n    expect(\n      screen.getByTestId('oauth-container-social-buttons'),\n    ).toBeInTheDocument()\n  })\n\n  it('should call proper actions after click on social button', async () => {\n    render(<WelcomeAiAssistant />)\n\n    fireEvent.click(screen.getByTestId('google-oauth'))\n\n    expect(store.getActions()).toEqual([\n      setSSOFlow(OAuthSocialAction.SignIn),\n      setOAuthCloudSource(OAuthSocialSource.AiChat),\n      signIn(),\n    ])\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.Google,\n        action: OAuthSocialAction.SignIn,\n        source: OAuthSocialSource.AiChat,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper actions after click on sso social button', async () => {\n    render(<WelcomeAiAssistant />)\n\n    fireEvent.click(screen.getByTestId('sso-oauth'))\n\n    expect(screen.getByTestId('sso-email')).toBeInTheDocument()\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption: OAuthStrategy.SSO,\n        action: OAuthSocialAction.SignIn,\n        source: OAuthSocialSource.AiChat,\n      },\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sso-email'), {\n        target: { value: MOCK_OAUTH_SSO_EMAIL },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expect(store.getActions()).toEqual([\n      setSSOFlow(OAuthSocialAction.SignIn),\n      setOAuthCloudSource(OAuthSocialSource.AiChat),\n      signIn(),\n    ])\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED,\n      eventData: {\n        action: OAuthSocialAction.SignIn,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/welcome-ai-assistant/WelcomeAiAssistant.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { OAuthAgreement } from 'uiSrc/components/oauth/shared'\n\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { setOAuthCloudSource } from 'uiSrc/slices/oauth/cloud'\nimport OAuthForm from 'uiSrc/components/oauth/shared/oauth-form'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport styles from './styles.module.scss'\n\nconst WelcomeAiAssistant = () => {\n  const dispatch = useDispatch()\n\n  const handleSsoClick = (accountOption: string) => {\n    dispatch(setSSOFlow(OAuthSocialAction.SignIn))\n    dispatch(setOAuthCloudSource(OAuthSocialSource.AiChat))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED,\n      eventData: {\n        accountOption,\n        action: OAuthSocialAction.SignIn,\n        source: OAuthSocialSource.AiChat,\n      },\n    })\n  }\n\n  return (\n    <div className={styles.wrapper} data-testid=\"copilot-welcome\">\n      <div className={styles.container}>\n        <OAuthForm action={OAuthSocialAction.SignIn} onClick={handleSsoClick}>\n          {(form: React.ReactNode) => (\n            <>\n              <Title size=\"L\" variant=\"semiBold\" color=\"primary\">\n                Welcome to Redis Copilot\n              </Title>\n              <Spacer size=\"s\" />\n              <Text size=\"s\" color=\"secondary\">\n                Learn about Redis and explore your data, in a conversational\n                manner.\n              </Text>\n              <Spacer size=\"s\" />\n              <Text size=\"s\" color=\"secondary\">\n                Build faster with Redis Copilot.\n              </Text>\n              <Spacer size=\"xxl\" />\n              <Title size=\"XS\" color=\"secondary\">\n                Sign in to get started\n              </Title>\n\n              <Spacer size=\"l\" />\n              {form}\n              <Spacer />\n\n              <div className={styles.agreement}>\n                <OAuthAgreement />\n              </div>\n            </>\n          )}\n        </OAuthForm>\n      </div>\n    </div>\n  )\n}\n\nexport default WelcomeAiAssistant\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/welcome-ai-assistant/index.ts",
    "content": "import WelcomeAiAssistant from './WelcomeAiAssistant'\n\nexport default WelcomeAiAssistant\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/welcome-ai-assistant/styles.module.scss",
    "content": ".wrapper {\n  width: 100%;\n  height: 100%;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  .container {\n    width: 100%;\n    display: flex;\n    flex-direction: column;\n    flex-basis: auto;\n    align-items: center;\n\n    padding: 24px 32px;\n    text-align: center;\n\n    @include eui.scrollBar;\n    overflow: auto;\n    max-height: 100%;\n  }\n}\n\n.agreement {\n  text-align: left;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/index.ts",
    "content": "import AiAssistant from './AiAssistant'\n\nexport default AiAssistant\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/ai-assistant/styles.module.scss",
    "content": ".wrapper {\n  width: 100%;\n  height: 100%;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/EnablementArea.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n  act,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport {\n  MOCK_TUTORIALS_ITEMS,\n  MOCK_CUSTOM_TUTORIALS_ITEMS,\n} from 'uiSrc/constants'\nimport {\n  EnablementAreaComponent,\n  IEnablementAreaItem,\n} from 'uiSrc/slices/interfaces'\n\nimport {\n  deleteCustomTutorial,\n  deleteWbCustomTutorial,\n  uploadWbCustomTutorial,\n} from 'uiSrc/slices/workbench/wb-custom-tutorials'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport EnablementArea, { Props } from './EnablementArea'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/workbench/wb-custom-tutorials', () => ({\n  ...jest.requireActual('uiSrc/slices/workbench/wb-custom-tutorials'),\n  deleteCustomTutorial: jest\n    .fn()\n    .mockImplementation(\n      jest.requireActual('uiSrc/slices/workbench/wb-custom-tutorials')\n        .deleteCustomTutorial,\n    ),\n}))\n\njest.mock('uiSrc/slices/workbench/wb-tutorials', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/workbench/wb-tutorials',\n  ).initialState\n  return {\n    ...jest.requireActual('uiSrc/slices/workbench/wb-tutorials'),\n    workbenchTutorialsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\n/**\n * Explore Redis tests\n *\n * @group component\n */\ndescribe('EnablementArea', () => {\n  beforeEach(() => {\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: jest.fn() })\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockImplementation(() => ({ search: '' }))\n  })\n  it('should render', () => {\n    expect(\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          tutorials={MOCK_TUTORIALS_ITEMS}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render loading', () => {\n    const { queryByTestId } = render(\n      <EnablementArea {...instance(mockedProps)} loading />,\n    )\n    const loaderEl = queryByTestId('enablementArea-loader')\n    const treeViewEl = queryByTestId('enablementArea-treeView')\n\n    expect(loaderEl).toBeInTheDocument()\n    expect(treeViewEl).not.toBeInTheDocument()\n  })\n\n  it('should render Group component', () => {\n    const item: IEnablementAreaItem = {\n      type: EnablementAreaComponent.Group,\n      id: 'quick-guides',\n      label: 'Quick Guides',\n      children: [\n        {\n          type: EnablementAreaComponent.InternalLink,\n          id: 'document-capabilities',\n          label: 'Document Capabilities',\n          args: {\n            path: 'static/workbench/quick-guides/document-capabilities.html',\n          },\n        },\n      ],\n    }\n\n    const { queryByTestId } = render(\n      <EnablementArea {...instance(mockedProps)} tutorials={[item]} />,\n    )\n\n    expect(queryByTestId('accordion-quick-guides')).toBeInTheDocument()\n  })\n  it('should render InternalLink component', () => {\n    const item = {\n      type: EnablementAreaComponent.InternalLink,\n      id: 'internal-page',\n      label: 'Internal Page',\n      args: {\n        path: 'static/workbench/quick-guides/document-capabilities.html',\n      },\n    }\n    const { queryByTestId } = render(\n      <EnablementArea {...instance(mockedProps)} tutorials={[item]} />,\n    )\n\n    expect(queryByTestId('internal-link-internal-page')).toBeInTheDocument()\n  })\n\n  it('should find guide and push proper search path', async () => {\n    const search = '?guidePath=quick-guides/working-with-json.html'\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockImplementationOnce(() => ({ search }))\n\n    await act(() => {\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          tutorials={MOCK_TUTORIALS_ITEMS}\n          onOpenInternalPage={jest.fn}\n        />,\n      )\n    })\n\n    await waitFor(\n      () => {\n        expect(pushMock).toBeCalledWith({ search: '?path=tutorials/0/1' })\n      },\n      { timeout: 1000 },\n    )\n  })\n\n  describe('Custom Tutorials', () => {\n    it('should render custom tutorials', () => {\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n      expect(screen.getByTestId('enablementArea')).toHaveTextContent(\n        'MY TUTORIALS',\n      )\n    })\n\n    it('should render add button and open form', () => {\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n\n      fireEvent.click(screen.getByTestId('open-upload-tutorial-btn'))\n      expect(screen.getByTestId('upload-tutorial-form')).toBeInTheDocument()\n    })\n\n    it('should render form directly when no tutorials', () => {\n      const customTutorials = [\n        { ...MOCK_CUSTOM_TUTORIALS_ITEMS[0], children: [] },\n      ]\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={customTutorials}\n        />,\n      )\n      expect(screen.getByTestId('upload-tutorial-form')).toBeInTheDocument()\n    })\n\n    it('should call proper actions after upload form submit', async () => {\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n\n      const afterRenderActions = [...store.getActions()]\n\n      fireEvent.click(screen.getByTestId('open-upload-tutorial-btn'))\n\n      await act(() => {\n        fireEvent.change(screen.getByTestId('tutorial-link-field'), {\n          target: { value: 'link' },\n        })\n      })\n\n      await act(() => {\n        fireEvent.click(screen.getByTestId('submit-upload-tutorial-btn'))\n      })\n\n      const expectedActions = [...afterRenderActions, uploadWbCustomTutorial()]\n      expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n        expectedActions,\n      )\n    })\n\n    it('should render delete button and call proper actions after click on delete', () => {\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n      const afterRenderActions = [...store.getActions()]\n\n      fireEvent.click(screen.getByTestId('delete-tutorial-icon-12mfp-rem'))\n      fireEvent.click(screen.getByTestId('delete-tutorial-12mfp-rem'))\n\n      const expectedActions = [...afterRenderActions, deleteWbCustomTutorial()]\n      expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n        expectedActions,\n      )\n    })\n  })\n\n  describe('Telemetry', () => {\n    it('should call proper event on click create button', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n\n      fireEvent.click(screen.getByTestId('open-upload-tutorial-btn'))\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.EXPLORE_PANEL_IMPORT_CLICKED,\n        eventData: {\n          databaseId: 'instanceId',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n\n    it('should call proper event on submit custom tutorial', async () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n      fireEvent.click(screen.getByTestId('open-upload-tutorial-btn'))\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n      await act(() => {\n        fireEvent.change(screen.getByTestId('tutorial-link-field'), {\n          target: { value: 'link' },\n        })\n      })\n\n      await act(() => {\n        fireEvent.click(screen.getByTestId('submit-upload-tutorial-btn'))\n      })\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.EXPLORE_PANEL_IMPORT_SUBMITTED,\n        eventData: {\n          databaseId: 'instanceId',\n          source: 'URL',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n\n    it('should call proper event on delete custom tutorial', async () => {\n      ;(deleteCustomTutorial as jest.Mock).mockImplementation(\n        (_, onSuccess: () => void) => () => onSuccess?.(),\n      )\n\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <EnablementArea\n          {...instance(mockedProps)}\n          customTutorials={MOCK_CUSTOM_TUTORIALS_ITEMS}\n        />,\n      )\n      await act(() => {\n        fireEvent.click(screen.getByTestId('delete-tutorial-icon-12mfp-rem'))\n      })\n\n      await act(() => {\n        fireEvent.click(screen.getByTestId('delete-tutorial-12mfp-rem'))\n      })\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_DELETED,\n        eventData: {\n          databaseId: 'instanceId',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/EnablementArea.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useHistory, useLocation } from 'react-router-dom'\nimport { useSelector, useDispatch } from 'react-redux'\nimport cx from 'classnames'\nimport { IEnablementAreaItem } from 'uiSrc/slices/interfaces'\nimport {\n  EnablementAreaProvider,\n  IInternalPage,\n} from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport {\n  ApiEndpoints,\n  EAManifestFirstKey,\n  CodeButtonParams,\n} from 'uiSrc/constants'\nimport { findMarkdownPath, Nullable } from 'uiSrc/utils'\nimport {\n  explorePanelSelector,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n  setExplorePanelManifest,\n} from 'uiSrc/slices/panels/sidePanels'\n\nimport { getMarkdownPathByManifest, getWBSourcePath } from './utils/getFileInfo'\nimport { LazyInternalPage, Navigation } from './components'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  tutorials: IEnablementAreaItem[]\n  customTutorials: IEnablementAreaItem[]\n  loading: boolean\n  openScript: (\n    script: string,\n    params?: CodeButtonParams,\n    onFinish?: () => void,\n  ) => void\n  onOpenInternalPage: (page: IInternalPage) => void\n  isCodeBtnDisabled?: boolean\n}\n\nconst EnablementArea = (props: Props) => {\n  const {\n    tutorials = [],\n    customTutorials = [],\n    openScript,\n    loading,\n    onOpenInternalPage,\n    isCodeBtnDisabled,\n  } = props\n  const { search } = useLocation()\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const {\n    manifest,\n    search: searchEAContext,\n    isPageOpen: isInternalPageVisible,\n  } = useSelector(explorePanelSelector)\n\n  const contextManifestPath = new URLSearchParams(searchEAContext).get('path')\n\n  const [internalPage, setInternalPage] = useState<IInternalPage>({\n    path: '',\n    manifestPath: contextManifestPath,\n  })\n\n  const searchRef = useRef<string>('')\n\n  useEffect(() => {\n    searchRef.current = search\n    const pagePath = new URLSearchParams(search).get('item')\n\n    if (pagePath) {\n      dispatch(setExplorePanelIsPageOpen(true))\n      setInternalPage({ path: pagePath })\n    }\n  }, [search])\n\n  useEffect(() => {\n    // handle guidePath or tutorialId search params\n    const tutorialPathParam = new URLSearchParams(search).get('guidePath') ?? ''\n    const tutorialIdParam = new URLSearchParams(search).get('tutorialId') ?? ''\n    if (tutorialPathParam || tutorialIdParam) {\n      let manifestPath = ''\n      const options = tutorialIdParam\n        ? { id: tutorialIdParam }\n        : { mdPath: tutorialPathParam }\n      const tutorialPath = findMarkdownPath(tutorials, options)\n\n      if (tutorialPath) {\n        manifestPath = `${EAManifestFirstKey.TUTORIALS}/${tutorialPath}`\n      }\n\n      if (manifestPath) {\n        handleOpenInternalPage({ path: '', manifestPath }, false)\n        return\n      }\n\n      dispatch(setExplorePanelIsPageOpen(false))\n    }\n  }, [search, tutorials])\n\n  useEffect(() => {\n    const searchParams = new URLSearchParams(search)\n\n    const guidePath = searchParams.get('guidePath')\n    const tutorialId = searchParams.get('tutorialId')\n    const itemPath = searchParams.get('item')\n\n    // if we have guidePath or tutorialId another useUffect handle it\n    if (guidePath || tutorialId || itemPath) {\n      return\n    }\n\n    // otherwise we handle path from url or from the context\n    const manifestPath = searchParams.get('path')\n    if (manifestPath) {\n      const { manifest, prefixFolder } = getManifestByPath(manifestPath)\n      const path = getMarkdownPathByManifest(\n        manifest,\n        manifestPath,\n        prefixFolder,\n      )\n\n      if (path) {\n        dispatch(setExplorePanelIsPageOpen(true))\n        dispatch(setExplorePanelManifest(manifest))\n\n        setInternalPage({ path, manifestPath })\n        return\n      }\n    }\n\n    if (contextManifestPath) {\n      handleOpenInternalPage(\n        { path: '', manifestPath: contextManifestPath },\n        false,\n      )\n      return\n    }\n\n    dispatch(setExplorePanelIsPageOpen(false))\n  }, [search, customTutorials, tutorials])\n\n  const getManifestByPath = (path: Nullable<string> = '') => {\n    const manifestPath = path?.replace(/^\\//, '') || ''\n    if (manifestPath.startsWith(EAManifestFirstKey.CUSTOM_TUTORIALS)) {\n      return {\n        manifest: customTutorials,\n        prefixFolder: ApiEndpoints.CUSTOM_TUTORIALS_PATH,\n      }\n    }\n    if (manifestPath.startsWith(EAManifestFirstKey.TUTORIALS)) {\n      return { manifest: tutorials, prefixFolder: ApiEndpoints.TUTORIALS_PATH }\n    }\n\n    return { manifest: null }\n  }\n\n  const handleOpenInternalPage = (page: IInternalPage, fromUser = true) => {\n    setTimeout(() => {\n      history.push({\n        search: page.manifestPath\n          ? `?path=${page.manifestPath}`\n          : `?item=${page.path}`,\n      })\n    }, 0)\n\n    if (fromUser) {\n      onOpenInternalPage(page)\n    }\n  }\n\n  const handleCloseInternalPage = () => {\n    dispatch(resetExplorePanelSearch())\n    dispatch(setExplorePanelIsPageOpen(false))\n    setInternalPage({ path: '' })\n    history.push({\n      // TODO: better to use query-string parser and update only one parameter (instead of replacing all)\n      search: '',\n    })\n  }\n\n  return (\n    <EnablementAreaProvider\n      value={{\n        setScript: openScript,\n        openPage: handleOpenInternalPage,\n        isCodeBtnDisabled,\n      }}\n    >\n      <div\n        data-testid=\"enablementArea\"\n        className={cx(styles.container, 'relative', 'enablement-area')}\n      >\n        {loading || (isInternalPageVisible && !internalPage?.path) ? (\n          <div\n            data-testid=\"enablementArea-loader\"\n            className={cx(styles.innerContainer, styles.innerContainerLoader)}\n          >\n            <LoadingContent lines={3} />\n          </div>\n        ) : (\n          <Navigation\n            tutorials={tutorials}\n            customTutorials={customTutorials}\n            isInternalPageVisible={isInternalPageVisible}\n          />\n        )}\n        <div\n          className={cx({\n            [styles.internalPage]: true,\n            [styles.internalPageVisible]: isInternalPageVisible,\n          })}\n        >\n          {internalPage?.path && (\n            <LazyInternalPage\n              onClose={handleCloseInternalPage}\n              title={internalPage?.label}\n              path={internalPage?.path}\n              manifest={manifest}\n              manifestPath={internalPage?.manifestPath}\n              sourcePath={getWBSourcePath(internalPage?.path)}\n              search={searchRef.current}\n            />\n          )}\n        </div>\n      </div>\n    </EnablementAreaProvider>\n  )\n}\n\nexport default EnablementArea\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Code/Code.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { MONACO_MANUAL } from 'uiSrc/constants'\nimport {\n  defaultValue,\n  EnablementAreaProvider,\n} from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport Code, { Props } from './Code'\n\nconst mockedProps = mock<Props>()\nconst label = 'btn'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\ndescribe('Code', () => {\n  it('should render', () => {\n    const component = render(\n      <Code {...instance(mockedProps)} label={label}>\n        {MONACO_MANUAL}\n      </Code>,\n    )\n    const { container } = component\n\n    expect(component).toBeTruthy()\n    expect(container).toHaveTextContent(label)\n  })\n  it('should correctly set script', () => {\n    const setScript = jest.fn()\n\n    const { queryByTestId } = render(\n      <EnablementAreaProvider value={{ ...defaultValue, setScript }}>\n        <Code {...instance(mockedProps)} label={label}>\n          {MONACO_MANUAL}\n        </Code>\n      </EnablementAreaProvider>,\n    )\n\n    const link = queryByTestId(`run-btn-${label}`)\n    fireEvent.click(link as Element)\n    expect(setScript).toBeCalledWith(\n      MONACO_MANUAL,\n      undefined,\n      expect.any(Function),\n    )\n  })\n\n  it('should correctly set script with auto execute', () => {\n    const setScript = jest.fn()\n\n    render(\n      <EnablementAreaProvider value={{ ...defaultValue, setScript }}>\n        <Code {...instance(mockedProps)} label={label} params=\"[auto=true]\">\n          {MONACO_MANUAL}\n        </Code>\n      </EnablementAreaProvider>,\n    )\n\n    fireEvent.click(screen.queryByTestId(`run-btn-${label}`) as Element)\n    expect(setScript).toBeCalledWith(\n      MONACO_MANUAL,\n      { auto: 'true' },\n      expect.any(Function),\n    )\n  })\n\n  it('should call proper telemetry on Copy', () => {\n    const setScript = jest.fn()\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(\n      <EnablementAreaProvider value={{ ...defaultValue, setScript }}>\n        <Code\n          {...instance(mockedProps)}\n          label={label}\n          path=\"path\"\n          params=\"[auto=true]\"\n        >\n          {MONACO_MANUAL}\n        </Code>\n      </EnablementAreaProvider>,\n    )\n\n    fireEvent.click(screen.queryByTestId(`copy-btn-${label}`) as Element)\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_COMMAND_COPIED,\n      eventData: {\n        buttonName: label,\n        databaseId: 'instanceId',\n        path: 'path',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n  })\n\n  it('should call proper telemetry on apply', () => {\n    const setScript = jest.fn()\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(\n      <EnablementAreaProvider value={{ ...defaultValue, setScript }}>\n        <Code {...instance(mockedProps)} label={label} path=\"path\" params=\"\">\n          {MONACO_MANUAL}\n        </Code>\n      </EnablementAreaProvider>,\n    )\n\n    fireEvent.click(screen.queryByTestId(`run-btn-${label}`) as Element)\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_COMMAND_RUN_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n        path: 'path',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Code/Code.tsx",
    "content": "import { startCase } from 'lodash'\nimport React, { useContext } from 'react'\nimport { useLocation, useParams } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\nimport EnablementAreaContext from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport { parseParams } from 'uiSrc/utils'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport CodeButtonBlock from 'uiSrc/components/markdown/CodeButtonBlock'\n\nimport { getFileInfo, getTutorialSection } from '../../utils'\n\nexport interface Props {\n  label: string\n  children: string\n  lang: string\n  params?: string\n  path?: string\n}\n\nconst Code = (props: Props) => {\n  const { children, params = '', label, path, lang, ...rest } = props\n  const {\n    provider,\n    modules = [],\n    isFreeDb,\n  } = useSelector(connectedInstanceSelector)\n\n  const { search } = useLocation()\n  const { setScript } = useContext(EnablementAreaContext)\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const parsedParams = parseParams(params)\n\n  const getFile = () => {\n    const pagePath = new URLSearchParams(search).get('item')\n    const manifestPath = new URLSearchParams(search).get('path')\n    const file: { path?: string; name?: string; section?: string } = {}\n\n    if (pagePath) {\n      const pageInfo = getFileInfo({ path: pagePath })\n      file.path = `${pageInfo.location}/${pageInfo.name}`\n      file.name = startCase(label)\n    }\n\n    if (manifestPath) {\n      file.section = getTutorialSection(manifestPath)\n    }\n\n    return file\n  }\n\n  const loadContent = (params?: CodeButtonParams, onFinish?: () => void) => {\n    setScript(children, params, onFinish)\n\n    const file = getFile()\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_COMMAND_RUN_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        path,\n        provider,\n        ...file,\n      },\n    })\n  }\n\n  const onCopyClicked = () => {\n    const file = getFile()\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_COMMAND_COPIED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        buttonName: label,\n        path,\n        provider,\n        ...file,\n      },\n    })\n  }\n\n  return (\n    <CodeButtonBlock\n      className=\"mb-s mt-s\"\n      onApply={loadContent}\n      onCopy={onCopyClicked}\n      content={children}\n      modules={modules}\n      label={label}\n      params={parsedParams}\n      isShowConfirmation={!isFreeDb}\n      lang={lang}\n      {...rest}\n    />\n  )\n}\n\nexport default Code\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Code/index.ts",
    "content": "import Code from './Code'\n\nexport default Code\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/CreateTutorialLink/CreateTutorialLink.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport CreateTutorailLink from './CreateTutorialLink'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('CreateTutorialLink', () => {\n  it('should render', () => {\n    expect(render(<CreateTutorailLink />)).toBeTruthy()\n  })\n\n  it('should call proper telemetry event after click read more', () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(<CreateTutorailLink />)\n\n    fireEvent.click(screen.getByTestId('read-more-link'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.EXPLORE_PANEL_CREATE_TUTORIAL_LINK_CLICKED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/CreateTutorialLink/CreateTutorialLink.tsx",
    "content": "import React from 'react'\nimport { useParams } from 'react-router-dom'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\n\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nconst CreateTutorialLink = () => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const onClickReadMore = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_CREATE_TUTORIAL_LINK_CLICKED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n      },\n    })\n  }\n\n  return (\n    <Link\n      external\n      underline\n      size=\"S\"\n      variant=\"inline\"\n      color=\"subdued\"\n      onClick={onClickReadMore}\n      href={EXTERNAL_LINKS.guidesRepo}\n      data-testid=\"read-more-link\"\n    >\n      Create your tutorial\n    </Link>\n  )\n}\n\nexport default CreateTutorialLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/CreateTutorialLink/index.ts",
    "content": "import CreateTutorialLink from './CreateTutorialLink'\n\nexport default CreateTutorialLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render, screen, fireEvent, act } from 'uiSrc/utils/test-utils'\n\nimport DeleteTutorialButton, { Props } from './DeleteTutorialButton'\n\nconst mockedProps = mock<Props>()\n\ndescribe('DeleteTutorialButton', () => {\n  it('should render', () => {\n    expect(render(<DeleteTutorialButton {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call onDelete', async () => {\n    const onDelete = jest.fn()\n    render(<DeleteTutorialButton {...mockedProps} onDelete={onDelete} id=\"1\" />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-tutorial-icon-1'))\n    })\n\n    fireEvent.click(screen.getByTestId('delete-tutorial-1'))\n\n    expect(onDelete).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.styles.ts",
    "content": "export { GroupHeaderButton } from '../Group/Group.styles'\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/DeleteTutorialButton.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { formatLongName } from 'uiSrc/utils'\n\nimport { DestructiveButton } from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiPopover } from 'uiSrc/components/base'\n\nimport * as S from './DeleteTutorialButton.styles'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  id: string\n  label: string\n  isLoading?: boolean\n  onDelete: (e: React.MouseEvent) => void\n}\n\nconst DeleteTutorialButton = (props: Props) => {\n  const { id, label, onDelete, isLoading } = props\n  const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState<boolean>(false)\n\n  const handleClickDelete = (\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  ) => {\n    e.stopPropagation()\n    setIsPopoverDeleteOpen((v) => !v)\n  }\n\n  return (\n    <RiPopover\n      anchorPosition=\"rightCenter\"\n      ownFocus\n      isOpen={isPopoverDeleteOpen}\n      closePopover={() => setIsPopoverDeleteOpen(false)}\n      panelPaddingSize=\"l\"\n      button={\n        <S.GroupHeaderButton\n          role=\"presentation\"\n          onClick={handleClickDelete}\n          data-testid={`delete-tutorial-icon-${id}`}\n        >\n          <RiIcon size=\"m\" type=\"DeleteIcon\" />\n        </S.GroupHeaderButton>\n      }\n      onClick={(e) => e.stopPropagation()}\n      data-testid={`delete-tutorial-popover-${id}`}\n    >\n      <div className={styles.popoverDeleteContainer}>\n        <Text size=\"m\" component=\"div\">\n          <h4 style={{ wordBreak: 'break-all' }}>\n            <b>{formatLongName(label)}</b>\n          </h4>\n          <Text size=\"s\">will be deleted.</Text>\n        </Text>\n        <div className={styles.popoverFooter}>\n          <DestructiveButton\n            size=\"s\"\n            icon={DeleteIcon}\n            onClick={onDelete}\n            loading={isLoading}\n            data-testid={`delete-tutorial-${id}`}\n          >\n            Delete\n          </DestructiveButton>\n        </div>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default DeleteTutorialButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/index.ts",
    "content": "import DeleteTutorialButton from './DeleteTutorialButton'\n\nexport default DeleteTutorialButton\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/DeleteTutorialButton/styles.module.scss",
    "content": ".popoverDeleteContainer {\n  overflow: hidden;\n  max-width: 350px !important;\n}\n\n.popoverFooter {\n  margin-top: 10px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/EmptyPrompt/EmptyPrompt.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport EmptyPrompt from './EmptyPrompt'\n\ndescribe('EmptyPrompt', () => {\n  it('should render', () => {\n    expect(render(<EmptyPrompt />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/EmptyPrompt/EmptyPrompt.tsx",
    "content": "import React from 'react'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { RiEmptyPrompt } from 'uiSrc/components/base/layout'\nimport styles from './styles.module.scss'\n\nconst EmptyPrompt = () => (\n  <div className={styles.container}>\n    <RiEmptyPrompt\n      data-testid=\"enablement-area__empty-prompt\"\n      icon={<RiIcon type=\"ToastDangerIcon\" color=\"danger600\" size=\"l\" />}\n      title={<h2>No information to display</h2>}\n      body={\n        <p className={styles.body}>\n          <span>Restart the application.</span>\n          <br />\n          <span>\n            If the problem persists, please{' '}\n            <Link\n              color=\"ghost\"\n              href={EXTERNAL_LINKS.githubIssues}\n              external={false}\n              target=\"_blank\"\n              data-testid=\"contact-us\"\n            >\n              contact us\n            </Link>\n            .\n          </span>\n        </p>\n      }\n    />\n  </div>\n)\n\nexport default EmptyPrompt\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/EmptyPrompt/index.ts",
    "content": "import EmptyPrompt from './EmptyPrompt'\n\nexport default EmptyPrompt\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/EmptyPrompt/styles.module.scss",
    "content": ".container {\n  display: flex;\n  height: 85%;\n  min-height: 200px;\n  align-items: center;\n  justify-content: center;\n  h2 {\n    font: normal normal normal 17px/20px Graphik, sans-serif !important;\n    margin-bottom: 0 !important;\n    color: var(--euiTextSubduedColor) !important;\n    font-weight: 300 !important;\n  }\n}\n\n.body {\n  font: normal normal normal 14px/18px Graphik, sans-serif !important;\n  color: var(--euiTextSubduedColor);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, toggleAccordion } from 'uiSrc/utils/test-utils'\nimport Group, { Props } from './Group'\n\nconst mockedProps = mock<Props>()\nconst testId = 'quick-guides'\n\ndescribe('Group', () => {\n  it('should render', () => {\n    expect(\n      render(<Group {...instance(mockedProps)} id={testId} />),\n    ).toBeTruthy()\n  })\n  it('should correctly render content', () => {\n    const label = 'Quick Guides'\n    const children = [\n      <span key=\"item-1\">Item 1</span>,\n      <span key=\"item-2\">Item 2</span>,\n    ]\n    const { queryByTestId } = render(\n      <Group {...instance(mockedProps)} id={testId} label={label}>\n        {children}\n      </Group>,\n    )\n    const accordionButton = queryByTestId(`accordion-${testId}`)\n\n    expect(accordionButton).toHaveTextContent(label)\n  })\n  it('should emit onToggle', async () => {\n    const callback = jest.fn()\n    const label = 'Quick Guides'\n\n    render(\n      <Group\n        {...instance(mockedProps)}\n        id={testId}\n        label={label}\n        onToggle={callback}\n      />,\n    )\n\n    await toggleAccordion(`accordion-${testId}`)\n\n    expect(callback).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.styles.ts",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const GroupHeaderButton = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  width: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n  height: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n  cursor: pointer;\n\n  &:hover {\n    color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.text.neutral100};\n    background-color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.background.neutral200};\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/Group.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport cx from 'classnames'\n\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { workbenchCustomTutorialsSelector } from 'uiSrc/slices/workbench/wb-custom-tutorials'\nimport { EAItemActions } from 'uiSrc/constants'\n\nimport { RiAccordion } from 'uiSrc/components/base/display/accordion/RiAccordion'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nimport DeleteTutorialButton from '../DeleteTutorialButton'\n\nimport * as S from './Group.styles'\nimport './styles.scss'\n\nexport interface Props {\n  id: string\n  label: string\n  actions?: string[]\n  onCreate?: () => void\n  onDelete?: (id: string) => void\n  children: React.ReactNode\n  withBorder?: boolean\n  initialIsOpen?: boolean\n  forceState?: 'open' | 'closed'\n  onToggle?: (isOpen: boolean) => void\n  isShowActions?: boolean\n  isShowFolder?: boolean\n  hasChildren?: boolean\n}\n\nconst Group = (props: Props) => {\n  const {\n    label,\n    actions,\n    isShowActions,\n    children,\n    id,\n    forceState,\n    withBorder = false,\n    initialIsOpen = false,\n    onToggle,\n    onCreate,\n    onDelete,\n    isShowFolder,\n    hasChildren = true,\n  } = props\n  const { deleting: deletingCustomTutorials } = useSelector(\n    workbenchCustomTutorialsSelector,\n  )\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const [isGroupOpen, setIsGroupOpen] = useState<boolean>(initialIsOpen)\n\n  const handleCreate = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    onCreate?.()\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_IMPORT_CLICKED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n      },\n    })\n  }\n\n  const handleDelete = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    onDelete?.(id)\n  }\n\n  const handleOpen = (isOpen: boolean) => {\n    if (forceState === 'open') return\n\n    setIsGroupOpen(isOpen)\n    onToggle?.(isOpen)\n  }\n\n  const actionsContent = (\n    <>\n      {actions?.includes(EAItemActions.Create) &&\n        hasChildren &&\n        (isGroupOpen || forceState === 'open') && (\n          <RiTooltip content=\"Upload tutorial\">\n            <S.GroupHeaderButton\n              role=\"presentation\"\n              onClick={handleCreate}\n              data-testid=\"open-upload-tutorial-btn\"\n            >\n              <RiIcon type=\"PlusSlimIcon\" size=\"m\" />\n            </S.GroupHeaderButton>\n          </RiTooltip>\n        )}\n      {actions?.includes(EAItemActions.Delete) && (\n        <DeleteTutorialButton\n          id={id}\n          label={label}\n          onDelete={handleDelete}\n          isLoading={deletingCustomTutorials}\n        />\n      )}\n    </>\n  )\n\n  return (\n    <RiAccordion\n      id={id}\n      data-testid={`accordion-${id}`}\n      defaultOpen={initialIsOpen}\n      open={forceState === 'open' || isGroupOpen}\n      label={\n        <Row align=\"end\" justify=\"start\" gap=\"s\">\n          {isShowFolder && (\n            <RiIcon type={isGroupOpen ? 'FolderOpenIcon' : 'FolderIcon'} />\n          )}\n\n          <Text className=\"group-header\" size=\"m\">\n            {label}\n          </Text>\n        </Row>\n      }\n      onOpenChange={handleOpen}\n      className={cx({ withBorder })}\n      actions={isShowActions ? actionsContent : null}\n    >\n      <Col gap=\"l\">{children}</Col>\n    </RiAccordion>\n  )\n}\n\nexport default Group\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/index.ts",
    "content": "import Group from './Group'\n\nexport default Group\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Group/styles.scss",
    "content": ".enablement-area {\n  .euiAccordion {\n    backface-visibility: hidden;\n\n    .group-header-wrapper {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n    }\n\n    .onboardingPopoverAnchor {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 24px;\n      height: 24px;\n    }\n\n    .group-header {\n      display: flex;\n      align-items: center;\n\n      letter-spacing: -0.12px;\n      overflow: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n\n    }\n\n    .divider {\n      visibility: hidden;\n      width: 100%;\n      height: 1px;\n      position: absolute;\n      bottom: 0;\n      hr {\n        border: none;\n        height: 1px;\n        width: 100%;\n        background-color: var(--separatorColor);\n      }\n    }\n  }\n\n  .euiAccordion__button {\n    padding: 5px 0;\n    flex-grow: 1;\n\n    & > span {\n      overflow: hidden;\n    }\n\n    .euiIEFlexWrapFix {\n      flex-grow: 1;\n    }\n\n    &:hover {\n      background-color: var(--hoverInListColorDarken);\n    }\n  }\n\n  .euiAccordion-isOpen {\n    min-width: 100%;\n\n    .divider {\n      visibility: visible;\n    }\n  }\n\n  .euiAccordion__triggerWrapper, .euiAccordion__childWrapper {\n    border: none !important;\n    background-color: transparent !important;\n  }\n\n  .euiAccordion__childWrapper {\n    padding-left: 0;\n\n    .euiListGroupItem {\n      button {\n        padding: 5px 8px;\n        line-height: 20px;\n      }\n\n      .euiListGroupItem__label {\n        font: normal normal normal 12px/14px Graphik, sans-serif;\n        white-space: break-spaces;\n      }\n    }\n\n    .euiListGroupItem:hover {\n      color: var(--euiTextColor);\n      background-color: var(--hoverInListColorDarken);\n    }\n  }\n}\n\n.hide {\n  display: none;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalLink/InternalLink.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render } from 'uiSrc/utils/test-utils'\nimport {\n  EnablementAreaProvider,\n  defaultValue,\n} from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\n\nimport InternalLink, { Props } from './InternalLink'\n\nconst mockedProps = mock<Props>()\n\ndescribe('InternalLink', () => {\n  it('should render', () => {\n    const label = 'Manual'\n    const component = render(\n      <InternalLink {...instance(mockedProps)} testId=\"manual\" label={label} />,\n    )\n    const { container } = component\n\n    expect(component).toBeTruthy()\n    expect(container).toHaveTextContent(label)\n  })\n  it('should call onClick function if path provided', () => {\n    const openPage = jest.fn()\n\n    const { queryByTestId } = render(\n      <EnablementAreaProvider value={{ ...defaultValue, openPage }}>\n        <InternalLink\n          {...instance(mockedProps)}\n          testId=\"manual\"\n          path=\"static/workbench\"\n          label=\"Manual\"\n        />\n      </EnablementAreaProvider>,\n    )\n\n    const link = queryByTestId(/internal-link-manual/)\n    fireEvent.click(link as Element)\n    expect(openPage).toBeCalled()\n  })\n  it('should not call onClick function if path not provided', () => {\n    const openPage = jest.fn()\n\n    const { queryByTestId } = render(\n      <EnablementAreaProvider value={{ ...defaultValue, openPage }}>\n        <InternalLink\n          {...instance(mockedProps)}\n          testId=\"manual\"\n          label=\"Manual\"\n        />\n      </EnablementAreaProvider>,\n    )\n\n    const link = queryByTestId(/internal-link-manual/)\n    fireEvent.click(link as Element)\n    expect(openPage).not.toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalLink/InternalLink.tsx",
    "content": "import React, { useContext } from 'react'\nimport cx from 'classnames'\nimport { truncateText } from 'uiSrc/utils'\nimport EnablementAreaContext from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\nimport { Item as ListItem } from 'uiSrc/components/base/layout/list'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport styles from './styles.module.scss'\nimport './styles.scss'\n\nexport interface Props {\n  testId: string\n  label: string\n  summary?: string\n  children: React.ReactElement[] | string\n  path?: string\n  size?: 's' | 'xs' | 'm' | 'l'\n  iconType?: string\n  iconPosition?: 'left' | 'right'\n  toolTip?: string\n  style?: any\n  sourcePath: string\n  manifestPath?: string\n}\nconst InternalLink = (props: Props) => {\n  const {\n    label,\n    summary,\n    testId,\n    children,\n    path = '',\n    size = 's',\n    iconType,\n    iconPosition = 'left',\n    toolTip,\n    sourcePath,\n    manifestPath,\n    ...rest\n  } = props\n  const { openPage } = useContext(EnablementAreaContext)\n  const handleOpenPage = () => {\n    if (path) {\n      openPage({ path: sourcePath, manifestPath, label })\n    }\n  }\n\n  const content = (\n    <RiTooltip content={toolTip} anchorClassName={styles.content}>\n      <span className={styles.content}>\n        <div className={styles.title}>{children || label}</div>\n        {!!summary && (\n          <div className={styles.summary}>{truncateText(summary, 140)}</div>\n        )}\n      </span>\n    </RiTooltip>\n  )\n  return (\n    <ListItem\n      data-testid={`internal-link-${testId}`}\n      className={cx(\n        styles.link,\n        iconPosition === 'right' && styles.linkIconRight,\n      )}\n      iconType={iconType}\n      size={size}\n      wrapText\n      color=\"subdued\"\n      onClick={handleOpenPage}\n      label={content}\n      {...rest}\n    />\n  )\n}\n\nexport default InternalLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalLink/index.ts",
    "content": "import InternalLink from './InternalLink'\n\nexport default InternalLink\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalLink/styles.module.scss",
    "content": ".link {\n  width: auto;\n  white-space: nowrap;\n  font: normal normal normal 13px/30px Graphik, sans-serif;\n  font-weight: normal !important;\n\n  & > button {\n    align-items: flex-start;\n    height: auto;\n\n    svg {\n      margin-top: 4px;\n      margin-right: 6px;\n    }\n  }\n}\n\n.linkIconRight {\n  & > button {\n    display: flex;\n    flex-direction: row-reverse;\n    justify-content: space-between;\n\n    svg {\n      margin-right: -8px;\n      margin-left: 12px;\n    }\n  }\n}\n\n.content {\n  max-width: 100%;\n\n  .title {\n    font-weight: 500;\n    font-size: 12px;\n    line-height: 24px;\n    text-decoration: underline;\n\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .summary {\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n\n    font-size: 12px;\n    color: var(--euiColorMediumShade);\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalLink/styles.scss",
    "content": ".euiListGroupItem--xSmall .euiListGroupItem__label {\n  line-height: 12px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalPage/InternalPage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render } from 'uiSrc/utils/test-utils'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  isShowCapabilityTutorialPopover,\n  setCapabilityPopoverShown,\n} from 'uiSrc/services'\nimport { connectedInstanceCDSelector } from 'uiSrc/slices/instances/instances'\nimport { getTutorialCapability } from 'uiSrc/utils'\n\nimport InternalPage, { Props } from './InternalPage'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextCapability: jest.fn().mockReturnValue({\n    source: 'workbench RediSearch',\n  }),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  isShowCapabilityTutorialPopover: jest.fn(),\n  setCapabilityPopoverShown: jest.fn(),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  getTutorialCapability: jest\n    .fn()\n    .mockReturnValue({ path: 'path', telemetryName: 'searchAndQuery' }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceCDSelector: jest.fn().mockReturnValue({\n    free: false,\n  }),\n}))\n\n/**\n * InternalPage tests\n *\n * @group component\n */\ndescribe('InternalPage', () => {\n  it('should render', () => {\n    expect(render(<InternalPage {...instance(mockedProps)} />)).toBeTruthy()\n  })\n  it('should display loader', () => {\n    const { queryByTestId } = render(\n      <InternalPage {...instance(mockedProps)} isLoading />,\n    )\n\n    expect(queryByTestId('enablement-area__page-loader')).toBeTruthy()\n  })\n  it('should display empty prompt on error', () => {\n    const { queryByTestId } = render(\n      <InternalPage {...instance(mockedProps)} error=\"Some error\" />,\n    )\n\n    expect(queryByTestId('enablement-area__empty-prompt')).toBeTruthy()\n  })\n  it('should call onClose function in click BackButton empty prompt on error', () => {\n    const onClose = jest.fn()\n    const { queryByTestId } = render(\n      <InternalPage {...instance(mockedProps)} onClose={onClose} />,\n    )\n\n    const button = queryByTestId(/enablement-area__page-close/)\n    fireEvent.click(button as Element)\n\n    expect(onClose).toBeCalled()\n  })\n  it('should parse and render JSX string', () => {\n    const content = '<h1 data-testid=\"header\">Header</h1>'\n    const { queryByTestId } = render(\n      <InternalPage {...instance(mockedProps)} content={content} />,\n    )\n\n    expect(queryByTestId('header')).toBeInTheDocument()\n  })\n\n  describe('capability', () => {\n    beforeEach(() => {\n      ;(connectedInstanceCDSelector as jest.Mock).mockReturnValueOnce({\n        free: true,\n      })\n    })\n    it('should call isShowCapabilityTutorialPopover, setCapabilityPopoverShown and getTutorialCapability', async () => {\n      const isShowCapabilityTutorialPopoverMock = jest.fn()\n      const setCapabilityPopoverShownMock = jest.fn()\n      ;(isShowCapabilityTutorialPopover as jest.Mock).mockImplementation(\n        () => isShowCapabilityTutorialPopoverMock,\n      )\n      ;(setCapabilityPopoverShown as jest.Mock).mockImplementation(\n        () => setCapabilityPopoverShownMock,\n      )\n\n      render(<InternalPage {...instance(mockedProps)} />)\n\n      expect(isShowCapabilityTutorialPopover).toBeCalled()\n      expect(setCapabilityPopoverShown).toBeCalled()\n      expect(getTutorialCapability).toBeCalled()\n    })\n\n    it('should send CAPABILITY_POPOVER_DISPLAYED telemetry event', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(<InternalPage {...instance(mockedProps)} />)\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.CAPABILITY_POPOVER_DISPLAYED,\n        eventData: {\n          databaseId: 'instanceId',\n          capabilityName: 'searchAndQuery',\n        },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx",
    "content": "import React, { useMemo, useRef, useEffect, useState } from 'react'\nimport JsxParser from 'react-jsx-parser'\nimport cx from 'classnames'\nimport { debounce } from 'lodash'\nimport { useLocation, useParams } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\n\nimport { ChevronLeftIcon, RocketIcon } from 'uiSrc/components/base/icons'\nimport { HorizontalRule, LoadingContent } from 'uiSrc/components'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { IEnablementAreaItem } from 'uiSrc/slices/interfaces'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { getTutorialCapability, Nullable } from 'uiSrc/utils'\n\nimport { appContextCapability } from 'uiSrc/slices/app/context'\nimport {\n  isShowCapabilityTutorialPopover,\n  setCapabilityPopoverShown,\n} from 'uiSrc/services'\nimport { connectedInstanceCDSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  Image,\n  RedisUploadButton,\n  CloudLink,\n  RedisInsightLink,\n} from 'uiSrc/components/markdown'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { getTutorialSection } from '../../utils'\nimport { EmptyPrompt, Pagination, Code } from '..'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  onClose: () => void\n  title: string\n  backTitle: string\n  content: string\n  isLoading?: boolean\n  error?: string\n  scrollTop?: number\n  onScroll?: (top: number) => void\n  activeKey?: Nullable<string>\n  path: string\n  manifestPath?: Nullable<string>\n  sourcePath: string\n  pagination?: IEnablementAreaItem[]\n}\nconst InternalPage = (props: Props) => {\n  const location = useLocation()\n  const {\n    onClose,\n    title,\n    backTitle,\n    isLoading,\n    error,\n    content,\n    onScroll,\n    scrollTop,\n    pagination,\n    activeKey,\n    path,\n    manifestPath,\n    sourcePath,\n  } = props\n  const components: any = {\n    Image,\n    Code,\n    RedisUploadButton,\n    CloudLink,\n    RedisInsightLink,\n    Link,\n  }\n  const containerRef = useRef<HTMLDivElement>(null)\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { source } = useSelector(appContextCapability)\n  const { free = false } = useSelector(connectedInstanceCDSelector) ?? {}\n  const [showCapabilityPopover, setShowCapabilityPopover] = useState(false)\n  const tutorialCapability = getTutorialCapability(source!)\n\n  const handleScroll = debounce(() => {\n    if (containerRef.current && onScroll) {\n      onScroll(containerRef.current.scrollTop)\n    }\n  }, 500)\n\n  const sendEventClickExternalLinkTelemetry = (link: string = '') => {\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_LINK_CLICKED,\n      eventData: {\n        path,\n        link,\n        section: getTutorialSection(manifestPath),\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n      },\n    })\n  }\n\n  const handleClick = (event: React.MouseEvent<HTMLElement>) => {\n    const target = event.target as HTMLElement\n\n    // send telemetry event after click on an external link\n    if (target?.getAttribute('href') && target?.getAttribute('target')) {\n      sendEventClickExternalLinkTelemetry(target?.innerText)\n    }\n  }\n\n  useEffect(() => {\n    if (isShowCapabilityTutorialPopover(free) && !!tutorialCapability?.path) {\n      setShowCapabilityPopover(true)\n      setCapabilityPopoverShown()\n      sendEventTelemetry({\n        event: TelemetryEvent.CAPABILITY_POPOVER_DISPLAYED,\n        eventData: {\n          capabilityName: tutorialCapability.telemetryName,\n          databaseId: instanceId,\n        },\n      })\n    }\n  }, [free])\n\n  useEffect(() => {\n    if (!isLoading && !error && containerRef.current) {\n      if (location?.hash) {\n        const target = containerRef.current?.querySelector(\n          location.hash,\n        ) as HTMLElement\n        if (target) {\n          // HACK: force scroll to element for electron app\n          target.setAttribute('tabindex', '-1')\n          target?.focus()\n          return\n        }\n      }\n\n      if (scrollTop && containerRef.current?.scrollTop === 0) {\n        requestAnimationFrame(() =>\n          setTimeout(() => {\n            containerRef.current?.scroll(0, scrollTop)\n          }, 0),\n        )\n      }\n    }\n  }, [isLoading, location])\n\n  const contentComponent = useMemo(\n    () => (\n      // @ts-ignore\n      <JsxParser\n        bindings={{ path }}\n        components={components}\n        blacklistedTags={['iframe', 'script']}\n        autoCloseVoidElements\n        jsx={content}\n        onError={(e) => console.error(e)}\n      />\n    ),\n    [content],\n  )\n\n  return (\n    <div className={styles.container} data-test-subj=\"internal-page\">\n      <div className={styles.header}>\n        <div style={{ padding: 0 }}>\n          <RiPopover\n            panelClassName={cx('popoverLikeTooltip', styles.popover)}\n            anchorClassName={styles.popoverAnchor}\n            anchorPosition=\"leftCenter\"\n            isOpen={showCapabilityPopover}\n            panelPaddingSize=\"m\"\n            closePopover={() => setShowCapabilityPopover(false)}\n            button={\n              <div className={styles.backButton}>\n                <EmptyButton\n                  data-testid=\"enablement-area__page-close\"\n                  icon={ChevronLeftIcon}\n                  onClick={onClose}\n                  aria-label=\"Back\"\n                >\n                  {backTitle}\n                </EmptyButton>\n              </div>\n            }\n          >\n            <div data-testid=\"explore-capability-popover\">\n              <RocketIcon className={styles.rocketIcon} />\n              <Text className={styles.popoverTitle}>Explore Redis</Text>\n              <Text className={styles.popoverText}>\n                {'You expressed interest in learning about the '}\n                <b>{tutorialCapability?.name}</b>. Try this tutorial to get\n                started.\n              </Text>\n            </div>\n          </RiPopover>\n        </div>\n        <div>\n          <HorizontalRule margin=\"xs\" />\n        </div>\n        <div>\n          <Text className={styles.pageTitle} color=\"default\">\n            {title?.toUpperCase()}\n          </Text>\n        </div>\n      </div>\n      <div\n        ref={containerRef}\n        className={cx(styles.content, 'jsx-markdown')}\n        onScroll={handleScroll}\n        onClick={handleClick}\n        role=\"none\"\n        data-testid=\"enablement-area__page\"\n      >\n        {isLoading && (\n          <LoadingContent\n            data-testid=\"enablement-area__page-loader\"\n            lines={3}\n          />\n        )}\n        {!isLoading && error && <EmptyPrompt />}\n        {!isLoading && !error && contentComponent}\n      </div>\n      {!!pagination?.length && (\n        <>\n          <div className={styles.footer}>\n            <Pagination\n              sourcePath={sourcePath}\n              items={pagination}\n              activePageKey={activeKey}\n              compressed\n            />\n          </div>\n        </>\n      )}\n    </div>\n  )\n}\n\nexport default InternalPage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalPage/index.ts",
    "content": "import InternalPage from './InternalPage'\n\nexport default InternalPage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/InternalPage/styles.module.scss",
    "content": ".container {\n  min-height: 1px;\n  height: 100%;\n  background-color: var(--euiColorEmptyShade);\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--euiTextSubduedColor) !important;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n}\n\n.header {\n  padding: 6px 0;\n  width: 100%;\n  background-color: var(--euiColorEmptyShade);\n  & > div {\n    width: 100%;\n    padding: 0 16px;\n  }\n  :global(.euiPopover) {\n    width: 100%;\n  }\n}\n\n.footer {\n  width: 100%;\n  background-color: var(--euiColorEmptyShade);\n}\n.pageTitle {\n  margin: 8px 0 4px;\n  font:\n    normal normal 500 14px/24px Graphik,\n    sans-serif;\n}\n.backButton {\n  padding: 0 4px;\n  max-height: 30px;\n  line-height: 24px;\n  width: 100%;\n  & > button {\n    font: normal normal 14px/24px Graphik,\n    sans-serif !important;\n    text-decoration: none;\n    color: var(--euiTextSubduedColor) !important;\n\n    & > span {\n      justify-content: flex-start;\n    }\n\n    &:hover {\n      background-color: var(--hoverInListColorDarken);\n      color: var(--euiTextColor) !important;\n      text-decoration: none !important;\n    }\n  }\n}\n.content {\n  @include eui.scrollBar;\n  padding: 8px 12px 4px 16px !important;\n  overflow: auto;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n}\n\n.popover {\n  min-width: 356px !important;\n  width: 356px;\n}\n\n.popoverAnchor {\n  width: 100%;\n}\n\n.rocketIcon {\n  position: absolute;\n  margin-top: 6px;\n}\n\n.popoverTitle,\n.popoverText {\n  padding-left: 45px;\n}\n\n.popoverTitle {\n  font-size: 16px !important;\n  font-weight: 500 !important;\n}\n.popoverText {\n  padding-top: 4px;\n  font-size: 14px !important;\n  line-height: 1.1rem !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport LazyInternalPage, { Props } from './LazyInternalPage'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\n/**\n * LazyInternalPage tests\n *\n * @group component\n */\ndescribe('LazyInternalPage', () => {\n  it('should render', () => {\n    expect(render(<LazyInternalPage {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { startCase } from 'lodash'\nimport { useHistory } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\nimport axios, { AxiosError } from 'axios'\n\nimport { getApiErrorMessage, isStatusSuccessful, Nullable } from 'uiSrc/utils'\nimport { resourcesService } from 'uiSrc/services'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\nimport { IEnablementAreaItem } from 'uiSrc/slices/interfaces'\nimport { workbenchTutorialsSelector } from 'uiSrc/slices/workbench/wb-tutorials'\nimport { workbenchCustomTutorialsSelector } from 'uiSrc/slices/workbench/wb-custom-tutorials'\n\nimport {\n  explorePanelSelector,\n  resetExplorePanelSearch,\n  setExplorePanelContent,\n  setExplorePanelSearch,\n  setExplorePanelScrollTop,\n} from 'uiSrc/slices/panels/sidePanels'\nimport FormatSelector from 'uiSrc/services/formatter/FormatSelector'\nimport InternalPage from '../InternalPage'\nimport {\n  getFileInfo,\n  getPagesInsideGroup,\n  IFileInfo,\n} from '../../utils/getFileInfo'\n\ninterface IPageData extends IFileInfo {\n  content: string\n  relatedPages?: IEnablementAreaItem[]\n}\nconst DEFAULT_PAGE_DATA: IPageData = {\n  label: '',\n  content: '',\n  name: '',\n  parent: '',\n  extension: '',\n  location: '',\n  relatedPages: [],\n  parents: [],\n}\n\nexport interface Props {\n  onClose: () => void\n  title?: string\n  path: string\n  manifest: Nullable<IEnablementAreaItem[]>\n  manifestPath?: Nullable<string>\n  sourcePath: string\n  search: string\n}\n\nconst LazyInternalPage = ({\n  onClose,\n  title,\n  path,\n  sourcePath,\n  manifest,\n  manifestPath,\n  search,\n}: Props) => {\n  const history = useHistory()\n  const {\n    itemScrollTop,\n    data: contentContext,\n    url,\n  } = useSelector(explorePanelSelector)\n  const { loading: tutorialsLoading } = useSelector(workbenchTutorialsSelector)\n  const { loading: customTutorialsLoading } = useSelector(\n    workbenchCustomTutorialsSelector,\n  )\n  const [isLoading, setLoading] = useState<boolean>(false)\n  const [error, setError] = useState<string>('')\n  const [pageData, setPageData] = useState<IPageData>(DEFAULT_PAGE_DATA)\n  const dispatch = useDispatch()\n  const fetchService = IS_ABSOLUTE_PATH.test(path) ? axios : resourcesService\n\n  const scrollTopRef = useRef(0)\n\n  useEffect(\n    () => () => {\n      dispatch(setExplorePanelScrollTop(scrollTopRef.current))\n    },\n    [],\n  )\n\n  useEffect(() => {\n    const startLoadContent = async () => {\n      await loadContent()\n    }\n    startLoadContent()\n  }, [path, sourcePath])\n\n  const isMarkdownLoading =\n    isLoading || tutorialsLoading || customTutorialsLoading\n  const getRelatedPages = () =>\n    manifest ? getPagesInsideGroup(manifest, manifestPath) : []\n  const loadContent = async () => {\n    setLoading(true)\n    setError('')\n\n    const pageInfo = getFileInfo({ manifestPath, path }, manifest)\n    const relatedPages = getRelatedPages()\n    setPageData({ ...DEFAULT_PAGE_DATA, ...pageInfo, relatedPages })\n\n    try {\n      const formatter = FormatSelector.selectFor(pageInfo.extension)\n      let content = contentContext\n\n      // if we have already downloaded content we take it from store\n      if (url !== path || !contentContext) {\n        const { data, status } = await fetchService.get<string>(path)\n        if (isStatusSuccessful(status)) {\n          content = data\n          dispatch(setExplorePanelContent({ data, url: path }))\n        }\n      }\n\n      dispatch(setExplorePanelSearch(search))\n      const contentData = await formatter.format(\n        { data: content, path },\n        { history },\n      )\n      setPageData((prevState) => ({ ...prevState, content: contentData }))\n      setLoading(false)\n    } catch (error) {\n      setLoading(false)\n      const errorMessage: string = getApiErrorMessage(error as AxiosError)\n      dispatch(resetExplorePanelSearch())\n      dispatch(setExplorePanelContent({ data: null, url: null }))\n      setError(errorMessage)\n    }\n  }\n\n  const handlePageScroll = (top: number) => {\n    scrollTopRef.current = top\n  }\n\n  return (\n    <InternalPage\n      activeKey={pageData._key}\n      path={path}\n      manifestPath={manifestPath}\n      sourcePath={sourcePath}\n      onClose={onClose}\n      title={startCase(title || pageData.label)}\n      backTitle={startCase(pageData?.parent)}\n      isLoading={isMarkdownLoading}\n      content={pageData.content}\n      error={error}\n      onScroll={handlePageScroll}\n      scrollTop={itemScrollTop}\n      pagination={pageData.relatedPages}\n    />\n  )\n}\n\nexport default LazyInternalPage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/LazyInternalPage/index.ts",
    "content": "import LazyInternalPage from './LazyInternalPage'\n\nexport default LazyInternalPage\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Navigation/Navigation.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport {\n  MOCK_CUSTOM_TUTORIALS_ITEMS,\n  MOCK_TUTORIALS_ITEMS,\n} from 'uiSrc/constants'\nimport Navigation from './Navigation'\n\nconst guides = {\n  tutorials: MOCK_TUTORIALS_ITEMS,\n  customTutorials: MOCK_CUSTOM_TUTORIALS_ITEMS,\n}\n\ndescribe('Navigation', () => {\n  it('should render', () => {\n    expect(\n      render(<Navigation {...guides} isInternalPageVisible />),\n    ).toBeTruthy()\n  })\n\n  it('should render navigation groups', () => {\n    render(<Navigation {...guides} isInternalPageVisible />)\n\n    expect(screen.queryByTestId('accordion-tutorials')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('accordion-custom-tutorials'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Navigation/Navigation.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { isArray } from 'lodash'\nimport { useParams } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Group as ListGroup } from 'uiSrc/components/base/layout/list'\nimport {\n  EnablementAreaComponent,\n  IEnablementAreaItem,\n} from 'uiSrc/slices/interfaces'\n\nimport {\n  ApiEndpoints,\n  EAItemActions,\n  EAManifestFirstKey,\n  FeatureFlags,\n} from 'uiSrc/constants'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  deleteCustomTutorial,\n  setWbCustomTutorialsState,\n  uploadCustomTutorial,\n} from 'uiSrc/slices/workbench/wb-custom-tutorials'\n\nimport UploadWarning from 'uiSrc/components/upload-warning'\nimport {\n  appFeatureFlagsFeaturesSelector,\n  appFeatureOnboardingSelector,\n} from 'uiSrc/slices/app/features'\nimport { OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { FormValues } from '../UploadTutorialForm/UploadTutorialForm'\n\nimport Group from '../Group'\nimport InternalLink from '../InternalLink'\nimport PlainText from '../PlainText'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport UploadTutorialForm from '../UploadTutorialForm'\n\nimport styles from './styles.module.scss'\n\nconst padding = 16\n\nexport interface Props {\n  tutorials: IEnablementAreaItem[]\n  customTutorials: IEnablementAreaItem[]\n  isInternalPageVisible: boolean\n}\n\nconst CUSTOM_TUTORIALS_ID = 'custom-tutorials'\n\nconst PATHS = {\n  guides: {\n    sourcePath: ApiEndpoints.GUIDES_PATH,\n    manifestPath: EAManifestFirstKey.GUIDES,\n  },\n  tutorials: {\n    sourcePath: ApiEndpoints.TUTORIALS_PATH,\n    manifestPath: EAManifestFirstKey.TUTORIALS,\n  },\n  customTutorials: {\n    sourcePath: ApiEndpoints.CUSTOM_TUTORIALS_PATH,\n    manifestPath: EAManifestFirstKey.CUSTOM_TUTORIALS,\n  },\n}\n\nconst Navigation = (props: Props) => {\n  const { tutorials, customTutorials, isInternalPageVisible } = props\n  const { currentStep, isActive } = useSelector(appFeatureOnboardingSelector)\n  const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const [isCreateOpen, setIsCreateOpen] = useState(false)\n\n  const dispatch = useDispatch()\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const isCustomTutorialsOnboarding =\n    currentStep === OnboardingSteps.CustomTutorials && isActive\n\n  useEffect(\n    () => () => {\n      dispatch(setWbCustomTutorialsState())\n    },\n    [],\n  )\n\n  // Open form when onboarding is triggered and form is not visible\n  useEffect(() => {\n    if (\n      isCustomTutorialsOnboarding &&\n      customTutorials?.length > 0 &&\n      !isCreateOpen\n    ) {\n      setIsCreateOpen(true)\n    }\n  }, [isCustomTutorialsOnboarding, customTutorials?.length, isCreateOpen])\n\n  const submitCreate = ({ file, link }: FormValues) => {\n    const formData = new FormData()\n\n    if (file) {\n      formData.append('file', file)\n    } else {\n      formData.append('link', link)\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_IMPORT_SUBMITTED,\n      eventData: {\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        source: file ? 'Upload' : 'URL',\n      },\n    })\n\n    dispatch(\n      uploadCustomTutorial(formData, () => {\n        setIsCreateOpen(false)\n        dispatch(setWbCustomTutorialsState(true))\n      }),\n    )\n  }\n\n  const onDeleteCustomTutorial = (id: string) => {\n    dispatch(\n      deleteCustomTutorial(id, () => {\n        sendEventTelemetry({\n          event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_DELETED,\n          eventData: {\n            databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n          },\n        })\n      }),\n    )\n  }\n\n  const renderSwitch = (\n    item: IEnablementAreaItem,\n    {\n      sourcePath,\n      manifestPath = '',\n    }: { sourcePath: string; manifestPath?: string },\n    level: number,\n  ) => {\n    const {\n      label,\n      type,\n      children,\n      id,\n      args,\n      _actions: actions,\n      _path: uriPath,\n      _key: key,\n      summary,\n    } = item\n\n    const paddingsStyle = {\n      paddingLeft: `${padding + level * 0}px`, // Note: Using the accordion component, we don't need to manually increase padding for nested items anymore\n      paddingRight: `${padding}px`,\n    }\n    const currentSourcePath =\n      sourcePath + (uriPath ? `${uriPath}` : (args?.path ?? ''))\n    const currentManifestPath = `${manifestPath}/${key}`\n\n    const isCustomTutorials = id === CUSTOM_TUTORIALS_ID && level === 0\n    const hasChildren = (children?.length ?? 0) > 0\n\n    switch (type) {\n      case EnablementAreaComponent.Group:\n        return (\n          <Group\n            id={id}\n            label={label}\n            actions={actions}\n            isShowFolder={level !== 0}\n            isShowActions={currentSourcePath.startsWith(\n              ApiEndpoints.CUSTOM_TUTORIALS_PATH,\n            )}\n            onCreate={() => setIsCreateOpen((v) => !v)}\n            onDelete={onDeleteCustomTutorial}\n            hasChildren={hasChildren}\n            forceState={\n              isCustomTutorials && isCustomTutorialsOnboarding\n                ? 'open'\n                : undefined\n            }\n            {...args}\n          >\n            {isCustomTutorials &&\n              actions?.includes(EAItemActions.Create) &&\n              (children?.length === 0 ? (\n                <Col gap=\"l\">\n                  <UploadTutorialForm\n                    onSubmit={submitCreate}\n                    isPageOpened={isInternalPageVisible}\n                  />\n                  <UploadWarning />\n                </Col>\n              ) : (\n                isCreateOpen && (\n                  <UploadTutorialForm\n                    onSubmit={submitCreate}\n                    onCancel={() => setIsCreateOpen(false)}\n                    isPageOpened={isInternalPageVisible}\n                  />\n                )\n              ))}\n            {renderTreeView(\n              children ? getManifestItems(children) : [],\n              {\n                sourcePath: currentSourcePath,\n                manifestPath: currentManifestPath,\n              },\n              level + 1,\n            )}\n          </Group>\n        )\n      case EnablementAreaComponent.InternalLink:\n        return (\n          <InternalLink\n            manifestPath={currentManifestPath}\n            sourcePath={currentSourcePath}\n            style={paddingsStyle}\n            testId={id || label}\n            label={label}\n            summary={summary}\n            path={args?.path}\n          >\n            {args?.content || label}\n          </InternalLink>\n        )\n      default:\n        return <PlainText style={paddingsStyle}>{label}</PlainText>\n    }\n  }\n\n  const renderTreeView = (\n    elements: IEnablementAreaItem[],\n    paths: { sourcePath: string; manifestPath?: string },\n    level: number = 0,\n  ) =>\n    elements?.map((item) => (\n      <div className=\"fluid\" key={`${item.id}_${item._key}`}>\n        {renderSwitch(item, paths, level)}\n      </div>\n    ))\n\n  return (\n    <ListGroup\n      style={{ padding: 5 }}\n      gap=\"m\"\n      maxWidth=\"false\"\n      data-testid=\"enablementArea-treeView\"\n      flush\n      className={cx(styles.innerContainer)}\n    >\n      {tutorials &&\n        renderTreeView(getManifestItems(tutorials), PATHS.tutorials)}\n      {customTutorials &&\n        envDependentFeature?.flag &&\n        renderTreeView(\n          getManifestItems(customTutorials),\n          PATHS.customTutorials,\n        )}\n    </ListGroup>\n  )\n}\n\nexport default Navigation\n\nconst getManifestItems = (manifest: IEnablementAreaItem[]) =>\n  isArray(manifest)\n    ? manifest.map((item, index) => ({ ...item, _key: `${index}` }))\n    : []\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Navigation/index.ts",
    "content": "import Navigation from './Navigation'\n\nexport default Navigation\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Navigation/styles.module.scss",
    "content": ".innerContainer {\n  @include eui.scrollBar;\n  overflow: auto;\n  height: 100%;\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--euiTextSubduedColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Pagination/Pagination.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { ApiEndpoints, MOCK_TUTORIALS_ITEMS } from 'uiSrc/constants'\nimport {\n  defaultValue,\n  EnablementAreaProvider,\n} from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\nimport { EnablementAreaComponent } from 'uiSrc/slices/interfaces'\n\nimport Pagination from './Pagination'\n\nconst paginationItems =\n  MOCK_TUTORIALS_ITEMS[0]?.children\n    ?.map((item, index) => ({ ...item, _key: `${index}` }))\n    ?.filter((item) => item.type === EnablementAreaComponent.InternalLink) || []\n\ndescribe('Pagination', () => {\n  it('should render', () => {\n    const component = render(\n      <Pagination\n        sourcePath={ApiEndpoints.GUIDES_PATH}\n        items={paginationItems}\n      />,\n    )\n    const { queryByTestId } = component\n\n    expect(component).toBeTruthy()\n    expect(\n      queryByTestId('enablement-area__prev-page-btn'),\n    ).not.toBeInTheDocument()\n    expect(queryByTestId('enablement-area__next-page-btn')).toBeInTheDocument()\n  })\n  it('should correctly open menu', () => {\n    const { queryByTestId } = render(\n      <Pagination\n        sourcePath={ApiEndpoints.GUIDES_PATH}\n        items={paginationItems}\n        activePageKey=\"0\"\n      />,\n    )\n    fireEvent.click(\n      screen.getByTestId('enablement-area__toggle-pagination-menu-btn'),\n    )\n    const menu = queryByTestId('enablement-area__pagination-menu')\n\n    expect(menu).toBeInTheDocument()\n    expect(menu?.querySelectorAll('[data-testid^=\"menu-item\"]').length).toEqual(\n      paginationItems.length,\n    )\n    expect(menu?.querySelector('.activeMenuItem')).toHaveTextContent(\n      paginationItems[0].label,\n    )\n  })\n  it('should correctly open next page', () => {\n    const openPage = jest.fn()\n    const pageIndex = 0\n\n    render(\n      <EnablementAreaProvider value={{ ...defaultValue, openPage }}>\n        <Pagination\n          sourcePath={ApiEndpoints.GUIDES_PATH}\n          items={paginationItems}\n          activePageKey=\"0\"\n        />\n      </EnablementAreaProvider>,\n    )\n    fireEvent.click(screen.getByTestId('enablement-area__next-page-btn'))\n\n    expect(openPage).toHaveBeenCalledWith({\n      path:\n        ApiEndpoints.GUIDES_PATH + paginationItems[pageIndex + 1]?.args?.path,\n      manifestPath: expect.any(String),\n    })\n  })\n  it('should correctly open previous page', () => {\n    const openPage = jest.fn()\n    const pageIndex = 1\n\n    render(\n      <EnablementAreaProvider value={{ ...defaultValue, openPage }}>\n        <Pagination\n          sourcePath={ApiEndpoints.GUIDES_PATH}\n          items={paginationItems}\n          activePageKey=\"1\"\n        />\n      </EnablementAreaProvider>,\n    )\n    fireEvent.click(screen.getByTestId('enablement-area__prev-page-btn'))\n\n    expect(openPage).toHaveBeenCalledWith({\n      path:\n        ApiEndpoints.GUIDES_PATH + paginationItems[pageIndex - 1]?.args?.path,\n      manifestPath: expect.any(String),\n    })\n  })\n  it('should correctly open by using pagination menu', async () => {\n    const openPage = jest.fn()\n    const ACTIVE_PAGE_KEY = '0'\n    const { queryByTestId } = render(\n      <EnablementAreaProvider value={{ ...defaultValue, openPage }}>\n        <Pagination\n          sourcePath={ApiEndpoints.GUIDES_PATH}\n          items={paginationItems}\n          activePageKey={ACTIVE_PAGE_KEY}\n        />\n      </EnablementAreaProvider>,\n    )\n\n    const toggleMenuBtnId = 'enablement-area__toggle-pagination-menu-btn'\n    for (let i = 0; i < paginationItems.length; i++) {\n      const pageItem = paginationItems[i]\n\n      if (pageItem._key !== ACTIVE_PAGE_KEY) {\n        // Reopen the menu each time\n        fireEvent.click(screen.getByTestId(toggleMenuBtnId))\n\n        const menu = queryByTestId('enablement-area__pagination-menu')\n        expect(menu).not.toBeNull()\n\n        const menuItem = menu?.querySelector(\n          `[data-testid=\"menu-item-${pageItem._key}\"]`,\n        )\n        expect(menuItem).not.toBeNull()\n\n        fireEvent.click(menuItem as Element)\n      }\n    }\n\n    expect(openPage).toHaveBeenCalledTimes(paginationItems.length - 1) // -1 because active item should not be clickable\n    expect(openPage).toHaveBeenLastCalledWith({\n      path:\n        ApiEndpoints.GUIDES_PATH +\n        paginationItems[paginationItems.length - 1]?.args?.path,\n      manifestPath: expect.any(String),\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Pagination/Pagination.tsx",
    "content": "import React, { useContext, useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { isNil } from 'lodash'\nimport { ChevronLeftIcon, ChevronRightIcon } from 'uiSrc/components/base/icons'\nimport { IEnablementAreaItem } from 'uiSrc/slices/interfaces'\nimport EnablementAreaContext from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  Menu,\n  MenuContent,\n  MenuDropdownArrow,\n  MenuItem,\n  MenuTrigger,\n} from 'uiSrc/components/base/layout/menu'\nimport { Text } from 'uiSrc/components/base/text'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  items: IEnablementAreaItem[]\n  sourcePath: string\n  activePageKey?: Nullable<string>\n  compressed?: boolean\n}\n\nconst Pagination = ({\n  items = [],\n  sourcePath,\n  activePageKey,\n  compressed,\n}: Props) => {\n  const [isMenuOpen, setMenuOpen] = useState(false)\n  const [activePage, setActivePage] = useState(0)\n  const { openPage } = useContext(EnablementAreaContext)\n\n  useEffect(() => {\n    if (activePageKey) {\n      const index = items.findIndex((item) => item._key === activePageKey)\n      setActivePage(index)\n    }\n  }, [activePageKey])\n\n  const toggleMenuOpen = () => {\n    setMenuOpen(!isMenuOpen)\n  }\n\n  const closeMenu = () => {\n    setMenuOpen(false)\n  }\n\n  const handleOpenPage = (index: number) => {\n    const path = items[index]?.args?.path\n    const groupPath = items[index]?._groupPath\n    const key = items[index]?._key\n\n    closeMenu()\n    if (index !== activePage && openPage && path) {\n      openPage({\n        path: sourcePath + path,\n        manifestPath: !isNil(key) ? `${groupPath}/${key}` : '',\n      })\n\n      // Scroll the context panel to top\n      const panel = document.querySelector(\n        '[data-testid=\"enablement-area__page\"]',\n      )\n      if (panel) {\n        panel.scrollTop = 0\n      }\n    }\n  }\n\n  const PagesControl = () => (\n    <Menu open={isMenuOpen}>\n      <MenuTrigger>\n        <button\n          data-testid=\"enablement-area__toggle-pagination-menu-btn\"\n          type=\"button\"\n          onClick={toggleMenuOpen}\n        >\n          <Text size=\"S\">\n            <strong\n              className={styles.underline}\n            >{`${activePage + 1} of ${items.length}`}</strong>\n          </Text>\n        </button>\n      </MenuTrigger>\n      <MenuContent\n        data-testid=\"enablement-area__pagination-menu\"\n        placement=\"top\"\n        onInteractOutside={() => setMenuOpen(false)}\n      >\n        {items.map((item, index) => (\n          <MenuItem\n            data-testid={`menu-item-${index}`}\n            key={item.id}\n            onClick={() => handleOpenPage(index)}\n            text={item.label}\n            className={cx({ [styles.activeMenuItem]: activePage === index })}\n          />\n        ))}\n        <MenuDropdownArrow />\n      </MenuContent>\n    </Menu>\n  )\n\n  const size = compressed ? 'small' : 'medium'\n  return (\n    <div\n      className={cx(styles.pagination, {\n        [styles.paginationCompressed]: compressed,\n      })}\n    >\n      <div>\n        {activePage > 0 && (\n          <PrimaryButton\n            aria-label=\"Previous page\"\n            data-testid=\"enablement-area__prev-page-btn\"\n            icon={ChevronLeftIcon}\n            iconSide=\"left\"\n            onClick={() => handleOpenPage(activePage - 1)}\n            size={size}\n            className={cx(styles.prevPage, {\n              [styles.prevPageCompressed]: compressed,\n            })}\n          >\n            Back\n          </PrimaryButton>\n        )}\n      </div>\n      <div>\n        <PagesControl />\n      </div>\n      <div>\n        {activePage < items.length - 1 && (\n          <PrimaryButton\n            aria-label=\"Next page\"\n            data-testid=\"enablement-area__next-page-btn\"\n            icon={ChevronRightIcon}\n            iconSide=\"right\"\n            onClick={() => handleOpenPage(activePage + 1)}\n            className={cx(styles.nextPage, {\n              [styles.nextPageCompressed]: compressed,\n            })}\n            size={size}\n          >\n            Next\n          </PrimaryButton>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport default Pagination\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Pagination/index.ts",
    "content": "import Pagination from './Pagination'\n\nexport default Pagination\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/Pagination/styles.module.scss",
    "content": ".pagination {\n  padding: 12px 16px;\n  width: 100%;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  border-top: 1px solid var(--separatorColor);\n  & > div:first-of-type,\n  & > div:last-of-type {\n    min-width: 94px;\n  }\n}\n\n.paginationCompressed {\n  padding: 8px 16px;\n  & > div:first-of-type,\n  & > div:last-of-type {\n    min-width: 70px;\n  }\n\n  :global(.euiButtonContent .euiIcon) {\n    width: 14px;\n    height: 14px;\n  }\n}\n\n.prevPage,\n.nextPage {\n  & > span {\n    justify-content: flex-start;\n  }\n}\n\n.activeMenuItem {\n  background-color: var(--euiColorPrimary) !important;\n  color: var(--euiColorEmptyShade) !important;\n}\n\n.underline {\n  text-decoration: underline;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/PlainText/PlainText.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport PlainText, { Props } from './PlainText'\n\nconst mockedProps = mock<Props>()\n\ndescribe('PlainText', () => {\n  it('should render', () => {\n    expect(render(<PlainText {...instance(mockedProps)} />)).toBeTruthy()\n  })\n  it('should contain provided children', () => {\n    const text = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.'\n    const { container } = render(\n      <PlainText {...instance(mockedProps)}>\n        <span>{text}</span>\n      </PlainText>,\n    )\n    expect(container).toHaveTextContent(text)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/PlainText/PlainText.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport interface Props {\n  children: React.ReactElement | string\n  style?: any\n}\nconst PlainText = ({ children, ...rest }: Props) => (\n  <Text\n    style={{ whiteSpace: 'nowrap', width: 'auto', ...rest.style }}\n    color=\"subdued\"\n    size=\"m\"\n  >\n    {children}\n  </Text>\n)\n\nexport default PlainText\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/PlainText/index.ts",
    "content": "import PlainText from './PlainText'\n\nexport default PlainText\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent, act } from 'uiSrc/utils/test-utils'\n\nimport UploadTutorialForm from './UploadTutorialForm'\n\ndescribe('UploadTutorialForm', () => {\n  it('should render', () => {\n    expect(render(<UploadTutorialForm onSubmit={jest.fn()} />)).toBeTruthy()\n  })\n\n  it('should render disabled submit button by default', () => {\n    render(<UploadTutorialForm onSubmit={jest.fn()} />)\n    expect(screen.getByTestId('submit-upload-tutorial-btn')).toBeDisabled()\n  })\n\n  it('should call onSubmit with proper data', async () => {\n    const mockOnSubmit = jest.fn()\n    render(<UploadTutorialForm onSubmit={mockOnSubmit} />)\n\n    const jsonString = JSON.stringify({})\n    const blob = new Blob([jsonString])\n    const file = new File([blob], 'file', {\n      type: 'application/JSON',\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-tutorial'), {\n        target: { files: [file] },\n      })\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('submit-upload-tutorial-btn'))\n    })\n\n    expect(mockOnSubmit).toBeCalledWith({ file: expect.any(Object), link: '' })\n  })\n\n  it('should call onSubmit with proper data without file', async () => {\n    const mockOnSubmit = jest.fn()\n    render(<UploadTutorialForm onSubmit={mockOnSubmit} />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('tutorial-link-field'), {\n        target: { value: 'link' },\n      })\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('submit-upload-tutorial-btn'))\n    })\n\n    expect(mockOnSubmit).toBeCalledWith({ file: null, link: 'link' })\n  })\n\n  it('should call onCancel', () => {\n    const onCancel = jest.fn()\n    render(<UploadTutorialForm onSubmit={jest.fn()} onCancel={onCancel} />)\n\n    fireEvent.click(screen.getByTestId('cancel-upload-tutorial-btn'))\n    expect(onCancel).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.styles.ts",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Wrapper = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  border: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral600};\n  border-radius: ${({ theme }: { theme: Theme }) =>\n    theme.components.card.borderRadius};\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/UploadTutorialForm.tsx",
    "content": "import React, { useState } from 'react'\nimport { useFormik } from 'formik'\nimport { FormikErrors } from 'formik/dist/types'\nimport { isEmpty } from 'lodash'\nimport cx from 'classnames'\n\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { Nullable } from 'uiSrc/utils'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { RiFilePicker, RiTooltip, OnboardingTour } from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport CreateTutorialLink from '../CreateTutorialLink'\n\nimport * as S from './UploadTutorialForm.styles'\nimport styles from './styles.module.scss'\n\nexport interface FormValues {\n  file: Nullable<File>\n  link: string\n}\n\nexport interface Props {\n  onSubmit: (data: FormValues) => void\n  onCancel?: () => void\n  isPageOpened?: boolean\n}\n\nconst UploadTutorialForm = (props: Props) => {\n  const { onSubmit, onCancel, isPageOpened } = props\n  const [errors, setErrors] = useState<FormikErrors<FormValues>>({})\n\n  const initialValues: FormValues = {\n    file: null,\n    link: '',\n  }\n\n  const isSubmitDisabled = !isEmpty(errors)\n\n  const validate = (values: FormValues) => {\n    const errs: FormikErrors<FormValues> = {}\n    if (!values.file && !values.link) errs.file = 'Tutorial Archive or Link'\n\n    setErrors(errs)\n    return errs\n  }\n\n  const formik = useFormik({\n    initialValues,\n    validate,\n    validateOnMount: true,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      onSubmit(values)\n    },\n  })\n\n  const getSubmitButtonContent = (isSubmitDisabled?: boolean) => {\n    const maxErrorsCount = 5\n    const errorsArr = Object.values(errors).map((err) => [\n      err,\n      <br key={err} />,\n    ])\n\n    if (errorsArr.length > maxErrorsCount) {\n      errorsArr.splice(maxErrorsCount, errorsArr.length, ['...'])\n    }\n    return isSubmitDisabled ? <span>{errorsArr}</span> : null\n  }\n\n  const handleFileChange = (files: FileList | null) => {\n    formik.setFieldValue('file', files?.[0] ?? null)\n  }\n\n  return (\n    <S.Wrapper className={styles.wrapper} data-testid=\"upload-tutorial-form\">\n      <OnboardingTour\n        options={ONBOARDING_FEATURES.EXPLORE_CUSTOM_TUTORIALS}\n        anchorPosition=\"downLeft\"\n        anchorWrapperClassName=\"onboardingPopoverAnchor\"\n        panelClassName={cx({ hide: isPageOpened })}\n        preventPropagation\n      >\n        <Text>Add new tutorial</Text>\n      </OnboardingTour>\n      <Spacer size=\"m\" />\n      <div>\n        <div className={styles.uploadFileWrapper}>\n          <RiFilePicker\n            id=\"import-tutorial\"\n            initialPromptText=\"Select or drop a file\"\n            className={styles.fileDrop}\n            onChange={handleFileChange}\n            display=\"large\"\n            accept=\".zip\"\n            data-testid=\"import-tutorial\"\n            aria-label=\"Select or drop file\"\n          />\n        </div>\n        <div className={styles.hr}>OR</div>\n        <TextInput\n          placeholder=\"GitHub link to tutorials\"\n          value={formik.values.link}\n          onChange={(value) => formik.setFieldValue('link', value)}\n          className={styles.input}\n          data-testid=\"tutorial-link-field\"\n        />\n        <Spacer size=\"l\" />\n        <div className={styles.footer}>\n          <CreateTutorialLink />\n          <Row align=\"center\" justify=\"end\" gap=\"s\">\n            {onCancel && (\n              <SecondaryButton\n                size=\"s\"\n                onClick={() => onCancel()}\n                data-testid=\"cancel-upload-tutorial-btn\"\n              >\n                Cancel\n              </SecondaryButton>\n            )}\n            <RiTooltip\n              position=\"top\"\n              anchorClassName=\"euiToolTip__btn-disabled\"\n              title={\n                isSubmitDisabled\n                  ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length)\n                  : null\n              }\n              content={getSubmitButtonContent(isSubmitDisabled)}\n            >\n              <PrimaryButton\n                size=\"s\"\n                onClick={() => formik.handleSubmit()}\n                icon={isSubmitDisabled ? InfoIcon : undefined}\n                disabled={isSubmitDisabled}\n                data-testid=\"submit-upload-tutorial-btn\"\n              >\n                Submit\n              </PrimaryButton>\n            </RiTooltip>\n          </Row>\n        </div>\n      </div>\n    </S.Wrapper>\n  )\n}\n\nexport default UploadTutorialForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/index.ts",
    "content": "import UploadTutorialForm from './UploadTutorialForm'\n\nexport default UploadTutorialForm\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/UploadTutorialForm/styles.module.scss",
    "content": ".wrapper {\n  .input {\n    height: 34px !important;\n    background: var(--euiColorEmptyShade) !important;\n  }\n\n  .uploadFileWrapper {\n    text-align: right;\n    margin-top: -8px;\n  }\n\n  .uploadFileName {\n    display: flex;\n    align-items: center;\n    padding-left: 4px;\n  }\n\n  .uploadFileNameTitle {\n    max-width: calc(100% - 30px);\n  }\n}\n\n.btnSubmit {\n  svg {\n    width: 16px !important;\n    height: 16px !important;\n    margin-top: 0;\n  }\n}\n\n.fileDrop {\n  width: 100%;\n  margin-top: 14px;\n\n  :global {\n    .RI-File-Picker__clearButton, .RI-File-Picker__clearButton .euiButtonEmpty__text {\n      color: var(--externalLinkColor) !important;\n      text-transform: lowercase;\n    }\n\n    .RI-File-Picker__showDrop .RI-File-Picker__prompt, .RI-File-Picker__input:focus + .RI-File-Picker__prompt {\n      background-color: var(--euiColorEmptyShade);\n    }\n\n    .RI-File-Picker__prompt {\n      background-color: var(--euiColorEmptyShade);\n      height: 120px;\n      border-radius: 4px;\n      box-shadow: none;\n      border: 1px dashed var(--controlsBorderColor);\n    }\n\n    .RI-File-Picker__clearButton {\n      margin-top: 4px;\n    }\n  }\n}\n\n.hr {\n  margin: 12px 0;\n  width: 100%;\n  text-align: center;\n  position: relative;\n\n  &:before, &:after {\n    content: '';\n    display: block;\n    width: 40%;\n    height: 1px;\n    background: var(--tableDarkestBorderColor);\n    position: absolute;\n    top: 50%;\n  }\n\n  &:before {\n    left: 0;\n  }\n\n  &:after {\n    right: 0;\n  }\n}\n\n.footer {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/components/index.ts",
    "content": "import EmptyPrompt from './EmptyPrompt'\nimport InternalPage from './InternalPage'\nimport LazyInternalPage from './LazyInternalPage'\nimport Pagination from './Pagination'\nimport Navigation from './Navigation'\nimport Code from './Code'\n\nexport {\n  EmptyPrompt,\n  InternalPage,\n  LazyInternalPage,\n  Pagination,\n  Navigation,\n  Code,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/index.ts",
    "content": "import EnablementArea from './EnablementArea'\n\nexport default EnablementArea\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/styles.module.scss",
    "content": ".container {\n  overflow: hidden;\n  height: 100%;\n  flex-grow: 1;\n  margin-top: 8px;\n}\n\n.innerContainerLoader {\n  padding: 12px 16px !important;\n}\n\n.internalPage {\n  width: 100%;\n  max-width: 100%;\n  position: absolute;\n  top: 0;\n  height: 100%;\n  transform: translateX(calc(100% + 16px));\n  backface-visibility: hidden;\n  //transition: transform 0.4s ease-in-out;\n  box-shadow: -5px 1px 10px rgba(0, 0, 0, 0.2);\n  z-index: 2;\n}\n\n.internalPageVisible {\n  transform: translateX(0);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/utils/getFileInfo.ts",
    "content": "import { forEach, get } from 'lodash'\nimport { API_URL, ApiEndpoints, EAManifestFirstKey } from 'uiSrc/constants'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\nimport {\n  EnablementAreaComponent,\n  IEnablementAreaItem,\n} from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\n\nexport interface IFileInfo {\n  extension: string\n  name: string\n  label: string\n  parent: string\n  location: string\n  _key?: Nullable<string>\n  parents: IEnablementAreaItem[]\n}\n\nconst getSubstrigsPath = (input?: string): Nullable<string[]> => {\n  if (!input) return null\n\n  const parts = input.split('/')\n  const substrings = []\n  while (parts.length > 1) {\n    substrings.push(parts.join('/'))\n    parts.pop()\n  }\n\n  return substrings\n}\n\nexport const getFileInfo = (\n  { manifestPath, path }: { manifestPath?: Nullable<string>; path: string },\n  manifest?: Nullable<IEnablementAreaItem[]>,\n): IFileInfo => {\n  const defaultResult: IFileInfo = {\n    extension: '',\n    name: '',\n    parent: '',\n    location: '',\n    label: '',\n    parents: [],\n  }\n  try {\n    const url = IS_ABSOLUTE_PATH.test(path)\n      ? new URL(path)\n      : new URL(path, API_URL)\n    const pathNames = url.pathname.split('/')\n    const file = pathNames.pop() || ''\n    const markdownParent = manifest\n      ? getParentByManifest(manifest, manifestPath)\n      : null\n    const parents = manifest ? getAllParents(manifest, manifestPath) : []\n    const [fileName, extension] = file.split('.')\n\n    let markdownInfo: Record<string, any> = {}\n    if (manifestPath && markdownParent?.children) {\n      markdownInfo = get(\n        markdownParent.children,\n        (manifestPath.split('/')?.pop() as string) || '-1',\n        {},\n      )\n    }\n\n    return {\n      parents,\n      location: pathNames.join('/'),\n      name: fileName || '',\n      label: markdownInfo?.label || fileName,\n      extension: extension || '',\n      parent: markdownParent\n        ? markdownParent.label\n        : (pathNames.pop() || '').replace(/[-_]+/g, ' '),\n      _key: manifestPath?.split('/').pop() ?? null,\n    } as IFileInfo\n  } catch (e) {\n    return defaultResult\n  }\n}\n\nexport const getPagesInsideGroup = (\n  structure: IEnablementAreaItem[],\n  manifestPath: Nullable<string> = '',\n): IEnablementAreaItem[] => {\n  try {\n    if (!manifestPath) return []\n    const groupPath = getGroupPath(manifestPath)\n    const groupChildren: IEnablementAreaItem[] = getParentByManifest(\n      structure,\n      manifestPath,\n    )?.children\n\n    if (groupChildren) {\n      return groupChildren\n        .map((item, index) => ({\n          ...item,\n          _key: `${index}`,\n          _groupPath: groupPath,\n        }))\n        .filter((item) => item.type === EnablementAreaComponent.InternalLink)\n    }\n    return []\n  } catch (e) {\n    return []\n  }\n}\n\nexport const getTutorialSection = (manifestPath?: Nullable<string>) => {\n  const path = manifestPath?.replace(/^\\//, '')\n  if (path?.startsWith(EAManifestFirstKey.CUSTOM_TUTORIALS))\n    return 'Custom Tutorials'\n  if (path?.startsWith(EAManifestFirstKey.TUTORIALS)) return 'Tutorials'\n  if (path?.startsWith(EAManifestFirstKey.GUIDES)) return 'Guides'\n  return undefined\n}\n\nexport const getWBSourcePath = (path: string): string => {\n  if (path.includes(ApiEndpoints.TUTORIALS_PATH)) {\n    return ApiEndpoints.TUTORIALS_PATH\n  }\n  if (path.includes(ApiEndpoints.GUIDES_PATH)) {\n    return ApiEndpoints.GUIDES_PATH\n  }\n  if (path.includes(ApiEndpoints.CUSTOM_TUTORIALS_PATH)) {\n    return ApiEndpoints.CUSTOM_TUTORIALS_PATH\n  }\n  return ''\n}\n\nexport const getMarkdownPathByManifest = (\n  manifest: Nullable<IEnablementAreaItem[]>,\n  manifestPath: Nullable<string> = '',\n  pathPrefix: string = '',\n) => {\n  if (!manifestPath || !manifest) return pathPrefix\n  const path = removeManifestPrefix(manifestPath)\n  const pathToMarkDown = path.replaceAll('/', '.children.')\n  const markDownPath = get(manifest, pathToMarkDown)?.args?.path?.replaceAll(\n    '\\\\',\n    '/',\n  )\n\n  if (!markDownPath) return ''\n\n  let currentChildren = manifest\n  let folderPath = ''\n\n  forEach(path.split('/'), (index) => {\n    const structureObject = currentChildren[Number(index)]\n    if (!structureObject) return false\n\n    folderPath += currentChildren[Number(index)]._path || ''\n\n    if (!structureObject.children) return false\n\n    currentChildren = structureObject.children\n    return undefined\n  })\n\n  return (\n    pathPrefix +\n    folderPath +\n    (markDownPath.match(/^(\\/)/) ? markDownPath : '/'.concat(markDownPath))\n  )\n}\n\nexport const removeManifestPrefix = (path?: string): string =>\n  path\n    ?.replace(/^\\//, '')\n    ?.replace(/^(quick-guides|tutorials|custom-tutorials)/, '')\n    ?.replace(/^\\//, '') || ''\n\nexport const getGroupPath = (manifestPath: Nullable<string> = '') =>\n  manifestPath?.replace(/^\\//, '').split('/').slice(0, -1).join('/')\n\nexport const getParentByManifest = (\n  manifest: IEnablementAreaItem[],\n  manifestPath: Nullable<string> = '',\n) => {\n  if (!manifestPath) return null\n\n  const groupPath = getGroupPath(manifestPath)\n  const groupObjectPath =\n    removeManifestPrefix(groupPath).replaceAll('/', '.children.') || ''\n\n  const parent = get(manifest, groupObjectPath)\n\n  return parent ?? null\n}\n\nexport const getAllParents = (\n  manifest: IEnablementAreaItem[],\n  manifestPath: Nullable<string> = '',\n) =>\n  manifest && manifestPath\n    ? getSubstrigsPath(removeManifestPrefix(manifestPath))?.map((path) =>\n        getParentByManifest(manifest, path),\n      )\n    : []\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/utils/index.ts",
    "content": "export * from './getFileInfo'\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementArea/utils/tests/getFileInfo.spec.ts",
    "content": "import { ApiEndpoints, MOCK_TUTORIALS_ITEMS } from 'uiSrc/constants'\nimport {\n  getFileInfo,\n  getGroupPath,\n  getMarkdownPathByManifest,\n  getPagesInsideGroup,\n  getParentByManifest,\n  getTutorialSection,\n  getWBSourcePath,\n  removeManifestPrefix,\n} from '../getFileInfo'\n\nconst getFileInfoTests = [\n  {\n    input: [{ path: 'static/workbench/quick-guides/file-name.txt' }],\n    expected: {\n      name: 'file-name',\n      parent: 'quick guides',\n      extension: 'txt',\n      location: '/static/workbench/quick-guides',\n      label: 'file-name',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: 'parent_folder\\\\file_name.txt' }],\n    expected: {\n      name: 'file_name',\n      parent: 'parent folder',\n      extension: 'txt',\n      location: '/parent_folder',\n      label: 'file_name',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [\n      { path: 'https://domen.com/workbench/enablement-area/introduction.html' },\n    ],\n    expected: {\n      name: 'introduction',\n      parent: 'enablement area',\n      extension: 'html',\n      location: '/workbench/enablement-area',\n      label: 'introduction',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: 'https://domen.com/introduction.html' }],\n    expected: {\n      name: 'introduction',\n      parent: '',\n      extension: 'html',\n      location: '',\n      label: 'introduction',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: '/introduction.html' }],\n    expected: {\n      name: 'introduction',\n      parent: '',\n      extension: 'html',\n      location: '',\n      label: 'introduction',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: '//parent/markdown.md' }],\n    expected: {\n      name: '',\n      parent: '',\n      extension: '',\n      location: '',\n      label: '',\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: '/file.txt' }],\n    expected: {\n      name: 'file',\n      parent: '',\n      extension: 'txt',\n      location: '',\n      label: 'file',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: '' }],\n    expected: {\n      name: '',\n      parent: '',\n      extension: '',\n      location: '',\n      label: '',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [{ path: '/' }],\n    expected: {\n      name: '',\n      parent: '',\n      extension: '',\n      location: '',\n      label: '',\n      _key: null,\n      parents: [],\n    },\n  },\n  {\n    input: [\n      {\n        manifestPath: 'quick-guides/0/0',\n        path: '/static/workbench/quick-guides/document/learn-more.md',\n      },\n      MOCK_TUTORIALS_ITEMS,\n    ],\n    expected: {\n      name: 'learn-more',\n      parent: MOCK_TUTORIALS_ITEMS[0].label,\n      extension: 'md',\n      location: '/static/workbench/quick-guides/document',\n      label: MOCK_TUTORIALS_ITEMS?.[0]?.children?.[0].label,\n      _key: '0',\n      parents: [MOCK_TUTORIALS_ITEMS[0]],\n    },\n  },\n]\n\ndescribe('getFileInfo', () => {\n  test.each(getFileInfoTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getFileInfo(...input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getPagesInsideGroupTests = [\n  {\n    input: [MOCK_TUTORIALS_ITEMS, 'quick-guides/0/0'],\n    expected: (MOCK_TUTORIALS_ITEMS[0].children || []).map((item, index) => ({\n      ...item,\n      _groupPath: 'quick-guides/0',\n      _key: `${index}`,\n    })),\n  },\n  {\n    input: [\n      MOCK_TUTORIALS_ITEMS,\n      'https://domen.com/workbench/enablement-area/',\n    ],\n    expected: [],\n  },\n  {\n    input: [],\n    expected: [],\n  },\n]\n\ndescribe('getPagesInsideGroup', () => {\n  test.each(getPagesInsideGroupTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getPagesInsideGroup(...input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getTutorialSectionTests = [\n  { input: 'custom-tutorials/0/1', expected: 'Custom Tutorials' },\n  { input: '/custom-tutorials/0/1', expected: 'Custom Tutorials' },\n  { input: 'quick-guides/0/1', expected: 'Guides' },\n  { input: 'tutorials/0/1', expected: 'Tutorials' },\n  { input: 'my-tutorials/0/1', expected: undefined },\n]\n\ndescribe('getTutorialSection', () => {\n  test.each(getTutorialSectionTests)('%j', ({ input, expected }) => {\n    const result = getTutorialSection(input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getWBSourcePathTests = [\n  {\n    input: '/static/tutorials/folder/md.md',\n    expected: ApiEndpoints.TUTORIALS_PATH,\n  },\n  { input: '/static/guides/folder/md.md', expected: ApiEndpoints.GUIDES_PATH },\n  {\n    input: '/static/custom-tutorials/folder/md.md',\n    expected: ApiEndpoints.CUSTOM_TUTORIALS_PATH,\n  },\n  { input: '/static/my-tutorials/folder/md.md', expected: '' },\n]\n\ndescribe('getWBSourcePath', () => {\n  test.each(getWBSourcePathTests)('%j', ({ input, expected }) => {\n    const result = getWBSourcePath(input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getMarkdownPathByManifestTests = [\n  {\n    input: [MOCK_TUTORIALS_ITEMS, '/quick-guides/0/0', 'static/my-folder'],\n    expected: `static/my-folder${MOCK_TUTORIALS_ITEMS[0]?.children?.[0]?.args?.path}`,\n  },\n  {\n    input: [MOCK_TUTORIALS_ITEMS, '/quick-guides/0/0'],\n    expected: MOCK_TUTORIALS_ITEMS[0]?.children?.[0]?.args?.path,\n  },\n  {\n    input: [MOCK_TUTORIALS_ITEMS, '/my-guides/0/0', 'path/'],\n    expected: '',\n  },\n  {\n    input: [MOCK_TUTORIALS_ITEMS, '/quick-guides/0/1'],\n    expected: `/${MOCK_TUTORIALS_ITEMS[0]?.children?.[1]?.args?.path}`,\n  },\n  {\n    input: [MOCK_TUTORIALS_ITEMS, '/quick-guides/0/2'],\n    expected: '/quick-guides/working-with-hash.html',\n  },\n]\n\ndescribe('getMarkdownPathByManifest', () => {\n  test.each(getMarkdownPathByManifestTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getMarkdownPathByManifest(...input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst removeManifestPrefixTests = [\n  { input: '/quick-guides/0/0/1', expected: '0/0/1' },\n  { input: '/tutorials/0/0/1', expected: '0/0/1' },\n  { input: '/custom-tutorials/0/0/1', expected: '0/0/1' },\n  { input: '/my-tutorials/0/0/1', expected: 'my-tutorials/0/0/1' },\n]\n\ndescribe('removeManifestPrefix', () => {\n  test.each(removeManifestPrefixTests)('%j', ({ input, expected }) => {\n    const result = removeManifestPrefix(input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getGroupPathTests = [\n  { input: '/quick-guides/0/0/1', expected: 'quick-guides/0/0' },\n  {\n    input: '/tutorials/another-folder/0/0/1',\n    expected: 'tutorials/another-folder/0/0',\n  },\n]\n\ndescribe('getGroupPath', () => {\n  test.each(getGroupPathTests)('%j', ({ input, expected }) => {\n    const result = getGroupPath(input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getParentByManifestTests = [\n  { input: [MOCK_TUTORIALS_ITEMS, '0/0'], expected: MOCK_TUTORIALS_ITEMS[0] },\n  { input: [MOCK_TUTORIALS_ITEMS, '100/0'], expected: null },\n  { input: [MOCK_TUTORIALS_ITEMS, null], expected: null },\n]\n\ndescribe('getParentByManifest', () => {\n  test.each(getParentByManifestTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getParentByManifest(...input)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport EnablementAreaWrapper, { Props } from './EnablementAreaWrapper'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/workbench/wb-tutorials', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/workbench/wb-tutorials',\n  ).initialState\n  return {\n    ...jest.requireActual('uiSrc/slices/workbench/wb-tutorials'),\n    workbenchTutorialsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\ndescribe('EnablementAreaWrapper', () => {\n  it('should render', () => {\n    expect(\n      render(<EnablementAreaWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { IInternalPage } from 'uiSrc/pages/workbench/contexts/enablementAreaContext'\nimport { workbenchTutorialsSelector } from 'uiSrc/slices/workbench/wb-tutorials'\nimport { workbenchCustomTutorialsSelector } from 'uiSrc/slices/workbench/wb-custom-tutorials'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results'\nimport { getTutorialSection } from './EnablementArea/utils'\nimport EnablementArea from './EnablementArea'\n\nexport interface Props {}\n\nconst EnablementAreaWrapper = () => {\n  const { loading: loadingTutorials, items: tutorials } = useSelector(\n    workbenchTutorialsSelector,\n  )\n  const { loading: loadingCustomTutorials, items: customTutorials } =\n    useSelector(workbenchCustomTutorialsSelector)\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  const openScript = (\n    script: string,\n    params?: CodeButtonParams,\n    onFinish?: () => void,\n  ) => {\n    dispatch(\n      sendWbQueryAction(script, null, params, { afterAll: onFinish }, onFinish),\n    )\n  }\n\n  const onOpenInternalPage = ({ path, manifestPath }: IInternalPage) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n      eventData: {\n        path,\n        section: getTutorialSection(manifestPath),\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        source: 'Workbench',\n      },\n    })\n  }\n\n  return (\n    <EnablementArea\n      tutorials={tutorials}\n      customTutorials={customTutorials}\n      loading={loadingTutorials || loadingCustomTutorials}\n      openScript={openScript}\n      onOpenInternalPage={onOpenInternalPage}\n      isCodeBtnDisabled={false}\n    />\n  )\n}\n\nexport default React.memo(EnablementAreaWrapper)\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/index.ts",
    "content": "import EnablementAreaWrapper from './EnablementAreaWrapper'\n\nexport default EnablementAreaWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/enablement-area/styles.module.scss",
    "content": ".content {\n  margin-top: 70px;\n  height: calc(100% - 70px) !important;\n  background-color: var(--euiColorEmptyShade) !important;\n  border-left: none !important;\n  border-radius: 4px 0 0 4px;\n  box-shadow: -5px 0px 16px rgba(0, 0, 0, 0.16) !important;\n  min-width: 476px !important;\n}\n\n:global {\n  img {\n    max-width: 100%;\n  }\n}\n\n.onboardingAnchor {\n  position: absolute;\n  top: 80px;\n  right: 0;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/LiveTimeRecommendations.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  cleanup,\n  fireEvent,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { RECOMMENDATIONS_DATA_MOCK } from 'uiSrc/mocks/handlers/recommendations/recommendationsHandler'\nimport {\n  appContextDbConfig,\n  setRecommendationsShowHidden,\n} from 'uiSrc/slices/app/context'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\n\nimport LiveTimeRecommendations from './LiveTimeRecommendations'\n\nlet store: typeof mockedStore\nconst recommendationsContent = MOCK_RECOMMENDATIONS\n\nconst mockRecommendationsSelector = {\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  content: recommendationsContent,\n}\nconst mockAppContextDbConfigSelector = jest.requireActual(\n  'uiSrc/slices/app/context',\n)\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'instanceId',\n    connectionType: 'CLUSTER',\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\njest.mock('uiSrc/slices/recommendations/recommendations', () => ({\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  recommendationsSelector: jest.fn().mockReturnValue({\n    data: {\n      recommendations: [],\n      totalUnread: 0,\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextDbConfig: jest.fn().mockReturnValue({\n    showHiddenRecommendations: false,\n  }),\n}))\n\n/**\n * LiveTimeRecommendations tests\n *\n * @group component\n */\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('LiveTimeRecommendations', () => {\n  it('should render', () => {\n    expect(render(<LiveTimeRecommendations />)).toBeTruthy()\n  })\n\n  it('should render github icon', () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: { recommendations: [{ name: 'RTS' }] },\n    }))\n\n    render(<LiveTimeRecommendations />)\n    expect(screen.getByTestId('github-repo-btn')).toHaveAttribute(\n      'href',\n      EXTERNAL_LINKS.githubRepo,\n    )\n    expect(screen.getByTestId('github-repo-icon')).toBeInTheDocument()\n  })\n\n  it('should render show hidden checkbox when there are some hidden', () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [{ name: 'RTS', hide: true }, { name: 'setPassword' }],\n      },\n    }))\n\n    render(<LiveTimeRecommendations />)\n    expect(screen.getByTestId('checkbox-show-hidden')).toBeInTheDocument()\n  })\n\n  it('should not render show hidden checkbox when there are no any hidden', () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [\n          { name: 'RTS', hide: false },\n          { name: 'setPassword' },\n        ],\n      },\n    }))\n\n    render(<LiveTimeRecommendations />)\n    expect(screen.queryByTestId('checkbox-show-hidden')).not.toBeInTheDocument()\n  })\n\n  it('should properly push history on databaseAnalysis page', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [{ name: 'RTS' }],\n      },\n    }))\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<LiveTimeRecommendations />)\n\n    fireEvent.click(screen.getByTestId('footer-db-analysis-link'))\n    ;(async () => {\n      await waitForRiPopoverVisible()\n    })()\n\n    fireEvent.click(screen.getByTestId('approve-insights-db-analysis-btn'))\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.databaseAnalysis('instanceId'))\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n        total: 1,\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    sendEventTelemetry.mockRestore()\n  })\n\n  it('should send INSIGHTS_RECOMMENDATION_SHOW_HIDDEN telemetry event', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: RECOMMENDATIONS_DATA_MOCK,\n    }))\n\n    const { queryByTestId } = render(<LiveTimeRecommendations />)\n\n    fireEvent.click(queryByTestId('checkbox-show-hidden')!)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_SHOW_HIDDEN,\n      eventData: {\n        databaseId: 'instanceId',\n        list: RECOMMENDATIONS_DATA_MOCK.recommendations?.map(\n          ({ name }) => recommendationsContent[name].telemetryEvent || name,\n        ),\n        total: 2,\n        action: 'show',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    sendEventTelemetry.mockRestore()\n  })\n\n  it('should render only not hide recommendations is showHiddenRecommendations=false', async () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: RECOMMENDATIONS_DATA_MOCK,\n    }))\n    const { queryByTestId } = render(<LiveTimeRecommendations />)\n\n    expect(queryByTestId('redisSearch-recommendation')).toBeInTheDocument()\n    expect(queryByTestId('bigHashes-recommendation')).not.toBeInTheDocument()\n  })\n\n  it('should render all recommendations is showHiddenRecommendations=true', async () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: RECOMMENDATIONS_DATA_MOCK,\n    }))\n    ;(appContextDbConfig as jest.Mock).mockImplementation(() => ({\n      ...mockAppContextDbConfigSelector,\n      showHiddenRecommendations: true,\n    }))\n    render(<LiveTimeRecommendations />)\n\n    expect(screen.getByTestId('redisSearch-recommendation')).toBeInTheDocument()\n    expect(screen.getByTestId('bigHashes-recommendation')).toBeInTheDocument()\n  })\n\n  it('should call \"setRecommendationsShowHidden\" after click hide/unhide btn', async () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: RECOMMENDATIONS_DATA_MOCK,\n    }))\n    ;(appContextDbConfig as jest.Mock).mockImplementation(() => ({\n      ...mockAppContextDbConfigSelector,\n      showHiddenRecommendations: true,\n    }))\n    const { queryByTestId } = render(<LiveTimeRecommendations />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(queryByTestId('checkbox-show-hidden')!)\n\n    const expectedActions = [setRecommendationsShowHidden(false)]\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should render WelcomeScreen if no visible recommendations', () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        ...RECOMMENDATIONS_DATA_MOCK,\n        recommendations: RECOMMENDATIONS_DATA_MOCK.recommendations.map(\n          (rec) => ({ ...rec, hide: true }),\n        ),\n      },\n    }))\n    ;(appContextDbConfig as jest.Mock).mockImplementation(() => ({\n      ...mockAppContextDbConfigSelector,\n      showHiddenRecommendations: false,\n    }))\n\n    const { queryByTestId } = render(<LiveTimeRecommendations />)\n\n    expect(queryByTestId('redisSearch-recommendation')).not.toBeInTheDocument()\n    expect(queryByTestId('bigHashes-recommendation')).not.toBeInTheDocument()\n    expect(queryByTestId('no-recommendations-screen')).toBeInTheDocument()\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: { recommendations: [{ name: 'RTS' }] },\n    }))\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<LiveTimeRecommendations />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('github-repo-btn')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: { recommendations: [{ name: 'RTS' }] },\n    }))\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<LiveTimeRecommendations />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('github-repo-btn')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/LiveTimeRecommendations.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { remove } from 'lodash'\nimport styled from 'styled-components'\n\nimport { FeatureFlags, DEFAULT_DELIMITER, Pages } from 'uiSrc/constants'\nimport {\n  ANALYZE_CLUSTER_TOOLTIP_MESSAGE,\n  ANALYZE_TOOLTIP_MESSAGE,\n} from 'uiSrc/constants/recommendations'\nimport {\n  recommendationsSelector,\n  fetchRecommendationsAction,\n  readRecommendationsAction,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { IRecommendation } from 'uiSrc/slices/interfaces/recommendations'\nimport {\n  appContextDbConfig,\n  setRecommendationsShowHidden,\n} from 'uiSrc/slices/app/context'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { createNewAnalysis } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { comboBoxToArray } from 'uiSrc/utils'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport {\n  FeatureFlagComponent,\n  LoadingContent,\n  RiTooltip,\n} from 'uiSrc/components'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport Recommendation from './components/recommendation'\nimport WelcomeScreen from './components/welcome-screen'\nimport PopoverRunAnalyze from './components/popover-run-analyze'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nimport styles from './styles.module.scss'\n\nconst FooterLink = styled.button<{\n  onClick?: () => void\n  'data-testid'?: string\n  children?: React.ReactNode\n}>`\n  font:\n    normal normal 400 12px/14px Graphik,\n    sans-serif !important;\n  padding: 2px 0 0;\n  margin: 0;\n  text-decoration: underline !important;\n  :hover {\n    text-decoration: none !important;\n  }\n`\n\nconst LiveTimeRecommendations = () => {\n  const { provider, connectionType } = useSelector(connectedInstanceSelector)\n  const {\n    loading,\n    data: { recommendations },\n    content: recommendationsContent,\n  } = useSelector(recommendationsSelector)\n  const {\n    showHiddenRecommendations: isShowHidden,\n    treeViewDelimiter = [DEFAULT_DELIMITER],\n  } = useSelector(appContextDbConfig)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const [isShowApproveRun, setIsShowApproveRun] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const isShowHiddenDisplayed = recommendations.filter((r) => r.hide).length > 0\n\n  useEffect(() => {\n    if (!instanceId) return undefined\n\n    dispatch(fetchRecommendationsAction(instanceId))\n\n    return () => {\n      dispatch(readRecommendationsAction(instanceId))\n    }\n  }, [])\n\n  const handleClickDbAnalysisLink = () => {\n    dispatch(createNewAnalysis(instanceId, comboBoxToArray(treeViewDelimiter)))\n    history.push(Pages.databaseAnalysis(instanceId))\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        total: recommendations?.length,\n        provider,\n      },\n    })\n    setIsShowApproveRun(false)\n  }\n\n  const onChangeShowHidden = (value: boolean) => {\n    dispatch(setRecommendationsShowHidden(value))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_TIPS_SHOW_HIDDEN,\n      eventData: {\n        action: !value ? 'hide' : 'show',\n        ...getTelemetryData(recommendations),\n      },\n    })\n  }\n\n  const getTelemetryData = (recommendationsData: IRecommendation[]) => ({\n    databaseId: instanceId,\n    total: recommendationsData?.length,\n    list: recommendationsData?.map(\n      ({ name }) => recommendationsContent[name]?.telemetryEvent ?? name,\n    ),\n    provider,\n  })\n\n  const renderBody = () => {\n    const recommendationsList = [...recommendations]\n    if (!isShowHidden) {\n      remove(recommendationsList, { hide: true })\n    }\n\n    if (!instanceId || !recommendationsList?.length) {\n      return <WelcomeScreen />\n    }\n\n    return recommendationsList?.map(\n      ({ id, name, read, vote, hide, params }) => (\n        <Recommendation\n          id={id}\n          key={name}\n          name={name}\n          isRead={read}\n          vote={vote}\n          hide={hide}\n          tutorialId={recommendationsContent[name]?.tutorialId}\n          provider={provider}\n          params={params}\n          recommendationsContent={recommendationsContent}\n        />\n      ),\n    )\n  }\n\n  const renderHeader = () => (\n    <Row align=\"center\" justify=\"between\" className={styles.actions}>\n      <Row align=\"center\" gap=\"m\">\n        <ColorText variant=\"semiBold\">Our Tips</ColorText>\n        <RiTooltip\n          position=\"bottom\"\n          className={styles.tooltip}\n          anchorClassName={styles.tooltipAnchor}\n          content={\n            <Text size=\"s\">\n              Tips will help you improve your database.\n              <Spacer size=\"s\" />\n              New tips appear while you work with your database, including how\n              to improve performance and optimize memory usage.\n              <FeatureFlagComponent name={FeatureFlags.envDependent}>\n                <>\n                  <Spacer size=\"s\" />\n                  Eager for more tips? Run Database Analysis to get started.\n                </>\n              </FeatureFlagComponent>\n            </Text>\n          }\n        >\n          <RiIcon\n            className={styles.infoIcon}\n            type=\"InfoIcon\"\n            size=\"m\"\n            data-testid=\"recommendations-info-icon\"\n          />\n        </RiTooltip>\n        <FeatureFlagComponent name={FeatureFlags.envDependent}>\n          <Link\n            variant=\"inline\"\n            size=\"M\"\n            href={EXTERNAL_LINKS.githubRepo}\n            target=\"_blank\"\n            data-testid=\"github-repo-btn\"\n          >\n            <RiIcon\n              className={styles.githubIcon}\n              aria-label=\"redis insight github repository\"\n              type=\"GithubIcon\"\n              size=\"m\"\n              data-testid=\"github-repo-icon\"\n            />\n          </Link>\n        </FeatureFlagComponent>\n      </Row>\n\n      {isShowHiddenDisplayed && (\n        <Checkbox\n          id=\"showHidden\"\n          name=\"showHidden\"\n          label=\"Show hidden\"\n          checked={isShowHidden}\n          className={styles.hideCheckbox}\n          onChange={(e) => onChangeShowHidden(e.target.checked)}\n          data-testid=\"checkbox-show-hidden\"\n          aria-label=\"checkbox show hidden\"\n        />\n      )}\n    </Row>\n  )\n\n  return (\n    <div className={styles.content}>\n      <div className={styles.header}>\n        {instanceId && recommendations.length ? renderHeader() : null}\n      </div>\n      <div className={styles.body}>\n        {loading ? (\n          <LoadingContent className={styles.loading} lines={4} />\n        ) : (\n          renderBody()\n        )}\n      </div>\n      {instanceId && (\n        <FeatureFlagComponent name={FeatureFlags.envDependent}>\n          <div className={styles.footer}>\n            <RiIcon\n              className={styles.footerIcon}\n              size=\"m\"\n              type=\"MessageInfoIcon\"\n            />\n            <Text className={styles.text}>\n              {'Run '}\n              <PopoverRunAnalyze\n                isShowPopover={isShowApproveRun}\n                setIsShowPopover={setIsShowApproveRun}\n                onApproveClick={handleClickDbAnalysisLink}\n                popoverContent={\n                  connectionType === ConnectionType.Cluster\n                    ? ANALYZE_CLUSTER_TOOLTIP_MESSAGE\n                    : ANALYZE_TOOLTIP_MESSAGE\n                }\n              >\n                <FooterLink\n                  onClick={() => setIsShowApproveRun(true)}\n                  data-testid=\"footer-db-analysis-link\"\n                >\n                  Database Analysis\n                </FooterLink>\n              </PopoverRunAnalyze>\n              {' to get more tips'}\n            </Text>\n          </div>\n        </FeatureFlagComponent>\n      )}\n    </div>\n  )\n}\n\nexport default LiveTimeRecommendations\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/popover-run-analyze/PopoverRunAnalyze.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport PopoverRunAnalyze, { Props } from './PopoverRunAnalyze'\n\nconst mockedProps = mock<Props>()\n\ndescribe('PopoverRunAnalyze', () => {\n  it('should render', () => {\n    expect(\n      render(<PopoverRunAnalyze {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/popover-run-analyze/PopoverRunAnalyze.tsx",
    "content": "import React from 'react'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  popoverContent: string\n  isShowPopover: boolean\n  children: React.ReactElement\n  onApproveClick: () => void\n  setIsShowPopover: (value: boolean) => void\n}\n\nconst PopoverRunAnalyze = (props: Props) => {\n  const {\n    isShowPopover,\n    popoverContent,\n    setIsShowPopover,\n    onApproveClick,\n    children,\n  } = props\n\n  return (\n    <RiPopover\n      ownFocus\n      anchorPosition=\"upCenter\"\n      isOpen={isShowPopover}\n      closePopover={() => setIsShowPopover(false)}\n      panelPaddingSize=\"m\"\n      panelClassName={styles.panelPopover}\n      button={children}\n      onClick={(e) => e.stopPropagation()}\n    >\n      <div\n        className={styles.popover}\n        data-testid=\"insights-db-analysis-popover\"\n      >\n        <Text className={styles.popoverTitle} size=\"m\">\n          Run database analysis\n        </Text>\n        <Spacer size=\"s\" />\n        <Text className={styles.popoverContent}>{popoverContent}</Text>\n        <Spacer size=\"m\" />\n        <PrimaryButton\n          aria-label=\"Analyze\"\n          data-testid=\"approve-insights-db-analysis-btn\"\n          onClick={onApproveClick}\n          size=\"s\"\n          className={styles.popoverApproveBtn}\n        >\n          Analyze\n        </PrimaryButton>\n      </div>\n    </RiPopover>\n  )\n}\n\nexport default PopoverRunAnalyze\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/popover-run-analyze/index.ts",
    "content": "import PopoverRunAnalyze from './PopoverRunAnalyze'\n\nexport default PopoverRunAnalyze\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/popover-run-analyze/styles.module.scss",
    "content": ".popoverApproveBtn {\n  float: right;\n  padding: 4px 12px !important;\n  height: 24px !important;\n\n  :global(.euiButtonContent) {\n    padding: 0 !important;\n  }\n}\n\n.popoverContent {\n  font-size: 13px !important;\n  line-height: 16px !important;\n}\n\n.popoverTitle {\n  color: var(--htmlColor) !important;\n  font-size: 14px !important;\n}\n\n.panelPopover {\n  width: 432px;\n  padding: 16px 30px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.spec.tsx",
    "content": "import React from 'react'\nimport { mock, instance } from 'ts-mockito'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  fireEvent,\n  screen,\n  render,\n  mockedStore,\n  cleanup,\n  act,\n  initialStateDefault,\n  mockStore,\n  userEvent,\n  within,\n} from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport {\n  updateRecommendation,\n  updateRecommendationSuccess,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\nimport { findTutorialPath } from 'uiSrc/utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport Recommendation, { IProps } from './Recommendation'\n\nconst recommendationsContent = MOCK_RECOMMENDATIONS\nconst mockedProps = mock<IProps>()\n\nconst instanceMock = {\n  ...instance(mockedProps),\n  recommendationsContent,\n}\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  findTutorialPath: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst PROVIDER = 'REDIS_CLOUD'\n\ndescribe('Recommendation', () => {\n  const getAccordionToggleButton = (name: string) =>\n    within(screen.getByTestId(`${name}-accordion`)).getByLabelText(\n      /Expand Section|Collapse Section/,\n    )\n\n  // Helper to check if accordion body content is hidden (either not visible or not in DOM)\n  // The @redis-ui/components Section removes content from DOM when collapsed\n  const expectBodyHidden = (testId: string) => {\n    const element = screen.queryByTestId(testId)\n    if (element) {\n      expect(element).not.toBeVisible()\n      return\n    }\n    expect(element).not.toBeInTheDocument()\n  }\n\n  it('should render', () => {\n    expect(render(<Recommendation {...instanceMock} />)).toBeTruthy()\n  })\n\n  it('should render content if recommendation is not read', async () => {\n    render(\n      <Recommendation\n        {...instanceMock}\n        name=\"searchJSON\"\n        tutorialId=\"\"\n        isRead={false}\n      />,\n    )\n\n    // isRead={false} means defaultOpen={true}, so accordion is already open\n    expect(screen.getByTestId('recommendation-voting')).toBeInTheDocument()\n    expect(screen.getByTestId('searchJSON-to-tutorial-btn')).toBeInTheDocument()\n  })\n\n  it('should render RecommendationVoting', async () => {\n    // initial state open (isRead defaults to undefined, so !isRead = true)\n    render(<Recommendation {...instanceMock} name=\"searchJSON\" />)\n    // accordion button\n    const button = getAccordionToggleButton('searchJSON')\n    expect(screen.queryByTestId('recommendation-voting')).toBeInTheDocument()\n    expect(button).toBeInTheDocument()\n    // close accordion\n    await userEvent.click(button)\n\n    // Content is removed from DOM when accordion is closed\n    expectBodyHidden('recommendation-voting')\n    // open accordion\n    await userEvent.click(button)\n\n    expect(screen.queryByTestId('recommendation-voting')).toBeInTheDocument()\n  })\n\n  it('should properly push history on workbench page', async () => {\n    // will be improved\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    const { getByTestId } = render(\n      <Recommendation\n        {...instanceMock}\n        isRead\n        name=\"searchJSON\"\n        tutorialId=\"123\"\n        provider={PROVIDER}\n      />,\n    )\n\n    await userEvent.click(getAccordionToggleButton('searchJSON'))\n    await userEvent.click(getByTestId('searchJSON-to-tutorial-btn'))\n\n    expect(pushMock).toHaveBeenCalledWith({ search: 'path=tutorials/path' })\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_TUTORIAL_CLICKED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        name: 'searchJSON',\n        provider: PROVIDER,\n      },\n    })\n    sendEventTelemetry.mockRestore()\n  })\n\n  it('should properly call openNewWindowDatabase and open a new window on workbench page to specific guide', async () => {\n    // will be improved\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    render(\n      <Recommendation\n        {...instanceMock}\n        isRead\n        name=\"searchJSON\"\n        tutorialId=\"123\"\n        provider={PROVIDER}\n      />,\n    )\n\n    await userEvent.click(getAccordionToggleButton('searchJSON'))\n    fireEvent.click(screen.getByTestId('searchJSON-to-tutorial-btn'))\n\n    expect(pushMock).toHaveBeenCalledWith({\n      search: 'path=tutorials/path',\n    })\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_TUTORIAL_CLICKED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        name: 'searchJSON',\n        provider: PROVIDER,\n      },\n    })\n    sendEventTelemetry.mockRestore()\n    pushMock.mockRestore()\n  })\n\n  it('should properly push history on workbench page to specific tutorial', async () => {\n    // will be improved\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => 'path')\n\n    render(\n      <Recommendation\n        {...instanceMock}\n        isRead\n        name=\"searchJSON\"\n        tutorialId=\"123\"\n        provider={PROVIDER}\n      />,\n    )\n\n    await userEvent.click(getAccordionToggleButton('searchJSON'))\n    fireEvent.click(screen.getByTestId('searchJSON-to-tutorial-btn'))\n\n    expect(pushMock).toHaveBeenCalledWith({\n      search: 'path=tutorials/path',\n    })\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_TUTORIAL_CLICKED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        name: 'searchJSON',\n        provider: PROVIDER,\n      },\n    })\n    sendEventTelemetry.mockRestore()\n    pushMock.mockRestore()\n  })\n\n  it('should render hide/unhide button', () => {\n    const name = 'searchJSON'\n    render(<Recommendation {...instanceMock} name={name} />)\n\n    expect(screen.getByTestId('toggle-hide-searchJSON-btn')).toBeInTheDocument()\n  })\n\n  it('click on hide/unhide button should call updateLiveRecommendation', async () => {\n    const idMock = 'id'\n    const nameMock = 'searchJSON'\n    const { queryByTestId } = render(\n      <Recommendation {...instanceMock} id={idMock} name={nameMock} />,\n    )\n\n    await act(() => {\n      fireEvent.click(\n        queryByTestId('toggle-hide-searchJSON-btn') as HTMLButtonElement,\n      )\n    })\n\n    const expectedActions = [\n      updateRecommendation(),\n      updateRecommendationSuccess({}),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n    expect(screen.getByTestId('toggle-hide-searchJSON-btn')).toBeInTheDocument()\n  })\n\n  it('should not render \"Tutorial\" btn if tutorial is Undefined', () => {\n    const name = 'searchJSON'\n    const { queryByTestId } = render(\n      <Recommendation {...instanceMock} name={name} tutorialId={undefined} />,\n    )\n\n    expect(queryByTestId(`${name}-to-tutorial-btn`)).not.toBeInTheDocument()\n  })\n\n  it('should render \"Tutorial\" if tutorialId=\"path\"', () => {\n    const name = 'searchJSON'\n    const { queryByTestId } = render(\n      <Recommendation {...instanceMock} name={name} tutorialId=\"path\" />,\n    )\n\n    expect(queryByTestId(`${name}-to-tutorial-btn`)).toHaveTextContent(\n      'Tutorial',\n    )\n  })\n\n  it('should render \"Workbench\" btn if tutorialId=\"\"', () => {\n    const name = 'searchJSON'\n    const { queryByTestId } = render(\n      <Recommendation {...instanceMock} name={name} tutorialId=\"\" />,\n    )\n\n    expect(queryByTestId(`${name}-to-tutorial-btn`)).toHaveTextContent(\n      'Workbench',\n    )\n  })\n\n  it('should render Snooze button', () => {\n    const name = 'searchJSON'\n    render(<Recommendation {...instanceMock} name={name} />)\n\n    expect(screen.getByTestId(`${name}-delete-btn`)).toBeInTheDocument()\n  })\n\n  it('click on Snooze button should call deleteLiveRecommendations', async () => {\n    const idMock = 'id'\n    const nameMock = 'searchJSON'\n    const { queryByTestId } = render(\n      <Recommendation {...instanceMock} id={idMock} name={nameMock} />,\n    )\n\n    fireEvent.click(\n      queryByTestId(`${nameMock}-delete-btn`) as HTMLButtonElement,\n    )\n\n    const expectedActions = [updateRecommendation()]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render vote section if feature flag is on', () => {\n    const name = 'searchJSON'\n    render(<Recommendation {...instanceMock} name={name} />)\n\n    expect(screen.queryByText('Is this useful?')).toBeInTheDocument()\n  })\n\n  it('should not render vote section if feature flag is off', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    store = mockStore(initialStoreState)\n\n    const name = 'searchJSON'\n    render(<Recommendation {...instanceMock} name={name} />, { store })\n\n    expect(screen.queryByText('Is this useful?')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Card } from 'uiSrc/components/base/layout'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const RecommendationContent = styled(Card)`\n  padding: 0;\n  border: none;\n  box-shadow: none;\n`\n\nexport const Title = styled(Text)`\n  margin-top: ${({ theme }) => theme.core?.space.space100};\n  margin-bottom: ${({ theme }) => theme.core?.space.space100};\n  font-weight: bold;\n  color: ${({ theme }) => theme.semantic.color.text.danger500};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { isUndefined } from 'lodash'\n\nimport { findTutorialPath, Maybe, Nullable } from 'uiSrc/utils'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport {\n  FeatureFlagComponent,\n  RecommendationBody,\n  RecommendationCopyComponent,\n  RecommendationVoting,\n  RiTooltip,\n} from 'uiSrc/components'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  deleteLiveRecommendations,\n  updateLiveRecommendation,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport {\n  IRecommendationParams,\n  IRecommendationsStatic,\n} from 'uiSrc/slices/interfaces/recommendations'\n\nimport {\n  HideIcon,\n  RediStackMinIcon,\n  ShowIcon,\n  SnoozeIcon,\n  StarsIcon,\n} from 'uiSrc/components/base/icons'\n\nimport { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport {\n  IconButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiAccordion } from 'uiSrc/components/base/display/accordion/RiAccordion'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nimport styles from './styles.module.scss'\nimport { RecommendationContent, Title } from './Recommendation.styles'\n\nconst TITLE_TRUNCATE_LENGTH = 30 // Note: Temporary dirty fix for RI-7474, before the full redesign of this component\n\nexport interface IProps {\n  id: string\n  name: string\n  isRead: boolean\n  vote: Nullable<Vote>\n  hide: boolean\n  tutorialId?: string\n  provider?: string\n  params: IRecommendationParams\n  recommendationsContent: IRecommendationsStatic\n}\n\nconst RecommendationTitle = ({\n  redisStack,\n  title,\n  id,\n}: {\n  redisStack: Maybe<boolean>\n  title?: string\n  id: string\n}) => {\n  return (\n    <Row\n      align=\"center\"\n      justify=\"start\"\n      gap=\"m\"\n      style={{\n        maxWidth: '60%',\n        textAlign: 'left',\n        overflow: 'hidden',\n      }}\n    >\n      {redisStack && (\n        <FlexItem>\n          <Link\n            target=\"_blank\"\n            href={EXTERNAL_LINKS.redisStack}\n            className={styles.redisStackLink}\n            data-testid={`${id}-redis-stack-link`}\n          >\n            <RiTooltip\n              content=\"Redis Stack\"\n              position=\"top\"\n              anchorClassName=\"flex-row\"\n            >\n              <RediStackMinIcon\n                className={styles.redisStackIcon}\n                data-testid={`${id}-redis-stack-icon`}\n              />\n            </RiTooltip>\n          </Link>\n        </FlexItem>\n      )}\n      <div className=\"truncateText\">\n        <span title={title}>{title}</span>\n      </div>\n    </Row>\n  )\n}\n\nconst Recommendation = ({\n  id,\n  name,\n  isRead,\n  vote,\n  tutorialId,\n  hide,\n  provider,\n  params,\n  recommendationsContent,\n}: IProps) => {\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const {\n    redisStack,\n    title,\n    liveTitle,\n    content = [],\n  } = recommendationsContent[name] || {}\n\n  const handleRedirect = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_TIPS_TUTORIAL_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        name: recommendationsContent[name].telemetryEvent || name,\n        provider,\n      },\n    })\n\n    if (!tutorialId) {\n      history.push(Pages.workbench(instanceId))\n      return\n    }\n\n    const tutorialPath = findTutorialPath({ id: tutorialId })\n    dispatch(openTutorialByPath(tutorialPath ?? '', history))\n  }\n\n  const toggleHide = (event: React.MouseEvent) => {\n    event.stopPropagation()\n    event.preventDefault()\n    dispatch(\n      updateLiveRecommendation(id, { hide: !hide }, ({ hide, name }) =>\n        sendEventTelemetry({\n          event: TelemetryEvent.INSIGHTS_TIPS_HIDE,\n          eventData: {\n            databaseId: instanceId,\n            action: hide ? 'hide' : 'show',\n            name: recommendationsContent[name]?.telemetryEvent ?? name,\n            provider,\n          },\n        }),\n      ),\n    )\n  }\n\n  const handleDelete = (event: React.MouseEvent) => {\n    event.stopPropagation()\n    event.preventDefault()\n    dispatch(deleteLiveRecommendations([{ id, isRead }], onSuccessActionDelete))\n  }\n\n  const onSuccessActionDelete = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_TIPS_SNOOZED,\n      eventData: {\n        databaseId: instanceId,\n        name: recommendationsContent[name]?.telemetryEvent ?? name,\n        provider,\n      },\n    })\n  }\n\n  const onRecommendationLinkClick = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_TIPS_LINK_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        name: recommendationsContent[name]?.telemetryEvent ?? name,\n        provider,\n      },\n    })\n  }\n\n  const recommendationContent = () => (\n    <Col>\n      {!isUndefined(tutorialId) && (\n        <Col align=\"start\">\n          <Spacer size=\"s\" />\n          <SecondaryButton\n            filled\n            icon={StarsIcon}\n            iconSide=\"right\"\n            className={styles.btn}\n            onClick={handleRedirect}\n            data-testid={`${name}-to-tutorial-btn`}\n          >\n            {tutorialId ? 'Start Tutorial' : 'Workbench'}\n          </SecondaryButton>\n          <Spacer size=\"m\" />\n        </Col>\n      )}\n      <RecommendationBody\n        elements={content}\n        params={params}\n        onLinkClick={onRecommendationLinkClick}\n        telemetryName={recommendationsContent[name]?.telemetryEvent ?? name}\n        insights\n      />\n\n      {!!params?.keys?.length && (\n        <RecommendationCopyComponent\n          keyName={params.keys[0]}\n          provider={provider}\n          telemetryEvent={recommendationsContent[name]?.telemetryEvent ?? name}\n          live\n        />\n      )}\n      <FeatureFlagComponent name={FeatureFlags.envDependent}>\n        <div className={styles.actions}>\n          <RecommendationVoting\n            live\n            id={id}\n            vote={vote}\n            name={name}\n            containerClass={styles.votingContainer}\n          />\n        </div>\n      </FeatureFlagComponent>\n    </Col>\n  )\n\n  const renderButtonContent = (\n    <Row className={styles.fullWidth} align=\"center\" gap=\"s\" justify=\"between\">\n      <FlexItem>\n        <RiTooltip\n          title=\"Snooze tip\"\n          content=\"This tip will be removed from the list and displayed again when relevant.\"\n          position=\"top\"\n          anchorClassName=\"flex-row\"\n        >\n          <IconButton\n            icon={SnoozeIcon}\n            className={styles.snoozeBtn}\n            onClick={handleDelete}\n            aria-label=\"snooze tip\"\n            data-testid={`${name}-delete-btn`}\n          />\n        </RiTooltip>\n      </FlexItem>\n      <FlexItem>\n        <RiTooltip\n          title={`${hide ? 'Show' : 'Hide'} tip`}\n          content={`${\n            hide\n              ? 'This tip will be shown in the list.'\n              : 'This tip will be removed from the list and not displayed again.'\n          }`}\n          position=\"top\"\n          anchorClassName=\"flex-row\"\n        >\n          <IconButton\n            icon={hide ? HideIcon : ShowIcon}\n            className={styles.hideBtn}\n            onClick={toggleHide}\n            aria-label=\"hide/unhide tip\"\n            data-testid={`toggle-hide-${name}-btn`}\n          />\n        </RiTooltip>\n      </FlexItem>\n    </Row>\n  )\n\n  if (!(name in recommendationsContent)) {\n    return null\n  }\n\n  return (\n    <div\n      data-testid={`${name}-recommendation`}\n      style={{ marginBottom: '1rem' }}\n    >\n      <RiAccordion\n        id={name}\n        defaultOpen={!isRead}\n        actions={renderButtonContent}\n        label={\n          <RecommendationTitle\n            redisStack={redisStack}\n            title={title || liveTitle}\n            id={name}\n          />\n        }\n        data-testid={`${name}-accordion`}\n        aria-label={`${name}-accordion`}\n      >\n        <Col>\n          {/* Note: Temporary dirty fix for RI-7474, before the full redesign of this component */}\n          {title?.length > TITLE_TRUNCATE_LENGTH && <Title>{title}</Title>}\n          <RecommendationContent\n            className={styles.accordionContent}\n            color=\"subdued\"\n          >\n            {recommendationContent()}\n          </RecommendationContent>\n        </Col>\n      </RiAccordion>\n    </div>\n  )\n}\n\nexport default Recommendation\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/index.ts",
    "content": "import Recommendation from './Recommendation'\n\nexport default Recommendation\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/styles.module.scss",
    "content": ".recommendationAccordion {\n  margin-bottom: 10px;\n  border-radius: 8px;\n  overflow: hidden;\n  border: 1px solid var(--recommendationLiveBorderColor);\n\n  &.read {\n    border: 1px solid var(--recommendationBgColor);\n  }\n\n  .redisStackLink {\n    margin-right: 12px;\n  }\n\n  :global {\n    .euiAccordion__childWrapper {\n      background-color: var(--recommendationBgColor) !important;\n    }\n    .euiAccordion__button {\n      padding: 6px 18px;\n      border-bottom: 1px solid transparent;\n      color: var(--euiTextSubduedColor) !important;\n      background-color: var(--recommendationBgColor) !important;\n      font-size: 14px;\n      font-weight: 400;\n\n      transition: border-bottom-color ease 0.3s;\n\n      .euiIEFlexWrapFix {\n        display: inline-block;\n        width: calc(100% - 28px);\n      }\n\n      .truncateText {\n        display: block;\n      }\n    }\n\n    .euiAccordion.euiAccordion-isOpen {\n      .euiAccordion__button {\n        padding: 14px 18px;\n        color: var(--htmlColor) !important;\n        font-weight: 500;\n      }\n\n      .euiIEFlexWrapFix .truncateText {\n        display: flex;\n        overflow: visible;\n        white-space: normal !important;\n      }\n    }\n\n    .euiPanel {\n      background: var(--recommendationBgColor) !important;\n      padding: 0 18px 18px !important;\n    }\n\n    .euiAccordion__iconWrapper {\n      display: flex;\n      justify-content: center;\n      align-items: center;\n    }\n\n    .euiAccordion__buttonReverse .euiAccordion__icon {\n      width: 12px;\n      height: 12px;\n      vertical-align: middle;\n    }\n  }\n}\n\n.wrapper,\n.recommendationAccordion {\n  .text {\n    font: normal normal normal 14px/19px Graphik !important;\n  }\n\n  .span {\n    display: inline;\n  }\n\n  .code {\n    background-color: var(--euiColorLightestShade);\n    border-radius: 4px;\n    padding: 3px 6px;\n\n    code {\n      font-size: inherit !important;\n    }\n  }\n}\n\n.actions {\n  display: flex;\n  margin-top: 15px;\n  justify-content: space-around;\n  align-items: center;\n  height: 48px;\n}\n\n.fullWidth {\n  width: 100%;\n}\n\n.votingContainer {\n  border-radius: 8px;\n  background-color: var(--recommendationsBgColor) !important;\n  padding: 4px 0 4px 10px;\n}\n\n.keyNameContainer {\n  background-color: var(--recommendationBgColor);\n}\n\n.snoozeBtn {\n  svg {\n    fill: var(--recommendationBgColor) !important;\n    g, circle, path {\n      stroke: currentColor !important;\n    }\n  }\n}\n\n.accordionContent {\n  .btn {\n    box-shadow: none !important;\n    display: block;\n    margin-top: 1px;\n    height: 32px !important;\n    min-width: 60px !important;\n    border-color: var(--highlightDotColor) !important;\n    background-color: var(--highlightDotColor) !important;\n    margin-bottom: 12px;\n\n    svg path {\n      fill: var(--euiColorEmptyShade);\n    }\n\n    :global(.euiButton__content) {\n      padding: 0 12px;\n    }\n\n    :global(.euiButton__text) {\n      color: var(--euiColorEmptyShade);\n      font:\n        normal normal 500 14px/17px Graphik,\n        sans-serif !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/WelcomeScreen.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep, set } from 'lodash'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  fireEvent,\n  mockedStore,\n  screen,\n  cleanup,\n  render,\n  waitForRiPopoverVisible,\n  initialStateDefault,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { getDBAnalysis } from 'uiSrc/slices/analytics/dbAnalysis'\n\nimport WelcomeScreen from './WelcomeScreen'\n\nlet store: typeof mockedStore\n\nconst mockRecommendationsSelector = jest.requireActual(\n  'uiSrc/slices/recommendations/recommendations',\n)\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'instanceId',\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\njest.mock('uiSrc/slices/recommendations/recommendations', () => ({\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  recommendationsSelector: jest.fn().mockReturnValue({\n    data: {\n      recommendations: [],\n      totalUnread: 0,\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('WelcomeScreen', () => {\n  it('should render', () => {\n    expect(render(<WelcomeScreen />)).toBeTruthy()\n  })\n\n  it('should properly push history on workbench page', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<WelcomeScreen />)\n\n    fireEvent.click(screen.getByTestId('insights-db-analysis-link'))\n\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('approve-insights-db-analysis-btn'))\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.databaseAnalysis('instanceId'))\n  })\n\n  it('should call db analysis after click link btn', () => {\n    render(<WelcomeScreen />)\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('insights-db-analysis-link'))\n    ;(async () => {\n      await waitForRiPopoverVisible()\n    })()\n\n    fireEvent.click(screen.getByTestId('approve-insights-db-analysis-btn'))\n\n    const expectedActions = [getDBAnalysis()]\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should call telemetry INSIGHTS_RECOMMENDATION_DATABASE_ANALYSIS_CLICKED after click link btn', async () => {\n    ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockRecommendationsSelector,\n      data: {\n        recommendations: [{ name: 'RTS' }],\n      },\n    }))\n\n    render(<WelcomeScreen />)\n\n    fireEvent.click(screen.getByTestId('insights-db-analysis-link'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('approve-insights-db-analysis-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n        total: 1,\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should not render part of content if no instanceId', () => {\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: undefined })\n    render(<WelcomeScreen />)\n\n    expect(\n      screen.queryByTestId('insights-db-analysis-link'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.getByTestId('no-recommendations-analyse-text'),\n    ).toHaveTextContent('Eager for tips? Connect to a database to get started.')\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n    reactRouterDom.useParams = jest.fn().mockReturnValue({ instanceId: 1 })\n\n    render(<WelcomeScreen />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByTestId('insights-db-analysis-link'),\n    ).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    reactRouterDom.useParams = jest.fn().mockReturnValue({ instanceId: 1 })\n\n    render(<WelcomeScreen />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(\n      screen.queryByTestId('insights-db-analysis-link'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/WelcomeScreen.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport cx from 'classnames'\n\nimport { DEFAULT_DELIMITER, FeatureFlags, Pages } from 'uiSrc/constants'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport WelcomeIcon from 'uiSrc/assets/img/icons/welcome.svg?react'\nimport { appContextDbConfig } from 'uiSrc/slices/app/context'\nimport { createNewAnalysis } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { comboBoxToArray } from 'uiSrc/utils'\nimport {\n  ANALYZE_CLUSTER_TOOLTIP_MESSAGE,\n  ANALYZE_TOOLTIP_MESSAGE,\n} from 'uiSrc/constants/recommendations'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport PopoverRunAnalyze from '../popover-run-analyze'\n\nimport styles from './styles.module.scss'\n\nconst NoRecommendationsScreen = () => {\n  const { provider, connectionType } = useSelector(connectedInstanceSelector)\n  const {\n    data: { recommendations },\n  } = useSelector(recommendationsSelector)\n  const { treeViewDelimiter = [DEFAULT_DELIMITER] } =\n    useSelector(appContextDbConfig)\n\n  const [isShowInfo, setIsShowInfo] = useState(false)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const handleClickDbAnalysisLink = () => {\n    dispatch(createNewAnalysis(instanceId, comboBoxToArray(treeViewDelimiter)))\n    history.push(Pages.databaseAnalysis(instanceId))\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        total: recommendations?.length,\n        provider,\n      },\n    })\n    setIsShowInfo(false)\n  }\n\n  return (\n    <div className={styles.container} data-testid=\"no-recommendations-screen\">\n      <Text className={styles.bigText}>Welcome to</Text>\n      <Text className={styles.hugeText}>Tips!</Text>\n      <Text className={styles.mediumText}>\n        Where we help improve your database.\n      </Text>\n      <Text className={cx(styles.text, styles.bigMargin)}>\n        New tips appear while you work with your database, including how to\n        improve performance and optimize memory usage.\n      </Text>\n      <WelcomeIcon className={styles.icon} />\n      {instanceId ? (\n        <FeatureFlagComponent name={FeatureFlags.envDependent}>\n          <Text\n            className={styles.text}\n            data-testid=\"no-recommendations-analyse-text\"\n          >\n            Eager for more tips? Run Database Analysis to get started.\n          </Text>\n\n          <PopoverRunAnalyze\n            isShowPopover={isShowInfo}\n            setIsShowPopover={setIsShowInfo}\n            onApproveClick={handleClickDbAnalysisLink}\n            popoverContent={\n              connectionType === ConnectionType.Cluster\n                ? ANALYZE_CLUSTER_TOOLTIP_MESSAGE\n                : ANALYZE_TOOLTIP_MESSAGE\n            }\n          >\n            <PrimaryButton\n              size=\"s\"\n              onClick={() => setIsShowInfo(true)}\n              data-testid=\"insights-db-analysis-link\"\n            >\n              Analyze Database\n            </PrimaryButton>\n          </PopoverRunAnalyze>\n        </FeatureFlagComponent>\n      ) : (\n        <Text\n          className={styles.text}\n          data-testid=\"no-recommendations-analyse-text\"\n        >\n          Eager for tips? Connect to a database to get started.\n        </Text>\n      )}\n    </div>\n  )\n}\n\nexport default NoRecommendationsScreen\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/index.ts",
    "content": "import WelcomeScreen from './WelcomeScreen'\n\nexport default WelcomeScreen\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/styles.module.scss",
    "content": ".container {\n  margin-top: 66px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  .bigMargin {\n    margin-bottom: 40px;\n  }\n}\n\n.bigText {\n  font: normal normal normal 22px/30px Graphik, sans-serif !important;\n  text-align: center;\n  color: var(--recommendationColor) !important;\n}\n\n.hugeText {\n  font: normal normal 500 28px/38px Graphik, sans-serif !important;\n  text-align: center;\n  color: var(--recommendationColor) !important;\n  margin-bottom: 9px;\n}\n\n.mediumText {\n  font: normal normal normal 16px/40px Graphik, sans-serif !important;\n  color: var(--recommendationColor) !important;\n  margin-bottom: 10px;\n}\n\n.icon {\n  margin-bottom: 40px;\n}\n\n.text {\n  font: normal normal normal 12px/20px Graphik, sans-serif !important;\n  margin-bottom: 12px;\n  max-width: 351px;\n  text-align: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/index.ts",
    "content": "import LiveTimeRecommendations from './LiveTimeRecommendations'\n\nexport default LiveTimeRecommendations\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/styles.module.scss",
    "content": "$animation-duration: 300ms;\n\n.content {\n  height: 100%;\n  width: 100%;\n\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.loading {\n  display: block;\n  width: 100%;\n\n  :global {\n    .euiLoadingContent__singleLine {\n      height: 46px;\n      margin-bottom: 10px;\n    }\n\n    .euiLoadingContent__singleLine:last-child:not(:only-child) {\n      width: 100%;\n    }\n  }\n}\n\n.header {\n  padding: 0 15px 0 !important;\n\n  .headerTop {\n    display: flex;\n    align-items: center;\n  }\n\n  .title {\n    font:\n      normal normal 500 18px/22px Graphik,\n      sans-serif;\n  }\n\n  .actions {\n    min-height: 36px;\n    margin: 0 -15px;\n  }\n}\n\n.body {\n  padding: 8px 15px 24px !important;\n  flex-grow: 1;\n\n  @include eui.scrollBar;\n  overflow-y: auto;\n}\n\n.footer {\n  min-height: 26px;\n  background: var(--tableRowSelectedColor) !important;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n\n  .footerIcon {\n    width: 32px;\n    height: 22px;\n    margin-right: 16px;\n  }\n\n  .text,\n  .link {\n    font:\n      normal normal 400 12px/14px Graphik,\n      sans-serif !important;\n    color: var(--recommendationColor) !important;\n  }\n\n  .link {\n    vertical-align: super;\n    padding-top: 2px;\n  }\n}\n\n.actions {\n  background: var(--browserTableRowEven);\n  padding: 8px 16px;\n\n  .githubIcon {\n    color: var(--htmlColor);\n  }\n\n  .infoIcon {\n    fill: var(--htmlColor);\n    cursor: pointer;\n  }\n\n  .tooltipAnchor {\n    svg {\n      vertical-align: middle;\n    }\n  }\n}\n\n.tooltip {\n  max-width: 314px !important;\n}\n\n.hideCheckbox {\n  > :global(.euiCheckbox__square + .euiCheckbox__input:not(:checked)) {\n    background: var(--recommendationBgColor) !important;\n  }\n}\n\n.hideBtn {\n  margin-bottom: 3px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/side-panels/styles.module.scss",
    "content": ".body {\n  height: calc(100% - 60px);\n}\n\n.tabs {\n  flex-shrink: 0 !important;\n  overflow: initial !important;\n  flex-grow: 0;\n\n  & > div {\n    padding: 0 12px;\n  }\n}\n\n.assistantHeader {\n  display: flex;\n  align-items: center;\n  flex-grow: 1;\n}\n\n.onboardingAnchorWrapper {\n  display: flex;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/table-column-search/TableColumnSearch.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport TableColumnSearch, { Props } from './TableColumnSearch'\n\nconst mockedProps = mock<Props>()\n\ndescribe('TableColumnSearch', () => {\n  it('should render', () => {\n    expect(\n      render(<TableColumnSearch {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should change search value', () => {\n    render(<TableColumnSearch {...instance(mockedProps)} />)\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, {\n      target: { value: '*1*' },\n    })\n    expect(searchInput).toHaveValue('*1*')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/table-column-search/TableColumnSearch.tsx",
    "content": "import React, { useState } from 'react'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\nimport { Maybe } from 'uiSrc/utils'\nimport { SearchInput } from 'uiSrc/components/base/inputs'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  appliedValue: string\n  fieldName: string\n  onApply?: (value: string) => void\n  searchValidation?: Maybe<(value: string) => string>\n}\n\nconst TableColumnSearch = (props: Props) => {\n  const {\n    fieldName,\n    appliedValue,\n    onApply = () => {},\n    searchValidation,\n  } = props\n  const [value, setValue] = useState<string>('')\n\n  const handleChangeValue = (initValue: string) => {\n    const value = searchValidation ? searchValidation(initValue) : initValue\n    setValue(value)\n  }\n\n  const handleApply = (_value: string): void => {\n    if (appliedValue !== _value) {\n      onApply(_value)\n    }\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === keys.ENTER) {\n      handleApply(value)\n    }\n  }\n\n  return (\n    <div className={styles.search}>\n      <SearchInput\n        onKeyDown={onKeyDown}\n        name={fieldName}\n        placeholder=\"Search\"\n        value={value || ''}\n        onChange={handleChangeValue}\n        data-testid=\"search\"\n      />\n    </div>\n  )\n}\n\nexport default TableColumnSearch\n"
  },
  {
    "path": "redisinsight/ui/src/components/table-column-search/styles.module.scss",
    "content": ".search {\n  position: absolute;\n  right: 0;\n  padding-right: 2px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/table-column-search-trigger/TableColumnSearchTrigger.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport TableColumnSearchTrigger, { Props } from './TableColumnSearchTrigger'\n\nconst mockedProps = mock<Props>()\n\ndescribe('TableColumnSearchTrigger', () => {\n  it('should render', () => {\n    expect(\n      render(<TableColumnSearchTrigger {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should change search value', () => {\n    render(<TableColumnSearchTrigger {...instance(mockedProps)} isOpen />)\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, {\n      target: { value: '*1*' },\n    })\n    expect(searchInput).toHaveValue('*1*')\n  })\n\n  it('should call \"handleOpenState\" on search blur if input is empty', () => {\n    const handleOpenState = jest.fn()\n\n    render(\n      <TableColumnSearchTrigger\n        {...instance(mockedProps)}\n        isOpen\n        handleOpenState={handleOpenState}\n      />,\n    )\n\n    const searchInput = screen.getByTestId('search')\n    expect(searchInput).toBeInTheDocument()\n    expect(searchInput).toHaveValue('')\n\n    fireEvent.blur(searchInput)\n    expect(handleOpenState).toHaveBeenCalledWith(false)\n  })\n\n  it('should not call \"handleOpenState\" on search blur if input has value', () => {\n    const handleOpenState = jest.fn()\n\n    render(\n      <TableColumnSearchTrigger\n        {...instance(mockedProps)}\n        isOpen\n        handleOpenState={handleOpenState}\n      />,\n    )\n\n    const searchInput = screen.getByTestId('search')\n    expect(searchInput).toBeInTheDocument()\n    expect(searchInput).toHaveValue('')\n\n    fireEvent.change(searchInput, {\n      target: { value: '*1*' },\n    })\n\n    expect(searchInput).toHaveValue('*1*')\n\n    fireEvent.blur(searchInput)\n    expect(handleOpenState).not.toHaveBeenCalled()\n  })\n\n  it('should call \"handleOpenState\" with false when ESCAPE key is pressed', () => {\n    const handleOpenState = jest.fn()\n\n    render(\n      <TableColumnSearchTrigger\n        {...instance(mockedProps)}\n        isOpen\n        handleOpenState={handleOpenState}\n      />,\n    )\n\n    const searchInput = screen.getByTestId('search')\n    expect(searchInput).toBeInTheDocument()\n\n    fireEvent.keyDown(searchInput, { key: 'Escape' })\n    expect(handleOpenState).toHaveBeenCalledWith(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/table-column-search-trigger/TableColumnSearchTrigger.tsx",
    "content": "import React, { useState, useEffect } from 'react'\nimport cx from 'classnames'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { SearchInput } from 'uiSrc/components/base/inputs'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport { SearchIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  isOpen: boolean\n  appliedValue: string\n  initialValue?: string\n  handleOpenState: (isOpen: boolean) => void\n  fieldName: string\n  onApply?: (value: string) => void\n  searchValidation?: Maybe<(value: string) => string>\n}\n\nconst TableColumnSearchTrigger = (props: Props) => {\n  const {\n    isOpen,\n    handleOpenState,\n    fieldName,\n    appliedValue,\n    initialValue = '',\n    onApply = () => {},\n    searchValidation,\n  } = props\n  const [inputEl, setInputEl] = useState<Nullable<HTMLInputElement>>(null)\n  const [value, setValue] = useState<string>(initialValue)\n\n  useEffect(() => {\n    if (isOpen && !!inputEl) {\n      inputEl.focus()\n    }\n  }, [isOpen])\n\n  const handleChangeValue = (initValue: string) => {\n    const value = searchValidation ? searchValidation(initValue) : initValue\n    setValue(value)\n  }\n\n  const handleOpen = () => {\n    handleOpenState(true)\n  }\n\n  const handleApply = (_value: string): void => {\n    if (appliedValue !== _value) {\n      onApply(_value)\n    }\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    if (event.key === keys.ENTER) {\n      handleApply(value)\n    } else if (event.key === keys.ESCAPE) {\n      handleOpenState(false)\n    }\n  }\n\n  const handleOnBlur = (event?: React.FocusEvent<HTMLInputElement>) => {\n    const target = event?.target as HTMLInputElement\n\n    if (!target.value) {\n      handleOpenState(false)\n    }\n  }\n\n  return (\n    <div style={{ paddingRight: 10 }}>\n      <IconButton\n        icon={SearchIcon}\n        aria-label={`Search ${fieldName}`}\n        onClick={handleOpen}\n        data-testid=\"search-button\"\n      />\n      <div\n        className={cx(styles.search)}\n        style={{ display: isOpen ? 'flex' : 'none' }}\n      >\n        <SearchInput\n          onKeyDown={onKeyDown}\n          onBlur={handleOnBlur}\n          ref={setInputEl}\n          name={fieldName}\n          placeholder=\"Search\"\n          value={value || ''}\n          onChange={handleChangeValue}\n          data-testid=\"search\"\n          style={{ width: '100%' }}\n        />\n      </div>\n    </div>\n  )\n}\n\nexport default TableColumnSearchTrigger\n"
  },
  {
    "path": "redisinsight/ui/src/components/table-column-search-trigger/styles.module.scss",
    "content": ".search {\n  display: flex;\n  position: absolute;\n  height: 100%;\n  width: 100%;\n  margin: auto;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  padding: 8px;\n  align-items: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/copilot-trigger/CopilotTrigger.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { IconButtonProps } from 'uiSrc/components/base/forms/buttons/IconButton'\n\nexport const CopilotWrapper = styled(FlexGroup)`\n  user-select: none;\n`\n\nexport const CopilotIconButton = styled(IconButton)<\n  { isOpen: boolean } & IconButtonProps\n>`\n  padding: ${({ theme }) => theme.core.space.space200};\n\n  // TODO: Remove this once size property is enabled for IconButton\n  svg {\n    width: 21px;\n    height: 21px;\n    color: ${({ theme }) => theme.semantic.color.text.attention600};\n  }\n\n  ${({ isOpen, theme }) =>\n    isOpen\n      ? `background-color: ${theme.semantic.color.background.primary200};`\n      : ''}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/copilot-trigger/CopilotTrigger.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  sidePanelsSelector,\n  toggleSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { CopilotIcon } from 'uiSrc/components/base/icons'\nimport { SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { CopilotWrapper, CopilotIconButton } from './CopilotTrigger.styles'\n\nconst CopilotTrigger = () => {\n  const { openedPanel } = useSelector(sidePanelsSelector)\n  const dispatch = useDispatch()\n\n  const handleClickTrigger = () => {\n    dispatch(toggleSidePanel(SidePanels.AiAssistant))\n  }\n\n  const isCopilotOpen = openedPanel === SidePanels.AiAssistant\n\n  return (\n    <CopilotWrapper align=\"center\" justify=\"end\">\n      <RiTooltip content=\"Redis Copilot\">\n        <CopilotIconButton\n          size=\"S\"\n          role=\"button\"\n          icon={CopilotIcon}\n          onClick={handleClickTrigger}\n          data-testid=\"copilot-trigger\"\n          isOpen={isCopilotOpen}\n          aria-label=\"Copilot-trigger\"\n        />\n      </RiTooltip>\n    </CopilotWrapper>\n  )\n}\n\nexport default CopilotTrigger\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/copilot-trigger/index.ts",
    "content": "import CopilotTrigger from './CopilotTrigger'\n\nexport default CopilotTrigger\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/index.ts",
    "content": "import InsightsTrigger from './insights-trigger'\nimport CopilotTrigger from './copilot-trigger'\n\nexport { InsightsTrigger, CopilotTrigger }\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  changeSelectedTab,\n  toggleSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\nimport {\n  recommendationsSelector,\n  resetRecommendationsHighlighting,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Pages } from 'uiSrc/constants'\nimport InsightsTrigger from './InsightsTrigger'\n\nlet store: typeof mockedStore\n\njest.mock('uiSrc/slices/recommendations/recommendations', () => ({\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  recommendationsSelector: jest.fn().mockReturnValue({\n    isHighlighted: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'instanceId',\n    connectionType: 'CLUSTER',\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('InsightsTrigger', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n  })\n\n  it('should render', () => {\n    expect(render(<InsightsTrigger />)).toBeTruthy()\n  })\n\n  it('should call proper actions after click on the button', () => {\n    render(<InsightsTrigger />)\n\n    fireEvent.click(screen.getByTestId('insights-trigger'))\n\n    expect(store.getActions()).toEqual([toggleSidePanel(SidePanels.Insights)])\n  })\n\n  it('should call proper actions after click on the button when there are any recommendations', () => {\n    ;(recommendationsSelector as jest.Mock).mockReturnValue({\n      isHighlighted: true,\n    })\n    render(<InsightsTrigger />)\n\n    fireEvent.click(screen.getByTestId('insights-trigger'))\n\n    expect(store.getActions()).toEqual([\n      resetRecommendationsHighlighting(),\n      changeSelectedTab(InsightsPanelTabs.Recommendations),\n      toggleSidePanel(SidePanels.Insights),\n    ])\n  })\n\n  it('should send proper telemetry', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.browser('instanceId') })\n    ;(recommendationsSelector as jest.Mock).mockReturnValue({\n      isHighlighted: true,\n    })\n    render(<InsightsTrigger />)\n\n    fireEvent.click(screen.getByTestId('insights-trigger'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      eventData: {\n        databaseId: 'instanceId',\n        provider: 'REDIS_CLOUD',\n        source: 'overview',\n        page: '/browser',\n        tab: 'tips',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { IconButtonProps } from 'uiSrc/components/base/forms/buttons/IconButton'\n\nexport const BulbWrapper = styled.div`\n  position: relative;\n`\n\nexport const BulbHighlighting = styled.span`\n  // TODO: Using the background color from the previous value until there is an appropriate color\n  // from the pallete to use for both light and dark themes.\n  background-color: ${({ theme }) => theme.semantic.color.text.attention600};\n  position: absolute;\n  left: 5px;\n  top: 5px;\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral100};\n`\n\nexport const BulbIconButton = styled(IconButton)<\n  { isOpen: boolean } & IconButtonProps\n>`\n  padding: ${({ theme }) => theme.core.space.space200};\n\n  // TODO: Remove this once size property is enabled for IconButton\n  svg {\n    width: 21px;\n    height: 21px;\n  }\n\n  ${({ isOpen, theme }) =>\n    isOpen\n      ? `background-color: ${theme.semantic.color.background.primary200} !important;`\n      : ''}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useLocation, useParams } from 'react-router-dom'\n\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  insightsPanelSelector,\n  sidePanelsSelector,\n  toggleSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\n\nimport {\n  recommendationsSelector,\n  resetRecommendationsHighlighting,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nimport { LightBulbIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport {\n  BulbHighlighting,\n  BulbIconButton,\n  BulbWrapper,\n} from './InsightsTrigger.styles'\n\nexport interface Props {\n  source?: string\n}\n\nconst InsightsTrigger = (props: Props) => {\n  const { source = 'overview' } = props\n  const { openedPanel } = useSelector(sidePanelsSelector)\n  const { tabSelected } = useSelector(insightsPanelSelector)\n  const { isHighlighted } = useSelector(recommendationsSelector)\n  const { provider } = useSelector(connectedInstanceSelector)\n\n  const isInsightsOpen = openedPanel === SidePanels.Insights\n\n  const dispatch = useDispatch()\n  const { pathname, search } = useLocation()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const page = pathname.replace(instanceId, '').replace(/^\\//g, '')\n\n  useEffect(() => {\n    const searchParams = new URLSearchParams(search)\n    const isExploreShouldBeOpened = searchParams.get('insights') === 'open'\n\n    if (isExploreShouldBeOpened) {\n      dispatch(changeSidePanel(SidePanels.Insights))\n      dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n    }\n  }, [search])\n\n  const handleClickTrigger = () => {\n    if (isHighlighted) {\n      dispatch(resetRecommendationsHighlighting())\n      dispatch(changeSelectedTab(InsightsPanelTabs.Recommendations))\n    }\n    dispatch(toggleSidePanel(SidePanels.Insights))\n\n    sendEventTelemetry({\n      event: isInsightsOpen\n        ? TelemetryEvent.INSIGHTS_PANEL_CLOSED\n        : TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      eventData: {\n        provider,\n        page,\n        source,\n        databaseId: instanceId || TELEMETRY_EMPTY_VALUE,\n        tab: isHighlighted ? InsightsPanelTabs.Recommendations : tabSelected,\n      },\n    })\n  }\n\n  return (\n    <RiTooltip\n      title={isHighlighted && instanceId ? undefined : 'Insights'}\n      content={\n        isHighlighted && instanceId\n          ? 'New tips are available'\n          : 'Open interactive tutorials to learn more about Redis or Redis Stack capabilities, or use tips to improve your database.'\n      }\n    >\n      <BulbWrapper>\n        <BulbIconButton\n          size=\"S\"\n          role=\"button\"\n          icon={LightBulbIcon}\n          onClick={handleClickTrigger}\n          data-testid=\"insights-trigger\"\n          isOpen={isInsightsOpen}\n          aria-label=\"Insights-trigger\"\n        />\n        {isHighlighted && instanceId && <BulbHighlighting />}\n      </BulbWrapper>\n    </RiTooltip>\n  )\n}\n\nexport default InsightsTrigger\n"
  },
  {
    "path": "redisinsight/ui/src/components/triggers/insights-trigger/index.ts",
    "content": "import InsightsTrigger from './InsightsTrigger'\n\nexport default InsightsTrigger\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-file/UploadFile.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, fireEvent, screen, waitFor } from 'uiSrc/utils/test-utils'\n\nimport UploadFile, { Props } from './UploadFile'\n\nconst mockedProps = mock<Props>()\n\ndescribe('UploadFile', () => {\n  it('should render', () => {\n    expect(render(<UploadFile {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call onClick', () => {\n    const onClick = jest.fn()\n\n    render(<UploadFile {...instance(mockedProps)} onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('upload-file-btn'))\n\n    expect(onClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should read file', async () => {\n    const onFileChange = jest.fn()\n\n    const jsonString = JSON.stringify({ a: 12 })\n    const blob = new Blob([jsonString])\n    const file = new File([blob], 'empty.json', {\n      type: 'application/JSON',\n    })\n    render(\n      <UploadFile {...instance(mockedProps)} onFileChange={onFileChange} />,\n    )\n\n    const fileInput = screen.getByTestId('upload-input-file')\n    fireEvent.change(fileInput, { target: { files: [file] } })\n    await waitFor(() => expect(onFileChange).toHaveBeenCalled())\n    await waitFor(() =>\n      expect(screen.getByTestId('upload-input-file')).toHaveValue(''),\n    )\n  })\n\n  it('should not call onFileChange', async () => {\n    const onFileChange = jest.fn()\n\n    render(\n      <UploadFile {...instance(mockedProps)} onFileChange={onFileChange} />,\n    )\n\n    const fileInput = screen.getByTestId('upload-input-file')\n    fireEvent.change(fileInput, { target: { files: [] } })\n    await waitFor(() => expect(onFileChange).not.toBeCalled())\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-file/UploadFile.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  onFileChange: (string: string) => void\n  onClick?: () => void\n  accept?: string\n  id?: string\n}\n\nconst UploadFile = (props: Props) => {\n  const { onFileChange, onClick, accept, id = 'upload-input-file' } = props\n\n  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (e.target.files && e.target.files.length > 0) {\n      const fileBlob = e.target.files[0]\n      fileBlob.text().then((text) => {\n        onFileChange(text)\n      })\n      e.target.value = ''\n    }\n  }\n\n  return (\n    <EmptyButton className={styles.emptyBtn}>\n      <label\n        htmlFor={id}\n        className={styles.uploadBtn}\n        data-testid=\"upload-file-btn\"\n      >\n        {/* todo: 'folderOpen', replace with redis-ui once available */}\n        <RiIcon className={styles.icon} type=\"KnowledgeBaseIcon\" />\n        <Text className={styles.label}>Upload</Text>\n        <input\n          type=\"file\"\n          id={id}\n          data-testid={id}\n          accept={accept || '*'}\n          onChange={handleFileChange}\n          onClick={(event) => {\n            event.stopPropagation()\n            onClick?.()\n          }}\n          className={styles.fileDrop}\n          aria-label=\"Select file\"\n        />\n      </label>\n    </EmptyButton>\n  )\n}\n\nexport default UploadFile\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-file/index.ts",
    "content": "import UploadFile from './UploadFile'\n\nexport default UploadFile\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-file/styles.module.scss",
    "content": ".fileDrop {\n  display: none;\n}\n\n.uploadBtn {\n  display: flex;\n  cursor: pointer;\n  padding: 4px 12px;\n}\n\n.emptyBtn:global(.euiButtonEmpty) {\n  height: 22px;\n  margin-top: 7px;\n}\n\n.emptyBtn:global(.euiButtonEmpty .euiButtonEmpty__content) {\n  padding: 0;\n}\n\n.emptyBtn .icon {\n  width: 14px;\n  height: 14px;\n  margin: 2px 4px 0 0;\n  color: var(--inputTextColor);\n}\n\n:global(.euiButtonEmpty.euiButtonEmpty--primary).emptyBtn .label {\n  color: var(--inputTextColor) !important;\n  line-height: 16px !important;\n  font-weight: 400 !important;\n  font-size: 12px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-warning/UploadWarning.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport UploadWarning from './UploadWarning'\n\ndescribe('UploadWarning', () => {\n  it('should render', () => {\n    expect(render(<UploadWarning />)).toBeTruthy()\n  })\n\n  it('should contain the upload warning text', () => {\n    render(<UploadWarning />)\n\n    expect(\n      screen.getByText(\n        'Use files only from trusted authors to avoid automatic execution of malicious code.',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-warning/UploadWarning.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { UploadWarningBanner } from 'uiSrc/components/upload-warning/styles'\n\nconst UploadWarning = () => (\n  <UploadWarningBanner\n    message={\n      <Text size=\"s\" component=\"span\">\n        Use files only from trusted authors to avoid automatic execution of\n        malicious code.\n      </Text>\n    }\n    show\n    showIcon\n    variant=\"attention\"\n  />\n)\n\nexport default UploadWarning\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-warning/index.tsx",
    "content": "import UploadWarning from './UploadWarning'\n\nexport default UploadWarning\n\nexport { UploadWarning }\n"
  },
  {
    "path": "redisinsight/ui/src/components/upload-warning/styles.ts",
    "content": "import styled from 'styled-components'\nimport { Banner } from 'uiSrc/components/base/display/banner'\n\nexport const UploadWarningBanner = styled(Banner)`\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/VirtualGrid.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport { render } from 'uiSrc/utils/test-utils'\nimport VirtualGrid from './VirtualGrid'\nimport { IProps } from './interfaces'\n\nconst mockedProps = mock<IProps>()\n\nconst columns: ITableColumn[] = [\n  {\n    id: 'name',\n    label: 'Member',\n    staySearchAlwaysOpen: true,\n    initialSearchValue: '',\n    truncateText: true,\n    minWidth: 50,\n  },\n]\n\nconst items = ['member1', 'member2']\n\ndescribe('VirtualGrid', () => {\n  it('should render', () => {\n    expect(render(<VirtualGrid {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render rows', () => {\n    expect(\n      render(\n        <VirtualGrid\n          {...instance(mockedProps)}\n          items={items}\n          columns={columns}\n          loading={false}\n          loadMoreItems={jest.fn()}\n          totalItemsCount={items.length}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/VirtualGrid.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport cx from 'classnames'\nimport AutoSizer, { Size } from 'react-virtualized-auto-sizer'\nimport { isObject, xor } from 'lodash'\nimport InfiniteLoader from 'react-window-infinite-loader'\nimport { VariableSizeGrid as Grid, GridChildComponentProps } from 'react-window'\n\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport { SortOrder } from 'uiSrc/constants'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { IProps } from './interfaces'\nimport { getColumnWidth, useInnerElementType } from './utils'\n\nimport styles from './styles.module.scss'\n\nconst loadingMsg = 'loading...'\nlet selectTimer: number = 0\nconst selectTimerDelay = 300\nlet preventSelect = false\n\nconst VirtualGrid = (props: IProps) => {\n  const {\n    rowHeight = 40,\n    totalItemsCount = 0,\n    onChangeSorting = () => {},\n    onRowToggleViewClick = () => {},\n    sortedColumn = null,\n    noItemsMessage = 'No items to display.',\n    loading,\n    columns = [],\n    items = [],\n    onWheel,\n    keyName,\n    loadMoreItems,\n    setScrollTopPosition = () => {},\n    scrollTopProp = 0,\n    maxTableWidth = 0,\n    hideProgress,\n    stickLastColumnHeaderCell,\n  } = props\n\n  const scrollTopRef = useRef<number>(0)\n  const [width, setWidth] = useState<number>(200)\n  const [height, setHeight] = useState<number>(100)\n  const [forceScrollTop, setForceScrollTop] =\n    useState<Maybe<number>>(scrollTopProp)\n  const [expandedRows, setExpandedRows] = useState<number[]>([])\n\n  const gridRef = useRef<Nullable<Grid>>()\n  const rowHeightsMap = useRef<{ [key: number]: { [key: number]: number } }>({})\n  const setRowHeight = useCallback(\n    (rowIndex: number, columnIndex: number, size: number) => {\n      rowHeightsMap.current = {\n        ...rowHeightsMap.current,\n        [rowIndex]: {\n          ...(rowHeightsMap.current[rowIndex] || {}),\n          [columnIndex]: size,\n        },\n      }\n\n      gridRef.current?.resetAfterRowIndex?.(rowIndex)\n    },\n    [],\n  )\n\n  const getRowHeight = (index: number) =>\n    expandedRows.indexOf(index) !== -1\n      ? Math.max(...Object.values(rowHeightsMap.current[index]))\n      : rowHeight\n\n  useEffect(\n    () => () => {\n      setScrollTopPosition(scrollTopRef.current)\n      setExpandedRows([])\n    },\n    [],\n  )\n\n  useEffect(() => {\n    setExpandedRows([])\n    rowHeightsMap.current = {}\n    gridRef.current?.resetAfterRowIndex?.(0)\n  }, [totalItemsCount])\n\n  useEffect(() => {\n    if (forceScrollTop !== undefined) {\n      setForceScrollTop(undefined)\n    }\n  }, [forceScrollTop])\n\n  const onScroll = useCallback(\n    ({ scrollTop }) => {\n      scrollTopRef.current = scrollTop\n    },\n    [scrollTopRef],\n  )\n\n  const changeSorting = (column: any) => {\n    if (\n      !sortedColumn ||\n      !sortedColumn.column ||\n      sortedColumn.column !== column\n    ) {\n      onChangeSorting(column, SortOrder.DESC)\n      return\n    }\n    setExpandedRows([])\n    onChangeSorting(\n      column,\n      sortedColumn.order === SortOrder.DESC ? SortOrder.ASC : SortOrder.DESC,\n    )\n  }\n\n  const loadMoreRows = async (\n    startIndex: number,\n    stopIndex: number,\n  ): Promise<any> => {\n    // We do not load more results for first load\n    if (forceScrollTop !== undefined) return\n\n    if (!loading) {\n      loadMoreItems?.({ keyName, startIndex, stopIndex })\n    }\n  }\n\n  const onResize = ({ height, width }: Size): void => {\n    setHeight(height)\n    setWidth(width)\n  }\n\n  const onCellClick = (event: React.MouseEvent, rowIndex: number) => {\n    selectTimer = window.setTimeout(() => {\n      const textSelected = window.getSelection()?.toString()\n      if (!preventSelect && !textSelected) {\n        setExpandedRows(xor(expandedRows, [rowIndex]))\n\n        onRowToggleViewClick?.(expandedRows.indexOf(rowIndex) === -1, rowIndex)\n      }\n      preventSelect = false\n    }, selectTimerDelay)\n\n    if (event?.detail === 3) {\n      clearSelectTimeout(selectTimer)\n      preventSelect = false\n    }\n  }\n\n  const clearSelectTimeout = (timer: number = 0) => {\n    clearTimeout(timer || selectTimer)\n    preventSelect = true\n  }\n\n  const renderNotEmptyContent = (text: string) => (\n    <Text color=\"primary\" component=\"span\" variant=\"semiBold\">\n      {text || <>&nbsp;</>}\n    </Text>\n  )\n\n  const Cell = ({\n    columnIndex,\n    rowIndex,\n    style,\n  }: GridChildComponentProps<null>) => {\n    const rowData = items[rowIndex]\n    const column = columns[columnIndex]\n    const content: string | { [key: string]: any } = rowData?.[column?.id] || ''\n    const cellRef = useRef<HTMLDivElement>(null)\n\n    const expanded = expandedRows.indexOf(rowIndex) !== -1\n\n    React.useEffect(() => {\n      if (cellRef.current) {\n        const paddingSize = 24\n        const cellHeight =\n          cellRef.current?.children?.[0]?.getBoundingClientRect?.().height +\n          paddingSize\n\n        if (rowIndex !== 0) {\n          setRowHeight(rowIndex, columnIndex, cellHeight)\n        }\n      }\n    }, [setRowHeight, rowIndex, expanded])\n\n    if (rowIndex === 0) {\n      const isLastColumn = columns.length - 1 === columnIndex\n      return (\n        <hgroup className={styles.gridHeaderCell} ref={cellRef} style={style}>\n          <div\n            className={cx(styles.gridHeaderItem, 'truncateText', {\n              [styles.lastHeaderItem]: isLastColumn,\n            })}\n          >\n            {isObject(content) && (\n              <>\n                {!!content?.sortable && (\n                  <button\n                    type=\"button\"\n                    data-testid=\"header-sorting-button\"\n                    className={styles.gridHeaderItemSortable}\n                    onClick={() => changeSorting(column.id)}\n                  >\n                    <Row align=\"center\">\n                      {content.render\n                        ? content.render(content)\n                        : renderNotEmptyContent(content.label)}\n                      <span style={{ paddingLeft: 0 }}>\n                        <RiIcon\n                          size=\"S\"\n                          style={{ marginLeft: '4px' }}\n                          type={\n                            sortedColumn?.order === SortOrder.DESC\n                              ? 'ArrowDownIcon'\n                              : 'ArrowUpIcon'\n                          }\n                        />\n                      </span>\n                    </Row>\n                  </button>\n                )}\n                {!content?.sortable &&\n                  (content.render\n                    ? content.render(content)\n                    : renderNotEmptyContent(content.label))}\n              </>\n            )}\n            {!isObject(content) && renderNotEmptyContent(content)}\n          </div>\n        </hgroup>\n      )\n    }\n\n    if (columnIndex === 0) {\n      const lastColumn = columns[columns.length - 1]\n      const allDynamicRowsHeight: number[] = Object.values(\n        rowHeightsMap.current,\n      ).map((row) => Math.max(...Object.values(row)))\n\n      const allRowsHeight =\n        allDynamicRowsHeight.reduce((a, b) => a + b, 0) +\n        (items.length - allDynamicRowsHeight.length) * rowHeight\n\n      const hasHorizontalScrollOffset = height < allRowsHeight\n\n      return (\n        <div\n          style={style}\n          ref={cellRef}\n          className={cx(\n            styles.gridItem,\n            rowIndex % 2 ? styles.gridItemOdd : styles.gridItemEven,\n          )}\n        >\n          {column?.render &&\n            isObject(rowData) &&\n            column?.render(rowData, expanded)}\n          {!column?.render && content}\n\n          <div\n            className={cx(\n              styles.gridItem,\n              styles.gridItemLast,\n              rowIndex % 2 ? styles.gridItemOdd : styles.gridItemEven,\n            )}\n            style={{\n              width: lastColumn?.minWidth,\n              height: getRowHeight(rowIndex),\n              marginLeft:\n                width -\n                lastColumn?.minWidth -\n                (hasHorizontalScrollOffset ? 23 : 13),\n            }}\n          >\n            {lastColumn?.render &&\n              isObject(rowData) &&\n              lastColumn?.render(rowData, expanded)}\n          </div>\n        </div>\n      )\n    }\n\n    return (\n      <div\n        ref={cellRef}\n        style={style}\n        className={cx(\n          styles.gridItem,\n          rowIndex % 2 ? styles.gridItemOdd : styles.gridItemEven,\n          columnIndex === columns.length - 2 ? 'penult' : '',\n        )}\n      >\n        {column?.render &&\n          isObject(rowData) &&\n          column?.render(rowData, expanded)}\n        {!column?.render && content}\n      </div>\n    )\n  }\n\n  const innerElementType = useInnerElementType(\n    Cell,\n    getColumnWidth,\n    getRowHeight,\n    columns.length - 1,\n    Math.max(maxTableWidth, width),\n    columns,\n    { stickLastColumnHeaderCell },\n  )\n\n  return (\n    <div\n      className={styles.container}\n      onWheel={onWheel}\n      data-testid=\"virtual-grid-container\"\n    >\n      {loading && !hideProgress && (\n        <ProgressBarLoader\n          color=\"primary\"\n          className={styles.progress}\n          data-testid=\"progress-entry-list\"\n        />\n      )}\n      {items.length > 1 && (\n        <AutoSizer onResize={onResize}>\n          {() => (\n            <InfiniteLoader\n              isItemLoaded={(index) => index < items.length}\n              loadMoreItems={loadMoreRows}\n              minimumBatchSize={SCAN_COUNT_DEFAULT}\n              threshold={100}\n              itemCount={totalItemsCount}\n            >\n              {({ onItemsRendered, ref }) => (\n                <Grid\n                  ref={(list) => {\n                    ref(list)\n                    gridRef.current = list\n                  }}\n                  onItemsRendered={(props) =>\n                    onItemsRendered({\n                      visibleStartIndex: props.visibleRowStartIndex || 0,\n                      visibleStopIndex: props.visibleRowStopIndex || 0,\n                      overscanStartIndex: props.overscanRowStartIndex || 0,\n                      overscanStopIndex: props.overscanRowStopIndex || 0,\n                    })\n                  }\n                  className={styles.grid}\n                  columnCount={columns.length}\n                  columnWidth={(i) => getColumnWidth(i, width, columns)}\n                  height={height}\n                  rowCount={items.length}\n                  rowHeight={getRowHeight}\n                  width={width}\n                  innerElementType={innerElementType}\n                  onScroll={onScroll}\n                  initialScrollTop={forceScrollTop}\n                  itemData={items}\n                >\n                  {({ data, rowIndex, columnIndex, style }) => (\n                    <div\n                      onClick={(e) => onCellClick(e, rowIndex)}\n                      role=\"presentation\"\n                    >\n                      <Cell\n                        style={style}\n                        data={data}\n                        columnIndex={columnIndex}\n                        rowIndex={rowIndex}\n                      />\n                    </div>\n                  )}\n                </Grid>\n              )}\n            </InfiniteLoader>\n          )}\n        </AutoSizer>\n      )}\n      {items.length === 1 && (\n        <Text className={styles.noItems}>\n          {loading ? loadingMsg : noItemsMessage}\n        </Text>\n      )}\n    </div>\n  )\n}\n\nexport default VirtualGrid\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/index.ts",
    "content": "import VirtualGrid from './VirtualGrid'\n\nexport * from './utils'\n\nexport default VirtualGrid\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/interfaces.ts",
    "content": "import { ReactNode } from 'react'\nimport {\n  SortOrder,\n  TableCellAlignment,\n  TableCellTextAlignment,\n} from 'uiSrc/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nexport interface IColumnSearchState {\n  initialSearchValue?: string\n  id: string\n  value: string\n  prependSearchName: string\n  isOpened: boolean\n  staySearchAlwaysOpen: boolean\n  searchValidation?: (value: string) => string\n}\n\nexport interface ITableColumn {\n  id: string\n  label: string | ReactNode\n  minWidth?: number\n  maxWidth?: number\n  isSortable?: boolean\n  isSearchable?: boolean\n  isSearchOpen?: boolean\n  initialSearchValue?: string\n  headerClassName?: string\n  headerCellClassName?: string\n  truncateText?: boolean\n  relativeWidth?: number\n  absoluteWidth?: number | string\n  alignment?: TableCellAlignment\n  textAlignment?: TableCellTextAlignment\n  render?: (cellData?: any, rowIndex?: any) => any\n  className?: string\n  prependSearchName?: string\n  staySearchAlwaysOpen?: boolean\n  searchValidation?: (value: string) => string\n}\n\nexport interface IProps {\n  loading: boolean\n  scanned?: number\n  columns: ITableColumn[]\n  loadMoreItems?: (config: any) => void\n  rowHeight?: number\n  footerHeight?: number\n  selectable?: boolean\n  keyName?: RedisResponseBuffer\n  headerHeight?: number\n  searching?: boolean\n  onRowToggleViewClick?: (expanded: boolean, rowIndex: number) => void\n  onSearch?: (newState: any) => void\n  onWheel?: (event: React.WheelEvent) => void\n  onChangeSorting?: (cellData?: any, columnItem?: any) => void\n  items?: any\n  noItemsMessage?: string | string[] | JSX.Element\n  totalItemsCount?: number\n  selectedKey?: any\n  sortedColumn?: ISortedColumn\n  disableScroll?: boolean\n  setScrollTopPosition?: (position: number) => void\n  scrollTopProp?: number\n  hideFooter?: boolean\n  maxTableWidth?: number\n  hideProgress?: boolean\n  stickLastColumnHeaderCell?: boolean\n}\n\nexport interface ISortedColumn {\n  column: string\n  order: SortOrder\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/styles.module.scss",
    "content": "$paddingCell: 12px;\n\n.customScroll {\n  @include eui.scrollBar;\n}\n\n.grid {\n  @include eui.scrollBar;\n  position: relative;\n}\n\n.gridItem {\n  z-index: 1;\n  padding: $paddingCell;\n  border: 1px solid var(--tableDarkestBorderColor);\n  border-left-width: 0;\n  border-top-width: 0;\n  cursor: pointer;\n\n  &:global(.penult) {\n    border-right: 0;\n  }\n}\n\n.gridItemLast {\n  z-index: 6;\n  margin-top: -50px;\n  padding-top: 16px;\n  border-left: 1px solid var(--tableDarkestBorderColor);\n}\n\n.gridHeaderCell {\n  border: 1px solid var(--separatorColorLight);\n\n  &:not(:last-of-type) {\n    border-right: 0;\n  }\n\n  &:first-of-type {\n    border-right: 1px solid var(--separatorColorLight);\n  }\n\n  &:last-of-type, &:nth-of-type(2) {\n    border-left: 0;\n  }\n\n  &:last-of-type {\n    border-left: 1px solid var(--separatorColorLight);\n  }\n}\n\n.gridHeaderItem {\n  overflow: hidden;\n  align-items: center;\n  line-height: 38px;\n  height: 58px;\n  text-transform: none;\n  z-index: 5;\n  padding: $paddingCell;\n  background-color: var(--euiColorEmptyShade);\n\n  &Sortable {\n    &:hover {\n      text-decoration: underline;\n      cursor: pointer;\n    }\n  }\n}\n\n.gridItemOdd {\n  background-color: var(--browserTableRowEven);\n}\n.gridItemEven {\n  background-color: var(--euiColorEmptyShade);\n}\n\n.disableScroll {\n  overflow-y: hidden !important;\n}\n\n.container {\n  position: relative;\n  height: 100%;\n  width: 100%;\n}\n\n.noItems {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border: 1px solid var(--tableDarkestBorderColor);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/tests/utils.spec.ts",
    "content": "import { getColumnWidth } from '../utils'\n\nconst getColumnWidthTests: any[] = [\n  [0, 500, [{ maxWidth: 70, minWidth: 50 }], 50],\n  [\n    1,\n    500,\n    [\n      { maxWidth: 70, minWidth: 50 },\n      { maxWidth: 170, minWidth: 20 },\n    ],\n    20,\n  ],\n  [\n    0,\n    500,\n    [\n      { maxWidth: 470, minWidth: 450 },\n      { maxWidth: 170, minWidth: 20 },\n    ],\n    450,\n  ],\n]\n\nconst minColumnWidth = 10\n\ndescribe('getColumnWidth', () => {\n  it.each(getColumnWidthTests)(\n    'for input: %s (i), %s (width), %s (columns) should be output: %s',\n    (i, width, columns, expected) => {\n      const result = getColumnWidth(i, width, columns, minColumnWidth)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-grid/utils.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport { GridChildComponentProps } from 'react-window'\nimport { ITableColumn } from './interfaces'\n\nexport const getCellIndicies = (child: React.ReactChild) => ({\n  row: child.props.rowIndex,\n  column: child.props.columnIndex,\n})\n\nexport const getShownIndicies = (children: typeof React.Children) => {\n  let minRow = Infinity\n  let maxRow = -Infinity\n  let minColumn = Infinity\n  let maxColumn = -Infinity\n\n  React.Children.forEach(children, (child) => {\n    const { row, column } = getCellIndicies(child)\n    minRow = Math.min(minRow, row)\n    maxRow = Math.max(maxRow, row)\n    minColumn = Math.min(minColumn, column)\n    maxColumn = Math.max(maxColumn, column)\n  })\n\n  return {\n    from: {\n      row: minRow,\n      column: minColumn,\n    },\n    to: {\n      row: maxRow,\n      column: maxColumn,\n    },\n  }\n}\n\nexport const useInnerElementType = (\n  Cell: GridChildComponentProps<null>,\n  columnWidth: (\n    index: number,\n    width: number,\n    columns: ITableColumn[],\n  ) => number,\n  rowHeight: (index: number) => number,\n  columnCount: number,\n  tableWidth: number,\n  columns: ITableColumn[],\n  options: Record<string, any> = {},\n) =>\n  React.useMemo(\n    () =>\n      React.forwardRef((props: ReactNode, ref) => {\n        const sumRowsHeights = (index: number) => {\n          let sum = 0\n          let currentIndex = index\n\n          while (currentIndex > 1) {\n            sum += rowHeight(index - 1)\n            currentIndex -= 1\n          }\n\n          return sum\n        }\n\n        const sumColumnWidths = (index: number) => {\n          let sum = 0\n          let currentIndex = index\n\n          while (currentIndex > 1) {\n            sum += columnWidth(index - 1, tableWidth, columns)\n            currentIndex -= 1\n          }\n\n          return sum\n        }\n\n        const shownIndecies = getShownIndicies(props.children)\n\n        let children = React.Children.map(props.children, (child) => {\n          const { column, row } = getCellIndicies(child)\n\n          // do not show non-sticky cell\n          if (column === 0 || row === 0 || column === columnCount) {\n            return null\n          }\n\n          return {\n            ...child,\n            props: {\n              ...child.props,\n              style: {\n                ...child.props.style,\n                width: columnWidth(column, tableWidth, columns),\n              },\n            },\n          }\n        })\n\n        children = React.Children.toArray(children)\n\n        for (let i = 1; i < children.length; i++) {\n          const child = children[i]\n          const prevChild = children[i - 1]\n          const { row } = getCellIndicies(child)\n          const { row: prevRow } = getCellIndicies(prevChild)\n\n          if (prevRow !== row) {\n            children[i] = child\n          } else {\n            children[i] = {\n              ...child,\n              props: {\n                ...child.props,\n                style: {\n                  ...child.props.style,\n                  left:\n                    prevChild.props.style.left + prevChild.props.style.width,\n                },\n              },\n            }\n          }\n        }\n\n        children.push(\n          React.createElement(Cell, {\n            key: '0:0',\n            rowIndex: 0,\n            columnIndex: 0,\n            style: {\n              display: 'inline-flex',\n              width: columnWidth(0, tableWidth, columns),\n              height: rowHeight(0),\n              position: 'sticky',\n              top: 0,\n              left: 0,\n              zIndex: 4,\n              borderBottomWidth: 1,\n            },\n          }),\n        )\n\n        const toColumnDelta = options?.stickLastColumnHeaderCell ? -1 : 0\n        const shownColumnsCount =\n          shownIndecies.to.column + toColumnDelta - shownIndecies.from.column\n\n        for (let i = 1; i <= shownColumnsCount; i += 1) {\n          const columnIndex = i + shownIndecies.from.column\n          const rowIndex = 0\n          const width = columnWidth(columnIndex, tableWidth, columns)\n          const height = rowHeight(rowIndex)\n\n          const marginLeft = i === 1 ? sumColumnWidths(columnIndex) : undefined\n\n          children.push(\n            React.createElement(Cell, {\n              key: `${rowIndex}:${columnIndex}`,\n              rowIndex,\n              columnIndex,\n              style: {\n                marginLeft,\n                display: 'inline-flex',\n                width,\n                height,\n                position: 'sticky',\n                top: 0,\n                zIndex: 3,\n              },\n            }),\n          )\n        }\n\n        // last sticky column\n        if (options?.stickLastColumnHeaderCell) {\n          const columnIndex = columns.length - 1\n          children.push(\n            React.createElement(Cell, {\n              key: `0:${columnIndex}`,\n              rowIndex: 0,\n              columnIndex,\n              style: {\n                display: 'inline-flex',\n                width: columnWidth(columnIndex, tableWidth, columns),\n                height: rowHeight(0),\n                position: 'sticky',\n                top: 0,\n                right: 0,\n                zIndex: 3,\n              },\n            }),\n          )\n        }\n\n        const shownRowsCount = shownIndecies.to.row - shownIndecies.from.row\n\n        for (let i = 1; i <= shownRowsCount; i += 1) {\n          const columnIndex = 0\n          const rowIndex = i + shownIndecies.from.row\n          const width = columnWidth(columnIndex, tableWidth, columns)\n          const height = rowHeight(rowIndex)\n\n          const marginTop = i === 1 ? sumRowsHeights(rowIndex) : undefined\n\n          children.push(\n            React.createElement(Cell, {\n              key: `${rowIndex}:${columnIndex}`,\n              rowIndex,\n              columnIndex,\n              style: {\n                marginTop,\n                width,\n                height,\n                position: 'sticky',\n                left: 0,\n                zIndex: 2,\n                borderLeftWidth: 1,\n              },\n            }),\n          )\n        }\n\n        return (\n          <div\n            ref={ref}\n            {...props}\n            style={{ ...props?.style, width: tableWidth }}\n          >\n            {children}\n          </div>\n        )\n      }),\n    [Cell, columnWidth, rowHeight, columnCount, tableWidth],\n  )\n\nexport const getColumnWidth = (\n  i: number,\n  width: number,\n  columns: ITableColumn[],\n  minColumnWidth: number = 190,\n) => {\n  const maxTableWidth = columns.reduce(\n    (a, { maxWidth = minColumnWidth }) => a + maxWidth,\n    0,\n  )\n\n  if (maxTableWidth < width) {\n    const growingColumnsWidth = columns\n      .filter(({ maxWidth = 0 }) => maxWidth)\n      .map(({ maxWidth }) => maxWidth)\n\n    const growingColumnsCount = columns.length - growingColumnsWidth.length\n    const maxWidthTable =\n      growingColumnsWidth?.reduce((a = 0, b = 0) => a + b, 0) ?? 0\n    const newColumns = columns.map((column) => {\n      const { minWidth, maxWidth = 0 } = column\n      const newMinWidth = (width - maxWidthTable) / growingColumnsCount\n\n      return {\n        ...column,\n        width: maxWidth ? minWidth : newMinWidth,\n      }\n    })\n\n    return newColumns[i]?.width\n  }\n  return columns[i]?.minWidth\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-list/VirtualList.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport VirtualGrid, { Props } from './VirtualList'\n\nconst mockedProps = mock<Props>()\n\ndescribe('VirtualList', () => {\n  it('should render', () => {\n    expect(render(<VirtualGrid {...instance(mockedProps)} />)).toBeTruthy()\n  })\n  it('should render with empty rows', () => {\n    expect(\n      render(<VirtualGrid {...instance(mockedProps)} items={[]} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-list/VirtualList.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { ListChildComponentProps, VariableSizeList as List } from 'react-window'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  items: (string | JSX.Element)[]\n  overscanCount?: number\n  minRowHeight?: number\n  dynamicHeight?: {\n    itemsCount: number\n    maxHeight: number\n  }\n}\n\nconst PROTRUDING_OFFSET = 2\nconst MIN_ROW_HEIGHT = 18\nconst OVERSCAN_COUNT = 20\nconst MAX_LIST_HEIGHT = globalThis.innerHeight\n\nconst VirtualList = (props: Props) => {\n  const {\n    items = [],\n    dynamicHeight,\n    overscanCount = OVERSCAN_COUNT,\n    minRowHeight = MIN_ROW_HEIGHT,\n  } = props\n  const {\n    itemsCount: dynamicItemsCount = 0,\n    maxHeight: dynamicMaxHeight = MAX_LIST_HEIGHT,\n  } = dynamicHeight || {}\n\n  const listRef = useRef<List>(null)\n  const rowHeights = useRef<{ [key: number]: number }>({})\n  const outerRef = useRef<HTMLDivElement>(null)\n\n  const [listHeight, setListHeight] = useState(MIN_ROW_HEIGHT)\n  const [, forceRender] = useState({})\n\n  const getRowHeight = (index: number) =>\n    rowHeights.current[index] > minRowHeight\n      ? rowHeights.current[index] + 2\n      : minRowHeight\n\n  const setRowHeight = (index: number, size: number) => {\n    listRef.current?.resetAfterIndex(0)\n    rowHeights.current = { ...rowHeights.current, [index]: size }\n  }\n\n  const calculateHeight = () => {\n    listRef.current?.resetAfterIndex(0)\n    if (dynamicItemsCount && items.length > dynamicItemsCount) {\n      setListHeight(dynamicMaxHeight)\n    }\n\n    setListHeight(\n      Math.min(\n        items.reduce((prev, _item, index) => getRowHeight(index) + prev, 0),\n        dynamicMaxHeight,\n      ),\n    )\n  }\n\n  const Row = ({ index, style }: ListChildComponentProps) => {\n    const rowRef = useRef<HTMLDivElement>(null)\n\n    useEffect(() => {\n      if (rowRef.current) {\n        setRowHeight(index, rowRef.current?.clientHeight)\n        calculateHeight()\n      }\n    }, [rowRef])\n\n    const rowContent = items[index]\n\n    return (\n      <div style={style} className={styles.item} data-testid={`row-${index}`}>\n        <div className={styles.message} ref={rowRef}>\n          {rowContent}\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <AutoSizer disableHeight={!!dynamicHeight} onResize={() => forceRender({})}>\n      {({ width, height = 0 }) => (\n        <List\n          itemCount={items.length}\n          itemSize={getRowHeight}\n          ref={listRef}\n          className={styles.listContent}\n          outerRef={outerRef}\n          overscanCount={overscanCount}\n          height={height || listHeight}\n          width={width - PROTRUDING_OFFSET}\n        >\n          {Row}\n        </List>\n      )}\n    </AutoSizer>\n  )\n}\n\nexport default VirtualList\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-list/index.ts",
    "content": "import VirtualList from './VirtualList'\n\nexport default VirtualList\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-list/styles.module.scss",
    "content": ".listContent {\n  @include eui.scrollBar;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-table/VirtualTable.spec.tsx",
    "content": "import React from 'react'\nimport { mock, instance } from 'ts-mockito'\n\nimport { SortOrder } from 'uiSrc/constants'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport VirtualTable from './VirtualTable'\nimport { IProps, ITableColumn } from './interfaces'\n\nconst mockedProps = mock<IProps>()\n\nconst columns: ITableColumn[] = [\n  {\n    id: 'name',\n    label: 'Member',\n    isSearchable: true,\n    staySearchAlwaysOpen: true,\n    initialSearchValue: '',\n    truncateText: true,\n  },\n]\n\nconst sortedColumn = {\n  column: 'name',\n  order: SortOrder.ASC,\n}\n\nconst members = ['member1', 'member2']\n\ndescribe('VirtualTable', () => {\n  it('should render with empty rows', () => {\n    expect(\n      render(\n        <VirtualTable\n          {...instance(mockedProps)}\n          items={[]}\n          columns={columns}\n          loading={false}\n          loadMoreItems={jest.fn()}\n          totalItemsCount={members.length}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render rows', () => {\n    expect(\n      render(\n        <VirtualTable\n          {...instance(mockedProps)}\n          items={members}\n          columns={columns}\n          loading={false}\n          loadMoreItems={jest.fn()}\n          totalItemsCount={members.length}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render search', () => {\n    render(\n      <VirtualTable\n        {...instance(mockedProps)}\n        items={members}\n        columns={columns}\n        loading={false}\n        loadMoreItems={jest.fn()}\n        totalItemsCount={members.length}\n      />,\n    )\n    const searchInput = screen.getByTestId('search')\n    expect(searchInput).toBeInTheDocument()\n  })\n\n  it('should open search clicked by search button', () => {\n    const updatedColumns = [\n      {\n        ...columns[0],\n        staySearchAlwaysOpen: false,\n      },\n    ]\n    render(\n      <VirtualTable\n        {...instance(mockedProps)}\n        items={members}\n        columns={updatedColumns}\n        loading={false}\n        loadMoreItems={jest.fn()}\n        totalItemsCount={members.length}\n      />,\n    )\n    const searchInput = screen.getByTestId('search')\n    expect(searchInput).not.toBeVisible()\n    const searchButton = screen.getByTestId('search-button')\n    fireEvent.click(searchButton)\n    expect(searchInput).toBeVisible()\n  })\n\n  it('should call sort column', () => {\n    const updatedColumns = [\n      {\n        ...columns[0],\n        isSortable: true,\n        isSearchable: false,\n      },\n    ]\n    const onChangeSorting = jest.fn()\n    const { container } = render(\n      <VirtualTable\n        {...instance(mockedProps)}\n        items={members}\n        columns={updatedColumns}\n        loading={false}\n        loadMoreItems={jest.fn()}\n        totalItemsCount={members.length}\n        sortedColumn={sortedColumn}\n        onChangeSorting={onChangeSorting}\n      />,\n    )\n\n    fireEvent.click(container.querySelector('.headerButtonSorted') as Element)\n\n    expect(onChangeSorting).toBeCalled()\n  })\n\n  it('should call onRowClick by clicking row', () => {\n    const onRowClick = jest.fn()\n    render(\n      <VirtualTable\n        {...instance(mockedProps)}\n        items={members}\n        columns={columns}\n        loading={false}\n        loadMoreItems={jest.fn()}\n        totalItemsCount={members.length}\n        onRowClick={onRowClick}\n      />,\n    )\n    const firstRow = screen.getAllByLabelText(/row/)[0]\n    fireEvent.click(firstRow)\n\n    expect(onRowClick).toBeCalled()\n  })\n\n  describe('Scan more', () => {\n    const scanMoreBtnId = 'scan-more'\n\n    it('Scan more button should be in the document when total > scanned', () => {\n      const { queryByTestId } = render(\n        <VirtualTable\n          {...instance(mockedProps)}\n          items={[]}\n          columns={[]}\n          scanned={20}\n          totalItemsCount={100}\n        />,\n      )\n      const scanMoreBtn = queryByTestId(scanMoreBtnId)\n\n      expect(scanMoreBtn).toBeInTheDocument()\n    })\n    // obsolete test. todo: review and remove or refactor\n    xit('Scan more button should no be in the document when total == scanned', () => {\n      const { queryByTestId } = render(\n        <VirtualTable\n          {...instance(mockedProps)}\n          items={[]}\n          columns={[]}\n          scanned={100}\n          totalItemsCount={100}\n        />,\n      )\n      const scanMoreBtn = queryByTestId(scanMoreBtnId)\n\n      expect(scanMoreBtn).not.toBeInTheDocument()\n    })\n\n    it('Scan more button should call loadMoreItems with arguments', () => {\n      const onLoadMoreItems = jest.fn()\n\n      const argMock = {\n        stopIndex: SCAN_COUNT_DEFAULT - 1,\n        startIndex: 0,\n      }\n\n      render(\n        <VirtualTable\n          {...instance(mockedProps)}\n          {...argMock}\n          items={[]}\n          columns={[]}\n          loadMoreItems={onLoadMoreItems}\n          scanned={20}\n          totalItemsCount={100}\n        />,\n      )\n      const scanMoreBtn = screen.getByTestId(scanMoreBtnId)\n\n      fireEvent.click(scanMoreBtn)\n\n      expect(scanMoreBtn).toBeInTheDocument()\n      expect(onLoadMoreItems).toBeCalledWith(argMock)\n    })\n  })\n\n  it('should show resize trigger for resizable column', () => {\n    const updatedColumns = [\n      {\n        ...columns[0],\n        isResizable: true,\n      },\n    ]\n\n    render(\n      <VirtualTable\n        {...instance(mockedProps)}\n        items={members}\n        columns={updatedColumns}\n        loading={false}\n        loadMoreItems={jest.fn()}\n        totalItemsCount={members.length}\n      />,\n    )\n\n    expect(screen.getByTestId('resize-trigger-name')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-table/VirtualTable.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport cx from 'classnames'\nimport { findIndex, isNumber, sumBy, xor } from 'lodash'\nimport {\n  CellMeasurer,\n  CellMeasurerCache,\n  Column,\n  IndexRange,\n  InfiniteLoader,\n  RowMouseEventHandlerParams,\n  Table,\n  TableCellProps,\n} from 'react-virtualized'\nimport TableColumnSearchTrigger from 'uiSrc/components/table-column-search-trigger/TableColumnSearchTrigger'\nimport TableColumnSearch from 'uiSrc/components/table-column-search/TableColumnSearch'\nimport { SortOrder } from 'uiSrc/constants'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\n\nimport { isEqualBuffers, Maybe, Nullable } from 'uiSrc/utils'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RIResizeObserver } from 'uiSrc/components/base/utils'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport {\n  ColumnWidthSizes,\n  IColumnSearchState,\n  IProps,\n  IResizeEvent,\n  ITableColumn,\n  ResizableState,\n} from './interfaces'\nimport KeysSummary from '../keys-summary'\n\nimport styles from './styles.module.scss'\n\n// this is needed to align content when scrollbar appears\nconst TABLE_OUTSIDE_WIDTH = 24\n\nconst VirtualTable = (props: IProps) => {\n  const {\n    autoHeight,\n    tableRef,\n    selectable = false,\n    expandable = false,\n    headerHeight = 44,\n    rowHeight = 40,\n    scanned = 0,\n    threshold = 100,\n    totalItemsCount = 0,\n    onRowClick = () => {},\n    onSearch = () => {},\n    onChangeSorting = () => {},\n    onRowToggleViewClick = () => {},\n    sortedColumn = null,\n    selectedKey = null,\n    noItemsMessage = 'No keys to display.',\n    searching,\n    loading,\n    columns,\n    items,\n    disableScroll,\n    onWheel,\n    keyName,\n    loadMoreItems,\n    setScrollTopPosition = () => {},\n    scrollTopProp = 0,\n    hideFooter = false,\n    tableWidth = 0,\n    hideProgress,\n    onChangeWidth = () => {},\n    expandedRows = [],\n    setExpandedRows = () => {},\n    onRowsRendered: onRowsRenderedProps,\n    cellCache = new CellMeasurerCache({\n      fixedWidth: true,\n      minHeight: rowHeight,\n    }),\n    onColResizeEnd,\n  } = props\n  let selectTimer: number = 0\n  const selectTimerDelay = 300\n  let preventSelect = false\n\n  const scrollTopRef = useRef<number>(0)\n  const resizeColRef = useRef<ResizableState>({\n    column: null,\n    active: false,\n    x: 0,\n  })\n\n  const [selectedRowIndex, setSelectedRowIndex] =\n    useState<Nullable<number>>(null)\n  const [search, setSearch] = useState<IColumnSearchState[]>([])\n  const [width, setWidth] = useState<number>(100)\n  const [height, setHeight] = useState<number>(100)\n  const [forceScrollTop, setForceScrollTop] =\n    useState<Maybe<number>>(scrollTopProp)\n  const [columnWidthSizes, setColumnWidthSizes] =\n    useState<Nullable<ColumnWidthSizes>>(null)\n  const [isColResizing, setIsColResizing] = useState(false)\n  const [, forceUpdate] = useState({})\n\n  const [minWidthAllCols, setMinWidthAllCols] = useState<number>(0)\n\n  useEffect(() => {\n    const searchableFields: ITableColumn[] = columns.filter(\n      (column: ITableColumn) => column.isSearchable,\n    )\n    searchableFields.forEach((column) => {\n      setSearch([\n        ...search,\n        {\n          id: column.id,\n          value: column.initialSearchValue ?? '',\n          initialSearchValue: column.initialSearchValue ?? '',\n          isOpened: !!column.staySearchAlwaysOpen || !!column.isSearchOpen,\n          staySearchAlwaysOpen: !!column.staySearchAlwaysOpen,\n          prependSearchName: column.prependSearchName ?? '',\n          searchValidation: column.searchValidation,\n        },\n      ])\n    })\n\n    return () => {\n      setScrollTopPosition(scrollTopRef.current)\n      setExpandedRows([])\n      cellCache?.clearAll()\n    }\n  }, [])\n\n  useEffect(() => {\n    setMinWidthAllCols(sumBy(columns, (col) => col?.minWidth || 0))\n  }, [columns])\n\n  useEffect(() => {\n    if (forceScrollTop !== undefined) {\n      setForceScrollTop(undefined)\n    }\n  }, [forceScrollTop])\n\n  useEffect(() => {\n    const selectedRowIndex = selectedKey\n      ? findIndex(items, ({ name }) => isEqualBuffers(name, selectedKey.name))\n      : null\n    setSelectedRowIndex(\n      isNumber(selectedRowIndex) && selectedRowIndex > -1\n        ? selectedRowIndex\n        : null,\n    )\n  }, [selectedKey, items])\n\n  useEffect(() => {\n    setExpandedRows([])\n    cellCache?.clearAll()\n  }, [totalItemsCount])\n\n  const clearSelectTimeout = (timer: number = 0) => {\n    clearTimeout(timer || selectTimer)\n    preventSelect = true\n  }\n\n  const clearCache = () =>\n    setTimeout(() => {\n      cellCache.clearAll()\n      forceUpdate({})\n    }, 0)\n\n  const getWidthOfColumn = (\n    colId: string,\n    colWidth: number,\n    width: number,\n    isRelative = false,\n  ) => {\n    let newColWidth = isRelative ? (colWidth / 100) * width : colWidth\n\n    const currentColumn = columns.find((col) => col.id === colId)\n    const maxWidthFromTable =\n      width -\n      (minWidthAllCols - (currentColumn?.minWidth || 0)) -\n      TABLE_OUTSIDE_WIDTH\n    const maxWidth = currentColumn?.maxWidth || maxWidthFromTable\n    const minWidth = currentColumn?.minWidth || 60\n\n    if (newColWidth > maxWidth) newColWidth = maxWidth\n    if (newColWidth < minWidth) newColWidth = minWidth\n\n    const newAbsWidth = Math.floor(newColWidth)\n    return {\n      abs: newAbsWidth,\n      relative: (newAbsWidth / width) * 100,\n    }\n  }\n\n  const onRowSelect = (data: RowMouseEventHandlerParams) => {\n    const isRowSelectable = checkIfRowSelectable(data.rowData)\n\n    onRowClick(data)\n    if (isRowSelectable && selectable) {\n      setSelectedRowIndex(data.index)\n    }\n\n    if (isRowSelectable && expandable) {\n      selectTimer = window.setTimeout(\n        () => {\n          const textSelected = window.getSelection()?.toString()\n          if (!preventSelect && !textSelected) {\n            setExpandedRows(xor(expandedRows, [data.index]))\n            onRowToggleViewClick?.(\n              expandedRows.indexOf(data.index) === -1,\n              data.index,\n            )\n\n            clearCache()\n            setTimeout(() => {\n              clearCache()\n            }, 0)\n          }\n          preventSelect = false\n        },\n        selectTimerDelay,\n        cellCache,\n      )\n\n      if (data.event?.detail === 3) {\n        clearSelectTimeout(selectTimer)\n        preventSelect = false\n      }\n\n      cellCache.clearAll()\n    }\n  }\n\n  const onScroll = useCallback(\n    ({ scrollTop }: { scrollTop: number }) => {\n      scrollTopRef.current = scrollTop\n    },\n    [scrollTopRef],\n  )\n\n  const onResize = ({ height, width }: IResizeEvent): void => {\n    setHeight(height)\n    setWidth(Math.floor(width))\n    onChangeWidth?.(width)\n\n    if (!columnWidthSizes) {\n      // init width sizes\n      setColumnWidthSizes(\n        columns\n          .filter((col) => col.isResizable)\n          .reduce((prev, next) => {\n            const propAbsWidth =\n              next.absoluteWidth && isNumber(next.absoluteWidth)\n                ? next.absoluteWidth\n                : 0\n            return {\n              ...prev,\n              [next.id]: next.relativeWidth\n                ? getWidthOfColumn(next.id, next.relativeWidth, width, true)\n                : getWidthOfColumn(next.id, propAbsWidth, width),\n            }\n          }, {}),\n      )\n    }\n\n    if (columnWidthSizes) {\n      setColumnWidthSizes((colWidthSizesPrev) => {\n        const newSizes: ColumnWidthSizes = {}\n        // eslint-disable-next-line guard-for-in,no-restricted-syntax\n        for (const col in colWidthSizesPrev) {\n          newSizes[col] = getWidthOfColumn(\n            col,\n            colWidthSizesPrev[col].relative,\n            width,\n            true,\n          )\n        }\n        return newSizes\n      })\n    }\n\n    cellCache?.clearAll()\n  }\n\n  const onDragColumn = (e: React.MouseEvent) => {\n    const { column, x, active } = resizeColRef.current\n    if (active && column) {\n      const diffX = x - e.clientX\n      setColumnWidthSizes((prev) => {\n        if (!prev) return null\n\n        resizeColRef.current.x = e.clientX\n        return {\n          ...prev,\n          [column]: getWidthOfColumn(column, prev[column].abs - diffX, width),\n        }\n      })\n\n      cellCache?.clearAll()\n    }\n  }\n\n  const onDragColumnStart = (e: React.MouseEvent, column: ITableColumn) => {\n    resizeColRef.current = {\n      column: column.id,\n      active: true,\n      x: e.clientX,\n    }\n    setIsColResizing(true)\n  }\n\n  const onDragColumnEnd = () => {\n    if (resizeColRef.current.active) {\n      resizeColRef.current = {\n        active: false,\n        column: null,\n        x: 0,\n      }\n      setIsColResizing(false)\n      cellCache?.clearAll()\n\n      if (columnWidthSizes) {\n        onColResizeEnd?.(\n          Object.keys(columnWidthSizes).reduce(\n            (prev, next) => ({\n              ...prev,\n              [next]: columnWidthSizes[next].relative,\n            }),\n            {},\n          ),\n        )\n      }\n    }\n  }\n\n  const checkIfRowSelectable = (rowData: any) => !!rowData\n\n  const cellRenderer = ({\n    cellData,\n    columnIndex,\n    rowData,\n    rowIndex,\n    parent,\n    dataKey,\n  }: TableCellProps) => {\n    const column = columns[columnIndex]\n    if (column.render) {\n      return (\n        <CellMeasurer\n          cache={cellCache}\n          columnIndex={columnIndex}\n          rowIndex={rowIndex}\n          parent={parent}\n          key={rowIndex + columnIndex + dataKey}\n        >\n          <div\n            className={styles.tableRowCell}\n            style={{\n              justifyContent: column.alignment,\n              wordBreak: 'break-word',\n            }}\n            role=\"presentation\"\n          >\n            {column?.render?.(\n              cellData,\n              rowData,\n              expandedRows.indexOf(rowIndex) !== -1,\n              rowIndex,\n            )}\n          </div>\n        </CellMeasurer>\n      )\n    }\n    return (\n      <CellMeasurer\n        cache={cellCache}\n        columnIndex={columnIndex}\n        rowIndex={rowIndex}\n        parent={parent}\n        key={rowIndex + columnIndex + dataKey}\n      >\n        <div\n          className={styles.tableRowCell}\n          style={{ justifyContent: column.alignment, whiteSpace: 'normal' }}\n        >\n          <Text component=\"div\" style={{ maxWidth: '100%' }}>\n            <div\n              style={{ display: 'flex' }}\n              className={column.truncateText ? 'truncateText' : ''}\n            >\n              {cellData}\n            </div>\n          </Text>\n        </div>\n      </CellMeasurer>\n    )\n  }\n\n  const changeSorting = (column: any) => {\n    if (\n      !sortedColumn ||\n      !sortedColumn.column ||\n      sortedColumn.column !== column\n    ) {\n      onChangeSorting(column, SortOrder.DESC)\n      return\n    }\n    onChangeSorting(\n      column,\n      sortedColumn.order === SortOrder.DESC ? SortOrder.ASC : SortOrder.DESC,\n    )\n  }\n\n  const headerRenderer = ({ columnIndex, cellClass = '' }: any) => {\n    const column = columns[columnIndex]\n    const isColumnSorted = sortedColumn && sortedColumn.column === column.id\n\n    return (\n      <div\n        className=\"flex-row fluid\"\n        style={{ justifyContent: column.alignment, position: 'relative' }}\n      >\n        {column.isSortable && !searching && (\n          <div\n            className={styles.headerCell}\n            style={{ justifyContent: column.alignment }}\n          >\n            <button\n              type=\"button\"\n              onClick={() => changeSorting(column.id)}\n              className={cx(\n                cellClass,\n                styles.headerButton,\n                isColumnSorted ? styles.headerButtonSorted : null,\n              )}\n              data-testid=\"score-button\"\n              style={{ justifyContent: column.alignment }}\n            >\n              <Text size=\"m\" className={cellClass} variant=\"semiBold\">\n                <span>{column.label}</span>\n              </Text>\n            </button>\n          </div>\n        )}\n        {(!column.isSortable || (column.isSortable && searching)) && (\n          <div\n            className={cx(styles.headerCell, cellClass, 'relative')}\n            style={{ flex: '1' }}\n          >\n            <div\n              style={{\n                justifyContent: column.alignment,\n                textAlign: column.textAlignment,\n                flex: '1',\n              }}\n            >\n              <Text size=\"m\" className={cellClass} variant=\"semiBold\">\n                <span>{column.label}</span>\n              </Text>\n            </div>\n            {column.isSearchable && searchRenderer(column)}\n          </div>\n        )}\n        {isColumnSorted && !searching && (\n          <div className={styles.headerCell} style={{ paddingLeft: 0 }}>\n            <button\n              type=\"button\"\n              onClick={() => changeSorting(column.id)}\n              className={cx(\n                styles.headerButton,\n                isColumnSorted ? styles.headerButtonSorted : null,\n              )}\n              data-testid=\"header-sorting-button\"\n            >\n              <RiIcon\n                style={{ marginLeft: '4px' }}\n                size=\"S\"\n                type={\n                  sortedColumn?.order === SortOrder.DESC\n                    ? 'ArrowDownIcon'\n                    : 'ArrowUpIcon'\n                }\n              />\n            </button>\n          </div>\n        )}\n        {column.isResizable && (\n          <div\n            className={styles.resizeTrigger}\n            onMouseDown={(e) => onDragColumnStart(e, column)}\n            data-testid={`resize-trigger-${column.id}`}\n            role=\"presentation\"\n          />\n        )}\n      </div>\n    )\n  }\n\n  const noRowsRenderer = () => (\n    <>\n      {noItemsMessage && (\n        <div className={styles.placeholder}>\n          <Text textAlign=\"center\" size=\"m\">\n            {loading ? 'loading...' : noItemsMessage}\n          </Text>\n        </div>\n      )}\n    </>\n  )\n\n  const loadMoreRows = async (params: IndexRange): Promise<any> => {\n    const { startIndex, stopIndex } = params\n\n    // We do not load more results for first load\n    if (forceScrollTop !== undefined) return\n\n    if (!loading) {\n      loadMoreItems?.({ keyName, startIndex, stopIndex })\n    }\n  }\n\n  const isRowLoaded = ({ index }: any) => !!items[index]\n\n  const handleColumnSearchVisibility = (\n    columnId: string,\n    isOpened: boolean,\n  ) => {\n    const newSearch = search.map((column) => {\n      if (column.id === columnId) {\n        return {\n          ...column,\n          isOpened,\n        }\n      }\n      return column\n    })\n    setSearch(newSearch)\n  }\n\n  const handleColumnSearchApply = (columnId: string, value: string) => {\n    const newState = search.map((column) => {\n      if (column.id === columnId) {\n        return {\n          ...column,\n          isOpened: !!value,\n          value,\n        }\n      }\n      return column\n    })\n    setSearch(newState)\n    onSearch?.(newState)\n  }\n\n  const searchRenderer = (column: ITableColumn) => {\n    const columnSearchState = search.find(\n      (columnState) => columnState.id === column.id,\n    )\n    if (!columnSearchState) {\n      return null\n    }\n    if (columnSearchState.staySearchAlwaysOpen) {\n      return (\n        <TableColumnSearch\n          appliedValue={columnSearchState.value}\n          fieldName={columnSearchState.id}\n          onApply={(value) => handleColumnSearchApply(column.id, value)}\n          prependSearchName={columnSearchState.prependSearchName}\n          searchValidation={columnSearchState.searchValidation}\n        />\n      )\n    }\n    return (\n      <TableColumnSearchTrigger\n        appliedValue={columnSearchState.value}\n        initialValue={columnSearchState.initialSearchValue}\n        fieldName={columnSearchState.id}\n        onApply={(value) => handleColumnSearchApply(column.id, value)}\n        isOpen={columnSearchState.isOpened}\n        prependSearchName={columnSearchState.prependSearchName}\n        handleOpenState={(isOpen) =>\n          handleColumnSearchVisibility(column.id, isOpen)\n        }\n        searchValidation={columnSearchState.searchValidation}\n      />\n    )\n  }\n\n  return (\n    <RIResizeObserver onResize={onResize}>\n      {(resizeRef) => (\n        <div\n          ref={resizeRef}\n          className={cx(styles.container, {\n            [styles.isResizing]: isColResizing,\n          })}\n          onWheel={onWheel}\n          onMouseMove={onDragColumn}\n          onMouseUp={onDragColumnEnd}\n          onMouseLeave={onDragColumnEnd}\n          role=\"presentation\"\n          data-testid=\"virtual-table-container\"\n        >\n          {loading && !hideProgress && (\n            <ProgressBarLoader\n              absolute\n              color=\"primary\"\n              data-testid=\"progress-key-table\"\n            />\n          )}\n          <InfiniteLoader\n            isRowLoaded={isRowLoaded}\n            minimumBatchSize={SCAN_COUNT_DEFAULT}\n            threshold={threshold}\n            loadMoreRows={loadMoreRows}\n            rowCount={totalItemsCount || undefined}\n          >\n            {({ onRowsRendered, registerChild }) => (\n              <Table\n                onRowClick={onRowSelect}\n                onRowDoubleClick={() => clearSelectTimeout()}\n                estimatedRowSize={rowHeight}\n                ref={(ref) => {\n                  if (tableRef) {\n                    tableRef.current = ref\n                  }\n\n                  return registerChild(ref)\n                }}\n                headerHeight={headerHeight}\n                rowHeight={cellCache.rowHeight}\n                width={tableWidth > width ? tableWidth : width}\n                noRowsRenderer={noRowsRenderer}\n                height={height}\n                className={cx(styles.table, {\n                  [styles.autoHeight]: autoHeight,\n                })}\n                gridClassName={cx(styles.customScroll, styles.grid, {\n                  [styles.disableScroll]: disableScroll,\n                })}\n                rowClassName={({ index }) =>\n                  cx([\n                    styles.tableRow,\n                    {\n                      'table-row-selected': selectedRowIndex === index,\n                      [styles.tableRowEven]: index % 2 === 0,\n                    },\n                  ])\n                }\n                headerClassName={styles.headerColumn}\n                rowCount={items.length}\n                rowGetter={({ index }) => items[index]}\n                onScroll={onScroll}\n                scrollTop={forceScrollTop}\n                deferredMeasurementCache={cellCache}\n                onRowsRendered={(props) => {\n                  onRowsRendered(props)\n                  onRowsRenderedProps?.(props)\n                }}\n              >\n                {columns.map((column: ITableColumn, index: number) => (\n                  <Column\n                    minWidth={column.minWidth}\n                    maxWidth={column.maxWidth}\n                    label={column.label}\n                    dataKey={column.id}\n                    width={\n                      columnWidthSizes?.[column.id]?.abs ||\n                      (column.absoluteWidth || column.relativeWidth\n                        ? (column.relativeWidth ?? 0)\n                        : 20)\n                    }\n                    flexGrow={\n                      !column.absoluteWidth && !column.relativeWidth ? 1 : 0\n                    }\n                    headerRenderer={(headerProps) =>\n                      headerRenderer({\n                        ...headerProps,\n                        columnIndex: index,\n                        cellClass: column.headerCellClassName,\n                      })\n                    }\n                    cellRenderer={cellRenderer}\n                    headerClassName={column.headerClassName ?? ''}\n                    className={cx(\n                      styles.tableRowColumn,\n                      column.className ?? '',\n                    )}\n                    key={column.id}\n                  />\n                ))}\n              </Table>\n            )}\n          </InfiniteLoader>\n          {!hideFooter && (\n            <div className={cx(styles.tableFooter)}>\n              <KeysSummary\n                scanned={scanned}\n                totalItemsCount={totalItemsCount || undefined}\n                loading={loading}\n                loadMoreItems={loadMoreItems}\n                items={items}\n              />\n            </div>\n          )}\n        </div>\n      )}\n    </RIResizeObserver>\n  )\n}\n\nexport default VirtualTable\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-table/index.ts",
    "content": "import VirtualTable from './VirtualTable'\n\nexport * from './utils'\n\nexport default VirtualTable\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-table/interfaces.ts",
    "content": "import { ReactNode } from 'react'\nimport {\n  CellMeasurerCache,\n  IndexRange,\n  OverscanIndexRange,\n} from 'react-virtualized'\nimport {\n  SortOrder,\n  TableCellAlignment,\n  TableCellTextAlignment,\n} from 'uiSrc/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\n\nexport interface IColumnSearchState {\n  initialSearchValue?: string\n  id: string\n  value: string\n  prependSearchName: string\n  isOpened: boolean\n  staySearchAlwaysOpen: boolean\n  searchValidation?: (value: string) => string\n}\n\nexport interface IResizeEvent {\n  width: number\n  height: number\n}\n\nexport interface ITableColumn {\n  id: string\n  label: string | ReactNode\n  minWidth?: number\n  maxWidth?: number\n  isSortable?: boolean\n  isResizable?: boolean\n  isSearchable?: boolean\n  isSearchOpen?: boolean\n  initialSearchValue?: string\n  headerClassName?: string\n  headerCellClassName?: string\n  truncateText?: boolean\n  relativeWidth?: number\n  absoluteWidth?: number | string\n  alignment?: TableCellAlignment\n  textAlignment?: TableCellTextAlignment\n  render?: (\n    cellData?: any,\n    columnItem?: any,\n    expanded?: boolean,\n    rowIndex?: number,\n  ) => any\n  className?: string\n  prependSearchName?: string\n  staySearchAlwaysOpen?: boolean\n  searchValidation?: (value: string) => string\n}\n\nexport interface IProps {\n  autoHeight?: boolean\n  tableRef?: React.Ref<any>\n  loading: boolean\n  scanned?: number\n  columns: ITableColumn[]\n  threshold?: number\n  loadMoreItems?: (config: any) => void\n  rowHeight?: number\n  footerHeight?: number\n  selectable?: boolean\n  expandable?: boolean\n  keyName?: RedisResponseBuffer\n  headerHeight?: number\n  searching?: boolean\n  onRowClick?: (rowData: any) => void\n  onRowToggleViewClick?: (expanded: boolean, rowIndex: number) => void\n  onSearch?: (newState: any) => void\n  onWheel?: (event: React.WheelEvent) => void\n  onChangeSorting?: (cellData?: any, columnItem?: any) => void\n  items?: any\n  noItemsMessage?: string | string[] | JSX.Element\n  totalItemsCount?: Nullable<number>\n  selectedKey?: any\n  sortedColumn?: ISortedColumn\n  disableScroll?: boolean\n  setScrollTopPosition?: (position: number) => void\n  scrollTopProp?: number\n  hideFooter?: boolean\n  tableWidth?: number\n  hideProgress?: boolean\n  onChangeWidth?: (width: number) => void\n  cellCache?: CellMeasurerCache\n  expandedRows?: number[]\n  overscanRowCount?: number\n  setExpandedRows?: (rows: number[]) => void\n  onRowsRendered?: (info: IndexRange & OverscanIndexRange) => void\n  onColResizeEnd?: (cols: RelativeWidthSizes) => void\n}\n\nexport interface ISortedColumn {\n  column: string\n  order: SortOrder\n}\n\nexport interface RelativeWidthSizes {\n  [key: string]: number\n}\n\nexport interface AbsoluteWidthSizes {\n  [key: string]: {\n    abs: number\n  }\n}\n\nexport type ColumnWidthSizes = AbsoluteWidthSizes & {\n  [key: string]: {\n    relative: number\n  }\n}\n\nexport interface ResizableState {\n  column: Nullable<string>\n  active: boolean\n  x: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-table/styles.module.scss",
    "content": "$headerHeight: 44px;\n$footerHeight: 38px;\n\n.customScroll {\n  @include eui.scrollBar;\n}\n\n.grid {\n  overflow: auto !important;\n}\n\n.disableScroll {\n  overflow-y: hidden !important;\n}\n\n.container {\n  position: relative;\n  height: 100%;\n  width: 100%;\n\n  &.isResizing {\n    user-select: none;\n    cursor: col-resize;\n  }\n}\n\n:global(.keys-tree__count) {\n  padding-left: 10px;\n}\n\n.table {\n  @include eui.scrollBar;\n  overflow-x: auto;\n  overflow-y: hidden;\n\n  &.autoHeight {\n    max-height: 100% !important;\n    height: auto !important;\n\n    display: flex;\n    flex-direction: column;\n\n    :global {\n      .ReactVirtualized__Table__headerRow {\n        flex-shrink: 0;\n      }\n\n      .ReactVirtualized__Table__Grid {\n        flex-grow: 1;\n        height: auto !important;\n      }\n    }\n  }\n\n  :global {\n    .ReactVirtualized__Table__headerRow {\n      cursor: initial !important;\n      border-bottom: 1px solid var(--euiColorLightShade);\n    }\n    .ReactVirtualized__Grid__innerScrollContainer {\n      & > div:hover {\n        background: var(--browserComponentActive);\n      }\n    }\n  }\n\n  .tableRowColumn {\n    margin: 0 !important;\n\n    &:global(.noPadding) {\n      .tableRowCell {\n        padding: 0 !important;\n      }\n    }\n  }\n\n  .tableRowCell {\n    display: flex;\n    align-items: center;\n    box-sizing: border-box;\n    padding: 8px 18px;\n    min-height: 43px;\n  }\n\n  .tableRow {\n    cursor: pointer;\n    border-top-width: 0;\n\n    & > div:first-of-type {\n      border-left: 3px solid transparent;\n    }\n  }\n\n  .tableRowEven {\n    background: var(--browserTableRowEven);\n\n    :global(.stream-entry-actions) {\n      background-color: var(--browserTableRowEven) !important;\n    }\n  }\n\n  :global(.table-row-selected) {\n    background: var(--browserComponentActive) !important;\n\n\n    & > div:first-of-type {\n      border-left: 3px solid transparent;\n      border-left-color: var(--euiColorPrimary) !important;\n    }\n  }\n\n  .tableRowSelectable {\n    cursor: pointer;\n  }\n}\n\n.headerColumn {\n  margin: 0 !important;\n}\n\n.headerCell {\n  overflow: hidden;\n  display: flex;\n  align-items: center;\n  padding: 8px;\n  min-height: $headerHeight;\n  text-transform: none;\n  white-space: nowrap;\n}\n\n.headerButton {\n  overflow: hidden;\n  display: flex;\n  align-items: center;\n  min-height: $headerHeight;\n  text-transform: none;\n  white-space: nowrap;\n  width: 100%;\n  &:hover,\n  &:focus {\n    text-decoration: underline;\n  }\n}\n\n.tableFooter {\n  width: 100%;\n  height: $footerHeight;\n  position: absolute;\n  bottom: -38px;\n  z-index: 1;\n  padding: 8px;\n  display: flex;\n  align-items: center;\n  border-top: 1px solid var(--euiColorLightShade);\n  background-color: var(--euiColorEmptyShade);\n  & > div {\n    min-width: 100px;\n    margin-right: 8px;\n  }\n}\n\n.placeholder {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 12px;\n  height: 100%;\n  width: 100%;\n  white-space: pre-wrap;\n}\n\n:global(.key-list-table) {\n  height: 100%;\n}\n\n:global(.key-details-table) {\n  height: calc(100% - 94px);\n  position: relative;\n\n  :global(.ReactVirtualized__Table__row) {\n    font-size: 13px;\n    align-items: normal;\n  }\n\n  :global(.ReactVirtualized__Table__headerRow) {\n    border: 1px solid var(--separatorColorLight) !important;\n  }\n\n  :global(.ReactVirtualized__Table__rowColumn) {\n    overflow: visible !important;\n\n    &:global(.actions.singleAction) {\n      .tableRowCell {\n        padding: 4px 8px !important;\n      }\n    }\n  }\n\n  :global(.ReactVirtualized__Table__Grid) {\n    border: 1px solid var(--separatorColor);\n    border-top: none;\n  }\n\n  &:not(&:global(.set-members-container)) {\n    .tableRow {\n      & > div:not(:last-of-type) {\n        border-right: 1px solid var(--separatorColor);\n      }\n\n      :global(.ReactVirtualized__Table__headerColumn) {\n        border-right-color: var(--separatorColorLight) !important;\n\n        &:last-child {\n          // fix border alignment, need to investigate why this happens\n          margin-left: -2px !important;\n        }\n      }\n    }\n  }\n\n  .headerCell {\n    padding: 18px 4px 18px 20px;\n  }\n\n  .tableRowColumn {\n    min-height: 100%;\n  }\n\n  .tableRowCell {\n    padding: 8px 6px 8px 20px !important;\n    min-height: 42px;\n\n    :global(.innerCellAsCell) {\n      display: flex;\n      max-width: 100%;\n      padding: 8px 6px 8px 20px !important;\n    }\n  }\n\n  .tableFooter {\n    display: none;\n  }\n\n  :global {\n    .value-table-actions {\n      .editFieldBtn {\n        margin-right: 10px;\n      }\n    }\n\n    .value-table-separate-border {\n      box-sizing: content-box;\n    }\n\n    .hidden {\n      display: none;\n    }\n  }\n}\n\n.loading {\n  opacity: 0;\n}\n\n.loadingShow {\n  opacity: 1;\n}\n\n.loading:after {\n  content: \" .\";\n  animation: dots 1s steps(5, end) infinite;\n}\n\n.resizeTrigger {\n  position: absolute;\n  height: 100%;\n  right: -4px;\n  width: 7px;\n\n  cursor: col-resize;\n  z-index: 2;\n\n  &:before {\n    content: '';\n    display: block;\n    width: 7px;\n    height: 8px;\n    border-left: 1px solid var(--tableLightestBorderColor);\n    border-right: 1px solid var(--tableLightestBorderColor);\n\n    position: absolute;\n    top: 50%;\n    transform: translateY(-50%);\n  }\n}\n\n@keyframes dots {\n  0%,\n  20% {\n    color: rgba(0, 0, 0, 0);\n    text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);\n  }\n  40% {\n    color: white;\n    text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);\n  }\n  60% {\n    text-shadow: 0.25em 0 0 white, 0.5em 0 0 rgba(0, 0, 0, 0);\n  }\n  80%,\n  100% {\n    text-shadow: 0.25em 0 0 white, 0.5em 0 0 white;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/virtual-table/utils.tsx",
    "content": "import React from 'react'\n\nexport const StopPropagation = ({ children }: { children: JSX.Element }) => (\n  <div\n    style={{ height: '100%', width: '100%', position: 'relative' }}\n    onClick={(e) => e.stopPropagation()}\n    role=\"presentation\"\n  >\n    {children}\n  </div>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/components/yaml-validator/index.ts",
    "content": "export * from './validatePipeline'\nexport * from './validateYamlSchema'\n"
  },
  {
    "path": "redisinsight/ui/src/components/yaml-validator/validatePipeline.test.ts",
    "content": "import { get } from 'lodash'\nimport { validatePipeline } from './validatePipeline'\nimport { validateYamlSchema, validateSchema } from './validateYamlSchema'\n\njest.mock('./validateYamlSchema')\n\ndescribe('validatePipeline', () => {\n  const mockSchema = {\n    config: {\n      type: 'object',\n      properties: {\n        name: { type: 'string' },\n      },\n      required: ['name'],\n    },\n    jobs: {\n      type: 'object',\n      properties: {\n        task: { type: 'string' },\n      },\n      required: ['task'],\n    },\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should return valid result when config and jobs are valid', () => {\n    ;(validateYamlSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n    ;(validateSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n\n    const result = validatePipeline({\n      config: 'name: valid-config',\n      schema: mockSchema,\n      monacoJobsSchema: null,\n      jobNameSchema: null,\n      jobs: [\n        { name: 'Job1', value: 'task: job1' },\n        { name: 'Job2', value: 'task: job2' },\n      ],\n    })\n\n    expect(result).toEqual({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {\n        Job1: [],\n        Job2: [],\n      },\n    })\n  })\n\n  it('should return invalid result when config is invalid', () => {\n    ;(validateYamlSchema as jest.Mock).mockImplementation((_, schema) =>\n      schema === get(mockSchema, 'config', null)\n        ? { valid: false, errors: [\"Missing required property 'name'\"] }\n        : { valid: true, errors: [] },\n    )\n    ;(validateSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n\n    const result = validatePipeline({\n      config: 'invalid-config-content',\n      schema: mockSchema,\n      monacoJobsSchema: null,\n      jobNameSchema: null,\n      jobs: [{ name: 'Job1', value: 'task: job1' }],\n    })\n\n    expect(result).toEqual({\n      result: false,\n      configValidationErrors: [\"Missing required property 'name'\"],\n      jobsValidationErrors: {\n        Job1: [],\n      },\n    })\n  })\n\n  it('should return invalid result when jobs are invalid', () => {\n    ;(validateYamlSchema as jest.Mock).mockImplementation((_, schema) =>\n      schema === null\n        ? { valid: false, errors: [\"Missing required property 'task'\"] }\n        : { valid: true, errors: [] },\n    )\n    ;(validateSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n\n    const result = validatePipeline({\n      config: 'name: valid-config',\n      schema: mockSchema,\n      monacoJobsSchema: null,\n      jobNameSchema: null,\n      jobs: [{ name: 'Job1', value: 'invalid-job-content' }],\n    })\n\n    expect(result).toEqual({\n      result: false,\n      configValidationErrors: [],\n      jobsValidationErrors: {\n        Job1: [\"Missing required property 'task'\"],\n      },\n    })\n  })\n\n  it('should return invalid result when both config and jobs are invalid', () => {\n    ;(validateYamlSchema as jest.Mock).mockImplementation((_, schema) => {\n      if (schema === get(mockSchema, 'config', null)) {\n        return { valid: false, errors: [\"Missing required property 'name'\"] }\n      }\n      if (schema === null) {\n        return { valid: false, errors: [\"Missing required property 'task'\"] }\n      }\n      return { valid: true, errors: [] }\n    })\n    ;(validateSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n\n    const result = validatePipeline({\n      config: 'invalid-config-content',\n      schema: mockSchema,\n      monacoJobsSchema: null,\n      jobNameSchema: null,\n      jobs: [{ name: 'Job1', value: 'invalid-job-content' }],\n    })\n\n    expect(result).toEqual({\n      result: false,\n      configValidationErrors: [\"Missing required property 'name'\"],\n      jobsValidationErrors: {\n        Job1: [\"Missing required property 'task'\"],\n      },\n    })\n  })\n\n  it('should filter duplicate errors per job', () => {\n    ;(validateYamlSchema as jest.Mock).mockImplementation(() => ({\n      valid: false,\n      errors: ['Duplicate error', 'Duplicate error'], // all the jobs get these errors\n    }))\n    ;(validateSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n\n    const result = validatePipeline({\n      config: 'invalid-config-content',\n      schema: mockSchema,\n      monacoJobsSchema: null,\n      jobNameSchema: null,\n      jobs: [\n        { name: 'Job1', value: 'invalid-job-content' },\n        { name: 'Job2', value: 'invalid-job-content' },\n      ],\n    })\n\n    expect(result).toEqual({\n      result: false,\n      configValidationErrors: ['Duplicate error'],\n      jobsValidationErrors: {\n        Job1: ['Duplicate error'],\n        Job2: ['Duplicate error'],\n      },\n    })\n  })\n\n  it('should return invalid result when job name validation fails', () => {\n    ;(validateYamlSchema as jest.Mock).mockImplementation(() => ({\n      valid: true,\n      errors: [],\n    }))\n    ;(validateSchema as jest.Mock).mockImplementation(() => ({\n      valid: false,\n      errors: ['Job name: Invalid job name'],\n    }))\n\n    const result = validatePipeline({\n      config: 'name: valid-config',\n      schema: mockSchema,\n      monacoJobsSchema: null,\n      jobNameSchema: { type: 'string', pattern: '^[a-zA-Z]+$' },\n      jobs: [{ name: 'Job-1', value: 'task: job1' }],\n    })\n\n    expect(result).toEqual({\n      result: false,\n      configValidationErrors: [],\n      jobsValidationErrors: {\n        'Job-1': ['Job name: Invalid job name'],\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/yaml-validator/validatePipeline.ts",
    "content": "import { get } from 'lodash'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  validateSchema,\n  validateYamlSchema,\n  VALID_RESULT,\n} from './validateYamlSchema'\n\ninterface PipelineValidationProps {\n  config: string\n  schema: any\n  monacoJobsSchema: Nullable<object>\n  jobNameSchema: Nullable<object>\n  jobs: { name: string; value: string }[]\n}\n\nexport const validatePipeline = ({\n  config,\n  schema,\n  monacoJobsSchema,\n  jobNameSchema,\n  jobs,\n}: PipelineValidationProps) => {\n  const { valid: isConfigValid, errors: configErrors } = validateYamlSchema(\n    config,\n    get(schema, 'config', null),\n  )\n\n  const { areJobsValid, jobsErrors } = jobs.reduce<{\n    areJobsValid: boolean\n    jobsErrors: Record<string, Set<string>>\n  }>(\n    (acc, j) => {\n      const validation = validateYamlSchema(j.value, monacoJobsSchema)\n      const jobNameValidation = !jobNameSchema\n        ? VALID_RESULT\n        : validateSchema(j.name, jobNameSchema, {\n            errorMessagePrefix: 'Job name',\n            includePathIntoErrorMessage: false,\n          })\n\n      if (!acc.jobsErrors[j.name]) {\n        acc.jobsErrors[j.name] = new Set()\n      }\n\n      if (!validation.valid) {\n        validation.errors.forEach((error) => acc.jobsErrors[j.name].add(error))\n      }\n\n      if (!jobNameValidation.valid) {\n        jobNameValidation.errors.forEach((error) =>\n          acc.jobsErrors[j.name].add(error),\n        )\n      }\n\n      acc.areJobsValid =\n        acc.areJobsValid && validation.valid && jobNameValidation.valid\n      return acc\n    },\n    { areJobsValid: true, jobsErrors: {} },\n  )\n\n  const result = isConfigValid && areJobsValid\n\n  return {\n    result,\n    configValidationErrors: [...new Set(configErrors)],\n    jobsValidationErrors: Object.fromEntries(\n      Object.entries(jobsErrors).map(([jobName, errors]) => [\n        jobName,\n        [...errors],\n      ]),\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/components/yaml-validator/validateYamlSchema.test.ts",
    "content": "import yaml from 'js-yaml'\nimport { validateYamlSchema, validateSchema } from './validateYamlSchema'\n\nconst schema = {\n  type: 'object',\n  properties: {\n    name: { type: 'string' },\n    version: { type: 'string', pattern: '^[0-9]+\\\\.[0-9]+\\\\.[0-9]+$' },\n  },\n  required: ['name', 'version'],\n}\n\ndescribe('validateYamlSchema', () => {\n  it('should return valid for correctly formatted YAML', () => {\n    const yamlContent = `\n      name: my-app\n      version: 1.0.0\n    `\n    expect(validateYamlSchema(yamlContent, schema)).toEqual({\n      valid: true,\n      errors: [],\n    })\n  })\n\n  it('should return error for missing required fields', () => {\n    const yamlContent = `\n      name: my-app\n    `\n    expect(validateYamlSchema(yamlContent, schema)).toEqual({\n      valid: false,\n      errors: expect.arrayContaining([\n        expect.stringContaining(\"must have required property 'version'\"),\n      ]),\n    })\n  })\n\n  it('should return error for invalid version format', () => {\n    const yamlContent = `\n      name: my-app\n      version: abc\n    `\n    expect(validateYamlSchema(yamlContent, schema)).toEqual({\n      valid: false,\n      errors: expect.arrayContaining([\n        expect.stringContaining('must match pattern'),\n      ]),\n    })\n  })\n\n  it('should return empty errors when schema is empty (accept all YAML)', () => {\n    const yamlContent = `\n      anyKey: anyValue\n    `\n    const emptySchema = {}\n    expect(validateYamlSchema(yamlContent, emptySchema)).toEqual({\n      valid: true,\n      errors: [],\n    })\n  })\n\n  it('should return YAML syntax error for invalid YAML format', () => {\n    const yamlContent = `\n      name: my-app\n      version\n    ` // Missing colon (:) for \"version\"\n    const result = validateYamlSchema(yamlContent, schema)\n\n    expect(result.valid).toBe(false)\n    expect(result.errors.length).toBeGreaterThan(0)\n    expect(result.errors[0]).toContain('Error:')\n  })\n\n  it('should handle YAML parsing errors gracefully', () => {\n    jest.spyOn(yaml, 'load').mockImplementation(() => {\n      throw new yaml.YAMLException('Simulated parsing error')\n    })\n\n    const result = validateYamlSchema('invalid yaml content', schema)\n\n    expect(result.valid).toBe(false)\n    expect(result.errors.length).toBeGreaterThan(0)\n    expect(result.errors[0]).toContain('Error:')\n\n    jest.restoreAllMocks()\n  })\n\n  it('should handle unknown errors gracefully', () => {\n    jest.spyOn(yaml, 'load').mockImplementation(() => {\n      throw new Error('Unexpected failure')\n    })\n\n    expect(validateYamlSchema('name: test', schema)).toEqual({\n      valid: false,\n      errors: ['Error: unknown error'],\n    })\n\n    jest.restoreAllMocks()\n  })\n})\n\ndescribe('validateSchema with ValidationConfig', () => {\n  const testSchema = {\n    type: 'object',\n    properties: {\n      name: { type: 'string' },\n      nested: {\n        type: 'object',\n        properties: {\n          value: { type: 'number' },\n        },\n        required: ['value'],\n      },\n    },\n    required: ['name'],\n  }\n\n  const invalidData = {\n    nested: {\n      value: 'not-a-number',\n    },\n    // missing required 'name' field\n  }\n\n  describe('default ValidationConfig', () => {\n    it('should use default error message prefix \"Error:\"', () => {\n      const result = validateSchema(invalidData, testSchema)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).toEqual(\n        expect.arrayContaining([expect.stringContaining('Error:')]),\n      )\n    })\n\n    it('should include path information by default', () => {\n      const result = validateSchema(invalidData, testSchema)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).toEqual(\n        expect.arrayContaining([\n          expect.stringContaining('(at root)'),\n          expect.stringContaining('(at /nested/value)'),\n        ]),\n      )\n    })\n  })\n\n  describe('custom ValidationConfig', () => {\n    it('should use custom error message prefix', () => {\n      const config = { errorMessagePrefix: 'Custom Prefix:' }\n      const result = validateSchema(invalidData, testSchema, config)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).toEqual(\n        expect.arrayContaining([expect.stringContaining('Custom Prefix:')]),\n      )\n      expect(result.errors).not.toEqual(\n        expect.arrayContaining([expect.stringContaining('Error:')]),\n      )\n    })\n\n    it('should exclude path information when includePathIntoErrorMessage is false', () => {\n      const config = { includePathIntoErrorMessage: false }\n      const result = validateSchema(invalidData, testSchema, config)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).not.toEqual(\n        expect.arrayContaining([expect.stringContaining('(at ')]),\n      )\n    })\n\n    it('should use both custom prefix and exclude path information', () => {\n      const config = {\n        errorMessagePrefix: 'Custom Error:',\n        includePathIntoErrorMessage: false,\n      }\n      const result = validateSchema(invalidData, testSchema, config)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).toEqual(\n        expect.arrayContaining([expect.stringContaining('Custom Error:')]),\n      )\n      expect(result.errors).not.toEqual(\n        expect.arrayContaining([expect.stringContaining('(at ')]),\n      )\n    })\n\n    it('should handle empty string as error message prefix', () => {\n      const config = { errorMessagePrefix: '' }\n      const result = validateSchema(invalidData, testSchema, config)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors.length).toBeGreaterThan(0)\n      // Should not start with \"Error:\" but with the actual error message\n      expect(result.errors[0]).not.toMatch(/^Error:/)\n    })\n  })\n\n  describe('ValidationConfig with exceptions', () => {\n    it('should use custom error prefix for unknown errors', () => {\n      const mockSchema = null // This will cause an error in AJV\n      const config = { errorMessagePrefix: 'Schema Error:' }\n\n      const result = validateSchema({}, mockSchema, config)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).toEqual(['Schema Error: unknown error'])\n    })\n\n    it('should use default error prefix for unknown errors when no config provided', () => {\n      const mockSchema = null // This will cause an error in AJV\n\n      const result = validateSchema({}, mockSchema)\n\n      expect(result.valid).toBe(false)\n      expect(result.errors).toEqual(['Error: unknown error'])\n    })\n  })\n\n  describe('edge cases', () => {\n    it('should handle valid data with custom config', () => {\n      const validData = { name: 'test', nested: { value: 42 } }\n      const config = {\n        errorMessagePrefix: 'Custom Error:',\n        includePathIntoErrorMessage: false,\n      }\n\n      const result = validateSchema(validData, testSchema, config)\n\n      expect(result).toEqual({\n        valid: true,\n        errors: [],\n      })\n    })\n\n    it('should handle undefined config properties gracefully', () => {\n      const config = {\n        errorMessagePrefix: undefined,\n        includePathIntoErrorMessage: undefined,\n      }\n      const result = validateSchema(invalidData, testSchema, config)\n\n      expect(result.valid).toBe(false)\n      // Should use defaults when undefined\n      expect(result.errors).toEqual(\n        expect.arrayContaining([\n          expect.stringContaining('Error:'),\n          expect.stringContaining('(at '),\n        ]),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/components/yaml-validator/validateYamlSchema.ts",
    "content": "import yaml, { YAMLException } from 'js-yaml'\nimport Ajv from 'ajv'\n\ntype ValidationConfig = {\n  errorMessagePrefix?: string\n  includePathIntoErrorMessage?: boolean\n}\n\ntype ValidationResult = {\n  valid: boolean\n  errors: string[]\n}\n\nexport const VALID_RESULT: ValidationResult = { valid: true, errors: [] }\n\nexport const validateSchema = (\n  parsed: any,\n  schema: any,\n  config: ValidationConfig = {},\n): ValidationResult => {\n  const errorMessagePrefix = config.errorMessagePrefix ?? 'Error:'\n  const includePathIntoErrorMessage = config.includePathIntoErrorMessage ?? true\n\n  try {\n    const ajv = new Ajv({\n      strict: false,\n      unicodeRegExp: false,\n      allErrors: true,\n    })\n\n    const validate = ajv.compile(schema)\n    const valid = validate(parsed)\n\n    if (!valid) {\n      const errors = validate.errors?.map((err) => {\n        const pathMessage = includePathIntoErrorMessage\n          ? ` (at ${err.instancePath || 'root'})`\n          : ''\n        return `${[errorMessagePrefix]} ${err.message}${pathMessage}`\n      })\n      return { valid: false, errors: errors || [] }\n    }\n\n    return { valid: true, errors: [] }\n  } catch (e) {\n    return { valid: false, errors: [`${errorMessagePrefix} unknown error`] }\n  }\n}\n\nexport const validateYamlSchema = (\n  content: string,\n  schema: any,\n): ValidationResult => {\n  try {\n    const parsed = yaml.load(content) as object\n\n    return validateSchema(parsed, schema)\n  } catch (e) {\n    if (e instanceof YAMLException) {\n      return { valid: false, errors: [`Error: ${e.reason}`] }\n    }\n    return { valid: false, errors: ['Error: unknown error'] }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/config/default.ts",
    "content": "import * as packageJson from '../../../package.json'\n\nconst intEnv = (envName: string, defaultValue: number): number => {\n  const value = parseInt(process?.env?.[envName] || '', 10)\n\n  return Number.isNaN(value) ? defaultValue : value\n}\n\nconst booleanEnv = (envName: string, defaultValue: boolean): boolean => {\n  const value = process?.env?.[envName]\n\n  if (value === undefined) {\n    return defaultValue\n  }\n\n  return ['true', '1'].includes(value || '')\n}\n\nconst apiUrl =\n  process.env.RI_SERVER_TLS_CERT && process.env.RI_SERVER_TLS_KEY\n    ? 'https://localhost'\n    : 'http://localhost'\n\nexport const defaultConfig = {\n  api: {\n    prefix: process.env.RI_API_PREFIX ?? 'api',\n    port: intEnv('RI_APP_PORT', 5540),\n    baseUrl: process.env.RI_BASE_API_URL ?? apiUrl,\n    hostedBaseUrl: process.env.RI_HOSTED_API_BASE_URL ?? '',\n    hostedBase: process.env.RI_HOSTED_BASE ?? '',\n    csrfEndpoint: process.env.RI_CSRF_ENDPOINT ?? '',\n    socketTransports: process.env.RI_SOCKET_TRANSPORTS,\n    socketCredentials: booleanEnv('RI_SOCKET_CREDENTIALS', false),\n    hostedSocketProxyPath: process.env.RI_HOSTED_SOCKET_PROXY_PATH,\n  },\n  database: {\n    defaultConnectionTimeout: intEnv('RI_CONNECTIONS_TIMEOUT_DEFAULT', 30_000),\n    defaultTimeoutToGetInfo: intEnv('RI_TIMEOUT_TO_GET_INFO', 5_000),\n    defaultTimeoutToGetRecommendations: intEnv(\n      'RI_TIMEOUT_TO_GET_RECOMMENDATIONS',\n      60_000,\n    ),\n    shouldGetRecommendations: booleanEnv(\n      'RI_SHOULD_GET_RECOMMENDATIONS',\n      false,\n    ),\n  },\n  app: {\n    version: packageJson.version,\n    sha: process.env.GITHUB_SHA,\n    env: process.env.NODE_ENV,\n    type: process.env.RI_APP_TYPE,\n    resourcesBaseUrl: process.env.RI_RESOURCES_BASE_URL ?? apiUrl, // todo: no usage found\n    unauthenticatedRedirect: process.env.RI_401_REDIRECT_URL ?? '',\n    smConsoleRedirect: process.env.RI_SM_REDIRECT_URL ?? '',\n    dbUpgradeRedirectBase: process.env.RI_DB_UPGRADE_REDIRECT_URL ?? '',\n    defaultTheme: process.env.RI_DEFAULT_THEME ?? 'SYSTEM',\n    lazyLoad: booleanEnv('RI_ROUTES_LAZY_LOAD', false),\n    routesExcludedByEnv: booleanEnv('RI_ROUTES_EXCLUDED_BY_ENV', false),\n    returnUrlBase: process.env.RI_RETURN_URL_BASE,\n    returnUrlLabel: process.env.RI_RETURN_URL_LABEL || 'Back',\n    returnUrlTooltip: process.env.RI_RETURN_URL_TOOLTIP || 'Back',\n    activityMonitorOrigin: process.env.RI_ACTIVITY_MONITOR_ORIGIN,\n    activityMonitorThrottleTimeout: intEnv(\n      'RI_ACTIVITY_MONITOR_THROTTLE_TIMEOUT',\n      30_000,\n    ),\n    sessionTtlSeconds: intEnv('RI_SESSION_TTL_SECONDS', 30 * 60),\n    localResourcesBaseUrl: process.env.RI_LOCAL_RESOURCES_BASE_URL,\n    useLocalResources: booleanEnv('RI_USE_LOCAL_RESOURCES', false),\n    indexedDbName: process.env.RI_INDEXED_DB_NAME || 'RI_LOCAL_STORAGE',\n    vectorSearchIndexedDbName:\n      process.env.RI_VECTOR_SEARCH_INDEXED_DB_NAME ||\n      'RI_VECTOR_SEARCH_STORAGE',\n    queryLibraryIndexedDbName:\n      process.env.RI_QUERY_LIBRARY_INDEXED_DB_NAME || 'RI_QUERY_LIBRARY',\n    truncatedStringPrefix:\n      process.env.RI_CLIENTS_TRUNCATED_STRING_PREFIX ||\n      '[Truncated due to length]',\n  },\n  workbench: {\n    pipelineCountDefault: intEnv('PIPELINE_COUNT_DEFAULT', 5),\n    maxResultSize: intEnv('RI_COMMAND_EXECUTION_MAX_RESULT_SIZE', 1024 * 1024),\n  },\n  browser: {\n    scanCountDefault: intEnv('RI_SCAN_COUNT_DEFAULT', 500),\n    scanTreeCountDefault: intEnv('RI_SCAN_TREE_COUNT_DEFAULT', 10_000),\n    databaseOverviewRefreshInterval: intEnv(\n      'RI_DATABASE_OVERVIEW_REFRESH_INTERVAL',\n      5,\n    ),\n    databaseOverviewMinimumRefreshInterval: intEnv(\n      'RI_DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL',\n      1,\n    ),\n    rejsonMonacoEditorMaxThreshold: intEnv(\n      'RI_REJSON_MONACO_EDITOR_MAX_THRESHOLD',\n      10_000,\n    ),\n  },\n  features: {\n    envDependent: {\n      defaultFlag: booleanEnv('RI_FEATURES_ENV_DEPENDENT_DEFAULT_FLAG', true),\n    },\n    cloudAds: {\n      defaultFlag: booleanEnv('RI_FEATURES_CLOUD_ADS_DEFAULT_FLAG', true),\n    },\n  },\n}\n\nexport type Config = typeof defaultConfig\n\ntype DeepPartial<T> = T extends object\n  ? {\n      [P in keyof T]?: DeepPartial<T[P]>\n    }\n  : T\nexport type PartialConfig = DeepPartial<Config>\n"
  },
  {
    "path": "redisinsight/ui/src/config/domain.ts",
    "content": "import { PartialConfig } from 'uiSrc/config/default'\n\ntype DomainConfigs = {\n  [key: string]: PartialConfig\n}\n\nconst config: DomainConfigs = {}\n\nconst domainConfig = config[window.location.host] || {}\n\nexport { domainConfig }\n"
  },
  {
    "path": "redisinsight/ui/src/config/index.ts",
    "content": "import { merge, cloneDeep } from 'lodash'\nimport { Config } from 'uiSrc/config/default'\nimport { domainConfig } from './domain'\n\nlet config: Config\n\nexport const getConfig = (): Config => {\n  if (config) {\n    return config\n  }\n\n  config = cloneDeep(riConfig)\n  merge(config, domainConfig)\n\n  return config\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/allRedisModules.json",
    "content": "[\n  {\n    \"name\": \"Disque\",\n    \"abbreviation\": \"DQ\",\n    \"license\": \"AGPL-3.0\",\n    \"repository\": \"https://github.com/antirez/disque-module\",\n    \"description\": \"Disque, an in-memory, distributed job queue, ported as Redis module.\",\n    \"authors\": [\"antirez\"],\n    \"stars\": 457\n  },\n  {\n    \"name\": \"RedisGears\",\n    \"abbreviation\": \"RG\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RedisGears/RedisGears\",\n    \"description\": \"Dynamic execution framework for your Redis data.\",\n    \"authors\": [\"MeirShpilraien\", \"RedisLabs\"],\n    \"stars\": 185\n  },\n  {\n    \"name\": \"redis-roaring\",\n    \"abbreviation\": \"RO\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/aviggiano/redis-roaring\",\n    \"description\": \"Uses the CRoaring library to implement roaring bitmap commands for Redis.\",\n    \"authors\": [\"aviggiano\"],\n    \"stars\": 130\n  },\n  {\n    \"name\": \"redis-cell\",\n    \"abbreviation\": \"CE\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/brandur/redis-cell\",\n    \"description\": \"A Redis module that provides rate limiting in Redis as a single command.\",\n    \"authors\": [\"brandur\"],\n    \"stars\": 651\n  },\n  {\n    \"name\": \"RedisGraph\",\n    \"abbreviation\": \"RG\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RedisGraph/RedisGraph\",\n    \"description\": \"A graph database with a Cypher-based querying language using sparse adjacency matrices\",\n    \"authors\": [\"swilly22\", \"RedisLabs\"],\n    \"stars\": 1409\n  },\n  {\n    \"name\": \"redis-tdigest\",\n    \"abbreviation\": \"TD\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/usmanm/redis-tdigest\",\n    \"description\": \"t-digest data structure wich can be used for accurate online accumulation of rank-based statistics such as quantiles and cumulative distribution at a point.\",\n    \"authors\": [\"usmanm\"],\n    \"stars\": 54\n  },\n  {\n    \"name\": \"RedisJSON\",\n    \"abbreviation\": \"RJ\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RedisJSON/RedisJSON\",\n    \"description\": \"A JSON data type for Redis\",\n    \"authors\": [\"itamarhaber\", \"RedisLabs\"],\n    \"stars\": 1358\n  },\n  {\n    \"name\": \"RediSearch\",\n    \"abbreviation\": \"RS\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RediSearch/RediSearch\",\n    \"description\": \"Full-Text search over Redis\",\n    \"authors\": [\"dvirsky\", \"RedisLabs\"],\n    \"stars\": 3051\n  },\n  {\n    \"name\": \"RedisBloom\",\n    \"abbreviation\": \"RB\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RedisBloom/RedisBloom\",\n    \"description\": \"Scalable Bloom filters\",\n    \"authors\": [\"mnunberg\", \"RedisLabs\"],\n    \"stars\": 960\n  },\n  {\n    \"name\": \"neural-redis\",\n    \"abbreviation\": \"NE\",\n    \"license\": \"BSD\",\n    \"repository\": \"https://github.com/antirez/neural-redis\",\n    \"description\": \"Online trainable neural networks as Redis data types.\",\n    \"authors\": [\"antirez\"],\n    \"stars\": 2160\n  },\n  {\n    \"name\": \"RedisTimeSeries\",\n    \"abbreviation\": \"RT\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RedisTimeSeries/RedisTimeSeries\",\n    \"description\": \"Time-series data structure for redis\",\n    \"authors\": [\"danni-m\", \"RedisLabs\"],\n    \"stars\": 593\n  },\n  {\n    \"name\": \"RedisAI\",\n    \"abbreviation\": \"RA\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/RedisAI/RedisAI\",\n    \"description\": \"A Redis module for serving tensors and executing deep learning graphs\",\n    \"authors\": [\"lantiga\", \"RedisLabs\"],\n    \"stars\": 604\n  },\n  {\n    \"name\": \"ReDe\",\n    \"abbreviation\": \"RD\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/TamarLabs/ReDe\",\n    \"description\": \"Low Latency timed queues (Dehydrators) as Redis data types.\",\n    \"authors\": [\"daTokenizer\"],\n    \"stars\": 36\n  },\n  {\n    \"name\": \"commentDis\",\n    \"abbreviation\": \"CD\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/picotera/commentDis\",\n    \"description\": \"Add comment syntax to your redis-cli scripts.\",\n    \"authors\": [\"daTokenizer\"],\n    \"stars\": 9\n  },\n  {\n    \"name\": \"redis-cuckoofilter\",\n    \"abbreviation\": \"CF\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/kristoff-it/redis-cuckoofilter\",\n    \"description\": \"Hashing-function agnostic Cuckoo filters.\",\n    \"authors\": [\"kristoff-it\"],\n    \"stars\": 123\n  },\n  {\n    \"name\": \"cthulhu\",\n    \"abbreviation\": \"CT\",\n    \"license\": \"BSD\",\n    \"repository\": \"https://github.com/sklivvz/cthulhu\",\n    \"description\": \"Extend Redis with JavaScript modules\",\n    \"authors\": [\"sklivvz\"],\n    \"stars\": 139\n  },\n  {\n    \"name\": \"Session Gate\",\n    \"abbreviation\": \"SG\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/f0rmiga/sessiongate\",\n    \"description\": \"Session management with multiple payloads using cryptographically signed tokens.\",\n    \"authors\": [\"f0rmiga\"],\n    \"stars\": 45\n  },\n  {\n    \"name\": \"rediSQL\",\n    \"abbreviation\": \"SQ\",\n    \"license\": \"AGPL-3.0\",\n    \"repository\": \"https://github.com/RedBeardLab/rediSQL\",\n    \"description\": \"A redis module that provides a full SQL capabilities embedding SQLite\",\n    \"authors\": [\"siscia\", \"RedBeardLab\"],\n    \"stars\": 1125\n  },\n  {\n    \"name\": \"TairHash\",\n    \"abbreviation\": \"TH\",\n    \"license\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/alibaba/TairHash\",\n    \"description\": \"A redis module, similar to redis hash, and you can set expire and version for the field\",\n    \"authors\": [\"Alibaba\"],\n    \"stars\": 37\n  },\n  {\n    \"name\": \"TairString\",\n    \"abbreviation\": \"TS\",\n    \"license\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/alibaba/TairString\",\n    \"description\": \"A redis module, similar to redis string, and support CAS/CAD operations\",\n    \"authors\": [\"Alibaba\"],\n    \"stars\": 28\n  },\n  {\n    \"name\": \"lqrm\",\n    \"abbreviation\": \"LQ\",\n    \"license\": \"BSD\",\n    \"repository\": \"https://github.com/halaei/lqrm\",\n    \"description\": \"A Laravel compatible queue driver for Redis that supports reliable blocking pop from FIFO and scheduled queues.\",\n    \"authors\": [\"halaei\"],\n    \"stars\": 4\n  },\n  {\n    \"name\": \"redis-rating\",\n    \"abbreviation\": \"RA\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/poga/redis-rating\",\n    \"description\": \"Estimate actual rating from postive/negative ratings\",\n    \"authors\": [\"devpoga\"],\n    \"stars\": 14\n  },\n  {\n    \"name\": \"smartcache\",\n    \"abbreviation\": \"SC\",\n    \"license\": \"AGPL-3.0\",\n    \"repository\": \"https://github.com/fcerbell/redismodule-smartcache\",\n    \"description\": \"A redis module that provides a pass-through cache\",\n    \"authors\": [\"fcerbell\"],\n    \"stars\": 5\n  },\n  {\n    \"name\": \"RedisPushIptables\",\n    \"abbreviation\": \"PT\",\n    \"license\": \"GPL-3.0\",\n    \"repository\": \"https://github.com/limithit/RedisPushIptables\",\n    \"description\": \"RedisPushIptables is used to update firewall rules to reject the IP addresses for a specified amount of time or forever reject.\",\n    \"authors\": [\"Gandalf\"],\n    \"stars\": 19\n  },\n  {\n    \"name\": \"redis-fpn\",\n    \"abbreviation\": \"FP\",\n    \"license\": \"Apache 2.0\",\n    \"repository\": \"https://github.com/infobip/redis-fpn\",\n    \"description\": \"Redis module for Fixed Point Number data type\",\n    \"authors\": [\"xxlabaza\"],\n    \"stars\": 8\n  },\n  {\n    \"name\": \"redis-percentile\",\n    \"abbreviation\": \"PC\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/poga/redis-percentile\",\n    \"description\": \"Redis module for efficient percentile estimation of streaming or distributed data with t-digest algorithm.\",\n    \"authors\": [\"devpoga\"],\n    \"stars\": 8\n  },\n  {\n    \"name\": \"redlock\",\n    \"abbreviation\": \"RL\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/wujunwei/redlock\",\n    \"description\": \"Redis module for distributed lock without using LUA script,safe unlock for different redis client.\",\n    \"authors\": [\"wujunwei\"],\n    \"stars\": 28\n  },\n  {\n    \"name\": \"redis-protobuf\",\n    \"abbreviation\": \"PB\",\n    \"license\": \"Apache-2.0\",\n    \"repository\": \"https://github.com/sewenew/redis-protobuf\",\n    \"description\": \"Redis module for reading and writing Protobuf messages\",\n    \"authors\": [\"sewenew\"],\n    \"stars\": 30\n  },\n  {\n    \"name\": \"redis-tree\",\n    \"abbreviation\": \"TR\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/OhBonsai/RedisTree\",\n    \"description\": \"Implements Polytree as a native data type. It allows creating,locating,pushing and detaching tree from Redis keys.\",\n    \"authors\": [\"ohbonsai\"],\n    \"stars\": 18\n  },\n  {\n    \"name\": \"Reventis\",\n    \"abbreviation\": \"RV\",\n    \"license\": \"Redis Source Available License\",\n    \"repository\": \"https://github.com/starkdg/reventis\",\n    \"description\": \"Redis module for storing and querying spatio-temporal event data\",\n    \"authors\": [\"starkdg\"],\n    \"stars\": 14\n  },\n  {\n    \"name\": \"redismodule-ratelimit\",\n    \"abbreviation\": \"RL\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/linfangrong/redismodule-ratelimit\",\n    \"description\": \"Redis module that provides ratelimit\",\n    \"authors\": [\"linfangrong\"],\n    \"stars\": 0\n  },\n  {\n    \"name\": \"dbx\",\n    \"abbreviation\": \"DB\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/cscan/dbx\",\n    \"description\": \"Redis module for maintaining hash by simple SQL\",\n    \"authors\": [\"cscan\"],\n    \"stars\": 0\n  },\n  {\n    \"name\": \"Redis-AudioScout\",\n    \"abbreviation\": \"AS\",\n    \"license\": \"pHash Source Available License\",\n    \"repository\": \"https://github.com/starkdg/Redis-AudioScout\",\n    \"description\": \"Redis module for Audio Track Recognition\",\n    \"authors\": [\"starkdg\"],\n    \"stars\": 1\n  },\n  {\n    \"name\": \"redis_hnsw\",\n    \"abbreviation\": \"HN\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/zhao-lang/redis_hnsw\",\n    \"description\": \"Redis module for Hierarchical Navigable Small World (HNSW) approxmiate nearest neighbor search\",\n    \"authors\": [\"zhao-lang\"],\n    \"stars\": 0\n  },\n  {\n    \"name\": \"Redis-ImageScout\",\n    \"abbreviation\": \"IS\",\n    \"license\": \"pHash Redis Source Available License\",\n    \"repository\": \"https://github.com/starkdg/Redis-ImageScout.git\",\n    \"description\": \"Redis module for Indexing of pHash Image fingerprints for Near-Duplicate Detection\",\n    \"authors\": [\"starkdg\"],\n    \"stars\": 2\n  },\n  {\n    \"name\": \"redex\",\n    \"abbreviation\": \"RX\",\n    \"license\": \"AGPL-3.0\",\n    \"repository\": \"https://github.com/RedisLabsModules/redex.git\",\n    \"description\": \"Extension modules to Redis' native data types and commands\",\n    \"authors\": [\"itamarhaber\"],\n    \"stars\": 52\n  },\n  {\n    \"name\": \"Redis Interval Sets\",\n    \"abbreviation\": \"IS\",\n    \"license\": \"BSD-3-Clause\",\n    \"repository\": \"https://github.com/danitseitlin/redis-interval-sets\",\n    \"description\": \"A Redis module for creating interval sets\",\n    \"authors\": [\"danitseitlin\"],\n    \"stars\": 3\n  },\n  {\n    \"name\": \"redicrypt\",\n    \"abbreviation\": \"CR\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/chayim/redicrypt\",\n    \"description\": \"Redis module for string encryption and decryption\",\n    \"authors\": [\"chayim\"],\n    \"stars\": 0\n  },\n  {\n    \"name\": \"redis-interval-module\",\n    \"abbreviation\": \"IM\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/ogama/redis-interval-module\",\n    \"description\": \"Redis module for creation and manipulation of interval set.\",\n    \"authors\": [\"ogama\"],\n    \"stars\": 3\n  },\n  {\n    \"name\": \"redisims\",\n    \"abbreviation\": \"SI\",\n    \"license\": \"MIT\",\n    \"repository\": \"https://github.com/Clement-Jean/RedisIMS\",\n    \"description\": \"A lightweight Redis module following the If Modified Since (IMS) pattern for caching\",\n    \"authors\": [\"Clement-Jean\"],\n    \"stars\": 0\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/api.ts",
    "content": "import { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\n\nenum ApiEndpoints {\n  DATABASES = 'databases',\n  DATABASES_IMPORT = 'databases/import',\n  DATABASES_TEST_CONNECTION = 'databases/test',\n  DATABASES_EXPORT = 'databases/export',\n\n  TAGS = 'tags',\n\n  BULK_ACTIONS_IMPORT = 'bulk-actions/import',\n  BULK_ACTIONS_IMPORT_DEFAULT_DATA = 'bulk-actions/import/default-data',\n  BULK_ACTIONS_IMPORT_TUTORIAL_DATA = 'bulk-actions/import/tutorial-data',\n  BULK_ACTIONS_IMPORT_VECTOR_COLLECTION = 'bulk-actions/import/vector-collection',\n\n  CA_CERTIFICATES = 'certificates/ca',\n  CLIENT_CERTIFICATES = 'certificates/client',\n\n  REDIS_CLUSTER_GET_DATABASES = 'redis-enterprise/cluster/get-databases',\n  REDIS_CLUSTER_DATABASES = 'redis-enterprise/cluster/databases',\n\n  REDIS_CLOUD_ACCOUNT = 'cloud/autodiscovery/account',\n  REDIS_CLOUD_SUBSCRIPTIONS = 'cloud/autodiscovery/subscriptions',\n  REDIS_CLOUD_GET_DATABASES = 'cloud/autodiscovery/get-databases',\n  REDIS_CLOUD_DATABASES = 'cloud/autodiscovery/databases',\n\n  SENTINEL_GET_DATABASES = 'redis-sentinel/get-databases',\n  SENTINEL_DATABASES = 'redis-sentinel/databases',\n\n  KEYS = 'keys',\n  KEYS_METADATA = 'keys/get-metadata',\n  KEY_INFO = 'keys/get-info',\n  KEY_NAME = 'keys/name',\n  KEY_TTL = 'keys/ttl',\n  KEYS_NAMESPACE_SEARCHABLE = 'keys/get-namespace-searchable',\n\n  ZSET = 'zSet',\n  ZSET_MEMBERS = 'zSet/members',\n  ZSET_MEMBERS_SEARCH = 'zSet/search',\n  ZSET_GET_MEMBERS = 'zSet/get-members',\n\n  SET = 'set',\n  SET_GET_MEMBERS = 'set/get-members',\n  SET_MEMBERS = 'set/members',\n\n  STRING = 'string',\n  STRING_VALUE = 'string/get-value',\n  STRING_VALUE_DOWNLOAD = 'string/download-value',\n\n  HASH = 'hash',\n  HASH_FIELDS = 'hash/fields',\n  HASH_GET_FIELDS = 'hash/get-fields',\n  HASH_TTL = 'hash/ttl',\n\n  LIST = 'list',\n  LIST_GET_ELEMENTS = 'list/get-elements',\n  LIST_DELETE_ELEMENTS = 'list/elements',\n\n  REJSON = 'rejson-rl',\n  REJSON_GET = 'rejson-rl/get',\n  REJSON_SET = 'rejson-rl/set',\n  REJSON_ARRAPPEND = 'rejson-rl/arrappend',\n\n  STREAMS = 'streams',\n  STREAMS_ENTRIES = 'streams/entries',\n  STREAMS_ENTRIES_GET = 'streams/entries/get',\n  STREAMS_CONSUMER_GROUPS = 'streams/consumer-groups',\n  STREAMS_CONSUMERS = 'streams/consumer-groups/consumers',\n  STREAMS_CONSUMER_GROUPS_GET = 'streams/consumer-groups/get',\n  STREAMS_CONSUMERS_GET = 'streams/consumer-groups/consumers/get',\n  STREAMS_CONSUMERS_MESSAGES_GET = 'streams/consumer-groups/consumers/pending-messages/get',\n  STREAM_CLAIM_PENDING_MESSAGES = 'streams/consumer-groups/consumers/pending-messages/claim',\n  STREAM_ACK_PENDING_ENTRIES = 'streams/consumer-groups/consumers/pending-messages/ack',\n\n  INFO = 'info',\n  CLI_BLOCKING_COMMANDS = 'info/cli-blocking-commands',\n  CLI_UNSUPPORTED_COMMANDS = 'info/cli-unsupported-commands',\n\n  CLI = 'cli',\n  SEND_COMMAND = 'send-command',\n  SEND_CLUSTER_COMMAND = 'send-cluster-command',\n\n  COMMAND_EXECUTIONS = 'command-executions',\n\n  SETTINGS = 'settings',\n  SETTINGS_AGREEMENTS_SPEC = 'settings/agreements/spec',\n\n  WORKBENCH_COMMAND_EXECUTIONS = 'workbench/command-executions',\n\n  PROFILER = 'profiler',\n  PROFILER_LOGS = 'profiler/logs',\n\n  REDIS_COMMANDS = 'commands',\n  GUIDES = 'static/guides/manifest.json',\n  TUTORIALS = 'static/tutorials/manifest.json',\n  CUSTOM_TUTORIALS = 'custom-tutorials',\n  CUSTOM_TUTORIALS_MANIFEST = 'custom-tutorials/manifest',\n  PLUGINS = 'plugins',\n  STATE = 'state',\n  CONTENT_CREATE_DATABASE = 'static/content/create-redis.json',\n  CONTENT_RECOMMENDATIONS = 'static/content/recommendations.json',\n  CONTENT_GUIDE_LINKS = 'static/content/guide-links.json',\n  GUIDES_PATH = 'static/guides',\n  TUTORIALS_PATH = 'static/tutorials',\n  CUSTOM_TUTORIALS_PATH = 'static/custom-tutorials',\n\n  SLOW_LOGS = 'slow-logs',\n  SLOW_LOGS_CONFIG = 'slow-logs/config',\n\n  PUB_SUB = 'pub-sub',\n  PUB_SUB_MESSAGES = 'pub-sub/messages',\n  CLUSTER_DETAILS = 'cluster-details',\n  DATABASE_ANALYSIS = 'analysis',\n  RECOMMENDATIONS = 'recommendations',\n  RECOMMENDATIONS_READ = 'recommendations/read',\n\n  NOTIFICATIONS = 'notifications',\n  NOTIFICATIONS_READ = 'notifications/read',\n\n  REDISEARCH = 'redisearch',\n  REDISEARCH_SEARCH = 'redisearch/search',\n  REDISEARCH_INFO = 'redisearch/info',\n  REDISEARCH_KEY_INDEXES = 'redisearch/key-indexes',\n\n  QUERY_LIBRARY = 'query-library',\n  QUERY_LIBRARY_SEED = 'query-library/seed',\n  HISTORY = 'history',\n\n  FEATURES = 'features',\n\n  CLOUD_ME = 'cloud/me',\n  CLOUD_ME_JOBS = 'cloud/me/jobs',\n  CLOUD_ME_ACCOUNTS = 'cloud/me/accounts',\n  CLOUD_ME_LOGOUT = 'cloud/me/logout',\n  CLOUD_CURRENT = 'current',\n\n  CLOUD_SUBSCRIPTION_PLANS = 'cloud/me/subscription/plans',\n\n  CLOUD_ME_AUTODISCOVERY_ACCOUNT = 'cloud/me/autodiscovery/account',\n  CLOUD_ME_AUTODISCOVERY_SUBSCRIPTIONS = 'cloud/me/autodiscovery/subscriptions',\n  CLOUD_ME_AUTODISCOVERY_GET_DATABASES = 'cloud/me/autodiscovery/get-databases',\n  CLOUD_ME_AUTODISCOVERY_DATABASES = 'cloud/me/autodiscovery/databases',\n  CLOUD_CAPI_KEYS = 'cloud/me/capi-keys',\n\n  AI_ASSISTANT_CHATS = 'ai/assistant/chats',\n  AI_EXPERT = 'ai/expert',\n\n  ANALYTICS_SEND_EVENT = 'analytics/send-event',\n  ANALYTICS_SEND_PAGE = 'analytics/send-page',\n\n  AZURE_AUTH_LOGIN = 'azure/auth/login',\n  AZURE_SUBSCRIPTIONS = 'azure/subscriptions',\n  AZURE_AUTODISCOVERY_DATABASES = 'azure/autodiscovery/databases',\n\n  RDI_INSTANCES = 'rdi',\n  RDI_PIPELINE = 'pipeline',\n  RDI_PIPELINE_SCHEMA = 'pipeline/schema',\n  RDI_DEPLOY_PIPELINE = 'pipeline/deploy',\n  RDI_TEST_CONNECTIONS = 'pipeline/test-connections',\n  RDI_PIPELINE_STRATEGIES = 'pipeline/strategies',\n  RDI_JOB_TEMPLATE = 'pipeline/job/template',\n  RDI_CONFIG_TEMPLATE = 'pipeline/config/template',\n  RDI_PIPELINE_JOB_FUNCTIONS = 'pipeline/job-functions',\n  RDI_STATISTICS = 'statistics',\n  RDI_PIPELINE_STATUS = 'pipeline/status',\n  RDI_PIPELINE_STOP = 'pipeline/stop',\n  RDI_PIPELINE_START = 'pipeline/start',\n  RDI_PIPELINE_RESET = 'pipeline/reset',\n}\n\nexport enum CustomHeaders {\n  DbIndex = 'ri-db-index',\n  WindowId = 'x-window-id',\n  CsrfToken = 'X-CSRF-Token',\n}\n\nexport const DEFAULT_SEARCH_MATCH = '*'\n\nexport const PIPELINE_COUNT_DEFAULT = riConfig.workbench.pipelineCountDefault\nexport const SCAN_COUNT_DEFAULT = riConfig.browser.scanCountDefault\nexport const SCAN_TREE_COUNT_DEFAULT = riConfig.browser.scanTreeCountDefault\nexport const SCAN_STREAM_START_DEFAULT = '-'\nexport const SCAN_STREAM_END_DEFAULT = '+'\n\nexport const CLOUD_AUTH_API_ENDPOINTS = [\n  ApiEndpoints.CLOUD_ME,\n  ApiEndpoints.CLOUD_ME_JOBS,\n  ApiEndpoints.CLOUD_ME_ACCOUNTS,\n  ApiEndpoints.CLOUD_SUBSCRIPTION_PLANS,\n  ApiEndpoints.CLOUD_ME_AUTODISCOVERY_ACCOUNT,\n  ApiEndpoints.CLOUD_ME_AUTODISCOVERY_SUBSCRIPTIONS,\n  ApiEndpoints.CLOUD_ME_AUTODISCOVERY_GET_DATABASES,\n  ApiEndpoints.CLOUD_ME_AUTODISCOVERY_DATABASES,\n  ApiEndpoints.CLOUD_CAPI_KEYS,\n]\n\nexport default ApiEndpoints\n"
  },
  {
    "path": "redisinsight/ui/src/constants/apiErrors.ts",
    "content": "import { secondsToMinutes } from 'uiSrc/utils/transformers/formatDate'\n\nenum ApiErrors {\n  SentinelParamsRequired = 'SENTINEL_PARAMS_REQUIRED',\n  KeytarUnavailable = 'KeytarUnavailable',\n  KeytarEncryption = 'KeytarEncryptionError',\n  KeytarDecryption = 'KeytarDecryptionError',\n  ClientNotFound = 'ClientNotFoundError',\n  RedisearchIndexNotFound = 'no such index',\n  ConnectionLost = 'The connection to the server has been lost.',\n}\n\nexport const ApiEncryptionErrors: string[] = [\n  ApiErrors.KeytarUnavailable,\n  ApiErrors.KeytarEncryption,\n  ApiErrors.KeytarDecryption,\n]\n\nexport const AI_CHAT_ERRORS = {\n  default: () => 'An error occurred. Try again or restart the session.',\n  unexpected: () => 'An unexpected error occurred. Try again later.',\n  timeout: () => 'Timeout occurred. Try again later.',\n  rateLimit: (limit?: number) => {\n    let error = 'Exceeded rate limit.'\n    if (limit) {\n      error += ` Try again in ${secondsToMinutes(limit)}.`\n    }\n\n    return error\n  },\n  tokenLimit: () => 'Conversation is too long. Restart the session.',\n}\n\nexport default ApiErrors\n"
  },
  {
    "path": "redisinsight/ui/src/constants/apiStatusCode.ts",
    "content": "enum ApiStatusCode {\n  Unauthorized = 401,\n  BadRequest = 400,\n  Forbidden = 403,\n  Timeout = 408,\n}\n\nexport default ApiStatusCode\n"
  },
  {
    "path": "redisinsight/ui/src/constants/breadcrumbs.ts",
    "content": "const RedisDatabases = {\n  text: 'Redis Databases',\n  href: '/',\n}\n\nexport interface BrowserPageOptions {\n  connectedInstanceName: string\n  postfix?: string\n  connection: string\n  version: string\n  user: string\n}\n\nexport const BreadcrumbsLinks = {\n  BrowserPage: ({\n    connectedInstanceName,\n    connection,\n    version,\n    user,\n    postfix = '',\n  }: BrowserPageOptions) => [\n    { ...RedisDatabases },\n    {\n      postfix,\n      text: connectedInstanceName,\n      tooltipOptions: [\n        {\n          label: 'Database Name',\n          value: connectedInstanceName + (postfix ? ` ${postfix}` : ''),\n        },\n        {\n          label: 'Connection',\n          value: connection,\n        },\n        {\n          label: 'Version',\n          value: version,\n        },\n        {\n          label: 'Username',\n          value: user,\n        },\n      ],\n    },\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/browser/keyDetailsHeader.ts",
    "content": "const PADDING_WRAPPER_SIZE = 36\nexport const HIDE_LAST_REFRESH = 850 - PADDING_WRAPPER_SIZE\nexport const MIDDLE_SCREEN_RESOLUTION = 740 - PADDING_WRAPPER_SIZE\n"
  },
  {
    "path": "redisinsight/ui/src/constants/browser.ts",
    "content": "import { EuiComboBoxOptionOption } from '@elastic/eui'\nimport { KeyValueFormat, SortOrder } from './keys'\n\nexport const DEFAULT_DELIMITER: EuiComboBoxOptionOption = {\n  label: ':',\n  value: ':',\n}\nexport const DEFAULT_TREE_SORTING = SortOrder.ASC\nexport const DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS = false\n\nexport const TEXT_UNPRINTABLE_CHARACTERS = {\n  title: 'Non-printable characters have been detected',\n  content: 'Use Workbench or CLI to edit without data loss.',\n}\nexport const TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA =\n  'This action is disabled because the key or value is too large to process within Redis Insight.'\nexport const AXIOS_ERROR_DISABLED_ACTION_WITH_TRUNCATED_DATA = {\n  response: {\n    data: {\n      message: TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n    },\n  },\n}\nexport const TEXT_CONSUMER_GROUP_NAME_TOO_LONG =\n  'The consumer group name is too long, details cannot be displayed.'\nexport const TEXT_CONSUMER_NAME_TOO_LONG =\n  'The consumer name is too long, details cannot be displayed.'\nexport const TEXT_DISABLED_FORMATTER_EDITING =\n  'Cannot edit the value in this format'\nexport const TEXT_DISABLED_STRING_EDITING = 'Load the entire value to edit it'\nexport const TEXT_DISABLED_STRING_FORMATTING =\n  'Load the entire value to select a format'\n\nexport const TEXT_INVALID_VALUE = {\n  title: 'Value will be saved as Unicode',\n  text: 'as it is not valid in the selected format.',\n}\n\nexport const TEXT_DISABLED_COMPRESSED_VALUE: string =\n  'Cannot edit the decompressed value'\n\nexport const TEXT_FAILED_CONVENT_FORMATTER = (format: KeyValueFormat) =>\n  `Failed to convert to ${format}`\n\nexport const TEXT_BULK_DELETE_TOOLTIP = (pattern: string) =>\n  `Delete all keys matching: ${pattern}`\nexport const TEXT_BULK_DELETE_DISABLED_UNPRINTABLE =\n  'Non-printable characters detected. Bulk delete disabled due to unreliable key grouping.'\nexport const TEXT_BULK_DELETE_DISABLED_MULTIPLE_DELIMITERS =\n  'To use bulk delete, configure tree view with one delimiter.'\n\nexport const DATABASE_OVERVIEW_REFRESH_INTERVAL =\n  riConfig.browser.databaseOverviewRefreshInterval\nexport const DATABASE_OVERVIEW_MINIMUM_REFRESH_INTERVAL =\n  riConfig.browser.databaseOverviewMinimumRefreshInterval\n\nexport enum BrowserColumns {\n  Size = 'size',\n  TTL = 'ttl',\n}\n\nexport const DEFAULT_SHOWN_COLUMNS = [BrowserColumns.Size, BrowserColumns.TTL]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/bulkActions.ts",
    "content": "export enum BulkActionsServerEvent {\n  Create = 'create',\n  Get = 'get',\n  Abort = 'abort',\n  Overview = 'overview',\n  Error = 'error',\n}\n\nexport enum BulkActionsType {\n  Delete = 'delete',\n  Upload = 'upload',\n  Unlink = 'unlink',\n}\n\nexport enum BulkActionsStatus {\n  Initializing = 'initializing',\n  Initialized = 'initialized',\n  Preparing = 'preparing',\n  Ready = 'ready',\n  Running = 'running',\n  Completed = 'completed',\n  Failed = 'failed',\n  Aborted = 'aborted',\n  Disconnected = 'disconnected',\n}\n\nexport const MAX_BULK_ACTION_ERRORS_LENGTH = 500\n"
  },
  {
    "path": "redisinsight/ui/src/constants/cliOutput.ts",
    "content": "export const ClearCommand = 'clear'\nexport const SelectCommand = 'select'\n\nexport enum CliOutputFormatterType {\n  Text = 'TEXT',\n  Raw = 'RAW',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/commands.ts",
    "content": "export interface ICommands {\n  [key: string]: ICommand\n}\n\nexport interface ICommand {\n  name?: string\n  summary: string\n  complexity?: string\n  arguments?: ICommandArg[]\n  since: string\n  group: CommandGroup | string\n  provider?: string\n}\n\nexport enum CommandProvider {\n  Main = 'main',\n  Unknown = 'unknown',\n}\n\nexport interface ICommandArg {\n  name?: string[] | string\n  type?: CommandArgsType[] | CommandArgsType | string | string[]\n  optional?: boolean\n  enum?: string[]\n  block?: ICommandArg[]\n  command?: string\n  multiple?: boolean\n  variadic?: boolean\n  dsl?: string\n}\n\nexport enum ICommandTokenType {\n  PureToken = 'pure-token',\n  Block = 'block',\n  OneOf = 'oneof',\n  String = 'string',\n  Double = 'double',\n  Enum = 'enum',\n  Integer = 'integer',\n  Key = 'key',\n  POSIXTime = 'posix time',\n  Pattern = 'pattern',\n}\n\nexport interface IRedisCommand {\n  name?: string\n  summary?: string\n  expression?: boolean\n  type?: ICommandTokenType\n  token?: string\n  optional?: boolean\n  multiple?: boolean\n  arguments?: IRedisCommand[]\n  variadic?: boolean\n  dsl?: string\n}\n\nexport interface IRedisCommandTree extends IRedisCommand {\n  parent?: IRedisCommandTree\n}\n\nexport interface ICommandArgGenerated extends ICommandArg {\n  generatedName?: string | string[]\n}\n\nexport enum CommandArgsType {\n  Block = 'block',\n  Double = 'double',\n  Enum = 'enum',\n  Integer = 'integer',\n  Key = 'key',\n  POSIXTime = 'posix time',\n  Pattern = 'pattern',\n  String = 'string',\n  Type = 'type',\n}\n\nexport enum CommandGroup {\n  Cluster = 'cluster',\n  Connection = 'connection',\n  Geo = 'geo',\n  Bitmap = 'bitmap',\n  Generic = 'generic',\n  PubSub = 'pubsub',\n  Scripting = 'scripting',\n  Transactions = 'transactions',\n  Server = 'server',\n  SortedSet = 'sorted-set',\n  HyperLogLog = 'hyperloglog',\n  Hash = 'hash',\n  Set = 'set',\n  Stream = 'stream',\n  List = 'list',\n  String = 'string',\n  Search = 'search',\n  JSON = 'json',\n  TimeSeries = 'timeseries',\n  Graph = 'graph',\n  AI = 'ai',\n  TDigest = 'tdigest',\n  CMS = 'cms',\n  TopK = 'topk',\n  BloomFilter = 'bf',\n  CuckooFilter = 'cf',\n}\n\nexport enum CommandPrefix {\n  AI = 'AI',\n  Graph = 'GRAPH',\n  TimeSeries = 'TS',\n  Search = 'FT',\n  JSON = 'JSON',\n  Gears = 'RG',\n  BloomFilter = 'BF',\n  CuckooFilter = 'CF',\n  CountMinSketchFilter = 'CMS',\n  TopK = 'TOPK',\n}\n\nexport const CommandMonitor = 'MONITOR'\nexport const CommandPSubscribe = 'PSUBSCRIBE'\nexport const CommandSubscribe = 'SUBSCRIBE'\nexport const CommandHello3 = 'HELLO 3'\n\nexport enum CommandRediSearch {\n  Search = 'FT.SEARCH',\n  Aggregate = 'FT.AGGREGATE',\n  Info = 'FT.INFO',\n}\n\nexport const commandsWBTableView = [\n  CommandRediSearch.Search,\n  CommandRediSearch.Aggregate,\n]\nexport const commandsWBTablePartView = [CommandRediSearch.Info]\n\nexport enum CommandRSSearchArgument {\n  NoContent = 'NOCONTENT',\n  Return = 'RETURN',\n  Highlight = 'HIGHLIGHT',\n  WithScores = 'WITHSCORES',\n  WithPayloads = 'WITHPAYLOADS',\n  WithSortKeys = 'WITHSORTKEYS',\n}\n\nexport enum DSL {\n  cypher = 'cypher',\n  lua = 'lua',\n  sqliteFunctions = 'sqliteFunctions',\n  jmespath = 'jmespath',\n}\n\nexport interface IDSLNaming {\n  [key: string]: string\n}\n\nexport const DSLNaming: IDSLNaming = {\n  [DSL.cypher]: 'Cypher',\n  [DSL.lua]: 'Lua',\n  [DSL.sqliteFunctions]: 'SQLite functions',\n  [DSL.jmespath]: 'JMESPath',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/commandsVersions.ts",
    "content": "export const CommandsVersions = {\n  REMOVE_MULTIPLE_LIST_ELEMENTS: {\n    since: '6.2',\n  },\n  FILTER_PER_KEY_TYPES: {\n    since: '6.0',\n  },\n  SPUBLISH_NOT_SUPPORTED: {\n    since: '7.0',\n  },\n  HASH_TTL: {\n    since: '7.3',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/customErrorCodes.ts",
    "content": "export enum CustomErrorCodes {\n  // General [10000, 10899]\n  WindowUnauthorized = 10_001,\n\n  // Redis Connection [10900, 10999]\n  RedisConnectionFailed = 10_900,\n  RedisConnectionTimeout = 10_901,\n  RedisConnectionUnauthorized = 10_902,\n  RedisConnectionClusterNodesUnavailable = 10_903,\n  RedisConnectionUnavailable = 10_904,\n  RedisConnectionAuthUnsupported = 10_905,\n  RedisConnectionSentinelMasterRequired = 10_906,\n  RedisConnectionIncorrectCertificate = 10_907,\n  RedisConnectionDefaultUserDisabled = 10_908,\n\n  // Cloud API [11001, 11099]\n  CloudApiInternalServerError = 11_000,\n  CloudApiUnauthorized = 11_001,\n  CloudApiForbidden = 11_002,\n  CloudApiBadRequest = 11_003,\n  CloudApiNotFound = 11_004,\n  CloudOauthMisconfiguration = 11_005,\n  CloudOauthGithubEmailPermission = 11_006,\n  CloudOauthUnknownAuthorizationRequest = 11_007,\n  CloudOauthUnexpectedError = 11_008,\n  CloudOauthMissedRequiredData = 11_009,\n  CloudOauthCanceled = 11_010,\n  CloudOauthSsoUnsupportedEmail = 11_011,\n  CloudCapiUnauthorized = 11_021,\n  CloudCapiKeyUnauthorized = 11_022,\n  CloudCapiKeyNotFound = 11_023,\n  AzureEntraIdTokenExpired = 11_024,\n\n  // Cloud Job errors [11100, 11199]\n  CloudJobUnexpectedError = 11_100,\n  CloudJobAborted = 11_101,\n  CloudJobUnsupported = 11_102,\n  CloudTaskProcessingError = 11_103,\n  CloudTaskNoResourceId = 11_104,\n  CloudSubscriptionIsInTheFailedState = 11_105,\n  CloudSubscriptionIsInUnexpectedState = 11_106,\n  CloudDatabaseIsInTheFailedState = 11_107,\n  CloudDatabaseAlreadyExistsFree = 11_108,\n  CloudDatabaseIsInUnexpectedState = 11_109,\n  CloudPlanUnableToFindFree = 11_110,\n  CloudSubscriptionUnableToDetermine = 11_111,\n  CloudTaskNotFound = 11_112,\n  CloudJobNotFound = 11_113,\n  CloudSubscriptionAlreadyExistsFree = 11_114,\n  CloudDatabaseImportForbidden = 11_115,\n  CloudDatabaseEndpointInvalid = 11_116,\n\n  // General database errors [11200, 11299]\n  DatabaseAlreadyExists = 11_200,\n\n  // AI errors [11300, 11399]\n  ConvAiInternalServerError = 11_300,\n  ConvAiUnauthorized = 11_301,\n  ConvAiForbidden = 11_302,\n  ConvAiBadRequest = 11_303,\n  ConvAiNotFound = 11_304,\n\n  QueryAiInternalServerError = 11_351,\n  QueryAiUnauthorized = 11_351,\n  QueryAiForbidden = 11_352,\n  QueryAiBadRequest = 11_353,\n  QueryAiNotFound = 11_354,\n  QueryAiRateLimitRequest = 11_360,\n  QueryAiRateLimitToken = 11_361,\n  QueryAiRateLimitMaxTokens = 11_362,\n\n  // RDI errors [11400, 11599]\n  RdiDeployPipelineFailure = 11_401,\n  RdiUnauthorized = 11_402,\n  RdiInternalServerError = 11_403,\n  RdiValidationError = 11_404,\n  RdiNotFound = 11_405,\n  RdiForbidden = 11_406,\n  RdiResetPipelineFailure = 11_407,\n  RdiStartPipelineFailure = 11_408,\n  RdiStopPipelineFailure = 11_409,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/databaseList.ts",
    "content": "export enum DatabaseListColumn {\n  Name = 'name',\n  Host = 'host',\n  ConnectionType = 'connectionType',\n  Modules = 'modules',\n  LastConnection = 'lastConnection',\n  Tags = 'tags',\n  Controls = 'controls',\n}\n\nexport const COLUMN_FIELD_NAME_MAP = new Map<DatabaseListColumn, string>([\n  [DatabaseListColumn.Name, 'Database Alias'],\n  [DatabaseListColumn.Host, 'Host:Port'],\n  [DatabaseListColumn.ConnectionType, 'Connection Type'],\n  [DatabaseListColumn.Modules, 'Capabilities'],\n  [DatabaseListColumn.LastConnection, 'Last connection'],\n  [DatabaseListColumn.Tags, 'Tags'],\n  [DatabaseListColumn.Controls, 'Controls'],\n])\n"
  },
  {
    "path": "redisinsight/ui/src/constants/datetime.ts",
    "content": "import { DATETIME_FORMATTER_DEFAULT } from './keys'\n\nexport const dateTimeOptions = [\n  {\n    inputDisplay: DATETIME_FORMATTER_DEFAULT,\n    value: DATETIME_FORMATTER_DEFAULT,\n  },\n  { inputDisplay: 'yyyy-MM-dd HH:mm:ss.sss', value: 'yyyy-MM-dd HH:mm:ss.sss' },\n  {\n    inputDisplay: 'dd-MMM-yyyy HH:mm:ss.SSS',\n    value: 'dd-MMM-yyyy HH:mm:ss.SSS',\n  },\n  { inputDisplay: 'dd.MM.yyyy HH:mm:ss', value: 'dd.MM.yyyy HH:mm:ss' },\n]\n\nexport enum TimezoneOption {\n  Local = 'local',\n  UTC = 'UTC',\n}\n\nexport const timezoneOptions = [\n  { inputDisplay: 'Match System', value: TimezoneOption.Local },\n  { inputDisplay: 'UTC', value: TimezoneOption.UTC },\n]\n\nexport enum DatetimeRadioOption {\n  Common = 'common',\n  Custom = 'custom',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/durationUnits.tsx",
    "content": "export enum DurationUnits {\n  microSeconds = 'µs',\n  milliSeconds = 'ms',\n  mSeconds = 'msec',\n}\n\nexport const DURATION_UNITS = [\n  {\n    inputDisplay: DurationUnits.microSeconds,\n    value: DurationUnits.microSeconds,\n    'data-test-subj': 'unit-micro-second',\n  },\n  {\n    inputDisplay: 'msec',\n    value: DurationUnits.milliSeconds,\n    'data-test-subj': 'unit-milli-second',\n  },\n]\n\nexport const MINUS_ONE = -1\nexport const DEFAULT_SLOWLOG_MAX_LEN = 128\nexport const DEFAULT_SLOWLOG_SLOWER_THAN = 10\nexport const DEFAULT_SLOWLOG_DURATION_UNIT = DurationUnits.milliSeconds\n\nexport const TOOLTIP_DELAY_LONG = 500 // ms\n\nexport default DURATION_UNITS\n"
  },
  {
    "path": "redisinsight/ui/src/constants/env.ts",
    "content": "export enum AppEnv {\n  WEB = 'web',\n  ELECTRON = 'electron',\n  DOCKER = 'docker',\n}\n\nexport enum BuildType {\n  RedisStack = 'REDIS_STACK',\n  Electron = 'ELECTRON',\n  DockerOnPremise = 'DOCKER_ON_PREMISE',\n}\n\nexport enum PackageType {\n  Flatpak = 'flatpak',\n  Snap = 'snap',\n  UnknownLinux = 'unknown-linux',\n  AppImage = 'app-image',\n  Mas = 'mas',\n  UnknownDarwin = 'unknown-darwin',\n  WindowsStore = 'windows-store',\n  UnknownWindows = 'unknown-windows',\n  Unknown = 'unknown',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/featureFlags.ts",
    "content": "export enum FeatureFlags {\n  insightsRecommendations = 'insightsRecommendations',\n  cloudSso = 'cloudSso',\n  cloudSsoRecommendedSettings = 'cloudSsoRecommendedSettings',\n  databaseChat = 'databaseChat',\n  documentationChat = 'documentationChat',\n  envDependent = 'envDependent',\n  rdi = 'redisDataIntegration',\n  hashFieldExpiration = 'hashFieldExpiration',\n  enhancedCloudUI = 'enhancedCloudUI',\n  cloudAds = 'cloudAds',\n  databaseManagement = 'databaseManagement',\n  vectorSearchV2 = 'vectorSearchV2',\n  azureEntraId = 'azureEntraId',\n  devBrowser = 'dev-browser',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/featuresHighlighting.tsx",
    "content": "import React from 'react'\n\nexport type FeaturesHighlightingType =\n  | 'plain'\n  | 'tooltip'\n  | 'popover'\n  | 'dialog'\n  | 'tooltip-badge'\n\ninterface BuildHighlightingFeature {\n  type: FeaturesHighlightingType\n  title?: string | React.ReactElement\n  content?: string | React.ReactElement\n  page?: string\n  asPageFeature?: boolean\n}\n\nexport const BUILD_FEATURES: Record<string, BuildHighlightingFeature> = {\n  aiChatbot: {\n    type: 'dialog',\n  },\n} as const\n"
  },
  {
    "path": "redisinsight/ui/src/constants/help-texts.tsx",
    "content": "import React from 'react'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport {\n  EXTERNAL_LINKS,\n  UTM_CAMPAINGS,\n  UTM_MEDIUMS,\n} from 'uiSrc/constants/links'\n\nimport { CloudLink } from 'uiSrc/components/markdown'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { FeatureFlags } from './featureFlags'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport default {\n  REJSON_SHOULD_BE_LOADED: (\n    <>\n      This database does not support the JSON data structure. Learn more about\n      JSON support{' '}\n      <a\n        href=\"https://redis.io/docs/latest/operate/oss_and_stack/stack-with-enterprise/json/\"\n        target=\"_blank\"\n        rel=\"noreferrer\"\n      >\n        here\n      </a>\n      .{' '}\n      <FeatureFlagComponent name={FeatureFlags.cloudAds}>\n        <>\n          You can also create a{' '}\n          <CloudLink\n            text=\"free Redis Cloud database\"\n            url={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n              source: UTM_MEDIUMS.App,\n              campaign: UTM_CAMPAINGS.RedisJson,\n            })}\n            source={OAuthSocialSource.BrowserRedisJSON}\n          />{' '}\n          with built-in JSON support.\n        </>\n      </FeatureFlagComponent>\n    </>\n  ),\n  REMOVE_LAST_ELEMENT: () => (\n    <Row align=\"center\">\n      <RiIcon size=\"s\" type=\"ToastDangerIcon\" style={{ marginRight: '1rem' }} />\n      <Text size=\"s\">Removing the last item deletes the entire key.</Text>\n    </Row>\n  ),\n  REMOVING_MULTIPLE_ELEMENTS_NOT_SUPPORT: (\n    <>\n      Removing multiple elements is available for Redis databases v. 6.2 or\n      later. Update your Redis database or create a new&nbsp;\n      <a\n        href={`${EXTERNAL_LINKS.tryFree}?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_redis_latest`}\n        target=\"_blank\"\n        className=\"link-underline\"\n        rel=\"noreferrer\"\n      >\n        free up-to-date\n      </a>\n      &nbsp;Redis database.\n    </>\n  ),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/importDatabasesTableResult.ts",
    "content": "export enum ImportTableResultColumn {\n  Index = 'index',\n  Host = 'host',\n  Errors = 'errors',\n}\n\nexport const TABLE_IMPORT_RESULT_COLUMN_ID_HEADER_MAP = new Map<\n  ImportTableResultColumn,\n  string\n>([\n  [ImportTableResultColumn.Index, '#'],\n  [ImportTableResultColumn.Host, 'Host:Port'],\n  [ImportTableResultColumn.Errors, 'Result'],\n])\n\nexport enum ImportDatabaseResultType {\n  Success = 'success',\n  Partial = 'partial',\n  Fail = 'fail',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/index.ts",
    "content": "import ApiEndpoints from './api'\nimport BrowserStorageItem from './storage'\nimport ApiStatusCode from './apiStatusCode'\nimport apiErrors from './apiErrors'\n\nexport * from './keys'\nexport * from './themes'\nexport * from './table'\nexport * from './redisinsight'\nexport * from './commands'\nexport * from './monaco/monaco'\nexport * from './monaco/monacoRedis'\nexport * from './monaco/monitorEvents'\nexport * from './keyboardShortcuts'\nexport * from './pages'\nexport * from './workbenchResults'\nexport * from './socketEvents'\nexport * from './mocks/mock-redis-commands'\nexport * from './mocks/mock-tutorials'\nexport * from './mocks/mock-custom-tutorials'\nexport * from './socketErrors'\nexport * from './browser'\nexport * from './string'\nexport * from './durationUnits'\nexport * from './streamViews'\nexport * from './bulkActions'\nexport * from './workbench'\nexport * from './featureFlags'\nexport * from './serverVersions'\nexport * from './customErrorCodes'\nexport * from './securityField'\nexport * from './redisearch'\nexport * from './browser/keyDetailsHeader'\nexport * from './tutorials'\nexport * from './datetime'\nexport * from './sorting'\nexport * from './databaseList'\nexport * from './rdiList'\nexport * from './importDatabasesTableResult'\nexport { ApiEndpoints, BrowserStorageItem, ApiStatusCode, apiErrors }\n"
  },
  {
    "path": "redisinsight/ui/src/constants/keyboardShortcuts.tsx",
    "content": "import React from 'react'\nimport { isMacOs } from 'uiSrc/utils/dom'\n\n// TODO: use i18n file for labels & descriptions\nconst COMMON_SHORTCUTS = {\n  _separator: '+',\n  desktop: {\n    newWindow: {\n      description: 'Open a new window',\n      keys: ['Ctrl', 'N'],\n    },\n    reloadPage: {\n      description: 'Reload the page',\n      keys: ['Ctrl', 'R'],\n    },\n  },\n  cli: {\n    autocompleteNext: {\n      description: 'Autocomplete with the next command',\n      keys: ['Tab'],\n    },\n    autocompletePrev: {\n      description: 'Autocomplete with the previous command',\n      keys: ['Shift', 'Tab'],\n    },\n    clearSearch: {\n      description: 'Clear the screen',\n      keys: ['Ctrl', 'L'],\n    },\n    prevCommand: {\n      description: 'Return to the previous command',\n      keys: ['Up Arrow'],\n    },\n    nextCommand: {\n      description:\n        'Scroll the list of commands in the opposite direction to the Up Arrow',\n      keys: ['Down Arrow'],\n    },\n  },\n  workbench: {\n    runQuery: {\n      label: 'Run',\n      description: 'Run Command',\n      keys: ['Ctrl', 'Enter'],\n    },\n    nextLine: {\n      description: 'Go to the next line',\n      keys: ['Enter'],\n    },\n    listOfCommands: {\n      description:\n        'Display the list of commands and information about commands and their arguments in the suggestion list',\n      keys: ['Ctrl', 'Space'],\n    },\n    triggerHints: {\n      description: 'Trigger Command Hints',\n      keys: ['Ctrl', 'Shift', 'Space'],\n    },\n    quickHistoryAccess: {\n      description: 'Quick-access to command history',\n      keys: ['Up Arrow'],\n    },\n    nonRedisEditor: {\n      description: 'Use Non-Redis Editor',\n      keys: ['Shift', 'Space'],\n    },\n  },\n  rdi: {\n    openDedicatedEditor: {\n      description: 'Open a dedicated SQL or JMESPath editor:',\n      keys: ['Shift', 'Space'],\n    },\n  },\n}\n\nconst MAC_SHORTCUTS = {\n  _separator: '',\n  desktop: {\n    newWindow: {\n      description: 'Open a new window',\n      keys: [<span className=\"cmdSymbol\">⌘</span>, 'N'],\n    },\n    reloadPage: {\n      description: 'Reload the page',\n      keys: [<span className=\"cmdSymbol\">⌘</span>, 'R'],\n    },\n  },\n  cli: {\n    autocompleteNext: {\n      description: 'Autocomplete with the next command',\n      keys: ['Tab'],\n    },\n    autocompletePrev: {\n      description: 'Autocomplete with the previous command',\n      keys: [<span className=\"shiftSymbol\">⇧</span>, 'Tab'],\n    },\n    clearSearch: {\n      description: 'Clear the screen',\n      keys: [<span className=\"cmdSymbol\">⌘</span>, 'K'],\n    },\n    prevCommand: {\n      description: 'Return to the previous command',\n      keys: [<span className=\"badgeArrowUp\">↑</span>],\n    },\n    nextCommand: {\n      description:\n        'Scroll the list of commands in the opposite direction to the Up Arrow',\n      keys: [<span className=\"badgeArrowDown\">↓</span>],\n    },\n  },\n  workbench: {\n    runQuery: {\n      label: 'Run commands',\n      description: 'Run Commands',\n      keys: [<span className=\"cmdSymbol\">⌘</span>, 'Enter'],\n    },\n    nextLine: {\n      description: 'Go to the next line',\n      keys: ['Enter'],\n    },\n    listOfCommands: {\n      description:\n        'Display the list of commands and information about commands and their arguments in the suggestion list',\n      keys: [<span className=\"cmdSymbol\">⌘</span>, 'Space'],\n    },\n    triggerHints: {\n      description: 'Trigger Command Hints',\n      keys: [\n        <span className=\"cmdSymbol\">⌘</span>,\n        <span className=\"shiftSymbol\">⇧</span>,\n        'Space',\n      ],\n    },\n    quickHistoryAccess: {\n      description: 'Quick-access to command history',\n      keys: ['Up Arrow'],\n    },\n    nonRedisEditor: {\n      description: 'Use Non-Redis Editor',\n      keys: [<span className=\"shiftSymbol\">⇧</span>, 'Space'],\n    },\n  },\n  rdi: {\n    openDedicatedEditor: {\n      description: 'Open a dedicated SQL or JMESPath editor',\n      keys: [<span className=\"shiftSymbol\">⇧</span>, 'Space'],\n    },\n  },\n}\n\nexport const KEYBOARD_SHORTCUTS = isMacOs() ? MAC_SHORTCUTS : COMMON_SHORTCUTS\n"
  },
  {
    "path": "redisinsight/ui/src/constants/keys.ts",
    "content": "import { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { CommandGroup } from './commands'\n\nexport enum KeyTypes {\n  Hash = 'hash',\n  List = 'list',\n  Set = 'set',\n  ZSet = 'zset',\n  String = 'string',\n  ReJSON = 'ReJSON-RL',\n  JSON = 'json',\n  Stream = 'stream',\n}\n\nexport const SEARCHABLE_KEY_TYPES: KeyTypes[] = [KeyTypes.Hash, KeyTypes.ReJSON]\n\nexport enum ModulesKeyTypes {\n  Graph = 'graphdata',\n  TimeSeries = 'TSDB-TYPE',\n}\n\nexport const GROUP_TYPES_DISPLAY = Object.freeze({\n  [KeyTypes.Hash]: 'Hash',\n  [KeyTypes.List]: 'List',\n  [KeyTypes.Set]: 'Set',\n  [KeyTypes.ZSet]: 'Sorted Set',\n  [KeyTypes.String]: 'String',\n  [KeyTypes.ReJSON]: 'JSON',\n  [KeyTypes.JSON]: 'JSON',\n  [KeyTypes.Stream]: 'Stream',\n  [ModulesKeyTypes.TimeSeries]: 'Time Series',\n  [CommandGroup.Bitmap]: 'Bitmap',\n  [CommandGroup.Cluster]: 'Cluster',\n  [CommandGroup.Connection]: 'Connection',\n  [CommandGroup.Geo]: 'Geo',\n  [CommandGroup.Generic]: 'Generic',\n  [CommandGroup.PubSub]: 'Pub/Sub',\n  [CommandGroup.Scripting]: 'Scripting',\n  [CommandGroup.Transactions]: 'Transactions',\n  [CommandGroup.TimeSeries]: 'TimeSeries',\n  [CommandGroup.Server]: 'Server',\n  [CommandGroup.SortedSet]: 'Sorted Set',\n  [CommandGroup.HyperLogLog]: 'HyperLogLog',\n  [CommandGroup.CMS]: 'CMS',\n  [CommandGroup.TDigest]: 'TDigest',\n  [CommandGroup.TopK]: 'TopK',\n  [CommandGroup.BloomFilter]: 'Bloom Filter',\n  [CommandGroup.CuckooFilter]: 'Cuckoo Filter',\n})\n\nexport type GroupTypesDisplay = keyof typeof GROUP_TYPES_DISPLAY\n\n// Enums don't allow to use dynamic key\nexport const GROUP_TYPES_COLORS = Object.freeze({\n  [KeyTypes.Hash]: 'var(--typeHashColor)',\n  [KeyTypes.List]: 'var(--typeListColor)',\n  [KeyTypes.Set]: 'var(--typeSetColor)',\n  [KeyTypes.ZSet]: 'var(--typeZSetColor)',\n  [KeyTypes.String]: 'var(--typeStringColor)',\n  [KeyTypes.ReJSON]: 'var(--typeReJSONColor)',\n  [KeyTypes.JSON]: 'var(--typeReJSONColor)',\n  [KeyTypes.Stream]: 'var(--typeStreamColor)',\n  [ModulesKeyTypes.Graph]: 'var(--typeGraphColor)',\n  [ModulesKeyTypes.TimeSeries]: 'var(--typeTimeSeriesColor)',\n  [CommandGroup.SortedSet]: 'var(--groupSortedSetColor)',\n  [CommandGroup.Bitmap]: 'var(--groupBitmapColor)',\n  [CommandGroup.Cluster]: 'var(--groupClusterColor)',\n  [CommandGroup.Connection]: 'var(--groupConnectionColor)',\n  [CommandGroup.Geo]: 'var(--groupGeoColor)',\n  [CommandGroup.Generic]: 'var(--groupGenericColor)',\n  [CommandGroup.PubSub]: 'var(--groupPubSubColor)',\n  [CommandGroup.Scripting]: 'var(--groupScriptingColor)',\n  [CommandGroup.Transactions]: 'var(--groupTransactionsColor)',\n  [CommandGroup.Server]: 'var(--groupServerColor)',\n  [CommandGroup.HyperLogLog]: 'var(--groupHyperLolLogColor)',\n})\n\nexport type GroupTypesColors = keyof typeof GROUP_TYPES_COLORS\n\nexport type KeyTypesActions = {\n  [key: string]: {\n    addItems?: {\n      name: string\n    }\n    removeItems?: {\n      name: string\n    }\n    editItem?: {\n      name: string\n    }\n  }\n}\n\nexport const STREAM_ADD_GROUP_VIEW_TYPES = [\n  StreamViewType.Groups,\n  StreamViewType.Consumers,\n  StreamViewType.Messages,\n]\n\nexport const STREAM_ADD_ACTION = Object.freeze({\n  [StreamViewType.Data]: {\n    name: 'New Entry',\n  },\n  [StreamViewType.Groups]: {\n    name: 'New Group',\n  },\n  [StreamViewType.Consumers]: {\n    name: 'New Group',\n  },\n  [StreamViewType.Messages]: {\n    name: 'New Group',\n  },\n})\n\nexport enum SortOrder {\n  ASC = 'ASC',\n  DESC = 'DESC',\n}\n\nexport interface LengthNamingByType {\n  [key: string]: string\n}\n\nexport const LENGTH_NAMING_BY_TYPE: LengthNamingByType = Object.freeze({\n  [ModulesKeyTypes.Graph]: 'Nodes',\n  [ModulesKeyTypes.TimeSeries]: 'Samples',\n  [KeyTypes.Stream]: 'Entries',\n  [KeyTypes.ReJSON]: 'Top-level values',\n})\n\nexport interface ModulesKeyTypesNames {\n  [key: string]: string\n}\n\nexport const MODULES_KEY_TYPES_NAMES: ModulesKeyTypesNames = Object.freeze({\n  [ModulesKeyTypes.Graph]: 'RedisGraph',\n  [ModulesKeyTypes.TimeSeries]: 'RedisTimeSeries',\n})\n\nexport enum KeyValueFormat {\n  Unicode = 'Unicode',\n  ASCII = 'ASCII',\n  JSON = 'JSON',\n  HEX = 'HEX',\n  Binary = 'Binary',\n  Msgpack = 'Msgpack',\n  PHP = 'PHP serialized',\n  JAVA = 'Java serialized',\n  Protobuf = 'Protobuf',\n  Pickle = 'Pickle',\n  Vector32Bit = 'Vector 32-bit',\n  Vector64Bit = 'Vector 64-bit',\n  DateTime = 'DateTime',\n}\n\nexport const DATETIME_FORMATTER_DEFAULT = 'HH:mm:ss d MMM yyyy'\n\nexport enum KeyValueCompressor {\n  GZIP = 'GZIP',\n  ZSTD = 'ZSTD',\n  LZ4 = 'LZ4',\n  SNAPPY = 'SNAPPY',\n  Brotli = 'Brotli',\n  PHPGZCompress = 'PHPGZCompress',\n}\n\nexport const COMPRESSOR_MAGIC_SYMBOLS: ICompressorMagicSymbols = Object.freeze({\n  [KeyValueCompressor.GZIP]: '31,139', // 1f 8b hex\n  [KeyValueCompressor.ZSTD]: '40,181,47,253', // 28 b5 2f fd hex\n  [KeyValueCompressor.LZ4]: '4,34,77,24', // 04 22 4d 18 hex\n  [KeyValueCompressor.SNAPPY]: '', // no magic symbols\n  [KeyValueCompressor.Brotli]: '', // no magic symbols\n  [KeyValueCompressor.PHPGZCompress]: '', // no magic symbols\n})\n\nexport type ICompressorMagicSymbols = {\n  [key in KeyValueCompressor]: string\n}\n\nexport const ENDPOINT_BASED_ON_KEY_TYPE = Object.freeze({\n  [KeyTypes.ZSet]: ApiEndpoints.ZSET,\n  [KeyTypes.Set]: ApiEndpoints.SET,\n  [KeyTypes.String]: ApiEndpoints.STRING,\n  [KeyTypes.Hash]: ApiEndpoints.HASH,\n  [KeyTypes.List]: ApiEndpoints.LIST,\n  [KeyTypes.ReJSON]: ApiEndpoints.REJSON,\n  [KeyTypes.Stream]: ApiEndpoints.STREAMS,\n})\n\nexport type EndpointBasedOnKeyType = keyof typeof ENDPOINT_BASED_ON_KEY_TYPE\n\nexport enum SearchHistoryMode {\n  Pattern = 'pattern',\n  Redisearch = 'redisearch',\n}\n\nexport const ENTER = 'Enter'\nexport const SPACE = ' '\nexport const ESCAPE = 'Escape'\nexport const TAB = 'Tab'\nexport const BACKSPACE = 'Backspace'\nexport const F2 = 'F2'\n\nexport const ALT = 'Alt'\nexport const SHIFT = 'Shift'\nexport const CTRL = 'Control'\nexport const META = 'Meta' // Windows, Command, Option\n\nexport const ARROW_DOWN = 'ArrowDown'\nexport const ARROW_UP = 'ArrowUp'\nexport const ARROW_LEFT = 'ArrowLeft'\nexport const ARROW_RIGHT = 'ArrowRight'\n\nexport const PAGE_UP = 'PageUp'\nexport const PAGE_DOWN = 'PageDown'\nexport const END = 'End'\nexport const HOME = 'Home'\n\nexport enum KeyboardKeys {\n  ENTER = 'Enter',\n  SPACE = ' ',\n  ESCAPE = 'Escape',\n  TAB = 'Tab',\n  BACKSPACE = 'Backspace',\n  F2 = 'F2',\n  ARROW_DOWN = 'ArrowDown',\n  ARROW_UP = 'ArrowUp',\n  ARROW_LEFT = 'ArrowLeft',\n  ARROW_RIGHT = 'ArrowRight',\n  PAGE_UP = 'PageUp',\n  PAGE_DOWN = 'PageDown',\n  END = 'End',\n  HOME = 'Home',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/links.ts",
    "content": "import { CloudSsoUtmCampaign, OAuthSocialSource } from 'uiSrc/slices/interfaces'\n\nexport const EXTERNAL_LINKS = {\n  redisIo: 'https://redis.io',\n  githubRepo: 'https://github.com/RedisInsight/RedisInsight',\n  githubIssues: 'https://github.com/RedisInsight/RedisInsight/issues',\n  releaseNotes: 'https://github.com/RedisInsight/RedisInsight/releases',\n  userSurvey: 'https://www.surveymonkey.com/r/redisinsight',\n  recommendationFeedback:\n    'https://github.com/RedisInsight/RedisInsight/issues/new/choose',\n  guidesRepo: 'https://github.com/RedisInsight/Tutorials',\n  redisStack:\n    'https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/',\n  cloudConsole: 'https://cloud.redis.io/#/databases/',\n  tryFree: 'https://redis.io/try-free',\n  docker: 'https://redis.io/docs/install/install-stack/docker',\n  rdiQuickStart:\n    'https://redis.io/docs/latest/integrate/redis-data-integration/ingest/quick-start-guide/',\n  rdiPipeline:\n    'https://redis.io/docs/latest/integrate/redis-data-integration/data-pipelines/',\n  rdiPipelineTransforms:\n    'https://redis.io/docs/latest/integrate/redis-data-integration/ingest/data-pipelines/transform-examples/',\n  pubSub: 'https://redis.io/docs/latest/commands/psubscribe/',\n  legalPrivacyPolicy: 'https://redis.io/legal/privacy-policy/',\n  redisEnterpriseCloud: 'https://redis.io/redis-enterprise-cloud/overview/',\n  redisQueryEngine: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n  redisForAI: 'https://redis.io/redis-for-ai/',\n  vectorDatabaseGettingStarted:\n    'https://redis.io/docs/latest/develop/get-started/vector-database/',\n  redisSandbox: 'https://redis.io/try/sandbox/',\n  searchIndexes:\n    'https://redis.io/docs/latest/develop/ai/search-and-query/query/vector-search/',\n}\n\nexport const UTM_CAMPAINGS: Record<any, string> = {\n  [OAuthSocialSource.Tutorials]: 'redisinsight_tutorials',\n  [OAuthSocialSource.BrowserSearch]: 'redisinsight_browser_search',\n  [OAuthSocialSource.BrowserFiltering]: 'redisinsight_browser_filtering',\n  [OAuthSocialSource.Workbench]: 'redisinsight_workbench',\n  [CloudSsoUtmCampaign.BrowserFilter]: 'browser_filter',\n  [OAuthSocialSource.EmptyDatabasesList]: 'empty_db_list',\n  [OAuthSocialSource.AddDbForm]: 'add_db_form',\n  PubSub: 'pub_sub',\n  Main: 'main',\n  RedisJson: 'redisinsight_redisjson',\n}\n\nexport const UTM_MEDIUMS = {\n  App: 'app',\n  Main: 'main',\n  Rdi: 'rdi',\n  Recommendation: 'recommendation',\n  Settings: 'settings',\n  VectorSearchOnboarding: 'vss_onboarding',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/mocks/mock-custom-tutorials.ts",
    "content": "import {\n  EnablementAreaComponent,\n  IEnablementAreaItem,\n} from 'uiSrc/slices/interfaces'\n\nexport const MOCK_CUSTOM_TUTORIALS_ITEMS: IEnablementAreaItem[] = [\n  {\n    id: 'custom-tutorials',\n    label: 'MY TUTORIALS',\n    type: EnablementAreaComponent.Group,\n    _actions: ['create'],\n    args: {\n      initialIsOpen: true,\n    },\n    children: [\n      {\n        id: '12mfp-rem',\n        label: 'My guide',\n        type: EnablementAreaComponent.Group,\n        _path: '/do21-d',\n        _actions: ['delete'],\n        children: [\n          {\n            type: EnablementAreaComponent.InternalLink,\n            id: 'document-capabilities',\n            label: 'Document Capabilities',\n            args: {\n              path: '/static/workbench/quick-guides/document/learn-more.md',\n            },\n          },\n          {\n            type: EnablementAreaComponent.InternalLink,\n            id: 'working-with-json',\n            label: 'Working with JSON',\n            args: {\n              path: 'quick-guides/working-with-json.html',\n            },\n          },\n        ],\n      },\n    ],\n  },\n]\n\nexport const MOCK_CUSTOM_TUTORIALS = {\n  id: 'custom-tutorials',\n  label: 'MY TUTORIALS',\n  type: EnablementAreaComponent.Group,\n  _actions: ['create'],\n  children: MOCK_CUSTOM_TUTORIALS_ITEMS,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/mocks/mock-explore-guides.ts",
    "content": "export const MOCK_EXPLORE_GUIDES = [\n  {\n    title: 'Search and Query',\n    tutorialId: 'sq-intro',\n    icon: 'search',\n  },\n  {\n    title: 'JSON',\n    tutorialId: 'ds-json-intro',\n    icon: 'json',\n  },\n  {\n    title: 'Time Series',\n    tutorialId: 'ds-ts-intro',\n    icon: 'time-series',\n  },\n  {\n    title: 'Probabilistic',\n    tutorialId: 'ds-prob-intro',\n    icon: 'probabilistic-data-structures',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/mocks/mock-recommendations.ts",
    "content": "import { IRecommendationsStatic } from 'uiSrc/slices/interfaces/recommendations'\n\nexport const MOCK_RECOMMENDATIONS: IRecommendationsStatic = {\n  luaScript: {\n    id: 'luaScript',\n    title: 'Avoid dynamic Lua script',\n    content: [\n      {\n        type: 'span',\n        value:\n          'Refrain from generating dynamic scripts, which can cause your Lua cache to grow and get out of control. Memory is consumed as scripts are loaded. If you have to use dynamic Lua scripts, then remember to track your Lua memory consumption and flush the cache periodically with a ',\n      },\n      {\n        type: 'code-link',\n        value: {\n          href: 'https://redis.io/commands/script-flush/',\n          name: 'SCRIPT FLUSH',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          '. Also do not hardcode and/or programmatically generate key names in your Lua scripts because they do not work in a clustered Redis setup.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/interact/programmability/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' to learn more about programmability in Redis.',\n      },\n    ],\n    badges: ['code_changes'],\n  },\n  useSmallerKeys: {\n    id: 'useSmallerKeys',\n    title: 'Use smaller keys',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'Shorten key names to optimize memory usage. Though, in general, descriptive key names are always preferred, these large key names can eat a lot of the memory.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/optimization/memory-optimization/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' for more memory optimization strategies.',\n      },\n    ],\n    badges: ['code_changes'],\n  },\n  bigHashes: {\n    id: 'bigHashes',\n    telemetryEvent: 'shardHashes',\n    title: 'Shard big hashes to small hashes',\n    redisStack: true,\n    tutorialId: 'ds-hashes',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'If you have a hash with a large number of field-value pairs (> 5,000), and each pair is small enough - break it into smaller hashes to optimize memory usage. Additionally, try using smaller or shortened field names.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Try the interactive tutorial to learn how to work with index and query documents modeled with hashes.',\n      },\n    ],\n    badges: ['code_changes', 'configuration_changes'],\n  },\n  avoidLogicalDatabases: {\n    id: 'avoidLogicalDatabases',\n    title: 'Avoid using logical databases',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'Using logical databases is an anti-pattern that Salvatore Sanfilippo, the creator of Redis, once called the worst design mistake he ever made in Redis.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Although supported in Redis, logical databases are neither independent nor isolated in any other way and can freeze each other. Also, they are not supported by any clustering system (open source or Redis Enterprise clustering).',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'If you need a multi-tenant environment, try ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/try-free/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          ', where each tenant has its own Redis database endpoint which is completely isolated from the other Redis databases.',\n      },\n    ],\n    badges: ['code_changes'],\n  },\n  combineSmallStringsToHashes: {\n    id: 'combineSmallStringsToHashes',\n    title: 'Combine small strings to hashes',\n    tutorialId: 'ds-hashes',\n    content: [\n      {\n        type: 'span',\n        value:\n          'The strings data type has an overhead of about 90 bytes on a 64-bit machine, so if there is no need for different expiration values for these keys, combine small strings into a larger hash to optimize the memory usage. Also, ensure that the hash has less than ',\n      },\n      {\n        type: 'code',\n        value: 'hash-max-ziplist-entries',\n      },\n      {\n        type: 'span',\n        value: ' elements and that the size of each element is within ',\n      },\n      {\n        type: 'code',\n        value: 'hash-max-ziplist-values',\n      },\n      {\n        type: 'span',\n        value: ' bytes.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Though this approach should not be used if you need different expiration values for string keys.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/optimization/memory-optimization/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' for more memory optimization strategies.',\n      },\n    ],\n    badges: ['code_changes'],\n  },\n  increaseSetMaxIntsetEntries: {\n    id: 'increaseSetMaxIntsetEntries',\n    title: 'Increase the set-max-intset-entries',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'Several set values with IntSet encoding exceed the set-max-intset-entries. Change the configuration in redis.conf to efficiently use the IntSet encoding. Though increasing this value will lead to an increase in the latency of set operations and CPU utilization.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'Run ',\n      },\n      {\n        type: 'code',\n        value: 'INFO COMMANDSTATS',\n      },\n      {\n        type: 'span',\n        value:\n          ' before and after making this change to verify the latency numbers.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/optimization/memory-optimization/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' for more memory optimization strategies.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  hashHashtableToZiplist: {\n    id: 'hashHashtableToZiplist',\n    title: 'Convert hashtable to ziplist for hashes',\n    content: [\n      {\n        type: 'span',\n        value: 'Increase ',\n      },\n      {\n        type: 'code',\n        value: 'hash-max-ziplist-entries',\n      },\n      {\n        type: 'span',\n        value: ' and/or ',\n      },\n      {\n        type: 'code',\n        value: 'hash-max-ziplist-values',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'If any value for a key exceeds ',\n      },\n      {\n        type: 'code',\n        value: 'hash-max-ziplist-entries',\n      },\n      {\n        type: 'span',\n        value: ' or ',\n      },\n      {\n        type: 'code',\n        value: 'hash-max-ziplist-values',\n      },\n      {\n        type: 'span',\n        value:\n          ', it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save the memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/optimization/memory-optimization/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' for more memory optimization strategies.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  compressHashFieldNames: {\n    id: 'compressHashFieldNames',\n    deprecated: true,\n    title: 'Compress Hash field names',\n    content: [\n      {\n        type: 'span',\n        value:\n          'Hash field name also consumes memory, so use smaller or shortened field names to reduce memory usage. ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://docs.redis.com/latest/ri/memory-optimizations/',\n          name: 'Read more',\n        },\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  compressionForList: {\n    id: 'compressionForList',\n    title: 'Enable compression for the list',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'If you use long lists, and mostly access elements from the head and tail only, then you can enable compression.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'Set ',\n      },\n      {\n        type: 'code',\n        value: 'list-compression-depth=1',\n      },\n      {\n        type: 'span',\n        value:\n          ' in redis.conf to compress every list node except the head and tail of the list. Though list operations that involve elements in the center of the list will get slower, the compression can increase CPU utilization.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'Run ',\n      },\n      {\n        type: 'code',\n        value: 'INFO COMMANDSTATS',\n      },\n      {\n        type: 'span',\n        value:\n          ' before and after making this change to verify the latency numbers.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/optimization/memory-optimization/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' for more memory optimization strategies.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  bigStrings: {\n    id: 'bigStrings',\n    title: 'Avoid large strings',\n    tutorialId: 'ds-json-intro',\n    content: [\n      {\n        type: 'span',\n        value:\n          'If you are working with long strings you may need to retrieve parts or split them to handle in your application. Consider modeling your data using hashes or JSON. Both data structures provide fast and efficient data retrieval via the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'query and search capabilities',\n        },\n      },\n      {\n        type: 'span',\n        value: ', natively developed in Redis, and available as part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Try the interactive tutorial to learn how to work with, index, and query documents modeled with JSON or hashes.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  zSetHashtableToZiplist: {\n    id: 'zSetHashtableToZiplist',\n    title: 'Convert hashtable to ziplist for sorted sets',\n    content: [\n      {\n        type: 'span',\n        value: 'Increase ',\n      },\n      {\n        type: 'code',\n        value: 'zset-max-ziplist-entries',\n      },\n      {\n        type: 'span',\n        value: ' and/or ',\n      },\n      {\n        type: 'code',\n        value: 'zset-max-ziplist-values',\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'If any value for a key exceeds ',\n      },\n      {\n        type: 'code',\n        value: 'zset-max-ziplist-entries',\n      },\n      {\n        type: 'span',\n        value: ' or ',\n      },\n      {\n        type: 'code',\n        value: 'zset-max-ziplist-values',\n      },\n      {\n        type: 'span',\n        value:\n          ', it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save the memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/optimization/memory-optimization/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' for more memory optimization strategies.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  bigSets: {\n    id: 'bigSets',\n    telemetryEvent: 'optimizeExistenceChecks',\n    title:\n      'Consider using probabilistic data structures such as Bloom Filter or HyperLogLog',\n    tutorialId: 'ds-prob-intro',\n    redisStack: true,\n    content: [\n      {\n        type: 'span',\n        value:\n          'If you are using sets for existence checks or duplicate elimination and can trade perfect accuracy for speed and memory efficiency, consider using the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/',\n          name: 'probabilistic data structures',\n        },\n      },\n      {\n        type: 'span',\n        value: ', especially useful for big data and streaming use cases.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'These capabilities are part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Try the interactive tutorial to learn more about the probabilistic data structures.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  bigAmountOfConnectedClients: {\n    id: 'bigAmountOfConnectedClients',\n    title: \"Don't open a new connection for every request / every command\",\n    content: [\n      {\n        type: 'span',\n        value: 'When the value of your ',\n      },\n      {\n        type: 'code',\n        value: 'total_connections_received',\n      },\n      {\n        type: 'span',\n        value:\n          ' in the stats section is high, it usually means that your application is opening and closing a connection for every request it makes. Opening a connection is an expensive operation that adds to both client and server latency. To rectify this, consult your ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/resources/clients/',\n          name: 'Redis client’s',\n        },\n      },\n      {\n        type: 'span',\n        value: ' documentation and configure it to use persistent connections.',\n      },\n    ],\n    badges: ['code_changes'],\n  },\n  setPassword: {\n    id: 'setPassword',\n    title: 'Set a password',\n    content: [\n      {\n        type: 'span',\n        value: 'Protect your database by setting a password and using the ',\n      },\n      {\n        type: 'code-link',\n        value: {\n          href: 'https://redis.io/commands/auth/',\n          name: 'AUTH',\n        },\n      },\n      {\n        type: 'span',\n        value: ' command to authenticate the connection.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'See the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/management/security/',\n          name: 'documentation',\n        },\n      },\n      {\n        type: 'span',\n        value: ' to learn more about Redis security.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  RTS: {\n    id: 'RTS',\n    telemetryEvent: 'optimizeTimeSeries',\n    title:\n      'Try using the Redis native time series data structure and querying capabilities',\n    redisStack: true,\n    tutorialId: 'ds-ts-intro',\n    content: [\n      {\n        type: 'span',\n        value:\n          'If you are using sorted sets to store time series data, consider using the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/timeseries/',\n          name: 'time series capabilities',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Take advantage of advanced toolings such as downsampling and aggregation to ensure a small memory footprint without impacting performance.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'It also supports a flexible query language for visualization and monitoring, allows querying of different time series keys across the entire keyspace, and provides built-in connectors to popular tools.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'The capabilities are part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value:\n          'Try the interactive tutorial to learn about the time series data structure and capabilities.',\n      },\n    ],\n    badges: ['configuration_changes'],\n  },\n  redisVersion: {\n    id: 'redisVersion',\n    telemetryEvent: 'updateDatabase',\n    title: 'Upgrade your Redis database to version 6 or above',\n    redisStack: true,\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'Upgrade your database to version 6 or above to take advantage of the following:',\n      },\n      {\n        type: 'list',\n        value: [\n          [\n            {\n              type: 'link',\n              value: {\n                href: 'https://redis.io/docs/management/security/acl/',\n                name: 'access control lists (ACLs)',\n              },\n            },\n            {\n              type: 'span',\n              value:\n                ' that let you create users with permissions for specific actions',\n            },\n          ],\n          [\n            {\n              type: 'link',\n              value: {\n                href: 'https://redis.io/docs/manual/client-side-caching/',\n                name: 'client-side caching',\n              },\n            },\n            {\n              type: 'span',\n              value: ' for high-performance services',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'SSL support',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value:\n                'optimized memory utilization through faster eviction of expired keys',\n            },\n          ],\n        ],\n      },\n      {\n        type: 'span',\n        value:\n          'For a quick trial of the features, spin up a free developer database with ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/try-free/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' that also provides support for Redis Stack ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'query and search',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/json/',\n          name: 'JSON',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/',\n          name: 'probabilistic data structures',\n        },\n      },\n      {\n        type: 'span',\n        value: ', and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/timeseries/',\n          name: 'Time Series',\n        },\n      },\n      {\n        type: 'span',\n        value: ' capabilities.',\n      },\n    ],\n    badges: ['upgrade'],\n  },\n  redisSearch: {\n    id: 'redisSearch',\n    title: 'Optimize your query and search experience',\n    deprecated: true,\n    redisStack: true,\n    tutorialId: 'ds-json-intro',\n    content: [\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'Redis Query Engine',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          ' was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/commands/?name=Ft',\n          name: 'powerful API options',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          ' and try them. Supports full-text search, wildcards, fuzzy logic, and more.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'Create a ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/try-free/',\n          name: 'free Redis Cloud database',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          ' which extends the core capabilities of Redis OSS and uses modern data models and processing engines.',\n      },\n    ],\n    badges: ['upgrade'],\n  },\n  searchIndexes: {\n    id: 'searchIndexes',\n    title:\n      'Try using the indexing, querying, and full-text search, natively developed in Redis',\n    redisStack: true,\n    tutorialId: 'sq-intro',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'If you are using sorted sets for indexing, this may have its downsides:',\n      },\n      {\n        type: 'list',\n        value: [\n          [\n            {\n              type: 'span',\n              value:\n                'limited query flexibility such as filtering, sorting, or aggregations;',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value:\n                'the risk of outdated or incorrect entries when other applications do not properly update or maintain the index;',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'difficulty in handling updates and deletions;',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'lack of horizontal scaling;',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value:\n                'increased storage requirements to store both scores and members.',\n            },\n          ],\n        ],\n      },\n      {\n        type: 'span',\n        value: 'Consider using the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'query and search capabilities',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          ', natively developed in Redis, for performing text searches and complex structured queries. It offers real-time searching through synchronous indexing, ensuring read-your-writes consistency, without compromising the database performance.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'The capability is part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '. Also supported in an ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://docs.redis.com/latest/stack/search/search-active-active/',\n          name: 'Active-Active setup',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Try the interactive tutorial to learn how to index and query documents modeled with JSON or hashes.',\n      },\n    ],\n    badges: ['upgrade'],\n  },\n  searchJSON: {\n    id: 'searchJSON',\n    title: 'Try indexing your JSON documents for efficient data retrieval',\n    redisStack: true,\n    tutorialId: 'sq-intro',\n    content: [\n      {\n        type: 'span',\n        value:\n          'If you are working with JSON and need fast and efficient data retrieval, consider leveraging the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'query and search capabilities',\n        },\n      },\n      {\n        type: 'span',\n        value: ', natively developed in Redis.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'With it, you can perform complex structured queries, full-text searches, aggregations, geo-filtering, and much more. It offers real-time searching through synchronous indexing, ensuring read-your-writes consistency, without compromising the database performance.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'The capabilities are part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '. Also supported in an ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://docs.redis.com/latest/stack/search/search-active-active/',\n          name: 'Active-Active',\n        },\n      },\n      {\n        type: 'span',\n        value: ' setup.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Try the interactive tutorial to learn how to index and query documents modeled with JSON.',\n      },\n    ],\n  },\n  stringToJson: {\n    id: 'stringToJson',\n    title: 'Try using our JSON native document store',\n    redisStack: true,\n    tutorialId: 'ds-json-intro',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'If you are using strings to store JSON documents, consider using the JSON capabilities.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value: 'Primary features include:',\n      },\n      {\n        type: 'list',\n        value: [\n          [\n            {\n              type: 'span',\n              value: 'full support for the JSON standard',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'a ',\n            },\n            {\n              type: 'link',\n              value: {\n                href: 'https://redis.io/docs/latest/develop/data-types/json/path/',\n                name: 'JSONPath',\n              },\n            },\n            {\n              type: 'span',\n              value: ' syntax for selecting/updating elements inside documents',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value:\n                'documents are stored as binary data in a tree structure, allowing fast access to sub-elements',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'typed atomic operations for all JSON value types',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'secondary indexing via the ',\n            },\n            {\n              type: 'link',\n              value: {\n                href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n                name: 'query and search capabilities',\n              },\n            },\n          ],\n        ],\n      },\n      {\n        type: 'span',\n        value: 'All these capabilities are natively part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'Try the interactive tutorial to learn how to work with index and query documents modeled with JSON.',\n      },\n    ],\n  },\n  searchVisualization: {\n    id: 'searchVisualization',\n    title: 'Try Workbench, the advanced command-line interface',\n    tutorialId: '',\n    content: [\n      {\n        type: 'paragraph',\n        value:\n          'Try Redis Insight Workbench, our advanced command-line interface with syntax highlighting, intelligent auto-complete, and the ability to work with commands in an editor mode.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value:\n          'It also provides user-friendly data visualizations and support for ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ' capabilities such as ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/json/',\n          name: 'JSON',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'query and search',\n        },\n      },\n      {\n        type: 'span',\n        value: ', and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/data-types/timeseries/',\n          name: 'Time Series',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n    ],\n  },\n  searchHash: {\n    id: 'searchHash',\n    title: 'Try indexing your hash documents to query and retrieve data',\n    tutorialId: 'sq-intro',\n    redisStack: true,\n    content: [\n      {\n        type: 'span',\n        value: 'If you are using hashes and would like to:',\n      },\n      {\n        type: 'list',\n        value: [\n          [\n            {\n              type: 'span',\n              value:\n                'query and retrieve data based on attributes other than the primary key;',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value: 'sort and rank your data based on specific attributes;',\n            },\n          ],\n          [\n            {\n              type: 'span',\n              value:\n                'perform complex structured aggregations transforming your data by grouping, sorting, and applying different functions (',\n            },\n            {\n              type: 'link',\n              value: {\n                href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n                name: 'Apply Functions',\n              },\n            },\n            {\n              type: 'span',\n              value: ');',\n            },\n          ],\n        ],\n      },\n      {\n        type: 'span',\n        value: 'consider using the ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/latest/develop/ai/search-and-query/',\n          name: 'query and search',\n        },\n      },\n      {\n        type: 'span',\n        value:\n          ' capabilities, natively developed in Redis. With it you can perform complex structured queries, full-text searches, aggregations, geo-filtering and much more.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'paragraph',\n        value:\n          'It offers real-time searching through synchronous indexing, ensuring read-your-writes consistency, without compromising the database performance.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value: 'The capability is part of ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/docs/about/about-stack/',\n          name: 'Redis Stack',\n        },\n      },\n      {\n        type: 'span',\n        value: ', ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-cloud/overview/',\n          name: 'Redis Cloud',\n        },\n      },\n      {\n        type: 'span',\n        value: ' and ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://redis.io/redis-enterprise-software/overview/',\n          name: 'Redis Enterprise Software',\n        },\n      },\n      {\n        type: 'span',\n        value: '. Also supported in an ',\n      },\n      {\n        type: 'link',\n        value: {\n          href: 'https://docs.redis.com/latest/stack/search/search-active-active/',\n          name: 'Active-Active setup',\n        },\n      },\n      {\n        type: 'span',\n        value: '.',\n      },\n      {\n        type: 'spacer',\n        value: 'l',\n      },\n      {\n        type: 'span',\n        value:\n          'Try the interactive tutorial to learn how to work with index and query documents modeled with JSON.',\n      },\n    ],\n    badges: ['code_changes', 'configuration_changes'],\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/mocks/mock-redis-commands.ts",
    "content": "export const MOCK_COMMANDS_SPEC = {\n  SET: {\n    provider: 'main',\n    summary: 'Set the string value of a key',\n    since: '1.0.0',\n    group: 'string',\n    complexity: 'O(1)',\n    history: [\n      ['2.6.12', 'Added the `EX`, `PX`, `NX` and `XX` options.'],\n      ['6.0.0', 'Added the `KEEPTTL` option.'],\n      ['6.2.0', 'Added the `GET`, `EXAT` and `PXAT` option.'],\n      ['7.0.0', 'Allowed the `NX` and `GET` options to be used together.'],\n    ],\n    acl_categories: ['@write', '@string', '@slow'],\n    arity: -3,\n    key_specs: [\n      {\n        notes: 'RW and ACCESS due to the optional `GET` argument',\n        begin_search: {\n          type: 'index',\n          spec: {\n            index: 1,\n          },\n        },\n        find_keys: {\n          type: 'range',\n          spec: {\n            lastkey: 0,\n            keystep: 1,\n            limit: 0,\n          },\n        },\n        RW: true,\n        access: true,\n        update: true,\n        variable_flags: true,\n      },\n    ],\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n        display_text: 'key',\n        key_spec_index: 0,\n      },\n      {\n        name: 'value',\n        type: 'string',\n        display_text: 'value',\n      },\n      {\n        name: 'condition',\n        type: 'oneof',\n        since: '2.6.12',\n        optional: true,\n        arguments: [\n          {\n            name: 'nx',\n            type: 'pure-token',\n            display_text: 'nx',\n            token: 'NX',\n          },\n          {\n            name: 'xx',\n            type: 'pure-token',\n            display_text: 'xx',\n            token: 'XX',\n          },\n        ],\n      },\n      {\n        name: 'get',\n        type: 'pure-token',\n        display_text: 'get',\n        token: 'GET',\n        since: '6.2.0',\n        optional: true,\n      },\n      {\n        name: 'expiration',\n        type: 'oneof',\n        optional: true,\n        arguments: [\n          {\n            name: 'seconds',\n            type: 'integer',\n            display_text: 'seconds',\n            token: 'EX',\n            since: '2.6.12',\n          },\n          {\n            name: 'milliseconds',\n            type: 'integer',\n            display_text: 'milliseconds',\n            token: 'PX',\n            since: '2.6.12',\n          },\n          {\n            name: 'unix-time-seconds',\n            type: 'unix-time',\n            display_text: 'unix-time-seconds',\n            token: 'EXAT',\n            since: '6.2.0',\n          },\n          {\n            name: 'unix-time-milliseconds',\n            type: 'unix-time',\n            display_text: 'unix-time-milliseconds',\n            token: 'PXAT',\n            since: '6.2.0',\n          },\n          {\n            name: 'keepttl',\n            type: 'pure-token',\n            display_text: 'keepttl',\n            token: 'KEEPTTL',\n            since: '6.0.0',\n          },\n        ],\n      },\n    ],\n    command_flags: ['write', 'denyoom'],\n  },\n  GET: {\n    provider: 'main',\n    summary: 'Get the value of a key',\n    since: '1.0.0',\n    group: 'string',\n    complexity: 'O(1)',\n    acl_categories: ['@read', '@string', '@fast'],\n    arity: 2,\n    key_specs: [\n      {\n        begin_search: {\n          type: 'index',\n          spec: {\n            index: 1,\n          },\n        },\n        find_keys: {\n          type: 'range',\n          spec: {\n            lastkey: 0,\n            keystep: 1,\n            limit: 0,\n          },\n        },\n        RO: true,\n        access: true,\n      },\n    ],\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n        display_text: 'key',\n        key_spec_index: 0,\n      },\n    ],\n    command_flags: ['readonly', 'fast'],\n  },\n  HSET: {\n    provider: 'main',\n    summary: 'Set the string value of a hash field',\n    since: '2.0.0',\n    group: 'hash',\n    complexity:\n      'O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.',\n    history: [['4.0.0', 'Accepts multiple `field` and `value` arguments.']],\n    acl_categories: ['@write', '@hash', '@fast'],\n    arity: -4,\n    key_specs: [\n      {\n        begin_search: {\n          type: 'index',\n          spec: {\n            index: 1,\n          },\n        },\n        find_keys: {\n          type: 'range',\n          spec: {\n            lastkey: 0,\n            keystep: 1,\n            limit: 0,\n          },\n        },\n        RW: true,\n        update: true,\n      },\n    ],\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n        display_text: 'key',\n        key_spec_index: 0,\n      },\n      {\n        name: 'data',\n        type: 'block',\n        multiple: true,\n        arguments: [\n          {\n            name: 'field',\n            type: 'string',\n            display_text: 'field',\n          },\n          {\n            name: 'value',\n            type: 'string',\n            display_text: 'value',\n          },\n        ],\n      },\n    ],\n    command_flags: ['write', 'denyoom', 'fast'],\n  },\n  'CLIENT KILL': {\n    provider: 'main',\n    summary: 'Kill the connection of a client',\n    since: '2.4.0',\n    group: 'connection',\n    complexity: 'O(N) where N is the number of client connections',\n    history: [\n      ['2.8.12', 'Added new filter format.'],\n      ['2.8.12', '`ID` option.'],\n      ['3.2.0', 'Added `master` type in for `TYPE` option.'],\n      [\n        '5.0.0',\n        'Replaced `slave` `TYPE` with `replica`. `slave` still supported for backward compatibility.',\n      ],\n      ['6.2.0', '`LADDR` option.'],\n    ],\n    acl_categories: ['@admin', '@slow', '@dangerous', '@connection'],\n    arity: -3,\n    arguments: [\n      {\n        name: 'filter',\n        type: 'oneof',\n        arguments: [\n          {\n            name: 'old-format',\n            type: 'string',\n            display_text: 'ip:port',\n            deprecated_since: '2.8.12',\n          },\n          {\n            name: 'new-format',\n            type: 'oneof',\n            multiple: true,\n            arguments: [\n              {\n                name: 'client-id',\n                type: 'integer',\n                display_text: 'client-id',\n                token: 'ID',\n                since: '2.8.12',\n                optional: true,\n              },\n              {\n                name: 'client-type',\n                type: 'oneof',\n                token: 'TYPE',\n                since: '2.8.12',\n                optional: true,\n                arguments: [\n                  {\n                    name: 'normal',\n                    type: 'pure-token',\n                    display_text: 'normal',\n                    token: 'NORMAL',\n                  },\n                  {\n                    name: 'master',\n                    type: 'pure-token',\n                    display_text: 'master',\n                    token: 'MASTER',\n                    since: '3.2.0',\n                  },\n                  {\n                    name: 'slave',\n                    type: 'pure-token',\n                    display_text: 'slave',\n                    token: 'SLAVE',\n                  },\n                  {\n                    name: 'replica',\n                    type: 'pure-token',\n                    display_text: 'replica',\n                    token: 'REPLICA',\n                    since: '5.0.0',\n                  },\n                  {\n                    name: 'pubsub',\n                    type: 'pure-token',\n                    display_text: 'pubsub',\n                    token: 'PUBSUB',\n                  },\n                ],\n              },\n              {\n                name: 'username',\n                type: 'string',\n                display_text: 'username',\n                token: 'USER',\n                optional: true,\n              },\n              {\n                name: 'addr',\n                type: 'string',\n                display_text: 'ip:port',\n                token: 'ADDR',\n                optional: true,\n              },\n              {\n                name: 'laddr',\n                type: 'string',\n                display_text: 'ip:port',\n                token: 'LADDR',\n                since: '6.2.0',\n                optional: true,\n              },\n              {\n                name: 'skipme',\n                type: 'oneof',\n                token: 'SKIPME',\n                optional: true,\n                arguments: [\n                  {\n                    name: 'yes',\n                    type: 'pure-token',\n                    display_text: 'yes',\n                    token: 'YES',\n                  },\n                  {\n                    name: 'no',\n                    type: 'pure-token',\n                    display_text: 'no',\n                    token: 'NO',\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      },\n    ],\n    command_flags: ['admin', 'noscript', 'loading', 'stale'],\n  },\n  XGROUP: {\n    provider: 'main',\n    summary: 'A container for consumer groups commands',\n    since: '5.0.0',\n    group: 'stream',\n    complexity: 'Depends on subcommand.',\n    acl_categories: ['@slow'],\n    arity: -2,\n  },\n  'ACL SETUSER': {\n    provider: 'main',\n    summary: 'Modify or create the rules for a specific ACL user',\n    since: '6.0.0',\n    group: 'server',\n    complexity: 'O(N). Where N is the number of rules provided.',\n    history: [\n      ['6.2.0', 'Added Pub/Sub channel patterns.'],\n      ['7.0.0', 'Added selectors and key based permissions.'],\n    ],\n    acl_categories: ['@admin', '@slow', '@dangerous'],\n    arity: -3,\n    arguments: [\n      {\n        name: 'username',\n        type: 'string',\n        display_text: 'username',\n      },\n      {\n        name: 'rule',\n        type: 'string',\n        display_text: 'rule',\n        optional: true,\n        multiple: true,\n      },\n    ],\n    command_flags: ['admin', 'noscript', 'loading', 'stale'],\n  },\n  GEOADD: {\n    provider: 'main',\n    summary:\n      'Add one or more geospatial items in the geospatial index represented using a sorted set',\n    since: '3.2.0',\n    group: 'geo',\n    complexity:\n      'O(log(N)) for each item added, where N is the number of elements in the sorted set.',\n    history: [['6.2.0', 'Added the `CH`, `NX` and `XX` options.']],\n    acl_categories: ['@write', '@geo', '@slow'],\n    arity: -5,\n    key_specs: [\n      {\n        begin_search: {\n          type: 'index',\n          spec: {\n            index: 1,\n          },\n        },\n        find_keys: {\n          type: 'range',\n          spec: {\n            lastkey: 0,\n            keystep: 1,\n            limit: 0,\n          },\n        },\n        RW: true,\n        update: true,\n      },\n    ],\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n        display_text: 'key',\n        key_spec_index: 0,\n      },\n      {\n        name: 'condition',\n        type: 'oneof',\n        since: '6.2.0',\n        optional: true,\n        arguments: [\n          {\n            name: 'nx',\n            type: 'pure-token',\n            display_text: 'nx',\n            token: 'NX',\n          },\n          {\n            name: 'xx',\n            type: 'pure-token',\n            display_text: 'xx',\n            token: 'XX',\n          },\n        ],\n      },\n      {\n        name: 'change',\n        type: 'pure-token',\n        display_text: 'change',\n        token: 'CH',\n        since: '6.2.0',\n        optional: true,\n      },\n      {\n        name: 'data',\n        type: 'block',\n        multiple: true,\n        arguments: [\n          {\n            name: 'longitude',\n            type: 'double',\n            display_text: 'longitude',\n          },\n          {\n            name: 'latitude',\n            type: 'double',\n            display_text: 'latitude',\n          },\n          {\n            name: 'member',\n            type: 'string',\n            display_text: 'member',\n          },\n        ],\n      },\n    ],\n    command_flags: ['write', 'denyoom'],\n  },\n  ZADD: {\n    provider: 'main',\n    summary:\n      'Add one or more members to a sorted set, or update its score if it already exists',\n    since: '1.2.0',\n    group: 'sorted-set',\n    complexity:\n      'O(log(N)) for each item added, where N is the number of elements in the sorted set.',\n    history: [\n      ['2.4.0', 'Accepts multiple elements.'],\n      ['3.0.2', 'Added the `XX`, `NX`, `CH` and `INCR` options.'],\n      ['6.2.0', 'Added the `GT` and `LT` options.'],\n    ],\n    acl_categories: ['@write', '@sortedset', '@fast'],\n    arity: -4,\n    key_specs: [\n      {\n        begin_search: {\n          type: 'index',\n          spec: {\n            index: 1,\n          },\n        },\n        find_keys: {\n          type: 'range',\n          spec: {\n            lastkey: 0,\n            keystep: 1,\n            limit: 0,\n          },\n        },\n        RW: true,\n        update: true,\n      },\n    ],\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n        display_text: 'key',\n        key_spec_index: 0,\n      },\n      {\n        name: 'condition',\n        type: 'oneof',\n        since: '3.0.2',\n        optional: true,\n        arguments: [\n          {\n            name: 'nx',\n            type: 'pure-token',\n            display_text: 'nx',\n            token: 'NX',\n          },\n          {\n            name: 'xx',\n            type: 'pure-token',\n            display_text: 'xx',\n            token: 'XX',\n          },\n        ],\n      },\n      {\n        name: 'comparison',\n        type: 'oneof',\n        since: '6.2.0',\n        optional: true,\n        arguments: [\n          {\n            name: 'gt',\n            type: 'pure-token',\n            display_text: 'gt',\n            token: 'GT',\n          },\n          {\n            name: 'lt',\n            type: 'pure-token',\n            display_text: 'lt',\n            token: 'LT',\n          },\n        ],\n      },\n      {\n        name: 'change',\n        type: 'pure-token',\n        display_text: 'change',\n        token: 'CH',\n        since: '3.0.2',\n        optional: true,\n      },\n      {\n        name: 'increment',\n        type: 'pure-token',\n        display_text: 'increment',\n        token: 'INCR',\n        since: '3.0.2',\n        optional: true,\n      },\n      {\n        name: 'data',\n        type: 'block',\n        multiple: true,\n        arguments: [\n          {\n            name: 'score',\n            type: 'double',\n            display_text: 'score',\n          },\n          {\n            name: 'member',\n            type: 'string',\n            display_text: 'member',\n          },\n        ],\n      },\n    ],\n    command_flags: ['write', 'denyoom', 'fast'],\n  },\n  RESET: {\n    provider: 'main',\n    summary: 'Reset the connection',\n    since: '6.2.0',\n    group: 'connection',\n    complexity: 'O(1)',\n    acl_categories: ['@fast', '@connection'],\n    arity: 1,\n    command_flags: [\n      'noscript',\n      'loading',\n      'stale',\n      'fast',\n      'no_auth',\n      'allow_busy',\n    ],\n  },\n  BITFIELD: {\n    provider: 'main',\n    summary: 'Perform arbitrary bitfield integer operations on strings',\n    since: '3.2.0',\n    group: 'bitmap',\n    complexity: 'O(1) for each subcommand specified',\n    acl_categories: ['@write', '@bitmap', '@slow'],\n    arity: -2,\n    key_specs: [\n      {\n        notes: 'This command allows both access and modification of the key',\n        begin_search: {\n          type: 'index',\n          spec: {\n            index: 1,\n          },\n        },\n        find_keys: {\n          type: 'range',\n          spec: {\n            lastkey: 0,\n            keystep: 1,\n            limit: 0,\n          },\n        },\n        RW: true,\n        access: true,\n        update: true,\n        variable_flags: true,\n      },\n    ],\n    arguments: [\n      {\n        name: 'key',\n        type: 'key',\n        display_text: 'key',\n        key_spec_index: 0,\n      },\n      {\n        name: 'operation',\n        type: 'oneof',\n        optional: true,\n        multiple: true,\n        arguments: [\n          {\n            name: 'get-block',\n            type: 'block',\n            token: 'GET',\n            arguments: [\n              {\n                name: 'encoding',\n                type: 'string',\n                display_text: 'encoding',\n              },\n              {\n                name: 'offset',\n                type: 'integer',\n                display_text: 'offset',\n              },\n            ],\n          },\n          {\n            name: 'write',\n            type: 'block',\n            arguments: [\n              {\n                name: 'overflow-block',\n                type: 'oneof',\n                token: 'OVERFLOW',\n                optional: true,\n                arguments: [\n                  {\n                    name: 'wrap',\n                    type: 'pure-token',\n                    display_text: 'wrap',\n                    token: 'WRAP',\n                  },\n                  {\n                    name: 'sat',\n                    type: 'pure-token',\n                    display_text: 'sat',\n                    token: 'SAT',\n                  },\n                  {\n                    name: 'fail',\n                    type: 'pure-token',\n                    display_text: 'fail',\n                    token: 'FAIL',\n                  },\n                ],\n              },\n              {\n                name: 'write-operation',\n                type: 'oneof',\n                arguments: [\n                  {\n                    name: 'set-block',\n                    type: 'block',\n                    token: 'SET',\n                    arguments: [\n                      {\n                        name: 'encoding',\n                        type: 'string',\n                        display_text: 'encoding',\n                      },\n                      {\n                        name: 'offset',\n                        type: 'integer',\n                        display_text: 'offset',\n                      },\n                      {\n                        name: 'value',\n                        type: 'integer',\n                        display_text: 'value',\n                      },\n                    ],\n                  },\n                  {\n                    name: 'incrby-block',\n                    type: 'block',\n                    token: 'INCRBY',\n                    arguments: [\n                      {\n                        name: 'encoding',\n                        type: 'string',\n                        display_text: 'encoding',\n                      },\n                      {\n                        name: 'offset',\n                        type: 'integer',\n                        display_text: 'offset',\n                      },\n                      {\n                        name: 'increment',\n                        type: 'integer',\n                        display_text: 'increment',\n                      },\n                    ],\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      },\n    ],\n    command_flags: ['write', 'denyoom'],\n  },\n  'RG.GETEXECUTION': {\n    provider: 'redisgears',\n    summary:\n      \"The RG.GETEXECUTION command returns the execution details of a function that's in the executions list.\",\n    complexity: ['O(1)'],\n    arguments: [\n      {\n        name: 'id',\n        type: 'string',\n      },\n      {\n        name: 'mode',\n        type: 'enum',\n        enum: ['SHARD', 'CLUSTER'],\n        optional: true,\n      },\n    ],\n    since: '1.0.0',\n    group: 'gears',\n  },\n  'RG.CONFIGSET': {\n    provider: 'redisgears',\n    summary:\n      'The RG.CONFIGGET command sets the value of one ore more built-in configuration or a user-defined options.',\n    complexity: ['O(1)'],\n    arguments: [\n      {\n        name: 'key value pair',\n        type: 'block',\n        multiple: true,\n        block: [\n          {\n            name: 'requirement',\n          },\n          {\n            type: 'string',\n          },\n        ],\n      },\n    ],\n    since: '1.0.0',\n    group: 'gears',\n  },\n  'RG.PYEXECUTE': {\n    provider: 'redisgears',\n    summary: 'The RG.PYEXECUTE command executes a Python function.',\n    complexity: ['Depends on what the python code does'],\n    arguments: [\n      {\n        name: 'function',\n        type: 'string',\n      },\n      {\n        name: 'UNBLOCKING',\n        type: 'enum',\n        enum: ['UNBLOCKING'],\n        optional: true,\n      },\n      {\n        name: 'REQUIREMENTS',\n        optional: true,\n        multiple: true,\n        type: 'block',\n        block: [\n          {\n            name: 'requirement',\n          },\n          {\n            type: 'string',\n          },\n        ],\n      },\n    ],\n    since: '1.0.0',\n    group: 'gears',\n  },\n  'TS.QUERYINDEX': {\n    provider: 'redistimeseries',\n    summary: 'Get all time series keys matching a filter list',\n    complexity:\n      'O(n) where n is the number of time-series that match the filters',\n    arguments: [\n      {\n        name: 'filterExpr',\n        type: 'oneof',\n        arguments: [\n          {\n            name: 'l=v',\n            type: 'string',\n          },\n          {\n            name: 'l!=v',\n            type: 'string',\n          },\n          {\n            name: 'l=',\n            type: 'string',\n          },\n          {\n            name: 'l!=',\n            type: 'string',\n          },\n          {\n            name: 'l=(v1,v2,...)',\n            type: 'string',\n          },\n          {\n            name: 'l!=(v1,v2,...)',\n            type: 'string',\n          },\n        ],\n        multiple: true,\n      },\n    ],\n    since: '1.0.0',\n    group: 'timeseries',\n  },\n  'GRAPH.QUERY': {\n    provider: 'graph',\n    summary: 'Executes the given query against a specified graph',\n    arguments: [\n      {\n        name: 'graph',\n        type: 'key',\n      },\n      {\n        name: 'query',\n        type: 'string',\n        dsl: 'cypher',\n      },\n      {\n        name: 'timeout',\n        type: 'integer',\n        optional: true,\n        token: 'TIMEOUT',\n      },\n    ],\n    since: '1.0.0',\n    group: 'generic',\n  },\n  'GRAPH.RO_QUERY': {\n    provider: 'graph',\n    summary: 'Executes a given read only query against a specified graph',\n    arguments: [\n      {\n        name: 'graph',\n        type: 'key',\n      },\n      {\n        name: 'query',\n        type: 'string',\n        dsl: 'cypher',\n      },\n      {\n        name: 'timeout',\n        type: 'integer',\n        optional: true,\n        token: 'TIMEOUT',\n      },\n    ],\n    since: '2.2.8',\n    group: 'generic',\n  },\n}\n\nexport const MOCK_COMMANDS_ARRAY = Object.keys(MOCK_COMMANDS_SPEC).sort()\n"
  },
  {
    "path": "redisinsight/ui/src/constants/mocks/mock-sso.ts",
    "content": "export const MOCK_NO_TF_REGION = {\n  id: 12148,\n  type: 'fixed',\n  name: 'Cache 30MB',\n  provider: 'AWS',\n  price: 0,\n  region: 'us-east-2',\n  regionId: 4,\n  details: {\n    id: 4,\n    name: 'us-east-2',\n    cloud: 'AWS',\n    displayOrder: 4,\n    countryName: 'Europe',\n    cityName: 'Ireland',\n    regionId: 4,\n    flag: 'ie',\n  },\n}\n\nexport const MOCK_RS_PREVIEW_REGION = {\n  id: 12148,\n  type: 'fixed',\n  name: 'Cache 30MB',\n  provider: 'AWS',\n  price: 0,\n  region: 'us-east-2',\n  regionId: 1,\n  details: {\n    id: 1,\n    name: 'us-east-2',\n    cloud: 'AWS',\n    displayOrder: 1,\n    countryName: 'US West',\n    cityName: 'Oregon',\n    regionId: 1,\n    flag: 'ie',\n  },\n}\n\nexport const MOCK_REGIONS = [\n  MOCK_NO_TF_REGION,\n  {\n    id: 12150,\n    type: 'fixed',\n    name: 'Cache 30MB',\n    provider: 'AWS',\n    price: 0,\n    region: 'ap-southeast-1',\n    regionId: 5,\n    details: {\n      id: 5,\n      name: 'ap-southeast-1',\n      cloud: 'AWS',\n      displayOrder: 7,\n      countryName: 'Asia Pacific',\n      cityName: 'Singapore',\n      regionId: 5,\n      flag: 'sg',\n    },\n  },\n  {\n    id: 12152,\n    type: 'fixed',\n    name: 'Cache 30MB',\n    provider: 'Azure',\n    price: 0,\n    region: 'east-us',\n    regionId: 16,\n    details: {\n      id: 16,\n      name: 'east-us',\n      cloud: 'Azure',\n      displayOrder: 10,\n      countryName: 'East US',\n      cityName: 'Virginia',\n      regionId: 16,\n      flag: 'us',\n    },\n  },\n  {\n    id: 12153,\n    type: 'fixed',\n    name: 'Cache 30MB',\n    provider: 'GCP',\n    price: 0,\n    region: 'us-central1',\n    regionId: 27,\n    details: {\n      id: 27,\n      name: 'us-central1',\n      cloud: 'GCP',\n      displayOrder: 17,\n      countryName: 'North America',\n      cityName: 'Iowa',\n      regionId: 27,\n      flag: 'us',\n    },\n  },\n]\n\nexport const MOCK_CUSTOM_REGIONS = [\n  {\n    id: 12150,\n    type: 'fixed',\n    name: 'Cache 30MB',\n    provider: 'AWS',\n    price: 0,\n    region: 'custom-1',\n    regionId: 11,\n    details: {\n      id: 11,\n      name: 'custom-1',\n      cloud: 'AWS',\n      displayOrder: 2,\n      countryName: 'Asia Pacific',\n      cityName: 'Singapore',\n      regionId: 11,\n      flag: 'sg',\n    },\n  },\n  {\n    id: 12152,\n    type: 'fixed',\n    name: 'Cache 30MB',\n    provider: 'Azure',\n    price: 0,\n    region: 'custom-2',\n    regionId: 16,\n    details: {\n      id: 16,\n      name: 'custom-2',\n      cloud: 'Azure',\n      displayOrder: 10,\n      countryName: 'East US',\n      cityName: 'Virginia',\n      regionId: 16,\n      flag: 'us',\n    },\n  },\n  {\n    id: 12153,\n    type: 'fixed',\n    name: 'Cache 30MB',\n    provider: 'GCP',\n    price: 0,\n    region: 'custom-3',\n    regionId: 27,\n    details: {\n      id: 27,\n      name: 'custom-3',\n      cloud: 'GCP',\n      displayOrder: 17,\n      countryName: 'North America',\n      cityName: 'Iowa',\n      regionId: 27,\n      flag: 'us',\n    },\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/mocks/mock-tutorials.ts",
    "content": "import {\n  EnablementAreaComponent,\n  IEnablementAreaItem,\n} from 'uiSrc/slices/interfaces'\n\nexport const MOCK_TUTORIALS_ITEMS: IEnablementAreaItem[] = [\n  {\n    type: EnablementAreaComponent.Group,\n    id: 'tutorials',\n    label: 'Tutorials',\n    children: [\n      {\n        type: EnablementAreaComponent.InternalLink,\n        id: 'document-capabilities',\n        label: 'Document Capabilities',\n        args: {\n          path: '/static/workbench/quick-guides/document/learn-more.md',\n        },\n      },\n      {\n        type: EnablementAreaComponent.InternalLink,\n        id: 'working-with-json',\n        label: 'Working with JSON',\n        args: {\n          path: 'quick-guides/working-with-json.html',\n        },\n      },\n      {\n        type: EnablementAreaComponent.InternalLink,\n        id: 'working-with-hash',\n        label: 'Working with HASH',\n        args: {\n          path: 'quick-guides/working-with-hash.html',\n        },\n      },\n    ],\n  },\n  {\n    type: EnablementAreaComponent.InternalLink,\n    id: 'internal-page',\n    label: 'Internal Page',\n    args: {\n      path: 'quick-guides/document-capabilities.html',\n    },\n  },\n  {\n    type: EnablementAreaComponent.InternalLink,\n    id: 'second-internal-page',\n    label: 'Second Internal Page',\n    args: {\n      path: 'quick-guides/document-capabilities.html',\n    },\n  },\n  {\n    type: EnablementAreaComponent.CodeButton,\n    id: 'manual',\n    label: 'Manual',\n    args: {\n      path: '_scripts/manual.txt',\n    },\n  },\n  {\n    type: EnablementAreaComponent.InternalLink,\n    id: 'working_with_json',\n    label: 'Working with JSON',\n    args: {\n      path: '/redis_stack/working_with_json.md',\n    },\n  },\n]\n\nexport const MOCK_TUTORIALS = {\n  type: EnablementAreaComponent.Group,\n  id: 'quick-guides',\n  label: 'Quick Guides',\n  children: MOCK_TUTORIALS_ITEMS,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/modules.ts",
    "content": "import {\n  DATABASE_LIST_MODULES_TEXT,\n  RedisDefaultModules,\n} from 'uiSrc/slices/interfaces'\nimport { AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\n\n// Define the type for each module info entry\nexport interface ModuleInfo {\n  icon: AllIconsType\n  text: string\n}\n\n// Define the type for the entire modules info object\nexport type ModulesInfoType = {\n  [Key in RedisDefaultModules]: ModuleInfo\n}\n\nconst rediSearchIcons: Omit<ModuleInfo, 'text'> = {\n  icon: 'QuerySearchIcon',\n}\n\nexport const DEFAULT_MODULES_INFO: ModulesInfoType = {\n  [RedisDefaultModules.AI]: {\n    icon: 'RedisAiIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.AI],\n  },\n  [RedisDefaultModules.Bloom]: {\n    icon: 'ProbabilisticIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.Bloom],\n  },\n  [RedisDefaultModules.Gears]: {\n    icon: 'RedisGearsIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.Gears],\n  },\n  [RedisDefaultModules.Graph]: {\n    icon: 'RedisGraphIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.Graph],\n  },\n  [RedisDefaultModules.RedisGears]: {\n    icon: 'RedisGearsIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.RedisGears],\n  },\n  [RedisDefaultModules.RedisGears2]: {\n    icon: 'RedisGears2Icon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.RedisGears2],\n  },\n  [RedisDefaultModules.ReJSON]: {\n    icon: 'JsonIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.ReJSON],\n  },\n  [RedisDefaultModules.Search]: {\n    ...rediSearchIcons,\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.Search],\n  },\n  [RedisDefaultModules.SearchLight]: {\n    ...rediSearchIcons,\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.SearchLight],\n  },\n  [RedisDefaultModules.FT]: {\n    ...rediSearchIcons,\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.FT],\n  },\n  [RedisDefaultModules.FTL]: {\n    ...rediSearchIcons,\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.FTL],\n  },\n  [RedisDefaultModules.TimeSeries]: {\n    icon: 'TimeSeriesIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.TimeSeries],\n  },\n  [RedisDefaultModules.VectorSet]: {\n    icon: 'VectorSetIcon',\n    text: DATABASE_LIST_MODULES_TEXT[RedisDefaultModules.VectorSet],\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/cypher/functions.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport default [\n  {\n    label: 'all',\n    detail:\n      '(variable :: VARIABLE IN list :: LIST OF ANY? WHERE predicate :: ANY?) :: (BOOLEAN?)',\n    documentation:\n      'Returns true if the predicate holds for all elements in the given list.',\n  },\n  {\n    label: 'any',\n    detail:\n      '(variable :: VARIABLE IN list :: LIST OF ANY? WHERE predicate :: ANY?) :: (BOOLEAN?)',\n    documentation:\n      'Returns true if the predicate holds for at least one element in the given list.',\n  },\n  {\n    label: 'exists',\n    detail: '(input :: ANY?) :: (BOOLEAN?)',\n    documentation:\n      'Returns true if a match for the pattern exists in the graph, or if the specified property exists in the node, relationship or map.',\n  },\n  {\n    label: 'isEmpty',\n    detail: '(input :: LIST? OF ANY? | MAP? | STRING?) :: (BOOLEAN?)',\n    documentation: 'Checks whether a list/map/string is empty.',\n  },\n  {\n    label: 'none',\n    detail:\n      '(variable :: VARIABLE IN list :: LIST OF ANY? WHERE predicate :: ANY?) :: (BOOLEAN?)',\n    documentation:\n      'Returns true if the predicate holds for no element in the given list.',\n  },\n  {\n    label: 'single',\n    detail:\n      '(variable :: VARIABLE IN list :: LIST OF ANY? WHERE predicate :: ANY?) :: (BOOLEAN?)',\n    documentation:\n      'Returns true if the predicate holds for exactly one of the elements in the given list.',\n  },\n  {\n    label: 'coalesce',\n    detail: '(input :: ANY?) :: (ANY?)',\n    documentation: 'Returns the first non-null value in a list of expressions.',\n  },\n  {\n    label: 'endNode',\n    detail: '(input :: RELATIONSHIP?) :: (NODE?)',\n    documentation: 'Returns the end node of a relationship.',\n  },\n  {\n    label: 'head',\n    detail: '(list :: LIST? OF ANY?) :: (ANY?)',\n    documentation: 'Returns the first element in a list.',\n  },\n  {\n    label: 'id',\n    detail: '(input :: NODE? | RELATIONSHIP?) :: (INTEGER?)',\n    documentation: 'Returns the id of a node/relationship.',\n  },\n  {\n    label: 'last',\n    detail: '(list :: LIST? OF ANY?) :: (ANY?)',\n    documentation: 'Returns the last element in a list.',\n  },\n  {\n    label: 'length',\n    detail: '(input :: PATH?) :: (INTEGER?)',\n    documentation: 'Returns the length of a path.',\n  },\n  {\n    label: 'properties',\n    detail: '(input :: MAP? | NODE? | RELATIONSHIP?) :: (MAP?)',\n    documentation:\n      'Returns a map containing all the properties of a map/node/relationship.',\n  },\n  {\n    label: 'randomUUID',\n    detail: '() :: (STRING?)',\n    documentation: 'Generates a random UUID.',\n  },\n  {\n    label: 'size',\n    detail: '(input :: LIST? OF ANY?) :: (INTEGER?)',\n    documentation: 'Returns the number of items in a list.',\n  },\n  {\n    label: 'startNode',\n    detail: '(input :: RELATIONSHIP?) :: (NODE?)',\n    documentation: 'Returns the start node of a relationship.',\n  },\n  {\n    label: 'timestamp',\n    detail: '() :: (INTEGER?)',\n    documentation:\n      'Returns the difference, measured in milliseconds, between the current time and midnight, January 1, 1970 UTC.',\n  },\n  {\n    label: 'toBoolean',\n    detail: '(input :: STRING? | BOOLEAN? | INTEGER?) :: (BOOLEAN?)',\n    documentation: 'Converts a string value to a boolean value.',\n  },\n  {\n    label: 'toBooleanOrNull',\n    detail: '(input :: ANY?) :: (BOOLEAN?)',\n    documentation:\n      'Converts a value to a boolean value, or null if the value cannot be converted.',\n  },\n  {\n    label: 'toFloat',\n    detail: '(input :: NUMBER? | STRING?) :: (FLOAT?)',\n    documentation: 'Converts a number value to a floating point value.',\n  },\n  {\n    label: 'toFloatOrNull',\n    detail: '(input :: ANY?) :: (FLOAT?)',\n    documentation:\n      'Converts a value to a floating point value, or null if the value cannot be converted.',\n  },\n  {\n    label: 'toInteger',\n    detail: '(input :: NUMBER? | BOOLEAN? | STRING?) :: (INTEGER?)',\n    documentation: 'Converts a number value to an integer value.',\n  },\n  {\n    label: 'toIntegerOrNull',\n    detail: '(input :: ANY?) :: (INTEGER?)',\n    documentation:\n      'Converts a value to an integer value, or null if the value cannot be converted.',\n  },\n  {\n    label: 'type',\n    detail: '(input :: RELATIONSHIP?) :: (STRING?)',\n    documentation:\n      'Returns the string representation of the relationship type.',\n  },\n  {\n    label: 'avg',\n    detail:\n      '(input :: DURATION? | FLOAT? | INTEGER?) :: (DURATION? | FLOAT? | INTEGER?)',\n    documentation: 'Returns the average of a set of duration values.',\n  },\n  {\n    label: 'collect',\n    detail: '(input :: ANY?) :: (LIST? OF ANY?)',\n    documentation:\n      'Returns a list containing the values returned by an expression.',\n  },\n  {\n    label: 'count',\n    detail: '(input :: ANY?) :: (INTEGER?)',\n    documentation: 'Returns the number of values or rows.',\n  },\n  {\n    label: 'max',\n    detail: '(input :: ANY?) :: (ANY?)',\n    documentation: 'Returns the maximum value in a set of values.',\n  },\n  {\n    label: 'min',\n    detail: '(input :: ANY?) :: (ANY?)',\n    documentation: 'Returns the minimum value in a set of values.',\n  },\n  {\n    label: 'percentileCont',\n    detail: '(input :: FLOAT?, percentile :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns the percentile of a value over a group using linear interpolation.',\n  },\n  {\n    label: 'percentileDisc',\n    detail:\n      '(input :: FLOAT? | INTEGER?, percentile :: FLOAT?) :: (FLOAT? | INTEGER?)',\n    documentation:\n      'Returns the nearest value to the given percentile over a group using a rounding method.',\n  },\n  {\n    label: 'stDev',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns the standard deviation for the given value over a group for a sample of a population.',\n  },\n  {\n    label: 'stDevp',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns the standard deviation for the given value over a group for an entire population.',\n  },\n  {\n    label: 'sum',\n    detail:\n      '(input :: DURATION? | FLOAT? | INTEGER?) :: (DURATION? | FLOAT? | INTEGER?)',\n    documentation: 'Returns the sum of a set of numeric values.',\n  },\n  {\n    label: 'keys',\n    detail: '(input :: MAP? | NODE? | RELATIONSHIP?) :: (LIST? OF STRING?)',\n    documentation:\n      'Returns a list containing the string representations for all the property names of a node, relationship, or map.',\n  },\n  {\n    label: 'labels',\n    detail: '(input :: NODE?) :: (LIST? OF STRING?)',\n    documentation:\n      'Returns a list containing the string representations for all the labels of a node.',\n  },\n  {\n    label: 'nodes',\n    detail: '(input :: PATH?) :: (LIST? OF NODE?)',\n    documentation: 'Returns a list containing all the nodes in a path.',\n  },\n  {\n    label: 'range',\n    detail:\n      '(start :: INTEGER?, end :: INTEGER?, step? :: INTEGER?) :: (LIST? OF INTEGER?)',\n    documentation:\n      'Returns a list comprising all integer values within a specified range.',\n  },\n  {\n    label: 'relationships',\n    detail: '(input :: PATH?) :: (LIST? OF RELATIONSHIP?)',\n    documentation: 'Returns a list containing all the relationships in a path.',\n  },\n  {\n    label: 'reverse',\n    detail: '(input :: LIST? OF ANY?) :: (LIST? OF ANY?)',\n    documentation:\n      'Returns a list in which the order of all elements in the original list have been reversed.',\n  },\n  {\n    label: 'tail',\n    detail: '(input :: LIST? OF ANY?) :: (LIST? OF ANY?)',\n    documentation: 'Returns all but the first element in a list.',\n  },\n  {\n    label: 'toBooleanList',\n    detail: '(input :: LIST? OF ANY?) :: (LIST? OF BOOLEAN?)',\n    documentation:\n      'Converts a list of values to a list of boolean values. If any values are not convertible to boolean they will be null in the list returned.',\n  },\n  {\n    label: 'toFloatList',\n    detail: '(input :: LIST? OF ANY?) :: (LIST? OF FLOAT?)',\n    documentation:\n      'Converts a list of values to a list of floating point values. If any values are not convertible to floating point they will be null in the list returned.',\n  },\n  {\n    label: 'toIntegerList',\n    detail: '(input :: LIST? OF ANY?) :: (LIST? OF INTEGER?)',\n    documentation:\n      'Converts a list of values to a list of integer values. If any values are not convertible to integer they will be null in the list returned.',\n  },\n  {\n    label: 'toStringList',\n    detail: '(input :: LIST? OF ANY?) :: (LIST? OF STRING?)',\n    documentation:\n      'Converts a list of values to a list of string values. If any values are not convertible to string they will be null in the list returned.',\n  },\n  {\n    label: 'abs',\n    detail: '(input :: FLOAT? | INTEGER?) :: (FLOAT? | INTEGER?)',\n    documentation: 'Returns the absolute value of a floating point number.',\n  },\n  {\n    label: 'ceil',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns the smallest floating point number that is greater than or equal to a number and equal to a mathematical integer.',\n  },\n  {\n    label: 'floor',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns the largest floating point number that is less than or equal to a number and equal to a mathematical integer.',\n  },\n  {\n    label: 'rand',\n    detail: '() :: (FLOAT?)',\n    documentation:\n      'Returns a random floating point number in the range from 0 (inclusive) to 1 (exclusive); i.e. [0,1).',\n  },\n  {\n    label: 'round',\n    detail:\n      '(input :: FLOAT?, precision? :: NUMBER?, mode? :: STRING?) :: (FLOAT?)',\n    documentation:\n      'Returns the value of a number rounded to the nearest integer.',\n  },\n  {\n    label: 'sign',\n    detail: '(input :: FLOAT? | INTEGER?) :: (INTEGER?)',\n    documentation:\n      'Returns the signum of a floating point number: 0 if the number is 0, -1 for any negative number, and 1 for any positive number.',\n  },\n  {\n    label: 'e',\n    detail: '() :: (FLOAT?)',\n    documentation: 'Returns the base of the natural logarithm, e.',\n  },\n  {\n    label: 'exp',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns e^n, where e is the base of the natural logarithm, and n is the value of the argument expression.',\n  },\n  {\n    label: 'log',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the natural logarithm of a number.',\n  },\n  {\n    label: 'log10',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the common logarithm (base 10) of a number.',\n  },\n  {\n    label: 'sqrt',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the square root of a number.',\n  },\n  {\n    label: 'acos',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the arccosine of a number in radians.',\n  },\n  {\n    label: 'asin',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the arcsine of a number in radians.',\n  },\n  {\n    label: 'atan',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the arctangent of a number in radians.',\n  },\n  {\n    label: 'atan2',\n    detail: '(y :: FLOAT?, x :: FLOAT?) :: (FLOAT?)',\n    documentation:\n      'Returns the arctangent2 of a set of coordinates in radians.',\n  },\n  {\n    label: 'cos',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the cosine  of a number.',\n  },\n  {\n    label: 'cot',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the cotangent of a number.',\n  },\n  {\n    label: 'degrees',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Converts radians to degrees.',\n  },\n  {\n    label: 'haversin',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns half the versine of a number.',\n  },\n  {\n    label: 'pi',\n    detail: '() :: (FLOAT?)',\n    documentation: 'Returns the mathematical constant pi.',\n  },\n  {\n    label: 'radians',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Converts degrees to radians.',\n  },\n  {\n    label: 'sin',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the sine of a number.',\n  },\n  {\n    label: 'tan',\n    detail: '(input :: FLOAT?) :: (FLOAT?)',\n    documentation: 'Returns the tangent of a number.',\n  },\n  {\n    label: 'left',\n    detail: '(original :: STRING?, length :: INTEGER?) :: (STRING?)',\n    documentation:\n      'Returns a string containing the specified number of leftmost characters of the original string.',\n  },\n  {\n    label: 'lTrim',\n    detail: '(input :: STRING?) :: (STRING?)',\n    documentation:\n      'Returns the original string with leading whitespace removed.',\n  },\n  {\n    label: 'replace',\n    detail:\n      '(original :: STRING?, search :: STRING?, replace :: STRING?) :: (STRING?)',\n    documentation:\n      'Returns a string in which all occurrences of a specified search string in the original string have been replaced by another (specified) replace string.',\n  },\n  {\n    label: 'reverse',\n    detail: '(input :: STRING?) :: (STRING?)',\n    documentation:\n      'Returns a string in which the order of all characters in the original string have been reversed.',\n  },\n  {\n    label: 'right',\n    detail: '(original :: STRING?, length :: INTEGER?) :: (STRING?)',\n    documentation:\n      'Returns a string containing the specified number of rightmost characters of the original string.',\n  },\n  {\n    label: 'rTrim',\n    detail: '(input :: STRING?) :: (STRING?)',\n    documentation:\n      'Returns the original string with trailing whitespace removed.',\n  },\n  {\n    label: 'split',\n    detail:\n      '(original :: STRING?, splitDelimiter :: STRING? | LIST? OF STRING?) :: (LIST? OF STRING?)',\n    documentation:\n      'Returns a list of strings resulting from the splitting of the original string around matches of (any) the given delimiter.',\n  },\n  {\n    label: 'substring',\n    detail:\n      '(original :: STRING?, start :: INTEGER?, length? :: INTEGER?) :: (STRING?)',\n    documentation:\n      \"Returns a substring of (length 'length') the original string, beginning with a 0-based index start.\",\n  },\n  {\n    label: 'toLower',\n    detail: '(input :: STRING?) :: (STRING?)',\n    documentation: 'Returns the original string in lowercase.',\n  },\n  {\n    label: 'toString',\n    detail: '(input :: ANY?) :: (STRING?)',\n    documentation:\n      'Converts an integer, float, boolean, point or temporal type (i.e. Date, Time, LocalTime, DateTime, LocalDateTime or Duration) value to a string.',\n  },\n  {\n    label: 'toStringOrNull',\n    detail: '(input :: ANY?) :: (STRING?)',\n    documentation:\n      'Converts an integer, float, boolean, point or temporal type (i.e. Date, Time, LocalTime, DateTime, LocalDateTime or Duration) value to a string, or null if the value cannot be converted.',\n  },\n  {\n    label: 'toUpper',\n    detail: '(input :: STRING?) :: (STRING?)',\n    documentation: 'Returns the original string in uppercase.',\n  },\n  {\n    label: 'trim',\n    detail: '(input :: STRING?) :: (STRING?)',\n    documentation:\n      'Returns the original string with leading and trailing whitespace removed.',\n  },\n] as monacoEditor.languages.CompletionItem[]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/cypher/index.ts",
    "content": "export * from './monacoCypher'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/cypher/monacoCypher.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport cypherFunctions from './functions'\n\nexport const cypherLanguageConfiguration: monacoEditor.languages.LanguageConfiguration =\n  {\n    brackets: [\n      ['(', ')'],\n      ['{', '}'],\n      ['[', ']'],\n      [\"'\", \"'\"],\n      ['\"', '\"'],\n    ],\n    comments: {\n      blockComment: ['/*', '*/'],\n      lineComment: '//',\n    },\n  }\n\nexport const KEYWORDS = [\n  'ACCESS',\n  'ACTIVE',\n  'ADMIN',\n  'ADMINISTRATOR',\n  'ALLSHORTESTPATHS',\n  'ALTER',\n  'AND',\n  'AS',\n  'ASC',\n  'ASCENDING',\n  'ASSERT',\n  'ASSIGN',\n  'BOOSTED',\n  'BRIEF',\n  'BTREE',\n  'BUILT',\n  'BY',\n  'CALL',\n  'CASE',\n  'CATALOG',\n  'CHANGE',\n  'COMMIT',\n  'CONSTRAINT',\n  'CONSTRAINTS',\n  'CONTAINS',\n  'COPY',\n  'CREATE',\n  'CSV',\n  'CURRENT',\n  'CYPHER',\n  'DATABASE',\n  'DATABASES',\n  'DBMS',\n  'DEFAULT',\n  'DEFINED',\n  'DELETE',\n  'DENY',\n  'DESC',\n  'DESCENDING',\n  'DETACH',\n  'DISTINCT',\n  'DROP',\n  'EACH',\n  'ELEMENT',\n  'ELEMENTS',\n  'ELSE',\n  'END',\n  'ENDS',\n  'EXECUTABLE',\n  'EXECUTE',\n  'EXISTENCE',\n  'EXPLAIN',\n  'EXTRACT',\n  'FALSE',\n  'FIELDTERMINATOR',\n  'FILTER',\n  'FOR',\n  'FOREACH',\n  'FROM',\n  'FULLTEXT',\n  'FUNCTION',\n  'FUNCTIONS',\n  'GRANT',\n  'GRAPH',\n  'GRAPHS',\n  'HEADERS',\n  'HOME',\n  'IF',\n  'IN',\n  'INDEX',\n  'INDEXES',\n  'IS',\n  'JOIN',\n  'KEY',\n  'LABEL',\n  'LIMIT',\n  'LOAD',\n  'LOOKUP',\n  'MANAGEMENT',\n  'MATCH',\n  'MERGE',\n  'NAME',\n  'NAMES',\n  'NEW',\n  'NODE',\n  'NOT',\n  'NULL',\n  'OF',\n  'ON',\n  'OPTIONAL',\n  'OPTIONS',\n  'OR',\n  'ORDER',\n  'OUTPUT',\n  'PASSWORD',\n  'PERIODIC',\n  'POPULATED',\n  'PRIVILEGES',\n  'PROCEDURE',\n  'PROCEDURES',\n  'PROFILE',\n  'PROPERTY',\n  'READ',\n  'REDUCE',\n  'REL',\n  'RELATIONSHIP',\n  'REMOVE',\n  'RENAME',\n  'REQUIRED',\n  'RETURN',\n  'REVOKE',\n  'ROLE',\n  'ROLES',\n  'SCAN',\n  'SET',\n  'SHORTESTPATH',\n  'SHOW',\n  'SKIP',\n  'START',\n  'STARTS',\n  'STATUS',\n  'STOP',\n  'SUSPENDED',\n  'THEN',\n  'TO',\n  'TRAVERSE',\n  'TRUE',\n  'TYPES',\n  'UNION',\n  'UNIQUE',\n  'UNWIND',\n  'USER',\n  'USERS',\n  'USING',\n  'VERBOSE',\n  'WHEN',\n  'WHERE',\n  'WITH',\n  'WRITE',\n  'XOR',\n  'YIELD',\n]\n\nexport const FUNCTIONS: monacoEditor.languages.CompletionItem[] =\n  cypherFunctions\n\nexport const STRINGS: string[] = ['stringliteral', 'urlhex']\n\nexport const NUMBERS: string[] = [\n  'hexinteger',\n  'decimalinteger',\n  'octalinteger',\n  'hexdigit',\n  'digit',\n  'nonzerodigit',\n  'nonzerooctdigit',\n  'octdigit',\n  'zerodigit',\n  'exponentdecimalreal',\n  'regulardecimalreal',\n]\n\nexport const OPERATORS: string[] = [\n  'identifierstart',\n  'identifierpart',\n  \"';'\",\n  \"':'\",\n  \"'-'\",\n  \"'=>'\",\n  \"'://'\",\n  \"'/'\",\n  \"'.'\",\n  \"'@'\",\n  \"'#'\",\n  \"'?'\",\n  \"'&'\",\n  \"'='\",\n  \"'+'\",\n  \"'{'\",\n  \"','\",\n  \"'}'\",\n  \"'['\",\n  \"']'\",\n  \"'('\",\n  \"')'\",\n  \"'+='\",\n  \"'|'\",\n  \"'*'\",\n  \"'..'\",\n  \"'%'\",\n  \"'^'\",\n  \"'=~'\",\n  \"'<>'\",\n  \"'!='\",\n  \"'<'\",\n  \"'>'\",\n  \"'<='\",\n  \"'>='\",\n  \"'$'\",\n  \"'\\u27E8'\",\n  \"'\\u3008'\",\n  \"'\\uFE64'\",\n  \"'\\uFF1C'\",\n  \"'\\u27E9'\",\n  \"'\\u3009'\",\n  \"'\\uFE65'\",\n  \"'\\uFF1E'\",\n  \"'\\u00AD'\",\n  \"'\\u2010'\",\n  \"'\\u2011'\",\n  \"'\\u2012'\",\n  \"'\\u2013'\",\n  \"'\\u2014'\",\n  \"'\\u2015'\",\n  \"'\\u2212'\",\n  \"'\\uFE58'\",\n  \"'\\uFE63'\",\n  \"'\\uFF0D'\",\n]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/index.ts",
    "content": "export * from './theme'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/jmespath/index.ts",
    "content": "export * from './monacoJmespath'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/jmespath/monacoJmespath.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport const jmespathLanguageConfiguration: monacoEditor.languages.LanguageConfiguration =\n  {\n    brackets: [\n      ['(', ')'],\n      ['{', '}'],\n      ['[', ']'],\n      [\"'\", \"'\"],\n      ['\"', '\"'],\n    ],\n    comments: {\n      blockComment: ['/*', '*/'],\n      lineComment: '//',\n    },\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/monaco.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { getCompletionProvider } from 'uiSrc/utils/monaco/completionProvider'\nimport { getCypherMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/cypherTokens'\nimport { getJmespathMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/jmespathTokens'\nimport { getSqliteFunctionsMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/sqliteFunctionsTokens'\nimport {\n  cypherLanguageConfiguration,\n  KEYWORDS as cypherKeywords,\n  FUNCTIONS as cypherFunctions,\n} from './cypher'\nimport { jmespathLanguageConfiguration } from './jmespath'\nimport {\n  sqliteFunctionsLanguageConfiguration,\n  SQLITE_FUNCTIONS,\n} from './sqliteFunctions'\nimport { DSL, DSLNaming } from '../commands'\n\nexport interface MonacoSyntaxLang {\n  name: string\n  id: string\n  language: string\n  config?: monacoEditor.languages.LanguageConfiguration\n  completionProvider?: (\n    keywords?: string[],\n    functions?: monacoEditor.languages.CompletionItem[],\n  ) => monacoEditor.languages.CompletionItemProvider\n  tokensProvider?: (\n    keywords?: string[],\n    functions?: monacoEditor.languages.CompletionItem[],\n  ) => monacoEditor.languages.IMonarchLanguage\n}\n\nexport interface MonacoSyntaxObject {\n  [key: string]: MonacoSyntaxLang\n}\n\nexport enum MonacoLanguage {\n  Redis = 'redisLanguage',\n  Cypher = 'cypherLanguage',\n  JMESPath = 'jmespathLanguage',\n  SQLiteFunctions = 'sqliteFunctions',\n  Text = 'text',\n  RediSearch = 'redisearch',\n}\n\nexport const defaultMonacoOptions: monacoEditor.editor.IStandaloneEditorConstructionOptions =\n  {\n    tabCompletion: 'on',\n    wordWrap: 'on',\n    padding: { top: 10 },\n    automaticLayout: true,\n    formatOnPaste: false,\n    glyphMargin: true,\n    fixedOverflowWidgets: true,\n    bracketPairColorization: {\n      enabled: true,\n      independentColorPoolPerBracketType: true,\n    },\n    stickyScroll: {\n      enabled: true,\n      defaultModel: 'indentationModel',\n    },\n    suggest: {\n      preview: true,\n      showStatusBar: true,\n      showIcons: false,\n    },\n    minimap: {\n      enabled: false,\n    },\n    lineNumbersMinChars: 4,\n  }\n\nexport const DEFAULT_MONACO_YAML_URI = 'http://example.com/schema-name.json'\nexport const DEFAULT_MONACO_FILE_MATCH = '*'\n\nexport const DEDICATED_EDITOR_LANGUAGES: MonacoSyntaxObject = {\n  [DSL.cypher]: {\n    name: DSLNaming[DSL.cypher],\n    id: MonacoLanguage.Cypher,\n    language: MonacoLanguage.Cypher,\n    config: cypherLanguageConfiguration,\n    completionProvider: () => ({\n      ...getCompletionProvider(cypherKeywords, cypherFunctions),\n    }),\n    tokensProvider: getCypherMonarchTokensProvider,\n  },\n  [DSL.sqliteFunctions]: {\n    name: DSLNaming[DSL.sqliteFunctions],\n    id: DSL.sqliteFunctions,\n    language: MonacoLanguage.SQLiteFunctions,\n    config: sqliteFunctionsLanguageConfiguration,\n    completionProvider: () => ({\n      ...getCompletionProvider([], SQLITE_FUNCTIONS),\n    }),\n    tokensProvider: () => ({\n      ...getSqliteFunctionsMonarchTokensProvider(\n        SQLITE_FUNCTIONS.map(({ label }) => label.toString()),\n      ),\n    }),\n  },\n  [DSL.jmespath]: {\n    name: DSLNaming[DSL.jmespath],\n    id: DSL.jmespath,\n    language: MonacoLanguage.JMESPath,\n    config: jmespathLanguageConfiguration,\n    completionProvider: (\n      keywords: string[] = [],\n      functions: monacoEditor.languages.CompletionItem[] = [],\n    ) => ({\n      ...getCompletionProvider(keywords, functions),\n    }),\n    tokensProvider: (\n      _,\n      functions: monacoEditor.languages.CompletionItem[] = [],\n    ) => ({\n      ...getJmespathMonarchTokensProvider(\n        functions.map(({ label }) => label.toString()),\n      ),\n    }),\n  },\n}\n\nexport const MONACO_MANUAL =\n  '// Workbench is the advanced Redis command-line interface that allows to send commands to Redis, read and visualize the replies sent by the server.\\n' +\n  '// Enter multiple commands at different rows to run them at once.\\n' +\n  '// Start a new line with an indent (Tab) to specify arguments for any Redis command in multiple line mode.\\n' +\n  '// Use F1 to see the full list of shortcuts available in Workbench.\\n' +\n  '// Use Ctrl+Space (Cmd+Space) to see the list of commands and information about commands and their arguments in the suggestion list.\\n' +\n  '// Use Ctrl+Shift+Space (Cmd+Shift+Space) to see the list of arguments for commands.\\n'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/monacoRedis.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport const redisLanguageConfig: monacoEditor.languages.LanguageConfiguration =\n  {\n    wordPattern: /\\w+\\.?(\\w?)+/g,\n    comments: {\n      lineComment: '//',\n      // blockComment: ['/*', '*/'],\n    },\n    brackets: [\n      ['{', '}'],\n      ['[', ']'],\n      ['(', ')'],\n    ],\n    autoClosingPairs: [\n      { open: '{', close: '}' },\n      { open: '[', close: ']' },\n      { open: '(', close: ')' },\n      { open: '\"', close: '\"' },\n      { open: \"'\", close: \"'\" },\n    ],\n    surroundingPairs: [\n      { open: '{', close: '}' },\n      { open: '[', close: ']' },\n      { open: '(', close: ')' },\n      { open: '\"', close: '\"' },\n      { open: \"'\", close: \"'\" },\n    ],\n    folding: {\n      offSide: true,\n      markers: {\n        start: new RegExp('^\\\\s*#region\\\\b'),\n        end: new RegExp('^\\\\s*#endregion\\\\b'),\n      },\n    },\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/monitorEvents.ts",
    "content": "export enum MonitorEvent {\n  Monitor = 'monitor',\n  MonitorData = 'monitorData',\n  Exception = 'exception',\n  Pause = 'pause',\n  FlushLogs = 'flushLogs',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/sqliteFunctions/functions.ts",
    "content": "export default {\n  date: {\n    summary: 'returns the date as text in this format: YYYY-MM-DD',\n    arguments: [\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'optional time value',\n        optional: false,\n      },\n      {\n        name: 'modifier',\n        type: 'string',\n        display_text: 'optional one or more modifiers',\n        optional: true,\n      },\n    ],\n  },\n  time: {\n    summary: 'returns the time as text in this format: HH:MM:SS',\n    arguments: [\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'optional time value',\n        optional: false,\n      },\n      {\n        name: 'modifier',\n        type: 'string',\n        display_text: 'optional one or more modifiers',\n        optional: true,\n      },\n    ],\n  },\n  datetime: {\n    summary:\n      'returns the date and time as text in this formats: YYYY-MM-DD HH:MM:SS',\n    arguments: [\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'optional time value',\n        optional: false,\n      },\n      {\n        name: 'modifier',\n        type: 'string',\n        display_text: 'optional one or more modifiers',\n        optional: true,\n      },\n    ],\n  },\n  julianday: {\n    summary:\n      'the fractional number of days since noon in Greenwich on November 24, 4714 B.C.',\n    arguments: [\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'optional time value',\n        optional: false,\n      },\n      {\n        name: 'modifier',\n        type: 'string',\n        display_text: 'optional one or more modifiers',\n        optional: true,\n      },\n    ],\n  },\n  unixepoch: {\n    summary:\n      'returns a unix timestamp - the number of seconds since 1970-01-01 00:00:00 UTC.',\n    arguments: [\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'optional time value',\n        optional: false,\n      },\n      {\n        name: 'modifier',\n        type: 'string',\n        display_text: 'optional one or more modifiers',\n        optional: true,\n      },\n    ],\n  },\n  strftime: {\n    summary:\n      'returns the date formatted according to the format string specified as the first argument. The format string supports the most common substitutions found in the strftime() function from the standard C library plus two new substitutions, %f and %J',\n    arguments: [\n      {\n        name: 'format',\n        type: 'string',\n        display_text:\n          'supports the most common substitutions found in the strftime() function',\n        optional: false,\n      },\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'optional time value',\n        optional: false,\n      },\n      {\n        name: 'modifier',\n        type: 'string',\n        display_text: 'optional one or more modifiers',\n        optional: true,\n      },\n    ],\n  },\n  timediff: {\n    summary:\n      'returns a string that describes the amount of time that must be added to B in order to reach time A. The format of the timediff() result is designed to be human-readable. The format is:(+|-)YYYY-MM-DD HH:MM:SS.SSS',\n    arguments: [\n      {\n        name: 'time_value',\n        type: 'number',\n        display_text: 'time to start from',\n        optional: false,\n      },\n    ],\n  },\n  avg: {\n    summary:\n      'returns the average value of all non-NULL X within a group. String and BLOB values that do not look like numbers are interpreted as 0.',\n    arguments: [\n      {\n        name: 'column_name',\n        type: 'string',\n        display_text: 'column name',\n        optional: false,\n      },\n    ],\n  },\n  count: {\n    summary:\n      'returns a count of the number of times that X is not NULL in a group.',\n    arguments: [\n      {\n        name: 'column_name',\n        type: 'string',\n        display_text: 'column name',\n        optional: false,\n      },\n    ],\n  },\n  group_concat: {\n    summary:\n      \"The group_concat() function returns a string which is the concatenation of all non-NULL values of X. If parameter Y is present then it is used as the separator between instances of X.A comma (',') is used as the separator if Y is omitted.\",\n    arguments: [\n      {\n        name: 'column_name',\n        type: 'string',\n        display_text: 'column name',\n        optional: false,\n      },\n      {\n        name: 'separator',\n        type: 'string',\n        display_text: 'separator',\n        optional: true,\n      },\n    ],\n  },\n  string_agg: {\n    summary:\n      \"The group_concat() function returns a string which is the concatenation of all non-NULL values of X. If parameter Y is present then it is used as the separator between instances of X.A comma (',') is used as the separator if Y is omitted.\",\n    arguments: [\n      {\n        name: 'column_name',\n        type: 'string',\n        display_text: 'column name',\n        optional: false,\n      },\n      {\n        name: 'separator',\n        type: 'string',\n        display_text: 'separator',\n        optional: true,\n      },\n    ],\n  },\n  sum: {\n    summary: 'return the sum of all non-NULL values in the group.',\n    arguments: [\n      {\n        name: 'column_name',\n        type: 'string',\n        display_text: 'column name',\n        optional: false,\n      },\n    ],\n  },\n  total: {\n    summary: 'return the sum of all non-NULL values in the group.',\n    arguments: [\n      {\n        name: 'column_name',\n        type: 'string',\n        display_text: 'column name',\n        optional: false,\n      },\n    ],\n  },\n  abs: {\n    summary: 'returns the absolute value of the numeric argument X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'number',\n        display_text: 'numeric argument',\n        optional: false,\n      },\n    ],\n  },\n  char: {\n    summary:\n      'returns a string composed of characters having the unicode code point values of integers X1 through XN, respectively.',\n    arguments: [\n      {\n        name: 'X1, X2, ..., XN',\n        type: 'number',\n        display_text: 'unicode code point values',\n        optional: false,\n      },\n    ],\n  },\n  coalesce: {\n    summary:\n      'returns a copy of its first non-NULL argument, or NULL if all arguments are NULL. Coalesce() must have at least 2 arguments.',\n    arguments: [\n      {\n        name: 'X, Y, ...',\n        type: 'any',\n        display_text: 'arguments to evaluate',\n        optional: false,\n      },\n    ],\n  },\n  concat: {\n    summary:\n      'returns a string which is the concatenation of the string representation of all of its non-NULL arguments. If all arguments are NULL, then concat() returns an empty string.',\n    arguments: [\n      {\n        name: 'X, ...',\n        type: 'any',\n        display_text: 'arguments to concatenate',\n        optional: false,\n      },\n    ],\n  },\n  concat_ws: {\n    summary:\n      'returns a string that is the concatenation of all non-null arguments beyond the first argument, using the text value of the first argument as a separator.',\n    arguments: [\n      {\n        name: 'SEP',\n        type: 'string',\n        display_text: 'separator',\n        optional: false,\n      },\n      {\n        name: 'X, ...',\n        type: 'any',\n        display_text: 'arguments to concatenate',\n        optional: false,\n      },\n    ],\n  },\n  format: {\n    summary: 'works like printf() function from the standard C library.',\n    arguments: [\n      {\n        name: 'FORMAT, ...',\n        type: 'string',\n        display_text: 'format string and arguments',\n        optional: false,\n      },\n    ],\n  },\n  glob: {\n    summary: 'function is equivalent to the expression \"Y GLOB X\".',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'pattern',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'string to match',\n        optional: false,\n      },\n    ],\n  },\n  hex: {\n    summary:\n      'interprets its argument as a BLOB and returns a string which is the upper-case hexadecimal rendering of the content of that blob.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'value to convert',\n        optional: false,\n      },\n    ],\n  },\n  ifnull: {\n    summary:\n      'returns a copy of its first non-NULL argument, or NULL if both arguments are NULL.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'first argument',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'any',\n        display_text: 'second argument',\n        optional: false,\n      },\n    ],\n  },\n  iif: {\n    summary: 'returns the value Y if X is true, and Z otherwise.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'condition',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'any',\n        display_text: 'value if true',\n        optional: false,\n      },\n      {\n        name: 'Z',\n        type: 'any',\n        display_text: 'value if false',\n        optional: false,\n      },\n    ],\n  },\n  instr: {\n    summary:\n      'finds the first occurrence of string Y within string X and returns the number of prior characters plus 1, or 0 if Y is nowhere found within X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to search',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'string to find',\n        optional: false,\n      },\n    ],\n  },\n  length: {\n    summary:\n      'returns the number of characters (not bytes) in X prior to the first NUL character.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to measure',\n        optional: false,\n      },\n    ],\n  },\n  like: {\n    summary: 'is used to implement the \"Y LIKE X [ESCAPE Z]\" expression.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'pattern',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'string to match',\n        optional: false,\n      },\n      {\n        name: 'Z',\n        type: 'string',\n        display_text: 'escape character',\n        optional: true,\n      },\n    ],\n  },\n  likelihood: {\n    summary:\n      'returns argument X unchanged. The value Y in likelihood(X,Y) must be a floating point constant between 0.0 and 1.0, inclusive.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'value to return',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'number',\n        display_text: 'likelihood',\n        optional: false,\n      },\n    ],\n  },\n  likely: {\n    summary:\n      'returns the argument X unchanged. The likely(X) function is a no-op that the code generator optimizes away so that it consumes no CPU cycles at run-time.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'value to return',\n        optional: false,\n      },\n    ],\n  },\n  lower: {\n    summary:\n      'returns a copy of string X with all ASCII characters converted to lower case.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to convert',\n        optional: false,\n      },\n    ],\n  },\n  ltrim: {\n    summary:\n      'returns a string formed by removing any and all characters that appear in Y from the left side of X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to trim',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'characters to remove',\n        optional: true,\n      },\n    ],\n  },\n  max: {\n    summary:\n      'returns the argument with the maximum value, or return NULL if any argument is NULL.',\n    arguments: [\n      {\n        name: 'X, Y, ...',\n        type: 'any',\n        display_text: 'arguments to compare',\n        optional: false,\n      },\n    ],\n  },\n  min: {\n    summary:\n      'returns the argument with the minimum value. The multi-argument min() function searches its arguments from left to right for an argument that defines a collating function and uses that collating function for all string comparisons.',\n    arguments: [\n      {\n        name: 'X, Y, ...',\n        type: 'any',\n        display_text: 'arguments to compare',\n        optional: false,\n      },\n    ],\n  },\n  nullif: {\n    summary:\n      'returns its first argument if the arguments are different and NULL if the arguments are the same. The nullif(X,Y) function searches its arguments from left to right for an argument that defines a collating function and uses that collating function for all string comparisons. If neither argument to nullif() defines a collating function then the BINARY collating function is used.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'first argument',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'any',\n        display_text: 'second argument',\n        optional: false,\n      },\n    ],\n  },\n  octet_length: {\n    summary:\n      'returns the number of bytes in the encoding of text string X. If X is NULL then octet_length(X) returns NULL. If X is a BLOB value, then octet_length(X) is the same as length(X). If X is a numeric value, then octet_length(X) returns the number of bytes in a text rendering of that number.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'value to measure',\n        optional: false,\n      },\n    ],\n  },\n  printf: {\n    summary: 'is an alias for the format() SQL function.',\n    arguments: [\n      {\n        name: 'FORMAT, ...',\n        type: 'string',\n        display_text: 'format string and arguments',\n        optional: false,\n      },\n    ],\n  },\n  quote: {\n    summary:\n      'returns the text of an SQL literal which is the value of its argument suitable for inclusion into an SQL statement. Strings are surrounded by single-quotes with escapes on interior quotes as needed. BLOBs are encoded as hexadecimal literals. Strings with embedded NUL characters cannot be represented as string literals in SQL and hence the returned string literal is truncated prior to the first NUL.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'value to quote',\n        optional: false,\n      },\n    ],\n  },\n  random: {\n    summary:\n      'returns a pseudo-random integer between -9223372036854775808 and +9223372036854775807.',\n    arguments: [],\n  },\n  randomblob: {\n    summary:\n      'The randomblob(N) function return an N-byte blob containing pseudo-random bytes. If N is less than 1 then a 1-byte random blob is returned.',\n    arguments: [\n      {\n        name: 'N',\n        type: 'number',\n        display_text: 'number of bytes',\n        optional: false,\n      },\n    ],\n  },\n  replace: {\n    summary:\n      'returns a string formed by substituting string Z for every occurrence of string Y in string X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'original string',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'string to replace',\n        optional: false,\n      },\n      {\n        name: 'Z',\n        type: 'string',\n        display_text: 'replacement string',\n        optional: false,\n      },\n    ],\n  },\n  round: {\n    summary:\n      'returns a floating-point value X rounded to Y digits to the right of the decimal point. If the Y argument is omitted or negative, it is taken to be 0.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'number',\n        display_text: 'value to round',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'number',\n        display_text: 'number of digits',\n        optional: true,\n      },\n    ],\n  },\n  rtrim: {\n    summary:\n      'returns a string formed by removing any and all characters that appear in Y from the right side of X. If the Y argument is omitted, rtrim(X) removes spaces from the right side of X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to trim',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'characters to remove',\n        optional: true,\n      },\n    ],\n  },\n  sign: {\n    summary:\n      'returns -1, 0, or +1 if the argument X is a numeric value that is negative, zero, or positive, respectively. If the argument to sign(X) is NULL or is a string or blob that cannot be losslessly converted into a number, then sign(X) returns NULL.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'number',\n        display_text: 'value to evaluate',\n        optional: false,\n      },\n    ],\n  },\n  soundex: {\n    summary: 'returns a string that is the soundex encoding of the string X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to encode',\n        optional: false,\n      },\n    ],\n  },\n  substr: {\n    summary:\n      'returns a substring of input string X that begins with the Y-th character and which is Z characters long. If Z is omitted then substr(X,Y) returns all characters through the end of the string X beginning with the Y-th. The left-most character of X is number 1. If Y is negative then the first character of the substring is found by counting from the right rather than the left. If Z is negative then the abs(Z) characters preceding the Y-th character are returned. If X is a string then characters indices refer to actual UTF-8 characters. If X is a BLOB then the indices refer to bytes.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'original string',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'number',\n        display_text: 'start position',\n        optional: false,\n      },\n      {\n        name: 'Z',\n        type: 'number',\n        display_text: 'length',\n        optional: true,\n      },\n    ],\n  },\n  trim: {\n    summary:\n      'function returns a string formed by removing any and all characters that appear in Y from both ends of X. If the Y argument is omitted, trim(X) removes spaces from both ends of X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to trim',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'characters to remove',\n        optional: true,\n      },\n    ],\n  },\n  unhex: {\n    summary:\n      'returns a BLOB value which is the decoding of the hexadecimal string X. If X contains any characters that are not hexadecimal digits and which are not in Y, then unhex(X,Y) returns NULL. If Y is omitted, it is understood to be an empty string and hence X must be a pure hexadecimal string.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'hexadecimal string',\n        optional: false,\n      },\n      {\n        name: 'Y',\n        type: 'string',\n        display_text: 'allowed characters',\n        optional: true,\n      },\n    ],\n  },\n  unicode: {\n    summary:\n      'function returns the numeric unicode code point corresponding to the first character of the string X.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to evaluate',\n        optional: false,\n      },\n    ],\n  },\n  unlikely: {\n    summary: 'returns the argument X unchanged.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'any',\n        display_text: 'value to return',\n        optional: false,\n      },\n    ],\n  },\n  upper: {\n    summary:\n      'returns a copy of input string X in which all lower-case ASCII characters are converted to their upper-case equivalent.',\n    arguments: [\n      {\n        name: 'X',\n        type: 'string',\n        display_text: 'string to convert',\n        optional: false,\n      },\n    ],\n  },\n  zeroblob: {\n    summary: 'returns a BLOB consisting of N bytes of 0x00.',\n    arguments: [\n      {\n        name: 'N',\n        type: 'number',\n        display_text: 'number of bytes',\n        optional: false,\n      },\n    ],\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/sqliteFunctions/index.ts",
    "content": "export * from './monacoSQLiteFunctions'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/sqliteFunctions/monacoSQLiteFunctions.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport sqliteFunctions from './functions'\nimport {\n  generateArgsNames,\n  generateArgsForInsertText,\n  getCommandMarkdown,\n} from '../../../utils/commands'\n\nexport const sqliteFunctionsLanguageConfiguration: monacoEditor.languages.LanguageConfiguration =\n  {\n    brackets: [\n      ['(', ')'],\n      ['{', '}'],\n      ['[', ']'],\n      [\"'\", \"'\"],\n      ['\"', '\"'],\n    ],\n  }\n\nconst sqliteFunctionsParsed: monacoEditor.languages.CompletionItem[] =\n  Object.entries(sqliteFunctions).map(([labelInit, func]) => {\n    const { arguments: args } = func\n    const label = labelInit.toUpperCase()\n    const range = {\n      startLineNumber: 0,\n      endLineNumber: 0,\n      startColumn: 0,\n      endColumn: 0,\n    }\n    const detail = `${label}(${generateArgsNames('', args).join(', ')})`\n    const argsNames = generateArgsNames('', args, false, true)\n    const insertText = `${label}(${generateArgsForInsertText(argsNames, ', ')})`\n\n    return {\n      label,\n      detail,\n      range,\n      documentation: {\n        value: getCommandMarkdown(func),\n      },\n      insertText,\n      kind: monacoEditor.languages.CompletionItemKind.Function,\n      insertTextRules:\n        monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    }\n  })\n\nexport const SQLITE_FUNCTIONS: monacoEditor.languages.CompletionItem[] =\n  sqliteFunctionsParsed\n"
  },
  {
    "path": "redisinsight/ui/src/constants/monaco/theme.ts",
    "content": "import { monaco } from 'react-monaco-editor'\n\nexport const redisearchDarKThemeRules = [\n  { token: 'keyword', foreground: '#C8B5F2' },\n  { token: 'argument.block.0', foreground: '#8CD7B9' },\n  { token: 'argument.block.1', foreground: '#72B59B' },\n  { token: 'argument.block.2', foreground: '#3A8365' },\n  { token: 'argument.block.3', foreground: '#244F3E' },\n  { token: 'argument.block.withToken.0', foreground: '#8CD7B9' },\n  { token: 'argument.block.withToken.1', foreground: '#72B59B' },\n  { token: 'argument.block.withToken.2', foreground: '#3A8365' },\n  { token: 'argument.block.withToken.3', foreground: '#244F3E' },\n  { token: 'index', foreground: '#DE47BB' },\n  { token: 'query', foreground: '#7B90E0' },\n  { token: 'field', foreground: '#B02C30' },\n  { token: 'query.operator', foreground: '#B9F0F3' },\n  { token: 'function', foreground: '#9E7EE8' },\n]\n\nexport const redisearchLightThemeRules = [\n  { token: 'keyword', foreground: '#7547DE' },\n  { token: 'argument.block.0', foreground: '#8CD7B9' },\n  { token: 'argument.block.1', foreground: '#72B59B' },\n  { token: 'argument.block.2', foreground: '#3A8365' },\n  { token: 'argument.block.3', foreground: '#244F3E' },\n  { token: 'argument.block.withToken.0', foreground: '#8CD7B9' },\n  { token: 'argument.block.withToken.1', foreground: '#72B59B' },\n  { token: 'argument.block.withToken.2', foreground: '#3A8365' },\n  { token: 'argument.block.withToken.3', foreground: '#244F3E' },\n  { token: 'index', foreground: '#DE47BB' },\n  { token: 'query', foreground: '#7B90E0' },\n  { token: 'field', foreground: '#B02C30' },\n  { token: 'query.operator', foreground: '#B9F0F3' },\n  { token: 'function', foreground: '#9E7EE8' },\n]\n\nexport const darkThemeRules = [\n  { token: 'function', foreground: 'BFBC4E' },\n  ...redisearchDarKThemeRules.map((rule) => ({\n    ...rule,\n  })),\n]\n\nexport const lightThemeRules = [\n  { token: 'function', foreground: '795E26' },\n  ...redisearchLightThemeRules.map((rule) => ({\n    ...rule,\n  })),\n]\n\nexport enum MonacoThemes {\n  Dark = 'dark',\n  Light = 'light',\n}\n\nexport const darkTheme: monaco.editor.IStandaloneThemeData = {\n  base: 'vs-dark',\n  inherit: true,\n  rules: darkThemeRules,\n  colors: {},\n}\n\nexport const lightTheme: monaco.editor.IStandaloneThemeData = {\n  base: 'vs',\n  inherit: true,\n  rules: lightThemeRules,\n  colors: {},\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/notifications.ts",
    "content": "export enum NotificationEvent {\n  Notification = 'notification',\n}\n\nexport const NOTIFICATION_DATE_FORMAT = 'dd MMM yyyy'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/onboarding.ts",
    "content": "enum OnboardingSteps {\n  Start,\n  BrowserPage,\n  BrowserTreeView,\n  BrowserFilterSearch,\n  BrowserCopilot,\n  BrowserCLI,\n  BrowserCommandHelper,\n  BrowserProfiler,\n  // BrowserInsights,\n  VectorSearchPage,\n  WorkbenchPage,\n  Tutorials,\n  CustomTutorials,\n  AnalyticsOverview,\n  AnalyticsDatabaseAnalysis,\n  AnalyticsRecommendations,\n  AnalyticsSlowLog,\n  PubSubPage,\n  Finish,\n}\n\nenum OnboardingStepName {\n  Start = 'start',\n  BrowserWithKeys = 'browser_with_keys',\n  BrowserWithoutKeys = 'browser_without_keys',\n  BrowserTreeView = 'browser_tree_view',\n  BrowserFilters = 'browser_filters',\n  BrowserCopilot = 'browser_copilot',\n  BrowserCLI = 'browser_cli',\n  BrowserCommandHelper = 'browser_command_helper',\n  BrowserProfiler = 'browser_profiler',\n  BrowserInsights = 'browser_insights',\n  VectorSearchIntro = 'vector_search_intro',\n  WorkbenchIntro = 'workbench_intro',\n  ExploreTutorials = 'insights_explore_tutorials',\n  ExploreCustomTutorials = 'insights_explore_custom_tutorials',\n  ClusterOverview = 'cluster_overview',\n  DatabaseAnalysisOverview = 'database_analysis_overview',\n  DatabaseAnalysisRecommendations = 'database_analysis_tips',\n  SlowLog = 'slow_log',\n  PubSub = 'pub_sub',\n  Finish = 'finish',\n}\n\nexport { OnboardingSteps, OnboardingStepName }\n"
  },
  {
    "path": "redisinsight/ui/src/constants/pages.ts",
    "content": "import { FeatureFlags } from 'uiSrc/constants'\nimport { Maybe } from 'uiSrc/utils'\n\nexport interface IRoute {\n  path: any\n  component?: (routes: any) => JSX.Element | Element | null\n  pageName?: PageNames\n  exact?: boolean\n  routes?: any\n  protected?: boolean\n  redirect?: (params: Record<string, Maybe<string>>) => string\n  isAvailableWithoutAgreements?: boolean\n  featureFlag?: FeatureFlags\n}\n\nexport enum PageNames {\n  workbench = 'workbench',\n  vectorSearch = 'vector-search',\n  vectorSearchCreateIndex = 'create-index',\n  vectorSearchQuery = 'query',\n  browser = 'browser',\n  search = 'search',\n  slowLog = 'slowlog',\n  pubSub = 'pub-sub',\n  analytics = 'analytics',\n  clusterDetails = 'cluster-details',\n  databaseAnalysis = 'database-analysis',\n  settings = 'settings',\n  // rdi pages\n  rdiPipelineManagement = 'pipeline-management',\n  rdiPipelineConfig = 'config',\n  rdiPipelineJobs = 'jobs',\n  rdiStatistics = 'statistics',\n}\n\nconst redisCloud = '/redis-cloud'\nconst sentinel = '/sentinel'\nconst azure = '/azure'\nconst rdi = '/integrate'\n\nexport type PageValues = (typeof Pages)[keyof typeof Pages]\nexport const Pages = {\n  home: '/',\n  homeEditInstance: (instanceId: string) => `/?editInstance=${instanceId}`,\n  notFound: '/not-found',\n  redisEnterpriseAutodiscovery: '/redis-enterprise-autodiscovery',\n  settings: `/${PageNames.settings}`,\n  redisCloud,\n  redisCloudSubscriptions: `${redisCloud}/subscriptions`,\n  redisCloudDatabases: `${redisCloud}/databases`,\n  redisCloudDatabasesResult: `${redisCloud}/databases-result`,\n  sentinel,\n  sentinelDatabases: `${sentinel}/databases`,\n  sentinelDatabasesResult: `${sentinel}/databases-result`,\n  azure,\n  azureSubscriptions: `${azure}/subscriptions`,\n  azureDatabases: `${azure}/databases`,\n  browser: (instanceId: string) => `/${instanceId}/${PageNames.browser}`,\n  vectorSearch: (instanceId: string) =>\n    `/${instanceId}/${PageNames.vectorSearch}`,\n  vectorSearchCreateIndex: (instanceId: string) =>\n    `/${instanceId}/${PageNames.vectorSearch}/${PageNames.vectorSearchCreateIndex}`,\n  vectorSearchQuery: (instanceId: string, indexName: string) =>\n    `/${instanceId}/${PageNames.vectorSearch}/${indexName}/${PageNames.vectorSearchQuery}`,\n  workbench: (instanceId: string) => `/${instanceId}/${PageNames.workbench}`,\n  search: (instanceId: string) => `/${instanceId}/${PageNames.search}`,\n  pubSub: (instanceId: string) => `/${instanceId}/${PageNames.pubSub}`,\n  analytics: (instanceId: string) => `/${instanceId}/${PageNames.analytics}`,\n  slowLog: (instanceId: string) =>\n    `/${instanceId}/${PageNames.analytics}/${PageNames.slowLog}`,\n  clusterDetails: (instanceId: string) =>\n    `/${instanceId}/${PageNames.analytics}/${PageNames.clusterDetails}`,\n  databaseAnalysis: (instanceId: string) =>\n    `/${instanceId}/${PageNames.analytics}/${PageNames.databaseAnalysis}`,\n  // rdi pages\n  rdi,\n  rdiPipeline: (rdiInstance: string) => `${rdi}/${rdiInstance}`,\n  rdiPipelineManagement: (rdiInstance: string) =>\n    `${rdi}/${rdiInstance}/${PageNames.rdiPipelineManagement}`,\n  rdiPipelineConfig: (rdiInstance: string) =>\n    `${rdi}/${rdiInstance}/${PageNames.rdiPipelineManagement}/${PageNames.rdiPipelineConfig}`,\n  rdiPipelineJobs: (rdiInstance: string, jobName: string) =>\n    `${rdi}/${rdiInstance}/${PageNames.rdiPipelineManagement}/${PageNames.rdiPipelineJobs}/${jobName}`,\n  rdiStatistics: (rdiInstance: string) => `${rdi}/${rdiInstance}/statistics`,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/prop-types/keys.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { KeyTypes, ModulesKeyTypes } from '../keys'\n\nexport interface IKeyPropTypes {\n  nameString: string\n  name: RedisResponseBuffer\n  type: KeyTypes | ModulesKeyTypes\n  ttl: number\n  size: number\n  length: number\n}\n\nexport interface IFetchKeyArgs {\n  resetData?: boolean\n  start?: number\n  end?: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/prop-types/zset.ts",
    "content": "export interface ZSetMemberPropTypes {\n  name: string\n  score: string\n}\n\nexport interface ZSetPropTypes {\n  keyName: string\n  total: number\n  ttl: number\n  size: number\n  members: ZSetMemberPropTypes[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/pubSub.ts",
    "content": "export enum SubscriptionType {\n  Subscribe = 's',\n  PSubscribe = 'p',\n  SSubscribe = 'ss',\n}\n\nexport enum PubSubEvent {\n  Subscribe = 'subscribe',\n  Unsubscribe = 'unsubscribe',\n  Exception = 'exception',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/rdiList.ts",
    "content": "export enum RdiListColumn {\n  Name = 'name',\n  Url = 'url',\n  Version = 'version',\n  LastConnection = 'lastConnection',\n  Controls = 'controls',\n}\n\nexport const RDI_COLUMN_FIELD_NAME_MAP = new Map<RdiListColumn, string>([\n  [RdiListColumn.Name, 'RDI alias'],\n  [RdiListColumn.Url, 'URL'],\n  [RdiListColumn.Version, 'RDI version'],\n  [RdiListColumn.LastConnection, 'Last connection'],\n  [RdiListColumn.Controls, 'Controls'],\n])\n\nexport const DEFAULT_RDI_SHOWN_COLUMNS = [\n  RdiListColumn.Name,\n  RdiListColumn.Url,\n  RdiListColumn.Version,\n  RdiListColumn.LastConnection,\n  RdiListColumn.Controls,\n]\n"
  },
  {
    "path": "redisinsight/ui/src/constants/recommendations.ts",
    "content": "export enum Vote {\n  Like = 'useful',\n  Dislike = 'not useful',\n}\n\nexport enum RecommendationsSocketEvents {\n  Recommendation = 'recommendation',\n}\n\nexport const ANALYZE_TOOLTIP_MESSAGE =\n  'Analyze up to 10 000 keys to get an overview of your data and tips on how to save memory and optimize the usage of your database.'\nexport const ANALYZE_CLUSTER_TOOLTIP_MESSAGE =\n  'Analyze up to 10 000 keys per shard to get an overview of your data and tips on how to save memory and optimize the usage of your database.'\n\nexport const ANIMATION_INSIGHT_PANEL_MS = 400\n"
  },
  {
    "path": "redisinsight/ui/src/constants/redisearch.ts",
    "content": "export const REDISEARCH_GEOSHAPE_SEMANTIC_VERSION = '2.8.4'\n\nexport const REDISEARCH_GEOSHAPE_VERSION = 20804\n"
  },
  {
    "path": "redisinsight/ui/src/constants/redisinsight.ts",
    "content": "export const APPLICATION_NAME = 'Redis Insight'\n\nexport const API_URL = 'http://localhost:5000'\n\nexport enum DbType {\n  STANDALONE = 'STANDALONE',\n  CLUSTER = 'CLUSTER',\n  SENTINEL = 'SENTINEL',\n}\n\nexport enum RedisDataType {\n  String = 'string',\n  Hash = 'hash',\n  List = 'list',\n  Set = 'set',\n  ZSet = 'zset',\n  Stream = 'stream',\n  JSON = 'ReJSON-RL',\n  Graph = 'graphdata',\n  TS = 'TSDB-TYPE',\n}\n\n// https://www.iana.org/assignments/uri-schemes/prov/redis\n// https://www.iana.org/assignments/uri-schemes/prov/rediss\nexport const REDIS_URI_SCHEMES = ['redis', 'rediss']\n"
  },
  {
    "path": "redisinsight/ui/src/constants/regex.ts",
    "content": "export const IS_ABSOLUTE_PATH = new RegExp('^(?:[a-z]+:)?//', 'i')\n"
  },
  {
    "path": "redisinsight/ui/src/constants/securityField.ts",
    "content": "export const SECURITY_FIELD = '••••••••••••'\n"
  },
  {
    "path": "redisinsight/ui/src/constants/serverVersions.ts",
    "content": "export const ServerVersions = {\n  MIN_SERVER_VERSION: '6.2.6',\n  SERVER_VERSION: '7.2',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/socketErrors.ts",
    "content": "export enum SocketErrors {\n  TransportError = 'TransportError',\n}\n\nexport enum MonitorErrorMessages {\n  NoPerm = \"The Profiler cannot be started. This user has no permissions to run the 'monitor' command\",\n  LostConnection = 'Error: Connection was lost',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/socketEvents.ts",
    "content": "export enum SocketEvent {\n  Connect = 'connect',\n  Disconnect = 'disconnect',\n  ConnectionError = 'connect_error',\n}\n\nexport enum SocketFeaturesEvent {\n  Features = 'features',\n}\n\nexport enum CloudJobEvents {\n  Monitor = 'cloud:job:monitor',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/sorting.ts",
    "content": "import { PropertySort } from '@elastic/eui'\n\nexport const DEFAULT_SORT: PropertySort = {\n  field: 'lastConnection',\n  direction: 'asc',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/storage.ts",
    "content": "enum BrowserStorageItem {\n  homePage = 'homePage',\n  instancesCount = 'instancesCount',\n  instancesSorting = 'instancesSorting',\n  rdiInstancesSorting = 'rdiInstancesSorting',\n  theme = 'theme',\n  browserViewType = 'browserViewType',\n  browserShownColumns = 'browserShownColumns',\n  browserSearchMode = 'browserSearchMode',\n  cliClientUuid = 'cliClientUuid',\n  cliResizableContainer = 'cliResizableContainer',\n  cliInputHistory = 'cliInputHistory',\n  cliIsShowHelper = 'cliIsShowHelper',\n  segmentAnonymousId = 'ajs_anonymous_id',\n  wbClientUuid = 'wbClientUuid',\n  wbInputHistory = 'wbInputHistory',\n  wbCommandsHistory = 'command_execution',\n  treeViewDelimiter = 'treeViewDelimiter',\n  treeViewSort = 'treeViewSort',\n  autoRefreshRate = 'autoRefreshRate',\n  bulkActionDeleteId = 'bulkActionDeleteId',\n  dbConfig = 'dbConfig_',\n  RunQueryMode = 'RunQueryMode',\n  SQRunQueryMode = 'SQRunQueryMode',\n  wbCleanUp = 'wbCleanUp',\n  viewFormat = 'viewFormat',\n  wbGroupMode = 'wbGroupMode',\n  keyDetailSizes = 'keyDetailSizes',\n  featuresHighlighting = 'featuresHighlighting',\n  dbIndex = 'dbIndex_',\n  onboardingStep = 'onboardingStep',\n  recommendationsViewed = 'recommendationsViewed',\n  showHiddenRecommendations = 'showHiddenRecommendations',\n  OAuthJobId = 'OAuthJobId',\n  OAuthAgreement = 'OAuthAgreement',\n  insightsPanel = 'insightsPanel',\n  sidePanel = 'sidePanel',\n  capability = 'capability',\n  aiChatSession = 'aiChatSession',\n  selectedAiChat = 'selectedAiChat',\n  generalChatAgreements = 'generalChatAgreements',\n  vectorSearchOnboarding = 'vectorSearchOnboarding',\n  tablePaginationState = 'tablePaginationState',\n  queryLibrary = 'query_library',\n  vectorSearchQueryOnboarding = 'vectorSearchQueryOnboarding',\n  vectorSearchSelectKeyOnboarding = 'vectorSearchSelectKeyOnboarding',\n  vectorSearchCreateIndexOnboarding = 'vectorSearchCreateIndexOnboarding',\n}\n\nexport default BrowserStorageItem\n\nexport enum ConfigDBStorageItem {\n  slowLogDurationUnit = 'slowLogDurationUnit',\n  notShowConfirmationRunTutorial = 'notShowConfirmationRunTutorial',\n}\n\nexport enum CapabilityStorageItem {\n  source = 'source',\n  tutorialPopoverShown = 'tutorialPopoverShown',\n}\n\nexport enum AppStorageItem {\n  returnUrl = 'returnUrl',\n}\n\nexport enum TableStorageKey {\n  dbList = 'dbList',\n  rdiList = 'rdiList',\n  pubSubList = 'pubSubList',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/streamViews.ts",
    "content": "import { StreamViewType } from 'uiSrc/slices/interfaces/stream'\n\nexport const StreamViews = Object.freeze({\n  [StreamViewType.Data]: 'entries',\n  [StreamViewType.Groups]: 'consumer_groups',\n  [StreamViewType.Consumers]: 'consumers',\n  [StreamViewType.Messages]: 'pending_messages_list',\n})\n"
  },
  {
    "path": "redisinsight/ui/src/constants/string.ts",
    "content": "export const STRING_MAX_LENGTH = 4999\nexport const TOOLTIP_CONTENT_MAX_LENGTH = 500\n"
  },
  {
    "path": "redisinsight/ui/src/constants/table.ts",
    "content": "export enum TableCellAlignment {\n  Left = 'flex-start',\n  Center = 'center',\n  Right = 'flex-end',\n}\n\nexport enum TableCellTextAlignment {\n  Left = 'left',\n  Center = 'center',\n  Right = 'right',\n}\n\nexport const OVER_RENDER_BUFFER_COUNT = 50\n"
  },
  {
    "path": "redisinsight/ui/src/constants/telemetry.ts",
    "content": "export enum ReleaseNotesSource {\n  helpCenter = 'Help Center',\n  updateNotification = 'Update notification',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/texts.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nexport const NoResultsFoundText = (\n  <Text size=\"m\" data-testid=\"no-result-found-only\">\n    No results found.\n  </Text>\n)\n\nexport const LoadingText = (\n  <Text size=\"m\" data-testid=\"loading-keys\" style={{ lineHeight: 1.4 }}>\n    loading...\n  </Text>\n)\n\nexport const NoSelectedIndexText = (\n  <Text size=\"m\" data-testid=\"no-result-select-index\">\n    Select an index and enter a query to search per values of keys.\n  </Text>\n)\n\nexport const FullScanNoResultsFoundText = (\n  <>\n    <Text size=\"m\" data-test-subj=\"no-result-found\">\n      No results found.\n    </Text>\n    <Spacer size=\"m\" />\n    <Text size=\"s\" data-test-subj=\"search-advices\">\n      Check the spelling.\n      <br />\n      Check upper and lower cases.\n      <br />\n      Use an asterisk (*) in your request for more generic results.\n    </Text>\n  </>\n)\nexport const ScanNoResultsFoundText = (\n  <>\n    <Text size=\"m\" data-testid=\"scan-no-results-found\">\n      No results found.\n    </Text>\n    <br />\n    <Text size=\"s\">\n      Use &quot;Scan more&quot; button to proceed or filter per exact Key Name\n      to scan more efficiently.\n    </Text>\n  </>\n)\n\nexport const lastDeliveredIDTooltipText = (\n  <>\n    <Text size=\"s\">\n      Specify the ID of the last delivered entry in the stream from the new\n      group's perspective.\n    </Text>\n    <Spacer size=\"xs\" />\n    <Text size=\"s\">\n      Otherwise, <b>$</b> represents the ID of the last entry in the\n      stream,&nbsp;\n      <b>0</b> fetches the entire stream from the beginning.\n    </Text>\n  </>\n)\n\nexport const streamIDTooltipText = (\n  <>\n    ID must be a timestamp and sequence number greater than the last ID.\n    <Spacer size=\"xs\" />\n    Otherwise, type * to auto-generate ID based on the database current time.\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/constants/themes.tsx",
    "content": "import { getConfig } from 'uiSrc/config'\nimport { RiSelectOption } from 'uiSrc/components/base/forms/select/RiSelect'\n\nconst riConfig = getConfig()\n\nexport enum Theme {\n  Dark = 'DARK',\n  Light = 'LIGHT',\n  System = 'SYSTEM',\n}\n\nexport const DEFAULT_THEME = riConfig.app.defaultTheme\n\nexport const THEMES: RiSelectOption[] = [\n  {\n    inputDisplay: 'Match System',\n    value: Theme.System,\n  },\n  {\n    inputDisplay: 'Dark Theme',\n    value: Theme.Dark,\n  },\n  {\n    inputDisplay: 'Light Theme',\n    value: Theme.Light,\n  },\n]\n\nexport const THEME_MATCH_MEDIA_DARK = '(prefers-color-scheme: dark)'\n\nexport default THEMES\n"
  },
  {
    "path": "redisinsight/ui/src/constants/tutorials.ts",
    "content": "enum TutorialsIds {\n  IntroToSearch = 'sq-intro',\n  IntroToJSON = 'ds-json-intro',\n  BasicRedisUseCases = 'redis_use_cases_basic',\n  RedisUseCases = 'redis_use_cases',\n  IntroVectorSearch = 'vss-intro',\n  ExactMatch = 'sq-exact-match',\n  FullTextSearch = 'sq-full-text',\n}\n\nexport { TutorialsIds }\n"
  },
  {
    "path": "redisinsight/ui/src/constants/validationErrors.ts",
    "content": "export default {\n  REQUIRED_TITLE: (count: number) =>\n    `Enter a value for required fields (${count}):`,\n  NO_DBS_SELECTED: 'No databases selected.',\n  SELECT_AT_LEAST_ONE: (text: string) => `Select at least one ${text}`,\n  NO_PRIMARY_GROUPS_SENTINEL: 'No primary groups selected.',\n  NO_SUBSCRIPTIONS_CLOUD: 'No subscriptions selected.',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/workbench.ts",
    "content": "import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces'\n\nexport const CodeButtonResults = {\n  group: ResultsMode.GroupMode,\n  single: ResultsMode.Default,\n  silent: ResultsMode.Silent,\n  [ResultsMode.GroupMode]: ResultsMode.GroupMode,\n  [ResultsMode.Default]: ResultsMode.Default,\n  [ResultsMode.Silent]: ResultsMode.Silent,\n}\n\nexport const CodeButtonRunQueryMode = {\n  raw: RunQueryMode.Raw,\n  ascii: RunQueryMode.ASCII,\n  [RunQueryMode.Raw]: RunQueryMode.Raw,\n  [RunQueryMode.ASCII]: RunQueryMode.ASCII,\n}\n\nexport const BooleanParams = {\n  true: 'true',\n  false: 'false',\n}\n\nexport enum EAItemActions {\n  Create = 'create',\n  Delete = 'delete',\n}\n\nexport enum EAManifestFirstKey {\n  CUSTOM_TUTORIALS = 'custom-tutorials',\n  TUTORIALS = 'tutorials',\n  GUIDES = 'quick-guides',\n}\n\nexport interface CodeButtonParams {\n  clearEditor?: boolean\n  pipeline?: string\n  results?: keyof typeof CodeButtonResults\n  mode?: keyof typeof CodeButtonRunQueryMode\n  run_confirmation?: keyof typeof BooleanParams\n  executable?: keyof typeof BooleanParams\n}\n\nexport enum ExecuteButtonMode {\n  Auto = 'auto',\n  Manual = 'manual',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/constants/workbenchResults.ts",
    "content": "import { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { EXTERNAL_LINKS } from './links'\n\nexport const bulkReplyCommands = [\n  'LOLWUT',\n  'INFO',\n  'CLIENT',\n  'CLUSTER',\n  'MEMORY',\n  'MONITOR',\n  'PSUBSCRIBE',\n]\n\nexport const EMPTY_COMMAND = 'Encrypted data'\n\nexport const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } =\n  {\n    [RedisDefaultModules.TimeSeries]: {\n      text: ['Time series data structure adds the capability to:'],\n      improvements: [\n        'Add sample data',\n        'Perform cross-time-series range and aggregation queries',\n        'Define compaction rules for economical retention of historical data',\n      ],\n      link: 'https://redis.io/docs/latest/develop/data-types/timeseries/',\n    },\n    [RedisDefaultModules.Search]: {\n      title: ['Redis Query Engine is not available for this database'],\n      text: ['Redis Query Engine allows to:'],\n      improvements: ['Query', 'Secondary index', 'Full-text search'],\n      additionalText: [\n        'These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, ',\n        'geo filtering and vector similarity semantic search on top of text queries.',\n      ],\n      ctaText: [\n        'Use your free trial all-in-one Redis Cloud database to start exploring these capabilities',\n      ],\n      link: EXTERNAL_LINKS.redisQueryEngine,\n    },\n    [RedisDefaultModules.ReJSON]: {\n      text: ['JSON adds the capability to:'],\n      improvements: [\n        'Store JSON documents',\n        'Update JSON documents',\n        'Retrieve JSON documents',\n      ],\n      additionalText: [\n        'JSON data structure also works seamlessly with Redis Query Engine to let you index and query JSON documents.',\n      ],\n      link: 'https://redis.io/docs/latest/develop/data-types/json/',\n    },\n    [RedisDefaultModules.Bloom]: {\n      text: ['Probabilistic data structures include:'],\n      improvements: [\n        'Bloom filter',\n        'Cuckoo filter',\n        'Count-min sketch',\n        'Top-K',\n        'T-digest',\n      ],\n      additionalText: [\n        'With these data structures, you can query streaming data without needing to store all the elements of the stream.',\n      ],\n      link: 'https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/',\n    },\n  }\n\nexport const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = {\n  [RedisDefaultModules.Bloom]: 'probabilistic data structures',\n  [RedisDefaultModules.ReJSON]: 'JSON data structure',\n  [RedisDefaultModules.Search]: 'Redis Query Engine',\n  [RedisDefaultModules.TimeSeries]: 'time series data structure',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/contexts/AppNavigationActionsProvider.tsx",
    "content": "import React, { createContext, useContext } from 'react'\nimport { Nullable } from 'uiSrc/utils'\n\ninterface AppNavigationActionsType {\n  actions: Nullable<React.ReactNode>\n  setActions: (actions: Nullable<React.ReactNode>) => void\n}\n\n// Create a context\nconst AppNavigationActionsContext = createContext<AppNavigationActionsType>({\n  actions: null,\n  setActions: () => {},\n})\n\n// Custom hook to access the header context\nexport const useAppNavigationActions = () =>\n  useContext(AppNavigationActionsContext)\nexport const AppNavigationActionsProvider = AppNavigationActionsContext.Provider\n"
  },
  {
    "path": "redisinsight/ui/src/contexts/ModalTitleProvider.tsx",
    "content": "import React, { createContext, useContext } from 'react'\nimport { Nullable } from 'uiSrc/utils'\n\ninterface ModalHeaderContextType {\n  modalHeader: Nullable<React.ReactNode>\n  setModalHeader: (\n    content: Nullable<React.ReactNode>,\n    withBack?: boolean,\n  ) => void\n}\n\n// Create a context\nconst ModalHeaderContext = createContext<ModalHeaderContextType>({\n  modalHeader: null,\n  setModalHeader: () => {},\n})\n\n// Custom hook to access the header context\nexport const useModalHeader = () => useContext(ModalHeaderContext)\nexport const ModalHeaderProvider = ModalHeaderContext.Provider\n"
  },
  {
    "path": "redisinsight/ui/src/contexts/themeContext.tsx",
    "content": "import React, { useContext } from 'react'\nimport { ThemeProvider as StyledThemeProvider } from 'styled-components'\nimport { CommonStyles, themesDefault } from '@redis-ui/styles'\nimport 'modern-normalize/modern-normalize.css'\nimport '@redis-ui/styles/normalized-styles.css'\nimport '@redis-ui/styles/fonts.css'\n\nimport { ipcThemeChange } from 'uiSrc/electron/utils/ipcThemeChange'\nimport { GlobalStyles } from 'uiSrc/styles/globalStyles'\nimport {\n  BrowserStorageItem,\n  Theme,\n  THEMES,\n  THEME_MATCH_MEDIA_DARK,\n  DEFAULT_THEME,\n} from '../constants'\nimport { localStorageService, themeService } from '../services'\n\ninterface Props {\n  children: React.ReactNode\n}\n\nconst { light: themeLight, dark: themeDark } = themesDefault\nconst THEME_NAMES = THEMES.map(({ value }) => value)\n\nconst getQueryTheme = () => {\n  const queryThemeParam = new URLSearchParams(window.location.search)\n    .get('theme')\n    ?.toUpperCase()\n\n  return THEMES.find(({ value }) => value === queryThemeParam)?.value\n}\n\nexport const defaultState = {\n  theme: DEFAULT_THEME || Theme.System,\n  usingSystemTheme:\n    localStorageService.get(BrowserStorageItem.theme) === Theme.System,\n  changeTheme: (themeValue: any) => {\n    themeService.applyTheme(themeValue)\n  },\n}\n\nexport const isValidTheme = (theme: unknown): theme is Theme => {\n  return typeof theme === 'string' && THEME_NAMES.includes(theme as Theme)\n}\n\nexport const ThemeContext = React.createContext(defaultState)\n\nexport class ThemeProvider extends React.Component<Props> {\n  constructor(props: any) {\n    super(props)\n\n    const queryTheme = getQueryTheme()\n    const storedThemeValue = localStorageService.get(BrowserStorageItem.theme)\n\n    let theme = defaultState.theme\n\n    if (queryTheme) {\n      theme = queryTheme\n    } else if (storedThemeValue && isValidTheme(storedThemeValue)) {\n      theme = storedThemeValue\n    }\n\n    const usingSystemTheme = theme === Theme.System\n\n    themeService.applyTheme(theme as Theme)\n\n    this.state = {\n      theme: theme === Theme.System ? this.getSystemTheme() : theme,\n      usingSystemTheme,\n    }\n  }\n\n  getSystemTheme = () =>\n    window.matchMedia?.(THEME_MATCH_MEDIA_DARK)?.matches\n      ? Theme.Dark\n      : Theme.Light\n\n  changeTheme = async (themeValue: any) => {\n    let actualTheme = themeValue\n\n    // since change theme is async need to wait to have a proper prefers-color-scheme\n    await ipcThemeChange(themeValue)\n\n    if (themeValue === Theme.System) {\n      actualTheme = this.getSystemTheme()\n    }\n\n    this.setState(\n      { theme: actualTheme, usingSystemTheme: themeValue === Theme.System },\n      () => {\n        themeService.applyTheme(themeValue)\n      },\n    )\n  }\n\n  render() {\n    const { children } = this.props\n    const { theme, usingSystemTheme }: any = this.state\n    const uiTheme = theme === Theme.Dark ? themeDark : themeLight\n    return (\n      <ThemeContext.Provider\n        value={{\n          theme,\n          usingSystemTheme,\n          changeTheme: this.changeTheme,\n        }}\n      >\n        <StyledThemeProvider theme={uiTheme}>\n          <CommonStyles />\n          <GlobalStyles />\n          {children}\n        </StyledThemeProvider>\n      </ThemeContext.Provider>\n    )\n  }\n}\n\nexport const useThemeContext = () => {\n  return useContext(ThemeContext)\n}\n\nexport default ThemeProvider\n"
  },
  {
    "path": "redisinsight/ui/src/electron/AppElectron.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport AppElectron from './AppElectron'\n\ndescribe('AppElectron', () => {\n  it('should render', () => {\n    expect(render(<AppElectron />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/electron/AppElectron.tsx",
    "content": "import React from 'react'\nimport App from 'uiSrc/App'\nimport Router from 'uiSrc/RouterElectron'\nimport { ConfigElectron, ConfigOAuth, ConfigAzureAuth } from './components'\n\nconst AppElectron = () => (\n  <Router>\n    <App>\n      <ConfigElectron />\n      <ConfigOAuth />\n      <ConfigAzureAuth />\n    </App>\n  </Router>\n)\n\nexport default AppElectron\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigAzureAuth/ConfigAzureAuth.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\n\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  createMockedStore,\n  mockStore,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { AzureAuthStatus } from 'apiSrc/modules/azure/constants'\nimport {\n  azureOAuthCallbackSuccess,\n  azureOAuthCallbackFailure,\n  setAzureLoginSource,\n} from 'uiSrc/slices/oauth/azure'\nimport { resetDataAzure } from 'uiSrc/slices/instances/azure'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\n\nimport ConfigAzureAuth from './ConfigAzureAuth'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n  window.app = {\n    azureOauthCallback: jest.fn(),\n  } as any\n})\n\nafterEach(() => {\n  delete (window as any).app\n})\n\nconst renderConfigAzureAuth = () => render(<ConfigAzureAuth />, { store })\n\ndescribe('ConfigAzureAuth', () => {\n  const mockMsalAccount = {\n    homeAccountId: faker.string.uuid(),\n    username: faker.internet.email(),\n    name: faker.person.fullName(),\n  }\n\n  const expectedAccount = {\n    id: mockMsalAccount.homeAccountId,\n    username: mockMsalAccount.username,\n    name: mockMsalAccount.name,\n  }\n\n  it('should call proper actions on success', () => {\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, {\n        status: AzureAuthStatus.Succeed,\n        account: mockMsalAccount,\n      }),\n    )\n    renderConfigAzureAuth()\n\n    const actions = store.getActions()\n    expect(actions[0]).toEqual(resetDataAzure())\n    expect(actions[1]).toEqual(azureOAuthCallbackSuccess(expectedAccount))\n    expect(actions[2]).toEqual(setAzureLoginSource(null))\n  })\n\n  it('should call proper actions on failure with error message', () => {\n    const errorMessage = faker.lorem.sentence()\n\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, { status: AzureAuthStatus.Failed, error: errorMessage }),\n    )\n    renderConfigAzureAuth()\n\n    const actions = store.getActions()\n\n    expect(actions[0]).toEqual(azureOAuthCallbackFailure(errorMessage))\n    expect(actions[1].type).toEqual(addErrorNotification({} as any).type)\n  })\n\n  it('should call proper actions on failure with default error message', () => {\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, { status: AzureAuthStatus.Failed }),\n    )\n    renderConfigAzureAuth()\n\n    const actions = store.getActions()\n\n    expect(actions[0]).toEqual(\n      azureOAuthCallbackFailure('Azure authentication failed'),\n    )\n    expect(actions[1].type).toEqual(addErrorNotification({} as any).type)\n  })\n\n  it('should call failure actions for unknown status', () => {\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, { status: 'unknown' }),\n    )\n    renderConfigAzureAuth()\n\n    const actions = store.getActions()\n\n    expect(actions[0]).toEqual(\n      azureOAuthCallbackFailure('Azure authentication failed'),\n    )\n    expect(actions[1].type).toEqual(addErrorNotification({} as any).type)\n  })\n\n  it('should call failure actions when success status but account is missing', () => {\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, { status: AzureAuthStatus.Succeed, account: undefined }),\n    )\n    renderConfigAzureAuth()\n\n    const actions = store.getActions()\n\n    expect(actions[0]).toEqual(\n      azureOAuthCallbackFailure('Azure authentication failed'),\n    )\n    expect(actions[1].type).toEqual(addErrorNotification({} as any).type)\n  })\n\n  it('should show success notification when source is token-refresh', () => {\n    const stateWithTokenRefresh = {\n      ...initialStateDefault,\n      oauth: {\n        ...initialStateDefault.oauth,\n        azure: {\n          loading: false,\n          account: null,\n          error: '',\n          source: AzureLoginSource.TokenRefresh,\n        },\n      },\n    }\n    store = mockStore(stateWithTokenRefresh)\n    store.clearActions()\n\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, {\n        status: AzureAuthStatus.Succeed,\n        account: mockMsalAccount,\n      }),\n    )\n    render(<ConfigAzureAuth />, { store })\n\n    const actions = store.getActions()\n    expect(actions[0]).toEqual(resetDataAzure())\n    expect(actions[1]).toEqual(azureOAuthCallbackSuccess(expectedAccount))\n    expect(actions[2]).toEqual(setAzureLoginSource(null))\n    expect(actions[3].type).toEqual(addMessageNotification({} as any).type)\n  })\n\n  it('should not show success notification when source is autodiscovery', () => {\n    const stateWithAutodiscovery = {\n      ...initialStateDefault,\n      oauth: {\n        ...initialStateDefault.oauth,\n        azure: {\n          loading: false,\n          account: null,\n          error: '',\n          source: AzureLoginSource.Autodiscovery,\n        },\n      },\n    }\n    store = mockStore(stateWithAutodiscovery)\n    store.clearActions()\n\n    window.app?.azureOauthCallback?.mockImplementation((cb: any) =>\n      cb(undefined, {\n        status: AzureAuthStatus.Succeed,\n        account: mockMsalAccount,\n      }),\n    )\n    render(<ConfigAzureAuth />, { store })\n\n    const actions = store.getActions()\n    expect(actions[0]).toEqual(resetDataAzure())\n    expect(actions[1]).toEqual(azureOAuthCallbackSuccess(expectedAccount))\n    expect(actions[2]).toEqual(setAzureLoginSource(null))\n    // No message notification should be dispatched\n    expect(actions.length).toBe(3)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigAzureAuth/ConfigAzureAuth.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  azureAuthSourceSelector,\n  handleAzureOAuthSuccess,\n  handleAzureOAuthFailure,\n  setAzureLoginSource,\n} from 'uiSrc/slices/oauth/azure'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { AzureAuthStatus } from 'apiSrc/modules/azure/constants'\nimport { AppDispatch } from 'uiSrc/slices/store'\nimport { Pages } from 'uiSrc/constants'\n\ninterface MsalAccountInfo {\n  homeAccountId: string\n  username: string\n  name?: string\n}\n\ninterface AzureAuthCallbackResponse {\n  status: string\n  account?: MsalAccountInfo\n  error?: string\n}\n\nconst ConfigAzureAuth = () => {\n  const dispatch = useDispatch<AppDispatch>()\n  const history = useHistory()\n  const source = useSelector(azureAuthSourceSelector)\n  const sourceRef = useRef(source)\n  sourceRef.current = source\n\n  useEffect(() => {\n    window.app?.azureOauthCallback?.(azureOauthCallback)\n  }, [])\n\n  const azureOauthCallback = (\n    _e: unknown,\n    { status, account, error }: AzureAuthCallbackResponse,\n  ) => {\n    if (status === AzureAuthStatus.Succeed && account) {\n      const azureAccount = {\n        id: account.homeAccountId,\n        username: account.username,\n        name: account.name,\n      }\n      const currentSource = sourceRef.current\n      dispatch(handleAzureOAuthSuccess(azureAccount))\n      dispatch(setAzureLoginSource(null))\n\n      // Only redirect to autodiscovery flow if login was initiated from there\n      if (currentSource === AzureLoginSource.Autodiscovery) {\n        history.push(Pages.azureSubscriptions)\n      }\n\n      // Show success notification only for token refresh (login from error notification)\n      if (currentSource === AzureLoginSource.TokenRefresh) {\n        dispatch(\n          addMessageNotification({\n            title: 'Signed in to Azure',\n            message: 'You can now connect to your Azure database.',\n          }),\n        )\n      }\n      return\n    }\n\n    // Handle failure or success without account (edge case)\n    const errorMessage = error || 'Azure authentication failed'\n    dispatch(handleAzureOAuthFailure(errorMessage))\n    dispatch(\n      addErrorNotification({\n        response: {\n          data: {\n            message: errorMessage,\n          },\n        },\n        persistent: true,\n      } as any),\n    )\n  }\n\n  return null\n}\n\nexport default ConfigAzureAuth\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigAzureAuth/index.tsx",
    "content": "import ConfigAzureAuth from './ConfigAzureAuth'\n\nexport { ConfigAzureAuth }\nexport default ConfigAzureAuth\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigElectron/ConfigElectron.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport ConfigElectron from './ConfigElectron'\n\ndescribe('ConfigElectron', () => {\n  it('should render', () => {\n    expect(render(<ConfigElectron />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigElectron/ConfigElectron.tsx",
    "content": "import { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { UpdateInfo } from 'electron-updater'\nimport { IParsedDeepLink } from 'desktopSrc/lib/app/deep-link.handlers'\nimport {\n  appServerInfoSelector,\n  appElectronInfoSelector,\n} from 'uiSrc/slices/app/info'\nimport {\n  ipcAppRestart,\n  ipcCheckUpdates,\n  ipcSendEvents,\n} from 'uiSrc/electron/utils'\nimport { ipcDeleteDownloadedVersion } from 'uiSrc/electron/utils/ipcDeleteStoreValues'\nimport { addInfiniteNotification } from 'uiSrc/slices/app/notifications'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\n\nconst ConfigElectron = () => {\n  let isCheckedUpdates = false\n  const { isReleaseNotesViewed } = useSelector(appElectronInfoSelector)\n  const serverInfo = useSelector(appServerInfoSelector)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  useEffect(() => {\n    window.app?.deepLinkAction?.(deepLinkAction)\n    window.app?.updateAvailable?.(updateAvailableAction)\n  }, [])\n\n  useEffect(() => {\n    if (serverInfo) {\n      ipcCheckUpdates(serverInfo, dispatch)\n    }\n  }, [serverInfo])\n\n  useEffect(() => {\n    if (!isCheckedUpdates && serverInfo) {\n      ipcSendEvents(serverInfo)\n      isCheckedUpdates = true\n    }\n  }, [serverInfo])\n\n  useEffect(() => {\n    if (isReleaseNotesViewed) {\n      ipcDeleteDownloadedVersion()\n    }\n  }, [isReleaseNotesViewed])\n\n  const deepLinkAction = (_e: any, url: IParsedDeepLink) => {\n    if (url.from) {\n      const fromUrl = encodeURIComponent(url.from)\n      history.push({\n        search: `from=${fromUrl}`,\n      })\n    }\n  }\n\n  const updateAvailableAction = (_e: any, { version }: UpdateInfo) => {\n    sendEventTelemetry({ event: TelemetryEvent.UPDATE_NOTIFICATION_DISPLAYED })\n    dispatch(\n      addInfiniteNotification(\n        INFINITE_MESSAGES.APP_UPDATE_AVAILABLE(version, () => {\n          sendEventTelemetry({\n            event: TelemetryEvent.UPDATE_NOTIFICATION_RESTART_CLICKED,\n          })\n          ipcAppRestart()\n        }),\n      ),\n    )\n  }\n\n  return null\n}\n\nexport default ConfigElectron\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigElectron/index.tsx",
    "content": "import ConfigElectron from './ConfigElectron'\n\nexport default ConfigElectron\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigOAuth/ConfigOAuth.spec.tsx",
    "content": "import React from 'react'\nimport {\n  cleanup,\n  createMockedStore,\n  mockedStore,\n  render,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  CloudAuthStatus,\n  CloudJobName,\n  CloudJobStep,\n} from 'uiSrc/electron/constants'\nimport {\n  addFreeDb,\n  fetchUserInfo,\n  getPlans,\n  getUserInfo,\n  setJob,\n  setOAuthCloudSource,\n  setSocialDialogState,\n  showOAuthProgress,\n  signInFailure,\n} from 'uiSrc/slices/oauth/cloud'\nimport {\n  cloudSelector,\n  loadSubscriptionsRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport ConfigOAuth from './ConfigOAuth'\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  fetchUserInfo: jest\n    .fn()\n    .mockImplementation(\n      jest.requireActual('uiSrc/slices/oauth/cloud').fetchUserInfo,\n    ),\n}))\n\njest.mock('uiSrc/slices/instances/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/cloud'),\n  cloudSelector: jest.fn().mockReturnValue({\n    ...jest.requireActual('uiSrc/slices/instances/cloud').initialState,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n  window.app = {\n    cloudOauthCallback: jest.fn(),\n  } as any\n})\n\nconst renderConfigOAuth = () => {\n  return render(<ConfigOAuth />, { store })\n}\n\ndescribe('ConfigOAuth', () => {\n  it('should render', () => {\n    expect(render(<ConfigOAuth />)).toBeTruthy()\n  })\n\n  it('should call proper actions on success', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: 'signIn',\n    })\n\n    window.app?.cloudOauthCallback.mockImplementation((cb: any) =>\n      cb(undefined, { status: CloudAuthStatus.Succeed }),\n    )\n    renderConfigOAuth()\n\n    const expectedActions = [\n      setJob({\n        id: '',\n        name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n        status: '',\n      }),\n      showOAuthProgress(true),\n      addInfiniteNotification(INFINITE_MESSAGES.AUTHENTICATING()),\n      setSocialDialogState(null),\n      getUserInfo(),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper actions on failed', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: 'signIn',\n    })\n\n    window.app?.cloudOauthCallback.mockImplementation((cb: any) =>\n      cb(undefined, {\n        status: CloudAuthStatus.Failed,\n        error: 'error',\n      }),\n    )\n    renderConfigOAuth()\n\n    const expectedActions = [\n      setOAuthCloudSource(null),\n      signInFailure('error'),\n      addErrorNotification({\n        response: {\n          data: {\n            message: 'error',\n          },\n          status: 500,\n        },\n      } as any),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should fetch plans with create flow', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: 'create',\n    })\n\n    const fetchUserInfoMock = jest\n      .fn()\n      .mockImplementation(\n        (onSuccessAction: () => void) => () => onSuccessAction(),\n      )\n    ;(fetchUserInfo as jest.Mock).mockImplementation(fetchUserInfoMock)\n\n    window.app?.cloudOauthCallback.mockImplementation((cb: any) =>\n      cb(undefined, { status: CloudAuthStatus.Succeed }),\n    )\n    renderConfigOAuth()\n\n    const afterCallbackActions = [\n      setJob({\n        id: '',\n        name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n        status: '',\n      }),\n      showOAuthProgress(true),\n      addInfiniteNotification(INFINITE_MESSAGES.AUTHENTICATING()),\n      setSocialDialogState(null),\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n    ]\n\n    const expectedActions = [getPlans()]\n    expect(store.getActions()).toEqual([\n      ...afterCallbackActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should call fetch subscriptions with autodiscovery flow', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      ssoFlow: 'import',\n    })\n\n    const fetchUserInfoMock = jest\n      .fn()\n      .mockImplementation(\n        (onSuccessAction: () => void) => () => onSuccessAction(),\n      )\n    ;(fetchUserInfo as jest.Mock).mockImplementation(fetchUserInfoMock)\n\n    window.app?.cloudOauthCallback.mockImplementation((cb: any) =>\n      cb(undefined, { status: CloudAuthStatus.Succeed }),\n    )\n    renderConfigOAuth()\n\n    const afterCallbackActions = [\n      setJob({\n        id: '',\n        name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n        status: '',\n      }),\n      showOAuthProgress(true),\n      addInfiniteNotification(INFINITE_MESSAGES.AUTHENTICATING()),\n      setSocialDialogState(null),\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n    ]\n\n    const expectedActions = [loadSubscriptionsRedisCloud()]\n    expect(store.getActions()).toEqual([\n      ...afterCallbackActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should call create free job after success with recommended settings', () => {\n    ;(cloudSelector as jest.Mock).mockReturnValue({\n      isRecommendedSettings: true,\n      ssoFlow: 'create',\n    })\n\n    const fetchUserInfoMock = jest\n      .fn()\n      .mockImplementation(\n        (onSuccessAction: () => void) => () => onSuccessAction(),\n      )\n    ;(fetchUserInfo as jest.Mock).mockImplementation(fetchUserInfoMock)\n\n    window.app?.cloudOauthCallback.mockImplementation((cb: any) =>\n      cb(undefined, { status: CloudAuthStatus.Succeed }),\n    )\n    renderConfigOAuth()\n\n    const afterCallbackActions = [\n      setJob({\n        id: '',\n        name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n        status: '',\n      }),\n      showOAuthProgress(true),\n      addInfiniteNotification(INFINITE_MESSAGES.AUTHENTICATING()),\n      setSocialDialogState(null),\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n    ]\n\n    const expectedActions = [addFreeDb()]\n    expect(store.getActions()).toEqual([\n      ...afterCallbackActions,\n      ...expectedActions,\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigOAuth/ConfigOAuth.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  createFreeDbJob,\n  fetchPlans,\n  fetchUserInfo,\n  setJob,\n  setOAuthCloudSource,\n  setSocialDialogState,\n  showOAuthProgress,\n  signInFailure,\n} from 'uiSrc/slices/oauth/cloud'\nimport { BrowserStorageItem, Pages } from 'uiSrc/constants'\nimport {\n  cloudSelector,\n  fetchSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport {\n  CloudAuthResponse,\n  CloudAuthStatus,\n  CloudJobName,\n  CloudJobStep,\n} from 'uiSrc/electron/constants'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { parseCustomError } from 'uiSrc/utils'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { localStorageService } from 'uiSrc/services'\nimport { CustomError, OAuthSocialAction } from 'uiSrc/slices/interfaces'\n\nconst ConfigOAuth = () => {\n  const { ssoFlow, isRecommendedSettings } = useSelector(cloudSelector)\n\n  const ssoFlowRef = useRef(ssoFlow)\n  const isRecommendedSettingsRef = useRef(isRecommendedSettings)\n  const isFlowInProgress = useRef(false)\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    window.app?.cloudOauthCallback?.(cloudOauthCallback)\n  }, [])\n\n  useEffect(() => {\n    ssoFlowRef.current = ssoFlow\n\n    if (!ssoFlow) {\n      isFlowInProgress.current = false\n    }\n  }, [ssoFlow])\n\n  useEffect(() => {\n    isRecommendedSettingsRef.current = isRecommendedSettings\n  }, [isRecommendedSettings])\n\n  const fetchUserInfoSuccess = (isSelectAccout: boolean) => {\n    if (isSelectAccout) return\n\n    if (ssoFlowRef.current === OAuthSocialAction.SignIn) {\n      dispatch(setSSOFlow(undefined))\n      closeInfinityNotification()\n      return\n    }\n\n    dispatch(\n      addInfiniteNotification(\n        INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n      ),\n    )\n\n    if (ssoFlowRef.current === OAuthSocialAction.Import) {\n      dispatch(\n        fetchSubscriptionsRedisCloud(\n          null,\n          true,\n          () => {\n            closeInfinityNotification()\n            history.push(Pages.redisCloudSubscriptions)\n          },\n          closeInfinityNotification,\n        ),\n      )\n      return\n    }\n\n    if (isRecommendedSettingsRef.current) {\n      dispatch(\n        createFreeDbJob({\n          name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          resources: {\n            isRecommendedSettings: isRecommendedSettingsRef.current,\n          },\n          onSuccessAction: () => {\n            dispatch(\n              addInfiniteNotification(\n                INFINITE_MESSAGES.PENDING_CREATE_DB(CloudJobStep.Credentials),\n              ),\n            )\n          },\n          onFailAction: closeInfinityNotification,\n        }),\n      )\n\n      return\n    }\n\n    dispatch(fetchPlans())\n  }\n\n  const closeInfinityNotification = () => {\n    dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress))\n  }\n\n  const cloudOauthCallback = (\n    _e: any,\n    { status, message = '', error }: CloudAuthResponse,\n  ) => {\n    if (status === CloudAuthStatus.Succeed) {\n      dispatch(\n        setJob({\n          id: '',\n          name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          status: '',\n        }),\n      )\n      localStorageService.remove(BrowserStorageItem.OAuthJobId)\n      dispatch(showOAuthProgress(true))\n      dispatch(addInfiniteNotification(INFINITE_MESSAGES.AUTHENTICATING()))\n      dispatch(setSocialDialogState(null))\n      dispatch(fetchUserInfo(fetchUserInfoSuccess, closeInfinityNotification))\n      isFlowInProgress.current = true\n    }\n\n    if (status === CloudAuthStatus.Failed) {\n      // don't do anything, because we are processing something\n      // covers situation when were made several clicks on the same time\n      if (isFlowInProgress.current) {\n        return\n      }\n\n      const err = parseCustomError((error as CustomError) || message || '')\n      dispatch(setOAuthCloudSource(null))\n      dispatch(signInFailure(err?.response?.data?.message || message))\n      dispatch(addErrorNotification(err))\n    }\n  }\n\n  return null\n}\n\nexport default ConfigOAuth\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/ConfigOAuth/index.tsx",
    "content": "import ConfigElectron from './ConfigOAuth'\n\nexport default ConfigElectron\n"
  },
  {
    "path": "redisinsight/ui/src/electron/components/index.ts",
    "content": "import ConfigElectron from './ConfigElectron'\nimport ConfigOAuth from './ConfigOAuth'\nimport ConfigAzureAuth from './ConfigAzureAuth'\n\nexport { ConfigElectron, ConfigOAuth, ConfigAzureAuth }\n"
  },
  {
    "path": "redisinsight/ui/src/electron/constants/cloudAuth.ts",
    "content": "export enum CloudAuthStatus {\n  Succeed = 'succeed',\n  Failed = 'failed',\n}\n\nexport interface CloudAuthResponse {\n  status: CloudAuthStatus\n  message?: string\n  error?: object | string\n}\n\nexport enum CloudJobStatus {\n  Initializing = 'initializing',\n  Running = 'running',\n  Finished = 'finished',\n  Failed = 'failed',\n}\n\nexport enum CloudJobStep {\n  Credentials = 'credentials',\n  Subscription = 'subscription',\n  Database = 'database',\n  Import = 'import',\n}\n\nexport enum CloudJobName {\n  CreateFreeDatabase = 'CREATE_FREE_DATABASE',\n  CreateFreeSubscription = 'CREATE_FREE_SUBSCRIPTION',\n  CreateFreeSubscriptionAndDatabase = 'CREATE_FREE_SUBSCRIPTION_AND_DATABASE',\n  ImportFreeDatabase = 'IMPORT_FREE_DATABASE',\n  WaitForActiveDatabase = 'WAIT_FOR_ACTIVE_DATABASE',\n  WaitForActiveSubscription = 'WAIT_FOR_ACTIVE_SUBSCRIPTION',\n  WaitForTask = 'WAIT_FOR_TASK',\n  Unknown = 'UNKNOWN',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/electron/constants/index.ts",
    "content": "import ElectronStorageItem from './storageElectron'\n\nexport * from './ipcEvent'\nexport * from './cloudAuth'\n\nexport { ElectronStorageItem }\n"
  },
  {
    "path": "redisinsight/ui/src/electron/constants/ipcEvent.ts",
    "content": "enum IpcInvokeEvent {\n  getStoreValue = 'store:get:value',\n  deleteStoreValue = 'store:delete:value',\n  getAppVersion = 'app:get:version',\n  cloudOauth = 'cloud:oauth',\n  windowOpen = 'window:open',\n  themeChange = 'theme:change',\n  appRestart = 'app:restart',\n}\n\nenum IpcOnEvent {\n  sendWindowId = 'window:send:id',\n  cloudOauthCallback = 'cloud:oauth:callback',\n  azureOauthCallback = 'azure:oauth:callback',\n  deepLinkAction = 'deep-link:action',\n  appUpdateAvailable = 'app:update:available',\n}\n\nexport { IpcInvokeEvent, IpcOnEvent }\n"
  },
  {
    "path": "redisinsight/ui/src/electron/constants/storageElectron.ts",
    "content": "enum ElectronStorageItem {\n  updateDownloaded = 'updateDownloaded',\n  updateDownloadedForTelemetry = 'updateDownloadedForTelemetry',\n  updateDownloadedVersion = 'updateDownloadedVersion',\n  isUpdateAvailable = 'isUpdateAvailable',\n  isDisplayAppInTray = 'isDisplayAppInTray',\n  updatePreviousVersion = 'updatePreviousVersion',\n  zoomFactor = 'zoomFactor',\n  themeSource = 'themeSource',\n  bounds = 'bounds',\n}\n\nexport default ElectronStorageItem\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/index.ts",
    "content": "import { ipcCheckUpdates, ipcSendEvents } from './ipcCheckUpdates'\n\nexport * from './ipcAuth'\nexport * from './ipcAppRestart'\nexport * from './ipcThemeChange'\n\nexport { ipcCheckUpdates, ipcSendEvents }\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/ipcAppRestart.ts",
    "content": "import { IpcInvokeEvent } from 'uiSrc/electron/constants'\n\nexport const ipcAppRestart = async () => {\n  await window.app?.ipc?.invoke(IpcInvokeEvent.appRestart)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/ipcAuth.ts",
    "content": "import { OAuthStrategy } from 'uiSrc/slices/interfaces'\nimport { IpcInvokeEvent } from '../constants'\n\nexport const ipcAuth = async (\n  strategy: OAuthStrategy,\n  action: string,\n  data?: {},\n) => {\n  await window.app?.ipc?.invoke?.(IpcInvokeEvent.cloudOauth, {\n    strategy,\n    action,\n    data,\n  })\n}\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/ipcCheckUpdates.ts",
    "content": "import { Dispatch } from 'react'\nimport { omit } from 'lodash'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { ReleaseNotesSource } from 'uiSrc/constants/telemetry'\nimport { setElectronInfo, setReleaseNotesViewed } from 'uiSrc/slices/app/info'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto'\nimport { ElectronStorageItem, IpcInvokeEvent } from '../constants'\n\nexport const ipcCheckUpdates = async (\n  serverInfo: GetServerInfoResponse,\n  dispatch: Dispatch<any>,\n) => {\n  const isUpdateDownloaded = await window.app.ipc.invoke(\n    IpcInvokeEvent.getStoreValue,\n    ElectronStorageItem.updateDownloaded,\n  )\n  const isUpdateAvailable = await window.app.ipc.invoke(\n    IpcInvokeEvent.getStoreValue,\n    ElectronStorageItem.isUpdateAvailable,\n  )\n  const updateDownloadedVersion = await window.app.ipc.invoke(\n    IpcInvokeEvent.getStoreValue,\n    ElectronStorageItem.updateDownloadedVersion,\n  )\n\n  if (isUpdateDownloaded && !isUpdateAvailable) {\n    if (serverInfo.appVersion === updateDownloadedVersion) {\n      dispatch(\n        addMessageNotification(\n          successMessages.INSTALLED_NEW_UPDATE(updateDownloadedVersion, () => {\n            dispatch(setReleaseNotesViewed(true))\n            sendEventTelemetry({\n              event: TelemetryEvent.RELEASE_NOTES_LINK_CLICKED,\n              eventData: {\n                source: ReleaseNotesSource.updateNotification,\n              },\n            })\n          }),\n        ),\n      )\n    }\n\n    await window.app.ipc.invoke(\n      IpcInvokeEvent.deleteStoreValue,\n      ElectronStorageItem.updateDownloaded,\n    )\n  }\n\n  if (\n    updateDownloadedVersion &&\n    !isUpdateAvailable &&\n    serverInfo.appVersion === updateDownloadedVersion\n  ) {\n    dispatch(setReleaseNotesViewed(false))\n  }\n\n  dispatch(setElectronInfo({ updateDownloadedVersion, isUpdateAvailable }))\n}\n\nexport const ipcSendEvents = async (serverInfo: GetServerInfoResponse) => {\n  const isUpdateDownloadedForTelemetry = await window.app.ipc.invoke(\n    IpcInvokeEvent.getStoreValue,\n    ElectronStorageItem.updateDownloadedForTelemetry,\n  )\n  const isUpdateAvailable = await window.app.ipc.invoke(\n    IpcInvokeEvent.getStoreValue,\n    ElectronStorageItem.isUpdateAvailable,\n  )\n\n  if (isUpdateDownloadedForTelemetry && !isUpdateAvailable) {\n    const newVer = await window.app.ipc.invoke(\n      IpcInvokeEvent.getStoreValue,\n      ElectronStorageItem.updateDownloadedVersion,\n    )\n    const prevVer = await window.app.ipc.invoke(\n      IpcInvokeEvent.getStoreValue,\n      ElectronStorageItem.updatePreviousVersion,\n    )\n    sendEventTelemetry({\n      event: TelemetryEvent.APPLICATION_UPDATED,\n      eventData: {\n        ...omit(serverInfo, ['id', 'createDateTime']),\n        fromVersion: prevVer,\n        toVersion: newVer,\n      },\n    })\n    await window.app.ipc.invoke(\n      IpcInvokeEvent.deleteStoreValue,\n      ElectronStorageItem.updateDownloadedForTelemetry,\n    )\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/ipcDeleteStoreValues.ts",
    "content": "import { ElectronStorageItem, IpcInvokeEvent } from 'uiSrc/electron/constants'\n\nexport const ipcDeleteDownloadedVersion = async () => {\n  await window.app.ipc.invoke(\n    IpcInvokeEvent.deleteStoreValue,\n    ElectronStorageItem.updateDownloadedVersion,\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/ipcThemeChange.ts",
    "content": "import { IpcInvokeEvent } from '../constants'\n\nexport const ipcThemeChange = async (value: string) => {\n  await window.app?.ipc?.invoke?.(IpcInvokeEvent.themeChange, value)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/electron/utils/tests/ipcCheckUpdates.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport { cleanup, mockedStore } from 'uiSrc/utils/test-utils'\nimport { ipcCheckUpdates, ipcSendEvents } from '../ipcCheckUpdates'\n\nconst invokeMock = jest.fn()\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  window.app = {\n    ipc: { invoke: invokeMock },\n  }\n})\n\ndescribe('ipcCheckUpdates', () => {\n  it('should call localStorageService.getAll if optimization needed', () => {\n    const appVersionMock = '1'\n    invokeMock\n      .mockReturnValueOnce(true)\n      .mockReturnValueOnce(false)\n      .mockReturnValueOnce(appVersionMock)\n\n    ipcCheckUpdates({ appVersion: appVersionMock }, () => {})\n\n    expect(invokeMock).toBeCalled()\n  })\n})\ndescribe('ipcSendEvents', () => {\n  it('should call localStorageService.getAll if optimization needed', () => {\n    const appVersionMock = '1'\n    invokeMock.mockReturnValueOnce(true).mockReturnValue(false)\n\n    ipcSendEvents({ appVersion: appVersionMock })\n\n    expect(invokeMock).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/helpers/constructKeysToTree.ts",
    "content": "import { SortOrder } from 'uiSrc/constants'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\n\ninterface Props {\n  items: IKeyPropTypes[]\n  delimiterPattern?: string\n  delimiters?: string[]\n  sorting?: SortOrder\n}\n\nexport const constructKeysToTree = (props: Props): any[] => {\n  const {\n    items: keys,\n    delimiterPattern = ':',\n    delimiters = [],\n    sorting = 'ASC',\n  } = props\n  const keysSymbol = `keys${delimiterPattern}keys`\n  const tree: any = {}\n\n  keys.forEach((key: any) => {\n    // eslint-disable-next-line prefer-object-spread\n    let currentNode: any = tree\n    const { nameString: name = '' } = key\n    const nameSplitted = name.split(new RegExp(delimiterPattern, 'g'))\n    const lastIndex = nameSplitted.length - 1\n\n    nameSplitted.forEach((value: any, index: number) => {\n      // create a key leaf\n      if (index === lastIndex) {\n        // eslint-disable-next-line prefer-object-spread\n        currentNode[name + keysSymbol] = Object.assign({}, key, {\n          isLeaf: true,\n        })\n      } else if (currentNode[value] === undefined) {\n        currentNode[value] = {}\n      }\n\n      currentNode = currentNode[value]\n    })\n  })\n\n  const ids: any = {}\n\n  // common functions\n  const getUniqueId = (): number | string => {\n    const candidateId = Math.random().toString(36)\n\n    if (ids[candidateId]) {\n      return getUniqueId()\n    }\n\n    ids[candidateId] = true\n    return candidateId\n  }\n\n  // Folders should be always before leaves\n  const sortKeysAndFolder = (nodes: string[]) => {\n    nodes.sort((a, b) => {\n      // Custom sorting for items ending with \"keys:keys\"\n      if (a.endsWith(keysSymbol) && !b.endsWith(keysSymbol)) {\n        return 1\n      }\n      if (!a.endsWith(keysSymbol) && b.endsWith(keysSymbol)) {\n        return -1\n      }\n\n      // Regular sorting\n      if (sorting === 'ASC') {\n        return a.localeCompare(b, 'en')\n      }\n      if (sorting === 'DESC') {\n        return b.localeCompare(a, 'en')\n      }\n\n      return 0\n    })\n  }\n\n  // FormatTreeData\n  const formatTreeData = (\n    tree: any,\n    previousKey = '',\n    delimiter = ':',\n    prevIndex = '',\n  ) => {\n    const treeNodes: string[] = Object.keys(tree)\n\n    sortKeysAndFolder(treeNodes)\n\n    return treeNodes.map((key, index) => {\n      const name = key?.toString()\n      const node: any = { nameString: name }\n      const path = prevIndex ? `${prevIndex}.${index}` : `${index}`\n\n      // populate node with children nodes\n      if (!tree[key].isLeaf && Object.keys(tree[key]).length > 0) {\n        const delimiterView = delimiters.length === 1 ? delimiters[0] : '-'\n        node.children = formatTreeData(\n          tree[key],\n          `${previousKey + name + delimiterView}`,\n          delimiter,\n          path,\n        )\n        node.keyCount = node.children.reduce(\n          (a: any, b: any) => a + (b.keyCount || 1),\n          0,\n        )\n        node.keyApproximate = (node.keyCount / keys.length) * 100\n        node.fullName = previousKey + name\n      } else {\n        // populate leaf\n        node.isLeaf = true\n        node.children = []\n        node.nameString = name.slice(0, -keysSymbol.length)\n        node.nameBuffer = tree[key]?.name\n        node.fullName = previousKey + name + delimiter\n      }\n\n      node.path = path\n      node.id = getUniqueId()\n      return node\n    })\n  }\n\n  return formatTreeData(tree, '', delimiterPattern)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/helpers/index.ts",
    "content": "export * from './constructKeysToTree'\n"
  },
  {
    "path": "redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts",
    "content": "import {\n  constructKeysToTreeMockResult,\n  delimiterMock,\n} from './constructKeysToTreeMockResult'\nimport { constructKeysToTree } from '../constructKeysToTree'\n\nconst constructKeysToTreeTests: any[] = [\n  [\n    {\n      items: [\n        { nameString: 'keys:1:2', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'keys:1:1', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'empty::test', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'test1', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'test2', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'keys:1', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'keys1', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'keys:3', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'keys:2', type: 'hash', ttl: -1, size: 71 },\n        { nameString: 'keys_2', type: 'hash', ttl: -1, size: 71 },\n      ],\n      delimiterPattern: delimiterMock,\n    },\n    constructKeysToTreeMockResult,\n  ],\n]\n\nconst removeIds = (nodes: any[]) =>\n  nodes.map(({ children, id: _id, ...rest }) => ({\n    ...rest,\n    children: removeIds(children),\n  }))\n\ndescribe('constructKeysToTree', () => {\n  it.each(constructKeysToTreeTests)(\n    'for input: %s (items), should be output: %s',\n    (items, expected) => {\n      const result = constructKeysToTree(items)\n      expect(removeIds(result)).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts",
    "content": "export const delimiterMock = ':|_'\nexport const constructKeysToTreeMockResult = [\n  {\n    nameString: 'empty',\n    children: [\n      {\n        nameString: '',\n        children: [\n          {\n            nameString: 'empty::test',\n            isLeaf: true,\n            children: [],\n            path: '0.0.0',\n            fullName: `empty--empty::testkeys${delimiterMock}keys${delimiterMock}`,\n          },\n        ],\n        keyCount: 1,\n        keyApproximate: 10,\n        path: '0.0',\n        fullName: 'empty-',\n      },\n    ],\n    keyCount: 1,\n    keyApproximate: 10,\n    path: '0',\n    fullName: 'empty',\n  },\n  {\n    nameString: 'keys',\n    children: [\n      {\n        nameString: '1',\n        children: [\n          {\n            nameString: 'keys:1:1',\n            isLeaf: true,\n            children: [],\n            path: '1.0.0',\n            fullName: `keys-1-keys:1:1keys${delimiterMock}keys${delimiterMock}`,\n          },\n          {\n            nameString: 'keys:1:2',\n            isLeaf: true,\n            children: [],\n            path: '1.0.1',\n            fullName: `keys-1-keys:1:2keys${delimiterMock}keys${delimiterMock}`,\n          },\n        ],\n        keyCount: 2,\n        keyApproximate: 20,\n        path: '1.0',\n        fullName: 'keys-1',\n      },\n      {\n        nameString: 'keys_2',\n        isLeaf: true,\n        children: [],\n        path: '1.1',\n        fullName: `keys-keys_2keys${delimiterMock}keys${delimiterMock}`,\n      },\n      {\n        nameString: 'keys:1',\n        isLeaf: true,\n        children: [],\n        path: '1.2',\n        fullName: `keys-keys:1keys${delimiterMock}keys${delimiterMock}`,\n      },\n      {\n        nameString: 'keys:2',\n        isLeaf: true,\n        children: [],\n        path: '1.3',\n        fullName: `keys-keys:2keys${delimiterMock}keys${delimiterMock}`,\n      },\n      {\n        nameString: 'keys:3',\n        isLeaf: true,\n        children: [],\n        path: '1.4',\n        fullName: `keys-keys:3keys${delimiterMock}keys${delimiterMock}`,\n      },\n    ],\n    keyCount: 6,\n    keyApproximate: 60,\n    path: '1',\n    fullName: 'keys',\n  },\n  {\n    nameString: 'keys1',\n    isLeaf: true,\n    children: [],\n    path: '2',\n    fullName: `keys1keys${delimiterMock}keys${delimiterMock}`,\n  },\n  {\n    nameString: 'test1',\n    isLeaf: true,\n    children: [],\n    path: '3',\n    fullName: `test1keys${delimiterMock}keys${delimiterMock}`,\n  },\n  {\n    nameString: 'test2',\n    isLeaf: true,\n    children: [],\n    path: '4',\n    fullName: `test2keys${delimiterMock}keys${delimiterMock}`,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/hoc/extractRouter.hoc.tsx",
    "content": "import React from 'react'\nimport * as H from 'history'\nimport { withRouter } from 'react-router-dom'\n\ninterface Props {\n  history: H.History\n  location: H.Location\n  match: any\n}\n\nconst extractRouter: any = (onRouter: any) => (WrappedComponent: any) =>\n  withRouter(\n    class extends React.Component<Props> {\n      componentDidMount() {\n        const { match, location, history }: any = this.props\n        const router = { route: { match, location }, history }\n        onRouter(router)\n      }\n\n      render() {\n        return <WrappedComponent {...this.props} />\n      }\n    },\n  )\n\nexport default extractRouter\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/content/content.ts",
    "content": "export const MOCKED_CREATE_REDIS_BTN_CONTENT = {\n  cloud: {\n    title: 'Try Redis Cloud: your ultimate Redis starting point',\n    description: 'Includes native support for JSON, Query and Search, and more',\n    links: {\n      main: {\n        altText: 'Try Redis Cloud.',\n        url: 'https://redis.io/redis-enterprise-cloud/overview/?utm_source=redisinsight&utm_medium=main&utm_campaign=main',\n      },\n    },\n  },\n  source: {\n    title: 'Build from source',\n    description: '',\n    links: {\n      main: {\n        altText: 'Build from source',\n        url: 'https://developer.redis.com/create/from-source/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight',\n      },\n    },\n  },\n  docker: {\n    title: 'Docker',\n    description: '',\n    links: {\n      main: {\n        altText: 'Docker',\n        url: 'https://developer.redis.com/create/docker/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight',\n      },\n    },\n  },\n  homebrew: {\n    title: 'Homebrew',\n    description: '',\n    links: {\n      main: {\n        altText: 'Homebrew',\n        url: 'https://developer.redis.com/create/homebrew/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/analysis.ts",
    "content": "export const MOCK_ANALYSIS_REPORT_DATA = {\n  id: '1',\n  databaseId: 'id',\n  filter: {\n    type: null,\n    match: '*',\n    count: 10000,\n  },\n  delimiter: ':',\n  progress: {\n    total: 77,\n    scanned: 10000,\n    processed: 77,\n  },\n  createdAt: '2022-10-06T13:09:35.000Z',\n  totalKeys: {\n    total: 77,\n    types: [\n      {\n        type: 'hash',\n        total: 19,\n      },\n      {\n        type: 'TSDB-TYPE',\n        total: 11,\n      },\n      {\n        type: 'string',\n        total: 11,\n      },\n      {\n        type: 'list',\n        total: 9,\n      },\n      {\n        type: 'stream',\n        total: 8,\n      },\n      {\n        type: 'zset',\n        total: 8,\n      },\n      {\n        type: 'set',\n        total: 7,\n      },\n      {\n        type: 'graphdata',\n        total: 2,\n      },\n      {\n        type: 'ReJSON-RL',\n        total: 1,\n      },\n      {\n        type: 'MBbloom--',\n        total: 1,\n      },\n    ],\n  },\n  totalMemory: {\n    total: 11911834,\n    types: [\n      {\n        type: 'hash',\n        total: 11316165,\n      },\n      {\n        type: 'zset',\n        total: 233571,\n      },\n      {\n        type: 'set',\n        total: 138184,\n      },\n      {\n        type: 'list',\n        total: 95886,\n      },\n      {\n        type: 'stream',\n        total: 79532,\n      },\n      {\n        type: 'TSDB-TYPE',\n        total: 47143,\n      },\n      {\n        type: 'string',\n        total: 941,\n      },\n      {\n        type: 'MBbloom--',\n        total: 280,\n      },\n      {\n        type: 'graphdata',\n        total: 72,\n      },\n      {\n        type: 'ReJSON-RL',\n        total: 60,\n      },\n    ],\n  },\n  topKeysNsp: [\n    {\n      nsp: 'ASCII',\n      memory: 533642,\n      keys: 6,\n      types: [\n        {\n          type: 'hash',\n          memory: 266544,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 118784,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 72352,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 43337,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 32549,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 76,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Unicode',\n      memory: 490400,\n      keys: 6,\n      types: [\n        {\n          type: 'string',\n          memory: 64,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 257112,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 64360,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 111008,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 31312,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 26544,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Proto',\n      memory: 2275,\n      keys: 6,\n      types: [\n        {\n          type: 'stream',\n          memory: 1045,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 248,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 93,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 312,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 143,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 434,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Pickle',\n      memory: 3494,\n      keys: 6,\n      types: [\n        {\n          type: 'hash',\n          memory: 560,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 1144,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 849,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 413,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 152,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 376,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'PHP',\n      memory: 3096,\n      keys: 6,\n      types: [\n        {\n          type: 'zset',\n          memory: 1032,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 215,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 136,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 288,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 889,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 536,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Msgpack',\n      memory: 1752,\n      keys: 6,\n      types: [\n        {\n          type: 'set',\n          memory: 264,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 112,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 203,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 839,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 121,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 213,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'JSON',\n      memory: 2437,\n      keys: 6,\n      types: [\n        {\n          type: 'list',\n          memory: 156,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 232,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 73,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 653,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 1240,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 83,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'school',\n      memory: 3528,\n      keys: 4,\n      types: [\n        {\n          type: 'hash',\n          memory: 3528,\n          keys: 4,\n        },\n      ],\n    },\n    {\n      nsp: 'animals',\n      memory: 318,\n      keys: 3,\n      types: [\n        {\n          type: 'hash',\n          memory: 318,\n          keys: 3,\n        },\n      ],\n    },\n    {\n      nsp: 'Java',\n      memory: 1592,\n      keys: 2,\n      types: [\n        {\n          type: 'list',\n          memory: 336,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 1256,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Test',\n      memory: 60,\n      keys: 1,\n      types: [\n        {\n          type: 'ReJSON-RL',\n          memory: 60,\n          keys: 1,\n        },\n      ],\n    },\n  ],\n  topMemoryNsp: [\n    {\n      nsp: 'ASCII',\n      memory: 533642,\n      keys: 6,\n      types: [\n        {\n          type: 'hash',\n          memory: 266544,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 118784,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 72352,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 43337,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 32549,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 76,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Unicode',\n      memory: 490400,\n      keys: 6,\n      types: [\n        {\n          type: 'hash',\n          memory: 257112,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 111008,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 64360,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 31312,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 26544,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 64,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'school',\n      memory: 3528,\n      keys: 4,\n      types: [\n        {\n          type: 'hash',\n          memory: 3528,\n          keys: 4,\n        },\n      ],\n    },\n    {\n      nsp: 'Pickle',\n      memory: 3494,\n      keys: 6,\n      types: [\n        {\n          type: 'zset',\n          memory: 1144,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 849,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 560,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 413,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 376,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 152,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'PHP',\n      memory: 3096,\n      keys: 6,\n      types: [\n        {\n          type: 'zset',\n          memory: 1032,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 889,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 536,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 288,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 215,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 136,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'JSON',\n      memory: 2437,\n      keys: 6,\n      types: [\n        {\n          type: 'hash',\n          memory: 1240,\n          keys: 1,\n        },\n        {\n          type: 'stream',\n          memory: 653,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 232,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 156,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 83,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 73,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Proto',\n      memory: 2275,\n      keys: 6,\n      types: [\n        {\n          type: 'stream',\n          memory: 1045,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 434,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 312,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 248,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 143,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 93,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Msgpack',\n      memory: 1752,\n      keys: 6,\n      types: [\n        {\n          type: 'stream',\n          memory: 839,\n          keys: 1,\n        },\n        {\n          type: 'set',\n          memory: 264,\n          keys: 1,\n        },\n        {\n          type: 'hash',\n          memory: 213,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 203,\n          keys: 1,\n        },\n        {\n          type: 'zset',\n          memory: 121,\n          keys: 1,\n        },\n        {\n          type: 'string',\n          memory: 112,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'Java',\n      memory: 1592,\n      keys: 2,\n      types: [\n        {\n          type: 'zset',\n          memory: 1256,\n          keys: 1,\n        },\n        {\n          type: 'list',\n          memory: 336,\n          keys: 1,\n        },\n      ],\n    },\n    {\n      nsp: 'animals',\n      memory: 318,\n      keys: 3,\n      types: [\n        {\n          type: 'hash',\n          memory: 318,\n          keys: 3,\n        },\n      ],\n    },\n    {\n      nsp: 'Test',\n      memory: 60,\n      keys: 1,\n      types: [\n        {\n          type: 'ReJSON-RL',\n          memory: 60,\n          keys: 1,\n        },\n      ],\n    },\n  ],\n  topKeysLength: [\n    {\n      name: 'hugeHash',\n      type: 'hash',\n      memory: 10743352,\n      length: 40000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:list',\n      type: 'list',\n      memory: 32549,\n      length: 1001,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:list',\n      type: 'list',\n      memory: 26544,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:stream',\n      type: 'stream',\n      memory: 31312,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:zset',\n      type: 'zset',\n      memory: 111008,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:set',\n      type: 'set',\n      memory: 64360,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:stream',\n      type: 'stream',\n      memory: 43337,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:set',\n      type: 'set',\n      memory: 72352,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:zset',\n      type: 'zset',\n      memory: 118784,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:hash',\n      type: 'hash',\n      memory: 266544,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:hash',\n      type: 'hash',\n      memory: 257112,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Pickle:string',\n      type: 'string',\n      memory: 152,\n      length: 88,\n      ttl: -1,\n    },\n    {\n      name: 'PHP:string',\n      type: 'string',\n      memory: 136,\n      length: 74,\n      ttl: -1,\n    },\n    {\n      name: 'Msgpack:string',\n      type: 'string',\n      memory: 112,\n      length: 50,\n      ttl: -1,\n    },\n    {\n      name: 'Proto:string',\n      type: 'string',\n      memory: 93,\n      length: 35,\n      ttl: -1,\n    },\n  ],\n  topKeysMemory: [\n    {\n      name: 'hugeHash',\n      type: 'hash',\n      memory: 10743352,\n      length: 40000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:hash',\n      type: 'hash',\n      memory: 266544,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:hash',\n      type: 'hash',\n      memory: 257112,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:zset',\n      type: 'zset',\n      memory: 118784,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:zset',\n      type: 'zset',\n      memory: 111008,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:set',\n      type: 'set',\n      memory: 72352,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:set',\n      type: 'set',\n      memory: 64360,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:stream',\n      type: 'stream',\n      memory: 43337,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'vdasd',\n      type: 'hash',\n      memory: 41168,\n      length: 1,\n      ttl: -1,\n    },\n    {\n      name: 'List',\n      type: 'list',\n      memory: 35222,\n      length: 16,\n      ttl: -1,\n    },\n    {\n      name: 'ASCII:list',\n      type: 'list',\n      memory: 32549,\n      length: 1001,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:stream',\n      type: 'stream',\n      memory: 31312,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'Unicode:list',\n      type: 'list',\n      memory: 26544,\n      length: 1000,\n      ttl: -1,\n    },\n    {\n      name: 'bike_sales_3',\n      type: 'TSDB-TYPE',\n      memory: 4337,\n      length: 21,\n      ttl: -1,\n    },\n    {\n      name: 'vd_bike_sales_5_per_day1',\n      type: 'TSDB-TYPE',\n      memory: 4298,\n      length: 0,\n      ttl: -1,\n    },\n  ],\n  expirationGroups: [\n    {\n      label: 'No expiry',\n      total: 11911834,\n      threshold: 0,\n    },\n    {\n      label: '<1 hr',\n      total: 5345345,\n      threshold: 3600,\n    },\n    {\n      label: '1-4 Hrs',\n      total: 0,\n      threshold: 144000,\n    },\n    {\n      label: '4-12 Hrs',\n      total: 0,\n      threshold: 432000,\n    },\n    {\n      label: '12-24 Hrs',\n      total: 0,\n      threshold: 864000,\n    },\n    {\n      label: '1-7 Days',\n      total: 0,\n      threshold: 6048000,\n    },\n    {\n      label: '>7 Days',\n      total: 0,\n      threshold: 25920000,\n    },\n    {\n      label: '>1 Month',\n      total: 0,\n      threshold: 9007199254740991,\n    },\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/bigString.ts",
    "content": "import { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters'\n\nexport const MOCK_TRUNCATED_STRING_VALUE =\n  '[Truncated due to length] some value...'\nexport const MOCK_TRUNCATED_BUFFER_VALUE = stringToBuffer(\n  MOCK_TRUNCATED_STRING_VALUE,\n)\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/dateNow.ts",
    "content": "export const MOCK_TIMESTAMP = 1629128049027\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/instances.ts",
    "content": "export const MOCK_INFO_API_RESPONSE = {\n  version: '7.4.0',\n  usedMemory: 1234,\n  connectedClients: 2,\n  totalKeys: 123,\n  stats: {\n    uptime_in_days: '2',\n    maxmemory_policy: 'allkeys-lru',\n    instantaneous_ops_per_sec: 123,\n    instantaneous_input_kbps: 123,\n    instantaneous_output_kbps: 123,\n    numberOfKeysRange: '0-123',\n  },\n}\n\nexport const MOCK_ADDITIONAL_INFO = {\n  connected_clients: 2,\n  instantaneous_input_kbps: 123,\n  instantaneous_ops_per_sec: 123,\n  instantaneous_output_kbps: 123,\n  maxmemory_policy: 'allkeys-lru',\n  numberOfKeysRange: '0-123',\n  redis_version: '7.4.0',\n  totalKeys: 123,\n  uptime_in_days: '2',\n  used_memory: 1234,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/mocked_redis_commands.ts",
    "content": "export const MOCKED_REDIS_COMMANDS = {\n  'FT.SEARCH': {\n    summary:\n      'Searches the index with a textual query, returning either documents or just ids',\n    complexity: 'O(N)',\n    history: [['2.0.0', 'Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments']],\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n      {\n        name: 'nocontent',\n        type: 'pure-token',\n        token: 'NOCONTENT',\n        optional: true,\n      },\n      {\n        name: 'verbatim',\n        type: 'pure-token',\n        token: 'VERBATIM',\n        optional: true,\n      },\n      {\n        name: 'nostopwords',\n        type: 'pure-token',\n        token: 'NOSTOPWORDS',\n        optional: true,\n      },\n      {\n        name: 'withscores',\n        type: 'pure-token',\n        token: 'WITHSCORES',\n        optional: true,\n      },\n      {\n        name: 'withpayloads',\n        type: 'pure-token',\n        token: 'WITHPAYLOADS',\n        optional: true,\n      },\n      {\n        name: 'withsortkeys',\n        type: 'pure-token',\n        token: 'WITHSORTKEYS',\n        optional: true,\n      },\n      {\n        name: 'filter',\n        type: 'block',\n        optional: true,\n        multiple: true,\n        arguments: [\n          {\n            name: 'numeric_field',\n            type: 'string',\n            token: 'FILTER',\n          },\n          {\n            name: 'min',\n            type: 'double',\n          },\n          {\n            name: 'max',\n            type: 'double',\n          },\n        ],\n      },\n      {\n        name: 'geo_filter',\n        type: 'block',\n        optional: true,\n        multiple: true,\n        arguments: [\n          {\n            name: 'geo_field',\n            type: 'string',\n            token: 'GEOFILTER',\n          },\n          {\n            name: 'lon',\n            type: 'double',\n          },\n          {\n            name: 'lat',\n            type: 'double',\n          },\n          {\n            name: 'radius',\n            type: 'double',\n          },\n          {\n            name: 'radius_type',\n            type: 'oneof',\n            arguments: [\n              {\n                name: 'm',\n                type: 'pure-token',\n                token: 'm',\n              },\n              {\n                name: 'km',\n                type: 'pure-token',\n                token: 'km',\n              },\n              {\n                name: 'mi',\n                type: 'pure-token',\n                token: 'mi',\n              },\n              {\n                name: 'ft',\n                type: 'pure-token',\n                token: 'ft',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'in_keys',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'count',\n            type: 'string',\n            token: 'INKEYS',\n          },\n          {\n            name: 'key',\n            type: 'string',\n            multiple: true,\n          },\n        ],\n      },\n      {\n        name: 'in_fields',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'count',\n            type: 'string',\n            token: 'INFIELDS',\n          },\n          {\n            name: 'field',\n            type: 'string',\n            multiple: true,\n          },\n        ],\n      },\n      {\n        name: 'return',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'count',\n            type: 'string',\n            token: 'RETURN',\n          },\n          {\n            name: 'identifiers',\n            type: 'block',\n            multiple: true,\n            arguments: [\n              {\n                name: 'identifier',\n                type: 'string',\n              },\n              {\n                name: 'property',\n                type: 'string',\n                token: 'AS',\n                optional: true,\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'summarize',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'summarize',\n            type: 'pure-token',\n            token: 'SUMMARIZE',\n          },\n          {\n            name: 'fields',\n            type: 'block',\n            optional: true,\n            arguments: [\n              {\n                name: 'count',\n                type: 'string',\n                token: 'FIELDS',\n              },\n              {\n                name: 'field',\n                type: 'string',\n                multiple: true,\n              },\n            ],\n          },\n          {\n            name: 'num',\n            type: 'integer',\n            token: 'FRAGS',\n            optional: true,\n          },\n          {\n            name: 'fragsize',\n            type: 'integer',\n            token: 'LEN',\n            optional: true,\n          },\n          {\n            name: 'separator',\n            type: 'string',\n            token: 'SEPARATOR',\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'highlight',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'highlight',\n            type: 'pure-token',\n            token: 'HIGHLIGHT',\n          },\n          {\n            name: 'fields',\n            type: 'block',\n            optional: true,\n            arguments: [\n              {\n                name: 'count',\n                type: 'string',\n                token: 'FIELDS',\n              },\n              {\n                name: 'field',\n                type: 'string',\n                multiple: true,\n              },\n            ],\n          },\n          {\n            name: 'tags',\n            type: 'block',\n            optional: true,\n            arguments: [\n              {\n                name: 'tags',\n                type: 'pure-token',\n                token: 'TAGS',\n              },\n              {\n                name: 'open',\n                type: 'string',\n              },\n              {\n                name: 'close',\n                type: 'string',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'slop',\n        type: 'integer',\n        optional: true,\n        token: 'SLOP',\n      },\n      {\n        name: 'timeout',\n        type: 'integer',\n        optional: true,\n        token: 'TIMEOUT',\n      },\n      {\n        name: 'inorder',\n        type: 'pure-token',\n        token: 'INORDER',\n        optional: true,\n      },\n      {\n        name: 'language',\n        type: 'string',\n        optional: true,\n        token: 'LANGUAGE',\n      },\n      {\n        name: 'expander',\n        type: 'string',\n        optional: true,\n        token: 'EXPANDER',\n      },\n      {\n        name: 'scorer',\n        type: 'string',\n        optional: true,\n        token: 'SCORER',\n      },\n      {\n        name: 'explainscore',\n        type: 'pure-token',\n        token: 'EXPLAINSCORE',\n        optional: true,\n      },\n      {\n        name: 'payload',\n        type: 'string',\n        optional: true,\n        token: 'PAYLOAD',\n      },\n      {\n        name: 'sortby',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'sortby',\n            type: 'string',\n            token: 'SORTBY',\n          },\n          {\n            name: 'order',\n            type: 'oneof',\n            optional: true,\n            arguments: [\n              {\n                name: 'asc',\n                type: 'pure-token',\n                token: 'ASC',\n              },\n              {\n                name: 'desc',\n                type: 'pure-token',\n                token: 'DESC',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'limit',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'limit',\n            type: 'pure-token',\n            token: 'LIMIT',\n          },\n          {\n            name: 'offset',\n            type: 'integer',\n          },\n          {\n            name: 'num',\n            type: 'integer',\n          },\n        ],\n      },\n      {\n        name: 'params',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'params',\n            type: 'pure-token',\n            token: 'PARAMS',\n          },\n          {\n            name: 'nargs',\n            type: 'integer',\n          },\n          {\n            name: 'values',\n            type: 'block',\n            multiple: true,\n            arguments: [\n              {\n                name: 'name',\n                type: 'string',\n              },\n              {\n                name: 'value',\n                type: 'string',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n  },\n  'FT.AGGREGATE': {\n    summary:\n      'Run a search query on an index and perform aggregate transformations on the results',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n      {\n        name: 'verbatim',\n        type: 'pure-token',\n        token: 'VERBATIM',\n        optional: true,\n      },\n      {\n        name: 'load',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'count',\n            type: 'string',\n            token: 'LOAD',\n          },\n          {\n            name: 'field',\n            type: 'string',\n            multiple: true,\n          },\n        ],\n      },\n      {\n        name: 'timeout',\n        type: 'integer',\n        optional: true,\n        token: 'TIMEOUT',\n      },\n      {\n        name: 'loadall',\n        type: 'pure-token',\n        token: 'LOAD *',\n        optional: true,\n      },\n      {\n        name: 'groupby',\n        type: 'block',\n        optional: true,\n        multiple: true,\n        arguments: [\n          {\n            name: 'nargs',\n            type: 'integer',\n            token: 'GROUPBY',\n          },\n          {\n            name: 'property',\n            type: 'string',\n            multiple: true,\n          },\n          {\n            name: 'reduce',\n            type: 'block',\n            optional: true,\n            multiple: true,\n            arguments: [\n              {\n                name: 'function',\n                type: 'string',\n                token: 'REDUCE',\n              },\n              {\n                name: 'nargs',\n                type: 'integer',\n              },\n              {\n                name: 'arg',\n                type: 'string',\n                multiple: true,\n              },\n              {\n                name: 'name',\n                type: 'string',\n                token: 'AS',\n                optional: true,\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'sortby',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'nargs',\n            type: 'integer',\n            token: 'SORTBY',\n          },\n          {\n            name: 'fields',\n            type: 'block',\n            optional: true,\n            multiple: true,\n            arguments: [\n              {\n                name: 'property',\n                type: 'string',\n              },\n              {\n                name: 'order',\n                type: 'oneof',\n                arguments: [\n                  {\n                    name: 'asc',\n                    type: 'pure-token',\n                    token: 'ASC',\n                  },\n                  {\n                    name: 'desc',\n                    type: 'pure-token',\n                    token: 'DESC',\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            name: 'num',\n            type: 'integer',\n            token: 'MAX',\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'apply',\n        type: 'block',\n        optional: true,\n        multiple: true,\n        arguments: [\n          {\n            name: 'expression',\n            type: 'string',\n            token: 'APPLY',\n          },\n          {\n            name: 'name',\n            type: 'string',\n            token: 'AS',\n          },\n        ],\n      },\n      {\n        name: 'limit',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'limit',\n            type: 'pure-token',\n            token: 'LIMIT',\n          },\n          {\n            name: 'offset',\n            type: 'integer',\n          },\n          {\n            name: 'num',\n            type: 'integer',\n          },\n        ],\n      },\n      {\n        name: 'filter',\n        type: 'string',\n        optional: true,\n        token: 'FILTER',\n      },\n      {\n        name: 'cursor',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'withcursor',\n            type: 'pure-token',\n            token: 'WITHCURSOR',\n          },\n          {\n            name: 'read_size',\n            type: 'integer',\n            optional: true,\n            token: 'COUNT',\n          },\n          {\n            name: 'idle_time',\n            type: 'integer',\n            optional: true,\n            token: 'MAXIDLE',\n          },\n        ],\n      },\n      {\n        name: 'params',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'params',\n            type: 'pure-token',\n            token: 'PARAMS',\n          },\n          {\n            name: 'nargs',\n            type: 'integer',\n          },\n          {\n            name: 'values',\n            type: 'block',\n            multiple: true,\n            arguments: [\n              {\n                name: 'name',\n                type: 'string',\n              },\n              {\n                name: 'value',\n                type: 'string',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    ],\n    since: '1.1.0',\n    group: 'search',\n  },\n  'FT.PROFILE': {\n    summary:\n      'Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information',\n    complexity: 'O(N)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'querytype',\n        type: 'oneof',\n        arguments: [\n          {\n            name: 'search',\n            type: 'pure-token',\n            token: 'SEARCH',\n          },\n          {\n            name: 'aggregate',\n            type: 'pure-token',\n            token: 'AGGREGATE',\n          },\n        ],\n      },\n      {\n        name: 'limited',\n        type: 'pure-token',\n        token: 'LIMITED',\n        optional: true,\n      },\n      {\n        name: 'queryword',\n        type: 'pure-token',\n        token: 'QUERY',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n    ],\n    since: '2.2.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.ALIASADD': {\n    summary: 'Adds an alias to the index',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'alias',\n        type: 'string',\n      },\n      {\n        name: 'index',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.ALIASDEL': {\n    summary: 'Deletes an alias from the index',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'alias',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.ALIASUPDATE': {\n    summary: 'Adds or updates an alias to the index',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'alias',\n        type: 'string',\n      },\n      {\n        name: 'index',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.ALTER': {\n    summary: 'Adds a new field to the index',\n    complexity: 'O(N) where N is the number of keys in the keyspace',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'skipinitialscan',\n        type: 'pure-token',\n        token: 'SKIPINITIALSCAN',\n        optional: true,\n      },\n      {\n        name: 'schema',\n        type: 'pure-token',\n        token: 'SCHEMA',\n      },\n      {\n        name: 'add',\n        type: 'pure-token',\n        token: 'ADD',\n      },\n      {\n        name: 'field',\n        type: 'string',\n      },\n      {\n        name: 'options',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.CONFIG GET': {\n    summary: 'Retrieves runtime configuration options',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'option',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.CONFIG HELP': {\n    summary: 'Help description of runtime configuration options',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'option',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.CONFIG SET': {\n    summary: 'Sets runtime configuration options',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'option',\n        type: 'string',\n      },\n      {\n        name: 'value',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.CREATE': {\n    summary: 'Creates an index with the given spec',\n    complexity:\n      'O(K) at creation where K is the number of fields, O(N) if scanning the keyspace is triggered, where N is the number of keys in the keyspace',\n    history: [\n      [\n        '2.0.0',\n        'Added `PAYLOAD_FIELD` argument for backward support of `FT.SEARCH` deprecated `WITHPAYLOADS` argument',\n      ],\n      ['2.0.0', 'Deprecated `PAYLOAD_FIELD` argument'],\n    ],\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'data_type',\n        token: 'ON',\n        type: 'oneof',\n        arguments: [\n          {\n            name: 'hash',\n            type: 'pure-token',\n            token: 'HASH',\n          },\n          {\n            name: 'json',\n            type: 'pure-token',\n            token: 'JSON',\n          },\n        ],\n        optional: true,\n      },\n      {\n        name: 'prefix',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'count',\n            type: 'integer',\n            token: 'PREFIX',\n          },\n          {\n            name: 'prefix',\n            type: 'string',\n            multiple: true,\n          },\n        ],\n      },\n      {\n        name: 'filter',\n        type: 'string',\n        optional: true,\n        token: 'FILTER',\n      },\n      {\n        name: 'default_lang',\n        type: 'string',\n        token: 'LANGUAGE',\n        optional: true,\n      },\n      {\n        name: 'lang_attribute',\n        type: 'string',\n        token: 'LANGUAGE_FIELD',\n        optional: true,\n      },\n      {\n        name: 'default_score',\n        type: 'double',\n        token: 'SCORE',\n        optional: true,\n      },\n      {\n        name: 'score_attribute',\n        type: 'string',\n        token: 'SCORE_FIELD',\n        optional: true,\n      },\n      {\n        name: 'payload_attribute',\n        type: 'string',\n        token: 'PAYLOAD_FIELD',\n        optional: true,\n      },\n      {\n        name: 'maxtextfields',\n        type: 'pure-token',\n        token: 'MAXTEXTFIELDS',\n        optional: true,\n      },\n      {\n        name: 'seconds',\n        type: 'double',\n        token: 'TEMPORARY',\n        optional: true,\n      },\n      {\n        name: 'nooffsets',\n        type: 'pure-token',\n        token: 'NOOFFSETS',\n        optional: true,\n      },\n      {\n        name: 'nohl',\n        type: 'pure-token',\n        token: 'NOHL',\n        optional: true,\n      },\n      {\n        name: 'nofields',\n        type: 'pure-token',\n        token: 'NOFIELDS',\n        optional: true,\n      },\n      {\n        name: 'nofreqs',\n        type: 'pure-token',\n        token: 'NOFREQS',\n        optional: true,\n      },\n      {\n        name: 'stopwords',\n        type: 'block',\n        optional: true,\n        token: 'STOPWORDS',\n        arguments: [\n          {\n            name: 'count',\n            type: 'integer',\n          },\n          {\n            name: 'stopword',\n            type: 'string',\n            multiple: true,\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'skipinitialscan',\n        type: 'pure-token',\n        token: 'SKIPINITIALSCAN',\n        optional: true,\n      },\n      {\n        name: 'schema',\n        type: 'pure-token',\n        token: 'SCHEMA',\n      },\n      {\n        name: 'field',\n        type: 'block',\n        multiple: true,\n        arguments: [\n          {\n            name: 'field_name',\n            type: 'string',\n          },\n          {\n            name: 'alias',\n            type: 'string',\n            token: 'AS',\n            optional: true,\n          },\n          {\n            name: 'field_type',\n            type: 'oneof',\n            arguments: [\n              {\n                name: 'text',\n                type: 'pure-token',\n                token: 'TEXT',\n              },\n              {\n                name: 'tag',\n                type: 'pure-token',\n                token: 'TAG',\n              },\n              {\n                name: 'numeric',\n                type: 'pure-token',\n                token: 'NUMERIC',\n              },\n              {\n                name: 'geo',\n                type: 'pure-token',\n                token: 'GEO',\n              },\n              {\n                name: 'vector',\n                type: 'pure-token',\n                token: 'VECTOR',\n              },\n            ],\n          },\n          {\n            name: 'withsuffixtrie',\n            type: 'pure-token',\n            token: 'WITHSUFFIXTRIE',\n            optional: true,\n          },\n          {\n            name: 'INDEXEMPTY',\n            type: 'pure-token',\n            token: 'INDEXEMPTY',\n            optional: true,\n          },\n          {\n            name: 'indexmissing',\n            type: 'pure-token',\n            token: 'INDEXMISSING',\n            optional: true,\n          },\n          {\n            name: 'sortable',\n            type: 'block',\n            optional: true,\n            arguments: [\n              {\n                name: 'sortable',\n                type: 'pure-token',\n                token: 'SORTABLE',\n              },\n              {\n                name: 'UNF',\n                type: 'pure-token',\n                token: 'UNF',\n                optional: true,\n              },\n            ],\n          },\n          {\n            name: 'noindex',\n            type: 'pure-token',\n            token: 'NOINDEX',\n            optional: true,\n          },\n        ],\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.CURSOR DEL': {\n    summary: 'Deletes a cursor',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'cursor_id',\n        type: 'integer',\n      },\n    ],\n    since: '1.1.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.CURSOR READ': {\n    summary: 'Reads from a cursor',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'cursor_id',\n        type: 'integer',\n      },\n      {\n        name: 'read size',\n        type: 'integer',\n        optional: true,\n        token: 'COUNT',\n      },\n    ],\n    since: '1.1.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.DICTADD': {\n    summary: 'Adds terms to a dictionary',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'dict',\n        type: 'string',\n      },\n      {\n        name: 'term',\n        type: 'string',\n        multiple: true,\n      },\n    ],\n    since: '1.4.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.DICTDEL': {\n    summary: 'Deletes terms from a dictionary',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'dict',\n        type: 'string',\n      },\n      {\n        name: 'term',\n        type: 'string',\n        multiple: true,\n      },\n    ],\n    since: '1.4.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.DICTDUMP': {\n    summary: 'Dumps all terms in the given dictionary',\n    complexity: 'O(N), where N is the size of the dictionary',\n    arguments: [\n      {\n        name: 'dict',\n        type: 'string',\n      },\n    ],\n    since: '1.4.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.DROPINDEX': {\n    summary: 'Deletes the index',\n    complexity:\n      'O(1) or O(N) if documents are deleted, where N is the number of keys in the keyspace',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'delete docs',\n        type: 'oneof',\n        arguments: [\n          {\n            name: 'delete docs',\n            type: 'pure-token',\n            token: 'DD',\n          },\n        ],\n        optional: true,\n      },\n    ],\n    since: '2.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.EXPLAIN': {\n    summary: 'Returns the execution plan for a complex query',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n      {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.EXPLAINCLI': {\n    summary: 'Returns the execution plan for a complex query',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n      {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.INFO': {\n    summary: 'Returns information and statistics on the index',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.SPELLCHECK': {\n    summary:\n      'Performs spelling correction on a query, returning suggestions for misspelled terms',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'query',\n        type: 'string',\n      },\n      {\n        name: 'distance',\n        token: 'DISTANCE',\n        type: 'integer',\n        optional: true,\n      },\n      {\n        name: 'terms',\n        token: 'TERMS',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'inclusion',\n            type: 'oneof',\n            arguments: [\n              {\n                name: 'include',\n                type: 'pure-token',\n                token: 'INCLUDE',\n              },\n              {\n                name: 'exclude',\n                type: 'pure-token',\n                token: 'EXCLUDE',\n              },\n            ],\n          },\n          {\n            name: 'dictionary',\n            type: 'string',\n          },\n          {\n            name: 'terms',\n            type: 'string',\n            multiple: true,\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    ],\n    since: '1.4.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.SUGADD': {\n    summary:\n      'Adds a suggestion string to an auto-complete suggestion dictionary',\n    complexity: 'O(1)',\n    history: [['2.0.0', 'Deprecated `PAYLOAD` argument']],\n    arguments: [\n      {\n        name: 'key',\n        type: 'string',\n      },\n      {\n        name: 'string',\n        type: 'string',\n      },\n      {\n        name: 'score',\n        type: 'double',\n      },\n      {\n        name: 'increment score',\n        type: 'oneof',\n        arguments: [\n          {\n            name: 'incr',\n            type: 'pure-token',\n            token: 'INCR',\n          },\n        ],\n        optional: true,\n      },\n      {\n        name: 'payload',\n        token: 'PAYLOAD',\n        type: 'string',\n        optional: true,\n      },\n    ],\n    since: '1.0.0',\n    group: 'suggestion',\n    provider: 'redisearch',\n  },\n  'FT.SUGDEL': {\n    summary: 'Deletes a string from a suggestion index',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'key',\n        type: 'string',\n      },\n      {\n        name: 'string',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'suggestion',\n    provider: 'redisearch',\n  },\n  'FT.SUGGET': {\n    summary: 'Gets completion suggestions for a prefix',\n    complexity: 'O(1)',\n    history: [['2.0.0', 'Deprecated `WITHPAYLOADS` argument']],\n    arguments: [\n      {\n        name: 'key',\n        type: 'string',\n      },\n      {\n        name: 'prefix',\n        type: 'string',\n      },\n      {\n        name: 'fuzzy',\n        type: 'pure-token',\n        token: 'FUZZY',\n        optional: true,\n      },\n      {\n        name: 'withscores',\n        type: 'pure-token',\n        token: 'WITHSCORES',\n        optional: true,\n      },\n      {\n        name: 'withpayloads',\n        type: 'pure-token',\n        token: 'WITHPAYLOADS',\n        optional: true,\n      },\n      {\n        name: 'max',\n        token: 'MAX',\n        type: 'integer',\n        optional: true,\n      },\n    ],\n    since: '1.0.0',\n    group: 'suggestion',\n    provider: 'redisearch',\n  },\n  'FT.SUGLEN': {\n    summary: 'Gets the size of an auto-complete suggestion dictionary',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'key',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'suggestion',\n    provider: 'redisearch',\n  },\n  'FT.SYNDUMP': {\n    summary: 'Dumps the contents of a synonym group',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n    ],\n    since: '1.2.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.SYNUPDATE': {\n    summary: 'Creates or updates a synonym group with additional terms',\n    complexity: 'O(1)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'synonym_group_id',\n        type: 'string',\n      },\n      {\n        name: 'skipinitialscan',\n        type: 'pure-token',\n        token: 'SKIPINITIALSCAN',\n        optional: true,\n      },\n      {\n        name: 'term',\n        type: 'string',\n        multiple: true,\n      },\n    ],\n    since: '1.2.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.TAGVALS': {\n    summary: 'Returns the distinct tags indexed in a Tag field',\n    complexity: 'O(N)',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'field_name',\n        type: 'string',\n      },\n    ],\n    since: '1.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT._LIST': {\n    summary: 'Returns a list of all existing indexes',\n    complexity: 'O(1)',\n    since: '2.0.0',\n    group: 'search',\n    provider: 'redisearch',\n  },\n  'FT.HYBRID': {\n    summary:\n      'Performs hybrid search combining text search and vector similarity search',\n    complexity:\n      'O(N+M) where N is the complexity of the text search and M is the complexity of the vector search',\n    arguments: [\n      {\n        name: 'index',\n        type: 'string',\n      },\n      {\n        name: 'search_clause',\n        type: 'block',\n        arguments: [\n          {\n            name: 'search',\n            type: 'pure-token',\n            token: 'SEARCH',\n          },\n          {\n            name: 'query',\n            type: 'string',\n          },\n          {\n            name: 'scorer',\n            type: 'string',\n            token: 'SCORER',\n            optional: true,\n          },\n          {\n            name: 'yield_score_as',\n            type: 'string',\n            token: 'YIELD_SCORE_AS',\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'vsim_clause',\n        type: 'block',\n        arguments: [\n          {\n            name: 'vsim',\n            type: 'pure-token',\n            token: 'VSIM',\n          },\n          {\n            name: 'field',\n            type: 'string',\n          },\n          {\n            name: 'vector',\n            type: 'string',\n          },\n          {\n            name: 'vector_query_type',\n            type: 'oneof',\n            optional: true,\n            arguments: [\n              {\n                name: 'knn_clause',\n                type: 'block',\n                arguments: [\n                  {\n                    name: 'knn',\n                    type: 'pure-token',\n                    token: 'KNN',\n                  },\n                  {\n                    name: 'count',\n                    type: 'integer',\n                  },\n                  {\n                    name: 'k',\n                    type: 'integer',\n                    token: 'K',\n                  },\n                  {\n                    name: 'ef_runtime',\n                    type: 'integer',\n                    token: 'EF_RUNTIME',\n                    optional: true,\n                  },\n                  {\n                    name: 'yield_score_as',\n                    type: 'string',\n                    token: 'YIELD_SCORE_AS',\n                    optional: true,\n                  },\n                ],\n              },\n              {\n                name: 'range_clause',\n                type: 'block',\n                arguments: [\n                  {\n                    name: 'range',\n                    type: 'pure-token',\n                    token: 'RANGE',\n                  },\n                  {\n                    name: 'count',\n                    type: 'integer',\n                  },\n                  {\n                    name: 'radius',\n                    type: 'double',\n                    token: 'RADIUS',\n                  },\n                  {\n                    name: 'epsilon',\n                    type: 'double',\n                    token: 'EPSILON',\n                    optional: true,\n                  },\n                  {\n                    name: 'yield_score_as',\n                    type: 'string',\n                    token: 'YIELD_SCORE_AS',\n                    optional: true,\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            name: 'filter',\n            type: 'string',\n            token: 'FILTER',\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'combine',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'combine',\n            type: 'pure-token',\n            token: 'COMBINE',\n          },\n          {\n            name: 'method',\n            type: 'oneof',\n            arguments: [\n              {\n                name: 'rrf_method',\n                type: 'block',\n                arguments: [\n                  {\n                    name: 'rrf',\n                    type: 'pure-token',\n                    token: 'RRF',\n                  },\n                  {\n                    name: 'count',\n                    type: 'integer',\n                  },\n                  {\n                    name: 'constant',\n                    type: 'double',\n                    token: 'CONSTANT',\n                    optional: true,\n                  },\n                  {\n                    name: 'window',\n                    type: 'integer',\n                    token: 'WINDOW',\n                    optional: true,\n                  },\n                  {\n                    name: 'yield_score_as',\n                    type: 'string',\n                    token: 'YIELD_SCORE_AS',\n                    optional: true,\n                  },\n                ],\n              },\n              {\n                name: 'linear_method',\n                type: 'block',\n                arguments: [\n                  {\n                    name: 'linear',\n                    type: 'pure-token',\n                    token: 'LINEAR',\n                  },\n                  {\n                    name: 'count',\n                    type: 'integer',\n                  },\n                  {\n                    name: 'alpha',\n                    type: 'double',\n                    token: 'ALPHA',\n                  },\n                  {\n                    name: 'beta',\n                    type: 'double',\n                    token: 'BETA',\n                  },\n                  {\n                    name: 'window',\n                    type: 'integer',\n                    token: 'WINDOW',\n                    optional: true,\n                  },\n                  {\n                    name: 'yield_score_as',\n                    type: 'string',\n                    token: 'YIELD_SCORE_AS',\n                    optional: true,\n                  },\n                ],\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'limit',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'limit',\n            type: 'pure-token',\n            token: 'LIMIT',\n          },\n          {\n            name: 'offset',\n            type: 'integer',\n          },\n          {\n            name: 'num',\n            type: 'integer',\n          },\n        ],\n      },\n      {\n        name: 'sorting',\n        type: 'oneof',\n        optional: true,\n        arguments: [\n          {\n            name: 'sortby',\n            type: 'block',\n            arguments: [\n              {\n                name: 'sortby',\n                type: 'string',\n                token: 'SORTBY',\n              },\n              {\n                name: 'order',\n                type: 'oneof',\n                optional: true,\n                arguments: [\n                  {\n                    name: 'asc',\n                    type: 'pure-token',\n                    token: 'ASC',\n                  },\n                  {\n                    name: 'desc',\n                    type: 'pure-token',\n                    token: 'DESC',\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            name: 'nosort',\n            type: 'pure-token',\n            token: 'NOSORT',\n          },\n        ],\n      },\n      {\n        name: 'params',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'params',\n            type: 'pure-token',\n            token: 'PARAMS',\n          },\n          {\n            name: 'nargs',\n            type: 'integer',\n          },\n          {\n            name: 'values',\n            type: 'block',\n            multiple: true,\n            arguments: [\n              {\n                name: 'name',\n                type: 'string',\n              },\n              {\n                name: 'value',\n                type: 'string',\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'timeout',\n        type: 'integer',\n        optional: true,\n        token: 'TIMEOUT',\n      },\n      {\n        name: 'format',\n        type: 'string',\n        optional: true,\n        token: 'FORMAT',\n      },\n      {\n        name: 'load',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'load',\n            type: 'pure-token',\n            token: 'LOAD',\n          },\n          {\n            name: 'count_or_all',\n            type: 'oneof',\n            arguments: [\n              {\n                name: 'count',\n                type: 'integer',\n              },\n              {\n                name: 'all',\n                type: 'pure-token',\n                token: '*',\n              },\n            ],\n          },\n          {\n            name: 'field',\n            type: 'string',\n            multiple: true,\n            optional: true,\n          },\n        ],\n      },\n      {\n        name: 'groupby',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'groupby',\n            type: 'pure-token',\n            token: 'GROUPBY',\n          },\n          {\n            name: 'nproperties',\n            type: 'integer',\n          },\n          {\n            name: 'property',\n            type: 'string',\n            multiple: true,\n          },\n          {\n            name: 'reduce',\n            type: 'block',\n            optional: true,\n            multiple: true,\n            arguments: [\n              {\n                name: 'reduce',\n                type: 'pure-token',\n                token: 'REDUCE',\n              },\n              {\n                name: 'function',\n                type: 'oneof',\n                arguments: [\n                  {\n                    name: 'count',\n                    type: 'pure-token',\n                    token: 'COUNT',\n                  },\n                  {\n                    name: 'count_distinct',\n                    type: 'pure-token',\n                    token: 'COUNT_DISTINCT',\n                  },\n                  {\n                    name: 'count_distinctish',\n                    type: 'pure-token',\n                    token: 'COUNT_DISTINCTISH',\n                  },\n                  {\n                    name: 'sum',\n                    type: 'pure-token',\n                    token: 'SUM',\n                  },\n                  {\n                    name: 'min',\n                    type: 'pure-token',\n                    token: 'MIN',\n                  },\n                  {\n                    name: 'max',\n                    type: 'pure-token',\n                    token: 'MAX',\n                  },\n                  {\n                    name: 'avg',\n                    type: 'pure-token',\n                    token: 'AVG',\n                  },\n                  {\n                    name: 'stddev',\n                    type: 'pure-token',\n                    token: 'STDDEV',\n                  },\n                  {\n                    name: 'quantile',\n                    type: 'pure-token',\n                    token: 'QUANTILE',\n                  },\n                  {\n                    name: 'tolist',\n                    type: 'pure-token',\n                    token: 'TOLIST',\n                  },\n                  {\n                    name: 'first_value',\n                    type: 'pure-token',\n                    token: 'FIRST_VALUE',\n                  },\n                  {\n                    name: 'random_sample',\n                    type: 'pure-token',\n                    token: 'RANDOM_SAMPLE',\n                  },\n                ],\n              },\n              {\n                name: 'nargs',\n                type: 'integer',\n              },\n              {\n                name: 'arg',\n                type: 'string',\n                multiple: true,\n              },\n              {\n                name: 'name',\n                type: 'string',\n                token: 'AS',\n                optional: true,\n              },\n            ],\n          },\n        ],\n      },\n      {\n        name: 'apply',\n        type: 'block',\n        optional: true,\n        multiple: true,\n        arguments: [\n          {\n            name: 'expression',\n            type: 'string',\n            expression: true,\n            token: 'APPLY',\n            arguments: [\n              {\n                name: 'exists',\n                token: 'exists',\n                type: 'function',\n                summary: 'Checks whether a field exists in a document.',\n                arguments: [\n                  {\n                    token: 's',\n                  },\n                ],\n              },\n              {\n                name: 'log',\n                token: 'log',\n                type: 'function',\n                summary:\n                  'Return the logarithm of a number, property or subexpression',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'abs',\n                token: 'abs',\n                type: 'function',\n                summary: 'Return the absolute value of a numeric expression',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'ceil',\n                token: 'ceil',\n                type: 'function',\n                summary: 'Round to the smallest integer not less than x',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'floor',\n                token: 'floor',\n                type: 'function',\n                summary: 'Round to largest integer not greater than x',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'log2',\n                token: 'log2',\n                type: 'function',\n                summary: 'Return the logarithm of x to base 2',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'exp',\n                token: 'exp',\n                type: 'function',\n                summary: 'Return the exponent of x, e.g., e^x',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'sqrt',\n                token: 'sqrt',\n                type: 'function',\n                summary: 'Return the square root of x',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                ],\n              },\n              {\n                name: 'upper',\n                token: 'upper',\n                type: 'function',\n                summary: 'Return the uppercase conversion of s',\n                arguments: [\n                  {\n                    token: 's',\n                  },\n                ],\n              },\n              {\n                name: 'lower',\n                token: 'lower',\n                type: 'function',\n                summary: 'Return the lowercase conversion of s',\n                arguments: [\n                  {\n                    token: 's',\n                  },\n                ],\n              },\n              {\n                name: 'startswith',\n                token: 'startswith',\n                type: 'function',\n                summary: 'Return 1 if s2 is the prefix of s1, 0 otherwise.',\n                arguments: [\n                  {\n                    token: 's1',\n                  },\n                  {\n                    token: 's2',\n                  },\n                ],\n              },\n              {\n                name: 'contains',\n                token: 'contains',\n                type: 'function',\n                summary:\n                  'Return the number of occurrences of s2 in s1, 0 otherwise. If s2 is an empty string, return length(s1) + 1.',\n                arguments: [\n                  {\n                    token: 's1',\n                  },\n                  {\n                    token: 's2',\n                  },\n                ],\n              },\n              {\n                name: 'strlen',\n                token: 'strlen',\n                type: 'function',\n                summary: 'Return the length of s',\n                arguments: [\n                  {\n                    token: 's',\n                  },\n                ],\n              },\n              {\n                name: 'substr',\n                token: 'substr',\n                type: 'function',\n                summary:\n                  'Return the substring of s, starting at offset and having count characters. If offset is negative, it represents the distance from the end of the string. If count is -1, it means \"the rest of the string starting at offset\".',\n                arguments: [\n                  {\n                    token: 's',\n                  },\n                  {\n                    token: 'offset',\n                  },\n                  {\n                    token: 'count',\n                  },\n                ],\n              },\n              {\n                name: 'format',\n                token: 'format',\n                type: 'function',\n                summary:\n                  'Use the arguments following fmt to format a string. Currently the only format argument supported is %s and it applies to all types of arguments.',\n                arguments: [\n                  {\n                    token: 'fmt',\n                  },\n                ],\n              },\n              {\n                name: 'matched_terms',\n                token: 'matched_terms',\n                type: 'function',\n                summary:\n                  'Return the query terms that matched for each record (up to 100), as a list. If a limit is specified, Redis will return the first N matches found, based on query order.',\n                arguments: [\n                  {\n                    token: 'max_terms=100',\n                    optional: true,\n                  },\n                ],\n              },\n              {\n                name: 'split',\n                token: 'split',\n                type: 'function',\n                summary:\n                  'Split a string by any character in the string sep, and strip any characters in strip. If only s is specified, it is split by commas and spaces are stripped. The output is an array.',\n                arguments: [\n                  {\n                    token: 's',\n                  },\n                ],\n              },\n              {\n                name: 'timefmt',\n                token: 'timefmt',\n                type: 'function',\n                summary:\n                  'Return a formatted time string based on a numeric timestamp value x.',\n                arguments: [\n                  {\n                    token: 'x',\n                  },\n                  {\n                    token: 'fmt',\n                    optional: true,\n                  },\n                ],\n              },\n              {\n                name: 'parsetime',\n                token: 'parsetime',\n                type: 'function',\n                summary:\n                  'The opposite of timefmt() - parse a time format using a given format string',\n                arguments: [\n                  {\n                    token: 'timesharing',\n                  },\n                  {\n                    token: 'fmt',\n                    optional: true,\n                  },\n                ],\n              },\n              {\n                name: 'day',\n                token: 'day',\n                type: 'function',\n                summary:\n                  'Round a Unix timestamp to midnight (00:00) start of the current day.',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'hour',\n                token: 'hour',\n                type: 'function',\n                summary:\n                  'Round a Unix timestamp to the beginning of the current hour.',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'minute',\n                token: 'minute',\n                type: 'function',\n                summary:\n                  'Round a Unix timestamp to the beginning of the current minute.',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'month',\n                token: 'month',\n                type: 'function',\n                summary:\n                  'Round a unix timestamp to the beginning of the current month.',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'dayofweek',\n                token: 'dayofweek',\n                type: 'function',\n                summary:\n                  'Convert a Unix timestamp to the day number (Sunday = 0).',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'dayofmonth',\n                token: 'dayofmonth',\n                type: 'function',\n                summary:\n                  'Convert a Unix timestamp to the day of month number (1 .. 31).',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'dayofyear',\n                token: 'dayofyear',\n                type: 'function',\n                summary:\n                  'Convert a Unix timestamp to the day of year number (0 .. 365).',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'year',\n                token: 'year',\n                type: 'function',\n                summary:\n                  'Convert a Unix timestamp to the current year (e.g. 2018).',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'monthofyear',\n                token: 'monthofyear',\n                type: 'function',\n                summary:\n                  'Convert a Unix timestamp to the current month (0 .. 11).',\n                arguments: [\n                  {\n                    token: 'timestamp',\n                  },\n                ],\n              },\n              {\n                name: 'geodistance',\n                token: 'geodistance',\n                type: 'function',\n                summary: 'Return distance in meters.',\n                arguments: [\n                  {\n                    token: '',\n                  },\n                ],\n              },\n            ],\n          },\n          {\n            name: 'name',\n            type: 'string',\n            token: 'AS',\n          },\n        ],\n      },\n      {\n        name: 'filter',\n        type: 'string',\n        optional: true,\n        expression: true,\n        token: 'FILTER',\n      },\n    ],\n    since: '8.4.0',\n    group: 'search',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/oauth.ts",
    "content": "export const OAUTH_CLOUD_CAPI_KEYS_DATA = [\n  {\n    id: '1',\n    name: 'RedisInsight-f4868252-a128-4a02-af75-bd3c99898267-2020-11-01T-123',\n    createdAt: '2023-08-02T09:07:41.680Z',\n    lastUsed: '2023-08-02T09:07:41.680Z',\n    valid: true,\n  },\n]\n\nexport const MOCK_OAUTH_USER_PROFILE = {\n  id: 1,\n  name: 'Bill Russell',\n  accounts: [\n    { id: 1, name: 'Bill R' },\n    { id: 2, name: 'Bill R 2' },\n  ],\n  currentAccountId: 1,\n}\n\nexport const MOCK_OAUTH_SSO_EMAIL = 'sso@mail.com'\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/data/rdi.ts",
    "content": "import { IPipelineStatus, PipelineStatus } from 'uiSrc/slices/interfaces'\n\nexport const MOCK_RDI_PIPELINE_CONFIG = `connections:\n  target:\n    type: redis\n`\n\nexport const MOCK_RDI_PIPELINE_JOB2 = {\n  name: 'job2',\n  value: `job2:\n  transform:\n    type: redis\n`,\n}\n\nexport const MOCK_RDI_PIPELINE_JOB1 = {\n  name: 'jobName',\n  value: `job:\n  transform:\n    type: sql\n`,\n}\n\nexport const MOCK_RDI_PIPELINE_DATA = {\n  config: MOCK_RDI_PIPELINE_CONFIG,\n  jobs: [MOCK_RDI_PIPELINE_JOB1, MOCK_RDI_PIPELINE_JOB2],\n}\n\nexport const MOCK_RDI_PIPELINE_JSON_DATA = {\n  config: {\n    connections: {\n      target: {\n        type: 'redis',\n      },\n    },\n  },\n  jobs: {\n    jobName: {\n      job: {\n        transform: {\n          type: 'sql',\n        },\n      },\n    },\n    job2: {\n      job2: {\n        transform: {\n          type: 'redis',\n        },\n      },\n    },\n  },\n}\n\nexport const MOCK_RDI_PIPELINE_STATUS_DATA: IPipelineStatus = {\n  components: { processor: 'ready' },\n  pipelines: {\n    defaults: {\n      status: PipelineStatus.Starting,\n      state: 'some',\n      tasks: 'none',\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/browser/bulkActions/bulkActionOverview.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { RedisDataType } from 'uiSrc/constants'\nimport { IBulkActionOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-overview.interface'\nimport { IBulkActionFilterOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-filter-overview.interface'\nimport { IBulkActionProgressOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-progress-overview.interface'\nimport { IBulkActionSummaryOverview } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-summary-overview.interface'\nimport {\n  BulkActionStatus,\n  BulkActionType,\n} from 'apiSrc/modules/bulk-actions/constants'\n\nexport const bulkActionOverviewFactory = Factory.define<IBulkActionOverview>(\n  ({ sequence }) => ({\n    id: `bulk-action-${sequence}`,\n    databaseId: faker.string.uuid(),\n    type: faker.helpers.enumValue(BulkActionType),\n    summary: bulkActionSummaryOverviewFactory.build(),\n    progress: bulkActionProgressOverviewFactory.build(),\n    filter: bulkActionFilterOverviewFactory.build(),\n    status: faker.helpers.enumValue(BulkActionStatus),\n    duration: faker.number.int({ min: 10, max: 100 }),\n  }),\n)\n\nexport const bulkActionSummaryOverviewFactory =\n  Factory.define<IBulkActionSummaryOverview>(() => ({\n    processed: faker.number.int({ min: 200, max: 299 }),\n    succeed: faker.number.int({ min: 300, max: 399 }),\n    failed: faker.number.int({ min: 400, max: 499 }),\n    errors: [],\n    keys: [],\n  }))\n\nexport const bulkActionProgressOverviewFactory =\n  Factory.define<IBulkActionProgressOverview>(() => ({\n    total: faker.number.int({ min: 100, max: 1000 }),\n    scanned: faker.number.int({ min: 0, max: 1000 }),\n  }))\n\nexport const bulkActionFilterOverviewFactory =\n  Factory.define<IBulkActionFilterOverview>(() => ({\n    type: faker.helpers.enumValue(RedisDataType),\n    match: faker.string.uuid(),\n  }))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/cloud/AzureAccount.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { AzureAccount } from 'uiSrc/slices/oauth/azure'\n\nexport const AzureAccountFactory = Factory.define<AzureAccount>(() => ({\n  id: faker.string.uuid(),\n  username: faker.internet.email(),\n  name: faker.person.fullName(),\n}))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/cloud/RedisCloudAccount.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { RedisCloudAccount } from 'uiSrc/slices/interfaces'\n\nexport const RedisCloudAccountFactory = Factory.define<RedisCloudAccount>(\n  () => ({\n    accountId: faker.number.int({ min: 100000, max: 999999 }),\n    accountName: faker.person.fullName(),\n    ownerName:\n      faker.helpers.maybe(() => faker.company.name(), { probability: 0.7 }) ??\n      null,\n    ownerEmail: faker.internet.email(),\n  }),\n)\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/cloud/RedisCloudInstance.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n  InstanceRedisCloud,\n  InstanceRedisClusterStatus,\n  AddRedisDatabaseStatus,\n  RedisCloudSubscriptionType,\n  RedisDefaultModules,\n  AddRedisClusterDatabaseOptions,\n} from 'uiSrc/slices/interfaces'\n\nexport const RedisCloudInstanceFactory = Factory.define<InstanceRedisCloud>(\n  () => {\n    const host = `${faker.word.noun()}-${faker.number.int({ min: 1000, max: 99999 })}.cloud.redis.example.com`\n    const port = faker.number.int({ min: 10000, max: 65535 })\n    const uid = faker.number.int({ min: 1000, max: 999999 })\n    const databaseId = faker.number.int({ min: 1, max: 999999 })\n    const subscriptionId = faker.number.int({ min: 100000, max: 99999999 })\n    const subscriptionName = `${faker.word.noun()}-subscription-${faker.number.int({ min: 100, max: 999 })}`\n\n    // Pick a random unique subset of modules\n    const allModules = Object.values(RedisDefaultModules)\n    const randomModules = faker.helpers.arrayElements(\n      allModules,\n      faker.number.int({ min: 0, max: allModules.length }),\n    ) as RedisDefaultModules[]\n    const modules = Array.from(randomModules) as RedisDefaultModules[]\n\n    return {\n      accessKey: faker.string.alphanumeric(16),\n      secretKey: faker.string.alphanumeric(24),\n      credentials: null,\n      account: null,\n      host,\n      port,\n      uid,\n      name: `${faker.word.noun()}-${faker.number.int({ min: 1000, max: 99999 })}`,\n      id: faker.number.int({ min: 1, max: 999999 }),\n      dnsName: host,\n      address: `${host}:${port}`,\n      status: faker.helpers.arrayElement([\n        InstanceRedisClusterStatus.Active,\n        InstanceRedisClusterStatus.Pending,\n        InstanceRedisClusterStatus.ImportPending,\n      ]),\n      modules,\n      tls: faker.datatype.boolean(),\n      options: (() => {\n        const persistenceChoices = [\n          'aof-every-1-second',\n          'aof-every-write',\n          'snapshot-every-1-hour',\n          'snapshot-every-6-hours',\n          'snapshot-every-12-hours',\n          'none',\n        ] as const\n\n        const generators: Record<string, () => any> = {\n          [AddRedisClusterDatabaseOptions.ActiveActive]: () =>\n            faker.datatype.boolean(),\n          [AddRedisClusterDatabaseOptions.Backup]: () =>\n            faker.helpers.arrayElement([\n              'snapshot-every-1-hour',\n              'snapshot-every-6-hours',\n              'snapshot-every-12-hours',\n              true,\n              false,\n            ]),\n          [AddRedisClusterDatabaseOptions.Clustering]: () =>\n            faker.datatype.boolean(),\n          // Present in instanceMock examples; not in enum mapping used by UI renderer, but safe to include\n          enabledDataPersistence: () => faker.datatype.boolean(),\n          [AddRedisClusterDatabaseOptions.PersistencePolicy]: () =>\n            faker.helpers.arrayElement<string>([\n              ...persistenceChoices,\n            ] as unknown as string[]),\n          [AddRedisClusterDatabaseOptions.Flash]: () =>\n            faker.datatype.boolean(),\n          [AddRedisClusterDatabaseOptions.Replication]: () =>\n            faker.datatype.boolean(),\n          [AddRedisClusterDatabaseOptions.ReplicaDestination]: () =>\n            faker.datatype.boolean(),\n          [AddRedisClusterDatabaseOptions.ReplicaSource]: () =>\n            faker.datatype.boolean(),\n        }\n\n        const keys = Object.keys(generators)\n        const subsetSize = faker.number.int({ min: 0, max: keys.length })\n        const selected = faker.helpers.arrayElements(keys, subsetSize)\n\n        return selected.reduce((acc: any, key: string) => {\n          acc[key] = generators[key]()\n          return acc\n        }, {})\n      })(),\n      message: undefined,\n      publicEndpoint: `${host}:${port}`,\n      databaseId,\n      databaseIdAdded: undefined,\n      subscriptionId,\n      subscriptionType: faker.helpers.enumValue(RedisCloudSubscriptionType),\n      subscriptionName,\n      subscriptionIdAdded: undefined,\n      statusAdded: faker.helpers.enumValue(AddRedisDatabaseStatus),\n      messageAdded:\n        faker.helpers.maybe(() => faker.lorem.sentence()) ?? undefined,\n      databaseDetails: undefined,\n      free: faker.datatype.boolean(),\n    }\n  },\n)\n\n// Predictable variants (\"traits\") for stories/tests\nexport const RedisCloudInstanceFactorySuccess =\n  RedisCloudInstanceFactory.params({\n    statusAdded: AddRedisDatabaseStatus.Success,\n    messageAdded: 'Added successfully',\n  })\n\nexport const RedisCloudInstanceFactoryFail = RedisCloudInstanceFactory.params({\n  statusAdded: AddRedisDatabaseStatus.Fail,\n  messageAdded: 'Failed to add database',\n})\n\nexport const RedisCloudInstanceFactoryFixed = RedisCloudInstanceFactory.params({\n  subscriptionType: RedisCloudSubscriptionType.Fixed,\n})\n\nexport const RedisCloudInstanceFactoryFlexible =\n  RedisCloudInstanceFactory.params({\n    subscriptionType: RedisCloudSubscriptionType.Flexible,\n  })\n\nexport const RedisCloudInstanceFactoryActive = RedisCloudInstanceFactory.params(\n  { status: InstanceRedisClusterStatus.Active },\n)\n\nexport const RedisCloudInstanceFactoryPending =\n  RedisCloudInstanceFactory.params({\n    status: InstanceRedisClusterStatus.Pending,\n  })\n\nexport const RedisCloudInstanceFactoryWithoutModules =\n  RedisCloudInstanceFactory.params({ modules: [] })\n\nexport const RedisCloudInstanceFactoryWithModules = (\n  modules: RedisDefaultModules[],\n) => RedisCloudInstanceFactory.params({ modules })\n\nexport const RedisCloudInstanceFactoryFree = RedisCloudInstanceFactory.params({\n  free: true,\n})\n\nexport const RedisCloudInstanceFactoryPaid = RedisCloudInstanceFactory.params({\n  free: false,\n})\n\n// Option-focused traits\nexport const RedisCloudInstanceFactoryOptionsNone =\n  RedisCloudInstanceFactory.params({\n    options: {\n      [AddRedisClusterDatabaseOptions.ActiveActive]: false,\n      [AddRedisClusterDatabaseOptions.Backup]: false,\n      [AddRedisClusterDatabaseOptions.Clustering]: false,\n      enabledDataPersistence: false,\n      [AddRedisClusterDatabaseOptions.PersistencePolicy]: 'none',\n      [AddRedisClusterDatabaseOptions.Flash]: false,\n      [AddRedisClusterDatabaseOptions.Replication]: false,\n      [AddRedisClusterDatabaseOptions.ReplicaDestination]: false,\n      [AddRedisClusterDatabaseOptions.ReplicaSource]: false,\n    },\n  })\n\nexport const RedisCloudInstanceFactoryOptionsFull =\n  RedisCloudInstanceFactory.params({\n    options: {\n      [AddRedisClusterDatabaseOptions.ActiveActive]: true,\n      // Use a concrete backup schedule value similar to instanceMock examples\n      [AddRedisClusterDatabaseOptions.Backup]: 'snapshot-every-12-hours',\n      [AddRedisClusterDatabaseOptions.Clustering]: true,\n      enabledDataPersistence: true,\n      [AddRedisClusterDatabaseOptions.PersistencePolicy]: 'aof-every-1-second',\n      [AddRedisClusterDatabaseOptions.Flash]: true,\n      [AddRedisClusterDatabaseOptions.Replication]: true,\n      [AddRedisClusterDatabaseOptions.ReplicaDestination]: false,\n      [AddRedisClusterDatabaseOptions.ReplicaSource]: false,\n    },\n  })\n\nexport const RedisCloudInstanceFactoryOptionsBackupSchedule = (\n  schedule:\n    | 'snapshot-every-1-hour'\n    | 'snapshot-every-6-hours'\n    | 'snapshot-every-12-hours',\n) =>\n  RedisCloudInstanceFactory.params({\n    options: {\n      [AddRedisClusterDatabaseOptions.Backup]: schedule,\n    },\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/cloud/RedisCloudSubscription.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n  RedisCloudSubscription,\n  RedisCloudSubscriptionStatus,\n  RedisCloudSubscriptionType,\n} from 'uiSrc/slices/interfaces'\n\nconst PROVIDERS = ['aws', 'google', 'azure'] as const\nconst REGIONS = [\n  'us-east-1',\n  'us-west-2',\n  'eu-west-1',\n  'eu-central-1',\n  'ap-southeast-1',\n] as const\n\nexport const RedisCloudSubscriptionFactory =\n  Factory.define<RedisCloudSubscription>(() => {\n    const region = faker.helpers.arrayElement([...REGIONS])\n    const provider = faker.helpers.arrayElement([...PROVIDERS])\n\n    return {\n      id: faker.number.int({ min: 100000, max: 99999999 }),\n      name: `${faker.word.noun()}-${faker.number.int({ min: 1000, max: 99999 })}.${region}.cloud`,\n      type: faker.helpers.enumValue(RedisCloudSubscriptionType),\n      numberOfDatabases: faker.number.int({ min: 0, max: 20 }),\n      provider,\n      region,\n      status: faker.helpers.enumValue(RedisCloudSubscriptionStatus),\n      free: faker.datatype.boolean(),\n    }\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/cluster/ClusterNodeDetails.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { ClusterNodeDetails } from 'src/modules/cluster-monitor/models'\n\nenum NodeRole {\n  Primary = 'primary',\n  Replica = 'replica',\n}\n\nenum HealthStatus {\n  Online = 'online',\n  Offline = 'offline',\n  Loading = 'loading',\n}\n\nexport const ClusterNodeDetailsFactory = Factory.define<ClusterNodeDetails>(\n  () => ({\n    id: faker.string.uuid(),\n    version: faker.system.semver(),\n    mode: faker.helpers.arrayElement(['standalone', 'cluster', 'sentinel']),\n    host: faker.internet.ip(),\n    port: faker.internet.port(),\n    role: faker.helpers.arrayElement([NodeRole.Primary, NodeRole.Replica]),\n    health: faker.helpers.arrayElement([\n      HealthStatus.Online,\n      HealthStatus.Offline,\n      HealthStatus.Loading,\n    ]),\n    slots: ['0-5460'],\n    totalKeys: faker.number.int({ min: 0, max: 1000000 }),\n    usedMemory: faker.number.int({ min: 1000000, max: 100000000 }),\n    opsPerSecond: faker.number.int({ min: 0, max: 10000 }),\n    connectionsReceived: faker.number.int({ min: 0, max: 10000 }),\n    connectedClients: faker.number.int({ min: 0, max: 100 }),\n    commandsProcessed: faker.number.int({ min: 0, max: 1000000000 }),\n    networkInKbps: faker.number.float({\n      min: 0,\n      max: 10000,\n      fractionDigits: 2,\n    }),\n    networkOutKbps: faker.number.float({\n      min: 0,\n      max: 10000,\n      fractionDigits: 2,\n    }),\n    cacheHitRatio: faker.number.float({ min: 0, max: 1, fractionDigits: 2 }),\n    replicationOffset: faker.number.int({ min: 0, max: 1000000 }),\n    uptimeSec: faker.number.int({ min: 0, max: 10000000 }),\n    replicas: [],\n  }),\n)\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/cluster/RedisClusterInstance.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n  AddRedisDatabaseStatus,\n  InstanceRedisCluster,\n  InstanceRedisClusterStatus,\n  RedisDefaultModules,\n} from 'uiSrc/slices/interfaces'\n\nexport const RedisClusterInstanceFactory = Factory.define<InstanceRedisCluster>(\n  () => {\n    const host = faker.internet.ip()\n    const port = faker.number.int({ min: 6379, max: 65535 })\n    const uid = faker.number.int({ min: 1, max: 999999 })\n    const dnsName = `redis-${faker.number.int({ min: 1000, max: 99999 })}.cluster.local`\n\n    // Pick a random unique subset of modules\n    const allModules = Object.values(RedisDefaultModules)\n    const randomModules = faker.helpers.arrayElements(\n      allModules,\n      faker.number.int({ min: 0, max: Math.min(3, allModules.length) }),\n    ) as RedisDefaultModules[]\n    const modules = Array.from(new Set(randomModules)) as RedisDefaultModules[]\n\n    return {\n      host,\n      port,\n      uid,\n      name: `redis-db-${faker.number.int({ min: 1, max: 999 })}`,\n      id: faker.number.int({ min: 1, max: 999999 }),\n      dnsName,\n      address: `${host}:${port}`,\n      status: faker.helpers.arrayElement([\n        InstanceRedisClusterStatus.Active,\n        InstanceRedisClusterStatus.Pending,\n        InstanceRedisClusterStatus.CreationFailed,\n      ]),\n      modules,\n      tls: faker.datatype.boolean(),\n      options: {},\n      message: faker.datatype.boolean() ? faker.lorem.sentence() : undefined,\n      uidAdded: undefined,\n      statusAdded: undefined,\n      messageAdded: undefined,\n      databaseDetails: undefined,\n    }\n  },\n)\n\nexport const RedisClusterInstanceAddedFactory =\n  RedisClusterInstanceFactory.afterBuild((instance) => {\n    const statusAdded = faker.helpers.arrayElement([\n      AddRedisDatabaseStatus.Success,\n      AddRedisDatabaseStatus.Fail,\n    ])\n\n    return {\n      ...instance,\n      uidAdded: faker.number.int({ min: 1, max: 999999 }),\n      statusAdded,\n      messageAdded:\n        statusAdded === AddRedisDatabaseStatus.Success\n          ? 'Successfully added'\n          : faker.lorem.sentence(),\n    }\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/database/DBInstance.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { ConnectionType, Instance } from 'uiSrc/slices/interfaces'\n\nexport const DBInstanceFactory = Factory.define<Instance>(() => ({\n  id: faker.string.uuid(),\n  name: faker.company.name(),\n  host: faker.internet.ip(),\n  port: faker.internet.port(),\n  username: faker.internet.userName(),\n  password: faker.internet.password(),\n  visible: faker.datatype.boolean(),\n  connectionType: faker.helpers.arrayElement([\n    ConnectionType.Standalone,\n    ConnectionType.Cluster,\n    ConnectionType.Sentinel,\n  ]),\n  nameFromProvider: faker.company.name(),\n  modules: [],\n  version: faker.system.semver(),\n  lastConnection: faker.date.past(),\n  provider: faker.company.name(),\n}))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/database/DbConnectionInfo.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { KeyValueFormat } from 'uiSrc/constants/keys'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\n\nexport const dbConnectionInfoFactory = Factory.define<DbConnectionInfo>(() => ({\n  id: faker.string.uuid(),\n  name: faker.company.name(),\n  host: faker.internet.ip(),\n  port: faker.internet.port().toString(),\n  username: faker.internet.userName(),\n  password: faker.internet.password(),\n  timeout: faker.number.int({ min: 10, max: 120 }).toString(),\n  selectedCaCertName: 'none',\n  keyNameFormat: KeyValueFormat,\n  modules: [],\n  version: faker.system.semver(),\n}))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/database-analysis/DatabaseAnalysis.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\n\nexport const DatabaseAnalysisFactory = Factory.define<DatabaseAnalysis>(() => ({\n  id: faker.string.uuid(),\n  databaseId: faker.string.uuid(),\n  filter: { match: '*', count: 10000 } as any,\n  delimiter: ':',\n  progress: { total: 100000, scanned: 50000, processed: 10000 } as any,\n  createdAt: faker.date.recent(),\n  totalKeys: { total: 10000, types: [] } as any,\n  totalMemory: { total: 1000000, types: [] } as any,\n  topKeysNsp: [],\n  topMemoryNsp: [],\n  topKeysLength: [],\n  topKeysMemory: [],\n  expirationGroups: [],\n  recommendations: [],\n}))\n\nexport const buildDatabaseAnalysisWithTopKeys = () => {\n  const data = DatabaseAnalysisFactory.build({\n    totalKeys: {\n      total: 7_500,\n      types: [\n        { type: 'string', total: 3_000 },\n        { type: 'hash', total: 2_500 },\n        { type: 'zset', total: 2_000 },\n      ],\n    } as any,\n    totalMemory: {\n      total: 450_000,\n      types: [\n        { type: 'string', total: 50_000 },\n        { type: 'hash', total: 250_000 },\n        { type: 'zset', total: 150_000 },\n      ],\n    } as any,\n    topKeysLength: [\n      {\n        name: 'user:sessions',\n        type: 'hash',\n        memory: 120_000,\n        length: 5_000,\n        ttl: -1,\n      },\n      {\n        name: 'orders:recent',\n        type: 'list',\n        memory: 80_000,\n        length: 2_000,\n        ttl: 3_600,\n      },\n    ] as any,\n    topKeysMemory: [\n      {\n        name: 'user:sessions',\n        type: 'hash',\n        memory: 120_000,\n        length: 5_000,\n        ttl: -1,\n      },\n      {\n        name: 'metrics:pageviews',\n        type: 'zset',\n        memory: 200_000,\n        length: 1_000,\n        ttl: -1,\n      },\n    ] as any,\n    expirationGroups: [\n      { label: 'No expiry', total: 8_000, threshold: 0 },\n      { label: '<1 hr', total: 1_500, threshold: 3_600 },\n      { label: '1–24 hrs', total: 500, threshold: 86_400 },\n    ] as any,\n  })\n\n  const reports = [\n    {\n      id: data.id,\n      createdAt: data.createdAt,\n      db: data.db,\n    },\n  ]\n\n  return { data, reports }\n}\n\nexport const buildDatabaseAnalysisWithNamespaces = () =>\n  DatabaseAnalysisFactory.build({\n    topMemoryNsp: [\n      {\n        nsp: 'users',\n        memory: 500000,\n        keys: 1200,\n        types: [\n          {\n            type: 'hash',\n            memory: 400000,\n            keys: 800,\n          },\n          {\n            type: 'string',\n            memory: 100000,\n            keys: 400,\n          },\n        ],\n      },\n      {\n        nsp: 'orders',\n        memory: 300000,\n        keys: 600,\n        types: [\n          {\n            type: 'zset',\n            memory: 200000,\n            keys: 300,\n          },\n          {\n            type: 'list',\n            memory: 100000,\n            keys: 300,\n          },\n        ],\n      },\n    ] as any,\n    topKeysNsp: [\n      {\n        nsp: 'users',\n        memory: 500000,\n        keys: 1200,\n        types: [\n          {\n            type: 'hash',\n            memory: 400000,\n            keys: 800,\n          },\n          {\n            type: 'string',\n            memory: 100000,\n            keys: 400,\n          },\n        ],\n      },\n      {\n        nsp: 'orders',\n        memory: 300000,\n        keys: 600,\n        types: [\n          {\n            type: 'zset',\n            memory: 200000,\n            keys: 300,\n          },\n          {\n            type: 'list',\n            memory: 100000,\n            keys: 300,\n          },\n        ],\n      },\n    ] as any,\n    delimiter: ':',\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/pubsub/PubSubMessage.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { PubSubMessage } from 'uiSrc/slices/interfaces/pubsub'\n\nconst CHANNELS = ['news:tech', 'news:alerts', 'metrics:events', 'system:logs']\n\nexport const PubSubMessageFactory = Factory.define<PubSubMessage>(() => ({\n  channel: faker.helpers.arrayElement(CHANNELS),\n  message: faker.lorem.sentences(faker.number.int({ min: 1, max: 6 })),\n  time: faker.date.recent().getTime() / 1000,\n}))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/query-library/queryLibraryItem.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n  QueryLibraryItem,\n  QueryLibraryType,\n  CreateQueryLibraryItem,\n  SeedQueryLibraryItem,\n} from 'uiSrc/services/query-library/types'\n\nexport const queryLibraryItemFactory = Factory.define<QueryLibraryItem>(() => ({\n  id: faker.string.uuid(),\n  databaseId: faker.string.uuid(),\n  indexName: `idx:${faker.word.noun()}`,\n  type: faker.helpers.enumValue(QueryLibraryType),\n  name: faker.lorem.words(3),\n  description: faker.lorem.sentence(),\n  query: `FT.SEARCH ${faker.word.noun()} \"*\"`,\n  createdAt: faker.date.past().toISOString(),\n  updatedAt: faker.date.recent().toISOString(),\n}))\n\nexport const createQueryLibraryItemFactory =\n  Factory.define<CreateQueryLibraryItem>(() => {\n    const { indexName, name, query } = queryLibraryItemFactory.build()\n    return { indexName, name, query }\n  })\n\nexport const seedQueryLibraryItemFactory = Factory.define<SeedQueryLibraryItem>(\n  () => {\n    const { indexName, name, description, query } =\n      queryLibraryItemFactory.build()\n    return { indexName, name, description, query }\n  },\n)\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/rdi/RdiStatistics.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n  IStatisticsInfoSection,\n  IStatisticsBlocksSection,\n  IStatisticsTableSection,\n  RdiStatisticsViewType,\n  StatisticsCellType,\n} from 'uiSrc/slices/interfaces'\n\nexport const StatisticsInfoItemFactory = Factory.define<{\n  label: string\n  value: string\n}>(() => ({\n  label: faker.lorem.words(2),\n  value: faker.lorem.word(),\n}))\n\nexport const StatisticsInfoSectionFactory =\n  Factory.define<IStatisticsInfoSection>(() => ({\n    name: faker.lorem.words(2),\n    view: RdiStatisticsViewType.Info,\n    data: StatisticsInfoItemFactory.buildList(\n      faker.number.int({ min: 1, max: 5 }),\n    ),\n  }))\n\nexport const StatisticsBlockItemFactory = Factory.define<{\n  label: string\n  value: number\n  units: string\n}>(() => ({\n  label: faker.lorem.words(2),\n  value: faker.number.float({ min: 0, max: 1000, fractionDigits: 2 }),\n  units: faker.helpers.arrayElement(['Total', 'MB', 'ms', 'sec', 'records']),\n}))\n\nexport const StatisticsBlocksSectionFactory =\n  Factory.define<IStatisticsBlocksSection>(() => ({\n    name: faker.lorem.words(2),\n    view: RdiStatisticsViewType.Blocks,\n    data: StatisticsBlockItemFactory.buildList(\n      faker.number.int({ min: 1, max: 7 }),\n    ),\n  }))\n\nexport const StatisticsTableSectionFactory =\n  Factory.define<IStatisticsTableSection>(() => {\n    const columns = [\n      { id: 'status', header: 'Status', type: StatisticsCellType.Status },\n      { id: 'name', header: 'Name' },\n      { id: 'host', header: 'Host' },\n    ]\n\n    return {\n      name: faker.lorem.words(2),\n      view: RdiStatisticsViewType.Table,\n      columns,\n      data: Array.from(\n        { length: faker.number.int({ min: 1, max: 5 }) },\n        () => ({\n          status: faker.helpers.arrayElement([\n            'connected',\n            'not yet used',\n            'disconnected',\n          ]),\n          name: faker.lorem.word(),\n          host: `${faker.internet.ip()}:${faker.internet.port()}`,\n        }),\n      ),\n    }\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/redisearch/IndexField.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  IndexField,\n  IndexFieldValue,\n} from 'uiSrc/pages/vector-search/components/index-details'\n\nexport const generateValueForType = (type: FieldTypes): IndexFieldValue => {\n  switch (type) {\n    case FieldTypes.TEXT:\n      return faker.lorem.words({ min: 1, max: 5 })\n    case FieldTypes.TAG:\n      return faker.lorem.word()\n    case FieldTypes.NUMERIC:\n      return faker.number.float({ min: 0, max: 1000, fractionDigits: 2 })\n    case FieldTypes.VECTOR:\n      return `[${Array.from({ length: 4 }, () =>\n        faker.number.float({ min: -1, max: 1, fractionDigits: 4 }),\n      ).join(', ')}]`\n    case FieldTypes.GEO:\n      return `${faker.location.latitude()}, ${faker.location.longitude()}`\n    case FieldTypes.GEOSHAPE:\n      return `POINT(${faker.location.longitude()} ${faker.location.latitude()})`\n    default:\n      return faker.lorem.words({ min: 1, max: 5 })\n  }\n}\n\nexport const indexFieldFactory = Factory.define<IndexField>(({ params }) => {\n  const type = params.type ?? faker.helpers.enumValue(FieldTypes)\n\n  return {\n    id: faker.string.uuid(),\n    name: faker.database.column(),\n    value: generateValueForType(type),\n    type,\n  }\n})\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/redisearch/IndexInfo.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  FieldStatisticsDto,\n  IndexAttibuteDto,\n  IndexInfoDto,\n} from 'apiSrc/modules/browser/redisearch/dto'\n\nexport const INDEX_INFO_SEPARATORS: string[] = [',', ';', '|', ':']\n\n// Note: Current data is replica of the sample data, but we can make it more realistic/diverse in the future\nexport const indexInfoFactory = Factory.define<IndexInfoDto>(() => ({\n  index_name: `idx:${faker.word.noun()}`,\n  index_options: {},\n  index_definition: {\n    key_type: 'JSON',\n    prefixes: [`$${faker.word.noun()}:`],\n    default_score: '1',\n    indexes_all: 'false',\n  },\n  attributes: indexInfoAttributeFactory.buildList(3),\n  num_docs: faker.number.int({ min: 0, max: 100 }).toString(), // Note: DTO and actual response have different types, it should be a number\n  max_doc_id: faker.number.int({ min: 101, max: 200 }).toString(), // Note: DTO and actual response have different types, it should be a number\n  num_terms: faker.number.int({ min: 201, max: 300 }).toString(), // Note: DTO and actual response have different types, it should be a number\n  num_records: faker.number.int({ min: 301, max: 400 }).toString(), // Note: DTO and actual response have different types, it should be a number\n  inverted_sz_mb: '0.06543350219726563',\n  vector_index_sz_mb: '0',\n  total_inverted_index_blocks: faker.number\n    .int({ min: 401, max: 500 })\n    .toString(), // Note: DTO and actual response have different types, it should be a number\n  offset_vectors_sz_mb: '0.0022459030151367188',\n  doc_table_size_mb: '0.023920059204101563',\n  sortable_values_size_mb: '0',\n  key_table_size_mb: '0.0032911300659179688',\n  tag_overhead_sz_mb: '6.361007690429688e-4',\n  text_overhead_sz_mb: '0.017991065979003906',\n  total_index_memory_sz_mb: '0.11714744567871094',\n  geoshapes_sz_mb: '0',\n  records_per_doc_avg: '24.952829360961914',\n  bytes_per_record_avg: '25.940263748168945',\n  offsets_per_term_avg: '0.8903591632843018',\n  offset_bits_per_record_avg: '8',\n  hash_indexing_failures: '0', // Note: DTO and actual response have different types, it should be a number\n  total_indexing_time: '1.7289999723434448',\n  indexing: '0', // Note: DTO and actual response have different types, it should be a number\n  percent_indexed: '1',\n  number_of_uses: 39,\n  cleaning: 0,\n  gc_stats: {\n    bytes_collected: '0',\n    total_ms_run: '0',\n    total_cycles: '0',\n    average_cycle_time_ms: 'nan',\n    last_run_time_ms: '0',\n    gc_numeric_trees_missed: '0',\n    gc_blocks_denied: '0',\n  },\n  cursor_stats: {\n    global_idle: 0,\n    global_total: 0,\n    index_capacity: 128,\n    index_total: 0,\n  },\n  dialect_stats: {\n    dialect_1: 0,\n    dialect_2: 0,\n    dialect_3: 0,\n    dialect_4: 0,\n  },\n  'Index Errors': {\n    'indexing failures': 0,\n    'last indexing error': 'N/A',\n    'last indexing error key': 'N/A',\n    'background indexing status': 'OK',\n  },\n  'field statistics': indexInfoFieldStatisticsFactory.buildList(3),\n}))\n\ntype IndexInfoAttributeFactoryTransientParams = {\n  includeWeight?: boolean\n  includeSeparator?: boolean\n  includeNoIndex?: boolean\n}\n\nexport const indexInfoAttributeFactory = Factory.define<\n  IndexAttibuteDto,\n  IndexInfoAttributeFactoryTransientParams\n>(({ transientParams }) => {\n  const name = faker.word.noun()\n\n  const {\n    includeWeight = faker.datatype.boolean(),\n    includeSeparator = faker.datatype.boolean(),\n    includeNoIndex = faker.datatype.boolean(),\n  } = transientParams\n\n  return {\n    identifier: `$.${name}`,\n    attribute: name,\n    type: faker.helpers.enumValue(FieldTypes).toString().toUpperCase(),\n\n    // Optional fields\n    ...(includeWeight && {\n      WEIGHT: faker.number\n        .float({ min: 0.1, max: 10, fractionDigits: 1 })\n        .toString(),\n    }),\n    ...(includeSeparator && {\n      SEPARATOR: faker.helpers.arrayElement(INDEX_INFO_SEPARATORS),\n    }),\n    ...(includeNoIndex && {\n      NOINDEX: faker.datatype.boolean(),\n    }),\n  }\n})\n\nexport const indexInfoFieldStatisticsFactory =\n  Factory.define<FieldStatisticsDto>(() => {\n    const name = faker.word.noun()\n\n    return {\n      identifier: `$.${name}`,\n      attribute: name,\n      'Index Errors': {\n        'indexing failures': 0,\n        'last indexing error': 'N/A',\n        'last indexing error key': 'N/A',\n      },\n    }\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/sentinel/SentinelMaster.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n  AddRedisDatabaseStatus,\n  ModifiedSentinelMaster,\n} from 'uiSrc/slices/interfaces'\n\nexport const SentinelMasterFactory = Factory.define<ModifiedSentinelMaster>(\n  () => {\n    const name = `mymaster${faker.number.int({ min: 1, max: 999 })}`\n    const host = faker.internet.ip()\n    const port = faker.number.int({ min: 6379, max: 65535 }).toString()\n\n    return {\n      id: faker.string.uuid(),\n      name,\n      alias: name,\n      host,\n      port,\n      username: faker.datatype.boolean() ? faker.internet.userName() : '',\n      password: faker.datatype.boolean() ? faker.internet.password() : '',\n      db: faker.number.int({ min: 0, max: 15 }),\n      numberOfSlaves: faker.number.int({ min: 0, max: 5 }),\n      status: faker.helpers.arrayElement([\n        AddRedisDatabaseStatus.Success,\n        AddRedisDatabaseStatus.Fail,\n      ]),\n      message: faker.datatype.boolean() ? faker.lorem.sentence() : '',\n      loading: false,\n    }\n  },\n)\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/vector-search/indexInfo.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { KeyTypes } from 'uiSrc/constants'\nimport {\n  IndexInfo,\n  IndexAttribute,\n  IndexDefinition,\n  IndexOptions,\n} from 'uiSrc/pages/vector-search/hooks/useIndexInfo'\n\n/**\n * Factory for frontend IndexAttribute type.\n * Field types are already normalized (lowercase).\n */\nexport const indexAttributeFactory = Factory.define<IndexAttribute>(\n  ({ transientParams }) => {\n    const name = faker.word.noun()\n    const { includeWeight = faker.datatype.boolean() } = transientParams as {\n      includeWeight?: boolean\n    }\n\n    return {\n      identifier: `$.${name}`,\n      attribute: name,\n      type: faker.helpers.enumValue(FieldTypes),\n      ...(includeWeight && {\n        weight: faker.number\n          .float({ min: 0.1, max: 10, fractionDigits: 1 })\n          .toString(),\n      }),\n    }\n  },\n)\n\n/**\n * Factory for frontend IndexDefinition type.\n */\nexport const indexDefinitionFactory = Factory.define<IndexDefinition>(() => ({\n  keyType: faker.helpers.arrayElement([KeyTypes.Hash, KeyTypes.JSON]),\n  prefixes: [`${faker.word.noun()}:`],\n}))\n\n/**\n * Factory for frontend IndexOptions type.\n */\nexport const indexOptionsFactory = Factory.define<IndexOptions>(() => ({\n  filter: faker.datatype.boolean()\n    ? `@status == \"${faker.word.noun()}\"`\n    : undefined,\n  defaultLang: faker.datatype.boolean()\n    ? faker.helpers.arrayElement(['english', 'german', 'french', 'spanish'])\n    : undefined,\n}))\n\n/**\n * Factory for frontend IndexInfo type (decoupled from backend DTO).\n * Used for component tests and Storybook.\n */\nexport const indexInfoFactory = Factory.define<IndexInfo>(() => ({\n  indexDefinition: indexDefinitionFactory.build(),\n  indexOptions: indexOptionsFactory.build(),\n  attributes: indexAttributeFactory.buildList(3),\n  numDocs: faker.number.int({ min: 0, max: 100 }),\n  maxDocId: faker.number.int({ min: 101, max: 200 }),\n  numRecords: faker.number.int({ min: 301, max: 400 }),\n  numTerms: faker.number.int({ min: 201, max: 300 }),\n}))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/vector-search/indexList.factory.ts",
    "content": "import { faker } from '@faker-js/faker'\nimport { Factory } from 'fishery'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { IndexListRow } from 'uiSrc/pages/vector-search/components/index-list/IndexList.types'\n\nconst FIELD_TYPES = Object.values(FieldTypes)\n\n/**\n * Factory for IndexListRow type.\n * Used for component tests and Storybook.\n */\nexport const indexListRowFactory = Factory.define<IndexListRow>(\n  ({ sequence }) => {\n    const numFields = faker.number.int({ min: 1, max: 8 })\n    const fieldTypes = faker.helpers.arrayElements(\n      Object.values(FieldTypes),\n      faker.number.int({\n        min: 1,\n        max: Math.min(numFields, FIELD_TYPES.length),\n      }),\n    )\n\n    return {\n      id: `idx-${sequence}`,\n      name: `${faker.word.noun()}-${faker.word.noun()}-idx`,\n      prefixes: faker.helpers.arrayElements(\n        [\n          `${faker.word.noun()}:`,\n          `${faker.word.noun()}:`,\n          `${faker.word.noun()}:`,\n        ],\n        faker.number.int({ min: 0, max: 3 }),\n      ),\n      fieldTypes,\n      numDocs: faker.number.int({ min: 0, max: 1000000 }),\n      numRecords: faker.number.int({ min: 1000001, max: 2000000 }),\n      numTerms: faker.number.int({ min: 2000001, max: 3000000 }),\n      numFields,\n    }\n  },\n)\n\n/**\n * Pre-defined example rows for consistent test assertions.\n */\nexport const exampleIndexListRows = {\n  products: indexListRowFactory.build({\n    id: 'idx-products',\n    name: 'products-idx',\n    prefixes: ['product:'],\n    fieldTypes: [FieldTypes.TEXT, FieldTypes.TAG, FieldTypes.NUMERIC],\n    numDocs: 10000,\n    numRecords: 50000,\n    numTerms: 5000,\n    numFields: 3,\n  }),\n  users: indexListRowFactory.build({\n    id: 'idx-users',\n    name: 'users-idx',\n    prefixes: ['user:', 'account:'],\n    fieldTypes: [FieldTypes.TEXT, FieldTypes.VECTOR],\n    numDocs: 20000,\n    numRecords: 80000,\n    numTerms: 10000,\n    numFields: 2,\n  }),\n  locations: indexListRowFactory.build({\n    id: 'idx-locations',\n    name: 'locations-idx',\n    prefixes: ['loc:'],\n    fieldTypes: [FieldTypes.GEO, FieldTypes.TEXT],\n    numDocs: 5000,\n    numRecords: 25000,\n    numTerms: 3000,\n    numFields: 2,\n  }),\n  allFieldTypes: indexListRowFactory.build({\n    id: 'idx-all',\n    name: 'all-types-idx',\n    prefixes: ['all:'],\n    fieldTypes: [\n      FieldTypes.TEXT,\n      FieldTypes.TAG,\n      FieldTypes.NUMERIC,\n      FieldTypes.GEO,\n      FieldTypes.VECTOR,\n    ],\n    numDocs: 500,\n    numRecords: 1000,\n    numTerms: 200,\n    numFields: 5,\n  }),\n  empty: indexListRowFactory.build({\n    id: 'idx-empty',\n    name: 'empty-idx',\n    prefixes: ['empty:'],\n    fieldTypes: [FieldTypes.TEXT],\n    numDocs: 0,\n    numRecords: 0,\n    numTerms: 0,\n    numFields: 1,\n  }),\n  noPrefix: indexListRowFactory.build({\n    id: 'idx-noprefix',\n    name: 'noprefix-idx',\n    prefixes: [],\n    fieldTypes: [FieldTypes.TEXT],\n    numDocs: 100,\n    numRecords: 200,\n    numTerms: 50,\n    numFields: 1,\n  }),\n}\n\n/**\n * Default mock data list for tests.\n */\nexport const mockIndexListData: IndexListRow[] = [\n  exampleIndexListRows.products,\n  exampleIndexListRows.users,\n  exampleIndexListRows.locations,\n]\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/factories/workbench/commandExectution.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport {\n  CommandExecution,\n  CommandExecutionResult,\n  CommandExecutionType,\n  CommandExecutionUI,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\n\nexport const commandExecutionFactory = Factory.define<CommandExecution>(\n  ({ sequence }) => ({\n    id: sequence.toString() ?? faker.string.uuid(),\n    databaseId: faker.string.uuid(),\n    db: faker.number.int({ min: 0, max: 15 }),\n    type: faker.helpers.enumValue(CommandExecutionType),\n    mode: faker.helpers.enumValue(RunQueryMode),\n    resultsMode: faker.helpers.enumValue(ResultsMode),\n    command: faker.lorem.paragraph(),\n    result: commandExecutionResultFactory.buildList(1),\n    executionTime: faker.number.int({ min: 1000, max: 5000 }),\n    createdAt: faker.date.past(),\n  }),\n)\n\nexport const commandExecutionResultFactory =\n  Factory.define<CommandExecutionResult>(() => {\n    const includeSizeLimitExceeded = faker.datatype.boolean()\n\n    return {\n      status: faker.helpers.enumValue(CommandExecutionStatus),\n      response: faker.lorem.paragraph(),\n\n      // Optional properties\n      ...(includeSizeLimitExceeded && {\n        sizeLimitExceeded: faker.datatype.boolean(),\n      }),\n    }\n  })\n\nexport const commandExecutionUIFactory = Factory.define<CommandExecutionUI>(\n  () => {\n    const commandExecution = commandExecutionFactory.build() as CommandExecution\n\n    const includeLoading = faker.datatype.boolean()\n    const includeIsOpen = faker.datatype.boolean()\n    const includeError = faker.datatype.boolean()\n    const includeEmptyCommand = faker.datatype.boolean()\n\n    return {\n      ...commandExecution,\n\n      // Optional properties\n      ...(includeLoading && {\n        loading: faker.datatype.boolean(),\n      }),\n      ...(includeIsOpen && {\n        isOpen: faker.datatype.boolean(),\n      }),\n      ...(includeError && {\n        error: faker.lorem.sentence(),\n      }),\n      ...(includeEmptyCommand && {\n        emptyCommand: faker.datatype.boolean(),\n      }),\n    }\n  },\n)\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/ai/assistantHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\nconst handlers: HttpHandler[] = [\n  http.get<{ id: string }>(\n    getMswURL(`/${ApiEndpoints.AI_ASSISTANT_CHATS}/:id`),\n    async ({ params }) => {\n      const { id } = params\n\n      return HttpResponse.json({ id, messages: [] }, { status: 200 })\n    },\n  ),\n  http.post<{ id: string }, { content: string }>(\n    getMswURL(`/${ApiEndpoints.AI_ASSISTANT_CHATS}/:id/messages`),\n    async ({ request }) => {\n      const { content } = await request.json()\n\n      return HttpResponse.json([content], { status: 200 })\n    },\n  ),\n  http.post<{ id: string }, { content: string }>(\n    getMswURL(`/${ApiEndpoints.AI_ASSISTANT_CHATS}`),\n    async ({ request }) => {\n      const { content } = await request.json()\n\n      return HttpResponse.json([content], { status: 200 })\n    },\n  ),\n  http.delete<{ id: string }>(\n    getMswURL(`/${ApiEndpoints.AI_ASSISTANT_CHATS}/:id`),\n    async () => {\n      return HttpResponse.text('', { status: 200 })\n    },\n  ),\n  http.options(getMswURL(`/${ApiEndpoints.AI_ASSISTANT_CHATS}*`), () => {\n    return new Response(null, {\n      status: 200,\n      headers: {\n        Allow: 'GET,HEAD,POST,DELETE',\n      },\n    })\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/ai/index.ts",
    "content": "import { HttpHandler } from 'msw'\nimport assistant from 'uiSrc/mocks/handlers/ai/assistantHandlers'\n\nconst handlers: HttpHandler[] = [...assistant]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/analytics/clusterDetailsHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl } from 'uiSrc/utils'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport {\n  ClusterDetails,\n  HealthStatus,\n  NodeRole,\n} from 'apiSrc/modules/cluster-monitor/models'\nimport { Database as DatabaseInstanceResponse } from 'apiSrc/modules/database/models/database'\n\nexport const INSTANCE_ID_MOCK = 'instanceId'\n\nconst handlers: HttpHandler[] = [\n  // useGetClusterDetailsQuery\n  http.get<any, DatabaseInstanceResponse[]>(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.CLUSTER_DETAILS)),\n    async () => {\n      return HttpResponse.json(CLUSTER_DETAILS_DATA_MOCK, { status: 200 })\n    },\n  ),\n]\n\nexport const CLUSTER_DETAILS_DATA_MOCK: ClusterDetails = {\n  state: 'ok',\n  slotsAssigned: 16384,\n  slotsOk: 16384,\n  slotsPFail: 0,\n  slotsFail: 0,\n  slotsUnassigned: 0,\n  statsMessagesSent: 0,\n  statsMessagesReceived: 0,\n  currentEpoch: 0,\n  myEpoch: 0,\n  size: 3,\n  knownNodes: 3,\n  uptimeSec: 1661931600,\n  nodes: [\n    {\n      id: '3',\n      host: '3.93.234.244',\n      port: 12511,\n      role: 'primary' as NodeRole,\n      slots: ['10923-16383'],\n      health: 'online' as HealthStatus,\n      totalKeys: 0,\n      usedMemory: 38448896,\n      opsPerSecond: 0,\n      connectionsReceived: 15,\n      connectedClients: 6,\n      commandsProcessed: 114,\n      networkInKbps: 0.35,\n      networkOutKbps: 3.62,\n      cacheHitRatio: 0,\n      replicationOffset: 0,\n      uptimeSec: 1661931600,\n      version: '6.2.6',\n      mode: 'standalone',\n      replicas: [],\n    },\n    {\n      id: '4',\n      host: '44.202.117.57',\n      port: 12511,\n      role: 'primary' as NodeRole,\n      slots: ['0-5460'],\n      health: 'online' as HealthStatus,\n      totalKeys: 0,\n      usedMemory: 38448896,\n      opsPerSecond: 0,\n      connectionsReceived: 15,\n      connectedClients: 6,\n      commandsProcessed: 114,\n      networkInKbps: 0.35,\n      networkOutKbps: 3.62,\n      cacheHitRatio: 0,\n      replicationOffset: 0,\n      uptimeSec: 1661931600,\n      version: '6.2.6',\n      mode: 'standalone',\n      replicas: [],\n    },\n    {\n      id: '5',\n      host: '44.210.115.34',\n      port: 12511,\n      role: 'primary' as NodeRole,\n      slots: ['5461-10922'],\n      health: 'online' as HealthStatus,\n      totalKeys: 0,\n      usedMemory: 38448896,\n      opsPerSecond: 0,\n      connectionsReceived: 15,\n      connectedClients: 6,\n      commandsProcessed: 114,\n      networkInKbps: 0.35,\n      networkOutKbps: 3.62,\n      cacheHitRatio: 0,\n      replicationOffset: 0,\n      uptimeSec: 1661931600,\n      version: '6.2.6',\n      mode: 'standalone',\n      replicas: [],\n    },\n  ],\n  version: '6.2.6',\n  mode: 'standalone',\n}\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/analytics/dbAnalysisHistoryHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl } from 'uiSrc/utils'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\nexport const INSTANCE_ID_MOCK = 'instanceId'\n\nconst handlers: HttpHandler[] = [\n  // fetchDBAnalysisReportsHistory\n  http.get(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.DATABASE_ANALYSIS)),\n    async () => {\n      return HttpResponse.json(DB_ANALYSIS_HISTORY_DATA_MOCK, { status: 200 })\n    },\n  ),\n  http.post(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.DATABASE_ANALYSIS)),\n    async () => {\n      return HttpResponse.json({}, { status: 200 })\n    },\n  ),\n]\n\nexport const DB_ANALYSIS_HISTORY_DATA_MOCK = [\n  { id: 'id_1', createdAt: '1', db: 0 },\n  { id: 'id_2', createdAt: '2', db: 0 },\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/analytics/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport clusterDetails from './clusterDetailsHandlers'\nimport dbAnalysisHistory from './dbAnalysisHistoryHandlers'\n\nconst handlers: HttpHandler[] = [...clusterDetails, ...dbAnalysisHistory]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/app/featureHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\nexport const FEATURES_DATA_MOCK = {\n  features: {\n    insightsRecommendations: {\n      name: 'insightsRecommendations',\n      flag: true,\n    },\n    envDependent: {\n      name: 'envDependent',\n      flag: true,\n    },\n    cloudSso: {\n      name: 'cloudSso',\n      flag: true,\n      strategy: 'deepLink',\n      data: {\n        filterFreePlan: [\n          {\n            field: 'name',\n            expression: '^(No HA?.)|(Cache?.)|(30MB$)',\n            options: 'i',\n          },\n        ],\n        selectPlan: {\n          components: {\n            triggersAndFunctions: [\n              {\n                provider: 'AWS',\n                regions: ['ap-southeast-1'],\n              },\n              {\n                provider: 'GCP',\n                regions: ['asia-northeast1'],\n              },\n            ],\n            redisStackPreview: [\n              {\n                provider: 'AWS',\n                regions: ['us-east-2', 'ap-southeast-1', 'sa-east-1'],\n              },\n              {\n                provider: 'GCP',\n                regions: ['asia-northeast1', 'europe-west1', 'us-central1'],\n              },\n            ],\n          },\n        },\n      },\n    },\n    cloudSsoRecommendedSettings: {\n      name: 'cloudSsoRecommendedSettings',\n      flag: false,\n    },\n    redisModuleFilter: {\n      name: 'redisModuleFilter',\n      flag: true,\n      data: {\n        hideByName: [\n          {\n            expression: '^RedisGraph.',\n            options: 'i',\n          },\n          {\n            expression: '^RedisStackCompat?.',\n            options: 'i',\n          },\n          {\n            expression: '^rediscompat?.',\n            options: 'i',\n          },\n        ],\n      },\n    },\n    redisClient: {\n      name: 'redisClient',\n      flag: true,\n      data: {\n        strategy: 'ioredis',\n      },\n    },\n    documentationChat: {\n      name: 'documentationChat',\n      flag: true,\n    },\n    databaseChat: {\n      name: 'databaseChat',\n      flag: true,\n    },\n  },\n}\n\nconst handlers: HttpHandler[] = [\n  // get features\n  http.get<any, (typeof FEATURES_DATA_MOCK)[]>(\n    getMswURL(ApiEndpoints.FEATURES),\n    async () => {\n      return HttpResponse.json(FEATURES_DATA_MOCK, { status: 200 })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/app/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport info from './infoHandlers'\nimport telemetry from './telemetryHandlers'\nimport featureHandlers from './featureHandlers'\n\nconst handlers: HttpHandler[] = [...info, ...telemetry, ...featureHandlers]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/app/infoHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { Database as DatabaseInstanceResponse } from 'apiSrc/modules/database/models/database'\n\nexport const APP_INFO_DATA_MOCK = {\n  id: 'id1',\n  createDateTime: '2000-01-01T00:00:00.000Z',\n  appVersion: '2.0.0',\n  osPlatform: 'win32',\n  buildType: 'ELECTRON',\n}\n\nconst handlers: HttpHandler[] = [\n  // fetchServerInfo\n  http.get<any, DatabaseInstanceResponse[]>(\n    getMswURL(ApiEndpoints.INFO),\n    async () => {\n      return HttpResponse.json(APP_INFO_DATA_MOCK, { status: 200 })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/app/telemetryHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\nconst handlers: HttpHandler[] = [\n  // sendEventTelemetry\n  http.post(getMswURL(ApiEndpoints.ANALYTICS_SEND_EVENT), async () => {\n    return HttpResponse.text('', { status: 200 })\n  }),\n  // sendPageViewTelemetry\n  http.post(getMswURL(ApiEndpoints.ANALYTICS_SEND_PAGE), async () => {\n    return HttpResponse.text('', { status: 200 })\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/browser/bulkActionsHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { IBulkActionOverview } from 'uiSrc/slices/interfaces'\nimport { bulkActionOverviewFactory } from 'uiSrc/mocks/factories/browser/bulkActions/bulkActionOverview.factory'\nimport { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'\n\nconst handlers: HttpHandler[] = [\n  http.post<any, IBulkActionOverview>(\n    getMswURL(\n      getUrl(\n        INSTANCE_ID_MOCK,\n        ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n      ),\n    ),\n    async () => {\n      return HttpResponse.json(bulkActionOverviewFactory.build(), {\n        status: 200,\n      })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/browser/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport redisearch from './redisearchHandlers'\nimport bulkActions from './bulkActionsHandlers'\nimport queryLibrary from './queryLibraryHandlers'\n\nconst handlers: HttpHandler[] = [...redisearch, ...bulkActions, ...queryLibrary]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/browser/queryLibraryHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { queryLibraryItemFactory } from 'uiSrc/mocks/factories/query-library/queryLibraryItem.factory'\nimport { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'\n\nexport const QUERY_LIBRARY_ITEMS_MOCK = queryLibraryItemFactory.buildList(2, {\n  databaseId: INSTANCE_ID_MOCK,\n})\n\nconst handlers: HttpHandler[] = [\n  http.get(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.QUERY_LIBRARY)),\n    async () => HttpResponse.json(QUERY_LIBRARY_ITEMS_MOCK, { status: 200 }),\n  ),\n  http.delete(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.QUERY_LIBRARY, ':id')),\n    async () => HttpResponse.text('', { status: 200 }),\n  ),\n  http.post(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.QUERY_LIBRARY_SEED)),\n    async () => HttpResponse.json([], { status: 200 }),\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/browser/redisearchHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl, stringToBuffer } from 'uiSrc/utils'\nimport { indexInfoFactory } from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory'\nimport {\n  IndexInfoDto,\n  ListRedisearchIndexesResponse,\n} from 'apiSrc/modules/browser/redisearch/dto'\nimport { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'\n\nexport const REDISEARCH_LIST_DATA_MOCK_UTF8 = ['idx: 1', 'idx:2']\nexport const REDISEARCH_LIST_DATA_MOCK = {\n  indexes: [...REDISEARCH_LIST_DATA_MOCK_UTF8].map((str) =>\n    stringToBuffer(str),\n  ),\n}\n\nconst handlers: HttpHandler[] = [\n  // fetchRedisearchListAction\n  http.get<any, ListRedisearchIndexesResponse>(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.REDISEARCH)),\n    async () => {\n      return HttpResponse.json(REDISEARCH_LIST_DATA_MOCK, { status: 200 })\n    },\n  ),\n  http.post<any, IndexInfoDto>(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.REDISEARCH_INFO)),\n    async () => {\n      return HttpResponse.json(indexInfoFactory.build(), { status: 200 })\n    },\n  ),\n  http.delete(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.REDISEARCH)),\n    async () => {\n      return HttpResponse.text('', { status: 204 })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/content/createRedisButtonsHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { getMswResourceURL } from 'uiSrc/utils/test-utils'\n\nconst handlers: HttpHandler[] = [\n  // fetchContentAction\n  http.get(\n    getMswResourceURL(ApiEndpoints.CONTENT_CREATE_DATABASE),\n    async () => {\n      return HttpResponse.json(\n        [\n          {\n            id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff',\n            host: 'localhost',\n            port: 6379,\n            name: 'localhost',\n            username: null,\n            password: null,\n            connectionType: ConnectionType.Standalone,\n            nameFromProvider: null,\n            modules: [],\n            lastConnection: new Date('2021-04-22T09:03:56.917Z'),\n          },\n          {\n            id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4',\n            host: 'localhost',\n            port: 12000,\n            name: 'oea123123',\n            username: null,\n            password: null,\n            connectionType: ConnectionType.Standalone,\n            nameFromProvider: null,\n            modules: [],\n            tls: {\n              verifyServerCert: true,\n              caCertId: '70b95d32-c19d-4311-bb24-e684af12cf15',\n              clientCertPairId: '70b95d32-c19d-4311-b23b24-e684af12cf15',\n            },\n          },\n          {\n            id: 'b83a3932-e95f-4f09-9d8a-55079f400186',\n            host: 'localhost',\n            port: 5005,\n            name: 'sentinel',\n            username: null,\n            password: null,\n            connectionType: ConnectionType.Sentinel,\n            nameFromProvider: null,\n            lastConnection: new Date('2021-04-22T18:40:44.031Z'),\n            modules: [],\n            endpoints: [\n              {\n                host: 'localhost',\n                port: 5005,\n              },\n              {\n                host: '127.0.0.1',\n                port: 5006,\n              },\n            ],\n            sentinelMaster: {\n              name: 'mymaster',\n            },\n          },\n        ],\n        { status: 200 },\n      )\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/content/index.ts",
    "content": "import { HttpHandler } from 'msw'\nimport crb from './createRedisButtonsHandlers'\n\nconst handlers: HttpHandler[] = [...crb]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/index.ts",
    "content": "import instances from './instances'\nimport content from './content'\nimport app from './app'\nimport analytics from './analytics'\nimport browser from './browser'\nimport recommendations from './recommendations'\nimport cloud from './oauth'\nimport tutorials from './tutorials'\nimport rdi from './rdi'\nimport user from './user'\nimport workbench from './workbench'\nimport ai from './ai'\n\n// @ts-ignore\nexport const handlers = [\n  ...instances,\n  ...content,\n  ...app,\n  ...analytics,\n  ...browser,\n  ...recommendations,\n  ...cloud,\n  ...tutorials,\n  ...rdi,\n  ...user,\n  ...workbench,\n  ...ai,\n]\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/instances/caCertsHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\ninterface CaCertRequestBody {\n  username: string\n}\n\nconst handlers: HttpHandler[] = [\n  http.post<any, CaCertRequestBody>(\n    getMswURL(ApiEndpoints.CA_CERTIFICATES),\n    async ({ request }) => {\n      const { username } = await request.clone().json()\n\n      return HttpResponse.json({\n        id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda',\n        username,\n        firstName: 'John',\n        lastName: 'Maverick',\n      })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/instances/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport instances from './instancesHandlers'\nimport caCerts from './caCertsHandlers'\n\nconst handlers: HttpHandler[] = [...instances, ...caCerts]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/instances/instancesHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { ConnectionType, Instance } from 'uiSrc/slices/interfaces'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { MOCK_INFO_API_RESPONSE } from 'uiSrc/mocks/data/instances'\n\nexport const INSTANCE_ID_MOCK = 'instanceId'\nexport const INSTANCES_MOCK: Instance[] = [\n  {\n    id: INSTANCE_ID_MOCK,\n    version: '6.2.6',\n    host: 'localhost',\n    port: 6379,\n    name: 'localhost',\n    username: null,\n    password: null,\n    connectionType: ConnectionType.Standalone,\n    nameFromProvider: null,\n    modules: [],\n    db: 123,\n    lastConnection: new Date('2021-04-22T09:03:56.917Z'),\n    version: null,\n  },\n  {\n    id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4',\n    host: 'localhost',\n    port: 12000,\n    name: 'oea123123',\n    username: null,\n    password: null,\n    connectionType: ConnectionType.Standalone,\n    nameFromProvider: null,\n    modules: [],\n    tls: {\n      verifyServerCert: true,\n      caCertId: '70b95d32-c19d-4311-bb24-e684af12cf15',\n      clientCertPairId: '70b95d32-c19d-4311-b23b24-e684af12cf15',\n    },\n  },\n  {\n    id: 'b83a3932-e95f-4f09-9d8a-55079f400186',\n    version: '6.2.6',\n    host: 'localhost',\n    port: 5005,\n    name: 'sentinel',\n    username: null,\n    password: null,\n    connectionType: ConnectionType.Sentinel,\n    nameFromProvider: null,\n    lastConnection: new Date('2021-04-22T18:40:44.031Z'),\n    modules: [],\n    version: null,\n    endpoints: [\n      {\n        host: 'localhost',\n        port: 5005,\n      },\n      {\n        host: '127.0.0.1',\n        port: 5006,\n      },\n    ],\n    sentinelMaster: {\n      name: 'mymaster',\n    },\n  },\n]\n\nexport const getDatabasesApiSpy = jest\n  .fn()\n  .mockImplementation(async () =>\n    HttpResponse.json(INSTANCES_MOCK, { status: 200 }),\n  )\n\nconst handlers: HttpHandler[] = [\n  // fetchInstancesAction\n  http.get(getMswURL(ApiEndpoints.DATABASES), getDatabasesApiSpy),\n  http.post(getMswURL(ApiEndpoints.DATABASES_EXPORT), async () => {\n    return HttpResponse.json(INSTANCES_MOCK, { status: 200 })\n  }),\n  http.get(getMswURL(getUrl(INSTANCE_ID_MOCK)), async () => {\n    return HttpResponse.json(INSTANCES_MOCK[0], { status: 200 })\n  }),\n  http.get(\n    getMswURL(`/${ApiEndpoints.DATABASES}/:id/info`),\n    // getMswURL(getUrl(INSTANCE_ID_MOCK, 'info')),\n    async () => {\n      return HttpResponse.json(MOCK_INFO_API_RESPONSE, { status: 200 })\n    },\n  ),\n  http.get(getMswURL(`${ApiEndpoints.DATABASES}/:id/connect`), async () => {\n    return HttpResponse.text('', { status: 200 })\n  }),\n  http.post<\n    any,\n    {\n      name: string\n      host: string\n      port: number\n      username: string\n      timeout: number\n      tls: boolean\n    },\n    Partial<Instance>\n  >(getMswURL(`${ApiEndpoints.DATABASES}`), async ({ request }) => {\n    const { username } = await request.json()\n\n    return HttpResponse.json(\n      {\n        id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda',\n        username,\n        host: 'localhost',\n        port: 6379,\n      },\n      { status: 201 },\n    )\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/misc/index.ts",
    "content": "import { getMswURL } from 'uiSrc/utils/test-utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { http, HttpHandler, HttpResponse } from 'msw'\nimport { USER_SETTINGS_DATA_MOCK } from 'uiSrc/mocks/handlers/user/userSettingsHandlers'\n\nconst apiSettings = getMswURL(ApiEndpoints.SETTINGS)\n\nexport const handlers: HttpHandler[] = [\n  http.get(apiSettings, async () => {\n    return HttpResponse.json(USER_SETTINGS_DATA_MOCK, { status: 200 })\n  }),\n  http.patch(apiSettings, async () => {\n    return HttpResponse.json(USER_SETTINGS_DATA_MOCK, { status: 200 })\n  }),\n]\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/oauth/cloud.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { OAUTH_CLOUD_CAPI_KEYS_DATA } from 'uiSrc/mocks/data/oauth'\n\nexport const CLOUD_ME_DATA_MOCK = {\n  id: 66830,\n  name: 'John Smith',\n  currentAccountId: 71011,\n  accounts: [\n    {\n      id: 71011,\n      name: 'Test account',\n    },\n  ],\n  data: {},\n}\n\nconst handlers: HttpHandler[] = [\n  // fetch cloud capi keys\n  http.get(getMswURL(ApiEndpoints.CLOUD_CAPI_KEYS), async () => {\n    return HttpResponse.json(OAUTH_CLOUD_CAPI_KEYS_DATA, { status: 200 })\n  }),\n\n  // fetch user profile\n  http.get(getMswURL(ApiEndpoints.CLOUD_ME), async () => {\n    return HttpResponse.json(CLOUD_ME_DATA_MOCK, { status: 200 })\n  }),\n  http.get(getMswURL(ApiEndpoints.CLOUD_SUBSCRIPTION_PLANS), async () => {\n    return HttpResponse.json(CLOUD_ME_DATA_MOCK, { status: 200 })\n  }),\n  http.post(getMswURL(ApiEndpoints.CLOUD_ME_JOBS), async () => {\n    return HttpResponse.json(CLOUD_ME_DATA_MOCK, { status: 200 })\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/oauth/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport cloud from './cloud'\n\nconst handlers: HttpHandler[] = [...cloud]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/rdi/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport rdiHandler from './rdiHandler'\nimport rdiStrategiesHandler from './rdiPipelineStrategiesHandlers'\n\n// @ts-ignore\nconst handlers: HttpHandler[] = [...rdiHandler, ...rdiStrategiesHandler]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/rdi/rdiHandler.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\n\nconst handlers: HttpHandler[] = [\n  // fetch rdi instances\n  http.get(getMswURL(getUrl(ApiEndpoints.RDI_INSTANCES)), async () => {\n    return HttpResponse.json(\n      [\n        {\n          id: '1',\n          name: 'My first integration',\n          url: 'redis-12345.c253.us-central1-1.gce.cloud.redislabs.com:12345',\n          lastConnection: new Date(),\n          version: '1.2',\n          type: 'api',\n          username: 'user',\n        },\n      ],\n      { status: 200 },\n    )\n  }),\n  http.get(\n    getMswURL(`/${ApiEndpoints.RDI_INSTANCES}/:id/pipeline`),\n    async () => {\n      return HttpResponse.json(\n        {\n          jobs: [\n            { name: 'job1', value: 'value' },\n            { name: 'job2', value: 'value' },\n          ],\n          config: { field: 'value' },\n        },\n        { status: 200 },\n      )\n    },\n  ),\n\n  // create rdi instance\n  http.post(getMswURL(ApiEndpoints.RDI_INSTANCES), async () => {\n    return HttpResponse.json({}, { status: 200 })\n  }),\n\n  // update rdi instance\n  http.patch(getMswURL(getUrl('1', ApiEndpoints.RDI_INSTANCES)), async () => {\n    return HttpResponse.json({}, { status: 200 })\n  }),\n\n  // delete rdi instance\n  http.delete(getMswURL(ApiEndpoints.RDI_INSTANCES), async () => {\n    return HttpResponse.json({}, { status: 200 })\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/rdi/rdiPipelineStrategiesHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getRdiUrl } from 'uiSrc/utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\n\nconst MOCK_RDI_STRATEGIES = {\n  'strategy-type': [\n    {\n      label: 'Ingest',\n      value: 'ingest',\n    },\n    {\n      label: 'Write behind',\n      value: 'write-behind',\n    },\n  ],\n  'db-type': [\n    {\n      label: 'SQL Server',\n      value: 'sql',\n    },\n    {\n      label: 'Oracle',\n      value: 'oracle',\n    },\n    {\n      label: 'MySQL',\n      value: 'my-sql',\n    },\n    {\n      label: 'MariaDB',\n      value: 'maria-db',\n    },\n    {\n      label: 'Cassandra',\n      value: 'cassandra',\n    },\n  ],\n}\n\nconst handlers: HttpHandler[] = [\n  // fetch rdi strategies\n  http.get(\n    getMswURL(getRdiUrl('rdiInstanceId', ApiEndpoints.RDI_PIPELINE_STRATEGIES)),\n    async () => {\n      return HttpResponse.json(MOCK_RDI_STRATEGIES, { status: 200 })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/recommendations/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport recommendations from './recommendationsHandler'\nimport readRecommendations from './recommendationsReadHandler'\n\nconst handlers: HttpHandler[] = [...recommendations, ...readRecommendations]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/recommendations/recommendationsHandler.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { DatabaseRecommendationsResponse as RecommendationResponse } from 'apiSrc/modules/database-recommendation/dto/database-recommendations.response'\nimport { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'\n\nexport const RECOMMENDATIONS_DATA_MOCK = {\n  recommendations: [\n    { name: 'redisSearch', id: 'id', read: false, hide: false },\n    { name: 'bigHashes', id: 'id2', read: false, hide: true },\n  ],\n  totalUnread: 1,\n}\n\nconst handlers: HttpHandler[] = [\n  // fetchRecommendationsAction\n  http.get<any, RecommendationResponse>(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.RECOMMENDATIONS)),\n    async () => {\n      return HttpResponse.json(RECOMMENDATIONS_DATA_MOCK, { status: 200 })\n    },\n  ),\n  http.delete(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.RECOMMENDATIONS)),\n    async () => {\n      return HttpResponse.text('', { status: 200 })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/recommendations/recommendationsReadHandler.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { DatabaseRecommendation as RecommendationResponse } from 'apiSrc/modules/database-recommendation/models/database-recommendation'\nimport { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'\n\nconst EMPTY_RECOMMENDATIONS_MOCK = {\n  recommendations: [],\n  totalUnread: 0,\n}\n\nconst handlers: HttpHandler[] = [\n  // readRecommendationsAction\n  http.patch<any, RecommendationResponse>(\n    getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.RECOMMENDATIONS_READ)),\n    async () => {\n      return HttpResponse.json(EMPTY_RECOMMENDATIONS_MOCK, { status: 200 })\n    },\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/tutorials/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport tutorials from './tutorialsHandlers'\n\n// @ts-ignore\nconst handlers: HttpHandler[] = [...tutorials]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/tutorials/tutorialsHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\nconst handlers: HttpHandler[] = [\n  http.post(getMswURL(ApiEndpoints.CUSTOM_TUTORIALS), () => {\n    return HttpResponse.json({\n      id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda',\n    })\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/user/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport userSettings from './userSettingsHandlers'\n\nconst handlers: HttpHandler[] = [...userSettings]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/user/userSettingsHandlers.ts",
    "content": "import { http, HttpHandler, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\n\nexport const USER_SETTINGS_DATA_MOCK = {\n  theme: 'DARK',\n  dateFormat: 'YYYY-MM-DD',\n  timezone: 'UTC',\n  batchSize: 5,\n  scanThreshold: 10_000,\n  agreements: {\n    eula: true,\n    analytics: true,\n    notifications: true,\n    version: '1.0.0',\n  },\n}\n\nconst apiSettings = getMswURL(ApiEndpoints.SETTINGS)\n\nconst handlers: HttpHandler[] = [\n  http.get(apiSettings, async () => {\n    return HttpResponse.json(USER_SETTINGS_DATA_MOCK, { status: 200 })\n  }),\n  http.patch(apiSettings, async () => {\n    return HttpResponse.json(USER_SETTINGS_DATA_MOCK, { status: 200 })\n  }),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/workbench/commands.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { commandExecutionFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\nimport { INSTANCE_ID_MOCK } from '../instances/instancesHandlers'\n\nconst handlers: RestHandler[] = [\n  http.get(\n    getMswURL(\n      getUrl(INSTANCE_ID_MOCK, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n    ),\n    async () =>\n      HttpResponse.json(commandExecutionFactory.buildList(1), { status: 200 }),\n  ),\n  http.get(\n    getMswURL(\n      getUrl(\n        INSTANCE_ID_MOCK,\n        `${ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS}/:commandId`,\n      ),\n    ),\n    async () =>\n      HttpResponse.json(commandExecutionFactory.build(), { status: 200 }),\n  ),\n  http.post(\n    getMswURL(\n      getUrl(INSTANCE_ID_MOCK, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n    ),\n    async () => {\n      return HttpResponse.json(commandExecutionFactory.buildList(1), {\n        status: 200,\n      })\n    },\n  ),\n  http.delete(\n    getMswURL(\n      getUrl(\n        INSTANCE_ID_MOCK,\n        `${ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS}/:commandId`,\n      ),\n    ),\n    async () => HttpResponse.text('', { status: 200 }),\n  ),\n  http.delete(\n    getMswURL(\n      getUrl(INSTANCE_ID_MOCK, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n    ),\n    async () => HttpResponse.text('', { status: 200 }),\n  ),\n]\n\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/handlers/workbench/index.ts",
    "content": "import { HttpHandler } from 'msw'\n\nimport commands from './commands'\n\nconst handlers: HttpHandler[] = [...commands]\nexport default handlers\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/rdi/RdiInstance.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\n\nexport const rdiInstanceFactory = Factory.define<RdiInstance>(() => ({\n  id: faker.string.uuid(),\n  name: faker.company.name(),\n  url: faker.internet.url(),\n  username: faker.internet.userName(),\n  password: faker.internet.password(),\n  version: faker.system.semver(),\n  lastConnection: faker.date.past(),\n  error: '',\n  loading: false,\n}))\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/res/responseComposition.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils'\n\nexport const errorHandlers = [\n  http.all('*', () => {\n    return HttpResponse.json(\n      { message: DEFAULT_ERROR_MESSAGE },\n      { status: 500 },\n    )\n  }),\n]\n"
  },
  {
    "path": "redisinsight/ui/src/mocks/server.ts",
    "content": "import { setupServer } from 'msw/node'\nimport { http, HttpResponse } from 'msw'\nimport { handlers } from './handlers'\n\n// Setup requests interception using the given handlers.\nexport const mswServer = setupServer(\n  ...handlers,\n  http.all(\n    '*',\n    jest.fn().mockImplementation(async ({ request }) => {\n      console.warn(`[MSW] Unhandled request: ${request.method} ${request.url}`)\n      return HttpResponse.json({}, { status: 200 })\n    }),\n  ),\n)\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/README.md",
    "content": "# Plugin for the “Client List” command\n\nThe plugin has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nyarn\nyarn start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html`\nfrom [RedisInsight](https://github.com/RedisInsight/RedisInsight) repository.\n\n_From Redis Insight Repo_:\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nyarn\nyarn --cwd redisinsight/api/ install\n\nyarn build:statics - for Linux or MacOs\nyarn build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nyarn\nyarn build\n```\n\n[Add](https://github.com/RedisInsight/RedisInsight/blob/main/docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Client list plugin</title>\n\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n    <!-- Run Conditions-->\n    <% if(isDev){ %>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/global_styles.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/dark_theme.css\"\n    />\n    <!-- <link rel=\"stylesheet\" href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/light_theme.css\" /> -->\n    <% } %>\n  </head>\n  <body class=\"theme_DARK\">\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"Show client list as table and highlighted json\",\n  \"source\": \"./src/main.tsx\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"client-list\",\n  \"version\": \"0.0.3\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\"\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"clients-list\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderClientsList\",\n      \"matchCommands\": [\n        \"CLIENT LIST\"\n      ],\n      \"iconDark\": \"./dist/table_view_icon_dark.svg\",\n      \"iconLight\": \"./dist/table_view_icon_light.svg\",\n      \"description\": \"Example of client list plugin\",\n      \"default\": true\n    },\n    {\n      \"id\": \"json-view\",\n      \"name\": \"JSON\",\n      \"activationMethod\": \"renderJSON\",\n      \"matchCommands\": [\n        \"JSON.GET\",\n        \"JSON.MGET\"\n      ],\n      \"iconDark\": \"./dist/json_view_icon_dark.svg\",\n      \"iconLight\": \"./dist/json_view_icon_light.svg\",\n      \"description\": \"Show value as JSON\",\n      \"default\": true\n    },\n    {\n      \"id\": \"json-string-view\",\n      \"name\": \"JSON\",\n      \"activationMethod\": \"renderJSON\",\n      \"matchCommands\": [\n        \"GET\"\n      ],\n      \"iconDark\": \"./dist/json_view_icon_dark.svg\",\n      \"iconLight\": \"./dist/json_view_icon_light.svg\",\n      \"description\": \"Show value as JSON\"\n    }\n  ],\n  \"devDependencies\": {\n    \"vite\": \"file:../node_modules/vite\"\n  },\n  \"dependencies\": {\n    \"@elastic/datemath\": \"^5.0.3\",\n    \"@elastic/eui\": \"34.6.0\",\n    \"buffer\": \"^6.0.3\",\n    \"classnames\": \"^2.3.1\",\n    \"json-bigint\": \"^1.0.0\",\n    \"lodash\": \"^4.17.23\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\"\n  },\n  \"resolutions\": {\n    \"trim\": \"0.0.3\",\n    \"@elastic/eui/**/prismjs\": \"~1.30.0\",\n    \"**/semver\": \"^7.5.2\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/App.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { JSONView, TableView } from './components'\n\nimport { parseClientListResponse, parseJSONASCIIResponse } from './utils'\n\nexport enum CommonPlugin {\n  ClientList = 'ClientList',\n  JSON = 'JSON',\n}\n\nexport enum RawMode {\n  RAW = 'RAW',\n  ASCII = 'ASCII',\n}\n\ninterface Props {\n  plugin: CommonPlugin\n  command: string\n  mode: RawMode\n  result?: { response: any; status: string }[]\n}\n\nconst getJsonResultString = (result: any, mode: RawMode) =>\n  mode !== RawMode.RAW && result !== null\n    ? parseJSONASCIIResponse(result)\n    : result\n\nconst getJsonResultStringFromArr = (response: any, mode: RawMode) =>\n  `[${response.map((result: any) => getJsonResultString(result, mode)).join(',')}]`\n\nconst App = (props: Props) => {\n  const {\n    command = '',\n    result: [{ response = '', status = '' } = {}] = [],\n    plugin,\n    mode,\n  } = props\n\n  if (status === 'fail') {\n    return (\n      <div className=\"cli-container\">\n        <div\n          data-testid=\"cli-output-response-fail\"\n          className=\"cli-output-response-fail\"\n        >\n          {JSON.stringify(response)}\n        </div>\n      </div>\n    )\n  }\n\n  switch (plugin) {\n    case CommonPlugin.ClientList:\n      const clientResult = parseClientListResponse(response)\n      return <TableView query={command} result={clientResult} />\n\n    case CommonPlugin.JSON:\n    default:\n      const jsonResultString = Array.isArray(response)\n        ? getJsonResultStringFromArr(response, mode)\n        : getJsonResultString(response, mode)\n\n      return (\n        <div className=\"cli-container\">\n          <JSONView value={jsonResultString} command={command} />\n        </div>\n      )\n  }\n}\n\nexport default App\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/index.ts",
    "content": "import TableView from './table-view'\nimport JSONView from './json-view'\n\nexport { TableView, JSONView }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/JSONView.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport JSONView from './JSONView'\n\ndescribe('JSONViewer', () => {\n  it('should render proper json', () => {\n    const jsx = JSONView({ value: JSON.stringify({}) })\n    render(jsx.value as React.ReactElement)\n\n    expect(jsx.isValid).toBeTruthy()\n    expect(screen.queryByTestId('value-as-json')).toBeInTheDocument()\n  })\n\n  it('should not render invalid json', () => {\n    const jsx = JSONView({ value: 'zxc' })\n\n    expect(jsx.value).toEqual('zxc')\n    expect(jsx.isValid).toBeFalsy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/JSONView.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport JSONBigInt from 'json-bigint'\nimport { formatRedisReply } from 'redisinsight-plugin-sdk'\nimport JsonPretty from './components/json-pretty'\n\ninterface Props {\n  value: string\n  command: string\n}\n\nconst JSONView = (props: Props) => {\n  const { value, command = '' } = props\n\n  const [result, setResult] = useState<any>(null)\n\n  useEffect(() => {\n    const parseJSON = async (value: string, command: string) => {\n      try {\n        const json = JSONBigInt({\n          useNativeBigInt: true,\n          protoAction: 'preserve',\n          constructorAction: 'preserve',\n        }).parse(value)\n        setResult({ value: json, isValid: true })\n      } catch (_err) {\n        const reply = await formatRedisReply(value, command)\n        setResult({ value: reply, isValid: false })\n      }\n    }\n\n    parseJSON(value, command)\n  }, [value])\n\n  if (!result) return null\n\n  return (\n    <>\n      {result.isValid ? (\n        <div className=\"jsonViewer\" data-testid=\"json-view\">\n          <JsonPretty data={result.value} space={2} />\n        </div>\n      ) : (\n        <div\n          data-testid=\"cli-output-response-success\"\n          className=\"cli-output-response-success\"\n        >\n          {result.value}\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default JSONView\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-array/JsonArray.tsx",
    "content": "import React, { Fragment } from 'react'\n\nimport { IJsonArrayProps } from '../../interfaces'\nimport JsonPretty from '../json-pretty'\n\nconst JsonArray = ({\n  data,\n  space = 2,\n  gap = 0,\n  lastElement = true,\n}: IJsonArrayProps) => (\n  <span data-testid=\"json-array-component\">\n    [{!!data.length && '\\n'}\n    {data.map((value, idx) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <Fragment key={`${idx}`}>\n        {!!space && Array.from({ length: space + gap }, () => ' ')}\n        <JsonPretty\n          data={value}\n          lastElement={idx === data.length - 1}\n          space={space}\n          gap={gap + space}\n        />\n      </Fragment>\n    ))}\n    {!!data.length && !!gap && Array.from({ length: gap }, () => ' ')}]\n    {!lastElement && ','}\n    {'\\n'}\n  </span>\n)\n\nexport default JsonArray\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-array/index.ts",
    "content": "import JsonArray from './JsonArray'\n\nexport default JsonArray\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-object/JsonObject.tsx",
    "content": "import React, { Fragment } from 'react'\n\nimport JsonPretty from '../json-pretty'\nimport { IJsonObjectProps } from '../../interfaces'\n\nconst JsonObject = ({\n  data,\n  space = 2,\n  gap = 0,\n  lastElement = true,\n}: IJsonObjectProps) => {\n  const keys = Object.keys(data)\n  return (\n    <span data-testid=\"json-object-component\">\n      {'{'}\n      {!!keys.length && '\\n'}\n      {keys.map((key, idx) => (\n        <Fragment key={`${key}-{idx}`}>\n          {!!space && Array.from({ length: space + gap }, () => ' ')}\n          <span className=\"json-pretty__key\">{`\"${key}\"`}</span>\n          {': '}\n          <JsonPretty\n            data={data[key]}\n            lastElement={idx === Object.keys(data).length - 1}\n            space={space}\n            gap={gap + space}\n          />\n        </Fragment>\n      ))}\n      {!!keys.length && !!gap && Array.from({ length: gap }, () => ' ')}\n      {'}'}\n      {!lastElement && ','}\n      {'\\n'}\n    </span>\n  )\n}\n\nexport default JsonObject\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-object/index.ts",
    "content": "import JsonObject from './JsonObject'\n\nexport default JsonObject\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-pretty/JsonPretty.tsx",
    "content": "import React from 'react'\n\nimport { IDefaultProps } from '../../interfaces'\nimport { isArray, isObject } from '../../utils'\nimport JsonPrimitive from '../json-primitive'\nimport JsonArray from '../json-array'\nimport JsonObject from '../json-object'\n\nconst JsonPretty = ({ data, ...props }: IDefaultProps) => {\n  if (isArray(data)) {\n    return <JsonArray data={data} {...props} />\n  }\n\n  if (isObject(data)) {\n    return <JsonObject data={data} {...props} />\n  }\n\n  return <JsonPrimitive data={data} {...props} />\n}\n\nexport default JsonPretty\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-pretty/index.ts",
    "content": "import JsonPretty from './JsonPretty'\n\nexport default JsonPretty\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-primitive/JsonPrimitive.tsx",
    "content": "import React from 'react'\nimport { isString, isBoolean, isNull, isNumber } from 'lodash'\nimport { isBigInt } from '../../utils'\nimport { IDefaultProps } from '../../interfaces'\n\nconst JsonPrimitive = ({ data, lastElement = true }: IDefaultProps) => {\n  let stringValue = data\n  let valueStyle = 'json-pretty__other_value'\n\n  if (isNull(data)) {\n    stringValue = 'null'\n    valueStyle = 'json-pretty__null-value'\n  } else if (isString(data)) {\n    stringValue = `\"${data}\"`\n    valueStyle = 'json-pretty__string-value'\n  } else if (isBoolean(data)) {\n    stringValue = data ? 'true' : 'false'\n    valueStyle = 'json-pretty__boolean-value'\n  } else if (isNumber(data)) {\n    stringValue = data.toString()\n    valueStyle = 'json-pretty__number-value'\n  } else if (isBigInt(data)) {\n    stringValue = data.toString()\n    valueStyle = 'json-pretty__bigint-value'\n  } else {\n    stringValue = data.toString()\n  }\n  return (\n    <span data-testid=\"json-primitive-component\">\n      <span className={valueStyle} data-testid=\"json-primitive-value\">\n        {stringValue}\n      </span>\n      {!lastElement && ','}\n      {'\\n'}\n    </span>\n  )\n}\n\nexport default JsonPrimitive\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/components/json-primitive/index.ts",
    "content": "import JsonPrimitive from './JsonPrimitive'\n\nexport default JsonPrimitive\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/index.ts",
    "content": "import JSONView from './JSONView'\n\nexport default JSONView\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/interfaces.ts",
    "content": "export interface IDefaultProps {\n  data: any\n  space?: number\n  gap?: number\n  lastElement?: boolean\n}\n\nexport interface IJsonArrayProps extends IDefaultProps {\n  data: Array<any>\n}\n\nexport interface IJsonObjectProps extends IDefaultProps {\n  data: Record<string, any>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/json-view/utils.ts",
    "content": "export const isBigInt = (data: any) =>\n  typeof data === 'bigint' || data instanceof BigInt\nexport const isArray = (data: any) => Array.isArray(data)\nexport const isObject = (data: any) =>\n  typeof data === 'object' && data !== null && !Array.isArray(data)\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/table-view/TableView.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport TableView, { Props } from './TableView'\nimport {\n  render,\n  waitFor,\n} from '../../../../../RedisInsight/redisinsight/ui/src/utils/test-utils'\n\nconst mockedProps = mock<Props>()\n\ndescribe.skip('TableResult', () => {\n  it('should render', () => {\n    expect(render(<TableView {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('Result element should be \"Not found.\" meanwhile result is [0]', async () => {\n    const { queryByTestId, rerender } = render(\n      <TableView {...instance(mockedProps)} result={null} query=\"ft.search\" />,\n    )\n\n    await waitFor(() => {\n      rerender(\n        <TableView {...instance(mockedProps)} result={[]} query=\"ft.search\" />,\n      )\n    })\n\n    const resultEl = queryByTestId(/query-table-no-results/)\n\n    expect(resultEl).toBeInTheDocument()\n  })\n\n  it('Result element should have 4 cell meanwhile result is not empty', async () => {\n    const result = [\n      {\n        Doc: 'red:2',\n        title: 'Redis Labs',\n      },\n      {\n        Doc: 'red:1',\n        title: 'Redis Labs',\n      },\n    ]\n\n    const { queryByTestId, queryAllByTestId, rerender } = render(\n      <TableView {...instance(mockedProps)} result={[]} query=\"ft.search\" />,\n    )\n\n    await waitFor(() => {\n      rerender(\n        <TableView\n          {...instance(mockedProps)}\n          result={result}\n          query=\"ft.search\"\n        />,\n      )\n    })\n\n    const resultEl = queryByTestId(/query-table-result/)\n    const columnsEl = queryAllByTestId(/query-column/)\n\n    expect(resultEl).toBeInTheDocument()\n    expect(columnsEl?.length).toEqual(4)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/table-view/TableView.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table'\n\nexport interface Props {\n  query: string\n  result: any\n}\n\nconst noResultMessage = 'No results'\n\nconst TableView = React.memo(({ result, query }: Props) => {\n  const [columns, setColumns] = useState<ColumnDefinition<any>[]>([])\n\n  useEffect(() => {\n    if (!result?.length) {\n      return\n    }\n\n    const newColumns = Object.keys(result[0]).map((item) => ({\n      header: item,\n      id: item,\n      accessorKey: item,\n      enableSorting: true,\n    }))\n\n    setColumns(newColumns)\n  }, [result, query])\n\n  return (\n    <div className={cx('queryResultsContainer', 'container')}>\n      <Table data={result ?? []} columns={columns} paginationEnabled />\n      {!result?.length && <span>{noResultMessage}</span>}\n    </div>\n  )\n})\n\nexport default TableView\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/components/table-view/index.ts",
    "content": "import TableView from './TableView'\n\nexport default TableView\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/global.d.ts",
    "content": "declare module '@elastic/eui/es/components/icon/*'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/arrow_down.jsx",
    "content": "function _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  var target = _objectWithoutPropertiesLoose(source, excluded);\n  var key, i;\n  if (Object.getOwnPropertySymbols) {\n    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  var target = {};\n  var sourceKeys = Object.keys(source);\n  var key, i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nimport * as React from 'react';\n\nvar EuiIconArrowDown = function EuiIconArrowDown(_ref) {\n  var title = _ref.title,\n    titleId = _ref.titleId,\n    props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. Hammerhead cannot create svg throw createElementNS\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /*#__PURE__*/ React.createElement(\n      'svg',\n      _extends(\n        {\n          width: 16,\n          height: 16,\n          viewBox: '0 0 16 16',\n          xmlns: 'http://www.w3.org/2000/svg',\n          'aria-labelledby': titleId,\n        },\n        props,\n      ),\n      title\n        ? /*#__PURE__*/ React.createElement(\n            'title',\n            {\n              id: titleId,\n            },\n            title,\n          )\n        : null,\n      /*#__PURE__*/ React.createElement('path', {\n        fillRule: 'non-zero',\n        d: 'M13.069 5.157L8.384 9.768a.546.546 0 01-.768 0L2.93 5.158a.552.552 0 00-.771 0 .53.53 0 000 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 000-.76.552.552 0 00-.771 0z',\n      }),\n    );\n  } catch (e) {\n    return <span>&#8595;</span>;\n  }\n};\n\nexport var icon = EuiIconArrowDown;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/arrow_left.jsx",
    "content": "function _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  var target = _objectWithoutPropertiesLoose(source, excluded);\n  var key, i;\n  if (Object.getOwnPropertySymbols) {\n    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  var target = {};\n  var sourceKeys = Object.keys(source);\n  var key, i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nimport * as React from 'react';\n\nvar EuiIconArrowLeft = function EuiIconArrowLeft(_ref) {\n  var title = _ref.title,\n    titleId = _ref.titleId,\n    props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. Hammerhead cannot create svg throw createElementNS\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /*#__PURE__*/ React.createElement(\n      'svg',\n      _extends(\n        {\n          width: 16,\n          height: 16,\n          viewBox: '0 0 16 16',\n          xmlns: 'http://www.w3.org/2000/svg',\n          'aria-labelledby': titleId,\n        },\n        props,\n      ),\n      title\n        ? /*#__PURE__*/ React.createElement(\n            'title',\n            {\n              id: titleId,\n            },\n            title,\n          )\n        : null,\n      /*#__PURE__*/ React.createElement('path', {\n        fillRule: 'nonzero',\n        d: 'M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z',\n      }),\n    );\n  } catch (e) {\n    return <span>&#8592;</span>;\n  }\n};\n\nexport var icon = EuiIconArrowLeft;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/arrow_right.jsx",
    "content": "import * as React from 'react';\n\nfunction _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (let i = 1; i < arguments.length; i++) {\n        const source = arguments[i];\n        for (const key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  const target = _objectWithoutPropertiesLoose(source, excluded);\n  let key;\n  let i;\n  if (Object.getOwnPropertySymbols) {\n    const sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  const target = {};\n  const sourceKeys = Object.keys(source);\n  let key;\n  let i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nconst EuiIconArrowRight = function EuiIconArrowRight(_ref) {\n  const { title } = _ref;\n  const { titleId } = _ref;\n  const props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. Hammerhead cannot create svg throw createElementNS\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /* #__PURE__ */ React.createElement(\n      'svg',\n      {\n        width: 16,\n        height: 16,\n        viewBox: '0 0 16 16',\n        xmlns: 'http://www.w3.org/2000/svg',\n        'aria-labelledby': titleId,\n        ...props,\n      },\n      title\n        ? /* #__PURE__ */ React.createElement(\n            'title',\n            {\n              id: titleId,\n            },\n            title,\n          )\n        : null,\n      /* #__PURE__ */ React.createElement('path', {\n        fillRule: 'nonzero',\n        d: 'M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z',\n      }),\n    );\n  } catch (e) {\n    return <span>&#8594;</span>;\n  }\n};\n\nexport var icon = EuiIconArrowRight;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/check.js",
    "content": "function _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  var target = _objectWithoutPropertiesLoose(source, excluded);\n  var key, i;\n  if (Object.getOwnPropertySymbols) {\n    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  var target = {};\n  var sourceKeys = Object.keys(source);\n  var key, i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nimport * as React from 'react';\n\nvar EuiIconCheck = function EuiIconCheck(_ref) {\n  var title = _ref.title,\n    titleId = _ref.titleId,\n    props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. TestCafe is failing for default icons\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /*#__PURE__*/ React.createElement(\n      'svg',\n      _extends(\n        {\n          width: 16,\n          height: 16,\n          viewBox: '0 0 16 16',\n          xmlns: 'http://www.w3.org/2000/svg',\n          'aria-labelledby': titleId,\n        },\n        props,\n      ),\n      title\n        ? /*#__PURE__*/ React.createElement(\n            'title',\n            {\n              id: titleId,\n            },\n            title,\n          )\n        : null,\n      /*#__PURE__*/ React.createElement('path', {\n        fillRule: 'evenodd',\n        d: 'M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12',\n      }),\n    );\n  } catch (e) {\n    return <span>&#10004;</span>;\n  }\n};\n\nexport var icon = EuiIconCheck;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/copy.js",
    "content": "function _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  var target = _objectWithoutPropertiesLoose(source, excluded);\n  var key, i;\n  if (Object.getOwnPropertySymbols) {\n    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  var target = {};\n  var sourceKeys = Object.keys(source);\n  var key, i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nimport * as React from 'react';\n\nvar EuiIconCopy = function EuiIconCopy(_ref) {\n  var title = _ref.title,\n    titleId = _ref.titleId,\n    props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. Hammerhead cannot create svg throw createElementNS\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /*#__PURE__*/ React.createElement(\n      'svg',\n      _extends(\n        {\n          width: 16,\n          height: 16,\n          viewBox: '0 0 16 16',\n          xmlns: 'http://www.w3.org/2000/svg',\n          'aria-labelledby': titleId,\n        },\n        props,\n      ),\n      title\n        ? /*#__PURE__*/ React.createElement(\n            'title',\n            {\n              id: titleId,\n            },\n            title,\n          )\n        : null,\n      /*#__PURE__*/ React.createElement('path', {\n        d: 'M11.4 0c.235 0 .46.099.622.273l2.743 3c.151.162.235.378.235.602v9.25a.867.867 0 01-.857.875H3.857A.867.867 0 013 13.125V.875C3 .392 3.384 0 3.857 0H11.4zM14 4h-2.6a.4.4 0 01-.4-.4V1H4v12h10V4z',\n      }),\n      /*#__PURE__*/ React.createElement('path', {\n        d: 'M3 1H2a1 1 0 00-1 1v13a1 1 0 001 1h10a1 1 0 001-1v-1h-1v1H2V2h1V1z',\n      }),\n    );\n  } catch (e) {\n    return <span>&#8595;</span>;\n  }\n};\n\nexport var icon = EuiIconCopy;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/cross.js",
    "content": "function _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  var target = _objectWithoutPropertiesLoose(source, excluded);\n  var key, i;\n  if (Object.getOwnPropertySymbols) {\n    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  var target = {};\n  var sourceKeys = Object.keys(source);\n  var key, i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nimport * as React from 'react';\n\nvar EuiIconCross = function EuiIconCross(_ref) {\n  var title = _ref.title,\n    titleId = _ref.titleId,\n    props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. Hammerhead cannot create svg throw createElementNS\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /*#__PURE__*/ React.createElement(\n      'svg',\n      _extends(\n        {\n          width: 16,\n          height: 16,\n          viewBox: '0 0 16 16',\n          xmlns: 'http://www.w3.org/2000/svg',\n          'aria-labelledby': titleId,\n        },\n        props,\n      ),\n      title\n        ? /*#__PURE__*/ React.createElement(\n            'title',\n            {\n              id: titleId,\n            },\n            title,\n          )\n        : null,\n      /*#__PURE__*/ React.createElement('path', {\n        d: 'M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z',\n      }),\n    );\n  } catch (e) {\n    return <span>&#10539;</span>;\n  }\n};\n\nexport var icon = EuiIconCross;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/icons/empty.js",
    "content": "function _extends() {\n  _extends =\n    Object.assign ||\n    function (target) {\n      for (var i = 1; i < arguments.length; i++) {\n        var source = arguments[i];\n        for (var key in source) {\n          if (Object.prototype.hasOwnProperty.call(source, key)) {\n            target[key] = source[key];\n          }\n        }\n      }\n      return target;\n    };\n  return _extends.apply(this, arguments);\n}\n\nfunction _objectWithoutProperties(source, excluded) {\n  if (source == null) return {};\n  var target = _objectWithoutPropertiesLoose(source, excluded);\n  var key, i;\n  if (Object.getOwnPropertySymbols) {\n    var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n    for (i = 0; i < sourceSymbolKeys.length; i++) {\n      key = sourceSymbolKeys[i];\n      if (excluded.indexOf(key) >= 0) continue;\n      if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n      target[key] = source[key];\n    }\n  }\n  return target;\n}\n\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n  if (source == null) return {};\n  var target = {};\n  var sourceKeys = Object.keys(source);\n  var key, i;\n  for (i = 0; i < sourceKeys.length; i++) {\n    key = sourceKeys[i];\n    if (excluded.indexOf(key) >= 0) continue;\n    target[key] = source[key];\n  }\n  return target;\n}\n\nimport * as React from 'react';\n\nvar EuiIconEmpty = function EuiIconEmpty(_ref) {\n  var title = _ref.title,\n    titleId = _ref.titleId,\n    props = _objectWithoutProperties(_ref, ['title', 'titleId']);\n\n  // For e2e tests. TestCafe is failing for default icons\n  try {\n    document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n\n    return /*#__PURE__*/ React.createElement(\n      'svg',\n      _extends(\n        {\n          width: 16,\n          height: 16,\n          viewBox: '0 0 16 16',\n          xmlns: 'http://www.w3.org/2000/svg',\n          'aria-labelledby': titleId,\n        },\n        props,\n      ),\n    );\n  } catch (e) {\n    return <span>''</span>;\n  }\n};\n\nexport var icon = EuiIconEmpty;\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/main.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { render } from 'react-dom'\nimport { ThemeProvider } from 'uiSrc/components/base/utils/pluginsThemeContext'\nimport App, { CommonPlugin, RawMode } from './App'\nimport './styles/styles.scss'\n\ninterface Props {\n  command?: string\n  mode: RawMode\n  data?: { response: any; status: string }[]\n}\n\nconst renderClientsList = (props: Props) => {\n  const { command = '', data: result = [], mode } = props\n  render(\n    <ThemeProvider>\n      <App\n        plugin={CommonPlugin.ClientList}\n        command={command}\n        result={result}\n        mode={mode}\n      />\n    </ThemeProvider>,\n    document.getElementById('app'),\n  )\n}\n\nconst renderJSON = (props: Props) => {\n  const { command = '', data: result = [], mode } = props\n\n  render(\n    <ThemeProvider>\n      <App\n        plugin={CommonPlugin.JSON}\n        command={command}\n        result={result}\n        mode={mode}\n      />\n    </ThemeProvider>,\n    document.getElementById('app'),\n  )\n}\n\nif (process.env.NODE_ENV === 'development') {\n  // renderClientsList({ command: '', data: result || [] })\n  const mode = RawMode.RAW\n\n  const data = [\n    {\n      status: 'success',\n      // response: ['{\\\\\"test\\\\\":\\\\\"test\\\\\"}', '{\\\\\"foo\\\\\":\\\\\"bar\\\\\"}']\n      response: '[{\"about\":\"test\\\\r\\\\n\"}]',\n    },\n  ]\n\n  renderJSON({ command: '', data, mode })\n}\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderClientsList, renderJSON }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/result.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": \"id=13091001001 addr=172.17.0.1:55380 fd=97 name=redisinsight-cli-bc31a9f9 age=2 idle=2 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=2147483647 obl=0 oll=0 omem=0 events=r cmd=CLIENT user=default\\r\\nid=6001002 addr=172.17.0.1:43276 fd=99 name=redisinsight-browser-808492c8 age=6561 idle=6561 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=2147483647 obl=0 oll=0 omem=0 events=r cmd=TTL user=default\\r\\nid=3765001002 addr=172.17.0.1:59014 fd=100 name=redisinsight-common-1239970f age=4681 idle=4681 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=2147483647 obl=0 oll=0 omem=0 events=r cmd=INFO user=default\\r\\n\"\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/styles/styles.scss",
    "content": "@charset \"UTF-8\";\n@use \"../../../../styles/mixins/eui\";\n\n* div,\n* span {\n  font-family: \"Graphik\", sans-serif !important;\n}\n\nhtml {\n  background-color: var(--wbRunResultsBg) !important;\n}\n\n.container {\n  @include eui.scrollBar;\n  flex: auto;\n  overflow: auto;\n  max-height: 810px;\n  white-space: pre-wrap;\n  word-break: break-all;\n  padding: 16px 20px;\n\n  font:\n    normal normal normal 13px/17px Graphik,\n    sans-serif;\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--textColorShade);\n\n  background-color: var(--wbRunResultsBg);\n\n  z-index: 10;\n}\n\n.table,\n.tableInfo {\n  .euiFlexGroup--justifyContentSpaceBetween {\n    display: none;\n\n    // hide <PerPageComponent/>  option {hidePerPageOptions} doesn't work\n    // with dynamic changing prop \"pagination\" for In-memory table\n    .euiFlexItem:first-child {\n      display: none;\n    }\n  }\n\n  &.tableWithPagination {\n    .euiFlexGroup--justifyContentSpaceBetween {\n      display: flex;\n    }\n  }\n}\n\n.tableInfo {\n  padding: 16px 0;\n}\n\n.tooltipContainer {\n  max-width: 100%;\n}\n\n.tooltip {\n  max-width: 100%;\n\n  display: inline-block !important;\n}\n\n.cell {\n  position: relative;\n}\n\n.row {\n  display: block;\n  padding-bottom: 10px;\n  word-break: break-word;\n\n  &:last-of-type {\n    padding-bottom: 0;\n  }\n}\n\n.icon {\n  position: relative;\n  margin: 0 auto;\n\n  @media only screen and (max-width: 767px) {\n    margin: 0;\n  }\n}\n\n.cli-container {\n  padding-left: 20px;\n  border-top: none;\n}\n\n.cli-output-response-fail,\n.cli-output-response-success {\n  font-family: Inconsolata, serif !important;\n}\n\n.jsonViewer {\n  font:\n    normal normal normal 13px/18px Inconsolata,\n    monospace;\n  letter-spacing: 0.15px;\n  padding: 0;\n  background: transparent;\n  color: var(--euiTextSubduedColor);\n  margin-bottom: 0;\n  white-space: pre-wrap;\n\n  &-collapsed {\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    width: 100%;\n    overflow: hidden;\n    line-height: inherit;\n  }\n\n  .key {\n    color: var(--jsonKeyNameColor);\n  }\n\n  .number,\n  .bigint,\n  .undefined,\n  .null {\n    color: var(--jsonNumberColor);\n  }\n\n  .string {\n    color: var(--jsonStringColor);\n  }\n\n  .boolean {\n    color: var(--jsonBooleanColor);\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/utils/cachedIcons.ts",
    "content": "import { icon as EuiIconArrowRight } from '@elastic/eui/es/components/icon/assets/arrow_right'\nimport { icon as EuiIconArrowLeft } from '@elastic/eui/es/components/icon/assets/arrow_left'\nimport { icon as EuiIconArrowDown } from '@elastic/eui/es/components/icon/assets/arrow_down'\n\nexport default {\n  arrowRight: EuiIconArrowRight,\n  arrowLeft: EuiIconArrowLeft,\n  arrowDown: EuiIconArrowDown,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/utils/index.ts",
    "content": "import cachedIcons from './cachedIcons'\n\nexport * from './parseResponse'\n\nexport { cachedIcons }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/clients-list/src/utils/parseResponse.ts",
    "content": "import { Buffer } from 'buffer'\n\nexport const parseClientListResponse = (response: string) =>\n  response\n    .split(/\\r?\\n/)\n    .filter((r: string) => r)\n    .map((row: string) => {\n      const value = row.split(' ')\n      const obj: any = {}\n      value.forEach((v: string) => {\n        const pair = v.split('=')\n        // eslint-disable-next-line prefer-destructuring\n        obj[pair[0]] = pair[1]\n      })\n      return obj\n    })\n\nexport const parseJSONASCIIResponse = (response: string): string =>\n  getBufferFromSafeASCIIString(response).toString()\n\nexport const getBufferFromSafeASCIIString = (str: string): Buffer => {\n  const bytes = []\n\n  for (let i = 0; i < str.length; i += 1) {\n    if (str[i] === '\\\\') {\n      if (str[i + 1] === 'x') {\n        const hexString = str.substr(i + 2, 2)\n        if (isHex(hexString)) {\n          bytes.push(Buffer.from(hexString, 'hex'))\n          i += 3\n          // eslint-disable-next-line no-continue\n          continue\n        }\n      }\n\n      if (['a', '\"', '\\\\', 'b', 't', 'n', 'r'].includes(str[i + 1])) {\n        switch (str[i + 1]) {\n          case 'a':\n            bytes.push(Buffer.from('\\u0007'))\n            break\n          case 'b':\n            bytes.push(Buffer.from('\\b'))\n            break\n          case 't':\n            bytes.push(Buffer.from('\\t'))\n            break\n          case 'n':\n            bytes.push(Buffer.from('\\n'))\n            break\n          case 'r':\n            bytes.push(Buffer.from('\\r'))\n            break\n          default:\n            bytes.push(Buffer.from(str[i + 1]))\n        }\n\n        i += 1\n        // eslint-disable-next-line no-continue\n        continue\n      }\n    }\n\n    bytes.push(Buffer.from(str[i]))\n  }\n\n  return Buffer.concat(bytes)\n}\n\nfunction isHex(str: string) {\n  return /^[A-F0-9]{1,2}$/i.test(str)\n}\n\nexport const isJson = (item: any): boolean => {\n  let value = typeof item !== 'string' ? JSON.stringify(item) : item\n  try {\n    value = JSON.parse(value)\n  } catch (e) {\n    return false\n  }\n\n  return typeof value === 'object' && value !== null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/common/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"Redis Insight common packages for plugins\",\n  \"main\": \"./index.js\",\n  \"name\": \"common\",\n  \"version\": \"0.0.1\",\n  \"visualizations\": [],\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"Redis Insight plugins\",\n  \"name\": \"shared\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"build\": \"vite build\"\n  },\n  \"devDependencies\": {\n    \"@types/d3\": \"^7.4.3\",\n    \"@types/file-saver\": \"^2.0.7\",\n    \"@types/jest\": \"^29.5.14\",\n    \"concurrently\": \"^9.1.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"esbuild\": \"^0.25.2\",\n    \"jest\": \"^29.7.0\",\n    \"process\": \"^0.11.10\",\n    \"rimraf\": \"^6.0.1\",\n    \"rollup-plugin-css-only\": \"^4.5.2\",\n    \"vite\": \"^6.4.1\",\n    \"vite-plugin-ejs\": \"^1.7.0\",\n    \"vite-plugin-static-copy\": \"^2.3.2\"\n  },\n  \"dependencies\": {}\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Redis Query Engine plugin</title>\n\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n    <!-- Run Conditions-->\n    <% if(isDev){ %>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/global_styles.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/dark_theme.css\"\n    />\n    <!-- <link rel=\"stylesheet\" href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/light_theme.css\" /> -->\n    <% } %>\n  </head>\n  <body class=\"theme_DARK\">\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"Redis Insight plugin for RediSearch module\",\n  \"source\": \"./src/index.tsx\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"redisearch\",\n  \"version\": \"0.0.2\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\"\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"redisearch\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderRediSearch\",\n      \"matchCommands\": [\n        \"FT.INFO\",\n        \"FT.SEARCH\",\n        \"FT.AGGREGATE\"\n      ],\n      \"iconDark\": \"./dist/table_view_icon_dark.svg\",\n      \"iconLight\": \"./dist/table_view_icon_light.svg\",\n      \"description\": \"RediSearch default plugin\",\n      \"default\": true\n    },\n    {\n      \"id\": \"redisearch-profile\",\n      \"name\": \"Table\",\n      \"activationMethod\": \"renderRediSearch\",\n      \"matchCommands\": [\n        \"FT.PROFILE\"\n      ],\n      \"iconDark\": \"./dist/table_view_icon_dark.svg\",\n      \"iconLight\": \"./dist/table_view_icon_light.svg\",\n      \"description\": \"RediSearch default plugin\",\n      \"default\": false\n    }\n  ],\n  \"dependencies\": {\n    \"@elastic/datemath\": \"^5.0.3\",\n    \"@elastic/eui\": \"34.6.0\",\n    \"classnames\": \"^2.3.1\",\n    \"lodash\": \"^4.17.23\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"redisinsight-plugin-sdk\": \"^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"vite\": \"file:../node_modules/vite\"\n  },\n  \"resolutions\": {\n    \"trim\": \"0.0.3\",\n    \"@elastic/eui/**/prismjs\": \"~1.30.0\",\n    \"**/semver\": \"^7.5.2\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/App.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { isArray } from 'lodash'\nimport { setHeaderText } from 'redisinsight-plugin-sdk'\n\nimport {\n  parseInfoRawResponse,\n  parseSearchRawResponse,\n  parseAggregateRawResponse,\n} from './utils'\nimport { Command, ProfileType } from './constants'\nimport { TableInfoResult, TableResult } from './components'\n\ninterface Props {\n  command: string\n  result?: { response: any; status: string }[]\n}\n\nconst App = (props: Props) => {\n  const { command = '', result: [{ response = '', status = '' } = {}] = [] } =\n    props\n\n  if (status === 'fail') {\n    return <div className=\"responseFail\">{response}</div>\n  }\n\n  const commandUpper = command.toUpperCase()\n\n  if (commandUpper.startsWith(Command.Info)) {\n    const result = parseInfoRawResponse(response)\n    return <TableInfoResult query={command} result={result} />\n  }\n\n  const isProfileCommand = commandUpper.startsWith(Command.Profile)\n  const profileQueryType = command?.split(' ')?.[2]\n\n  if (\n    commandUpper.startsWith(Command.Aggregate) ||\n    (isProfileCommand &&\n      profileQueryType.toUpperCase() === ProfileType.Aggregate)\n  ) {\n    const isResponseInArray = isArray(response[0])\n    const [matched, ...arrayResponse] = isResponseInArray\n      ? response[0]\n      : response\n    setHeaderText(`Matched:${matched}`)\n\n    const result = parseAggregateRawResponse(arrayResponse)\n    return (\n      <TableResult\n        query={command}\n        result={result}\n        matched={matched}\n        cursorId={isResponseInArray ? response[1] : null}\n      />\n    )\n  }\n\n  if (\n    commandUpper.startsWith(Command.Search) ||\n    (isProfileCommand && profileQueryType.toUpperCase() === ProfileType.Search)\n  ) {\n    const [matched, ...arrayResponse] = isProfileCommand\n      ? response[0]\n      : response\n    setHeaderText(`Matched:${matched}`)\n\n    const result = parseSearchRawResponse(command, arrayResponse)\n    return <TableResult query={command} result={result} matched={matched} />\n  }\n\n  return null\n}\n\nexport default App\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/GroupBadge/GroupBadge.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { RiBadge } from '../../../../../components/base/display/badge/RiBadge'\n\nimport { GROUP_TYPES_COLORS, GROUP_TYPES_DISPLAY } from '../../constants'\n\nexport interface Props {\n  type: any\n  name?: string\n  className?: string\n}\n\nconst GroupBadge = ({ type, name = '', className = '' }: Props) => {\n  // @ts-ignore\n  const groupTypeDisplay = GROUP_TYPES_DISPLAY[type]\n  // @ts-ignore\n  const backgroundColor = GROUP_TYPES_COLORS[type] ?? '#14708D'\n  return (\n    <RiBadge\n      style={{ backgroundColor, color: 'var(--euiTextSubduedColorHover)' }}\n      className={cx(className, 'text-uppercase')}\n      data-testid={`badge-${type} ${name}`}\n      label={groupTypeDisplay ?? type}\n    />\n  )\n}\n\nexport default GroupBadge\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/GroupBadge/index.ts",
    "content": "import GroupBadge from './GroupBadge'\n\nexport default GroupBadge\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/TableInfoResult/TableInfoResult.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, act } from 'uiSrc/utils/test-utils'\nimport TableInfoResult, { Props } from './TableInfoResult'\n\nconst mockedProps = mock<Props>()\n\nconst resultMock: any[] = []\n\ndescribe.skip('TableInfoResult', () => {\n  it('should render', () => {\n    expect(\n      render(<TableInfoResult query=\"get\" result={resultMock} />),\n    ).toBeTruthy()\n  })\n\n  it('Result element should be \"Not found.\" meanwhile result is [0]', async () => {\n    const { queryByTestId, rerender } = render(\n      <TableInfoResult\n        {...instance(mockedProps)}\n        result={null}\n        query=\"ft.search\"\n      />,\n    )\n\n    await act(() => {\n      rerender(\n        <TableInfoResult\n          {...instance(mockedProps)}\n          result={[]}\n          query=\"ft.search\"\n        />,\n      )\n    })\n\n    const resultEl = queryByTestId(/query-table-no-results/)\n\n    expect(resultEl).toBeInTheDocument()\n  })\n\n  it.skip('Result element should have 4 cell meanwhile result is not empty', async () => {\n    const result = [\n      {\n        Doc: 'red:2',\n        title: 'Redis Labs',\n      },\n      {\n        Doc: 'red:1',\n        title: 'Redis Labs',\n      },\n    ]\n\n    const { queryByTestId, queryAllByTestId, rerender } = render(\n      <TableInfoResult\n        {...instance(mockedProps)}\n        result={[]}\n        query=\"ft.search\"\n      />,\n    )\n\n    await act(() => {\n      rerender(\n        <TableInfoResult\n          {...instance(mockedProps)}\n          result={result}\n          query=\"ft.search\"\n        />,\n      )\n    })\n\n    const resultEl = queryByTestId(/query-table-result/)\n    const columnsEl = queryAllByTestId(/query-column/)\n\n    expect(resultEl).toBeInTheDocument()\n    expect(columnsEl?.length).toEqual(4)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/TableInfoResult/TableInfoResult.tsx",
    "content": "/* eslint-disable react/prop-types */\nimport React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { toUpper, flatten, isArray, isEmpty, map, uniq } from 'lodash'\nimport { Table, ColumnDefinition } from 'uiSrc/components/base/layout/table'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { ColorText, Text } from '../../../../../components/base/text'\nimport { LoadingContent, Spacer } from '../../../../../components/base/layout'\nimport GroupBadge from '../GroupBadge'\nimport { InfoAttributesBoolean } from '../../constants'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  query: string\n  result: any\n}\n\nconst noResultsMessage = 'No results found.'\nconst noOptionsMessage = 'No options found'\n\nconst TableInfoResult = React.memo((props: Props) => {\n  const { result: resultProp, query } = props\n\n  const [result, setResult] = useState(resultProp)\n  const [items, setItems] = useState([])\n\n  useEffect(() => {\n    setResult(resultProp)\n\n    const items = resultProp?.attributes || resultProp?.fields\n    if (!items?.length) {\n      return\n    }\n\n    setItems(items)\n  }, [resultProp, query])\n\n  const isBooleanColumn = (title = '') =>\n    InfoAttributesBoolean.indexOf(title) !== -1\n\n  const uniqColumns =\n    uniq(flatten(map(items, (item) => Object.keys(item)))) ?? []\n\n  const columns: ColumnDefinition<any>[] = uniqColumns.map(\n    (title: string = ' ') => ({\n      header: toUpper(title),\n      id: title,\n      accessorKey: title,\n      enableSorting: false,\n      cell: ({ row: { original } }) => {\n        const initValue = original[title]\n        if (isBooleanColumn(title)) {\n          return (\n            <div className=\"icon\" data-testid={`query-column-${title}`}>\n              <RiIcon\n                type={initValue ? 'CheckThinIcon' : 'CancelSlimIcon'}\n                color={initValue ? 'primary500' : 'danger600'}\n              />\n            </div>\n          )\n        }\n        return <Text>{initValue}</Text>\n      },\n    }),\n  )\n\n  const Header = () => (\n    <div>\n      {result ? (\n        <>\n          <Text className=\"row\" size=\"s\">\n            Indexing\n            <GroupBadge\n              type={result?.index_definition?.key_type?.toLowerCase()}\n              className=\"badge\"\n            />\n            documents prefixed by{' '}\n            {result?.index_definition?.prefixes\n              ?.map((prefix: any) => `\"${prefix}\"`)\n              .join(',')}\n          </Text>\n          <Text className=\"row\" size=\"s\">\n            Options:{' '}\n            {result?.index_options?.length ? (\n              <ColorText style={{ color: 'var(--euiColorFullShade)' }}>\n                {result?.index_options?.join(', ')}\n              </ColorText>\n            ) : (\n              <span className=\"italic\">{noOptionsMessage}</span>\n            )}\n          </Text>\n        </>\n      ) : (\n        <LoadingContent lines={2} />\n      )}\n    </div>\n  )\n  const Footer = () => (\n    <div>\n      {result ? (\n        <Text className=\"row\" size=\"s\">\n          {`Number of docs: ${result?.num_docs || '0'} (max ${result?.max_doc_id || '0'}) | `}\n          {`Number of records: ${result?.num_records || '0'} | `}\n          {`Number of terms: ${result?.num_terms || '0'}`}\n        </Text>\n      ) : (\n        <LoadingContent lines={1} />\n      )}\n    </div>\n  )\n\n  const isDataArr =\n    !React.isValidElement(result) && !(isArray(result) && isEmpty(result))\n  const isDataEl = React.isValidElement(result)\n\n  return (\n    <div className=\"container\">\n      {isDataArr && (\n        <FlexGroup\n          direction=\"column\"\n          gap=\"m\"\n          className=\"content\"\n          data-testid={`query-table-result-${query}`}\n        >\n          {Header()}\n          <Table columns={columns} data={items ?? []} />\n          {Footer()}\n        </FlexGroup>\n      )}\n      {isDataEl && <div className={cx('resultEl')}>{result}</div>}\n      {!isDataArr && !isDataEl && (\n        <div className={cx('resultEl')} data-testid=\"query-table-no-results\">\n          {noResultsMessage}\n        </div>\n      )}\n    </div>\n  )\n})\n\nexport default TableInfoResult\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/TableInfoResult/index.ts",
    "content": "import TableInfoResult from './TableInfoResult'\n\nexport default TableInfoResult\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/TableResult/TableResult.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, act } from 'uiSrc/utils/test-utils'\nimport TableResult, { Props } from './TableResult'\n\nconst mockedProps = mock<Props>()\n\ndescribe.skip('TableResult', () => {\n  it('should render', () => {\n    expect(render(<TableResult {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('Result element should be \"Not found.\" meanwhile result is [0]', async () => {\n    const { queryByTestId, rerender } = render(\n      <TableResult\n        {...instance(mockedProps)}\n        result={null}\n        query=\"ft.search\"\n      />,\n    )\n\n    await act(() => {\n      rerender(\n        <TableResult\n          {...instance(mockedProps)}\n          result={[]}\n          query=\"ft.search\"\n        />,\n      )\n    })\n\n    const resultEl = queryByTestId(/query-table-no-results/)\n\n    expect(resultEl).toBeInTheDocument()\n  })\n\n  it('Result element should have 4 cell meanwhile result is not empty', async () => {\n    const result = [\n      {\n        Doc: 'red:2',\n        title: 'Redis Labs',\n      },\n      {\n        Doc: 'red:1',\n        title: 'Redis Labs',\n      },\n    ]\n\n    const { queryByTestId, queryAllByTestId, rerender } = render(\n      <TableResult {...instance(mockedProps)} result={[]} query=\"ft.search\" />,\n    )\n\n    await act(() => {\n      rerender(\n        <TableResult\n          {...instance(mockedProps)}\n          result={result}\n          query=\"ft.search\"\n        />,\n      )\n    })\n\n    const resultEl = queryByTestId(/query-table-result/)\n    const columnsEl = queryAllByTestId(/query-column/)\n\n    expect(resultEl).toBeInTheDocument()\n    expect(columnsEl?.length).toEqual(4)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/TableResult/TableResult.tsx",
    "content": "import React, { useEffect, useMemo, useState } from 'react'\nimport parse from 'html-react-parser'\nimport cx from 'classnames'\nimport { flatten, isArray, isEmpty, map, uniq } from 'lodash'\nimport styled from 'styled-components'\n\nimport { handleCopy as handleCopyUtil } from 'uiSrc/utils'\nimport { Table, ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { ColorText } from 'uiSrc/components/base/text/ColorText'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CopyIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip/RITooltip'\nimport {\n  CommandArgument,\n  Command,\n} from 'uiSrc/packages/redisearch/src/constants'\nimport {\n  formatLongName,\n  replaceSpaces,\n} from 'uiSrc/packages/redisearch/src/utils'\nimport MultilineEllipsisText from 'uiSrc/components/base/text/MultilineEllipsisText'\n\nexport interface Props {\n  query: string\n  result: any\n  matched?: number\n  cursorId?: null | number\n}\n\nconst EllipsisText = styled(ColorText)`\n  overflow: hidden;\n  text-overflow: ellipsis;\n`\n\nconst TableWrapper = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  min-width: 0;\n  overflow: auto;\n  padding: 2px;\n`\n\nconst noResultsMessage = 'No results found.'\n\nconst TableResult = React.memo((props: Props) => {\n  const { result, query, matched, cursorId } = props\n\n  const [columns, setColumns] = useState<ColumnDef<any>[]>([])\n\n  const checkShouldParsedHTML = (query: string) => {\n    const command = query.toUpperCase()\n    return (\n      command.startsWith(Command.Search) &&\n      command.includes(CommandArgument.Highlight)\n    )\n  }\n\n  const handleCopy = (event: React.MouseEvent, text: string) => {\n    event.preventDefault()\n    event.stopPropagation()\n\n    handleCopyUtil(text)\n  }\n\n  useEffect(() => {\n    if (!result?.length) {\n      return\n    }\n\n    const shouldParsedHTML = checkShouldParsedHTML(query)\n    const uniqColumns =\n      uniq(flatten(map(result, (doc) => Object.keys(doc)))) ?? []\n\n    const newColumns: ColumnDef<any>[] = uniqColumns.map(\n      (title: string = ' ') => ({\n        header: title,\n        id: title,\n        accessorKey: title,\n        cell: ({ row: { original } }) => {\n          const initValue = original[title] || ''\n          if (!initValue || (isArray(initValue) && isEmpty(initValue))) {\n            return ''\n          }\n\n          const value = initValue.toString()\n          let cellContent: string | JSX.Element | JSX.Element[] = replaceSpaces(\n            initValue.toString().substring(0, 200),\n          )\n\n          if (shouldParsedHTML) {\n            cellContent = parse(cellContent)\n          }\n\n          return (\n            <div\n              role=\"presentation\"\n              className={cx('tooltipContainer')}\n              data-testid={`query-column-${title}`}\n            >\n              <RiTooltip\n                position=\"left\"\n                title={title}\n                anchorClassName={cx('tooltip')}\n                content={\n                  <MultilineEllipsisText lineCount={7} paddingBlock=\"s\">\n                    {formatLongName(value.toString())}\n                  </MultilineEllipsisText>\n                }\n              >\n                <div className=\"copy-btn-wrapper\">\n                  <EllipsisText className={cx('cell', 'test')}>\n                    {cellContent}\n                  </EllipsisText>\n                  <IconButton\n                    icon={CopyIcon}\n                    aria-label=\"Copy result\"\n                    className=\"copy-near-btn\"\n                    onClick={(event: React.MouseEvent) =>\n                      handleCopy(event, initValue)\n                    }\n                  />\n                </div>\n              </RiTooltip>\n            </div>\n          )\n        },\n      }),\n    )\n\n    setColumns(newColumns)\n  }, [result, query])\n\n  const isDataArr =\n    !React.isValidElement(result) && !(isArray(result) && isEmpty(result))\n  const isDataEl = React.isValidElement(result)\n\n  const MIN_COLUMN_WIDTH = 180\n  const tableMinWidth = useMemo(\n    () => `${Math.max(columns.length * MIN_COLUMN_WIDTH, 920)}px`,\n    [columns.length],\n  )\n\n  return (\n    <div className={cx('queryResultsContainer', 'container')}>\n      <div className=\"queryHeader\">\n        {!!matched && (\n          <div className={cx('matched')}>{`Matched: ${matched}`}</div>\n        )}\n        {!!cursorId && (\n          <div className={cx('matched')}>{`Cursor ID: ${cursorId}`}</div>\n        )}\n      </div>\n      {isDataArr && (\n        <TableWrapper data-testid={`query-table-result-${query}`}>\n          <Table columns={columns} data={result ?? []} minWidth={tableMinWidth} />\n        </TableWrapper>\n      )}\n      {isDataEl && <div className={cx('resultEl')}>{result}</div>}\n      {!isDataArr && !isDataEl && (\n        <div className={cx('resultEl')} data-testid=\"query-table-no-results\">\n          {noResultsMessage}\n        </div>\n      )}\n    </div>\n  )\n})\n\nexport default TableResult\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/TableResult/index.ts",
    "content": "import TableResult from './TableResult'\n\nexport default TableResult\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/components/index.ts",
    "content": "import GroupBadge from './GroupBadge'\nimport TableInfoResult from './TableInfoResult'\nimport TableResult from './TableResult'\n\nexport { GroupBadge, TableResult, TableInfoResult }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/constants/constants.ts",
    "content": "export const InfoAttributesBoolean: string[] = ['NOSTEM', 'NOINDEX', 'SORTABLE']\n\nexport enum Command {\n  Search = 'FT.SEARCH',\n  Aggregate = 'FT.AGGREGATE',\n  Info = 'FT.INFO',\n  Profile = 'FT.PROFILE',\n}\n\nexport enum ProfileType {\n  Search = 'SEARCH',\n  Aggregate = 'AGGREGATE',\n}\n\nexport enum CommandArgument {\n  NoContent = 'NOCONTENT',\n  Return = 'RETURN',\n  Highlight = 'HIGHLIGHT',\n  WithScores = 'WITHSCORES',\n  WithPayloads = 'WITHPAYLOADS',\n  WithSortKeys = 'WITHSORTKEYS',\n}\n\nexport enum ResultFieldNameView {\n  Score = 'Score',\n  Payloads = 'Payloads',\n  Name = 'Name',\n}\n\nexport enum ResultInfoField {\n  Attributes = 'attributes',\n  Fields = 'fields',\n  Options = 'index_options',\n}\n\nexport const ResultInfoAttributes: string[] = [\n  'Name',\n  'Type',\n  'WEIGHT',\n  'NOSTEM',\n  'NOINDEX',\n  'SORTABLE',\n  'SEPARATOR',\n  'PHONETIC',\n]\n\nexport enum KeyTypes {\n  Hash = 'hash',\n  List = 'list',\n  Set = 'set',\n  ZSet = 'zset',\n  String = 'string',\n  ReJSON = 'ReJSON-RL',\n  JSON = 'json',\n}\n\nexport const GROUP_TYPES_DISPLAY = Object.freeze({\n  [KeyTypes.Hash]: 'Hash',\n  [KeyTypes.List]: 'List',\n  [KeyTypes.Set]: 'Set',\n  [KeyTypes.ZSet]: 'Zset',\n  [KeyTypes.String]: 'String',\n  [KeyTypes.ReJSON]: 'JSON',\n  [KeyTypes.JSON]: 'JSON',\n})\n\n// Enums don't allow to use dynamic key\nexport const GROUP_TYPES_COLORS = Object.freeze({\n  [KeyTypes.Hash]: 'var(--typeHashColor)',\n  [KeyTypes.List]: 'var(--typeListColor)',\n  [KeyTypes.Set]: 'var(--typeSetColor)',\n  [KeyTypes.ZSet]: 'var(--typeZSetColor)',\n  [KeyTypes.String]: 'var(--typeStringColor)',\n  [KeyTypes.ReJSON]: 'var(--typeReJSONColor)',\n  [KeyTypes.JSON]: 'var(--typeReJSONColor)',\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/constants/index.ts",
    "content": "export * from './constants'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/global.d.ts",
    "content": "declare module '@elastic/eui/es/components/icon/*'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/main.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { render } from 'react-dom'\nimport result from './result2.json'\n// import result from './resultInfo.json'\n// import result from './result3.json'\nimport App from './App'\nimport { ThemeProvider } from 'uiSrc/components/base/utils/pluginsThemeContext'\nimport './styles/styles.scss'\n\ninterface Props {\n  command?: string\n  data?: { response: any; status: string }[]\n}\n\nconst renderRediSearch = (props: Props) => {\n  const { command = '', data: result = [] } = props\n  render(\n    <ThemeProvider>\n      <App command={command} result={result} />\n    </ThemeProvider>,\n    document.getElementById('app'),\n  )\n}\n\nif (process.env.NODE_ENV === 'development') {\n  const command = 'ft.search idx'\n  // const command = 'ft.info idx'\n\n  renderRediSearch({ command, data: result })\n}\n\nexport default { renderRediSearch }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/result.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      \"index_name\",\n      \"idx\",\n      \"index_options\",\n      [],\n      \"index_definition\",\n      [\n        \"key_type\",\n        \"HASH\",\n        \"prefixes\",\n        [\"doc:\"],\n        \"language_field\",\n        \"__language\",\n        \"default_score\",\n        \"0.5\",\n        \"score_field\",\n        \"doc_score\",\n        \"payload_field\",\n        \"__payload\"\n      ],\n      \"fields\",\n      [\n        [\"title\", \"type\", \"TEXT\", \"WEIGHT\", \"1\"],\n        [\"body\", \"type\", \"TEXT\", \"WEIGHT\", \"1\"],\n        [\"url\", \"type\", \"TEXT\", \"WEIGHT\", \"1\"],\n        [\"visits\", \"type\", \"NUMERIC\"]\n      ],\n      \"num_docs\",\n      \"2\",\n      \"max_doc_id\",\n      \"2\",\n      \"num_terms\",\n      \"12\",\n      \"num_records\",\n      \"22\",\n      \"inverted_sz_mb\",\n      \"0.0001201629638671875\",\n      \"total_inverted_index_blocks\",\n      \"40\",\n      \"offset_vectors_sz_mb\",\n      \"2.288818359375e-05\",\n      \"doc_table_size_mb\",\n      \"0.0001659393310546875\",\n      \"sortable_values_size_mb\",\n      \"0\",\n      \"key_table_size_mb\",\n      \"8.296966552734375e-05\",\n      \"records_per_doc_avg\",\n      \"11\",\n      \"bytes_per_record_avg\",\n      \"5.7272725105285645\",\n      \"offsets_per_term_avg\",\n      \"1.0909091234207153\",\n      \"offset_bits_per_record_avg\",\n      \"8\",\n      \"hash_indexing_failures\",\n      \"0\",\n      \"indexing\",\n      \"0\",\n      \"percent_indexed\",\n      \"1\",\n      \"gc_stats\",\n      [\n        \"bytes_collected\",\n        \"0\",\n        \"total_ms_run\",\n        \"0\",\n        \"total_cycles\",\n        \"0\",\n        \"average_cycle_time_ms\",\n        \"-nan\",\n        \"last_run_time_ms\",\n        \"0\",\n        \"gc_numeric_trees_missed\",\n        \"0\",\n        \"gc_blocks_denied\",\n        \"0\"\n      ],\n      \"cursor_stats\",\n      [\n        \"global_idle\",\n        0,\n        \"global_total\",\n        0,\n        \"index_capacity\",\n        128,\n        \"index_total\",\n        0\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/result2.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      2,\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:1\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Primary and caching\",\n        \"url\",\n        \"<https://redislabs.com/primary-caching>\",\n        \"visits\",\n        \"108\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/result3.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      2,\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:1\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Primary and caching\",\n        \"url\",\n        \"<https://redislabs.com/primary-caching>\",\n        \"visits\",\n        \"108\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ],\n      \"doc:2\",\n      [\n        \"title\",\n        \"Redis\",\n        \"body\",\n        \"Modules\",\n        \"url\",\n        \"<https://redislabs.com/modules>\",\n        \"visits\",\n        \"102\"\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/resultInfo.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      \"index_name\",\n      \"idx2\",\n      \"index_options\",\n      [],\n      \"index_definition\",\n      [\n        \"key_type\",\n        \"HASH\",\n        \"prefixes\",\n        [\"\"],\n        \"language_field\",\n        \"__language\",\n        \"default_score\",\n        \"1\",\n        \"score_field\",\n        \"__score\",\n        \"payload_field\",\n        \"__payload\"\n      ],\n      \"fields\",\n      [\n        [\"name\", \"type\", \"TEXT\", \"WEIGHT\", \"1\", \"SORTABLE\"],\n        [\"docid\", \"type\", \"TAG\", \"SEPARATOR\", \",\", \"SORTABLE\", \"NOINDEX\"]\n      ],\n      \"num_docs\",\n      \"5136\",\n      \"max_doc_id\",\n      \"5157\",\n      \"num_terms\",\n      \"6\",\n      \"num_records\",\n      \"47\",\n      \"inverted_sz_mb\",\n      \"0.0002689361572265625\",\n      \"total_inverted_index_blocks\",\n      \"40\",\n      \"offset_vectors_sz_mb\",\n      \"4.482269287109375e-05\",\n      \"doc_table_size_mb\",\n      \"0.52691459655761719\",\n      \"sortable_values_size_mb\",\n      \"0.000782012939453125\",\n      \"key_table_size_mb\",\n      \"0.19832515716552734\",\n      \"records_per_doc_avg\",\n      \"0.0091510899364948273\",\n      \"bytes_per_record_avg\",\n      \"6\",\n      \"offsets_per_term_avg\",\n      \"1\",\n      \"offset_bits_per_record_avg\",\n      \"8\",\n      \"hash_indexing_failures\",\n      \"0\",\n      \"indexing\",\n      \"0\",\n      \"percent_indexed\",\n      \"1\",\n      \"gc_stats\",\n      [\n        \"bytes_collected\",\n        \"0\",\n        \"total_ms_run\",\n        \"0\",\n        \"total_cycles\",\n        \"0\",\n        \"average_cycle_time_ms\",\n        \"-nan\",\n        \"last_run_time_ms\",\n        \"0\",\n        \"gc_numeric_trees_missed\",\n        \"0\",\n        \"gc_blocks_denied\",\n        \"0\"\n      ],\n      \"cursor_stats\",\n      [\n        \"global_idle\",\n        0,\n        \"global_total\",\n        0,\n        \"index_capacity\",\n        128,\n        \"index_total\",\n        0\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/result_aggregate_array.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      [\n        50,\n        [\n          \"category\",\n          \"category0\",\n          \"total_quantit\",\n          \"9921\",\n          \"avg_price\",\n          \"528.235\"\n        ],\n        [\n          \"category\",\n          \"category1\",\n          \"total_quantit\",\n          \"10572\",\n          \"avg_price\",\n          \"536.8\"\n        ],\n        [\n          \"category\",\n          \"category10\",\n          \"total_quantit\",\n          \"9564\",\n          \"avg_price\",\n          \"486.76\"\n        ],\n        [\n          \"category\",\n          \"category11\",\n          \"total_quantit\",\n          \"10605\",\n          \"avg_price\",\n          \"494.415\"\n        ],\n        [\n          \"category\",\n          \"category12\",\n          \"total_quantit\",\n          \"9991\",\n          \"avg_price\",\n          \"506.77\"\n        ],\n        [\n          \"category\",\n          \"category13\",\n          \"total_quantit\",\n          \"9980\",\n          \"avg_price\",\n          \"479.96\"\n        ],\n        [\n          \"category\",\n          \"category14\",\n          \"total_quantit\",\n          \"9786\",\n          \"avg_price\",\n          \"514.145\"\n        ],\n        [\n          \"category\",\n          \"category15\",\n          \"total_quantit\",\n          \"10164\",\n          \"avg_price\",\n          \"510.15\"\n        ],\n        [\n          \"category\",\n          \"category16\",\n          \"total_quantit\",\n          \"10035\",\n          \"avg_price\",\n          \"494.715\"\n        ],\n        [\n          \"category\",\n          \"category17\",\n          \"total_quantit\",\n          \"10589\",\n          \"avg_price\",\n          \"507.645\"\n        ]\n      ],\n      940326616\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/styles/styles.scss",
    "content": "@use \"../../../../styles/mixins/eui\";\n\n* div,\n* span {\n  font-family: \"Graphik\", sans-serif !important;\n}\n\nhtml {\n  background-color: var(--euiPageBackgroundColor) !important;\n}\n\n.container {\n  @include eui.scrollBar;\n  flex: auto;\n  overflow: auto;\n  max-height: 810px;\n  white-space: pre-wrap;\n  word-break: break-all;\n  padding: 16px 20px;\n\n  font:\n    normal normal normal 13px/18px Graphik,\n    sans-serif;\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--textColorShade);\n\n  background-color: var(--euiPageBackgroundColor);\n\n  z-index: 10;\n}\n.responseFail {\n  display: inline-block;\n  padding: 16px 20px;\n  color: var(--euiColorColorDanger) !important;\n  font-size: 14px;\n  word-break: break-word;\n}\n\n.queryHeader {\n  display: flex;\n  align-items: center;\n\n  .matched {\n    margin-right: 12px;\n  }\n}\n\n.matched {\n  color: var(--euiColorFullShade);\n  padding-bottom: 12px;\n  font: normal normal 500 13px/19px;\n}\n\n.table,\n.tableInfo {\n  .euiFlexGroup--justifyContentSpaceBetween {\n    display: none;\n\n    // hide <PerPageComponent/>  option {hidePerPageOptions} doesn't work\n    // with dynamic changing prop \"pagination\" for In-memory table\n    .euiFlexItem:first-child {\n      display: none;\n    }\n  }\n\n  &.tableWithPagination {\n    .euiFlexGroup--justifyContentSpaceBetween {\n      display: flex;\n    }\n  }\n}\n\n.tableInfo {\n  padding: 16px 0;\n}\n\n.tooltipContainer {\n  max-width: 100%;\n}\n\n.tooltip {\n  max-width: 100%;\n\n  display: inline-block !important;\n}\n\n.cell {\n  position: relative;\n}\n\n.row {\n  display: block;\n  padding-bottom: 10px;\n  word-break: break-word;\n\n  &:last-of-type {\n    padding-bottom: 0;\n  }\n}\n\n.badge {\n  display: inline-flex !important;\n  position: relative;\n  font: normal normal normal 12px/15px;\n  margin: 0 5px;\n  top: -2px;\n}\n\n.icon {\n  position: relative;\n  margin: 0 auto;\n\n  @media only screen and (max-width: 767px) {\n    margin: 0;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/utils/formatLongName.ts",
    "content": "import { replaceSpaces } from './replaceSpaces'\n\nexport function formatLongName(\n  name = '',\n  maxNameLength = 500,\n  endPartLength = 50,\n  separator = '  ...  ',\n) {\n  // replace whitespace characters to no-break spaces - to prevent collapse spaces\n  const currentName = replaceSpaces(name)\n  if (currentName.length <= maxNameLength) {\n    return currentName\n  }\n  const startPart = currentName.substring(\n    0,\n    maxNameLength - endPartLength - separator.length,\n  )\n  const endPart = currentName.substring(currentName.length - endPartLength)\n  return `${startPart}${separator}${endPart}`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/utils/index.ts",
    "content": "export * from './parseResponse'\nexport * from './replaceSpaces'\nexport * from './formatLongName'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/utils/parseResponse.ts",
    "content": "import { chunk, fromPairs, isArray, isEmpty, last } from 'lodash'\nimport {\n  CommandArgument,\n  ResultFieldNameView,\n  ResultInfoField,\n  InfoAttributesBoolean,\n} from '../constants'\n\nconst parseSearchRawResponse = (command: string, initResult: any[]) => {\n  const chunkCount = getChunkCountSearch(command)\n\n  return chunk(initResult, chunkCount).map(([key, ...initValues]: string[]) => {\n    let values: any = last(initValues)\n    let fields: any = {}\n\n    if (isEmpty(initValues)) {\n      return { Doc: key }\n    }\n\n    if (command.includes(CommandArgument.WithScores)) {\n      const [score, scoreValues, others] = initValues\n      values = [\n        ResultFieldNameView.Score,\n        score,\n        ...(scoreValues || []),\n        ...(others || []),\n      ]\n\n      if (command.includes(CommandArgument.WithPayloads)) {\n        const [payload] = scoreValues || []\n        values = [ResultFieldNameView.Payloads, payload, ...values]\n      }\n    } else if (command.includes(CommandArgument.WithPayloads)) {\n      const [payload, payloadValues = [], ...other] = initValues\n      values = [\n        ResultFieldNameView.Payloads,\n        payload,\n        ...(payloadValues || []),\n        ...other,\n      ]\n    }\n\n    values = chunk(values, 2)\n    fields = fromPairs(values)\n\n    return { Doc: key, ...fields }\n  })\n}\n\nconst parseAggregateRawResponse = (initResult: any[]) => {\n  const result: any[] = initResult?.map((values: any) => {\n    if (isArray(values) && values?.length > 1) {\n      return { ...fromPairsChunk(values, 2) }\n    }\n    if (isArray(values)) {\n      return []\n    }\n\n    return values.toString()\n  })\n\n  return result\n}\n\nconst parseInfoRawResponse = (initResult: any[]) => {\n  const result: any[] = chunk(initResult, 2).map(([field, value]: any) => {\n    if (\n      isArray(value) &&\n      (field === ResultInfoField.Fields || field === ResultInfoField.Attributes)\n    ) {\n      const values =\n        field === ResultInfoField.Fields\n          ? value.map((field) => [\n              ResultFieldNameView.Name.toLowerCase(),\n              ...field,\n            ])\n          : value\n      return [\n        field,\n        values.map((attrs: any[]) => {\n          const newAttrs = attrs.reduce(\n            (prev, current) =>\n              InfoAttributesBoolean.indexOf(current) !== -1\n                ? [...prev, current, true]\n                : [...prev, current],\n            [],\n          )\n\n          return fromPairsChunk(newAttrs, 2)\n        }),\n      ]\n    }\n    if (isArray(value) && field !== ResultInfoField.Options) {\n      return [field, fromPairsChunk(value, 2)]\n    }\n\n    return [field, value]\n  })\n\n  return fromPairs(result)\n}\n\nconst fromPairsChunk = (arr: any[] = [], count: number = 2) =>\n  fromPairs(chunk(arr, count))\n\nconst getChunkCountSearch = (command: string = '') => {\n  let count = 2\n  const onlyKeysChunkCount = 1\n  const specialArgs = [\n    CommandArgument.WithSortKeys,\n    CommandArgument.WithScores,\n    CommandArgument.WithPayloads,\n  ]\n\n  if (getIsKeysOnly(command)) count = onlyKeysChunkCount\n\n  specialArgs.forEach((arg) => command.toUpperCase().includes(arg) && ++count)\n\n  return count\n}\n\nconst getIsKeysOnly = (command: string = '') =>\n  command.toUpperCase().includes(CommandArgument.NoContent) ||\n  command.toUpperCase().includes(`${CommandArgument.Return} 0`)\n\nexport {\n  parseInfoRawResponse,\n  parseSearchRawResponse,\n  parseAggregateRawResponse,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/utils/replaceSpaces.ts",
    "content": "const replaceSpaces = (text = '') => {\n  if (text === ' ') {\n    return '\\u00a0'\n  }\n  return text.replace(/\\s\\s/g, '\\u00a0\\u00a0')\n}\n\nexport { replaceSpaces }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/utils/tests/formatLongName.spec.ts",
    "content": "import { formatLongName, formatNameShort } from 'uiSrc/utils'\n\nconst formatLongNameTests: any[] = [\n  ['11111111111112', 7, 1, '...', '111...2'],\n  ['uaoe uaoeu aoeuaoeua', 7, 1, ' ... ', 'u ... a'],\n  ['u aoeu aoeuaoeuaoeuaoe eoua uoaeu aoeu', 10, 3, ';', 'u aoeu;oeu'],\n]\n\ndescribe('formatLongName', () => {\n  it.each(formatLongNameTests)(\n    'for input: %s (name), %s (maxNameLength), %s (endPartLength), %s (separator), should be output: %s',\n    (name, maxNameLength, endPartLength, separator, expected) => {\n      const result = formatLongName(\n        name,\n        maxNameLength,\n        endPartLength,\n        separator,\n      )\n      expect(result).toBe(expected)\n    },\n  )\n})\n\nconst formatNameShortTests: any[] = [\n  ['11111111111112', '11111111111112'],\n  ['uaoe uaoeu aoeuaoeua', 'uaoe uaoeu aoeuaoeua'],\n  [\n    'test test test test test test test test test test test test test test test test test test test ',\n    'test test test test test test test test test test ...test test test ',\n  ],\n]\n\ndescribe('formatNameShort', () => {\n  it.each(formatNameShortTests)(\n    'for input: %s (name), should be output: %s',\n    (name, expected) => {\n      const result = formatNameShort(name)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisearch/src/utils/tests/parseResponse.spec.ts",
    "content": "import { parseSearchRawResponse, parseAggregateRawResponse } from '..'\n\nconst resultFTSearch: any[] = [\n  'red:2',\n  [\n    'title',\n    'Redis Labs',\n    'body',\n    'Primary and caching',\n    'url',\n    '<https://redis.com/primary-caching>',\n    'visits',\n    '108',\n  ],\n  'red:1',\n  [\n    'title',\n    'Redis Labs',\n    'body',\n    'Primary and caching',\n    'url',\n    '<https://redis.com/primary-caching>',\n    'visits',\n    '108',\n  ],\n]\n\nconst resultFTSearchNoContent: any[] = [\n  'red:2',\n  'red:3',\n  'red:4',\n  'red:5',\n  'red:6',\n]\n\ndescribe('parseSearchRawResponse', () => {\n  it('command \"get\" should return result is not modified 1', () => {\n    const command = 'get'\n    const result: any[] = []\n\n    expect(parseSearchRawResponse(command, result)).toEqual(result)\n  })\n\n  it('command \"get\" should return result is not modified 2', () => {\n    const command = 'get'\n    const result: any = []\n\n    expect(parseSearchRawResponse(command, result)).toEqual(result)\n  })\n\n  it('command \"ft.search\" should return array with parsed object', () => {\n    const command = 'ft.search'\n    const parsedResultFTSearch = [\n      {\n        Doc: 'red:2',\n        body: 'Primary and caching',\n        title: 'Redis Labs',\n        url: '<https://redis.com/primary-caching>',\n        visits: '108',\n      },\n      {\n        Doc: 'red:1',\n        body: 'Primary and caching',\n        title: 'Redis Labs',\n        url: '<https://redis.com/primary-caching>',\n        visits: '108',\n      },\n    ]\n\n    expect(parseSearchRawResponse(command, resultFTSearch)).toEqual(\n      parsedResultFTSearch,\n    )\n  })\n\n  it('command \"ft.search\" with attr NOCONTENT should return array of doc names', () => {\n    const command = 'ft.search NOCONTENT'\n    const parsedResultFTSearch = [\n      {\n        Doc: 'red:2',\n      },\n      {\n        Doc: 'red:3',\n      },\n      {\n        Doc: 'red:4',\n      },\n      {\n        Doc: 'red:5',\n      },\n      {\n        Doc: 'red:6',\n      },\n    ]\n\n    expect(parseSearchRawResponse(command, resultFTSearchNoContent)).toEqual(\n      parsedResultFTSearch,\n    )\n  })\n})\n\ndescribe('parseAggregateRawResponse', () => {\n  it('command \"ft.aggregate\" should return array of array with objects count of docs ', () => {\n    const resultFTAggregate = [[], [], [], [], []]\n\n    expect(parseAggregateRawResponse(resultFTAggregate)).toEqual(\n      resultFTAggregate,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/.gitignore",
    "content": "node_modules/\ndist/\n.parcel-cache"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/README.md",
    "content": "# RedisGraph Plugin for Redis Insight v2\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nyarn\nyarn start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nyarn build:statics - for Linux or MacOs\nyarn build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nyarn\nyarn build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>RedisGraph plugin</title>\n\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n    <!-- Run Conditions-->\n    <% if(isDev){ %>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/global_styles.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/dark_theme.css\"\n    />\n    <!-- <link rel=\"stylesheet\" href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/light_theme.css\" /> -->\n    <% } %>\n  </head>\n  <body class=\"theme_DARK\">\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"Show graph Visualization/table\",\n  \"source\": \"./src/main.tsx\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"graph-plugin\",\n  \"version\": \"0.0.2\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\"\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"graph-viz\",\n      \"name\": \"Graph\",\n      \"activationMethod\": \"renderGraph\",\n      \"matchCommands\": [\n        \"GRAPH.RO_QUERY\",\n        \"GRAPH.QUERY\"\n      ],\n      \"description\": \"Example of graph plugin\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {\n    \"vite\": \"file:../node_modules/vite\"\n  },\n  \"dependencies\": {\n    \"@elastic/eui\": \"34.6.0\",\n    \"@emotion/react\": \"^11.7.1\",\n    \"classnames\": \"^2.3.1\",\n    \"d3\": \"^7.3.0\",\n    \"prop-types\": \"^15.8.1\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-json-tree\": \"^0.16.1\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\"\n  },\n  \"resolutions\": {\n    \"trim\": \"0.0.3\",\n    \"@elastic/eui/**/prismjs\": \"~1.30.0\",\n    \"**/semver\": \"^7.5.2\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/App.tsx",
    "content": "import React from 'react'\nimport { JSONTree } from 'react-json-tree'\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nimport { ResultsParser } from './parser'\nimport Graph from './Graph'\nimport { COMPACT_FLAG } from './constants'\n\nconst isDarkTheme = document.body.classList.contains('theme_DARK')\n\nconst json_tree_theme = {\n  scheme: 'solarized',\n  author: 'ethan schoonover (http://ethanschoonover.com/solarized)',\n  base00: '#002b36',\n  base01: '#073642',\n  base02: '#586e75',\n  base03: '#657b83',\n  base04: '#839496',\n  base05: '#93a1a1',\n  base06: '#eee8d5',\n  base07: '#fdf6e3',\n  base08: '#dc322f',\n  base09: '#098658',\n  base0A: '#b58900',\n  base0B: '#A31515',\n  base0C: '#2aa198',\n  base0D: '#0451A5',\n  base0E: '#6c71c4',\n  base0F: '#d33682',\n}\n\nexport function TableApp(props: { command?: string; data: any }) {\n  const ErrorResponse = HandleError(props)\n\n  if (ErrorResponse !== null) return ErrorResponse\n\n  const tableData = ResultsParser(props.data[0].response as any)\n\n  return (\n    <div className=\"table-view\">\n      <Table\n        data={tableData.results}\n        columns={tableData.headers.map((h) => ({\n          id: h,\n          header: h,\n          accessorKey: h,\n          cell: ({ row: { original: d } }) => (\n            <JSONTree\n              invertTheme={isDarkTheme}\n              theme={{\n                extend: json_tree_theme,\n                tree: ({ style }) => ({\n                  style: { ...style, backgroundColor: undefined }, // removing default background color from styles\n                }),\n              }}\n              labelRenderer={(key) => key || null}\n              hideRoot\n              data={d}\n            />\n          ),\n        }))}\n      />\n    </div>\n  )\n}\n\nexport function GraphApp(props: { command?: string; data: any }) {\n  const { data, command = '' } = props\n  const ErrorResponse = HandleError(props)\n\n  if (ErrorResponse !== null) return ErrorResponse\n\n  return (\n    <div style={{ height: '100%' }}>\n      <Graph\n        graphKey={command.split(' ')[1]}\n        data={data[0].response}\n        command={command}\n      />\n    </div>\n  )\n}\n\nfunction HandleError(props: { command?: string; data: any }): JSX.Element {\n  const { data: [{ response = '', status = '' } = {}] = [] } = props\n\n  if (status === 'fail') {\n    return <div className=\"responseFail\">{JSON.stringify(response)}</div>\n  }\n\n  if (status === 'success' && typeof response === 'string') {\n    return <div className=\"responseFail\">{JSON.stringify(response)}</div>\n  }\n\n  const command = props.command.split(' ')\n\n  if (command[command.length - 1] === COMPACT_FLAG) {\n    return (\n      <div className=\"responseFail\">\n        '{COMPACT_FLAG}' flag is currently not supported.\n      </div>\n    )\n  }\n\n  return null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/Graph.tsx",
    "content": "import React, { useEffect, useRef, useState, useMemo } from 'react'\nimport * as d3 from 'd3'\nimport { executeRedisCommand, formatRedisReply } from 'redisinsight-plugin-sdk'\nimport Graphd3, { IGraphD3 } from './graphd3'\nimport { responseParser } from './parser'\nimport {\n  IGoodColor,\n  GoodColorPicker,\n  getFetchNodesByIdQuery,\n  getFetchDirectNeighboursOfNodeQuery,\n  getFetchNodeRelationshipsQuery,\n  getFetchEdgesByIdQuery,\n  getFetchNodesByEdgeIdQuery,\n  commandIsSuccess,\n} from './utils'\nimport {\n  EDGE_COLORS,\n  EDGE_COLORS_DARK,\n  NODE_COLORS,\n  NODE_COLORS_DARK,\n} from './constants'\nimport { IconButton } from '../../../components/base/forms/buttons'\nimport { CancelSlimIcon } from '../../../components/base/icons'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip/RITooltip'\nimport { TOOLTIP_DELAY_LONG } from 'uiSrc/constants/durationUnits'\n\nenum EntityType {\n  Node = 'Node',\n  Edge = 'Edge',\n}\n\ninterface ISelectedEntityProps {\n  property: string\n  color: string\n  backgroundColor: string\n  props: { [key: string]: string | number | object }\n  type: EntityType\n}\n\nconst isDarkTheme = document.body.classList.contains('theme_DARK')\n\nconst colorPicker = (COLORS: IGoodColor[]) => {\n  const color = new GoodColorPicker(COLORS)\n  return (label: string) => color.getColor(label)\n}\n\nconst labelColors = colorPicker(isDarkTheme ? NODE_COLORS_DARK : NODE_COLORS)\nconst edgeColors = colorPicker(isDarkTheme ? EDGE_COLORS_DARK : EDGE_COLORS)\nexport default function Graph(props: {\n  graphKey: string\n  data: any[]\n  command: string\n}) {\n  const d3Container = useRef<HTMLDivElement>()\n  const [container, setContainer] = useState<IGraphD3>(null)\n  const [selectedEntity, setSelectedEntity] =\n    useState<ISelectedEntityProps | null>(null)\n  const [start, setStart] = useState<boolean>(false)\n  const [showAutomaticEdges, setShowAutomaticEdges] = useState(false)\n  const [parsedRedisReply, setParsedRedisReply] = useState('')\n\n  const parsedResponse = responseParser(props.data)\n  const nodeIds = new Set(parsedResponse.nodes.map((n) => n.id))\n  const edgeIds = new Set(parsedResponse.edges.map((e) => e.id))\n  const isNoDataToVisualize =\n    nodeIds.size === 0 &&\n    parsedResponse.nodeIds.size === 0 &&\n    parsedResponse.danglingEdgeIds.size === 0\n\n  useEffect(() => {\n    if (isNoDataToVisualize) {\n      const getParsedResponse = async () => {\n        const formattedResponse = await formatRedisReply(\n          props.data,\n          props.command,\n        )\n        setParsedRedisReply(formattedResponse)\n      }\n      getParsedResponse()\n    }\n  }, [])\n\n  // TODO: refactor to do not call hooks conditionally\n  if (isNoDataToVisualize) {\n    return (\n      <>\n        <div className=\"responseInfo\">\n          No data to visualize. Raw information is presented below.\n        </div>\n        <div className=\"parsedRedisReply\">{parsedRedisReply}</div>\n      </>\n    )\n  }\n\n  const data = {\n    results: [\n      {\n        columns: parsedResponse.headers,\n        data: [\n          {\n            graph: {\n              nodes: parsedResponse.nodes,\n              relationships: parsedResponse.edges\n                /* If edge is present in dangling edges, add them since we are gonna retrive them from the node by dangling edges query */\n                .filter(\n                  (e) =>\n                    (nodeIds.has(e.source) && nodeIds.has(e.target)) ||\n                    parsedResponse.danglingEdgeIds.has(e.id),\n                )\n                .map((e) => ({ ...e, startNode: e.source, endNode: e.target })),\n            },\n          },\n        ],\n      },\n    ],\n    errors: [],\n  }\n\n  const [nodeLabels, setNodeLabels] = useState(parsedResponse.labels)\n  const [edgeTypes, setEdgeTypes] = useState(parsedResponse.types)\n\n  const [graphData, setGraphData] = useState(data)\n\n  useMemo(async () => {\n    let newGraphData = graphData\n    const newNodeLabels: { [key: string]: number } = nodeLabels\n    const newEdgeTypes: { [key: string]: number } = edgeTypes\n\n    if (parsedResponse.danglingEdgeIds.size > 0) {\n      /*\n       * Fetch dangling edges\n       */\n      try {\n        const resp = await executeRedisCommand(\n          getFetchNodesByEdgeIdQuery(\n            props.graphKey,\n            [...parsedResponse.danglingEdgeIds],\n            [...nodeIds],\n          ),\n        )\n\n        if (commandIsSuccess(resp)) {\n          const parsedData = responseParser(resp[0].response)\n          parsedData.nodes.forEach((n) => {\n            nodeIds.add(n.id)\n            n.labels.forEach(\n              (l) => (newNodeLabels[l] = newNodeLabels[l] + 1 || 1),\n            )\n          })\n\n          /* Since its obvious from the query that only nodes will be\n          returned, so putting empty array for relationships field. */\n          newGraphData = {\n            ...newGraphData,\n            results: [\n              ...newGraphData.results,\n              {\n                columns: parsedData.headers,\n                data: [\n                  {\n                    graph: {\n                      nodes: parsedData.nodes,\n                      relationships: [],\n                    },\n                  },\n                ],\n              },\n            ],\n          }\n        }\n      } catch {}\n    }\n\n    if (\n      parsedResponse.hasNamedPathItem &&\n      parsedResponse.npNodeIds.length > 0\n    ) {\n      try {\n        /* Fetch named path nodes */\n        let resp = await executeRedisCommand(\n          getFetchNodesByIdQuery(props.graphKey, [...parsedResponse.npNodeIds]),\n        )\n        if (commandIsSuccess(resp)) {\n          const parsedData = responseParser(resp[0].response)\n          parsedData.nodes.forEach((n) => {\n            nodeIds.add(n.id)\n            n.labels.forEach(\n              (l) => (newNodeLabels[l] = newNodeLabels[l] + 1 || 1),\n            )\n          })\n\n          if (parsedResponse.npEdgeIds.length > 0) {\n            resp = await executeRedisCommand(\n              getFetchEdgesByIdQuery(props.graphKey, [\n                ...parsedResponse.npEdgeIds,\n              ]),\n            )\n            if (commandIsSuccess(resp)) {\n              const edgeParsedData = responseParser(resp[0].response)\n              parsedData.edges = [...edgeParsedData.edges]\n              parsedData.edgeIds = edgeParsedData.edgeIds\n            }\n          }\n\n          parsedData.edges = parsedData.edges.filter((e) => !edgeIds.has(e.id))\n          parsedData.edges.forEach((e) => {\n            edgeIds.add(e.id)\n            newEdgeTypes[e.type] = newEdgeTypes[e.type] + 1 || 1\n          })\n\n          newGraphData = {\n            ...newGraphData,\n            results: [\n              ...newGraphData.results,\n              {\n                columns: parsedData.headers,\n                data: [\n                  {\n                    graph: {\n                      nodes: parsedData.nodes,\n                      relationships: parsedData.edges\n                        .filter(\n                          (e) => nodeIds.has(e.source) && nodeIds.has(e.target),\n                        )\n                        .map((e) => ({\n                          ...e,\n                          startNode: e.source,\n                          endNode: e.target,\n                        })),\n                    },\n                  },\n                ],\n              },\n            ],\n          }\n        }\n      } catch {}\n    }\n\n    try {\n      /* Fetch neighbours automatically */\n      if (nodeIds.size > 0) {\n        const resp = await executeRedisCommand(\n          getFetchNodeRelationshipsQuery(\n            props.graphKey,\n            [...nodeIds],\n            [...nodeIds],\n            [...edgeIds],\n          ),\n        )\n\n        if (commandIsSuccess(resp)) {\n          const parsedData = responseParser(resp[0].response)\n\n          /* This block is not needed since only edges are retrieved. */\n          parsedData.nodes.forEach((n) => {\n            nodeIds.add(n.id)\n            n.labels.forEach(\n              (l) => (newNodeLabels[l] = newNodeLabels[l] + 1 || 1),\n            )\n          })\n\n          const filteredEdges = parsedData.edges\n            .filter((e) => !edgeIds.has(e.id))\n            .filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target))\n            .map((e) => {\n              edgeIds.add(e.id)\n              newEdgeTypes[e.type] = newEdgeTypes[e.type] + 1 || 1\n              return {\n                ...e,\n                startNode: e.source,\n                endNode: e.target,\n                fetchedAutomatically: true,\n              }\n            })\n\n          setGraphData({\n            ...newGraphData,\n            results: [\n              ...newGraphData.results,\n              {\n                columns: parsedData.headers,\n                data: [\n                  {\n                    graph: {\n                      nodes: parsedData.nodes,\n                      relationships: filteredEdges,\n                    },\n                  },\n                ],\n              },\n            ],\n          })\n        }\n      }\n    } catch {}\n\n    setNodeLabels(newNodeLabels)\n    setEdgeTypes(newEdgeTypes)\n\n    setStart(true)\n  }, [])\n\n  const zoom = d3.zoom().scaleExtent([0, 3]) /* min, mac of zoom */\n  useEffect(() => {\n    if (container != null) return\n    if (!start) return\n\n    const graphd3 = Graphd3(d3Container.current, {\n      labelColors,\n      edgeColors,\n      highlight: [],\n      graphZoom: zoom,\n      minCollision: 60,\n      graphData,\n      infoPanel: true,\n      // nodeRadius: 25,\n      onLabelNode: (node) =>\n        node.properties?.name ||\n        node.properties?.title ||\n        node.id.toString() ||\n        (node.labels ? node.labels[0] : ''),\n      onNodeClick: (nodeSvg, node, event) => {\n        if (d3.select(nodeSvg).attr('class').indexOf('selected') > 0) {\n          d3.select(nodeSvg).attr('class', 'node')\n        }\n      },\n      async onNodeDoubleClick(nodeSvg, node) {\n        /* Get direct neighbours automatically */\n        const data = await executeRedisCommand(\n          getFetchDirectNeighboursOfNodeQuery(props.graphKey, node.id),\n        )\n        if (!commandIsSuccess(data)) return\n        const parsedData = responseParser(data[0].response)\n\n        const newNodeLabels = nodeLabels\n        const newEdgeTypes = edgeTypes\n\n        parsedData.nodes.forEach((n) => {\n          nodeIds.add(n.id)\n          n.labels.forEach(\n            (l) => (newNodeLabels[l] = newNodeLabels[l] + 1 || 1),\n          )\n        })\n        const filteredEdges = parsedData.edges\n          .filter((e) => !edgeIds.has(e.id))\n          .map((e) => ({\n            ...e,\n            startNode: e.source,\n            endNode: e.target,\n          }))\n        filteredEdges.forEach((e) => {\n          edgeIds.add(e.id)\n          newEdgeTypes[e.type] = newEdgeTypes[e.type] + 1 || 1\n        })\n\n        graphd3.updateWithGraphData({\n          results: [\n            {\n              columns: parsedData.headers,\n              data: [\n                {\n                  graph: {\n                    nodes: parsedData.nodes,\n                    relationships: filteredEdges,\n                  },\n                },\n              ],\n            },\n          ],\n          errors: [],\n        })\n\n        setNodeLabels(newNodeLabels)\n        setEdgeTypes(newEdgeTypes)\n      },\n      onRelationshipDoubleClick(relationship) {},\n      onDisplayInfo: (infoSvg, entity) => {\n        let property: string\n        let entityColor: IGoodColor\n        let t: EntityType\n\n        if (entity.labels) {\n          ;[property] = entity.labels\n          entityColor = labelColors(property)\n          t = EntityType.Node\n        } else {\n          property = entity.type\n          entityColor = edgeColors(property)\n          t = EntityType.Edge\n        }\n\n        setSelectedEntity({\n          property,\n          type: t,\n          backgroundColor: entityColor.color,\n          props: { '<id>': entity.id, ...entity.properties },\n          color: entityColor.textColor,\n        })\n      },\n      zoomFit: false,\n    })\n\n    setContainer(graphd3)\n  }, [start])\n\n  return (\n    <div className=\"core-container\" data-testid=\"query-graph-container\">\n      <div className=\"automatic-edges-switch\">\n        <RiTooltip\n          position=\"bottom\"\n          delay={TOOLTIP_DELAY_LONG}\n          content=\"Toggle visibility of automatically fetched relationships\"\n        >\n          <SwitchInput\n            title=\"All relationships\"\n            checked={showAutomaticEdges}\n            onCheckedChange={() => {\n              container.toggleShowAutomaticEdges()\n              setShowAutomaticEdges(!showAutomaticEdges)\n            }}\n          />\n        </RiTooltip>\n      </div>\n      <div className=\"d3-info\">\n        <div className=\"graph-legends\">\n          {Object.keys(nodeLabels).length > 0 && (\n            <div className=\"d3-info-labels\">\n              {Object.keys(nodeLabels).map((item, i) => (\n                <div\n                  className=\"box-node-label\"\n                  style={{\n                    backgroundColor: labelColors(item).color,\n                    color: labelColors(item).textColor,\n                  }}\n                  key={item + i}\n                >\n                  {item}\n                </div>\n              ))}\n            </div>\n          )}\n          {Object.keys(edgeTypes).length > 0 && (\n            <div className=\"d3-info-labels\">\n              {Object.keys(edgeTypes).map((item, i) => (\n                <div\n                  key={item + i.toString()}\n                  className=\"box-edge-type\"\n                  style={{\n                    borderColor: edgeColors(item).color,\n                    color: edgeColors(item).color,\n                  }}\n                >\n                  {item}\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n        {selectedEntity && (\n          <div className=\"info-component\">\n            <div className=\"info-header\">\n              {selectedEntity.type === EntityType.Node ? (\n                <div\n                  className=\"box-node-label\"\n                  style={{\n                    backgroundColor: selectedEntity.backgroundColor,\n                    color: selectedEntity.color,\n                  }}\n                >\n                  {selectedEntity.property}\n                </div>\n              ) : (\n                <div\n                  className=\"box-edge-type\"\n                  style={{\n                    borderColor: selectedEntity.backgroundColor,\n                    color: selectedEntity.backgroundColor,\n                  }}\n                >\n                  {selectedEntity.property}\n                </div>\n              )}\n              <IconButton\n                onClick={() => setSelectedEntity(null)}\n                icon={CancelSlimIcon}\n                aria-label=\"Close\"\n              />\n            </div>\n            <div className=\"info-props\">\n              {Object.keys(selectedEntity.props).map((k) => (\n                <>\n                  <div>{k}</div>\n                  <div>{JSON.stringify(selectedEntity.props[k])}</div>\n                </>\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n      <div ref={d3Container} id=\"graphd3\" />\n      <div\n        style={{\n          position: 'absolute',\n          bottom: '110px',\n          right: '10px',\n          borderRadius: '4px',\n          boxShadow: '0 1px 6px rgb(0 0 0 / 16%), 0 1px 6px rgb(0 0 0 / 23%)',\n          display: 'flex',\n          flexDirection: 'column',\n        }}\n      >\n        {[\n          {\n            name: 'Zoom In',\n            onClick: () => container.zoomFuncs.zoomIn(),\n            icon: 'magnifyWithPlus',\n          },\n          {\n            name: 'Zoom Out',\n            onClick: () => container.zoomFuncs.zoomOut(),\n            icon: 'magnifyWithMinus',\n          },\n          {\n            name: 'Reset Zoom',\n            onClick: () => container.zoomFuncs.resetZoom(),\n            icon: 'bullseye',\n          },\n          {\n            name: 'Center',\n            onClick: () => container.zoomFuncs.center(),\n            icon: 'editorItemAlignCenter',\n          },\n        ].map((item) => (\n          <RiTooltip position=\"left\" content={item.name}>\n            <IconButton\n              onClick={item.onClick}\n              icon={item.icon}\n              aria-label={item.name}\n            />\n          </RiTooltip>\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/constants.ts",
    "content": "export const NODE_STROKE_WIDTH = 1.5\nexport const NODE_RADIUS = 25\n\nexport const EDGE_STROKE = 1.8\n\nexport const ZOOM_PROPS = {\n  ZOOM_IN: 2,\n  ZOOM_OUT: 0.5,\n  ZOOM_RESET: 1,\n  CAMERA_CENTER: (width: number, height: number) => [0.5 * width, 0.5 * height],\n  CAMERA_LEFT: -50,\n  CAMERA_RIGHT: 50,\n}\n\nexport const COMPACT_FLAG = '--compact'\n\nexport const EDGE_CAPTION_EXTERNAL = 'external'\n\nexport const NODE_COLORS_DARK = [\n  { color: '#6A1DC3', borderColor: '#6A1DC3', textColor: '#FFFFFF' },\n  { color: '#364CFF', borderColor: '#364CFF', textColor: '#FFFFFF' },\n  { color: '#008556', borderColor: '#008556', textColor: '#FFFFFF' },\n  { color: '#333D4F', borderColor: '#333D4F', textColor: '#FFFFFF' },\n  { color: '#9C5C2B', borderColor: '#9C5C2B', textColor: '#FFFFFF' },\n  { color: '#A00A6B', borderColor: '#A00A6B', textColor: '#FFFFFF' },\n  { color: '#6F7C07', borderColor: '#6F7C07', textColor: '#FFFFFF' },\n  { color: '#14708D', borderColor: '#14708D', textColor: '#FFFFFF' },\n  { color: '#AA4E4E', borderColor: '#AA4E4E', textColor: '#FFFFFF' },\n  { color: '#6E6E6E', borderColor: '#6E6E6E', textColor: '#FFFFFF' },\n]\n\nexport const EDGE_COLORS_DARK = [\n  { color: '#C7C7C7', borderColor: '#C7C7C7', textColor: '#FFFFFF' },\n  { color: '#E3AAAA', borderColor: '#E3AAAA', textColor: '#FFFFFF' },\n  { color: '#ACCCD7', borderColor: '#ACCCD7', textColor: '#FFFFFF' },\n  { color: '#C7CEA8', borderColor: '#C7CEA8', textColor: '#FFFFFF' },\n  { color: '#D9A0C6', borderColor: '#D9A0C6', textColor: '#FFFFFF' },\n  { color: '#D4BAA7', borderColor: '#D4BAA7', textColor: '#FFFFFF' },\n  { color: '#B8C5DB', borderColor: '#B8C5DB', textColor: '#FFFFFF' },\n  { color: '#A5D4C3', borderColor: '#A5D4C3', textColor: '#FFFFFF' },\n  { color: '#CDDDF8', borderColor: '#CDDDF8', textColor: '#FFFFFF' },\n  { color: '#C7B0EA', borderColor: '#C7B0EA', textColor: '#FFFFFF' },\n]\n\nexport const NODE_COLORS = [\n  { color: '#C7B0EA', borderColor: '#C7B0EA', textColor: '#000000' },\n  { color: '#CDDDF8', borderColor: '#CDDDF8', textColor: '#000000' },\n  { color: '#A5D4C3', borderColor: '#A5D4C3', textColor: '#000000' },\n  { color: '#B8C5DB', borderColor: '#B8C5DB', textColor: '#000000' },\n  { color: '#D4BAA7', borderColor: '#D4BAA7', textColor: '#000000' },\n  { color: '#D9A0C6', borderColor: '#D9A0C6', textColor: '#000000' },\n  { color: '#C7CEA8', borderColor: '#C7CEA8', textColor: '#000000' },\n  { color: '#ACCCD7', borderColor: '#ACCCD7', textColor: '#000000' },\n  { color: '#E3AAAA', borderColor: '#E3AAAA', textColor: '#000000' },\n  { color: '#C7C7C7', borderColor: '#C7C7C7', textColor: '#000000' },\n]\n\nexport const EDGE_COLORS = [\n  { color: '#6E6E6E', borderColor: '#6E6E6E', textColor: '#000000' },\n  { color: '#A85050', borderColor: '#A85050', textColor: '#000000' },\n  { color: '#1D6F8A', borderColor: '#1D6F8A', textColor: '#000000' },\n  { color: '#6F7B23', borderColor: '#6F7B23', textColor: '#000000' },\n  { color: '#9E1669', borderColor: '#9E1669', textColor: '#000000' },\n  { color: '#9A5D34', borderColor: '#9A5D34', textColor: '#000000' },\n  { color: '#363F4F', borderColor: '#363F4F', textColor: '#000000' },\n  { color: '#0F8459', borderColor: '#0F8459', textColor: '#000000' },\n  { color: '#384EF9', borderColor: '#384EF9', textColor: '#000000' },\n  { color: '#6924BD', borderColor: '#6924BD', textColor: '#000000' },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/global.d.ts",
    "content": "declare module '@elastic/eui/es/components/icon/*'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/graphd3.ts",
    "content": "import * as d3 from 'd3'\nimport * as Utils from './utils'\n\nimport {\n  NODE_STROKE_WIDTH,\n  NODE_RADIUS,\n  ZOOM_PROPS,\n  EDGE_CAPTION_EXTERNAL,\n} from './constants'\n\nconst DEFAULT_OPTIONS = {\n  arrowSize: 5,\n  highlight: undefined,\n  minCollision: undefined,\n  graphData: undefined,\n  nodeOutlineFillColor: undefined,\n  nodeRadius: NODE_RADIUS,\n  relationshipColor: '#a5abb6',\n  zoomFit: false,\n  labelProperty: 'name',\n  infoPanel: false,\n  relationshipWidth: 1.5,\n}\n\nconst COLORS = [\n  '#68bdf6', // light blue\n  '#6dce9e', // green #1\n  '#faafc2', // light pink\n  '#f2baf6', // purple\n  '#ff928c', // light red\n  '#fcea7e', // light yellow\n  '#ffc766', // light orange\n  '#405f9e', // navy blue\n  '#a5abb6', // dark gray\n  '#78cecb', // green #2,\n  '#b88cbb', // dark purple\n  '#ced2d9', // light gray\n  '#e84646', // dark red\n  '#fa5f86', // dark pink\n  '#ffab1a', // dark orange\n  '#fcda19', // dark yellow\n  '#797b80', // black\n  '#c9d96f', // pistacchio\n  '#47991f', // green #3\n  '#70edee', // turquoise\n  '#ff75ea', // pink\n]\n\nclass Point {\n  x: number\n  y: number\n\n  constructor(_x: number, _y: number) {\n    this.x = _x\n    this.y = _y\n  }\n\n  toString(): string {\n    return `${this.x} ${this.y}`\n  }\n}\n\nfunction LoopArrow(\n  nodeRadius: number,\n  straightLength: number,\n  spreadDegrees: number,\n  shaftWidth: number,\n  headWidth: number,\n  headLength: number,\n  captionHeight: number,\n) {\n  this.outline\n  this.overlay, this.shaftLength\n  this.midShaftPoint\n\n  const spread = (spreadDegrees * Math.PI) / 180\n  const r1 = nodeRadius\n  const r2 = nodeRadius + headLength\n  const r3 = nodeRadius + straightLength\n  const loopRadius = r3 * Math.tan(spread / 2)\n  const shaftRadius = shaftWidth / 2\n  this.shaftLength = loopRadius * 3 + shaftWidth\n\n  const normalPoint = function (\n    sweep: number,\n    radius: number,\n    displacement: number,\n  ) {\n    const localLoopRadius = radius * Math.tan(spread / 2)\n    const cy = radius / Math.cos(spread / 2)\n    return new Point(\n      (localLoopRadius + displacement) * Math.sin(sweep),\n      cy + (localLoopRadius + displacement) * Math.cos(sweep),\n    )\n  }\n\n  this.midShaftPoint = normalPoint(0, r3, shaftRadius + captionHeight / 2 + 2)\n  const startPoint = (radius: number, displacement: number) =>\n    normalPoint((Math.PI + spread) / 2, radius, displacement)\n  const endPoint = (radius: number, displacement: number) =>\n    normalPoint(-(Math.PI + spread) / 2, radius, displacement)\n\n  this.outline = function () {\n    const inner = loopRadius - shaftRadius\n    const outer = loopRadius + shaftRadius\n    return [\n      'M',\n      startPoint(r1, shaftRadius),\n      'L',\n      startPoint(r3, shaftRadius),\n      'A',\n      outer,\n      outer,\n      0,\n      1,\n      1,\n      endPoint(r3, shaftRadius),\n      'L',\n      endPoint(r2, shaftRadius),\n      'L',\n      endPoint(r2, -headWidth / 2),\n      'L',\n      endPoint(r1, 0),\n      'L',\n      endPoint(r2, headWidth / 2),\n      'L',\n      endPoint(r2, -shaftRadius),\n      'L',\n      endPoint(r3, -shaftRadius),\n      'A',\n      inner,\n      inner,\n      0,\n      1,\n      0,\n      startPoint(r3, -shaftRadius),\n      'L',\n      startPoint(r1, -shaftRadius),\n      'Z',\n    ].join(' ')\n  }\n\n  this.overlay = function (minWidth: number) {\n    const displacement = Math.max(minWidth / 2, shaftRadius)\n    const inner = loopRadius - displacement\n    const outer = loopRadius + displacement\n    return [\n      'M',\n      startPoint(r1, displacement),\n      'L',\n      startPoint(r3, displacement),\n      'A',\n      outer,\n      outer,\n      0,\n      1,\n      1,\n      endPoint(r3, displacement),\n      'L',\n      endPoint(r2, displacement),\n      'L',\n      endPoint(r2, -displacement),\n      'L',\n      endPoint(r3, -displacement),\n      'A',\n      inner,\n      inner,\n      0,\n      1,\n      0,\n      startPoint(r3, -displacement),\n      'L',\n      startPoint(r1, -displacement),\n      'Z',\n    ].join(' ')\n  }\n}\n\nfunction StraightArrow(\n  startRadius: number,\n  endRadius: number,\n  centreDistance: number,\n  shaftWidth: number,\n  headWidth: number,\n  headHeight: number,\n  captionLayout: string,\n) {\n  this.length\n  this.midShaftPoint\n  this.outline\n  this.overlay\n  this.shaftLength\n  this.deflection = 0\n\n  this.length = centreDistance - (startRadius + endRadius)\n  this.shaftLength = this.length - headHeight\n\n  const startArrow = startRadius\n  const endShaft = startArrow + this.shaftLength\n  const endArrow = startArrow + this.length\n  const shaftRadius = shaftWidth / 2\n  const headRadius = headWidth / 2\n\n  this.midShaftPoint = {\n    x: startArrow + this.shaftLength / 2,\n    y: 0,\n  }\n\n  // for shortCaptionLength we use textBoundingBox = text.node().getComputedTextLength(),\n  this.outline = function (shortCaptionLength: number) {\n    if (captionLayout === EDGE_CAPTION_EXTERNAL) {\n      const startBreak =\n        startArrow + (this.shaftLength - shortCaptionLength) / 2\n      const endBreak = endShaft - (this.shaftLength - shortCaptionLength) / 2\n\n      return [\n        'M',\n        startArrow,\n        shaftRadius,\n        'L',\n        startBreak,\n        shaftRadius,\n        'L',\n        startBreak,\n        -shaftRadius,\n        'L',\n        startArrow,\n        -shaftRadius,\n        'Z',\n        'M',\n        endBreak,\n        shaftRadius,\n        'L',\n        endShaft,\n        shaftRadius,\n        'L',\n        endShaft,\n        headRadius,\n        'L',\n        endArrow,\n        0,\n        'L',\n        endShaft,\n        -headRadius,\n        'L',\n        endShaft,\n        -shaftRadius,\n        'L',\n        endBreak,\n        -shaftRadius,\n        'Z',\n      ].join(' ')\n    } else {\n      return [\n        'M',\n        startArrow,\n        shaftRadius,\n        'L',\n        endShaft,\n        shaftRadius,\n        'L',\n        endShaft,\n        headRadius,\n        'L',\n        endArrow,\n        0,\n        'L',\n        endShaft,\n        -headRadius,\n        'L',\n        endShaft,\n        -shaftRadius,\n        'L',\n        startArrow,\n        -shaftRadius,\n        'Z',\n      ].join(' ')\n    }\n  }\n\n  this.overlay = function (minWidth: number) {\n    const radius = Math.max(minWidth / 2, shaftRadius)\n    return [\n      'M',\n      startArrow,\n      radius,\n      'L',\n      endArrow,\n      radius,\n      'L',\n      endArrow,\n      -radius,\n      'L',\n      startArrow,\n      -radius,\n      'Z',\n    ].join(' ')\n  }\n}\n\nfunction ArcArrow(\n  startRadius: number,\n  endRadius: number,\n  endCentre: number,\n  _deflection: number,\n  arrowWidth: number,\n  headWidth: number,\n  headLength: number,\n  captionLayout: string,\n) {\n  this.deflection = _deflection\n  const square = (l: number) => l * l\n\n  const deflectionRadians = (this.deflection * Math.PI) / 180\n  const startAttach = {\n    x: Math.cos(deflectionRadians) * startRadius,\n    y: Math.sin(deflectionRadians) * startRadius,\n  }\n\n  const radiusRatio = startRadius / (endRadius + headLength)\n  const homotheticCenter = (-endCentre * radiusRatio) / (1 - radiusRatio)\n\n  const intersectWithOtherCircle = function (\n    fixedPoint: Point,\n    radius: number,\n    xCenter: number,\n    polarity: number,\n  ) {\n    const gradient = fixedPoint.y / (fixedPoint.x - homotheticCenter)\n    const hc = fixedPoint.y - gradient * fixedPoint.x\n\n    const A = 1 + square(gradient)\n    const B = 2 * (gradient * hc - xCenter)\n    const C = square(hc) + square(xCenter) - square(radius)\n\n    const intersection: Point = {\n      x: (-B + polarity * Math.sqrt(square(B) - 4 * A * C)) / (2 * A),\n      y: 0,\n    }\n    intersection.y = (intersection.x - homotheticCenter) * gradient\n\n    return intersection\n  }\n\n  const endAttach = intersectWithOtherCircle(\n    startAttach,\n    endRadius + headLength,\n    endCentre,\n    -1,\n  )\n\n  const g1 = -startAttach.x / startAttach.y\n  const c1 = startAttach.y + square(startAttach.x) / startAttach.y\n  const g2 = -(endAttach.x - endCentre) / endAttach.y\n  const c2 =\n    endAttach.y + ((endAttach.x - endCentre) * endAttach.x) / endAttach.y\n\n  const cx = (c1 - c2) / (g2 - g1)\n  const cy = g1 * cx + c1\n\n  const arcRadius = Math.sqrt(\n    square(cx - startAttach.x) + square(cy - startAttach.y),\n  )\n  const startAngle = Math.atan2(startAttach.x - cx, cy - startAttach.y)\n  const endAngle = Math.atan2(endAttach.x - cx, cy - endAttach.y)\n  let sweepAngle = endAngle - startAngle\n  if (this.deflection > 0) {\n    sweepAngle = 2 * Math.PI - sweepAngle\n  }\n\n  this.shaftLength = sweepAngle * arcRadius\n  if (startAngle > endAngle) {\n    this.shaftLength = 0\n  }\n\n  let midShaftAngle = (startAngle + endAngle) / 2\n  if (this.deflection > 0) {\n    midShaftAngle += Math.PI\n  }\n  this.midShaftPoint = {\n    x: cx + arcRadius * Math.sin(midShaftAngle),\n    y: cy - arcRadius * Math.cos(midShaftAngle),\n  }\n\n  const startTangent = function (dr: number) {\n    const dx = (dr < 0 ? 1 : -1) * Math.sqrt(square(dr) / (1 + square(g1)))\n    const dy = g1 * dx\n    return {\n      x: startAttach.x + dx,\n      y: startAttach.y + dy,\n    }\n  }\n\n  const endTangent = function (dr: number) {\n    const dx = (dr < 0 ? -1 : 1) * Math.sqrt(square(dr) / (1 + square(g2)))\n    const dy = g2 * dx\n    return {\n      x: endAttach.x + dx,\n      y: endAttach.y + dy,\n    }\n  }\n\n  const angleTangent = (angle: number, dr: number) => ({\n    x: cx + (arcRadius + dr) * Math.sin(angle),\n    y: cy - (arcRadius + dr) * Math.cos(angle),\n  })\n\n  const endNormal = function (dc: number) {\n    const dx = (dc < 0 ? -1 : 1) * Math.sqrt(square(dc) / (1 + square(1 / g2)))\n    const dy = dx / g2\n    return {\n      x: endAttach.x + dx,\n      y: endAttach.y - dy,\n    }\n  }\n\n  const endOverlayCorner = function (dr: number, dc: number) {\n    const shoulder = endTangent(dr)\n    const arrowTip = endNormal(dc)\n    return {\n      x: shoulder.x + arrowTip.x - endAttach.x,\n      y: shoulder.y + arrowTip.y - endAttach.y,\n    }\n  }\n\n  const coord = (point: Point) => `${point.x},${point.y}`\n\n  const shaftRadius = arrowWidth / 2\n  const headRadius = headWidth / 2\n  const positiveSweep = startAttach.y > 0 ? 0 : 1\n  const negativeSweep = startAttach.y < 0 ? 0 : 1\n\n  this.outline = function (shortCaptionLength: number) {\n    if (startAngle > endAngle) {\n      return [\n        'M',\n        coord(endTangent(-headRadius)),\n        'L',\n        coord(endNormal(headLength)),\n        'L',\n        coord(endTangent(headRadius)),\n        'Z',\n      ].join(' ')\n    }\n\n    if (captionLayout === EDGE_CAPTION_EXTERNAL) {\n      let captionSweep = shortCaptionLength / arcRadius\n      if (this.deflection > 0) {\n        captionSweep *= -1\n      }\n\n      const startBreak = midShaftAngle - captionSweep / 2\n      const endBreak = midShaftAngle + captionSweep / 2\n\n      return [\n        'M',\n        coord(startTangent(shaftRadius)),\n        'L',\n        coord(startTangent(-shaftRadius)),\n        'A',\n        arcRadius - shaftRadius,\n        arcRadius - shaftRadius,\n        0,\n        0,\n        positiveSweep,\n        coord(angleTangent(startBreak, -shaftRadius)),\n        'L',\n        coord(angleTangent(startBreak, shaftRadius)),\n        'A',\n        arcRadius + shaftRadius,\n        arcRadius + shaftRadius,\n        0,\n        0,\n        negativeSweep,\n        coord(startTangent(shaftRadius)),\n        'Z',\n        'M',\n        coord(angleTangent(endBreak, shaftRadius)),\n        'L',\n        coord(angleTangent(endBreak, -shaftRadius)),\n        'A',\n        arcRadius - shaftRadius,\n        arcRadius - shaftRadius,\n        0,\n        0,\n        positiveSweep,\n        coord(endTangent(-shaftRadius)),\n        'L',\n        coord(endTangent(-headRadius)),\n        'L',\n        coord(endNormal(headLength)),\n        'L',\n        coord(endTangent(headRadius)),\n        'L',\n        coord(endTangent(shaftRadius)),\n        'A',\n        arcRadius + shaftRadius,\n        arcRadius + shaftRadius,\n        0,\n        0,\n        negativeSweep,\n        coord(angleTangent(endBreak, shaftRadius)),\n      ].join(' ')\n    } else {\n      return [\n        'M',\n        coord(startTangent(shaftRadius)),\n        'L',\n        coord(startTangent(-shaftRadius)),\n        'A',\n        arcRadius - shaftRadius,\n        arcRadius - shaftRadius,\n        0,\n        0,\n        positiveSweep,\n        coord(endTangent(-shaftRadius)),\n        'L',\n        coord(endTangent(-headRadius)),\n        'L',\n        coord(endNormal(headLength)),\n        'L',\n        coord(endTangent(headRadius)),\n        'L',\n        coord(endTangent(shaftRadius)),\n        'A',\n        arcRadius + shaftRadius,\n        arcRadius + shaftRadius,\n        0,\n        0,\n        negativeSweep,\n        coord(startTangent(shaftRadius)),\n      ].join(' ')\n    }\n  }\n\n  this.overlay = function (minWidth: number) {\n    const radius = Math.max(minWidth / 2, shaftRadius)\n    return [\n      'M',\n      coord(startTangent(radius)),\n      'L',\n      coord(startTangent(-radius)),\n      'A',\n      arcRadius - radius,\n      arcRadius - radius,\n      0,\n      0,\n      positiveSweep,\n      coord(endTangent(-radius)),\n      'L',\n      coord(endOverlayCorner(-radius, headLength)),\n      'L',\n      coord(endOverlayCorner(radius, headLength)),\n      'L',\n      coord(endTangent(radius)),\n      'A',\n      arcRadius + radius,\n      arcRadius + radius,\n      0,\n      0,\n      negativeSweep,\n      coord(startTangent(radius)),\n    ].join(' ')\n  }\n}\n\ninterface INode extends d3.SimulationNodeDatum {\n  id: string\n  properties: { [key: string]: string | number | object }\n  labels: string[]\n  color: string\n  angleX: number\n  angleY: number\n  links: string[]\n  targetLabels: { [label: string]: number }\n}\n\ninterface IRelationship extends d3.SimulationLinkDatum<INode> {\n  id: string\n  properties: { [key: string]: string | number | object }\n  type: string\n  startNode: string\n  endNode: string\n  source: INode\n  target: INode\n  naturalAngle: number\n  centreDistance: number\n  isLoop: () => boolean\n  captionLayout: string\n  captionHeight: number\n  arrow: {\n    outline: Function\n    overlay: Function\n    shaftLength: number\n    midShaftPoint: Point\n  }\n  fetchedAutomatically?: boolean\n}\n\ninterface IGraph {\n  nodes: INode[]\n  relationships: IRelationship[]\n}\n\ninterface IZoomFuncs {\n  zoomIn: () => d3.Transition<SVGSVGElement, unknown, null, undefined>\n  zoomOut: () => d3.Transition<SVGSVGElement, unknown, null, undefined>\n  resetZoom: () => d3.Transition<SVGSVGElement, unknown, null, undefined>\n  center: () => d3.Transition<SVGSVGElement, unknown, null, undefined>\n}\n\nexport interface IGraphD3 {\n  graphDataToD3Data: (data: any) => IGraph\n  size: () => {\n    nodes: number\n    relationships: number\n  }\n  updateWithD3Data: (d3Data: any) => void\n  updateWithGraphData: (graphData: any) => void\n  zoomFuncs: IZoomFuncs\n  toggleShowAutomaticEdges: () => void\n}\n\nfunction GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {\n  let info: any\n  let nodes: INode[]\n  let shouldShowAutomaticEdges = false\n  let relationship: d3.Selection<SVGGElement, IRelationship, SVGGElement, any>\n  let labelCounter = 0\n  let labels: { [key: string]: number } = {}\n  let relationshipOutline\n  let relationshipOverlay\n  let relationshipText\n  let relationships: IRelationship[]\n  let selector: HTMLDivElement\n  let simulation: d3.Simulation<any, any>\n  let svg: d3.Selection<any, unknown, null, undefined>\n  let svgNodes: d3.Selection<\n    d3.BaseType | SVGPathElement,\n    any,\n    SVGGElement,\n    unknown\n  >\n  let svgRelationships: d3.Selection<\n    d3.BaseType | SVGGElement,\n    any,\n    SVGGElement,\n    unknown\n  >\n  let svgScale: number\n  let svgTranslate: number[]\n  let node: any\n  let justLoaded = false\n  let nominalTextSize = 10\n  let maxTextSize = 24\n  let coreSvg = null\n  let height = 585\n\n  let zoomFuncs: IZoomFuncs\n\n  const options = { ...DEFAULT_OPTIONS, ..._options }\n  let zoom: d3.ZoomBehavior<Element, unknown> = options.graphZoom\n\n  const { labelColors, edgeColors } = options\n\n  function color() {\n    return COLORS[Math.floor(Math.random() * COLORS.length)]\n  }\n\n  function appendGraph(container: d3.Selection<any, unknown, null, undefined>) {\n    let mainSvg = container\n      .append('svg')\n      .attr('width', '100%')\n      .attr('height', height)\n      .attr('class', 'graphd3-graph')\n      .call(\n        options.graphZoom.on('zoom', (event) => {\n          let scale = event.transform.k\n          const translate = [event.transform.x, event.transform.y]\n          if (svgTranslate) {\n            translate[0] += svgTranslate[0]\n            translate[1] += svgTranslate[1]\n          }\n          if (svgScale) {\n            scale *= svgScale\n          }\n\n          let node = svg.selectAll('.node')\n          let textSize = nominalTextSize\n          if (nominalTextSize * scale > maxTextSize)\n            textSize = maxTextSize / scale\n          appendTextToNode(node, textSize)\n          let text = node.selectAll('.text')\n          text.attr('font-size', textSize - 3 + 'px')\n          svg.attr(\n            'transform',\n            `translate(${translate[0]}, ${translate[1]}) scale(${scale})`,\n          )\n        }),\n      )\n      .on('dblclick.zoom', null)\n    svg = mainSvg.append('g').attr('width', '100%').attr('height', '100%')\n\n    zoomFuncs = {\n      zoomIn: () => mainSvg.transition().call(zoom.scaleBy, ZOOM_PROPS.ZOOM_IN),\n      zoomOut: () =>\n        mainSvg.transition().call(zoom.scaleBy, ZOOM_PROPS.ZOOM_OUT),\n      resetZoom: () =>\n        mainSvg.transition().call(zoom.scaleTo, ZOOM_PROPS.ZOOM_RESET),\n      center: () =>\n        mainSvg\n          .transition()\n          .call(\n            zoom.translateTo,\n            ...ZOOM_PROPS.CAMERA_CENTER(\n              mainSvg.node().getBoundingClientRect().width,\n              mainSvg.node().getBoundingClientRect().height,\n            ),\n          ),\n    }\n\n    svgRelationships = svg.append('g').attr('class', 'relationships')\n\n    svgNodes = svg.append('g').attr('class', 'nodes')\n\n    coreSvg = mainSvg\n  }\n\n  function appendInfoPanel(container) {\n    return container.append('div').attr('class', 'd3-info')\n  }\n\n  function appendInfoElement(className, nodeData) {\n    let colorNode\n    let property\n\n    if (nodeData.labels) {\n      ;[property] = nodeData.labels\n      colorNode = nodeData.color\n    } else {\n      colorNode = '#808080'\n      property = nodeData.type\n    }\n\n    info\n      .append('span')\n      .attr('class', className)\n      .html(`<strong>${property}</strong>`)\n      .style('background-color', colorNode)\n      .style('border-color', Utils.darkenColor(colorNode))\n      .style('color', Utils.invertColor(colorNode))\n  }\n\n  function appendInfoElementProperty(cls, property, value) {\n    info\n      .append('span')\n      .attr('class', cls)\n      .html(`<strong>${property}</strong> ${value}`)\n  }\n\n  function stickNode(ele, event, d) {\n    /* eslint-disable */\n    d.fx = event.x\n    d.fy = event.y\n    /* eslint-enable */\n\n    // Add a ring to specify that the node was selected\n    d3.select(ele).attr('class', 'node selected')\n  }\n\n  function dragEnded(event, d) {\n    if (!event.active) {\n      simulation.alphaTarget(0)\n    }\n\n    if (typeof options.onNodeDragEnd === 'function') {\n      options.onNodeDragEnd(this, d, event)\n    }\n  }\n\n  function dragged(event, d) {\n    stickNode(this, event, d)\n\n    // Don't move other nodes on drag of one node.\n    svg.selectAll('.node').each((n: any) => {\n      if (d.id !== n.id) {\n        n.fx = n.x\n        n.fy = n.y\n      }\n    })\n  }\n\n  function dragStarted(event, d) {\n    if (!event.active) {\n      simulation.alphaTarget(0.3).restart()\n    }\n    /* eslint-disable */\n    d.fx = d.x\n    d.fy = d.y\n    /* eslint-enable */\n\n    if (typeof options.onNodeDragStart === 'function') {\n      options.onNodeDragStart(this, d, event)\n    }\n\n    // Select the entity to display its info\n    if (info) {\n      updateInfo(d)\n    }\n  }\n\n  function clearInfo() {\n    info.html('')\n  }\n\n  function updateInfo(d) {\n    clearInfo()\n\n    if (typeof options.onDisplayInfo === 'function') {\n      options.onDisplayInfo(info, d)\n      return\n    }\n\n    appendInfoElement('class', d)\n    appendInfoElementProperty('property', '&lt;id&gt;', d.id)\n\n    Object.keys(d.properties).forEach((property) => {\n      appendInfoElementProperty(\n        'property',\n        property,\n        JSON.stringify(d.properties[property]),\n      )\n    })\n  }\n\n  function appendNode() {\n    return node\n      .enter()\n      .append('g')\n      .attr('class', (d) => {\n        const label = d.labels?.length ? d.labels[0] : ''\n        let classes = 'node'\n        let highlight\n\n        if (options.highlight) {\n          for (let i = 0; i < options.highlight.length; i += 1) {\n            highlight = options.highlight[i]\n\n            if (\n              label === highlight.class &&\n              d.properties[highlight.property] === highlight.value\n            ) {\n              classes += ' node-highlighted'\n              break\n            }\n          }\n        }\n\n        return classes\n      })\n      .on('click', function onNodeClick(event, d) {\n        /* eslint-disable */\n        d.fx = null\n        d.fy = null\n        /* eslint-enable */\n\n        if (typeof options.onNodeClick === 'function') {\n          options.onNodeClick(this, d, event)\n        }\n\n        if (info) {\n          updateInfo(d)\n        }\n      })\n      .on('dblclick', function onNodeDoubleClick(event, d) {\n        stickNode(this, event, d)\n\n        if (typeof options.onNodeDoubleClick === 'function') {\n          options.onNodeDoubleClick(this, d, event)\n        }\n\n        ;[...new Set(nodes.map((n) => n.labels[0]))].forEach((l) => {\n          if (labels[l] === undefined) {\n            labels[l] = labelCounter\n            labelCounter += 1\n          }\n        })\n\n        // Pulse on double click\n        Utils.pulse(d3.select(event.currentTarget).select('.outline'))\n\n        // Calculating the next positio of the node takes some times\n        // so start transition only after the calculation. So delay\n        // starting the transition by some milliseconds.\n        setTimeout(\n          () =>\n            d3\n              .select('.graphd3-graph')\n              .transition()\n              .duration(500)\n              .call(zoom.translateTo as any, d.x, d.y),\n          10,\n        )\n      })\n      .on('mouseenter', function onNodeMouseEnter(event, d) {\n        if (typeof options.onNodeMouseEnter === 'function') {\n          options.onNodeMouseEnter(this, d, event)\n        }\n      })\n      .on('mouseleave', function onNodeMouseLeave(event, d) {\n        if (info) {\n          // clearInfo(d)\n        }\n\n        if (typeof options.onNodeMouseLeave === 'function') {\n          options.onNodeMouseLeave(this, d, event)\n        }\n      })\n      .call(\n        d3\n          .drag()\n          .on('start', dragStarted)\n          .on('drag', dragged)\n          .on('end', dragEnded),\n      )\n  }\n\n  function appendRingToNode(svgNode) {\n    return svgNode\n      .append('circle')\n      .attr('class', 'ring')\n      .style(\n        'stroke',\n        (d) => labelColors(d.labels?.length ? d.labels[0] : '').borderColor,\n      )\n      .attr('r', options.nodeRadius * 1.16)\n  }\n\n  function appendOutlineToNode(svgNode) {\n    return svgNode\n      .append('circle')\n      .attr('class', 'outline')\n      .attr('r', options.nodeRadius)\n      .style(\n        'fill',\n        (d) => labelColors(d.labels?.length ? d.labels[0] : '').color,\n      )\n      .style(\n        'stroke',\n        (d) => labelColors(d.labels?.length ? d.labels[0] : '').borderColor,\n      )\n  }\n\n  function appendNodeInfo(svgNode) {\n    if (!options.onNodeInfoClick) {\n      return\n    }\n\n    const g = svgNode\n      .append('g')\n      .attr('class', 'info')\n      .attr('transform', 'translate(9, -28)')\n      .on('click', function onNodeInfoClick(event, d) {\n        if (typeof options.onNodeInfoClick === 'function') {\n          options.onNodeInfoClick(this, d, event)\n        }\n\n        if (info) {\n          updateInfo(d)\n        }\n      })\n\n    g.append('rect')\n      .attr('width', '20px')\n      .attr('height', '20px')\n      .style('fill', '#444')\n      .style('stroke', '#54b3ff')\n      .attr('rx', 10)\n      .attr('ry', 10)\n\n    g.append('text')\n      .text('i')\n      .attr('fill', 'white')\n      .attr('font-size', 11)\n      .attr('x', '9')\n      .attr('y', '14')\n  }\n\n  function appendTextToNode(svgNode, textSize = null) {\n    svgNode\n      .selectAll('text')\n      .data((d) => {\n        let label: string\n        if (typeof options.onLabelNode === 'function') {\n          label = options.onLabelNode(d)\n        } else {\n          label = d.properties?.name || d.properties?.title\n        }\n        label ||= ''\n        if (textSize !== null) {\n          let maxLength = maxTextSize - textSize + 15\n          label =\n            label.length > maxLength\n              ? Utils.truncateText(label, maxLength)\n              : label\n        }\n        let wrappedLabel = Utils.wrapText(label, 7)\n        return wrappedLabel.split('\\n').map((k) => ({\n          text: k,\n          actual: wrappedLabel,\n          d,\n        }))\n      })\n      .join('text')\n      .text((d) => d.text)\n      .attr('y', (d, i) => {\n        const calculatePosition = (l: number) => {\n          if (l == 1) return 0\n          else {\n            let arr = []\n            for (let m = 0; m < l / 2; m++) {\n              let z = m * 10\n              arr = [-z - 5, ...arr]\n              arr = [...arr, z + 5]\n            }\n            return arr\n          }\n        }\n        const r = calculatePosition(d.actual.split('\\n').length)\n        return r[i]\n      })\n      .attr('class', 'text')\n      .attr('font-size', () => nominalTextSize + 'px')\n      .attr(\n        'fill',\n        ({ d }) => labelColors(d.labels?.length ? d.labels[0] : '').textColor,\n      )\n      .attr('pointer-events', 'none')\n      .attr('text-anchor', 'middle')\n      .attr('dy', () => options.nodeRadius / ((options.nodeRadius * 25) / 100))\n  }\n\n  function appendNodeToGraph() {\n    const n = appendNode()\n\n    appendRingToNode(n)\n    appendOutlineToNode(n)\n    appendNodeInfo(n)\n    appendTextToNode(n, null)\n\n    return n\n  }\n\n  function contains(array, id) {\n    const filter = array.filter((elem) => elem.id === id)\n\n    return filter.length > 0\n  }\n\n  function appendRelationship() {\n    return relationship\n      .enter()\n      .append('g')\n      .attr('class', (r) => `relationship relationship-${r.id}`)\n      .on('dblclick', function onRelationshipDoubleClick(event, d) {\n        if (typeof options.onRelationshipDoubleClick === 'function') {\n          options.onRelationshipDoubleClick(this, d, event)\n        }\n      })\n      .on('click', (event, d) => {\n        if (info) {\n          updateInfo(d)\n        }\n      })\n  }\n\n  function appendOutlineToRelationship(r) {\n    return r\n      .append('path')\n      .attr('class', 'outline')\n      .attr('fill', (d) => edgeColors(d.type).color)\n  }\n\n  function appendOverlayToRelationship(r) {\n    return r\n      .append('path')\n      .attr('class', 'overlay')\n      .style('fill', (d) => edgeColors(d.type).color)\n  }\n\n  function appendTextToRelationship(r) {\n    return r\n      .append('text')\n      .attr('class', 'text')\n      .attr('fill', (d) => edgeColors(d.type).color)\n      .attr('font-size', '12px')\n      .attr('pointer-events', 'none')\n      .attr('text-anchor', 'middle')\n      .text((d) => d.type)\n  }\n\n  function appendRelationshipToGraph() {\n    const svgRelationship = appendRelationship()\n\n    const text = appendTextToRelationship(svgRelationship)\n\n    const outline = appendOutlineToRelationship(svgRelationship)\n    const overlay = appendOverlayToRelationship(svgRelationship)\n\n    return {\n      outline,\n      overlay,\n      relationship: svgRelationship,\n      text,\n    }\n  }\n\n  function updateNodes(n: INode) {\n    Array.prototype.push.apply(nodes, n)\n\n    node = svgNodes.selectAll('.node').data(nodes, (d: INode) => d.id)\n\n    const nodeEnter = appendNodeToGraph()\n    node = nodeEnter.merge(node)\n  }\n\n  function updateRelationships(r: IRelationship) {\n    Array.prototype.push.apply(relationships, r)\n    let a = svgRelationships.selectAll('.relationship')\n    relationship = svgRelationships\n      .selectAll('.relationship')\n      .data(relationships, (d: IRelationship) => d.id) as d3.Selection<\n      SVGGElement,\n      IRelationship,\n      SVGGElement,\n      any\n    >\n\n    const relationshipEnter = appendRelationshipToGraph()\n\n    relationship = relationshipEnter.relationship.merge(relationship)\n    relationshipOutline = svg.selectAll('.relationship .outline')\n    relationshipOutline = relationshipEnter.outline.merge(relationshipOutline)\n\n    relationshipOverlay = svg.selectAll('.relationship .overlay')\n    relationshipOverlay = relationshipEnter.overlay.merge(relationshipOverlay)\n\n    relationshipText = svg.selectAll('.relationship .text')\n    relationshipText = relationshipEnter.text.merge(relationshipText)\n  }\n\n  function updateNodesAndRelationships(n, r) {\n    let nodeIds = nodes.map((n) => n.id)\n    n = n.filter((k) => !nodeIds.includes(k.id))\n\n    let edgeIds = relationships.map((e) => e.id)\n    const previousEdges = [...r]\n    r = r.filter((k) => !edgeIds.includes(k.id))\n\n    if (relationship !== undefined) {\n      relationship.each((r) => {\n        // If an edge is being fetchedAutomatically and is now added\n        // in new data, mark fetchedAutomatically to false.\n        if (\n          r.fetchedAutomatically &&\n          previousEdges.map((k) => k.id).includes(r.id)\n        ) {\n          r.fetchedAutomatically = false\n        }\n      })\n    }\n\n    updateRelationships(r)\n    updateNodes(n)\n\n    simulation.nodes(nodes)\n    simulation.force(\n      'link',\n      d3.forceLink(relationships).id((d: IRelationship) => d.id),\n    )\n\n    // Every time the function is run, do check whether automatically fetched edges must be rendered.\n    d3.selectAll('.relationship').each((r: IRelationship) => {\n      if (!shouldShowAutomaticEdges && r.fetchedAutomatically) {\n        d3.selectAll(`.relationship-${r.id}`).remove()\n      }\n    })\n  }\n\n  function graphDataToD3Data(data) {\n    const graph: IGraph = {\n      nodes: nodes,\n      relationships: [],\n    }\n\n    data.results.forEach((result) => {\n      result.data.forEach((dataItem) => {\n        dataItem.graph.nodes.forEach((nodeData) => {\n          if (!contains(graph.nodes, nodeData.id)) {\n            const randomColor = nodeData.labels?.length === 0 ? 'gray' : color()\n\n            graph.nodes.push({\n              ...nodeData,\n              color: options.nodeOutlineFillColor\n                ? options.nodeOutlineFillColor\n                : randomColor,\n            })\n          }\n        })\n\n        dataItem.graph.relationships.forEach((relationshipData) => {\n          graph.relationships.push({\n            ...relationshipData,\n            source: relationshipData.startNode,\n            target: relationshipData.endNode,\n          })\n        })\n\n        dataItem.graph.relationships.sort((a, b) => {\n          if (a.source > b.source) {\n            return 1\n          }\n\n          if (a.source < b.source) {\n            return -1\n          }\n\n          if (a.target > b.target) {\n            return 1\n          }\n\n          if (a.target < b.target) {\n            return -1\n          }\n\n          return 0\n        })\n      })\n    })\n\n    mapData(graph)\n\n    return graph\n  }\n\n  function updateWithD3Data(d3Data) {\n    // marker\n    updateNodesAndRelationships(d3Data.nodes, d3Data.relationships)\n  }\n\n  function updateWithGraphData(graphData) {\n    const d3Data = graphDataToD3Data(graphData)\n    updateWithD3Data(d3Data)\n  }\n\n  function loadGraphData(graphData) {\n    nodes = []\n    relationships = []\n\n    updateWithGraphData(graphData)\n  }\n\n  function rotatePoint(c, p, angle) {\n    return Utils.rotate(c.x, c.y, p.x, p.y, angle)\n  }\n\n  function rotation(source, target) {\n    return (\n      (Math.atan2(target.y - source.y, target.x - source.x) * 180) / Math.PI\n    )\n  }\n\n  function size() {\n    return {\n      nodes: nodes.length,\n      relationships: relationships.length,\n    }\n  }\n\n  function unitaryNormalVector(source, target, newLength) {\n    const center = { x: 0, y: 0 }\n    const vector = Utils.unitaryVector(source, target, newLength)\n\n    return rotatePoint(center, vector, 90)\n  }\n\n  function tickRelationshipsOutlines() {\n    relationship.each(function (relationship) {\n      // FIXME:\n\n      let rel = d3.select(this),\n        outline = rel.select('.outline') as unknown as d3.Selection<\n          d3.BaseType,\n          IRelationship,\n          null,\n          undefined\n        >,\n        text = rel.select('.text'),\n        textPadding = 8,\n        textLength = text.node().getComputedTextLength(),\n        captionLength = textLength > 0 ? textLength + textPadding : 0\n\n      outline.attr('d', (d) => {\n        if (captionLength > d.arrow.shaftLength) {\n          captionLength = d.arrow.shaftLength\n        }\n        return d.arrow.outline(captionLength)\n      })\n    })\n  }\n\n  function tickRelationshipsOverlays() {\n    relationshipOverlay.attr('d', (d) => {\n      return d.arrow.overlay(options.arrowSize)\n    })\n  }\n\n  function tickRelationshipsTexts() {\n    relationshipText.attr('transform', (rel) => {\n      if (rel.naturalAngle < 90 || rel.naturalAngle > 270) {\n        return `rotate(180 ${rel.arrow.midShaftPoint.x} ${rel.arrow.midShaftPoint.y})`\n      } else {\n        return null\n      }\n    })\n\n    relationshipText.attr('x', (rel) => rel.arrow.midShaftPoint.x)\n    relationshipText.attr(\n      'y',\n      //TODO: Make the fontsize and padding dynamic\n      (rel) => rel.arrow.midShaftPoint.y + 8.5 / 2 - 1,\n    )\n  }\n\n  function tickRelationships() {\n    //TODO: add multiple cases\n\n    layoutRelationships()\n\n    if (relationship) {\n      layoutRelationships()\n\n      relationship.attr('transform', (d) => {\n        return `translate(${d.source.x} ${d.source.y}) rotate(${\n          d.naturalAngle + 180\n        })`\n      })\n\n      tickRelationshipsTexts()\n      tickRelationshipsOutlines()\n      tickRelationshipsOverlays()\n    }\n  }\n\n  function tickNodes() {\n    if (node) {\n      node.attr('transform', (d) => `translate(${d.x}, ${d.y})`)\n    }\n  }\n\n  function tick() {\n    simulation.tick(1)\n    tickNodes()\n    tickRelationships()\n  }\n\n  // eslint-disable-next-line no-unused-vars\n  function zoomFit() {\n    const bounds = svg.node().getBBox()\n    const parent = svg.node().parentElement.parentElement\n\n    if (!parent) {\n      return\n    }\n\n    const fullWidth = parent.clientWidth\n    const fullHeight = parent.clientHeight\n    const { width, height } = bounds\n    const midX = bounds.x + width / 2\n    const midY = bounds.y + height / 2\n\n    if (width === 0 || height === 0) {\n      return // nothing to fit\n    }\n\n    svgScale = 0.85 / Math.max(width / fullWidth, height / fullHeight)\n    svgTranslate = [\n      fullWidth / 2 - svgScale * midX,\n      fullHeight / 2 - svgScale * midY,\n    ]\n\n    svg.attr(\n      'transform',\n      `translate(${svgTranslate[0]}, ${svgTranslate[1]}) scale(${svgScale})`,\n    )\n  }\n\n  function initSimulation() {\n    const spreadFactor = 1.25\n    return d3\n      .forceSimulation()\n      .force(\n        'link',\n        d3\n          .forceLink()\n          .id((d: IRelationship) => d.id)\n          .distance(70),\n      )\n      .force(\n        'charge',\n        d3.forceManyBody().strength((d, i) => (i ? -5000 : 500)),\n      )\n      .force(\n        'y',\n        d3.forceY(svg.node().parentElement.parentElement.clientHeight / 2),\n      )\n      .force(\n        'center',\n        d3.forceCenter(\n          svg.node().parentElement.parentElement.clientWidth / 2,\n          svg.node().parentElement.parentElement.clientHeight / 2,\n        ),\n      )\n      .on('tick', () => {\n        tick()\n      })\n      .on('end', () => {\n        if (options.zoomFit && !justLoaded) {\n          justLoaded = true\n          zoomFit()\n        }\n      })\n  }\n\n  function init() {\n    if (!options.minCollision) {\n      options.minCollision = options.nodeRadius * 2\n    }\n\n    selector = _selector\n\n    const container = d3.select(selector)\n\n    container.attr('class', 'graphd3').html('')\n\n    if (options.infoPanel) {\n      info = appendInfoPanel(container)\n    }\n\n    appendGraph(container)\n\n    simulation = initSimulation()\n\n    if (options.graphData) {\n      loadGraphData(options.graphData)\n    } else {\n      console.error('Error: graphData is empty!')\n    }\n\n    ;[...new Set(nodes.map((n) => n.labels[0]))].forEach((l) => {\n      if (labels[l] === undefined) {\n        labels[l] = labelCounter\n        labelCounter += 1\n      }\n    })\n\n    simulation\n      .force(\n        'x',\n        d3.forceX((d) => d.angleX || 0),\n      )\n      .force(\n        'y',\n        d3.forceY((d) => d.angleY || 0),\n      )\n      .force(\n        'center',\n        d3.forceCenter(\n          svg.node().parentElement.parentElement.clientWidth / 2,\n          svg.node().parentElement.parentElement.clientHeight / 2,\n        ),\n      )\n      .force('centerX', d3.forceX(0).strength(0.03))\n      .force('centerX', d3.forceX(0).strength(0.03))\n  }\n\n  init()\n\n  class NodePair {\n    nodeA: INode\n    nodeB: INode\n    relationships: IRelationship[]\n\n    constructor(node1: INode, node2: INode) {\n      if (node1.id < node2.id) {\n        this.nodeA = node1\n        this.nodeB = node2\n      } else {\n        this.nodeA = node2\n        this.nodeB = node1\n      }\n      this.relationships = []\n    }\n\n    isLoop(): boolean {\n      return this.nodeA.id === this.nodeB.id\n    }\n\n    toString() {\n      return `${this.nodeA.id}:${this.nodeB.id}`\n    }\n  }\n\n  function layoutRelationships() {\n    const nodePairs = groupedRelationships()\n    computeGeometryForNonLoopArrows(nodePairs)\n    distributeAnglesForLoopArrows(nodePairs, relationships)\n\n    return (() => {\n      const result = []\n      for (let nodePair of Array.from(nodePairs)) {\n        for (let relationship of Array.from(nodePair.relationships)) {\n          delete relationship.arrow\n        }\n\n        let middleRelationshipIndex = (nodePair.relationships.length - 1) / 2\n        let defaultDeflectionStep = 30\n        const maximumTotalDeflection = 150\n        const numberOfSteps = nodePair.relationships.length - 1\n        const totalDeflection = defaultDeflectionStep * numberOfSteps\n\n        let deflectionStep =\n          totalDeflection > maximumTotalDeflection\n            ? maximumTotalDeflection / numberOfSteps\n            : defaultDeflectionStep\n\n        result.push(\n          (() => {\n            for (let i = 0; i < nodePair.relationships.length; i++) {\n              let ref\n              const relationship = nodePair.relationships[i]\n              const nodeRadius = options.nodeRadius\n              const shaftWidth = options.relationshipWidth\n              const headWidth = options.arrowSize\n              const headHeight = headWidth\n\n              if (nodePair.isLoop()) {\n                relationship.arrow = new LoopArrow(\n                  nodeRadius,\n                  40,\n                  defaultDeflectionStep,\n                  shaftWidth,\n                  headWidth,\n                  headHeight,\n                  relationship.captionHeight || 11,\n                )\n              } else {\n                if (i === middleRelationshipIndex) {\n                  relationship.arrow = new StraightArrow(\n                    nodeRadius,\n                    nodeRadius,\n                    relationship.centreDistance,\n                    shaftWidth,\n                    headWidth,\n                    headHeight,\n                    relationship.captionLayout || EDGE_CAPTION_EXTERNAL,\n                  )\n                } else {\n                  let deflection =\n                    deflectionStep * (i - middleRelationshipIndex)\n\n                  if (nodePair.nodeA !== relationship.source) {\n                    deflection *= -1\n                  }\n\n                  relationship.arrow = new ArcArrow(\n                    nodeRadius,\n                    nodeRadius,\n                    relationship.centreDistance,\n                    deflection,\n                    shaftWidth,\n                    headWidth,\n                    headHeight,\n                    relationship.captionLayout || EDGE_CAPTION_EXTERNAL,\n                  )\n                }\n              }\n            }\n          })(),\n        )\n      }\n      return result\n    })()\n  }\n\n  //FIXME:DONT HAVE TO REPEAT\n\n  function findNode(id, nodes) {\n    let match\n    nodes.forEach((node) => {\n      if (node.id == id) match = node\n    })\n    return match\n  }\n\n  function mapData(d: IGraph) {\n    d.relationships.map((r) => {\n      let source = findNode(r.startNode, d.nodes)\n      let target = findNode(r.endNode, d.nodes)\n\n      source.links = source.links\n        ? Array.from(new Set([...source.links, target.labels[0]]))\n        : [target.labels[0]]\n      ;(r.source = source),\n        (r.target = target),\n        (r.naturalAngle = 0),\n        (r.isLoop = function () {\n          return this.source === this.target\n        })\n      return r\n    })\n\n    nodes.map((n) => {\n      if (n.links !== undefined) {\n        let labels = {}\n        n.links.forEach((l, i) => {\n          labels[l] = i\n        })\n\n        const equalAngles = 360 / Object.keys(labels).length\n        let angleIterator = 0\n        Object.keys(labels).map((l) => {\n          labels[l] = angleIterator\n          angleIterator += equalAngles\n        })\n\n        n.targetLabels = labels\n      }\n    })\n\n    d.relationships.map((r) => {\n      const radius = 1300\n      const onlyOne = Object.keys(r.source.targetLabels).length === 1\n\n      const degree =\n        (Math.PI * 2 * r.source.targetLabels[r.target.labels[0]]) / 360\n      const angleX = onlyOne ? 0 : radius * Math.sin(degree)\n      const angleY = onlyOne ? 0 : radius * Math.cos(degree)\n\n      r.target.angleX = angleX\n      r.target.angleY = angleY\n    })\n  }\n\n  function groupedRelationships(): NodePair[] {\n    const groups: {\n      [key: string]: NodePair\n    } = {}\n    for (const relationship of Array.from(relationships)) {\n      let nodePair = new NodePair(relationship.source, relationship.target)\n      nodePair =\n        groups[nodePair.toString()] != null\n          ? groups[nodePair.toString()]\n          : nodePair\n      nodePair.relationships.push(relationship)\n      groups[nodePair.toString()] = nodePair\n    }\n    return (() => {\n      const result = []\n      for (const ignored in groups) {\n        const pair = groups[ignored]\n        result.push(pair)\n      }\n      return result\n    })()\n  }\n\n  function computeGeometryForNonLoopArrows(nodePairs: NodePair[]) {\n    const square = (distance: number) => distance * distance\n    return (() => {\n      const result: number[][] | undefined = []\n      for (let nodePair of Array.from(nodePairs)) {\n        if (!nodePair.isLoop()) {\n          const dx = nodePair.nodeA.x - nodePair.nodeB.x\n          const dy = nodePair.nodeA.y - nodePair.nodeB.y\n          let angle = ((Math.atan2(dy, dx) / Math.PI) * 180 + 360) % 360\n          let centreDistance = Math.sqrt(square(dx) + square(dy))\n          result.push(\n            (() => {\n              const result1: number[] = []\n              for (const relationship of Array.from(nodePair.relationships)) {\n                relationship.naturalAngle =\n                  relationship.target === nodePair.nodeA\n                    ? (angle + 180) % 360\n                    : angle\n                result1.push((relationship.centreDistance = centreDistance))\n              }\n              return result1\n            })(),\n          )\n        } else {\n          result.push(undefined)\n        }\n      }\n      return result\n    })()\n  }\n\n  function distributeAnglesForLoopArrows(\n    nodePairs: NodePair[],\n    relationships: IRelationship[],\n  ) {\n    return (() => {\n      const result = []\n      for (let nodePair of Array.from(nodePairs)) {\n        if (nodePair.isLoop()) {\n          let i: number, separation: number\n          let angles = []\n          const node = nodePair.nodeA\n          for (let relationship of Array.from(relationships)) {\n            if (!relationship.isLoop()) {\n              if (relationship.source === node) {\n                angles.push(relationship.naturalAngle)\n              }\n              if (relationship.target === node) {\n                angles.push(relationship.naturalAngle + 180)\n              }\n            }\n          }\n          angles = angles.map((a) => (a + 360) % 360).sort((a, b) => a - b)\n          if (angles.length > 0) {\n            let end: number, start: number\n            let biggestGap = {\n              start: 0,\n              end: 0,\n            }\n            for (i = 0; i < angles.length; i++) {\n              const angle = angles[i]\n              start = angle\n              end = i === angles.length - 1 ? angles[0] + 360 : angles[i + 1]\n              if (end - start > biggestGap.end - biggestGap.start) {\n                biggestGap.start = start\n                biggestGap.end = end\n              }\n            }\n            separation =\n              (biggestGap.end - biggestGap.start) /\n              (nodePair.relationships.length + 1)\n            result.push(\n              (() => {\n                const result1 = []\n                for (i = 0; i < nodePair.relationships.length; i++) {\n                  const relationship = nodePair.relationships[i]\n                  result1.push(\n                    (relationship.naturalAngle =\n                      (biggestGap.start + (i + 1) * separation - 90) % 360),\n                  )\n                }\n                return result1\n              })(),\n            )\n          } else {\n            separation = 360 / nodePair.relationships.length\n\n            result.push(\n              (() => {\n                const result2 = []\n                for (i = 0; i < nodePair.relationships.length; i++) {\n                  const relationship = nodePair.relationships[i]\n                  result2.push((relationship.naturalAngle = i * separation))\n                }\n                return result2\n              })(),\n            )\n          }\n        } else {\n          result.push(undefined)\n        }\n      }\n      return result\n    })()\n  }\n\n  function resize() {\n    const isFullScreen =\n      parent.document.body.getElementsByClassName('fullscreen').length > 0\n    if (isFullScreen) {\n      coreSvg.attr('height', parent.document.body.offsetHeight - 50)\n      coreSvg\n        .transition()\n        .call(\n          zoom.translateTo,\n          ...ZOOM_PROPS.CAMERA_CENTER(\n            coreSvg.node().getBoundingClientRect().width,\n            coreSvg.node().getBoundingClientRect().height - 300,\n          ),\n        )\n    } else {\n      coreSvg.attr('height', height)\n      coreSvg\n        .transition()\n        .call(\n          zoom.translateTo,\n          ...ZOOM_PROPS.CAMERA_CENTER(\n            coreSvg.node().getBoundingClientRect().width,\n            coreSvg.node().getBoundingClientRect().height,\n          ),\n        )\n    }\n    simulation.restart()\n  }\n\n  d3.select(window).on('resize', resize)\n\n  resize()\n\n  function toggleShowAutomaticEdges() {\n    // Simply re-run the function. `updateNodesAndRelationships` internally checks for `shouldShowAutomaticEdges` prop to render edges that were fetched automatically.\n    shouldShowAutomaticEdges = !shouldShowAutomaticEdges\n    updateNodesAndRelationships([], [])\n    simulation.restart()\n  }\n\n  return {\n    graphDataToD3Data,\n    size,\n    updateWithD3Data,\n    updateWithGraphData,\n    zoomFuncs,\n    toggleShowAutomaticEdges,\n  }\n}\n\nexport default GraphD3\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/main.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { render } from 'react-dom'\nimport { GraphApp, TableApp } from './App'\nimport { ThemeProvider } from 'uiSrc/components/base/utils/pluginsThemeContext'\nimport './styles/styles.scss'\nimport { appendIconComponentCache } from '@elastic/eui/es/components/icon/icon'\nimport { icon as EuiIconMagnifyWithPlus } from '@elastic/eui/es/components/icon/assets/magnifyWithPlus'\nimport { icon as EuiIconMagnifyWithMinus } from '@elastic/eui/es/components/icon/assets/magnifyWithMinus'\nimport { icon as EuiIconBullsEye } from '@elastic/eui/es/components/icon/assets/bullseye'\nimport { icon as EuiIconEditorItemAlignCenter } from '@elastic/eui/es/components/icon/assets/editorItemAlignCenter'\nimport result from './mockData/resultGraph.json'\n\ninterface Props {\n  command?: string\n  data?: { response: any; status: string }[]\n}\n\nappendIconComponentCache({\n  magnifyWithPlus: EuiIconMagnifyWithPlus,\n  magnifyWithMinus: EuiIconMagnifyWithMinus,\n  bullseye: EuiIconBullsEye,\n  editorItemAlignCenter: EuiIconEditorItemAlignCenter,\n})\n\nconst renderApp = (element: JSX.Element) =>\n  render(\n    <ThemeProvider>{element}</ThemeProvider>,\n    document.getElementById('app'),\n  )\n\nconst renderGraphTable = (props: Props) =>\n  renderApp(<TableApp data={props.data} command={props.command} />)\n\nconst renderGraph = (props: Props) =>\n  renderApp(<GraphApp data={props.data} command={props.command} />)\n\nif (process.env.NODE_ENV === 'development') {\n  renderGraph({ data: result, command: 'graph' })\n}\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderGraphTable, renderGraph }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/mockData/resultGraph.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      {\n        \"status\": \"success\",\n        \"response\": [\n          [\"r\", \"t\"],\n          [\n            [\n              [\n                [\"id\", 0],\n                [\"labels\", [\"Rider\"]],\n                [\"properties\", [[\"name\", \"Valentino Rossi\"]]]\n              ],\n              [\n                [\"id\", 1],\n                [\"labels\", [\"Team\"]],\n                [\"properties\", [[\"name\", \"Yamaha\"]]]\n              ]\n            ]\n          ],\n          [\n            \"Cached execution: 0\",\n            \"Query internal execution time: 26.122792 milliseconds\"\n          ]\n        ]\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/parser.ts",
    "content": "function resolveProps(d: Object | Array<unknown>): any {\n  if (!Array.isArray(d) && typeof d === 'object') {\n    return d\n  } else if (Array.isArray(d)) {\n    const key = d[0]\n    if (d.length === 2) {\n      const value = d[1]\n      return {\n        key,\n        value,\n      }\n    } else {\n      return {\n        key,\n        value: d.filter((_, i) => i !== 0),\n      }\n    }\n  }\n}\n\ninterface INode {\n  id: string\n  labels: string[]\n  properties: { [key: string]: string | number | object }\n}\n\ninterface IEdge {\n  id: string\n  type: string\n  source: string\n  target: string\n  properties: { [key: string]: string | number | object }\n}\n\ninterface IResponseParser {\n  nodes: INode[]\n  edges: IEdge[]\n  nodeIds: Set<string>\n  edgeIds: Set<string>\n  labels: { [key: string]: number }\n  types: { [key: string]: number }\n  headers: any\n  hasNamedPathItem: boolean\n  npNodeIds: string[]\n  npEdgeIds: string[]\n  danglingEdgeIds: Set<string>\n}\n\nfunction responseParser(data: any): IResponseParser {\n  const headers = data[0]\n  let nodes: INode[] = []\n  let nodeIds = new Set<string>()\n  let edgeIds = new Set<string>()\n  let edges: IEdge[] = []\n  let types: { [key: string]: number } = {}\n  let labels: { [key: string]: number } = {}\n  let hasNamedPathItem = false\n  let npNodeIds: string[] = []\n  let npEdgeIds: string[] = []\n  let danglingEdgeIds = new Set<string>()\n  if (data.length < 2)\n    return {\n      nodes,\n      edges,\n      types,\n      labels,\n      headers,\n      nodeIds,\n      edgeIds,\n      hasNamedPathItem,\n      npNodeIds,\n      npEdgeIds,\n      danglingEdgeIds,\n    }\n\n  const entries = data[1].map((entry: any) => {\n    /* entry -> has headers number of items */\n    entry.map((item: any) => {\n      if (Array.isArray(item)) {\n        if (item[0][0] === 'id' && item[1][0] === 'labels') {\n          const node: INode = {\n            id: item[0][1],\n            labels: item[1][1],\n            properties: {},\n          }\n          labels[item[1][1]] = labels[item[1][1]] + 1 || 1\n          const propValues = item[2][1]\n          propValues.map((x: any) => {\n            const v = resolveProps(x)\n            node['properties'][v.key] = v.value\n          })\n          if (nodes.findIndex((e: INode) => e.id === item[0][1]) === -1) {\n            nodes.push(node)\n          }\n        } else if (item[0][0] === 'id' && item[1][0] === 'type') {\n          const edge: IEdge = {\n            id: item[0][1],\n            type: item[1][1],\n            source: item[2][1],\n            target: item[3][1],\n            properties: {},\n          }\n          types[item[1][1]] = types[item[1][1]] + 1 || 1\n          const propValues = item[4][1]\n          propValues.map((x: any) => {\n            const v = resolveProps(x)\n            edge['properties'][v.key] = v.value\n          })\n          if (!edgeIds.has(edge.id)) {\n            edges.push(edge)\n            edgeIds.add(edge.id)\n          }\n        } else {\n          // unknown item?\n        }\n      } else if (typeof item === 'string') {\n        try {\n          // If named path response, try to parse it\n          hasNamedPathItem = true\n          let [nIds, eIds] = ParseEntitesFromNamedPathResponse(item)\n          nodeIds = new Set([...nodeIds, ...nIds])\n          edgeIds = new Set([...edgeIds, ...eIds])\n\n          npNodeIds = Array.from(new Set([...npNodeIds, ...nIds]))\n          npEdgeIds = Array.from(new Set([...npEdgeIds, ...eIds]))\n        } catch {\n          // maybe just a normal string\n        }\n      }\n    })\n  })\n\n  danglingEdgeIds = new Set([...edgeIds].filter((eId) => !nodeIds.has(eId)))\n\n  return {\n    headers,\n    nodes,\n    edges,\n    types,\n    labels,\n    nodeIds,\n    edgeIds,\n    hasNamedPathItem,\n    npNodeIds,\n    npEdgeIds,\n    danglingEdgeIds,\n  }\n}\n\nfunction ResultsParser(data: any[][]): { headers: any[]; results: any[] } {\n  if (data.length === 0) return { headers: [], results: [] }\n\n  const headers = data[0]\n  const records = data[1]\n\n  const results: any[] = []\n\n  records.forEach((record: any[]) => {\n    const result: any = {}\n    record.forEach((entity, i) => {\n      result[headers[i]] = {}\n      if (Array.isArray(entity)) {\n        if (entity[0][0] === 'id') {\n          const item: any = {\n            id: entity[0][1],\n            properties: {},\n          }\n          let propValues = []\n          if (entity[1][0] === 'labels') {\n            item.labels = entity[1][1]\n            propValues = entity[2][1]\n          } else if (entity[1][0] === 'type') {\n            item.type = entity[1][1]\n            item.source = entity[2][1]\n            item.target = entity[3][1]\n            propValues = entity[4][1]\n          }\n          propValues.map((x: any) => {\n            const v = resolveProps(x)\n            item['properties'][v.key] = v.value\n          })\n          result[headers[i]] = item.properties /* here */\n        } else {\n          result[headers[i]] = entity\n        }\n      } else {\n        result[headers[i]] = entity\n      }\n    })\n    results.push(result)\n  })\n  return {\n    headers,\n    results,\n  }\n}\n\nfunction isDigit(c: string): boolean {\n  return '0' <= c && c <= '9'\n}\n\nfunction parseDigit(resp: string, i: number): [string, number] {\n  let k = ''\n  while (isDigit(resp[i])) {\n    k += resp[i]\n    i += 1\n  }\n  return [k, i]\n}\n\nfunction ParseEntitesFromNamedPathResponse(resp: string): [string[], string[]] {\n  const EOF = -1\n  let tok = 0\n  let k = 1\n\n  let nodes: string[] = []\n  let edges: string[] = []\n\n  while (tok !== EOF) {\n    switch (resp[k]) {\n      case '(':\n        k += 1\n        let nodeId = ''\n        ;[nodeId, k] = parseDigit(resp, k)\n        if (nodeId !== '') nodes.push(nodeId)\n        else throw Error('Parse error: Unable to parse Node id')\n        k += 1\n        break\n      case ' ':\n        k += 1\n        break\n      case '[':\n        k += 1\n        let edgeId = ''\n        ;[edgeId, k] = parseDigit(resp, k)\n        if (edgeId !== '') edges.push(edgeId)\n        else throw Error('Parse error: Unable to parse Edge id')\n        k += 1\n        break\n      case ',':\n        k += 1\n        break\n      case ']':\n        if (k === resp.length - 1) {\n          tok = EOF\n        }\n        break\n      default:\n        throw Error('Parse error: Unknown')\n    }\n  }\n\n  return [nodes, edges]\n}\n\nexport { responseParser, ResultsParser, ParseEntitesFromNamedPathResponse }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/result.ts",
    "content": "export default [\n  {\n    status: 'success',\n    response:\n      'id=13091001001 addr=172.17.0.1:55380 fd=97 name=redisinsight-cli-bc31a9f9 age=2 idle=2 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=2147483647 obl=0 oll=0 omem=0 events=r cmd=CLIENT user=default\\r\\nid=6001002 addr=172.17.0.1:43276 fd=99 name=redisinsight-browser-808492c8 age=6561 idle=6561 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=2147483647 obl=0 oll=0 omem=0 events=r cmd=TTL user=default\\r\\nid=3765001002 addr=172.17.0.1:59014 fd=100 name=redisinsight-common-1239970f age=4681 idle=4681 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=2147483647 obl=0 oll=0 omem=0 events=r cmd=INFO user=default\\r\\n',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/styles/styles.scss",
    "content": "@charset \"UTF-8\";\n\n* {\n  margin: 0px;\n  padding: 0px;\n}\n\n.theme_DARK {\n  --info-background: #20222B;\n  --info-color: white;\n  --svg-background: #010101;\n  --tooltip-background: #3E4B5E;\n}\n\n.theme_LIGHT {\n  --info-background: white;\n  --info-color: black;\n  --svg-background: #FFFFFF;\n  --tooltip-background: white;\n}\n\n\n* div,\n* span {\n  font-family: 'Graphik', sans-serif !important;\n}\n\n.euiPagination__list {\n  list-style: none;\n  padding-inline-start: 0px;\n}\n\n\n.table-view {\n\n  // top-align-table json content\n  .euiTableRowCell  {\n    vertical-align: top;\n  }\n\n  // left-align table json content\n  .euiTableCellContent ul li ul li {\n    padding-left: 0 !important;\n  }\n\n}\n\n\n.neo4jd3-graph {\n}\n\n.core-container {\n}\n\n.d3-info {\n  margin-top: 12px !important;\n  margin-left: 24px !important;\n\n  font-size: 12px;\n  line-height: 18px;\n  padding: 10px;\n  position: absolute;\n\n  .info-component {\n    display: inline-block;\n    margin-top: 12px;\n    margin-left: -12px;\n    box-shadow: 0px 3px 12px #17336952;\n    border-radius: 4px;\n    opacity: 1;\n    background: var(--info-background);\n    color: var(--info-color);\n\n    .info-header {\n      display: flex;\n      justify-content: space-between;\n      padding-top: 12px;\n      padding-left: 12px;\n\n      .euiButtonIcon {\n        position: relative;\n\t      top: 0px !important;\n\t      right:6px !important;\n      }\n\n    }\n\n    .info-props {\n      display: grid;\n      grid-template-columns: auto auto;\n      gap: 5px 18px;\n      padding: 12px 18px 18px 18px\n    }\n  }\n\n  span {\n    border: 1px solid;\n    display: inline-block;\n    font-size: 14px;\n    line-height: 1.428571429;\n    margin-left: 5px;\n    margin-top: 5px;\n    padding: 6px 12px;\n\n    &.class {\n      color: white;\n    }\n\n    &.property {\n      background-color: #fff;\n      border-color: #ccc;\n      color: #333;\n    }\n  }\n\n  span.btn {\n    margin-left: 5px;\n    margin-top: 5px;\n    opacity: 1;\n  }\n\n  span.info {\n    box-shadow: 0px 0px 28px 0px;\n  }\n\n  .d3-info-labels {\n    display: flex;\n  }\n\n  .graph-legends {\n    display: flex;\n  }\n\n}\n\n.node {\n  &.node-highlighted {\n    .ring {\n      -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)';\n      filter: alpha(opacity=50);\n      opacity: .5;\n      stroke: #888;\n      stroke-width: 12px;\n    }\n  }\n\n  .outline {\n    cursor: pointer;\n    fill: rgb(165, 171, 182);\n    pointer-events: all;\n    stroke: rgb(154, 161, 172);\n    stroke-width: 2px;\n  }\n\n  .ring {\n    fill: none;\n    -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)';\n    filter: alpha(opacity=0);\n    opacity: 0;\n    stroke: #6ac6ff;\n    stroke-width: 8px;\n  }\n\n  .text {\n    &.icon {\n      font-family: FontAwesome;\n    }\n  }\n}\n\n.node.selected .ring,\n.node:hover .ring {\n  -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=30)';\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\n\n.relationship {\n\n  cursor: default;\n\n  line {\n    stroke: #aaa;\n  }\n\n  .outline {\n    cursor: default;\n  }\n\n  .overlay {\n    cursor: default;\n    fill: #6ac6ff;\n    -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)';\n    filter: alpha(opacity=0);\n    opacity: 0;\n  }\n\n  text {\n    cursor: default;\n  }\n}\n\n.relationship.selected .overlay,\n.relationship:hover .overlay {\n  -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=30)';\n  filter: alpha(opacity=50);\n  opacity: .5;\n}\n\n#graphd3 {\n  height: 100%;\n}\n\n.graphd3-graph {\n  cursor: move;\n  background-color: var(--svg-background);\n}\n\n\n.box-node-label {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-width: 46px;\n  height: 18px;\n  border-radius: 4px;\n  opacity: 1;\n  margin-right: 12px !important;\n  padding-right: 10px !important;\n  padding-left: 10px !important;\n}\n\n.box-edge-type {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  border-bottom: 2px solid !important;\n  padding-left: 5px !important;\n  padding-right: 5px !important;\n  margin-right: 12px !important;\n}\n\n\n.responseFail {\n  color: #e06c75;\n  padding: 12px !important;\n  font-family: monospace !important;\n}\n\n.responseInfo {\n  color: var(--info-color);\n  padding: 12px !important;\n  font-family: monospace !important;\n}\n\n.parsedRedisReply {\n  white-space: pre-wrap;\n  word-break: break-all;\n  font: normal normal normal 14px/17px Inconsolata !important;\n  padding: 4px 12px !important;\n}\n\n.euiToolTip {\n  color: var(--info-color) !important;\n  background-color: var(--tooltip-background) !important;\n  font-size: 12px !important;\n}\n\n.euiToolTip__arrow {\n  background-color: var(--tooltip-background) !important;\n}\n\n\n.automatic-edges-switch {\n  border-radius: 4px;\n  right: 4px;\n  position: absolute;\n  display: flex;\n  flex-direction: row;\n  color: var(--info-color);\n  margin-top: 12px !important;\n  margin-left: 24px !important;\n  font-size: 12px;\n  line-height: 18px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/src/utils.ts",
    "content": "import * as d3 from 'd3'\n\nexport function pulse(node: d3.Selection<SVGElement, any, any, any>) {\n  var times = 0\n  ;(function repeat() {\n    node\n      .transition()\n      .duration(100)\n      .attr('class', '')\n      .attr('data-pulse', 'true')\n      .attr('stroke', 'purple')\n      .attr('stroke-width', 0)\n      .attr('stroke-opacity', 0)\n      .transition()\n      .duration(500)\n      .attr('stroke-width', 0)\n      .attr('stroke-opacity', 1)\n      .transition()\n      .duration(1000)\n      .attr('stroke-width', 65)\n      .attr('stroke-opacity', 0)\n      .ease(d3.easeCubicInOut)\n      .on('end', () => {\n        if (times === 3) {\n          node.transition().attr('data-pulse', '').attr('class', 'outline')\n          return\n        }\n        times++\n        repeat()\n      })\n  })()\n}\nexport const toString = (d) => {\n  let s = d.labels ? d.labels[0] : d.type\n\n  s += ` (<id>:  ${d.id}`\n\n  Object.keys(d.properties).forEach((property) => {\n    s += `, ${property} : ${JSON.stringify(d.properties[property])}`\n  })\n\n  s += ')'\n\n  return s\n}\n\nexport const truncateText = (str = '', length = 100) => {\n  const ending = '...'\n\n  if (str.length > length) {\n    return str.substring(0, length - ending.length) + ending\n  }\n\n  return str\n}\n\nexport const rotate = (cx, cy, x, y, angle) => {\n  const radians = (Math.PI / 180) * angle\n  const cos = Math.cos(radians)\n  const sin = Math.sin(radians)\n  const nx = cos * (x - cx) + sin * (y - cy) + cx\n  const ny = cos * (y - cy) - sin * (x - cx) + cy\n\n  return { x: nx, y: ny }\n}\n\nexport const unitaryVector = (source, target, newLength) => {\n  const length =\n    Math.sqrt((target.x - source.x) ** 2 + (target.y - source.y) ** 2) /\n    Math.sqrt(newLength || 1)\n\n  return {\n    x: (target.x - source.x) / length,\n    y: (target.y - source.y) / length,\n  }\n}\n\nexport const darkenColor = (color) => d3.rgb(color).darker(1)\n\nfunction charCodeSum(str: string | undefined) {\n  if (str === undefined) return 0\n  let sum = 0\n  for (let i = 0; i < str.length; i++) {\n    sum += str.charCodeAt(i)\n  }\n  return sum\n}\n\nexport function invertColor(hex: string) {\n  if (hex.indexOf('#') === 0) {\n    hex = hex.slice(1)\n  }\n  // convert 3-digit hex to 6-digits.\n  if (hex.length === 3) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]\n  }\n  if (hex.length !== 6) {\n    throw new Error('Invalid HEX color.')\n  }\n  // invert color components\n  var r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16),\n    g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16),\n    b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16)\n  // pad each with zeros and return\n  return '#' + padZero(r) + padZero(g) + padZero(b)\n}\n\nfunction padZero(str) {\n  let len = str.length || 2\n  var zeros = new Array(len).join('0')\n  return (zeros + str).slice(-len)\n}\n\ninterface IColor {\n  color: string\n  textColor: string\n  borderColor: string\n}\n\nexport interface IGoodColor extends IColor {}\n\n/*\n * ColorPicker: Get colors based on `label`.\n */\nexport class ColorPicker<T extends IColor> {\n  // All the default colors are stored here.\n  private readonly colors: T[]\n\n  // store all the colors that are not taken by any label at a certain window.\n  private currentColorStore: T[]\n\n  // cache for label and its chosen color.\n  private labelStore: { [keyName: string]: T }\n\n  constructor(colors: T[]) {\n    this.colors = [...colors]\n    this.currentColorStore = [...colors]\n    this.labelStore = {}\n  }\n\n  /*\n   * Get a color object of type `T` based on `label`.\n   */\n  getColor(label: string) {\n    // if the label has been seen previously, return the stored color.\n    if (this.labelStore[label] !== undefined) {\n      return this.labelStore[label]\n    }\n\n    // if the current color store is empty, i.e., all the colors has\n    // been taken, so reset the color store to default color set.\n    if (this.currentColorStore.length === 0) {\n      this.currentColorStore = [...this.colors]\n    }\n    // get the color by hashing the label.\n    const goodColor =\n      this.currentColorStore[charCodeSum(label) % this.currentColorStore.length]\n\n    // since the color has been taken by `label`, remove it from the current color store.\n    this.currentColorStore = this.currentColorStore.filter(\n      (color) => color !== goodColor,\n    )\n\n    // cache the label and color key value pair.\n    this.labelStore[label] = goodColor\n    return goodColor\n  }\n}\n\n/*\n * GoodColorPicker: ColorPicker but only good colors.\n */\nexport class GoodColorPicker extends ColorPicker<IGoodColor> {\n  constructor(COLORS: IGoodColor[]) {\n    super(COLORS)\n  }\n}\n\nexport function wrapText(s: string, w: number) {\n  return s.replace(\n    new RegExp(`(?![^\\\\n]{1,${w}}$)([^\\\\n]{1,${w}})\\\\s`, 'g'),\n    '$1\\n',\n  )\n}\n\nexport function commandIsSuccess(resp: [{ response: any; status: string }]) {\n  return (\n    (Array.isArray(resp) && resp.length >= 1) || resp[0].status === 'success'\n  )\n}\n\nexport function getFetchNodesByIdQuery(\n  graphKey: string,\n  nodeIds: number[],\n): string {\n  return `graph.ro_query ${graphKey} \"MATCH (n) WHERE id(n) IN [${nodeIds}] RETURN DISTINCT n\"`\n}\n\nexport function getFetchNodesByEdgeIdQuery(\n  graphKey: string,\n  edgeIds: number[],\n  existingNodeIds: number[],\n): string {\n  return `graph.ro_query ${graphKey} \"MATCH (n)-[t]->(m) WHERE id(t) IN [${edgeIds}] AND NOT id(n) IN [${existingNodeIds}] AND NOT id(m) IN [${existingNodeIds}] RETURN n, m\"`\n}\n\nexport function getFetchEdgesByIdQuery(\n  graphKey: string,\n  edgeIds: number[],\n): string {\n  return `graph.ro_query ${graphKey} \"MATCH ()-[t]->() WHERE id(t) IN [${edgeIds}] RETURN DISTINCT t\"`\n}\n\nexport function getFetchDirectNeighboursOfNodeQuery(\n  graphKey: string,\n  nodeId: number,\n): string {\n  return `graph.ro_query \"${graphKey}\" \"MATCH (n)-[t]-(m) WHERE id(n)=${nodeId} RETURN t, m\"`\n}\n\nexport function getFetchNodeRelationshipsQuery(\n  graphKey: string,\n  sourceNodeIds: number[],\n  destNodeIds: number[],\n  existingEdgeIds: number[],\n): string {\n  return `graph.ro_query ${graphKey} \"MATCH (n)-[t]->(m) WHERE (ID(n) IN [${sourceNodeIds}] OR ID(m) IN [${destNodeIds}]) AND NOT ID(t) IN [${existingEdgeIds}] RETURN DISTINCT t\"`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisgraph/types/@elastic/index.d.ts",
    "content": "// yourLibrary.d.ts\n\ndeclare module '@elastic/eui/es/components/icon/icon' {\n  export function appendIconComponentCache(args: any): any\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/magnifyWithPlus' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/magnifyWithMinus' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/bullseye' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/editorItemAlignLeft' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/editorItemAlignRight' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/editorItemAlignCenter' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/arrow_left' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/arrow_right' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/arrow_down' {\n  export { icon }\n}\n\ndeclare module '@elastic/eui/es/components/icon/assets/cross' {\n  export { icon }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/.gitignore",
    "content": "node_modules\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# OS\n.DS_Store\n\n# Tests\n/coverage\n/.nyc_output\n\n# IDEs and editors\n/.idea\n.idea/\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n*.css.d.ts\n*.sass.d.ts\n*.scss.d.ts\n**/*.scss.d.ts\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Redis, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/README.md",
    "content": "# RedisInsight-plugin-sdk\n\nThe high-level API for communication between Redis Insight\nplugin and Redis Insight application.\n\n## Usage\n\n```\nnpm install redisinsight-plugin-sdk\nor\nyarn add redisinsight-plugin-sdk\n```\n\n## Available methods\n\n### setHeaderText(text)\n\nSets any custom text to the header of the command result\n\n**Parameters:**\n\n- `text` **{String}**\n\n**Example:**\n\n```js\nimport { setHeaderText } from 'redisinsight-plugin-sdk';\nsetHeaderText('Matched: 10');\n```\n\n### executeRedisCommand(command)\n\nExecutes a Redis command _(currently, only read-only commands are supported)_.\n\n**Parameters:**\n\n- `command` **{String}** - command to execute\n\n**Returns:**\n\n- `Promise<[{ response, status }]>`\n\n```js\n/**\n * @async\n * @param {String} command\n * @returns {Promise.<[{ response, status }]>}\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { executeRedisCommand } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await executeRedisCommand('GET foo');\n  const [{ response, status }] = result;\n  if (status === 'success') {\n    // Do smth\n  }\n} catch (e) {\n  console.error(e);\n}\n```\n\n### getState()\n\nReturns saved state for the command visualization.\n\nThrow an error if the state has not been saved.\n\n**Parameters:**\n\n- `state` **{any}** - any data to save\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { getState } from 'redisinsight-plugin-sdk';\ntry {\n  const result = await getState();\n} catch (e) {\n  console.error(e);\n}\n```\n\n### setState(state)\n\nSave the state for the command visualization.\n\n**Returns:**\n\n- `Promise<any>`\n\n```js\n/**\n * @async\n * @param {any} state\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { setState } from 'redisinsight-plugin-sdk';\ntry {\n  await setState({ a: 1, b: 2 });\n} catch (e) {\n  console.error(e);\n}\n```\n\n### formatRedisReply(response, command)\n\nUtil function to parse Redis response\n\nReturns string with parsed cli-like response\n\n**Returns:**\n\n- `Promise<string>`\n\n```js\n/**\n * @async\n * @param {any} response\n * @param {String} command\n * @returns {Promise.<string>} data\n * @throws {Error}\n */\n```\n\n**Example:**\n\n```js\nimport { formatRedisReply } from 'redisinsight-plugin-sdk';\n\ntry {\n  const parsedReply = await formatRedisReply(data[0].response, command);\n\n  /*\n    parsedReply:\n    \n    1) 1) \"COUNT(a)\"\n    2) 1) 1) \"0\"\n    3) 1) \"Cached execution: 1\"\n       2) \"Query internal execution time: 3.134125 milliseconds\"\n   */\n} catch (e) {\n  console.error(e);\n}\n```\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/events.js",
    "content": "export const POST_MESSAGE_EVENTS = {\n  setHeaderText: 'setHeaderText',\n  executeRedisCommand: 'executeRedisCommand',\n  getState: 'getState',\n  setState: 'setState',\n  formatRedisReply: 'formatRedisReply',\n};\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/helpers.js",
    "content": "/**\n * helper function to send postMessage to the main app\n *\n * @param data\n */\nexport const sendMessageToMain = (data = {}) => {\n  const event = document.createEvent('Event');\n  event.initEvent('message', true, true);\n  event.data = data;\n  event.origin = '*';\n  // eslint-disable-next-line no-restricted-globals\n  parent.dispatchEvent(event);\n};\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/index.d.ts",
    "content": "/**\n * Set text to the header result\n *\n */\nexport function setHeaderText(text?: string): void\n\n/**\n * Execute Read-only Redis Command\n *\n */\nexport function executeRedisCommand(\n  command?: string,\n): Promise<[{ response: any; status: string }]>\n\n/**\n * Set state for the plugin\n *\n */\nexport function setState<State>(state?: State): Promise<State>\n\n/**\n * Returns the current state\n *\n */\nexport function getState(): Promise<any>\n\n/**\n * Parse Redis response\n * Returns string with parsed cli-like response\n *\n */\nexport function formatRedisReply(\n  response: any,\n  command?: string,\n): Promise<string>\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/index.js",
    "content": "import { sendMessageToMain } from './helpers';\nimport { POST_MESSAGE_EVENTS } from './events';\n\nconst { config, callbacks = { counter: 0 } } = window.state || {};\nconst { iframeId } = config || {};\n\n/**\n * Set text to the header result\n *\n * @param {string} text\n */\nexport const setHeaderText = (text = '') => {\n  sendMessageToMain({\n    event: POST_MESSAGE_EVENTS.setHeaderText,\n    iframeId,\n    text,\n  });\n};\n\n/**\n * Execute Read-only Redis Command\n *\n * @async\n * @param {String} command\n * @returns {Promise.<[{ response, status }]>}\n * @throws {Error}\n */\nexport const executeRedisCommand = (command = '') =>\n  new Promise((resolve, reject) => {\n    callbacks[callbacks.counter] = { resolve, reject };\n    sendMessageToMain({\n      event: POST_MESSAGE_EVENTS.executeRedisCommand,\n      iframeId,\n      command,\n      requestId: callbacks.counter++,\n    });\n  });\n\n/**\n * Returns the current state\n *\n * @async\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\nexport const getState = () =>\n  new Promise((resolve, reject) => {\n    callbacks[callbacks.counter] = { resolve, reject };\n    sendMessageToMain({\n      event: POST_MESSAGE_EVENTS.getState,\n      iframeId,\n      requestId: callbacks.counter++,\n    });\n  });\n\n/**\n * Set state for the plugin\n *\n * @async\n * @param {any} state\n * @returns {Promise.<any>} state\n * @throws {Error}\n */\nexport const setState = (state) =>\n  new Promise((resolve, reject) => {\n    callbacks[callbacks.counter] = { resolve, reject };\n    sendMessageToMain({\n      event: POST_MESSAGE_EVENTS.setState,\n      iframeId,\n      state,\n      requestId: callbacks.counter++,\n    });\n  });\n\n/**\n * Parse Redis response\n * Returns string with parsed cli-like response\n *\n * @async\n * @param {any} response\n * @param {String} command\n * @returns {Promise.<string>} data\n * @throws {Error}\n */\nexport const formatRedisReply = (response, command = '') =>\n  new Promise((resolve, reject) => {\n    callbacks[callbacks.counter] = { resolve, reject };\n    sendMessageToMain({\n      event: POST_MESSAGE_EVENTS.formatRedisReply,\n      iframeId,\n      data: { response, command },\n      requestId: callbacks.counter++,\n    });\n  });\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redisinsight-plugin-sdk/package.json",
    "content": "{\n  \"name\": \"redisinsight-plugin-sdk\",\n  \"description\": \"Redis API for creating packages for Redis v.2 application\",\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"redis\",\n    \"redis-gui\"\n  ],\n  \"version\": \"1.1.0\",\n  \"author\": {\n    \"name\": \"Redis\",\n    \"email\": \"redisinsight@redis.com\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/RedisInsight/RedisInsight/tree/main/redisinsight/ui/src/packages/redisinsight-plugin-sdk\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/RedisInsight/RedisInsight/issues\"\n  },\n  \"main\": \"index.js\"\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/.gitignore",
    "content": "node_modules/\ndist/\n.parcel-cache/"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/README.md",
    "content": "# RedisTimeseries Plugin for Redis Insight\n\nThe example has been created using React, TypeScript, and [Elastic UI](https://elastic.github.io/eui/#/).\n[Parcel](https://parceljs.org/) is used to build the plugin.\n\n## Running locally\n\nThe following commands will install dependencies and start the server to run the plugin locally:\n\n```\nyarn\nyarn start\n```\n\nThese commands will install dependencies and start the server.\n\n_Note_: Base styles are included to `index.html` from the repository.\n\nThis command will generate the `vendor` folder with styles and fonts of the core app. Add this folder\ninside the folder for your plugin and include appropriate styles to the `index.html` file.\n\n```\nyarn build:statics - for Linux or MacOs\nyarn build:statics:win - for Windows\n```\n\n## Build plugin\n\nThe following commands will build plugins to be used in Redis Insight:\n\n```\nyarn\nyarn build\n```\n\n[Add](../../../../../docs/plugins/installation.md) the package.json file and the\n`dist` folder to the folder with your plugin, which should be located in the `plugins` folder.\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/global.d.ts",
    "content": "declare module '@elastic/eui/es/components/icon/*'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>RedisTimeseries plugin</title>\n\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n    <!-- Run Conditions-->\n    <% if(isDev){ %>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/global_styles.css\"\n    />\n    <!-- <link rel=\"stylesheet\" href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/dark_theme.css\" /> -->\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/light_theme.css\"\n    />\n    <% } %>\n  </head>\n  <body class=\"theme_LIGHT\">\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/jest.config.js",
    "content": "/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */\nmodule.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n};\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/mockData/resultTimeSeries.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      [1643846400000, \"4604.83\"],\n      [1644019200000, \"3924.99\"],\n      [1644364800000, \"4653.02\"],\n      [1644624000000, \"4171.72\"]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"RedisTimeseries module\",\n  \"source\": \"./src/main.tsx\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"redistimeseries\",\n  \"version\": \"0.0.2\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\"\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"redistimeseries-chart\",\n      \"name\": \"Chart\",\n      \"activationMethod\": \"renderChart\",\n      \"matchCommands\": [\n        \"TS.MRANGE\",\n        \"TS.MREVRANGE\",\n        \"TS.RANGE\",\n        \"TS.REVRANGE\"\n      ],\n      \"description\": \"Redistimeseries chart view\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {\n    \"vite\": \"file:../node_modules/vite\"\n  },\n  \"dependencies\": {\n    \"@elastic/eui\": \"34.6.0\",\n    \"@emotion/react\": \"^11.7.1\",\n    \"classnames\": \"^2.3.1\",\n    \"date-fns\": \"^2.28.0\",\n    \"file-saver\": \"^2.0.5\",\n    \"fscreen\": \"^1.2.0\",\n    \"plotly.js-dist-min\": \"^2.9.0\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\"\n  },\n  \"resolutions\": {\n    \"jest/**/micromatch\": \"^4.0.8\",\n    \"trim\": \"0.0.3\",\n    \"**/cross-spawn\": \"^7.0.5\",\n    \"@elastic/eui/**/prismjs\": \"~1.30.0\",\n    \"**/semver\": \"^7.5.2\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/App.tsx",
    "content": "import React from 'react'\nimport ChartResultView from './components/Chart/ChartResultView'\n\ninterface Props {\n  command: string\n  result?: { response: any; status: string }[]\n}\n\nenum TsCmdRangePrefix {\n  RANGE = 'TS.RANGE',\n  REVRANGE = 'TS.REVRANGE',\n}\n\nconst App = (props: Props) => {\n  const { result: [{ response = '', status = '' } = {}] = [] } = props\n\n  if (status === 'fail') {\n    return <div className=\"responseFail\">{response}</div>\n  }\n\n  if (status === 'success' && typeof response === 'string') {\n    return <div className=\"responseFail\">{response}</div>\n  }\n\n  function responseParser(command: string, data: any) {\n    let [cmd, key] = command.split(' ')\n\n    if (\n      [\n        TsCmdRangePrefix.RANGE.toString(),\n        TsCmdRangePrefix.REVRANGE.toString(),\n      ].includes(cmd.toUpperCase())\n    ) {\n      return [\n        {\n          key,\n          datapoints: data,\n        },\n      ]\n    }\n\n    return data.map((e: any[]) => ({\n      key: e[0],\n      labels: e[1],\n      datapoints: e[2],\n    }))\n  }\n\n  return (\n    <ChartResultView\n      data={responseParser(props.command, props.result?.[0].response) as any}\n    />\n  )\n}\n\nexport default App\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/Chart.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport Plotly from 'plotly.js-dist-min'\nimport {\n  Layout,\n  LayoutAxis,\n  Legend,\n  PlotData,\n  PlotMouseEvent,\n  PlotRelayoutEvent,\n} from 'plotly.js'\nimport { format } from 'date-fns'\nimport {\n  COLORS,\n  COLORS_DARK,\n  GoodColorPicker,\n  hexToRGBA,\n  IGoodColor,\n} from './utils'\n\nimport { Datapoint, GraphMode, ChartProps, PlotlyEvents } from './interfaces'\n\nconst GRAPH_MODE_MAP: { [mode: string]: 'lines' | 'markers' } = {\n  [GraphMode.line]: 'lines',\n  [GraphMode.points]: 'markers',\n}\n\nconst isDarkTheme = document.body.classList.contains('theme_DARK')\n\nconst colorPicker = (COLORS: IGoodColor[]) => {\n  const color = new GoodColorPicker(COLORS)\n  return (label: string) => color.getColor(label).color\n}\n\nconst labelColors = colorPicker(isDarkTheme ? COLORS_DARK : COLORS)\n\nexport default function Chart(props: ChartProps) {\n  const chartContainer = useRef<any>()\n\n  const colorPicker = labelColors\n\n  useEffect(() => {\n    Plotly.newPlot(chartContainer.current, getData(props), getLayout(props), {\n      displayModeBar: false,\n      autosizable: true,\n      responsive: true,\n      setBackground: () => 'transparent',\n    })\n    chartContainer.current.on(\n      PlotlyEvents.PLOTLY_HOVER,\n      function (eventdata: PlotMouseEvent) {\n        const points = eventdata.points[0]\n        const pointNum = points.pointNumber\n        Plotly.Fx.hover(\n          chartContainer.current,\n          props.data.map((_, i) => ({\n            curveNumber: i,\n            pointNumber: pointNum,\n          })),\n          Object.keys(chartContainer.current._fullLayout._plots),\n        )\n      },\n    )\n    chartContainer.current.on(\n      PlotlyEvents.PLOTLY_RELAYOUT,\n      function (eventdata: PlotRelayoutEvent) {\n        if (\n          eventdata.autosize === undefined &&\n          eventdata['xaxis.autorange'] === undefined\n        ) {\n          props.onRelayout()\n        }\n      },\n    )\n\n    chartContainer.current.on(PlotlyEvents.PLOTLY_DBLCLICK, () =>\n      props.onDoubleClick(),\n    )\n  }, [props.chartConfig])\n\n  function getData(props: ChartProps): Partial<PlotData>[] {\n    return props.data.map((timeSeries) => {\n      /*\n       * Time format for inclusion of milliseconds:\n       * https://github.com/moment/moment/issues/4864#issuecomment-440142542\n       */\n      const x = selectCol(timeSeries.datapoints, 0).map((time: number) =>\n        format(time, 'yyyy-MM-dd HH:mm:ss.SSS'),\n      )\n      const y = selectCol(timeSeries.datapoints, 1)\n\n      return {\n        x,\n        y,\n        yaxis:\n          props.chartConfig.yAxis2 &&\n          props.chartConfig.keyToY2Axis[timeSeries.key]\n            ? 'y2'\n            : 'y',\n        name: timeSeries.key,\n        type: 'scatter',\n        marker: { color: colorPicker(timeSeries.key) },\n        fill: props.chartConfig.fill ? 'tozeroy' : undefined,\n        fillcolor: hexToRGBA(colorPicker(timeSeries.key), 0.3),\n        mode: GRAPH_MODE_MAP[props.chartConfig.mode],\n        line: { shape: props.chartConfig.staircase ? 'hv' : 'spline' },\n      }\n    })\n  }\n\n  function getLayout(props: ChartProps): Partial<Layout> {\n    const axisConfig: { [key: string]: Partial<LayoutAxis> } = {\n      xaxis: {\n        title: props.chartConfig.xlabel,\n        rangeslider: {\n          visible: true,\n          thickness: 0.03,\n          bgcolor: isDarkTheme ? '#3D3D3D' : '#CDD7EA',\n          bordercolor: 'red',\n        },\n        color: isDarkTheme ? '#898A90' : '#527298',\n      },\n      yaxis: {\n        title: props.chartConfig.yAxisConfig.label,\n        type: props.chartConfig.yAxisConfig.scale,\n        fixedrange: true,\n        color: isDarkTheme ? '#898A90' : '#527298',\n        gridcolor: isDarkTheme ? '#898A90' : '#527298',\n      },\n      yaxis2: {\n        visible: props.chartConfig.yAxis2,\n        title: props.chartConfig.yAxis2Config.label,\n        type: props.chartConfig.yAxis2Config.scale,\n        overlaying: 'y',\n        side: 'right',\n        fixedrange: true,\n        color: isDarkTheme ? '#8191CF' : '#6E6E6E',\n        gridcolor: isDarkTheme ? '#8191CF' : '#6E6E6E',\n      } as LayoutAxis,\n    }\n\n    const legend: Partial<Legend> = {\n      xanchor: 'center',\n      yanchor: 'top',\n      x: 0.5,\n      y: -0.5,\n      orientation: 'h',\n    }\n    return {\n      ...axisConfig,\n      legend,\n      showlegend: true,\n      title: props.chartConfig.title,\n      uirevision: 1,\n      autosize: true,\n      font: { color: isDarkTheme ? 'darkgrey' : 'black' },\n      paper_bgcolor: 'rgba(0,0,0,0)',\n      plot_bgcolor: 'rgba(0,0,0,0)',\n      margin: {\n        pad: 6,\n      },\n    }\n  }\n\n  function selectCol(twoDArray: Datapoint[], colIndex: number) {\n    return twoDArray.map((arr) => arr[colIndex])\n  }\n\n  return <div ref={chartContainer}></div>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/ChartConfigForm.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { SwitchInput, TextInput } from 'uiSrc/components/base/inputs'\nimport { FormFieldset } from 'uiSrc/components/base/forms/fieldset'\nimport {\n  AxisScale,\n  GraphMode,\n  ChartConfigFormProps,\n  TimeUnit,\n} from './interfaces'\nimport {\n  X_LABEL_MAX_LENGTH,\n  Y_LABEL_MAX_LENGTH,\n  TITLE_MAX_LENGTH,\n} from './constants'\nimport { RiAccordion } from 'uiSrc/components/base/display/accordion/RiAccordion'\nimport {\n  ButtonGroup,\n  ButtonGroupProps,\n} from 'uiSrc/components/base/forms/button-group/ButtonGroup'\n\nconst NewEnumSelect = ({\n  selected,\n  values,\n  onClick,\n}: {\n  selected: string\n  values: string[]\n  onClick: (v: string) => void\n}) => (\n  <div className=\"new-button\">\n    {values.map((v) => (\n      <div\n        title={v.charAt(0).toUpperCase() + v.slice(1)}\n        onClick={() => onClick(v)}\n        className={`button-point ${selected === v ? 'button-selected' : null}`}\n        key={v}\n      >\n        {v}\n      </div>\n    ))}\n  </div>\n)\n\nexport default function ChartConfigForm(props: ChartConfigFormProps) {\n  const [moreOptions, setMoreOptions] = useState(false)\n\n  const { onChange, value } = props\n\n  const yAxisButtonGroupItems = [\n    {\n      label: 'Left',\n      value: false,\n    },\n    {\n      label: 'Right',\n      value: true,\n    },\n  ]\n\n  return (\n    <form className=\"chart-config-form\">\n      <div className=\"chart-form-top\">\n        <NewEnumSelect\n          values={Object.keys(GraphMode)}\n          selected={value.mode}\n          onClick={(v) => onChange('mode', v)}\n        />\n        <NewEnumSelect\n          values={Object.keys(TimeUnit)}\n          selected={value.timeUnit}\n          onClick={(v) => onChange('timeUnit', v)}\n        />\n        <SwitchInput\n          title=\"Staircase\"\n          checked={value.staircase}\n          onCheckedChange={(checked) => onChange('staircase', checked)}\n        />\n        <SwitchInput\n          title=\"Fill\"\n          checked={value.fill}\n          onCheckedChange={(checked) => onChange('fill', checked)}\n        />\n      </div>\n      <RiAccordion\n        className=\"chart-form-accordion\"\n        collapsible\n        open={moreOptions}\n        onOpenChange={setMoreOptions}\n        label={moreOptions ? 'Less options' : 'More options'}\n        content={\n          <div className=\"more-options\">\n            <section>\n              <FormFieldset legend={{ children: 'Title' }}>\n                <TextInput\n                  placeholder=\"Title\"\n                  value={value.title}\n                  onChange={(value) => onChange('title', value)}\n                  aria-label=\"Title\"\n                  maxLength={parseInt(TITLE_MAX_LENGTH)}\n                />\n              </FormFieldset>\n              <FormFieldset legend={{ children: 'X axis Label' }}>\n                <TextInput\n                  placeholder=\"X axis label\"\n                  value={value.xlabel}\n                  onChange={(value) => onChange('xlabel', value)}\n                  aria-label=\"X Label\"\n                  maxLength={parseInt(X_LABEL_MAX_LENGTH)}\n                />\n              </FormFieldset>\n            </section>\n            <section>\n              <div className=\"right-y-axis\">\n                <div className=\"switch-wrapper\">\n                  <SwitchInput\n                    title=\"Use Right Y Axis\"\n                    checked={value.yAxis2}\n                    onCheckedChange={(checked) => onChange('yAxis2', checked)}\n                  />\n                </div>\n                {value.yAxis2 && (\n                  <div className=\"y-axis-2\">\n                    {Object.keys(value.keyToY2Axis).map((key) => (\n                      <div className=\"y-axis-2-item\" key={key}>\n                        <div>{key}</div>\n                        <ButtonGroup>\n                          {yAxisButtonGroupItems.map((item) => (\n                            <ButtonGroup.Button\n                              isSelected={value.keyToY2Axis[key] === item.value}\n                              onClick={() =>\n                                onChange('keyToY2Axis', {\n                                  ...value.keyToY2Axis,\n                                  [key]: item.value,\n                                })\n                              }\n                            >\n                              {item.label}\n                            </ButtonGroup.Button>\n                          ))}\n                        </ButtonGroup>\n                      </div>\n                    ))}\n                  </div>\n                )}\n              </div>\n            </section>\n            <section className=\"y-axis-config\">\n              <YAxisConfigForm\n                label=\"Left Y Axis\"\n                onChange={(v: any) => onChange('yAxisConfig', v)}\n                isLeftYAxis={true}\n                value={value.yAxisConfig}\n              />\n              {value.yAxis2 && (\n                <YAxisConfigForm\n                  label=\"Right Y Axis\"\n                  onChange={(v: any) => onChange('yAxis2Config', v)}\n                  isLeftYAxis={false}\n                  value={value.yAxis2Config}\n                />\n              )}\n            </section>\n          </div>\n        }\n      />\n    </form>\n  )\n}\n\nconst YAxisConfigForm = ({ value, onChange, label }: any) => (\n  <div>\n    <FormFieldset legend={{ children: `${label} Label` }}>\n      <TextInput\n        placeholder=\"Label\"\n        value={value.label}\n        onChange={(value) => onChange({ ...value, label: value })}\n        aria-label=\"label\"\n        maxLength={parseInt(Y_LABEL_MAX_LENGTH)}\n      />\n    </FormFieldset>\n    <FormFieldset legend={{ children: `${label} Scale` }}>\n      <EnumSelect\n        inputLabel=\"Scale\"\n        onChange={(e) =>\n          onChange({ ...value, scale: e.target.value as string })\n        }\n        value={value.scale}\n        enumType={AxisScale}\n      />\n    </FormFieldset>\n  </div>\n)\n\nconst capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1)\n\ninterface EnumSelectProps {\n  enumType: any\n  inputLabel: string\n  value: string\n}\nconst EnumSelect = ({\n  enumType,\n  inputLabel,\n  ...props\n}: EnumSelectProps & ButtonGroupProps) => (\n  <ButtonGroup>\n    {Object.values(enumType).map((v) => (\n      <ButtonGroup.Button\n        isSelected={props.value === v}\n        key={String(v)}\n        onClick={() =>\n          props.onChange?.({ target: { value: String(v) } } as any)\n        }\n      >\n        {capitalize(String(v))}\n      </ButtonGroup.Button>\n    ))}\n  </ButtonGroup>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/ChartResultView.tsx",
    "content": "import React, { useMemo, useState } from 'react'\nimport {\n  AxisScale,\n  ChartConfig,\n  GraphMode,\n  TimeSeries,\n  YAxisConfig,\n} from './interfaces'\nimport ChartConfigForm from './ChartConfigForm'\nimport Chart from './Chart'\nimport { determineDefaultTimeUnits, normalizeDatapointUnits } from './utils'\n\nenum LAYOUT_STATE {\n  INITIAL_STATE,\n  RELAYOUT_STATE,\n}\n\ninterface ChartResultViewProps {\n  data: TimeSeries[]\n}\n\nexport default function ChartResultView(props: ChartResultViewProps) {\n  const defaultYAxisConfig: YAxisConfig = { label: '', scale: AxisScale.linear }\n  const keyToY2AxisDefault = props.data.reduce(\n    (keyToYAxis: any, timeSeries) => {\n      keyToYAxis[timeSeries.key] = false\n      return keyToYAxis\n    },\n    {},\n  )\n\n  const [chartConfig, setChartConfig] = useState<ChartConfig>({\n    mode: GraphMode.line,\n    timeUnit: determineDefaultTimeUnits(props.data),\n    title: '',\n    xlabel: '',\n    staircase: false,\n    fill: true,\n    yAxis2: false,\n    keyToY2Axis: keyToY2AxisDefault,\n    yAxisConfig: defaultYAxisConfig,\n    yAxis2Config: defaultYAxisConfig,\n  })\n  const [chartState, setChartState] = useState<LAYOUT_STATE>(\n    LAYOUT_STATE.INITIAL_STATE,\n  )\n\n  function handleChartConfigChanged(control: string, value: any) {\n    onChartConfigChange(control, value)\n    if (chartState !== LAYOUT_STATE.INITIAL_STATE) {\n      setChartState(LAYOUT_STATE.INITIAL_STATE)\n    }\n  }\n\n  function onChartConfigChange(control: string, value: any) {\n    setChartConfig({ ...chartConfig, [control]: value })\n  }\n\n  function onRelayout() {\n    if (chartState !== LAYOUT_STATE.RELAYOUT_STATE) {\n      setChartState(LAYOUT_STATE.RELAYOUT_STATE)\n    }\n  }\n\n  function onDoubleClick() {\n    if (chartState !== LAYOUT_STATE.INITIAL_STATE) {\n      setChartState(LAYOUT_STATE.INITIAL_STATE)\n    }\n  }\n\n  const memoizedChartData = useMemo(\n    () => normalizeDatapointUnits(props.data, chartConfig.timeUnit),\n    [props.data, chartConfig.timeUnit],\n  )\n\n  return (\n    <div>\n      <div className=\"zoom-helper-text\">\n        <i>\n          {chartState === LAYOUT_STATE.INITIAL_STATE\n            ? 'Drag over the part of the chart to zoom into it'\n            : 'Double click on the graph to reset the view'}\n        </i>\n      </div>\n      <Chart\n        chartConfig={chartConfig}\n        data={memoizedChartData}\n        onRelayout={onRelayout}\n        onDoubleClick={onDoubleClick}\n      />\n      <div className=\"config-form-wrapper\">\n        <ChartConfigForm\n          value={chartConfig}\n          onChange={handleChartConfigChanged}\n        />\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/constants.ts",
    "content": "export const X_LABEL_MAX_LENGTH = '60'\nexport const Y_LABEL_MAX_LENGTH = '30'\nexport const TITLE_MAX_LENGTH = '60'\nexport const DOWNLOAD_IMAGE_WIDTH = 1366\nexport const DOWNLOAD_IMAGE_HEIGHT = 400\nexport const DOWNLOAD_IMAGE_FILENAME = 'redistimeseries_chart'\n\nexport const DOWNLOAD_CSV_FILENAME = 'redistimeseries_data.csv'\nexport const TIMESERIES_HISTORY_CONTAINER_NAME = 'REDIS_TIMESERIES'\n\nexport const AUTO_UPDATE_TIMER_DEFAULT_VALUE = 5000 // time in milliseconds\nexport const AUTO_UPDATE_NUM_SAMPLES_DEFAULT_VALUE = 50\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/interfaces.ts",
    "content": "export type Datapoint = [number, string]\n\nexport interface TimeSeries {\n  key: string\n  labels: { [labelName: string]: string }\n  datapoints: Datapoint[]\n}\n\nexport interface TimeSeriesQueryResult {\n  query: string\n  result?: TimeSeries[]\n  msg?: string // msg will be present in case of error\n}\n\nexport interface TimeSeriesError {\n  msg: string\n  alt?: string[]\n}\n\nexport enum TimeUnit {\n  seconds = 'seconds',\n  milliseconds = 'milliseconds',\n}\n\nexport enum GraphMode {\n  line = 'line',\n  points = 'points',\n}\n\nexport enum AxisScale {\n  linear = 'linear',\n  log = 'log',\n}\n\nexport interface YAxisConfig {\n  label: string\n  scale: AxisScale\n}\n\nexport interface ChartConfig {\n  mode: GraphMode\n  timeUnit: TimeUnit\n  xlabel: string\n  title: string\n  staircase: boolean\n  fill: boolean\n  yAxis2: boolean\n  keyToY2Axis: { [keyName: string]: boolean }\n  yAxisConfig: YAxisConfig\n  yAxis2Config: YAxisConfig\n}\n\nexport type ChartImageExportOption = 'png' | 'svg'\n\nexport interface ChartProps {\n  data: TimeSeries[]\n  chartConfig: ChartConfig\n  onRelayout: () => void\n  onDoubleClick: () => void\n}\n\nexport enum PlotlyEvents {\n  PLOTLY_HOVER = 'plotly_hover',\n  PLOTLY_RELAYOUT = 'plotly_relayout',\n  PLOTLY_DBLCLICK = 'plotly_doubleclick',\n}\n\nexport interface ChartConfigFormProps {\n  value: ChartConfig\n  onChange: (control: string, value: any) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/utils.test.ts",
    "content": "import { hexToRGBA, normalizeDatapointUnits, determineDefaultTimeUnits } from './utils'\nimport { TimeSeries, TimeUnit } from './interfaces'\n\nconst hexToRGBATests: [string, string][] = [\n  ['#fbafff', 'rgb(251, 175, 255)'],\n  ['#af087b', 'rgb(175, 8, 123)'],\n  ['#0088ff', 'rgb(0, 136, 255)'],\n  ['#123456', 'rgb(18, 52, 86)'],\n  ['#FF0000', 'rgb(255, 0, 0)'],\n  ['#345465', 'rgb(52, 84, 101)'],\n]\n\ndescribe('hexToRGBA', () => {\n  it.each(hexToRGBATests)(\n    'for input hex: %s, should be output: %s',\n    (hex, expected) => {\n      const result = hexToRGBA(hex, 0)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\n\nconst normalizeDatapointUnitsTests: [TimeSeries[], TimeUnit, TimeSeries[]][] = [\n  // Test with seconds - should multiply timestamps by 1000\n  [\n    [\n      {\n        key: 'test-key-1',\n        labels: { app: 'redis' },\n        datapoints: [[1690000000, 'value1'], [1690000001, 'value2']]\n      }\n    ],\n    TimeUnit.seconds,\n    [\n      {\n        key: 'test-key-1',\n        labels: { app: 'redis' },\n        datapoints: [[1690000000000, 'value1'], [1690000001000, 'value2']]\n      }\n    ]\n  ],\n  // Test with milliseconds - should return unchanged\n  [\n    [\n      {\n        key: 'test-key-2',\n        labels: { service: 'cache' },\n        datapoints: [[1690000000000, 'value1'], [1690000001000, 'value2']]\n      }\n    ],\n    TimeUnit.milliseconds,\n    [\n      {\n        key: 'test-key-2',\n        labels: { service: 'cache' },\n        datapoints: [[1690000000000, 'value1'], [1690000001000, 'value2']]\n      }\n    ]\n  ],\n  // Test with multiple time series and seconds\n  [\n    [\n      {\n        key: 'series-1',\n        labels: { type: 'memory' },\n        datapoints: [[1690000000, 'mem1'], [1690000002, 'mem2']]\n      },\n      {\n        key: 'series-2',\n        labels: { type: 'cpu' },\n        datapoints: [[1690000001, 'cpu1'], [1690000003, 'cpu2']]\n      }\n    ],\n    TimeUnit.seconds,\n    [\n      {\n        key: 'series-1',\n        labels: { type: 'memory' },\n        datapoints: [[1690000000000, 'mem1'], [1690000002000, 'mem2']]\n      },\n      {\n        key: 'series-2',\n        labels: { type: 'cpu' },\n        datapoints: [[1690000001000, 'cpu1'], [1690000003000, 'cpu2']]\n      }\n    ]\n  ],\n  // Test with empty datapoints\n  [\n    [\n      {\n        key: 'empty-series',\n        labels: {},\n        datapoints: []\n      }\n    ],\n    TimeUnit.seconds,\n    [\n      {\n        key: 'empty-series',\n        labels: {},\n        datapoints: []\n      }\n    ]\n  ]\n]\n\ndescribe('normalizeDatapointUnits', () => {\n  it.each(normalizeDatapointUnitsTests)(\n    'should normalize datapoint units for input: %j with unit: %s',\n    (timeSeries, unit, expected) => {\n      const result = normalizeDatapointUnits(timeSeries, unit)\n      expect(result).toEqual(expected)\n    },\n  )\n\n  it('should return original data when an error occurs', () => {\n    // Create a scenario that might cause an error by passing invalid data\n    const invalidTimeSeries = null as any\n    const result = normalizeDatapointUnits(invalidTimeSeries, TimeUnit.seconds)\n    \n    expect(result).toBe(invalidTimeSeries)\n  })\n})\n\nconst determineDefaultTimeUnitsTests: [TimeSeries[], TimeUnit][] = [\n  // Test with timestamp in milliseconds (> 1e10)\n  [\n    [\n      {\n        key: 'test-key-1',\n        labels: { app: 'redis' },\n        datapoints: [[1690000000000, 'value1']]\n      }\n    ],\n    TimeUnit.milliseconds\n  ],\n  // Test with timestamp in seconds (< 1e10)\n  [\n    [\n      {\n        key: 'test-key-2',\n        labels: { app: 'redis' },\n        datapoints: [[1690000000, 'value1']]\n      }\n    ],\n    TimeUnit.seconds\n  ],\n  // Test with exactly 1e10 (boundary case - should be seconds)\n  [\n    [\n      {\n        key: 'boundary-key',\n        labels: { type: 'boundary' },\n        datapoints: [[1e10, 'boundary-value']]\n      }\n    ],\n    TimeUnit.seconds\n  ],\n  // Test with timestamp much larger than 1e10\n  [\n    [\n      {\n        key: 'large-timestamp',\n        labels: { type: 'large' },\n        datapoints: [[1690000000000000, 'large-value']]\n      }\n    ],\n    TimeUnit.milliseconds\n  ],\n  // Test with multiple time series - should use first one\n  [\n    [\n      {\n        key: 'first-series',\n        labels: { order: 'first' },\n        datapoints: [[1690000000000, 'first-value']]\n      },\n      {\n        key: 'second-series',\n        labels: { order: 'second' },\n        datapoints: [[1690000000, 'second-value']]\n      }\n    ],\n    TimeUnit.milliseconds\n  ]\n]\n\ndescribe('determineDefaultTimeUnits', () => {\n  it.each(determineDefaultTimeUnitsTests)(\n    'should determine time unit for input: %j to be: %s',\n    (timeSeries, expected) => {\n      const result = determineDefaultTimeUnits(timeSeries)\n      expect(result).toBe(expected)\n    },\n  )\n\n  it('should return seconds for empty or invalid data', () => {\n    expect(determineDefaultTimeUnits([])).toBe(TimeUnit.seconds)\n    expect(determineDefaultTimeUnits(null as any)).toBe(TimeUnit.seconds)\n    expect(determineDefaultTimeUnits(undefined as any)).toBe(TimeUnit.seconds)\n  })\n\n  it('should return seconds for time series with no datapoints', () => {\n    const timeSeries: TimeSeries[] = [\n      {\n        key: 'empty-datapoints',\n        labels: {},\n        datapoints: []\n      }\n    ]\n    expect(determineDefaultTimeUnits(timeSeries)).toBe(TimeUnit.seconds)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/components/Chart/utils.ts",
    "content": "import {\n  TimeSeries,\n  TimeUnit,\n} from './interfaces'\n\nfunction charCodeSum(str: string) {\n  let sum = 0\n  for (let i = 0; i < str.length; i++) {\n    sum += str.charCodeAt(i)\n  }\n  return sum\n}\n\nexport const COLORS_DARK = [\n  { color: '#6A1DC3', borderColor: '#6A1DC3', textColor: '#FFFFFF' },\n  { color: '#364CFF', borderColor: '#364CFF', textColor: '#FFFFFF' },\n  { color: '#008556', borderColor: '#008556', textColor: '#FFFFFF' },\n  { color: '#333D4F', borderColor: '#333D4F', textColor: '#FFFFFF' },\n  { color: '#9C5C2B', borderColor: '#9C5C2B', textColor: '#FFFFFF' },\n  { color: '#A00A6B', borderColor: '#A00A6B', textColor: '#FFFFFF' },\n  { color: '#6F7C07', borderColor: '#6F7C07', textColor: '#FFFFFF' },\n  { color: '#14708D', borderColor: '#14708D', textColor: '#FFFFFF' },\n  { color: '#AA4E4E', borderColor: '#AA4E4E', textColor: '#FFFFFF' },\n  { color: '#6E6E6E', borderColor: '#6E6E6E', textColor: '#FFFFFF' },\n]\n\nexport const EDGE_COLORS_DARK = [\n  { color: '#C7C7C7', borderColor: '#C7C7C7', textColor: '#FFFFFF' },\n  { color: '#E3AAAA', borderColor: '#E3AAAA', textColor: '#FFFFFF' },\n  { color: '#ACCCD7', borderColor: '#ACCCD7', textColor: '#FFFFFF' },\n  { color: '#C7CEA8', borderColor: '#C7CEA8', textColor: '#FFFFFF' },\n  { color: '#D9A0C6', borderColor: '#D9A0C6', textColor: '#FFFFFF' },\n  { color: '#D4BAA7', borderColor: '#D4BAA7', textColor: '#FFFFFF' },\n  { color: '#B8C5DB', borderColor: '#B8C5DB', textColor: '#FFFFFF' },\n  { color: '#A5D4C3', borderColor: '#A5D4C3', textColor: '#FFFFFF' },\n  { color: '#CDDDF8', borderColor: '#CDDDF8', textColor: '#FFFFFF' },\n  { color: '#C7B0EA', borderColor: '#C7B0EA', textColor: '#FFFFFF' },\n]\n\nexport const COLORS = [\n  { color: '#C7B0EA', borderColor: '#C7B0EA', textColor: '#000000' },\n  { color: '#CDDDF8', borderColor: '#CDDDF8', textColor: '#000000' },\n  { color: '#A5D4C3', borderColor: '#A5D4C3', textColor: '#000000' },\n  { color: '#B8C5DB', borderColor: '#B8C5DB', textColor: '#000000' },\n  { color: '#D4BAA7', borderColor: '#D4BAA7', textColor: '#000000' },\n  { color: '#D9A0C6', borderColor: '#D9A0C6', textColor: '#000000' },\n  { color: '#C7CEA8', borderColor: '#C7CEA8', textColor: '#000000' },\n  { color: '#ACCCD7', borderColor: '#ACCCD7', textColor: '#000000' },\n  { color: '#E3AAAA', borderColor: '#E3AAAA', textColor: '#000000' },\n  { color: '#C7C7C7', borderColor: '#C7C7C7', textColor: '#000000' },\n]\n\nexport const EDGE_COLORS = [\n  { color: '#6E6E6E', borderColor: '#6E6E6E', textColor: '#000000' },\n  { color: '#A85050', borderColor: '#A85050', textColor: '#000000' },\n  { color: '#1D6F8A', borderColor: '#1D6F8A', textColor: '#000000' },\n  { color: '#6F7B23', borderColor: '#6F7B23', textColor: '#000000' },\n  { color: '#9E1669', borderColor: '#9E1669', textColor: '#000000' },\n  { color: '#9A5D34', borderColor: '#9A5D34', textColor: '#000000' },\n  { color: '#363F4F', borderColor: '#363F4F', textColor: '#000000' },\n  { color: '#0F8459', borderColor: '#0F8459', textColor: '#000000' },\n  { color: '#384EF9', borderColor: '#384EF9', textColor: '#000000' },\n  { color: '#6924BD', borderColor: '#6924BD', textColor: '#000000' },\n]\n\ninterface IColor {\n  color: string\n  textColor: string\n  borderColor: string\n}\n\nexport interface IGoodColor extends IColor {}\n\n/*\n * ColorPicker: Get colors based on `label`.\n */\nexport class ColorPicker<T extends IColor> {\n  // All the default colors are stored here.\n  private readonly colors: T[]\n\n  // store all the colors that are not taken by any label at a certain window.\n  private currentColorStore: T[]\n\n  // cache for label and its chosen color.\n  private labelStore: { [keyName: string]: T }\n\n  constructor(colors: T[]) {\n    this.colors = [...colors]\n    this.currentColorStore = [...colors]\n    this.labelStore = {}\n  }\n\n  /*\n   * Get a color object of type `T` based on `label`.\n   */\n  getColor(label: string) {\n    // if the label has been seen previously, return the stored color.\n    if (this.labelStore[label] !== undefined) {\n      return this.labelStore[label]\n    }\n\n    // if the current color store is empty, i.e., all the colors has\n    // been taken, so reset the color store to default color set.\n    if (this.currentColorStore.length === 0) {\n      this.currentColorStore = [...this.colors]\n    }\n    // get the color by hashing the label.\n    const goodColor =\n      this.currentColorStore[charCodeSum(label) % this.currentColorStore.length]\n\n    // since the color has been taken by `label`, remove it from the current color store.\n    this.currentColorStore = this.currentColorStore.filter(\n      (color) => color !== goodColor,\n    )\n\n    // cache the label and color key value pair.\n    this.labelStore[label] = goodColor\n    return goodColor\n  }\n}\n\n/*\n * GoodColorPicker: ColorPicker but only good colors.\n */\nexport class GoodColorPicker extends ColorPicker<IGoodColor> {\n  constructor(COLORS: IGoodColor[]) {\n    super(COLORS)\n  }\n}\n\nexport function hexToRGBA(hex: string, alpha: number): string {\n  const r = parseInt(hex.slice(1, 3), 16),\n    g = parseInt(hex.slice(3, 5), 16),\n    b = parseInt(hex.slice(5, 7), 16)\n\n  if (alpha) {\n    return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'\n  } else {\n    return 'rgb(' + r + ', ' + g + ', ' + b + ')'\n  }\n}\n\nexport function normalizeDatapointUnits(\n  timeSeries: TimeSeries[],\n  unit: TimeUnit,\n): TimeSeries[] {\n  try {\n    if (unit === TimeUnit.seconds) {\n      return timeSeries.map((ts) => {\n        return {\n          ...ts,\n          datapoints: ts.datapoints.map(([timestamp, label]) => [\n            timestamp * 1_000,\n            label,\n          ]),\n        }\n      })\n    }\n  } catch (e) {\n    // ignore an error to return original data\n  }\n\n  return timeSeries\n}\n\nexport function determineDefaultTimeUnits(timeSeries: TimeSeries[]): TimeUnit {\n  return timeSeries?.[0]?.datapoints?.[0]?.[0] > 1e10\n    ? TimeUnit.milliseconds\n    : TimeUnit.seconds\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/main.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { render } from 'react-dom'\nimport { ThemeProvider } from 'uiSrc/components/base/utils/pluginsThemeContext'\nimport App from './App'\n\nimport './styles/styles.scss'\nimport result from '../mockData/resultTimeSeries.json'\n\ninterface Props {\n  command?: string\n  data?: { response: any; status: string }[]\n}\n\nconst renderChart = (props: Props) => {\n  const { command = '', data: result = [] } = props\n  render(\n    <ThemeProvider>\n      <App command={command} result={result} />\n    </ThemeProvider>,\n    document.getElementById('app'),\n  )\n}\n\nif (process.env.NODE_ENV === 'development') {\n  const command = 'TS.RANGE bike_sales_3_per_day - + FILTER_BY_VALUE 3000 5000'\n  renderChart({ command, data: result })\n}\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderChart }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/response.ts",
    "content": "/* eslint-disable */\nexport const response1 = {\n  query: 'TS.MRANGE - + COUNT 50 FILTER metric=cpu',\n  result: [\n    {\n      key: 'ts:cpu:1:ind',\n      labels: {},\n      datapoints: [\n        [1550510257111, '100'],\n        [1550510258111, '101.25370686268391'],\n        [1550510259111, '101.77500836030259'],\n        [1550510260111, '102.37114279051823'],\n        [1550510261111, '100.70924932898184'],\n        [1550510262111, '100.96271927638976'],\n        [1550510263111, '101.84620970109472'],\n        [1550510264111, '100.23523301306179'],\n        [1550510265111, '98.82336702008972'],\n        [1550510266111, '97.62974369812069'],\n        [1550510267111, '96.61141780929017'],\n        [1550510268111, '97.68645989521474'],\n        [1550510269111, '96.11140079823213'],\n        [1550510270111, '95.95708135046364'],\n        [1550510271111, '94.7818923205837'],\n        [1550510272111, '95.22863247729673'],\n        [1550510273111, '95.46987734871995'],\n        [1550510274111, '96.0582021956672'],\n        [1550510275111, '98.04665092287263'],\n        [1550510276111, '97.63028174045374'],\n        [1550510277111, '97.30553655712471'],\n        [1550510278111, '95.66209250674443'],\n        [1550510279111, '95.77206715035226'],\n        [1550510280111, '94.03582207874183'],\n        [1550510281111, '93.59878228691228'],\n        [1550510282111, '95.07857348596144'],\n        [1550510283111, '96.90168645440566'],\n        [1550510284111, '97.40176226686248'],\n        [1550510285111, '98.10545840095983'],\n        [1550510286111, '99.10230873395624'],\n        [1550510287111, '97.19445898635804'],\n        [1550510288111, '98.73411400113018'],\n        [1550510289111, '99.63572727884399'],\n        [1550510290111, '101.1719731242495'],\n        [1550510291111, '101.38556298083219'],\n        [1550510292111, '101.71913719093472'],\n        [1550510293111, '102.79565117276488'],\n        [1550510294111, '101.16905104463837'],\n        [1550510295111, '101.86655289446178'],\n        [1550510296111, '102.12260403165118'],\n        [1550510297111, '102.4435147763059'],\n        [1550510298111, '101.76113582326975'],\n        [1550510299111, '101.26415238813502'],\n        [1550510300111, '102.9354476048847'],\n        [1550510301111, '103.20575117057719'],\n        [1550510302111, '102.41224587036683'],\n        [1550510303111, '100.4841909462497'],\n        [1550510304111, '98.98040440558316'],\n        [1550510305111, '97.05163290409813'],\n        [1550510306111, '96.11396599827768'],\n      ],\n    },\n    {\n      key: 'ts:cpu:1:us',\n      labels: {},\n      datapoints: [\n        [1550510257111, '100'],\n        [1550510258111, '101.3776874061002'],\n        [1550510259111, '102.4095050178614'],\n        [1550510260111, '102.09179134118479'],\n        [1550510261111, '101.12745834235665'],\n        [1550510262111, '101.17255722783108'],\n        [1550510263111, '100.79229377763274'],\n        [1550510264111, '101.92748813377183'],\n        [1550510265111, '101.14073903808755'],\n        [1550510266111, '101.04712685469697'],\n        [1550510267111, '101.3806550125171'],\n        [1550510268111, '103.01310655329844'],\n        [1550510269111, '103.031853976568'],\n        [1550510270111, '102.15920535416682'],\n        [1550510271111, '103.18242217079572'],\n        [1550510272111, '103.65589815749705'],\n        [1550510273111, '102.65792352294682'],\n        [1550510274111, '104.29690854681978'],\n        [1550510275111, '106.22805045097039'],\n        [1550510276111, '107.46891939495674'],\n        [1550510277111, '109.07758319671507'],\n        [1550510278111, '108.3181734739924'],\n        [1550510279111, '109.23750046703292'],\n        [1550510280111, '110.83285361890489'],\n        [1550510281111, '111.56878934656666'],\n        [1550510282111, '111.45736020837751'],\n        [1550510283111, '109.86016504065097'],\n        [1550510284111, '109.59685238246611'],\n        [1550510285111, '110.04040027624131'],\n        [1550510286111, '111.69244448919291'],\n        [1550510287111, '113.55886996027594'],\n        [1550510288111, '113.4669090664868'],\n        [1550510289111, '114.92814877757337'],\n        [1550510290111, '113.97011801914121'],\n        [1550510291111, '115.1902293271933'],\n        [1550510292111, '115.38502654253566'],\n        [1550510293111, '113.44119334319173'],\n        [1550510294111, '114.32001208880754'],\n        [1550510295111, '113.91530625770461'],\n        [1550510296111, '115.21468616629754'],\n        [1550510297111, '115.88729897122494'],\n        [1550510298111, '113.89187024848265'],\n        [1550510299111, '113.86618171434395'],\n        [1550510300111, '115.33659281631508'],\n        [1550510301111, '114.3122363238636'],\n        [1550510302111, '113.61305377485316'],\n        [1550510303111, '115.09493870328778'],\n        [1550510304111, '113.85920706929734'],\n        [1550510305111, '114.12925003178003'],\n        [1550510306111, '113.08371374624092'],\n      ],\n    },\n    {\n      key: 'ts:cpu:2:ind',\n      labels: {},\n      datapoints: [\n        [1550510257111, '100'],\n        [1550510258111, '98.73959598446271'],\n        [1550510259111, '100.25143576698089'],\n        [1550510260111, '99.3764470802896'],\n        [1550510261111, '98.48331204136353'],\n        [1550510262111, '97.04227872620508'],\n        [1550510263111, '96.72410120905901'],\n        [1550510264111, '98.3350011421087'],\n        [1550510265111, '98.36683243032081'],\n        [1550510266111, '97.67041337179381'],\n        [1550510267111, '95.75351301520804'],\n        [1550510268111, '93.99945684092745'],\n        [1550510269111, '94.73954095690829'],\n        [1550510270111, '96.71827778537721'],\n        [1550510271111, '97.21321598443637'],\n        [1550510272111, '97.88917108980229'],\n        [1550510273111, '99.05710743808955'],\n        [1550510274111, '98.10797343137791'],\n        [1550510275111, '97.99551985599346'],\n        [1550510276111, '98.16684293402491'],\n        [1550510277111, '96.68281584399311'],\n        [1550510278111, '95.89239252213798'],\n        [1550510279111, '95.93322479954445'],\n        [1550510280111, '94.11269927563103'],\n        [1550510281111, '94.6582934083936'],\n        [1550510282111, '93.71036092744936'],\n        [1550510283111, '95.16592739728554'],\n        [1550510284111, '93.3759827888403'],\n        [1550510285111, '92.11321821177614'],\n        [1550510286111, '90.12049395505545'],\n        [1550510287111, '88.21104501027277'],\n        [1550510288111, '88.14482361719176'],\n        [1550510289111, '88.62250984928843'],\n        [1550510290111, '88.32070992647436'],\n        [1550510291111, '89.89489868237678'],\n        [1550510292111, '88.57530416785465'],\n        [1550510293111, '90.27142569909853'],\n        [1550510294111, '90.88414075056788'],\n        [1550510295111, '91.36818499882189'],\n        [1550510296111, '91.81691583764517'],\n        [1550510297111, '93.72864124343647'],\n        [1550510298111, '95.11181631254156'],\n        [1550510299111, '95.65150532703822'],\n        [1550510300111, '94.64383395498115'],\n        [1550510301111, '95.71079057571716'],\n        [1550510302111, '97.35417403528722'],\n        [1550510303111, '95.53342196962552'],\n        [1550510304111, '94.17285695988532'],\n        [1550510305111, '95.92714340375848'],\n        [1550510306111, '97.5725477364058'],\n      ],\n    },\n    {\n      key: 'ts:cpu:2:us',\n      labels: {},\n      datapoints: [\n        [1550510257111, '100'],\n        [1550510258111, '99.20315492454021'],\n        [1550510259111, '98.7695176629229'],\n        [1550510260111, '99.59608835366409'],\n        [1550510261111, '99.54370948815631'],\n        [1550510262111, '101.47775097088726'],\n        [1550510263111, '100.35669953724523'],\n        [1550510264111, '101.2492433411521'],\n        [1550510265111, '100.3394824654466'],\n        [1550510266111, '98.86718598764821'],\n        [1550510267111, '97.02339741868121'],\n        [1550510268111, '96.55790184053775'],\n        [1550510269111, '95.09936157661629'],\n        [1550510270111, '96.72990749970467'],\n        [1550510271111, '98.47610318359934'],\n        [1550510272111, '98.83087201609408'],\n        [1550510273111, '98.51474703337368'],\n        [1550510274111, '98.60574781262909'],\n        [1550510275111, '97.99050690275075'],\n        [1550510276111, '97.99558567908426'],\n        [1550510277111, '98.28484236980026'],\n        [1550510278111, '99.6175460304959'],\n        [1550510279111, '98.33629180856946'],\n        [1550510280111, '99.86927782788749'],\n        [1550510281111, '99.50778251407216'],\n        [1550510282111, '101.10719934185667'],\n        [1550510283111, '100.20205682711939'],\n        [1550510284111, '100.94400690910751'],\n        [1550510285111, '99.61516944970336'],\n        [1550510286111, '100.68115171014014'],\n        [1550510287111, '99.19398491228273'],\n        [1550510288111, '98.84654174455997'],\n        [1550510289111, '97.6597530324471'],\n        [1550510290111, '99.56617653094565'],\n        [1550510291111, '99.71599775150214'],\n        [1550510292111, '98.40732280794381'],\n        [1550510293111, '98.06465228757533'],\n        [1550510294111, '96.44180274020538'],\n        [1550510295111, '95.51208433474983'],\n        [1550510296111, '94.0000146801394'],\n        [1550510297111, '95.52113192784222'],\n        [1550510298111, '96.9362199799547'],\n        [1550510299111, '96.95226066915588'],\n        [1550510300111, '97.7213860981982'],\n        [1550510301111, '96.48597662043018'],\n        [1550510302111, '94.53476534791717'],\n        [1550510303111, '93.33781186409271'],\n        [1550510304111, '93.00820590555274'],\n        [1550510305111, '93.53152804621065'],\n        [1550510306111, '92.59221056838048'],\n      ],\n    },\n  ],\n  auto_update: true,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/redistimeseries-app/src/styles/styles.scss",
    "content": "@charset \"UTF-8\";\n\n// disable plotly notifier\ndiv.plotly-notifier {\n  visibility: hidden;\n}\n\n.theme_DARK {\n  --body-color: white;\n  --text-color: #B5B6C0;\n\n  .rangeslider-mask-min, .rangeslider-mask-max {\n    fill: #161617 !important;\n    fill-opacity: 1 !important;\n  }\n\n  .chart-config-form {\n    .more-options {\n      section {\n        background-color: #171717;\n      }\n    }\n  }\n\n  .button-point {\n    background-color: black;\n    border: 1px solid #465282;\n  }\n\n  .button-selected {\n    color: #8BA2FF;\n    background-color: #292F47;\n  }\n}\n\n.rangeslider-handle-min, .rangeslider-handle-max {\n  height: 0;\n}\n\n.theme_LIGHT {\n  --body-color: black;\n  --text-color: #527298;\n\n  .rangeslider-mask-min, .rangeslider-mask-max {\n    fill: #F6F7F9 !important;\n    fill-opacity: 1 !important;\n  }\n\n  .chart-config-form {\n    .more-options {\n      section {\n        background-color: #F6F6F8;\n      }\n    }\n  }\n\n  .new-button {\n    color: #343741;\n  }\n\n  .button-point {\n    border: 1px solid #243DAC;\n  }\n\n  .button-selected {\n    color: #3163D8;\n    background-color: #D7E3FA;\n  }\n}\n\nbody {\n  color: var(--body-color);\n  overflow: hidden;\n}\n\n* div,\n* span {\n  font-family: 'Graphik', sans-serif !important;\n}\n\n.responseFail {\n  color: red;\n  padding: 12px !important;\n  font-family: monospace !important;\n}\n\n\n.y-axis-config  {\n  fieldset {\n    width: 100%;\n  }\n}\n\n.chart-config-form {\n  width: 50%;\n  min-width: fit-content;\n  display: flex;\n  flex-direction: column;\n\n  .chart-form-top {\n    display: flex;\n    justify-content: center;\n    & > :not(:first-child) {\n      margin-left: 36px;\n    }\n  }\n\n  .chart-form-accordion {\n    margin-top: 20px;\n\n    .more-options {\n      width: 100%;\n      section {\n        display: flex;\n        padding: 15px;\n        justify-content: space-between;\n        gap: 10px;\n\n        &:not(:first-child) {\n          margin-top: 10px;\n        }\n\n        .right-y-axis {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          width: 100%;\n\n          .switch-wrapper {\n            width: 100%;\n          }\n        }\n\n        .y-axis-2 {\n          width: 100%;\n          .y-axis-2-item {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            font-size: 13px;\n            gap: 5px;\n          }\n        }\n      }\n    }\n  }\n}\n\n.zoom-helper-text {\n  display: flex;\n  justify-content: flex-end;\n  padding-right: 3em !important;\n  padding-top: 0.3em !important;\n  color: var(--text-color);\n}\n\n\n.config-form-wrapper {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n\n.theme_DARK {\n    .button-point {\n        background-color: black;\n        border: 1px solid #465282;\n    }\n\n    .button-selected {\n        color: #8BA2FF;\n        background-color: #292F47;\n    }\n}\n\n.theme_LIGHT {\n    .new-button {\n        color: #343741;\n    }\n\n    .button-point {\n        border: 1px solid #243DAC;\n    }\n\n    .button-selected {\n        color: #3163D8;\n        background-color: #D7E3FA;\n    }\n}\n\n.new-button {\n    box-shadow: none!important;\n    border-radius: 2px;\n    cursor: pointer;\n    height: 32px;\n    overflow: visible;\n    display: flex;\n\n    div {\n        font-size: 13px;\n        padding: 0px;\n        font-weight: normal;\n        height: 30px;\n        min-width: 68px;\n    }\n\n    div:first-child {\n        border-radius: 4px;\n        border-right: 0px;\n        border-top-right-radius: 0px;\n        border-bottom-right-radius: 0px;\n    }\n\n    div:last-child {\n        border-radius: 4px;\n        border-top-left-radius: 0px;\n        border-bottom-left-radius: 0px;\n    }\n\n    .button-point {\n        text-transform: capitalize;\n        text-align: center;\n        padding: 6px;\n    }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/README.md",
    "content": "# RI-Explain plugin\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Explain plugin</title>\n\n    <script type=\"module\" src=\"./src/main.tsx\"></script>\n    <!-- Run Conditions-->\n    <% if(isDev){ %>\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/global_styles.css\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/dark_theme.css\"\n    />\n    <!-- <link rel=\"stylesheet\" href=\"https://s3.us-east-1.amazonaws.com/redisinsight.test/public/plugins/static/light_theme.css\" /> -->\n    <% } %>\n  </head>\n  <body class=\"theme_DARK\">\n    <div id=\"app\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/package.json",
    "content": "{\n  \"author\": {\n    \"name\": \"Redis Ltd.\",\n    \"email\": \"support@redis.com\",\n    \"url\": \"https://redis.com/redis-enterprise/redis-insight\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/\"\n  },\n  \"description\": \"Show Profile/Explain Visualization\",\n  \"source\": \"./src/main.tsx\",\n  \"styles\": \"./dist/styles.css\",\n  \"main\": \"./dist/index.js\",\n  \"name\": \"explain-plugin\",\n  \"version\": \"0.0.2\",\n  \"scripts\": {\n    \"dev\": \"vite -c ../vite.config.mjs\"\n  },\n  \"targets\": {\n    \"main\": false,\n    \"module\": {\n      \"includeNodeModules\": true\n    }\n  },\n  \"visualizations\": [\n    {\n      \"id\": \"profile-explain-viz\",\n      \"name\": \"Visualization\",\n      \"activationMethod\": \"renderCore\",\n      \"matchCommands\": [\n        \"FT.EXPLAIN\",\n        \"FT.EXPLAINCLI\",\n        \"FT.PROFILE\",\n        \"GRAPH.EXPLAIN\",\n        \"GRAPH.PROFILE\"\n      ],\n      \"iconDark\": \"./dist/profile_icon_dark.svg\",\n      \"iconLight\": \"./dist/profile_icon_light.svg\",\n      \"description\": \"Profile/Explain plugin Visualization\",\n      \"default\": true\n    }\n  ],\n  \"devDependencies\": {\n    \"vite\": \"file:../node_modules/vite\"\n  },\n  \"dependencies\": {\n    \"@antv/hierarchy\": \"^0.6.8\",\n    \"@antv/x6\": \"^2.1.3\",\n    \"@antv/x6-react-shape\": \"^2.1.0\",\n    \"@elastic/eui\": \"34.6.0\",\n    \"@emotion/react\": \"^11.7.1\",\n    \"classnames\": \"^2.3.1\",\n    \"prop-types\": \"^15.8.1\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"redisinsight-plugin-sdk\": \"^1.1.0\",\n    \"uuid\": \"^9.0.0\"\n  },\n  \"resolutions\": {\n    \"trim\": \"0.0.3\",\n    \"@elastic/eui/**/prismjs\": \"~1.30.0\",\n    \"**/semver\": \"^7.5.2\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/App.tsx",
    "content": "import React from 'react'\nimport Explain from './Explain'\n\nexport function App(props: { command?: string; data: any }) {\n  const ErrorResponse = HandleError(props)\n\n  if (ErrorResponse !== null) return ErrorResponse\n\n  return (\n    <div\n      id=\"mainApp\"\n      style={{ height: '100%', width: '100%', overflow: 'hidden' }}\n    >\n      <Explain command={props.command || ''} data={props.data} />\n    </div>\n  )\n}\n\nfunction HandleError(props: {\n  command?: string\n  data: any\n}): JSX.Element | null {\n  const { data: [{ response = '', status = '' } = {}] = [] } = props\n\n  if (status === 'fail') {\n    return <div className=\"responseFail\">{JSON.stringify(response)}</div>\n  }\n\n  return null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/Explain.tsx",
    "content": "/* eslint-disable no-restricted-globals */\nimport React, { useEffect, useState, useRef } from 'react'\nimport { Model, Graph } from '@antv/x6'\nimport { register } from '@antv/x6-react-shape'\nimport Hierarchy from '@antv/hierarchy'\nimport { formatRedisReply } from 'redisinsight-plugin-sdk'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip/RITooltip'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\n\nimport {\n  EDGE_COLOR_BODY_DARK,\n  EDGE_COLOR_BODY_LIGHT,\n  NODE_COLOR_BODY_DARK,\n  NODE_COLOR_BODY_LIGHT,\n} from './constants'\n\nimport {\n  CoreType,\n  ModuleType,\n  EntityInfo,\n  ParseExplain,\n  ParseGraphV2,\n  ParseProfile,\n  GetAncestors,\n  GetTotalExecutionTime,\n  transformProfileResult,\n  findFlatProfile,\n} from './parser'\nimport { ExplainNode, ProfileNode } from './Node'\n\ninterface IExplain {\n  command: string\n  data: [{ response: string[] | string | any }]\n}\n\nfunction getEdgeSize(c: number) {\n  return Math.floor(Math.log(c || 1) + 1)\n}\n\nfunction getNodeColor(isDarkTheme: boolean) {\n  return isDarkTheme ? NODE_COLOR_BODY_DARK : NODE_COLOR_BODY_LIGHT\n}\n\nfunction getEdgeColor(isDarkTheme: boolean) {\n  return isDarkTheme ? EDGE_COLOR_BODY_DARK : EDGE_COLOR_BODY_LIGHT\n}\n\nexport default function Explain({ command, data }: IExplain): JSX.Element {\n  const cmd = command.split(' ')[0].toLowerCase()\n  useEffect(() => {\n    if (cmd === 'ft.profile') {\n      const getParsedResponse = async () => {\n        const formattedResponse = await formatRedisReply(\n          data[0].response,\n          command,\n        )\n        setParsedRedisReply(formattedResponse)\n      }\n      getParsedResponse()\n    }\n  }, [cmd])\n  const [parsedRedisReply, setParsedRedisReply] = useState('')\n\n  if (cmd.startsWith('graph')) {\n    const info = data[0].response\n    const resp = ParseGraphV2(info)\n\n    let profilingTime: IProfilingTime = {}\n    const t = command.endsWith('explain') ? CoreType.Explain : CoreType.Profile\n    if (t === CoreType.Profile) {\n      profilingTime = {\n        'Total Execution Time': GetTotalExecutionTime(resp),\n      }\n    }\n\n    return (\n      <ExplainDraw\n        data={resp}\n        module={ModuleType.Graph}\n        type={t}\n        profilingTime={profilingTime}\n      />\n    )\n  }\n\n  const module = ModuleType.Search\n\n  if (cmd === 'ft.profile') {\n    try {\n      const isNewResponse = typeof data[0].response[1]?.[0] === 'string'\n\n      const [, profiles] = data[0].response || []\n      const transformedProfiles = isNewResponse\n        ? profiles\n        : transformProfileResult(profiles)\n      const [shard] = findFlatProfile('Shards', transformedProfiles)\n      const profileInfo: EntityInfo = ParseProfile(shard)\n\n      const profilingTime = {\n        'Total Profile Time': findFlatProfile('Total Profile Time', shard),\n        'Parsing Time': findFlatProfile('Parsing Time', shard),\n        'Pipeline Creation Time': findFlatProfile(\n          'Pipeline Creation Time',\n          shard,\n        ),\n      }\n\n      return (\n        <ExplainDraw\n          data={profileInfo}\n          module={module}\n          type={CoreType.Profile}\n          profilingTime={profilingTime}\n        />\n      )\n    } catch (e) {\n      console.error(e)\n\n      return (\n        <>\n          <div className=\"responseFail\">\n            Some error happened during parsing the data.\n          </div>\n          <div className=\"parsedRedisReply\">{parsedRedisReply}</div>\n        </>\n      )\n    }\n  }\n\n  const resp = data[0].response\n\n  const explainDrawData = ParseExplain(\n    Array.isArray(resp) ? resp.join('\\n') : resp.split('\\\\n').join('\\n'),\n  )\n  return (\n    <ExplainDraw\n      data={explainDrawData}\n      module={module}\n      type={CoreType.Explain}\n    />\n  )\n}\n\nregister({\n  shape: 'react-explain-node',\n  width: 100,\n  height: 100,\n  component: ExplainNode as any,\n})\n\nregister({\n  shape: 'react-profile-node',\n  width: 100,\n  height: 100,\n  component: ProfileNode as any,\n})\n\nconst isDarkTheme = document.body.classList.contains('theme_DARK')\n\ninterface IProfilingTime {\n  [key: string]: string\n}\n\nfunction ExplainDraw({\n  data,\n  type,\n  module,\n  profilingTime,\n}: {\n  data: any\n  type: CoreType\n  module: ModuleType\n  profilingTime?: IProfilingTime\n}): JSX.Element {\n  const container = useRef<HTMLDivElement | null>(null)\n\n  const [done, setDone] = useState(false)\n  const [collapse, setCollapse] = useState(type !== CoreType.Profile)\n  const [isFullScreen, setIsFullScreen] = useState(false)\n  const [core, setCore] = useState<Graph>()\n\n  function resize() {\n    const isFullScreen =\n      parent.document.body.getElementsByClassName('fullscreen').length > 0\n    const b = core?.getAllCellsBBox()\n    const width = Math.max((b?.width || 1080) + 100, document.body.offsetWidth)\n    if (isFullScreen) {\n      setIsFullScreen(true)\n      const height = Math.max(\n        (b?.height || 585) + 100,\n        parent.document.body.offsetHeight,\n      )\n      if (type !== CoreType.Profile && collapse) {\n        core?.resize(width, window.outerHeight - 250)\n        core?.positionContent('top')\n      } else {\n        core?.resize(width, height)\n      }\n    } else {\n      setIsFullScreen(false)\n      if (type !== CoreType.Profile && collapse) {\n        core?.resize(width, 400)\n        core?.positionContent('top')\n      } else {\n        core?.resize(width, (b?.height || 585) + 100)\n      }\n    }\n  }\n\n  window.addEventListener('resize', resize)\n  useEffect(() => {\n    if (done) return\n    setDone(true)\n\n    const graph = new Graph({\n      container: container?.current as HTMLElement,\n      autoResize: false,\n      interacting: false,\n      background: {\n        color: isDarkTheme ? 'black' : 'white',\n      },\n      translating: {\n        restrict: true,\n      },\n      async: true,\n      virtual: false, // if set to true, only visible parts of the graph will be rendered,\n      // which can improve performance, but may cause some issues with missing edges\n      panning: true,\n    })\n\n    setCore(graph)\n\n    graph.on('resize', () => graph.centerContent())\n    graph.on('node:mouseenter', (x) => {\n      const { id } = x.node.getData()\n      // Find ancestors of a node\n      const ancestors = GetAncestors(data, id, { found: false, pairs: [] })\n      ancestors.pairs.forEach((p) => {\n        // Highlight ancestor and their ancestor\n        document\n          .querySelector(`#node-${p[0]}`)\n          ?.setAttribute('style', 'outline: 1px solid #85A2FE !important;')\n        // Get edge size of parent ancestor to apply the right edge stroke\n        const edge = graph.getCellById(`${p[0]}-${p[1]}`)\n        const edgeColor = '#85A2FE'\n        edge.setAttrs({\n          line: {\n            stroke: edgeColor,\n            strokeWidth: (edge.getAttrs() as any)?.line?.strokeWidth,\n          },\n        })\n      })\n    })\n\n    graph.on('node:mouseleave', (x) => {\n      const { id } = x.node.getData()\n      const ancestors = GetAncestors(data, id, { found: false, pairs: [] })\n      ancestors.pairs.forEach((p) => {\n        document.querySelector(`#node-${p[0]}`)?.setAttribute('style', '')\n        const edge = graph.getCellById(`${p[0]}-${p[1]}`)\n        const edgeColor = getEdgeColor(isDarkTheme)\n        edge.setAttrs({\n          line: {\n            stroke: edgeColor,\n            strokeWidth: (edge.getAttrs() as any)?.line?.strokeWidth,\n          },\n        })\n      })\n    })\n\n    resize()\n\n    const result = Hierarchy.compactBox(data, {\n      direction: 'BT',\n      getHeight() {\n        return 200\n      },\n      getWidth() {\n        return 250\n      },\n      getHGap() {\n        return 50\n      },\n      getVGap() {\n        return 0\n      },\n      nodeSep: type === CoreType.Explain ? 250 : 350,\n      rankSep: 150,\n      subTreeSep: 0,\n    })\n\n    const model: Model.FromJSONData = { nodes: [], edges: [] }\n    const traverse = (data: any) => {\n      if (data) {\n        const info = data.data as EntityInfo\n\n        // snippet if prefix with parent suffix will always be followed by ':'.\n        //\n        // Currently snippets are passed to child only for TAG\n        // expressions which has ':' at the center.\n        //\n        // Example child data with parent snippet: <PARENT_SNIPPET>:<DATA>\n        if (\n          !info.snippet &&\n          info.parentSnippet &&\n          info.data?.startsWith(`${info.parentSnippet}:`)\n        ) {\n          info.data = info.data.substr(info.parentSnippet.length + 1)\n          info.snippet = info.parentSnippet\n        }\n\n        if (module === ModuleType.Graph) {\n          info.recordsProduced = info.counter\n          delete info.counter\n          delete info.size\n        }\n\n        let nodeProps = {\n          shape: 'react-explain-node',\n          width: 240,\n          height: info.snippet ? 64 : 42,\n        }\n        if (type === CoreType.Profile) {\n          nodeProps = {\n            shape: 'react-profile-node',\n            width: 320,\n            height: info.snippet ? 114 : 86,\n          }\n        }\n\n        const portId = `${data.id}-source`\n        const targetPort = {}\n        const targetItem: any = []\n        if (info.parentId) {\n          targetItem.push({\n            id: `${info.id}-${info.parentId}-target`,\n            group: `${info.parentId}-target`,\n          })\n          targetPort[`${info.parentId}-target`] = {\n            position: { name: 'bottom' },\n            attrs: {\n              circle: {\n                r: 0,\n              },\n            },\n          }\n        }\n        model.nodes?.push({\n          id: data.id,\n          x: (data.x || 0) + document.body.clientWidth / 2,\n          y: (data.y || 0) + document.body.clientHeight,\n          ...nodeProps,\n          data: info,\n          attrs: {\n            body: {\n              fill: getNodeColor(isDarkTheme),\n              stroke: 'transparent',\n            },\n          },\n          ports: {\n            groups: {\n              [portId]: {\n                position: { name: 'top' },\n                attrs: {\n                  circle: {\n                    r: 0,\n                  },\n                },\n              },\n              ...targetPort,\n            },\n            items: [\n              ...data.children.map((c: { id: string }) => ({\n                id: `${data.id}-${c.id}`,\n                group: portId,\n              })),\n              ...targetItem,\n            ],\n          },\n        })\n      }\n      if (data.children) {\n        data.children.forEach((item: any) => {\n          const itemRecords = parseInt(item.data.counter || 0)\n          const edgeColor = getEdgeColor(isDarkTheme)\n          model.edges?.push({\n            id: `${data.id}-${item.id}`,\n            source: {\n              cell: data.id,\n              port: `${data.id}-${item.id}`,\n            },\n            target: {\n              cell: item.id,\n              port: `${data.id}-${item.id}`,\n            },\n            router: {\n              name: 'manhattan',\n              args: {\n                startDirections: ['top'],\n                endDirections: ['bottom'],\n                // cost: 33,\n                // step: 10,\n                padding: {\n                  top: 15,\n                  bottom: 10,\n                  right: 20,\n                  left: 10,\n                },\n              },\n            },\n            attrs: {\n              line: {\n                stroke: edgeColor,\n                strokeWidth: getEdgeSize(itemRecords),\n                targetMarker: null,\n              },\n            },\n          })\n          traverse(item)\n        })\n      }\n    }\n    traverse(result)\n\n    graph.fromJSON(model)\n    graph.centerContent()\n  }, [done])\n\n  const ele = document.querySelector('#container-parent')\n\n  let pos = { top: 0, left: 0, x: 0, y: 0 }\n\n  const mouseMoveHandler = (e: MouseEvent) => {\n    // How far the mouse has been moved\n    const dx = e.clientX - pos.x\n    const dy = e.clientY - pos.y\n\n    // Scroll the element\n    if (ele) {\n      ele.scrollTop = pos.top - dy\n      ele.scrollLeft = pos.left - dx\n    }\n  }\n\n  const mouseUpHandler = () => {\n    document.removeEventListener('mousemove', mouseMoveHandler)\n    document.removeEventListener('mouseup', mouseUpHandler)\n  }\n\n  const mouseDownHandler = (e: MouseEvent) => {\n    pos = {\n      // The current scroll\n      left: ele?.scrollLeft || 0,\n      top: ele?.scrollTop || 0,\n      // Get the current mouse position\n      x: e.clientX,\n      y: e.clientY,\n    }\n\n    document.addEventListener('mousemove', mouseMoveHandler)\n    setTimeout(() => document.addEventListener('mouseup', mouseUpHandler), 100)\n  }\n\n  ele?.addEventListener('mousedown', mouseDownHandler as EventListener)\n\n  if (type !== CoreType.Profile && collapse) {\n    core?.resize(undefined, isFullScreen ? window.outerHeight - 250 : 400)\n    core?.positionContent('top')\n  } else {\n    core?.resize(undefined, core?.getContentBBox().height + 100)\n  }\n\n  return (\n    <div>\n      {type !== CoreType.Profile && collapse && (\n        <div style={{ paddingTop: '50px' }} />\n      )}\n      <div\n        id=\"container-parent\"\n        style={{\n          height: isFullScreen\n            ? `${window.outerHeight - 170}px`\n            : type !== CoreType.Profile && collapse\n              ? '500px'\n              : '585px',\n          width: '100%',\n          overflow: 'auto',\n        }}\n      >\n        <div\n          style={{ margin: 0, width: '100vw' }}\n          ref={container}\n          id=\"container\"\n        />\n        {!collapse && (\n          <div className=\"ZoomMenu\">\n            {[\n              {\n                name: 'Zoom In',\n                onClick: () => {\n                  setTimeout(\n                    () => document.addEventListener('mouseup', mouseUpHandler),\n                    100,\n                  )\n                  core && Math.floor(core.zoom()) <= 3 && core?.zoom(0.5)\n                  core?.resize(undefined, core?.getContentBBox().height + 50)\n                },\n                // icon: 'magnifyWithPlus', TODO: needs replacement\n                icon: 'PlusSlimIcon',\n              },\n              {\n                name: 'Zoom Out',\n                onClick: () => {\n                  setTimeout(\n                    () => document.addEventListener('mouseup', mouseUpHandler),\n                    100,\n                  )\n                  if (Math.floor(core?.zoom() || 0) <= 0.5) {\n                    core?.centerContent()\n                  } else {\n                    core?.zoom(-0.5)\n                  }\n                  core?.resize(undefined, core?.getContentBBox().height + 50)\n                },\n                // icon: 'magnifyWithMinus', TODO: needs replacement\n                icon: 'MinusIcon',\n              },\n              {\n                name: 'Reset Zoom',\n                onClick: () => {\n                  setTimeout(\n                    () => document.addEventListener('mouseup', mouseUpHandler),\n                    100,\n                  )\n                  core?.zoomTo(1)\n                  core?.resize(undefined, core?.getContentBBox().height + 50)\n                },\n                // icon: 'bullseye', TODO: needs replacement\n                icon: 'IndicatorXIcon',\n              },\n            ].map((item) => (\n              <RiTooltip position=\"left\" content={item.name}>\n                <IconButton\n                  color=\"text\"\n                  onClick={item.onClick}\n                  icon={item.icon}\n                  aria-label={item.name}\n                />\n              </RiTooltip>\n            ))}\n          </div>\n        )}\n        {type !== CoreType.Profile && (\n          <div\n            style={{\n              paddingBottom:\n                isFullScreen && profilingTime && ModuleType.Search\n                  ? '60px'\n                  : '35px',\n            }}\n            className=\"CollapseButton\"\n            onClick={(e) => {\n              e.preventDefault()\n              setTimeout(\n                () => document.addEventListener('mouseup', mouseUpHandler),\n                100,\n              )\n              if (!collapse) {\n                // About to collapse?\n                core?.zoomTo(1)\n                core?.resize(undefined, core?.getContentBBox().height + 50)\n              }\n              setCollapse(!collapse)\n            }}\n            role=\"button\"\n            tabIndex={-1}\n          >\n            {collapse ? (\n              <>\n                <div>Expand</div>\n                <RiIcon className=\"NodeIcon\" size=\"m\" type=\"ArrowDownIcon\" />\n              </>\n            ) : (\n              <>\n                <div>Collapse</div>\n                <RiIcon className=\"NodeIcon\" size=\"m\" type=\"ArrowUpIcon\" />\n              </>\n            )}\n          </div>\n        )}\n        {profilingTime && module === ModuleType.Search && (\n          <div className=\"ProfileInfo ProfileTimeInfo\">\n            {Object.keys(profilingTime).map((key) => (\n              <div className=\"Item\">\n                <div className=\"Value\">{profilingTime[key]}</div>\n                <div className=\"Key\">{key}</div>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/Node.tsx",
    "content": "import React from 'react'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip/RITooltip'\nimport { TOOLTIP_DELAY_LONG } from 'uiSrc/constants/durationUnits'\n\nimport { EntityInfo, EntityType } from './parser'\n\ninterface INodeProps {\n  label: string\n  numRecords?: string\n  executionTime?: string\n  snippet?: string\n}\n\nfunction Snippet({ content }: { content: string }) {\n  return (\n    <div className=\"FooterCommon Footer\">\n      <RiTooltip delay={TOOLTIP_DELAY_LONG} content={content}>\n        <span>{content}</span>\n      </RiTooltip>\n    </div>\n  )\n}\n\nexport function ExplainNode(props: INodeProps) {\n  const propData: EntityInfo = (props as any).node.getData()\n  const { id, type, data, snippet, subType } = propData\n\n  const infoData = data || type\n\n  return (\n    <div className=\"ExplainContainer\" id={`node-${id}`}>\n      <div className=\"Main\">\n        <div className=\"Info\">\n          <div className=\"InfoData\">\n            <RiTooltip delay={TOOLTIP_DELAY_LONG} content={infoData}>\n              <span>{infoData}</span>\n            </RiTooltip>\n          </div>\n          {subType &&\n            [\n              EntityType.GEO,\n              EntityType.NUMERIC,\n              EntityType.TEXT,\n              EntityType.TAG,\n              EntityType.FUZZY,\n              EntityType.WILDCARD,\n              EntityType.PREFIX,\n              EntityType.IDS,\n              EntityType.LEXRANGE,\n              EntityType.NUMBER,\n            ].includes(subType) && <div className=\"Type\">{subType}</div>}\n        </div>\n      </div>\n      {snippet && <Snippet content={snippet} />}\n    </div>\n  )\n}\n\ninterface INodeToolTip {\n  content?: string\n  items?: { [key: string]: string }\n}\n\nfunction NodeToolTipContent(props: INodeToolTip) {\n  if (props.content !== undefined) {\n    return <div className=\"NodeToolTip\">{props.content}</div>\n  }\n\n  if (props.items !== undefined) {\n    const { items } = props\n    return (\n      <div className=\"NodeToolTip\">\n        {Object.keys(items).map((k) => (\n          <div className=\"NodeToolTipItem\">\n            {k}: {items[k]}\n          </div>\n        ))}\n      </div>\n    )\n  }\n\n  return null\n}\n\nexport function ProfileNode(props: INodeProps) {\n  const info: EntityInfo = (props as any).node.getData()\n  const { id, data, type, snippet, time, counter, size, recordsProduced } = info\n\n  const items = {}\n\n  if (counter !== undefined) {\n    items.Counter = counter\n  }\n\n  if (size !== undefined) {\n    items.Size = size\n  }\n\n  const infoData = data || type\n  return (\n    <div className=\"ProfileContainer\" id={`node-${id}`}>\n      <div className=\"Main\">\n        <div className=\"InfoData\">\n          <RiTooltip delay={TOOLTIP_DELAY_LONG} content={infoData}>\n            <span>{infoData}</span>\n          </RiTooltip>\n        </div>\n        <div className=\"Type\">\n          {[\n            EntityType.GEO,\n            EntityType.NUMERIC,\n            EntityType.TEXT,\n            EntityType.TAG,\n          ].includes(type)\n            ? type\n            : ''}\n        </div>\n      </div>\n      {snippet && <Snippet content={snippet} />}\n      <div className=\"MetaData\">\n        <RiTooltip content={<NodeToolTipContent content=\"Execution Time\" />}>\n          <div className=\"Time\">\n            <div className=\"IconContainer\">\n              <RiIcon className=\"NodeIcon\" size=\"m\" type=\"ExecutionTimeIcon\" />\n            </div>\n            <div>{time} ms</div>\n          </div>\n        </RiTooltip>\n        <RiTooltip\n          content={\n            <NodeToolTipContent\n              {...{\n                items: recordsProduced === undefined ? items : undefined,\n                content: recordsProduced ? 'Records produced' : undefined,\n              }}\n            />\n          }\n        >\n          <div className=\"Size\">\n            <div>\n              {counter !== undefined\n                ? counter\n                : size !== undefined\n                  ? size\n                  : recordsProduced}\n            </div>\n            <div className=\"IconContainer\">\n              <RiIcon className=\"NodeIcon\" size=\"m\" type=\"ContractsIcon\" />\n            </div>\n          </div>\n        </RiTooltip>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/constants.ts",
    "content": "export const NODE_COLOR_BODY_DARK = '#5F95FF'\nexport const NODE_COLOR_BODY_LIGHT = '#8992B3'\n\nexport const EDGE_COLOR_BODY_DARK = '#6B6B6B'\nexport const EDGE_COLOR_BODY_LIGHT = '#8992B3'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/global.d.ts",
    "content": "declare module '@elastic/eui/es/components/icon/*'\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/main.tsx",
    "content": "/* eslint-disable react/jsx-filename-extension */\nimport React from 'react'\nimport { render } from 'react-dom'\nimport { ThemeProvider } from 'uiSrc/components/base/utils/pluginsThemeContext'\nimport { App } from './App'\nimport './styles/styles.scss'\n\nimport data from '../test-data/result-explain-pd.json'\n// import data from '../test-data/result-explain.json'\n// import data from '../test-data/result-profile_r7.json'\n// import data from '../test-data/result-profile_r7--aggregate.json'\n// import data from '../test-data/result-profile_r8.json'\n\ninterface Props {\n  command?: string\n  data?: { response: any; status: string }[]\n  mode?: string\n}\n\n\nconst renderApp = (element: JSX.Element) =>\n  render(element, document.getElementById('app'))\n\nconst renderCore = (props: Props) =>\n  renderApp(\n    <ThemeProvider>\n      <App data={props.data} command={props.command} />\n    </ThemeProvider>,\n  )\n\nif (process.env.NODE_ENV === 'development') {\n  const command = \"FT.EXPLAIN 'idx:bicycle' 'query to search'\"\n  // const command = 'FT.PROFILE \\'idx:bicycle\\' SEARCH QUERY \\'*\\' NOCONTENT'\n\n  renderCore({ command, data, mode: 'ASCII' })\n}\n\n// This is a required action - export the main function for execution of the visualization\nexport default { renderCore }\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/parser.ts",
    "content": "import { v4 as uuidv4 } from 'uuid'\n\nenum TokenType {\n  INIT = 'INIT',\n\n  EOF = 'EOF',\n  ILLEGAL = 'ILLEGAL',\n\n  UNION = 'UNION',\n  INTERSECT = 'INTERSECT',\n  NOT = 'NOT',\n  OPTIONAL = 'OPTIONAL',\n  EXACT = 'EXACT',\n  TAG = 'TAG',\n  VECTOR = 'VECTOR',\n  FUZZY = 'FUZZY',\n  WILDCARD = 'WILDCARD',\n  WILDCARD_EMPTY = 'WILDCARD_EMPTY', // <WILDCARD>}\\n\n  PREFIX = 'PREFIX',\n  GEO_EXPR = 'GEO_EXPR',\n  IDS_EXPR = 'IDS_EXPR',\n  LEXRANGE_EXPR = 'LEXRANGE_EXPR',\n  NUMERIC = 'NUMERIC',\n  LBRACE = 'LBRACE',\n  RBRACE = 'RBRACE',\n  LPAREN = 'LAPAREN',\n  RPAREN = 'RAPAREN',\n  NUMBER = 'NUMBER',\n  NEW_LINE = 'NEW_LINE',\n\n  PLUS = 'PLUS',\n  MINUS = 'MINUS',\n  COMMA = 'COMMA',\n  DOT = 'DOT',\n\n  LESS = 'LESS',\n  GREATER = 'GREATER',\n\n  EQUAL = 'EQUAL',\n  LESS_EQUAL = 'LESS_EQUAL',\n  GREATER_EQUAL = 'GREATER_EQUAL',\n\n  IDENTIFIER = 'IDENTIFIER',\n}\n\nclass Token {\n  T: TokenType\n\n  Data: string\n\n  constructor(t: TokenType, data: string) {\n    this.T = t\n    this.Data = data\n  }\n}\n\nconst KEYWORDS = {\n  [TokenType.EOF.toString()]: TokenType.EOF,\n  [TokenType.ILLEGAL.toString()]: TokenType.ILLEGAL,\n\n  [TokenType.UNION.toString()]: TokenType.UNION,\n  [TokenType.INTERSECT.toString()]: TokenType.INTERSECT,\n  [TokenType.NOT.toString()]: TokenType.NOT,\n  [TokenType.OPTIONAL.toString()]: TokenType.OPTIONAL,\n  [TokenType.EXACT.toString()]: TokenType.EXACT,\n  [TokenType.VECTOR.toString()]: TokenType.VECTOR,\n  [TokenType.TAG.toString()]: TokenType.TAG,\n  [TokenType.NUMERIC.toString()]: TokenType.NUMERIC,\n\n  inf: TokenType.NUMBER,\n}\n\nclass Lexer {\n  Input: string\n\n  Position: number\n\n  ReadPosition: number\n\n  C?: string\n\n  constructor(input: string) {\n    this.Input = input\n    this.Position = 0\n    this.ReadPosition = 0\n    this.C = undefined\n\n    this.ReadChar()\n  }\n\n  ReadChar() {\n    if (this.ReadPosition >= this.Input.length) {\n      this.C = undefined\n    } else {\n      this.C = this.Input[this.ReadPosition]\n    }\n    this.Position = this.ReadPosition++\n  }\n\n  PeekChar() {\n    if (this.ReadPosition >= this.Input.length) {\n      return null\n    }\n    return this.Input[this.ReadPosition]\n  }\n\n  SkipWhitespace() {\n    while (this.C == ' ' || this.C == '\\t' || this.C == '\\r') {\n      this.ReadChar()\n    }\n  }\n\n  ReadIdentifier(): string {\n    let str = ''\n\n    // variable identifiers start with @\n    // For the below expression, we can parse the identifier \"@t1\" successfully\n    // @t1:INTERSECT\n    //\n    // Sample Query - `FT.EXPLAIN idx @t1:hello world @t2:howdy`\n    const startsWithAt = this.C === '@'\n\n    // If a '/' was found, next char can be escaped.\n    //\n    // Sample Query - `FT.EXPLAIN rs:recipes 'very simple | @t:hello @t2:{ free\\\\world } (@n:[1 2]|@n:[3 4]) (@g:[1.5 0.5 0.5 km] -@g:[2.5 1.5 0.5 km])'`\n    let prevEscape = false\n    while (\n      this.C !== undefined &&\n      (isLetter(this.C) ||\n        ['@', ':', '\\\\'].includes(this.C) ||\n        (startsWithAt && isDigit(this.C)) ||\n        // Text can be searched in multiple schemas via '|'\n        //\n        // Example:\n        // FT.CREATE idx SCHEMA t1 TEXT t2 TEXT\n        // FT.EXPLAIN idx '@t1|t2:(text value)'\n        (startsWithAt && this.C === '|') ||\n        (str.startsWith('TAG:@') && isDigit(this.C)) ||\n        prevEscape)\n    ) {\n      str += this.C\n      if (this.C === '\\\\' && this.PeekChar() === '\\\\') {\n        // '\\' appears twice query result when escaped a character.\n        //\n        // For example, if space has to be escaped, instead of '\\ ', you will find '\\\\ '.\n        this.ReadChar() // read of extra '\\'\n        prevEscape = true\n      } else {\n        prevEscape = false\n      }\n      this.ReadChar()\n    }\n    return str\n  }\n\n  ReadNumber(): string {\n    let str = ''\n    while (\n      this.C !== undefined &&\n      (isDigit(this.C) || this.C === '.') &&\n      !Number.isNaN(parseFloat(str + this.C))\n    ) {\n      str += this.C\n      this.ReadChar()\n    }\n    return str\n  }\n\n  NextToken() {\n    let t: Token | null = null\n\n    this.SkipWhitespace()\n\n    switch (this.C) {\n      case '\\n':\n        t = new Token(TokenType.NEW_LINE, this.C)\n        break\n      case '{':\n        t = new Token(TokenType.LBRACE, this.C)\n        break\n      case '}':\n        t = new Token(TokenType.RBRACE, this.C)\n        break\n      case '(':\n        t = new Token(TokenType.LPAREN, this.C)\n        break\n      case ')':\n        t = new Token(TokenType.RPAREN, this.C)\n        break\n      case '+': // TODO: This should be PLUS token\n        t = new Token(TokenType.IDENTIFIER, this.C)\n        break\n      case '-': // TODO: This should be MINUS token\n        t = new Token(TokenType.IDENTIFIER, this.C)\n        const p = this.PeekChar()\n        if (p !== null && isDigit(p)) {\n          this.ReadChar()\n          const n = this.ReadNumber()\n          t = new Token(TokenType.NUMBER, `-${n}`)\n          return t\n        }\n        break\n      case ',':\n        t = new Token(TokenType.COMMA, this.C)\n        break\n      case '.':\n        t = new Token(TokenType.DOT, this.C)\n        break\n      case '<':\n        const lPeekChar = this.PeekChar()\n        if (lPeekChar !== null && lPeekChar === '=') {\n          t = new Token(TokenType.LESS_EQUAL, '<=')\n          this.ReadChar()\n        } else {\n          t = new Token(TokenType.LESS, '<')\n        }\n        break\n      case '>':\n        const rPeekChar = this.PeekChar()\n        if (rPeekChar !== null && rPeekChar === '=') {\n          t = new Token(TokenType.GREATER_EQUAL, '>=')\n          this.ReadChar()\n        } else {\n          t = new Token(TokenType.GREATER, '>')\n        }\n        break\n      case '=':\n        const ePeekChar = this.PeekChar()\n        if (ePeekChar !== null && ePeekChar === '=') {\n          t = new Token(TokenType.EQUAL, '==')\n          this.ReadChar()\n        } else {\n          // No Assign Token\n          t = new Token(TokenType.ILLEGAL, this.C)\n        }\n        break\n      case undefined:\n        t = new Token(TokenType.EOF, '')\n        break\n      default:\n        if (\n          this.C !== undefined &&\n          (isLetter(this.C) || ['@', ':'].includes(this.C))\n        ) {\n          const literal = this.ReadIdentifier()\n          let tokenType = KEYWORDS[literal] || TokenType.IDENTIFIER\n          if (literal.startsWith('TAG:')) {\n            tokenType = TokenType.TAG\n          } else if (literal === 'FUZZY') {\n            tokenType = TokenType.FUZZY\n          } else if (literal === 'WILDCARD') {\n            tokenType = TokenType.WILDCARD\n          } else if (literal === 'PREFIX') {\n            tokenType = TokenType.PREFIX\n          } else if (literal === 'IDS') {\n            tokenType = TokenType.IDS_EXPR\n          } else if (literal === 'LEXRANGE') {\n            tokenType = TokenType.LEXRANGE_EXPR\n          } else if (literal === 'GEO') {\n            tokenType = TokenType.GEO_EXPR\n          } else if (literal.startsWith('@') && literal.endsWith(':OPTIONAL')) {\n            tokenType = TokenType.OPTIONAL\n          } else if (literal.startsWith('@') && literal.endsWith(':NOT')) {\n            tokenType = TokenType.NOT\n          } else if (literal.startsWith('@') && literal.endsWith(':EXACT')) {\n            tokenType = TokenType.EXACT\n          } else if (literal.startsWith('@') && literal.endsWith(':VECTOR')) {\n            tokenType = TokenType.VECTOR\n          } else if (literal.startsWith('@') && literal.endsWith(':UNION')) {\n            tokenType = TokenType.UNION\n          } else if (\n            literal.startsWith('@') &&\n            literal.endsWith(':INTERSECT')\n          ) {\n            tokenType = TokenType.INTERSECT\n          }\n          t = new Token(tokenType, literal)\n          return t\n        }\n        if (this.C !== undefined && isDigit(this.C)) {\n          const n = this.ReadNumber()\n          t = new Token(TokenType.NUMBER, n)\n          return t\n        }\n        t = new Token(TokenType.ILLEGAL, this.C)\n    }\n    this.ReadChar()\n    return t\n  }\n}\n\nexport enum EntityType {\n  Expr = 'Expr',\n  UNION = 'UNION',\n  INTERSECT = 'INTERSECT',\n  OPTIONAL = 'OPTIONAL',\n  NOT = 'NOT',\n  EXACT = 'EXACT',\n  VECTOR = 'VECTOR',\n  NUMERIC = 'NUMERIC',\n\n  // These are used exclusively in FT.PROFILE\n  GEO = 'GEO',\n  FUZZY = 'FUZZY',\n  WILDCARD = 'WILDCARD',\n  PREFIX = 'PREFIX',\n  TEXT = 'TEXT',\n  NUMBER = 'NUMBER',\n  TAG = 'TAG',\n\n  IDS = 'IDS',\n  LEXRANGE = 'LEXRANGE',\n\n  Index = 'Index',\n  Scorer = 'Scorer',\n  Sorter = 'Sorter',\n  Loader = 'Loader',\n\n  CLUSTER_MERGE = 'CLUSTER MERGE',\n}\n\nexport interface EntityInfo {\n  id: string\n  type: EntityType\n  subType?: EntityType\n  data?: string\n  snippet?: string\n  children: EntityInfo[]\n  time?: string\n  counter?: string\n  size?: string\n  parentId?: string\n  parentSnippet?: string\n  level?: number\n  recordsProduced?: string\n}\n\ninterface IAncestors {\n  found: boolean\n  pairs: [string, string][]\n}\n\nexport function GetAncestors(\n  info: EntityInfo,\n  searchId: string,\n  a: IAncestors,\n): IAncestors {\n  if (searchId === info.id) {\n    return {\n      found: true,\n      pairs: info.parentId ? [[info.parentId, info.id]] : [],\n    }\n  }\n  const r: IAncestors = { ...a }\n  for (let i = 0; i < info.children.length; i++) {\n    const c = info.children[i]\n    const ci = GetAncestors(c, searchId, a)\n    if (ci.found) {\n      r.found = true\n      r.pairs = [...a.pairs, ...ci.pairs]\n      if (info.parentId) {\n        r.pairs = [...r.pairs, [info.parentId, info.id]]\n      }\n      return r\n    }\n  }\n  return r\n}\n\nclass Expr {\n  Core: string\n\n  SubType: EntityType\n\n  Time?: string\n\n  Info?: string\n\n  constructor(\n    expr: string,\n    subType: EntityType,\n    info: string | undefined = undefined,\n  ) {\n    this.Core = expr\n    this.SubType = subType\n    this.Info = info\n  }\n\n  toJSON(): EntityInfo {\n    let snippet: string | undefined\n\n    if (this.SubType === EntityType.TAG && this.Info?.startsWith('TAG:')) {\n      snippet = this.Info?.substr(4)\n    } else if (this.SubType === EntityType.GEO) {\n      snippet = this.Info\n      if (snippet?.endsWith(':')) {\n        snippet = snippet?.slice(0, -1)\n      }\n    }\n\n    return {\n      id: uuidv4(),\n      // data: 'Expr',\n      // snippet: this.Core,\n      type: EntityType.Expr,\n      subType: this.SubType,\n      snippet,\n      data: this.Core,\n      children: [],\n      time: this.Time,\n    }\n  }\n}\n\nclass NumericExpr {\n  Left: number\n\n  LSign: Token\n\n  Identifier: Token\n\n  Right: number\n\n  RSign: Token\n\n  constructor(\n    left: number,\n    lsign: Token,\n    identifier: Token,\n    rsign: Token,\n    right: number,\n  ) {\n    this.Left = left\n    this.LSign = lsign\n    this.Identifier = identifier\n    this.Right = right\n    this.RSign = rsign\n  }\n\n  toJSON(): EntityInfo {\n    return {\n      id: uuidv4(),\n      type: EntityType.NUMERIC,\n      data: 'Numeric',\n      snippet: `${this.Left.toString()} ${this.LSign.Data} ${this.Identifier.Data} ${this.RSign.Data} ${this.Right.toString()}`,\n      children: [],\n    }\n  }\n}\n\ntype SearchExpr = NumericExpr | Expr | ExpandExpr\n\ntype ExprTuple2 = SearchExpr[]\n\nclass ExpandExpr {\n  Type: EntityType\n\n  Info?: string\n\n  Core: ExprTuple2\n\n  constructor(type: EntityType, e: ExprTuple2, info?: string) {\n    this.Core = e\n    this.Info = info\n    this.Type = type\n  }\n\n  toJSON(): EntityInfo {\n    const id = uuidv4()\n\n    let snippet: string | undefined\n\n    if (this.Type === EntityType.TAG && this.Info?.startsWith('TAG:')) {\n      snippet = this.Info?.substr(4)\n    }\n\n    if (!this.Info?.startsWith(this.Type)) {\n      snippet = this.Info?.substring(0, this.Info.indexOf(`:${this.Type}`))\n    }\n\n    return {\n      id,\n      type: this.Type,\n      snippet,\n      children: this.Core.map((x) => x.toJSON()).map((d: EntityInfo) => ({\n        ...d,\n        parentId: id,\n        parentSnippet: snippet,\n      })),\n    }\n  }\n}\n\nclass Parser {\n  private L: Lexer\n\n  CurrentToken: Token\n\n  PeekToken: Token\n\n  Errors: string[]\n\n  constructor(l: Lexer) {\n    this.L = l\n\n    this.Errors = []\n    this.CurrentToken = new Token(TokenType.INIT, '')\n    this.PeekToken = new Token(TokenType.INIT, '')\n\n    this.nextToken()\n    this.nextToken()\n  }\n\n  currentTokenIs(t: TokenType) {\n    return this.CurrentToken?.T === t\n  }\n\n  peekTokenIs(t: TokenType) {\n    return this.PeekToken?.T === t\n  }\n\n  nextToken() {\n    this.CurrentToken = this.PeekToken\n    this.PeekToken = this.L.NextToken()\n\n    if (this.CurrentToken.T === TokenType.EOF) {\n      throw new Error(\"Didn't expect EOF token\")\n    }\n  }\n\n  assertToken(t: TokenType) {\n    assertToken(t, this.CurrentToken.T)\n  }\n\n  // Parse an entity which can expand, i.e., has further children of\n  // an entity type.\n  //\n  // Example:\n  // <ENTITY_TYPE> { <ENTITY_TYPE> { ... } (<ENTITY_TYPE> { ... } ...) }\n  parseExpandExpr(t: EntityType): ExpandExpr {\n    assertExpandEntity(t)\n\n    this.assertToken(t as unknown as TokenType)\n\n    const data = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.LBRACE)\n\n    const Exprs: SearchExpr[] = []\n    this.nextToken()\n\n    this.assertToken(TokenType.NEW_LINE)\n\n    this.nextToken()\n\n    while (true) {\n      if (\n        this.CurrentToken.T === TokenType.RBRACE &&\n        this.PeekToken.T === TokenType.NEW_LINE\n      ) {\n        this.nextToken()\n        break\n      }\n\n      const t = this.CurrentToken.T\n\n      if (this.CurrentToken?.T === TokenType.NUMERIC) {\n        Exprs.push(this.parseNumericExpr())\n      } else if (this.CurrentToken?.T === TokenType.IDENTIFIER) {\n        Exprs.push(this.parseExpr())\n      } else if (\n        [\n          TokenType.UNION,\n          TokenType.INTERSECT,\n          TokenType.NOT,\n          TokenType.OPTIONAL,\n          TokenType.EXACT,\n          TokenType.VECTOR,\n          TokenType.TAG,\n        ].includes(t)\n      ) {\n        Exprs.push(this.parseExpandExpr(EntityType[t]))\n      } else if (this.CurrentToken.T === TokenType.GEO_EXPR) {\n        Exprs.push(this.parseGeoExpr())\n      } else if (\n        [TokenType.FUZZY, TokenType.WILDCARD, TokenType.PREFIX].includes(t)\n      ) {\n        Exprs.push(this.parseSimpleExpr(EntityType[t]))\n      } else if (this.CurrentToken.T === TokenType.IDS_EXPR) {\n        Exprs.push(this.parseIdsExpr())\n      } else if (this.CurrentToken.T === TokenType.LEXRANGE_EXPR) {\n        Exprs.push(this.parseLexrangeExpr())\n      } else if (this.CurrentToken.T === TokenType.NUMBER) {\n        Exprs.push(\n          new Expr(this.CurrentToken.Data.toString(), EntityType.NUMBER),\n        )\n      } else if (this.CurrentToken.T === TokenType.LESS) {\n        Exprs.push(this.parseWildcardEmpty())\n      }\n\n      this.nextToken()\n    }\n\n    return new ExpandExpr(t, Exprs, data)\n  }\n\n  parseLexrangeExpr() {\n    this.assertToken(TokenType.LEXRANGE_EXPR)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.LBRACE)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    const first = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.DOT)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.DOT)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.DOT)\n\n    this.nextToken()\n\n    const second = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.RBRACE)\n\n    return new Expr(`${first}...${second}`, EntityType.LEXRANGE)\n  }\n\n  parseIdsExpr() {\n    this.assertToken(TokenType.IDS_EXPR)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.LBRACE)\n\n    this.nextToken()\n\n    const ids: number[] = []\n\n    while (this.CurrentToken.T !== TokenType.RBRACE) {\n      ids.push(parseInt(this.CurrentToken.Data))\n\n      this.nextToken()\n\n      this.assertToken(TokenType.COMMA)\n\n      this.nextToken()\n    }\n\n    this.assertToken(TokenType.RBRACE)\n\n    this.nextToken()\n\n    return new Expr(ids.join(','), EntityType.IDS)\n  }\n\n  // This is a special result.\n  //\n  // Example output: <WILDCARD>}\\n\n  parseWildcardEmpty() {\n    // TODO: Check for WILDCARD_EMPTY\n    this.assertToken(TokenType.LESS)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.WILDCARD)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.GREATER)\n\n    this.nextToken()\n\n    // TODO: Once fixed by redisearch team, remove this.\n    this.assertToken(TokenType.RBRACE)\n\n    return new Expr('<WILDCARD>', EntityType.WILDCARD)\n  }\n\n  parseExpr() {\n    this.assertToken(TokenType.IDENTIFIER)\n\n    let str = ''\n\n    while (this.CurrentToken.T !== TokenType.NEW_LINE) {\n      str += this.CurrentToken.Data\n      this.nextToken()\n    }\n\n    return new Expr(str, EntityType.TEXT)\n  }\n\n  // Parse a very simple entity with format:\n  // <ENTITY_TYPE> { <IDENTIFIER> }\n  parseSimpleExpr(e: EntityType) {\n    assertSimpleEntity(e)\n\n    this.assertToken(TokenType[e])\n\n    this.nextToken()\n\n    this.assertToken(TokenType.LBRACE)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    const identifierData = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.RBRACE)\n\n    this.nextToken()\n\n    return new Expr(identifierData, e)\n  }\n\n  parseGeoExpr() {\n    this.assertToken(TokenType.GEO_EXPR)\n\n    const geoData = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    const identifierData = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.LBRACE)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.NUMBER)\n\n    const first = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.COMMA)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.NUMBER)\n\n    const second = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    assert(this.CurrentToken.Data === '-', 'Expected Identifier to be MINUS')\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    assert(this.CurrentToken.Data === '-', 'Expected Identifier to be MINUS')\n\n    this.nextToken()\n\n    this.assertToken(TokenType.GREATER)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.NUMBER)\n\n    const third = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    const metric = this.CurrentToken.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.RBRACE)\n\n    this.nextToken()\n\n    return new Expr(\n      `${first},${second} --> ${third} ${metric}`,\n      EntityType.GEO,\n      identifierData,\n    )\n  }\n\n  parseNumericExpr() {\n    this.assertToken(TokenType.NUMERIC)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.LBRACE)\n\n    this.nextToken()\n\n    this.assertToken(TokenType.NUMBER)\n\n    const left = this.CurrentToken?.Data\n\n    this.nextToken()\n\n    const lsign = this.CurrentToken // TODO: Check sign\n\n    this.nextToken()\n\n    this.assertToken(TokenType.IDENTIFIER)\n\n    const identifier = this.CurrentToken\n\n    this.nextToken()\n\n    while (this.CurrentToken.T === TokenType.IDENTIFIER) {\n      identifier.Data += this.CurrentToken.Data\n      this.nextToken()\n    }\n\n    const rsign = this.CurrentToken\n\n    this.nextToken()\n\n    this.assertToken(TokenType.NUMBER)\n\n    const right = this.CurrentToken?.Data\n\n    this.nextToken()\n\n    this.assertToken(TokenType.RBRACE)\n\n    this.nextToken() // read off RBRACE\n\n    // assertToken(TokenType.NEW_LINE, this.CurrentToken?.T)\n    //\n    // this.nextToken() // read off new line\n\n    return new NumericExpr(\n      left !== 'inf' ? parseFloat(left) : Infinity,\n      lsign,\n      identifier,\n      rsign,\n      right !== 'inf' ? parseFloat(right) : Infinity,\n    )\n  }\n}\n\nfunction Parse(data: string): SearchExpr {\n  const l = new Lexer(data)\n\n  const p = new Parser(l)\n\n  const t = p.CurrentToken.T\n\n  if (p.CurrentToken?.T === TokenType.NUMERIC) {\n    return p.parseNumericExpr()\n  }\n  if (\n    [\n      TokenType.UNION,\n      TokenType.INTERSECT,\n      TokenType.NOT,\n      TokenType.OPTIONAL,\n      TokenType.EXACT,\n      TokenType.VECTOR,\n      TokenType.TAG,\n    ].includes(t)\n  ) {\n    return p.parseExpandExpr(EntityType[t])\n  }\n  if (p.CurrentToken.T === TokenType.GEO_EXPR) {\n    return p.parseGeoExpr()\n  }\n  if ([TokenType.FUZZY, TokenType.WILDCARD, TokenType.PREFIX].includes(t)) {\n    return p.parseSimpleExpr(EntityType[t])\n  }\n  if (p.CurrentToken.T === TokenType.IDS_EXPR) {\n    return p.parseIdsExpr()\n  }\n  if (p.CurrentToken.T === TokenType.LEXRANGE_EXPR) {\n    return p.parseLexrangeExpr()\n  }\n  if (p.CurrentToken.T === TokenType.LESS) {\n    return p.parseWildcardEmpty()\n  }\n  return p.parseExpr()\n}\n\nexport function ParseExplain(output: string) {\n  return Parse(output).toJSON()\n}\n\nfunction isLetter(str: string): boolean {\n  return str.length === 1 && str.match(/[a-z]/i) !== null\n}\n\nfunction isDigit(str: string): boolean {\n  return str >= '0' && str <= '9'\n}\n\nfunction assert(c: boolean, errorMsg: string) {\n  if (!c) {\n    throw new Error(errorMsg)\n  }\n}\n\nfunction assertToken(expected: TokenType, actual: TokenType | undefined) {\n  if (actual === undefined) {\n    throw new Error('Token is undefined')\n  }\n\n  assert(expected === actual, `Expected ${expected}, Actual: ${actual}`)\n}\n\nfunction assertExpandEntity(t: EntityType) {\n  if (\n    ![\n      EntityType.UNION,\n      EntityType.INTERSECT,\n      EntityType.NOT,\n      EntityType.OPTIONAL,\n      EntityType.EXACT,\n      EntityType.VECTOR,\n      EntityType.TAG,\n    ].includes(t)\n  ) {\n    throw new Error(`${t} is not an expand entity`)\n  }\n}\n\nfunction assertSimpleEntity(t: EntityType) {\n  if (![EntityType.FUZZY, EntityType.WILDCARD, EntityType.PREFIX].includes(t)) {\n    throw new Error(`${t} is not a simple entity`)\n  }\n}\n\nexport function ParseProfileCluster(info: any[]): [Object, EntityInfo] {\n  const clusterInfo: { [key: string]: any[] } = {}\n  let key: string = ''\n  let i = 0\n  while (i < info.length) {\n    if (Array.isArray(info[i])) {\n      clusterInfo[key].push(info[i])\n    } else if (typeof info[i] === 'string') {\n      key = info[i]\n      clusterInfo[key] = []\n    } else {\n      throw new Error(`Expected array or string - ${JSON.stringify(info)}`)\n    }\n    i++\n  }\n\n  const shards: EntityInfo[] = []\n\n  Object.keys(clusterInfo).map((k) => {\n    if (k.toLowerCase().startsWith('shard')) {\n      const shardProfileInfo = ParseProfile(clusterInfo[k])\n      shards.push({\n        id: uuidv4(),\n        type: k as EntityType,\n        children: [shardProfileInfo],\n      })\n    }\n  })\n\n  return [\n    clusterInfo,\n    {\n      id: uuidv4(),\n      type: EntityType.CLUSTER_MERGE,\n      // children: shards,\n      children: Object.keys(clusterInfo)\n        .filter((k) => k.toLowerCase().startsWith('shard'))\n        .map((k) => ParseProfile(clusterInfo[k])),\n    },\n  ]\n}\n\nexport function ParseProfile(shard: Array<any>): EntityInfo {\n  const iterators = findFlatProfile('Iterators profile', shard)\n  let result = iterators ? ParseIteratorProfile(iterators) : null\n  const processorsProfile: string[][] = findFlatProfile(\n    'Result processors profile',\n    shard,\n  )\n\n  for (let i = 0; i < processorsProfile.length; i++) {\n    const e = processorsProfile[i]\n    const id = uuidv4()\n    result = {\n      id,\n      type: e[1] as EntityType,\n      time: e[3],\n      counter: e[5],\n      children: result ? [{ ...result, parentId: id }] : [],\n    }\n  }\n\n  return result as EntityInfo\n}\n\nexport function ParseIteratorProfile(data: any[]): EntityInfo {\n  const props: { [key: string]: any } = {}\n\n  // Parse items with the following format [key1, value1, key2, value2, null, key3, value3, key4, value4_1[], value4_2[]]\n  for (let x = 0; x < data.length; x += 2) {\n    let key = data[x]\n    if (key === null) {\n      while (data[x] === null) {\n        x += 1\n      }\n      key = data[x]\n    }\n\n    let val = data[x + 1]\n\n    while (data[x + 1] === null) x += 1\n    val = data[x + 1]\n\n    if (Array.isArray(val)) {\n      const arr: any[] = []\n      while (x + 1 < data.length && Array.isArray(data[x + 1])) {\n        arr.push(data[x + 1])\n        x += 1\n      }\n      props[key] = arr\n    } else {\n      props[key] = val\n    }\n  }\n\n  const childrens = props['Child iterators'] || props['Child Iterators'] || []\n\n  const id = uuidv4()\n  return {\n    id,\n    type: props.Type || props.TYPE,\n    time: props.Time,\n    counter: props.Counter,\n    size: props.Size,\n    data: props.Term,\n    children: childrens\n      .map(ParseIteratorProfile)\n      .map((d: EntityInfo) => ({ ...d, parentId: id })),\n  }\n\n  // const t: EntityType = props['Type']\n  // if ([EntityType.UNION, EntityType.INTERSECT].includes(t)) {\n  //   const l = data.length\n\n  //   return {\n  //     id: uuidv4(),\n  //     type: t,\n  //     time: data[5],\n  //     counter: data[7],\n  //     children: props['Child iterators'].map(x => ParseIteratorProfile(x)),\n  //   }\n  // // } else if (t === EntityType.NUMERIC) {\n  // //   return {\n  // //     id: uuidv4(),\n  // //     type: EntityType.NUMERIC,\n  // //     snippet: 'Numeric',\n  // //     children: [],\n  // //   }\n  // } else {\n  //   return {\n  //     id: uuidv4(),\n  //     type: data[1],\n  //     data: data[3],\n  //     time: data[5],\n  //     counter: data[7],\n  //     size: data[9],\n  //     children: [],\n  //   }\n  // }\n}\n\nexport enum ModuleType {\n  Graph,\n  Search,\n}\n\nexport enum CoreType {\n  Profile,\n  Explain,\n}\n\nexport function getOutputLevel(output: string) {\n  let i = 0\n  while (output[i] == ' ' && i < output.length) {\n    i++\n  }\n  return (i > 0 ? i / 4 : 0) + 1\n}\n\nfunction ParseEntity(entity: string, children: EntityInfo[]): EntityInfo {\n  const info = entity.trim().split('|')\n\n  let time: string | undefined = ''\n  let size: string | undefined = ''\n\n  const metaData = info.slice(-1)[0].trim()\n\n  // Is GRAPH.PROFILE output\n  if (metaData.startsWith('Records produced')) {\n    ;[size, time] = metaData.trim().split(',')\n\n    size = size.split(': ')[1]\n    time = time.split(': ')[1].split(' ')[0]\n    info.pop()\n  }\n\n  const snippet = [...info.slice(1)].join('|').trim()\n\n  return {\n    id: uuidv4(),\n    type: info[0] as EntityType,\n    snippet,\n    children,\n    time,\n    size,\n    counter: size,\n    level: getOutputLevel(entity),\n  }\n}\n\nexport function ParseGraphV2(output: string[]) {\n  const level = getOutputLevel(output[0]) + 1\n\n  const entity = ParseEntity(output[0], [])\n  const children: EntityInfo[] = []\n\n  const pairs: [number, number][] = []\n\n  let s: number | null = null\n  const e: number | null = null\n  let i = 1\n\n  while (i < output.length) {\n    const l = getOutputLevel(output[i])\n    if (l === level) {\n      if (s == null) {\n        s = i\n      } else if (s != null) {\n        pairs.push([s, i])\n        s = i\n      }\n    }\n    i++\n  }\n\n  if (s !== null) {\n    pairs.push([s, i])\n  }\n\n  for (let k = 0; k < pairs.length; k++) {\n    const p = pairs[k]\n    children.push({\n      ...ParseGraphV2(output.slice(p[0], p[1])),\n      parentId: entity.id,\n    })\n  }\n\n  entity.children = children\n  return entity\n}\n\nexport function GetTotalExecutionTime(g: EntityInfo) {\n  return (\n    parseFloat(g.time || '') +\n    g.children.reduce((a, c) => a + GetTotalExecutionTime(c), 0)\n  )\n}\n\nexport const findFlatProfile = (key: string, profiles: any) => {\n  const index = profiles.findIndex(\n    (k: string) => k?.toLowerCase?.() === key?.toLowerCase?.(),\n  )\n  return index > -1 ? profiles[index + 1] : undefined\n}\n\n// Helper to find a profile by key (redis < 8)\nexport const findProfile = (\n  key: string,\n  profiles: Array<[string, any]>,\n  defautlVal: any = [],\n) => {\n  const [, ...rest] =\n    profiles.find(([k]) => k?.toLowerCase?.() === key?.toLowerCase?.()) || []\n  return rest?.length ? rest : [defautlVal]\n}\n\n// transform profile result to be campatible with redis 8+\nexport const transformProfileResult = (profiles: Array<[string, any]>) => [\n  'Shards',\n  [\n    [\n      'Total profile time',\n      ...findProfile('Total profile time', profiles),\n      'Parsing time',\n      ...findProfile('Parsing time', profiles),\n      'Pipeline creation time',\n      ...findProfile('Pipeline creation time', profiles),\n      'Warning',\n      ...findProfile('Warning', profiles, 'None'),\n      'Iterators profile',\n      ...findProfile('Iterators profile', profiles),\n      'Result processors profile',\n      findProfile('Result processors profile', profiles),\n    ],\n  ],\n  'Coordinator',\n  [],\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/styles/_dark_theme.less",
    "content": ".theme_DARK {\n  @import (less) '../../node_modules/@elastic/eui/dist/eui_theme_dark.min.css';\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/styles/_light_theme.less",
    "content": ".theme_LIGHT {\n  @import (less) '../../node_modules/@elastic/eui/dist/eui_theme_light.min.css';\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/src/styles/styles.scss",
    "content": "@charset \"UTF-8\";\n\n* {\n  margin: 0px;\n  padding: 0px;\n}\n\n#mainApp::-webkit-scrollbar {\n  width: 2em;\n}\n\n#mainApp::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid transparent;\n    background-clip: content-box;\n}\n\n#container-parent::-webkit-scrollbar {\n  width: 16px;\n  height: 16px;\n}\n\n#container-parent::-webkit-scrollbar-corner, #container-parent::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n#container-parent::-webkit-scrollbar-thumb {\n  background-color: rgba(105, 112, 125, 0.5);\n  border: 6px solid transparent;\n  background-clip: content-box;\n}\n\n.theme_DARK {\n  --info-background: #2b2b2b;;\n  --info-color: white;\n  --svg-background: #010101;\n  --tooltip-background: #3E4B5E;\n\n  --node-border-color: #3D3D3D;\n  --node-border-shadow: #17336952;\n  --node-metadata-border-color: #3C3C3C;\n  --node-metadata-color: #B5B6C0;\n  --node-background: #212021;\n  --edge-background: #6B6B6B;\n  --text-color: #DFE5EF;\n}\n\n.theme_LIGHT {\n  --info-background: #EBEFFA;\n  --info-color: black;\n  --svg-background: #FFFFFF;\n  --tooltip-background: white;\n\n  --node-border-color: #E4EAF2;\n  --node-border-shadow: #17336926;\n  --node-metadata-border-color: #CCD7E7;\n  --node-metadata-color: #415681;\n  --node-background: #FFF;\n  --edge-background: #8992B3;\n  --text-color: #173369;\n}\n\nbody {\n    background-color: var(--svg-background);\n    overflow: hidden;\n}\n\n\n* div,\n* span {\n  font-family: 'Graphik', sans-serif !important;\n}\n\n.euiPagination__list {\n  list-style: none;\n  padding-inline-start: 0px;\n}\n\n\n.responseFail {\n  color: #e06c75;\n  padding: 12px !important;\n  font-family: monospace !important;\n}\n\n.parsedRedisReply {\n  white-space: pre-wrap;\n  word-break: break-all;\n  font: normal normal normal 14px/17px Inconsolata !important;\n  padding: 4px 12px !important;\n  color: var(--info-color);\n}\n\n.responseInfo {\n  color: var(--info-color);\n  padding: 12px !important;\n  font-family: monospace !important;\n}\n\n.euiToolTip {\n  color: var(--info-color) !important;\n  background-color: var(--tooltip-background) !important;\n  font-size: 12px !important;\n}\n\n.euiToolTip__arrow {\n  background-color: var(--tooltip-background) !important;\n}\n\n\n.euiToolTipAnchor {\n  display: unset !important;\n}\n\n.InfoData {\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  // Make text selectable\n  -moz-user-select: text;\n  -khtml-user-select: text;\n  -webkit-user-select: text;\n  -ms-user-select: text;\n  user-select: text;\n}\n\n.FooterCommon {\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  color: var(--node-metadata-color);\n  font-size: 12px;\n\n\n  // Make text selectable\n  -moz-user-select: text;\n  -khtml-user-select: text;\n  -webkit-user-select: text;\n  -ms-user-select: text;\n  user-select: text;\n}\n\n.ProfileContainer {\n  width: 320px;\n  min-height: 84px;\n\n  padding-left: 18px !important;\n  padding-right: 18px !important;\n\n  box-shadow: 0px 3px 12px var(--node-border-shadow);\n  border: 1px solid var(--node-border-color) !important;\n  border-radius: 4px;\n  opacity: 1;\n\n  display: flex;\n  flex-direction: column;\n  justify-content: space-around;\n\n  background-color: var(--node-background);\n  color: var(--text-color);\n\n  &:hover {\n      outline: 2px solid #85A2FE !important;\n      box-sizing: border-box;\n  }\n\n  .Main {\n\n      display: flex;\n      justify-content: space-between;\n      line-height: 18px;\n\n      font-size: 13px;\n      color: var(--text-color);\n\n      padding-top: 12px;\n      padding-bottom: 12px;\n\n      .Type {\n          color: #CE915B;\n          text-transform: lowercase;\n      }\n  }\n\n  .Footer{\n    padding-top: 4px;\n    padding-bottom: 12px;\n    height: auto;\n  }\n\n  .MetaData {\n      display: flex;\n      justify-content: space-between;\n      line-height: 18px;\n      color: var(--node-metadata-color);\n\n      padding-top: 12px;\n      padding-bottom: 12px;\n\n      height: 42px;             //FIXME: fixed height?\n      border-top: 0.5px solid var(--node-metadata-border-color);\n\n      .Time {\n          display: flex;\n          align-items: center;\n          font-size: 12px;\n\n          div:first-child {\n              padding-right: 5px;\n          }\n\n      }\n\n      .Size {\n          display: flex;\n          align-items: center;\n          font-size: 13px;\n\n          div:first-child {\n              padding-right: 5px;\n          }\n\n\n      }\n\n      .IconContainer {\n          > svg {\n              width: 12px !important;\n              height: 13px !important;\n          }\n      }\n  }\n}\n\n.ExplainContainer {\n  width: 240px;\n  min-height: 42px;\n\n  box-shadow: 0px 3px 12px var(--node-border-shadow);\n  border: 1px solid var(--node-border-color) !important;\n  border-radius: 4px;\n  opacity: 1;\n\n  padding: 12px 18px 12px 18px !important;\n\n  display: flex;\n  flex-direction: column;\n  justify-content: space-around;\n\n  background-color: var(--node-background);\n\n  &:hover {\n      outline: 2px solid #85A2FE !important;\n      box-sizing: border-box;\n  }\n\n  .Main {\n    // height: 42px;\n    font-size: 13px;\n    color: var(--text-color);\n\n    .Info {\n        display: flex;\n        justify-content: space-between;\n        .Type {\n            color: #CE915B;\n            text-transform: lowercase;\n        }\n    }\n\n  }\n\n  .Footer{\n    height: 24px;\n    padding-top: 6px;\n  }\n}\n\n\n.ProfileInfo {\n  display: flex;\n  justify-content: center;\n  color: var(--text-color);\n\n  font-family: 'Graphik', sans-serif !important;\n  font-size: 13px;\n\n  padding-bottom: 3px !important;\n\n  position: absolute;\n  bottom: 12px;\n  width: 100%;\n}\n\n.ProfileTimeInfo {\n    background-color: var(--info-background);\n\n    padding-top: 3px !important;\n\n    .Item {\n\n        padding-left: 18px;\n        padding-right: 18px;\n\n        .Key {\n            font-size: 13px;\n            line-height: 18px;\n        }\n\n        .Value {\n            font-size: 18px;\n            line-height: 24px;\n        }\n    }\n}\n\n.ProfileTimeMini {\n  background-color: var(--svg-background);\n}\n\n.NodeType {\n  color: #CE915B;\n}\n\n.NodeMetadata {\n  display: flex;\n  justify-content: space-between;\n  font-size: 12px;\n  color: darkgrey;\n}\n\n.NodeInfoDivider {\n  width: 284px;\n  height: 29px;\n  border-bottom: 1px solid #3D3D3D !important;\n  position: absolute;\n  color: #B5B6C0;\n}\n\n.NodeTime {\n  display: flex;\n  align-items: center;\n}\n\n\n.NodeContentInfo {\n  display: flex;\n  align-items: center;\n}\n\n\n.NodeTimeIcon {\n  padding-right: 5px !important;\n}\n\n.NodeContentIcon {\n  padding-left: 5px !important;\n}\n\n.Box {\n  width: 5px;\n  height: 9px;\n  background-color: #FF6280;\n\n  align-self: center;\n\n  margin-left: 2px !important;\n}\n\n.ZoomMenu {\n    position: absolute;\n    bottom: 110px;\n    right: 10px;\n    border-right: 4px;\n    box-shadow: 0 1px 6px rgb(0 0 0 / 16%), 0 1px 6px rgb(0 0 0 / 23%);\n    display: flex;\n    flex-direction: column;\n}\n\n.CollapseButton {\n    color: var(--text-color);\n    line-height: 18px;\n    font-size: 12px;\n    display: flex;\n    justify-content: center;\n    cursor: pointer;\n\n    div:first-child {\n        padding-right: 5px;\n    }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/test-data/result-explain-pd.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": \"INTERSECT {\\\\n  UNION {\\\\n    query\\\\n    +queri(expanded)\\\\n    queri(expanded)\\\\n  }\\\\n  UNION {\\\\n    search\\\\n    +search(expanded)\\\\n  }\\\\n}\\\\n\"\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/test-data/result-explain.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      \"Results\",\n      \"    Project\",\n      \"        Conditional Traverse | (h:State)->(p:President)\",\n      \"            Filter\",\n      \"                Node By Label Scan | (h:State)\"\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7--aggregate.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      [1, [], [], [], [], [], [], [], [], [], []],\n      [\n        [\"Total profile time\", \"0\"],\n        [\"Parsing time\", \"0\"],\n        [\"Pipeline creation time\", \"0\"],\n        [\"Warning\"],\n        [\"Iterators profile\", [\"Type\", \"WILDCARD\", \"Time\", \"0\", \"Counter\", 10]],\n        [\n          \"Result processors profile\",\n          [\"Type\", \"Index\", \"Time\", \"0\", \"Counter\", 10]\n        ]\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r7.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      [\n        10,\n        \"bicycle:1\",\n        \"bicycle:2\",\n        \"bicycle:4\",\n        \"bicycle:5\",\n        \"bicycle:0\",\n        \"bicycle:6\",\n        \"bicycle:7\",\n        \"bicycle:9\",\n        \"bicycle:3\",\n        \"bicycle:8\"\n      ],\n      [\n        [\"Total profile time\", \"1\"],\n        [\"Parsing time\", \"2\"],\n        [\"Pipeline creation time\", \"3\"],\n        [\"Warning\"],\n        [\"Iterators profile\", [\"Type\", \"WILDCARD\", \"Time\", \"0\", \"Counter\", 10]],\n        [\n          \"Result processors profile\",\n          [\"Type\", \"Index\", \"Time\", \"0\", \"Counter\", 10],\n          [\"Type\", \"Scorer\", \"Time\", \"0\", \"Counter\", 10],\n          [\"Type\", \"Sorter\", \"Time\", \"0\", \"Counter\", 10]\n        ]\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/ri-explain/test-data/result-profile_r8.json",
    "content": "[\n  {\n    \"status\": \"success\",\n    \"response\": [\n      [\n        10,\n        \"bicycle:1\",\n        \"bicycle:2\",\n        \"bicycle:4\",\n        \"bicycle:5\",\n        \"bicycle:0\",\n        \"bicycle:6\",\n        \"bicycle:7\",\n        \"bicycle:9\",\n        \"bicycle:3\",\n        \"bicycle:8\"\n      ],\n      [\n        \"Shards\",\n        [\n          [\n            \"Total profile time\",\n            \"1\",\n            \"Parsing time\",\n            \"2\",\n            \"Pipeline creation time\",\n            \"3\",\n            \"Warning\",\n            \"None\",\n            \"Iterators profile\",\n            [\"Type\", \"WILDCARD\", \"Time\", \"0\", \"Counter\", 10],\n            \"Result processors profile\",\n            [\n              [\"Type\", \"Index\", \"Time\", \"0\", \"Counter\", 10],\n              [\"Type\", \"Scorer\", \"Time\", \"0\", \"Counter\", 10],\n              [\"Type\", \"Sorter\", \"Time\", \"0\", \"Counter\", 10]\n            ]\n          ]\n        ],\n        \"Coordinator\",\n        []\n      ]\n    ]\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/packages/vite.config.mjs",
    "content": "import { defineConfig } from 'vite';\nimport { transform } from 'esbuild';\nimport react from '@vitejs/plugin-react';\nimport svgr from 'vite-plugin-svgr';\nimport { ViteEjsPlugin } from 'vite-plugin-ejs';\nimport { viteStaticCopy } from 'vite-plugin-static-copy';\nimport path, { resolve } from 'path';\nimport { fileURLToPath } from 'url';\nimport { defaultConfig } from '../config/default';\n\nconst riPlugins = [\n  { name: 'redisearch', entry: 'src/main.tsx' },\n  { name: 'clients-list', entry: 'src/main.tsx' },\n  { name: 'redisgraph', entry: 'src/main.tsx' },\n  { name: 'redistimeseries-app', entry: 'src/main.tsx' },\n  { name: 'ri-explain', entry: 'src/main.tsx' },\n];\n\n/**\n * @type {import('vite').UserConfig}\n */\nexport default defineConfig({\n  plugins: [\n    react(),\n    svgr({ include: ['**/*.svg?react'] }),\n    ViteEjsPlugin(),\n    minifyEs(),\n    // Copy public static for all plugins\n    viteStaticCopy({\n      silent: true,\n      targets: riPlugins.map(({ name: pluginDir }) => ({\n        src: `./${pluginDir}/public/*`,\n        dest: `./${pluginDir}/dist/`,\n      })),\n    }),\n  ],\n  resolve: {\n    alias: {\n      lodash: 'lodash-es',\n      '@elastic/eui$': '@elastic/eui/optimize/lib',\n      '@redislabsdev/redis-ui-components': '@redis-ui/components',\n      '@redislabsdev/redis-ui-styles': '@redis-ui/styles',\n      '@redislabsdev/redis-ui-icons': '@redis-ui/icons',\n      '@redislabsdev/redis-ui-table': '@redis-ui/table',\n      uiSrc: fileURLToPath(new URL('../../src', import.meta.url)),\n      apiSrc: fileURLToPath(new URL('../../../api/src', import.meta.url)),\n    },\n  },\n  server: {\n    port: 8081,\n    fs: {\n      allow: ['..'],\n    },\n  },\n  envPrefix: 'RI_',\n  build: {\n    outDir: './',\n    cssCodeSplit: true,\n    lib: {\n      // Multi entries\n      entry: Object.fromEntries(\n        riPlugins.map(({ name: pluginDir, entry }) => [\n          pluginDir,\n          resolve(__dirname, `./${pluginDir}/${entry}`),\n        ]),\n      ),\n    },\n\n    rollupOptions: {\n      output: [\n        {\n          dir: './',\n          format: 'esm',\n          entryFileNames: '[name]/dist/index.js',\n          assetFileNames: '[name]/dist/styles.css',\n          chunkFileNames: 'common/index.js',\n        },\n      ],\n    },\n    target: 'es2022',\n    minify: 'esbuild',\n\n    define: {\n      this: 'window',\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      scss: {\n        // add @layer app for css ordering. Styles without layer have the highest priority\n        // https://github.com/vitejs/vite/issues/3924\n        additionalData: (source, filename) => {\n          if (path.extname(filename) === '.scss') {\n            const skipFiles = [\n              '/main.scss',\n              '/App.scss',\n              '/packages/clients-list/src/styles/styles.scss',\n              '/packages/redisearch/src/styles/styles.scss',\n            ];\n            if (skipFiles.every((file) => !filename.endsWith(file))) {\n              return `\n                @use \"uiSrc/styles/mixins/_eui.scss\";\n                @use \"uiSrc/styles/mixins/_global.scss\";\n                @layer app { ${source} }\n              `;\n            }\n          }\n          return source;\n        },\n      },\n    },\n  },\n  define: {\n    global: 'globalThis',\n    'process.env': {},\n    // setup default riConfig since it might be used in constants\n    riConfig: defaultConfig,\n  },\n});\n\nfunction minifyEs() {\n  return {\n    name: 'minifyEs',\n    renderChunk: {\n      order: 'post',\n      async handler(code, chunk, outputOptions) {\n        if (outputOptions.format === 'es' && chunk.fileName.endsWith('.js')) {\n          return transform(code, { minify: true });\n        }\n        return code;\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/analytics/AnalyticsPage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { BrowserRouter } from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\n\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport AnalyticsPage, { Props } from './AnalyticsPage'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\n/**\n * AnalyticsPage tests\n *\n * @group component\n */\ndescribe('AnalyticsPage', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <BrowserRouter>\n          <AnalyticsPage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams, useLocation } from 'react-router-dom'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  appContextAnalytics,\n  setLastAnalyticsPage,\n} from 'uiSrc/slices/app/context'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { useConnectionType } from 'uiSrc/components/hooks/useConnectionType'\n\nimport AnalyticsPageRouter from './AnalyticsPageRouter'\n\nexport interface Props {\n  routes: any[]\n}\n\nconst AnalyticsPage = ({ routes = [] }: Props) => {\n  const history = useHistory()\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { pathname } = useLocation()\n  const connectionType = useConnectionType()\n  const { lastViewedPage } = useSelector(appContextAnalytics)\n  const pathnameRef = useRef<string>('')\n\n  const dispatch = useDispatch()\n\n  useEffect(\n    () => () => {\n      dispatch(setLastAnalyticsPage(pathnameRef.current))\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (\n      pathname === Pages.clusterDetails(instanceId) &&\n      connectionType !== ConnectionType.Cluster\n    ) {\n      history.push(Pages.databaseAnalysis(instanceId))\n      return\n    }\n\n    if (pathname === Pages.analytics(instanceId)) {\n      // restore current inner page and ignore context (as we store context on unmount)\n      if (pathnameRef.current && pathnameRef.current !== lastViewedPage) {\n        history.push(pathnameRef.current)\n        return\n      }\n\n      // restore from context\n      if (lastViewedPage) {\n        history.push(lastViewedPage)\n        return\n      }\n\n      history.push(\n        connectionType === ConnectionType.Cluster\n          ? Pages.clusterDetails(instanceId)\n          : Pages.databaseAnalysis(instanceId),\n      )\n    }\n\n    pathnameRef.current =\n      pathname === Pages.analytics(instanceId) ? '' : pathname\n  }, [pathname])\n\n  return <AnalyticsPageRouter routes={routes} />\n}\n\nexport default AnalyticsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport AnalyticsPageRouter from './AnalyticsPageRouter'\n\nconst mockedRoutes = [\n  {\n    path: '/slowlog',\n  },\n]\n\ndescribe('AnalyticsPageRouter', () => {\n  it('should render', () => {\n    expect(\n      render(<AnalyticsPageRouter routes={mockedRoutes} />, {\n        withRouter: true,\n      }),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\n\nexport interface Props {\n  routes: any[]\n}\nconst InstancePageRouter = ({ routes }: Props) => (\n  <Switch>\n    {routes.map((route, i) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <RouteWithSubRoutes key={i} {...route} />\n    ))}\n  </Switch>\n)\n\nexport default React.memo(InstancePageRouter)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/analytics/index.ts",
    "content": "import AnalyticsPage from './AnalyticsPage'\nimport AnalyticsPageRouter from './AnalyticsPageRouter'\n\nexport { AnalyticsPage, AnalyticsPageRouter }\n\nexport default AnalyticsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/analytics/styles.module.scss",
    "content": ".main {\n  margin: 0 16px 0;\n  height: 100%;\n  background-color: var(--euiColorEmptyShade);\n  padding: 18px 24px;\n  overflow: auto;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure/AzurePage.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\n\nexport interface Props {\n  routes: any[]\n}\n\nconst AzurePage = ({ routes = [] }: Props) => (\n  <Switch>\n    {routes.map((route, i) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <RouteWithSubRoutes key={i} {...route} />\n    ))}\n  </Switch>\n)\n\nexport default AzurePage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure/index.ts",
    "content": "export { default as AzurePage } from './AzurePage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/AzureDatabases/AzureDatabases.constants.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef, Table } from 'uiSrc/components/base/layout/table'\nimport { AzureRedisDatabase } from 'uiSrc/slices/interfaces'\nimport { Text } from 'uiSrc/components/base/text'\nimport { ColumnHeader } from 'uiSrc/components/column-header'\nimport { DescriptionsTooltip } from 'uiSrc/pages/autodiscover-azure/components'\nimport {\n  AZURE_DATABASE_TYPE_DESCRIPTIONS,\n  AZURE_PROVISIONING_STATE_DESCRIPTIONS,\n} from 'uiSrc/pages/autodiscover-azure/constants'\n\nexport const MAX_DATABASES_SELECTION = 10\n\nexport const AZURE_DATABASES_COLUMNS: ColumnDef<AzureRedisDatabase>[] = [\n  {\n    id: 'row-selection',\n    maxSize: 20,\n    size: 20,\n    isHeaderCustom: true,\n    header: ({ table }) => (\n      <Table.HeaderMultiRowSelectionButton\n        table={table}\n        data-testid=\"row-selection\"\n      />\n    ),\n    cell: ({ row }) => (\n      <Table.RowSelectionButton\n        row={row}\n        data-testid={`row-selection-${row.id}`}\n      />\n    ),\n  },\n  {\n    id: 'name',\n    header: 'Database Name',\n    accessorKey: 'name',\n    enableSorting: true,\n    cell: ({ getValue }) => <Text size=\"M\">{getValue() as string}</Text>,\n  },\n  {\n    id: 'type',\n    accessorKey: 'type',\n    enableSorting: true,\n    isHeaderCustom: true,\n    header: () => (\n      <ColumnHeader\n        label=\"Type\"\n        tooltip={\n          <DescriptionsTooltip\n            descriptions={AZURE_DATABASE_TYPE_DESCRIPTIONS}\n          />\n        }\n      />\n    ),\n    cell: ({ getValue }) => (\n      <Text size=\"M\" style={{ textTransform: 'capitalize' }}>\n        {getValue() as string}\n      </Text>\n    ),\n  },\n  {\n    id: 'location',\n    header: 'Region',\n    accessorKey: 'location',\n    enableSorting: true,\n    cell: ({ getValue }) => <Text size=\"M\">{getValue() as string}</Text>,\n  },\n  {\n    id: 'provisioningState',\n    accessorKey: 'provisioningState',\n    enableSorting: true,\n    isHeaderCustom: true,\n    header: () => (\n      <ColumnHeader\n        label=\"Status\"\n        tooltip={\n          <DescriptionsTooltip\n            descriptions={AZURE_PROVISIONING_STATE_DESCRIPTIONS}\n          />\n        }\n      />\n    ),\n    cell: ({ getValue }) => <Text size=\"M\">{getValue() as string}</Text>,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/AzureDatabases/AzureDatabases.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { AzureRedisDatabase, AzureRedisType } from 'uiSrc/slices/interfaces'\nimport AzureDatabases, { Props } from './AzureDatabases'\n\nconst mockDatabase = (): AzureRedisDatabase => ({\n  id: faker.string.uuid(),\n  name: faker.internet.domainWord(),\n  host: faker.internet.domainName(),\n  port: 6379,\n  type: AzureRedisType.Standard,\n  location: faker.location.city(),\n  provisioningState: 'Succeeded',\n  resourceGroup: faker.string.alphanumeric(10),\n  subscriptionId: faker.string.uuid(),\n})\n\ndescribe('AzureDatabases', () => {\n  const defaultProps: Props = {\n    databases: [],\n    selectedDatabases: [],\n    subscriptionName: 'Test Subscription',\n    loading: false,\n    error: '',\n    onBack: jest.fn(),\n    onClose: jest.fn(),\n    onSubmit: jest.fn(),\n    onSelectionChange: jest.fn(),\n    onRefresh: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<Props>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<AzureDatabases {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(renderComponent()).toBeTruthy()\n  })\n\n  it('should render title', () => {\n    renderComponent()\n    expect(screen.getByText('Azure Redis Databases')).toBeInTheDocument()\n  })\n\n  it('should render subscription name', () => {\n    renderComponent({ subscriptionName: 'My Azure Subscription' })\n    expect(screen.getByText('Subscription:')).toBeInTheDocument()\n    expect(screen.getByText('My Azure Subscription')).toBeInTheDocument()\n  })\n\n  it('should render databases in table', () => {\n    const databases = [mockDatabase(), mockDatabase()]\n    renderComponent({ databases })\n\n    expect(screen.getByText(databases[0].name)).toBeInTheDocument()\n    expect(screen.getByText(databases[1].name)).toBeInTheDocument()\n  })\n\n  it('should render empty state when no databases', () => {\n    renderComponent({ databases: [] })\n    expect(\n      screen.getByText('No Redis databases found in this subscription.'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render error message when error is provided', () => {\n    const errorMessage = 'Failed to fetch databases'\n    renderComponent({ databases: [], error: errorMessage })\n    expect(screen.getByText(errorMessage)).toBeInTheDocument()\n  })\n\n  it('should render loader when loading', () => {\n    renderComponent({ loading: true })\n    expect(screen.getByTestId('azure-databases-loader')).toBeInTheDocument()\n  })\n\n  it('should call onBack when back button is clicked', () => {\n    const onBack = jest.fn()\n    renderComponent({ onBack })\n\n    fireEvent.click(screen.getByTestId('btn-back-adding'))\n    expect(onBack).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onClose when cancel button is clicked', () => {\n    const onClose = jest.fn()\n    renderComponent({ onClose })\n\n    fireEvent.click(screen.getByText('Cancel'))\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should have disabled submit button when no databases are selected', () => {\n    const databases = [mockDatabase()]\n    renderComponent({ databases, selectedDatabases: [] })\n\n    const submitButton = screen.getByTestId('btn-submit')\n    expect(submitButton).toBeDisabled()\n  })\n\n  it('should have disabled submit button when loading', () => {\n    const databases = [mockDatabase()]\n    renderComponent({ databases, selectedDatabases: databases, loading: true })\n\n    const submitButton = screen.getByTestId('btn-submit')\n    expect(submitButton).toBeDisabled()\n  })\n\n  it('should show count in submit button when databases are selected', () => {\n    const databases = [mockDatabase(), mockDatabase()]\n    renderComponent({ databases, selectedDatabases: databases })\n\n    expect(screen.getByText(/Add \\(2\\) Databases/i)).toBeInTheDocument()\n  })\n\n  it('should show singular \"Database\" when one database is selected', () => {\n    const databases = [mockDatabase()]\n    renderComponent({ databases, selectedDatabases: databases })\n\n    expect(screen.getByText(/Add \\(1\\) Database$/i)).toBeInTheDocument()\n  })\n\n  it('should filter databases when searching', () => {\n    const databases = [\n      { ...mockDatabase(), name: 'production-redis' },\n      { ...mockDatabase(), name: 'development-redis' },\n    ]\n    renderComponent({ databases })\n\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, { target: { value: 'production' } })\n\n    expect(screen.getByText('production-redis')).toBeInTheDocument()\n    expect(screen.queryByText('development-redis')).not.toBeInTheDocument()\n  })\n\n  it('should preserve search filter when loading state changes', () => {\n    const databases = [\n      { ...mockDatabase(), name: 'production-redis' },\n      { ...mockDatabase(), name: 'development-redis' },\n    ]\n    const { rerender } = renderComponent({ databases, loading: false })\n\n    // Filter the list\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, { target: { value: 'production' } })\n    expect(screen.queryByText('development-redis')).not.toBeInTheDocument()\n\n    // Rerender with loading=true - filter should still be applied\n    rerender(<AzureDatabases {...defaultProps} databases={databases} loading />)\n    expect(screen.queryByText('development-redis')).not.toBeInTheDocument()\n  })\n\n  it('should call onSubmit when submit button is clicked', () => {\n    const databases = [mockDatabase()]\n    const onSubmit = jest.fn()\n    renderComponent({ databases, selectedDatabases: databases, onSubmit })\n\n    fireEvent.click(screen.getByTestId('btn-submit'))\n    expect(onSubmit).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onRefresh when refresh button is clicked', () => {\n    const onRefresh = jest.fn()\n    renderComponent({ onRefresh })\n\n    fireEvent.click(screen.getByTestId('btn-refresh-databases'))\n    expect(onRefresh).toHaveBeenCalledTimes(1)\n  })\n\n  it('should have disabled refresh button when loading', () => {\n    renderComponent({ loading: true })\n\n    const refreshButton = screen.getByTestId('btn-refresh-databases')\n    expect(refreshButton).toBeDisabled()\n  })\n\n  it('should sync rowSelection with selectedDatabases prop changes', () => {\n    const databases = [mockDatabase(), mockDatabase()]\n    const { rerender } = render(\n      <AzureDatabases\n        {...defaultProps}\n        databases={databases}\n        selectedDatabases={databases}\n      />,\n    )\n\n    // Initially both databases are selected, so max message should not show\n    expect(\n      screen.queryByTestId('max-selection-message'),\n    ).not.toBeInTheDocument()\n\n    // Simulate parent resetting selectedDatabases (e.g., on refresh)\n    rerender(\n      <AzureDatabases\n        {...defaultProps}\n        databases={databases}\n        selectedDatabases={[]}\n      />,\n    )\n\n    // After reset, no databases should be selected\n    expect(\n      screen.queryByTestId('max-selection-message'),\n    ).not.toBeInTheDocument()\n    expect(screen.getByText(/Add Database/)).toBeInTheDocument()\n  })\n\n  it('should show max selection message when 10 databases are selected', () => {\n    // Use exactly 10 databases so pagination doesn't kick in (pagination at > 10)\n    const databases = Array.from({ length: 10 }, () => mockDatabase())\n    renderComponent({ databases })\n\n    // Select all 10 databases by clicking on their rows\n    databases.forEach((db) => {\n      fireEvent.click(screen.getByText(db.name))\n    })\n\n    expect(screen.getByTestId('max-selection-message')).toBeInTheDocument()\n    expect(\n      screen.getByText(/Maximum of 10 databases can be added at a time/),\n    ).toBeInTheDocument()\n  })\n\n  it('should not show max selection message when less than 10 databases are selected', () => {\n    const databases = Array.from({ length: 10 }, () => mockDatabase())\n    renderComponent({ databases })\n\n    // Select only 5 databases\n    databases.slice(0, 5).forEach((db) => {\n      fireEvent.click(screen.getByText(db.name))\n    })\n\n    expect(\n      screen.queryByTestId('max-selection-message'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should not allow selecting more than 10 databases', () => {\n    // Use exactly 10 databases so pagination doesn't kick in\n    const databases = Array.from({ length: 10 }, () => mockDatabase())\n    const onSelectionChange = jest.fn()\n    renderComponent({ databases, onSelectionChange })\n\n    // Select all 10 databases\n    databases.forEach((db) => {\n      fireEvent.click(screen.getByText(db.name))\n    })\n\n    // Verify 10 databases were selected\n    const lastCall = onSelectionChange.mock.calls.at(-1)?.[0]\n    expect(lastCall?.length).toBe(10)\n\n    // Clicking an already selected database should deselect it\n    fireEvent.click(screen.getByText(databases[0].name))\n    const afterDeselect = onSelectionChange.mock.calls.at(-1)?.[0]\n    expect(afterDeselect?.length).toBe(9)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/AzureDatabases/AzureDatabases.tsx",
    "content": "import React, { useEffect, useState } from 'react'\n\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\nimport {\n  type RowSelectionState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { AzureRedisDatabase } from 'uiSrc/slices/interfaces'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  IconButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { RefreshIcon } from 'uiSrc/components/base/icons'\n\nimport {\n  AZURE_DATABASES_COLUMNS,\n  MAX_DATABASES_SELECTION,\n} from './AzureDatabases.constants'\n\nexport interface Props {\n  databases: AzureRedisDatabase[]\n  selectedDatabases: AzureRedisDatabase[]\n  subscriptionName: string\n  loading: boolean\n  error: string\n  onBack: () => void\n  onClose: () => void\n  onSubmit: () => void\n  onSelectionChange: (databases: AzureRedisDatabase[]) => void\n  onRefresh: () => void\n}\n\nconst AzureDatabases = ({\n  databases,\n  selectedDatabases,\n  subscriptionName,\n  loading,\n  error,\n  onBack,\n  onClose,\n  onSubmit,\n  onSelectionChange,\n  onRefresh,\n}: Props) => {\n  const [items, setItems] = useState<AzureRedisDatabase[]>(databases)\n\n  useEffect(() => {\n    setItems(databases)\n  }, [databases])\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n    const filtered = databases.filter(\n      (item) =>\n        item.name?.toLowerCase().includes(value) ||\n        item.host?.toLowerCase().includes(value) ||\n        item.resourceGroup?.toLowerCase().includes(value),\n    )\n\n    setItems(filtered)\n  }\n\n  const [rowSelection, setRowSelection] = useState<RowSelectionState>({})\n\n  // Sync rowSelection with selectedDatabases prop (e.g., when parent resets on refresh)\n  useEffect(() => {\n    const newSelection: RowSelectionState = {}\n    selectedDatabases.forEach((db) => {\n      newSelection[db.id] = true\n    })\n    setRowSelection(newSelection)\n  }, [selectedDatabases])\n\n  const selectedCount = Object.values(rowSelection).filter(Boolean).length\n  const isMaxSelected = selectedCount >= MAX_DATABASES_SELECTION\n\n  const handleSelectionChange = (state: RowSelectionState) => {\n    const selectedIds = Object.keys(state).filter((id) => state[id])\n\n    // If trying to select more than max, limit to max\n    if (selectedIds.length > MAX_DATABASES_SELECTION) {\n      const limitedIds = selectedIds.slice(0, MAX_DATABASES_SELECTION)\n      const limitedState: RowSelectionState = {}\n      limitedIds.forEach((id) => {\n        limitedState[id] = true\n      })\n      setRowSelection(limitedState)\n      onSelectionChange(databases.filter((db) => limitedState[db.id]))\n      return\n    }\n\n    setRowSelection(state)\n    onSelectionChange(databases.filter((db) => state[db.id]))\n  }\n\n  const handleRowClick = (database: AzureRedisDatabase) => {\n    const isSelected = rowSelection[database.id]\n\n    // Don't allow selecting more if max is reached\n    if (!isSelected && isMaxSelected) {\n      return\n    }\n\n    const newSelection = { ...rowSelection, [database.id]: !isSelected }\n\n    if (isSelected) {\n      delete newSelection[database.id]\n    }\n\n    handleSelectionChange(newSelection)\n  }\n\n  const canSelectRow = (row: { original: AzureRedisDatabase }) =>\n    rowSelection[row.original.id] || !isMaxSelected\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title=\"Azure Redis Databases\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n          backButtonText=\"Subscriptions\"\n          subTitle={\n            <Row gap=\"l\" align=\"center\">\n              <Text size=\"M\">\n                Subscription:{' '}\n                <Text component=\"span\" variant=\"semiBold\">\n                  {subscriptionName}\n                </Text>\n              </Text>\n              <IconButton\n                icon={RefreshIcon}\n                onClick={onRefresh}\n                disabled={loading}\n                aria-label=\"Refresh databases\"\n                data-testid=\"btn-refresh-databases\"\n              />\n            </Row>\n          }\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            rowSelectionMode=\"multiple\"\n            rowSelection={rowSelection}\n            onRowSelectionChange={handleSelectionChange}\n            onRowClick={handleRowClick}\n            getRowId={(row) => row.id}\n            getRowCanSelect={canSelectRow}\n            columns={AZURE_DATABASES_COLUMNS}\n            data={items}\n            defaultSorting={[{ id: 'name', desc: false }]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() =>\n              loading ? (\n                <Col full centered>\n                  <Loader size=\"xl\" data-testid=\"azure-databases-loader\" />\n                </Col>\n              ) : (\n                <EmptyState\n                  message={\n                    error || 'No Redis databases found in this subscription.'\n                  }\n                />\n              )\n            }\n          />\n        </DatabaseWrapper>\n      </DatabaseContainer>\n\n      <Spacer size=\"l\" />\n\n      <Footer>\n        <Row justify=\"between\" align=\"center\">\n          {isMaxSelected ? (\n            <Text size=\"S\" data-testid=\"max-selection-message\">\n              Maximum of {MAX_DATABASES_SELECTION} databases can be added at a\n              time.\n            </Text>\n          ) : (\n            <div />\n          )}\n          <Row gap=\"m\" grow={false}>\n            <SecondaryButton onClick={onClose}>Cancel</SecondaryButton>\n            <PrimaryButton\n              data-testid=\"btn-submit\"\n              disabled={selectedDatabases.length === 0 || loading}\n              loading={loading}\n              onClick={onSubmit}\n            >\n              Add{' '}\n              {selectedDatabases.length > 0\n                ? `(${selectedDatabases.length})`\n                : ''}{' '}\n              Database\n              {selectedDatabases.length !== 1 ? 's' : ''}\n            </PrimaryButton>\n          </Row>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default AzureDatabases\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/AzureDatabases/index.ts",
    "content": "export { default } from './AzureDatabases'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/AzureDatabasesPage.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport { faker } from '@faker-js/faker'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  fireEvent,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  azureSelector,\n  fetchDatabasesAzure,\n  clearDatabasesAzure,\n} from 'uiSrc/slices/instances/azure'\nimport { azureAuthAccountSelector } from 'uiSrc/slices/oauth/azure'\nimport { AzureRedisDatabase, AzureRedisType } from 'uiSrc/slices/interfaces'\nimport { AzureAccountFactory } from 'uiSrc/mocks/factories/cloud/AzureAccount.factory'\n\nimport AzureDatabasesPage from './AzureDatabasesPage'\n\njest.mock('uiSrc/slices/instances/azure', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/azure'),\n  azureSelector: jest.fn(),\n  fetchDatabasesAzure: jest\n    .fn()\n    .mockReturnValue({ type: 'fetchDatabasesAzure' }),\n  clearDatabasesAzure: jest\n    .fn()\n    .mockReturnValue({ type: 'clearDatabasesAzure' }),\n}))\n\njest.mock('uiSrc/slices/oauth/azure', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/azure'),\n  azureAuthAccountSelector: jest.fn(),\n}))\n\nconst mockDatabase = (): AzureRedisDatabase => ({\n  id: faker.string.uuid(),\n  name: faker.internet.domainWord(),\n  host: faker.internet.domainName(),\n  port: 6379,\n  type: AzureRedisType.Standard,\n  location: faker.location.city(),\n  provisioningState: 'Succeeded',\n  resourceGroup: faker.string.alphanumeric(10),\n  subscriptionId: faker.string.uuid(),\n})\n\nconst mockSubscription = {\n  subscriptionId: faker.string.uuid(),\n  displayName: 'Test Subscription',\n  state: 'Enabled',\n}\n\nconst mockAccount = AzureAccountFactory.build()\n\nconst defaultAzureState = {\n  loading: false,\n  error: '',\n  subscriptions: [mockSubscription],\n  selectedSubscription: mockSubscription,\n  databases: [],\n  databasesAdded: [],\n  loaded: {\n    subscriptions: true,\n    databases: false,\n    databasesAdded: false,\n  },\n}\n\nlet store: typeof mockedStore\n\nconst mockedAzureSelector = azureSelector as jest.Mock\nconst mockedAzureAuthAccountSelector = azureAuthAccountSelector as jest.Mock\nconst mockedFetchDatabasesAzure = fetchDatabasesAzure as jest.Mock\n\ndescribe('AzureDatabasesPage', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n    mockedAzureSelector.mockReturnValue(defaultAzureState)\n    mockedAzureAuthAccountSelector.mockReturnValue(mockAccount)\n    mockedFetchDatabasesAzure.mockClear()\n  })\n\n  it('should render', () => {\n    expect(render(<AzureDatabasesPage />, { store })).toBeTruthy()\n  })\n\n  it('should redirect to subscriptions page when no subscription is selected', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    mockedAzureSelector.mockReturnValue({\n      ...defaultAzureState,\n      selectedSubscription: null,\n    })\n\n    render(<AzureDatabasesPage />, { store })\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.azureSubscriptions)\n  })\n\n  it('should fetch databases on mount when not already loaded', () => {\n    render(<AzureDatabasesPage />, { store })\n\n    expect(fetchDatabasesAzure).toHaveBeenCalledWith(\n      mockAccount.id,\n      mockSubscription.subscriptionId,\n    )\n  })\n\n  it('should not fetch databases when already loaded', () => {\n    mockedAzureSelector.mockReturnValue({\n      ...defaultAzureState,\n      loaded: { ...defaultAzureState.loaded, databases: true },\n    })\n\n    render(<AzureDatabasesPage />, { store })\n\n    expect(fetchDatabasesAzure).not.toHaveBeenCalled()\n  })\n\n  it('should navigate to home when cancel is clicked', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    const databases = [mockDatabase()]\n    mockedAzureSelector.mockReturnValue({\n      ...defaultAzureState,\n      databases,\n      loaded: { ...defaultAzureState.loaded, databases: true },\n    })\n\n    render(<AzureDatabasesPage />, { store })\n\n    fireEvent.click(screen.getByText('Cancel'))\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.home)\n  })\n\n  it('should navigate to subscriptions when back is clicked', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    const databases = [mockDatabase()]\n    mockedAzureSelector.mockReturnValue({\n      ...defaultAzureState,\n      databases,\n      loaded: { ...defaultAzureState.loaded, databases: true },\n    })\n\n    render(<AzureDatabasesPage />, { store })\n\n    fireEvent.click(screen.getByTestId('btn-back-adding'))\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.azureSubscriptions)\n  })\n\n  describe('refresh functionality', () => {\n    it('should clear and refetch databases when refresh is clicked', () => {\n      const databases = [mockDatabase()]\n      mockedAzureSelector.mockReturnValue({\n        ...defaultAzureState,\n        databases,\n        loaded: { ...defaultAzureState.loaded, databases: true },\n      })\n\n      render(<AzureDatabasesPage />, { store })\n\n      fireEvent.click(screen.getByTestId('btn-refresh-databases'))\n\n      expect(clearDatabasesAzure).toHaveBeenCalled()\n      expect(fetchDatabasesAzure).toHaveBeenCalledWith(\n        mockAccount.id,\n        mockSubscription.subscriptionId,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/AzureDatabasesPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Pages } from 'uiSrc/constants'\nimport { setTitle } from 'uiSrc/utils'\nimport { Text } from 'uiSrc/components/base/text'\nimport { fetchInstancesAction } from 'uiSrc/slices/instances/instances'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport errorMessages from 'uiSrc/components/notifications/error-messages'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { defaultContainerId } from 'uiSrc/components/notifications/constants'\nimport { AppDispatch } from 'uiSrc/slices/store'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  ActionStatus,\n  AzureRedisDatabase,\n  ImportAzureDatabaseResponse,\n} from 'uiSrc/slices/interfaces'\nimport { azureAuthAccountSelector } from 'uiSrc/slices/oauth/azure'\nimport {\n  addDatabasesAzureAction,\n  azureSelector,\n  clearDatabasesAzure,\n  fetchDatabasesAzure,\n} from 'uiSrc/slices/instances/azure'\nimport AzureDatabases from './AzureDatabases/AzureDatabases'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\nconst groupErrorsByMessage = (\n  failedResults: ImportAzureDatabaseResponse[],\n  selectedDatabases: AzureRedisDatabase[],\n): Record<string, string[]> =>\n  failedResults.reduce<Record<string, string[]>>((acc, r) => {\n    const db = selectedDatabases.find((db) => db.id === r.id)\n    const dbName = db?.name || 'database'\n    const errorMessage = r.message || 'Failed to add database'\n\n    if (!acc[errorMessage]) {\n      acc[errorMessage] = []\n    }\n    acc[errorMessage].push(dbName)\n    return acc\n  }, {})\n\nconst renderErrorList = (errorGroups: Record<string, string[]>) =>\n  Object.entries(errorGroups).map(([errorMessage, dbNames]) => (\n    <React.Fragment key={errorMessage}>\n      <div>\n        <Text variant=\"semiBold\" component=\"span\">\n          {dbNames.join(', ')}\n        </Text>{' '}\n        - {errorMessage}\n      </div>\n      <Spacer size=\"m\" />\n    </React.Fragment>\n  ))\n\nconst showErrorToast = (\n  failedResults: ImportAzureDatabaseResponse[],\n  selectedDatabases: AzureRedisDatabase[],\n) => {\n  const errorGroups = groupErrorsByMessage(failedResults, selectedDatabases)\n  const errorList = renderErrorList(errorGroups)\n\n  riToast(\n    errorMessages.DEFAULT(\n      <>{errorList}</>,\n      () => {},\n      `Failed to add ${failedResults.length} database${failedResults.length > 1 ? 's' : ''}`,\n    ),\n    {\n      variant: riToast.Variant.Danger,\n      containerId: defaultContainerId,\n    },\n  )\n}\n\nconst AzureDatabasesPage = () => {\n  const history = useHistory()\n  const dispatch = useDispatch<AppDispatch>()\n  const account = useSelector(azureAuthAccountSelector)\n  const { loading, error, databases, selectedSubscription, loaded } =\n    useSelector(azureSelector)\n\n  // Local state for selected databases (UI state)\n  const [selectedDatabases, setSelectedDatabases] = useState<\n    AzureRedisDatabase[]\n  >([])\n\n  useEffect(() => {\n    // Redirect to home if not authenticated\n    if (!account) {\n      history.push(Pages.home)\n      return\n    }\n\n    // Redirect to subscriptions if no subscription selected\n    if (!selectedSubscription) {\n      history.push(Pages.azureSubscriptions)\n      return\n    }\n\n    setTitle('Azure Databases')\n\n    // Only fetch if not already loaded\n    if (!loaded.databases) {\n      dispatch(\n        fetchDatabasesAzure(account.id, selectedSubscription.subscriptionId),\n      )\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [account, selectedSubscription])\n\n  const handleBack = () => {\n    setSelectedDatabases([])\n    history.push(Pages.azureSubscriptions)\n  }\n\n  const handleClose = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_IMPORT_DATABASES_CANCELLED,\n    })\n    history.push(Pages.home)\n  }\n\n  const handleSuccess = (successResults: ImportAzureDatabaseResponse[]) => {\n    dispatch(fetchInstancesAction())\n\n    const successDb = selectedDatabases.find(\n      (db) => db.id === successResults[0]?.id,\n    )\n    dispatch(\n      addMessageNotification(\n        successMessages.ADDED_NEW_INSTANCE(\n          successResults.length > 1\n            ? `${successResults.length} databases`\n            : successDb?.name || 'Database',\n        ),\n      ),\n    )\n  }\n\n  const handleSubmit = async () => {\n    if (!account?.id || selectedDatabases.length === 0) {\n      return\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_IMPORT_DATABASES_SUBMITTED,\n      eventData: {\n        totalDatabases: selectedDatabases.length,\n      },\n    })\n\n    const databaseIds = selectedDatabases.map((db) => db.id)\n    const results = await dispatch(\n      addDatabasesAzureAction(account.id, databaseIds),\n    )\n\n    const successResults = results.filter(\n      (r) => r.status === ActionStatus.Success,\n    )\n    const failedResults = results.filter((r) => r.status === ActionStatus.Fail)\n\n    if (successResults.length > 0) {\n      handleSuccess(successResults)\n    }\n\n    if (failedResults.length > 0) {\n      showErrorToast(failedResults, selectedDatabases)\n    }\n\n    // Only navigate home if all databases were added successfully\n    if (successResults.length > 0 && failedResults.length === 0) {\n      history.push(Pages.home)\n    }\n  }\n\n  const handleRefresh = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_DATABASES_REFRESH_CLICKED,\n    })\n    if (account?.id && selectedSubscription) {\n      dispatch(clearDatabasesAzure())\n      dispatch(\n        fetchDatabasesAzure(account.id, selectedSubscription.subscriptionId),\n      )\n      setSelectedDatabases([])\n    }\n  }\n\n  return (\n    <AzureDatabases\n      databases={databases || []}\n      selectedDatabases={selectedDatabases}\n      subscriptionName={selectedSubscription?.displayName || ''}\n      loading={loading}\n      error={error}\n      onBack={handleBack}\n      onClose={handleClose}\n      onSubmit={handleSubmit}\n      onSelectionChange={setSelectedDatabases}\n      onRefresh={handleRefresh}\n    />\n  )\n}\n\nexport default AzureDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-databases/index.ts",
    "content": "export { default as AzureDatabasesPage } from './AzureDatabasesPage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/AzureSubscriptions/AzureSubscriptions.constants.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef, Table } from 'uiSrc/components/base/layout/table'\nimport { AzureSubscription } from 'uiSrc/slices/interfaces'\nimport { Text } from 'uiSrc/components/base/text'\nimport { ColumnHeader } from 'uiSrc/components/column-header'\nimport { DescriptionsTooltip } from 'uiSrc/pages/autodiscover-azure/components'\nimport { AZURE_SUBSCRIPTION_STATE_DESCRIPTIONS } from 'uiSrc/pages/autodiscover-azure/constants'\n\nexport const AZURE_SUBSCRIPTIONS_COLUMNS: ColumnDef<AzureSubscription>[] = [\n  {\n    id: 'row-selection',\n    maxSize: 15,\n    size: 15,\n    isHeaderCustom: true,\n    header: '#',\n    cell: ({ row }) => (\n      <Table.RowSelectionButton\n        row={row}\n        data-testid={`row-selection-${row.id}`}\n      />\n    ),\n  },\n  {\n    id: 'displayName',\n    header: 'Subscription Name',\n    accessorKey: 'displayName',\n    enableSorting: true,\n    cell: ({ getValue }) => <Text size=\"M\">{getValue() as string}</Text>,\n  },\n  {\n    id: 'subscriptionId',\n    header: 'Subscription ID',\n    accessorKey: 'subscriptionId',\n    enableSorting: true,\n    cell: ({ getValue }) => <Text size=\"M\">{getValue() as string}</Text>,\n  },\n  {\n    id: 'state',\n    accessorKey: 'state',\n    enableSorting: true,\n    isHeaderCustom: true,\n    header: () => (\n      <ColumnHeader\n        label=\"State\"\n        tooltip={\n          <DescriptionsTooltip\n            descriptions={AZURE_SUBSCRIPTION_STATE_DESCRIPTIONS}\n          />\n        }\n      />\n    ),\n    cell: ({ getValue }) => <Text size=\"M\">{getValue() as string}</Text>,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/AzureSubscriptions/AzureSubscriptions.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { AzureSubscription } from 'uiSrc/slices/interfaces'\nimport AzureSubscriptions, { Props } from './AzureSubscriptions'\n\njest.mock('uiSrc/slices/oauth/azure', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/azure'),\n  azureAuthAccountSelector: jest.fn().mockReturnValue({\n    id: 'test-account-id',\n    username: 'test@example.com',\n    name: 'Test User',\n  }),\n}))\n\nconst mockSubscription = (): AzureSubscription => ({\n  subscriptionId: faker.string.uuid(),\n  displayName: faker.company.name(),\n  state: 'Enabled',\n  tenantId: faker.string.uuid(),\n})\n\ndescribe('AzureSubscriptions', () => {\n  const defaultProps: Props = {\n    subscriptions: [],\n    loading: false,\n    error: '',\n    onBack: jest.fn(),\n    onClose: jest.fn(),\n    onSubmit: jest.fn(),\n    onSwitchAccount: jest.fn(),\n    onRefresh: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<Props>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<AzureSubscriptions {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(renderComponent()).toBeTruthy()\n  })\n\n  it('should render title', () => {\n    renderComponent()\n    expect(screen.getByText('Azure Subscriptions')).toBeInTheDocument()\n  })\n\n  it('should render signed in user', () => {\n    renderComponent()\n    expect(screen.getByText('Signed in as')).toBeInTheDocument()\n    expect(screen.getByText('test@example.com')).toBeInTheDocument()\n  })\n\n  it('should call onSwitchAccount when switch account button is clicked', () => {\n    const onSwitchAccount = jest.fn()\n    renderComponent({ onSwitchAccount })\n\n    fireEvent.click(screen.getByTestId('btn-switch-account'))\n    expect(onSwitchAccount).toHaveBeenCalledTimes(1)\n  })\n\n  it('should render subscriptions in table', () => {\n    const subscriptions = [mockSubscription(), mockSubscription()]\n    renderComponent({ subscriptions })\n\n    expect(screen.getByText(subscriptions[0].displayName)).toBeInTheDocument()\n    expect(screen.getByText(subscriptions[1].displayName)).toBeInTheDocument()\n  })\n\n  it('should render empty state when no subscriptions', () => {\n    renderComponent({ subscriptions: [] })\n    expect(\n      screen.getByText('No Azure subscriptions found for this account.'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render error message when error is provided', () => {\n    const errorMessage = 'Failed to fetch subscriptions'\n    renderComponent({ subscriptions: [], error: errorMessage })\n    expect(screen.getByText(errorMessage)).toBeInTheDocument()\n  })\n\n  it('should render loader when loading', () => {\n    renderComponent({ loading: true })\n    expect(screen.getByTestId('azure-subscriptions-loader')).toBeInTheDocument()\n  })\n\n  it('should call onBack when back button is clicked', () => {\n    const onBack = jest.fn()\n    renderComponent({ onBack })\n\n    fireEvent.click(screen.getByTestId('btn-back-adding'))\n    expect(onBack).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onClose when cancel button is clicked', () => {\n    const onClose = jest.fn()\n    renderComponent({ onClose })\n\n    fireEvent.click(screen.getByText('Cancel'))\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should have disabled submit button when no subscription is selected', () => {\n    const subscriptions = [mockSubscription()]\n    renderComponent({ subscriptions })\n\n    const submitButton = screen.getByText('Show Databases')\n    expect(submitButton).toBeDisabled()\n  })\n\n  it('should have disabled submit button when loading', () => {\n    const subscriptions = [mockSubscription()]\n    renderComponent({ subscriptions, loading: true })\n\n    const submitButton = screen.getByText('Show Databases')\n    expect(submitButton).toBeDisabled()\n  })\n\n  it('should filter subscriptions when searching', () => {\n    const subscriptions = [\n      { ...mockSubscription(), displayName: 'Production Subscription' },\n      { ...mockSubscription(), displayName: 'Development Subscription' },\n    ]\n    renderComponent({ subscriptions })\n\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, { target: { value: 'Production' } })\n\n    expect(screen.getByText('Production Subscription')).toBeInTheDocument()\n    expect(\n      screen.queryByText('Development Subscription'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should call onRefresh when refresh button is clicked', () => {\n    const onRefresh = jest.fn()\n    renderComponent({ onRefresh })\n\n    fireEvent.click(screen.getByTestId('btn-refresh-subscriptions'))\n    expect(onRefresh).toHaveBeenCalledTimes(1)\n  })\n\n  it('should have disabled refresh button when loading', () => {\n    renderComponent({ loading: true })\n\n    const refreshButton = screen.getByTestId('btn-refresh-subscriptions')\n    expect(refreshButton).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/AzureSubscriptions/AzureSubscriptions.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\nimport {\n  type RowSelectionState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { AzureSubscription } from 'uiSrc/slices/interfaces'\nimport { azureAuthAccountSelector } from 'uiSrc/slices/oauth/azure'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  EmptyButton,\n  IconButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { RefreshIcon } from 'uiSrc/components/base/icons'\n\nimport { AZURE_SUBSCRIPTIONS_COLUMNS } from './AzureSubscriptions.constants'\n\nexport interface Props {\n  subscriptions: AzureSubscription[]\n  loading: boolean\n  error: string\n  onBack: () => void\n  onClose: () => void\n  onSubmit: (subscription: AzureSubscription) => void\n  onSwitchAccount: () => void\n  onRefresh: () => void\n}\n\nconst AzureSubscriptions = ({\n  subscriptions,\n  loading,\n  error,\n  onBack,\n  onClose,\n  onSubmit,\n  onSwitchAccount,\n  onRefresh,\n}: Props) => {\n  const account = useSelector(azureAuthAccountSelector)\n  const [items, setItems] = useState<AzureSubscription[]>(subscriptions)\n  const [selectedId, setSelectedId] = useState<string | null>(null)\n\n  useEffect(() => {\n    setItems(subscriptions)\n  }, [subscriptions])\n\n  // Reset selection if selected subscription no longer exists (e.g., after refresh)\n  useEffect(() => {\n    if (\n      selectedId &&\n      !subscriptions.some((s) => s.subscriptionId === selectedId)\n    ) {\n      setSelectedId(null)\n    }\n  }, [subscriptions, selectedId])\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n    const filtered = subscriptions.filter(\n      (item) =>\n        item.displayName?.toLowerCase().includes(value) ||\n        item.subscriptionId?.toLowerCase().includes(value),\n    )\n\n    setItems(filtered)\n  }\n\n  const handleSelectionChange = (state: RowSelectionState) => {\n    const newSelectedId = Object.keys(state).find((key) => state[key])\n    setSelectedId(newSelectedId || null)\n  }\n\n  const handleRowClick = (subscription: AzureSubscription) => {\n    const isSelected = selectedId === subscription.subscriptionId\n    const newState: RowSelectionState = isSelected\n      ? {}\n      : { [subscription.subscriptionId]: true }\n\n    handleSelectionChange(newState)\n  }\n\n  const handleSubmit = () => {\n    if (!selectedId) return\n\n    const selected = subscriptions.find((s) => s.subscriptionId === selectedId)\n\n    if (!selected) {\n      // Subscription no longer exists (e.g., after refresh), reset selection\n      setSelectedId(null)\n      return\n    }\n\n    onSubmit(selected)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title=\"Azure Subscriptions\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n          subTitle={\n            account && (\n              <Row gap=\"l\" align=\"center\">\n                <Text size=\"M\">\n                  Signed in as{' '}\n                  <Text component=\"span\" variant=\"semiBold\">\n                    {account.username}\n                  </Text>\n                </Text>\n                <EmptyButton\n                  variant=\"primary-inline\"\n                  onClick={onSwitchAccount}\n                  data-testid=\"btn-switch-account\"\n                >\n                  Switch account\n                </EmptyButton>\n                <IconButton\n                  icon={RefreshIcon}\n                  onClick={onRefresh}\n                  disabled={loading}\n                  aria-label=\"Refresh subscriptions\"\n                  data-testid=\"btn-refresh-subscriptions\"\n                />\n              </Row>\n            )\n          }\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            rowSelectionMode=\"single\"\n            rowSelection={selectedId ? { [selectedId]: true } : {}}\n            onRowSelectionChange={handleSelectionChange}\n            onRowClick={handleRowClick}\n            getRowId={(row) => row.subscriptionId}\n            columns={AZURE_SUBSCRIPTIONS_COLUMNS}\n            data={items}\n            defaultSorting={[{ id: 'displayName', desc: false }]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() =>\n              loading ? (\n                <Col full centered>\n                  <Loader size=\"xl\" data-testid=\"azure-subscriptions-loader\" />\n                </Col>\n              ) : (\n                <EmptyState\n                  message={\n                    error || 'No Azure subscriptions found for this account.'\n                  }\n                />\n              )\n            }\n          />\n        </DatabaseWrapper>\n      </DatabaseContainer>\n\n      <Spacer size=\"m\" />\n\n      <Footer>\n        <Row justify=\"end\">\n          <Row gap=\"m\" grow={false}>\n            <SecondaryButton onClick={onClose}>Cancel</SecondaryButton>\n            <PrimaryButton\n              disabled={!selectedId || loading}\n              loading={loading}\n              onClick={handleSubmit}\n            >\n              Show Databases\n            </PrimaryButton>\n          </Row>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default AzureSubscriptions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/AzureSubscriptions/index.ts",
    "content": "export { default } from './AzureSubscriptions'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/AzureSubscriptionsPage.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport { faker } from '@faker-js/faker'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  fireEvent,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  azureSelector,\n  fetchSubscriptionsAzure,\n  clearSubscriptionsAzure,\n} from 'uiSrc/slices/instances/azure'\nimport { useAzureAuth } from 'uiSrc/components/hooks/useAzureAuth'\nimport { azureAuthAccountSelector } from 'uiSrc/slices/oauth/azure'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { AzureAccountFactory } from 'uiSrc/mocks/factories/cloud/AzureAccount.factory'\n\nimport AzureSubscriptionsPage from './AzureSubscriptionsPage'\n\njest.mock('uiSrc/slices/instances/azure', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/azure'),\n  azureSelector: jest.fn(),\n  fetchSubscriptionsAzure: jest\n    .fn()\n    .mockReturnValue({ type: 'fetchSubscriptionsAzure' }),\n  clearSubscriptionsAzure: jest\n    .fn()\n    .mockReturnValue({ type: 'clearSubscriptionsAzure' }),\n}))\n\njest.mock('uiSrc/components/hooks/useAzureAuth', () => ({\n  useAzureAuth: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/oauth/azure', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/azure'),\n  azureAuthAccountSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockSubscription = {\n  subscriptionId: faker.string.uuid(),\n  displayName: 'Test Subscription',\n  state: 'Enabled',\n  tenantId: faker.string.uuid(),\n}\n\nconst mockAccount = AzureAccountFactory.build()\n\nconst defaultAzureState = {\n  loading: false,\n  error: '',\n  subscriptions: [mockSubscription],\n  selectedSubscription: null,\n  databases: [],\n  databasesAdded: [],\n  loaded: {\n    subscriptions: true,\n    databases: false,\n    databasesAdded: false,\n  },\n}\n\nlet store: typeof mockedStore\n\nconst mockedAzureSelector = azureSelector as jest.Mock\nconst mockedAzureAuthAccountSelector = azureAuthAccountSelector as jest.Mock\nconst mockedUseAzureAuth = useAzureAuth as jest.Mock\nconst mockedSendEventTelemetry = sendEventTelemetry as jest.Mock\n\ndescribe('AzureSubscriptionsPage', () => {\n  const mockInitiateLogin = jest.fn()\n\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n    mockedAzureSelector.mockReturnValue(defaultAzureState)\n    mockedAzureAuthAccountSelector.mockReturnValue(mockAccount)\n    mockedUseAzureAuth.mockReturnValue({\n      initiateLogin: mockInitiateLogin,\n      account: mockAccount,\n      loading: false,\n      error: '',\n    })\n    mockedSendEventTelemetry.mockClear()\n    mockInitiateLogin.mockClear()\n  })\n\n  it('should redirect to home when not authenticated', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    mockedUseAzureAuth.mockReturnValue({\n      initiateLogin: mockInitiateLogin,\n      account: null,\n    })\n\n    render(<AzureSubscriptionsPage />, { store })\n\n    expect(pushMock).toHaveBeenCalledWith(Pages.home)\n  })\n\n  it('should fetch subscriptions on mount when not already loaded', () => {\n    mockedAzureSelector.mockReturnValue({\n      ...defaultAzureState,\n      loaded: { ...defaultAzureState.loaded, subscriptions: false },\n    })\n\n    render(<AzureSubscriptionsPage />, { store })\n\n    expect(fetchSubscriptionsAzure).toHaveBeenCalledWith(mockAccount.id)\n  })\n\n  describe('switch account', () => {\n    it('should call initiateLogin when switch account button is clicked', () => {\n      render(<AzureSubscriptionsPage />, { store })\n\n      fireEvent.click(screen.getByTestId('btn-switch-account'))\n\n      expect(mockInitiateLogin).toHaveBeenCalledTimes(1)\n    })\n\n    it('should send telemetry when switch account is clicked', () => {\n      render(<AzureSubscriptionsPage />, { store })\n\n      fireEvent.click(screen.getByTestId('btn-switch-account'))\n\n      expect(mockedSendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.AZURE_SWITCH_ACCOUNT_CLICKED,\n      })\n    })\n  })\n\n  describe('refresh', () => {\n    it('should clear and refetch subscriptions when refresh is clicked', () => {\n      render(<AzureSubscriptionsPage />, { store })\n\n      fireEvent.click(screen.getByTestId('btn-refresh-subscriptions'))\n\n      expect(clearSubscriptionsAzure).toHaveBeenCalled()\n      expect(fetchSubscriptionsAzure).toHaveBeenCalledWith(mockAccount.id)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/AzureSubscriptionsPage.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Pages } from 'uiSrc/constants'\nimport { setTitle } from 'uiSrc/utils'\nimport { useAzureAuth } from 'uiSrc/components/hooks/useAzureAuth'\nimport { AzureSubscription } from 'uiSrc/slices/interfaces'\nimport { AppDispatch } from 'uiSrc/slices/store'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  azureSelector,\n  clearSubscriptionsAzure,\n  fetchSubscriptionsAzure,\n  setSelectedSubscriptionAzure,\n} from 'uiSrc/slices/instances/azure'\nimport AzureSubscriptions from './AzureSubscriptions/AzureSubscriptions'\n\nconst AzureSubscriptionsPage = () => {\n  const history = useHistory()\n  const dispatch = useDispatch<AppDispatch>()\n  const { initiateLogin, account } = useAzureAuth()\n  const { loading, error, subscriptions, loaded } = useSelector(azureSelector)\n\n  useEffect(() => {\n    // Redirect to home if not authenticated\n    if (!account) {\n      history.push(Pages.home)\n      return\n    }\n\n    setTitle('Azure Subscriptions')\n\n    // Only fetch if not already loaded or if account changed\n    if (!loaded.subscriptions) {\n      dispatch(fetchSubscriptionsAzure(account.id))\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [account])\n\n  const handleBack = () => {\n    history.push(Pages.home)\n  }\n\n  const handleClose = () => {\n    history.push(Pages.home)\n  }\n\n  const handleSubmit = (subscription: AzureSubscription) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_SUBSCRIPTION_SELECTED,\n    })\n    dispatch(setSelectedSubscriptionAzure(subscription))\n    history.push(Pages.azureDatabases)\n  }\n\n  const handleRefresh = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_SUBSCRIPTIONS_REFRESH_CLICKED,\n    })\n    if (account?.id) {\n      dispatch(clearSubscriptionsAzure())\n      dispatch(fetchSubscriptionsAzure(account.id))\n    }\n  }\n\n  const handleSwitchAccount = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_SWITCH_ACCOUNT_CLICKED,\n    })\n    initiateLogin()\n  }\n\n  return (\n    <AzureSubscriptions\n      subscriptions={subscriptions || []}\n      loading={loading}\n      error={error}\n      onBack={handleBack}\n      onClose={handleClose}\n      onSubmit={handleSubmit}\n      onSwitchAccount={handleSwitchAccount}\n      onRefresh={handleRefresh}\n    />\n  )\n}\n\nexport default AzureSubscriptionsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/azure-subscriptions/index.ts",
    "content": "export { default as AzureSubscriptionsPage } from './AzureSubscriptionsPage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/components/DescriptionsTooltip.tsx",
    "content": "import React from 'react'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport interface DescriptionsTooltipProps {\n  descriptions: Record<string, string>\n}\n\nexport const DescriptionsTooltip = ({\n  descriptions,\n}: DescriptionsTooltipProps) => (\n  <Col gap=\"s\">\n    {Object.entries(descriptions).map(([key, description]) => (\n      <Text key={key} size=\"S\">\n        <Text size=\"S\" variant=\"semiBold\" component=\"span\">\n          {key}:\n        </Text>{' '}\n        {description}\n      </Text>\n    ))}\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/components/index.ts",
    "content": "export { DescriptionsTooltip } from './DescriptionsTooltip'\nexport type { DescriptionsTooltipProps } from './DescriptionsTooltip'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/constants.ts",
    "content": "/**\n * Azure subscription state descriptions.\n * @see https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list#subscriptionstate\n */\nexport const AZURE_SUBSCRIPTION_STATE_DESCRIPTIONS: Record<string, string> = {\n  Enabled: 'Subscription is active and fully functional.',\n  Warned:\n    'Subscription has payment issues but is still operational during a grace period.',\n  PastDue: 'Payment is overdue. Services may be limited.',\n  Disabled:\n    'Subscription is suspended. Resources are not accessible until the subscription is re-enabled.',\n  Deleted: 'Subscription has been deleted and cannot be recovered.',\n}\n\n/**\n * Azure database type descriptions.\n * @see https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-overview\n */\nexport const AZURE_DATABASE_TYPE_DESCRIPTIONS: Record<string, string> = {\n  Standard:\n    'Azure Cache for Redis with Basic, Standard, or Premium tiers. Suitable for most caching scenarios.',\n  Enterprise:\n    'Azure Cache for Redis Enterprise with dedicated infrastructure, higher performance, and Redis modules support.',\n}\n\n/**\n * Azure database provisioning state descriptions.\n * @see https://learn.microsoft.com/en-us/rest/api/redis/redis/get#provisioningstate\n */\nexport const AZURE_PROVISIONING_STATE_DESCRIPTIONS: Record<string, string> = {\n  Succeeded: 'Database is fully provisioned and ready to use.',\n  Creating: 'Database is being created and is not yet available.',\n  Updating: 'Database configuration is being updated.',\n  Deleting: 'Database is being deleted.',\n  Failed: 'Provisioning failed. The database is not usable.',\n  Linking: 'Database is being linked for geo-replication.',\n  Unlinking: 'Database is being unlinked from geo-replication.',\n  Recovering: 'Database is recovering from a failure.',\n  Provisioning: 'Database is being provisioned.',\n  Scaling: 'Database is being scaled.',\n  ConfiguringAAD: 'Entra ID (Azure AD) authentication is being configured.',\n  Importing: 'Data is being imported into the database.',\n  Exporting: 'Data is being exported from the database.',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-azure/index.ts",
    "content": "export { AzurePage } from './azure'\nexport { AzureSubscriptionsPage } from './azure-subscriptions'\nexport { AzureDatabasesPage } from './azure-databases'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/alert.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport { AlertCell } from '../components/AlertCell/AlertCell'\nimport { AutoDiscoverCloudIds } from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const alertColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Alert,\n    accessorKey: AutoDiscoverCloudIds.Alert,\n    header: '',\n    enableResizing: false,\n    enableSorting: false,\n    size: 50,\n    cell: ({\n      row: {\n        original: { status, numberOfDatabases },\n      },\n    }) => <AlertCell status={status} numberOfDatabases={numberOfDatabases} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/database.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { DatabaseCell } from '../components/DatabaseCell/DatabaseCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const databaseColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Database,\n    id: AutoDiscoverCloudIds.Name,\n    accessorKey: AutoDiscoverCloudIds.Name,\n    enableSorting: true,\n    maxSize: 150,\n    cell: ({\n      row: {\n        original: { name },\n      },\n    }) => <DatabaseCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/databaseResult.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { DatabaseCell } from '../components/DatabaseCell/DatabaseCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const databaseResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Database,\n    id: AutoDiscoverCloudIds.Name,\n    accessorKey: AutoDiscoverCloudIds.Name,\n    enableSorting: true,\n    maxSize: 120,\n    cell: ({\n      row: {\n        original: { name },\n      },\n    }) => <DatabaseCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpoint.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { EndpointCell } from '../components/EndpointCell/EndpointCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const endpointColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Endpoint,\n    id: AutoDiscoverCloudIds.PublicEndpoint,\n    accessorKey: AutoDiscoverCloudIds.PublicEndpoint,\n    enableSorting: true,\n    minSize: 200,\n    cell: ({\n      row: {\n        original: { publicEndpoint },\n      },\n    }) => <EndpointCell publicEndpoint={publicEndpoint} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/endpointResult.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { EndpointCell } from '../components/EndpointCell/EndpointCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const endpointResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Endpoint,\n    id: AutoDiscoverCloudIds.PublicEndpoint,\n    accessorKey: AutoDiscoverCloudIds.PublicEndpoint,\n    enableSorting: true,\n    minSize: 250,\n    maxSize: 310,\n    cell: ({\n      row: {\n        original: { publicEndpoint },\n      },\n    }) => <EndpointCell publicEndpoint={publicEndpoint} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/id.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const idColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Id,\n    accessorKey: AutoDiscoverCloudIds.Id,\n    header: AutoDiscoverCloudTitles.Id,\n    enableSorting: true,\n    size: 80,\n    cell: ({\n      row: {\n        original: { id },\n      },\n    }) => <CellText data-testid={`id_${id}`}>{id}</CellText>,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/messageResult.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { MessageResultCell } from '../components/MessageResultCell/MessageResultCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const messageResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Result,\n    id: AutoDiscoverCloudIds.MessageAdded,\n    accessorKey: AutoDiscoverCloudIds.MessageAdded,\n    enableSorting: true,\n    minSize: 110,\n    cell: ({\n      row: {\n        original: { statusAdded, messageAdded },\n      },\n    }) => (\n      <MessageResultCell\n        statusAdded={statusAdded}\n        messageAdded={messageAdded}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modules.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { DatabaseListModules } from 'uiSrc/components'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const modulesColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Capabilities,\n    id: AutoDiscoverCloudIds.Modules,\n    accessorKey: AutoDiscoverCloudIds.Modules,\n    enableSorting: true,\n    maxSize: 120,\n    cell: function Modules({ row: { original: instance } }) {\n      return (\n        <DatabaseListModules\n          modules={instance.modules.map((name) => ({ name }))}\n        />\n      )\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/modulesResult.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { DatabaseListModules } from 'uiSrc/components'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const modulesResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Capabilities,\n    id: AutoDiscoverCloudIds.Modules,\n    accessorKey: AutoDiscoverCloudIds.Modules,\n    enableSorting: true,\n    maxSize: 150,\n    cell: function Modules({ row: { original: instance } }) {\n      return (\n        <DatabaseListModules\n          modules={instance.modules?.map((name) => ({ name }))}\n        />\n      )\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/numberOfDbs.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport { isNumber } from 'lodash'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const numberOfDbsColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.NumberOfDatabases,\n    accessorKey: AutoDiscoverCloudIds.NumberOfDatabases,\n    header: AutoDiscoverCloudTitles.NumberOfDatabases,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { numberOfDatabases },\n      },\n    }) => (\n      <CellText>\n        {isNumber(numberOfDatabases) ? numberOfDatabases : '-'}\n      </CellText>\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/options.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { DatabaseListOptions } from 'uiSrc/components'\nimport { parseInstanceOptionsCloud } from 'uiSrc/utils'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const optionsColumn = (\n  instances: InstanceRedisCloud[],\n): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Options,\n    id: AutoDiscoverCloudIds.Options,\n    accessorKey: AutoDiscoverCloudIds.Options,\n    enableSorting: true,\n    maxSize: 120,\n    cell: ({ row: { original: instance } }) => {\n      const options = parseInstanceOptionsCloud(\n        instance.databaseId,\n        instances || [],\n      )\n      return <DatabaseListOptions options={options} />\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/optionsResult.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport { DatabaseListOptions } from 'uiSrc/components'\nimport { parseInstanceOptionsCloud } from 'uiSrc/utils'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const optionsResultColumn = (\n  instancesForOptions: InstanceRedisCloud[],\n): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Options,\n    id: AutoDiscoverCloudIds.Options,\n    accessorKey: AutoDiscoverCloudIds.Options,\n    enableSorting: true,\n    maxSize: 180,\n    cell: function Options({ row: { original: instance } }) {\n      const options = parseInstanceOptionsCloud(\n        instance.databaseId,\n        instancesForOptions,\n      )\n      return <DatabaseListOptions options={options} />\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/provider.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const providerColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Provider,\n    accessorKey: AutoDiscoverCloudIds.Provider,\n    header: AutoDiscoverCloudTitles.Provider,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { provider },\n      },\n    }) => <CellText>{provider ?? '-'}</CellText>,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/region.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const regionColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Region,\n    accessorKey: AutoDiscoverCloudIds.Region,\n    header: AutoDiscoverCloudTitles.Region,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { region },\n      },\n    }) => <CellText>{region ?? '-'}</CellText>,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/selection.ts",
    "content": "import { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\nimport { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils'\n\nexport const selectionColumn = () => {\n  return getSelectionColumn<RedisCloudSubscription>()\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/status.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  type RedisCloudSubscription,\n  RedisCloudSubscriptionStatusText,\n} from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const statusColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Status,\n    accessorKey: AutoDiscoverCloudIds.Status,\n    header: AutoDiscoverCloudTitles.Status,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { status },\n      },\n    }) => (\n      <CellText>{RedisCloudSubscriptionStatusText[status] ?? '-'}</CellText>\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDb.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { StatusColumnText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const statusDbColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Status,\n    id: AutoDiscoverCloudIds.Status,\n    accessorKey: AutoDiscoverCloudIds.Status,\n    enableSorting: true,\n    maxSize: 100,\n    cell: ({\n      row: {\n        original: { status },\n      },\n    }) => <StatusColumnText>{status}</StatusColumnText>,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/statusDbResult.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const statusDbResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Status,\n    id: AutoDiscoverCloudIds.Status,\n    accessorKey: AutoDiscoverCloudIds.Status,\n    enableSorting: true,\n    size: 80,\n    cell: ({\n      row: {\n        original: { status },\n      },\n    }) => <CellText className=\"column_status\">{status}</CellText>,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscription.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport { SubscriptionCell } from '../components/SubscriptionCell/SubscriptionCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Name,\n    accessorKey: AutoDiscoverCloudIds.Name,\n    header: AutoDiscoverCloudTitles.Subscription,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { name },\n      },\n    }) => <SubscriptionCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDb.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { SubscriptionCell } from '../components/SubscriptionCell/SubscriptionCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionDbColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Subscription,\n    id: AutoDiscoverCloudIds.SubscriptionName,\n    accessorKey: AutoDiscoverCloudIds.SubscriptionName,\n    enableSorting: true,\n    minSize: 200,\n    cell: ({\n      row: {\n        original: { subscriptionName: name },\n      },\n    }) => <SubscriptionCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionDbResult.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nimport { SubscriptionCell } from '../components/SubscriptionCell/SubscriptionCell'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionDbResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Subscription,\n    id: AutoDiscoverCloudIds.SubscriptionName,\n    accessorKey: AutoDiscoverCloudIds.SubscriptionName,\n    enableSorting: true,\n    maxSize: 270,\n    cell: ({\n      row: {\n        original: { subscriptionName: name },\n      },\n    }) => <SubscriptionCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionId.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionIdColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.SubscriptionId,\n    id: AutoDiscoverCloudIds.SubscriptionId,\n    accessorKey: AutoDiscoverCloudIds.SubscriptionId,\n    enableSorting: true,\n    maxSize: 120,\n    cell: ({\n      row: {\n        original: { subscriptionId },\n      },\n    }) => (\n      <CellText data-testid={`sub_id_${subscriptionId}`}>\n        {subscriptionId}\n      </CellText>\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionIdResult.tsx",
    "content": "import { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionIdResultColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.SubscriptionId,\n    id: AutoDiscoverCloudIds.SubscriptionId,\n    accessorKey: AutoDiscoverCloudIds.SubscriptionId,\n    enableSorting: true,\n    maxSize: 150,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionType.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  type InstanceRedisCloud,\n  RedisCloudSubscriptionTypeText,\n} from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionTypeColumn = (): ColumnDef<InstanceRedisCloud> => {\n  return {\n    header: AutoDiscoverCloudTitles.Type,\n    id: AutoDiscoverCloudIds.SubscriptionType,\n    accessorKey: AutoDiscoverCloudIds.SubscriptionType,\n    enableSorting: true,\n    maxSize: 100,\n    cell: ({\n      row: {\n        original: { subscriptionType },\n      },\n    }) => (\n      <CellText>\n        {RedisCloudSubscriptionTypeText[subscriptionType!] ?? '-'}\n      </CellText>\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/subscriptionTypeResult.tsx",
    "content": "import { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  type InstanceRedisCloud,\n  RedisCloudSubscriptionTypeText,\n} from 'uiSrc/slices/interfaces'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const subscriptionTypeResultColumn =\n  (): ColumnDef<InstanceRedisCloud> => {\n    return {\n      header: AutoDiscoverCloudTitles.Type,\n      id: AutoDiscoverCloudIds.SubscriptionType,\n      accessorKey: AutoDiscoverCloudIds.SubscriptionType,\n      enableSorting: true,\n      size: 95,\n      cell: ({\n        row: {\n          original: { subscriptionType },\n        },\n      }) => RedisCloudSubscriptionTypeText[subscriptionType!] ?? '-',\n    }\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/columns/type.tsx",
    "content": "import React from 'react'\n\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  type RedisCloudSubscription,\n  RedisCloudSubscriptionTypeText,\n} from 'uiSrc/slices/interfaces'\n\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport {\n  AutoDiscoverCloudIds,\n  AutoDiscoverCloudTitles,\n} from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\nexport const typeColumn = (): ColumnDef<RedisCloudSubscription> => {\n  return {\n    id: AutoDiscoverCloudIds.Type,\n    accessorKey: AutoDiscoverCloudIds.Type,\n    header: AutoDiscoverCloudTitles.Type,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { type },\n      },\n    }) => <CellText>{RedisCloudSubscriptionTypeText[type] ?? '-'}</CellText>,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { RedisCloudSubscriptionStatus } from 'uiSrc/slices/interfaces'\n\nimport { AlertCell } from './AlertCell'\n\ndescribe('AlertCell', () => {\n  it('should render success icon when subscription is active and has databases', () => {\n    render(\n      <AlertCell\n        status={RedisCloudSubscriptionStatus.Active}\n        numberOfDatabases={5}\n      />,\n    )\n\n    const icon = screen.getByRole('img', { hidden: true })\n    expect(icon).toBeInTheDocument()\n  })\n\n  it('should render warning icon when subscription is not active', () => {\n    render(\n      <AlertCell\n        status={RedisCloudSubscriptionStatus.Deleting}\n        numberOfDatabases={5}\n      />,\n    )\n\n    const alertIcon = screen.getByLabelText('subscription alert')\n    expect(alertIcon).toBeInTheDocument()\n  })\n\n  it('should render warning icon when subscription has no databases', () => {\n    render(\n      <AlertCell\n        status={RedisCloudSubscriptionStatus.Active}\n        numberOfDatabases={0}\n      />,\n    )\n\n    const alertIcon = screen.getByLabelText('subscription alert')\n    expect(alertIcon).toBeInTheDocument()\n  })\n\n  it('should render warning icon when subscription is not active and has no databases', () => {\n    render(\n      <AlertCell\n        status={RedisCloudSubscriptionStatus.Error}\n        numberOfDatabases={0}\n      />,\n    )\n\n    const alertIcon = screen.getByLabelText('subscription alert')\n    expect(alertIcon).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { RedisCloudSubscriptionStatus } from 'uiSrc/slices/interfaces'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport { AlertStatusContent } from 'uiSrc/pages/autodiscover-cloud/components/AlertStatusContent'\nimport styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions/styles.module.scss'\n\nimport { AlertCellProps } from './AlertCell.types'\n\nexport const AlertCell = ({ status, numberOfDatabases }: AlertCellProps) => {\n  const isUnavailable =\n    status !== RedisCloudSubscriptionStatus.Active || numberOfDatabases === 0\n\n  if (isUnavailable) {\n    return (\n      <RiTooltip\n        title={\n          <CellText variant=\"semiBold\">\n            This subscription is not available for one of the following reasons:\n          </CellText>\n        }\n        content={<AlertStatusContent />}\n        position=\"right\"\n        className={styles.tooltipStatus}\n      >\n        <RiIcon\n          type=\"ToastDangerIcon\"\n          color=\"danger500\"\n          size=\"m\"\n          aria-label=\"subscription alert\"\n        />\n      </RiTooltip>\n    )\n  }\n\n  return <RiIcon type=\"CheckBoldIcon\" color=\"success500\" size=\"m\" />\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/AlertCell/AlertCell.types.ts",
    "content": "import { RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nexport interface AlertCellProps {\n  status: RedisCloudSubscription['status']\n  numberOfDatabases: RedisCloudSubscription['numberOfDatabases']\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { DatabaseCell } from './DatabaseCell'\n\ndescribe('DatabaseCell', () => {\n  it('should render database name', () => {\n    const name = 'test-database'\n    render(<DatabaseCell name={name} />)\n\n    expect(screen.getByText(name)).toBeInTheDocument()\n  })\n\n  it('should render with testid', () => {\n    const name = 'my-db'\n    render(<DatabaseCell name={name} />)\n\n    expect(screen.getByTestId(`db_name_${name}`)).toBeInTheDocument()\n  })\n\n  it('should truncate long names to 200 characters', () => {\n    const longName = 'a'.repeat(300)\n    render(<DatabaseCell name={longName} />)\n\n    const displayedText = screen.getByText('a'.repeat(200))\n    expect(displayedText).toBeInTheDocument()\n  })\n\n  it('should replace spaces in names', () => {\n    const nameWithSpaces = 'my database name'\n    render(<DatabaseCell name={nameWithSpaces} />)\n\n    // replaceSpaces replaces spaces with nbsp\n    const element = screen.getByTestId(`db_name_${nameWithSpaces}`)\n    expect(element).toBeInTheDocument()\n  })\n\n  it('should apply custom className', () => {\n    const name = 'test-db'\n    const customClass = 'custom-class'\n    const { container } = render(\n      <DatabaseCell name={name} className={customClass} />,\n    )\n\n    const element = container.querySelector(`.${customClass}`)\n    expect(element).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.tsx",
    "content": "import React from 'react'\n\nimport { formatLongName, replaceSpaces } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-databases/styles.module.scss'\n\nimport { DatabaseCellProps } from './DatabaseCell.types'\n\nexport const DatabaseCell = ({ name, className }: DatabaseCellProps) => {\n  const cellContent = replaceSpaces(name.substring(0, 200))\n\n  return (\n    <div\n      role=\"presentation\"\n      data-testid={`db_name_${name}`}\n      className={className}\n    >\n      <RiTooltip\n        position=\"bottom\"\n        title=\"Database\"\n        className={styles.tooltipColumnName}\n        anchorClassName=\"truncateText\"\n        content={formatLongName(name)}\n      >\n        <CellText>{cellContent}</CellText>\n      </RiTooltip>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/DatabaseCell/DatabaseCell.types.ts",
    "content": "export interface DatabaseCellProps {\n  name: string\n  className?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { EndpointCell } from './EndpointCell'\n\ndescribe('EndpointCell', () => {\n  it('should render endpoint text', () => {\n    const endpoint = 'redis-12345.c1.us-east-1.ec2.cloud.redislabs.com:12345'\n    render(<EndpointCell publicEndpoint={endpoint} />)\n\n    expect(screen.getByText(endpoint)).toBeInTheDocument()\n  })\n\n  it('should render copy button', () => {\n    const endpoint = 'redis-12345.c1.us-east-1.ec2.cloud.redislabs.com:12345'\n    render(<EndpointCell publicEndpoint={endpoint} />)\n\n    const copyButton = screen.getByLabelText('Copy public endpoint')\n    expect(copyButton).toBeInTheDocument()\n  })\n\n  it('should render both endpoint text and copy button together', () => {\n    const endpoint = 'test-endpoint:6379'\n    render(<EndpointCell publicEndpoint={endpoint} />)\n\n    expect(screen.getByText(endpoint)).toBeInTheDocument()\n    expect(screen.getByLabelText('Copy public endpoint')).toBeInTheDocument()\n  })\n\n  it('should render \"-\" when publicEndpoint is undefined', () => {\n    render(<EndpointCell publicEndpoint={undefined} />)\n\n    expect(screen.getByText('-')).toBeInTheDocument()\n    expect(\n      screen.queryByLabelText('Copy public endpoint'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render \"-\" when publicEndpoint is empty string', () => {\n    render(<EndpointCell publicEndpoint=\"\" />)\n\n    expect(screen.getByText('-')).toBeInTheDocument()\n    expect(\n      screen.queryByLabelText('Copy public endpoint'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.tsx",
    "content": "import React from 'react'\n\nimport {\n  CellText,\n  CopyPublicEndpointText,\n  CopyTextContainer,\n  CopyBtnWrapper,\n} from 'uiSrc/components/auto-discover'\nimport { RiTooltip } from 'uiSrc/components'\nimport { formatLongName } from 'uiSrc/utils'\n\nimport { EndpointCellProps } from './EndpointCell.types'\n\nexport const EndpointCell = ({ publicEndpoint }: EndpointCellProps) => {\n  if (!publicEndpoint) {\n    return <CellText>-</CellText>\n  }\n\n  return (\n    <CopyTextContainer>\n      <RiTooltip\n        position=\"bottom\"\n        title=\"Endpoint\"\n        content={formatLongName(publicEndpoint)}\n      >\n        <CopyPublicEndpointText>{publicEndpoint}</CopyPublicEndpointText>\n      </RiTooltip>\n\n      <CopyBtnWrapper\n        copy={publicEndpoint}\n        aria-label=\"Copy public endpoint\"\n        successLabel=\"\"\n      />\n    </CopyTextContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/EndpointCell/EndpointCell.types.ts",
    "content": "export interface EndpointCellProps {\n  publicEndpoint?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nimport { MessageResultCell } from './MessageResultCell'\n\ndescribe('MessageResultCell', () => {\n  it('should render success message when status is success', () => {\n    const message = 'Database added successfully'\n    render(\n      <MessageResultCell\n        statusAdded={AddRedisDatabaseStatus.Success}\n        messageAdded={message}\n      />,\n    )\n\n    expect(screen.getByText(message)).toBeInTheDocument()\n  })\n\n  it('should render error icon and text when status is not success', () => {\n    const message = 'Failed to add database'\n    render(\n      <MessageResultCell\n        statusAdded={AddRedisDatabaseStatus.Fail}\n        messageAdded={message}\n      />,\n    )\n\n    expect(screen.getByText('Error')).toBeInTheDocument()\n  })\n\n  it('should not render success message when status is fail', () => {\n    const message = 'Failed to add database'\n    render(\n      <MessageResultCell\n        statusAdded={AddRedisDatabaseStatus.Fail}\n        messageAdded={message}\n      />,\n    )\n\n    expect(screen.queryByText(message)).not.toBeInTheDocument()\n  })\n\n  it('should render dash when statusAdded is undefined', () => {\n    render(\n      <MessageResultCell statusAdded={undefined} messageAdded=\"Some message\" />,\n    )\n\n    expect(screen.getByText('-')).toBeInTheDocument()\n  })\n\n  it('should handle missing messageAdded gracefully', () => {\n    const { container } = render(\n      <MessageResultCell\n        statusAdded={AddRedisDatabaseStatus.Success}\n        messageAdded={undefined}\n      />,\n    )\n\n    const cellText = container.querySelector('.RI-text')\n    expect(cellText).toBeInTheDocument()\n    expect(cellText?.textContent).toBe('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.tsx",
    "content": "import React from 'react'\n\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nimport { MessageResultCellProps } from './MessageResultCell.types'\n\nexport const MessageResultCell = ({\n  statusAdded,\n  messageAdded = '',\n}: MessageResultCellProps) => {\n  if (!statusAdded) {\n    return <CellText>-</CellText>\n  }\n\n  if (statusAdded === AddRedisDatabaseStatus.Success) {\n    return <CellText>{messageAdded}</CellText>\n  }\n\n  return (\n    <RiTooltip\n      position=\"left\"\n      title=\"Error\"\n      content={messageAdded}\n      anchorClassName=\"truncateText\"\n    >\n      <Row align=\"center\" gap=\"s\">\n        <FlexItem>\n          <RiIcon type=\"ToastDangerIcon\" color=\"danger600\" />\n        </FlexItem>\n\n        <FlexItem>\n          <ColorText color=\"danger\" className=\"flex-row\" size=\"S\">\n            Error\n          </ColorText>\n        </FlexItem>\n      </Row>\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/MessageResultCell/MessageResultCell.types.ts",
    "content": "import { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nexport interface MessageResultCellProps {\n  statusAdded?: AddRedisDatabaseStatus\n  messageAdded?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { SubscriptionCell } from './SubscriptionCell'\n\ndescribe('SubscriptionCell', () => {\n  it('should render subscription name', () => {\n    const name = 'test-subscription'\n    render(<SubscriptionCell name={name} />)\n\n    expect(screen.getByText(name)).toBeInTheDocument()\n  })\n\n  it('should truncate long names to 200 characters', () => {\n    const longName = 'a'.repeat(300)\n    render(<SubscriptionCell name={longName} />)\n\n    const displayedText = screen.getByText('a'.repeat(200))\n    expect(displayedText).toBeInTheDocument()\n  })\n\n  it('should replace spaces in names', () => {\n    const nameWithSpaces = 'my subscription name'\n    render(<SubscriptionCell name={nameWithSpaces} />)\n\n    // replaceSpaces replaces spaces with nbsp\n    const element = screen.getByRole('presentation')\n    expect(element).toBeInTheDocument()\n  })\n\n  it('should apply custom className', () => {\n    const name = 'test-sub'\n    const customClass = 'custom-class'\n    const { container } = render(\n      <SubscriptionCell name={name} className={customClass} />,\n    )\n\n    const element = container.querySelector(`.${customClass}`)\n    expect(element).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.tsx",
    "content": "import React from 'react'\n\nimport { formatLongName, replaceSpaces } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport styles from 'uiSrc/pages/autodiscover-cloud/redis-cloud-databases/styles.module.scss'\n\nimport { SubscriptionCellProps } from './SubscriptionCell.types'\n\nexport const SubscriptionCell = ({\n  name,\n  className,\n}: SubscriptionCellProps) => {\n  const cellContent = replaceSpaces(name.substring(0, 200))\n\n  return (\n    <div role=\"presentation\" className={className}>\n      <RiTooltip\n        position=\"bottom\"\n        title=\"Subscription\"\n        className={styles.tooltipColumnName}\n        anchorClassName=\"truncateText\"\n        content={formatLongName(name)}\n      >\n        <CellText>{cellContent}</CellText>\n      </RiTooltip>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/SubscriptionCell/SubscriptionCell.types.ts",
    "content": "export interface SubscriptionCellProps {\n  name: string\n  className?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/components/index.ts",
    "content": "export * from './AlertCell/AlertCell'\nexport * from './DatabaseCell/DatabaseCell'\nexport * from './EndpointCell/EndpointCell'\nexport * from './MessageResultCell/MessageResultCell'\nexport * from './SubscriptionCell/SubscriptionCell'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/column-definitions/index.ts",
    "content": "export * from './columns/alert'\nexport * from './columns/database'\nexport * from './columns/databaseResult'\nexport * from './columns/endpoint'\nexport * from './columns/endpointResult'\nexport * from './columns/id'\nexport * from './columns/messageResult'\nexport * from './columns/modules'\nexport * from './columns/modulesResult'\nexport * from './columns/numberOfDbs'\nexport * from './columns/options'\nexport * from './columns/optionsResult'\nexport * from './columns/provider'\nexport * from './columns/region'\nexport * from './columns/selection'\nexport * from './columns/status'\nexport * from './columns/statusDb'\nexport * from './columns/statusDbResult'\nexport * from './columns/subscription'\nexport * from './columns/subscriptionDb'\nexport * from './columns/subscriptionDbResult'\nexport * from './columns/subscriptionId'\nexport * from './columns/subscriptionIdResult'\nexport * from './columns/subscriptionType'\nexport * from './columns/subscriptionTypeResult'\nexport * from './columns/type'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/components/AlertStatusContent.tsx",
    "content": "import React from 'react'\nimport {\n  AlertStatusDot,\n  AlertStatusList,\n  AlertStatusListItem,\n} from 'uiSrc/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles'\n\nexport const AlertStatusContent = () => (\n  <AlertStatusList gap=\"none\" flush>\n    <AlertStatusListItem\n      size=\"s\"\n      label=\"Subscription status is not Active\"\n      icon={<AlertStatusDot />}\n    />\n    <AlertStatusListItem\n      size=\"s\"\n      wrapText\n      label=\"Subscription does not have any databases\"\n      icon={<AlertStatusDot />}\n    />\n    <AlertStatusListItem\n      size=\"s\"\n      label=\"Error fetching subscription details\"\n      icon={<AlertStatusDot />}\n    />\n  </AlertStatusList>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/constants/constants.ts",
    "content": "export enum AutoDiscoverCloudIds {\n  Alert = 'alert',\n  Name = 'name',\n  PublicEndpoint = 'publicEndpoint',\n  Id = 'id',\n  MessageAdded = 'messageAdded',\n  Modules = 'modules',\n  NumberOfDatabases = 'numberOfDatabases',\n  Options = 'options',\n  Provider = 'provider',\n  Region = 'region',\n  Status = 'status',\n  SubscriptionName = 'subscriptionName',\n  SubscriptionId = 'subscriptionId',\n  SubscriptionType = 'subscriptionType',\n  Type = 'type',\n}\n\nexport enum AutoDiscoverCloudTitles {\n  Id = 'Id',\n  Database = 'Database',\n  Endpoint = 'Endpoint',\n  Result = 'Result',\n  Capabilities = 'Capabilities',\n  NumberOfDatabases = '# databases',\n  Options = 'Options',\n  Provider = 'Cloud provider',\n  Region = 'Region',\n  Status = 'Status',\n  Subscription = 'Subscription',\n  SubscriptionId = 'Subscription id',\n  Type = 'Type',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/index.ts",
    "content": "export * from './redis-cloud'\nexport * from './redis-cloud-subscriptions'\nexport * from './redis-cloud-databases'\nexport * from './redis-cloud-databases-result'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud/RedisCloudPage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RedisCloudPage, { Props } from './RedisCloudPage'\n\nconst mockedProps = mock<Props>()\n\nconst mockedRoutes = [\n  {\n    path: '/redis-enterprise-autodiscovery',\n  },\n]\n\n/**\n * RedisCloudPage tests\n *\n * @group component\n */\ndescribe('RedisCloudPage', () => {\n  it('should render', () => {\n    expect(\n      render(<RedisCloudPage {...instance(mockedProps)} />, {\n        withRouter: true,\n      }),\n    ).toBeTruthy()\n\n    expect(\n      render(<RedisCloudPage routes={mockedRoutes} />, { withRouter: true }),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud/RedisCloudPage.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\n\nexport interface Props {\n  routes: any[]\n}\nconst RedisCloudPage = ({ routes = [] }: Props) => (\n  <Switch>\n    {routes.map((route, i) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <RouteWithSubRoutes key={i} {...route} />\n    ))}\n  </Switch>\n)\n\nexport default RedisCloudPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud/index.ts",
    "content": "import RedisCloudPage from './RedisCloudPage'\n\nexport { RedisCloudPage }\n\nexport default RedisCloudPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RedisCloudDatabases from './RedisCloudDatabases'\n\ndescribe('RedisCloudDatabases', () => {\n  it('should render', () => {\n    const columnsMock = [\n      {\n        header: 'Subscription ID',\n        id: 'subscriptionId',\n        accessorKey: 'subscriptionId',\n        enableSorting: true,\n      },\n    ]\n    expect(\n      render(\n        <RedisCloudDatabases\n          selection={[]}\n          columns={columnsMock}\n          instances={[]}\n          loading={false}\n          onSelectionChange={jest.fn()}\n          onClose={jest.fn()}\n          onBack={jest.fn()}\n          onSubmit={jest.fn()}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport RedisCloudDatabases from './'\nimport { colFactory } from '../utils/colFactory'\n\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\nimport { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport { RedisCloudInstanceFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudInstance.factory'\n\nconst emptyColumns = colFactory([])\n\nconst meta: Meta<typeof RedisCloudDatabases> = {\n  component: RedisCloudDatabases,\n  args: {\n    columns: emptyColumns,\n    instances: [],\n    selection: [],\n    loading: true,\n    onSubmit: () => {},\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Empty: Story = {}\n\nconst RenderStory = () => {\n  const instancesMock: InstanceRedisCloud[] =\n    RedisCloudInstanceFactory.buildList(6)\n  const columns = colFactory(instancesMock)\n  const [selection, setSelection] = useState<InstanceRedisCloud[]>([])\n\n  const handleSelectionChange = (currentSelected: RowSelectionState) => {\n    const newSelection = instancesMock.filter((item) => {\n      const { id } = item\n      if (!id) {\n        return false\n      }\n      return currentSelected[id]\n    })\n    setSelection(newSelection)\n  }\n\n  return (\n    <RedisCloudDatabases\n      loading={false}\n      instances={instancesMock}\n      onClose={fn()}\n      onBack={fn()}\n      onSubmit={fn()}\n      selection={selection}\n      columns={columns}\n      onSelectionChange={handleSelectionChange}\n    />\n  )\n}\n\nexport const WithDatabases: Story = {\n  render: () => <RenderStory />,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/RedisCloudDatabases.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { map, pick } from 'lodash'\n\nimport { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport {\n  DestructiveButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  ColumnDef,\n  RowSelectionState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\nimport styles from '../styles.module.scss'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\n\nexport interface Props {\n  columns: ColumnDef<InstanceRedisCloud>[]\n  instances: InstanceRedisCloud[]\n  selection: InstanceRedisCloud[]\n  loading: boolean\n  onSelectionChange: (currentSelected: RowSelectionState) => void\n  onClose: () => void\n  onBack: () => void\n  onSubmit: (\n    databases: Pick<\n      InstanceRedisCloud,\n      'subscriptionId' | 'subscriptionType' | 'databaseId' | 'free'\n    >[],\n  ) => void\n}\n\ninterface IPopoverProps {\n  isPopoverOpen: boolean\n}\n\nconst loadingMsg = 'loading...'\nconst notFoundMsg = 'Not found'\nconst noResultsMessage =\n  'Your Redis Enterprise Cloud has no databases available'\n\nconst RedisCloudDatabasesPage = ({\n  columns,\n  selection,\n  instances,\n  loading,\n  onSelectionChange,\n  onClose,\n  onBack,\n  onSubmit,\n}: Props) => {\n  const [items, setItems] = useState<InstanceRedisCloud[]>([])\n  const [message, setMessage] = useState(loadingMsg)\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  useEffect(() => {\n    if (instances !== null) {\n      setItems(instances)\n    }\n\n    if (instances?.length === 0 && !loading) {\n      setMessage(noResultsMessage)\n    }\n  }, [instances, loading])\n\n  const handleSubmit = () => {\n    onSubmit(\n      map(selection, (i) =>\n        pick(i, 'subscriptionId', 'subscriptionType', 'databaseId', 'free'),\n      ),\n    )\n  }\n\n  const showPopover = () => {\n    setIsPopoverOpen(true)\n  }\n\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n\n    const itemsTemp =\n      instances?.filter(\n        (item: InstanceRedisCloud) =>\n          item.name?.toLowerCase().indexOf(value) !== -1 ||\n          item.publicEndpoint?.toLowerCase().indexOf(value) !== -1 ||\n          item.subscriptionId?.toString()?.indexOf(value) !== -1 ||\n          item.subscriptionName?.toLowerCase().indexOf(value) !== -1 ||\n          item.databaseId?.toString()?.indexOf(value) !== -1,\n      ) || []\n\n    if (!itemsTemp.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  const CancelButton = ({ isPopoverOpen: popoverIsOpen }: IPopoverProps) => (\n    <RiPopover\n      anchorPosition=\"upCenter\"\n      isOpen={popoverIsOpen}\n      closePopover={closePopover}\n      panelClassName={styles.panelCancelBtn}\n      panelPaddingSize=\"l\"\n      button={\n        <SecondaryButton\n          onClick={showPopover}\n          className=\"btn-cancel\"\n          data-testid=\"btn-cancel\"\n        >\n          Cancel\n        </SecondaryButton>\n      }\n    >\n      <Text size=\"m\">\n        Your changes have not been saved.&#10;&#13; Do you want to proceed to\n        the list of databases?\n      </Text>\n      <br />\n      <div>\n        <DestructiveButton\n          size=\"s\"\n          onClick={onClose}\n          data-testid=\"btn-cancel-proceed\"\n        >\n          Proceed\n        </DestructiveButton>\n      </div>\n    </RiPopover>\n  )\n\n  const SubmitButton = ({ isDisabled }: { isDisabled: boolean }) => (\n    <RiTooltip\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      title={\n        isDisabled ? validationErrors.SELECT_AT_LEAST_ONE('database') : null\n      }\n      content={\n        isDisabled ? <span>{validationErrors.NO_DBS_SELECTED}</span> : null\n      }\n    >\n      <PrimaryButton\n        size=\"m\"\n        disabled={isDisabled}\n        onClick={handleSubmit}\n        loading={loading}\n        icon={isDisabled ? InfoIcon : undefined}\n        data-testid=\"btn-add-databases\"\n      >\n        Add selected Databases\n      </PrimaryButton>\n    </RiTooltip>\n  )\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title=\"Redis Cloud Databases\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n          subTitle={`\n            These are ${items.length > 1 ? 'databases ' : 'database '}\n            in your Redis Cloud. Select the\n            ${items.length > 1 ? ' databases ' : ' database '} that you want to\n            add.`}\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            rowSelectionMode=\"multiple\"\n            onRowSelectionChange={onSelectionChange}\n            getRowId={(row) => `${row.databaseId}`}\n            columns={columns}\n            data={items}\n            defaultSorting={[\n              {\n                id: 'name',\n                desc: false,\n              },\n            ]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            pageSizes={[5, 10, 25, 50, 100]}\n            emptyState={() => (\n              <Col centered full>\n                <FlexItem padding={13}>\n                  <Text size=\"L\">{message}</Text>\n                </FlexItem>\n              </Col>\n            )}\n          />\n          {!items.length && (\n            <Col centered full>\n              <Text size=\"L\">{message}</Text>\n            </Col>\n          )}\n        </DatabaseWrapper>\n      </DatabaseContainer>\n      <Footer>\n        <Row justify=\"end\">\n          <Row gap=\"m\" grow={false}>\n            <CancelButton isPopoverOpen={isPopoverOpen} />\n            <SubmitButton isDisabled={selection.length < 1} />\n          </Row>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default RedisCloudDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabases/index.ts",
    "content": "import RedisCloudDatabases from './RedisCloudDatabases'\n\nexport default RedisCloudDatabases\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nimport RedisCloudDatabasesPage from './RedisCloudDatabasesPage'\nimport RedisCloudDatabases from './RedisCloudDatabases'\nimport { Props as RedisCloudDatabasesProps } from './RedisCloudDatabases/RedisCloudDatabases'\n\njest.mock('./RedisCloudDatabases', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockRedisCloudDatabases = (props: RedisCloudDatabasesProps) => (\n  <div>\n    <button\n      type=\"button\"\n      onClick={() => props.onClose()}\n      data-testid=\"close-btn\"\n    >\n      onClose\n    </button>\n    <button type=\"button\" onClick={() => props.onBack()} data-testid=\"back-btn\">\n      onBack\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.onSubmit([])}\n      data-testid=\"submit-btn\"\n    >\n      onSubmit\n    </button>\n    <div className=\"itemList\">\n      <Table\n        columns={props.columns}\n        data={[]}\n        defaultSorting={[{ id: 'name', desc: false }]}\n      />\n    </div>\n  </div>\n)\n\n/**\n * RedisCloudDatabasesPage tests\n *\n * @group component\n */\ndescribe('RedisCloudDatabasesPage', () => {\n  beforeAll(() => {\n    RedisCloudDatabases.mockImplementation(mockRedisCloudDatabases)\n  })\n\n  it('should render', () => {\n    expect(render(<RedisCloudDatabasesPage />)).toBeTruthy()\n  })\n\n  it('should call onClose', () => {\n    const component = render(<RedisCloudDatabasesPage />)\n    fireEvent.click(screen.getByTestId('close-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onBack', () => {\n    const component = render(<RedisCloudDatabasesPage />)\n    fireEvent.click(screen.getByTestId('back-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onSubmit', () => {\n    const component = render(<RedisCloudDatabasesPage />)\n    fireEvent.click(screen.getByTestId('submit-btn'))\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx",
    "content": "import React from 'react'\nimport { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport RedisCloudDatabases from './RedisCloudDatabases'\nimport { useCloudDatabasesConfig } from './hooks/useCloudDatabasesConfig'\n\nconst EMPTY_INSTANCES: InstanceRedisCloud[] = []\n\nconst RedisCloudDatabasesPage = () => {\n  const {\n    columns,\n    selection,\n    handleClose,\n    handleBackAdding,\n    handleAddInstances,\n    handleSelectionChange,\n    instances,\n  } = useCloudDatabasesConfig()\n\n  return (\n    <RedisCloudDatabases\n      selection={selection}\n      onClose={handleClose}\n      onBack={handleBackAdding}\n      onSubmit={handleAddInstances}\n      columns={columns}\n      instances={instances || EMPTY_INSTANCES}\n      loading={false}\n      onSelectionChange={handleSelectionChange}\n    />\n  )\n}\n\nexport default RedisCloudDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.test.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport {\n  mockStore,\n  initialStateDefault,\n  renderHook,\n  act,\n} from 'uiSrc/utils/test-utils'\nimport { LoadedCloud, OAuthSocialAction } from 'uiSrc/slices/interfaces'\nimport {\n  resetDataRedisCloud,\n  resetLoadedRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\n\nimport { useCloudDatabasesConfig } from './useCloudDatabasesConfig'\n\ndescribe('useCloudDatabasesConfig', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should return correct initial state with columns', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.data = [\n      { databaseId: 1, name: 'db1', subscriptionId: 1, free: false } as any,\n      { databaseId: 2, name: 'db2', subscriptionId: 2, free: false } as any,\n    ]\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n    expect(result.current.columns).toHaveLength(9)\n    expect(result.current.columns[0].id).toBe('row-selection')\n    expect(result.current.columns[1].id).toBe('name')\n    expect(result.current.selection).toEqual([])\n    expect(result.current.instances).toHaveLength(2)\n    expect(result.current.loading).toBe(false)\n    expect(typeof result.current.handleClose).toBe('function')\n    expect(typeof result.current.handleBackAdding).toBe('function')\n    expect(typeof result.current.handleAddInstances).toBe('function')\n    expect(typeof result.current.handleSelectionChange).toBe('function')\n  })\n\n  it('should return columns without selection when instances array is empty', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.data = []\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n    expect(result.current.columns).toHaveLength(8)\n    expect(result.current.columns[0].id).toBe('name')\n  })\n\n  describe('handleSelectionChange', () => {\n    it('should update selection based on current selected state', () => {\n      const state = cloneDeep(initialStateDefault)\n      state.connections.cloud.data = [\n        { databaseId: 1, name: 'db1', subscriptionId: 1, free: false } as any,\n        { databaseId: 2, name: 'db2', subscriptionId: 2, free: false } as any,\n      ]\n      const store = mockStore(state)\n\n      const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n      act(() => {\n        result.current.handleSelectionChange({ 1: true, 2: false })\n      })\n\n      expect(result.current.selection).toEqual([\n        { databaseId: 1, name: 'db1', subscriptionId: 1, free: false },\n      ])\n    })\n\n    it('should filter out items without databaseId', () => {\n      const state = cloneDeep(initialStateDefault)\n      state.connections.cloud.data = [\n        { databaseId: 1, name: 'db1' } as any,\n        { databaseId: null, name: 'db2' } as any,\n      ]\n      const store = mockStore(state)\n\n      const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n      act(() => {\n        result.current.handleSelectionChange({ 1: true })\n      })\n\n      expect(result.current.selection).toEqual([{ databaseId: 1, name: 'db1' }])\n    })\n  })\n\n  describe('handleClose', () => {\n    it('should dispatch reset data action', () => {\n      const state = cloneDeep(initialStateDefault)\n      state.connections.cloud.data = []\n      const store = mockStore(state)\n\n      const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n      act(() => {\n        result.current.handleClose()\n      })\n\n      const actions = store.getActions()\n      expect(actions).toContainEqual(resetDataRedisCloud())\n    })\n  })\n\n  describe('handleBackAdding', () => {\n    it('should dispatch reset loaded state action', () => {\n      const state = cloneDeep(initialStateDefault)\n      state.connections.cloud.data = []\n      const store = mockStore(state)\n\n      const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n      act(() => {\n        result.current.handleBackAdding()\n      })\n\n      const actions = store.getActions()\n      expect(actions).toContainEqual(\n        resetLoadedRedisCloud(LoadedCloud.Instances),\n      )\n    })\n  })\n\n  describe('handleAddInstances', () => {\n    it('should dispatch create instances action', () => {\n      const state = cloneDeep(initialStateDefault)\n      state.connections.cloud.data = []\n      state.connections.cloud.credentials = {\n        accessKey: 'test-key',\n        secretKey: 'test-secret',\n      }\n      const store = mockStore(state)\n\n      const { result } = renderHook(() => useCloudDatabasesConfig(), { store })\n\n      const databases = [\n        { databaseId: 1, subscriptionId: 1, free: false },\n        { databaseId: 2, subscriptionId: 2, free: false },\n      ]\n\n      act(() => {\n        result.current.handleAddInstances(databases)\n      })\n\n      const actions = store.getActions()\n      const createAction = actions.find(\n        (action) => action.type === 'cloud/createInstancesRedisCloud',\n      )\n      expect(createAction).toBeDefined()\n    })\n  })\n\n  describe('SSO Flow handling', () => {\n    it('should dispatch reset data when userOAuthProfile is null in Import flow', () => {\n      const state = cloneDeep(initialStateDefault)\n      state.connections.cloud.data = []\n      state.connections.cloud.ssoFlow = OAuthSocialAction.Import\n      state.oauth.cloud.user.data = null\n      const store = mockStore(state)\n\n      renderHook(() => useCloudDatabasesConfig(), { store })\n\n      const actions = store.getActions()\n      expect(actions).toContainEqual(resetDataRedisCloud())\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.ts",
    "content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  addInstancesRedisCloud,\n  cloudSelector,\n  fetchSubscriptionsRedisCloud,\n  resetDataRedisCloud,\n  resetLoadedRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud'\nimport { setTitle } from 'uiSrc/utils'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  InstanceRedisCloud,\n  LoadedCloud,\n  OAuthSocialAction,\n} from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\n\nimport { UseCloudDatabasesConfigReturn } from './useCloudDatabasesConfig.types'\nimport { colFactory } from '../utils/colFactory'\n\nexport const useCloudDatabasesConfig = (): UseCloudDatabasesConfigReturn => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const {\n    ssoFlow,\n    credentials,\n    loading,\n    data: instances,\n    dataAdded: instancesAdded,\n  } = useSelector(cloudSelector)\n  const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector)\n\n  const currentAccountIdRef = useRef(userOAuthProfile?.id)\n  const ssoFlowRef = useRef(ssoFlow)\n\n  const [selection, setSelection] = useState<InstanceRedisCloud[]>([])\n\n  const handleSelectionChange = useCallback(\n    (currentSelected: RowSelectionState) => {\n      const newSelection = instances?.filter((item) => {\n        const { databaseId } = item\n        if (!databaseId) {\n          return false\n        }\n        return currentSelected[databaseId]\n      })\n      setSelection(newSelection || [])\n    },\n    [instances],\n  )\n\n  useEffect(() => {\n    if (instances === null) {\n      history.push(Pages.home)\n    }\n    setTitle('Redis Cloud Databases')\n\n    dispatch(resetLoadedRedisCloud(LoadedCloud.Instances))\n  }, [instances])\n\n  useEffect(() => {\n    if (ssoFlowRef.current !== OAuthSocialAction.Import) return\n\n    if (!userOAuthProfile) {\n      dispatch(resetDataRedisCloud())\n      history.push(Pages.home)\n      return\n    }\n\n    if (currentAccountIdRef.current !== userOAuthProfile?.id) {\n      dispatch(\n        fetchSubscriptionsRedisCloud(null, true, () => {\n          history.push(Pages.redisCloudSubscriptions)\n        }),\n      )\n    }\n  }, [userOAuthProfile])\n\n  useEffect(() => {\n    if (instancesAdded.length) {\n      history.push(Pages.redisCloudDatabasesResult)\n    }\n  }, [instancesAdded])\n\n  const sendCancelEvent = useCallback(() => {\n    sendEventTelemetry({\n      event:\n        TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED,\n    })\n  }, [])\n\n  const handleClose = useCallback(() => {\n    sendCancelEvent()\n    dispatch(resetDataRedisCloud())\n    history.push(Pages.home)\n  }, [dispatch, history, sendCancelEvent])\n\n  const handleBackAdding = useCallback(() => {\n    sendCancelEvent()\n    dispatch(resetLoadedRedisCloud(LoadedCloud.Instances))\n    history.push(Pages.home)\n  }, [dispatch, history, sendCancelEvent])\n\n  const handleAddInstances = useCallback(\n    (\n      databases: Pick<\n        InstanceRedisCloud,\n        'subscriptionId' | 'databaseId' | 'free'\n      >[],\n    ) => {\n      dispatch(\n        addInstancesRedisCloud(\n          { databases, credentials },\n          ssoFlow === OAuthSocialAction.Import,\n        ),\n      )\n    },\n    [dispatch, credentials, ssoFlow],\n  )\n\n  const columns = useMemo(() => colFactory(instances || []), [instances])\n\n  return {\n    columns,\n    selection,\n    instances,\n    loading,\n    handleClose,\n    handleBackAdding,\n    handleAddInstances,\n    handleSelectionChange,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/hooks/useCloudDatabasesConfig.types.ts",
    "content": "import { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport {\n  ColumnDef,\n  RowSelectionState,\n} from 'uiSrc/components/base/layout/table'\n\nexport interface UseCloudDatabasesConfigReturn {\n  columns: ColumnDef<InstanceRedisCloud>[]\n  selection: InstanceRedisCloud[]\n  instances: InstanceRedisCloud[] | null\n  loading: boolean\n  handleClose: () => void\n  handleBackAdding: () => void\n  handleAddInstances: (\n    databases: Pick<\n      InstanceRedisCloud,\n      'subscriptionId' | 'databaseId' | 'free'\n    >[],\n  ) => void\n  handleSelectionChange: (currentSelected: RowSelectionState) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/index.ts",
    "content": "import RedisCloudDatabasesPage from './RedisCloudDatabasesPage'\n\nexport { RedisCloudDatabasesPage }\n\nexport default RedisCloudDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/styles.module.scss",
    "content": ".footer {\n  padding: 15px 14px 10px 30px !important;\n}\n\n.title {\n  padding-bottom: 5px !important;\n}\n\n.subTitle {\n  padding-bottom: 10px;\n}\n\n.searchForm {\n  width: 266px !important;\n}\n\n.panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n\n.tooltipColumnName {\n  max-width: 370px !important;\n  * {\n    line-height: 1.19;\n    font-size: 14px !important;\n\n    &:not(:global(.euiToolTip__title)) {\n      font-weight: 300 !important;\n    }\n  }\n}\n\n.errorBtn {\n  height: 100% !important;\n  > span {\n    padding: 0 !important;\n  }\n}\n\n.tooltipColumnNameText {\n  text-decoration: underline;\n  display: inline-block;\n  width: 100%;\n  overflow: hidden;\n  vertical-align: top;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.spec.ts",
    "content": "import { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport { colFactory } from './colFactory'\n\ndescribe('colFactory', () => {\n  it('should return columns without selection column when instances array is empty', () => {\n    const instances: InstanceRedisCloud[] = []\n\n    const columns = colFactory(instances)\n\n    expect(columns).toHaveLength(8)\n    expect(columns[0].id).toBe('name')\n  })\n\n  it('should return columns with selection column when instances array has items', () => {\n    const instances: InstanceRedisCloud[] = [\n      {\n        databaseId: 1,\n        name: 'test-db',\n        subscriptionId: 1,\n        subscriptionType: 1,\n        free: false,\n      } as any,\n    ]\n\n    const columns = colFactory(instances)\n\n    expect(columns).toHaveLength(9)\n    expect(columns[0].id).toBe('row-selection')\n    expect(columns[1].id).toBe('name')\n  })\n\n  it('should include all required column definitions in correct order', () => {\n    const instances: InstanceRedisCloud[] = [\n      { databaseId: 1, name: 'test-db' } as any,\n    ]\n\n    const columns = colFactory(instances)\n\n    const columnIds = columns.map((col) => col.id)\n\n    expect(columnIds).toEqual([\n      'row-selection',\n      'name',\n      'subscriptionId',\n      'subscriptionName',\n      'subscriptionType',\n      'status',\n      'publicEndpoint',\n      'modules',\n      'options',\n    ])\n  })\n\n  it('should pass instances to optionsColumn', () => {\n    const instances: InstanceRedisCloud[] = [\n      { databaseId: 1, name: 'test-db' } as any,\n      { databaseId: 2, name: 'test-db-2' } as any,\n    ]\n\n    const columns = colFactory(instances)\n\n    // The options column should be the last one\n    const optionsColumn = columns[columns.length - 1]\n    expect(optionsColumn.id).toBe('options')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/utils/colFactory.ts",
    "content": "import { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils'\nimport {\n  databaseColumn,\n  endpointColumn,\n  modulesColumn,\n  optionsColumn,\n  statusDbColumn,\n  subscriptionDbColumn,\n  subscriptionIdColumn,\n  subscriptionTypeColumn,\n} from '../../column-definitions'\n\nexport const colFactory = (\n  instances: InstanceRedisCloud[],\n): ColumnDef<InstanceRedisCloud>[] => {\n  const columns: ColumnDef<InstanceRedisCloud>[] = [\n    databaseColumn(),\n    subscriptionIdColumn(),\n    subscriptionDbColumn(),\n    subscriptionTypeColumn(),\n    statusDbColumn(),\n    endpointColumn(),\n    modulesColumn(),\n    optionsColumn(instances),\n  ]\n\n  if (instances.length) {\n    return [getSelectionColumn<InstanceRedisCloud>(), ...columns]\n  }\n\n  return columns\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RedisCloudDatabasesResult from './RedisCloudDatabasesResult'\n\ndescribe('RedisCloudDatabasesResult', () => {\n  it('should render', () => {\n    const columnsMock = [\n      {\n        id: 'subscriptionId',\n        accessorKey: 'subscriptionId',\n        header: 'Subscription ID',\n        enableSorting: true,\n      },\n    ]\n    expect(\n      render(\n        <RedisCloudDatabasesResult\n          columns={columnsMock}\n          instances={[]}\n          onView={jest.fn()}\n          onBack={jest.fn()}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport RedisCloudDatabasesResult from './RedisCloudDatabasesResult'\nimport {\n  RedisCloudInstanceFactory,\n  RedisCloudInstanceFactorySuccess,\n  RedisCloudInstanceFactoryFail,\n  RedisCloudInstanceFactoryWithModules,\n  RedisCloudInstanceFactoryOptionsFull,\n} from 'uiSrc/mocks/factories/cloud/RedisCloudInstance.factory'\nimport { colFactory } from './utils/colFactory'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\n\nconst meta: Meta<typeof RedisCloudDatabasesResult> = {\n  component: RedisCloudDatabasesResult,\n  args: {\n    instances: [],\n    columns: [],\n    onView: () => {},\n    onBack: () => {},\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Empty: Story = {}\n\nconst mixedInstances = RedisCloudInstanceFactory.buildList(10)\nconst mixedColumns = colFactory(mixedInstances, mixedInstances)\nexport const MixedResults: Story = {\n  args: {\n    instances: mixedInstances,\n    columns: mixedColumns,\n  },\n}\n\nconst successInstances = RedisCloudInstanceFactorySuccess.buildList(8)\nconst successColumns = colFactory(successInstances, successInstances)\nexport const AllSuccess: Story = {\n  args: {\n    instances: successInstances,\n    columns: successColumns,\n  },\n}\n\nconst failInstances = RedisCloudInstanceFactoryFail.buildList(8)\nconst failColumns = colFactory(failInstances, failInstances)\nexport const AllFailed: Story = {\n  args: {\n    instances: failInstances,\n    columns: failColumns,\n  },\n}\n\nconst withModulesInstances = RedisCloudInstanceFactoryWithModules([\n  RedisDefaultModules.Search,\n  RedisDefaultModules.ReJSON,\n  RedisDefaultModules.TimeSeries,\n]).buildList(8)\nconst withModulesColumns = colFactory(\n  withModulesInstances,\n  withModulesInstances,\n)\nexport const WithModules: Story = {\n  args: {\n    instances: withModulesInstances,\n    columns: withModulesColumns,\n  },\n}\n\nconst withOptionsInstances = RedisCloudInstanceFactoryOptionsFull.buildList(8)\nconst withOptionsColumns = colFactory(\n  withOptionsInstances,\n  withOptionsInstances,\n)\nexport const WithOptions: Story = {\n  args: {\n    instances: withOptionsInstances,\n    columns: withOptionsColumns,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResult.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport type { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\nimport MessageBar from 'uiSrc/components/message-bar/MessageBar'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { SummaryText } from './components'\n\nexport interface Props {\n  instances: InstanceRedisCloud[]\n  columns: ColumnDef<InstanceRedisCloud>[]\n  onView: () => void\n  onBack: () => void\n}\n\nconst loadingMsg = 'loading...'\nconst notFoundMsg = 'Not found'\n\nconst RedisCloudDatabaseListResult = ({\n  instances,\n  columns,\n  onBack,\n  onView,\n}: Props) => {\n  const [items, setItems] = useState<InstanceRedisCloud[]>([])\n  const [message, setMessage] = useState(loadingMsg)\n\n  useEffect(() => setItems(instances), [instances])\n\n  const countSuccessAdded = instances.filter(\n    ({ statusAdded }) => statusAdded === AddRedisDatabaseStatus.Success,\n  )?.length\n\n  const countFailAdded = instances.filter(\n    ({ statusAdded }) => statusAdded === AddRedisDatabaseStatus.Fail,\n  )?.length\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n\n    const itemsTemp = instances.filter(\n      (item: InstanceRedisCloud) =>\n        item.name?.toLowerCase().indexOf(value) !== -1 ||\n        (item.publicEndpoint || '')?.toLowerCase().indexOf(value) !== -1 ||\n        item.subscriptionId?.toString()?.indexOf(value) !== -1 ||\n        item.subscriptionName?.toLowerCase().indexOf(value) !== -1 ||\n        item.databaseId?.toString()?.indexOf(value) !== -1,\n    )\n\n    if (!itemsTemp.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer>\n        <Header\n          title=\"Redis Enterprise Databases Added\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            columns={columns}\n            data={items}\n            defaultSorting={[\n              {\n                id: 'name',\n                desc: false,\n              },\n            ]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() => <EmptyState message={message} />}\n          />\n        </DatabaseWrapper>\n        <MessageBar\n          opened={!!countSuccessAdded || !!countFailAdded}\n          variant={\n            !!countFailAdded\n              ? riToast.Variant.Attention\n              : riToast.Variant.Success\n          }\n        >\n          <SummaryText\n            countSuccessAdded={countSuccessAdded}\n            countFailAdded={countFailAdded}\n          />\n        </MessageBar>\n      </DatabaseContainer>\n      <Footer>\n        <Row justify=\"end\">\n          <PrimaryButton\n            onClick={onView}\n            data-testid=\"btn-view-databases\"\n            disabled={items.length === 0}\n          >\n            View Databases\n          </PrimaryButton>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default RedisCloudDatabaseListResult\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, fireEvent, screen } from 'uiSrc/utils/test-utils'\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nimport RedisCloudDatabasesResultPage from './RedisCloudDatabasesResultPage'\nimport RedisCloudDatabasesResult, {\n  Props as RedisCloudDatabasesResultProps,\n} from './RedisCloudDatabasesResult'\n\njest.mock('./RedisCloudDatabasesResult', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockRedisCloudDatabasesResult = (\n  props: RedisCloudDatabasesResultProps,\n) => (\n  <div>\n    <button type=\"button\" onClick={() => props.onView()} data-testid=\"view-btn\">\n      onView\n    </button>\n    <button type=\"button\" onClick={() => props.onBack()} data-testid=\"back-btn\">\n      onBack\n    </button>\n    <div className=\"itemList\">\n      <Table\n        columns={props.columns}\n        data={[]}\n        defaultSorting={[{ id: 'name', desc: false }]}\n      />\n    </div>\n  </div>\n)\n\n/**\n * RedisCloudDatabasesResultPage tests\n *\n * @group component\n */\ndescribe('RedisCloudDatabasesResultPage', () => {\n  beforeAll(() => {\n    RedisCloudDatabasesResult.mockImplementation(mockRedisCloudDatabasesResult)\n  })\n  it('should render', () => {\n    const wrapped = render(<RedisCloudDatabasesResultPage />)\n\n    expect(wrapped).toBeTruthy()\n  })\n\n  it('should call onBack', () => {\n    const component = render(<RedisCloudDatabasesResultPage />)\n    fireEvent.click(screen.getByTestId('back-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onView', () => {\n    const component = render(<RedisCloudDatabasesResultPage />)\n    fireEvent.click(screen.getByTestId('view-btn'))\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx",
    "content": "import React from 'react'\nimport RedisCloudDatabasesResult from './RedisCloudDatabasesResult'\nimport { useCloudDatabasesResultConfig } from './hooks/useCloudDatabasesResultConfig'\n\nconst RedisCloudDatabasesResultPage = () => {\n  const { instances, columns, handleClose, handleBackAdding } =\n    useCloudDatabasesResultConfig()\n\n  return (\n    <RedisCloudDatabasesResult\n      instances={instances}\n      columns={columns}\n      onView={handleClose}\n      onBack={handleBackAdding}\n    />\n  )\n}\n\nexport default RedisCloudDatabasesResultPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.tsx",
    "content": "import React from 'react'\n\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport type { SummaryTextProps } from './SummaryText.types'\n\nexport const SummaryText = ({\n  countSuccessAdded,\n  countFailAdded,\n}: SummaryTextProps) => (\n  <Text size=\"M\">\n    <ColorText variant=\"semiBold\">Summary: </ColorText>{' '}\n    {countSuccessAdded ? (\n      <span>\n        Successfully added {countSuccessAdded} database(s)\n        {countFailAdded ? '. ' : '.'}\n      </span>\n    ) : null}\n    {countFailAdded ? (\n      <span>Failed to add {countFailAdded} database(s).</span>\n    ) : null}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/SummaryText/SummaryText.types.ts",
    "content": "export interface SummaryTextProps {\n  countSuccessAdded: number\n  countFailAdded: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/components/index.ts",
    "content": "export { SummaryText } from './SummaryText/SummaryText'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.ts",
    "content": "import { useCallback, useEffect, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport { Pages } from 'uiSrc/constants'\nimport {\n  cloudSelector,\n  resetDataRedisCloud,\n  resetLoadedRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport { LoadedCloud } from 'uiSrc/slices/interfaces'\nimport { setTitle } from 'uiSrc/utils'\nimport { colFactory } from '../utils/colFactory'\nimport { UseCloudDatabasesResultConfigReturn } from './useCloudDatabasesResultConfig.types'\n\nexport const useCloudDatabasesResultConfig =\n  (): UseCloudDatabasesResultConfigReturn => {\n    const dispatch = useDispatch()\n    const history = useHistory()\n\n    const { data: instancesForOptions, dataAdded: instances } =\n      useSelector(cloudSelector)\n\n    useEffect(() => {\n      if (!instances.length) {\n        history.push(Pages.home)\n      }\n      setTitle('Redis Enterprise Databases Added')\n    }, [instances.length, history])\n\n    const handleClose = useCallback(() => {\n      dispatch(resetDataRedisCloud())\n      history.push(Pages.home)\n    }, [dispatch, history])\n\n    const handleBackAdding = useCallback(() => {\n      dispatch(resetLoadedRedisCloud(LoadedCloud.InstancesAdded))\n      history.push(Pages.home)\n    }, [dispatch, history])\n\n    const columns = useMemo(\n      () => colFactory(instances || [], instancesForOptions || []),\n      [instances, instancesForOptions],\n    )\n\n    return {\n      instances,\n      columns,\n      handleClose,\n      handleBackAdding,\n    }\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/hooks/useCloudDatabasesResultConfig.types.ts",
    "content": "import { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { InstanceRedisCloud } from 'uiSrc/slices/interfaces'\n\nexport interface UseCloudDatabasesResultConfigReturn {\n  instances: InstanceRedisCloud[]\n  columns: ColumnDef<InstanceRedisCloud>[]\n  handleClose: () => void\n  handleBackAdding: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/index.ts",
    "content": "import RedisCloudDatabasesResultPage from './RedisCloudDatabasesResultPage'\n\nexport { RedisCloudDatabasesResultPage }\n\nexport default RedisCloudDatabasesResultPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/styles.module.scss",
    "content": ".footer {\n  padding: 15px 14px 10px 30px !important;\n}\n\n.title {\n  padding-bottom: 5px !important;\n}\n\n.subTitle {\n}\n\n.searchForm {\n  width: 266px !important;\n}\n\n.panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n\n.tooltipColumnName {\n  max-width: 370px !important;\n  * {\n    line-height: 1.19;\n    font-size: 14px !important;\n\n    &:not(:global(.euiToolTip__title)) {\n      font-weight: 300 !important;\n    }\n  }\n}\n\n.errorBtn {\n  height: 100% !important;\n  > span {\n    padding: 0 !important;\n  }\n}\n\n.tooltipColumnNameText {\n  text-decoration: underline;\n  display: inline-block;\n  width: 100%;\n  overflow: hidden;\n  vertical-align: top;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.spec.ts",
    "content": "import {\n  RedisDefaultModules,\n  AddRedisClusterDatabaseOptions,\n  type InstanceRedisCloud,\n} from 'uiSrc/slices/interfaces'\nimport { colFactory } from './colFactory'\nimport { RedisCloudInstanceFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudInstance.factory'\nimport { AutoDiscoverCloudIds } from 'uiSrc/pages/autodiscover-cloud/constants/constants'\n\ndescribe('colFactory', () => {\n  it('should return base columns without modules and options when instances array is empty', () => {\n    const instances: InstanceRedisCloud[] = []\n\n    const columns = colFactory(instances, instances)\n\n    expect(columns).toHaveLength(7)\n    expect(columns.map((col) => col.id)).toEqual([\n      AutoDiscoverCloudIds.Name,\n      AutoDiscoverCloudIds.SubscriptionId,\n      AutoDiscoverCloudIds.SubscriptionName,\n      AutoDiscoverCloudIds.SubscriptionType,\n      AutoDiscoverCloudIds.Status,\n      AutoDiscoverCloudIds.PublicEndpoint,\n      AutoDiscoverCloudIds.MessageAdded,\n    ])\n  })\n\n  it('should return base columns without modules and options when instances have no modules or options', () => {\n    const instances = RedisCloudInstanceFactory.buildList(1, {\n      modules: [],\n      options: undefined,\n    })\n\n    const columns = colFactory(instances, instances)\n\n    expect(columns).toHaveLength(7)\n    expect(columns.map((col) => col.id)).toEqual([\n      AutoDiscoverCloudIds.Name,\n      AutoDiscoverCloudIds.SubscriptionId,\n      AutoDiscoverCloudIds.SubscriptionName,\n      AutoDiscoverCloudIds.SubscriptionType,\n      AutoDiscoverCloudIds.Status,\n      AutoDiscoverCloudIds.PublicEndpoint,\n      AutoDiscoverCloudIds.MessageAdded,\n    ])\n  })\n\n  it('should include modules column when at least one instance has modules', () => {\n    const instances = [\n      RedisCloudInstanceFactory.build({ modules: [], options: undefined }),\n      RedisCloudInstanceFactory.build({\n        modules: [RedisDefaultModules.ReJSON],\n        options: undefined,\n      }),\n    ]\n\n    const columns = colFactory(instances, instances)\n\n    expect(columns).toHaveLength(8)\n    expect(columns.map((col) => col.id)).toContain(AutoDiscoverCloudIds.Modules)\n    expect(columns[6].id).toBe(AutoDiscoverCloudIds.Modules)\n    expect(columns.map((col) => col.id)).not.toContain(\n      AutoDiscoverCloudIds.Options,\n    )\n  })\n\n  it('should include options column when at least one instance has options with truthy values', () => {\n    const instances = [\n      RedisCloudInstanceFactory.build({ modules: [], options: {} }),\n      RedisCloudInstanceFactory.build({\n        modules: [],\n        options: {\n          [AddRedisClusterDatabaseOptions.Backup]: true,\n          [AddRedisClusterDatabaseOptions.Clustering]: false,\n        },\n      }),\n    ]\n\n    const columns = colFactory(instances, instances)\n\n    expect(columns).toHaveLength(8)\n    expect(columns.map((col) => col.id)).toContain(AutoDiscoverCloudIds.Options)\n    expect(columns[6].id).toBe(AutoDiscoverCloudIds.Options)\n    expect(columns.map((col) => col.id)).not.toContain(\n      AutoDiscoverCloudIds.Modules,\n    )\n  })\n  it('should include both modules and options columns when instances have both', () => {\n    const instances = RedisCloudInstanceFactory.buildList(1, {\n      modules: [RedisDefaultModules.ReJSON],\n      options: {\n        [AddRedisClusterDatabaseOptions.Backup]: true,\n      },\n    })\n\n    const columns = colFactory(instances, instances)\n\n    expect(columns).toHaveLength(9)\n    expect(columns.map((col) => col.id)).toEqual([\n      AutoDiscoverCloudIds.Name,\n      AutoDiscoverCloudIds.SubscriptionId,\n      AutoDiscoverCloudIds.SubscriptionName,\n      AutoDiscoverCloudIds.SubscriptionType,\n      AutoDiscoverCloudIds.Status,\n      AutoDiscoverCloudIds.PublicEndpoint,\n      AutoDiscoverCloudIds.Modules,\n      AutoDiscoverCloudIds.Options,\n      AutoDiscoverCloudIds.MessageAdded,\n    ])\n  })\n\n  it('should always have message column as the last column', () => {\n    const instancesWithModules = RedisCloudInstanceFactory.buildList(1, {\n      modules: [RedisDefaultModules.ReJSON],\n      options: undefined,\n    })\n\n    const columnsWithModules = colFactory(\n      instancesWithModules,\n      instancesWithModules,\n    )\n    expect(columnsWithModules[columnsWithModules.length - 1].id).toBe(\n      AutoDiscoverCloudIds.MessageAdded,\n    )\n\n    const instancesWithOptions = RedisCloudInstanceFactory.buildList(1, {\n      modules: [],\n      options: { [AddRedisClusterDatabaseOptions.Backup]: true },\n    })\n\n    const columnsWithOptions = colFactory(\n      instancesWithOptions,\n      instancesWithOptions,\n    )\n    expect(columnsWithOptions[columnsWithOptions.length - 1].id).toBe(\n      AutoDiscoverCloudIds.MessageAdded,\n    )\n\n    const instancesWithBoth = RedisCloudInstanceFactory.buildList(1, {\n      modules: [RedisDefaultModules.ReJSON],\n      options: { [AddRedisClusterDatabaseOptions.Backup]: true },\n    })\n\n    const columnsWithBoth = colFactory(instancesWithBoth, instancesWithBoth)\n    expect(columnsWithBoth[columnsWithBoth.length - 1].id).toBe(\n      AutoDiscoverCloudIds.MessageAdded,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/utils/colFactory.ts",
    "content": "import { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCloud } from 'uiSrc/slices/interfaces'\nimport {\n  databaseResultColumn,\n  subscriptionIdResultColumn,\n  subscriptionDbResultColumn,\n  subscriptionTypeResultColumn,\n  statusDbResultColumn,\n  endpointResultColumn,\n  modulesResultColumn,\n  optionsResultColumn,\n  messageResultColumn,\n} from '../../column-definitions'\n\nexport const colFactory = (\n  instances: InstanceRedisCloud[] = [],\n  instancesForOptions: InstanceRedisCloud[] = [],\n): ColumnDef<InstanceRedisCloud>[] => {\n  const shouldShowCapabilities = instances.some(\n    (instance) => instance.modules?.length,\n  )\n  const shouldShowOptions = instances.some(\n    (instance) =>\n      instance.options &&\n      Object.values(instance.options).filter(Boolean).length,\n  )\n\n  const columns: ColumnDef<InstanceRedisCloud>[] = [\n    databaseResultColumn(),\n    subscriptionIdResultColumn(),\n    subscriptionDbResultColumn(),\n    subscriptionTypeResultColumn(),\n    statusDbResultColumn(),\n    endpointResultColumn(),\n  ]\n\n  if (shouldShowCapabilities) {\n    columns.push(modulesResultColumn())\n  }\n\n  if (shouldShowOptions) {\n    columns.push(optionsResultColumn(instancesForOptions))\n  }\n\n  columns.push(messageResultColumn())\n\n  return columns\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  RedisCloudSubscription,\n  RedisCloudSubscriptionStatus,\n  RedisCloudSubscriptionType,\n} from 'uiSrc/slices/interfaces'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RedisCloudSubscriptions, { Props } from './RedisCloudSubscriptions'\n\nconst mockedProps = mock<Props>()\n\ndescribe('RedisCloudSubscriptions', () => {\n  it('should render', () => {\n    const columnsMock = [\n      {\n        id: 'subscriptionId',\n        accessorKey: 'subscriptionId',\n        header: 'Subscription ID',\n        enableSorting: true,\n      },\n    ]\n\n    const subscriptionsMock: RedisCloudSubscription[] = [\n      {\n        id: 123,\n        name: 'name',\n        numberOfDatabases: 123,\n        provider: 'provider',\n        region: 'region',\n        status: RedisCloudSubscriptionStatus.Active,\n        type: RedisCloudSubscriptionType.Fixed,\n        free: false,\n      },\n    ]\n    expect(\n      render(\n        <RedisCloudSubscriptions\n          {...instance(mockedProps)}\n          columns={columnsMock}\n          subscriptions={subscriptionsMock}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport RedisCloudSubscriptions from './RedisCloudSubscriptions'\nimport { colFactory } from '../utils/colFactory'\nimport {\n  RedisCloudAccount,\n  RedisCloudSubscription,\n} from 'uiSrc/slices/interfaces'\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\nimport { RedisCloudSubscriptionFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudSubscription.factory'\nimport { RedisCloudAccountFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudAccount.factory'\n\nconst subscriptionsMock: RedisCloudSubscription[] =\n  RedisCloudSubscriptionFactory.buildList(3)\nconst subscriptions100: RedisCloudSubscription[] =\n  RedisCloudSubscriptionFactory.buildList(100)\n\nconst emptyColumns = colFactory([])\n\nconst accountMock = RedisCloudAccountFactory.build()\nconst meta: Meta<typeof RedisCloudSubscriptions> = {\n  component: RedisCloudSubscriptions,\n  args: {\n    columns: emptyColumns,\n    subscriptions: [],\n    selection: [],\n    account: null,\n    loading: false,\n    onSubmit: () => {},\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Empty: Story = {}\n\nconst RenderStory = ({\n  account,\n  columns,\n  subscriptions,\n}: {\n  account: RedisCloudAccount\n  columns: ReturnType<typeof colFactory>\n  subscriptions: RedisCloudSubscription[]\n}) => {\n  const [selection, setSelection] = useState<RedisCloudSubscription[]>([])\n\n  const handleSelectionChange = (currentSelected: RowSelectionState) => {\n    const newSelection = subscriptions.filter((item) => {\n      const { id } = item\n      if (!id) {\n        return false\n      }\n      return currentSelected[id]\n    })\n    setSelection(newSelection)\n  }\n\n  return (\n    <RedisCloudSubscriptions\n      error=\"\"\n      onClose={fn()}\n      onBack={fn()}\n      onSelectionChange={handleSelectionChange}\n      selection={selection}\n      columns={columns}\n      subscriptions={subscriptions}\n      loading={false}\n      account={account}\n      onSubmit={fn()}\n    />\n  )\n}\n\nexport const WithSubscription: Story = {\n  render: () => (\n    <RenderStory\n      account={accountMock}\n      columns={colFactory(subscriptionsMock)}\n      subscriptions={subscriptionsMock}\n    />\n  ),\n}\n\nexport const With100Subscriptions: Story = {\n  render: () => (\n    <RenderStory\n      account={accountMock}\n      columns={colFactory(subscriptions100)}\n      subscriptions={subscriptions100}\n    />\n  ),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Group, Item } from 'uiSrc/components/base/layout/list'\n\nexport const AlertStatusDot = styled.span`\n  &::before {\n    font-size: 8px;\n    padding: 0 14px;\n    content: ' \\\\25CF';\n    vertical-align: middle;\n  }\n`\n\nexport const AlertStatusListItem = styled(Item)`\n  line-height: 20px;\n`\nexport const AlertStatusList = styled(Group)`\n  opacity: 0.85;\n  padding-bottom: 5px;\n  padding-top: 10px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptions/RedisCloudSubscriptions.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { map } from 'lodash'\nimport {\n  type InstanceRedisCloud,\n  type RedisCloudAccount,\n  type RedisCloudSubscription,\n  RedisCloudSubscriptionStatus,\n} from 'uiSrc/slices/interfaces'\nimport { type Maybe, type Nullable } from 'uiSrc/utils'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport MessageBar from 'uiSrc/components/message-bar/MessageBar'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\nimport {\n  type ColumnDef,\n  type RowSelectionState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { canSelectRow } from '../utils/canSelectRow'\nimport { Account, CancelButton, SubmitButton, SummaryText } from '../components'\n\nexport interface Props {\n  columns: ColumnDef<RedisCloudSubscription>[]\n  subscriptions: Nullable<RedisCloudSubscription[]>\n  selection: Nullable<RedisCloudSubscription[]>\n  loading: boolean\n  account: Nullable<RedisCloudAccount>\n  error: string\n  onClose: () => void\n  onBack: () => void\n  onSubmit: (\n    subscriptions: Maybe<\n      Pick<InstanceRedisCloud, 'subscriptionId' | 'subscriptionType' | 'free'>\n    >[],\n  ) => void\n  onSelectionChange: (state: RowSelectionState) => void\n}\n\nconst loadingMsg = 'loading...'\nconst notFoundMsg = 'Not found'\nconst noResultsMessage = 'Your Redis Cloud has no subscriptions available.'\n\nconst RedisCloudSubscriptions = ({\n  subscriptions,\n  selection,\n  columns,\n  loading,\n  account = null,\n  onClose,\n  onBack,\n  onSubmit,\n  onSelectionChange,\n}: Props) => {\n  // const subscriptions = [];\n  const [items, setItems] = useState(subscriptions || [])\n  const [message, setMessage] = useState(loadingMsg)\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  useEffect(() => {\n    if (subscriptions !== null) {\n      setItems(subscriptions)\n    }\n\n    if (subscriptions?.length === 0 && !loading) {\n      setMessage(noResultsMessage)\n    }\n  }, [subscriptions, loading])\n\n  const countStatusActive = items.filter(\n    ({ status, numberOfDatabases }: RedisCloudSubscription) =>\n      status === RedisCloudSubscriptionStatus.Active && numberOfDatabases !== 0,\n  )?.length\n\n  const countStatusFailed = items.length - countStatusActive\n\n  const handleSubmit = () => {\n    onSubmit(\n      map(selection, ({ id, type, free }) => ({\n        subscriptionId: id,\n        subscriptionType: type,\n        free,\n      })),\n    )\n  }\n\n  const showPopover = () => {\n    setIsPopoverOpen(true)\n  }\n\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n    const itemsTemp =\n      subscriptions?.filter(\n        (item: RedisCloudSubscription) =>\n          item.name?.toLowerCase()?.indexOf(value) !== -1 ||\n          item.id?.toString()?.toLowerCase().indexOf(value) !== -1,\n      ) ?? []\n\n    if (!itemsTemp?.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title=\"Redis Cloud Subscriptions\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          {account && (\n            <>\n              <Account account={account} />\n              <Spacer size=\"m\" />\n            </>\n          )}\n          <Table\n            rowSelectionMode=\"multiple\"\n            getRowCanSelect={canSelectRow}\n            onRowSelectionChange={onSelectionChange}\n            getRowId={(row) => `${row.id}`}\n            columns={columns}\n            data={items}\n            defaultSorting={[\n              {\n                id: 'name',\n                desc: false,\n              },\n            ]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() => <EmptyState message={message} />}\n          />\n        </DatabaseWrapper>\n        <MessageBar\n          opened={countStatusActive + countStatusFailed > 0}\n          variant={\n            !!countStatusFailed\n              ? riToast.Variant.Attention\n              : riToast.Variant.Success\n          }\n        >\n          <SummaryText\n            countStatusActive={countStatusActive}\n            countStatusFailed={countStatusFailed}\n          />\n        </MessageBar>\n      </DatabaseContainer>\n\n      <Footer>\n        <Row justify=\"end\">\n          <Row gap=\"m\" grow={false}>\n            <CancelButton\n              isPopoverOpen={isPopoverOpen}\n              onClose={onClose}\n              onShowPopover={showPopover}\n              onClosePopover={closePopover}\n            />\n            <SubmitButton\n              isDisabled={(selection?.length || 0) < 1}\n              loading={loading}\n              onClick={handleSubmit}\n            />\n          </Row>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default RedisCloudSubscriptions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.spec.tsx",
    "content": "import React from 'react'\nimport { cloudSelector } from 'uiSrc/slices/instances/cloud'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport RedisCloudSubscriptionsPage from './RedisCloudSubscriptionsPage'\n\njest.mock('uiSrc/slices/instances/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/cloud'),\n  cloudSelector: jest.fn().mockReturnValue({\n    ...jest.requireActual('uiSrc/slices/instances/cloud').initialState,\n  }),\n}))\n\n/**\n * RedisCloudSubscriptionsPage tests\n *\n * @group component\n */\ndescribe('RedisCloudSubscriptionsPage', () => {\n  it('should render', () => {\n    expect(render(<RedisCloudSubscriptionsPage />)).toBeTruthy()\n  })\n\n  it('should render with subscriptions', () => {\n    const cloudSelectorMock = jest.fn().mockReturnValue({\n      credentials: null,\n      loading: false,\n      error: '',\n      loaded: { instances: false },\n      account: { error: '', data: [] },\n      subscriptions: [\n        {\n          id: 123,\n          name: 'name',\n          numberOfDatabases: 123,\n          provider: 'provider',\n          region: 'region',\n          status: 'active',\n        },\n      ],\n    })\n    cloudSelector.mockImplementation(cloudSelectorMock)\n    expect(render(<RedisCloudSubscriptionsPage />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx",
    "content": "import React from 'react'\n\nimport RedisCloudSubscriptions from './RedisCloudSubscriptions/RedisCloudSubscriptions'\nimport { useCloudSubscriptionConfig } from './hooks/useCloudSubscriptionConfig'\n\nconst RedisCloudSubscriptionsPage = () => {\n  const {\n    loading,\n    account,\n    selection,\n    columns,\n    subscriptions,\n    subscriptionsError,\n    accountError,\n    handleClose,\n    handleBackAdding,\n    handleLoadInstances,\n    handleSelectionChange,\n  } = useCloudSubscriptionConfig()\n\n  return (\n    <RedisCloudSubscriptions\n      selection={selection}\n      columns={columns}\n      subscriptions={subscriptions}\n      loading={loading}\n      account={account}\n      error={subscriptionsError || accountError || ''}\n      onClose={handleClose}\n      onBack={handleBackAdding}\n      onSubmit={handleLoadInstances}\n      onSelectionChange={handleSelectionChange}\n    />\n  )\n}\n\nexport default RedisCloudSubscriptionsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.style.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { type Theme } from 'uiSrc/components/base/theme/types'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nexport const AccountWrapper = styled(Row).attrs({\n  justify: 'start',\n  gap: 'l',\n  align: 'center',\n})`\n  align-self: stretch;\n  width: 100%;\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n  min-height: 44px;\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral200};\n`\n\nexport const AccountItem = styled(FlexItem).attrs({\n  grow: false,\n  direction: 'row',\n})`\n  align-items: center;\n  gap: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n  &:not(:last-child):after {\n    content: '';\n    margin-left: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n    border-right: 1px solid\n      ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral400};\n    height: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n  }\n`\n\nexport const AccountItemTitle = styled(ColorText).attrs({\n  size: 'M',\n})`\n  color: ${({ theme }: { theme: Theme }) =>\n    theme.components.typography.colors.secondary};\n  white-space: nowrap;\n`\n\nexport const LoadingWrapper = styled.div`\n  width: 80px;\n  height: 15px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.tsx",
    "content": "import React from 'react'\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nimport * as S from './Account.style'\nimport { type AccountProps, type AccountValueProps } from './Account.types'\n\nconst AccountValue = ({ value, ...rest }: AccountValueProps) => {\n  if (!value) {\n    return (\n      <S.LoadingWrapper>\n        <LoadingContent lines={1} />\n      </S.LoadingWrapper>\n    )\n  }\n\n  return (\n    <ColorText color=\"primary\" size=\"M\" {...rest}>\n      {value}\n    </ColorText>\n  )\n}\n\nexport const Account = ({\n  account: { accountId, accountName, ownerEmail, ownerName },\n}: AccountProps) => (\n  <S.AccountWrapper>\n    {accountId && (\n      <S.AccountItem>\n        <S.AccountItemTitle>Account ID:</S.AccountItemTitle>\n        <AccountValue data-testid=\"account-id\" value={accountId} />\n      </S.AccountItem>\n    )}\n    {accountName && (\n      <S.AccountItem>\n        <S.AccountItemTitle>Name:</S.AccountItemTitle>\n        <AccountValue data-testid=\"account-name\" value={accountName} />\n      </S.AccountItem>\n    )}\n    {ownerName && (\n      <S.AccountItem>\n        <S.AccountItemTitle>Owner Name:</S.AccountItemTitle>\n        <AccountValue data-testid=\"account-owner-name\" value={ownerName} />\n      </S.AccountItem>\n    )}\n    {ownerEmail && (\n      <S.AccountItem>\n        <S.AccountItemTitle>Owner Email:</S.AccountItemTitle>\n        <AccountValue data-testid=\"account-owner-email\" value={ownerEmail} />\n      </S.AccountItem>\n    )}\n  </S.AccountWrapper>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/Account/Account.types.ts",
    "content": "import { type RedisCloudAccount } from 'uiSrc/slices/interfaces'\nimport { type Nullable } from 'uiSrc/utils'\n\nexport interface AccountProps {\n  account: RedisCloudAccount\n}\n\nexport interface AccountValueProps {\n  value?: Nullable<string | number>\n  'data-testid'?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.tsx",
    "content": "import React from 'react'\nimport {\n  SecondaryButton,\n  DestructiveButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\n\nimport styles from '../../styles.module.scss'\nimport { type CancelButtonProps } from './CancelButton.types'\n\nexport const CancelButton = ({\n  isPopoverOpen,\n  onClose,\n  onShowPopover,\n  onClosePopover,\n}: CancelButtonProps) => (\n  <RiPopover\n    anchorPosition=\"upCenter\"\n    isOpen={isPopoverOpen}\n    closePopover={onClosePopover}\n    panelClassName={styles.panelCancelBtn}\n    panelPaddingSize=\"l\"\n    button={\n      <SecondaryButton\n        onClick={onShowPopover}\n        className=\"btn-cancel\"\n        data-testid=\"btn-cancel\"\n      >\n        Cancel\n      </SecondaryButton>\n    }\n  >\n    <Text size=\"m\">\n      Your changes have not been saved.&#10;&#13; Do you want to proceed to the\n      list of databases?\n    </Text>\n    <br />\n    <div>\n      <DestructiveButton\n        size=\"s\"\n        onClick={onClose}\n        data-testid=\"btn-cancel-proceed\"\n      >\n        Proceed\n      </DestructiveButton>\n    </div>\n  </RiPopover>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/CancelButton/CancelButton.types.ts",
    "content": "export interface CancelButtonProps {\n  isPopoverOpen: boolean\n  onClose: () => void\n  onShowPopover: () => void\n  onClosePopover: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.tsx",
    "content": "import React from 'react'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport validationErrors from 'uiSrc/constants/validationErrors'\n\nimport { type SubmitButtonProps } from './SubmitButton.types'\n\nexport const SubmitButton = ({\n  isDisabled,\n  loading,\n  onClick,\n}: SubmitButtonProps) => (\n  <RiTooltip\n    position=\"top\"\n    anchorClassName=\"euiToolTip__btn-disabled\"\n    title={\n      isDisabled ? validationErrors.SELECT_AT_LEAST_ONE('subscription') : null\n    }\n    content={\n      isDisabled ? <span>{validationErrors.NO_SUBSCRIPTIONS_CLOUD}</span> : null\n    }\n  >\n    <PrimaryButton\n      size=\"m\"\n      disabled={isDisabled}\n      onClick={onClick}\n      loading={loading}\n      data-testid=\"btn-show-databases\"\n    >\n      Show databases\n    </PrimaryButton>\n  </RiTooltip>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SubmitButton/SubmitButton.types.ts",
    "content": "export interface SubmitButtonProps {\n  isDisabled: boolean\n  loading: boolean\n  onClick: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.tsx",
    "content": "import React from 'react'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\n\nimport { type SummaryTextProps } from './SummaryText.types'\n\nexport const SummaryText = ({\n  countStatusActive,\n  countStatusFailed,\n}: SummaryTextProps) => (\n  <Text size=\"M\">\n    <ColorText variant=\"semiBold\">Summary: </ColorText>\n    {countStatusActive ? (\n      <span>\n        Successfully discovered database(s) in {countStatusActive}\n        &nbsp;\n        {countStatusActive > 1 ? 'subscriptions' : 'subscription'}\n        .&nbsp;\n      </span>\n    ) : null}\n\n    {countStatusFailed ? (\n      <span>\n        Failed to discover database(s) in {countStatusFailed}\n        &nbsp;\n        {countStatusFailed > 1 ? 'subscriptions.' : ' subscription.'}\n      </span>\n    ) : null}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/SummaryText/SummaryText.types.ts",
    "content": "export interface SummaryTextProps {\n  countStatusActive: number\n  countStatusFailed: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/components/index.ts",
    "content": "export { Account } from './Account/Account'\nexport { CancelButton } from './CancelButton/CancelButton'\nexport { SubmitButton } from './SubmitButton/SubmitButton'\nexport { SummaryText } from './SummaryText/SummaryText'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.test.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport {\n  act,\n  initialStateDefault,\n  mockStore,\n  renderHook,\n} from 'uiSrc/utils/test-utils'\nimport {\n  OAuthSocialAction,\n  RedisCloudSubscriptionStatus,\n  RedisCloudSubscriptionType,\n} from 'uiSrc/slices/interfaces'\nimport {\n  resetDataRedisCloud,\n  resetLoadedRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\n\nimport { useCloudSubscriptionConfig } from './useCloudSubscriptionConfig'\n\ndescribe('useCloudSubscriptionConfig', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should return correct initial state with subscriptions', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [\n      {\n        id: 1,\n        name: 'sub1',\n        status: RedisCloudSubscriptionStatus.Active,\n        numberOfDatabases: 5,\n      } as any,\n    ]\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    expect(result.current.columns).toHaveLength(9)\n    expect(result.current.columns[0].id).toBe('row-selection')\n    expect(result.current.subscriptions).toHaveLength(1)\n    expect(result.current.selection).toEqual([])\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should redirect to home when subscriptions is null', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = null\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    // The redirect happens in useEffect, check that it would be triggered\n    // expect(mockPush).toHaveBeenCalledWith(Pages.home)\n    expect(result.current.subscriptions).toBeNull()\n  })\n\n  it('should handle selection changes correctly', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [\n      {\n        id: 1,\n        name: 'sub1',\n        status: RedisCloudSubscriptionStatus.Active,\n        numberOfDatabases: 5,\n      } as any,\n      {\n        id: 2,\n        name: 'sub2',\n        status: RedisCloudSubscriptionStatus.Active,\n        numberOfDatabases: 3,\n      } as any,\n    ]\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    act(() => {\n      result.current.handleSelectionChange({ 1: true, 2: false })\n    })\n\n    expect(result.current.selection).toHaveLength(1)\n    expect(result.current.selection[0].id).toBe(1)\n  })\n\n  it('should dispatch resetDataRedisCloud on close', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [{ id: 1 } as any]\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    act(() => {\n      result.current.handleClose()\n    })\n\n    const actions = store.getActions()\n    expect(actions.map((a) => a.type)).toContain(resetDataRedisCloud.type)\n  })\n\n  it('should dispatch resetLoadedRedisCloud on back', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [{ id: 1 } as any]\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    act(() => {\n      result.current.handleBackAdding()\n    })\n\n    const actions = store.getActions()\n    expect(actions.map((a) => a.type)).toContain(resetLoadedRedisCloud.type)\n  })\n\n  it('should call handleLoadInstances with correct parameters', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [{ id: 1 } as any]\n    state.connections.cloud.credentials = {\n      accessKey: 'test-key',\n      secretKey: 'test-secret',\n    }\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    const subscriptionsToLoad = [\n      {\n        subscriptionId: 1,\n        subscriptionType: RedisCloudSubscriptionType.Flexible,\n        free: false,\n      },\n    ]\n\n    // Just check that the handler exists and can be called without error\n    expect(result.current.handleLoadInstances).toBeDefined()\n    expect(() => {\n      act(() => {\n        result.current.handleLoadInstances(subscriptionsToLoad)\n      })\n    }).not.toThrow()\n  })\n\n  it('should check instances loaded state', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [{ id: 1 } as any]\n    state.connections.cloud.loaded.instances = true\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    // The navigation happens in useEffect based on instancesLoaded\n    expect(result.current).toBeDefined()\n  })\n\n  it('should handle SSO flow correctly', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [{ id: 1 } as any]\n    state.connections.cloud.ssoFlow = OAuthSocialAction.Import\n    state.oauth.cloud.user.data = { id: 123 }\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), {\n      store,\n    })\n\n    // Check that hook returns expected values for SSO flow\n    expect(result.current.subscriptions).toHaveLength(1)\n  })\n\n  it('should filter out items without id when handling selection', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = [\n      { id: 1, name: 'sub1' } as any,\n      { id: undefined, name: 'sub2' } as any,\n      { id: 3, name: 'sub3' } as any,\n    ]\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    act(() => {\n      result.current.handleSelectionChange({\n        1: true,\n        undefined: true,\n        3: true,\n      })\n    })\n\n    expect(result.current.selection).toHaveLength(2)\n    expect(result.current.selection.map((s) => s.id)).toEqual([1, 3])\n  })\n\n  it('should return 7 columns when subscriptions array is empty', () => {\n    const state = cloneDeep(initialStateDefault)\n    state.connections.cloud.subscriptions = []\n    const store = mockStore(state)\n\n    const { result } = renderHook(() => useCloudSubscriptionConfig(), { store })\n\n    expect(result.current.columns).toHaveLength(7)\n    expect(result.current.columns[0].id).toBe('id')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.ts",
    "content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  InstanceRedisCloud,\n  LoadedCloud,\n  OAuthSocialAction,\n  RedisCloudSubscription,\n} from 'uiSrc/slices/interfaces'\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\nimport {\n  cloudSelector,\n  fetchInstancesRedisCloud,\n  fetchSubscriptionsRedisCloud,\n  resetDataRedisCloud,\n  resetLoadedRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud'\nimport { Maybe, setTitle } from 'uiSrc/utils'\nimport { Pages } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { colFactory } from '../utils/colFactory'\nimport { UseCloudSubscriptionConfigReturn } from './useCloudSubscriptionConfig.types'\n\nexport const useCloudSubscriptionConfig =\n  (): UseCloudSubscriptionConfigReturn => {\n    const dispatch = useDispatch()\n    const history = useHistory()\n\n    const {\n      ssoFlow,\n      credentials,\n      subscriptions,\n      loading,\n      error: subscriptionsError,\n      loaded: { instances: instancesLoaded },\n      account: { error: accountError, data: account },\n    } = useSelector(cloudSelector)\n    const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector)\n    const currentAccountIdRef = useRef(userOAuthProfile?.id)\n    const ssoFlowRef = useRef(ssoFlow)\n\n    const [selection, setSelection] = useState<RedisCloudSubscription[]>([])\n\n    useEffect(() => {\n      if (subscriptions === null) {\n        history.push(Pages.home)\n      } else {\n        setTitle('Redis Cloud Subscriptions')\n      }\n    }, [])\n\n    useEffect(() => {\n      if (ssoFlowRef.current !== OAuthSocialAction.Import) return\n\n      if (!userOAuthProfile) {\n        history.push(Pages.home)\n        return\n      }\n\n      if (currentAccountIdRef.current !== userOAuthProfile?.id) {\n        dispatch(fetchSubscriptionsRedisCloud(null, true))\n        currentAccountIdRef.current = userOAuthProfile?.id\n      }\n    }, [userOAuthProfile])\n\n    useEffect(() => {\n      if (instancesLoaded) {\n        history.push(Pages.redisCloudDatabases)\n      }\n    }, [instancesLoaded])\n\n    const sendCancelEvent = useCallback(() => {\n      sendEventTelemetry({\n        event:\n          TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED,\n      })\n    }, [])\n\n    const handleClose = useCallback(() => {\n      sendCancelEvent()\n      dispatch(resetDataRedisCloud())\n      history.push(Pages.home)\n    }, [dispatch, history, sendCancelEvent])\n\n    const handleBackAdding = useCallback(() => {\n      sendCancelEvent()\n      dispatch(resetLoadedRedisCloud(LoadedCloud.Subscriptions))\n      history.push(Pages.home)\n    }, [dispatch, history, sendCancelEvent])\n\n    const handleLoadInstances = useCallback(\n      (\n        subscriptions: Maybe<\n          Pick<\n            InstanceRedisCloud,\n            'subscriptionId' | 'subscriptionType' | 'free'\n          >\n        >[],\n      ) => {\n        dispatch(\n          fetchInstancesRedisCloud(\n            { subscriptions, credentials },\n            ssoFlow === OAuthSocialAction.Import,\n          ),\n        )\n      },\n      [dispatch, credentials, ssoFlow],\n    )\n\n    const handleSelectionChange = useCallback(\n      (currentSelected: RowSelectionState) => {\n        const newSelection = subscriptions?.filter(({ id }) => {\n          if (!id) {\n            return false\n          }\n          return currentSelected[id]\n        })\n        setSelection(newSelection || [])\n      },\n      [subscriptions],\n    )\n\n    const columns = useMemo(\n      () => colFactory(subscriptions || []),\n      [subscriptions],\n    )\n\n    return {\n      columns,\n      selection,\n      loading,\n      account,\n      subscriptions,\n      subscriptionsError,\n      accountError,\n      handleClose,\n      handleBackAdding,\n      handleLoadInstances,\n      handleSelectionChange,\n    }\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/hooks/useCloudSubscriptionConfig.types.ts",
    "content": "import {\n  ColumnDef,\n  RowSelectionState,\n} from 'uiSrc/components/base/layout/table'\nimport {\n  InstanceRedisCloud,\n  RedisCloudAccount,\n  RedisCloudSubscription,\n} from 'uiSrc/slices/interfaces'\nimport { Maybe } from 'uiSrc/utils'\n\nexport interface UseCloudSubscriptionConfigReturn {\n  columns: ColumnDef<RedisCloudSubscription>[]\n  selection: RedisCloudSubscription[]\n  loading: boolean\n  account: RedisCloudAccount | null\n  subscriptions: RedisCloudSubscription[] | null\n  subscriptionsError: string\n  accountError: string\n  handleClose: () => void\n  handleBackAdding: () => void\n  handleLoadInstances: (\n    subscriptions: Maybe<\n      Pick<InstanceRedisCloud, 'subscriptionId' | 'subscriptionType' | 'free'>\n    >[],\n  ) => void\n  handleSelectionChange: (currentSelected: RowSelectionState) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/index.ts",
    "content": "import RedisCloudSubscriptionsPage from './RedisCloudSubscriptionsPage'\n\nexport { RedisCloudSubscriptionsPage }\n\nexport default RedisCloudSubscriptionsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/styles.module.scss",
    "content": ":global {\n  .homePage {\n    .column_status_alert {\n      padding-bottom: 6px !important;\n\n      svg {\n        width: 22px !important;\n        height: 22px !important;\n      }\n    }\n\n    .euiButton.btn-back {\n      float: left;\n    }\n  }\n}\n\n.footer {\n  padding: 15px 14px 10px 30px !important;\n}\n\n.title {\n  padding-bottom: 5px !important;\n}\n\n.hideTableMessage {\n  tbody tr {\n    display: none;\n  }\n}\n\n.searchForm {\n  width: 266px !important;\n}\n\n.account {\n  width: 100%;\n  min-height: 44px;\n  padding-left: 14px;\n  padding-top: 9px;\n  padding-bottom: 9px;\n  border: 1px solid var(--euiColorLightShade);\n  background-color: var(--euiColorLightestShade) !important;\n}\n\n.account_item {\n  display: inline-block;\n  padding-right: 35px;\n  padding-top: 5px;\n  padding-bottom: 5px;\n  font-size: 13px;\n  color: var(--euiTextColor);\n\n  :global(.euiLoadingContent) {\n    display: inline-block;\n    width: 100px;\n    position: relative;\n  }\n\n  :global(.euiLoadingContent__singleLine) {\n    margin-bottom: 0;\n    display: inline-block;\n    height: 12px;\n    vertical-align: middle;\n  }\n}\n\n.account_item_title {\n  color: var(--euiTextSubduedColor);\n}\n\n.panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n\n.tooltipColumnName {\n  max-width: 370px !important;\n\n  * {\n    line-height: 1.19;\n    font-size: 14px !important;\n\n    &:not(:global(.euiToolTip__title)) {\n      font-weight: 300 !important;\n    }\n  }\n}\n\n.errorBtn {\n  height: 100% !important;\n  > span {\n    padding: 0 !important;\n  }\n}\n\n.tooltipStatus {\n  width: 375px !important;\n  max-width: 375px !important;\n\n  padding-left: 15px !important;\n  padding-top: 15px !important;\n  font-size: 14px !important;\n\n  :global(.euiToolTip__title) {\n    padding: 5px 10px 0 !important;\n  }\n}\n\n.tooltipStatusList {\n  font-weight: 300 !important;\n  opacity: 0.85;\n  padding-bottom: 5px;\n  padding-top: 10px;\n  font-size: 14px !important;\n\n  li {\n    line-height: 24px;\n  }\n\n  .dot {\n    margin-left: 10px;\n  }\n\n  .dot::before {\n    font-size: 8px;\n    padding: 0 14px;\n    content: \" \\25CF\";\n    vertical-align: middle;\n  }\n}\n\n.tooltipColumnNameText {\n  text-decoration: underline;\n  display: inline-block;\n  width: 100%;\n  overflow: hidden;\n  vertical-align: top;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.noSubscriptions {\n  color: var(--euiTextSubduedColor) !important;\n  margin: 50px auto 0;\n  text-align: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.test.ts",
    "content": "import { RedisCloudSubscriptionStatus } from 'uiSrc/slices/interfaces'\nimport { canSelectRow } from './canSelectRow'\nimport { RedisCloudSubscriptionFactory } from 'uiSrc/mocks/factories/cloud/RedisCloudSubscription.factory'\n\ndescribe('canSelectRow', () => {\n  it('should return true when subscription is active and has databases', () => {\n    const row = {\n      original: RedisCloudSubscriptionFactory.build({\n        status: RedisCloudSubscriptionStatus.Active,\n        numberOfDatabases: 5,\n      }),\n    }\n\n    expect(canSelectRow(row)).toBe(true)\n  })\n\n  it('should return false when subscription is not active', () => {\n    const row = {\n      original: RedisCloudSubscriptionFactory.build({\n        status: RedisCloudSubscriptionStatus.Deleting,\n        numberOfDatabases: 5,\n      }),\n    }\n\n    expect(canSelectRow(row)).toBe(false)\n  })\n\n  it('should return false when subscription has no databases', () => {\n    const row = {\n      original: RedisCloudSubscriptionFactory.build({\n        status: RedisCloudSubscriptionStatus.Active,\n        numberOfDatabases: 0,\n      }),\n    } as any\n\n    expect(canSelectRow(row)).toBe(false)\n  })\n\n  it('should return false when subscription is not active and has no databases', () => {\n    const row = {\n      original: RedisCloudSubscriptionFactory.build({\n        status: RedisCloudSubscriptionStatus.Error,\n        numberOfDatabases: 0,\n      }),\n    }\n\n    expect(canSelectRow(row)).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/canSelectRow.ts",
    "content": "import {\n  RedisCloudSubscription,\n  RedisCloudSubscriptionStatus,\n} from 'uiSrc/slices/interfaces'\n\nexport function canSelectRow({\n  original,\n}: {\n  original: RedisCloudSubscription\n}): boolean {\n  return (\n    original.status === RedisCloudSubscriptionStatus.Active &&\n    original.numberOfDatabases !== 0\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.spec.ts",
    "content": "import { RedisCloudSubscription } from 'uiSrc/slices/interfaces'\nimport { colFactory } from './colFactory'\n\ndescribe('colFactory', () => {\n  it('should return columns without selection column when items array is empty', () => {\n    const items: RedisCloudSubscription[] = []\n    const columns = colFactory(items)\n\n    expect(columns).toHaveLength(7)\n    expect(columns[0].id).toBe('id')\n    expect(columns[1].id).toBe('name')\n  })\n\n  it('should return columns with selection column when items array has data', () => {\n    const items: RedisCloudSubscription[] = [\n      {\n        id: 1,\n        name: 'test-subscription',\n      } as any,\n    ]\n    const columns = colFactory(items)\n\n    expect(columns).toHaveLength(9)\n    expect(columns[0].id).toBe('row-selection')\n    expect(columns[1].id).toBe('alert')\n    expect(columns[2].id).toBe('id')\n  })\n\n  it('should include all required column definitions in correct order', () => {\n    const items: RedisCloudSubscription[] = [{ id: 1, name: 'test-sub' } as any]\n    const columns = colFactory(items)\n\n    const columnIds = columns.map((col) => col.id)\n\n    expect(columnIds).toEqual([\n      'row-selection',\n      'alert',\n      'id',\n      'name',\n      'type',\n      'provider',\n      'region',\n      'numberOfDatabases',\n      'status',\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/utils/colFactory.ts",
    "content": "import { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type RedisCloudSubscription } from 'uiSrc/slices/interfaces'\n\nimport {\n  alertColumn,\n  idColumn,\n  numberOfDbsColumn,\n  providerColumn,\n  regionColumn,\n  selectionColumn,\n  statusColumn,\n  subscriptionColumn,\n  typeColumn,\n} from 'uiSrc/pages/autodiscover-cloud/column-definitions'\n\nexport const colFactory = (\n  items: RedisCloudSubscription[],\n): ColumnDef<RedisCloudSubscription>[] => {\n  const cols: ColumnDef<RedisCloudSubscription>[] = [\n    idColumn(),\n    subscriptionColumn(),\n    typeColumn(),\n    providerColumn(),\n    regionColumn(),\n    numberOfDbsColumn(),\n    statusColumn(),\n  ]\n  if (items.length > 0) {\n    cols.unshift(alertColumn())\n    cols.unshift(selectionColumn())\n  }\n  return cols\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-cloud/utils.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef, Table } from 'uiSrc/components/base/layout/table'\n\ntype Props = {\n  size?: number\n  id?: string\n}\n\n/**\n * @see [Row selection]{@link https://redislabsdev.github.io/redis-ui/?path=/docs/table-table-rowselection--docs#usage}\n */\nexport const getSelectionColumn = <T extends object>({\n  size = 50,\n  id = 'row-selection',\n}: Props = {}): ColumnDef<T> => {\n  return {\n    id,\n    maxSize: size,\n    size,\n    isHeaderCustom: true,\n    header: ({ table }) => (\n      <Table.HeaderMultiRowSelectionButton table={table} data-testid={id} />\n    ),\n    cell: ({ row }) => (\n      <Table.RowSelectionButton row={row} data-testid={`${id}-${row.id}`} />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/constants/constants.ts",
    "content": "export enum SentinelDatabaseTitles {\n  Address = 'Address',\n  Alias = 'Database alias*',\n  Username = 'Username',\n  DatabaseIndex = 'Database index',\n  NumberOfReplicas = '# of replicas',\n  Password = 'Password',\n  PrimaryGroup = 'Primary group',\n  Result = 'Result',\n}\n\nexport enum SentinelDatabaseIds {\n  Message = 'message',\n  Address = 'host',\n  Alias = 'alias',\n  Username = 'username',\n  DatabaseIndex = 'db',\n  NumberOfReplicas = 'numberOfSlaves',\n  Password = 'password',\n  PrimaryGroup = 'name',\n  Result = 'result',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/index.ts",
    "content": "export * from './sentinel'\nexport * from './sentinel-databases'\nexport * from './sentinel-databases-result'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel/SentinelPage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport SentinelPage, { Props } from './SentinelPage'\n\nconst mockedProps = mock<Props>()\n\nconst mockedRoutes = [\n  {\n    path: '/sentinel',\n  },\n]\n\n/**\n * SentinelPage tests\n *\n * @group component\n */\ndescribe('SentinelPage', () => {\n  it('should render', () => {\n    expect(\n      render(<SentinelPage {...instance(mockedProps)} />, {\n        withRouter: true,\n      }),\n    ).toBeTruthy()\n\n    expect(\n      render(<SentinelPage routes={mockedRoutes} />, { withRouter: true }),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel/SentinelPage.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\n\nexport interface Props {\n  routes: any[]\n}\nconst SentinelPage = ({ routes = [] }: Props) => (\n  <Switch>\n    {routes.map((route, i) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <RouteWithSubRoutes key={i} {...route} />\n    ))}\n  </Switch>\n)\n\nexport default SentinelPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel/index.ts",
    "content": "import SentinelPage from './SentinelPage'\n\nexport { SentinelPage }\n\nexport default SentinelPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/SentinelDatabasesPage.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport SentinelDatabasesPage from './SentinelDatabasesPage'\n\njest.mock('uiSrc/slices/instances/sentinel', () => ({\n  sentinelSelector: jest.fn().mockReturnValue({\n    data: [\n      {\n        status: 'success',\n        name: 'mymaster',\n        host: 'localhost',\n        port: 6379,\n        alias: 'alias',\n        numberOfSlaves: 0,\n      },\n    ],\n  }),\n  createMastersSentinelAction: () => jest.fn(),\n  resetLoadedSentinel: () => jest.fn,\n  updateMastersSentinel: () => jest.fn(),\n  resetDataSentinel: jest.fn,\n}))\n\n/**\n * SentinelDatabasesPage tests\n *\n * @group component\n */\ndescribe('SentinelDatabasesPage', () => {\n  it('should render', () => {\n    expect(render(<SentinelDatabasesPage />)).toBeTruthy()\n  })\n\n  it('should call onClose', async () => {\n    const component = render(<SentinelDatabasesPage />)\n    fireEvent.click(screen.getByTestId('btn-cancel'))\n    fireEvent.click(screen.getByTestId('btn-cancel-proceed'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onBack', () => {\n    const component = render(<SentinelDatabasesPage />)\n    fireEvent.click(screen.getByTestId('btn-back-adding'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onSubmit', () => {\n    const component = render(<SentinelDatabasesPage />)\n    fireEvent.click(screen.getByTestId('btn-add-primary-group'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should be ability to change database alias', () => {\n    const component = render(<SentinelDatabasesPage />)\n    fireEvent.change(screen.getByPlaceholderText(/Enter Database Alias/i))\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/SentinelDatabasesPage.tsx",
    "content": "import React from 'react'\n\nimport { useSentinelDatabasesConfig } from './useSentinelDatabasesConfig'\nimport SentinelDatabases from './components/SentinelDatabases/SentinelDatabases'\n\nconst SentinelDatabasesPage = () => {\n  const {\n    columns,\n    selection,\n    items,\n    handleClose,\n    handleBackAdding,\n    handleAddInstances,\n    handleSelectionChange,\n  } = useSentinelDatabasesConfig()\n\n  return (\n    <SentinelDatabases\n      columns={columns}\n      selection={selection}\n      masters={items}\n      onClose={handleClose}\n      onBack={handleBackAdding}\n      onSubmit={handleAddInstances}\n      onSelectionChange={handleSelectionChange}\n    />\n  )\n}\n\nexport default SentinelDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport SentinelDatabases, { Props } from './SentinelDatabases'\n\nconst mockedProps = mock<Props>()\n\nlet mastersMock: ModifiedSentinelMaster[]\nlet columnsMock: ColumnDef<ModifiedSentinelMaster>[]\n\nbeforeEach(() => {\n  cleanup()\n\n  columnsMock = [\n    {\n      header: 'Master group',\n      id: 'name',\n      accessorKey: 'name',\n      enableSorting: true,\n    },\n  ]\n\n  mastersMock = [\n    {\n      name: 'mymaster',\n      host: 'localhost',\n      port: '6379',\n      alias: 'alias',\n      numberOfSlaves: 0,\n    },\n  ]\n})\n\ndescribe('SentinelDatabases', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <SentinelDatabases\n          {...instance(mockedProps)}\n          selection={[]}\n          columns={columnsMock}\n          masters={mastersMock}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render \"empty\" text if noone master was fetched', () => {\n    render(\n      <SentinelDatabases\n        {...instance(mockedProps)}\n        selection={[]}\n        columns={columnsMock}\n        masters={[]}\n      />,\n    )\n    expect(\n      screen.getAllByText(\n        'Your Redis Sentinel has no primary groups available.',\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render search input', () => {\n    render(\n      <SentinelDatabases\n        {...instance(mockedProps)}\n        selection={[]}\n        columns={columnsMock}\n        masters={mastersMock}\n      />,\n    )\n    expect(screen.getByTestId(/search/i)).toBeTruthy()\n  })\n\n  it('should call search and result exist', () => {\n    render(\n      <SentinelDatabases\n        {...instance(mockedProps)}\n        selection={[]}\n        columns={columnsMock}\n        masters={mastersMock}\n      />,\n    )\n    const searchInput = screen.getByTestId(/search/i)\n\n    const searchQuery = 'mymaster'\n    fireEvent.change(searchInput, { target: { value: searchQuery } })\n    expect(searchInput).toHaveValue(searchQuery)\n    const searchResultRow1 = screen.getByText(searchQuery)\n    expect(searchResultRow1).toBeTruthy()\n  })\n\n  it(\"should call search and result doesn't exist\", () => {\n    render(\n      <SentinelDatabases\n        {...instance(mockedProps)}\n        selection={[]}\n        columns={columnsMock}\n        masters={mastersMock}\n      />,\n    )\n    const searchQuery = 'mymaster2'\n    const searchInput = screen.getByTestId(/search/i)\n\n    fireEvent.change(searchInput, { target: { value: searchQuery } })\n    expect(searchInput).toHaveValue(searchQuery)\n\n    expect(screen.getByText('Not found.')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { expect, fn, screen } from 'storybook/test'\n\nimport { SentinelMasterFactory } from 'uiSrc/mocks/factories/sentinel/SentinelMaster.factory'\nimport SentinelDatabases from './SentinelDatabases'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport { RowSelectionState } from '@redis-ui/table'\nimport { colFactory, getRowId } from '../../useSentinelDatabasesConfig'\n\nconst emptyColumnsMock = colFactory([], () => {})\n\nconst meta: Meta<typeof SentinelDatabases> = {\n  component: SentinelDatabases,\n  args: {\n    selection: [],\n    columns: emptyColumnsMock,\n    masters: [],\n    onBack: fn(),\n    onClose: fn(),\n    onSubmit: fn(),\n    onSelectionChange: fn(),\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof SentinelDatabases>\n\nconst DefaultRender = () => {\n  const mastersMock: ModifiedSentinelMaster[] = [\n    SentinelMasterFactory.build({\n      id: '1',\n      name: 'mymaster',\n      alias: 'mymaster',\n    }),\n    SentinelMasterFactory.build({\n      name: 'mymaster2',\n      alias: 'mymaster2',\n    }),\n    SentinelMasterFactory.build({\n      name: 'mymaster3',\n      alias: 'mymaster3',\n    }),\n  ]\n  let columnsMock = colFactory(mastersMock, () => {})\n  const [rowSelection, setSelection] = useState<RowSelectionState>({})\n  const selection = Object.keys(rowSelection)\n    .map((key) => mastersMock.find((master) => getRowId(master) === key))\n    .filter((item): item is ModifiedSentinelMaster => Boolean(item))\n\n  return (\n    <SentinelDatabases\n      selection={selection || []}\n      columns={columnsMock}\n      masters={mastersMock}\n      onClose={meta.args?.onClose!}\n      onBack={meta.args?.onBack!}\n      onSubmit={meta.args?.onSubmit!}\n      onSelectionChange={(sel) => {\n        setSelection(sel)\n      }}\n    />\n  )\n}\n\nexport const Default: Story = {\n  render: () => <DefaultRender />,\n  play: async ({ canvas, userEvent, args, step }) => {\n    await step('Ensure render', async () => {\n      await expect(canvas.getByTestId('row-selection')).toBeInTheDocument()\n      await expect(canvas.getByText('mymaster')).toBeInTheDocument()\n      await expect(canvas.getByText('mymaster2')).toBeInTheDocument()\n      await expect(\n        canvas.getByText('Auto-Discover Redis Sentinel Primary Groups'),\n      ).toBeInTheDocument()\n      await expect(canvas.getByText('Add databases')).toBeInTheDocument()\n    })\n    // you can group interactions with `step`\n    await step('Ensure cancel is called', async () => {\n      await userEvent.click(canvas.getByRole('button', { name: 'Cancel' }))\n      await userEvent.click(screen.getByRole('button', { name: 'Proceed' }))\n      await expect(args.onClose).toHaveBeenCalled()\n    })\n    // or you can just call actions sequentially\n    // await userEvent.click(canvas.getByRole('button', { name: 'Cancel' }))\n    // await userEvent.click(screen.getByRole('button', { name: 'Proceed' }))\n    // await expect(args.onClose).toHaveBeenCalled()\n    await step('Back to add databases screen is called', async () => {\n      await userEvent.click(\n        canvas.getByRole('button', { name: 'Add databases' }),\n      )\n      await expect(args.onBack).toHaveBeenCalled()\n    })\n    await step('Primary group selection', async () => {\n      await userEvent.click(canvas.getByTestId('row-selection-1'))\n      await userEvent.click(\n        // name can be exact matching string or regex\n        canvas.getByRole('button', { name: /add primary group/i }),\n      )\n      await expect(args.onSubmit).toHaveBeenCalled()\n    })\n  },\n}\n\nexport const Empty: Story = {\n  play: async ({ canvas }) => {\n    await expect(\n      canvas.getByText('Your Redis Sentinel has no primary groups available'),\n    ).toBeInTheDocument()\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/SentinelDatabases.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { sentinelSelector } from 'uiSrc/slices/instances/sentinel'\nimport { type ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  type ColumnDef,\n  type RowSelectionState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { getRowId } from '../../useSentinelDatabasesConfig'\nimport { CancelButton, SubmitButton, NoMastersMessage } from './components'\n\nexport interface Props {\n  columns: ColumnDef<ModifiedSentinelMaster>[]\n  masters: ModifiedSentinelMaster[]\n  selection: ModifiedSentinelMaster[]\n  onSelectionChange: (state: RowSelectionState) => void\n  onClose: () => void\n  onBack: () => void\n  onSubmit: (databases: ModifiedSentinelMaster[]) => void\n}\n\nconst loadingMsg = 'loading...'\nconst notMastersMsg = 'Your Redis Sentinel has no primary groups available.'\nconst notFoundMsg = 'Not found.'\n\nconst SentinelDatabases = ({\n  columns,\n  onSelectionChange,\n  onClose,\n  onBack,\n  onSubmit,\n  masters,\n  selection,\n}: Props) => {\n  const [items, setItems] = useState<ModifiedSentinelMaster[]>(masters)\n\n  const [message, setMessage] = useState(loadingMsg)\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  const { loading } = useSelector(sentinelSelector)\n\n  const handleSubmit = () => {\n    onSubmit(selection)\n  }\n\n  const showPopover = () => {\n    setIsPopoverOpen(true)\n  }\n\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const isSubmitDisabled = () => {\n    const selected = selection.length < 1\n    const emptyAliases = selection.filter(({ alias }) => !alias)\n    return selected || emptyAliases.length !== 0\n  }\n\n  useEffect(() => {\n    if (masters.length) {\n      setItems(masters)\n    }\n\n    if (!masters.length) {\n      setMessage(notMastersMsg)\n    }\n  }, [masters.length])\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n\n    const itemsTemp = masters.filter(\n      (item: ModifiedSentinelMaster) =>\n        item.name?.toLowerCase().includes(value) ||\n        (item.host?.toLowerCase() || '').includes(value) ||\n        item.alias?.toLowerCase().includes(value) ||\n        (item.username?.toLowerCase() || '').includes(value) ||\n        item.port?.toString()?.includes(value) ||\n        item.numberOfSlaves?.toString().includes(value),\n    )\n\n    if (!itemsTemp.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title=\"Auto-Discover Redis Sentinel Primary Groups\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n          subTitle={\n            masters.length > 0 && (\n              <Text size=\"m\">\n                Redis Sentinel instance found. Here is a list of primary groups\n                your Sentinel instance is managing. <br />\n                Select the primary group(s) you want to add:\n              </Text>\n            )\n          }\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            rowSelectionMode=\"multiple\"\n            // rowSelection={rowSelection}\n            onRowSelectionChange={onSelectionChange}\n            getRowCanSelect={(row) => getRowId(row.original) !== ''}\n            getRowId={getRowId}\n            columns={columns}\n            data={items}\n            defaultSorting={[\n              {\n                id: 'name',\n                desc: false,\n              },\n            ]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() => <EmptyState message={message} />}\n          />\n          {!masters.length && <NoMastersMessage message={notMastersMsg} />}\n        </DatabaseWrapper>\n      </DatabaseContainer>\n      <Footer>\n        <Row justify=\"end\">\n          <Row gap=\"m\" grow={false}>\n            <CancelButton\n              isPopoverOpen={isPopoverOpen}\n              onClose={onClose}\n              onShowPopover={showPopover}\n              onClosePopover={closePopover}\n            />\n            <SubmitButton\n              selection={selection}\n              loading={loading}\n              onClick={handleSubmit}\n              isDisabled={isSubmitDisabled()}\n            />\n          </Row>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default SentinelDatabases\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.tsx",
    "content": "import React from 'react'\nimport {\n  SecondaryButton,\n  DestructiveButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\n\nimport { type CancelButtonProps } from './CancelButton.types'\nimport styles from './styles.module.scss'\n\nexport const CancelButton = ({\n  isPopoverOpen,\n  onClose,\n  onShowPopover,\n  onClosePopover,\n}: CancelButtonProps) => (\n  <RiPopover\n    anchorPosition=\"upCenter\"\n    isOpen={isPopoverOpen}\n    closePopover={onClosePopover}\n    panelClassName={styles.panelCancelBtn}\n    panelPaddingSize=\"l\"\n    button={\n      <SecondaryButton\n        onClick={onShowPopover}\n        className=\"btn-cancel\"\n        data-testid=\"btn-cancel\"\n      >\n        Cancel\n      </SecondaryButton>\n    }\n  >\n    <Text size=\"S\">\n      Your changes have not been saved.&#10;&#13; Do you want to proceed to the\n      list of databases?\n    </Text>\n    <br />\n    <div>\n      <DestructiveButton\n        size=\"s\"\n        onClick={onClose}\n        data-testid=\"btn-cancel-proceed\"\n      >\n        Proceed\n      </DestructiveButton>\n    </div>\n  </RiPopover>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/CancelButton.types.ts",
    "content": "export interface CancelButtonProps {\n  isPopoverOpen: boolean\n  onClose: () => void\n  onShowPopover: () => void\n  onClosePopover: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/CancelButton/styles.module.scss",
    "content": ".panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.tsx",
    "content": "import React from 'react'\n\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { type NoMastersMessageProps } from './NoMastersMessage.types'\n\nexport const NoMastersMessage = ({ message }: NoMastersMessageProps) => (\n  <Col centered full>\n    <Text size=\"L\">{message}</Text>\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/NoMastersMessage/NoMastersMessage.types.ts",
    "content": "export interface NoMastersMessageProps {\n  message: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.tsx",
    "content": "import React from 'react'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport validationErrors from 'uiSrc/constants/validationErrors'\n\nimport { type SubmitButtonProps } from './SubmitButton.types'\n\nconst TooltipIcon = ({\n  title,\n  content,\n}: {\n  title: string | null\n  content: string | null\n}) => (\n  <RiTooltip position=\"top\" title={title} content={<span>{content}</span>}>\n    <RiIcon type=\"InfoIcon\" />\n  </RiTooltip>\n)\n\nexport const SubmitButton = ({\n  selection,\n  loading,\n  onClick,\n  isDisabled,\n}: SubmitButtonProps) => {\n  let title: string | null = null\n  let content: string | null = null\n  const emptyAliases = selection.filter(({ alias }) => !alias)\n\n  if (selection.length < 1) {\n    title = validationErrors.SELECT_AT_LEAST_ONE('primary group')\n    content = validationErrors.NO_PRIMARY_GROUPS_SENTINEL\n  }\n\n  if (emptyAliases.length !== 0) {\n    title = validationErrors.REQUIRED_TITLE(emptyAliases.length)\n    content = 'Database Alias'\n  }\n\n  return (\n    <PrimaryButton\n      type=\"submit\"\n      onClick={onClick}\n      disabled={isDisabled}\n      loading={loading}\n      icon={\n        isDisabled\n          ? () => <TooltipIcon title={title} content={content} />\n          : undefined\n      }\n      data-testid=\"btn-add-primary-group\"\n    >\n      Add Primary Group\n    </PrimaryButton>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/SubmitButton/SubmitButton.types.ts",
    "content": "import { type ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\n\nexport interface SubmitButtonProps {\n  selection: ModifiedSentinelMaster[]\n  loading: boolean\n  onClick: () => void\n  isDisabled: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/SentinelDatabases/components/index.ts",
    "content": "export { CancelButton } from './CancelButton/CancelButton'\nexport { SubmitButton } from './SubmitButton/SubmitButton'\nexport { NoMastersMessage } from './NoMastersMessage/NoMastersMessage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/address.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nimport { AddressCell } from '../components'\n\nexport const addressColumn = (): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Address,\n    id: SentinelDatabaseIds.Address,\n    accessorKey: SentinelDatabaseIds.Address,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { host, port },\n      },\n    }) => <AddressCell host={host} port={port} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/alias.tsx",
    "content": "import React from 'react'\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\n\nimport { AliasCell } from '../components'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nexport const aliasColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Alias,\n    id: SentinelDatabaseIds.Alias,\n    accessorKey: SentinelDatabaseIds.Alias,\n    enableSorting: true,\n    size: 200,\n    cell: ({\n      row: {\n        original: { id, alias, name },\n      },\n    }) => (\n      <AliasCell\n        id={id}\n        alias={alias}\n        name={name}\n        handleChangedInput={handleChangedInput}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/dbIndex.tsx",
    "content": "import React from 'react'\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\n\nimport { DbIndexCell } from '../components'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nexport const dbIndexColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.DatabaseIndex,\n    id: SentinelDatabaseIds.DatabaseIndex,\n    accessorKey: SentinelDatabaseIds.DatabaseIndex,\n    size: 140,\n    cell: ({\n      row: {\n        original: { db = 0, id },\n      },\n    }) => (\n      <DbIndexCell db={db} id={id!} handleChangedInput={handleChangedInput} />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/numberOfReplicas.ts",
    "content": "import type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\n\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nexport const numberOfReplicasColumn = (): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.NumberOfReplicas,\n    id: SentinelDatabaseIds.NumberOfReplicas,\n    accessorKey: SentinelDatabaseIds.NumberOfReplicas,\n    enableSorting: true,\n    size: 120,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/password.tsx",
    "content": "import React from 'react'\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\n\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nimport { PasswordCell } from '../components'\n\nexport const passwordColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Password,\n    id: SentinelDatabaseIds.Password,\n    accessorKey: SentinelDatabaseIds.Password,\n    cell: ({\n      row: {\n        original: { password, id },\n      },\n    }) => (\n      <PasswordCell\n        password={password}\n        id={id!}\n        handleChangedInput={handleChangedInput}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/primaryGroup.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\n\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { PrimaryGroupCell } from '../components'\n\nexport const primaryGroupColumn = (): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.PrimaryGroup,\n    id: SentinelDatabaseIds.PrimaryGroup,\n    accessorKey: SentinelDatabaseIds.PrimaryGroup,\n    enableSorting: true,\n    size: 200,\n    cell: ({\n      row: {\n        original: { name },\n      },\n    }) => <PrimaryGroupCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/selection.ts",
    "content": "import type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils'\n\nexport const selectionColumn = () => {\n  return getSelectionColumn<ModifiedSentinelMaster>()\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/columns/username.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nimport { UsernameCell } from '../components'\n\nexport const usernameColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Username,\n    id: SentinelDatabaseIds.Username,\n    accessorKey: SentinelDatabaseIds.Username,\n    cell: ({\n      row: {\n        original: { username, id },\n      },\n    }) => (\n      <UsernameCell\n        username={username!}\n        id={id!}\n        handleChangedInput={handleChangedInput}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.tsx",
    "content": "import React from 'react'\nimport {\n  CopyTextContainer,\n  CopyPublicEndpointText,\n  CopyBtnWrapper,\n} from 'uiSrc/components/auto-discover'\n\nimport type { AddressCellProps } from './AddressCell.types'\n\nexport const AddressCell = ({ host, port }: AddressCellProps) => {\n  if (!host || !port) {\n    return null\n  }\n\n  const text = `${host}:${port}`\n  return (\n    <CopyTextContainer>\n      <CopyPublicEndpointText>{text}</CopyPublicEndpointText>\n      <CopyBtnWrapper\n        copy={text}\n        aria-label=\"Copy public endpoint\"\n        successLabel=\"\"\n      />\n    </CopyTextContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AddressCell/AddressCell.types.ts",
    "content": "export interface AddressCellProps {\n  host?: string\n  port?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\n\nimport type { AliasCellProps } from './AliasCell.types'\n\nexport const AliasCell = ({\n  id,\n  alias,\n  name,\n  handleChangedInput,\n}: AliasCellProps) => (\n  <div role=\"presentation\">\n    <InputFieldSentinel\n      name={`alias-${id}`}\n      value={alias || name}\n      placeholder=\"Enter Database Alias\"\n      inputType={SentinelInputFieldType.Text}\n      onChangedInput={handleChangedInput}\n      maxLength={500}\n    />\n  </div>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/AliasCell/AliasCell.types.ts",
    "content": "export interface AliasCellProps {\n  id?: string\n  alias?: string\n  name?: string\n  handleChangedInput: (name: string, value: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel, RiTooltip } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nimport type { DbIndexCellProps } from './DbIndexCell.types'\n\nexport const DbIndexCell = ({\n  db = 0,\n  id,\n  handleChangedInput,\n}: DbIndexCellProps) => (\n  <div role=\"presentation\">\n    <InputFieldSentinel\n      min={0}\n      value={`${db}` || '0'}\n      name={`db-${id}`}\n      placeholder=\"Enter Index\"\n      inputType={SentinelInputFieldType.Number}\n      onChangedInput={handleChangedInput}\n      append={\n        <RiTooltip\n          anchorClassName=\"inputAppendIcon\"\n          position=\"left\"\n          content=\"Select the Redis logical database to work with in Browser and Workbench.\"\n        >\n          <RiIcon type=\"InfoIcon\" style={{ cursor: 'pointer' }} />\n        </RiTooltip>\n      }\n    />\n  </div>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/DbIndexCell/DbIndexCell.types.ts",
    "content": "export interface DbIndexCellProps {\n  db: number\n  id: string\n  handleChangedInput: (name: string, value: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\n\nimport type { PasswordCellProps } from './PasswordCell.types'\n\nexport const PasswordCell = ({\n  password,\n  id,\n  handleChangedInput,\n}: PasswordCellProps) => (\n  <div role=\"presentation\">\n    <InputFieldSentinel\n      value={password}\n      name={`password-${id}`}\n      placeholder=\"Enter Password\"\n      inputType={SentinelInputFieldType.Password}\n      onChangedInput={handleChangedInput}\n    />\n  </div>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PasswordCell/PasswordCell.types.ts",
    "content": "export interface PasswordCellProps {\n  password?: string\n  id: string\n  handleChangedInput: (name: string, value: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx",
    "content": "import React from 'react'\nimport { CellText } from 'uiSrc/components/auto-discover'\n\nimport type { PrimaryGroupCellProps } from './PrimaryGroupCell.types'\n\nexport const PrimaryGroupCell = ({ name }: PrimaryGroupCellProps) => (\n  <CellText data-testid={`primary-group_${name}`}>{name}</CellText>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts",
    "content": "export interface PrimaryGroupCellProps {\n  name: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\n\nimport type { UsernameCellProps } from './UsernameCell.types'\n\nexport const UsernameCell = ({\n  username,\n  id,\n  handleChangedInput,\n}: UsernameCellProps) => (\n  <div role=\"presentation\">\n    <InputFieldSentinel\n      value={username}\n      name={`username-${id}`}\n      placeholder=\"Enter Username\"\n      inputType={SentinelInputFieldType.Text}\n      onChangedInput={handleChangedInput}\n    />\n  </div>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/UsernameCell/UsernameCell.types.ts",
    "content": "export interface UsernameCellProps {\n  username: string\n  id: string\n  handleChangedInput: (name: string, value: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/components/index.ts",
    "content": "export { AddressCell } from './AddressCell/AddressCell'\nexport { AliasCell } from './AliasCell/AliasCell'\nexport { DbIndexCell } from './DbIndexCell/DbIndexCell'\nexport { PasswordCell } from './PasswordCell/PasswordCell'\nexport { PrimaryGroupCell } from './PrimaryGroupCell/PrimaryGroupCell'\nexport { UsernameCell } from './UsernameCell/UsernameCell'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/column-definitions/index.ts",
    "content": "export * from './columns/primaryGroup'\nexport * from './columns/alias'\nexport * from './columns/address'\nexport * from './columns/numberOfReplicas'\nexport * from './columns/username'\nexport * from './columns/password'\nexport * from './columns/dbIndex'\nexport * from './columns/selection'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/components/index.ts",
    "content": "import SentinelDatabases from './SentinelDatabases'\n\nexport { SentinelDatabases }\n\nexport default SentinelDatabases\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/index.ts",
    "content": "import SentinelDatabasesPage from './SentinelDatabasesPage'\n\nexport { SentinelDatabasesPage }\n\nexport default SentinelDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases/useSentinelDatabasesConfig.tsx",
    "content": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { map, pick } from 'lodash'\nimport { useHistory } from 'react-router-dom'\n\nimport { LoadedSentinel, ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  createMastersSentinelAction,\n  resetDataSentinel,\n  resetLoadedSentinel,\n  sentinelSelector,\n  updateMastersSentinel,\n} from 'uiSrc/slices/instances/sentinel'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Pages } from 'uiSrc/constants'\nimport { setTitle } from 'uiSrc/utils'\nimport { CreateSentinelDatabaseDto } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.dto'\nimport {\n  ColumnDef,\n  RowSelectionState,\n} from 'uiSrc/components/base/layout/table'\nimport {\n  primaryGroupColumn,\n  aliasColumn,\n  addressColumn,\n  numberOfReplicasColumn,\n  usernameColumn,\n  passwordColumn,\n  dbIndexColumn,\n  selectionColumn,\n} from './components/column-definitions'\n\nconst updateSelection = (\n  selected: ModifiedSentinelMaster[],\n  masters: ModifiedSentinelMaster[],\n) => {\n  return selected.map(\n    (select) =>\n      masters.find((master) => getRowId(master) === getRowId(select)) ?? select,\n  )\n}\n\nconst sendCancelEvent = () => {\n  sendEventTelemetry({\n    event:\n      TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_CANCELLED,\n  })\n}\nexport const colFactory = (\n  items: ModifiedSentinelMaster[],\n  handleChangedInput: (name: string, value: string) => void,\n) => {\n  const cols: ColumnDef<ModifiedSentinelMaster>[] = [\n    primaryGroupColumn(),\n    aliasColumn(handleChangedInput),\n    addressColumn(),\n    numberOfReplicasColumn(),\n    usernameColumn(handleChangedInput),\n    passwordColumn(handleChangedInput),\n    dbIndexColumn(handleChangedInput),\n  ]\n  if (items.length > 0) {\n    cols.unshift(selectionColumn())\n  }\n  return cols\n}\n\nexport const getRowId = (row: ModifiedSentinelMaster) => row.id || ''\n\nexport const useSentinelDatabasesConfig = () => {\n  const [items, setItems] = useState<ModifiedSentinelMaster[]>([])\n\n  const { data: masters } = useSelector(sentinelSelector)\n\n  const [selection, setSelection] = useState<ModifiedSentinelMaster[]>([])\n  const dispatch = useDispatch()\n  const history = useHistory()\n  useEffect(() => {\n    if (masters.length) {\n      setItems(masters)\n      setSelection((prevState) => updateSelection(prevState, masters))\n    }\n  }, [masters.length])\n\n  useEffect(() => setTitle('Auto-Discover Redis Sentinel Primary Groups'), [])\n  const handleClose = useCallback(() => {\n    sendCancelEvent()\n    dispatch(resetDataSentinel())\n    history.push(Pages.home)\n  }, [dispatch, history])\n\n  const handleBackAdding = useCallback(() => {\n    sendCancelEvent()\n    dispatch(resetLoadedSentinel(LoadedSentinel.Masters))\n    history.push(Pages.home)\n  }, [dispatch, history])\n\n  const handleSelectionChange = useCallback(\n    (currentSelected: RowSelectionState) => {\n      const newSelection = items.filter((item) => {\n        const id = getRowId(item)\n        if (!id) {\n          return false\n        }\n        return currentSelected[id]\n      })\n      setSelection(newSelection)\n    },\n    [items],\n  )\n\n  const handleAddInstances = useCallback(\n    (databases: ModifiedSentinelMaster[]) => {\n      const pikedDatabases = map(databases, (i) => {\n        const database: CreateSentinelDatabaseDto = {\n          name: i.name,\n          alias: i.alias || i.name,\n        }\n        if (i.username) {\n          database.username = i.username\n        }\n        if (i.password) {\n          database.password = i.password\n        }\n        if (i.db) {\n          database.db = i.db\n        }\n        return pick(database, 'alias', 'name', 'username', 'password', 'db')\n      })\n\n      dispatch(updateMastersSentinel(databases))\n      dispatch(\n        createMastersSentinelAction(pikedDatabases, () =>\n          history.push(Pages.sentinelDatabasesResult),\n        ),\n      )\n    },\n    [dispatch, history],\n  )\n\n  const handleChangedInput = useCallback(\n    (name: string, value: string) => {\n      const [field, id] = name.split('-')\n\n      setItems((items) =>\n        items.map((item) => {\n          const itemId = getRowId(item)\n          if (itemId !== id) {\n            return item\n          }\n\n          return { ...item, [field]: value }\n        }),\n      )\n    },\n    [setItems],\n  )\n\n  const columns: ColumnDef<ModifiedSentinelMaster>[] = useMemo(\n    () => colFactory(items, handleChangedInput),\n    [handleChangedInput, items.length],\n  )\n\n  return {\n    columns,\n    selection,\n    items,\n    handleClose,\n    handleBackAdding,\n    handleAddInstances,\n    handleSelectionChange,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/SentinelDatabasesResultPage.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport SentinelDatabasesResultPage from './SentinelDatabasesResultPage'\n\njest.mock('uiSrc/slices/instances/sentinel', () => ({\n  sentinelSelector: jest.fn().mockReturnValue({\n    data: [\n      {\n        status: 'success',\n        name: 'mymaster',\n        host: 'localhost',\n        port: 6379,\n        alias: 'alias',\n        numberOfSlaves: 0,\n      },\n    ],\n  }),\n  createMastersSentinelAction: () => jest.fn(),\n  resetLoadedSentinel: () => jest.fn,\n  updateMastersSentinel: () => jest.fn(),\n  resetDataSentinel: jest.fn,\n}))\n\n/**\n * SentinelDatabasesResultPage tests\n *\n * @group component\n */\ndescribe('SentinelDatabasesResultPage', () => {\n  it('should render', () => {\n    expect(render(<SentinelDatabasesResultPage />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/SentinelDatabasesResultPage.tsx",
    "content": "import React from 'react'\nimport SentinelDatabasesResult from './components/SentinelDatabasesResult/SentinelDatabasesResult'\nimport { useSentinelDatabasesResultConfig } from './useSentinelDatabasesResultConfig'\n\nconst SentinelDatabasesResultPage = () => {\n  const {\n    columns,\n    items,\n    countSuccessAdded,\n    handleBackAdding,\n    handleViewDatabases,\n  } = useSentinelDatabasesResultConfig()\n  return (\n    <SentinelDatabasesResult\n      columns={columns}\n      masters={items}\n      countSuccessAdded={countSuccessAdded}\n      onBack={handleBackAdding}\n      onViewDatabases={handleViewDatabases}\n    />\n  )\n}\n\nexport default SentinelDatabasesResultPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport { cleanup, render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport SentinelDatabasesResult, { Props } from './SentinelDatabasesResult'\n\nconst mockedProps = mock<Props>()\n\nlet mastersMock: ModifiedSentinelMaster[]\nlet columnsMock: ColumnDef<ModifiedSentinelMaster>[]\n\nbeforeEach(() => {\n  cleanup()\n\n  columnsMock = [\n    {\n      header: 'Master group',\n      id: 'name',\n      accessorKey: 'name',\n      enableSorting: true,\n    },\n  ]\n\n  mastersMock = [\n    {\n      name: 'mymaster',\n      host: 'localhost',\n      port: '6379',\n      alias: 'alias',\n      numberOfSlaves: 0,\n    },\n  ]\n})\n\ndescribe('SentinelDatabasesResult', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <SentinelDatabasesResult\n          {...instance(mockedProps)}\n          columns={columnsMock}\n          masters={mastersMock}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render search input', () => {\n    render(\n      <SentinelDatabasesResult\n        {...instance(mockedProps)}\n        columns={columnsMock}\n        masters={mastersMock}\n      />,\n    )\n    expect(screen.getByTestId(/search/i)).toBeTruthy()\n  })\n\n  it('should call search and result exist', () => {\n    render(\n      <SentinelDatabasesResult\n        {...instance(mockedProps)}\n        columns={columnsMock}\n        masters={mastersMock}\n      />,\n    )\n    const searchInput = screen.getByTestId(/search/i)\n\n    const searchQuery = 'mymaster'\n    fireEvent.change(searchInput, { target: { value: searchQuery } })\n    expect(searchInput).toHaveValue(searchQuery)\n    const searchResultRow1 = screen.getByText(searchQuery)\n    expect(searchResultRow1).toBeTruthy()\n  })\n\n  it(\"should call search and result doesn't exist\", () => {\n    render(\n      <SentinelDatabasesResult\n        {...instance(mockedProps)}\n        columns={columnsMock}\n        masters={mastersMock}\n      />,\n    )\n    const searchQuery = 'mymaster2'\n    const searchInput = screen.getByTestId(/search/i)\n\n    fireEvent.change(searchInput, { target: { value: searchQuery } })\n    expect(searchInput).toHaveValue(searchQuery)\n\n    expect(screen.getByText('Not found.')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { action } from 'storybook/actions'\n\nimport SentinelDatabasesResult from './SentinelDatabasesResult'\nimport {\n  AddRedisDatabaseStatus,\n  ModifiedSentinelMaster,\n} from 'uiSrc/slices/interfaces'\nimport { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { colFactory } from '../../useSentinelDatabasesResultConfig'\nimport { fn } from 'storybook/test'\nimport { faker } from '@faker-js/faker'\n\nlet mastersMock: ModifiedSentinelMaster[] = [\n  {\n    name: 'mymaster',\n    status: AddRedisDatabaseStatus.Fail,\n    message: 'Lorem ipsum dolor sit.',\n    host: '192.168.0.19',\n    port: '6379',\n    numberOfSlaves: 0,\n    // nodes: [],\n    id: '1',\n    alias: 'mymaster',\n    username: '',\n    password: '',\n    db: 1,\n    error: {\n      statusCode: 404,\n      name: 'Not Found',\n    },\n  },\n  {\n    name: 'mymaster4',\n    status: AddRedisDatabaseStatus.Fail,\n    loading: true,\n    message: 'Lorem ipsum dolor sit.',\n    host: '192.168.0.19',\n    port: '6379',\n    numberOfSlaves: 0,\n    // nodes: [],\n    id: '4',\n    alias: 'mymaster4',\n    username: '',\n    password: '',\n    db: 1,\n    error: {\n      statusCode: 400,\n      name: 'Not Found',\n    },\n  },\n  {\n    name: 'mymaster2',\n    status: AddRedisDatabaseStatus.Success,\n    message: 'Yay',\n    host: '192.168.0.18',\n    port: '6380',\n    numberOfSlaves: 0,\n    // nodes: [],\n    id: '2',\n    alias: 'mymaster2',\n    username: '',\n    password: '',\n    db: 1,\n  },\n  {\n    name: 'mymaster3',\n    status: AddRedisDatabaseStatus.Fail,\n    message: 'Lorem ipsum dolor.',\n    host: '192.168.0.18',\n    port: '6380',\n    numberOfSlaves: 0,\n    alias: 'mymaster3',\n    username: 'default',\n    password: 'abcde',\n    db: 1,\n  },\n]\n\nlet columnsMock: ColumnDef<ModifiedSentinelMaster>[] = colFactory(\n  action('onChangedInput'),\n  action('onAddInstance'),\n  false,\n  mastersMock.length - 2,\n  mastersMock.length,\n)\nconst meta: Meta<typeof SentinelDatabasesResult> = {\n  component: SentinelDatabasesResult,\n  args: {\n    columns: columnsMock,\n    countSuccessAdded: mastersMock.filter(\n      (m) => m.status === AddRedisDatabaseStatus.Success,\n    ).length,\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof SentinelDatabasesResult>\n\nconst DefaultRender = () => {\n  return (\n    <SentinelDatabasesResult\n      onViewDatabases={action('onViewDatabases')}\n      columns={columnsMock}\n      masters={mastersMock}\n      countSuccessAdded={\n        mastersMock.filter((m) => m.status === AddRedisDatabaseStatus.Success)\n          .length\n      }\n      onBack={action('onBack')}\n    />\n  )\n}\n\nexport const Default: Story = {\n  render: () => <DefaultRender />,\n}\n\nexport const WithManyRows: Story = {\n  render: () => {\n    const mastersMock: ModifiedSentinelMaster[] = Array.from(\n      { length: 100 },\n      (_, i) => {\n        const status = Object.values(AddRedisDatabaseStatus)[\n          Math.floor(\n            Math.random() * Object.values(AddRedisDatabaseStatus).length,\n          )\n        ]\n        return {\n          name: `mymaster${i}`,\n          status,\n          message: faker.lorem.sentence(),\n          host: faker.internet.ip(),\n          port: `${faker.internet.port()}`,\n          numberOfSlaves: Math.floor(Math.random() * 10) + 1,\n          id: i.toString(),\n          alias: `mymaster${i}`,\n          username: '',\n          password: '',\n          db: 1,\n          ...(status === AddRedisDatabaseStatus.Fail\n            ? {\n                error: {\n                  statusCode: 404,\n                  name: 'Not Found',\n                },\n              }\n            : {}),\n        }\n      },\n    )\n\n    const columnsMock = colFactory(\n      fn(),\n      fn(),\n      false,\n      mastersMock.filter((m) => m.status === AddRedisDatabaseStatus.Success)\n        .length,\n      mastersMock.length,\n    )\n\n    return (\n      <SentinelDatabasesResult\n        onViewDatabases={fn()}\n        onBack={fn()}\n        columns={columnsMock}\n        masters={mastersMock}\n        countSuccessAdded={\n          mastersMock.filter((m) => m.status === AddRedisDatabaseStatus.Success)\n            .length\n        }\n      />\n    )\n  },\n}\n\nexport const AllValid: Story = {\n  args: {\n    columns: colFactory(\n      action('onChangedInput'),\n      action('onAddInstance'),\n      false,\n      1,\n      1,\n    ),\n    countSuccessAdded: 1,\n    masters: [\n      mastersMock.find((m) => m.status === AddRedisDatabaseStatus.Success) ||\n        mastersMock[0],\n    ],\n    onBack: action('onBack'),\n  },\n}\n\nexport const AllInvalid: Story = {\n  args: {\n    columns: colFactory(\n      action('onChangedInput'),\n      action('onAddInstance'),\n      false,\n      0,\n      1,\n    ),\n    masters: [mastersMock[0]],\n    onBack: action('onBack'),\n    countSuccessAdded: 0,\n  },\n}\n\nexport const Empty: Story = {\n  args: {\n    columns: colFactory(\n      action('onChangedInput'),\n      action('onAddInstance'),\n      false,\n      0,\n      0,\n    ),\n    masters: [],\n    onBack: action('onBack'),\n    countSuccessAdded: 0,\n  },\n}\n\nexport const NoNotificationWhenMastersAreNotProvided: Story = {\n  args: {\n    columns: colFactory(\n      action('onChangedInput'),\n      action('onAddInstance'),\n      false,\n      0,\n      0,\n    ),\n    masters: [],\n    onBack: action('onBack'),\n    countSuccessAdded: 2,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/SentinelDatabasesResult.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { sentinelSelector } from 'uiSrc/slices/instances/sentinel'\nimport { type ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport MessageBar from 'uiSrc/components/message-bar/MessageBar'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\n\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { type ColumnDef, Table } from 'uiSrc/components/base/layout/table'\n\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n} from 'uiSrc/components/auto-discover'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Header } from 'uiSrc/components/auto-discover/Header'\nimport { SummaryText } from './components/Summary'\n\nexport interface Props {\n  countSuccessAdded: number\n  columns: ColumnDef<ModifiedSentinelMaster>[]\n  masters: ModifiedSentinelMaster[]\n  onBack: () => void\n  onViewDatabases: () => void\n}\n\nconst loadingMsg = 'loading...'\nconst notFoundMsg = 'Not found.'\n\nconst SentinelDatabasesResult = ({\n  columns,\n  onBack,\n  onViewDatabases,\n  countSuccessAdded,\n  masters,\n}: Props) => {\n  const [items, setItems] = useState<ModifiedSentinelMaster[]>(masters)\n  const [message, setMessage] = useState(loadingMsg)\n\n  const { loading } = useSelector(sentinelSelector)\n\n  const countFailAdded = masters?.length\n    ? masters.length - countSuccessAdded\n    : 0\n\n  useEffect(() => {\n    if (masters.length) {\n      setItems(masters)\n    }\n  }, [masters])\n\n  const handleViewDatabases = () => {\n    onViewDatabases()\n  }\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n\n    const itemsTemp = masters.filter(\n      (item: ModifiedSentinelMaster) =>\n        item.name?.toLowerCase()?.includes(value) ||\n        (item.host || '')?.toLowerCase()?.includes(value) ||\n        item.alias?.toLowerCase()?.includes(value) ||\n        (item.username || '')?.toLowerCase()?.includes(value) ||\n        item.port?.toString()?.includes(value) ||\n        item.numberOfSlaves?.toString().includes(value),\n    )\n\n    if (!itemsTemp.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title=\"Auto-Discover Redis Sentinel Primary Groups\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n        />\n\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          {loading ? (\n            <Col full centered>\n              <Text size=\"XL\" variant=\"semiBold\">\n                {message}\n              </Text>\n            </Col>\n          ) : (\n            <Table\n              rowSelectionMode={undefined}\n              columns={columns}\n              data={items}\n              defaultSorting={[{ id: 'message', desc: false }]}\n              emptyState={() => <EmptyState message={message} />}\n              stripedRows\n              paginationEnabled={items?.length > 10}\n            />\n          )}\n        </DatabaseWrapper>\n        {!!masters?.length && (\n          <MessageBar\n            opened={!!countSuccessAdded || !!countFailAdded}\n            variant={\n              !!countFailAdded\n                ? riToast.Variant.Attention\n                : riToast.Variant.Success\n            }\n          >\n            <SummaryText\n              countSuccessAdded={countSuccessAdded}\n              countFailAdded={countFailAdded}\n            />\n          </MessageBar>\n        )}\n      </DatabaseContainer>\n      <Footer>\n        <Row justify=\"end\">\n          <PrimaryButton\n            size=\"m\"\n            onClick={handleViewDatabases}\n            data-testid=\"btn-view-databases\"\n          >\n            View Databases\n          </PrimaryButton>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default SentinelDatabasesResult\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/components/Summary.tsx",
    "content": "import React from 'react'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { type SummaryTextProps } from './SummaryTextProps.types'\n\nexport const SummaryText = ({\n  countSuccessAdded,\n  countFailAdded,\n}: SummaryTextProps) => (\n  <Text component=\"div\" color=\"primary\" data-testid=\"summary\">\n    <ColorText variant=\"semiBold\" size=\"S\">\n      Summary:&nbsp;\n    </ColorText>\n    {countSuccessAdded ? (\n      <ColorText size=\"S\">\n        Successfully added {countSuccessAdded}\n        {' primary group(s)'}\n        {countFailAdded ? '; ' : ' '}\n      </ColorText>\n    ) : null}\n    {countFailAdded ? (\n      <ColorText size=\"S\">\n        Failed to add {countFailAdded}\n        {' primary group(s)'}\n      </ColorText>\n    ) : null}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/components/SummaryTextProps.types.ts",
    "content": "export type SummaryTextProps = {\n  countSuccessAdded: number\n  countFailAdded: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/SentinelDatabasesResult/styles.module.scss",
    "content": ".footer {\n  padding: 15px 14px 10px 30px !important;\n}\n\n.title {\n  padding-bottom: 5px !important;\n}\n\n.subTitle {\n}\n\n.searchForm {\n  width: 266px !important;\n}\n\n.panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n\n.tooltipColumnName {\n  max-width: 370px !important;\n  * {\n    line-height: 1.19;\n    font-size: 14px !important;\n\n    &:not(:global(.euiToolTip__title)) {\n      font-weight: 300 !important;\n    }\n  }\n}\n\n.errorBtn {\n  height: 100% !important;\n  > span {\n    padding: 0 !important;\n  }\n}\n\n.tooltipColumnNameText {\n  text-decoration: underline;\n  display: inline-block;\n  width: 100%;\n  overflow: hidden;\n  vertical-align: top;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/address.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { AddressCell } from '../components'\n\nexport const addressColumn = (): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Address,\n    id: SentinelDatabaseIds.Address,\n    accessorKey: SentinelDatabaseIds.Address,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { host, port },\n      },\n    }) => <AddressCell host={host} port={port} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/alias.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type {\n  ModifiedSentinelMaster,\n  AddRedisDatabaseStatus,\n} from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { AliasCell } from '../components'\n\nexport const aliasColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n  errorNotAuth: (\n    error?: string | object | null,\n    status?: AddRedisDatabaseStatus,\n  ) => boolean,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Alias,\n    id: SentinelDatabaseIds.Alias,\n    accessorKey: SentinelDatabaseIds.Alias,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { id, alias, error, loading = false, status },\n      },\n    }) => (\n      <AliasCell\n        id={id}\n        alias={alias}\n        error={error}\n        loading={loading}\n        status={status}\n        handleChangedInput={handleChangedInput}\n        errorNotAuth={errorNotAuth}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/db.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { DbCell } from '../components'\n\nexport const dbColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.DatabaseIndex,\n    id: SentinelDatabaseIds.DatabaseIndex,\n    accessorKey: SentinelDatabaseIds.DatabaseIndex,\n    size: 140,\n    cell: ({\n      row: {\n        original: { db, id, loading = false, status, error },\n      },\n    }) => (\n      <DbCell\n        db={db}\n        id={id}\n        loading={loading}\n        status={status}\n        error={error}\n        handleChangedInput={handleChangedInput}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/numberOfReplicas.ts",
    "content": "import type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\n\nexport const numberOfReplicasColumn = (): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.NumberOfReplicas,\n    id: SentinelDatabaseIds.NumberOfReplicas,\n    accessorKey: SentinelDatabaseIds.NumberOfReplicas,\n    enableSorting: true,\n    maxSize: 120,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/password.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type {\n  ModifiedSentinelMaster,\n  AddRedisDatabaseStatus,\n} from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { PasswordCell } from '../components'\n\nexport const passwordColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n  isInvalid: boolean,\n  errorNotAuth: (\n    error?: string | object | null,\n    status?: AddRedisDatabaseStatus,\n  ) => boolean,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Password,\n    id: SentinelDatabaseIds.Password,\n    accessorKey: SentinelDatabaseIds.Password,\n    cell: ({\n      row: {\n        original: { password, id, error, loading = false, status },\n      },\n    }) => (\n      <PasswordCell\n        password={password}\n        id={id}\n        error={error}\n        loading={loading}\n        status={status}\n        handleChangedInput={handleChangedInput}\n        isInvalid={isInvalid}\n        errorNotAuth={errorNotAuth}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/primaryGroup.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { PrimaryGroupCell } from '../components'\n\nexport const primaryGroupColumn = (): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.PrimaryGroup,\n    id: SentinelDatabaseIds.PrimaryGroup,\n    accessorKey: SentinelDatabaseIds.PrimaryGroup,\n    enableSorting: true,\n    maxSize: 200,\n    cell: ({\n      row: {\n        original: { name },\n      },\n    }) => <PrimaryGroupCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/result.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { ResultCell } from '../components'\n\nexport const resultColumn = (\n  addActions?: boolean,\n  onAddInstance?: (name: string) => void,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Result,\n    id: SentinelDatabaseIds.Message,\n    accessorKey: SentinelDatabaseIds.Message,\n    enableSorting: true,\n    minSize: addActions ? 250 : 110,\n    cell: ({\n      row: {\n        original: { status, message, name, error, alias, loading = false },\n      },\n    }) => (\n      <ResultCell\n        status={status}\n        message={message}\n        name={name}\n        error={error}\n        alias={alias}\n        loading={loading}\n        addActions={addActions}\n        onAddInstance={onAddInstance}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/columns/username.tsx",
    "content": "import React from 'react'\n\nimport type { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport type {\n  ModifiedSentinelMaster,\n  AddRedisDatabaseStatus,\n} from 'uiSrc/slices/interfaces'\nimport {\n  SentinelDatabaseIds,\n  SentinelDatabaseTitles,\n} from 'uiSrc/pages/autodiscover-sentinel/constants/constants'\nimport { UsernameCell } from '../components'\n\nexport const usernameColumn = (\n  handleChangedInput: (name: string, value: string) => void,\n  isInvalid: boolean,\n  errorNotAuth: (\n    error?: string | object | null,\n    status?: AddRedisDatabaseStatus,\n  ) => boolean,\n): ColumnDef<ModifiedSentinelMaster> => {\n  return {\n    header: SentinelDatabaseTitles.Username,\n    id: SentinelDatabaseIds.Username,\n    accessorKey: SentinelDatabaseIds.Username,\n    cell: ({\n      row: {\n        original: { username, id, loading = false, error, status },\n      },\n    }) => (\n      <UsernameCell\n        username={username}\n        id={id}\n        loading={loading}\n        error={error}\n        status={status}\n        handleChangedInput={handleChangedInput}\n        isInvalid={isInvalid}\n        errorNotAuth={errorNotAuth}\n      />\n    ),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.tsx",
    "content": "import React from 'react'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { RiTooltip } from 'uiSrc/components'\nimport { ApiStatusCode } from 'uiSrc/constants'\nimport { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\n\nimport type { AddErrorButtonProps } from './AddErrorButton.types'\n\nexport const AddErrorButton = ({\n  name,\n  error,\n  alias,\n  loading = false,\n  onAddInstance = () => {},\n}: AddErrorButtonProps) => {\n  const isDisabled = !alias\n  if (\n    typeof error === 'object' &&\n    error !== null &&\n    'statusCode' in error &&\n    error?.statusCode !== ApiStatusCode.Unauthorized &&\n    !ApiEncryptionErrors.includes(error?.name || '') &&\n    error?.statusCode !== ApiStatusCode.BadRequest\n  ) {\n    return null\n  }\n  return (\n    <FlexItem padding role=\"presentation\">\n      <RiTooltip\n        position=\"top\"\n        title={isDisabled ? validationErrors.REQUIRED_TITLE(1) : null}\n        content={isDisabled ? <span>Database Alias</span> : null}\n      >\n        <PrimaryButton\n          size=\"s\"\n          disabled={isDisabled}\n          loading={loading}\n          onClick={() => onAddInstance(name)}\n          icon={isDisabled ? InfoIcon : undefined}\n        >\n          Add Primary Group\n        </PrimaryButton>\n      </RiTooltip>\n    </FlexItem>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddErrorButton/AddErrorButton.types.ts",
    "content": "interface ErrorWithStatusCode {\n  statusCode?: number\n  name?: string\n  [key: string]: any\n}\n\nexport interface AddErrorButtonProps {\n  name: string\n  error?: string | ErrorWithStatusCode | null\n  alias?: string\n  loading?: boolean\n  onAddInstance?: (name: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.tsx",
    "content": "import React from 'react'\nimport {\n  CopyTextContainer,\n  CopyPublicEndpointText,\n  CopyBtnWrapper,\n} from 'uiSrc/components/auto-discover'\n\nimport type { AddressCellProps } from './AddressCell.types'\n\nexport const AddressCell = ({ host = '', port = '' }: AddressCellProps) => {\n  if (!host || !port) {\n    return null\n  }\n\n  const text = `${host}:${port}`\n  return (\n    <CopyTextContainer>\n      <CopyPublicEndpointText className=\"copyHostPortText\">\n        {text}\n      </CopyPublicEndpointText>\n      <CopyBtnWrapper copy={text} aria-label=\"Copy address\" successLabel=\"\" />\n    </CopyTextContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AddressCell/AddressCell.types.ts",
    "content": "export interface AddressCellProps {\n  host?: string\n  port?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.tsx",
    "content": "import React from 'react'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\n\nimport type { AliasCellProps } from './AliasCell.types'\n\nexport const AliasCell = ({\n  id = '',\n  alias,\n  error,\n  loading = false,\n  status,\n  handleChangedInput,\n  errorNotAuth,\n}: AliasCellProps) => {\n  if (errorNotAuth(error, status)) {\n    return <CellText>{alias}</CellText>\n  }\n  return (\n    <InputFieldSentinel\n      name={`alias-${id}`}\n      value={alias}\n      placeholder=\"Database\"\n      disabled={loading}\n      inputType={SentinelInputFieldType.Text}\n      onChangedInput={handleChangedInput}\n      maxLength={500}\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/AliasCell/AliasCell.types.ts",
    "content": "import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nexport interface AliasCellProps {\n  id?: string\n  alias?: string\n  error?: string | object | null\n  loading?: boolean\n  status?: AddRedisDatabaseStatus\n  handleChangedInput: (name: string, value: string) => void\n  errorNotAuth: (\n    error?: string | object | null,\n    status?: AddRedisDatabaseStatus,\n  ) => boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\nimport { ApiStatusCode } from 'uiSrc/constants'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nimport type { DbCellProps } from './DbCell.types'\n\nexport const DbCell = ({\n  db,\n  id = '',\n  loading = false,\n  status,\n  error,\n  handleChangedInput,\n}: DbCellProps) => {\n  if (status === AddRedisDatabaseStatus.Success) {\n    return db !== undefined ? <span>{db}</span> : <i>not assigned</i>\n  }\n  const isDBInvalid =\n    typeof error === 'object' &&\n    error !== null &&\n    'statusCode' in error &&\n    error.statusCode === ApiStatusCode.BadRequest\n  return (\n    <div role=\"presentation\" style={{ position: 'relative' }}>\n      <InputFieldSentinel\n        min={0}\n        disabled={loading}\n        value={`${db}` || '0'}\n        name={`db-${id}`}\n        isInvalid={isDBInvalid}\n        placeholder=\"Enter Index\"\n        inputType={SentinelInputFieldType.Number}\n        onChangedInput={handleChangedInput}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/DbCell/DbCell.types.ts",
    "content": "import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nexport interface DbCellProps {\n  db?: number\n  id?: string\n  loading?: boolean\n  status?: AddRedisDatabaseStatus\n  error?: string | object | null\n  handleChangedInput: (name: string, value: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nimport type { PasswordCellProps } from './PasswordCell.types'\n\nexport const PasswordCell = ({\n  password = '',\n  id = '',\n  error,\n  loading = false,\n  status,\n  handleChangedInput,\n  isInvalid,\n  errorNotAuth,\n}: PasswordCellProps) => {\n  if (\n    errorNotAuth(error, status) ||\n    status === AddRedisDatabaseStatus.Success\n  ) {\n    return password ? <span>************</span> : <i>not assigned</i>\n  }\n  return (\n    <div role=\"presentation\" style={{ position: 'relative' }}>\n      <InputFieldSentinel\n        isInvalid={isInvalid}\n        value={password}\n        name={`password-${id}`}\n        placeholder=\"Enter Password\"\n        disabled={loading}\n        inputType={SentinelInputFieldType.Password}\n        onChangedInput={handleChangedInput}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PasswordCell/PasswordCell.types.ts",
    "content": "import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nexport interface PasswordCellProps {\n  password?: string\n  id?: string\n  error?: string | object | null\n  loading?: boolean\n  status?: AddRedisDatabaseStatus\n  handleChangedInput: (name: string, value: string) => void\n  isInvalid: boolean\n  errorNotAuth: (\n    error?: string | object | null,\n    status?: AddRedisDatabaseStatus,\n  ) => boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.tsx",
    "content": "import React from 'react'\nimport { CellText } from 'uiSrc/components/auto-discover'\n\nimport type { PrimaryGroupCellProps } from './PrimaryGroupCell.types'\n\nexport const PrimaryGroupCell = ({ name }: PrimaryGroupCellProps) => (\n  <CellText data-testid={`primary-group_${name}`}>{name}</CellText>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/PrimaryGroupCell/PrimaryGroupCell.types.ts",
    "content": "export interface PrimaryGroupCellProps {\n  name: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.tsx",
    "content": "import React from 'react'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport { RiTooltip } from 'uiSrc/components'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nimport { AddErrorButton } from '../AddErrorButton/AddErrorButton'\nimport type { ResultCellProps } from './ResultCell.types'\n\nexport const ResultCell = ({\n  status,\n  message = '',\n  name,\n  error,\n  alias,\n  loading = false,\n  addActions,\n  onAddInstance,\n}: ResultCellProps) => {\n  return (\n    <Row\n      data-testid={`status_${name}_${status}`}\n      align=\"center\"\n      justify=\"between\"\n      gap=\"m\"\n    >\n      {loading && <Loader size=\"L\" />}\n      {!loading && status === AddRedisDatabaseStatus.Success && (\n        <CellText>{message}</CellText>\n      )}\n      {!loading && status !== AddRedisDatabaseStatus.Success && (\n        <RiTooltip position=\"right\" title=\"Error\" content={message}>\n          <FlexItem direction=\"row\" grow={false}>\n            <ColorText size=\"S\" color=\"danger\" style={{ cursor: 'pointer' }}>\n              Error\n            </ColorText>\n            <Spacer size=\"s\" direction=\"horizontal\" />\n            <RiIcon size=\"M\" type=\"ToastDangerIcon\" color=\"danger600\" />\n          </FlexItem>\n        </RiTooltip>\n      )}\n      {addActions && onAddInstance && (\n        <AddErrorButton\n          name={name}\n          error={error}\n          alias={alias}\n          loading={loading}\n          onAddInstance={onAddInstance}\n        />\n      )}\n    </Row>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/ResultCell/ResultCell.types.ts",
    "content": "import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nexport interface ResultCellProps {\n  status?: AddRedisDatabaseStatus\n  message?: string\n  name: string\n  error?: string | object | null\n  alias?: string\n  loading?: boolean\n  addActions?: boolean\n  onAddInstance?: (name: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.tsx",
    "content": "import React from 'react'\nimport { InputFieldSentinel } from 'uiSrc/components'\nimport { SentinelInputFieldType } from 'uiSrc/components/input-field-sentinel/InputFieldSentinel'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nimport type { UsernameCellProps } from './UsernameCell.types'\n\nexport const UsernameCell = ({\n  username,\n  id,\n  loading = false,\n  error,\n  status,\n  handleChangedInput,\n  isInvalid,\n  errorNotAuth,\n}: UsernameCellProps) => {\n  if (\n    errorNotAuth(error, status) ||\n    status === AddRedisDatabaseStatus.Success\n  ) {\n    return username ? <span>{username}</span> : <i>Default</i>\n  }\n  return (\n    <div role=\"presentation\" style={{ position: 'relative' }}>\n      <InputFieldSentinel\n        isText\n        isInvalid={isInvalid}\n        value={username}\n        name={`username-${id}`}\n        placeholder=\"Enter Username\"\n        disabled={loading}\n        inputType={SentinelInputFieldType.Text}\n        onChangedInput={handleChangedInput}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/UsernameCell/UsernameCell.types.ts",
    "content": "import type { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\n\nexport interface UsernameCellProps {\n  username?: string\n  id?: string\n  loading?: boolean\n  error?: string | object | null\n  status?: AddRedisDatabaseStatus\n  handleChangedInput: (name: string, value: string) => void\n  isInvalid: boolean\n  errorNotAuth: (\n    error?: string | object | null,\n    status?: AddRedisDatabaseStatus,\n  ) => boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/components/index.ts",
    "content": "export { AddErrorButton } from './AddErrorButton/AddErrorButton'\nexport { AddressCell } from './AddressCell/AddressCell'\nexport { AliasCell } from './AliasCell/AliasCell'\nexport { DbCell } from './DbCell/DbCell'\nexport { PasswordCell } from './PasswordCell/PasswordCell'\nexport { PrimaryGroupCell } from './PrimaryGroupCell/PrimaryGroupCell'\nexport { ResultCell } from './ResultCell/ResultCell'\nexport { UsernameCell } from './UsernameCell/UsernameCell'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/column-definitions/index.ts",
    "content": "export * from './columns/result'\nexport * from './columns/primaryGroup'\nexport * from './columns/alias'\nexport * from './columns/address'\nexport * from './columns/numberOfReplicas'\nexport * from './columns/username'\nexport * from './columns/password'\nexport * from './columns/db'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/components/index.ts",
    "content": "import SentinelDatabasesResult from './SentinelDatabasesResult/SentinelDatabasesResult'\n\nexport default SentinelDatabasesResult\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/index.ts",
    "content": "import SentinelDatabasesResultPage from './SentinelDatabasesResultPage'\n\nexport { SentinelDatabasesResultPage }\n\nexport default SentinelDatabasesResultPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/sentinel-databases-result/useSentinelDatabasesResultConfig.tsx",
    "content": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport { ApiStatusCode, Pages } from 'uiSrc/constants'\nimport {\n  createMastersSentinelAction,\n  resetDataSentinel,\n  resetLoadedSentinel,\n  sentinelSelector,\n  updateMastersSentinel,\n} from 'uiSrc/slices/instances/sentinel'\nimport {\n  AddRedisDatabaseStatus,\n  LoadedSentinel,\n  ModifiedSentinelMaster,\n} from 'uiSrc/slices/interfaces'\nimport { removeEmpty, setTitle } from 'uiSrc/utils'\nimport { pick } from 'lodash'\nimport { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  aliasColumn,\n  dbColumn,\n  addressColumn,\n  numberOfReplicasColumn,\n  passwordColumn,\n  primaryGroupColumn,\n  resultColumn,\n  usernameColumn,\n} from './components/column-definitions'\n\n// Define an interface for the error object\ninterface ErrorWithStatusCode {\n  statusCode?: number\n  name?: string\n  [key: string]: any\n}\n\nfunction errorNotAuth(\n  error?: string | ErrorWithStatusCode | null,\n  status?: AddRedisDatabaseStatus,\n) {\n  return (\n    (typeof error === 'object' &&\n      error?.statusCode !== ApiStatusCode.Unauthorized) ||\n    status === AddRedisDatabaseStatus.Success\n  )\n}\n\nexport const colFactory = (\n  handleChangedInput: (name: string, value: string) => void,\n  handleAddInstance: (masterName: string) => void,\n  isInvalid: boolean,\n  countSuccessAdded: number,\n  itemsLength: number,\n) => {\n  const cols: ColumnDef<ModifiedSentinelMaster>[] = [\n    resultColumn(countSuccessAdded !== itemsLength, handleAddInstance),\n    primaryGroupColumn(),\n    aliasColumn(handleChangedInput, errorNotAuth),\n    addressColumn(),\n    numberOfReplicasColumn(),\n    usernameColumn(handleChangedInput, isInvalid, errorNotAuth),\n    passwordColumn(handleChangedInput, isInvalid, errorNotAuth),\n    dbColumn(handleChangedInput),\n  ]\n\n  return cols\n}\n\nexport const useSentinelDatabasesResultConfig = () => {\n  const [items, setItems] = useState<ModifiedSentinelMaster[]>([])\n  const [isInvalid, setIsInvalid] = useState(true)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const handleBackAdding = useCallback(() => {\n    dispatch(resetLoadedSentinel(LoadedSentinel.MastersAdded))\n    history.push(Pages.home)\n  }, [])\n\n  const handleViewDatabases = useCallback(() => {\n    dispatch(resetDataSentinel())\n    history.push(Pages.home)\n  }, [])\n\n  const { data: masters } = useSelector(sentinelSelector)\n  const mastersLength = masters.length\n\n  const countSuccessAdded = masters.filter(\n    ({ status }) => status === AddRedisDatabaseStatus.Success,\n  )?.length\n\n  useEffect(() => {\n    if (!mastersLength) {\n      history.push(Pages.home)\n      return\n    }\n    setTitle('Redis Sentinel Primary Groups Added')\n\n    setIsInvalid(true)\n    setItems(masters)\n    dispatch(resetLoadedSentinel(LoadedSentinel.MastersAdded))\n  }, [mastersLength])\n\n  const handleAddInstance = useCallback(\n    (masterName: string) => {\n      const instance: ModifiedSentinelMaster = {\n        ...removeEmpty(items.find((item) => item.name === masterName)),\n        loading: true,\n      }\n\n      const updatedItems = items.map((item) =>\n        item.name === masterName ? instance : item,\n      )\n\n      const pikedInstance = [\n        pick(instance, 'alias', 'name', 'username', 'password', 'db'),\n      ]\n\n      dispatch(updateMastersSentinel(updatedItems))\n      dispatch(createMastersSentinelAction(pikedInstance))\n    },\n    [items],\n  )\n\n  const handleChangedInput = useCallback(\n    (name: string, value: string) => {\n      const [field, id] = name.split('-')\n\n      setItems((items) => {\n        const item = items.find((item) => item.id === id)\n        // @ts-ignore\n        if (!item || item[field] === value) {\n          return items\n        }\n        return items.map((item) => {\n          if (item.id !== id) {\n            return item\n          }\n\n          return { ...item, [field]: value }\n        })\n      })\n    },\n    [setItems],\n  )\n\n  const columns: ColumnDef<ModifiedSentinelMaster>[] = useMemo(() => {\n    return colFactory(\n      handleChangedInput,\n      handleAddInstance,\n      isInvalid,\n      countSuccessAdded,\n      items.length,\n    )\n  }, [\n    countSuccessAdded,\n    isInvalid,\n    items.length,\n    handleAddInstance,\n    handleChangedInput,\n  ])\n\n  return {\n    columns,\n    items,\n    countSuccessAdded,\n    handleBackAdding,\n    handleViewDatabases,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/autodiscover-sentinel/styles.module.scss",
    "content": ".footer {\n  padding: 15px 14px 10px 30px !important;\n}\n\n.title {\n  padding-bottom: 5px !important;\n}\n\n.subTitle {\n  padding-bottom: 10px;\n}\n\n.searchForm {\n  width: 266px !important;\n}\n\n.table {\n  @include eui.scrollBar;\n  overflow: auto;\n  max-height: calc(100vh - 320px) !important;\n\n  @media (min-width: 1180px) {\n    max-height: calc(100vh - 260px) !important;\n  }\n}\n\n.tableEmpty :global(.euiTableRowCell) {\n  display: none !important;\n}\n.tableEmpty :global(.euiTableRow) {\n  height: 1px !important;\n}\n\n.notFoundMsg {\n  margin: 50px auto;\n  text-align: center;\n}\n\n.panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n\n.dbInfo {\n  width: 67px !important;\n}\n\n.input {\n  width: 264px !important;\n}\n\n.tooltipColumnName {\n  max-width: 370px !important;\n  * {\n    line-height: 1.19;\n    font-size: 14px !important;\n\n    &:not(:global(.euiToolTip__title)) {\n      font-weight: 300 !important;\n    }\n  }\n}\n\n.errorBtn {\n  height: 100% !important;\n  > span {\n    padding: 0 !important;\n  }\n}\n\n.tooltipColumnNameText {\n  text-decoration: underline;\n  display: inline-block;\n  width: 100%;\n  overflow: hidden;\n  vertical-align: top;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx",
    "content": "/* eslint-disable sonarjs/no-identical-functions */\nimport React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\nimport { setConnectedInstanceId } from 'uiSrc/slices/instances/instances'\nimport { loadKeys, toggleBrowserFullScreen } from 'uiSrc/slices/browser/keys'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { resetErrors } from 'uiSrc/slices/app/notifications'\nimport {\n  setBrowserBulkActionOpen,\n  setBrowserKeyListDataLoaded,\n  setBrowserPanelSizes,\n  setBrowserSelectedKey,\n} from 'uiSrc/slices/app/context'\nimport BrowserPage from './BrowserPage'\nimport KeyList, { Props as KeyListProps } from './components/key-tree/KeyTree'\nimport { Props as KeyDetailsWrapperProps } from './modules/key-details/KeyDetails'\nimport { KeyDetails } from './modules'\nimport AddKey, { Props as AddKeyProps } from './components/add-key/AddKey'\nimport BrowserSearchPanel from './components/browser-search-panel'\nimport { Props as KeysHeaderProps } from './components/keys-header/KeysHeader'\n\njest.mock('./components/key-tree/KeyTree', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\njest.mock('./components/add-key/AddKey', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\njest.mock('uiSrc/pages/browser/modules', () => ({\n  __esModule: true,\n  KeyDetails: jest.fn(),\n}))\n\njest.mock('./components/browser-search-panel', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockKeyList = (props: KeyListProps) => (\n  <div>\n    <button\n      type=\"button\"\n      data-testid=\"loadMoreItems-btn\"\n      onClick={() =>\n        props?.handleScanMoreClick?.({ startIndex: 1, stopIndex: 2 })\n      }\n    >\n      loadMoreItems\n    </button>\n  </div>\n)\n\nconst mockKeyDetailsWrapper = (props: KeyDetailsWrapperProps) => (\n  <div>\n    <button\n      type=\"button\"\n      data-testid=\"onCloseKey-btn\"\n      onClick={() => props.onCloseKey()}\n    >\n      onCloseKey\n    </button>\n  </div>\n)\n\nconst mockAddKey = (props: AddKeyProps) => (\n  <div>\n    <button\n      type=\"button\"\n      data-testid=\"handleCloseKey-btn\"\n      onClick={() => props.handleCloseKey()}\n    >\n      handleCloseKey\n    </button>\n  </div>\n)\n\nconst mockBrowserSearchPanel = (props: KeysHeaderProps) => (\n  <div>\n    <button\n      type=\"button\"\n      data-testid=\"handleAddKeyPanel-btn\"\n      onClick={() => props?.handleAddKeyPanel?.(true)}\n    >\n      handleAddKeyPanel\n    </button>\n    <button\n      type=\"button\"\n      data-testid=\"handleBulkActionsPanel-btn\"\n      onClick={() => props?.handleBulkActionsPanel?.(true)}\n    >\n      handleBulkActionsPanel\n    </button>\n  </div>\n)\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\n/**\n * BrowserPage tests\n *\n * @group component\n */\ndescribe('BrowserPage', () => {\n  beforeAll(() => {\n    KeyList.mockImplementation(mockKeyList)\n    BrowserSearchPanel.mockImplementation(mockBrowserSearchPanel)\n    KeyDetails.mockImplementation(mockKeyDetailsWrapper)\n    AddKey.mockImplementation(mockAddKey)\n  })\n\n  it('should render', () => {\n    expect(render(<BrowserPage />)).toBeTruthy()\n    const afterRenderActions = [\n      setConnectedInstanceId('instanceId'),\n      loadKeys(),\n      resetErrors(),\n    ]\n    expect(store.getActions().slice(0, afterRenderActions.length)).toEqual([\n      ...afterRenderActions,\n    ])\n  })\n\n  it('should call loadMoreItems without nextCursor', () => {\n    render(<BrowserPage />)\n    // select Browser/List view\n    fireEvent.click(screen.getByTestId('view-type-list-btn'))\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('loadMoreItems-btn'))\n\n    expect(store.getActions()).toEqual([...afterRenderActions])\n  })\n\n  it('should call onCloseKey', () => {\n    render(<BrowserPage />)\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('onCloseKey-btn'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      toggleBrowserFullScreen(true),\n    ])\n  })\n\n  it('should call proper actions on onmount', () => {\n    const { unmount } = render(<BrowserPage />)\n    const afterRenderActions = [...store.getActions()]\n\n    unmount()\n\n    const unmountActions = [\n      setBrowserPanelSizes(expect.any(Object)),\n      setBrowserBulkActionOpen(expect.any(Boolean)),\n      setBrowserSelectedKey(null),\n      setBrowserKeyListDataLoaded(SearchMode.Pattern, false),\n      toggleBrowserFullScreen(false),\n    ]\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      ...unmountActions,\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/BrowserPage.styles.ts",
    "content": "import React from 'react'\nimport styled, { css } from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  ResizableContainer,\n  ResizablePanel,\n} from 'uiSrc/components/base/layout'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const PageContainer = styled(Col)`\n  max-width: 100vw;\n  overflow: hidden;\n`\n\nexport const MainContent = styled(Row)`\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n  overflow: hidden;\n`\n\nexport const BackButtonWrapper = styled.div`\n  align-self: self-start;\n  margin: ${({ theme }) =>\n    `0 0 ${theme.core.space.space100} ${theme.core.space.space200}`};\n\n  > button {\n    background-color: transparent;\n    border: 0;\n    box-shadow: none;\n  }\n`\n\nexport const SearchPanelWrapper = styled.div<{\n  $hidden: boolean\n  children?: React.ReactNode\n}>`\n  ${({ $hidden }) =>\n    $hidden &&\n    css`\n      display: none;\n    `}\n`\n\nexport const StyledResizableContainer = styled(ResizableContainer)`\n  position: relative;\n`\n\nconst fullWidthStyles = css`\n  width: 100%;\n  min-width: 100%;\n`\n\nconst keyDetailsStyles = css`\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  left: 100%;\n  top: 0;\n  transition: left 0.25s ease;\n  will-change: left;\n`\n\nconst keyDetailsOpenStyles = css`\n  left: 0;\n\n  @media (max-width: 1123.98px) {\n    width: 100%;\n  }\n`\n\nexport const BorderedResizablePanel = styled(ResizablePanel)<{\n  $fullWidth?: boolean\n  $keyDetails?: boolean\n  $keyDetailsOpen?: boolean\n}>`\n  border-radius: ${({ theme }: { theme: Theme }) =>\n    theme.components.card.borderRadius};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  ${({ $fullWidth }) => $fullWidth && fullWidthStyles}\n  ${({ $keyDetails }) => $keyDetails && keyDetailsStyles}\n  ${({ $keyDetailsOpen }) => $keyDetailsOpen && keyDetailsOpenStyles}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/BrowserPage.test.tsx",
    "content": "/* eslint-disable sonarjs/no-identical-functions */\nimport React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { useSelector } from 'react-redux'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  act,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { RootState } from 'uiSrc/slices/store'\nimport {\n  setSelectedKeyRefreshDisabled,\n  toggleBrowserFullScreen,\n} from 'uiSrc/slices/browser/keys'\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport {\n  connectedInstanceOverviewSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport BrowserPage from './BrowserPage'\nimport KeyList, { Props as KeyListProps } from './components/key-list/KeyList'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\njest.mock('./components/key-list/KeyList', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst unprintableStringBuffer = {\n  type: 'Buffer',\n  data: [172, 237, 0],\n}\n\nconst mockKeyList = (props: KeyListProps) => (\n  <div>\n    <button\n      type=\"button\"\n      data-testid=\"loadMoreItems-btn\"\n      onClick={() =>\n        props?.handleScanMoreClick?.({ startIndex: 1, stopIndex: 2 })\n      }\n    >\n      loadMoreItems\n    </button>\n  </div>\n)\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst selectKey = (state: any, selectedKey: any, data?: any = {}) => {\n  ;(useSelector as jest.Mock).mockImplementation(\n    (callback: (arg0: RootState) => RootState) =>\n      callback({\n        ...state,\n        app: {\n          ...state.app,\n          context: {\n            ...state.app.context,\n            contextInstanceId: 'instanceId',\n            browser: {\n              ...state.app.context.browser,\n              bulkActions: {\n                opened: false,\n              },\n              keyList: {\n                ...state.app.context.keyList,\n                isDataLoaded: true,\n                selectedKey,\n              },\n            },\n          },\n        },\n\n        browser: {\n          ...state.browser,\n          ...data,\n          keys: {\n            ...state.browser.keys,\n            selectedKey,\n          },\n        },\n      }),\n  )\n}\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn(),\n  connectedInstanceOverviewSelector: jest.fn(),\n}))\n/**\n * BrowserPage tests\n *\n * @group component\n */\n\nconst originalUseSelector = jest.requireActual('react-redux').useSelector\n\ndescribe('BrowserPage', () => {\n  const commonOptions = {\n    id: 'instanceId',\n    name: 'test',\n    connectionType: 'CLUSTER',\n    provider: 'REDIS_CLOUD',\n  }\n\n  beforeAll(() => {\n    ;(useSelector as jest.Mock).mockImplementation(originalUseSelector)\n  })\n\n  it.each([true, false])(\n    'should call proper sendPageViewTelemetry when isFreeDb is %s',\n    (isFreeDb) => {\n      const sendPageViewTelemetryMock = jest.fn()\n      ;(sendPageViewTelemetry as jest.Mock).mockImplementation(\n        () => sendPageViewTelemetryMock,\n      )\n      ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n        ...commonOptions,\n        isFreeDb,\n      }))\n      ;(connectedInstanceOverviewSelector as jest.Mock).mockImplementation(\n        () => ({\n          totalKeys: 25,\n        }),\n      )\n\n      render(<BrowserPage />)\n\n      expect(sendPageViewTelemetry).toBeCalledWith({\n        name: TelemetryPageView.BROWSER_PAGE,\n        eventData: {\n          databaseId: 'instanceId',\n          isFree: isFreeDb,\n          totalKeys: 25,\n        },\n      })\n    },\n  )\n})\n\ndescribe('KeyDetailsHeader', () => {\n  beforeAll(() => {\n    KeyList.mockImplementation(mockKeyList)\n  })\n\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    // key with unprintable characters\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.Hash,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    selectKey(state, selectedKey)\n  })\n\n  it('Verify that user cannot rename key name with unprintable characters and check tooltip', async () => {\n    const { queryByTestId } = render(<BrowserPage />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId(/edit-key-btn/))\n\n    expect(screen.getByTestId(/edit-key-btn/)).toBeInTheDocument()\n    fireEvent.change(screen.getByTestId(/edit-key-input/), {\n      target: { value: 'val123' },\n    })\n\n    expect(queryByTestId('apply-btn')).toBeInTheDocument()\n    fireEvent.click(screen.getByTestId(/apply-btn/))\n    expect(queryByTestId('apply-btn')).toBeDisabled()\n\n    expect(store.getActions()).toEqual([...afterRenderActions])\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('apply-btn'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.queryByTestId('apply-tooltip')).toBeInTheDocument()\n  })\n})\n\ndescribe('KeyDetailsWrapper', () => {\n  beforeAll(() => {\n    KeyList.mockImplementation(mockKeyList)\n  })\n\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    // key with unprintable characters\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.Hash,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    selectKey(state, selectedKey)\n  })\n\n  it('Verify that user cannot save key value (String) with unprintable characters and check tooltip', async () => {\n    const state: any = store.getState()\n\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.String,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    // String value with unprintable characters\n    const data = {\n      string: {\n        loading: false,\n        error: '',\n        data: {\n          key: unprintableStringBuffer,\n          value: {\n            type: 'Buffer',\n            data: [172, 237, 0, 5, 115, 114, 0],\n          },\n        },\n      },\n    }\n    selectKey(state, selectedKey, data)\n\n    const { queryByTestId } = render(<BrowserPage />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId(/string-value/))\n\n    expect(screen.getByTestId(/string-value/)).toBeInTheDocument()\n    fireEvent.change(screen.getByTestId(/string-value/), {\n      target: { value: 'val123' },\n    })\n\n    expect(queryByTestId('apply-btn')).toBeInTheDocument()\n    expect(queryByTestId('apply-btn')).toBeDisabled()\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('apply-btn'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.queryByTestId('apply-tooltip')).toBeInTheDocument()\n  })\n\n  it('Verify that user cannot save key value (Hash) with unprintable characters', () => {\n    const state: any = store.getState()\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.Hash,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    // Hash value with unprintable characters\n    const data = {\n      hash: {\n        loading: false,\n        error: '',\n        data: {\n          nextCursor: 0,\n          match: '*',\n          key: unprintableStringBuffer,\n          total: 1,\n          fields: [\n            {\n              value: unprintableStringBuffer,\n              field: {\n                type: 'Buffer',\n                data: [49], // 1\n              },\n            },\n          ],\n        },\n        updateValue: {\n          loading: false,\n        },\n      },\n    }\n    selectKey(state, selectedKey, data)\n\n    const { queryByTestId } = render(<BrowserPage />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId(/hash_content-value-1/))\n    })\n\n    fireEvent.click(screen.getByTestId(/hash_edit-btn-1/))\n\n    expect(screen.getByTestId(/hash_value-editor-1/)).toBeInTheDocument()\n    fireEvent.change(screen.getByTestId(/hash_value-editor-1/), {\n      target: { value: 'val123' },\n    })\n\n    expect(queryByTestId('apply-btn')).toBeInTheDocument()\n    expect(queryByTestId('apply-btn')).toBeDisabled()\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  it('Verify that user cannot save key value (List) with unprintable characters', () => {\n    const state: any = store.getState()\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.List,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    // List value with unprintable characters\n    const data = {\n      list: {\n        loading: false,\n        error: '',\n        data: {\n          count: 0,\n          offset: 0,\n          key: unprintableStringBuffer,\n          total: 1,\n          elements: [\n            {\n              index: 0,\n              element: {\n                type: 'Buffer',\n                data: [172, 237, 0],\n              },\n            },\n          ],\n        },\n        updateValue: {\n          loading: false,\n        },\n      },\n    }\n    selectKey(state, selectedKey, data)\n\n    const { queryByTestId } = render(<BrowserPage />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId(/list_content-value-0/))\n    })\n\n    fireEvent.click(screen.getByTestId(/list_edit-btn-0/))\n\n    expect(screen.getByTestId(/list_value-editor-0/)).toBeInTheDocument()\n    fireEvent.change(screen.getByTestId(/list_value-editor-0/), {\n      target: { value: 'val123' },\n    })\n\n    expect(queryByTestId('apply-btn')).toBeInTheDocument()\n    expect(queryByTestId('apply-btn')).toBeDisabled()\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n})\n\ndescribe('back btn', () => {\n  beforeAll(() => {\n    KeyList.mockImplementation(mockKeyList)\n  })\n\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    // key with unprintable characters\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.Hash,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    selectKey(state, selectedKey)\n  })\n\n  it('Should call toggleBrowserFullScreen after back btn click', () => {\n    const state: any = store.getState()\n\n    const selectedKey = {\n      lastRefreshTime: 1664380909470,\n      data: {\n        name: unprintableStringBuffer,\n        type: KeyTypes.String,\n        ttl: -1,\n        size: 57,\n        length: 7,\n        nameString: '��',\n      },\n    }\n\n    // String value with unprintable characters\n    const data = {\n      string: {\n        loading: false,\n        error: '',\n        data: {\n          key: unprintableStringBuffer,\n          value: {\n            type: 'Buffer',\n            data: [172, 237, 0],\n          },\n        },\n      },\n    }\n    selectKey(state, selectedKey, data)\n\n    render(<BrowserPage />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('back-right-panel-btn'))\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      toggleBrowserFullScreen(true),\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/BrowserPage.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { isNumber } from 'lodash'\n\nimport {\n  formatLongName,\n  getDbIndex,\n  isEqualBuffers,\n  Nullable,\n  setTitle,\n} from 'uiSrc/utils'\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport {\n  fetchKeys,\n  keysSelector,\n  resetKeyInfo,\n  selectedKeyDataSelector,\n  setInitialStateByType,\n  toggleBrowserFullScreen,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  setBrowserSelectedKey,\n  appContextBrowser,\n  setBrowserPanelSizes,\n  setBrowserBulkActionOpen,\n  setBrowserKeyListDataLoaded,\n  appContextSelector,\n} from 'uiSrc/slices/app/context'\nimport { resetErrors } from 'uiSrc/slices/app/notifications'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  connectedInstanceOverviewSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\n\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { FeatureFlags, Pages } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport OnboardingStartPopover from 'uiSrc/pages/browser/components/onboarding-start-popover'\nimport { sidePanelsSelector } from 'uiSrc/slices/panels/sidePanels'\nimport { useStateWithContext } from 'uiSrc/services/hooks'\n\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { ArrowLeftIcon } from 'uiSrc/components/base/icons'\nimport { ResizablePanelHandle } from 'uiSrc/components/base/layout'\n\nimport { useAppNavigationActions } from 'uiSrc/contexts/AppNavigationActionsProvider'\nimport Actions from 'uiSrc/pages/browser/components/actions/Actions'\nimport { MakeSearchableModalProvider } from './components/make-searchable-modal'\nimport BrowserSearchPanel from './components/browser-search-panel'\nimport BrowserLeftPanel from './components/browser-left-panel'\nimport BrowserRightPanel from './components/browser-right-panel'\n\nimport * as S from './BrowserPage.styles'\n\nconst widthResponsiveSize = 1280\nconst widthExplorePanel = 460\n\nexport const firstPanelId = 'keys'\nexport const secondPanelId = 'keyDetails'\n\nconst isOneSideMode = (isInsightsOpen: boolean) =>\n  globalThis.innerWidth <\n  widthResponsiveSize + (isInsightsOpen ? widthExplorePanel : 0)\n\nconst BrowserPage = () => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const {\n    name: connectedInstanceName,\n    db = 0,\n    isFreeDb,\n  } = useSelector(connectedInstanceSelector)\n  const {\n    panelSizes,\n    keyList: { selectedKey: selectedKeyContext },\n    bulkActions: { opened: bulkActionOpenContext },\n  } = useSelector(appContextBrowser)\n  const { contextInstanceId } = useSelector(appContextSelector)\n\n  const { isBrowserFullScreen } = useSelector(keysSelector)\n  const { type } = useSelector(selectedKeyDataSelector) ?? {\n    type: '',\n    length: 0,\n  }\n  const { viewType, searchMode } = useSelector(keysSelector)\n  const { openedPanel: openedSidePanel } = useSelector(sidePanelsSelector)\n  const overview = useSelector(connectedInstanceOverviewSelector)\n  const featureFlags = useSelector(appFeatureFlagsFeaturesSelector)\n  const isDevBrowser = featureFlags?.[FeatureFlags.devBrowser]?.flag ?? false\n  const isVectorSearch =\n    featureFlags?.[FeatureFlags.vectorSearchV2]?.flag ?? false\n  const panelMinSize = isDevBrowser ? 20 : 45\n  const panelDefaultSize = 50\n\n  const [isPageViewSent, setIsPageViewSent] = useState(false)\n  const [arePanelsCollapsed, setArePanelsCollapsed] = useState(\n    isOneSideMode(!!openedSidePanel),\n  )\n  const [isAddKeyPanelOpen, setIsAddKeyPanelOpen] = useState(false)\n  const [isBulkActionsPanelOpen, setIsBulkActionsPanelOpen] = useState(\n    bulkActionOpenContext,\n  )\n\n  const [selectedKey, setSelectedKey] = useStateWithContext<\n    Nullable<RedisResponseBuffer>\n  >(selectedKeyContext, null)\n\n  const [sizes, setSizes] = useState(panelSizes)\n\n  const prevSelectedType = useRef<string>(type)\n  const prevDbIndex = useRef(db)\n  const selectedKeyRef = useRef<Nullable<RedisResponseBuffer>>(selectedKey)\n  const isBulkActionsPanelOpenRef = useRef<boolean>(isBulkActionsPanelOpen)\n  const isSidePanelOpenRef = useRef<boolean>(!!openedSidePanel)\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}`\n  setTitle(`${dbName} - Browser`)\n  const { setActions } = useAppNavigationActions()\n  useEffect(() => {\n    dispatch(resetErrors())\n    updateWindowDimensions()\n    globalThis.addEventListener('resize', updateWindowDimensions)\n    setActions(\n      <Actions\n        handleAddKeyPanel={handleAddKeyPanel}\n        handleBulkActionsPanel={handleBulkActionsPanel}\n      />,\n    )\n    // componentWillUnmount\n    return () => {\n      globalThis.removeEventListener('resize', updateWindowDimensions)\n      setSizes((prevSizes: number[]) => {\n        dispatch(setBrowserPanelSizes(prevSizes))\n        return []\n      })\n      dispatch(setBrowserBulkActionOpen(isBulkActionsPanelOpenRef.current))\n      dispatch(setBrowserSelectedKey(selectedKeyRef.current))\n      dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false))\n\n      if (!selectedKeyRef.current) {\n        dispatch(toggleBrowserFullScreen(false))\n      }\n    }\n  }, [])\n\n  useEffect(() => {\n    isBulkActionsPanelOpenRef.current = isBulkActionsPanelOpen\n  }, [isBulkActionsPanelOpen])\n\n  useEffect(() => {\n    if (contextInstanceId === instanceId) setSelectedKey(selectedKeyContext)\n  }, [selectedKeyContext, contextInstanceId])\n\n  useEffect(() => {\n    selectedKeyRef.current = selectedKey\n  }, [selectedKey])\n\n  useEffect(() => {\n    setArePanelsCollapsed(() => isOneSideMode(!!openedSidePanel))\n    isSidePanelOpenRef.current = !!openedSidePanel\n  }, [openedSidePanel])\n\n  useEffect(() => {\n    if (\n      connectedInstanceName &&\n      overview?.totalKeys !== undefined &&\n      !isPageViewSent\n    ) {\n      sendPageView(instanceId, overview?.totalKeys)\n    }\n  }, [connectedInstanceName, overview, isPageViewSent])\n\n  const updateWindowDimensions = () => {\n    setArePanelsCollapsed(isOneSideMode(isSidePanelOpenRef.current))\n  }\n\n  const onPanelWidthChange = useCallback((newSizes: any) => {\n    setSizes((prevSizes: any) => ({\n      ...prevSizes,\n      ...newSizes,\n    }))\n  }, [])\n\n  const sendPageView = (instanceId: string, totalKeys: number | null) => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.BROWSER_PAGE,\n      eventData: {\n        databaseId: instanceId,\n        isFree: isFreeDb,\n        totalKeys,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  const handlePanel = (value: boolean, keyName?: RedisResponseBuffer) => {\n    if (value && !isAddKeyPanelOpen && !isBulkActionsPanelOpen) {\n      dispatch(resetKeyInfo())\n    }\n\n    dispatch(toggleBrowserFullScreen(false))\n    setSelectedKey(keyName ?? null)\n    closeRightPanels()\n  }\n\n  const handleAddKeyPanel = useCallback(\n    (value: boolean, keyName?: RedisResponseBuffer) => {\n      handlePanel(value, keyName)\n      setIsAddKeyPanelOpen(value)\n      dispatch(setBrowserSelectedKey(keyName || null))\n    },\n    [],\n  )\n\n  const handleBulkActionsPanel = useCallback((value: boolean) => {\n    handlePanel(value)\n    setIsBulkActionsPanelOpen(value)\n  }, [])\n\n  const handleRemoveSelectedKey = useCallback(() => {\n    setBrowserSelectedKey(null)\n    handlePanel(true)\n  }, [])\n\n  const handleCreateIndexPanel = useCallback(\n    (value: boolean) => {\n      if (value && isVectorSearch) {\n        history.push(Pages.vectorSearch(instanceId))\n        return\n      }\n    },\n    [isVectorSearch, instanceId],\n  )\n\n  const closeRightPanels = useCallback(() => {\n    setIsAddKeyPanelOpen(false)\n    setIsBulkActionsPanelOpen(false)\n  }, [])\n\n  useEffect(() => {\n    if (isNumber(db) && db !== prevDbIndex.current) {\n      onChangeDbIndex()\n    }\n    prevDbIndex.current = db\n  }, [db])\n\n  const onChangeDbIndex = () => {\n    if (selectedKey) {\n      dispatch(toggleBrowserFullScreen(true))\n      setSelectedKey(null)\n    }\n\n    dispatch(\n      fetchKeys({\n        searchMode,\n        cursor: '0',\n        count:\n          viewType === KeyViewType.Browser\n            ? SCAN_COUNT_DEFAULT\n            : SCAN_TREE_COUNT_DEFAULT,\n      }),\n    )\n  }\n\n  const selectKey = useCallback(({ rowData }: { rowData: any }) => {\n    if (!isEqualBuffers(rowData.name, selectedKeyRef.current)) {\n      dispatch(toggleBrowserFullScreen(false))\n\n      dispatch(setInitialStateByType(prevSelectedType.current))\n      setSelectedKey(rowData.name)\n      dispatch(setBrowserSelectedKey(rowData.name))\n      closeRightPanels()\n      prevSelectedType.current = rowData.type\n    }\n  }, [])\n\n  const closePanel = () => {\n    dispatch(toggleBrowserFullScreen(true))\n\n    setSelectedKey(null)\n    closeRightPanels()\n  }\n\n  const isRightPanelOpen =\n    selectedKey !== null || isAddKeyPanelOpen || isBulkActionsPanelOpen\n  const isRightPanelFullScreen =\n    (isBrowserFullScreen && isRightPanelOpen) ||\n    (arePanelsCollapsed && isRightPanelOpen)\n\n  return (\n    <MakeSearchableModalProvider>\n      <S.PageContainer className=\"browserPage\" data-testid=\"browser-page\">\n        {arePanelsCollapsed && isRightPanelOpen && !isBrowserFullScreen && (\n          <S.BackButtonWrapper>\n            <EmptyButton\n              icon={ArrowLeftIcon}\n              size=\"small\"\n              onClick={closePanel}\n              data-testid=\"back-right-panel-btn\"\n            >\n              Back\n            </EmptyButton>\n          </S.BackButtonWrapper>\n        )}\n        <S.SearchPanelWrapper $hidden={isRightPanelFullScreen}>\n          <BrowserSearchPanel handleCreateIndexPanel={handleCreateIndexPanel} />\n        </S.SearchPanelWrapper>\n        <S.MainContent grow>\n          <S.StyledResizableContainer\n            direction=\"horizontal\"\n            onLayout={onPanelWidthChange}\n          >\n            <S.BorderedResizablePanel\n              defaultSize={sizes && sizes[0] ? sizes[0] : panelDefaultSize}\n              minSize={panelMinSize}\n              id={firstPanelId}\n              $fullWidth={\n                arePanelsCollapsed || (isBrowserFullScreen && !isRightPanelOpen)\n              }\n            >\n              <BrowserLeftPanel\n                selectedKey={selectedKey}\n                selectKey={selectKey}\n                removeSelectedKey={handleRemoveSelectedKey}\n                handleAddKeyPanel={handleAddKeyPanel}\n                handleBulkActionsPanel={handleBulkActionsPanel}\n              />\n            </S.BorderedResizablePanel>\n            {!arePanelsCollapsed && !isBrowserFullScreen && (\n              <ResizablePanelHandle />\n            )}\n            <S.BorderedResizablePanel\n              defaultSize={sizes && sizes[1] ? sizes[1] : panelDefaultSize}\n              minSize={panelMinSize}\n              id={secondPanelId}\n              $keyDetailsOpen={isRightPanelOpen}\n              $fullWidth={\n                arePanelsCollapsed || (isRightPanelOpen && isBrowserFullScreen)\n              }\n              $keyDetails={\n                arePanelsCollapsed || (isRightPanelOpen && isBrowserFullScreen)\n              }\n            >\n              <BrowserRightPanel\n                arePanelsCollapsed={arePanelsCollapsed}\n                setSelectedKey={setSelectedKey}\n                selectedKey={selectedKey}\n                isAddKeyPanelOpen={isAddKeyPanelOpen}\n                isBulkActionsPanelOpen={isBulkActionsPanelOpen}\n                handleAddKeyPanel={handleAddKeyPanel}\n                handleBulkActionsPanel={handleBulkActionsPanel}\n                closeRightPanels={closeRightPanels}\n              />\n            </S.BorderedResizablePanel>\n          </S.StyledResizableContainer>\n        </S.MainContent>\n        <OnboardingStartPopover />\n      </S.PageContainer>\n    </MakeSearchableModalProvider>\n  )\n}\n\nexport default BrowserPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/action-footer/ActionFooter.tsx",
    "content": "import React from 'react'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport AddKeyFooter from 'uiSrc/pages/browser/components/add-key/AddKeyFooter/AddKeyFooter'\nimport { SpacerSize } from 'uiSrc/components/base/layout/spacer/spacer.styles'\nimport { Panel } from 'uiSrc/components/panel'\n\nexport interface ActionFooterProps {\n  cancelText?: string\n  actionText?: string\n  onCancel: () => void\n  onAction: () => void\n  disabled?: boolean\n  loading?: boolean\n  gap?: SpacerSize\n  actionTestId?: string\n  cancelTestId?: string\n  cancelClassName?: string\n  actionClassName?: string\n  usePortal?: boolean\n  enableFormSubmit?: boolean\n}\n\nexport const ActionFooter = ({\n  cancelText = 'Cancel',\n  actionText = 'Save',\n  onCancel,\n  onAction,\n  disabled = false,\n  loading = false,\n  gap = 'm',\n  actionTestId,\n  cancelTestId,\n  cancelClassName = 'btn-cancel btn-back',\n  actionClassName = 'btn-add',\n  usePortal = true,\n  enableFormSubmit = true,\n}: ActionFooterProps) => {\n  const content = (\n    <Panel justify=\"end\" gap={gap}>\n      <FlexItem>\n        <SecondaryButton\n          onClick={onCancel}\n          data-testid={cancelTestId}\n          className={cancelClassName}\n        >\n          {cancelText}\n        </SecondaryButton>\n      </FlexItem>\n      <FlexItem>\n        <PrimaryButton\n          type={enableFormSubmit ? 'submit' : 'button'}\n          loading={loading}\n          onClick={onAction}\n          disabled={disabled || loading}\n          data-testid={actionTestId}\n          className={actionClassName}\n        >\n          {actionText}\n        </PrimaryButton>\n      </FlexItem>\n    </Panel>\n  )\n\n  if (enableFormSubmit) {\n    return (\n      <>\n        <PrimaryButton type=\"submit\" style={{ display: 'none' }}>\n          Submit\n        </PrimaryButton>\n        {usePortal ? <AddKeyFooter>{content}</AddKeyFooter> : content}\n      </>\n    )\n  }\n\n  if (usePortal) {\n    return <AddKeyFooter>{content}</AddKeyFooter>\n  }\n\n  return content\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/action-footer/index.ts",
    "content": "export { ActionFooter } from './ActionFooter'\nexport type { ActionFooterProps } from './ActionFooter'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/actions/Actions.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  setBulkActionType,\n  setBulkDeleteFilter,\n  setBulkDeleteKeyCount,\n  setBulkDeleteSearch,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { BulkActionsType, FeatureFlags, KeyTypes } from 'uiSrc/constants'\nimport Actions, { Props } from './Actions'\n\nlet store: typeof mockedStore\n\nconst mockedProps: Props = {\n  handleBulkActionsPanel: jest.fn(),\n  handleAddKeyPanel: jest.fn(),\n}\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('Actions', () => {\n  it('should render', () => {\n    expect(render(<Actions {...mockedProps} />)).toBeTruthy()\n\n    // Verify the buttons are present\n    const bulkActionsButton = screen.getByTestId('btn-bulk-actions')\n    const addKeyButton = screen.getByTestId('btn-add-key')\n\n    expect(bulkActionsButton).toBeInTheDocument()\n    expect(addKeyButton).toBeInTheDocument()\n  })\n\n  it('should show feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<Actions {...mockedProps} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('btn-bulk-actions')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<Actions {...mockedProps} />, {\n      store: mockStore(initialStoreState),\n    })\n    expect(screen.queryByTestId('btn-bulk-actions')).not.toBeInTheDocument()\n  })\n\n  it('should dispatch bulk delete state sync actions when clicking bulk actions button', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      'browser.keys',\n      {\n        ...initialStateDefault.browser.keys,\n        search: 'user:*',\n        filter: KeyTypes.Hash,\n      },\n    )\n\n    const testStore = mockStore(initialStoreState)\n    const handleBulkActionsPanel = jest.fn()\n    render(\n      <Actions\n        {...mockedProps}\n        handleBulkActionsPanel={handleBulkActionsPanel}\n      />,\n      { store: testStore },\n    )\n\n    fireEvent.click(screen.getByTestId('btn-bulk-actions'))\n\n    const expectedActions = [\n      setBulkDeleteSearch('user:*'),\n      setBulkDeleteFilter(KeyTypes.Hash),\n      setBulkDeleteKeyCount(null),\n      setBulkActionType(BulkActionsType.Delete),\n    ]\n\n    expect(testStore.getActions()).toEqual(\n      expect.arrayContaining(expectedActions),\n    )\n    expect(handleBulkActionsPanel).toHaveBeenCalledWith(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/actions/Actions.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  EmptyButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport {\n  setBulkActionType,\n  setBulkDeleteFilter,\n  setBulkDeleteKeyCount,\n  setBulkDeleteSearch,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { BulkActionsType, FeatureFlags } from 'uiSrc/constants'\nimport { SubscriptionsIcon } from 'uiSrc/components/base/icons'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { keysSelector } from 'uiSrc/slices/browser/keys'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  handleAddKeyPanel: (value: boolean) => void\n  handleBulkActionsPanel: (value: boolean) => void\n}\nconst Actions = ({ handleAddKeyPanel, handleBulkActionsPanel }: Props) => {\n  const dispatch = useDispatch()\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { viewType, search, filter } = useSelector(keysSelector)\n  const openAddKeyPanel = () => {\n    handleAddKeyPanel(true)\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_ADD_BUTTON_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_ADD_BUTTON_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const AddKeyBtn = (\n    <SecondaryButton\n      inverted\n      size=\"m\"\n      filled\n      onClick={openAddKeyPanel}\n      data-testid=\"btn-add-key\"\n    >\n      Add key\n    </SecondaryButton>\n  )\n  const openBulkActions = () => {\n    // Sync current search/filter to bulk delete state\n    dispatch(setBulkDeleteSearch(search))\n    dispatch(setBulkDeleteFilter(filter))\n    dispatch(setBulkDeleteKeyCount(null))\n\n    dispatch(setBulkActionType(BulkActionsType.Delete))\n    handleBulkActionsPanel(true)\n  }\n  const BulkActionsBtn = (\n    <EmptyButton\n      color=\"secondary\"\n      icon={SubscriptionsIcon}\n      onClick={openBulkActions}\n      data-testid=\"btn-bulk-actions\"\n      aria-label=\"bulk actions\"\n    >\n      Bulk actions\n    </EmptyButton>\n  )\n  return (\n    <Row\n      grow={false}\n      gap=\"m\"\n      align=\"center\"\n      style={{\n        flexShrink: 0,\n        marginLeft: 12,\n      }}\n    >\n      <FeatureFlagComponent name={FeatureFlags.envDependent}>\n        {BulkActionsBtn}\n      </FeatureFlagComponent>\n      {AddKeyBtn}\n    </Row>\n  )\n}\n\nexport default Actions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKey.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\n\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\nimport { ADD_KEY_TYPE_OPTIONS } from 'uiSrc/pages/browser/components/add-key/constants/key-type-options'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  OAuthSocialAction,\n  OAuthSocialSource,\n  RedisDefaultModules,\n} from 'uiSrc/slices/interfaces'\nimport * as appFeaturesSlice from 'uiSrc/slices/app/features'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport AddKey from './AddKey'\n\nconst handleAddKeyPanelMock = () => {}\nconst handleCloseKeyMock = () => {}\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '1',\n    modules: [],\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('AddKey', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <AddKey\n          onAddKeyPanel={handleAddKeyPanelMock}\n          onClosePanel={handleCloseKeyMock}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render type select label', () => {\n    render(\n      <AddKey\n        onAddKeyPanel={handleAddKeyPanelMock}\n        onClosePanel={handleCloseKeyMock}\n      />,\n    )\n\n    expect(screen.getByText(/Key Type\\*/i)).toBeInTheDocument()\n  })\n\n  it('should have key type select with predefined first value from options', () => {\n    render(\n      <AddKey\n        onAddKeyPanel={handleAddKeyPanelMock}\n        onClosePanel={handleCloseKeyMock}\n      />,\n    )\n\n    expect(\n      screen.getByTestId(ADD_KEY_TYPE_OPTIONS[0].value),\n    ).toBeInTheDocument()\n  })\n\n  it('should show text if db not contains ReJSON module', async () => {\n    render(\n      <AddKey\n        onAddKeyPanel={handleAddKeyPanelMock}\n        onClosePanel={handleCloseKeyMock}\n      />,\n    )\n\n    await userEvent.click(screen.getByTestId('select-key-type'))\n    await userEvent.click((await screen.findByText('JSON')) || document)\n\n    expect(screen.getByTestId('json-not-loaded-text')).toBeInTheDocument()\n  })\n\n  it('should dispatch open oauth modal open actions if db not contains ReJSON module and has cloudSso is enabled', async () => {\n    jest\n      .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n      .mockReturnValue({\n        cloudSso: {\n          flag: true,\n        },\n        cloudAds: {\n          flag: true,\n        },\n      })\n\n    render(\n      <AddKey\n        onAddKeyPanel={handleAddKeyPanelMock}\n        onClosePanel={handleCloseKeyMock}\n      />,\n    )\n    const afterRenderActions = [...store.getActions()]\n\n    await userEvent.click(screen.getByTestId('select-key-type'))\n    await userEvent.click((await screen.findByText('JSON')) || document)\n\n    await userEvent.click(screen.getByTestId('guide-free-database-link'))\n\n    const expectedActions = [\n      setSSOFlow(OAuthSocialAction.Create),\n      setSocialDialogState(OAuthSocialSource.BrowserRedisJSON),\n    ]\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should not show text if db contains ReJSON module', async () => {\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      modules: [\n        { name: RedisDefaultModules.FT },\n        { name: RedisDefaultModules.ReJSON },\n      ],\n    }))\n\n    render(\n      <AddKey\n        onAddKeyPanel={handleAddKeyPanelMock}\n        onClosePanel={handleCloseKeyMock}\n      />,\n    )\n\n    await userEvent.click(screen.getByTestId('select-key-type'))\n    await userEvent.click((await screen.findByText('JSON')) || document)\n    expect(screen.queryByTestId('json-not-loaded-text')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKey.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const ContentFields = styled.div`\n  margin: 0 auto;\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKey.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { KeyTypes } from 'uiSrc/constants'\nimport HelpTexts from 'uiSrc/constants/help-texts'\nimport AddKeyCommonFields from 'uiSrc/pages/browser/components/add-key/AddKeyCommonFields/AddKeyCommonFields'\nimport {\n  addKeyStateSelector,\n  resetAddKey,\n  keysSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  sendEventTelemetry,\n  TelemetryEvent,\n  getBasedOnViewTypeEvent,\n} from 'uiSrc/telemetry'\nimport { isContainJSONModule, Maybe, stringToBuffer } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { HealthText } from 'uiSrc/components/base/text/HealthText'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { ADD_KEY_TYPE_OPTIONS } from './constants/key-type-options'\nimport AddKeyHash from './AddKeyHash'\nimport AddKeyZset from './AddKeyZset'\nimport AddKeyString from './AddKeyString'\nimport AddKeySet from './AddKeySet'\nimport AddKeyList from './AddKeyList'\nimport AddKeyReJSON from './AddKeyReJSON'\nimport AddKeyStream from './AddKeyStream'\nimport { ContentFields } from './AddKey.styles'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  onAddKeyPanel: (value: boolean, keyName?: RedisResponseBuffer) => void\n  onClosePanel: () => void\n  arePanelsCollapsed?: boolean\n}\nconst AddKey = (props: Props) => {\n  const { onAddKeyPanel, onClosePanel, arePanelsCollapsed } = props\n  const dispatch = useDispatch()\n\n  const { loading } = useSelector(addKeyStateSelector)\n  const { id: instanceId, modules = [] } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewType } = useSelector(keysSelector)\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        dispatch(resetAddKey())\n      },\n    [],\n  )\n\n  const options = ADD_KEY_TYPE_OPTIONS.map((item) => {\n    const { value, color, text } = item\n    return {\n      value,\n      inputDisplay: (\n        <HealthText\n          color={color}\n          style={{ lineHeight: 'inherit' }}\n          data-test-subj={value}\n          data-testid={value}\n        >\n          {text}\n        </HealthText>\n      ),\n    }\n  })\n  const [typeSelected, setTypeSelected] = useState<string>(options[0].value)\n  const [keyName, setKeyName] = useState<string>('')\n  const [keyTTL, setKeyTTL] = useState<Maybe<number>>(undefined)\n\n  const onChangeType = (value: string) => {\n    setTypeSelected(value)\n  }\n\n  const closeKeyTelemetry = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_ADD_CANCELLED,\n        TelemetryEvent.TREE_VIEW_KEY_ADD_CANCELLED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const closeKey = () => {\n    onClosePanel()\n    closeKeyTelemetry()\n  }\n\n  const closeAddKeyPanel = (isCancelled?: boolean) => {\n    // meaning that the user closed the \"Add Key\" panel when clicked on the cancel button\n    if (isCancelled) {\n      onAddKeyPanel(false)\n      onClosePanel()\n      closeKeyTelemetry()\n    }\n    // meaning that the user closed the \"Add Key\" panel when added a key\n    else {\n      onAddKeyPanel(false, stringToBuffer(keyName))\n    }\n  }\n\n  const defaultFields = {\n    keyName,\n    keyTTL,\n  }\n\n  return (\n    <div className={styles.page}>\n      <Row\n        justify=\"center\"\n        className={cx(styles.contentWrapper, 'relative')}\n        gap=\"none\"\n      >\n        <Col justify=\"center\" className={styles.content}>\n          <FlexItem grow style={{ marginBottom: '36px' }}>\n            <Title size=\"M\">New Key</Title>\n            {!arePanelsCollapsed && (\n              <RiTooltip\n                content=\"Close\"\n                position=\"left\"\n                anchorClassName={styles.closeKeyTooltip}\n              >\n                <IconButton\n                  icon={CancelSlimIcon}\n                  aria-label=\"Close key\"\n                  className={styles.closeBtn}\n                  onClick={() => closeKey()}\n                />\n              </RiTooltip>\n            )}\n          </FlexItem>\n          <div className={cx('eui-yScroll', styles.scrollContainer)}>\n            <ContentFields>\n              <AddKeyCommonFields\n                typeSelected={typeSelected}\n                onChangeType={onChangeType}\n                options={options}\n                loading={loading}\n                keyName={keyName}\n                setKeyName={setKeyName}\n                keyTTL={keyTTL}\n                setKeyTTL={setKeyTTL}\n              />\n\n              <Spacer size=\"xl\" />\n\n              <Divider />\n\n              <Spacer size=\"xl\" />\n\n              {typeSelected === KeyTypes.Hash && (\n                <AddKeyHash onCancel={closeAddKeyPanel} {...defaultFields} />\n              )}\n              {typeSelected === KeyTypes.ZSet && (\n                <AddKeyZset onCancel={closeAddKeyPanel} {...defaultFields} />\n              )}\n              {typeSelected === KeyTypes.Set && (\n                <AddKeySet onCancel={closeAddKeyPanel} {...defaultFields} />\n              )}\n              {typeSelected === KeyTypes.String && (\n                <AddKeyString onCancel={closeAddKeyPanel} {...defaultFields} />\n              )}\n              {typeSelected === KeyTypes.List && (\n                <AddKeyList onCancel={closeAddKeyPanel} {...defaultFields} />\n              )}\n              {typeSelected === KeyTypes.ReJSON && (\n                <>\n                  {!isContainJSONModule(modules) && (\n                    <span\n                      className={styles.helpText}\n                      data-testid=\"json-not-loaded-text\"\n                    >\n                      {HelpTexts.REJSON_SHOULD_BE_LOADED}\n                    </span>\n                  )}\n                  <AddKeyReJSON\n                    onCancel={closeAddKeyPanel}\n                    {...defaultFields}\n                  />\n                </>\n              )}\n              {typeSelected === KeyTypes.Stream && (\n                <AddKeyStream onCancel={closeAddKeyPanel} {...defaultFields} />\n              )}\n            </ContentFields>\n          </div>\n        </Col>\n        <div id=\"formFooterBar\" className=\"formFooterBar\" />\n      </Row>\n    </div>\n  )\n}\n\nexport default AddKey\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyCommonFields/AddKeyCommonFields.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport AddKeyCommonFields, { Props } from './AddKeyCommonFields'\n\nconst mockedProps = mock<Props>()\nconst options = ['one', 'two']\n\ndescribe('AddKeyCommonFields', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <AddKeyCommonFields {...instance(mockedProps)} options={options} />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should call setKeyName onChange KeyName', () => {\n    const setKeyName = jest.fn()\n    render(\n      <AddKeyCommonFields\n        {...instance(mockedProps)}\n        setKeyName={setKeyName}\n        options={options}\n      />,\n    )\n    const ttlInput = screen.getByPlaceholderText(\n      AddCommonFieldsFormConfig.keyName.placeholder,\n    )\n\n    fireEvent.change(ttlInput, { target: { value: 123 } })\n    expect(setKeyName).toBeCalledTimes(1)\n  })\n\n  it('should call setKeyTTL onChange TTL', () => {\n    const setKeyTTL = jest.fn()\n    render(\n      <AddKeyCommonFields\n        {...instance(mockedProps)}\n        setKeyTTL={setKeyTTL}\n        options={options}\n      />,\n    )\n    const ttlInput = screen.getByPlaceholderText(\n      AddCommonFieldsFormConfig.keyTTL.placeholder,\n    )\n\n    fireEvent.change(ttlInput, { target: { value: 123 } })\n    expect(setKeyTTL).toBeCalledTimes(1)\n  })\n\n  it('should properly return TTL value with wrong data', () => {\n    let ttlValue: number = 0\n    const setKeyTTL = (value: number) => {\n      ttlValue = value\n    }\n    render(\n      <AddKeyCommonFields\n        {...instance(mockedProps)}\n        // @ts-ignore\n        setKeyTTL={setKeyTTL}\n        options={options}\n      />,\n    )\n    const ttlInput = screen.getByPlaceholderText(\n      AddCommonFieldsFormConfig.keyTTL.placeholder,\n    )\n\n    fireEvent.change(ttlInput, { target: { value: 'q123' } })\n    expect(ttlValue).toBe(123)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyCommonFields/AddKeyCommonFields.tsx",
    "content": "import React from 'react'\nimport { toNumber } from 'lodash'\nimport { MAX_TTL_NUMBER, Maybe, validateTTLNumberForAddKey } from 'uiSrc/utils'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { FormFieldset } from 'uiSrc/components/base/forms/fieldset'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { AddCommonFieldsFormConfig as config } from '../constants/fields-config'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  typeSelected: string\n  onChangeType: (type: string) => void\n  options: any\n  loading: boolean\n  keyName: string\n  setKeyName: React.Dispatch<React.SetStateAction<string>>\n  keyTTL: Maybe<number>\n  setKeyTTL: React.Dispatch<React.SetStateAction<Maybe<number>>>\n}\n\nconst AddKeyCommonFields = (props: Props) => {\n  const {\n    typeSelected,\n    onChangeType = () => {},\n    options,\n    loading,\n    keyName,\n    setKeyName,\n    keyTTL,\n    setKeyTTL,\n  } = props\n\n  const handleTTLChange = (value: string) => {\n    const validatedValue = validateTTLNumberForAddKey(value)\n    if (validatedValue.toString().length) {\n      setKeyTTL(toNumber(validatedValue))\n    } else {\n      setKeyTTL(undefined)\n    }\n  }\n\n  return (\n    <div className={styles.wrapper}>\n      <Row className={styles.container} gap=\"m\">\n        <FlexItem grow>\n          <FormFieldset\n            legend={{ children: 'Select key type', display: 'hidden' }}\n          >\n            <FormField label=\"Key Type*\">\n              <RiSelect\n                options={options}\n                valueRender={({ option }): JSX.Element =>\n                  (option.inputDisplay ?? option.value) as JSX.Element\n                }\n                value={typeSelected}\n                onChange={(value: string) => onChangeType(value)}\n                disabled={loading}\n                data-testid=\"select-key-type\"\n              />\n            </FormField>\n          </FormFieldset>\n        </FlexItem>\n        <FlexItem grow>\n          <FormField label={config.keyTTL.label}>\n            <TextInput\n              name={config.keyTTL.name}\n              id={config.keyTTL.name}\n              maxLength={200}\n              min={0}\n              max={MAX_TTL_NUMBER}\n              placeholder={config.keyTTL.placeholder}\n              value={`${keyTTL ?? ''}`}\n              onChange={handleTTLChange}\n              disabled={loading}\n              autoComplete=\"off\"\n              data-testid=\"ttl\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n      <Spacer size=\"m\" />\n      <FormField label={config.keyName.label}>\n        <TextInput\n          name={config.keyName.name}\n          id={config.keyName.name}\n          value={keyName}\n          placeholder={config.keyName.placeholder}\n          onChange={setKeyName}\n          disabled={loading}\n          autoComplete=\"off\"\n          data-testid=\"key\"\n        />\n      </FormField>\n    </div>\n  )\n}\n\nexport default AddKeyCommonFields\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyCommonFields/styles.module.scss",
    "content": ".wrapper {\n  .container {\n    &:global(.euiFlexGroup.euiFlexGroup--gutterLarge) {\n      margin: -12px -12px 12px !important;\n    }\n  }\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyFooter/AddKeyFooter.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport AddKeyFooter, { Props } from './AddKeyFooter'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddKeyFooter', () => {\n  it('should render', () => {\n    expect(render(<AddKeyFooter {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyFooter/AddKeyFooter.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport ReactDOM from 'react-dom'\n\nexport interface Props {\n  children: React.ReactNode\n}\n\nconst AddKeyFooter = (props: Props) => {\n  const [el] = useState(document.createElement('div'))\n  let container: HTMLElement | null\n\n  useEffect(() => {\n    // componentDidMount\n    container = document.getElementById('formFooterBar')\n    if (container) {\n      container.appendChild(el)\n    }\n    // componentWillUnmount\n    return () => {\n      if (container) {\n        container.removeChild(el)\n      }\n    }\n  }, [])\n\n  return ReactDOM.createPortal(props.children, el)\n}\nexport default AddKeyFooter\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport AddKeyHash, { Props } from './AddKeyHash'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddKeyHash', () => {\n  it('should render', () => {\n    expect(render(<AddKeyHash {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set field name properly', () => {\n    render(<AddKeyHash {...instance(mockedProps)} />)\n    const fieldName = screen.getByTestId('field-name')\n    fireEvent.change(fieldName, { target: { value: 'field name' } })\n    expect(fieldName).toHaveValue('field name')\n  })\n\n  it('should set field value properly', () => {\n    render(<AddKeyHash {...instance(mockedProps)} />)\n    const fieldName = screen.getByTestId('field-value')\n    fireEvent.change(fieldName, { target: { value: '123' } })\n    expect(fieldName).toHaveValue('123')\n  })\n\n  it('should render add button', () => {\n    render(<AddKeyHash {...instance(mockedProps)} />)\n    expect(screen.getByTestId('add-item')).toBeTruthy()\n  })\n\n  it('should render one more field name & value inputs after click add item', () => {\n    render(<AddKeyHash {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('add-item'))\n\n    expect(screen.getAllByTestId('field-name')).toHaveLength(2)\n    expect(screen.getAllByTestId('field-value')).toHaveLength(2)\n  })\n\n  it('should clear fieldName & fieldValue after click clear button', () => {\n    render(<AddKeyHash {...instance(mockedProps)} />)\n    const fieldName = screen.getByTestId('field-name')\n    const fieldValue = screen.getByTestId('field-value')\n    fireEvent.change(fieldName, { target: { value: 'name' } })\n    fireEvent.change(fieldValue, { target: { value: 'val' } })\n    fireEvent.click(screen.getByTestId('remove-item'))\n\n    expect(fieldName).toHaveValue('')\n    expect(fieldValue).toHaveValue('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/AddKeyHash.tsx",
    "content": "import React, { FormEvent, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { toNumber } from 'lodash'\nimport {\n  isVersionHigherOrEquals,\n  Maybe,\n  stringToBuffer,\n  validateTTLNumberForAddKey,\n} from 'uiSrc/utils'\nimport { addHashKey, addKeyStateSelector } from 'uiSrc/slices/browser/keys'\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\n\nimport { CommandsVersions } from 'uiSrc/constants/commandsVersions'\nimport { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport {\n  CreateHashWithExpireDto,\n  HashFieldDto,\n} from 'apiSrc/modules/browser/hash/dto'\n\nimport { IHashFieldState, INITIAL_HASH_FIELD_STATE } from './interfaces'\nimport { AddHashFormConfig as config } from '../constants/fields-config'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nconst AddKeyHash = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n  const { loading } = useSelector(addKeyStateSelector)\n  const { version } = useSelector(connectedInstanceOverviewSelector)\n  const { [FeatureFlags.hashFieldExpiration]: hashFieldExpirationFeature } =\n    useSelector(appFeatureFlagsFeaturesSelector)\n\n  const [fields, setFields] = useState<IHashFieldState[]>([\n    { ...INITIAL_HASH_FIELD_STATE },\n  ])\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n  const lastAddedFieldName = useRef<HTMLInputElement>(null)\n  const prevCountFields = useRef<number>(0)\n\n  const isTTLAvailable =\n    hashFieldExpirationFeature?.flag &&\n    isVersionHigherOrEquals(version, CommandsVersions.HASH_TTL.since)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsFormValid(`${keyName}`.length > 0)\n  }, [keyName])\n\n  useEffect(() => {\n    if (\n      prevCountFields.current !== 0 &&\n      prevCountFields.current < fields.length\n    ) {\n      lastAddedFieldName.current?.focus()\n    }\n    prevCountFields.current = fields.length\n  }, [fields.length])\n\n  const addField = () => {\n    const lastField = fields[fields.length - 1]\n    const newState = [\n      ...fields,\n      {\n        ...INITIAL_HASH_FIELD_STATE,\n        id: lastField.id + 1,\n      },\n    ]\n    setFields(newState)\n  }\n\n  const removeField = (id: number) => {\n    const newState = fields.filter((item) => item.id !== id)\n    setFields(newState)\n  }\n\n  const clearFieldsValues = (id: number) => {\n    const newState = fields.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            fieldName: '',\n            fieldValue: '',\n            fieldTTL: undefined,\n          }\n        : item,\n    )\n    setFields(newState)\n  }\n\n  const onClickRemove = ({ id }: IHashFieldState) => {\n    if (fields.length === 1) {\n      clearFieldsValues(id)\n      return\n    }\n\n    removeField(id)\n  }\n\n  const handleFieldChange = (formField: string, id: number, value: any) => {\n    const newState = fields.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: value,\n        }\n      }\n      return item\n    })\n    setFields(newState)\n  }\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const submitData = (): void => {\n    const data: CreateHashWithExpireDto = {\n      keyName: stringToBuffer(keyName),\n      fields: fields.map((item) => {\n        const defaultFields: HashFieldDto = {\n          field: stringToBuffer(item.fieldName),\n          value: stringToBuffer(item.fieldValue),\n        }\n\n        if (isTTLAvailable && item.fieldTTL) {\n          defaultFields.expire = toNumber(item.fieldTTL)\n        }\n\n        return defaultFields\n      }),\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addHashKey(data, onCancel))\n  }\n\n  const isClearDisabled = (item: IHashFieldState): boolean =>\n    fields.length === 1 &&\n    !(item.fieldName.length || item.fieldValue.length || item.fieldTTL?.length)\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <AddMultipleFields\n        items={fields}\n        isClearDisabled={isClearDisabled}\n        onClickRemove={onClickRemove}\n        onClickAdd={addField}\n      >\n        {(item, index) => (\n          <Row align=\"center\" gap=\"m\">\n            <FlexItem grow={2}>\n              <FormField>\n                <TextInput\n                  name={`fieldName-${item.id}`}\n                  id={`fieldName-${item.id}`}\n                  placeholder={config.fieldName.placeholder}\n                  value={item.fieldName}\n                  disabled={loading}\n                  onChange={(value) =>\n                    handleFieldChange('fieldName', item.id, value)\n                  }\n                  ref={index === fields.length - 1 ? lastAddedFieldName : null}\n                  data-testid=\"field-name\"\n                />\n              </FormField>\n            </FlexItem>\n            <FlexItem grow={2}>\n              <FormField>\n                <TextInput\n                  name={`fieldValue-${item.id}`}\n                  id={`fieldValue-${item.id}`}\n                  placeholder={config.fieldValue.placeholder}\n                  value={item.fieldValue}\n                  disabled={loading}\n                  onChange={(value) =>\n                    handleFieldChange('fieldValue', item.id, value)\n                  }\n                  data-testid=\"field-value\"\n                />\n              </FormField>\n            </FlexItem>\n            {isTTLAvailable && (\n              <FlexItem grow={1}>\n                <FormField>\n                  <TextInput\n                    name={`fieldTTL-${item.id}`}\n                    id={`fieldTTL-${item.id}`}\n                    placeholder=\"Enter TTL\"\n                    value={item.fieldTTL || ''}\n                    disabled={loading}\n                    onChange={(value) =>\n                      handleFieldChange(\n                        'fieldTTL',\n                        item.id,\n                        validateTTLNumberForAddKey(value),\n                      )\n                    }\n                    data-testid=\"hash-ttl\"\n                  />\n                </FormField>\n              </FlexItem>\n            )}\n          </Row>\n        )}\n      </AddMultipleFields>\n\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        loading={loading}\n        disabled={!isFormValid}\n        actionTestId=\"add-key-hash-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeyHash\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/index.ts",
    "content": "import AddKeyHash from './AddKeyHash'\n\nexport default AddKeyHash\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyHash/interfaces.ts",
    "content": "export interface IHashFieldState {\n  fieldName: string\n  fieldValue: string\n  fieldTTL?: string\n  id: number\n}\n\nexport const INITIAL_HASH_FIELD_STATE: IHashFieldState = {\n  fieldName: '',\n  fieldValue: '',\n  id: 0,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyList/AddKeyList.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport { HEAD_DESTINATION } from 'uiSrc/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements'\nimport AddKeyList, { Props } from './AddKeyList'\nimport AddKeyFooter from '../AddKeyFooter/AddKeyFooter'\n\nconst mockedProps = mock<Props>()\n\nconst elementFindingRegex = /^element-\\d+$/\n\njest.mock('../AddKeyFooter/AddKeyFooter', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst MockAddKeyFooter = (props) => <div {...props} />\n\ndescribe('AddKeyList', () => {\n  beforeAll(() => {\n    AddKeyFooter.mockImplementation(MockAddKeyFooter)\n  })\n\n  it('should render', () => {\n    expect(render(<AddKeyList {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set value properly', () => {\n    render(<AddKeyList {...instance(mockedProps)} />)\n    const valueInput = screen.getByTestId(elementFindingRegex)\n    const value = 'list list list list list list '\n    fireEvent.change(valueInput, { target: { value } })\n    expect(valueInput).toHaveValue(value)\n  })\n\n  it('should set destination properly', () => {\n    render(<AddKeyList {...instance(mockedProps)} />)\n    const destinationSelect = screen.getByTestId('destination-select')\n    fireEvent.change(destinationSelect, { target: { value: HEAD_DESTINATION } })\n    expect(destinationSelect).toHaveValue(HEAD_DESTINATION)\n  })\n\n  it('should render disabled add key button with empty keyName', () => {\n    const { container } = render(<AddKeyList {...instance(mockedProps)} />)\n    expect(container.querySelector('.btn-add')).toBeDisabled()\n  })\n\n  it('should not be disabled add key with proper values', () => {\n    const { container } = render(\n      <AddKeyList {...instance(mockedProps)} keyName=\"name\" />,\n    )\n    expect(container.querySelector('.btn-add')).not.toBeDisabled()\n  })\n\n  it('should allow for adding multiple elements', () => {\n    render(<AddKeyList {...instance(mockedProps)} keyName=\"name\" />)\n    fireEvent.click(screen.getByTestId('add-item'))\n    const valueInputs = screen.getAllByTestId(elementFindingRegex)\n    expect([...valueInputs].length).toBe(2)\n  })\n\n  it('should not allow deleting the last element', () => {\n    render(<AddKeyList {...instance(mockedProps)} keyName=\"name\" />)\n    const deleteButtons = screen.getAllByTestId('remove-item')\n    expect(deleteButtons[0]).toBeDisabled()\n  })\n\n  it('should allow deleting of the elements after the first one', () => {\n    render(<AddKeyList {...instance(mockedProps)} keyName=\"name\" />)\n    fireEvent.click(screen.getByTestId('add-item'))\n    const deleteButtons = screen.getAllByTestId('remove-item')\n    expect(deleteButtons[1]).not.toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyList/AddKeyList.tsx",
    "content": "import React, { FormEvent, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Maybe, stringToBuffer } from 'uiSrc/utils'\nimport { addKeyStateSelector, addListKey } from 'uiSrc/slices/browser/keys'\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport {\n  optionsDestinations,\n  TAIL_DESTINATION,\n} from 'uiSrc/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport {\n  CreateListWithExpireDto,\n  ListElementDestination,\n} from 'apiSrc/modules/browser/list/dto'\n\nimport { AddListFormConfig as config } from '../constants/fields-config'\nimport AddMultipleFields from '../../add-multiple-fields'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nconst AddKeyList = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n  const [elements, setElements] = useState<string[]>([''])\n  const [destination, setDestination] =\n    useState<ListElementDestination>(TAIL_DESTINATION)\n\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n\n  const { loading } = useSelector(addKeyStateSelector)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsFormValid(keyName.length > 0)\n  }, [keyName])\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const addField = () => {\n    setElements([...elements, ''])\n  }\n\n  const onClickRemove = (_item: string, index?: number) => {\n    if (elements.length === 1) {\n      setElements([''])\n    } else {\n      setElements(elements.filter((_el, i) => i !== index))\n    }\n  }\n\n  const isClearDisabled = (item: string) =>\n    elements.length === 1 && !item.length\n\n  const handleElementChange = (value: string, index: number) => {\n    const newElements = [...elements]\n    newElements[index] = value\n    setElements(newElements)\n  }\n\n  const submitData = (): void => {\n    const data: CreateListWithExpireDto = {\n      destination,\n      keyName: stringToBuffer(keyName),\n      elements: elements.map((el) => stringToBuffer(el)),\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addListKey(data, onCancel))\n  }\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <RiSelect\n        value={destination}\n        options={optionsDestinations}\n        onChange={(value) => setDestination(value as ListElementDestination)}\n        data-testid=\"destination-select\"\n      />\n      <Spacer size=\"m\" />\n      <AddMultipleFields\n        items={elements}\n        onClickRemove={onClickRemove}\n        onClickAdd={addField}\n        isClearDisabled={isClearDisabled}\n      >\n        {(item, index) => (\n          <TextInput\n            name={`element-${index}`}\n            id={`element-${index}`}\n            placeholder={config.element.placeholder}\n            value={item}\n            disabled={loading}\n            onChange={(value) => handleElementChange(value, index)}\n            data-testid={`element-${index}`}\n          />\n        )}\n      </AddMultipleFields>\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        loading={loading}\n        disabled={!isFormValid}\n        actionTestId=\"add-key-list-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeyList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyList/index.ts",
    "content": "import AddKeyList from './AddKeyList'\n\nexport default AddKeyList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport {\n  fireEvent,\n  userEvent,\n  render,\n  screen,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport AddKeyReJSON, { Props } from './AddKeyReJSON'\nimport AddKeyFooter from '../AddKeyFooter/AddKeyFooter'\n\nconst mockedProps = mock<Props>()\n\njest.mock('../AddKeyFooter/AddKeyFooter', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst MockAddKeyFooter = (props: any) => <div {...props} />\n\ndescribe('AddKeyReJSON', () => {\n  beforeAll(() => {\n    AddKeyFooter.mockImplementation(MockAddKeyFooter)\n  })\n\n  it('should render', () => {\n    expect(render(<AddKeyReJSON {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set value properly', () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} />)\n    const valueArea = screen.getByTestId('json-value')\n    fireEvent.change(valueArea, { target: { value: '{}' } })\n    expect(valueArea).toHaveValue('{}')\n  })\n\n  it('should render add key button', () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} />)\n    expect(screen.getByTestId('add-key-json-btn')).toBeTruthy()\n  })\n\n  it('should render add button disabled with wrong json', () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} keyName=\"name\" />)\n    const valueArea = screen.getByTestId('json-value')\n    fireEvent.change(valueArea, { target: { value: '{\"12' } })\n    expect(screen.getByTestId('add-key-json-btn')).toBeDisabled()\n  })\n\n  it('should render add button not disabled', () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} keyName=\"name\" />)\n    const valueArea = screen.getByTestId('json-value')\n    fireEvent.change(valueArea, { target: { value: '{}' } })\n    expect(screen.getByTestId('add-key-json-btn')).not.toBeDisabled()\n  })\n\n  it('should call proper telemetry events after click Upload', () => {\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<AddKeyReJSON {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('upload-input-file'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.BROWSER_JSON_VALUE_IMPORT_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n  })\n\n  it('should load file', async () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} keyName=\"name\" />)\n\n    const jsonString = JSON.stringify({ a: 12 })\n    const blob = new Blob([jsonString])\n    const file = new File([blob], 'empty.json', {\n      type: 'application/JSON',\n    })\n    const fileInput = screen.getByTestId('upload-input-file')\n\n    expect(fileInput).toHaveAttribute('accept', 'application/json, text/plain')\n\n    await userEvent.upload(fileInput, file)\n\n    await waitFor(() =>\n      expect(screen.getByTestId('json-value')).toHaveValue('{\"a\":12}'),\n    )\n  })\n\n  it('should set the value from json file', async () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} keyName=\"name\" />)\n\n    const jsonString = JSON.stringify({ a: 12 })\n    const blob = new Blob([jsonString])\n    const file = new File([blob], 'empty.json', {\n      type: 'application/JSON',\n    })\n    const fileInput = screen.getByTestId('upload-input-file')\n\n    expect(fileInput).toHaveAttribute('accept', 'application/json, text/plain')\n\n    await userEvent.upload(fileInput, file)\n\n    await waitFor(() =>\n      expect(screen.getByTestId('json-value')).toHaveValue('{\"a\":12}'),\n    )\n  })\n\n  it('should set the incorrect json value from json file', async () => {\n    render(<AddKeyReJSON {...instance(mockedProps)} keyName=\"name\" />)\n\n    const jsonString = JSON.stringify('{ a: 12')\n    const blob = new Blob([jsonString])\n    const file = new File([blob], 'empty.json', {\n      type: 'application/JSON',\n    })\n    const fileInput = screen.getByTestId('upload-input-file')\n\n    await userEvent.upload(fileInput, file)\n\n    await waitFor(() =>\n      expect(screen.getByTestId('json-value')).toHaveValue('\"{ a: 12\"'),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/AddKeyReJSON.tsx",
    "content": "import React, { FormEvent, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { Maybe, stringToBuffer } from 'uiSrc/utils'\nimport { addKeyStateSelector, addReJSONKey } from 'uiSrc/slices/browser/keys'\n\nimport { MonacoJson } from 'uiSrc/components/monaco-editor'\nimport UploadFile from 'uiSrc/components/upload-file'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport { CreateRejsonRlWithExpireDto } from 'apiSrc/modules/browser/rejson-rl/dto'\n\nimport { AddJSONFormConfig as config } from '../constants/fields-config'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nconst AddKeyReJSON = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n  const { loading } = useSelector(addKeyStateSelector)\n  const [ReJSONValue, setReJSONValue] = useState<string>('')\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  useEffect(() => {\n    try {\n      JSON.parse(ReJSONValue)\n      if (keyName.length > 0) {\n        setIsFormValid(true)\n        return\n      }\n    } catch (e) {\n      setIsFormValid(false)\n    }\n\n    setIsFormValid(false)\n  }, [keyName, ReJSONValue])\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const submitData = (): void => {\n    const data: CreateRejsonRlWithExpireDto = {\n      keyName: stringToBuffer(keyName),\n      data: ReJSONValue,\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addReJSONKey(data, onCancel))\n  }\n\n  const onClick = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.BROWSER_JSON_VALUE_IMPORT_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <FormField label={config.value.label}>\n        <>\n          <MonacoJson\n            value={ReJSONValue}\n            onChange={setReJSONValue}\n            disabled={loading}\n            data-testid=\"json-value\"\n          />\n          <Row justify=\"end\">\n            <FlexItem>\n              <UploadFile\n                onClick={onClick}\n                onFileChange={setReJSONValue}\n                accept=\"application/json, text/plain\"\n              />\n            </FlexItem>\n          </Row>\n        </>\n      </FormField>\n\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        loading={loading}\n        disabled={!isFormValid}\n        actionTestId=\"add-key-json-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeyReJSON\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyReJSON/index.ts",
    "content": "import AddKeyReJSON from './AddKeyReJSON'\n\nexport default AddKeyReJSON\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport AddKeySet, { Props } from './AddKeySet'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddKeyZset', () => {\n  it('should render', () => {\n    expect(render(<AddKeySet {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set member value properly', () => {\n    render(<AddKeySet {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId('member-name')\n    fireEvent.change(memberInput, { target: { value: 'member name' } })\n    expect(memberInput).toHaveValue('member name')\n  })\n\n  it('should render add button', () => {\n    render(<AddKeySet {...instance(mockedProps)} />)\n    expect(screen.getByTestId('add-item')).toBeTruthy()\n  })\n\n  it('should render one more member input after click add item', () => {\n    render(<AddKeySet {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('add-item'))\n\n    expect(screen.getAllByTestId('member-name')).toHaveLength(2)\n  })\n\n  it('should remove one member input after add item & remove one', () => {\n    render(<AddKeySet {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('add-item'))\n\n    expect(screen.getAllByTestId('member-name')).toHaveLength(2)\n\n    const removeButtons = screen.getAllByTestId('remove-item')\n    fireEvent.click(removeButtons[1])\n\n    expect(screen.getAllByTestId('member-name')).toHaveLength(1)\n  })\n\n  it('should clear member after click clear button', () => {\n    render(<AddKeySet {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId('member-name')\n    fireEvent.change(memberInput, { target: { value: 'member' } })\n    fireEvent.click(screen.getByTestId('remove-item'))\n\n    expect(memberInput).toHaveValue('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/AddKeySet.tsx",
    "content": "import React, { FormEvent, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Maybe, stringToBuffer } from 'uiSrc/utils'\nimport { addKeyStateSelector, addSetKey } from 'uiSrc/slices/browser/keys'\n\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { CreateSetWithExpireDto } from 'apiSrc/modules/browser/set/dto'\n\nimport { INITIAL_SET_MEMBER_STATE, ISetMemberState } from './interfaces'\nimport { AddSetFormConfig as config } from '../constants/fields-config'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nconst AddKeySet = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n  const { loading } = useSelector(addKeyStateSelector)\n  const [members, setMembers] = useState<ISetMemberState[]>([\n    { ...INITIAL_SET_MEMBER_STATE },\n  ])\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n  const lastAddedMemberName = useRef<HTMLInputElement>(null)\n  const prevCountMembers = useRef<number>(0)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsFormValid(keyName.length > 0)\n  }, [keyName])\n\n  useEffect(() => {\n    if (\n      prevCountMembers.current !== 0 &&\n      prevCountMembers.current < members.length\n    ) {\n      lastAddedMemberName.current?.focus()\n    }\n    prevCountMembers.current = members.length\n  }, [members.length])\n\n  const addMember = () => {\n    const lastMember = members[members.length - 1]\n    const newState = [\n      ...members,\n      {\n        ...INITIAL_SET_MEMBER_STATE,\n        id: lastMember.id + 1,\n      },\n    ]\n    setMembers(newState)\n  }\n\n  const removeMember = (id: number) => {\n    const newState = members.filter((item) => item.id !== id)\n    setMembers(newState)\n  }\n\n  const clearMemberValues = (id: number) => {\n    const newState = members.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            name: '',\n          }\n        : item,\n    )\n    setMembers(newState)\n  }\n\n  const onClickRemove = ({ id }: ISetMemberState) => {\n    if (members.length === 1) {\n      clearMemberValues(id)\n      return\n    }\n\n    removeMember(id)\n  }\n\n  const handleMemberChange = (formField: string, id: number, value: string) => {\n    const newState = members.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: value,\n        }\n      }\n      return item\n    })\n    setMembers(newState)\n  }\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const submitData = (): void => {\n    const data: CreateSetWithExpireDto = {\n      keyName: stringToBuffer(keyName),\n      members: members.map((item) => stringToBuffer(item.name)),\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addSetKey(data, onCancel))\n  }\n\n  const isClearDisabled = (item: ISetMemberState): boolean =>\n    members.length === 1 && !item.name.length\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <AddMultipleFields\n        items={members}\n        isClearDisabled={isClearDisabled}\n        onClickRemove={onClickRemove}\n        onClickAdd={addMember}\n      >\n        {(item, index) => (\n          <Row align=\"center\">\n            <FlexItem grow>\n              <FormField>\n                <TextInput\n                  name={`member-${item.id}`}\n                  id={`member-${item.id}`}\n                  placeholder={config.member.placeholder}\n                  value={item.name}\n                  onChange={(value) =>\n                    handleMemberChange('name', item.id, value)\n                  }\n                  ref={\n                    index === members.length - 1 ? lastAddedMemberName : null\n                  }\n                  disabled={loading}\n                  data-testid=\"member-name\"\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n        )}\n      </AddMultipleFields>\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        loading={loading}\n        disabled={!isFormValid}\n        actionTestId=\"add-key-set-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeySet\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/index.ts",
    "content": "import AddKeySet from './AddKeySet'\n\nexport default AddKeySet\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeySet/interfaces.ts",
    "content": "export interface ISetMemberState {\n  name: string\n  id: number\n}\n\nexport const INITIAL_SET_MEMBER_STATE: ISetMemberState = {\n  name: '',\n  id: 0,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { instance, mock } from 'ts-mockito'\nimport AddKeyStream, { Props } from './AddKeyStream'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddKeyStream', () => {\n  it('should render', () => {\n    expect(render(<AddKeyStream {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx",
    "content": "import React, { FormEvent, useEffect, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { addStreamKey } from 'uiSrc/slices/browser/keys'\nimport {\n  entryIdRegex,\n  isRequiredStringsValid,\n  Maybe,\n  stringToBuffer,\n} from 'uiSrc/utils'\nimport { AddStreamFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport { StreamEntryFields } from 'uiSrc/pages/browser/modules/key-details/components/stream-details/add-stream-entity'\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport { CreateStreamDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nexport const INITIAL_STREAM_FIELD_STATE = {\n  name: '',\n  value: '',\n  id: 0,\n}\n\nconst AddKeyStream = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n\n  const [entryIdError, setEntryIdError] = useState('')\n  const [entryID, setEntryID] = useState<string>('*')\n  const [fields, setFields] = useState<any[]>([\n    { ...INITIAL_STREAM_FIELD_STATE },\n  ])\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    const isValid = isRequiredStringsValid(keyName) && !entryIdError\n    setIsFormValid(isValid)\n  }, [keyName, fields, entryIdError])\n\n  useEffect(() => {\n    validateEntryID()\n  }, [entryID])\n\n  const validateEntryID = () => {\n    setEntryIdError(\n      entryIdRegex.test(entryID)\n        ? ''\n        : `${config.entryId.name} format is incorrect`,\n    )\n  }\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const submitData = (): void => {\n    const data: CreateStreamDto = {\n      keyName: stringToBuffer(keyName),\n      entries: [\n        {\n          id: entryID,\n          fields: [\n            ...fields.map(({ name, value }) => ({\n              name: stringToBuffer(name),\n              value: stringToBuffer(value),\n            })),\n          ],\n        },\n      ],\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addStreamKey(data, onCancel))\n  }\n\n  return (\n    <form className={styles.container} onSubmit={onFormSubmit}>\n      <StreamEntryFields\n        entryID={entryID}\n        entryIdError={entryIdError}\n        fields={fields}\n        setFields={setFields}\n        setEntryID={setEntryID}\n      />\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        disabled={!isFormValid}\n        actionTestId=\"add-key-hash-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeyStream\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/index.ts",
    "content": "import AddKeyStream from './AddKeyStream'\n\nexport default AddKeyStream\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/styles.module.scss",
    "content": ".container {\n\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyString/AddKeyString.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport AddKeyString, { Props } from './AddKeyString'\nimport AddKeyFooter from '../AddKeyFooter/AddKeyFooter'\n\nconst mockedProps = mock<Props>()\n\njest.mock('../AddKeyFooter/AddKeyFooter', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst MockAddKeyFooter = (props) => <div {...props} />\n\ndescribe('AddKeyString', () => {\n  beforeAll(() => {\n    AddKeyFooter.mockImplementation(MockAddKeyFooter)\n  })\n\n  it('should render', () => {\n    expect(render(<AddKeyString {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set value properly', () => {\n    render(<AddKeyString {...instance(mockedProps)} />)\n    const valueInput = screen.getByTestId('string-value')\n    const value = 'string stringstringstringstringstring stringstring string'\n    fireEvent.change(valueInput, { target: { value } })\n    expect(valueInput).toHaveValue(value)\n  })\n\n  it('should render disabled add key button with empty keyName', () => {\n    const { container } = render(<AddKeyString {...instance(mockedProps)} />)\n    expect(container.querySelector('.btn-add')).toBeDisabled()\n  })\n\n  it('should not be disabled add key with proper values', () => {\n    const { container } = render(\n      <AddKeyString {...instance(mockedProps)} keyName=\"name\" />,\n    )\n    expect(container.querySelector('.btn-add')).not.toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyString/AddKeyString.tsx",
    "content": "import React, { FormEvent, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Maybe, stringToBuffer } from 'uiSrc/utils'\n\nimport { addKeyStateSelector, addStringKey } from 'uiSrc/slices/browser/keys'\n\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { TextArea } from 'uiSrc/components/base/inputs'\nimport { SetStringWithExpireDto } from 'apiSrc/modules/browser/string/dto'\nimport { AddStringFormConfig as config } from '../constants/fields-config'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nconst AddKeyString = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n  const { loading } = useSelector(addKeyStateSelector)\n  const [value, setValue] = useState<string>('')\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsFormValid(keyName.length > 0)\n  }, [keyName])\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const submitData = (): void => {\n    const data: SetStringWithExpireDto = {\n      keyName: stringToBuffer(keyName),\n      value: stringToBuffer(value),\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addStringKey(data, onCancel))\n  }\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <FormField label={config.value.label}>\n        <TextArea\n          name=\"value\"\n          id=\"value\"\n          placeholder={config.value.placeholder}\n          value={value}\n          onChange={setValue}\n          disabled={loading}\n          data-testid=\"string-value\"\n        />\n      </FormField>\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        loading={loading}\n        disabled={!isFormValid}\n        actionTestId=\"add-key-string-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeyString\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyString/index.ts",
    "content": "import AddKeyString from './AddKeyString'\n\nexport default AddKeyString\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport AddKeyZset, { Props } from './AddKeyZset'\n\nconst MEMBER_SCORE = 'member-score'\nconst MEMBER_NAME = 'member-name'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddKeyZset', () => {\n  it('should render', () => {\n    expect(render(<AddKeyZset {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set member value properly', () => {\n    render(<AddKeyZset {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId(MEMBER_NAME)\n    fireEvent.change(memberInput, { target: { value: 'member name' } })\n    expect(memberInput).toHaveValue('member name')\n  })\n\n  it('should set score value properly if input wrong value', () => {\n    render(<AddKeyZset {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    expect(scoreInput).toHaveValue('100')\n  })\n\n  it('should set by blur score value properly if input wrong value', () => {\n    render(<AddKeyZset {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '.1' } })\n    fireEvent.focusOut(scoreInput)\n    expect(scoreInput).toHaveValue('0.1')\n  })\n\n  it('should render add button after input score', () => {\n    render(<AddKeyZset {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    expect(screen.getByTestId('add-item')).toBeTruthy()\n  })\n\n  it('should render one more member & score inputs after click add item', () => {\n    render(<AddKeyZset {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    fireEvent.click(screen.getByTestId('add-item'))\n\n    expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(2)\n    expect(screen.getAllByTestId(MEMBER_SCORE)).toHaveLength(2)\n  })\n\n  it('should clear member & score after click clear button', () => {\n    render(<AddKeyZset {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId(MEMBER_NAME)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(memberInput, { target: { value: 'member' } })\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    fireEvent.click(screen.getByTestId('remove-item'))\n\n    expect(memberInput).toHaveValue('')\n    expect(scoreInput).toHaveValue('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/AddKeyZset.tsx",
    "content": "import React, { FormEvent, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { toNumber } from 'lodash'\nimport { Maybe, stringToBuffer, validateScoreNumber } from 'uiSrc/utils'\nimport { isNaNConvertedString } from 'uiSrc/utils/numbers'\nimport { addKeyStateSelector, addZsetKey } from 'uiSrc/slices/browser/keys'\n\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\nimport { ISetMemberState } from 'uiSrc/pages/browser/components/add-key/AddKeySet/interfaces'\nimport {\n  INITIAL_ZSET_MEMBER_STATE,\n  IZsetMemberState,\n} from 'uiSrc/pages/browser/components/add-key/AddKeyZset/interfaces'\nimport { ActionFooter } from 'uiSrc/pages/browser/components/action-footer'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { CreateZSetWithExpireDto } from 'apiSrc/modules/browser/z-set/dto'\nimport { AddZsetFormConfig as config } from '../constants/fields-config'\n\nexport interface Props {\n  keyName: string\n  keyTTL: Maybe<number>\n  onCancel: (isCancelled?: boolean) => void\n}\n\nconst AddKeyZset = (props: Props) => {\n  const { keyName = '', keyTTL, onCancel } = props\n  const { loading } = useSelector(addKeyStateSelector)\n  const [members, setMembers] = useState<IZsetMemberState[]>([\n    { ...INITIAL_ZSET_MEMBER_STATE },\n  ])\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n  const lastAddedMemberName = useRef<HTMLInputElement>(null)\n  const prevCountMembers = useRef<number>(0)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    members.every((member) => {\n      if (!(keyName.length && member.score?.toString().length)) {\n        setIsFormValid(false)\n        return false\n      }\n\n      if (!isNaNConvertedString(member.score)) {\n        setIsFormValid(true)\n        return true\n      }\n\n      setIsFormValid(false)\n      return false\n    })\n  }, [keyName, members])\n\n  useEffect(() => {\n    if (\n      prevCountMembers.current !== 0 &&\n      prevCountMembers.current < members.length\n    ) {\n      lastAddedMemberName.current?.focus()\n    }\n    prevCountMembers.current = members.length\n  }, [members.length])\n\n  const addMember = () => {\n    const lastMember = members[members.length - 1]\n    const newState = [\n      ...members,\n      {\n        ...INITIAL_ZSET_MEMBER_STATE,\n        id: lastMember.id + 1,\n      },\n    ]\n    setMembers(newState)\n  }\n\n  const removeMember = (id: number) => {\n    const newState = members.filter((item) => item.id !== id)\n    setMembers(newState)\n  }\n\n  const clearMemberValues = (id: number) => {\n    const newState = members.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            name: '',\n            score: '',\n          }\n        : item,\n    )\n    setMembers(newState)\n  }\n\n  const onClickRemove = ({ id }: ISetMemberState) => {\n    if (members.length === 1) {\n      clearMemberValues(id)\n      return\n    }\n\n    removeMember(id)\n  }\n\n  const handleMemberChange = (formField: string, id: number, value: any) => {\n    let validatedValue = value\n    if (formField === 'score') {\n      validatedValue = validateScore(value)\n    }\n    const newState = members.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: validatedValue,\n        }\n      }\n      return item\n    })\n    setMembers(newState)\n  }\n\n  const validateScore = (value: any) => {\n    const validatedValue = validateScoreNumber(value)\n    return validatedValue.toString().length ? validatedValue : ''\n  }\n\n  const handleScoreBlur = (item: IZsetMemberState) => {\n    const { score } = item\n    const newState = members.map((currentItem) => {\n      if (currentItem.id !== item.id) {\n        return currentItem\n      }\n      if (isNaNConvertedString(score)) {\n        return {\n          ...currentItem,\n          score: '',\n        }\n      }\n      if (score.length) {\n        return {\n          ...currentItem,\n          score: toNumber(score).toString(),\n        }\n      }\n      return currentItem\n    })\n    setMembers(newState)\n  }\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    if (isFormValid) {\n      submitData()\n    }\n  }\n\n  const submitData = (): void => {\n    const data: CreateZSetWithExpireDto = {\n      keyName: stringToBuffer(keyName),\n      members: members.map((item) => ({\n        name: stringToBuffer(item.name),\n        score: toNumber(item.score),\n      })),\n    }\n    if (keyTTL !== undefined) {\n      data.expire = keyTTL\n    }\n    dispatch(addZsetKey(data, onCancel))\n  }\n\n  const isClearDisabled = (item: IZsetMemberState): boolean =>\n    members.length === 1 && !(item.name.length || item.score.length)\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <AddMultipleFields\n        items={members}\n        isClearDisabled={isClearDisabled}\n        onClickRemove={onClickRemove}\n        onClickAdd={addMember}\n      >\n        {(item, index) => (\n          <Row align=\"center\" gap=\"m\">\n            <FlexItem grow>\n              <FormField>\n                <TextInput\n                  name={`member-${item.id}`}\n                  id={`member-${item.id}`}\n                  placeholder={config.member.placeholder}\n                  value={item.name}\n                  onChange={(value) =>\n                    handleMemberChange('name', item.id, value)\n                  }\n                  ref={\n                    index === members.length - 1 ? lastAddedMemberName : null\n                  }\n                  disabled={loading}\n                  data-testid=\"member-name\"\n                />\n              </FormField>\n            </FlexItem>\n            <FlexItem grow>\n              <FormField>\n                <TextInput\n                  name={`score-${item.id}`}\n                  id={`score-${item.id}`}\n                  maxLength={200}\n                  placeholder={config.score.placeholder}\n                  value={item.score}\n                  onChange={(value) =>\n                    handleMemberChange('score', item.id, value)\n                  }\n                  onBlur={() => {\n                    handleScoreBlur(item)\n                  }}\n                  disabled={loading}\n                  data-testid=\"member-score\"\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n        )}\n      </AddMultipleFields>\n\n      <ActionFooter\n        onCancel={() => onCancel(true)}\n        onAction={submitData}\n        actionText=\"Add Key\"\n        loading={loading}\n        disabled={!isFormValid}\n        actionTestId=\"add-key-zset-btn\"\n      />\n    </form>\n  )\n}\n\nexport default AddKeyZset\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/index.ts",
    "content": "import AddKeyZset from './AddKeyZset'\n\nexport default AddKeyZset\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/AddKeyZset/interfaces.ts",
    "content": "export interface IZsetMemberState {\n  name: string\n  score: string\n  id: number\n}\n\nexport const INITIAL_ZSET_MEMBER_STATE: IZsetMemberState = {\n  name: '',\n  score: '',\n  id: 0,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/constants/fields-config.ts",
    "content": "interface IFormField {\n  id?: string\n  name: string\n  isRequire: boolean\n  label: string\n  placeholder: string\n}\n\nexport interface IAddCommonFieldsFormConfig {\n  keyName: IFormField\n  keyTTL: IFormField\n}\n\nexport const AddCommonFieldsFormConfig: IAddCommonFieldsFormConfig = {\n  keyName: {\n    name: 'keyName',\n    isRequire: true,\n    label: 'Key Name*',\n    placeholder: 'Enter Key Name',\n  },\n  keyTTL: {\n    name: 'keyTTL',\n    isRequire: false,\n    label: 'TTL',\n    placeholder: 'No limit',\n  },\n}\n\ninterface IAddHashFormConfig {\n  fieldName: IFormField\n  fieldValue: IFormField\n}\n\nexport const AddHashFormConfig: IAddHashFormConfig = {\n  fieldName: {\n    name: 'fieldName',\n    isRequire: false,\n    label: 'Field',\n    placeholder: 'Enter Field',\n  },\n  fieldValue: {\n    name: 'fieldValue',\n    isRequire: false,\n    label: 'Value',\n    placeholder: 'Enter Value',\n  },\n}\n\ninterface IAddZsetFormConfig {\n  score: IFormField\n  member: IFormField\n}\n\nexport const AddZsetFormConfig: IAddZsetFormConfig = {\n  score: {\n    name: 'score',\n    isRequire: true,\n    label: 'Score*',\n    placeholder: 'Enter Score*',\n  },\n  member: {\n    name: 'member',\n    isRequire: false,\n    label: 'Member',\n    placeholder: 'Enter Member',\n  },\n}\n\ninterface IAddSetFormConfig {\n  member: IFormField\n}\n\nexport const AddSetFormConfig: IAddSetFormConfig = {\n  member: {\n    name: 'member',\n    isRequire: false,\n    label: 'Member',\n    placeholder: 'Enter Member',\n  },\n}\n\ninterface IAddStringFormConfig {\n  value: IFormField\n}\n\nexport const AddStringFormConfig: IAddStringFormConfig = {\n  value: {\n    name: 'value',\n    isRequire: false,\n    label: 'Value',\n    placeholder: 'Enter Value',\n  },\n}\n\ninterface IAddListFormConfig {\n  element: IFormField\n  count: IFormField\n}\n\nexport const AddListFormConfig: IAddListFormConfig = {\n  element: {\n    name: 'element',\n    isRequire: false,\n    label: 'Element',\n    placeholder: 'Enter Element',\n  },\n  count: {\n    name: 'count',\n    isRequire: true,\n    label: 'Count',\n    placeholder: 'Enter Count*',\n  },\n}\n\ninterface IAddJSONFormConfig {\n  value: IFormField\n}\n\nexport const AddJSONFormConfig: IAddJSONFormConfig = {\n  value: {\n    name: 'value',\n    isRequire: false,\n    label: 'Value*',\n    placeholder: 'Enter JSON',\n  },\n}\n\ninterface IAddStreamFormConfig {\n  entryId: IFormField\n  name: IFormField\n  value: IFormField\n}\n\nexport const AddStreamFormConfig: IAddStreamFormConfig = {\n  entryId: {\n    id: 'entryId',\n    name: 'Entry ID',\n    isRequire: true,\n    label: 'Entry ID*',\n    placeholder: 'Enter Entry ID',\n  },\n  name: {\n    id: 'name',\n    name: 'Field Name',\n    isRequire: false,\n    label: 'Field',\n    placeholder: 'Enter Field',\n  },\n  value: {\n    id: 'value',\n    name: 'Field Value',\n    isRequire: false,\n    label: 'Value',\n    placeholder: 'Enter Value',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/constants/key-type-options.ts",
    "content": "import { GROUP_TYPES_COLORS, KeyTypes } from 'uiSrc/constants'\n\nexport const ADD_KEY_TYPE_OPTIONS = [\n  {\n    text: 'Hash',\n    value: KeyTypes.Hash,\n    color: GROUP_TYPES_COLORS[KeyTypes.Hash],\n  },\n  {\n    text: 'List',\n    value: KeyTypes.List,\n    color: GROUP_TYPES_COLORS[KeyTypes.List],\n  },\n  {\n    text: 'Set',\n    value: KeyTypes.Set,\n    color: GROUP_TYPES_COLORS[KeyTypes.Set],\n  },\n  {\n    text: 'Sorted Set',\n    value: KeyTypes.ZSet,\n    color: GROUP_TYPES_COLORS[KeyTypes.ZSet],\n  },\n  {\n    text: 'String',\n    value: KeyTypes.String,\n    color: GROUP_TYPES_COLORS[KeyTypes.String],\n  },\n  {\n    text: 'JSON',\n    value: KeyTypes.ReJSON,\n    color: GROUP_TYPES_COLORS[KeyTypes.ReJSON],\n  },\n  {\n    text: 'Stream',\n    value: KeyTypes.Stream,\n    color: GROUP_TYPES_COLORS[KeyTypes.Stream],\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss",
    "content": ".page {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.contentWrapper {\n  background-color: var(--euiColorEmptyShade);\n  border-bottom-width: 0;\n}\n\n:global(.show-cli) .contentWrapper {\n  border-bottom-width: 1px;\n}\n\n.content {\n  min-height: 100%;\n  height: 1px !important;\n  width: 100%;\n  position: relative;\n  padding: 24px 18px 96px 18px;\n  scroll-padding-bottom: 80px;\n\n  @media screen and (max-width: 767px) {\n    scroll-padding-bottom: 96px;\n  }\n}\n\n.scrollContainer {\n  scroll-padding-bottom: 60px;\n\n  // move scrollbar outside\n  margin: 0 -16px;\n  padding: 0 16px;\n}\n\n.contentFields {\n  max-width: 680px;\n  margin: 0 auto;\n  width: 100%;\n}\n\n.helpText {\n  color: var(--euiTextSubduedColor);\n  display: block;\n  margin-bottom: 12px;\n  font: normal normal normal 14px/24px Graphik, sans-serif;\n}\n\n.closeKeyTooltip {\n  position: absolute;\n  top: 22px;\n  right: 18px;\n\n  svg {\n    width: 20px;\n    height: 20px;\n  }\n}\n\n.backBtn:global(.euiButton.euiButton--fill.euiButton--secondary) {\n  background-color: var(--browserComponentActive) !important;\n  border-color: var(--browserComponentActive) !important;\n  color: var(--buttonSecondaryTextColor) !important;\n\n  position: absolute;\n  top: 22px;\n  right: 18px;\n\n  &:hover, &:focus {\n    color: var(--buttonSecondaryTextColor) !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport AddMultipleFields from './AddMultipleFields'\n\nconst testItems1 = [{ id: '0', field: '' }]\n\ndescribe('AddMultipleFields', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <AddMultipleFields\n          items={testItems1}\n          isClearDisabled={() => false}\n          onClickAdd={jest.fn()}\n          onClickRemove={jest.fn()}\n        >\n          {() => <div />}\n        </AddMultipleFields>,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const ItemsWrapper = styled(Col)`\n  overflow: auto;\n  flex: 1;\n  min-height: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-multiple-fields/AddMultipleFields.tsx",
    "content": "import React from 'react'\n\nimport { DeleteIcon, PlusIcon } from 'uiSrc/components/base/icons'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  ActionIconButton,\n  IconButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { HorizontalSpacer } from 'uiSrc/components/base/layout'\nimport { RiTooltip } from 'uiSrc/components'\nimport { ItemsWrapper } from './AddMultipleFields.styles'\n\nexport interface Props<T> {\n  items: T[]\n  children: (item: T, index: number) => React.ReactNode\n  isClearDisabled: (item: T, index?: number) => boolean\n  onClickRemove: (item: T, index?: number) => void\n  onClickAdd: () => void\n}\n\nconst AddMultipleFields = <T,>(props: Props<T>) => {\n  const { items, children, isClearDisabled, onClickRemove, onClickAdd } = props\n\n  const renderItem = (child: React.ReactNode, item: T, index?: number) => (\n    <FlexItem key={index} grow>\n      <Row align=\"center\" gap=\"m\">\n        <FlexItem grow>{child}</FlexItem>\n        <FlexItem>\n          <RiTooltip content=\"Remove\" position=\"left\">\n            <IconButton\n              icon={DeleteIcon}\n              disabled={isClearDisabled(item, index)}\n              aria-label=\"Remove Item\"\n              onClick={() => onClickRemove(item, index)}\n              data-testid=\"remove-item\"\n            />\n          </RiTooltip>\n        </FlexItem>\n      </Row>\n    </FlexItem>\n  )\n\n  return (\n    <Col gap=\"m\">\n      <ItemsWrapper gap=\"m\">\n        {items.map((item, index) =>\n          renderItem(children(item, index), item, index),\n        )}\n      </ItemsWrapper>\n      <Row align=\"center\" justify=\"end\">\n        <RiTooltip content=\"Add\" position=\"left\" delay={500}>\n          <ActionIconButton\n            variant=\"secondary\"\n            icon={PlusIcon}\n            aria-label=\"Add new item\"\n            onClick={onClickAdd}\n            data-testid=\"add-item\"\n          />\n        </RiTooltip>\n        <HorizontalSpacer size=\"l\" />\n      </Row>\n      <Spacer size=\"s\" />\n    </Col>\n  )\n}\n\nexport default AddMultipleFields\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/add-multiple-fields/index.ts",
    "content": "import AddMultipleFields from './AddMultipleFields'\n\nexport default AddMultipleFields\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanel.tsx",
    "content": "import React from 'react'\n\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport KeysBrowserPanel from '../keys-browser-panel'\nimport BrowserLeftPanelLegacy from './BrowserLeftPanelLegacy'\n\nexport interface Props {\n  selectedKey: Nullable<RedisResponseBuffer>\n  selectKey: ({ rowData }: { rowData: any }) => void\n  removeSelectedKey: () => void\n  handleAddKeyPanel: (value: boolean) => void\n  handleBulkActionsPanel: (value: boolean) => void\n}\n\nconst BrowserLeftPanel = (props: Props) => (\n  <FeatureFlagComponent\n    name={FeatureFlags.devBrowser}\n    otherwise={<BrowserLeftPanelLegacy {...props} />}\n  >\n    <KeysBrowserPanel {...props} />\n  </FeatureFlagComponent>\n)\n\nexport default React.memo(BrowserLeftPanel)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-left-panel/BrowserLeftPanelLegacy.tsx",
    "content": "import React, { useCallback, useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  appContextBrowser,\n  appContextSelector,\n  setBrowserKeyListDataLoaded,\n} from 'uiSrc/slices/app/context'\nimport {\n  fetchKeys,\n  fetchMoreKeys,\n  keysDataSelector,\n  keysSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { setConnectedInstanceId } from 'uiSrc/slices/instances/instances'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport {\n  redisearchDataSelector,\n  redisearchListSelector,\n  redisearchSelector,\n} from 'uiSrc/slices/browser/redisearch'\nimport { isEqualBuffers, Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport KeyList from '../key-list'\nimport KeyTree from '../key-tree'\nimport KeysHeader from '../keys-header'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  selectedKey: Nullable<RedisResponseBuffer>\n  selectKey: ({ rowData }: { rowData: any }) => void\n  removeSelectedKey: () => void\n  handleAddKeyPanel: (value: boolean) => void\n  handleBulkActionsPanel: (value: boolean) => void\n}\n\nconst BrowserLeftPanelLegacy = (props: Props) => {\n  const {\n    selectedKey,\n    selectKey,\n    removeSelectedKey,\n    handleAddKeyPanel,\n    handleBulkActionsPanel,\n  } = props\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const patternKeysState = useSelector(keysDataSelector)\n  const redisearchKeysState = useSelector(redisearchDataSelector)\n  const { loading: redisearchLoading, isSearched: redisearchIsSearched } =\n    useSelector(redisearchSelector)\n  const { loading: redisearchListLoading } = useSelector(redisearchListSelector)\n  const {\n    loading: patternLoading,\n    viewType,\n    searchMode,\n    isSearched: patternIsSearched,\n    filter,\n    deleting,\n    error: keysError,\n  } = useSelector(keysSelector)\n  const { contextInstanceId } = useSelector(appContextSelector)\n  const {\n    keyList: {\n      isDataPatternLoaded,\n      isDataRedisearchLoaded,\n      scrollPatternTopPosition,\n      scrollRedisearchTopPosition,\n    },\n  } = useSelector(appContextBrowser)\n\n  const keyListRef = useRef<any>()\n\n  const dispatch = useDispatch()\n\n  const isDataLoaded =\n    searchMode === SearchMode.Pattern\n      ? isDataPatternLoaded\n      : isDataRedisearchLoaded\n  const keysState =\n    searchMode === SearchMode.Pattern ? patternKeysState : redisearchKeysState\n  const loading =\n    searchMode === SearchMode.Pattern ? patternLoading : redisearchLoading\n  const headerLoading =\n    searchMode === SearchMode.Pattern ? patternLoading : redisearchListLoading\n  const isSearched =\n    searchMode === SearchMode.Pattern ? patternIsSearched : redisearchIsSearched\n  const scrollTopPosition =\n    searchMode === SearchMode.Pattern\n      ? scrollPatternTopPosition\n      : scrollRedisearchTopPosition\n  const commonFilterType =\n    searchMode === SearchMode.Pattern ? filter : keysState.keys?.[0]?.type\n\n  useEffect(() => {\n    if (\n      (!isDataLoaded || contextInstanceId !== instanceId) &&\n      searchMode === SearchMode.Pattern\n    ) {\n      loadKeys(viewType)\n    }\n  }, [searchMode, isDataLoaded])\n\n  const loadKeys = useCallback(\n    (keyViewType: KeyViewType = KeyViewType.Browser) => {\n      dispatch(setConnectedInstanceId(instanceId))\n\n      dispatch(\n        fetchKeys(\n          {\n            searchMode,\n            cursor: '0',\n            count:\n              keyViewType === KeyViewType.Browser\n                ? SCAN_COUNT_DEFAULT\n                : SCAN_TREE_COUNT_DEFAULT,\n          },\n          () => dispatch(setBrowserKeyListDataLoaded(searchMode, true)),\n          () => dispatch(setBrowserKeyListDataLoaded(searchMode, false)),\n        ),\n      )\n    },\n    [searchMode],\n  )\n\n  const loadMoreItems = (\n    oldKeys: IKeyPropTypes[],\n    { startIndex, stopIndex }: { startIndex: number; stopIndex: number },\n  ) => {\n    if (keysState.nextCursor !== '0') {\n      dispatch(\n        fetchMoreKeys(\n          searchMode,\n          oldKeys,\n          keysState.nextCursor,\n          stopIndex - startIndex + 1,\n        ),\n      )\n    }\n  }\n\n  const handleScanMoreClick = (config: {\n    startIndex: number\n    stopIndex: number\n  }) => {\n    keyListRef.current?.handleLoadMoreItems?.(config)\n  }\n\n  const onDeleteKey = useCallback(\n    (key: RedisResponseBuffer) => {\n      if (isEqualBuffers(key, selectedKey)) {\n        removeSelectedKey()\n      }\n    },\n    [selectedKey],\n  )\n  return (\n    <div className={styles.container}>\n      <KeysHeader\n        keysState={keysState}\n        loading={headerLoading}\n        isSearched={isSearched}\n        loadKeys={loadKeys}\n        handleScanMoreClick={handleScanMoreClick}\n        nextCursor={keysState.nextCursor}\n      />\n      {keysError && (\n        <div className={styles.error}>\n          <div>{keysError}</div>\n        </div>\n      )}\n      {viewType === KeyViewType.Browser && !keysError && (\n        <KeyList\n          hideFooter\n          ref={keyListRef}\n          keysState={keysState}\n          loading={loading}\n          scrollTopPosition={scrollTopPosition}\n          commonFilterType={commonFilterType as Nullable<KeyTypes>}\n          loadMoreItems={loadMoreItems}\n          selectKey={selectKey}\n          onDelete={onDeleteKey}\n          onAddKeyPanel={handleAddKeyPanel}\n        />\n      )}\n      {viewType === KeyViewType.Tree && !keysError && (\n        <KeyTree\n          ref={keyListRef}\n          keysState={keysState}\n          loading={loading}\n          commonFilterType={commonFilterType as Nullable<KeyTypes>}\n          selectKey={selectKey}\n          loadMoreItems={loadMoreItems}\n          onDelete={onDeleteKey}\n          deleting={deleting}\n          onAddKeyPanel={handleAddKeyPanel}\n          onBulkActionsPanel={handleBulkActionsPanel}\n        />\n      )}\n    </div>\n  )\n}\n\nexport default React.memo(BrowserLeftPanelLegacy)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-left-panel/index.ts",
    "content": "import BrowserLeftPanel from './BrowserLeftPanel'\n\nexport default BrowserLeftPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-left-panel/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.error {\n  display: flex;\n  flex-direction: column;\n  background-color: var(--euiColorEmptyShade);\n  border-top: 1px solid var(--euiColorLightShade);\n  flex-grow: 1;\n  overflow: hidden;\n  align-items: center;\n  justify-content: center;\n  color: var(--euiColorColorWarning);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  initialStateDefault,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport BrowserRightPanel from 'uiSrc/pages/browser/components/browser-right-panel/index'\n\ndescribe('BrowserRightPanel', () => {\n  it('should show feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(\n      <BrowserRightPanel\n        selectedKey={null}\n        setSelectedKey={jest.fn()}\n        arePanelsCollapsed={false}\n        isAddKeyPanelOpen={false}\n        handleAddKeyPanel={jest.fn()}\n        isBulkActionsPanelOpen\n        handleBulkActionsPanel={jest.fn()}\n        closeRightPanels={jest.fn()}\n      />,\n      {\n        store: mockStore(initialStoreState),\n      },\n    )\n    expect(screen.queryByTestId('bulk-actions-content')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(\n      <BrowserRightPanel\n        selectedKey={null}\n        setSelectedKey={jest.fn()}\n        arePanelsCollapsed={false}\n        isAddKeyPanelOpen={false}\n        handleAddKeyPanel={jest.fn()}\n        isBulkActionsPanelOpen\n        handleBulkActionsPanel={jest.fn()}\n        closeRightPanels={jest.fn()}\n      />,\n      {\n        store: mockStore(initialStoreState),\n      },\n    )\n    expect(screen.queryByTestId('bulk-actions-content')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-right-panel/BrowserRightPanel.tsx",
    "content": "import { every } from 'lodash'\nimport React, { useCallback } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport AddKey from 'uiSrc/pages/browser/components/add-key/AddKey'\nimport BulkActions from 'uiSrc/pages/browser/components/bulk-actions'\nimport { KeyDetails } from 'uiSrc/pages/browser/modules'\n\nimport {\n  keysDataSelector,\n  keysSelector,\n  selectedKeyDataSelector,\n  toggleBrowserFullScreen,\n} from 'uiSrc/slices/browser/keys'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { Nullable } from 'uiSrc/utils'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\n\nexport interface Props {\n  selectedKey: Nullable<RedisResponseBuffer>\n  setSelectedKey: (keyName: Nullable<RedisResponseBuffer>) => void\n  arePanelsCollapsed: boolean\n  isAddKeyPanelOpen: boolean\n  handleAddKeyPanel: (value: boolean) => void\n  isBulkActionsPanelOpen: boolean\n  handleBulkActionsPanel: (value: boolean) => void\n  closeRightPanels: () => void\n}\n\nconst BrowserRightPanel = (props: Props) => {\n  const {\n    selectedKey,\n    arePanelsCollapsed,\n    setSelectedKey,\n    isAddKeyPanelOpen,\n    handleAddKeyPanel,\n    isBulkActionsPanelOpen,\n    handleBulkActionsPanel,\n    closeRightPanels,\n  } = props\n\n  const { isBrowserFullScreen, viewType } = useSelector(keysSelector)\n  const { total, lastRefreshTime: keysLastRefreshTime } =\n    useSelector(keysDataSelector)\n  const { type, length } = useSelector(selectedKeyDataSelector) ?? {\n    type: '',\n    length: 0,\n  }\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  const closePanel = () => {\n    dispatch(toggleBrowserFullScreen(true))\n\n    setSelectedKey(null)\n    closeRightPanels()\n  }\n\n  const handleToggleFullScreen = () => {\n    dispatch(toggleBrowserFullScreen())\n\n    const browserViewEvent = !isBrowserFullScreen\n      ? TelemetryEvent.BROWSER_KEY_DETAILS_FULL_SCREEN_ENABLED\n      : TelemetryEvent.BROWSER_KEY_DETAILS_FULL_SCREEN_DISABLED\n    const treeViewEvent = !isBrowserFullScreen\n      ? TelemetryEvent.TREE_VIEW_KEY_DETAILS_FULL_SCREEN_ENABLED\n      : TelemetryEvent.TREE_VIEW_KEY_DETAILS_FULL_SCREEN_DISABLED\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        databaseId: instanceId,\n        keyType: type,\n        length,\n      },\n    })\n  }\n\n  const handleEditKey = (\n    _key: RedisResponseBuffer,\n    newKey: RedisResponseBuffer,\n  ) => {\n    setSelectedKey(newKey)\n  }\n\n  const onEditKey = useCallback(\n    (key: RedisResponseBuffer, newKey: RedisResponseBuffer) =>\n      handleEditKey(key, newKey),\n    [],\n  )\n\n  const onSelectKey = useCallback(() => setSelectedKey(null), [])\n\n  return (\n    <>\n      {every([!isAddKeyPanelOpen, !isBulkActionsPanelOpen], Boolean) && (\n        <KeyDetails\n          isFullScreen={isBrowserFullScreen}\n          arePanelsCollapsed={arePanelsCollapsed}\n          onToggleFullScreen={handleToggleFullScreen}\n          keyProp={selectedKey}\n          onCloseKey={closePanel}\n          onEditKey={onEditKey}\n          onRemoveKey={onSelectKey}\n          totalKeys={total}\n          keysLastRefreshTime={keysLastRefreshTime}\n        />\n      )}\n      {isAddKeyPanelOpen && !isBulkActionsPanelOpen && (\n        <AddKey\n          onAddKeyPanel={handleAddKeyPanel}\n          onClosePanel={closePanel}\n          arePanelsCollapsed={arePanelsCollapsed}\n        />\n      )}\n      {isBulkActionsPanelOpen && !isAddKeyPanelOpen && (\n        <FeatureFlagComponent name={FeatureFlags.envDependent}>\n          <BulkActions\n            isFullScreen={isBrowserFullScreen}\n            arePanelsCollapsed={arePanelsCollapsed}\n            onClosePanel={closePanel}\n            onBulkActionsPanel={handleBulkActionsPanel}\n            onToggleFullScreen={handleToggleFullScreen}\n          />\n        </FeatureFlagComponent>\n      )}\n    </>\n  )\n}\n\nexport default React.memo(BrowserRightPanel)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-right-panel/index.ts",
    "content": "import BrowserRightPanel from './BrowserRightPanel'\n\nexport default BrowserRightPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-search-panel/BrowserSearchPanel.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { isRedisearchAvailable, getRedisearchVersion } from 'uiSrc/utils'\nimport { isRedisVersionSupported } from 'uiSrc/utils/comparisons/compareVersions'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport BrowserSearchPanel, { Props } from './BrowserSearchPanel'\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  isRedisearchAvailable: jest.fn(),\n  getRedisearchVersion: jest.fn(),\n}))\n\njest.mock('uiSrc/utils/comparisons/compareVersions', () => ({\n  ...jest.requireActual('uiSrc/utils/comparisons/compareVersions'),\n  isRedisVersionSupported: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockIsRedisearchAvailable = isRedisearchAvailable as jest.Mock\nconst mockGetRedisearchVersion = getRedisearchVersion as jest.Mock\nconst mockIsRedisVersionSupported = isRedisVersionSupported as jest.Mock\n\nconst mockedProps: Props = {\n  handleCreateIndexPanel: jest.fn,\n}\n\ndescribe('BrowserSearchPanel', () => {\n  beforeEach(() => {\n    mockIsRedisearchAvailable.mockReturnValue(true)\n    mockGetRedisearchVersion.mockReturnValue('2.6.0')\n    mockIsRedisVersionSupported.mockReturnValue(true)\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(render(<BrowserSearchPanel {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render search properly', () => {\n    render(<BrowserSearchPanel {...mockedProps} />)\n    const searchInput = screen.queryByTestId('search-key')\n    expect(searchInput).toBeInTheDocument()\n  })\n\n  it('should show version required modal when RediSearch is present but < 2.0', () => {\n    mockIsRedisearchAvailable.mockReturnValue(true)\n    mockGetRedisearchVersion.mockReturnValue('1.6.14')\n    mockIsRedisVersionSupported.mockReturnValue(false)\n\n    render(<BrowserSearchPanel {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId('search-mode-redisearch-btn'))\n\n    expect(\n      screen.getByTestId('redisearch-version-required'),\n    ).toBeInTheDocument()\n  })\n\n  it('should send telemetry with reason module_not_loaded when RediSearch is missing', () => {\n    mockIsRedisearchAvailable.mockReturnValue(false)\n\n    render(<BrowserSearchPanel {...mockedProps} />)\n    fireEvent.click(screen.getByTestId('search-mode-redisearch-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MODE_CHANGE_FAILED,\n      eventData: expect.objectContaining({ reason: 'module_not_loaded' }),\n    })\n  })\n\n  it('should send telemetry with reason version_not_supported when RediSearch < 2.0', () => {\n    mockIsRedisearchAvailable.mockReturnValue(true)\n    mockGetRedisearchVersion.mockReturnValue('1.6.14')\n    mockIsRedisVersionSupported.mockReturnValue(false)\n\n    render(<BrowserSearchPanel {...mockedProps} />)\n    fireEvent.click(screen.getByTestId('search-mode-redisearch-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MODE_CHANGE_FAILED,\n      eventData: expect.objectContaining({ reason: 'version_not_supported' }),\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-search-panel/BrowserSearchPanel.tsx",
    "content": "/* eslint-disable react/no-this-in-sfc */\n/* eslint-disable react/destructuring-assignment */\nimport React, { useCallback, useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport styled from 'styled-components'\n\nimport {\n  FilterTableIcon,\n  IconType,\n  QuerySearchIcon,\n} from 'uiSrc/components/base/icons'\nimport {\n  FeatureNotAvailable,\n  ModuleNotLoaded,\n  OnboardingTour,\n  RiTooltip,\n} from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport FilterKeyType from 'uiSrc/pages/browser/components/filter-key-type'\nimport RediSearchIndexesList from 'uiSrc/pages/browser/components/redisearch-key-list'\nimport SearchKeyList from 'uiSrc/pages/browser/components/search-key-list'\n\nimport { changeSearchMode, keysSelector } from 'uiSrc/slices/browser/keys'\nimport { getRedisearchVersion, isRedisearchAvailable } from 'uiSrc/utils'\nimport { isRedisVersionSupported } from 'uiSrc/utils/comparisons/compareVersions'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { resetBrowserTree } from 'uiSrc/slices/app/context'\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { Modal } from 'uiSrc/components/base/display'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { ButtonGroup } from 'uiSrc/components/base/forms/button-group/ButtonGroup'\nimport { REDISEARCH_VERSION_REQUIRED_CONTENT } from 'uiSrc/components/messages'\n\nimport styles from './styles.module.scss'\n\nconst MIN_REDISEARCH_VERSION = '2.0.0'\n\ninterface ISwitchType<T> {\n  tooltipText: string\n  type: T\n  disabled?: boolean\n  ariaLabel: string\n  dataTestId: string\n  onClick: () => void\n  isActiveView: () => boolean\n  getIconType: () => IconType\n}\n\nexport interface Props {\n  handleCreateIndexPanel: (value: boolean) => void\n}\n\nconst SwitchSearchModeButtonGroup = styled(ButtonGroup)`\n  button {\n    height: 32px;\n    svg {\n      height: 20px;\n      width: 20px;\n    }\n  }\n`\n\nconst BrowserSearchPanel = (props: Props) => {\n  const { handleCreateIndexPanel } = props\n  const { viewType, searchMode } = useSelector(keysSelector)\n  const { id: instanceId, modules } = useSelector(connectedInstanceSelector)\n\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const [isVersionModalOpen, setIsVersionModalOpen] = useState(false)\n\n  const dispatch = useDispatch()\n\n  const hasRedisearch = isRedisearchAvailable(modules)\n\n  const hasMinimumRedisearchVersion = useMemo(() => {\n    if (!hasRedisearch) return false\n    const version = getRedisearchVersion(modules)\n    return version\n      ? isRedisVersionSupported(version, MIN_REDISEARCH_VERSION)\n      : false\n  }, [modules, hasRedisearch])\n\n  const searchModes: ISwitchType<SearchMode>[] = [\n    {\n      type: SearchMode.Pattern,\n      tooltipText: 'Filter by Key Name or Pattern',\n      ariaLabel: 'Filter by Key Name or Pattern button',\n      dataTestId: 'search-mode-pattern-btn',\n      isActiveView() {\n        return searchMode === this.type\n      },\n      getIconType() {\n        return FilterTableIcon\n      },\n      onClick() {\n        handleSwitchSearchMode(this.type)\n      },\n    },\n    {\n      type: SearchMode.Redisearch,\n      tooltipText: 'Search by Values of Keys',\n      ariaLabel: 'Search by Values of Keys button',\n      dataTestId: 'search-mode-redisearch-btn',\n      disabled: !hasRedisearch || !hasMinimumRedisearchVersion,\n      isActiveView() {\n        return searchMode === this.type\n      },\n      getIconType() {\n        return QuerySearchIcon\n      },\n      onClick() {\n        if (!hasRedisearch) {\n          showPopover()\n          sendEventTelemetry({\n            event: TelemetryEvent.SEARCH_MODE_CHANGE_FAILED,\n            eventData: {\n              databaseId: instanceId,\n              view: viewType,\n              reason: 'module_not_loaded',\n            },\n          })\n        } else if (!hasMinimumRedisearchVersion) {\n          setIsVersionModalOpen(true)\n          sendEventTelemetry({\n            event: TelemetryEvent.SEARCH_MODE_CHANGE_FAILED,\n            eventData: {\n              databaseId: instanceId,\n              view: viewType,\n              reason: 'version_not_supported',\n            },\n          })\n        } else {\n          handleSwitchSearchMode(this.type)\n        }\n      },\n    },\n  ]\n\n  const handleSwitchSearchMode = (mode: SearchMode) => {\n    if (searchMode !== mode) {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_MODE_CHANGED,\n        eventData: {\n          databaseId: instanceId,\n          previous: searchMode,\n          current: mode,\n          view: viewType,\n        },\n      })\n    }\n\n    dispatch(changeSearchMode(mode))\n\n    if (viewType === KeyViewType.Tree) {\n      dispatch(resetBrowserTree())\n    }\n\n    localStorageService.set(BrowserStorageItem.browserSearchMode, mode)\n  }\n\n  const hidePopover = useCallback(() => {\n    setIsPopoverOpen(false)\n  }, [])\n\n  const showPopover = useCallback(() => {\n    setIsPopoverOpen(true)\n  }, [])\n\n  const hideVersionModal = useCallback(() => {\n    setIsVersionModalOpen(false)\n  }, [])\n\n  const SwitchModeBtn = (item: ISwitchType<SearchMode>) => (\n    <ButtonGroup.Button\n      aria-label={item.ariaLabel}\n      onClick={() => item.onClick?.()}\n      data-testid={item.dataTestId}\n      isSelected={item.isActiveView?.()}\n    >\n      <ButtonGroup.Icon icon={item.getIconType()} />\n    </ButtonGroup.Button>\n  )\n\n  const SearchModeSwitch = () => (\n    <SwitchSearchModeButtonGroup data-testid=\"search-mode-switcher\">\n      {searchModes.map((mode) => (\n        <RiTooltip\n          content={mode.tooltipText}\n          position=\"bottom\"\n          key={mode.tooltipText}\n        >\n          {SwitchModeBtn(mode)}\n        </RiTooltip>\n      ))}\n    </SwitchSearchModeButtonGroup>\n  )\n\n  return (\n    <div className={styles.content}>\n      <Modal\n        open={isPopoverOpen}\n        onCancel={hidePopover}\n        className={styles.moduleNotLoaded}\n        content={\n          <ModuleNotLoaded\n            moduleName={RedisDefaultModules.Search}\n            type=\"browser\"\n            id=\"0\"\n            onClose={hidePopover}\n          />\n        }\n        title={null}\n      />\n      <Modal\n        open={isVersionModalOpen}\n        onCancel={hideVersionModal}\n        data-testid=\"redisearch-version-required-modal\"\n        content={\n          <FeatureNotAvailable\n            onClose={hideVersionModal}\n            content={REDISEARCH_VERSION_REQUIRED_CONTENT}\n          />\n        }\n        title={null}\n      />\n      <Row className={styles.searchWrapper} gap=\"m\" align=\"center\">\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.BROWSER_FILTER_SEARCH}\n          anchorPosition=\"downLeft\"\n          panelClassName={styles.browserFilterOnboard}\n        >\n          {SearchModeSwitch()}\n        </OnboardingTour>\n        {searchMode === SearchMode.Pattern ? (\n          <FilterKeyType modules={modules} />\n        ) : (\n          <RediSearchIndexesList onCreateIndex={handleCreateIndexPanel} />\n        )}\n        <SearchKeyList />\n      </Row>\n    </div>\n  )\n}\n\nexport default BrowserSearchPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-search-panel/index.ts",
    "content": "import BrowserSearchPanel from './BrowserSearchPanel'\n\nexport default BrowserSearchPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/browser-search-panel/styles.module.scss",
    "content": ".content {\n  width: 100%;\n  padding: 0 16px 8px 16px;\n\n  display: flex;\n  justify-content: space-between;\n  position: relative;\n\n  .searchWrapper {\n    display: flex;\n    flex-grow: 1;\n  }\n}\n\n.moduleNotLoaded {\n  max-width: 520px !important;\n  z-index: 10000 !important;\n}\n\n.searchModeSwitch {\n  display: flex;\n  background-color: var(--euiColorEmptyShade) !important;\n  border: 1px solid var(--controlsBorderColor) !important;\n  border-radius: 4px 0 0 4px;\n}\n\n.browserFilterOnboard {\n  margin-left: -5px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionSummary/BulkActionSummary.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport BulkActionSummary from './BulkActionSummary'\n\ndescribe('BulkActionSummary', () => {\n  it('should render', () => {\n    render(\n      <BulkActionSummary\n        succeed={10}\n        failed={1}\n        duration={10}\n        processed={100}\n        data-testid=\"testid\"\n      />,\n    )\n\n    expect(screen.getByTestId('testid')).toBeInTheDocument()\n\n    expect(screen.getByTestId('testid')).toHaveTextContent(\n      '100Keys Processed10Success1Errors0:00:00.010Time Taken',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionSummary/BulkActionSummary.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const SummaryContainer = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral300};\n  padding: ${({ theme }) => theme.core.space.space200}\n    ${({ theme }) => theme.core.space.space600};\n  border-radius: ${({ theme }) => theme.core.space.space050};\n`\n\nexport const SummaryValue = styled(Text)`\n  line-height: 24px;\n  font-weight: 500;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionSummary/BulkActionSummary.tsx",
    "content": "import React from 'react'\n\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { millisecondsFormat } from 'uiSrc/utils'\nimport { BulkActionsType } from 'uiSrc/constants'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { SummaryContainer, SummaryValue } from './BulkActionSummary.styles'\n\nexport interface Props {\n  type?: BulkActionsType\n  processed?: number\n  succeed?: number\n  failed?: number\n  duration?: number\n  'data-testid': string\n}\n\nconst BulkActionSummary = ({\n  type = BulkActionsType.Delete,\n  processed = 0,\n  succeed = 0,\n  failed = 0,\n  duration = 0,\n  'data-testid': testId,\n}: Props) => (\n  <Col gap=\"xxl\">\n    <Text color=\"primary\" size=\"m\" variant=\"semiBold\">\n      Results\n    </Text>\n    <SummaryContainer data-testid={testId} gap=\"xl\">\n      <FlexItem grow>\n        <SummaryValue color=\"primary\" size=\"L\">\n          {numberWithSpaces(processed)}\n        </SummaryValue>\n        <SummaryValue color=\"secondary\" size=\"s\">\n          {type === BulkActionsType.Delete ? 'Keys' : 'Commands'} Processed\n        </SummaryValue>\n      </FlexItem>\n      <FlexItem grow>\n        <SummaryValue color=\"primary\" size=\"L\">\n          {numberWithSpaces(succeed)}\n        </SummaryValue>\n        <SummaryValue color=\"secondary\" size=\"s\">\n          Success\n        </SummaryValue>\n      </FlexItem>\n      <FlexItem grow>\n        <SummaryValue color=\"primary\" size=\"L\">\n          {numberWithSpaces(failed)}\n        </SummaryValue>\n        <SummaryValue color=\"secondary\" size=\"s\">\n          Errors\n        </SummaryValue>\n      </FlexItem>\n      <FlexItem grow>\n        <SummaryValue color=\"primary\" size=\"L\">\n          {millisecondsFormat(duration, 'H:mm:ss.SSS')}\n        </SummaryValue>\n        <SummaryValue color=\"secondary\" size=\"s\">\n          Time Taken\n        </SummaryValue>\n      </FlexItem>\n    </SummaryContainer>\n  </Col>\n)\n\nexport default BulkActionSummary\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionSummary/index.ts",
    "content": "import BulkActionSummary from './BulkActionSummary'\n\nexport default BulkActionSummary\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.spec.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\n\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\nimport { RootState } from 'uiSrc/slices/store'\nimport { BulkActionsType, KeyTypes } from 'uiSrc/constants'\nimport { setBulkActionType } from 'uiSrc/slices/browser/bulkActions'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport BulkActions, { Props } from './BulkActions'\n\nconst mockedProps = {\n  ...mock<Props>(),\n}\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  selectedBulkActionsSelector: jest.fn().mockReturnValue({\n    type: 'delete',\n  }),\n}))\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nbeforeEach(() => {\n  const state: any = store.getState()\n\n  ;(useSelector as jest.Mock).mockImplementation(\n    (callback: (arg0: RootState) => RootState) =>\n      callback({\n        ...state,\n        browser: {\n          ...state.browser,\n          keys: {\n            ...state.browser.keys,\n          },\n        },\n      }),\n  )\n})\n\ndescribe('BulkActions', () => {\n  it('should render', () => {\n    expect(render(<BulkActions {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('placeholder should render', () => {\n    render(<BulkActions {...mockedProps} />)\n\n    expect(screen.queryByTestId('bulk-actions-placeholder')).toBeInTheDocument()\n    expect(screen.queryByTestId('bulk-actions-info')).not.toBeInTheDocument()\n  })\n\n  it('bulk actions summary should render with any search', () => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: any) => any) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              search: '1',\n              isSearched: true,\n            },\n            bulkActions: {\n              ...state.browser.bulkActions,\n              bulkDelete: {\n                ...state.browser.bulkActions.bulkDelete,\n                search: '1',\n              },\n            },\n          },\n        }),\n    )\n\n    render(<BulkActions {...mockedProps} />)\n\n    expect(screen.queryByTestId('bulk-actions-info')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('bulk-actions-placeholder'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('bulk actions summary should render with any filter', () => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: any) => any) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              filter: KeyTypes.Hash,\n              isFiltered: true,\n            },\n            bulkActions: {\n              ...state.browser.bulkActions,\n              bulkDelete: {\n                ...state.browser.bulkActions.bulkDelete,\n                filter: KeyTypes.Hash,\n              },\n            },\n          },\n        }),\n    )\n\n    render(<BulkActions {...mockedProps} />)\n\n    expect(screen.queryByTestId('bulk-actions-info')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('bulk-actions-placeholder'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should call proper event after switch tab', async () => {\n    render(<BulkActions {...mockedProps} />)\n\n    fireEvent.mouseDown(screen.getByText('Upload Data'))\n\n    const expectedActions = [setBulkActionType(BulkActionsType.Upload)]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  describe('Telemetry', () => {\n    it('should call proper telemetry events', async () => {\n      const state: any = store.getState()\n      ;(useSelector as jest.Mock).mockImplementation(\n        (callback: (arg0: any) => any) =>\n          callback({\n            ...state,\n            browser: {\n              ...state.browser,\n              keys: {\n                ...state.browser.keys,\n                filter: KeyTypes.Hash,\n                isFiltered: true,\n              },\n              bulkActions: {\n                ...state.browser.bulkActions,\n                bulkDelete: {\n                  ...state.browser.bulkActions.bulkDelete,\n                  filter: KeyTypes.Hash,\n                },\n              },\n            },\n          }),\n      )\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(\n        <BulkActions\n          {...mockedProps}\n          onBulkActionsPanel={jest.fn()}\n          onClosePanel={jest.fn()}\n        />,\n      )\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.BULK_ACTIONS_OPENED,\n        eventData: {\n          databaseId: 'instanceId',\n          filter: {\n            match: '*',\n            filter: 'hash',\n          },\n          action: 'delete',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n      fireEvent.click(screen.getByTestId('bulk-action-cancel-btn'))\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.BULK_ACTIONS_CANCELLED,\n        eventData: {\n          databaseId: 'instanceId',\n          action: BulkActionsType.Delete,\n          filter: {\n            match: '*',\n            type: 'hash',\n          },\n        },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const BulkActionsPage = styled(Col)`\n  height: 100%;\n  overflow: hidden;\n`\n\nexport const BulkActionsContentActions = styled(Col)`\n  height: 100%;\n  width: 100%;\n`\n\nexport const BulkActionsContainer = styled(Col)`\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral100};\n  padding: 24px 0;\n  height: 100%;\n  position: relative;\n`\n\nexport const BulkActionsHeader = styled(Row)`\n  padding: 0 ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n\nexport const BulkActionsScrollPanel = styled.div`\n  overflow-y: auto;\n  overflow-x: hidden;\n  scrollbar-width: thin;\n  height: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  selectedBulkActionsSelector,\n  setBulkActionsInitialState,\n  setBulkActionType,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { BulkActionsType } from 'uiSrc/constants'\nimport { keysSelector } from 'uiSrc/slices/browser/keys'\nimport {\n  getMatchType,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\nimport { FullScreen, RiTooltip } from 'uiSrc/components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport BulkUpload from './BulkUpload'\nimport BulkDelete from './BulkDelete'\nimport BulkActionsTabs from './BulkActionsTabs'\nimport styles from './styles.module.scss'\nimport {\n  BulkActionsContainer,\n  BulkActionsContentActions,\n  BulkActionsHeader,\n  BulkActionsPage,\n  BulkActionsScrollPanel,\n} from './BulkActions.styles'\n\nexport interface Props {\n  isFullScreen: boolean\n  arePanelsCollapsed: boolean\n  onBulkActionsPanel: (value: boolean) => void\n  onClosePanel: () => void\n  onToggleFullScreen: () => void\n}\nconst BulkActions = (props: Props) => {\n  const {\n    isFullScreen,\n    arePanelsCollapsed,\n    onClosePanel,\n    onBulkActionsPanel,\n    onToggleFullScreen,\n  } = props\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const { filter, search } = useSelector(keysSelector)\n  const { type } = useSelector(selectedBulkActionsSelector)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.BULK_ACTIONS_OPENED,\n      eventData: {\n        databaseId: instanceId,\n        filter: {\n          filter,\n          match:\n            search && search !== DEFAULT_SEARCH_MATCH\n              ? getMatchType(search)\n              : DEFAULT_SEARCH_MATCH,\n        },\n        action: type,\n      },\n    })\n  }, [])\n\n  const handleChangeType = (value: BulkActionsType) => {\n    dispatch(setBulkActionType(value))\n  }\n\n  const closePanel = () => {\n    onBulkActionsPanel(false)\n    dispatch(setBulkActionsInitialState())\n\n    onClosePanel()\n\n    const eventData: Record<string, any> = {\n      databaseId: instanceId,\n      action: type,\n    }\n\n    if (type === BulkActionsType.Delete) {\n      eventData.filter = {\n        match:\n          search && search !== DEFAULT_SEARCH_MATCH\n            ? getMatchType(search)\n            : DEFAULT_SEARCH_MATCH,\n        type: filter,\n      }\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.BULK_ACTIONS_CANCELLED,\n      eventData,\n    })\n  }\n\n  return (\n    <BulkActionsPage>\n      <BulkActionsContainer justify=\"center\" gap=\"l\">\n        <BulkActionsHeader align=\"center\" justify=\"between\">\n          <Title size=\"M\">Bulk Actions</Title>\n          <Row align=\"center\" gap=\"s\" grow={false}>\n            {!arePanelsCollapsed && (\n              <FullScreen\n                isFullScreen={isFullScreen}\n                onToggleFullScreen={onToggleFullScreen}\n                anchorClassName={styles.anchorTooltipFullScreen}\n              />\n            )}\n            {(!arePanelsCollapsed || isFullScreen) && (\n              <RiTooltip\n                content=\"Close\"\n                position=\"left\"\n                anchorClassName={styles.anchorTooltip}\n              >\n                <IconButton\n                  icon={CancelSlimIcon}\n                  aria-label=\"Close panel\"\n                  data-testid=\"bulk-close-panel\"\n                  onClick={closePanel}\n                />\n              </RiTooltip>\n            )}\n          </Row>\n        </BulkActionsHeader>\n        <BulkActionsScrollPanel>\n          <BulkActionsContentActions data-testid=\"bulk-actions-content\">\n            <BulkActionsTabs onChangeType={handleChangeType} />\n            {type === BulkActionsType.Upload && (\n              <BulkUpload onCancel={closePanel} />\n            )}\n            {type === BulkActionsType.Delete && (\n              <BulkDelete onCancel={closePanel} />\n            )}\n          </BulkActionsContentActions>\n        </BulkActionsScrollPanel>\n      </BulkActionsContainer>\n    </BulkActionsPage>\n  )\n}\n\nexport default BulkActions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { BulkActionsStatus, KeyTypes } from 'uiSrc/constants'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport BulkActionsInfo, { Props } from './BulkActionsInfo'\n\nconst mockedProps = {\n  ...mock<Props>(),\n}\n\ndescribe('BulkActionsInfo', () => {\n  it('should render', () => {\n    expect(render(<BulkActionsInfo {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('filter should render when exists', () => {\n    render(<BulkActionsInfo {...mockedProps} filter={KeyTypes.Hash} />)\n\n    expect(screen.queryByTestId('bulk-actions-info-filter')).toBeInTheDocument()\n  })\n\n  it('filter should not render when does not exist', () => {\n    render(<BulkActionsInfo {...mockedProps} filter={null} />)\n\n    expect(\n      screen.queryByTestId('bulk-actions-info-filter'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should show connection lost when status is disconnect', () => {\n    render(\n      <BulkActionsInfo\n        {...mockedProps}\n        filter={null}\n        status={BulkActionsStatus.Disconnected}\n      />,\n    )\n\n    expect(screen.getByTestId('bulk-status-disconnected')).toHaveTextContent(\n      'Connection Lost',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.styles.tsx",
    "content": "import styled from 'styled-components'\nimport React from 'react'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const BulkActionsInfoFilter = styled.div<{\n  className?: string\n  children?: React.ReactNode\n}>`\n  display: inline-flex;\n  gap: ${({ theme }) => theme.core.space.space050};\n  align-items: center;\n  font-size: ${({ theme }) => theme.core.font.fontSize.s12};\n`\n\nexport const BulkActionsTitle = styled(Text).attrs({\n  size: 'M',\n})<React.ComponentProps<typeof Text> & { $full?: boolean }>`\n  color: ${({ theme, color }) =>\n    !color && theme.semantic.color.text.informative400};\n  ${({ $full }) => $full && 'width: 100%'}\n`\n\nexport const BulkActionsInfoSearch = styled(ColorText).attrs({\n  size: 'M',\n})`\n  word-break: break-all;\n`\n\nexport const BulkActionsProgressLine = styled.div<{\n  children?: React.ReactNode\n}>`\n  height: 2px;\n  width: calc(100% - 24px);\n  margin-top: -1px;\n  & > div {\n    height: 100%;\n    background-color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.background.informative300};\n  }\n`\n\nexport const BulkActionsContainer = styled.div<{ children: React.ReactNode }>`\n  position: relative;\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n  min-height: 162px;\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n  gap: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n  display: flex;\n  flex-direction: column;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/BulkActionsInfo.tsx",
    "content": "import React from 'react'\n\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { BulkActionsStatus, KeyTypes, RedisDataType } from 'uiSrc/constants'\nimport GroupBadge from 'uiSrc/components/group-badge/GroupBadge'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport BulkActionsStatusDisplay from '../BulkActionsStatusDisplay'\nimport {\n  BulkActionsContainer,\n  BulkActionsInfoFilter,\n  BulkActionsInfoSearch,\n  BulkActionsProgressLine,\n  BulkActionsTitle,\n} from './BulkActionsInfo.styles'\n\nexport interface Props {\n  title?: string | React.ReactNode\n  subTitle?: string | React.ReactNode\n  loading: boolean\n  filter?: Nullable<KeyTypes> | RedisDataType\n  status: Maybe<BulkActionsStatus>\n  search?: string\n  progress?: {\n    total: Maybe<number>\n    scanned: Maybe<number>\n  }\n  children?: React.ReactNode\n  error?: string\n}\n\nconst BulkActionsInfo = (props: Props) => {\n  const {\n    children,\n    loading,\n    filter,\n    search,\n    status,\n    progress,\n    title = 'Delete Keys with',\n    subTitle,\n    error,\n  } = props\n  const { total = 0, scanned = 0 } = progress || {}\n\n  return (\n    <BulkActionsContainer data-testid=\"bulk-actions-info\">\n      <BulkActionsStatusDisplay\n        status={status}\n        total={total}\n        scanned={scanned}\n        error={error}\n      />\n      <Col justify=\"between\" gap=\"xxl\">\n        <BulkActionsTitle color=\"primary\" $full>\n          {title}\n        </BulkActionsTitle>\n        {subTitle && (\n          <BulkActionsTitle color=\"primary\" $full>\n            {subTitle}\n          </BulkActionsTitle>\n        )}\n        {(filter || search) && (\n          <Row justify=\"start\" align=\"center\" gap=\"xxl\">\n            {filter && (\n              <BulkActionsInfoFilter data-testid=\"bulk-actions-info-filter\">\n                <Text size=\"s\" color=\"primary\">\n                  Key type:\n                </Text>\n                <GroupBadge type={filter} />\n              </BulkActionsInfoFilter>\n            )}\n            {search && (\n              <BulkActionsInfoFilter data-testid=\"bulk-actions-info-search\">\n                <Text size=\"s\" color=\"primary\">\n                  Pattern:\n                </Text>\n                <BulkActionsInfoSearch color=\"primary\">\n                  {' '}\n                  {search}\n                </BulkActionsInfoSearch>\n              </BulkActionsInfoFilter>\n            )}\n          </Row>\n        )}\n      </Col>\n      <Divider />\n      {loading && (\n        <BulkActionsProgressLine data-testid=\"progress-line\">\n          <div style={{ width: `${(total ? scanned / total : 0) * 100}%` }} />\n        </BulkActionsProgressLine>\n      )}\n      <div>{children}</div>\n    </BulkActionsContainer>\n  )\n}\n\nexport default BulkActionsInfo\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsInfo/index.ts",
    "content": "import BulkActionsInfo from './BulkActionsInfo'\n\nexport default BulkActionsInfo\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsStatusDisplay/BulkActionsStatusDisplay.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport BulkActionsStatusDisplay, {\n  BulkActionsStatusDisplayProps,\n} from './BulkActionsStatusDisplay'\nimport { faker } from '@faker-js/faker/locale/af_ZA'\nimport { BulkActionsStatus } from 'uiSrc/constants'\n\nconst renderBulkActionsStatusDisplay = (\n  props?: Partial<BulkActionsStatusDisplayProps>,\n) => {\n  const defaultProps: BulkActionsStatusDisplayProps = {\n    status: faker.helpers.enumValue(BulkActionsStatus),\n    total: faker.number.int({ min: 0, max: 1000 }),\n    scanned: faker.number.int({ min: 0, max: 1000 }),\n  }\n\n  return render(<BulkActionsStatusDisplay {...defaultProps} {...props} />)\n}\n\ndescribe('BulkActionsStatusDisplay', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render banner when status is in progress', () => {\n    const props: BulkActionsStatusDisplayProps = {\n      status: BulkActionsStatus.Running,\n      total: 200,\n      scanned: 50,\n    }\n\n    renderBulkActionsStatusDisplay(props)\n\n    const banner = screen.getByTestId('bulk-status-progress')\n\n    expect(banner).toBeInTheDocument()\n    expect(banner).toHaveTextContent('In progress: 25%')\n  })\n\n  it('should render banner when status is in Aborted', () => {\n    const props: BulkActionsStatusDisplayProps = {\n      status: BulkActionsStatus.Aborted,\n      total: 100,\n      scanned: 30,\n    }\n\n    renderBulkActionsStatusDisplay(props)\n\n    const banner = screen.getByTestId('bulk-status-stopped')\n\n    expect(banner).toBeInTheDocument()\n    expect(banner).toHaveTextContent('Stopped: 30%')\n  })\n\n  it('should render banner when status is Completed', () => {\n    renderBulkActionsStatusDisplay({ status: BulkActionsStatus.Completed })\n\n    const banner = screen.getByTestId('bulk-status-completed')\n\n    expect(banner).toBeInTheDocument()\n    expect(banner).toHaveTextContent('Action completed')\n  })\n\n  it('should render banner when status is Disconnected', () => {\n    const props: BulkActionsStatusDisplayProps = {\n      status: BulkActionsStatus.Disconnected,\n      total: 100,\n      scanned: 50,\n    }\n\n    renderBulkActionsStatusDisplay(props)\n\n    const banner = screen.getByTestId('bulk-status-disconnected')\n\n    expect(banner).toBeInTheDocument()\n    expect(banner).toHaveTextContent('Connection Lost: 50%')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsStatusDisplay/BulkActionsStatusDisplay.tsx",
    "content": "import React from 'react'\nimport { isUndefined } from 'lodash'\n\nimport { BulkActionsStatus } from 'uiSrc/constants'\nimport { getApproximatePercentage, Maybe } from 'uiSrc/utils'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nimport { isProcessedBulkAction } from '../utils'\nimport { Props } from '../BulkActionsInfo/BulkActionsInfo'\nimport { Banner } from 'uiSrc/components/base/display'\n\nexport interface BulkActionsStatusDisplayProps {\n  status: Props['status']\n  total: Maybe<number>\n  scanned: Maybe<number>\n  error?: string\n}\n\nexport const BulkActionsStatusDisplay = ({\n  status,\n  total,\n  scanned,\n  error,\n}: BulkActionsStatusDisplayProps) => {\n  if (!isUndefined(status) && !isProcessedBulkAction(status)) {\n    return (\n      <Banner\n        message={\n          <>\n            In progress:\n            <ColorText size=\"XS\">{` ${getApproximatePercentage(total, scanned)}`}</ColorText>\n          </>\n        }\n        data-testid=\"bulk-status-progress\"\n      />\n    )\n  }\n\n  if (status === BulkActionsStatus.Aborted) {\n    return (\n      <Banner\n        variant=\"danger\"\n        message={<>Stopped: {getApproximatePercentage(total, scanned)}</>}\n        data-testid=\"bulk-status-stopped\"\n      />\n    )\n  }\n\n  if (status === BulkActionsStatus.Completed) {\n    return (\n      <Banner\n        showIcon\n        variant=\"success\"\n        message=\"Action completed\"\n        data-testid=\"bulk-status-completed\"\n      />\n    )\n  }\n\n  if (status === BulkActionsStatus.Failed) {\n    return (\n      <Banner\n        variant=\"danger\"\n        message={error || 'Action failed'}\n        data-testid=\"bulk-status-failed\"\n      />\n    )\n  }\n\n  if (status === BulkActionsStatus.Disconnected) {\n    return (\n      <Banner\n        variant=\"danger\"\n        message={`Connection Lost: ${getApproximatePercentage(total, scanned)}`}\n        data-testid=\"bulk-status-disconnected\"\n      />\n    )\n  }\n\n  return null\n}\n\nexport default BulkActionsStatusDisplay\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsStatusDisplay/index.ts",
    "content": "import BulkActionsStatusDisplay from './BulkActionsStatusDisplay'\n\nexport default BulkActionsStatusDisplay\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\n\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { BulkActionsType } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport BulkActionsTabs, { Props } from './BulkActionsTabs'\n\nconst mockedProps = {\n  ...mock<Props>(),\n}\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  selectedBulkActionsSelector: jest.fn().mockReturnValue({\n    type: 'delete',\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  keysSelector: jest.fn().mockReturnValue({\n    filter: 'set',\n    search: 'dawkmdk*',\n  }),\n}))\n\ndescribe('BulkActionsTabs', () => {\n  it('should render', () => {\n    expect(render(<BulkActionsTabs {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call proper telemetry events', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<BulkActionsTabs {...mockedProps} onChangeType={jest.fn()} />)\n\n    fireEvent.mouseDown(screen.getByText('Upload Data'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.BULK_ACTIONS_OPENED,\n      eventData: {\n        databaseId: '',\n        action: BulkActionsType.Upload,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.styles.ts",
    "content": "import styled from 'styled-components'\nimport Tabs from 'uiSrc/components/base/layout/tabs'\n\nexport const StyledTabs = styled(Tabs)`\n  padding: ${({ theme }) => theme.core.space.space025}\n    ${({ theme }) => theme.core.space.space200} 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/BulkActionsTabs.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { BulkActionsType } from 'uiSrc/constants'\nimport { selectedBulkActionsSelector } from 'uiSrc/slices/browser/bulkActions'\n\nimport {\n  getMatchType,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\nimport { keysSelector } from 'uiSrc/slices/browser/keys'\nimport { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { StyledTabs } from './BulkActionsTabs.styles'\n\nexport interface Props {\n  onChangeType: (id: BulkActionsType) => void\n}\n\nconst BulkActionsTabs = (props: Props) => {\n  const { onChangeType } = props\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { filter, search } = useSelector(keysSelector)\n  const { type } = useSelector(selectedBulkActionsSelector)\n\n  const onSelectedTabChanged = (id: string) => {\n    const eventData: Record<string, any> = {\n      databaseId: instanceId,\n      action: id,\n    }\n\n    if (id === BulkActionsType.Delete) {\n      eventData.filter = {\n        match:\n          search && search !== DEFAULT_SEARCH_MATCH\n            ? getMatchType(search)\n            : DEFAULT_SEARCH_MATCH,\n        type: filter,\n      }\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.BULK_ACTIONS_OPENED,\n      eventData,\n    })\n    onChangeType(id as BulkActionsType)\n  }\n\n  const tabs: TabInfo[] = useMemo(\n    () => [\n      {\n        value: BulkActionsType.Delete,\n        label: <Text size=\"S\">Delete Keys</Text>,\n        content: null,\n      },\n      {\n        value: BulkActionsType.Upload,\n        label: <Text size=\"S\">Upload Data</Text>,\n        content: null,\n      },\n    ],\n    [],\n  )\n\n  return (\n    <StyledTabs\n      tabs={tabs}\n      value={type ?? undefined}\n      onChange={onSelectedTabChanged}\n      data-testid=\"bulk-actions-tabs\"\n    />\n  )\n}\n\nexport default BulkActionsTabs\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/index.ts",
    "content": "import BulkActionsTabs from './BulkActionsTabs'\n\nexport default BulkActionsTabs\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActionsTabs/styles.module.scss",
    "content": ".tabs {\n  padding: 2px 16px 0;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDelete.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { isUndefined } from 'lodash'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  bulkActionsDeleteOverviewSelector,\n  bulkActionsDeleteSelector,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nimport BulkDeleteFooter from './BulkDeleteFooter'\nimport BulkDeleteSummary from './BulkDeleteSummary'\nimport BulkActionsInfo from '../BulkActionsInfo'\n\nexport interface Props {\n  onCancel: () => void\n}\n\nconst BulkDelete = (props: Props) => {\n  const { onCancel } = props\n  const { filter, search, loading } = useSelector(bulkActionsDeleteSelector)\n  const {\n    status,\n    filter: { match, type: filterType },\n    progress,\n    error,\n  } = useSelector(bulkActionsDeleteOverviewSelector) ?? { filter: {} }\n\n  const hasSearchOrFilter = !!search || filter !== null\n\n  const [showPlaceholder, setShowPlaceholder] =\n    useState<boolean>(!hasSearchOrFilter)\n\n  useEffect(() => {\n    setShowPlaceholder(!status && !hasSearchOrFilter)\n  }, [status, hasSearchOrFilter])\n\n  const searchPattern = match || search || '*'\n\n  return (\n    <>\n      {!showPlaceholder && (\n        <>\n          <BulkActionsInfo\n            search={searchPattern}\n            loading={loading}\n            filter={isUndefined(filterType) ? filter : filterType}\n            status={status}\n            progress={progress}\n            error={error}\n          >\n            <Col gap=\"l\">\n              <BulkDeleteSummary />\n            </Col>\n          </BulkActionsInfo>\n          <BulkDeleteFooter onCancel={onCancel} />\n        </>\n      )}\n\n      {showPlaceholder && (\n        <Col\n          gap=\"l\"\n          justify=\"center\"\n          align=\"center\"\n          data-testid=\"bulk-actions-placeholder\"\n        >\n          <Text size=\"XL\" color=\"primary\" variant=\"semiBold\">\n            No pattern or key type set\n          </Text>\n          <Text color=\"secondary\">\n            To perform a bulk action, set the pattern or select the key type\n          </Text>\n        </Col>\n      )}\n    </>\n  )\n}\n\nexport default BulkDelete\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteContent/BulkDeleteContent.spec.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { cloneDeep } from 'lodash'\n\nimport { RootState } from 'uiSrc/slices/store'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\n\nimport BulkDeleteContent from './BulkDeleteContent'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  selectedBulkActionsSelector: jest.fn().mockReturnValue({\n    type: 'delete',\n  }),\n}))\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nbeforeEach(() => {\n  const state: any = store.getState()\n\n  ;(useSelector as jest.Mock).mockImplementation(\n    (callback: (arg0: RootState) => RootState) =>\n      callback({\n        ...state,\n        browser: {\n          ...state.browser,\n          keys: {\n            ...state.browser.keys,\n            data: {\n              ...state.browser.keys.data,\n            },\n          },\n        },\n      }),\n  )\n})\n\ndescribe('BulkDeleteContent', () => {\n  it('should render', () => {\n    expect(render(<BulkDeleteContent />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteContent/BulkDeleteContent.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { ListChildComponentProps, VariableSizeList as List } from 'react-window'\nimport AutoSizer from 'react-virtualized-auto-sizer'\nimport { useSelector } from 'react-redux'\n\nimport { MAX_BULK_ACTION_ERRORS_LENGTH } from 'uiSrc/constants'\nimport { Text } from 'uiSrc/components/base/text'\nimport { bulkActionsDeleteSummarySelector } from 'uiSrc/slices/browser/bulkActions'\nimport styles from './styles.module.scss'\n\nconst MIN_ROW_HEIGHT = 30\nconst PROTRUDING_OFFSET = 2\n\nconst BulkDeleteContent = () => {\n  const { errors = [] } = useSelector(bulkActionsDeleteSummarySelector) ?? {}\n\n  const outerRef = useRef<HTMLDivElement>(null)\n  const listRef = useRef<List>(null)\n  const rowHeights = useRef<{ [key: number]: number }>({})\n\n  const getRowHeight = (index: number) =>\n    rowHeights.current[index] > MIN_ROW_HEIGHT\n      ? rowHeights.current[index] + 2\n      : MIN_ROW_HEIGHT\n\n  const setRowHeight = (index: number, size: number) => {\n    listRef.current?.resetAfterIndex(0)\n    rowHeights.current = { ...rowHeights.current, [index]: size }\n  }\n\n  const Row = ({ index, style, data: width }: ListChildComponentProps) => {\n    const rowRef = useRef<HTMLDivElement>(null)\n\n    useEffect(() => {\n      if (rowRef.current) {\n        setRowHeight(index, rowRef.current?.offsetHeight + 16)\n      }\n    }, [rowRef, width])\n\n    const { key, error } = errors[index]\n\n    return (\n      <div style={style} className={styles.item} data-testid={`row-${index}`}>\n        <span ref={rowRef}>\n          <span className={styles.key}>{key}</span>\n          <span className={styles.error}>{error}</span>\n        </span>\n      </div>\n    )\n  }\n\n  if (!errors.length) {\n    return null\n  }\n\n  return (\n    <div className={styles.container}>\n      <div className={styles.header}>\n        <Text className={styles.headerTitle}>Error list</Text>\n        {errors.length >= MAX_BULK_ACTION_ERRORS_LENGTH && (\n          <Text className={styles.headerSummary}>\n            last {MAX_BULK_ACTION_ERRORS_LENGTH} errors are shown\n          </Text>\n        )}\n      </div>\n      <div className={styles.list}>\n        <AutoSizer>\n          {({ width, height }) => (\n            <List\n              ref={listRef}\n              outerRef={outerRef}\n              height={height}\n              itemCount={errors.length}\n              itemSize={getRowHeight}\n              width={width - PROTRUDING_OFFSET}\n              className={styles.listContent}\n              overscanCount={30}\n              itemData={width}\n            >\n              {Row}\n            </List>\n          )}\n        </AutoSizer>\n      </div>\n    </div>\n  )\n}\n\nexport default BulkDeleteContent\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteContent/index.ts",
    "content": "import BulkDeleteContent from './BulkDeleteContent'\n\nexport default BulkDeleteContent\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteContent/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n\n  display: flex;\n  flex-grow: 1;\n  flex-direction: column;\n  padding-bottom: 40px;\n  min-height: 200px;\n  width: 100%;\n  position: relative;\n}\n\n.header {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  align-items: flex-end;\n  padding: 0 12px 10px;\n  height: 40px;\n\n  .headerSummary {\n    font-size: 13px !important;\n    line-height: 18px !important;\n    padding-top: 6px;\n    color: var(--euiColorMediumShade) !important;\n  }\n}\n\n.headerTitle {\n  padding-bottom: 6px;\n  font-size: 14px !important;\n  line-height: 24px !important;\n}\n\n.list {\n  display: flex;\n  flex-grow: 1;\n  width: 100%;\n  border: 1px solid var(--euiColorLightestShade);\n  padding-top: 4px;\n  padding-left: 12px;\n  position: relative;\n}\n\n.item {\n  width: 100%;\n  word-break: break-all;\n}\n\n.key,\n.error {\n  display: inline;\n}\n\n.key {\n  padding-right: 12px;\n  color: var(--euiTextSubduedColor);\n}\n\n.error {\n  color: var(--euiColorColorDanger) !important;\n  word-break: break-word;\n}\n\n.listContent {\n  @include eui.scrollBar;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport BulkDeleteFooter, { Props } from './BulkDeleteFooter'\nimport { setBulkDeleteGenerateReport } from 'uiSrc/slices/browser/bulkActions'\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  setBulkDeleteGenerateReport: jest.fn().mockReturnValue({\n    type: 'bulkActions/setBulkDeleteGenerateReport',\n  }),\n}))\n\nconst mockedProps = {\n  ...mock<Props>(),\n}\n\ndescribe('BulkDeleteFooter', () => {\n  it('should render', () => {\n    expect(render(<BulkDeleteFooter {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call onCancel prop when click on Cancel btn', () => {\n    const mockOnCancel = jest.fn()\n    render(<BulkDeleteFooter {...mockedProps} onCancel={mockOnCancel} />)\n\n    fireEvent.click(screen.getByTestId('bulk-action-cancel-btn'))\n\n    expect(mockOnCancel).toBeCalled()\n  })\n\n  it('should render download report checkbox', () => {\n    render(<BulkDeleteFooter {...mockedProps} />)\n\n    expect(screen.getByTestId('download-report-checkbox')).toBeInTheDocument()\n  })\n\n  it('should dispatch setBulkDeleteGenerateReport when checkbox is toggled', () => {\n    render(<BulkDeleteFooter {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId('download-report-checkbox'))\n\n    // Checkbox default is false, clicking toggles to true\n    expect(setBulkDeleteGenerateReport).toHaveBeenCalledWith(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const BulkDeleteFooterContainer = styled(Row)`\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/BulkDeleteFooter.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  bulkActionsDeleteOverviewSelector,\n  setBulkDeleteStartAgain,\n  toggleBulkDeleteActionTriggered,\n  setBulkDeleteGenerateReport,\n  bulkActionsDeleteSelector,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { keysDataSelector } from 'uiSrc/slices/browser/keys'\nimport {\n  getMatchType,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { BulkActionsType } from 'uiSrc/constants'\nimport { getRangeForNumber, BULK_THRESHOLD_BREAKPOINTS } from 'uiSrc/utils'\n\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\nimport {\n  DestructiveButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RefreshIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { isProcessedBulkAction } from '../../utils'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { ConfirmationPopover, RiTooltip } from 'uiSrc/components'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { BulkDeleteFooterContainer } from './BulkDeleteFooter.styles'\n\nexport interface Props {\n  onCancel: () => void\n}\n\nconst BulkDeleteFooter = (props: Props) => {\n  const { onCancel } = props\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { scanned, total } = useSelector(keysDataSelector)\n  const { loading, generateReport, filter, search } = useSelector(\n    bulkActionsDeleteSelector,\n  )\n  const { status } = useSelector(bulkActionsDeleteOverviewSelector) ?? {}\n\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  const handleDelete = () => {\n    setIsPopoverOpen(false)\n    dispatch(toggleBulkDeleteActionTriggered())\n  }\n\n  const handleDeleteWarning = () => {\n    setIsPopoverOpen(true)\n\n    let matchValue = DEFAULT_SEARCH_MATCH\n    if (search !== DEFAULT_SEARCH_MATCH && !!search) {\n      matchValue = getMatchType(search)\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.BULK_ACTIONS_WARNING,\n      eventData: {\n        filter: {\n          match: matchValue,\n          type: filter,\n        },\n        progress: {\n          scanned,\n          scannedRange: getRangeForNumber(scanned, BULK_THRESHOLD_BREAKPOINTS),\n          total,\n          totalRange: getRangeForNumber(total, BULK_THRESHOLD_BREAKPOINTS),\n        },\n        databaseId: instanceId,\n        action: BulkActionsType.Delete,\n      },\n    })\n  }\n\n  const handleStartNew = () => {\n    dispatch(setBulkDeleteStartAgain())\n  }\n\n  const handleStop = () => {\n    dispatch(toggleBulkDeleteActionTriggered())\n  }\n\n  const handleCancel = () => {\n    onCancel()\n  }\n\n  return (\n    <Col data-testid=\"bulk-actions-delete\" justify=\"end\">\n      <BulkDeleteFooterContainer\n        align=\"center\"\n        justify=\"end\"\n        gap=\"l\"\n        grow={false}\n      >\n        <Row grow={false}>\n          <Checkbox\n            checked={generateReport}\n            onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n              dispatch(setBulkDeleteGenerateReport(e.target.checked))\n            }\n            label=\"Download report\"\n            data-testid=\"download-report-checkbox\"\n          />\n\n          <RiTooltip\n            content=\"Download a detailed report of deleted keys.\"\n            position=\"left\"\n          >\n            <RiIcon\n              type=\"InfoIcon\"\n              // TODO: fixes for the icon positioning\n              style={{ display: 'flex', alignItems: 'center' }}\n            />\n          </RiTooltip>\n        </Row>\n\n        {!loading && (\n          <SecondaryButton\n            onClick={handleCancel}\n            data-testid=\"bulk-action-cancel-btn\"\n          >\n            {isProcessedBulkAction(status) ? 'Close' : 'Cancel'}\n          </SecondaryButton>\n        )}\n        {loading && (\n          <SecondaryButton\n            onClick={handleStop}\n            data-testid=\"bulk-action-stop-btn\"\n          >\n            Stop\n          </SecondaryButton>\n        )}\n\n        {!isProcessedBulkAction(status) && (\n          <ConfirmationPopover\n            anchorPosition=\"upCenter\"\n            ownFocus\n            isOpen={isPopoverOpen}\n            closePopover={() => setIsPopoverOpen(false)}\n            panelPaddingSize=\"m\"\n            anchorClassName=\"deleteFieldPopover\"\n            button={\n              <PrimaryButton\n                loading={loading}\n                disabled={loading}\n                onClick={handleDeleteWarning}\n                data-testid=\"bulk-action-warning-btn\"\n              >\n                Delete\n              </PrimaryButton>\n            }\n            title={'Are you sure you want to perform this action?'}\n            message={\n              'This will delete all keys matching the selected type and pattern.'\n            }\n            appendInfo={\n              <Row align=\"center\" gap=\"m\">\n                <RiIcon size=\"xl\" type=\"ToastDangerIcon\" />\n                <Text size=\"s\">\n                  Bulk deletion may impact performance and cause memory spikes.\n                  Avoid running in production.\n                </Text>\n              </Row>\n            }\n            confirmButton={\n              <DestructiveButton\n                size=\"s\"\n                onClick={handleDelete}\n                data-testid=\"bulk-action-apply-btn\"\n              >\n                Delete\n              </DestructiveButton>\n            }\n          />\n        )}\n        {isProcessedBulkAction(status) && (\n          <PrimaryButton\n            icon={RefreshIcon}\n            onClick={handleStartNew}\n            data-testid=\"bulk-action-start-again-btn\"\n          >\n            Start New\n          </PrimaryButton>\n        )}\n      </BulkDeleteFooterContainer>\n    </Col>\n  )\n}\n\nexport default BulkDeleteFooter\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteFooter/index.ts",
    "content": "import BulkDeleteFooter from './BulkDeleteFooter'\n\nexport default BulkDeleteFooter\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteSummary/BulkDeleteSummary.spec.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { cloneDeep } from 'lodash'\n\nimport { RootState } from 'uiSrc/slices/store'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\n\nimport BulkDeleteSummary from './BulkDeleteSummary'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  selectedBulkActionsSelector: jest.fn().mockReturnValue({\n    type: 'delete',\n  }),\n}))\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nbeforeEach(() => {\n  const state: any = store.getState()\n\n  ;(useSelector as jest.Mock).mockImplementation(\n    (callback: (arg0: RootState) => RootState) =>\n      callback({\n        ...state,\n        browser: {\n          ...state.browser,\n          keys: {\n            ...state.browser.keys,\n            data: {\n              ...state.browser.keys.data,\n            },\n          },\n        },\n      }),\n  )\n})\n\ndescribe('BulkDeleteSummary', () => {\n  it('should render', () => {\n    expect(render(<BulkDeleteSummary />)).toBeTruthy()\n  })\n\n  it('summary should contain calculated text', () => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              data: {\n                ...state.browser.keys.data,\n                scanned: 10,\n                total: 100,\n                keys: [1],\n              },\n            },\n          },\n        }),\n    )\n\n    render(<BulkDeleteSummary />)\n    const summaryEl = screen.queryByTestId('bulk-delete-summary')\n    const expectedText = 'Scanned 10% (10/100) and found 1 keys'\n\n    expect(summaryEl).toHaveTextContent(expectedText)\n  })\n\n  it('should show folder key count when keyCount is set (folder delete)', () => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              data: {\n                ...state.browser.keys.data,\n                scanned: 50,\n                total: 100,\n                keys: [1, 2, 3],\n              },\n            },\n            bulkActions: {\n              ...state.browser.bulkActions,\n              bulkDelete: {\n                ...state.browser.bulkActions.bulkDelete,\n                keyCount: 25,\n              },\n            },\n          },\n        }),\n    )\n\n    render(<BulkDeleteSummary />)\n    const summaryEl = screen.queryByTestId('bulk-delete-summary')\n    // For folder delete, should show the folder's keyCount (25) instead of keys.length (3)\n    const expectedText = 'Scanned 50% (50/100) and found 25 keys'\n\n    expect(summaryEl).toHaveTextContent(expectedText)\n  })\n\n  it('should show approximate title for folder delete when scan not complete', () => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              data: {\n                ...state.browser.keys.data,\n                scanned: 50,\n                total: 100,\n                keys: [],\n              },\n            },\n            bulkActions: {\n              ...state.browser.bulkActions,\n              bulkDelete: {\n                ...state.browser.bulkActions.bulkDelete,\n                keyCount: 25,\n              },\n            },\n          },\n        }),\n    )\n\n    render(<BulkDeleteSummary />)\n    // Expected amount should be ~50 (keyCount * total / scanned = 25 * 100 / 50)\n    expect(screen.getByText('Expected amount: ~50 keys')).toBeInTheDocument()\n  })\n\n  it('should show N/A when scanned is 0 (avoid division by zero)', () => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              data: {\n                ...state.browser.keys.data,\n                scanned: 0,\n                total: 100,\n                keys: [],\n              },\n            },\n            bulkActions: {\n              ...state.browser.bulkActions,\n              bulkDelete: {\n                ...state.browser.bulkActions.bulkDelete,\n                keyCount: 25,\n              },\n            },\n          },\n        }),\n    )\n\n    render(<BulkDeleteSummary />)\n    // When scanned is 0, should show N/A instead of Infinity\n    expect(screen.getByText('Expected amount: N/A')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteSummary/BulkDeleteSummary.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { isUndefined } from 'lodash'\n\nimport { numberWithSpaces, nullableNumberWithSpaces } from 'uiSrc/utils/numbers'\nimport { keysDataSelector } from 'uiSrc/slices/browser/keys'\nimport { getApproximatePercentage } from 'uiSrc/utils/validations'\nimport {\n  bulkActionsDeleteOverviewSelector,\n  bulkActionsDeleteSelector,\n  bulkActionsDeleteSummarySelector,\n} from 'uiSrc/slices/browser/bulkActions'\nimport BulkActionSummary from 'uiSrc/pages/browser/components/bulk-actions/BulkActionSummary'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nconst BulkDeleteSummary = () => {\n  const [title, setTitle] = useState<string>('')\n  const { scanned = 0, total = 0, keys } = useSelector(keysDataSelector)\n  const { keyCount } = useSelector(bulkActionsDeleteSelector)\n  const { processed, succeed, failed } =\n    useSelector(bulkActionsDeleteSummarySelector) ?? {}\n  const { duration = 0, status } =\n    useSelector(bulkActionsDeleteOverviewSelector) ?? {}\n\n  // Check if this is a folder delete (keyCount is set)\n  const isFolderDelete = keyCount !== null && keyCount !== undefined\n\n  useEffect(() => {\n    // If no keys have been scanned yet, can't calculate approximation (avoid division by zero)\n    if (scanned === 0) {\n      setTitle('Expected amount: N/A')\n      return\n    }\n\n    // If keyCount is set (folder delete), calculate approximate based on scan progress\n    if (isFolderDelete) {\n      const approximateCount =\n        scanned < total ? (keyCount * total) / scanned : keyCount\n      setTitle(\n        `Expected amount: ${scanned < total ? '~' : ''}${nullableNumberWithSpaces(Math.round(approximateCount))} keys`,\n      )\n      return\n    }\n\n    // Otherwise, calculate from scanned keys (normal bulk delete)\n    if (scanned < total && !keys.length) {\n      setTitle('Expected amount: N/A')\n      return\n    }\n\n    const approximateCount =\n      scanned < total ? (keys.length * total) / scanned : keys.length\n    setTitle(\n      `Expected amount: ${scanned < total ? '~' : ''}${nullableNumberWithSpaces(Math.round(approximateCount))} keys`,\n    )\n  }, [scanned, total, keys, keyCount, isFolderDelete])\n\n  // For folder delete: use folder's key count for \"found\"\n  // For normal bulk delete: use browser scan progress and found keys count\n  const displayFound = isFolderDelete ? keyCount : keys.length\n\n  return (\n    <div>\n      {isUndefined(status) && (\n        <Col gap=\"l\">\n          <Row gap=\"s\">\n            <Text color=\"primary\" size=\"m\" variant=\"semiBold\">\n              {title}\n            </Text>\n            <RiTooltip\n              position=\"right\"\n              content={\n                <Text size=\"XS\">\n                  Expected amount is estimated based on the number of keys\n                  scanned and the scan percentage. The final number may be\n                  different.\n                </Text>\n              }\n            >\n              <RiIcon type=\"InfoIcon\" data-testid=\"bulk-delete-tooltip\" />\n            </RiTooltip>\n          </Row>\n          <Text color=\"primary\" size=\"S\" data-testid=\"bulk-delete-summary\">\n            {`Scanned ${getApproximatePercentage(total, scanned)} `}\n            {`(${numberWithSpaces(scanned)}/${nullableNumberWithSpaces(total)}) `}\n            {`and found ${numberWithSpaces(displayFound)} keys`}\n          </Text>\n        </Col>\n      )}\n      {!isUndefined(status) && (\n        <BulkActionSummary\n          succeed={succeed}\n          processed={processed}\n          failed={failed}\n          duration={duration}\n          data-testid=\"bulk-delete-completed-summary\"\n        />\n      )}\n    </div>\n  )\n}\n\nexport default BulkDeleteSummary\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/BulkDeleteSummary/index.ts",
    "content": "import BulkDeleteSummary from './BulkDeleteSummary'\n\nexport default BulkDeleteSummary\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkDelete/index.ts",
    "content": "import BulkDelete from './BulkDelete'\n\nexport default BulkDelete\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  act,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  bulkActionsUploadOverviewSelector,\n  bulkUpload,\n  setBulkUploadStartAgain,\n  uploadController,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { BulkActionsType } from 'uiSrc/constants'\nimport BulkUpload from './BulkUpload'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/bulkActions', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/bulkActions'),\n  bulkActionsUploadSelector: jest.fn().mockReturnValue({\n    loading: false,\n    fileName: '',\n  }),\n  bulkActionsUploadOverviewSelector: jest.fn().mockReturnValue(null),\n  bulkActionsUploadSummarySelector: jest.fn().mockReturnValue(null),\n  uploadController: {\n    abort: jest.fn(),\n  },\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('BulkUpload', () => {\n  it('should render', () => {\n    expect(render(<BulkUpload onCancel={jest.fn()} />)).toBeTruthy()\n  })\n\n  it('should call onCancel', () => {\n    const onCancel = jest.fn()\n    render(<BulkUpload onCancel={onCancel} />)\n\n    fireEvent.click(screen.getByTestId('bulk-action-cancel-btn'))\n\n    expect(onCancel).toBeCalled()\n  })\n\n  it('should call abort controller', () => {\n    const onCancel = jest.fn()\n    const abortMock = jest.fn()\n    ;(uploadController as any).abort = abortMock\n\n    render(<BulkUpload onCancel={onCancel} />)\n\n    fireEvent.click(screen.getByTestId('bulk-action-cancel-btn'))\n\n    expect(abortMock).toBeCalled()\n  })\n\n  it('submit btn should be disabled without file', () => {\n    render(<BulkUpload onCancel={jest.fn()} />)\n\n    expect(screen.getByTestId('bulk-action-warning-btn')).toBeDisabled()\n  })\n\n  it('should open warning popover and call proper actions after submit', async () => {\n    render(<BulkUpload onCancel={jest.fn()} />)\n\n    const data = 'set a b'\n    const blob = new Blob([data])\n\n    const file = new File([blob], 'text.txt')\n    await act(() => {\n      fireEvent.change(screen.getByTestId('bulk-upload-file-input'), {\n        target: { files: [file] },\n      })\n    })\n\n    expect(screen.getByTestId('bulk-action-warning-btn')).not.toBeDisabled()\n    fireEvent.click(screen.getByTestId('bulk-action-warning-btn'))\n\n    expect(screen.getByTestId('bulk-action-tooltip')).toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('bulk-action-apply-btn'))\n\n    const expectedActions = [bulkUpload()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render summary', () => {\n    ;(bulkActionsUploadOverviewSelector as jest.Mock).mockImplementation(\n      () => ({\n        status: 'completed',\n        progress: 100,\n        duration: 10,\n      }),\n    )\n\n    render(<BulkUpload onCancel={jest.fn()} />)\n\n    expect(\n      screen.getByTestId('bulk-upload-completed-summary'),\n    ).toBeInTheDocument()\n  })\n\n  it('should call start new button', () => {\n    ;(bulkActionsUploadOverviewSelector as jest.Mock).mockImplementation(\n      () => ({\n        status: 'completed',\n        progress: 100,\n        duration: 10,\n      }),\n    )\n\n    render(<BulkUpload onCancel={jest.fn()} />)\n\n    fireEvent.click(screen.getByTestId('bulk-action-start-new-btn'))\n    const expectedActions = [setBulkUploadStartAgain()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper telemetry events', async () => {\n    ;(bulkActionsUploadOverviewSelector as jest.Mock).mockRestore()\n\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<BulkUpload onCancel={jest.fn()} />)\n\n    const data = 'set a b'\n    const blob = new Blob([data])\n\n    const file = new File([blob], 'text.txt')\n    await act(() => {\n      fireEvent.change(screen.getByTestId('bulk-upload-file-input'), {\n        target: { files: [file] },\n      })\n    })\n\n    fireEvent.click(screen.getByTestId('bulk-action-warning-btn'))\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.BULK_ACTIONS_WARNING,\n      eventData: {\n        action: BulkActionsType.Upload,\n        databaseId: '',\n      },\n    })\n  })\n\n  it('should contain the upload warning text', () => {\n    render(<BulkUpload onCancel={jest.fn()} />)\n\n    expect(\n      screen.getByText(\n        'Use files only from trusted authors to avoid automatic execution of malicious code.',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.styles.ts",
    "content": "import styled from 'styled-components'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const StyledContent = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space200};\n`\n\nexport const StyledPopoverContainer = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space200};\n  width: 430px;\n}`\n\nexport const StyledPopoverIcon = styled(RiIcon)`\n  position: absolute;\n`\n\nexport const StyledPopoverText = styled(Text)`\n  padding-left: ${({ theme }) => theme.core.space.space400};\n`\n\nexport const StyledFooter = styled(Row)`\n  padding: 0 ${({ theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/BulkUpload.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { BulkActionsStatus, BulkActionsType } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  bulkActionsUploadOverviewSelector,\n  bulkActionsUploadSelector,\n  bulkActionsUploadSummarySelector,\n  bulkUploadDataAction,\n  setBulkUploadStartAgain,\n  uploadController,\n} from 'uiSrc/slices/browser/bulkActions'\n\nimport BulkActionsInfo from 'uiSrc/pages/browser/components/bulk-actions/BulkActionsInfo'\nimport BulkActionSummary from 'uiSrc/pages/browser/components/bulk-actions/BulkActionSummary'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { isProcessedBulkAction } from 'uiSrc/pages/browser/components/bulk-actions/utils'\nimport {\n  RiFilePicker,\n  UploadWarning,\n  RiPopover,\n  RiTooltip,\n} from 'uiSrc/components'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RefreshIcon } from 'uiSrc/components/base/icons'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  StyledContent,\n  StyledFooter,\n  StyledPopoverContainer,\n  StyledPopoverIcon,\n  StyledPopoverText,\n} from './BulkUpload.styles'\n\nexport interface Props {\n  onCancel: () => void\n}\n\nconst MAX_MB_FILE = 3_000\nconst MAX_FILE_SIZE = MAX_MB_FILE * 1024 * 1024\n\nconst BulkUpload = (props: Props) => {\n  const { onCancel } = props\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { loading, fileName } = useSelector(bulkActionsUploadSelector)\n  const { status, progress, duration } =\n    useSelector(bulkActionsUploadOverviewSelector) ?? {}\n  const { succeed, processed, failed } =\n    useSelector(bulkActionsUploadSummarySelector) ?? {}\n\n  const [files, setFiles] = useState<Nullable<FileList>>(null)\n  const [isInvalid, setIsInvalid] = useState<boolean>(false)\n  const [isSubmitDisabled, setIsSubmitDisabled] = useState<boolean>(true)\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)\n\n  const isCompleted = status && status === BulkActionsStatus.Completed\n\n  const dispatch = useDispatch()\n\n  const onStartAgain = () => {\n    dispatch(setBulkUploadStartAgain())\n    setFiles(null)\n    setIsSubmitDisabled(true)\n  }\n\n  const handleUploadWarning = () => {\n    setIsPopoverOpen(true)\n    sendEventTelemetry({\n      event: TelemetryEvent.BULK_ACTIONS_WARNING,\n      eventData: {\n        databaseId: instanceId,\n        action: BulkActionsType.Upload,\n      },\n    })\n  }\n\n  const onFileChange = (files: Nullable<FileList>) => {\n    const isOutOfSize = (files?.[0]?.size || 0) > MAX_FILE_SIZE\n\n    setFiles(files)\n    setIsInvalid(!!files?.length && isOutOfSize)\n    setIsSubmitDisabled(!files?.length || isOutOfSize)\n  }\n\n  const handleUpload = () => {\n    if (files) {\n      setIsPopoverOpen(false)\n\n      const formData = new FormData()\n      formData.append('file', files[0])\n      dispatch(\n        bulkUploadDataAction(instanceId, {\n          file: formData,\n          fileName: files[0].name,\n        }),\n      )\n    }\n  }\n\n  const handleClickCancel = () => {\n    uploadController?.abort()\n    onCancel?.()\n  }\n\n  return (\n    <Col justify=\"between\" data-testid=\"bulk-upload-container\">\n      {!isCompleted ? (\n        <StyledContent gap=\"l\" align=\"start\">\n          <Row align=\"start\" grow={false}>\n            <Text color=\"primary\">\n              Upload the text file with the list of Redis commands\n            </Text>\n            <RiTooltip\n              content={\n                <>\n                  <Text size=\"xs\">SET Key0 Value0</Text>\n                  <Text size=\"xs\">SET Key1 Value1</Text>\n                  <Text size=\"xs\">...</Text>\n                  <Text size=\"xs\">SET KeyN ValueN</Text>\n                </>\n              }\n              data-testid=\"bulk-upload-tooltip-example\"\n            >\n              <RiIcon\n                type=\"InfoIcon\"\n                style={{\n                  marginLeft: 4,\n                  marginBottom: 2,\n                }}\n              />\n            </RiTooltip>\n          </Row>\n          <RiFilePicker\n            id=\"bulk-upload-file-input\"\n            initialPromptText=\"Select or drag and drop a file\"\n            isInvalid={isInvalid}\n            onChange={onFileChange}\n            display=\"large\"\n            data-testid=\"bulk-upload-file-input\"\n            aria-label=\"Select or drag and drop file\"\n          />\n          {isInvalid && (\n            <ColorText color=\"danger\" data-testid=\"input-file-error-msg\">\n              File should not exceed {MAX_MB_FILE} MB\n            </ColorText>\n          )}\n          <UploadWarning />\n        </StyledContent>\n      ) : (\n        <BulkActionsInfo\n          loading={loading}\n          status={status}\n          progress={progress}\n          title=\"Commands executed from file\"\n          subTitle={<div className=\"truncateText\">{fileName}</div>}\n        >\n          <BulkActionSummary\n            type={BulkActionsType.Upload}\n            succeed={succeed}\n            processed={processed}\n            failed={failed}\n            duration={duration}\n            data-testid=\"bulk-upload-completed-summary\"\n          />\n        </BulkActionsInfo>\n      )}\n\n      <StyledFooter gap=\"l\" justify=\"end\" grow={false}>\n        <SecondaryButton\n          onClick={handleClickCancel}\n          data-testid=\"bulk-action-cancel-btn\"\n        >\n          {isProcessedBulkAction(status) ? 'Close' : 'Cancel'}\n        </SecondaryButton>\n        {!isCompleted ? (\n          <RiPopover\n            id=\"bulk-upload-warning-popover\"\n            anchorPosition=\"upCenter\"\n            isOpen={isPopoverOpen}\n            closePopover={() => setIsPopoverOpen(false)}\n            panelPaddingSize=\"none\"\n            button={\n              <PrimaryButton\n                onClick={handleUploadWarning}\n                disabled={isSubmitDisabled || loading}\n                loading={loading}\n                data-testid=\"bulk-action-warning-btn\"\n              >\n                Upload\n              </PrimaryButton>\n            }\n          >\n            <StyledPopoverContainer gap=\"m\">\n              <Col data-testid=\"bulk-action-tooltip\" gap=\"s\">\n                <StyledPopoverIcon type=\"ToastDangerIcon\" />\n                <StyledPopoverText size=\"L\" color=\"primary\">\n                  Are you sure you want to perform this action?\n                </StyledPopoverText>\n                <StyledPopoverText size=\"M\" color=\"secondary\">\n                  All commands from the file will be executed against your\n                  database.\n                </StyledPopoverText>\n              </Col>\n              <Row justify=\"end\">\n                <PrimaryButton\n                  size=\"s\"\n                  onClick={handleUpload}\n                  data-testid=\"bulk-action-apply-btn\"\n                >\n                  Upload\n                </PrimaryButton>\n              </Row>\n            </StyledPopoverContainer>\n          </RiPopover>\n        ) : (\n          <PrimaryButton\n            icon={RefreshIcon}\n            color=\"secondary\"\n            onClick={onStartAgain}\n            data-testid=\"bulk-action-start-new-btn\"\n          >\n            Start New\n          </PrimaryButton>\n        )}\n      </StyledFooter>\n    </Col>\n  )\n}\n\nexport default BulkUpload\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/BulkUpload/index.ts",
    "content": "import BulkUpload from './BulkUpload'\n\nexport default BulkUpload\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/constants/bulk-type-options.tsx",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/index.ts",
    "content": "import BulkActions from './BulkActions'\n\nexport default BulkActions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/styles.module.scss",
    "content": ".anchorTooltip {\n  svg {\n    width: 20px;\n    height: 20px;\n  }\n}\n\n.anchorTooltipFullScreen {\n  svg {\n    width: 16px;\n    height: 16px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/bulk-actions/utils.ts",
    "content": "import { BulkActionsStatus } from 'uiSrc/constants'\n\nexport const isProcessingBulkAction = (status?: BulkActionsStatus) =>\n  status === BulkActionsStatus.Running ||\n  status === BulkActionsStatus.Preparing ||\n  status === BulkActionsStatus.Initializing\n\nexport const isProcessedBulkAction = (status?: BulkActionsStatus) =>\n  status === BulkActionsStatus.Completed ||\n  status === BulkActionsStatus.Aborted ||\n  status === BulkActionsStatus.Failed ||\n  status === BulkActionsStatus.Disconnected\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/create-redisearch-index/constants.ts",
    "content": "import { GROUP_TYPES_COLORS, KeyTypes } from 'uiSrc/constants'\n\nexport enum FieldTypes {\n  TEXT = 'text',\n  TAG = 'tag',\n  NUMERIC = 'numeric',\n  GEO = 'geo',\n  VECTOR = 'vector',\n}\n\nexport enum RedisearchIndexKeyType {\n  HASH = 'hash',\n  JSON = 'json',\n}\n\nexport const KEY_TYPE_OPTIONS = [\n  {\n    text: 'Hash',\n    value: RedisearchIndexKeyType.HASH,\n    color: GROUP_TYPES_COLORS[KeyTypes.Hash],\n  },\n  {\n    text: 'JSON',\n    value: RedisearchIndexKeyType.JSON,\n    color: GROUP_TYPES_COLORS[KeyTypes.JSON],\n  },\n]\n\nexport const FIELD_TYPE_OPTIONS = [\n  {\n    text: 'TEXT',\n    value: FieldTypes.TEXT,\n    description: 'Use TEXT for full-text search and indexing free-form text.',\n  },\n  {\n    text: 'TAG',\n    value: FieldTypes.TAG,\n    description: 'Use TAG for filtering by exact match values.',\n  },\n  {\n    text: 'NUMERIC',\n    value: FieldTypes.NUMERIC,\n    description: 'Use NUMERIC for storing and querying numbers.',\n  },\n  {\n    text: 'GEO',\n    value: FieldTypes.GEO,\n    description: 'Use GEO for geographic coordinates (latitude and longitude).',\n  },\n  {\n    text: 'VECTOR',\n    value: FieldTypes.VECTOR,\n    description: 'Use VECTOR for semantic search using vector embeddings.',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/delete-key-popover/DeleteKeyPopover.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { DeleteKeyPopover } from './DeleteKeyPopover'\n\ndescribe('DeleteKeyPopover', () => {\n  const mockProps = {\n    nameString: 'test-key',\n    name: stringToBuffer('test-key'),\n    type: KeyTypes.String,\n    rowId: 1,\n    onDelete: jest.fn(),\n    onOpenPopover: jest.fn(),\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render delete button with proper data-testid', () => {\n    render(<DeleteKeyPopover {...mockProps} />)\n\n    expect(\n      screen.getByTestId(`delete-key-btn-${mockProps.nameString}`),\n    ).toBeInTheDocument()\n  })\n\n  it('should not show popover content by default', () => {\n    render(<DeleteKeyPopover {...mockProps} />)\n\n    expect(screen.queryByText('will be deleted.')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('submit-delete-key')).not.toBeInTheDocument()\n  })\n\n  it('should show popover content when deletePopoverId matches rowId', () => {\n    render(\n      <DeleteKeyPopover {...mockProps} deletePopoverId={mockProps.rowId} />,\n    )\n\n    expect(screen.getByText('will be deleted.')).toBeInTheDocument()\n    expect(screen.getByTestId('submit-delete-key')).toBeInTheDocument()\n  })\n\n  it('should call onOpenPopover when delete button is clicked', () => {\n    render(<DeleteKeyPopover {...mockProps} />)\n\n    fireEvent.click(\n      screen.getByTestId(`delete-key-btn-${mockProps.nameString}`),\n    )\n\n    expect(mockProps.onOpenPopover).toHaveBeenCalledWith(\n      mockProps.rowId,\n      mockProps.type,\n    )\n  })\n\n  it('should call onOpenPopover with -1 when closing the popover', () => {\n    const { container } = render(\n      <DeleteKeyPopover {...mockProps} deletePopoverId={mockProps.rowId} />,\n    )\n\n    container.querySelector('.euiPopover')\n    const closePopover = () => mockProps.onOpenPopover(-1, mockProps.type)\n    closePopover()\n\n    expect(mockProps.onOpenPopover).toHaveBeenCalledWith(-1, mockProps.type)\n  })\n\n  it('should call onDelete with proper arguments when confirm button is clicked', () => {\n    render(\n      <DeleteKeyPopover {...mockProps} deletePopoverId={mockProps.rowId} />,\n    )\n\n    fireEvent.click(screen.getByTestId('submit-delete-key'))\n\n    expect(mockProps.onDelete).toHaveBeenCalledWith(mockProps.name)\n  })\n\n  it('should disable delete button when deleting is true', () => {\n    render(\n      <DeleteKeyPopover\n        {...mockProps}\n        deletePopoverId={mockProps.rowId}\n        deleting\n      />,\n    )\n\n    expect(screen.getByTestId('submit-delete-key')).toBeDisabled()\n  })\n\n  it('should format long names in the confirmation message', () => {\n    const longNameProps = {\n      ...mockProps,\n      nameString: 'very-long-key-name-that-might-need-formatting',\n      deletePopoverId: mockProps.rowId,\n    }\n\n    render(<DeleteKeyPopover {...longNameProps} />)\n\n    expect(screen.getByText(longNameProps.nameString)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/delete-key-popover/DeleteKeyPopover.tsx",
    "content": "import React from 'react'\n\nimport cx from 'classnames'\nimport { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants'\nimport { formatLongName } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  DestructiveButton,\n  IconButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport ConfirmationPopover from 'uiSrc/components/confirmation-popover'\n\nexport interface DeleteProps {\n  nameString: string\n  name: RedisResponseBuffer\n  type: KeyTypes | ModulesKeyTypes\n  rowId: number\n  deletePopoverId?: number\n  deleting?: boolean\n  onDelete: (key: RedisResponseBuffer) => void\n  onOpenPopover: (index: number, type: KeyTypes | ModulesKeyTypes) => void\n}\n\nexport const DeleteKeyPopover = ({\n  nameString,\n  name,\n  type,\n  rowId,\n  deletePopoverId,\n  deleting,\n  onDelete,\n  onOpenPopover,\n}: DeleteProps) => {\n  const onClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {\n    e.stopPropagation()\n    onOpenPopover(rowId, type)\n  }\n\n  return (\n    <ConfirmationPopover\n      anchorClassName={cx('showOnHoverKey', {\n        show: deletePopoverId === rowId,\n      })}\n      anchorPosition=\"leftCenter\"\n      isOpen={deletePopoverId === rowId}\n      closePopover={() => onOpenPopover(-1, type)}\n      panelPaddingSize=\"l\"\n      button={\n        <IconButton\n          icon={DeleteIcon}\n          onClick={onClick}\n          aria-label=\"Delete Key\"\n          data-testid={`delete-key-btn-${nameString}`}\n        />\n      }\n      onClick={(e) => e.stopPropagation()}\n      title={formatLongName(nameString)}\n      message=\"will be deleted.\"\n      confirmButton={\n        <DestructiveButton\n          size=\"small\"\n          icon={DeleteIcon}\n          disabled={deleting}\n          onClick={() => onDelete(name)}\n          data-testid=\"submit-delete-key\"\n        >\n          Delete\n        </DestructiveButton>\n      }\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport {\n  cleanup,\n  expectActionsToContain,\n  fireEvent,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n  screen,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\nimport { loadKeys, setFilter } from 'uiSrc/slices/browser/keys'\nimport { setBulkDeleteFilter } from 'uiSrc/slices/browser/bulkActions'\nimport { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'\nimport { FeatureFlags, KeyTypes } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport FilterKeyType from './FilterKeyType'\nimport { resetBrowserTree } from 'uiSrc/slices/app/context'\n\nlet store: typeof mockedStore\n\nconst filterSelectId = 'select-filter-key-type'\nconst unsupportedAnchorId = 'unsupported-btn-anchor'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceOverviewSelector: jest.fn().mockReturnValue({\n    version: '6.2.1',\n  }),\n  connectedInstanceSelector: jest.fn().mockReturnValue({ id: '123' }),\n}))\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('FilterKeyType', () => {\n  it('should render', () => {\n    expect(render(<FilterKeyType />)).toBeTruthy()\n    const searchInput = screen.getByTestId(filterSelectId)\n    expect(searchInput).toBeInTheDocument()\n  })\n\n  it('should not be disabled filter with database redis version > 6.0', () => {\n    render(<FilterKeyType />)\n    const filterSelect = screen.getByTestId(filterSelectId)\n\n    expect(filterSelect).not.toBeDisabled()\n  })\n\n  it('should not be info anchor with database redis version > 6.0', () => {\n    const { queryByTestId } = render(<FilterKeyType />)\n    expect(queryByTestId(unsupportedAnchorId)).not.toBeInTheDocument()\n  })\n\n  it('\"setFilter\", \"setBulkDeleteFilter\" and \"loadKeys\" should be called after select \"Hash\" type', async () => {\n    const { findByText } = render(<FilterKeyType />)\n\n    await userEvent.click(screen.getByTestId(filterSelectId))\n    await userEvent.click(await findByText('Hash'))\n\n    const expectedActions = [\n      setFilter(KeyTypes.Hash),\n      setBulkDeleteFilter(KeyTypes.Hash),\n      resetBrowserTree(),\n      loadKeys(),\n    ]\n\n    expectActionsToContain(store.getActions(), expectedActions)\n  })\n\n  it('\"setBulkDeleteFilter\" should be called with null when selecting \"All Key Types\"', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      'browser.keys.filter',\n      KeyTypes.Hash,\n    )\n\n    const testStore = mockStore(initialStoreState)\n    const { findByText } = render(<FilterKeyType />, {\n      store: testStore,\n    })\n\n    await userEvent.click(screen.getByTestId(filterSelectId))\n    await userEvent.click(await findByText('All Key Types'))\n\n    const expectedActions = [setFilter(null), setBulkDeleteFilter(null)]\n\n    expectActionsToContain(testStore.getActions(), expectedActions)\n  })\n\n  it('should be disabled filter with database redis version < 6.0', () => {\n    connectedInstanceOverviewSelector.mockImplementationOnce(() => ({\n      version: '5.1',\n    }))\n    render(<FilterKeyType />)\n    const filterSelect = screen.getByTestId(filterSelectId)\n\n    expect(filterSelect).toBeDisabled()\n  })\n\n  it('should be info box with database redis version < 6.0', () => {\n    connectedInstanceOverviewSelector.mockImplementationOnce(() => ({\n      version: '5.1',\n    }))\n    render(<FilterKeyType />)\n    expect(screen.getByTestId(unsupportedAnchorId)).toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId(unsupportedAnchorId))\n\n    expect(screen.getByTestId('filter-not-available-modal')).toBeInTheDocument()\n  })\n\n  it('should send telemetry event with redis v < 6.0 after click on anchor', async () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n    connectedInstanceOverviewSelector.mockImplementationOnce(() => ({\n      version: '5.1',\n    }))\n\n    render(<FilterKeyType />)\n\n    fireEvent.click(screen.getByTestId(unsupportedAnchorId))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.BROWSER_FILTER_MODE_CHANGE_FAILED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n  })\n\n  it('should filter out graph if redis db does not have graph module', () => {\n    const { queryByText } = render(<FilterKeyType modules={[]} />)\n\n    fireEvent.click(screen.getByTestId(filterSelectId))\n\n    const graphElement = queryByText('Graph')\n    expect(graphElement).not.toBeInTheDocument()\n  })\n\n  it('should not filter out items if required feature flags are set to true', async () => {\n    const { queryByText } = render(\n      <FilterKeyType\n        modules={[\n          {\n            name: RedisDefaultModules.Graph,\n            version: 1,\n            semanticVersion: '1.3',\n          },\n        ]}\n      />,\n    )\n\n    await userEvent.click(screen.getByTestId(filterSelectId))\n\n    const graphElement = queryByText('Graph')\n    expect(graphElement).toBeInTheDocument()\n  })\n\n  it('should filter out items if required feature flags are not set to true', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n    const { queryByText } = render(<FilterKeyType />, {\n      store: mockStore(initialStoreState),\n    })\n\n    fireEvent.click(screen.getByTestId(filterSelectId))\n\n    const graphElement = queryByText('Graph')\n    expect(graphElement).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx",
    "content": "import cx from 'classnames'\nimport React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { CommandsVersions } from 'uiSrc/constants/commandsVersions'\nimport { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  fetchKeys,\n  fetchSearchHistoryAction,\n  keysSelector,\n  setFilter,\n} from 'uiSrc/slices/browser/keys'\nimport { setBulkDeleteFilter } from 'uiSrc/slices/browser/bulkActions'\nimport { isVersionHigherOrEquals } from 'uiSrc/utils'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\nimport { FeatureNotAvailable } from 'uiSrc/components'\nimport { FILTER_NOT_AVAILABLE_CONTENT } from 'uiSrc/components/messages'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { resetBrowserTree } from 'uiSrc/slices/app/context'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { AdditionalRedisModule } from 'uiSrc/slices/interfaces'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\nimport { HealthText } from 'uiSrc/components/base/text/HealthText'\nimport {\n  defaultValueRender,\n  RiSelect,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { Modal } from 'uiSrc/components/base/display'\nimport { FILTER_KEY_TYPE_OPTIONS } from './constants'\n\nimport styles from './styles.module.scss'\nimport styled from 'styled-components'\n\nconst ALL_KEY_TYPES_VALUE = 'all'\n\nexport interface Props {\n  modules?: AdditionalRedisModule[]\n}\n\nconst FilterKeyTypeSelect = styled(RiSelect)`\n  height: 100%;\n`\n\nconst FilterKeyType = ({ modules }: Props) => {\n  const [isSelectOpen, setIsSelectOpen] = useState<boolean>(false)\n  const [typeSelected, setTypeSelected] = useState<string>('all')\n  const [isVersionSupported, setIsVersionSupported] = useState<boolean>(true)\n  const [isInfoPopoverOpen, setIsInfoPopoverOpen] = useState<boolean>(false)\n\n  const { version } = useSelector(connectedInstanceOverviewSelector)\n  const { filter, viewType, searchMode } = useSelector(keysSelector)\n  const features = useSelector(appFeatureFlagsFeaturesSelector)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsVersionSupported(\n      isVersionHigherOrEquals(\n        version,\n        CommandsVersions.FILTER_PER_KEY_TYPES.since,\n      ),\n    )\n  }, [version])\n\n  useEffect(() => {\n    setTypeSelected(filter ?? ALL_KEY_TYPES_VALUE)\n  }, [filter])\n\n  const options: {\n    value: string\n    inputDisplay: JSX.Element\n    dropdownDisplay: JSX.Element\n  }[] = FILTER_KEY_TYPE_OPTIONS.filter(({ featureFlag, skipIfNoModule }) => {\n    if (\n      skipIfNoModule &&\n      !modules?.some(({ name }) => name === skipIfNoModule)\n    ) {\n      return false\n    }\n    return !featureFlag || features[featureFlag]?.flag\n  }).map((item) => {\n    const { value, color, text } = item\n    return {\n      value,\n      inputDisplay: (\n        <HealthText\n          color={color}\n          data-test-subj={`filter-option-type-${value}`}\n        >\n          {text}\n        </HealthText>\n      ),\n      dropdownDisplay: (\n        <HealthText\n          color={color}\n          data-test-subj={`filter-option-type-${value}`}\n        >\n          {text}\n        </HealthText>\n      ),\n      'data-test-subj': `filter-option-type-${value}`,\n    }\n  })\n\n  options.unshift({\n    value: ALL_KEY_TYPES_VALUE,\n    inputDisplay: (\n      <div className={styles.dropdownOption} data-testid=\"all-key-types-option\">\n        All Key Types\n      </div>\n    ),\n    dropdownDisplay: <span>All Key Types</span>,\n  })\n\n  const onChangeType = (initValue: string) => {\n    const value = initValue || ALL_KEY_TYPES_VALUE\n    const filterValue = value === ALL_KEY_TYPES_VALUE ? null : value\n    setTypeSelected(value)\n    setIsSelectOpen(false)\n    dispatch(setFilter(filterValue))\n    // Sync filter to bulk delete state (for when bulk actions panel is open)\n    dispatch(setBulkDeleteFilter(filterValue as KeyTypes))\n    if (viewType === KeyViewType.Tree) {\n      dispatch(resetBrowserTree())\n    }\n    dispatch(\n      fetchKeys(\n        {\n          searchMode,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n        },\n        () => {\n          dispatch(fetchSearchHistoryAction(searchMode))\n        },\n      ),\n    )\n  }\n\n  const handleClickSelect = () => {\n    setIsInfoPopoverOpen(true)\n    sendEventTelemetry({\n      event: TelemetryEvent.BROWSER_FILTER_MODE_CHANGE_FAILED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  return (\n    <OutsideClickDetector\n      onOutsideClick={() => isVersionSupported && setIsSelectOpen(false)}\n    >\n      <div\n        className={cx(\n          styles.container,\n          !isVersionSupported && styles.unsupported,\n        )}\n      >\n        <Modal\n          open={!isVersionSupported && isInfoPopoverOpen}\n          onCancel={() => setIsInfoPopoverOpen(false)}\n          className={styles.unsupportedInfoModal}\n          data-testid=\"filter-not-available-modal\"\n          content={\n            <FeatureNotAvailable\n              onClose={() => setIsInfoPopoverOpen(false)}\n              content={FILTER_NOT_AVAILABLE_CONTENT}\n            />\n          }\n          title={null}\n        />\n        {!isVersionSupported && (\n          <div\n            role=\"presentation\"\n            onClick={handleClickSelect}\n            className={styles.unsupportedInfo}\n            data-testid=\"unsupported-btn-anchor\"\n          />\n        )}\n        <FilterKeyTypeSelect\n          disabled={!isVersionSupported}\n          options={options}\n          valueRender={defaultValueRender}\n          defaultOpen={isSelectOpen}\n          value={typeSelected}\n          onChange={(value: string) => onChangeType(value)}\n          data-testid=\"select-filter-key-type\"\n        />\n      </div>\n    </OutsideClickDetector>\n  )\n}\n\nexport default FilterKeyType\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/filter-key-type/constants.ts",
    "content": "import {\n  GROUP_TYPES_COLORS,\n  KeyTypes,\n  ModulesKeyTypes,\n  FeatureFlags,\n} from 'uiSrc/constants'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\n\nexport const FILTER_KEY_TYPE_OPTIONS = [\n  {\n    text: 'Hash',\n    value: KeyTypes.Hash,\n    color: GROUP_TYPES_COLORS[KeyTypes.Hash],\n  },\n  {\n    text: 'List',\n    value: KeyTypes.List,\n    color: GROUP_TYPES_COLORS[KeyTypes.List],\n  },\n  {\n    text: 'Set',\n    value: KeyTypes.Set,\n    color: GROUP_TYPES_COLORS[KeyTypes.Set],\n  },\n  {\n    text: 'Sorted Set',\n    value: KeyTypes.ZSet,\n    color: GROUP_TYPES_COLORS[KeyTypes.ZSet],\n  },\n  {\n    text: 'String',\n    value: KeyTypes.String,\n    color: GROUP_TYPES_COLORS[KeyTypes.String],\n  },\n  {\n    text: 'JSON',\n    value: KeyTypes.ReJSON,\n    color: GROUP_TYPES_COLORS[KeyTypes.ReJSON],\n  },\n  {\n    text: 'Stream',\n    value: KeyTypes.Stream,\n    color: GROUP_TYPES_COLORS[KeyTypes.Stream],\n  },\n  {\n    text: 'Graph',\n    value: ModulesKeyTypes.Graph,\n    color: GROUP_TYPES_COLORS[ModulesKeyTypes.Graph],\n    skipIfNoModule: RedisDefaultModules.Graph,\n    featureFlag: FeatureFlags.envDependent,\n  },\n  {\n    text: 'Time Series',\n    value: ModulesKeyTypes.TimeSeries,\n    color: GROUP_TYPES_COLORS[ModulesKeyTypes.TimeSeries],\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/filter-key-type/index.ts",
    "content": "import FilterKeyType from './FilterKeyType'\n\nexport default FilterKeyType\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/filter-key-type/styles.module.scss",
    "content": ".container {\n  width: 168px;\n  height: 36px;\n  flex-shrink: 0;\n  position: relative;\n\n  :global {\n    .euiFormControlLayout {\n      .euiSuperSelectControl {\n        display: flex;\n        align-items: center;\n        height: 36px !important;\n        background-color: var(--euiColorEmptyShade) !important;\n        box-shadow: none !important;\n        border: 1px solid var(--controlsBorderColor) !important;\n        border-left: 0 !important;\n        border-right: 0 !important;\n        padding: 0 25px 0 6px !important;\n\n        &.euiSuperSelect--isOpen__button,\n        &:focus,\n        &:not(:disabled):hover {\n          background-color: var(--euiColorEmptyShade) !important;\n        }\n      }\n    }\n    .euiPopover:not(.euiSuperSelect) {\n      svg {\n        width: 18px !important;\n        height: 18px !important;\n      }\n    }\n    .euiPopover.euiPopover-isOpen {\n      .euiIcon {\n        color: var(--euiTextSubduedColorHover);\n      }\n    }\n    .euiFormControlLayoutIcons {\n      :global(.euiIcon) {\n        width: 16px;\n        height: 12px;\n      }\n    }\n  }\n}\n\n@include global.insights-open {\n  .container {\n    width: 128px;\n  }\n}\n\n.filterKeyType {\n  position: relative;\n  height: 30px;\n  line-height: 14px !important;\n  padding: 4px !important;\n  background-color: var(--euiColorEmptyShade);\n\n  &:hover,\n  &:focus {\n    background-color: var(--hoverInListColorDarken) !important;\n  }\n\n  :global {\n    svg {\n      margin-left: 8px;\n      margin-right: 2px;\n      width: 16px;\n      height: 14px;\n    }\n  }\n}\n\n.controlsIcon {\n  cursor: pointer;\n  margin-left: 6px;\n  height: 16px !important;\n  width: 16px !important;\n  &:global(.euiIcon) {\n    color: var(--inputTextColor) !important;\n  }\n}\n\n.unsupported {\n  .allTypes {\n    cursor: not-allowed;\n  }\n  .allTypesIcon {\n    cursor: not-allowed;\n    color: var(--euiTextSubduedColor);\n  }\n\n  :global(.euiFormControlLayoutIcons) {\n    display: none;\n  }\n}\n\n.unsupportedInfo {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 1;\n\n  cursor: pointer;\n}\n\n.unsupportedInfoModal {\n  width: 520px !important;\n  max-width: 520px !important;\n  z-index: 10000 !important;\n}\n\n.dropdownOption {\n  padding-left: 6px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-list/KeyList.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { fireEvent } from '@testing-library/react'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  waitFor,\n  screen,\n  clearStoreActions,\n} from 'uiSrc/utils/test-utils'\nimport {\n  KeysStoreData,\n  KeyViewType,\n  SearchMode,\n} from 'uiSrc/slices/interfaces/keys'\nimport {\n  deleteKey,\n  keysSelector,\n  setLastBatchKeys,\n} from 'uiSrc/slices/browser/keys'\nimport { apiService } from 'uiSrc/services'\nimport { BrowserColumns } from 'uiSrc/constants'\nimport { bufferToHex, bufferToString } from 'uiSrc/utils'\nimport { RedisResponseBufferType } from 'uiSrc/slices/interfaces'\n\nimport KeyList from './KeyList'\n\nconst propsMock = {\n  keysState: {\n    keys: [\n      {\n        name: {\n          data: Buffer.from('key1'),\n          type: RedisResponseBufferType.Buffer,\n        },\n        type: 'hash',\n        ttl: -1,\n        size: 100,\n        length: 100,\n        nameString: 'key1',\n      },\n      {\n        name: {\n          data: Buffer.from('key2'),\n          type: RedisResponseBufferType.Buffer,\n        },\n        type: 'hash',\n        ttl: -1,\n        size: 150,\n        length: 100,\n        nameString: 'key2',\n      },\n      {\n        name: {\n          data: Buffer.from('key3'),\n          type: RedisResponseBufferType.Buffer,\n        },\n        type: 'hash',\n        ttl: -1,\n        size: 110,\n        length: 100,\n        nameString: 'key3',\n      },\n    ],\n    nextCursor: '0',\n    total: 3,\n    scanned: 5,\n    shardsMeta: {},\n    previousResultCount: 1,\n    lastRefreshTime: 3,\n  } as KeysStoreData,\n  loading: false,\n  selectKey: jest.fn(),\n  loadMoreItems: jest.fn(),\n  handleAddKeyPanel: jest.fn(),\n  onDelete: jest.fn(),\n  commonFilterType: null,\n  onAddKeyPanel: jest.fn(),\n}\n\nconst mockedKeySlice = {\n  viewType: KeyViewType.Browser,\n  searchMode: SearchMode.Pattern,\n  isSearch: false,\n  isFiltered: false,\n  deleting: false,\n  data: {\n    keys: [],\n    nextCursor: '0',\n    previousResultCount: 0,\n    total: 0,\n    scanned: 0,\n    lastRefreshTime: Date.now(),\n  },\n  selectedKey: {\n    data: null,\n  },\n}\n\nconst getKeyFormat = (keyName: string) => ({\n  data: Buffer.from(keyName),\n  type: RedisResponseBufferType.Buffer,\n})\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  keysSelector: jest.fn().mockImplementation(() => mockedKeySlice),\n}))\n\nconst mockedUseKeyFormatHandler = jest.fn().mockImplementation(bufferToString)\n\njest.mock('../use-key-format', () => ({\n  useKeyFormat: () => ({\n    handler: mockedUseKeyFormatHandler,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('KeyList', () => {\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(render(<KeyList {...propsMock} />)).toBeTruthy()\n  })\n\n  it('should render keys encoded in hex', () => {\n    mockedUseKeyFormatHandler.mockImplementation(bufferToHex)\n\n    render(<KeyList {...propsMock} />)\n\n    expect(screen.getByTestId('key-6b657931')).toBeInTheDocument()\n  })\n\n  it('should render rows properly', () => {\n    const { container } = render(<KeyList {...propsMock} />)\n    const rows = container.querySelectorAll(\n      '.ReactVirtualized__Table__row[role=\"row\"]',\n    )\n    expect(rows).toHaveLength(3)\n  })\n\n  // TODO: find solution for mock \"setLastBatchKeys\" action\n  it.skip('should call \"setLastBatchKeys\" after unmount for Browser view', () => {\n    ;(keysSelector as jest.Mock).mockImplementation(() => mockedKeySlice)\n\n    const { unmount } = render(<KeyList {...propsMock} />)\n    expect(setLastBatchKeys).not.toBeCalled()\n\n    unmount()\n\n    expect(setLastBatchKeys).toBeCalledTimes(1)\n  })\n\n  // TODO: find solution for mock \"setLastBatchKeys\" action\n  it.skip('should not call \"setLastBatchKeys\" after unmount for Tree view', () => {\n    ;(keysSelector as jest.Mock).mockImplementation(() => ({\n      ...mockedKeySlice,\n      viewType: KeyViewType.Tree,\n    }))\n\n    const { unmount } = render(<KeyList {...propsMock} />)\n    expect(setLastBatchKeys).not.toBeCalled()\n\n    unmount()\n\n    expect(setLastBatchKeys).not.toBeCalled()\n  })\n\n  it('should call apiService.post to get key info', async () => {\n    const apiServiceMock = jest\n      .fn()\n      .mockResolvedValue(cloneDeep(propsMock.keysState.keys))\n    apiService.post = apiServiceMock\n\n    const { rerender } = render(\n      <KeyList\n        {...propsMock}\n        keysState={{ ...propsMock.keysState, keys: [] }}\n      />,\n    )\n\n    rerender(\n      <KeyList\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          keys: propsMock.keysState.keys.map(({ name }) => ({ name })),\n        }}\n      />,\n    )\n\n    await waitFor(\n      async () => {\n        expect(apiServiceMock).toBeCalled()\n      },\n      { timeout: 150 },\n    )\n  })\n\n  it('apiService.post should be called with only keys without info', async () => {\n    const controller = new AbortController()\n    const params = { params: { encoding: 'buffer' }, signal: controller.signal }\n    const apiServiceMock = jest\n      .fn()\n      .mockResolvedValue(cloneDeep(propsMock.keysState.keys))\n    apiService.post = apiServiceMock\n\n    const { rerender } = render(\n      <KeyList\n        {...propsMock}\n        keysState={{ ...propsMock.keysState, keys: [] }}\n      />,\n    )\n\n    rerender(\n      <KeyList\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          keys: [\n            ...cloneDeep(propsMock.keysState.keys).map(({ name }) => ({\n              name,\n            })),\n            { name: getKeyFormat('key5'), size: 100, length: 100 }, // key with info\n          ],\n        }}\n      />,\n    )\n\n    await waitFor(\n      async () => {\n        expect(apiServiceMock.mock.calls[0]).toEqual([\n          '/databases//keys/get-metadata',\n          { keys: [getKeyFormat('key1')], includeSize: true, includeTTL: true },\n          params,\n        ])\n\n        expect(apiServiceMock.mock.calls[1]).toEqual([\n          '/databases//keys/get-metadata',\n          {\n            keys: [\n              getKeyFormat('key1'),\n              getKeyFormat('key2'),\n              getKeyFormat('key3'),\n            ],\n            includeSize: true,\n            includeTTL: true,\n          },\n          params,\n        ])\n      },\n      { timeout: 150 },\n    )\n  })\n\n  it('key info loadings (type, ttl, size) should be in the DOM if keys do not have info', async () => {\n    const { rerender, queryAllByTestId } = render(\n      <KeyList\n        {...propsMock}\n        keysState={{ ...propsMock.keysState, keys: [] }}\n      />,\n    )\n\n    rerender(\n      <KeyList\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          keys: [\n            ...cloneDeep(propsMock).keysState.keys.map(({ name }) => ({\n              name,\n            })),\n          ],\n        }}\n      />,\n    )\n\n    expect(queryAllByTestId(/ttl-loading/).length).toEqual(\n      propsMock.keysState.keys.length,\n    )\n    expect(queryAllByTestId(/type-loading/).length).toEqual(\n      propsMock.keysState.keys.length,\n    )\n    expect(queryAllByTestId(/size-loading/).length).toEqual(\n      propsMock.keysState.keys.length,\n    )\n  })\n\n  it('should call proper action after click on delete', async () => {\n    const { container } = render(<KeyList {...propsMock} />)\n\n    fireEvent.focus(\n      container.querySelectorAll(\n        '.ReactVirtualized__Table__row[role=\"row\"]',\n      )[0],\n    )\n\n    fireEvent.click(screen.getByTestId('delete-key-btn-key1'))\n    fireEvent.click(screen.getByTestId('submit-delete-key'))\n\n    const expectedActions = [deleteKey()]\n    expect(clearStoreActions(store.getActions().slice(-1))).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should refetch metadata when columns change', async () => {\n    const spy = jest.spyOn(apiService, 'post')\n\n    const keySelectorMocked = keysSelector as jest.Mock\n\n    keySelectorMocked.mockReturnValue({\n      ...mockedKeySlice,\n      shownColumns: [],\n    })\n\n    const { rerender } = render(\n      <KeyList\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          keys: [\n            {\n              name: {\n                data: Buffer.from('test-key'),\n                type: RedisResponseBufferType.Buffer,\n              },\n            },\n          ],\n        }}\n      />,\n    )\n\n    keySelectorMocked.mockReturnValue({\n      ...mockedKeySlice,\n      shownColumns: [BrowserColumns.TTL],\n    })\n\n    rerender(\n      <KeyList\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          keys: [\n            {\n              name: {\n                data: Buffer.from('test-key'),\n                type: RedisResponseBufferType.Buffer,\n              },\n            },\n          ],\n        }}\n      />,\n    )\n\n    await waitFor(\n      () => {\n        expect(spy).toHaveBeenCalled()\n      },\n      { timeout: 1000 },\n    )\n  })\n\n  it.each`\n    columns                                      | description\n    ${[]}                                        | ${'no columns are shown'}\n    ${[BrowserColumns.TTL]}                      | ${'only TTL column is shown'}\n    ${[BrowserColumns.Size]}                     | ${'only Size column is shown'}\n    ${[BrowserColumns.TTL, BrowserColumns.Size]} | ${'both TTL and Size columns are shown'}\n  `('should render DeleteKeyPopover when $description', ({ columns }) => {\n    ;(keysSelector as jest.Mock).mockImplementation(() => ({\n      ...mockedKeySlice,\n      shownColumns: columns,\n    }))\n\n    const { container } = render(<KeyList {...propsMock} />)\n\n    expect(\n      container.querySelector(\n        `[data-testid=\"delete-key-btn-${propsMock.keysState.keys[0].nameString}\"]`,\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-list/KeyList.styles.ts",
    "content": "import { ReactNode, HTMLAttributes } from 'react'\nimport styled, { css } from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Page = styled(Col)`\n  height: 100%;\n  overflow: hidden;\n`\n\nexport const Content = styled.div`\n  width: 100%;\n  height: 100%;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.card.bgColor};\n`\n\nexport const KeyListTable = styled.div<HTMLAttributes<HTMLDivElement>>`\n  height: 100%;\n`\n\nexport const Table = styled.div<{\n  $withoutFooter?: boolean\n  children?: ReactNode\n}>`\n  height: 100%;\n\n  .deleteAnchor {\n    margin-right: -10px;\n  }\n\n  .ReactVirtualized__Table__row {\n    .ReactVirtualized__Table__rowColumn {\n      .moveOnHoverKey {\n        transition: transform ease 0.3s;\n\n        &.hide {\n          transform: translateX(-8px);\n        }\n      }\n\n      .showOnHoverKey {\n        display: none;\n\n        &.show {\n          display: block !important;\n        }\n      }\n    }\n\n    &:hover {\n      .ReactVirtualized__Table__rowColumn {\n        .moveOnHoverKey {\n          transform: translateX(-8px);\n        }\n\n        .showOnHoverKey {\n          display: block !important;\n        }\n      }\n    }\n  }\n\n  ${({ $withoutFooter }) =>\n    $withoutFooter &&\n    css`\n      border-bottom: 1px solid\n        ${({ theme }: { theme: Theme }) =>\n          theme.semantic.color.border.neutral500};\n    `}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx",
    "content": "import React, {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { debounce, findIndex, isUndefined, reject } from 'lodash'\n\nimport { CellMeasurerCache } from 'react-virtualized'\nimport {\n  bufferToString,\n  bufferFormatRangeItems,\n  Nullable,\n  Maybe,\n} from 'uiSrc/utils'\nimport {\n  deleteKeyAction,\n  fetchKeysMetadata,\n  keysDataSelector,\n  keysSelector,\n  selectedKeySelector,\n  sourceKeysFetch,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  appContextBrowser,\n  setBrowserPatternScrollPosition,\n  setBrowserIsNotRendered,\n  setBrowserRedisearchScrollPosition,\n  appContextDbConfig,\n} from 'uiSrc/slices/app/context'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport {\n  BrowserColumns,\n  KeyTypes,\n  ModulesKeyTypes,\n  TableCellAlignment,\n  TableCellTextAlignment,\n} from 'uiSrc/constants'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport KeyRowTTL from 'uiSrc/pages/browser/components/key-row-ttl'\nimport KeyRowSize from 'uiSrc/pages/browser/components/key-row-size'\nimport KeyRowName from 'uiSrc/pages/browser/components/key-row-name'\nimport KeyRowType from 'uiSrc/pages/browser/components/key-row-type'\n\nimport { GetKeyInfoResponse } from 'apiSrc/modules/browser/keys/dto'\n\nimport * as S from './KeyList.styles'\nimport { Props } from './KeyList.types'\nimport NoKeysMessage from '../no-keys-message'\nimport { DeleteKeyPopover } from '../delete-key-popover/DeleteKeyPopover'\nimport { useKeyFormat } from '../use-key-format'\n\nconst cellCache = new CellMeasurerCache({\n  fixedWidth: true,\n  minHeight: 43,\n})\n\nconst KeyList = forwardRef((props: Props, ref) => {\n  let wheelTimer = 0\n  const {\n    selectKey,\n    loadMoreItems,\n    loading,\n    keysState,\n    scrollTopPosition,\n    hideFooter,\n    visibleColumns: visibleColumnsProp,\n    onDelete,\n    commonFilterType,\n    onAddKeyPanel,\n  } = props\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { handler: keyFormatConvertor } = useKeyFormat()\n\n  const selectedKey = useSelector(selectedKeySelector)\n  const { nextCursor, previousResultCount } = useSelector(keysDataSelector)\n  const { isSearched, isFiltered, searchMode } = useSelector(keysSelector)\n  const { shownColumns } = useSelector(appContextDbConfig)\n  const visibleColumns = visibleColumnsProp ?? shownColumns\n  const {\n    keyList: { isNotRendered: isNotRenderedContext },\n  } = useSelector(appContextBrowser)\n\n  const [, rerender] = useState({})\n  const [firstDataLoaded, setFirstDataLoaded] = useState<boolean>(\n    !!keysState.keys.length || !isNotRenderedContext,\n  )\n  const [deletePopoverIndex, setDeletePopoverIndex] =\n    useState<Maybe<number>>(undefined)\n\n  const controller = useRef<Nullable<AbortController>>(null)\n  const itemsRef = useRef(keysState.keys)\n  const renderedRowsIndexesRef = useRef({ startIndex: 0, lastIndex: 0 })\n\n  const dispatch = useDispatch()\n\n  const prevIncludeSize = useRef(shownColumns?.includes(BrowserColumns.Size))\n  const prevIncludeTTL = useRef(shownColumns?.includes(BrowserColumns.TTL))\n\n  useImperativeHandle(ref, () => ({\n    handleLoadMoreItems(config: { startIndex: number; stopIndex: number }) {\n      onLoadMoreItems(config)\n    },\n  }))\n\n  useEffect(() => {\n    cancelAllMetadataRequests()\n  }, [searchMode])\n\n  useEffect(() => {\n    itemsRef.current = [...keysState.keys]\n\n    if (\n      (!firstDataLoaded && keysState.lastRefreshTime) ||\n      (searchMode === SearchMode.Redisearch && itemsRef.current.length === 0)\n    ) {\n      setFirstDataLoaded(true)\n      dispatch(setBrowserIsNotRendered(false))\n    }\n\n    if (itemsRef.current.length === 0) {\n      cancelAllMetadataRequests()\n      rerender({})\n      return\n    }\n\n    cancelAllMetadataRequests()\n    controller.current = new AbortController()\n\n    const { startIndex, lastIndex } = renderedRowsIndexesRef.current\n    onRowsRendered(startIndex, lastIndex)\n    rerender({})\n  }, [keysState.keys])\n\n  useEffect(() => {\n    const isSizeReenabled =\n      !prevIncludeSize.current && shownColumns.includes(BrowserColumns.Size)\n    const isTtlReenabled =\n      !prevIncludeTTL.current && shownColumns.includes(BrowserColumns.TTL)\n\n    if (\n      (isSizeReenabled || isTtlReenabled) &&\n      firstDataLoaded &&\n      itemsRef.current.length > 0\n    ) {\n      cancelAllMetadataRequests()\n      controller.current = new AbortController()\n\n      const { startIndex, lastIndex } = renderedRowsIndexesRef.current\n      const visibleItems = bufferFormatRangeItems(\n        itemsRef.current,\n        startIndex,\n        lastIndex,\n        formatItem,\n      )\n\n      getMetadata(startIndex, visibleItems, true)\n    }\n\n    prevIncludeSize.current = shownColumns.includes(BrowserColumns.Size)\n    prevIncludeTTL.current = shownColumns.includes(BrowserColumns.TTL)\n  }, [shownColumns])\n\n  const cancelAllMetadataRequests = () => {\n    controller.current?.abort()\n  }\n\n  const NoItemsMessage = () => (\n    <NoKeysMessage\n      isLoading={loading || !firstDataLoaded}\n      total={keysState.total}\n      scanned={keysState.scanned}\n      onAddKeyPanel={onAddKeyPanel}\n    />\n  )\n\n  const onLoadMoreItems = (props: {\n    startIndex: number\n    stopIndex: number\n  }) => {\n    if (\n      searchMode === SearchMode.Redisearch &&\n      keysState.maxResults &&\n      keysState.keys.length >= keysState.maxResults\n    ) {\n      return\n    }\n    loadMoreItems?.(itemsRef.current as IKeyPropTypes[], props)\n  }\n\n  const onWheelSearched = (event: React.WheelEvent) => {\n    setDeletePopoverIndex(undefined)\n    if (\n      !loading &&\n      (isSearched || isFiltered) &&\n      event.deltaY > 0 &&\n      !sourceKeysFetch &&\n      nextCursor !== '0' &&\n      previousResultCount === 0\n    ) {\n      clearTimeout(wheelTimer)\n      wheelTimer = window.setTimeout(() => {\n        onLoadMoreItems({ stopIndex: SCAN_COUNT_DEFAULT, startIndex: 1 })\n      }, 100)\n    }\n  }\n\n  const handleDeletePopoverOpen = (\n    index: Maybe<number>,\n    type: KeyTypes | ModulesKeyTypes,\n  ) => {\n    if (index !== deletePopoverIndex) {\n      sendEventTelemetry({\n        event: TelemetryEvent.BROWSER_KEY_DELETE_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          keyType: type,\n          source: 'keyList',\n        },\n      })\n    }\n    setDeletePopoverIndex(index !== deletePopoverIndex ? index : undefined)\n  }\n\n  const handleRemoveKey = (key: RedisResponseBuffer) => {\n    dispatch(\n      deleteKeyAction(key, () => {\n        setDeletePopoverIndex(undefined)\n        onDelete(key)\n      }),\n    )\n  }\n\n  const setScrollTopPosition = useCallback(\n    (position: number) => {\n      if (searchMode === SearchMode.Pattern) {\n        dispatch(setBrowserPatternScrollPosition(position))\n      } else {\n        dispatch(setBrowserRedisearchScrollPosition(position))\n      }\n    },\n    [searchMode],\n  )\n\n  const formatItem = useCallback(\n    (item: GetKeyInfoResponse) => ({\n      ...item,\n      nameString: bufferToString(item.name as string),\n    }),\n    [],\n  )\n\n  const onRowsRendered = (startIndex: number, lastIndex: number) => {\n    renderedRowsIndexesRef.current = { lastIndex, startIndex }\n\n    const newItems = bufferFormatRows(startIndex, lastIndex)\n\n    getMetadata(startIndex, newItems)\n    rerender({})\n  }\n\n  const onRowsRenderedOverscan = (startIndex: number, lastIndex: number) => {\n    const { startIndex: prevStartIndex, lastIndex: prevLastIndex } =\n      renderedRowsIndexesRef.current\n    if (prevStartIndex === startIndex && prevLastIndex === lastIndex) return\n\n    onRowsRendered(startIndex, lastIndex)\n  }\n  const onRowsRenderedDebounced = debounce(onRowsRenderedOverscan, 100)\n\n  const bufferFormatRows = (\n    startIndex: number,\n    lastIndex: number,\n  ): IKeyPropTypes[] => {\n    const newItems = bufferFormatRangeItems(\n      itemsRef.current,\n      startIndex,\n      lastIndex,\n      formatItem,\n    )\n    itemsRef.current.splice(startIndex, newItems.length, ...newItems)\n\n    return newItems\n  }\n\n  const getMetadata = useCallback(\n    (\n      initialStartIndex: number,\n      itemsInit: IKeyPropTypes[] = [],\n      forceRefresh?: boolean,\n    ): void => {\n      const isSomeNotUndefined = ({ type, size, length }: IKeyPropTypes) =>\n        (!commonFilterType && !isUndefined(type)) ||\n        !isUndefined(size) ||\n        !isUndefined(length)\n\n      let startIndex = initialStartIndex\n      let itemsToProcess = itemsInit\n\n      if (!forceRefresh) {\n        const firstEmptyItemIndex = findIndex(\n          itemsInit,\n          (item) => !isSomeNotUndefined(item),\n        )\n        if (firstEmptyItemIndex === -1) return\n\n        startIndex = initialStartIndex + firstEmptyItemIndex\n        itemsToProcess = itemsInit.slice(firstEmptyItemIndex)\n      }\n\n      const itemsToFetch = forceRefresh\n        ? itemsToProcess\n        : reject(itemsToProcess, isSomeNotUndefined)\n\n      dispatch(\n        fetchKeysMetadata(\n          itemsToFetch.map(({ name }) => name),\n          commonFilterType,\n          controller.current?.signal,\n          (loadedItems) => onSuccessFetchedMetadata(startIndex, loadedItems),\n          () => {\n            rerender({})\n          },\n        ),\n      )\n    },\n    [commonFilterType],\n  )\n\n  const onSuccessFetchedMetadata = (\n    startIndex: number,\n    loadedItems: GetKeyInfoResponse[],\n  ) => {\n    const items = loadedItems.map(formatItem)\n    itemsRef.current.splice(startIndex, items.length, ...items)\n\n    rerender({})\n  }\n\n  const isTtlTheLastColumn = !visibleColumns.includes(BrowserColumns.Size)\n  const ttlColumnSize = isTtlTheLastColumn ? 146 : 86\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'type',\n      label: 'Type',\n      absoluteWidth: 'auto',\n      minWidth: 126,\n      render: (cellData: any, { nameString }: any) => (\n        <KeyRowType type={cellData} nameString={nameString} />\n      ),\n    },\n    {\n      id: 'nameString',\n      label: 'Key',\n      minWidth: 94,\n      truncateText: true,\n      render: (\n        _cellData: string,\n        { name, type }: IKeyPropTypes,\n        _expanded,\n        rowIndex,\n      ) => {\n        const nameString = keyFormatConvertor(name)\n        return (\n          <>\n            <KeyRowName nameString={nameString} shortName={nameString} />\n            {columns[columns.length - 1].id === 'nameString' && (\n              <DeleteKeyPopover\n                deletePopoverId={deletePopoverIndex}\n                nameString={nameString}\n                name={name}\n                type={type}\n                rowId={rowIndex || 0}\n                onDelete={handleRemoveKey}\n                onOpenPopover={handleDeletePopoverOpen}\n              />\n            )}\n          </>\n        )\n      },\n    },\n    visibleColumns.includes(BrowserColumns.TTL)\n      ? {\n          id: 'ttl',\n          label: 'TTL',\n          absoluteWidth: ttlColumnSize,\n          minWidth: ttlColumnSize,\n          truncateText: true,\n          alignment: TableCellAlignment.Right,\n          render: (\n            cellData: number,\n            { nameString, name, type }: IKeyPropTypes,\n            _expanded,\n            rowIndex,\n          ) => (\n            <>\n              <KeyRowTTL\n                ttl={cellData}\n                nameString={nameString}\n                deletePopoverId={deletePopoverIndex}\n                rowId={rowIndex || 0}\n              />\n              {isTtlTheLastColumn && (\n                <DeleteKeyPopover\n                  deletePopoverId={deletePopoverIndex}\n                  nameString={nameString}\n                  name={name}\n                  type={type}\n                  rowId={rowIndex || 0}\n                  onDelete={handleRemoveKey}\n                  onOpenPopover={handleDeletePopoverOpen}\n                />\n              )}\n            </>\n          ),\n        }\n      : null,\n    visibleColumns.includes(BrowserColumns.Size)\n      ? {\n          id: 'size',\n          label: 'Size',\n          absoluteWidth: 90,\n          minWidth: 90,\n          alignment: TableCellAlignment.Right,\n          textAlignment: TableCellTextAlignment.Right,\n          render: (\n            cellData: number,\n            { nameString, name, type }: IKeyPropTypes,\n            _expanded,\n            rowIndex,\n          ) => (\n            <>\n              <KeyRowSize\n                size={cellData}\n                nameString={nameString}\n                deletePopoverId={deletePopoverIndex}\n                rowId={rowIndex || 0}\n              />\n              {columns[columns.length - 1].id === 'size' && (\n                <DeleteKeyPopover\n                  deletePopoverId={deletePopoverIndex}\n                  nameString={nameString}\n                  name={name}\n                  type={type}\n                  rowId={rowIndex || 0}\n                  onDelete={handleRemoveKey}\n                  onOpenPopover={handleDeletePopoverOpen}\n                />\n              )}\n            </>\n          ),\n        }\n      : null,\n  ].filter((el) => !!el)\n\n  const noItemsMessage = NoItemsMessage()\n\n  const VirtualizeTable = () => (\n    <VirtualTable\n      selectable\n      onRowClick={selectKey}\n      headerHeight={0}\n      rowHeight={43}\n      threshold={50}\n      columns={columns}\n      cellCache={cellCache}\n      loadMoreItems={onLoadMoreItems}\n      onWheel={onWheelSearched}\n      loading={loading || !firstDataLoaded}\n      items={itemsRef.current}\n      totalItemsCount={keysState.total ?? Infinity}\n      scanned={isSearched || isFiltered ? keysState.scanned : 0}\n      noItemsMessage={noItemsMessage}\n      selectedKey={selectedKey.data}\n      scrollTopProp={scrollTopPosition}\n      setScrollTopPosition={setScrollTopPosition}\n      hideFooter={hideFooter}\n      onRowsRendered={({ overscanStartIndex, overscanStopIndex }) =>\n        onRowsRenderedDebounced(overscanStartIndex, overscanStopIndex)\n      }\n    />\n  )\n\n  return (\n    <S.Page>\n      <S.Content>\n        <S.Table $withoutFooter={hideFooter}>\n          <S.KeyListTable data-testid=\"keyList-table\">\n            {searchMode === SearchMode.Pattern && VirtualizeTable()}\n            {searchMode !== SearchMode.Pattern && VirtualizeTable()}\n          </S.KeyListTable>\n        </S.Table>\n      </S.Content>\n    </S.Page>\n  )\n})\n\nexport default React.memo(KeyList)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-list/KeyList.types.ts",
    "content": "import { BrowserColumns, KeyTypes } from 'uiSrc/constants'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { KeysStoreData } from 'uiSrc/slices/interfaces/keys'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\n\nexport interface Props {\n  keysState: KeysStoreData\n  loading: boolean\n  scrollTopPosition?: number\n  hideFooter?: boolean\n  visibleColumns?: BrowserColumns[]\n  selectKey: ({ rowData }: { rowData: any }) => void\n  loadMoreItems?: (\n    oldKeys: IKeyPropTypes[],\n    { startIndex, stopIndex }: { startIndex: number; stopIndex: number },\n  ) => void\n  onDelete: (key: RedisResponseBuffer) => void\n  commonFilterType: Nullable<KeyTypes>\n  onAddKeyPanel: (value: boolean) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-list/index.ts",
    "content": "import KeyList from './KeyList'\n\nexport default KeyList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-list/styles.module.scss",
    "content": ".page {\n  height: 100%;\n  overflow: hidden;\n}\n\n.tooltip {\n  max-width: 372px !important;\n}\n\n.content {\n  width: 100%;\n  height: 100%;\n  background-color: var(--euiColorEmptyShade);\n}\n\n.deletePopover {\n  max-width: 400px !important;\n}\n\n.table {\n  height: 100%;\n\n  .deleteAnchor {\n    margin-right: -10px;\n  }\n\n  :global {\n    .ReactVirtualized__Table__row {\n      .ReactVirtualized__Table__rowColumn {\n        .moveOnHoverKey {\n          transition: transform ease 0.3s;\n\n          &.hide {\n            transform: translateX(-8px)\n          }\n        }\n\n        .showOnHoverKey {\n          display: none;\n\n          &.show {\n            display: block !important;\n          }\n        }\n      }\n\n      &:hover {\n        .ReactVirtualized__Table__rowColumn {\n          .moveOnHoverKey {\n            transform: translateX(-8px)\n          }\n\n          .showOnHoverKey {\n            display: block !important;\n          }\n        }\n      }\n    }\n  }\n}\n\n:global(.show-cli) .table {\n  border-bottom: 1px solid var(--euiColorLightShade);\n}\n\n.filter {\n  display: inline-block;\n  width: 100px;\n  height: 38px;\n  margin-left: 8px;\n  background-color: var(--tableLightestBorderColor);\n}\n\n.action {\n  opacity: 0;\n  transition: opacity 250ms ease-in-out;\n}\n\n.keyInfoLoading {\n  width: 44px;\n\n  :global(.euiLoadingContent__singleLine) {\n    margin-bottom: 0;\n  }\n}\n\n.keyNameLoading {\n  width: 50%;\n  min-width: 100px;\n  max-width: 300px;\n}\n\n:global(.table-row-selected) .action {\n  opacity: 1;\n}"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport KeyRowName, { Props } from './KeyRowName'\n\nconst mockedProps = mock<Props>()\n\nconst loadingTestId = 'name-loading'\n\ndescribe('KeyRowName', () => {\n  it('should render', () => {\n    expect(render(<KeyRowName {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render Loading if no nameString and shortName', () => {\n    const { queryByTestId } = render(\n      <KeyRowName nameString={undefined} shortName={undefined} />,\n    )\n\n    expect(queryByTestId(loadingTestId)).toBeInTheDocument()\n  })\n\n  it('content should be no more than 200 symbols', () => {\n    const longName = Array.from({ length: 250 }, () => '1').join('')\n    const { queryByTestId } = render(\n      <KeyRowName nameString={longName} shortName={longName} />,\n    )\n\n    expect(queryByTestId(loadingTestId)).not.toBeInTheDocument()\n    expect(queryByTestId(`key-${longName}`)).toHaveTextContent(\n      longName.slice(0, 200),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx",
    "content": "import React from 'react'\nimport { isUndefined } from 'lodash'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Maybe, formatLongName, replaceSpaces } from 'uiSrc/utils'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  nameString: Maybe<string>\n  shortName: Maybe<string>\n}\n\nconst KeyRowName = (props: Props) => {\n  const { nameString, shortName } = props\n\n  if (isUndefined(shortName)) {\n    return (\n      <LoadingContent\n        lines={1}\n        className={styles.keyInfoLoading}\n        data-testid=\"name-loading\"\n      />\n    )\n  }\n\n  // Better to cut the long string, because it could affect virtual scroll performance\n  const nameContent = replaceSpaces(shortName?.substring?.(0, 200))\n  const nameTooltipContent = formatLongName(nameString)\n\n  return (\n    <div className={styles.keyName}>\n      <Text\n        component=\"div\"\n        color=\"secondary\"\n        style={{ maxWidth: '100%', display: 'flex', paddingRight: 16 }}\n      >\n        <div\n          style={{ display: 'flex' }}\n          className=\"truncateText\"\n          data-testid={`key-${shortName}`}\n        >\n          <RiTooltip\n            title=\"Key Name\"\n            className={styles.tooltip}\n            position=\"bottom\"\n            content={nameTooltipContent}\n          >\n            <>{nameContent}</>\n          </RiTooltip>\n        </div>\n      </Text>\n    </div>\n  )\n}\n\nexport default KeyRowName\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-name/index.ts",
    "content": "import KeyRowName from './KeyRowName'\n\nexport default KeyRowName\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-name/styles.module.scss",
    "content": ".keyInfoLoading {\n  width: 70%;\n  margin-top: 7px;\n}\n\n.keyName {\n  flex-grow: 1;\n  position: relative;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  :global(.euiTextColor) {\n    max-width: 100%;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport { formatBytes } from 'uiSrc/utils'\nimport KeyRowSize, { Props } from './KeyRowSize'\n\nconst mockedProps = mock<Props>()\nconst loadingTestId = 'size-loading_'\nconst nameString = 'name'\n\ndescribe('KeyRowSize', () => {\n  it('should render', () => {\n    expect(render(<KeyRowSize {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render Loading if no size', () => {\n    const { queryByTestId } = render(\n      <KeyRowSize\n        {...instance(mockedProps)}\n        size={undefined}\n        nameString={nameString}\n      />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument()\n  })\n\n  it('should render \"-\" if size is empty', () => {\n    const { queryByTestId } = render(\n      <KeyRowSize\n        {...instance(mockedProps)}\n        size={0}\n        nameString={nameString}\n      />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument()\n    expect(queryByTestId(`size-${nameString}`)).toHaveTextContent('-')\n  })\n\n  it('should render formatted size', () => {\n    const size = 123123123\n    const { queryByTestId } = render(\n      <KeyRowSize\n        {...instance(mockedProps)}\n        size={123123123}\n        nameString={nameString}\n      />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument()\n    expect(queryByTestId(`size-${nameString}`)).toHaveTextContent(\n      formatBytes(size, 0) as string,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-size/KeyRowSize.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { isUndefined } from 'lodash'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Maybe, formatBytes } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  size: Maybe<number>\n  deletePopoverId: Maybe<number | string>\n  rowId: number | string\n  nameString: string\n}\n\nconst KeyRowSize = (props: Props) => {\n  const { size, nameString, deletePopoverId, rowId } = props\n\n  if (isUndefined(size)) {\n    return (\n      <LoadingContent\n        lines={1}\n        className={cx(styles.keyInfoLoading, styles.keySize)}\n        data-testid={`size-loading_${nameString}`}\n      />\n    )\n  }\n\n  if (!size) {\n    return (\n      <Text\n        component=\"div\"\n        color=\"secondary\"\n        size=\"s\"\n        className={cx(styles.keySize)}\n        data-testid={`size-${nameString}`}\n      >\n        -\n      </Text>\n    )\n  }\n  return (\n    <>\n      <Text\n        component=\"div\"\n        color=\"secondary\"\n        size=\"s\"\n        className={cx(styles.keySize, 'moveOnHoverKey', {\n          hide: deletePopoverId === rowId,\n        })}\n        style={{ maxWidth: '100%' }}\n      >\n        <div\n          style={{ display: 'flex' }}\n          className=\"truncateText\"\n          data-testid={`size-${nameString}`}\n        >\n          <RiTooltip\n            title=\"Key Size\"\n            className={styles.tooltip}\n            anchorClassName=\"truncateText\"\n            position=\"right\"\n            content={<>{formatBytes(size, 3)}</>}\n          >\n            <>{formatBytes(size, 0)}</>\n          </RiTooltip>\n        </div>\n      </Text>\n    </>\n  )\n}\n\nexport default KeyRowSize\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-size/index.ts",
    "content": "import KeyRowSize from './KeyRowSize'\n\nexport default KeyRowSize\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-size/styles.module.scss",
    "content": ".keyInfoLoading {\n  margin-top: 8px;\n  padding-left: 16px;\n}\n\n.keySize {\n  width: 90px;\n  min-width: 90px;\n  text-align: right;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport { truncateNumberToFirstUnit } from 'uiSrc/utils'\nimport KeyRowTTL, { Props } from './KeyRowTTL'\n\nconst mockedProps = mock<Props>()\nconst loadingTestId = 'ttl-loading_'\nconst nameString = 'name'\n\ndescribe('KeyRowTTL', () => {\n  it('should render', () => {\n    expect(render(<KeyRowTTL {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render Loading if no ttl', () => {\n    const { queryByTestId } = render(\n      <KeyRowTTL\n        {...instance(mockedProps)}\n        ttl={undefined}\n        nameString={nameString}\n      />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument()\n  })\n\n  it('should render \"No limit\" if ttl is -1', () => {\n    const { queryByTestId } = render(\n      <KeyRowTTL {...instance(mockedProps)} ttl={-1} nameString={nameString} />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument()\n    expect(queryByTestId(`ttl-${nameString}`)).toHaveTextContent('No limit')\n  })\n\n  it('should render formatted ttl', () => {\n    const ttl = 123123123\n    const { queryByTestId } = render(\n      <KeyRowTTL\n        {...instance(mockedProps)}\n        ttl={123123123}\n        nameString={nameString}\n      />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument()\n    expect(queryByTestId(`ttl-${nameString}`)).toHaveTextContent(\n      truncateNumberToFirstUnit(ttl),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-ttl/KeyRowTTL.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { isUndefined } from 'lodash'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport {\n  Maybe,\n  truncateNumberToDuration,\n  truncateNumberToFirstUnit,\n  truncateTTLToSeconds,\n} from 'uiSrc/utils'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  ttl: Maybe<number>\n  deletePopoverId: Maybe<number | string>\n  rowId: number | string\n  nameString: string\n}\n\nconst KeyRowTTL = (props: Props) => {\n  const { ttl, nameString, deletePopoverId, rowId } = props\n\n  if (isUndefined(ttl)) {\n    return (\n      <LoadingContent\n        lines={1}\n        className={cx(styles.keyInfoLoading, styles.keyTTL)}\n        data-testid={`ttl-loading_${nameString}`}\n      />\n    )\n  }\n  if (ttl === -1) {\n    return (\n      <ColorText\n        className={cx(styles.keyTTL, 'moveOnHoverKey', {\n          hide: deletePopoverId === rowId,\n        })}\n        color=\"secondary\"\n        data-testid={`ttl-${nameString}`}\n      >\n        No limit\n      </ColorText>\n    )\n  }\n  return (\n    <Text\n      component=\"div\"\n      className={cx(styles.keyTTL, 'moveOnHoverKey', {\n        hide: deletePopoverId === rowId,\n      })}\n      color=\"secondary\"\n      size=\"s\"\n    >\n      <div\n        style={{ display: 'flex' }}\n        className=\"truncateText\"\n        data-testid={`ttl-${nameString}`}\n      >\n        <RiTooltip\n          title=\"Time to Live\"\n          className={styles.tooltip}\n          anchorClassName=\"truncateText\"\n          position=\"right\"\n          content={\n            <>\n              {`${truncateTTLToSeconds(ttl)} s`}\n              <br />\n              {`(${truncateNumberToDuration(ttl)})`}\n            </>\n          }\n        >\n          <>{truncateNumberToFirstUnit(ttl)}</>\n        </RiTooltip>\n      </div>\n    </Text>\n  )\n}\n\nexport default KeyRowTTL\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-ttl/index.ts",
    "content": "import KeyRowTTL from './KeyRowTTL'\n\nexport default KeyRowTTL\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-ttl/styles.module.scss",
    "content": ".keyInfoLoading {\n  margin-top: 8px;\n  padding-left: 16px;\n}\n\n.keyTTL {\n  width: 86px;\n  min-width: 86px;\n  text-align: right;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport KeyRowType, { Props } from './KeyRowType'\n\nconst mockedProps = mock<Props>()\nconst loadingTestId = 'type-loading_'\nconst nameString = 'name'\n\ndescribe('KeyRowType', () => {\n  it('should render', () => {\n    expect(render(<KeyRowType {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render Loading if no type', () => {\n    const { queryByTestId } = render(\n      <KeyRowType {...instance(mockedProps)} nameString={nameString} />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).toBeInTheDocument()\n  })\n\n  it('should render Badge if type exists', () => {\n    const type = KeyTypes.Hash\n    const { queryByTestId } = render(\n      <KeyRowType\n        {...instance(mockedProps)}\n        nameString={nameString}\n        type={type}\n      />,\n    )\n\n    expect(queryByTestId(loadingTestId + nameString)).not.toBeInTheDocument()\n    expect(queryByTestId(`badge-${type}_${nameString}`)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-type/KeyRowType.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants'\nimport { GroupBadge, LoadingContent } from 'uiSrc/components'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  nameString: string\n  type: KeyTypes | ModulesKeyTypes\n}\n\nconst KeyRowType = (props: Props) => {\n  const { nameString, type } = props\n\n  return (\n    <>\n      {!type && (\n        <LoadingContent\n          lines={1}\n          className={cx(styles.keyInfoLoading, styles.keyType)}\n          data-testid={`type-loading_${nameString}`}\n        />\n      )}\n      {!!type && (\n        <div className={styles.keyType}>\n          <GroupBadge type={type} name={nameString} />\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default KeyRowType\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-type/index.ts",
    "content": "import KeyRowType from './KeyRowType'\n\nexport default KeyRowType\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-row-type/styles.module.scss",
    "content": ".keyInfoLoading {\n  margin-top: 8px;\n  padding-left: 16px;\n}\n\n.keyType {\n  padding-right: 16px;\n  padding-left: 12px;\n  width: 126px;\n  min-width: 126px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { cleanup, fireEvent, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { KeysStoreData } from 'uiSrc/slices/interfaces/keys'\nimport { setBrowserTreeNodesOpen } from 'uiSrc/slices/app/context'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { MakeSearchableModalProvider } from 'uiSrc/pages/browser/components/make-searchable-modal'\nimport KeyTree from './KeyTree'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst propsMock = {\n  keysState: {\n    keys: [\n      {\n        name: 'key1',\n        type: 'hash',\n        ttl: -1,\n        size: 100,\n        length: 100,\n      },\n      {\n        name: 'key2',\n        type: 'hash',\n        ttl: -1,\n        size: 150,\n        length: 100,\n      },\n      {\n        name: 'key3',\n        type: 'hash',\n        ttl: -1,\n        size: 110,\n        length: 100,\n      },\n    ],\n    nextCursor: '0',\n    total: 3,\n    scanned: 5,\n    shardsMeta: {},\n    previousResultCount: 1,\n    lastRefreshTime: 3,\n  } as KeysStoreData,\n  loading: false,\n  deleting: false,\n  commonFilterType: null,\n  selectKey: jest.fn(),\n  loadMoreItems: jest.fn(),\n  onDelete: jest.fn(),\n  onAddKeyPanel: jest.fn(),\n  onBulkActionsPanel: jest.fn(),\n}\n\nconst leafRootFullName = 'test'\nconst folderFullName = 'car:'\nconst leaf1FullName = 'car:110'\nconst leaf2FullName = 'car:210'\n\nconst mockWebWorkerResult = [\n  {\n    children: [\n      {\n        children: [],\n        fullName: leaf1FullName,\n        id: '0.0',\n        keyApproximate: 0.01,\n        keyCount: 1,\n        name: '110',\n        type: KeyTypes.String,\n        isLeaf: true,\n        nameBuffer: stringToBuffer(leaf1FullName),\n      },\n      {\n        children: [],\n        fullName: leaf2FullName,\n        id: '0.1',\n        keyApproximate: 0.01,\n        keyCount: 1,\n        name: '110',\n        type: KeyTypes.Hash,\n        isLeaf: true,\n        nameBuffer: stringToBuffer(leaf2FullName),\n      },\n    ],\n    fullName: folderFullName,\n    id: '0',\n    keyApproximate: 47.18,\n    keyCount: 4718,\n    name: 'car',\n  },\n  {\n    children: [],\n    fullName: leafRootFullName,\n    id: '1',\n    keyApproximate: 0.01,\n    keyCount: 1,\n    type: KeyTypes.Stream,\n    isLeaf: true,\n    name: 'test',\n    nameBuffer: stringToBuffer(leafRootFullName),\n  },\n]\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  useDisposableWebworker: () => ({\n    result: mockWebWorkerResult,\n    run: jest.fn(),\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeyDataSelector: jest.fn().mockReturnValue(null),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('KeyTree', () => {\n  it('should be rendered', () => {\n    expect(\n      render(\n        <MakeSearchableModalProvider>\n          <KeyTree {...propsMock} />\n        </MakeSearchableModalProvider>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('\"setBrowserTreeNodesOpen\" to be called after click on folder', () => {\n    const onSelectedKeyMock = jest.fn()\n    const { getByTestId } = render(\n      <MakeSearchableModalProvider>\n        <KeyTree {...propsMock} selectKey={onSelectedKeyMock} />\n      </MakeSearchableModalProvider>,\n    )\n\n    // set open state\n    fireEvent.click(getByTestId(`node-item_${folderFullName}`))\n\n    const expectedActions = [\n      setBrowserTreeNodesOpen({ [folderFullName]: true }),\n    ]\n\n    expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions))\n  })\n\n  it('\"selectKey\" to be called after click on leaf', async () => {\n    const onSelectedKeyMock = jest.fn()\n    const { getByTestId } = render(\n      <MakeSearchableModalProvider>\n        <KeyTree {...propsMock} selectKey={onSelectedKeyMock} />\n      </MakeSearchableModalProvider>,\n    )\n\n    // open parent folder\n    fireEvent.click(getByTestId(`node-item_${folderFullName}`))\n\n    // click on the leaf\n    fireEvent.click(getByTestId(`node-item_${leaf2FullName}`))\n\n    expect(onSelectedKeyMock).toBeCalled()\n  })\n\n  it('selected key from key list should be opened and selected in the tree', async () => {\n    const selectedKeyDataSelectorMock = jest.fn().mockReturnValue({\n      name: stringToBuffer(leaf2FullName),\n      nameString: leaf2FullName,\n    })\n\n    ;(selectedKeyDataSelector as jest.Mock).mockImplementation(\n      selectedKeyDataSelectorMock,\n    )\n\n    const { getByTestId } = render(\n      <MakeSearchableModalProvider>\n        <KeyTree {...propsMock} />\n      </MakeSearchableModalProvider>,\n    )\n\n    expect(getByTestId(`node-item_${leaf2FullName}`)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx",
    "content": "import React, {\n  forwardRef,\n  useEffect,\n  useImperativeHandle,\n  useState,\n} from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { useParams } from 'react-router-dom'\nimport { escapeRegExp } from 'lodash'\n\nimport {\n  appContextBrowserTree,\n  appContextDbConfig,\n  resetBrowserTree,\n  setBrowserTreeNodesOpen,\n} from 'uiSrc/slices/app/context'\nimport { constructKeysToTree } from 'uiSrc/helpers'\nimport VirtualTree from 'uiSrc/pages/browser/components/virtual-tree'\nimport TreeViewSVG from 'uiSrc/assets/img/icons/treeview.svg'\nimport { bufferToString, comboBoxToArray, Nullable } from 'uiSrc/utils'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { BulkActionsType, KeyTypes, ModulesKeyTypes } from 'uiSrc/constants'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\nimport {\n  deleteKeyAction,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  setBulkActionType,\n  setBulkDeleteFilter,\n  setBulkDeleteKeyCount,\n  setBulkDeleteSearch,\n  setBulkDeleteStartAgain,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { GetKeyInfoResponse } from 'apiSrc/modules/browser/keys/dto'\n\nimport styles from './styles.module.scss'\nimport { KeyTreeProps } from './KeyTree.types'\nimport NoKeysMessage from '../no-keys-message'\n\nexport const firstPanelId = 'tree'\nexport const secondPanelId = 'keys'\n\nconst parseKeyNames = (keys: GetKeyInfoResponse[]) =>\n  keys.map((item) => ({\n    ...item,\n    nameString: item.nameString ?? bufferToString(item.name),\n  }))\n\nconst KeyTree = forwardRef((props: KeyTreeProps, ref) => {\n  const {\n    selectKey,\n    loadMoreItems,\n    loading,\n    keysState,\n    onDelete,\n    commonFilterType,\n    deleting,\n    onAddKeyPanel,\n    onBulkActionsPanel,\n    visibleColumns,\n    showFolderMetadata,\n    showDeleteAction,\n    showSelectedIndicator,\n  } = props\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { openNodes } = useSelector(appContextBrowserTree)\n  const { treeViewDelimiter, treeViewSort: sorting } =\n    useSelector(appContextDbConfig)\n  const { nameString: selectedKeyName = null } =\n    useSelector(selectedKeyDataSelector) ?? {}\n\n  const [statusOpen, setStatusOpen] = useState(openNodes)\n  const [constructingTree, setConstructingTree] = useState(false)\n  const [firstDataLoaded, setFirstDataLoaded] = useState<boolean>(\n    !!keysState.keys.length,\n  )\n  const [items, setItems] = useState<IKeyPropTypes[]>(\n    parseKeyNames(keysState.keys ?? []),\n  )\n\n  // escape regexp symbols and join and transform to regexp\n  const delimiters = comboBoxToArray(treeViewDelimiter)\n  const delimiterPattern = delimiters.map(escapeRegExp).join('|')\n\n  const dispatch = useDispatch()\n\n  useImperativeHandle(ref, () => ({\n    handleLoadMoreItems(config: { startIndex: number; stopIndex: number }) {\n      onLoadMoreItems(config)\n    },\n  }))\n\n  useEffect(() => {\n    openSelectedKey(selectedKeyName)\n  }, [])\n\n  useEffect(() => {\n    setStatusOpen(openNodes)\n  }, [openNodes])\n\n  // open all parents for selected key\n  const openSelectedKey = (selectedKeyName: Nullable<string> = '') => {\n    if (selectedKeyName) {\n      const parts = selectedKeyName.split(delimiterPattern)\n      const parents = parts.map(\n        (_, index) =>\n          parts.slice(0, index + 1).join(delimiterPattern) + delimiterPattern,\n      )\n\n      // remove key name from parents\n      parents.pop()\n\n      if (parents.length === 0) return\n\n      // Use functional update to avoid stale closure issues\n      setStatusOpen((prevState) => {\n        const newOpenNodes = { ...prevState }\n        parents.forEach((parent) => {\n          newOpenNodes[parent] = true\n        })\n        dispatch(setBrowserTreeNodesOpen(newOpenNodes))\n        return newOpenNodes\n      })\n    }\n  }\n\n  useEffect(() => {\n    setItems(parseKeyNames(keysState.keys))\n\n    if (keysState.keys?.length === 0) {\n      updateSelectedKeys()\n    }\n  }, [keysState.keys])\n\n  useEffect(() => {\n    if (keysState.lastRefreshTime) {\n      setFirstDataLoaded(true)\n    }\n\n    setItems(parseKeyNames(keysState.keys))\n  }, [keysState.lastRefreshTime, delimiterPattern, sorting])\n\n  useEffect(() => {\n    openSelectedKey(selectedKeyName)\n  }, [selectedKeyName])\n\n  const onLoadMoreItems = (props: {\n    startIndex: number\n    stopIndex: number\n  }) => {\n    const formattedAllKeys = parseKeyNames(keysState.keys)\n    loadMoreItems?.(formattedAllKeys, props)\n  }\n\n  // select default leaf \"Keys\" after each change delimiter, filter or search\n  const updateSelectedKeys = () => {\n    dispatch(resetBrowserTree())\n    openSelectedKey(selectedKeyName)\n  }\n\n  const handleStatusOpen = (name: string, value: boolean) => {\n    setStatusOpen((prevState) => {\n      const newState = { ...prevState }\n      // add or remove opened node\n      if (!value) {\n        delete newState[name]\n      } else {\n        newState[name] = value\n      }\n\n      dispatch(setBrowserTreeNodesOpen(newState))\n      return newState\n    })\n  }\n\n  const handleStatusSelected = (name: RedisString) => {\n    selectKey({ rowData: { name } })\n  }\n\n  const handleDeleteLeaf = (key: RedisResponseBuffer) => {\n    dispatch(\n      deleteKeyAction(key, () => {\n        onDelete(key)\n      }),\n    )\n  }\n\n  const handleDeleteClicked = (type: KeyTypes | ModulesKeyTypes) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        keyType: type,\n        source: 'keyList',\n      },\n    })\n  }\n\n  const handleDeleteFolder = (\n    pattern: string,\n    fullName: string,\n    keyCount: number,\n  ) => {\n    // Reset previous bulk delete state first\n    dispatch(setBulkDeleteStartAgain())\n\n    // Set bulk delete state - preserve current key type filter\n    dispatch(setBulkDeleteSearch(pattern))\n    dispatch(setBulkDeleteFilter(commonFilterType))\n    dispatch(setBulkDeleteKeyCount(keyCount))\n    dispatch(setBulkActionType(BulkActionsType.Delete))\n\n    // Open panel\n    onBulkActionsPanel(true)\n\n    // Telemetry\n    sendEventTelemetry({\n      event: TelemetryEvent.TREE_VIEW_FOLDER_DELETE_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        keyCount,\n      },\n    })\n  }\n\n  if (keysState.keys.length === 0) {\n    const NoItemsMessage = () => (\n      <NoKeysMessage\n        isLoading={loading || !firstDataLoaded}\n        total={keysState.total}\n        scanned={keysState.scanned}\n        onAddKeyPanel={onAddKeyPanel}\n      />\n    )\n\n    return (\n      <div className={cx(styles.content)}>\n        <div className={cx(styles.noKeys)}>\n          <NoItemsMessage />\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div className={styles.container}>\n      <div className={styles.content}>\n        <VirtualTree\n          items={items}\n          loadingIcon={TreeViewSVG}\n          delimiters={delimiters}\n          delimiterPattern={delimiterPattern}\n          sorting={sorting}\n          deleting={deleting}\n          statusSelected={selectedKeyName}\n          statusOpen={statusOpen}\n          loading={loading || constructingTree}\n          commonFilterType={commonFilterType}\n          setConstructingTree={setConstructingTree}\n          webworkerFn={constructKeysToTree}\n          onStatusSelected={handleStatusSelected}\n          onStatusOpen={handleStatusOpen}\n          onDeleteClicked={handleDeleteClicked}\n          onDeleteLeaf={handleDeleteLeaf}\n          onDeleteFolder={handleDeleteFolder}\n          visibleColumns={visibleColumns}\n          showFolderMetadata={showFolderMetadata}\n          showDeleteAction={showDeleteAction}\n          showSelectedIndicator={showSelectedIndicator}\n        />\n      </div>\n    </div>\n  )\n})\n\nexport default React.memo(KeyTree)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.types.ts",
    "content": "import { KeysStoreData } from 'uiSrc/slices/interfaces/keys'\nimport { Nullable } from 'uiSrc/utils'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { BrowserColumns, KeyTypes } from 'uiSrc/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nexport interface KeyTreeProps {\n  keysState: KeysStoreData\n  loading: boolean\n  deleting: boolean\n  commonFilterType: Nullable<KeyTypes>\n  selectKey: ({ rowData }: { rowData: any }) => void\n  loadMoreItems: (\n    oldKeys: IKeyPropTypes[],\n    { startIndex, stopIndex }: { startIndex: number; stopIndex: number },\n  ) => void\n  onDelete: (key: RedisResponseBuffer) => void\n  onAddKeyPanel: (value: boolean) => void\n  onBulkActionsPanel: (value: boolean) => void\n  visibleColumns?: BrowserColumns[]\n  showFolderMetadata?: boolean\n  showDeleteAction?: boolean\n  showSelectedIndicator?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { DEFAULT_DELIMITER, SortOrder } from 'uiSrc/constants'\nimport {\n  resetBrowserTree,\n  setBrowserTreeDelimiter,\n  setBrowserTreeSort,\n} from 'uiSrc/slices/app/context'\nimport {\n  cleanup,\n  clearStoreActions,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  act,\n  waitForRiPopoverVisible,\n  waitForRedisUiSelectVisible,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\n\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { comboBoxToArray } from 'uiSrc/utils'\nimport KeyTreeSettings, { Props } from './KeyTreeSettings'\n\nconst mockedProps = mock<Props>()\nlet store: typeof mockedStore\nconst APPLY_BTN = 'tree-view-apply-btn'\nconst TREE_SETTINGS_TRIGGER_BTN = 'tree-view-settings-btn'\nconst SORTING_SELECT = 'tree-view-sorting-select'\nconst SORTING_DESC_ITEM = 'tree-view-sorting-item-DESC'\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('KeyTreeDelimiter', () => {\n  it('should render', () => {\n    expect(render(<KeyTreeSettings {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('Settings button should be rendered', () => {\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN)).toBeInTheDocument()\n  })\n\n  it('Delimiter input and Sorting selector should be rendered after click on button', async () => {\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n    await waitForRiPopoverVisible()\n\n    const comboboxInput = document.querySelector(\n      '[data-testid=\"delimiter-combobox\"] [data-test-subj=\"autoTagInput\"]',\n    ) as HTMLInputElement\n\n    expect(comboboxInput).toBeInTheDocument()\n    expect(screen.getByTestId(SORTING_SELECT)).toBeInTheDocument()\n  })\n\n  it('\"setBrowserTreeDelimiter\" and \"setBrowserTreeSort\" should be called after Apply change delimiter', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    const value = 'val'\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n\n    await waitForRiPopoverVisible()\n\n    const comboboxInput = document.querySelector(\n      '[data-testid=\"delimiter-combobox\"] [data-test-subj=\"autoTagInput\"]',\n    ) as HTMLInputElement\n\n    fireEvent.change(comboboxInput, { target: { value } })\n\n    fireEvent.keyDown(comboboxInput, { key: 'Enter', code: 13, charCode: 13 })\n\n    const containerLabels = document.querySelector(\n      '[data-test-subj=\"autoTagWrapper\"]',\n    )!\n    expect(\n      containerLabels.querySelector(`[title=\"${value}\"]`),\n    ).toBeInTheDocument()\n\n    fireEvent.click(\n      containerLabels.querySelector('[data-test-subj=\"autoTagChip\"] button')!,\n    )\n    expect(containerLabels.querySelector('[title=\":\"]')).not.toBeInTheDocument()\n\n    await userEvent.click(screen.getByTestId(SORTING_SELECT))\n\n    await waitForRedisUiSelectVisible()\n\n    await userEvent.click(screen.getByTestId(SORTING_DESC_ITEM))\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(APPLY_BTN))\n    })\n\n    const expectedActions = [\n      setBrowserTreeDelimiter([{ label: value }]),\n      resetBrowserTree(),\n      setBrowserTreeSort(SortOrder.DESC),\n      resetBrowserTree(),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        from: comboBoxToArray([DEFAULT_DELIMITER]),\n        to: [value],\n      },\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.TREE_VIEW_KEYS_SORTED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        sorting: SortOrder.DESC,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('\"setBrowserTreeDelimiter\" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => {\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n\n    await waitForRiPopoverVisible()\n\n    const containerLabels = document.querySelector(\n      '[data-test-subj=\"autoTagWrapper\"]',\n    )!\n    fireEvent.click(\n      containerLabels.querySelector('[data-test-subj=\"autoTagChip\"] button')!,\n    )\n    expect(\n      containerLabels.querySelector('[data-test-subj=\"autoTagChip\"]'),\n    ).not.toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(APPLY_BTN))\n    })\n\n    const expectedActions = [\n      setBrowserTreeDelimiter([DEFAULT_DELIMITER]),\n      resetBrowserTree(),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should handle pending input when Apply is clicked without pressing Enter', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    const pendingValue = 'newDelimiter'\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n\n    await waitForRiPopoverVisible()\n\n    const comboboxInput = document.querySelector(\n      '[data-testid=\"delimiter-combobox\"] [data-test-subj=\"autoTagInput\"]',\n    ) as HTMLInputElement\n\n    // Type in the input but don't press Enter\n    fireEvent.change(comboboxInput, { target: { value: pendingValue } })\n\n    // Verify the input has the value but no tag is created yet\n    expect(comboboxInput.value).toBe(pendingValue)\n    const containerLabels = document.querySelector(\n      '[data-test-subj=\"autoTagWrapper\"]',\n    )!\n    expect(\n      containerLabels.querySelector(`[title=\"${pendingValue}\"]`),\n    ).not.toBeInTheDocument()\n\n    // Click Apply - this should handle the pending input\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(APPLY_BTN))\n    })\n\n    const expectedActions = [\n      setBrowserTreeDelimiter([DEFAULT_DELIMITER, { label: pendingValue }]),\n      resetBrowserTree(),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        from: comboBoxToArray([DEFAULT_DELIMITER]),\n        to: comboBoxToArray([DEFAULT_DELIMITER, { label: pendingValue }]),\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should not handle pending input when it is empty or whitespace only', async () => {\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n\n    await waitForRiPopoverVisible()\n\n    const comboboxInput = document.querySelector(\n      '[data-testid=\"delimiter-combobox\"] [data-test-subj=\"autoTagInput\"]',\n    ) as HTMLInputElement\n\n    // Type whitespace only\n    fireEvent.change(comboboxInput, { target: { value: '   ' } })\n\n    // Click Apply - this should not handle the pending input\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(APPLY_BTN))\n    })\n\n    // Should not dispatch any actions since no changes were made\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should clear pending input after successful Apply', async () => {\n    const pendingValue = 'testDelimiter'\n    render(<KeyTreeSettings {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n\n    await waitForRiPopoverVisible()\n\n    const comboboxInput = document.querySelector(\n      '[data-testid=\"delimiter-combobox\"] [data-test-subj=\"autoTagInput\"]',\n    ) as HTMLInputElement\n\n    // Type in the input\n    fireEvent.change(comboboxInput, { target: { value: pendingValue } })\n    expect(comboboxInput.value).toBe(pendingValue)\n\n    // Click Apply\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(APPLY_BTN))\n    })\n\n    // Open the popover again to check if input is cleared\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(TREE_SETTINGS_TRIGGER_BTN))\n    })\n\n    await waitForRiPopoverVisible()\n\n    const comboboxInputAfter = document.querySelector(\n      '[data-testid=\"delimiter-combobox\"] [data-test-subj=\"autoTagInput\"]',\n    ) as HTMLInputElement\n\n    // Input should be cleared after Apply\n    expect(comboboxInputAfter.value).toBe('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { isEqual } from 'lodash'\nimport styled from 'styled-components'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  DEFAULT_DELIMITER,\n  DEFAULT_TREE_SORTING,\n  SortOrder,\n} from 'uiSrc/constants'\nimport {\n  appContextDbConfig,\n  resetBrowserTree,\n  setBrowserTreeDelimiter,\n  setBrowserTreeSort,\n} from 'uiSrc/slices/app/context'\nimport { comboBoxToArray } from 'uiSrc/utils'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  IconButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { SettingsIcon } from 'uiSrc/components/base/icons'\nimport {\n  AutoTag,\n  AutoTagOption,\n} from 'uiSrc/components/base/forms/combo-box/AutoTag'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\n\nconst StyledCol = styled(Col)`\n  width: 300px;\n`\n\nconst TreeViewSettingsButton = styled(IconButton)<{\n  isPopoverOpen: boolean\n}>`\n  background-color: ${({ theme, isPopoverOpen }) =>\n    isPopoverOpen ? theme.semantic.color.background.neutral100 : 'transparent'};\n`\n\nexport interface Props {\n  loading: boolean\n}\nconst sortOptions = [SortOrder.ASC, SortOrder.DESC].map((value) => ({\n  value,\n  inputDisplay: (\n    <span data-testid={`tree-view-sorting-item-${value}`}>\n      Key name {value}\n    </span>\n  ),\n}))\n\nconst KeyTreeSettings = ({ loading }: Props) => {\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const {\n    treeViewDelimiter = [DEFAULT_DELIMITER],\n    treeViewSort = DEFAULT_TREE_SORTING,\n  } = useSelector(appContextDbConfig)\n  const [sorting, setSorting] = useState<SortOrder>(treeViewSort)\n  const [delimiters, setDelimiters] =\n    useState<AutoTagOption[]>(treeViewDelimiter)\n  const [pendingInput, setPendingInput] = useState('')\n\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setSorting(treeViewSort)\n  }, [treeViewSort])\n\n  useEffect(() => {\n    setDelimiters(treeViewDelimiter)\n  }, [treeViewDelimiter])\n\n  const onButtonClick = () =>\n    setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen)\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n    setTimeout(() => {\n      resetStates()\n    }, 500)\n  }\n\n  const resetStates = useCallback(() => {\n    setSorting(treeViewSort)\n    setDelimiters(treeViewDelimiter)\n    setPendingInput('')\n  }, [treeViewSort, treeViewDelimiter])\n\n  const button = (\n    <TreeViewSettingsButton\n      isPopoverOpen={isPopoverOpen}\n      icon={SettingsIcon}\n      onClick={onButtonClick}\n      disabled={loading}\n      aria-label=\"open tree view settings\"\n      data-testid=\"tree-view-settings-btn\"\n    />\n  )\n\n  const handleApply = () => {\n    let finalDelimiters = delimiters\n    if (pendingInput.trim()) {\n      finalDelimiters = [...delimiters, { label: pendingInput.trim() }]\n      setPendingInput('')\n    }\n\n    if (!isEqual(finalDelimiters, treeViewDelimiter)) {\n      const delimitersValue = finalDelimiters.length\n        ? finalDelimiters\n        : [DEFAULT_DELIMITER]\n\n      dispatch(setBrowserTreeDelimiter(delimitersValue))\n      sendEventTelemetry({\n        event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED,\n        eventData: {\n          databaseId: instanceId,\n          from: comboBoxToArray(treeViewDelimiter),\n          to: comboBoxToArray(delimitersValue),\n        },\n      })\n\n      dispatch(resetBrowserTree())\n    }\n\n    if (sorting !== treeViewSort) {\n      dispatch(setBrowserTreeSort(sorting))\n\n      sendEventTelemetry({\n        event: TelemetryEvent.TREE_VIEW_KEYS_SORTED,\n        eventData: {\n          databaseId: instanceId,\n          sorting: sorting || DEFAULT_TREE_SORTING,\n        },\n      })\n\n      dispatch(resetBrowserTree())\n    }\n\n    setIsPopoverOpen(false)\n  }\n\n  const onChangeSort = (value: SortOrder) => {\n    setSorting(value)\n  }\n\n  return (\n    <RiPopover\n      ownFocus={false}\n      isOpen={isPopoverOpen}\n      closePopover={closePopover}\n      button={button}\n    >\n      <StyledCol gap=\"l\">\n        <FlexItem>\n          <AutoTag\n            layout=\"horizontal\"\n            label=\"Delimiter\"\n            placeholder=\":\"\n            delimiter=\" \"\n            selectedOptions={delimiters}\n            onCreateOption={(del) =>\n              setDelimiters([...delimiters, { label: del }])\n            }\n            onChange={(selectedOptions) => setDelimiters(selectedOptions)}\n            onInputChange={setPendingInput}\n            data-testid=\"delimiter-combobox\"\n          />\n        </FlexItem>\n        <FlexItem>\n          <FormField layout=\"horizontal\" label=\"Sort by\">\n            <RiSelect\n              options={sortOptions}\n              valueRender={({ option }) => option.inputDisplay ?? option.value}\n              value={sorting}\n              onChange={(value: SortOrder) => onChangeSort(value)}\n              data-testid=\"tree-view-sorting-select\"\n            />\n          </FormField>\n        </FlexItem>\n        <FlexItem />\n        <FlexItem>\n          <Row gap=\"m\" justify=\"end\">\n            <SecondaryButton\n              data-testid=\"tree-view-cancel-btn\"\n              onClick={closePopover}\n            >\n              Cancel\n            </SecondaryButton>\n            <PrimaryButton\n              data-testid=\"tree-view-apply-btn\"\n              onClick={handleApply}\n            >\n              Apply\n            </PrimaryButton>\n          </Row>\n        </FlexItem>\n      </StyledCol>\n    </RiPopover>\n  )\n}\n\nexport default React.memo(KeyTreeSettings)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/index.ts",
    "content": "import KeyTreeSettings from './KeyTreeSettings'\n\nexport default KeyTreeSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/index.ts",
    "content": "import KeyTree from './KeyTree'\nimport KeyTreeSettings from './KeyTreeSettings'\n\nexport default KeyTree\n\nexport { KeyTreeSettings }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/key-tree/styles.module.scss",
    "content": ".container {\n  height: 100%;\n  overflow: hidden;\n}\n\n.tooltip {\n  max-width: 372px !important;\n}\n\n.noKeys {\n  @include eui.scrollBar;\n\n  text-align: center;\n  margin: auto;\n}\n\n.content {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background-color: var(--euiColorEmptyShade);\n  border-top: 1px solid var(--euiColorLightShade);\n\n  :global(.ReactVirtualized__Table__headerRow) {\n    border: none !important;\n  }\n}\n\n.filter {\n  display: inline-block;\n  width: 100px;\n  height: 38px;\n  margin-left: 8px;\n  background-color: var(--tableLightestBorderColor);\n}\n\n.action {\n  opacity: 0;\n  transition: opacity 250ms ease-in-out;\n}\n\n:global(.table-row-selected) .action {\n  opacity: 1;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/KeysBrowserPanel.styles.ts",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Container = styled.div<{\n  children?: React.ReactNode\n  ref?: React.Ref<HTMLDivElement>\n}>`\n  height: 100%;\n  overflow: hidden;\n`\n\nexport const ErrorContainer = styled(Col)`\n  overflow: hidden;\n  color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic?.color?.text?.danger500 ?? 'inherit'};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/KeysBrowserPanel.tsx",
    "content": "import React from 'react'\n\nimport { KeysBrowser } from 'uiSrc/components/browser'\n\nimport {\n  Context as KeysBrowserPanelProvider,\n  useKeysBrowserPanel,\n} from './contexts/Context'\nimport Header from './components/Header'\nimport Content from './components/Content'\nimport Footer from './components/Footer'\nimport * as S from './KeysBrowserPanel.styles'\nimport { Props } from '../browser-left-panel/BrowserLeftPanel'\n\nconst KeysBrowserPanelInner = () => {\n  const { containerRef } = useKeysBrowserPanel()\n\n  return (\n    <S.Container ref={containerRef}>\n      <KeysBrowser.Compose data-testid=\"keys-browser-panel\">\n        <KeysBrowser.Header>\n          <Header />\n        </KeysBrowser.Header>\n\n        <KeysBrowser.Content>\n          <Content />\n        </KeysBrowser.Content>\n\n        <KeysBrowser.Footer>\n          <Footer />\n        </KeysBrowser.Footer>\n      </KeysBrowser.Compose>\n    </S.Container>\n  )\n}\n\nconst KeysBrowserPanel = (props: Props) => (\n  <KeysBrowserPanelProvider {...props}>\n    <KeysBrowserPanelInner />\n  </KeysBrowserPanelProvider>\n)\n\nexport default React.memo(KeysBrowserPanel)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/KeysBrowserPanel.types.ts",
    "content": "import React from 'react'\n\nimport {\n  KeyViewType,\n  KeysStoreData,\n  SearchMode,\n} from 'uiSrc/slices/interfaces/keys'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { BrowserColumns, KeyTypes } from 'uiSrc/constants'\n\nexport interface KeysBrowserPanelContextValue {\n  viewType: KeyViewType\n  searchMode: SearchMode\n  isTreeViewDisabled: boolean\n\n  loading: boolean\n  headerLoading: boolean\n\n  keysState: KeysStoreData\n  keysError: string\n  commonFilterType: Nullable<KeyTypes>\n  scrollTopPosition: number\n\n  isSearched: boolean\n  isFiltered: boolean\n\n  shownColumns: BrowserColumns[]\n  effectiveColumns: BrowserColumns[]\n\n  selectedIndex: Nullable<RedisResponseBuffer>\n  connectedInstanceId: string\n  deleting: boolean\n\n  keyListRef: React.RefObject<any>\n  containerRef: React.RefObject<HTMLDivElement>\n\n  selectKey: ({ rowData }: { rowData: any }) => void\n  handleAddKeyPanel: (value: boolean) => void\n  handleBulkActionsPanel: (value: boolean) => void\n\n  handleRefreshKeys: () => void\n  handleEnableAutoRefresh: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  handleChangeAutoRefreshRate: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  handleToggleColumn: (checked: boolean, columnType: BrowserColumns) => void\n  openAddKeyPanel: () => void\n  handleSwitchView: (type: KeyViewType) => void\n  loadMoreItems: (\n    oldKeys: IKeyPropTypes[],\n    range: { startIndex: number; stopIndex: number },\n  ) => void\n  onDeleteKey: (key: RedisResponseBuffer) => void\n  handleScanMore: (config: any) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/components/Content.tsx",
    "content": "import React from 'react'\n\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\nimport { Nullable } from 'uiSrc/utils'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport KeyList from '../../key-list'\nimport KeyTree from '../../key-tree'\nimport { useKeysBrowserPanel } from '../contexts/Context'\nimport * as S from '../KeysBrowserPanel.styles'\n\nconst Content = () => {\n  const {\n    viewType,\n    keysState,\n    keysError,\n    loading,\n    scrollTopPosition,\n    effectiveColumns,\n    commonFilterType,\n    deleting,\n    keyListRef,\n    selectKey,\n    loadMoreItems,\n    onDeleteKey,\n    handleAddKeyPanel,\n    handleBulkActionsPanel,\n  } = useKeysBrowserPanel()\n\n  return (\n    <>\n      {keysError && (\n        <S.ErrorContainer data-testid=\"keys-error\">\n          <div>{keysError}</div>\n        </S.ErrorContainer>\n      )}\n      {viewType === KeyViewType.Browser && !keysError && (\n        <KeyList\n          hideFooter\n          ref={keyListRef}\n          keysState={keysState}\n          loading={loading}\n          scrollTopPosition={scrollTopPosition}\n          visibleColumns={effectiveColumns}\n          commonFilterType={commonFilterType as Nullable<KeyTypes>}\n          loadMoreItems={loadMoreItems}\n          selectKey={selectKey}\n          onDelete={onDeleteKey}\n          onAddKeyPanel={handleAddKeyPanel}\n        />\n      )}\n      {viewType === KeyViewType.Tree && !keysError && (\n        <KeyTree\n          ref={keyListRef}\n          keysState={keysState}\n          loading={loading}\n          commonFilterType={commonFilterType as Nullable<KeyTypes>}\n          selectKey={selectKey}\n          loadMoreItems={loadMoreItems}\n          onDelete={onDeleteKey}\n          deleting={deleting}\n          onAddKeyPanel={handleAddKeyPanel}\n          onBulkActionsPanel={handleBulkActionsPanel}\n          visibleColumns={effectiveColumns}\n        />\n      )}\n    </>\n  )\n}\n\nexport default React.memo(Content)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/components/Footer.tsx",
    "content": "import React from 'react'\nimport { isNull } from 'lodash'\n\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport ScanMore from 'uiSrc/components/scan-more'\nimport { numberWithSpaces, nullableNumberWithSpaces } from 'uiSrc/utils/numbers'\nimport { Text, ColorText } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\n\nimport { useKeysBrowserPanel } from '../contexts/Context'\n\nconst Footer = () => {\n  const {\n    viewType,\n    searchMode,\n    keysState,\n    headerLoading,\n    isSearched,\n    isFiltered,\n    handleScanMore,\n  } = useKeysBrowserPanel()\n\n  const footerScanned =\n    isSearched ||\n    (isFiltered && searchMode === SearchMode.Pattern) ||\n    viewType === KeyViewType.Tree\n      ? keysState.scanned\n      : 0\n\n  const footerScannedDisplay =\n    keysState.keys.length > (footerScanned ?? 0)\n      ? keysState.keys.length\n      : (footerScanned ?? 0)\n\n  const footerNotAccurateScanned =\n    keysState.total &&\n    (footerScanned ?? 0) >= keysState.total &&\n    keysState.nextCursor &&\n    keysState.nextCursor !== '0'\n      ? '~'\n      : ''\n\n  const showScanMore = !(\n    searchMode === SearchMode.Redisearch &&\n    keysState.maxResults &&\n    keysState.keys.length >= keysState.maxResults\n  )\n\n  return (\n    <Row align=\"center\" justify=\"between\" grow data-testid=\"keys-summary\">\n      <FlexItem>\n        {headerLoading && !keysState.total && !isNull(keysState.total) && (\n          <Text size=\"s\" data-testid=\"scanning-text\">\n            Scanning...\n          </Text>\n        )}\n        {!!footerScanned && (\n          <ColorText size=\"s\" variant=\"semiBold\" component=\"span\">\n            {'Results: '}\n            <span data-testid=\"keys-number-of-results\">\n              {numberWithSpaces(keysState.keys.length)}\n            </span>\n          </ColorText>\n        )}\n        {!footerScanned && (!!keysState.total || isNull(keysState.total)) && (\n          <Text size=\"s\" variant=\"semiBold\" component=\"span\">\n            {'Total: '}\n            {nullableNumberWithSpaces(keysState.total)}\n          </Text>\n        )}\n      </FlexItem>\n      <Row gap=\"l\" align=\"center\" grow={false}>\n        {!!footerScanned && (\n          <FlexItem>\n            <ColorText size=\"s\" color=\"secondary\" component=\"span\">\n              {'Scanned '}\n              <span data-testid=\"keys-number-of-scanned\">\n                {footerNotAccurateScanned}\n                {numberWithSpaces(footerScannedDisplay)}\n              </span>\n              {' / '}\n              <span data-testid=\"keys-total\">\n                {nullableNumberWithSpaces(keysState.total)}\n              </span>\n            </ColorText>\n          </FlexItem>\n        )}\n        {showScanMore && (\n          <FlexItem>\n            <ScanMore\n              withAlert={false}\n              scanned={footerScanned}\n              totalItemsCount={keysState.total}\n              loading={headerLoading}\n              loadMoreItems={handleScanMore}\n              nextCursor={keysState.nextCursor}\n            />\n          </FlexItem>\n        )}\n      </Row>\n    </Row>\n  )\n}\n\nexport default React.memo(Footer)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/components/Header.tsx",
    "content": "import React from 'react'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { AutoRefresh } from 'uiSrc/components'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\nimport { ActionIconButton } from 'uiSrc/components/base/forms/buttons'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\n\nimport { ViewSwitch, ColumnsMenu } from 'uiSrc/components/browser'\n\nimport { KeyTreeSettings } from '../../key-tree'\nimport { useKeysBrowserPanel } from '../contexts/Context'\n\nconst HIDE_REFRESH_LABEL_WIDTH = 640\n\nconst Header = () => {\n  const {\n    viewType,\n    searchMode,\n    isTreeViewDisabled,\n    loading,\n    headerLoading,\n    keysState,\n    shownColumns,\n    selectedIndex,\n    handleRefreshKeys,\n    handleEnableAutoRefresh,\n    handleChangeAutoRefreshRate,\n    handleToggleColumn,\n    openAddKeyPanel,\n    handleSwitchView,\n  } = useKeysBrowserPanel()\n\n  return (\n    <AutoSizer disableHeight>\n      {({ width }) => (\n        <Row align=\"center\" justify=\"between\" style={{ width }}>\n          <Row gap=\"m\" align=\"center\" grow={false}>\n            <FlexItem>\n              <AutoRefresh\n                disabled={\n                  searchMode === SearchMode.Redisearch && !selectedIndex\n                }\n                disabledRefreshButtonMessage=\"Select an index to refresh keys.\"\n                iconSize=\"S\"\n                postfix=\"keys\"\n                loading={loading}\n                lastRefreshTime={keysState.lastRefreshTime}\n                displayText={(width || 0) > HIDE_REFRESH_LABEL_WIDTH}\n                onRefresh={handleRefreshKeys}\n                onEnableAutoRefresh={handleEnableAutoRefresh}\n                onChangeAutoRefreshRate={handleChangeAutoRefreshRate}\n                testid=\"keys\"\n              />\n            </FlexItem>\n            <FlexItem>\n              <ColumnsMenu\n                shownColumns={shownColumns}\n                onToggleColumn={handleToggleColumn}\n              />\n            </FlexItem>\n            {viewType === KeyViewType.Tree && (\n              <FlexItem>\n                <KeyTreeSettings loading={headerLoading} />\n              </FlexItem>\n            )}\n          </Row>\n          <Row gap=\"l\" align=\"center\" grow={false}>\n            <FlexItem>\n              <ActionIconButton\n                icon={PlusIcon}\n                variant=\"secondary\"\n                aria-label=\"Add key\"\n                onClick={openAddKeyPanel}\n                data-testid=\"btn-add-key\"\n              />\n            </FlexItem>\n            <FlexItem>\n              <ViewSwitch\n                viewType={viewType}\n                isTreeViewDisabled={isTreeViewDisabled}\n                onChange={handleSwitchView}\n              />\n            </FlexItem>\n          </Row>\n        </Row>\n      )}\n    </AutoSizer>\n  )\n}\n\nexport default React.memo(Header)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/contexts/Context.tsx",
    "content": "import React, {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n} from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  appContextBrowser,\n  appContextDbConfig,\n  appContextSelector,\n  resetBrowserTree,\n  setBrowserKeyListDataLoaded,\n  setBrowserSelectedKey,\n  setBrowserShownColumns,\n} from 'uiSrc/slices/app/context'\nimport {\n  changeKeyViewType,\n  fetchKeys,\n  fetchMoreKeys,\n  keysDataSelector,\n  keysSelector,\n  resetKeyInfo,\n  resetKeysData,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  redisearchDataSelector,\n  redisearchListSelector,\n  redisearchSelector,\n} from 'uiSrc/slices/browser/redisearch'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport {\n  setConnectedInstanceId,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { isEqualBuffers, Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { BrowserColumns, KeyTypes, KeyValueFormat } from 'uiSrc/constants'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { incrementOnboardStepAction } from 'uiSrc/slices/app/features'\n\nimport { useResponsiveColumns } from 'uiSrc/components/browser'\n\nimport { Props } from '../../browser-left-panel/BrowserLeftPanel'\nimport { KeysBrowserPanelContextValue } from '../KeysBrowserPanel.types'\n\nconst KeysBrowserPanelContext =\n  createContext<KeysBrowserPanelContextValue | null>(null)\n\nexport const useKeysBrowserPanel = (): KeysBrowserPanelContextValue => {\n  const ctx = useContext(KeysBrowserPanelContext)\n  if (!ctx) {\n    throw new Error('useKeysBrowserPanel must be used within Context provider')\n  }\n  return ctx\n}\n\nexport const Context = ({\n  selectedKey,\n  selectKey,\n  removeSelectedKey,\n  handleAddKeyPanel,\n  handleBulkActionsPanel,\n  children,\n}: Props & { children: React.ReactNode }) => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const patternKeysState = useSelector(keysDataSelector)\n  const redisearchKeysState = useSelector(redisearchDataSelector)\n  const { loading: redisearchLoading, isSearched: redisearchIsSearched } =\n    useSelector(redisearchSelector)\n  const { loading: redisearchListLoading } = useSelector(redisearchListSelector)\n  const {\n    loading: patternLoading,\n    viewType,\n    searchMode,\n    isSearched: patternIsSearched,\n    isFiltered,\n    filter,\n    deleting,\n    error: keysError,\n  } = useSelector(keysSelector)\n  const { id: connectedInstanceId, keyNameFormat } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { contextInstanceId } = useSelector(appContextSelector)\n  const { shownColumns } = useSelector(appContextDbConfig)\n  const { selectedIndex } = useSelector(redisearchSelector)\n  const {\n    keyList: {\n      isDataPatternLoaded,\n      isDataRedisearchLoaded,\n      scrollPatternTopPosition,\n      scrollRedisearchTopPosition,\n    },\n  } = useSelector(appContextBrowser)\n\n  const keyListRef = useRef<any>()\n  const dispatch = useDispatch()\n\n  const { effectiveColumns, containerRef } = useResponsiveColumns(shownColumns)\n\n  const isDataLoaded =\n    searchMode === SearchMode.Pattern\n      ? isDataPatternLoaded\n      : isDataRedisearchLoaded\n  const keysState =\n    searchMode === SearchMode.Pattern ? patternKeysState : redisearchKeysState\n  const loading =\n    searchMode === SearchMode.Pattern ? patternLoading : redisearchLoading\n  const headerLoading =\n    searchMode === SearchMode.Pattern ? patternLoading : redisearchListLoading\n  const isSearched =\n    searchMode === SearchMode.Pattern ? patternIsSearched : redisearchIsSearched\n  const scrollTopPosition =\n    searchMode === SearchMode.Pattern\n      ? scrollPatternTopPosition\n      : scrollRedisearchTopPosition\n  const commonFilterType =\n    searchMode === SearchMode.Pattern ? filter : keysState.keys?.[0]?.type\n\n  const format = keyNameFormat as unknown as KeyValueFormat\n  const isTreeViewDisabled =\n    (format || KeyValueFormat.Unicode) === KeyValueFormat.HEX\n\n  const loadKeys = useCallback(\n    (keyViewType: KeyViewType = KeyViewType.Browser) => {\n      dispatch(setConnectedInstanceId(instanceId))\n\n      dispatch(\n        fetchKeys(\n          {\n            searchMode,\n            cursor: '0',\n            count:\n              keyViewType === KeyViewType.Browser\n                ? SCAN_COUNT_DEFAULT\n                : SCAN_TREE_COUNT_DEFAULT,\n          },\n          () => dispatch(setBrowserKeyListDataLoaded(searchMode, true)),\n          () => dispatch(setBrowserKeyListDataLoaded(searchMode, false)),\n        ),\n      )\n    },\n    [searchMode],\n  )\n\n  useEffect(() => {\n    if (\n      (!isDataLoaded || contextInstanceId !== instanceId) &&\n      searchMode === SearchMode.Pattern\n    ) {\n      loadKeys(viewType)\n    }\n  }, [searchMode, isDataLoaded])\n\n  const loadMoreItems = useCallback(\n    (\n      oldKeys: IKeyPropTypes[],\n      { startIndex, stopIndex }: { startIndex: number; stopIndex: number },\n    ) => {\n      if (keysState.nextCursor !== '0') {\n        dispatch(\n          fetchMoreKeys(\n            searchMode,\n            oldKeys,\n            keysState.nextCursor,\n            stopIndex - startIndex + 1,\n          ),\n        )\n      }\n    },\n    [searchMode, keysState.nextCursor],\n  )\n\n  const onDeleteKey = useCallback(\n    (key: RedisResponseBuffer) => {\n      if (isEqualBuffers(key, selectedKey)) {\n        removeSelectedKey()\n      }\n    },\n    [selectedKey],\n  )\n\n  const handleRefreshKeys = useCallback(() => {\n    dispatch(\n      fetchKeys(\n        {\n          searchMode,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n        },\n        (data) => {\n          const keys = Array.isArray(data) ? data[0].keys : data.keys\n\n          if (!keys.length) {\n            dispatch(resetKeyInfo())\n            dispatch(setBrowserSelectedKey(null))\n          }\n\n          dispatch(setBrowserKeyListDataLoaded(searchMode, true))\n          dispatch(setConnectivityError(null))\n        },\n        () => dispatch(setBrowserKeyListDataLoaded(searchMode, false)),\n      ),\n    )\n  }, [searchMode, viewType])\n\n  const handleEnableAutoRefresh = useCallback(\n    (enableAutoRefresh: boolean, refreshRate: string) => {\n      const browserViewEvent = enableAutoRefresh\n        ? TelemetryEvent.BROWSER_KEY_LIST_AUTO_REFRESH_ENABLED\n        : TelemetryEvent.BROWSER_KEY_LIST_AUTO_REFRESH_DISABLED\n      const treeViewEvent = enableAutoRefresh\n        ? TelemetryEvent.TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED\n        : TelemetryEvent.TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED\n      sendEventTelemetry({\n        event: getBasedOnViewTypeEvent(\n          viewType,\n          browserViewEvent,\n          treeViewEvent,\n        ),\n        eventData: {\n          databaseId: connectedInstanceId,\n          refreshRate: +refreshRate,\n        },\n      })\n    },\n    [viewType, connectedInstanceId],\n  )\n\n  const handleChangeAutoRefreshRate = useCallback(\n    (enableAutoRefresh: boolean, refreshRate: string) => {\n      if (enableAutoRefresh) {\n        handleEnableAutoRefresh(enableAutoRefresh, refreshRate)\n      }\n    },\n    [handleEnableAutoRefresh],\n  )\n\n  const openAddKeyPanel = useCallback(() => {\n    handleAddKeyPanel(true)\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_ADD_BUTTON_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_ADD_BUTTON_CLICKED,\n      ),\n      eventData: {\n        databaseId: connectedInstanceId,\n      },\n    })\n  }, [viewType, connectedInstanceId, handleAddKeyPanel])\n\n  const handleSwitchView = useCallback(\n    (type: KeyViewType) => {\n      sendEventTelemetry({\n        event:\n          type === KeyViewType.Tree\n            ? TelemetryEvent.TREE_VIEW_OPENED\n            : TelemetryEvent.LIST_VIEW_OPENED,\n        eventData: {\n          databaseId: connectedInstanceId,\n        },\n      })\n\n      dispatch(resetBrowserTree())\n      dispatch(resetKeysData(searchMode))\n\n      if (!(searchMode === SearchMode.Redisearch && !selectedIndex)) {\n        loadKeys(type)\n      }\n\n      setTimeout(() => {\n        dispatch(changeKeyViewType(type))\n      }, 0)\n\n      if (type === KeyViewType.Tree) {\n        dispatch(\n          incrementOnboardStepAction(\n            OnboardingSteps.BrowserTreeView,\n            undefined,\n            () =>\n              sendEventTelemetry({\n                event: TelemetryEvent.ONBOARDING_TOUR_ACTION_MADE,\n                eventData: {\n                  databaseId: connectedInstanceId,\n                  step: OnboardingStepName.BrowserTreeView,\n                },\n              }),\n          ),\n        )\n      }\n    },\n    [searchMode, connectedInstanceId, selectedIndex, loadKeys],\n  )\n\n  const handleToggleColumn = useCallback(\n    (checked: boolean, columnType: BrowserColumns) => {\n      const shown: BrowserColumns[] = []\n      const hidden: BrowserColumns[] = []\n      const newColumns = checked\n        ? [...shownColumns, columnType]\n        : shownColumns.filter((col) => col !== columnType)\n\n      if (checked) {\n        shown.push(columnType)\n      } else {\n        hidden.push(columnType)\n      }\n\n      dispatch(setBrowserShownColumns(newColumns))\n      sendEventTelemetry({\n        event: TelemetryEvent.SHOW_BROWSER_COLUMN_CLICKED,\n        eventData: {\n          databaseId: connectedInstanceId,\n          shown,\n          hidden,\n        },\n      })\n    },\n    [shownColumns, connectedInstanceId],\n  )\n\n  const handleScanMore = useCallback(\n    (config: any) => {\n      keyListRef.current?.handleLoadMoreItems?.({\n        ...config,\n        stopIndex:\n          (viewType === KeyViewType.Browser\n            ? SCAN_COUNT_DEFAULT\n            : SCAN_TREE_COUNT_DEFAULT) - 1,\n      })\n    },\n    [viewType],\n  )\n\n  const value: KeysBrowserPanelContextValue = {\n    viewType,\n    searchMode,\n    isTreeViewDisabled,\n\n    loading,\n    headerLoading,\n\n    keysState,\n    keysError,\n    commonFilterType: commonFilterType as Nullable<KeyTypes>,\n    scrollTopPosition,\n\n    isSearched,\n    isFiltered,\n\n    shownColumns,\n    effectiveColumns,\n\n    selectedIndex,\n    connectedInstanceId,\n    deleting,\n\n    keyListRef,\n    containerRef,\n\n    selectKey,\n    handleAddKeyPanel,\n    handleBulkActionsPanel,\n\n    handleRefreshKeys,\n    handleEnableAutoRefresh,\n    handleChangeAutoRefreshRate,\n    handleToggleColumn,\n    openAddKeyPanel,\n    handleSwitchView,\n    loadMoreItems,\n    onDeleteKey,\n    handleScanMore,\n  }\n\n  return (\n    <KeysBrowserPanelContext.Provider value={value}>\n      {children}\n    </KeysBrowserPanelContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-browser-panel/index.ts",
    "content": "import KeysBrowserPanel from './KeysBrowserPanel'\n\nexport default KeysBrowserPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  setBrowserPatternKeyListDataLoaded,\n  setBrowserSelectedKey,\n} from 'uiSrc/slices/app/context'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport * as keysSlice from 'uiSrc/slices/browser/keys'\nimport {\n  KeysStoreData,\n  KeyViewType,\n  SearchMode,\n} from 'uiSrc/slices/interfaces/keys'\nimport { BrowserColumns } from 'uiSrc/constants'\n\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport KeysHeader from './KeysHeader'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(keysSlice.keysSelector as jest.Mock).mockReturnValue({\n    ...mockSelectorData,\n  })\n})\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  keysSelector: jest.fn().mockReturnValue(mockSelectorData),\n  fetchKeys: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest\n    .fn()\n    .mockReturnValue({ keyNameFormat: 'Unicode' }),\n}))\nconst connectedInstanceSelectorMock = connectedInstanceSelector as jest.Mock\n\nconst propsMock = {\n  keysState: {\n    keys: [\n      {\n        name: 'key1',\n        type: 'hash',\n        ttl: -1,\n        size: 100,\n        length: 100,\n      },\n      {\n        name: 'key2',\n        type: 'hash',\n        ttl: -1,\n        size: 150,\n        length: 100,\n      },\n      {\n        name: 'key3',\n        type: 'hash',\n        ttl: -1,\n        size: 110,\n        length: 100,\n      },\n    ],\n    nextCursor: '0',\n    total: 3,\n    scanned: 5,\n    shardsMeta: {},\n    previousResultCount: 1,\n    lastRefreshTime: 3,\n  } as KeysStoreData,\n  loading: false,\n  sizes: {},\n  loadKeys: jest.fn(),\n  loadMoreItems: jest.fn(),\n  handleAddKeyPanel: jest.fn(),\n  nextCursor: '0',\n  isSearched: false,\n  handleScanMoreClick: jest.fn(),\n}\n\nconst mockSelectorData = {\n  searchMode: SearchMode.Pattern,\n  viewType: KeyViewType.Browser,\n  isSearch: false,\n  isFiltered: false,\n  shownColumns: [BrowserColumns.TTL, BrowserColumns.Size],\n}\n\ndescribe('KeysHeader', () => {\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(render(<KeysHeader {...propsMock} />)).toBeTruthy()\n  })\n\n  it('should render key view type switcher properly', () => {\n    render(<KeysHeader {...propsMock} />)\n\n    const keyViewTypeSwitcherInput = screen.queryByTestId('view-type-switcher')\n    expect(keyViewTypeSwitcherInput).toBeInTheDocument()\n  })\n\n  it('should render Tree view enabled when format is Unicode', () => {\n    render(<KeysHeader {...propsMock} />)\n\n    const keyViewTypeSwitcherInput = screen.queryByTestId('view-type-list-btn')\n    expect(keyViewTypeSwitcherInput).toBeInTheDocument()\n    expect(keyViewTypeSwitcherInput).not.toBeDisabled()\n  })\n\n  it('should disable Tree view when key format is HEX', () => {\n    connectedInstanceSelectorMock.mockReturnValue({\n      keyNameFormat: 'HEX',\n    })\n    render(<KeysHeader {...propsMock} />)\n\n    const keyViewTypeSwitcherInput = screen.queryByTestId('view-type-list-btn')\n    expect(keyViewTypeSwitcherInput).toBeInTheDocument()\n    expect(keyViewTypeSwitcherInput).toBeDisabled()\n  })\n\n  it('should render Scan more button if total => keys.length', () => {\n    ;(keysSlice.keysSelector as jest.Mock).mockReturnValue({\n      ...mockSelectorData,\n      searchMode: SearchMode.Redisearch,\n      viewType: KeyViewType.Tree,\n    })\n\n    const { queryByTestId } = render(\n      <KeysHeader\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          maxResults: 200,\n          total: 200,\n        }}\n        nextCursor=\"3\"\n      />,\n    )\n\n    expect(queryByTestId('scan-more')).toBeInTheDocument()\n  })\n\n  it('should not render Scan more button for if searchMode = \"Redisearch\" and keys.length > maxResults', () => {\n    ;(keysSlice.keysSelector as jest.Mock).mockReturnValue({\n      ...mockSelectorData,\n      searchMode: SearchMode.Redisearch,\n      viewType: KeyViewType.Tree,\n    })\n\n    const { queryByTestId } = render(\n      <KeysHeader\n        {...propsMock}\n        keysState={{\n          ...propsMock.keysState,\n          maxResults: propsMock.keysState.keys.length - 1,\n          total: 200,\n          nextCursor: '3',\n        }}\n      />,\n    )\n\n    expect(queryByTestId('scan-more')).not.toBeInTheDocument()\n  })\n\n  it('should reset selected key data when no keys data is returned', async () => {\n    ;(keysSlice.fetchKeys as jest.Mock).mockImplementation(\n      (_options: any, onSuccess: (data: any) => void) => () => {\n        onSuccess({ keys: [] }) // Simulate empty data response\n      },\n    )\n\n    render(<KeysHeader {...propsMock} />)\n\n    fireEvent.click(screen.getByTestId('keys-refresh-btn'))\n\n    const expectedActions = [\n      keysSlice.resetKeyInfo(),\n      setBrowserSelectedKey(null),\n      setBrowserPatternKeyListDataLoaded(true),\n      setConnectivityError(null),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx",
    "content": "/* eslint-disable react/destructuring-assignment */\n/* eslint-disable react/no-this-in-sfc */\nimport React, { Ref, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport AutoSizer from 'react-virtualized-auto-sizer'\nimport { IconType, EqualIcon, FoldersIcon } from 'uiSrc/components/base/icons'\nimport KeysSummary from 'uiSrc/components/keys-summary'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport {\n  appContextDbConfig,\n  resetBrowserTree,\n  setBrowserKeyListDataLoaded,\n  setBrowserSelectedKey,\n  setBrowserShownColumns,\n} from 'uiSrc/slices/app/context'\n\nimport {\n  changeKeyViewType,\n  fetchKeys,\n  keysSelector,\n  resetKeyInfo,\n  resetKeysData,\n} from 'uiSrc/slices/browser/keys'\nimport { redisearchSelector } from 'uiSrc/slices/browser/redisearch'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  KeysStoreData,\n  KeyViewType,\n  SearchMode,\n} from 'uiSrc/slices/interfaces/keys'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\n\nimport { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { incrementOnboardStepAction } from 'uiSrc/slices/app/features'\nimport { AutoRefresh, OnboardingTour } from 'uiSrc/components'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { BrowserColumns, KeyValueFormat } from 'uiSrc/constants'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\nimport { ButtonGroup } from 'uiSrc/components/base/forms/button-group/ButtonGroup'\nimport styled from 'styled-components'\nimport { ToggleButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\n\nconst HIDE_REFRESH_LABEL_WIDTH = 640\n\ninterface ISwitchType<T> {\n  tooltipText: string\n  type: T\n  disabled?: boolean\n  ariaLabel: string\n  dataTestId: string\n  onClick: () => void\n  isActiveView: () => boolean\n  getIconType: () => IconType\n}\n\nexport interface Props {\n  loading: boolean\n  keysState: KeysStoreData\n  nextCursor: string\n  isSearched: boolean\n  loadKeys: (type?: KeyViewType) => void\n  handleScanMoreClick: (config: any) => void\n}\n\nconst ViewSwitchButton = styled(ButtonGroup.Button)`\n  width: 24px !important;\n  min-width: 24px !important;\n`\n\nconst KeysHeader = (props: Props) => {\n  const {\n    loading,\n    keysState,\n    isSearched,\n    loadKeys,\n    handleScanMoreClick,\n    nextCursor,\n  } = props\n\n  const { id: instanceId, keyNameFormat } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewType, searchMode, isFiltered } = useSelector(keysSelector)\n  const { shownColumns } = useSelector(appContextDbConfig)\n  const { selectedIndex } = useSelector(redisearchSelector)\n\n  const [columnsConfigShown, setColumnsConfigShown] = useState(false)\n\n  const rootDivRef: Ref<HTMLDivElement> = useRef(null)\n\n  const dispatch = useDispatch()\n\n  // TODO: Check if encoding can be reused from BE and FE\n  const format = keyNameFormat as unknown as KeyValueFormat\n  const isTreeViewDisabled =\n    (format || KeyValueFormat.Unicode) === KeyValueFormat.HEX\n  const viewTypes: ISwitchType<KeyViewType>[] = [\n    {\n      type: KeyViewType.Browser,\n      tooltipText: 'List View',\n      ariaLabel: 'List view button',\n      dataTestId: 'view-type-browser-btn',\n      isActiveView() {\n        return viewType === this.type\n      },\n      getIconType() {\n        return EqualIcon\n      },\n      onClick() {\n        handleSwitchView(this.type)\n      },\n    },\n    {\n      type: KeyViewType.Tree,\n      tooltipText: isTreeViewDisabled\n        ? 'Tree View is unavailable when the HEX key name format is selected.'\n        : 'Tree View',\n      ariaLabel: 'Tree view button',\n      dataTestId: 'view-type-list-btn',\n      disabled: isTreeViewDisabled,\n      isActiveView() {\n        return viewType === this.type\n      },\n      getIconType() {\n        return FoldersIcon\n      },\n      onClick() {\n        handleSwitchView(this.type)\n        dispatch(\n          incrementOnboardStepAction(\n            OnboardingSteps.BrowserTreeView,\n            undefined,\n            () =>\n              sendEventTelemetry({\n                event: TelemetryEvent.ONBOARDING_TOUR_ACTION_MADE,\n                eventData: {\n                  databaseId: instanceId,\n                  step: OnboardingStepName.BrowserTreeView,\n                },\n              }),\n          ),\n        )\n      },\n    },\n  ]\n\n  const scanMoreStyle = {\n    marginLeft: 10,\n    height: '36px !important',\n  }\n\n  const toggleColumnsConfigVisibility = () =>\n    setColumnsConfigShown(!columnsConfigShown)\n\n  const handleRefreshKeys = () => {\n    dispatch(\n      fetchKeys(\n        {\n          searchMode,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n        },\n        (data) => {\n          const keys = Array.isArray(data) ? data[0].keys : data.keys\n\n          if (!keys.length) {\n            dispatch(resetKeyInfo())\n            dispatch(setBrowserSelectedKey(null))\n          }\n\n          dispatch(setBrowserKeyListDataLoaded(searchMode, true))\n          dispatch(setConnectivityError(null))\n        },\n        () => dispatch(setBrowserKeyListDataLoaded(searchMode, false)),\n      ),\n    )\n  }\n\n  const handleEnableAutoRefresh = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    const browserViewEvent = enableAutoRefresh\n      ? TelemetryEvent.BROWSER_KEY_LIST_AUTO_REFRESH_ENABLED\n      : TelemetryEvent.BROWSER_KEY_LIST_AUTO_REFRESH_DISABLED\n    const treeViewEvent = enableAutoRefresh\n      ? TelemetryEvent.TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED\n      : TelemetryEvent.TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        databaseId: instanceId,\n        refreshRate: +refreshRate,\n      },\n    })\n  }\n\n  const handleChangeAutoRefreshRate = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    if (enableAutoRefresh) {\n      handleEnableAutoRefresh(enableAutoRefresh, refreshRate)\n    }\n  }\n\n  const handleScanMore = (config: any) => {\n    handleScanMoreClick?.({\n      ...config,\n      stopIndex:\n        (viewType === KeyViewType.Browser\n          ? SCAN_COUNT_DEFAULT\n          : SCAN_TREE_COUNT_DEFAULT) - 1,\n    })\n  }\n\n  const handleSwitchView = (type: KeyViewType) => {\n    sendEventTelemetry({\n      event:\n        type === KeyViewType.Tree\n          ? TelemetryEvent.TREE_VIEW_OPENED\n          : TelemetryEvent.LIST_VIEW_OPENED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n\n    dispatch(resetBrowserTree())\n    dispatch(resetKeysData(searchMode))\n\n    if (!(searchMode === SearchMode.Redisearch && !selectedIndex)) {\n      loadKeys(type)\n    }\n\n    setTimeout(() => {\n      dispatch(changeKeyViewType(type))\n    }, 0)\n  }\n\n  const changeColumnsShown = (status: boolean, columnType: BrowserColumns) => {\n    const shown = []\n    const hidden = []\n    const newColumns = status\n      ? [...shownColumns, columnType]\n      : shownColumns.filter((col) => col !== columnType)\n\n    if (columnType === BrowserColumns.TTL) {\n      status ? shown.push(BrowserColumns.TTL) : hidden.push(BrowserColumns.TTL)\n    } else if (columnType === BrowserColumns.Size) {\n      status\n        ? shown.push(BrowserColumns.Size)\n        : hidden.push(BrowserColumns.Size)\n    }\n\n    dispatch(setBrowserShownColumns(newColumns))\n    sendEventTelemetry({\n      event: TelemetryEvent.SHOW_BROWSER_COLUMN_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        shown,\n        hidden,\n      },\n    })\n  }\n\n  const ViewSwitch = () => (\n    <OnboardingTour options={ONBOARDING_FEATURES.BROWSER_TREE_VIEW}>\n      <ButtonGroup data-testid=\"view-type-switcher\">\n        {viewTypes.map((view) => (\n          <RiTooltip\n            content={view.tooltipText}\n            position=\"top\"\n            key={view.tooltipText}\n          >\n            <ViewSwitchButton\n              aria-label={view.ariaLabel}\n              onClick={() => view.onClick()}\n              isSelected={view.isActiveView()}\n              data-testid={view.dataTestId}\n              disabled={view.disabled || false}\n            >\n              <ButtonGroup.Icon icon={view.getIconType()} />\n            </ViewSwitchButton>\n          </RiTooltip>\n        ))}\n      </ButtonGroup>\n    </OnboardingTour>\n  )\n\n  return (\n    <div className={styles.content} ref={rootDivRef}>\n      <AutoSizer disableHeight>\n        {({ width }) => (\n          <Row justify=\"between\" style={{ width }}>\n            <FlexItem>\n              <KeysSummary\n                items={keysState.keys}\n                totalItemsCount={keysState.total}\n                scanned={\n                  isSearched ||\n                  (isFiltered && searchMode === SearchMode.Pattern) ||\n                  viewType === KeyViewType.Tree\n                    ? keysState.scanned\n                    : 0\n                }\n                loading={loading}\n                showScanMore={\n                  !(\n                    searchMode === SearchMode.Redisearch &&\n                    keysState.maxResults &&\n                    keysState.keys.length >= keysState.maxResults\n                  )\n                }\n                scanMoreStyle={scanMoreStyle}\n                loadMoreItems={handleScanMore}\n                nextCursor={nextCursor}\n              />\n            </FlexItem>\n            <FlexItem>\n              <Row gap=\"l\">\n                <FlexItem>\n                  <AutoRefresh\n                    disabled={\n                      searchMode === SearchMode.Redisearch && !selectedIndex\n                    }\n                    disabledRefreshButtonMessage=\"Select an index to refresh keys.\"\n                    iconSize=\"S\"\n                    postfix=\"keys\"\n                    loading={loading}\n                    lastRefreshTime={keysState.lastRefreshTime}\n                    displayText={(width || 0) > HIDE_REFRESH_LABEL_WIDTH}\n                    onRefresh={handleRefreshKeys}\n                    onEnableAutoRefresh={handleEnableAutoRefresh}\n                    onChangeAutoRefreshRate={handleChangeAutoRefreshRate}\n                    testid=\"keys\"\n                  />\n                </FlexItem>\n                <FlexItem>\n                  <RiPopover\n                    ownFocus={false}\n                    anchorPosition=\"downLeft\"\n                    isOpen={columnsConfigShown}\n                    anchorClassName={styles.anchorWrapper}\n                    panelClassName={styles.popoverWrapper}\n                    closePopover={() => setColumnsConfigShown(false)}\n                    button={\n                      <ToggleButton\n                        onPressedChange={toggleColumnsConfigVisibility}\n                        className={styles.columnsButton}\n                        data-testid=\"btn-columns-actions\"\n                        aria-label=\"columns\"\n                        pressed={columnsConfigShown}\n                      >\n                        <RiIcon size=\"m\" type=\"ColumnsIcon\" />\n                        <Text size=\"s\">Columns</Text>\n                      </ToggleButton>\n                    }\n                  >\n                    <Col gap=\"m\">\n                      <FlexItem>\n                        <Row align=\"center\" gap=\"m\">\n                          <FlexItem grow>\n                            <Checkbox\n                              id=\"show-key-size\"\n                              name=\"show-key-size\"\n                              label=\"Key size\"\n                              checked={shownColumns.includes(\n                                BrowserColumns.Size,\n                              )}\n                              onChange={(e) =>\n                                changeColumnsShown(\n                                  e.target.checked,\n                                  BrowserColumns.Size,\n                                )\n                              }\n                              data-testid=\"show-key-size\"\n                              className={styles.checkbox}\n                            />\n                          </FlexItem>\n                          <FlexItem>\n                            <RiTooltip\n                              content=\"Hide the key size to avoid performance issues when working with large keys.\"\n                              position=\"top\"\n                              anchorClassName=\"flex-row\"\n                            >\n                              <RiIcon\n                                className={styles.infoIcon}\n                                type=\"InfoIcon\"\n                                size=\"m\"\n                                style={{ cursor: 'pointer' }}\n                                data-testid=\"key-size-info-icon\"\n                              />\n                            </RiTooltip>\n                          </FlexItem>\n                        </Row>\n                      </FlexItem>\n                      <FlexItem>\n                        <Checkbox\n                          id=\"show-ttl\"\n                          name=\"show-ttl\"\n                          label=\"TTL\"\n                          checked={shownColumns.includes(BrowserColumns.TTL)}\n                          onChange={(e) =>\n                            changeColumnsShown(\n                              e.target.checked,\n                              BrowserColumns.TTL,\n                            )\n                          }\n                          data-testid=\"show-ttl\"\n                        />\n                      </FlexItem>\n                    </Col>\n                  </RiPopover>\n                </FlexItem>\n                <FlexItem>{ViewSwitch()}</FlexItem>\n              </Row>\n            </FlexItem>\n          </Row>\n        )}\n      </AutoSizer>\n    </div>\n  )\n}\n\nexport default React.memo(KeysHeader)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-header/index.ts",
    "content": "import KeysHeader from './KeysHeader'\n\nexport default KeysHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/keys-header/styles.module.scss",
    "content": ".content {\n  width: 100%;\n  background-color: var(--euiColorEmptyShade);\n\n  padding: 4px 12px;\n\n  display: flex;\n  flex-shrink: 0;\n  flex-direction: column;\n  position: relative;\n}\n\n.bottom {\n  display: flex;\n  text-align: center;\n  align-items: center;\n  justify-content: space-between;\n  min-height: 38px;\n\n  flex-wrap: wrap;\n}\n\n.viewTypeBtn {\n  width: 24px !important;\n  height: 24px !important;\n  background-color: var(--browserViewTypePassive) !important;\n\n  svg {\n    color: var(--euiTextSubduedColor) !important;\n  }\n\n  svg>g>path {\n    fill: var(--euiTextSubduedColor) !important;\n  }\n\n  &:hover {\n    background-color: var(--euiColorLightestShade) !important;\n    ;\n  }\n\n  &.active {\n    background-color: var(--browserComponentActive) !important;\n\n    svg {\n      color: var(--euiColorPrimary) !important;\n    }\n\n    svg>g>path {\n      fill: var(--euiColorPrimary) !important;\n    }\n  }\n}\n\n.keysSummary {\n  margin: 4px 8px 4px 4px;\n}\n\n.columnsButton {\n  padding: 4px 6px 4px 4px;\n  border-color: transparent !important;\n  box-shadow: none !important;\n}\n\n.checkbox {\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/load-sample-data/LoadSampleData.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  waitForRiPopoverVisible,\n  mockedStore,\n  cleanup,\n  waitForStack,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  bulkImportDefaultData,\n  bulkImportDefaultDataSuccess,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { apiService } from 'uiSrc/services'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport LoadSampleData from './LoadSampleData'\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '1',\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('LoadSampleData', () => {\n  it('should render', () => {\n    expect(render(<LoadSampleData />)).toBeTruthy()\n  })\n\n  it('should call proper actions', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    apiService.post = jest\n      .fn()\n      .mockResolvedValueOnce({ status: 200, data: { data: {} } })\n\n    render(<LoadSampleData />)\n\n    fireEvent.click(screen.getByTestId('load-sample-data-btn'))\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('load-sample-data-btn-confirm'))\n\n    await waitForStack()\n\n    const expectedActions = [\n      bulkImportDefaultData(),\n      bulkImportDefaultDataSuccess(),\n      addMessageNotification(successMessages.UPLOAD_DATA_BULK()),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.IMPORT_SAMPLES_CLICKED,\n      eventData: { databaseId: '1' },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/load-sample-data/LoadSampleData.tsx",
    "content": "import React, { useState } from 'react'\nimport cx from 'classnames'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  bulkActionsSelector,\n  bulkImportDefaultDataAction,\n} from 'uiSrc/slices/browser/bulkActions'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { PlayFilledIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  anchorClassName?: string\n  onSuccess?: () => void\n}\n\nconst LoadSampleData = (props: Props) => {\n  const { anchorClassName, onSuccess } = props\n  const [isConfirmationOpen, setIsConfirmationOpen] = useState(false)\n\n  const { id } = useSelector(connectedInstanceSelector)\n  const { loading } = useSelector(bulkActionsSelector)\n\n  const dispatch = useDispatch()\n\n  const handleSampleData = () => {\n    setIsConfirmationOpen(false)\n    dispatch(bulkImportDefaultDataAction(id, onSuccess))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.IMPORT_SAMPLES_CLICKED,\n      eventData: {\n        databaseId: id,\n      },\n    })\n  }\n\n  return (\n    <RiPopover\n      ownFocus\n      id=\"load-sample-data-popover\"\n      anchorPosition=\"upCenter\"\n      isOpen={isConfirmationOpen}\n      closePopover={() => setIsConfirmationOpen(false)}\n      panelClassName={cx('popoverLikeTooltip', styles.popover)}\n      panelPaddingSize=\"none\"\n      anchorClassName={cx(styles.buttonWrapper, anchorClassName)}\n      button={\n        <SecondaryButton\n          filled\n          onClick={() => setIsConfirmationOpen(true)}\n          className={styles.loadDataBtn}\n          loading={loading}\n          disabled={loading}\n          data-testid=\"load-sample-data-btn\"\n        >\n          Load sample data\n        </SecondaryButton>\n      }\n    >\n      <Row gap=\"m\" responsive={false} style={{ padding: 15 }}>\n        <FlexItem>\n          <RiIcon size=\"m\" type=\"ToastDangerIcon\" color=\"attention500\" />\n        </FlexItem>\n        <FlexItem>\n          <Text variant=\"semiBold\">Execute commands in bulk</Text>\n          <Spacer size=\"m\" />\n          <Text size=\"s\">\n            All commands from the file will be automatically executed against\n            your database. Avoid executing them in production databases.\n          </Text>\n          <Spacer size=\"l\" />\n          <Row justify=\"end\">\n            <FlexItem>\n              <PrimaryButton\n                size=\"s\"\n                icon={PlayFilledIcon}\n                iconSide=\"right\"\n                color=\"secondary\"\n                onClick={handleSampleData}\n                data-testid=\"load-sample-data-btn-confirm\"\n              >\n                Execute\n              </PrimaryButton>\n            </FlexItem>\n          </Row>\n        </FlexItem>\n      </Row>\n    </RiPopover>\n  )\n}\n\nexport default LoadSampleData\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/load-sample-data/index.ts",
    "content": "import LoadSampleData from './LoadSampleData'\n\nexport default LoadSampleData\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/load-sample-data/styles.module.scss",
    "content": ".popover {\n  min-width: 380px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-button/MakeSearchableButton.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, userEvent, cleanup } from 'uiSrc/utils/test-utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { TelemetryEvent } from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { SearchBrowserSource } from 'uiSrc/pages/vector-search/telemetry.constants'\n\nimport { MakeSearchableButton } from './MakeSearchableButton'\nimport { MakeSearchableButtonProps } from './MakeSearchableButton.types'\n\nconst mockSendEventTelemetry = jest.fn()\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: (...args: unknown[]) => mockSendEventTelemetry(...args),\n}))\n\nconst mockUseSelector = jest.fn()\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: (...args: unknown[]) => mockUseSelector(...args),\n}))\n\nconst mockInstanceId = faker.string.uuid()\n\nconst defaultProps: MakeSearchableButtonProps = {\n  keyName: { data: [116, 101, 115, 116], type: 'Buffer' },\n  keyNameString: 'product:123',\n  keyType: KeyTypes.Hash,\n}\n\nconst renderComponent = (\n  propsOverride?: Partial<MakeSearchableButtonProps>,\n) => {\n  const props = { ...defaultProps, ...propsOverride }\n  return render(<MakeSearchableButton {...props} />)\n}\n\ndescribe('MakeSearchableButton', () => {\n  beforeEach(() => {\n    cleanup()\n    mockUseSelector.mockImplementation((selector: any) => {\n      if (selector === connectedInstanceSelector) {\n        return { id: mockInstanceId }\n      }\n      return {}\n    })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render the button', () => {\n    renderComponent()\n\n    const makeSearchableBtn = screen.getByTestId('make-searchable-btn')\n\n    expect(makeSearchableBtn).toBeInTheDocument()\n    expect(makeSearchableBtn).toHaveTextContent('Make searchable')\n  })\n\n  it('should send SEARCH_MAKE_SEARCHABLE_CLICKED telemetry on click', async () => {\n    renderComponent()\n\n    const makeSearchableBtn = screen.getByTestId('make-searchable-btn')\n    await userEvent.click(makeSearchableBtn)\n\n    expect(mockSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CLICKED,\n      eventData: {\n        databaseId: mockInstanceId,\n        keyType: RedisearchIndexKeyType.HASH,\n        source: SearchBrowserSource.KeyDetails,\n      },\n    })\n  })\n\n  it('should send mapped keyType for JSON keys', async () => {\n    renderComponent({ keyType: KeyTypes.ReJSON })\n\n    const makeSearchableBtn = screen.getByTestId('make-searchable-btn')\n    await userEvent.click(makeSearchableBtn)\n\n    expect(mockSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CLICKED,\n      eventData: {\n        databaseId: mockInstanceId,\n        keyType: RedisearchIndexKeyType.JSON,\n        source: SearchBrowserSource.KeyDetails,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-button/MakeSearchableButton.tsx",
    "content": "import React, { useCallback, useMemo } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiTooltip } from 'uiSrc/components'\nimport { KEY_TYPE_MAP } from 'uiSrc/pages/vector-search/constants'\nimport { extractNamespace } from 'uiSrc/pages/vector-search/utils'\nimport { useMakeSearchableModal } from 'uiSrc/pages/browser/components/make-searchable-modal'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { SearchBrowserSource } from 'uiSrc/pages/vector-search/telemetry.constants'\n\nimport { MakeSearchableButtonProps } from './MakeSearchableButton.types'\n\nexport const MakeSearchableButton = ({\n  keyName,\n  keyNameString,\n  keyType,\n}: MakeSearchableButtonProps) => {\n  const { openMakeSearchableModal } = useMakeSearchableModal()\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const prefix = useMemo(() => extractNamespace(keyNameString), [keyNameString])\n\n  const source = SearchBrowserSource.KeyDetails\n\n  const mappedKeyType = KEY_TYPE_MAP[keyType]\n\n  const handleOpen = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CLICKED,\n      eventData: { databaseId: instanceId, keyType: mappedKeyType, source },\n    })\n    openMakeSearchableModal({\n      prefix,\n      initialKey: keyName,\n      initialKeyType: mappedKeyType,\n      initialPrefix: prefix,\n      source,\n    })\n  }, [\n    openMakeSearchableModal,\n    keyName,\n    keyType,\n    prefix,\n    instanceId,\n    mappedKeyType,\n  ])\n\n  return (\n    <RiTooltip\n      position=\"top\"\n      content={\n        <span>\n          Index data with the \"<strong>{prefix}</strong>\" prefix so you can\n          query it using full-text, vector, exact matching, and geospatial\n          search.\n        </span>\n      }\n    >\n      <PrimaryButton\n        size=\"small\"\n        onClick={handleOpen}\n        data-testid=\"make-searchable-btn\"\n      >\n        Make searchable\n      </PrimaryButton>\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-button/MakeSearchableButton.types.ts",
    "content": "import { KeyTypes } from 'uiSrc/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nexport interface MakeSearchableButtonProps {\n  keyName: RedisResponseBuffer\n  keyNameString: string\n  keyType: KeyTypes\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-button/index.ts",
    "content": "export { MakeSearchableButton } from './MakeSearchableButton'\nexport type { MakeSearchableButtonProps } from './MakeSearchableButton.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModal.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, userEvent, cleanup } from 'uiSrc/utils/test-utils'\n\nimport { MakeSearchableModal } from './MakeSearchableModal'\nimport { MakeSearchableModalProps } from './MakeSearchableModal.types'\n\nconst defaultProps: MakeSearchableModalProps = {\n  isOpen: true,\n  onConfirm: jest.fn(),\n  onCancel: jest.fn(),\n}\n\nconst renderComponent = (propsOverride?: Partial<MakeSearchableModalProps>) => {\n  const props = { ...defaultProps, ...propsOverride }\n  return render(<MakeSearchableModal {...props} />)\n}\n\ndescribe('MakeSearchableModal', () => {\n  beforeEach(() => {\n    cleanup()\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render nothing when isOpen is false', () => {\n    const { container } = renderComponent({ isOpen: false })\n\n    expect(container.innerHTML).toBe('')\n  })\n\n  it('should render the modal when isOpen is true', () => {\n    renderComponent()\n\n    expect(screen.getByTestId('make-searchable-modal-body')).toBeInTheDocument()\n  })\n\n  it('should display the title', () => {\n    renderComponent()\n\n    expect(screen.getByText('Make this data searchable')).toBeInTheDocument()\n  })\n\n  it('should show prefix in the body text when provided', () => {\n    renderComponent({ prefix: 'bicycle:' })\n\n    expect(screen.getByText(\"'bicycle:'\")).toBeInTheDocument()\n    expect(screen.getByText(/All keys starting with/)).toBeInTheDocument()\n  })\n\n  it('should show prefix text when prefix is empty string', () => {\n    renderComponent({ prefix: '' })\n\n    expect(screen.getByText(/All keys starting with/)).toBeInTheDocument()\n  })\n\n  it('should not show prefix text when prefix is undefined', () => {\n    renderComponent({ prefix: undefined })\n\n    expect(screen.queryByText(/All keys starting with/)).not.toBeInTheDocument()\n  })\n\n  it('should call onConfirm when Continue button is clicked', async () => {\n    const onConfirm = jest.fn()\n    renderComponent({ onConfirm })\n\n    await userEvent.click(screen.getByTestId('make-searchable-modal-confirm'))\n\n    expect(onConfirm).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when Cancel button is clicked', async () => {\n    const onCancel = jest.fn()\n    renderComponent({ onCancel })\n\n    await userEvent.click(screen.getByTestId('make-searchable-modal-cancel'))\n\n    expect(onCancel).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when close button is clicked', async () => {\n    const onCancel = jest.fn()\n    renderComponent({ onCancel })\n\n    await userEvent.click(screen.getByTestId('make-searchable-modal-close'))\n\n    expect(onCancel).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModal.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport { MakeSearchableModal } from './MakeSearchableModal'\n\nconst meta: Meta<typeof MakeSearchableModal> = {\n  component: MakeSearchableModal,\n  tags: ['autodocs'],\n  args: {\n    onConfirm: fn(),\n    onCancel: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    isOpen: true,\n    prefix: 'bicycle:',\n  },\n}\n\nexport const WithoutPrefix: Story = {\n  args: {\n    isOpen: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display/modal'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text, Title } from 'uiSrc/components/base/text'\n\nexport const ModalContent = styled(Modal.Content.Compose)`\n  width: 540px;\n`\n\nexport const Header = styled(Modal.Content.Header.Compose)`\n  flex-direction: column;\n  gap: ${({ theme }) => theme.core.space.space300};\n`\n\nexport const Illustration = styled(Row)`\n  height: 132px;\n  justify-content: center;\n`\n\nexport const Heading = styled(Title)`\n  text-align: center;\n`\n\nexport const Description = styled(Text)`\n  text-align: center;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModal.tsx",
    "content": "import React from 'react'\nimport { useTheme } from '@redis-ui/styles'\n\nimport { Modal } from 'uiSrc/components/base/display'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  SecondaryButton,\n  PrimaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nimport MakeSearchableImg from 'uiSrc/assets/img/vector-search/make-searchable-modal-img.svg?react'\nimport MakeSearchableImgDark from 'uiSrc/assets/img/vector-search/make-searchable-modal-img-dark.svg?react'\n\nimport { MakeSearchableModalProps } from './MakeSearchableModal.types'\nimport * as S from './MakeSearchableModal.styles'\n\nconst TEST_ID = 'make-searchable-modal'\n\nexport const MakeSearchableModal = ({\n  isOpen,\n  prefix,\n  onConfirm,\n  onCancel,\n}: MakeSearchableModalProps) => {\n  const theme = useTheme()\n  const Illustration =\n    theme.name === 'dark' ? MakeSearchableImgDark : MakeSearchableImg\n\n  if (!isOpen) return null\n\n  return (\n    <Modal.Compose open={isOpen}>\n      <S.ModalContent persistent onCancel={onCancel}>\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={onCancel}\n          data-testid={`${TEST_ID}-close`}\n        />\n\n        <S.Header>\n          <S.Illustration>\n            <Illustration />\n          </S.Illustration>\n\n          <S.Heading\n            size=\"XL\"\n            color=\"primary\"\n            data-testid={`${TEST_ID}-heading`}\n          >\n            Make this data searchable\n          </S.Heading>\n        </S.Header>\n\n        <S.Description color=\"secondary\" data-testid={`${TEST_ID}-body`}>\n          We&rsquo;ll take you to the Search workspace to set up the index.\n          {prefix != null && (\n            <>\n              {' '}\n              All keys starting with{' '}\n              <Text color=\"secondary\" variant=\"semiBold\" component=\"span\">\n                '{prefix}'\n              </Text>{' '}\n              will be included.\n            </>\n          )}{' '}\n          You can review and adjust the schema before creating the index.\n        </S.Description>\n\n        <Modal.Content.Footer.Compose>\n          <Row gap=\"m\" justify=\"end\">\n            <SecondaryButton\n              size=\"large\"\n              onClick={onCancel}\n              data-testid={`${TEST_ID}-cancel`}\n            >\n              Cancel\n            </SecondaryButton>\n            <PrimaryButton\n              size=\"large\"\n              onClick={onConfirm}\n              data-testid={`${TEST_ID}-confirm`}\n            >\n              Continue\n            </PrimaryButton>\n          </Row>\n        </Modal.Content.Footer.Compose>\n      </S.ModalContent>\n    </Modal.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModal.types.ts",
    "content": "export interface MakeSearchableModalProps {\n  isOpen: boolean\n  prefix?: string\n  onConfirm: () => void\n  onCancel: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModalProvider.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, userEvent, cleanup } from 'uiSrc/utils/test-utils'\nimport { TelemetryEvent } from 'uiSrc/telemetry'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  SearchBrowserSource,\n  SearchTelemetrySource,\n} from 'uiSrc/pages/vector-search/telemetry.constants'\n\nimport {\n  MakeSearchableModalProvider,\n  useMakeSearchableModal,\n} from './MakeSearchableModalProvider'\n\nconst mockSendEventTelemetry = jest.fn()\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: (...args: unknown[]) => mockSendEventTelemetry(...args),\n}))\n\nconst mockInstanceId = faker.string.uuid()\nconst mockPush = jest.fn()\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({ push: mockPush }),\n}))\n\nconst mockUseSelector = jest.fn()\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: (...args: unknown[]) => mockUseSelector(...args),\n}))\n\nconst TestConsumer = () => {\n  const { openMakeSearchableModal } = useMakeSearchableModal()\n\n  return (\n    <button\n      data-testid=\"open-modal\"\n      onClick={() =>\n        openMakeSearchableModal({\n          prefix: 'product:',\n          initialKey: { data: [116, 101, 115, 116], type: 'Buffer' },\n          initialKeyType: RedisearchIndexKeyType.HASH,\n          initialPrefix: 'product:',\n          source: SearchBrowserSource.KeyDetails,\n        })\n      }\n    >\n      Open\n    </button>\n  )\n}\n\nconst renderComponent = () =>\n  render(\n    <MakeSearchableModalProvider>\n      <TestConsumer />\n    </MakeSearchableModalProvider>,\n  )\n\ndescribe('MakeSearchableModalProvider', () => {\n  beforeEach(() => {\n    cleanup()\n    mockUseSelector.mockImplementation((selector: any) => {\n      if (selector === connectedInstanceSelector) {\n        return { id: mockInstanceId }\n      }\n      return {}\n    })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should open modal when openMakeSearchableModal is called', async () => {\n    renderComponent()\n\n    const openModalBtn = screen.getByTestId('open-modal')\n    await userEvent.click(openModalBtn)\n\n    const modalBody = screen.getByTestId('make-searchable-modal-body')\n    expect(modalBody).toBeInTheDocument()\n  })\n\n  it('should send SEARCH_MAKE_SEARCHABLE_CONFIRMED on confirm', async () => {\n    renderComponent()\n\n    const openModalBtn = screen.getByTestId('open-modal')\n    await userEvent.click(openModalBtn)\n\n    const confirmBtn = screen.getByTestId('make-searchable-modal-confirm')\n    await userEvent.click(confirmBtn)\n\n    expect(mockSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CONFIRMED,\n      eventData: {\n        databaseId: mockInstanceId,\n        keyType: RedisearchIndexKeyType.HASH,\n        source: SearchBrowserSource.KeyDetails,\n      },\n    })\n  })\n\n  it('should send SEARCH_OWN_DATA_INDEX_TRIGGERED with browser source on confirm', async () => {\n    renderComponent()\n\n    const openModalBtn = screen.getByTestId('open-modal')\n    await userEvent.click(openModalBtn)\n\n    const confirmBtn = screen.getByTestId('make-searchable-modal-confirm')\n    await userEvent.click(confirmBtn)\n\n    expect(mockSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_OWN_DATA_INDEX_TRIGGERED,\n      eventData: {\n        databaseId: mockInstanceId,\n        source: SearchTelemetrySource.Browser,\n      },\n    })\n  })\n\n  it('should send SEARCH_MAKE_SEARCHABLE_CANCELLED on cancel', async () => {\n    renderComponent()\n\n    const openModalBtn = screen.getByTestId('open-modal')\n    await userEvent.click(openModalBtn)\n\n    const cancelBtn = screen.getByTestId('make-searchable-modal-cancel')\n    await userEvent.click(cancelBtn)\n\n    expect(mockSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CANCELLED,\n      eventData: {\n        databaseId: mockInstanceId,\n        source: SearchBrowserSource.KeyDetails,\n      },\n    })\n  })\n\n  it('should send SEARCH_MAKE_SEARCHABLE_CANCELLED on close button', async () => {\n    renderComponent()\n\n    const openModalBtn = screen.getByTestId('open-modal')\n    await userEvent.click(openModalBtn)\n\n    const closeBtn = screen.getByTestId('make-searchable-modal-close')\n    await userEvent.click(closeBtn)\n\n    expect(mockSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CANCELLED,\n      eventData: {\n        databaseId: mockInstanceId,\n        source: SearchBrowserSource.KeyDetails,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/MakeSearchableModalProvider.tsx",
    "content": "import React, {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\n\nimport { Pages } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { bufferToString } from 'uiSrc/utils'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { CreateIndexMode } from 'uiSrc/pages/vector-search/pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  SearchBrowserSource,\n  SearchTelemetrySource,\n} from 'uiSrc/pages/vector-search/telemetry.constants'\n\nimport { MakeSearchableModal } from './MakeSearchableModal'\n\nexport interface MakeSearchableModalConfig {\n  prefix?: string\n  initialKey?: RedisResponseBuffer\n  initialKeyType?: RedisearchIndexKeyType\n  initialPrefix?: string\n  source?: SearchBrowserSource\n}\n\nconst MakeSearchableModalContext = createContext<{\n  openMakeSearchableModal: (config: MakeSearchableModalConfig) => void\n} | null>(null)\n\nconst NOOP_CONTEXT = {\n  openMakeSearchableModal: () => {},\n}\n\nexport const useMakeSearchableModal = () => {\n  const ctx = useContext(MakeSearchableModalContext)\n  return ctx ?? NOOP_CONTEXT\n}\n\nexport const MakeSearchableModalProvider = ({\n  children,\n}: {\n  children: React.ReactNode\n}) => {\n  const [config, setConfig] = useState<MakeSearchableModalConfig | null>(null)\n  const history = useHistory()\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const openMakeSearchableModal = useCallback(\n    (cfg: MakeSearchableModalConfig) => setConfig(cfg),\n    [],\n  )\n\n  const handleConfirm = useCallback(() => {\n    if (\n      !config ||\n      !config?.initialKey ||\n      !config?.initialKeyType ||\n      config?.initialPrefix == null\n    ) {\n      return\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CONFIRMED,\n      eventData: {\n        databaseId: instanceId,\n        keyType: config.initialKeyType,\n        source: config.source,\n      },\n    })\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_OWN_DATA_INDEX_TRIGGERED,\n      eventData: {\n        databaseId: instanceId,\n        source: SearchTelemetrySource.Browser,\n      },\n    })\n\n    setConfig(null)\n\n    const search = new URLSearchParams()\n    search.set('mode', CreateIndexMode.ExistingData)\n    search.set('initialKey', bufferToString(config.initialKey))\n    search.set('initialKeyType', config.initialKeyType)\n    search.set('initialPrefix', config.initialPrefix)\n    history.push({\n      pathname: Pages.vectorSearchCreateIndex(instanceId),\n      search: search.toString(),\n    })\n  }, [config, history, instanceId])\n\n  const handleCancel = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CANCELLED,\n      eventData: { databaseId: instanceId, source: config?.source },\n    })\n    setConfig(null)\n  }, [instanceId, config?.source])\n\n  const contextValue = useMemo(\n    () => ({ openMakeSearchableModal }),\n    [openMakeSearchableModal],\n  )\n\n  return (\n    <MakeSearchableModalContext.Provider value={contextValue}>\n      {children}\n      <MakeSearchableModal\n        isOpen={config !== null}\n        prefix={config?.prefix}\n        onConfirm={handleConfirm}\n        onCancel={handleCancel}\n      />\n    </MakeSearchableModalContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/make-searchable-modal/index.ts",
    "content": "export { MakeSearchableModal } from './MakeSearchableModal'\nexport type { MakeSearchableModalProps } from './MakeSearchableModal.types'\nexport {\n  MakeSearchableModalProvider,\n  useMakeSearchableModal,\n} from './MakeSearchableModalProvider'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  waitForRiPopoverVisible,\n  waitForStack,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry } from 'uiSrc/telemetry'\nimport { apiService } from 'uiSrc/services'\nimport {\n  bulkImportDefaultData,\n  bulkImportDefaultDataSuccess,\n} from 'uiSrc/slices/browser/bulkActions'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { loadKeys } from 'uiSrc/slices/browser/keys'\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n  resetExplorePanelSearch,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport NoKeysFound, { Props } from './NoKeysFound'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('NoKeysFound', () => {\n  it('should render', () => {\n    expect(render(<NoKeysFound {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call props on click buttons', () => {\n    const onAddMock = jest.fn()\n\n    render(<NoKeysFound {...mockedProps} onAddKeyPanel={onAddMock} />)\n\n    fireEvent.click(screen.getByTestId('add-key-msg-btn'))\n\n    expect(onAddMock).toBeCalled()\n  })\n\n  it('should call proper actions after click load sample data', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    apiService.post = jest\n      .fn()\n      .mockResolvedValueOnce({ status: 200, data: { data: {} } })\n\n    render(<NoKeysFound {...mockedProps} onAddKeyPanel={jest.fn()} />)\n\n    fireEvent.click(screen.getByTestId('load-sample-data-btn'))\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('load-sample-data-btn-confirm'))\n\n    await waitForStack()\n\n    const expectedActions = [\n      bulkImportDefaultData(),\n      bulkImportDefaultDataSuccess(),\n      addMessageNotification(successMessages.UPLOAD_DATA_BULK()),\n      changeSelectedTab(InsightsPanelTabs.Explore),\n      changeSidePanel(SidePanels.Insights),\n      resetExplorePanelSearch(),\n      loadKeys(),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.styles.ts",
    "content": "import styled from 'styled-components'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\n\nexport const AddKeysManuallyButton = styled(EmptyButton)`\n  height: 30px;\n`\n\nexport const StyledImage = styled(RiImage)`\n  width: 174px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport NoDataImg from 'uiSrc/assets/img/no-data.svg'\n\nimport { findTutorialPath } from 'uiSrc/utils'\nimport {\n  openTutorialByPath,\n  sidePanelsSelector,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport {\n  changeKeyViewType,\n  fetchKeys,\n  keysSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { SCAN_TREE_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { TutorialsIds } from 'uiSrc/constants'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\n\nimport LoadSampleData from '../load-sample-data'\nimport { AddKeysManuallyButton, StyledImage } from './NoKeysFound.styles'\n\nexport interface Props {\n  onAddKeyPanel: (value: boolean) => void\n}\n\nconst NoKeysFound = (props: Props) => {\n  const { onAddKeyPanel } = props\n  const { openedPanel } = useSelector(sidePanelsSelector)\n  const { viewType } = useSelector(keysSelector)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const onSuccessLoadData = () => {\n    if (openedPanel !== SidePanels.AiAssistant) {\n      const tutorialPath = findTutorialPath({ id: TutorialsIds.RedisUseCases })\n      dispatch(openTutorialByPath(tutorialPath, history, true))\n    }\n\n    if (viewType === KeyViewType.Browser) {\n      dispatch(changeKeyViewType(KeyViewType.Tree))\n    }\n\n    dispatch(\n      fetchKeys({\n        searchMode: SearchMode.Pattern,\n        cursor: '0',\n        count: SCAN_TREE_COUNT_DEFAULT,\n      }),\n    )\n\n    onAddKeyPanel(false)\n  }\n\n  return (\n    <Col align=\"center\" data-testid=\"no-result-found-msg\">\n      <StyledImage src={NoDataImg} alt=\"no results\" />\n      <Spacer />\n      <Title color=\"primary\" size=\"XL\">\n        Let&apos;s start working\n      </Title>\n      <Spacer />\n      <Row gap=\"m\" align=\"center\">\n        <LoadSampleData onSuccess={onSuccessLoadData} />\n        <AddKeysManuallyButton\n          icon={PlusIcon}\n          onClick={() => onAddKeyPanel(true)}\n          data-testid=\"add-key-msg-btn\"\n        >\n          Add key manually\n        </AddKeysManuallyButton>\n      </Row>\n    </Col>\n  )\n}\n\nexport default NoKeysFound\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-found/index.ts",
    "content": "import NoKeysFound from './NoKeysFound'\n\nexport default NoKeysFound\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport { render, mockedStore, cleanup } from 'uiSrc/utils/test-utils'\n\nimport { keysSelector } from 'uiSrc/slices/browser/keys'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { redisearchSelector } from 'uiSrc/slices/browser/redisearch'\nimport NoKeysMessage, { Props } from './NoKeysMessage'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  keysSelector: jest.fn().mockReturnValue({\n    searchMode: 'Pattern',\n    filter: null,\n    search: '',\n    viewType: 'Browser',\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/redisearch'),\n  redisearchSelector: jest.fn().mockReturnValue({\n    search: '',\n    isSearched: false,\n    selectedIndex: null,\n  }),\n}))\n\ndescribe('NoKeysMessage', () => {\n  it('should render', () => {\n    expect(render(<NoKeysMessage {...mockedProps} />)).toBeTruthy()\n  })\n\n  describe('SearchMode = Pattern', () => {\n    it('NoKeysFound should be rendered if total=0', () => {\n      const { queryByTestId } = render(\n        <NoKeysMessage {...instance(mockedProps)} total={0} />,\n      )\n      expect(queryByTestId('no-result-found-msg')).toBeInTheDocument()\n    })\n\n    it('\"scan-no-results-found\" should be rendered if searched and scanned < total', () => {\n      const keysSelectorMock = jest.fn().mockReturnValue({\n        isSearched: true,\n        searchMode: SearchMode.Pattern,\n      })\n      const total = 100\n\n      ;(keysSelector as jest.Mock).mockImplementation(keysSelectorMock)\n      const { queryByTestId } = render(\n        <NoKeysMessage\n          {...instance(mockedProps)}\n          total={total}\n          scanned={total - 1}\n        />,\n      )\n\n      expect(queryByTestId('scan-no-results-found')).toBeInTheDocument()\n    })\n\n    it('\"no-result-found\" should be rendered if searched and scanned===total', () => {\n      const keysSelectorMock = jest.fn().mockReturnValue({\n        isSearched: true,\n        searchMode: SearchMode.Pattern,\n      })\n      const total = 100\n\n      ;(keysSelector as jest.Mock).mockImplementation(keysSelectorMock)\n      const { container } = render(\n        <NoKeysMessage\n          {...instance(mockedProps)}\n          total={total}\n          scanned={total}\n        />,\n      )\n\n      expect(\n        container.querySelector('[data-test-subj=\"no-result-found\"]'),\n      ).toBeInTheDocument()\n    })\n\n    it('\"scan-no-results-found\" should be rendered if filtered and scanned<total', () => {\n      const keysSelectorMock = jest.fn().mockReturnValue({\n        isFiltered: true,\n        searchMode: SearchMode.Pattern,\n      })\n      const total = 100\n\n      ;(keysSelector as jest.Mock).mockImplementation(keysSelectorMock)\n      const { queryByTestId } = render(\n        <NoKeysMessage\n          {...instance(mockedProps)}\n          total={total}\n          scanned={total - 1}\n        />,\n      )\n\n      expect(queryByTestId('scan-no-results-found')).toBeInTheDocument()\n    })\n  })\n\n  describe('SearchMode = RediSearch', () => {\n    it('\"no-result-select-index\" should be rendered if searched and scanned < total', () => {\n      const keysSelectorMock = jest.fn().mockReturnValue({\n        isSearched: true,\n        searchMode: SearchMode.Redisearch,\n      })\n      const redisearchSelectorMock = jest.fn().mockReturnValue({\n        selectedIndex: null,\n      })\n      const total = 100\n\n      ;(keysSelector as jest.Mock).mockImplementation(keysSelectorMock)\n      ;(redisearchSelector as jest.Mock).mockImplementation(\n        redisearchSelectorMock,\n      )\n      const { queryByTestId } = render(\n        <NoKeysMessage\n          {...instance(mockedProps)}\n          total={total}\n          scanned={total - 1}\n        />,\n      )\n\n      expect(queryByTestId('no-result-select-index')).toBeInTheDocument()\n    })\n    it('\"no-result-found-only\" should be rendered total = 0', () => {\n      const keysSelectorMock = jest.fn().mockReturnValue({\n        isSearched: true,\n        searchMode: SearchMode.Redisearch,\n      })\n      const redisearchSelectorMock = jest.fn().mockReturnValue({\n        selectedIndex: '123',\n      })\n      const total = 0\n\n      ;(keysSelector as jest.Mock).mockImplementation(keysSelectorMock)\n      ;(redisearchSelector as jest.Mock).mockImplementation(\n        redisearchSelectorMock,\n      )\n      const { queryByTestId } = render(\n        <NoKeysMessage {...instance(mockedProps)} total={total} />,\n      )\n\n      expect(queryByTestId('no-result-found-only')).toBeInTheDocument()\n    })\n\n    it('\"no-result-found-only\" should be rendered if searched and scanned<total', () => {\n      const keysSelectorMock = jest.fn().mockReturnValue({\n        searchMode: SearchMode.Redisearch,\n      })\n      const redisearchSelectorMock = jest.fn().mockReturnValue({\n        isSearched: true,\n        selectedIndex: '123',\n      })\n      const total = 100\n\n      ;(keysSelector as jest.Mock).mockImplementation(keysSelectorMock)\n      ;(redisearchSelector as jest.Mock).mockImplementation(\n        redisearchSelectorMock,\n      )\n      const { queryByTestId } = render(\n        <NoKeysMessage\n          {...instance(mockedProps)}\n          total={total}\n          scanned={total - 1}\n        />,\n      )\n\n      expect(queryByTestId('no-result-found-only')).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-message/NoKeysMessage.tsx",
    "content": "import React from 'react'\n\nimport { useSelector } from 'react-redux'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\n\nimport {\n  FullScanNoResultsFoundText,\n  LoadingText,\n  NoResultsFoundText,\n  NoSelectedIndexText,\n  ScanNoResultsFoundText,\n} from 'uiSrc/constants/texts'\nimport { keysSelector } from 'uiSrc/slices/browser/keys'\nimport { redisearchSelector } from 'uiSrc/slices/browser/redisearch'\n\nimport NoKeysFound from '../no-keys-found'\n\nexport interface Props {\n  isLoading: boolean\n  total: number\n  scanned: number\n  onAddKeyPanel: (value: boolean) => void\n}\n\nconst NoKeysMessage = (props: Props) => {\n  const { total, scanned, onAddKeyPanel, isLoading } = props\n\n  const { selectedIndex, isSearched: redisearchIsSearched } =\n    useSelector(redisearchSelector)\n  const {\n    isSearched: patternIsSearched,\n    isFiltered,\n    searchMode,\n  } = useSelector(keysSelector)\n\n  if (searchMode === SearchMode.Redisearch) {\n    if (!selectedIndex) {\n      return NoSelectedIndexText\n    }\n\n    if (isLoading) {\n      return LoadingText\n    }\n\n    if (total === 0) {\n      return NoResultsFoundText\n    }\n\n    if (redisearchIsSearched) {\n      return scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText\n    }\n  }\n\n  if (isLoading) {\n    return LoadingText\n  }\n\n  if (total === 0) {\n    return <NoKeysFound onAddKeyPanel={onAddKeyPanel} />\n  }\n\n  if (patternIsSearched) {\n    return scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText\n  }\n\n  if (isFiltered && scanned < total) {\n    return ScanNoResultsFoundText\n  }\n\n  return NoResultsFoundText\n}\n\nexport default NoKeysMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/no-keys-message/index.ts",
    "content": "import NoKeysMessage from './NoKeysMessage'\n\nexport default NoKeysMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/onboarding-start-popover/OnboardingStartPopover.spec.tsx",
    "content": "import React from 'react'\n\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  clearStoreActions,\n} from 'uiSrc/utils/test-utils'\nimport { setOnboardNextStep, skipOnboarding } from 'uiSrc/slices/app/features'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OnboardingStepName } from 'uiSrc/constants/onboarding'\nimport OnboardingStartPopover from './OnboardingStartPopover'\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureOnboardingSelector: jest.fn().mockReturnValue({\n    currentStep: 0,\n    isActive: true,\n    totalSteps: 14,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('OnboardingStartPopover', () => {\n  it('should render', () => {\n    expect(render(<OnboardingStartPopover />)).toBeTruthy()\n  })\n\n  it('should render start popover', () => {\n    render(<OnboardingStartPopover />)\n\n    expect(screen.getByTestId('onboarding-start-popover')).toBeInTheDocument()\n    expect(screen.getByTestId('onboarding-start-content')).toBeInTheDocument()\n  })\n\n  it('should call proper actions after click start', () => {\n    render(<OnboardingStartPopover />)\n\n    fireEvent.click(screen.getByTestId('start-tour-btn'))\n\n    const expectedActions = [setOnboardNextStep()]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call proper actions after click skip button', () => {\n    render(<OnboardingStartPopover />)\n\n    fireEvent.click(screen.getByTestId('skip-tour-btn'))\n\n    const expectedActions = [skipOnboarding()]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call proper telemetry after click start', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(<OnboardingStartPopover />)\n\n    fireEvent.click(screen.getByTestId('start-tour-btn'))\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.ONBOARDING_TOUR_CLICKED,\n      eventData: {\n        action: 'next',\n        databaseId: '',\n        step: OnboardingStepName.Start,\n      },\n    })\n    sendEventTelemetry.mockRestore()\n  })\n\n  it('should call proper telemetry after click skip button', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(<OnboardingStartPopover />)\n\n    fireEvent.click(screen.getByTestId('skip-tour-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.ONBOARDING_TOUR_CLICKED,\n      eventData: {\n        action: 'closed',\n        databaseId: '',\n        step: OnboardingStepName.Start,\n      },\n    })\n    sendEventTelemetry.mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/onboarding-start-popover/OnboardingStartPopover.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  appFeatureOnboardingSelector,\n  setOnboardNextStep,\n  skipOnboarding,\n} from 'uiSrc/slices/app/features'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { EmptyButton, PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport styles from './styles.module.scss'\n\nconst OnboardingStartPopover = () => {\n  const { id: connectedInstanceId = '' } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { isActive, currentStep } = useSelector(appFeatureOnboardingSelector)\n  const dispatch = useDispatch()\n\n  const sendTelemetry = (action: string) =>\n    sendEventTelemetry({\n      event: TelemetryEvent.ONBOARDING_TOUR_CLICKED,\n      eventData: {\n        databaseId: connectedInstanceId,\n        step: OnboardingStepName.Start,\n        action,\n      },\n    })\n\n  const handleSkip = () => {\n    dispatch(skipOnboarding())\n    sendTelemetry('closed')\n  }\n\n  const handleStart = () => {\n    dispatch(setOnboardNextStep())\n    sendTelemetry('next')\n  }\n\n  return (\n    <RiPopover\n      button={<></>}\n      isOpen={isActive && currentStep === OnboardingSteps.Start}\n      ownFocus={false}\n      closePopover={() => {}}\n      panelClassName={styles.onboardingStartPopover}\n      anchorPosition=\"downRight\"\n      data-testid=\"onboarding-start-popover\"\n    >\n      <Title size=\"S\">Take a quick tour of Redis Insight?</Title>\n      <Spacer size=\"s\" />\n      <Text data-testid=\"onboarding-start-content\">\n        Hi! Redis Insight has many tools that can help you to optimize the\n        development process.\n        <br />\n        Would you like us to show them to you?\n      </Text>\n      <Spacer />\n      <Row justify=\"between\">\n        <EmptyButton\n          onClick={handleSkip}\n          size=\"small\"\n          data-testid=\"skip-tour-btn\"\n        >\n          Skip tour\n        </EmptyButton>\n        <PrimaryButton\n          onClick={handleStart}\n          color=\"secondary\"\n          size=\"s\"\n          data-testid=\"start-tour-btn\"\n        >\n          Show me around\n        </PrimaryButton>\n      </Row>\n    </RiPopover>\n  )\n}\n\nexport default OnboardingStartPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/onboarding-start-popover/index.ts",
    "content": "import OnboardingStartPopover from './OnboardingStartPopover'\n\nexport default OnboardingStartPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/onboarding-start-popover/styles.module.scss",
    "content": ".onboardingStartPopover {\n  width: 360px;\n  margin-right: 30px; // Dirty placement fix, to position popover in the bottom right corner of the screen, in line with the browser containers\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { act } from '@testing-library/react'\nimport { anyToBuffer } from 'uiSrc/utils'\nimport {\n  render,\n  screen,\n  fireEvent,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { MOCK_TRUNCATED_STRING_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA } from 'uiSrc/constants'\nimport PopoverDelete, { Props } from './PopoverDelete'\n\nconst mockedProps = mock<Props>()\n\ndescribe('PopoverDelete', () => {\n  it('should render', () => {\n    expect(render(<PopoverDelete {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call showPopover on delete', () => {\n    const showPopover = jest.fn()\n    render(\n      <PopoverDelete\n        {...instance(mockedProps)}\n        item=\"name\"\n        showPopover={showPopover}\n      />,\n    )\n    fireEvent.click(screen.getByLabelText(/remove field/i))\n\n    expect(showPopover).toBeCalledTimes(1)\n  })\n\n  it('should disable delete button for truncated strings', async () => {\n    const showPopover = jest.fn()\n    render(\n      <PopoverDelete\n        {...instance(mockedProps)}\n        item={MOCK_TRUNCATED_STRING_VALUE}\n        showPopover={showPopover}\n      />,\n    )\n    fireEvent.click(screen.getByLabelText(/remove field/i))\n\n    expect(showPopover).toBeCalledTimes(0)\n\n    const removeButton = screen.getByTestId('remove-icon')\n\n    expect(removeButton).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.focus(removeButton)\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('remove-tooltip')).toHaveTextContent(\n      TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n    )\n  })\n\n  it('should call handleDeleteItem on delete', () => {\n    const handleDeleteItem = jest.fn()\n    render(\n      <PopoverDelete\n        {...instance(mockedProps)}\n        item=\"name\"\n        suffix=\"_\"\n        deleting=\"name_\"\n        handleDeleteItem={handleDeleteItem}\n      />,\n    )\n\n    const deleteBtn = screen.getByTestId('remove')\n    fireEvent.click(deleteBtn)\n    expect(handleDeleteItem).toBeCalledTimes(1)\n  })\n\n  it('should call handleDeleteItem on delete with itemRaw prop', () => {\n    const itemRawMock = anyToBuffer([1, 2, 3])\n    const handleDeleteItem = jest.fn()\n    render(\n      <PopoverDelete\n        {...instance(mockedProps)}\n        item=\"name\"\n        itemRaw={itemRawMock}\n        suffix=\"_\"\n        deleting=\"name_\"\n        handleDeleteItem={handleDeleteItem}\n      />,\n    )\n\n    const deleteBtn = screen.getByTestId('remove')\n    fireEvent.click(deleteBtn)\n    expect(handleDeleteItem).toBeCalledTimes(1)\n    expect(handleDeleteItem).toBeCalledWith(itemRawMock)\n  })\n\n  it('should call handleDeleteItem on delete with item prop if itemRaw is not defined', () => {\n    const itemMock = 'name'\n    const handleDeleteItem = jest.fn()\n    render(\n      <PopoverDelete\n        {...instance(mockedProps)}\n        item={itemMock}\n        itemRaw={undefined}\n        suffix=\"_\"\n        deleting=\"name_\"\n        handleDeleteItem={handleDeleteItem}\n      />,\n    )\n\n    const deleteBtn = screen.getByTestId('remove')\n    fireEvent.click(deleteBtn)\n    expect(handleDeleteItem).toBeCalledTimes(1)\n    expect(handleDeleteItem).toBeCalledWith(itemMock)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/popover-delete/PopoverDelete.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components/base'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { RedisString } from 'uiSrc/slices/interfaces'\nimport { isTruncatedString } from 'uiSrc/utils'\nimport { TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA } from 'uiSrc/constants'\nimport {\n  DestructiveButton,\n  EmptyButton,\n  IconButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport styles from './styles.module.scss'\nimport ConfirmationPopover from 'uiSrc/components/confirmation-popover'\n\nexport interface Props {\n  header?: JSX.Element | string\n  text: JSX.Element | string\n  item: string\n  itemRaw?: RedisString\n  suffix?: string\n  deleting: string\n  closePopover: () => void\n  showPopover: (item: string) => void\n  updateLoading: boolean\n  handleDeleteItem: (item: RedisString | string) => void\n  handleButtonClick?: () => void\n  appendInfo?: JSX.Element | string | null\n  testid?: string\n  buttonLabel?: string\n  persistent?: boolean\n  customOutsideDetector?: boolean\n}\n\nconst PopoverDelete = (props: Props) => {\n  const {\n    header,\n    text,\n    item,\n    itemRaw,\n    suffix = '',\n    deleting,\n    closePopover,\n    updateLoading,\n    showPopover,\n    handleDeleteItem,\n    handleButtonClick,\n    appendInfo,\n    testid = '',\n    buttonLabel,\n    persistent,\n    customOutsideDetector,\n  } = props\n\n  const isDisabled = isTruncatedString(item)\n\n  const onButtonClick = (\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  ) => {\n    e.stopPropagation()\n    if (item + suffix !== deleting) {\n      showPopover(item)\n      handleButtonClick?.()\n    } else {\n      closePopover()\n    }\n  }\n\n  const deleteButton = buttonLabel ? (\n    <EmptyButton\n      icon={DeleteIcon}\n      aria-label=\"Remove field\"\n      disabled={isDisabled || updateLoading}\n      onClick={isDisabled ? () => {} : onButtonClick}\n      data-testid={testid ? `${testid}-icon` : 'remove-icon'}\n    >\n      {buttonLabel}\n    </EmptyButton>\n  ) : (\n    <IconButton\n      size=\"M\"\n      icon={DeleteIcon}\n      aria-label=\"Remove field\"\n      disabled={isDisabled || updateLoading}\n      onClick={isDisabled ? () => {} : onButtonClick}\n      data-testid={testid ? `${testid}-icon` : 'remove-icon'}\n    />\n  )\n\n  const deleteButtonWithTooltip = (\n    <RiTooltip\n      content={TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA}\n      anchorClassName={styles.editBtnAnchor}\n      data-testid={testid ? `${testid}-tooltip` : 'remove-tooltip'}\n    >\n      {deleteButton}\n    </RiTooltip>\n  )\n\n  return (\n    <ConfirmationPopover\n      key={item}\n      anchorPosition=\"leftCenter\"\n      ownFocus\n      isOpen={item + suffix === deleting}\n      closePopover={() => closePopover()}\n      panelPaddingSize=\"m\"\n      anchorClassName=\"deleteFieldPopover\"\n      button={isDisabled ? deleteButtonWithTooltip : deleteButton}\n      onClick={(e) => e.stopPropagation()}\n      title={header}\n      message={text}\n      persistent={persistent}\n      appendInfo={appendInfo}\n      confirmButton={\n        <DestructiveButton\n          size=\"small\"\n          icon={DeleteIcon}\n          onClick={() => handleDeleteItem(itemRaw || item)}\n          data-testid={testid || 'remove'}\n        >\n          Remove\n        </DestructiveButton>\n      }\n      customOutsideDetector={customOutsideDetector}\n    />\n  )\n}\n\nexport default PopoverDelete\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/popover-delete/styles.module.scss",
    "content": ":global {\n  .euiPanel--paddingMedium {\n    padding: 18px !important;\n  }\n}\n\n.popover {\n  max-width: 564px !important;\n  word-wrap: break-word;\n}\n\n.popoverFooter {\n  display: flex;\n  justify-content: flex-end;\n  margin-top: 12px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.spec.tsx",
    "content": "import { merge } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { useSelector } from 'react-redux'\n\nimport {\n  cleanup,\n  clearStoreActions,\n  fireEvent,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n  render,\n  screen,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\nimport {\n  loadList,\n  redisearchListSelector,\n  setSelectedIndex,\n} from 'uiSrc/slices/browser/redisearch'\nimport {\n  resetKeyInfo,\n  changeSearchMode,\n  fetchKeys,\n} from 'uiSrc/slices/browser/keys'\nimport { setBrowserSelectedKey } from 'uiSrc/slices/app/context'\nimport { bufferToString, stringToBuffer } from 'uiSrc/utils'\nimport { localStorageService } from 'uiSrc/services'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { BrowserStorageItem, FeatureFlags } from 'uiSrc/constants'\nimport RediSearchIndexesList, { Props } from './RediSearchIndexesList'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { setStoreRef } from 'uiSrc/utils/test-store'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = mockStore(\n    merge({}, initialStateDefault, {\n      connections: {\n        instances: {\n          connectedInstance: {\n            id: INSTANCE_ID_MOCK,\n          },\n        },\n      },\n    }),\n  )\n  setStoreRef(store)\n  store.clearActions()\n})\n\nconst mockedProps = mock<Props>()\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  fetchKeys: jest.fn(),\n  keysSelector: jest.fn().mockReturnValue({\n    searchMode: 'Redisearch',\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/redisearch'),\n  redisearchListSelector: jest.fn().mockReturnValue({\n    data: [],\n    loading: false,\n    error: '',\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '123',\n    connectionType: 'STANDALONE',\n    db: 0,\n  }),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\nconst renderRediSearchIndexesList = (props: Props) => {\n  return render(<RediSearchIndexesList {...props} />, { store })\n}\n\ndescribe('RediSearchIndexesList', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: any) => any) =>\n        callback({\n          ...state,\n          browser: {\n            ...state.browser,\n            keys: {\n              ...state.browser.keys,\n              searchMode: SearchMode.Redisearch,\n            },\n            redisearch: { ...state.browser.redisearch, loading: false },\n          },\n          connections: {\n            ...state.connections,\n            instances: {\n              ...state.connections.instances,\n              connectedInstance: {\n                ...state.connections.instances.connectedInstance,\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n          app: {\n            ...state.app,\n            features: {\n              ...state.app.features,\n              featureFlags: {\n                ...state.app.features.featureFlags,\n                features: {\n                  ...state.app.features.featureFlags?.features,\n                  [FeatureFlags.vectorSearchV2]: { flag: true },\n                },\n              },\n            },\n          },\n        }),\n    )\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      ...state.connections.instances.connectedInstance,\n      loading: true,\n    }))\n  })\n\n  it('should render', () => {\n    expect(renderRediSearchIndexesList(instance(mockedProps))).toBeTruthy()\n    const searchInput = screen.getByTestId('select-search-mode')\n    expect(searchInput).toBeInTheDocument()\n  })\n\n  it('should render and call changeSearchMode if no RediSearch module', () => {\n    localStorageService.set = jest.fn()\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      host: '123.123.2.2',\n      modules: [],\n    }))\n\n    expect(renderRediSearchIndexesList(instance(mockedProps))).toBeTruthy()\n\n    const expectedActions = [changeSearchMode(SearchMode.Pattern)]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.browserSearchMode,\n      SearchMode.Pattern,\n    )\n  })\n\n  it('\"loadList\" should be called after render', () => {\n    const { rerender } = renderRediSearchIndexesList(instance(mockedProps))\n\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      host: '123.23.1.1',\n      modules: [{ name: RedisDefaultModules.Search }],\n    }))\n\n    rerender(<RediSearchIndexesList {...instance(mockedProps)} />)\n\n    const expectedActions = [loadList()]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('\"onCreateIndex\" should be called after click Create Index', async () => {\n    const onCreateIndexMock = jest.fn()\n    const { findByText } = renderRediSearchIndexesList({\n      ...instance(mockedProps),\n      onCreateIndex: onCreateIndexMock,\n    })\n\n    await userEvent.click(screen.getByTestId('select-search-mode'))\n    await userEvent.click((await findByText('Create Index')) || document)\n\n    expect(onCreateIndexMock).toHaveBeenCalled()\n  })\n\n  it('\"setSelectedIndex\" and \"loadKeys\" should be called after select Index', async () => {\n    const index = stringToBuffer('idx')\n    const fetchKeysMock = jest.fn()\n\n    ;(fetchKeys as jest.Mock).mockReturnValue(fetchKeysMock)\n    ;(redisearchListSelector as jest.Mock).mockReturnValue({\n      data: [index],\n      loading: false,\n      error: '',\n      selectedIndex: null,\n    })\n\n    const { queryByText } = renderRediSearchIndexesList(instance(mockedProps))\n\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      host: '123.123.1.1',\n      modules: [{ name: RedisDefaultModules.Search }],\n    }))\n\n    await userEvent.click(screen.getByTestId('select-search-mode'))\n    await userEvent.click(queryByText(bufferToString(index)) || document)\n\n    const expectedActions = [\n      resetKeyInfo(),\n      setBrowserSelectedKey(null),\n      setSelectedIndex(index),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n\n    expect(fetchKeysMock).toHaveBeenCalled()\n  })\n\n  it('should load indexes after click on refresh', () => {\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      host: '123.23.1.1',\n      modules: [{ name: RedisDefaultModules.Search }],\n    }))\n\n    renderRediSearchIndexesList(instance(mockedProps))\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('refresh-indexes-btn'))\n\n    const expectedActions = [...afterRenderActions, loadList()]\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const Container = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  height: 36px;\n  width: 200px;\n  min-width: 80px;\n  overflow: hidden;\n  position: relative;\n  flex-shrink: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/redisearch-key-list/RediSearchIndexesList.tsx",
    "content": "import React, { useCallback, useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useLocation } from 'react-router-dom'\n\nimport {\n  setSelectedIndex,\n  redisearchSelector,\n  redisearchListSelector,\n  fetchRedisearchListAction,\n  controller as redisearchController,\n} from 'uiSrc/slices/browser/redisearch'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport {\n  changeSearchMode,\n  fetchKeys,\n  keysSelector,\n  resetKeyInfo,\n} from 'uiSrc/slices/browser/keys'\nimport { setBrowserSelectedKey } from 'uiSrc/slices/app/context'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  bufferToString,\n  formatLongName,\n  isRedisearchAvailable,\n} from 'uiSrc/utils'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem, FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { PlusIcon, ResetIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\nimport {\n  RiSelect,\n  SelectValueRenderParams,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { getIndexDisplayName } from 'uiSrc/pages/vector-search/utils'\nimport * as S from './RediSearchIndexesList.styles'\n\nexport const CREATE = JSON.stringify('create')\n\nexport interface Props {\n  onCreateIndex: (value: boolean) => void\n}\n\nconst RediSearchIndexesList = (props: Props) => {\n  const { onCreateIndex } = props\n\n  const { viewType, searchMode } = useSelector(keysSelector)\n  const { selectedIndex } = useSelector(redisearchSelector)\n  const { data: list = [], loading } = useSelector(redisearchListSelector)\n  const {\n    id: instanceId,\n    modules,\n    host: instanceHost,\n  } = useSelector(connectedInstanceSelector)\n\n  const selectedValue = selectedIndex ? bufferToString(selectedIndex) : ''\n  const featureFlags = useSelector(appFeatureFlagsFeaturesSelector)\n  const isVectorSearch =\n    featureFlags?.[FeatureFlags.vectorSearchV2]?.flag ?? false\n\n  const dispatch = useDispatch()\n  const location = useLocation()\n  const history = useHistory()\n\n  const selectIndex = useCallback(\n    (indexName: string) => {\n      const matchingBuffer = list.find(\n        (item) => bufferToString(item) === indexName,\n      )\n      if (!matchingBuffer) return false\n\n      dispatch(resetKeyInfo())\n      dispatch(setBrowserSelectedKey(null))\n      dispatch(setSelectedIndex(matchingBuffer))\n      dispatch(\n        fetchKeys({\n          searchMode,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n        }),\n      )\n\n      return true\n    },\n    [dispatch, list, searchMode, viewType],\n  )\n\n  useEffect(() => {\n    if (!instanceHost) return\n\n    const moduleExists = isRedisearchAvailable(modules)\n    if (moduleExists) {\n      dispatch(fetchRedisearchListAction())\n    } else {\n      dispatch(changeSearchMode(SearchMode.Pattern))\n\n      localStorageService.set(\n        BrowserStorageItem.browserSearchMode,\n        SearchMode.Pattern,\n      )\n    }\n  }, [instanceHost, modules])\n\n  useEffect(() => {\n    const params = new URLSearchParams(location.search)\n    const browseIndex = params.get('browseIndex')\n    if (!browseIndex || list.length === 0) return\n\n    if (selectIndex(browseIndex)) {\n      params.delete('browseIndex')\n      history.replace({ ...location, search: params.toString() })\n    }\n  }, [list])\n\n  useEffect(\n    () => () => {\n      redisearchController?.abort()\n    },\n    [],\n  )\n\n  const options = list.map((item) => {\n    const stringValue = bufferToString(item)\n    const displayValue = formatLongName(\n      getIndexDisplayName(stringValue),\n      100,\n      10,\n    )\n\n    return {\n      value: stringValue,\n      inputDisplay: (\n        <Text data-test-subj={`mode-option-type-${displayValue}`}>\n          {displayValue}\n        </Text>\n      ),\n      dropdownDisplay: (\n        <Text\n          color=\"primary\"\n          data-test-subj={`mode-option-type-${displayValue}`}\n        >\n          {displayValue}\n        </Text>\n      ),\n    }\n  })\n\n  if (isVectorSearch) {\n    options.push({\n      value: CREATE,\n      inputDisplay: <span>CREATE</span>,\n      dropdownDisplay: (\n        <Row align=\"center\" justify=\"start\" gap=\"xs\">\n          <PlusIcon size=\"M\" />\n          <Text\n            size=\"M\"\n            variant=\"semiBold\"\n            color=\"primary\"\n            data-testid=\"create-index-btn\"\n          >\n            Create Index\n          </Text>\n        </Row>\n      ),\n    })\n  }\n\n  const onChangeIndex = (value: string) => {\n    if (value === CREATE) {\n      onCreateIndex(true)\n\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_ADD_BUTTON_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          view: viewType,\n        },\n      })\n\n      return\n    }\n\n    if (!selectIndex(value)) return\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_INDEX_CHANGED,\n      eventData: {\n        databaseId: instanceId,\n        totalNumberOfIndexes: list.length,\n        view: viewType,\n      },\n    })\n  }\n\n  const handleRefresh = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    dispatch(fetchRedisearchListAction())\n  }\n\n  const selectValueRender = ({\n    option,\n    isOptionValue,\n  }: SelectValueRenderParams): JSX.Element => {\n    if (isOptionValue) {\n      return option.dropdownDisplay as JSX.Element\n    }\n    return option.inputDisplay as JSX.Element\n  }\n\n  return (\n    <S.Container>\n      <RiSelect.Compose\n        disabled={loading}\n        options={options}\n        value={selectedValue}\n        onChange={onChangeIndex}\n      >\n        <RiSelect.Trigger.Compose data-testid=\"select-search-mode\">\n          <RiSelect.Trigger.Value\n            placeholder=\"Select Index\"\n            data-testid=\"select-index-placeholder\"\n            valueRender={selectValueRender}\n          />\n          <RiSelect.Trigger.LoadingIndicator loading={loading} />\n          <RiSelect.Trigger.Arrow data-testid=\"select-index-arrow\" />\n          <div style={{ zIndex: 6 }}>\n            <RiTooltip content=\"Refresh Indexes\">\n              <IconButton\n                size=\"M\"\n                icon={ResetIcon}\n                disabled={loading}\n                onClick={handleRefresh}\n                aria-label=\"refresh indexes list\"\n                data-testid=\"refresh-indexes-btn\"\n                onPointerDown={(e) => e.stopPropagation()}\n              />\n            </RiTooltip>\n          </div>\n        </RiSelect.Trigger.Compose>\n        <RiSelect.Content optionValueRender={selectValueRender} />\n      </RiSelect.Compose>\n    </S.Container>\n  )\n}\n\nexport default RediSearchIndexesList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/redisearch-key-list/index.ts",
    "content": "import RediSearchIndexesList from './RediSearchIndexesList'\n\nexport default RediSearchIndexesList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { KeyboardKeys as keys } from 'uiSrc/constants/keys'\nimport {\n  act,\n  cleanup,\n  clearStoreActions,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  keysSelector,\n  loadKeys,\n  loadSearchHistory,\n  setFilter,\n  setPatternSearchMatch,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  setBulkDeleteFilter,\n  setBulkDeleteKeyCount,\n  setBulkDeleteSearch,\n} from 'uiSrc/slices/browser/bulkActions'\n\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { changeSidePanel } from 'uiSrc/slices/panels/sidePanels'\nimport { SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { setSelectedTab } from 'uiSrc/slices/panels/aiAssistant'\nimport { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport SearchKeyList from './SearchKeyList'\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  keysSearchHistorySelector: jest.fn().mockReturnValue({\n    data: [\n      { id: '1', mode: 'pattern', filter: { type: 'list', match: '*' } },\n      { id: '2', mode: 'pattern', filter: { type: 'list', match: '*' } },\n    ],\n  }),\n  keysSelector: jest.fn().mockReturnValue({\n    searchMode: 'Pattern',\n    filter: null,\n    search: '',\n    viewType: 'Browser',\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '',\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/redisearch'),\n  redisearchSelector: jest.fn().mockReturnValue({\n    search: '',\n    selectedIndex: null,\n  }),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    databaseChat: {\n      flag: true,\n    },\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('SearchKeyList', () => {\n  it('should render', () => {\n    expect(render(<SearchKeyList />)).toBeTruthy()\n    const searchInput = screen.getByTestId('search-key')\n    expect(searchInput).toBeInTheDocument()\n  })\n\n  it('should load history after render', () => {\n    ;(connectedInstanceSelector as jest.Mock).mockImplementationOnce(() => ({\n      id: '1',\n    }))\n\n    render(<SearchKeyList />)\n    const expectedActions = [loadSearchHistory()]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('\"setSearchMatch\" should be called after \"onChange\"', () => {\n    const searchTerm = 'a'\n\n    render(<SearchKeyList />)\n\n    fireEvent.change(screen.getByTestId('search-key'), {\n      target: { value: searchTerm },\n    })\n\n    fireEvent.keyDown(screen.getByTestId('search-key'), { key: keys.ENTER })\n\n    const expectedActions = [\n      setPatternSearchMatch(searchTerm),\n      setBulkDeleteSearch(searchTerm),\n      setBulkDeleteFilter(null),\n      setBulkDeleteKeyCount(null),\n      loadKeys(),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call proper actions after apply suggestion', async () => {\n    await act(() => {\n      render(<SearchKeyList />)\n    })\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('show-suggestions-btn'))\n    fireEvent.click(screen.getByTestId('suggestion-item-2'))\n\n    const expectedActions = [\n      setFilter('list'),\n      setPatternSearchMatch('*'),\n      setBulkDeleteSearch('*'),\n      setBulkDeleteFilter(KeyTypes.List),\n      setBulkDeleteKeyCount(null),\n      loadKeys(),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions([...afterRenderActions, ...expectedActions]),\n    )\n  })\n\n  it('\"loadKeys\" should not be called after Enter if searchMode=Redisearch and index=null', async () => {\n    const searchTerm = 'a'\n\n    ;(keysSelector as jest.Mock).mockImplementation(() => ({\n      searchMode: SearchMode.Redisearch,\n      viewType: KeyViewType.Browser,\n      isSearch: false,\n      isFiltered: false,\n    }))\n\n    render(<SearchKeyList />)\n\n    fireEvent.change(screen.getByTestId('search-key'), {\n      target: { value: searchTerm },\n    })\n\n    fireEvent.keyDown(screen.getByTestId('search-key'), { key: keys.ENTER })\n\n    const afterRenderActions = [...store.getActions()]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions([...afterRenderActions]),\n    )\n\n    fireEvent.click(screen.getByTestId('search-btn'))\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions([...afterRenderActions]),\n    )\n  })\n\n  it('should call proper actions after click on ask copilot', async () => {\n    ;(keysSelector as jest.Mock).mockImplementation(() => ({\n      searchMode: SearchMode.Redisearch,\n      viewType: KeyViewType.Browser,\n      isSearch: false,\n      isFiltered: false,\n    }))\n\n    render(<SearchKeyList />)\n\n    fireEvent.click(screen.getByTestId('ask-redis-copilot-btn'))\n\n    const expectedActions = [\n      setSelectedTab(AiChatType.Query),\n      changeSidePanel(SidePanels.AiAssistant),\n    ]\n\n    expect(clearStoreActions(store.getActions())).toEqual(\n      clearStoreActions([...expectedActions]),\n    )\n  })\n\n  it('should not render ask copilot if feature is disabled', async () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      databaseChat: {\n        flag: false,\n      },\n    })\n    ;(keysSelector as jest.Mock).mockImplementation(() => ({\n      searchMode: SearchMode.Redisearch,\n      viewType: KeyViewType.Browser,\n      isSearch: false,\n      isFiltered: false,\n    }))\n\n    render(<SearchKeyList />)\n\n    expect(\n      screen.queryByTestId('ask-redis-copilot-btn'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport MultiSearch from 'uiSrc/components/multi-search/MultiSearch'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { replaceSpaces } from 'uiSrc/utils'\nimport {\n  deleteSearchHistoryAction,\n  fetchKeys,\n  fetchSearchHistoryAction,\n  keysSearchHistorySelector,\n  keysSelector,\n  setFilter,\n  setSearchMatch,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  setBulkDeleteFilter,\n  setBulkDeleteKeyCount,\n  setBulkDeleteSearch,\n} from 'uiSrc/slices/browser/bulkActions'\nimport {\n  KeyViewType,\n  SearchHistoryItem,\n  SearchMode,\n} from 'uiSrc/slices/interfaces/keys'\nimport {\n  redisearchHistorySelector,\n  redisearchSelector,\n} from 'uiSrc/slices/browser/redisearch'\n\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { resetBrowserTree } from 'uiSrc/slices/app/context'\n\nimport { changeSidePanel } from 'uiSrc/slices/panels/sidePanels'\nimport { AiChatType } from 'uiSrc/slices/interfaces/aiAssistant'\nimport { setSelectedTab } from 'uiSrc/slices/panels/aiAssistant'\nimport { SidePanels } from 'uiSrc/slices/interfaces/insights'\n\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { FeatureFlagComponent } from 'uiSrc/components'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nconst placeholders = {\n  [SearchMode.Pattern]: 'Filter by Key Name or Pattern',\n  [SearchMode.Redisearch]: 'Search per Values of Keys',\n}\n\nconst SearchKeyList = () => {\n  const { id } = useSelector(connectedInstanceSelector)\n  const { search, filter, viewType, searchMode } = useSelector(keysSelector)\n  const { search: redisearchQuery, selectedIndex } =\n    useSelector(redisearchSelector)\n  const { data: rediSearchHistory, loading: rediSearchHistoryLoading } =\n    useSelector(redisearchHistorySelector)\n  const { data: searchHistory, loading: searchHistoryLoading } = useSelector(\n    keysSearchHistorySelector,\n  )\n\n  const [value, setValue] = useState(search || '')\n  const [disableSubmit, setDisableSubmit] = useState(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (id) {\n      dispatch(fetchSearchHistoryAction(searchMode))\n    }\n  }, [id, searchMode])\n\n  useEffect(() => {\n    setValue(searchMode === SearchMode.Pattern ? search : redisearchQuery)\n  }, [searchMode, search, redisearchQuery])\n\n  useEffect(() => {\n    setDisableSubmit(searchMode === SearchMode.Redisearch && !selectedIndex)\n  }, [searchMode, selectedIndex])\n\n  const mapOptions = (data: null | Array<SearchHistoryItem>) =>\n    data?.map((item) => ({\n      id: item.id,\n      option: item.filter?.type,\n      value: item.filter?.match,\n    })) || []\n\n  const handleApply = (\n    match = value,\n    telemetryProperties: {} = {},\n    filterOverride?: typeof filter,\n  ) => {\n    if (disableSubmit) return\n\n    const effectiveFilter =\n      filterOverride !== undefined ? filterOverride : filter\n\n    dispatch(setSearchMatch(match, searchMode))\n\n    // Sync to bulk delete state\n    dispatch(setBulkDeleteSearch(match))\n    dispatch(setBulkDeleteFilter(effectiveFilter))\n    dispatch(setBulkDeleteKeyCount(null))\n\n    if (viewType === KeyViewType.Tree) {\n      dispatch(resetBrowserTree())\n    }\n\n    dispatch(\n      fetchKeys(\n        {\n          searchMode,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n          telemetryProperties,\n        },\n        () => {\n          dispatch(fetchSearchHistoryAction(searchMode))\n        },\n      ),\n    )\n  }\n\n  const handleChangeValue = (initValue: string) => {\n    setValue(initValue)\n  }\n\n  const handleChangeOptions = () => {\n    // now only one filter, so we delete option\n    dispatch(setFilter(null))\n    handleApply(value, {}, null)\n  }\n\n  const handleApplySuggestion = (suggestion?: {\n    option: string\n    value: string\n  }) => {\n    if (!suggestion) {\n      handleApply()\n      return\n    }\n\n    dispatch(setFilter(suggestion.option))\n    setValue(suggestion.value)\n    handleApply(\n      suggestion.value,\n      { source: 'history' },\n      suggestion.option as typeof filter,\n    )\n  }\n\n  const handleDeleteSuggestions = (ids: string[]) => {\n    dispatch(deleteSearchHistoryAction(searchMode, ids))\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === keys.ENTER) {\n      handleApply()\n    }\n  }\n\n  const onClear = () => {\n    handleChangeValue('')\n    dispatch(setFilter(null))\n    handleApply('', {}, null)\n  }\n\n  const handleClickAskCopilot = () => {\n    dispatch(setSelectedTab(AiChatType.Query))\n    dispatch(changeSidePanel(SidePanels.AiAssistant))\n  }\n\n  return (\n    <div\n      className={cx(styles.container, {\n        [styles.redisearchMode]: searchMode === SearchMode.Redisearch,\n      })}\n    >\n      <MultiSearch\n        value={value}\n        onSubmit={handleApply}\n        onKeyDown={onKeyDown}\n        onChange={handleChangeValue}\n        onChangeOptions={handleChangeOptions}\n        onClear={onClear}\n        suggestions={{\n          options: mapOptions(\n            searchMode === SearchMode.Pattern\n              ? searchHistory\n              : rediSearchHistory,\n          ),\n          buttonTooltipTitle: 'Show History',\n          loading:\n            searchMode === SearchMode.Pattern\n              ? searchHistoryLoading\n              : rediSearchHistoryLoading,\n          onApply: handleApplySuggestion,\n          onDelete: handleDeleteSuggestions,\n        }}\n        appendRight={\n          searchMode === SearchMode.Redisearch ? (\n            <FeatureFlagComponent name={FeatureFlags.databaseChat}>\n              <EmptyButton\n                className={styles.askCopilotBtn}\n                size=\"small\"\n                onClick={handleClickAskCopilot}\n                data-testid=\"ask-redis-copilot-btn\"\n              >\n                <RiIcon className={styles.cloudIcon} type=\"StarsIcon\" />\n              </EmptyButton>\n            </FeatureFlagComponent>\n          ) : undefined\n        }\n        disableSubmit={disableSubmit}\n        placeholder={placeholders[searchMode]}\n        className={styles.input}\n        data-testid=\"search-key\"\n      />\n      <p className={styles.hiddenText}>{replaceSpaces(value)}</p>\n    </div>\n  )\n}\n\nexport default SearchKeyList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/search-key-list/index.ts",
    "content": "import SearchKeyList from './SearchKeyList'\n\nexport default SearchKeyList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/search-key-list/styles.module.scss",
    "content": ".container {\n  flex-grow: 1;\n  height: 36px;\n  position: relative;\n\n  :global(.euiFormControlLayout) {\n    max-width: calc(100%) !important;\n    height: 36px !important;\n  }\n}\n\n.hiddenText {\n  display: inline-block;\n  visibility: hidden;\n  height: 1px;\n  overflow: hidden;\n  font-size: 15px;\n  max-width: 100%;\n  margin-left: 40px;\n  margin-right: 50px;\n  word-break: break-all;\n}\n\n.askCopilotBtn {\n  margin-left: 4px;\n\n  .cloudIcon {\n    width: 14px;\n    height: 14px;\n\n    :global {\n      path {\n        fill: var(--triggerIconActiveColor) !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/use-key-format/index.ts",
    "content": "import useKeyFormat from './useKeyFormat'\n\nexport { useKeyFormat }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/use-key-format/useKeyFormat.spec.tsx",
    "content": "import { renderHook } from '@testing-library/react'\nimport { useSelector } from 'react-redux'\nimport { KeyValueFormat } from 'uiSrc/constants'\nimport { bufferToHex, bufferToString } from 'uiSrc/utils'\nimport useKeyFormat from './useKeyFormat'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nconst mockUseSelector = useSelector as jest.Mock\n\ndescribe('useKeyFormat hook', () => {\n  const renderUseKeyFormat = () => renderHook(() => useKeyFormat())\n\n  it('should return bufferToString as the default handler when keyNameFormat is not set', () => {\n    mockUseSelector.mockReturnValueOnce({\n      keyNameFormat: undefined,\n    })\n\n    const { result } = renderUseKeyFormat()\n\n    expect(result.current.handler).toBe(bufferToString)\n  })\n\n  it('should return bufferToString when keyNameFormat is Unicode', () => {\n    mockUseSelector.mockReturnValueOnce({\n      keyNameFormat: KeyValueFormat.Unicode,\n    })\n    const { result } = renderUseKeyFormat()\n\n    expect(result.current.handler).toBe(bufferToString)\n  })\n\n  it('should return bufferToHex when keyNameFormat is HEX', () => {\n    mockUseSelector.mockReturnValueOnce({\n      keyNameFormat: KeyValueFormat.HEX,\n    })\n\n    const { result } = renderUseKeyFormat()\n\n    expect(result.current.handler).toBe(bufferToHex)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/use-key-format/useKeyFormat.ts",
    "content": "import { useSelector } from 'react-redux'\nimport { KeyValueFormat } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { bufferToHex, bufferToString } from 'uiSrc/utils'\n\nconst encodingHandlerMap = {\n  [KeyValueFormat.Unicode]: bufferToString,\n  [KeyValueFormat.HEX]: bufferToHex,\n}\n\nconst useKeyFormat = () => {\n  const { keyNameFormat } = useSelector(connectedInstanceSelector)\n  const format = keyNameFormat || KeyValueFormat.Unicode\n  const handler = encodingHandlerMap[format]\n\n  return { handler }\n}\n\nexport default useKeyFormat\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/view-index-data-button/ViewIndexDataButton.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { faker } from '@faker-js/faker'\nimport { cleanup, render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport { Pages } from 'uiSrc/constants'\nimport { IndexSummary } from 'uiSrc/slices/interfaces/redisearch'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { SearchBrowserSource } from 'uiSrc/pages/vector-search/telemetry.constants'\n\nimport { ViewIndexDataButton } from './ViewIndexDataButton'\nimport { ViewIndexDataButtonProps } from './ViewIndexDataButton.types'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockPush = jest.fn()\nconst mockInstanceId = faker.string.uuid()\n\nconst buildIndex = (overrides?: Partial<IndexSummary>): IndexSummary => ({\n  name: faker.string.alpha(10),\n  prefixes: [faker.string.alpha(5)],\n  keyType: 'HASH',\n  ...overrides,\n})\n\nconst defaultProps: ViewIndexDataButtonProps = {\n  indexes: [],\n  instanceId: mockInstanceId,\n}\n\nconst renderComponent = (propsOverride?: Partial<ViewIndexDataButtonProps>) => {\n  const props = { ...defaultProps, ...propsOverride }\n  return render(<ViewIndexDataButton {...props} />)\n}\n\ndescribe('ViewIndexDataButton', () => {\n  beforeEach(() => {\n    cleanup()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: mockPush })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  describe('0 indexes', () => {\n    it('should render nothing', () => {\n      const { container } = renderComponent({ indexes: [] })\n\n      expect(container.innerHTML).toBe('')\n    })\n  })\n\n  describe('1 index (direct button)', () => {\n    it('should render a single text button with \"View index\" label', () => {\n      const index = buildIndex({ name: 'movies_index' })\n      renderComponent({ indexes: [index] })\n\n      const btn = screen.getByTestId('view-index-data-btn')\n      expect(btn).toBeInTheDocument()\n      expect(btn).toHaveTextContent('View index')\n      expect(btn).not.toBeDisabled()\n    })\n\n    it('should navigate to the index query page on click', async () => {\n      const index = buildIndex({ name: 'movies_index' })\n      renderComponent({ indexes: [index] })\n\n      await userEvent.click(screen.getByTestId('view-index-data-btn'))\n\n      expect(mockPush).toHaveBeenCalledWith(\n        Pages.vectorSearchQuery(\n          mockInstanceId,\n          encodeURIComponent('movies_index'),\n        ),\n      )\n    })\n\n    it('should send SEARCH_VIEW_INDEX_CLICKED telemetry on click', async () => {\n      const index = buildIndex({ name: 'movies_index' })\n      renderComponent({ indexes: [index] })\n\n      const viewIndexDataBtn = screen.getByTestId('view-index-data-btn')\n      await userEvent.click(viewIndexDataBtn)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_VIEW_INDEX_CLICKED,\n        eventData: {\n          databaseId: mockInstanceId,\n          numberOfIndexes: 1,\n          source: SearchBrowserSource.KeyDetails,\n        },\n      })\n    })\n\n    it('should call onNavigate callback instead of history.push when provided', async () => {\n      const index = buildIndex({ name: 'movies_index' })\n      const onNavigate = jest.fn()\n      renderComponent({ indexes: [index], onNavigate })\n\n      await userEvent.click(screen.getByTestId('view-index-data-btn'))\n\n      expect(onNavigate).toHaveBeenCalledWith('movies_index')\n      expect(mockPush).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('2+ indexes (dropdown menu)', () => {\n    const indexes = [\n      buildIndex({ name: 'products_index' }),\n      buildIndex({ name: 'users_index' }),\n      buildIndex({ name: 'id_index' }),\n    ]\n\n    it('should render menu trigger with \"View index\" label and count badge', () => {\n      renderComponent({ indexes })\n\n      const trigger = screen.getByTestId('view-index-data-menu-trigger')\n      expect(trigger).toBeInTheDocument()\n      expect(trigger).toHaveTextContent('View index')\n\n      const badge = screen.getByTestId('view-index-data-count-badge')\n      expect(badge).toHaveTextContent('3')\n    })\n\n    it('should show menu items when trigger is clicked', async () => {\n      renderComponent({ indexes })\n\n      await userEvent.click(screen.getByTestId('view-index-data-menu-trigger'))\n\n      for (const index of indexes) {\n        expect(\n          screen.getByTestId(`view-index-data-item-${index.name}`),\n        ).toBeInTheDocument()\n      }\n    })\n\n    it('should navigate to the correct index when a menu item is clicked', async () => {\n      renderComponent({ indexes })\n\n      await userEvent.click(screen.getByTestId('view-index-data-menu-trigger'))\n      await userEvent.click(\n        screen.getByTestId('view-index-data-item-users_index'),\n      )\n\n      expect(mockPush).toHaveBeenCalledWith(\n        Pages.vectorSearchQuery(\n          mockInstanceId,\n          encodeURIComponent('users_index'),\n        ),\n      )\n    })\n\n    it('should send SEARCH_VIEW_INDEX_CLICKED telemetry with correct count when menu item is clicked', async () => {\n      renderComponent({ indexes })\n\n      const menuTrigger = screen.getByTestId('view-index-data-menu-trigger')\n      await userEvent.click(menuTrigger)\n\n      const menuItem = screen.getByTestId('view-index-data-item-users_index')\n      await userEvent.click(menuItem)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_VIEW_INDEX_CLICKED,\n        eventData: {\n          databaseId: mockInstanceId,\n          numberOfIndexes: 3,\n          source: SearchBrowserSource.KeyDetails,\n        },\n      })\n    })\n\n    it('should call onNavigate callback for menu items when provided', async () => {\n      const onNavigate = jest.fn()\n      renderComponent({ indexes, onNavigate })\n\n      await userEvent.click(screen.getByTestId('view-index-data-menu-trigger'))\n      await userEvent.click(\n        screen.getByTestId('view-index-data-item-products_index'),\n      )\n\n      expect(onNavigate).toHaveBeenCalledWith('products_index')\n      expect(mockPush).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/view-index-data-button/ViewIndexDataButton.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport { ViewIndexDataButton } from './ViewIndexDataButton'\n\nconst meta: Meta<typeof ViewIndexDataButton> = {\n  component: ViewIndexDataButton,\n  tags: ['autodocs'],\n  parameters: {\n    layout: 'centered',\n    docs: {\n      description: {\n        component:\n          'Navigates from a key to its search index query page. Adapts UI based on index count: disabled placeholder (0), text button (1), or dropdown menu (2+).',\n      },\n    },\n  },\n  args: {\n    instanceId: 'test-instance',\n    onNavigate: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const NoIndexes: Story = {\n  args: {\n    indexes: [],\n  },\n  parameters: {\n    docs: {\n      description: {\n        story:\n          'Key belongs to no indexes. Shows a disabled placeholder button.',\n      },\n    },\n  },\n}\n\nexport const SingleIndex: Story = {\n  args: {\n    indexes: [\n      { name: 'products_index', prefixes: ['product:'], keyType: 'HASH' },\n    ],\n  },\n  parameters: {\n    docs: {\n      description: {\n        story:\n          'Key belongs to one index. Shows a text button that navigates directly.',\n      },\n    },\n  },\n}\n\nexport const MultipleIndexes: Story = {\n  args: {\n    indexes: [\n      { name: 'products_index', prefixes: ['product:'], keyType: 'HASH' },\n      { name: 'users_index', prefixes: ['user:'], keyType: 'HASH' },\n      { name: 'id_index', prefixes: ['id:'], keyType: 'JSON' },\n    ],\n  },\n  parameters: {\n    docs: {\n      description: {\n        story:\n          'Key belongs to multiple indexes. Shows a dropdown with count badge and chevron.',\n      },\n    },\n  },\n}\n\nexport const ManyIndexes: Story = {\n  args: {\n    indexes: Array.from({ length: 12 }, (_, i) => ({\n      name: `index_${i + 1}`,\n      prefixes: [`prefix${i + 1}:`],\n      keyType: 'HASH',\n    })),\n  },\n  parameters: {\n    docs: {\n      description: {\n        story: 'Key belongs to many indexes. Badge shows double-digit count.',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/view-index-data-button/ViewIndexDataButton.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nexport const CountBadge = styled(Col)`\n  min-width: ${({ theme }) => theme.core.space.space250};\n  height: ${({ theme }) => theme.core.space.space250};\n  padding: ${({ theme }) =>\n    `${theme.core.space.space010} ${theme.core.space.space050}`};\n  border-radius: ${({ theme }) => theme.core.space.space800};\n  overflow: clip;\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.secondary200};\n  line-height: normal;\n  white-space: nowrap;\n`\n\nexport const CountBadgeText = styled(ColorText)`\n  color: ${({ theme }) => theme.semantic.color.text.secondary700};\n  line-height: normal;\n  white-space: nowrap;\n  text-align: center;\n  font-variant-numeric: tabular-nums;\n  font-feature-settings: 'tnum' on;\n`\n\nexport const TriggerRow = styled(Row)`\n  cursor: pointer;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/view-index-data-button/ViewIndexDataButton.tsx",
    "content": "import React, { useCallback } from 'react'\nimport { useHistory } from 'react-router-dom'\n\nimport { Pages } from 'uiSrc/constants'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { ChevronDownIcon } from 'uiSrc/components/base/icons'\nimport {\n  Menu,\n  MenuTrigger,\n  MenuContent,\n  MenuItem,\n} from 'uiSrc/components/base/layout/menu'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { SearchBrowserSource } from 'uiSrc/pages/vector-search/telemetry.constants'\n\nimport { ViewIndexDataButtonProps } from './ViewIndexDataButton.types'\nimport * as S from './ViewIndexDataButton.styles'\n\nconst VIEW_INDEX_LABEL = 'View index'\n\nexport const ViewIndexDataButton = ({\n  indexes,\n  instanceId,\n  onNavigate,\n}: ViewIndexDataButtonProps) => {\n  const history = useHistory()\n\n  const navigateTo = useCallback(\n    (indexName: string) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_VIEW_INDEX_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          numberOfIndexes: indexes.length,\n          source: SearchBrowserSource.KeyDetails,\n        },\n      })\n      if (onNavigate) {\n        onNavigate(indexName)\n        return\n      }\n      history.push(\n        Pages.vectorSearchQuery(instanceId, encodeURIComponent(indexName)),\n      )\n    },\n    [history, instanceId, onNavigate, indexes.length],\n  )\n\n  if (indexes.length === 0) {\n    return null\n  }\n\n  if (indexes.length === 1) {\n    return (\n      <EmptyButton\n        size=\"small\"\n        onClick={() => navigateTo(indexes[0].name)}\n        data-testid=\"view-index-data-btn\"\n      >\n        {VIEW_INDEX_LABEL}\n      </EmptyButton>\n    )\n  }\n\n  return (\n    <Menu data-testid=\"view-index-data-menu\">\n      <MenuTrigger>\n        <EmptyButton size=\"small\" data-testid=\"view-index-data-menu-trigger\">\n          <S.TriggerRow gap=\"s\" align=\"center\">\n            {VIEW_INDEX_LABEL}\n            <S.CountBadge\n              grow={false}\n              centered\n              data-testid=\"view-index-data-count-badge\"\n            >\n              <S.CountBadgeText size=\"s\" color=\"primary\" variant=\"semiBold\">\n                {indexes.length}\n              </S.CountBadgeText>\n            </S.CountBadge>\n            <ChevronDownIcon size=\"S\" />\n          </S.TriggerRow>\n        </EmptyButton>\n      </MenuTrigger>\n      <MenuContent placement=\"bottom\" align=\"end\">\n        {indexes.map((index) => (\n          <MenuItem\n            key={index.name}\n            text={index.name}\n            onClick={() => navigateTo(index.name)}\n            data-testid={`view-index-data-item-${index.name}`}\n          />\n        ))}\n      </MenuContent>\n    </Menu>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/view-index-data-button/ViewIndexDataButton.types.ts",
    "content": "import { IndexSummary } from 'uiSrc/slices/interfaces/redisearch'\n\nexport interface ViewIndexDataButtonProps {\n  indexes: IndexSummary[]\n  instanceId: string\n  onNavigate?: (indexName: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/view-index-data-button/index.ts",
    "content": "export { ViewIndexDataButton } from './ViewIndexDataButton'\nexport type { ViewIndexDataButtonProps } from './ViewIndexDataButton.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.spec.tsx",
    "content": "import React from 'react'\nimport { mock, instance } from 'ts-mockito'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport { MakeSearchableModalProvider } from 'uiSrc/pages/browser/components/make-searchable-modal'\nimport VirtualTree, { Props } from './VirtualTree'\n\nconst mockedProps = mock<Props>()\n\nconst mockedItems = [\n  {\n    name: 'test',\n    type: 'hash',\n    ttl: 2147474450,\n    size: 3041,\n  },\n]\n\nexport const mockVirtualTreeResult = [\n  {\n    children: [\n      {\n        children: [],\n        fullName: 'car:110:',\n        id: '0.snc1rc3zwgo',\n        keyApproximate: 0.01,\n        keyCount: 1,\n        name: '110',\n      },\n    ],\n    fullName: 'car:',\n    id: '0.sz1ie1koqi8',\n    keyApproximate: 47.18,\n    keyCount: 4718,\n    name: 'car',\n  },\n  {\n    children: [],\n    fullName: 'test',\n    id: '0.snc1rc3zwg1o',\n    keyApproximate: 0.01,\n    keyCount: 1,\n    name: 'test',\n  },\n]\n\njest.mock('uiSrc/services', () => ({\n  __esModule: true,\n  ...jest.requireActual('uiSrc/services'),\n  useDisposableWebworker: () => ({\n    result: mockVirtualTreeResult,\n    run: jest.fn(),\n  }),\n}))\n\ndescribe('VirtualTree', () => {\n  it('should render with empty nodes', () => {\n    expect(render(<VirtualTree {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render Spinner with empty nodes and loading', () => {\n    const mockFn = jest.fn()\n    const { queryByTestId } = render(\n      <VirtualTree\n        {...instance(mockedProps)}\n        loading\n        setConstructingTree={mockFn}\n      />,\n    )\n\n    expect(queryByTestId('virtual-tree-spinner')).toBeInTheDocument()\n  })\n\n  it('should render items', async () => {\n    const mockFn = jest.fn()\n    const { queryByTestId } = render(\n      <MakeSearchableModalProvider>\n        <VirtualTree\n          {...instance(mockedProps)}\n          items={mockedItems}\n          setConstructingTree={mockFn}\n        />\n      </MakeSearchableModalProvider>,\n    )\n\n    expect(queryByTestId('node-item_test')).toBeInTheDocument()\n  })\n\n  it('should not call onStatusOpen if more than one folder is exist', () => {\n    const mockFn = jest.fn()\n    const mockOnStatusOpen = jest.fn()\n\n    render(\n      <VirtualTree\n        {...instance(mockedProps)}\n        onStatusOpen={mockOnStatusOpen}\n        setConstructingTree={mockFn}\n      />,\n    )\n\n    expect(mockOnStatusOpen).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx",
    "content": "import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport AutoSizer from 'react-virtualized-auto-sizer'\nimport { debounce, get, set } from 'lodash'\nimport { TreeWalker, TreeWalkerValue, FixedSizeTree as Tree } from 'react-vtree'\nimport { useDispatch } from 'react-redux'\n\nimport { bufferToString, Nullable, stringToBuffer } from 'uiSrc/utils'\nimport { useDisposableWebworker } from 'uiSrc/services'\nimport { DEFAULT_TREE_SORTING, KeyTypes } from 'uiSrc/constants'\nimport { RedisString } from 'uiSrc/slices/interfaces'\nimport {\n  fetchKeysMetadataTree,\n  fetchNamespaceSearchable,\n} from 'uiSrc/slices/browser/keys'\nimport { NamespaceSearchableResult } from 'uiSrc/slices/interfaces/keys'\nimport {\n  Loader,\n  ProgressBarLoader,\n  RiImage,\n} from 'uiSrc/components/base/display'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { GetKeyInfoResponse } from 'apiSrc/modules/browser/keys/dto'\n\nimport { Node } from './components/Node'\nimport {\n  NodeMeta,\n  TreeData,\n  TreeNode,\n  VirtualTreeProps,\n} from './VirtualTree.types'\n\nimport styles from './styles.module.scss'\n\nexport const KEYS = 'keys'\n\nconst VirtualTree = (props: VirtualTreeProps) => {\n  const {\n    items,\n    delimiterPattern,\n    delimiters,\n    loadingIcon = 'empty',\n    statusOpen = {},\n    statusSelected,\n    loading,\n    deleting,\n    sorting = DEFAULT_TREE_SORTING,\n    commonFilterType,\n    onStatusOpen,\n    onStatusSelected,\n    setConstructingTree,\n    webworkerFn = () => {},\n    onDeleteClicked,\n    onDeleteLeaf,\n    onDeleteFolder,\n    visibleColumns,\n    showFolderMetadata,\n    showDeleteAction,\n    showSelectedIndicator,\n  } = props\n\n  const [rerenderState, rerender] = useState({})\n  const controller = useRef<Nullable<AbortController>>(null)\n  const elements = useRef<any>({})\n  const searchableElements = useRef<Record<string, string>>({})\n  const nodes = useRef<TreeNode[]>([])\n\n  const { result, run: runWebworker } = useDisposableWebworker(webworkerFn)\n\n  const dispatch = useDispatch()\n\n  useEffect(\n    () => () => {\n      nodes.current = []\n      elements.current = {}\n      searchableElements.current = {}\n    },\n    [],\n  )\n\n  // receive result from the \"runWebworker\"\n  useEffect(() => {\n    if (!result) {\n      return\n    }\n\n    elements.current = {}\n    nodes.current = result\n    rerender({})\n    setConstructingTree?.(false)\n\n    openSingleFolderNode(nodes.current)\n  }, [result])\n\n  useEffect(() => {\n    if (!items?.length) {\n      nodes.current = []\n      elements.current = {}\n      rerender({})\n      runWebworker?.({ items: [], delimiterPattern, delimiters, sorting })\n      return\n    }\n\n    setConstructingTree(true)\n    runWebworker?.({ items, delimiterPattern, delimiters, sorting })\n  }, [items, delimiterPattern])\n\n  const handleUpdateSelected = useCallback(\n    (name: RedisString) => {\n      onStatusSelected?.(name)\n    },\n    [onStatusSelected],\n  )\n\n  const handleUpdateOpen = useCallback(\n    (fullName: string, value: boolean) => {\n      onStatusOpen?.(fullName, value)\n    },\n    [onStatusOpen, nodes],\n  )\n\n  const updateNodeByPath = (path: string, data: any) => {\n    const paths = path.replaceAll('.', '.children.')\n\n    const node = get(nodes.current, paths)\n    const fullData = { ...node, ...data }\n\n    if (node) {\n      set(nodes.current, paths, fullData)\n    }\n  }\n\n  const formatItem = useCallback(\n    (item: GetKeyInfoResponse) => ({\n      ...item,\n      nameString: bufferToString(item.name as string),\n    }),\n    [],\n  )\n\n  const getMetadata = useCallback(\n    (itemsInit: any[] = [], filter: Nullable<KeyTypes>): void => {\n      dispatch(\n        fetchKeysMetadataTree(\n          itemsInit,\n          filter,\n          controller.current?.signal,\n          (loadedItems) => onSuccessFetchedMetadata(loadedItems),\n          () => {\n            rerender({})\n          },\n        ),\n      )\n    },\n    [],\n  )\n\n  const onSuccessFetchedMetadata = (loadedItems: any[]) => {\n    const items = loadedItems.map(formatItem)\n\n    items.forEach((item: any) => updateNodeByPath(item.path, item))\n\n    rerender({})\n  }\n\n  const getMetadataDebounced = debounce((filter: Nullable<KeyTypes>) => {\n    const entries = Object.entries(elements.current)\n\n    getMetadata(entries, filter)\n\n    elements.current = {}\n  }, 100)\n\n  const getMetadataNode = useCallback(\n    (nameBuffer: any, path: string) => {\n      elements.current[path] = nameBuffer\n      getMetadataDebounced(commonFilterType)\n    },\n    [commonFilterType],\n  )\n\n  const onSuccessFetchedSearchable = (results: NamespaceSearchableResult[]) => {\n    results.forEach((item) => {\n      if (!item.path) return\n      const update: Record<string, any> = { searchableChecked: true }\n      if (item.key) {\n        update.firstSearchableKey = {\n          nameBuffer: stringToBuffer(item.key.name),\n          nameString: item.key.name,\n          type: item.key.type,\n        }\n      }\n      updateNodeByPath(item.path, update)\n    })\n    rerender({})\n  }\n\n  const getSearchable = useCallback((entries: [string, string][]): void => {\n    dispatch(\n      fetchNamespaceSearchable(entries, controller.current?.signal, (results) =>\n        onSuccessFetchedSearchable(results),\n      ),\n    )\n  }, [])\n\n  const getSearchableDebounced = useMemo(\n    () =>\n      debounce(() => {\n        const entries = Object.entries(searchableElements.current)\n        if (entries.length === 0) return\n\n        getSearchable(entries)\n        searchableElements.current = {}\n      }, 100),\n    [getSearchable],\n  )\n\n  const checkSearchable = useCallback(\n    (prefix: string, path: string) => {\n      searchableElements.current[path] = prefix\n      getSearchableDebounced()\n    },\n    [getSearchableDebounced],\n  )\n\n  // This helper function constructs the object that will be sent back at the step\n  // [2] during the treeWalker function work. Except for the mandatory `data`\n  // field you can put any additional data here.\n  const getNodeData = (\n    node: TreeNode,\n    nestingLevel: number,\n  ): TreeWalkerValue<TreeData, NodeMeta> => {\n    return {\n      data: {\n        id: node.id.toString(),\n        isLeaf: node.isLeaf,\n        keyCount: node.keyCount,\n        name: node.name,\n        nameString: node.nameString,\n        nameBuffer: node.nameBuffer,\n        ttl: node.ttl,\n        size: node.size,\n        type: node.type,\n        fullName: node.fullName,\n        shortName: node.nameString\n          ?.split(new RegExp(delimiterPattern, 'g'))\n          .pop(),\n        delimiters,\n        nestingLevel,\n        deleting,\n        path: node.path,\n        getMetadata: getMetadataNode,\n        onDeleteClicked,\n        updateStatusSelected: handleUpdateSelected,\n        updateStatusOpen: handleUpdateOpen,\n        onDelete: onDeleteLeaf,\n        onDeleteFolder,\n        keyApproximate: node.keyApproximate,\n        hasSearchableKeys: !!node.firstSearchableKey,\n        firstSearchableKey: node.firstSearchableKey,\n        checkSearchable:\n          !node.isLeaf && !node.searchableChecked ? checkSearchable : undefined,\n        isSelected: !!node.isLeaf && statusSelected === node?.nameString,\n        isOpenByDefault: statusOpen[node.fullName],\n        visibleColumns,\n        showFolderMetadata,\n        showDeleteAction,\n        showSelectedIndicator,\n      },\n      nestingLevel,\n      node,\n    }\n  }\n\n  const openSingleFolderNode = useCallback(\n    (treeNodes?: TreeNode[]) => {\n      let nodes = treeNodes\n      while (nodes?.length === 1) {\n        const singleNode = nodes[0]\n        onStatusOpen?.(singleNode.fullName, true)\n        nodes = singleNode.children\n      }\n    },\n    [onStatusOpen],\n  )\n\n  // The `treeWalker` function runs only on tree re-build which is performed\n  // whenever the `treeWalker` prop is changed.\n  const treeWalker = useCallback(\n    function* treeWalker(): ReturnType<TreeWalker<TreeData, NodeMeta>> {\n      // Step [1]: Define the root multiple nodes of our tree\n      for (let i = 0; i < nodes.current.length; i++) {\n        yield getNodeData(nodes.current[i], 0)\n      }\n\n      // Step [2]: Get the parent component back. It will be the object\n      // the `getNodeData` function constructed, so you can read any data from it.\n      while (true) {\n        const parentMeta = yield\n\n        for (let i = 0; i < parentMeta.node.children?.length; i++) {\n          // Step [3]: Yielding all the children of the provided component. Then we\n          // will return for the step [2] with the first children.\n          yield getNodeData(\n            parentMeta.node.children[i],\n            parentMeta.nestingLevel + 1,\n          )\n        }\n      }\n    },\n    [statusSelected, statusOpen, rerenderState, visibleColumns],\n  )\n\n  return (\n    <AutoSizer>\n      {({ height, width }) => (\n        <div data-testid=\"virtual-tree\" style={{ position: 'relative' }}>\n          {nodes.current.length > 0 && (\n            <>\n              {loading && (\n                <ProgressBarLoader\n                  color=\"primary\"\n                  data-testid=\"progress-key-tree\"\n                  absolute\n                  style={{ width, zIndex: 1 }}\n                />\n              )}\n              <Tree\n                async\n                height={height}\n                width={width}\n                itemSize={42}\n                treeWalker={treeWalker}\n                className={styles.customScroll}\n              >\n                {Node}\n              </Tree>\n            </>\n          )}\n          {nodes.current.length === 0 && loading && (\n            <div\n              className={styles.loadingContainer}\n              style={{ width, height }}\n              data-testid=\"virtual-tree-spinner\"\n            >\n              <div className={styles.loadingBody}>\n                <Loader size=\"xl\" className={styles.loadingSpinner} />\n                {loadingIcon ? (\n                  <RiImage\n                    className={styles.loadingIcon}\n                    src={loadingIcon}\n                    alt=\"loading\"\n                  />\n                ) : (\n                  <RiIcon\n                    type=\"LoaderLargeIcon\"\n                    className={styles.loadingIcon}\n                  />\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      )}\n    </AutoSizer>\n  )\n}\n\nexport default VirtualTree\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.types.ts",
    "content": "import { FixedSizeNodeData } from 'react-vtree'\nimport {\n  BrowserColumns,\n  KeyTypes,\n  ModulesKeyTypes,\n  SortOrder,\n} from 'uiSrc/constants'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\n\nexport interface TreeNode {\n  children: TreeNode[]\n  id: number\n  keyCount: number\n  keyApproximate: number\n  fullName: string\n  name: string\n  keys: any[]\n  [key: string]: any\n}\n\nexport interface NodeMeta {\n  nestingLevel: number\n  node: TreeNode\n  data: NodeMetaData\n}\n\nexport interface NodeMetaData {\n  id: string\n  isLeaf: boolean\n  keyCount: number\n  name: string\n  fullName: string\n  updateStatusSelected: (fullName: string, keys: any) => void\n  updateStatusOpen: (name: string, value: boolean) => void\n  keyApproximate: number\n  isSelected: boolean\n  isOpenByDefault: boolean\n}\n\nexport interface FirstSearchableKey {\n  nameBuffer: RedisResponseBuffer\n  nameString: string\n  type: KeyTypes\n}\n\nexport interface TreeData extends FixedSizeNodeData {\n  isLeaf: boolean\n  name: string\n  nameString: string\n  nameBuffer: RedisResponseBuffer\n  path: string\n  keyCount: number\n  keyApproximate: number\n  fullName: string\n  shortName?: string\n  type: KeyTypes | ModulesKeyTypes\n  ttl: number\n  size: number\n  nestingLevel: number\n  deleting: boolean\n  isSelected: boolean\n  delimiters: string[]\n  children?: TreeData[]\n  hasSearchableKeys?: boolean\n  firstSearchableKey?: FirstSearchableKey\n  checkSearchable?: (prefix: string, path: string) => void\n  updateStatusOpen: (fullName: string, value: boolean) => void\n  updateStatusSelected: (key: RedisString) => void\n  getMetadata: (key: RedisString, path: string) => void\n  onDelete: (key: RedisResponseBuffer) => void\n  onDeleteClicked: (type: KeyTypes | ModulesKeyTypes) => void\n  onDeleteFolder?: (pattern: string, fullName: string, keyCount: number) => void\n  visibleColumns?: BrowserColumns[]\n  showFolderMetadata?: boolean\n  showDeleteAction?: boolean\n  showSelectedIndicator?: boolean\n}\n\nexport interface OpenedNodes {\n  [key: string]: boolean\n}\n\nexport interface VirtualTreeProps {\n  items: IKeyPropTypes[]\n  delimiterPattern: string\n  delimiters: string[]\n  loadingIcon?: string\n  loading: boolean\n  deleting: boolean\n  sorting: Maybe<SortOrder>\n  commonFilterType: Nullable<KeyTypes>\n  statusSelected: Nullable<string>\n  statusOpen: OpenedNodes\n  webworkerFn: (...args: any) => any\n  onStatusOpen?: (name: string, value: boolean) => void\n  onStatusSelected?: (key: RedisString) => void\n  setConstructingTree: (status: boolean) => void\n  onDeleteLeaf: (key: RedisResponseBuffer) => void\n  onDeleteClicked: (type: KeyTypes | ModulesKeyTypes) => void\n  onDeleteFolder?: (pattern: string, fullName: string, keyCount: number) => void\n  visibleColumns?: BrowserColumns[]\n  showFolderMetadata?: boolean\n  showDeleteAction?: boolean\n  showSelectedIndicator?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.spec.tsx",
    "content": "import React from 'react'\nimport { NodePublicState } from 'react-vtree/dist/es/Tree'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport { faker } from '@faker-js/faker'\nimport {\n  cleanup,\n  mockedStore,\n  mockFeatureFlags,\n  render,\n  screen,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { FeatureFlags, KeyTypes, BrowserColumns, Pages } from 'uiSrc/constants'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { CreateIndexMode } from 'uiSrc/pages/vector-search/pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\nimport { MakeSearchableModalProvider } from 'uiSrc/pages/browser/components/make-searchable-modal'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { SearchBrowserSource } from 'uiSrc/pages/vector-search/telemetry.constants'\nimport Node from './Node'\nimport { TreeData } from '../../VirtualTree.types'\nimport { mockVirtualTreeResult } from '../../VirtualTree.spec'\n\nconst mockPush = jest.fn()\nconst mockInstanceId = faker.string.uuid()\n\nconst mockDataFullName = 'test'\nconst mockedProps = mock<NodePublicState<TreeData>>()\nconst mockedPropsData = mock<TreeData>()\n\nconst mockedData: TreeData = {\n  ...instance(mockedPropsData),\n  nestingLevel: 3,\n  isLeaf: true,\n  path: '0.0.5.6',\n  fullName: mockDataFullName,\n  nameString: mockDataFullName,\n  nameBuffer: stringToBuffer(mockDataFullName),\n}\n\nconst mockedDataWithMetadata = {\n  ...mockedData,\n  type: KeyTypes.Hash,\n  ttl: 123,\n  size: 123,\n}\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  useDisposableWebworker: () => ({\n    result: mockVirtualTreeResult,\n    run: jest.fn(),\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  const state = store.getState()\n  state.connections.instances.connectedInstance.id = mockInstanceId\n  reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: mockPush })\n})\n\nafterEach(() => {\n  jest.clearAllMocks()\n})\n\nconst renderNode = (\n  props: Partial<NodePublicState<TreeData>> = {},\n  options?: { store?: any },\n) => {\n  const mergedProps = { ...instance(mockedProps), ...props }\n  return render(\n    <MakeSearchableModalProvider>\n      <Node {...mergedProps} />\n    </MakeSearchableModalProvider>,\n    { store: options?.store ?? store },\n  )\n}\n\ndescribe('Node', () => {\n  it('should render', () => {\n    expect(renderNode({ data: mockedData })).toBeTruthy()\n  })\n\n  it('should render arrow and folder icons for Node properly', () => {\n    const mockData: TreeData = {\n      ...mockedData,\n      isLeaf: false,\n      fullName: mockDataFullName,\n    }\n\n    const { container } = renderNode({ data: mockData })\n\n    expect(\n      container.querySelector(\n        `[data-test-subj=\"node-arrow-icon_${mockDataFullName}\"`,\n      ),\n    ).toBeInTheDocument()\n    expect(\n      container.querySelector(\n        `[data-test-subj=\"node-folder-icon_${mockDataFullName}\"`,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('\"setItems\", \"updateStatusSelected\", \"mockGetMetadata\" should be called after click on Leaf', () => {\n    const mockUpdateStatusSelected = jest.fn()\n    const mockUpdateStatusOpen = jest.fn()\n    const mockSetOpen = jest.fn()\n    const mockGetMetadata = jest.fn()\n\n    const mockData: TreeData = {\n      ...mockedData,\n      updateStatusSelected: mockUpdateStatusSelected,\n      updateStatusOpen: mockUpdateStatusOpen,\n      getMetadata: mockGetMetadata,\n    }\n\n    renderNode({ setOpen: mockSetOpen, isOpen: false, data: mockData })\n\n    screen.getByTestId(`node-item_${mockDataFullName}`).click()\n\n    expect(mockUpdateStatusSelected).toBeCalledWith(mockData.nameBuffer)\n    expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true)\n    expect(mockGetMetadata).toBeCalledWith(mockData.nameBuffer, mockData.path)\n    expect(mockSetOpen).not.toBeCalled()\n  })\n\n  it('\"mockGetMetadata\" not be call if size and ttl exists', () => {\n    const mockUpdateStatusSelected = jest.fn()\n    const mockUpdateStatusOpen = jest.fn()\n    const mockSetOpen = jest.fn()\n    const mockGetMetadata = jest.fn()\n\n    const mockData: TreeData = {\n      ...mockedDataWithMetadata,\n      updateStatusSelected: mockUpdateStatusSelected,\n      updateStatusOpen: mockUpdateStatusOpen,\n      getMetadata: mockGetMetadata,\n    }\n\n    renderNode({ setOpen: mockSetOpen, isOpen: false, data: mockData })\n\n    screen.getByTestId(`node-item_${mockDataFullName}`).click()\n\n    expect(mockUpdateStatusSelected).toBeCalledWith(mockData.nameBuffer)\n    expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true)\n    expect(mockGetMetadata).not.toBeCalled()\n    expect(mockSetOpen).not.toBeCalled()\n  })\n\n  it('name, ttl and size should be rendered', () => {\n    const { getByTestId } = renderNode({ data: mockedDataWithMetadata })\n\n    expect(getByTestId(`node-item_${mockDataFullName}`)).toBeInTheDocument()\n    expect(\n      getByTestId(`badge-${mockedDataWithMetadata.type}_${mockDataFullName}`),\n    ).toBeInTheDocument()\n    expect(getByTestId(`ttl-${mockDataFullName}`)).toBeInTheDocument()\n    expect(getByTestId(`size-${mockDataFullName}`)).toBeInTheDocument()\n  })\n\n  it('\"updateStatusOpen\", \"setOpen\" should be called after click on Node', () => {\n    const mockUpdateStatusSelected = jest.fn()\n    const mockUpdateStatusOpen = jest.fn()\n    const mockSetOpen = jest.fn()\n    const mockIsOpen = false\n\n    const mockData: TreeData = {\n      ...mockedData,\n      isLeaf: mockIsOpen,\n      fullName: mockDataFullName,\n      updateStatusSelected: mockUpdateStatusSelected,\n      updateStatusOpen: mockUpdateStatusOpen,\n    }\n\n    renderNode({ isOpen: false, setOpen: mockSetOpen, data: mockData })\n\n    screen.getByTestId(`node-item_${mockDataFullName}`).click()\n\n    expect(mockUpdateStatusSelected).not.toBeCalled()\n    expect(mockUpdateStatusOpen).toHaveBeenCalledWith(\n      mockDataFullName,\n      !mockIsOpen,\n    )\n    expect(mockSetOpen).toBeCalledWith(!mockIsOpen)\n  })\n\n  describe('Folder delete', () => {\n    it('should render folder delete button for non-leaf nodes', () => {\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder',\n        keyCount: 100,\n        delimiters: [':'],\n        onDeleteFolder: jest.fn(),\n      }\n\n      renderNode({ data: mockData })\n\n      expect(screen.getByTestId('delete-folder-btn-folder')).toBeInTheDocument()\n    })\n\n    it('should call onDeleteFolder with correct params when folder delete button is clicked', () => {\n      const mockOnDeleteFolder = jest.fn()\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'user:session',\n        keyCount: 42,\n        delimiters: [':'],\n        onDeleteFolder: mockOnDeleteFolder,\n      }\n\n      renderNode({ data: mockData })\n\n      screen.getByTestId('delete-folder-btn-user:session').click()\n\n      expect(mockOnDeleteFolder).toHaveBeenCalledWith(\n        'user:session:*',\n        'user:session',\n        42,\n      )\n    })\n\n    it('should disable folder delete when folder has unprintable characters', () => {\n      const mockOnDeleteFolder = jest.fn()\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder\\uFFFD',\n        nameString: 'folder\\uFFFD',\n        keyCount: 100,\n        delimiters: [':'],\n        onDeleteFolder: mockOnDeleteFolder,\n      }\n\n      renderNode({ data: mockData })\n\n      const deleteBtn = screen.getByTestId('delete-folder-btn-folder\\uFFFD')\n      expect(deleteBtn).toBeDisabled()\n    })\n\n    it('should disable folder delete when multiple delimiters are configured', () => {\n      const mockOnDeleteFolder = jest.fn()\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder',\n        keyCount: 100,\n        delimiters: [':', '-'],\n        onDeleteFolder: mockOnDeleteFolder,\n      }\n\n      renderNode({ data: mockData })\n\n      const deleteBtn = screen.getByTestId('delete-folder-btn-folder')\n      expect(deleteBtn).toBeDisabled()\n    })\n\n    it('should stop propagation when folder delete button is clicked', () => {\n      const mockOnDeleteFolder = jest.fn()\n      const mockUpdateStatusOpen = jest.fn()\n      const mockSetOpen = jest.fn()\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder',\n        keyCount: 100,\n        delimiters: [':'],\n        onDeleteFolder: mockOnDeleteFolder,\n        updateStatusOpen: mockUpdateStatusOpen,\n      }\n\n      renderNode({ setOpen: mockSetOpen, isOpen: false, data: mockData })\n\n      screen.getByTestId('delete-folder-btn-folder').click()\n\n      expect(mockOnDeleteFolder).toHaveBeenCalled()\n      expect(mockSetOpen).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('showFolderMetadata', () => {\n    it('should hide folder actions when showFolderMetadata is false', () => {\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder',\n        keyCount: 100,\n        keyApproximate: 50,\n        delimiters: [':'],\n        onDeleteFolder: jest.fn(),\n        showFolderMetadata: false,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(screen.queryByTestId('percentage_folder')).not.toBeInTheDocument()\n      expect(screen.queryByTestId('count_folder')).not.toBeInTheDocument()\n      expect(\n        screen.queryByTestId('delete-folder-btn-folder'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should show folder actions when showFolderMetadata is true', () => {\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder',\n        keyCount: 100,\n        keyApproximate: 50,\n        delimiters: [':'],\n        onDeleteFolder: jest.fn(),\n        showFolderMetadata: true,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(screen.getByTestId('percentage_folder')).toBeInTheDocument()\n      expect(screen.getByTestId('count_folder')).toBeInTheDocument()\n      expect(screen.getByTestId('delete-folder-btn-folder')).toBeInTheDocument()\n    })\n  })\n\n  describe('showDeleteAction', () => {\n    it('should hide leaf DeleteKeyPopover when showDeleteAction is false', () => {\n      const mockData: TreeData = {\n        ...mockedDataWithMetadata,\n        onDelete: jest.fn(),\n        onDeleteClicked: jest.fn(),\n        showDeleteAction: false,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.queryByTestId(`delete-key-btn-${mockDataFullName}`),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should show leaf DeleteKeyPopover when showDeleteAction is true', () => {\n      const mockData: TreeData = {\n        ...mockedDataWithMetadata,\n        onDelete: jest.fn(),\n        onDeleteClicked: jest.fn(),\n        showDeleteAction: true,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.getByTestId(`delete-key-btn-${mockDataFullName}`),\n      ).toBeInTheDocument()\n    })\n\n    it('should show leaf DeleteKeyPopover by default when showDeleteAction is not set', () => {\n      const mockData: TreeData = {\n        ...mockedDataWithMetadata,\n        onDelete: jest.fn(),\n        onDeleteClicked: jest.fn(),\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.getByTestId(`delete-key-btn-${mockDataFullName}`),\n      ).toBeInTheDocument()\n    })\n\n    it('should still show folder metadata when showDeleteAction is false and showFolderMetadata is true', () => {\n      const mockData: TreeData = {\n        ...mockedData,\n        isLeaf: false,\n        fullName: 'folder',\n        keyCount: 100,\n        keyApproximate: 50,\n        delimiters: [':'],\n        onDeleteFolder: jest.fn(),\n        showFolderMetadata: true,\n        showDeleteAction: false,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(screen.getByTestId('percentage_folder')).toBeInTheDocument()\n      expect(screen.getByTestId('count_folder')).toBeInTheDocument()\n    })\n  })\n\n  describe('Node metadata and column visibility', () => {\n    it('should call getMetadata when node is clicked and TTL column is visible', () => {\n      const mockGetMetadata = jest.fn()\n      const mockUpdateStatusSelected = jest.fn()\n      const mockUpdateStatusOpen = jest.fn()\n\n      const mockData: TreeData = {\n        ...mockedData,\n        getMetadata: mockGetMetadata,\n        updateStatusSelected: mockUpdateStatusSelected,\n        updateStatusOpen: mockUpdateStatusOpen,\n      }\n\n      renderNode({ data: mockData })\n\n      screen.getByTestId(`node-item_${mockDataFullName}`).click()\n\n      expect(mockGetMetadata).toBeCalledWith(\n        mockedData.nameBuffer,\n        mockedData.path,\n      )\n      expect(mockUpdateStatusSelected).toBeCalledWith(mockedData.nameBuffer)\n      expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true)\n    })\n\n    it('should not call getMetadata when node is clicked and metadata exists', () => {\n      const mockGetMetadata = jest.fn()\n      const mockUpdateStatusSelected = jest.fn()\n      const mockUpdateStatusOpen = jest.fn()\n\n      const mockData: TreeData = {\n        ...mockedDataWithMetadata,\n        getMetadata: mockGetMetadata,\n        updateStatusSelected: mockUpdateStatusSelected,\n        updateStatusOpen: mockUpdateStatusOpen,\n      }\n\n      renderNode({ data: mockData })\n\n      screen.getByTestId(`node-item_${mockDataFullName}`).click()\n\n      expect(mockUpdateStatusSelected).toBeCalledWith(\n        mockedDataWithMetadata.nameBuffer,\n      )\n      expect(mockUpdateStatusOpen).toBeCalledWith(mockDataFullName, true)\n      expect(mockGetMetadata).not.toBeCalled()\n    })\n\n    it('should render TTL and Size when metadata exists', () => {\n      renderNode({ data: mockedDataWithMetadata })\n\n      expect(screen.getByTestId(`ttl-${mockDataFullName}`)).toBeInTheDocument()\n      expect(screen.getByTestId(`size-${mockDataFullName}`)).toBeInTheDocument()\n    })\n\n    it('should not render TTL and Size when metadata does not exist', () => {\n      renderNode({ data: mockedData })\n\n      expect(\n        screen.queryByTestId(`ttl-${mockDataFullName}`),\n      ).not.toBeInTheDocument()\n      expect(\n        screen.queryByTestId(`size-${mockDataFullName}`),\n      ).not.toBeInTheDocument()\n    })\n\n    it.each`\n      description      | initialState                                                | updatedState\n      ${'TTL column'}  | ${{ app: { context: { dbConfig: { shownColumns: [] } } } }} | ${{ app: { context: { dbConfig: { shownColumns: [BrowserColumns.TTL] } } } }}\n      ${'Size column'} | ${{ app: { context: { dbConfig: { shownColumns: [] } } } }} | ${{ app: { context: { dbConfig: { shownColumns: [BrowserColumns.Size] } } } }}\n    `(\n      'should refetch metadata when $description is re-enabled even with existing metadata',\n      ({ initialState, updatedState }) => {\n        const mockGetMetadata = jest.fn()\n        const mockData: TreeData = {\n          ...mockedDataWithMetadata,\n          getMetadata: mockGetMetadata,\n        }\n\n        const connectionState = {\n          connections: {\n            instances: { connectedInstance: { id: mockInstanceId } },\n          },\n        }\n        const customStore = {\n          getState: () => ({ ...initialState, ...connectionState }),\n          subscribe: jest.fn(),\n          dispatch: jest.fn(),\n        }\n\n        const { rerender } = renderNode(\n          { data: mockData },\n          { store: customStore },\n        )\n\n        customStore.getState = () => ({\n          ...updatedState,\n          ...connectionState,\n        })\n\n        rerender(\n          <MakeSearchableModalProvider>\n            <Node {...instance(mockedProps)} data={mockData} />\n          </MakeSearchableModalProvider>,\n        )\n\n        expect(mockGetMetadata).toHaveBeenCalledWith(\n          mockData.nameBuffer,\n          mockData.path,\n        )\n      },\n    )\n\n    it.each`\n      columns                                      | description\n      ${[]}                                        | ${'no columns are shown'}\n      ${[BrowserColumns.TTL]}                      | ${'only TTL column is shown'}\n      ${[BrowserColumns.Size]}                     | ${'only Size column is shown'}\n      ${[BrowserColumns.TTL, BrowserColumns.Size]} | ${'both TTL and Size columns are shown'}\n    `('should render DeleteKeyPopover when $description', ({ columns }) => {\n      const mockData: TreeData = {\n        ...mockedDataWithMetadata,\n        onDelete: jest.fn(),\n        onDeleteClicked: jest.fn(),\n      }\n\n      const customStore = {\n        getState: () => ({\n          app: {\n            context: {\n              dbConfig: {\n                shownColumns: columns,\n              },\n            },\n          },\n          connections: {\n            instances: { connectedInstance: { id: mockInstanceId } },\n          },\n        }),\n        subscribe: jest.fn(),\n        dispatch: jest.fn(),\n      }\n\n      const { container } = renderNode(\n        { data: mockData },\n        { store: customStore },\n      )\n\n      expect(\n        container.querySelector(\n          `[data-testid=\"delete-key-btn-${mockData.nameString}\"]`,\n        ),\n      ).toBeInTheDocument()\n    })\n  })\n\n  describe('Index button (folder searchable)', () => {\n    const mockFolderName = 'users'\n    const mockFirstSearchableKey = {\n      nameBuffer: stringToBuffer('users:1'),\n      nameString: 'users:1',\n      type: KeyTypes.Hash,\n    }\n\n    const baseFolderData: TreeData = {\n      ...mockedData,\n      isLeaf: false,\n      fullName: mockFolderName,\n      keyCount: 10,\n      delimiters: [':'],\n      onDeleteFolder: jest.fn(),\n      showFolderMetadata: true,\n    }\n\n    it('should render Index button when hasSearchableKeys is true and feature flag is on', () => {\n      const spy = mockFeatureFlags({\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const mockData: TreeData = {\n        ...baseFolderData,\n        hasSearchableKeys: true,\n        firstSearchableKey: mockFirstSearchableKey,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.getByTestId(`index-folder-btn-${mockFolderName}`),\n      ).toBeInTheDocument()\n\n      spy.mockRestore()\n    })\n\n    it('should not render Index button when hasSearchableKeys is false', () => {\n      const spy = mockFeatureFlags({\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const mockData: TreeData = {\n        ...baseFolderData,\n        hasSearchableKeys: false,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.queryByTestId(`index-folder-btn-${mockFolderName}`),\n      ).not.toBeInTheDocument()\n\n      spy.mockRestore()\n    })\n\n    it('should not render Index button when feature flag is off', () => {\n      const spy = mockFeatureFlags({\n        [FeatureFlags.vectorSearchV2]: { flag: false },\n      })\n\n      const mockData: TreeData = {\n        ...baseFolderData,\n        hasSearchableKeys: true,\n        firstSearchableKey: mockFirstSearchableKey,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.queryByTestId(`index-folder-btn-${mockFolderName}`),\n      ).not.toBeInTheDocument()\n\n      spy.mockRestore()\n    })\n\n    it('should send SEARCH_MAKE_SEARCHABLE_CLICKED telemetry with tree_view source on Index button click', () => {\n      const spy = mockFeatureFlags({\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const mockData: TreeData = {\n        ...baseFolderData,\n        hasSearchableKeys: true,\n        firstSearchableKey: mockFirstSearchableKey,\n      }\n\n      renderNode({ data: mockData })\n\n      const indexFolderBtn = screen.getByTestId(\n        `index-folder-btn-${mockFolderName}`,\n      )\n      fireEvent.click(indexFolderBtn)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CLICKED,\n        eventData: {\n          databaseId: mockInstanceId,\n          keyType: RedisearchIndexKeyType.HASH,\n          source: SearchBrowserSource.TreeView,\n        },\n      })\n\n      spy.mockRestore()\n    })\n\n    it('should open modal on Index button click', () => {\n      const spy = mockFeatureFlags({\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const mockData: TreeData = {\n        ...baseFolderData,\n        hasSearchableKeys: true,\n        firstSearchableKey: mockFirstSearchableKey,\n      }\n\n      renderNode({ data: mockData })\n\n      fireEvent.click(screen.getByTestId(`index-folder-btn-${mockFolderName}`))\n\n      expect(\n        screen.getByTestId('make-searchable-modal-body'),\n      ).toBeInTheDocument()\n\n      spy.mockRestore()\n    })\n\n    it('should navigate to create index page with correct query params on confirm', () => {\n      const spy = mockFeatureFlags({\n        [FeatureFlags.vectorSearchV2]: { flag: true },\n      })\n\n      const mockData: TreeData = {\n        ...baseFolderData,\n        hasSearchableKeys: true,\n        firstSearchableKey: mockFirstSearchableKey,\n      }\n\n      renderNode({ data: mockData })\n\n      fireEvent.click(screen.getByTestId(`index-folder-btn-${mockFolderName}`))\n      fireEvent.click(screen.getByTestId('make-searchable-modal-confirm'))\n\n      expect(mockPush).toHaveBeenCalledWith({\n        pathname: Pages.vectorSearchCreateIndex(mockInstanceId),\n        search:\n          `mode=${CreateIndexMode.ExistingData}&initialKey=users%3A1` +\n          `&initialKeyType=${RedisearchIndexKeyType.HASH}&initialPrefix=users%3A`,\n      })\n\n      spy.mockRestore()\n    })\n\n    it('should call checkSearchable on mount when prop is provided', () => {\n      const mockCheckSearchable = jest.fn()\n      const mockData: TreeData = {\n        ...baseFolderData,\n        checkSearchable: mockCheckSearchable,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(mockCheckSearchable).toHaveBeenCalledWith(\n        `${mockFolderName}:`,\n        mockData.path,\n      )\n    })\n\n    it('should not call checkSearchable when prop is not provided', () => {\n      const mockData: TreeData = {\n        ...baseFolderData,\n      }\n\n      renderNode({ data: mockData })\n\n      expect(\n        screen.getByTestId(`node-item_${mockFolderName}`),\n      ).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const NodeContainer = styled.div<\n  React.HTMLAttributes<HTMLDivElement> & {\n    $isSelected?: boolean\n    $isEven?: boolean\n  }\n>`\n  border-left: 3px solid transparent;\n\n  &:hover {\n    background-color: ${({ theme }) =>\n      theme.semantic.color.background.primary200};\n  }\n\n  ${({ $isEven, $isSelected, theme }) =>\n    $isEven &&\n    !$isSelected &&\n    css`\n      background-color: ${theme.semantic.color.background.neutral200};\n    `}\n\n  ${({ $isSelected, theme }) =>\n    $isSelected &&\n    css`\n      border-left-color: ${theme.semantic.color.background.primary200};\n      background-color: ${theme.semantic.color.background.primary200};\n    `}\n`\n\nexport const FOLDER_ANCHOR_CLASS = 'node-folder-anchor'\n\nexport const IndexButton = styled.button<\n  React.ButtonHTMLAttributes<HTMLButtonElement>\n>`\n  all: unset;\n  display: none;\n  cursor: pointer;\n  padding: 0 ${({ theme }) => theme.core.space.space100};\n  color: ${({ theme }) => theme.semantic.color.text.informative400};\n  font-size: inherit;\n  white-space: nowrap;\n`\n\nexport const NodeContent = styled(Row).attrs({\n  align: 'center',\n  justify: 'between',\n})`\n  height: 100%;\n  cursor: pointer;\n  padding: ${({ theme }) =>\n    `${theme.core.space.space100} ${theme.core.space.space200}`};\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n\n  .${FOLDER_ANCHOR_CLASS} {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    min-width: 0;\n  }\n\n  .moveOnHoverKey {\n    transition: transform ease 0.3s;\n\n    &.hide {\n      transform: translateX(-8px);\n    }\n  }\n\n  .showOnHoverKey {\n    display: none;\n\n    &.show {\n      display: flex;\n    }\n  }\n\n  &:hover {\n    .moveOnHoverKey {\n      transform: translateX(-8px);\n    }\n\n    .showOnHoverKey {\n      display: flex;\n    }\n\n    ${IndexButton} {\n      display: inline;\n    }\n  }\n`\n\nexport const FolderContent = styled(Row)`\n  width: 100%;\n  height: 42px;\n  position: relative;\n`\n\nexport const NodeIcon = styled.span<React.HTMLAttributes<HTMLSpanElement>>`\n  margin-right: ${({ theme }) => theme.core.space.space100};\n  display: inline-flex;\n`\n\nexport const NodeIconArrow = styled.span<React.HTMLAttributes<HTMLSpanElement>>`\n  margin-left: ${({ theme }) => theme.core.space.space100};\n  margin-right: ${({ theme }) => theme.core.space.space050};\n  display: inline-flex;\n\n  svg {\n    width: ${({ theme }) => theme.core.space.space150};\n    height: ${({ theme }) => theme.core.space.space150};\n  }\n`\n\nexport const FolderTooltipHeader = styled(Row).attrs({\n  align: 'center',\n  justify: 'between',\n  gap: 'l',\n})`\n  flex-wrap: wrap;\n  word-break: break-all;\n`\n\nexport const FolderPattern = styled.span<React.HTMLAttributes<HTMLSpanElement>>`\n  font-weight: bold;\n  margin-right: ${({ theme }) => theme.core.space.space050};\n  white-space: normal;\n`\n\nexport const Delimiters = styled.span<React.HTMLAttributes<HTMLSpanElement>>`\n  display: inline-flex;\n  flex-wrap: wrap;\n`\n\nexport const Delimiter = styled.span<React.HTMLAttributes<HTMLSpanElement>>`\n  margin-bottom: ${({ theme }) => theme.core.space.space025};\n  padding: ${({ theme }) =>\n    `${theme.core.space.space025} ${theme.core.space.space050}`};\n  margin-right: ${({ theme }) => theme.core.space.space050};\n  border-radius: ${({ theme }) => theme.core.space.space025};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral200};\n`\n\nexport const FolderApproximate = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  display: inline-block;\n  width: 86px;\n  min-width: 86px;\n  text-align: right;\n  transition: transform ease 0.3s;\n`\n\nexport const FolderKeyCount = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  display: inline-block;\n  width: 90px;\n  min-width: 90px;\n  text-align: right;\n  transition: transform ease 0.3s;\n`\n\nexport const FolderActions = styled(Row)`\n  &:hover {\n    ${FolderApproximate},\n    ${FolderKeyCount} {\n      transform: translateX(-8px);\n    }\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { NodePublicState } from 'react-vtree/dist/es/Tree'\nimport { useSelector } from 'react-redux'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { Maybe } from 'uiSrc/utils'\nimport {\n  BrowserColumns,\n  FeatureFlags,\n  KeyTypes,\n  ModulesKeyTypes,\n  TEXT_BULK_DELETE_DISABLED_MULTIPLE_DELIMITERS,\n  TEXT_BULK_DELETE_DISABLED_UNPRINTABLE,\n  TEXT_BULK_DELETE_TOOLTIP,\n} from 'uiSrc/constants'\nimport KeyRowTTL from 'uiSrc/pages/browser/components/key-row-ttl'\nimport KeyRowSize from 'uiSrc/pages/browser/components/key-row-size'\nimport KeyRowName from 'uiSrc/pages/browser/components/key-row-name'\nimport KeyRowType from 'uiSrc/pages/browser/components/key-row-type'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { appContextDbConfig } from 'uiSrc/slices/app/context'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { FeatureFlagComponent, RiTooltip } from 'uiSrc/components'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { Flex } from 'uiSrc/components/base/layout/flex'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { KEY_TYPE_MAP } from 'uiSrc/pages/vector-search/constants'\nimport { useMakeSearchableModal } from 'uiSrc/pages/browser/components/make-searchable-modal'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { SearchBrowserSource } from 'uiSrc/pages/vector-search/telemetry.constants'\nimport * as S from './Node.styles'\nimport { TreeData } from '../../VirtualTree.types'\nimport { DeleteKeyPopover } from '../../../delete-key-popover/DeleteKeyPopover'\n\nconst MAX_NESTING_LEVEL = 20\n\nconst Node = ({\n  data,\n  isOpen,\n  index,\n  style,\n  setOpen,\n}: NodePublicState<TreeData>) => {\n  const {\n    id: nodeId,\n    isLeaf,\n    keyCount,\n    nestingLevel,\n    fullName,\n    nameBuffer,\n    path,\n    type,\n    ttl,\n    shortName,\n    size,\n    deleting,\n    nameString,\n    keyApproximate,\n    isSelected,\n    delimiters = [],\n    hasSearchableKeys,\n    firstSearchableKey,\n    checkSearchable,\n    getMetadata,\n    onDelete,\n    onDeleteClicked,\n    onDeleteFolder,\n    updateStatusOpen,\n    updateStatusSelected,\n    visibleColumns: visibleColumnsProp,\n    showFolderMetadata: showFolderMetadataProp = true,\n    showDeleteAction: showDeleteActionProp = true,\n    showSelectedIndicator: showSelectedIndicatorProp = false,\n  } = data\n\n  const delimiterView = delimiters.length === 1 ? delimiters[0] : '-'\n  const folderPrefix = `${fullName}${delimiterView}`\n\n  const { shownColumns } = useSelector(appContextDbConfig)\n  const visibleColumns = visibleColumnsProp ?? shownColumns\n  const includeSize = visibleColumns.includes(BrowserColumns.Size)\n  const includeTTL = visibleColumns.includes(BrowserColumns.TTL)\n\n  const { openMakeSearchableModal } = useMakeSearchableModal()\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const [deletePopoverId, setDeletePopoverId] =\n    useState<Maybe<string>>(undefined)\n  const prevIncludeSize = useRef(includeSize)\n  const prevIncludeTTL = useRef(includeTTL)\n\n  useEffect(() => {\n    const isSizeReenabled = !prevIncludeSize.current && includeSize\n    const isTtlReenabled = !prevIncludeTTL.current && includeTTL\n\n    if (\n      isLeaf &&\n      nameBuffer &&\n      (isSizeReenabled || isTtlReenabled || (!size && !ttl))\n    ) {\n      getMetadata?.(nameBuffer, path)\n    }\n\n    prevIncludeSize.current = includeSize\n    prevIncludeTTL.current = includeTTL\n  }, [includeSize, includeTTL, isLeaf, nameBuffer, size, ttl])\n\n  useEffect(() => {\n    if (checkSearchable) {\n      checkSearchable(folderPrefix, path)\n    }\n  }, [checkSearchable, folderPrefix, path])\n\n  const handleClick = () => {\n    if (isLeaf) {\n      updateStatusSelected?.(nameBuffer)\n    }\n\n    updateStatusOpen?.(fullName, !isOpen)\n    !isLeaf && setOpen(!isOpen)\n  }\n\n  const handleKeyDown = ({ key }: React.KeyboardEvent<HTMLDivElement>) => {\n    if (key === keys.SPACE) {\n      handleClick()\n    }\n  }\n\n  const handleDelete = (nameBuffer: RedisResponseBuffer) => {\n    onDelete(nameBuffer)\n    setDeletePopoverId(undefined)\n  }\n\n  const handleDeletePopoverOpen = (\n    index: Maybe<string>,\n    type: KeyTypes | ModulesKeyTypes,\n  ) => {\n    if (index !== deletePopoverId) {\n      onDeleteClicked(type)\n    }\n    setDeletePopoverId(index !== deletePopoverId ? index : undefined)\n  }\n\n  const deletePattern = `${fullName}${delimiterView}*`\n\n  const handleDeleteFolder = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    onDeleteFolder?.(deletePattern, fullName, keyCount)\n  }\n\n  const getKeyPrefix = useCallback(\n    (keyName: string) => {\n      const lastDelimiterIndex = keyName.lastIndexOf(delimiterView)\n      if (lastDelimiterIndex === -1) return folderPrefix\n      return keyName.substring(0, lastDelimiterIndex + delimiterView.length)\n    },\n    [delimiterView, folderPrefix],\n  )\n\n  const handleIndexClick = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    const source = SearchBrowserSource.TreeView\n    const keyType = firstSearchableKey\n      ? KEY_TYPE_MAP[firstSearchableKey.type]\n      : undefined\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_MAKE_SEARCHABLE_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        keyType,\n        source,\n      },\n    })\n    const initialPrefix = firstSearchableKey?.nameString\n      ? getKeyPrefix(firstSearchableKey.nameString)\n      : folderPrefix\n    openMakeSearchableModal({\n      prefix: folderPrefix,\n      initialKey: firstSearchableKey?.nameBuffer,\n      initialKeyType: keyType,\n      initialPrefix,\n      source,\n    })\n  }\n\n  const hasUnprintableChars =\n    fullName?.includes('\\uFFFD') || nameString?.includes('\\uFFFD')\n\n  const isDeleteDisabled = delimiters.length > 1 || hasUnprintableChars\n\n  const getDeleteTooltip = () => {\n    if (hasUnprintableChars) {\n      return TEXT_BULK_DELETE_DISABLED_UNPRINTABLE\n    }\n    if (delimiters.length > 1) {\n      return TEXT_BULK_DELETE_DISABLED_MULTIPLE_DELIMITERS\n    }\n    return TEXT_BULK_DELETE_TOOLTIP(deletePattern)\n  }\n  const deleteTooltip = getDeleteTooltip()\n\n  const folderTextColor = isOpen ? 'primary' : 'secondary'\n\n  const Folder = () => (\n    <RiTooltip\n      content={tooltipContent}\n      position=\"bottom\"\n      anchorClassName={S.FOLDER_ANCHOR_CLASS}\n    >\n      <S.FolderContent align=\"center\">\n        <Flex align=\"center\">\n          <S.NodeIconArrow>\n            <RiIcon\n              size=\"xs\"\n              type={isOpen ? 'ChevronDownIcon' : 'ChevronRightIcon'}\n              data-test-subj={`node-arrow-icon_${fullName}`}\n            />\n          </S.NodeIconArrow>\n          <S.NodeIcon>\n            <RiIcon\n              size=\"m\"\n              type=\"FolderIcon\"\n              data-test-subj={`node-folder-icon_${fullName}`}\n            />\n          </S.NodeIcon>\n          <Text\n            color={folderTextColor}\n            className=\"truncateText\"\n            data-testid={`folder-${nameString}`}\n          >\n            {nameString}\n          </Text>\n        </Flex>\n        {showFolderMetadataProp && (\n          <S.FolderActions align=\"center\" justify=\"end\">\n            <S.FolderApproximate data-testid={`percentage_${fullName}`}>\n              <ColorText color=\"secondary\">\n                {keyApproximate\n                  ? `${keyApproximate < 1 ? '<1' : Math.round(keyApproximate)}%`\n                  : ''}\n              </ColorText>\n            </S.FolderApproximate>\n            <S.FolderKeyCount data-testid={`count_${fullName}`}>\n              <ColorText color=\"secondary\">{keyCount ?? ''}</ColorText>\n            </S.FolderKeyCount>\n            {hasSearchableKeys && (\n              <FeatureFlagComponent name={FeatureFlags.vectorSearchV2}>\n                <RiTooltip\n                  position=\"top\"\n                  content={\n                    <span>\n                      Index data with the \"<strong>{folderPrefix}</strong>\"{' '}\n                      prefix so you can query it using full-text, vector, exact\n                      matching, and geospatial search.\n                    </span>\n                  }\n                >\n                  <S.IndexButton\n                    onClick={handleIndexClick}\n                    data-testid={`index-folder-btn-${fullName}`}\n                  >\n                    Index\n                  </S.IndexButton>\n                </RiTooltip>\n              </FeatureFlagComponent>\n            )}\n            <FeatureFlagComponent name={FeatureFlags.envDependent}>\n              <RiTooltip content={deleteTooltip} position=\"left\">\n                <IconButton\n                  icon={DeleteIcon}\n                  onClick={handleDeleteFolder}\n                  disabled={isDeleteDisabled}\n                  className=\"showOnHoverKey\"\n                  aria-label=\"Delete Folder Keys\"\n                  data-testid={`delete-folder-btn-${fullName}`}\n                />\n              </RiTooltip>\n            </FeatureFlagComponent>\n          </S.FolderActions>\n        )}\n      </S.FolderContent>\n    </RiTooltip>\n  )\n\n  const Leaf = () => (\n    <>\n      <KeyRowType type={type} nameString={nameString} />\n      <KeyRowName shortName={shortName} nameString={nameString} />\n      {includeTTL && (\n        <KeyRowTTL\n          ttl={ttl}\n          nameString={nameString}\n          deletePopoverId={deletePopoverId}\n          rowId={nodeId}\n        />\n      )}\n      {includeSize && (\n        <KeyRowSize\n          size={size}\n          nameString={nameString}\n          deletePopoverId={deletePopoverId}\n          rowId={nodeId}\n        />\n      )}\n      {showDeleteActionProp && (\n        <DeleteKeyPopover\n          deletePopoverId={deletePopoverId === nodeId ? nodeId : undefined}\n          nameString={nameString}\n          name={nameBuffer}\n          type={type}\n          rowId={nodeId}\n          deleting={deleting}\n          onDelete={handleDelete}\n          onOpenPopover={handleDeletePopoverOpen}\n        />\n      )}\n      {showSelectedIndicatorProp && isSelected && (\n        <RiIcon\n          size=\"m\"\n          type=\"ChevronRightIcon\"\n          data-testid={`selected-indicator_${fullName}`}\n        />\n      )}\n    </>\n  )\n\n  const NodeItem = (\n    <S.NodeContent\n      role=\"treeitem\"\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      tabIndex={0}\n      onFocus={() => {}}\n      data-testid={`node-item_${fullName}${isOpen && !isLeaf ? '--expanded' : ''}`}\n    >\n      {!isLeaf && <Folder />}\n      {isLeaf && <Leaf />}\n    </S.NodeContent>\n  )\n\n  const tooltipContent = (\n    <>\n      <S.FolderTooltipHeader>\n        <S.FolderPattern>{`${fullName + delimiterView}*`}</S.FolderPattern>\n        {delimiters.length > 1 && (\n          <S.Delimiters>\n            {delimiters.map((delimiter) => (\n              <S.Delimiter key={delimiter}>{delimiter}</S.Delimiter>\n            ))}\n          </S.Delimiters>\n        )}\n      </S.FolderTooltipHeader>\n      <ColorText color=\"secondary\">\n        {`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`}\n      </ColorText>\n    </>\n  )\n\n  return (\n    <S.NodeContainer\n      style={{\n        ...style,\n        paddingLeft:\n          (nestingLevel > MAX_NESTING_LEVEL\n            ? MAX_NESTING_LEVEL\n            : nestingLevel) * 8,\n      }}\n      $isSelected={isSelected && isLeaf}\n      $isEven={index % 2 === 0}\n    >\n      {NodeItem}\n    </S.NodeContainer>\n  )\n}\n\nexport default Node\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/index.ts",
    "content": "import Node from './Node'\n\nexport { Node }\n\nexport default Node\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/index.ts",
    "content": "import VirtualTree from './VirtualTree'\n\nexport * from './VirtualTree.types'\n\nexport { VirtualTree }\n\nexport default VirtualTree\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/components/virtual-tree/styles.module.scss",
    "content": ".customScroll {\n  @include eui.scrollBar;\n\n  height: 100%;\n  position: relative;\n}\n\n.loadingContainer {\n  display: flex;\n  width: 100%;\n  height: 100%;\n  align-items: center;\n}\n\n.loadingBody {\n  display: flex;\n  width: 54px;\n  height: 54px;\n  margin: auto;\n  position: relative;\n}\n\n.loadingSpinner {\n  width: 54px !important;\n  height: 54px !important;\n}\n\n.loadingIcon {\n  position: absolute !important;\n  width: 28px !important;\n  height: 28px !important;\n  top: 12px;\n  left: 12px;\n}"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/index.ts",
    "content": "import BrowserPage from './BrowserPage'\n\nexport { BrowserPage }\n\nexport default BrowserPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/index.ts",
    "content": "export { KeyDetails } from './key-details'\nexport { KeyDetailsHeader } from './key-details-header'\nexport type { KeyDetailsHeaderProps } from './key-details-header'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  act,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  defaultSelectedKeyAction,\n  setSelectedKeyRefreshDisabled,\n} from 'uiSrc/slices/browser/keys'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { apiService } from 'uiSrc/services'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport KeyDetails, { Props as KeyDetailsProps } from './KeyDetails'\nimport { KeyValueFormat } from 'uiSrc/constants'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockedProps = mock<KeyDetailsProps>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('KeyDetails', () => {\n  it('should render', () => {\n    expect(render(<KeyDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call proper actions after render', () => {\n    render(<KeyDetails {...instance(mockedProps)} />)\n\n    expect(store.getActions()).toEqual([\n      defaultSelectedKeyAction(),\n      setSelectedKeyRefreshDisabled(false),\n    ])\n  })\n\n  it('should call proper actions after render when key name is truncated', () => {\n    render(\n      <KeyDetails\n        {...instance(mockedProps)}\n        keyProp={MOCK_TRUNCATED_BUFFER_VALUE}\n      />,\n    )\n\n    expect(store.getActions()).toEqual([defaultSelectedKeyAction()])\n  })\n\n  it('should render nothing when there are no keys', () => {\n    render(\n      <KeyDetails\n        {...instance(mockedProps)}\n        totalKeys={0}\n        keysLastRefreshTime={null}\n      />,\n    )\n\n    expect(screen.queryByTestId('explore-guides')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('select-key-message')).not.toBeInTheDocument()\n  })\n\n  it('should render explore-guides when there are no keys', () => {\n    render(\n      <KeyDetails\n        {...instance(mockedProps)}\n        totalKeys={0}\n        keysLastRefreshTime={1}\n      />,\n    )\n\n    expect(screen.getByTestId('explore-guides')).toBeInTheDocument()\n  })\n\n  it('should render proper message when there are keys', () => {\n    render(\n      <KeyDetails\n        {...instance(mockedProps)}\n        totalKeys={10}\n        keysLastRefreshTime={1}\n      />,\n    )\n\n    expect(screen.getByTestId('select-key-message')).toBeInTheDocument()\n  })\n\n  it('should call proper telemetry after open key details', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    apiService.post = jest\n      .fn()\n      .mockResolvedValueOnce({ status: 200, data: { length: 1, type: 'hash' } })\n\n    await act(async () => {\n      render(\n        <KeyDetails\n          {...instance(mockedProps)}\n          keyProp={stringToBuffer('key')}\n        />,\n      )\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.TREE_VIEW_KEY_VALUE_VIEWED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        length: 1,\n        keyType: 'hash',\n        formatter: KeyValueFormat.Unicode,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { isNull } from 'lodash'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport cx from 'classnames'\n\nimport {\n  defaultSelectedKeyAction,\n  fetchKeyInfo,\n  keysSelector,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n  setSelectedKeyRefreshDisabled,\n} from 'uiSrc/slices/browser/keys'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { isTruncatedString, Nullable } from 'uiSrc/utils'\nimport { NoKeySelected } from './components/no-key-selected'\nimport { DynamicTypeDetails } from './components/dynamic-type-details'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  isFullScreen: boolean\n  arePanelsCollapsed: boolean\n  onToggleFullScreen: () => void\n  onCloseKey: () => void\n  onEditKey: (key: RedisResponseBuffer, newKey: RedisResponseBuffer) => void\n  onRemoveKey: () => void\n  keyProp: RedisResponseBuffer | null\n  totalKeys: number\n  keysLastRefreshTime: Nullable<number>\n}\n\nconst KeyDetails = (props: Props) => {\n  const { onCloseKey, keyProp, totalKeys, keysLastRefreshTime } = props\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { viewType } = useSelector(keysSelector)\n  const {\n    loading,\n    error = '',\n    data,\n    viewFormat,\n  } = useSelector(selectedKeySelector)\n  const isKeySelected = !isNull(useSelector(selectedKeyDataSelector))\n  const { type: keyType } = useSelector(selectedKeyDataSelector) ?? {\n    type: KeyTypes.String,\n  }\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (keyProp === null) return\n\n    if (isTruncatedString(keyProp)) {\n      dispatch(defaultSelectedKeyAction())\n      return\n    }\n\n    dispatch(\n      fetchKeyInfo(keyProp, undefined, (data) => {\n        if (!data) return\n\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_VIEWED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_VIEWED,\n          ),\n          eventData: {\n            keyType: data.type,\n            databaseId: instanceId,\n            length: data.length,\n            formatter: viewFormat,\n          },\n        })\n      }),\n    )\n\n    dispatch(setSelectedKeyRefreshDisabled(false))\n  }, [keyProp])\n\n  const onCloseAddItemPanel = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_ADD_VALUE_CANCELLED,\n        TelemetryEvent.TREE_VIEW_KEY_ADD_VALUE_CANCELLED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType,\n      },\n    })\n  }\n\n  const onOpenAddItemPanel = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_ADD_VALUE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_ADD_VALUE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType,\n      },\n    })\n  }\n\n  return (\n    <div className={styles.container}>\n      <div\n        className={cx(styles.content, {\n          [styles.contentActive]: data || error || loading,\n        })}\n      >\n        {!isKeySelected && !loading ? (\n          <NoKeySelected\n            keyProp={keyProp}\n            totalKeys={totalKeys}\n            keysLastRefreshTime={keysLastRefreshTime}\n            error={error}\n            onClosePanel={onCloseKey}\n          />\n        ) : (\n          <DynamicTypeDetails\n            {...props}\n            keyType={keyType}\n            onOpenAddItemPanel={onOpenAddItemPanel}\n            onCloseAddItemPanel={onCloseAddItemPanel}\n          />\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport default React.memo(KeyDetails)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport ChangeEditorTypeButton from './ChangeEditorTypeButton'\n\nconst mockSwitchEditorType = jest.fn()\nlet mockIsTextEditorDisabled = false\n\njest.mock('./useChangeEditorType', () => ({\n  useChangeEditorType: () => ({\n    switchEditorType: mockSwitchEditorType,\n    isTextEditorDisabled: mockIsTextEditorDisabled,\n  }),\n}))\n\ndescribe('ChangeEditorTypeButton', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render an enabled button with default tooltip', async () => {\n    mockIsTextEditorDisabled = false\n\n    render(<ChangeEditorTypeButton />)\n\n    const button = screen.getByRole('button', { name: /change editor type/i })\n    expect(button).toBeEnabled()\n\n    await userEvent.hover(button)\n    expect(\n      (await screen.findAllByText('Edit value in text editor'))[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should render a disabled button with a tooltip', async () => {\n    mockIsTextEditorDisabled = true\n\n    render(<ChangeEditorTypeButton />)\n\n    const button = screen.getByRole('button', { name: /change editor type/i })\n    expect(button).toBeDisabled()\n\n    await userEvent.hover(button)\n    expect(\n      (\n        await screen.findAllByText(\n          'This JSON document is too large to view or edit in full.',\n        )\n      )[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should call switchEditorType on click when not disabled', async () => {\n    mockIsTextEditorDisabled = false\n\n    render(<ChangeEditorTypeButton />)\n\n    const button = screen.getByRole('button', { name: /change editor type/i })\n    await userEvent.click(button)\n\n    expect(mockSwitchEditorType).toHaveBeenCalled()\n  })\n\n  it('should not call switchEditorType when disabled', async () => {\n    mockIsTextEditorDisabled = true\n\n    render(<ChangeEditorTypeButton />)\n\n    const button = screen.getByRole('button', { name: /change editor type/i })\n    await userEvent.click(button)\n\n    expect(mockSwitchEditorType).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/ChangeEditorTypeButton.tsx",
    "content": "import React from 'react'\nimport { RiTooltip } from 'uiSrc/components'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { useChangeEditorType } from './useChangeEditorType'\n\nconst ChangeEditorTypeButton = () => {\n  const { switchEditorType, isTextEditorDisabled } = useChangeEditorType()\n\n  const isDisabled = isTextEditorDisabled\n  const tooltip = isTextEditorDisabled\n    ? 'This JSON document is too large to view or edit in full.'\n    : 'Edit value in text editor'\n\n  return (\n    <RiTooltip content={tooltip} position=\"right\">\n      <IconButton\n        size=\"S\"\n        icon={EditIcon}\n        onClick={switchEditorType}\n        aria-label=\"Change editor type\"\n        disabled={isDisabled}\n        data-testid=\"change-editor-type\"\n      />\n    </RiTooltip>\n  )\n}\n\nexport default ChangeEditorTypeButton\n\nexport class ButtonMode {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/index.ts",
    "content": "import ChangeEditorTypeButton from './ChangeEditorTypeButton'\n\nexport { useChangeEditorType } from './useChangeEditorType'\nexport default ChangeEditorTypeButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.spec.ts",
    "content": "import * as reactRedux from 'react-redux'\nimport { renderHook, act } from '@testing-library/react-hooks'\nimport { EditorType } from 'uiSrc/slices/interfaces'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { useChangeEditorType } from './useChangeEditorType'\n\njest.mock('react-redux', () => ({\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/rejson', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/rejson'),\n  fetchReJSON: jest.fn((key) => ({ type: 'FETCH_REJSON', payload: key })),\n}))\n\nconst mockedUseDispatch = reactRedux.useDispatch as jest.Mock\nconst mockedUseSelector = reactRedux.useSelector as jest.Mock\nconst mockKeyName = stringToBuffer('test-key')\n\ndescribe('useChangeEditorType', () => {\n  const dispatchMock = jest.fn()\n\n  beforeEach(() => {\n    mockedUseDispatch.mockReturnValue(dispatchMock)\n    dispatchMock.mockClear()\n  })\n\n  it('should return opposite editor type correctly', () => {\n    mockedUseSelector.mockReturnValue({\n      editorType: EditorType.Default,\n    })\n\n    const { result } = renderHook(() => useChangeEditorType())\n\n    expect(result.current.editorType).toBe(EditorType.Default)\n\n    act(() => {\n      result.current.switchEditorType()\n    })\n\n    expect(dispatchMock).toHaveBeenCalledWith({\n      type: 'rejson/setEditorType',\n      payload: EditorType.Text,\n    })\n  })\n\n  it('should return opposite editor type correctly when editorType is Text', () => {\n    mockedUseSelector.mockReturnValue({\n      editorType: EditorType.Text,\n    })\n\n    const { result } = renderHook(() => useChangeEditorType())\n\n    expect(result.current.editorType).toBe(EditorType.Text)\n\n    act(() => {\n      result.current.switchEditorType()\n    })\n\n    expect(dispatchMock).toHaveBeenCalledWith({\n      type: 'rejson/setEditorType',\n      payload: EditorType.Default,\n    })\n  })\n\n  it('should fetch json when type switched', async () => {\n    mockedUseSelector\n      .mockReturnValue({\n        editorType: EditorType.Default,\n      })\n      .mockReturnValue({\n        [FeatureFlags.envDependent]: { flag: false },\n      })\n      .mockReturnValue({\n        name: mockKeyName,\n      })\n\n    const { result } = renderHook(() => useChangeEditorType())\n\n    act(() => {\n      result.current.switchEditorType()\n    })\n\n    expect(dispatchMock).toHaveBeenCalledWith({\n      type: 'rejson/setEditorType',\n      payload: EditorType.Default,\n    })\n    expect(dispatchMock).toHaveBeenCalledWith({\n      type: 'FETCH_REJSON',\n      payload: mockKeyName,\n    })\n  })\n\n  it('should not fetch json when there is no selected key', () => {\n    mockedUseSelector\n      .mockReturnValue({\n        editorType: EditorType.Default,\n      })\n      .mockReturnValue({\n        [FeatureFlags.envDependent]: { flag: false },\n      })\n\n    const { result } = renderHook(() => useChangeEditorType())\n\n    act(() => {\n      result.current.switchEditorType()\n    })\n\n    expect(dispatchMock).not.toHaveBeenCalledWith({\n      type: 'FETCH_REJSON',\n      payload: expect.anything(),\n    })\n  })\n\n  describe('isTextEditorDisabled', () => {\n    it('should be false when isWithinThreshold is true', () => {\n      mockedUseSelector\n        .mockImplementationOnce(() => ({\n          editorType: EditorType.Default,\n          isWithinThreshold: true,\n        }))\n        .mockImplementationOnce(() => ({\n          [FeatureFlags.envDependent]: { flag: false },\n        }))\n\n      const { result } = renderHook(() => useChangeEditorType())\n      expect(result.current.isTextEditorDisabled).toBe(false)\n    })\n\n    it('should be false when not within threshold but feature flag is true', () => {\n      mockedUseSelector\n        .mockImplementationOnce(() => ({\n          editorType: EditorType.Default,\n          isWithinThreshold: false,\n        }))\n        .mockImplementationOnce(() => ({\n          [FeatureFlags.envDependent]: { flag: true },\n        }))\n\n      const { result } = renderHook(() => useChangeEditorType())\n      expect(result.current.isTextEditorDisabled).toBe(false)\n    })\n\n    it('should be true when not within threshold and feature flag is false', () => {\n      mockedUseSelector\n        .mockImplementationOnce(() => ({\n          editorType: EditorType.Default,\n          isWithinThreshold: false,\n        }))\n        .mockImplementationOnce(() => ({\n          [FeatureFlags.envDependent]: { flag: false },\n        }))\n\n      const { result } = renderHook(() => useChangeEditorType())\n      expect(result.current.isTextEditorDisabled).toBe(true)\n    })\n\n    it('should be true when envDependentFeature is undefined', () => {\n      mockedUseSelector\n        .mockImplementationOnce(() => ({\n          editorType: EditorType.Default,\n          isWithinThreshold: false,\n        }))\n        .mockImplementationOnce(() => ({\n          [FeatureFlags.envDependent]: undefined,\n        }))\n\n      const { result } = renderHook(() => useChangeEditorType())\n      expect(result.current.isTextEditorDisabled).toBe(true)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/change-editor-type-button/useChangeEditorType.tsx",
    "content": "import { useCallback } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  fetchReJSON,\n  rejsonSelector,\n  setEditorType,\n} from 'uiSrc/slices/browser/rejson'\n\nimport { EditorType } from 'uiSrc/slices/interfaces'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\n\nexport const useChangeEditorType = () => {\n  const dispatch = useDispatch()\n  const { editorType, isWithinThreshold } = useSelector(rejsonSelector)\n  const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const selectedKey = useSelector(selectedKeyDataSelector)?.name\n\n  const isTextEditorDisabled = !isWithinThreshold && !envDependentFeature?.flag\n\n  const switchEditorType = useCallback(() => {\n    const opposite =\n      editorType === EditorType.Default ? EditorType.Text : EditorType.Default\n    dispatch(setEditorType(opposite))\n\n    if (selectedKey) {\n      dispatch(fetchReJSON(selectedKey))\n    }\n  }, [dispatch, editorType])\n\n  return { switchEditorType, editorType, isTextEditorDisabled }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/common/AddKeysContainer.styled.ts",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const AddKeysContainer = styled.div<\n  React.HTMLAttributes<HTMLDivElement> & {\n    theme: Theme\n  }\n>`\n  padding: ${({ theme }) => theme.core.space.space150};\n  position: absolute;\n  bottom: 0;\n  background: ${({ theme }) => theme.semantic.color.background.neutral300};\n  border-style: solid;\n  border-width: 0;\n  border-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.border.neutral500};\n  border-top-width: 1px;\n  width: 100%;\n\n  z-index: 2;\n\n  max-height: 100%;\n  overflow-y: auto;\n\n  &.contentActive {\n    border-color: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.border.neutral500};\n    border-bottom-width: 1px;\n  }\n`\nexport const EntryContent = styled(Col)`\n  max-height: calc(50vh - 100px);\n  scroll-padding-bottom: 60px;\n  overflow-y: auto;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { Props, DynamicTypeDetails } from './DynamicTypeDetails'\n\nconst mockedProps = mock<Props>()\n\nconst DynamicTypeDetailsTypeTests: any[] = [\n  [KeyTypes.Hash, 'hash-details'],\n  [KeyTypes.ZSet, 'zset-details'],\n  [KeyTypes.Set, 'set-details'],\n  [KeyTypes.List, 'list-details'],\n  [KeyTypes.Stream, 'stream-details'],\n  [KeyTypes.ReJSON, 'json-details'],\n  [ModulesKeyTypes.Graph, 'modules-type-details'],\n  [ModulesKeyTypes.TimeSeries, 'modules-type-details'],\n  ['123', 'unsupported-type-details'],\n]\n\ndescribe('DynamicTypeDetails', () => {\n  it('should render', () => {\n    expect(\n      render(<DynamicTypeDetails {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it.each(DynamicTypeDetailsTypeTests)(\n    'for key type: %s (reply), data-subj should exists: %s',\n    (type: KeyTypes, testId: string) => {\n      const { queryByTestId } = render(\n        <DynamicTypeDetails {...instance(mockedProps)} keyType={type} />,\n      )\n      expect(queryByTestId(testId)).toBeInTheDocument()\n    },\n  )\n\n  it('should show TooLongKeyNameDetails component when key name is truncated', () => {\n    const { queryByTestId } = render(\n      <DynamicTypeDetails\n        {...instance(mockedProps)}\n        keyProp={MOCK_TRUNCATED_BUFFER_VALUE}\n      />,\n    )\n    expect(queryByTestId('too-long-key-name-details')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/DynamicTypeDetails.tsx",
    "content": "import React from 'react'\nimport {\n  KeyTypes,\n  MODULES_KEY_TYPES_NAMES,\n  ModulesKeyTypes,\n} from 'uiSrc/constants'\nimport { KeyDetailsHeaderProps } from 'uiSrc/pages/browser/modules'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { isTruncatedString } from 'uiSrc/utils'\nimport TooLongKeyNameDetails from 'uiSrc/pages/browser/modules/key-details/components/too-long-key-name-details/TooLongKeyNameDetails'\nimport ModulesTypeDetails from '../modules-type-details/ModulesTypeDetails'\nimport UnsupportedTypeDetails from '../unsupported-type-details/UnsupportedTypeDetails'\nimport { RejsonDetailsWrapper } from '../rejson-details'\nimport { StringDetails } from '../string-details'\nimport { ZSetDetails } from '../zset-details'\nimport { SetDetails } from '../set-details'\nimport { HashDetails } from '../hash-details'\nimport { ListDetails } from '../list-details'\nimport { StreamDetails } from '../stream-details'\n\nexport interface Props extends KeyDetailsHeaderProps {\n  onOpenAddItemPanel: () => void\n  onCloseAddItemPanel: () => void\n  keyType: KeyTypes | ModulesKeyTypes\n  keyProp: RedisResponseBuffer | null\n}\n\nconst DynamicTypeDetails = (props: Props) => {\n  const { keyType: selectedKeyType, keyProp } = props\n\n  const TypeDetails: any = {\n    [KeyTypes.ZSet]: <ZSetDetails {...props} />,\n    [KeyTypes.Set]: <SetDetails {...props} />,\n    [KeyTypes.String]: <StringDetails {...props} />,\n    [KeyTypes.Hash]: <HashDetails {...props} />,\n    [KeyTypes.List]: <ListDetails {...props} />,\n    [KeyTypes.ReJSON]: <RejsonDetailsWrapper {...props} />,\n    [KeyTypes.Stream]: <StreamDetails {...props} />,\n  }\n\n  if (isTruncatedString(keyProp)) {\n    return <TooLongKeyNameDetails onClose={props.onCloseKey} />\n  }\n\n  // Supported key type\n  if (selectedKeyType && selectedKeyType in TypeDetails) {\n    return TypeDetails[selectedKeyType]\n  }\n\n  // Unsupported redis modules key type\n  if (\n    Object.values(ModulesKeyTypes).includes(selectedKeyType as ModulesKeyTypes)\n  ) {\n    return (\n      <ModulesTypeDetails\n        moduleName={MODULES_KEY_TYPES_NAMES[selectedKeyType]}\n        onClose={props.onCloseKey}\n      />\n    )\n  }\n\n  // Unsupported key type\n  return <UnsupportedTypeDetails onClose={props.onCloseKey} />\n}\n\nexport { DynamicTypeDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/dynamic-type-details/index.ts",
    "content": "export { DynamicTypeDetails } from './DynamicTypeDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent, act } from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { Props, HashDetails } from './HashDetails'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceOverviewSelector: jest.fn().mockReturnValue({\n    version: '7.4.2',\n  }),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    hashFieldExpiration: { flag: true },\n  }),\n}))\n\ndescribe('HashDetails', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n  it('should render', () => {\n    expect(render(<HashDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render subheader', () => {\n    render(<HashDetails {...instance(mockedProps)} />)\n    expect(screen.getByTestId('select-format-key-value')).toBeInTheDocument()\n  })\n\n  it('opens and closes the add item panel', () => {\n    render(\n      <HashDetails\n        {...instance(mockedProps)}\n        onOpenAddItemPanel={() => {}}\n        onCloseAddItemPanel={() => {}}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('add-key-value-items-btn'))\n    expect(screen.getByText('Save')).toBeInTheDocument()\n    fireEvent.click(screen.getByText('Cancel'))\n    expect(screen.queryByText('Save')).not.toBeInTheDocument()\n  })\n\n  describe('when hashFieldFeatureFlag and version higher 7.3', () => {\n    it('renders subheader with checkbox', () => {\n      render(<HashDetails {...instance(mockedProps)} />)\n      expect(screen.getByText('Show TTL')).toBeInTheDocument()\n    })\n\n    it('toggles the show TTL button', async () => {\n      render(<HashDetails {...instance(mockedProps)} />)\n      let el = screen.getByTestId('test-check-ttl') as HTMLInputElement\n      expect(el).toHaveAttribute('aria-checked', 'true')\n      // expect(el.checked).toBe(true)\n      await act(async () => {\n        fireEvent.click(el)\n      })\n      el = screen.getByTestId('test-check-ttl') as HTMLInputElement\n      expect(el).toHaveAttribute('aria-checked', 'false')\n      // expect(el.checked).toBe(false)\n    })\n\n    it('should call proper telemetry event after click on showTtl', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(<HashDetails {...instance(mockedProps)} />)\n\n      fireEvent.click(screen.getByTestId('test-check-ttl'))\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SHOW_HASH_TTL_CLICKED,\n        eventData: {\n          databaseId: INSTANCE_ID_MOCK,\n          action: 'hide',\n        },\n      })\n\n      fireEvent.click(screen.getByTestId('test-check-ttl'))\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SHOW_HASH_TTL_CLICKED,\n        eventData: {\n          databaseId: INSTANCE_ID_MOCK,\n          action: 'show',\n        },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { useParams } from 'react-router-dom'\nimport { selectedKeySelector } from 'uiSrc/slices/browser/keys'\nimport { FeatureFlags, KeyTypes } from 'uiSrc/constants'\n\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { isVersionHigherOrEquals } from 'uiSrc/utils'\nimport { CommandsVersions } from 'uiSrc/constants/commandsVersions'\nimport { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport AddHashFields from './add-hash-fields/AddHashFields'\nimport { HashDetailsTable } from './hash-details-table'\nimport { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'\nimport { AddItemsAction } from '../key-details-actions'\nimport styles from './styles.module.scss'\nimport { AddKeysContainer } from '../common/AddKeysContainer.styled'\n\nexport interface Props extends KeyDetailsHeaderProps {\n  onRemoveKey: () => void\n  onOpenAddItemPanel: () => void\n  onCloseAddItemPanel: () => void\n}\n\nconst HashDetails = (props: Props) => {\n  const keyType = KeyTypes.Hash\n  const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props\n\n  const { loading } = useSelector(selectedKeySelector)\n  const { version } = useSelector(connectedInstanceOverviewSelector)\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { [FeatureFlags.hashFieldExpiration]: hashFieldExpirationFeature } =\n    useSelector(appFeatureFlagsFeaturesSelector)\n\n  const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState<boolean>(false)\n  const [showTtl, setShowTtl] = useState<boolean>(true)\n\n  const isExpireFieldsAvailable =\n    hashFieldExpirationFeature?.flag &&\n    isVersionHigherOrEquals(version, CommandsVersions.HASH_TTL.since)\n\n  const openAddItemPanel = () => {\n    setIsAddItemPanelOpen(true)\n    onOpenAddItemPanel()\n  }\n\n  const closeAddItemPanel = (isCancelled?: boolean) => {\n    setIsAddItemPanelOpen(false)\n    if (isCancelled) {\n      onCloseAddItemPanel()\n    }\n  }\n  const handleSelectShow = (show: boolean) => {\n    setShowTtl(show)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SHOW_HASH_TTL_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        action: show ? 'show' : 'hide',\n      },\n    })\n  }\n\n  const Actions = ({ width }: { width: number }) => (\n    <>\n      {isExpireFieldsAvailable && (\n        <>\n          <Checkbox\n            id=\"showTtl\"\n            name=\"showTtl\"\n            label=\"Show TTL\"\n            checked={showTtl}\n            onChange={(e) => handleSelectShow(e.target.checked)}\n            data-testid=\"test-check-ttl\"\n          />\n          <Divider className={styles.divider} orientation=\"vertical\" />\n        </>\n      )}\n      <AddItemsAction\n        title=\"Add Fields\"\n        width={width}\n        openAddItemPanel={openAddItemPanel}\n      />\n    </>\n  )\n  return (\n    <div className=\"fluid flex-column relative\">\n      <KeyDetailsHeader {...props} key=\"key-details-header\" />\n      <KeyDetailsSubheader keyType={keyType} Actions={Actions} />\n      <div className=\"key-details-body\" key=\"key-details-body\">\n        {!loading && (\n          <div className=\"flex-column\" style={{ flex: '1', height: '100%' }}>\n            <HashDetailsTable\n              isExpireFieldsAvailable={isExpireFieldsAvailable && showTtl}\n              onRemoveKey={onRemoveKey}\n            />\n          </div>\n        )}\n        {isAddItemPanelOpen && (\n          <AddKeysContainer>\n            <AddHashFields\n              isExpireFieldsAvailable={isExpireFieldsAvailable}\n              closePanel={closeAddItemPanel}\n            />\n          </AddKeysContainer>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { HashDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport AddHashFields, { Props } from './AddHashFields'\n\nconst HASH_FIELD = 'hash-field'\nconst HASH_VALUE = 'hash-value'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddHashFields', () => {\n  it('should render', () => {\n    expect(render(<AddHashFields {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set field name properly', () => {\n    render(<AddHashFields {...instance(mockedProps)} />)\n    const fieldName = screen.getByTestId(HASH_FIELD)\n    fireEvent.change(fieldName, { target: { value: 'field name' } })\n    expect(fieldName).toHaveValue('field name')\n  })\n\n  it('should set field value properly', () => {\n    render(<AddHashFields {...instance(mockedProps)} />)\n    const fieldName = screen.getByTestId(HASH_VALUE)\n    fireEvent.change(fieldName, { target: { value: '123' } })\n    expect(fieldName).toHaveValue('123')\n  })\n\n  it('should render add button', () => {\n    render(<AddHashFields {...instance(mockedProps)} />)\n    expect(screen.getByTestId('add-item')).toBeTruthy()\n  })\n\n  it('should render one more field name & value inputs after click add item', () => {\n    render(<AddHashFields {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('add-item'))\n\n    expect(screen.getAllByTestId(HASH_FIELD)).toHaveLength(2)\n    expect(screen.getAllByTestId(HASH_VALUE)).toHaveLength(2)\n  })\n\n  it('should clear fieldName & fieldValue after click clear button', () => {\n    render(<AddHashFields {...instance(mockedProps)} />)\n    const fieldName = screen.getByTestId(HASH_FIELD)\n    const fieldValue = screen.getByTestId(HASH_VALUE)\n    fireEvent.change(fieldName, { target: { value: 'name' } })\n    fireEvent.change(fieldValue, { target: { value: 'val' } })\n    fireEvent.click(screen.getByTestId('remove-item'))\n\n    expect(fieldName).toHaveValue('')\n    expect(fieldValue).toHaveValue('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/add-hash-fields/AddHashFields.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { toNumber } from 'lodash'\nimport {\n  keysSelector,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  addHashFieldsAction,\n  resetUpdateValue,\n  updateHashValueStateSelector,\n} from 'uiSrc/slices/browser/hash'\nimport { KeyTypes } from 'uiSrc/constants'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\n\nimport { stringToBuffer, validateTTLNumberForAddKey } from 'uiSrc/utils'\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\nimport {\n  IHashFieldState,\n  INITIAL_HASH_FIELD_STATE,\n} from 'uiSrc/pages/browser/components/add-key/AddKeyHash/interfaces'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport {\n  AddFieldsToHashDto,\n  HashFieldDto,\n} from 'apiSrc/modules/browser/hash/dto'\n\nimport { EntryContent } from '../../common/AddKeysContainer.styled'\n\nexport interface Props {\n  isExpireFieldsAvailable?: boolean\n  closePanel: (isCancelled?: boolean) => void\n}\n\nconst AddHashFields = (props: Props) => {\n  const { isExpireFieldsAvailable, closePanel } = props\n  const dispatch = useDispatch()\n  const [fields, setFields] = useState<IHashFieldState[]>([\n    { ...INITIAL_HASH_FIELD_STATE },\n  ])\n  const { loading } = useSelector(updateHashValueStateSelector)\n  const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: undefined,\n  }\n  const { viewType } = useSelector(keysSelector)\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const lastAddedFieldName = useRef<HTMLInputElement>(null)\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        dispatch(resetUpdateValue())\n      },\n    [],\n  )\n\n  useEffect(() => {\n    lastAddedFieldName.current?.focus()\n  }, [fields.length])\n\n  const addField = () => {\n    const lastField = fields[fields.length - 1]\n    const newState = [\n      ...fields,\n      {\n        ...INITIAL_HASH_FIELD_STATE,\n        id: lastField.id + 1,\n      },\n    ]\n    setFields(newState)\n  }\n\n  const removeField = (id: number) => {\n    const newState = fields.filter((item) => item.id !== id)\n    setFields(newState)\n  }\n\n  const clearFieldsValues = (id: number) => {\n    const newState = fields.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            fieldName: '',\n            fieldValue: '',\n            fieldTTL: undefined,\n          }\n        : item,\n    )\n    setFields(newState)\n  }\n\n  const onClickRemove = ({ id }: IHashFieldState) => {\n    if (fields.length === 1) {\n      clearFieldsValues(id)\n      return\n    }\n\n    removeField(id)\n  }\n\n  const onSuccessAdded = () => {\n    closePanel()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_ADDED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_ADDED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Hash,\n        numberOfAdded: fields.length,\n      },\n    })\n  }\n\n  const handleFieldChange = (formField: string, id: number, value: any) => {\n    const newState = fields.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: value,\n        }\n      }\n      return item\n    })\n    setFields(newState)\n  }\n\n  const submitData = (): void => {\n    const data: AddFieldsToHashDto = {\n      keyName: selectedKey,\n      fields: fields.map((item) => {\n        const defaultFields: HashFieldDto = {\n          field: stringToBuffer(item.fieldName),\n          value: stringToBuffer(item.fieldValue),\n        }\n\n        if (isExpireFieldsAvailable && item.fieldTTL) {\n          defaultFields.expire = toNumber(item.fieldTTL)\n        }\n\n        return defaultFields\n      }),\n    }\n    dispatch(addHashFieldsAction(data, onSuccessAdded))\n  }\n\n  const isClearDisabled = (item: IHashFieldState): boolean =>\n    fields.length === 1 &&\n    !(item.fieldName.length || item.fieldValue.length || item.fieldTTL?.length)\n\n  return (\n    <Col gap=\"m\">\n      <EntryContent>\n        <AddMultipleFields\n          items={fields}\n          isClearDisabled={isClearDisabled}\n          onClickRemove={onClickRemove}\n          onClickAdd={addField}\n        >\n          {(item, index) => (\n            <Row align=\"center\" gap=\"m\">\n              <FlexItem grow={2}>\n                <FormField>\n                  <TextInput\n                    name={`fieldName-${item.id}`}\n                    id={`fieldName-${item.id}`}\n                    placeholder=\"Enter Field\"\n                    value={item.fieldName}\n                    disabled={loading}\n                    onChange={(value) =>\n                      handleFieldChange('fieldName', item.id, value)\n                    }\n                    ref={\n                      index === fields.length - 1 ? lastAddedFieldName : null\n                    }\n                    data-testid=\"hash-field\"\n                  />\n                </FormField>\n              </FlexItem>\n              <FlexItem grow={2}>\n                <FormField>\n                  <TextInput\n                    name={`fieldValue-${item.id}`}\n                    id={`fieldValue-${item.id}`}\n                    placeholder=\"Enter Value\"\n                    value={item.fieldValue}\n                    disabled={loading}\n                    onChange={(value) =>\n                      handleFieldChange('fieldValue', item.id, value)\n                    }\n                    data-testid=\"hash-value\"\n                  />\n                </FormField>\n              </FlexItem>\n              {isExpireFieldsAvailable && (\n                <FlexItem grow={1}>\n                  <FormField>\n                    <TextInput\n                      name={`fieldTTL-${item.id}`}\n                      id={`fieldTTL-${item.id}`}\n                      placeholder=\"Enter TTL\"\n                      value={item.fieldTTL || ''}\n                      disabled={loading}\n                      onChange={(value) =>\n                        handleFieldChange(\n                          'fieldTTL',\n                          item.id,\n                          validateTTLNumberForAddKey(value),\n                        )\n                      }\n                      data-testid=\"hash-ttl\"\n                    />\n                  </FormField>\n                </FlexItem>\n              )}\n            </Row>\n          )}\n        </AddMultipleFields>\n      </EntryContent>\n      <Row justify=\"end\" gap=\"m\">\n        <FlexItem>\n          <div>\n            <SecondaryButton\n              onClick={() => closePanel(true)}\n              data-testid=\"cancel-fields-btn\"\n            >\n              Cancel\n            </SecondaryButton>\n          </div>\n        </FlexItem>\n        <FlexItem>\n          <div>\n            <PrimaryButton\n              disabled={loading}\n              loading={loading}\n              onClick={submitData}\n              data-testid=\"save-fields-btn\"\n            >\n              Save\n            </PrimaryButton>\n          </div>\n        </FlexItem>\n      </Row>\n    </Col>\n  )\n}\n\nexport default AddHashFields\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  KeyValueCompressor,\n  KeyValueFormat,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n  TEXT_DISABLED_COMPRESSED_VALUE,\n  TEXT_DISABLED_FORMATTER_EDITING,\n} from 'uiSrc/constants'\nimport { hashDataSelector } from 'uiSrc/slices/browser/hash'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { anyToBuffer, bufferToString } from 'uiSrc/utils'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  GZIP_COMPRESSED_VALUE_2,\n  DECOMPRESSED_VALUE_STR_1,\n  DECOMPRESSED_VALUE_STR_2,\n} from 'uiSrc/utils/tests/decompressors'\nimport {\n  setSelectedKeyRefreshDisabled,\n  selectedKeySelector,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  MOCK_TRUNCATED_BUFFER_VALUE,\n  MOCK_TRUNCATED_STRING_VALUE,\n} from 'uiSrc/mocks/data/bigString'\nimport { HashDetailsTable, Props } from './HashDetailsTable'\n\nconst mockedProps = mock<Props>()\nconst fields: Array<{ field: any; value: any; expire?: number }> = [\n  {\n    field: { type: 'Buffer', data: [49] },\n    value: { type: 'Buffer', data: [49, 65] },\n  },\n  {\n    field: { type: 'Buffer', data: [49, 50, 51] },\n    value: { type: 'Buffer', data: [49, 11] },\n  },\n  {\n    field: { type: 'Buffer', data: [50] },\n    value: { type: 'Buffer', data: [49, 234, 453] },\n    expire: 300,\n  },\n]\n\njest.mock('uiSrc/slices/browser/hash', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/browser/hash',\n  ).initialState\n  return {\n    hashSelector: jest.fn().mockReturnValue(defaultState),\n    updateHashValueStateSelector: jest\n      .fn()\n      .mockReturnValue(defaultState.updateValue),\n    hashDataSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      total: 3,\n      key: '123zxczxczxc',\n      fields,\n    }),\n    setHashInitialState: jest.fn,\n    fetchHashFields: () => jest.fn,\n  }\n})\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    compressor: null,\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeySelector: jest.fn().mockReturnValue({\n    viewFormat: 'Unicode',\n    lastRefreshTime: Date.now(),\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  jest.clearAllMocks()\n})\n\ndescribe('HashDetailsTable', () => {\n  it('should render', () => {\n    expect(render(<HashDetailsTable {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render rows properly', () => {\n    const { container } = render(\n      <HashDetailsTable {...instance(mockedProps)} />,\n    )\n    const rows = container.querySelectorAll(\n      '.ReactVirtualized__Table__row[role=\"row\"]',\n    )\n    expect(rows).toHaveLength(fields.length)\n  })\n\n  it('should render search input', () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n    expect(screen.getByTestId('search')).toBeTruthy()\n  })\n\n  it('should call search', () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, { target: { value: '*1*' } })\n    expect(searchInput).toHaveValue('*1*')\n  })\n\n  it('should render delete popup after click remove button', () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n    fireEvent.click(screen.getAllByTestId(/remove-hash-button/)[0])\n    expect(\n      screen.getByTestId(\n        `remove-hash-button-${bufferToString(fields[0].field)}-icon`,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('should render editor after click edit button', () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('hash_content-value-1'))\n    })\n\n    fireEvent.click(screen.getByTestId('hash_edit-btn-1'))\n    expect(screen.getByTestId('hash_value-editor-1')).toBeInTheDocument()\n  })\n\n  it('should render resize trigger for field column', () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n    expect(screen.getByTestId('resize-trigger-field')).toBeInTheDocument()\n  })\n\n  it('should disable refresh after click on edit', async () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('hash_content-value-1'))\n    })\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('hash_edit-btn-1'))\n    })\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  it('should not render ttl column', async () => {\n    render(<HashDetailsTable {...instance(mockedProps)} />)\n    expect(\n      screen.queryByTestId('hash-ttl_content-value-2'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render ttl column', async () => {\n    render(\n      <HashDetailsTable {...instance(mockedProps)} isExpireFieldsAvailable />,\n    )\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('hash-ttl_content-value-2'))\n    })\n\n    act(() => {\n      fireEvent.click(screen.getByTestId('hash-ttl_edit-btn-2'))\n    })\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  const nonEditableFormats = [KeyValueFormat.HEX, KeyValueFormat.Binary]\n\n  test.each(nonEditableFormats)(\n    'should disable edit button when viewFormat is not editable for format: %s',\n    async (format) => {\n      ;(selectedKeySelector as jest.Mock).mockReturnValueOnce({\n        viewFormat: format,\n        lastRefreshTime: Date.now(),\n      })\n\n      render(<HashDetailsTable {...instance(mockedProps)} />)\n\n      act(() => {\n        fireEvent.mouseEnter(screen.getByTestId('hash_content-value-1'))\n      })\n\n      const editBtn = screen.getByTestId('hash_edit-btn-1')\n      expect(editBtn).toBeDisabled()\n\n      act(() => {\n        fireEvent.focus(editBtn)\n      })\n\n      await waitForRiTooltipVisible()\n      expect(screen.getByTestId('hash_edit-tooltip-1')).toHaveTextContent(\n        TEXT_DISABLED_FORMATTER_EDITING,\n      )\n    },\n  )\n\n  describe('decompressed  data', () => {\n    it('should render decompressed GZIP data', () => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/hash',\n      ).initialState\n      const hashDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        total: 1,\n        key: '123zxczxczxc',\n        fields: [\n          {\n            field: anyToBuffer(GZIP_COMPRESSED_VALUE_1),\n            value: anyToBuffer(GZIP_COMPRESSED_VALUE_2),\n          },\n        ],\n      })\n      ;(hashDataSelector as jest.Mock).mockImplementation(hashDataSelectorMock)\n\n      const { queryAllByTestId } = render(\n        <HashDetailsTable {...instance(mockedProps)} />,\n      )\n      const fieldEl = queryAllByTestId(/hash-field-/)?.[0]\n      const valueEl = queryAllByTestId(/hash_content-value/)?.[0]\n\n      expect(fieldEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1)\n      expect(valueEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_2)\n    })\n\n    it('edit button should be disabled if data was compressed', async () => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/hash',\n      ).initialState\n      const hashDataSelectorMock = jest.fn().mockReturnValueOnce({\n        ...defaultState,\n        total: 1,\n        key: '123zxczxczxc',\n        fields: [\n          {\n            field: anyToBuffer(GZIP_COMPRESSED_VALUE_1),\n            value: anyToBuffer(GZIP_COMPRESSED_VALUE_2),\n          },\n        ],\n      })\n      ;(hashDataSelector as jest.Mock).mockImplementationOnce(\n        hashDataSelectorMock,\n      )\n      ;(connectedInstanceSelector as jest.Mock).mockImplementationOnce(() => ({\n        compressor: KeyValueCompressor.GZIP,\n      }))\n\n      const { queryByTestId } = render(\n        <HashDetailsTable {...instance(mockedProps)} />,\n      )\n\n      act(() => {\n        fireEvent.mouseEnter(screen.getByTestId('hash_content-value-1'))\n      })\n\n      const editBtn = screen.getByTestId('hash_edit-btn-1')\n      fireEvent.click(editBtn)\n\n      await act(async () => {\n        fireEvent.focus(editBtn)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(editBtn).toBeDisabled()\n      expect(screen.getByTestId('hash_edit-tooltip-1')).toHaveTextContent(\n        TEXT_DISABLED_COMPRESSED_VALUE,\n      )\n      expect(queryByTestId('hash_value-editor-1')).not.toBeInTheDocument()\n    })\n  })\n\n  describe('truncated values', () => {\n    beforeEach(() => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/hash',\n      ).initialState\n      const hashDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        total: 2,\n        key: '123zxczxczxc',\n        fields: [\n          { field: 'regular-field', value: MOCK_TRUNCATED_BUFFER_VALUE },\n          { field: MOCK_TRUNCATED_BUFFER_VALUE, value: 'regular-value' },\n        ],\n      })\n      ;(hashDataSelector as jest.Mock).mockImplementation(hashDataSelectorMock)\n    })\n\n    it('should disable delete entry when field name is truncated', async () => {\n      render(<HashDetailsTable {...instance(mockedProps)} />)\n\n      // check delete actions\n      const removeHashButtons = screen.getAllByTestId(\n        /remove-hash-button.+-icon$/,\n      )\n      expect(removeHashButtons.length).toEqual(2)\n      expect(removeHashButtons[0]).toBeEnabled()\n      expect(removeHashButtons[1]).toBeDisabled()\n\n      // button with disabled removing\n      await act(async () => {\n        fireEvent.focus(removeHashButtons[1])\n      })\n      await waitForRiTooltipVisible()\n      expect(\n        screen.getByTestId(\n          `remove-hash-button-${MOCK_TRUNCATED_STRING_VALUE}-tooltip`,\n        ),\n      ).toHaveTextContent(TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA)\n    })\n\n    it('should disable editing value when entry value is truncated', async () => {\n      const { queryByTestId } = render(\n        <HashDetailsTable {...instance(mockedProps)} />,\n      )\n\n      const hashValue = screen.getByTestId('hash_content-value-regular-field')\n\n      await act(async () => {\n        fireEvent.mouseEnter(hashValue)\n      })\n\n      const editButton = screen.getByTestId('hash_edit-btn-regular-field')\n      expect(editButton).toBeDisabled()\n\n      await act(async () => {\n        fireEvent.focus(editButton)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(\n        screen.getByTestId('hash_edit-tooltip-regular-field'),\n      ).toHaveTextContent(TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA)\n\n      fireEvent.click(editButton)\n      expect(\n        queryByTestId('hash_value-editor-regular-field'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should disable editing value when entry field is truncated', async () => {\n      const { queryByTestId } = render(\n        <HashDetailsTable {...instance(mockedProps)} />,\n      )\n\n      // check edit action\n      const hashValue = screen.getByTestId(\n        `hash_content-value-${MOCK_TRUNCATED_STRING_VALUE}`,\n      )\n\n      await act(async () => {\n        fireEvent.mouseEnter(hashValue)\n      })\n\n      const editButton = screen.getByTestId(\n        `hash_edit-btn-${MOCK_TRUNCATED_STRING_VALUE}`,\n      )\n      expect(editButton).toBeDisabled()\n\n      await act(async () => {\n        fireEvent.focus(editButton)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(\n        screen.getByTestId(`hash_edit-tooltip-${MOCK_TRUNCATED_STRING_VALUE}`),\n      ).toHaveTextContent(TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA)\n\n      fireEvent.click(editButton)\n      expect(\n        queryByTestId(`hash_value-editor-${MOCK_TRUNCATED_STRING_VALUE}`),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should disable editing ttl when entry field is truncated', async () => {\n      render(\n        <HashDetailsTable {...instance(mockedProps)} isExpireFieldsAvailable />,\n      )\n\n      const ttl = screen.getByTestId(\n        `hash-ttl_content-value-${MOCK_TRUNCATED_STRING_VALUE}`,\n      )\n\n      await act(async () => {\n        fireEvent.mouseEnter(ttl)\n      })\n\n      const editTtlButton = screen.getByTestId(\n        `hash-ttl_edit-btn-${MOCK_TRUNCATED_STRING_VALUE}`,\n      )\n\n      expect(editTtlButton).toBeDisabled()\n\n      await act(async () => {\n        fireEvent.focus(editTtlButton)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(\n        screen.getByTestId(\n          `hash-ttl_edit-tooltip-${MOCK_TRUNCATED_STRING_VALUE}`,\n        ),\n      ).toHaveTextContent(TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/HashDetailsTable.tsx",
    "content": "import cx from 'classnames'\nimport React, { Ref, useCallback, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { CellMeasurerCache } from 'react-virtualized'\n\nimport { isNumber, toNumber } from 'lodash'\nimport { Text } from 'uiSrc/components/base/text'\nimport { getColumnWidth } from 'uiSrc/components/virtual-grid'\nimport { StopPropagation } from 'uiSrc/components/virtual-table'\nimport {\n  IColumnSearchState,\n  ITableColumn,\n  RelativeWidthSizes,\n} from 'uiSrc/components/virtual-table/interfaces'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport {\n  KeyTypes,\n  OVER_RENDER_BUFFER_COUNT,\n  TableCellAlignment,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n  TEXT_DISABLED_COMPRESSED_VALUE,\n  TEXT_DISABLED_FORMATTER_EDITING,\n  TEXT_FAILED_CONVENT_FORMATTER,\n  TEXT_INVALID_VALUE,\n  TEXT_UNPRINTABLE_CHARACTERS,\n} from 'uiSrc/constants'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport HelpTexts from 'uiSrc/constants/help-texts'\nimport { NoResultsFoundText } from 'uiSrc/constants/texts'\nimport {\n  appContextBrowserKeyDetails,\n  updateKeyDetailsSizes,\n} from 'uiSrc/slices/app/context'\n\nimport {\n  deleteHashFields,\n  fetchHashFields,\n  fetchMoreHashFields,\n  hashDataSelector,\n  hashSelector,\n  updateHashFieldsAction,\n  updateHashTTLAction,\n  updateHashValueStateSelector,\n} from 'uiSrc/slices/browser/hash'\nimport {\n  keysSelector,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n  setSelectedKeyRefreshDisabled,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\nimport {\n  getBasedOnViewTypeEvent,\n  getMatchType,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  bufferToSerializedFormat,\n  bufferToString,\n  createDeleteFieldHeader,\n  createDeleteFieldMessage,\n  createTooltipContent,\n  formattingBuffer,\n  isTruncatedString,\n  isEqualBuffers,\n  isFormatEditable,\n  isNonUnicodeFormatter,\n  Nullable,\n  stringToSerializedBufferFormat,\n  truncateNumberToDuration,\n  validateTTLNumber,\n} from 'uiSrc/utils'\nimport { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters'\nimport { decompressingBuffer } from 'uiSrc/utils/decompressors'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport {\n  EditableInput,\n  EditableTextArea,\n  FormattedValue,\n} from 'uiSrc/pages/browser/modules/key-details/shared'\nimport { RiTooltip } from 'uiSrc/components'\nimport {\n  AddFieldsToHashDto,\n  GetHashFieldsResponse,\n  HashFieldDto,\n  UpdateHashFieldsTtlDto,\n} from 'apiSrc/modules/browser/hash/dto'\n\nimport styles from './styles.module.scss'\n\nconst suffix = '_hash'\nconst matchAllValue = '*'\nconst headerHeight = 60\nconst rowHeight = 43\n\nconst cellCache = new CellMeasurerCache({\n  fixedWidth: true,\n  minHeight: rowHeight,\n})\n\ninterface IHashField extends HashFieldDto {}\n\nexport interface Props {\n  isExpireFieldsAvailable?: boolean\n  onRemoveKey: () => void\n}\n\nconst HashDetailsTable = (props: Props) => {\n  const { isExpireFieldsAvailable, onRemoveKey } = props\n\n  const {\n    total,\n    nextCursor,\n    fields: loadedFields,\n  } = useSelector(hashDataSelector)\n  const { loading } = useSelector(hashSelector)\n  const { viewType } = useSelector(keysSelector)\n  const { id: instanceId, compressor = null } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewFormat: viewFormatProp, lastRefreshTime } =\n    useSelector(selectedKeySelector)\n  const { name: key, length } = useSelector(selectedKeyDataSelector) ?? {\n    name: '',\n  }\n  const { loading: updateLoading } = useSelector(updateHashValueStateSelector)\n  const { [KeyTypes.Hash]: hashSizes } = useSelector(\n    appContextBrowserKeyDetails,\n  )\n\n  const [match, setMatch] = useState<Nullable<string>>(matchAllValue)\n  const [deleting, setDeleting] = useState('')\n  const [fields, setFields] = useState<IHashField[]>([])\n  const [editingIndex, setEditingIndex] =\n    useState<Nullable<{ index: number; field: string }>>(null)\n  const [width, setWidth] = useState(100)\n  const [expandedRows, setExpandedRows] = useState<number[]>([])\n  const [viewFormat, setViewFormat] = useState(viewFormatProp)\n\n  const formattedLastIndexRef = useRef(OVER_RENDER_BUFFER_COUNT)\n  const tableRef: Ref<any> = useRef(null)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    resetState()\n  }, [lastRefreshTime])\n\n  useEffect(() => {\n    setFields(loadedFields)\n\n    if (loadedFields.length < fields.length) {\n      formattedLastIndexRef.current = 0\n    }\n\n    if (viewFormat !== viewFormatProp) {\n      resetState()\n    }\n  }, [loadedFields, viewFormatProp])\n\n  const resetState = () => {\n    setExpandedRows([])\n    setViewFormat(viewFormatProp)\n    setEditingIndex(null)\n    dispatch(setSelectedKeyRefreshDisabled(false))\n\n    clearCache()\n  }\n\n  const clearCache = (rowIndex?: number) => {\n    if (isNumber(rowIndex)) {\n      cellCache.clear(rowIndex, 1)\n      tableRef.current?.recomputeRowHeights(rowIndex)\n      return\n    }\n\n    cellCache.clearAll()\n  }\n\n  const closePopover = useCallback(() => {\n    setDeleting('')\n  }, [])\n\n  const showPopover = useCallback((field = '') => {\n    setDeleting(`${field + suffix}`)\n  }, [])\n\n  const onSuccessRemoved = (newTotalValue: number) => {\n    newTotalValue === 0 && onRemoveKey()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Hash,\n        numberOfRemoved: 1,\n      },\n    })\n  }\n\n  const handleDeleteField = (field: RedisString | string = '') => {\n    dispatch(deleteHashFields(key, [field], onSuccessRemoved))\n    closePopover()\n  }\n\n  const handleEditField = useCallback(\n    (index: number, editing: boolean, field: string) => {\n      setEditingIndex(editing ? { index, field } : null)\n      dispatch(setSelectedKeyRefreshDisabled(editing))\n\n      clearCache(index)\n    },\n    [viewFormat],\n  )\n\n  const handleApplyEditValue = (\n    field = '',\n    value: string,\n    rowIndex: number,\n  ) => {\n    const data: AddFieldsToHashDto = {\n      keyName: key,\n      fields: [\n        { field, value: stringToSerializedBufferFormat(viewFormat, value) },\n      ],\n    }\n\n    dispatch(\n      updateHashFieldsAction(data, () =>\n        handleEditField(rowIndex, false, 'value'),\n      ),\n    )\n  }\n\n  const handleApplyEditExpire = (\n    field = '',\n    expire: string,\n    rowIndex: number,\n  ) => {\n    const data: UpdateHashFieldsTtlDto = {\n      keyName: key,\n      fields: [{ field, expire: expire ? toNumber(expire) : -1 }],\n    }\n\n    dispatch(\n      updateHashTTLAction(data, () => handleEditField(rowIndex, false, 'ttl')),\n    )\n  }\n\n  const handleRemoveIconClick = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Hash,\n      },\n    })\n  }\n\n  const handleSearch = (search: IColumnSearchState[]) => {\n    const fieldColumn = search.find((column) => column.id === 'field')\n    if (fieldColumn) {\n      const { value: match } = fieldColumn\n      const onSuccess = (data: GetHashFieldsResponse) => {\n        const matchValue = getMatchType(match)\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_FILTERED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_FILTERED,\n          ),\n          eventData: {\n            databaseId: instanceId,\n            keyType: KeyTypes.Hash,\n            match: matchValue,\n            length: data.total,\n          },\n        })\n      }\n      setMatch(match)\n      dispatch(\n        fetchHashFields(\n          key,\n          0,\n          SCAN_COUNT_DEFAULT,\n          match || matchAllValue,\n          true,\n          onSuccess,\n        ),\n      )\n    }\n  }\n\n  const handleRowToggleViewClick = (expanded: boolean, rowIndex: number) => {\n    const browserViewEvent = expanded\n      ? TelemetryEvent.BROWSER_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.BROWSER_KEY_FIELD_VALUE_COLLAPSED\n    const treeViewEvent = expanded\n      ? TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED\n\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        keyType: KeyTypes.Hash,\n        databaseId: instanceId,\n        largestCellLength:\n          Math.max(\n            ...Object.values(fields[rowIndex]).map((a) => a.toString().length),\n          ) || 0,\n      },\n    })\n\n    cellCache.clearAll()\n  }\n\n  const loadMoreItems = () => {\n    if (nextCursor !== 0) {\n      dispatch(\n        fetchMoreHashFields(\n          key as RedisResponseBuffer,\n          nextCursor,\n          SCAN_COUNT_DEFAULT,\n          match || matchAllValue,\n        ),\n      )\n    }\n  }\n\n  const onColResizeEnd = (sizes: RelativeWidthSizes) => {\n    dispatch(\n      updateKeyDetailsSizes({\n        type: KeyTypes.Hash,\n        sizes,\n      }),\n    )\n  }\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'field',\n      label: 'Field',\n      isSearchable: true,\n      isResizable: true,\n      minWidth: 120,\n      relativeWidth: hashSizes?.field || 40,\n      prependSearchName: 'Field:',\n      initialSearchValue: '',\n      truncateText: true,\n      alignment: TableCellAlignment.Left,\n      className: 'value-table-separate-border',\n      headerClassName: 'value-table-separate-border',\n      render: (\n        _name: string,\n        { field: fieldItem }: HashFieldDto,\n        expanded?: boolean,\n      ) => {\n        const { value: decompressedItem } = decompressingBuffer(\n          fieldItem,\n          compressor,\n        )\n        const field = bufferToString(fieldItem) || ''\n        const { value, isValid } = formattingBuffer(\n          decompressedItem,\n          viewFormatProp,\n          { expanded, skipVector: true },\n        )\n        const tooltipContent = createTooltipContent(\n          value,\n          decompressedItem,\n          viewFormatProp,\n          { skipVector: true },\n        )\n\n        return (\n          <Text\n            color=\"secondary\"\n            style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}\n            component=\"div\"\n          >\n            <div\n              style={{ display: 'flex' }}\n              data-testid={`hash-field-${field}`}\n            >\n              <FormattedValue\n                value={value}\n                expanded={expanded}\n                title={\n                  isValid\n                    ? 'Field'\n                    : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n                }\n                tooltipContent={tooltipContent}\n              />\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'value',\n      label: 'Value',\n      minWidth: 120,\n      truncateText: true,\n      alignment: TableCellAlignment.Left,\n      className: 'noPadding',\n      render: function Value(\n        _name: string,\n        { field: fieldItem, value: valueItem }: IHashField,\n        expanded?: boolean,\n        rowIndex = 0,\n      ) {\n        const { value: decompressedFieldItem } = decompressingBuffer(\n          fieldItem,\n          compressor,\n        )\n        const { value: decompressedValueItem, isCompressed } =\n          decompressingBuffer(valueItem, compressor)\n        const isTruncatedFieldOrValue =\n          isTruncatedString(valueItem) || isTruncatedString(fieldItem)\n        const value = bufferToString(valueItem)\n        const field = bufferToString(decompressedFieldItem)\n        const { value: formattedValue, isValid } = formattingBuffer(\n          decompressedValueItem,\n          viewFormatProp,\n          { expanded },\n        )\n        const tooltipContent = createTooltipContent(\n          formattedValue,\n          decompressedValueItem,\n          viewFormatProp,\n        )\n        const disabled =\n          !isNonUnicodeFormatter(viewFormat, isValid) &&\n          !isEqualBuffers(valueItem, stringToBuffer(value))\n        const isEditable =\n          !isCompressed &&\n          isFormatEditable(viewFormat) &&\n          !isTruncatedFieldOrValue\n        const editTooltipContent = isCompressed\n          ? TEXT_DISABLED_COMPRESSED_VALUE\n          : isTruncatedFieldOrValue\n            ? TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA\n            : TEXT_DISABLED_FORMATTER_EDITING\n        const isEditing =\n          editingIndex?.field === 'value' && editingIndex?.index === rowIndex\n\n        const serializedValue = isEditing\n          ? bufferToSerializedFormat(viewFormat, valueItem, 4)\n          : ''\n\n        return (\n          <EditableTextArea\n            initialValue={serializedValue}\n            isLoading={updateLoading}\n            isDisabled={disabled}\n            isEditing={isEditing}\n            isEditDisabled={!isEditable || updateLoading}\n            disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS}\n            onDecline={() => handleEditField(rowIndex, false, 'value')}\n            onApply={(value) =>\n              handleApplyEditValue(fieldItem, value, rowIndex)\n            }\n            approveText={TEXT_INVALID_VALUE}\n            approveByValidation={(value) =>\n              formattingBuffer(\n                stringToSerializedBufferFormat(viewFormat, value),\n                viewFormat,\n              )?.isValid\n            }\n            onEdit={(isEditing) =>\n              handleEditField(rowIndex, isEditing, 'value')\n            }\n            editToolTipContent={!isEditable ? editTooltipContent : null}\n            onUpdateTextAreaHeight={() => clearCache(rowIndex)}\n            field={field}\n            testIdPrefix=\"hash\"\n          >\n            <div className=\"innerCellAsCell\">\n              <FormattedValue\n                value={formattedValue}\n                expanded={expanded}\n                title={\n                  isValid\n                    ? 'Value'\n                    : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n                }\n                tooltipContent={tooltipContent}\n              />\n            </div>\n          </EditableTextArea>\n        )\n      },\n    },\n    {\n      id: 'actions',\n      label: '',\n      className: 'actions singleAction',\n      absoluteWidth: 40,\n      minWidth: 40,\n      maxWidth: 40,\n      render: function Actions(_act: any, { field: fieldItem }: HashFieldDto) {\n        const field = bufferToString(fieldItem, viewFormat)\n        return (\n          <StopPropagation>\n            <PopoverDelete\n              header={createDeleteFieldHeader(fieldItem as RedisString)}\n              text={createDeleteFieldMessage(key ?? '')}\n              item={field}\n              itemRaw={fieldItem as RedisString}\n              suffix={suffix}\n              deleting={deleting}\n              closePopover={closePopover}\n              updateLoading={updateLoading}\n              showPopover={showPopover}\n              testid={`remove-hash-button-${field}`}\n              handleDeleteItem={handleDeleteField}\n              handleButtonClick={handleRemoveIconClick}\n              appendInfo={length === 1 ? HelpTexts.REMOVE_LAST_ELEMENT() : null}\n            />\n          </StopPropagation>\n        )\n      },\n    },\n  ]\n\n  if (isExpireFieldsAvailable) {\n    columns.splice(2, 0, {\n      id: 'ttl',\n      label: 'TTL',\n      absoluteWidth: 140,\n      minWidth: 140,\n      truncateText: true,\n      className: 'noPadding',\n      render: function TTL(\n        _name: string,\n        { field: fieldItem, expire }: IHashField,\n        _expanded?: boolean,\n        rowIndex = 0,\n      ) {\n        const field = bufferToString(fieldItem, viewFormat)\n        const isEditing =\n          editingIndex?.field === 'ttl' && editingIndex?.index === rowIndex\n        const isTruncatedFieldName = isTruncatedString(fieldItem)\n        const editTooltipContent = isTruncatedFieldName\n          ? TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA\n          : null\n\n        return (\n          <EditableInput\n            initialValue={expire === -1 ? '' : expire?.toString()}\n            placeholder=\"Enter TTL\"\n            field={field}\n            isEditing={isEditing}\n            onEdit={(value: boolean) => handleEditField(rowIndex, value, 'ttl')}\n            onDecline={() => handleEditField(rowIndex, false, 'ttl')}\n            onApply={(value) => handleApplyEditExpire(fieldItem, value, 'ttl')}\n            testIdPrefix=\"hash-ttl\"\n            validation={validateTTLNumber}\n            variant=\"underline\"\n            isEditDisabled={isTruncatedFieldName}\n            editToolTipContent={editTooltipContent}\n          >\n            <div className=\"innerCellAsCell\">\n              {expire === -1 ? (\n                'No Limit'\n              ) : (\n                <RiTooltip\n                  title=\"Time to Live\"\n                  className={styles.tooltip}\n                  anchorClassName=\"truncateText\"\n                  position=\"right\"\n                  content={truncateNumberToDuration(expire || 0)}\n                >\n                  <>{expire}</>\n                </RiTooltip>\n              )}\n            </div>\n          </EditableInput>\n        )\n      },\n    })\n  }\n\n  return (\n    <>\n      <div\n        data-testid=\"hash-details\"\n        className={cx(\n          'key-details-table',\n          'hash-fields-container',\n          styles.container,\n        )}\n      >\n        <VirtualTable\n          expandable\n          autoHeight\n          tableRef={tableRef}\n          keyName={key as RedisResponseBuffer}\n          headerHeight={headerHeight}\n          rowHeight={rowHeight}\n          onChangeWidth={setWidth}\n          columns={columns.map((column, i, arr) => ({\n            ...column,\n            width: getColumnWidth(i, width, arr),\n          }))}\n          footerHeight={0}\n          overscanRowCount={10}\n          loadMoreItems={loadMoreItems}\n          loading={loading}\n          items={fields}\n          totalItemsCount={total}\n          noItemsMessage={NoResultsFoundText}\n          onWheel={closePopover}\n          onSearch={handleSearch}\n          cellCache={cellCache}\n          onRowToggleViewClick={handleRowToggleViewClick}\n          expandedRows={expandedRows}\n          setExpandedRows={setExpandedRows}\n          onColResizeEnd={onColResizeEnd}\n        />\n      </div>\n    </>\n  )\n}\n\nexport { HashDetailsTable }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/index.ts",
    "content": "export { HashDetailsTable } from './HashDetailsTable'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/hash-details-table/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding: 16px;\n  background-color: var(--euiColorEmptyShade);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/index.ts",
    "content": "export { HashDetails } from './HashDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/styles.module.scss",
    "content": ".divider {\n  margin: 0 14px;\n  height: 20px;\n  width: 1px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, AddItemsAction } from './AddItemsAction'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddItemsAction', () => {\n  it('should render', () => {\n    expect(render(<AddItemsAction {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/add-items-action/AddItemsAction.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { MIDDLE_SCREEN_RESOLUTION } from 'uiSrc/constants'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport { PlusInCircleIcon } from 'uiSrc/components/base/icons'\nimport { IconButton, EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  width: number\n  title: string\n  openAddItemPanel: () => void\n}\n\nconst AddItemsAction = ({ width, title, openAddItemPanel }: Props) => (\n  <RiTooltip\n    content={width > MIDDLE_SCREEN_RESOLUTION ? '' : title}\n    position=\"left\"\n    anchorClassName={cx(styles.actionBtn, {\n      [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION,\n    })}\n  >\n    <span\n      className={cx(styles.actionBtn, {\n        [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION,\n      })}\n    >\n      {width > MIDDLE_SCREEN_RESOLUTION ? (\n        <EmptyButton\n          size=\"small\"\n          icon={PlusInCircleIcon}\n          aria-label={title}\n          onClick={openAddItemPanel}\n          data-testid=\"add-key-value-items-btn\"\n        >\n          {title}\n        </EmptyButton>\n      ) : (\n        <IconButton\n          icon={PlusInCircleIcon}\n          aria-label={title}\n          onClick={openAddItemPanel}\n          data-testid=\"add-key-value-items-btn\"\n        />\n      )}\n    </span>\n  </RiTooltip>\n)\n\nexport { AddItemsAction }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, EditItemAction } from './EditItemAction'\n\nconst mockedProps = mock<Props>()\n\ndescribe('EditItemAction', () => {\n  it('should render', () => {\n    expect(render(<EditItemAction {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/edit-item-action/EditItemAction.tsx",
    "content": "import React from 'react'\nimport { Nullable } from 'uiSrc/utils'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  title: string\n  isEditable: boolean\n  tooltipContent: Nullable<string>\n  onEditItem: () => void\n}\n\nconst EditItemAction = ({\n  title,\n  isEditable,\n  tooltipContent,\n  onEditItem,\n}: Props) => (\n  <div className={styles.actionBtn}>\n    <RiTooltip content={tooltipContent} data-testid=\"edit-key-value-tooltip\">\n      <IconButton\n        disabled={!isEditable}\n        icon={EditIcon}\n        aria-label={title}\n        onClick={onEditItem}\n        data-testid=\"edit-key-value-btn\"\n      />\n    </RiTooltip>\n  </div>\n)\n\nexport { EditItemAction }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/index.ts",
    "content": "export { AddItemsAction } from './add-items-action/AddItemsAction'\nexport { RemoveItemsAction } from './remove-items-action/RemoveItemsAction'\nexport { EditItemAction } from './edit-item-action/EditItemAction'\nexport { StreamItemsAction } from './stream-items-action/StreamItemsAction'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, RemoveItemsAction } from './RemoveItemsAction'\n\nconst mockedProps = mock<Props>()\n\ndescribe('RemoveItemsAction', () => {\n  it('should render', () => {\n    expect(\n      render(<RemoveItemsAction {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/remove-items-action/RemoveItemsAction.tsx",
    "content": "import React from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { MinusInCircleIcon } from 'uiSrc/components/base/icons'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  title: string\n  openRemoveItemPanel: () => void\n}\n\nconst RemoveItemsAction = ({ title, openRemoveItemPanel }: Props) => (\n  <RiTooltip content={title} position=\"left\" anchorClassName={styles.actionBtn}>\n    <IconButton\n      icon={MinusInCircleIcon}\n      aria-label={title}\n      onClick={openRemoveItemPanel}\n      data-testid=\"remove-key-value-items-btn\"\n    />\n  </RiTooltip>\n)\n\nexport { RemoveItemsAction }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, StreamItemsAction } from './StreamItemsAction'\n\nconst mockedProps = mock<Props>()\n\ndescribe('StreamItemsAction', () => {\n  it('should render', () => {\n    expect(\n      render(<StreamItemsAction {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/stream-items-action/StreamItemsAction.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { MIDDLE_SCREEN_RESOLUTION } from 'uiSrc/constants'\nimport { PlusInCircleIcon } from 'uiSrc/components/base/icons'\nimport {\n  IconButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  width: number\n  title: string\n  openAddItemPanel: () => void\n}\n\nconst StreamItemsAction = ({ width, title, openAddItemPanel }: Props) => (\n  <RiTooltip\n    content={width > MIDDLE_SCREEN_RESOLUTION ? '' : title}\n    position=\"left\"\n    anchorClassName={cx(styles.actionBtn, {\n      [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION,\n    })}\n  >\n    <span\n      className={cx(styles.actionBtn, {\n        [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION,\n      })}\n    >\n      {width > MIDDLE_SCREEN_RESOLUTION ? (\n        <SecondaryButton\n          size=\"small\"\n          icon={PlusInCircleIcon}\n          aria-label={title}\n          onClick={openAddItemPanel}\n          data-testid=\"add-key-value-items-btn\"\n        >\n          {title}\n        </SecondaryButton>\n      ) : (\n        <IconButton\n          icon={PlusInCircleIcon}\n          aria-label={title}\n          onClick={openAddItemPanel}\n          data-testid=\"add-key-value-items-btn\"\n        />\n      )}\n    </span>\n  </RiTooltip>\n)\n\nexport { StreamItemsAction }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-actions/styles.module.scss",
    "content": ".actionBtn {\n  position: relative;\n  z-index: 2;\n\n  &.withText {\n    color: var(--euiTextSubduedColor) !important;\n    :global(.euiButton__text) {\n      font: normal normal normal 12px/18px Graphik !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-subheader/KeyDetailsSubheader.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { KeyDetailsSubheader, Props } from './KeyDetailsSubheader'\n\nconst mockedProps = mock<Props>()\n\ndescribe('KeyDetailsSubheader', () => {\n  it('should render', () => {\n    expect(\n      render(<KeyDetailsSubheader {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-subheader/KeyDetailsSubheader.tsx",
    "content": "import React, { ReactElement } from 'react'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport { isUndefined } from 'lodash'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { KeyTypes, ModulesKeyTypes } from 'uiSrc/constants'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { KeyDetailsHeaderFormatter } from '../../../key-details-header/components/key-details-header-formatter'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  keyType: KeyTypes | ModulesKeyTypes\n  Actions?: (props: { width: number }) => ReactElement\n}\n\nexport const KeyDetailsSubheader = ({ keyType, Actions }: Props) => (\n  <FlexItem className={styles.subheaderContainer}>\n    <AutoSizer disableHeight>\n      {({ width = 0 }) => (\n        <div style={{ width }}>\n          <Row justify=\"end\" align=\"center\">\n            {Object.values(KeyTypes).includes(keyType as KeyTypes) && (\n              <>\n                <FlexItem className={styles.keyFormatterItem}>\n                  <KeyDetailsHeaderFormatter width={width} />\n                </FlexItem>\n                <Divider className={styles.divider} orientation=\"vertical\" />\n              </>\n            )}\n            {!isUndefined(Actions) && <Actions width={width} />}\n          </Row>\n        </div>\n      )}\n    </AutoSizer>\n  </FlexItem>\n)\n\nexport default KeyDetailsSubheader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/key-details-subheader/styles.module.scss",
    "content": ".subheaderContainer {\n  padding: 12px 18px 0px 18px;\n\n  .divider {\n    margin: 0 14px;\n    height: 20px;\n    width: 1px;\n  }\n\n  .actionItem {\n    margin-left: 12px;\n  }\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, ListDetails } from './ListDetails'\n\nconst mockedProps = mock<Props>()\n\ndescribe('ListDetails', () => {\n  it('should render', () => {\n    expect(render(<ListDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/ListDetails.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { selectedKeySelector } from 'uiSrc/slices/browser/keys'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { ListDetailsTable } from './list-details-table'\n\nimport { RemoveListElements } from './remove-list-elements'\n\nimport AddListElements from './add-list-elements/AddListElements'\nimport { AddItemsAction, RemoveItemsAction } from '../key-details-actions'\nimport { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'\nimport styles from './styles.module.scss'\nimport { AddKeysContainer } from '../common/AddKeysContainer.styled'\n\nexport interface Props extends KeyDetailsHeaderProps {\n  onRemoveKey: () => void\n  onOpenAddItemPanel: () => void\n  onCloseAddItemPanel: () => void\n}\n\nconst ListDetails = (props: Props) => {\n  const keyType = KeyTypes.List\n  const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props\n  const { loading } = useSelector(selectedKeySelector)\n\n  const [isRemoveItemPanelOpen, setIsRemoveItemPanelOpen] =\n    useState<boolean>(false)\n  const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState<boolean>(false)\n\n  const openAddItemPanel = () => {\n    setIsRemoveItemPanelOpen(false)\n    setIsAddItemPanelOpen(true)\n    onOpenAddItemPanel()\n  }\n\n  const closeAddItemPanel = (isCancelled?: boolean) => {\n    setIsAddItemPanelOpen(false)\n    if (isCancelled) {\n      onCloseAddItemPanel()\n    }\n  }\n\n  const closeRemoveItemPanel = () => {\n    setIsRemoveItemPanelOpen(false)\n  }\n\n  const openRemoveItemPanel = () => {\n    setIsAddItemPanelOpen(false)\n    setIsRemoveItemPanelOpen(true)\n  }\n\n  const Actions = ({ width }: { width: number }) => (\n    <>\n      <AddItemsAction\n        title=\"Add Elements\"\n        width={width}\n        openAddItemPanel={openAddItemPanel}\n      />\n      <div className={styles.removeBtnContainer}>\n        <RemoveItemsAction\n          title=\"Remove Elements\"\n          openRemoveItemPanel={openRemoveItemPanel}\n        />\n      </div>\n    </>\n  )\n\n  return (\n    <div className=\"fluid flex-column relative\">\n      <KeyDetailsHeader {...props} key=\"key-details-header\" />\n      <KeyDetailsSubheader keyType={keyType} Actions={Actions} />\n      <div className=\"key-details-body\" key=\"key-details-body\">\n        {!loading && (\n          <div className=\"flex-column\" style={{ flex: '1', height: '100%' }}>\n            <ListDetailsTable />\n          </div>\n        )}\n        {isAddItemPanelOpen && (\n          <AddKeysContainer>\n            <AddListElements closePanel={closeAddItemPanel} />\n          </AddKeysContainer>\n        )}\n        {isRemoveItemPanelOpen && (\n          <AddKeysContainer>\n            <RemoveListElements\n              closePanel={closeRemoveItemPanel}\n              onRemoveKey={onRemoveKey}\n            />\n          </AddKeysContainer>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { ListDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport AddListElements, { HEAD_DESTINATION, Props } from './AddListElements'\n\nconst mockedProps = mock<Props>()\n\nconst elementFindingRegex = /^element-\\d+$/\n\ndescribe('AddListElements', () => {\n  it('should render', () => {\n    expect(render(<AddListElements {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set elements input properly', () => {\n    render(<AddListElements {...instance(mockedProps)} />)\n    const elementsInput = screen.getByTestId(elementFindingRegex)\n    fireEvent.change(elementsInput, { target: { value: '123' } })\n    expect(elementsInput).toHaveValue('123')\n  })\n\n  it('should set destination properly', () => {\n    render(<AddListElements {...instance(mockedProps)} />)\n    const destinationSelect = screen.getByTestId('destination-select')\n    fireEvent.change(destinationSelect, { target: { value: HEAD_DESTINATION } })\n    expect(destinationSelect).toHaveValue(HEAD_DESTINATION)\n  })\n\n  it('should allow for adding multiple elements', () => {\n    render(<AddListElements {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('add-item'))\n    const valueInputs = screen.getAllByTestId(elementFindingRegex)\n    expect([...valueInputs].length).toBe(2)\n  })\n\n  it('should not allow deleting the last element', () => {\n    render(<AddListElements {...instance(mockedProps)} />)\n    const deleteButtons = screen.getAllByTestId('remove-item')\n    expect(deleteButtons[0]).toBeDisabled()\n  })\n\n  it('should allow deleting of the elements after the first one', () => {\n    render(<AddListElements {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('add-item'))\n    const deleteButtons = screen.getAllByTestId('remove-item')\n    expect(deleteButtons[1]).not.toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/add-list-elements/AddListElements.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  selectedKeyDataSelector,\n  keysSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { insertListElementsAction } from 'uiSrc/slices/browser/list'\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { AddListFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { PushElementToListDto } from 'apiSrc/modules/browser/list/dto'\n\nimport { EntryContent } from '../../common/AddKeysContainer.styled'\n\nexport interface Props {\n  closePanel: (isCancelled?: boolean) => void\n}\n\nexport enum ListElementDestination {\n  Tail = 'TAIL',\n  Head = 'HEAD',\n}\n\nexport const TAIL_DESTINATION: ListElementDestination =\n  ListElementDestination.Tail\nexport const HEAD_DESTINATION: ListElementDestination =\n  ListElementDestination.Head\n\nexport const optionsDestinations = [\n  {\n    value: TAIL_DESTINATION,\n    inputDisplay: 'Push to tail',\n    label: 'Push to tail',\n  },\n  {\n    value: HEAD_DESTINATION,\n    inputDisplay: 'Push to head',\n    label: 'Push to head',\n  },\n]\n\nconst AddListElements = (props: Props) => {\n  const { closePanel } = props\n\n  const [elements, setElements] = useState<string[]>([''])\n  const [destination, setDestination] =\n    useState<ListElementDestination>(TAIL_DESTINATION)\n  const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: undefined,\n  }\n  const { viewType } = useSelector(keysSelector)\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const elementInput = useRef<HTMLInputElement>(null)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    // ComponentDidMount\n    elementInput.current?.focus()\n  }, [])\n\n  const onSuccessAdded = () => {\n    closePanel()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_ADDED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_ADDED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.List,\n        numberOfAdded: elements.length,\n      },\n    })\n  }\n\n  const addField = () => {\n    setElements([...elements, ''])\n  }\n\n  const onClickRemove = (_item: string, index?: number) => {\n    if (elements.length === 1) {\n      setElements([''])\n    } else {\n      setElements(elements.filter((_el, i) => i !== index))\n    }\n  }\n\n  const isClearDisabled = (item: string) =>\n    elements.length === 1 && !item.length\n\n  const handleElementChange = (value: string, index: number) => {\n    const newElements = [...elements]\n    newElements[index] = value\n    setElements(newElements)\n  }\n\n  const submitData = (): void => {\n    const data: PushElementToListDto = {\n      keyName: selectedKey,\n      elements: elements.map((el) => stringToBuffer(el)),\n      destination,\n    }\n    dispatch(insertListElementsAction(data, onSuccessAdded))\n  }\n\n  return (\n    <Col gap=\"m\">\n      <EntryContent gap=\"m\">\n        <FlexItem>\n          <RiSelect\n            value={destination}\n            options={optionsDestinations}\n            onChange={(value) =>\n              setDestination(value as ListElementDestination)\n            }\n            data-testid=\"destination-select\"\n          />\n        </FlexItem>\n        <AddMultipleFields\n          items={elements}\n          onClickRemove={onClickRemove}\n          onClickAdd={addField}\n          isClearDisabled={isClearDisabled}\n        >\n          {(item, index) => (\n            <TextInput\n              name={`element-${index}`}\n              id={`element-${index}`}\n              placeholder={config.element.placeholder}\n              value={item}\n              onChange={(value) => handleElementChange(value, index)}\n              data-testid={`element-${index}`}\n            />\n          )}\n        </AddMultipleFields>\n      </EntryContent>\n      <Row justify=\"end\" gap=\"m\">\n        <FlexItem>\n          <div>\n            <SecondaryButton\n              onClick={() => closePanel(true)}\n              data-testid=\"cancel-members-btn\"\n            >\n              Cancel\n            </SecondaryButton>\n          </div>\n        </FlexItem>\n        <FlexItem>\n          <div>\n            <PrimaryButton onClick={submitData} data-testid=\"save-elements-btn\">\n              Save\n            </PrimaryButton>\n          </div>\n        </FlexItem>\n      </Row>\n    </Col>\n  )\n}\n\nexport default AddListElements\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/index.ts",
    "content": "export { ListDetails } from './ListDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  KeyValueCompressor,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n  TEXT_DISABLED_COMPRESSED_VALUE,\n} from 'uiSrc/constants'\nimport { listDataSelector } from 'uiSrc/slices/browser/list'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { anyToBuffer } from 'uiSrc/utils'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  DECOMPRESSED_VALUE_STR_1,\n} from 'uiSrc/utils/tests/decompressors'\nimport { setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { ListDetailsTable, Props } from './ListDetailsTable'\n\nconst mockedProps = mock<Props>()\n\nconst elements = [\n  { element: { type: 'Buffer', data: [49] }, index: 0 },\n  { element: { type: 'Buffer', data: [50] }, index: 1 },\n  { element: { type: 'Buffer', data: [51] }, index: 2 },\n]\n\njest.mock('uiSrc/slices/browser/list', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/browser/list',\n  ).initialState\n  return {\n    listSelector: jest.fn().mockReturnValue(defaultState),\n    updateListValueStateSelector: jest\n      .fn()\n      .mockReturnValue(defaultState.updateValue),\n    listDataSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      total: 3,\n      key: { type: 'Buffer', data: [49] },\n      keyName: { type: 'Buffer', data: [49] },\n      elements,\n    }),\n    fetchListElements: jest.fn(),\n    fetchSearchingListElementAction: jest.fn,\n    setListInitialState: jest.fn,\n  }\n})\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    compressor: null,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ListDetailsTable', () => {\n  it('should render', () => {\n    expect(render(<ListDetailsTable {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render rows properly', () => {\n    const { container } = render(<ListDetailsTable {...mockedProps} />)\n    const rows = container.querySelectorAll(\n      '.ReactVirtualized__Table__row[role=\"row\"]',\n    )\n    expect(rows).toHaveLength(elements.length)\n  })\n\n  it('should render search input', () => {\n    render(<ListDetailsTable {...mockedProps} />)\n    expect(screen.getByTestId('search')).toBeTruthy()\n  })\n\n  it('should call search', () => {\n    render(<ListDetailsTable {...mockedProps} />)\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, { target: { value: '111' } })\n    expect(searchInput).toHaveValue('111')\n  })\n\n  it('should render editor after click edit button', async () => {\n    render(<ListDetailsTable {...mockedProps} />)\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('list_content-value-1'))\n    })\n\n    fireEvent.click(screen.getByTestId('list_edit-btn-1'))\n    expect(screen.getByTestId('list_value-editor-1')).toBeInTheDocument()\n  })\n\n  it('should render resize trigger for index column', () => {\n    render(<ListDetailsTable {...mockedProps} />)\n    expect(screen.getByTestId('resize-trigger-index')).toBeInTheDocument()\n  })\n\n  it('should disable refresh when editing', async () => {\n    render(<ListDetailsTable {...mockedProps} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('list_content-value-0'))\n    })\n\n    fireEvent.click(screen.getByTestId('list_edit-btn-0'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  describe('decompressed  data', () => {\n    it('should render decompressed GZIP data = \"1\"', () => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/list',\n      ).initialState\n      const listDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        key: '123zxczxczxc',\n        elements: [{ element: anyToBuffer(GZIP_COMPRESSED_VALUE_1), index: 0 }],\n      })\n      ;(listDataSelector as jest.Mock).mockImplementation(listDataSelectorMock)\n\n      render(<ListDetailsTable {...mockedProps} />)\n      const elementEl = screen.getByTestId('list_content-value-0')\n\n      expect(elementEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1)\n    })\n\n    it('edit button should be disabled if data was compressed', async () => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/list',\n      ).initialState\n      const listDataSelectorMock = jest.fn().mockReturnValueOnce({\n        ...defaultState,\n        key: '123zxczxczxc',\n        elements: [{ element: anyToBuffer(GZIP_COMPRESSED_VALUE_1), index: 0 }],\n      })\n      ;(listDataSelector as jest.Mock).mockImplementationOnce(\n        listDataSelectorMock,\n      )\n      ;(connectedInstanceSelector as jest.Mock).mockImplementationOnce(() => ({\n        compressor: KeyValueCompressor.GZIP,\n      }))\n\n      const { queryByTestId } = render(<ListDetailsTable {...mockedProps} />)\n      act(() => {\n        fireEvent.mouseEnter(screen.getByTestId('list_content-value-0'))\n      })\n\n      const editBtn = screen.getByTestId('list_edit-btn-0')\n\n      fireEvent.click(editBtn)\n\n      await act(async () => {\n        fireEvent.focus(editBtn)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(editBtn).toBeDisabled()\n      expect(screen.getByTestId('list_edit-tooltip-0')).toHaveTextContent(\n        TEXT_DISABLED_COMPRESSED_VALUE,\n      )\n      expect(queryByTestId('list_value-editor-0')).not.toBeInTheDocument()\n    })\n  })\n\n  describe('truncated values', () => {\n    beforeEach(() => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/list',\n      ).initialState\n      const listDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        key: '123zxczxczxc',\n        elements: [{ element: MOCK_TRUNCATED_BUFFER_VALUE, index: 0 }],\n      })\n      ;(listDataSelector as jest.Mock).mockImplementation(listDataSelectorMock)\n    })\n\n    it('edit button should be disabled if data was truncated', async () => {\n      const { queryByTestId } = render(<ListDetailsTable {...mockedProps} />)\n      act(() => {\n        fireEvent.mouseEnter(screen.getByTestId('list_content-value-0'))\n      })\n\n      const editBtn = screen.getByTestId('list_edit-btn-0')\n\n      await act(async () => {\n        fireEvent.focus(editBtn)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(editBtn).toBeDisabled()\n      expect(screen.getByTestId('list_edit-tooltip-0')).toHaveTextContent(\n        TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n      )\n\n      fireEvent.click(editBtn)\n      expect(queryByTestId('list_value-editor-0')).not.toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/ListDetailsTable.tsx",
    "content": "import React, { Ref, useCallback, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { isNull, isNumber } from 'lodash'\nimport { CellMeasurerCache } from 'react-virtualized'\nimport {\n  appContextBrowserKeyDetails,\n  updateKeyDetailsSizes,\n} from 'uiSrc/slices/app/context'\nimport { RiTooltip } from 'uiSrc/components'\n\nimport {\n  listSelector,\n  listDataSelector,\n  fetchListElements,\n  fetchMoreListElements,\n  updateListElementAction,\n  updateListValueStateSelector,\n  fetchSearchingListElementAction,\n} from 'uiSrc/slices/browser/list'\nimport {\n  ITableColumn,\n  IColumnSearchState,\n  RelativeWidthSizes,\n} from 'uiSrc/components/virtual-table/interfaces'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  sendEventTelemetry,\n  TelemetryEvent,\n  getBasedOnViewTypeEvent,\n} from 'uiSrc/telemetry'\nimport {\n  KeyTypes,\n  OVER_RENDER_BUFFER_COUNT,\n  TableCellAlignment,\n  TEXT_INVALID_VALUE,\n  TEXT_DISABLED_FORMATTER_EDITING,\n  TEXT_UNPRINTABLE_CHARACTERS,\n  TEXT_DISABLED_COMPRESSED_VALUE,\n  TEXT_FAILED_CONVENT_FORMATTER,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n} from 'uiSrc/constants'\nimport {\n  bufferToString,\n  formatLongName,\n  formattingBuffer,\n  isFormatEditable,\n  isNonUnicodeFormatter,\n  isEqualBuffers,\n  stringToBuffer,\n  stringToSerializedBufferFormat,\n  validateListIndex,\n  Nullable,\n  createTooltipContent,\n  bufferToSerializedFormat,\n  isTruncatedString,\n} from 'uiSrc/utils'\nimport {\n  selectedKeyDataSelector,\n  keysSelector,\n  selectedKeySelector,\n  setSelectedKeyRefreshDisabled,\n} from 'uiSrc/slices/browser/keys'\nimport { NoResultsFoundText } from 'uiSrc/constants/texts'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { getColumnWidth } from 'uiSrc/components/virtual-grid'\nimport { decompressingBuffer } from 'uiSrc/utils/decompressors'\n\nimport {\n  EditableTextArea,\n  FormattedValue,\n} from 'uiSrc/pages/browser/modules/key-details/shared'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  SetListElementDto,\n  SetListElementResponse,\n} from 'apiSrc/modules/browser/list/dto'\n\nimport styles from './styles.module.scss'\n\nconst headerHeight = 60\nconst rowHeight = 43\nconst footerHeight = 0\nconst initSearchingIndex = null\n\nconst cellCache = new CellMeasurerCache({\n  fixedWidth: true,\n  minHeight: rowHeight,\n})\n\ninterface IListElement extends SetListElementResponse {}\n\nconst ListDetailsTable = () => {\n  const { loading } = useSelector(listSelector)\n  const { loading: updateLoading } = useSelector(updateListValueStateSelector)\n  const {\n    elements: loadedElements,\n    total,\n    searchedIndex,\n  } = useSelector(listDataSelector)\n  const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' }\n  const { id: instanceId, compressor = null } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewType } = useSelector(keysSelector)\n  const { viewFormat: viewFormatProp, lastRefreshTime } =\n    useSelector(selectedKeySelector)\n  const { [KeyTypes.List]: listSizes } = useSelector(\n    appContextBrowserKeyDetails,\n  )\n\n  const [elements, setElements] = useState<IListElement[]>([])\n  const [width, setWidth] = useState(100)\n  const [expandedRows, setExpandedRows] = useState<number[]>([])\n  const [editingIndex, setEditingIndex] = useState<Nullable<number>>(null)\n  const [viewFormat, setViewFormat] = useState(viewFormatProp)\n\n  const formattedLastIndexRef = useRef(OVER_RENDER_BUFFER_COUNT)\n  const tableRef: Ref<any> = useRef(null)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    resetState()\n  }, [lastRefreshTime])\n\n  useEffect(() => {\n    setElements(loadedElements)\n\n    if (loadedElements.length < elements.length) {\n      formattedLastIndexRef.current = 0\n    }\n\n    if (viewFormat !== viewFormatProp) {\n      resetState()\n    }\n  }, [loadedElements, viewFormatProp])\n\n  const resetState = () => {\n    setExpandedRows([])\n    setViewFormat(viewFormatProp)\n    setEditingIndex(null)\n    dispatch(setSelectedKeyRefreshDisabled(false))\n\n    clearCache()\n  }\n\n  const clearCache = (rowIndex?: number) => {\n    if (isNumber(rowIndex)) {\n      cellCache.clear(rowIndex, 1)\n      tableRef.current?.recomputeRowHeights(rowIndex)\n      return\n    }\n\n    cellCache.clearAll()\n  }\n\n  const handleEditElement = useCallback(\n    (index: number, editing: boolean) => {\n      setEditingIndex(editing ? index : null)\n      dispatch(setSelectedKeyRefreshDisabled(editing))\n\n      clearCache(index)\n    },\n    [cellCache, viewFormat],\n  )\n\n  const handleApplyEditElement = (index = 0, value: string) => {\n    const data: SetListElementDto = {\n      keyName: key,\n      element: stringToSerializedBufferFormat(viewFormat, value),\n      index,\n    }\n    dispatch(updateListElementAction(data, () => onElementEditedSuccess(index)))\n  }\n\n  const onElementEditedSuccess = (elementIndex = 0) => {\n    handleEditElement(elementIndex, false)\n  }\n\n  const handleSearch = (search: IColumnSearchState[]) => {\n    formattedLastIndexRef.current = 0\n    const indexColumn = search.find((column) => column.id === 'index')\n    const onSuccess = () => {\n      sendEventTelemetry({\n        event: getBasedOnViewTypeEvent(\n          viewType,\n          TelemetryEvent.BROWSER_KEY_VALUE_FILTERED,\n          TelemetryEvent.TREE_VIEW_KEY_VALUE_FILTERED,\n        ),\n        eventData: {\n          databaseId: instanceId,\n          keyType: KeyTypes.List,\n          match: 'EXACT_VALUE_NAME',\n        },\n      })\n    }\n\n    if (!indexColumn?.value) {\n      dispatch(fetchListElements(key, 0, SCAN_COUNT_DEFAULT))\n      return\n    }\n\n    if (indexColumn) {\n      const { value } = indexColumn\n      dispatch(\n        fetchSearchingListElementAction(\n          key,\n          value ? +value : initSearchingIndex,\n          onSuccess,\n        ),\n      )\n    }\n  }\n\n  const handleRowToggleViewClick = (expanded: boolean, rowIndex: number) => {\n    const browserViewEvent = expanded\n      ? TelemetryEvent.BROWSER_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.BROWSER_KEY_FIELD_VALUE_COLLAPSED\n    const treeViewEvent = expanded\n      ? TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED\n\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        keyType: KeyTypes.List,\n        databaseId: instanceId,\n        largestCellLength: elements[rowIndex]?.element?.length || 0,\n      },\n    })\n\n    cellCache.clearAll()\n  }\n\n  const onColResizeEnd = (sizes: RelativeWidthSizes) => {\n    dispatch(\n      updateKeyDetailsSizes({\n        type: KeyTypes.List,\n        sizes,\n      }),\n    )\n  }\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'index',\n      label: 'Index',\n      minWidth: 120,\n      relativeWidth: listSizes?.index || 30,\n      truncateText: true,\n      isSearchable: true,\n      isResizable: true,\n      prependSearchName: 'Index:',\n      initialSearchValue: '',\n      searchValidation: validateListIndex,\n      className: 'value-table-separate-border',\n      headerClassName: 'value-table-separate-border',\n      render: function Index(_name: string, { index }: IListElement) {\n        // Better to cut the long string, because it could affect virtual scroll performance\n        const cellContent = index?.toString().substring(0, 200)\n        const tooltipContent = formatLongName(index?.toString())\n        return (\n          <Text component=\"div\" color=\"secondary\" style={{ maxWidth: '100%' }}>\n            <div\n              style={{ display: 'flex' }}\n              className=\"truncateText\"\n              data-testid={`list-index-value-${index}`}\n            >\n              <RiTooltip\n                title=\"Index\"\n                className={styles.tooltip}\n                anchorClassName=\"truncateText\"\n                position=\"bottom\"\n                content={tooltipContent}\n              >\n                <>{cellContent}</>\n              </RiTooltip>\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'element',\n      label: 'Element',\n      minWidth: 150,\n      truncateText: true,\n      alignment: TableCellAlignment.Left,\n      className: 'noPadding',\n      render: function Element(\n        _element: string,\n        { element: elementItem, index }: IListElement,\n        expanded: boolean = false,\n        rowIndex = 0,\n      ) {\n        const { value: decompressedElementItem, isCompressed } =\n          decompressingBuffer(elementItem, compressor)\n        const isTruncatedValue = isTruncatedString(elementItem)\n        const element = bufferToString(elementItem)\n        const { value, isValid } = formattingBuffer(\n          decompressedElementItem,\n          viewFormatProp,\n          { expanded },\n        )\n        const disabled =\n          !isNonUnicodeFormatter(viewFormat, isValid) &&\n          !isEqualBuffers(elementItem, stringToBuffer(element))\n        const isEditable =\n          !isCompressed && isFormatEditable(viewFormat) && !isTruncatedValue\n        const isEditing = index === editingIndex\n\n        const tooltipContent = createTooltipContent(\n          value,\n          decompressedElementItem,\n          viewFormatProp,\n        )\n        const editTooltipContent = isCompressed\n          ? TEXT_DISABLED_COMPRESSED_VALUE\n          : isTruncatedValue\n            ? TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA\n            : TEXT_DISABLED_FORMATTER_EDITING\n        const serializedValue = isEditing\n          ? bufferToSerializedFormat(viewFormat, elementItem, 4)\n          : ''\n\n        return (\n          <EditableTextArea\n            initialValue={serializedValue}\n            isLoading={updateLoading}\n            isDisabled={disabled}\n            isEditing={isEditing}\n            isEditDisabled={!isEditable || updateLoading}\n            disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS}\n            onDecline={() => handleEditElement(index, false)}\n            onApply={(value) => handleApplyEditElement(index, value)}\n            approveText={TEXT_INVALID_VALUE}\n            approveByValidation={(value) =>\n              formattingBuffer(\n                stringToSerializedBufferFormat(viewFormat, value),\n                viewFormat,\n              )?.isValid\n            }\n            onEdit={(isEditing) => handleEditElement(index, isEditing)}\n            editToolTipContent={!isEditable ? editTooltipContent : null}\n            onUpdateTextAreaHeight={() => clearCache(rowIndex)}\n            field={index.toString()}\n            testIdPrefix=\"list\"\n          >\n            <div className=\"innerCellAsCell\">\n              <FormattedValue\n                value={value}\n                expanded={expanded}\n                title={\n                  isValid\n                    ? 'Element'\n                    : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n                }\n                tooltipContent={tooltipContent}\n              />\n            </div>\n          </EditableTextArea>\n        )\n      },\n    },\n  ]\n\n  const loadMoreItems = ({ startIndex, stopIndex }: any) => {\n    if (isNull(searchedIndex)) {\n      dispatch(\n        fetchMoreListElements(key, startIndex, stopIndex - startIndex + 1),\n      )\n    }\n  }\n\n  return (\n    <div\n      data-testid=\"list-details\"\n      className={cx(\n        'key-details-table',\n        'list-elements-container',\n        styles.container,\n      )}\n    >\n      <VirtualTable\n        autoHeight\n        expandable\n        tableRef={tableRef}\n        selectable={false}\n        keyName={key}\n        headerHeight={headerHeight}\n        rowHeight={rowHeight}\n        footerHeight={footerHeight}\n        onChangeWidth={setWidth}\n        columns={columns.map((column, i, arr) => ({\n          ...column,\n          width: getColumnWidth(i, width, arr),\n        }))}\n        loadMoreItems={loadMoreItems}\n        loading={loading}\n        items={elements}\n        totalItemsCount={total}\n        noItemsMessage={NoResultsFoundText}\n        onSearch={handleSearch}\n        cellCache={cellCache}\n        onRowToggleViewClick={handleRowToggleViewClick}\n        expandedRows={expandedRows}\n        setExpandedRows={setExpandedRows}\n        onColResizeEnd={onColResizeEnd}\n      />\n    </div>\n  )\n}\n\nexport { ListDetailsTable }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/index.ts",
    "content": "export { ListDetailsTable } from './ListDetailsTable'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/list-details-table/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding: 16px;\n  background-color: var(--euiColorEmptyShade);\n\n  .textAreaControls {\n    right: -56px !important;\n    bottom: -4px !important;\n    top: auto !important;\n    background-color: var(--euiPageBackgroundColor) !important;\n  }\n\n  .textArea {\n    background-color: var(--euiPageBackgroundColor) !important;\n    border-color: var(--euiColorPrimary) !important;\n    z-index: 3;\n    padding-left: 20px;\n    padding-bottom: 36px !important;\n    margin: -8px -6px -8px -20px !important;\n    min-width: calc(100% + 106px) !important;\n    font: normal normal normal 13px/18px Graphik, sans-serif;\n    min-height: 43px;\n    overflow: hidden;\n    overflow-wrap: break-word;\n    resize: none;\n\n    &:focus {\n      background-image: none !important;\n    }\n\n    &.areaWarning {\n      border-color: var(--euiColorWarningLight) !important;\n    }\n  }\n}\n\n.inlineItemEditor {\n  max-width: calc(100% - 20px);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'\n\nimport { Props, RemoveListElements } from './RemoveListElements'\nimport { HEAD_DESTINATION } from '../add-list-elements/AddListElements'\n\nconst COUNT_INPUT = 'count-input'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  connectedInstanceOverviewSelector: jest.fn().mockReturnValue({\n    version: '6.2.1',\n  }),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'id1',\n  }),\n}))\n\ndescribe('RemoveListElements', () => {\n  it('should render', () => {\n    expect(\n      render(<RemoveListElements {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should set count input properly', () => {\n    render(<RemoveListElements {...instance(mockedProps)} />)\n    const countInput = screen.getByTestId(COUNT_INPUT)\n    fireEvent.change(countInput, { target: { value: '123' } })\n    expect(countInput).toHaveValue('123')\n  })\n\n  it('should set destination properly', () => {\n    render(<RemoveListElements {...instance(mockedProps)} />)\n    const destinationSelect = screen.getByTestId('destination-select')\n    fireEvent.change(destinationSelect, {\n      target: { value: HEAD_DESTINATION },\n    })\n    expect(destinationSelect).toHaveValue(HEAD_DESTINATION)\n  })\n\n  it('should show remove popover', () => {\n    render(<RemoveListElements {...instance(mockedProps)} />)\n    const countInput = screen.getByTestId(COUNT_INPUT)\n    fireEvent.change(countInput, { target: { value: '123' } })\n    fireEvent.click(screen.getByTestId('remove-elements-btn'))\n    expect(screen.getByTestId('remove-submit')).toBeInTheDocument()\n  })\n\n  it('should be disabled count with database redis version < 6.2', () => {\n    connectedInstanceOverviewSelector.mockImplementation(() => ({\n      version: '5.1',\n    }))\n\n    render(<RemoveListElements {...instance(mockedProps)} />)\n    const countInput = screen.getByTestId(COUNT_INPUT)\n    fireEvent.change(countInput, { target: { value: '123' } })\n    expect(countInput).toBeDisabled()\n  })\n\n  it('should be info box with database redis version < 6.2', () => {\n    connectedInstanceOverviewSelector.mockImplementation(() => ({\n      version: '5.1',\n    }))\n\n    render(<RemoveListElements {...instance(mockedProps)} />)\n    expect(screen.getByTestId('info-tooltip-icon')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/RemoveListElements.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { toNumber } from 'lodash'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { KeyTypes } from 'uiSrc/constants'\nimport {\n  bufferToString,\n  formatNameShort,\n  isVersionHigherOrEquals,\n  validateCountNumber,\n} from 'uiSrc/utils'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport HelpTexts from 'uiSrc/constants/help-texts'\nimport { CommandsVersions } from 'uiSrc/constants/commandsVersions'\n\nimport {\n  keysSelector,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { deleteListElementsAction } from 'uiSrc/slices/browser/list'\nimport {\n  connectedInstanceOverviewSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\n\nimport { AddListFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  DestructiveButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { DeleteListElementsDto } from 'apiSrc/modules/browser/list/dto'\n\nimport {\n  HEAD_DESTINATION,\n  ListElementDestination,\n  TAIL_DESTINATION,\n} from '../add-list-elements/AddListElements'\n\nimport styles from './styles.module.scss'\nimport { Panel } from 'uiSrc/components/panel'\nimport { EntryContent } from 'uiSrc/pages/browser/modules/key-details/components/common/AddKeysContainer.styled'\n\nexport interface Props {\n  closePanel: (isCancelled?: boolean) => void\n  onRemoveKey: () => void\n}\n\nconst optionsDestinations = [\n  {\n    value: TAIL_DESTINATION,\n    label: 'Remove from tail',\n  },\n  {\n    value: HEAD_DESTINATION,\n    label: 'Remove from head',\n  },\n]\n\nconst RemoveListElements = (props: Props) => {\n  const { closePanel, onRemoveKey } = props\n\n  const [count, setCount] = useState<string>('')\n  const [destination, setDestination] =\n    useState<ListElementDestination>(TAIL_DESTINATION)\n  const [isFormValid, setIsFormValid] = useState<boolean>(true)\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)\n\n  const [canRemoveMultiple, setCanRemoveMultiple] = useState<boolean>(true)\n  const { name: selectedKey = '', length } = useSelector(\n    selectedKeyDataSelector,\n  ) ?? {\n    name: undefined,\n    length: 0,\n  }\n  const { version: databaseVersion = '' } = useSelector(\n    connectedInstanceOverviewSelector,\n  )\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { viewType } = useSelector(keysSelector)\n\n  const countInput = useRef<HTMLInputElement>(null)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    // ComponentDidMount\n    countInput.current?.focus()\n  }, [])\n\n  useEffect(() => {\n    setIsFormValid(toNumber(count) > 0)\n  }, [count])\n\n  useEffect(() => {\n    if (\n      !isVersionHigherOrEquals(\n        databaseVersion,\n        CommandsVersions.REMOVE_MULTIPLE_LIST_ELEMENTS.since,\n      )\n    ) {\n      setCount('1')\n      setCanRemoveMultiple(false)\n    }\n  }, [databaseVersion])\n\n  const handleCountChange = (value: string) => {\n    setCount(validateCountNumber(value))\n  }\n\n  const showPopover = () => {\n    setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen)\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.List,\n      },\n    })\n  }\n\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const onSuccessRemoved = (newTotal: number) => {\n    if (newTotal <= 0) onRemoveKey()\n    closePanel()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.List,\n        numberOfRemoved: toNumber(count),\n      },\n    })\n  }\n\n  const submitData = (): void => {\n    const data: DeleteListElementsDto = {\n      keyName: selectedKey,\n      count: toNumber(count),\n      destination,\n    }\n    closePopover()\n    dispatch(deleteListElementsAction(data, onSuccessRemoved))\n  }\n\n  const RemoveButton = () => (\n    <RiPopover\n      anchorPosition=\"upCenter\"\n      isOpen={isPopoverOpen}\n      closePopover={closePopover}\n      panelClassName={styles.panelCancelBtn}\n      panelPaddingSize=\"l\"\n      button={\n        <PrimaryButton\n          onClick={showPopover}\n          disabled={!isFormValid}\n          data-testid=\"remove-elements-btn\"\n        >\n          Remove\n        </PrimaryButton>\n      }\n    >\n      <div className={styles.popover}>\n        <Text size=\"m\" component=\"div\">\n          <h4 style={{ marginTop: 0 }}>\n            <b>{count}</b> Element(s)\n          </h4>\n          <Text size=\"s\">\n            will be removed from the {destination.toLowerCase()} of{' '}\n            <b>{formatNameShort(bufferToString(selectedKey))}</b>\n          </Text>\n          {(!length || length <= +count) && (\n            <div className={styles.appendInfo}>\n              <RiIcon\n                type=\"ToastDangerIcon\"\n                style={{ marginRight: '1rem', marginTop: '4px' }}\n              />\n              <Text size=\"s\">\n                If you remove all Elements, the whole Key will be deleted.\n              </Text>\n            </div>\n          )}\n        </Text>\n        <Spacer />\n        <Row justify=\"end\">\n          <DestructiveButton\n            size=\"small\"\n            onClick={submitData}\n            icon={DeleteIcon}\n            data-testid=\"remove-submit\"\n          >\n            Remove\n          </DestructiveButton>\n        </Row>\n      </div>\n    </RiPopover>\n  )\n\n  const InfoBoxTooltip = () => (\n    <RiTooltip\n      interactive\n      position=\"left\"\n      content={HelpTexts.REMOVING_MULTIPLE_ELEMENTS_NOT_SUPPORT}\n    >\n      <RiIcon\n        className={styles.infoIcon}\n        type=\"InfoIcon\"\n        style={{ cursor: 'pointer' }}\n        data-testid=\"info-tooltip-icon\"\n      />\n    </RiTooltip>\n  )\n\n  return (\n    <Col gap=\"m\">\n      <EntryContent gap=\"m\">\n        <FlexItem grow>\n          <Row align=\"start\" gap=\"m\" className={styles.formFieldsRow}>\n            <FlexItem style={{ minWidth: '220px' }}>\n              <FormField>\n                <RiSelect\n                  value={destination}\n                  options={optionsDestinations}\n                  onChange={(value) =>\n                    setDestination(value as ListElementDestination)\n                  }\n                  data-testid=\"destination-select\"\n                />\n              </FormField>\n            </FlexItem>\n            <Row grow>\n              <FlexItem grow>\n                <FormField>\n                  <TextInput\n                    name={config.count.name}\n                    id={config.count.name}\n                    maxLength={200}\n                    placeholder={config.count.placeholder}\n                    value={count}\n                    data-testid=\"count-input\"\n                    autoComplete=\"off\"\n                    onChange={handleCountChange}\n                    ref={countInput}\n                    disabled={!canRemoveMultiple}\n                  />\n                </FormField>\n              </FlexItem>\n\n              {!canRemoveMultiple && (\n                <FlexItem>\n                  <InfoBoxTooltip />\n                </FlexItem>\n              )}\n            </Row>\n          </Row>\n        </FlexItem>\n      </EntryContent>\n      <Panel justify=\"end\" gap=\"xl\">\n        <FlexItem>\n          <div>\n            <SecondaryButton\n              onClick={() => closePanel(true)}\n              data-testid=\"cancel-elements-btn\"\n            >\n              Cancel\n            </SecondaryButton>\n          </div>\n        </FlexItem>\n        <FlexItem>\n          <div>{RemoveButton()}</div>\n        </FlexItem>\n      </Panel>\n    </Col>\n  )\n}\n\nexport { RemoveListElements }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/index.ts",
    "content": "export { RemoveListElements } from './RemoveListElements'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/remove-list-elements/styles.module.scss",
    "content": ".content {\n  display: flex;\n  flex-direction: column;\n  max-height: 184px;\n  width: 100%;\n  border: none !important;\n  border-top: 1px solid var(--euiColorPrimary);\n  padding: 12px 20px;\n  scroll-padding-bottom: 72px;\n\n  :global(.euiFormControlLayout--group) {\n    height: 43px !important;\n  }\n}\n\n.popover {\n  max-width: 312px !important;\n  word-wrap: break-word;\n}\n\n.infoIcon {\n  height: 41px !important;\n  width: 40px !important;\n  color: #b5b6c0 !important;\n  padding: 0 8px;\n}\n\n.appendInfo {\n  color: var(--euiTextSubduedColor);\n  font-size: 12px !important;\n\n  display: flex;\n  margin-top: 1rem;\n}\n\n.select {\n  height: 43px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/list-details/styles.module.scss",
    "content": ".removeBtnContainer {\n  margin-left: 12px;\n}\n\n.container {\n  max-height: 234px;\n  scroll-padding-bottom: 60px;\n  overflow-y: scroll;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/ModulesTypeDetails.spec.tsx",
    "content": "import React from 'react'\nimport { ModulesKeyTypes, MODULES_KEY_TYPES_NAMES } from 'uiSrc/constants'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport ModulesTypeDetails from './ModulesTypeDetails'\n\ndescribe('ModulesTypeDetails', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <ModulesTypeDetails\n          moduleName={MODULES_KEY_TYPES_NAMES[ModulesKeyTypes.Graph]}\n          onClose={jest.fn()}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/ModulesTypeDetails.tsx",
    "content": "import React from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { Pages } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { Title } from 'uiSrc/components/base/text/Title'\n\nimport TextDetailsWrapper from '../text-details-wrapper/TextDetailsWrapper'\nimport styles from './styles.module.scss'\n\ntype ModulesTypeDetailsProps = {\n  moduleName: string\n  onClose: () => void\n}\nconst ModulesTypeDetails = ({\n  moduleName = 'unsupported',\n  onClose,\n}: ModulesTypeDetailsProps) => {\n  const history = useHistory()\n  const { id: connectedInstanceId = '' } = useSelector(\n    connectedInstanceSelector,\n  )\n\n  const handleGoWorkbenchPage = (e: React.MouseEvent) => {\n    e.preventDefault()\n    history.push(Pages.workbench(connectedInstanceId))\n  }\n\n  return (\n    <TextDetailsWrapper onClose={onClose} testid=\"modules-type\">\n      <Title size=\"M\">{`This is a ${moduleName} key.`}</Title>\n      <Text size=\"S\">\n        {'Use Redis commands in the '}\n        <a\n          tabIndex={0}\n          onClick={handleGoWorkbenchPage}\n          className={styles.link}\n          data-testid=\"internal-workbench-link\"\n          onKeyDown={() => ({})}\n          role=\"link\"\n          rel=\"noreferrer\"\n        >\n          Workbench\n        </a>\n        {' tool to view the value.'}\n      </Text>\n    </TextDetailsWrapper>\n  )\n}\n\nexport default ModulesTypeDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/modules-type-details/styles.module.scss",
    "content": ".link {\n  text-decoration: underline;\n  color: var(--euiColorFullShade);\n}"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, NoKeySelected } from './NoKeySelected'\n\nconst mockedProps = mock<Props>()\n\ndescribe('NoKeySelected', () => {\n  it('should render', () => {\n    expect(render(<NoKeySelected {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/NoKeySelected.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport ExploreGuides from 'uiSrc/components/explore-guides'\nimport { Nullable } from 'uiSrc/utils'\n\nimport { toggleBrowserFullScreen } from 'uiSrc/slices/browser/keys'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  keyProp: Nullable<RedisResponseBuffer>\n  totalKeys: number\n  keysLastRefreshTime: Nullable<number>\n  onClosePanel: () => void\n  error?: string\n}\n\nexport const NoKeySelected = (props: Props) => {\n  const { keyProp, totalKeys, onClosePanel, error, keysLastRefreshTime } = props\n\n  const dispatch = useDispatch()\n\n  const handleClosePanel = () => {\n    dispatch(toggleBrowserFullScreen(true))\n    keyProp && onClosePanel()\n  }\n\n  const NoKeysSelectedMessage = () => (\n    <>\n      {totalKeys > 0 ? (\n        <Text textAlign=\"center\">\n          <span data-testid=\"select-key-message\">\n            Select the key from the list on the left to see the details of the\n            key.\n          </span>\n        </Text>\n      ) : (\n        <ExploreGuides />\n      )}\n    </>\n  )\n\n  return (\n    <>\n      <RiTooltip\n        content=\"Close\"\n        position=\"left\"\n        anchorClassName={styles.closeRightPanel}\n      >\n        <IconButton\n          icon={CancelSlimIcon}\n          aria-label=\"Close panel\"\n          className={styles.closeBtn}\n          onClick={handleClosePanel}\n          data-testid=\"close-right-panel-btn\"\n        />\n      </RiTooltip>\n\n      <div className={styles.placeholder}>\n        {error ? (\n          <Text textAlign=\"center\">\n            <span data-testid=\"no-keys-selected-text\">{error}</span>\n          </Text>\n        ) : (\n          !!keysLastRefreshTime && <NoKeysSelectedMessage />\n        )}\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/index.ts",
    "content": "export { NoKeySelected } from './NoKeySelected'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/no-key-selected/styles.module.scss",
    "content": ".closeRightPanel {\n  position: absolute;\n  top: 22px;\n  right: 18px;\n\n  .closeBtn {\n    :global(svg) {\n      width: 20px;\n      height: 20px;\n    }\n  }\n}\n\n.placeholder {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  height: 100%;\n  padding: 12px;\n  width: 100%;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { EditorType } from 'uiSrc/slices/interfaces'\nimport { stringToBuffer } from 'uiSrc/utils'\n\nimport { RejsonDetailsWrapper, Props } from './RejsonDetailsWrapper'\n\njest.mock('react-redux', () => ({\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n  connect: () => (Component: any) => Component,\n}))\n\njest.mock('uiSrc/slices/browser/rejson', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/rejson'),\n  fetchReJSON: jest.fn((key) => ({ type: 'FETCH_REJSON', payload: key })),\n}))\n\nconst mockedProps = mock<Props>()\n\nconst mockUseSelector = useSelector as jest.Mock\nconst mockUseDispatch = useDispatch as jest.Mock\n\ntype Selector = {\n  name: string\n}\n\nconst commonSelectorsMock = (selector: Selector) => {\n  if (selector.name === 'rejsonSelector') {\n    return { loading: false, editorType: EditorType.Default }\n  }\n  if (selector.name === 'rejsonDataSelector') {\n    return { data: '{}', downloaded: true, type: 'string', path: '' }\n  }\n  if (selector.name === 'selectedKeyDataSelector') {\n    return {\n      name: stringToBuffer('test-key'),\n      nameString: 'test-key',\n      length: 1,\n    }\n  }\n  if (selector.name === 'keysSelector') {\n    return { viewType: 'Browser' }\n  }\n  if (selector.name === 'connectedInstanceSelector') {\n    return { id: 'instanceId' }\n  }\n  return {}\n}\n\ndescribe('RejsonDetailsWrapper', () => {\n  const mockDispatch = jest.fn()\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockUseDispatch.mockReturnValue(mockDispatch)\n  })\n\n  it('should render', () => {\n    mockUseSelector.mockImplementation(commonSelectorsMock)\n\n    expect(\n      render(<RejsonDetailsWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should not dispatch fetchReJSON on init', () => {\n    let editorType = EditorType.Default\n\n    mockUseSelector.mockImplementation((selector) => {\n      if (selector.name === 'rejsonSelector') {\n        return { loading: false, editorType }\n      }\n      return commonSelectorsMock(selector)\n    })\n\n    const { rerender } = render(\n      <RejsonDetailsWrapper {...instance(mockedProps)} />,\n    )\n\n    editorType = EditorType.Text\n    rerender(<RejsonDetailsWrapper {...instance(mockedProps)} />)\n\n    expect(mockDispatch).not.toHaveBeenCalledWith({\n      type: 'FETCH_REJSON',\n      payload: expect.anything(),\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/RejsonDetailsWrapper.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { isUndefined } from 'lodash'\n\nimport { rejsonDataSelector, rejsonSelector } from 'uiSrc/slices/browser/rejson'\nimport {\n  selectedKeyDataSelector,\n  keysSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { EditorType } from 'uiSrc/slices/interfaces'\nimport {\n  sendEventTelemetry,\n  TelemetryEvent,\n  getBasedOnViewTypeEvent,\n} from 'uiSrc/telemetry'\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { IJSONData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/interfaces'\n\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport RejsonDetails from './rejson-details'\nimport MonacoEditor from './monaco-editor'\nimport { parseJsonData } from './utils'\nimport styles from './styles.module.scss'\n\nexport interface Props extends KeyDetailsHeaderProps {}\n\nconst RejsonDetailsWrapper = (props: Props) => {\n  const { loading, editorType } = useSelector(rejsonSelector)\n  const { data, downloaded, type, path } = useSelector(rejsonDataSelector)\n\n  const {\n    name: selectedKey,\n    nameString,\n    length,\n  } = useSelector(selectedKeyDataSelector) || {}\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { viewType } = useSelector(keysSelector)\n\n  const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())\n\n  const updatedData = parseJsonData(data)\n\n  useEffect(() => {\n    setExpandedRows(new Set())\n  }, [nameString])\n\n  const reportJSONKeyCollapsed = (level: number) => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_JSON_KEY_COLLAPSED,\n        TelemetryEvent.TREE_VIEW_JSON_KEY_COLLAPSED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        level,\n      },\n    })\n  }\n\n  const reportJSONKeyExpanded = (level: number) => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_JSON_KEY_EXPANDED,\n        TelemetryEvent.TREE_VIEW_JSON_KEY_EXPANDED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        level,\n      },\n    })\n  }\n\n  const handleJsonKeyExpandAndCollapse = (\n    isExpanded: boolean,\n    path: string,\n  ) => {\n    const matchedPath = path.match(/\\[.+?\\]/g)\n    const levelFromPath = matchedPath ? matchedPath.length - 1 : 0\n    if (isExpanded) {\n      reportJSONKeyExpanded(levelFromPath)\n    } else {\n      reportJSONKeyCollapsed(levelFromPath)\n    }\n\n    setExpandedRows((rows) => {\n      const copyOfSet = new Set(rows)\n      if (isExpanded) copyOfSet.add(path)\n      else copyOfSet.delete(path)\n\n      return copyOfSet\n    })\n  }\n\n  const shouldShowDefaultEditor =\n    !isUndefined(updatedData) && editorType === EditorType.Default\n\n  const shouldShowTextEditor =\n    !isUndefined(updatedData) && editorType === EditorType.Text\n\n  const keyDetailsBodyName = shouldShowDefaultEditor\n    ? 'key-details-body'\n    : 'key-details-body-monaco-editor'\n\n  return (\n    <div className=\"fluid flex-column relative\">\n      <KeyDetailsHeader {...props} key=\"key-details-header\" />\n\n      <div className={keyDetailsBodyName} key={keyDetailsBodyName}>\n        <div className=\"flex-column\" style={{ flex: '1', height: '100%' }}>\n          <div data-testid=\"json-details\" className={styles.container}>\n            {loading && (\n              <ProgressBarLoader\n                color=\"primary\"\n                data-testid=\"progress-key-json\"\n              />\n            )}\n\n            {shouldShowDefaultEditor && (\n              <RejsonDetails\n                selectedKey={selectedKey || stringToBuffer('')}\n                dataType={type || ''}\n                data={updatedData as IJSONData}\n                length={length}\n                parentPath={path}\n                expandedRows={expandedRows}\n                onJsonKeyExpandAndCollapse={handleJsonKeyExpandAndCollapse}\n                isDownloaded={downloaded}\n              />\n            )}\n\n            {shouldShowTextEditor && (\n              <MonacoEditor\n                selectedKey={selectedKey || stringToBuffer('')}\n                dataType={type || ''}\n                data={updatedData as IJSONData}\n                length={length}\n                parentPath={path}\n                expandedRows={expandedRows}\n                onJsonKeyExpandAndCollapse={handleJsonKeyExpandAndCollapse}\n                isDownloaded={downloaded}\n              />\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport { RejsonDetailsWrapper }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item/AddItem.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { fireEvent, waitFor } from '@testing-library/react'\nimport { useSelector } from 'react-redux'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport AddItem, { Props } from './AddItem'\nimport { JSONErrors } from '../../constants'\n\nconst mockedProps = mock<Props>()\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nconst mockUseSelector = useSelector as jest.Mock\n\ndescribe('AddItem', () => {\n  beforeEach(() => {\n    mockUseSelector.mockImplementation((selectorFn: any) =>\n      selectorFn({\n        browser: { rejson: { data: { data: JSON.stringify({ test: true }) } } },\n      }),\n    )\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render', () => {\n    expect(render(<AddItem {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should show error with invalid key', () => {\n    render(<AddItem {...mockedProps} isPair onCancel={jest.fn} />)\n\n    fireEvent.change(screen.getByTestId('json-key'), { target: { value: '\"' } })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    expect(screen.getByTestId('edit-json-error')).toHaveTextContent(\n      JSONErrors.keyCorrectSyntax,\n    )\n  })\n\n  it('should show error with invalid value', () => {\n    render(<AddItem {...mockedProps} onCancel={jest.fn} />)\n\n    expect(screen.queryByTestId('json-key')).not.toBeInTheDocument()\n\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '\"' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    expect(screen.getByTestId('edit-json-error')).toHaveTextContent(\n      JSONErrors.valueJSONFormat,\n    )\n  })\n\n  it('should submit with proper key and value', () => {\n    const onSubmit = jest.fn()\n    render(\n      <AddItem\n        {...mockedProps}\n        isPair\n        onCancel={jest.fn}\n        onSubmit={onSubmit}\n      />,\n    )\n\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"key\"' },\n    })\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '1' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    expect(onSubmit).toBeCalledWith({ key: '\"key\"', value: '1' })\n  })\n\n  it('should show confirmation and submit on Overwrite', async () => {\n    const onSubmit = jest.fn()\n    const onCancel = jest.fn()\n\n    mockUseSelector.mockImplementation((selectorFn: any) =>\n      selectorFn({\n        browser: {\n          rejson: { data: { data: JSON.stringify({ existingKey: true }) } },\n        },\n      }),\n    )\n\n    render(\n      <AddItem isPair onSubmit={onSubmit} onCancel={onCancel} parentPath=\"$\" />,\n    )\n\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"existingKey\"' },\n    })\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '\"newValue\"' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    await waitFor(() => {\n      expect(\n        screen.getByText(/Duplicate JSON key detected/i),\n      ).toBeInTheDocument()\n    })\n\n    fireEvent.click(screen.getByTestId('overwrite-btn'))\n\n    expect(onSubmit).toHaveBeenCalledWith({\n      key: '\"existingKey\"',\n      value: '\"newValue\"',\n    })\n  })\n\n  it('should show confirmation and hide confirmation dialog on cancel click, but not cancel the add item form', async () => {\n    const onSubmit = jest.fn()\n    const onCancel = jest.fn()\n\n    mockUseSelector.mockImplementation((selectorFn: any) =>\n      selectorFn({\n        browser: {\n          rejson: { data: { data: JSON.stringify({ existingKey: true }) } },\n        },\n      }),\n    )\n\n    render(\n      <AddItem isPair onSubmit={onSubmit} onCancel={onCancel} parentPath=\"$\" />,\n    )\n\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"existingKey\"' },\n    })\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '\"newValue\"' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    await waitFor(() => {\n      expect(\n        screen.getByText(/Duplicate JSON key detected/i),\n      ).toBeInTheDocument()\n    })\n\n    fireEvent.click(screen.getByTestId('cancel-confirmation-btn'))\n\n    await waitFor(() => {\n      expect(\n        screen.queryByText(/Duplicate JSON key detected/i),\n      ).not.toBeInTheDocument()\n    })\n\n    expect(onSubmit).not.toHaveBeenCalled()\n    expect(onCancel).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item/AddItem.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\nimport { useSelector } from 'react-redux'\nimport styled from 'styled-components'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { rejsonDataSelector } from 'uiSrc/slices/browser/rejson'\nimport { checkExistingPath } from 'uiSrc/utils/rejson'\nimport FieldMessage from 'uiSrc/components/field-message/FieldMessage'\nimport { Nullable } from 'uiSrc/utils'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { WindowEvent } from 'uiSrc/components/base/utils/WindowEvent'\nimport { FocusTrap } from 'uiSrc/components/base/utils/FocusTrap'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\nimport { CancelSlimIcon, CheckThinIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport ConfirmOverwrite from './ConfirmOverwrite'\nimport { isValidJSON, isValidKey, parseJsonData, wrapPath } from '../../utils'\nimport { JSONErrors } from '../../constants'\n\nimport styles from '../../styles.module.scss'\n\nconst ControlsWrapper = styled.div.attrs({ className: styles.controls })`\n  height: 34px;\n  min-height: 34px;\n`\n\nexport interface Props {\n  isPair: boolean\n  onCancel: () => void\n  onSubmit: (pair: { key?: string; value: string }) => void\n  leftPadding?: number\n  parentPath: string\n}\n\nconst AddItem = (props: Props) => {\n  const { isPair, leftPadding = 0, onCancel, onSubmit, parentPath } = props\n  const [isConfirmationVisible, setIsConfirmationVisible] =\n    useState<boolean>(false)\n\n  const { data } = useSelector(rejsonDataSelector)\n  const jsonContent = parseJsonData(data)\n\n  const [key, setKey] = useState<string>('')\n  const [value, setValue] = useState<string>('')\n  const [error, setError] = useState<Nullable<string>>(null)\n\n  useEffect(() => {\n    setError(null)\n  }, [key, value])\n\n  const handleOnEsc = (e: KeyboardEvent) => {\n    if (e.code?.toLowerCase() === keys.ESCAPE) {\n      e.stopPropagation()\n      onCancel?.()\n    }\n  }\n\n  const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (isPair && !isValidKey(key)) {\n      setError(JSONErrors.keyCorrectSyntax)\n      return\n    }\n\n    if (!isValidJSON(value)) {\n      setError(JSONErrors.valueJSONFormat)\n      return\n    }\n\n    const wrappedKey = wrapPath(key, parentPath) || ''\n    if (isPair && checkExistingPath(wrappedKey, jsonContent)) {\n      setIsConfirmationVisible(true)\n      return\n    }\n\n    onSubmit({ key, value })\n  }\n\n  const confirmApply = () => {\n    onSubmit({ key, value })\n  }\n\n  return (\n    <div\n      className={styles.row}\n      style={{\n        display: 'flex',\n        flexDirection: 'row',\n        paddingLeft: `${leftPadding}em`,\n      }}\n    >\n      <OutsideClickDetector onOutsideClick={() => {}}>\n        <div>\n          <WindowEvent event=\"keydown\" handler={(e) => handleOnEsc(e)} />\n          <FocusTrap>\n            <form\n              className=\"relative\"\n              onSubmit={(e) => handleFormSubmit(e)}\n              style={{ display: 'flex' }}\n              noValidate\n            >\n              {isPair && (\n                <FlexItem grow>\n                  <TextInput\n                    name=\"newRootKey\"\n                    value={key}\n                    error={error || undefined}\n                    placeholder=\"Enter JSON key\"\n                    onChange={setKey}\n                    data-testid=\"json-key\"\n                  />\n                </FlexItem>\n              )}\n              <FlexItem grow>\n                <TextInput\n                  name=\"newValue\"\n                  value={value}\n                  placeholder=\"Enter JSON value\"\n                  error={error || undefined}\n                  onChange={(value) => setValue(value)}\n                  data-testid=\"json-value\"\n                />\n              </FlexItem>\n              <ConfirmOverwrite\n                isOpen={isConfirmationVisible}\n                onCancel={() => setIsConfirmationVisible(false)}\n                onConfirm={confirmApply}\n              >\n                <ControlsWrapper>\n                  <IconButton\n                    size=\"M\"\n                    icon={CancelSlimIcon}\n                    color=\"primary\"\n                    aria-label=\"Cancel editing\"\n                    className={styles.declineBtn}\n                    onClick={() => onCancel?.()}\n                  />\n\n                  <IconButton\n                    size=\"M\"\n                    icon={CheckThinIcon}\n                    color=\"primary\"\n                    type=\"submit\"\n                    aria-label=\"Apply\"\n                    className={styles.applyBtn}\n                    data-testid=\"apply-btn\"\n                  />\n                </ControlsWrapper>\n              </ConfirmOverwrite>\n            </form>\n            {!!error && (\n              <div className={cx(styles.errorMessage)}>\n                <FieldMessage\n                  scrollViewOnAppear\n                  icon=\"ToastDangerIcon\"\n                  testID=\"edit-json-error\"\n                >\n                  {error}\n                </FieldMessage>\n              </div>\n            )}\n          </FocusTrap>\n        </div>\n      </OutsideClickDetector>\n    </div>\n  )\n}\n\nexport default AddItem\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item/ConfirmOverwrite.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\ninterface ConfirmOverwriteProps {\n  isOpen: boolean\n  onCancel: () => void\n  onConfirm: () => void\n  children: NonNullable<React.ReactNode>\n}\n\nconst ConfirmOverwrite = ({\n  isOpen,\n  onCancel,\n  onConfirm,\n  children,\n}: ConfirmOverwriteProps) => (\n  <RiPopover\n    ownFocus\n    anchorPosition=\"downRight\"\n    isOpen={isOpen}\n    closePopover={onCancel}\n    panelClassName={cx('popoverLikeTooltip')}\n    button={children}\n  >\n    <Text size=\"m\" style={{ fontWeight: 'bold' }}>\n      Duplicate JSON key detected\n    </Text>\n    <Text size=\"s\">\n      You already have the same JSON key. If you proceed, a value of the\n      existing JSON key will be overwritten.\n    </Text>\n    <Spacer size=\"l\" />\n    <Row justify=\"end\" gap=\"m\">\n      <SecondaryButton\n        aria-label=\"Cancel\"\n        size=\"small\"\n        onClick={onCancel}\n        data-testid=\"cancel-confirmation-btn\"\n      >\n        Cancel\n      </SecondaryButton>\n\n      <PrimaryButton\n        aria-label=\"Overwrite\"\n        size=\"small\"\n        onClick={onConfirm}\n        data-testid=\"overwrite-btn\"\n      >\n        Overwrite\n      </PrimaryButton>\n    </Row>\n  </RiPopover>\n)\n\nexport default ConfirmOverwrite\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item/index.ts",
    "content": "import AddItem from './AddItem'\n\nexport default AddItem\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item-field-action/AddItemFieldAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport AddItemFieldAction, { Props } from './AddItemFieldAction'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddItemFieldAction', () => {\n  it('renders correctly with object type', () => {\n    render(<AddItemFieldAction {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('add-field-btn')).toBeInTheDocument()\n    expect(screen.getByLabelText('Add field')).toBeInTheDocument()\n  })\n\n  it('triggers onClickSetKVPair when the button is clicked', () => {\n    const onClickSetKVPair = jest.fn()\n    render(\n      <AddItemFieldAction\n        {...instance(mockedProps)}\n        onClickSetKVPair={onClickSetKVPair}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-field-btn'))\n    expect(onClickSetKVPair).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item-field-action/AddItemFieldAction.tsx",
    "content": "import React from 'react'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\nimport { getBrackets } from '../../utils'\nimport styles from '../../styles.module.scss'\n\nexport interface Props {\n  leftPadding: number\n  type: string\n  onClickSetKVPair: () => void\n}\n\nconst AddItemFieldAction = ({ leftPadding, type, onClickSetKVPair }: Props) => (\n  <div className={styles.row} style={{ paddingLeft: `${leftPadding}em` }}>\n    <span className={styles.defaultFont}>{getBrackets(type, 'end')}</span>\n    <IconButton\n      icon={PlusIcon}\n      size=\"S\"\n      className={styles.jsonButtonStyle}\n      onClick={onClickSetKVPair}\n      aria-label=\"Add field\"\n      data-testid=\"add-field-btn\"\n    />\n  </div>\n)\n\nexport default AddItemFieldAction\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/add-item-field-action/index.ts",
    "content": "import AddItemFieldAction from './AddItemFieldAction'\n\nexport default AddItemFieldAction\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/edit-entire-item-action/EditEntireItemAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { JSONErrors } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/constants'\nimport EditEntireItemAction, { Props } from './EditEntireItemAction'\n\nconst mockedProps = mock<Props>()\n\nconst valueOfEntireItem = '\"Sample string\"'\n\ndescribe('EditEntireItemAction', () => {\n  it('renders correctly with provided props', () => {\n    render(\n      <EditEntireItemAction\n        {...instance(mockedProps)}\n        initialValue={valueOfEntireItem}\n      />,\n    )\n\n    expect(screen.getByTestId('json-value')).toBeInTheDocument()\n    expect(screen.getByTestId('json-value')).toHaveValue(valueOfEntireItem)\n  })\n\n  it('triggers handleUpdateValueFormSubmit when the form is submitted', () => {\n    const handleUpdateValueFormSubmit = jest.fn()\n    render(\n      <EditEntireItemAction\n        {...instance(mockedProps)}\n        initialValue={valueOfEntireItem}\n        onSubmit={handleUpdateValueFormSubmit}\n      />,\n    )\n\n    fireEvent.submit(screen.getByTestId('json-entire-form'))\n    expect(handleUpdateValueFormSubmit).toHaveBeenCalled()\n  })\n\n  it('shouuld show error and do not submit', () => {\n    const handleUpdateValueFormSubmit = jest.fn()\n    render(\n      <EditEntireItemAction\n        {...instance(mockedProps)}\n        initialValue=\"xxxx\"\n        onSubmit={handleUpdateValueFormSubmit}\n      />,\n    )\n\n    fireEvent.submit(screen.getByTestId('json-entire-form'))\n    expect(screen.getByTestId('edit-json-error')).toHaveTextContent(\n      JSONErrors.valueJSONFormat,\n    )\n    expect(handleUpdateValueFormSubmit).not.toHaveBeenCalled()\n  })\n\n  it('should show confirmation modal when JSON has duplicate keys, and confirm submit on confirm', () => {\n    const handleUpdateValueFormSubmit = jest.fn()\n\n    const duplicateKeyJson = `\n  {\n    \"test\": \"one\",\n    \"test\": \"two\"\n  }\n  `\n\n    render(\n      <EditEntireItemAction\n        {...instance(mockedProps)}\n        initialValue={duplicateKeyJson}\n        onSubmit={handleUpdateValueFormSubmit}\n      />,\n    )\n\n    fireEvent.submit(screen.getByTestId('json-entire-form'))\n\n    expect(screen.getByText(/Duplicate JSON key detected/i)).toBeInTheDocument()\n\n    const confirmButton = screen.getByLabelText(/overwrite/i)\n    fireEvent.click(confirmButton)\n\n    expect(handleUpdateValueFormSubmit).toHaveBeenCalledWith(duplicateKeyJson)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/edit-entire-item-action/EditEntireItemAction.tsx",
    "content": "import React, { useState } from 'react'\nimport cx from 'classnames'\nimport jsonValidator from 'json-dup-key-validator'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { CancelSlimIcon, CheckThinIcon } from 'uiSrc/components/base/icons'\nimport FieldMessage from 'uiSrc/components/field-message/FieldMessage'\nimport { Nullable } from 'uiSrc/utils'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { WindowEvent } from 'uiSrc/components/base/utils/WindowEvent'\nimport { FocusTrap } from 'uiSrc/components/base/utils/FocusTrap'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { TextArea } from 'uiSrc/components/base/inputs'\nimport { isValidJSON } from '../../utils'\nimport { JSONErrors } from '../../constants'\n\nimport styles from '../../styles.module.scss'\nimport ConfirmOverwrite from '../add-item/ConfirmOverwrite'\n\nexport interface Props {\n  initialValue: string\n  onCancel?: () => void\n  onSubmit: (value: string) => void\n}\n\nconst EditEntireItemAction = (props: Props) => {\n  const { initialValue, onCancel, onSubmit } = props\n  const [value, setValue] = useState<string>(initialValue)\n  const [error, setError] = useState<Nullable<string>>(null)\n  const [isConfirmationVisible, setIsConfirmationVisible] =\n    useState<boolean>(false)\n\n  const handleOnEsc = (e: KeyboardEvent) => {\n    if (e.code?.toLowerCase() === keys.ESCAPE) {\n      e.stopPropagation()\n      onCancel?.()\n    }\n  }\n\n  const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n\n    if (!isValidJSON(value)) {\n      setError(JSONErrors.valueJSONFormat)\n      return\n    }\n\n    const validationError = jsonValidator.validate(value, false)\n\n    if (validationError) {\n      setIsConfirmationVisible(true)\n      return\n    }\n\n    onSubmit(value)\n  }\n\n  const confirmApply = () => {\n    onSubmit(value)\n  }\n\n  return (\n    <div className={styles.row}>\n      <div className={styles.fullWidthContainer}>\n        <OutsideClickDetector onOutsideClick={() => onCancel?.()}>\n          <div>\n            <WindowEvent event=\"keydown\" handler={(e) => handleOnEsc(e)} />\n            <FocusTrap>\n              <form\n                className=\"relative\"\n                onSubmit={handleFormSubmit}\n                data-testid=\"json-entire-form\"\n                noValidate\n              >\n                <FlexItem grow>\n                  <TextArea\n                    valid={!error}\n                    className={styles.fullWidthTextArea}\n                    value={value}\n                    placeholder=\"Enter JSON value\"\n                    onChange={setValue}\n                    data-testid=\"json-value\"\n                  />\n                </FlexItem>\n                <ConfirmOverwrite\n                  isOpen={isConfirmationVisible}\n                  onCancel={() => setIsConfirmationVisible(false)}\n                  onConfirm={confirmApply}\n                >\n                  <div className={cx(styles.controls, styles.controlsBottom)}>\n                    <IconButton\n                      icon={CancelSlimIcon}\n                      aria-label=\"Cancel add\"\n                      className={styles.declineBtn}\n                      onClick={onCancel}\n                      data-testid=\"cancel-edit-btn\"\n                    />\n                    <IconButton\n                      icon={CheckThinIcon}\n                      color=\"primary\"\n                      type=\"submit\"\n                      aria-label=\"Apply\"\n                      className={styles.applyBtn}\n                      data-testid=\"apply-edit-btn\"\n                    />\n                  </div>\n                </ConfirmOverwrite>\n              </form>\n              {error && (\n                <div\n                  className={cx(\n                    styles.errorMessage,\n                    styles.errorMessageForTextArea,\n                  )}\n                >\n                  <FieldMessage\n                    scrollViewOnAppear\n                    icon=\"ToastDangerIcon\"\n                    testID=\"edit-json-error\"\n                  >\n                    {error}\n                  </FieldMessage>\n                </div>\n              )}\n            </FocusTrap>\n          </div>\n        </OutsideClickDetector>\n      </div>\n    </div>\n  )\n}\n\nexport default EditEntireItemAction\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/edit-entire-item-action/index.ts",
    "content": "import EditEntireItemAction from './EditEntireItemAction'\n\nexport default EditEntireItemAction\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/edit-item-field-action/EditItemFieldAction.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  fireEvent,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n  act,\n} from 'uiSrc/utils/test-utils'\nimport EditItemFieldAction, { Props } from './EditItemFieldAction'\n\nconst mockedProps = mock<Props>()\n\ndescribe('EditItemFieldAction Component', () => {\n  it('renders correctly', () => {\n    render(<EditItemFieldAction {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('edit-json-field')).toBeInTheDocument()\n  })\n\n  it('triggers onClickEditEntireItem when the edit button is clicked', () => {\n    const onClickEditEntireItem = jest.fn()\n    render(\n      <EditItemFieldAction\n        {...instance(mockedProps)}\n        onClickEditEntireItem={onClickEditEntireItem}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('edit-json-field'))\n    expect(onClickEditEntireItem).toHaveBeenCalled()\n  })\n\n  it('triggers handleSubmitRemoveKey when the delete button is clicked', async () => {\n    const handleSubmitRemoveKey = jest.fn()\n    render(\n      <EditItemFieldAction\n        {...instance(mockedProps)}\n        keyName=\"a\"\n        handleSubmitRemoveKey={handleSubmitRemoveKey}\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('remove-json-field-icon'))\n    })\n\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('remove-json-field'))\n\n    expect(handleSubmitRemoveKey).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/edit-item-field-action/EditItemFieldAction.tsx",
    "content": "import React, { useState } from 'react'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport {\n  bufferToString,\n  createDeleteFieldHeader,\n  createDeleteFieldMessage,\n} from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport styles from '../../styles.module.scss'\n\nexport interface Props {\n  keyName: string\n  selectedKey: string | RedisResponseBuffer\n  path: string\n  handleSubmitRemoveKey: (path: string, jsonKeyName: string) => void\n  onClickEditEntireItem: () => void\n  ['data-testid']?: string\n}\n\nconst EditItemFieldAction = ({\n  keyName,\n  selectedKey,\n  path,\n  handleSubmitRemoveKey,\n  onClickEditEntireItem,\n  'data-testid': testId = 'edit-json-field',\n}: Props) => {\n  const [deleting, setDeleting] = useState<string>('')\n\n  return (\n    <div className={styles.actionButtons}>\n      <IconButton\n        icon={EditIcon}\n        className={styles.jsonButtonStyle}\n        onClick={onClickEditEntireItem}\n        aria-label=\"Edit field\"\n        size=\"S\"\n        data-testid={testId}\n      />\n      <PopoverDelete\n        header={createDeleteFieldHeader(keyName)}\n        text={createDeleteFieldMessage(bufferToString(selectedKey))}\n        item={keyName}\n        deleting={deleting}\n        closePopover={() => setDeleting('')}\n        updateLoading={false}\n        showPopover={setDeleting}\n        handleDeleteItem={() => handleSubmitRemoveKey(path, keyName)}\n        testid=\"remove-json-field\"\n      />\n    </div>\n  )\n}\n\nexport default EditItemFieldAction\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/edit-item-field-action/index.ts",
    "content": "import EditItemFieldAction from './EditItemFieldAction'\n\nexport default EditItemFieldAction\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/components/index.ts",
    "content": "import AddItemFieldAction from './add-item-field-action'\nimport EditEntireItemAction from './edit-entire-item-action'\nimport EditItemFieldAction from './edit-item-field-action'\nimport AddItem from './add-item'\n\nexport {\n  AddItem,\n  AddItemFieldAction,\n  EditEntireItemAction,\n  EditItemFieldAction,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/constants.ts",
    "content": "export const JSONErrors = {\n  keyCorrectSyntax: 'Key should have correct syntax.',\n  valueJSONFormat: 'Value should have JSON format.',\n}\n\nexport const MIN_LEFT_PADDING_NESTING = 1\nexport const MAX_LEFT_PADDING_NESTING = 8\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/index.ts",
    "content": "export { RejsonDetailsWrapper } from './RejsonDetailsWrapper'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/interfaces.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nexport type JSONScalarValue = string | number | boolean | null\n\nexport type JSONObjectValue = JSONScalarValue | JSONScalarValue[]\n\nexport interface IJSONObject {\n  [keyName: string]: IJSONValue\n}\n\nexport enum ObjectTypes {\n  Object = 'object',\n  Array = 'array',\n}\nexport type JSONArrayValue = IJSONObject | JSONObjectValue\nexport type IJSONValue =\n  | JSONScalarValue\n  | IJSONObject\n  | JSONObjectValue\n  | JSONArrayValue[]\n\nexport interface IJSONDocument {\n  key: string\n  type: string\n  cardinality?: number\n  value: IJSONValue\n}\n\nexport interface REJSONResponse {\n  type: string\n  downloaded: boolean\n  data: JSONArrayValue[] | IJSONDocument\n}\n\ninterface UpdateValueBody {\n  // This interface has been kept empty for now.\n  // If any changes is to be made to the body format for updating purpose , the necessary properties will be added here.\n}\n\nexport type IJSONData = IJSONValue | IJSONDocument | IJSONDocument[]\n\nexport interface BaseProps {\n  length?: number\n  data: IJSONData\n  dataType: string\n  parentPath?: string\n  selectedKey: RedisResponseBuffer\n  isDownloaded: boolean\n  onJsonKeyExpandAndCollapse: (isExpanded: boolean, path: string) => void\n  expandedRows: Set<string>\n}\n\nexport interface DynamicTypesProps {\n  data: IJSONData\n  parentPath?: string\n  leftPadding?: number\n  expandedRows: Set<string>\n  selectedKey: RedisResponseBuffer\n  isDownloaded: boolean\n  onClickRemoveKey: (path: string, keyName: string) => void\n  onClickFunc?: (path: string) => void\n  onJsonKeyExpandAndCollapse: (isExpanded: boolean, path: string) => void\n  handleSubmitUpdateValue?: (body: UpdateValueBody) => void\n  handleFetchVisualisationResults: (\n    path: string,\n    forceRetrieve?: boolean,\n  ) => Promise<any>\n  handleAppendRejsonObjectItemAction: (\n    keyName: RedisResponseBuffer,\n    path: string,\n    data: string,\n  ) => void\n  handleSetRejsonDataAction: (\n    keyName: RedisResponseBuffer,\n    path: string,\n    data: string,\n  ) => void\n}\n\ninterface JSONCommonProps {\n  keyName: string | number\n  value: string | number | boolean | bigint\n  cardinality?: number\n  selectedKey: RedisResponseBuffer\n  path?: string\n  parentPath: string\n  leftPadding: number\n  handleSubmitRemoveKey: (path: string, jsonKeyName: string) => void\n}\n\nexport interface JSONScalarProps extends JSONCommonProps {\n  isRoot?: boolean\n}\n\nexport interface JSONObjectProps extends JSONCommonProps {\n  type: ObjectTypes\n  isDownloaded: boolean\n  expandedRows: Set<string>\n  onJsonKeyExpandAndCollapse: (isExpanded: boolean, path: string) => void\n  onClickRemoveKey: (path: string, keyName: string) => void\n  handleSubmitUpdateValue?: (body: UpdateValueBody) => void\n  handleSetRejsonDataAction: (\n    keyName: RedisResponseBuffer,\n    path: string,\n    data: string,\n  ) => void\n  handleAppendRejsonObjectItemAction: (\n    keyName: RedisResponseBuffer,\n    path: string,\n    data: string,\n  ) => void\n  handleFetchVisualisationResults: (\n    path: string,\n    forceRetrieve?: boolean,\n  ) => Promise<any>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/MonacoEditor.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { configureStore } from '@reduxjs/toolkit'\nimport { Provider } from 'react-redux'\n\nimport MonacoEditor from './MonacoEditor'\n\nconst mockStore = configureStore({\n  reducer: () => ({\n    browser: {\n      rejson: {},\n    },\n    app: {\n      features: {\n        featureFlags: { features: { envDependent: { flag: true } } },\n      },\n    },\n  }),\n})\n\nconst renderWithRedux = (ui: React.ReactNode) =>\n  render(<Provider store={mockStore}>{ui}</Provider>)\n\nconst commonProps = {\n  length: 1,\n  selectedKey: 'key' as any,\n  dataType: 'ReJSON-RL',\n  isDownloaded: true,\n  onJsonKeyExpandAndCollapse: () => {},\n  expandedRows: new Set(['someKey']),\n}\n\njest.mock('uiSrc/components/monaco-editor/useMonacoValidation', () => ({\n  __esModule: true,\n  default: () => ({\n    isValid: true,\n    isValidating: false,\n  }),\n}))\n\nit('should preserve large numbers in Monaco editor', async () => {\n  const bigNumber = '245343644508855571'\n  const data = { huge: BigInt(bigNumber) }\n\n  renderWithRedux(<MonacoEditor data={data as any} {...commonProps} />)\n\n  const editor = await screen.findByTestId('json-data-editor')\n\n  expect(editor.textContent).toContain(`${bigNumber}`)\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/MonacoEditor.tsx",
    "content": "import React, { useRef, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { monaco } from 'react-monaco-editor'\nimport JSONbig from 'json-bigint'\n\nimport {\n  MonacoEditor as Editor,\n  useMonacoValidation,\n} from 'uiSrc/components/monaco-editor'\nimport { setReJSONDataAction } from 'uiSrc/slices/browser/rejson'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { BaseProps } from '../interfaces'\nimport { useChangeEditorType } from '../../change-editor-type-button'\n\nimport styles from '../styles.module.scss'\n\nconst ROOT_PATH = '$'\n\n// We use `storeAsString: true` to ensure large numbers are serialized as strings.\n// This avoids precision loss for values larger than Number.MAX_SAFE_INTEGER (2^53 - 1),\n// which would otherwise be inaccurately represented in JavaScript.\n// Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER\nconst jsonToReadableString = (data: any) =>\n  JSONbig({ storeAsString: true }).stringify(data, null, 2)\n\nconst MonacoEditor = (props: BaseProps) => {\n  const { data, length, selectedKey } = props\n  const dispatch = useDispatch()\n  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)\n\n  const originalData = jsonToReadableString(data)\n  const [value, setValue] = useState(originalData)\n\n  const { isValid, isValidating } = useMonacoValidation(editorRef)\n  const isButtonEnabled = isValid && !isValidating && originalData !== value\n\n  const onEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {\n    editorRef.current = editor\n  }\n\n  const { switchEditorType } = useChangeEditorType()\n\n  const submitUpdate = () => {\n    dispatch(setReJSONDataAction(selectedKey, ROOT_PATH, value, true, length))\n  }\n\n  return (\n    <div\n      className={styles.monacoEditorJsonData}\n      id=\"monaco-editor-json-data\"\n      data-testid=\"monaco-editor-json-data\"\n    >\n      <Editor\n        language=\"json\"\n        value={value}\n        isEditable\n        onChange={setValue}\n        data-testid=\"json-data-editor\"\n        wrapperClassName={styles.editor}\n        editorWrapperClassName={styles.editorWrapper}\n        onEditorDidMount={onEditorDidMount}\n      />\n      <Spacer size=\"m\" />\n      <Row justify=\"end\" gap=\"m\" className={styles.actions}>\n        <SecondaryButton\n          onClick={switchEditorType}\n          data-testid=\"json-data-cancel-btn\"\n        >\n          Close\n        </SecondaryButton>\n\n        <PrimaryButton\n          disabled={!isButtonEnabled}\n          onClick={submitUpdate}\n          data-testid=\"json-data-update-btn\"\n        >\n          Overwrite Data\n        </PrimaryButton>\n      </Row>\n    </div>\n  )\n}\n\nexport default MonacoEditor\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/monaco-editor/index.ts",
    "content": "import MonacoEditor from './MonacoEditor'\n\nexport default MonacoEditor\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport { BaseProps } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/interfaces'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport * as appFeaturesSlice from 'uiSrc/slices/app/features'\nimport RejsonDetails from './RejsonDetails'\n\nconst mockedProps = mock<BaseProps>()\n\nconst mockedJSONObject = [\n  {\n    key: '_id',\n    path: '$[\"_id\"]',\n    cardinality: 1,\n    type: 'string',\n    value: '60adf79282e738b05531b345',\n  },\n  {\n    key: '_id2',\n    path: '$[\"_id\"]',\n    cardinality: 1,\n    type: 'string',\n    value: '60adf79282b05531b345',\n  },\n  {\n    key: '_id3',\n    path: '$[\"_id\"]',\n    cardinality: 3,\n    type: 'array',\n    value: [1, 2, 3],\n  },\n]\n\nconst mockedJSONString = 'string'\nconst mockedJSONNull = null\nconst mockedJSONBoolean = true\nconst mockedJSONNumber = 123123\nconst mockedSelectedKey = stringToBuffer('key')\n\nconst mockEnvDependentFeatureFlag = (value = true) => {\n  jest\n    .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n    .mockReturnValue({\n      envDependent: {\n        flag: value,\n      },\n    })\n}\n\ndescribe('RejsonDetails', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <RejsonDetails\n          {...instance(mockedProps)}\n          selectedKey={mockedSelectedKey}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render switch editor button ENABLED when envDependent flag is enabled', () => {\n    mockEnvDependentFeatureFlag()\n\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={mockedJSONObject}\n        dataType=\"object\"\n        parentPath=\"$\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded={false}\n      />,\n    )\n\n    const button = screen.getByRole('button', { name: /change editor type/i })\n    expect(button).toBeEnabled()\n  })\n\n  it('should render switch editor button DISABLED when envDependent flag is enabled', () => {\n    mockEnvDependentFeatureFlag(false)\n\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={mockedJSONObject}\n        dataType=\"object\"\n        parentPath=\"$\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded={false}\n      />,\n    )\n\n    const button = screen.getByRole('button', { name: /change editor type/i })\n    expect(button).not.toBeEnabled()\n  })\n\n  describe('should render JSON object', () => {\n    it('should be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={mockedJSONObject}\n            dataType=\"object\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded={false}\n          />,\n        ),\n      ).toBeTruthy()\n    })\n    it('should not be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={mockedJSONObject}\n            dataType=\"object\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded\n          />,\n        ),\n      ).toBeTruthy()\n    })\n  })\n\n  describe('should render JSON array', () => {\n    it('should not be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={[1, 2, 3]}\n            dataType=\"array\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded\n          />,\n        ),\n      ).toBeTruthy()\n    })\n  })\n\n  describe('should render JSON string', () => {\n    it('should be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={mockedJSONString}\n            dataType=\"string\"\n            parentPath=\"$\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded={false}\n          />,\n        ),\n      ).toBeTruthy()\n    })\n    it('should not be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={mockedJSONString}\n            dataType=\"string\"\n            parentPath=\"$\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded\n          />,\n        ),\n      ).toBeTruthy()\n    })\n  })\n\n  describe('should render JSON null', () => {\n    it('should be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={mockedJSONNull}\n            dataType=\"null\"\n            parentPath=\"$\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded={false}\n          />,\n        ),\n      ).toBeTruthy()\n    })\n    it('should not be downloaded', () => {\n      expect(\n        render(\n          <RejsonDetails\n            {...instance(mockedProps)}\n            data={mockedJSONNull}\n            dataType=\"null\"\n            parentPath=\"$\"\n            selectedKey={mockedSelectedKey}\n            isDownloaded\n          />,\n        ),\n      ).toBeTruthy()\n    })\n  })\n\n  it('should render JSON boolean', () => {\n    expect(\n      render(\n        <RejsonDetails\n          {...instance(mockedProps)}\n          data={mockedJSONBoolean}\n          dataType=\"boolean\"\n          parentPath=\"$\"\n          selectedKey={mockedSelectedKey}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render JSON number', () => {\n    expect(\n      render(\n        <RejsonDetails\n          {...instance(mockedProps)}\n          data={mockedJSONNumber}\n          dataType=\"number\"\n          parentPath=\"$\"\n          selectedKey={mockedSelectedKey}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should open inline editor to add JSON key value for object', () => {\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={{ a: 1, b: 2 }}\n        dataType=\"object\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-object-btn'))\n    expect(screen.getByTestId('json-key')).toBeInTheDocument()\n    expect(screen.getByTestId('json-value')).toBeInTheDocument()\n  })\n\n  it.skip('should be able to add proper key value into json object', () => {\n    const handleSubmitJsonUpdateValue = jest.fn()\n    const handleSubmitUpdateValue = jest.fn()\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={{ a: 1, b: 2 }}\n        dataType=\"object\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-object-btn'))\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"key\"' },\n    })\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '\"value\"' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n    expect(handleSubmitJsonUpdateValue).toBeCalled()\n    expect(handleSubmitUpdateValue).not.toBeCalled()\n  })\n\n  it.skip('should not be able to add wrong key value into json object', () => {\n    const handleSubmitJsonUpdateValue = jest.fn()\n    const handleSubmitUpdateValue = jest.fn()\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={{ a: 1, b: 2 }}\n        dataType=\"object\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-object-btn'))\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"key\"' },\n    })\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '{' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n    expect(handleSubmitJsonUpdateValue).not.toBeCalled()\n    expect(handleSubmitUpdateValue).not.toBeCalled()\n  })\n\n  it.skip('should be able to add proper value into json array', () => {\n    const handleSubmitJsonUpdateValue = jest.fn()\n    const handleSubmitUpdateValue = jest.fn()\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={[1, 2, 3]}\n        dataType=\"array\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-array-btn'))\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '1' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n    expect(handleSubmitJsonUpdateValue).toBeCalled()\n    expect(handleSubmitUpdateValue).not.toBeCalled()\n  })\n\n  it.skip('should not be able to add wrong value into json array', () => {\n    const handleSubmitJsonUpdateValue = jest.fn()\n    const handleSubmitUpdateValue = jest.fn()\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={[1, 2, 3]}\n        dataType=\"array\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-array-btn'))\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '{' },\n    })\n    expect(handleSubmitJsonUpdateValue).not.toBeCalled()\n    expect(handleSubmitUpdateValue).not.toBeCalled()\n  })\n\n  it.skip('should submit to add proper key value into json object', () => {\n    const handleSubmitJsonUpdateValue = jest.fn()\n    const handleSubmitUpdateValue = jest.fn()\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={{ a: 1, b: 2 }}\n        dataType=\"object\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-object-btn'))\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"key\"' },\n    })\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '\"value\"' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n    expect(handleSubmitJsonUpdateValue).toBeCalled()\n    expect(handleSubmitUpdateValue).not.toBeCalled()\n  })\n\n  it.skip('should submit to add proper value into json array', () => {\n    const handleSubmitJsonUpdateValue = jest.fn()\n    const handleSubmitUpdateValue = jest.fn()\n    render(\n      <RejsonDetails\n        {...instance(mockedProps)}\n        data={[1, 2, 3]}\n        dataType=\"array\"\n        selectedKey={mockedSelectedKey}\n        isDownloaded\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('add-array-btn'))\n    fireEvent.change(screen.getByTestId('json-value'), {\n      target: { value: '1' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n    expect(handleSubmitJsonUpdateValue).toBeCalled()\n    expect(handleSubmitUpdateValue).not.toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/RejsonDetails.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { useDispatch } from 'react-redux'\n\nimport cx from 'classnames'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\nimport {\n  appendReJSONArrayItemAction,\n  fetchVisualisationResults,\n  removeReJSONKeyAction,\n  setReJSONDataAction,\n} from 'uiSrc/slices/browser/rejson'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { getBrackets, isRealArray, isRealObject, wrapPath } from '../utils'\nimport { BaseProps, ObjectTypes } from '../interfaces'\nimport RejsonDynamicTypes from '../rejson-dynamic-types'\nimport { AddItem } from '../components'\nimport ChangeEditorTypeButton from '../../change-editor-type-button'\n\nimport styles from '../styles.module.scss'\n\nconst RejsonDetails = (props: BaseProps) => {\n  const {\n    data,\n    selectedKey,\n    length,\n    dataType,\n    parentPath,\n    isDownloaded,\n    onJsonKeyExpandAndCollapse,\n    expandedRows,\n  } = props\n\n  const [addRootKVPair, setAddRootKVPair] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  const handleFetchVisualisationResults = (\n    path: string,\n    forceRetrieve = false,\n  ) => dispatch<any>(fetchVisualisationResults(path, forceRetrieve))\n\n  const handleAppendRejsonArrayItemAction = (\n    keyName: RedisResponseBuffer,\n    path: string,\n    data: string,\n  ) => {\n    dispatch(appendReJSONArrayItemAction(keyName, path, data, length))\n  }\n\n  const handleSetRejsonDataAction = (\n    keyName: RedisResponseBuffer,\n    path: string,\n    data: string,\n  ) => {\n    dispatch(setReJSONDataAction(keyName, path, data, false, length))\n  }\n\n  const handleFormSubmit = ({\n    key,\n    value,\n  }: {\n    key?: string\n    value: string\n  }) => {\n    setAddRootKVPair(false)\n    if (isRealArray(data, dataType)) {\n      handleAppendRejsonArrayItemAction(selectedKey, '$', value)\n      return\n    }\n\n    const updatedPath = wrapPath(key as string)\n    if (updatedPath) {\n      handleSetRejsonDataAction(selectedKey, updatedPath, value)\n    }\n  }\n\n  const onClickRemoveKey = (path: string, keyName: string) => {\n    dispatch(removeReJSONKeyAction(selectedKey, path || '$', keyName, length))\n  }\n\n  const onClickSetRootKVPair = () => {\n    setAddRootKVPair(!addRootKVPair)\n  }\n\n  const isObject = isRealObject(data, dataType)\n  const isArray = isRealArray(data, dataType)\n\n  return (\n    <div className={styles.jsonData} id=\"jsonData\" data-testid=\"json-data\">\n      <>\n        {(isObject || isArray) && (\n          <div className={cx(styles.row, styles.topRow)}>\n            <span>\n              {getBrackets(\n                isObject ? ObjectTypes.Object : ObjectTypes.Array,\n                'start',\n              )}\n            </span>\n            <ChangeEditorTypeButton />\n          </div>\n        )}\n        <RejsonDynamicTypes\n          data={data}\n          parentPath={parentPath}\n          selectedKey={selectedKey}\n          isDownloaded={isDownloaded}\n          expandedRows={expandedRows}\n          onClickRemoveKey={onClickRemoveKey}\n          onJsonKeyExpandAndCollapse={onJsonKeyExpandAndCollapse}\n          handleAppendRejsonObjectItemAction={handleAppendRejsonArrayItemAction}\n          handleSetRejsonDataAction={handleSetRejsonDataAction}\n          handleFetchVisualisationResults={handleFetchVisualisationResults}\n        />\n        {addRootKVPair && (\n          <AddItem\n            isPair={isObject}\n            onCancel={() => setAddRootKVPair(false)}\n            onSubmit={handleFormSubmit}\n            parentPath={parentPath || '$'}\n          />\n        )}\n        {(isObject || isArray) && (\n          <div className={styles.row}>\n            <span>\n              {getBrackets(\n                isObject ? ObjectTypes.Object : ObjectTypes.Array,\n                'end',\n              )}\n            </span>\n            {!addRootKVPair && (\n              <IconButton\n                icon={PlusIcon}\n                size=\"S\"\n                className={styles.buttonStyle}\n                onClick={onClickSetRootKVPair}\n                aria-label=\"Add field\"\n                data-testid={isObject ? 'add-object-btn' : 'add-array-btn'}\n              />\n            )}\n          </div>\n        )}\n      </>\n    </div>\n  )\n}\n\nexport default RejsonDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-details/index.ts",
    "content": "import RejsonDetails from './RejsonDetails'\n\nexport default RejsonDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-dynamic-types/RejsonDynamicTypes.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { DynamicTypesProps } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/interfaces'\nimport RejsonDynamicTypes from './RejsonDynamicTypes'\n\nconst mockedProps = mock<DynamicTypesProps>()\n\nconst mockedDownloadedSimpleArray = [1, 2, 3]\n\ndescribe('RejsonDynamicTypes Component', () => {\n  it('renders correctly simple downloaded JSON', () => {\n    render(\n      <RejsonDynamicTypes\n        {...instance(mockedProps)}\n        data={mockedDownloadedSimpleArray}\n        isDownloaded\n      />,\n    )\n\n    expect(screen.queryAllByTestId('json-scalar-value')).toHaveLength(3)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-dynamic-types/RejsonDynamicTypes.tsx",
    "content": "import React from 'react'\n\nimport { isNull, isObject } from 'lodash'\nimport { MAX_LEFT_PADDING_NESTING } from '../constants'\nimport { DynamicTypesProps, ObjectTypes } from '../interfaces'\nimport { generatePath, isScalar } from '../utils'\n\nimport RejsonScalar from '../rejson-scalar'\nimport RejsonObject from '../rejson-object'\n\nconst RejsonDynamicTypes = (props: DynamicTypesProps) => {\n  const {\n    data,\n    selectedKey,\n    parentPath = '',\n    isDownloaded,\n    expandedRows,\n    leftPadding = 0,\n    onClickRemoveKey,\n    onJsonKeyExpandAndCollapse,\n    handleSubmitUpdateValue,\n    handleFetchVisualisationResults,\n    handleAppendRejsonObjectItemAction,\n    handleSetRejsonDataAction,\n  } = props\n\n  const nextLeftPadding = Math.min(leftPadding + 1, MAX_LEFT_PADDING_NESTING)\n\n  const renderScalar = (data: any) => (\n    <RejsonScalar\n      isRoot={isNull(data.key) && data.parentPath === '$'}\n      leftPadding={nextLeftPadding}\n      selectedKey={selectedKey}\n      path={data.path}\n      parentPath={data.parentPath}\n      keyName={data.key}\n      key={generatePath(data.parentPath, data.key)}\n      value={data.value}\n      handleSubmitRemoveKey={(path: string, keyName: string) =>\n        onClickRemoveKey(path, keyName)\n      }\n    />\n  )\n\n  const renderJSONObject = (data: any, type: string) => (\n    <RejsonObject\n      expandedRows={expandedRows}\n      type={type as ObjectTypes}\n      isDownloaded={isDownloaded}\n      leftPadding={nextLeftPadding}\n      selectedKey={selectedKey}\n      path={data.path}\n      parentPath={data.parentPath}\n      key={generatePath(data.parentPath, data.key)}\n      keyName={data.key}\n      onJsonKeyExpandAndCollapse={onJsonKeyExpandAndCollapse}\n      value={data.value || {}}\n      cardinality={data.cardinality}\n      handleSubmitRemoveKey={(path: string, keyName: string) =>\n        onClickRemoveKey(path, keyName)\n      }\n      onClickRemoveKey={onClickRemoveKey}\n      handleSubmitUpdateValue={handleSubmitUpdateValue}\n      handleSetRejsonDataAction={handleSetRejsonDataAction}\n      handleFetchVisualisationResults={handleFetchVisualisationResults}\n      handleAppendRejsonObjectItemAction={handleAppendRejsonObjectItemAction}\n    />\n  )\n\n  const renderRejsonDataBeDownloaded = (item: any, i: number) => {\n    if (isScalar(item))\n      return renderScalar({ key: i || null, value: item, parentPath })\n\n    const data = { ...item, parentPath }\n    if (['array', 'object'].includes(item.type))\n      return renderJSONObject(data, item.type)\n\n    return renderScalar(data)\n  }\n\n  const renderArrayItem = (key: string | number, value: any) => {\n    // it is the same to render object or array\n    if (isObject(value)) {\n      return renderJSONObject(\n        {\n          key,\n          value,\n          cardinality: Object.keys(value).length,\n          parentPath,\n        },\n        Array.isArray(value) ? ObjectTypes.Array : ObjectTypes.Object,\n      )\n    }\n\n    return renderScalar({ key, value, parentPath })\n  }\n\n  const renderResult = (data: any) => {\n    if (isScalar(data)) {\n      return renderScalar({ key: null, value: data, parentPath })\n    }\n\n    if (!isDownloaded) {\n      return data?.map(renderRejsonDataBeDownloaded)\n    }\n\n    if (Array.isArray(data)) {\n      return data?.map((item, i) => renderArrayItem(i, item))\n    }\n\n    return Object.entries(data).map(([key, value]) =>\n      renderArrayItem(key, value),\n    )\n  }\n\n  return renderResult(data)\n}\n\nexport default RejsonDynamicTypes\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-dynamic-types/index.ts",
    "content": "import RejsonDynamicTypes from './RejsonDynamicTypes'\n\nexport default RejsonDynamicTypes\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/RejsonObject.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen, act } from 'uiSrc/utils/test-utils'\n\nimport JSONObject from './RejsonObject'\nimport { JSONObjectProps, ObjectTypes } from '../interfaces'\n\nconst EXPAND_OBJECT = 'expand-object'\nconst JSON_VALUE = 'json-value'\nconst JSON_VALUE_DOT = '.jsonValue'\nconst EDIT_OBJECT_BTN = 'edit-object-btn'\n\nconst mockedProps = mock<JSONObjectProps>()\n\nconst mockedSimpleJSONObject = { a: 1, b: null, c: 'string', d: true }\nconst mockedDownloadedObjectWithObjects = {\n  a: { b: 1, c: 2, d: null, e: 'string' },\n}\nconst mockedDownloadedObjectWithArray = {\n  a: [1, null, 'aaa'],\n}\n\njest.mock('uiSrc/slices/browser/rejson', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/rejson'),\n  setReJSONDataAction: jest.fn,\n  fetchVisualisationResults: jest\n    .fn()\n    .mockReturnValue(Promise.resolve({ data: mockedSimpleJSONObject })),\n}))\n\ndescribe.skip('JSONObject', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <JSONObject\n          {...instance(mockedProps)}\n          value={mockedSimpleJSONObject}\n          keyName=\"keyName\"\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should expand simple downloaded JSON', async () => {\n    const { container } = render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n\n    expect(container.querySelectorAll(JSON_VALUE_DOT)).toHaveLength(4)\n  })\n\n  it('should render and expand downloaded JSON with objects', async () => {\n    const { container } = render(\n      <JSONObject\n        {...instance(mockedProps)}\n        value={mockedDownloadedObjectWithObjects}\n        isDownloaded\n        keyName=\"\"\n        type={ObjectTypes.Object}\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n\n    expect(container.querySelectorAll(JSON_VALUE_DOT)).toHaveLength(4)\n  })\n\n  it('should render and expand downloaded JSON with array', async () => {\n    const { container } = render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedDownloadedObjectWithArray}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId('expand-array'))\n    })\n\n    expect(container.querySelectorAll(JSON_VALUE_DOT)).toHaveLength(3)\n  })\n\n  it('should render simple not downloaded JSON', async () => {\n    const fetchVisualisationResults = jest\n      .fn()\n      .mockReturnValue(Promise.resolve({ data: mockedSimpleJSONObject }))\n    const { container } = render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded={false}\n        onJsonKeyExpandAndCollapse={jest.fn()}\n        handleFetchVisualisationResults={fetchVisualisationResults}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n\n    expect(container.querySelectorAll(JSON_VALUE_DOT)).toHaveLength(4)\n  })\n\n  it('should render not downloaded JSON with array', async () => {\n    const fetchVisualisationResults = jest.fn().mockReturnValue(\n      Promise.resolve({\n        data: [\n          1,\n          2,\n          {\n            cardinality: 1,\n            key: 'latitude',\n            path: '[3][\"latitude\"]',\n            type: 'number',\n            value: -7.655525,\n          },\n          {\n            cardinality: 1,\n            key: 'latitude1',\n            path: '[3][\"latitude1\"]',\n            type: 'array',\n            value: [],\n          },\n          {\n            cardinality: 1,\n            key: 'latitude2',\n            path: '[3][\"latitude2\"]',\n            type: 'object',\n            value: {},\n          },\n        ],\n      }),\n    )\n    const { container } = render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded={false}\n        onJsonKeyExpandAndCollapse={jest.fn()}\n        handleFetchVisualisationResults={fetchVisualisationResults}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n\n    expect(container.querySelectorAll(JSON_VALUE_DOT)).toHaveLength(3)\n  })\n\n  it('should render inline editor to add', async () => {\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(/expand-object/i))\n    })\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(/add-field-btn/i))\n    })\n\n    expect(screen.getByTestId('apply-btn')).toBeInTheDocument()\n  })\n\n  it('should not be able to add value with wrong json', async () => {\n    const onJSONPropertyAdded = jest.fn()\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(/expand-object/i))\n    })\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(/add-field-btn/i))\n    })\n\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"a\"' },\n    })\n\n    fireEvent.change(screen.getByTestId(JSON_VALUE), {\n      target: { value: '{' },\n    })\n\n    expect(onJSONPropertyAdded).not.toBeCalled()\n    // expect(screen.getByTestId('apply-btn')).toBeDisabled();\n  })\n\n  it('should apply proper value to add element in object', async () => {\n    const onJSONPropertyAdded = jest.fn()\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n        handleSubmitJsonUpdateValue={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EXPAND_OBJECT))\n    })\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId('add-field-btn'))\n    })\n\n    fireEvent.change(screen.getByTestId('json-key'), {\n      target: { value: '\"key\"' },\n    })\n\n    fireEvent.change(screen.getByTestId(JSON_VALUE), {\n      target: { value: '{}' },\n    })\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId('apply-btn'))\n    })\n\n    expect(onJSONPropertyAdded).toBeCalled()\n  })\n\n  it('should render inline editor to edit value', async () => {\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EDIT_OBJECT_BTN))\n    })\n\n    expect(screen.getByTestId(JSON_VALUE)).toBeInTheDocument()\n  })\n\n  it('should change value when editing', async () => {\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EDIT_OBJECT_BTN))\n    })\n\n    fireEvent.change(screen.getByTestId(JSON_VALUE), {\n      target: { value: '{}' },\n    })\n\n    expect(screen.getByTestId(JSON_VALUE)).toHaveValue('{}')\n  })\n\n  it('should not apply wrong value for edit', async () => {\n    const onJSONPropertyEdited = jest.fn()\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EDIT_OBJECT_BTN))\n    })\n\n    fireEvent.change(screen.getByTestId(JSON_VALUE), {\n      target: { value: '{' },\n    })\n\n    expect(onJSONPropertyEdited).not.toBeCalled()\n    // expect(screen.getByTestId('apply-edit-btn')).toBeDisabled()\n  })\n\n  it('should apply proper value for edit', async () => {\n    const onJSONPropertyEdited = jest.fn()\n    render(\n      <JSONObject\n        {...instance(mockedProps)}\n        keyName=\"keyName\"\n        value={mockedSimpleJSONObject}\n        isDownloaded\n        onJsonKeyExpandAndCollapse={jest.fn()}\n      />,\n    )\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId(EDIT_OBJECT_BTN))\n    })\n\n    fireEvent.change(screen.getByTestId(JSON_VALUE), {\n      target: { value: '{}' },\n    })\n\n    await act(async () => {\n      await fireEvent.click(screen.getByTestId('apply-edit-btn'))\n    })\n\n    expect(onJSONPropertyEdited).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/RejsonObject.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\n\nimport { useDispatch } from 'react-redux'\nimport { AxiosError } from 'axios'\nimport { isTruncatedString } from 'uiSrc/utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { AXIOS_ERROR_DISABLED_ACTION_WITH_TRUNCATED_DATA } from 'uiSrc/constants'\nimport { Loader } from 'uiSrc/components/base/display'\nimport RejsonDynamicTypes from '../rejson-dynamic-types'\nimport { JSONObjectProps, ObjectTypes, REJSONResponse } from '../interfaces'\nimport { generatePath, getBrackets, wrapPath } from '../utils'\n\nimport {\n  AddItem,\n  AddItemFieldAction,\n  EditEntireItemAction,\n  EditItemFieldAction,\n} from '../components'\n\nimport styles from '../styles.module.scss'\n\nconst defaultValue: [] = []\n\nconst RejsonObject = (props: JSONObjectProps) => {\n  const {\n    type,\n    parentPath,\n    keyName,\n    isDownloaded,\n    expandedRows,\n    leftPadding,\n    selectedKey,\n    cardinality = 0,\n    handleSubmitRemoveKey,\n    onClickRemoveKey,\n    handleSubmitUpdateValue,\n    onJsonKeyExpandAndCollapse,\n    handleFetchVisualisationResults,\n    handleAppendRejsonObjectItemAction,\n    handleSetRejsonDataAction,\n    path: currentFullPath,\n    value: currentValue,\n  } = props\n\n  const [path] = useState<string>(\n    currentFullPath || generatePath(parentPath, keyName),\n  )\n  const [value, setValue] = useState<any>(defaultValue)\n  const [downloaded, setDownloaded] = useState<boolean>(isDownloaded)\n  const [editEntireObject, setEditEntireObject] = useState<boolean>(false)\n  const [valueOfEntireObject, setValueOfEntireObject] = useState<any>('')\n  const [addNewKeyValuePair, setAddNewKeyValuePair] = useState<boolean>(false)\n  const [loading, setLoading] = useState<boolean>(false)\n  const [isExpanded, setIsExpanded] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!expandedRows?.has(path)) {\n      setValue(defaultValue)\n      return\n    }\n\n    if (isDownloaded) {\n      setValue(currentValue)\n      setIsExpanded(expandedRows?.has(path))\n      return\n    }\n\n    fetchObject()\n  }, [])\n\n  const handleFormSubmit = ({\n    key,\n    value,\n  }: {\n    key?: string\n    value: string\n  }) => {\n    setAddNewKeyValuePair(false)\n\n    if (type === ObjectTypes.Array) {\n      handleAppendRejsonObjectItemAction(selectedKey, path, value)\n      return\n    }\n\n    const updatedPath = wrapPath(key as string, path)\n    if (updatedPath) {\n      handleSetRejsonDataAction(selectedKey, updatedPath, value)\n    }\n  }\n\n  const handleUpdateValueFormSubmit = (value: string) => {\n    setEditEntireObject(false)\n    handleSetRejsonDataAction(selectedKey, path, value as string)\n  }\n\n  const onClickEditEntireObject = () => {\n    handleFetchVisualisationResults(path, true).then((data: REJSONResponse) => {\n      if (isTruncatedString(data?.data)) {\n        return dispatch(\n          addErrorNotification(\n            AXIOS_ERROR_DISABLED_ACTION_WITH_TRUNCATED_DATA as AxiosError,\n          ),\n        )\n      }\n\n      setEditEntireObject(true)\n      setValueOfEntireObject(\n        typeof data.data === 'object'\n          ? JSON.stringify(\n              data.data,\n              (_key, value) =>\n                typeof value === 'bigint' ? value.toString() : value,\n              4,\n            )\n          : data.data,\n      )\n    })\n  }\n\n  const onClickExpandCollapse = (path: string) => {\n    if (isExpanded) {\n      onJsonKeyExpandAndCollapse(false, path)\n      setIsExpanded(false)\n      setValue(defaultValue)\n\n      return\n    }\n\n    if (isDownloaded) {\n      onJsonKeyExpandAndCollapse(true, path)\n      setIsExpanded(true)\n      setValue(currentValue)\n\n      return\n    }\n\n    fetchObject()\n  }\n\n  const fetchObject = async () => {\n    const spinnerDelay = setTimeout(() => setLoading(true), 300)\n\n    try {\n      const { data, downloaded } = await handleFetchVisualisationResults(path)\n      setValue(data)\n      onJsonKeyExpandAndCollapse(true, path)\n      setDownloaded(downloaded)\n      clearTimeout(spinnerDelay)\n      setLoading(false)\n      setIsExpanded(true)\n    } catch {\n      clearTimeout(spinnerDelay)\n      setIsExpanded(false)\n    }\n  }\n\n  return (\n    <>\n      <div className={styles.row} key={keyName + parentPath}>\n        <div className={styles.rowContainer}>\n          <div\n            className={styles.quotedKeyName}\n            style={{ paddingLeft: `${leftPadding}em` }}\n          >\n            <span\n              className={cx(styles.quoted, styles.keyName)}\n              onClick={() => onClickExpandCollapse(path)}\n              role=\"presentation\"\n            >\n              {keyName}\n            </span>\n            <div style={{ paddingLeft: '0.2em', display: 'inline-block' }}>\n              :\n            </div>\n            {!isExpanded && !editEntireObject && (\n              <div\n                className={styles.defaultFontExpandArray}\n                onClick={() => onClickExpandCollapse(path)}\n                data-testid=\"expand-object\"\n                role=\"presentation\"\n              >\n                {getBrackets(type, 'start')}\n                {cardinality ? '...' : ''}\n                {getBrackets(type, 'end')}\n              </div>\n            )}\n            {isExpanded && !editEntireObject && (\n              <span className={styles.defaultFontOpenIndex}>\n                {getBrackets(type, 'start')}\n              </span>\n            )}\n          </div>\n          {!editEntireObject && !loading && (\n            <EditItemFieldAction\n              keyName={keyName.toString()}\n              selectedKey={selectedKey}\n              path={path}\n              handleSubmitRemoveKey={handleSubmitRemoveKey}\n              onClickEditEntireItem={onClickEditEntireObject}\n            />\n          )}\n          {loading && (\n            <div\n              className={styles.actionButtons}\n              style={{ justifyContent: 'flex-end' }}\n            >\n              <div className={styles.spinner}>\n                <Loader size=\"m\" />\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n      {editEntireObject ? (\n        <EditEntireItemAction\n          initialValue={valueOfEntireObject}\n          onCancel={() => setEditEntireObject(false)}\n          onSubmit={handleUpdateValueFormSubmit}\n        />\n      ) : (\n        <RejsonDynamicTypes\n          expandedRows={expandedRows}\n          leftPadding={leftPadding}\n          data={value}\n          parentPath={path}\n          selectedKey={selectedKey}\n          isDownloaded={downloaded}\n          onClickRemoveKey={onClickRemoveKey}\n          onJsonKeyExpandAndCollapse={onJsonKeyExpandAndCollapse}\n          handleSubmitUpdateValue={handleSubmitUpdateValue}\n          handleFetchVisualisationResults={handleFetchVisualisationResults}\n          handleAppendRejsonObjectItemAction={\n            handleAppendRejsonObjectItemAction\n          }\n          handleSetRejsonDataAction={handleSetRejsonDataAction}\n        />\n      )}\n      {addNewKeyValuePair && (\n        <AddItem\n          isPair={type === ObjectTypes.Object}\n          onCancel={() => setAddNewKeyValuePair(false)}\n          onSubmit={handleFormSubmit}\n          leftPadding={leftPadding}\n          parentPath={path}\n        />\n      )}\n      {isExpanded && !editEntireObject && (\n        <AddItemFieldAction\n          leftPadding={leftPadding}\n          type={type}\n          onClickSetKVPair={() => setAddNewKeyValuePair(true)}\n        />\n      )}\n    </>\n  )\n}\n\nexport default React.memo(RejsonObject)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/index.ts",
    "content": "import RejsonObject from './RejsonObject'\n\nexport default RejsonObject\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { JSONScalarProps } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/interfaces'\nimport { MOCK_TRUNCATED_STRING_VALUE } from 'uiSrc/mocks/data/bigString'\nimport RejsonScalar from './RejsonScalar'\n\nconst INLINE_ITEM_EDITOR = 'inline-item-editor'\n\nconst mockedProps = mock<JSONScalarProps>()\n\ndescribe('JSONScalar', () => {\n  it('should render', () => {\n    expect(\n      render(<RejsonScalar {...instance(mockedProps)} keyName=\"keyName\" />),\n    ).toBeTruthy()\n  })\n\n  it('should render string', () => {\n    expect(\n      render(\n        <RejsonScalar\n          {...instance(mockedProps)}\n          value=\"string\"\n          keyName=\"keyName\"\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render null', () => {\n    expect(\n      render(\n        <RejsonScalar\n          {...instance(mockedProps)}\n          value={null}\n          keyName=\"keyName\"\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render number', () => {\n    expect(\n      render(\n        <RejsonScalar\n          {...instance(mockedProps)}\n          value={123123}\n          keyName=\"keyName\"\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render boolean', () => {\n    expect(\n      render(\n        <RejsonScalar {...instance(mockedProps)} value keyName=\"keyName\" />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render inline edit after click', () => {\n    render(\n      <RejsonScalar\n        {...instance(mockedProps)}\n        value=\"string\"\n        keyName=\"keyName\"\n      />,\n    )\n    fireEvent.click(screen.getByTestId(/json-scalar-value/i))\n    expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toBeInTheDocument()\n  })\n\n  it('should change value', () => {\n    render(\n      <RejsonScalar\n        {...instance(mockedProps)}\n        value=\"string\"\n        keyName=\"keyName\"\n      />,\n    )\n    fireEvent.click(screen.getByTestId(/json-scalar-value/i))\n    fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n      target: { value: 'true' },\n    })\n\n    expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('true')\n  })\n\n  it('should be able to apply value with wrong json', () => {\n    const handleEdit = jest.fn()\n    render(\n      <RejsonScalar\n        {...instance(mockedProps)}\n        handleSubmitJsonUpdateValue={jest.fn()}\n        value=\"string\"\n        keyName=\"keyName\"\n      />,\n    )\n    fireEvent.click(screen.getByTestId(/json-scalar-value/i))\n    fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), {\n      target: { value: '{' },\n    })\n\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    expect(handleEdit).not.toBeCalled()\n  })\n\n  it('should render BigInt value when root', () => {\n    render(\n      <RejsonScalar\n        {...instance(mockedProps)}\n        isRoot\n        value={BigInt('1188950299261208742')}\n      />,\n    )\n\n    expect(screen.getByText('1188950299261208742')).toBeInTheDocument()\n  })\n\n  it('should render BigInt value when not root', () => {\n    render(\n      <RejsonScalar\n        {...instance(mockedProps)}\n        isRoot={false}\n        value={BigInt('1188950299261208742')}\n      />,\n    )\n\n    expect(screen.getByTestId('json-scalar-value')).toHaveTextContent(\n      '1188950299261208742',\n    )\n  })\n\n  it('should render regular number without n suffix', () => {\n    render(<RejsonScalar {...instance(mockedProps)} isRoot value={123} />)\n\n    expect(screen.getByText('123')).toBeInTheDocument()\n  })\n\n  it('should render string value with quotes', () => {\n    render(<RejsonScalar {...instance(mockedProps)} isRoot value=\"test\" />)\n\n    expect(screen.getByText('\"test\"')).toBeInTheDocument()\n  })\n\n  describe('truncated data', () => {\n    it('should not render inline edit after click', () => {\n      render(\n        <RejsonScalar\n          {...instance(mockedProps)}\n          value={MOCK_TRUNCATED_STRING_VALUE}\n          keyName=\"keyName\"\n        />,\n      )\n      fireEvent.click(screen.getByTestId(/json-scalar-value/i))\n      expect(screen.queryByTestId(INLINE_ITEM_EDITOR)).not.toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport cx from 'classnames'\n\nimport { setReJSONDataAction } from 'uiSrc/slices/browser/rejson'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport {\n  bufferToString,\n  createDeleteFieldHeader,\n  createDeleteFieldMessage,\n  isTruncatedString,\n  Nullable,\n} from 'uiSrc/utils'\nimport FieldMessage from 'uiSrc/components/field-message/FieldMessage'\n\nimport { JSONScalarProps } from '../interfaces'\nimport {\n  generatePath,\n  getClassNameByValue,\n  isValidJSON,\n  stringifyScalarValue,\n} from '../utils'\nimport { JSONErrors } from '../constants'\n\nimport styles from '../styles.module.scss'\nimport '../styles.scss'\n\nconst RejsonScalar = (props: JSONScalarProps) => {\n  const {\n    keyName = '',\n    value,\n    isRoot,\n    parentPath,\n    leftPadding,\n    selectedKey,\n    path: currentFullPath,\n    handleSubmitRemoveKey,\n  } = props\n  const [changedValue, setChangedValue] = useState<any>('')\n  const [path] = useState<string>(\n    currentFullPath || generatePath(parentPath, keyName),\n  )\n  const [error, setError] = useState<Nullable<string>>(null)\n  const [editing, setEditing] = useState<boolean>(false)\n  const [deleting, setDeleting] = useState<string>('')\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setChangedValue(stringifyScalarValue(value))\n  }, [value])\n\n  const onDeclineChanges = () => {\n    setEditing(false)\n    setError(null)\n  }\n\n  const onApplyValue = (value: string) => {\n    if (!isValidJSON(value)) {\n      setError(JSONErrors.valueJSONFormat)\n      return\n    }\n\n    dispatch<any>(\n      setReJSONDataAction(\n        selectedKey,\n        path,\n        String(value),\n        true,\n        undefined,\n        () => setEditing(false),\n      ),\n    )\n  }\n\n  return (\n    <>\n      {isRoot ? (\n        <p className={getClassNameByValue(value)}>{`${changedValue}`}</p>\n      ) : (\n        <div className={styles.row}>\n          <div className={styles.rowContainer}>\n            <div\n              style={{ display: 'flex', alignItems: 'flex-start', flexGrow: 1 }}\n            >\n              <span\n                className={cx(styles.quoted, styles.keyName)}\n                style={{ paddingLeft: `${leftPadding}em` }}\n              >\n                {keyName}\n              </span>\n              <div style={{ paddingLeft: '0.2em', display: 'inline-block' }}>\n                :\n              </div>\n              {editing ? (\n                <div className=\"jsonItemEditor\">\n                  <InlineItemEditor\n                    styles={{\n                      inputContainer: {\n                        height: `24px`,\n                      },\n                      input: {\n                        height: `24px !important`,\n                      },\n                      actionsContainer: {\n                        height: `24px`,\n                      },\n                    }}\n                    initialValue={changedValue}\n                    controlsPosition=\"right\"\n                    placeholder=\"Enter JSON value\"\n                    fieldName=\"stringValue\"\n                    expandable\n                    isInvalid={!!error}\n                    onDecline={onDeclineChanges}\n                    onChange={() => setError('')}\n                    onApply={(value) => onApplyValue(value)}\n                    iconSize=\"M\"\n                  />\n                  {!!error && (\n                    <div className={cx(styles.errorMessage)}>\n                      <FieldMessage\n                        scrollViewOnAppear\n                        icon=\"ToastDangerIcon\"\n                        testID=\"edit-json-error\"\n                      >\n                        {error}\n                      </FieldMessage>\n                    </div>\n                  )}\n                </div>\n              ) : (\n                <span\n                  className={cx(styles.jsonValue, getClassNameByValue(value))}\n                  onClick={() => setEditing(!isTruncatedString(changedValue))}\n                  style={{ flexGrow: 1 }}\n                  data-testid=\"json-scalar-value\"\n                  role=\"presentation\"\n                >\n                  {String(changedValue)}\n                </span>\n              )}\n            </div>\n            <div className={styles.deleteBtn}>\n              <PopoverDelete\n                header={createDeleteFieldHeader(keyName.toString())}\n                text={createDeleteFieldMessage(bufferToString(selectedKey))}\n                item={keyName.toString()}\n                suffix=\"scalar\"\n                deleting={deleting}\n                closePopover={() => setDeleting('')}\n                updateLoading={false}\n                showPopover={(item) => setDeleting(`${item}scalar`)}\n                handleDeleteItem={() =>\n                  handleSubmitRemoveKey(path, keyName.toString())\n                }\n              />\n            </div>\n          </div>\n        </div>\n      )}\n    </>\n  )\n}\n\nexport default RejsonScalar\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/index.ts",
    "content": "import RejsonScalar from './RejsonScalar'\n\nexport default RejsonScalar\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n  width: 100%;\n  padding: 16px;\n  overflow-y: auto;\n  overflow-x: auto;\n  background-color: var(--euiColorEmptyShade);\n  color: var(--euiTextSubduedColor);\n  position: relative;\n\n  input {\n    font-family: 'Graphik', sans-serif !important;\n  }\n\n  :global {\n    .euiFieldText {\n      font-size: 13px !important;\n      max-width: initial !important;\n      height: 26px !important;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n\n      &.withoutBorder {\n        &:not(:focus) {\n          background-color: inherit;\n          box-shadow: none !important;\n        }\n      }\n    }\n\n    .euiFormControlLayout {\n      height: 24px !important;\n    }\n  }\n}\n\n.fullWidthContainer {\n  width: 100%;\n  padding: 10px 0;\n}\n\n.placeholder {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  height: 100%;\n  padding: 12px;\n  width: 100%;\n}\n\n.jsonData {\n  font-size: 14px;\n  line-height: 25px;\n  font-family: 'Inconsolata', monospace !important;\n  letter-spacing: 0.15px;\n  flex-grow: 1;\n\n  input {\n    width: 100%;\n    min-width: 140px;\n  }\n}\n\n.monacoEditorJsonData {\n  font-size: 14px;\n  line-height: 25px;\n  font-family: 'Inconsolata', monospace !important;\n  letter-spacing: 0.15px;\n  flex-grow: 1;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.defaultFont {\n  font-family: 'Graphik', sans-serif !important;\n}\n\n.errorMessage {\n  background-color: var(--tableDarkestBorderColor);\n  padding: 4px 8px 0 8px;\n  width: 100%;\n}\n\n.controls,\n.controlsBottom {\n  background-color: var(--euiColorLightestShade);\n  height: 24px !important;\n  margin-bottom: 4px !important;\n  z-index: 2;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 12px;\n  box-shadow: 0 3px 3px var(--controlsBoxShadowColor);\n\n  &.controls {\n    width: 80px;\n    border-radius: 0 10px 10px 0;\n\n    :global(.euiButtonIcon) {\n      width: 50% !important;\n      height: 100% !important;\n    }\n  }\n\n  &.controlsBottom {\n    height: 34px;\n    top: 100%;\n    right: 0;\n    left: auto;\n    border-radius: 0 0 10px 10px;\n  }\n}\n\n.row {\n  position: relative;\n  display: flex;\n  align-items: center;\n\n  &:before {\n    content: '';\n    display: block;\n    position: absolute;\n    height: 100%;\n    top: 0;\n    left: 0;\n    right: 0;\n    margin: 0 -16px;\n    z-index: 0;\n  }\n\n  &:nth-child(2n):before {\n    background: var(--browserTableRowEven);\n  }\n\n  &:hover:before {\n    background: var(--hoverInListColor);\n  }\n\n  > div,\n  span,\n  button {\n    z-index: 1;\n  }\n}\n\n.topRow {\n  justify-content: space-between;\n}\n\n.rowContainer {\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  width: 100%;\n}\n\n.keyName {\n  color: var(--jsonKeyNameColor) !important;\n  width: max-content;\n  word-break: break-all;\n  max-width: 300px;\n  box-sizing: content-box;\n  flex-shrink: 0;\n}\n\n.quotedKeyName {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  padding-left: 1em;\n}\n\n.defaultFontExpandArray {\n  display: inline-block;\n  cursor: pointer;\n  padding-left: 8px;\n}\n\n.defaultFontOpenIndex {\n  display: inline-block;\n  cursor: pointer;\n  padding-left: 8px;\n}\n\n.jsonValue {\n  font-size: 14px;\n  line-height: 25px;\n  font-family: 'Inconsolata', monospace;\n  letter-spacing: 0.15px;\n  padding: 0 8px;\n  max-width: 1000px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  &:hover {\n    outline: 1px solid #b5b6c0;\n  }\n}\n\n.keyNameArray {\n  color: var(--jsonKeyNameArrayColor) !important;\n}\n\n.jsonString {\n  color: var(--jsonStringColor) !important;\n}\n\n.jsonNumber {\n  color: var(--jsonNumberColor) !important;\n}\n\n.jsonBoolean {\n  color: var(--jsonBooleanColor) !important;\n}\n\n.jsonNull {\n  color: var(--jsonNullColor) !important;\n}\n\n.jsonNonStringPrimitive {\n  color: var(--jsonNonStringPrimitiveColor) !important;\n}\n\n.newValue {\n  color: var(--euiColorDanger) !important;\n}\n\n.stringStyle {\n  word-break: break-all;\n}\n\n.quoted {\n  &:before,\n  &:after {\n    content: '\"';\n  }\n}\n\n.actionButtons,\n.deleteBtn {\n  margin-left: 1em;\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  min-width: 24px;\n}\n\n.declineBtn:hover {\n  color: var(--euiColorColorDanger) !important;\n}\n\n.applyBtn:hover {\n  color: var(--euiColorPrimary) !important;\n}\n\n.spinner {\n  width: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.fullWidthTextArea {\n  height: 150px;\n  width: 100%;\n  max-width: none;\n}\n\n.editor,\n.editorWrapper {\n  // Using 100% height starts a weird Monaco animation,\n  // which causes the editor to overflow its container.\n  // Probably it's related to the borders of the inside elements.\n  // TODO: Find a cleaner fix for this.\n  height: calc(100% - 2px) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/styles.scss",
    "content": ".jsonItemEditor {\n  padding-right: 80px;\n  max-width: 600px;\n  .inlineItemEditor__controls {\n    display: flex;\n    align-items: center;\n\n    width: 80px;\n    height: 24px;\n  }\n}\n\n.euiPopover__anchor.deleteFieldPopover {\n  display: flex;\n  align-items: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/index.ts",
    "content": "export * from './utils'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.spec.ts",
    "content": "import {\n  generatePath,\n  getBrackets,\n  isRealArray,\n  isRealObject,\n  isScalar,\n  isValidKey,\n  parseJsonData,\n  parseValue,\n  stringifyScalarValue,\n  wrapPath,\n} from './utils'\nimport { ObjectTypes } from '../interfaces'\n\ndescribe('JSONUtils', () => {\n  describe('generatePath', () => {\n    it('should generate path for Parent and Children', () => {\n      const expectPath = \"['parent']['children']\"\n      const parentPath = \"['parent']\"\n      const children = 'children'\n      const result = generatePath(parentPath, children)\n      expect(expectPath).toEqual(result)\n    })\n  })\n\n  describe('wrapPath', () => {\n    it('should properly wrap path', () => {\n      expect(wrapPath('\"key\"')).toEqual('[\"key\"]')\n      expect(wrapPath('\"ke\\\\\"y\"')).toEqual(\"['ke\\\"y']\")\n      expect(wrapPath('\"key\"', 'path')).toEqual('path[\"key\"]')\n      expect(wrapPath('\"key\\\\\"\"', 'path')).toEqual(\"path['key\\\"']\")\n    })\n  })\n\n  describe('isScalar', () => {\n    it('should return Truthy for scalar variables', () => {\n      const string = 'string'\n      const number = 123\n      const boolean = false\n\n      expect(isScalar(string)).toBeTruthy()\n      expect(isScalar(number)).toBeTruthy()\n      expect(isScalar(boolean)).toBeTruthy()\n    })\n\n    it('should return Falsy for object and array variables', () => {\n      expect(isScalar([1, 2, 3] as any)).toBeFalsy()\n      expect(isScalar({ foo: '' } as any)).toBeFalsy()\n    })\n  })\n\n  describe('isRealObject', () => {\n    it('should properly check for an object', () => {\n      expect(isRealObject({})).toBeTruthy()\n      expect(isRealObject([], 'object')).toBeTruthy()\n      expect(isRealObject([])).toBeFalsy()\n      expect(isRealObject(null)).toBeFalsy()\n      expect(isRealObject(undefined)).toBeFalsy()\n    })\n  })\n\n  describe('isRealArray', () => {\n    it('should properly check for an array', () => {\n      expect(isRealArray([])).toBeTruthy()\n      expect(isRealArray([], 'array')).toBeTruthy()\n      expect(isRealArray({}, 'array')).toBeTruthy()\n      expect(isRealArray([], 'object')).toBeFalsy()\n      expect(isRealArray({})).toBeFalsy()\n      expect(isRealArray('')).toBeFalsy()\n      expect(isRealArray(null)).toBeFalsy()\n      expect(isRealArray(undefined)).toBeFalsy()\n    })\n  })\n\n  describe('getBrackets', () => {\n    it('should properly return bracket', () => {\n      expect(getBrackets(ObjectTypes.Object, 'start')).toEqual('{')\n      expect(getBrackets(ObjectTypes.Object, 'end')).toEqual('}')\n      expect(getBrackets(ObjectTypes.Array, 'start')).toEqual('[')\n      expect(getBrackets(ObjectTypes.Array, 'end')).toEqual(']')\n    })\n  })\n\n  describe('isValidKey', () => {\n    it('should properly validate key', () => {\n      expect(isValidKey('\"a\"')).toBeTruthy()\n      expect(isValidKey('\"key_name\"')).toBeTruthy()\n      expect(isValidKey('\"a\\'\"')).toBeTruthy()\n      expect(isValidKey('\"a')).toBeFalsy()\n      expect(isValidKey('\"')).toBeFalsy()\n    })\n  })\n\n  describe('JSON Parsing Utils', () => {\n    const bigintAsString = '1188950299261208742'\n    const scientificNotation = 1.2345678901234568e29\n\n    describe('parseValue', () => {\n      it('should handle non-string values', () => {\n        expect(parseValue(123)).toBe(123)\n        expect(parseValue(null)).toBe(null)\n        expect(parseValue(undefined)).toBe(undefined)\n      })\n\n      it('should parse typed integer values', () => {\n        const result = parseValue(bigintAsString, 'integer')\n        expect(typeof result).toBe('bigint')\n        expect(result.toString()).toBe(bigintAsString)\n      })\n\n      it('should parse regular numbers as numbers, not bigints', () => {\n        const result = parseValue('42', 'integer')\n        expect(typeof result).toBe('number')\n        expect(result).toBe(42)\n      })\n\n      it('should preserve string values in JSON objects', () => {\n        const input = '{\"a\":\"111\"}'\n        const result = parseValue(input)\n        expect(result.a).toBe('111')\n        expect(typeof result.a).toBe('string')\n      })\n\n      it('should handle mixed string and number values in JSON objects', () => {\n        const input = '{\"stringVal\":\"111\",\"numberVal\":111}'\n        const result = parseValue(input)\n        expect(result.stringVal).toBe('111')\n        expect(typeof result.stringVal).toBe('string')\n        expect(result.numberVal).toBe(111)\n        expect(typeof result.numberVal).toBe('number')\n      })\n\n      it('should handle string type with quotes', () => {\n        expect(parseValue('\"test\"', 'string')).toBe('test')\n        expect(parseValue('test', 'string')).toBe('test')\n      })\n\n      it('should parse boolean values', () => {\n        expect(parseValue('true', 'boolean')).toBe(true)\n        expect(parseValue('false', 'boolean')).toBe(false)\n      })\n\n      it('should parse null values', () => {\n        expect(parseValue('null', 'null')).toBe(null)\n      })\n\n      it('should parse JSON objects without type', () => {\n        const input = `{\"value\": ${bigintAsString}, \"text\": \"test\"}`\n        const result = parseValue(input)\n        expect(typeof result.value).toBe('bigint')\n        expect(result.value.toString()).toBe(bigintAsString)\n        expect(result.text).toBe('test')\n      })\n\n      it('should parse JSON arrays without type', () => {\n        const input = `[${bigintAsString}, \"test\"]`\n        const result = parseValue(input)\n        expect(typeof result[0]).toBe('bigint')\n        expect(result[0].toString()).toBe(bigintAsString)\n        expect(result[1]).toBe('test')\n      })\n\n      it('should handle extremely large integers and maintain scientific notation', () => {\n        const resultFromString = parseValue(\n          `'${scientificNotation}'`,\n          'integer',\n        )\n        expect(resultFromString).toBe(`'${scientificNotation}'`)\n\n        const resultFromInt = parseValue(scientificNotation, 'integer')\n        expect(resultFromInt).toBe(scientificNotation)\n\n        // Also test parsing as part of JSON\n        const jsonWithLargeInt = `{\"value\": ${scientificNotation}}`\n        const parsedJson = parseValue(jsonWithLargeInt)\n        expect(parsedJson.value).toBe(scientificNotation)\n      })\n    })\n\n    describe('parseJsonData', () => {\n      it('should handle null or undefined data', () => {\n        expect(parseJsonData(null)).toBe(null)\n        expect(parseJsonData(undefined)).toBe(undefined)\n      })\n\n      it('should parse array of typed values', () => {\n        const input = [\n          { type: 'string', value: '\"John\"' },\n          { type: 'integer', value: bigintAsString },\n        ]\n        const result = parseJsonData(input)\n\n        expect(result[0].value).toBe('John')\n        expect(typeof result[1].value).toBe('bigint')\n        expect(result[1].value.toString()).toBe(bigintAsString)\n      })\n\n      it('should preserve non-typed array items', () => {\n        const input = [{ value: '\"John\"' }, { someOtherProp: 'test' }]\n        const result = parseJsonData(input)\n\n        expect(result[0].value).toBe('\"John\"')\n        expect(result[1].someOtherProp).toBe('test')\n      })\n    })\n  })\n\n  describe('stringifyScalarValue', () => {\n    it('should handle bigint values', () => {\n      const bigIntValue = BigInt('9007199254740991')\n      expect(stringifyScalarValue(bigIntValue)).toBe('9007199254740991')\n    })\n\n    it('should wrap string values in quotes', () => {\n      expect(stringifyScalarValue('hello')).toBe('\"hello\"')\n      expect(stringifyScalarValue('')).toBe('\"\"')\n    })\n\n    it('should convert null to \"null\" string', () => {\n      expect(stringifyScalarValue(null as any)).toBe('null')\n    })\n\n    it('should convert numbers to string representation', () => {\n      expect(stringifyScalarValue(42)).toBe('42')\n      expect(stringifyScalarValue(-123.456)).toBe('-123.456')\n      expect(stringifyScalarValue(0)).toBe('0')\n    })\n\n    it('should convert boolean values to string representation', () => {\n      expect(stringifyScalarValue(true)).toBe('true')\n      expect(stringifyScalarValue(false)).toBe('false')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.ts",
    "content": "import { isArray, isNull, isString } from 'lodash'\nimport JSONBigInt from 'json-bigint'\nimport { JSONScalarValue, ObjectTypes } from '../interfaces'\nimport styles from '../styles.module.scss'\n\nenum ClassNames {\n  string = 'jsonString',\n  number = 'jsonNumber',\n  object = 'jsonNull',\n  boolean = 'jsonBoolean',\n  others = 'jsonNonStringPrimitive',\n}\n\nexport function isScalar(x: JSONScalarValue) {\n  return (\n    ['string', 'number', 'boolean', 'bigint'].indexOf(typeof x) !== -1 ||\n    x === null\n  )\n}\n\nexport const isValidJSON = (value: string): boolean => {\n  try {\n    JSON.parse(value)\n    return true\n  } catch (e) {\n    return false\n  }\n}\n\nexport const generatePath = (parentPath: string, keyName: string | number) => {\n  const currentPath =\n    typeof keyName === 'number' ? `${keyName}` : `'${keyName}'`\n  return parentPath ? `${parentPath}[${currentPath}]` : `[${currentPath}]`\n}\n\nexport const wrapPath = (key: string, path: string = '') => {\n  try {\n    const unescapedKey = JSON.parse(key!)\n    return unescapedKey.includes('\"')\n      ? `${path}['${unescapedKey}']`\n      : `${path}[\"${unescapedKey}\"]`\n  } catch {\n    return null\n  }\n}\n\nexport const getClassNameByValue = (value: any) => {\n  const type = typeof value\n  // @ts-ignore\n  const className = type in ClassNames ? ClassNames[type] : ClassNames.others\n  return styles[className]\n}\n\nexport const isRealObject = (data: any, knownType?: string) => {\n  if (knownType === ObjectTypes.Object) return true\n  return (\n    typeof data === ObjectTypes.Object &&\n    data !== null &&\n    !(data instanceof Array)\n  )\n}\n\nexport const isRealArray = (data: any, knownType?: string) => {\n  if (knownType === ObjectTypes.Array) return true\n  if (knownType) return false\n  return isArray(data)\n}\n\nexport const getBrackets = (\n  type: string,\n  position: 'start' | 'end' = 'start',\n) => {\n  if (type === ObjectTypes.Array) return position === 'start' ? '[' : ']'\n  return position === 'start' ? '{' : '}'\n}\n\nexport const isValidKey = (key: string): boolean =>\n  /^\"([^\"\\\\]|\\\\.)*\"$/.test(key)\n\nconst JSONParser = JSONBigInt({\n  useNativeBigInt: true,\n  strict: false,\n  alwaysParseAsBig: false,\n  protoAction: 'preserve',\n  constructorAction: 'preserve',\n})\n\nconst safeJSONParse = (value: string) => {\n  // Pre-process the string to handle scientific notation\n  const preprocessed = value.replace(\n    /-?\\d+\\.?\\d*e[+-]?\\d+/gi,\n    (match) =>\n      // Wrap scientific notation numbers in quotes to prevent BigInt conversion\n      `\"${match}\"`,\n  )\n\n  return JSONParser.parse(preprocessed, (_key: string, value: any) => {\n    // Convert quoted scientific notation back to numbers\n    if (typeof value === 'string' && /^-?\\d+\\.?\\d*e[+-]?\\d+$/i.test(value)) {\n      return Number(value)\n    }\n    return value\n  })\n}\n\nexport const parseValue = (value: any, type?: string): any => {\n  try {\n    if (typeof value !== 'string' || !value) {\n      return value\n    }\n\n    if (type) {\n      switch (type) {\n        case 'integer': {\n          const num = BigInt(value)\n          return num > Number.MAX_SAFE_INTEGER ? num : Number(value)\n        }\n        case 'number':\n          return Number(value)\n        case 'boolean':\n          return value === 'true'\n        case 'null':\n          return null\n        case 'string':\n          if (value.startsWith('\"') && value.endsWith('\"')) {\n            return value.slice(1, -1)\n          }\n          return value\n        default:\n          return value\n      }\n    }\n\n    const parsed = safeJSONParse(value)\n\n    if (typeof parsed === 'object' && parsed !== null) {\n      if (Array.isArray(parsed)) {\n        return parsed.map((val) => parseValue(val))\n      }\n      const result: { [key: string]: any } = {}\n      Object.entries(parsed).forEach(([key, val]) => {\n        // This prevents double-parsing of JSON string values.\n        if (typeof val === 'string') {\n          result[key] = val\n        } else {\n          result[key] = parseValue(val)\n        }\n      })\n      return result\n    }\n    return parsed\n  } catch (e) {\n    try {\n      return JSON.parse(value)\n    } catch (error) {\n      return value\n    }\n  }\n}\n\nexport const parseJsonData = (data: any) => {\n  if (!data) {\n    return data\n  }\n  try {\n    if (data && Array.isArray(data)) {\n      return data.map((item: { type?: string; value?: any }) => ({\n        ...item,\n        value:\n          item.type && item.value\n            ? parseValue(item.value, item.type)\n            : item.value,\n      }))\n    }\n\n    return parseValue(data)\n  } catch (e) {\n    return data\n  }\n}\n\nexport const stringifyScalarValue = (\n  value: string | number | boolean | bigint,\n): string => {\n  if (typeof value === 'bigint') {\n    return value.toString()\n  }\n  if (isString(value)) {\n    return `\"${value}\"`\n  }\n  if (isNull(value)) {\n    return 'null'\n  }\n  return String(value)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, SetDetails } from './SetDetails'\n\nconst mockedProps = mock<Props>()\n\ndescribe('SetDetails', () => {\n  it('should render', () => {\n    expect(render(<SetDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/SetDetails.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { selectedKeySelector } from 'uiSrc/slices/browser/keys'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { SetDetailsTable } from './set-details-table'\nimport { AddSetMembers } from './add-set-members'\nimport { AddItemsAction } from '../key-details-actions'\nimport { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'\nimport { AddKeysContainer } from 'uiSrc/pages/browser/modules/key-details/components/common/AddKeysContainer.styled'\n\nexport interface Props extends KeyDetailsHeaderProps {\n  onRemoveKey: () => void\n  onOpenAddItemPanel: () => void\n  onCloseAddItemPanel: () => void\n}\n\nconst SetDetails = (props: Props) => {\n  const keyType = KeyTypes.Set\n  const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props\n\n  const { loading } = useSelector(selectedKeySelector)\n\n  const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState<boolean>(false)\n\n  const openAddItemPanel = () => {\n    setIsAddItemPanelOpen(true)\n    onOpenAddItemPanel()\n  }\n\n  const closeAddItemPanel = (isCancelled?: boolean) => {\n    setIsAddItemPanelOpen(false)\n    if (isCancelled) {\n      onCloseAddItemPanel()\n    }\n  }\n\n  const Actions = ({ width }: { width: number }) => (\n    <AddItemsAction\n      title=\"Add Members\"\n      width={width}\n      openAddItemPanel={openAddItemPanel}\n    />\n  )\n\n  return (\n    <div className=\"fluid flex-column relative\">\n      <KeyDetailsHeader {...props} key=\"key-details-header\" />\n      <KeyDetailsSubheader keyType={keyType} Actions={Actions} />\n      <div className=\"key-details-body\" key=\"key-details-body\">\n        {!loading && (\n          <div className=\"flex-column\" style={{ flex: '1', height: '100%' }}>\n            <SetDetailsTable onRemoveKey={onRemoveKey} />\n          </div>\n        )}\n        {isAddItemPanelOpen && (\n          <AddKeysContainer>\n            <AddSetMembers closePanel={closeAddItemPanel} />\n          </AddKeysContainer>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { SetDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { AddSetMembers, Props } from './AddSetMembers'\n\nconst MEMBER_NAME = 'member-name'\nconst ADD_NEW_ITEM = 'add-item'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddZsetMembers', () => {\n  it('should render', () => {\n    expect(render(<AddSetMembers {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set member value properly', () => {\n    render(<AddSetMembers {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId(MEMBER_NAME)\n    fireEvent.change(memberInput, { target: { value: 'member name' } })\n    expect(memberInput).toHaveValue('member name')\n  })\n\n  it('should render add button', () => {\n    render(<AddSetMembers {...instance(mockedProps)} />)\n    expect(screen.getByTestId(ADD_NEW_ITEM)).toBeTruthy()\n  })\n\n  it('should render one more member input after click add item', () => {\n    render(<AddSetMembers {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId(ADD_NEW_ITEM))\n\n    expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(2)\n  })\n\n  it('should remove one member input after add item & remove one', () => {\n    render(<AddSetMembers {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId(ADD_NEW_ITEM))\n\n    expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(2)\n\n    const removeButtons = screen.getAllByTestId('remove-item')\n    fireEvent.click(removeButtons[1])\n\n    expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(1)\n  })\n\n  it('should clear member after click clear button', () => {\n    render(<AddSetMembers {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId(MEMBER_NAME)\n    fireEvent.change(memberInput, { target: { value: 'member' } })\n    fireEvent.click(screen.getByTestId('remove-item'))\n\n    expect(memberInput).toHaveValue('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/AddSetMembers.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { ColorText } from 'uiSrc/components/base/text'\n\nimport {\n  selectedKeyDataSelector,\n  keysSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { addSetMembersAction, setSelector } from 'uiSrc/slices/browser/set'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { KeyTypes } from 'uiSrc/constants'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\n\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { AddZsetFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport {\n  INITIAL_SET_MEMBER_STATE,\n  ISetMemberState,\n} from 'uiSrc/pages/browser/components/add-key/AddKeySet/interfaces'\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\n\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\n\nimport { EntryContent } from '../../common/AddKeysContainer.styled'\n\nexport interface Props {\n  closePanel: (isCancelled?: boolean) => void\n}\n\nconst AddSetMembers = (props: Props) => {\n  const { closePanel } = props\n  const dispatch = useDispatch()\n  const [members, setMembers] = useState<ISetMemberState[]>([\n    { ...INITIAL_SET_MEMBER_STATE },\n  ])\n  const { loading } = useSelector(setSelector)\n  const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: undefined,\n  }\n  const { viewType } = useSelector(keysSelector)\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const lastAddedMemberName = useRef<HTMLInputElement>(null)\n\n  useEffect(() => {\n    lastAddedMemberName.current?.focus()\n  }, [members.length])\n\n  const onSuccessAdded = () => {\n    closePanel()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_ADDED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_ADDED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Set,\n        numberOfAdded: members.length,\n      },\n    })\n  }\n\n  const addMember = () => {\n    const lastField = members[members.length - 1]\n    const newState = [\n      ...members,\n      {\n        ...INITIAL_SET_MEMBER_STATE,\n        id: lastField.id + 1,\n      },\n    ]\n    setMembers(newState)\n  }\n\n  const removeMember = (id: number) => {\n    const newState = members.filter((item) => item.id !== id)\n    setMembers(newState)\n  }\n\n  const clearMemberValues = (id: number) => {\n    const newState = members.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            name: '',\n          }\n        : item,\n    )\n    setMembers(newState)\n  }\n\n  const onClickRemove = ({ id }: ISetMemberState) => {\n    if (members.length === 1) {\n      clearMemberValues(id)\n      return\n    }\n\n    removeMember(id)\n  }\n\n  const handleMemberChange = (formField: string, id: number, value: string) => {\n    const newState = members.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: value,\n        }\n      }\n      return item\n    })\n    setMembers(newState)\n  }\n\n  const submitData = (): void => {\n    const data = {\n      keyName: selectedKey,\n      members: members.map((item) => stringToBuffer(item.name)),\n    }\n\n    dispatch(addSetMembersAction(data, onSuccessAdded))\n  }\n\n  const isClearDisabled = (item: ISetMemberState): boolean =>\n    members.length === 1 && !item.name.length\n\n  return (\n    <Col gap=\"m\">\n      <EntryContent>\n        <AddMultipleFields\n          items={members}\n          isClearDisabled={isClearDisabled}\n          onClickRemove={onClickRemove}\n          onClickAdd={addMember}\n        >\n          {(item, index) => (\n            <Row align=\"center\">\n              <FlexItem grow>\n                <FormField>\n                  <TextInput\n                    name={`member-${item.id}`}\n                    id={`member-${item.id}`}\n                    placeholder={config.member.placeholder}\n                    value={item.name}\n                    onChange={(value) =>\n                      handleMemberChange('name', item.id, value)\n                    }\n                    ref={\n                      index === members.length - 1 ? lastAddedMemberName : null\n                    }\n                    disabled={loading}\n                    data-testid=\"member-name\"\n                  />\n                </FormField>\n              </FlexItem>\n            </Row>\n          )}\n        </AddMultipleFields>\n      </EntryContent>\n      <Row justify=\"end\" gap=\"xl\">\n        <FlexItem>\n          <SecondaryButton\n            onClick={() => closePanel(true)}\n            data-testid=\"cancel-members-btn\"\n          >\n            <ColorText color=\"default\">Cancel</ColorText>\n          </SecondaryButton>\n        </FlexItem>\n        <FlexItem>\n          <PrimaryButton\n            disabled={loading}\n            loading={loading}\n            onClick={submitData}\n            data-testid=\"save-members-btn\"\n          >\n            Save\n          </PrimaryButton>\n        </FlexItem>\n      </Row>\n    </Col>\n  )\n}\n\nexport { AddSetMembers }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/add-set-members/index.ts",
    "content": "export { AddSetMembers } from './AddSetMembers'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/index.ts",
    "content": "export { SetDetails } from './SetDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { setDataSelector } from 'uiSrc/slices/browser/set'\nimport { anyToBuffer } from 'uiSrc/utils'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  DECOMPRESSED_VALUE_STR_1,\n} from 'uiSrc/utils/tests/decompressors'\nimport { SetDetailsTable, Props } from './SetDetailsTable'\n\nconst members = [\n  { type: 'Buffer', data: [49] },\n  { type: 'Buffer', data: [50] },\n  { type: 'Buffer', data: [51] },\n]\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/browser/set', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/browser/set',\n  ).initialState\n  return {\n    setSelector: jest.fn().mockReturnValue(defaultState),\n    setDataSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      total: 3,\n      key: { type: 'Buffer', data: [49] },\n      keyName: { type: 'Buffer', data: [49] },\n      members,\n    }),\n    fetchSetMembers: () => jest.fn(),\n  }\n})\n\ndescribe('SetDetailsTable', () => {\n  it('should render', () => {\n    expect(render(<SetDetailsTable {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render rows properly', () => {\n    const { container } = render(<SetDetailsTable {...instance(mockedProps)} />)\n    const rows = container.querySelectorAll(\n      '.ReactVirtualized__Table__row[role=\"row\"]',\n    )\n    expect(rows).toHaveLength(members.length)\n  })\n\n  it('should render search input', () => {\n    render(<SetDetailsTable {...instance(mockedProps)} />)\n    expect(screen.getByTestId('search')).toBeTruthy()\n  })\n\n  it('should call search', () => {\n    render(<SetDetailsTable {...instance(mockedProps)} />)\n    const searchInput = screen.getByTestId('search')\n    fireEvent.change(searchInput, { target: { value: '*1*' } })\n    expect(searchInput).toHaveValue('*1*')\n  })\n\n  it('should render delete popup after click remove button', () => {\n    render(<SetDetailsTable {...instance(mockedProps)} />)\n    fireEvent.click(screen.getAllByTestId(/set-remove-btn/)[0])\n    expect(screen.getByTestId(/set-remove-btn-1-icon/)).toBeInTheDocument()\n  })\n\n  describe('decompressed  data', () => {\n    it('should render decompressed GZIP data = \"1\"', () => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/set',\n      ).initialState\n      const setDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        key: '123zxczxczxc',\n        members: [anyToBuffer(GZIP_COMPRESSED_VALUE_1)],\n      })\n      setDataSelector.mockImplementation(setDataSelectorMock)\n\n      const { queryByTestId } = render(\n        <SetDetailsTable {...instance(mockedProps)} />,\n      )\n      const memberEl = queryByTestId(/set-member-value-/)\n\n      expect(memberEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/SetDetailsTable.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { CellMeasurerCache } from 'react-virtualized'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\n\nimport {\n  bufferToString,\n  createDeleteFieldHeader,\n  createDeleteFieldMessage,\n  createTooltipContent,\n  formattingBuffer,\n} from 'uiSrc/utils'\nimport {\n  KeyTypes,\n  OVER_RENDER_BUFFER_COUNT,\n  TEXT_FAILED_CONVENT_FORMATTER,\n} from 'uiSrc/constants'\nimport {\n  sendEventTelemetry,\n  TelemetryEvent,\n  getBasedOnViewTypeEvent,\n  getMatchType,\n} from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  selectedKeyDataSelector,\n  keysSelector,\n  selectedKeySelector,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  deleteSetMembers,\n  fetchSetMembers,\n  fetchMoreSetMembers,\n  setDataSelector,\n  setSelector,\n} from 'uiSrc/slices/browser/set'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport HelpTexts from 'uiSrc/constants/help-texts'\nimport { NoResultsFoundText } from 'uiSrc/constants/texts'\nimport VirtualTable from 'uiSrc/components/virtual-table'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport { getColumnWidth } from 'uiSrc/components/virtual-grid'\nimport {\n  IColumnSearchState,\n  ITableColumn,\n} from 'uiSrc/components/virtual-table/interfaces'\nimport { decompressingBuffer } from 'uiSrc/utils/decompressors'\nimport { FormattedValue } from 'uiSrc/pages/browser/modules/key-details/shared'\nimport { GetSetMembersResponse } from 'apiSrc/modules/browser/set/dto'\nimport styles from './styles.module.scss'\n\nconst suffix = '_set'\nconst headerHeight = 60\nconst rowHeight = 43\nconst footerHeight = 0\nconst matchAllValue = '*'\n\nconst cellCache = new CellMeasurerCache({\n  fixedWidth: true,\n  minHeight: rowHeight,\n})\n\nexport interface Props {\n  onRemoveKey: () => void\n}\n\nconst SetDetailsTable = (props: Props) => {\n  const { onRemoveKey } = props\n\n  const { loading } = useSelector(setSelector)\n  const {\n    members: loadedMembers,\n    total,\n    nextCursor,\n  } = useSelector(setDataSelector)\n  const { length = 0, name: key } = useSelector(selectedKeyDataSelector) ?? {}\n  const { id: instanceId, compressor = null } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewType } = useSelector(keysSelector)\n  const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector)\n\n  const [match, setMatch] = useState('*')\n  const [deleting, setDeleting] = useState('')\n  const [width, setWidth] = useState(100)\n  const [expandedRows, setExpandedRows] = useState<number[]>([])\n  const [members, setMembers] = useState<any[]>(loadedMembers)\n  const [viewFormat, setViewFormat] = useState(viewFormatProp)\n\n  const formattedLastIndexRef = useRef(OVER_RENDER_BUFFER_COUNT)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setMembers(loadedMembers)\n\n    if (loadedMembers.length < members.length) {\n      formattedLastIndexRef.current = 0\n    }\n\n    if (viewFormat !== viewFormatProp) {\n      setExpandedRows([])\n      setViewFormat(viewFormatProp)\n\n      cellCache.clearAll()\n      setTimeout(() => {\n        cellCache.clearAll()\n      }, 0)\n    }\n  }, [loadedMembers, viewFormatProp])\n\n  const closePopover = () => {\n    setDeleting('')\n  }\n\n  const showPopover = (member = '') => {\n    setDeleting(`${member + suffix}`)\n  }\n\n  const onSuccessRemoved = (newTotal: number) => {\n    newTotal === 0 && onRemoveKey()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Set,\n        numberOfRemoved: 1,\n      },\n    })\n  }\n\n  const handleDeleteMember = (member: string | RedisString = '') => {\n    dispatch(deleteSetMembers(key, [member], onSuccessRemoved))\n    closePopover()\n  }\n\n  const handleRemoveIconClick = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Set,\n      },\n    })\n  }\n\n  const handleSearch = (search: IColumnSearchState[]) => {\n    const fieldColumn = search.find((column) => column.id === 'name')\n    if (!fieldColumn) {\n      return\n    }\n\n    const { value: match } = fieldColumn\n    const onSuccess = (data: GetSetMembersResponse) => {\n      const matchValue = getMatchType(match)\n      sendEventTelemetry({\n        event: getBasedOnViewTypeEvent(\n          viewType,\n          TelemetryEvent.BROWSER_KEY_VALUE_FILTERED,\n          TelemetryEvent.TREE_VIEW_KEY_VALUE_FILTERED,\n        ),\n        eventData: {\n          databaseId: instanceId,\n          keyType: KeyTypes.Set,\n          match: matchValue,\n          length: data.total,\n        },\n      })\n    }\n    setMatch(match)\n    dispatch(\n      fetchSetMembers(\n        key,\n        0,\n        SCAN_COUNT_DEFAULT,\n        match || matchAllValue,\n        true,\n        onSuccess,\n      ),\n    )\n  }\n\n  const handleRowToggleViewClick = (expanded: boolean, rowIndex: number) => {\n    const browserViewEvent = expanded\n      ? TelemetryEvent.BROWSER_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.BROWSER_KEY_FIELD_VALUE_COLLAPSED\n    const treeViewEvent = expanded\n      ? TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED\n\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        keyType: KeyTypes.Set,\n        databaseId: instanceId,\n        largestCellLength: members[rowIndex]?.length || 0,\n      },\n    })\n\n    cellCache.clearAll()\n  }\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'name',\n      label: 'Member',\n      isSearchable: true,\n      staySearchAlwaysOpen: true,\n      initialSearchValue: '',\n      truncateText: true,\n      render: function Name(\n        _name: string,\n        memberItem: RedisResponseBuffer,\n        expanded: boolean = false,\n      ) {\n        const { value: decompressedMemberItem } = decompressingBuffer(\n          memberItem,\n          compressor,\n        )\n        const { value, isValid } = formattingBuffer(\n          decompressedMemberItem,\n          viewFormatProp,\n          { expanded },\n        )\n        const tooltipContent = createTooltipContent(\n          value,\n          decompressedMemberItem,\n          viewFormatProp,\n        )\n        const cellContent = value?.substring?.(0, 200) ?? value\n\n        return (\n          <Text\n            color=\"secondary\"\n            component=\"div\"\n            style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}\n          >\n            <div\n              style={{ display: 'flex' }}\n              data-testid={`set-member-value-${cellContent}`}\n            >\n              <FormattedValue\n                value={value}\n                expanded={expanded}\n                title={\n                  isValid\n                    ? 'Member'\n                    : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n                }\n                tooltipContent={tooltipContent}\n                position=\"left\"\n              />\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'actions',\n      label: '',\n      relativeWidth: 60,\n      minWidth: 60,\n      maxWidth: 60,\n      headerClassName: 'hidden',\n      render: function Actions(_act: any, memberItem: RedisResponseBuffer) {\n        const member = bufferToString(memberItem, viewFormat)\n        return (\n          <div className=\"value-table-actions\">\n            <PopoverDelete\n              header={createDeleteFieldHeader(memberItem)}\n              text={createDeleteFieldMessage(key ?? '')}\n              item={member}\n              itemRaw={memberItem}\n              suffix={suffix}\n              deleting={deleting}\n              closePopover={closePopover}\n              updateLoading={false}\n              showPopover={showPopover}\n              handleDeleteItem={handleDeleteMember}\n              handleButtonClick={handleRemoveIconClick}\n              testid={`set-remove-btn-${member}`}\n              appendInfo={length === 1 ? HelpTexts.REMOVE_LAST_ELEMENT() : null}\n            />\n          </div>\n        )\n      },\n    },\n  ]\n\n  const loadMoreItems = () => {\n    if (nextCursor !== 0) {\n      dispatch(\n        fetchMoreSetMembers(\n          key,\n          nextCursor,\n          SCAN_COUNT_DEFAULT,\n          match || matchAllValue,\n        ),\n      )\n    }\n  }\n\n  return (\n    <div\n      data-testid=\"set-details\"\n      className={cx(\n        'key-details-table',\n        'set-members-container',\n        styles.container,\n      )}\n    >\n      <VirtualTable\n        autoHeight\n        expandable\n        selectable={false}\n        keyName={key}\n        headerHeight={headerHeight}\n        rowHeight={rowHeight}\n        footerHeight={footerHeight}\n        loadMoreItems={loadMoreItems}\n        loading={loading}\n        items={members}\n        totalItemsCount={total}\n        noItemsMessage={NoResultsFoundText}\n        onWheel={closePopover}\n        onSearch={handleSearch}\n        columns={columns.map((column, i, arr) => ({\n          ...column,\n          width: getColumnWidth(i, width, arr),\n        }))}\n        onChangeWidth={setWidth}\n        cellCache={cellCache}\n        onRowToggleViewClick={handleRowToggleViewClick}\n        expandedRows={expandedRows}\n        setExpandedRows={setExpandedRows}\n      />\n    </div>\n  )\n}\n\nexport { SetDetailsTable }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/index.ts",
    "content": "export { SetDetailsTable } from './SetDetailsTable'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/set-details/set-details-table/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding: 16px;\n  background-color: var(--euiColorEmptyShade);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render } from 'uiSrc/utils/test-utils'\nimport { streamSelector } from 'uiSrc/slices/browser/stream'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { Props, StreamDetails } from './StreamDetails'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  streamSelector: jest.fn().mockReturnValue({\n    viewType: 'Data',\n  }),\n}))\n\ndescribe('StreamDetails', () => {\n  it('should render', () => {\n    expect(render(<StreamDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('\"add-key-value-items-btn\" should render', () => {\n    const { queryByTestId } = render(\n      <StreamDetails {...instance(mockedProps)} />,\n    )\n    expect(queryByTestId('add-key-value-items-btn')).toBeInTheDocument()\n  })\n\n  it('\"add-stream-field-panel\" should render', () => {\n    const { container, queryByTestId } = render(\n      <StreamDetails\n        {...instance(mockedProps)}\n        onOpenAddItemPanel={() => {}}\n      />,\n    )\n\n    fireEvent.click(queryByTestId('add-key-value-items-btn')!)\n    expect(\n      container.querySelector('[data-test-subj=\"add-stream-field-panel\"]'),\n    ).toBeInTheDocument()\n    expect(\n      container.querySelector(\n        '[data-test-subj=\"add-stream-groups-field-panel\"]',\n      ),\n    ).not.toBeInTheDocument()\n  })\n  it('\"add-stream-groups-field-panel\" should render', () => {\n    const streamSelectorMock = jest.fn().mockReturnValue({\n      viewType: StreamViewType.Groups,\n    })\n    ;(streamSelector as jest.Mock).mockImplementation(streamSelectorMock)\n\n    const { container, queryByTestId } = render(\n      <StreamDetails\n        {...instance(mockedProps)}\n        onOpenAddItemPanel={() => {}}\n      />,\n    )\n\n    fireEvent.click(queryByTestId('add-key-value-items-btn')!)\n    expect(\n      container.querySelector('[data-test-subj=\"add-stream-field-panel\"]'),\n    ).not.toBeInTheDocument()\n    expect(\n      container.querySelector(\n        '[data-test-subj=\"add-stream-groups-field-panel\"]',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/StreamDetails.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { selectedKeySelector } from 'uiSrc/slices/browser/keys'\nimport {\n  KeyTypes,\n  STREAM_ADD_ACTION,\n  STREAM_ADD_GROUP_VIEW_TYPES,\n} from 'uiSrc/constants'\n\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { streamSelector } from 'uiSrc/slices/browser/stream'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { StreamDetailsBody } from './stream-details-body'\nimport AddStreamEntries from './add-stream-entity'\nimport AddStreamGroup from './add-stream-group'\nimport { StreamItemsAction } from '../key-details-actions'\nimport { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'\nimport { AddKeysContainer } from '../common/AddKeysContainer.styled'\n\nexport interface Props extends KeyDetailsHeaderProps {\n  onRemoveKey: () => void\n  onOpenAddItemPanel: () => void\n  onCloseAddItemPanel: () => void\n}\n\nconst StreamDetails = (props: Props) => {\n  const keyType = KeyTypes.Stream\n  const { onOpenAddItemPanel, onCloseAddItemPanel } = props\n\n  const { loading } = useSelector(selectedKeySelector)\n  const { viewType: streamViewType } = useSelector(streamSelector)\n\n  const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState<boolean>(false)\n\n  const openAddItemPanel = () => {\n    setIsAddItemPanelOpen(true)\n\n    if (!STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType)) {\n      onOpenAddItemPanel()\n    }\n  }\n\n  const closeAddItemPanel = (isCancelled?: boolean) => {\n    setIsAddItemPanelOpen(false)\n    if (\n      isCancelled &&\n      isAddItemPanelOpen &&\n      !STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType)\n    ) {\n      onCloseAddItemPanel()\n    }\n  }\n\n  const Actions = ({ width }: { width: number }) => (\n    <StreamItemsAction\n      width={width}\n      title={STREAM_ADD_ACTION[streamViewType].name}\n      openAddItemPanel={openAddItemPanel}\n    />\n  )\n\n  return (\n    <div className=\"fluid flex-column relative\">\n      <KeyDetailsHeader {...props} key=\"key-details-header\" />\n      <KeyDetailsSubheader keyType={keyType} Actions={Actions} />\n      <div className=\"key-details-body\" key=\"key-details-body\">\n        {!loading && (\n          <div className=\"flex-column\" style={{ flex: '1', height: '100%' }}>\n            <StreamDetailsBody />\n          </div>\n        )}\n        {isAddItemPanelOpen && (\n          <AddKeysContainer>\n            {streamViewType === StreamViewType.Data && (\n              <AddStreamEntries closePanel={closeAddItemPanel} />\n            )}\n            {STREAM_ADD_GROUP_VIEW_TYPES.includes(streamViewType!) && (\n              <AddStreamGroup closePanel={closeAddItemPanel} />\n            )}\n          </AddKeysContainer>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { StreamDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render, screen, act, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { streamDataSelector } from 'uiSrc/slices/browser/stream'\nimport AddStreamEntries, { Props } from './AddStreamEntries'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  streamDataSelector: jest.fn().mockReturnValue({}),\n}))\n\ndescribe('AddStreamEntries', () => {\n  it('should render', () => {\n    expect(render(<AddStreamEntries {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should be valid by default', () => {\n    render(<AddStreamEntries {...mockedProps} />)\n\n    expect(screen.getByTestId('save-elements-btn')).not.toBeDisabled()\n  })\n\n  it('should properly validate/show error', async () => {\n    ;(streamDataSelector as jest.Mock).mockReturnValue({\n      lastGeneratedId: '100-0',\n    })\n    render(<AddStreamEntries {...mockedProps} />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('entryId'), {\n        target: { value: '99-0' },\n      })\n    })\n\n    expect(screen.getByTestId('stream-entry-error')).toHaveTextContent(\n      'Must be greater than the last ID',\n    )\n    expect(screen.getByTestId('save-elements-btn')).toBeDisabled()\n  })\n\n  it('should properly validate', async () => {\n    ;(streamDataSelector as jest.Mock).mockReturnValue({\n      lastGeneratedId: '100-0',\n    })\n    render(<AddStreamEntries {...mockedProps} />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('entryId'), {\n        target: { value: '101-0' },\n      })\n    })\n\n    expect(screen.queryByTestId('stream-entry-error')).not.toBeInTheDocument()\n    expect(screen.getByTestId('save-elements-btn')).not.toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const EntryIdContainer = styled.div`\n  width: 50%;\n  flex-shrink: 0;\n`\n\nexport const FieldsWrapper = styled.div`\n  flex-grow: 1;\n  flex-shrink: 0;\n  width: 100%;\n  margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/AddStreamEntries.tsx",
    "content": "import { toNumber } from 'lodash'\nimport React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { entryIdRegex, stringToBuffer } from 'uiSrc/utils'\nimport {\n  keysSelector,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  addNewEntriesAction,\n  streamDataSelector,\n} from 'uiSrc/slices/browser/stream'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { AddStreamFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport { INITIAL_STREAM_FIELD_STATE } from 'uiSrc/pages/browser/components/add-key/AddKeyStream/AddKeyStream'\nimport { KeyTypes } from 'uiSrc/constants'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { AddStreamEntriesDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport StreamEntryFields from './StreamEntryFields/StreamEntryFields'\nimport { Panel } from 'uiSrc/components/panel'\n\nimport { EntryContent } from '../../common/AddKeysContainer.styled'\n\nexport interface Props {\n  closePanel: (isCancelled?: boolean) => void\n}\n\nconst AddStreamEntries = (props: Props) => {\n  const { closePanel } = props\n  const { lastGeneratedId } = useSelector(streamDataSelector)\n  const { name: keyName = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: undefined,\n  }\n  const { viewType } = useSelector(keysSelector)\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const [entryID, setEntryID] = useState<string>('*')\n  const [entryIdError, setEntryIdError] = useState('')\n  const [fields, setFields] = useState<any[]>([\n    { ...INITIAL_STREAM_FIELD_STATE },\n  ])\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    const isValid = !entryIdError\n    setIsFormValid(isValid)\n  }, [fields, entryIdError])\n\n  useEffect(() => {\n    validateEntryID()\n  }, [entryID])\n\n  const validateEntryID = () => {\n    if (!entryIdRegex.test(entryID)) {\n      setEntryIdError(`${config.entryId.name} format is incorrect`)\n      return\n    }\n\n    if (!lastGeneratedId) {\n      setEntryIdError('')\n      return\n    }\n\n    if (entryID === '*') {\n      setEntryIdError('')\n      return\n    }\n\n    const [lastIdTimestamp, lastId] = lastGeneratedId.split('-')\n    const [idTimestamp, id] = entryID?.split('-')\n\n    if (toNumber(idTimestamp) > toNumber(lastIdTimestamp)) {\n      setEntryIdError('')\n      return\n    }\n\n    if (\n      toNumber(lastIdTimestamp) === toNumber(idTimestamp) &&\n      (id === '*' || toNumber(id) > toNumber(lastId))\n    ) {\n      setEntryIdError('')\n      return\n    }\n    setEntryIdError('Must be greater than the last ID')\n  }\n\n  const onSuccessAdded = () => {\n    closePanel()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_ADDED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_ADDED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Stream,\n        numberOfAdded: fields.length,\n      },\n    })\n  }\n\n  const submitData = (): void => {\n    if (isFormValid) {\n      const data: AddStreamEntriesDto = {\n        keyName,\n        entries: [\n          {\n            id: entryID,\n            fields: [\n              ...fields.map(({ name, value }) => ({\n                name: stringToBuffer(name),\n                value: stringToBuffer(value),\n              })),\n            ],\n          },\n        ],\n      }\n      dispatch(addNewEntriesAction(data, onSuccessAdded))\n    }\n  }\n\n  return (\n    <Col gap=\"m\">\n      <EntryContent data-test-subj=\"add-stream-field-panel\">\n        <StreamEntryFields\n          entryIdError={entryIdError}\n          entryID={entryID}\n          setEntryID={setEntryID}\n          fields={fields}\n          setFields={setFields}\n        />\n      </EntryContent>\n      <Panel justify=\"end\" gap=\"m\">\n        <SecondaryButton\n          onClick={() => closePanel(true)}\n          data-testid=\"cancel-members-btn\"\n        >\n          Cancel\n        </SecondaryButton>\n        <PrimaryButton\n          size=\"m\"\n          color=\"secondary\"\n          onClick={submitData}\n          disabled={!isFormValid}\n          data-testid=\"save-elements-btn\"\n        >\n          Save\n        </PrimaryButton>\n      </Panel>\n    </Col>\n  )\n}\n\nexport default AddStreamEntries\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/StreamEntryFields/StreamEntryFields.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport StreamEntryFields, { Props } from './StreamEntryFields'\n\nconst mockedProps = mock<Props>()\n\ndescribe('StreamEntryFields', () => {\n  it('should render', () => {\n    expect(\n      render(<StreamEntryFields {...mockedProps} fields={[]} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/StreamEntryFields/StreamEntryFields.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const InlineRow = styled(Row).attrs({ as: 'span' })``\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/StreamEntryFields/StreamEntryFields.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { validateEntryId } from 'uiSrc/utils'\nimport { INITIAL_STREAM_FIELD_STATE } from 'uiSrc/pages/browser/components/add-key/AddKeyStream/AddKeyStream'\nimport { AddStreamFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Text } from 'uiSrc/components/base/text'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { streamIDTooltipText } from 'uiSrc/constants/texts'\nimport { EntryIdContainer, FieldsWrapper } from '../AddStreamEntries.styles'\nimport { InlineRow } from './StreamEntryFields.styles'\nimport {\n  StreamGroupContent,\n  TimeStampInfoIcon,\n} from '../../add-stream-group/AddStreamGroup.styles'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  entryIdError?: string\n  entryID: string\n  setEntryID: React.Dispatch<React.SetStateAction<string>>\n  fields: any[]\n  setFields: React.Dispatch<React.SetStateAction<any[]>>\n}\n\nconst MIN_ENTRY_ID_VALUE = '0-1'\n\nconst StreamEntryFields = (props: Props) => {\n  const { entryID, setEntryID, entryIdError, fields, setFields } = props\n\n  const [isEntryIdFocused, setIsEntryIdFocused] = React.useState(false)\n  const prevCountFields = useRef<number>(0)\n  const lastAddedFieldName = useRef<HTMLInputElement>(null)\n  const entryIdRef = useRef<HTMLInputElement>(null)\n\n  const isClearDisabled = (item: any): boolean =>\n    fields.length === 1 && !(item.name.length || item.value.length)\n\n  useEffect(() => {\n    if (\n      prevCountFields.current !== 0 &&\n      prevCountFields.current < fields.length\n    ) {\n      lastAddedFieldName.current?.focus()\n    }\n    prevCountFields.current = fields.length\n  }, [fields.length])\n\n  const addField = () => {\n    const lastField = fields[fields.length - 1]\n    const newState = [\n      ...fields,\n      {\n        ...INITIAL_STREAM_FIELD_STATE,\n        id: lastField.id + 1,\n      },\n    ]\n    setFields(newState)\n  }\n\n  const removeField = (id: number) => {\n    const newState = fields.filter((item) => item.id !== id)\n    setFields(newState)\n  }\n\n  const clearFieldsValues = (id: number) => {\n    const newState = fields.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            name: '',\n            value: '',\n          }\n        : item,\n    )\n    setFields(newState)\n  }\n\n  const onClickRemove = ({ id }: any) => {\n    if (fields.length === 1) {\n      clearFieldsValues(id)\n      return\n    }\n\n    removeField(id)\n  }\n\n  const handleEntryIdChange = (value: string) => {\n    setEntryID(validateEntryId(value))\n  }\n\n  const handleFieldChange = (formField: string, id: number, value: any) => {\n    const newState = fields.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: value,\n        }\n      }\n      return item\n    })\n    setFields(newState)\n  }\n\n  const onEntryIdBlur = (e: React.FocusEvent<HTMLInputElement>) => {\n    e.target.value === '0-0' && setEntryID(MIN_ENTRY_ID_VALUE)\n    setIsEntryIdFocused(false)\n  }\n\n  const showEntryError = !isEntryIdFocused && entryIdError\n\n  return (\n    <StreamGroupContent>\n      <EntryIdContainer>\n        <FormField\n          label={config.entryId.label}\n          additionalText={\n            <InlineRow align=\"center\" gap=\"s\">\n              <RiTooltip\n                anchorClassName=\"inputAppendIcon\"\n                className={styles.entryIdTooltip}\n                position=\"left\"\n                title=\"Enter Valid ID or *\"\n                content={streamIDTooltipText}\n              >\n                <TimeStampInfoIcon />\n              </RiTooltip>\n              {!showEntryError && (\n                <Text component=\"span\" size=\"XS\" color=\"primary\">\n                  Timestamp - Sequence Number or *\n                </Text>\n              )}\n              {showEntryError && (\n                <Text\n                  component=\"span\"\n                  size=\"XS\"\n                  color=\"danger\"\n                  data-testid=\"stream-entry-error\"\n                >\n                  {entryIdError}\n                </Text>\n              )}\n            </InlineRow>\n          }\n        >\n          <TextInput\n            ref={entryIdRef}\n            name={config.entryId.name}\n            id={config.entryId.id}\n            placeholder={config.entryId.placeholder}\n            value={entryID}\n            onChange={handleEntryIdChange}\n            onBlur={onEntryIdBlur}\n            onFocus={() => setIsEntryIdFocused(true)}\n            autoComplete=\"off\"\n            data-testid={config.entryId.id}\n          />\n        </FormField>\n      </EntryIdContainer>\n\n      <FieldsWrapper>\n        <AddMultipleFields\n          items={fields}\n          isClearDisabled={isClearDisabled}\n          onClickRemove={onClickRemove}\n          onClickAdd={addField}\n        >\n          {(item, index) => (\n            <Row align=\"center\" gap=\"m\">\n              <FlexItem grow>\n                <FormField>\n                  <TextInput\n                    name={`fieldName-${item.id}`}\n                    id={`fieldName-${item.id}`}\n                    placeholder={config.name.placeholder}\n                    value={item.name}\n                    onChange={(value) =>\n                      handleFieldChange('name', item.id, value)\n                    }\n                    ref={\n                      index === fields.length - 1 ? lastAddedFieldName : null\n                    }\n                    autoComplete=\"off\"\n                    data-testid=\"field-name\"\n                  />\n                </FormField>\n              </FlexItem>\n              <FlexItem grow={2}>\n                <FormField>\n                  <TextInput\n                    name={`fieldValue-${item.id}`}\n                    id={`fieldValue-${item.id}`}\n                    placeholder={config.value.placeholder}\n                    value={item.value}\n                    onChange={(value) =>\n                      handleFieldChange('value', item.id, value)\n                    }\n                    autoComplete=\"off\"\n                    data-testid=\"field-value\"\n                  />\n                </FormField>\n              </FlexItem>\n            </Row>\n          )}\n        </AddMultipleFields>\n      </FieldsWrapper>\n    </StreamGroupContent>\n  )\n}\n\nexport default StreamEntryFields\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/index.ts",
    "content": "import StreamEntryFields from './StreamEntryFields/StreamEntryFields'\nimport AddStreamEntries from './AddStreamEntries'\n\nexport { StreamEntryFields }\nexport default AddStreamEntries\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-entity/styles.module.scss",
    "content": ".entryIdTooltip {\n  max-width: 275px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { instance, mock } from 'ts-mockito'\nimport AddStreamGroup, { Props } from './AddStreamGroup'\n\nconst GROUP_NAME_FIELD = 'group-name-field'\nconst ID_FIELD = 'id-field'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddStreamGroup', () => {\n  it('should render', () => {\n    expect(render(<AddStreamGroup {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set member value properly', () => {\n    render(<AddStreamGroup {...instance(mockedProps)} />)\n    const groupNameInput = screen.getByTestId(GROUP_NAME_FIELD)\n    fireEvent.change(groupNameInput, { target: { value: 'group name' } })\n    expect(groupNameInput).toHaveValue('group name')\n  })\n\n  it('should set score value properly if input wrong value', () => {\n    render(<AddStreamGroup {...instance(mockedProps)} />)\n    const idInput = screen.getByTestId(ID_FIELD)\n    fireEvent.change(idInput, { target: { value: 'aa1x-5' } })\n    expect(idInput).toHaveValue('1-5')\n  })\n\n  it('should able to save with valid data', () => {\n    render(<AddStreamGroup {...instance(mockedProps)} />)\n    const groupNameInput = screen.getByTestId(GROUP_NAME_FIELD)\n    const idInput = screen.getByTestId(ID_FIELD)\n    fireEvent.change(groupNameInput, { target: { value: 'name' } })\n    fireEvent.change(idInput, { target: { value: '11111-3' } })\n    expect(screen.getByTestId('save-groups-btn')).not.toBeDisabled()\n  })\n\n  it('should not able to save with valid data', () => {\n    render(<AddStreamGroup {...instance(mockedProps)} />)\n    const groupNameInput = screen.getByTestId(GROUP_NAME_FIELD)\n    const idInput = screen.getByTestId(ID_FIELD)\n    fireEvent.change(groupNameInput, { target: { value: 'name' } })\n    fireEvent.change(idInput, { target: { value: '11111----' } })\n    expect(screen.getByTestId('save-groups-btn')).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nexport const TimeStampWrapper = styled(FlexItem).attrs({ grow: true })`\n  min-width: 225px;\n`\nexport const TimeStampInfoIcon = styled(RiIcon).attrs({ type: 'InfoIcon' })`\n  cursor: pointer;\n`\n\nexport const StreamGroupContent = styled(Col)`\n  max-height: calc(50vh - 100px);\n  scroll-padding-bottom: 30px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/AddStreamGroup.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport { addNewGroupAction } from 'uiSrc/slices/browser/stream'\nimport {\n  consumerGroupIdRegex,\n  stringToBuffer,\n  validateConsumerGroupId,\n} from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiTooltip } from 'uiSrc/components'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { CreateConsumerGroupsDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport { Panel } from 'uiSrc/components/panel'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  StreamGroupContent,\n  TimeStampInfoIcon,\n  TimeStampWrapper,\n} from './AddStreamGroup.styles'\n\nexport interface Props {\n  closePanel: (isCancelled?: boolean) => void\n}\n\nconst AddStreamGroup = (props: Props) => {\n  const { closePanel } = props\n  const { name: keyName = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: undefined,\n  }\n\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n  const [groupName, setGroupName] = useState<string>('')\n  const [id, setId] = useState<string>('$')\n  const [idError, setIdError] = useState<string>('')\n  const [isIdFocused, setIsIdFocused] = useState<boolean>(false)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    const isValid = !!groupName.length && !idError\n    setIsFormValid(isValid)\n  }, [groupName, idError])\n\n  useEffect(() => {\n    if (!consumerGroupIdRegex.test(id)) {\n      setIdError('ID format is not correct')\n      return\n    }\n    setIdError('')\n  }, [id])\n\n  const onSuccessAdded = () => {\n    closePanel()\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_GROUP_CREATED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const submitData = () => {\n    if (isFormValid) {\n      const data: CreateConsumerGroupsDto = {\n        keyName,\n        consumerGroups: [\n          {\n            name: stringToBuffer(groupName),\n            lastDeliveredId: id,\n          },\n        ],\n      }\n      dispatch(addNewGroupAction(data, onSuccessAdded))\n    }\n  }\n\n  const showIdError = !isIdFocused && idError\n\n  return (\n    <Col gap=\"m\">\n      <StreamGroupContent data-test-subj=\"add-stream-groups-field-panel\">\n        <FlexItem grow>\n          <Row gap=\"m\">\n            <FlexItem grow>\n              <Row align=\"start\" gap=\"m\">\n                <FlexItem grow={2}>\n                  <FormField>\n                    <TextInput\n                      name=\"group-name\"\n                      id=\"group-name\"\n                      placeholder=\"Enter Group Name*\"\n                      value={groupName}\n                      onChange={(value) => setGroupName(value)}\n                      autoComplete=\"off\"\n                      data-testid=\"group-name-field\"\n                    />\n                  </FormField>\n                </FlexItem>\n                <TimeStampWrapper>\n                  <FormField\n                    additionalText={\n                      <Row align=\"center\" gap=\"s\">\n                        <RiTooltip\n                          anchorClassName=\"inputAppendIcon\"\n                          position=\"left\"\n                          title=\"Enter Valid ID, 0 or $\"\n                          content={lastDeliveredIDTooltipText}\n                        >\n                          <TimeStampInfoIcon data-testid=\"entry-id-info-icon\" />\n                        </RiTooltip>\n                        {!showIdError && (\n                          <Text\n                            size=\"XS\"\n                            color=\"primary\"\n                            data-testid=\"id-help-text\"\n                          >\n                            Timestamp - Sequence Number or $\n                          </Text>\n                        )}\n                        {showIdError && (\n                          <Text size=\"XS\" color=\"danger\" data-testid=\"id-error\">\n                            {idError}\n                          </Text>\n                        )}\n                      </Row>\n                    }\n                  >\n                    <TextInput\n                      name=\"id\"\n                      id=\"id\"\n                      placeholder=\"ID*\"\n                      value={id}\n                      onChange={(value) =>\n                        setId(validateConsumerGroupId(value))\n                      }\n                      onBlur={() => setIsIdFocused(false)}\n                      onFocus={() => setIsIdFocused(true)}\n                      autoComplete=\"off\"\n                      data-testid=\"id-field\"\n                    />\n                  </FormField>\n                </TimeStampWrapper>\n              </Row>\n            </FlexItem>\n          </Row>\n        </FlexItem>\n      </StreamGroupContent>\n      <Panel justify=\"end\" gap=\"m\">\n        <SecondaryButton\n          onClick={() => closePanel(true)}\n          data-testid=\"cancel-stream-groups-btn\"\n        >\n          Cancel\n        </SecondaryButton>\n        <PrimaryButton\n          onClick={submitData}\n          disabled={!isFormValid}\n          data-testid=\"save-groups-btn\"\n        >\n          Save\n        </PrimaryButton>\n      </Panel>\n    </Col>\n  )\n}\n\nexport default AddStreamGroup\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/add-stream-group/index.ts",
    "content": "import AddStreamGroup from './AddStreamGroup'\n\nexport default AddStreamGroup\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/constants.ts",
    "content": "export const MAX_FORMAT_LENGTH_STREAM_TIMESTAMP = 16\nexport const MAX_VISIBLE_LENGTH_STREAM_TIMESTAMP = 25\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/ConsumersView/ConsumersView.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { ConsumerDto } from 'apiSrc/modules/browser/stream/dto'\nimport ConsumersView, { Props } from './ConsumersView'\n\nconst mockedProps = mock<Props>()\nconst mockConsumers: ConsumerDto[] = [\n  {\n    name: 'test',\n    idle: 123,\n    pending: 321,\n  },\n  {\n    name: 'test2',\n    idle: 13,\n    pending: 31,\n  },\n]\n\ndescribe('ConsumersView', () => {\n  it('should render', () => {\n    expect(\n      render(<ConsumersView {...instance(mockedProps)} data={mockConsumers} />),\n    ).toBeTruthy()\n  })\n\n  it('should render default message when no consumers and custom message is not specified', () => {\n    render(<ConsumersView {...instance(mockedProps)} data={[]} />)\n    expect(screen.getByTestId('stream-consumers-container')).toHaveTextContent(\n      'Your Consumer Group has no Consumers available.',\n    )\n  })\n\n  it('should render custom message when no consumers and custom message is specified', () => {\n    render(\n      <ConsumersView\n        {...instance(mockedProps)}\n        data={[]}\n        noItemsMessageString=\"customNoItemsMessageString\"\n      />,\n    )\n    expect(screen.getByTestId('stream-consumers-container')).toHaveTextContent(\n      'customNoItemsMessageString',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/ConsumersView/ConsumersView.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { orderBy } from 'lodash'\n\nimport { streamGroupsSelector } from 'uiSrc/slices/browser/stream'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport { SortOrder } from 'uiSrc/constants'\nimport { ConsumerDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport styles from './styles.module.scss'\n\nconst headerHeight = 60\nconst rowHeight = 54\n\nexport interface Props {\n  data: ConsumerDto[]\n  columns: ITableColumn[]\n  onClosePopover: () => void\n  onSelectConsumer: ({ rowData }: { rowData: any }) => void\n  noItemsMessageString?: string\n}\n\nconst ConsumersView = (props: Props) => {\n  const {\n    data = [],\n    columns = [],\n    onClosePopover,\n    onSelectConsumer,\n    noItemsMessageString = 'Your Consumer Group has no Consumers available.',\n  } = props\n\n  const { loading } = useSelector(streamGroupsSelector)\n  const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? {}\n\n  const [consumers, setConsumers] = useState(data)\n  const [sortedColumnName, setSortedColumnName] = useState<string>('name')\n  const [sortedColumnOrder, setSortedColumnOrder] = useState<SortOrder>(\n    SortOrder.ASC,\n  )\n\n  useEffect(() => {\n    setConsumers(\n      orderBy(data, sortedColumnName, sortedColumnOrder?.toLowerCase()),\n    )\n  }, [data])\n\n  const onChangeSorting = (column: any, order: SortOrder) => {\n    setSortedColumnName(column)\n    setSortedColumnOrder(order)\n\n    setConsumers(orderBy(consumers, column, order?.toLowerCase()))\n  }\n\n  return (\n    <>\n      <div\n        className={cx(\n          'key-details-table',\n          'stream-details-table',\n          styles.container,\n        )}\n        data-testid=\"stream-consumers-container\"\n      >\n        <VirtualTable\n          autoHeight\n          hideProgress\n          onRowClick={onSelectConsumer}\n          selectable={false}\n          keyName={key}\n          totalItemsCount={consumers.length}\n          headerHeight={consumers?.length ? headerHeight : 0}\n          rowHeight={rowHeight}\n          columns={columns}\n          footerHeight={0}\n          loading={loading}\n          items={consumers}\n          onWheel={onClosePopover}\n          onChangeSorting={onChangeSorting}\n          noItemsMessage={noItemsMessageString}\n          sortedColumn={\n            consumers?.length\n              ? {\n                  column: sortedColumnName,\n                  order: sortedColumnOrder,\n                }\n              : undefined\n          }\n        />\n      </div>\n    </>\n  )\n}\n\nexport default ConsumersView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/ConsumersView/index.ts",
    "content": "import ConsumersView from './ConsumersView'\n\nexport * from './ConsumersView'\n\nexport default ConsumersView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/ConsumersView/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding-top: 3px;\n  background-color: var(--euiColorEmptyShade);\n}\n\n.actions,\n.actionsHeader {\n  width: 54px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/ConsumersViewWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  deleteConsumers,\n  loadConsumerGroups,\n  selectedGroupSelector,\n  setSelectedConsumer,\n} from 'uiSrc/slices/browser/stream'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { bufferToString } from 'uiSrc/utils'\nimport { TEXT_CONSUMER_GROUP_NAME_TOO_LONG } from 'uiSrc/constants'\nimport {\n  MOCK_TRUNCATED_BUFFER_VALUE,\n  MOCK_TRUNCATED_STRING_VALUE,\n} from 'uiSrc/mocks/data/bigString'\nimport { ConsumerDto } from 'apiSrc/modules/browser/stream/dto'\nimport ConsumersViewWrapper, { Props } from './ConsumersViewWrapper'\nimport ConsumersView, { Props as ConsumersViewProps } from './ConsumersView'\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  selectedGroupSelector: jest.fn(\n    () =>\n      jest.requireActual('uiSrc/slices/browser/stream').selectedGroupSelector,\n  ),\n}))\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('./ConsumersView', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockConsumerName = 'group'\nconst mockConsumers: ConsumerDto[] = [\n  {\n    name: {\n      ...bufferToString('test'),\n      viewValue: 'test',\n    },\n    idle: 123,\n    pending: 321,\n  },\n  {\n    name: {\n      ...bufferToString('test2'),\n      viewValue: 'test2',\n    },\n    idle: 13,\n    pending: 31,\n  },\n]\n\nconst mockConsumersView = jest.fn((props: ConsumersViewProps) => (\n  <div data-testid=\"stream-consumers-container\">\n    <button\n      type=\"button\"\n      data-testid=\"select-consumer-btn\"\n      onClick={() => props?.onSelectConsumer?.({ name: mockConsumerName })}\n    >\n      some consumer name\n    </button>\n\n    <VirtualTable\n      items={mockConsumers}\n      loading={false}\n      onRowClick={props?.onSelectConsumer}\n      columns={props.columns}\n    />\n  </div>\n))\n\ndescribe('ConsumersViewWrapper', () => {\n  beforeAll(() => {\n    ConsumersView.mockImplementation(mockConsumersView)\n  })\n\n  it('should render', () => {\n    expect(\n      render(<ConsumersViewWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render Consumers container', () => {\n    render(<ConsumersViewWrapper {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('stream-consumers-container')).toBeInTheDocument()\n  })\n\n  it('should select Consumer', () => {\n    render(<ConsumersViewWrapper {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('select-consumer-btn'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedConsumer(),\n      loadConsumerGroups(false),\n    ])\n  })\n\n  it('should delete Consumer', () => {\n    render(<ConsumersViewWrapper {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('remove-consumer-button-test-icon'))\n    fireEvent.click(screen.getByTestId('remove-consumer-button-test'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      deleteConsumers(),\n    ])\n  })\n\n  describe('truncated data', () => {\n    it('should pass special noItemsMessageString message for truncated group name', () => {\n      ;(selectedGroupSelector as jest.Mock).mockImplementationOnce(() => ({\n        name: MOCK_TRUNCATED_BUFFER_VALUE,\n        nameString: MOCK_TRUNCATED_STRING_VALUE,\n        data: [],\n        selectedConsumer: null,\n        lastRefreshTime: null,\n      }))\n\n      render(<ConsumersViewWrapper {...instance(mockedProps)} />)\n\n      expect(mockConsumersView).toHaveBeenCalledWith(\n        expect.objectContaining({\n          noItemsMessageString: TEXT_CONSUMER_GROUP_NAME_TOO_LONG,\n          data: [],\n        }),\n        expect.anything(),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/ConsumersViewWrapper.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  setStreamViewType,\n  selectedGroupSelector,\n  setSelectedConsumer,\n  fetchConsumerMessages,\n  deleteConsumersAction,\n} from 'uiSrc/slices/browser/stream'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport {\n  TableCellAlignment,\n  TableCellTextAlignment,\n  TEXT_CONSUMER_GROUP_NAME_TOO_LONG,\n} from 'uiSrc/constants'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport {\n  selectedKeyDataSelector,\n  updateSelectedKeyRefreshTime,\n} from 'uiSrc/slices/browser/keys'\nimport { formatLongName, isTruncatedString } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { ConsumerDto } from 'apiSrc/modules/browser/stream/dto'\nimport ConsumersView from './ConsumersView'\n\nimport styles from './ConsumersView/styles.module.scss'\n\nconst suffix = '_stream_consumer'\nconst actionsWidth = 50\n\nexport interface Props {}\n\nconst ConsumersViewWrapper = (props: Props) => {\n  const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: '',\n  }\n  const {\n    name: selectedGroupName = '',\n    nameString: selectedGroupNameString = '',\n    lastRefreshTime,\n    data: loadedConsumers = [],\n  } = useSelector(selectedGroupSelector) ?? {}\n\n  const isTruncatedGroupName = isTruncatedString(selectedGroupName)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  const [deleting, setDeleting] = useState<string>('')\n\n  useEffect(() => {\n    dispatch(updateSelectedKeyRefreshTime(lastRefreshTime))\n  }, [])\n\n  const closePopover = () => {\n    setDeleting('')\n  }\n\n  const showPopover = (consumer = '') => {\n    setDeleting(`${consumer + suffix}`)\n  }\n\n  const onSuccessDeletedConsumer = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_DELETED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    closePopover()\n  }\n\n  const handleDeleteConsumer = (consumerName = '') => {\n    dispatch(\n      deleteConsumersAction(\n        key,\n        selectedGroupName,\n        [consumerName],\n        onSuccessDeletedConsumer,\n      ),\n    )\n  }\n\n  const handleRemoveIconClick = () => {\n    // sendEventTelemetry({\n    //   event: getBasedOnViewTypeEvent(\n    //     viewType,\n    //     TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n    //     TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED\n    //   ),\n    //   eventData: {\n    //     databaseId: instanceId,\n    //     keyType: KeyTypes.Stream\n    //   }\n    // })\n  }\n\n  const handleSelectConsumer = ({ rowData }: { rowData: any }) => {\n    dispatch(setSelectedConsumer(rowData))\n    dispatch(\n      fetchConsumerMessages(false, () =>\n        dispatch(setStreamViewType(StreamViewType.Messages)),\n      ),\n    )\n  }\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'name',\n      label: 'Consumer Name',\n      minWidth: 200,\n      truncateText: true,\n      isSortable: true,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: function Name(_name: string, { name }: ConsumerDto) {\n        // Better to cut the long string, because it could affect virtual scroll performance\n        const viewName = name?.viewValue ?? ''\n        const cellContent = viewName.substring(0, 200)\n        const tooltipContent = formatLongName(viewName)\n        return (\n          <Text component=\"div\" style={{ maxWidth: '100%' }} color=\"secondary\">\n            <div\n              style={{ display: 'flex' }}\n              className=\"truncateText\"\n              data-testid={`stream-consumer-${viewName}`}\n            >\n              <RiTooltip\n                className={styles.tooltipName}\n                anchorClassName=\"truncateText\"\n                position=\"bottom\"\n                content={tooltipContent}\n              >\n                <>{cellContent}</>\n              </RiTooltip>\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'pending',\n      label: 'Pending',\n      minWidth: 106,\n      maxWidth: 106,\n      absoluteWidth: 106,\n      truncateText: true,\n      isSortable: true,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: (cellData: number) => <Text color=\"secondary\">{cellData}</Text>,\n    },\n    {\n      id: 'idle',\n      label: 'Idle Time, msec',\n      minWidth: 140,\n      maxWidth: 140,\n      absoluteWidth: 140,\n      isSortable: true,\n      alignment: TableCellAlignment.Right,\n      className: styles.cell,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: (cellData: number) => (\n        <Text color=\"secondary\">{numberWithSpaces(cellData)}</Text>\n      ),\n    },\n    {\n      id: 'actions',\n      label: '',\n      headerClassName: 'streamItemHeader',\n      textAlignment: TableCellTextAlignment.Left,\n      absoluteWidth: actionsWidth,\n      maxWidth: actionsWidth,\n      minWidth: actionsWidth,\n      render: function Actions(_act: any, { name }: ConsumerDto) {\n        const viewName = name?.viewValue ?? ''\n        return (\n          <div>\n            <PopoverDelete\n              header={viewName}\n              text={\n                <>\n                  will be removed from Consumer Group{' '}\n                  <b>{selectedGroupNameString}</b>\n                </>\n              }\n              item={viewName}\n              suffix={suffix}\n              deleting={deleting}\n              closePopover={closePopover}\n              updateLoading={false}\n              showPopover={showPopover}\n              testid={`remove-consumer-button-${viewName}`}\n              handleDeleteItem={() => handleDeleteConsumer(name)}\n              handleButtonClick={handleRemoveIconClick}\n            />\n          </div>\n        )\n      },\n    },\n  ]\n\n  return (\n    <>\n      <ConsumersView\n        data={loadedConsumers}\n        columns={columns}\n        onClosePopover={closePopover}\n        onSelectConsumer={handleSelectConsumer}\n        {...props}\n        noItemsMessageString={\n          isTruncatedGroupName ? TEXT_CONSUMER_GROUP_NAME_TOO_LONG : undefined\n        }\n      />\n    </>\n  )\n}\n\nexport default ConsumersViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/consumers-view/index.ts",
    "content": "import ConsumersViewWrapper from './ConsumersViewWrapper'\n\nexport * from './ConsumersViewWrapper'\n\nexport default ConsumersViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/GroupsView/GroupsView.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport GroupsView, { Props, IConsumerGroup } from './GroupsView'\n\nconst mockedProps = mock<Props>()\n\nconst mockGroups: IConsumerGroup[] = [\n  {\n    name: 'test',\n    consumers: 123,\n    pending: 321,\n    smallestPendingId: '123',\n    greatestPendingId: '123',\n    lastDeliveredId: '123',\n    editing: false,\n  },\n  {\n    name: 'test2',\n    consumers: 13,\n    pending: 31,\n    smallestPendingId: '3',\n    greatestPendingId: '23',\n    lastDeliveredId: '12',\n    editing: false,\n  },\n]\n\ndescribe('GroupsView', () => {\n  it('should render', () => {\n    expect(\n      render(<GroupsView {...instance(mockedProps)} data={mockGroups} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/GroupsView/GroupsView.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { orderBy } from 'lodash'\n\nimport { streamGroupsSelector } from 'uiSrc/slices/browser/stream'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport { SortOrder } from 'uiSrc/constants'\nimport { ConsumerGroupDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport styles from './styles.module.scss'\n\nconst headerHeight = 60\nconst rowHeight = 54\nconst noItemsMessageString = 'Your Key has no Consumer Groups available.'\n\nexport interface IConsumerGroup extends ConsumerGroupDto {\n  editing: boolean\n}\n\nexport interface Props {\n  data: IConsumerGroup[]\n  columns: ITableColumn[]\n  onClosePopover: () => void\n  onSelectGroup: ({ rowData }: { rowData: any }) => void\n}\n\nconst ConsumerGroups = (props: Props) => {\n  const { data = [], columns = [], onClosePopover, onSelectGroup } = props\n\n  const { loading } = useSelector(streamGroupsSelector)\n  const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? {}\n\n  const [groups, setGroups] = useState<IConsumerGroup[]>([])\n  const [sortedColumnName, setSortedColumnName] = useState<string>('name')\n  const [sortedColumnOrder, setSortedColumnOrder] = useState<SortOrder>(\n    SortOrder.ASC,\n  )\n\n  useEffect(() => {\n    setGroups(orderBy(data, sortedColumnName, sortedColumnOrder?.toLowerCase()))\n  }, [data])\n\n  const onChangeSorting = useCallback(\n    (column: any, order: SortOrder) => {\n      setSortedColumnName(column)\n      setSortedColumnOrder(order)\n\n      setGroups(\n        orderBy(\n          data,\n          [column === 'name' ? `${column}.viewValue` : column],\n          order?.toLowerCase(),\n        ),\n      )\n    },\n    [groups],\n  )\n\n  return (\n    <>\n      <div\n        className={cx(\n          'key-details-table',\n          'stream-details-table',\n          styles.container,\n        )}\n        data-testid=\"stream-groups-container\"\n      >\n        <VirtualTable\n          autoHeight\n          hideProgress\n          onRowClick={onSelectGroup}\n          selectable={false}\n          keyName={key}\n          totalItemsCount={groups.length}\n          headerHeight={groups?.length ? headerHeight : 0}\n          rowHeight={rowHeight}\n          columns={columns}\n          footerHeight={0}\n          loading={loading}\n          items={groups}\n          tableWidth={columns.reduce((a, b) => a + (b.minWidth ?? 0), 0)}\n          onWheel={onClosePopover}\n          onChangeSorting={onChangeSorting}\n          noItemsMessage={noItemsMessageString}\n          sortedColumn={\n            groups?.length\n              ? {\n                  column: sortedColumnName,\n                  order: sortedColumnOrder,\n                }\n              : undefined\n          }\n        />\n      </div>\n    </>\n  )\n}\n\nexport default ConsumerGroups\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/GroupsView/index.ts",
    "content": "import GroupsView from './GroupsView'\n\nexport * from './GroupsView'\n\nexport default GroupsView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/GroupsView/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding-top: 3px;\n  background-color: var(--euiColorEmptyShade);\n}\n\n.actions,\n.actionsHeader {\n  width: 54px;\n}\n\n.tooltip {\n  min-width: 330px;\n}\n\n.editableCell {\n  padding: 12px 40px 12px 12px;\n}\n\n.idText, .error {\n  display: inline-block;\n  color: var(--euiColorMediumShade);\n  font: normal normal normal 12px/18px Graphik, sans-serif;\n  margin-top: 6px;\n  padding-right: 6px;\n}\n\n.error {\n  color: var(--euiColorDangerText);\n}\n\n.container {\n  :global(.ReactVirtualized__Table__rowColumn) {\n    display: flex;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/GroupsViewWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  deleteConsumerGroups,\n  loadConsumerGroups,\n  setSelectedGroup,\n  fetchConsumers,\n} from 'uiSrc/slices/browser/stream'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { ConsumerGroupDto } from 'apiSrc/modules/browser/stream/dto'\nimport GroupsView, { Props as GroupsViewProps } from './GroupsView'\nimport GroupsViewWrapper, { Props } from './GroupsViewWrapper'\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  streamGroupsSelector: jest.fn().mockReturnValue({\n    loading: false,\n    error: '',\n    data: [],\n    selectedGroup: null,\n    lastRefreshTime: 0,\n  }),\n  fetchConsumers: jest\n    .fn()\n    .mockImplementation(\n      jest.requireActual('uiSrc/slices/browser/stream').fetchConsumers,\n    ),\n}))\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('./GroupsView', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockGroups: ConsumerGroupDto[] = [\n  {\n    name: {\n      ...stringToBuffer('test'),\n      viewValue: 'test',\n    },\n    consumers: 123,\n    pending: 321,\n    smallestPendingId: '123',\n    greatestPendingId: '123',\n    lastDeliveredId: '123',\n  },\n  {\n    name: {\n      ...stringToBuffer('test2'),\n      viewValue: 'test2',\n    },\n    consumers: 13,\n    pending: 31,\n    smallestPendingId: '3',\n    greatestPendingId: '23',\n    lastDeliveredId: '12',\n  },\n  {\n    name: MOCK_TRUNCATED_BUFFER_VALUE,\n    consumers: 1,\n    pending: 1,\n    smallestPendingId: 'n/a',\n    greatestPendingId: 'n/a',\n    lastDeliveredId: 'n/a',\n  },\n]\n\nconst mockGroupsView = jest.fn((props: GroupsViewProps) => (\n  <div data-testid=\"stream-groups-container\">\n    <VirtualTable\n      items={mockGroups}\n      loading={false}\n      onRowClick={props?.onSelectGroup}\n      columns={props.columns}\n    />\n  </div>\n))\n\ndescribe('GroupsViewWrapper', () => {\n  beforeAll(() => {\n    ;(GroupsView as jest.Mock).mockImplementation(mockGroupsView)\n  })\n\n  it('should render', () => {\n    expect(\n      render(<GroupsViewWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render Groups container', () => {\n    render(<GroupsViewWrapper {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('stream-groups-container')).toBeInTheDocument()\n  })\n\n  it('should select Group', () => {\n    render(<GroupsViewWrapper {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getAllByRole('row')[1])\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedGroup(mockGroups[0]),\n      loadConsumerGroups(false),\n    ])\n  })\n\n  it('should delete Group', () => {\n    render(<GroupsViewWrapper {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('remove-groups-button-test-icon'))\n    fireEvent.click(screen.getByTestId('remove-groups-button-test'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      deleteConsumerGroups(),\n    ])\n  })\n\n  it('should disable refresh when editing Group', () => {\n    render(<GroupsViewWrapper {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('stream-group_content-value-123'))\n    })\n    fireEvent.click(screen.getByTestId('stream-group_edit-btn-123'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  describe('truncated values', () => {\n    it('should not try to fetch consumers for truncated groups', async () => {\n      jest.clearAllMocks()\n\n      render(<GroupsViewWrapper {...instance(mockedProps)} />)\n\n      fireEvent.click(screen.getAllByRole('row')[3])\n\n      expect(fetchConsumers as jest.Mock).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/GroupsViewWrapper.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport cx from 'classnames'\nimport { lastDeliveredIDTooltipText } from 'uiSrc/constants/texts'\nimport {\n  selectedKeyDataSelector,\n  setSelectedKeyRefreshDisabled,\n  updateSelectedKeyRefreshTime,\n} from 'uiSrc/slices/browser/keys'\n\nimport {\n  streamGroupsSelector,\n  setSelectedGroup,\n  fetchConsumers,\n  setStreamViewType,\n  modifyLastDeliveredIdAction,\n  deleteConsumerGroupsAction,\n} from 'uiSrc/slices/browser/stream'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport {\n  bufferToString,\n  consumerGroupIdRegex,\n  formatLongName,\n  isTruncatedString,\n  isEqualBuffers,\n  validateConsumerGroupId,\n} from 'uiSrc/utils'\nimport { TableCellTextAlignment } from 'uiSrc/constants'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport EditablePopover from 'uiSrc/pages/browser/modules/key-details/shared/editable-popover'\n\nimport { FormatedDate, RiTooltip } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { ComposedInput } from 'uiSrc/components/base/inputs'\n\nimport {\n  ConsumerDto,\n  ConsumerGroupDto,\n  UpdateConsumerGroupDto,\n} from 'apiSrc/modules/browser/stream/dto'\n\nimport GroupsView from './GroupsView'\n\nimport styles from './GroupsView/styles.module.scss'\n\nexport interface IConsumerGroup extends ConsumerGroupDto {\n  editing: boolean\n}\n\nconst suffix = '_stream_group'\nconst actionsWidth = 48\n\nexport interface Props {}\n\nconst GroupsViewWrapper = (props: Props) => {\n  const {\n    lastRefreshTime,\n    data: loadedGroups = [],\n    loading,\n  } = useSelector(streamGroupsSelector)\n  const { name: selectedKey, nameString: selectedKeyString } = useSelector(\n    selectedKeyDataSelector,\n  ) ?? { name: '', nameString: '' }\n\n  const dispatch = useDispatch()\n\n  const [groups, setGroups] = useState<IConsumerGroup[]>([])\n  const [deleting, setDeleting] = useState<string>('')\n  const [editValue, setEditValue] = useState<string>('')\n  const [idError, setIdError] = useState<string>('')\n  const [isIdFocused, setIsIdFocused] = useState<boolean>(false)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  useEffect(() => {\n    dispatch(updateSelectedKeyRefreshTime(lastRefreshTime))\n  }, [lastRefreshTime])\n\n  useEffect(() => {\n    const streamItem: IConsumerGroup[] = loadedGroups?.map((item) =>\n      formatItem(item),\n    )\n\n    setGroups(streamItem)\n  }, [loadedGroups, deleting])\n\n  useEffect(() => {\n    if (!consumerGroupIdRegex.test(editValue)) {\n      setIdError('ID format is not correct')\n      return\n    }\n    setIdError('')\n  }, [editValue])\n\n  const formatItem = useCallback(\n    (item: ConsumerGroupDto): IConsumerGroup => ({\n      ...item,\n      editing: false,\n      name: {\n        ...item.name,\n        viewValue: bufferToString(item.name),\n      },\n      greatestPendingId: {\n        ...item.greatestPendingId,\n        viewValue: bufferToString(item.greatestPendingId),\n      },\n      smallestPendingId: {\n        ...item.smallestPendingId,\n        viewValue: bufferToString(item.smallestPendingId),\n      },\n    }),\n    [],\n  )\n\n  const closePopover = () => {\n    setDeleting('')\n  }\n\n  const showPopover = (groupName = '') => {\n    setDeleting(`${groupName + suffix}`)\n  }\n\n  const onSuccessDeletedGroup = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_GROUP_DELETED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    closePopover()\n  }\n\n  const handleDeleteGroup = (name: RedisResponseBuffer) => {\n    dispatch(\n      deleteConsumerGroupsAction(selectedKey, [name], onSuccessDeletedGroup),\n    )\n  }\n\n  const handleRemoveIconClick = () => {\n    // sendEventTelemetry({\n    //   event: getBasedOnViewTypeEvent(\n    //     viewType,\n    //     TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n    //     TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED\n    //   ),\n    //   eventData: {\n    //     databaseId: instanceId,\n    //     keyType: KeyTypes.Stream\n    //   }\n    // })\n  }\n\n  const onSuccessSelectedGroup = (data: ConsumerDto[]) => {\n    dispatch(setStreamViewType(StreamViewType.Consumers))\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMERS_LOADED,\n      eventData: {\n        databaseId: instanceId,\n        length: data.length,\n      },\n    })\n  }\n\n  const onSuccessApplyEditId = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_GROUP_ID_SET,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const handleSelectGroup = ({ rowData }: { rowData: any }) => {\n    dispatch(setSelectedGroup(rowData))\n\n    if (!isTruncatedString(rowData?.name)) {\n      dispatch(fetchConsumers(false, onSuccessSelectedGroup))\n    } else {\n      onSuccessSelectedGroup([])\n    }\n  }\n\n  const handleApplyEditId = (groupName: RedisResponseBuffer) => {\n    if (!!groupName?.data?.length && !idError && selectedKey) {\n      const data: UpdateConsumerGroupDto = {\n        keyName: selectedKey,\n        name: groupName,\n        lastDeliveredId: editValue,\n      }\n      dispatch(modifyLastDeliveredIdAction(data, onSuccessApplyEditId))\n    }\n  }\n\n  const handleEditId = (name: RedisResponseBuffer, lastDeliveredId: string) => {\n    const newGroupsState: IConsumerGroup[] = groups?.map((item) =>\n      isEqualBuffers(item.name, name) ? { ...item, editing: true } : item,\n    )\n\n    setGroups(newGroupsState)\n    setEditValue(lastDeliveredId)\n    dispatch(setSelectedKeyRefreshDisabled(true))\n  }\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'name',\n      label: 'Group Name',\n      truncateText: true,\n      isSortable: true,\n      minWidth: 100,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: function Name(_name: string, { name }: IConsumerGroup) {\n        const viewName = name?.viewValue ?? ''\n        // Better to cut the long string, because it could affect virtual scroll performance\n        const cellContent = viewName.substring(0, 200)\n        const tooltipContent = formatLongName(viewName)\n        return (\n          <Text component=\"div\" style={{ maxWidth: '100%' }} color=\"secondary\">\n            <div\n              style={{ display: 'flex' }}\n              className=\"truncateText\"\n              data-testid={`stream-group-name-${viewName}`}\n            >\n              <RiTooltip\n                className={styles.tooltipName}\n                anchorClassName=\"truncateText\"\n                position=\"bottom\"\n                content={tooltipContent}\n              >\n                <>{cellContent}</>\n              </RiTooltip>\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'consumers',\n      label: 'Consumers',\n      minWidth: 120,\n      maxWidth: 120,\n      absoluteWidth: 120,\n      truncateText: true,\n      isSortable: true,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: function Name(_name: string, { consumers }: IConsumerGroup) {\n        return <Text color=\"secondary\">{consumers}</Text>\n      },\n    },\n    {\n      id: 'pending',\n      label: 'Pending',\n      minWidth: 95,\n      maxWidth: 95,\n      absoluteWidth: 95,\n      isSortable: true,\n      className: styles.cell,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: function P(\n        _name: string,\n        { pending, greatestPendingId, smallestPendingId, name }: IConsumerGroup,\n      ) {\n        const viewName = name?.viewValue ?? ''\n        const smallestTimestamp = smallestPendingId?.viewValue?.split('-')?.[0]\n        const greatestTimestamp = greatestPendingId?.viewValue?.split('-')?.[0]\n\n        const tooltipContent = (\n          <>\n            <FormatedDate date={smallestTimestamp} />\n            <span>&nbsp;–&nbsp;</span>\n            <FormatedDate date={greatestTimestamp} />\n          </>\n        )\n\n        return (\n          <Text component=\"div\" style={{ maxWidth: '100%' }} color=\"secondary\">\n            <div\n              style={{ display: 'flex' }}\n              className=\"truncateText\"\n              data-testid={`group-pending-${viewName}`}\n            >\n              {!!pending && (\n                <RiTooltip\n                  title={`${pending} Pending Messages`}\n                  className={styles.tooltip}\n                  anchorClassName=\"truncateText\"\n                  position=\"bottom\"\n                  content={tooltipContent}\n                >\n                  <>{pending}</>\n                </RiTooltip>\n              )}\n              {!pending && pending}\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'lastDeliveredId',\n      label: 'Last Delivered ID',\n      minWidth: 200,\n      maxWidth: 200,\n      absoluteWidth: 200,\n      isSortable: true,\n      className: cx(styles.cell, 'noPadding'),\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: function Id(\n        _name: string,\n        { lastDeliveredId: id, name, editing }: IConsumerGroup,\n      ) {\n        const timestamp = id?.split('-')?.[0]\n        const showIdError = !isIdFocused && idError\n        const isTruncatedGroupName = isTruncatedString(name)\n\n        return (\n          <EditablePopover\n            content={\n              <div className={styles.editableCell}>\n                <Text\n                  color=\"secondary\"\n                  size=\"s\"\n                  style={{ maxWidth: '100%' }}\n                  component=\"div\"\n                >\n                  <div\n                    className=\"truncateText streamItem\"\n                    style={{ display: 'flex', maxWidth: '190px' }}\n                    data-testid={`stream-group-date-${id}`}\n                  >\n                    <FormatedDate date={timestamp} />\n                  </div>\n                </Text>\n                <Text size=\"s\" style={{ maxWidth: '100%' }} component=\"div\">\n                  <div\n                    className=\"streamItemId\"\n                    data-testid={`stream-group-id-${id}`}\n                  >\n                    {id}\n                  </div>\n                </Text>\n              </div>\n            }\n            field={id}\n            prefix=\"stream-group\"\n            isOpen={editing}\n            onOpen={() => handleEditId(name, id)}\n            onDecline={() => dispatch(setSelectedKeyRefreshDisabled(false))}\n            onApply={() => {\n              handleApplyEditId(name)\n              dispatch(setSelectedKeyRefreshDisabled(false))\n            }}\n            className={styles.editLastId}\n            isDisabled={!editValue.length || !!idError}\n            isDisabledEditButton={isTruncatedGroupName}\n            isLoading={loading}\n            delay={500}\n            editBtnClassName={styles.editBtn}\n          >\n            <FormField>\n              <ComposedInput\n                name=\"id\"\n                id=\"id\"\n                placeholder=\"ID*\"\n                value={editValue}\n                onChange={(value) =>\n                  setEditValue(validateConsumerGroupId(value))\n                }\n                onBlur={() => setIsIdFocused(false)}\n                onFocus={() => setIsIdFocused(true)}\n                style={{ width: 240 }}\n                autoComplete=\"off\"\n                data-testid=\"last-id-field\"\n                after={\n                  <RiTooltip\n                    anchorClassName=\"inputAppendIcon\"\n                    position=\"left\"\n                    title=\"Enter Valid ID, 0 or $\"\n                    content={lastDeliveredIDTooltipText}\n                  >\n                    <RiIcon type=\"InfoIcon\" style={{ cursor: 'pointer' }} />\n                  </RiTooltip>\n                }\n              />\n              {!showIdError && (\n                <span className={styles.idText} data-testid=\"id-help-text\">\n                  Timestamp - Sequence Number or $\n                </span>\n              )}\n              {showIdError && (\n                <span className={styles.error} data-testid=\"id-error\">\n                  {idError}\n                </span>\n              )}\n            </FormField>\n          </EditablePopover>\n        )\n      },\n    },\n    {\n      id: 'actions',\n      label: '',\n      headerClassName: styles.actionsHeader,\n      textAlignment: TableCellTextAlignment.Left,\n      absoluteWidth: actionsWidth,\n      maxWidth: actionsWidth,\n      minWidth: actionsWidth,\n      render: function Actions(_act: any, { name }: IConsumerGroup) {\n        const viewName = name?.viewValue ?? ''\n        return (\n          <div>\n            <PopoverDelete\n              header={viewName}\n              text={\n                <>\n                  and all its consumers will be removed from{' '}\n                  <b>{selectedKeyString}</b>\n                </>\n              }\n              item={viewName}\n              suffix={suffix}\n              deleting={deleting}\n              closePopover={closePopover}\n              updateLoading={false}\n              showPopover={showPopover}\n              testid={`remove-groups-button-${viewName}`}\n              handleDeleteItem={() => handleDeleteGroup(name)}\n              handleButtonClick={handleRemoveIconClick}\n            />\n          </div>\n        )\n      },\n    },\n  ]\n\n  return (\n    <>\n      <GroupsView\n        data={groups}\n        columns={columns}\n        onClosePopover={closePopover}\n        onSelectGroup={handleSelectGroup}\n        {...props}\n      />\n    </>\n  )\n}\n\nexport default GroupsViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/groups-view/index.ts",
    "content": "import GroupsViewWrapper from './GroupsViewWrapper'\n\nexport default GroupsViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/index.ts",
    "content": "export { StreamDetails } from './StreamDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageAckPopover/MessageAckPopover.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport MessageAckPopover, { Props } from './MessageAckPopover'\n\nconst mockedProps = mock<Props>()\n\ndescribe('MessageAckPopover', () => {\n  it('should render', () => {\n    expect(\n      render(<MessageAckPopover {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageAckPopover/MessageAckPopover.tsx",
    "content": "import React from 'react'\n\nimport {\n  DestructiveButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { HorizontalSpacer } from 'uiSrc/components/base/layout'\nimport ConfirmationPopover from 'uiSrc/components/confirmation-popover'\n\nexport interface Props {\n  id: string\n  isOpen: boolean\n  closePopover: () => void\n  showPopover: () => void\n  acknowledge: (entry: string) => void\n}\n\nconst AckPopover = (props: Props) => {\n  const {\n    id,\n    isOpen,\n    closePopover = () => {},\n    showPopover = () => {},\n    acknowledge = () => {},\n  } = props\n  return (\n    <ConfirmationPopover\n      key={id}\n      title={id}\n      message=\"will be acknowledged and removed from the pending messages list\"\n      anchorPosition=\"leftCenter\"\n      ownFocus\n      isOpen={isOpen}\n      closePopover={closePopover}\n      panelPaddingSize=\"m\"\n      anchorClassName=\"ackMessagePopover\"\n      confirmButton={\n        <DestructiveButton\n          size=\"s\"\n          onClick={() => acknowledge(id)}\n          data-testid=\"acknowledge-submit\"\n        >\n          Acknowledge\n        </DestructiveButton>\n      }\n      button={\n        <>\n          <SecondaryButton\n            size=\"s\"\n            aria-label=\"Acknowledge pending message\"\n            onClick={showPopover}\n            data-testid=\"acknowledge-btn\"\n          >\n            ACK\n          </SecondaryButton>\n          <HorizontalSpacer size=\"s\" />\n        </>\n      }\n    />\n  )\n}\n\nexport default AckPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageAckPopover/index.tsx",
    "content": "import MessageAckPopover from './MessageAckPopover'\n\nexport default MessageAckPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageClaimPopover/MessageClaimPopover.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  selectedConsumerSelector,\n  selectedGroupSelector,\n} from 'uiSrc/slices/browser/stream'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport MessageClaimPopover, { Props } from './MessageClaimPopover'\n\nconst mockConsumers = [\n  { name: stringToBuffer('Consumer 1') },\n  { name: stringToBuffer('Consumer 2') },\n  { name: MOCK_TRUNCATED_BUFFER_VALUE },\n]\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  selectedGroupSelector: jest.fn(),\n  selectedConsumerSelector: jest.fn(),\n}))\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(selectedGroupSelector as jest.Mock).mockReturnValue({\n    data: [mockConsumers[0], mockConsumers[1]],\n  })\n  ;(selectedConsumerSelector as jest.Mock).mockReturnValue(mockConsumers[0])\n})\n\ndescribe('MessageClaimPopover', () => {\n  it('should render', () => {\n    expect(\n      render(<MessageClaimPopover {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n  it('should not disable button when there are consumers to claim', async () => {\n    render(<MessageClaimPopover {...instance(mockedProps)} />)\n\n    const [claimButton] = screen.getAllByTestId(/claim-pending-message$/)\n    expect(claimButton).toBeEnabled()\n  })\n  it('should disable button when there are no other consumers to claim', () => {\n    ;(selectedGroupSelector as jest.Mock).mockReturnValueOnce({\n      data: [mockConsumers[0]],\n    })\n\n    render(<MessageClaimPopover {...instance(mockedProps)} />)\n\n    const [claimButton] = screen.getAllByTestId(/claim-pending-message$/)\n    expect(claimButton).toBeDisabled()\n  })\n  it('should disable button when there are truncated consumers only', () => {\n    ;(selectedGroupSelector as jest.Mock).mockReturnValueOnce({\n      data: [mockConsumers[0], mockConsumers[2]],\n    })\n\n    render(<MessageClaimPopover {...instance(mockedProps)} />)\n\n    const [claimButton] = screen.getAllByTestId(/claim-pending-message$/)\n    expect(claimButton).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageClaimPopover/MessageClaimPopover.tsx",
    "content": "import React, { useState, useEffect, ChangeEvent } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { useFormik } from 'formik'\nimport { orderBy, filter } from 'lodash'\n\nimport { isTruncatedString, isEqualBuffers } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  selectedGroupSelector,\n  selectedConsumerSelector,\n} from 'uiSrc/slices/browser/stream'\nimport {\n  prepareDataForClaimRequest,\n  getDefaultConsumer,\n  ClaimTimeOptions,\n} from 'uiSrc/utils/streamUtils'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { NumericInput, SwitchInput } from 'uiSrc/components/base/inputs'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport {\n  ClaimPendingEntryDto,\n  ClaimPendingEntriesResponse,\n  ConsumerDto,\n} from 'apiSrc/modules/browser/stream/dto'\n\nimport styles from './styles.module.scss'\n\nconst getConsumersOptions = (consumers: ConsumerDto[]) =>\n  consumers.map((consumer) => ({\n    value: consumer.name?.viewValue,\n    inputDisplay: (\n      <Text\n        size=\"m\"\n        className={styles.option}\n        data-testid=\"consumer-option\"\n        component=\"div\"\n      >\n        <Text className={styles.consumerName}>{consumer.name?.viewValue}</Text>\n        <Text\n          size=\"s\"\n          className={styles.pendingCount}\n          data-testid=\"pending-count\"\n        >\n          {`pending: ${consumer.pending}`}\n        </Text>\n      </Text>\n    ),\n  }))\n\nconst timeOptions = [\n  { value: ClaimTimeOptions.RELATIVE, label: 'Relative Time' },\n  { value: ClaimTimeOptions.ABSOLUTE, label: 'Timestamp' },\n]\n\nexport interface Props {\n  id: string\n  isOpen: boolean\n  closePopover: () => void\n  showPopover: () => void\n  claimMessage: (\n    data: Partial<ClaimPendingEntryDto>,\n    successAction: (data: ClaimPendingEntriesResponse) => void,\n  ) => void\n  handleCancelClaim: () => void\n}\n\nconst MessageClaimPopover = (props: Props) => {\n  const {\n    id,\n    isOpen,\n    closePopover,\n    showPopover,\n    claimMessage,\n    handleCancelClaim,\n  } = props\n\n  const { data: consumers = [] } = useSelector(selectedGroupSelector) ?? {}\n  const { name: currentConsumerName, pending = 0 } = useSelector(\n    selectedConsumerSelector,\n  ) ?? { name: '' }\n\n  const [isOptionalShow, setIsOptionalShow] = useState<boolean>(false)\n  const [consumerOptions, setConsumerOptions] = useState<any[]>([])\n  const [initialValues, setInitialValues] = useState({\n    consumerName: '',\n    minIdleTime: '0',\n    timeCount: '0',\n    timeOption: ClaimTimeOptions.RELATIVE,\n    retryCount: '0',\n    force: false,\n  })\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const formik = useFormik({\n    initialValues,\n    enableReinitialize: true,\n    validateOnBlur: false,\n    onSubmit: (values) => {\n      const data = prepareDataForClaimRequest(values, [id], isOptionalShow)\n      claimMessage(data, onSuccessSubmit)\n    },\n  })\n\n  const onSuccessSubmit = (data: ClaimPendingEntriesResponse) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_MESSAGE_CLAIMED,\n      eventData: {\n        databaseId: instanceId,\n        pending: pending - data.affected.length,\n      },\n    })\n    setIsOptionalShow(false)\n    formik.resetForm()\n    closePopover()\n  }\n\n  const handleClosePopover = () => {\n    closePopover()\n    setIsOptionalShow(false)\n    formik.resetForm()\n  }\n\n  const handleChangeTimeFormat = (value: ClaimTimeOptions) => {\n    formik.setFieldValue('timeOption', value)\n    if (value === ClaimTimeOptions.ABSOLUTE) {\n      formik.setFieldValue('timeCount', new Date().getTime())\n    } else {\n      formik.setFieldValue('timeCount', '0')\n    }\n  }\n\n  const handleCancel = () => {\n    handleCancelClaim()\n    handleClosePopover()\n  }\n\n  useEffect(() => {\n    const consumersWithoutTruncatedNames = filter(\n      consumers,\n      ({ name }) => !isTruncatedString(name),\n    )\n    const consumersWithoutCurrent = filter(\n      consumersWithoutTruncatedNames,\n      (consumer) => !isEqualBuffers(consumer.name, currentConsumerName),\n    )\n    const sortedConsumers = orderBy(\n      getConsumersOptions(consumersWithoutCurrent),\n      ['name.viewValue'],\n      ['asc'],\n    )\n    if (sortedConsumers.length) {\n      setConsumerOptions(sortedConsumers)\n      setInitialValues({\n        ...initialValues,\n        consumerName: getDefaultConsumer(consumersWithoutCurrent)?.name\n          ?.viewValue,\n      })\n    }\n  }, [consumers, currentConsumerName])\n\n  const button = (\n    <SecondaryButton\n      size=\"s\"\n      aria-label=\"Claim pending message\"\n      onClick={showPopover}\n      data-testid=\"claim-pending-message\"\n      className={styles.claimBtn}\n      disabled={consumerOptions.length < 1}\n    >\n      CLAIM\n    </SecondaryButton>\n  )\n\n  const buttonTooltip = (\n    <RiTooltip\n      content=\"There is no consumer to claim the message.\"\n      position=\"top\"\n      anchorClassName=\"flex-row\"\n      data-testid=\"claim-pending-message-tooltip\"\n    >\n      {button}\n    </RiTooltip>\n  )\n\n  return (\n    <RiPopover\n      key={id}\n      onWheel={(e) => e.stopPropagation()}\n      anchorPosition=\"leftCenter\"\n      ownFocus\n      isOpen={isOpen}\n      panelPaddingSize=\"m\"\n      anchorClassName=\"claimPendingMessage\"\n      panelClassName={styles.popoverWrapper}\n      closePopover={() => {}}\n      button={consumerOptions.length < 1 ? buttonTooltip : button}\n    >\n      <form>\n        <Row responsive gap=\"m\">\n          <FlexItem>\n            <FormField label=\"Consumer\">\n              <RiSelect\n                value={formik.values.consumerName}\n                options={consumerOptions}\n                valueRender={({ option }) => option.inputDisplay as JSX.Element}\n                className={styles.consumerField}\n                name=\"consumerName\"\n                onChange={(value) =>\n                  formik.setFieldValue('consumerName', value)\n                }\n                data-testid=\"destination-select\"\n              />\n            </FormField>\n          </FlexItem>\n          <FlexItem grow>\n            <FormField label=\"Min Idle Time\">\n              <div className={styles.timeWrapper}>\n                <NumericInput\n                  autoValidate\n                  min={0}\n                  name=\"minIdleTime\"\n                  id=\"minIdleTime\"\n                  data-testid=\"min-idle-time\"\n                  placeholder=\"0\"\n                  className={styles.fieldWithAppend}\n                  value={Number(formik.values.minIdleTime)}\n                  onChange={(value) =>\n                    formik.setFieldValue('minIdleTime', value)\n                  }\n                />\n                <div className={styles.timeUnit}>msec</div>\n              </div>\n            </FormField>\n          </FlexItem>\n        </Row>\n        {isOptionalShow && (\n          <>\n            <Spacer size=\"xl\" />\n            <Row align=\"center\" justify=\"between\" gap=\"m\">\n              <FlexItem grow>\n                <FormField label=\"Idle Time\">\n                  <div className={styles.timeWrapper}>\n                    <NumericInput\n                      autoValidate\n                      min={0}\n                      name=\"timeCount\"\n                      id=\"timeCount\"\n                      data-testid=\"time-count\"\n                      placeholder=\"0\"\n                      className={styles.fieldWithAppend}\n                      value={Number(formik.values.timeCount)}\n                      onChange={(value) =>\n                        formik.setFieldValue('timeCount', value)\n                      }\n                    />\n                    <div className={styles.timeUnit}>msec</div>\n                  </div>\n                </FormField>\n              </FlexItem>\n              <FlexItem className={styles.timeSelect}>\n                <FormField label=\"Time\">\n                  <RiSelect\n                    value={formik.values.timeOption}\n                    options={timeOptions}\n                    className={styles.timeOptionField}\n                    name=\"consumerName\"\n                    onChange={handleChangeTimeFormat}\n                    data-testid=\"time-option-select\"\n                  />\n                </FormField>\n              </FlexItem>\n              <FlexItem>\n                <FormField label=\"Retry Count\">\n                  <NumericInput\n                    autoValidate\n                    min={0}\n                    name=\"retryCount\"\n                    id=\"retryCount\"\n                    data-testid=\"retry-count\"\n                    placeholder=\"0\"\n                    className={styles.retryCountField}\n                    value={Number(formik.values.retryCount)}\n                    onChange={(value) =>\n                      formik.setFieldValue('retryCount', value)\n                    }\n                  />\n                </FormField>\n              </FlexItem>\n              <FlexItem grow={2}>\n                <FormField className={styles.hiddenLabel} label=\"Force\">\n                  <Checkbox\n                    id=\"force_claim\"\n                    name=\"force\"\n                    label=\"Force Claim\"\n                    checked={formik.values.force}\n                    onChange={(e: ChangeEvent<HTMLInputElement>) => {\n                      formik.setFieldValue(e.target.name, !formik.values.force)\n                    }}\n                    data-testid=\"force-claim-checkbox\"\n                  />\n                </FormField>\n              </FlexItem>\n            </Row>\n          </>\n        )}\n        <Spacer size=\"xl\" />\n        <Row responsive justify=\"between\" align=\"center\">\n          <FlexItem>\n            <SwitchInput\n              title=\"Optional Parameters\"\n              checked={isOptionalShow}\n              onCheckedChange={setIsOptionalShow}\n              data-testid=\"optional-parameters-switcher\"\n            />\n          </FlexItem>\n          <Row grow={false} gap=\"m\">\n            <SecondaryButton onClick={handleCancel}>Cancel</SecondaryButton>\n            <PrimaryButton\n              onClick={() => formik.handleSubmit()}\n              data-testid=\"btn-submit\"\n            >\n              Claim\n            </PrimaryButton>\n          </Row>\n        </Row>\n      </form>\n    </RiPopover>\n  )\n}\n\nexport default MessageClaimPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageClaimPopover/index.tsx",
    "content": "import MessageClaimPopover from './MessageClaimPopover'\n\nexport default MessageClaimPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessageClaimPopover/styles.module.scss",
    "content": ".option {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n  height: 36px;\n}\n\n.option .pendingCount {\n  font: normal normal normal 13px/24px Graphik, sans-serif;\n  letter-spacing: -0.13px;\n  color: var(--inputPlaceholderColor) !important;\n  white-space: nowrap;\n}\n\n.consumerOption :global(.euiContextMenu__itemLayout) .pendingCount {\n  padding-right: 13px;\n}\n\n.consumerOption :global(.euiContextMenu__itemLayout) {\n  margin-right: 20px;\n  height: 20px;\n}\n\n.consumerField {\n  width: 389px !important;\n  height: 36px !important;\n}\n\n.fieldWithAppend {\n  width: 162px !important;\n  height: 36px !important;\n  padding-right: 40px !important;\n}\n\n.retryCountField {\n  width: 88px;\n  height: 36px !important;\n}\n\n.consumerName {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.timeWrapper {\n  position: relative;\n\n  .timeUnit {\n    position: absolute;\n    top: 12px;\n    right: 5px;\n    font-size: 12px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessagesView/MessagesView.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { PendingEntryDto } from 'apiSrc/modules/browser/stream/dto'\nimport MessagesView, { Props } from './MessagesView'\n\nconst mockedProps = mock<Props>()\nconst mockMessages: PendingEntryDto[] = [\n  {\n    id: '123',\n    consumerName: 'test',\n    idle: 321,\n    delivered: 321,\n  },\n  {\n    id: '1234',\n    consumerName: 'test2',\n    idle: 3213,\n    delivered: 1321,\n  },\n]\n\ndescribe('MessagesView', () => {\n  it('should render', () => {\n    expect(\n      render(<MessagesView {...instance(mockedProps)} data={mockMessages} />),\n    ).toBeTruthy()\n  })\n\n  it('should show custom \"empty message\" when defined', () => {\n    render(\n      <MessagesView\n        {...instance(mockedProps)}\n        data={[]}\n        noItemsMessageString=\"custom message\"\n      />,\n    )\n\n    expect(screen.getByTestId('stream-messages-container')).toHaveTextContent(\n      'custom message',\n    )\n  })\n\n  it('should show default \"empty message\" when not defined', () => {\n    render(<MessagesView {...instance(mockedProps)} data={[]} />)\n\n    expect(screen.getByTestId('stream-messages-container')).toHaveTextContent(\n      'Your Consumer has no pending messages.',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessagesView/MessagesView.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport cx from 'classnames'\n\nimport { streamGroupsSelector } from 'uiSrc/slices/browser/stream'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport { PendingEntryDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport styles from './styles.module.scss'\n\nconst headerHeight = 60\nconst rowHeight = 54\n\nexport interface Props {\n  data: PendingEntryDto[]\n  columns: ITableColumn[]\n  total: number\n  onClosePopover: () => void\n  loadMoreItems: () => void\n  noItemsMessageString?: string\n}\n\nconst MessagesView = (props: Props) => {\n  const {\n    data = [],\n    columns = [],\n    total,\n    onClosePopover,\n    loadMoreItems,\n    noItemsMessageString = 'Your Consumer has no pending messages.',\n  } = props\n\n  const { loading } = useSelector(streamGroupsSelector)\n  const { name: key = '' } = useSelector(selectedKeyDataSelector) ?? {}\n\n  return (\n    <>\n      <div\n        className={cx(\n          'key-details-table',\n          'stream-details-table',\n          styles.container,\n        )}\n        data-testid=\"stream-messages-container\"\n      >\n        <VirtualTable\n          autoHeight\n          hideProgress\n          selectable={false}\n          keyName={key}\n          totalItemsCount={total}\n          headerHeight={data?.length ? headerHeight : 0}\n          rowHeight={rowHeight}\n          columns={columns}\n          footerHeight={0}\n          loading={loading}\n          items={data}\n          tableWidth={columns.reduce((a, b) => a + (b.minWidth ?? 0), 0)}\n          onWheel={onClosePopover}\n          loadMoreItems={loadMoreItems}\n          noItemsMessage={noItemsMessageString}\n        />\n      </div>\n    </>\n  )\n}\n\nexport default MessagesView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessagesView/index.ts",
    "content": "import MessagesView from './MessagesView'\n\nexport * from './MessagesView'\n\nexport default MessagesView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessagesView/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding-top: 3px;\n  background-color: var(--euiColorEmptyShade);\n}\n\n.actions,\n.actionsHeader {\n  width: 54px;\n}\n\n.actionCell {\n  width: 100%;\n  display: flex;\n  justify-content: flex-end;\n}\n\n.deliveredHeaderCell {\n  min-width: 200px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessagesViewWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  loadConsumerGroups,\n  selectedConsumerSelector,\n  setSelectedGroup,\n} from 'uiSrc/slices/browser/stream'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport {\n  MOCK_TRUNCATED_BUFFER_VALUE,\n  MOCK_TRUNCATED_STRING_VALUE,\n} from 'uiSrc/mocks/data/bigString'\nimport { TEXT_CONSUMER_NAME_TOO_LONG } from 'uiSrc/constants'\nimport { PendingEntryDto } from 'apiSrc/modules/browser/stream/dto'\nimport MessagesView, { Props as MessagesViewProps } from './MessagesView'\nimport MessagesViewWrapper, { Props } from './MessagesViewWrapper'\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  selectedConsumerSelector: jest.fn(\n    () =>\n      jest.requireActual('uiSrc/slices/browser/stream')\n        .selectedConsumerSelector,\n  ),\n}))\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('./MessagesView', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockMessages: PendingEntryDto[] = [\n  {\n    id: '123',\n    consumerName: 'test',\n    idle: 321,\n    delivered: 321,\n  },\n  {\n    id: '1234',\n    consumerName: 'test2',\n    idle: 3213,\n    delivered: 1321,\n  },\n]\n\nconst mockMessagesView = jest.fn((props: MessagesViewProps) => (\n  <div data-testid=\"stream-messages-container\">\n    <VirtualTable\n      items={mockMessages}\n      loading={false}\n      columns={props.columns}\n    />\n  </div>\n))\n\ndescribe('MessagesViewWrapper', () => {\n  beforeAll(() => {\n    MessagesView.mockImplementation(mockMessagesView)\n  })\n\n  it('should render', () => {\n    expect(\n      render(<MessagesViewWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  // it('should render Messages container', () => {\n  //   render(<MessagesViewWrapper {...instance(mockedProps)} />)\n\n  //   expect(screen.getByTestId('stream-messages-container')).toBeInTheDocument()\n  // })\n\n  it.skip('should claim Message', () => {\n    render(<MessagesViewWrapper {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('claim-message-btn'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedGroup(),\n      loadConsumerGroups(false),\n    ])\n  })\n\n  describe('truncated data', () => {\n    it('should pass special noItemsMessageString message for truncated group name', () => {\n      ;(selectedConsumerSelector as jest.Mock).mockImplementationOnce(() => ({\n        name: MOCK_TRUNCATED_BUFFER_VALUE,\n        nameString: MOCK_TRUNCATED_STRING_VALUE,\n        pending: 0,\n        idle: 0,\n        data: [],\n        lastRefreshTime: null,\n      }))\n\n      render(<MessagesViewWrapper {...instance(mockedProps)} />)\n\n      expect(mockMessagesView).toHaveBeenCalledWith(\n        expect.objectContaining({\n          noItemsMessageString: TEXT_CONSUMER_NAME_TOO_LONG,\n          data: [],\n        }),\n        expect.anything(),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/MessagesViewWrapper.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { last, toNumber } from 'lodash'\nimport cx from 'classnames'\n\nimport {\n  fetchMoreConsumerMessages,\n  selectedConsumerSelector,\n  selectedGroupSelector,\n  ackPendingEntriesAction,\n  claimPendingMessages,\n} from 'uiSrc/slices/browser/stream'\nimport {\n  selectedKeyDataSelector,\n  updateSelectedKeyRefreshTime,\n} from 'uiSrc/slices/browser/keys'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport { getFormatTime, getNextId } from 'uiSrc/utils/streamUtils'\nimport { SortOrder, TEXT_CONSUMER_NAME_TOO_LONG } from 'uiSrc/constants'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { isTruncatedString } from 'uiSrc/utils'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport {\n  AckPendingEntriesResponse,\n  PendingEntryDto,\n  ClaimPendingEntryDto,\n  ClaimPendingEntriesResponse,\n} from 'apiSrc/modules/browser/stream/dto'\n\nimport MessagesView from './MessagesView'\nimport MessageClaimPopover from './MessageClaimPopover'\nimport MessageAckPopover from './MessageAckPopover'\n\nimport styles from './MessagesView/styles.module.scss'\n\nconst actionsWidth = 150\nconst minColumnWidth = 195\nconst claimPrefix = '-claim'\nconst ackPrefix = '-ack'\n\nexport interface Props {}\n\nconst MessagesViewWrapper = (props: Props) => {\n  const {\n    lastRefreshTime,\n    data: loadedMessages = [],\n    name: consumerName,\n    pending = 0,\n  } = useSelector(selectedConsumerSelector) ?? {}\n  const isTruncatedConsumerName = isTruncatedString(consumerName)\n  const { name: group } = useSelector(selectedGroupSelector) ?? { name: '' }\n  const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' }\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const [openPopover, setOpenPopover] = useState<string>('')\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(updateSelectedKeyRefreshTime(lastRefreshTime))\n  }, [])\n\n  const showAchPopover = (id: string) => {\n    setOpenPopover(id + ackPrefix)\n  }\n\n  const closePopover = () => {\n    setOpenPopover('')\n  }\n\n  const handleCancelClaim = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_MESSAGE_CLAIM_CANCELED,\n      eventData: {\n        databaseId: instanceId,\n        pending,\n      },\n    })\n  }\n\n  const showClaimPopover = (id: string) => {\n    setOpenPopover(id + claimPrefix)\n  }\n\n  const onSuccessAck = (data: AckPendingEntriesResponse) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_MESSAGE_ACKNOWLEDGED,\n      eventData: {\n        databaseId: instanceId,\n        pending: pending - data.affected,\n      },\n    })\n    setOpenPopover('')\n  }\n\n  const handleAchPendingMessage = (entry: string) => {\n    dispatch(ackPendingEntriesAction(key, group, [entry], onSuccessAck))\n  }\n\n  const handleClaimingId = (\n    data: Partial<ClaimPendingEntryDto>,\n    onSuccess: (data: ClaimPendingEntriesResponse) => void,\n  ) => {\n    dispatch(claimPendingMessages(data, onSuccess))\n  }\n  const loadMoreItems = useCallback(() => {\n    const lastLoadedEntryId = last(loadedMessages)?.id ?? '-'\n    const nextId = `(${getNextId(lastLoadedEntryId, SortOrder.ASC)}`\n\n    dispatch(fetchMoreConsumerMessages(SCAN_COUNT_DEFAULT, nextId))\n  }, [loadedMessages])\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'id',\n      label: 'Entry ID',\n      absoluteWidth: minColumnWidth,\n      minWidth: minColumnWidth,\n      className: styles.cell,\n      headerClassName: 'streamItemHeader',\n      render: function Id(_name: string, { id }: PendingEntryDto) {\n        const timestamp = id?.split('-')?.[0]\n        return (\n          <FlexItem>\n            <Text\n              color=\"secondary\"\n              size=\"s\"\n              style={{ maxWidth: '100%' }}\n              className=\"truncateText streamItem\"\n              data-testid={`stream-message-${id}-date`}\n            >\n              {getFormatTime(timestamp)}\n            </Text>\n            <Text\n              size=\"s\"\n              className=\"streamItemId\"\n              data-testid={`stream-message-${id}`}\n              style={{ maxWidth: '100%' }}\n            >\n              {id}\n            </Text>\n          </FlexItem>\n        )\n      },\n    },\n    {\n      id: 'idle',\n      label: 'Last Message Delivered',\n      minWidth: 256,\n      absoluteWidth: 106,\n      truncateText: true,\n      headerClassName: 'streamItemHeader',\n      headerCellClassName: 'truncateText',\n      render: function Idle(_name: string, { id, idle }: PendingEntryDto) {\n        const timestamp = id?.split('-')?.[0]\n        return (\n          <Text\n            className=\"truncateText streamItem\"\n            color=\"secondary\"\n            size=\"s\"\n            data-testid={`stream-message-${id}-idle`}\n            style={{ maxWidth: '100%' }}\n          >\n            {getFormatTime(`${toNumber(timestamp) + idle}`)}\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'delivered',\n      label: 'Times Message Delivered',\n      minWidth: 106,\n      truncateText: true,\n      headerClassName: cx('streamItemHeader', styles.deliveredHeaderCell),\n      headerCellClassName: 'truncateText',\n      render: (cellData: number) => <Text color=\"secondary\">{cellData}</Text>,\n    },\n    {\n      id: 'actions',\n      label: '',\n      headerClassName: 'streamItemHeader',\n      className: styles.actionCell,\n      minWidth: actionsWidth,\n      absoluteWidth: actionsWidth,\n      render: function Actions(_act: any, { id }: PendingEntryDto) {\n        return (\n          <FlexItem direction=\"row\">\n            <MessageAckPopover\n              id={id}\n              isOpen={openPopover === id + ackPrefix}\n              closePopover={() => closePopover()}\n              showPopover={() => showAchPopover(id)}\n              acknowledge={handleAchPendingMessage}\n            />\n            <MessageClaimPopover\n              id={id}\n              isOpen={openPopover === id + claimPrefix}\n              closePopover={() => closePopover()}\n              showPopover={() => showClaimPopover(id)}\n              claimMessage={handleClaimingId}\n              handleCancelClaim={handleCancelClaim}\n            />\n          </FlexItem>\n        )\n      },\n    },\n  ]\n\n  return (\n    <>\n      <MessagesView\n        data={loadedMessages}\n        total={pending}\n        columns={columns}\n        onClosePopover={closePopover}\n        loadMoreItems={loadMoreItems}\n        {...props}\n        noItemsMessageString={\n          isTruncatedConsumerName ? TEXT_CONSUMER_NAME_TOO_LONG : undefined\n        }\n      />\n    </>\n  )\n}\n\nexport default MessagesViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/messages-view/index.ts",
    "content": "import MessagesViewWrapper from './MessagesViewWrapper'\n\nexport * from './MessagesViewWrapper'\n\nexport default MessagesViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/StreamDataView/StreamDataView.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport StreamDataView, { Props } from './StreamDataView'\n\nconst mockedProps = mock<Props>()\n\ndescribe('StreamDataView', () => {\n  it('should render', () => {\n    expect(render(<StreamDataView {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/StreamDataView/StreamDataView.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { flatMap, isNull } from 'lodash'\nimport cx from 'classnames'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  fetchStreamEntries,\n  streamDataSelector,\n  streamSelector,\n} from 'uiSrc/slices/browser/stream'\nimport { ITableColumn } from 'uiSrc/components/virtual-grid/interfaces'\nimport {\n  keysSelector,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { KeyTypes, SortOrder } from 'uiSrc/constants'\nimport VirtualGrid from 'uiSrc/components/virtual-grid'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { StreamEntryDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport styles from './styles.module.scss'\n\nconst headerHeight = 60\nconst rowHeight = 60\nconst minColumnWidth = 190\nconst noItemsMessageInEmptyStream = 'There are no Entries in the Stream.'\nconst noItemsMessageInRange = 'No results found.'\n\nexport interface Props {\n  data: StreamEntryDto[]\n  columns: ITableColumn[]\n  onClosePopover: () => void\n  loadMoreItems: () => void\n}\n\nconst StreamDataView = (props: Props) => {\n  const {\n    data: entries = [],\n    columns = [],\n    onClosePopover,\n    loadMoreItems,\n  } = props\n  const dispatch = useDispatch()\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { viewType } = useSelector(keysSelector)\n  const { loading } = useSelector(streamSelector)\n  const { total, firstEntry, lastEntry } = useSelector(streamDataSelector)\n  const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' }\n\n  const [sortedColumnName, setSortedColumnName] = useState<string>('id')\n  const [sortedColumnOrder, setSortedColumnOrder] = useState<SortOrder>(\n    SortOrder.DESC,\n  )\n\n  const onChangeSorting = (column: any, order: SortOrder) => {\n    setSortedColumnName(column)\n    setSortedColumnOrder(order)\n\n    dispatch(fetchStreamEntries(key, SCAN_COUNT_DEFAULT, order, false))\n  }\n\n  const handleRowToggleViewClick = (expanded: boolean, rowIndex: number) => {\n    const browserViewEvent = expanded\n      ? TelemetryEvent.BROWSER_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.BROWSER_KEY_FIELD_VALUE_COLLAPSED\n    const treeViewEvent = expanded\n      ? TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED\n\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        keyType: KeyTypes.Stream,\n        databaseId: instanceId,\n        largestCellLength:\n          Math.max(\n            ...flatMap(entries[rowIndex]?.fields).map(\n              (a) => a.toString().length,\n            ),\n          ) || 0,\n      },\n    })\n  }\n\n  return (\n    <>\n      <div\n        className={cx(\n          'key-details-table',\n          'stream-details-table',\n          styles.container,\n        )}\n        data-testid=\"stream-entries-container\"\n      >\n        <VirtualGrid\n          hideProgress\n          stickLastColumnHeaderCell\n          selectable={false}\n          keyName={key}\n          headerHeight={entries?.length ? headerHeight : 0}\n          rowHeight={rowHeight}\n          columns={columns}\n          footerHeight={0}\n          loadMoreItems={loadMoreItems}\n          loading={loading}\n          items={entries}\n          totalItemsCount={total}\n          onWheel={onClosePopover}\n          onChangeSorting={onChangeSorting}\n          noItemsMessage={\n            isNull(firstEntry) && isNull(lastEntry)\n              ? noItemsMessageInEmptyStream\n              : noItemsMessageInRange\n          }\n          onRowToggleViewClick={handleRowToggleViewClick}\n          maxTableWidth={columns.reduce(\n            (a, { maxWidth = minColumnWidth }) => a + maxWidth,\n            0,\n          )}\n          sortedColumn={\n            entries?.length\n              ? {\n                  column: sortedColumnName,\n                  order: sortedColumnOrder,\n                }\n              : undefined\n          }\n        />\n      </div>\n    </>\n  )\n}\n\nexport default StreamDataView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/StreamDataView/index.ts",
    "content": "import StreamDataView from './StreamDataView'\n\nexport default StreamDataView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/StreamDataView/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  height: 100%;\n  position: relative;\n  padding-top: 3px;\n  background-color: var(--euiColorEmptyShade);\n}\n\n.actions,\n.actionsHeader {\n  width: 54px;\n}\n\n.columnManager {\n  z-index: 11;\n  position: absolute;\n  right: 18px;\n  margin-top: 20px;\n  width: 40px;\n  button {\n    width: 40px;\n    background-color: var(--euiColorEmptyShade) !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/StreamDataViewWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport StreamDataViewWrapper, { Props } from './StreamDataViewWrapper'\n\nconst mockedProps = mock<Props>()\n\ndescribe('StreamDataViewWrapper', () => {\n  it('should render', () => {\n    expect(\n      render(<StreamDataViewWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render Stream Data container', () => {\n    render(<StreamDataViewWrapper {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('stream-entries-container')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { last, mergeWith, toNumber } from 'lodash'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport {\n  bufferToString,\n  createDeleteFieldHeader,\n  createDeleteFieldMessage,\n  createTooltipContent,\n  formattingBuffer,\n  stringToBuffer,\n} from 'uiSrc/utils'\nimport {\n  streamDataSelector,\n  deleteStreamEntry,\n} from 'uiSrc/slices/browser/stream'\nimport { ITableColumn } from 'uiSrc/components/virtual-table/interfaces'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport {\n  KeyTypes,\n  TableCellTextAlignment,\n  TEXT_FAILED_CONVENT_FORMATTER,\n} from 'uiSrc/constants'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  keysSelector,\n  selectedKeySelector,\n  updateSelectedKeyRefreshTime,\n} from 'uiSrc/slices/browser/keys'\nimport { decompressingBuffer } from 'uiSrc/utils/decompressors'\n\nimport { FormattedValue } from 'uiSrc/pages/browser/modules/key-details/shared'\nimport { FormatedDate } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { StreamEntryDto } from 'apiSrc/modules/browser/stream/dto'\nimport StreamDataView from './StreamDataView'\nimport styles from './StreamDataView/styles.module.scss'\nimport {\n  MAX_FORMAT_LENGTH_STREAM_TIMESTAMP,\n  MAX_VISIBLE_LENGTH_STREAM_TIMESTAMP,\n} from '../constants'\n\nconst suffix = '_stream'\nconst actionsWidth = 50\nconst minColumnWidth = 190\n\nexport interface Props {\n  loadMoreItems: () => void\n}\n\nconst StreamDataViewWrapper = (props: Props) => {\n  const {\n    entries: loadedEntries = [],\n    keyName: key,\n    keyNameString: keyString,\n    lastRefreshTime,\n  } = useSelector(streamDataSelector)\n  const { id: instanceId, compressor = null } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewType: browserViewType } = useSelector(keysSelector)\n  const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector)\n\n  const dispatch = useDispatch()\n\n  // for Manager columns\n  // const [uniqFields, setUniqFields] = useState({})\n  const [entries, setEntries] = useState<StreamEntryDto[]>([])\n  const [columns, setColumns] = useState<ITableColumn[]>([])\n  const [deleting, setDeleting] = useState<string>('')\n  const [viewFormat, setViewFormat] = useState(viewFormatProp)\n\n  useEffect(() => {\n    dispatch(updateSelectedKeyRefreshTime(lastRefreshTime))\n  }, [])\n\n  useEffect(() => {\n    const fieldsNames: {\n      [key: string]: { index: number; name: RedisResponseBuffer }\n    } = {}\n\n    const streamEntries = loadedEntries?.map((entry) => {\n      const namesInEntry: {\n        [key: string]: { index: number; name: RedisResponseBuffer }\n      } = {}\n      const entryFields = entry.fields.map((field) => {\n        const { name } = field\n        const nameViewValue = bufferToString(name, viewFormat)\n\n        namesInEntry[nameViewValue] = {\n          index: namesInEntry[nameViewValue]\n            ? namesInEntry[nameViewValue].index + 1\n            : 1,\n          name,\n        }\n\n        return formatItem(field)\n      })\n\n      const mergeByCount = (accCount: number, newCount: number) =>\n        newCount > accCount ? newCount : accCount\n\n      mergeWith(fieldsNames, namesInEntry, mergeByCount)\n      return {\n        ...entry,\n        fields: entryFields,\n      }\n    })\n\n    const columnsNames = Object.keys(fieldsNames).reduce((acc, field) => {\n      let names = {}\n      // add index to each field name\n      const { index, name } = fieldsNames[field] || {}\n      for (let i = 0; i < index; i++) {\n        names = {\n          ...names,\n          [`${field}-${i}`]: {\n            id: field,\n            label: field,\n            render: () => {\n              const { value: decompressedName } = decompressingBuffer(\n                name,\n                compressor,\n              )\n              const buffer = decompressedName || stringToBuffer('')\n              const { value: formattedValue, isValid } = formattingBuffer(\n                buffer,\n                viewFormatProp,\n                { skipVector: true },\n              )\n              const tooltipContent = createTooltipContent(\n                formattedValue,\n                buffer,\n                viewFormatProp,\n                { skipVector: true },\n              )\n\n              return (\n                <>\n                  {formattedValue ? (\n                    <div\n                      style={{\n                        display: 'flex',\n                        whiteSpace: 'break-spaces',\n                        wordBreak: 'break-all',\n                        width: 'max-content',\n                      }}\n                      data-testid={`stream-field-name-${field}`}\n                    >\n                      <Text variant=\"semiBold\" color=\"primary\">\n                        <FormattedValue\n                          value={formattedValue}\n                          title={\n                            isValid\n                              ? 'Field'\n                              : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n                          }\n                          tooltipContent={tooltipContent}\n                        />\n                      </Text>\n                    </div>\n                  ) : (\n                    <div>&nbsp;</div>\n                  )}\n                </>\n              )\n            },\n          },\n        }\n      }\n      return { ...acc, ...names }\n    }, {})\n\n    // for Manager columns\n    // setUniqFields(fields)\n    const headerRow = {\n      id: {\n        id: 'id',\n        label: 'Entry ID',\n        sortable: true,\n      },\n      ...columnsNames,\n      actions: '',\n    }\n    setEntries([headerRow, ...streamEntries])\n    setColumns([\n      idColumn,\n      ...Object.keys(columnsNames).map((field) =>\n        getTemplateColumn(field, columnsNames[field]?.id),\n      ),\n      actionsColumn,\n    ])\n\n    if (viewFormat !== viewFormatProp) {\n      setViewFormat(viewFormatProp)\n    }\n  }, [loadedEntries, deleting, viewFormatProp])\n\n  const closePopover = useCallback(() => {\n    setDeleting('')\n  }, [])\n\n  const showPopover = useCallback((entry = '') => {\n    setDeleting(`${entry + suffix}`)\n  }, [])\n\n  const formatItem = useCallback(\n    (field) => ({\n      name: field.name,\n      value: field.value,\n    }),\n    [viewFormatProp],\n  )\n\n  const onSuccessRemoved = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        browserViewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Stream,\n        numberOfRemoved: 1,\n      },\n    })\n  }\n\n  const handleDeleteEntry = (entryId = '') => {\n    dispatch(deleteStreamEntry(key, [entryId], onSuccessRemoved))\n    closePopover()\n  }\n\n  const handleRemoveIconClick = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        browserViewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.Stream,\n      },\n    })\n  }\n\n  const getTemplateColumn = (label: string, name: string): ITableColumn => ({\n    id: label,\n    label: name,\n    minWidth: minColumnWidth,\n    isSortable: false,\n    className: styles.cell,\n    headerClassName: 'streamItemHeader',\n    render: function Id({ id, fields }: StreamEntryDto, expanded: boolean) {\n      const index = toNumber(last(label.split('-')))\n      const values = fields.filter(\n        ({ name: fieldName }) => bufferToString(fieldName, viewFormat) === name,\n      )\n\n      const { value: decompressedBufferValue } = decompressingBuffer(\n        values[index]?.value || stringToBuffer(''),\n        compressor,\n      )\n      // const bufferValue = values[index]?.value || stringToBuffer('')\n      const { value: formattedValue, isValid } = formattingBuffer(\n        decompressedBufferValue,\n        viewFormatProp,\n        { expanded },\n      )\n      const tooltipContent = createTooltipContent(\n        formattedValue,\n        decompressedBufferValue,\n        viewFormatProp,\n      )\n\n      return (\n        <Text\n          style={{ maxWidth: '100%', minHeight: '36px' }}\n          component=\"div\"\n          color=\"secondary\"\n        >\n          <div\n            style={{ display: 'flex', whiteSpace: 'break-spaces' }}\n            className=\"streamItem\"\n            data-testid={`stream-entry-field-${id}`}\n          >\n            <FormattedValue\n              value={formattedValue}\n              title={\n                isValid\n                  ? 'Value'\n                  : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n              }\n              tooltipContent={tooltipContent}\n              expanded={expanded}\n              truncateLength={650}\n              anchorClassName=\"streamItem line-clamp-2\"\n            />\n          </div>\n        </Text>\n      )\n    },\n  })\n\n  const idColumn: ITableColumn = {\n    id: 'id',\n    label: 'Entry ID',\n    maxWidth: minColumnWidth,\n    minWidth: minColumnWidth,\n    isSortable: true,\n    className: styles.cell,\n    headerClassName: 'streamItemHeader',\n    render: function Id({ id }: StreamEntryDto) {\n      const idStr = bufferToString(id, viewFormat)\n      const timestamp = idStr.split('-')?.[0]\n\n      return (\n        <div style={{ minHeight: '38px' }}>\n          {id.length < MAX_VISIBLE_LENGTH_STREAM_TIMESTAMP && (\n            <Text\n              size=\"s\"\n              color=\"secondary\"\n              style={{ maxWidth: '100%' }}\n              component=\"div\"\n            >\n              <div\n                className=\"streamItem truncateText\"\n                style={{ display: 'flex' }}\n                data-testid={`stream-entry-${id}-date`}\n              >\n                {timestamp.length > MAX_FORMAT_LENGTH_STREAM_TIMESTAMP ? (\n                  '-'\n                ) : (\n                  <FormatedDate date={timestamp} />\n                )}\n              </div>\n            </Text>\n          )}\n          <Text\n            component=\"div\"\n            size=\"s\"\n            style={{ maxWidth: '100%' }}\n            className=\"truncateText\"\n          >\n            <div\n              className=\"streamItemId truncateText\"\n              data-testid={`stream-entry-${id}`}\n              title={idStr}\n            >\n              {id}\n            </div>\n          </Text>\n        </div>\n      )\n    },\n  }\n  const actionsColumn: ITableColumn = {\n    id: 'actions',\n    label: '',\n    headerClassName: styles.actionsHeader,\n    textAlignment: TableCellTextAlignment.Left,\n    absoluteWidth: actionsWidth,\n    maxWidth: actionsWidth,\n    minWidth: actionsWidth,\n    render: function Actions({ id }: StreamEntryDto) {\n      return (\n        <div>\n          <PopoverDelete\n            header={createDeleteFieldHeader(id)}\n            text={createDeleteFieldMessage(keyString)}\n            item={id}\n            suffix={suffix}\n            deleting={deleting}\n            closePopover={closePopover}\n            updateLoading={false}\n            showPopover={showPopover}\n            testid={`remove-entry-button-${id}`}\n            handleDeleteItem={handleDeleteEntry}\n            handleButtonClick={handleRemoveIconClick}\n          />\n        </div>\n      )\n    },\n  }\n\n  return (\n    <StreamDataView\n      data={entries}\n      columns={columns}\n      onClosePopover={closePopover}\n      {...props}\n    />\n  )\n}\n\nexport default StreamDataViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-data-view/index.ts",
    "content": "import StreamDataViewWrapper from './StreamDataViewWrapper'\n\nexport default StreamDataViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/StreamDetailsBody.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  streamDataSelector,\n  streamRangeSelector,\n} from 'uiSrc/slices/browser/stream'\nimport { anyToBuffer, bufferToString, stringToBuffer } from 'uiSrc/utils'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  GZIP_COMPRESSED_VALUE_2,\n  DECOMPRESSED_VALUE_STR_1,\n  DECOMPRESSED_VALUE_STR_2,\n} from 'uiSrc/utils/tests/decompressors'\nimport { StreamDetailsBody, Props } from './StreamDetailsBody'\nimport { MAX_FORMAT_LENGTH_STREAM_TIMESTAMP } from '../constants'\n\njest.mock('uiSrc/slices/browser/stream', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/stream'),\n  streamRangeSelector: jest.fn().mockReturnValue({ start: '', end: '' }),\n  streamDataSelector: jest.fn().mockReturnValue({\n    total: 0,\n    entries: [],\n    keyName: '',\n    keyNameString: '',\n    lastGeneratedId: '',\n    firstEntry: {\n      id: '',\n      fields: [],\n    },\n    lastEntry: {\n      id: '',\n      fields: [],\n    },\n    lastRefreshTime: null,\n  }),\n}))\n\nconst mockedProps = mock<Props>()\n\nconst mockedEntryData = {\n  keyName: bufferToString('stream_example'),\n  keyNameString: 'stream_example',\n  total: 1,\n  lastGeneratedId: '1652942518811-0',\n  lastRefreshTime: 1231231,\n  firstEntry: {\n    id: '1652942518810-0',\n    fields: [{ value: stringToBuffer('1'), name: stringToBuffer('2') }],\n  },\n  lastEntry: {\n    id: '1652942518811-0',\n    fields: [{ value: stringToBuffer('1'), name: stringToBuffer('2') }],\n  },\n  entries: [\n    {\n      id: '1652942518810-0',\n      fields: [{ value: stringToBuffer('1'), name: stringToBuffer('2') }],\n    },\n    {\n      id: '1652942518811-0',\n      fields: [{ value: stringToBuffer('1'), name: stringToBuffer('2') }],\n    },\n  ],\n}\n\nconst mockedRangeData = {\n  start: '1675751507404',\n  end: '1675751507406',\n}\n\ndescribe('StreamDetailsBody', () => {\n  it('should render', () => {\n    expect(\n      render(<StreamDetailsBody {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render Stream Data container', () => {\n    render(<StreamDetailsBody {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('stream-entries-container')).toBeInTheDocument()\n  })\n\n  it('should render Range filter', () => {\n    streamDataSelector.mockImplementation(() => ({\n      ...mockedEntryData,\n    }))\n\n    streamRangeSelector.mockImplementation(() => ({\n      ...mockedRangeData,\n    }))\n\n    render(<StreamDetailsBody {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('range-bar')).toBeInTheDocument()\n  })\n\n  it(`should not render Range filter if id more than ${MAX_FORMAT_LENGTH_STREAM_TIMESTAMP}`, () => {\n    const entryWithHugeId = {\n      id: '3123123123123123123123-123123123',\n      fields: [{ value: stringToBuffer('1'), name: stringToBuffer('2') }],\n    }\n\n    const mockedEntries = [...mockedEntryData.entries, entryWithHugeId]\n\n    streamDataSelector.mockImplementation(() => ({\n      ...mockedEntryData,\n      lastEntry: entryWithHugeId,\n      entries: mockedEntries,\n    }))\n\n    streamRangeSelector.mockImplementation(() => ({\n      ...mockedRangeData,\n    }))\n\n    const { queryByTestId } = render(\n      <StreamDetailsBody {...instance(mockedProps)} />,\n    )\n\n    expect(queryByTestId('range-bar')).not.toBeInTheDocument()\n  })\n\n  describe('decompressed  data', () => {\n    it('should render decompressed GZIP data', () => {\n      const mockId = '1232-123123123'\n      const entryWithCompressedGZIPData = {\n        id: mockId,\n        fields: [\n          {\n            name: anyToBuffer(GZIP_COMPRESSED_VALUE_1),\n            value: anyToBuffer(GZIP_COMPRESSED_VALUE_2),\n          },\n        ],\n      }\n\n      streamDataSelector.mockImplementation(() => ({\n        ...mockedEntryData,\n        firstEntry: entryWithCompressedGZIPData,\n        lastEntry: entryWithCompressedGZIPData,\n        entries: [entryWithCompressedGZIPData],\n      }))\n\n      const { queryAllByTestId } = render(\n        <StreamDetailsBody {...instance(mockedProps)} />,\n      )\n\n      const fieldNameEl = queryAllByTestId(/stream-field-name-/)?.[0]\n      const entryFieldEl = queryAllByTestId(/stream-entry-field-/)?.[0]\n\n      expect(fieldNameEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1)\n      expect(entryFieldEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_2)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/StreamDetailsBody.tsx",
    "content": "import React, { useCallback, useEffect, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { isNull, last, toString } from 'lodash'\nimport cx from 'classnames'\n\nimport {\n  streamSelector,\n  streamGroupsSelector,\n  streamRangeSelector,\n  streamDataSelector,\n  fetchMoreStreamEntries,\n  updateStart,\n  updateEnd,\n  fetchStreamEntries,\n  setStreamInitialState,\n} from 'uiSrc/slices/browser/stream'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { getNextId, getTimestampFromId } from 'uiSrc/utils/streamUtils'\nimport { SortOrder } from 'uiSrc/constants'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport RangeFilter from 'uiSrc/components/range-filter'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { GetStreamEntriesResponse } from 'apiSrc/modules/browser/stream/dto'\n\nimport ConsumersViewWrapper from '../consumers-view'\nimport GroupsViewWrapper from '../groups-view'\nimport MessagesViewWrapper from '../messages-view'\nimport StreamDataViewWrapper from '../stream-data-view'\nimport StreamTabs from '../stream-tabs'\nimport { MAX_FORMAT_LENGTH_STREAM_TIMESTAMP } from '../constants'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {}\n\nconst StreamDetailsBody = (props: Props) => {\n  const {\n    viewType,\n    loading,\n    sortOrder: entryColumnSortOrder,\n  } = useSelector(streamSelector)\n  const { loading: loadingGroups } = useSelector(streamGroupsSelector)\n  const { start, end } = useSelector(streamRangeSelector)\n  const { firstEntry, lastEntry, entries } = useSelector(streamDataSelector)\n  const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' }\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  const dispatch = useDispatch()\n\n  const firstEntryTimeStamp = useMemo(\n    () => getTimestampFromId(firstEntry?.id),\n    [firstEntry?.id],\n  )\n  const lastEntryTimeStamp = useMemo(\n    () => getTimestampFromId(lastEntry?.id),\n    [lastEntry?.id],\n  )\n\n  const startNumber = useMemo(\n    () => (start === '' ? 0 : parseInt(start, 10)),\n    [start],\n  )\n  const endNumber = useMemo(() => (end === '' ? 0 : parseInt(end, 10)), [end])\n\n  const shouldFilterRender =\n    !isNull(firstEntry) &&\n    firstEntry.id !== '' &&\n    !isNull(lastEntry) &&\n    lastEntry.id !== '' &&\n    toString(firstEntryTimeStamp)?.length <\n      MAX_FORMAT_LENGTH_STREAM_TIMESTAMP &&\n    toString(lastEntryTimeStamp)?.length < MAX_FORMAT_LENGTH_STREAM_TIMESTAMP\n\n  useEffect(\n    () => () => {\n      dispatch(setStreamInitialState())\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (isNull(firstEntry)) {\n      dispatch(updateStart(''))\n    }\n    if (start === '' && firstEntry?.id !== '') {\n      dispatch(updateStart(firstEntryTimeStamp.toString()))\n    }\n  }, [firstEntryTimeStamp])\n\n  useEffect(() => {\n    if (isNull(lastEntry)) {\n      dispatch(updateEnd(''))\n    }\n    if (end === '' && lastEntry?.id !== '') {\n      dispatch(updateEnd(lastEntryTimeStamp.toString()))\n    }\n  }, [lastEntryTimeStamp])\n\n  const loadMoreItems = () => {\n    const lastLoadedEntryId = last(entries)?.id ?? ''\n    const lastLoadedEntryTimeStamp = getTimestampFromId(lastLoadedEntryId)\n\n    const lastRangeEntryTimestamp = end\n      ? parseInt(end, 10)\n      : getTimestampFromId(lastEntry?.id)\n    const firstRangeEntryTimestamp = start\n      ? parseInt(start, 10)\n      : getTimestampFromId(firstEntry?.id)\n    const shouldLoadMore = () => {\n      if (!lastLoadedEntryTimeStamp) {\n        return false\n      }\n      return entryColumnSortOrder === SortOrder.ASC\n        ? lastLoadedEntryTimeStamp <= lastRangeEntryTimestamp\n        : lastLoadedEntryTimeStamp >= firstRangeEntryTimestamp\n    }\n    const nextId = getNextId(lastLoadedEntryId, entryColumnSortOrder)\n\n    if (shouldLoadMore()) {\n      dispatch(\n        fetchMoreStreamEntries(\n          key,\n          entryColumnSortOrder === SortOrder.DESC ? start : nextId,\n          entryColumnSortOrder === SortOrder.DESC ? nextId : end,\n          SCAN_COUNT_DEFAULT,\n          entryColumnSortOrder,\n        ),\n      )\n    }\n  }\n\n  const filterTelemetry = (data: GetStreamEntriesResponse) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_DATA_FILTERED,\n      eventData: {\n        databaseId: instanceId,\n        total: data.total,\n      },\n    })\n  }\n\n  const resetFilterTelemetry = (data: GetStreamEntriesResponse) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_DATA_FILTER_RESET,\n      eventData: {\n        databaseId: instanceId,\n        total: data.total,\n      },\n    })\n  }\n\n  const loadEntries = (\n    telemetryAction?: (data: GetStreamEntriesResponse) => void,\n  ) => {\n    dispatch(\n      fetchStreamEntries(\n        key,\n        SCAN_COUNT_DEFAULT,\n        entryColumnSortOrder,\n        false,\n        telemetryAction,\n      ),\n    )\n  }\n\n  const handleChangeStartFilter = useCallback(\n    (value: number, shouldSentEventTelemetry: boolean) => {\n      dispatch(updateStart(value.toString()))\n      loadEntries(shouldSentEventTelemetry ? filterTelemetry : undefined)\n    },\n    [],\n  )\n\n  const handleChangeEndFilter = useCallback(\n    (value: number, shouldSentEventTelemetry: boolean) => {\n      dispatch(updateEnd(value.toString()))\n      loadEntries(shouldSentEventTelemetry ? filterTelemetry : undefined)\n    },\n    [],\n  )\n\n  const handleResetFilter = useCallback(() => {\n    dispatch(updateStart(firstEntryTimeStamp.toString()))\n    dispatch(updateEnd(lastEntryTimeStamp.toString()))\n    loadEntries(resetFilterTelemetry)\n  }, [lastEntryTimeStamp, firstEntryTimeStamp])\n\n  const handleUpdateRangeMin = useCallback((min: number) => {\n    dispatch(updateStart(min.toString()))\n  }, [])\n\n  const handleUpdateRangeMax = useCallback((max: number) => {\n    dispatch(updateEnd(max.toString()))\n  }, [])\n\n  return (\n    <Col data-testid=\"stream-details\" className={styles.container} gap=\"m\">\n      {(loading || loadingGroups) && (\n        <ProgressBarLoader color=\"primary\" data-testid=\"progress-key-stream\" />\n      )}\n      {shouldFilterRender ? (\n        <RangeFilter\n          disabled={viewType !== StreamViewType.Data}\n          max={lastEntryTimeStamp}\n          min={firstEntryTimeStamp}\n          start={startNumber}\n          end={endNumber}\n          handleChangeStart={handleChangeStartFilter}\n          handleChangeEnd={handleChangeEndFilter}\n          handleResetFilter={handleResetFilter}\n          handleUpdateRangeMax={handleUpdateRangeMax}\n          handleUpdateRangeMin={handleUpdateRangeMin}\n        />\n      ) : (\n        <div className={styles.rangeWrapper}>\n          <div className={cx(styles.sliderTrack, styles.mockRange)} />\n        </div>\n      )}\n      <StreamTabs />\n      {viewType === StreamViewType.Data && (\n        <StreamDataViewWrapper loadMoreItems={loadMoreItems} {...props} />\n      )}\n      {viewType === StreamViewType.Groups && <GroupsViewWrapper {...props} />}\n      {viewType === StreamViewType.Consumers && (\n        <ConsumersViewWrapper {...props} />\n      )}\n      {viewType === StreamViewType.Messages && (\n        <MessagesViewWrapper {...props} />\n      )}\n    </Col>\n  )\n}\n\nexport { StreamDetailsBody }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/index.ts",
    "content": "export { StreamDetailsBody } from './StreamDetailsBody'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-details-body/styles.module.scss",
    "content": "$cellPaddingWidth: 12px;\n\n.container {\n  padding: 0 18px;\n  height: 100%;\n  position: relative;\n\n  :global(.stream-details-table) {\n    height: calc(100% - 125px);\n  }\n\n  :global {\n    .ReactVirtualized__Grid__innerScrollContainer {\n      .ReactVirtualized__Table__rowColumn {\n        padding-right: 6px !important;\n        border-left: none !important;\n\n        > div {\n          max-width: 100%;\n          min-height: 54px !important;\n          padding: $cellPaddingWidth !important;\n        }\n      }\n\n      .ReactVirtualized__Table__row {\n        &:last-of-type {\n          border-bottom: none !important;\n        }\n      }\n\n      & > div:hover {\n        background: var(--euiColorLightestShade);\n\n        .value-table-actions {\n          background-color: var(--euiColorLightestShade) !important;\n        }\n\n        .streamItem {\n          color: var(--inputTextColor) !important;\n        }\n\n        .streamItemId {\n          color: var(--euiTextSubduedColor) !important;\n        }\n      }\n    }\n\n    .ReactVirtualized__Table__headerRow {\n      .streamItemHeader {\n        padding-right: 4px !important;\n\n        > div > div:first-of-type {\n          padding: 18px 0 18px $cellPaddingWidth !important;\n        }\n      }\n    }\n\n    .ReactVirtualized__Table__Grid {\n      border: 1px solid var(--separatorColor) !important;\n      border-top: 0 !important;\n    }\n  }\n}\n\n.rangeWrapper {\n  margin: 30px 30px 26px;\n  padding: 12px 0;\n}\n\n.sliderTrack.mockRange {\n  left: 18px;\n  width: calc(100% - 36px);\n}\n\n.sliderTrack {\n  position: absolute;\n  background-color: var(--separatorColor);\n  width: 100%;\n  height: 1px;\n  margin-top: 2px;\n  z-index: 1;\n}\n\n:global(.streamItem) {\n  white-space: normal;\n  max-width: 100%;\n  word-break: break-all;\n}\n\n:global(.streamItemId) {\n  color: var(--euiColorMediumShade) !important;\n  display: flex;\n}\n\n:global(.stream-entry-actions) {\n  margin-left: -5px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/StreamTabs.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport StreamTabs from './StreamTabs'\n\ndescribe('StreamTabs', () => {\n  it('should render', () => {\n    expect(render(<StreamTabs />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/StreamTabs.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  streamSelector,\n  setStreamViewType,\n  fetchConsumerGroups,\n  selectedGroupSelector,\n  selectedConsumerSelector,\n  fetchStreamEntries,\n} from 'uiSrc/slices/browser/stream'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { SortOrder } from 'uiSrc/constants'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { ConsumerGroupDto } from 'apiSrc/modules/browser/stream/dto'\n\nconst StreamTabs = () => {\n  const { viewType } = useSelector(streamSelector)\n  const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' }\n  const { nameString: selectedGroupName = '' } =\n    useSelector(selectedGroupSelector) ?? {}\n  const { nameString: selectedConsumerName = '' } =\n    useSelector(selectedConsumerSelector) ?? {}\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  const onSuccessLoadedConsumerGroups = (data: ConsumerGroupDto[]) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.STREAM_CONSUMER_GROUPS_LOADED,\n      eventData: {\n        databaseId: instanceId,\n        length: data.length,\n      },\n    })\n  }\n\n  const onSelectedTabChanged = (id: StreamViewType) => {\n    if (id === StreamViewType.Data) {\n      dispatch<any>(\n        fetchStreamEntries(key, SCAN_COUNT_DEFAULT, SortOrder.DESC, true),\n      )\n    }\n    if (id === StreamViewType.Groups) {\n      dispatch(fetchConsumerGroups(true, onSuccessLoadedConsumerGroups))\n    }\n    dispatch(setStreamViewType(id))\n  }\n\n  const tabs: TabInfo[] = useMemo(() => {\n    const baseTabs: TabInfo[] = [\n      {\n        value: StreamViewType.Data,\n        label: 'Stream Data',\n        content: null,\n      },\n      {\n        value: StreamViewType.Groups,\n        label: 'Consumer Groups',\n        content: null,\n      },\n    ]\n\n    if (\n      selectedGroupName &&\n      (viewType === StreamViewType.Consumers ||\n        viewType === StreamViewType.Messages)\n    ) {\n      baseTabs.push({\n        value: StreamViewType.Consumers,\n        label: selectedGroupName,\n        content: null,\n      })\n    }\n\n    if (selectedConsumerName && viewType === StreamViewType.Messages) {\n      baseTabs.push({\n        value: StreamViewType.Messages,\n        label: selectedConsumerName,\n        content: null,\n      })\n    }\n\n    return baseTabs\n  }, [viewType, selectedGroupName, selectedConsumerName])\n\n  return (\n    <Tabs\n      tabs={tabs}\n      value={viewType}\n      onChange={(id) => onSelectedTabChanged(id as StreamViewType)}\n      data-testid=\"stream-tabs\"\n    />\n  )\n}\n\nexport default StreamTabs\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/stream-details/stream-tabs/index.ts",
    "content": "import StreamTabs from './StreamTabs'\n\nexport default StreamTabs\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n  act,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { stringDataSelector, stringSelector } from 'uiSrc/slices/browser/string'\nimport { setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA } from 'uiSrc/constants'\nimport { Props, StringDetails } from './StringDetails'\n\nconst mockedProps = mock<Props>()\nconst EDIT_VALUE_BTN_TEST_ID = 'edit-key-value-btn'\n\njest.mock('uiSrc/slices/browser/string', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/string'),\n  stringDataSelector: jest.fn().mockReturnValue({\n    value: {\n      type: 'Buffer',\n      data: [49, 50, 51, 52],\n    },\n  }),\n  stringSelector: jest.fn().mockReturnValue({\n    isCompressed: false,\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeyDataSelector: jest.fn().mockReturnValue({\n    name: {\n      type: 'Buffer',\n      data: [116, 101, 115, 116],\n    },\n    nameString: 'test',\n    length: 4,\n  }),\n}))\n\njest.mock('uiSrc/pages/vector-search/hooks/useIsKeyIndexed', () => ({\n  useIsKeyIndexed: jest.fn().mockReturnValue({\n    indexes: [],\n    status: 'idle',\n  }),\n  UseIsKeyIndexedStatus: { Idle: 'idle', Loading: 'loading', Ready: 'ready' },\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('StringDetails', () => {\n  it('should render', () => {\n    expect(render(<StringDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should be able to change value (long string fully load)', () => {\n    render(<StringDetails {...mockedProps} />)\n\n    const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`)\n    expect(editValueBtn).toHaveProperty('disabled', false)\n  })\n\n  it('should not be able to change value (long string not fully load)', () => {\n    const stringDataSelectorMock = jest.fn().mockReturnValueOnce({\n      value: {\n        type: 'Buffer',\n        data: [49, 50, 51],\n      },\n    })\n    ;(stringDataSelector as jest.Mock).mockImplementationOnce(\n      stringDataSelectorMock,\n    )\n\n    render(<StringDetails {...mockedProps} />)\n\n    const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`)\n    expect(editValueBtn).toHaveProperty('disabled', true)\n  })\n\n  it('should not be able to change value (compressed)', () => {\n    const stringSelectorMock = jest.fn().mockReturnValueOnce({\n      isCompressed: true,\n    })\n    ;(stringSelector as jest.Mock).mockImplementationOnce(stringSelectorMock)\n\n    render(<StringDetails {...mockedProps} />)\n\n    const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`)\n    expect(editValueBtn).toHaveProperty('disabled', true)\n  })\n\n  it('\"edit-key-value-btn\" should render', () => {\n    const { queryByTestId } = render(\n      <StringDetails {...instance(mockedProps)} />,\n    )\n    expect(queryByTestId('edit-key-value-btn')).toBeInTheDocument()\n  })\n\n  it('should disable refresh when editing', async () => {\n    render(<StringDetails {...mockedProps} />)\n    const afterRenderActions = [...store.getActions()]\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`))\n    })\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  describe('truncated data', () => {\n    it('should not be able to change value when value is truncated', async () => {\n      const stringDataSelectorMock = jest.fn().mockReturnValueOnce({\n        value: MOCK_TRUNCATED_BUFFER_VALUE,\n      })\n      ;(stringDataSelector as jest.Mock).mockImplementationOnce(\n        stringDataSelectorMock,\n      )\n\n      render(<StringDetails {...mockedProps} />)\n\n      const editValueBtn = screen.getByTestId(`${EDIT_VALUE_BTN_TEST_ID}`)\n      expect(editValueBtn).toBeDisabled()\n\n      await act(async () => {\n        fireEvent.focus(editValueBtn)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getByTestId('edit-key-value-tooltip')).toHaveTextContent(\n        TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/StringDetails.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  initialKeyInfo,\n  refreshKey,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n  setSelectedKeyRefreshDisabled,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  KeyTypes,\n  ModulesKeyTypes,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n  TEXT_DISABLED_COMPRESSED_VALUE,\n  TEXT_DISABLED_FORMATTER_EDITING,\n  TEXT_DISABLED_STRING_EDITING,\n} from 'uiSrc/constants'\n\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { IFetchKeyArgs } from 'uiSrc/constants/prop-types/keys'\nimport {\n  resetStringValue,\n  stringDataSelector,\n  stringSelector,\n} from 'uiSrc/slices/browser/string'\nimport {\n  isTruncatedString,\n  isFormatEditable,\n  isFullStringLoaded,\n} from 'uiSrc/utils'\nimport { StringDetailsValue } from './string-details-value'\nimport { EditItemAction } from '../key-details-actions'\nimport { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'\n\nexport interface Props extends KeyDetailsHeaderProps {}\n\nconst StringDetails = (props: Props) => {\n  const { onRemoveKey } = props\n  const keyType = KeyTypes.String\n\n  const { loading, viewFormat: viewFormatProp } =\n    useSelector(selectedKeySelector)\n  const { length } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo\n  const { value: keyValue } = useSelector(stringDataSelector)\n  const { isCompressed: isStringCompressed } = useSelector(stringSelector)\n\n  const isTruncatedValue = isTruncatedString(keyValue)\n  const isEditable =\n    !isTruncatedValue && !isStringCompressed && isFormatEditable(viewFormatProp)\n  const isStringEditable = isFullStringLoaded(keyValue?.data?.length, length)\n  const noEditableText = isTruncatedValue\n    ? TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA\n    : isStringCompressed\n      ? TEXT_DISABLED_COMPRESSED_VALUE\n      : TEXT_DISABLED_FORMATTER_EDITING\n  const editToolTip = !isEditable\n    ? noEditableText\n    : !isStringEditable\n      ? TEXT_DISABLED_STRING_EDITING\n      : null\n\n  const [editItem, setEditItem] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  const handleRefreshKey = (\n    key: RedisResponseBuffer,\n    type: KeyTypes | ModulesKeyTypes,\n    args: IFetchKeyArgs,\n  ) => {\n    dispatch(refreshKey(key, type, args))\n  }\n\n  const handleRemoveKey = () => {\n    dispatch(resetStringValue())\n    onRemoveKey()\n  }\n\n  const Actions = () => (\n    <EditItemAction\n      title=\"Edit Value\"\n      tooltipContent={editToolTip}\n      isEditable={isStringEditable && isEditable}\n      onEditItem={() => {\n        dispatch(setSelectedKeyRefreshDisabled(!editItem))\n        setEditItem(!editItem)\n      }}\n    />\n  )\n\n  return (\n    <div className=\"fluid flex-column relative\">\n      <KeyDetailsHeader\n        {...props}\n        key=\"key-details-header\"\n        onRemoveKey={handleRemoveKey}\n      />\n      <KeyDetailsSubheader keyType={keyType} Actions={Actions} />\n      <div className=\"key-details-body\" key=\"key-details-body\">\n        {!loading && (\n          <div className=\"flex-column\" style={{ flex: '1', height: '100%' }}>\n            <StringDetailsValue\n              isEditItem={editItem}\n              setIsEdit={(isEdit: boolean) => {\n                setEditItem(isEdit)\n                dispatch(setSelectedKeyRefreshDisabled(isEdit))\n              }}\n              onRefresh={handleRefreshKey}\n            />\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { StringDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/index.ts",
    "content": "export { StringDetails } from './StringDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-value/StringDetailsValue.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { KeyValueCompressor, KeyValueFormat } from 'uiSrc/constants'\nimport {\n  fetchDownloadStringValue,\n  stringDataSelector,\n} from 'uiSrc/slices/browser/string'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { anyToBuffer, bufferToString } from 'uiSrc/utils'\nimport { render, screen, fireEvent, act } from 'uiSrc/utils/test-utils'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  GZIP_COMPRESSED_VALUE_2,\n  DECOMPRESSED_VALUE_STR_1,\n  DECOMPRESSED_VALUE_STR_2,\n} from 'uiSrc/utils/tests/decompressors'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { downloadFile } from 'uiSrc/utils/dom/downloadFile'\nimport { selectedKeySelector } from 'uiSrc/slices/browser/keys'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { StringDetailsValue, Props } from './StringDetailsValue'\n\nconst STRING_VALUE = 'string-value'\nconst STRING_VALUE_SPACE = 'string value'\nconst LOAD_ALL_BTN = 'load-all-value-btn'\nconst DOWNLOAD_BTN = 'download-all-value-btn'\n\nconst STRING_MAX_LENGTH = 2\nconst STRING_LENGTH = 4\n\nconst fullValue = { type: 'Buffer', data: [49, 50, 51, 52] }\nconst partValue = { type: 'Buffer', data: [49, 50] }\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/browser/string', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/string'),\n  stringDataSelector: jest.fn().mockReturnValue({\n    value: fullValue,\n  }),\n  fetchDownloadStringValue: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeyDataSelector: jest.fn().mockReturnValue({\n    name: fullValue,\n    type: 'string',\n    length: STRING_LENGTH,\n  }),\n  selectedKeySelector: jest.fn(),\n}))\n\njest.mock('uiSrc/constants', () => ({\n  ...jest.requireActual('uiSrc/constants'),\n  STRING_MAX_LENGTH,\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    compressor: null,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useDispatch: () => jest.fn().mockReturnValue(() => jest.fn()),\n}))\n\nbeforeEach(async () => {\n  const selectedKeySelectorMock = jest.fn().mockReturnValue({\n    viewFormat: KeyValueFormat.Unicode,\n  })\n  ;(selectedKeySelector as jest.Mock).mockImplementation(\n    selectedKeySelectorMock,\n  )\n})\n\ndescribe('StringDetailsValue', () => {\n  it('should render', () => {\n    expect(\n      render(<StringDetailsValue {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render textarea if edit mode', () => {\n    render(\n      <StringDetailsValue\n        {...instance(mockedProps)}\n        isEditItem\n        setIsEdit={jest.fn()}\n      />,\n    )\n    const textArea = screen.getByTestId(STRING_VALUE)\n    expect(textArea).toBeInTheDocument()\n  })\n\n  it('should update string value', () => {\n    render(\n      <StringDetailsValue\n        {...instance(mockedProps)}\n        isEditItem\n        setIsEdit={jest.fn()}\n      />,\n    )\n    const textArea = screen.getByTestId(STRING_VALUE)\n    fireEvent.change(textArea, { target: { value: STRING_VALUE_SPACE } })\n    expect(textArea).toHaveValue(STRING_VALUE_SPACE)\n  })\n\n  it('should stay empty string after cancel', async () => {\n    render(\n      <StringDetailsValue\n        {...instance(mockedProps)}\n        isEditItem\n        setIsEdit={jest.fn()}\n      />,\n    )\n    const textArea = screen.getByTestId(STRING_VALUE)\n    fireEvent.change(textArea, { target: { value: STRING_VALUE_SPACE } })\n    const btnACancel = screen.getByTestId('cancel-btn')\n    await act(() => {\n      fireEvent.click(btnACancel)\n    })\n    const textArea2 = screen.getByTestId(STRING_VALUE)\n    expect(textArea2).toHaveValue(bufferToString(fullValue))\n  })\n\n  it('should update value after apply', () => {\n    render(\n      <StringDetailsValue\n        {...instance(mockedProps)}\n        isEditItem\n        setIsEdit={jest.fn()}\n      />,\n    )\n    const textArea = screen.getByTestId(STRING_VALUE)\n    fireEvent.change(textArea, { target: { value: STRING_VALUE_SPACE } })\n    const btnApply = screen.getByTestId('apply-btn')\n    fireEvent.click(btnApply)\n    expect(textArea).toHaveValue(STRING_VALUE_SPACE)\n  })\n\n  it('should render load button and download button if long string is partially loaded', () => {\n    const stringDataSelectorMock = jest.fn().mockReturnValue({\n      value: partValue,\n    })\n    stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n    render(<StringDetailsValue {...instance(mockedProps)} />)\n    const loadAllBtn = screen.getByTestId(LOAD_ALL_BTN)\n    const downloadBtn = screen.getByTestId(DOWNLOAD_BTN)\n    expect(loadAllBtn).toBeInTheDocument()\n    expect(downloadBtn).toBeInTheDocument()\n  })\n\n  it('should call onRefresh and sendEventTelemetry after clicking on load button', () => {\n    const onRefresh = jest.fn()\n    const stringDataSelectorMock = jest.fn().mockReturnValue({\n      value: partValue,\n    })\n    stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n    render(\n      <StringDetailsValue {...instance(mockedProps)} onRefresh={onRefresh} />,\n    )\n\n    fireEvent.click(screen.getByTestId(LOAD_ALL_BTN))\n\n    expect(onRefresh).toBeCalled()\n    expect(onRefresh).toBeCalledWith(fullValue, 'string', {\n      end: STRING_MAX_LENGTH + 1,\n    })\n    expect(sendEventTelemetry).toBeCalled()\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.STRING_LOAD_ALL_CLICKED,\n      eventData: { databaseId: undefined, length: STRING_LENGTH },\n    })\n  })\n\n  it('Should add \"...\" in the end of the part value', async () => {\n    const stringDataSelectorMock = jest.fn().mockReturnValue({\n      value: partValue,\n    })\n    stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n    render(<StringDetailsValue {...instance(mockedProps)} />)\n    expect(screen.getByTestId(STRING_VALUE)).toHaveTextContent(\n      `${bufferToString(partValue)}...`,\n    )\n  })\n\n  it('Should render partValue in the Unicode format', async () => {\n    const stringDataSelectorMock = jest.fn().mockReturnValue({\n      // vector value\n      value: anyToBuffer(new Float32Array([1.0]).buffer),\n    })\n    const selectedKeySelectorMock = jest.fn().mockReturnValue({\n      viewFormat: KeyValueFormat.Vector32Bit,\n    })\n    ;(selectedKeySelector as jest.Mock).mockImplementation(\n      selectedKeySelectorMock,\n    )\n    ;(stringDataSelector as jest.Mock).mockImplementation(\n      stringDataSelectorMock,\n    )\n\n    render(<StringDetailsValue {...instance(mockedProps)} />)\n    expect(screen.getByTestId(STRING_VALUE)).toHaveTextContent('�?...')\n    expect(screen.getByTestId(STRING_VALUE)).not.toHaveTextContent(\n      '[object Object]',\n    )\n  })\n\n  it('Should not add \"...\" in the end of the full value', async () => {\n    const stringDataSelectorMock = jest.fn().mockReturnValue({\n      value: fullValue,\n    })\n    stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n    render(<StringDetailsValue {...instance(mockedProps)} />)\n    expect(screen.getByTestId(STRING_VALUE)).toHaveTextContent(\n      bufferToString(fullValue),\n    )\n  })\n\n  it('should call fetchDownloadStringValue and sendEventTelemetry after clicking on load button and download button', async () => {\n    const stringDataSelectorMock = jest.fn().mockReturnValue({\n      value: partValue,\n    })\n    stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n    render(<StringDetailsValue {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId(DOWNLOAD_BTN))\n\n    expect(sendEventTelemetry).toBeCalled()\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.STRING_DOWNLOAD_VALUE_CLICKED,\n      eventData: { databaseId: undefined, length: STRING_LENGTH },\n    })\n    expect(fetchDownloadStringValue).toBeCalled()\n    expect(fetchDownloadStringValue).toBeCalledWith(fullValue, downloadFile)\n  })\n\n  describe('decompressed  data', () => {\n    it('should render decompressed GZIP data = \"1\"', () => {\n      const stringDataSelectorMock = jest.fn().mockReturnValue({\n        value: anyToBuffer(GZIP_COMPRESSED_VALUE_1),\n      })\n      stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n      connectedInstanceSelector.mockImplementation(() => ({\n        compressor: KeyValueCompressor.GZIP,\n      }))\n\n      render(\n        <StringDetailsValue\n          {...instance(mockedProps)}\n          isEditItem\n          setIsEdit={jest.fn()}\n        />,\n      )\n      const textArea = screen.getByTestId(STRING_VALUE)\n\n      expect(textArea).toHaveValue(DECOMPRESSED_VALUE_STR_1)\n    })\n\n    it('should render decompressed GZIP data = \"2\"', () => {\n      const stringDataSelectorMock = jest.fn().mockReturnValue({\n        value: anyToBuffer(GZIP_COMPRESSED_VALUE_2),\n      })\n      stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n      connectedInstanceSelector.mockImplementation(() => ({\n        compressor: KeyValueCompressor.GZIP,\n      }))\n\n      render(\n        <StringDetailsValue\n          {...instance(mockedProps)}\n          isEditItem\n          setIsEdit={jest.fn()}\n        />,\n      )\n      const textArea = screen.getByTestId(STRING_VALUE)\n\n      expect(textArea).toHaveValue(DECOMPRESSED_VALUE_STR_2)\n    })\n  })\n\n  describe('truncated data', () => {\n    it('should hide download button when value is truncated', async () => {\n      const stringDataSelectorMock = jest.fn().mockReturnValue({\n        value: MOCK_TRUNCATED_BUFFER_VALUE,\n      })\n      stringDataSelector.mockImplementation(stringDataSelectorMock)\n\n      render(<StringDetailsValue {...instance(mockedProps)} />)\n\n      expect(screen.queryByTestId(DOWNLOAD_BTN)).not.toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-value/StringDetailsValue.tsx",
    "content": "import React, {\n  Ref,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  bufferToSerializedFormat,\n  bufferToString,\n  formattingBuffer,\n  isEqualBuffers,\n  isFormatEditable,\n  isFullStringLoaded,\n  isNonUnicodeFormatter,\n  isTruncatedString,\n  stringToBuffer,\n  stringToSerializedBufferFormat,\n} from 'uiSrc/utils'\nimport {\n  fetchDownloadStringValue,\n  resetStringValue,\n  setIsStringCompressed,\n  stringDataSelector,\n  stringSelector,\n  updateStringValueAction,\n} from 'uiSrc/slices/browser/string'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'\nimport { AddStringFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport {\n  selectedKeyDataSelector,\n  selectedKeySelector,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  KeyTypes,\n  KeyValueFormat,\n  ModulesKeyTypes,\n  STRING_MAX_LENGTH,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n  TEXT_DISABLED_COMPRESSED_VALUE,\n  TEXT_FAILED_CONVENT_FORMATTER,\n  TEXT_INVALID_VALUE,\n  TEXT_UNPRINTABLE_CHARACTERS,\n} from 'uiSrc/constants'\nimport { calculateTextareaLines } from 'uiSrc/utils/calculateTextareaLines'\nimport { decompressingBuffer } from 'uiSrc/utils/decompressors'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { downloadFile } from 'uiSrc/utils/dom/downloadFile'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { IFetchKeyArgs } from 'uiSrc/constants/prop-types/keys'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { DownloadIcon } from 'uiSrc/components/base/icons'\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { TextArea } from 'uiSrc/components/base/inputs'\nimport { RiTooltip } from 'uiSrc/components'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport styles from './styles.module.scss'\n\nconst MIN_ROWS = 8\nconst APPROXIMATE_WIDTH_OF_SIGN = 8.6\nconst APPROXIMATE_ROW_HEIGHT = 17\nconst TEXTAREA_PADDING = 80\nconst MAX_LENGTH = STRING_MAX_LENGTH + 1\n\nexport interface Props {\n  isEditItem: boolean\n  setIsEdit: (isEdit: boolean) => void\n  onRefresh: (\n    key: RedisResponseBuffer,\n    type: KeyTypes | ModulesKeyTypes,\n    args: IFetchKeyArgs,\n  ) => void\n}\n\nconst StringDetailsValue = (props: Props) => {\n  const { isEditItem, setIsEdit, onRefresh } = props\n\n  const { compressor = null } = useSelector(connectedInstanceSelector)\n  const { loading } = useSelector(stringSelector)\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { value: initialValue } = useSelector(stringDataSelector)\n  const {\n    name: key,\n    type: keyType,\n    length,\n  } = useSelector(selectedKeyDataSelector) ?? { name: '' }\n  const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector)\n  const isTruncatedValue = isTruncatedString(initialValue)\n\n  const [rows, setRows] = useState<number>(MIN_ROWS)\n  const [value, setValue] = useState<JSX.Element | string>('')\n  const [areaValue, setAreaValue] = useState<string>('')\n  const [viewFormat, setViewFormat] = useState(viewFormatProp)\n  const [isValid, setIsValid] = useState(true)\n  const [isDisabled, setIsDisabled] = useState(false)\n  const [isEditable, setIsEditable] = useState(true)\n  const [noEditableText, setNoEditableText] = useState<string>(\n    TEXT_DISABLED_COMPRESSED_VALUE,\n  )\n\n  const textAreaRef: Ref<HTMLTextAreaElement> = useRef(null)\n  const containerRef: Ref<HTMLDivElement> = useRef(null)\n\n  const dispatch = useDispatch()\n\n  useEffect(\n    () => () => {\n      dispatch(resetStringValue())\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (!initialValue) return\n\n    const { value: decompressedValue, isCompressed } = decompressingBuffer(\n      initialValue,\n      compressor,\n    )\n\n    const initialValueString = bufferToString(decompressedValue, viewFormat)\n    const fullStringLoaded = isFullStringLoaded(\n      initialValue?.data?.length,\n      length,\n    )\n\n    const { value: formattedValue, isValid } = formattingBuffer(\n      decompressedValue,\n      fullStringLoaded ? viewFormatProp : KeyValueFormat.Unicode,\n      { expanded: true },\n    )\n    setAreaValue(initialValueString)\n\n    setValue(!fullStringLoaded ? `${formattedValue}...` : formattedValue)\n    setIsValid(isValid)\n    setIsDisabled(\n      !isNonUnicodeFormatter(viewFormatProp, isValid) &&\n        !isEqualBuffers(initialValue, stringToBuffer(initialValueString)),\n    )\n    setIsEditable(\n      !isCompressed &&\n        !isTruncatedValue &&\n        isFormatEditable(viewFormatProp) &&\n        fullStringLoaded,\n    )\n    setNoEditableText(\n      isCompressed\n        ? TEXT_DISABLED_COMPRESSED_VALUE\n        : isTruncatedValue\n          ? TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA\n          : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp),\n    )\n\n    dispatch(setIsStringCompressed(isCompressed))\n\n    if (viewFormat !== viewFormatProp) {\n      setViewFormat(viewFormatProp)\n    }\n  }, [initialValue, viewFormatProp, compressor, length])\n\n  useEffect(() => {\n    // Approximate calculation of textarea rows by areaValue\n    if (!isEditItem || !textAreaRef.current || value === null) {\n      return\n    }\n    const calculatedRows = calculateTextareaLines(\n      areaValue,\n      textAreaRef.current.clientWidth,\n      APPROXIMATE_WIDTH_OF_SIGN,\n    )\n\n    // Calculate max rows based on container height\n    const containerHeight = containerRef.current?.clientHeight || 0\n    const availableHeight = containerHeight - TEXTAREA_PADDING\n    const maxRows = Math.floor(availableHeight / APPROXIMATE_ROW_HEIGHT)\n\n    // Clamp rows between MIN_ROWS and maxRows\n    const newRows = Math.max(MIN_ROWS, Math.min(calculatedRows, maxRows))\n    setRows(newRows)\n  }, [areaValue, isEditItem])\n\n  useMemo(() => {\n    if (isEditItem && initialValue) {\n      ;(document.activeElement as HTMLElement)?.blur()\n      setAreaValue(bufferToSerializedFormat(viewFormat, initialValue, 4))\n    }\n  }, [isEditItem])\n\n  const onApplyChanges = () => {\n    const data = stringToSerializedBufferFormat(viewFormat, areaValue)\n    const onSuccess = () => {\n      setIsEdit(false)\n      setValue(formattingBuffer(data, viewFormat, { expanded: true })?.value)\n    }\n    dispatch(updateStringValueAction(key, data, onSuccess))\n  }\n\n  const onDeclineChanges = useCallback(() => {\n    if (!initialValue) return\n\n    setAreaValue(bufferToSerializedFormat(viewFormat, initialValue, 4))\n    setIsEdit(false)\n  }, [initialValue])\n\n  const isLoading = loading || value === null\n\n  const handleLoadAll = (\n    key: RedisResponseBuffer,\n    type: KeyTypes | ModulesKeyTypes,\n  ) => {\n    const endString = length - 1\n    onRefresh(key, type, { end: endString })\n    sendEventTelemetry({\n      event: TelemetryEvent.STRING_LOAD_ALL_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        length,\n      },\n    })\n  }\n\n  const handleDownloadString = (e: React.MouseEvent<HTMLButtonElement>) => {\n    e.preventDefault()\n    dispatch(fetchDownloadStringValue(key, downloadFile))\n    sendEventTelemetry({\n      event: TelemetryEvent.STRING_DOWNLOAD_VALUE_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        length,\n      },\n    })\n  }\n\n  const renderValue = (value: string) => {\n    const textEl = (\n      <Text\n        color=\"secondary\"\n        className={styles.stringValue}\n        onClick={() => isEditable && setIsEdit(true)}\n        style={{ whiteSpace: 'break-spaces' }}\n        data-testid=\"string-value\"\n      >\n        {areaValue !== ''\n          ? value\n          : !isLoading && <span style={{ fontStyle: 'italic' }}>Empty</span>}\n      </Text>\n    )\n\n    return (\n      <RiTooltip\n        title={!isValid ? noEditableText : undefined}\n        anchorClassName={styles.tooltipAnchor}\n        className={styles.tooltip}\n        position=\"left\"\n        data-testid=\"string-value-tooltip\"\n      >\n        {textEl}\n      </RiTooltip>\n    )\n  }\n\n  return (\n    <>\n      <div\n        className={styles.container}\n        ref={containerRef}\n        data-testid=\"string-details\"\n      >\n        {isLoading && (\n          <ProgressBarLoader\n            color=\"primary\"\n            data-testid=\"progress-key-string\"\n          />\n        )}\n        {!isEditItem && renderValue(value as string)}\n        {isEditItem && (\n          <InlineItemEditor\n            controlsPosition=\"bottom\"\n            placeholder=\"Enter Value\"\n            fieldName=\"value\"\n            expandable\n            isLoading={false}\n            isDisabled={isDisabled}\n            disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS}\n            onDecline={onDeclineChanges}\n            onApply={onApplyChanges}\n            declineOnUnmount={false}\n            approveText={TEXT_INVALID_VALUE}\n            approveByValidation={() =>\n              formattingBuffer(\n                stringToSerializedBufferFormat(viewFormat, areaValue),\n                viewFormat,\n              )?.isValid\n            }\n          >\n            <TextArea\n              name=\"value\"\n              id=\"value\"\n              rows={rows}\n              placeholder={config.value.placeholder}\n              value={areaValue}\n              onChange={setAreaValue}\n              disabled={loading}\n              ref={textAreaRef}\n              height=\"100%\"\n              data-testid=\"string-value\"\n            />\n          </InlineItemEditor>\n        )}\n      </div>\n\n      {length > MAX_LENGTH && (\n        <div className=\"key-details-footer\" key=\"key-details-footer\">\n          <Row justify=\"between\" align=\"center\">\n            <FlexItem>\n              {!isFullStringLoaded(initialValue?.data?.length, length) && (\n                <SecondaryButton\n                  className={styles.stringFooterBtn}\n                  size=\"small\"\n                  data-testid=\"load-all-value-btn\"\n                  onClick={() => handleLoadAll(key, keyType)}\n                >\n                  Load all\n                </SecondaryButton>\n              )}\n            </FlexItem>\n            {!isTruncatedValue && (\n              <FlexItem>\n                <SecondaryButton\n                  className={styles.stringFooterBtn}\n                  size=\"small\"\n                  icon={DownloadIcon}\n                  iconSide=\"right\"\n                  data-testid=\"download-all-value-btn\"\n                  onClick={handleDownloadString}\n                  disabled={isTruncatedValue}\n                >\n                  Download\n                </SecondaryButton>\n              </FlexItem>\n            )}\n          </Row>\n        </div>\n      )}\n    </>\n  )\n}\n\nexport { StringDetailsValue }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-value/index.ts",
    "content": "export { StringDetailsValue } from './StringDetailsValue'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/string-details/string-details-value/styles.module.scss",
    "content": "$outer-height: 220px;\n$outer-height-mobile: 340px;\n\n.container {\n  padding: 20px 16px 20px;\n  max-height: calc(100vh - #{$outer-height});\n\n  overflow: hidden;\n  color: var(--euiTextSubduedColor);\n  flex: 1;\n  position: relative;\n\n  @media only screen and (max-width: 767px) {\n    max-height: calc(100vh - #{$outer-height-mobile});\n  }\n\n  > div {\n    line-height: 1.19 !important;\n    max-height: 100%;\n  }\n}\n\n.stringValue {\n  @include eui.scrollBar;\n  overflow-y: auto;\n  overflow-x: hidden;\n  word-break: break-word;\n  line-height: 1.2;\n  width: 100%;\n\n  pre {\n    background-color: transparent !important;\n    padding: 0 !important;\n  }\n}\n\n.tooltipAnchor {\n  display: inline-flex !important;\n  height: auto;\n  max-height: 100%;\n  width: 100%;\n}\n\n.stringFooterBtn {\n  &:global(.euiButton) {\n    color: var(--euiTextSubduedColor) !important;\n    font-size: 13px;\n    height: auto !important;\n    padding: 4px 8px;\n\n    :global(.euiButton__text) {\n      font-weight: 400 !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/text-details-wrapper/TextDetailsWrapper.spec.tsx",
    "content": "import React from 'react'\nimport TextDetailsWrapper from './TextDetailsWrapper'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\ndescribe('TextDetailsWrapper', () => {\n  it('should render children correctly', () => {\n    const { queryByTestId } = render(\n      <TextDetailsWrapper onClose={jest.fn()}>\n        <div data-testid=\"children-wrapper\">Children</div>\n      </TextDetailsWrapper>,\n    )\n\n    expect(queryByTestId('children-wrapper')).toBeInTheDocument()\n  })\n\n  it('should call onClose when close button is clicked', () => {\n    const mockOnClose = jest.fn()\n\n    render(\n      <TextDetailsWrapper onClose={mockOnClose}>\n        <div data-testid=\"children-wrapper\">Children</div>\n      </TextDetailsWrapper>,\n    )\n\n    fireEvent.click(screen.getByTestId('close-key-btn'))\n\n    expect(mockOnClose).toBeCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/text-details-wrapper/TextDetailsWrapper.tsx",
    "content": "import React, { ReactNode } from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport styles from './styles.module.scss'\n\nconst TextDetailsWrapper = ({\n  onClose,\n  children,\n  testid,\n}: {\n  onClose: () => void\n  children: ReactNode\n  testid?: string\n}) => {\n  const getDataTestid = (suffix: string) =>\n    testid ? `${testid}-${suffix}` : suffix\n\n  return (\n    <div className={styles.container} data-testid={getDataTestid('details')}>\n      <RiTooltip\n        content=\"Close\"\n        position=\"left\"\n        anchorClassName={styles.closeRightPanel}\n      >\n        <IconButton\n          icon={CancelSlimIcon}\n          aria-label=\"Close key\"\n          className={styles.closeBtn}\n          onClick={() => onClose()}\n          data-testid={getDataTestid('close-key-btn')}\n        />\n      </RiTooltip>\n      <Row centered>\n        <FlexItem className={styles.textWrapper}>\n          <div>{children}</div>\n        </FlexItem>\n      </Row>\n    </div>\n  )\n}\n\nexport default TextDetailsWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/text-details-wrapper/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex-grow: 1;\n  padding: 16px 40px 30px;\n\n  text-align: center;\n\n  h4 {\n    font-size: 18px;\n    font-weight: normal;\n    margin-bottom: 18px;\n    line-height: 24px;\n  }\n}\n\n.textWrapper {\n  max-width: 640px;\n  position: relative;\n  top: -7%;\n}\n\n.closeRightPanel {\n  position: absolute;\n  top: 22px;\n  right: 18px;\n\n  .closeBtn {\n    :global(svg) {\n      width: 20px;\n      height: 20px;\n    }\n  }\n}"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/too-long-key-name-details/TooLongKeyNameDetails.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport TooLongKeyNameDetails from './TooLongKeyNameDetails'\n\ndescribe('TooLongKeyNameDetails', () => {\n  it('should render', () => {\n    expect(render(<TooLongKeyNameDetails onClose={jest.fn()} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/too-long-key-name-details/TooLongKeyNameDetails.tsx",
    "content": "import React from 'react'\n\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport TextDetailsWrapper from '../text-details-wrapper/TextDetailsWrapper'\n\nconst TooLongKeyNameDetails = ({ onClose }: { onClose: () => void }) => (\n  <TextDetailsWrapper onClose={onClose} testid=\"too-long-key-name\">\n    <Title size=\"M\">The key name is too long</Title>\n    <Text size=\"s\">Details cannot be displayed.</Text>\n  </TextDetailsWrapper>\n)\n\nexport default TooLongKeyNameDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/UnsupportedTypeDetails.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport UnsupportedTypeDetails from './UnsupportedTypeDetails'\n\ndescribe('UnsupportedTypeDetails', () => {\n  it('should render', () => {\n    expect(render(<UnsupportedTypeDetails onClose={jest.fn()} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/UnsupportedTypeDetails.tsx",
    "content": "import React from 'react'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport TextDetailsWrapper from '../text-details-wrapper/TextDetailsWrapper'\n\nimport styles from './styles.module.scss'\n\nconst UnsupportedTypeDetails = ({ onClose }: { onClose: () => void }) => (\n  <TextDetailsWrapper onClose={onClose} testid=\"unsupported-type\">\n    <Title size=\"M\">This key type is not currently supported.</Title>\n    <Text size=\"s\">\n      See{' '}\n      <a\n        href={EXTERNAL_LINKS.githubRepo}\n        className={styles.link}\n        target=\"_blank\"\n        rel=\"noreferrer\"\n      >\n        our repository\n      </a>{' '}\n      for the list of supported key types.\n    </Text>\n  </TextDetailsWrapper>\n)\n\nexport default UnsupportedTypeDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/unsupported-type-details/styles.module.scss",
    "content": ".link {\n  text-decoration: underline;\n  color: var(--euiColorFullShade) !important;\n}"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, ZSetDetails } from './ZSetDetails'\n\nconst mockedProps = mock<Props>()\n\ndescribe('ZSetDetails', () => {\n  it('should render', () => {\n    expect(render(<ZSetDetails {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/ZSetDetails.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { selectedKeySelector } from 'uiSrc/slices/browser/keys'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport {\n  KeyDetailsHeader,\n  KeyDetailsHeaderProps,\n} from 'uiSrc/pages/browser/modules'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { ZSetDetailsTable } from './zset-details-table'\nimport AddZsetMembers from './add-zset-members/AddZsetMembers'\nimport { AddItemsAction } from '../key-details-actions'\nimport { KeyDetailsSubheader } from '../key-details-subheader/KeyDetailsSubheader'\nimport { AddKeysContainer } from '../common/AddKeysContainer.styled'\n\nexport interface Props extends KeyDetailsHeaderProps {\n  onRemoveKey: () => void\n  onOpenAddItemPanel: () => void\n  onCloseAddItemPanel: () => void\n}\n\nconst ZSetDetails = (props: Props) => {\n  const keyType = KeyTypes.ZSet\n  const { onRemoveKey, onOpenAddItemPanel, onCloseAddItemPanel } = props\n\n  const { loading } = useSelector(selectedKeySelector)\n\n  const [isAddItemPanelOpen, setIsAddItemPanelOpen] = useState<boolean>(false)\n\n  const openAddItemPanel = () => {\n    setIsAddItemPanelOpen(true)\n    onOpenAddItemPanel()\n  }\n\n  const closeAddItemPanel = (isCancelled?: boolean) => {\n    setIsAddItemPanelOpen(false)\n\n    if (isCancelled) {\n      onCloseAddItemPanel()\n    }\n  }\n\n  const Actions = ({ width }: { width: number }) => (\n    <AddItemsAction\n      title=\"Add Members\"\n      width={width}\n      openAddItemPanel={openAddItemPanel}\n    />\n  )\n\n  return (\n    <Col className=\"fluid relative\" justify=\"between\">\n      <KeyDetailsHeader {...props} key=\"key-details-header\" />\n      <KeyDetailsSubheader keyType={keyType} Actions={Actions} />\n      <FlexItem\n        grow\n        className=\"key-details-body\"\n        key=\"key-details-body\"\n        style={{ height: 300 }} // a hack to make flex-item grow to fill parent and not overflow\n      >\n        {!loading && (\n          <FlexItem grow style={{ height: '100%' }}>\n            <ZSetDetailsTable onRemoveKey={onRemoveKey} />\n          </FlexItem>\n        )}\n        {isAddItemPanelOpen && (\n          <AddKeysContainer>\n            <AddZsetMembers closePanel={closeAddItemPanel} />\n          </AddKeysContainer>\n        )}\n      </FlexItem>\n    </Col>\n  )\n}\n\nexport { ZSetDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport AddZsetMembers, { Props } from './AddZsetMembers'\n\nconst MEMBER_NAME = 'member-name'\nconst MEMBER_SCORE = 'member-score'\n\nconst mockedProps = mock<Props>()\n\ndescribe('AddZsetMembers', () => {\n  it('should render', () => {\n    expect(render(<AddZsetMembers {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should set member value properly', () => {\n    render(<AddZsetMembers {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId(MEMBER_NAME)\n    fireEvent.change(memberInput, { target: { value: 'member name' } })\n    expect(memberInput).toHaveValue('member name')\n  })\n\n  it('should set score value properly if input wrong value', () => {\n    render(<AddZsetMembers {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    expect(scoreInput).toHaveValue('100')\n  })\n\n  it('should set by blur score value properly if input wrong value', () => {\n    render(<AddZsetMembers {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '.1' } })\n    fireEvent.focusOut(scoreInput)\n    expect(scoreInput).toHaveValue('0.1')\n  })\n\n  it('should render add button after input score', () => {\n    render(<AddZsetMembers {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    expect(screen.getByTestId('add-item')).toBeTruthy()\n  })\n\n  it('should render one more member & score inputs after click add item', () => {\n    render(<AddZsetMembers {...instance(mockedProps)} />)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    fireEvent.click(screen.getByTestId('add-item'))\n\n    expect(screen.getAllByTestId(MEMBER_NAME)).toHaveLength(2)\n    expect(screen.getAllByTestId(MEMBER_SCORE)).toHaveLength(2)\n  })\n\n  it('should clear member & score after click clear button', () => {\n    render(<AddZsetMembers {...instance(mockedProps)} />)\n    const memberInput = screen.getByTestId(MEMBER_NAME)\n    const scoreInput = screen.getByTestId(MEMBER_SCORE)\n    fireEvent.change(memberInput, { target: { value: 'member' } })\n    fireEvent.change(scoreInput, { target: { value: '100q' } })\n    fireEvent.click(screen.getByTestId('remove-item'))\n\n    expect(memberInput).toHaveValue('')\n    expect(scoreInput).toHaveValue('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/add-zset-members/AddZsetMembers.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { toNumber } from 'lodash'\n\nimport { stringToBuffer, validateScoreNumber } from 'uiSrc/utils'\nimport { isNaNConvertedString } from 'uiSrc/utils/numbers'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\nimport {\n  fetchAddZSetMembers,\n  resetUpdateScore,\n  updateZsetScoreStateSelector,\n} from 'uiSrc/slices/browser/zset'\n\nimport { AddZsetFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport {\n  INITIAL_ZSET_MEMBER_STATE,\n  IZsetMemberState,\n} from 'uiSrc/pages/browser/components/add-key/AddKeyZset/interfaces'\nimport AddMultipleFields from 'uiSrc/pages/browser/components/add-multiple-fields'\nimport { ISetMemberState } from 'uiSrc/pages/browser/components/add-key/AddKeySet/interfaces'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { TextInput } from 'uiSrc/components/base/inputs'\n\nimport { EntryContent } from '../../common/AddKeysContainer.styled'\n\nexport interface Props {\n  closePanel: (isCancelled?: boolean) => void\n}\n\nconst AddZsetMembers = (props: Props) => {\n  const { closePanel } = props\n  const dispatch = useDispatch()\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n  const [members, setMembers] = useState<IZsetMemberState[]>([\n    { ...INITIAL_ZSET_MEMBER_STATE },\n  ])\n  const { loading } = useSelector(updateZsetScoreStateSelector)\n  const { name: selectedKey = '' } = useSelector(selectedKeyDataSelector) ?? {\n    name: undefined,\n  }\n  const lastAddedMemberName = useRef<HTMLInputElement>(null)\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        dispatch(resetUpdateScore())\n      },\n    [],\n  )\n\n  useEffect(() => {\n    members.every((member) => {\n      if (!member.score?.toString().length) {\n        setIsFormValid(false)\n        return false\n      }\n\n      if (!isNaNConvertedString(member.score)) {\n        setIsFormValid(true)\n        return true\n      }\n\n      setIsFormValid(false)\n      return false\n    })\n  }, [members])\n\n  useEffect(() => {\n    lastAddedMemberName.current?.focus()\n  }, [members.length])\n\n  const addMember = () => {\n    const lastField = members[members.length - 1]\n    const newState = [\n      ...members,\n      {\n        ...INITIAL_ZSET_MEMBER_STATE,\n        id: lastField.id + 1,\n      },\n    ]\n    setMembers(newState)\n  }\n\n  const removeMember = (id: number) => {\n    const newState = members.filter((item) => item.id !== id)\n    setMembers(newState)\n  }\n\n  const clearMemberValues = (id: number) => {\n    const newState = members.map((item) =>\n      item.id === id\n        ? {\n            ...item,\n            name: '',\n            score: '',\n          }\n        : item,\n    )\n    setMembers(newState)\n  }\n\n  const onClickRemove = ({ id }: ISetMemberState) => {\n    if (members.length === 1) {\n      clearMemberValues(id)\n      return\n    }\n\n    removeMember(id)\n  }\n\n  const validateScore = (value: any) => {\n    const validatedValue = validateScoreNumber(value)\n    return validatedValue.toString().length ? validatedValue : ''\n  }\n\n  const handleScoreBlur = (item: IZsetMemberState) => {\n    const { score } = item\n    const newState = members.map((currentItem) => {\n      if (currentItem.id !== item.id) {\n        return currentItem\n      }\n      if (isNaNConvertedString(score)) {\n        return {\n          ...currentItem,\n          score: '',\n        }\n      }\n      if (score.length) {\n        return {\n          ...currentItem,\n          score: toNumber(score).toString(),\n        }\n      }\n      return currentItem\n    })\n    setMembers(newState)\n  }\n\n  const handleMemberChange = (formField: string, id: number, value: any) => {\n    let validatedValue = value\n    if (formField === 'score') {\n      validatedValue = validateScore(value)\n    }\n\n    const newState = members.map((item) => {\n      if (item.id === id) {\n        return {\n          ...item,\n          [formField]: validatedValue,\n        }\n      }\n      return item\n    })\n    setMembers(newState)\n  }\n\n  const submitData = (): void => {\n    const data = {\n      keyName: selectedKey,\n      members: members.map((item) => ({\n        name: stringToBuffer(item.name),\n        score: toNumber(item.score),\n      })),\n    }\n    dispatch(fetchAddZSetMembers(data, closePanel))\n  }\n\n  const isClearDisabled = (item: IZsetMemberState): boolean =>\n    members.length === 1 && !(item.name.length || item.score.length)\n\n  return (\n    <Col gap=\"m\">\n      <EntryContent>\n        <AddMultipleFields\n          items={members}\n          isClearDisabled={isClearDisabled}\n          onClickRemove={onClickRemove}\n          onClickAdd={addMember}\n        >\n          {(item, index) => (\n            <Row align=\"center\" gap=\"m\">\n              <FlexItem grow>\n                <FormField>\n                  <TextInput\n                    name={`member-${item.id}`}\n                    id={`member-${item.id}`}\n                    placeholder={config.member.placeholder}\n                    value={item.name}\n                    onChange={(value) =>\n                      handleMemberChange('name', item.id, value)\n                    }\n                    ref={\n                      index === members.length - 1 ? lastAddedMemberName : null\n                    }\n                    disabled={loading}\n                    data-testid=\"member-name\"\n                  />\n                </FormField>\n              </FlexItem>\n              <FlexItem grow>\n                <FormField>\n                  <TextInput\n                    name={`score-${item.id}`}\n                    id={`score-${item.id}`}\n                    maxLength={200}\n                    placeholder={config.score.placeholder}\n                    value={item.score}\n                    onChange={(value) =>\n                      handleMemberChange('score', item.id, value)\n                    }\n                    onBlur={() => {\n                      handleScoreBlur(item)\n                    }}\n                    disabled={loading}\n                    data-testid=\"member-score\"\n                  />\n                </FormField>\n              </FlexItem>\n            </Row>\n          )}\n        </AddMultipleFields>\n      </EntryContent>\n      <Row justify=\"end\" gap=\"l\">\n        <FlexItem>\n          <div>\n            <SecondaryButton\n              onClick={() => closePanel(true)}\n              data-testid=\"cancel-members-btn\"\n            >\n              Cancel\n            </SecondaryButton>\n          </div>\n        </FlexItem>\n        <FlexItem>\n          <div>\n            <PrimaryButton\n              disabled={loading || !isFormValid}\n              loading={loading}\n              onClick={submitData}\n              data-testid=\"save-members-btn\"\n            >\n              Save\n            </PrimaryButton>\n          </div>\n        </FlexItem>\n      </Row>\n    </Col>\n  )\n}\n\nexport default AddZsetMembers\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/index.ts",
    "content": "export { ZSetDetails } from './ZSetDetails'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport { zsetDataSelector } from 'uiSrc/slices/browser/zset'\nimport { anyToBuffer } from 'uiSrc/utils'\nimport {\n  render,\n  screen,\n  fireEvent,\n  act,\n  mockedStore,\n  cleanup,\n  waitForRiPopoverVisible,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  DECOMPRESSED_VALUE_STR_1,\n} from 'uiSrc/utils/tests/decompressors'\nimport { setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys'\nimport { MOCK_TRUNCATED_BUFFER_VALUE } from 'uiSrc/mocks/data/bigString'\nimport { TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA } from 'uiSrc/constants'\nimport { ZSetDetailsTable, Props } from './ZSetDetailsTable'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/browser/zset', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/browser/zset',\n  ).initialState\n  return {\n    zsetSelector: jest.fn().mockReturnValue(defaultState),\n    setZsetInitialState: jest.fn,\n    zsetDataSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n      total: 4,\n      key: 'z',\n      keyName: 'z',\n      members: [\n        { name: { type: 'Buffer', data: [49] }, score: 1 },\n        { name: { type: 'Buffer', data: [50] }, score: 2 },\n        { name: { type: 'Buffer', data: [51] }, score: 3 },\n        { name: { type: 'Buffer', data: [52] }, score: 'inf' },\n      ],\n    }),\n    updateZsetScoreStateSelector: jest\n      .fn()\n      .mockReturnValue(defaultState.updateScore),\n    fetchSearchZSetMembers: () => jest.fn(),\n  }\n})\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ZSetDetailsTable', () => {\n  it('should render', () => {\n    expect(render(<ZSetDetailsTable {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render search input', () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n    expect(screen.getByPlaceholderText(/search/i)).toBeTruthy()\n  })\n\n  it('should call search', () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n    const searchInput = screen.getByPlaceholderText(/search/i)\n    fireEvent.change(searchInput, { target: { value: '*' } })\n    expect(searchInput).toHaveValue('*')\n  })\n\n  it('should render delete popup after click remove button', async () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('zset-remove-button-1-icon'))\n    await waitForRiPopoverVisible()\n    expect(screen.getByTestId('zset-remove-button-1')).toBeInTheDocument()\n  })\n\n  it('should render disabled edit button', () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('zset_content-value-3'))\n    })\n\n    expect(screen.getByTestId('zset_edit-btn-3')).toBeDisabled()\n  })\n\n  it('should render enabled edit button', () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('zset_content-value-2'))\n    })\n\n    expect(screen.getByTestId('zset_edit-btn-2')).not.toBeDisabled()\n  })\n\n  it('should render editor after click edit button and able to change value', () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('zset_content-value-2'))\n    })\n\n    fireEvent.click(screen.getByTestId('zset_edit-btn-2'))\n\n    fireEvent.change(screen.getByTestId('inline-item-editor'), {\n      target: { value: '123' },\n    })\n    expect(screen.getByTestId('inline-item-editor')).toHaveValue('123')\n  })\n\n  it('should render resize trigger for name column', () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n    expect(screen.getByTestId('resize-trigger-name')).toBeInTheDocument()\n  })\n\n  it('should disable refresh when editing', async () => {\n    render(<ZSetDetailsTable {...instance(mockedProps)} />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('zset_content-value-2'))\n    })\n\n    fireEvent.click(screen.getByTestId('zset_edit-btn-2'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setSelectedKeyRefreshDisabled(true),\n    ])\n  })\n\n  describe('decompressed  data', () => {\n    it('should render decompressed GZIP data = \"1\"', () => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/zset',\n      ).initialState\n      const zsetDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        key: '123zxczxczxc',\n        members: [{ name: anyToBuffer(GZIP_COMPRESSED_VALUE_1), score: 1 }],\n      })\n      ;(zsetDataSelector as jest.Mock).mockImplementation(zsetDataSelectorMock)\n\n      const { queryByTestId } = render(\n        <ZSetDetailsTable {...instance(mockedProps)} />,\n      )\n      const memberEl = queryByTestId(/zset-member-value-/)\n\n      expect(memberEl).toHaveTextContent(DECOMPRESSED_VALUE_STR_1)\n    })\n  })\n\n  describe('truncated data', () => {\n    beforeEach(() => {\n      const defaultState = jest.requireActual(\n        'uiSrc/slices/browser/zset',\n      ).initialState\n      const zsetDataSelectorMock = jest.fn().mockReturnValue({\n        ...defaultState,\n        key: '123zxczxczxc',\n        members: [{ name: MOCK_TRUNCATED_BUFFER_VALUE, score: 1 }],\n      })\n      ;(zsetDataSelector as jest.Mock).mockImplementation(zsetDataSelectorMock)\n    })\n\n    it('should not be able to edit when member name is truncated', async () => {\n      render(<ZSetDetailsTable {...instance(mockedProps)} />)\n      const score = screen.getByTestId(/zset_content-value-/)\n\n      await act(async () => {\n        fireEvent.mouseOver(score)\n      })\n\n      const scoreEditButton = screen.getByTestId(/zset_edit-btn-/)\n\n      await act(async () => {\n        fireEvent.focus(scoreEditButton)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getByTestId(/zset_edit-tooltip-/)).toHaveTextContent(\n        TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n      )\n    })\n\n    it('should not be able to delete when member name is truncated', async () => {\n      render(<ZSetDetailsTable {...instance(mockedProps)} />)\n      const removeButton = screen.getByTestId(/zset-remove-button-/)\n\n      expect(removeButton).toBeDisabled()\n\n      await act(async () => {\n        fireEvent.focus(removeButton)\n      })\n      await waitForRiTooltipVisible()\n\n      expect(\n        screen.getByTestId(/zset-remove-button-.+-tooltip$/),\n      ).toHaveTextContent(TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/ZSetDetailsTable.tsx",
    "content": "import React, { Ref, useCallback, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { toNumber, isNumber } from 'lodash'\nimport cx from 'classnames'\nimport { CellMeasurerCache } from 'react-virtualized'\nimport {\n  appContextBrowserKeyDetails,\n  updateKeyDetailsSizes,\n} from 'uiSrc/slices/app/context'\n\nimport {\n  zsetSelector,\n  fetchZSetMembers,\n  fetchMoreZSetMembers,\n  zsetDataSelector,\n  deleteZSetMembers,\n  updateZsetScoreStateSelector,\n  updateZSetMembers,\n  fetchSearchZSetMembers,\n  fetchSearchMoreZSetMembers,\n} from 'uiSrc/slices/browser/zset'\nimport {\n  KeyTypes,\n  OVER_RENDER_BUFFER_COUNT,\n  SortOrder,\n  TEXT_FAILED_CONVENT_FORMATTER,\n  TableCellAlignment,\n  TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA,\n} from 'uiSrc/constants'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport HelpTexts from 'uiSrc/constants/help-texts'\nimport { NoResultsFoundText } from 'uiSrc/constants/texts'\nimport {\n  selectedKeyDataSelector,\n  keysSelector,\n  selectedKeySelector,\n  setSelectedKeyRefreshDisabled,\n} from 'uiSrc/slices/browser/keys'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\nimport { ZsetMember } from 'uiSrc/slices/interfaces/zset'\nimport {\n  bufferToString,\n  createDeleteFieldHeader,\n  createDeleteFieldMessage,\n  createTooltipContent,\n  formatLongName,\n  formattingBuffer,\n  isTruncatedString,\n  isEqualBuffers,\n  validateScoreNumber,\n} from 'uiSrc/utils'\nimport {\n  sendEventTelemetry,\n  TelemetryEvent,\n  getBasedOnViewTypeEvent,\n  getMatchType,\n} from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport VirtualTable from 'uiSrc/components/virtual-table/VirtualTable'\nimport {\n  IColumnSearchState,\n  ITableColumn,\n  RelativeWidthSizes,\n} from 'uiSrc/components/virtual-table/interfaces'\nimport { StopPropagation } from 'uiSrc/components/virtual-table'\nimport { getColumnWidth } from 'uiSrc/components/virtual-grid'\nimport { decompressingBuffer } from 'uiSrc/utils/decompressors'\nimport {\n  EditableInput,\n  FormattedValue,\n} from 'uiSrc/pages/browser/modules/key-details/shared'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport {\n  AddMembersToZSetDto,\n  SearchZSetMembersResponse,\n} from 'apiSrc/modules/browser/z-set/dto'\n\nimport styles from './styles.module.scss'\n\nconst suffix = '_zset'\nconst headerHeight = 60\nconst rowHeight = 43\n\nconst cellCache = new CellMeasurerCache({\n  fixedWidth: true,\n  minHeight: rowHeight,\n})\n\ninterface IZsetMember extends ZsetMember {\n  editing: boolean\n}\n\nexport interface Props {\n  onRemoveKey: () => void\n}\n\nconst ZSetDetailsTable = (props: Props) => {\n  const { onRemoveKey } = props\n\n  const { loading, searching } = useSelector(zsetSelector)\n  const { loading: updateLoading } = useSelector(updateZsetScoreStateSelector)\n  const [sortedColumnOrder, setSortedColumnOrder] = useState(SortOrder.ASC)\n  const { name: key, length } = useSelector(selectedKeyDataSelector) ?? {\n    name: '',\n  }\n  const {\n    total,\n    nextCursor,\n    members: loadedMembers,\n  } = useSelector(zsetDataSelector)\n  const { id: instanceId, compressor = null } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { viewType } = useSelector(keysSelector)\n  const { viewFormat: viewFormatProp } = useSelector(selectedKeySelector)\n  const { [KeyTypes.ZSet]: ZSetSizes } = useSelector(\n    appContextBrowserKeyDetails,\n  )\n\n  const [match, setMatch] = useState<string>('')\n  const [deleting, setDeleting] = useState('')\n  const [members, setMembers] = useState<IZsetMember[]>([])\n  const [sortedColumnName, setSortedColumnName] = useState('score')\n  const [width, setWidth] = useState(100)\n  const [expandedRows, setExpandedRows] = useState<number[]>([])\n  const [viewFormat, setViewFormat] = useState(viewFormatProp)\n\n  const dispatch = useDispatch()\n\n  const formattedLastIndexRef = useRef(OVER_RENDER_BUFFER_COUNT)\n  const tableRef: Ref<any> = useRef(null)\n\n  useEffect(() => {\n    const newMembers = loadedMembers.map((item) => ({\n      ...item,\n      editing: false,\n    }))\n\n    setMembers(newMembers)\n\n    if (loadedMembers.length < members.length) {\n      formattedLastIndexRef.current = 0\n    }\n\n    if (viewFormat !== viewFormatProp) {\n      setExpandedRows([])\n      setViewFormat(viewFormatProp)\n\n      clearCache()\n    }\n  }, [loadedMembers, viewFormatProp])\n\n  const clearCache = (rowIndex?: number) => {\n    if (isNumber(rowIndex)) {\n      cellCache.clear(rowIndex, 1)\n      tableRef.current?.recomputeRowHeights(rowIndex)\n      return\n    }\n\n    cellCache.clearAll()\n  }\n\n  const closePopover = useCallback(() => {\n    setDeleting('')\n  }, [])\n\n  const showPopover = useCallback((member = '') => {\n    setDeleting(`${member + suffix}`)\n  }, [])\n\n  const onSuccessRemoved = (newTotal: number) => {\n    newTotal === 0 && onRemoveKey()\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.ZSet,\n        numberOfRemoved: 1,\n      },\n    })\n  }\n\n  const handleDeleteMember = (member: RedisString | string = '') => {\n    dispatch(deleteZSetMembers(key, [member], onSuccessRemoved))\n    closePopover()\n  }\n\n  const handleEditMember = (\n    name: RedisResponseBuffer,\n    editing: boolean,\n    index?: number,\n  ) => {\n    const newMemberState = members.map((item) => {\n      if (isEqualBuffers(item.name, name)) {\n        return { ...item, editing }\n      }\n      return item\n    })\n    setMembers(newMemberState)\n    dispatch(setSelectedKeyRefreshDisabled(editing))\n\n    clearCache(index)\n  }\n\n  const handleApplyEditScore = (\n    name: RedisResponseBuffer,\n    score: string = '',\n  ) => {\n    const data: AddMembersToZSetDto = {\n      keyName: key,\n      members: [\n        {\n          name,\n          score: toNumber(score),\n        },\n      ],\n    }\n    dispatch(updateZSetMembers(data, () => handleEditMember(name, false)))\n  }\n\n  const handleRemoveIconClick = () => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_VALUE_REMOVE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_VALUE_REMOVE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: KeyTypes.ZSet,\n      },\n    })\n  }\n\n  const handleSearch = (search: IColumnSearchState[]) => {\n    const fieldColumn = search.find((column) => column.id === 'name')\n    if (!fieldColumn) {\n      return\n    }\n\n    const { value: match } = fieldColumn\n    const onSuccess = (data: SearchZSetMembersResponse) => {\n      const matchValue = getMatchType(match)\n      sendEventTelemetry({\n        event: getBasedOnViewTypeEvent(\n          viewType,\n          TelemetryEvent.BROWSER_KEY_VALUE_FILTERED,\n          TelemetryEvent.TREE_VIEW_KEY_VALUE_FILTERED,\n        ),\n        eventData: {\n          databaseId: instanceId,\n          keyType: KeyTypes.ZSet,\n          match: matchValue,\n          length: data.total,\n        },\n      })\n    }\n    setMatch(match)\n    if (match === '') {\n      dispatch(fetchZSetMembers(key, 0, SCAN_COUNT_DEFAULT, sortedColumnOrder))\n      return\n    }\n    dispatch(\n      fetchSearchZSetMembers(key, 0, SCAN_COUNT_DEFAULT, match, onSuccess),\n    )\n  }\n\n  const handleRowToggleViewClick = (expanded: boolean, rowIndex: number) => {\n    const browserViewEvent = expanded\n      ? TelemetryEvent.BROWSER_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.BROWSER_KEY_FIELD_VALUE_COLLAPSED\n    const treeViewEvent = expanded\n      ? TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_EXPANDED\n      : TelemetryEvent.TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED\n\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        keyType: KeyTypes.ZSet,\n        databaseId: instanceId,\n        largestCellLength: members[rowIndex]?.name?.length || 0,\n      },\n    })\n\n    cellCache.clearAll()\n  }\n\n  const onColResizeEnd = (sizes: RelativeWidthSizes) => {\n    dispatch(\n      updateKeyDetailsSizes({\n        type: KeyTypes.ZSet,\n        sizes,\n      }),\n    )\n  }\n\n  const columns: ITableColumn[] = [\n    {\n      id: 'name',\n      label: 'Member',\n      isSearchable: true,\n      prependSearchName: 'Member:',\n      initialSearchValue: '',\n      truncateText: true,\n      isResizable: true,\n      minWidth: 140,\n      relativeWidth: ZSetSizes?.name || 60,\n      alignment: TableCellAlignment.Left,\n      className: 'value-table-separate-border',\n      headerClassName: 'value-table-separate-border',\n      render: function Name(\n        _name: string,\n        { name: nameItem }: IZsetMember,\n        expanded?: boolean,\n      ) {\n        const { value: decompressedNameItem } = decompressingBuffer(\n          nameItem,\n          compressor,\n        )\n        const name = bufferToString(nameItem)\n        const { value, isValid } = formattingBuffer(\n          decompressedNameItem,\n          viewFormat,\n          { expanded },\n        )\n        const tooltipContent = createTooltipContent(\n          value,\n          decompressedNameItem,\n          viewFormat,\n        )\n\n        return (\n          <Text\n            color=\"secondary\"\n            component=\"div\"\n            style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}\n          >\n            <div\n              style={{ display: 'flex' }}\n              data-testid={`zset-member-value-${name}`}\n            >\n              <FormattedValue\n                value={value}\n                expanded={expanded}\n                title={\n                  isValid\n                    ? 'Member'\n                    : TEXT_FAILED_CONVENT_FORMATTER(viewFormatProp)\n                }\n                tooltipContent={tooltipContent}\n              />\n            </div>\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'score',\n      label: 'Score',\n      minWidth: 154,\n      isSortable: true,\n      truncateText: true,\n      className: 'noPadding',\n      render: function Score(\n        _name: string,\n        { name: nameItem, score, editing }: IZsetMember,\n        expanded?: boolean,\n        rowIndex?: number,\n      ) {\n        const cellContent = score.toString().substring(0, 200)\n        const tooltipContent = formatLongName(score.toString())\n        const isTruncatedValue = isTruncatedString(nameItem)\n        const isEditable = isNumber(score) && !isTruncatedValue\n        const editToolTipContent = !isNumber(score)\n          ? 'Use CLI or Workbench to edit the score'\n          : TEXT_DISABLED_ACTION_WITH_TRUNCATED_DATA\n\n        return (\n          <EditableInput\n            initialValue={score.toString()}\n            placeholder=\"Enter Score\"\n            field={rowIndex?.toString()}\n            editToolTipContent={!isEditable ? editToolTipContent : null}\n            isEditing={editing}\n            isEditDisabled={updateLoading || !isEditable}\n            onEdit={(value: boolean) =>\n              handleEditMember(nameItem, value, rowIndex)\n            }\n            onDecline={() => handleEditMember(nameItem, false, rowIndex)}\n            onApply={(value) => handleApplyEditScore(nameItem, value)}\n            validation={validateScoreNumber}\n            testIdPrefix=\"zset\"\n          >\n            <div className=\"innerCellAsCell\">\n              {!expanded && (\n                <RiTooltip\n                  title=\"Score\"\n                  className={styles.tooltip}\n                  anchorClassName=\"truncateText\"\n                  position=\"bottom\"\n                  content={tooltipContent}\n                >\n                  <>{cellContent}</>\n                </RiTooltip>\n              )}\n              {expanded && score}\n            </div>\n          </EditableInput>\n        )\n      },\n    },\n    {\n      id: 'actions',\n      label: '',\n      headerClassName: 'value-table-header-actions',\n      className: 'actions singleAction',\n      minWidth: 40,\n      maxWidth: 40,\n      absoluteWidth: 40,\n      render: function Actions(_act: any, { name: nameItem }: IZsetMember) {\n        const name = bufferToString(nameItem, viewFormat)\n        return (\n          <StopPropagation>\n            <div className=\"value-table-actions\">\n              <PopoverDelete\n                header={createDeleteFieldHeader(nameItem)}\n                text={createDeleteFieldMessage(key ?? '')}\n                item={name}\n                itemRaw={nameItem}\n                suffix={suffix}\n                deleting={deleting}\n                closePopover={closePopover}\n                updateLoading={false}\n                showPopover={showPopover}\n                handleDeleteItem={handleDeleteMember}\n                handleButtonClick={handleRemoveIconClick}\n                testid={`zset-remove-button-${name}`}\n                appendInfo={\n                  length === 1 ? HelpTexts.REMOVE_LAST_ELEMENT() : null\n                }\n              />\n            </div>\n          </StopPropagation>\n        )\n      },\n    },\n  ]\n\n  const onChangeSorting = (column: any, order: SortOrder) => {\n    setSortedColumnName(column)\n    setSortedColumnOrder(order)\n\n    dispatch(fetchZSetMembers(key, 0, SCAN_COUNT_DEFAULT, order))\n  }\n\n  const loadMoreItems = ({ startIndex, stopIndex }: any) => {\n    if (!searching) {\n      dispatch(\n        fetchMoreZSetMembers(\n          key,\n          startIndex,\n          stopIndex - startIndex + 1,\n          sortedColumnOrder,\n        ),\n      )\n      return\n    }\n    if (nextCursor !== 0) {\n      dispatch(\n        fetchSearchMoreZSetMembers(key, nextCursor, SCAN_COUNT_DEFAULT, match),\n      )\n    }\n  }\n\n  const sortedColumn = {\n    column: sortedColumnName,\n    order: sortedColumnOrder,\n  }\n\n  return (\n    <>\n      <div\n        data-testid=\"zset-details\"\n        className={cx(\n          'key-details-table',\n          'hash-fields-container',\n          styles.container,\n        )}\n      >\n        <VirtualTable\n          autoHeight\n          tableRef={tableRef}\n          expandable\n          keyName={key}\n          headerHeight={headerHeight}\n          rowHeight={rowHeight}\n          onChangeWidth={setWidth}\n          columns={columns.map((column, i, arr) => ({\n            ...column,\n            width: getColumnWidth(i, width, arr),\n          }))}\n          footerHeight={0}\n          loadMoreItems={loadMoreItems}\n          loading={loading}\n          searching={searching}\n          items={members}\n          sortedColumn={sortedColumn}\n          onChangeSorting={onChangeSorting}\n          totalItemsCount={total}\n          noItemsMessage={NoResultsFoundText}\n          onWheel={closePopover}\n          onSearch={handleSearch}\n          cellCache={cellCache}\n          onRowToggleViewClick={handleRowToggleViewClick}\n          expandedRows={expandedRows}\n          setExpandedRows={setExpandedRows}\n          onColResizeEnd={onColResizeEnd}\n        />\n      </div>\n    </>\n  )\n}\n\nexport { ZSetDetailsTable }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/index.ts",
    "content": "export { ZSetDetailsTable } from './ZSetDetailsTable'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/components/zset-details/zset-details-table/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex: 1;\n  width: 100%;\n  padding: 16px;\n  background-color: var(--euiColorEmptyShade);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/index.ts",
    "content": "import KeyDetails from './KeyDetails'\n\nexport { KeyDetails }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/EditableInput.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport EditableInput, { Props } from './EditableInput'\n\nconst mockedProps = mock<Props>()\nconst Text = () => <span data-testid=\"text\">text</span>\n\ndescribe('EditableInput', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <EditableInput {...mockedProps}>\n          <Text />\n        </EditableInput>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should display editor', () => {\n    render(\n      <EditableInput\n        {...mockedProps}\n        isEditing\n        field=\"field\"\n        testIdPrefix=\"item\"\n        onDecline={jest.fn()}\n      >\n        <Text />\n      </EditableInput>,\n    )\n\n    expect(screen.getByTestId('inline-item-editor')).toBeInTheDocument()\n  })\n\n  it('should call on apply', () => {\n    const onApply = jest.fn()\n    render(\n      <EditableInput\n        {...mockedProps}\n        isEditing\n        field=\"field\"\n        testIdPrefix=\"item\"\n        onEdit={jest.fn()}\n        onDecline={jest.fn()}\n        onApply={onApply}\n      >\n        <Text />\n      </EditableInput>,\n    )\n\n    fireEvent.change(screen.getByTestId('inline-item-editor'), {\n      target: { value: 'value' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    expect(onApply).toBeCalledWith('value', expect.any(Object))\n  })\n\n  it('should call on decline', () => {\n    const onDecline = jest.fn()\n    render(\n      <EditableInput\n        {...mockedProps}\n        isEditing\n        field=\"field\"\n        testIdPrefix=\"item\"\n        onEdit={jest.fn()}\n        onDecline={onDecline}\n      >\n        <Text />\n      </EditableInput>,\n    )\n\n    fireEvent.change(screen.getByTestId('inline-item-editor'), {\n      target: { value: 'value' },\n    })\n    fireEvent.click(screen.getByTestId('cancel-btn'))\n\n    expect(onDecline).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/EditableInput.tsx",
    "content": "import React, { useState } from 'react'\nimport cx from 'classnames'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { StopPropagation } from 'uiSrc/components/virtual-table'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor'\nimport { Props as InlineItemEditorProps } from 'uiSrc/components/inline-item-editor/InlineItemEditor'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  children: React.ReactNode\n  initialValue?: string\n  field?: string\n  placeholder?: string\n  isEditing: boolean\n  isEditDisabled?: boolean\n  onEdit: (isEditing: boolean) => void\n  validation?: (value: string) => string\n  editToolTipContent?: React.ReactNode\n  onDecline: (event?: React.MouseEvent<HTMLElement>) => void\n  onApply: (value: string, event: React.MouseEvent) => void\n  testIdPrefix?: string\n  variant?: InlineItemEditorProps['variant']\n}\n\nconst EditableInput = (props: Props) => {\n  const {\n    children,\n    initialValue = '',\n    field,\n    placeholder,\n    isEditing,\n    isEditDisabled,\n    editToolTipContent,\n    validation,\n    onEdit,\n    onDecline,\n    onApply,\n    testIdPrefix = '',\n    variant,\n  } = props\n\n  const [isHovering, setIsHovering] = useState(false)\n\n  if (!isEditing) {\n    return (\n      <div\n        className={styles.contentWrapper}\n        onMouseEnter={() => setIsHovering(true)}\n        onMouseLeave={() => setIsHovering(false)}\n        data-testid={`${testIdPrefix}_content-value-${field}`}\n      >\n        <Text\n          component=\"div\"\n          color=\"secondary\"\n          style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}\n        >\n          <div style={{ display: 'flex' }}>{children}</div>\n        </Text>\n        {isHovering && (\n          <RiTooltip\n            content={editToolTipContent}\n            anchorClassName={styles.editBtnAnchor}\n            data-testid={`${testIdPrefix}_edit-tooltip-${field}`}\n          >\n            <IconButton\n              icon={EditIcon}\n              aria-label=\"Edit field\"\n              className={cx('editFieldBtn', styles.editBtn)}\n              disabled={isEditDisabled}\n              onClick={(e: React.MouseEvent) => {\n                e.stopPropagation()\n                onEdit?.(true)\n                setIsHovering(false)\n              }}\n              data-testid={`${testIdPrefix}_edit-btn-${field}`}\n            />\n          </RiTooltip>\n        )}\n      </div>\n    )\n  }\n\n  return (\n    <StopPropagation>\n      <div className={styles.inputWrapper}>\n        <InlineItemEditor\n          initialValue={initialValue}\n          controlsPosition=\"right\"\n          controlsClassName={styles.controls}\n          placeholder={placeholder}\n          fieldName={field}\n          expandable\n          iconSize=\"M\"\n          onDecline={(event) => {\n            onDecline(event)\n            onEdit?.(false)\n          }}\n          onApply={(value, event) => {\n            onApply(value, event)\n            onEdit?.(false)\n          }}\n          validation={validation}\n          variant={variant}\n        />\n      </div>\n    </StopPropagation>\n  )\n}\n\nexport default EditableInput\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/index.ts",
    "content": "import EditableInput from './EditableInput'\n\nexport default EditableInput\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-input/styles.module.scss",
    "content": ".contentWrapper {\n  display: flex;\n  align-items: center;\n  position: relative;\n  flex-grow: 1;\n  width: 100%;\n  height: 100%;\n\n  min-height: 42px;\n  padding-right: 32px;\n\n  .editBtnAnchor {\n    position: absolute;\n    right: 4px;\n  }\n}\n\n.inputWrapper {\n  max-width: calc(100% - 48px);\n  padding: 0 4px;\n}\n\n.controls {\n  padding: 2px;\n  width: 48px !important;\n  box-shadow: none !important;\n  background-color: transparent !important;\n\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  :global(.euiButtonIcon), :global(.euiToolTipAnchor) {\n    width: 20px !important;\n    height: 20px !important;\n    min-width: 20px !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-popover/EditablePopover.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport EditablePopover, { Props } from './EditablePopover'\n\nconst mockedProps = mock<Props>()\nconst Input = () => <input data-testid=\"input\" />\nconst Text = () => <span data-testid=\"text\" />\n\ndescribe('EditableInput', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <EditablePopover {...mockedProps}>\n          <Input />\n        </EditablePopover>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should not display popover, display content', () => {\n    render(\n      <EditablePopover {...mockedProps} content={<Text />}>\n        <Input />\n      </EditablePopover>,\n    )\n\n    expect(screen.queryByTestId('input')).not.toBeInTheDocument()\n    expect(screen.getByTestId('text')).toBeInTheDocument()\n  })\n\n  it('should display popover', () => {\n    render(\n      <EditablePopover {...mockedProps}>\n        <Text />\n      </EditablePopover>,\n    )\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('_content-value-'))\n    })\n    fireEvent.click(screen.getByTestId('_edit-btn-'))\n    expect(screen.getByTestId('popover-item-editor')).toBeInTheDocument()\n  })\n\n  it('should hide edit button when no editable', () => {\n    render(\n      <EditablePopover {...mockedProps} isDisabledEditButton>\n        <Text />\n      </EditablePopover>,\n    )\n\n    act(() => {\n      fireEvent.mouseEnter(screen.getByTestId('_content-value-'))\n    })\n    expect(screen.queryByTestId('_edit-btn-')).not.toBeInTheDocument()\n  })\n\n  it('should call on apply', () => {\n    const onApply = jest.fn()\n    render(\n      <EditablePopover\n        {...mockedProps}\n        isOpen\n        onDecline={jest.fn()}\n        onApply={onApply}\n        content={<Input />}\n      >\n        <Text />\n      </EditablePopover>,\n    )\n\n    fireEvent.change(screen.getByTestId('input'), {\n      target: { value: 'value' },\n    })\n    fireEvent.click(screen.getByTestId('save-btn'))\n\n    expect(onApply).toBeCalled()\n  })\n\n  it('should call on decline', () => {\n    const onDecline = jest.fn()\n    render(\n      <EditablePopover {...mockedProps} isOpen onDecline={onDecline}>\n        <Text />\n      </EditablePopover>,\n    )\n\n    fireEvent.click(screen.getByTestId('cancel-btn'))\n\n    expect(onDecline).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-popover/EditablePopover.tsx",
    "content": "import React, { FormEvent, useEffect, useState } from 'react'\nimport cx from 'classnames'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  IconButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { RiPopover } from 'uiSrc/components/base'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  content: React.ReactElement\n  children: React.ReactElement\n  className?: string\n  editBtnClassName?: string\n  isOpen?: boolean\n  onOpen: () => void\n  onApply: () => void\n  onDecline?: () => void\n  isLoading?: boolean\n  isDisabled?: boolean\n  declineOnUnmount?: boolean\n  field?: string\n  prefix?: string\n  btnIconType?: string\n  delay?: number\n  isDisabledEditButton?: boolean\n}\n\nconst EditablePopover = (props: Props) => {\n  const {\n    content,\n    isOpen = false,\n    onOpen,\n    onDecline,\n    onApply,\n    children,\n    isLoading,\n    declineOnUnmount = true,\n    isDisabled,\n    field = '',\n    prefix = '',\n    btnIconType,\n    className,\n    editBtnClassName = '',\n    isDisabledEditButton,\n    delay,\n  } = props\n  const [isHovering, setIsHovering] = useState(false)\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(isOpen)\n  const [isDelayed, setIsDelayed] = useState(false)\n\n  const delayPopover = () => {\n    if (!delay) return\n\n    setIsDelayed(() => {\n      setTimeout(() => setIsDelayed(false), delay)\n      return true\n    })\n  }\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        declineOnUnmount && handleDecline()\n      },\n    [],\n  )\n\n  useEffect(() => {\n    if (isOpen) delayPopover()\n    setIsPopoverOpen(isOpen)\n  }, [isOpen])\n\n  const onFormSubmit = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault()\n    handleApply()\n  }\n\n  const handleApply = (): void => {\n    setIsPopoverOpen(false)\n    onApply()\n  }\n\n  const handleDecline = () => {\n    setIsPopoverOpen(false)\n    onDecline?.()\n  }\n\n  const handleButtonClick = (\n    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  ) => {\n    e.stopPropagation()\n    onOpen?.()\n    delayPopover()\n    setIsPopoverOpen(true)\n  }\n\n  const isDisabledApply = (): boolean => !!(isLoading || isDisabled)\n\n  const button = (\n    <IconButton\n      disabled={isPopoverOpen || isDisabledEditButton}\n      icon={btnIconType || EditIcon}\n      aria-label=\"Edit field\"\n      color=\"primary\"\n      onClick={isDisabledEditButton ? () => {} : handleButtonClick}\n      className={editBtnClassName}\n      data-testid={`${prefix}_edit-btn-${field}`}\n    />\n  )\n\n  return (\n    <RiPopover\n      ownFocus\n      anchorPosition=\"downLeft\"\n      isOpen={isPopoverOpen}\n      anchorClassName={className}\n      panelClassName={cx(styles.popoverWrapper, {\n        [styles.isDelayed]: isDelayed,\n      })}\n      closePopover={handleDecline}\n      button={\n        <Row\n          align=\"center\"\n          className={styles.contentWrapper}\n          onMouseEnter={() => setIsHovering(!isDisabledEditButton)}\n          onMouseLeave={() => setIsHovering(false)}\n          onClick={(e) => e.stopPropagation()}\n          data-testid={`${prefix}_content-value-${field}`}\n        >\n          {content}\n          <FlexItem style={{ marginLeft: '-19px' }}>\n            {isDelayed && (\n              <Loader\n                className={cx(editBtnClassName, styles.spinner)}\n                size=\"m\"\n              />\n            )}\n            {(isPopoverOpen || isHovering) && !isDelayed && button}\n          </FlexItem>\n        </Row>\n      }\n      data-testid=\"popover-item-editor\"\n      onClick={(e) => e.stopPropagation()}\n    >\n      <form onSubmit={onFormSubmit}>\n        <Col gap=\"l\">\n          <Row>{children}</Row>\n          <Row justify=\"end\" gap=\"m\">\n            <FlexItem>\n              <SecondaryButton\n                size=\"s\"\n                onClick={() => handleDecline()}\n                data-testid=\"cancel-btn\"\n              >\n                Cancel\n              </SecondaryButton>\n            </FlexItem>\n            <FlexItem>\n              <PrimaryButton\n                size=\"s\"\n                type=\"submit\"\n                disabled={isDisabledApply()}\n                data-testid=\"save-btn\"\n              >\n                Save\n              </PrimaryButton>\n            </FlexItem>\n          </Row>\n        </Col>\n      </form>\n    </RiPopover>\n  )\n}\n\nexport default EditablePopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-popover/index.ts",
    "content": "import EditablePopover from './EditablePopover'\n\nexport default EditablePopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-popover/styles.module.scss",
    "content": ".contentWrapper {\n  display: flex;\n}\n\n.popoverWrapper {\n  &.isDelayed {\n    opacity: 0 !important;\n  }\n}\n\n.spinner {\n  margin: 3px 4px;\n  border-top-color: var(--euiColorPrimary) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport EditableTextArea, { Props } from './EditableTextArea'\n\nconst mockedProps = mock<Props>()\nconst Text = () => <span data-testid=\"text\">text</span>\n\ndescribe('EditableTextArea', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <EditableTextArea {...mockedProps}>\n          <Text />\n        </EditableTextArea>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should display editor', () => {\n    render(\n      <EditableTextArea\n        {...mockedProps}\n        isEditing\n        field=\"field\"\n        testIdPrefix=\"item\"\n        onDecline={jest.fn()}\n      >\n        <Text />\n      </EditableTextArea>,\n    )\n\n    expect(screen.getByTestId('item_value-editor-field')).toBeInTheDocument()\n  })\n\n  it('should call on apply', () => {\n    const onApply = jest.fn()\n    render(\n      <EditableTextArea\n        {...mockedProps}\n        isEditing\n        field=\"field\"\n        testIdPrefix=\"item\"\n        onEdit={jest.fn()}\n        onDecline={jest.fn()}\n        onApply={onApply}\n      >\n        <Text />\n      </EditableTextArea>,\n    )\n\n    fireEvent.change(screen.getByTestId('item_value-editor-field'), {\n      target: { value: 'value' },\n    })\n    fireEvent.click(screen.getByTestId('apply-btn'))\n\n    expect(onApply).toBeCalledWith('value', expect.any(Object))\n  })\n\n  it('should call on decline', () => {\n    const onDecline = jest.fn()\n    render(\n      <EditableTextArea\n        {...mockedProps}\n        isEditing\n        field=\"field\"\n        testIdPrefix=\"item\"\n        onEdit={jest.fn()}\n        onDecline={onDecline}\n      >\n        <Text />\n      </EditableTextArea>,\n    )\n\n    fireEvent.change(screen.getByTestId('item_value-editor-field'), {\n      target: { value: 'value' },\n    })\n    fireEvent.click(screen.getByTestId('cancel-btn'))\n\n    expect(onDecline).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/EditableTextArea.tsx",
    "content": "import React, { ChangeEvent, Ref, useEffect, useRef, useState } from 'react'\nimport AutoSizer from 'react-virtualized-auto-sizer'\nimport cx from 'classnames'\n\nimport { StopPropagation } from 'uiSrc/components/virtual-table'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { TextArea } from 'uiSrc/components/base/inputs'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  children: React.ReactNode\n  initialValue?: string\n  field?: string\n  isEditing: boolean\n  isLoading?: boolean\n  isDisabled?: boolean\n  isInvalid?: boolean\n  isEditDisabled?: boolean\n  textAreaMaxHeight?: number\n  disabledTooltipText?: { title: string; content: string }\n  approveText?: { title: string; text: string }\n  editToolTipContent?: React.ReactNode\n  approveByValidation?: (value: string) => boolean\n  onEdit: (isEditing: boolean) => void\n  onUpdateTextAreaHeight?: () => void\n  onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void\n  onDecline: (event?: React.MouseEvent<HTMLElement>) => void\n  onApply: (value: string, event: React.MouseEvent) => void\n  testIdPrefix?: string\n}\n\nconst EditableTextArea = (props: Props) => {\n  const {\n    children,\n    initialValue = '',\n    field = '',\n    textAreaMaxHeight = 300,\n    isEditing,\n    isEditDisabled,\n    isLoading,\n    isDisabled,\n    isInvalid,\n    disabledTooltipText,\n    approveText,\n    editToolTipContent,\n    approveByValidation = () => true,\n    onEdit,\n    onUpdateTextAreaHeight,\n    onChange,\n    onDecline,\n    onApply,\n    testIdPrefix = '',\n  } = props\n\n  const [value, setValue] = useState('')\n  const [isHovering, setIsHovering] = useState(false)\n  const textAreaRef: Ref<HTMLTextAreaElement> = useRef(null)\n\n  useEffect(() => {\n    setValue(initialValue)\n  }, [initialValue])\n\n  useEffect(() => {\n    if (isEditing) {\n      updateTextAreaHeight()\n      setTimeout(() => textAreaRef?.current?.focus(), 0)\n    }\n  }, [isEditing])\n\n  const updateTextAreaHeight = () => {\n    if (textAreaRef.current) {\n      textAreaRef.current.style.height = '0px'\n      textAreaRef.current.style.height = `${textAreaRef.current?.scrollHeight || 0}px`\n      onUpdateTextAreaHeight?.()\n    }\n  }\n\n  const handleOnChange = (e: ChangeEvent<HTMLTextAreaElement>) => {\n    setValue(e.target.value)\n    updateTextAreaHeight()\n    onChange?.(e)\n  }\n\n  if (!isEditing) {\n    return (\n      <div\n        className={styles.contentWrapper}\n        onMouseEnter={() => setIsHovering(true)}\n        onMouseLeave={() => setIsHovering(false)}\n        data-testid={`${testIdPrefix}_content-value-${field}`}\n      >\n        <Text\n          component=\"div\"\n          color=\"secondary\"\n          style={{ maxWidth: '100%', whiteSpace: 'break-spaces' }}\n        >\n          {children}\n        </Text>\n        {isHovering && (\n          <RiTooltip\n            content={editToolTipContent}\n            anchorClassName={styles.editBtnAnchor}\n            data-testid={`${testIdPrefix}_edit-tooltip-${field}`}\n          >\n            <IconButton\n              icon={EditIcon}\n              aria-label=\"Edit field\"\n              className={cx('editFieldBtn', styles.editBtn)}\n              disabled={isEditDisabled}\n              onClick={(e: React.MouseEvent) => {\n                e.stopPropagation()\n                onEdit?.(true)\n                setIsHovering(false)\n              }}\n              data-testid={`${testIdPrefix}_edit-btn-${field}`}\n            />\n          </RiTooltip>\n        )}\n      </div>\n    )\n  }\n\n  return (\n    <AutoSizer\n      disableHeight\n      onResize={() => setTimeout(updateTextAreaHeight, 0)}\n    >\n      {({ width }) => (\n        <div style={{ width }}>\n          <StopPropagation>\n            <InlineItemEditor\n              expandable\n              preventOutsideClick\n              disableFocusTrap\n              declineOnUnmount={false}\n              initialValue={initialValue}\n              controlsPosition=\"inside\"\n              controlsDesign=\"separate\"\n              fieldName=\"fieldValue\"\n              isLoading={isLoading}\n              isDisabled={isDisabled}\n              isInvalid={isInvalid}\n              disabledTooltipText={disabledTooltipText}\n              controlsClassName={styles.textAreaControls}\n              onDecline={(event) => {\n                onDecline(event)\n                setValue(initialValue)\n                onEdit(false)\n              }}\n              onApply={(_, event) => {\n                onApply(value, event)\n                setValue(initialValue)\n                onEdit(false)\n              }}\n              approveText={approveText}\n              approveByValidation={() => approveByValidation?.(value)}\n            >\n              <TextArea\n                name=\"value\"\n                id=\"value\"\n                placeholder=\"Enter Value\"\n                value={value}\n                onChangeCapture={handleOnChange}\n                disabled={isLoading}\n                ref={textAreaRef}\n                spellCheck={false}\n                style={{\n                  height: textAreaRef.current?.scrollHeight || 0,\n                  maxHeight: textAreaMaxHeight,\n                }}\n                data-testid={`${testIdPrefix}_value-editor-${field}`}\n              />\n            </InlineItemEditor>\n          </StopPropagation>\n        </div>\n      )}\n    </AutoSizer>\n  )\n}\n\nexport default EditableTextArea\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/index.ts",
    "content": "import EditableTextArea from './EditableTextArea'\n\nexport default EditableTextArea\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/editable-textarea/styles.module.scss",
    "content": ".contentWrapper {\n  display: flex;\n  align-items: center;\n  position: relative;\n  flex-grow: 1;\n  width: 100%;\n  height: 100%;\n\n  min-height: 42px;\n  padding-right: 32px;\n\n  .editBtnAnchor {\n    position: absolute;\n    right: 4px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/formatted-value/FormattedValue.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport FormattedValue, { Props } from './FormattedValue'\n\nconst mockedProps = mock<Props>()\n\ndescribe('FormattedValue', () => {\n  it('should render', () => {\n    expect(\n      render(<FormattedValue {...mockedProps} value=\"Some string\" />),\n    ).toBeTruthy()\n  })\n\n  it('should display text provided in a props', () => {\n    const value = 'Some string'\n    render(<FormattedValue {...mockedProps} value={value} />)\n    expect(screen.getAllByText(value).length).toEqual(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/formatted-value/FormattedValue.tsx",
    "content": "import { ToolTipPositions } from '@elastic/eui'\nimport React from 'react'\nimport { RiTooltip, RiTooltipProps } from 'uiSrc/components'\n\nexport interface Props\n  extends Omit<RiTooltipProps, 'children' | 'delay' | 'position'> {\n  value: string | JSX.Element\n  tooltipContent: string | JSX.Element\n  expanded?: boolean\n  title?: string\n  truncateLength?: number\n  position?: ToolTipPositions\n}\n\nconst FormattedValue = ({\n  expanded,\n  value,\n  title,\n  tooltipContent,\n  truncateLength = 200,\n  position = 'bottom',\n  ...rest\n}: Props) => {\n  if (expanded) return <>{value}</>\n\n  const truncated = value?.substring?.(0, truncateLength) ?? value\n\n  return (\n    <RiTooltip\n      title={title}\n      content={tooltipContent}\n      anchorClassName=\"truncateText\"\n      position={position}\n      {...rest}\n    >\n      <>{truncated}</>\n    </RiTooltip>\n  )\n}\n\nexport default FormattedValue\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/formatted-value/index.ts",
    "content": "import FormattedValue from './FormattedValue'\n\nexport default FormattedValue\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/shared/index.ts",
    "content": "import EditableTextArea from './editable-textarea'\nimport EditableInput from './editable-input'\nimport FormattedValue from './formatted-value'\n\nexport { EditableTextArea, EditableInput, FormattedValue }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details/styles.module.scss",
    "content": ".container {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.header {\n  height: 46px;\n  display: flex;\n  align-items: center;\n\n  @media (max-width: 1123px) {\n    padding: 0 10px;\n  }\n}\n\n.content {\n  height: 100%;\n  background-color: var(--euiColorEmptyShade);\n  position: relative;\n  > div {\n    height: 100%;\n  }\n}\n\n:global(.show-cli) .content {\n  border-bottom-width: 1px !important;\n}\n\n.contentActive {\n  border-color: var(--euiColorPrimary) !important;\n  border-bottom-width: 1px !important;\n}\n\n:global(.key-details-body) {\n  position: relative;\n  height: calc(100% - 144px);\n}\n\n// 108px is the key details header height\n:global(.key-details-body-monaco-editor) {\n  position: relative;\n  height: calc(100% - 108px);\n}\n\n:global(.key-details-footer) {\n  margin: 0 16px;\n  padding: 16px 0 20px 0;\n  border-top: 1px solid var(--euiColorLightShade);\n}\n\n.actionBtn {\n  margin-right: 12px;\n  position: relative;\n  z-index: 2;\n\n  &.withText {\n    color: var(--euiTextSubduedColor) !important;\n    :global(.euiButton__text) {\n      font: normal normal normal 12px/18px Graphik !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { deleteSelectedKey } from 'uiSrc/slices/browser/keys'\nimport { KeyDetailsHeaderProps, KeyDetailsHeader } from './KeyDetailsHeader'\n\nconst mockedProps = mock<KeyDetailsHeaderProps>()\n\nconst KEY_INPUT_TEST_ID = 'edit-key-input'\nconst KEY_BTN_TEST_ID = 'edit-key-btn'\nconst TTL_INPUT_TEST_ID = 'edit-ttl-input'\nconst DELETE_KEY_BTN_TEST_ID = 'delete-key-btn'\nconst DELETE_KEY_CONFIRM_BTN_TEST_ID = 'delete-key-confirm-btn'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/browser/string', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/string'),\n  stringDataSelector: jest.fn().mockReturnValue({\n    value: {\n      type: 'Buffer',\n      data: [49, 50, 51, 52],\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeyDataSelector: jest.fn().mockReturnValue({\n    name: {\n      type: 'Buffer',\n      data: [116, 101, 115, 116],\n    },\n    nameString: 'test',\n    length: 4,\n  }),\n}))\n\ndescribe('KeyDetailsHeader', () => {\n  global.navigator.clipboard = {\n    writeText: jest.fn(),\n  }\n\n  it('should render', () => {\n    expect(render(<KeyDetailsHeader {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should change key properly', () => {\n    render(<KeyDetailsHeader {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId(KEY_BTN_TEST_ID))\n\n    fireEvent.change(screen.getByTestId(KEY_INPUT_TEST_ID), {\n      target: { value: 'key' },\n    })\n    expect(screen.getByTestId(KEY_INPUT_TEST_ID)).toHaveValue('key')\n  })\n\n  it('should be able to copy key', () => {\n    render(<KeyDetailsHeader {...mockedProps} />)\n\n    fireEvent.focus(screen.getByTestId(KEY_BTN_TEST_ID))\n\n    fireEvent.mouseEnter(screen.getByTestId(KEY_BTN_TEST_ID))\n\n    expect(screen.getByLabelText(/Copy key name/i)).toBeInTheDocument()\n\n    fireEvent.click(screen.getByLabelText(/Copy key name/i))\n  })\n\n  it('should change ttl properly', () => {\n    render(<KeyDetailsHeader {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId('edit-ttl-btn'))\n\n    fireEvent.change(screen.getByTestId(TTL_INPUT_TEST_ID), {\n      target: { value: '100' },\n    })\n\n    expect(screen.getByTestId(TTL_INPUT_TEST_ID)).toHaveValue('100')\n  })\n\n  describe('should call onRefresh', () => {\n    test.each(Object.values(KeyTypes))(\n      'should call onRefresh for keyType: %s',\n      (keyType) => {\n        const component = render(\n          <KeyDetailsHeader {...mockedProps} keyType={keyType} />,\n        )\n        fireEvent.click(screen.getByTestId('key-refresh-btn'))\n        expect(component).toBeTruthy()\n      },\n    )\n  })\n\n  describe('should call onDelete', () => {\n    test.each(Object.values(KeyTypes))(\n      'should call onDelete for keyType: %s',\n      (keyType) => {\n        const onRemoveKeyMock = jest.fn()\n        const component = render(\n          <KeyDetailsHeader\n            {...mockedProps}\n            keyType={keyType}\n            onRemoveKey={onRemoveKeyMock}\n          />,\n        )\n        fireEvent.click(screen.getByTestId(DELETE_KEY_BTN_TEST_ID))\n        fireEvent.click(screen.getByTestId(DELETE_KEY_CONFIRM_BTN_TEST_ID))\n        expect(component).toBeTruthy()\n\n        const expectedActions = [deleteSelectedKey()]\n        expect(store.getActions()).toEqual(\n          expect.arrayContaining(expectedActions),\n        )\n      },\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const MakeSearchableWrapper = styled.div`\n  margin-right: ${({ theme }) => theme.core.space.space100};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/KeyDetailsHeader.tsx",
    "content": "import React, { ReactElement } from 'react'\nimport { isUndefined } from 'lodash'\nimport { useDispatch, useSelector } from 'react-redux'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport {\n  GroupBadge,\n  AutoRefresh,\n  FullScreen,\n  LoadingContent,\n  RiTooltip,\n  FeatureFlagComponent,\n} from 'uiSrc/components'\nimport {\n  HIDE_LAST_REFRESH,\n  FeatureFlags,\n  KeyTypes,\n  SEARCHABLE_KEY_TYPES,\n} from 'uiSrc/constants'\nimport {\n  deleteSelectedKeyAction,\n  editKey,\n  editKeyTTL,\n  initialKeyInfo,\n  keysSelector,\n  refreshKey,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  useIsKeyIndexed,\n  UseIsKeyIndexedStatus,\n} from 'uiSrc/pages/vector-search/hooks/useIsKeyIndexed'\nimport { ViewIndexDataButton } from 'uiSrc/pages/browser/components/view-index-data-button'\nimport { MakeSearchableButton } from 'uiSrc/pages/browser/components/make-searchable-button'\nimport { KeyDetailsHeaderName } from './components/key-details-header-name'\nimport { KeyDetailsHeaderTTL } from './components/key-details-header-ttl'\nimport { KeyDetailsHeaderDelete } from './components/key-details-header-delete'\nimport { KeyDetailsHeaderSizeLength } from './components/key-details-header-size-length'\n\nimport * as S from './KeyDetailsHeader.styles'\nimport styles from './styles.module.scss'\n\nexport interface KeyDetailsHeaderProps {\n  onCloseKey: () => void\n  onRemoveKey: () => void\n  onEditKey: (\n    key: RedisResponseBuffer,\n    newKey: RedisResponseBuffer,\n    onFailure?: () => void,\n  ) => void\n  isFullScreen: boolean\n  arePanelsCollapsed: boolean\n  onToggleFullScreen: () => void\n  Actions?: (props: { width: number }) => ReactElement\n}\n\nconst KeyDetailsHeader = ({\n  isFullScreen,\n  arePanelsCollapsed,\n  onToggleFullScreen = () => {},\n  onCloseKey,\n  onRemoveKey,\n  onEditKey,\n  Actions,\n}: KeyDetailsHeaderProps) => {\n  const { refreshing, loading, lastRefreshTime, isRefreshDisabled } =\n    useSelector(selectedKeySelector)\n  const {\n    type,\n    length,\n    name: keyBuffer,\n    nameString: keyName,\n  } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { viewType } = useSelector(keysSelector)\n\n  const isSearchableType = SEARCHABLE_KEY_TYPES.includes(type as KeyTypes)\n  const { indexes, status: keyIndexedStatus } = useIsKeyIndexed(\n    isSearchableType ? keyName || '' : '',\n  )\n\n  const dispatch = useDispatch()\n\n  const handleRefreshKey = () => {\n    dispatch(refreshKey(keyBuffer!, type, undefined, length))\n  }\n\n  const handleEditTTL = (key: RedisResponseBuffer, ttl: number) => {\n    dispatch(editKeyTTL(key, ttl))\n  }\n  const handleEditKey = (\n    oldKey: RedisResponseBuffer,\n    newKey: RedisResponseBuffer,\n    onFailure?: () => void,\n  ) => {\n    dispatch(\n      editKey(oldKey, newKey, () => onEditKey(oldKey, newKey), onFailure),\n    )\n  }\n\n  const handleDeleteKey = (key: RedisResponseBuffer) => {\n    dispatch(deleteSelectedKeyAction(key, onRemoveKey))\n  }\n\n  const handleEnableAutoRefresh = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    const browserViewEvent = enableAutoRefresh\n      ? TelemetryEvent.BROWSER_KEY_DETAILS_AUTO_REFRESH_ENABLED\n      : TelemetryEvent.BROWSER_KEY_DETAILS_AUTO_REFRESH_DISABLED\n    const treeViewEvent = enableAutoRefresh\n      ? TelemetryEvent.TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_ENABLED\n      : TelemetryEvent.TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_DISABLED\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(viewType, browserViewEvent, treeViewEvent),\n      eventData: {\n        length,\n        databaseId: instanceId,\n        keyType: type,\n        refreshRate: +refreshRate,\n      },\n    })\n  }\n\n  const handleChangeAutoRefreshRate = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    if (enableAutoRefresh) {\n      handleEnableAutoRefresh(enableAutoRefresh, refreshRate)\n    }\n  }\n\n  return (\n    <FlexItem\n      className={`key-details-header ${styles.container}`}\n      data-testid=\"key-details-header\"\n    >\n      {loading ? (\n        <div>\n          <LoadingContent lines={2} />\n        </div>\n      ) : (\n        <AutoSizer disableHeight>\n          {({ width = 0 }) => (\n            <div style={{ width }}>\n              <Row gap=\"s\" align=\"center\" className={styles.keyFlexGroup}>\n                <FlexItem>\n                  <GroupBadge type={type} />\n                </FlexItem>\n                <KeyDetailsHeaderName onEditKey={handleEditKey} />\n                <FlexItem grow />\n                {isSearchableType &&\n                  keyIndexedStatus === UseIsKeyIndexedStatus.Ready && (\n                    <FeatureFlagComponent name={FeatureFlags.vectorSearchV2}>\n                      <FlexItem>\n                        {indexes.length > 0 ? (\n                          <ViewIndexDataButton\n                            indexes={indexes}\n                            instanceId={instanceId}\n                          />\n                        ) : (\n                          <S.MakeSearchableWrapper>\n                            <MakeSearchableButton\n                              keyName={keyBuffer!}\n                              keyNameString={keyName ?? ''}\n                              keyType={type as KeyTypes}\n                            />\n                          </S.MakeSearchableWrapper>\n                        )}\n                      </FlexItem>\n                    </FeatureFlagComponent>\n                  )}\n                {!arePanelsCollapsed && (\n                  <FlexItem>\n                    <FullScreen\n                      isFullScreen={isFullScreen}\n                      onToggleFullScreen={onToggleFullScreen}\n                    />\n                  </FlexItem>\n                )}\n                <FlexItem>\n                  {(!arePanelsCollapsed || isFullScreen) && (\n                    <RiTooltip content=\"Close\" position=\"left\">\n                      <IconButton\n                        icon={CancelSlimIcon}\n                        aria-label=\"Close key\"\n                        className={styles.closeBtn}\n                        onClick={() => onCloseKey()}\n                        data-testid=\"close-key-btn\"\n                      />\n                    </RiTooltip>\n                  )}\n                </FlexItem>\n              </Row>\n              <Row\n                className={styles.groupSecondLine}\n                gap=\"m\"\n                align=\"start\"\n                justify=\"between\"\n              >\n                <Row gap=\"l\">\n                  <KeyDetailsHeaderSizeLength width={width} />\n                  <KeyDetailsHeaderTTL onEditTTL={handleEditTTL} />\n                </Row>\n                <FlexItem>\n                  <div className={styles.subtitleActionBtns}>\n                    <AutoRefresh\n                      postfix={type}\n                      disabled={isRefreshDisabled}\n                      loading={loading || refreshing}\n                      lastRefreshTime={lastRefreshTime}\n                      displayText={width > HIDE_LAST_REFRESH}\n                      containerClassName={styles.actionBtn}\n                      onRefresh={handleRefreshKey}\n                      onEnableAutoRefresh={handleEnableAutoRefresh}\n                      onChangeAutoRefreshRate={handleChangeAutoRefreshRate}\n                      testid=\"key\"\n                    />\n                    {!isUndefined(Actions) && <Actions width={width} />}\n                    <KeyDetailsHeaderDelete onDelete={handleDeleteKey} />\n                  </div>\n                </FlexItem>\n              </Row>\n            </div>\n          )}\n        </AutoSizer>\n      )}\n    </FlexItem>\n  )\n}\n\nexport { KeyDetailsHeader }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, KeyDetailsHeaderDelete } from './KeyDetailsHeaderDelete'\n\nconst mockedProps = mock<Props>()\n\ndescribe('KeyDetailsHeaderDelete', () => {\n  it('should render', () => {\n    expect(\n      render(<KeyDetailsHeaderDelete {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/KeyDetailsHeaderDelete.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport {\n  initialKeyInfo,\n  keysSelector,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { formatLongName } from 'uiSrc/utils'\n\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport {\n  DestructiveButton,\n  IconButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { ConfirmationPopover } from 'uiSrc/components'\n\nexport interface Props {\n  onDelete: (key: RedisResponseBuffer) => void\n}\n\nconst KeyDetailsHeaderDelete = ({ onDelete }: Props) => {\n  const {\n    type,\n    nameString: keyProp,\n    name: keyBuffer,\n  } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { viewType } = useSelector(keysSelector)\n\n  const [isPopoverDeleteOpen, setIsPopoverDeleteOpen] = useState(false)\n\n  const tooltipContent = formatLongName(keyProp || '')\n\n  const closePopoverDelete = () => {\n    setIsPopoverDeleteOpen(false)\n  }\n\n  const showPopoverDelete = () => {\n    setIsPopoverDeleteOpen((isPopoverDeleteOpen) => !isPopoverDeleteOpen)\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_DELETE_CLICKED,\n        TelemetryEvent.TREE_VIEW_KEY_DELETE_CLICKED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        source: 'keyValue',\n        keyType: type,\n      },\n    })\n  }\n\n  return (\n    <ConfirmationPopover\n      key={keyProp}\n      anchorPosition=\"leftCenter\"\n      ownFocus\n      isOpen={isPopoverDeleteOpen}\n      closePopover={closePopoverDelete}\n      panelPaddingSize=\"l\"\n      button={\n        <IconButton\n          icon={DeleteIcon}\n          aria-label=\"Delete Key\"\n          className=\"deleteKeyBtn\"\n          onClick={showPopoverDelete}\n          data-testid=\"delete-key-btn\"\n        />\n      }\n      title={tooltipContent}\n      message=\"will be deleted.\"\n      confirmButton={\n        <DestructiveButton\n          size=\"small\"\n          icon={DeleteIcon}\n          onClick={() => onDelete(keyBuffer!)}\n          data-testid=\"delete-key-confirm-btn\"\n        >\n          Delete\n        </DestructiveButton>\n      }\n    />\n  )\n}\n\nexport { KeyDetailsHeaderDelete }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/index.ts",
    "content": "export { KeyDetailsHeaderDelete } from './KeyDetailsHeaderDelete'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-delete/styles.module.scss",
    "content": ".popoverDeleteContainer {\n  overflow: hidden;\n  max-width: 350px !important;\n}\n\n.popoverFooter {\n  margin-top: 10px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport {\n  render,\n  screen,\n  userEvent,\n  waitForRedisUiSelectVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport { KeyDetailsHeaderFormatter, Props } from './KeyDetailsHeaderFormatter'\n\nconst mockedProps = {\n  ...mock<Props>(),\n}\n\ndescribe('KeyValueFormatter', () => {\n  it('should render', () => {\n    expect(render(<KeyDetailsHeaderFormatter {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render options in the strict order', async () => {\n    const strictOrder = [\n      'Unicode',\n      'ASCII',\n      'Binary',\n      'HEX',\n      'JSON',\n      'Msgpack',\n      'Pickle',\n      'Protobuf',\n      'PHP serialized',\n      'Java serialized',\n      'Vector 32-bit',\n      'Vector 64-bit',\n    ]\n    render(<KeyDetailsHeaderFormatter {...mockedProps} />)\n\n    await userEvent.click(screen.getByTestId('select-format-key-value'))\n\n    await waitForRedisUiSelectVisible()\n    strictOrder.forEach((option) => {\n      expect(screen.getByText(option)).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { ComponentProps } from 'react'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { insightsOpen } from 'uiSrc/styles/mixins'\n\ntype KeyDetailsSelectProps = ComponentProps<typeof RiSelect> & {\n  $fullWidth?: boolean\n}\n\nconst KeyDetailsSelect = styled(RiSelect)<KeyDetailsSelectProps>`\n  border: none !important;\n  background-color: inherit !important;\n  max-width: 100%;\n  padding-right: 18px;\n  padding-left: 0;\n  height: 28px;\n\n  & ~ div {\n    right: 7px;\n    top: 4px;\n\n    svg {\n      width: 10px !important;\n      height: 10px !important;\n    }\n  }\n`\n\nconst OptionText = styled(ColorText)`\n  padding-left: 6px;\n  padding-right: 4px;\n  font-size: 13px;\n  line-height: 30px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n`\n\nconst ControlsIcon = styled(RiIcon)`\n  position: relative;\n  margin-left: 3px;\n  margin-top: 2px;\n\n  ${insightsOpen(1440)`\n    width: 18px !important;\n    height: 18px !important;\n  `}\n`\n\nconst Container = styled.div<{\n  className?: string\n  children: React.ReactNode\n}>`\n  display: flex;\n  align-items: center;\n  height: 30px;\n  border-radius: 4px;\n  transition: transform 0.3s ease;\n  overflow: hidden;\n\n  [class*='TriggerContainer'] {\n    height: 100%;\n  }\n\n  &:not(.fullWidth) {\n    width: 56px;\n\n    [class*='TriggerContainer'] {\n      width: 56px;\n    }\n  }\n`\n\nexport { Container, KeyDetailsSelect, OptionText, ControlsIcon }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  KeyTypes,\n  KeyValueFormat,\n  MIDDLE_SCREEN_RESOLUTION,\n  TEXT_DISABLED_STRING_FORMATTING,\n} from 'uiSrc/constants'\nimport {\n  keysSelector,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n  setViewFormat,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { stringDataSelector } from 'uiSrc/slices/browser/string'\nimport { isFullStringLoaded } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  Container,\n  ControlsIcon,\n  KeyDetailsSelect,\n  OptionText,\n} from 'uiSrc/pages/browser/modules/key-details-header/components/key-details-header-formatter/KeyDetailsHeaderFormatter.styles'\nimport { getKeyValueFormatterOptions } from './constants'\n\nexport interface Props {\n  width: number\n}\nconst KeyDetailsHeaderFormatter = (props: Props) => {\n  const { width } = props\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { viewType } = useSelector(keysSelector)\n  const { viewFormat } = useSelector(selectedKeySelector)\n  const { type: keyType, length } = useSelector(selectedKeyDataSelector) ?? {}\n  const { value: keyValue } = useSelector(stringDataSelector)\n\n  const [isSelectOpen, setIsSelectOpen] = useState<boolean>(false)\n  const [typeSelected, setTypeSelected] = useState<KeyValueFormat>(viewFormat)\n  const [options, setOptions] = useState<any[]>([])\n\n  const dispatch = useDispatch()\n\n  const isStringFormattingEnabled =\n    keyType === KeyTypes.String\n      ? isFullStringLoaded(keyValue?.data?.length, length)\n      : true\n\n  useEffect(() => {\n    const newOptions = getKeyValueFormatterOptions(keyType).map(\n      ({ value, text }) => ({\n        value,\n        label: value,\n        inputDisplay: (\n          <RiTooltip\n            data-test-subj={`format-option-${value}`}\n            content={\n              !isStringFormattingEnabled\n                ? TEXT_DISABLED_STRING_FORMATTING\n                : typeSelected\n            }\n            position=\"top\"\n            anchorClassName=\"flex-row\"\n          >\n            <>\n              {width >= MIDDLE_SCREEN_RESOLUTION ? (\n                <OptionText>{text}</OptionText>\n              ) : (\n                <ControlsIcon\n                  size=\"m\"\n                  type=\"FormatterIcon\"\n                  data-testid={`key-value-formatter-option-selected-${value}`}\n                />\n              )}\n            </>\n          </RiTooltip>\n        ),\n        dropdownDisplay: (\n          <Text\n            component=\"span\"\n            size=\"s\"\n            data-test-subj={`format-option-${value}`}\n          >\n            {text}\n          </Text>\n        ),\n      }),\n    )\n\n    setOptions(newOptions)\n  }, [viewFormat, keyType, width, isStringFormattingEnabled])\n\n  const onChangeType = (value: KeyValueFormat) => {\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_DETAILS_FORMATTER_CHANGED,\n        TelemetryEvent.TREE_VIEW_KEY_DETAILS_FORMATTER_CHANGED,\n      ),\n      eventData: {\n        keyType,\n        databaseId: instanceId,\n        fromFormatter: viewFormat,\n        toFormatter: value,\n      },\n    })\n\n    setTypeSelected(value)\n    setIsSelectOpen(false)\n    dispatch(setViewFormat(value))\n  }\n\n  if (!options.length) {\n    return null\n  }\n\n  return (\n    <Container className={width >= MIDDLE_SCREEN_RESOLUTION ? 'fullWidth' : ''}>\n      <div className=\"selectWrapper\">\n        <KeyDetailsSelect\n          $fullWidth={width >= MIDDLE_SCREEN_RESOLUTION}\n          disabled={!isStringFormattingEnabled}\n          defaultOpen={isSelectOpen}\n          options={options}\n          valueRender={({ option, isOptionValue }) => {\n            if (isOptionValue) {\n              return option.dropdownDisplay as JSX.Element\n            }\n            return option.inputDisplay as JSX.Element\n          }}\n          value={typeSelected}\n          onChange={(value: any) => onChangeType(value)}\n          data-testid=\"select-format-key-value\"\n        />\n      </div>\n    </Container>\n  )\n}\n\nexport { KeyDetailsHeaderFormatter }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/constants.ts",
    "content": "import { KeyTypes, KeyValueFormat, ModulesKeyTypes } from 'uiSrc/constants'\n\nexport const KEY_VALUE_FORMATTER_OPTIONS = [\n  {\n    text: 'Unicode',\n    value: KeyValueFormat.Unicode,\n  },\n  {\n    text: 'ASCII',\n    value: KeyValueFormat.ASCII,\n  },\n  {\n    text: 'Binary',\n    value: KeyValueFormat.Binary,\n  },\n  {\n    text: 'HEX',\n    value: KeyValueFormat.HEX,\n  },\n  {\n    text: 'JSON',\n    value: KeyValueFormat.JSON,\n  },\n  {\n    text: 'Msgpack',\n    value: KeyValueFormat.Msgpack,\n  },\n  {\n    text: 'Pickle',\n    value: KeyValueFormat.Pickle,\n  },\n  {\n    text: 'Protobuf',\n    value: KeyValueFormat.Protobuf,\n  },\n  {\n    text: 'PHP serialized',\n    value: KeyValueFormat.PHP,\n  },\n  {\n    text: 'Java serialized',\n    value: KeyValueFormat.JAVA,\n  },\n  {\n    text: 'Vector 32-bit',\n    value: KeyValueFormat.Vector32Bit,\n  },\n  {\n    text: 'Vector 64-bit',\n    value: KeyValueFormat.Vector64Bit,\n  },\n  {\n    text: 'Timestamp to DateTime',\n    value: KeyValueFormat.DateTime,\n  },\n]\n\nexport const KEY_VALUE_JSON_FORMATTER_OPTIONS = []\n\nexport const getKeyValueFormatterOptions = (\n  viewFormat?: KeyTypes | ModulesKeyTypes,\n) =>\n  viewFormat !== KeyTypes.ReJSON\n    ? [...KEY_VALUE_FORMATTER_OPTIONS]\n    : [...KEY_VALUE_FORMATTER_OPTIONS].filter(\n        (option) =>\n          (KEY_VALUE_JSON_FORMATTER_OPTIONS as Array<any>).indexOf(\n            option.value,\n          ) !== -1,\n      )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/index.ts",
    "content": "export { KeyDetailsHeaderFormatter } from './KeyDetailsHeaderFormatter'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-formatter/styles.module.scss",
    "content": ".container {\n  border-radius: 4px;\n  transition: transform 0.3s ease;\n  overflow: hidden;\n\n  &:hover {\n    transform: translateY(-1px);\n    background-color: var(--tableRowSelectedColor);\n  }\n  &:active {\n    transform: translateY(1px);\n  }\n\n  &:not(.fullWidth) {\n    width: 46px;\n  }\n\n  .optionText {\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n}\n\n@include global.insights-open {\n  .container {\n    margin-right: 8px;\n  }\n}\n\n.formatType {\n  margin-top: 3px;\n  margin-bottom: 3px;\n  padding: 6px !important;\n  min-height: 36px !important;\n}\n\n.optionText {\n  padding-left: 6px;\n  padding-right: 4px;\n  font-size: 13px;\n  line-height: 30px;\n}\n\n.controlsIcon {\n  position: relative;\n  margin-left: 3px;\n  margin-top: 2px;\n  width: 20px !important;\n  height: 20px !important;\n  rotate: 90deg;\n}\n\n@include global.insights-open {\n  .controlsIcon {\n    width: 18px !important;\n    height: 18px !important;\n  }\n}\n\n.dropdownDisplaySmall {\n  left: 100px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, KeyDetailsHeaderName } from './KeyDetailsHeaderName'\n\nconst mockedProps = mock<Props>()\n\ndescribe('KeyDetailsHeaderName', () => {\n  it('should render', () => {\n    expect(\n      render(<KeyDetailsHeaderName {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/KeyDetailsHeaderName.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport cx from 'classnames'\nimport { isNull } from 'lodash'\nimport styled from 'styled-components'\nimport { useSelector } from 'react-redux'\n\nimport { formatLongName, isEqualBuffers, stringToBuffer } from 'uiSrc/utils'\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'\nimport { TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants'\nimport { AddCommonFieldsFormConfig } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'\nimport {\n  initialKeyInfo,\n  keysSelector,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n} from 'uiSrc/slices/browser/keys'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiTooltip } from 'uiSrc/components'\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport styles from './styles.module.scss'\n\nconst StyledInputWrapper = styled(Row)`\n  min-width: 150px;\n`\n\nconst StyledFlexWrapper = styled(FlexItem)`\n  max-width: 450px;\n  gap: ${({ theme }) => theme.core.space.space050};\n`\n\nexport interface Props {\n  onEditKey: (\n    key: RedisResponseBuffer,\n    newKey: RedisResponseBuffer,\n    onFailure?: () => void,\n  ) => void\n}\n\nconst COPY_KEY_NAME_ICON = 'copyKeyNameIcon'\n\nconst KeyDetailsHeaderName = ({ onEditKey }: Props) => {\n  const { loading } = useSelector(selectedKeySelector)\n  const {\n    ttl: ttlProp,\n    type,\n    nameString: keyProp,\n    name: keyBuffer,\n  } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { viewType } = useSelector(keysSelector)\n\n  const [key, setKey] = useState(keyProp)\n  const [keyIsEditing, setKeyIsEditing] = useState(false)\n  const [keyIsHovering, setKeyIsHovering] = useState(false)\n  const [keyIsEditable, setKeyIsEditable] = useState(true)\n\n  useEffect(() => {\n    setKey(keyProp)\n    setKeyIsEditable(isEqualBuffers(keyBuffer, stringToBuffer(keyProp || '')))\n  }, [keyProp, ttlProp, keyBuffer])\n\n  const keyNameRef = useRef<HTMLInputElement>(null)\n\n  const tooltipContent = formatLongName(keyProp || '')\n\n  const onMouseEnterKey = () => {\n    setKeyIsHovering(true)\n  }\n\n  const onMouseLeaveKey = () => {\n    setKeyIsHovering(false)\n  }\n\n  const onClickKey = () => {\n    setKeyIsEditing(true)\n  }\n\n  const onChangeKey = (value: string) => {\n    keyIsEditing && setKey(value)\n  }\n\n  const applyEditKey = () => {\n    setKeyIsEditing(false)\n    setKeyIsHovering(false)\n\n    const newKeyBuffer = stringToBuffer(key || '')\n\n    if (\n      keyBuffer &&\n      !isEqualBuffers(keyBuffer, newKeyBuffer) &&\n      !isNull(keyProp)\n    ) {\n      onEditKey(keyBuffer, newKeyBuffer, () => setKey(keyProp))\n    }\n  }\n\n  const cancelEditKey = (event?: React.MouseEvent<HTMLElement>) => {\n    const { id } = (event?.target as HTMLElement) || {}\n    if (id === COPY_KEY_NAME_ICON) {\n      return\n    }\n    setKey(keyProp)\n    setKeyIsEditing(false)\n    setKeyIsHovering(false)\n\n    event?.stopPropagation()\n  }\n\n  const handleCopy = useCallback(() => {\n    if (keyIsEditing) {\n      keyNameRef?.current?.focus()\n    }\n\n    sendEventTelemetry({\n      event: getBasedOnViewTypeEvent(\n        viewType,\n        TelemetryEvent.BROWSER_KEY_COPIED,\n        TelemetryEvent.TREE_VIEW_KEY_COPIED,\n      ),\n      eventData: {\n        databaseId: instanceId,\n        keyType: type,\n      },\n    })\n  }, [keyIsEditing, viewType, instanceId, type])\n\n  return (\n    <StyledFlexWrapper\n      direction=\"row\"\n      onMouseEnter={onMouseEnterKey}\n      onMouseLeave={onMouseLeaveKey}\n      onClick={onClickKey}\n      data-testid=\"edit-key-btn\"\n    >\n      <RiTooltip\n        title=\"Key Name\"\n        position=\"left\"\n        content={tooltipContent}\n        anchorClassName={styles.toolTipAnchorKey}\n      >\n        <InlineItemEditor\n          onApply={() => applyEditKey()}\n          isDisabled={!keyIsEditable}\n          disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS}\n          onDecline={(event) => cancelEditKey(event)}\n          viewChildrenMode={!keyIsEditing}\n          isLoading={loading}\n          declineOnUnmount={false}\n        >\n          <StyledInputWrapper align=\"center\" style={{ maxWidth: 420 }}>\n            <TextInput\n              autoSize\n              name=\"key\"\n              id=\"key\"\n              ref={keyNameRef}\n              className={cx(styles.keyInput, {\n                [styles.keyInputEditing]: keyIsEditing,\n                'input-warning': !keyIsEditable,\n              })}\n              placeholder={AddCommonFieldsFormConfig?.keyName?.placeholder}\n              value={key!}\n              loading={loading}\n              onChange={onChangeKey}\n              readOnly={!keyIsEditing}\n              autoComplete=\"off\"\n              data-testid=\"edit-key-input\"\n              // todo: do not hardcode. align with other components in a single place\n              style={{ paddingLeft: 9, lineHeight: '31px' }}\n            />\n          </StyledInputWrapper>\n        </InlineItemEditor>\n      </RiTooltip>\n      {!keyIsEditing && keyIsHovering && (\n        <Row align=\"center\">\n          <RiIcon size=\"M\" type=\"EditIcon\" />\n          <CopyButton\n            copy={key!}\n            onCopy={handleCopy}\n            id={COPY_KEY_NAME_ICON}\n            tooltipConfig={{ anchorClassName: styles.copyKey }}\n            data-testid=\"copy-key-name\"\n            aria-label=\"Copy Key Name\"\n          />\n        </Row>\n      )}\n    </StyledFlexWrapper>\n  )\n}\n\nexport { KeyDetailsHeaderName }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/index.ts",
    "content": "export { KeyDetailsHeaderName } from './KeyDetailsHeaderName'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-name/styles.module.scss",
    "content": ".classNameGridComponent {\n  position: relative;\n}\n\n.flexItemKeyInput {\n  flex-direction: row !important;\n  width: 100% !important;\n}\n\n.toolTipAnchorKey {\n  max-width: 450px;\n  height: 31px !important;\n  width: 100%;\n}\n\n.keyInput {\n  height: 31px !important;\n  font-size: 14px !important;\n  font-weight: 500 !important;\n  flex: 1;\n  min-width: 150px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  padding-right: 6px;\n}\n\n:global(.browserPage .key-details-header .euiFormControlLayout) {\n  .keyInputEditing {\n    height: 31px !important;\n  }\n}\n\n.keyHiddenText {\n  display: inline-block;\n  visibility: hidden;\n  height: 1px;\n  overflow: hidden;\n  max-width: 100%;\n  margin-right: 80px;\n  word-break: break-all;\n}\n\n.copyKey {\n  display: flex;\n  align-items: center;\n  height: 31px;\n  flex-shrink: 0;\n}\n\n.capitalize {\n  text-transform: capitalize;\n}\n\n.key {\n  display: flex;\n  width: 100%;\n  min-width: 100%;\n  max-width: 400px;\n  padding-left: 9px;\n  line-height: 31px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  userEvent,\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport * as keysSlice from 'uiSrc/slices/browser/keys'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { Props, KeyDetailsHeaderSizeLength } from './KeyDetailsHeaderSizeLength'\n\nlet store: typeof mockedStore\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeyDataSelector: jest.fn(),\n}))\n\nconst mockSelectedKeyDataSelector =\n  keysSlice.selectedKeyDataSelector as jest.Mock\n\ndescribe('KeyDetailsHeaderSizeLength', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n  })\n\n  it('should render normal size correctly', () => {\n    mockSelectedKeyDataSelector.mockReturnValueOnce({\n      type: 'string',\n      size: 1024,\n      length: 1,\n    })\n\n    render(\n      <KeyDetailsHeaderSizeLength {...instance(mockedProps)} width={1920} />,\n    )\n\n    expect(screen.getByTestId('key-size-text')).toBeInTheDocument()\n    expect(screen.queryByTestId('key-size-info-icon')).not.toBeInTheDocument()\n  })\n\n  it('should render too large size with warning icon and expected tooltip', async () => {\n    mockSelectedKeyDataSelector.mockReturnValueOnce({\n      type: 'string',\n      size: -1,\n      length: 1,\n    })\n\n    render(\n      <KeyDetailsHeaderSizeLength {...instance(mockedProps)} width={1920} />,\n    )\n\n    expect(screen.getByTestId('key-size-info-icon')).toBeInTheDocument()\n\n    const infoIcon = screen.getByTestId('key-size-info-icon')\n    userEvent.hover(infoIcon)\n\n    const tooltipText = await screen.findAllByText(\n      'The key size is too large to run the MEMORY USAGE command, as it may lead to performance issues.',\n    )\n    expect(tooltipText[0]).toBeInTheDocument()\n  })\n\n  it('should render \"Top-level values\" label when type is json', () => {\n    mockSelectedKeyDataSelector.mockReturnValueOnce({\n      type: KeyTypes.ReJSON,\n      size: 512,\n      length: 5,\n    })\n\n    render(\n      <KeyDetailsHeaderSizeLength {...instance(mockedProps)} width={1920} />,\n    )\n\n    expect(screen.getByTestId('key-length-text')).toHaveTextContent(\n      'Top-level values: 5',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/KeyDetailsHeaderSizeLength.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport {\n  LENGTH_NAMING_BY_TYPE,\n  MIDDLE_SCREEN_RESOLUTION,\n} from 'uiSrc/constants'\nimport {\n  initialKeyInfo,\n  selectedKeyDataSelector,\n} from 'uiSrc/slices/browser/keys'\nimport { formatBytes } from 'uiSrc/utils'\n\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  width: number\n}\n\nconst KeyDetailsHeaderSizeLength = ({ width }: Props) => {\n  const { type, size, length } =\n    useSelector(selectedKeyDataSelector) ?? initialKeyInfo\n\n  const isSizeTooLarge = size === -1\n\n  return (\n    <>\n      {size && (\n        <FlexItem>\n          <Text\n            size=\"s\"\n            className={styles.subtitleText}\n            data-testid=\"key-size-text\"\n          >\n            <RiTooltip\n              title=\"Key Size\"\n              position=\"left\"\n              content={\n                <>\n                  {isSizeTooLarge\n                    ? 'The key size is too large to run the MEMORY USAGE command, as it may lead to performance issues.'\n                    : formatBytes(size, 3)}\n                </>\n              }\n            >\n              <>\n                {width > MIDDLE_SCREEN_RESOLUTION && 'Key Size: '}\n                {formatBytes(size, 0)}\n                {isSizeTooLarge && (\n                  <>\n                    {' '}\n                    <RiIcon\n                      className={styles.infoIcon}\n                      type=\"InfoIcon\"\n                      size=\"m\"\n                      style={{ cursor: 'pointer' }}\n                      data-testid=\"key-size-info-icon\"\n                    />\n                  </>\n                )}\n              </>\n            </RiTooltip>\n          </Text>\n        </FlexItem>\n      )}\n      <FlexItem>\n        <Text\n          size=\"s\"\n          className={styles.subtitleText}\n          data-testid=\"key-length-text\"\n        >\n          {LENGTH_NAMING_BY_TYPE[type] ?? 'Length'}\n          {': '}\n          {length ?? '-'}\n        </Text>\n      </FlexItem>\n    </>\n  )\n}\n\nexport { KeyDetailsHeaderSizeLength }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/index.ts",
    "content": "export { KeyDetailsHeaderSizeLength } from './KeyDetailsHeaderSizeLength'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-size-length/styles.module.scss",
    "content": ".subtitleText {\n  padding: 6px 2px 6px 0;\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { Props, KeyDetailsHeaderTTL } from './KeyDetailsHeaderTTL'\n\nconst mockedProps = mock<Props>()\n\ndescribe('KeyDetailsHeaderTTL', () => {\n  it('should render', () => {\n    expect(\n      render(<KeyDetailsHeaderTTL {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/KeyDetailsHeaderTTL.tsx",
    "content": "import cx from 'classnames'\nimport React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'\nimport {\n  initialKeyInfo,\n  selectedKeyDataSelector,\n  selectedKeySelector,\n} from 'uiSrc/slices/browser/keys'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { MAX_TTL_NUMBER, validateTTLNumber } from 'uiSrc/utils'\n\nimport { FlexItem, Grid } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  onEditTTL: (key: RedisResponseBuffer, ttl: number) => void\n}\n\nconst KeyDetailsHeaderTTL = ({ onEditTTL }: Props) => {\n  const { loading } = useSelector(selectedKeySelector)\n  const {\n    ttl: ttlProp,\n    nameString: keyProp,\n    name: keyBuffer,\n  } = useSelector(selectedKeyDataSelector) ?? initialKeyInfo\n\n  const [ttl, setTTL] = useState(`${ttlProp}`)\n  const [ttlIsEditing, setTTLIsEditing] = useState(false)\n  const [ttlIsHovering, setTTLIsHovering] = useState(false)\n\n  useEffect(() => {\n    setTTL(`${ttlProp}`)\n  }, [keyProp, ttlProp, keyBuffer])\n\n  const onMouseEnterTTL = () => {\n    setTTLIsHovering(true)\n  }\n\n  const onMouseLeaveTTL = () => {\n    setTTLIsHovering(false)\n  }\n\n  const onClickTTL = () => {\n    setTTLIsEditing(true)\n  }\n\n  const onChangeTtl = (value: string) => {\n    ttlIsEditing && setTTL(validateTTLNumber(value) || '-1')\n  }\n\n  const applyEditTTL = () => {\n    const ttlValue = ttl || '-1'\n\n    setTTLIsEditing(false)\n    setTTLIsHovering(false)\n\n    if (`${ttlProp}` !== ttlValue && keyBuffer) {\n      onEditTTL(keyBuffer, +ttlValue)\n    }\n  }\n\n  const cancelEditTTl = (event: any) => {\n    setTTL(`${ttlProp}`)\n    setTTLIsEditing(false)\n    setTTLIsHovering(false)\n\n    event?.stopPropagation()\n  }\n\n  const appendTTLEditing = () =>\n    !ttlIsEditing ? (\n      <RiIcon\n        className={styles.iconPencil}\n        type=\"EditIcon\"\n        color=\"informative400\"\n      />\n    ) : (\n      ''\n    )\n\n  return (\n    <FlexItem\n      onMouseEnter={onMouseEnterTTL}\n      onMouseLeave={onMouseLeaveTTL}\n      onClick={onClickTTL}\n      className={styles.flexItemTTL}\n      data-testid=\"edit-ttl-btn\"\n    >\n      <>\n        {(ttlIsEditing || ttlIsHovering) && (\n          <Grid\n            columns={2}\n            responsive={false}\n            gap=\"s\"\n            className={styles.ttlGridComponent}\n            data-testid=\"edit-ttl-grid\"\n          >\n            <FlexItem>\n              <Text\n                size=\"s\"\n                className={cx(styles.subtitleText, styles.subtitleTextTTL)}\n              >\n                TTL:\n              </Text>\n            </FlexItem>\n            <FlexItem grow>\n              <InlineItemEditor\n                onApply={() => applyEditTTL()}\n                onDecline={(event) => cancelEditTTl(event)}\n                viewChildrenMode={!ttlIsEditing}\n                isLoading={loading}\n                declineOnUnmount={false}\n              >\n                <TextInput\n                  name=\"ttl\"\n                  id=\"ttl\"\n                  className={cx(\n                    styles.ttlInput,\n                    ttlIsEditing && styles.editing,\n                  )}\n                  maxLength={200}\n                  placeholder=\"No limit\"\n                  value={ttl === '-1' ? '' : ttl}\n                  fullWidth={false}\n                  compressed\n                  min={0}\n                  max={MAX_TTL_NUMBER}\n                  isLoading={loading}\n                  onChange={onChangeTtl}\n                  append={appendTTLEditing()}\n                  autoComplete=\"off\"\n                  data-testid=\"edit-ttl-input\"\n                />\n              </InlineItemEditor>\n            </FlexItem>\n          </Grid>\n        )}\n        <Text\n          size=\"s\"\n          className={cx(styles.subtitleTextTTL, {\n            [styles.hidden]: ttlIsEditing || ttlIsHovering,\n          })}\n          data-testid=\"key-ttl-text\"\n        >\n          TTL:\n          <span className={styles.ttlTextValue}>\n            {ttl === '-1' ? 'No limit' : ttl}\n          </span>\n        </Text>\n      </>\n    </FlexItem>\n  )\n}\n\nexport { KeyDetailsHeaderTTL }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/index.ts",
    "content": "export { KeyDetailsHeaderTTL } from './KeyDetailsHeaderTTL'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/components/key-details-header-ttl/styles.module.scss",
    "content": ".subtitleText {\n  padding: 6px 2px 6px 0;\n}\n\n.subtitleTextTTL {\n  height: 26px;\n  line-height: 26px;\n  padding: 0;\n}\n\n.controlsKey {\n  right: 25px;\n}\n\n.cancelEditBtn:hover {\n  color: var(--euiColorColorDanger) !important;\n}\n\n.applyEditBtn:hover {\n  color: var(--euiColorSecondary) !important;\n}\n\n.flexItemTTL {\n  width: 152px;\n  min-width: 152px;\n}\n\n.ttlInput {\n  min-width: 106px;\n  font-size: 12px !important;\n  height: 24px !important;\n  padding: 2px 0 0 9px !important;\n}\n\n.ttlGridComponent,\n.classNameGridComponent {\n  position: relative;\n}\n\n.ttlGridComponent {\n  height: 24px;\n  line-height: 24px;\n}\n\n.hidden {\n  display: none;\n}\n\n.ttlTextValue {\n  padding-left: 14px;\n}\n\n@include global.insights-open {\n  .flexItemTTL {\n    width: 134px;\n    min-width: 134px;\n  }\n\n  .ttlInput {\n    min-width: 90px;\n    font-size: 12px !important;\n    height: 25px !important;\n  }\n\n  :global(.euiFormControlLayout--compressed > .euiFormControlLayout__append) {\n    &.iconPencil {\n      width: 24px !important;\n      padding: 0 6px !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/index.ts",
    "content": "export { KeyDetailsHeader } from './KeyDetailsHeader'\nexport type { KeyDetailsHeaderProps } from './KeyDetailsHeader'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/modules/key-details-header/styles.module.scss",
    "content": ":global {\n  .browserPage {\n    .key-details-header {\n      .euiFieldText--compressed,\n      .euiFormControlLayout--compressed {\n        height: 29px !important;\n      }\n\n      .euiFormControlLayout {\n        width: 100%;\n        max-width: 100%;\n\n        &.euiFormControlLayout--readOnly {\n          border: 1px solid var(--controlsBorderColor);\n          cursor: auto;\n        }\n\n        input {\n          height: 29px !important;\n          cursor: pointer;\n          max-width: none;\n          font-family: \"Graphik\", sans-serif !important;\n        }\n      }\n    }\n  }\n}\n\n@include global.insights-open {\n  :global {\n    .key-details-header {\n      .euiText--small {\n        font-size: 12px;\n      }\n    }\n\n    .euiFlexGroup--gutterMedium {\n      margin: -4px;\n    }\n    .euiFlexGroup--gutterMedium > .euiFlexItem {\n      margin: 4px;\n    }\n  }\n}\n\n.container {\n  padding: 18px 18px 12px 18px;\n  border-bottom: 1px solid var(--euiColorLightShade);\n  min-width: 100%;\n  position: relative;\n}\n\n.closeBtn {\n  padding-top: 0 !important;\n\n  svg {\n    width: 20px;\n    height: 20px;\n  }\n}\n\n.groupSecondLine {\n  margin-top: 4px !important;\n}\n\n.subtitleActionBtns {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  right: 13px;\n}\n\n.actionBtn {\n  margin-right: 12px;\n  position: relative;\n  z-index: 2;\n}\n\n@include global.insights-open {\n  .actionBtn {\n    margin-right: 8px;\n\n    :global {\n      .auto-refresh-btn {\n        width: 32px;\n        height: 32px;\n\n        .euiButtonIcon__icon {\n          width: 16px;\n          height: 16px;\n        }\n      }\n    }\n  }\n}\n\n@include global.insights-open(1024px) {\n  .actionBtn {\n    margin-right: 8px;\n\n    :global {\n      .refresh-message-time {\n        display: none;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/browser/styles.module.scss",
    "content": "$breakpoint-to-hide-resize-panel: 1280px;\n\n.container {\n  max-width: 100vw;\n  display: flex;\n  flex-direction: column;\n  flex-grow: 1;\n  overflow: hidden;\n}\n\n.main {\n  display: flex;\n  flex-grow: 1;\n  padding: 0 16px;\n  overflow: hidden;\n}\n\n.resizableButton {\n  z-index: 10 !important;\n}\n\n.hidden {\n  display: none;\n}\n\n.keysContainer {\n  position: relative;\n  overflow: hidden;\n  height: 100%;\n  width: 100%;\n\n  @media (min-width: $breakpoint-to-hide-resize-panel) {\n    display: none;\n  }\n}\n\n.keyList {\n  width: 100%;\n  height: 100%;\n  position: relative;\n  border: none !important;\n  z-index: 0;\n}\n\n.keyDetails {\n  width: 100%;\n  height: 100%;\n  position: absolute !important;\n  left: 100%;\n  top: 0;\n  transition: left 0.25s ease;\n  will-change: left;\n}\n\n.keyDetailsOpen {\n  left: 0;\n\n  @media (max-width: 1123.98px) {\n    width: 100% !important;\n  }\n}\n\n.backBtn {\n  flex-grow: 0;\n  align-self: self-start;\n  margin: 0 0 8px 16px;\n\n  background-color: transparent !important;\n  border: 0 !important;\n  box-shadow: none !important;\n\n  &:hover {\n    color: var(--buttonSecondaryTextColor) !important\n  }\n}\n\n.resizableContainer {\n  position: relative;\n\n  .noVisible{\n    visibility: hidden;\n  }\n\n  .fullWidth {\n    width: 100% !important;\n    min-width: 100% !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { CLUSTER_DETAILS_DATA_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport {\n  getClusterDetails,\n  getClusterDetailsSuccess,\n} from 'uiSrc/slices/analytics/clusterDetails'\nimport { act, cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\n\nimport ClusterDetailsPage from './ClusterDetailsPage'\n\nlet store: typeof mockedStore\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '123',\n    connectionType: 'CLUSTER',\n  }),\n}))\n\n/**\n * ClusterDetailsPage tests\n *\n * @group component\n */\ndescribe('ClusterDetailsPage', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n  })\n\n  it('should render', async () => {\n    await act(() => {\n      expect(render(<ClusterDetailsPage />)).toBeTruthy()\n    })\n  })\n\n  it('should call fetchClusterDetailsAction after rendering', async () => {\n    await act(async () => {\n      render(<ClusterDetailsPage />)\n    })\n\n    const expectedActions = [\n      getClusterDetails(),\n      getClusterDetailsSuccess(CLUSTER_DETAILS_DATA_MOCK),\n    ]\n    expect(store.getActions()).toEqual([...expectedActions])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.styles.ts",
    "content": "import { Theme } from '@redis-ui/styles'\nimport styled from 'styled-components'\nimport { scrollbarStyles } from 'uiSrc/styles/mixins'\n\nexport const ClusterDetailsPageWrapper = styled.div`\n  ${scrollbarStyles()};\n  height: 100%;\n  padding: 0 ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/ClusterDetailsPage.tsx",
    "content": "import { orderBy } from 'lodash'\nimport React, { useContext, useEffect, useState, useMemo } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { ClusterNodeDetails } from 'src/modules/cluster-monitor/models'\n\nimport { Theme } from 'uiSrc/constants'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport {\n  clusterDetailsSelector,\n  fetchClusterDetailsAction,\n} from 'uiSrc/slices/analytics/clusterDetails'\nimport {\n  analyticsSettingsSelector,\n  setAnalyticsViewTab,\n} from 'uiSrc/slices/analytics/settings'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport {\n  formatLongName,\n  getDbIndex,\n  getLetterByIndex,\n  setTitle,\n} from 'uiSrc/utils'\nimport { ColorScheme, getRGBColorByScheme, RGBColor } from 'uiSrc/utils/colors'\n\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport {\n  ClusterDetailsHeader,\n  ClusterDetailsGraphics,\n  ClusterNodesTable,\n} from './components'\n\nimport * as S from './ClusterDetailsPage.styles'\n\nexport interface ModifiedClusterNodes extends ClusterNodeDetails {\n  letter: string\n  index: number\n  color: RGBColor\n}\n\nconst POLLING_INTERVAL = 5_000\n\nconst ClusterDetailsPage = () => {\n  let interval: NodeJS.Timeout\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const {\n    db,\n    name: connectedInstanceName,\n    connectionType,\n  } = useSelector(connectedInstanceSelector)\n  const { viewTab } = useSelector(analyticsSettingsSelector)\n  const { loading, data } = useSelector(clusterDetailsSelector)\n\n  const [isPageViewSent, setIsPageViewSent] = useState(false)\n\n  const dispatch = useDispatch()\n  const { theme } = useContext(ThemeContext)\n\n  const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}`\n  setTitle(`${dbName} - Overview`)\n\n  const colorScheme: ColorScheme = {\n    cHueStart: 180,\n    cHueRange: 140,\n    cSaturation: 55,\n    cLightness: theme === Theme.Dark ? 45 : 55,\n  }\n\n  useEffect(() => {\n    if (connectionType !== ConnectionType.Cluster) return\n\n    dispatch(\n      fetchClusterDetailsAction(\n        instanceId,\n        () => {},\n        () => clearInterval(interval),\n      ),\n    )\n\n    if (viewTab !== AnalyticsViewTab.ClusterDetails) {\n      dispatch(setAnalyticsViewTab(AnalyticsViewTab.ClusterDetails))\n    }\n  }, [])\n\n  useEffect(() => {\n    if (!loading) {\n      interval = setInterval(() => {\n        if (document.hidden) return\n\n        dispatch(\n          fetchClusterDetailsAction(\n            instanceId,\n            () => {},\n            () => clearInterval(interval),\n          ),\n        )\n      }, POLLING_INTERVAL)\n    }\n    return () => clearInterval(interval)\n  }, [instanceId, loading])\n\n  const nodes = useMemo(() => {\n    if (data) {\n      const nodes = orderBy(data.nodes, ['asc', 'host'])\n      const shift = colorScheme.cHueRange / nodes.length\n\n      return nodes.map((d, index) => ({\n        ...d,\n        letter: getLetterByIndex(index),\n        index,\n        color: getRGBColorByScheme(index, shift, colorScheme),\n      })) as ModifiedClusterNodes[]\n    }\n\n    return [] as ModifiedClusterNodes[]\n  }, [data])\n\n  useEffect(() => {\n    if (connectedInstanceName && !isPageViewSent) {\n      sendPageView(instanceId)\n    }\n  }, [connectedInstanceName, isPageViewSent])\n\n  const sendPageView = (instanceId: string) => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.CLUSTER_DETAILS_PAGE,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  return (\n    <S.ClusterDetailsPageWrapper as=\"div\" data-testid=\"cluster-details-page\">\n      <ClusterDetailsHeader />\n      <ClusterDetailsGraphics nodes={nodes} dataLoaded={!!data} />\n      <ClusterNodesTable nodes={nodes} dataLoaded={!!data} />\n    </S.ClusterDetailsPageWrapper>\n  )\n}\n\nexport default ClusterDetailsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.constants.ts",
    "content": "import { ColumnDef, SortingState } from 'uiSrc/components/base/layout/table'\n\nimport { ModifiedClusterNodes } from '../../ClusterDetailsPage'\nimport { ClusterNodesHostCell } from './components/ClusterNodesHostCell/ClusterNodesHostCell'\nimport { ClusterNodesNumericCell } from './components/ClusterNodesNumericCell/ClusterNodesNumericCell'\n\nexport const DEFAULT_SORTING: SortingState = [\n  {\n    id: 'host',\n    desc: false,\n  },\n]\n\nexport const DEFAULT_CLUSTER_NODES_COLUMNS: ColumnDef<ModifiedClusterNodes>[] =\n  [\n    {\n      header: ({ table }) => `${table.options.data.length} Primary nodes`,\n      isHeaderCustom: true,\n      id: 'host',\n      accessorKey: 'host',\n      enableSorting: true,\n      cell: ClusterNodesHostCell,\n    },\n    {\n      header: 'Commands/s',\n      id: 'opsPerSecond',\n      accessorKey: 'opsPerSecond',\n      enableSorting: true,\n      cell: ClusterNodesNumericCell,\n    },\n    {\n      header: 'Network Input',\n      id: 'networkInKbps',\n      accessorKey: 'networkInKbps',\n      enableSorting: true,\n      cell: ClusterNodesNumericCell,\n    },\n    {\n      header: 'Network Output',\n      id: 'networkOutKbps',\n      accessorKey: 'networkOutKbps',\n      enableSorting: true,\n      cell: ClusterNodesNumericCell,\n    },\n    {\n      header: 'Total Memory',\n      id: 'usedMemory',\n      accessorKey: 'usedMemory',\n      enableSorting: true,\n      cell: ClusterNodesNumericCell,\n    },\n    {\n      header: 'Total Keys',\n      id: 'totalKeys',\n      accessorKey: 'totalKeys',\n      enableSorting: true,\n      cell: ClusterNodesNumericCell,\n    },\n    {\n      header: 'Clients',\n      id: 'connectedClients',\n      accessorKey: 'connectedClients',\n      enableSorting: true,\n      cell: ClusterNodesNumericCell,\n    },\n  ]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.spec.tsx",
    "content": "import React from 'react'\nimport { getLetterByIndex } from 'uiSrc/utils'\nimport { rgb } from 'uiSrc/utils/colors'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ClusterNodesTable from './ClusterNodesTable'\nimport { ModifiedClusterNodes } from '../../ClusterDetailsPage'\nimport { ClusterNodeDetailsFactory } from 'uiSrc/mocks/factories/cluster/ClusterNodeDetails.factory'\n\nconst mockNodes = [\n  ClusterNodeDetailsFactory.build({\n    totalKeys: 1,\n    opsPerSecond: 1,\n  }),\n  ClusterNodeDetailsFactory.build({\n    totalKeys: 4,\n    opsPerSecond: 1,\n  }),\n  ClusterNodeDetailsFactory.build({\n    totalKeys: 10,\n    opsPerSecond: 0,\n  }),\n].map((d, index) => ({\n  ...d,\n  letter: getLetterByIndex(index),\n  index,\n  color: [0, 0, 0],\n})) as ModifiedClusterNodes[]\n\ndescribe('ClusterNodesTable', () => {\n  it('should render', () => {\n    expect(\n      render(<ClusterNodesTable nodes={mockNodes} dataLoaded />),\n    ).toBeTruthy()\n  })\n\n  it('should render loading content when data not yet loaded', () => {\n    render(<ClusterNodesTable nodes={[]} dataLoaded={false} />)\n    expect(\n      screen.getByTestId('primary-nodes-table-loading'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render empty state when data loaded and no nodes', () => {\n    render(<ClusterNodesTable nodes={[]} dataLoaded />)\n    expect(screen.getByTestId('primary-nodes-table-empty')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('primary-nodes-table-loading'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render table', () => {\n    const { container } = render(\n      <ClusterNodesTable nodes={mockNodes} dataLoaded />,\n    )\n    expect(container).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('primary-nodes-table-loading'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render table with 3 items', () => {\n    render(<ClusterNodesTable nodes={mockNodes} dataLoaded />)\n    expect(screen.getAllByTestId('node-letter')).toHaveLength(3)\n  })\n\n  it('should highlight max value for total keys', () => {\n    render(<ClusterNodesTable nodes={mockNodes} dataLoaded />)\n    expect(screen.getByTestId('totalKeys-value-max')).toHaveTextContent(\n      mockNodes[2].totalKeys.toString(),\n    )\n  })\n\n  it('should not highlight max value for opsPerSecond with equals values', () => {\n    render(<ClusterNodesTable nodes={mockNodes} dataLoaded />)\n    expect(\n      screen.queryByTestId('opsPerSecond-value-max'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render background color for each node', () => {\n    render(<ClusterNodesTable nodes={mockNodes} dataLoaded />)\n    mockNodes.forEach(({ letter, color }) => {\n      expect(screen.getByTestId(`node-color-${letter}`)).toHaveStyle({\n        'background-color': rgb(color),\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.tsx",
    "content": "import React, { useCallback } from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nimport {\n  DEFAULT_CLUSTER_NODES_COLUMNS,\n  DEFAULT_SORTING,\n} from './ClusterNodesTable.constants'\nimport { ClusterNodesEmptyState } from './components/ClusterNodesEmptyState/ClusterNodesEmptyState'\nimport { ClusterNodesTableProps } from './ClusterNodesTable.types'\n\nconst ClusterNodesTable = ({ nodes, dataLoaded }: ClusterNodesTableProps) => {\n  // Show loading until data is received; don't show during refresh polls\n  const showLoading = !dataLoaded\n\n  const renderEmptyState = useCallback(\n    () => <ClusterNodesEmptyState loading={showLoading} />,\n    [showLoading],\n  )\n\n  return (\n    <Table\n      columns={DEFAULT_CLUSTER_NODES_COLUMNS}\n      data={nodes}\n      defaultSorting={DEFAULT_SORTING}\n      emptyState={renderEmptyState}\n      maxHeight=\"20rem\"\n    />\n  )\n}\n\nexport default ClusterNodesTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types.ts",
    "content": "import { CellContext } from 'uiSrc/components/base/layout/table'\nimport { ModifiedClusterNodes } from '../../ClusterDetailsPage'\n\nexport type ClusterNodesTableProps = {\n  nodes: ModifiedClusterNodes[]\n  dataLoaded: boolean\n}\n\nexport type ClusterNodesTableCell = (\n  props: CellContext<ModifiedClusterNodes, unknown>,\n) => React.ReactElement<any, any> | null\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const EmptyStateWrapper = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  margin-top: 40px;\n  width: 100%;\n`\n\nexport const EmptyStateContent = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space400};\n  text-align: center;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesEmptyState/ClusterNodesEmptyState.tsx",
    "content": "import React from 'react'\n\nimport { LoadingContent } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport * as S from './ClusterNodesEmptyState.styles'\n\ninterface ClusterNodesEmptyStateProps {\n  loading: boolean\n}\n\nexport const ClusterNodesEmptyState = ({\n  loading,\n}: ClusterNodesEmptyStateProps) => {\n  if (loading) {\n    return (\n      <S.EmptyStateWrapper data-testid=\"primary-nodes-table-loading\">\n        <LoadingContent lines={4} />\n      </S.EmptyStateWrapper>\n    )\n  }\n\n  return (\n    <S.EmptyStateContent data-testid=\"primary-nodes-table-empty\">\n      <Text>\n        Primary node details are not available for this cluster configuration.\n      </Text>\n    </S.EmptyStateContent>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const LineIndicator = styled.div<{ $backgroundColor: string }>`\n  position: absolute;\n  left: 0;\n  top: 1px;\n  bottom: 1px;\n  width: 3px;\n  background-color: ${({ $backgroundColor }) => $backgroundColor};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesHostCell/ClusterNodesHostCell.tsx",
    "content": "import React from 'react'\n\nimport { rgb } from 'uiSrc/utils/colors'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { ClusterNodesTableCell } from 'uiSrc/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types'\n\nimport * as S from './ClusterNodesHostCell.styles'\n\nexport const ClusterNodesHostCell: ClusterNodesTableCell = ({\n  row: {\n    original: { letter, port, color, host },\n  },\n}) => (\n  <>\n    <S.LineIndicator\n      data-testid={`node-color-${letter}`}\n      $backgroundColor={rgb(color)}\n    />\n    <Row justify=\"between\">\n      <Text variant=\"semiBold\" data-testid=\"node-letter\">\n        {letter}\n      </Text>\n      <Text variant=\"regular\">\n        {host}:{port}\n      </Text>\n    </Row>\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { CellContext } from 'uiSrc/components/base/layout/table'\nimport { ModifiedClusterNodes } from '../../../../ClusterDetailsPage'\nimport { ClusterNodesNumericCell } from './ClusterNodesNumericCell'\nimport { ClusterNodeDetailsFactory } from 'uiSrc/mocks/factories/cluster/ClusterNodeDetails.factory'\n\nconst mockNodes: ModifiedClusterNodes[] = [\n  {\n    ...ClusterNodeDetailsFactory.build({\n      totalKeys: 100,\n      usedMemory: 2867968,\n      opsPerSecond: 50,\n      connectedClients: 6,\n      networkInKbps: 10.5,\n      networkOutKbps: 5.2,\n    }),\n    letter: 'A',\n    index: 0,\n    color: [0, 0, 0],\n  },\n  {\n    ...ClusterNodeDetailsFactory.build({\n      totalKeys: 200,\n      usedMemory: 2825880,\n      opsPerSecond: 75,\n      connectedClients: 4,\n      networkInKbps: 20.3,\n      networkOutKbps: 10.1,\n    }),\n    letter: 'B',\n    index: 1,\n    color: [0, 0, 0],\n  },\n  {\n    ...ClusterNodeDetailsFactory.build({\n      totalKeys: 150,\n      usedMemory: 2886960,\n      opsPerSecond: 60,\n      connectedClients: 7,\n      networkInKbps: 15.7,\n      networkOutKbps: 8.3,\n    }),\n    letter: 'C',\n    index: 2,\n    color: [0, 0, 0],\n  },\n]\n\nconst createMockCellContext = (\n  nodeIndex: number,\n  field: keyof ModifiedClusterNodes,\n): CellContext<ModifiedClusterNodes, unknown> =>\n  ({\n    row: {\n      original: mockNodes[nodeIndex],\n    },\n    column: {\n      id: field,\n    },\n    table: {\n      options: {\n        data: mockNodes,\n      },\n    },\n  }) as CellContext<ModifiedClusterNodes, unknown>\n\ndescribe('ClusterNodesNumericCell', () => {\n  describe('renderComponent', () => {\n    const renderComponent = (\n      nodeIndex: number,\n      field: keyof ModifiedClusterNodes,\n    ) => {\n      const context = createMockCellContext(nodeIndex, field)\n      return render(<ClusterNodesNumericCell {...context} />)\n    }\n\n    it('should render numeric value', () => {\n      renderComponent(0, 'totalKeys')\n      expect(screen.getByTestId('totalKeys-value')).toHaveTextContent('100')\n    })\n\n    it('should render max value with semiBold variant', () => {\n      renderComponent(1, 'totalKeys')\n      expect(screen.getByTestId('totalKeys-value-max')).toBeInTheDocument()\n    })\n\n    it('should not render max indicator when value is not maximum', () => {\n      renderComponent(0, 'totalKeys')\n      expect(\n        screen.queryByTestId('totalKeys-value-max'),\n      ).not.toBeInTheDocument()\n      expect(screen.getByTestId('totalKeys-value')).toBeInTheDocument()\n    })\n\n    it('should format usedMemory with bytes formatter', () => {\n      renderComponent(0, 'usedMemory')\n      // formatBytes(2867968, 3, false) should format to something like \"2.73 MB\"\n      const element = screen.getByTestId('usedMemory-value')\n      expect(element.textContent).toMatch(/MB|KB|GB/)\n    })\n\n    it('should format networkInKbps with kb/s suffix', () => {\n      renderComponent(0, 'networkInKbps')\n      expect(screen.getByTestId('networkInKbps-value')).toHaveTextContent(\n        '10.5 kb/s',\n      )\n    })\n\n    it('should format networkOutKbps with kb/s suffix', () => {\n      renderComponent(0, 'networkOutKbps')\n      expect(screen.getByTestId('networkOutKbps-value')).toHaveTextContent(\n        '5.2 kb/s',\n      )\n    })\n\n    it('should return null for non-numeric fields', () => {\n      const context = createMockCellContext(0, 'host')\n      const { container } = render(<ClusterNodesNumericCell {...context} />)\n      expect(container.firstChild).toBeNull()\n    })\n\n    it('should handle zero values', () => {\n      const nodesWithZero = [\n        { ...mockNodes[0], opsPerSecond: 0 },\n        { ...mockNodes[1], opsPerSecond: 10 },\n        { ...mockNodes[2], opsPerSecond: 5 },\n      ]\n\n      const context = {\n        row: { original: nodesWithZero[0] },\n        column: { id: 'opsPerSecond' },\n        table: { options: { data: nodesWithZero } },\n      } as CellContext<ModifiedClusterNodes, unknown>\n\n      render(<ClusterNodesNumericCell {...context} />)\n      expect(screen.getByTestId('opsPerSecond-value')).toHaveTextContent('0')\n    })\n\n    it('should not highlight max value when there is a tie', () => {\n      const nodesWithTie = [\n        { ...mockNodes[0], connectedClients: 10 },\n        { ...mockNodes[1], connectedClients: 10 },\n        { ...mockNodes[2], connectedClients: 5 },\n      ]\n\n      const context = {\n        row: { original: nodesWithTie[0] },\n        column: { id: 'connectedClients' },\n        table: { options: { data: nodesWithTie } },\n      } as CellContext<ModifiedClusterNodes, unknown>\n\n      render(<ClusterNodesNumericCell {...context} />)\n      expect(\n        screen.queryByTestId('connectedClients-value-max'),\n      ).not.toBeInTheDocument()\n      expect(screen.getByTestId('connectedClients-value')).toBeInTheDocument()\n    })\n\n    it('should highlight max value for opsPerSecond', () => {\n      renderComponent(1, 'opsPerSecond')\n      expect(screen.getByTestId('opsPerSecond-value-max')).toBeInTheDocument()\n    })\n\n    it('should highlight max value for networkInKbps', () => {\n      renderComponent(1, 'networkInKbps')\n      expect(screen.getByTestId('networkInKbps-value-max')).toBeInTheDocument()\n    })\n\n    it('should highlight max value for networkOutKbps', () => {\n      renderComponent(1, 'networkOutKbps')\n      expect(screen.getByTestId('networkOutKbps-value-max')).toBeInTheDocument()\n    })\n\n    it('should highlight max value for connectedClients', () => {\n      renderComponent(2, 'connectedClients')\n      expect(\n        screen.getByTestId('connectedClients-value-max'),\n      ).toBeInTheDocument()\n    })\n\n    it('should format large numbers with spaces', () => {\n      const nodesWithLargeNumbers = [\n        { ...mockNodes[0], totalKeys: 1000000 },\n        { ...mockNodes[1], totalKeys: 500000 },\n        { ...mockNodes[2], totalKeys: 250000 },\n      ]\n\n      const context = {\n        row: { original: nodesWithLargeNumbers[0] },\n        column: { id: 'totalKeys' },\n        table: { options: { data: nodesWithLargeNumbers } },\n      } as CellContext<ModifiedClusterNodes, unknown>\n\n      render(<ClusterNodesNumericCell {...context} />)\n      // numberWithSpaces should format 1000000 as \"1 000 000\"\n      const element = screen.getByTestId('totalKeys-value-max')\n      expect(element.textContent).toContain('000')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/ClusterNodesNumericCell.tsx",
    "content": "import React from 'react'\n\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { ClusterNodesTableCell } from 'uiSrc/pages/cluster-details/components/ClusterNodesTable/ClusterNodesTable.types'\n\nimport { isMaxColumnFieldValue } from './utils/isMaxColumnFieldValue'\nimport {\n  displayValueFormatter,\n  tooltipContentFormatter,\n} from './utils/formatters'\n\nexport const ClusterNodesNumericCell: ClusterNodesTableCell = ({\n  row,\n  column,\n  table,\n}) => {\n  const item = row.original\n  const field = column.id as keyof typeof item\n  const value = item[field] ?? 0\n\n  if (typeof value !== 'number') {\n    return null\n  }\n\n  const data = table.options.data\n  const isMax = isMaxColumnFieldValue(field, value, data)\n\n  const displayValue = (displayValueFormatter[field] ?? numberWithSpaces)(value)\n  const tooltipContent = tooltipContentFormatter[field]?.(value)\n\n  return (\n    <RiTooltip content={tooltipContent} data-testid={`${field}-tooltip`}>\n      <Text\n        variant={isMax ? 'semiBold' : 'regular'}\n        data-testid={`${field}-value${isMax ? '-max' : ''}`}\n      >\n        {displayValue}\n      </Text>\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/formatters.ts",
    "content": "import { formatBytes } from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { ModifiedClusterNodes } from 'uiSrc/pages/cluster-details/ClusterDetailsPage'\n\nexport const displayValueFormatter: Partial<\n  Record<keyof ModifiedClusterNodes, (v: number) => string>\n> = {\n  usedMemory: (v) => formatBytes(v, 3, false).toString(),\n  networkInKbps: (v) => `${numberWithSpaces(v)} kb/s`,\n  networkOutKbps: (v) => `${numberWithSpaces(v)} kb/s`,\n}\n\nexport const tooltipContentFormatter: Partial<\n  Record<keyof ModifiedClusterNodes, (v: number) => string>\n> = {\n  usedMemory: (v) => `${numberWithSpaces(v)} B`,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.spec.ts",
    "content": "import { isMaxColumnFieldValue } from './isMaxColumnFieldValue'\n\ninterface TestNode {\n  id: string\n  value: number\n  otherValue: number\n}\n\ndescribe('isMaxColumnFieldValue', () => {\n  it('should return true when value is the maximum and unique', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 10, otherValue: 5 },\n      { id: '2', value: 20, otherValue: 15 },\n      { id: '3', value: 5, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 20, data)).toBe(true)\n  })\n\n  it('should return false when value is not the maximum', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 10, otherValue: 5 },\n      { id: '2', value: 20, otherValue: 15 },\n      { id: '3', value: 5, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 10, data)).toBe(false)\n    expect(isMaxColumnFieldValue('value', 5, data)).toBe(false)\n  })\n\n  it('should return false when there is a tie (multiple nodes with max value)', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 20, otherValue: 5 },\n      { id: '2', value: 20, otherValue: 15 },\n      { id: '3', value: 5, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 20, data)).toBe(false)\n  })\n\n  it('should return true when all other values are lower', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 100, otherValue: 5 },\n      { id: '2', value: 50, otherValue: 15 },\n      { id: '3', value: 25, otherValue: 25 },\n      { id: '4', value: 10, otherValue: 30 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 100, data)).toBe(true)\n  })\n\n  it('should handle zero values correctly', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 0, otherValue: 5 },\n      { id: '2', value: 10, otherValue: 15 },\n      { id: '3', value: 5, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 10, data)).toBe(true)\n    expect(isMaxColumnFieldValue('value', 0, data)).toBe(false)\n  })\n\n  it('should return false when all values are zero', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 0, otherValue: 5 },\n      { id: '2', value: 0, otherValue: 15 },\n      { id: '3', value: 0, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 0, data)).toBe(false)\n  })\n\n  it('should return true when there is only one node', () => {\n    const data: TestNode[] = [{ id: '1', value: 10, otherValue: 5 }]\n\n    expect(isMaxColumnFieldValue('value', 10, data)).toBe(true)\n  })\n\n  it('should handle empty data array', () => {\n    const data: TestNode[] = []\n\n    expect(isMaxColumnFieldValue('value', 10, data)).toBe(false)\n  })\n\n  it('should handle negative values', () => {\n    const data: TestNode[] = [\n      { id: '1', value: -10, otherValue: 5 },\n      { id: '2', value: -5, otherValue: 15 },\n      { id: '3', value: -20, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', -5, data)).toBe(true)\n    expect(isMaxColumnFieldValue('value', -10, data)).toBe(false)\n  })\n\n  it('should handle large numbers', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 1000000, otherValue: 5 },\n      { id: '2', value: 999999, otherValue: 15 },\n      { id: '3', value: 500000, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 1000000, data)).toBe(true)\n  })\n\n  it('should work with different field names', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 10, otherValue: 5 },\n      { id: '2', value: 20, otherValue: 15 },\n      { id: '3', value: 5, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('otherValue', 25, data)).toBe(true)\n    expect(isMaxColumnFieldValue('otherValue', 15, data)).toBe(false)\n  })\n\n  it('should return false when value does not exist in data', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 10, otherValue: 5 },\n      { id: '2', value: 20, otherValue: 15 },\n      { id: '3', value: 5, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 100, data)).toBe(false)\n  })\n\n  it('should handle three-way tie', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 10, otherValue: 5 },\n      { id: '2', value: 10, otherValue: 15 },\n      { id: '3', value: 10, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 10, data)).toBe(false)\n  })\n\n  it('should handle decimal values', () => {\n    const data: TestNode[] = [\n      { id: '1', value: 10.5, otherValue: 5 },\n      { id: '2', value: 10.7, otherValue: 15 },\n      { id: '3', value: 10.3, otherValue: 25 },\n    ]\n\n    expect(isMaxColumnFieldValue('value', 10.7, data)).toBe(true)\n    expect(isMaxColumnFieldValue('value', 10.5, data)).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/ClusterNodesTable/components/ClusterNodesNumericCell/utils/isMaxColumnFieldValue.ts",
    "content": "export const isMaxColumnFieldValue = <T>(\n  field: keyof T,\n  value: number,\n  data: T[],\n): boolean => {\n  const numericValues = data\n    .map((node) => node[field])\n    .filter((v) => typeof v === 'number')\n\n  return (\n    Math.max(...numericValues) === value &&\n    numericValues.filter((v) => v === value).length === 1\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-graphics/ClusterDetailsGraphics.spec.tsx",
    "content": "import React from 'react'\nimport { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ClusterDetailsGraphics from './ClusterDetailsGraphics'\n\nconst mockNodes = [\n  {\n    id: '1',\n    host: '0.0.0.1',\n    port: 6379,\n    role: 'primary',\n    slots: ['10923-16383'],\n    health: 'online',\n    totalKeys: 1,\n    usedMemory: 2867968,\n    opsPerSecond: 1,\n    connectionsReceived: 13,\n    connectedClients: 6,\n    commandsProcessed: 5678,\n    networkInKbps: 0.02,\n    networkOutKbps: 0,\n    cacheHitRatio: 1,\n    replicationOffset: 6924,\n    uptimeSec: 5614,\n    version: '6.2.6',\n    mode: 'cluster',\n    replicas: [],\n  },\n  {\n    id: '2',\n    host: '0.0.0.2',\n    port: 6379,\n    role: 'primary',\n    slots: ['0-5460'],\n    health: 'online',\n    totalKeys: 4,\n    usedMemory: 2825880,\n    opsPerSecond: 1,\n    connectionsReceived: 15,\n    connectedClients: 4,\n    commandsProcessed: 5667,\n    networkInKbps: 0.04,\n    networkOutKbps: 0,\n    cacheHitRatio: 1,\n    replicationOffset: 6910,\n    uptimeSec: 5609,\n    version: '6.2.6',\n    mode: 'cluster',\n    replicas: [],\n  },\n  {\n    id: '3',\n    host: '0.0.0.3',\n    port: 6379,\n    role: 'primary',\n    slots: ['5461-10922'],\n    health: 'online',\n    totalKeys: 10,\n    usedMemory: 2886960,\n    opsPerSecond: 0,\n    connectionsReceived: 18,\n    connectedClients: 7,\n    commandsProcessed: 5697,\n    networkInKbps: 0.02,\n    networkOutKbps: 0,\n    cacheHitRatio: 0,\n    replicationOffset: 6991,\n    uptimeSec: 5609,\n    version: '6.2.6',\n    mode: 'cluster',\n    replicas: [],\n  },\n].map((d, index) => ({\n  ...d,\n  letter: 'A',\n  index,\n  color: [0, 0, 0],\n})) as ModifiedClusterNodes[]\n\ndescribe('ClusterDetailsGraphics', () => {\n  it('should render', () => {\n    expect(\n      render(<ClusterDetailsGraphics nodes={mockNodes} dataLoaded />),\n    ).toBeTruthy()\n  })\n\n  it('should render loading when data not yet loaded', () => {\n    render(<ClusterDetailsGraphics nodes={[]} dataLoaded={false} />)\n    expect(\n      screen.getByTestId('cluster-details-graphics-loading'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('cluster-details-charts'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render nothing when data loaded but no nodes', () => {\n    render(<ClusterDetailsGraphics nodes={[]} dataLoaded />)\n    expect(\n      screen.queryByTestId('cluster-details-graphics-loading'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('cluster-details-charts'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render donuts', () => {\n    render(<ClusterDetailsGraphics nodes={mockNodes} dataLoaded />)\n    expect(screen.getByTestId('donut-memory')).toBeInTheDocument()\n    expect(screen.queryByTestId('donut-keys')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-graphics/ClusterDetailsGraphics.tsx",
    "content": "import cx from 'classnames'\nimport { sumBy } from 'lodash'\nimport React, { useEffect, useState } from 'react'\nimport { DonutChart } from 'uiSrc/components/charts'\nimport { ChartData } from 'uiSrc/components/charts/donut-chart/DonutChart'\nimport { ModifiedClusterNodes } from 'uiSrc/pages/cluster-details/ClusterDetailsPage'\nimport { formatBytes, Nullable } from 'uiSrc/utils'\nimport { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { Title } from 'uiSrc/components/base/text/Title'\n\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nconst ClusterDetailsGraphics = ({\n  nodes,\n  dataLoaded,\n}: {\n  nodes: Nullable<ModifiedClusterNodes[]>\n  dataLoaded: boolean\n}) => {\n  // Show loading until data is received; don't show during refresh polls\n  const showLoading = !dataLoaded\n  const [memoryData, setMemoryData] = useState<ChartData[]>([])\n  const [memorySum, setMemorySum] = useState(0)\n  const [keysData, setKeysData] = useState<ChartData[]>([])\n  const [keysSum, setKeysSum] = useState(0)\n\n  const renderMemoryTooltip = (data: ChartData) => (\n    <div className={styles.labelTooltip}>\n      <div className={styles.tooltipTitle}>\n        <span data-testid=\"tooltip-node-name\">{data.name}: </span>\n        <span data-testid=\"tooltip-host-port\">\n          {data.meta?.host}:{data.meta?.port}\n        </span>\n      </div>\n      <b>\n        <span\n          className={styles.tooltipPercentage}\n          data-testid=\"tooltip-node-percent\"\n        >\n          {getPercentage(data.value, memorySum)}%\n        </span>\n        <span data-testid=\"tooltip-total-memory\">\n          (&thinsp;{formatBytes(data.value, 3, false)}&thinsp;)\n        </span>\n      </b>\n    </div>\n  )\n\n  const renderKeysTooltip = (data: ChartData) => (\n    <div className={styles.labelTooltip}>\n      <div className={styles.tooltipTitle}>\n        <span data-testid=\"tooltip-node-name\">{data.name}: </span>\n        <span data-testid=\"tooltip-host-port\">\n          {data.meta?.host}:{data.meta?.port}\n        </span>\n      </div>\n      <b>\n        <span\n          className={styles.tooltipPercentage}\n          data-testid=\"tooltip-node-percent\"\n        >\n          {getPercentage(data.value, keysSum)}%\n        </span>\n        <span data-testid=\"tooltip-total-keys\">\n          (&thinsp;{numberWithSpaces(data.value)}&thinsp;)\n        </span>\n      </b>\n    </div>\n  )\n\n  useEffect(() => {\n    if (nodes) {\n      const memory = nodes.map((n) => ({\n        value: n.usedMemory,\n        name: n.letter,\n        color: n.color,\n        meta: { ...n },\n      }))\n      const keys = nodes.map((n) => ({\n        value: n.totalKeys,\n        name: n.letter,\n        color: n.color,\n        meta: { ...n },\n      }))\n\n      setMemoryData(memory as ChartData[])\n      setKeysData(keys as ChartData[])\n\n      setMemorySum(sumBy(memory, 'value'))\n      setKeysSum(sumBy(keys, 'value'))\n    }\n  }, [nodes])\n\n  if (showLoading && !nodes?.length) {\n    return (\n      <div\n        className={cx(styles.wrapper, styles.loadingWrapper)}\n        data-testid=\"cluster-details-graphics-loading\"\n      >\n        <div className={styles.preloaderCircle} />\n        <div className={styles.preloaderCircle} />\n      </div>\n    )\n  }\n\n  if (!nodes || nodes.length === 0) {\n    return null\n  }\n\n  return (\n    <div className={styles.wrapper} data-testid=\"cluster-details-charts\">\n      <DonutChart\n        name=\"memory\"\n        data={memoryData}\n        renderTooltip={renderMemoryTooltip}\n        labelAs=\"percentage\"\n        title={\n          <div className={styles.chartCenter}>\n            <div className={styles.chartTitle} data-testid=\"donut-title-memory\">\n              <RiIcon type=\"MemoryIconIcon\" className={styles.icon} size=\"m\" />\n              <Title size=\"XS\">Memory</Title>\n            </div>\n            <hr className={styles.titleSeparator} />\n            <div className={styles.centerCount}>\n              {formatBytes(memorySum, 3)}\n            </div>\n          </div>\n        }\n      />\n      <DonutChart\n        name=\"keys\"\n        data={keysData}\n        renderTooltip={renderKeysTooltip}\n        labelAs=\"percentage\"\n        title={\n          <div className={styles.chartCenter}>\n            <div className={styles.chartTitle} data-testid=\"donut-title-keys\">\n              <RiIcon type=\"KeyIconIcon\" className={styles.icon} size=\"m\" />\n              <Title size=\"XS\">Keys</Title>\n            </div>\n            <hr className={styles.titleSeparator} />\n            <div className={styles.centerCount}>\n              {numberWithSpaces(keysSum)}\n            </div>\n          </div>\n        }\n      />\n    </div>\n  )\n}\n\nexport default ClusterDetailsGraphics\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-graphics/index.ts",
    "content": "import ClusterDetailsGraphics from './ClusterDetailsGraphics'\n\nexport default ClusterDetailsGraphics\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-graphics/styles.module.scss",
    "content": ".wrapper {\n  background-color: var(--euiColorLightestShade);\n  border-radius: 16px;\n\n  display: flex;\n  align-items: center;\n  justify-content: space-around;\n  margin-bottom: 24px;\n\n  &.loadingWrapper {\n    margin-top: 36px;\n  }\n\n  .chartCenter {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n  }\n\n  .chartTitle {\n    display: flex;\n    align-items: center;\n\n    .icon {\n      margin-right: 10px;\n    }\n  }\n\n  .titleSeparator {\n    height: 1px;\n    border: 0;\n    background-color: var(--separatorColorLight);\n    margin: 6px 0;\n    width: 60px;\n  }\n\n  .centerCount {\n    margin-top: 2px;\n    font-weight: 500;\n    font-size: 14px;\n  }\n\n  .preloaderCircle {\n    width: 180px;\n    height: 180px;\n    margin: 60px 0;\n    border-radius: 100%;\n    background-color: var(--separatorColor);\n  }\n\n  .labelTooltip {\n    font-size: 12px;\n\n    .tooltipPercentage {\n      margin-right: 6px;\n    }\n\n    .tooltipTitle {\n      margin-bottom: 6px;\n    }\n  }\n}\n\n@include global.insights-open(1260px) {\n  .wrapper {\n    flex-direction: column;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-header/ClusterDetailsHeader.spec.tsx",
    "content": "import React from 'react'\nimport { clusterDetailsSelector } from 'uiSrc/slices/analytics/clusterDetails'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ClusterDetailsHeader from './ClusterDetailsHeader'\n\njest.mock('uiSrc/slices/analytics/clusterDetails', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/clusterDetails'),\n  clusterDetailsSelector: jest.fn().mockReturnValue({\n    data: null,\n    loading: false,\n    error: '',\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    username: '',\n  }),\n}))\n\ndescribe('ClusterDetailsHeader', () => {\n  it('should render', () => {\n    expect(render(<ClusterDetailsHeader />)).toBeTruthy()\n  })\n\n  it('should render \"EuiLoadingContent\" until loading and no data', () => {\n    ;(clusterDetailsSelector as jest.Mock).mockImplementation(() => ({\n      data: null,\n      loading: true,\n      error: '',\n    }))\n\n    render(<ClusterDetailsHeader />)\n\n    expect(screen.getByTestId('cluster-details-loading')).toBeInTheDocument()\n  })\n  it('should render \"cluster-details-content\" after loading and with data', () => {\n    ;(clusterDetailsSelector as jest.Mock).mockImplementation(() => ({\n      data: { version: '111' },\n      loading: false,\n      error: '',\n    }))\n\n    const { queryByTestId } = render(<ClusterDetailsHeader />)\n\n    expect(queryByTestId('cluster-details-loading')).not.toBeInTheDocument()\n    expect(queryByTestId('cluster-details-username')).not.toBeInTheDocument()\n    expect(queryByTestId('cluster-details-content')).toBeInTheDocument()\n  })\n\n  it('huge username should be truncated', () => {\n    ;(clusterDetailsSelector as jest.Mock).mockImplementation(() => ({\n      data: { version: '111' },\n      loading: false,\n      error: '',\n    }))\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      username: Array.from({ length: 50 }).fill('test').join(''),\n    }))\n\n    const { queryByTestId } = render(<ClusterDetailsHeader />)\n\n    expect(queryByTestId('cluster-details-username')).toBeInTheDocument()\n  })\n\n  it.skip('uptime should be with truncated to first unit', () => {\n    ;(clusterDetailsSelector as jest.Mock).mockImplementation(() => ({\n      data: { uptimeSec: 11111 },\n      loading: false,\n      error: '',\n    }))\n\n    const { queryByTestId } = render(<ClusterDetailsHeader />)\n\n    expect(queryByTestId('cluster-details-uptime')).toHaveTextContent('3 h')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-header/ClusterDetailsHeader.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { Row, Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Col)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nexport const Content = styled(Row)`\n  padding-top: ${({ theme }) => theme.core.space.space150};\n  padding-bottom: ${({ theme }) => theme.core.space.space250};\n`\n\nexport const Item = styled(Col)<{ $borderLeft?: boolean }>`\n  padding-right: ${({ theme }) => theme.core.space.space150};\n\n  ${({ $borderLeft, theme }) =>\n    $borderLeft &&\n    css`\n      border-left: 2px solid ${theme.semantic.color.border.neutral500};\n      padding-left: ${theme.core.space.space150};\n    `}\n`\n\nexport const Loading = styled.div`\n  width: 422px;\n  padding-top: ${({ theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-header/ClusterDetailsHeader.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport {\n  truncateNumberToFirstUnit,\n  formatLongName,\n  truncateNumberToDuration,\n} from 'uiSrc/utils'\nimport { nullableNumberWithSpaces } from 'uiSrc/utils/numbers'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  ConnectionType,\n  CONNECTION_TYPE_DISPLAY,\n} from 'uiSrc/slices/interfaces'\nimport { clusterDetailsSelector } from 'uiSrc/slices/analytics/clusterDetails'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { AnalyticsPageHeader } from 'uiSrc/pages/database-analysis/components/analytics-page-header'\n\nimport {\n  Container,\n  Content,\n  Item,\n  Loading,\n} from './ClusterDetailsHeader.styles'\n\ninterface IMetrics {\n  label: string\n  value: any\n  border?: 'left'\n}\n\nconst MAX_NAME_LENGTH = 30\nconst DEFAULT_USERNAME = 'Default'\n\nconst ClusterDetailsHeader = () => {\n  const {\n    username = DEFAULT_USERNAME,\n    connectionType = ConnectionType.Cluster,\n  } = useSelector(connectedInstanceSelector)\n\n  const { data, loading } = useSelector(clusterDetailsSelector)\n\n  const metrics: IMetrics[] = [\n    {\n      label: 'Type',\n      value: CONNECTION_TYPE_DISPLAY[connectionType],\n    },\n    {\n      label: 'Version',\n      value: data?.version || '',\n    },\n    {\n      label: 'User',\n      value:\n        (username || DEFAULT_USERNAME)?.length < MAX_NAME_LENGTH ? (\n          username || DEFAULT_USERNAME\n        ) : (\n          <RiTooltip\n            anchorClassName=\"truncateText\"\n            position=\"bottom\"\n            content={<>{formatLongName(username || DEFAULT_USERNAME)}</>}\n          >\n            <div data-testid=\"cluster-details-username\">\n              {formatLongName(username || DEFAULT_USERNAME, MAX_NAME_LENGTH, 5)}\n            </div>\n          </RiTooltip>\n        ),\n    },\n    {\n      label: 'Uptime',\n      border: 'left',\n      value: (\n        <RiTooltip\n          position=\"top\"\n          content={\n            <>\n              {`${nullableNumberWithSpaces(data?.uptimeSec) || 0} s`}\n              <br />\n              {`(${truncateNumberToDuration(data?.uptimeSec || 0)})`}\n            </>\n          }\n        >\n          <div data-testid=\"cluster-details-uptime\">\n            {truncateNumberToFirstUnit(data?.uptimeSec || 0)}\n          </div>\n        </RiTooltip>\n      ),\n    },\n  ]\n\n  return (\n    <Container data-testid=\"cluster-details-header\">\n      <AnalyticsPageHeader />\n      {loading && !data && (\n        <Loading as=\"div\" data-testid=\"cluster-details-loading\">\n          <LoadingContent lines={2} />\n        </Loading>\n      )}\n      {data && (\n        <Content data-testid=\"cluster-details-content\">\n          {metrics.map(({ value, label, border }) => (\n            <Item\n              key={label}\n              $borderLeft={border === 'left'}\n              data-testid={`cluster-details-item-${label}`}\n            >\n              <Text color=\"subdued\">{value}</Text>\n              <Text>{label}</Text>\n            </Item>\n          ))}\n        </Content>\n      )}\n    </Container>\n  )\n}\n\nexport default ClusterDetailsHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/cluster-details-header/index.ts",
    "content": "import ClusterDetailsHeader from './ClusterDetailsHeader'\n\nexport default ClusterDetailsHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/components/index.ts",
    "content": "import ClusterDetailsHeader from './cluster-details-header'\nimport ClusterDetailsGraphics from './cluster-details-graphics'\nimport ClusterNodesTable from './ClusterNodesTable/ClusterNodesTable'\n\nexport { ClusterDetailsHeader, ClusterDetailsGraphics, ClusterNodesTable }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/cluster-details/index.ts",
    "content": "import ClusterDetailsPage from './ClusterDetailsPage'\n\nexport default ClusterDetailsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPage.spec.tsx",
    "content": "import React from 'react'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { fetchDBAnalysisReportsHistory } from 'uiSrc/slices/analytics/dbAnalysis'\nimport {\n  render,\n  screen,\n  waitForRedisUiSelectVisible,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\n\nimport DatabaseAnalysisPage from './DatabaseAnalysisPage'\n\njest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'),\n  fetchDBAnalysisReportsHistory: jest.fn(),\n  dbAnalysisReportsSelector: jest.fn().mockReturnValue({\n    data: [{ id: '123', createdAt: Date.now(), db: 0 }],\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\n/**\n * DatabaseAnalysisPage tests\n *\n * @group component\n */\ndescribe('DatabaseAnalysisPage', () => {\n  it('should call fetchDBAnalysisReportsHistory after rendering', async () => {\n    const fetchDBAnalysisReportsHistoryMock = jest.fn()\n    ;(fetchDBAnalysisReportsHistory as jest.Mock).mockImplementation(\n      () => fetchDBAnalysisReportsHistoryMock,\n    )\n\n    expect(render(<DatabaseAnalysisPage />)).toBeTruthy()\n    expect(fetchDBAnalysisReportsHistoryMock).toBeCalled()\n  })\n\n  it('should send telemetry event after click \"new analysis\" btn', async () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<DatabaseAnalysisPage />)\n\n    await userEvent.click(screen.getByTestId('select-report'))\n\n    await waitForRedisUiSelectVisible()\n\n    await userEvent.click(\n      document.querySelector('[data-test-subj=\"items-report-123\"]') as Element,\n    )\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.DATABASE_ANALYSIS_HISTORY_VIEWED,\n      eventData: {\n        databaseId: 'instanceId',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n\n    sendEventTelemetry.mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  dbAnalysisReportsSelector,\n  dbAnalysisSelector,\n  fetchDBAnalysisAction,\n  fetchDBAnalysisReportsHistory,\n  setSelectedAnalysisId,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport {\n  analyticsSettingsSelector,\n  setAnalyticsViewTab,\n} from 'uiSrc/slices/analytics/settings'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport {\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n  TelemetryEvent,\n  TelemetryPageView,\n} from 'uiSrc/telemetry'\nimport { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils'\nimport { DatabaseAnalysisPageView } from './DatabaseAnalysisPageView'\n\nexport const DatabaseAnalysisPage = () => {\n  const { viewTab } = useSelector(analyticsSettingsSelector)\n  const { loading: analysisLoading, data } = useSelector(dbAnalysisSelector)\n  const { data: reports, selectedAnalysis } = useSelector(\n    dbAnalysisReportsSelector,\n  )\n  const {\n    name: connectedInstanceName,\n    db,\n    provider,\n  } = useSelector(connectedInstanceSelector)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const [isPageViewSent, setIsPageViewSent] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n  const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}`\n  setTitle(`${dbName} - Database Analysis`)\n\n  useEffect(() => {\n    dispatch(fetchDBAnalysisReportsHistory(instanceId))\n\n    if (viewTab !== AnalyticsViewTab.DatabaseAnalysis) {\n      dispatch(setAnalyticsViewTab(AnalyticsViewTab.DatabaseAnalysis))\n    }\n  }, [])\n\n  useEffect(() => {\n    if (!selectedAnalysis && reports?.length) {\n      dispatch(setSelectedAnalysisId(reports[0].id!))\n      dispatch(fetchDBAnalysisAction(instanceId, reports[0].id!))\n    }\n  }, [selectedAnalysis, reports])\n\n  const handleSelectAnalysis = (reportId: string) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.DATABASE_ANALYSIS_HISTORY_VIEWED,\n      eventData: {\n        databaseId: instanceId,\n        provider,\n      },\n    })\n    dispatch(setSelectedAnalysisId(reportId))\n    dispatch(fetchDBAnalysisAction(instanceId, reportId))\n  }\n\n  useEffect(() => {\n    if (connectedInstanceName && !isPageViewSent) {\n      sendPageView(instanceId)\n    }\n  }, [connectedInstanceName, isPageViewSent])\n\n  const sendPageView = (instanceId: string) => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.DATABASE_ANALYSIS,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  return (\n    <DatabaseAnalysisPageView\n      reports={reports}\n      selectedAnalysis={selectedAnalysis}\n      analysisLoading={analysisLoading}\n      data={data}\n      handleSelectAnalysis={handleSelectAnalysis}\n    />\n  )\n}\n\nexport default DatabaseAnalysisPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.stories.tsx",
    "content": "import React, { useEffect, useMemo } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { buildDatabaseAnalysisWithTopKeys } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory'\n\nimport { DatabaseAnalysisPageView } from './DatabaseAnalysisPageView'\nimport {\n  getDBAnalysisSuccess,\n  loadDBAnalysisReportsSuccess,\n  setSelectedAnalysisId,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { useDispatch } from 'react-redux'\nimport { fn } from 'storybook/test'\n\nconst meta: Meta<typeof DatabaseAnalysisPageView> = {\n  component: DatabaseAnalysisPageView,\n  args: {\n    reports: [],\n    selectedAnalysis: null,\n    analysisLoading: false,\n    data: null,\n    handleSelectAnalysis: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Loading: Story = {\n  args: {\n    reports: [],\n    selectedAnalysis: null,\n    analysisLoading: true,\n    data: null,\n  },\n}\n\nconst WithDataRender = () => {\n  const dispatch = useDispatch()\n  const { data, reports } = useMemo(\n    () => buildDatabaseAnalysisWithTopKeys(),\n    [],\n  )\n\n  useEffect(() => {\n    dispatch(getDBAnalysisSuccess(data))\n    dispatch(loadDBAnalysisReportsSuccess(reports))\n    dispatch(setSelectedAnalysisId(data.id))\n  }, [dispatch, data, reports])\n\n  return (\n    <DatabaseAnalysisPageView\n      reports={reports}\n      selectedAnalysis={data.id}\n      analysisLoading={false}\n      data={data}\n      handleSelectAnalysis={fn()}\n    />\n  )\n}\n\nexport const WithData: Story = {\n  render: () => <WithDataRender />,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/DatabaseAnalysisPageView.tsx",
    "content": "import React from 'react'\nimport {\n  type DatabaseAnalysis,\n  type ShortDatabaseAnalysis,\n} from 'apiSrc/modules/database-analysis/models'\nimport { Nullable } from 'uiSrc/utils'\nimport { AnalysisPageContainer } from './components/analysis-page-container'\nimport { Header } from './components'\nimport DatabaseAnalysisTabs from './components/data-nav-tabs'\n\ntype Props = {\n  reports: ShortDatabaseAnalysis[]\n  selectedAnalysis: Nullable<string>\n  analysisLoading: boolean\n  data: DatabaseAnalysis | null\n  handleSelectAnalysis: (value: string) => void\n}\nexport const DatabaseAnalysisPageView = ({\n  reports,\n  selectedAnalysis,\n  analysisLoading,\n  data,\n  handleSelectAnalysis,\n}: Props) => {\n  return (\n    <AnalysisPageContainer data-testid=\"database-analysis-page\">\n      <Header\n        items={reports}\n        selectedValue={selectedAnalysis}\n        onChangeSelectedAnalysis={handleSelectAnalysis}\n        progress={data?.progress}\n        analysisLoading={analysisLoading}\n      />\n      <DatabaseAnalysisTabs\n        loading={analysisLoading}\n        reports={reports}\n        data={data}\n      />\n    </AnalysisPageContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.spec.tsx",
    "content": "import React from 'react'\nimport { MOCK_ANALYSIS_REPORT_DATA } from 'uiSrc/mocks/data/analysis'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport {\n  dbAnalysisSelector,\n  dbAnalysisReportsSelector,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { SectionName } from 'uiSrc/pages/database-analysis'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { formatBytes, getGroupTypeDisplay } from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport {\n  fireEvent,\n  userEvent,\n  render,\n  screen,\n  within,\n} from 'uiSrc/utils/test-utils'\n\nimport AnalysisDataView from './AnalysisDataView'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockdbAnalysisSelector = jest.requireActual(\n  'uiSrc/slices/analytics/dbAnalysis',\n)\nconst mockdbAnalysisReportsSelector = jest.requireActual(\n  'uiSrc/slices/analytics/dbAnalysis',\n)\n\njest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'),\n  dbAnalysisSelector: jest.fn().mockReturnValue({\n    loading: false,\n    error: '',\n    data: null,\n    history: {\n      loading: false,\n      error: '',\n      data: [],\n      selectedAnalysis: null,\n    },\n  }),\n  dbAnalysisReportsSelector: jest.fn().mockReturnValue({\n    loading: false,\n    error: '',\n    data: [],\n    selectedAnalysis: null,\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'instanceId',\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\nconst mockReports = [\n  {\n    id: MOCK_ANALYSIS_REPORT_DATA.id,\n    createdAt: '2022-09-23T05:30:23.000Z',\n  },\n  {\n    id: 'id_2',\n    createdAt: '2022-09-23T05:15:19.000Z',\n  },\n]\n\nconst summaryContainerId = 'summary-per-data'\nconst analyticsTTLContainerId = 'analysis-ttl'\nconst topNameSpacesContainerId = 'top-namespaces'\nconst extrapolateResultsId = 'extrapolate-results'\n\ndescribe('AnalysisDataView', () => {\n  it('should render', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n    }))\n\n    expect(render(<AnalysisDataView />)).toBeTruthy()\n  })\n\n  it('should render only table when loading=\"true\"', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      loading: true,\n    }))\n\n    render(<AnalysisDataView />)\n\n    expect(\n      screen.queryByTestId('empty-analysis-no-reports'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('empty-analysis-no-keys'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render empty-data-message-no-keys when total=0 ', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: { totalKeys: { total: 0 } },\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n\n    render(<AnalysisDataView />)\n\n    expect(\n      screen.queryByTestId('empty-analysis-no-reports'),\n    ).not.toBeInTheDocument()\n    expect(screen.queryByTestId('empty-analysis-no-keys')).toBeInTheDocument()\n  })\n})\n\n/**\n * AnalysisDataView tests\n *\n * @group component\n */\ndescribe('AnalysisDataView', () => {\n  it('should render properly extrapolated data for summary per data', () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 40,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n\n    render(<AnalysisDataView />)\n\n    expect(screen.getByTestId('total-memory-value')).toHaveTextContent(\n      `~${formatBytes(mockedData.totalMemory.total * 2, 3)}`,\n    )\n    expect(screen.getByTestId('total-keys-value')).toHaveTextContent(\n      `~${numberWithSpaces(mockedData.totalKeys.total * 2)}`,\n    )\n\n    const arcItemMemory = mockedData.totalMemory.types[0]\n    const donutMemory = screen.getByTestId('donut-memory')\n    const arcMemory = (wrapperEl: HTMLElement) =>\n      within(wrapperEl).getByTestId(\n        `arc-${getGroupTypeDisplay(arcItemMemory.type)}-${arcItemMemory.total}`,\n      )\n\n    fireEvent.mouseEnter(arcMemory(donutMemory))\n    expect(\n      within(donutMemory).getByTestId('chart-value-tooltip'),\n    ).toHaveTextContent(`~${formatBytes(arcItemMemory.total * 2, 3)}`)\n    fireEvent.mouseLeave(donutMemory)\n\n    const arcItemkeys = mockedData.totalKeys.types[0]\n    const donutKeys = screen.getByTestId('donut-keys')\n    const arcKeys = (wrapperEl: HTMLElement) =>\n      within(wrapperEl).getByTestId(\n        `arc-${getGroupTypeDisplay(arcItemkeys.type)}-${arcItemkeys.total}`,\n      )\n    fireEvent.mouseEnter(arcKeys(donutKeys))\n    expect(\n      within(donutKeys).getByTestId('chart-value-tooltip'),\n    ).toHaveTextContent(`~${numberWithSpaces(arcItemkeys.total * 2)}`)\n  })\n\n  it('should render properly not extrapolated data for summary per data after switching off', async () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 40,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    render(<AnalysisDataView />)\n\n    await userEvent.click(\n      within(screen.getByTestId(summaryContainerId)).getByTestId(\n        extrapolateResultsId,\n      ),\n    )\n\n    expect(screen.getByTestId('total-memory-value')).toHaveTextContent(\n      `${formatBytes(mockedData.totalMemory.total, 3)}`,\n    )\n    expect(screen.getByTestId('total-keys-value')).toHaveTextContent(\n      `${numberWithSpaces(mockedData.totalKeys.total)}`,\n    )\n\n    const arcItemMemory = mockedData.totalMemory.types[0]\n    const donutMemory = screen.getByTestId('donut-memory')\n    const arcMemory = (wrapperEl: HTMLElement) =>\n      within(wrapperEl).getByTestId(\n        `arc-${getGroupTypeDisplay(arcItemMemory.type)}-${arcItemMemory.total}`,\n      )\n\n    fireEvent.mouseEnter(arcMemory(donutMemory))\n    expect(\n      within(donutMemory).getByTestId('chart-value-tooltip'),\n    ).toHaveTextContent(`${formatBytes(arcItemMemory.total, 3)}`)\n    fireEvent.mouseLeave(donutMemory)\n\n    const arcItemkeys = mockedData.totalKeys.types[0]\n    const donutKeys = screen.getByTestId('donut-keys')\n    const arcKeys = (wrapperEl: HTMLElement) =>\n      within(wrapperEl).getByTestId(\n        `arc-${getGroupTypeDisplay(arcItemkeys.type)}-${arcItemkeys.total}`,\n      )\n    fireEvent.mouseEnter(arcKeys(donutKeys))\n    expect(\n      within(donutKeys).getByTestId('chart-value-tooltip'),\n    ).toHaveTextContent(`${numberWithSpaces(arcItemkeys.total)}`)\n  })\n\n  it('should render properly extrapolated data for ttl chart', () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 40,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    render(<AnalysisDataView />)\n\n    const expirationGroup = mockedData.expirationGroups[1]\n\n    fireEvent.mouseEnter(\n      screen.getByTestId(\n        `bar-${expirationGroup.threshold}-${expirationGroup.total * 2}`,\n      ),\n    )\n    expect(screen.getByTestId('bar-tooltip')).toHaveTextContent(\n      `~${formatBytes(expirationGroup.total * 2, 3)}`,\n    )\n  })\n\n  it('should render properly not extrapolated data for ttl chart after switching off', async () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 40,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    render(<AnalysisDataView />)\n    await userEvent.click(\n      within(screen.getByTestId(analyticsTTLContainerId)).getByTestId(\n        extrapolateResultsId,\n      ),\n    )\n\n    const expirationGroup = mockedData.expirationGroups[1]\n\n    fireEvent.mouseEnter(\n      screen.getByTestId(\n        `bar-${expirationGroup.threshold}-${expirationGroup.total}`,\n      ),\n    )\n    expect(screen.getByTestId('bar-tooltip')).toHaveTextContent(\n      `${formatBytes(expirationGroup.total, 3)}`,\n    )\n  })\n\n  it('should render properly extrapolated data for top namespaces table', () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 40,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    render(<AnalysisDataView />)\n\n    const nspTopKeyItem = mockedData.topKeysNsp[0]\n    expect(\n      screen.getByTestId(`nsp-usedMemory-value=${nspTopKeyItem.memory}`),\n    ).toHaveTextContent(`~${formatBytes(nspTopKeyItem.memory * 2, 3, true)[0]}`)\n\n    expect(\n      screen.getAllByTestId(`keys-value-${nspTopKeyItem.keys}`)[0],\n    ).toHaveTextContent(`~${numberWithSpaces(nspTopKeyItem.keys * 2)}`)\n  })\n\n  it('should render properly not extrapolated data for top namespaces table after switching off', async () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 40,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    render(<AnalysisDataView />)\n    await userEvent.click(\n      within(screen.getByTestId(topNameSpacesContainerId)).getByTestId(\n        extrapolateResultsId,\n      ),\n    )\n\n    const nspTopKeyItem = mockedData.topKeysNsp[0]\n    expect(\n      screen.getByTestId(`nsp-usedMemory-value=${nspTopKeyItem.memory}`),\n    ).toHaveTextContent(`${formatBytes(nspTopKeyItem.memory, 3, true)[0]}`)\n\n    expect(\n      screen.getAllByTestId(`keys-value-${nspTopKeyItem.keys}`)[0],\n    ).toHaveTextContent(`${numberWithSpaces(nspTopKeyItem.keys)}`)\n  })\n\n  it('should not render extrapolation switcher and extrapolated data for full scanned db', () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 10000,\n        processed: 80,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    render(<AnalysisDataView />)\n\n    expect(screen.queryByTestId(extrapolateResultsId)).not.toBeInTheDocument()\n\n    expect(screen.getByTestId('total-memory-value')).toHaveTextContent(\n      `${formatBytes(mockedData.totalMemory.total, 3)}`,\n    )\n    expect(screen.getByTestId('total-keys-value')).toHaveTextContent(\n      `${numberWithSpaces(mockedData.totalKeys.total)}`,\n    )\n\n    const expirationGroup = mockedData.expirationGroups[1]\n\n    fireEvent.mouseEnter(\n      screen.getByTestId(\n        `bar-${expirationGroup.threshold}-${expirationGroup.total}`,\n      ),\n    )\n    expect(screen.getByTestId('bar-tooltip')).toHaveTextContent(\n      `${formatBytes(expirationGroup.total, 3)}`,\n    )\n\n    const nspTopKeyItem = mockedData.topKeysNsp[0]\n    expect(\n      screen.getByTestId(`nsp-usedMemory-value=${nspTopKeyItem.memory}`),\n    ).toHaveTextContent(`${formatBytes(nspTopKeyItem.memory, 3, true)[0]}`)\n\n    expect(\n      screen.getAllByTestId(`keys-value-${nspTopKeyItem.keys}`)[0],\n    ).toHaveTextContent(`${numberWithSpaces(nspTopKeyItem.keys)}`)\n  })\n\n  it('should call proper telemetry events after click extrapolation', () => {\n    const mockedData = {\n      ...MOCK_ANALYSIS_REPORT_DATA,\n      progress: {\n        total: 80,\n        scanned: 10000,\n        processed: 40,\n      },\n    }\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: mockedData,\n    }))\n    ;(dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisReportsSelector,\n      data: mockReports,\n    }))\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<AnalysisDataView />)\n\n    const clickAndCheckTelemetry = async (\n      el: HTMLInputElement,\n      section: SectionName,\n    ) => {\n      await userEvent.click(el)\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.DATABASE_ANALYSIS_EXTRAPOLATION_CHANGED,\n        eventData: {\n          databaseId: INSTANCE_ID_MOCK,\n          from: !el.checked,\n          to: el.checked,\n          section,\n          provider: 'REDIS_CLOUD',\n        },\n      })\n      sendEventTelemetry.mockRestore()\n    }\n\n    ;[\n      { id: summaryContainerId, section: SectionName.SUMMARY_PER_DATA },\n      {\n        id: analyticsTTLContainerId,\n        section: SectionName.MEMORY_LIKELY_TO_BE_FREED,\n      },\n      { id: topNameSpacesContainerId, section: SectionName.TOP_NAMESPACES },\n    ].forEach(({ id, section }) => {\n      const extrapolateSwitch = within(screen.getByTestId(id)).getByTestId(\n        extrapolateResultsId,\n      )\n      clickAndCheckTelemetry(extrapolateSwitch as HTMLInputElement, section)\n      clickAndCheckTelemetry(extrapolateSwitch as HTMLInputElement, section)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.styles.ts",
    "content": "import styled from 'styled-components'\nimport { scrollbarStyles } from 'uiSrc/styles/mixins'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const ContentWrapper = styled.div`\n  ${scrollbarStyles()}\n  max-height: 100%;\n  padding: ${({ theme }: { theme: Theme }) =>\n    `${theme.core.space.space300} ${theme.core.space.space500}`};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/AnalysisDataView.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  dbAnalysisSelector,\n  dbAnalysisReportsSelector,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  DEFAULT_EXTRAPOLATION,\n  EmptyMessage,\n  SectionName,\n} from 'uiSrc/pages/database-analysis/constants'\nimport {\n  TopKeys,\n  EmptyAnalysisMessage,\n  TopNamespace,\n  SummaryPerData,\n  ExpirationGroupsView,\n} from 'uiSrc/pages/database-analysis/components'\n\nimport { ContentWrapper } from './AnalysisDataView.styles'\n\nconst AnalysisDataView = () => {\n  const { id: instanceId, provider } = useSelector(connectedInstanceSelector)\n  const { loading, data } = useSelector(dbAnalysisSelector)\n  const { data: reports } = useSelector(dbAnalysisReportsSelector)\n\n  const [extrapolation, setExtrapolation] = useState(DEFAULT_EXTRAPOLATION)\n\n  useEffect(() => {\n    if (data?.progress?.processed) {\n      setExtrapolation(data.progress.total / data.progress.processed)\n    }\n  }, [data])\n\n  const onSwitchExtrapolation = (value: boolean, section: SectionName) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.DATABASE_ANALYSIS_EXTRAPOLATION_CHANGED,\n      eventData: {\n        databaseId: instanceId,\n        from: !value,\n        to: value,\n        section,\n        provider,\n      },\n    })\n  }\n\n  if (!loading && !!reports?.length && data?.totalKeys?.total === 0) {\n    return <EmptyAnalysisMessage name={EmptyMessage.Keys} />\n  }\n\n  return (\n    <ContentWrapper>\n      <SummaryPerData\n        data={data}\n        loading={loading}\n        extrapolation={extrapolation}\n        onSwitchExtrapolation={onSwitchExtrapolation}\n      />\n      <ExpirationGroupsView\n        data={data}\n        loading={loading}\n        extrapolation={extrapolation}\n        onSwitchExtrapolation={onSwitchExtrapolation}\n      />\n      <TopNamespace\n        data={data}\n        loading={loading}\n        extrapolation={extrapolation}\n        onSwitchExtrapolation={onSwitchExtrapolation}\n      />\n      <TopKeys data={data} loading={loading} />\n    </ContentWrapper>\n  )\n}\n\nexport default AnalysisDataView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/index.ts",
    "content": "import AnalysisDataView from './AnalysisDataView'\n\nexport default AnalysisDataView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-data-view/styles.module.scss",
    "content": "$breakpoint-table: 1680px;\n\n.content {\n  @include eui.scrollBar;\n  overflow-y: auto;\n  overflow-x: hidden;\n  max-height: 100%;\n}\n\n.grid {\n  display: grid;\n  grid-template-columns: 61% 39%;\n\n  > div:nth-child(2n-1) {\n    margin-right: 24px;\n  }\n\n  @media screen and (max-width: $breakpoint-table) {\n    > div:nth-child(2n-1) {\n      margin-right: 0;\n    }\n    grid-template-columns: 100%;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-page-container/AnalysisPageContainer.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const StyledAnalysisPageContainer = styled(Col)`\n  overflow: auto;\n  padding-inline: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-page-container/AnalysisPageContainer.tsx",
    "content": "import React from 'react'\nimport { StyledAnalysisPageContainer } from './AnalysisPageContainer.styles'\nimport { AnalysisPageContainerProps } from './AnalysisPageContainer.types'\n\nexport const AnalysisPageContainer = ({\n  children,\n  ...rest\n}: AnalysisPageContainerProps) => (\n  <StyledAnalysisPageContainer {...rest}>\n    {children}\n  </StyledAnalysisPageContainer>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-page-container/AnalysisPageContainer.types.ts",
    "content": "import { ComponentPropsWithoutRef } from 'react'\nimport { StyledAnalysisPageContainer } from './AnalysisPageContainer.styles'\n\nexport type AnalysisPageContainerProps = ComponentPropsWithoutRef<\n  typeof StyledAnalysisPageContainer\n>\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-page-container/index.ts",
    "content": "export { AnalysisPageContainer } from './AnalysisPageContainer'\nexport type { AnalysisPageContainerProps } from './AnalysisPageContainer.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport ExpirationGroupsView from './ExpirationGroupsView'\n\ndescribe('ExpirationGroupsView', () => {\n  it('should be rendered', async () => {\n    expect(\n      render(\n        <ExpirationGroupsView data={null} extrapolation={1} loading={false} />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render spinner if loading=true and data=null', async () => {\n    const { queryByTestId } = render(\n      <ExpirationGroupsView data={null} extrapolation={1} loading />,\n    )\n\n    expect(queryByTestId('summary-per-ttl-loading')).toBeInTheDocument()\n    expect(queryByTestId('analysis-ttl')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.stories.tsx",
    "content": "import React, { useEffect, useMemo } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\nimport { useDispatch } from 'react-redux'\n\nimport ExpirationGroupsView from './ExpirationGroupsView'\nimport { setShowNoExpiryGroup } from 'uiSrc/slices/analytics/dbAnalysis'\n\nconst meta: Meta<typeof ExpirationGroupsView> = {\n  component: ExpirationGroupsView,\n  decorators: [\n    (Story) => (\n      <div style={{ height: '600px', padding: '20px' }}>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nconst sampleData = {\n  totalMemory: { total: 50000 },\n  totalKeys: { total: 5000 },\n  expirationGroups: [\n    { label: 'No Expiry', total: 15000, threshold: 0 },\n    { label: '<1 hr', total: 2000, threshold: 3600 },\n    { label: '1-4 Hrs', total: 3000, threshold: 14400 },\n    { label: '4-12 Hrs', total: 2500, threshold: 43200 },\n    { label: '12-24 Hrs', total: 2000, threshold: 86400 },\n    { label: '1-7 Days', total: 1500, threshold: 604800 },\n    { label: '>7 Days', total: 1000, threshold: 2592000 },\n    { label: '>1 Month', total: 500, threshold: 9007199254740991 },\n  ],\n}\n\nconst DefaultRender = () => {\n  const dispatch = useDispatch()\n\n  const data = useMemo(() => sampleData, [])\n\n  useEffect(() => {\n    dispatch(setShowNoExpiryGroup(true))\n  }, [dispatch])\n\n  return (\n    <ExpirationGroupsView\n      data={data as any}\n      loading={false}\n      extrapolation={1}\n      onSwitchExtrapolation={fn()}\n    />\n  )\n}\n\nexport const Default: Story = {\n  render: () => <DefaultRender />,\n}\n\nexport const Loading: Story = {\n  args: {\n    data: null,\n    loading: true,\n    extrapolation: 1,\n    onSwitchExtrapolation: fn(),\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport {\n  Section,\n  sectionContent,\n  SectionTitleWrapper,\n} from 'uiSrc/pages/database-analysis/components/styles'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\n\nexport const Container = styled(Section)`\n  position: relative;\n  padding-right: 0;\n\n  @media screen and (max-width: 920px) {\n    ${SectionTitleWrapper} {\n      flex-direction: column;\n      align-items: flex-start !important;\n    }\n  }\n`\n\nexport const TitleWrapper = styled.div`\n  flex: 1;\n  display: flex;\n  align-items: center;\n\n  @media screen and (max-width: 920px) {\n    margin-bottom: ${({ theme }: { theme: Theme }) =>\n      theme.core.space.space150};\n  }\n`\n\nexport const Content = styled.div`\n  width: 100%;\n  height: 300px;\n  ${sectionContent}\n`\n\nexport const LoadingWrapper = styled(Content)`\n  margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space400};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral200};\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n`\n\nexport const Chart = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  height: calc(\n    100% - ${({ theme }: { theme: Theme }) => theme.core.space.space400}\n  );\n  clear: both;\n  width: 100%;\n`\n\nexport const Switch = styled(SwitchInput)`\n  float: right;\n  padding-right: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n\n  @media screen and (max-width: 920px) {\n    margin-left: ${({ theme }: { theme: Theme }) =>\n      `-${theme.core.space.space100}`};\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/ExpirationGroupsView.tsx",
    "content": "import { useDispatch, useSelector } from 'react-redux'\nimport React, { useEffect, useState } from 'react'\nimport AutoSizer from 'react-virtualized-auto-sizer'\n\nimport {\n  DEFAULT_EXTRAPOLATION,\n  SectionName,\n} from 'uiSrc/pages/database-analysis/constants'\nimport {\n  extrapolate,\n  formatBytes,\n  formatExtrapolation,\n  Nullable,\n} from 'uiSrc/utils'\nimport { BarChart } from 'uiSrc/components/charts'\nimport {\n  BarChartData,\n  BarChartDataType,\n  DEFAULT_BAR_WIDTH,\n  DEFAULT_MULTIPLIER_GRID,\n  DEFAULT_Y_TICKS,\n} from 'uiSrc/components/charts/bar-chart'\nimport {\n  dbAnalysisReportsSelector,\n  setShowNoExpiryGroup,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\n\nimport {\n  SectionTitleWrapper,\n  SwitchExtrapolateResults,\n} from 'uiSrc/pages/database-analysis/components/styles'\nimport {\n  Chart,\n  Container,\n  Content,\n  LoadingWrapper,\n  Switch,\n  TitleWrapper,\n} from './ExpirationGroupsView.styles'\n\nexport interface Props {\n  data: Nullable<DatabaseAnalysis>\n  loading: boolean\n  extrapolation: number\n  onSwitchExtrapolation?: (value: boolean, section: SectionName) => void\n}\n\nconst ExpirationGroupsView = (props: Props) => {\n  const { data, loading, extrapolation, onSwitchExtrapolation } = props\n  const { totalMemory, totalKeys } = data || {}\n\n  const { showNoExpiryGroup } = useSelector(dbAnalysisReportsSelector)\n  const [expirationGroups, setExpirationGroups] = useState<BarChartData[]>([])\n  const [isExtrapolated, setIsExtrapolated] = useState<boolean>(true)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsExtrapolated(extrapolation !== DEFAULT_EXTRAPOLATION)\n  }, [extrapolation])\n\n  useEffect(() => {\n    if (!data?.expirationGroups || data?.expirationGroups?.length === 0) {\n      return\n    }\n\n    const newExpirationGroups = [...(data?.expirationGroups || [])]\n\n    // move \"No expire\" column to the end if should be shown\n    const noExpireGroup = newExpirationGroups.shift()\n    if (showNoExpiryGroup && noExpireGroup && newExpirationGroups.length > 0) {\n      newExpirationGroups.push(noExpireGroup)\n    }\n\n    const extrapolationOptions = {\n      apply: isExtrapolated,\n      extrapolation,\n      showPrefix: false,\n    }\n\n    setExpirationGroups(\n      newExpirationGroups.map(({ total, threshold, label, ...group }) => ({\n        ...group,\n        y: extrapolate(total, extrapolationOptions) as number,\n        x: threshold,\n        xlabel: label,\n        ylabel: '',\n      })),\n    )\n  }, [data?.expirationGroups, showNoExpiryGroup, isExtrapolated, extrapolation])\n\n  if (loading) {\n    return <LoadingWrapper data-testid=\"summary-per-ttl-loading\" />\n  }\n\n  const onSwitchChange = (value: boolean) => {\n    dispatch(setShowNoExpiryGroup(value))\n  }\n\n  if (\n    !data?.expirationGroups?.length ||\n    !totalMemory?.total ||\n    !totalKeys?.total\n  ) {\n    return null\n  }\n\n  const multiplierGrid = DEFAULT_MULTIPLIER_GRID\n  const yCountTicks = DEFAULT_Y_TICKS\n\n  return (\n    <Container data-testid=\"analysis-ttl\">\n      <SectionTitleWrapper>\n        <TitleWrapper>\n          <Title size=\"M\" className=\"section-title\">\n            MEMORY LIKELY TO BE FREED OVER TIME\n          </Title>\n          {extrapolation !== DEFAULT_EXTRAPOLATION && (\n            <SwitchExtrapolateResults\n              title=\"Extrapolate results\"\n              checked={isExtrapolated}\n              onCheckedChange={(checked) => {\n                setIsExtrapolated(checked)\n                onSwitchExtrapolation?.(\n                  checked,\n                  SectionName.MEMORY_LIKELY_TO_BE_FREED,\n                )\n              }}\n              data-testid=\"extrapolate-results\"\n            />\n          )}\n        </TitleWrapper>\n        <Switch\n          title={'Show \"No Expiry\"'}\n          checked={showNoExpiryGroup}\n          onCheckedChange={onSwitchChange}\n          data-testid=\"show-no-expiry-switch\"\n        />\n      </SectionTitleWrapper>\n      <Content>\n        <Chart>\n          <AutoSizer>\n            {({ width = 0, height = 0 }) => (\n              <BarChart\n                name=\"expiration-groups\"\n                width={width}\n                height={height}\n                dataType={BarChartDataType.Bytes}\n                divideLastColumn={showNoExpiryGroup}\n                multiplierGrid={multiplierGrid}\n                data={expirationGroups}\n                yCountTicks={yCountTicks}\n                barWidth={\n                  width > 1000 ? 70 : width < 800 ? 30 : DEFAULT_BAR_WIDTH\n                }\n                tooltipValidation={(val) =>\n                  `${formatExtrapolation(formatBytes(val, 3) as string, isExtrapolated)}`\n                }\n                leftAxiosValidation={(val, i) =>\n                  i % 2 ? '' : formatBytes(val, 1)\n                }\n                bottomAxiosValidation={(_val, i) =>\n                  expirationGroups[i / multiplierGrid]?.xlabel\n                }\n              />\n            )}\n          </AutoSizer>\n        </Chart>\n      </Content>\n    </Container>\n  )\n}\n\nexport default ExpirationGroupsView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/index.ts",
    "content": "import ExpirationGroupsView from './ExpirationGroupsView'\n\nexport default ExpirationGroupsView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analysis-ttl-view/styles.module.scss",
    "content": ".container {\n  position: relative;\n  padding-right: 0;\n\n  @media screen and (max-width: 920px) {\n    :global(.section-title-wrapper) {\n      flex-direction: column;\n      align-items: flex-start !important;\n\n      .titleWrapper {\n        margin-bottom: 12px;\n      }\n\n      .switch {\n        margin-left: -10px;\n      }\n    }\n  }\n\n  .titleWrapper {\n    flex: 1;\n    display: flex;\n    align-items: center;\n  }\n}\n\n.content {\n  width: 100%;\n  height: 300px;\n}\n\n.loadingWrapper {\n  margin-top: 40px;\n  background-color: var(--euiColorLightestShade);\n  border-radius: 4px;\n}\n\n.chart {\n  height: calc(100% - 40px);\n  clear: both;\n  width: 100%;\n}\n\n.switch {\n  float: right;\n  padding-right: 20px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analytics-page-header/AnalyticsPageHeader.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const HeaderContainer = styled.div`\n  width: 100%;\n  border-bottom: ${({ theme }) => {\n    const { tabsLine } = theme.components.tabs.variants.default\n    return `${tabsLine.size} solid ${tabsLine.color}`\n  }};\n`\n\nexport const HeaderContent = styled(Row).attrs({\n  align: 'center',\n  justify: 'between',\n})`\n  min-height: 36px;\n`\n\nexport const TabsWrapper = styled.div`\n  margin-bottom: -14px; /* Move it so it overlaps with the border of the tabs container */\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analytics-page-header/AnalyticsPageHeader.tsx",
    "content": "import React from 'react'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport AnalyticsTabs from 'uiSrc/components/analytics-tabs'\nimport { AnalyticsPageHeaderProps } from './AnalyticsPageHeader.types'\nimport {\n  HeaderContainer,\n  HeaderContent,\n  TabsWrapper,\n} from './AnalyticsPageHeader.styles'\n\nexport const AnalyticsPageHeader = ({ actions }: AnalyticsPageHeaderProps) => (\n  <HeaderContainer>\n    <HeaderContent>\n      <FlexItem>\n        <TabsWrapper>\n          <AnalyticsTabs />\n        </TabsWrapper>\n      </FlexItem>\n      {actions && <FlexItem>{actions}</FlexItem>}\n    </HeaderContent>\n  </HeaderContainer>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analytics-page-header/AnalyticsPageHeader.types.ts",
    "content": "import { ReactNode } from 'react'\n\nexport interface AnalyticsPageHeaderProps {\n  actions?: ReactNode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/analytics-page-header/index.ts",
    "content": "export { AnalyticsPageHeader } from './AnalyticsPageHeader'\nexport type { AnalyticsPageHeaderProps } from './AnalyticsPageHeader.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/base/TableTextBtn.tsx",
    "content": "import styled, { css } from 'styled-components'\nimport React from 'react'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nconst expandedStyle = css<{ theme: Theme }>`\n  padding: 0 20px 0 12px;\n`\n\nexport const TableTextBtn = styled(EmptyButton).attrs({\n  variant: 'primary-inline',\n})<\n  React.ComponentProps<typeof EmptyButton> & {\n    $expanded?: boolean\n    theme: Theme\n  }\n>`\n  max-width: calc(100% - 20px);\n  width: auto;\n  color: ${({ theme }) => theme.semantic.color.text.primary600};\n  text-decoration: underline;\n\n  &:hover,\n  &:focus {\n    text-decoration: none;\n  }\n\n  ${({ $expanded }) => $expanded && expandedStyle}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/base/TextBtn.tsx",
    "content": "import styled from 'styled-components'\nimport React from 'react'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\n/**\n * Text button component\n *\n * This is how we can implement custom styles\n */\nexport const TextBtn = styled(EmptyButton)<\n  React.ComponentProps<typeof EmptyButton> & {\n    $active?: boolean\n  }\n>`\n  border: none;\n  min-width: auto;\n  min-height: auto;\n  border-radius: 4px;\n  opacity: ${({ $active }) => ($active ? '1' : '')};\n  background: ${({ $active }) =>\n    $active ? 'var(--browserComponentActive)' : 'transparent'};\n  color: ${({ $active }) =>\n    $active ? 'var(--wbActiveIconColor)' : 'var(--wbHoverIconColor)'};\n  margin-left: 18px;\n  box-shadow: none;\n  font:\n    normal normal normal 13px/17px Graphik,\n    sans-serif;\n\n  &:hover {\n    color: var(--wbHoverIconColor);\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport { MOCK_ANALYSIS_REPORT_DATA } from 'uiSrc/mocks/data/analysis'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport {\n  render,\n  screen,\n  mockedStore,\n  cleanup,\n  fireEvent,\n} from 'uiSrc/utils/test-utils'\nimport { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport { setDatabaseAnalysisViewTab } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport { ShortDatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\nimport DatabaseAnalysisTabs, { Props } from './DatabaseAnalysisTabs'\n\nconst mockRecommendationsSelector = jest.requireActual(\n  'uiSrc/slices/recommendations/recommendations',\n)\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/recommendations/recommendations', () => ({\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  recommendationsSelector: jest.fn().mockReturnValue({\n    data: {\n      recommendations: [],\n      totalUnread: 0,\n    },\n  }),\n}))\n\nconst mockedProps = mock<Props>()\n\nconst mockReports: ShortDatabaseAnalysis[] = [\n  {\n    id: MOCK_ANALYSIS_REPORT_DATA.id,\n    createdAt: '2022-09-23T05:30:23.000Z' as any,\n  },\n]\n\nconst recommendationsContent = MOCK_RECOMMENDATIONS\n\nlet store: typeof mockedStore\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: 'instanceId',\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n    ...mockRecommendationsSelector,\n    content: recommendationsContent,\n  }))\n})\n\ndescribe('DatabaseAnalysisTabs', () => {\n  it('should render', () => {\n    expect(\n      render(<DatabaseAnalysisTabs {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should call setDatabaseAnalysisViewTab', () => {\n    render(\n      <DatabaseAnalysisTabs {...instance(mockedProps)} reports={mockReports} />,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Tips'))\n\n    const expectedActions = [\n      setDatabaseAnalysisViewTab(DatabaseAnalysisViewTab.Recommendations),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render encrypt message', () => {\n    const mockData: any = {\n      totalKeys: null,\n    }\n    render(\n      <DatabaseAnalysisTabs\n        {...instance(mockedProps)}\n        reports={mockReports}\n        data={mockData}\n      />,\n    )\n\n    expect(screen.queryByTestId('empty-encrypt-wrapper')).toBeTruthy()\n  })\n\n  describe('recommendations count', () => {\n    it('should render \"Tips (3)\" in the tab name', () => {\n      const mockData: any = {\n        recommendations: [\n          { name: 'luaScript' },\n          { name: 'luaScript' },\n          { name: 'luaScript' },\n        ],\n      }\n\n      render(\n        <DatabaseAnalysisTabs\n          {...instance(mockedProps)}\n          reports={mockReports}\n          data={mockData}\n        />,\n      )\n\n      expect(screen.getByText('Tips (3)')).toBeVisible()\n    })\n\n    it('should render \"Tips (1)\" in the tab name', () => {\n      const mockData: any = {\n        recommendations: [{ name: 'luaScript' }],\n      }\n      render(\n        <DatabaseAnalysisTabs\n          {...instance(mockedProps)}\n          reports={mockReports}\n          data={mockData}\n        />,\n      )\n\n      expect(screen.getByText('Tips (1)')).toBeVisible()\n    })\n\n    it('should render \"Tips\" in the tab name', () => {\n      const mockData: any = {\n        recommendations: [],\n      }\n      render(\n        <DatabaseAnalysisTabs\n          {...instance(mockedProps)}\n          reports={mockReports}\n          data={mockData}\n        />,\n      )\n\n      expect(screen.getByText(/^Tips$/)).toBeVisible()\n    })\n  })\n\n  describe('Telemetry', () => {\n    it.skip('should call DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED telemetry event with 0 count', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      const mockData: any = {\n        recommendations: [],\n      }\n      render(\n        <DatabaseAnalysisTabs\n          {...instance(mockedProps)}\n          reports={mockReports}\n          data={mockData}\n        />,\n      )\n\n      fireEvent.mouseDown(screen.getByText('Data Summary'))\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED,\n        eventData: {\n          databaseId: INSTANCE_ID_MOCK,\n          provider: 'REDIS_CLOUD',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n\n    it('should call DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED telemetry event with 0 count', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      const mockData: any = {\n        recommendations: [],\n      }\n      render(\n        <DatabaseAnalysisTabs\n          {...instance(mockedProps)}\n          reports={mockReports}\n          data={mockData}\n        />,\n      )\n\n      fireEvent.mouseDown(screen.getByText('Tips'))\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.DATABASE_ANALYSIS_TIPS_CLICKED,\n        eventData: {\n          databaseId: INSTANCE_ID_MOCK,\n          tipsCount: 0,\n          list: [],\n          provider: 'REDIS_CLOUD',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n\n    it('should call DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED telemetry event with 2 count', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      const mockData: any = {\n        recommendations: [{ name: 'luaScript' }, { name: 'bigHashes' }],\n      }\n      render(\n        <DatabaseAnalysisTabs\n          {...instance(mockedProps)}\n          reports={mockReports}\n          data={mockData}\n        />,\n      )\n\n      fireEvent.mouseDown(screen.getByText('Tips (2)'))\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.DATABASE_ANALYSIS_TIPS_CLICKED,\n        eventData: {\n          databaseId: INSTANCE_ID_MOCK,\n          tipsCount: 2,\n          list: ['luaScript', 'shardHashes'],\n          provider: 'REDIS_CLOUD',\n        },\n      })\n      ;(sendEventTelemetry as jest.Mock).mockRestore()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport Tabs from 'uiSrc/components/base/layout/tabs'\n\nexport const EmptyMessageContainer = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  height: calc(100% - 96px);\n`\n\nexport const StyledTabs = styled(Tabs)`\n  padding-top: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { isNull } from 'lodash'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { EmptyMessage } from 'uiSrc/pages/database-analysis/constants'\nimport { EmptyAnalysisMessage } from 'uiSrc/pages/database-analysis/components'\nimport {\n  setDatabaseAnalysisViewTab,\n  dbAnalysisViewTabSelector,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics'\nimport { Nullable } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { Text } from 'uiSrc/components/base/text'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport {\n  ShortDatabaseAnalysis,\n  DatabaseAnalysis,\n} from 'apiSrc/modules/database-analysis/models'\nimport Recommendations from '../recommendations-view'\nimport AnalysisDataView from '../analysis-data-view'\n\nimport {\n  EmptyMessageContainer,\n  StyledTabs,\n} from './DatabaseAnalysisTabs.styles'\n\nexport interface Props {\n  loading: boolean\n  reports: ShortDatabaseAnalysis[]\n  data: Nullable<DatabaseAnalysis>\n}\n\nconst DatabaseAnalysisTabs = (props: Props) => {\n  const { loading, reports, data } = props\n\n  const viewTab = useSelector(dbAnalysisViewTabSelector)\n  const { id: instanceId = '', provider } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { content: recommendationsContent } = useSelector(\n    recommendationsSelector,\n  )\n\n  const dispatch = useDispatch()\n\n  const tabs: TabInfo[] = useMemo(\n    () => [\n      {\n        label: <Text>Data Summary</Text>,\n        value: DatabaseAnalysisViewTab.DataSummary,\n        content: <AnalysisDataView />,\n      },\n      {\n        label: renderOnboardingTourWithChild(\n          <Text>\n            Tips{' '}\n            {data?.recommendations?.length\n              ? `(${data.recommendations.length})`\n              : ''}\n          </Text>,\n          {\n            options: { ...ONBOARDING_FEATURES.ANALYTICS_RECOMMENDATIONS },\n            anchorPosition: 'downLeft',\n          },\n          viewTab === DatabaseAnalysisViewTab.Recommendations,\n          'analytics-recommendations-tab',\n        ),\n        value: DatabaseAnalysisViewTab.Recommendations,\n        content: <Recommendations />,\n      },\n    ],\n    [viewTab, data?.recommendations],\n  )\n\n  const handleTabChange = (id: string) => {\n    if (viewTab === id) return\n\n    if (id === DatabaseAnalysisViewTab.DataSummary) {\n      sendEventTelemetry({\n        event: TelemetryEvent.DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          provider,\n        },\n      })\n    }\n    if (id === DatabaseAnalysisViewTab.Recommendations) {\n      sendEventTelemetry({\n        event: TelemetryEvent.DATABASE_ANALYSIS_TIPS_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          tipsCount: data?.recommendations?.length,\n          list: data?.recommendations?.map(\n            ({ name }) => recommendationsContent[name]?.telemetryEvent || name,\n          ),\n          provider,\n        },\n      })\n    }\n    dispatch(setDatabaseAnalysisViewTab(id as DatabaseAnalysisViewTab))\n  }\n\n  if (!loading && !reports?.length) {\n    return (\n      <EmptyMessageContainer data-testid=\"empty-reports-wrapper\">\n        <EmptyAnalysisMessage name={EmptyMessage.Reports} />\n      </EmptyMessageContainer>\n    )\n  }\n  if (!loading && !!reports?.length && isNull(data?.totalKeys)) {\n    return (\n      <EmptyMessageContainer data-testid=\"empty-encrypt-wrapper\">\n        <EmptyAnalysisMessage name={EmptyMessage.Encrypt} />\n      </EmptyMessageContainer>\n    )\n  }\n\n  return (\n    <StyledTabs\n      tabs={tabs}\n      value={viewTab}\n      onChange={handleTabChange}\n      data-testid=\"database-analysis-tabs\"\n    />\n  )\n}\n\nexport default DatabaseAnalysisTabs\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/data-nav-tabs/index.ts",
    "content": "import DatabaseAnalysisTabs from './DatabaseAnalysisTabs'\n\nexport default DatabaseAnalysisTabs\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/empty-analysis-message/EmptyAnalysisMessage.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { EmptyMessage } from 'uiSrc/pages/database-analysis/constants'\n\nimport EmptyAnalysisMessage from './EmptyAnalysisMessage'\n\ndescribe('EmptyAnalysisMessage', () => {\n  it('should render', () => {\n    expect(\n      render(<EmptyAnalysisMessage name={EmptyMessage.Keys} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/empty-analysis-message/EmptyAnalysisMessage.tsx",
    "content": "import React from 'react'\nimport { useParams } from 'react-router-dom'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { Pages } from 'uiSrc/constants'\nimport { EmptyMessage, Content } from 'uiSrc/pages/database-analysis/constants'\nimport { getRouterLinkProps } from 'uiSrc/services'\n\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport styles from './styles.module.scss'\n\ninterface Props {\n  name: EmptyMessage\n}\n\nconst emptyMessageContent: { [key in EmptyMessage]: Content } = {\n  [EmptyMessage.Reports]: {\n    title: 'No Reports found',\n    text: () => 'Click \"Analyze\" to generate the first report.',\n  },\n  [EmptyMessage.Keys]: {\n    title: 'No keys to display',\n    text: (path) => (\n      <>\n        <Link\n          {...getRouterLinkProps(path)}\n          className={styles.summary}\n          data-test-subj=\"workbench-page-btn\"\n        >\n          Use Workbench Guides and Tutorials\n        </Link>\n        {' to quickly load the data.'}\n      </>\n    ),\n  },\n  [EmptyMessage.Encrypt]: {\n    title: 'Encrypted data',\n    text: () =>\n      'Unable to decrypt. Check the system keychain or re-run the report generation.',\n  },\n}\n\nconst EmptyAnalysisMessage = (props: Props) => {\n  const { name } = props\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const { text, title } = emptyMessageContent[name]\n\n  return (\n    <div className={styles.container} data-testid={`empty-analysis-no-${name}`}>\n      <div className={styles.content}>\n        <Text className={styles.title}>{title}</Text>\n        <Text className={styles.summary}>\n          {text(Pages.workbench(instanceId))}\n        </Text>\n      </div>\n    </div>\n  )\n}\n\nexport default EmptyAnalysisMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/empty-analysis-message/index.ts",
    "content": "import EmptyAnalysisMessage from './EmptyAnalysisMessage'\n\nexport default EmptyAnalysisMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/empty-analysis-message/styles.module.scss",
    "content": ".container {\n  @include eui.scrollBar;\n  display: flex;\n  justify-content: center;\n  overflow: auto;\n  width: 100%;\n  height: 100%;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  margin: auto;\n\n  .title {\n    font:\n      normal normal 500 18px/24px Graphik,\n      sans-serif !important;\n    padding-bottom: 12px;\n  }\n\n  .summary {\n    font-size: 13px !important;\n    line-height: 18px !important;\n    letter-spacing: -0.13px !important;\n    color: var(--euiColorMediumShade) !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/header/Header.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { useSelector } from 'react-redux'\nimport { instance, mock } from 'ts-mockito'\nimport { getDBAnalysis } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { RootState } from 'uiSrc/slices/store'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\n\nimport {\n  act,\n  cleanup,\n  mockedStore,\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport Header, { Props } from './Header'\n\nconst mockedProps = mock<Props>()\n\nconst mockReports: any = [\n  { id: 'id_1', createdAt: '2022-09-23T05:30:23.000Z' },\n  { id: 'id_2', createdAt: '2022-09-23T05:15:19.000Z' },\n]\n\nconst mockProgress = {\n  total: 10,\n  scanned: 10,\n  processed: 10,\n}\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nconst connectType = (state: any, connectionType: any) => {\n  ;(useSelector as jest.Mock).mockImplementation(\n    (callback: (arg0: RootState) => RootState) =>\n      callback({\n        ...state,\n        connections: {\n          ...state.connections,\n          instances: {\n            ...state.connections.instances,\n            connectedInstance: {\n              ...state.connections.instances.connectedInstance,\n              connectionType,\n              provider: 'REDIS_CLOUD',\n            },\n          },\n        },\n      }),\n  )\n}\n\ndescribe('DatabaseAnalysisHeader', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n    connectType(state, 'STANDALONE')\n  })\n\n  it('should render', () => {\n    expect(render(<Header {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should not render progress', () => {\n    const { queryByTestId } = render(\n      <Header\n        {...instance(mockedProps)}\n        items={mockReports}\n        progress={undefined}\n      />,\n    )\n\n    expect(queryByTestId('analysis-progress')).not.toBeInTheDocument()\n  })\n\n  it('should render progress', () => {\n    render(\n      <Header\n        {...instance(mockedProps)}\n        items={mockReports}\n        progress={mockProgress}\n      />,\n    )\n\n    expect(screen.getByTestId('analysis-progress')).toBeInTheDocument()\n  })\n\n  it('should call \"getDBAnalysis\" action be called after click \"start-database-analysis-btn\"', () => {\n    render(<Header {...instance(mockedProps)} />)\n    fireEvent.click(screen.getByTestId('start-database-analysis-btn'))\n\n    const expectedActions = [getDBAnalysis()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should send telemetry event after click \"new analysis\" btn', async () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<Header {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('start-database-analysis-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.DATABASE_ANALYSIS_STARTED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should show \"Analyze\" text on the start analysis button', async () => {\n    render(\n      <Header\n        {...instance(mockedProps)}\n        items={mockReports}\n        progress={mockProgress}\n      />,\n    )\n\n    const analizeButtonId = screen.getByTestId('start-database-analysis-btn')\n    expect(analizeButtonId).toBeInTheDocument()\n    expect(analizeButtonId.textContent).toContain('New Report')\n  })\n\n  it.skip('should call onChangeSelectedAnalysis after change selector', async () => {\n    const onChangeSelectedAnalysis = jest.fn()\n\n    const { queryByText } = render(\n      <Header\n        {...instance(mockedProps)}\n        onChangeSelectedAnalysis={onChangeSelectedAnalysis}\n        items={mockReports}\n        progress={mockProgress}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('select-report'))\n    fireEvent.click(queryByText('23 Sep 2022 05:30') || document)\n\n    expect(onChangeSelectedAnalysis).toBeCalled()\n  })\n})\n\ndescribe('CLUSTER db', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n    connectType(state, 'CLUSTER')\n  })\n\n  it('should render cluster tooltip message', async () => {\n    render(<Header {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('db-new-reports-icon'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('db-new-reports-tooltip')).toHaveTextContent(\n      'Analyze up to 10 000 keys per shard to get an overview of your data and tips on how to save memory and optimize the usage of your database.',\n    )\n  })\n})\n\ndescribe('STANDALONE db', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n    connectType(state, 'STANDALONE')\n  })\n\n  it('should render default tooltip message', async () => {\n    render(<Header {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('db-new-reports-icon'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('db-new-reports-tooltip')).toHaveTextContent(\n      'Analyze up to 10 000 keys to get an overview of your data and tips on how to save memory and optimize the usage of your database.',\n    )\n  })\n})\n\ndescribe('SENTINEL db', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n    connectType(state, 'SENTINEL')\n  })\n\n  it('should render default tooltip message', async () => {\n    render(<Header {...instance(mockedProps)} />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('db-new-reports-icon'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('db-new-reports-tooltip')).toHaveTextContent(\n      'Analyze up to 10 000 keys to get an overview of your data and tips on how to save memory and optimize the usage of your database.',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/header/Header.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\n\nexport const Container = styled(Row).attrs({\n  align: 'center',\n})``\nexport const InfoIcon = styled(RiIcon).attrs({\n  type: 'InfoIcon',\n  size: 'l',\n})`\n  cursor: pointer;\n`\n\nexport const HeaderSelect = styled(RiSelect)`\n  border: 0 none;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { CaretRightIcon } from 'uiSrc/components/base/icons'\nimport { createNewAnalysis } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { getApproximatePercentage } from 'uiSrc/utils/validations'\nimport { appContextDbConfig } from 'uiSrc/slices/app/context'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { comboBoxToArray, getDbIndex, Nullable } from 'uiSrc/utils'\nimport { AnalyticsPageHeader } from 'uiSrc/pages/database-analysis/components/analytics-page-header'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  ANALYZE_CLUSTER_TOOLTIP_MESSAGE,\n  ANALYZE_TOOLTIP_MESSAGE,\n} from 'uiSrc/constants/recommendations'\nimport { FormatedDate, RiTooltip } from 'uiSrc/components'\nimport { DEFAULT_DELIMITER } from 'uiSrc/constants'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { HideFor } from 'uiSrc/components/base/utils/ShowHide'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { ShortDatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\nimport { AnalysisProgress } from 'apiSrc/modules/database-analysis/models/analysis-progress'\n\nimport styles from './styles.module.scss'\nimport { Container, HeaderSelect, InfoIcon } from './Header.styles'\n\nexport interface Props {\n  items: ShortDatabaseAnalysis[]\n  selectedValue: Nullable<string>\n  progress?: AnalysisProgress\n  analysisLoading: boolean\n  onChangeSelectedAnalysis: (value: string) => void\n}\n\nconst Header = (props: Props) => {\n  const {\n    items = [],\n    selectedValue,\n    onChangeSelectedAnalysis,\n    progress = null,\n    analysisLoading,\n  } = props\n\n  const { connectionType, provider } = useSelector(connectedInstanceSelector)\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  const { treeViewDelimiter = [DEFAULT_DELIMITER] } =\n    useSelector(appContextDbConfig)\n\n  const analysisOptions = items.map((item) => {\n    const { createdAt, id, db } = item\n    return {\n      value: id || '',\n      label: createdAt?.toString() || '',\n      inputDisplay: (\n        <>\n          <span\n            data-test-subj={`items-report-${id}`}\n          >{`${getDbIndex(db)} `}</span>\n          <FormatedDate date={createdAt || ''} />\n        </>\n      ),\n    }\n  })\n\n  const handleClick = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.DATABASE_ANALYSIS_STARTED,\n      eventData: {\n        databaseId: instanceId,\n        provider,\n      },\n    })\n    dispatch(createNewAnalysis(instanceId, comboBoxToArray(treeViewDelimiter)))\n  }\n\n  return (\n    <div data-testid=\"db-analysis-header\">\n      <AnalyticsPageHeader\n        actions={\n          <Container justify={items.length ? 'between' : 'end'} gap=\"l\">\n            {!!items.length && (\n              <FlexItem>\n                <Row align=\"center\" wrap>\n                  <HideFor sizes={['xs', 's']}>\n                    <FlexItem>\n                      <Text size=\"s\">Report generated on:</Text>\n                    </FlexItem>\n                  </HideFor>\n                  <FlexItem grow>\n                    <HeaderSelect\n                      options={analysisOptions}\n                      valueRender={({ option }) =>\n                        option.inputDisplay as JSX.Element\n                      }\n                      value={selectedValue ?? ''}\n                      onChange={(value: string) =>\n                        onChangeSelectedAnalysis(value)\n                      }\n                      data-testid=\"select-report\"\n                    />\n                  </FlexItem>\n                  {!!progress && (\n                    <FlexItem>\n                      <Text\n                        className={styles.progress}\n                        size=\"s\"\n                        data-testid=\"bulk-delete-summary\"\n                      >\n                        <Text\n                          component=\"span\"\n                          color={\n                            progress.total === progress.processed\n                              ? undefined\n                              : 'warning'\n                          }\n                          className={styles.progress}\n                          size=\"s\"\n                          data-testid=\"analysis-progress\"\n                        >\n                          {`Scanned ${getApproximatePercentage(\n                            progress.total,\n                            progress.processed,\n                          )}`}\n                        </Text>\n                        {` (${numberWithSpaces(progress.processed)}`}/\n                        {numberWithSpaces(progress.total)}\n                        {' keys) '}\n                      </Text>\n                    </FlexItem>\n                  )}\n                </Row>\n              </FlexItem>\n            )}\n            <FlexItem>\n              <Row justify=\"end\" align=\"center\" gap=\"s\">\n                <PrimaryButton\n                  aria-label=\"New reports\"\n                  data-testid=\"start-database-analysis-btn\"\n                  icon={CaretRightIcon}\n                  iconSide=\"left\"\n                  size=\"small\"\n                  disabled={analysisLoading}\n                  onClick={handleClick}\n                >\n                  New Report\n                </PrimaryButton>\n                <RiTooltip\n                  position=\"bottom\"\n                  anchorClassName={styles.tooltipAnchor}\n                  title=\"Database Analysis\"\n                  data-testid=\"db-new-reports-tooltip\"\n                  content={\n                    connectionType === ConnectionType.Cluster\n                      ? ANALYZE_CLUSTER_TOOLTIP_MESSAGE\n                      : ANALYZE_TOOLTIP_MESSAGE\n                  }\n                >\n                  <InfoIcon data-testid=\"db-new-reports-icon\" />\n                </RiTooltip>\n              </Row>\n            </FlexItem>\n          </Container>\n        }\n      />\n    </div>\n  )\n}\n\nexport default Header\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/header/index.ts",
    "content": "import Header from './Header'\n\nexport default Header\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/header/styles.module.scss",
    "content": ".container {\n  padding: 12px 0;\n  .text {\n    color: var(--euiColorMediumShade) !important;\n  }\n}\n\n.progress {\n  display: inline-block;\n}\n\n.changeReport {\n  max-width: 180px !important;\n}\n\n.changeReport :global(.euiSuperSelectControl) {\n  border: none !important;\n  padding-right: 34px !important;\n\n  & ~ :global(.euiFormControlLayoutIcons) svg {\n    width: 12px;\n  }\n}\n\n.infoIcon {\n  color: var(--euiColorMediumShade);\n  cursor: pointer;\n}\n\n.tooltipAnchor {\n  display: inline-flex;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/index.ts",
    "content": "import AnalysisDataView from './analysis-data-view'\nimport ExpirationGroupsView from './analysis-ttl-view'\nimport EmptyAnalysisMessage from './empty-analysis-message'\nimport Header from './header'\nimport SummaryPerData from './summary-per-data'\nimport TableLoader from './table-loader'\nimport TopKeys from './top-keys/TopKeys'\nimport TopNamespace from './top-namespace/TopNamespace'\n\nexport {\n  AnalysisDataView,\n  ExpirationGroupsView,\n  EmptyAnalysisMessage,\n  Header,\n  SummaryPerData,\n  TableLoader,\n  TopKeys,\n  TopNamespace,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/recommendations-view/Recommendations.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { fireEvent, render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\n\nimport { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\nimport { findTutorialPath } from 'uiSrc/utils'\nimport Recommendations from './Recommendations'\n\nconst recommendationsContent = MOCK_RECOMMENDATIONS\nconst mockdbAnalysisSelector = jest.requireActual(\n  'uiSrc/slices/analytics/dbAnalysis',\n)\nconst mockRecommendationsSelector = jest.requireActual(\n  'uiSrc/slices/recommendations/recommendations',\n)\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\njest.mock('uiSrc/slices/recommendations/recommendations', () => ({\n  ...jest.requireActual('uiSrc/slices/recommendations/recommendations'),\n  recommendationsSelector: jest.fn().mockReturnValue({\n    data: {\n      recommendations: [],\n      totalUnread: 0,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'),\n  dbAnalysisSelector: jest.fn().mockReturnValue({\n    loading: false,\n    error: '',\n    data: null,\n    history: {\n      loading: false,\n      error: '',\n      data: [],\n      selectedAnalysis: null,\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  findTutorialPath: jest.fn(),\n}))\n\nbeforeEach(() => {\n  ;(recommendationsSelector as jest.Mock).mockImplementation(() => ({\n    ...mockRecommendationsSelector,\n    data: { recommendations: [{ name: 'RTS' }] },\n    content: recommendationsContent,\n  }))\n})\n\ndescribe('Recommendations', () => {\n  it('should render', () => {\n    expect(render(<Recommendations />)).toBeTruthy()\n  })\n\n  it('should render loader', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      loading: true,\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('recommendations-loader')).toBeInTheDocument()\n  })\n\n  it('should not render loader', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n    }))\n\n    render(<Recommendations />)\n\n    expect(\n      screen.queryByTestId('recommendations-loader'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render RecommendationVoting', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'luaScript' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.getByTestId('recommendation-voting')).toBeInTheDocument()\n  })\n\n  it('should render code changes badge in luaScript recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'luaScript' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render code changes badge in useSmallerKeys recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'useSmallerKeys' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render code changes badge and configuration_changes in bigHashes recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigHashes' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render code changes badge in avoidLogicalDatabases recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'avoidLogicalDatabases' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render code changes badge in combineSmallStringsToHashes recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'combineSmallStringsToHashes' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render configuration_changes badge in increaseSetMaxIntsetEntries recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'increaseSetMaxIntsetEntries' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render code changes badge in hashHashtableToZiplist recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'hashHashtableToZiplist' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render configuration_changes badge in compressionForList recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'compressionForList' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render configuration_changes badge in bigStrings recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigStrings' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render configuration_changes badge in zSetHashtableToZiplist recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'zSetHashtableToZiplist' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render configuration_changes badge in bigSets recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigSets' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render code_changes badge in bigAmountOfConnectedClients recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigAmountOfConnectedClients' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render configuration_changes badge in setPassword recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'setPassword' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument()\n  })\n\n  it('should render upgrade badge in redisSearch recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'redisSearch' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render upgrade badge in redisVersion recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'redisVersion' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render upgrade badge in searchIndexes recommendation', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'searchIndexes' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('upgrade')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('configuration_changes'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should collapse/expand and sent proper telemetry event', async () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'luaScript' }],\n      },\n    }))\n\n    const sendEventTelemetryMock = jest.fn()\n\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<Recommendations />)\n    let element = screen.queryByTestId('luaScript-accordion')\n    expect(element).toBeInTheDocument()\n    const toggle = screen.getByLabelText('Collapse Section')\n    const body = screen.getByTestId('ri-accordion-body-luaScript')\n    expect(body).toBeVisible()\n    await userEvent.click(toggle)\n\n    expect(body).not.toBeVisible()\n    // expect(element?.querySelector('[data-state=\"open\"]')).not.toBeTruthy()\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.DATABASE_ANALYSIS_TIPS_COLLAPSED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        recommendation: 'luaScript',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    await userEvent.click(toggle)\n    expect(body).toBeVisible()\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.DATABASE_ANALYSIS_TIPS_EXPANDED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        recommendation: 'luaScript',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should not render badges legend', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('badges-legend')).not.toBeInTheDocument()\n  })\n\n  it('should render badges legend', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'luaScript' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('badges-legend')).toBeInTheDocument()\n  })\n\n  it('should render redis-stack link', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigSets' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.queryByTestId('bigSets-redis-stack-link')).toBeInTheDocument()\n    expect(screen.queryByTestId('bigSets-redis-stack-link')).toHaveAttribute(\n      'href',\n      'https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/',\n    )\n  })\n\n  it('should render go tutorial button', () => {\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigHashes' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.getByTestId('bigHashes-to-tutorial-btn')).toBeInTheDocument()\n  })\n\n  it('should call proper telemetry after click go tutorial button', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigHashes' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.getByTestId('bigHashes-to-tutorial-btn')).toBeInTheDocument()\n    fireEvent.click(screen.getByTestId('bigHashes-to-tutorial-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.DATABASE_TIPS_TUTORIAL_CLICKED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        recommendation: 'shardHashes',\n        provider: 'REDIS_CLOUD',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper history push after click go tutorial button', () => {\n    const pushMock = jest.fn()\n    const path = 'path'\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    ;(findTutorialPath as jest.Mock).mockImplementation(() => path)\n    ;(dbAnalysisSelector as jest.Mock).mockImplementation(() => ({\n      ...mockdbAnalysisSelector,\n      data: {\n        recommendations: [{ name: 'bigHashes' }],\n      },\n    }))\n\n    render(<Recommendations />)\n\n    expect(screen.getByTestId('bigHashes-to-tutorial-btn')).toBeInTheDocument()\n    fireEvent.click(screen.getByTestId('bigHashes-to-tutorial-btn'))\n\n    expect(pushMock).toHaveBeenCalledWith({\n      search: 'path=tutorials/path',\n    })\n    pushMock.mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/recommendations-view/Recommendations.tsx",
    "content": "import React, { useContext } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { isNull } from 'lodash'\nimport cx from 'classnames'\nimport styled from 'styled-components'\n\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport {\n  FeatureFlagComponent,\n  RecommendationBadges,\n  RecommendationBadgesLegend,\n  RecommendationBody,\n  RecommendationCopyComponent,\n  RecommendationVoting,\n  RiTooltip,\n} from 'uiSrc/components'\nimport { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { FeatureFlags, Theme } from 'uiSrc/constants'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations'\nimport { sortRecommendations } from 'uiSrc/utils/recommendation'\nimport { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels'\nimport { findTutorialPath } from 'uiSrc/utils'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nimport { RiAccordion } from 'uiSrc/components/base/display/accordion/RiAccordion'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Card } from 'uiSrc/components/base/layout'\nimport { RediStackMinIcon } from 'uiSrc/components/base/icons'\n\nimport styles from './styles.module.scss'\n\nconst RecommendationContent = styled(Card)`\n  padding: ${({ theme }) => theme.core.space.space150};\n`\n\nconst Recommendations = () => {\n  const { data, loading } = useSelector(dbAnalysisSelector)\n  const { provider } = useSelector(connectedInstanceSelector)\n  const { content: recommendationsContent } = useSelector(\n    recommendationsSelector,\n  )\n  const { recommendations = [] } = data ?? {}\n\n  const { theme } = useContext(ThemeContext)\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const handleToggle = (isOpen: boolean, id: string) =>\n    sendEventTelemetry({\n      event: isOpen\n        ? TelemetryEvent.DATABASE_ANALYSIS_TIPS_EXPANDED\n        : TelemetryEvent.DATABASE_ANALYSIS_TIPS_COLLAPSED,\n      eventData: {\n        databaseId: instanceId,\n        recommendation: recommendationsContent[id]?.telemetryEvent || id,\n        provider,\n      },\n    })\n\n  const goToTutorial = (tutorialId: string, id: string) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.DATABASE_TIPS_TUTORIAL_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        recommendation: recommendationsContent[id]?.telemetryEvent || id,\n        provider,\n      },\n    })\n\n    const tutorialPath = findTutorialPath({ id: tutorialId || '' })\n    dispatch(openTutorialByPath(tutorialPath || '', history))\n  }\n\n  const onRedisStackClick = (\n    event: React.MouseEvent<HTMLDivElement, MouseEvent>,\n  ) => event.stopPropagation()\n\n  const renderButtonContent = (badges: string[], id: string) => (\n    <FlexItem className=\"recommendation-badges\" data-test-subj={`${id}-button`}>\n      <RecommendationBadges badges={badges} />\n    </FlexItem>\n  )\n  const renderLabel = (redisStack: boolean, title: string, id: string) => (\n    <Row\n      className={cx(styles.accordionBtn, styles.accordionButton)}\n      align=\"center\"\n      justify=\"start\"\n      gap=\"m\"\n      data-test-subj={`${id}-label`}\n    >\n      {redisStack && (\n        <FlexItem onClick={onRedisStackClick}>\n          <Link\n            target=\"_blank\"\n            href={EXTERNAL_LINKS.redisStack}\n            className={styles.redisStackLink}\n            data-testid={`${id}-redis-stack-link`}\n          >\n            <RiTooltip\n              content=\"Redis Stack\"\n              position=\"top\"\n              anchorClassName=\"flex-row\"\n            >\n              <RediStackMinIcon\n                className={styles.redisStackIcon}\n                data-testid={`${id}-redis-stack-icon`}\n              />\n            </RiTooltip>\n          </Link>\n        </FlexItem>\n      )}\n      <FlexItem>{title}</FlexItem>\n    </Row>\n  )\n\n  if (loading) {\n    return (\n      <div\n        className={styles.loadingWrapper}\n        data-testid=\"recommendations-loader\"\n      />\n    )\n  }\n\n  if (isNull(recommendations) || !recommendations.length) {\n    return (\n      <div\n        className={styles.container}\n        data-testid=\"empty-recommendations-message\"\n      >\n        <RiIcon\n          type={\n            theme === Theme.Dark\n              ? 'NoRecommendationsDarkIcon'\n              : 'NoRecommendationsLightIcon'\n          }\n          className={styles.noRecommendationsIcon}\n          data-testid=\"no=recommendations-icon\"\n        />\n        <Text className={styles.bigText}>AMAZING JOB!</Text>\n        <Text size=\"m\">No Tips at the moment,</Text>\n        <br />\n        <Text size=\"m\">keep up the good work!</Text>\n      </div>\n    )\n  }\n\n  return (\n    <div className={styles.wrapper}>\n      <RecommendationBadgesLegend />\n      <div className={styles.recommendationsContainer}>\n        {sortRecommendations(recommendations, recommendationsContent).map(\n          ({ name, params, vote }) => {\n            const {\n              id = '',\n              title = '',\n              content = [],\n              badges = [],\n              redisStack = false,\n              tutorialId,\n              telemetryEvent,\n            } = recommendationsContent[name] || {}\n\n            if (!(name in recommendationsContent)) {\n              return null\n            }\n\n            return (\n              <div\n                key={id}\n                className={styles.recommendation}\n                data-testid={`${id}-recommendation`}\n              >\n                <RiAccordion\n                  id={name}\n                  key={`${name}-accordion`}\n                  label={renderLabel(redisStack, title, id)}\n                  actions={renderButtonContent(badges, id)}\n                  className={styles.accordion}\n                  defaultOpen\n                  onOpenChange={(isOpen) => handleToggle(isOpen, id)}\n                  data-testid={`${id}-accordion`}\n                >\n                  <RecommendationContent>\n                    <RecommendationBody\n                      elements={content}\n                      params={params}\n                      telemetryName={telemetryEvent ?? name}\n                    />\n                    {!!params?.keys?.length && (\n                      <RecommendationCopyComponent\n                        keyName={params.keys[0]}\n                        provider={provider}\n                        telemetryEvent={\n                          recommendationsContent[name]?.telemetryEvent ?? name\n                        }\n                      />\n                    )}\n                  </RecommendationContent>\n                </RiAccordion>\n                <div className={styles.footer}>\n                  <FeatureFlagComponent name={FeatureFlags.envDependent}>\n                    <RecommendationVoting vote={vote as Vote} name={name} />\n                  </FeatureFlagComponent>\n                  {tutorialId && (\n                    <PrimaryButton\n                      size=\"s\"\n                      onClick={() => goToTutorial(tutorialId, id)}\n                      data-testid={`${id}-to-tutorial-btn`}\n                    >\n                      Tutorial\n                    </PrimaryButton>\n                  )}\n                </div>\n              </div>\n            )\n          },\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport default Recommendations\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/recommendations-view/index.ts",
    "content": "import Recommendations from './Recommendations'\n\nexport default Recommendations\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/recommendations-view/styles.module.scss",
    "content": ".wrapper {\n  height: 100%;\n}\n\n.recommendationsContainer {\n  @include eui.scrollBar;\n  overflow-y: auto;\n  overflow-x: hidden;\n  max-height: calc(100% - 70px);\n\n  .accordionButton :global(.RI-flex-item) {\n    margin: 0;\n\n    .redisStackLink {\n      margin-right: 16px;\n      animation: none !important;\n    }\n\n    .redisStackIcon {\n      width: 20px;\n      height: 20px;\n    }\n\n    :global(.euiScreenReaderOnly) {\n      display: none;\n    }\n  }\n\n  :global(.euiAccordion__buttonReverse .euiAccordion__iconWrapper) {\n    margin-left: 0;\n  }\n\n  :global(.euiFlexGroup--gutterLarge) {\n    margin: 0;\n  }\n}\n\n.container {\n  display: flex;\n  justify-content: center;\n  flex-direction: column;\n  align-items: center;\n  height: 100%;\n  padding-bottom: 162px;\n\n  .noRecommendationsIcon {\n    width: 154px;\n    height: 127px;\n  }\n}\n\n.bigText {\n  font:\n    normal normal 600 18px/22px Graphik,\n    sans-serif !important;\n  margin: 16px 0 12px;\n}\n\n.loadingWrapper {\n  width: 100%;\n  height: 129px;\n  margin-top: 30px;\n  background-color: var(--euiColorLightestShade);\n  border-radius: 4px;\n}\n\n.recommendation {\n  border-radius: 8px;\n  border: 1px solid var(--recommendationBorderColor);\n  background-color: var(--euiColorLightestShade) !important;\n  margin-bottom: 6px;\n  padding: 30px 18px 11px;\n\n  .footer {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n\n    padding-top: 15px;\n    margin-top: 15px !important;\n    border-top: 1px solid var(--separatorColor);\n  }\n\n  :global(.euiAccordion__triggerWrapper) {\n    background-color: transparent;\n  }\n\n  :global(.euiPanel.euiPanel--subdued) {\n    background-color: transparent;\n  }\n\n  :global(.euiAccordion.euiAccordion-isOpen .euiAccordion__triggerWrapper) {\n    border-bottom: none;\n  }\n\n  :global(.euiIEFlexWrapFix) {\n    display: block;\n    width: 100%;\n  }\n\n  .accordionBtn {\n    font:\n      normal normal 500 16px/19px Graphik,\n      sans-serif;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/styles.ts",
    "content": "import React from 'react'\nimport styled, { css } from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { Title } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Section = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n  margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space250};\n  overflow: hidden;\n  padding: ${({ theme }: { theme: Theme }) =>\n    `${theme.core.space.space300} ${theme.core.space.space500}`};\n\n  @media screen and (max-width: 920px) {\n    padding: ${({ theme }: { theme: Theme }) =>\n      `${theme.core.space.space200} ${theme.core.space.space250}`};\n  }\n`\n\nexport const sectionContent = css`\n  max-width: 1720px;\n  margin: 0 auto;\n`\n\nexport const SectionTitleWrapper = styled(Row).attrs({ align: 'center' })`\n  margin-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space250};\n`\n\nexport const SwitchExtrapolateResults = styled(SwitchInput)`\n  margin-left: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n`\n\nexport const SectionTitle = styled(Title)`\n  display: inline-block;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.spec.tsx",
    "content": "import React from 'react'\nimport { getGroupTypeDisplay } from 'uiSrc/utils'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\n\nimport SummaryPerData from './SummaryPerData'\n\nconst mockData = {\n  totalMemory: {\n    total: 75,\n    types: [\n      {\n        type: 'hash',\n        total: 18,\n      },\n      {\n        type: 'TSDB-TYPE',\n        total: 11,\n      },\n      {\n        type: 'string',\n        total: 10,\n      },\n      {\n        type: 'list',\n        total: 9,\n      },\n      {\n        type: 'stream',\n        total: 8,\n      },\n      {\n        type: 'zset',\n        total: 8,\n      },\n      {\n        type: 'set',\n        total: 7,\n      },\n      {\n        type: 'graphdata',\n        total: 2,\n      },\n      {\n        type: 'ReJSON-RL',\n        total: 1,\n      },\n      {\n        type: 'MBbloom--',\n        total: 1,\n      },\n    ],\n  },\n  totalKeys: {\n    total: 1168424,\n    types: [\n      {\n        type: 'hash',\n        total: 572813,\n      },\n      {\n        type: 'zset',\n        total: 233571,\n      },\n      {\n        type: 'set',\n        total: 138184,\n      },\n      {\n        type: 'list',\n        total: 95886,\n      },\n      {\n        type: 'stream',\n        total: 79532,\n      },\n      {\n        type: 'TSDB-TYPE',\n        total: 47143,\n      },\n      {\n        type: 'string',\n        total: 891,\n      },\n      {\n        type: 'MBbloom--',\n        total: 272,\n      },\n      {\n        type: 'graphdata',\n        total: 72,\n      },\n      {\n        type: 'ReJSON-RL',\n        total: 60,\n      },\n    ],\n  },\n} as DatabaseAnalysis\n\ndescribe('SummaryPerData', () => {\n  it('should render', () => {\n    expect(\n      render(<SummaryPerData data={mockData} loading={false} />),\n    ).toBeTruthy()\n  })\n\n  it('should render nothing without data', () => {\n    render(<SummaryPerData data={null} loading={false} />)\n\n    expect(\n      screen.queryByTestId('summary-per-data-loading'),\n    ).not.toBeInTheDocument()\n    expect(screen.queryByTestId('summary-per-data')).not.toBeInTheDocument()\n  })\n\n  it('should render loading', () => {\n    render(<SummaryPerData data={mockData} loading />)\n\n    expect(screen.getByTestId('summary-per-data-loading')).toBeInTheDocument()\n  })\n\n  it('should render charts', () => {\n    render(<SummaryPerData data={mockData} loading={false} />)\n    expect(screen.getByTestId('donut-memory')).toBeInTheDocument()\n    expect(screen.queryByTestId('donut-keys')).toBeInTheDocument()\n  })\n\n  it('should render chart arcs', () => {\n    render(<SummaryPerData data={mockData} loading={false} />)\n\n    mockData.totalKeys.types.forEach((t) => {\n      expect(\n        screen.getByTestId(`arc-${getGroupTypeDisplay(t.type)}-${t.total}`),\n      ).toBeInTheDocument()\n    })\n\n    mockData.totalMemory.types.forEach((t) => {\n      expect(\n        screen.getByTestId(`arc-${getGroupTypeDisplay(t.type)}-${t.total}`),\n      ).toBeInTheDocument()\n    })\n  })\n\n  it('should render chart labels', () => {\n    render(<SummaryPerData data={mockData} loading={false} />)\n\n    mockData.totalKeys.types.forEach((t) => {\n      expect(\n        screen.getByTestId(`label-${getGroupTypeDisplay(t.type)}-${t.total}`),\n      ).toBeInTheDocument()\n    })\n\n    mockData.totalMemory.types.forEach((t) => {\n      expect(\n        screen.getByTestId(`label-${getGroupTypeDisplay(t.type)}-${t.total}`),\n      ).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { insightsOpen } from 'uiSrc/styles/mixins'\nimport { sectionContent } from 'uiSrc/pages/database-analysis/components/styles'\n\nexport const Wrapper = styled.div<React.HTMLAttributes<HTMLDivElement>>``\n\nexport const ChartsWrapper = styled.div<\n  React.HTMLAttributes<HTMLDivElement> & {\n    $isSection?: boolean\n    $isLoading?: boolean\n  }\n>`\n  display: flex;\n  align-items: center;\n  justify-content: space-around;\n  ${insightsOpen()`\n    flex-direction: column;\n  `}\n  ${({ $isSection }) => $isSection && sectionContent}\n  ${({ $isLoading, theme }) =>\n    $isLoading && `margin-top: ${theme.core.space.space400};`}\n`\n\nexport const PreloaderCircle = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  width: 180px;\n  height: 180px;\n  margin: 60px 0;\n  border-radius: 100%;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral200};\n`\nexport const LabelTooltip = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  font-size: 12px;\n  font-weight: bold;\n`\n\nexport const TooltipPercentage = styled.span<\n  React.HTMLAttributes<HTMLSpanElement>\n>`\n  margin-right: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n`\n\nexport const TitleSeparator = styled.hr<React.HTMLAttributes<HTMLHRElement>>`\n  height: 1px;\n  border: 0;\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.border.neutral500};\n  margin: ${({ theme }: { theme: Theme }) => theme.core.space.space050} 0;\n  min-width: 60px;\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/summary-per-data/SummaryPerData.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\n\nimport { DonutChart } from 'uiSrc/components/charts'\nimport { ChartData } from 'uiSrc/components/charts/donut-chart/DonutChart'\nimport { GROUP_TYPES_COLORS, GroupTypesColors } from 'uiSrc/constants'\nimport {\n  DEFAULT_EXTRAPOLATION,\n  SectionName,\n} from 'uiSrc/pages/database-analysis'\nimport {\n  extrapolate,\n  formatBytes,\n  getGroupTypeDisplay,\n  Nullable,\n} from 'uiSrc/utils'\nimport { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { RiIcon, AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\nimport {\n  DatabaseAnalysis,\n  SimpleTypeSummary,\n} from 'apiSrc/modules/database-analysis/models'\n\nimport {\n  ChartsWrapper,\n  LabelTooltip,\n  PreloaderCircle,\n  TitleSeparator,\n  TooltipPercentage,\n  Wrapper,\n} from './SummaryPerData.styles'\nimport {\n  Section,\n  SectionTitleWrapper,\n  SwitchExtrapolateResults,\n} from 'uiSrc/pages/database-analysis/components/styles'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport interface Props {\n  data: Nullable<DatabaseAnalysis>\n  loading: boolean\n  extrapolation?: number\n  onSwitchExtrapolation?: (value: boolean, section: SectionName) => void\n}\n\ninterface DonutChartTitleProps {\n  icon: AllIconsType\n  title: string\n  value: string | number\n  testId: string\n}\n\nconst DonutChartTitle = ({\n  icon,\n  title,\n  value,\n  testId,\n}: DonutChartTitleProps) => (\n  <Col align=\"center\" grow={false} gap=\"xs\">\n    <Row align=\"center\" gap=\"m\" data-testid={`donut-title-${testId}`}>\n      <RiIcon type={icon} size=\"m\" />\n      <Title size=\"XS\">{title}</Title>\n    </Row>\n    <TitleSeparator />\n    <Text\n      component=\"div\"\n      variant=\"semiBold\"\n      data-testid={`total-${testId}-value`}\n    >\n      {String(value)}\n    </Text>\n  </Col>\n)\n\nconst widthResponsiveSize = 1024\nconst CHART_WITH_LABELS_WIDTH = 432\nconst CHART_WIDTH = 320\nconst getChartData = (t: SimpleTypeSummary): ChartData => ({\n  value: t.total,\n  name: getGroupTypeDisplay(t.type),\n  color:\n    t.type in GROUP_TYPES_COLORS\n      ? GROUP_TYPES_COLORS[t.type as GroupTypesColors]\n      : 'var(--defaultTypeColor)',\n  meta: { ...t },\n})\n\nconst SummaryPerData = ({\n  data,\n  loading,\n  extrapolation,\n  onSwitchExtrapolation,\n}: Props) => {\n  const { totalMemory, totalKeys } = data || {}\n  const [memoryData, setMemoryData] = useState<ChartData[]>([])\n  const [keysData, setKeysData] = useState<ChartData[]>([])\n  const [isExtrapolated, setIsExtrapolated] = useState<boolean>(true)\n  const [hideLabelTitle, setHideLabelTitle] = useState(false)\n\n  const updateChartSize = () => {\n    setHideLabelTitle(globalThis.innerWidth < widthResponsiveSize)\n  }\n\n  useEffect(() => {\n    setIsExtrapolated(extrapolation !== DEFAULT_EXTRAPOLATION)\n  }, [data, extrapolation])\n\n  useEffect(() => {\n    updateChartSize()\n    globalThis.addEventListener('resize', updateChartSize)\n    return () => {\n      globalThis.removeEventListener('resize', updateChartSize)\n    }\n  }, [])\n\n  useEffect(() => {\n    if (data && totalMemory && totalKeys) {\n      setMemoryData(totalMemory.types?.map(getChartData))\n      setKeysData(totalKeys.types?.map(getChartData))\n    }\n  }, [data])\n\n  const renderMemoryTooltip = useCallback(\n    ({ value, name }: ChartData) => (\n      <LabelTooltip data-testid=\"tooltip-memory\">\n        <span data-testid=\"tooltip-key-type\">{name}: </span>\n        <TooltipPercentage data-testid=\"tooltip-key-percent\">\n          {getPercentage(value, totalMemory?.total)}%\n        </TooltipPercentage>\n        <span data-testid=\"tooltip-total-memory\">\n          (&thinsp;\n          {extrapolate(\n            value,\n            { extrapolation, apply: isExtrapolated },\n            (val: number) => formatBytes(val, 3, false) as string,\n          )}\n          &thinsp;)\n        </span>\n      </LabelTooltip>\n    ),\n    [totalMemory, extrapolation, isExtrapolated],\n  )\n\n  const renderKeysTooltip = useCallback(\n    ({ name, value }: ChartData) => (\n      <LabelTooltip data-testid=\"tooltip-keys\">\n        <span data-testid=\"tooltip-key-type\">{name}: </span>\n        <TooltipPercentage data-testid=\"tooltip-key-percent\">\n          {getPercentage(value, totalKeys?.total)}%\n        </TooltipPercentage>\n        <span data-testid=\"tooltip-total-keys\">\n          (&thinsp;\n          {extrapolate(\n            value,\n            { extrapolation, apply: isExtrapolated },\n            (val: number) => numberWithSpaces(Math.round(val)),\n          )}\n          &thinsp;)\n        </span>\n      </LabelTooltip>\n    ),\n    [totalKeys, extrapolation, isExtrapolated],\n  )\n\n  if (loading) {\n    return (\n      <Wrapper>\n        <ChartsWrapper $isLoading data-testid=\"summary-per-data-loading\">\n          <PreloaderCircle />\n          <PreloaderCircle />\n        </ChartsWrapper>\n      </Wrapper>\n    )\n  }\n\n  if (\n    (!totalMemory || memoryData.length === 0) &&\n    (!totalKeys || keysData.length === 0)\n  ) {\n    return null\n  }\n\n  return (\n    <Section data-testid=\"summary-per-data\">\n      <SectionTitleWrapper>\n        <Title size=\"M\">SUMMARY PER DATA TYPE</Title>\n        {extrapolation !== DEFAULT_EXTRAPOLATION && (\n          <SwitchExtrapolateResults\n            title=\"Extrapolate results\"\n            checked={isExtrapolated}\n            onCheckedChange={(checked) => {\n              setIsExtrapolated(checked)\n              onSwitchExtrapolation?.(checked, SectionName.SUMMARY_PER_DATA)\n            }}\n            data-testid=\"extrapolate-results\"\n          />\n        )}\n      </SectionTitleWrapper>\n      <ChartsWrapper $isSection data-testid=\"summary-per-data-charts\">\n        <DonutChart\n          name=\"memory\"\n          data={memoryData}\n          labelAs=\"percentage\"\n          hideLabelTitle={hideLabelTitle}\n          width={hideLabelTitle ? CHART_WIDTH : CHART_WITH_LABELS_WIDTH}\n          config={{ radius: 94 }}\n          renderTooltip={renderMemoryTooltip}\n          title={\n            <DonutChartTitle\n              icon=\"MemoryIconIcon\"\n              title=\"Memory\"\n              testId=\"memory\"\n              value={extrapolate(\n                totalMemory?.total || 0,\n                { extrapolation, apply: isExtrapolated },\n                (val: number) => formatBytes(val || 0, 3) as string,\n              )}\n            />\n          }\n        />\n        <DonutChart\n          name=\"keys\"\n          data={keysData}\n          labelAs=\"percentage\"\n          hideLabelTitle={hideLabelTitle}\n          width={hideLabelTitle ? CHART_WIDTH : CHART_WITH_LABELS_WIDTH}\n          config={{ radius: 94 }}\n          renderTooltip={renderKeysTooltip}\n          title={\n            <DonutChartTitle\n              icon=\"KeyIconIcon\"\n              title=\"Keys\"\n              testId=\"keys\"\n              value={extrapolate(\n                totalKeys?.total || 0,\n                { extrapolation, apply: isExtrapolated },\n                (val: number) =>\n                  numberWithSpaces(Math.round(val) || 0) as string,\n              )}\n            />\n          }\n        />\n      </ChartsWrapper>\n    </Section>\n  )\n}\n\nexport default SummaryPerData\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/summary-per-data/index.ts",
    "content": "import SummaryPerData from './SummaryPerData'\n\nexport default SummaryPerData\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport TableLoader from './TableLoader'\n\ndescribe('TableLoader', () => {\n  it('should render', () => {\n    expect(render(<TableLoader />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.styles.ts",
    "content": "import React from 'react'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport LoadingContent from 'uiSrc/components/base/layout/loading-content/LoadingContent'\nimport { SingleLine } from 'uiSrc/components/base/layout/loading-content/loading-content.styles'\n\nexport const Container = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  margin-top: ${({ theme }: { theme: Theme }) => theme.core.space.space250};\n`\nexport const TableLoaderTitle = styled(LoadingContent)`\n  ${SingleLine} {\n    height: ${({ theme }) => theme.core.space.space550} !important;\n    margin-bottom: ${({ theme }) => theme.core.space.space150};\n  }\n`\nexport const TableLoaderTable = styled(LoadingContent)`\n  ${SingleLine} {\n    height: ${({ theme }) => theme.core.space.space400} !important;\n    margin-bottom: ${({ theme }) => theme.core.space.space100};\n\n    &:last-child:not(:only-child) {\n      width: 100% !important;\n    }\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/table-loader/TableLoader.tsx",
    "content": "import React from 'react'\nimport {\n  Container,\n  TableLoaderTable,\n  TableLoaderTitle,\n} from './TableLoader.styles'\n\nconst TableLoader = () => (\n  <Container data-testid=\"table-loader\">\n    <TableLoaderTitle lines={1} />\n    <TableLoaderTable lines={3} />\n  </Container>\n)\n\nexport default TableLoader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/table-loader/index.ts",
    "content": "import TableLoader from './TableLoader'\n\nexport default TableLoader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { DatabaseAnalysisFactory } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory'\n\nimport TopKeys, { Props } from './TopKeys'\n\nconst mockedProps = mock<Props>()\n\nconst mockKey = {\n  name: 'name',\n  type: 'HASH',\n  memory: 1000,\n  length: 10,\n  ttl: -1,\n}\n\nconst mockData = DatabaseAnalysisFactory.build({\n  topKeysLength: [mockKey],\n  topKeysMemory: [mockKey],\n})\n\ndescribe('TopKeys', () => {\n  it('should render', () => {\n    expect(render(<TopKeys {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render top-keys-table-length when click \"btn-change-table-keys\" ', () => {\n    const { queryByTestId } = render(\n      <TopKeys {...instance(mockedProps)} data={mockData} />,\n    )\n\n    fireEvent.click(screen.getByTestId('btn-change-table-keys'))\n\n    expect(queryByTestId('top-keys-table-memory')).not.toBeInTheDocument()\n    expect(queryByTestId('top-keys-table-length')).toBeInTheDocument()\n    expect(queryByTestId('btn-change-table-memory')).not.toBeDisabled()\n    expect(queryByTestId('btn-change-table-keys')).toBeDisabled()\n  })\n\n  it('should render top-keys-table-memory when click \"btn-change-table-memory\" and memory button should be disabled', () => {\n    const { queryByTestId } = render(\n      <TopKeys {...instance(mockedProps)} data={mockData} />,\n    )\n\n    // memory button is disabled by default\n    fireEvent.click(screen.getByTestId('btn-change-table-keys'))\n    fireEvent.click(screen.getByTestId('btn-change-table-memory'))\n\n    expect(queryByTestId('top-keys-table-memory')).toBeInTheDocument()\n    expect(queryByTestId('top-keys-table-length')).not.toBeInTheDocument()\n    expect(queryByTestId('btn-change-table-memory')).toBeDisabled()\n    expect(queryByTestId('btn-change-table-keys')).not.toBeDisabled()\n  })\n\n  it('should render top-keys-table-memory by default\" ', () => {\n    const { queryByTestId } = render(\n      <TopKeys {...instance(mockedProps)} data={mockData} />,\n    )\n\n    expect(queryByTestId('top-keys-table-memory')).toBeInTheDocument()\n    expect(queryByTestId('top-keys-table-length')).not.toBeInTheDocument()\n    expect(queryByTestId('btn-change-table-memory')).toBeDisabled()\n    expect(queryByTestId('btn-change-table-keys')).not.toBeDisabled()\n  })\n\n  it('should not render tables when topKeysLength and topKeysMemory are empty array', () => {\n    const emptyMockData = DatabaseAnalysisFactory.build({\n      topKeysLength: [],\n      topKeysMemory: [],\n    })\n    const { queryByTestId } = render(\n      <TopKeys {...instance(mockedProps)} data={emptyMockData} />,\n    )\n\n    expect(queryByTestId('top-keys-table-memory')).not.toBeInTheDocument()\n    expect(queryByTestId('top-keys-table-length')).not.toBeInTheDocument()\n  })\n\n  it('should render loader when loading=\"true\"', () => {\n    const { queryByTestId } = render(\n      <TopKeys {...instance(mockedProps)} data={mockData} loading />,\n    )\n    expect(queryByTestId('top-keys-table-memory')).not.toBeInTheDocument()\n    expect(queryByTestId('top-keys-table-length')).not.toBeInTheDocument()\n    expect(queryByTestId('table-loader')).toBeInTheDocument()\n  })\n\n  it('should render TOP KEYS title', () => {\n    const { queryByText } = render(\n      <TopKeys {...instance(mockedProps)} data={mockData} />,\n    )\n    expect(queryByText('TOP KEYS')).toBeInTheDocument()\n    expect(queryByText('TOP 15 KEYS')).not.toBeInTheDocument()\n  })\n\n  it('should render TOP 15 KEYS title', () => {\n    const largeMockData = DatabaseAnalysisFactory.build({\n      topKeysLength: Array.from({ length: 15 }, () => mockKey),\n      topKeysMemory: Array.from({ length: 15 }, () => mockKey),\n    })\n\n    const { queryByText } = render(\n      <TopKeys {...instance(mockedProps)} data={largeMockData} />,\n    )\n\n    expect(queryByText('TOP KEYS')).not.toBeInTheDocument()\n    expect(queryByText('TOP 15 KEYS')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { DatabaseAnalysisFactory } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory'\n\nimport TopKeys from './TopKeys'\n\nconst meta: Meta<typeof TopKeys> = {\n  component: TopKeys,\n  args: {\n    data: null,\n    loading: false,\n  },\n  decorators: [\n    (Story) => (\n      <div style={{ padding: '20px', border: '1px solid #ccc' }}>\n        <h1>Top Keys</h1>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Loading: Story = {\n  args: {\n    loading: true,\n  },\n  decorators: [\n    (Story) => (\n      <div>\n        <h2>Loading</h2>\n        <p>Component shows loader while fetching keys data</p>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport const NoKeys: Story = {\n  args: {\n    loading: false,\n    data: DatabaseAnalysisFactory.build({\n      topKeysMemory: [],\n      topKeysLength: [],\n    }),\n  },\n  decorators: [\n    (Story) => (\n      <div>\n        <h2>No Keys</h2>\n        <p>Component returns null when no top keys data is available</p>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport const WithData: Story = {\n  args: {\n    loading: false,\n    data: DatabaseAnalysisFactory.build({\n      topKeysMemory: [\n        {\n          name: 'user:sessions',\n          type: 'hash',\n          memory: 1_000_000,\n          length: 5000,\n          ttl: -1,\n        },\n        {\n          name: 'orders:recent',\n          type: 'list',\n          memory: 500_000,\n          length: 2000,\n          ttl: 3600,\n        },\n        {\n          name: 'metrics:pageviews',\n          type: 'zset',\n          memory: 250_000,\n          length: 1000,\n          ttl: -1,\n        },\n      ],\n      topKeysLength: [\n        {\n          name: 'users:all',\n          type: 'set',\n          memory: 400_000,\n          length: 10000,\n          ttl: -1,\n        },\n        {\n          name: 'logs:errors',\n          type: 'list',\n          memory: 150_000,\n          length: 5000,\n          ttl: 7200,\n        },\n        {\n          name: 'cache:products',\n          type: 'hash',\n          memory: 80_000,\n          length: 2500,\n          ttl: 86400,\n        },\n      ],\n      delimiter: ':',\n    }),\n  },\n  decorators: [\n    (Story) => (\n      <div>\n        <h2>With Data - Memory View</h2>\n        <p>Component shows keys sorted by memory consumption (default view)</p>\n        <Story />\n      </div>\n    ),\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeys.tsx",
    "content": "import React, { useState } from 'react'\nimport { TableView } from 'uiSrc/pages/database-analysis/constants'\nimport { Nullable } from 'uiSrc/utils'\nimport TableLoader from 'uiSrc/pages/database-analysis/components/table-loader'\nimport { TextBtn } from 'uiSrc/pages/database-analysis/components/base/TextBtn'\nimport { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\nimport {\n  Section,\n  SectionTitle,\n  SectionTitleWrapper,\n} from 'uiSrc/pages/database-analysis/components/styles'\n\nimport TopKeysTable from './TopKeysTable'\nimport { SectionContent } from 'uiSrc/pages/database-analysis/components/top-namespace/TopNamespace.styles'\n\nexport interface Props {\n  data: Nullable<DatabaseAnalysis>\n  loading: boolean\n}\n\nconst MAX_TOP_KEYS = 15\nconst TopKeys = ({ data, loading }: Props) => {\n  const { topKeysLength = [], topKeysMemory = [], delimiter } = data || {}\n  const [tableView, setTableView] = useState<TableView>(TableView.MEMORY)\n\n  if (loading) {\n    return <TableLoader />\n  }\n\n  if (!topKeysLength?.length && !topKeysMemory?.length) {\n    return null\n  }\n\n  return (\n    <Section>\n      <SectionTitleWrapper>\n        <SectionTitle size=\"M\" data-testid=\"top-keys-title\">\n          {topKeysLength.length < MAX_TOP_KEYS &&\n          topKeysMemory?.length < MAX_TOP_KEYS\n            ? 'TOP KEYS'\n            : `TOP ${MAX_TOP_KEYS} KEYS`}\n        </SectionTitle>\n        <TextBtn\n          $active={tableView === TableView.MEMORY}\n          size=\"small\"\n          onClick={() => setTableView(TableView.MEMORY)}\n          disabled={tableView === TableView.MEMORY}\n          data-testid=\"btn-change-table-memory\"\n        >\n          by Memory\n        </TextBtn>\n        <TextBtn\n          $active={tableView === TableView.KEYS}\n          size=\"small\"\n          onClick={() => setTableView(TableView.KEYS)}\n          disabled={tableView === TableView.KEYS}\n          data-testid=\"btn-change-table-keys\"\n        >\n          by Length\n        </TextBtn>\n      </SectionTitleWrapper>\n      <SectionContent>\n        {tableView === TableView.MEMORY && (\n          <TopKeysTable\n            data={topKeysMemory}\n            defaultSortField=\"memory\"\n            delimiter={delimiter}\n            dataTestid=\"top-keys-table-memory\"\n          />\n        )}\n        {tableView === TableView.KEYS && (\n          <TopKeysTable\n            data={topKeysLength}\n            defaultSortField=\"length\"\n            delimiter={delimiter}\n            dataTestid=\"top-keys-table-length\"\n          />\n        )}\n      </SectionContent>\n    </Section>\n  )\n}\n\nexport default TopKeys\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { Key } from 'apiSrc/modules/database-analysis/models/key'\n\nimport TopKeysTable, { Props } from './TopKeysTable'\n\nconst mockedProps = mock<Props>()\n\nconst mockData: Key[] = [\n  {\n    name: 'name',\n    type: 'hash',\n    memory: 10_000_000,\n    length: 100_000_000,\n    ttl: 10,\n  },\n  {\n    name: 'name_1',\n    type: 'hash',\n    memory: 1000,\n    length: null as any,\n    ttl: -1,\n  },\n]\n\ndescribe('Table', () => {\n  it('should render', () => {\n    expect(render(<TopKeysTable {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render', () => {\n    expect(render(<TopKeysTable {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render table with 2 items', () => {\n    render(<TopKeysTable {...instance(mockedProps)} data={mockData} />)\n    expect(screen.getAllByTestId('top-keys-table-name')).toHaveLength(2)\n  })\n\n  it('should render correct ttl', () => {\n    render(<TopKeysTable {...instance(mockedProps)} data={mockData} />)\n    expect(screen.getByTestId('ttl-no-limit-name_1')).toHaveTextContent(\n      'No limit',\n    )\n    expect(screen.getByTestId('ttl-name')).toHaveTextContent('10 s')\n  })\n\n  it('should render correct length', () => {\n    render(<TopKeysTable {...instance(mockedProps)} data={mockData} />)\n    expect(screen.getByTestId('length-empty-name_1')).toHaveTextContent('-')\n    expect(screen.getByTestId(/length-value-name/).textContent).toEqual(\n      '100 000 000',\n    )\n  })\n\n  it('should highlight big keys', () => {\n    render(<TopKeysTable {...instance(mockedProps)} data={mockData} />)\n    expect(\n      screen.getByTestId('nsp-usedMemory-value=10000000-highlighted'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByTestId('length-value-name-highlighted'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-keys/TopKeysTable.tsx",
    "content": "import { isNil } from 'lodash'\nimport React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\n\nimport { GroupBadge, RiTooltip } from 'uiSrc/components'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport {\n  resetBrowserTree,\n  setBrowserKeyListDataLoaded,\n  setBrowserSelectedKey,\n  setBrowserTreeDelimiter,\n} from 'uiSrc/slices/app/context'\nimport {\n  changeSearchMode,\n  fetchKeys,\n  keysSelector,\n  resetKeysData,\n  setFilter,\n  setSearchMatch,\n} from 'uiSrc/slices/browser/keys'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\n\nimport {\n  formatBytes,\n  formatLongName,\n  HighlightType,\n  isBigKey,\n  stringToBuffer,\n  truncateNumberToDuration,\n  truncateNumberToFirstUnit,\n  truncateTTLToSeconds,\n} from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { TableTextBtn } from 'uiSrc/pages/database-analysis/components/base/TableTextBtn'\nimport { Table, ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { Key } from 'apiSrc/modules/database-analysis/models/key'\nimport { CellText } from 'uiSrc/components/auto-discover'\n\nexport interface Props {\n  data: Key[]\n  defaultSortField: string\n  delimiter?: string\n  dataTestid?: string\n}\n\nconst TopKeysTable = ({\n  data = [],\n  defaultSortField,\n  delimiter = ':',\n  dataTestid = '',\n}: Props) => {\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const { viewType } = useSelector(keysSelector)\n\n  const handleRedirect = (name: string) => {\n    dispatch(changeSearchMode(SearchMode.Pattern))\n    dispatch(setBrowserTreeDelimiter([{ label: delimiter }]))\n    dispatch(setFilter(null))\n    dispatch(setSearchMatch(name, SearchMode.Pattern))\n    dispatch(resetKeysData(SearchMode.Pattern))\n    dispatch(\n      fetchKeys(\n        {\n          searchMode: SearchMode.Pattern,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n        },\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, true)),\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false)),\n      ),\n    )\n    dispatch(resetBrowserTree())\n    dispatch(setBrowserSelectedKey(stringToBuffer(name)))\n\n    history.push(Pages.browser(instanceId))\n  }\n\n  const columns: ColumnDef<Key>[] = [\n    {\n      header: 'Key Type',\n      id: 'type',\n      accessorKey: 'type',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { type },\n        },\n      }) => <GroupBadge key={type} type={type} />,\n    },\n    {\n      header: 'Key Name',\n      id: 'name',\n      accessorKey: 'name',\n      enableSorting: true,\n      minSize: 200,\n      cell: ({\n        row: {\n          original: { name },\n        },\n      }) => {\n        const maxLength = 35\n        const nameAsString = name as string\n        const tooltipContent = formatLongName(nameAsString)\n        const cellContent = nameAsString.substring(0, maxLength)\n        const isTruncated = nameAsString.length > maxLength\n        return (\n          <div data-testid=\"top-keys-table-name\">\n            <RiTooltip\n              title=\"Key Name\"\n              position=\"bottom\"\n              content={tooltipContent}\n            >\n              <TableTextBtn onClick={() => handleRedirect(nameAsString)}>\n                {`${cellContent}${isTruncated ? '...' : ''}`}\n              </TableTextBtn>\n            </RiTooltip>\n          </div>\n        )\n      },\n    },\n    {\n      header: 'TTL',\n      id: 'ttl',\n      accessorKey: 'ttl',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { name, ttl: value },\n        },\n      }) => {\n        if (isNil(value)) {\n          return <CellText data-testid={`ttl-empty-${value}`}>-</CellText>\n        }\n        if (value === -1) {\n          return (\n            <CellText data-testid={`ttl-no-limit-${name}`}>No limit</CellText>\n          )\n        }\n\n        return (\n          <RiTooltip\n            title=\"Time to Live\"\n            anchorClassName=\"truncateText\"\n            position=\"bottom\"\n            content={\n              <>\n                {`${truncateTTLToSeconds(value)} s`}\n                <br />\n                {`(${truncateNumberToDuration(value)})`}\n              </>\n            }\n          >\n            <CellText data-testid={`ttl-${name}`}>\n              {truncateNumberToFirstUnit(value)}\n            </CellText>\n          </RiTooltip>\n        )\n      },\n    },\n    {\n      header: 'Key Size',\n      id: 'memory',\n      accessorKey: 'memory',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { type, memory: value },\n        },\n      }) => {\n        if (isNil(value)) {\n          return <CellText data-testid={`size-empty-${value}`}>-</CellText>\n        }\n        const [number, size] = formatBytes(value, 3, true)\n        const isHighlight = isBigKey(type, HighlightType.Memory, value)\n        return (\n          <RiTooltip\n            content={\n              <>\n                {isHighlight ? (\n                  <>\n                    Consider splitting it into multiple keys\n                    <br />\n                  </>\n                ) : null}\n                {numberWithSpaces(value)} B\n              </>\n            }\n            data-testid=\"usedMemory-tooltip\"\n          >\n            <CellText\n              data-testid={`nsp-usedMemory-value=${value}${isHighlight ? '-highlighted' : ''}`}\n            >\n              {number} {size}\n            </CellText>\n          </RiTooltip>\n        )\n      },\n    },\n    {\n      header: 'Length',\n      id: 'length',\n      accessorKey: 'length',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { name, type, length: value },\n        },\n      }) => {\n        if (isNil(value)) {\n          return <CellText data-testid={`length-empty-${name}`}>-</CellText>\n        }\n\n        const isHighlight = isBigKey(type, HighlightType.Length, value)\n        return (\n          <RiTooltip\n            content={\n              isHighlight ? 'Consider splitting it into multiple keys' : ''\n            }\n            data-testid=\"usedMemory-tooltip\"\n          >\n            <CellText\n              data-testid={`length-value-${name}${isHighlight ? '-highlighted' : ''}`}\n            >\n              {numberWithSpaces(value)}\n            </CellText>\n          </RiTooltip>\n        )\n      },\n    },\n  ]\n\n  return (\n    <div data-testid={dataTestid}>\n      <Table\n        columns={columns}\n        data={data}\n        defaultSorting={[\n          {\n            id: defaultSortField,\n            desc: true,\n          },\n        ]}\n      />\n    </div>\n  )\n}\n\nexport default TopKeysTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\nimport { resetBrowserTree } from 'uiSrc/slices/app/context'\nimport { changeKeyViewType } from 'uiSrc/slices/browser/keys'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { DatabaseAnalysisFactory } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory'\n\nimport TopNamespace, { Props } from './TopNamespace'\n\nconst mockedProps = mock<Props>()\n\nconst mockNspData = {\n  nsp: 'nsp_name' as any,\n  memory: 1,\n  keys: 1,\n  types: [{ type: 'hash', memory: 1, keys: 1 }],\n}\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('TopNamespace', () => {\n  it('should render', () => {\n    expect(render(<TopNamespace {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should render nsp-table-keys when click \"btn-change-table-keys\" ', () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [mockNspData],\n      topMemoryNsp: [mockNspData],\n    })\n\n    const { queryByTestId } = render(\n      <TopNamespace {...instance(mockedProps)} data={mockedData} />,\n    )\n\n    fireEvent.click(screen.getByTestId('btn-change-table-keys'))\n\n    expect(queryByTestId('nsp-table-memory')).not.toBeInTheDocument()\n    expect(queryByTestId('nsp-table-keys')).toBeInTheDocument()\n    expect(queryByTestId('btn-change-table-memory')).not.toBeDisabled()\n    expect(queryByTestId('btn-change-table-keys')).toBeDisabled()\n  })\n\n  it('should render nsp-table-keys when click \"btn-change-table-memory\" and memory button should be disabled', () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [mockNspData],\n      topMemoryNsp: [mockNspData],\n    })\n\n    const { queryByTestId } = render(\n      <TopNamespace {...instance(mockedProps)} data={mockedData} />,\n    )\n\n    // memory button is disabled by default\n    fireEvent.click(screen.getByTestId('btn-change-table-keys'))\n    fireEvent.click(screen.getByTestId('btn-change-table-memory'))\n\n    expect(queryByTestId('nsp-table-memory')).toBeInTheDocument()\n    expect(queryByTestId('nsp-table-keys')).not.toBeInTheDocument()\n    expect(queryByTestId('btn-change-table-memory')).toBeDisabled()\n    expect(queryByTestId('btn-change-table-keys')).not.toBeDisabled()\n  })\n\n  it('should render nsp-table-keys by default\" ', () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [mockNspData],\n      topMemoryNsp: [mockNspData],\n    })\n\n    const { queryByTestId } = render(\n      <TopNamespace {...instance(mockedProps)} data={mockedData} />,\n    )\n\n    expect(queryByTestId('nsp-table-memory')).toBeInTheDocument()\n    expect(queryByTestId('nsp-table-keys')).not.toBeInTheDocument()\n    expect(queryByTestId('btn-change-table-memory')).toBeDisabled()\n    expect(queryByTestId('btn-change-table-keys')).not.toBeDisabled()\n  })\n\n  it('should not render tables when topMemoryNsp and topKeysNsp are empty array', () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [],\n      topMemoryNsp: [],\n    })\n    const { queryByTestId } = render(\n      <TopNamespace {...instance(mockedProps)} data={mockedData} />,\n    )\n\n    expect(queryByTestId('nsp-table-memory')).not.toBeInTheDocument()\n    expect(queryByTestId('nsp-table-keys')).not.toBeInTheDocument()\n  })\n\n  it('should render loader when loading=\"true\"', () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [mockNspData],\n      topMemoryNsp: [mockNspData],\n    })\n    const { queryByTestId } = render(\n      <TopNamespace {...instance(mockedProps)} loading data={mockedData} />,\n    )\n\n    expect(queryByTestId('nsp-table-memory')).not.toBeInTheDocument()\n    expect(queryByTestId('nsp-table-keys')).not.toBeInTheDocument()\n    expect(queryByTestId('table-loader')).toBeInTheDocument()\n  })\n\n  it('should render message when no namespaces', () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [],\n      topMemoryNsp: [],\n    })\n    render(<TopNamespace {...instance(mockedProps)} data={mockedData} />)\n\n    expect(screen.queryByTestId('top-namespaces-empty')).toBeInTheDocument()\n  })\n\n  it('should call proper actions and push history after click tree view link', async () => {\n    const mockedData = DatabaseAnalysisFactory.build({\n      topKeysNsp: [],\n      topMemoryNsp: [],\n    })\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<TopNamespace {...instance(mockedProps)} data={mockedData} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('tree-view-page-link'))\n    })\n\n    const expectedActions = [\n      resetBrowserTree(),\n      changeKeyViewType(KeyViewType.Tree),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('/instanceId/browser')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { buildDatabaseAnalysisWithNamespaces } from 'uiSrc/mocks/factories/database-analysis/DatabaseAnalysis.factory'\n\nimport TopNamespace from './TopNamespace'\nimport { DEFAULT_EXTRAPOLATION } from '../../constants'\n\nconst meta: Meta<typeof TopNamespace> = {\n  component: TopNamespace,\n  args: {\n    data: null,\n    loading: false,\n    extrapolation: DEFAULT_EXTRAPOLATION,\n    onSwitchExtrapolation: () => undefined,\n  },\n  decorators: [\n    (Story) => (\n      <div style={{ padding: '20px 100px', border: '1px solid #ccc' }}>\n        <h1>Top Namespace</h1>\n        <Story />\n      </div>\n    ),\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Loading: Story = {\n  args: {\n    loading: true,\n  },\n}\n\nexport const Default: Story = {\n  args: {\n    loading: false,\n    data: buildDatabaseAnalysisWithNamespaces(),\n    extrapolation: 50,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.styles.ts",
    "content": "import styled from 'styled-components'\nimport { sectionContent } from 'uiSrc/pages/database-analysis/components/styles'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { truncateText } from 'uiSrc/styles/mixins'\n\nexport const SectionContent = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  ${sectionContent}\n`\n\nexport const NoNamespaceMsg = styled(Col).attrs({\n  align: 'center',\n  justify: 'center',\n})`\n  margin: 80px auto 100px;\n`\n\nexport const NoNamespaceText = styled(Text).attrs({\n  size: 'M',\n})`\n  margin-top: 10px;\n`\n\nexport const NoNamespaceBtn = styled(EmptyButton)`\n  text-decoration: underline;\n\n  &:hover,\n  &:focus {\n    text-decoration: none;\n  }\n`\n\nexport const ExpandedRowItem = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  display: flex;\n  height: 42px;\n  padding-left: ${({ theme }) => theme.core.space.space300};\n\n  & > div {\n    display: flex;\n    align-items: center;\n    flex: 1;\n  }\n`\n\nexport const TruncatedContent = styled(Row)`\n  ${truncateText}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.tsx",
    "content": "import { isNull } from 'lodash'\nimport React, { useEffect, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  DEFAULT_EXTRAPOLATION,\n  SectionName,\n  TableView,\n} from 'uiSrc/pages/database-analysis/constants'\nimport TableLoader from 'uiSrc/pages/database-analysis/components/table-loader'\nimport { resetBrowserTree } from 'uiSrc/slices/app/context'\nimport { changeKeyViewType } from 'uiSrc/slices/browser/keys'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\nimport { Nullable } from 'uiSrc/utils'\nimport { TextBtn } from 'uiSrc/pages/database-analysis/components/base/TextBtn'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models'\nimport TopNamespacesTable from './TopNamespacesTable'\nimport {\n  Section,\n  SectionTitle,\n  SectionTitleWrapper,\n} from 'uiSrc/pages/database-analysis/components/styles'\nimport {\n  NoNamespaceBtn,\n  NoNamespaceMsg,\n  NoNamespaceText,\n  SectionContent,\n} from './TopNamespace.styles'\n\nexport interface Props {\n  data: Nullable<DatabaseAnalysis>\n  loading: boolean\n  extrapolation: number\n  onSwitchExtrapolation?: (value: boolean, section: SectionName) => void\n}\n\nconst TopNamespace = (props: Props) => {\n  const { data, loading, extrapolation, onSwitchExtrapolation } = props\n  const [tableView, setTableView] = useState<TableView>(TableView.MEMORY)\n  const [isExtrapolated, setIsExtrapolated] = useState<boolean>(true)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    setIsExtrapolated(extrapolation !== DEFAULT_EXTRAPOLATION)\n  }, [data, extrapolation])\n\n  const handleTreeViewClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n    e.preventDefault()\n\n    dispatch(resetBrowserTree())\n    dispatch(changeKeyViewType(KeyViewType.Tree))\n    history.push(Pages.browser(instanceId))\n  }\n\n  if (loading) {\n    return <TableLoader />\n  }\n\n  if (isNull(data)) {\n    return null\n  }\n\n  if (!data?.topMemoryNsp || data?.totalKeys?.total === 0) {\n    return null\n  }\n\n  if (!data?.topMemoryNsp?.length && !data?.topKeysNsp?.length) {\n    return (\n      <Section data-testid=\"top-namespaces-empty\">\n        <SectionTitleWrapper>\n          <SectionTitle size=\"M\">TOP NAMESPACES</SectionTitle>\n        </SectionTitleWrapper>\n        <SectionContent data-testid=\"top-namespaces-message\">\n          <NoNamespaceMsg>\n            <Title size=\"L\">No namespaces to display</Title>\n            <NoNamespaceText>\n              {'Configure the delimiter in '}\n              <NoNamespaceBtn\n                data-testid=\"tree-view-page-link\"\n                onClick={handleTreeViewClick}\n              >\n                Tree View\n              </NoNamespaceBtn>\n              {' to customize the namespaces displayed.'}\n            </NoNamespaceText>\n          </NoNamespaceMsg>\n        </SectionContent>\n      </Section>\n    )\n  }\n\n  return (\n    <Section data-testid=\"top-namespaces\">\n      <SectionTitleWrapper gap=\"m\">\n        <SectionTitle size=\"M\">TOP NAMESPACES</SectionTitle>\n        <TextBtn\n          $active={tableView === TableView.MEMORY}\n          size=\"small\"\n          onClick={() => setTableView(TableView.MEMORY)}\n          disabled={tableView === TableView.MEMORY}\n          data-testid=\"btn-change-table-memory\"\n        >\n          by Memory\n        </TextBtn>\n        <TextBtn\n          $active={tableView === TableView.KEYS}\n          size=\"small\"\n          onClick={() => setTableView(TableView.KEYS)}\n          disabled={tableView === TableView.KEYS}\n          data-testid=\"btn-change-table-keys\"\n        >\n          by Number of Keys\n        </TextBtn>\n        {extrapolation !== DEFAULT_EXTRAPOLATION && (\n          <SwitchInput\n            color=\"subdued\"\n            className=\"switch-extrapolate-results\"\n            title=\"Extrapolate results\"\n            checked={isExtrapolated}\n            onCheckedChange={(checked) => {\n              setIsExtrapolated(checked)\n              onSwitchExtrapolation?.(checked, SectionName.TOP_NAMESPACES)\n            }}\n            data-testid=\"extrapolate-results\"\n          />\n        )}\n      </SectionTitleWrapper>\n      <SectionContent>\n        {tableView === TableView.MEMORY && (\n          <TopNamespacesTable\n            data={data?.topMemoryNsp ?? []}\n            defaultSortField=\"memory\"\n            delimiter={data?.delimiter ?? ''}\n            isExtrapolated={isExtrapolated}\n            extrapolation={extrapolation}\n            dataTestid=\"nsp-table-memory\"\n          />\n        )}\n        {tableView === TableView.KEYS && (\n          <TopNamespacesTable\n            data={data?.topKeysNsp ?? []}\n            defaultSortField=\"keys\"\n            delimiter={data?.delimiter ?? ''}\n            isExtrapolated={isExtrapolated}\n            extrapolation={extrapolation}\n            dataTestid=\"nsp-table-keys\"\n          />\n        )}\n      </SectionContent>\n    </Section>\n  )\n}\n\nexport default TopNamespace\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport Table, { Props } from './TopNamespacesTable'\n\nconst mockedProps = mock<Props>()\n\ndescribe('Top Namespaces Table', () => {\n  it('should render', () => {\n    expect(render(<Table {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespacesTable.tsx",
    "content": "import React from 'react'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  extrapolate,\n  formatBytes,\n  formatExtrapolation,\n  formatLongName,\n  Nullable,\n} from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { GroupBadge, RiTooltip } from 'uiSrc/components'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  changeSearchMode,\n  fetchKeys,\n  keysSelector,\n  resetKeysData,\n  setFilter,\n  setSearchMatch,\n} from 'uiSrc/slices/browser/keys'\nimport {\n  SCAN_COUNT_DEFAULT,\n  SCAN_TREE_COUNT_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport {\n  resetBrowserTree,\n  setBrowserKeyListDataLoaded,\n  setBrowserTreeDelimiter,\n} from 'uiSrc/slices/app/context'\nimport { TableTextBtn } from 'uiSrc/pages/database-analysis/components/base/TableTextBtn'\nimport { Table, ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { CellText } from 'uiSrc/components/auto-discover'\nimport { NspSummary } from 'apiSrc/modules/database-analysis/models'\n\nimport { ExpandedRowItem, TruncatedContent } from './TopNamespace.styles'\n\nexport interface Props {\n  data: Nullable<NspSummary[]>\n  defaultSortField: string\n  delimiter: string\n  isExtrapolated: boolean\n  extrapolation: number\n  dataTestid?: string\n}\n\nconst NameSpacesTable = ({\n  data = [],\n  defaultSortField,\n  delimiter,\n  isExtrapolated,\n  extrapolation,\n  dataTestid = '',\n}: Props) => {\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const { viewType } = useSelector(keysSelector)\n\n  const handleRedirect = (nsp: string, filter: string | null) => {\n    dispatch(changeSearchMode(SearchMode.Pattern))\n    dispatch(setBrowserTreeDelimiter([{ label: delimiter }]))\n    dispatch(setFilter(filter))\n    dispatch(setSearchMatch(`${nsp}${delimiter}*`, SearchMode.Pattern))\n    dispatch(resetKeysData(SearchMode.Pattern))\n    dispatch(\n      fetchKeys(\n        {\n          searchMode: SearchMode.Pattern,\n          cursor: '0',\n          count:\n            viewType === KeyViewType.Browser\n              ? SCAN_COUNT_DEFAULT\n              : SCAN_TREE_COUNT_DEFAULT,\n        },\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, true)),\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false)),\n      ),\n    )\n    dispatch(resetBrowserTree())\n    history.push(Pages.browser(instanceId))\n  }\n\n  const expandedRow = (item: NspSummary) => (\n    <div>\n      {item.types.map((type, index) => {\n        const extrapolated = extrapolate(type.memory, {\n          apply: isExtrapolated,\n          extrapolation,\n          showPrefix: false,\n        })\n        const [number, size] = formatBytes(extrapolated as number, 3, true)\n        const formatNumber = formatExtrapolation(number, isExtrapolated)\n\n        return (\n          <ExpandedRowItem\n            key={type.type}\n            data-testid={`expanded-${item.nsp}-${index}`}\n          >\n            <TruncatedContent align=\"center\">\n              <RiTooltip\n                title=\"Key Pattern\"\n                position=\"bottom\"\n                content={`${item.nsp}:*`}\n              >\n                <TableTextBtn\n                  $expanded\n                  onClick={() => handleRedirect(item.nsp as string, type.type)}\n                >\n                  {`${item.nsp}${delimiter}*`}\n                </TableTextBtn>\n              </RiTooltip>\n            </TruncatedContent>\n            <div>\n              <GroupBadge type={type.type} />\n            </div>\n            <div>\n              <CellText data-testid=\"usedMemory-value\">\n                {formatNumber} {size}\n              </CellText>\n            </div>\n            <div>\n              <CellText>\n                {extrapolate(\n                  type.keys,\n                  { extrapolation, apply: isExtrapolated },\n                  (val: number) => numberWithSpaces(Math.round(val)),\n                )}\n              </CellText>\n            </div>\n          </ExpandedRowItem>\n        )\n      })}\n    </div>\n  )\n\n  const columns: ColumnDef<NspSummary>[] = [\n    {\n      header: 'Key Pattern',\n      id: 'nsp',\n      accessorKey: 'nsp',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { nsp, types },\n        },\n      }) => {\n        const filterType = types.length > 1 ? null : types[0].type\n        const textWithDelimiter = `${nsp}${delimiter}*`\n        const cellContent = textWithDelimiter?.substring(0, 200)\n        const tooltipContent = formatLongName(textWithDelimiter)\n        return (\n          <RiTooltip\n            title=\"Key Pattern\"\n            position=\"bottom\"\n            content={tooltipContent}\n          >\n            <TableTextBtn\n              onClick={() => handleRedirect(nsp as string, filterType)}\n            >\n              {cellContent}\n            </TableTextBtn>\n          </RiTooltip>\n        )\n      },\n    },\n    {\n      header: 'Data Type',\n      id: 'types',\n      accessorKey: 'types',\n      cell: ({\n        row: {\n          original: { types: value },\n        },\n      }) => (\n        <div>\n          {value.map(({ type }) => (\n            <GroupBadge key={type} type={type} />\n          ))}\n        </div>\n      ),\n    },\n    {\n      header: 'Total Memory',\n      id: 'memory',\n      accessorKey: 'memory',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { memory: value },\n        },\n      }) => {\n        const extrapolated = extrapolate(value, {\n          apply: isExtrapolated,\n          extrapolation,\n          showPrefix: false,\n        }) as number\n        const [number, size] = formatBytes(extrapolated, 3, true)\n\n        const formatValue = formatExtrapolation(number, isExtrapolated)\n        const formatValueBytes = formatExtrapolation(\n          numberWithSpaces(Math.round(extrapolated)),\n          isExtrapolated,\n        )\n\n        return (\n          <RiTooltip\n            content={`${formatValueBytes} B`}\n            data-testid=\"usedMemory-tooltip\"\n          >\n            <CellText data-testid={`nsp-usedMemory-value=${value}`}>\n              {formatValue} {size}\n            </CellText>\n          </RiTooltip>\n        )\n      },\n    },\n    {\n      header: 'Total Keys',\n      id: 'keys',\n      accessorKey: 'keys',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { keys: value },\n        },\n      }) => (\n        <CellText data-testid={`keys-value-${value}`}>\n          {extrapolate(\n            value,\n            { extrapolation, apply: isExtrapolated },\n            (val: number) => numberWithSpaces(Math.round(val)),\n          )}\n        </CellText>\n      ),\n    },\n    {\n      id: 'expand',\n      header: () => null,\n      size: 40,\n      cell: ({ row }) => <Table.ExpandRowButton row={row} />,\n    },\n  ]\n\n  return (\n    <div data-testid={dataTestid}>\n      <Table\n        columns={columns}\n        data={data ?? []}\n        defaultSorting={[\n          {\n            id: defaultSortField,\n            desc: true,\n          },\n        ]}\n        stripedRows\n        expandRowOnClick\n        getIsRowExpandable={(row) => row.types.length > 1}\n        renderExpandedRow={({ original }) => expandedRow(original)}\n      />\n    </div>\n  )\n}\n\nexport default NameSpacesTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/components/top-namespace/styles.module.scss",
    "content": ":global(.section-content) {\n  .noNamespaceMsg {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-direction: column;\n    margin: 80px auto 100px;\n\n    .noNamespaceParagraph {\n      margin-top: 10px;\n\n      .displayInlineBlock {\n        display: inline;\n      }\n\n      .treeViewBtn {\n        text-decoration: underline;\n\n        &:hover, &:focus {\n          text-decoration: none;\n        }\n      }\n    }\n  }\n}\n\n.expanded {\n  display: flex;\n  height: 42px;\n\n  > div {\n    display: flex;\n    align-items: center;\n    flex: 1;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/constants.ts",
    "content": "import { ReactNode } from 'react'\n\nexport enum TableView {\n  MEMORY = 'memory',\n  KEYS = 'keys',\n}\n\nexport enum EmptyMessage {\n  Reports = 'reports',\n  Keys = 'keys',\n  Encrypt = 'encrypt',\n}\n\nexport type Content = {\n  title: string\n  text: (path: string) => ReactNode\n}\n\nexport const DEFAULT_EXTRAPOLATION = 1\n\nexport enum SectionName {\n  SUMMARY_PER_DATA = 'SUMMARY_PER_DATA',\n  MEMORY_LIKELY_TO_BE_FREED = 'MEMORY_LIKELY_TO_BE_FREED',\n  TOP_NAMESPACES = 'TOP_NAMESPACES',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/index.ts",
    "content": "import DatabaseAnalysisPage from './DatabaseAnalysisPage'\n\nexport * from './constants'\n\nexport default DatabaseAnalysisPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/database-analysis/styles.module.scss",
    "content": ".main {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.main :global {\n  .section {\n    background-color: var(--euiColorLightestShade);\n    border-radius: 16px;\n    margin-top: 20px;\n    overflow: hidden;\n    padding: 30px 40px;\n\n    @media screen and (max-width: 920px) {\n      padding: 18px 20px;\n    }\n  }\n\n  .section-content {\n    max-width: 1720px;\n    margin: 0 auto;\n  }\n\n  .section-title-wrapper {\n    display: flex;\n    align-items: center;\n    margin-bottom: 20px;\n\n    .switch-extrapolate-results {\n      margin-left: 30px;\n    }\n  }\n\n  .section-title {\n    display: inline-block;\n    font-size: 16px;\n    line-height: 24px;\n  }\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/HomePage.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport HomePage from './HomePage'\n\njest.mock('uiSrc/slices/panels/sidePanels', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/sidePanels'),\n  sidePanelsSelector: jest.fn().mockReturnValue({\n    openedPanel: 'insights',\n  }),\n}))\n\njest.mock('uiSrc/slices/content/create-redis-buttons', () => ({\n  ...jest.requireActual('uiSrc/slices/content/create-redis-buttons'),\n  contentSelector: jest.fn().mockReturnValue({\n    data: {\n      cloud_list_of_databases: {},\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    enhancedCloudUI: {\n      flag: false,\n    },\n  }),\n}))\n\n/**\n * HomePage tests\n *\n * @group component\n */\ndescribe('HomePage', () => {\n  it('should render', async () => {\n    expect(await render(<HomePage />)).toBeTruthy()\n  })\n\n  it('should render insights trigger', async () => {\n    await render(<HomePage />)\n\n    expect(screen.getByTestId('insights-trigger')).toBeInTheDocument()\n  })\n\n  it('should render side panel', async () => {\n    await render(<HomePage />)\n\n    expect(screen.getByTestId('side-panels-insights')).toBeInTheDocument()\n  })\n\n  it('should not render free cloud db button with enhanced cloud ui feature flag disabled', async () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      enhancedCloudUI: {\n        flag: false,\n      },\n      cloudAds: {\n        flag: true,\n      },\n    })\n    await render(<HomePage />)\n\n    expect(\n      screen.queryByTestId('create-free-cloud-db-button'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should not render free cloud db button with cloud ads feature flag disabled', async () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      enhancedCloudUI: {\n        flag: true,\n      },\n      cloudAds: {\n        flag: false,\n      },\n    })\n    await render(<HomePage />)\n\n    expect(\n      screen.queryByTestId('create-free-cloud-db-button'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render free cloud db button with feature flags enabled', async () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      enhancedCloudUI: {\n        flag: true,\n      },\n      cloudAds: {\n        flag: true,\n      },\n    })\n    await render(<HomePage />)\n\n    expect(\n      screen.getByTestId('create-free-cloud-db-button'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/HomePage.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  clusterSelector,\n  resetDataRedisCluster,\n  resetInstancesRedisCluster,\n} from 'uiSrc/slices/instances/cluster'\nimport { setTitle } from 'uiSrc/utils'\nimport { HomePageTemplate } from 'uiSrc/templates'\nimport { resetKeys } from 'uiSrc/slices/browser/keys'\nimport {\n  resetCliHelperSettings,\n  resetCliSettingsAction,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { resetRedisearchKeysData } from 'uiSrc/slices/browser/redisearch'\nimport {\n  appContextSelector,\n  setAppContextInitialState,\n} from 'uiSrc/slices/app/context'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport {\n  cloudSelector,\n  resetDataRedisCloud,\n  resetSubscriptionsRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport {\n  editedInstanceSelector,\n  fetchEditedInstanceAction,\n  fetchInstancesAction,\n  instancesSelector,\n  resetImportInstances,\n  setEditedInstance,\n} from 'uiSrc/slices/instances/instances'\nimport { fetchTags } from 'uiSrc/slices/instances/tags'\nimport {\n  resetDataSentinel,\n  sentinelSelector,\n} from 'uiSrc/slices/instances/sentinel'\nimport { fetchContentAction as fetchCreateRedisButtonsAction } from 'uiSrc/slices/content/create-redis-buttons'\nimport {\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n  TelemetryEvent,\n  TelemetryPageView,\n} from 'uiSrc/telemetry'\nimport {\n  appRedirectionSelector,\n  setUrlHandlingInitialState,\n} from 'uiSrc/slices/app/url-handling'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\n\nimport { Page, PageBody } from 'uiSrc/components/base/layout/page'\nimport { Card } from 'uiSrc/components/base/layout'\nimport DatabasesList from './components/databases-list/DatabasesList'\nimport DatabaseListHeader from './components/database-list-header'\nimport EmptyMessage from './components/empty-message/EmptyMessage'\nimport DatabasePanelDialog from './components/database-panel-dialog'\nimport { ManageTagsModal } from './components/database-manage-tags-modal/ManageTagsModal'\nimport {\n  HomePageDataProviderProvider,\n  useHomePageDataProvider,\n} from './contexts/HomePageDataProvider'\n\nimport './styles.scss'\nimport styles from './styles.module.scss'\n\nenum OpenDialogName {\n  AddDatabase = 'add',\n  ManageTags = 'manage-tags',\n  EditDatabase = 'edit',\n}\n\nconst HomePage = () => {\n  const { openDialog, setOpenDialog } = useHomePageDataProvider()\n\n  const dispatch = useDispatch()\n\n  const { credentials: clusterCredentials } = useSelector(clusterSelector)\n  const { credentials: cloudCredentials } = useSelector(cloudSelector)\n  const { instance: sentinelInstance } = useSelector(sentinelSelector)\n  const { action, dbConnection } = useSelector(appRedirectionSelector)\n\n  const {\n    loading,\n    loadingChanging,\n    data: instances,\n    changedSuccessfully: isChangedInstance,\n    deletedSuccessfully: isDeletedInstance,\n  } = useSelector(instancesSelector)\n\n  const { data: editedInstance } = useSelector(editedInstanceSelector)\n\n  const { contextInstanceId } = useSelector(appContextSelector)\n\n  const hideDbList = instances.length === 0 && !loading && !loadingChanging\n\n  useEffect(() => {\n    setTitle('Redis databases')\n\n    dispatch(fetchInstancesAction(handleOpenPage))\n    dispatch(resetInstancesRedisCluster())\n    dispatch(resetSubscriptionsRedisCloud())\n    dispatch(fetchCreateRedisButtonsAction())\n    dispatch(fetchTags())\n\n    return () => {\n      dispatch(setEditedInstance(null))\n    }\n  }, [])\n\n  useEffect(() => {\n    if (isDeletedInstance) {\n      dispatch(fetchInstancesAction())\n    }\n  }, [isDeletedInstance])\n\n  useEffect(() => {\n    if (isChangedInstance) {\n      setOpenDialog(null)\n      dispatch(setEditedInstance(null))\n    }\n  }, [isChangedInstance])\n\n  useEffect(() => {\n    if (clusterCredentials || cloudCredentials || sentinelInstance) {\n      setOpenDialog(OpenDialogName.AddDatabase)\n    }\n  }, [clusterCredentials, cloudCredentials, sentinelInstance])\n\n  useEffect(() => {\n    if (action === UrlHandlingActions.Connect) {\n      setOpenDialog(OpenDialogName.AddDatabase)\n    }\n  }, [action, dbConnection])\n\n  useEffect(() => {\n    if (editedInstance) {\n      const found = instances.find(\n        (item: Instance) => item.id === editedInstance.id,\n      )\n      if (found) {\n        dispatch(fetchEditedInstanceAction(found))\n      }\n    }\n  }, [instances])\n\n  const handleOpenPage = (instances: Instance[]) => {\n    const instancesWithTagsCount = instances.filter(\n      (instance) => instance.tags && instance.tags.length > 0,\n    ).length\n\n    sendPageViewTelemetry({\n      name: TelemetryPageView.DATABASES_LIST_PAGE,\n      eventData: {\n        instancesCount: instances.length,\n        instancesWithTagsCount,\n      },\n    })\n  }\n\n  const onDbEdited = () => {\n    if (contextInstanceId && contextInstanceId === editedInstance?.id) {\n      dispatch(resetKeys())\n      dispatch(resetRedisearchKeysData())\n      dispatch(resetCliSettingsAction())\n      dispatch(resetCliHelperSettings())\n      dispatch(setAppContextInitialState())\n    }\n  }\n\n  const closeEditDialog = () => {\n    dispatch(setEditedInstance(null))\n    setOpenDialog(null)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED,\n      eventData: {\n        databaseId: editedInstance?.id,\n      },\n    })\n  }\n\n  const handleClose = () => {\n    dispatch(resetDataRedisCluster())\n    dispatch(resetDataSentinel())\n    dispatch(resetImportInstances())\n    dispatch(resetDataRedisCloud())\n\n    setOpenDialog(null)\n    dispatch(setEditedInstance(null))\n\n    if (action === UrlHandlingActions.Connect) {\n      dispatch(setUrlHandlingInitialState())\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_ADD_FORM_DISMISSED,\n    })\n  }\n\n  const handleAddInstance = () => {\n    setOpenDialog(OpenDialogName.AddDatabase)\n    dispatch(setEditedInstance(null))\n  }\n\n  return (\n    <HomePageTemplate>\n      <div className={styles.pageWrapper}>\n        <Page className={styles.page}>\n          <PageBody component=\"div\">\n            <DatabaseListHeader\n              key=\"instance-controls\"\n              onAddInstance={handleAddInstance}\n            />\n            {openDialog && openDialog !== OpenDialogName.ManageTags && (\n              <DatabasePanelDialog\n                editMode={openDialog === OpenDialogName.EditDatabase}\n                urlHandlingAction={action}\n                editedInstance={\n                  openDialog === OpenDialogName.EditDatabase\n                    ? editedInstance\n                    : (sentinelInstance ?? null)\n                }\n                onClose={\n                  openDialog === OpenDialogName.EditDatabase\n                    ? closeEditDialog\n                    : handleClose\n                }\n                onDbEdited={onDbEdited}\n              />\n            )}\n            {openDialog === OpenDialogName.ManageTags && editedInstance && (\n              <ManageTagsModal\n                instance={editedInstance}\n                onClose={handleClose}\n              />\n            )}\n            <div key=\"homePage\" className=\"homePage\">\n              {hideDbList && (\n                <Card>\n                  <EmptyMessage onAddInstanceClick={handleAddInstance} />\n                </Card>\n              )}\n              {!hideDbList && <DatabasesList />}\n            </div>\n          </PageBody>\n        </Page>\n      </div>\n    </HomePageTemplate>\n  )\n}\n\nconst HomePageWithProvider = () => (\n  <HomePageDataProviderProvider>\n    <HomePage />\n  </HomePageDataProviderProvider>\n)\n\nexport default HomePageWithProvider\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/ManualConnection.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const FixedWrapper = styled.div`\n  height: 100%;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n`\n\nexport const ScrollableWrapper = styled.div<\n  React.HTMLAttributes<HTMLDivElement>\n>`\n  flex: 1;\n  overflow-y: auto;\n  overflow-x: hidden;\n  scrollbar-width: thin;\n  min-height: 0;\n`\n\nexport const ContentWrapper = styled.div<React.HTMLAttributes<HTMLDivElement>>`\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/AddDatabaseScreen.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  act,\n  expectActionsToContain,\n} from 'uiSrc/utils/test-utils'\n\nimport { defaultInstanceChanging } from 'uiSrc/slices/instances/instances'\nimport { AddDbType } from 'uiSrc/pages/home/constants'\nimport AddDatabaseScreen, { Props } from './AddDatabaseScreen'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('AddDatabaseScreen', () => {\n  it('should render', () => {\n    expect(render(<AddDatabaseScreen {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call proper actions with empty connection url', async () => {\n    render(<AddDatabaseScreen {...mockedProps} />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expectActionsToContain(store.getActions(), [defaultInstanceChanging()])\n  })\n\n  it('should disable test connection and submit buttons when connection url is invalid', async () => {\n    render(<AddDatabaseScreen {...mockedProps} />)\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('connection-url'), {\n        target: { value: 'q' },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).toBeDisabled()\n    expect(screen.getByTestId('btn-test-connection')).toBeDisabled()\n  })\n\n  it('should not disable buttons with proper connection url', async () => {\n    render(<AddDatabaseScreen {...mockedProps} />)\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('connection-url'), {\n        target: { value: 'redis://localhost:6322' },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n    expect(screen.getByTestId('btn-test-connection')).not.toBeDisabled()\n  })\n\n  it('should call proper actions after click manual settings', async () => {\n    const onSelectOptionMock = jest.fn()\n    render(\n      <AddDatabaseScreen\n        {...mockedProps}\n        onSelectOption={onSelectOptionMock}\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('connection-url'), {\n        target: { value: 'redis://localhost:6322' },\n      })\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('btn-connection-settings'))\n    })\n\n    expect(onSelectOptionMock).toBeCalledWith(AddDbType.manual, {\n      db: undefined,\n      host: 'localhost',\n      name: 'localhost:6322',\n      password: undefined,\n      port: 6322,\n      timeout: 30000,\n      tls: false,\n      username: 'default',\n    })\n  })\n\n  it('should call proper actions after click connectivity option', async () => {\n    const onSelectOptionMock = jest.fn()\n    render(\n      <AddDatabaseScreen\n        {...mockedProps}\n        onSelectOption={onSelectOptionMock}\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('option-btn-sentinel'))\n    })\n\n    expect(onSelectOptionMock).toBeCalledWith(\n      AddDbType.sentinel,\n      expect.any(Object),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/AddDatabaseScreen.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const CustomHorizontalRule = styled.div`\n  margin: 12px 0;\n  width: 100%;\n  text-align: center;\n  position: relative;\n\n  &:before,\n  &:after {\n    content: '';\n    display: block;\n    width: 47%;\n    height: 1px;\n    background: ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.background.neutral500};\n    position: absolute;\n    top: 50%;\n  }\n\n  &:after {\n    right: 0;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/AddDatabaseScreen.tsx",
    "content": "import React, { useState } from 'react'\nimport { useFormik } from 'formik'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router'\nimport { toNumber } from 'lodash'\n\nimport { Nullable, parseRedisUrl } from 'uiSrc/utils'\nimport { AddDbType, DEFAULT_TIMEOUT } from 'uiSrc/pages/home/constants'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport {\n  createInstanceStandaloneAction,\n  instancesSelector,\n  testInstanceStandaloneAction,\n} from 'uiSrc/slices/instances/instances'\nimport { Pages } from 'uiSrc/constants'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  EmptyButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\nimport ConnectivityOptions from './components/connectivity-options'\nimport ConnectionUrl from './components/connection-url'\nimport { Values } from './constants'\nimport { CustomHorizontalRule } from './AddDatabaseScreen.styles'\n\nimport { ScrollableWrapper } from '../ManualConnection.styles'\n\nexport interface Props {\n  onSelectOption: (type: AddDbType, db: Nullable<Record<string, any>>) => void\n  onClose?: () => void\n}\n\nconst getPayload = (connectionUrl: string, returnOnError = false) => {\n  const details = parseRedisUrl(connectionUrl.trim())\n\n  if (!details && returnOnError) return null\n\n  return {\n    name: details?.hostname || '127.0.0.1:6379',\n    host: details?.host || '127.0.0.1',\n    port: details?.port || 6379,\n    username: details?.username || 'default',\n    password: details?.password || undefined,\n    timeout: toNumber(DEFAULT_TIMEOUT),\n    tls: details?.protocol === 'rediss',\n    db: details?.dbNumber,\n  }\n}\n\nconst ConnectionUrlError = (\n  <>\n    The connection URL format provided is not supported.\n    <br />\n    Try adding a database using a connection form.\n  </>\n)\n\nconst AddDatabaseScreen = (props: Props) => {\n  const { onSelectOption, onClose } = props\n  const [isInvalid, setIsInvalid] = useState<Boolean>(false)\n  const { loadingChanging: isLoading } = useSelector(instancesSelector)\n\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const validate = (values: Values) => {\n    const payload = getPayload(values.connectionURL, true)\n    setIsInvalid(!payload && !!values.connectionURL)\n  }\n\n  const handleTestConnection = () => {\n    const payload = getPayload(formik.values.connectionURL)\n    dispatch(testInstanceStandaloneAction(payload as Instance))\n  }\n\n  const handleProceedForm = (type: AddDbType) => {\n    const details = getPayload(formik.values.connectionURL)\n    onSelectOption(type, details)\n  }\n\n  const onSubmit = () => {\n    if (isInvalid) return\n\n    const payload = getPayload(formik.values.connectionURL)\n    dispatch(\n      createInstanceStandaloneAction(payload as Instance, () => {\n        history.push(Pages.sentinelDatabases)\n      }),\n    )\n  }\n\n  const formik = useFormik<Values>({\n    initialValues: {\n      connectionURL: 'redis://default@127.0.0.1:6379',\n    },\n    validate,\n    enableReinitialize: true,\n    validateOnMount: true,\n    onSubmit,\n  })\n\n  return (\n    <ScrollableWrapper>\n      <form onSubmit={formik.handleSubmit} data-testid=\"form\">\n        <Row responsive>\n          <FlexItem grow>\n            <ConnectionUrl\n              value={formik.values.connectionURL}\n              onChange={formik.handleChange}\n            />\n          </FlexItem>\n        </Row>\n        <Spacer size=\"xxl\" />\n        <Row responsive justify=\"between\" align=\"center\">\n          <FlexItem>\n            <RiTooltip\n              position=\"top\"\n              anchorClassName=\"euiToolTip__btn-disabled\"\n              content={isInvalid ? <span>{ConnectionUrlError}</span> : null}\n            >\n              <EmptyButton\n                size=\"small\"\n                className=\"empty-btn\"\n                disabled={!!isInvalid}\n                icon={isInvalid ? InfoIcon : undefined}\n                onClick={handleTestConnection}\n                loading={isLoading}\n                data-testid=\"btn-test-connection\"\n              >\n                Test connection\n              </EmptyButton>\n            </RiTooltip>\n          </FlexItem>\n          <FlexItem>\n            <Row responsive gap=\"l\">\n              <FlexItem>\n                <SecondaryButton\n                  onClick={() => handleProceedForm(AddDbType.manual)}\n                  data-testid=\"btn-connection-settings\"\n                >\n                  Connection settings\n                </SecondaryButton>\n              </FlexItem>\n              <FlexItem>\n                <RiTooltip\n                  position=\"top\"\n                  anchorClassName=\"euiToolTip__btn-disabled\"\n                  content={isInvalid ? <span>{ConnectionUrlError}</span> : null}\n                >\n                  <PrimaryButton\n                    type=\"submit\"\n                    disabled={!!isInvalid}\n                    icon={isInvalid ? InfoIcon : undefined}\n                    data-testid=\"btn-submit\"\n                  >\n                    Add database\n                  </PrimaryButton>\n                </RiTooltip>\n              </FlexItem>\n            </Row>\n          </FlexItem>\n        </Row>\n      </form>\n      <Spacer />\n      <CustomHorizontalRule>Or</CustomHorizontalRule>\n      <Spacer />\n      <ConnectivityOptions\n        onClickOption={handleProceedForm}\n        onClose={onClose}\n      />\n    </ScrollableWrapper>\n  )\n}\n\nexport default AddDatabaseScreen\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connection-url/ConnectionUrl.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport ConnectionUrl from './ConnectionUrl'\n\ndescribe('ConnectionUrl', () => {\n  it('should render', () => {\n    expect(render(<ConnectionUrl value=\"\" onChange={() => {}} />)).toBeTruthy()\n  })\n\n  it('should change connection url', () => {\n    const onChangeMock = jest.fn()\n    render(<ConnectionUrl value=\"val\" onChange={onChangeMock} />)\n\n    expect(screen.getByTestId('connection-url')).toHaveValue('val')\n\n    fireEvent.change(screen.getByTestId('connection-url'), {\n      target: { value: 'val1' },\n    })\n\n    expect(onChangeMock).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connection-url/ConnectionUrl.tsx",
    "content": "import React from 'react'\n\nimport {\n  FormField,\n  RiInfoIconProps,\n} from 'uiSrc/components/base/forms/FormField'\nimport { TextArea } from 'uiSrc/components/base/inputs'\nimport { Text } from 'uiSrc/components/base/text'\nimport { HostInfoTooltipContent } from '../../../host-info-tooltip-content/HostInfoTooltipContent'\n\nexport interface Props {\n  value: string\n  onChange: (e: React.ChangeEvent<any>) => void\n}\n\nconst connectionUrlInfo: RiInfoIconProps = {\n  content: HostInfoTooltipContent({ includeAutofillInfo: false }),\n  placement: 'right',\n  maxWidth: '100%',\n}\n\nconst ConnectionUrl = ({ value, onChange }: Props) => (\n  <FormField\n    label={<Text>Connection URL</Text>}\n    infoIconProps={connectionUrlInfo}\n  >\n    <TextArea\n      name=\"connectionURL\"\n      id=\"connectionURL\"\n      value={value}\n      onChangeCapture={onChange}\n      placeholder=\"redis://default@127.0.0.1:6379\"\n      data-testid=\"connection-url\"\n    />\n  </FormField>\n)\n\nexport default ConnectionUrl\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connection-url/index.ts",
    "content": "import ConnectionUrl from './ConnectionUrl'\n\nexport default ConnectionUrl\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { AddDbType } from 'uiSrc/pages/home/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport ConnectivityOptions, { Props } from './ConnectivityOptions'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: false,\n    },\n    cloudAds: {\n      flag: true,\n    },\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ConnectivityOptions', () => {\n  it('should render', () => {\n    expect(render(<ConnectivityOptions {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render all additional options', () => {\n    const onClickOption = jest.fn()\n    render(\n      <ConnectivityOptions {...mockedProps} onClickOption={onClickOption} />,\n    )\n\n    fireEvent.click(screen.getByTestId('option-btn-sentinel'))\n    expect(onClickOption).toBeCalledWith(AddDbType.sentinel)\n\n    fireEvent.click(screen.getByTestId('option-btn-software'))\n    expect(onClickOption).toBeCalledWith(AddDbType.software)\n\n    fireEvent.click(screen.getByTestId('option-btn-import'))\n    expect(onClickOption).toBeCalledWith(AddDbType.import)\n\n    fireEvent.click(screen.getByTestId('discover-cloud-btn'))\n    expect(onClickOption).toBeCalledWith(AddDbType.cloud)\n  })\n\n  it('should not call any actions after click on create cloud btn', () => {\n    render(<ConnectivityOptions {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId('create-free-db-btn'))\n\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should call proper actions after click on create cloud btn', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      cloudSso: {\n        flag: true,\n      },\n      cloudAds: {\n        flag: true,\n      },\n    })\n\n    const onClose = jest.fn()\n    render(<ConnectivityOptions {...mockedProps} onClose={onClose} />)\n\n    fireEvent.click(screen.getByTestId('create-free-db-btn'))\n\n    expect(store.getActions()).toEqual([\n      setSSOFlow(OAuthSocialAction.Create),\n      setSocialDialogState(OAuthSocialSource.AddDbForm),\n    ])\n    expect(onClose).toBeCalled()\n  })\n\n  it('should not should create free db button if cloud ads feature flag is disabled', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      cloudSso: {\n        flag: true,\n      },\n      cloudAds: {\n        flag: false,\n      },\n    })\n\n    const onClose = jest.fn()\n    render(<ConnectivityOptions {...mockedProps} onClose={onClose} />)\n\n    expect(screen.queryByTestId('create-free-db-btn')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.styles.ts",
    "content": "import styled from 'styled-components'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nexport const StyledConnectivityLink = styled(Link)`\n  min-width: 150px;\n  padding: 10px;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  border-radius: 5px;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral700};\n  color: ${({ theme }) => theme.semantic.color.text.neutral800};\n\n  // adding position relative to allow possible elements\n  // inside the link to be positioned absolutely\n  position: relative;\n\n  &:hover {\n    text-decoration: none !important;\n    background: none;\n    color: ${({ theme }) => theme.semantic.color.text.neutral800};\n    opacity: 0.8;\n    transform: translateY(-1px);\n    transition: transform 0.2s ease-in-out;\n    box-shadow: none;\n  }\n`\n\nexport const StyledBadge = styled(RiBadge)`\n  position: absolute;\n  top: 10px;\n  left: 10px;\n`\n\n// This style is needed as the max built in size of the icon is not sufficient in this case\nexport const StyledIcon = styled(RiIcon)`\n  width: 30px;\n  height: 30px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/ConnectivityOptions.tsx",
    "content": "import React from 'react'\n\nimport { AddDbType } from 'uiSrc/pages/home/constants'\nimport { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { Col, FlexItem, Grid, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text } from 'uiSrc/components/base/text/Text'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { useConnectivityOptions } from '../../hooks/useConnectivityOptions'\n\nimport {\n  StyledBadge,\n  StyledConnectivityLink,\n  StyledIcon,\n} from './ConnectivityOptions.styles'\n\nexport interface Props {\n  onClickOption: (type: AddDbType) => void\n  onClose?: () => void\n}\n\nconst ConnectivityOptions = (props: Props) => {\n  const { onClickOption, onClose } = props\n  const connectivityOptions = useConnectivityOptions({ onClickOption })\n\n  return (\n    <>\n      <section>\n        <Text color=\"primary\">Get started with Redis Cloud account</Text>\n        <Spacer />\n        <Grid gap=\"l\" columns={3} responsive>\n          <FlexItem>\n            <StyledConnectivityLink\n              onClick={() => onClickOption(AddDbType.cloud)}\n              data-testid=\"discover-cloud-btn\"\n            >\n              <Col align=\"center\" gap=\"s\">\n                <StyledIcon type=\"CloudIcon\" size=\"xl\" />\n                <Text color=\"primary\">Add databases</Text>\n              </Col>\n            </StyledConnectivityLink>\n          </FlexItem>\n          <FeatureFlagComponent name={FeatureFlags.cloudAds}>\n            <FlexItem>\n              <OAuthSsoHandlerDialog>\n                {(ssoCloudHandlerClick, isSSOEnabled) => (\n                  <StyledConnectivityLink\n                    data-testid=\"create-free-db-btn\"\n                    color=\"primary\"\n                    onClick={(e: React.MouseEvent) => {\n                      ssoCloudHandlerClick(e, {\n                        source: OAuthSocialSource.AddDbForm,\n                        action: OAuthSocialAction.Create,\n                      })\n                      isSSOEnabled && onClose?.()\n                    }}\n                    href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n                      campaign: UTM_CAMPAINGS[OAuthSocialSource.AddDbForm],\n                    })}\n                    target=\"_blank\"\n                  >\n                    <StyledBadge label=\"FREE\" variant=\"notice\" />\n                    <Col align=\"center\" gap=\"s\">\n                      <StyledIcon type=\"RocketIcon\" size=\"xl\" />\n                      <Text color=\"primary\">New database</Text>\n                    </Col>\n                  </StyledConnectivityLink>\n                )}\n              </OAuthSsoHandlerDialog>\n            </FlexItem>\n          </FeatureFlagComponent>\n        </Grid>\n      </section>\n      <Spacer size=\"xxl\" />\n      <section>\n        <Text color=\"primary\">More connectivity options</Text>\n        <Spacer />\n        <Grid gap=\"l\" responsive columns={4}>\n          {connectivityOptions.map((option) => (\n            <FlexItem key={option.id}>\n              <StyledConnectivityLink\n                onClick={() => !option.loading && option.onClick()}\n                data-testid={`option-btn-${option.id}`}\n              >\n                <Row gap=\"s\" align=\"center\" justify=\"center\">\n                  {option.loading ? (\n                    <Loader size=\"xl\" />\n                  ) : (\n                    <RiIcon type={option.icon} size=\"xl\" />\n                  )}\n                  <Text color=\"primary\">{option.title}</Text>\n                </Row>\n              </StyledConnectivityLink>\n            </FlexItem>\n          ))}\n        </Grid>\n      </section>\n    </>\n  )\n}\n\nexport default ConnectivityOptions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/components/connectivity-options/index.ts",
    "content": "import ConnectivityOptions from './ConnectivityOptions'\n\nexport default ConnectivityOptions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/constants.tsx",
    "content": "import { AllIconsType } from 'uiSrc/components/base/icons'\nimport { AddDbType } from 'uiSrc/pages/home/constants'\n\nexport interface Values {\n  connectionURL: string\n}\n\nexport interface ConnectivityOptionConfig {\n  id: string\n  title: string\n  type: AddDbType\n  icon: AllIconsType\n}\n\nexport interface ConnectivityOption extends ConnectivityOptionConfig {\n  onClick: () => void\n  loading?: boolean\n}\n\nexport const CONNECTIVITY_OPTIONS_CONFIG: ConnectivityOptionConfig[] = [\n  {\n    id: 'sentinel',\n    title: 'Redis Sentinel',\n    type: AddDbType.sentinel,\n    icon: 'ShieldIcon',\n  },\n  {\n    id: 'software',\n    title: 'Redis Software',\n    type: AddDbType.software,\n    icon: 'RedisSoftwareIcon',\n  },\n  {\n    id: 'azure',\n    title: 'Azure Managed Redis',\n    type: AddDbType.azure,\n    icon: 'CloudIcon',\n  },\n  {\n    id: 'import',\n    title: 'Import from file',\n    type: AddDbType.import,\n    icon: 'DownloadIcon',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/hooks/useConnectivityOptions.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\n\nimport { cleanup, mockedStore, renderHook } from 'uiSrc/utils/test-utils'\nimport { isAzureEntraIdEnabledSelector } from 'uiSrc/slices/app/features'\nimport { useAzureAuth } from 'uiSrc/components/hooks/useAzureAuth'\nimport { AddDbType } from 'uiSrc/pages/home/constants'\nimport { Pages } from 'uiSrc/constants'\n\nimport { useConnectivityOptions } from './useConnectivityOptions'\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  isAzureEntraIdEnabledSelector: jest.fn().mockReturnValue(false),\n}))\n\njest.mock('uiSrc/components/hooks/useAzureAuth', () => ({\n  useAzureAuth: jest.fn().mockReturnValue({\n    initiateLogin: jest.fn(),\n    loading: false,\n    account: null,\n  }),\n}))\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  jest.clearAllMocks()\n})\n\nconst mockedIsAzureEntraIdEnabledSelector =\n  isAzureEntraIdEnabledSelector as unknown as jest.Mock\nconst mockedUseAzureAuth = useAzureAuth as jest.Mock\n\ndescribe('useConnectivityOptions', () => {\n  const mockOnClickOption = jest.fn()\n  const mockInitiateLogin = jest.fn()\n\n  beforeEach(() => {\n    mockedUseAzureAuth.mockReturnValue({\n      initiateLogin: mockInitiateLogin,\n      loading: false,\n      account: null,\n    })\n  })\n\n  it('should return options without Azure when Azure Entra ID is disabled', () => {\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(false)\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const options = result.current\n    const azureOption = options.find((opt) => opt.type === AddDbType.azure)\n\n    expect(azureOption).toBeUndefined()\n  })\n\n  it('should return options with Azure when Azure Entra ID is enabled', () => {\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(true)\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const options = result.current\n    const azureOption = options.find((opt) => opt.type === AddDbType.azure)\n\n    expect(azureOption).toBeDefined()\n    expect(azureOption?.title).toBe('Azure Managed Redis')\n  })\n\n  it('should use initiateLogin for Azure option onClick when not logged in', () => {\n    const mockHistoryPush = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValue({ push: mockHistoryPush })\n\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(true)\n    mockedUseAzureAuth.mockReturnValue({\n      initiateLogin: mockInitiateLogin,\n      loading: false,\n      account: null,\n    })\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const azureOption = result.current.find(\n      (opt) => opt.type === AddDbType.azure,\n    )\n\n    azureOption?.onClick()\n\n    expect(mockInitiateLogin).toHaveBeenCalled()\n    expect(mockHistoryPush).not.toHaveBeenCalled()\n    expect(mockOnClickOption).not.toHaveBeenCalled()\n  })\n\n  it('should navigate to Azure subscriptions page when already logged in', () => {\n    const mockHistoryPush = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValue({ push: mockHistoryPush })\n\n    const mockAccount = { id: 'test-id', username: 'test@example.com' }\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(true)\n    mockedUseAzureAuth.mockReturnValue({\n      initiateLogin: mockInitiateLogin,\n      loading: false,\n      account: mockAccount,\n    })\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const azureOption = result.current.find(\n      (opt) => opt.type === AddDbType.azure,\n    )\n\n    azureOption?.onClick()\n\n    expect(mockHistoryPush).toHaveBeenCalledWith(Pages.azureSubscriptions)\n    expect(mockInitiateLogin).not.toHaveBeenCalled()\n    expect(mockOnClickOption).not.toHaveBeenCalled()\n  })\n\n  it('should use onClickOption for non-Azure options', () => {\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(false)\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const sentinelOption = result.current.find(\n      (opt) => opt.type === AddDbType.sentinel,\n    )\n\n    sentinelOption?.onClick()\n\n    expect(mockOnClickOption).toHaveBeenCalledWith(AddDbType.sentinel)\n    expect(mockInitiateLogin).not.toHaveBeenCalled()\n  })\n\n  it('should return Azure loading state from useAzureAuth', () => {\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(true)\n    mockedUseAzureAuth.mockReturnValue({\n      initiateLogin: mockInitiateLogin,\n      loading: true,\n    })\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const azureOption = result.current.find(\n      (opt) => opt.type === AddDbType.azure,\n    )\n\n    expect(azureOption?.loading).toBe(true)\n  })\n\n  it('should return loading = false for non-Azure options', () => {\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(false)\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const nonAzureOptions = result.current.filter(\n      (opt) => opt.type !== AddDbType.azure,\n    )\n\n    nonAzureOptions.forEach((option) => {\n      expect(option.loading).toBe(false)\n    })\n  })\n\n  it('should include all non-Azure options regardless of Azure flag', () => {\n    mockedIsAzureEntraIdEnabledSelector.mockReturnValue(false)\n\n    const { result } = renderHook(() =>\n      useConnectivityOptions({ onClickOption: mockOnClickOption }),\n    )\n\n    const sentinelOption = result.current.find(\n      (opt) => opt.type === AddDbType.sentinel,\n    )\n    const softwareOption = result.current.find(\n      (opt) => opt.type === AddDbType.software,\n    )\n    const importOption = result.current.find(\n      (opt) => opt.type === AddDbType.import,\n    )\n\n    expect(sentinelOption).toBeDefined()\n    expect(softwareOption).toBeDefined()\n    expect(importOption).toBeDefined()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/hooks/useConnectivityOptions.ts",
    "content": "import { useCallback, useMemo } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport { isAzureEntraIdEnabledSelector } from 'uiSrc/slices/app/features'\nimport { useAzureAuth } from 'uiSrc/components/hooks/useAzureAuth'\nimport { AddDbType } from 'uiSrc/pages/home/constants'\nimport { Pages } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  CONNECTIVITY_OPTIONS_CONFIG,\n  ConnectivityOption,\n  ConnectivityOptionConfig,\n} from '../constants'\n\ninterface UseConnectivityOptionsProps {\n  onClickOption: (type: AddDbType) => void\n}\n\nexport const useConnectivityOptions = ({\n  onClickOption,\n}: UseConnectivityOptionsProps): ConnectivityOption[] => {\n  const history = useHistory()\n  const isAzureEntraIdEnabled = useSelector(isAzureEntraIdEnabledSelector)\n  const { initiateLogin, loading: azureLoading, account } = useAzureAuth()\n\n  const handleAzureClick = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_IMPORT_DATABASES_CLICKED,\n    })\n    if (account) {\n      history.push(Pages.azureSubscriptions)\n    } else {\n      initiateLogin()\n    }\n  }, [account, history, initiateLogin])\n\n  return useMemo(() => {\n    const getClickHandler = (option: ConnectivityOptionConfig) => {\n      if (option.type === AddDbType.azure) {\n        return handleAzureClick\n      }\n      return () => onClickOption(option.type)\n    }\n\n    const getLoadingState = (option: ConnectivityOptionConfig) => {\n      if (option.type === AddDbType.azure) {\n        return azureLoading\n      }\n      return false\n    }\n\n    const isFeatureEnabled = (option: ConnectivityOptionConfig) => {\n      if (option.type === AddDbType.azure) {\n        return isAzureEntraIdEnabled\n      }\n      return true\n    }\n\n    return CONNECTIVITY_OPTIONS_CONFIG.filter(isFeatureEnabled).map(\n      (config) => ({\n        ...config,\n        onClick: getClickHandler(config),\n        loading: getLoadingState(config),\n      }),\n    )\n  }, [isAzureEntraIdEnabled, handleAzureClick, azureLoading, onClickOption])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/add-database-screen/index.ts",
    "content": "import AddDatabaseScreen from './AddDatabaseScreen'\n\nexport default AddDatabaseScreen\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport CloudConnectionFormWrapper, { Props } from './CloudConnectionFormWrapper'\n\nconst mockedProps = mock<Props>()\n\ndescribe('CloudConnectionFormWrapper', () => {\n  it('should render', () => {\n    expect(\n      render(<CloudConnectionFormWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/CloudConnectionFormWrapper.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport { Pages } from 'uiSrc/constants'\nimport {\n  cloudSelector,\n  fetchSubscriptionsRedisCloud,\n  setSSOFlow,\n} from 'uiSrc/slices/instances/cloud'\nimport { resetErrors } from 'uiSrc/slices/app/notifications'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { useModalHeader } from 'uiSrc/contexts/ModalTitleProvider'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport CloudConnectionForm from './cloud-connection-form'\n\nexport interface Props {\n  onClose?: () => void\n}\n\nexport interface ICloudConnectionSubmit {\n  accessKey: string\n  secretKey: string\n}\n\nconst CloudConnectionFormWrapper = ({ onClose }: Props) => {\n  const dispatch = useDispatch()\n\n  const history = useHistory()\n  const { loading, credentials } = useSelector(cloudSelector)\n\n  const { setModalHeader } = useModalHeader()\n\n  useEffect(() => {\n    setModalHeader(<Title size=\"M\">Discover Cloud databases</Title>, true)\n\n    return () => {\n      setModalHeader(null)\n      dispatch(resetErrors())\n    }\n  }, [])\n\n  const formSubmit = (credentials: ICloudConnectionSubmit) => {\n    sendEventTelemetry({\n      event:\n        TelemetryEvent.CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_SUBMITTED,\n    })\n    dispatch(setSSOFlow(undefined))\n    dispatch(fetchSubscriptionsRedisCloud(credentials, false, onSuccess))\n  }\n\n  const onSuccess = () => {\n    history.push(Pages.redisCloudSubscriptions)\n  }\n\n  return (\n    <CloudConnectionForm\n      accessKey={credentials?.accessKey ?? ''}\n      secretKey={credentials?.secretKey ?? ''}\n      onClose={onClose}\n      onSubmit={formSubmit}\n      loading={loading}\n    />\n  )\n}\n\nexport default CloudConnectionFormWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport CloudConnectionForm, { Props } from './CloudConnectionForm'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: false,\n    },\n  }),\n}))\n\ndescribe('CloudConnectionForm', () => {\n  it('should render', () => {\n    expect(\n      render(<CloudConnectionForm {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should not render cloud sso form by default', () => {\n    render(<CloudConnectionForm {...instance(mockedProps)} />)\n    expect(screen.getByTestId('access-key')).toBeInTheDocument()\n    expect(screen.getByTestId('secret-key')).toBeInTheDocument()\n  })\n\n  it('should render cloud sso form with feature flag', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      cloudSso: {\n        flag: true,\n      },\n    })\n\n    render(<CloudConnectionForm {...instance(mockedProps)} />)\n\n    expect(\n      screen.getByTestId('oauth-container-social-buttons'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/CloudConnectionForm.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport ReactDOM from 'react-dom'\nimport { FormikErrors, useFormik } from 'formik'\nimport { isEmpty } from 'lodash'\nimport { useSelector } from 'react-redux'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { validateField } from 'uiSrc/utils/validations'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { FeatureFlagComponent, RiTooltip } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { CloudConnectionOptions } from 'uiSrc/pages/home/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { OAuthAutodiscovery } from 'uiSrc/components/oauth/oauth-sso'\nimport { MessageCloudApiKeys } from 'uiSrc/pages/home/components/form/Messages'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { WindowEvent } from 'uiSrc/components/base/utils/WindowEvent'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiRadioGroup } from 'uiSrc/components/base/forms/radio-group/RadioGroup'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { ICloudConnectionSubmit } from '../CloudConnectionFormWrapper'\n\nimport { ScrollableWrapper } from '../../ManualConnection.styles'\n\nexport interface Props {\n  accessKey: string\n  secretKey: string\n  onClose?: () => void\n  onSubmit: ({ accessKey, secretKey }: ICloudConnectionSubmit) => void\n  loading: boolean\n}\n\ninterface ISubmitButton {\n  onClick: () => void\n  submitIsDisabled: boolean\n}\n\ninterface Values {\n  accessKey: string\n  secretKey: string\n}\n\nconst fieldDisplayNames: Values = {\n  accessKey: 'Enter API Account Key',\n  secretKey: 'Enter API User Key',\n}\n\nconst options = [\n  {\n    id: CloudConnectionOptions.Account,\n    value: CloudConnectionOptions.Account,\n    label: 'Redis Cloud account',\n  },\n  {\n    id: CloudConnectionOptions.ApiKeys,\n    value: CloudConnectionOptions.ApiKeys,\n    label: 'Redis Cloud API keys',\n  },\n]\n\nconst CloudConnectionForm = (props: Props) => {\n  const { accessKey, secretKey, onClose, onSubmit, loading } = props\n\n  const { [FeatureFlags.cloudSso]: cloudSsoFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const [domReady, setDomReady] = useState(false)\n  const [errors, setErrors] = useState<FormikErrors<Values>>(\n    accessKey || secretKey ? {} : fieldDisplayNames,\n  )\n  const [type, setType] = useState<CloudConnectionOptions>(\n    cloudSsoFeature?.flag\n      ? CloudConnectionOptions.Account\n      : CloudConnectionOptions.ApiKeys,\n  )\n\n  useEffect(() => {\n    setDomReady(true)\n  }, [])\n\n  const validate = (values: Values) => {\n    const errs: FormikErrors<Values> = {}\n\n    Object.entries(values).forEach(\n      ([key, value]) =>\n        !value && Object.assign(errs, { [key]: fieldDisplayNames[key] }),\n    )\n\n    setErrors(errs)\n    return errs\n  }\n\n  const formik = useFormik({\n    initialValues: {\n      accessKey,\n      secretKey,\n    },\n    validate,\n    onSubmit: (values) => {\n      onSubmit(values)\n    },\n  })\n\n  const submitIsEnable = () => isEmpty(errors)\n\n  const onKeyDown = (event: KeyboardEvent) => {\n    if (event.key === keys.ENTER && submitIsEnable()) {\n      formik.submitForm()\n      event.stopPropagation()\n    }\n  }\n\n  const CancelButton = ({ onClick }: { onClick: () => void }) => (\n    <SecondaryButton\n      className=\"btn-cancel\"\n      onClick={onClick}\n      style={{ marginRight: 12 }}\n    >\n      Cancel\n    </SecondaryButton>\n  )\n\n  const SubmitButton = ({ onClick, submitIsDisabled }: ISubmitButton) => (\n    <RiTooltip\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      title={\n        submitIsDisabled\n          ? validationErrors.REQUIRED_TITLE(Object.values(errors).length)\n          : null\n      }\n      content={\n        submitIsDisabled ? (\n          <span>\n            {Object.values(errors).map((err) => [err, <br key={err} />])}\n          </span>\n        ) : null\n      }\n    >\n      <PrimaryButton\n        type=\"submit\"\n        onClick={onClick}\n        disabled={submitIsDisabled}\n        loading={loading}\n        icon={submitIsDisabled ? InfoIcon : undefined}\n        data-testid=\"btn-submit\"\n      >\n        Submit\n      </PrimaryButton>\n    </RiTooltip>\n  )\n\n  const Footer = () => {\n    if (!domReady) return null\n\n    const footerEl = document.getElementById('footerDatabaseForm')\n    if (footerEl) {\n      return ReactDOM.createPortal(\n        <Row justify=\"end\" gap=\"m\">\n          {onClose && <CancelButton onClick={onClose} />}\n          <SubmitButton\n            onClick={formik.submitForm}\n            submitIsDisabled={!submitIsEnable()}\n          />\n        </Row>,\n        footerEl,\n      )\n    }\n    return null\n  }\n\n  const CloudApiForm = (\n    <div data-testid=\"add-db_cloud-api\">\n      <MessageCloudApiKeys />\n      <Spacer />\n      <WindowEvent event=\"keydown\" handler={onKeyDown} />\n      <form onSubmit={formik.handleSubmit}>\n        <Row responsive>\n          <FlexItem grow>\n            <FormField label=\"API Account Key\" required>\n              <TextInput\n                name=\"accessKey\"\n                id=\"accessKey\"\n                data-testid=\"access-key\"\n                maxLength={200}\n                placeholder={fieldDisplayNames.accessKey}\n                value={formik.values.accessKey}\n                autoComplete=\"off\"\n                onChange={(value) => {\n                  formik.setFieldValue('accessKey', validateField(value.trim()))\n                }}\n              />\n            </FormField>\n          </FlexItem>\n        </Row>\n        <Spacer size=\"l\" />\n        <Row responsive>\n          <FlexItem grow>\n            <FormField label=\"API User Key\" required>\n              <TextInput\n                name=\"secretKey\"\n                id=\"secretKey\"\n                data-testid=\"secret-key\"\n                maxLength={200}\n                placeholder={fieldDisplayNames.secretKey}\n                value={formik.values.secretKey}\n                autoComplete=\"off\"\n                onChange={(value) => {\n                  formik.setFieldValue('secretKey', validateField(value.trim()))\n                }}\n              />\n            </FormField>\n          </FlexItem>\n        </Row>\n        <Footer />\n      </form>\n    </div>\n  )\n\n  return (\n    <ScrollableWrapper>\n      <FeatureFlagComponent name={FeatureFlags.cloudSso}>\n        <Col gap=\"l\">\n          <FlexItem grow>\n            <Text color=\"primary\">Connect with</Text>\n          </FlexItem>\n          <FlexItem grow>\n            <RiRadioGroup\n              layout=\"horizontal\"\n              items={options}\n              value={type}\n              onChange={(id) => setType(id as CloudConnectionOptions)}\n              data-testid=\"cloud-options\"\n            />\n          </FlexItem>\n        </Col>\n        <Spacer size=\"l\" />\n      </FeatureFlagComponent>\n      {type === CloudConnectionOptions.Account && (\n        <OAuthAutodiscovery\n          source={OAuthSocialSource.DiscoveryForm}\n          onClose={onClose}\n        />\n      )}\n      {type === CloudConnectionOptions.ApiKeys && CloudApiForm}\n    </ScrollableWrapper>\n  )\n}\n\nexport default CloudConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/cloud-connection-form/index.ts",
    "content": "import CloudConnectionForm from './CloudConnectionForm'\n\nexport default CloudConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/index.ts",
    "content": "import CloudConnectionFormWrapper from './CloudConnectionFormWrapper'\n\nexport default CloudConnectionFormWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cloud-connection/styles.module.scss",
    "content": ".link {\n  text-decoration: underline !important;\n\n  &:hover {\n    text-decoration: none !important;\n  }\n}\n\n.divider {\n  width: 224px;\n  margin: 16px auto;\n  color: var(--htmlColor) !important;\n\n  &:before,\n  &:after {\n    content: \"\";\n    display: inline-block;\n    width: 80px;\n    height: 1px;\n    background-color: var(--tableLightestBorderColor);\n    margin-top: -3px;\n    margin-bottom: 4px;\n  }\n\n  &:before {\n    margin-right: 22px;\n  }\n  &:after {\n    margin-left: 22px;\n  }\n}\n\n.messageTitle {\n  text-align: center;\n  font-size: 14px !important;\n  font-style: normal;\n  font-weight: 400 !important;\n  line-height: 150% !important;\n  color: var(--htmlColor) !important;\n}\n\n.message {\n  font-family: \"Graphik\", sans-serif;\n  color: var(--euiTextSubduedColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport ClusterConnectionForm, {\n  Props as ClusterConnectionFormProps,\n} from './cluster-connection-form/ClusterConnectionForm'\nimport ClusterConnectionFormWrapper, {\n  Props,\n} from './ClusterConnectionFormWrapper'\n\nconst mockedProps = mock<Props>()\n\njest.mock('./cluster-connection-form/ClusterConnectionForm', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst mockClusterConnectionForm = (props: ClusterConnectionFormProps) => (\n  <div>\n    <button\n      type=\"button\"\n      onClick={() => props.onHostNamePaste('redis-12000.cluster.local:12000')}\n      data-testid=\"onHostNamePaste-btn\"\n    >\n      onHostNamePaste\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.onSubmit()}\n      data-testid=\"onSubmit-btn\"\n    >\n      onSubmit\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.onClose()}\n      data-testid=\"onClose-btn\"\n    >\n      onClose\n    </button>\n  </div>\n)\n\ndescribe('ClusterConnectionFormWrapper', () => {\n  beforeAll(() => {\n    ClusterConnectionForm.mockImplementation(mockClusterConnectionForm)\n  })\n  it('should render', () => {\n    expect(\n      render(<ClusterConnectionFormWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should call onHostNamePaste', () => {\n    const component = render(\n      <ClusterConnectionFormWrapper {...instance(mockedProps)} />,\n    )\n    fireEvent.click(screen.getByTestId('onHostNamePaste-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onSubmit', () => {\n    const component = render(\n      <ClusterConnectionFormWrapper {...instance(mockedProps)} />,\n    )\n    fireEvent.click(screen.getByTestId('onSubmit-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onClose', () => {\n    const onClose = jest.fn()\n    render(\n      <ClusterConnectionFormWrapper\n        {...instance(mockedProps)}\n        onClose={onClose}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('onClose-btn'))\n    expect(onClose).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/ClusterConnectionFormWrapper.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  clusterSelector,\n  fetchInstancesRedisCluster,\n} from 'uiSrc/slices/instances/cluster'\nimport { Pages } from 'uiSrc/constants'\nimport { resetErrors } from 'uiSrc/slices/app/notifications'\nimport { ICredentialsRedisCluster, InstanceType } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { autoFillFormDetails } from 'uiSrc/pages/home/utils'\n\nimport { useModalHeader } from 'uiSrc/contexts/ModalTitleProvider'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport ClusterConnectionForm from './cluster-connection-form/ClusterConnectionForm'\n\nimport { ContentWrapper } from '../ManualConnection.styles'\n\nexport interface Props {\n  onClose?: () => void\n}\n\nconst ClusterConnectionFormWrapper = ({ onClose }: Props) => {\n  const [initialValues, setInitialValues] = useState({\n    host: '',\n    port: '',\n    username: '',\n    password: '',\n  })\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { setModalHeader } = useModalHeader()\n\n  const formRef = useRef<HTMLDivElement>(null)\n\n  const { loading, credentials } = useSelector(clusterSelector)\n\n  useEffect(() => {\n    setModalHeader(<Title size=\"M\">Redis Software</Title>, true)\n\n    return () => {\n      setModalHeader(null)\n      dispatch(resetErrors())\n    }\n  }, [])\n\n  useEffect(() => {\n    if (credentials) {\n      setInitialValues({\n        host: credentials?.host,\n        port: credentials?.port?.toString(),\n        username: credentials?.username,\n        password: credentials?.password,\n      })\n    }\n  }, [credentials])\n\n  const formSubmit = (values: ICredentialsRedisCluster) => {\n    sendEventTelemetry({\n      event:\n        TelemetryEvent.CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_SUBMITTED,\n    })\n\n    dispatch(fetchInstancesRedisCluster(values, onSuccess))\n  }\n\n  const onSuccess = () => {\n    history.push(Pages.redisEnterpriseAutodiscovery)\n  }\n\n  const handlePostHostName = (content: string) =>\n    autoFillFormDetails(\n      content,\n      initialValues,\n      setInitialValues,\n      InstanceType.RedisEnterpriseCluster,\n    )\n\n  return (\n    <ContentWrapper as=\"div\" ref={formRef}>\n      <ClusterConnectionForm\n        host={credentials?.host ?? ''}\n        port={credentials?.port?.toString() ?? ''}\n        username={credentials?.username ?? ''}\n        password={credentials?.password ?? ''}\n        initialValues={initialValues}\n        onHostNamePaste={handlePostHostName}\n        onClose={onClose}\n        onSubmit={formSubmit}\n        loading={loading}\n      />\n    </ContentWrapper>\n  )\n}\n\nexport default ClusterConnectionFormWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\nimport ClusterConnectionForm, { Props } from './ClusterConnectionForm'\n\nconst mockedProps = mock<Props>()\n\ndescribe('ClusterConnectionForm', () => {\n  it('should render', () => {\n    expect(\n      render(<ClusterConnectionForm {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport ClusterConnectionForm from './'\n\nconst meta: Meta<typeof ClusterConnectionForm> = {\n  component: ClusterConnectionForm,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/ClusterConnectionForm.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport ReactDOM from 'react-dom'\nimport { isEmpty } from 'lodash'\n\nimport { FormikErrors, useFormik } from 'formik'\nimport * as keys from 'uiSrc/constants/keys'\nimport { MAX_PORT_NUMBER, validateField } from 'uiSrc/utils/validations'\nimport { handlePasteHostName } from 'uiSrc/utils'\nimport validationErrors from 'uiSrc/constants/validationErrors'\n\nimport { ICredentialsRedisCluster } from 'uiSrc/slices/interfaces'\n\nimport { MessageEnterpriceSoftware } from 'uiSrc/pages/home/components/form/Messages'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { WindowEvent } from 'uiSrc/components/base/utils/WindowEvent'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport {\n  FormField,\n  RiInfoIconProps,\n} from 'uiSrc/components/base/forms/FormField'\nimport {\n  NumericInput,\n  PasswordInput,\n  TextInput,\n} from 'uiSrc/components/base/inputs'\nimport { RiTooltip } from 'uiSrc/components'\nimport { HostInfoTooltipContent } from '../../host-info-tooltip-content/HostInfoTooltipContent'\n\nimport { ScrollableWrapper } from '../../ManualConnection.styles'\n\nexport interface Props {\n  host: string\n  port: string\n  username: string\n  password: string\n  onHostNamePaste: (text: string) => boolean\n  onClose?: () => void\n  initialValues: Values\n  onSubmit: (values: ICredentialsRedisCluster) => void\n  loading: boolean\n}\n\ninterface ISubmitButton {\n  onClick: () => void\n  submitIsDisabled: boolean\n}\n\ninterface Values {\n  host: string\n  port: string\n  username: string\n  password: string\n}\n\nconst fieldDisplayNames: Values = {\n  host: 'Cluster Host',\n  port: 'Cluster Port',\n  username: 'Admin Username',\n  // deepcode ignore NoHardcodedPasswords: <Not a password but \"password\" field placeholder>\n  password: 'Admin Password',\n}\n\nconst hostInfo: RiInfoIconProps = {\n  content: HostInfoTooltipContent({ includeAutofillInfo: true }),\n  placement: 'right',\n  maxWidth: '100%',\n}\n\nconst ClusterConnectionForm = (props: Props) => {\n  const {\n    host,\n    port,\n    username,\n    password,\n    initialValues: initialValuesProp,\n    onHostNamePaste,\n    onClose,\n    onSubmit,\n    loading,\n  } = props\n\n  const [errors, setErrors] = useState<FormikErrors<Values>>(\n    host || port || username || password ? {} : fieldDisplayNames,\n  )\n\n  const [initialValues, setInitialValues] = useState({\n    host,\n    port: port?.toString(),\n    username,\n    password,\n  })\n\n  useEffect(() => {\n    const values = {\n      ...initialValues,\n      ...initialValuesProp,\n    }\n\n    setInitialValues(values)\n    formik.validateForm(values)\n  }, [initialValuesProp])\n\n  const validate = (values: Values) => {\n    const errs: FormikErrors<Values> = {}\n\n    Object.entries(values).forEach(\n      ([key, value]) =>\n        !value && Object.assign(errs, { [key]: fieldDisplayNames[key] }),\n    )\n\n    setErrors(errs)\n    return errs\n  }\n\n  const formik = useFormik({\n    initialValues,\n    validate,\n    enableReinitialize: true,\n    validateOnMount: true,\n    validateOnBlur: false,\n    onSubmit: (values) => {\n      onSubmit({ ...values, port: parseInt(values.port) })\n    },\n  })\n\n  const submitIsEnable = () => isEmpty(errors)\n\n  const onKeyDown = (event: any) => {\n    if (event.key === keys.ENTER && submitIsEnable()) {\n      formik.submitForm()\n      event.stopPropagation()\n    }\n  }\n\n  const CancelButton = ({ onClick }: { onClick: () => void }) => (\n    <SecondaryButton className=\"btn-cancel\" onClick={onClick}>\n      Cancel\n    </SecondaryButton>\n  )\n\n  const SubmitButton = ({ onClick, submitIsDisabled }: ISubmitButton) => (\n    <RiTooltip\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      title={\n        submitIsDisabled\n          ? validationErrors.REQUIRED_TITLE(Object.values(errors).length)\n          : null\n      }\n      content={\n        submitIsDisabled ? (\n          <span>\n            {Object.values(errors).map((err) => [err, <br key={err} />])}\n          </span>\n        ) : null\n      }\n    >\n      <PrimaryButton\n        type=\"submit\"\n        onClick={onClick}\n        disabled={submitIsDisabled}\n        loading={loading}\n        data-testid=\"btn-submit\"\n      >\n        Submit\n      </PrimaryButton>\n    </RiTooltip>\n  )\n\n  const Footer = () => {\n    const footerEl = document.getElementById('footerDatabaseForm')\n    if (footerEl) {\n      return ReactDOM.createPortal(\n        <Row justify=\"end\" gap=\"m\">\n          {onClose && <CancelButton onClick={onClose} />}\n          <SubmitButton\n            onClick={formik.submitForm}\n            submitIsDisabled={!submitIsEnable()}\n          />\n        </Row>,\n        footerEl,\n      )\n    }\n    return null\n  }\n\n  return (\n    <ScrollableWrapper data-testid=\"add-db_cluster\">\n      <MessageEnterpriceSoftware />\n      <br />\n      <form>\n        <WindowEvent event=\"keydown\" handler={onKeyDown} />\n\n        <Col gap=\"l\">\n          <Row gap=\"m\" responsive>\n            <FlexItem grow={4}>\n              <FormField label=\"Cluster Host\" required infoIconProps={hostInfo}>\n                <TextInput\n                  name=\"host\"\n                  id=\"host\"\n                  data-testid=\"host\"\n                  maxLength={200}\n                  placeholder=\"Enter Cluster Host\"\n                  value={formik.values.host}\n                  onChange={(value) => {\n                    formik.setFieldValue('host', validateField(value.trim()))\n                  }}\n                  onPaste={(event: React.ClipboardEvent<HTMLInputElement>) =>\n                    handlePasteHostName(onHostNamePaste, event)\n                  }\n                />\n              </FormField>\n            </FlexItem>\n\n            <FlexItem grow={2}>\n              <FormField label=\"Cluster Port\" required>\n                <NumericInput\n                  autoValidate\n                  min={0}\n                  max={MAX_PORT_NUMBER}\n                  name=\"port\"\n                  id=\"port\"\n                  data-testid=\"port\"\n                  placeholder=\"Enter Cluster Port\"\n                  value={Number(formik.values.port)}\n                  onChange={(value) => formik.setFieldValue('port', value)}\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n\n          <Row gap=\"m\" responsive>\n            <FlexItem grow>\n              <FormField label=\"Admin Username\" required>\n                <TextInput\n                  name=\"username\"\n                  id=\"username\"\n                  data-testid=\"username\"\n                  maxLength={200}\n                  placeholder=\"Enter Admin Username\"\n                  value={formik.values.username}\n                  onChange={(value) => formik.setFieldValue('username', value)}\n                />\n              </FormField>\n            </FlexItem>\n\n            <FlexItem grow>\n              <FormField label=\"Admin Password\" required>\n                <PasswordInput\n                  type=\"dual\"\n                  name=\"password\"\n                  id=\"password\"\n                  data-testid=\"password\"\n                  maxLength={200}\n                  placeholder=\"Enter Password\"\n                  value={formik.values.password}\n                  onChange={(value) => formik.setFieldValue('password', value)}\n                  autoComplete=\"new-password\"\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n        </Col>\n      </form>\n      <Footer />\n    </ScrollableWrapper>\n  )\n}\n\nexport default ClusterConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/cluster-connection-form/index.ts",
    "content": "import ClusterConnectionForm from './ClusterConnectionForm'\n\nexport default ClusterConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/index.ts",
    "content": "import ClusterConnectionFormWrapper from './ClusterConnectionFormWrapper'\n\nexport default ClusterConnectionFormWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/styles.module.scss",
    "content": ".message {\n  font-family: 'Graphik', sans-serif;\n  color: var(--euiTextSubduedColor) !important;\n}\n\n.link {\n  text-decoration: underline !important;\n\n  &:hover {\n    text-decoration: none !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/cluster-connection/types.ts",
    "content": "export interface DatabaseDetails {\n  uid: number\n  name: string\n  host: string\n  port: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\n\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  ConnectionType,\n  Instance,\n  OAuthSocialAction,\n  OAuthSocialSource,\n} from 'uiSrc/slices/interfaces'\nimport { DatabaseListColumn } from 'uiSrc/constants'\nimport {\n  instancesSelector,\n  setShownColumns,\n} from 'uiSrc/slices/instances/instances'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { CREATE_CLOUD_DB_ID } from 'uiSrc/pages/home/constants'\n\nimport DatabaseListHeader, { Props } from './DatabaseListHeader'\n\nconst mockInstances: Instance[] = [\n  {\n    id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff',\n    host: 'localhost',\n    port: 6379,\n    name: 'localhost',\n    username: null,\n    password: null,\n    connectionType: ConnectionType.Standalone,\n    nameFromProvider: null,\n    new: true,\n    modules: [],\n    version: null,\n    lastConnection: new Date('2021-04-22T09:03:56.917Z'),\n    provider: 'provider',\n  },\n]\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    enhancedCloudUI: {\n      flag: false,\n    },\n    databaseManagement: {\n      flag: true,\n    },\n    cloudAds: {\n      flag: true,\n    },\n  }),\n}))\n\nconst mockShownColumns: DatabaseListColumn[] = [\n  DatabaseListColumn.Name,\n  DatabaseListColumn.Host,\n  DatabaseListColumn.Controls,\n]\n\nconst mockHiddenColumns: DatabaseListColumn[] = [\n  DatabaseListColumn.ConnectionType,\n  DatabaseListColumn.Modules,\n  DatabaseListColumn.LastConnection,\n]\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  instancesSelector: jest.fn().mockReturnValue({\n    loading: false,\n    shownColumns: ['name', 'host', 'controls'],\n    data: [...mockInstances],\n  }),\n}))\n\njest.mock('uiSrc/slices/content/create-redis-buttons', () => ({\n  ...jest.requireActual('uiSrc/slices/content/create-redis-buttons'),\n  contentSelector: jest.fn().mockReturnValue({\n    data: {\n      cloud: {\n        title: 'Try Redis Cloud: your ultimate Redis starting point',\n        description:\n          'Includes native support for JSON, Search and Query, and more',\n        links: {\n          main: {\n            altText: 'Try Redis Cloud.',\n            url: 'https://redis.io/try-free/?utm_source=redisinsight&utm_medium=main&utm_campaign=main',\n          },\n        },\n      },\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(instancesSelector as jest.Mock).mockReturnValue({\n    loading: false,\n    shownColumns: ['name', 'host', 'controls'],\n    data: [...mockInstances],\n  })\n})\n\ndescribe('DatabaseListHeader', () => {\n  it('should render', () => {\n    expect(\n      render(<DatabaseListHeader {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should not show promo cloud button with disabled feature flag', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValueOnce({\n      enhancedCloudUI: {\n        flag: true,\n      },\n    })\n\n    render(<DatabaseListHeader {...instance(mockedProps)} />)\n\n    expect(screen.queryByTestId('promo-btn')).not.toBeInTheDocument()\n  })\n\n  it('should show promo cloud button with enabled feature flag', () => {\n    render(<DatabaseListHeader {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('promo-btn')).toBeInTheDocument()\n  })\n\n  it('should show \"create database\" button when database management feature flag is enabled', () => {\n    const { queryByTestId } = render(\n      <DatabaseListHeader {...instance(mockedProps)} />,\n    )\n\n    expect(queryByTestId('add-redis-database-short')).toBeInTheDocument()\n  })\n\n  it('should hide \"create database\" button when database management feature flag is disabled', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      databaseManagement: {\n        flag: false,\n      },\n    })\n\n    const { queryByTestId } = render(\n      <DatabaseListHeader {...instance(mockedProps)} />,\n    )\n\n    expect(queryByTestId('add-redis-database-short')).not.toBeInTheDocument()\n  })\n\n  it('should show checkboxes with the right checked state when columns config button is clicked', async () => {\n    render(<DatabaseListHeader {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n\n    mockShownColumns.forEach((column) => {\n      const checkbox = screen.getByTestId(`show-${column}`)\n      expect(checkbox).toBeInTheDocument()\n      expect(checkbox).toBeChecked()\n    })\n\n    mockHiddenColumns.forEach((column) => {\n      const checkbox = screen.getByTestId(`show-${column}`)\n      expect(checkbox).toBeInTheDocument()\n      expect(checkbox).not.toBeChecked()\n    })\n  })\n\n  it('should dispatch setShownColumns action when checkbox clicked', async () => {\n    render(<DatabaseListHeader {...instance(mockedProps)} />)\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n\n    const checkbox = screen.getByTestId('show-name')\n    expect(checkbox).toBeInTheDocument()\n\n    fireEvent.click(checkbox)\n\n    const expectedActions = [setShownColumns(['host', 'controls'])]\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should log telemetry event when columns config changed', async () => {\n    const sendEventTelemetryMock = sendEventTelemetry as jest.Mock\n\n    render(<DatabaseListHeader {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n\n    const checkbox = screen.getByTestId('show-host')\n    expect(checkbox).toBeInTheDocument()\n\n    // clicking this checkbox will hide the column\n    fireEvent.click(checkbox)\n\n    expect(sendEventTelemetryMock).toBeCalledWith({\n      event: TelemetryEvent.DATABASE_LIST_COLUMNS_CLICKED,\n      eventData: {\n        shown: [],\n        hidden: [DatabaseListColumn.Host],\n      },\n    })\n  })\n\n  it('should dispatch SSO actions when clicking Create Free Cloud DB header button', () => {\n    const featureMock = appFeatureFlagsFeaturesSelector as jest.Mock\n\n    // Ensure the header button is visible and SSO path is taken\n    featureMock.mockReturnValue({\n      enhancedCloudUI: { flag: true },\n      cloudAds: { flag: true },\n      cloudSso: { flag: true },\n      databaseManagement: { flag: true },\n    })\n\n    render(<DatabaseListHeader {...instance(mockedProps)} />)\n\n    const btn = screen.getByTestId(`${CREATE_CLOUD_DB_ID}-button`)\n    expect(btn).toBeInTheDocument()\n\n    fireEvent.click(btn)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        setSSOFlow(OAuthSocialAction.Create),\n        setSocialDialogState(OAuthSocialSource.DatabaseConnectionList),\n      ]),\n    )\n\n    // Restore default flags for other tests\n    featureMock.mockReturnValue({\n      enhancedCloudUI: { flag: false },\n      databaseManagement: { flag: true },\n      cloudAds: { flag: true },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.tsx",
    "content": "import React, { useContext, useEffect, useState } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { isEmpty } from 'lodash'\nimport cx from 'classnames'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  instancesSelector,\n  setShownColumns,\n} from 'uiSrc/slices/instances/instances'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport PromoLink from 'uiSrc/components/promo-link/PromoLink'\n\nimport { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { getPathToResource } from 'uiSrc/services/resourcesService'\nimport { ContentCreateRedis } from 'uiSrc/slices/interfaces/content'\nimport { CREATE_CLOUD_DB_ID, HELP_LINKS } from 'uiSrc/pages/home/constants'\nimport { contentSelector } from 'uiSrc/slices/content/create-redis-buttons'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { getContentByFeature } from 'uiSrc/utils/content'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport {\n  COLUMN_FIELD_NAME_MAP,\n  DatabaseListColumn,\n  FeatureFlags,\n} from 'uiSrc/constants'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { EmptyButton, PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\nimport ColumnsConfigPopover from 'uiSrc/components/columns-config/ColumnsConfigPopover'\nimport handleClickFreeCloudDb from './handleClickFreeCloudDb'\nimport SearchDatabasesList from '../search-databases-list'\n\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  onAddInstance: () => void\n}\n\nconst DatabaseListHeader = ({ onAddInstance }: Props) => {\n  const { data: instances, shownColumns } = useSelector(instancesSelector)\n  const featureFlags = useSelector(appFeatureFlagsFeaturesSelector)\n  const { loading, data } = useSelector(contentSelector)\n\n  const [promoData, setPromoData] = useState<ContentCreateRedis>()\n\n  const { theme } = useContext(ThemeContext)\n  const { [FeatureFlags.enhancedCloudUI]: enhancedCloudUIFeature } =\n    featureFlags\n  const isShowPromoBtn = !enhancedCloudUIFeature?.flag\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (loading || !data || isEmpty(data)) {\n      return\n    }\n\n    if (data?.cloud && !isEmpty(data.cloud)) {\n      setPromoData(getContentByFeature(data.cloud as any, featureFlags))\n    }\n  }, [loading, data, featureFlags])\n\n  const handleOnAddDatabase = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_CLICKED,\n      eventData: {\n        source: OAuthSocialSource.DatabasesList,\n      },\n    })\n    onAddInstance()\n  }\n\n  const handleClickLink = (event: TelemetryEvent, eventData: any = {}) => {\n    if (event) {\n      sendEventTelemetry({\n        event,\n        eventData: {\n          ...eventData,\n        },\n      })\n    }\n  }\n\n  const handleCreateDatabaseClick = (\n    event: TelemetryEvent,\n    eventData: any = {},\n  ) => {\n    handleClickLink(event, eventData)\n  }\n\n  const handleColumnsChange = (\n    next: DatabaseListColumn[],\n    diff: { shown: DatabaseListColumn[]; hidden: DatabaseListColumn[] },\n  ) => {\n    dispatch(setShownColumns(next))\n    sendEventTelemetry({\n      event: TelemetryEvent.DATABASE_LIST_COLUMNS_CLICKED,\n      eventData: diff,\n    })\n  }\n\n  const AddCloudInstanceButton = () => (\n    <FeatureFlagComponent\n      name={[FeatureFlags.enhancedCloudUI, FeatureFlags.cloudAds]}\n    >\n      <PrimaryButton\n        onClick={handleClickFreeCloudDb}\n        data-testid={`${CREATE_CLOUD_DB_ID}-button`}\n      >\n        Create free Cloud database\n      </PrimaryButton>\n    </FeatureFlagComponent>\n  )\n\n  const AddLocalInstanceButton = () => (\n    <FeatureFlagComponent name={FeatureFlags.databaseManagement}>\n      <EmptyButton\n        variant=\"primary\"\n        onClick={handleOnAddDatabase}\n        data-testid=\"add-redis-database-short\"\n        icon={PlusIcon}\n      >\n        Connect existing database\n      </EmptyButton>\n    </FeatureFlagComponent>\n  )\n\n  const CreateBtn = ({ content }: { content: ContentCreateRedis }) => {\n    if (!isShowPromoBtn) return null\n\n    const { title, description, styles: stylesCss, links } = content\n    // @ts-ignore\n    const linkStyles = stylesCss ? stylesCss[theme] : {}\n    return (\n      <OAuthSsoHandlerDialog>\n        {(ssoCloudHandlerClick, isSSOEnabled) => (\n          <PromoLink\n            title={title}\n            description={description}\n            url={links?.main?.url}\n            testId=\"promo-btn\"\n            styles={{\n              ...linkStyles,\n              backgroundImage: linkStyles?.backgroundImage\n                ? `url(${getPathToResource(linkStyles.backgroundImage)})`\n                : undefined,\n            }}\n            onClick={(e) => {\n              !isSSOEnabled &&\n                handleCreateDatabaseClick(HELP_LINKS.cloud.event, {\n                  source: HELP_LINKS.cloud.sources.databaseList,\n                })\n              ssoCloudHandlerClick(e, {\n                source: OAuthSocialSource.ListOfDatabases,\n                action: OAuthSocialAction.Create,\n              })\n            }}\n          />\n        )}\n      </OAuthSsoHandlerDialog>\n    )\n  }\n\n  return (\n    <div className={styles.containerDl}>\n      <Row\n        className={styles.contentDL}\n        align=\"center\"\n        responsive={false}\n        gap=\"s\"\n      >\n        <FlexItem direction=\"row\" $gap=\"m\">\n          <AddCloudInstanceButton />\n          <AddLocalInstanceButton />\n        </FlexItem>\n        {!loading && !isEmpty(data) && (\n          <FlexItem className={cx(styles.promo)}>\n            <Row align=\"center\" gap=\"s\">\n              {promoData && (\n                <FeatureFlagComponent name={FeatureFlags.cloudAds}>\n                  <FlexItem>\n                    <CreateBtn content={promoData} />\n                  </FlexItem>\n                </FeatureFlagComponent>\n              )}\n            </Row>\n          </FlexItem>\n        )}\n        {instances.length > 0 && (\n          <Row justify=\"end\" align=\"center\" gap=\"l\">\n            <ColumnsConfigPopover\n              columnsMap={COLUMN_FIELD_NAME_MAP}\n              shownColumns={shownColumns}\n              onChange={handleColumnsChange}\n            />\n            <SearchDatabasesList />\n          </Row>\n        )}\n      </Row>\n      <Spacer className={styles.spacerDl} />\n    </div>\n  )\n}\n\nexport default DatabaseListHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-list-header/handleClickFreeCloudDb.ts",
    "content": "import { FeatureFlags } from 'uiSrc/constants'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\n\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { setSocialDialogState } from 'uiSrc/slices/oauth/cloud'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { HELP_LINKS } from 'uiSrc/pages/home/constants'\n\nimport { dispatch, store } from 'uiSrc/slices/store'\n\nconst handleClickFreeCloudDb = () => {\n  const { [FeatureFlags.cloudSso]: cloudSsoFeature } =\n    appFeatureFlagsFeaturesSelector(store.getState())\n\n  if (cloudSsoFeature?.flag) {\n    dispatch(setSSOFlow(OAuthSocialAction.Create))\n    dispatch(setSocialDialogState(OAuthSocialSource.DatabaseConnectionList))\n    sendEventTelemetry({\n      event: TelemetryEvent.CLOUD_FREE_DATABASE_CLICKED,\n      eventData: { source: OAuthSocialSource.DatabaseConnectionList },\n    })\n    return\n  }\n\n  sendEventTelemetry({\n    event: HELP_LINKS.cloud.event,\n    eventData: { source: HELP_LINKS.cloud.sources.databaseConnectionList },\n  })\n\n  const link = document.createElement('a')\n  link.setAttribute(\n    'href',\n    getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n      campaign: 'list_of_databases',\n    }),\n  )\n  link.setAttribute('target', '_blank')\n\n  link.click()\n  link.remove()\n}\n\nexport default handleClickFreeCloudDb\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-list-header/index.ts",
    "content": "import DatabaseListHeader from './DatabaseListHeader'\n\nexport default DatabaseListHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-list-header/styles.module.scss",
    "content": ".addInstanceBtn {\n  padding-left: 0;\n  padding-right: 0;\n  height: 43px;\n  margin: 0 auto;\n  text-decoration: none !important;\n\n  &:global(.euiButton) {\n    min-width: 54px !important;\n  }\n\n  :global(.euiButton__text) {\n    font-weight: 500 !important;\n  }\n}\n\n.clearMarginFlexItem {\n  margin-bottom: 0 !important;\n}\n\n.contentDL {\n  @include eui.euiBreakpoint(\"xs\", \"s\") {\n    & > div:first-of-type {\n      margin-left: 0 !important;\n    }\n    & > div:last-of-type {\n      margin-right: 0 !important;\n    }\n  }\n}\n\n.spacerDl {\n  @include eui.euiBreakpoint(\"xs\", \"s\") {\n    height: 6px !important;\n  }\n  @include eui.euiBreakpoint(\"m\", \"l\", \"xl\") {\n    height: 12px !important;\n  }\n}\n\n.promo {\n  display: flex !important;\n  @media only screen and (max-width: 800px) {\n    display: none !important;\n  }\n}\n\n.cloudSsoPromoTooltip {\n  display: flex;\n  flex-direction: row;\n  line-height: normal;\n  font-size: 12px !important;\n}\n.cloudSsoPromoTooltipIcon {\n  width: 20px !important;\n  height: 20px !important;\n  margin-right: 8px;\n}\n\n@include global.insights-open(1350px) {\n  .promo {\n    display: none !important;\n  }\n}\n\n.columnsButtonItem {\n  margin-right: 16px !important;\n\n  .columnsButton {\n    padding: 4px 6px 4px;\n    border-color: transparent !important;\n    box-shadow: none !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/ManageTagsModal.spec.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { updateInstanceAction } from 'uiSrc/slices/instances/instances'\n\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { ManageTagsModal, ManageTagsModalProps } from './ManageTagsModal'\n\njest.mock('react-redux', () => ({\n  useDispatch: jest.fn(),\n  connect: () => (Component: any) => Component,\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  updateInstanceAction: jest.fn().mockReturnValue({ type: 'UPDATE_INSTANCE' }),\n}))\n\nconst mockDispatch = useDispatch as jest.MockedFunction<typeof useDispatch>\nconst mockInstance: Partial<Instance> = {\n  id: '1',\n  name: 'Test Instance',\n  tags: [\n    {\n      id: '1',\n      key: 'env',\n      value: 'prod',\n      createdAt: new Date().toISOString(),\n      updatedAt: new Date().toISOString(),\n    },\n    {\n      id: '2',\n      key: 'version',\n      value: '1.0',\n      createdAt: new Date().toISOString(),\n      updatedAt: new Date().toISOString(),\n    },\n  ],\n  provider: 'REDIS_CLOUD',\n}\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\ndescribe('ManageTagsModal', () => {\n  const mockOnClose = jest.fn()\n  const mockDispatchFn = jest.fn()\n  mockDispatch.mockReturnValue(mockDispatchFn)\n\n  const renderComponent = (props: Partial<ManageTagsModalProps> = {}) => {\n    const defaultProps: ManageTagsModalProps = {\n      instance: mockInstance as any,\n      onClose: mockOnClose,\n      ...props,\n    }\n    return render(<ManageTagsModal {...defaultProps} />)\n  }\n\n  // Skipping until the title issue in the modal is fixed\n  it.skip('should render ManageTagsModal component', () => {\n    renderComponent()\n    expect(\n      screen.getByText('Manage tags for Test Instance'),\n    ).toBeInTheDocument()\n  })\n\n  it('should call onClose when Close button is clicked', () => {\n    renderComponent()\n    fireEvent.click(screen.getByTestId('close-button'))\n    expect(mockOnClose).toHaveBeenCalled()\n  })\n\n  it('should call dispatch with updateInstanceAction when Save tags button is clicked', () => {\n    renderComponent()\n    fireEvent.change(screen.getAllByRole('textbox')[0], {\n      target: { value: 'new-key' },\n    })\n    fireEvent.change(screen.getAllByRole('textbox')[1], {\n      target: { value: 'new-value' },\n    })\n    fireEvent.click(screen.getByTestId('save-tags-button'))\n    expect(mockDispatchFn).toHaveBeenCalledWith(\n      updateInstanceAction({\n        id: '1',\n        tags: [\n          { key: 'new-key', value: 'new-value' },\n          { key: 'version', value: '1.0' },\n        ],\n      }),\n    )\n  })\n\n  it('should add a new tag row when Add additional tag button is clicked', () => {\n    renderComponent()\n    fireEvent.click(screen.getByTestId('add-tag-button'))\n    expect(screen.getAllByRole('textbox').length).toBe(6)\n  })\n\n  it('should remove a tag row when trash icon is clicked', () => {\n    renderComponent()\n    fireEvent.click(screen.getAllByTestId('remove-tag-button')[0])\n    expect(screen.getAllByRole('textbox').length).toBe(2)\n  })\n\n  it('should disable Save tags button when an invalid tag value is input', () => {\n    renderComponent()\n    fireEvent.change(screen.getAllByRole('textbox')[0], {\n      target: { value: 'new-key' },\n    })\n    fireEvent.change(screen.getAllByRole('textbox')[1], {\n      target: { value: 'invalid value!@££@' },\n    })\n    expect(screen.getByTestId('save-tags-button')).toBeDisabled()\n  })\n\n  it('should disable Save tags button when a tag key is too long', () => {\n    renderComponent()\n    fireEvent.change(screen.getAllByRole('textbox')[0], {\n      target: { value: 'a'.repeat(66) },\n    })\n    fireEvent.change(screen.getAllByRole('textbox')[1], {\n      target: { value: 'value' },\n    })\n    expect(screen.getByTestId('save-tags-button')).toBeDisabled()\n  })\n\n  it('should disable Save tags button when a tag value is too long', () => {\n    renderComponent()\n    fireEvent.change(screen.getAllByRole('textbox')[0], {\n      target: { value: 'new-key' },\n    })\n    fireEvent.change(screen.getAllByRole('textbox')[1], {\n      target: { value: 'a'.repeat(130) },\n    })\n    expect(screen.getByTestId('save-tags-button')).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/ManageTagsModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const TagFormWrapper = styled.div`\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.core.space.space050};\n`\n\nexport const TagFormRow = styled(Row)`\n  > div {\n    flex: 1;\n    padding: ${({ theme }) => theme.core.space.space100};\n    display: flex;\n    align-items: center;\n    position: relative;\n    gap: ${({ theme }) => theme.core.space.space200};\n\n    > span {\n      width: 100%;\n      position: relative;\n    }\n\n    svg {\n      cursor: pointer;\n    }\n  }\n`\n\nexport const WarningBannerWrapper = styled(Row)`\n  align-items: center;\n  gap: ${({ theme }) => theme.core.space.space100};\n  padding: 6px 12px;\n  margin: ${({ theme }) => theme.core.space.space200};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.notice300};\n  background-color: ${({ theme }) => theme.semantic.color.background.notice100};\n  border-radius: ${({ theme }) => theme.core.space.space100};\n`\n\nexport const HeaderWrapper = styled(Row)`\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n\n  > p {\n    flex: 1;\n    font-weight: bold;\n    padding: ${({ theme }) =>\n      `${theme.core.space.space150} ${theme.core.space.space100}`};\n    margin: ${({ theme }) => theme.core.space.space050};\n    margin-right: 10px;\n  }\n\n  p:first-child {\n    border-right: 1px solid\n      ${({ theme }) => theme.semantic.color.border.neutral500};\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/ManageTagsModal.tsx",
    "content": "/* eslint-disable react/no-array-index-key */\nimport React, { useCallback, useMemo, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\nimport { ConnectionProvider, Instance } from 'uiSrc/slices/interfaces'\nimport { FormDialog } from 'uiSrc/components'\n\nimport { updateInstanceAction } from 'uiSrc/slices/instances/instances'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport {\n  EmptyButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { VALID_TAG_KEY_REGEX, VALID_TAG_VALUE_REGEX } from './constants'\nimport { TagInputField } from './TagInputField'\nimport { getInvalidTagErrors } from './utils'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  HeaderWrapper,\n  TagFormRow,\n  TagFormWrapper,\n  WarningBannerWrapper,\n} from './ManageTagsModal.styles'\n\nexport type ManageTagsModalProps = {\n  instance: Instance\n  onClose: () => void\n}\n\nexport const ManageTagsModal = ({\n  instance,\n  onClose,\n}: ManageTagsModalProps) => {\n  const dispatch = useDispatch()\n  const editedInstanceTags = useMemo(\n    () => (instance?.tags || []).map(({ key, value }) => ({ key, value })),\n    [instance?.tags],\n  )\n  const [tags, setTags] = useState(editedInstanceTags)\n  const currentTagKeys = useMemo(\n    () => new Set(tags.map((tag) => tag.key)),\n    [tags],\n  )\n\n  const isModified = useMemo(\n    () =>\n      tags.length !== editedInstanceTags.length ||\n      tags.some(\n        (tag, index) =>\n          tag.key !== editedInstanceTags[index].key ||\n          tag.value !== editedInstanceTags[index].value,\n      ),\n    [tags, editedInstanceTags],\n  )\n\n  const hasErrors = useMemo(\n    () =>\n      tags.some(\n        (tag) =>\n          !VALID_TAG_KEY_REGEX.test(tag.key) ||\n          !VALID_TAG_VALUE_REGEX.test(tag.value),\n      ),\n    [tags],\n  )\n\n  const isSaveButtonDisabled = !isModified || hasErrors\n  const isCloudDb = instance.provider === ConnectionProvider.REDIS_CLOUD\n  const isClusterDb = instance.provider === ConnectionProvider.REDIS_SOFTWARE\n\n  const handleTagChange = useCallback(\n    (index: number, key: 'key' | 'value', value: string) => {\n      if (value[0] === ' ') {\n        return\n      }\n\n      setTags((tags) => {\n        const newTags = [...tags]\n        newTags[index] = { ...newTags[index], [key]: value.toLowerCase() }\n\n        return newTags\n      })\n    },\n    [],\n  )\n\n  const handleAddTag = useCallback(() => {\n    setTags((tags) => [...tags, { key: '', value: '' }])\n  }, [])\n\n  const handleRemoveTag = useCallback((index: number) => {\n    setTags((tags) => tags.filter((_, i) => i !== index))\n  }, [])\n\n  const handleSave = useCallback(() => {\n    const tagsToSave = tags.filter((tag) => tag.key && tag.value)\n    dispatch(\n      updateInstanceAction({ id: instance.id, tags: tagsToSave }, () => {\n        dispatch(addMessageNotification(successMessages.SUCCESS_TAGS_UPDATED()))\n      }),\n    )\n  }, [instance.id, tags])\n\n  return (\n    <FormDialog\n      isOpen\n      onClose={onClose}\n      header={\n        <Col gap=\"xl\">\n          <Title size=\"L\">Manage tags for {instance.name}</Title>\n          <Text size=\"m\" color=\"secondary\">\n            Tags are key-value pairs that let you categorize your databases.\n          </Text>\n        </Col>\n      }\n      footer={\n        <>\n          {(isCloudDb || isClusterDb) && (\n            <WarningBannerWrapper>\n              <RiIcon type=\"ToastNotificationIcon\" color=\"notice600\" size=\"m\" />\n              <Text size=\"m\">\n                Tag changes in Redis Insight apply locally and are not synced\n                with Redis {isCloudDb ? 'Cloud' : 'Software'}.\n              </Text>\n            </WarningBannerWrapper>\n          )}\n          <Row justify=\"end\" gap=\"m\">\n            <SecondaryButton onClick={onClose} data-testid=\"close-button\">\n              Cancel\n            </SecondaryButton>\n            <PrimaryButton\n              onClick={handleSave}\n              disabled={isSaveButtonDisabled}\n              data-testid=\"save-tags-button\"\n            >\n              Save tags\n            </PrimaryButton>\n          </Row>\n        </>\n      }\n    >\n      <Col gap=\"l\">\n        <TagFormWrapper>\n          <HeaderWrapper>\n            <Text color=\"primary\">Key</Text>\n            <Text color=\"primary\">Value</Text>\n          </HeaderWrapper>\n          <Col gap=\"none\">\n            {tags.map((tag, index) => {\n              const { keyError, valueError } = getInvalidTagErrors(tags, index)\n\n              return (\n                <TagFormRow key={`tag-row-${index}`}>\n                  <TagInputField\n                    errorMessage={keyError}\n                    value={tag.key}\n                    currentTagKeys={currentTagKeys}\n                    placeholder=\"Select a key or type your own\"\n                    onChange={(value) => {\n                      handleTagChange(index, 'key', value)\n                    }}\n                    rightContent={<>:</>}\n                  />\n                  <TagInputField\n                    errorMessage={valueError}\n                    disabled={!tag.key || Boolean(keyError)}\n                    value={tag.value}\n                    currentTagKeys={currentTagKeys}\n                    suggestedTagKey={tag.key}\n                    placeholder=\"Select a value or type your own\"\n                    onChange={(value) => {\n                      handleTagChange(index, 'value', value)\n                    }}\n                    rightContent={\n                      <RiIcon\n                        type=\"DeleteIcon\"\n                        onClick={() => handleRemoveTag(index)}\n                        data-testid=\"remove-tag-button\"\n                      />\n                    }\n                  />\n                </TagFormRow>\n              )\n            })}\n          </Col>\n        </TagFormWrapper>\n        <div>\n          <EmptyButton\n            icon={PlusIcon}\n            onClick={handleAddTag}\n            size=\"small\"\n            data-testid=\"add-tag-button\"\n          >\n            Add additional tag\n          </EmptyButton>\n        </div>\n      </Col>\n    </FormDialog>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/TagInputField.spec.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { act, fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport { TagInputField } from './TagInputField'\n\njest.mock('react-redux', () => ({\n  useSelector: jest.fn(),\n  connect: () => (Component: any) => Component,\n}))\n\nconst mockSelector = useSelector as jest.MockedFunction<typeof useSelector>\n\ndescribe('TagInputField', () => {\n  const mockOnChange = jest.fn()\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockSelector.mockReturnValue({\n      data: [],\n    })\n  })\n\n  const renderComponent = (\n    props: Partial<React.ComponentProps<typeof TagInputField>> = {},\n  ) => {\n    const defaultProps = {\n      value: '',\n      currentTagKeys: new Set<string>(),\n      onChange: mockOnChange,\n      ...props,\n    }\n    return render(<TagInputField {...defaultProps} />)\n  }\n\n  it('should render input field with value', () => {\n    renderComponent({ value: 'test-tag' })\n\n    const input = screen.getByDisplayValue('test-tag')\n    expect(input).toBeInTheDocument()\n  })\n\n  it('should call onChange when input value changes', () => {\n    renderComponent()\n\n    const input = screen.getByRole('textbox')\n    fireEvent.change(input, { target: { value: 'new-value' } })\n\n    expect(mockOnChange).toHaveBeenCalledWith('new-value')\n  })\n\n  it('should render with placeholder', () => {\n    renderComponent({ placeholder: 'Enter tag key' })\n\n    expect(screen.getByPlaceholderText('Enter tag key')).toBeInTheDocument()\n  })\n\n  it('should be disabled when disabled prop is true', () => {\n    renderComponent({ disabled: true })\n\n    const input = screen.getByRole('textbox')\n    expect(input).toBeDisabled()\n  })\n\n  it('should show error state when errorMessage is provided', () => {\n    const { container } = renderComponent({ errorMessage: 'Invalid tag' })\n\n    // The input should have valid=undefined when there's an error\n    const input = container.querySelector('input')\n    expect(input).toBeInTheDocument()\n  })\n\n  it('should show TagSuggestions when input is focused and no error', async () => {\n    renderComponent({ value: 'env' })\n\n    const input = screen.getByRole('textbox')\n    fireEvent.focus(input)\n\n    await waitFor(() => {\n      expect(screen.getByTestId('tag-suggestions')).toBeInTheDocument()\n    })\n  })\n\n  it('should not show TagSuggestions when input has error', async () => {\n    renderComponent({ value: 'env', errorMessage: 'Invalid tag' })\n\n    const input = screen.getByRole('textbox')\n    fireEvent.focus(input)\n\n    await waitFor(() => {\n      expect(screen.queryByTestId('tag-suggestions')).not.toBeInTheDocument()\n    })\n  })\n\n  it('should hide TagSuggestions when input loses focus', async () => {\n    renderComponent({ value: 'env' })\n\n    const input = screen.getByRole('textbox')\n    fireEvent.focus(input)\n\n    await waitFor(() => {\n      expect(screen.getByTestId('tag-suggestions')).toBeInTheDocument()\n    })\n\n    fireEvent.blur(input)\n\n    await act(async () => {\n      await new Promise((resolve) => setTimeout(resolve, 200))\n    })\n\n    expect(screen.queryByTestId('tag-suggestions')).not.toBeInTheDocument()\n  })\n\n  it('should call onChange and hide suggestions when suggestion is selected', async () => {\n    renderComponent({ value: 'env' })\n\n    const input = screen.getByRole('textbox')\n    fireEvent.focus(input)\n\n    await waitFor(() => {\n      expect(screen.getByTestId('tag-suggestions')).toBeInTheDocument()\n    })\n\n    const suggestion = screen.getByText('environment')\n    fireEvent.click(suggestion)\n\n    expect(mockOnChange).toHaveBeenCalledWith('environment')\n    expect(screen.queryByTestId('tag-suggestions')).not.toBeInTheDocument()\n  })\n\n  it('should render rightContent when provided', () => {\n    renderComponent({\n      rightContent: <button data-testid=\"right-button\">Action</button>,\n    })\n\n    expect(screen.getByTestId('right-button')).toBeInTheDocument()\n  })\n\n  it('should pass suggestedTagKey to TagSuggestions', async () => {\n    renderComponent({ value: 'prod', suggestedTagKey: 'environment' })\n\n    const input = screen.getByRole('textbox')\n    fireEvent.focus(input)\n\n    await waitFor(() => {\n      expect(screen.getByTestId('tag-suggestions')).toBeInTheDocument()\n    })\n\n    // When suggestedTagKey is set, it shows values for that key\n    expect(screen.getByText('production')).toBeInTheDocument()\n  })\n\n  it('should pass currentTagKeys to TagSuggestions', async () => {\n    const currentTagKeys = new Set(['env', 'version'])\n    renderComponent({ value: '', currentTagKeys })\n\n    const input = screen.getByRole('textbox')\n    fireEvent.focus(input)\n\n    await waitFor(() => {\n      expect(screen.getByTestId('tag-suggestions')).toBeInTheDocument()\n    })\n\n    // currentTagKeys should filter out already used keys\n    expect(screen.queryByText('env')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/TagInputField.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { TagSuggestions } from './TagSuggestions'\nimport { TagInputFieldProps } from './TagInputField.types'\n\nexport const TagInputField = ({\n  value,\n  disabled,\n  currentTagKeys,\n  suggestedTagKey,\n  rightContent,\n  errorMessage,\n  placeholder,\n  onChange,\n}: TagInputFieldProps) => {\n  const isInvalid = Boolean(errorMessage)\n  const [isFocused, setIsFocused] = useState(false)\n\n  return (\n    <div>\n      <span>\n        <TextInput\n          value={value}\n          disabled={disabled}\n          valid={!isInvalid ? false : undefined}\n          error={isInvalid ? errorMessage : undefined}\n          onChange={(value) => onChange(value)}\n          placeholder={placeholder}\n          onFocusCapture={() => {\n            setIsFocused(true)\n          }}\n          onBlurCapture={() => {\n            setTimeout(() => {\n              isFocused && setIsFocused(false)\n            }, 150)\n          }}\n        />\n        {isFocused && !isInvalid && (\n          <TagSuggestions\n            targetKey={suggestedTagKey}\n            searchTerm={value}\n            currentTagKeys={currentTagKeys}\n            onChange={(value) => {\n              setIsFocused(false)\n              onChange(value)\n            }}\n          />\n        )}\n      </span>\n      {rightContent}\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/TagInputField.types.ts",
    "content": "export type TagInputFieldProps = {\n  value: string\n  disabled?: boolean\n  currentTagKeys: Set<string>\n  suggestedTagKey?: string\n  rightContent?: React.ReactNode\n  errorMessage?: string\n  placeholder?: string\n  onChange: (value: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/TagSuggestions.spec.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { Tag } from 'uiSrc/slices/interfaces/tag'\nimport { TagSuggestions, TagSuggestionsProps } from './TagSuggestions'\nimport { presetTagSuggestions } from './constants'\n\njest.mock('react-redux', () => ({\n  useSelector: jest.fn(),\n  connect: () => (Component: any) => Component,\n}))\n\nconst mockSelector = useSelector as jest.MockedFunction<typeof useSelector>\n\nconst mockTags: Tag[] = [\n  {\n    id: '1',\n    key: 'env',\n    value: 'prod',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n  {\n    id: '2',\n    key: 'version',\n    value: '1.0',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n]\n\ndescribe('TagSuggestions', () => {\n  const mockOnChange = jest.fn()\n\n  beforeEach(() => {\n    mockSelector.mockReturnValue({\n      data: mockTags,\n    })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  const renderComponent = (props: Partial<TagSuggestionsProps> = {}) => {\n    const defaultProps: TagSuggestionsProps = {\n      searchTerm: '',\n      currentTagKeys: new Set(),\n      onChange: mockOnChange,\n      ...props,\n    }\n    return render(<TagSuggestions {...defaultProps} />)\n  }\n\n  it('should render TagSuggestions component', () => {\n    renderComponent()\n    expect(screen.getByTestId('tag-suggestions')).toBeInTheDocument()\n  })\n\n  it('should call onChange when a tag key is clicked', () => {\n    renderComponent()\n    fireEvent.click(screen.getByText('env'))\n\n    expect(mockOnChange).toHaveBeenCalledWith('env')\n  })\n\n  it('should call onChange when a tag value is clicked', () => {\n    renderComponent({\n      targetKey: 'env',\n    })\n    fireEvent.click(screen.getByText('prod'))\n\n    expect(mockOnChange).toHaveBeenCalledWith('prod')\n  })\n\n  it('should display the correct number of tags', () => {\n    renderComponent()\n    const tagElements = screen.getAllByRole('listitem')\n\n    expect(tagElements.length).toBe(7)\n  })\n\n  it('should display no preset + local tags, without duplicates', () => {\n    mockSelector.mockReturnValue({\n      data: [...mockTags, presetTagSuggestions[0]],\n    })\n\n    renderComponent()\n    const tagElements = screen.getAllByRole('listitem')\n\n    expect(tagElements.length).toBe(7)\n  })\n\n  it('should display correct number of value suggestions when duplicated tag keys are present', () => {\n    mockSelector.mockReturnValue({\n      data: presetTagSuggestions,\n    })\n\n    renderComponent({\n      targetKey: 'environment',\n    })\n    const tagElements = screen.getAllByRole('listitem')\n\n    expect(tagElements.length).toBe(3)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/TagSuggestions.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const SuggestionsListWrapper = styled.div<{ children: React.ReactNode }>`\n  position: absolute;\n  top: calc(100% + ${({ theme }) => theme.core.space.space050});\n  z-index: 100;\n  background: ${({ theme }) => theme.semantic.color.background.neutral100};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.core.space.space050};\n  overflow-y: auto;\n  width: 100%;\n  max-height: 225px;\n\n  div[data-role='heading'] {\n    padding: ${({ theme }) => theme.core.space.space150};\n  }\n\n  ul {\n    li {\n      cursor: pointer;\n      padding: ${({ theme }) => theme.core.space.space150};\n    }\n\n    li:hover {\n      background: ${({ theme }) => theme.semantic.color.background.neutral300};\n    }\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/TagSuggestions.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { useSelector } from 'react-redux'\nimport { uniqBy } from 'lodash'\nimport { tagsSelector } from 'uiSrc/slices/instances/tags'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { presetTagSuggestions } from './constants'\nimport { SuggestionsListWrapper } from './TagSuggestions.styles'\n\ntype SelectOption = {\n  label: string\n  value: string\n}\n\nexport type TagSuggestionsProps = {\n  targetKey?: string\n  searchTerm: string\n  currentTagKeys: Set<string>\n  onChange: (value: string) => void\n}\n\nexport const TagSuggestions = ({\n  targetKey,\n  searchTerm,\n  currentTagKeys,\n  onChange,\n}: TagSuggestionsProps) => {\n  const { data: allTags } = useSelector(tagsSelector)\n  const tagsSuggestions: SelectOption[] = useMemo(() => {\n    const options = uniqBy(presetTagSuggestions.concat(allTags), (tag) =>\n      targetKey ? tag.value : tag.key,\n    )\n      .filter(({ key, value }) => {\n        if (targetKey !== undefined) {\n          return key === targetKey && value !== '' && value.includes(searchTerm)\n        }\n\n        return (\n          key.includes(searchTerm) &&\n          (!currentTagKeys.has(key) || key === searchTerm)\n        )\n      })\n      .map(({ key, value }) => ({\n        label: targetKey ? value : key,\n        value: targetKey ? value : key,\n      }))\n\n    const isNewTag = options.length === 0 && searchTerm\n\n    if (isNewTag) {\n      options.push({\n        label: `${searchTerm} (new ${targetKey ? 'value' : 'tag'})`,\n        value: searchTerm,\n      })\n    }\n\n    return options\n  }, [allTags, targetKey, searchTerm, currentTagKeys])\n\n  if (tagsSuggestions.length === 0) {\n    return null\n  }\n\n  return (\n    <SuggestionsListWrapper data-testid=\"tag-suggestions\">\n      <Title size=\"XS\" color=\"primary\">\n        Suggestions\n      </Title>\n      <ul role=\"list\">\n        {tagsSuggestions.map((option) => (\n          <li\n            role=\"listitem\"\n            key={option.value}\n            onClick={() => onChange(option.value)}\n          >\n            <Text>{option.label}</Text>\n          </li>\n        ))}\n      </ul>\n    </SuggestionsListWrapper>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/constants.ts",
    "content": "export const presetTagSuggestions: {\n  key: string\n  value: string\n}[] = [\n  {\n    key: 'environment',\n    value: 'production',\n  },\n  {\n    key: 'environment',\n    value: 'staging',\n  },\n  {\n    key: 'environment',\n    value: 'qa',\n  },\n  {\n    key: 'application',\n    value: '',\n  },\n  {\n    key: 'product',\n    value: '',\n  },\n  {\n    key: 'team',\n    value: '',\n  },\n  {\n    key: 'owner',\n    value: '',\n  },\n]\n\nexport const MAX_KEY_LENGTH = 64\nexport const MAX_VALUE_LENGTH = 128\n\nexport const VALID_TAG_KEY_REGEX = new RegExp(\n  `^[a-zA-Z0-9\\\\-_.@:+ ]{1,${MAX_KEY_LENGTH}}$`,\n)\nexport const VALID_TAG_VALUE_REGEX = new RegExp(\n  `^[a-zA-Z0-9\\\\-_.@:+ ]{1,${MAX_VALUE_LENGTH}}$`,\n)\n\nexport const INVALID_FIELD_MESSAGE =\n  'Tag can only have letters, numbers, spaces, and these special characters: “- _ . + @ :”'\nexport const INVALID_FIELD_UNIQUE_KEY_MESSAGE = 'Key should be unique'\nexport const INVALID_FIELD_MAX_KEY_LENGTH_MESSAGE = `Key must be under ${MAX_KEY_LENGTH} characters`\nexport const INVALID_FIELD_MAX_VALUE_LENGTH_MESSAGE = `Value must be under ${MAX_VALUE_LENGTH} characters`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-manage-tags-modal/utils.ts",
    "content": "import {\n  MAX_KEY_LENGTH,\n  MAX_VALUE_LENGTH,\n  VALID_TAG_KEY_REGEX,\n  VALID_TAG_VALUE_REGEX,\n  INVALID_FIELD_MESSAGE,\n  INVALID_FIELD_UNIQUE_KEY_MESSAGE,\n  INVALID_FIELD_MAX_KEY_LENGTH_MESSAGE,\n  INVALID_FIELD_MAX_VALUE_LENGTH_MESSAGE,\n} from './constants'\n\nexport function getInvalidTagErrors(\n  tags: { key: string; value: string }[],\n  index: number,\n) {\n  const tag = tags[index]\n\n  let keyError: string | undefined\n  let valueError: string | undefined\n\n  if (tag?.key) {\n    if (tag.key.length > MAX_KEY_LENGTH) {\n      keyError = INVALID_FIELD_MAX_KEY_LENGTH_MESSAGE\n    } else if (!VALID_TAG_KEY_REGEX.test(tag.key)) {\n      keyError = INVALID_FIELD_MESSAGE\n    } else if (tags.some((t, i) => i !== index && t.key === tag.key)) {\n      keyError = INVALID_FIELD_UNIQUE_KEY_MESSAGE\n    }\n  }\n\n  if (tag?.value) {\n    if (tag.value.length > MAX_VALUE_LENGTH) {\n      valueError = INVALID_FIELD_MAX_VALUE_LENGTH_MESSAGE\n    } else if (!VALID_TAG_VALUE_REGEX.test(tag.value)) {\n      valueError = INVALID_FIELD_MESSAGE\n    }\n  }\n\n  return { keyError, valueError }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-panel-dialog/DatabasePanelDialog.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport DatabasePanelDialog, { Props } from './DatabasePanelDialog'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\ndescribe('DatabasePanelDialog', () => {\n  it('should render', () => {\n    expect(\n      render(<DatabasePanelDialog {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should render proper form by dfeault', () => {\n    render(\n      <DatabasePanelDialog {...instance(mockedProps)} onClose={jest.fn()} />,\n    )\n\n    expect(screen.getByTestId('connection-url')).toBeInTheDocument()\n  })\n\n  it('should change screen to cloud and render proper form', () => {\n    render(\n      <DatabasePanelDialog {...instance(mockedProps)} onClose={jest.fn()} />,\n    )\n\n    fireEvent.click(screen.getByTestId('discover-cloud-btn'))\n\n    expect(screen.getByTestId('add-db_cloud-api')).toBeInTheDocument()\n  })\n\n  it('should change screen to software and render proper form', () => {\n    render(\n      <DatabasePanelDialog {...instance(mockedProps)} onClose={jest.fn()} />,\n    )\n\n    fireEvent.click(screen.getByTestId('option-btn-software'))\n\n    expect(screen.getByTestId('add-db_cluster')).toBeInTheDocument()\n  })\n\n  it('should change tab to sentinel and render proper form', async () => {\n    render(\n      <DatabasePanelDialog {...instance(mockedProps)} onClose={jest.fn()} />,\n    )\n\n    fireEvent.click(screen.getByTestId('option-btn-sentinel'))\n\n    expect(screen.getByTestId('add-db_sentinel')).toBeInTheDocument()\n  })\n\n  it('should change screen to import render proper form', async () => {\n    render(\n      <DatabasePanelDialog {...instance(mockedProps)} onClose={jest.fn()} />,\n    )\n\n    fireEvent.click(screen.getByTestId('option-btn-import'))\n\n    expect(screen.getByTestId('add-db_import')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-panel-dialog/DatabasePanelDialog.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { action } from 'storybook/actions'\n\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport DatabasePanelDialog from './index'\n\nconst meta = {\n  component: DatabasePanelDialog,\n} satisfies Meta<typeof DatabasePanelDialog>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    editMode: false,\n    editedInstance: null,\n    onClose: action('onClose'),\n  },\n}\n\nconst mockInstance = DBInstanceFactory.build({\n  id: '13bd1fb0-0af6-4433-b138-99eba801f3fe',\n  host: '127.0.0.1',\n  port: 6666,\n  name: '127.0.0.1:6666',\n  connectionType: ConnectionType.Standalone,\n  provider: 'REDIS_STACK',\n  lastConnection: new Date('2025-10-17T06:29:06.536Z'),\n  modules: [\n    { name: 'timeseries', version: 11202, semanticVersion: '1.12.2' },\n    { name: 'search', version: 21005, semanticVersion: '2.10.5' },\n    { name: 'ReJSON', version: 20803, semanticVersion: '2.8.3' },\n    { name: 'bf', version: 20802, semanticVersion: '2.8.2' },\n    { name: 'redisgears_2', version: 20020, semanticVersion: '2.0.20' },\n  ],\n  version: '7.4.0',\n})\n\nexport const EditModeTrue: Story = {\n  args: {\n    editMode: true,\n    editedInstance: mockInstance,\n    onClose: action('onClose'),\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-panel-dialog/DatabasePanelDialog.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { AddDbType } from 'uiSrc/pages/home/constants'\nimport {\n  clusterSelector,\n  resetDataRedisCluster,\n} from 'uiSrc/slices/instances/cluster'\nimport {\n  cloudSelector,\n  resetDataRedisCloud,\n} from 'uiSrc/slices/instances/cloud'\nimport {\n  resetDataSentinel,\n  sentinelSelector,\n} from 'uiSrc/slices/instances/sentinel'\nimport {\n  appRedirectionSelector,\n  setUrlHandlingInitialState,\n} from 'uiSrc/slices/app/url-handling'\n\nimport ManualConnectionWrapper from 'uiSrc/pages/home/components/manual-connection'\nimport SentinelConnectionWrapper from 'uiSrc/pages/home/components/sentinel-connection'\nimport AddDatabaseScreen from 'uiSrc/pages/home/components/add-database-screen'\n\nimport CloudConnectionFormWrapper from 'uiSrc/pages/home/components/cloud-connection'\nimport ImportDatabase from 'uiSrc/pages/home/components/import-database'\nimport { FormDialog } from 'uiSrc/components'\nimport { ModalHeaderProvider } from 'uiSrc/contexts/ModalTitleProvider'\nimport ClusterConnectionFormWrapper from 'uiSrc/pages/home/components/cluster-connection'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { ChevronLeftIcon } from 'uiSrc/components/base/icons'\nimport { FooterDatabaseForm } from 'uiSrc/components/form-dialog/FooterDatabaseForm'\nimport { Title } from 'uiSrc/components/base/text'\n\nimport { FixedWrapper } from '../ManualConnection.styles'\n\nexport interface Props {\n  editMode: boolean\n  urlHandlingAction?: Nullable<UrlHandlingActions>\n  editedInstance: Nullable<Instance>\n  onClose: () => void\n  onDbEdited?: () => void\n  initConnectionType?: Nullable<AddDbType>\n}\n\nconst DatabasePanelDialog = (props: Props) => {\n  const { editMode, onClose } = props\n\n  const [initialValues, setInitialValues] = useState(null)\n  const [connectionType, setConnectionType] =\n    useState<Nullable<AddDbType>>(null)\n  const [modalHeader, setModalHeader] =\n    useState<Nullable<React.ReactNode>>(null)\n\n  const { credentials: clusterCredentials } = useSelector(clusterSelector)\n  const { credentials: cloudCredentials } = useSelector(cloudSelector)\n  const { data: sentinelMasters } = useSelector(sentinelSelector)\n  const { action, dbConnection } = useSelector(appRedirectionSelector)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (editMode) return\n    if (clusterCredentials) {\n      setConnectionType(AddDbType.software)\n    }\n\n    if (cloudCredentials) {\n      setConnectionType(AddDbType.cloud)\n    }\n\n    if (sentinelMasters.length) {\n      setConnectionType(AddDbType.sentinel)\n    }\n  }, [])\n\n  useEffect(() => {\n    if (action === UrlHandlingActions.Connect) {\n      setConnectionType(AddDbType.manual)\n      setInitialValues(dbConnection)\n    }\n  }, [action, dbConnection])\n\n  useEffect(() => {\n    if (editMode) {\n      setConnectionType(AddDbType.manual)\n    }\n  }, [editMode])\n\n  useEffect(\n    () => () => {\n      if (connectionType === AddDbType.manual) return\n\n      switch (connectionType) {\n        case AddDbType.cloud: {\n          dispatch(resetDataRedisCluster())\n          dispatch(resetDataSentinel())\n          break\n        }\n\n        case AddDbType.sentinel: {\n          dispatch(resetDataRedisCloud())\n          dispatch(resetDataRedisCluster())\n          break\n        }\n\n        case AddDbType.software: {\n          dispatch(resetDataRedisCloud())\n          dispatch(resetDataSentinel())\n          break\n        }\n        default:\n          break\n      }\n    },\n    [connectionType],\n  )\n\n  const changeConnectionType = (connectionType: AddDbType, db: any) => {\n    dispatch(setUrlHandlingInitialState())\n    setInitialValues(db)\n    setConnectionType(connectionType)\n  }\n\n  const handleClickBack = () => {\n    setConnectionType(null)\n  }\n\n  const Form = () => (\n    <>\n      {connectionType === null && (\n        <AddDatabaseScreen\n          onSelectOption={changeConnectionType}\n          onClose={onClose}\n        />\n      )}\n      {connectionType === AddDbType.manual && (\n        <ManualConnectionWrapper\n          {...props}\n          initialValues={initialValues}\n          onClickBack={handleClickBack}\n        />\n      )}\n      {connectionType === AddDbType.cloud && (\n        <CloudConnectionFormWrapper {...props} />\n      )}\n      {connectionType === AddDbType.import && (\n        <ImportDatabase onClose={onClose} />\n      )}\n      {connectionType === AddDbType.sentinel && (\n        <SentinelConnectionWrapper {...props} />\n      )}\n      {connectionType === AddDbType.software && (\n        <ClusterConnectionFormWrapper {...props} />\n      )}\n    </>\n  )\n\n  const handleSetModalHeader = (\n    content: Nullable<React.ReactNode>,\n    withBack = false,\n  ) => {\n    const header =\n      withBack && content ? (\n        <Row align=\"center\" gap=\"s\">\n          <FlexItem>\n            <IconButton\n              onClick={handleClickBack}\n              icon={ChevronLeftIcon}\n              aria-label=\"back\"\n              data-testid=\"back-btn\"\n            />\n          </FlexItem>\n          <FlexItem grow>{content}</FlexItem>\n        </Row>\n      ) : (\n        content\n      )\n\n    setModalHeader(header)\n  }\n\n  return (\n    <FormDialog\n      isOpen\n      onClose={onClose}\n      header={modalHeader ?? <Title size=\"L\">Add database</Title>}\n      footer={<FooterDatabaseForm />}\n    >\n      <FixedWrapper>\n        <ModalHeaderProvider\n          value={{ modalHeader, setModalHeader: handleSetModalHeader }}\n        >\n          {Form()}\n        </ModalHeaderProvider>\n      </FixedWrapper>\n    </FormDialog>\n  )\n}\n\nexport default DatabasePanelDialog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/database-panel-dialog/index.ts",
    "content": "import DatabasePanelDialog from './DatabasePanelDialog'\n\nexport default DatabasePanelDialog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/DatabasesList.config.tsx",
    "content": "import React from 'react'\n\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport {\n  ColumnDef,\n  SortingState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\nimport {\n  BrowserStorageItem,\n  COLUMN_FIELD_NAME_MAP,\n  DatabaseListColumn,\n  DEFAULT_SORT,\n} from 'uiSrc/constants'\nimport { localStorageService } from 'uiSrc/services'\n\nimport DatabasesListCellName from './components/DatabasesListCellName/DatabasesListCellName'\nimport DatabasesListCellHost from './components/DatabasesListCellHost/DatabasesListCellHost'\nimport DatabasesListCellConnectionType from './components/DatabasesListCellConnectionType/DatabasesListCellConnectionType'\nimport DatabasesListCellLastConnection from './components/DatabasesListCellLastConnection/DatabasesListCellLastConnection'\nimport DatabasesListCellModules from './components/DatabasesListCellModules/DatabasesListCellModules'\nimport DatabasesListCellControls from './components/DatabasesListCellControls/DatabasesListCellControls'\nimport DatabasesListCellTags from './components/DatabasesListCellTags/DatabasesListCellTags'\nimport { TagsCellHeader } from '../tags-cell/TagsCellHeader'\n\nexport const SELECT_COL_ID = 'select-col-db'\n\nexport const ENABLE_PAGINATION_COUNT = 15\n\nexport const DEFAULT_SORTING: SortingState = [\n  {\n    id: (\n      localStorageService.get(BrowserStorageItem.instancesSorting) ??\n      DEFAULT_SORT\n    ).field,\n    desc:\n      (\n        localStorageService.get(BrowserStorageItem.instancesSorting) ??\n        DEFAULT_SORT\n      ).direction === 'desc',\n  },\n]\n\nexport const BASE_COLUMNS: ColumnDef<Instance>[] = [\n  {\n    id: SELECT_COL_ID,\n    size: 40,\n    isHeaderCustom: true,\n    enableSorting: false,\n    header: Table.HeaderMultiRowSelectionButton,\n    cell: ({ row }) => (\n      <Table.RowSelectionButton\n        row={row}\n        onClick={(e: any) => e.stopPropagation()}\n      />\n    ),\n  },\n  {\n    id: DatabaseListColumn.Name,\n    accessorKey: DatabaseListColumn.Name,\n    header: COLUMN_FIELD_NAME_MAP.get(DatabaseListColumn.Name),\n    enableSorting: true,\n    cell: DatabasesListCellName,\n    sortingFn: (rowA, rowB) => {\n      return `${rowA.original.name?.toLowerCase()}`.localeCompare(\n        `${rowB.original.name?.toLowerCase()}`,\n      )\n    },\n  },\n  {\n    id: DatabaseListColumn.Host,\n    accessorKey: DatabaseListColumn.Host,\n    header: COLUMN_FIELD_NAME_MAP.get(DatabaseListColumn.Host),\n    enableSorting: true,\n    cell: DatabasesListCellHost,\n    sortingFn: (rowA, rowB) => {\n      return `${rowA.original.host?.toLowerCase()}:${rowA.original.port}`.localeCompare(\n        `${rowB.original.host?.toLowerCase()}:${rowB.original.port}`,\n      )\n    },\n  },\n  {\n    id: DatabaseListColumn.ConnectionType,\n    accessorKey: DatabaseListColumn.ConnectionType,\n    header: COLUMN_FIELD_NAME_MAP.get(DatabaseListColumn.ConnectionType),\n    enableSorting: true,\n    cell: DatabasesListCellConnectionType,\n  },\n  {\n    id: DatabaseListColumn.Modules,\n    accessorKey: DatabaseListColumn.Modules,\n    header: COLUMN_FIELD_NAME_MAP.get(DatabaseListColumn.Modules),\n    enableSorting: false,\n    cell: DatabasesListCellModules,\n  },\n  {\n    id: DatabaseListColumn.LastConnection,\n    accessorKey: DatabaseListColumn.LastConnection,\n    header: COLUMN_FIELD_NAME_MAP.get(DatabaseListColumn.LastConnection),\n    enableSorting: true,\n    sortingFn: (rowA, rowB) => {\n      const conn1 = rowA.original.lastConnection\n      const conn2 = rowB.original.lastConnection\n      if (conn1 && conn2) {\n        return new Date(conn2).getTime() - new Date(conn1).getTime()\n      }\n      if (conn1 && !conn2) {\n        return -1\n      }\n      if (!conn1 && conn2) {\n        return 1\n      }\n      return 0\n    },\n    cell: DatabasesListCellLastConnection,\n  },\n  {\n    id: DatabaseListColumn.Tags,\n    accessorKey: DatabaseListColumn.Tags,\n    isHeaderCustom: true,\n    header: TagsCellHeader,\n    enableSorting: true,\n    cell: DatabasesListCellTags,\n    sortingFn: (rowA, rowB) => {\n      // compare value of first tag only\n      const tagA = rowA.original.tags?.[0]\n      const tagB = rowB.original.tags?.[0]\n\n      return `${tagA?.key || ''}:${tagA?.value || ''}`\n        .toLowerCase()\n        .localeCompare(`${tagB?.key || ''}:${tagB?.value || ''}`.toLowerCase())\n    },\n  },\n  {\n    id: DatabaseListColumn.Controls,\n    accessorKey: DatabaseListColumn.Controls,\n    header: '',\n    enableSorting: false,\n    cell: DatabasesListCellControls,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/DatabasesList.tsx",
    "content": "import React, { memo } from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport {\n  handleCheckConnectToInstance,\n  handlePaginationChange,\n  handleSortingChange,\n  getDefaultPagination,\n} from './methods/handlers'\nimport BulkItemsActions from './components/BulkItemsActions/BulkItemsActions'\nimport { DEFAULT_SORTING } from './DatabasesList.config'\nimport useDatabaseListData from './hooks/useDatabaseListData'\n\nconst DatabasesList = () => {\n  const {\n    columns,\n    visibleInstances,\n    selectedInstances,\n    paginationEnabled,\n    rowSelection,\n    emptyMessage,\n    setRowSelection,\n    resetRowSelection,\n  } = useDatabaseListData()\n\n  return (\n    <>\n      <Table\n        data={visibleInstances}\n        columns={columns}\n        stripedRows\n        rowSelectionMode=\"multiple\"\n        paginationEnabled={paginationEnabled}\n        onRowClick={handleCheckConnectToInstance}\n        emptyState={emptyMessage}\n        onRowSelectionChange={setRowSelection}\n        rowSelection={rowSelection}\n        onSortingChange={handleSortingChange}\n        defaultPagination={getDefaultPagination()}\n        onPaginationChange={handlePaginationChange}\n        defaultSorting={DEFAULT_SORTING}\n        maxHeight=\"60rem\" // this enables vertical scroll\n      />\n      <BulkItemsActions items={selectedInstances} onClose={resetRowSelection} />\n    </>\n  )\n}\n\nexport default memo(DatabasesList)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/DatabasesList.types.ts",
    "content": "import { ReactElement } from 'react'\n\nimport { CellContext } from 'uiSrc/components/base/layout/table'\nimport { Instance } from 'uiSrc/slices/interfaces'\n\nexport type IDatabaseListCell = (\n  props: CellContext<Instance, unknown>,\n) => ReactElement<any, any> | null\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/BulkItemsActions/BulkItemsActions.tsx",
    "content": "import React, { memo } from 'react'\n\nimport {\n  ActionBar,\n  ExportAction,\n  DeleteAction,\n} from 'uiSrc/components/item-list/components'\n\nimport {\n  handleDeleteInstances,\n  handleExportInstances,\n} from './methods/handlers'\nimport { Instance } from 'uiSrc/slices/interfaces'\n\nconst actionMessage = (action: string, length: number) =>\n  `Selected ${length} items will be ${action} from RedisInsight:`\n\ntype BulkItemsActionsProps = {\n  items: Instance[]\n  onClose: () => void\n}\n\nconst BulkItemsActions = ({ items, onClose }: BulkItemsActionsProps) => {\n  if (!items.length) {\n    return null\n  }\n\n  return (\n    <ActionBar\n      selectionCount={items.length}\n      onCloseActionBar={onClose}\n      actions={[\n        <ExportAction<Instance>\n          selection={items}\n          onExport={(_, withSecrets) => {\n            handleExportInstances(items, withSecrets)\n            onClose()\n          }}\n          subTitle={actionMessage('exported', items.length)}\n        />,\n        <DeleteAction<Instance>\n          selection={items}\n          onDelete={() => {\n            handleDeleteInstances(items)\n            onClose()\n          }}\n          subTitle={actionMessage('deleted', items.length)}\n        />,\n      ]}\n    />\n  )\n}\n\nexport default memo(BulkItemsActions)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/BulkItemsActions/methods/handlers.spec.ts",
    "content": "import saveAs from 'file-saver'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { Instance } from 'uiSrc/slices/interfaces'\n\nimport { handleDeleteInstances, handleExportInstances } from './handlers'\n\n// Mocks\njest.mock('uiSrc/slices/store', () => ({\n  dispatch: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => {\n  const actual = jest.requireActual('uiSrc/telemetry')\n  return { ...actual, sendEventTelemetry: jest.fn() }\n})\n\nconst mockDeleteInstancesAction = jest.fn(\n  (instances: Instance[], cb?: () => void) => ({\n    type: 'MOCK_DELETE',\n    payload: { instances, cb },\n  }),\n)\nconst mockExportInstancesAction = jest.fn(\n  (\n    ids: string[],\n    withSecrets: boolean,\n    onSuccess?: (data: any) => void,\n    onFail?: () => void,\n  ) => {\n    // Return a dummy action object; tests will trigger callbacks manually\n    return {\n      type: 'MOCK_EXPORT',\n      payload: { ids, withSecrets, onSuccess, onFail },\n    }\n  },\n)\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  deleteInstancesAction: (...args: any[]) =>\n    // @ts-expect-error\n    mockDeleteInstancesAction(...(args as any)),\n  exportInstancesAction: (...args: any[]) =>\n    // @ts-expect-error\n    mockExportInstancesAction(...(args as any)),\n}))\n\njest.mock('file-saver', () => ({ __esModule: true, default: jest.fn() }))\n\nconst instances: Instance[] = [\n  { id: '1', name: 'A', host: 'h1', port: 6379, modules: [], version: null },\n  { id: '2', name: 'B', host: 'h2', port: 6380, modules: [], version: null },\n]\n\ndescribe('BulkItemsActions handlers', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('handleDeleteInstances should dispatch delete and send telemetry for multiple', () => {\n    handleDeleteInstances(instances)\n\n    expect(mockDeleteInstancesAction).toHaveBeenCalled()\n    expect(dispatch).toHaveBeenCalledWith(\n      expect.objectContaining({ type: 'MOCK_DELETE' }),\n    )\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event:\n          TelemetryEvent.CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED,\n        eventData: { ids: ['1', '2'] },\n      }),\n    )\n  })\n\n  it('handleDeleteInstances should not send multiple-delete telemetry for single', () => {\n    handleDeleteInstances([instances[0]])\n\n    expect(mockDeleteInstancesAction).toHaveBeenCalled()\n    expect(sendEventTelemetry).not.toHaveBeenCalledWith(\n      expect.objectContaining({\n        event:\n          TelemetryEvent.CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED,\n      }),\n    )\n  })\n\n  it('handleExportInstances should dispatch export and call saveAs on success', () => {\n    const withSecrets = true\n\n    handleExportInstances(instances, withSecrets)\n\n    expect(mockExportInstancesAction).toHaveBeenCalledWith(\n      ['1', '2'],\n      withSecrets,\n      expect.any(Function),\n      expect.any(Function),\n    )\n\n    // Simulate success callback\n    const action = mockExportInstancesAction.mock.results[0].value as any\n    action.payload.onSuccess?.({ foo: 'bar' })\n\n    expect(saveAs).toHaveBeenCalled()\n    // Second arg should be filename with prefix\n    expect((saveAs as unknown as jest.Mock).mock.calls[0][1]).toMatch(\n      /RedisInsight_connections_\\d+\\.json/,\n    )\n\n    // Should send click + success telemetry\n    expect((sendEventTelemetry as jest.Mock).mock.calls[0][0]).toEqual(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_REDIS_EXPORT_CLICKED,\n      }),\n    )\n    expect((sendEventTelemetry as jest.Mock).mock.calls[1][0]).toEqual(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_REDIS_EXPORT_SUCCEEDED,\n        eventData: { numberOfDatabases: 2 },\n      }),\n    )\n  })\n\n  it('handleExportInstances should call failure telemetry on fail', () => {\n    handleExportInstances(instances, false)\n\n    const action = mockExportInstancesAction.mock.results[0].value as any\n    action.payload.onFail?.()\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_REDIS_EXPORT_FAILED,\n        eventData: { numberOfDatabases: 2 },\n      }),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/BulkItemsActions/methods/handlers.ts",
    "content": "import { map } from 'lodash'\nimport saveAs from 'file-saver'\n\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  deleteInstancesAction,\n  exportInstancesAction,\n  setEditedInstance,\n} from 'uiSrc/slices/instances/instances'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { localStorageService } from 'uiSrc/services'\n\nconst onDeleteInstances = (instances: Instance[]) => {\n  dispatch(setEditedInstance(null))\n\n  instances.forEach((instance) => {\n    localStorageService.remove(BrowserStorageItem.dbConfig + instance.id)\n  })\n}\n\nexport const handleDeleteInstances = (instances: Instance[]) => {\n  if (instances.length > 1) {\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED,\n      eventData: {\n        ids: instances.map((instance) => instance.id),\n      },\n    })\n  }\n\n  dispatch(deleteInstancesAction(instances, () => onDeleteInstances(instances)))\n}\n\nexport const handleExportInstances = (\n  instances: Instance[],\n  withSecrets: boolean,\n) => {\n  const ids = map(instances, 'id')\n\n  sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_REDIS_EXPORT_CLICKED,\n  })\n\n  dispatch(\n    exportInstancesAction(\n      ids,\n      withSecrets,\n      (data) => {\n        const file = new Blob([JSON.stringify(data, null, 2)], {\n          type: 'text/plain;charset=utf-8',\n        })\n        saveAs(file, `RedisInsight_connections_${Date.now()}.json`)\n\n        sendEventTelemetry({\n          event: TelemetryEvent.CONFIG_DATABASES_REDIS_EXPORT_SUCCEEDED,\n          eventData: {\n            numberOfDatabases: ids.length,\n          },\n        })\n      },\n      () => {\n        sendEventTelemetry({\n          event: TelemetryEvent.CONFIG_DATABASES_REDIS_EXPORT_FAILED,\n          eventData: {\n            numberOfDatabases: ids.length,\n          },\n        })\n      },\n    ),\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellConnectionType/DatabasesListCellConnectionType.tsx",
    "content": "import { capitalize } from 'lodash'\n\nimport { CONNECTION_TYPE_DISPLAY } from 'uiSrc/slices/interfaces'\n\nimport { IDatabaseListCell } from '../../DatabasesList.types'\n\nconst DatabasesListCellConnectionType: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const { connectionType } = instance\n\n  return CONNECTION_TYPE_DISPLAY[connectionType!] || capitalize(connectionType)\n}\n\nexport default DatabasesListCellConnectionType\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellControls/DatabasesListCellControls.spec.tsx",
    "content": "import React from 'react'\nimport {\n  render,\n  screen,\n  userEvent,\n  waitForRiPopoverVisible,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport {\n  HomePageDataProviderProvider,\n  useHomePageDataProvider,\n  OpenDialogName,\n} from 'uiSrc/pages/home/contexts/HomePageDataProvider'\n\nimport DatabasesListCellControls from './DatabasesListCellControls'\n\n// Handlers mocks\nconst mockHandleManageInstanceTags = jest.fn()\nconst mockHandleClickGoToCloud = jest.fn()\nconst mockHandleClickEditInstance = jest.fn()\nconst mockHandleDeleteInstances = jest.fn()\nconst mockHandleClickDeleteInstance = jest.fn()\n\njest.mock('./methods/handlers', () => ({\n  handleManageInstanceTags: (...args: any[]) =>\n    mockHandleManageInstanceTags(...args),\n  handleClickGoToCloud: (...args: any[]) => mockHandleClickGoToCloud(...args),\n  handleClickEditInstance: (...args: any[]) =>\n    mockHandleClickEditInstance(...args),\n  handleDeleteInstances: (...args: any[]) => mockHandleDeleteInstances(...args),\n  handleClickDeleteInstance: (...args: any[]) =>\n    mockHandleClickDeleteInstance(...args),\n}))\n\nconst instance: Instance = {\n  id: 'db-1',\n  name: 'My DB',\n  host: 'h',\n  port: 6379,\n  modules: [],\n  version: null,\n  provider: 'LOCALHOST',\n  cloudDetails: { test: true } as any,\n}\n\nconst row = { original: instance }\n\nconst openDialogSpy = jest.fn()\nconst Observer = () => {\n  const { openDialog } = useHomePageDataProvider()\n  React.useEffect(() => {\n    openDialogSpy(openDialog)\n  }, [openDialog])\n  return null\n}\n\nconst renderWithProvider = () =>\n  render(\n    <HomePageDataProviderProvider>\n      <>\n        <Observer />\n        <DatabasesListCellControls {...({ row } as any)} />\n      </>\n    </HomePageDataProviderProvider>,\n  )\n\ndescribe('DatabasesListCellControls component', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should show manage tags and call handler + open dialog', async () => {\n    renderWithProvider()\n\n    const btn = await screen.findByTestId(`manage-instance-tags-${instance.id}`)\n    await userEvent.click(btn)\n\n    expect(mockHandleManageInstanceTags).toHaveBeenCalledWith(instance)\n    expect(openDialogSpy).toHaveBeenLastCalledWith(OpenDialogName.ManageTags)\n  })\n\n  it('should show cloud link and call go-to-cloud handler', async () => {\n    renderWithProvider()\n\n    const link = await screen.findByTestId(`cloud-link-${instance.id}`)\n    await userEvent.click(link)\n\n    expect(mockHandleClickGoToCloud).toHaveBeenCalled()\n  })\n\n  it('should call edit handler and open edit dialog from popover', async () => {\n    renderWithProvider()\n\n    await userEvent.click(screen.getByTestId(`controls-button-${instance.id}`))\n    await waitForRiPopoverVisible()\n    const editBtn = await screen.findByTestId(`edit-instance-${instance.id}`)\n    await userEvent.click(editBtn)\n\n    expect(mockHandleClickEditInstance).toHaveBeenCalledWith(instance)\n    expect(openDialogSpy).toHaveBeenLastCalledWith(OpenDialogName.EditDatabase)\n  })\n\n  it('should trigger delete click and confirm delete', async () => {\n    renderWithProvider()\n\n    await userEvent.click(screen.getByTestId(`controls-button-${instance.id}`))\n    await waitForRiPopoverVisible()\n    const trigger = await screen.findByTestId(\n      `delete-instance-${instance.id}-icon`,\n    )\n    await userEvent.click(trigger)\n    expect(mockHandleClickDeleteInstance).toHaveBeenCalledWith(instance)\n\n    await waitForRiPopoverVisible()\n    const confirm = await screen.findByTestId(`delete-instance-${instance.id}`)\n    await userEvent.click(confirm, { pointerEventsCheck: 0 })\n\n    expect(mockHandleDeleteInstances).toHaveBeenCalledWith(instance)\n    expect(openDialogSpy).toHaveBeenLastCalledWith(null)\n  })\n\n  it('should close controls popover after confirming delete (closeControlsPopover)', async () => {\n    renderWithProvider()\n\n    // Open controls popover\n    await userEvent.click(screen.getByTestId(`controls-button-${instance.id}`))\n    await waitForRiPopoverVisible()\n    expect(\n      await screen.findByTestId(`edit-instance-${instance.id}`),\n    ).toBeInTheDocument()\n\n    // Trigger delete flow\n    await userEvent.click(\n      await screen.findByTestId(`delete-instance-${instance.id}-icon`),\n    )\n    await waitForRiPopoverVisible() // wait for confirmation popover\n\n    // Confirm deletion -> this calls closeControlsPopover inside handleDeleteItem\n    await userEvent.click(\n      await screen.findByTestId(`delete-instance-${instance.id}`),\n      { pointerEventsCheck: 0 },\n    )\n\n    // Assert the controls popover content is gone (popover closed)\n    await waitFor(() =>\n      expect(\n        screen.queryByTestId(`edit-instance-${instance.id}`),\n      ).not.toBeInTheDocument(),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellControls/DatabasesListCellControls.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\n\nexport const HoverableIconButton = styled(IconButton)`\n  transition: opacity 200ms ease-in-out;\n  opacity: 0;\n\n  tr:hover & {\n    opacity: 1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellControls/DatabasesListCellControls.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { FeatureFlagComponent, RiPopover, RiTooltip } from 'uiSrc/components'\nimport { EmptyButton, IconButton } from 'uiSrc/components/base/forms/buttons'\n\nimport { IDatabaseListCell } from '../../DatabasesList.types'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport {\n  RiIcon,\n  TagIcon,\n  EditIcon,\n  MoreactionsIcon,\n} from 'uiSrc/components/base/icons'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport { formatLongName } from 'uiSrc/utils'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { HoverableIconButton } from './DatabasesListCellControls.styles'\nimport {\n  OpenDialogName,\n  useHomePageDataProvider,\n} from 'uiSrc/pages/home/contexts/HomePageDataProvider'\nimport {\n  handleManageInstanceTags,\n  handleClickGoToCloud,\n  handleClickEditInstance,\n  handleDeleteInstances,\n  handleClickDeleteInstance,\n} from './methods/handlers'\n\nconst suffix = '_db_instance'\n\nconst DatabasesListCellControls: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const { setOpenDialog } = useHomePageDataProvider()\n  const [isControlsPopoverOpen, setControlsPopoverOpen] = useState(false)\n  const [isDeletePopoverOpen, setIsDeletePopoverOpen] = useState(false)\n\n  const deletingId = isDeletePopoverOpen ? `${instance.id + suffix}` : ''\n  const closeControlsPopover = () => setControlsPopoverOpen(false)\n  const closeDeletePopover = () => setIsDeletePopoverOpen(false)\n  const showDeletePopover = () => setIsDeletePopoverOpen(true)\n\n  return (\n    <Row\n      justify=\"end\"\n      align=\"center\"\n      gap=\"xs\"\n      onClick={(e) => e.stopPropagation()}\n    >\n      <FeatureFlagComponent name={FeatureFlags.databaseManagement}>\n        <RiTooltip content=\"Manage Tags\">\n          <HoverableIconButton\n            icon={TagIcon}\n            aria-label=\"Manage Instance Tags\"\n            data-testid={`manage-instance-tags-${instance.id}`}\n            onClick={() => {\n              handleManageInstanceTags(instance)\n              setOpenDialog(OpenDialogName.ManageTags)\n            }}\n          />\n        </RiTooltip>\n      </FeatureFlagComponent>\n      {instance.cloudDetails && (\n        <RiTooltip content=\"Go to Redis Cloud\">\n          <Link\n            target=\"_blank\"\n            href={EXTERNAL_LINKS.cloudConsole}\n            onClick={handleClickGoToCloud}\n            data-testid={`cloud-link-${instance.id}`}\n          >\n            <RiIcon type=\"CloudLinkIcon\" fill=\"currentColor\" size={'m'} />\n          </Link>\n        </RiTooltip>\n      )}\n      <FeatureFlagComponent name={FeatureFlags.databaseManagement}>\n        <RiPopover\n          ownFocus\n          anchorPosition=\"leftUp\"\n          panelPaddingSize=\"s\"\n          isOpen={isControlsPopoverOpen}\n          closePopover={closeControlsPopover}\n          onOpenChange={setControlsPopoverOpen}\n          button={\n            <IconButton\n              icon={MoreactionsIcon}\n              aria-label=\"Controls icon\"\n              data-testid={`controls-button-${instance.id}`}\n            />\n          }\n          data-testid={`controls-popover-${instance.id}`}\n        >\n          <Col>\n            <EmptyButton\n              justify=\"start\"\n              icon={EditIcon}\n              className=\"editInstanceBtn\"\n              aria-label=\"Edit instance\"\n              onClick={() => {\n                handleClickEditInstance(instance)\n                setOpenDialog(OpenDialogName.EditDatabase) // if instance?\n              }}\n              data-testid={`edit-instance-${instance.id}`}\n            >\n              Edit database\n            </EmptyButton>\n            <PopoverDelete\n              header={formatLongName(instance.name, 50, 10, '...')}\n              text=\"will be removed from Redis Insight.\"\n              item={instance.id}\n              suffix={suffix}\n              deleting={deletingId}\n              closePopover={closeDeletePopover}\n              updateLoading={false}\n              showPopover={showDeletePopover}\n              handleDeleteItem={() => {\n                handleDeleteInstances(instance)\n                setOpenDialog(null)\n                closeControlsPopover()\n              }}\n              handleButtonClick={() => handleClickDeleteInstance(instance)}\n              testid={`delete-instance-${instance.id}`}\n              buttonLabel=\"Remove database\"\n            />\n          </Col>\n        </RiPopover>\n      </FeatureFlagComponent>\n    </Row>\n  )\n}\n\nexport default DatabasesListCellControls\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellControls/methods/handlers.spec.ts",
    "content": "import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\n\nimport {\n  handleDeleteInstances,\n  handleClickDeleteInstance,\n  handleClickGoToCloud,\n  handleClickEditInstance,\n  handleManageInstanceTags,\n} from './handlers'\n\njest.mock('uiSrc/slices/store', () => ({\n  dispatch: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => {\n  const actual = jest.requireActual('uiSrc/telemetry')\n  return { ...actual, sendEventTelemetry: jest.fn() }\n})\n\nconst mockDeleteInstancesAction = jest.fn(\n  (instances: Instance[], cb?: () => void) => ({\n    type: 'MOCK_DELETE',\n    payload: { instances, cb },\n  }),\n)\nconst mockFetchEditedInstanceAction = jest.fn((instance: Instance) => ({\n  type: 'MOCK_FETCH_EDITED',\n  payload: instance,\n}))\nconst mockSetEditedInstance = jest.fn((instance: Instance | null) => ({\n  type: 'MOCK_SET_EDITED',\n  payload: instance,\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  deleteInstancesAction: (...args: any[]) =>\n    // @ts-expect-error\n    mockDeleteInstancesAction(...(args as any)),\n  fetchEditedInstanceAction: (...args: any[]) =>\n    // @ts-expect-error\n    mockFetchEditedInstanceAction(...(args as any)),\n  setEditedInstance: (...args: any[]) =>\n    // @ts-expect-error\n    mockSetEditedInstance(...(args as any)),\n}))\n\nconst instance: Instance = DBInstanceFactory.build()\n\ndescribe('DatabasesListCellControls handlers', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('handleDeleteInstances should dispatch delete with single instance', () => {\n    handleDeleteInstances(instance)\n\n    expect(mockDeleteInstancesAction).toHaveBeenCalledWith(\n      [instance],\n      expect.any(Function),\n    )\n    expect(dispatch).toHaveBeenCalledWith(\n      expect.objectContaining({ type: 'MOCK_DELETE' }),\n    )\n  })\n\n  it('handleClickDeleteInstance should send telemetry', () => {\n    handleClickDeleteInstance(instance)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_SINGLE_DATABASE_DELETE_CLICKED,\n        eventData: expect.objectContaining({\n          databaseId: instance.id,\n          provider: instance.provider,\n        }),\n      }),\n    )\n  })\n\n  it('handleClickGoToCloud should send telemetry', () => {\n    handleClickGoToCloud()\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({ event: TelemetryEvent.CLOUD_LINK_CLICKED }),\n    )\n  })\n\n  it('handleClickEditInstance should send telemetry and dispatch fetch', () => {\n    handleClickEditInstance(instance)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CLICKED,\n        eventData: expect.objectContaining({\n          databaseId: instance.id,\n          provider: instance.provider,\n        }),\n      }),\n    )\n\n    expect(mockFetchEditedInstanceAction).toHaveBeenCalledWith(instance)\n    expect(dispatch).toHaveBeenCalledWith(\n      expect.objectContaining({ type: 'MOCK_FETCH_EDITED' }),\n    )\n  })\n\n  it('handleManageInstanceTags should send telemetry and set edited instance', () => {\n    handleManageInstanceTags(instance)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_DATABASE_MANAGE_TAGS_CLICKED,\n        eventData: expect.objectContaining({\n          databaseId: instance.id,\n          provider: instance.provider,\n        }),\n      }),\n    )\n    expect(mockSetEditedInstance).toHaveBeenCalledWith(instance)\n    expect(dispatch).toHaveBeenCalledWith(\n      expect.objectContaining({ type: 'MOCK_SET_EDITED' }),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellControls/methods/handlers.ts",
    "content": "import { Instance } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  deleteInstancesAction,\n  fetchEditedInstanceAction,\n  setEditedInstance,\n} from 'uiSrc/slices/instances/instances'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { localStorageService } from 'uiSrc/services'\n\nconst onDeleteInstances = (instances: Instance[]) => {\n  dispatch(setEditedInstance(null))\n\n  instances.forEach((instance) => {\n    localStorageService.remove(BrowserStorageItem.dbConfig + instance.id)\n  })\n}\n\nexport const handleDeleteInstances = (instances: Instance) => {\n  dispatch(\n    deleteInstancesAction([instances], () => onDeleteInstances([instances])),\n  )\n}\n\nexport const handleClickDeleteInstance = ({ id, provider }: Instance) => {\n  sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_SINGLE_DATABASE_DELETE_CLICKED,\n    eventData: {\n      databaseId: id,\n      provider,\n    },\n  })\n}\n\nexport const handleClickGoToCloud = () => {\n  sendEventTelemetry({\n    event: TelemetryEvent.CLOUD_LINK_CLICKED,\n  })\n}\n\nexport const handleClickEditInstance = (instance: Instance) => {\n  sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_DATABASE_EDIT_CLICKED,\n    eventData: {\n      databaseId: instance.id,\n      provider: instance.provider,\n    },\n  })\n\n  if (instance) {\n    dispatch(fetchEditedInstanceAction(instance))\n  }\n}\n\nexport const handleManageInstanceTags = (instance: Instance) => {\n  sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_DATABASE_MANAGE_TAGS_CLICKED,\n    eventData: {\n      databaseId: instance.id,\n      provider: instance.provider,\n    },\n  })\n  dispatch(setEditedInstance(instance))\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellHost/DatabasesListCellHost.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const HostPortContainer = styled(Row)`\n  button {\n    opacity: 0;\n    transition: opacity 250ms ease-in-out;\n  }\n\n  &:hover button {\n    opacity: 1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellHost/DatabasesListCellHost.tsx",
    "content": "import React from 'react'\n\nimport { CopyButton } from 'uiSrc/components/copy-button'\n\nimport { sendCopyTelemetry } from './methods/handlers'\nimport { IDatabaseListCell } from '../../DatabasesList.types'\nimport { HostPortContainer } from './DatabasesListCellHost.styles'\nimport { CellText } from 'uiSrc/components/auto-discover'\n\nconst DatabasesListCellHost: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const { host, port, id } = instance\n  const text = `${host}:${port}`\n\n  return (\n    <HostPortContainer gap=\"m\" data-testid=\"host-port\">\n      <CellText>{text}</CellText>\n      <CopyButton\n        copy={text}\n        aria-label=\"Copy host:port\"\n        onCopy={() => sendCopyTelemetry(id)}\n      />\n    </HostPortContainer>\n  )\n}\n\nexport default DatabasesListCellHost\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellHost/methods/handlers.spec.ts",
    "content": "import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\n\nimport { sendCopyTelemetry, handleCopyToClipboard } from './handlers'\n\njest.mock('uiSrc/telemetry', () => {\n  const actual = jest.requireActual('uiSrc/telemetry')\n  return { ...actual, sendEventTelemetry: jest.fn() }\n})\n\ndescribe('DatabasesListCellHost handlers', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n    ;(navigator as any).clipboard = { writeText: jest.fn() }\n  })\n\n  describe('handleCopy', () => {\n    it('should send telemetry with databaseId', () => {\n      sendCopyTelemetry('db-1')\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.CONFIG_DATABASES_HOST_PORT_COPIED,\n        eventData: { databaseId: 'db-1' },\n      })\n    })\n\n    it('should send telemetry with undefined databaseId when not provided', () => {\n      sendCopyTelemetry()\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.CONFIG_DATABASES_HOST_PORT_COPIED,\n        eventData: { databaseId: undefined },\n      })\n    })\n  })\n\n  describe('handleCopyToClipboard', () => {\n    it('should stop propagation, copy and send telemetry', () => {\n      const stopPropagation = jest.fn()\n      const e = { stopPropagation } as any\n\n      handleCopyToClipboard(e, 'host:6379', 'db-1')\n\n      expect(stopPropagation).toHaveBeenCalled()\n      expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith(\n        'host:6379',\n      )\n      expect(sendEventTelemetry).toHaveBeenCalledWith(\n        expect.objectContaining({\n          event: TelemetryEvent.CONFIG_DATABASES_HOST_PORT_COPIED,\n          eventData: { databaseId: 'db-1' },\n        }),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellHost/methods/handlers.ts",
    "content": "import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { handleCopy } from 'uiSrc/utils'\n\nexport const sendCopyTelemetry = async (databaseId?: string) => {\n  return sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_HOST_PORT_COPIED,\n    eventData: {\n      databaseId,\n    },\n  })\n}\n\nexport const handleCopyToClipboard = (\n  e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  text = '',\n  databaseId?: string,\n) => {\n  e.stopPropagation()\n  handleCopy(text)\n  sendCopyTelemetry(databaseId)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellLastConnection/DatabasesListCellLastConnection.tsx",
    "content": "import { lastConnectionFormat } from 'uiSrc/utils'\n\nimport { IDatabaseListCell } from '../../DatabasesList.types'\n\nconst DatabasesListCellLastConnection: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const { lastConnection } = instance\n\n  return lastConnectionFormat(lastConnection)\n}\n\nexport default DatabasesListCellLastConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellModules/DatabasesListCellModules.tsx",
    "content": "import React from 'react'\n\nimport { DatabaseListModules } from 'uiSrc/components'\n\nimport { IDatabaseListCell } from '../../DatabasesList.types'\n\nconst DatabasesListCellModules: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const { modules = [] } = instance\n\n  return <DatabaseListModules maxViewModules={5} modules={modules} />\n}\n\nexport default DatabasesListCellModules\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledCellNameWrapper = styled(Row)`\n  > span {\n    overflow: hidden;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellName/DatabasesListCellName.tsx",
    "content": "import React from 'react'\n\nimport { IDatabaseListCell } from '../../DatabasesList.types'\nimport { RiTooltip } from 'uiSrc/components'\nimport { replaceSpaces, formatLongName, getDbIndex } from 'uiSrc/utils'\nimport DbStatus from 'uiSrc/pages/home/components/db-status'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { StyledCellNameWrapper } from './DatabasesListCellName.styles'\n\nconst DatabasesListCellName: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const {\n    id,\n    db,\n    name = '',\n    new: newStatus = false,\n    lastConnection,\n    createdAt,\n    cloudDetails,\n  } = instance\n  const cellContent = replaceSpaces(name.substring(0, 200))\n\n  return (\n    <StyledCellNameWrapper role=\"presentation\" align=\"center\" gap=\"xs\">\n      <div>\n        <DbStatus\n          id={id}\n          isNew={newStatus}\n          lastConnection={lastConnection}\n          createdAt={createdAt}\n          isFree={cloudDetails?.free}\n        />\n      </div>\n\n      <RiTooltip\n        position=\"bottom\"\n        title=\"Database Alias\"\n        content={`${formatLongName(name)} ${getDbIndex(db)}`}\n      >\n        <Text\n          data-testid={`instance-name-${id}`}\n          color=\"primary\"\n          ellipsis\n          style={{\n            overflow: 'hidden',\n          }}\n        >\n          {cellContent}\n          {` ${getDbIndex(db)}`}\n        </Text>\n      </RiTooltip>\n    </StyledCellNameWrapper>\n  )\n}\n\nexport default DatabasesListCellName\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/components/DatabasesListCellTags/DatabasesListCellTags.tsx",
    "content": "import React from 'react'\n\nimport { IDatabaseListCell } from '../../DatabasesList.types'\nimport { TagsCell } from '../../../tags-cell/TagsCell'\n\nconst DatabasesListCellTags: IDatabaseListCell = ({ row }) => {\n  const instance = row.original\n  const { tags = [] } = instance\n\n  return <TagsCell tags={tags} />\n}\n\nexport default DatabasesListCellTags\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/hooks/useDatabaseListData.spec.ts",
    "content": "import { act } from '@testing-library/react'\n\nimport {\n  mockStore,\n  initialStateDefault,\n  renderHook,\n} from 'uiSrc/utils/test-utils'\nimport { DatabaseListColumn } from 'uiSrc/constants'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\n\nimport { SELECT_COL_ID, ENABLE_PAGINATION_COUNT } from '../DatabasesList.config'\n\nimport useDatabaseListData from './useDatabaseListData'\n\nconst getStoreWith = ({\n  instances,\n  loading = false,\n  shownColumns = [\n    DatabaseListColumn.Name,\n    DatabaseListColumn.Host,\n    DatabaseListColumn.ConnectionType,\n    DatabaseListColumn.LastConnection,\n    DatabaseListColumn.Modules,\n    DatabaseListColumn.Tags,\n    DatabaseListColumn.Controls,\n  ],\n}: {\n  instances: Instance[]\n  loading?: boolean\n  shownColumns?: DatabaseListColumn[]\n}) => {\n  const state = {\n    ...initialStateDefault,\n    connections: {\n      ...initialStateDefault.connections,\n      instances: {\n        ...initialStateDefault.connections.instances,\n        data: instances,\n        loading,\n        shownColumns,\n      },\n    },\n  } as typeof initialStateDefault\n\n  return mockStore(state)\n}\n\nconst mockInstances: Instance[] = [\n  DBInstanceFactory.build({ visible: true }),\n  DBInstanceFactory.build({ visible: false }),\n  DBInstanceFactory.build({ visible: true }),\n]\n\ndescribe('useDatabaseListData', () => {\n  it('should expose loading state', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should return only visible instances', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.visibleInstances).toEqual([\n      mockInstances[0],\n      mockInstances[2],\n    ])\n  })\n\n  it('should filter columns based on shownColumns', () => {\n    const store = getStoreWith({\n      instances: mockInstances,\n      shownColumns: [DatabaseListColumn.Name, DatabaseListColumn.Host],\n    })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    const ids = result.current.columns.map((c) => c.id)\n    expect(ids).toEqual([\n      SELECT_COL_ID,\n      DatabaseListColumn.Name,\n      DatabaseListColumn.Host,\n    ])\n  })\n\n  it('should return empty selected instances when no selection', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.selectedInstances).toEqual([])\n  })\n\n  it('should return selected instances based on rowSelection', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    act(() => {\n      result.current.setRowSelection({ 0: true })\n    })\n\n    expect(result.current.selectedInstances).toEqual([mockInstances[0]])\n\n    act(() => {\n      result.current.setRowSelection({ 0: true, 1: true })\n    })\n\n    // index 1 corresponds to second visible instance (mockInstances[2])\n    expect(result.current.selectedInstances).toEqual([\n      mockInstances[0],\n      mockInstances[2],\n    ])\n  })\n\n  it('should reset row selection', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    act(() => {\n      result.current.setRowSelection({ 0: true, 1: true })\n    })\n    expect(Object.keys(result.current.rowSelection)).toHaveLength(2)\n\n    act(() => {\n      result.current.resetRowSelection()\n    })\n    expect(result.current.rowSelection).toEqual({})\n  })\n\n  it('should return \"Loading...\" message when loading', () => {\n    const store = getStoreWith({ instances: mockInstances, loading: true })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.emptyMessage).toBe('Loading...')\n  })\n\n  it('should return \"No added instances\" message when no instances', () => {\n    const store = getStoreWith({ instances: [] })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.emptyMessage).toBe('No added instances')\n  })\n\n  it('should return \"No results found\" message when instances exist but none visible', () => {\n    const instances: Instance[] = [\n      {\n        id: '1',\n        name: 'Hidden',\n        host: 'h',\n        port: 6379,\n        modules: [],\n        version: null,\n        visible: false,\n      },\n    ]\n    const store = getStoreWith({ instances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.emptyMessage).toBe('No results found')\n  })\n\n  it('should include select column even when no columns are shown', () => {\n    const store = getStoreWith({ instances: mockInstances, shownColumns: [] })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    const ids = result.current.columns.map((c) => c.id)\n    expect(ids).toEqual([SELECT_COL_ID])\n  })\n\n  it('should disable pagination when instances count <= threshold', () => {\n    const instances: Instance[] = Array.from(\n      { length: ENABLE_PAGINATION_COUNT },\n      (_, i) => ({\n        id: `${i + 1}`,\n        name: `Instance ${i + 1}`,\n        host: 'h',\n        port: 6379,\n        modules: [],\n        version: null,\n        visible: true,\n      }),\n    )\n    const store = getStoreWith({ instances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.paginationEnabled).toBe(false)\n  })\n\n  it('should enable pagination when instances count > threshold', () => {\n    const instances: Instance[] = Array.from(\n      { length: ENABLE_PAGINATION_COUNT + 1 },\n      (_, i) => ({\n        id: `${i + 1}`,\n        name: `Instance ${i + 1}`,\n        host: 'h',\n        port: 6379,\n        modules: [],\n        version: null,\n        visible: true,\n      }),\n    )\n    const store = getStoreWith({ instances })\n\n    const { result } = renderHook(() => useDatabaseListData(), { store })\n\n    expect(result.current.paginationEnabled).toBe(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/hooks/useDatabaseListData.ts",
    "content": "import { useMemo, useState, useCallback, useRef } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { instancesSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  ColumnDef,\n  RowSelectionState,\n} from 'uiSrc/components/base/layout/table'\nimport { DatabaseListColumn } from 'uiSrc/constants'\n\nimport {\n  SELECT_COL_ID,\n  BASE_COLUMNS,\n  ENABLE_PAGINATION_COUNT,\n} from '../DatabasesList.config'\n\nconst useDatabaseListData = () => {\n  const {\n    data: instances,\n    loading,\n    shownColumns,\n  } = useSelector(instancesSelector)\n  const [rowSelection, setRowSelection] = useState<RowSelectionState>({})\n  const resetRowSelection = useCallback(() => {\n    setRowSelection({})\n  }, [])\n\n  const paginationEnabledRef = useRef(false)\n  paginationEnabledRef.current =\n    // Workaround: table breaks if pagination is disabled after it was previously enabled\n    paginationEnabledRef.current || instances.length > ENABLE_PAGINATION_COUNT\n\n  const columns: ColumnDef<Instance>[] = useMemo(\n    () =>\n      BASE_COLUMNS.filter(\n        (col) =>\n          col.id === SELECT_COL_ID ||\n          shownColumns.includes(col.id as DatabaseListColumn),\n      ),\n    [shownColumns],\n  )\n\n  const visibleInstances = useMemo(\n    () => instances.filter(({ visible = true }) => visible),\n    [instances],\n  )\n\n  const selectedInstances = useMemo(\n    () => visibleInstances.filter((_instance, index) => rowSelection[index]),\n    [rowSelection, visibleInstances],\n  )\n\n  const emptyMessage = useMemo(() => {\n    if (loading) {\n      return 'Loading...'\n    }\n    if (!instances.length) {\n      return 'No added instances'\n    }\n    return 'No results found'\n  }, [loading, instances.length])\n\n  return {\n    loading,\n    columns,\n    visibleInstances,\n    selectedInstances,\n    paginationEnabled: paginationEnabledRef.current,\n    rowSelection,\n    emptyMessage,\n    setRowSelection,\n    resetRowSelection,\n  }\n}\n\nexport default useDatabaseListData\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.spec.ts",
    "content": "import {\n  TelemetryEvent,\n  sendEventTelemetry,\n  getRedisInfoSummary,\n  getRedisModulesSummary,\n} from 'uiSrc/telemetry'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { Pages, BrowserStorageItem } from 'uiSrc/constants'\n\nimport { handleCheckConnectToInstance, handleSortingChange } from './handlers'\n\njest.mock('uiSrc/slices/store', () => ({\n  dispatch: jest.fn(),\n  store: { getState: jest.fn() },\n}))\n\njest.mock('uiSrc/telemetry', () => {\n  const actual = jest.requireActual('uiSrc/telemetry')\n  return {\n    ...actual,\n    sendEventTelemetry: jest.fn(),\n    getRedisInfoSummary: jest.fn(),\n    getRedisModulesSummary: jest.fn(),\n  }\n})\n\nconst mockCheckConnectToInstanceAction = jest.fn(\n  (id: string, onSuccess?: (id: string) => void) => {\n    onSuccess?.(id) // simulate success immediately\n    return { type: 'MOCK_CHECK_CONNECT', payload: { id } }\n  },\n)\nconst mockSetConnectedInstanceId = jest.fn((id: string) => ({\n  type: 'MOCK_SET_CONN_ID',\n  payload: id,\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  checkConnectToInstanceAction: (...args: any[]) =>\n    // @ts-expect-error\n    mockCheckConnectToInstanceAction(...(args as any)),\n  setConnectedInstanceId: (...args: any[]) =>\n    // @ts-expect-error\n    mockSetConnectedInstanceId(...(args as any)),\n}))\n\nconst mockResetRdiContext = jest.fn(() => ({ type: 'MOCK_RESET_RDI' }))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  appContextSelector: jest.fn(() => ({ contextInstanceId: 'ctx-1' })),\n  // @ts-expect-error\n  resetRdiContext: (...args: any[]) => mockResetRdiContext(...(args as any)),\n}))\n\nconst mockHistoryPush = jest.fn()\njest.mock('uiSrc/Router', () => ({\n  navigate: jest.fn((...args) => mockHistoryPush(...args)),\n}))\n\nconst mockLocalStorageSet = jest.fn()\njest.mock('uiSrc/services', () => {\n  const actual = jest.requireActual('uiSrc/services')\n  return {\n    ...actual,\n    localStorageService: {\n      ...actual.localStorageService,\n      set: (...args: any[]) => mockLocalStorageSet(...(args as any)),\n    },\n  }\n})\n\nconst instance = {\n  id: '1',\n  provider: 'LOCALHOST',\n  modules: [],\n}\n\ndescribe('databases-list methods/handlers', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n    ;(getRedisInfoSummary as jest.Mock).mockResolvedValue({\n      redis_version: '7.2',\n    })\n    ;(getRedisModulesSummary as jest.Mock).mockReturnValue({\n      RedisJSON: { loaded: false },\n      customModules: [],\n    })\n  })\n\n  it('handleCheckConnectToInstance should send telemetry and navigate on success', async () => {\n    await handleCheckConnectToInstance(instance as any)\n\n    // send telemetry with event\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE,\n        eventData: expect.objectContaining({\n          databaseId: '1',\n          provider: 'LOCALHOST',\n        }),\n      }),\n    )\n\n    // dispatch reset and set connected id\n    expect(mockResetRdiContext).toHaveBeenCalled()\n    expect(mockSetConnectedInstanceId).toHaveBeenCalledWith('1')\n\n    // navigate\n    expect(mockHistoryPush).toHaveBeenCalledWith(Pages.browser('1'))\n\n    // check connect action dispatched\n    expect(dispatch).toHaveBeenCalledWith(\n      expect.objectContaining({ type: 'MOCK_CHECK_CONNECT' }),\n    )\n  })\n\n  it('handleSortingChange should set sort and send telemetry', () => {\n    handleSortingChange([{ id: 'name', desc: false }])\n\n    expect(mockLocalStorageSet).toHaveBeenCalledWith(\n      BrowserStorageItem.instancesSorting,\n      { field: 'name', direction: 'asc' },\n    )\n    expect(sendEventTelemetry).toHaveBeenCalledWith(\n      expect.objectContaining({\n        event: TelemetryEvent.CONFIG_DATABASES_DATABASE_LIST_SORTED,\n        eventData: { field: 'name', direction: 'asc' },\n      }),\n    )\n  })\n\n  it('handleSortingChange should do nothing when sorting is empty', () => {\n    handleSortingChange([])\n\n    expect(mockLocalStorageSet).not.toHaveBeenCalled()\n    expect(sendEventTelemetry).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/databases-list/methods/handlers.ts",
    "content": "import { Instance } from 'uiSrc/slices/interfaces'\nimport {\n  getRedisInfoSummary,\n  getRedisModulesSummary,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  checkConnectToInstanceAction,\n  setConnectedInstanceId,\n} from 'uiSrc/slices/instances/instances'\nimport { appContextSelector, resetRdiContext } from 'uiSrc/slices/app/context'\nimport { BrowserStorageItem, Pages } from 'uiSrc/constants'\nimport { store, dispatch } from 'uiSrc/slices/store'\nimport {\n  SortingState,\n  PaginationState,\n} from 'uiSrc/components/base/layout/table'\nimport { navigate } from 'uiSrc/Router'\nimport { TableStorageKey } from 'uiSrc/constants/storage'\nimport {\n  getObjectStorageField,\n  localStorageService,\n  setObjectStorageField,\n} from 'uiSrc/services'\n\nconst connectToInstance = (id: string) => {\n  dispatch(resetRdiContext())\n  dispatch(setConnectedInstanceId(id))\n\n  navigate(Pages.browser(id))\n}\n\nexport const handleCheckConnectToInstance = async (instance: Instance) => {\n  const { id, provider, modules } = instance\n  const { contextInstanceId } = appContextSelector(store.getState())\n\n  dispatch(\n    checkConnectToInstanceAction(\n      id,\n      connectToInstance,\n      undefined,\n      contextInstanceId !== id,\n    ),\n  )\n\n  const modulesSummary = getRedisModulesSummary(modules)\n  const infoData = await getRedisInfoSummary(id)\n\n  sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_OPEN_DATABASE,\n    eventData: {\n      databaseId: id,\n      provider,\n      source: 'db_list',\n      ...modulesSummary,\n      ...infoData,\n    },\n  })\n}\n\nexport const handleSortingChange = (sorting: SortingState) => {\n  if (!sorting.length) {\n    return\n  }\n\n  const sort = {\n    field: sorting[0].id,\n    direction: sorting[0].desc ? 'desc' : 'asc',\n  }\n  localStorageService.set(BrowserStorageItem.instancesSorting, sort)\n  sendEventTelemetry({\n    event: TelemetryEvent.CONFIG_DATABASES_DATABASE_LIST_SORTED,\n    eventData: sort,\n  })\n}\n\nexport const handlePaginationChange = (paginationState: PaginationState) =>\n  setObjectStorageField(\n    BrowserStorageItem.tablePaginationState,\n    TableStorageKey.dbList,\n    paginationState,\n  )\n\nexport const getDefaultPagination = () =>\n  getObjectStorageField(\n    BrowserStorageItem.tablePaginationState,\n    TableStorageKey.dbList,\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/db-status/DbStatus.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport DbStatus, { Props, WarningTypes } from './DbStatus'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockedProps = mock<Props>()\nconst daysToMs = (days: number) => days * 60 * 60 * 24 * 1000\nlet mockDate: Date\n\ndescribe('DbStatus', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    // Set up fake timers\n    jest.useFakeTimers()\n    mockDate = new Date('2024-11-22T12:00:00Z')\n    jest.setSystemTime(mockDate)\n  })\n\n  afterEach(() => {\n    jest.useRealTimers()\n  })\n\n  it('should render', () => {\n    expect(render(<DbStatus {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should not render any status', () => {\n    render(<DbStatus {...mockedProps} id=\"1\" />)\n\n    expect(\n      screen.queryByTestId('database-status-new-1'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId(`database-status-${WarningTypes.TryDatabase}-1`),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId(`database-status-${WarningTypes.CheckIfDeleted}-1`),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render TryDatabase status', () => {\n    const lastConnection = new Date(Date.now() - daysToMs(3))\n    render(\n      <DbStatus\n        {...mockedProps}\n        id=\"1\"\n        lastConnection={lastConnection}\n        isFree\n      />,\n    )\n\n    expect(\n      screen.getByTestId(`database-status-${WarningTypes.TryDatabase}-1`),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('database-status-new-1'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId(`database-status-${WarningTypes.CheckIfDeleted}-1`),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render CheckIfDeleted status', () => {\n    const lastConnection = new Date(Date.now() - daysToMs(16))\n    render(\n      <DbStatus\n        {...mockedProps}\n        id=\"1\"\n        lastConnection={lastConnection}\n        isFree\n        isNew\n      />,\n    )\n\n    expect(\n      screen.getByTestId(`database-status-${WarningTypes.CheckIfDeleted}-1`),\n    ).toBeInTheDocument()\n\n    expect(\n      screen.queryByTestId('database-status-new-1'),\n    ).not.toBeInTheDocument()\n    expect(\n      screen.queryByTestId(`database-status-${WarningTypes.TryDatabase}-1`),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render new status', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const lastConnection = new Date(Date.now() - daysToMs(3))\n    render(\n      <DbStatus\n        {...mockedProps}\n        id=\"1\"\n        lastConnection={lastConnection}\n        isFree\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.focus(\n        screen.getByTestId(`database-status-${WarningTypes.TryDatabase}-1`),\n      )\n    })\n\n    await waitForRiTooltipVisible(1_000)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED,\n      eventData: {\n        capability: expect.any(String),\n        databaseId: '1',\n        type: WarningTypes.TryDatabase,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/db-status/DbStatus.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nexport const IconWrapper = styled.div<{ children?: React.ReactNode }>`\n  margin-left: -${({ theme }) => theme.core.space.space150};\n`\n\nexport const InfoIcon = styled(RiIcon).attrs({\n  type: 'ToastInfoIcon',\n  size: 'S',\n  color: 'custom',\n})`\n  color: ${({ theme }) => theme.semantic.color.icon.attention500};\n  margin-left: -1px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/db-status/DbStatus.tsx",
    "content": "import React from 'react'\nimport { differenceInDays } from 'date-fns'\n\nimport { useSelector } from 'react-redux'\nimport { getTutorialCapability, Maybe } from 'uiSrc/utils'\n\nimport { appContextCapability } from 'uiSrc/slices/app/context'\nimport { isShowCapabilityTutorialPopover } from 'uiSrc/services'\nimport {\n  sendEventTelemetry,\n  TELEMETRY_EMPTY_VALUE,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Indicator } from 'uiSrc/components/base/text/text.styles'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport {\n  CHECK_CLOUD_DATABASE,\n  WARNING_WITH_CAPABILITY,\n  WARNING_WITHOUT_CAPABILITY,\n} from './texts'\nimport { IconWrapper, InfoIcon } from './DbStatus.styles'\n\nexport interface Props {\n  id: string\n  lastConnection: Maybe<Date>\n  createdAt: Maybe<Date>\n  isNew: boolean\n  isFree?: boolean\n}\n\nexport enum WarningTypes {\n  CheckIfDeleted = 'checkIfDeleted',\n  TryDatabase = 'tryDatabase',\n}\n\ninterface WarningTooltipProps {\n  id: string\n  content: React.ReactNode\n  capabilityTelemetry?: string\n  type?: string\n  isCapabilityNotShown?: boolean\n}\n\nconst LAST_CONNECTION_SM = 3\nconst LAST_CONNECTION_L = 16\n\nconst DbStatus = (props: Props) => {\n  const { id, lastConnection, createdAt, isNew, isFree } = props\n\n  const { source } = useSelector(appContextCapability)\n  const capability = getTutorialCapability(source!)\n  const isCapabilityNotShown = Boolean(isShowCapabilityTutorialPopover(isFree))\n  let daysDiff = 0\n\n  try {\n    daysDiff = lastConnection\n      ? differenceInDays(new Date(), new Date(lastConnection))\n      : createdAt\n        ? differenceInDays(new Date(), new Date(createdAt))\n        : 0\n  } catch {\n    // nothing to do\n  }\n\n  const renderWarningTooltip = (content: React.ReactNode, type?: string) => (\n    <RiTooltip\n      content={\n        <WarningTooltipContent\n          id={id}\n          capabilityTelemetry={capability?.telemetryName}\n          content={content}\n          type={type}\n          isCapabilityNotShown={isCapabilityNotShown}\n        />\n      }\n      position=\"right\"\n    >\n      <IconWrapper data-testid={`database-status-${type}-${id}`}>\n        <InfoIcon />\n      </IconWrapper>\n    </RiTooltip>\n  )\n\n  if (isFree && daysDiff >= LAST_CONNECTION_L) {\n    return renderWarningTooltip(\n      CHECK_CLOUD_DATABASE,\n      WarningTypes.CheckIfDeleted,\n    )\n  }\n\n  if (isFree && daysDiff >= LAST_CONNECTION_SM) {\n    return renderWarningTooltip(\n      isCapabilityNotShown && capability.name\n        ? WARNING_WITH_CAPABILITY(capability.name)\n        : WARNING_WITHOUT_CAPABILITY,\n      'tryDatabase',\n    )\n  }\n\n  if (isNew) {\n    return (\n      <RiTooltip content=\"New\" position=\"top\">\n        <IconWrapper data-testid={`database-status-new-${id}`}>\n          <Indicator $color=\"var(--euiColorPrimary)\" />\n        </IconWrapper>\n      </RiTooltip>\n    )\n  }\n\n  return null\n}\n\n// separated to send event when content is displayed\nconst WarningTooltipContent = (props: WarningTooltipProps) => {\n  const { id, content, capabilityTelemetry, type, isCapabilityNotShown } = props\n\n  sendEventTelemetry({\n    event: TelemetryEvent.CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED,\n    eventData: {\n      databaseId: id,\n      capability: isCapabilityNotShown\n        ? capabilityTelemetry\n        : TELEMETRY_EMPTY_VALUE,\n      type,\n    },\n  })\n\n  return <Col>{content}</Col>\n}\n\nexport default DbStatus\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/db-status/index.ts",
    "content": "import DbStatus from './DbStatus'\n\nexport default DbStatus\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/db-status/texts.tsx",
    "content": "import React from 'react'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Title } from 'uiSrc/components/base/text/Title'\n\nexport const CHECK_CLOUD_DATABASE = (\n  <>\n    <Title size=\"XS\">Build your app with Redis Cloud</Title>\n    <Spacer size=\"s\" />\n    <div>\n      Free Redis Cloud DBs auto-delete after 15 days of inactivity.\n      <Spacer size=\"s\" />\n      But not to worry, you can always re-create it to test your ideas.\n      <br />\n      Includes native support for JSON, Query Engine and more.\n    </div>\n  </>\n)\n\nexport const WARNING_WITH_CAPABILITY = (capability: string) => (\n  <>\n    <Title size=\"XS\">Build your app with {capability}</Title>\n    <Spacer size=\"s\" />\n    <div>\n      Hey, remember your interest in {capability}?\n      <br />\n      Use your free Redis Cloud DB to try it.\n    </div>\n    <Spacer size=\"s\" />\n    <div>\n      <b>Note</b>: Free Cloud DBs auto-delete after 15 days of inactivity.\n    </div>\n  </>\n)\nexport const WARNING_WITHOUT_CAPABILITY = (\n  <>\n    <Title size=\"XS\">Your free Redis Cloud DB is waiting.</Title>\n    <Spacer size=\"s\" />\n    <div>\n      Test ideas and build prototypes.\n      <br />\n      Includes native support for JSON, Query Engine and more.\n    </div>\n    <Spacer size=\"s\" />\n    <div>\n      <b>Note</b>: Free Redis Cloud DBs auto-delete after 15 days of inactivity.\n    </div>\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/empty-message/EmptyMessage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport EmptyMessage, { Props } from './EmptyMessage'\n\nconst mockedProps = mock<Props>()\n\ndescribe('EmptyMessage', () => {\n  it('should render', () => {\n    expect(render(<EmptyMessage {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/empty-message/EmptyMessage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Col)`\n  height: calc(100vh - 185px);\n  gap: ${({ theme }) => theme.core.space.space200};\n`\n\nexport const Icon = styled.img`\n  width: 12rem;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/empty-message/EmptyMessage.tsx",
    "content": "import React from 'react'\n\nimport CakeIcon from 'uiSrc/assets/img/databases/cake.svg'\n\nimport { OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Title } from 'uiSrc/components/base/text'\nimport * as S from './EmptyMessage.styles'\n\nexport interface Props {\n  onAddInstanceClick: () => void\n}\n\nconst EmptyMessage = ({ onAddInstanceClick }: Props) => (\n  <S.Container contentCentered data-testid=\"empty-database-instance-list\">\n    <S.Icon src={CakeIcon} alt=\"empty\" />\n    <Title size=\"M\">No databases yet, let&apos;s add one!</Title>\n    <Spacer size=\"m\" />\n    <Col gap=\"m\" grow={false} align=\"center\">\n      <PrimaryButton\n        size=\"m\"\n        onClick={() => {\n          sendEventTelemetry({\n            event: TelemetryEvent.CONFIG_DATABASES_CLICKED,\n            eventData: {\n              source: OAuthSocialSource.EmptyDatabasesList,\n            },\n          })\n          onAddInstanceClick?.()\n        }}\n        data-testid=\"empty-rdi-instance-button\"\n      >\n        + Add Redis database\n      </PrimaryButton>\n      <OAuthSsoHandlerDialog>\n        {(ssoCloudHandlerClick) => (\n          <Link\n            data-testid=\"empty-database-cloud-button\"\n            target=\"_blank\"\n            href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n              campaign: UTM_CAMPAINGS[OAuthSocialSource.EmptyDatabasesList],\n              medium: 'main',\n            })}\n            onClick={(e) => {\n              ssoCloudHandlerClick(e as any, {\n                action: OAuthSocialAction.Create,\n                source: OAuthSocialSource.EmptyDatabasesList,\n              })\n            }}\n          >\n            Create a free Redis Cloud database\n          </Link>\n        )}\n      </OAuthSsoHandlerDialog>\n    </Col>\n  </S.Container>\n)\n\nexport default EmptyMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/DatabaseForm.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { useFormik } from 'formik'\nimport { cleanup, render, screen, fireEvent, act } from 'uiSrc/utils/test-utils'\nimport { SECURITY_FIELD } from 'uiSrc/constants/securityField'\nimport { dbConnectionInfoFactory } from 'uiSrc/mocks/factories/database/DbConnectionInfo.factory'\nimport DatabaseForm, { Props } from './DatabaseForm'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/info', () => ({\n  appInfoSelector: jest.fn().mockReturnValue({\n    server: {\n      buildType: 'DOCKER_ON_PREMISE',\n    },\n  }),\n}))\n\nconst TestWrapper = ({\n  formikValues,\n  ...props\n}: Partial<Props> & { formikValues?: DbConnectionInfo }) => {\n  const defaultFormikValues = formikValues || dbConnectionInfoFactory.build()\n  const formik = useFormik({\n    initialValues: defaultFormikValues,\n    onSubmit: jest.fn(),\n  })\n\n  return (\n    <DatabaseForm\n      {...instance(mockedProps)}\n      formik={formik}\n      showFields={{ alias: true, host: true, port: true, timeout: true }}\n      onHostNamePaste={jest.fn()}\n      {...props}\n    />\n  )\n}\n\nconst renderComponent = (\n  props?: Partial<Props>,\n  formikValues?: DbConnectionInfo,\n) => {\n  return render(<TestWrapper {...props} formikValues={formikValues} />)\n}\n\ndescribe('DatabaseForm', () => {\n  beforeEach(() => {\n    cleanup()\n  })\n\n  it('should render', () => {\n    const { container } = renderComponent()\n    expect(container).toBeTruthy()\n  })\n\n  it('should render all fields when showFields is true', () => {\n    renderComponent()\n\n    expect(screen.getByTestId('name')).toBeInTheDocument()\n    expect(screen.getByTestId('host')).toBeInTheDocument()\n    expect(screen.getByTestId('port')).toBeInTheDocument()\n    expect(screen.getByTestId('username')).toBeInTheDocument()\n    expect(screen.getByTestId('password')).toBeInTheDocument()\n    expect(screen.getByTestId('timeout')).toBeInTheDocument()\n  })\n\n  it('should hide fields when showFields is false', () => {\n    renderComponent({\n      showFields: { alias: false, host: false, port: false, timeout: false },\n    })\n\n    expect(screen.queryByTestId('name')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('host')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('port')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('timeout')).not.toBeInTheDocument()\n    // username and password always show\n    expect(screen.getByTestId('username')).toBeInTheDocument()\n    expect(screen.getByTestId('password')).toBeInTheDocument()\n  })\n\n  it('should display initial values correctly', () => {\n    const mockData = dbConnectionInfoFactory.build({\n      name: 'Test Database',\n      host: 'localhost',\n      port: '6379',\n      username: 'testuser',\n      password: 'testpass',\n      timeout: '30',\n    })\n\n    renderComponent({}, mockData)\n\n    expect(screen.getByDisplayValue('Test Database')).toBeInTheDocument()\n    expect(screen.getByDisplayValue('localhost')).toBeInTheDocument()\n    expect(screen.getByDisplayValue('6379')).toBeInTheDocument()\n    expect(screen.getByDisplayValue('testuser')).toBeInTheDocument()\n    expect(screen.getByDisplayValue('testpass')).toBeInTheDocument()\n    expect(screen.getByDisplayValue('30')).toBeInTheDocument()\n  })\n\n  it('should update input values when changed', async () => {\n    renderComponent()\n\n    const nameInput = screen.getByTestId('name')\n    await act(async () => {\n      fireEvent.change(nameInput, { target: { value: 'New Database Name' } })\n    })\n    expect(nameInput).toHaveValue('New Database Name')\n\n    const hostInput = screen.getByTestId('host')\n    await act(async () => {\n      fireEvent.change(hostInput, { target: { value: '192.168.1.1' } })\n    })\n    expect(hostInput).toHaveValue('192.168.1.1')\n\n    const portInput = screen.getByTestId('port')\n    await act(async () => {\n      fireEvent.change(portInput, { target: { value: '6380' } })\n    })\n    expect(portInput).toHaveValue('6380')\n\n    const usernameInput = screen.getByTestId('username')\n    await act(async () => {\n      fireEvent.change(usernameInput, { target: { value: 'newuser' } })\n    })\n    expect(usernameInput).toHaveValue('newuser')\n\n    const passwordInput = screen.getByTestId('password')\n    await act(async () => {\n      fireEvent.change(passwordInput, { target: { value: 'newpassword' } })\n    })\n    expect(passwordInput).toHaveValue('newpassword')\n  })\n\n  it('should disable fields when in readyOnlyFields', () => {\n    renderComponent({ readyOnlyFields: ['alias', 'host', 'port'] })\n\n    expect(screen.getByTestId('name')).toBeDisabled()\n    expect(screen.getByTestId('host')).toBeDisabled()\n    expect(screen.getByTestId('port')).toBeDisabled()\n    expect(screen.getByTestId('username')).not.toBeDisabled()\n  })\n\n  it('should display SECURITY_FIELD when password is true', () => {\n    const securityFieldData = dbConnectionInfoFactory.build({ password: true })\n    renderComponent({}, securityFieldData)\n\n    expect(screen.getByDisplayValue(SECURITY_FIELD)).toBeInTheDocument()\n  })\n\n  // TODO [DA]: this test should be changed as part of RI-7330 as this is not the expected behavior\n  it('should clear password field when focused and value shown', async () => {\n    const securityFieldData = dbConnectionInfoFactory.build({ password: true })\n    renderComponent({}, securityFieldData)\n\n    const passwordInput = screen.getByTestId('password')\n    expect(passwordInput).toHaveValue(SECURITY_FIELD)\n\n    await act(async () => {\n      fireEvent.focus(passwordInput)\n    })\n\n    expect(passwordInput).toHaveValue('')\n  })\n\n  it('should set autoFocus on host field when autoFocus is true', () => {\n    renderComponent({ autoFocus: true })\n\n    const hostInput = screen.getByTestId('host')\n    expect(hostInput).toEqual(document.activeElement)\n  })\n\n  it('should call onHostNamePaste when pasting into host field', () => {\n    const mockOnHostNamePaste = jest.fn()\n    renderComponent({ onHostNamePaste: mockOnHostNamePaste })\n\n    const hostInput = screen.getByTestId('host')\n    const pasteEvent = {\n      clipboardData: {\n        getData: jest.fn().mockReturnValue('redis://localhost:6379'),\n      },\n      preventDefault: jest.fn(),\n    }\n\n    fireEvent.paste(hostInput, pasteEvent)\n    expect(mockOnHostNamePaste).toHaveBeenCalled()\n  })\n\n  it('should render with correct field labels', () => {\n    renderComponent()\n\n    expect(screen.getByText('Database alias')).toBeInTheDocument()\n    expect(screen.getByText('Host')).toBeInTheDocument()\n    expect(screen.getByText('Port')).toBeInTheDocument()\n    expect(screen.getByText('Username')).toBeInTheDocument()\n    expect(screen.getByText('Password')).toBeInTheDocument()\n    expect(screen.getByText('Timeout (s)')).toBeInTheDocument()\n  })\n\n  it('should render with correct maxLength attributes', () => {\n    renderComponent()\n\n    expect(screen.getByTestId('name')).toHaveAttribute('maxLength', '500')\n    expect(screen.getByTestId('host')).toHaveAttribute('maxLength', '200')\n    expect(screen.getByTestId('username')).toHaveAttribute('maxLength', '200')\n    expect(screen.getByTestId('password')).toHaveAttribute('maxLength', '10000')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/DatabaseForm.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { FormikProps } from 'formik'\n\nimport { BuildType } from 'uiSrc/constants/env'\nimport { SECURITY_FIELD } from 'uiSrc/constants'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport {\n  handlePasteHostName,\n  MAX_PORT_NUMBER,\n  MAX_TIMEOUT_NUMBER,\n  selectOnFocus,\n  validateField,\n} from 'uiSrc/utils'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  FormField,\n  RiInfoIconProps,\n} from 'uiSrc/components/base/forms/FormField'\nimport {\n  NumericInput,\n  PasswordInput,\n  TextInput,\n} from 'uiSrc/components/base/inputs'\nimport { HostInfoTooltipContent } from '../host-info-tooltip-content/HostInfoTooltipContent'\n\ninterface IShowFields {\n  alias: boolean\n  host: boolean\n  port: boolean\n  timeout: boolean\n}\n\nconst hostInfo: RiInfoIconProps = {\n  content: HostInfoTooltipContent({ includeAutofillInfo: true }),\n  placement: 'right',\n  maxWidth: '100%',\n}\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n  onHostNamePaste: (content: string) => boolean\n  showFields: IShowFields\n  autoFocus?: boolean\n  readyOnlyFields?: string[]\n}\n\nconst DatabaseForm = (props: Props) => {\n  const {\n    formik,\n    onHostNamePaste,\n    autoFocus = false,\n    showFields,\n    readyOnlyFields = [],\n  } = props\n\n  const { server } = useSelector(appInfoSelector)\n\n  const isShowPort =\n    server?.buildType !== BuildType.RedisStack && showFields.port\n  const isFieldDisabled = (name: string) => readyOnlyFields.includes(name)\n\n  return (\n    <Col gap=\"l\">\n      {showFields.alias && (\n        <Row gap=\"m\">\n          <FlexItem grow>\n            <FormField label=\"Database alias\" required>\n              <TextInput\n                name=\"name\"\n                id=\"name\"\n                data-testid=\"name\"\n                placeholder=\"Enter Database Alias\"\n                onFocus={selectOnFocus}\n                value={formik.values.name ?? ''}\n                maxLength={500}\n                onChangeCapture={formik.handleChange}\n                disabled={isFieldDisabled('alias')}\n              />\n            </FormField>\n          </FlexItem>\n        </Row>\n      )}\n      {(showFields.host || isShowPort) && (\n        <Row gap=\"m\">\n          {showFields.host && (\n            <FlexItem grow={4}>\n              <FormField label=\"Host\" required infoIconProps={hostInfo}>\n                <TextInput\n                  autoFocus={autoFocus}\n                  name=\"ip\"\n                  id=\"host\"\n                  data-testid=\"host\"\n                  color=\"secondary\"\n                  maxLength={200}\n                  placeholder=\"Enter Hostname / IP address / Connection URL\"\n                  value={formik.values.host ?? ''}\n                  onChange={(value) => {\n                    formik.setFieldValue('host', validateField(value.trim()))\n                  }}\n                  onPaste={(event: React.ClipboardEvent<HTMLInputElement>) =>\n                    handlePasteHostName(onHostNamePaste, event)\n                  }\n                  onFocus={selectOnFocus}\n                  disabled={isFieldDisabled('host')}\n                />\n              </FormField>\n            </FlexItem>\n          )}\n          {isShowPort && (\n            <FlexItem grow={2}>\n              <FormField label=\"Port\" required>\n                <NumericInput\n                  autoValidate\n                  name=\"port\"\n                  id=\"port\"\n                  data-testid=\"port\"\n                  placeholder=\"Enter Port\"\n                  onChange={(value) => formik.setFieldValue('port', value)}\n                  value={Number(formik.values.port)}\n                  min={0}\n                  max={MAX_PORT_NUMBER}\n                  onFocus={selectOnFocus}\n                  disabled={isFieldDisabled('port')}\n                />\n              </FormField>\n            </FlexItem>\n          )}\n        </Row>\n      )}\n\n      <Row gap=\"m\">\n        <FlexItem grow>\n          <FormField label=\"Username\">\n            <TextInput\n              name=\"username\"\n              id=\"username\"\n              data-testid=\"username\"\n              maxLength={200}\n              placeholder=\"Enter Username\"\n              value={formik.values.username ?? ''}\n              onChangeCapture={formik.handleChange}\n              disabled={isFieldDisabled('username')}\n            />\n          </FormField>\n        </FlexItem>\n\n        <FlexItem grow>\n          <FormField label=\"Password\">\n            <PasswordInput\n              name=\"password\"\n              id=\"password\"\n              data-testid=\"password\"\n              maxLength={10_000}\n              placeholder=\"Enter Password\"\n              value={\n                formik.values.password === true\n                  ? SECURITY_FIELD\n                  : (formik.values.password ?? '')\n              }\n              onChangeCapture={formik.handleChange}\n              onFocus={() => {\n                if (formik.values.password === true) {\n                  formik.setFieldValue('password', '')\n                }\n              }}\n              autoComplete=\"new-password\"\n              disabled={isFieldDisabled('password')}\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n\n      {showFields.timeout && (\n        <Row gap=\"m\" responsive>\n          <FlexItem grow>\n            <FormField label=\"Timeout (s)\">\n              <NumericInput\n                autoValidate\n                name=\"timeout\"\n                id=\"timeout\"\n                data-testid=\"timeout\"\n                placeholder=\"Enter Timeout (in seconds)\"\n                onChange={(value) => formik.setFieldValue('timeout', value)}\n                value={Number(formik.values.timeout)}\n                min={1}\n                max={MAX_TIMEOUT_NUMBER}\n                onFocus={selectOnFocus}\n                disabled={isFieldDisabled('timeout')}\n              />\n            </FormField>\n          </FlexItem>\n          <FlexItem grow />\n        </Row>\n      )}\n    </Col>\n  )\n}\n\nexport default DatabaseForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/DbCompressor.tsx",
    "content": "import React, { ChangeEvent } from 'react'\nimport { FormikProps } from 'formik'\n\nimport { KeyValueCompressor } from 'uiSrc/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { NONE } from 'uiSrc/pages/home/constants'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { Text } from 'uiSrc/components/base/text/Text'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n}\n\nconst DbCompressor = (props: Props) => {\n  const { formik } = props\n\n  const optionsCompressor = [\n    {\n      value: NONE,\n      label: 'No decompression',\n    },\n    {\n      value: KeyValueCompressor.GZIP,\n      label: 'GZIP',\n    },\n    {\n      value: KeyValueCompressor.LZ4,\n      label: 'LZ4',\n    },\n    {\n      value: KeyValueCompressor.SNAPPY,\n      label: 'SNAPPY',\n    },\n    {\n      value: KeyValueCompressor.ZSTD,\n      label: 'ZSTD',\n    },\n    {\n      value: KeyValueCompressor.Brotli,\n      label: 'Brotli',\n    },\n    {\n      value: KeyValueCompressor.PHPGZCompress,\n      label: 'PHP GZCompress',\n    },\n  ]\n\n  const handleChangeDbCompressorCheckbox = (\n    e: ChangeEvent<HTMLInputElement>,\n  ): void => {\n    const isChecked = e.target.checked\n    if (!isChecked) {\n      // Reset db field to initial value\n      formik.setFieldValue('compressor', NONE)\n    }\n    formik.setFieldValue('showCompressor', isChecked)\n  }\n  const id = useGenerateId('', ' over db compressor')\n\n  return (\n    <>\n      <Row gap=\"m\" responsive={false}>\n        <FlexItem>\n          <FormField>\n            <Checkbox\n              id={id}\n              name=\"showCompressor\"\n              label={<Text>Enable Automatic Data Decompression</Text>}\n              checked={!!formik.values.showCompressor}\n              onChange={handleChangeDbCompressorCheckbox}\n              data-testid=\"showCompressor\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n\n      {formik.values.showCompressor && (\n        <Row gap=\"m\">\n          <FlexItem grow>\n            <FormField label=\"Decompression format\">\n              <RiSelect\n                name=\"compressor\"\n                placeholder=\"Decompression format\"\n                value={formik.values.compressor ?? NONE}\n                options={optionsCompressor}\n                onChange={(value) => {\n                  formik.setFieldValue('compressor', value || NONE)\n                }}\n                data-testid=\"select-compressor\"\n              />\n            </FormField>\n          </FlexItem>\n          <FlexItem grow />\n        </Row>\n      )}\n    </>\n  )\n}\n\nexport default DbCompressor\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/DbIndex.tsx",
    "content": "import React, { ChangeEvent } from 'react'\nimport { FormikProps } from 'formik'\n\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { NumericInput } from 'uiSrc/components/base/inputs'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\nimport styles from '../styles.module.scss'\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n}\n\nconst DbIndex = (props: Props) => {\n  const { formik } = props\n\n  const handleChangeDbIndexCheckbox = (\n    e: ChangeEvent<HTMLInputElement>,\n  ): void => {\n    // Need to check the type of event to safely access properties\n    const isChecked = 'checked' in e.target ? e.target.checked : false\n    if (!isChecked) {\n      // Reset db field to initial value\n      formik.setFieldValue('db', null)\n    }\n    formik.handleChange(e)\n  }\n  const id = useGenerateId('', ' over db')\n\n  return (\n    <>\n      <Row gap=\"s\">\n        <FlexItem>\n          <FormField>\n            <Checkbox\n              id={id}\n              name=\"showDb\"\n              labelSize=\"M\"\n              label=\"Select Logical Database\"\n              checked={!!formik.values.showDb}\n              onChange={handleChangeDbIndexCheckbox}\n              data-testid=\"showDb\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n\n      {formik.values.showDb && (\n        <Row gap=\"m\" responsive>\n          <FlexItem grow className={styles.dbInput}>\n            <FormField label=\"Database Index\">\n              <NumericInput\n                autoValidate\n                min={0}\n                name=\"db\"\n                id=\"db\"\n                data-testid=\"db\"\n                placeholder=\"Enter Database Index\"\n                value={Number(formik.values.db)}\n                onChange={(value) => formik.setFieldValue('db', value)}\n              />\n            </FormField>\n          </FlexItem>\n          <FlexItem grow />\n        </Row>\n      )}\n    </>\n  )\n}\n\nexport default DbIndex\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/DbInfo.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Group } from 'uiSrc/components/base/layout/list'\n\nexport const DbInfoGroup = styled(Group)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral200};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral400};\n  padding: ${({ theme }) => theme.core.space.space100};\n  border-radius: 5px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/DbInfo.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { capitalize } from 'lodash'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { DatabaseListModules, RiTooltip } from 'uiSrc/components'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\nimport { Item as ListGroupItem } from 'uiSrc/components/base/layout/list'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Endpoint } from 'apiSrc/common/models'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\n\nimport styles from '../styles.module.scss'\nimport { DbInfoGroup } from './DbInfo.styles'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { DbInfoLabelValue } from './types'\n\nexport interface Props {\n  connectionType?: ConnectionType\n  nameFromProvider?: Nullable<string>\n  nodes: Nullable<Endpoint[]>\n  host: string\n  port: string\n  db: Nullable<number>\n  modules: AdditionalRedisModule[]\n  isFromCloud: boolean\n}\n\nexport const ListGroupItemLabelValue = ({\n  label,\n  value,\n  dataTestId,\n  additionalContent,\n}: {\n  label: string\n  value: string | React.ReactNode\n  dataTestId?: string\n  additionalContent?: React.ReactNode\n}) => (\n  <ListGroupItem\n    label={\n      <Row align=\"center\" gap=\"m\">\n        <Text color=\"secondary\">{label}</Text>\n        <Text color=\"primary\" data-testid={dataTestId}>\n          {value}\n        </Text>\n        {additionalContent}\n      </Row>\n    }\n  />\n)\n\nconst AppendEndpoints = ({\n  nodes,\n  host,\n  port,\n}: {\n  nodes: Endpoint[]\n  host: string\n  port: string\n}) => (\n  <RiTooltip\n    title=\"Host:port\"\n    position=\"left\"\n    anchorClassName={styles.anchorEndpoints}\n    content={\n      <ul className={styles.endpointsList}>\n        {nodes?.map(({ host: eHost, port: ePort }) => (\n          <li key={host + port}>\n            <Text>\n              {eHost}:{ePort};\n            </Text>\n          </li>\n        ))}\n      </ul>\n    }\n  >\n    <RiIcon type=\"InfoIcon\" style={{ cursor: 'pointer' }} />\n  </RiTooltip>\n)\n\nconst DbInfo = (props: Props) => {\n  const {\n    connectionType,\n    nameFromProvider,\n    nodes = null,\n    host,\n    port,\n    db,\n    modules,\n    isFromCloud,\n  } = props\n\n  const { server } = useSelector(appInfoSelector)\n\n  const dbInfo: DbInfoLabelValue[] = [\n    {\n      label: 'Connection Type:',\n      value: capitalize(connectionType),\n      dataTestId: 'connection-type',\n      hide: isFromCloud,\n    },\n    {\n      label: 'Database Name from Provider:',\n      value: nameFromProvider,\n      dataTestId: 'db-name-from-provider',\n      hide: !nameFromProvider,\n    },\n    {\n      label: 'Host:',\n      value: host,\n      dataTestId: 'db-info-host',\n      additionalContent: !!nodes?.length && (\n        <AppendEndpoints nodes={nodes} host={host} port={port} />\n      ),\n    },\n    {\n      label: 'Port:',\n      value: port,\n      dataTestId: 'db-info-port',\n      hide: server?.buildType !== BuildType.RedisStack && !isFromCloud,\n    },\n    {\n      label: 'Database Index:',\n      value: db?.toString(),\n      dataTestId: 'db-index',\n      hide: !db,\n    },\n    {\n      label: 'Capabilities:',\n      value: <DatabaseListModules modules={modules} />,\n      dataTestId: 'capabilities',\n      hide: !modules?.length,\n    },\n  ]\n\n  return (\n    <DbInfoGroup flush maxWidth={false}>\n      {dbInfo\n        .filter((item) => !item.hide)\n        .map((item) => (\n          <ListGroupItemLabelValue\n            key={item.label}\n            label={item.label}\n            value={item.value}\n            dataTestId={item.dataTestId}\n            additionalContent={item.additionalContent}\n          />\n        ))}\n    </DbInfoGroup>\n  )\n}\n\nexport default DbInfo\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/ForceStandalone.tsx",
    "content": "import React, { ChangeEvent } from 'react'\nimport { FormikProps } from 'formik'\n\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiTooltip } from 'uiSrc/components'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\nimport { Text } from 'uiSrc/components/base/text/Text'\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n}\n\nconst ForceStandaloneLabel = () => (\n  <Row align=\"center\" gap=\"s\">\n    <Text>Force Standalone Connection</Text>\n    <RiTooltip\n      position=\"right\"\n      content={\n        <Text>\n          Override the default connection logic and connect to the specified\n          endpoint as a standalone database.\n        </Text>\n      }\n    >\n      <FlexItem>\n        <RiIcon type=\"InfoIcon\" style={{ cursor: 'pointer' }} />\n      </FlexItem>\n    </RiTooltip>\n  </Row>\n)\nconst ForceStandalone = (props: Props) => {\n  const { formik } = props\n\n  const handleChangeForceStandaloneCheckbox = (\n    e: ChangeEvent<HTMLInputElement>,\n  ): void => {\n    formik.handleChange(e)\n  }\n  const id = useGenerateId('', ' over forceStandalone')\n\n  return (\n    <Row gap=\"s\">\n      <FlexItem>\n        <FormField>\n          <Checkbox\n            id={id}\n            name=\"forceStandalone\"\n            label={<ForceStandaloneLabel />}\n            checked={!!formik.values.forceStandalone}\n            onChange={handleChangeForceStandaloneCheckbox}\n            data-testid=\"forceStandalone\"\n          />\n        </FormField>\n      </FlexItem>\n    </Row>\n  )\n}\n\nexport default ForceStandalone\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/KeyFormatSelector.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\n\nimport { KeyValueFormat } from 'uiSrc/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n}\n\nconst KeyFormatSelector = (props: Props) => {\n  const { formik } = props\n\n  const options = [\n    {\n      value: KeyValueFormat.Unicode,\n      label: 'Unicode',\n    },\n    {\n      value: KeyValueFormat.HEX,\n      label: 'HEX',\n    },\n  ]\n\n  return (\n    <Row gap=\"m\">\n      <FlexItem grow>\n        <FormField label=\"Key name format\">\n          <RiSelect\n            name=\"key-name-format\"\n            placeholder=\"Key name format\"\n            // TODO: fix the type\n            value={\n              (formik.values.keyNameFormat as unknown as string) ||\n              KeyValueFormat.Unicode\n            }\n            options={options}\n            onChange={(value) => {\n              formik.setFieldValue('keyNameFormat', value)\n            }}\n            data-testid=\"select-key-name-format\"\n          />\n        </FormField>\n      </FlexItem>\n      <FlexItem grow />\n    </Row>\n  )\n}\n\nexport default KeyFormatSelector\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/Messages.tsx",
    "content": "import React from 'react'\nimport { APPLICATION_NAME } from 'uiSrc/constants'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nconst MessageCloudApiKeys = () => (\n  <Text data-testid=\"summary\" color=\"primary\">\n    {\n      'Enter Redis Cloud API keys to discover and add databases. API keys can be enabled by following the steps mentioned in the '\n    }\n    <Link\n      external\n      variant=\"inline\"\n      color=\"subdued\"\n      href=\"https://docs.redis.com/latest/rc/api/get-started/enable-the-api/\"\n    >\n      documentation\n    </Link>\n    {'.'}\n  </Text>\n)\n\nconst MessageStandalone = () => (\n  <Text data-testid=\"summary\" color=\"primary\">\n    You can manually add your Redis databases. Enter host and port of your Redis\n    database to add it to {APPLICATION_NAME}. &nbsp;\n    <Link\n      external\n      variant=\"inline\"\n      color=\"subdued\"\n      href={getUtmExternalLink(\n        'https://redis.io/docs/latest/develop/connect/insight#connection-management',\n        { campaign: 'redisinsight' },\n      )}\n    >\n      Learn more here.\n    </Link>\n  </Text>\n)\n\nconst MessageSentinel = () => (\n  <Text data-testid=\"summary\" color=\"primary\">\n    You can automatically discover and add primary groups from your Redis\n    Sentinel. Enter host and port of your Redis Sentinel to automatically\n    discover your primary groups and add them to {APPLICATION_NAME}. &nbsp;\n    <Link\n      external\n      variant=\"inline\"\n      color=\"subdued\"\n      href={getUtmExternalLink(\n        'https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/',\n        { campaign: 'redisinsight' },\n      )}\n    >\n      Learn more here.\n    </Link>\n  </Text>\n)\n\nconst MessageEnterpriceSoftware = () => (\n  <Text data-testid=\"summary\" color=\"primary\">\n    Your Redis Software databases can be automatically added. Enter the\n    connection details of your Redis Software Cluster to automatically discover\n    your databases and add them to {APPLICATION_NAME}. &nbsp;\n    <Link\n      external\n      variant=\"inline\"\n      color=\"subdued\"\n      href={getUtmExternalLink(\n        'https://redis.io/redis-enterprise-software/overview/',\n        { campaign: 'redisinsight' },\n      )}\n    >\n      Learn more here.\n    </Link>\n  </Text>\n)\n\nexport {\n  MessageStandalone,\n  MessageSentinel,\n  MessageCloudApiKeys,\n  MessageEnterpriceSoftware,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/SSHDetails.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\n\nimport { MAX_PORT_NUMBER, selectOnFocus, validateField } from 'uiSrc/utils'\nimport { SECURITY_FIELD } from 'uiSrc/constants'\n\nimport { SshPassType } from 'uiSrc/pages/home/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport {\n  NumericInput,\n  PasswordInput,\n  TextArea,\n  TextInput,\n} from 'uiSrc/components/base/inputs'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiRadioGroup } from 'uiSrc/components/base/forms/radio-group/RadioGroup'\nimport { Text } from 'uiSrc/components/base/text/Text'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n}\n\nconst sshPassTypeOptions = [\n  {\n    id: SshPassType.Password,\n    value: SshPassType.Password,\n    label: 'Password',\n    // 'data-test-subj': 'radio-btn-password',\n  },\n  {\n    id: SshPassType.PrivateKey,\n    value: SshPassType.PrivateKey,\n    label: 'Private Key',\n    // 'data-test-subj': 'radio-btn-privateKey',\n  },\n]\n\nconst SSHDetails = (props: Props) => {\n  const { formik } = props\n  const id = useGenerateId('', ' ssh')\n\n  return (\n    <Col gap=\"m\">\n      <Row>\n        <FormField>\n          <Checkbox\n            id={id}\n            name=\"ssh\"\n            label={<Text>Use SSH Tunnel</Text>}\n            checked={!!formik.values.ssh}\n            onChange={formik.handleChange}\n            data-testid=\"use-ssh\"\n          />\n        </FormField>\n      </Row>\n\n      {formik.values.ssh && (\n        <Col gap=\"l\">\n          <Row gap=\"m\" responsive>\n            <FlexItem grow>\n              <FormField label=\"Host\" required>\n                <TextInput\n                  name=\"sshHost\"\n                  id=\"sshHost\"\n                  data-testid=\"sshHost\"\n                  color=\"secondary\"\n                  maxLength={200}\n                  placeholder=\"Enter SSH Host\"\n                  value={formik.values.sshHost ?? ''}\n                  onChange={(value) => {\n                    formik.setFieldValue('sshHost', validateField(value.trim()))\n                  }}\n                />\n              </FormField>\n            </FlexItem>\n            <FlexItem grow>\n              <FormField label=\"Port\" required>\n                <NumericInput\n                  autoValidate\n                  min={0}\n                  max={MAX_PORT_NUMBER}\n                  name=\"sshPort\"\n                  id=\"sshPort\"\n                  data-testid=\"sshPort\"\n                  placeholder=\"Enter SSH Port\"\n                  value={Number(formik.values.sshPort)}\n                  onChange={(value) => formik.setFieldValue('sshPort', value)}\n                  onFocus={selectOnFocus}\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n          <Row responsive>\n            <FlexItem grow>\n              <FormField label=\"Username\" required>\n                <TextInput\n                  name=\"sshUsername\"\n                  id=\"sshUsername\"\n                  data-testid=\"sshUsername\"\n                  color=\"secondary\"\n                  maxLength={200}\n                  placeholder=\"Enter SSH Username\"\n                  value={formik.values.sshUsername ?? ''}\n                  onChange={(value) => {\n                    formik.setFieldValue(\n                      'sshUsername',\n                      validateField(value.trim()),\n                    )\n                  }}\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n          <Row responsive>\n            <FlexItem grow>\n              <RiRadioGroup\n                id=\"sshPassType\"\n                items={sshPassTypeOptions}\n                layout=\"horizontal\"\n                value={formik.values.sshPassType}\n                onChange={(id) => formik.setFieldValue('sshPassType', id)}\n                data-testid=\"ssh-pass-type\"\n              />\n            </FlexItem>\n          </Row>\n          {formik.values.sshPassType === SshPassType.Password && (\n            <Row responsive>\n              <FlexItem grow>\n                <FormField label=\"Password\">\n                  <PasswordInput\n                    name=\"sshPassword\"\n                    id=\"sshPassword\"\n                    data-testid=\"sshPassword\"\n                    maxLength={10_000}\n                    placeholder=\"Enter SSH Password\"\n                    value={\n                      formik.values.sshPassword === true\n                        ? SECURITY_FIELD\n                        : (formik.values.sshPassword ?? '')\n                    }\n                    onChangeCapture={formik.handleChange}\n                    onFocus={() => {\n                      if (formik.values.sshPassword === true) {\n                        formik.setFieldValue('sshPassword', '')\n                      }\n                    }}\n                    autoComplete=\"new-password\"\n                  />\n                </FormField>\n              </FlexItem>\n            </Row>\n          )}\n\n          {formik.values.sshPassType === SshPassType.PrivateKey && (\n            <Col gap=\"l\">\n              <Row responsive>\n                <FlexItem grow>\n                  <FormField label=\"Private Key\" required>\n                    <TextArea\n                      name=\"sshPrivateKey\"\n                      id=\"sshPrivateKey\"\n                      data-testid=\"sshPrivateKey\"\n                      maxLength={50_000}\n                      placeholder=\"Enter SSH Private Key in PEM format\"\n                      value={\n                        formik.values.sshPrivateKey === true\n                          ? SECURITY_FIELD\n                          : (formik?.values?.sshPrivateKey?.replace(\n                              /./g,\n                              '•',\n                            ) ?? '')\n                      }\n                      onChangeCapture={formik.handleChange}\n                      onFocus={() => {\n                        if (formik.values.sshPrivateKey === true) {\n                          formik.setFieldValue('sshPrivateKey', '')\n                        }\n                      }}\n                    />\n                  </FormField>\n                </FlexItem>\n              </Row>\n              <Row responsive>\n                <FlexItem grow>\n                  <FormField label=\"Passphrase\">\n                    <PasswordInput\n                      name=\"sshPassphrase\"\n                      id=\"sshPassphrase\"\n                      data-testid=\"sshPassphrase\"\n                      maxLength={50_000}\n                      placeholder=\"Enter Passphrase for Private Key\"\n                      value={\n                        formik.values.sshPassphrase === true\n                          ? SECURITY_FIELD\n                          : (formik.values.sshPassphrase ?? '')\n                      }\n                      onChangeCapture={formik.handleChange}\n                      onFocus={() => {\n                        if (formik.values.sshPassphrase === true) {\n                          formik.setFieldValue('sshPassphrase', '')\n                        }\n                      }}\n                      autoComplete=\"new-password\"\n                    />\n                  </FormField>\n                </FlexItem>\n              </Row>\n            </Col>\n          )}\n        </Col>\n      )}\n    </Col>\n  )\n}\n\nexport default SSHDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/TlsDetails.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { act, render } from 'uiSrc/utils/test-utils'\nimport TlsDetails, { Props } from './TlsDetails'\n\nconst mockedProps = mock<Props>()\n\ndescribe('TlsDetails', () => {\n  it('should render', async () => {\n    let renderResult\n    await act(async () => {\n      renderResult = render(\n        <TlsDetails\n          {...instance(mockedProps)}\n          formik={{\n            // @ts-ignore\n            values: {\n              tls: false,\n            },\n            setFieldValue: jest.fn(),\n            setFieldTouched: jest.fn(),\n            errors: {},\n            touched: {},\n            handleChange: jest.fn(),\n            handleBlur: jest.fn(),\n          }}\n        />,\n      )\n    })\n    expect(renderResult).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/TlsDetails.tsx",
    "content": "import React, { ChangeEvent, useState } from 'react'\nimport cx from 'classnames'\nimport { FormikProps } from 'formik'\n\nimport { useDispatch } from 'react-redux'\nimport { Nullable, validateCertName, validateField } from 'uiSrc/utils'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\n\nimport {\n  ADD_NEW,\n  ADD_NEW_CA_CERT,\n  NO_CA_CERT,\n} from 'uiSrc/pages/home/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { deleteCaCertificateAction } from 'uiSrc/slices/instances/caCerts'\nimport { deleteClientCertAction } from 'uiSrc/slices/instances/clientCerts'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { TextArea, TextInput } from 'uiSrc/components/base/inputs'\nimport { RiSelectOption } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { useGenerateId } from 'uiSrc/components/base/utils/hooks/generate-id'\nimport styles from '../styles.module.scss'\nimport { RISelectWithActions } from 'uiSrc/components/base/forms/select/RISelectWithActions'\n\nconst suffix = '_tls_details'\n\nexport interface Certificate {\n  id: string\n  name: string\n}\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n  caCertificates?: Certificate[]\n  certificates?: Certificate[]\n}\n\nconst TlsDetails = (props: Props) => {\n  const dispatch = useDispatch()\n  const { formik, caCertificates, certificates } = props\n  const [activeCertId, setActiveCertId] = useState<Nullable<string>>(null)\n\n  const handleDeleteCaCert = (id: string) => {\n    dispatch(\n      deleteCaCertificateAction(id, () => {\n        if (formik.values.selectedCaCertName === id) {\n          formik.setFieldValue('selectedCaCertName', NO_CA_CERT)\n        }\n        handleClickDeleteCert('CA')\n      }),\n    )\n  }\n\n  const handleDeleteClientCert = (id: string) => {\n    dispatch(\n      deleteClientCertAction(id, () => {\n        if (formik.values.selectedTlsClientCertId === id) {\n          formik.setFieldValue('selectedTlsClientCertId', ADD_NEW)\n        }\n        handleClickDeleteCert('Client')\n      }),\n    )\n  }\n\n  const handleClickDeleteCert = (certificateType: 'Client' | 'CA') => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_CERTIFICATE_REMOVED,\n      eventData: {\n        certificateType,\n      },\n    })\n  }\n\n  const closePopover = () => {\n    setActiveCertId(null)\n  }\n\n  const showPopover = (id: string) => {\n    setActiveCertId(`${id}${suffix}`)\n  }\n\n  const optionsCertsCA: RiSelectOption[] = [\n    {\n      value: NO_CA_CERT,\n      label: 'No CA Certificate',\n    },\n    {\n      value: ADD_NEW_CA_CERT,\n      label: 'Add new CA certificate',\n    },\n  ]\n\n  caCertificates?.forEach((cert) => {\n    optionsCertsCA.push({\n      value: cert.id,\n      label: cert.name,\n      actions: (\n        <PopoverDelete\n          header={cert.name}\n          text=\"will be removed from RedisInsight.\"\n          item={cert.id}\n          suffix={suffix}\n          deleting={activeCertId ?? ''}\n          closePopover={closePopover}\n          updateLoading={false}\n          showPopover={showPopover}\n          handleDeleteItem={handleDeleteCaCert}\n          testid={`delete-ca-cert-${cert.id}`}\n          persistent\n          customOutsideDetector\n        />\n      ),\n    })\n  })\n\n  const optionsCertsClient: RiSelectOption[] = [\n    {\n      value: 'ADD_NEW',\n      label: 'Add new certificate',\n    },\n  ]\n\n  certificates?.forEach((cert) => {\n    optionsCertsClient.push({\n      value: `${cert.id}`,\n      label: cert.name,\n      actions: (\n        <PopoverDelete\n          header={cert.name}\n          text=\"will be removed from RedisInsight.\"\n          item={cert.id}\n          suffix={suffix}\n          deleting={activeCertId}\n          closePopover={closePopover}\n          updateLoading={false}\n          showPopover={showPopover}\n          handleDeleteItem={handleDeleteClientCert}\n          testid={`delete-client-cert-${cert.id}`}\n          persistent\n          customOutsideDetector\n        />\n      ),\n    })\n  })\n\n  const sslId = useGenerateId('', ' over ssl')\n  const sni = useGenerateId('', ' sni')\n  const verifyTlsId = useGenerateId('', ' verifyServerTlsCert')\n  const isTlsAuthId = useGenerateId('', ' is_tls_client_auth_required')\n  return (\n    <Col gap=\"l\">\n      <Row gap=\"m\">\n        <FlexItem>\n          <FormField>\n            <Checkbox\n              id={sslId}\n              name=\"tls\"\n              label=\"Use TLS\"\n              labelSize=\"M\"\n              checked={!!formik.values.tls}\n              onChange={formik.handleChange}\n              data-testid=\"tls\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n\n      {formik.values.tls && (\n        <Col gap=\"l\">\n          <Row gap=\"m\">\n            <FlexItem grow={1}>\n              <Checkbox\n                id={sni}\n                name=\"sni\"\n                labelSize=\"M\"\n                label=\"Use SNI\"\n                checked={!!formik.values.sni}\n                onChange={(e: ChangeEvent<HTMLInputElement>) => {\n                  formik.setFieldValue(\n                    'servername',\n                    formik.values.servername ?? formik.values.host ?? '',\n                  )\n                  return formik.handleChange(e)\n                }}\n                data-testid=\"sni\"\n              />\n            </FlexItem>\n          </Row>\n          {formik.values.sni && (\n            <Row gap=\"m\">\n              <FlexItem grow>\n                <FormField label=\"Server Name\" required>\n                  <TextInput\n                    name=\"servername\"\n                    id=\"servername\"\n                    maxLength={200}\n                    placeholder=\"Enter Server Name\"\n                    value={formik.values.servername ?? ''}\n                    onChange={(value) =>\n                      formik.setFieldValue(\n                        'servername',\n                        validateField(value.trim()),\n                      )\n                    }\n                    data-testid=\"sni-servername\"\n                  />\n                </FormField>\n              </FlexItem>\n            </Row>\n          )}\n          <Row gap=\"m\" responsive>\n            <FlexItem\n              grow\n              className={cx({ [styles.fullWidth]: formik.values.sni })}\n            >\n              <Checkbox\n                id={verifyTlsId}\n                name=\"verifyServerTlsCert\"\n                label=\"Verify TLS Certificate\"\n                labelSize=\"M\"\n                checked={!!formik.values.verifyServerTlsCert}\n                onChange={formik.handleChange}\n                data-testid=\"verify-tls-cert\"\n              />\n            </FlexItem>\n          </Row>\n        </Col>\n      )}\n      {formik.values.tls && (\n        <Col gap=\"l\">\n          <Row gap=\"m\" responsive>\n            <FlexItem grow>\n              <FormField\n                label=\"CA Certificate\"\n                required={formik.values.verifyServerTlsCert}\n              >\n                <RISelectWithActions\n                  placeholder=\"Select CA certificate\"\n                  value={formik.values.selectedCaCertName ?? NO_CA_CERT}\n                  options={optionsCertsCA}\n                  onChange={(value) => {\n                    formik.setFieldValue(\n                      'selectedCaCertName',\n                      value || NO_CA_CERT,\n                    )\n                  }}\n                  data-testid=\"select-ca-cert\"\n                />\n              </FormField>\n            </FlexItem>\n\n            {formik.values.tls &&\n            formik.values.selectedCaCertName === ADD_NEW_CA_CERT ? (\n              <FlexItem grow>\n                <FormField label=\"Name\" required>\n                  <TextInput\n                    name=\"newCaCertName\"\n                    id=\"newCaCertName\"\n                    maxLength={200}\n                    placeholder=\"Enter CA Certificate Name\"\n                    value={formik.values.newCaCertName ?? ''}\n                    onChange={(value) =>\n                      formik.setFieldValue(\n                        'newCaCertName',\n                        validateCertName(value),\n                      )\n                    }\n                    data-testid=\"qa-ca-cert\"\n                  />\n                </FormField>\n              </FlexItem>\n            ) : (\n              <FlexItem grow />\n            )}\n          </Row>\n\n          {formik.values.tls &&\n            formik.values.selectedCaCertName === ADD_NEW_CA_CERT && (\n              <Row gap=\"m\" responsive>\n                <FlexItem grow>\n                  <FormField label=\"Certificate\" required>\n                    <TextArea\n                      name=\"newCaCert\"\n                      id=\"newCaCert\"\n                      value={formik.values.newCaCert ?? ''}\n                      onChangeCapture={formik.handleChange}\n                      placeholder=\"Enter CA Certificate\"\n                      data-testid=\"new-ca-cert\"\n                    />\n                  </FormField>\n                </FlexItem>\n              </Row>\n            )}\n        </Col>\n      )}\n      {formik.values.tls && (\n        <Row responsive>\n          <FlexItem grow>\n            <Checkbox\n              id={isTlsAuthId}\n              name=\"tlsClientAuthRequired\"\n              label=\"Requires TLS Client Authentication\"\n              labelSize=\"M\"\n              checked={!!formik.values.tlsClientAuthRequired}\n              onChange={(e: ChangeEvent<HTMLInputElement>) =>\n                formik.setFieldValue('tlsClientAuthRequired', e.target.checked)\n              }\n              data-testid=\"tls-required-checkbox\"\n            />\n          </FlexItem>\n        </Row>\n      )}\n      {formik.values.tls && formik.values.tlsClientAuthRequired && (\n        <Col gap=\"l\">\n          <Row gap=\"m\" responsive>\n            <FlexItem grow>\n              <FormField label=\"Client Certificate\" required>\n                <RISelectWithActions\n                  placeholder=\"Select certificate\"\n                  value={formik.values.selectedTlsClientCertId}\n                  options={optionsCertsClient}\n                  onChange={(value) => {\n                    formik.setFieldValue('selectedTlsClientCertId', value)\n                  }}\n                  data-testid=\"select-cert\"\n                />\n              </FormField>\n            </FlexItem>\n\n            {formik.values.tls &&\n            formik.values.tlsClientAuthRequired &&\n            formik.values.selectedTlsClientCertId === 'ADD_NEW' ? (\n              <FlexItem grow>\n                <FormField label=\"Name\" required>\n                  <TextInput\n                    name=\"newTlsCertPairName\"\n                    id=\"newTlsCertPairName\"\n                    maxLength={200}\n                    placeholder=\"Enter Client Certificate Name\"\n                    value={formik.values.newTlsCertPairName ?? ''}\n                    onChange={(value) =>\n                      formik.setFieldValue(\n                        'newTlsCertPairName', // same as the name prop passed a few lines above\n                        validateCertName(value),\n                      )\n                    }\n                    data-testid=\"new-tsl-cert-pair-name\"\n                  />\n                </FormField>\n              </FlexItem>\n            ) : (\n              <FlexItem grow />\n            )}\n          </Row>\n\n          {formik.values.tls &&\n            formik.values.tlsClientAuthRequired &&\n            formik.values.selectedTlsClientCertId === 'ADD_NEW' && (\n              <Col gap=\"l\">\n                <Row gap=\"m\" responsive>\n                  <FlexItem grow>\n                    <FormField label=\"Certificate\" required>\n                      <TextArea\n                        name=\"newTlsClientCert\"\n                        id=\"newTlsClientCert\"\n                        value={formik.values.newTlsClientCert}\n                        onChangeCapture={formik.handleChange}\n                        draggable={false}\n                        placeholder=\"Enter Client Certificate\"\n                        data-testid=\"new-tls-client-cert\"\n                      />\n                    </FormField>\n                  </FlexItem>\n                </Row>\n                <Row gap=\"m\" responsive>\n                  <FlexItem grow>\n                    <FormField label=\"Private Key\" required>\n                      <TextArea\n                        placeholder=\"Enter Private Key\"\n                        name=\"newTlsClientKey\"\n                        id=\"newTlsClientKey\"\n                        value={formik.values.newTlsClientKey}\n                        onChangeCapture={formik.handleChange}\n                        data-testid=\"new-tls-client-cert-key\"\n                      />\n                    </FormField>\n                  </FlexItem>\n                </Row>\n              </Col>\n            )}\n        </Col>\n      )}\n    </Col>\n  )\n}\n\nexport default TlsDetails\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/index.ts",
    "content": "import DbInfo from './DbInfo'\nimport { MessageStandalone, MessageSentinel } from './Messages'\nimport DbIndex from './DbIndex'\nimport DbCompressor from './DbCompressor'\nimport TlsDetails from './TlsDetails'\nimport DatabaseForm from './DatabaseForm'\nimport SSHDetails from './SSHDetails'\nimport ForceStandalone from './ForceStandalone'\nimport KeyFormatSelector from './KeyFormatSelector'\n\nexport {\n  DbInfo,\n  MessageStandalone,\n  MessageSentinel,\n  DbIndex,\n  TlsDetails,\n  DatabaseForm,\n  SSHDetails,\n  DbCompressor,\n  ForceStandalone,\n  KeyFormatSelector,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/sentinel/DbInfoSentinel.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const StyledCopyContainer = styled.div`\n  opacity: 0;\n  transition: opacity 0.25s ease-in-out;\n\n  :hover {\n    opacity: 1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/sentinel/DbInfoSentinel.tsx",
    "content": "import React from 'react'\nimport { capitalize } from 'lodash'\n\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\nimport { SentinelMaster } from 'apiSrc/modules/redis-sentinel/models/sentinel-master'\nimport { CopyButton } from 'uiSrc/components/copy-button'\n\nimport { DbInfoGroup } from '../DbInfo.styles'\nimport { ListGroupItemLabelValue } from '../DbInfo'\nimport { DbInfoLabelValue } from '../types'\nimport { StyledCopyContainer } from 'uiSrc/pages/home/components/form/sentinel/DbInfoSentinel.styles'\n\nexport interface Props {\n  host?: string\n  port?: string\n  connectionType?: ConnectionType\n  nameFromProvider?: Nullable<string>\n  sentinelMaster?: SentinelMaster\n}\n\nconst DbInfoSentinel = (props: Props) => {\n  const { connectionType, nameFromProvider, sentinelMaster, host, port } = props\n\n  const dbInfo: DbInfoLabelValue[] = [\n    {\n      label: 'Connection Type:',\n      value: capitalize(connectionType),\n      dataTestId: 'connection-type',\n    },\n    {\n      label: 'Primary Group Name:',\n      value: sentinelMaster?.name,\n      dataTestId: 'primary-group-name',\n      hide: !sentinelMaster?.name,\n    },\n    {\n      label: 'Database Name from Provider:',\n      value: nameFromProvider,\n      dataTestId: 'db-name-from-provider',\n      hide: !nameFromProvider,\n    },\n    {\n      label: 'Sentinel Host & Port:',\n      value: `${host}:${port}`,\n      dataTestId: 'host-and-port',\n      additionalContent: (\n        <StyledCopyContainer>\n          <CopyButton copy={`${host}:${port}`} aria-label=\"Copy host:port\" />\n        </StyledCopyContainer>\n      ),\n      hide: !host || !port,\n    },\n  ]\n\n  return (\n    <DbInfoGroup flush maxWidth={false}>\n      {dbInfo\n        .filter((item) => !item.hide)\n        .map((item) => (\n          <ListGroupItemLabelValue\n            key={item.label}\n            label={item.label}\n            value={item.value}\n            dataTestId={item.dataTestId}\n            additionalContent={item.additionalContent}\n          />\n        ))}\n    </DbInfoGroup>\n  )\n}\n\nexport default DbInfoSentinel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/sentinel/PrimaryGroupSentinel.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\n\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { TextInput } from 'uiSrc/components/base/inputs'\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n}\n\nconst PrimaryGroupSentinel = (props: Props) => {\n  const { formik } = props\n  return (\n    <Col gap=\"l\">\n      <Row gap=\"m\" responsive>\n        <FlexItem grow>\n          <FormField label=\"Database alias\" required>\n            <TextInput\n              name=\"name\"\n              id=\"name\"\n              data-testid=\"name\"\n              placeholder=\"Enter Database Alias\"\n              value={formik.values.name ?? ''}\n              maxLength={500}\n              onChange={formik.handleChange}\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n      <Row gap=\"m\" responsive>\n        <FlexItem grow>\n          <FormField label=\"Primary group name\" required>\n            <TextInput\n              name=\"sentinelMasterName\"\n              id=\"sentinelMasterName\"\n              data-testid=\"primary-group\"\n              placeholder=\"Enter Primary Group Name\"\n              value={formik.values.sentinelMasterName ?? ''}\n              maxLength={500}\n              onChange={formik.handleChange}\n              disabled\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n    </Col>\n  )\n}\n\nexport default PrimaryGroupSentinel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/sentinel/SentinelMasterDatabase.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { SECURITY_FIELD } from 'uiSrc/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { PasswordInput, TextInput } from 'uiSrc/components/base/inputs'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport styles from '../../styles.module.scss'\n\nexport interface Props {\n  formik: FormikProps<DbConnectionInfo>\n  isCloneMode: boolean\n  db: Nullable<number>\n}\n\nconst SentinelMasterDatabase = (props: Props) => {\n  const { db, isCloneMode, formik } = props\n  return (\n    <>\n      {!!db && !isCloneMode && (\n        <Text color=\"subdued\" className={styles.sentinelCollapsedField}>\n          Database Index:\n          <span style={{ paddingLeft: 5 }}>\n            <ColorText>{db}</ColorText>\n          </span>\n        </Text>\n      )}\n      <Row gap=\"m\" responsive>\n        <FlexItem grow>\n          <FormField label=\"Username\">\n            <TextInput\n              name=\"sentinelMasterUsername\"\n              id=\"sentinelMasterUsername\"\n              maxLength={200}\n              placeholder=\"Enter Username\"\n              value={formik.values.sentinelMasterUsername ?? ''}\n              onChange={(value) =>\n                formik.setFieldValue('sentinelMasterUsername', value)\n              }\n              data-testid=\"sentinel-mater-username\"\n            />\n          </FormField>\n        </FlexItem>\n\n        <FlexItem grow>\n          <FormField label=\"Password\">\n            <PasswordInput\n              type=\"password\"\n              name=\"sentinelMasterPassword\"\n              id=\"sentinelMasterPassword\"\n              data-testid=\"sentinel-master-password\"\n              maxLength={200}\n              placeholder=\"Enter Password\"\n              value={\n                formik.values.sentinelMasterPassword === true\n                  ? SECURITY_FIELD\n                  : (formik.values.sentinelMasterPassword ?? '')\n              }\n              onChangeCapture={formik.handleChange}\n              onFocus={() => {\n                if (formik.values.sentinelMasterPassword === true) {\n                  formik.setFieldValue('sentinelMasterPassword', '')\n                }\n              }}\n              autoComplete=\"new-password\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n    </>\n  )\n}\n\nexport default SentinelMasterDatabase\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/sentinel/index.ts",
    "content": "import DbInfoSentinel from './DbInfoSentinel'\nimport PrimaryGroupSentinel from './PrimaryGroupSentinel'\nimport SentinelMasterDatabase from './SentinelMasterDatabase'\n\nexport { DbInfoSentinel, PrimaryGroupSentinel, SentinelMasterDatabase }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/form/types.ts",
    "content": "export interface DbInfoLabelValue {\n  label: string\n  value: string | React.ReactNode\n  dataTestId?: string\n  additionalContent?: React.ReactNode\n  hide?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/host-info-tooltip-content/HostInfoTooltipContent.styles.ts",
    "content": "import { HTMLAttributes } from 'react'\nimport styled from 'styled-components'\n\nexport const StyledUrlList = styled.ul<HTMLAttributes<HTMLUListElement>>`\n  margin-top: ${({ theme }) => theme.core.space.space050};\n  padding-left: ${({ theme }) => theme.core.space.space400};\n`\n\nexport const StyledUrlItem = styled.li<HTMLAttributes<HTMLLIElement>>`\n  font-weight: 300;\n  opacity: 0.85;\n  list-style: disc;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/host-info-tooltip-content/HostInfoTooltipContent.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { StyledUrlItem, StyledUrlList } from './HostInfoTooltipContent.styles'\n\nconst supportedUrls = [\n  'redis://[[username]:[password]]@host:port',\n  'rediss://[[username]:[password]]@host:port',\n  'host:port',\n]\n\nexport const HostInfoTooltipContent = ({\n  includeAutofillInfo,\n}: {\n  includeAutofillInfo: boolean\n}) => (\n  <>\n    {includeAutofillInfo && (\n      <Text variant=\"semiBold\">\n        Pasting a connection URL auto fills the database details.\n      </Text>\n    )}\n    <Text variant=\"semiBold\">The following connection URLs are supported:</Text>\n    <StyledUrlList>\n      {supportedUrls.map((url) => (\n        <StyledUrlItem key={url}>{url}</StyledUrlItem>\n      ))}\n    </StyledUrlList>\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/ImportDatabase.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  importInstancesFromFile,\n  importInstancesSelector,\n  resetImportInstances,\n} from 'uiSrc/slices/instances/instances'\nimport ImportDatabase from './ImportDatabase'\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  importInstancesSelector: jest.fn().mockReturnValue({\n    loading: false,\n    error: '',\n    data: null,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ImportDatabase', () => {\n  it('should render', () => {\n    expect(render(<ImportDatabase onClose={jest.fn} />)).toBeTruthy()\n  })\n\n  it('should call proper actions and send telemetry', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(\n      <div>\n        <ImportDatabase onClose={jest.fn()} />\n        <div id=\"footerDatabaseForm\" />\n      </div>,\n    )\n\n    const jsonString = JSON.stringify({})\n    const blob = new Blob([jsonString])\n    const file = new File([blob], 'empty.json', {\n      type: 'application/JSON',\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: [file] },\n      })\n    })\n\n    expect(screen.getByTestId('btn-submit')).not.toBeDisabled()\n    fireEvent.click(screen.getByTestId('btn-submit'))\n\n    const expectedActions = [importInstancesFromFile()]\n    expect(store.getActions()).toEqual(expectedActions)\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED,\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper actions on retry', async () => {\n    ;(importInstancesSelector as jest.Mock).mockImplementation(() => ({\n      loading: false,\n      data: null,\n      error: 'Error message',\n    }))\n\n    render(\n      <div>\n        <ImportDatabase onClose={jest.fn()} />\n        <div id=\"footerDatabaseForm\" />\n      </div>,\n    )\n\n    fireEvent.click(screen.getByTestId('btn-retry'))\n\n    const expectedActions = [resetImportInstances()]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render error message when 0 success databases added', () => {\n    ;(importInstancesSelector as jest.Mock).mockImplementation(() => ({\n      loading: false,\n      data: null,\n      error: 'Error message',\n    }))\n\n    render(<ImportDatabase onClose={jest.fn()} />)\n    expect(screen.getByTestId('result-failed')).toBeInTheDocument()\n    expect(screen.getByTestId('result-failed')).toHaveTextContent(\n      'Error message',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/ImportDatabase.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport ReactDOM from 'react-dom'\n\nimport {\n  fetchInstancesAction,\n  importInstancesSelector,\n  resetImportInstances,\n  uploadInstancesFile,\n} from 'uiSrc/slices/instances/instances'\nimport { Nullable } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { RiFilePicker, RiTooltip, UploadWarning } from 'uiSrc/components'\nimport { useModalHeader } from 'uiSrc/contexts/ModalTitleProvider'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon, RiIcon } from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { Loader } from 'uiSrc/components/base/display'\nimport ResultsLog from './components/ResultsLog'\n\nimport { ScrollableWrapper } from '../ManualConnection.styles'\n\nexport interface Props {\n  onClose: () => void\n}\n\nconst MAX_MB_FILE = 10\nconst MAX_FILE_SIZE = MAX_MB_FILE * 1024 * 1024\n\nconst ImportDatabase = (props: Props) => {\n  const { onClose } = props\n  const { loading, data, error } = useSelector(importInstancesSelector)\n  const [files, setFiles] = useState<Nullable<FileList>>(null)\n  const [isInvalid, setIsInvalid] = useState<boolean>(false)\n  const [isSubmitDisabled, setIsSubmitDisabled] = useState<boolean>(true)\n  const [domReady, setDomReady] = useState(false)\n\n  const dispatch = useDispatch()\n  const { setModalHeader } = useModalHeader()\n\n  useEffect(() => {\n    setDomReady(true)\n\n    setModalHeader(<Title size=\"M\">Import from file</Title>, true)\n\n    return () => {\n      setModalHeader(null)\n    }\n  }, [])\n\n  const onFileChange = (files: FileList | null) => {\n    setFiles(files)\n    setIsInvalid(!!files?.length && files?.[0].size > MAX_FILE_SIZE)\n    setIsSubmitDisabled(!files?.length || files[0].size > MAX_FILE_SIZE)\n  }\n\n  const handleOnClose = () => {\n    onClose()\n    dispatch(resetImportInstances())\n\n    if (!data) {\n      sendEventTelemetry({\n        event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_CANCELLED,\n      })\n    }\n  }\n\n  const onClickRetry = () => {\n    dispatch(resetImportInstances())\n    onFileChange(null)\n  }\n\n  const onSubmit = () => {\n    if (files) {\n      const formData = new FormData()\n      formData.append('file', files[0])\n\n      dispatch(\n        uploadInstancesFile(formData, (data) => {\n          if (data?.success?.length || data?.partial?.length) {\n            dispatch(fetchInstancesAction())\n          }\n        }),\n      )\n\n      sendEventTelemetry({\n        event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED,\n      })\n    }\n  }\n\n  const Footer = () => {\n    const footerEl = document.getElementById('footerDatabaseForm')\n    if (!domReady || !footerEl) return null\n\n    if (error) {\n      return ReactDOM.createPortal(\n        <Row justify=\"end\" gap=\"m\" data-testid=\"footer-import-database\">\n          <PrimaryButton\n            color=\"secondary\"\n            onClick={onClickRetry}\n            data-testid=\"btn-retry\"\n          >\n            Retry\n          </PrimaryButton>\n        </Row>,\n        footerEl,\n      )\n    }\n\n    if (data) {\n      return ReactDOM.createPortal(\n        <Row justify=\"end\" gap=\"m\" data-testid=\"footer-import-database\">\n          <PrimaryButton\n            type=\"submit\"\n            onClick={handleOnClose}\n            data-testid=\"btn-close\"\n          >\n            OK\n          </PrimaryButton>\n        </Row>,\n        footerEl,\n      )\n    }\n\n    return ReactDOM.createPortal(\n      <Row justify=\"end\" gap=\"m\" data-testid=\"footer-import-database\">\n        <SecondaryButton className=\"btn-cancel\" onClick={handleOnClose}>\n          Cancel\n        </SecondaryButton>\n        <RiTooltip\n          position=\"top\"\n          content={isSubmitDisabled ? 'Upload a file' : undefined}\n        >\n          <PrimaryButton\n            type=\"submit\"\n            onClick={onSubmit}\n            loading={loading}\n            disabled={isSubmitDisabled}\n            icon={isSubmitDisabled ? InfoIcon : undefined}\n            data-testid=\"btn-submit\"\n          >\n            Submit\n          </PrimaryButton>\n        </RiTooltip>\n      </Row>,\n      footerEl,\n    )\n  }\n\n  const isShowForm = !loading && !data && !error\n\n  return (\n    <>\n      <ScrollableWrapper data-testid=\"add-db_import\">\n        <Col gap=\"xl\">\n          <Col grow gap=\"xl\">\n            {isShowForm && (\n              <Col gap=\"xl\">\n                <Text>\n                  Use a JSON file to import your database connections. Ensure\n                  that you only use files from trusted sources to prevent the\n                  risk of automatically executing malicious code.\n                </Text>\n\n                <RiFilePicker\n                  id=\"import-file-modal-filepicker\"\n                  initialPromptText=\"Select or drag and drop a file\"\n                  isInvalid={isInvalid}\n                  onChange={onFileChange}\n                  display=\"large\"\n                  data-testid=\"import-file-modal-filepicker\"\n                  aria-label=\"Select or drag and drop file\"\n                />\n\n                {isInvalid && (\n                  <ColorText color=\"danger\" data-testid=\"input-file-error-msg\">\n                    {`File should not exceed ${MAX_MB_FILE} MB`}\n                  </ColorText>\n                )}\n              </Col>\n            )}\n            {loading && (\n              <Col\n                justify=\"center\"\n                gap=\"l\"\n                align=\"center\"\n                data-testid=\"file-loading-indicator\"\n              >\n                <Loader size=\"xl\" />\n                <Text>Uploading...</Text>\n              </Col>\n            )}\n            {error && (\n              <Col\n                align=\"center\"\n                gap=\"l\"\n                justify=\"center\"\n                data-testid=\"result-failed\"\n              >\n                <RiIcon\n                  type=\"IndicatorXIcon\"\n                  color=\"danger600\"\n                  customSize=\"5rem\"\n                />\n                <Text>Failed to add database connections</Text>\n                <Text>{error}</Text>\n              </Col>\n            )}\n          </Col>\n          {isShowForm && (\n            <FlexItem>\n              <UploadWarning />\n            </FlexItem>\n          )}\n        </Col>\n        {data && (\n          <Row justify=\"center\">\n            <ResultsLog data={data} />\n          </Row>\n        )}\n      </ScrollableWrapper>\n      <Footer />\n    </>\n  )\n}\n\nexport default ImportDatabase\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/ResultsLog/ResultLog.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\n// Ideally this should not be needed, but the section component\n// will not let the parent cut the border, more precisely the box-shadow,\n// so we need to add padding to the parent container\nexport const StyledColWrapper = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space025};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/ResultsLog/ResultsLog.config.tsx",
    "content": "import { ImportDatabaseResultType } from 'uiSrc/constants'\nimport { TableResultData } from './ResultsLog'\n\nexport const RESULTS_DATA_CONFIG: TableResultData[] = [\n  {\n    type: ImportDatabaseResultType.Success,\n    title: 'Fully imported',\n  },\n  {\n    type: ImportDatabaseResultType.Partial,\n    title: 'Partially imported',\n  },\n  {\n    type: ImportDatabaseResultType.Fail,\n    title: 'Failed to import',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/ResultsLog/ResultsLog.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, within, toggleAccordion } from 'uiSrc/utils/test-utils'\nimport { ImportDatabasesData } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport ResultsLog from './ResultsLog'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockedError = { statusCode: 400, message: 'message', error: 'error' }\ndescribe('ResultsLog', () => {\n  it('should render', () => {\n    const mockedData = { total: 0, fail: [], partial: [], success: [] }\n    render(<ResultsLog data={mockedData} />)\n  })\n\n  it('should be all collapsed nav groups', () => {\n    const mockedData: ImportDatabasesData = {\n      total: 3,\n      fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n      partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n      success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }],\n    }\n    render(<ResultsLog data={mockedData} />)\n\n    expect(screen.getByTestId('success-results-closed')).toBeInTheDocument()\n    expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument()\n    expect(screen.getByTestId('fail-results-closed')).toBeInTheDocument()\n  })\n\n  it('should open and collapse other groups', async () => {\n    const mockedData: ImportDatabasesData = {\n      total: 3,\n      fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n      partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n      success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }],\n    }\n    render(<ResultsLog data={mockedData} />)\n\n    await toggleAccordion('success-results-closed')\n    expect(screen.getByTestId('success-results-open')).toBeInTheDocument()\n\n    expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument()\n    expect(screen.getByTestId('fail-results-closed')).toBeInTheDocument()\n\n    await toggleAccordion('fail-results-closed')\n    expect(screen.getByTestId('fail-results-open')).toBeInTheDocument()\n\n    expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument()\n    expect(screen.getByTestId('success-results-closed')).toBeInTheDocument()\n\n    await toggleAccordion('partial-results-closed')\n    expect(screen.getByTestId('partial-results-open')).toBeInTheDocument()\n\n    expect(screen.getByTestId('fail-results-closed')).toBeInTheDocument()\n    expect(screen.getByTestId('success-results-closed')).toBeInTheDocument()\n  })\n\n  it('should show proper items length', () => {\n    const mockedData: ImportDatabasesData = {\n      total: 4,\n      fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n      partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n      success: [\n        { index: 1, status: 'success', port: 1233, host: 'localhost' },\n        { index: 3, status: 'success', port: 1233, host: 'localhost' },\n      ],\n    }\n    render(<ResultsLog data={mockedData} />)\n\n    expect(\n      within(screen.getByTestId('success-results-closed')).getByTestId(\n        'number-of-dbs',\n      ),\n    ).toHaveTextContent('2')\n    expect(\n      within(screen.getByTestId('partial-results-closed')).getByTestId(\n        'number-of-dbs',\n      ),\n    ).toHaveTextContent('1')\n    expect(\n      within(screen.getByTestId('fail-results-closed')).getByTestId(\n        'number-of-dbs',\n      ),\n    ).toHaveTextContent('1')\n  })\n\n  it('should call proper telemetry event after click', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    const mockedData: ImportDatabasesData = {\n      total: 3,\n      fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n      partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n      success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }],\n    }\n    render(<ResultsLog data={mockedData} />)\n\n    await toggleAccordion('success-results-closed')\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED,\n      eventData: {\n        length: 1,\n        name: 'success',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    await toggleAccordion('success-results-open')\n\n    expect(sendEventTelemetry).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/ResultsLog/ResultsLog.tsx",
    "content": "import React, { useEffect, useMemo, useState } from 'react'\n\nimport { ImportDatabasesData } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Nullable } from 'uiSrc/utils'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RICollapsibleNavGroup } from 'uiSrc/components/base/display'\nimport { ImportDatabaseResultType } from 'uiSrc/constants'\n\nimport TableResult from '../TableResult'\nimport { StyledColWrapper } from './ResultLog.styles'\nimport { RESULTS_DATA_CONFIG } from './ResultsLog.config'\n\ninterface Props {\n  data: Nullable<ImportDatabasesData>\n}\n\nexport interface TableResultData {\n  type: ImportDatabaseResultType\n  title: string\n}\n\nconst ResultsLog = ({ data }: Props) => {\n  const [openedNav, setOpenedNav] =\n    useState<Nullable<ImportDatabaseResultType>>(null)\n\n  const resultsData: TableResultData[] = useMemo(() => {\n    return RESULTS_DATA_CONFIG.filter(\n      (item) => data && data[item.type] && data[item.type].length > 0,\n    )\n  }, [data])\n\n  useEffect(() => {\n    if (openedNav) {\n      sendEventTelemetry({\n        event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED,\n        eventData: {\n          length: data?.[openedNav]?.length ?? 0,\n          name: openedNav,\n        },\n      })\n    }\n  }, [openedNav])\n\n  return (\n    <StyledColWrapper gap=\"l\">\n      {resultsData.map((item) => {\n        const navState = openedNav === item.type ? 'open' : 'closed'\n        return (\n          <RICollapsibleNavGroup\n            key={item.type}\n            title={\n              <Row gap=\"s\">\n                <Text data-testid=\"nav-group-title\">{item.title}:</Text>\n                <Text data-testid=\"number-of-dbs\">\n                  {data?.[item.type]?.length ?? 0}\n                </Text>\n              </Row>\n            }\n            data-testid={`${item.type}-results-${navState}`}\n            id={`${item.type}-results-${navState}`}\n            initialIsOpen={false}\n            onToggle={(isOpen) => setOpenedNav(isOpen ? item.type : null)}\n            forceState={navState}\n            open={openedNav === item.type}\n          >\n            <TableResult data={data?.[item.type] ?? []} />\n          </RICollapsibleNavGroup>\n        )\n      })}\n    </StyledColWrapper>\n  )\n}\n\nexport default ResultsLog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/ResultsLog/index.ts",
    "content": "import ResultsLog from './ResultsLog'\n\nexport default ResultsLog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/TableResult/TableResult.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport TableResult from './TableResult'\n\nconst mockedError = { statusCode: 400, message: 'message', error: 'error' }\n\ndescribe('TableResult', () => {\n  it('should render', () => {\n    render(<TableResult data={[{ index: 0, status: 'success' }]} />)\n  })\n\n  it('should not render table for empty data', () => {\n    const { container } = render(<TableResult data={[]} />)\n\n    expect(container.childNodes.length).toBe(0)\n  })\n\n  it('should render table data with success messages', () => {\n    render(\n      <TableResult\n        data={[\n          { index: 0, status: 'success', port: 1233, host: 'localhost' },\n          { index: 1, status: 'success', port: 5233, host: 'localhost2' },\n        ]}\n      />,\n    )\n\n    expect(screen.getByTestId('table-index-0')).toHaveTextContent('(0)')\n    expect(screen.getByTestId('table-index-1')).toHaveTextContent('(1)')\n    expect(screen.getByTestId('table-host-port-0')).toHaveTextContent(\n      'localhost:1233',\n    )\n    expect(screen.getByTestId('table-host-port-1')).toHaveTextContent(\n      'localhost2:5233',\n    )\n    expect(screen.getByTestId('table-result-0')).toHaveTextContent('Successful')\n    expect(screen.getByTestId('table-result-1')).toHaveTextContent('Successful')\n  })\n\n  it('should render table data with error messages', () => {\n    render(\n      <TableResult\n        data={[\n          {\n            index: 0,\n            status: 'error',\n            port: 1233,\n            errors: [mockedError, mockedError],\n          },\n          {\n            index: 1,\n            status: 'error',\n            host: 'localhost2',\n            errors: [mockedError],\n          },\n        ]}\n      />,\n    )\n    expect(screen.getByTestId('table-result-0')).toHaveTextContent(\n      [mockedError, mockedError].map((e) => e.message).join(''),\n    )\n    expect(screen.getByTestId('table-result-1')).toHaveTextContent(\n      [mockedError].map((e) => e.message).join(''),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/TableResult/TableResult.tsx",
    "content": "import React from 'react'\n\nimport { Table, ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  ImportTableResultColumn,\n  TABLE_IMPORT_RESULT_COLUMN_ID_HEADER_MAP,\n} from 'uiSrc/constants'\nimport { ErrorImportResult } from 'uiSrc/slices/interfaces'\n\nexport interface DataImportResult {\n  index: number\n  status: string\n  errors?: Array<ErrorImportResult>\n  host?: string\n  port?: number\n}\n\nexport interface Props {\n  data: Array<DataImportResult>\n}\n\nconst TableResult = (props: Props) => {\n  const { data } = props\n\n  const ErrorResult = ({ errors }: { errors: string[] }) => (\n    <ul>\n      {errors.map((message, i) => (\n        <li key={String(Math.random() * i)}>{message}</li>\n      ))}\n    </ul>\n  )\n\n  const columns: ColumnDef<DataImportResult>[] = [\n    {\n      header: TABLE_IMPORT_RESULT_COLUMN_ID_HEADER_MAP.get(\n        ImportTableResultColumn.Index,\n      ),\n      id: ImportTableResultColumn.Index,\n      accessorKey: ImportTableResultColumn.Index,\n      cell: ({\n        row: {\n          original: { index },\n        },\n      }) => <span data-testid={`table-index-${index}`}>({index})</span>,\n      size: 50,\n    },\n    {\n      header: TABLE_IMPORT_RESULT_COLUMN_ID_HEADER_MAP.get(\n        ImportTableResultColumn.Host,\n      ),\n      id: ImportTableResultColumn.Host,\n      accessorKey: ImportTableResultColumn.Host,\n      cell: ({\n        row: {\n          original: { host, port, index },\n        },\n      }) => (\n        <div data-testid={`table-host-port-${index}`}>\n          {host}:{port}\n        </div>\n      ),\n    },\n    {\n      header: TABLE_IMPORT_RESULT_COLUMN_ID_HEADER_MAP.get(\n        ImportTableResultColumn.Errors,\n      ),\n      id: ImportTableResultColumn.Errors,\n      accessorKey: 'errors',\n      cell: ({\n        row: {\n          original: { errors, index },\n        },\n      }) => (\n        <div data-testid={`table-result-${index}`}>\n          {errors ? (\n            <ErrorResult errors={errors.map((e) => e.message)} />\n          ) : (\n            'Successful'\n          )}\n        </div>\n      ),\n    },\n  ]\n\n  if (data?.length === 0) return null\n\n  return (\n    <Table\n      columns={columns}\n      data={data}\n      defaultSorting={[]}\n      maxHeight=\"20rem\"\n    />\n  )\n}\n\nexport default TableResult\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/components/TableResult/index.ts",
    "content": "import TableResult from './TableResult'\n\nexport default TableResult\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/import-database/index.ts",
    "content": "import ImportDatabase from './ImportDatabase'\n\nexport default ImportDatabase\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { act } from '@testing-library/react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { SubmitBtnText } from 'uiSrc/pages/home/constants'\nimport ManualConnectionFrom, {\n  Props as ManualConnectionFromProps,\n} from 'uiSrc/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport ManualConnectionWrapper, { Props } from './ManualConnectionWrapper'\n\nconst mockedProps = mock<Props>()\n\njest.mock('./manual-connection-form/ManualConnectionForm', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockManualConnectionFrom = (props: ManualConnectionFromProps) => (\n  <div>\n    <button\n      type=\"button\"\n      onClick={() => props.onHostNamePaste('redis-12000.cluster.local:12000')}\n      data-testid=\"onHostNamePaste-btn\"\n    >\n      onHostNamePaste\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.onClose()}\n      data-testid=\"onClose-btn\"\n    >\n      onClose\n    </button>\n    <button\n      type=\"submit\"\n      data-testid=\"btn-submit\"\n      onClick={() => props.onSubmit({})}\n    >\n      {props.submitButtonText}\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.setIsCloneMode(!props.isCloneMode)}\n      data-testid=\"onClone-btn\"\n    >\n      onClone\n    </button>\n  </div>\n)\n\ndescribe('ManualConnectionWrapper', () => {\n  beforeAll(() => {\n    ManualConnectionFrom.mockImplementation(mockManualConnectionFrom)\n  })\n  it('should render', () => {\n    expect(\n      render(<ManualConnectionWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should call onHostNamePaste', () => {\n    const component = render(\n      <ManualConnectionWrapper {...instance(mockedProps)} />,\n    )\n    fireEvent.click(screen.getByTestId('onHostNamePaste-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onClose', () => {\n    const onClose = jest.fn()\n    render(\n      <ManualConnectionWrapper {...instance(mockedProps)} onClose={onClose} />,\n    )\n    fireEvent.click(screen.getByTestId('onClose-btn'))\n    expect(onClose).toBeCalled()\n  })\n\n  it('should have add database submit button', () => {\n    render(<ManualConnectionWrapper {...instance(mockedProps)} />)\n    expect(screen.getByTestId('btn-submit')).toHaveTextContent(\n      SubmitBtnText.AddDatabase,\n    )\n  })\n\n  it('should have edit database submit button', () => {\n    render(<ManualConnectionWrapper {...instance(mockedProps)} editMode />)\n    expect(screen.getByTestId('btn-submit')).toHaveTextContent(\n      SubmitBtnText.EditDatabase,\n    )\n  })\n\n  it('should have edit database submit button', () => {\n    render(<ManualConnectionWrapper {...instance(mockedProps)} editMode />)\n    act(() => {\n      fireEvent.click(screen.getByTestId('onClone-btn'))\n    })\n    expect(screen.getByTestId('btn-submit')).toHaveTextContent(\n      SubmitBtnText.CloneDatabase,\n    )\n  })\n\n  it('should call proper telemetry event on Add database', () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    sendEventTelemetry.mockRestore()\n    render(<ManualConnectionWrapper {...instance(mockedProps)} />)\n    act(() => {\n      fireEvent.click(screen.getByTestId('btn-submit'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED,\n    })\n  })\n\n  it('should call proper telemetry event on Clone database', () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    sendEventTelemetry.mockRestore()\n    render(<ManualConnectionWrapper {...instance(mockedProps)} editMode />)\n    act(() => {\n      fireEvent.click(screen.getByTestId('onClone-btn'))\n    })\n    act(() => {\n      fireEvent.click(screen.getByTestId('onClose-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED,\n      eventData: { databaseId: undefined },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/ManualConnectionWrapper.tsx",
    "content": "import { pick, toNumber, omit } from 'lodash'\nimport React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router'\n\nimport {\n  checkConnectToInstanceAction,\n  createInstanceStandaloneAction,\n  instancesSelector,\n  testInstanceStandaloneAction,\n  updateInstanceAction,\n  cloneInstanceAction,\n} from 'uiSrc/slices/instances/instances'\nimport {\n  Nullable,\n  removeEmpty,\n  getFormUpdates,\n  transformQueryParamsObject,\n  getDiffKeysOfObjectValues,\n  isAzureDatabase,\n} from 'uiSrc/utils'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { fetchCaCerts } from 'uiSrc/slices/instances/caCerts'\nimport { ConnectionType, Instance, InstanceType } from 'uiSrc/slices/interfaces'\nimport { DbType, Pages } from 'uiSrc/constants'\nimport { fetchClientCerts } from 'uiSrc/slices/instances/clientCerts'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\nimport {\n  appRedirectionSelector,\n  setUrlHandlingInitialState,\n} from 'uiSrc/slices/app/url-handling'\nimport { getRedirectionPage } from 'uiSrc/utils/routing'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport {\n  applyTlSDatabase,\n  applySSHDatabase,\n  autoFillFormDetails,\n  getTlsSettings,\n  getFormValues,\n} from 'uiSrc/pages/home/utils'\nimport {\n  DEFAULT_TIMEOUT,\n  SubmitBtnText,\n  ADD_NEW,\n  ADD_NEW_CA_CERT,\n} from 'uiSrc/pages/home/constants'\nimport ManualConnectionForm from './manual-connection-form'\n\nexport interface Props {\n  editMode: boolean\n  urlHandlingAction?: Nullable<UrlHandlingActions>\n  initialValues?: Nullable<Record<string, any>>\n  editedInstance: Nullable<Instance>\n  onClose?: () => void\n  onClickBack?: () => void\n  onDbEdited?: () => void\n  onAliasEdited?: (value: string) => void\n}\n\nconst ManualConnectionWrapper = (props: Props) => {\n  const {\n    editMode,\n    onClose,\n    onClickBack,\n    onDbEdited,\n    onAliasEdited,\n    editedInstance,\n    urlHandlingAction,\n    initialValues: initialValuesProp,\n  } = props\n  const [formFields, setFormFields] = useState(\n    getFormValues(editedInstance || initialValuesProp),\n  )\n\n  const [isCloneMode, setIsCloneMode] = useState<boolean>(false)\n\n  const { loadingChanging: loadingStandalone } = useSelector(instancesSelector)\n  const { server } = useSelector(appInfoSelector)\n  const { properties: urlHandlingProperties } = useSelector(\n    appRedirectionSelector,\n  )\n\n  const connectionType = editedInstance?.connectionType ?? DbType.STANDALONE\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(fetchCaCerts())\n    dispatch(fetchClientCerts())\n  }, [])\n\n  useEffect(() => {\n    setFormFields(getFormValues(editedInstance || initialValuesProp))\n    setIsCloneMode(false)\n  }, [editedInstance, initialValuesProp])\n\n  const onMastersSentinelFetched = () => {\n    history.push(Pages.sentinelDatabases)\n  }\n\n  const handleSuccessConnectWithRedirect = (id: string) => {\n    const { redirect } = urlHandlingProperties\n    dispatch(setUrlHandlingInitialState())\n\n    dispatch(\n      checkConnectToInstanceAction(id, (id) => {\n        if (redirect) {\n          const pageToRedirect = getRedirectionPage(redirect, id)\n\n          if (pageToRedirect) {\n            history.push(pageToRedirect)\n          }\n        }\n      }),\n    )\n  }\n\n  const handleAddDatabase = (payload: any) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_MANUALLY_SUBMITTED,\n    })\n\n    if (urlHandlingAction === UrlHandlingActions.Connect) {\n      const cloudDetails = transformQueryParamsObject(\n        pick(urlHandlingProperties, [\n          'cloudId',\n          'subscriptionType',\n          'planMemoryLimit',\n          'memoryLimitMeasurementUnit',\n          'free',\n        ]),\n      )\n\n      const db = { ...payload }\n      if (cloudDetails?.cloudId) {\n        db.cloudDetails = cloudDetails\n      }\n\n      dispatch(\n        createInstanceStandaloneAction(\n          db,\n          undefined,\n          handleSuccessConnectWithRedirect,\n        ),\n      )\n      return\n    }\n\n    dispatch(createInstanceStandaloneAction(payload, onMastersSentinelFetched))\n  }\n  const handleEditDatabase = (payload: any) => {\n    dispatch(updateInstanceAction(payload, onDbEdited))\n  }\n\n  const handleCloneDatabase = (payload: any) => {\n    dispatch(cloneInstanceAction(payload))\n  }\n\n  const handleTestConnectionDatabase = (values: DbConnectionInfo) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_TEST_CONNECTION_CLICKED,\n    })\n    const payload = preparePayload(values)\n\n    dispatch(testInstanceStandaloneAction(payload))\n  }\n\n  const handleConnectionFormSubmit = (values: DbConnectionInfo) => {\n    if (isCloneMode) {\n      const diffKeys = getDiffKeysOfObjectValues(formFields, values)\n      sendEventTelemetry({\n        event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED,\n        eventData: {\n          fieldsModified: diffKeys,\n        },\n      })\n    }\n    const payload = preparePayload(values)\n\n    if (isCloneMode) {\n      handleCloneDatabase(payload)\n      return\n    }\n    if (editMode) {\n      handleEditDatabase(payload)\n      return\n    }\n\n    handleAddDatabase(payload)\n  }\n\n  const preparePayload = (values: any) => {\n    const tlsSettings = getTlsSettings(values)\n\n    const {\n      name,\n      host,\n      port,\n      db,\n      username,\n      password,\n      timeout,\n      compressor,\n      sentinelMasterName,\n      sentinelMasterUsername,\n      sentinelMasterPassword,\n      ssh,\n      tls,\n      forceStandalone,\n      keyNameFormat,\n    } = values\n\n    const database: any = {\n      id: editedInstance?.id,\n      name,\n      host,\n      port: +port,\n      db: +(db || 0),\n      username,\n      password,\n      compressor,\n      timeout: timeout ? toNumber(timeout) * 1_000 : toNumber(DEFAULT_TIMEOUT),\n      ssh,\n      tls,\n      forceStandalone,\n      keyNameFormat,\n    }\n\n    // add tls & ssh for database (modifies database object)\n    applyTlSDatabase(database, tlsSettings)\n    applySSHDatabase(database, values)\n\n    if (connectionType === ConnectionType.Sentinel) {\n      database.sentinelMaster = {\n        name: sentinelMasterName,\n        username: sentinelMasterUsername,\n        password: sentinelMasterPassword,\n      }\n    }\n\n    if (editMode) {\n      database.id = editedInstance?.id\n\n      const updatedValues = getFormUpdates(\n        database,\n        omit(editedInstance, ['id']),\n      )\n\n      // When a new caCert/clientCert is deleted, the editedInstance\n      // is not updated with the deletion until 'apply' is\n      // clicked. Once the apply is clicked, the editedInstance object\n      // that is validated against, still has the older certificates\n      // attached. Attaching the new certs to the final object helps.\n      if (\n        values.selectedCaCertName === ADD_NEW_CA_CERT &&\n        values.newCaCertName !== '' &&\n        editedInstance &&\n        values.newCaCertName === editedInstance.caCert?.name\n      ) {\n        updatedValues.caCert = database.caCert\n      }\n\n      if (\n        values.selectedTlsClientCertId === ADD_NEW &&\n        values.newTlsCertPairName !== '' &&\n        values.newTlsCertPairName === editedInstance?.clientCert?.name\n      ) {\n        updatedValues.clientCert = database.clientCert\n      }\n\n      return updatedValues\n    }\n\n    return removeEmpty(database)\n  }\n\n  const handleOnClose = () => {\n    dispatch(setUrlHandlingInitialState())\n\n    if (isCloneMode) {\n      sendEventTelemetry({\n        event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED,\n        eventData: {\n          databaseId: editedInstance?.id,\n        },\n      })\n    }\n    onClose?.()\n  }\n\n  const getSubmitButtonText = () => {\n    if (isCloneMode) {\n      return SubmitBtnText.CloneDatabase\n    }\n    if (editMode) {\n      return SubmitBtnText.EditDatabase\n    }\n    return SubmitBtnText.AddDatabase\n  }\n\n  const handlePostHostName = (content: string): boolean =>\n    autoFillFormDetails(\n      content,\n      formFields,\n      setFormFields,\n      InstanceType.Standalone,\n    )\n\n  const isFromAzure = isAzureDatabase(editedInstance)\n\n  return (\n    <ManualConnectionForm\n      formFields={formFields}\n      connectionType={connectionType}\n      loading={loadingStandalone}\n      buildType={server?.buildType as BuildType}\n      submitButtonText={getSubmitButtonText()}\n      onSubmit={handleConnectionFormSubmit}\n      onTestConnection={handleTestConnectionDatabase}\n      onClose={handleOnClose}\n      onHostNamePaste={handlePostHostName}\n      isEditMode={editMode}\n      isCloneMode={isCloneMode}\n      setIsCloneMode={setIsCloneMode}\n      onAliasEdited={onAliasEdited}\n      onClickBack={onClickBack}\n      isFromAzure={isFromAzure}\n    />\n  )\n}\n\nexport default ManualConnectionWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/index.ts",
    "content": "import ManualConnectionWrapper from './ManualConnectionWrapper'\n\nexport default ManualConnectionWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\nimport { NO_CA_CERT } from 'uiSrc/pages/home/constants'\nimport ManualConnectionForm from './'\n\nconst conn = DBInstanceFactory.build()\nconst meta: Meta<typeof ManualConnectionForm> = {\n  component: ManualConnectionForm,\n  args: {\n    formFields: {\n      ...conn,\n      port: conn.port.toString(),\n      selectedCaCertName: NO_CA_CERT,\n    } as any,\n    loading: false,\n    isEditMode: false,\n    isCloneMode: false,\n    setIsCloneMode: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const AddConnection: Story = {}\nexport const CloneConnection: Story = {\n  args: {\n    ...meta.args,\n    isCloneMode: true,\n  },\n}\nexport const EditConnection: Story = {\n  args: {\n    ...meta.args,\n    isEditMode: true,\n  },\n}\n\nexport const EditWithNodesConnection: Story = {\n  args: {\n    ...meta.args,\n    formFields: {\n      ...conn,\n      port: conn.port.toString(),\n      selectedCaCertName: NO_CA_CERT,\n      nodes: [\n        {\n          host: '127.0.0.1',\n          port: 6666,\n        },\n        {\n          host: '127.0.0.1',\n          port: 7777,\n        },\n      ],\n    } as any,\n    isEditMode: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionForm.tsx",
    "content": "import { FormikErrors, useFormik } from 'formik'\nimport { isEmpty, pick } from 'lodash'\nimport React, { useEffect, useRef, useState } from 'react'\nimport ReactDOM from 'react-dom'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { resetInstanceUpdateAction } from 'uiSrc/slices/instances/instances'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { appRedirectionSelector } from 'uiSrc/slices/app/url-handling'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\n\nimport { fieldDisplayNames, SubmitBtnText } from 'uiSrc/pages/home/constants'\nimport { getFormErrors } from 'uiSrc/pages/home/utils'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { DbInfo } from 'uiSrc/pages/home/components/form'\nimport { DbInfoSentinel } from 'uiSrc/pages/home/components/form/sentinel'\nimport { caCertsSelector } from 'uiSrc/slices/instances/caCerts'\nimport { clientCertsSelector } from 'uiSrc/slices/instances/clientCerts'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { useModalHeader } from 'uiSrc/contexts/ModalTitleProvider'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { ChevronLeftIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport TabsComponent from 'uiSrc/components/base/layout/tabs'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { MANUAL_FORM_TABS, ManualFormTab } from './constants'\nimport CloneConnection from './components/CloneConnection'\nimport FooterActions from './components/FooterActions'\nimport { AddConnection, EditConnection, EditSentinelConnection } from './forms'\n\nimport {\n  ScrollableWrapper,\n  ContentWrapper,\n} from '../../ManualConnection.styles'\n\nexport interface Props {\n  formFields: DbConnectionInfo\n  submitButtonText?: SubmitBtnText\n  loading: boolean\n  buildType?: BuildType\n  isEditMode: boolean\n  isCloneMode: boolean\n  isFromAzure?: boolean\n  setIsCloneMode: (value: boolean) => void\n  onSubmit: (values: DbConnectionInfo) => void\n  onTestConnection: (values: DbConnectionInfo) => void\n  onHostNamePaste: (content: string) => boolean\n  onClose?: () => void\n}\n\nconst getInitFieldsDisplayNames = ({ host, port, name }: any) => {\n  if (!host || !port || !name) {\n    return pick(fieldDisplayNames, ['host', 'port', 'name'])\n  }\n  return {}\n}\n\nconst ManualConnectionForm = (props: Props) => {\n  const {\n    formFields,\n    onClose,\n    onSubmit,\n    onTestConnection,\n    onHostNamePaste,\n    submitButtonText,\n    buildType,\n    loading,\n    isEditMode,\n    isCloneMode,\n    setIsCloneMode,\n    isFromAzure = false,\n  } = props\n\n  const {\n    id,\n    host,\n    name,\n    port,\n    db = null,\n    nameFromProvider,\n    sentinelMaster,\n    connectionType,\n    nodes = null,\n    modules,\n  } = formFields\n\n  const { action } = useSelector(appRedirectionSelector)\n  const { data: caCertificates } = useSelector(caCertsSelector)\n  const { data: certificates } = useSelector(clientCertsSelector)\n  const { server } = useSelector(appInfoSelector)\n\n  const [errors, setErrors] = useState<FormikErrors<DbConnectionInfo>>(\n    getInitFieldsDisplayNames({ host, port, name }),\n  )\n  const [activeTab, setActiveTab] = useState<ManualFormTab>(\n    ManualFormTab.General,\n  )\n\n  const { setModalHeader } = useModalHeader()\n\n  const dispatch = useDispatch()\n\n  const formRef = useRef<HTMLDivElement>(null)\n\n  const submitIsDisable = () => !isEmpty(errors)\n  const isFromCloud = action === UrlHandlingActions.Connect\n\n  const validate = (values: DbConnectionInfo) => {\n    const errs = getFormErrors(values)\n\n    if (\n      isCloneMode &&\n      connectionType === ConnectionType.Sentinel &&\n      !values.sentinelMasterName\n    ) {\n      errs.sentinelMasterName = fieldDisplayNames.sentinelMasterName\n    }\n\n    if (!values.name) {\n      errs.name = fieldDisplayNames.name\n    }\n\n    setErrors(errs)\n    return errs\n  }\n\n  const formik = useFormik({\n    initialValues: formFields,\n    validate,\n    enableReinitialize: true,\n    validateOnMount: true,\n    onSubmit: (values: any) => {\n      onSubmit(values)\n    },\n  })\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLFormElement>) => {\n    if (event.key === keys.ENTER && !submitIsDisable()) {\n      // event.\n      formik.submitForm()\n    }\n  }\n\n  useEffect(\n    () =>\n      // componentWillUnmount\n      () => {\n        setModalHeader(null)\n        if (isEditMode) {\n          dispatch(resetInstanceUpdateAction())\n        }\n      },\n    [],\n  )\n\n  useEffect(() => {\n    if (isCloneMode) {\n      setModalHeader(\n        <Row align=\"center\" gap=\"s\">\n          <FlexItem>\n            <IconButton\n              onClick={handleClickBackClone}\n              icon={ChevronLeftIcon}\n              aria-label=\"back\"\n              data-testid=\"back-btn\"\n            />\n          </FlexItem>\n          <FlexItem grow>\n            <Title size=\"L\">Clone Database</Title>\n          </FlexItem>\n        </Row>,\n      )\n      return\n    }\n\n    if (isEditMode) {\n      setModalHeader(<Title size=\"L\">Edit Database</Title>)\n      return\n    }\n\n    setModalHeader(<Title size=\"L\">Connection settings</Title>, true)\n  }, [isEditMode, isCloneMode])\n\n  useEffect(() => {\n    formik.resetForm()\n  }, [isCloneMode])\n\n  const handleTestConnectionDatabase = () => {\n    onTestConnection(formik.values)\n  }\n\n  const handleClickBackClone = () => {\n    setIsCloneMode(false)\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_CANCELLED,\n      eventData: {\n        databaseId: id,\n      },\n    })\n  }\n\n  const handleTabClick = (tab: ManualFormTab) => {\n    setActiveTab(tab)\n  }\n\n  const Footer = () => {\n    const footerEl = document.getElementById('footerDatabaseForm')\n\n    if (!footerEl) return null\n\n    return ReactDOM.createPortal(\n      <FooterActions\n        submitIsDisable={submitIsDisable}\n        errors={errors}\n        isLoading={loading}\n        onClickTestConnection={handleTestConnectionDatabase}\n        onClose={onClose}\n        onClickSubmit={formik.submitForm}\n        submitButtonText={submitButtonText}\n      />,\n      footerEl,\n    )\n  }\n\n  const Tabs = () => (\n    <TabsComponent\n      tabs={MANUAL_FORM_TABS}\n      value={activeTab}\n      onChange={(id) => handleTabClick(id as ManualFormTab)}\n      data-testid=\"manual-form-tabs\"\n    />\n  )\n\n  return (\n    <ContentWrapper data-testid=\"add-db_manual\">\n      {isEditMode &&\n        !isCloneMode &&\n        server?.buildType !== BuildType.RedisStack && (\n          <CloneConnection id={id} setIsCloneMode={setIsCloneMode} />\n        )}\n      <ContentWrapper as=\"div\" ref={formRef}>\n        {!isEditMode && !isFromCloud && (\n          <>\n            <Tabs />\n            <Spacer />\n            <ScrollableWrapper>\n              <AddConnection\n                activeTab={activeTab}\n                formik={formik}\n                onKeyDown={onKeyDown}\n                onHostNamePaste={onHostNamePaste}\n                certificates={certificates}\n                caCertificates={caCertificates}\n                buildType={buildType}\n              />\n            </ScrollableWrapper>\n          </>\n        )}\n        {(isEditMode || isCloneMode || isFromCloud) &&\n          connectionType !== ConnectionType.Sentinel && (\n            <>\n              {!isCloneMode && (\n                <>\n                  <DbInfo\n                    host={host}\n                    port={port}\n                    connectionType={connectionType}\n                    db={db}\n                    modules={modules}\n                    nameFromProvider={nameFromProvider}\n                    nodes={nodes}\n                    isFromCloud={isFromCloud}\n                  />\n                  <Spacer />\n                </>\n              )}\n              <Tabs />\n              <Spacer />\n              <ScrollableWrapper>\n                <EditConnection\n                  activeTab={activeTab}\n                  isCloneMode={isCloneMode}\n                  isEditMode={isEditMode}\n                  isFromCloud={isFromCloud}\n                  isFromAzure={isFromAzure}\n                  formik={formik}\n                  onKeyDown={onKeyDown}\n                  onHostNamePaste={onHostNamePaste}\n                  certificates={certificates}\n                  caCertificates={caCertificates}\n                  buildType={buildType}\n                />\n              </ScrollableWrapper>\n            </>\n          )}\n        {(isEditMode || isCloneMode) &&\n          connectionType === ConnectionType.Sentinel && (\n            <>\n              {!isCloneMode && (\n                <>\n                  <DbInfoSentinel\n                    nameFromProvider={nameFromProvider}\n                    connectionType={connectionType}\n                    sentinelMaster={sentinelMaster}\n                    host={host}\n                    port={port}\n                  />\n                  <Spacer />\n                </>\n              )}\n              <Tabs />\n              <Spacer />\n              <ScrollableWrapper>\n                <EditSentinelConnection\n                  activeTab={activeTab}\n                  isCloneMode={isCloneMode}\n                  formik={formik}\n                  onKeyDown={onKeyDown}\n                  onHostNamePaste={onHostNamePaste}\n                  certificates={certificates}\n                  caCertificates={caCertificates}\n                  db={db}\n                />\n              </ScrollableWrapper>\n            </>\n          )}\n      </ContentWrapper>\n      <Footer />\n    </ContentWrapper>\n  )\n}\n\nexport default ManualConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/ManualConnectionFrom.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { appRedirectionSelector } from 'uiSrc/slices/app/url-handling'\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\nimport { ADD_NEW_CA_CERT, SshPassType } from 'uiSrc/pages/home/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\n\nimport ManualConnectionForm, { Props } from './ManualConnectionForm'\n\nconst BTN_SUBMIT = 'btn-submit'\nconst NEW_CA_CERT = 'new-ca-cert'\nconst QA_CA_CERT = 'qa-ca-cert'\nconst RADIO_BTN_PRIVATE_KEY = '[for=\"privateKey\"]'\nconst BTN_TEST_CONNECTION = 'btn-test-connection'\n\nconst mockedProps = mock<Props>()\nconst mockedDbConnectionInfo = mock<DbConnectionInfo>()\n\nconst formFields = {\n  ...instance(mockedDbConnectionInfo),\n  host: 'localhost',\n  port: '6379',\n  name: 'lala',\n}\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  checkConnectToInstanceAction: () => jest.fn,\n  resetInstanceUpdateAction: () => jest.fn,\n  changeInstanceAliasAction: () => jest.fn,\n  setConnectedInstanceId: jest.fn,\n}))\n\njest.mock('uiSrc/slices/app/url-handling', () => ({\n  ...jest.requireActual('uiSrc/slices/app/url-handling'),\n  appRedirectionSelector: jest.fn().mockReturnValue(() => ({ action: null })),\n}))\n\ndescribe('InstanceForm', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={formFields}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render with ConnectionType.Sentinel', () => {\n    expect(\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Sentinel,\n          }}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render with ConnectionType.Cluster', () => {\n    expect(\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{ ...formFields, connectionType: ConnectionType.Cluster }}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render tooltip with nodes', () => {\n    expect(\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            nodes: [{ host: '1', port: 1 }],\n            connectionType: ConnectionType.Cluster,\n          }}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render DatabaseForm', () => {\n    expect(\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode={false}\n          formFields={{\n            ...formFields,\n            tls: true,\n            caCert: { id: '123' },\n            host: '123',\n            tlsClientAuthRequired: true,\n            nodes: [{ host: '1', port: 1 }],\n            connectionType: ConnectionType.Cluster,\n          }}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should change sentinelMasterUsername input properly', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Sentinel,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sentinel-mater-username'), {\n        target: { value: 'user' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sentinelMasterUsername: 'user',\n      }),\n    )\n\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sentinelMasterUsername: 'user',\n      }),\n    )\n  })\n\n  it('should change port input properly', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('port'), {\n        target: { value: '123' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        port: 123,\n      }),\n    )\n  })\n\n  it('should change tls checkbox', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Cluster,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('tls'))\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        tls: true,\n      }),\n    )\n\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        tls: true,\n      }),\n    )\n  })\n\n  it('should change Database Index checkbox', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('showDb'))\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        showDb: true,\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        showDb: true,\n      }),\n    )\n  })\n\n  it('should change db checkbox and value', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('showDb'))\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('db'), {\n        target: { value: '12' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        showDb: true,\n        db: 12,\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        showDb: true,\n        db: 12,\n      }),\n    )\n  })\n\n  it('should change \"Use SNI\" with prepopulated with host', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Cluster,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('sni'))\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sni: true,\n        servername: formFields.host,\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sni: true,\n        servername: formFields.host,\n      }),\n    )\n  })\n\n  it('should change \"Use SNI\"', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Cluster,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('sni'))\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sni-servername'), {\n        target: { value: '12' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sni: true,\n        servername: '12',\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sni: true,\n        servername: '12',\n      }),\n    )\n  })\n\n  it('should change \"Verify TLS Certificate\"', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Cluster,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('verify-tls-cert'))\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        verifyServerTlsCert: true,\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        verifyServerTlsCert: true,\n      }),\n    )\n  })\n\n  it('should select value from \"CA Certificate\"', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    const { findByText } = render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Cluster,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await userEvent.click(screen.getByTestId('select-ca-cert'))\n    await userEvent.click(\n      (await findByText('Add new CA certificate')) || document,\n    )\n    expect(screen.getByTestId(NEW_CA_CERT)).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId(NEW_CA_CERT), {\n        target: { value: '123' },\n      })\n    })\n\n    expect(screen.getByTestId(QA_CA_CERT)).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId(QA_CA_CERT), {\n        target: { value: '321' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        selectedCaCertName: ADD_NEW_CA_CERT,\n        newCaCertName: '321',\n        newCaCert: '123',\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        selectedCaCertName: ADD_NEW_CA_CERT,\n        newCaCertName: '321',\n        newCaCert: '123',\n      }),\n    )\n  })\n\n  it('should render fields for add new CA and change them properly', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Cluster,\n            selectedCaCertName: 'ADD_NEW_CA_CERT',\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    expect(screen.getByTestId(QA_CA_CERT)).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId(QA_CA_CERT), {\n        target: { value: '321' },\n      })\n    })\n\n    expect(screen.getByTestId(NEW_CA_CERT)).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId(NEW_CA_CERT), {\n        target: { value: '123' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        newCaCert: '123',\n        newCaCertName: '321',\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        newCaCert: '123',\n        newCaCertName: '321',\n      }),\n    )\n  })\n\n  it('should change \"Requires TLS Client Authentication\"', async () => {\n    const handleSubmit = jest.fn()\n    const handleTestConnection = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Cluster,\n          }}\n          onSubmit={handleSubmit}\n          onTestConnection={handleTestConnection}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('tls-required-checkbox'))\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    const testConnectionBtn = screen.getByTestId(BTN_TEST_CONNECTION)\n    await act(async () => {\n      fireEvent.click(testConnectionBtn)\n    })\n    expect(handleTestConnection).toHaveBeenCalledWith(\n      expect.objectContaining({\n        tlsClientAuthRequired: true,\n      }),\n    )\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        tlsClientAuthRequired: true,\n      }),\n    )\n  })\n\n  it('should render fields for add new CA with required tls auth and change them properly', async () => {\n    const handleSubmit = jest.fn()\n    const { container } = render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            tls: true,\n            connectionType: ConnectionType.Standalone,\n            selectedCaCertName: 'NO_CA_CERT',\n            tlsClientAuthRequired: true,\n            selectedTlsClientCertId: 'ADD_NEW',\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    expect(screen.getByTestId('select-cert')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('select-cert'))\n    })\n\n    await act(async () => {\n      fireEvent.click(\n        container.querySelectorAll('.euiContextMenuItem__text')[0] || document,\n      )\n    })\n\n    expect(screen.getByTestId('new-tsl-cert-pair-name')).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('new-tsl-cert-pair-name'), {\n        target: { value: '123' },\n      })\n    })\n\n    expect(screen.getByTestId('new-tls-client-cert')).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('new-tls-client-cert'), {\n        target: { value: '321' },\n      })\n    })\n\n    expect(screen.getByTestId('new-tls-client-cert-key')).toBeInTheDocument()\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('new-tls-client-cert-key'), {\n        target: { value: '231' },\n      })\n    })\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n\n    await act(async () => {\n      fireEvent.click(submitBtn)\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        newTlsClientCert: '321',\n        newTlsCertPairName: '123',\n        newTlsClientKey: '231',\n      }),\n    )\n  })\n\n  it('should render clone mode btn', () => {\n    render(\n      <ManualConnectionForm\n        {...instance(mockedProps)}\n        isEditMode\n        formFields={{\n          ...formFields,\n          connectionType: ConnectionType.Standalone,\n        }}\n      />,\n    )\n    expect(screen.getByTestId('clone-db-btn')).toBeTruthy()\n  })\n\n  describe('should render proper fields with Clone mode', () => {\n    it('should render proper fields for standalone db', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          isCloneMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n        />,\n      )\n      const fieldsTestIds = ['host', 'port', 'username', 'password', 'showDb']\n      fieldsTestIds.forEach((id) => {\n        expect(screen.getByTestId(id)).toBeTruthy()\n      })\n\n      fireEvent.mouseDown(screen.getByText('Security'))\n      expect(screen.getByTestId('tls')).toBeTruthy()\n    })\n\n    it('should render proper fields for sentinel db', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          isCloneMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Sentinel,\n          }}\n        />,\n      )\n      const fieldsTestIds = [\n        'name',\n        'primary-group',\n        'sentinel-mater-username',\n        'sentinel-master-password',\n        'host',\n        'port',\n        'username',\n        'password',\n        'showDb',\n      ]\n      fieldsTestIds.forEach((id) => {\n        expect(screen.getByTestId(id)).toBeTruthy()\n      })\n\n      fireEvent.mouseDown(screen.getByText('Security'))\n      expect(screen.getByTestId('tls')).toBeTruthy()\n    })\n\n    it('should render selected logical database with proper db index', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          isCloneMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n            showDb: true,\n            db: 5,\n          }}\n        />,\n      )\n      expect(screen.getByTestId('showDb')).toBeChecked()\n      expect(screen.getByTestId('db')).toHaveValue('5')\n    })\n\n    it('should render proper database alias as field', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          isCloneMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n        />,\n      )\n      expect(screen.getByTestId('name')).toHaveValue('lala')\n    })\n\n    //   it('should render proper default values for standalone', () => {\n    //     render(\n    //       <ManualConnectionForm\n    //         {...instance(mockedProps)}\n    //         formFields={{}}\n    //       />\n    //     )\n    //     expect(screen.getByTestId('host')).toHaveValue('127.0.0.1')\n    //     expect(screen.getByTestId('port')).toHaveValue('6379')\n    //     expect(screen.getByTestId('name')).toHaveValue('127.0.0.1:6379')\n    //   })\n  })\n\n  it('should change Use SSH checkbox', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    act(() => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n\n    expect(screen.getByTestId('use-ssh')).toBeChecked()\n  })\n\n  it('should not render Use SSH checkbox for redis stack buidlType', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          buildType={BuildType.RedisStack}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    expect(screen.queryByTestId('use-ssh')).not.toBeInTheDocument()\n  })\n\n  it('should change Use SSH checkbox and show proper fields for password radio', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            sshPassType: SshPassType.Password,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    act(() => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n\n    expect(screen.getByTestId('sshHost')).toBeInTheDocument()\n    expect(screen.getByTestId('sshPort')).toBeInTheDocument()\n    expect(screen.getByTestId('sshPassword')).toBeInTheDocument()\n    expect(screen.queryByTestId('sshPrivateKey')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('sshPassphrase')).not.toBeInTheDocument()\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    expect(submitBtn).toBeDisabled()\n  })\n\n  it('should change Use SSH checkbox and show proper fields for passphrase radio', async () => {\n    const handleSubmit = jest.fn()\n    const { container } = render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n    await act(async () => {\n      fireEvent.click(\n        container.querySelector(RADIO_BTN_PRIVATE_KEY) as HTMLLabelElement,\n      )\n    })\n\n    expect(screen.getByTestId('sshHost')).toBeInTheDocument()\n    expect(screen.getByTestId('sshPort')).toBeInTheDocument()\n    expect(screen.queryByTestId('sshPassword')).not.toBeInTheDocument()\n    expect(screen.getByTestId('sshPrivateKey')).toBeInTheDocument()\n    expect(screen.getByTestId('sshPassphrase')).toBeInTheDocument()\n\n    const submitBtn = screen.getByTestId(BTN_SUBMIT)\n    expect(submitBtn).toBeDisabled()\n  })\n\n  it('should be proper validation for ssh via ssh password', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n            sshPassType: SshPassType.Password,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled()\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n\n    expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshHost'), {\n        target: { value: 'localhost' },\n      })\n    })\n\n    expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshUsername'), {\n        target: { value: 'username' },\n      })\n    })\n\n    expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshPort'), {\n        target: { value: '22' },\n      })\n    })\n\n    expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled()\n  })\n\n  it('should be proper validation for ssh via ssh passphrase', async () => {\n    const handleSubmit = jest.fn()\n    const { container } = render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n            sshPassType: SshPassType.Password,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled()\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n    fireEvent.click(\n      container.querySelector(RADIO_BTN_PRIVATE_KEY) as HTMLLabelElement,\n    )\n\n    expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshHost'), {\n        target: { value: 'localhost' },\n      })\n      fireEvent.change(screen.getByTestId('sshPort'), {\n        target: { value: '22' },\n      })\n      fireEvent.change(screen.getByTestId('sshUsername'), {\n        target: { value: 'username' },\n      })\n    })\n\n    expect(screen.getByTestId(BTN_SUBMIT)).toBeDisabled()\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshPrivateKey'), {\n        target: { value: 'PRIVATEKEY' },\n      })\n    })\n\n    expect(screen.getByTestId(BTN_SUBMIT)).not.toBeDisabled()\n  })\n\n  it('should call submit btn with proper fields', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n            sshPassType: SshPassType.Password,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshHost'), {\n        target: { value: 'localhost' },\n      })\n\n      fireEvent.change(screen.getByTestId('sshPort'), {\n        target: { value: 1771 },\n      })\n\n      fireEvent.change(screen.getByTestId('sshUsername'), {\n        target: { value: 'username' },\n      })\n\n      fireEvent.change(screen.getByTestId('sshPassword'), {\n        target: { value: '123' },\n      })\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(BTN_SUBMIT))\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sshHost: 'localhost',\n        sshPort: 1771,\n        sshUsername: 'username',\n        sshPassword: '123',\n      }),\n    )\n  })\n\n  it('should call submit btn with proper fields via passphrase', async () => {\n    const handleSubmit = jest.fn()\n    const { container } = render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('use-ssh'))\n    })\n    fireEvent.click(\n      container.querySelector(RADIO_BTN_PRIVATE_KEY) as HTMLLabelElement,\n    )\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('sshHost'), {\n        target: { value: 'localhost' },\n      })\n\n      fireEvent.change(screen.getByTestId('sshPort'), {\n        target: { value: 1771 },\n      })\n\n      fireEvent.change(screen.getByTestId('sshUsername'), {\n        target: { value: 'username' },\n      })\n\n      fireEvent.change(screen.getByTestId('sshPrivateKey'), {\n        target: { value: '123444' },\n      })\n\n      fireEvent.change(screen.getByTestId('sshPassphrase'), {\n        target: { value: '123444' },\n      })\n    })\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(BTN_SUBMIT))\n    })\n\n    expect(handleSubmit).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sshHost: 'localhost',\n        sshPort: 1771,\n        sshUsername: 'username',\n        sshPrivateKey: '123444',\n        sshPassphrase: '123444',\n      }),\n    )\n  })\n\n  it('should render password input with 10_000 length limit', () => {\n    render(\n      <ManualConnectionForm\n        {...instance(mockedProps)}\n        formFields={{\n          ...formFields,\n          connectionType: ConnectionType.Standalone,\n        }}\n      />,\n    )\n\n    expect(screen.getByTestId('password')).toHaveAttribute('maxLength', '10000')\n  })\n\n  it('should render security fields with proper attributes', () => {\n    render(\n      <ManualConnectionForm\n        {...instance(mockedProps)}\n        formFields={{\n          ...formFields,\n          connectionType: ConnectionType.Standalone,\n          ssh: true,\n          password: true,\n          sshPassphrase: true,\n          sshPassType: SshPassType.PrivateKey,\n        }}\n      />,\n    )\n\n    expect(screen.getByTestId('password')).toHaveAttribute(\n      'value',\n      '••••••••••••',\n    )\n    expect(screen.getByTestId('password')).toHaveAttribute('type', 'password')\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    expect(screen.getByTestId('sshPassphrase')).toHaveAttribute(\n      'value',\n      '••••••••••••',\n    )\n    expect(screen.getByTestId('sshPassphrase')).toHaveAttribute(\n      'type',\n      'password',\n    )\n\n    fireEvent.mouseDown(screen.getByText('General'))\n    fireEvent.focus(screen.getByTestId('password'))\n\n    expect(screen.getByTestId('password')).toHaveAttribute('value', '')\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    fireEvent.focus(screen.getByTestId('sshPassphrase'))\n    expect(screen.getByTestId('sshPassphrase')).toHaveAttribute('value', '')\n  })\n\n  it('should render ssh password with proper attributes', () => {\n    render(\n      <ManualConnectionForm\n        {...instance(mockedProps)}\n        formFields={{\n          ...formFields,\n          connectionType: ConnectionType.Standalone,\n          ssh: true,\n          sshPassword: true,\n          sshPassType: SshPassType.Password,\n        }}\n      />,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    expect(screen.getByTestId('sshPassword')).toHaveAttribute(\n      'value',\n      '••••••••••••',\n    )\n    expect(screen.getByTestId('sshPassword')).toHaveAttribute(\n      'type',\n      'password',\n    )\n\n    fireEvent.focus(screen.getByTestId('sshPassword'))\n\n    expect(screen.getByTestId('sshPassword')).toHaveAttribute('value', '')\n  })\n\n  it('should render ssh password input with 10_000 length limit', () => {\n    render(\n      <ManualConnectionForm\n        {...instance(mockedProps)}\n        formFields={{\n          ...formFields,\n          connectionType: ConnectionType.Standalone,\n          ssh: true,\n          sshPassType: SshPassType.Password,\n        }}\n      />,\n    )\n\n    fireEvent.mouseDown(screen.getByText('Security'))\n    expect(screen.getByTestId('sshPassword')).toHaveAttribute(\n      'maxLength',\n      '10000',\n    )\n  })\n\n  describe('timeout', () => {\n    it('should render timeout input with 7 length limit and 1_000_000 value', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{ ...formFields, timeout: '30' }}\n        />,\n      )\n\n      expect(screen.getByTestId('timeout')).toBeInTheDocument()\n\n      fireEvent.change(screen.getByTestId('timeout'), {\n        target: { value: '2000000' },\n      })\n      fireEvent.focusOut(screen.getByTestId('timeout'))\n\n      expect(screen.getByTestId('timeout')).toHaveAttribute('value', '1000000')\n    })\n\n    it('should default  to previous value when value other than just numbers is provided', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={{ ...formFields, timeout: '30' }}\n        />,\n      )\n\n      fireEvent.change(screen.getByTestId('timeout'), {\n        target: { value: '11a2EU$#@' },\n      })\n\n      expect(screen.getByTestId('timeout')).toHaveAttribute('value', '30')\n    })\n  })\n\n  describe('cloud', () => {\n    it('some fields should be readonly if instance data source from cloud', () => {\n      ;(appRedirectionSelector as jest.Mock).mockImplementation(() => ({\n        action: UrlHandlingActions.Connect,\n      }))\n\n      const { queryByTestId } = render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={formFields}\n        />,\n      )\n\n      expect(queryByTestId('connection-type')).not.toBeInTheDocument()\n      expect(queryByTestId('host')).not.toBeInTheDocument()\n      expect(queryByTestId('port')).not.toBeInTheDocument()\n      expect(queryByTestId('db-info-port')).toBeInTheDocument()\n      expect(queryByTestId('db-info-host')).toBeInTheDocument()\n    })\n  })\n\n  describe('Azure databases', () => {\n    beforeEach(() => {\n      // Reset appRedirectionSelector mock to ensure isFromCloud = false\n      ;(appRedirectionSelector as jest.Mock).mockImplementation(() => ({\n        action: null,\n      }))\n    })\n\n    it('should disable connection fields when editing Azure database', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          isFromAzure\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n        />,\n      )\n\n      // Host is not shown in edit mode (shown as info above form)\n      expect(screen.queryByTestId('host')).not.toBeInTheDocument()\n      // Port, username, password should be disabled\n      expect(screen.getByTestId('port')).toBeDisabled()\n      expect(screen.getByTestId('username')).toBeDisabled()\n      expect(screen.getByTestId('password')).toBeDisabled()\n    })\n\n    it('should disable connection fields when cloning Azure database', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          isCloneMode\n          isFromAzure\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n        />,\n      )\n\n      expect(screen.getByTestId('host')).toBeDisabled()\n      expect(screen.getByTestId('port')).toBeDisabled()\n      expect(screen.getByTestId('username')).toBeDisabled()\n      expect(screen.getByTestId('password')).toBeDisabled()\n    })\n\n    it('should not disable connection fields for non-Azure database in edit mode', () => {\n      render(\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          isEditMode\n          formFields={{\n            ...formFields,\n            connectionType: ConnectionType.Standalone,\n          }}\n        />,\n      )\n\n      // Host is not shown in edit mode (shown as info above form)\n      expect(screen.queryByTestId('host')).not.toBeInTheDocument()\n      // Port, username, password should NOT be disabled for non-Azure databases\n      expect(screen.getByTestId('port')).not.toBeDisabled()\n      expect(screen.getByTestId('username')).not.toBeDisabled()\n      expect(screen.getByTestId('password')).not.toBeDisabled()\n    })\n  })\n\n  it('should call submit on press Enter', async () => {\n    const handleSubmit = jest.fn()\n    render(\n      <div id=\"footerDatabaseForm\">\n        <ManualConnectionForm\n          {...instance(mockedProps)}\n          formFields={formFields}\n          onSubmit={handleSubmit}\n        />\n      </div>,\n    )\n\n    await act(async () => {\n      fireEvent.keyDown(screen.getByTestId('form'), {\n        key: 'Enter',\n        code: 13,\n        charCode: 13,\n      })\n    })\n    expect(handleSubmit).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/components/CloneConnection.tsx",
    "content": "import React from 'react'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\n\nexport interface Props {\n  id?: string\n  setIsCloneMode: (val: boolean) => void\n}\n\nconst CloneConnection = (props: Props) => {\n  const { id, setIsCloneMode } = props\n\n  const handleClickClone = () => {\n    setIsCloneMode(true)\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_DATABASE_CLONE_REQUESTED,\n      eventData: {\n        databaseId: id,\n      },\n    })\n  }\n\n  return (\n    <>\n      <Row gap=\"m\" justify=\"end\" style={{ flexGrow: 0 }}>\n        <FlexItem>\n          <SecondaryButton\n            aria-label=\"Clone database\"\n            data-testid=\"clone-db-btn\"\n            onClick={handleClickClone}\n          >\n            Clone Connection\n          </SecondaryButton>\n        </FlexItem>\n      </Row>\n      <Spacer />\n    </>\n  )\n}\n\nexport default CloneConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/components/FooterActions.tsx",
    "content": "import React from 'react'\nimport { FormikErrors } from 'formik'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { getSubmitButtonContent } from 'uiSrc/pages/home/utils'\nimport { DbConnectionInfo, ISubmitButton } from 'uiSrc/pages/home/interfaces'\nimport { SubmitBtnText } from 'uiSrc/pages/home/constants'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  EmptyButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\n\nexport interface Props {\n  submitIsDisable: () => boolean\n  errors: FormikErrors<DbConnectionInfo>\n  isLoading?: boolean\n  onClickTestConnection: () => void\n  onClose?: () => void\n  onClickSubmit: () => void\n  submitButtonText?: SubmitBtnText\n}\n\nconst FooterActions = (props: Props) => {\n  const {\n    isLoading,\n    submitButtonText,\n    submitIsDisable,\n    errors,\n    onClickTestConnection,\n    onClose,\n    onClickSubmit,\n  } = props\n\n  const SubmitButton = ({\n    text = '',\n    onClick,\n    submitIsDisabled,\n  }: ISubmitButton) => (\n    <RiTooltip\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      title={\n        submitIsDisabled\n          ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length)\n          : null\n      }\n      content={getSubmitButtonContent(errors, submitIsDisabled)}\n    >\n      <PrimaryButton\n        type=\"submit\"\n        onClick={onClick}\n        disabled={submitIsDisabled}\n        loading={isLoading}\n        icon={submitIsDisabled ? InfoIcon : undefined}\n        data-testid=\"btn-submit\"\n      >\n        {text}\n      </PrimaryButton>\n    </RiTooltip>\n  )\n\n  return (\n    <Row justify=\"between\" align=\"center\">\n      <FlexItem className=\"btn-back\">\n        <RiTooltip\n          position=\"top\"\n          anchorClassName=\"euiToolTip__btn-disabled\"\n          title={\n            submitIsDisable()\n              ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length)\n              : null\n          }\n          content={getSubmitButtonContent(errors, submitIsDisable())}\n        >\n          <EmptyButton\n            className=\"empty-btn\"\n            disabled={submitIsDisable()}\n            icon={submitIsDisable() ? InfoIcon : undefined}\n            onClick={onClickTestConnection}\n            loading={isLoading}\n            data-testid=\"btn-test-connection\"\n          >\n            Test Connection\n          </EmptyButton>\n        </RiTooltip>\n      </FlexItem>\n\n      <FlexItem>\n        <Row>\n          {onClose && (\n            <SecondaryButton\n              onClick={onClose}\n              className=\"btn-cancel\"\n              data-testid=\"btn-cancel\"\n              style={{ marginRight: 12 }}\n            >\n              Cancel\n            </SecondaryButton>\n          )}\n          <SubmitButton\n            onClick={onClickSubmit}\n            text={submitButtonText}\n            submitIsDisabled={submitIsDisable()}\n          />\n        </Row>\n      </FlexItem>\n    </Row>\n  )\n}\n\nexport default FooterActions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/constants.ts",
    "content": "import { TabInfo } from 'uiSrc/components/base/layout/tabs'\n\nexport enum ManualFormTab {\n  General = 'general',\n  Security = 'security',\n  Decompression = 'decompression',\n}\n\nexport const MANUAL_FORM_TABS: TabInfo[] = [\n  { value: ManualFormTab.General, label: 'General', content: null },\n  { value: ManualFormTab.Security, label: 'Security', content: null },\n  {\n    value: ManualFormTab.Decompression,\n    label: 'Decompression & Formatters',\n    content: null,\n  },\n]\n\nexport const AZURE_READONLY_FIELDS = ['host', 'port', 'username', 'password']\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/forms/AddConnection.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\nimport {\n  DatabaseForm,\n  DbIndex,\n  ForceStandalone,\n  SSHDetails,\n  TlsDetails,\n} from 'uiSrc/pages/home/components/form'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport DecompressionAndFormatters from './DecompressionAndFormatters'\nimport { ManualFormTab } from '../constants'\n\nexport interface Props {\n  activeTab: ManualFormTab\n  formik: FormikProps<DbConnectionInfo>\n  onKeyDown: (event: React.KeyboardEvent<HTMLFormElement>) => void\n  onHostNamePaste: (content: string) => boolean\n  caCertificates?: { id: string; name: string }[]\n  certificates?: { id: number; name: string }[]\n  buildType?: BuildType\n}\n\nconst AddConnection = (props: Props) => {\n  const {\n    activeTab,\n    formik,\n    onKeyDown,\n    onHostNamePaste,\n    certificates,\n    caCertificates,\n    buildType,\n  } = props\n\n  return (\n    <form\n      onSubmit={formik.handleSubmit}\n      data-testid=\"form\"\n      onKeyDown={onKeyDown}\n      role=\"presentation\"\n    >\n      {activeTab === ManualFormTab.General && (\n        <>\n          <DatabaseForm\n            formik={formik}\n            onHostNamePaste={onHostNamePaste}\n            showFields={{ host: true, alias: true, port: true, timeout: true }}\n          />\n          <Spacer size=\"l\" />\n          <Divider />\n          <Spacer size=\"m\" />\n          <DbIndex formik={formik} />\n          <Spacer size=\"m\" />\n          <Divider />\n          <Spacer size=\"m\" />\n          <ForceStandalone formik={formik} />\n        </>\n      )}\n      {activeTab === ManualFormTab.Security && (\n        <>\n          <TlsDetails\n            formik={formik}\n            certificates={certificates}\n            caCertificates={caCertificates}\n          />\n          {buildType !== BuildType.RedisStack && (\n            <>\n              <Spacer size=\"m\" />\n              <Divider />\n              <Spacer size=\"m\" />\n              <SSHDetails formik={formik} />\n            </>\n          )}\n        </>\n      )}\n      {activeTab === ManualFormTab.Decompression && (\n        <DecompressionAndFormatters formik={formik} />\n      )}\n    </form>\n  )\n}\n\nexport default AddConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/forms/DecompressionAndFormatters.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\n\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport {\n  DbCompressor,\n  KeyFormatSelector,\n} from 'uiSrc/pages/home/components/form'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\nconst DecompressionAndFormatters = ({\n  formik,\n}: {\n  formik: FormikProps<DbConnectionInfo>\n}) => (\n  <>\n    <DbCompressor formik={formik} />\n    <Spacer size=\"m\" />\n    <Divider />\n    <Spacer size=\"m\" />\n    <KeyFormatSelector formik={formik} />\n  </>\n)\n\nexport default DecompressionAndFormatters\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/forms/EditConnection.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\nimport {\n  DatabaseForm,\n  DbIndex,\n  ForceStandalone,\n  SSHDetails,\n  TlsDetails,\n} from 'uiSrc/pages/home/components/form'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport DecompressionAndFormatters from './DecompressionAndFormatters'\n\nimport { AZURE_READONLY_FIELDS, ManualFormTab } from '../constants'\n\nexport interface Props {\n  activeTab: ManualFormTab\n  isEditMode: boolean\n  isCloneMode: boolean\n  isFromCloud: boolean\n  isFromAzure?: boolean\n  formik: FormikProps<DbConnectionInfo>\n  onKeyDown: (event: React.KeyboardEvent<HTMLFormElement>) => void\n  onHostNamePaste: (content: string) => boolean\n  caCertificates?: { id: string; name: string }[]\n  certificates?: { id: number; name: string }[]\n  buildType?: BuildType\n}\n\nconst EditConnection = (props: Props) => {\n  const {\n    activeTab,\n    isCloneMode,\n    isEditMode,\n    isFromCloud,\n    isFromAzure = false,\n    formik,\n    onKeyDown,\n    onHostNamePaste,\n    certificates,\n    caCertificates,\n    buildType,\n  } = props\n\n  // For Azure databases in edit/clone mode, disable connection fields\n  const readOnlyFields = isFromAzure && isEditMode ? AZURE_READONLY_FIELDS : []\n\n  return (\n    <form\n      onSubmit={formik.handleSubmit}\n      data-testid=\"form\"\n      onKeyDown={onKeyDown}\n      role=\"presentation\"\n    >\n      {activeTab === ManualFormTab.General && (\n        <>\n          <DatabaseForm\n            formik={formik}\n            showFields={{\n              alias: true,\n              host: (!isEditMode || isCloneMode) && !isFromCloud,\n              port: !isFromCloud,\n              timeout: true,\n            }}\n            autoFocus={!isCloneMode && isEditMode}\n            onHostNamePaste={onHostNamePaste}\n            readyOnlyFields={readOnlyFields}\n          />\n          <Spacer size=\"l\" />\n          <Divider />\n          <Spacer size=\"m\" />\n          <ForceStandalone formik={formik} />\n          {isCloneMode && (\n            <>\n              <Spacer size=\"m\" />\n              <Divider />\n              <Spacer size=\"m\" />\n              <DbIndex formik={formik} />\n            </>\n          )}\n        </>\n      )}\n      {activeTab === ManualFormTab.Security && (\n        <>\n          <TlsDetails\n            formik={formik}\n            certificates={certificates}\n            caCertificates={caCertificates}\n          />\n          {buildType !== BuildType.RedisStack && (\n            <>\n              <Spacer size=\"m\" />\n              <Divider />\n              <Spacer size=\"m\" />\n              <SSHDetails formik={formik} />\n            </>\n          )}\n        </>\n      )}\n      {activeTab === ManualFormTab.Decompression && (\n        <DecompressionAndFormatters formik={formik} />\n      )}\n    </form>\n  )\n}\n\nexport default EditConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/forms/EditSentinelConnection.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\nimport {\n  PrimaryGroupSentinel,\n  SentinelMasterDatabase,\n} from 'uiSrc/pages/home/components/form/sentinel'\nimport { Nullable, selectOnFocus } from 'uiSrc/utils'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport {\n  DatabaseForm,\n  DbIndex,\n  TlsDetails,\n} from 'uiSrc/pages/home/components/form'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport DecompressionAndFormatters from './DecompressionAndFormatters'\n\nimport { ManualFormTab } from '../constants'\n\nexport interface Props {\n  activeTab: ManualFormTab\n  isCloneMode: boolean\n  formik: FormikProps<DbConnectionInfo>\n  onKeyDown: (event: React.KeyboardEvent<HTMLFormElement>) => void\n  onHostNamePaste: (content: string) => boolean\n  caCertificates?: { id: string; name: string }[]\n  certificates?: { id: number; name: string }[]\n  db: Nullable<number>\n}\n\nconst EditSentinelConnection = (props: Props) => {\n  const {\n    activeTab,\n    isCloneMode,\n    formik,\n    onKeyDown,\n    onHostNamePaste,\n    certificates,\n    caCertificates,\n    db,\n  } = props\n\n  const GeneralFormClodeMode = (\n    <Col gap=\"l\">\n      <PrimaryGroupSentinel formik={formik} />\n      <Divider />\n      <Title color=\"primary\" size=\"M\">\n        Database\n      </Title>\n      <SentinelMasterDatabase\n        formik={formik}\n        db={db}\n        isCloneMode={isCloneMode}\n      />\n      <Divider />\n      <Title color=\"primary\" size=\"M\">\n        Sentinel\n      </Title>\n      <DatabaseForm\n        formik={formik}\n        showFields={{ host: true, port: true, alias: false, timeout: false }}\n        onHostNamePaste={onHostNamePaste}\n      />\n      <Divider />\n      <DbIndex formik={formik} />\n    </Col>\n  )\n\n  const GeneralFormEditMode = (\n    <Col gap=\"l\">\n      <Row gap=\"m\">\n        <FlexItem grow>\n          <FormField label=\"Database Alias\" required>\n            <TextInput\n              name=\"name\"\n              id=\"name\"\n              data-testid=\"name\"\n              placeholder=\"Enter Database Alias\"\n              onFocus={selectOnFocus}\n              value={formik.values.name ?? ''}\n              maxLength={500}\n              onChange={formik.handleChange}\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n      <Divider />\n      <Title color=\"primary\" size=\"M\">\n        Database\n      </Title>\n      <SentinelMasterDatabase\n        formik={formik}\n        db={db}\n        isCloneMode={isCloneMode}\n      />\n      <Divider />\n      <Title color=\"primary\" size=\"M\">\n        Sentinel\n      </Title>\n      <DatabaseForm\n        formik={formik}\n        showFields={{ host: false, port: true, alias: false, timeout: false }}\n        onHostNamePaste={onHostNamePaste}\n      />\n    </Col>\n  )\n\n  return (\n    <form\n      onSubmit={formik.handleSubmit}\n      data-testid=\"form\"\n      onKeyDown={onKeyDown}\n      role=\"presentation\"\n    >\n      {activeTab === ManualFormTab.General && (\n        <>{isCloneMode ? GeneralFormClodeMode : GeneralFormEditMode}</>\n      )}\n      {activeTab === ManualFormTab.Security && (\n        <TlsDetails\n          formik={formik}\n          certificates={certificates}\n          caCertificates={caCertificates}\n        />\n      )}\n\n      {activeTab === ManualFormTab.Decompression && (\n        <DecompressionAndFormatters formik={formik} />\n      )}\n    </form>\n  )\n}\n\nexport default EditSentinelConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/forms/index.ts",
    "content": "import AddConnection from './AddConnection'\nimport EditConnection from './EditConnection'\nimport EditSentinelConnection from './EditSentinelConnection'\n\nexport { AddConnection, EditConnection, EditSentinelConnection }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/manual-connection/manual-connection-form/index.ts",
    "content": "import ManualConnectionForm from './ManualConnectionForm'\n\nexport default ManualConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.spec.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  act,\n} from 'uiSrc/utils/test-utils'\nimport { loadInstancesSuccess } from 'uiSrc/slices/instances/instances'\nimport { RootState, store } from 'uiSrc/slices/store'\nimport { ConnectionType, Instance } from 'uiSrc/slices/interfaces'\nimport SearchDatabasesList from './SearchDatabasesList'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\nlet storeMock: typeof mockedStore\nconst connectedInstancesMock: Instance[] = [\n  {\n    id: '1',\n    name: 'local',\n    host: 'localhost',\n    port: 6379,\n    visible: true,\n    modules: [],\n    connectionType: ConnectionType.Sentinel,\n    lastConnection: new Date(),\n    tags: [\n      {\n        id: '1',\n        key: 'env',\n        value: 'prod',\n        createdAt: new Date().toISOString(),\n        updatedAt: new Date().toISOString(),\n      },\n    ],\n    version: '',\n  },\n  {\n    id: '2',\n    name: 'cloud',\n    host: 'cloud',\n    port: 6379,\n    visible: true,\n    modules: [],\n    connectionType: ConnectionType.Cluster,\n    lastConnection: new Date(),\n    tags: [],\n    version: '',\n  },\n]\n\nconst otherInstancesMock: Instance[] = [\n  /*\n    Reasoning behind the mock data:\n    - The 'not_connected' instance simulates a Redis instance that is not currently connected.\n    - The 'some_future_unrecognized_connection_type' instance represents a Redis instance with a connection type that is not\n      recognized by the current application logic (e.g something is added in future and this part of the application is not yet aware of it).\n  */\n  {\n    id: '3',\n    name: 'not_connected',\n    host: 'not_connected',\n    port: 6379,\n    visible: true,\n    modules: [],\n    connectionType: 'NOT CONNECTED' as any,\n    tags: [],\n    version: '',\n  },\n  {\n    id: '4',\n    name: 'some_future_unrecognized_connection_type',\n    host: 'some_future_unrecognized_connection_type',\n    port: 6379,\n    visible: true,\n    modules: [],\n    connectionType: 'UNRECOGNIZED' as any,\n    tags: [],\n    version: '',\n  },\n  {\n    id: '5',\n    name: 'undefined_connection_type',\n    host: 'undefined_connection_type',\n    port: 6379,\n    visible: true,\n    modules: [],\n    connectionType: undefined,\n    tags: [],\n    version: '',\n  },\n]\n\nconst mockInitialState = (\n  state: RootState,\n  instances: Instance[],\n  options?: { selectedTags?: Set<string> },\n) => {\n  ;(useSelector as jest.Mock).mockImplementation(\n    (callback: (arg0: RootState) => RootState) =>\n      callback({\n        ...state,\n        connections: {\n          ...state.connections,\n          instances: {\n            ...state.connections.instances,\n            data: instances,\n          },\n          tags: {\n            ...state.connections.tags,\n            selectedTags:\n              options?.selectedTags ?? state.connections.tags?.selectedTags,\n          },\n        },\n      }),\n  )\n}\n\nconst simulateUserTypedInSearchBox = async (value: string) => {\n  await act(() => {\n    fireEvent.change(screen.getByTestId('search-database-list'), {\n      target: { value },\n    })\n  })\n}\n\nbeforeEach(() => {\n  cleanup()\n  storeMock = cloneDeep(mockedStore)\n  storeMock.clearActions()\n})\n\ndescribe('SearchDatabasesList', () => {\n  it('should render', () => {\n    mockInitialState(store.getState(), connectedInstancesMock)\n    expect(render(<SearchDatabasesList />)).toBeTruthy()\n  })\n\n  it.each([\n    {\n      description: 'with connected instances',\n      instancesMock: connectedInstancesMock,\n      expectedInstances: [\n        { ...connectedInstancesMock[0], visible: false },\n        { ...connectedInstancesMock[1], visible: false },\n      ],\n    },\n    {\n      description: 'with other than connected connectionType',\n      instancesMock: otherInstancesMock,\n      expectedInstances: [\n        { ...otherInstancesMock[0], visible: false },\n        { ...otherInstancesMock[1], visible: false },\n        { ...otherInstancesMock[2], visible: false },\n      ],\n    },\n  ])(\n    'should call loadInstancesSuccess with all instances hidden after typing value not matching anything ($description)',\n    async ({ instancesMock, expectedInstances }) => {\n      mockInitialState(store.getState(), instancesMock)\n\n      render(<SearchDatabasesList />)\n\n      await simulateUserTypedInSearchBox('value_which_matches_nothing')\n\n      const expectedActions = [loadInstancesSuccess(expectedInstances)]\n      expect(storeMock.getActions()).toEqual(expectedActions)\n    },\n  )\n\n  it('should call loadInstancesSuccess with not matching instances hidden when selected tags in state are provided', async () => {\n    mockInitialState(store.getState(), connectedInstancesMock, {\n      selectedTags: new Set(['env:prod']),\n    })\n\n    const expectedInstancesAfterRendering = [\n      { ...connectedInstancesMock[0], visible: true },\n      { ...connectedInstancesMock[1], visible: false },\n    ]\n\n    render(<SearchDatabasesList />)\n\n    const expectedActions = [\n      loadInstancesSuccess(expectedInstancesAfterRendering),\n    ]\n    expect(storeMock.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/search-databases-list/SearchDatabasesList.tsx",
    "content": "import React, { useState, useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  instancesSelector,\n  loadInstancesSuccess,\n} from 'uiSrc/slices/instances/instances'\nimport { CONNECTION_TYPE_DISPLAY, Instance } from 'uiSrc/slices/interfaces'\nimport { tagsSelector } from 'uiSrc/slices/instances/tags'\nimport { lastConnectionFormat } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { SearchInput } from 'uiSrc/components/base/inputs'\n\nexport const instanceHasTags = (\n  instance: Instance,\n  selectedTags: Set<string>,\n) =>\n  selectedTags.size === 0 ||\n  instance.tags?.some((tag) => selectedTags.has(`${tag.key}:${tag.value}`))\n\nconst SearchDatabasesList = () => {\n  const [value, setValue] = useState('')\n  const { data: instances } = useSelector(instancesSelector)\n  const { selectedTags } = useSelector(tagsSelector)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    const isInitialRender =\n      value === '' &&\n      selectedTags.size === 0 &&\n      (!instances ||\n        instances.length === 0 ||\n        !instances.some((instance) => instance?.visible === false))\n\n    if (isInitialRender) {\n      return\n    }\n\n    const itemsTemp = instances.map((item: Instance) => ({\n      ...item,\n      visible:\n        (instanceHasTags(item, selectedTags) &&\n          (item.name?.toLowerCase().indexOf(value) !== -1 ||\n            item.host?.toString()?.indexOf(value) !== -1 ||\n            item.port?.toString()?.indexOf(value) !== -1 ||\n            (item.connectionType &&\n              CONNECTION_TYPE_DISPLAY[item.connectionType] &&\n              CONNECTION_TYPE_DISPLAY[item.connectionType]\n                ?.toLowerCase()\n                ?.indexOf(value) !== -1) ||\n            item.modules\n              ?.map((m) => m.name?.toLowerCase())\n              .join(',')\n              .indexOf(value) !== -1 ||\n            lastConnectionFormat(item.lastConnection)?.indexOf(value) !== -1 ||\n            item.tags?.some(\n              (tag) =>\n                `${tag.key.toLowerCase()}:${tag.value.toLowerCase()}`.indexOf(\n                  value,\n                ) !== -1,\n            ))) ||\n        false, // force boolean type\n    }))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.CONFIG_DATABASES_DATABASE_LIST_SEARCHED,\n      eventData: {\n        instancesFullCount: instances.length,\n        instancesSearchedCount: itemsTemp.filter(({ visible }) => visible)\n          ?.length,\n      },\n    })\n\n    dispatch(loadInstancesSuccess(itemsTemp))\n  }, [value, selectedTags])\n\n  return (\n    <SearchInput\n      placeholder=\"Database List Search\"\n      onChange={(value) => setValue(value.toLowerCase())}\n      value={value}\n      aria-label=\"Search database list\"\n      data-testid=\"search-database-list\"\n    />\n  )\n}\n\nexport default SearchDatabasesList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/search-databases-list/index.ts",
    "content": "import SearchDatabasesList from './SearchDatabasesList'\n\nexport default SearchDatabasesList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport SentinelConnectionForm, {\n  Props as SentinelConnectionFormProps,\n} from 'uiSrc/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport SentinelConnectionWrapper, { Props } from './SentinelConnectionWrapper'\n\nconst mockedProps = mock<Props>()\n\njest.mock('./sentinel-connection-form/SentinelConnectionForm', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockSentinelConnectionForm = (props: SentinelConnectionFormProps) => (\n  <div>\n    <button\n      type=\"button\"\n      onClick={() => props.onHostNamePaste('redis-12000.cluster.local:12000')}\n      data-testid=\"onHostNamePaste-btn\"\n    >\n      onHostNamePaste\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.onSubmit({})}\n      data-testid=\"onSubmit-btn\"\n    >\n      onSubmit\n    </button>\n    <button\n      type=\"button\"\n      onClick={() => props.onClose()}\n      data-testid=\"onClose-btn\"\n    >\n      onClose\n    </button>\n  </div>\n)\n\ndescribe('SentinelConnectionWrapper', () => {\n  beforeAll(() => {\n    SentinelConnectionForm.mockImplementation(mockSentinelConnectionForm)\n  })\n  it('should render', () => {\n    expect(\n      render(<SentinelConnectionWrapper {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should call onHostNamePaste', () => {\n    const component = render(\n      <SentinelConnectionWrapper {...instance(mockedProps)} />,\n    )\n    fireEvent.click(screen.getByTestId('onHostNamePaste-btn'))\n    expect(component).toBeTruthy()\n  })\n\n  it('should call onClose', () => {\n    const onClose = jest.fn()\n    render(\n      <SentinelConnectionWrapper\n        {...instance(mockedProps)}\n        onClose={onClose}\n      />,\n    )\n    fireEvent.click(screen.getByTestId('onClose-btn'))\n    expect(onClose).toBeCalled()\n  })\n\n  it('Should call proper telemetry event', () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<SentinelConnectionWrapper {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('onSubmit-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event:\n        TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED,\n    })\n\n    sendEventTelemetry.mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/sentinel-connection/SentinelConnectionWrapper.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router'\n\nimport {\n  fetchMastersSentinelAction,\n  sentinelSelector,\n} from 'uiSrc/slices/instances/sentinel'\nimport { removeEmpty } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { caCertsSelector, fetchCaCerts } from 'uiSrc/slices/instances/caCerts'\nimport { Pages } from 'uiSrc/constants'\nimport {\n  clientCertsSelector,\n  fetchClientCerts,\n} from 'uiSrc/slices/instances/clientCerts'\n\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport {\n  applyTlSDatabase,\n  autoFillFormDetails,\n  getTlsSettings,\n} from 'uiSrc/pages/home/utils'\nimport { ADD_NEW, NO_CA_CERT } from 'uiSrc/pages/home/constants'\nimport { InstanceType } from 'uiSrc/slices/interfaces'\nimport { useModalHeader } from 'uiSrc/contexts/ModalTitleProvider'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport SentinelConnectionForm from './sentinel-connection-form'\n\nexport interface Props {\n  onClose?: () => void\n}\nconst DEFAULT_SENTINEL_HOST = '127.0.0.1'\nconst DEFAULT_SENTINEL_PORT = '26379'\n\nconst INITIAL_VALUES = {\n  host: DEFAULT_SENTINEL_HOST,\n  port: DEFAULT_SENTINEL_PORT,\n  username: '',\n  password: '',\n  tls: false,\n  tlsClientAuthRequired: false,\n  selectedTlsClientCertId: ADD_NEW,\n  verifyServerTlsCert: false,\n  selectedCaCertName: NO_CA_CERT,\n}\n\nconst SentinelConnectionWrapper = (props: Props) => {\n  const { onClose } = props\n  const [initialValues, setInitialValues] = useState(INITIAL_VALUES)\n\n  const { loading } = useSelector(sentinelSelector)\n  const { data: caCertificates } = useSelector(caCertsSelector)\n  const { data: certificates } = useSelector(clientCertsSelector)\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { setModalHeader } = useModalHeader()\n\n  useEffect(() => {\n    dispatch(fetchCaCerts())\n    dispatch(fetchClientCerts())\n\n    setModalHeader(<Title size=\"M\">Redis Sentinel</Title>, true)\n\n    return () => {\n      setModalHeader(null)\n    }\n  }, [])\n\n  const onMastersSentinelFetched = () => {\n    history.push(Pages.sentinelDatabases)\n  }\n\n  const handleSubmitDatabase = (payload: any) => {\n    sendEventTelemetry({\n      event:\n        TelemetryEvent.CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED,\n    })\n\n    dispatch(fetchMastersSentinelAction(payload, onMastersSentinelFetched))\n  }\n\n  const addDatabase = (tlsSettings: any, values: DbConnectionInfo) => {\n    const { host, port, username, password } = values\n    const database: any = {\n      host,\n      port: +port,\n      username,\n      password,\n    }\n\n    // add tls for database\n    applyTlSDatabase(database, tlsSettings)\n    handleSubmitDatabase(removeEmpty(database))\n  }\n\n  const handleConnectionFormSubmit = (values: DbConnectionInfo) => {\n    const tlsSettings = getTlsSettings(values)\n\n    addDatabase(tlsSettings, values)\n  }\n\n  const handlePostHostName = (content: string): boolean =>\n    autoFillFormDetails(\n      content,\n      initialValues,\n      setInitialValues,\n      InstanceType.Sentinel,\n    )\n\n  return (\n    <SentinelConnectionForm\n      initialValues={initialValues}\n      loading={loading}\n      onSubmit={handleConnectionFormSubmit}\n      onClose={onClose}\n      onHostNamePaste={handlePostHostName}\n      certificates={certificates}\n      caCertificates={caCertificates}\n    />\n  )\n}\n\nexport default SentinelConnectionWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/sentinel-connection/index.ts",
    "content": "import SentinelConnectionWrapper from './SentinelConnectionWrapper'\n\nexport default SentinelConnectionWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport SentinelConnectionForm, { Props } from './SentinelConnectionForm'\n\nconst mockedProps = mock<Props>()\n\nconst mockValues = {\n  host: 'host',\n  port: '123',\n}\n\ndescribe('SentinelConnectionForm', () => {\n  it('should render', () => {\n    expect(\n      render(<SentinelConnectionForm {...instance(mockedProps)} />),\n    ).toBeTruthy()\n  })\n\n  it('should call submit form on press Enter', async () => {\n    const mockSubmit = jest.fn()\n    render(\n      <SentinelConnectionForm\n        {...instance(mockedProps)}\n        onSubmit={mockSubmit}\n        // @ts-ignore\n        initialValues={mockValues}\n      />,\n    )\n\n    await act(async () => {\n      fireEvent.keyDown(screen.getByTestId('form'), {\n        key: 'Enter',\n        code: 13,\n        charCode: 13,\n      })\n    })\n\n    expect(mockSubmit).toHaveBeenCalled()\n  })\n\n  it('should render Footer', async () => {\n    render(\n      <div id=\"footerDatabaseForm\">\n        <SentinelConnectionForm {...instance(mockedProps)} />\n      </div>,\n    )\n\n    expect(screen.getByTestId('btn-submit')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/SentinelConnectionForm.tsx",
    "content": "import { FormikErrors, useFormik } from 'formik'\nimport { isEmpty, pick } from 'lodash'\nimport React, { useRef, useState } from 'react'\nimport ReactDOM from 'react-dom'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { fieldDisplayNames } from 'uiSrc/pages/home/constants'\nimport { getFormErrors, getSubmitButtonContent } from 'uiSrc/pages/home/utils'\nimport { DbConnectionInfo, ISubmitButton } from 'uiSrc/pages/home/interfaces'\nimport {\n  DatabaseForm,\n  MessageSentinel,\n  TlsDetails,\n} from 'uiSrc/pages/home/components/form'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nimport {\n  ContentWrapper,\n  ScrollableWrapper,\n} from '../../ManualConnection.styles'\n\nexport interface Props {\n  loading: boolean\n  initialValues: DbConnectionInfo\n  certificates: { id: string; name: string }[]\n  caCertificates: { id: string; name: string }[]\n  onSubmit: (values: DbConnectionInfo) => void\n  onHostNamePaste: (content: string) => boolean\n  onClose?: () => void\n}\n\nconst getInitFieldsDisplayNames = ({ host, port }: any) => {\n  if (!host || !port) {\n    return pick(fieldDisplayNames, ['host', 'port'])\n  }\n  return {}\n}\n\nconst SentinelConnectionForm = (props: Props) => {\n  const {\n    initialValues = {},\n    onClose,\n    onSubmit,\n    onHostNamePaste,\n    loading,\n    certificates,\n    caCertificates,\n  } = props\n\n  const [errors, setErrors] = useState<FormikErrors<DbConnectionInfo>>(\n    getInitFieldsDisplayNames(initialValues),\n  )\n\n  const formRef = useRef<HTMLDivElement>(null)\n\n  const submitIsDisable = () => !isEmpty(errors)\n\n  const validate = (values: DbConnectionInfo) => {\n    const errs = getFormErrors(values)\n    setErrors(errs)\n    return errs\n  }\n\n  const formik = useFormik({\n    initialValues,\n    validate,\n    enableReinitialize: true,\n    validateOnMount: true,\n    onSubmit: (values: any) => {\n      onSubmit(values)\n    },\n  })\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLFormElement>) => {\n    if (event.key === keys.ENTER && !submitIsDisable()) {\n      // event.\n      formik.submitForm()\n    }\n  }\n\n  const SubmitButton = ({ onClick, submitIsDisabled }: ISubmitButton) => (\n    <RiTooltip\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      title={\n        submitIsDisabled\n          ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length)\n          : null\n      }\n      content={getSubmitButtonContent(errors, submitIsDisabled)}\n    >\n      <PrimaryButton\n        type=\"submit\"\n        onClick={onClick}\n        disabled={submitIsDisabled}\n        loading={loading}\n        icon={submitIsDisabled ? InfoIcon : undefined}\n        data-testid=\"btn-submit\"\n      >\n        Discover database\n      </PrimaryButton>\n    </RiTooltip>\n  )\n\n  const Footer = () => {\n    const footerEl = document.getElementById('footerDatabaseForm')\n\n    if (footerEl) {\n      return ReactDOM.createPortal(\n        <Row justify=\"end\" gap=\"m\" className=\"footerAddDatabase\">\n          {onClose && (\n            <SecondaryButton\n              onClick={onClose}\n              className=\"btn-cancel\"\n              data-testid=\"btn-cancel\"\n            >\n              Cancel\n            </SecondaryButton>\n          )}\n          <SubmitButton\n            onClick={formik.submitForm}\n            submitIsDisabled={submitIsDisable()}\n          />\n        </Row>,\n        footerEl,\n      )\n    }\n    return null\n  }\n\n  return (\n    <ContentWrapper data-testid=\"add-db_sentinel\">\n      <ScrollableWrapper as=\"div\" ref={formRef}>\n        <MessageSentinel />\n        <br />\n        <form\n          onSubmit={formik.handleSubmit}\n          data-testid=\"form\"\n          onKeyDown={onKeyDown}\n          role=\"presentation\"\n        >\n          <DatabaseForm\n            formik={formik}\n            showFields={{\n              host: true,\n              port: true,\n              alias: false,\n              timeout: false,\n            }}\n            onHostNamePaste={onHostNamePaste}\n          />\n          <Spacer />\n          <TlsDetails\n            formik={formik}\n            certificates={certificates}\n            caCertificates={caCertificates}\n          />\n        </form>\n      </ScrollableWrapper>\n      <Footer />\n    </ContentWrapper>\n  )\n}\n\nexport default SentinelConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/sentinel-connection/sentinel-connection-form/index.ts",
    "content": "import SentinelConnectionForm from './SentinelConnectionForm'\n\nexport default SentinelConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/styles.module.scss",
    "content": ".errorFieldText {\n  float: left;\n  font-size: 12px !important;\n}\n\n.error {\n  color: var(--euiColorDangerText) !important;\n}\n\n:global(body .footerAddDatabase .empty-btn) {\n  border-color: transparent;\n\n  &:hover,\n  &:active {\n    background-color: var(--euiTooltipBackgroundColor);\n  }\n}\n\n.errorForm {\n  color: var(--euiColorDangerText) !important;\n  padding-top: 25px;\n  display: inline-block;\n}\n\n.anchorEndpoints {\n  pointer-events: auto !important;\n\n  svg {\n    width: 24px !important;\n    height: 24px !important;\n  }\n}\n\n.endpointsList * {\n  font-weight: 300 !important;\n}\n\n.dbAliasInputContainer {\n  display: inline-block;\n  position: relative;\n  width: calc(100% - 190px);\n}\n\n.btnOpen {\n  float: right;\n  svg {\n    height: 20px;\n    width: 20px;\n  }\n}\n\n.hostPort {\n  display: inline-block;\n  margin-left: 10px;\n}\n\n.hostPort:hover .copyHostPortBtn {\n  opacity: 1;\n}\n\n.copyHostPortBtn {\n  margin-left: 10px;\n  margin-bottom: 3px;\n  opacity: 0;\n  height: 0;\n  transition: opacity 250ms ease-in-out;\n}\n\n.sentinelCollapsedField {\n  padding: 4px 8px;\n  font-size: 13px !important;\n  word-break: break-all;\n}\n\n#dbAliasInputIcon {\n  color: var(--euiTextSubduedColor) !important;\n  width: 32px !important;\n}\n\n.message {\n  font-family: 'Graphik', sans-serif;\n  color: var(--euiTextSubduedColor) !important;\n}\n\n.link {\n  text-decoration: underline !important;\n\n  &:hover {\n    text-decoration: none !important;\n  }\n\n  &.external:global(.euiLink) {\n    color: var(--externalLinkColor) !important;\n  }\n}\n\n.fullWidth {\n  flex-basis: 100% !important;\n}\n\n.selectedOptionWithLongTextSupport {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  padding-right: 1.5rem;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/TagsCell.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { Tag } from 'uiSrc/slices/interfaces/tag'\nimport { TagsCell } from './TagsCell'\n\nconst tags: Tag[] = [\n  {\n    id: '1',\n    key: 'env',\n    value: 'prod',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n  {\n    id: '2',\n    key: 'version',\n    value: '1.0',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n]\n\ndescribe('TagsCell', () => {\n  it('should render the first tag and the count of remaining tags', () => {\n    render(<TagsCell tags={tags} />)\n    expect(screen.getByText('env : prod')).toBeInTheDocument()\n    expect(screen.getByText('+1')).toBeInTheDocument()\n  })\n\n  it('should render null if no tags are provided', () => {\n    const { container } = render(<TagsCell tags={[]} />)\n    expect(container.firstChild).toBeNull()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/TagsCell.tsx",
    "content": "import React from 'react'\n\nimport { Tag } from 'uiSrc/slices/interfaces/tag'\nimport { RiTooltip } from 'uiSrc/components'\nimport { Chip } from '@redis-ui/components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\ntype TagsCellProps = {\n  tags?: Tag[]\n}\n\nexport const TagsCell = ({ tags }: TagsCellProps) => {\n  if (!tags?.[0]) {\n    return null\n  }\n\n  const firstTagText = `${tags[0].key} : ${tags[0].value}`\n  const remainingTagsCount = tags.length - 1\n\n  return (\n    <Row gap=\"s\">\n      <Chip text={firstTagText} size=\"S\" />\n      {remainingTagsCount > 0 && (\n        <RiTooltip\n          position=\"top\"\n          content={\n            <Col>\n              {tags.slice(1).map((tag) => (\n                <Text key={tag.id}>\n                  {tag.key} : {tag.value}\n                </Text>\n              ))}\n            </Col>\n          }\n        >\n          <Chip text={`+${remainingTagsCount}`} size=\"S\" />\n        </RiTooltip>\n      )}\n    </Row>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/TagsCellHeader.spec.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport {\n  fireEvent,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\nimport { Tag } from 'uiSrc/slices/interfaces/tag'\nimport { TagsCellHeader } from './TagsCellHeader'\n\njest.mock('react-redux', () => ({\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n  connect: () => (Component: any) => Component,\n}))\n\nconst mockDispatch = useDispatch as jest.MockedFunction<typeof useDispatch>\nconst mockSelector = useSelector as jest.MockedFunction<typeof useSelector>\n\nconst mockTags: Tag[] = [\n  {\n    id: '1',\n    key: 'env',\n    value: 'prod',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n  {\n    id: '2',\n    key: 'version',\n    value: '1.0',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n]\n\ndescribe('TagsCellHeader', () => {\n  beforeEach(() => {\n    mockDispatch.mockReturnValue(jest.fn())\n    mockSelector.mockReturnValue({\n      data: mockTags,\n      selectedTags: new Set(),\n    })\n  })\n\n  it('should render the TagsCellHeader component', () => {\n    render(<TagsCellHeader />)\n    expect(screen.getByText('Tags')).toBeInTheDocument()\n  })\n\n  it('should open the popover when the filter icon is clicked', async () => {\n    const { getByRole } = render(<TagsCellHeader />)\n    fireEvent.click(getByRole('button'))\n    await waitForRiPopoverVisible()\n\n    expect(screen.getByTestId('tag-search')).toBeInTheDocument()\n  })\n\n  it('should filter tags based on search input', async () => {\n    const { getByRole, getByTestId } = render(<TagsCellHeader />)\n    fireEvent.click(getByRole('button'))\n    await waitForRiPopoverVisible()\n\n    expect(getByTestId(`${mockTags[0].key}:${mockTags[0].value}`)).toBeVisible()\n    expect(getByTestId(`${mockTags[1].key}:${mockTags[1].value}`)).toBeVisible()\n\n    fireEvent.change(getByTestId('tag-search'), {\n      target: { value: 'version' },\n    })\n\n    expect(getByTestId('tag-search')).toHaveValue('version')\n    try {\n      getByTestId(`${mockTags[0].key}:${mockTags[0].value}`)\n    } catch (e) {\n      expect(e).toBeInstanceOf(Error)\n      expect(\n        (e as Error)?.message.startsWith(\n          'Unable to find an element by: [data-testid=\"env:prod\"]',\n        ),\n      ).toBeTruthy()\n    }\n    expect(getByTestId(`${mockTags[1].key}:${mockTags[1].value}`)).toBeVisible()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/TagsCellHeader.tsx",
    "content": "import React, { memo } from 'react'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { SearchInput } from 'uiSrc/components/base/inputs'\nimport { useFilterTags } from './useFilterTags'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport styles from './styles.module.scss'\nimport { COLUMN_FIELD_NAME_MAP, DatabaseListColumn } from 'uiSrc/constants'\n\nconst headerText = COLUMN_FIELD_NAME_MAP.get(DatabaseListColumn.Tags)\n\nexport const TagsCellHeader = memo(() => {\n  const {\n    isPopoverOpen,\n    tagSearch,\n    tagsData,\n    selectedTags,\n    setTagSearch,\n    onPopoverToggle,\n    onTagChange,\n    onKeyChange,\n    groupedTags,\n  } = useFilterTags()\n\n  if (!tagsData.length) {\n    return <Row centered>{headerText}</Row>\n  }\n\n  return (\n    <Row centered>\n      {headerText}\n      <RiPopover\n        button={\n          <RiIcon\n            role=\"button\"\n            type=\"FilterTableIcon\"\n            size=\"l\"\n            className={styles.filterByTagIcon}\n            onClick={(e) => {\n              e.stopPropagation()\n              onPopoverToggle()\n            }}\n          />\n        }\n        isOpen={isPopoverOpen}\n        closePopover={onPopoverToggle}\n        anchorPosition=\"downCenter\"\n      >\n        {/* stop propagation to prevent sorting by column header */}\n        {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}\n        <div style={{ width: 300 }} onClick={(e) => e.stopPropagation()}>\n          <FormField>\n            <SearchInput\n              data-testid=\"tag-search\"\n              placeholder=\"Enter tag key or value\"\n              value={tagSearch}\n              onChange={(value) => {\n                setTagSearch(value)\n              }}\n            />\n          </FormField>\n          <Spacer size=\"m\" />\n          {Object.keys(groupedTags).map((key) => (\n            <div key={key}>\n              <Checkbox\n                id={key}\n                className={styles.filterTagLabel}\n                label={key}\n                checked={groupedTags[key].every((value) =>\n                  selectedTags.has(`${key}:${value}`),\n                )}\n                onChange={(event) => {\n                  onKeyChange(key, event.target.checked)\n                }}\n              />\n              {groupedTags[key].map((value) => (\n                <div key={value} style={{ margin: '10px 0 0 20px' }}>\n                  <Checkbox\n                    id={`${key}:${value}`}\n                    className={styles.filterTagLabel}\n                    data-testid={`${key}:${value}`}\n                    label={value}\n                    checked={selectedTags.has(`${key}:${value}`)}\n                    onChange={(event) => {\n                      onTagChange(`${key}:${value}`, event.target.checked)\n                    }}\n                  />\n                </div>\n              ))}\n            </div>\n          ))}\n        </div>\n      </RiPopover>\n    </Row>\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/styles.module.scss",
    "content": ".filterByTagIcon {\n  pointer-events: all;\n  cursor: pointer;\n  & {\n    margin: 8px;\n  }\n}\n\n.filterTagLabel {\n  label {\n    text-overflow: ellipsis;\n    width: 100%;\n    overflow: hidden;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/useFilterTags.spec.ts",
    "content": "import { renderHook, act } from '@testing-library/react-hooks'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { setSelectedTags } from 'uiSrc/slices/instances/tags'\nimport { Tag } from 'uiSrc/slices/interfaces/tag'\nimport { useFilterTags } from './useFilterTags'\n\njest.mock('react-redux', () => ({\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n}))\n\nconst mockTags: Tag[] = [\n  {\n    id: '1',\n    key: 'env',\n    value: 'prod',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n  {\n    id: '2',\n    key: 'version',\n    value: '1.0',\n    createdAt: new Date().toISOString(),\n    updatedAt: new Date().toISOString(),\n  },\n]\n\nconst mockDispatch = useDispatch as jest.MockedFunction<typeof useDispatch>\nconst mockSelector = useSelector as jest.MockedFunction<typeof useSelector>\n\ndescribe('useFilterTags', () => {\n  beforeEach(() => {\n    mockDispatch.mockReturnValue(jest.fn())\n    mockSelector.mockReturnValue({\n      data: mockTags,\n      selectedTags: new Set(),\n    })\n  })\n\n  it('should toggle the popover state', () => {\n    const { result } = renderHook(() => useFilterTags())\n    act(() => {\n      result.current.onPopoverToggle()\n    })\n    expect(result.current.isPopoverOpen).toBe(true)\n  })\n\n  it('should update selected tags on tag change', () => {\n    const { result } = renderHook(() => useFilterTags())\n    act(() => {\n      result.current.onTagChange('env:prod', true)\n    })\n    expect(mockDispatch()).toHaveBeenCalledWith(\n      setSelectedTags(new Set(['env:prod'])),\n    )\n  })\n\n  it('should filter tags based on search input', () => {\n    const { result } = renderHook(() => useFilterTags())\n    expect(result.current.groupedTags).toEqual({\n      env: ['prod'],\n      version: ['1.0'],\n    })\n    act(() => {\n      result.current.setTagSearch('env')\n    })\n    expect(result.current.groupedTags).toEqual({ env: ['prod'] })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/components/tags-cell/useFilterTags.ts",
    "content": "import { useState, useCallback, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { tagsSelector, setSelectedTags } from 'uiSrc/slices/instances/tags'\n\nexport const useFilterTags = () => {\n  const dispatch = useDispatch()\n  const { data: tagsData, selectedTags } = useSelector(tagsSelector)\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n  const [tagSearch, setTagSearch] = useState('')\n\n  const onPopoverToggle = () => {\n    setIsPopoverOpen(!isPopoverOpen)\n  }\n\n  const onTagChange = useCallback(\n    (tag: string, checked: boolean) => {\n      const newSelectedTags = new Set(selectedTags)\n      const setMethod = checked ? 'add' : 'delete'\n      newSelectedTags[setMethod](tag)\n      dispatch(setSelectedTags(newSelectedTags))\n    },\n    [dispatch, selectedTags],\n  )\n\n  const onKeyChange = useCallback(\n    (key: string, checked: boolean) => {\n      const tagsWithKey = tagsData\n        .filter((tag) => tag.key === key)\n        .map((tag) => `${tag.key}:${tag.value}`)\n      const newSelectedTags = new Set(selectedTags)\n      const setMethod = checked ? 'add' : 'delete'\n      tagsWithKey.forEach((tag) => newSelectedTags[setMethod](tag))\n      dispatch(setSelectedTags(newSelectedTags))\n    },\n    [dispatch, tagsData, selectedTags],\n  )\n\n  const filteredTags = useMemo(\n    () =>\n      tagsData.filter((tag) => `${tag.key}:${tag.value}`.includes(tagSearch)),\n    [tagSearch, tagsData],\n  )\n\n  const groupedTags = useMemo(\n    () =>\n      filteredTags.reduce(\n        (acc, tag) => {\n          if (!acc[tag.key]) {\n            acc[tag.key] = []\n          }\n          acc[tag.key].push(tag.value)\n          return acc\n        },\n        {} as Record<string, string[]>,\n      ),\n    [filteredTags],\n  )\n\n  return {\n    isPopoverOpen,\n    tagSearch,\n    tagsData,\n    selectedTags,\n    setTagSearch,\n    onPopoverToggle,\n    onTagChange,\n    onKeyChange,\n    groupedTags,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/constants/database.ts",
    "content": "export enum AddDbType {\n  manual,\n  cloud,\n  sentinel,\n  software,\n  azure,\n  import,\n}\n\nexport enum CloudConnectionOptions {\n  Account = 'cloud-account',\n  ApiKeys = 'cloud-api-keys',\n}\n\nexport const CREATE_CLOUD_DB_ID = 'create-free-cloud-db'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/constants/form.ts",
    "content": "import { getConfig } from 'uiSrc/config'\n\nexport const ADD_NEW_CA_CERT = 'ADD_NEW_CA_CERT'\nexport const NO_CA_CERT = 'NO_CA_CERT'\nexport const ADD_NEW = 'ADD_NEW'\nexport const NONE = 'NONE'\nexport const DEFAULT_HOST = '127.0.0.1'\nexport const DEFAULT_PORT = '6379'\nexport const DEFAULT_ALIAS = `${DEFAULT_HOST}:${DEFAULT_PORT}`\n\nexport enum SshPassType {\n  Password = 'password',\n  PrivateKey = 'privateKey',\n}\n\nexport const fieldDisplayNames = {\n  port: 'Port',\n  host: 'Host',\n  name: 'Database alias',\n  selectedCaCertName: 'CA Certificate',\n  newCaCertName: 'CA Certificate Name',\n  newCaCert: 'CA certificate',\n  newTlsCertPairName: 'Client Certificate Name',\n  newTlsClientCert: 'Client Certificate',\n  newTlsClientKey: 'Private Key',\n  servername: 'Server Name',\n  sentinelMasterName: 'Primary Group Name',\n  sshHost: 'SSH Host',\n  sshPort: 'SSH Port',\n  sshPrivateKey: 'SSH Private Key',\n  sshUsername: 'SSH Username',\n}\n\nexport const DEFAULT_TIMEOUT = getConfig().database.defaultConnectionTimeout\n\nexport enum SubmitBtnText {\n  AddDatabase = 'Add Redis Database',\n  EditDatabase = 'Apply Changes',\n  CloneDatabase = 'Clone Database',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/constants/help-links.ts",
    "content": "import { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { TelemetryEvent } from 'uiSrc/telemetry'\n\nexport interface IHelpGuide {\n  id: string\n  title: string\n  url: string\n  description?: string\n  event?: string\n  primary?: boolean\n  onClick?: (e: React.MouseEvent, source: OAuthSocialSource) => void\n}\n\nexport const HELP_LINKS = {\n  cloud: {\n    event: TelemetryEvent.CREATE_FREE_CLOUD_DATABASE_CLICKED,\n    sources: {\n      welcome: 'Welcome page',\n      databaseList: 'list of databases',\n      databaseConnectionList: 'database connection list',\n      redisearch: 'RediSearch is not loaded',\n    },\n  },\n  source: {\n    event: TelemetryEvent.BUILD_FROM_SOURCE_CLICKED,\n  },\n  docker: {\n    event: TelemetryEvent.BUILD_USING_DOCKER_CLICKED,\n  },\n  homebrew: {\n    event: TelemetryEvent.BUILD_USING_HOMEBREW_CLICKED,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/constants/index.ts",
    "content": "export * from './form'\nexport * from './help-links'\nexport * from './database'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/contexts/HomePageDataProvider.tsx",
    "content": "import React, { createContext, useContext, useState } from 'react'\n\nexport enum OpenDialogName {\n  AddDatabase = 'add',\n  ManageTags = 'manage-tags',\n  EditDatabase = 'edit',\n}\n\nexport interface HomePageDataProviderContextType {\n  openDialog: OpenDialogName | null\n  setOpenDialog: (openDialog: OpenDialogName | null) => void\n}\n\nexport const HomePageDataProviderContext = createContext<\n  HomePageDataProviderContextType | undefined\n>(undefined)\n\nexport const HomePageDataProviderProvider: React.FC<{\n  children: React.ReactNode\n}> = ({ children }) => {\n  const [openDialog, setOpenDialog] = useState<OpenDialogName | null>(null)\n\n  return (\n    <HomePageDataProviderContext.Provider\n      value={{\n        openDialog,\n        setOpenDialog,\n      }}\n    >\n      {children}\n    </HomePageDataProviderContext.Provider>\n  )\n}\n\nexport const useHomePageDataProvider = () => {\n  const context = useContext(HomePageDataProviderContext)\n\n  if (!context) {\n    throw new Error(\n      'useHomePageDataProvider must be used within a HomePageDataProviderProvider',\n    )\n  }\n\n  return context\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/index.ts",
    "content": "import HomePage from './HomePage'\n\nexport { HomePage }\n\nexport default HomePage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/interfaces/form.ts",
    "content": "import { Instance } from 'uiSrc/slices/interfaces'\nimport { ADD_NEW_CA_CERT, NO_CA_CERT } from 'uiSrc/pages/home/constants'\nimport { KeyValueFormat } from 'uiSrc/constants'\n\nexport interface DbConnectionInfo extends Instance {\n  id?: string\n  port: string\n  tlsClientAuthRequired?: boolean\n  certificates?: { id: number; name: string }[]\n  selectedTlsClientCertId?: string | 'ADD_NEW' | undefined\n  newTlsCertPairName?: string\n  newTlsClientCert?: string\n  newTlsClientKey?: string\n  servername?: string\n  verifyServerTlsCert?: boolean\n  caCertificates?: { name: string; id: string }[]\n  selectedCaCertName: string | typeof ADD_NEW_CA_CERT | typeof NO_CA_CERT\n  newCaCertName?: string\n  newCaCert?: string\n  username?: string\n  password?: string | true\n  timeout?: string\n  showDb?: boolean\n  forceStandalone?: boolean\n  showCompressor?: boolean\n  keyNameFormat: typeof KeyValueFormat\n  sni?: boolean\n  sentinelMasterUsername?: string\n  sentinelMasterPassword?: string | true\n  sentinelMasterName?: string\n  ssh?: boolean\n  sshPassType?: string\n  sshHost?: string\n  sshPort?: string\n  sshUsername?: string\n  sshPassword?: string | true\n  sshPrivateKey?: string | true\n  sshPassphrase?: string | true\n}\n\nexport interface ISubmitButton {\n  onClick: () => void\n  text?: string\n  submitIsDisabled?: boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/interfaces/index.ts",
    "content": "export * from './form'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/styles.module.scss",
    "content": ".pageWrapper {\n  height: 100%;\n  overflow: hidden;\n}\n\n.page {\n  height: 100%;\n  padding: 1px 16px 16px !important;\n\n  :global {\n    .homePage {\n      height: calc(100% - 60px);\n    }\n  }\n}\n\n.explorePanel {\n  padding-bottom: 16px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/styles.scss",
    "content": ".homePage {\n  .euiTitle {\n    padding-bottom: 15px;\n    font-size: 18px;\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n\n  .euiFormControlLayout {\n    max-width: 100% !important;\n\n    input,\n    select,\n    textarea {\n      max-width: 100% !important;\n      font-size: 14px;\n      line-height: 24px;\n    }\n  }\n\n  .euiRadioGroup__item {\n    display: inline-block;\n    vertical-align: top !important;\n    margin-top: 1px !important;\n    margin-bottom: 3px !important;\n    margin-right: 30px !important;\n  }\n\n  .euiFormHelpText {\n    color: var(--euiColorMediumShade);\n  }\n\n  .euiCheckbox__label {\n    font-size: 14px !important;\n  }\n\n  textarea {\n    resize: none;\n    max-height: 85px;\n    transition: max-height 0.4s ease;\n    &:focus {\n      max-height: 180px;\n    }\n  }\n\n  .container {\n    @include eui.scrollBar;\n\n    background-color: var(--euiColorEmptyShade);\n    overflow-y: auto;\n    overflow-x: hidden;\n    height: calc(100% - 74px);\n    padding: 40px 30px 25px;\n    margin-right: 5px;\n    border-radius: 4px;\n  }\n\n  .footerAddDatabase {\n    margin: 0;\n    width: 100%;\n    bottom: 0;\n    float: right;\n    text-align: right;\n    vertical-align: bottom;\n    padding: 15px 24px 0;\n    border-radius: 0 0 4px 4px;\n\n    border-top: 1px solid var(--euiColorLightShade);\n\n    .btn-cancel,\n    .btn-back {\n      margin-right: 10px;\n      min-width: 71px !important;\n    }\n  }\n\n  .clusterDatabaseList,\n  .clusterDatabaseListResult,\n  .cloudDatabaseList,\n  .cloudDatabaseListResult,\n  .sentinelDatabaseList,\n  .sentinelDatabaseListResult {\n    overflow: hidden !important;\n\n    .column_status * {\n      text-transform: capitalize !important;\n    }\n\n    .euiButton.btn-back {\n      float: left;\n    }\n  }\n\n\n  .clusterDatabaseListResult .euiTableRowCell:first-child,\n  .clusterDatabaseListResult .euiTableHeaderCell:first-child,\n  .sentinelDatabaseListResult .euiTableRowCell:first-child,\n  .sentinelDatabaseListResult .euiTableHeaderCell:first-child,\n  .cloudDatabaseListResult .euiTableRowCell:first-child,\n  .cloudDatabaseListResult .euiTableHeaderCell:first-child {\n    padding-left: 10px;\n  }\n}\n\n.theme_DARK {\n  .homePage {\n    .databaseList {\n      .euiTableCellContent {\n        color: var(--textColorShade) !important;\n      }\n\n      .euiTableHeaderCell .euiTableCellContent {\n        color: #fff !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/utils/form.tsx",
    "content": "import { isUndefined, toString } from 'lodash'\nimport React from 'react'\nimport { FormikErrors } from 'formik'\nimport { InstanceType } from 'uiSrc/slices/interfaces'\nimport {\n  ADD_NEW,\n  ADD_NEW_CA_CERT,\n  DEFAULT_ALIAS,\n  DEFAULT_HOST,\n  DEFAULT_PORT,\n  DEFAULT_TIMEOUT,\n  fieldDisplayNames,\n  NO_CA_CERT,\n  NONE,\n  SshPassType,\n} from 'uiSrc/pages/home/constants'\nimport { DbConnectionInfo } from 'uiSrc/pages/home/interfaces'\nimport { Nullable, parseRedisUrl } from 'uiSrc/utils'\n\nexport const getTlsSettings = (values: DbConnectionInfo) => ({\n  useTls: values.tls,\n  servername: (values.sni && values.servername) || undefined,\n  verifyServerCert: values.verifyServerTlsCert,\n  caCert:\n    !values.tls || values.selectedCaCertName === NO_CA_CERT\n      ? undefined\n      : values.selectedCaCertName === ADD_NEW_CA_CERT\n        ? {\n            new: {\n              name: values.newCaCertName,\n              certificate: values.newCaCert,\n            },\n          }\n        : {\n            name: values.selectedCaCertName,\n          },\n  clientAuth: values.tls && values.tlsClientAuthRequired,\n  clientCert: !values.tls\n    ? undefined\n    : typeof values.selectedTlsClientCertId === 'string' &&\n        values.tlsClientAuthRequired &&\n        values.selectedTlsClientCertId !== ADD_NEW\n      ? { id: values.selectedTlsClientCertId }\n      : values.selectedTlsClientCertId === ADD_NEW &&\n          values.tlsClientAuthRequired\n        ? {\n            new: {\n              name: values.newTlsCertPairName,\n              certificate: values.newTlsClientCert,\n              key: values.newTlsClientKey,\n            },\n          }\n        : undefined,\n})\n\nexport const applyTlSDatabase = (database: any, tlsSettings: any) => {\n  const {\n    useTls,\n    verifyServerCert,\n    servername,\n    caCert,\n    clientAuth,\n    clientCert,\n  } = tlsSettings\n  if (!useTls) return\n\n  database.tls = useTls\n  database.tlsServername = servername\n  database.verifyServerCert = !!verifyServerCert\n  database.clientCert = clientCert\n\n  if (!isUndefined(caCert?.new)) {\n    database.caCert = {\n      name: caCert?.new.name,\n      certificate: caCert?.new.certificate,\n    }\n  }\n\n  if (!isUndefined(caCert?.name)) {\n    database.caCert = { id: caCert?.name }\n  }\n\n  if (clientAuth) {\n    if (!isUndefined(clientCert?.new)) {\n      database.clientCert = {\n        name: clientCert.new.name,\n        certificate: clientCert.new.certificate,\n        key: clientCert.new.key,\n      }\n    }\n\n    if (!isUndefined(clientCert?.id)) {\n      database.clientCert = { id: clientCert.id }\n    }\n  }\n}\n\nexport const applySSHDatabase = (database: any, values: DbConnectionInfo) => {\n  const {\n    ssh,\n    sshPassType,\n    sshHost,\n    sshPort,\n    sshPassword,\n    sshUsername,\n    sshPassphrase,\n    sshPrivateKey,\n  } = values\n\n  if (ssh) {\n    database.ssh = true\n    database.sshOptions = {\n      host: sshHost,\n      port: sshPort ? +sshPort : undefined,\n      username: sshUsername,\n    }\n\n    if (sshPassType === SshPassType.Password) {\n      database.sshOptions.password = sshPassword\n      database.sshOptions.passphrase = null\n      database.sshOptions.privateKey = null\n    }\n\n    if (sshPassType === SshPassType.PrivateKey) {\n      database.sshOptions.password = null\n      database.sshOptions.passphrase = sshPassphrase\n      database.sshOptions.privateKey = sshPrivateKey\n    }\n  }\n}\n\nexport const getFormErrors = (values: DbConnectionInfo) => {\n  const errs: FormikErrors<DbConnectionInfo> = {}\n\n  if (!values.host) {\n    errs.host = fieldDisplayNames.host\n  }\n  if (!values.port) {\n    errs.port = fieldDisplayNames.port\n  }\n\n  if (\n    values.tls &&\n    values.verifyServerTlsCert &&\n    values.selectedCaCertName === NO_CA_CERT\n  ) {\n    errs.selectedCaCertName = fieldDisplayNames.selectedCaCertName\n  }\n\n  if (\n    values.tls &&\n    values.selectedCaCertName === ADD_NEW_CA_CERT &&\n    values.newCaCertName === ''\n  ) {\n    errs.newCaCertName = fieldDisplayNames.newCaCertName\n  }\n\n  if (\n    values.tls &&\n    values.selectedCaCertName === ADD_NEW_CA_CERT &&\n    values.newCaCert === ''\n  ) {\n    errs.newCaCert = fieldDisplayNames.newCaCert\n  }\n\n  if (values.tls && values.sni && values.servername === '') {\n    errs.servername = fieldDisplayNames.servername\n  }\n\n  if (\n    values.tls &&\n    values.tlsClientAuthRequired &&\n    values.selectedTlsClientCertId === ADD_NEW\n  ) {\n    if (values.newTlsCertPairName === '') {\n      errs.newTlsCertPairName = fieldDisplayNames.newTlsCertPairName\n    }\n    if (values.newTlsClientCert === '') {\n      errs.newTlsClientCert = fieldDisplayNames.newTlsClientCert\n    }\n    if (values.newTlsClientKey === '') {\n      errs.newTlsClientKey = fieldDisplayNames.newTlsClientKey\n    }\n  }\n\n  if (values.ssh) {\n    if (!values.sshHost) {\n      errs.sshHost = fieldDisplayNames.sshHost\n    }\n    if (!values.sshPort) {\n      errs.sshPort = fieldDisplayNames.sshPort\n    }\n    if (!values.sshUsername) {\n      errs.sshUsername = fieldDisplayNames.sshUsername\n    }\n    if (\n      values.sshPassType === SshPassType.PrivateKey &&\n      !values.sshPrivateKey\n    ) {\n      errs.sshPrivateKey = fieldDisplayNames.sshPrivateKey\n    }\n  }\n\n  return errs\n}\n\nexport const autoFillFormDetails = (\n  content: string,\n  initialValues: any,\n  setInitialValues: (data: any) => void,\n  instanceType: InstanceType,\n): boolean => {\n  try {\n    const details = parseRedisUrl(content)\n\n    if (!details) return false\n\n    const getUpdatedInitialValues = () => {\n      switch (instanceType) {\n        case InstanceType.RedisEnterpriseCluster: {\n          return {\n            host: details.host || initialValues.host || 'localhost',\n            port: `${details.port || initialValues.port || 9443}`,\n            username: details.username || '',\n            password: details.password || '',\n          }\n        }\n\n        case InstanceType.Sentinel: {\n          return getFormValues({\n            host: details.host || initialValues.host || 'localhost',\n            port: `${details.port || initialValues.port || 9443}`,\n            username: details.username || '',\n            password: details.password,\n            tls: details.protocol === 'rediss',\n          })\n        }\n\n        case InstanceType.Standalone: {\n          return getFormValues({\n            name: details.hostname || initialValues.name || 'localhost:6379',\n            host: details.host || initialValues.host || 'localhost',\n            port: `${details.port || initialValues.port || 9443}`,\n            username: details.username || '',\n            password: details.password,\n            tls: details.protocol === 'rediss',\n            db: details.dbNumber,\n            ssh: false,\n            sshPassType: SshPassType.Password,\n          })\n        }\n        default: {\n          return {}\n        }\n      }\n    }\n    setInitialValues(getUpdatedInitialValues())\n    /*\n     * autofill was successfull so return true\n     */\n    return true\n  } catch (err) {\n    /* The pasted content is not a connection URI so ignore. */\n    return false\n  }\n}\n\nexport const getSubmitButtonContent = (\n  errors: FormikErrors<DbConnectionInfo>,\n  submitIsDisabled?: boolean,\n) => {\n  const maxErrorsCount = 5\n  const errorsArr = Object.values(errors).map((err) => [\n    err,\n    <br key={err as string} />,\n  ])\n\n  if (errorsArr.length > maxErrorsCount) {\n    errorsArr.splice(maxErrorsCount, errorsArr.length, ['...'])\n  }\n  return submitIsDisabled ? <span>{errorsArr}</span> : null\n}\n\nexport const getFormValues = (instance?: Nullable<Record<string, any>>) => ({\n  ...instance,\n  host: instance?.host ?? (instance ? '' : DEFAULT_HOST),\n  port: instance?.port?.toString() ?? (instance ? '' : DEFAULT_PORT),\n  timeout: instance?.timeout\n    ? toString(instance?.timeout / 1_000)\n    : toString(DEFAULT_TIMEOUT / 1_000),\n  name: instance?.name ?? (instance ? '' : DEFAULT_ALIAS),\n  username: instance?.username ?? '',\n  password: instance?.password ?? '',\n  tls: instance?.tls ?? false,\n  db: instance?.db,\n  compressor: instance?.compressor ?? NONE,\n  modules: instance?.modules,\n  showDb: !!instance?.db,\n  forceStandalone: instance?.forceStandalone ?? false,\n  showCompressor:\n    instance && instance.compressor && instance.compressor !== NONE,\n  sni: !!instance?.tlsServername,\n  servername: instance?.tlsServername,\n  newCaCert: '',\n  newCaCertName: '',\n  selectedCaCertName: instance?.caCert?.id ?? NO_CA_CERT,\n  tlsClientAuthRequired: instance?.clientCert?.id ?? false,\n  verifyServerTlsCert: instance?.verifyServerCert ?? false,\n  newTlsCertPairName: '',\n  selectedTlsClientCertId: instance?.clientCert?.id ?? ADD_NEW,\n  newTlsClientCert: '',\n  newTlsClientKey: '',\n  sentinelMasterName: instance?.sentinelMaster?.name || '',\n  sentinelMasterUsername: instance?.sentinelMaster?.username,\n  sentinelMasterPassword: instance?.sentinelMaster?.password,\n  ssh: instance?.ssh ?? false,\n  sshPassType: instance?.sshOptions\n    ? instance.sshOptions.privateKey\n      ? SshPassType.PrivateKey\n      : SshPassType.Password\n    : SshPassType.Password,\n  sshHost: instance?.sshOptions?.host ?? '',\n  sshPort: instance?.sshOptions?.port ?? 22,\n  sshUsername: instance?.sshOptions?.username ?? '',\n  sshPassword: instance?.sshOptions?.password ?? '',\n  sshPrivateKey: instance?.sshOptions?.privateKey ?? '',\n  sshPassphrase: instance?.sshOptions?.passphrase ?? '',\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/home/utils/index.ts",
    "content": "export * from './form'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/index.ts",
    "content": "export * from './browser'\nexport * from './settings'\nexport * from './instance'\nexport * from './home'\nexport * from './redis-cluster'\nexport * from './autodiscover-cloud'\nexport * from './autodiscover-azure'\nexport * from './vector-search'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/InstancePage.spec.tsx",
    "content": "import { cloneDeep, set } from 'lodash'\nimport React from 'react'\nimport { BrowserRouter } from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\n\nimport { waitFor, within } from '@testing-library/react'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  act,\n  mockStore,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { resetKeys, resetPatternKeysData } from 'uiSrc/slices/browser/keys'\nimport { setMonitorInitialState } from 'uiSrc/slices/cli/monitor'\nimport { setInitialPubSubState } from 'uiSrc/slices/pubsub/pubsub'\nimport { setBulkActionsInitialState } from 'uiSrc/slices/browser/bulkActions'\nimport {\n  appContextSelector,\n  setAppContextConnectedInstanceId,\n  setAppContextInitialState,\n  setDbConfig,\n} from 'uiSrc/slices/app/context'\nimport * as appFeaturesSlice from 'uiSrc/slices/app/features'\nimport {\n  resetCliHelperSettings,\n  resetCliSettings,\n} from 'uiSrc/slices/cli/cli-settings'\nimport {\n  resetRedisearchKeysData,\n  setRedisearchInitialState,\n} from 'uiSrc/slices/browser/redisearch'\nimport { setClusterDetailsInitialState } from 'uiSrc/slices/analytics/clusterDetails'\nimport { setDatabaseAnalysisInitialState } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { setInitialAnalyticsSettings } from 'uiSrc/slices/analytics/settings'\nimport {\n  getRecommendations,\n  setInitialRecommendationsState,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  getDatabaseConfigInfo,\n  loadInstances,\n  setConnectedInfoInstance,\n  setConnectedInstance,\n  setDefaultInstance,\n} from 'uiSrc/slices/instances/instances'\nimport * as rdiInstanceSlice from 'uiSrc/slices/rdi/instances'\nimport { loadInstances as loadRdiInstances } from 'uiSrc/slices/rdi/instances'\n\nimport { clearExpertChatHistory } from 'uiSrc/slices/panels/aiAssistant'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport { getAllPlugins } from 'uiSrc/slices/app/plugins'\nimport { DEFAULT_RDI_SHOWN_COLUMNS, FeatureFlags } from 'uiSrc/constants'\nimport { getDatabasesApiSpy } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport InstancePage, { Props } from './InstancePage'\n\nconst INSTANCE_ID_MOCK = 'instanceId'\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/services', () => ({\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextSelector: jest.fn().mockReturnValue({\n    contextInstanceId: INSTANCE_ID_MOCK,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  jest\n    .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n    .mockReturnValue({\n      insightsRecommendations: {\n        flag: false,\n      },\n      envDependent: {\n        flag: true,\n      },\n    })\n\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  getDatabasesApiSpy.mockClear()\n})\n\n/**\n * InstancePage tests\n *\n * @group component\n */\ndescribe('InstancePage', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render with CLI Header Minimized Component', () => {\n    const { queryByTestId } = render(\n      <BrowserRouter>\n        <InstancePage {...instance(mockedProps)} />\n      </BrowserRouter>,\n    )\n\n    expect(queryByTestId('expand-cli')).toBeInTheDocument()\n  })\n\n  it('should call proper actions with resetting context', async () => {\n    ;(appContextSelector as jest.Mock).mockReturnValue({\n      contextInstanceId: 'prevId',\n    })\n\n    // Flush pending async thunks leaked from previous test renders\n    await act(async () => {})\n    store.clearActions()\n\n    await act(() => {\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      )\n    })\n\n    const resetContextActions = [\n      resetKeys(),\n      setMonitorInitialState(),\n      setInitialPubSubState(),\n      setBulkActionsInitialState(),\n      setAppContextInitialState(),\n      resetPatternKeysData(),\n      resetCliHelperSettings(),\n      resetCliSettings(),\n      resetRedisearchKeysData(),\n      setClusterDetailsInitialState(),\n      setDatabaseAnalysisInitialState(),\n      setInitialAnalyticsSettings(),\n      setRedisearchInitialState(),\n      setInitialRecommendationsState(),\n    ]\n\n    const expectedActions = [\n      loadInstances(),\n      loadRdiInstances(),\n      getAllPlugins(),\n      setDefaultInstance(),\n      setConnectedInstance(),\n      getDatabaseConfigInfo(),\n      setConnectedInfoInstance(),\n      getRecommendations(),\n      ...resetContextActions,\n      clearExpertChatHistory(),\n      setConnectivityError(null),\n      setAppContextConnectedInstanceId(INSTANCE_ID_MOCK),\n      setDbConfig(undefined),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n\n  it('should call databases list api', async () => {\n    ;(appContextSelector as jest.Mock).mockReturnValue({\n      contextInstanceId: 'prevId',\n    })\n\n    const initialState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    await act(() => {\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n        {\n          store: mockStore(initialState),\n        },\n      )\n    })\n\n    await waitFor(() => expect(getDatabasesApiSpy).toHaveBeenCalledTimes(1))\n  })\n\n  it('should not call databases list api when flag disabled', async () => {\n    ;(appContextSelector as jest.Mock).mockReturnValue({\n      contextInstanceId: 'prevId',\n    })\n\n    const initialState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    await act(() => {\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n        {\n          store: mockStore(initialState),\n        },\n      )\n    })\n\n    await waitFor(() => expect(getDatabasesApiSpy).toHaveBeenCalledTimes(0))\n  })\n\n  it('should not render connectivity error page when envDependent feature flag is on', () => {\n    const initialState = set(\n      cloneDeep(initialStateDefault),\n      'app.connectivity',\n      {\n        loading: false,\n        error: 'Test error',\n      },\n    )\n\n    const { queryByTestId } = render(\n      <BrowserRouter>\n        <InstancePage {...instance(mockedProps)} />\n      </BrowserRouter>,\n      {\n        store: mockStore(initialState),\n      },\n    )\n\n    expect(queryByTestId('connectivity-error-message')).not.toBeInTheDocument()\n  })\n\n  it('should render connectivity error page when error occurs and flag is off', () => {\n    jest\n      .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n      .mockReturnValue({\n        insightsRecommendations: {\n          flag: false,\n        },\n        envDependent: {\n          flag: false,\n        },\n      })\n\n    const initialState = set(\n      cloneDeep(initialStateDefault),\n      'app.connectivity',\n      {\n        loading: false,\n        error: 'Test error',\n      },\n    )\n\n    const { getByTestId } = render(\n      <BrowserRouter>\n        <InstancePage {...instance(mockedProps)} />\n      </BrowserRouter>,\n      {\n        store: mockStore(initialState),\n      },\n    )\n\n    const { getByText } = within(getByTestId('connectivity-error-message'))\n    expect(getByText('Test error')).toBeInTheDocument()\n  })\n\n  it('should dispatch fetchRdiInstancesAction when rdiInstances is empty and envDependent flag is true', async () => {\n    jest.spyOn(rdiInstanceSlice, 'instancesSelector').mockReturnValue({\n      data: [],\n      loading: false,\n      error: '',\n      connectedInstance: {} as unknown as RdiInstance,\n      loadingChanging: false,\n      errorChanging: '',\n      changedSuccessfully: false,\n      isPipelineLoaded: false,\n    })\n    const mockFetchInstancesAction = jest.fn()\n    jest\n      .spyOn(rdiInstanceSlice, 'fetchInstancesAction')\n      .mockImplementation(() => mockFetchInstancesAction)\n\n    jest\n      .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n      .mockReturnValue({\n        [FeatureFlags.envDependent]: { flag: true },\n      })\n\n    await act(async () => {\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n        { store: mockStore(initialStateDefault) },\n      )\n    })\n\n    expect(mockFetchInstancesAction).toHaveBeenCalled()\n  })\n\n  it('should not dispatch fetchRdiInstancesAction when envDependent flag is false', async () => {\n    jest.spyOn(rdiInstanceSlice, 'instancesSelector').mockReturnValue({\n      data: [],\n      loading: false,\n      error: '',\n      connectedInstance: {} as unknown as RdiInstance,\n      loadingChanging: false,\n      errorChanging: '',\n      changedSuccessfully: false,\n      shownColumns: DEFAULT_RDI_SHOWN_COLUMNS,\n    })\n    const mockFetchInstancesAction = jest.fn()\n    jest\n      .spyOn(rdiInstanceSlice, 'fetchInstancesAction')\n      .mockImplementation(() => mockFetchInstancesAction)\n\n    jest\n      .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n      .mockReturnValue({\n        [FeatureFlags.envDependent]: { flag: false },\n      })\n\n    await act(async () => {\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n        { store: mockStore(initialStateDefault) },\n      )\n    })\n\n    expect(mockFetchInstancesAction).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/InstancePage.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useLocation, useParams } from 'react-router-dom'\n\nimport {\n  fetchConnectedInstanceAction,\n  fetchConnectedInstanceInfoAction,\n  fetchInstancesAction,\n  getDatabaseConfigInfoAction,\n  instancesSelector as dbInstancesSelector,\n} from 'uiSrc/slices/instances/instances'\nimport {\n  fetchInstancesAction as fetchRdiInstancesAction,\n  instancesSelector as rdiInstancesSelector,\n} from 'uiSrc/slices/rdi/instances'\nimport { fetchRecommendationsAction } from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  appContextSelector,\n  resetDatabaseContext,\n  setAppContextConnectedInstanceId,\n  setDbConfig,\n} from 'uiSrc/slices/app/context'\nimport { BrowserStorageItem, FeatureFlags } from 'uiSrc/constants'\nimport { localStorageService } from 'uiSrc/services'\nimport { InstancePageTemplate } from 'uiSrc/templates'\nimport { getPageName } from 'uiSrc/utils/routing'\nimport { loadPluginsAction } from 'uiSrc/slices/app/plugins'\nimport { appConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { getConfig } from 'uiSrc/config'\nimport InstancePageRouter from './InstancePageRouter'\nimport InstanceConnectionLost from './instanceConnectionLost'\n\nconst riConfig = getConfig()\n\nconst { shouldGetRecommendations, defaultTimeoutToGetRecommendations } =\n  riConfig.database\n\nexport interface Props {\n  routes: any[]\n}\n\nconst InstancePage = ({ routes = [] }: Props) => {\n  const [isShouldChildrenRerender, setIsShouldChildrenRerender] =\n    useState(false)\n\n  const dispatch = useDispatch()\n  const { pathname } = useLocation()\n\n  const { data: rdiInstances } = useSelector(rdiInstancesSelector)\n  const { data: dbInstances } = useSelector(dbInstancesSelector)\n\n  const { instanceId: connectionInstanceId } = useParams<{\n    instanceId: string\n  }>()\n  const { contextInstanceId } = useSelector(appContextSelector)\n  const connectivityError = useSelector(appConnectivityError)\n  const { [FeatureFlags.envDependent]: envDependent } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const lastPageRef = useRef<string>()\n\n  useEffect(() => {\n    if (!dbInstances?.length) {\n      dispatch(fetchInstancesAction())\n    }\n    if (!rdiInstances?.length && envDependent?.flag) {\n      dispatch(fetchRdiInstancesAction())\n    }\n  }, [])\n\n  useEffect(() => {\n    dispatch(loadPluginsAction())\n  }, [])\n\n  useEffect(() => {\n    dispatch(fetchConnectedInstanceAction(connectionInstanceId))\n    dispatch(getDatabaseConfigInfoAction(connectionInstanceId))\n    dispatch(fetchConnectedInstanceInfoAction(connectionInstanceId))\n    dispatch(fetchRecommendationsAction(connectionInstanceId))\n    let intervalId: ReturnType<typeof setInterval>\n\n    if (shouldGetRecommendations) {\n      intervalId = setInterval(() => {\n        dispatch(fetchRecommendationsAction(connectionInstanceId))\n      }, defaultTimeoutToGetRecommendations)\n    }\n\n    if (contextInstanceId && contextInstanceId !== connectionInstanceId) {\n      // rerender children only if the same page from scratch to clear all component states\n      if (lastPageRef.current === getPageName(connectionInstanceId, pathname)) {\n        setIsShouldChildrenRerender(true)\n      }\n\n      dispatch(resetDatabaseContext())\n    }\n\n    dispatch(setAppContextConnectedInstanceId(connectionInstanceId))\n    dispatch(\n      setDbConfig(\n        localStorageService.get(\n          BrowserStorageItem.dbConfig + connectionInstanceId,\n        ),\n      ),\n    )\n\n    return () => {\n      intervalId && clearInterval(intervalId)\n    }\n  }, [connectionInstanceId])\n\n  useEffect(() => {\n    lastPageRef.current = getPageName(connectionInstanceId, pathname)\n  }, [pathname])\n\n  useEffect(() => {\n    if (isShouldChildrenRerender) {\n      dispatch(resetDatabaseContext())\n      setIsShouldChildrenRerender(false)\n    }\n  }, [isShouldChildrenRerender])\n\n  if (isShouldChildrenRerender) {\n    return null\n  }\n\n  return (\n    <InstancePageTemplate>\n      {!envDependent?.flag && connectivityError ? (\n        <InstanceConnectionLost />\n      ) : (\n        <InstancePageRouter routes={routes} />\n      )}\n    </InstancePageTemplate>\n  )\n}\n\nexport default InstancePage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/InstancePageRouter.spec.tsx",
    "content": "import React from 'react'\nimport { screen } from '@testing-library/react'\nimport reactRouterDom, { MemoryRouter, Route } from 'react-router-dom'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { PageNames, Pages } from 'uiSrc/constants'\nimport InstancePageRouter from './InstancePageRouter'\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n}))\n\nconst MockComponent = () => <div data-testid=\"mock-component\">Test match</div>\n\nconst mockedRoutes = [\n  {\n    path: '/redis-enterprise-autodiscovery',\n  },\n]\n\nconst mockComponentRoutes = [\n  {\n    pageName: PageNames.browser,\n    path: Pages.browser(':instanceId'),\n    component: MockComponent,\n  },\n]\n\ndescribe('InstancePageRouter', () => {\n  it('should render', () => {\n    expect(\n      render(<InstancePageRouter routes={mockedRoutes} />, {\n        withRouter: true,\n      }),\n    ).toBeTruthy()\n  })\n\n  it('should not render 404 when route matches', () => {\n    render(\n      <MemoryRouter initialEntries={[Pages.browser('123')]}>\n        <InstancePageRouter routes={mockComponentRoutes} />\n        <Route path=\"/not-found\">\n          <div>Not found</div>\n        </Route>\n      </MemoryRouter>,\n    )\n\n    expect(screen.queryByText('Not found')).not.toBeInTheDocument()\n    expect(screen.queryByTestId('mock-component')).toBeInTheDocument()\n  })\n\n  it('should redirect to the 404 page when unmatched route is used', async () => {\n    const pushMock = jest.fn()\n    jest\n      .spyOn(reactRouterDom, 'useHistory')\n      .mockReturnValue({ push: pushMock } as any)\n\n    render(\n      <MemoryRouter initialEntries={['/foo/bar']}>\n        <InstancePageRouter routes={mockComponentRoutes} />\n        <Route path=\"/not-found\">\n          <div>Not found</div>\n        </Route>\n      </MemoryRouter>,\n    )\n\n    expect(screen.queryByText('Not found')).toBeInTheDocument()\n    expect(screen.queryByTestId('mock-component')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/InstancePageRouter.tsx",
    "content": "import React from 'react'\nimport { Redirect, Route, Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { Pages } from 'uiSrc/constants'\n\nexport interface Props {\n  routes: any[]\n}\nconst InstancePageRouter = ({ routes }: Props) => (\n  <Switch>\n    {routes.map((route, i) => (\n      // eslint-disable-next-line react/no-array-index-key\n      <RouteWithSubRoutes key={i} {...route} />\n    ))}\n    <Route path=\"*\" render={() => <Redirect to={Pages.notFound} />} />\n  </Switch>\n)\n\nexport default React.memo(InstancePageRouter)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/index.ts",
    "content": "import InstancePage from './InstancePage'\nimport InstancePageRouter from './InstancePageRouter'\n\nexport { InstancePage, InstancePageRouter }\n\nexport default InstancePage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/instanceConnectionLost.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, screen, waitFor, within } from '@testing-library/react'\nimport { cloneDeep } from 'lodash'\nimport { http, HttpResponse } from 'msw'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport {\n  getMswURL,\n  initialStateDefault,\n  mockStore,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport InstanceConnectionLost from 'uiSrc/pages/instance/instanceConnectionLost'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { INSTANCES_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { store } from 'uiSrc/slices/store'\n\nlet mockedStore: ReturnType<typeof mockStore>\n\nbeforeEach(() => {\n  jest.resetAllMocks()\n\n  // Create fresh state for each test, inheriting all defaults\n  const initialState: typeof initialStateDefault = {\n    ...cloneDeep(initialStateDefault),\n    app: {\n      ...initialStateDefault.app,\n      connectivity: {\n        ...initialStateDefault.app.connectivity,\n        loading: false,\n        error: 'Test error',\n      },\n    },\n    connections: {\n      ...initialStateDefault.connections,\n      instances: {\n        ...initialStateDefault.connections.instances,\n        connectedInstance: INSTANCES_MOCK[0],\n      },\n    },\n  }\n\n  mockedStore = mockStore(initialState)\n})\n\ndescribe('instanceConnectionLost', () => {\n  test.each(['success', 'error'])(\n    'should handle retry and %s',\n    async (type) => {\n      // mock actual store.dispatch so the axios error interceptor calls our mocked store's dispatch\n      jest\n        .spyOn(store, 'dispatch')\n        .mockImplementation((action: unknown) =>\n          mockedStore.dispatch(action as any),\n        )\n\n      mswServer.use(\n        http.get(\n          getMswURL(`${ApiEndpoints.DATABASES}/instanceId/overview`),\n          async () => {\n            if (type === 'error') {\n              return HttpResponse.json(\n                { error: 'test', code: 'serviceUnavailable' },\n                { status: 503 },\n              )\n            }\n\n            return HttpResponse.json(INSTANCES_MOCK, { status: 200 })\n          },\n        ),\n      )\n\n      render(<InstanceConnectionLost />, {\n        store: mockedStore,\n      })\n\n      const errorElement = screen.getByTestId('connectivity-error-message')\n      expect(within(errorElement).getByText('Test error')).toBeInTheDocument()\n\n      // click retry\n      const retryButton = screen.getByRole('button', { name: 'Retry' })\n      fireEvent.click(retryButton)\n\n      // wait until the expected actions have beeen dispatched\n      await waitFor(() => {\n        if (type === 'error') {\n          expect(mockedStore.getActions()).toEqual([\n            { type: 'appConnectivity/setConnectivityLoading', payload: true },\n            { type: 'instances/getDatabaseConfigInfo', payload: undefined },\n            {\n              type: 'appConnectivity/setConnectivityError',\n              payload: 'The connection to the server has been lost.', // set by the Axios error interceptor\n            },\n            {\n              type: 'instances/getDatabaseConfigInfoFailure',\n              payload: undefined,\n            },\n            { type: 'appConnectivity/setConnectivityLoading', payload: false },\n          ])\n        } else {\n          expect(mockedStore.getActions()).toEqual([\n            {\n              payload: true,\n              type: 'appConnectivity/setConnectivityLoading',\n            },\n            {\n              type: 'instances/getDatabaseConfigInfo',\n            },\n            {\n              payload: expect.anything(),\n              type: 'instances/getDatabaseConfigInfoSuccess',\n            },\n            {\n              payload: null,\n              type: 'appConnectivity/setConnectivityError',\n            },\n            {\n              payload: false,\n              type: 'appConnectivity/setConnectivityLoading',\n            },\n          ])\n        }\n      })\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/instanceConnectionLost.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { appConnectivity, retryConnection } from 'uiSrc/slices/app/connectivity'\nimport ConnectivityError from 'uiSrc/components/connectivity-error/ConnectivityError'\n\nconst InstanceConnectionLost = () => {\n  const dispatch = useDispatch()\n  const { instanceId: connectionInstanceId } = useParams<{\n    instanceId: string\n  }>()\n  const { error, loading: isLoading } = useSelector(appConnectivity)\n\n  const onRetry = () => {\n    dispatch(retryConnection(connectionInstanceId))\n  }\n\n  return (\n    <ConnectivityError isLoading={isLoading} error={error} onRetry={onRetry} />\n  )\n}\n\nexport default InstanceConnectionLost\n"
  },
  {
    "path": "redisinsight/ui/src/pages/instance/styles.module.scss",
    "content": ".mainComponent {\n  height: calc(100% - 26px) !important;\n}\n\n.resizableButton {\n  display: none;\n  margin: -8px 16px -8px !important;\n  z-index: 10;\n  background-color: var(--euiPageBackgroundColor);\n}\n\n:global(.show-cli) {\n  .resizableButton {\n    display: block;\n    z-index: 10;\n  }\n\n  .panelTop {\n    padding-bottom: 8px;\n  }\n\n  .panelBottom {\n    padding-top: 8px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/not-found-error/NotFoundErrorPage.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, screen } from '@testing-library/react'\nimport { cloneDeep, set } from 'lodash'\nimport reactRouterDom from 'react-router-dom'\nimport {\n  initialStateDefault,\n  mockStore,\n  mockWindowLocation,\n  render,\n} from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport NotFoundErrorPage from 'uiSrc/pages/not-found-error/NotFoundErrorPage'\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\njest.mock('uiSrc/config', () => ({\n  ...jest.requireActual('uiSrc/config'),\n  getConfig: () => {\n    const actualConfig = jest.requireActual('uiSrc/config')\n    const config = actualConfig.getConfig()\n    return {\n      ...config,\n      app: {\n        activityMonitorOrigin: 'http://foo.bar',\n      },\n    }\n  },\n}))\n\nbeforeEach(() => {\n  jest.resetAllMocks()\n  mockWindowLocation()\n})\n\ndescribe('NotFoundErrorPage', () => {\n  it('should render the correct button when envDependent feature is on', async () => {\n    const pushMock = jest.fn()\n    jest\n      .spyOn(reactRouterDom, 'useHistory')\n      .mockReturnValue({ push: pushMock } as any)\n\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: true },\n    )\n\n    render(<NotFoundErrorPage />, {\n      store: mockStore(initialStoreState),\n    })\n\n    const dbListButton = screen.getByTestId('not-found-db-list-button')\n    fireEvent.click(dbListButton)\n\n    expect(pushMock).toHaveBeenCalledTimes(1)\n    expect(pushMock).toHaveBeenCalledWith('/')\n  })\n\n  it('should render the correct button when envDependent feature is off', () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.envDependent}`,\n      { flag: false },\n    )\n\n    render(<NotFoundErrorPage />, {\n      store: mockStore(initialStoreState),\n    })\n\n    const dbListButton = screen.getByTestId('not-found-db-list-button')\n    fireEvent.click(dbListButton)\n\n    expect(window.location.href).toBe('http://foo.bar/#/databases')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/not-found-error/NotFoundErrorPage.tsx",
    "content": "import React, { useCallback } from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { getConfig } from 'uiSrc/config'\nimport Robot from 'uiSrc/assets/img/robot.svg?react'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\nconst NotFoundErrorPage = () => {\n  const history = useHistory()\n  const config = getConfig()\n  const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const onDbButtonClick = useCallback(() => {\n    if (envDependentFeature?.flag) {\n      history.push('/')\n    } else {\n      window.location.href = `${config.app.activityMonitorOrigin}/#/databases`\n    }\n  }, [envDependentFeature, config])\n\n  return (\n    <div className={styles.notfoundpage}>\n      <Col align=\"start\" className={styles.notfoundgroup}>\n        <FlexItem grow>\n          <Col align=\"start\" gap=\"xl\">\n            <FlexItem grow>\n              <RiIcon\n                className={styles.logoIcon}\n                size=\"original\"\n                type=\"RedisLogoFullIcon\"\n              />\n            </FlexItem>\n            <FlexItem grow>\n              <Title size=\"XXL\">\n                Whoops!\n                <br />\n                This Page Is an Empty Set\n              </Title>\n              <Text component=\"div\">\n                <p\n                  className={styles.errorSubtext}\n                  style={{ marginBottom: '.8rem' }}\n                >\n                  We searched every shard, <br />\n                  But couldn&apos;t find the page you&apos;re after.\n                </p>\n                <PrimaryButton\n                  size=\"s\"\n                  onClick={onDbButtonClick}\n                  data-testid=\"not-found-db-list-button\"\n                >\n                  Databases page\n                </PrimaryButton>\n              </Text>\n            </FlexItem>\n          </Col>\n        </FlexItem>\n      </Col>\n      <div className={styles.robotHolder}>\n        <Robot className={styles.robot} />\n      </div>\n    </div>\n  )\n}\n\nexport default NotFoundErrorPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/not-found-error/styles.module.scss",
    "content": ".notfoundpage {\n  position: relative;\n  height: 100vh;\n  width: 100%;\n}\n\n.notfoundgroup {\n  position: absolute;\n  top: 80px;\n  left: 80px;\n  z-index: 1;\n}\n\n.notfoundpage h1 {\n  font-weight: bold;\n}\n\n.errorSubtext {\n  margin-top: 1.4rem;\n}\n\n.logoIcon {\n  width: 128px;\n  height: 100%;\n}\n\n.robotHolder {\n  position: absolute;\n  bottom: 0;\n  right: 100px;\n  z-index: 0;\n  height: calc(100vh - 80px);\n  width: auto;\n}\n\n@media (min-width: 1500px) {\n  .robotHolder {\n    right: 15%;\n  }\n}\n\n.robot {\n  height: 100%;\n  width: auto;\n}\n\n@media (max-width: 1000px) {\n  .robotHolder {\n    height: max(calc(100vh - 200px), 300px);\n  }\n}\n\n@media (max-width: 800px) {\n  .robotHolder {\n    height: max(calc(100vh - 300px), 300px);\n  }\n}\n\n@media (max-width: 600px) {\n  .robotHolder {\n    height: max(calc(100vh - 400px), 300px);\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/PubSubPage.spec.tsx",
    "content": "import React from 'react'\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport PubSubPage from './PubSubPage'\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    name: 'db_name',\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n  sendPageViewTelemetry: jest.fn(),\n}))\n\n/**\n * PubSubPage tests\n *\n * @group component\n */\ndescribe('PubSubPage', () => {\n  it('should render', () => {\n    expect(render(<PubSubPage />)).toBeTruthy()\n  })\n\n  it('should call proper sendPageViewTelemetry', () => {\n    const sendPageViewTelemetryMock = jest.fn()\n    sendPageViewTelemetry.mockImplementation(() => sendPageViewTelemetryMock)\n\n    render(<PubSubPage />)\n\n    expect(sendPageViewTelemetry).toBeCalledWith({\n      name: TelemetryPageView.PUBSUB_PAGE,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/PubSubPage.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const OnboardingWrapper = styled(Col)`\n  align-items: flex-end;\n  /* Custom margin for onboarding popover */\n  /* TODO: Rework the positioning of the onboarding container in order to remove this */\n  margin-right: 28px;\n`\n\nexport const MessagesListWrapper = styled(FlexItem)`\n  overflow-y: auto;\n  scrollbar-width: thin;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/PubSubPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport styled from 'styled-components'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n  TelemetryEvent,\n  TelemetryPageView,\n} from 'uiSrc/telemetry'\nimport { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils'\n\nimport { OnboardingTour } from 'uiSrc/components'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\nimport { incrementOnboardStepAction } from 'uiSrc/slices/app/features'\nimport { OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { MessagesListTable, PublishMessage } from './components'\n\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { MessagesListWrapper, OnboardingWrapper } from './PubSubPage.styles'\n\nconst FooterPanel = styled(FlexItem)`\n  border-top: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n`\n\nconst PubSubPage = () => {\n  const { name: connectedInstanceName, db } = useSelector(\n    connectedInstanceSelector,\n  )\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const [isPageViewSent, setIsPageViewSent] = useState(false)\n\n  const dispatch = useDispatch()\n\n  const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}`\n  setTitle(`${dbName} - Pub/Sub`)\n\n  useEffect(\n    () => () => {\n      // as here is the last step of onboarding, we set next step when move from the page\n      // remove it when triggers&functions won't be the last page\n      dispatch(\n        incrementOnboardStepAction(OnboardingSteps.Finish, 0, () => {\n          sendEventTelemetry({\n            event: TelemetryEvent.ONBOARDING_TOUR_FINISHED,\n            eventData: {\n              databaseId: instanceId,\n            },\n          })\n        }),\n      )\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (connectedInstanceName && !isPageViewSent) {\n      sendPageView(instanceId)\n    }\n  }, [connectedInstanceName, isPageViewSent])\n\n  const sendPageView = (instanceId: string) => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.PUBSUB_PAGE,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  return (\n    <Col data-testid=\"pub-sub-page\" justify=\"between\">\n      <MessagesListWrapper grow={true}>\n        <MessagesListTable />\n      </MessagesListWrapper>\n\n      <FooterPanel grow={false}>\n        <PublishMessage />\n      </FooterPanel>\n\n      <OnboardingWrapper grow={false}>\n        <OnboardingTour\n          options={ONBOARDING_FEATURES.FINISH}\n          anchorPosition=\"downRight\"\n        >\n          <span />\n        </OnboardingTour>\n      </OnboardingWrapper>\n    </Col>\n  )\n}\n\nexport default PubSubPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/index.ts",
    "content": "import MessagesListTable from './messages-list'\nimport PublishMessage from './publish-message'\n\nexport { MessagesListTable, PublishMessage }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.spec.tsx",
    "content": "import React from 'react'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport EmptyMessagesList from './EmptyMessagesList'\n\ndescribe('EmptyMessagesList', () => {\n  it('renders base layout and copy', () => {\n    render(<EmptyMessagesList isSpublishNotSupported />)\n\n    expect(screen.getByTestId('empty-messages-list')).toBeInTheDocument()\n\n    expect(screen.getByText('You are not subscribed')).toBeInTheDocument()\n    expect(\n      screen.getByText(\n        /Subscribe to the Channel to see all the messages published to your database/i,\n      ),\n    ).toBeInTheDocument()\n\n    expect(\n      screen.getByText(\n        /Running in production may decrease performance and memory available\\./i,\n      ),\n    ).toBeInTheDocument()\n  })\n\n  it('shows cluster banner only when Cluster AND isSpublishNotSupported=true', () => {\n    // visible when both conditions true\n    const { rerender } = render(\n      <EmptyMessagesList\n        connectionType={ConnectionType.Cluster}\n        isSpublishNotSupported\n      />,\n    )\n    const banner = screen.getByTestId('empty-messages-list-cluster')\n    expect(banner).toBeInTheDocument()\n    expect(\n      screen.getByText(\n        /Messages published with SPUBLISH will not appear in this channel/i,\n      ),\n    ).toBeInTheDocument()\n\n    // hide when flag is false\n    rerender(\n      <EmptyMessagesList\n        connectionType={ConnectionType.Cluster}\n        isSpublishNotSupported={false}\n      />,\n    )\n    expect(\n      screen.queryByTestId('empty-messages-list-cluster'),\n    ).not.toBeInTheDocument()\n\n    // hide when connection is not Cluster\n    rerender(\n      <EmptyMessagesList\n        connectionType={ConnectionType.Standalone}\n        isSpublishNotSupported\n      />,\n    )\n    expect(\n      screen.queryByTestId('empty-messages-list-cluster'),\n    ).not.toBeInTheDocument()\n\n    // also hide when connectionType is undefined\n    rerender(<EmptyMessagesList isSpublishNotSupported />)\n    expect(\n      screen.queryByTestId('empty-messages-list-cluster'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const HeroImage = styled(RiImage)`\n  user-select: none;\n  pointer-events: none;\n`\n\nexport const InnerContainer = styled(Col)`\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral300};\n  border-radius: ${({ theme }) => theme.core.space.space100};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  padding: ${({ theme }) => theme.core.space.space300};\n  height: 100%;\n`\n\nexport const Wrapper = styled(FlexItem)`\n  margin: ${({ theme }) =>\n    `${theme.core.space.space050} ${theme.core.space.space200}`};\n  height: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/EmptyMessagesList.tsx",
    "content": "import React from 'react'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Banner } from 'uiSrc/components/base/display'\nimport { CallOut } from 'uiSrc/components/base/display/call-out/CallOut'\nimport LightBulbImage from 'uiSrc/assets/img/pub-sub/light-bulb.svg'\n\nimport SubscribeForm from '../../subscribe-form'\nimport { HeroImage, InnerContainer, Wrapper } from './EmptyMessagesList.styles'\n\nexport interface Props {\n  connectionType?: ConnectionType\n  isSpublishNotSupported: boolean\n}\n\nconst EmptyMessagesList = ({\n  connectionType,\n  isSpublishNotSupported,\n}: Props) => (\n  <Wrapper>\n    <InnerContainer\n      align=\"center\"\n      justify=\"center\"\n      data-testid=\"empty-messages-list\"\n      gap=\"xxl\"\n    >\n      <HeroImage src={LightBulbImage} alt=\"Pub/Sub\" />\n\n      <Col align=\"center\" justify=\"center\" grow={false} gap=\"s\">\n        <Title size=\"XXL\">You are not subscribed</Title>\n\n        <Text>\n          Subscribe to the Channel to see all the messages published to your\n          database\n        </Text>\n      </Col>\n\n      <SubscribeForm grow={false} />\n\n      <CallOut variant=\"attention\">\n        Running in production may decrease performance and memory available.\n      </CallOut>\n\n      {connectionType === ConnectionType.Cluster && isSpublishNotSupported && (\n        <>\n          <Banner\n            data-testid=\"empty-messages-list-cluster\"\n            variant=\"attention\"\n            showIcon={true}\n            message=\"Messages published with SPUBLISH will not appear in this channel\"\n          />\n        </>\n      )}\n    </InnerContainer>\n  </Wrapper>\n)\n\nexport default EmptyMessagesList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/EmptyMessagesList/index.ts",
    "content": "import EmptyMessagesList from './EmptyMessagesList'\n\nexport default EmptyMessagesList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.config.tsx",
    "content": "import { ColumnDef, PaginationState } from 'uiSrc/components/base/layout/table'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { TableStorageKey } from 'uiSrc/constants/storage'\nimport { setObjectStorageField, getObjectStorageField } from 'uiSrc/services'\nimport { IMessage } from 'apiSrc/modules/pub-sub/interfaces/message.interface'\nimport {\n  PUB_SUB_TABLE_COLUMN_FIELD_NAME_MAP,\n  PubSubTableColumn,\n} from './MessagesListTable.constants'\nimport MessagesListTableCellTimestamp from './components/MessagesListTableCellTimestamp'\nimport MessagesListTableCellMessage from './components/MessagesListTableCellMessage'\n\nexport const PUB_SUB_TABLE_COLUMNS: ColumnDef<IMessage>[] = [\n  {\n    id: PubSubTableColumn.Timestamp,\n    accessorKey: PubSubTableColumn.Timestamp,\n    header: PUB_SUB_TABLE_COLUMN_FIELD_NAME_MAP.get(\n      PubSubTableColumn.Timestamp,\n    ),\n    size: 30,\n    enableSorting: true,\n    cell: MessagesListTableCellTimestamp,\n  },\n  {\n    id: PubSubTableColumn.Channel,\n    accessorKey: PubSubTableColumn.Channel,\n    header: PUB_SUB_TABLE_COLUMN_FIELD_NAME_MAP.get(PubSubTableColumn.Channel),\n    size: 40,\n  },\n  {\n    id: PubSubTableColumn.Message,\n    accessorKey: PubSubTableColumn.Message,\n    header: PUB_SUB_TABLE_COLUMN_FIELD_NAME_MAP.get(PubSubTableColumn.Message),\n    cell: MessagesListTableCellMessage,\n  },\n]\n\nexport const handlePaginationChange = (paginationState: PaginationState) =>\n  setObjectStorageField(\n    BrowserStorageItem.tablePaginationState,\n    TableStorageKey.pubSubList,\n    paginationState,\n  )\n\nexport const getDefaultPagination = () =>\n  getObjectStorageField(\n    BrowserStorageItem.tablePaginationState,\n    TableStorageKey.pubSubList,\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.constants.ts",
    "content": "export enum PubSubTableColumn {\n  Timestamp = 'time',\n  Channel = 'channel',\n  Message = 'message',\n}\n\nexport const PUB_SUB_TABLE_COLUMN_FIELD_NAME_MAP = new Map<\n  PubSubTableColumn,\n  string\n>([\n  [PubSubTableColumn.Timestamp, 'Timestamp'],\n  [PubSubTableColumn.Channel, 'Channel'],\n  [PubSubTableColumn.Message, 'Message'],\n])\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { pubSubSelector as pubSubSelectorMock } from 'uiSrc/slices/pubsub/pubsub'\nimport MessagesListTable from './MessagesListTable'\n\njest.mock('uiSrc/slices/pubsub/pubsub', () => ({\n  ...jest.requireActual('uiSrc/slices/pubsub/pubsub'),\n  pubSubSelector: jest.fn().mockReturnValue({\n    isSubscribed: false,\n    messages: [],\n  }),\n}))\n\nconst pubSubSelector = pubSubSelectorMock as jest.Mock\n\nafterEach(() => {\n  jest.clearAllMocks()\n})\n\ndescribe('MessagesListTable', () => {\n  it('should render EmptyMessagesList by default', () => {\n    const { queryByTestId } = render(<MessagesListTable />)\n\n    expect(queryByTestId('messages-list')).not.toBeInTheDocument()\n    expect(queryByTestId('empty-messages-list')).toBeInTheDocument()\n  })\n\n  it('should render empty MessagesList if client is subscribed and no messages', () => {\n    pubSubSelector.mockReturnValue({\n      isSubscribed: true,\n      messages: [],\n    })\n\n    const { queryByTestId } = render(<MessagesListTable />)\n\n    expect(queryByTestId('messages-list')).toBeInTheDocument()\n    expect(queryByTestId('empty-messages-list')).not.toBeInTheDocument()\n    expect(screen.queryByText('No messages published yet')).toBeInTheDocument()\n  })\n\n  it('should render messages if there are some', () => {\n    pubSubSelector.mockReturnValue({\n      messages: [{ time: 123, channel: 'channel', message: 'msg' }],\n    })\n\n    render(<MessagesListTable />)\n\n    expect(screen.queryByTestId('messages-list')).toBeInTheDocument()\n    expect(screen.queryByTestId('empty-messages-list')).not.toBeInTheDocument()\n    expect(screen.queryByText('msg')).toBeInTheDocument()\n  })\n\n  it('should render messages if there are some no matter if client is subscribed', () => {\n    pubSubSelector.mockReturnValue({\n      messages: [{ time: 123, channel: 'channel', message: 'msg' }],\n      isSubscribed: false,\n    })\n\n    render(<MessagesListTable />)\n    expect(screen.queryByText('msg')).toBeInTheDocument()\n  })\n\n  it('should render header with count and \"Subscribed\" badge when subscribed and no messages', () => {\n    pubSubSelector.mockReturnValue({ isSubscribed: true, messages: [] })\n\n    render(<MessagesListTable />)\n\n    expect(screen.getByText('Messages:')).toBeInTheDocument()\n    expect(screen.getByText('0')).toBeInTheDocument()\n    expect(screen.getByText('Status:')).toBeInTheDocument()\n    expect(screen.getByText('Subscribed')).toBeInTheDocument()\n\n    expect(screen.getByText('Timestamp')).toBeInTheDocument()\n    expect(screen.getByText('Channel')).toBeInTheDocument()\n    expect(screen.getByText('Message')).toBeInTheDocument()\n\n    expect(screen.getByText('No messages published yet')).toBeInTheDocument()\n  })\n\n  it('should render header with count and \"Unsubscribed\" badge when messages exist but not subscribed', () => {\n    const items = [\n      { time: 123, channel: 'a', message: 'x' },\n      { time: 456, channel: 'b', message: 'y' },\n    ]\n    pubSubSelector.mockReturnValue({ isSubscribed: false, messages: items })\n\n    render(<MessagesListTable />)\n\n    expect(screen.getByText('Messages:')).toBeInTheDocument()\n    expect(screen.getByText(String(items.length))).toBeInTheDocument()\n    expect(screen.getByText('Status:')).toBeInTheDocument()\n    expect(screen.getByText('Unsubscribed')).toBeInTheDocument()\n\n    expect(screen.getByTestId('messages-list')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.stories.tsx",
    "content": "import React, { useEffect } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useDispatch } from 'react-redux'\n\nimport { PubSubMessage } from 'uiSrc/slices/interfaces/pubsub'\nimport {\n  concatPubSubMessages,\n  setInitialPubSubState,\n  setIsPubSubSubscribed,\n  toggleSubscribeTriggerPubSub,\n} from 'uiSrc/slices/pubsub/pubsub'\nimport { PubSubMessageFactory } from 'uiSrc/mocks/factories/pubsub/PubSubMessage.factory'\nimport {\n  getDatabaseConfigInfoSuccess,\n  setConnectedInstanceSuccess,\n} from 'uiSrc/slices/instances/instances'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\nimport MessagesListTable from './MessagesListTable'\n\ninterface MessagesListTableArgs {\n  messages?: PubSubMessage[]\n  isSubscribed?: boolean\n  subscriptions?: string\n  connectionType?: ConnectionType\n  version?: string\n}\n\nconst SAMPLE_MESSAGES: PubSubMessage[] = PubSubMessageFactory.buildList(20)\n\nconst StorePopulator = ({ args }: { args: MessagesListTableArgs }) => {\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(setInitialPubSubState())\n\n    const instance = DBInstanceFactory.build({\n      connectionType: args.connectionType ?? ConnectionType.Standalone,\n      version: args.version ?? '7.2.0',\n    })\n\n    dispatch(setConnectedInstanceSuccess(instance))\n    dispatch(\n      getDatabaseConfigInfoSuccess({\n        version: args.version ?? '7.2.0',\n      } as any),\n    )\n\n    if (args.subscriptions) {\n      dispatch(toggleSubscribeTriggerPubSub(args.subscriptions))\n    }\n\n    if (args.isSubscribed) {\n      dispatch(setIsPubSubSubscribed())\n    }\n\n    if (args.messages?.length) {\n      dispatch(\n        concatPubSubMessages({\n          messages: args.messages,\n          count: args.messages.length,\n        }),\n      )\n    }\n  }, [dispatch, args])\n\n  return null\n}\n\nconst meta: Meta<typeof MessagesListTable> = {\n  component: MessagesListTable,\n  decorators: [\n    (Story, context) => {\n      const storeArgs =\n        (context.parameters?.storeArgs as MessagesListTableArgs) || {}\n\n      return (\n        <>\n          <StorePopulator args={storeArgs} />\n          <Story />\n        </>\n      )\n    },\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const WithMessages: Story = {\n  parameters: {\n    storeArgs: {\n      isSubscribed: true,\n      subscriptions: 'news:*',\n      messages: SAMPLE_MESSAGES,\n    } as MessagesListTableArgs,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled(Col)`\n  margin: ${({ theme }) => theme.core.space.space200};\n  /* \n  TODO: Remove margin-top when <InstancePageTemplate />\n  don't apply custom padding to the page\n  */\n  margin-top: ${({ theme }) => theme.core.space.space100};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances'\nimport { pubSubSelector } from 'uiSrc/slices/pubsub/pubsub'\nimport { isVersionHigherOrEquals } from 'uiSrc/utils'\nimport { CommandsVersions } from 'uiSrc/constants/commandsVersions'\nimport { useConnectionType } from 'uiSrc/components/hooks/useConnectionType'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { HorizontalSpacer } from 'uiSrc/components/base/layout'\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport { Wrapper } from './MessagesListTable.styles'\nimport {\n  getDefaultPagination,\n  handlePaginationChange,\n  PUB_SUB_TABLE_COLUMNS,\n} from './MessagesListTable.config'\nimport { PubSubTableColumn } from './MessagesListTable.constants'\nimport PatternsInfo from '../../patternsInfo'\nimport SubscribeForm from '../../subscribe-form'\nimport EmptyMessagesList from '../EmptyMessagesList'\n\nconst MessagesListTable = () => {\n  const {\n    messages = [],\n    isSubscribed,\n    subscriptions,\n  } = useSelector(pubSubSelector)\n  const connectionType = useConnectionType()\n  const { version } = useSelector(connectedInstanceOverviewSelector)\n\n  const channels = subscriptions?.length\n    ? subscriptions.map((sub) => sub.channel).join(' ')\n    : DEFAULT_SEARCH_MATCH\n\n  const [isSpublishNotSupported, setIsSpublishNotSupported] =\n    useState<boolean>(true)\n\n  useEffect(() => {\n    setIsSpublishNotSupported(\n      isVersionHigherOrEquals(\n        version,\n        CommandsVersions.SPUBLISH_NOT_SUPPORTED.since,\n      ),\n    )\n  }, [version])\n\n  const hasMessages = messages.length > 0\n\n  if (hasMessages || isSubscribed) {\n    return (\n      <Wrapper gap=\"l\">\n        <Row align=\"center\" justify=\"between\" grow={false}>\n          <Row align=\"center\" gap=\"m\">\n            <PatternsInfo channels={channels} />\n\n            <Row align=\"center\" gap=\"s\">\n              <Text>Messages:</Text>\n              <Text data-testid=\"pub-sub-messages-count\">\n                {messages.length}\n              </Text>\n            </Row>\n          </Row>\n\n          <Row\n            align=\"center\"\n            justify=\"end\"\n            gap=\"s\"\n            data-testid=\"pub-sub-status\"\n          >\n            <Text>Status:</Text>\n            {isSubscribed ? (\n              <RiBadge label=\"Subscribed\" variant=\"success\" />\n            ) : (\n              <RiBadge label=\"Unsubscribed\" variant=\"default\" />\n            )}\n            <HorizontalSpacer size=\"s\" />\n\n            <SubscribeForm grow={false} />\n          </Row>\n        </Row>\n\n        <div data-testid=\"messages-list\">\n          <Table\n            columns={PUB_SUB_TABLE_COLUMNS}\n            data={messages}\n            stripedRows\n            enableSorting\n            paginationEnabled\n            defaultSorting={[{ id: PubSubTableColumn.Timestamp, desc: true }]}\n            onPaginationChange={handlePaginationChange}\n            defaultPagination={getDefaultPagination()}\n            emptyState=\"No messages published yet\"\n          />\n        </div>\n      </Wrapper>\n    )\n  }\n\n  return (\n    <EmptyMessagesList\n      isSpublishNotSupported={isSpublishNotSupported}\n      connectionType={connectionType}\n    />\n  )\n}\n\nexport default MessagesListTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/MessagesListTable.types.ts",
    "content": "import { ReactElement } from 'react'\nimport { CellContext } from 'uiSrc/components/base/layout/table'\nimport { IMessage } from 'apiSrc/modules/pub-sub/interfaces/message.interface'\n\nexport type IMessagesListTableCell = (\n  props: CellContext<IMessage, unknown>,\n) => ReactElement<any, any> | null\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/components/MessagesListTableCellMessage.tsx",
    "content": "import React from 'react'\n\nimport {\n  CopyTextContainer,\n  CellText,\n  CopyBtnWrapper,\n} from 'uiSrc/components/auto-discover'\nimport { RiTooltip } from 'uiSrc/components'\nimport { IMessagesListTableCell } from '../MessagesListTable.types'\n\nconst MessagesListTableCellMessage: IMessagesListTableCell = ({ row }) => {\n  const { message = '' } = row.original\n\n  return (\n    <CopyTextContainer>\n      <RiTooltip title=\"Message\" content={message}>\n        <CellText>{message}</CellText>\n      </RiTooltip>\n      <CopyBtnWrapper copy={message} aria-label=\"Copy message\" />\n    </CopyTextContainer>\n  )\n}\n\nexport default MessagesListTableCellMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/MessagesListTable/components/MessagesListTableCellTimestamp.tsx",
    "content": "import React from 'react'\n\nimport { FormatedDate } from 'uiSrc/components'\nimport { IMessagesListTableCell } from '../MessagesListTable.types'\n\nconst MessagesListTableCellTimestamp: IMessagesListTableCell = ({ row }) => {\n  const date = row.original.time * 1000\n\n  return <FormatedDate date={date} />\n}\n\nexport default MessagesListTableCellTimestamp\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/messages-list/index.ts",
    "content": "import MessagesListTable from './MessagesListTable/MessagesListTable'\n\nexport default MessagesListTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport PatternsInfo from './PatternsInfo'\n\ndescribe('PatternsInfo', () => {\n  it('should render', () => {\n    expect(render(<PatternsInfo />)).toBeTruthy()\n  })\n\n  it('should show info text on hover', async () => {\n    const content = 'hello'\n    render(<PatternsInfo channels={content} />)\n    expect(screen.getByText('Patterns: 1')).toBeInTheDocument()\n\n    fireEvent.focus(screen.getByTestId('append-info-icon'))\n    await waitFor(() => screen.getAllByText(content))\n    expect(screen.getAllByText(content)[0]).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nexport const InfoIcon = styled(RiIcon).attrs({\n  type: 'InfoIcon',\n  'data-testid': 'append-info-icon',\n})`\n  cursor: pointer;\n  // TODO: Remove margin-top\n  // Hack: for some reason this icon has extra height, which breaks flex alignment\n  margin-top: 4px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/patternsInfo/PatternsInfo.tsx",
    "content": "import React from 'react'\nimport { RiTooltip } from 'uiSrc/components'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { HorizontalSpacer } from 'uiSrc/components/base/layout'\nimport { InfoIcon } from './PatternsInfo.styles'\n\nexport interface PatternsInfoProps {\n  channels?: string\n}\n\nconst PatternsInfo = ({ channels }: PatternsInfoProps) => {\n  const getChannelsCount = () => {\n    if (!channels || channels?.trim() === DEFAULT_SEARCH_MATCH) return 'All'\n    return channels.trim().split(' ').length\n  }\n\n  return (\n    <Row grow={false} align=\"center\">\n      <Text data-testid=\"patterns-count\">\n        Patterns:&nbsp;{getChannelsCount()}\n      </Text>\n\n      <HorizontalSpacer size=\"s\" />\n\n      <RiTooltip\n        position=\"right\"\n        title={\n          <>\n            {channels\n              ?.trim()\n              .split(' ')\n              .map((ch) => <Text key={`${ch}`}>{ch}</Text>)}\n          </>\n        }\n      >\n        <InfoIcon />\n      </RiTooltip>\n    </Row>\n  )\n}\n\nexport default PatternsInfo\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/patternsInfo/index.ts",
    "content": "import PatternsInfo from './PatternsInfo'\n\nexport default PatternsInfo\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.spec.tsx",
    "content": "import React from 'react'\nimport { act, fireEvent, waitFor } from '@testing-library/react'\nimport { cloneDeep } from 'lodash'\n\nimport {\n  cleanup,\n  initialStateDefault,\n  mockStore,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport PublishMessage from './PublishMessage'\nimport { publishMessageAction } from 'uiSrc/slices/pubsub/pubsub'\nimport { setPubSubFieldsContext } from 'uiSrc/slices/app/context'\n\nlet mockedConnType = ConnectionType.Standalone\njest.mock('uiSrc/components/hooks/useConnectionType', () => ({\n  useConnectionType: () => mockedConnType,\n}))\n\njest.mock('uiSrc/slices/pubsub/pubsub', () => {\n  const actual = jest.requireActual('uiSrc/slices/pubsub/pubsub')\n  return {\n    ...actual,\n    publishMessageAction: jest.fn(\n      (instanceId, channel, message, onSuccess) => (dispatch: any) => {\n        const action = {\n          type: 'pubsub/publishMessageAction',\n          payload: [instanceId, channel, message, onSuccess],\n        }\n        dispatch(action)\n        return Promise.resolve()\n      },\n    ),\n  }\n})\n\njest.mock('uiSrc/slices/app/context', () => {\n  const actual = jest.requireActual('uiSrc/slices/app/context')\n  return {\n    ...actual,\n    appContextPubSub: (state: any) =>\n      state?.app?.context?.pubsub ?? { channel: '', message: '' },\n    setPubSubFieldsContext: jest.fn((fields: any) => (dispatch: any) => {\n      const action = {\n        type: 'app/setPubSubFieldsContext',\n        payload: fields,\n      }\n      dispatch(action)\n      return action\n    }),\n  }\n})\n\nlet store: typeof mockedStore\n\nconst createTestStateWithContext = (\n  contextOverrides = {},\n  pubsubOverrides = {},\n) => {\n  const state = cloneDeep(initialStateDefault)\n  state.app.context = {\n    ...state.app.context,\n    ...contextOverrides,\n  }\n  state.pubsub = {\n    loading: false,\n    publishing: false,\n    error: '',\n    subscriptions: [],\n    isSubscribeTriggered: false,\n    isConnected: false,\n    isSubscribed: false,\n    messages: [],\n    count: 0,\n    ...pubsubOverrides,\n  }\n  return state\n}\n\nconst renderPublishMessage = (contextOverrides = {}, pubsubOverrides = {}) => {\n  const initialStoreState = createTestStateWithContext(\n    contextOverrides,\n    pubsubOverrides,\n  )\n  return render(<PublishMessage />, {\n    store: mockStore(initialStoreState),\n  })\n}\n\nconst getChannelField = () => screen.getByTestId('field-channel-name')\nconst getMessageField = () => screen.getByTestId('field-message')\nconst getSubmitBtn = () => screen.getByTestId('publish-message-submit')\n\nbeforeEach(() => {\n  jest.useFakeTimers()\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  jest.mocked(publishMessageAction).mockClear()\n  jest.mocked(setPubSubFieldsContext).mockClear()\n  mockedConnType = ConnectionType.Standalone\n})\n\nafterEach(() => {\n  jest.runOnlyPendingTimers()\n  jest.useRealTimers()\n})\n\ndescribe('PublishMessage', () => {\n  it('should render basic form fields and button', () => {\n    expect(render(<PublishMessage />)).toBeTruthy()\n    expect(getChannelField()).toBeInTheDocument()\n    expect(getMessageField()).toBeInTheDocument()\n    expect(getSubmitBtn()).toBeInTheDocument()\n  })\n\n  it('should initialize channel/message from app context', async () => {\n    renderPublishMessage({\n      pubsub: { channel: 'orders', message: 'hello' },\n    })\n\n    await waitFor(() => {\n      expect(screen.getByDisplayValue('orders')).toBeInTheDocument()\n      expect(screen.getByDisplayValue('hello')).toBeInTheDocument()\n    })\n  })\n\n  it('should dispatche publish action with instanceId, channel, message, and a callback', () => {\n    render(<PublishMessage />, {\n      store: mockedStore,\n    })\n\n    fireEvent.change(getChannelField(), {\n      target: { value: 'news' },\n    })\n    fireEvent.change(getMessageField(), {\n      target: { value: 'ping' },\n    })\n\n    fireEvent.click(getSubmitBtn())\n\n    const actions = mockedStore.getActions()\n    expect(actions[0].type).toBe('pubsub/publishMessageAction')\n\n    const [iid, ch, msg, cb] = actions[0].payload\n    expect(iid).toBe('instanceId')\n    expect(ch).toBe('news')\n    expect(msg).toBe('ping')\n    expect(typeof cb).toBe('function')\n  })\n\n  it('should clear message, shows success badge with affected clients, hides button on success published message', async () => {\n    render(<PublishMessage />)\n\n    fireEvent.change(getChannelField(), {\n      target: { value: 'alpha' },\n    })\n    fireEvent.change(getMessageField(), {\n      target: { value: 'hello world' },\n    })\n    fireEvent.click(getSubmitBtn())\n\n    const [, , , onSuccess] = mockedStore.getActions()[0].payload\n    const affectedClients = 7\n    act(() => onSuccess(affectedClients))\n\n    await waitFor(() => {\n      expect(getMessageField()).toHaveValue('')\n      expect(\n        screen.queryByTestId('publish-message-submit'),\n      ).not.toBeInTheDocument()\n    })\n\n    expect(\n      screen.getByText(`Published (${affectedClients})`),\n    ).toBeInTheDocument()\n  })\n\n  it('should hide success badge client count (just \"Published\") when connection type is cluster', async () => {\n    mockedConnType = ConnectionType.Cluster\n    render(<PublishMessage />)\n\n    fireEvent.click(getSubmitBtn())\n    const [, , , onSuccess] = mockedStore.getActions()[0].payload\n    const affectedClients = 123\n    act(() => onSuccess(affectedClients))\n\n    await waitFor(() => {\n      expect(screen.getByText(/^Published$/)).toBeInTheDocument()\n    })\n    expect(\n      screen.queryByText(`Published (${affectedClients})`),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should auto-hide success badge after HIDE_BADGE_TIMER and shows submit button again', () => {\n    render(<PublishMessage />)\n\n    fireEvent.click(getSubmitBtn())\n    const [, , , onSuccess] = mockedStore.getActions()[0].payload\n    act(() => onSuccess(1))\n\n    expect(screen.getByText(/Published/)).toBeInTheDocument()\n\n    act(() => {\n      jest.advanceTimersByTime(3000)\n    })\n\n    expect(screen.queryByText(/Published/)).not.toBeInTheDocument()\n    expect(getSubmitBtn()).toBeInTheDocument()\n  })\n\n  it('should persist latest channel/message to context on unmount', () => {\n    const { unmount } = render(<PublishMessage />, {\n      store: mockedStore,\n    })\n\n    fireEvent.change(getChannelField(), {\n      target: { value: 'finalCh' },\n    })\n    fireEvent.change(getMessageField(), {\n      target: { value: 'finalMsg' },\n    })\n\n    unmount()\n\n    const actions = mockedStore.getActions()\n    const setCtx = actions.find(\n      (a: any) => a.type === 'app/setPubSubFieldsContext',\n    )\n    expect(setCtx).toBeTruthy()\n    expect(setCtx.payload).toEqual({ channel: 'finalCh', message: 'finalMsg' })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const ChannelColumn = styled(Col)`\n  // There are 2 columns next to each other.\n  // The channel one doesn't grow, but it should have a minimum width.\n  min-width: 250px;\n`\n\nexport const ButtonWrapper = styled(Row)`\n  min-width: 100px;\n`\n\nexport const ResultWrapper = styled(Row)`\n  min-height: 36px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/publish-message/PublishMessage.tsx",
    "content": "import React, { FormEvent, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport {\n  appContextPubSub,\n  setPubSubFieldsContext,\n} from 'uiSrc/slices/app/context'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport { publishMessageAction } from 'uiSrc/slices/pubsub/pubsub'\nimport { useConnectionType } from 'uiSrc/components/hooks/useConnectionType'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { ToastCheckIcon, Icon } from 'uiSrc/components/base/icons'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  ButtonWrapper,\n  ChannelColumn,\n  ResultWrapper,\n} from './PublishMessage.styles'\n\nconst HIDE_BADGE_TIMER = 3000\n\nconst PublishMessage = () => {\n  const { channel: channelContext, message: messageContext } =\n    useSelector(appContextPubSub)\n  const connectionType = useConnectionType()\n\n  const [channel, setChannel] = useState<string>(channelContext)\n  const [message, setMessage] = useState<string>(messageContext)\n  const [isShowBadge, setIsShowBadge] = useState<boolean>(false)\n  const [affectedClients, setAffectedClients] = useState<number>(0)\n\n  const fieldsRef = useRef({ channel, message })\n  const timeOutRef = useRef<NodeJS.Timeout>()\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  useEffect(\n    () => () => {\n      dispatch(setPubSubFieldsContext(fieldsRef.current))\n      timeOutRef.current && clearTimeout(timeOutRef.current)\n    },\n    [],\n  )\n\n  useEffect(() => {\n    fieldsRef.current = { channel, message }\n  }, [channel, message])\n\n  useEffect(() => {\n    if (isShowBadge) {\n      timeOutRef.current = setTimeout(() => {\n        isShowBadge && setIsShowBadge(false)\n      }, HIDE_BADGE_TIMER)\n\n      return\n    }\n\n    timeOutRef.current && clearTimeout(timeOutRef.current)\n  }, [isShowBadge])\n\n  const onSuccess = (affected: number) => {\n    setMessage('')\n    setAffectedClients(affected)\n    setIsShowBadge(true)\n  }\n\n  const onFormSubmit = (event: FormEvent<HTMLFormElement>): void => {\n    event.preventDefault()\n    setIsShowBadge(false)\n    dispatch(publishMessageAction(instanceId, channel, message, onSuccess))\n  }\n\n  const getClientsText = (clients?: number) =>\n    typeof clients !== 'number' ? 'Published' : `Published (${clients})`\n\n  return (\n    <form onSubmit={onFormSubmit}>\n      <Row justify=\"between\" gap=\"xl\" align=\"end\">\n        <Row grow={true} gap=\"m\">\n          <ChannelColumn grow={false} gap=\"s\">\n            <Text>Channel name</Text>\n            <FormField>\n              <TextInput\n                name=\"channel\"\n                id=\"channel\"\n                placeholder=\"Enter Channel Name\"\n                value={channel}\n                onChange={(value) => setChannel(value)}\n                autoComplete=\"off\"\n                data-testid=\"field-channel-name\"\n              />\n            </FormField>\n          </ChannelColumn>\n\n          <Col gap=\"s\">\n            <Text>Message</Text>\n            <TextInput\n              name=\"message\"\n              id=\"message\"\n              placeholder=\"Enter Message\"\n              value={message}\n              onChange={(value) => setMessage(value)}\n              autoComplete=\"off\"\n              data-testid=\"field-message\"\n            />\n          </Col>\n        </Row>\n\n        {isShowBadge && (\n          <ResultWrapper\n            grow={false}\n            align=\"center\"\n            data-testid=\"publish-result\"\n          >\n            <Icon icon={ToastCheckIcon} color=\"success500\" />\n            <Text color=\"success\">\n              {getClientsText(\n                connectionType !== ConnectionType.Cluster\n                  ? affectedClients\n                  : undefined,\n              )}\n            </Text>\n          </ResultWrapper>\n        )}\n\n        {!isShowBadge && (\n          <ButtonWrapper justify=\"end\" grow={false}>\n            <FlexItem>\n              <PrimaryButton\n                size=\"large\"\n                type=\"submit\"\n                data-testid=\"publish-message-submit\"\n              >\n                Publish\n              </PrimaryButton>\n            </FlexItem>\n          </ButtonWrapper>\n        )}\n      </Row>\n    </form>\n  )\n}\n\nexport default PublishMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/publish-message/index.ts",
    "content": "import PublishMessage from './PublishMessage'\n\nexport default PublishMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.spec.tsx",
    "content": "// SubscribeForm.spec.tsx\nimport React from 'react'\nimport { fireEvent, waitFor } from '@testing-library/react'\nimport { cloneDeep } from 'lodash'\nimport { toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub'\nimport {\n  cleanup,\n  clearStoreActions,\n  initialStateDefault,\n  mockStore,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport SubscribeForm from './SubscribeForm'\nimport { SubscriptionType } from 'apiSrc/modules/pub-sub/constants'\n\nlet store: typeof mockedStore\n\nconst createTestStateWithPubSub = (pubsubOverrides = {}) => {\n  const state = cloneDeep(initialStateDefault)\n  state.pubsub = {\n    isSubscribed: false,\n    loading: false,\n    publishing: false,\n    error: '',\n    isSubscribeTriggered: false,\n    isConnected: false,\n    subscriptions: [],\n    messages: [],\n    count: 0,\n    ...pubsubOverrides,\n  }\n  return state\n}\n\nconst renderSubscribeForm = (pubsubOverrides = {}) => {\n  const initialStoreState = createTestStateWithPubSub(pubsubOverrides)\n  return render(<SubscribeForm />, {\n    store: mockStore(initialStoreState),\n  })\n}\n\nconst getChannelsInput = () => screen.getByTestId('channels-input')\nconst getSubscribeBtn = () => screen.getByTestId('subscribe-btn')\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('SubscribeForm', () => {\n  it('should initialize channels from subscriptions', async () => {\n    renderSubscribeForm({\n      subscriptions: [\n        { channel: 'a.*', type: SubscriptionType.PSubscribe },\n        { channel: 'b.c', type: SubscriptionType.PSubscribe },\n      ],\n    })\n\n    await waitFor(() =>\n      expect(screen.getByDisplayValue('a.* b.c')).toBeInTheDocument(),\n    )\n  })\n\n  it('should use default \"*\" when no subscriptions', async () => {\n    renderSubscribeForm({\n      subscriptions: [],\n    })\n\n    await waitFor(() =>\n      expect(screen.getByDisplayValue('*')).toBeInTheDocument(),\n    )\n  })\n\n  it('should restore default \"*\" on blur when empty', async () => {\n    render(<SubscribeForm />)\n\n    fireEvent.change(getChannelsInput(), {\n      target: { value: '' },\n    })\n    fireEvent.blur(getChannelsInput())\n\n    await waitFor(() =>\n      expect(screen.getByDisplayValue('*')).toBeInTheDocument(),\n    )\n  })\n\n  it('should update channels as user types', () => {\n    render(<SubscribeForm />)\n    fireEvent.change(getChannelsInput(), {\n      target: { value: 'alpha beta.*' },\n    })\n    expect(screen.getByDisplayValue('alpha beta.*')).toBeInTheDocument()\n  })\n\n  it('should dispatch toggleSubscribe with current channels value', () => {\n    render(<SubscribeForm />)\n\n    fireEvent.change(getChannelsInput(), {\n      target: { value: 'news.* logs.error' },\n    })\n    fireEvent.click(getSubscribeBtn())\n\n    expect(clearStoreActions(mockedStore.getActions())).toEqual(\n      clearStoreActions([toggleSubscribeTriggerPubSub('news.* logs.error')]),\n    )\n  })\n\n  it('should disable input when subscribed and shows \"Unsubscribe\" label', () => {\n    renderSubscribeForm({\n      isSubscribed: true,\n    })\n\n    expect(getChannelsInput()).toBeDisabled()\n    expect(getSubscribeBtn()).toHaveTextContent('Unsubscribe')\n  })\n\n  it('should show \"Subscribe\" label when not subscribed', () => {\n    renderSubscribeForm({\n      isSubscribed: false,\n    })\n    expect(getSubscribeBtn()).toHaveTextContent('Subscribe')\n  })\n\n  it('should disable subscribe button when loading', () => {\n    renderSubscribeForm({\n      loading: true,\n    })\n    expect(getSubscribeBtn()).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { TextInput } from 'uiSrc/components/base/inputs'\n\nexport const TopicNameField = styled(TextInput)`\n  min-width: 250px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport {\n  DeleteIcon,\n  UserIcon,\n  IndicatorExcludedIcon,\n} from 'uiSrc/components/base/icons'\nimport {\n  clearPubSubMessages,\n  pubSubSelector,\n  toggleSubscribeTriggerPubSub,\n} from 'uiSrc/slices/pubsub/pubsub'\n\nimport { Button, IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { RiTooltip } from 'uiSrc/components'\nimport type { SubscribeFormProps } from './SubscribeForm.types'\nimport * as S from './SubscribeForm.styles'\nimport SubscribeInformation from '../subscribe-information'\n\nconst SubscribeForm = (props: SubscribeFormProps) => {\n  const dispatch = useDispatch()\n\n  const { isSubscribed, subscriptions, loading, count } =\n    useSelector(pubSubSelector)\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n\n  const [channels, setChannels] = useState(() =>\n    subscriptions?.length\n      ? subscriptions.map((sub) => sub.channel).join(' ')\n      : DEFAULT_SEARCH_MATCH,\n  )\n\n  const onFocusOut = () => {\n    if (!channels) {\n      setChannels(DEFAULT_SEARCH_MATCH)\n    }\n  }\n\n  const toggleSubscribe = () => {\n    dispatch(toggleSubscribeTriggerPubSub(channels))\n  }\n\n  const onClickClear = () => {\n    dispatch(clearPubSubMessages())\n    return sendEventTelemetry({\n      event: TelemetryEvent.PUBSUB_MESSAGES_CLEARED,\n      eventData: {\n        databaseId: instanceId,\n        messages: count,\n      },\n    })\n  }\n\n  return (\n    <Row align=\"center\" gap=\"m\" {...props}>\n      <FormField>\n        <S.TopicNameField\n          value={channels}\n          disabled={isSubscribed}\n          onChange={(value) => setChannels(value)}\n          onBlur={onFocusOut}\n          placeholder=\"Enter Pattern\"\n          aria-label=\"channel names for filtering\"\n          data-testid=\"channels-input\"\n        />\n      </FormField>\n\n      <SubscribeInformation />\n\n      <Button\n        variant={isSubscribed ? 'secondary-ghost' : 'primary'}\n        size=\"large\"\n        icon={isSubscribed ? IndicatorExcludedIcon : UserIcon}\n        data-testid=\"subscribe-btn\"\n        onClick={toggleSubscribe}\n        disabled={loading}\n      >\n        {isSubscribed ? 'Unsubscribe' : 'Subscribe'}\n      </Button>\n      <RiTooltip content={!!count ? 'Clear Messages' : ''}>\n        <IconButton\n          disabled={!count}\n          icon={DeleteIcon}\n          onClick={onClickClear}\n          aria-label=\"clear pub sub\"\n          data-testid=\"clear-pubsub-btn\"\n        />\n      </RiTooltip>\n    </Row>\n  )\n}\n\nexport default SubscribeForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-form/SubscribeForm.types.ts",
    "content": "import { type FlexProps } from 'uiSrc/components/base/layout/flex/flex.styles'\n\nexport interface SubscribeFormProps extends Omit<FlexProps, 'direction'> {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-form/index.ts",
    "content": "import SubscribeForm from './SubscribeForm'\n\nexport default SubscribeForm\nexport type { SubscribeFormProps } from './SubscribeForm.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.spec.tsx",
    "content": "import React from 'react'\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport SubscribeInformation from './SubscribeInformation'\n\ndescribe('SubscribeInformation', () => {\n  it('should render', () => {\n    expect(render(<SubscribeInformation />)).toBeTruthy()\n  })\n\n  it('should show tooltip on hover', async () => {\n    render(<SubscribeInformation />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('append-info-icon'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(\n      screen.getAllByText(/Subscribe to one or more channels/)[0],\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nexport const InfoIcon = styled(RiIcon).attrs({\n  type: 'InfoIcon',\n  size: 'l',\n  'data-testid': 'append-info-icon',\n})`\n  cursor: pointer;\n  // TODO: Remove margin-top\n  // Hack: for some reason this icon has extra height, which breaks flex alignment\n  margin-top: 4px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-information/SubscribeInformation.tsx",
    "content": "import React from 'react'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  EXTERNAL_LINKS,\n  UTM_CAMPAINGS,\n  UTM_MEDIUMS,\n} from 'uiSrc/constants/links'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { InfoIcon } from './SubscribeInformation.styles'\n\nconst SubscribeInformation = () => (\n  <RiTooltip\n    interactive\n    data-testid=\"pub-sub-examples\"\n    content={\n      <Col gap=\"l\">\n        <Text>\n          Subscribe to one or more channels or patterns by entering them,\n          separated by spaces.\n        </Text>\n\n        <Text>\n          Supported glob-style patterns are described&nbsp;\n          <Link\n            variant=\"inline\"\n            target=\"_blank\"\n            href={getUtmExternalLink(EXTERNAL_LINKS.pubSub, {\n              medium: UTM_MEDIUMS.Main,\n              campaign: UTM_CAMPAINGS.PubSub,\n            })}\n          >\n            here.\n          </Link>\n        </Text>\n      </Col>\n    }\n  >\n    <InfoIcon />\n  </RiTooltip>\n)\n\nexport default SubscribeInformation\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/components/subscribe-information/index.ts",
    "content": "import SubscribeInformation from './SubscribeInformation'\n\nexport default SubscribeInformation\n"
  },
  {
    "path": "redisinsight/ui/src/pages/pub-sub/index.ts",
    "content": "import PubSubPage from './PubSubPage'\n\nexport default PubSubPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/components/confirmation-popover/ConfirmationPopover.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport ConfirmationPopover from './ConfirmationPopover'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockedProps = {\n  title: 'title',\n  body: <div>body</div>,\n  onConfirm: jest.fn(),\n  onCancel: jest.fn(),\n  submitBtn: (\n    <button type=\"button\" data-testid=\"confirm-btn\">\n      Submit button\n    </button>\n  ),\n  button: (\n    <button type=\"button\" data-testid=\"button\">\n      Button\n    </button>\n  ),\n  onButtonClick: jest.fn(),\n}\n\ndescribe('ConfirmationPopover', () => {\n  it('should render', () => {\n    expect(render(<ConfirmationPopover {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should open confirmation message', async () => {\n    render(<ConfirmationPopover {...mockedProps} />)\n\n    expect(screen.queryByTestId('confirm-btn')).not.toBeInTheDocument()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('button'))\n    })\n\n    expect(screen.queryByTestId('confirm-btn')).toBeInTheDocument()\n  })\n\n  it('should call proper actions', async () => {\n    render(<ConfirmationPopover {...mockedProps} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('button'))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('confirm-btn'))\n    })\n\n    expect(mockedProps.onConfirm).toHaveBeenCalled()\n  })\n\n  it('should close confirmation message', async () => {\n    render(<ConfirmationPopover {...mockedProps} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('button'))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('confirm-btn'))\n    })\n\n    waitFor(() => {\n      expect(screen.queryByTestId('confirm-btn')).not.toBeInTheDocument()\n    })\n  })\n\n  it('should close confirmation message on outside click', async () => {\n    render(<ConfirmationPopover {...mockedProps} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('button'))\n    })\n\n    await act(() => {\n      fireEvent.click(document)\n    })\n\n    waitFor(() => {\n      expect(screen.queryByTestId('confirm-btn')).not.toBeInTheDocument()\n    })\n  })\n\n  it('should truncate title', async () => {\n    render(\n      <ConfirmationPopover\n        {...mockedProps}\n        title=\"job1fjdsafdhsjalkfhdsjlafhdjksalhfjdsalgldkafhjdsalfdhsjaflkdsahjfdsalkfhdsa\"\n      />,\n    )\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('button'))\n    })\n\n    expect(\n      screen.getByText(\n        'job1fjdsafdhsjalkfhdsjlafhdjksalhfjdsalgldkafhjdsalfdhs...',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/components/confirmation-popover/ConfirmationPopover.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { formatLongName } from 'uiSrc/utils'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport styles from './styles.module.scss'\n\ninterface Props {\n  title: string\n  body: JSX.Element\n  onConfirm: () => void\n  button: JSX.Element\n  submitBtn: JSX.Element\n  onButtonClick?: () => void\n  appendAction?: JSX.Element\n}\n\nconst ConfirmationPopover = (props: Props) => {\n  const {\n    title,\n    body,\n    submitBtn,\n    onConfirm,\n    button,\n    onButtonClick,\n    appendAction,\n  } = props\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)\n\n  const handleClosePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const handleConfirm = () => {\n    onConfirm()\n    setIsPopoverOpen(false)\n  }\n\n  const handleButtonClick = () => {\n    setIsPopoverOpen(true)\n    onButtonClick?.()\n  }\n\n  const popoverButton = React.cloneElement(button, {\n    onClick: handleButtonClick,\n  })\n  const confirmBtn = React.cloneElement(submitBtn, { onClick: handleConfirm })\n\n  return (\n    <OutsideClickDetector onOutsideClick={handleClosePopover}>\n      <RiPopover\n        id=\"confirmation-popover\"\n        ownFocus\n        anchorPosition=\"downCenter\"\n        isOpen={isPopoverOpen}\n        closePopover={handleClosePopover}\n        panelPaddingSize=\"m\"\n        panelClassName={styles.panelPopover}\n        button={popoverButton}\n      >\n        <Row align=\"center\">\n          <FlexItem>\n            <RiIcon type=\"ToastDangerIcon\" className={styles.alertIcon} />\n          </FlexItem>\n          <FlexItem className=\"eui-textNoWrap\">\n            <Text>{formatLongName(title, 58, 0, '...')}</Text>\n          </FlexItem>\n        </Row>\n        <Spacer size=\"xs\" />\n        {body}\n        <Spacer size=\"m\" />\n        <Row justify={appendAction ? 'between' : 'end'} align=\"center\">\n          <FlexItem>{!!appendAction && appendAction}</FlexItem>\n          <FlexItem>{confirmBtn}</FlexItem>\n        </Row>\n      </RiPopover>\n    </OutsideClickDetector>\n  )\n}\n\nexport default ConfirmationPopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/components/confirmation-popover/styles.module.scss",
    "content": ".popoverTitle {\n  color: var(--htmlColor) !important;\n  font-size: 14px !important;\n}\n\n.panelPopover {\n  width: 493px;\n  padding: 16px 30px !important;\n  border: 1px solid var(--euiColorPrimary) !important;\n  :global(.euiPopover__panelArrow:before) {\n    border-bottom-color: var(--euiColorPrimary) !important;\n  }\n}\n\n.alertIcon {\n  margin-right: 8px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/constants/errors.tsx",
    "content": "import { upperFirst } from 'lodash'\nimport React from 'react'\n\nexport const rdiErrorMessages = {\n  invalidStructure: (\n    name = 'Value',\n    msg = 'Failed to convert YAML to JSON structure',\n  ) => (\n    <>\n      {`${upperFirst(name)} has an invalid structure.`}\n      <br />\n      {msg}\n    </>\n  ),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/constants/index.ts",
    "content": "export * from './errors'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport {\n  createInstanceAction,\n  editInstanceAction,\n  instancesSelector,\n} from 'uiSrc/slices/rdi/instances'\nimport {\n  TelemetryEvent,\n  TelemetryPageView,\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n} from 'uiSrc/telemetry'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n  waitForStack,\n} from 'uiSrc/utils/test-utils'\nimport { DEFAULT_RDI_SHOWN_COLUMNS } from 'uiSrc/constants'\n\nimport { apiService } from 'uiSrc/services'\nimport RdiPage from './RdiPage'\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  editInstanceAction: jest.fn().mockReturnValue({ type: null }),\n  createInstanceAction: jest.fn().mockReturnValue({ type: null }),\n  instancesSelector: jest.fn().mockReturnValue({\n    loading: false,\n    loadingChanging: false,\n    shownColumns: ['name', 'url', 'version', 'lastConnection', 'controls'],\n    data: [\n      {\n        id: '1',\n        name: 'My first integration',\n        url: 'redis-12345.c253.us-central1-1.gce.cloud.redislabs.com:12345',\n        lastConnection: new Date('1/1/2024'),\n        version: '1.2',\n        username: 'user',\n        visible: true,\n        error: '',\n      },\n    ],\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet storeMock: typeof mockedStore\n\ndescribe('RdiPage', () => {\n  beforeEach(() => {\n    cleanup()\n    storeMock = cloneDeep(mockedStore)\n    storeMock.clearActions()\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n    ;(sendPageViewTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should render', () => {\n    expect(render(<RdiPage />)).toBeTruthy()\n  })\n\n  it('should render instance list when instances are found', () => {\n    render(<RdiPage />)\n\n    expect(screen.getByTestId('rdi-instance-list')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('empty-rdi-instance-list'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render empty panel when initially loading', () => {\n    ;(instancesSelector as jest.Mock).mockReturnValueOnce({\n      loading: true,\n      shownColumns: DEFAULT_RDI_SHOWN_COLUMNS,\n      data: [],\n    })\n    render(<RdiPage />)\n\n    // New table-based list renders with an empty state \"Loading...\" but still exists\n    expect(screen.getByTestId('rdi-instance-list')).toBeInTheDocument()\n\n    expect(\n      screen.queryByTestId('empty-rdi-instance-list'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render empty message when no instances are found', () => {\n    ;(instancesSelector as jest.Mock).mockReturnValueOnce({\n      data: [],\n      shownColumns: DEFAULT_RDI_SHOWN_COLUMNS,\n    })\n    render(<RdiPage />)\n\n    expect(screen.queryByTestId('rdi-instance-list')).not.toBeInTheDocument()\n    expect(screen.getByTestId('empty-rdi-instance-list')).toBeInTheDocument()\n  })\n\n  it('should open connection form when using header button', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('rdi-instance'))\n    const form = await screen.findByTestId('connection-form')\n\n    expect(form).toBeInTheDocument()\n  })\n\n  it('should open connection form when using empty message button', async () => {\n    ;(instancesSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      shownColumns: DEFAULT_RDI_SHOWN_COLUMNS,\n      data: [],\n    })\n    const { container } = render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('empty-rdi-instance-button'))\n    expect(container.getElementsByClassName('hidden').length).toBe(0)\n  })\n\n  it('should open connection form when using edit button', async () => {\n    const { container } = render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    expect(container.getElementsByClassName('hidden').length).toBe(0)\n  })\n\n  it('should close connection form when using cancel button', async () => {\n    render(<RdiPage />)\n\n    // open form\n    fireEvent.click(screen.getByTestId('rdi-instance'))\n\n    expect(screen.getByTestId('connection-form')).toBeInTheDocument()\n\n    // close form\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('connection-form-cancel-button'))\n    })\n\n    expect(screen.queryByTestId('connection-form')).not.toBeInTheDocument()\n  })\n\n  it('should populate connection form with existing values when using edit button', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    await screen.findByTestId('connection-form')\n\n    expect(screen.getByTestId('connection-form-name-input')).toHaveValue(\n      'My first integration',\n    )\n    expect(screen.getByTestId('connection-form-url-input')).toHaveValue(\n      'redis-12345.c253.us-central1-1.gce.cloud.redislabs.com:12345',\n    )\n    expect(screen.getByTestId('connection-form-username-input')).toHaveValue(\n      'user',\n    )\n    expect(screen.getByTestId('connection-form-password-input')).toHaveValue(\n      '••••••••••••',\n    )\n  })\n\n  it('should open empty connection form with \"default\" username value when using header button', async () => {\n    render(<RdiPage />)\n\n    expect(screen.queryByTestId('connection-form')).not.toBeInTheDocument()\n    // open form\n    fireEvent.click(screen.getByTestId('rdi-instance'))\n    await screen.findByTestId('connection-form')\n    expect(screen.queryByTestId('connection-form')).toBeInTheDocument()\n\n    expect(screen.getByTestId('connection-form-name-input')).toHaveValue('')\n    expect(screen.getByTestId('connection-form-url-input')).toHaveValue('')\n    expect(screen.getByTestId('connection-form-username-input')).toHaveValue(\n      'default',\n    )\n    expect(screen.getByTestId('connection-form-password-input')).toHaveValue('')\n  })\n\n  it('should clear password input when focused for an edited instance', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    await screen.findByTestId('connection-form')\n\n    await act(() => {\n      // focus input to clear it first\n      fireEvent.focus(screen.getByTestId('connection-form-password-input'))\n    })\n\n    expect(screen.getByTestId('connection-form-password-input')).toHaveValue('')\n  })\n\n  it('should call edit instance with proper data when editInstance is provided', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    await screen.findByTestId('connection-form')\n\n    await act(async () => {\n      fireEvent.change(screen.getByTestId('connection-form-name-input'), {\n        target: { value: 'name' },\n      })\n\n      // focus input to clear it first\n      fireEvent.focus(screen.getByTestId('connection-form-password-input'))\n      fireEvent.change(screen.getByTestId('connection-form-password-input'), {\n        target: { value: 'password2' },\n      })\n\n      // submit form\n      fireEvent.click(screen.getByTestId('connection-form-add-button'))\n    })\n\n    expect(editInstanceAction).toBeCalledWith(\n      '1',\n      {\n        name: 'name',\n        password: 'password2',\n      },\n      expect.any(Function),\n    )\n  })\n\n  it('should not pass password when password did not changed', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    await screen.findByTestId('connection-form')\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('connection-form-name-input'), {\n        target: { value: 'name' },\n      })\n\n      // submit form\n      fireEvent.click(screen.getByTestId('connection-form-add-button'))\n    })\n\n    expect(editInstanceAction).toBeCalledWith(\n      '1',\n      {\n        name: 'name',\n      },\n      expect.any(Function),\n    )\n  })\n\n  it('should call create instance when editInstance is not provided', async () => {\n    render(<RdiPage />)\n\n    // open form\n    fireEvent.click(screen.getByTestId('rdi-instance'))\n    await screen.findByTestId('connection-form')\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('connection-form-name-input'), {\n        target: { value: 'name' },\n      })\n      fireEvent.change(screen.getByTestId('connection-form-url-input'), {\n        target: { value: 'url' },\n      })\n      fireEvent.change(screen.getByTestId('connection-form-username-input'), {\n        target: { value: 'username' },\n      })\n\n      // focus input to trigger password change flow\n      fireEvent.focus(screen.getByTestId('connection-form-password-input'))\n      fireEvent.change(screen.getByTestId('connection-form-password-input'), {\n        target: { value: 'password' },\n      })\n\n      // submit form\n      fireEvent.click(screen.getByTestId('connection-form-add-button'))\n    })\n\n    expect(createInstanceAction).toBeCalledWith(\n      { name: 'name', url: 'url', username: 'username', password: 'password' },\n      expect.any(Function),\n      expect.any(Function),\n    )\n  })\n\n  it('should call proper telemetry when connection form is opened', () => {\n    render(<RdiPage />)\n\n    // open form\n    fireEvent.click(screen.getByTestId('rdi-instance'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_INSTANCE_ADD_CLICKED,\n    })\n  })\n\n  it('should call proper telemetry when instance is submitted', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    await screen.findByTestId('connection-form')\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('connection-form-password-input'), {\n        target: { value: 'password3' },\n      })\n\n      // submit form\n      fireEvent.click(screen.getByTestId('connection-form-add-button'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_INSTANCE_SUBMITTED,\n    })\n  })\n\n  it('should call proper telemetry when connection form is closed', async () => {\n    render(<RdiPage />)\n\n    fireEvent.click(screen.getByTestId('controls-button-1'))\n    await waitForRiPopoverVisible()\n    fireEvent.click(screen.getByTestId('edit-instance-1'))\n    await screen.findByTestId('connection-form')\n\n    // close form\n    fireEvent.click(screen.getByTestId('connection-form-cancel-button'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_INSTANCE_ADD_CANCELLED,\n    })\n  })\n\n  it('should call proper sendPageViewTelemetry', async () => {\n    apiService.get = jest.fn().mockResolvedValue({ data: [''], status: 200 })\n    render(<RdiPage />)\n\n    await waitForStack()\n\n    expect(sendPageViewTelemetry).toBeCalledWith({\n      name: TelemetryPageView.RDI_INSTANCES_PAGE,\n      eventData: {\n        instancesCount: 1,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/RdiPage.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport cx from 'classnames'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport {\n  createInstanceAction,\n  editInstanceAction,\n  fetchInstancesAction,\n  instancesSelector,\n} from 'uiSrc/slices/rdi/instances'\nimport {\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n  TelemetryEvent,\n  TelemetryPageView,\n} from 'uiSrc/telemetry'\nimport HomePageTemplate from 'uiSrc/templates/home-page-template'\nimport { setTitle } from 'uiSrc/utils'\nimport { Page, PageBody } from 'uiSrc/components/base/layout/page'\nimport { Rdi as RdiInstanceResponse } from 'apiSrc/modules/rdi/models/rdi'\nimport { dispatch } from 'uiSrc/slices/store'\nimport EmptyMessage from './empty-message/EmptyMessage'\nimport ConnectionForm from './connection-form/ConnectionFormWrapper'\nimport RdiHeader from './header/RdiHeader'\nimport RdiInstancesList from './components/rdi-instances-list/RdiInstancesList'\nimport {\n  RdiPageDataProviderProvider,\n  useRdiPageDataProvider,\n} from './contexts/RdiPageDataProvider'\n\nimport styles from './styles.module.scss'\n\nconst handleOpenPage = (data: RdiInstance[]) => {\n  sendPageViewTelemetry({\n    name: TelemetryPageView.RDI_INSTANCES_PAGE,\n    eventData: {\n      instancesCount: data.length,\n    },\n  })\n}\n\nconst RdiPage = () => {\n  const {\n    editInstance,\n    setEditInstance,\n    isConnectionFormOpen,\n    setIsConnectionFormOpen,\n  } = useRdiPageDataProvider()\n\n  const { data, loading, loadingChanging } = useSelector(instancesSelector)\n  const hideInstancesList = data.length === 0 && !loading && !loadingChanging\n\n  useEffect(() => {\n    dispatch(fetchInstancesAction(handleOpenPage))\n\n    setTitle('Redis Data Integration')\n  }, [])\n\n  const handleFormSubmit = (instance: Partial<RdiInstance>) => {\n    const onSuccess = () => {\n      setIsConnectionFormOpen(false)\n      setEditInstance(null)\n    }\n\n    if (editInstance) {\n      dispatch(editInstanceAction(editInstance.id, instance, onSuccess))\n    } else {\n      dispatch(\n        createInstanceAction(\n          { ...instance },\n          (data: RdiInstanceResponse) => {\n            sendEventTelemetry({\n              event: TelemetryEvent.RDI_ENDPOINT_ADDED,\n              eventData: {\n                rdiId: data.id,\n              },\n            })\n            onSuccess()\n          },\n          (error) => {\n            sendEventTelemetry({\n              event: TelemetryEvent.RDI_ENDPOINT_ADD_FAILED,\n              eventData: {\n                error,\n              },\n            })\n          },\n        ),\n      )\n    }\n\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_INSTANCE_SUBMITTED,\n    })\n  }\n\n  const handleOpenConnectionForm = () => {\n    setIsConnectionFormOpen(true)\n    setEditInstance(null)\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_INSTANCE_ADD_CLICKED,\n    })\n  }\n\n  const handleCloseConnectionForm = () => {\n    setIsConnectionFormOpen(false)\n    setEditInstance(null)\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_INSTANCE_ADD_CANCELLED,\n    })\n  }\n\n  return (\n    <HomePageTemplate>\n      <Page className={cx(styles.page, 'homePage')}>\n        <PageBody component=\"div\">\n          <RdiHeader onRdiInstanceClick={handleOpenConnectionForm} />\n          {hideInstancesList ? (\n            <EmptyMessage onAddInstanceClick={handleOpenConnectionForm} />\n          ) : (\n            <RdiInstancesList />\n          )}\n          <ConnectionForm\n            isOpen={isConnectionFormOpen}\n            onSubmit={handleFormSubmit}\n            onCancel={handleCloseConnectionForm}\n            editInstance={editInstance}\n            isLoading={loading || loadingChanging}\n          />\n        </PageBody>\n      </Page>\n    </HomePageTemplate>\n  )\n}\n\nconst RdiPageWithProvider = () => (\n  <RdiPageDataProviderProvider>\n    <RdiPage />\n  </RdiPageDataProviderProvider>\n)\n\nexport default RdiPageWithProvider\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/RdiInstancesList.config.ts",
    "content": "import { ColumnDef, Table } from 'uiSrc/components/base/layout/table'\nimport { RDI_COLUMN_FIELD_NAME_MAP, RdiListColumn } from 'uiSrc/constants'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\n\nimport RdiInstancesListCellSelect from './components/RdiInstancesListCellSelect/RdiInstancesListCellSelect'\nimport RdiInstancesListCellControls from './components/RdiInstancesListCellControls/RdiInstancesListCellControls'\nimport RdiInstancesListCell from './components/RdiInstancesListCell/RdiInstancesListCell'\n\nexport const SELECT_COL_ID = 'select-col-rdi'\nexport const ENABLE_PAGINATION_COUNT = 15\n\nexport const BASE_COLUMNS: ColumnDef<RdiInstance>[] = [\n  {\n    id: SELECT_COL_ID,\n    size: 40,\n    isHeaderCustom: true,\n    enableSorting: false,\n    header: Table.HeaderMultiRowSelectionButton,\n    cell: RdiInstancesListCellSelect,\n  },\n  {\n    id: RdiListColumn.Name,\n    accessorKey: RdiListColumn.Name,\n    header: RDI_COLUMN_FIELD_NAME_MAP.get(RdiListColumn.Name),\n    enableSorting: true,\n    cell: RdiInstancesListCell,\n    sortingFn: (rowA, rowB) =>\n      `${rowA.original.name?.toLowerCase()}`.localeCompare(\n        `${rowB.original.name?.toLowerCase()}`,\n      ),\n  },\n  {\n    id: RdiListColumn.Url,\n    accessorKey: RdiListColumn.Url,\n    header: RDI_COLUMN_FIELD_NAME_MAP.get(RdiListColumn.Url),\n    enableSorting: true,\n    cell: RdiInstancesListCell,\n    sortingFn: (rowA, rowB) =>\n      `${rowA.original.url?.toLowerCase()}`.localeCompare(\n        `${rowB.original.url?.toLowerCase()}`,\n      ),\n  },\n  {\n    id: RdiListColumn.Version,\n    accessorKey: RdiListColumn.Version,\n    header: RDI_COLUMN_FIELD_NAME_MAP.get(RdiListColumn.Version),\n    enableSorting: true,\n    cell: RdiInstancesListCell,\n  },\n  {\n    id: RdiListColumn.LastConnection,\n    accessorKey: RdiListColumn.LastConnection,\n    header: RDI_COLUMN_FIELD_NAME_MAP.get(RdiListColumn.LastConnection),\n    enableSorting: true,\n    cell: RdiInstancesListCell,\n    sortingFn: (rowA, rowB) => {\n      const a = rowA.original.lastConnection\n      const b = rowB.original.lastConnection\n      const getTime = (v: any) => (v ? new Date(`${v}`).getTime() : -Infinity)\n      return getTime(a) - getTime(b)\n    },\n  },\n  {\n    id: RdiListColumn.Controls,\n    accessorKey: RdiListColumn.Controls,\n    header: '',\n    enableSorting: false,\n    cell: RdiInstancesListCellControls,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/RdiInstancesList.tsx",
    "content": "import React, { memo } from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nimport useRdiInstancesListData from './hooks/useRdiInstancesListData'\nimport {\n  handleCheckConnectToRdiInstance,\n  handlePaginationChange,\n  handleSortingChange,\n  getDefaultPagination,\n} from './methods/handlers'\nimport { getDefaultSorting } from './methods/sortingAdapters'\nimport BulkItemsActions from './components/BulkItemsActions/BulkItemsActions'\n\nconst RdiInstancesList = () => {\n  const {\n    columns,\n    visibleInstances,\n    selectedInstances,\n    paginationEnabled,\n    rowSelection,\n    emptyMessage,\n    setRowSelection,\n    resetRowSelection,\n  } = useRdiInstancesListData()\n\n  return (\n    <div data-testid=\"rdi-instance-list\">\n      <Table\n        data={visibleInstances}\n        columns={columns}\n        stripedRows\n        rowSelectionMode=\"multiple\"\n        paginationEnabled={paginationEnabled}\n        onRowClick={handleCheckConnectToRdiInstance}\n        emptyState={emptyMessage}\n        onRowSelectionChange={setRowSelection}\n        rowSelection={rowSelection}\n        onSortingChange={handleSortingChange}\n        defaultSorting={getDefaultSorting()}\n        onPaginationChange={handlePaginationChange}\n        defaultPagination={getDefaultPagination()}\n        maxHeight=\"60rem\"\n      />\n      <BulkItemsActions items={selectedInstances} onClose={resetRowSelection} />\n    </div>\n  )\n}\n\nexport default memo(RdiInstancesList)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/RdiInstancesList.types.ts",
    "content": "import type { ReactElement } from 'react'\n\nimport type { CellContext } from 'uiSrc/components/base/layout/table'\nimport type { RdiInstance } from 'uiSrc/slices/interfaces'\n\nexport type IRdiListCell = (\n  props: CellContext<RdiInstance, unknown>,\n) => ReactElement<any, any> | null\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/BulkItemsActions/BulkItemsActions.spec.tsx",
    "content": "import React from 'react'\nimport { cleanup, render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport { rdiInstanceFactory } from 'uiSrc/mocks/rdi/RdiInstance.factory'\nimport BulkItemsActions from './BulkItemsActions'\nimport { handleDeleteInstances } from './methods/handlers'\n\njest.mock('./methods/handlers', () => ({\n  handleDeleteInstances: jest.fn(),\n}))\n\ndescribe('BulkItemsActions', () => {\n  beforeEach(() => {\n    cleanup()\n    jest.clearAllMocks()\n  })\n\n  it('should render nothing when no items selected', () => {\n    const { container } = render(\n      <BulkItemsActions items={[]} onClose={jest.fn()} />,\n    )\n\n    // No delete button should be present\n    expect(screen.queryByTestId('delete-btn')).toBeNull()\n    // Container should be essentially empty\n    expect(container.children.length).toBe(0)\n  })\n\n  it('should call handleDeleteInstances and onClose on confirm', async () => {\n    const items = rdiInstanceFactory.buildList(2)\n    const onClose = jest.fn()\n\n    render(<BulkItemsActions items={items} onClose={onClose} />)\n\n    // Open delete popover\n    fireEvent.click(screen.getByTestId('delete-btn'))\n\n    // Confirm delete in the popover\n    const confirmBtn = await screen.findByTestId('delete-selected-dbs')\n    fireEvent.click(confirmBtn)\n\n    expect(handleDeleteInstances).toHaveBeenCalledTimes(1)\n    expect(handleDeleteInstances).toHaveBeenCalledWith(items)\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/BulkItemsActions/BulkItemsActions.tsx",
    "content": "import React, { memo } from 'react'\n\nimport { ActionBar, DeleteAction } from 'uiSrc/components/item-list/components'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\n\nimport { handleDeleteInstances } from './methods/handlers'\n\ntype BulkItemsActionsProps = {\n  items: RdiInstance[]\n  onClose: () => void\n}\n\nconst BulkItemsActions = ({ items, onClose }: BulkItemsActionsProps) => {\n  if (!items.length) return null\n\n  return (\n    <ActionBar\n      selectionCount={items.length}\n      onCloseActionBar={onClose}\n      actions={[\n        <DeleteAction<RdiInstance>\n          selection={items}\n          onDelete={() => {\n            handleDeleteInstances(items)\n            onClose()\n          }}\n          subTitle={`Selected ${items.length} items will be deleted from RedisInsight:`}\n        />,\n      ]}\n    />\n  )\n}\n\nexport default memo(BulkItemsActions)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/BulkItemsActions/methods/handlers.ts",
    "content": "import { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { deleteInstancesAction } from 'uiSrc/slices/rdi/instances'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\n\nexport const handleDeleteInstances = (items: RdiInstance[]) => {\n  sendEventTelemetry({\n    event: TelemetryEvent.RDI_INSTANCE_MULTIPLE_DELETE_CLICKED,\n    eventData: { ids: items.map((i) => i.id) },\n  })\n  dispatch(deleteInstancesAction(items))\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/RdiInstancesListCell/RdiInstancesListCell.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport { rdiInstanceFactory } from 'uiSrc/mocks/rdi/RdiInstance.factory'\nimport RdiInstancesListCell from './RdiInstancesListCell'\nimport { lastConnectionFormat } from 'uiSrc/utils'\n\n// Mock only the handler used by the cell so we can assert call args\nconst mockHandleCopyUrl = jest.fn()\njest.mock('../../methods/handlers', () => ({\n  ...jest.requireActual('../../methods/handlers'),\n  sendCopyUrlTelemetry: (...args: any[]) => mockHandleCopyUrl(...args),\n}))\n\n// Stabilize lastConnection formatting to avoid time-dependence\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  lastConnectionFormat: jest.fn(() => '3 min ago'),\n}))\n\nconst makeProps = (\n  columnId: string,\n  overrides: Partial<ReturnType<typeof rdiInstanceFactory.build>> = {},\n) => {\n  const instance = rdiInstanceFactory.build({\n    id: 'id-1',\n    name: 'Endpoint A',\n    url: 'https://example',\n    ...overrides,\n  })\n  return {\n    row: { original: instance } as any,\n    column: { id: columnId } as any,\n    instance,\n  }\n}\n\ndescribe('RdiInstancesListCell', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render null when value is missing for the column', () => {\n    const { row, column } = makeProps('version', { version: undefined as any })\n    const { container } = render(\n      <RdiInstancesListCell {...({ row, column } as any)} />,\n    )\n    expect(container.firstChild).toBeNull()\n  })\n\n  it('should render text value and data-testid for a string field (name)', () => {\n    const { row, column, instance } = makeProps('name', {\n      id: 'cell-1',\n      name: 'My Endpoint',\n    })\n\n    render(<RdiInstancesListCell {...({ row, column } as any)} />)\n\n    expect(screen.getByText('My Endpoint')).toBeInTheDocument()\n    expect(\n      screen.getByTestId(`rdi-list-cell-${instance.id}-${instance.name}`),\n    ).toBeInTheDocument()\n  })\n\n  it('should not show copy icon for non-url field', () => {\n    const { row, column } = makeProps('name')\n\n    render(<RdiInstancesListCell {...({ row, column } as any)} />)\n\n    expect(screen.queryByRole('button')).not.toBeInTheDocument()\n  })\n\n  it('should show copy icon and call handleCopyUrl with url text and id', async () => {\n    const { row, column, instance } = makeProps('url', {\n      id: 'cpy-1',\n      url: 'https://ri.example',\n    })\n\n    render(<RdiInstancesListCell {...({ row, column } as any)} />)\n\n    const btn = screen.getByRole('button')\n    await userEvent.click(btn, { pointerEventsCheck: 0 })\n\n    expect(mockHandleCopyUrl).toHaveBeenCalledTimes(1)\n    const [id] = mockHandleCopyUrl.mock.calls[0]\n    expect(id).toBe(instance.id)\n  })\n\n  it('should copy url to clipboard when copy button is clicked', async () => {\n    const writeTextMock = jest.fn()\n    Object.assign(navigator, {\n      clipboard: { writeText: writeTextMock },\n    })\n\n    const { row, column, instance } = makeProps('url', {\n      id: 'clipboard-1',\n      url: 'https://clipboard.example',\n    })\n\n    render(<RdiInstancesListCell {...({ row, column } as any)} />)\n\n    const btn = screen.getByRole('button')\n    await userEvent.click(btn, { pointerEventsCheck: 0 })\n\n    expect(writeTextMock).toHaveBeenCalledWith(instance.url)\n  })\n\n  it('should format lastConnection via lastConnectionFormat and render formatted text (no copy icon)', async () => {\n    const date = new Date('2023-01-01T00:00:00.000Z')\n    const { row, column } = makeProps('lastConnection', {\n      id: 'last-1',\n      lastConnection: date,\n    })\n\n    render(<RdiInstancesListCell {...({ row, column } as any)} />)\n\n    expect(lastConnectionFormat).toHaveBeenCalledWith(date as any)\n    expect(screen.getByText('3 min ago')).toBeInTheDocument()\n    expect(screen.queryByRole('button')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/RdiInstancesListCell/RdiInstancesListCell.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const CellContainer = styled(Row)`\n  button {\n    opacity: 0;\n    transition: opacity 250ms ease-in-out;\n  }\n\n  &:hover button {\n    opacity: 1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/RdiInstancesListCell/RdiInstancesListCell.tsx",
    "content": "import React from 'react'\n\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { Text } from 'uiSrc/components/base/text'\nimport { lastConnectionFormat } from 'uiSrc/utils'\nimport { RdiListColumn } from 'uiSrc/constants'\n\nimport { sendCopyUrlTelemetry } from '../../methods/handlers'\nimport { IRdiListCell } from '../../RdiInstancesList.types'\nimport { CellContainer } from './RdiInstancesListCell.styles'\n\nconst fieldCopyIcon: Record<string, boolean> = {\n  [RdiListColumn.Url]: true,\n}\n\nconst fieldFormatters: Record<string, (v: any) => string> = {\n  [RdiListColumn.LastConnection]: lastConnectionFormat,\n}\n\nconst RdiInstancesListCell: IRdiListCell = ({ row, column }) => {\n  const item = row.original\n  const id = item.id\n  const field = column.id as keyof typeof item\n  const value = item[field]\n\n  if (!field || !value) {\n    return null\n  }\n\n  const text = fieldFormatters[field]?.(value) ?? value?.toString()\n  const withCopyIcon = fieldCopyIcon[field]\n\n  return (\n    <CellContainer\n      data-testid={`rdi-list-cell-${id}-${text}`}\n      gap=\"s\"\n      align=\"center\"\n    >\n      <Text>{text}</Text>\n      {withCopyIcon && (\n        <CopyButton\n          copy={text}\n          onCopy={() => sendCopyUrlTelemetry(id)}\n          aria-label=\"Copy URL\"\n        />\n      )}\n    </CellContainer>\n  )\n}\n\nexport default RdiInstancesListCell\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/RdiInstancesListCellControls/RdiInstancesListCellControls.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { EmptyButton, IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { EditIcon, MoreactionsIcon } from 'uiSrc/components/base/icons'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport { formatLongName } from 'uiSrc/utils'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiPopover } from 'uiSrc/components'\nimport { useRdiPageDataProvider } from 'uiSrc/pages/rdi/home/contexts/RdiPageDataProvider'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { deleteInstancesAction } from 'uiSrc/slices/rdi/instances'\nimport { IRdiListCell } from '../../RdiInstancesList.types'\n\nconst suffix = '_rdi_instance'\n\nconst handleClickDeleteInstance = (id: string) => {\n  sendEventTelemetry({\n    event: TelemetryEvent.RDI_INSTANCE_SINGLE_DELETE_CLICKED,\n    eventData: { id },\n  })\n}\n\nconst RdiInstancesListCellControls: IRdiListCell = ({ row }) => {\n  const instance = row.original as RdiInstance\n  const [isDeletePopoverOpen, setIsDeletePopoverOpen] = useState(false)\n  const { setEditInstance, setIsConnectionFormOpen } = useRdiPageDataProvider()\n\n  const deletingId = isDeletePopoverOpen ? `${instance.id + suffix}` : ''\n  const closePopover = () => setIsDeletePopoverOpen(false)\n  const showPopover = () => setIsDeletePopoverOpen(true)\n\n  const handleClickEditInstance = (event: React.MouseEvent<HTMLElement>) => {\n    event.stopPropagation()\n    setEditInstance(instance)\n    setIsConnectionFormOpen(true)\n  }\n\n  const handleConfirmDelete = () => {\n    dispatch(deleteInstancesAction([instance], () => setEditInstance(null)))\n  }\n\n  return (\n    <Row\n      justify=\"end\"\n      align=\"center\"\n      gap=\"xs\"\n      onClick={(e) => e.stopPropagation()}\n    >\n      <RiPopover\n        ownFocus\n        anchorPosition=\"leftUp\"\n        panelPaddingSize=\"s\"\n        button={\n          <IconButton\n            icon={MoreactionsIcon}\n            aria-label=\"Controls icon\"\n            data-testid={`controls-button-${instance.id}`}\n          />\n        }\n        data-testid={`controls-popover-${instance.id}`}\n      >\n        <Col>\n          <EmptyButton\n            justify=\"start\"\n            icon={EditIcon}\n            aria-label=\"Edit instance\"\n            onClick={handleClickEditInstance}\n            data-testid={`edit-instance-${instance.id}`}\n          >\n            Edit endpoint\n          </EmptyButton>\n          <PopoverDelete\n            header={formatLongName(instance.name, 50, 10, '...')}\n            text=\"will be removed from RedisInsight.\"\n            item={instance.id}\n            suffix={suffix}\n            deleting={deletingId}\n            closePopover={closePopover}\n            updateLoading={false}\n            showPopover={showPopover}\n            handleDeleteItem={handleConfirmDelete}\n            handleButtonClick={() => handleClickDeleteInstance(instance.id)}\n            testid={`delete-instance-${instance.id}`}\n            buttonLabel=\"Remove instance\"\n          />\n        </Col>\n      </RiPopover>\n    </Row>\n  )\n}\n\nexport default RdiInstancesListCellControls\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/components/RdiInstancesListCellSelect/RdiInstancesListCellSelect.tsx",
    "content": "import React from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport { IRdiListCell } from '../../RdiInstancesList.types'\n\nconst RdiInstancesListCellSelect: IRdiListCell = (props) => (\n  <Table.RowSelectionButton\n    {...props}\n    onClick={(e: any) => e.stopPropagation()}\n  />\n)\n\nexport default RdiInstancesListCellSelect\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/hooks/useRdiInstancesListData.spec.ts",
    "content": "import { act } from '@testing-library/react'\nimport {\n  mockStore,\n  initialStateDefault,\n  renderHook,\n} from 'uiSrc/utils/test-utils'\nimport { rdiInstanceFactory } from 'uiSrc/mocks/rdi/RdiInstance.factory'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { RdiListColumn } from 'uiSrc/constants'\nimport useRdiInstancesListData from './useRdiInstancesListData'\nimport {\n  ENABLE_PAGINATION_COUNT,\n  SELECT_COL_ID,\n} from '../RdiInstancesList.config'\n\nconst getStoreWith = ({\n  instances,\n  loading = false,\n  shownColumns,\n}: {\n  instances: RdiInstance[]\n  loading?: boolean\n  shownColumns?: RdiListColumn[]\n}) => {\n  const state = {\n    ...initialStateDefault,\n    rdi: {\n      ...initialStateDefault.rdi,\n      instances: {\n        ...initialStateDefault.rdi.instances,\n        data: instances,\n        loading,\n        ...(shownColumns ? { shownColumns } : {}),\n      },\n    },\n  } as typeof initialStateDefault\n\n  return mockStore(state)\n}\n\nconst mockInstances: RdiInstance[] = [\n  rdiInstanceFactory.build({ visible: true }),\n  rdiInstanceFactory.build({ visible: false }),\n  rdiInstanceFactory.build({ visible: true }),\n]\n\ndescribe('useRdiInstancesListData', () => {\n  it('should expose loading state', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should include select column as the first column', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.columns[0]?.id).toBe(SELECT_COL_ID)\n  })\n\n  it('should return only visible instances', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.visibleInstances).toEqual([\n      mockInstances[0],\n      mockInstances[2],\n    ])\n  })\n\n  it('should return empty selected instances when no selection', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.selectedInstances).toEqual([])\n  })\n\n  it('should return selected instances based on rowSelection', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    act(() => {\n      result.current.setRowSelection({ 0: true })\n    })\n\n    expect(result.current.selectedInstances).toEqual([mockInstances[0]])\n\n    act(() => {\n      result.current.setRowSelection({ 0: true, 1: true })\n    })\n\n    // index 1 corresponds to second visible instance (mockInstances[2])\n    expect(result.current.selectedInstances).toEqual([\n      mockInstances[0],\n      mockInstances[2],\n    ])\n  })\n\n  it('should reset row selection', () => {\n    const store = getStoreWith({ instances: mockInstances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    act(() => {\n      result.current.setRowSelection({ 0: true, 1: true })\n    })\n    expect(Object.keys(result.current.rowSelection)).toHaveLength(2)\n\n    act(() => {\n      result.current.resetRowSelection()\n    })\n    expect(result.current.rowSelection).toEqual({})\n  })\n\n  it('should return \"Loading...\" message when loading', () => {\n    const store = getStoreWith({ instances: mockInstances, loading: true })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.emptyMessage).toBe('Loading...')\n  })\n\n  it('should return \"No added endpoints\" message when no instances', () => {\n    const store = getStoreWith({ instances: [] })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.emptyMessage).toBe('No added endpoints')\n  })\n\n  it('should return \"No results found\" message when instances exist but none visible', () => {\n    const instances: RdiInstance[] = [\n      rdiInstanceFactory.build({ visible: false }),\n    ]\n    const store = getStoreWith({ instances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.emptyMessage).toBe('No results found')\n  })\n\n  it('should disable pagination when instances count <= threshold', () => {\n    const instances: RdiInstance[] = Array.from(\n      { length: ENABLE_PAGINATION_COUNT },\n      () => rdiInstanceFactory.build({ visible: true }),\n    )\n    const store = getStoreWith({ instances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.paginationEnabled).toBe(false)\n  })\n\n  it('should enable pagination when instances count > threshold', () => {\n    const instances: RdiInstance[] = Array.from(\n      { length: ENABLE_PAGINATION_COUNT + 1 },\n      () => rdiInstanceFactory.build({ visible: true }),\n    )\n    const store = getStoreWith({ instances })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n\n    expect(result.current.paginationEnabled).toBe(true)\n  })\n\n  it('should filter columns based on shownColumns from state', () => {\n    const store = getStoreWith({\n      instances: mockInstances,\n      shownColumns: [\n        RdiListColumn.Name,\n        RdiListColumn.Url,\n        RdiListColumn.Controls,\n      ],\n    })\n\n    const { result } = renderHook(() => useRdiInstancesListData(), { store })\n    const columnIds = result.current.columns.map((c) => c.id)\n\n    expect(columnIds).toEqual(\n      expect.arrayContaining([\n        SELECT_COL_ID,\n        RdiListColumn.Name,\n        RdiListColumn.Url,\n        RdiListColumn.Controls,\n      ]),\n    )\n    expect(columnIds).not.toEqual(\n      expect.arrayContaining([\n        RdiListColumn.Version,\n        RdiListColumn.LastConnection,\n      ]),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/hooks/useRdiInstancesListData.ts",
    "content": "import { useMemo, useRef, useState, useCallback } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport {\n  ColumnDef,\n  RowSelectionState,\n} from 'uiSrc/components/base/layout/table'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { instancesSelector } from 'uiSrc/slices/rdi/instances'\nimport { RdiListColumn } from 'uiSrc/constants'\n\nimport {\n  ENABLE_PAGINATION_COUNT,\n  BASE_COLUMNS,\n  SELECT_COL_ID,\n} from '../RdiInstancesList.config'\n\nconst useRdiInstancesListData = () => {\n  const {\n    data: instances,\n    loading,\n    shownColumns,\n  } = useSelector(instancesSelector)\n\n  const [rowSelection, setRowSelection] = useState<RowSelectionState>({})\n  const resetRowSelection = useCallback(() => setRowSelection({}), [])\n\n  const paginationEnabledRef = useRef(false)\n  paginationEnabledRef.current =\n    paginationEnabledRef.current || instances.length > ENABLE_PAGINATION_COUNT\n\n  const columns: ColumnDef<RdiInstance>[] = useMemo(\n    () =>\n      BASE_COLUMNS.filter(\n        (col) =>\n          col.id === SELECT_COL_ID ||\n          (shownColumns as RdiListColumn[]).includes(col.id as RdiListColumn),\n      ),\n    [shownColumns],\n  )\n\n  const visibleInstances = useMemo(\n    () => instances.filter(({ visible = true }) => visible),\n    [instances],\n  )\n\n  const selectedInstances = useMemo(\n    () =>\n      visibleInstances.filter((_instance: RdiInstance, index: number) =>\n        Boolean(rowSelection[index]),\n      ),\n    [rowSelection, visibleInstances],\n  )\n\n  const emptyMessage = useMemo(() => {\n    if (loading) return 'Loading...'\n    if (!instances.length) return 'No added endpoints'\n    return 'No results found'\n  }, [loading, instances.length])\n\n  return {\n    loading,\n    columns,\n    visibleInstances,\n    selectedInstances,\n    paginationEnabled: paginationEnabledRef.current,\n    rowSelection,\n    emptyMessage,\n    setRowSelection,\n    resetRowSelection,\n  }\n}\n\nexport default useRdiInstancesListData\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/methods/handlers.ts",
    "content": "import {\n  SortingState,\n  PaginationState,\n} from 'uiSrc/components/base/layout/table'\nimport { BrowserStorageItem, Pages } from 'uiSrc/constants'\nimport {\n  getObjectStorageField,\n  localStorageService,\n  setObjectStorageField,\n} from 'uiSrc/services'\nimport { dispatch } from 'uiSrc/slices/store'\nimport { navigate } from 'uiSrc/Router'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { setAppContextConnectedRdiInstanceId } from 'uiSrc/slices/app/context'\nimport { checkConnectToRdiInstanceAction } from 'uiSrc/slices/rdi/instances'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { TableStorageKey } from 'uiSrc/constants/storage'\n\nimport { sortingStateToPropertySort } from './sortingAdapters'\nimport { handleCopy } from 'uiSrc/utils'\n\nexport const handleSortingChange = (sorting: SortingState) => {\n  if (!sorting.length) return\n\n  const sort = sortingStateToPropertySort(sorting)\n  localStorageService.set(BrowserStorageItem.rdiInstancesSorting, sort)\n  sendEventTelemetry({\n    event: TelemetryEvent.RDI_INSTANCE_LIST_SORTED,\n    eventData: sort,\n  })\n}\n\nexport const handleCheckConnectToRdiInstance = (instance: RdiInstance) => {\n  const { id } = instance\n\n  sendEventTelemetry({\n    event: TelemetryEvent.OPEN_RDI_CLICKED,\n    eventData: { rdiId: id },\n  })\n\n  dispatch(\n    checkConnectToRdiInstanceAction(\n      id,\n      (rdiId: string) => navigate(Pages.rdiPipeline(rdiId)),\n      () => dispatch(setAppContextConnectedRdiInstanceId('')),\n    ),\n  )\n}\n\nexport const sendCopyUrlTelemetry = async (id?: string) => {\n  return sendEventTelemetry({\n    event: TelemetryEvent.RDI_INSTANCE_URL_COPIED,\n    eventData: { id },\n  })\n}\n\nexport const handleCopyUrl = (\n  e: React.MouseEvent<HTMLButtonElement, MouseEvent>,\n  url = '',\n  id?: string,\n) => {\n  e.stopPropagation()\n  handleCopy(url)\n  sendCopyUrlTelemetry(id)\n}\n\nexport const handlePaginationChange = (paginationState: PaginationState) =>\n  setObjectStorageField(\n    BrowserStorageItem.tablePaginationState,\n    TableStorageKey.rdiList,\n    paginationState,\n  )\n\nexport const getDefaultPagination = () =>\n  getObjectStorageField(\n    BrowserStorageItem.tablePaginationState,\n    TableStorageKey.rdiList,\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/methods/sortingAdapters.spec.ts",
    "content": "import type { SortingState } from 'uiSrc/components/base/layout/table'\nimport { DEFAULT_SORT, BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  sortingStateToPropertySort,\n  propertySortToSortingState,\n  getDefaultSorting,\n} from './sortingAdapters'\n\nconst mockLocalStorageGet = jest.fn()\n\njest.mock('uiSrc/services', () => {\n  const actual = jest.requireActual('uiSrc/services')\n  return {\n    ...actual,\n    localStorageService: {\n      ...actual.localStorageService,\n      get: (...args: any[]) => mockLocalStorageGet(...(args as any)),\n    },\n  }\n})\n\ndescribe('sortingAdapters', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('propertySortToSortingState should convert EUI sort to table SortingState', () => {\n    const sorting = propertySortToSortingState({\n      field: 'name',\n      direction: 'desc',\n    })\n    expect(sorting).toEqual([{ id: 'name', desc: true }])\n  })\n\n  it('propertySortToSortingState should fallback to DEFAULT_SORT when sort is undefined', () => {\n    const sorting = propertySortToSortingState(undefined as any)\n    expect(sorting).toEqual([\n      { id: DEFAULT_SORT.field, desc: DEFAULT_SORT.direction === 'desc' },\n    ])\n  })\n\n  it('sortingStateToPropertySort should convert table SortingState to EUI sort', () => {\n    const state: SortingState = [{ id: 'url', desc: false }]\n    const sort = sortingStateToPropertySort(state)\n    expect(sort).toEqual({ field: 'url', direction: 'asc' })\n  })\n\n  it('sortingStateToPropertySort should return fallback when SortingState is empty', () => {\n    const fallback = { field: 'version', direction: 'desc' as const }\n    const sort = sortingStateToPropertySort([], fallback)\n    expect(sort).toEqual(fallback)\n  })\n\n  it('getDefaultSorting should read sort from localStorage when present', () => {\n    mockLocalStorageGet.mockReturnValue({ field: 'name', direction: 'desc' })\n\n    const sorting = getDefaultSorting()\n\n    expect(mockLocalStorageGet).toHaveBeenCalledWith(\n      BrowserStorageItem.rdiInstancesSorting,\n    )\n    expect(sorting).toEqual([{ id: 'name', desc: true }])\n  })\n\n  it('getDefaultSorting should fallback to DEFAULT_SORT when storage is empty', () => {\n    mockLocalStorageGet.mockReturnValue(undefined)\n\n    const sorting = getDefaultSorting()\n\n    expect(sorting).toEqual([\n      { id: DEFAULT_SORT.field, desc: DEFAULT_SORT.direction === 'desc' },\n    ])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/components/rdi-instances-list/methods/sortingAdapters.ts",
    "content": "import type { PropertySort } from '@elastic/eui'\nimport type { SortingState } from 'uiSrc/components/base/layout/table'\nimport { BrowserStorageItem, DEFAULT_SORT } from 'uiSrc/constants'\nimport { localStorageService } from 'uiSrc/services'\n\n// This is for backwards compatibility with the old sorting state format\nexport const sortingStateToPropertySort = (\n  sorting: SortingState,\n  fallback: PropertySort = DEFAULT_SORT,\n): PropertySort => {\n  if (!sorting.length) return fallback\n  return {\n    field: sorting[0].id as string,\n    direction: sorting[0].desc ? 'desc' : 'asc',\n  }\n}\n\nexport const propertySortToSortingState = (\n  sort: PropertySort = DEFAULT_SORT,\n): SortingState => [\n  {\n    id: sort.field,\n    desc: sort.direction === 'desc',\n  },\n]\n\nexport const getDefaultSorting = (): SortingState =>\n  propertySortToSortingState(\n    localStorageService.get(BrowserStorageItem.rdiInstancesSorting) ||\n      undefined,\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionForm.spec.tsx",
    "content": "import React from 'react'\n\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { act, fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport ConnectionForm, { Props } from './ConnectionForm'\n\nconst mockedProps: Props = {\n  onSubmit: jest.fn(),\n  onCancel: jest.fn(),\n  isLoading: false,\n  editInstance: null,\n}\n\nconst mockedEditInstance: RdiInstance = {\n  id: '1',\n  name: 'name',\n  url: 'url',\n  username: 'username',\n  password: 'password',\n  error: '',\n  loading: false,\n}\n\ndescribe('ConnectionForm', () => {\n  it('should render', () => {\n    expect(render(<ConnectionForm {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should disable submit button when form is invalid', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    await waitFor(() => {\n      expect(screen.getByTestId('connection-form-add-button')).toBeDisabled()\n    })\n  })\n\n  // TODO update when add test connection endpoint\n  it.skip('should disable test connection button when form is invalid', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    await waitFor(() => {\n      expect(screen.getByTestId('connection-form-test-button')).toBeDisabled()\n    })\n  })\n\n  it('should not disable submit button when form is valid', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    await waitFor(() => {\n      expect(screen.getByTestId('connection-form-add-button')).toBeDisabled()\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('connection-form-name-input'), {\n        target: { value: 'alias' },\n      })\n      fireEvent.change(screen.getByTestId('connection-form-url-input'), {\n        target: { value: 'url' },\n      })\n      fireEvent.change(screen.getByTestId('connection-form-username-input'), {\n        target: { value: 'username' },\n      })\n      fireEvent.change(screen.getByTestId('connection-form-password-input'), {\n        target: { value: 'password' },\n      })\n    })\n\n    expect(screen.getByTestId('connection-form-add-button')).not.toBeDisabled()\n  })\n\n  it('should not disable submit button when form is provided editInstance', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} editInstance={mockedEditInstance} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    expect(screen.getByTestId('connection-form-add-button')).not.toBeDisabled()\n  })\n\n  it('should disable URL input when form is provided editInstance', () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} editInstance={mockedEditInstance} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    expect(screen.getByTestId('connection-form-url-input')).toBeDisabled()\n  })\n\n  it('should show validation tooltip when submit button is disabled', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    fireEvent.focus(screen.getByTestId('connection-form-add-button'))\n\n    const tooltip = await screen.findByTestId(\n      'connection-form-validation-tooltip',\n    )\n\n    expect(tooltip).toBeInTheDocument()\n  })\n\n  it.skip('should show validation tooltip when test connection button is disabled', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    fireEvent.focus(screen.getByTestId('connection-form-test-button'))\n\n    const tooltip = await screen.findByTestId(\n      'connection-form-validation-tooltip',\n    )\n\n    expect(tooltip).toBeInTheDocument()\n  })\n\n  it('should disable submit button when isLoading = true', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} isLoading />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    expect(screen.getByTestId('connection-form-add-button')).toBeDisabled()\n  })\n\n  it.skip('should disable test connection button when isLoading = true', async () => {\n    render(\n      <>\n        <ConnectionForm {...mockedProps} isLoading />\n        <div id=\"footerDatabaseForm\" />\n      </>,\n    )\n\n    expect(screen.getByTestId('connection-form-test-button')).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionForm.tsx",
    "content": "import {\n  Field,\n  FieldInputProps,\n  FieldMetaProps,\n  Form,\n  Formik,\n  FormikErrors,\n  FormikHelpers,\n} from 'formik'\nimport React, { useEffect, useState } from 'react'\nimport { isNull } from 'lodash'\n\nimport ReactDOM from 'react-dom'\nimport { SECURITY_FIELD } from 'uiSrc/constants'\nimport { RiTooltipProps } from 'uiSrc/components'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { getFormUpdates, Nullable } from 'uiSrc/utils'\nimport { useModalHeader } from 'uiSrc/contexts/ModalTitleProvider'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { PasswordInput, TextInput } from 'uiSrc/components/base/inputs'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport ValidationTooltip from './components/ValidationTooltip'\n\nexport interface AppendInfoProps\n  extends Omit<RiTooltipProps, 'children' | 'delay' | 'position'> {\n  position?: RiTooltipProps['position']\n}\n\nexport interface ConnectionFormValues {\n  name: string\n  url: string\n  username: string\n  password: Nullable<string>\n}\n\nexport interface Props {\n  onSubmit: (instance: Partial<RdiInstance>) => void\n  onCancel: () => void\n  editInstance: RdiInstance | null\n  isLoading: boolean\n}\n\nconst getInitialValues = (\n  values: RdiInstance | null,\n): ConnectionFormValues => ({\n  name: values?.name || '',\n  url: values?.url || '',\n  username: values ? (values.username ?? '') : 'default',\n  password: values ? null : '',\n})\n\nconst ConnectionForm = (props: Props) => {\n  const { onSubmit, onCancel, editInstance, isLoading } = props\n\n  const [initialFormValues, setInitialFormValues] = useState(\n    getInitialValues(editInstance),\n  )\n  const { setModalHeader } = useModalHeader()\n\n  useEffect(() => {\n    setInitialFormValues(getInitialValues(editInstance))\n    setModalHeader(\n      <Title size=\"M\">\n        {editInstance ? 'Edit endpoint' : 'Add RDI endpoint'}\n      </Title>,\n    )\n  }, [editInstance])\n\n  const validate = (values: ConnectionFormValues) => {\n    const errors: FormikErrors<ConnectionFormValues> = {}\n\n    if (!values.name) {\n      errors.name = 'RDI Alias'\n    }\n    if (!values.url) {\n      errors.url = 'URL'\n    }\n\n    return errors\n  }\n\n  const handleSubmit = (values: ConnectionFormValues) => {\n    const updates = getFormUpdates(values, editInstance || {})\n    onSubmit(updates)\n  }\n\n  const Footer = ({\n    isValid,\n    errors,\n    onSubmit,\n  }: {\n    isValid: boolean\n    errors: FormikErrors<ConnectionFormValues>\n    onSubmit: () => void\n  }) => {\n    const footerEl = document.getElementById('footerDatabaseForm')\n\n    if (!footerEl) return null\n\n    return ReactDOM.createPortal(\n      <Row justify=\"end\">\n        <FlexItem>\n          <Row gap=\"m\">\n            <FlexItem>\n              <SecondaryButton\n                data-testid=\"connection-form-cancel-button\"\n                onClick={onCancel}\n              >\n                Cancel\n              </SecondaryButton>\n            </FlexItem>\n            <FlexItem>\n              <ValidationTooltip isValid={isValid} errors={errors}>\n                <PrimaryButton\n                  data-testid=\"connection-form-add-button\"\n                  type=\"submit\"\n                  icon={!isValid ? InfoIcon : undefined}\n                  loading={isLoading}\n                  disabled={!isValid}\n                  onClick={onSubmit}\n                >\n                  {editInstance ? 'Apply Changes' : 'Add Endpoint'}\n                </PrimaryButton>\n              </ValidationTooltip>\n            </FlexItem>\n          </Row>\n        </FlexItem>\n      </Row>,\n      footerEl,\n    )\n  }\n\n  return (\n    <Formik\n      enableReinitialize\n      initialValues={initialFormValues}\n      validateOnMount\n      validate={validate}\n      onSubmit={handleSubmit}\n    >\n      {({ isValid, errors, values }) => (\n        <Form>\n          <Col data-testid=\"connection-form\" gap=\"l\">\n            <FormField label=\"RDI Alias\" required>\n              <Field name=\"name\">\n                {({ field }: { field: FieldInputProps<string> }) => (\n                  <TextInput\n                    data-testid=\"connection-form-name-input\"\n                    placeholder=\"Enter RDI Alias\"\n                    maxLength={500}\n                    name={field.name}\n                    value={field.value}\n                    onChange={(value) =>\n                      field.onChange({ target: { name: field.name, value } })\n                    }\n                  />\n                )}\n              </Field>\n            </FormField>\n            <FormField\n              label=\"URL\"\n              required\n              infoIconProps={{\n                content:\n                  'The RDI machine servers REST API via port 443. Ensure that Redis Insight can access the RDI host over port 443.',\n              }}\n            >\n              <Field name=\"url\">\n                {({ field }: { field: FieldInputProps<string> }) => (\n                  <TextInput\n                    data-testid=\"connection-form-url-input\"\n                    placeholder=\"Enter the RDI host IP as: https://[IP-Address]\"\n                    disabled={!!editInstance}\n                    name={field.name}\n                    value={field.value}\n                    onChange={(value) =>\n                      field.onChange({ target: { name: field.name, value } })\n                    }\n                  />\n                )}\n              </Field>\n            </FormField>\n            <FormField>\n              <Row gap=\"xxl\">\n                <FlexItem grow={2}>\n                  <FormField\n                    label=\"Username\"\n                    infoIconProps={{\n                      content:\n                        'The RDI REST API authentication is using the RDI Redis username and password.',\n                    }}\n                  >\n                    <Field name=\"username\">\n                      {({ field }: { field: FieldInputProps<string> }) => (\n                        <TextInput\n                          data-testid=\"connection-form-username-input\"\n                          placeholder=\"Enter the RDI Redis username\"\n                          maxLength={500}\n                          name={field.name}\n                          value={field.value}\n                          onChange={(value) =>\n                            field.onChange({\n                              target: { name: field.name, value },\n                            })\n                          }\n                        />\n                      )}\n                    </Field>\n                  </FormField>\n                </FlexItem>\n                <FlexItem grow={1}>\n                  <FormField\n                    infoIconProps={{\n                      content:\n                        'The RDI REST API authentication is using the RDI Redis username and password.',\n                    }}\n                    label=\"Password\"\n                  >\n                    <Field name=\"password\">\n                      {({\n                        field,\n                        form,\n                        meta,\n                      }: {\n                        field: FieldInputProps<string>\n                        form: FormikHelpers<string>\n                        meta: FieldMetaProps<string>\n                      }) => (\n                        <PasswordInput\n                          data-testid=\"connection-form-password-input\"\n                          placeholder=\"Enter the RDI Redis password\"\n                          maxLength={500}\n                          {...field}\n                          onChangeCapture={field.onChange}\n                          value={\n                            isNull(field.value) ? SECURITY_FIELD : field.value\n                          }\n                          onFocus={() => {\n                            if (isNull(field.value) && !meta.touched) {\n                              form.setFieldValue('password', '')\n                            }\n                          }}\n                        />\n                      )}\n                    </Field>\n                  </FormField>\n                </FlexItem>\n              </Row>\n            </FormField>\n            <FlexItem grow>\n              <Col justify=\"end\">\n                <Footer\n                  isValid={isValid}\n                  errors={errors}\n                  onSubmit={() => handleSubmit(values)}\n                />\n              </Col>\n            </FlexItem>\n          </Col>\n        </Form>\n      )}\n    </Formik>\n  )\n}\n\nexport default ConnectionForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionFormWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ConnectionFormWrapper, { Props } from './ConnectionFormWrapper'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\ndescribe('ConnectionFormWrapper', () => {\n  it('should render', () => {\n    expect(render(<ConnectionFormWrapper {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should not render form with isOpen = false', () => {\n    render(<ConnectionFormWrapper {...mockedProps} isOpen={false} />)\n\n    expect(screen.queryByTestId('connection-form')).not.toBeInTheDocument()\n  })\n\n  it('should render form with isOpen = true', () => {\n    render(<ConnectionFormWrapper {...mockedProps} isOpen />)\n\n    expect(screen.getByTestId('connection-form')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/connection-form/ConnectionFormWrapper.tsx",
    "content": "import React, { useState } from 'react'\nimport { FormDialog } from 'uiSrc/components'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Nullable } from 'uiSrc/utils'\nimport { ModalHeaderProvider } from 'uiSrc/contexts/ModalTitleProvider'\nimport ConnectionForm, { Props as ConnectionFormProps } from './ConnectionForm'\n\nimport { FooterDatabaseForm } from 'uiSrc/components/form-dialog/FooterDatabaseForm'\n\nexport interface Props extends ConnectionFormProps {\n  isOpen: boolean\n}\n\nconst ConnectionFormWrapper = (props: Props) => {\n  const { isOpen, onCancel } = props\n  const [modalHeader, setModalHeader] =\n    useState<Nullable<React.ReactNode>>(null)\n\n  return (\n    <FormDialog\n      isOpen={isOpen}\n      onClose={onCancel}\n      header={modalHeader ?? <Title size=\"M\">Add endpoint</Title>}\n      footer={<FooterDatabaseForm />}\n    >\n      <ModalHeaderProvider value={{ modalHeader, setModalHeader }}>\n        <ConnectionForm {...props} />\n      </ModalHeaderProvider>\n    </FormDialog>\n  )\n}\n\nexport default ConnectionFormWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/connection-form/components/ValidationTooltip.spec.tsx",
    "content": "import React from 'react'\n\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport ValidationTooltip, { Props } from './ValidationTooltip'\n\nconst mockedProps: Props = {\n  isValid: true,\n  errors: {},\n  children: <div data-testid=\"child\" />,\n}\n\ndescribe('ValidationTooltip', () => {\n  it('should render', () => {\n    expect(render(<ValidationTooltip {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render children', () => {\n    render(<ValidationTooltip {...mockedProps} />)\n\n    expect(screen.getByTestId('child')).toBeInTheDocument()\n  })\n\n  it('should not show tooltip when no errors are present', async () => {\n    render(<ValidationTooltip {...mockedProps} />)\n\n    fireEvent.focus(screen.getByTestId('child'))\n\n    const tooltip = screen.queryByTestId('validation-errors-list')\n\n    expect(tooltip).not.toBeInTheDocument()\n  })\n\n  it('should show tooltip when an error is present', async () => {\n    render(\n      <ValidationTooltip\n        {...mockedProps}\n        isValid={false}\n        errors={{ name: 'error' }}\n      />,\n    )\n\n    fireEvent.focus(screen.getByTestId('child'))\n\n    const tooltip = await screen.findByTestId(\n      'connection-form-validation-tooltip',\n    )\n\n    expect(tooltip).toHaveTextContent('Enter a value for required fields (1)')\n    expect(tooltip).toHaveTextContent('error')\n  })\n\n  it('should show tooltip when multiple errors are present', async () => {\n    render(\n      <ValidationTooltip\n        {...mockedProps}\n        isValid={false}\n        errors={{ name: 'error 1', url: 'error 2' }}\n      />,\n    )\n\n    fireEvent.focus(screen.getByTestId('child'))\n\n    const tooltip = await screen.findByTestId(\n      'connection-form-validation-tooltip',\n    )\n\n    expect(tooltip).toHaveTextContent('Enter a value for required fields (2)')\n    expect(tooltip).toHaveTextContent('error 1')\n    expect(tooltip).toHaveTextContent('error 2')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/connection-form/components/ValidationTooltip.tsx",
    "content": "import { FormikErrors } from 'formik'\nimport React from 'react'\n\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { RiTooltip } from 'uiSrc/components'\nimport { ConnectionFormValues } from '../ConnectionForm'\n\nexport interface Props {\n  isValid: boolean\n  errors: FormikErrors<ConnectionFormValues>\n  children: React.ReactElement\n}\n\nconst ValidationTooltip = ({ isValid, errors, children }: Props) => {\n  const tooltipContent = (\n    <ul data-testid=\"validation-errors-list\">\n      {Object.values(errors).map((value) => (\n        <li key={value}>{value}</li>\n      ))}\n    </ul>\n  )\n\n  return (\n    <RiTooltip\n      data-testid=\"connection-form-validation-tooltip\"\n      position=\"top\"\n      anchorClassName=\"euiToolTip__btn-disabled\"\n      title={\n        !isValid\n          ? validationErrors.REQUIRED_TITLE(Object.keys(errors).length)\n          : null\n      }\n      content={!isValid ? tooltipContent : null}\n    >\n      {children}\n    </RiTooltip>\n  )\n}\n\nexport default ValidationTooltip\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/contexts/RdiPageDataProvider.tsx",
    "content": "import React, { createContext, useContext, useState } from 'react'\n\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\n\nexport interface RdiPageDataProviderContextType {\n  editInstance: RdiInstance | null\n  setEditInstance: (instance: RdiInstance | null) => void\n  isConnectionFormOpen: boolean\n  setIsConnectionFormOpen: (isOpen: boolean) => void\n}\n\nexport const RdiPageDataProviderContext = createContext<\n  RdiPageDataProviderContextType | undefined\n>(undefined)\n\nexport const RdiPageDataProviderProvider: React.FC<{\n  children: React.ReactNode\n}> = ({ children }) => {\n  const [editInstance, setEditInstance] = useState<RdiInstance | null>(null)\n  const [isConnectionFormOpen, setIsConnectionFormOpen] = useState(false)\n\n  return (\n    <RdiPageDataProviderContext.Provider\n      value={{\n        editInstance,\n        setEditInstance,\n        isConnectionFormOpen,\n        setIsConnectionFormOpen,\n      }}\n    >\n      {children}\n    </RdiPageDataProviderContext.Provider>\n  )\n}\n\nexport const useRdiPageDataProvider = () => {\n  const context = useContext(RdiPageDataProviderContext)\n\n  if (!context) {\n    throw new Error(\n      'useRdiPageDataProvider must be used within a RdiPageDataProviderProvider',\n    )\n  }\n\n  return context\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/empty-message/EmptyMessage.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport EmptyMessage, { Props } from './EmptyMessage'\n\nconst mockedProps = mock<Props>()\n\ndescribe('EmptyMessage', () => {\n  it('should render', () => {\n    expect(render(<EmptyMessage {...instance(mockedProps)} />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/empty-message/EmptyMessage.tsx",
    "content": "import React, { useContext } from 'react'\n\nimport EmptyListDarkIcon from 'uiSrc/assets/img/rdi/empty_list_dark.svg'\nimport EmptyListLightIcon from 'uiSrc/assets/img/rdi/empty_list_light.svg'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { Theme } from 'uiSrc/constants'\n\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport { EmptyPageContainer } from 'uiSrc/pages/rdi/home/empty-message/styles'\n\nexport interface Props {\n  onAddInstanceClick: () => void\n}\n\nconst EmptyMessage = ({ onAddInstanceClick }: Props) => {\n  const { theme } = useContext(ThemeContext)\n  return (\n    <EmptyPageContainer grow>\n      <Col data-testid=\"empty-rdi-instance-list\" align=\"center\" gap=\"xxl\">\n        <Spacer size=\"space400\" />\n        <FlexItem>\n          <Col align=\"center\" gap=\"m\">\n            <Title color=\"primary\">Create data pipeline</Title>\n            <FlexItem>\n              <Col align=\"center\">\n                <Text color=\"primary\">\n                  Redis data integration (RDI) streams data to Redis Cloud,\n                  ensuring\n                </Text>\n                <Text color=\"primary\">\n                  real-time sync while saving time and costs. It eliminates\n                  cache\n                </Text>\n                <Text color=\"primary\">\n                  misses and simplifies data management.\n                </Text>\n              </Col>\n            </FlexItem>\n          </Col>\n        </FlexItem>\n        <FlexItem>\n          <PrimaryButton\n            data-testid=\"empty-rdi-instance-button\"\n            size=\"l\"\n            onClick={onAddInstanceClick}\n          >\n            Let’s connect to RDI\n          </PrimaryButton>\n        </FlexItem>\n        <Spacer size=\"space600\" />\n        <FlexItem>\n          <RiImage\n            src={theme === Theme.Dark ? EmptyListDarkIcon : EmptyListLightIcon}\n            alt=\"empty\"\n          />\n        </FlexItem>\n      </Col>\n    </EmptyPageContainer>\n  )\n}\n\nexport default EmptyMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/empty-message/styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const EmptyPageContainer = styled(FlexItem)`\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n  border: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }: { theme: Theme }) =>\n    theme.components.card.borderRadius};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/header/RdiHeader.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { RdiListColumn } from 'uiSrc/constants'\nimport { instancesSelector, setShownColumns } from 'uiSrc/slices/rdi/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { rdiInstanceFactory } from 'uiSrc/mocks/rdi/RdiInstance.factory'\nimport RdiHeader from './RdiHeader'\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  instancesSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(instancesSelector as jest.Mock).mockReturnValue({\n    loading: false,\n    shownColumns: [\n      RdiListColumn.Name,\n      RdiListColumn.Url,\n      RdiListColumn.Controls,\n    ],\n    data: rdiInstanceFactory.buildList(1),\n  })\n})\n\ndescribe('RdiHeader', () => {\n  it('should render', () => {\n    expect(render(<RdiHeader onRdiInstanceClick={() => {}} />)).toBeTruthy()\n  })\n\n  it('should show checkboxes with correct checked state when columns config button is clicked', async () => {\n    render(<RdiHeader onRdiInstanceClick={() => {}} />)\n\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n\n    const shown = [\n      RdiListColumn.Name,\n      RdiListColumn.Url,\n      RdiListColumn.Controls,\n    ]\n    shown.forEach((column) => {\n      const checkbox = screen.getByTestId(`show-${column}`)\n      expect(checkbox).toBeInTheDocument()\n      expect(checkbox).toBeChecked()\n    })\n\n    const hidden = [RdiListColumn.Version, RdiListColumn.LastConnection]\n    hidden.forEach((column) => {\n      const checkbox = screen.getByTestId(`show-${column}`)\n      expect(checkbox).toBeInTheDocument()\n      expect(checkbox).not.toBeChecked()\n    })\n  })\n\n  it('should dispatch setShownColumns action when checkbox clicked', async () => {\n    render(<RdiHeader onRdiInstanceClick={() => {}} />)\n    const afterRenderActions = [...store.getActions()]\n\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n\n    const checkbox = screen.getByTestId('show-name')\n    expect(checkbox).toBeInTheDocument()\n\n    fireEvent.click(checkbox)\n\n    const expectedActions = [\n      setShownColumns([RdiListColumn.Url, RdiListColumn.Controls]),\n    ]\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      ...expectedActions,\n    ])\n  })\n\n  it('should log telemetry event when columns config changed', async () => {\n    const sendEventTelemetryMock = sendEventTelemetry as jest.Mock\n\n    render(<RdiHeader onRdiInstanceClick={() => {}} />)\n\n    fireEvent.click(screen.getByTestId('btn-columns-config'))\n\n    const popover = await screen.findByTestId('columns-config-popover')\n    expect(popover).toBeInTheDocument()\n\n    // clicking this checkbox will hide the column\n    const checkbox = screen.getByTestId('show-url')\n    expect(checkbox).toBeInTheDocument()\n    fireEvent.click(checkbox)\n\n    expect(sendEventTelemetryMock).toBeCalledWith({\n      event: TelemetryEvent.RDI_INSTANCE_LIST_COLUMNS_CLICKED,\n      eventData: {\n        shown: [],\n        hidden: [RdiListColumn.Url],\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/header/RdiHeader.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { instancesSelector, setShownColumns } from 'uiSrc/slices/rdi/instances'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RDI_COLUMN_FIELD_NAME_MAP, RdiListColumn } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport ColumnsConfigPopover from 'uiSrc/components/columns-config/ColumnsConfigPopover'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\nimport SearchRdiList from '../search/SearchRdiList'\n\nexport interface Props {\n  onRdiInstanceClick: () => void\n}\n\nconst RdiHeader = ({ onRdiInstanceClick }: Props) => {\n  const dispatch = useDispatch()\n  const { data: instances, shownColumns } = useSelector(instancesSelector)\n\n  if (instances.length === 0) {\n    return null\n  }\n\n  const handleColumnsChange = (\n    next: RdiListColumn[],\n    diff: { shown: RdiListColumn[]; hidden: RdiListColumn[] },\n  ) => {\n    dispatch(setShownColumns(next))\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_INSTANCE_LIST_COLUMNS_CLICKED,\n      eventData: diff,\n    })\n  }\n\n  return (\n    <div className=\"containerDl\">\n      <Row className=\"contentDL\" align=\"center\" gap=\"s\">\n        <FlexItem>\n          <PrimaryButton\n            onClick={onRdiInstanceClick}\n            data-testid=\"rdi-instance\"\n            icon={PlusIcon}\n          >\n            RDI Instance\n          </PrimaryButton>\n        </FlexItem>\n        {instances.length > 0 && (\n          <Row justify=\"end\" align=\"center\" gap=\"l\">\n            <ColumnsConfigPopover\n              columnsMap={RDI_COLUMN_FIELD_NAME_MAP}\n              shownColumns={shownColumns}\n              onChange={handleColumnsChange}\n            />\n            <SearchRdiList />\n          </Row>\n        )}\n      </Row>\n      <Spacer className=\"spacerDl\" />\n    </div>\n  )\n}\n\nexport default RdiHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/index.ts",
    "content": "import RdiPage from './RdiPage'\n\nexport default RdiPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/search/SearchRdiList.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { useSelector } from 'react-redux'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport { loadInstancesSuccess } from 'uiSrc/slices/rdi/instances'\nimport { RootState, store } from 'uiSrc/slices/store'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport SearchRdiList from './SearchRdiList'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet storeMock: typeof mockedStore\nconst instancesMock: RdiInstance[] = [\n  {\n    id: '1',\n    name: 'My first integration',\n    url: 'redis-12345.c253.us-central1-1.gce.cloud.redislabs.com:12345',\n    lastConnection: new Date(),\n    version: '1.2',\n    visible: true,\n    error: '',\n    loading: false,\n  },\n  {\n    id: '2',\n    name: 'My second integration',\n    url: 'redis-67890.c253.us-central1-1.gce.cloud.redislabs.com:67890',\n    lastConnection: new Date(),\n    version: '1.3',\n    visible: true,\n    error: '',\n    loading: false,\n  },\n]\n\ndescribe('SearchRdiList', () => {\n  beforeEach(() => {\n    cleanup()\n    storeMock = cloneDeep(mockedStore)\n    storeMock.clearActions()\n\n    const state: RootState = store.getState()\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          rdi: {\n            ...state.rdi,\n            instances: {\n              ...state.rdi.instances,\n              data: instancesMock,\n            },\n          },\n        }),\n    )\n  })\n\n  it('should render', () => {\n    expect(render(<SearchRdiList />)).toBeTruthy()\n  })\n\n  it('should call proper telemetry on instance search', async () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n    render(<SearchRdiList />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('search-rdi-instance-list'), {\n        target: { value: 'first int' },\n      })\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_INSTANCE_LIST_SEARCHED,\n      eventData: {\n        instancesFullCount: 2,\n        instancesSearchedCount: 1,\n      },\n    })\n  })\n\n  it('should return all results if filter is blank', async () => {\n    render(<SearchRdiList />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('search-rdi-instance-list'), {\n        target: { value: ' ' },\n      })\n    })\n\n    const expectedActions = [loadInstancesSuccess(instancesMock)]\n    expect(storeMock.getActions()).toEqual(expectedActions)\n  })\n\n  it('should filter by name', async () => {\n    const newInstancesMock = [...instancesMock]\n    render(<SearchRdiList />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('search-rdi-instance-list'), {\n        target: { value: 'second int' },\n      })\n    })\n\n    newInstancesMock[0].visible = false\n    newInstancesMock[1].visible = true\n\n    const expectedActions = [loadInstancesSuccess(newInstancesMock)]\n    expect(storeMock.getActions()).toEqual(expectedActions)\n  })\n\n  it('should filter by url', async () => {\n    const newInstancesMock = [...instancesMock]\n    render(<SearchRdiList />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('search-rdi-instance-list'), {\n        target: { value: 'redislabs.com:12345' },\n      })\n    })\n\n    newInstancesMock[0].visible = true\n    newInstancesMock[1].visible = false\n\n    const expectedActions = [loadInstancesSuccess(newInstancesMock)]\n    expect(storeMock.getActions()).toEqual(expectedActions)\n  })\n\n  it('should filter by version', async () => {\n    const newInstancesMock = [...instancesMock]\n    render(<SearchRdiList />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('search-rdi-instance-list'), {\n        target: { value: '1.2' },\n      })\n    })\n\n    newInstancesMock[0].visible = true\n    newInstancesMock[1].visible = false\n\n    const expectedActions = [loadInstancesSuccess(newInstancesMock)]\n    expect(storeMock.getActions()).toEqual(expectedActions)\n  })\n\n  it('should filter by lastConnection', async () => {\n    const newInstancesMock = [...instancesMock]\n    render(<SearchRdiList />)\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('search-rdi-instance-list'), {\n        target: { value: 'minute ago' },\n      })\n    })\n\n    newInstancesMock[0].visible = true\n    newInstancesMock[1].visible = true\n\n    const expectedActions = [loadInstancesSuccess(newInstancesMock)]\n    expect(storeMock.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/search/SearchRdiList.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { SearchInput } from 'uiSrc/components/base/inputs'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport {\n  instancesSelector,\n  loadInstancesSuccess,\n} from 'uiSrc/slices/rdi/instances'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { lastConnectionFormat } from 'uiSrc/utils'\n\nconst SearchRdiList = () => {\n  const { data: instances } = useSelector(instancesSelector)\n\n  const dispatch = useDispatch()\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n\n    const visibleItems = instances.map((item: RdiInstance) => ({\n      ...item,\n      visible:\n        item.name.toLowerCase().indexOf(value) !== -1 ||\n        item.url?.toString()?.indexOf(value) !== -1 ||\n        item.version?.toString()?.indexOf(value) !== -1 ||\n        lastConnectionFormat(item.lastConnection)?.indexOf(value) !== -1,\n    }))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_INSTANCE_LIST_SEARCHED,\n      eventData: {\n        instancesFullCount: instances.length,\n        instancesSearchedCount: visibleItems.filter(({ visible }) => visible)\n          ?.length,\n      },\n    })\n\n    dispatch(loadInstancesSuccess(visibleItems))\n  }\n\n  return (\n    <SearchInput\n      placeholder=\"Endpoint List Search\"\n      onChange={onQueryChange}\n      aria-label=\"Search rdi instance list\"\n      data-testid=\"search-rdi-instance-list\"\n    />\n  )\n}\n\nexport default SearchRdiList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/home/styles.module.scss",
    "content": ".page {\n  padding: 1px 16px 16px !important;\n\n  :global {\n    .homePage {\n      height: calc(100% - 60px);\n    }\n    .home__resizePanelLeft {\n      flex-grow: 1;\n      height: 100%;\n    }\n\n    .hidden {\n      display: none;\n    }\n\n    .fullWidth {\n      width: 100% !important;\n      min-width: 100% !important;\n      padding: 0;\n      :global(.euiResizablePanel__content) {\n        padding-right: 0;\n        padding-left: 0;\n      }\n    }\n  }\n}\n\n.header {\n  padding-bottom: 16px;\n}\n\n.fullHeight {\n  height: 100%;\n}\n\n@include global.insights-open {\n  :global {\n    .home {\n      &__resizePanelLeft {\n        &.openedRightPanel {\n          display: none;\n        }\n      }\n\n      &__resizableButton {\n        display: none;\n      }\n\n      &__resizePanelRight {\n        padding-left: 0;\n        width: 100% !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/InstancePage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport reactRouterDom, { BrowserRouter } from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\n\nimport {\n  act,\n  cleanup,\n  mockedStore,\n  render,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\nimport { resetKeys, resetPatternKeysData } from 'uiSrc/slices/browser/keys'\nimport { setMonitorInitialState } from 'uiSrc/slices/cli/monitor'\nimport { setInitialPubSubState } from 'uiSrc/slices/pubsub/pubsub'\nimport { setBulkActionsInitialState } from 'uiSrc/slices/browser/bulkActions'\nimport {\n  appContextSelector,\n  resetPipelineManagement,\n  setAppContextConnectedRdiInstanceId,\n  setAppContextInitialState,\n} from 'uiSrc/slices/app/context'\nimport {\n  resetCliHelperSettings,\n  resetCliSettings,\n} from 'uiSrc/slices/cli/cli-settings'\nimport {\n  resetRedisearchKeysData,\n  setRedisearchInitialState,\n} from 'uiSrc/slices/browser/redisearch'\nimport { setClusterDetailsInitialState } from 'uiSrc/slices/analytics/clusterDetails'\nimport { setDatabaseAnalysisInitialState } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { setInitialAnalyticsSettings } from 'uiSrc/slices/analytics/settings'\nimport { setInitialRecommendationsState } from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  loadInstances,\n  loadInstancesSuccess,\n  resetConnectedInstance as resetConnectedDatabaseInstance,\n} from 'uiSrc/slices/instances/instances'\nimport {\n  loadInstances as loadRdiInstances,\n  setConnectedInstance,\n} from 'uiSrc/slices/rdi/instances'\nimport { PageNames, Pages } from 'uiSrc/constants'\nimport {\n  getPipelineStatus,\n  setConfigValidationErrors,\n  setIsPipelineValid,\n  setJobsValidationErrors,\n  setPipelineConfig,\n  setPipelineInitialState,\n  setPipelineJobs,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { clearExpertChatHistory } from 'uiSrc/slices/panels/aiAssistant'\n\nimport InstancePage, { Props } from './InstancePage'\n\nconst RDI_INSTANCE_ID_MOCK = 'rdiInstanceId'\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextSelector: jest.fn().mockReturnValue({\n    contextRdiInstanceId: RDI_INSTANCE_ID_MOCK,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  reactRouterDom.useHistory = jest.fn().mockReturnValue({\n    push: jest.fn(),\n    block: jest.fn(() => jest.fn()),\n  })\n})\n\n/**\n * Rdi InstancePage tests\n *\n * @group component\n */\ndescribe('InstancePage', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should call proper actions with resetting context', async () => {\n    ;(appContextSelector as jest.Mock).mockReturnValue({\n      contextRdiInstanceId: '',\n    })\n\n    await act(async () =>\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    )\n\n    const resetContextActions = [\n      resetKeys(),\n      setMonitorInitialState(),\n      setInitialPubSubState(),\n      setBulkActionsInitialState(),\n      setAppContextInitialState(),\n      resetPatternKeysData(),\n      resetCliHelperSettings(),\n      resetCliSettings(),\n      resetRedisearchKeysData(),\n      setClusterDetailsInitialState(),\n      setDatabaseAnalysisInitialState(),\n      setInitialAnalyticsSettings(),\n      setRedisearchInitialState(),\n      setInitialRecommendationsState(),\n      clearExpertChatHistory(),\n    ]\n\n    const expectedActions = [\n      setConfigValidationErrors(['Error: unknown error']),\n      setJobsValidationErrors({}),\n      setIsPipelineValid(false),\n      getPipelineStatus(),\n      loadInstances(),\n      loadRdiInstances(),\n      setAppContextConnectedRdiInstanceId(''),\n      setPipelineInitialState(),\n      setPipelineConfig(''),\n      setPipelineJobs([]),\n      resetPipelineManagement(),\n      setConnectedInstance(),\n      setAppContextConnectedRdiInstanceId('rdiInstanceId'),\n      resetConnectedDatabaseInstance(),\n      ...resetContextActions,\n      loadInstancesSuccess(expect.any(Array)),\n    ]\n\n    const actualActions = store.getActions()\n    // eslint-disable-next-line no-restricted-syntax\n    for (const ac of expectedActions) {\n      expect(actualActions).toContainEqual(ac)\n    }\n    // expect(actualActions).toEqual(expectedActions)\n  })\n\n  it('should fetch rdi instance info', async () => {\n    ;(appContextSelector as jest.Mock).mockReturnValue({\n      contextRdiInstanceId: 'prevId',\n    })\n\n    // this MUST be awaited, in order for all effects to happen and all actions to be dispatched\n    await act(async () =>\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    )\n\n    const expectedActions = [\n      setConfigValidationErrors(['Error: unknown error']),\n      setJobsValidationErrors({}),\n      setIsPipelineValid(false),\n      getPipelineStatus(),\n      loadInstances(),\n      loadRdiInstances(),\n      setAppContextConnectedRdiInstanceId(''),\n      setPipelineInitialState(),\n      setPipelineConfig(''),\n      setPipelineJobs([]),\n      resetPipelineManagement(),\n      setConnectedInstance(),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n\n  it('should redirect to rdi pipeline management page', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({\n      push: pushMock,\n      block: jest.fn(() => jest.fn()),\n    })\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdiPipeline(RDI_INSTANCE_ID_MOCK) })\n\n    await act(() =>\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    )\n\n    expect(pushMock).toHaveBeenCalledWith(\n      Pages.rdiPipelineManagement(RDI_INSTANCE_ID_MOCK),\n    )\n  })\n\n  it('should navigate to rdi pipeline management page via clicking on navigation', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({\n      push: pushMock,\n      block: jest.fn(() => jest.fn()),\n    })\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdiStatistics(RDI_INSTANCE_ID_MOCK) })\n\n    const { getByRole } = render(\n      <BrowserRouter>\n        <InstancePage {...instance(mockedProps)} />\n      </BrowserRouter>,\n    )\n    expect(pushMock).not.toHaveBeenCalledWith(\n      Pages.rdiPipelineManagement(RDI_INSTANCE_ID_MOCK),\n    )\n    const analyticsTab = getByRole('tab', { name: 'Analytics' })\n    expect(analyticsTab).toBeInTheDocument()\n\n    await userEvent.click(analyticsTab)\n\n    expect(pushMock).not.toHaveBeenCalledWith(\n      Pages.rdiPipelineManagement(RDI_INSTANCE_ID_MOCK),\n    )\n  })\n\n  it('should redirect to rdi pipeline statistics page', async () => {\n    ;(appContextSelector as jest.Mock).mockReturnValue({\n      contextRdiInstanceId: RDI_INSTANCE_ID_MOCK,\n      lastPage: PageNames.rdiStatistics,\n    })\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({\n      push: pushMock,\n      block: jest.fn(() => jest.fn()),\n    })\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdiPipeline(RDI_INSTANCE_ID_MOCK) })\n    await act(() =>\n      render(\n        <BrowserRouter>\n          <InstancePage {...instance(mockedProps)} />\n        </BrowserRouter>,\n      ),\n    )\n\n    expect(pushMock).toHaveBeenCalledWith(\n      Pages.rdiStatistics(RDI_INSTANCE_ID_MOCK),\n    )\n  })\n\n  it('should navigate to rdi pipeline analytics page via clicking on navigation', async () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({\n      push: pushMock,\n      block: jest.fn(() => jest.fn()),\n    })\n\n    reactRouterDom.useLocation = jest.fn().mockReturnValue({\n      pathname: Pages.rdiPipelineManagement(RDI_INSTANCE_ID_MOCK),\n    })\n\n    const { getByRole } = render(\n      <BrowserRouter>\n        <InstancePage {...instance(mockedProps)} />\n      </BrowserRouter>,\n    )\n    expect(pushMock).not.toHaveBeenCalledWith(\n      Pages.rdiStatistics(RDI_INSTANCE_ID_MOCK),\n    )\n    const pipelineTab = getByRole('tab', { name: 'Pipeline' })\n    expect(pipelineTab).toBeInTheDocument()\n\n    await userEvent.click(pipelineTab)\n\n    expect(pushMock).not.toHaveBeenCalledWith(\n      Pages.rdiStatistics(RDI_INSTANCE_ID_MOCK),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/InstancePage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useLocation, useParams } from 'react-router-dom'\nimport {\n  appContextSelector,\n  resetDatabaseContext,\n  resetRdiContext,\n  setAppContextConnectedRdiInstanceId,\n} from 'uiSrc/slices/app/context'\nimport { IRoute, PageNames, Pages } from 'uiSrc/constants'\nimport {\n  fetchConnectedInstanceAction,\n  fetchInstancesAction as fetchRdiInstancesAction,\n  instancesSelector as rdiInstancesSelector,\n} from 'uiSrc/slices/rdi/instances'\nimport {\n  fetchInstancesAction,\n  instancesSelector as dbInstancesSelector,\n  resetConnectedInstance as resetConnectedDatabaseInstance,\n} from 'uiSrc/slices/instances/instances'\n\nimport { RdiInstancePageTemplate } from 'uiSrc/templates'\nimport { AppNavigation, RdiInstanceHeader } from 'uiSrc/components'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport InstancePageRouter from './InstancePageRouter'\nimport { RdiPipelineHeader } from './components'\nimport styles from './styles.module.scss'\nimport { Nullable } from 'uiSrc/utils'\nimport { useNavigation } from 'uiSrc/components/navigation-menu/hooks/useNavigation'\n\nexport interface Props {\n  routes: IRoute[]\n}\n\nconst RdiInstancePage = ({ routes = [] }: Props) => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n  const { pathname } = useLocation()\n  const { privateRdiRoutes } = useNavigation()\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const { lastPage, contextRdiInstanceId } = useSelector(appContextSelector)\n  const { data: rdiInstances } = useSelector(rdiInstancesSelector)\n  const { data: dbInstances } = useSelector(dbInstancesSelector)\n\n  const [actions, setActions] = useState<Nullable<React.ReactNode>>(null)\n\n  useEffect(() => {\n    if (!dbInstances?.length) {\n      dispatch(fetchInstancesAction())\n    }\n    if (!rdiInstances?.length) {\n      dispatch(fetchRdiInstancesAction())\n    }\n  }, [])\n\n  useEffect(() => {\n    if (!contextRdiInstanceId || contextRdiInstanceId !== rdiInstanceId) {\n      dispatch(resetRdiContext())\n      dispatch(fetchConnectedInstanceAction(rdiInstanceId))\n    }\n    dispatch(setAppContextConnectedRdiInstanceId(rdiInstanceId))\n\n    // clear database context\n    dispatch(resetConnectedDatabaseInstance())\n    dispatch(resetDatabaseContext())\n  }, [rdiInstanceId])\n\n  useEffect(() => {\n    // redirect only if there is no exact path\n    if (pathname === Pages.rdiPipeline(rdiInstanceId)) {\n      if (\n        lastPage === PageNames.rdiStatistics &&\n        contextRdiInstanceId === rdiInstanceId\n      ) {\n        history.push(Pages.rdiStatistics(rdiInstanceId))\n        return\n      }\n      history.push(Pages.rdiPipelineManagement(rdiInstanceId))\n    }\n  }, [])\n\n  return (\n    <Col className={styles.page} gap=\"none\" responsive={false}>\n      <FlexItem>\n        <RdiInstanceHeader />\n      </FlexItem>\n      <FlexItem>\n        <AppNavigation\n          actions={actions}\n          onChange={() => setActions(null)}\n          routes={privateRdiRoutes}\n        />\n      </FlexItem>\n      <FlexItem grow={false}>\n        <RdiPipelineHeader />\n      </FlexItem>\n      <RdiInstancePageTemplate>\n        <InstancePageRouter routes={routes} />\n      </RdiInstancePageTemplate>\n    </Col>\n  )\n}\n\nexport default RdiInstancePage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/InstancePageRouter.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { IRoute } from 'uiSrc/constants'\nimport InstancePageRouter from './InstancePageRouter'\n\nconst mockedRoutes = [\n  {\n    path: '/path',\n  },\n]\n\ndescribe('InstancePageRouter', () => {\n  it('should render', () => {\n    expect(\n      render(<InstancePageRouter routes={mockedRoutes as IRoute[]} />, {\n        withRouter: true,\n      }),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/InstancePageRouter.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { IRoute } from 'uiSrc/constants'\n\nexport interface Props {\n  routes: IRoute[]\n}\nconst InstancePageRouter = ({ routes }: Props) => (\n  <Switch>\n    {routes.map((route) => (\n      <RouteWithSubRoutes key={Math.random()} {...route} />\n    ))}\n  </Switch>\n)\n\nexport default React.memo(InstancePageRouter)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/download/Download.spec.tsx",
    "content": "import React from 'react'\n\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport Download, { Props } from './Download'\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    config: 'value',\n    jobs: [\n      { name: 'job1', value: 'value' },\n      { name: 'job2', value: 'value' },\n    ],\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst button = (\n  <button type=\"button\" data-testid=\"download-pipeline-btn\">\n    test\n  </button>\n)\n\nconst renderDownload = (props: Partial<Props> = {}) => {\n  const { trigger = button, ...rest } = props\n  return render(<Download trigger={trigger} {...rest} />)\n}\n\ndescribe('Download', () => {\n  it('should render', () => {\n    expect(renderDownload()).toBeTruthy()\n  })\n\n  it('should call onClose when download clicked', async () => {\n    const onClose = jest.fn()\n    renderDownload({ onClose })\n\n    await userEvent.click(screen.getByTestId('download-pipeline-btn'))\n\n    expect(onClose).toBeCalledTimes(1)\n  })\n\n  it('should call proper telemetry event when button is clicked', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    renderDownload()\n\n    await userEvent.click(screen.getByTestId('download-pipeline-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_DOWNLOAD_CLICKED,\n      eventData: {\n        id: 'rdiInstanceId',\n        jobsNumber: 2,\n      },\n    })\n  })\n\n  it('should render disabled download button when loading', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(() => ({\n      loading: true,\n    }))\n\n    renderDownload()\n\n    expect(screen.getByTestId('download-pipeline-btn')).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/download/Download.tsx",
    "content": "import { saveAs } from 'file-saver'\nimport JSZip from 'jszip'\nimport React from 'react'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nexport interface Props {\n  trigger: React.ReactElement\n  onClose?: () => void\n}\n\nconst Download = ({ onClose, trigger }: Props) => {\n  const { loading, jobs, config } = useSelector(rdiPipelineSelector)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const handleDownloadClick = async () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_DOWNLOAD_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n        jobsNumber: jobs?.length,\n      },\n    })\n\n    // zip config and job contents\n    const zip = new JSZip()\n    zip.file('config.yaml', config || '')\n\n    const rdiJobs = zip.folder('jobs')\n    jobs.forEach(({ name, value }) => rdiJobs?.file(`${name}.yaml`, value))\n\n    const content = await zip.generateAsync({ type: 'blob' })\n    saveAs(content, 'RDI_pipeline.zip')\n\n    onClose?.()\n  }\n\n  const button = trigger\n    ? React.cloneElement(trigger, {\n        disabled: loading,\n        onClick: handleDownloadClick,\n      })\n    : null\n\n  return <>{button}</>\n}\n\nexport default Download\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/download/index.ts",
    "content": "import Download from './Download'\n\nexport default Download\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/RdiPipelineHeader.spec.tsx",
    "content": "import { useFormikContext } from 'formik'\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport { MOCK_RDI_PIPELINE_DATA } from 'uiSrc/mocks/data/rdi'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport { setConfigValidationErrors } from 'uiSrc/slices/rdi/pipeline'\nimport RdiPipelineHeader from './RdiPipelineHeader'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineStatusSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: {},\n    error: '',\n  }),\n}))\n\njest.mock('formik')\n\nconst mockHandleSubmit = jest.fn()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('RdiPipelineHeader', () => {\n  beforeEach(() => {\n    const mockUseFormikContext = {\n      handleSubmit: mockHandleSubmit,\n      values: MOCK_RDI_PIPELINE_DATA,\n    }\n    ;(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)\n  })\n\n  it('should render', () => {\n    expect(render(<RdiPipelineHeader />)).toBeTruthy()\n  })\n\n  it('should call proper actions', () => {\n    render(<RdiPipelineHeader />)\n\n    const expectedActions = [\n      setConfigValidationErrors(['Error: unknown error']),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/RdiPipelineHeader.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  getPipelineStatusAction,\n  rdiPipelineStatusSelector,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport CurrentPipelineStatus from './components/current-pipeline-status'\n\nimport PipelineActions from './components/pipeline-actions'\nimport styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nconst StyledRdiPipelineHeader = styled(Row)`\n  padding: 0 16px;\n  border-bottom: 4px solid\n    ${({ theme }: { theme: Theme }) =>\n      theme.components.tabs.variants.default.tabsLine.color};\n  height: 58px;\n`\n\nconst RdiPipelineHeader = () => {\n  const [headerLoading, setHeaderLoading] = useState(true)\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const { data: statusData, error: statusError } = useSelector(\n    rdiPipelineStatusSelector,\n  )\n  const dispatch = useDispatch()\n\n  let intervalId: any\n\n  useEffect(() => {\n    if (!intervalId) {\n      dispatch(\n        getPipelineStatusAction(\n          rdiInstanceId,\n          () => setHeaderLoading(false),\n          () => setHeaderLoading(false),\n        ),\n      )\n      intervalId = setInterval(() => {\n        dispatch(getPipelineStatusAction(rdiInstanceId))\n      }, 10000)\n    }\n    return () => clearInterval(intervalId)\n  }, [])\n\n  return (\n    <StyledRdiPipelineHeader align=\"center\" justify=\"between\">\n      <FlexItem grow>\n        <CurrentPipelineStatus\n          pipelineState={statusData?.state}\n          pipelineStatus={statusData?.status}\n          statusError={statusError}\n          headerLoading={headerLoading}\n        />\n      </FlexItem>\n      <PipelineActions pipelineStatus={statusData?.status} />\n    </StyledRdiPipelineHeader>\n  )\n}\n\nexport default RdiPipelineHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/DeployPipelineButton.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  within,\n} from 'uiSrc/utils/test-utils'\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\nimport DeployPipelineButton, { Props } from './DeployPipelineButton'\n\nconst mockedProps: Props = {\n  loading: false,\n  disabled: false,\n}\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    config: 'value',\n    isPipelineValid: true,\n    jobs: [\n      { name: 'job1', value: '1' },\n      { name: 'job2', value: '2' },\n    ],\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('DeployPipelineButton', () => {\n  it('should render', () => {\n    expect(render(<DeployPipelineButton {...mockedProps} />)).toBeTruthy()\n  })\n\n  describe('TelemetryEvent', () => {\n    beforeEach(() => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n\n      render(<DeployPipelineButton {...mockedProps} />)\n    })\n\n    it('should call proper telemetry on Deploy', () => {\n      fireEvent.click(screen.getByTestId('deploy-rdi-pipeline'))\n\n      const confirmDeployButton = within(\n        screen.getByRole('dialog'),\n      ).getByLabelText('Deploy')\n      expect(confirmDeployButton).toBeInTheDocument()\n\n      fireEvent.click(confirmDeployButton)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.RDI_DEPLOY_CLICKED,\n        eventData: {\n          id: 'rdiInstanceId',\n          reset: false,\n          jobsNumber: 2,\n        },\n      })\n    })\n\n    it('should reset true if reset checkbox is in the checked state telemetry on Deploy', () => {\n      fireEvent.click(screen.getByTestId('deploy-rdi-pipeline'))\n\n      const el = screen.getByTestId(\n        'reset-pipeline-checkbox',\n      ) as HTMLInputElement\n      expect(el).toHaveAttribute('aria-checked', 'false')\n      fireEvent.click(el)\n      expect(el).toHaveAttribute('aria-checked', 'true')\n\n      const confirmDeployButton = within(\n        screen.getByRole('dialog'),\n      ).getByLabelText('Deploy')\n      expect(confirmDeployButton).toBeInTheDocument()\n\n      fireEvent.click(confirmDeployButton)\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.RDI_DEPLOY_CLICKED,\n        eventData: {\n          id: 'rdiInstanceId',\n          reset: true,\n          jobsNumber: 2,\n        },\n      })\n    })\n  })\n\n  it('should open confirmation popover with default message', () => {\n    render(<DeployPipelineButton {...mockedProps} />)\n\n    expect(screen.queryByTestId('deploy-confirm-btn')).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('deploy-rdi-pipeline'))\n\n    const confirmDeployButton = within(\n      screen.getByRole('dialog'),\n    ).getByLabelText('Deploy')\n    expect(confirmDeployButton).toBeInTheDocument()\n    expect(\n      screen.queryByText('Are you sure you want to deploy the pipeline?'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByText(\n        'Your RDI pipeline contains errors. Are you sure you want to continue?',\n      ),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should open confirmation popover with warning message due to validation errors', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(() => ({\n      isPipelineValid: false,\n    }))\n\n    render(<DeployPipelineButton {...mockedProps} />)\n\n    expect(screen.queryByTestId('deploy-confirm-btn')).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('deploy-rdi-pipeline'))\n\n    const confirmDeployButton = within(\n      screen.getByRole('dialog'),\n    ).getByLabelText('Deploy')\n    expect(confirmDeployButton).toBeInTheDocument()\n    expect(\n      screen.queryByText('Are you sure you want to deploy the pipeline?'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByText(\n        'Your RDI pipeline contains errors. Are you sure you want to continue?',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/DeployPipelineButton.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  deployPipelineAction,\n  getPipelineStatusAction,\n  rdiPipelineSelector,\n  resetPipelineChecked,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { createAxiosError, pipelineToJson } from 'uiSrc/utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { rdiErrorMessages } from 'uiSrc/pages/rdi/constants'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Icon, RocketIcon, InfoIcon } from 'uiSrc/components/base/icons'\nimport { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport { Modal } from 'uiSrc/components/base/display/modal'\nimport { UploadWarningBanner } from 'uiSrc/components/upload-warning/styles'\n\nexport interface Props {\n  loading?: boolean\n  disabled: boolean\n  onReset: () => void\n}\n\nconst DeployPipelineButton = ({ loading, disabled, onReset }: Props) => {\n  const [resetPipeline, setResetPipeline] = useState(false)\n\n  const { config, jobs, resetChecked, isPipelineValid } =\n    useSelector(rdiPipelineSelector)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const dispatch = useDispatch()\n\n  const updatePipelineStatus = () => {\n    if (resetChecked) {\n      dispatch(resetPipelineChecked(false))\n      onReset?.()\n    } else {\n      dispatch(getPipelineStatusAction(rdiInstanceId))\n    }\n  }\n\n  const handleDeployPipeline = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_DEPLOY_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n        reset: resetPipeline,\n        jobsNumber: jobs?.length,\n      },\n    })\n    setResetPipeline(false)\n    const JSONValues = pipelineToJson({ config, jobs }, (errors) => {\n      dispatch(\n        addErrorNotification(\n          createAxiosError({\n            message: rdiErrorMessages.invalidStructure(\n              errors[0].filename,\n              errors[0].msg,\n            ),\n          }),\n        ),\n      )\n    })\n    if (!JSONValues) {\n      return\n    }\n    dispatch(\n      deployPipelineAction(\n        rdiInstanceId,\n        JSONValues,\n        updatePipelineStatus,\n        () => dispatch(getPipelineStatusAction(rdiInstanceId)),\n      ),\n    )\n  }\n\n  const handleSelectReset = (reset: boolean) => {\n    setResetPipeline(reset)\n    dispatch(resetPipelineChecked(reset))\n  }\n\n  return (\n    <Modal\n      id=\"deploy-pipeline-modal\"\n      title=\"Are you sure you want to deploy the pipeline?\"\n      content={\n        <Col gap=\"l\">\n          {!isPipelineValid && (\n            <UploadWarningBanner\n              message=\"Your RDI pipeline contains errors. Are you sure you want to continue?\"\n              show\n              showIcon\n              variant=\"attention\"\n            />\n          )}\n          <FlexItem>\n            <Text>\n              When deployed, this local configuration will overwrite any\n              existing pipeline.\n            </Text>\n            <Text>\n              After deployment, consider flushing the target Redis database and\n              resetting the pipeline to ensure that all data is reprocessed.\n            </Text>\n          </FlexItem>\n          <Row align=\"center\">\n            <Checkbox\n              id=\"resetPipeline\"\n              name=\"resetPipeline\"\n              label=\"Reset\"\n              labelSize=\"M\"\n              checked={resetPipeline}\n              onChange={(e) => handleSelectReset(e.target.checked)}\n              data-testid=\"reset-pipeline-checkbox\"\n            />\n\n            <RiTooltip content=\"The pipeline will take a new snapshot of the data and process it, then continue tracking changes.\">\n              <Icon icon={InfoIcon} data-testid=\"reset-checkbox-info-icon\" />\n            </RiTooltip>\n          </Row>\n        </Col>\n      }\n      primaryButtonText=\"Deploy\"\n      onPrimaryButtonClick={handleDeployPipeline}\n    >\n      <PrimaryButton\n        icon={RocketIcon}\n        disabled={disabled}\n        loading={loading}\n        data-testid=\"deploy-rdi-pipeline\"\n      >\n        Deploy\n      </PrimaryButton>\n    </Modal>\n  )\n}\n\nexport default DeployPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/index.ts",
    "content": "import DeployPipelineButton from './DeployPipelineButton'\n\nexport default DeployPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/index.ts",
    "content": "import DeployPipelineButton from './deploy-pipeline-button'\nimport ResetPipelineButton from './reset-pipeline-button'\nimport StartPipelineButton from './start-pipeline-button'\nimport StopPipelineButton from './stop-pipeline-button'\n\nexport default {\n  DeployPipelineButton,\n  ResetPipelineButton,\n  StartPipelineButton,\n  StopPipelineButton,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/reset-pipeline-button/ResetPipelineButton.spec.tsx",
    "content": "import React from 'react'\n\nimport {\n  fireEvent,\n  render,\n  screen,\n  userEvent,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport ResetPipelineButton, { PipelineButtonProps } from './ResetPipelineButton'\n\nconst mockedProps: PipelineButtonProps = {\n  loading: false,\n  disabled: false,\n  onClick: jest.fn(),\n}\n\ndescribe('ResetPipelineButton', () => {\n  it('should render', () => {\n    expect(render(<ResetPipelineButton {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should show reset info text when hovered', async () => {\n    render(<ResetPipelineButton {...mockedProps} />)\n\n    fireEvent.focus(screen.getByTestId('reset-pipeline-btn'))\n    await waitFor(() =>\n      screen.getAllByText(/flushing the target Redis database/),\n    )\n    expect(\n      screen.getAllByText(/flushing the target Redis database/)[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should call onClick when clicked', () => {\n    const onClick = jest.fn()\n    render(<ResetPipelineButton {...mockedProps} onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('reset-pipeline-btn'))\n    expect(onClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should not be clickable event, when disabled || loading', () => {\n    const onClick = jest.fn()\n    render(<ResetPipelineButton {...mockedProps} disabled onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('reset-pipeline-btn'))\n    expect(onClick).not.toHaveBeenCalled()\n  })\n\n  it('should not be clickable event, when loading', () => {\n    const onClick = jest.fn()\n    render(<ResetPipelineButton {...mockedProps} loading onClick={onClick} />)\n\n    userEvent.click(screen.getByTestId('reset-pipeline-btn'))\n    expect(onClick).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/reset-pipeline-button/ResetPipelineButton.tsx",
    "content": "import React from 'react'\n\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { RiTooltip } from 'uiSrc/components'\nimport styles from '../styles.module.scss'\nimport { Button, TextButton } from '@redis-ui/components'\nimport { ResetIcon } from '@redis-ui/icons'\n\nexport interface PipelineButtonProps {\n  onClick: () => void\n  disabled: boolean\n  loading: boolean\n}\n\nconst ResetPipelineButton = ({\n  onClick,\n  disabled,\n  loading,\n}: PipelineButtonProps) => (\n  <RiTooltip\n    content={\n      !(disabled || loading) ? (\n        <>\n          <p>\n            The pipeline will take a new snapshot of the data and process it,\n            then continue tracking changes.\n          </p>\n          <Spacer size=\"m\" />\n          <p>\n            Before resetting the RDI pipeline, consider stopping the pipeline\n            and flushing the target Redis database.\n          </p>\n        </>\n      ) : null\n    }\n    anchorClassName={disabled || loading ? styles.disabled : styles.tooltip}\n  >\n    <TextButton\n      aria-label=\"Reset pipeline button\"\n      data-testid=\"reset-pipeline-btn\"\n      onClick={onClick}\n      disabled={disabled}\n    >\n      <Button.Icon icon={ResetIcon} />\n      Reset\n    </TextButton>\n  </RiTooltip>\n)\n\nexport default ResetPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/reset-pipeline-button/index.ts",
    "content": "import ResetPipelineButton from './ResetPipelineButton'\n\nexport default ResetPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/start-pipeline-button/StartPipelineButton.spec.tsx",
    "content": "import React from 'react'\n\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport StartPipelineButton from './StartPipelineButton'\nimport { PipelineButtonProps } from '../reset-pipeline-button/ResetPipelineButton'\n\nconst mockedProps: PipelineButtonProps = {\n  loading: false,\n  disabled: false,\n  onClick: jest.fn(),\n}\n\ndescribe('StartPipelineButton', () => {\n  it('should render', () => {\n    expect(render(<StartPipelineButton {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should show reset info text when hovered', async () => {\n    render(<StartPipelineButton {...mockedProps} />)\n\n    fireEvent.focus(screen.getByTestId('start-pipeline-btn'))\n    await waitFor(() =>\n      screen.getAllByText(\n        /Start the pipeline to resume processing new data arrivals/,\n      ),\n    )\n    expect(\n      screen.getAllByText(\n        /Start the pipeline to resume processing new data arrivals/,\n      )[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should call onClick when clicked', () => {\n    const onClick = jest.fn()\n    render(<StartPipelineButton {...mockedProps} onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('start-pipeline-btn'))\n    expect(onClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should not be clickable event, when disabled || loading', () => {\n    const onClick = jest.fn()\n    render(<StartPipelineButton {...mockedProps} disabled onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('start-pipeline-btn'))\n    expect(onClick).not.toHaveBeenCalled()\n  })\n\n  it('should not be clickable event, when loading', () => {\n    const onClick = jest.fn()\n    render(<StartPipelineButton {...mockedProps} loading onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('start-pipeline-btn'))\n    expect(onClick).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/start-pipeline-button/StartPipelineButton.tsx",
    "content": "import React from 'react'\n\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { PlayFilledIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\nimport { PipelineButtonProps } from '../reset-pipeline-button/ResetPipelineButton'\nimport styles from '../styles.module.scss'\n\nconst StartPipelineButton = ({\n  onClick,\n  disabled,\n  loading,\n}: PipelineButtonProps) => (\n  <RiTooltip\n    content=\"Start the pipeline to resume processing new data arrivals.\"\n    anchorClassName={disabled ? styles.disabled : styles.tooltip}\n  >\n    <SecondaryButton\n      aria-label=\"Start running pipeline\"\n      icon={PlayFilledIcon}\n      data-testid=\"start-pipeline-btn\"\n      disabled={disabled}\n      loading={loading}\n      onClick={onClick}\n    >\n      Start\n    </SecondaryButton>\n  </RiTooltip>\n)\n\nexport default StartPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/start-pipeline-button/index.ts",
    "content": "import ResetPipelineButton from '../reset-pipeline-button'\n\nexport default ResetPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/stop-pipeline-button/StopPipelineButton.spec.tsx",
    "content": "import React from 'react'\n\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport StopPipelineButton from './StopPipelineButton'\nimport { PipelineButtonProps } from '../reset-pipeline-button/ResetPipelineButton'\n\nconst mockedProps: PipelineButtonProps = {\n  loading: false,\n  disabled: false,\n  onClick: jest.fn(),\n}\n\ndescribe('StopPipelineButton', () => {\n  it('should render', () => {\n    expect(render(<StopPipelineButton {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should show reset info text when hovered', async () => {\n    render(<StopPipelineButton {...mockedProps} />)\n\n    fireEvent.focus(screen.getByTestId('stop-pipeline-btn'))\n    await waitFor(() =>\n      screen.getAllByText(\n        /Stop the pipeline to prevent processing of new data arrivals/,\n      ),\n    )\n    expect(\n      screen.getAllByText(\n        /Stop the pipeline to prevent processing of new data arrivals/,\n      )[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should call onClick when clicked', () => {\n    const onClick = jest.fn()\n    render(<StopPipelineButton {...mockedProps} onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('stop-pipeline-btn'))\n    expect(onClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should not be clickable event, when disabled || loading', () => {\n    const onClick = jest.fn()\n    render(<StopPipelineButton {...mockedProps} disabled onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('stop-pipeline-btn'))\n    expect(onClick).not.toHaveBeenCalled()\n  })\n\n  it('should not be clickable event, when loading', () => {\n    const onClick = jest.fn()\n    render(<StopPipelineButton {...mockedProps} loading onClick={onClick} />)\n\n    fireEvent.click(screen.getByTestId('stop-pipeline-btn'))\n    expect(onClick).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/stop-pipeline-button/StopPipelineButton.tsx",
    "content": "import React from 'react'\n\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiStopIcon } from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components'\nimport { PipelineButtonProps } from '../reset-pipeline-button/ResetPipelineButton'\nimport styles from '../styles.module.scss'\n\nconst StopPipelineButton = ({\n  onClick,\n  disabled,\n  loading,\n}: PipelineButtonProps) => (\n  <RiTooltip\n    content=\"Stop the pipeline to prevent processing of new data arrivals.\"\n    anchorClassName={disabled ? styles.disabled : undefined}\n  >\n    <SecondaryButton\n      aria-label=\"Stop running pipeline\"\n      loading={loading}\n      disabled={disabled}\n      icon={RiStopIcon}\n      data-testid=\"stop-pipeline-btn\"\n      onClick={onClick}\n    >\n      Stop\n    </SecondaryButton>\n  </RiTooltip>\n)\n\nexport default StopPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/stop-pipeline-button/index.ts",
    "content": "import StopPipelineButton from './StopPipelineButton'\n\nexport default StopPipelineButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/styles.module.scss",
    "content": ".pipelineBtn {\n  border: 1px solid var(--euiColorSecondary) !important;\n\n  svg {\n    color: var(--buttonSecondaryTextColor) !important;\n  }\n}\n\n.btnDisabled {\n  border: 1px solid var(--controlsBorderColor) !important;\n  color: var(--controlsBorderColor) !important;\n  cursor: not-allowed;\n  svg {\n    color: var(--controlsBorderColor) !important;\n  }\n}\n\n.disabled {\n  cursor: not-allowed;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/current-pipeline-status/CurrentPipelineStatus.spec.tsx",
    "content": "import React from 'react'\n\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport { PipelineState, PipelineStatus } from 'uiSrc/slices/interfaces'\nimport CurrentPipelineStatus, { Props } from './CurrentPipelineStatus'\n\nconst mockedProps: Props = {\n  pipelineState: PipelineState.CDC,\n  statusError: '',\n  headerLoading: false,\n}\n\ndescribe('CurrentPipelineStatus', () => {\n  it('should render', () => {\n    expect(render(<CurrentPipelineStatus {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should show status based on pipelineState prop (V1)', () => {\n    render(<CurrentPipelineStatus {...mockedProps} />)\n    expect(screen.getByText('Streaming')).toBeInTheDocument()\n  })\n\n  it('should show status based on pipelineStatus prop (V2)', () => {\n    render(\n      <CurrentPipelineStatus\n        pipelineStatus={PipelineStatus.Started}\n        headerLoading={false}\n      />,\n    )\n    expect(screen.getByText('Started')).toBeInTheDocument()\n  })\n\n  it('should show Stopped for V2 stopped status', () => {\n    render(\n      <CurrentPipelineStatus\n        pipelineStatus={PipelineStatus.Stopped}\n        headerLoading={false}\n      />,\n    )\n    expect(screen.getByText('Stopped')).toBeInTheDocument()\n  })\n\n  it('should show error label and tooltip when statusError is not empty', async () => {\n    const errorMessage = 'Some Error Message'\n    render(\n      <CurrentPipelineStatus\n        pipelineState={undefined}\n        statusError={errorMessage}\n        headerLoading={false}\n      />,\n    )\n    expect(screen.getByText('Error')).toBeInTheDocument()\n\n    fireEvent.focus(screen.getByTestId('pipeline-status-badge'))\n    await waitFor(() => screen.getAllByText(errorMessage)[0])\n    expect(screen.getAllByText(errorMessage)[0]).toBeInTheDocument()\n  })\n\n  it('should show loader when headerLoading is true', () => {\n    render(<CurrentPipelineStatus headerLoading />)\n    expect(screen.getByText('Loading...')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/current-pipeline-status/CurrentPipelineStatus.tsx",
    "content": "import React from 'react'\nimport { PipelineState, PipelineStatus } from 'uiSrc/slices/interfaces'\nimport { formatLongName } from 'uiSrc/utils'\nimport { Icon } from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { getStatusToShowFromState, getStatusToShowFromStatus } from './utils'\n\nexport interface Props {\n  pipelineState?: PipelineState\n  pipelineStatus?: PipelineStatus\n  statusError?: string\n  headerLoading: boolean\n}\n\nconst CurrentPipelineStatus = ({\n  pipelineState,\n  pipelineStatus,\n  statusError,\n  headerLoading,\n}: Props) => {\n  const stateInfo = pipelineState\n    ? getStatusToShowFromState(pipelineState)\n    : getStatusToShowFromStatus(pipelineStatus)\n  const errorTooltipContent = statusError && formatLongName(statusError)\n\n  return (\n    <Row align=\"center\" gap=\"m\">\n      <FlexItem>\n        <Title size=\"XS\" color=\"primary\">\n          Pipeline status\n        </Title>\n      </FlexItem>\n      <FlexItem>\n        {headerLoading ? (\n          <Loader size=\"m\" style={{ marginLeft: '8px' }} />\n        ) : (\n          <RiTooltip\n            content={errorTooltipContent}\n            anchorClassName={statusError}\n          >\n            <Row data-testid=\"pipeline-status-badge\" gap=\"s\" align=\"center\">\n              <Icon icon={stateInfo.icon} color={stateInfo.iconColor} />\n              <Text>{stateInfo.label}</Text>\n            </Row>\n          </RiTooltip>\n        )}\n      </FlexItem>\n    </Row>\n  )\n}\n\nexport default CurrentPipelineStatus\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/current-pipeline-status/index.ts",
    "content": "import CurrentPipelineStatus from './CurrentPipelineStatus'\n\nexport default CurrentPipelineStatus\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/current-pipeline-status/utils.spec.ts",
    "content": "import {\n  IndicatorSyncingIcon,\n  IndicatorSyncedIcon,\n  IndicatorSyncstoppedIcon,\n  IndicatorSyncerrorIcon,\n} from '@redis-ui/icons'\nimport { PipelineState, PipelineStatus } from 'uiSrc/slices/interfaces'\nimport {\n  getStatusToShowFromState,\n  getStatusToShowFromStatus,\n  StatusInfo,\n} from './utils'\n\ndescribe('getStatusToShowFromState', () => {\n  it('should return Initial sync info for InitialSync state', () => {\n    const result = getStatusToShowFromState(PipelineState.InitialSync)\n\n    expect(result).toEqual<StatusInfo>({\n      icon: IndicatorSyncingIcon,\n      iconColor: 'success300',\n      label: 'Initial sync',\n    })\n  })\n\n  it('should return Streaming info for CDC state', () => {\n    const result = getStatusToShowFromState(PipelineState.CDC)\n\n    expect(result).toEqual<StatusInfo>({\n      icon: IndicatorSyncedIcon,\n      iconColor: 'success500',\n      label: 'Streaming',\n    })\n  })\n\n  it('should return Not running info for NotRunning state', () => {\n    const result = getStatusToShowFromState(PipelineState.NotRunning)\n\n    expect(result).toEqual<StatusInfo>({\n      icon: IndicatorSyncstoppedIcon,\n      iconColor: 'attention500',\n      label: 'Not running',\n    })\n  })\n\n  it('should return Error info for undefined state', () => {\n    const result = getStatusToShowFromState(undefined)\n\n    expect(result).toEqual<StatusInfo>({\n      icon: IndicatorSyncerrorIcon,\n      iconColor: 'danger500',\n      label: 'Error',\n    })\n  })\n\n  it('should return Error info for unknown state', () => {\n    const result = getStatusToShowFromState('unknown' as PipelineState)\n\n    expect(result).toEqual<StatusInfo>({\n      icon: IndicatorSyncerrorIcon,\n      iconColor: 'danger500',\n      label: 'Error',\n    })\n  })\n})\n\ndescribe('getStatusToShowFromStatus', () => {\n  describe('transitioning statuses - syncing icon with attention color', () => {\n    it.each([\n      PipelineStatus.Creating,\n      PipelineStatus.Deleting,\n      PipelineStatus.Pending,\n      PipelineStatus.Resetting,\n      PipelineStatus.Starting,\n      PipelineStatus.Stopping,\n      PipelineStatus.Updating,\n      PipelineStatus.NotReady,\n    ])('should return syncing icon for %s status', (status) => {\n      const result = getStatusToShowFromStatus(status)\n\n      expect(result).toEqual<StatusInfo>({\n        label: expect.any(String),\n        icon: IndicatorSyncingIcon,\n        iconColor: 'attention500',\n      })\n      expect(result.label).toBe(\n        status.charAt(0).toUpperCase() + status.slice(1),\n      )\n    })\n  })\n\n  describe('stopped status - stopped icon with attention color', () => {\n    it('should return stopped icon for Stopped status', () => {\n      const result = getStatusToShowFromStatus(PipelineStatus.Stopped)\n\n      expect(result).toEqual<StatusInfo>({\n        label: 'Stopped',\n        icon: IndicatorSyncstoppedIcon,\n        iconColor: 'attention500',\n      })\n    })\n  })\n\n  describe('running statuses - synced icon with success color', () => {\n    it.each([PipelineStatus.Started, PipelineStatus.Ready])(\n      'should return synced icon for %s status',\n      (status) => {\n        const result = getStatusToShowFromStatus(status)\n\n        expect(result).toEqual<StatusInfo>({\n          label: expect.any(String),\n          icon: IndicatorSyncedIcon,\n          iconColor: 'success500',\n        })\n      },\n    )\n  })\n\n  describe('error/unknown statuses - error icon with danger color', () => {\n    it('should return error icon for Error status', () => {\n      const result = getStatusToShowFromStatus(PipelineStatus.Error)\n\n      expect(result).toEqual<StatusInfo>({\n        label: 'Error',\n        icon: IndicatorSyncerrorIcon,\n        iconColor: 'danger500',\n      })\n    })\n\n    it('should return error icon for Unknown status', () => {\n      const result = getStatusToShowFromStatus(PipelineStatus.Unknown)\n\n      expect(result).toEqual<StatusInfo>({\n        label: 'Unknown',\n        icon: IndicatorSyncerrorIcon,\n        iconColor: 'danger500',\n      })\n    })\n\n    it('should return error icon for undefined status', () => {\n      const result = getStatusToShowFromStatus(undefined)\n\n      expect(result).toEqual<StatusInfo>({\n        label: 'Error',\n        icon: IndicatorSyncerrorIcon,\n        iconColor: 'danger500',\n      })\n    })\n\n    it('should return error icon for unknown status value', () => {\n      const result = getStatusToShowFromStatus(\n        'some-unknown-status' as PipelineStatus,\n      )\n\n      expect(result).toEqual<StatusInfo>({\n        label: 'Some-unknown-status',\n        icon: IndicatorSyncerrorIcon,\n        iconColor: 'danger500',\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/current-pipeline-status/utils.ts",
    "content": "import { capitalize } from 'lodash'\nimport {\n  IndicatorSyncingIcon,\n  IndicatorSyncedIcon,\n  IndicatorSyncstoppedIcon,\n  IndicatorSyncerrorIcon,\n} from '@redis-ui/icons'\nimport { PipelineState, PipelineStatus } from 'uiSrc/slices/interfaces'\nimport { IconProps } from 'uiSrc/components/base/icons'\nimport { IconType } from 'uiSrc/components/base/forms/buttons'\nimport { Maybe } from 'uiSrc/utils'\n\nexport interface StatusInfo {\n  label: string\n  icon: IconType\n  iconColor: IconProps['color']\n}\n\nexport const getStatusToShowFromState = (\n  pipelineState: Maybe<PipelineState>,\n): StatusInfo => {\n  switch (pipelineState) {\n    case PipelineState.InitialSync:\n      return {\n        icon: IndicatorSyncingIcon,\n        iconColor: 'success300',\n        label: 'Initial sync',\n      }\n    case PipelineState.CDC:\n      return {\n        icon: IndicatorSyncedIcon,\n        iconColor: 'success500',\n        label: 'Streaming',\n      }\n    case PipelineState.NotRunning:\n      return {\n        icon: IndicatorSyncstoppedIcon,\n        iconColor: 'attention500',\n        label: 'Not running',\n      }\n    default:\n      return {\n        icon: IndicatorSyncerrorIcon,\n        iconColor: 'danger500',\n        label: 'Error',\n      }\n  }\n}\n\nexport const getStatusToShowFromStatus = (\n  status: Maybe<PipelineStatus>,\n): StatusInfo => {\n  const label = capitalize(status || 'Error')\n\n  switch (status) {\n    case PipelineStatus.Creating:\n    case PipelineStatus.Deleting:\n    case PipelineStatus.Pending:\n    case PipelineStatus.Resetting:\n    case PipelineStatus.Starting:\n    case PipelineStatus.Stopping:\n    case PipelineStatus.Updating:\n    case PipelineStatus.NotReady:\n      return {\n        label,\n        icon: IndicatorSyncingIcon,\n        iconColor: 'attention500',\n      }\n    case PipelineStatus.Stopped:\n      return {\n        label,\n        icon: IndicatorSyncstoppedIcon,\n        iconColor: 'attention500',\n      }\n    case PipelineStatus.Started:\n    case PipelineStatus.Ready:\n      return {\n        label,\n        icon: IndicatorSyncedIcon,\n        iconColor: 'success500',\n      }\n    default:\n      return {\n        label,\n        icon: IndicatorSyncerrorIcon,\n        iconColor: 'danger500',\n      }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/index.ts",
    "content": "import PipelineButtons from './buttons'\nimport CurrentPipelineStatus from './current-pipeline-status'\nimport FetchPipelinePopover from './fetch-pipeline-popover'\nimport PipelineActions from './pipeline-actions'\nimport RdiConfigFileActionMenu from './rdi-config-file-action-menu'\n\nexport default {\n  PipelineButtons,\n  CurrentPipelineStatus,\n  FetchPipelinePopover,\n  PipelineActions,\n  RdiConfigFileActionMenu,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/pipeline-actions/PipelineActions.spec.tsx",
    "content": "import { useFormikContext } from 'formik'\nimport { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport { MOCK_RDI_PIPELINE_DATA } from 'uiSrc/mocks/data/rdi'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { PipelineAction, PipelineStatus } from 'uiSrc/slices/interfaces'\nimport { validatePipeline } from 'uiSrc/components/yaml-validator'\nimport {\n  rdiPipelineActionSelector,\n  rdiPipelineSelector,\n  setConfigValidationErrors,\n  setIsPipelineValid,\n  setJobsValidationErrors,\n} from 'uiSrc/slices/rdi/pipeline'\nimport PipelineActions, { Props } from './PipelineActions'\n\nconst mockedProps: Props = {\n  pipelineStatus: PipelineStatus.Ready,\n}\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n  }),\n  rdiPipelineActionSelector: jest.fn().mockReturnValue({\n    loading: false,\n    action: null,\n  }),\n}))\n\njest.mock('formik')\n\njest.mock('uiSrc/components/yaml-validator', () => ({\n  validatePipeline: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('PipelineActions', () => {\n  beforeEach(() => {\n    const mockUseFormikContext = {\n      handleSubmit: jest.fn(),\n      values: MOCK_RDI_PIPELINE_DATA,\n    }\n    ;(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)\n  })\n\n  it('should render', () => {\n    expect(render(<PipelineActions {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should display stopBtn if pipelineStatus is Ready', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Ready}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n  })\n\n  it('should display startBtn if pipelineStatus is Stopped', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Stopped}\n      />,\n    )\n    expect(screen.getByText('Start')).toBeInTheDocument()\n  })\n\n  it('should display startBtn if pipelineStatus is NotReady', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.NotReady}\n      />,\n    )\n    expect(screen.getByText('Start')).toBeInTheDocument()\n  })\n\n  // V2 status tests\n  it('should display stopBtn if pipelineStatus is Started (V2)', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Started}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.queryByText('Start')).not.toBeInTheDocument()\n  })\n\n  it('should display stopBtn if pipelineStatus is Error (V2)', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Error}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.queryByText('Start')).not.toBeInTheDocument()\n  })\n\n  it('should display stopBtn if pipelineStatus is Unknown (V2)', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Unknown}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.queryByText('Start')).not.toBeInTheDocument()\n  })\n\n  // Transitional state tests\n  it('should display disabled stopBtn if pipelineStatus is Starting', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Starting}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.getByTestId('stop-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should display disabled stopBtn if pipelineStatus is Stopping', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Stopping}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.getByTestId('stop-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should display disabled stopBtn if pipelineStatus is Pending', () => {\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Pending}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.getByTestId('stop-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should display no button if pipelineStatus is undefined', () => {\n    render(<PipelineActions {...mockedProps} pipelineStatus={undefined} />)\n    expect(screen.queryByText('Stop')).not.toBeInTheDocument()\n    expect(screen.queryByText('Start')).not.toBeInTheDocument()\n  })\n\n  it('should display disabled stopBtn if deployLoading is true', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {},\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      loading: true,\n      schema: null,\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Started}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.getByTestId('stop-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should display disabled startBtn if deployLoading is true', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {},\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      loading: true,\n      schema: null,\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Stopped}\n      />,\n    )\n    expect(screen.getByText('Start')).toBeInTheDocument()\n    expect(screen.getByTestId('start-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should display disabled stopBtn when Reset action is in progress', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {},\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      schema: null,\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n    ;(rdiPipelineActionSelector as jest.Mock).mockReturnValueOnce({\n      loading: true,\n      action: PipelineAction.Reset,\n    })\n\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Started}\n      />,\n    )\n    expect(screen.getByText('Stop')).toBeInTheDocument()\n    expect(screen.getByTestId('stop-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should display disabled startBtn when Reset action is in progress', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {},\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      schema: null,\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n    ;(rdiPipelineActionSelector as jest.Mock).mockReturnValueOnce({\n      loading: true,\n      action: PipelineAction.Reset,\n    })\n\n    render(\n      <PipelineActions\n        {...mockedProps}\n        pipelineStatus={PipelineStatus.Stopped}\n      />,\n    )\n    expect(screen.getByText('Start')).toBeInTheDocument()\n    expect(screen.getByTestId('start-pipeline-btn')).toBeDisabled()\n  })\n\n  it('should validate pipeline when schema, config, or jobs change', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {},\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      schema: 'test-schema',\n      monacoJobsSchema: 'test-monaco-jobs-schema',\n      jobNameSchema: 'test-job-name-schema',\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n\n    render(<PipelineActions {...mockedProps} />)\n\n    expect(validatePipeline).toHaveBeenCalledWith({\n      schema: 'test-schema',\n      monacoJobsSchema: 'test-monaco-jobs-schema',\n      jobNameSchema: 'test-job-name-schema',\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n\n    expect(store.getActions()).toEqual([\n      setConfigValidationErrors([]),\n      setJobsValidationErrors({}),\n      setIsPipelineValid(true),\n    ])\n  })\n\n  it('should set pipeline as invalid if config and jobs are empty (no configuration is entered)', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: false,\n      configValidationErrors: ['Error'],\n      jobsValidationErrors: [],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      config: '',\n      jobs: '',\n    })\n\n    render(<PipelineActions {...mockedProps} />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          type: setIsPipelineValid.type,\n          payload: false,\n        }),\n      ]),\n    )\n  })\n\n  it('should set pipeline as invalid if config and jobs are missing or empty (no configuration is entered)', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: false,\n      configValidationErrors: ['Error'],\n      jobsValidationErrors: [],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      config: undefined,\n      jobs: undefined,\n    })\n\n    render(<PipelineActions {...mockedProps} />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          type: setIsPipelineValid.type,\n          payload: false,\n        }),\n      ]),\n    )\n  })\n\n  it('should dispatch validation errors if validation fails but still deploy button should be enabled', () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: false,\n      configValidationErrors: ['Missing field'],\n      jobsValidationErrors: ['Invalid job config'],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      schema: 'test-schema',\n      monacoJobsSchema: 'test-monaco-jobs-schema',\n      jobNameSchema: 'test-job-name-schema',\n      config: 'test-config',\n      jobs: 'test-jobs',\n    })\n\n    render(<PipelineActions {...mockedProps} />)\n\n    expect(store.getActions()).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          type: setConfigValidationErrors.type,\n          payload: ['Missing field'],\n        }),\n        expect.objectContaining({\n          type: setJobsValidationErrors.type,\n          payload: ['Invalid job config'],\n        }),\n        expect.objectContaining({\n          type: setIsPipelineValid.type,\n          payload: false,\n        }),\n      ]),\n    )\n\n    expect(screen.queryByTestId('deploy-rdi-pipeline')).not.toBeDisabled()\n  })\n\n  describe('validation with new schema parameters', () => {\n    it('should pass monacoJobsSchema and jobNameSchema to validatePipeline when available', () => {\n      const mockMonacoJobsSchema = {\n        type: 'object',\n        properties: { task: { type: 'string' } },\n      }\n      const mockJobNameSchema = {\n        type: 'string',\n        pattern: '^[a-zA-Z][a-zA-Z0-9_]*$',\n      }\n\n      ;(validatePipeline as jest.Mock).mockReturnValue({\n        result: true,\n        configValidationErrors: [],\n        jobsValidationErrors: {},\n      })\n      ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n        loading: false,\n        schema: 'test-schema',\n        monacoJobsSchema: mockMonacoJobsSchema,\n        jobNameSchema: mockJobNameSchema,\n        config: 'test-config',\n        jobs: 'test-jobs',\n      })\n\n      render(<PipelineActions {...mockedProps} />)\n\n      expect(validatePipeline).toHaveBeenCalledWith({\n        schema: 'test-schema',\n        monacoJobsSchema: mockMonacoJobsSchema,\n        jobNameSchema: mockJobNameSchema,\n        config: 'test-config',\n        jobs: 'test-jobs',\n      })\n    })\n\n    it('should pass null/undefined schemas to validatePipeline when not available', () => {\n      ;(validatePipeline as jest.Mock).mockReturnValue({\n        result: true,\n        configValidationErrors: [],\n        jobsValidationErrors: {},\n      })\n      ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n        loading: false,\n        schema: 'test-schema',\n        monacoJobsSchema: null,\n        jobNameSchema: undefined,\n        config: 'test-config',\n        jobs: 'test-jobs',\n      })\n\n      render(<PipelineActions {...mockedProps} />)\n\n      expect(validatePipeline).toHaveBeenCalledWith({\n        schema: 'test-schema',\n        monacoJobsSchema: null,\n        jobNameSchema: undefined,\n        config: 'test-config',\n        jobs: 'test-jobs',\n      })\n    })\n\n    it('should include monacoJobsSchema and jobNameSchema in dependency array for validation effect', () => {\n      // This test verifies that the useEffect dependency array includes the new schema parameters\n      // by checking that different schema values trigger different validatePipeline calls\n\n      ;(validatePipeline as jest.Mock).mockReturnValue({\n        result: true,\n        configValidationErrors: [],\n        jobsValidationErrors: {},\n      })\n\n      // First render with specific schemas\n      ;(rdiPipelineSelector as jest.Mock).mockReturnValueOnce({\n        loading: false,\n        schema: 'test-schema',\n        monacoJobsSchema: {\n          type: 'object',\n          properties: { task: { type: 'string' } },\n        },\n        jobNameSchema: { type: 'string', pattern: '^[a-zA-Z]+$' },\n        config: 'test-config',\n        jobs: 'test-jobs',\n      })\n\n      render(<PipelineActions {...mockedProps} />)\n\n      // Verify that validatePipeline was called with all the correct parameters including schemas\n      expect(validatePipeline).toHaveBeenCalledWith({\n        schema: 'test-schema',\n        monacoJobsSchema: {\n          type: 'object',\n          properties: { task: { type: 'string' } },\n        },\n        jobNameSchema: { type: 'string', pattern: '^[a-zA-Z]+$' },\n        config: 'test-config',\n        jobs: 'test-jobs',\n      })\n    })\n  })\n\n  describe('TelemetryEvent', () => {\n    beforeEach(() => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n    })\n\n    it('should call proper telemetry on reset btn click', () => {\n      render(<PipelineActions {...mockedProps} />)\n      fireEvent.click(screen.getByTestId('reset-pipeline-btn'))\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.RDI_PIPELINE_RESET_CLICKED,\n        eventData: {\n          id: 'rdiInstanceId',\n          pipelineStatus: mockedProps.pipelineStatus,\n        },\n      })\n    })\n\n    it('should call proper telemetry on start btn click', () => {\n      render(\n        <PipelineActions\n          {...mockedProps}\n          pipelineStatus={PipelineStatus.Stopped}\n        />,\n      )\n      fireEvent.click(screen.getByTestId('start-pipeline-btn'))\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.RDI_PIPELINE_START_CLICKED,\n        eventData: {\n          id: 'rdiInstanceId',\n        },\n      })\n    })\n\n    it('should call proper telemetry on stop btn click', () => {\n      render(<PipelineActions {...mockedProps} />)\n      fireEvent.click(screen.getByTestId('stop-pipeline-btn'))\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.RDI_PIPELINE_STOP_CLICKED,\n        eventData: {\n          id: 'rdiInstanceId',\n        },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/pipeline-actions/PipelineActions.tsx",
    "content": "import React, { useCallback, useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport styled from 'styled-components'\nimport {\n  getPipelineStatusAction,\n  rdiPipelineActionSelector,\n  rdiPipelineSelector,\n  resetPipelineAction,\n  setConfigValidationErrors,\n  setIsPipelineValid,\n  setJobsValidationErrors,\n  startPipelineAction,\n  stopPipelineAction,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { validatePipeline } from 'uiSrc/components/yaml-validator'\nimport {\n  IActionPipelineResultProps,\n  PipelineAction,\n  PipelineStatus,\n} from 'uiSrc/slices/interfaces'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport DeployPipelineButton from '../buttons/deploy-pipeline-button'\nimport ResetPipelineButton from '../buttons/reset-pipeline-button'\nimport RdiConfigFileActionMenu from '../rdi-config-file-action-menu'\nimport StopPipelineButton from '../buttons/stop-pipeline-button'\nimport StartPipelineButton from '../buttons/start-pipeline-button/StartPipelineButton'\nimport { getActionButtonState } from './utils'\n\nconst VerticalDelimiter = styled(FlexItem)`\n  border: ${({ theme }: { theme: Theme }) => theme.components.appBar.separator};\n  align-self: stretch;\n`\n\nexport interface Props {\n  pipelineStatus?: PipelineStatus\n}\n\nconst PipelineActions = ({ pipelineStatus }: Props) => {\n  const {\n    loading: deployLoading,\n    schema,\n    monacoJobsSchema,\n    jobNameSchema,\n    config,\n    jobs,\n  } = useSelector(rdiPipelineSelector)\n  const { loading: actionLoading, action } = useSelector(\n    rdiPipelineActionSelector,\n  )\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!jobs && !config) {\n      dispatch(setIsPipelineValid(false))\n      return\n    }\n\n    const { result, configValidationErrors, jobsValidationErrors } =\n      validatePipeline({\n        schema,\n        monacoJobsSchema,\n        jobNameSchema,\n        config,\n        jobs,\n      })\n\n    dispatch(setConfigValidationErrors(configValidationErrors))\n    dispatch(setJobsValidationErrors(jobsValidationErrors))\n    dispatch(setIsPipelineValid(result))\n  }, [schema, config, jobs])\n\n  const actionPipelineCallback = useCallback(\n    (event: TelemetryEvent, result: IActionPipelineResultProps) => {\n      sendEventTelemetry({\n        event,\n        eventData: {\n          id: rdiInstanceId,\n          ...result,\n        },\n      })\n      dispatch(getPipelineStatusAction(rdiInstanceId))\n    },\n    [rdiInstanceId],\n  )\n\n  const resetPipeline = useCallback(() => {\n    dispatch(\n      resetPipelineAction(\n        rdiInstanceId,\n        (result: IActionPipelineResultProps) =>\n          actionPipelineCallback(TelemetryEvent.RDI_PIPELINE_RESET, result),\n        (result: IActionPipelineResultProps) =>\n          actionPipelineCallback(TelemetryEvent.RDI_PIPELINE_RESET, result),\n      ),\n    )\n  }, [rdiInstanceId])\n\n  const onReset = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_RESET_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n        pipelineStatus,\n      },\n    })\n    resetPipeline()\n  }\n\n  const onStartPipeline = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_START_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n      },\n    })\n    dispatch(\n      startPipelineAction(\n        rdiInstanceId,\n        (result: IActionPipelineResultProps) =>\n          actionPipelineCallback(TelemetryEvent.RDI_PIPELINE_STARTED, result),\n        (result: IActionPipelineResultProps) =>\n          actionPipelineCallback(TelemetryEvent.RDI_PIPELINE_STARTED, result),\n      ),\n    )\n  }\n\n  const onStopPipeline = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_STOP_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n      },\n    })\n    dispatch(\n      stopPipelineAction(\n        rdiInstanceId,\n        (result) =>\n          actionPipelineCallback(TelemetryEvent.RDI_PIPELINE_STOPPED, result),\n        (result) =>\n          actionPipelineCallback(TelemetryEvent.RDI_PIPELINE_STOPPED, result),\n      ),\n    )\n  }\n\n  const isLoadingBtn = (actionBtn: PipelineAction) =>\n    action === actionBtn && actionLoading\n  const disabled = deployLoading || actionLoading\n\n  const actionButtonState = getActionButtonState(\n    action,\n    pipelineStatus,\n    disabled,\n  )\n\n  return (\n    <Row gap=\"l\" justify=\"end\" align=\"center\">\n      <FlexItem>\n        <ResetPipelineButton\n          onClick={onReset}\n          disabled={disabled}\n          loading={isLoadingBtn(PipelineAction.Reset)}\n        />\n      </FlexItem>\n      <VerticalDelimiter />\n      <FlexItem>\n        {actionButtonState.button === 'stop' ? (\n          <StopPipelineButton\n            onClick={onStopPipeline}\n            disabled={actionButtonState.disabled}\n            loading={isLoadingBtn(PipelineAction.Stop)}\n          />\n        ) : actionButtonState.button === 'start' ? (\n          <StartPipelineButton\n            onClick={onStartPipeline}\n            disabled={actionButtonState.disabled}\n            loading={isLoadingBtn(PipelineAction.Start)}\n          />\n        ) : null}\n      </FlexItem>\n      <FlexItem>\n        <DeployPipelineButton disabled={disabled} onReset={resetPipeline} />\n      </FlexItem>\n      <FlexItem>\n        <RdiConfigFileActionMenu />\n      </FlexItem>\n    </Row>\n  )\n}\n\nexport default PipelineActions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/pipeline-actions/index.ts",
    "content": "import PipelineActions from './PipelineActions'\n\nexport default PipelineActions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/pipeline-actions/utils.spec.ts",
    "content": "import { PipelineAction, PipelineStatus } from 'uiSrc/slices/interfaces'\nimport { getActionButtonState, ActionButtonState } from './utils'\n\ndescribe('getActionButtonState', () => {\n  describe('when action is in progress', () => {\n    it('should return disabled stop button when Stop action is in progress', () => {\n      const result = getActionButtonState(PipelineAction.Stop, undefined, false)\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: 'stop',\n      })\n    })\n\n    it('should return disabled start button when Start action is in progress', () => {\n      const result = getActionButtonState(\n        PipelineAction.Start,\n        undefined,\n        false,\n      )\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: 'start',\n      })\n    })\n\n    it('should ignore pipeline status when action is Stop', () => {\n      const result = getActionButtonState(\n        PipelineAction.Stop,\n        PipelineStatus.Stopped,\n        false,\n      )\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: 'stop',\n      })\n    })\n\n    it('should ignore pipeline status when action is Start', () => {\n      const result = getActionButtonState(\n        PipelineAction.Start,\n        PipelineStatus.Started,\n        false,\n      )\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: 'start',\n      })\n    })\n\n    it('should ignore disabled param when action is in progress', () => {\n      const result = getActionButtonState(\n        PipelineAction.Stop,\n        PipelineStatus.Started,\n        true,\n      )\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: 'stop',\n      })\n    })\n  })\n\n  describe('when no action is in progress - show start button', () => {\n    it.each([PipelineStatus.NotReady, PipelineStatus.Stopped])(\n      'should return start button with disabled=false for %s status',\n      (status) => {\n        const result = getActionButtonState(null, status, false)\n\n        expect(result).toEqual<ActionButtonState>({\n          disabled: false,\n          button: 'start',\n        })\n      },\n    )\n\n    it.each([PipelineStatus.NotReady, PipelineStatus.Stopped])(\n      'should return start button with disabled=true for %s status when disabled param is true',\n      (status) => {\n        const result = getActionButtonState(null, status, true)\n\n        expect(result).toEqual<ActionButtonState>({\n          disabled: true,\n          button: 'start',\n        })\n      },\n    )\n  })\n\n  describe('when no action is in progress - show stop button', () => {\n    it.each([\n      PipelineStatus.Ready,\n      PipelineStatus.Unknown,\n      PipelineStatus.Error,\n      PipelineStatus.Started,\n    ])(\n      'should return stop button with disabled=false for %s status',\n      (status) => {\n        const result = getActionButtonState(null, status, false)\n\n        expect(result).toEqual<ActionButtonState>({\n          disabled: false,\n          button: 'stop',\n        })\n      },\n    )\n\n    it.each([\n      PipelineStatus.Ready,\n      PipelineStatus.Unknown,\n      PipelineStatus.Error,\n      PipelineStatus.Started,\n    ])(\n      'should return stop button with disabled=true for %s status when disabled param is true',\n      (status) => {\n        const result = getActionButtonState(null, status, true)\n\n        expect(result).toEqual<ActionButtonState>({\n          disabled: true,\n          button: 'stop',\n        })\n      },\n    )\n  })\n\n  describe('when no action is in progress - transitioning statuses (always disabled)', () => {\n    it.each([\n      PipelineStatus.Stopping,\n      PipelineStatus.Starting,\n      PipelineStatus.Creating,\n      PipelineStatus.Updating,\n      PipelineStatus.Deleting,\n      PipelineStatus.Resetting,\n      PipelineStatus.Pending,\n    ])(\n      'should return disabled stop button for %s status regardless of disabled param',\n      (status) => {\n        const resultWithDisabledFalse = getActionButtonState(\n          null,\n          status,\n          false,\n        )\n        const resultWithDisabledTrue = getActionButtonState(null, status, true)\n\n        expect(resultWithDisabledFalse).toEqual<ActionButtonState>({\n          disabled: true,\n          button: 'stop',\n        })\n        expect(resultWithDisabledTrue).toEqual<ActionButtonState>({\n          disabled: true,\n          button: 'stop',\n        })\n      },\n    )\n  })\n\n  describe('when no action and undefined/unknown status', () => {\n    it('should return disabled null button when status is undefined', () => {\n      const result = getActionButtonState(null, undefined, false)\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: null,\n      })\n    })\n\n    it('should return disabled null button for unknown status value', () => {\n      const result = getActionButtonState(\n        null,\n        'some-unknown-status' as PipelineStatus,\n        false,\n      )\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: true,\n        button: null,\n      })\n    })\n  })\n\n  describe('Reset action does not affect button state', () => {\n    it('should use pipeline status when action is Reset', () => {\n      const result = getActionButtonState(\n        PipelineAction.Reset,\n        PipelineStatus.Started,\n        false,\n      )\n\n      expect(result).toEqual<ActionButtonState>({\n        disabled: false,\n        button: 'stop',\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/pipeline-actions/utils.ts",
    "content": "import { PipelineAction, PipelineStatus } from 'uiSrc/slices/interfaces'\n\nexport interface ActionButtonState {\n  button: 'start' | 'stop' | null\n  disabled: boolean\n}\n\nexport const getActionButtonState = (\n  action: PipelineAction | null,\n  pipelineStatus: PipelineStatus | undefined,\n  disabled: boolean,\n): ActionButtonState => {\n  if (action === PipelineAction.Stop) {\n    return {\n      disabled: true,\n      button: 'stop',\n    }\n  }\n\n  if (action === PipelineAction.Start) {\n    return {\n      disabled: true,\n      button: 'start',\n    }\n  }\n\n  switch (pipelineStatus) {\n    case PipelineStatus.NotReady: // v1 status\n    case PipelineStatus.Stopped:\n      return {\n        disabled,\n        button: 'start',\n      }\n    case PipelineStatus.Ready: // v1 status\n    case PipelineStatus.Unknown:\n    case PipelineStatus.Error:\n    case PipelineStatus.Started:\n      return {\n        disabled,\n        button: 'stop',\n      }\n    case PipelineStatus.Stopping:\n    case PipelineStatus.Starting:\n    case PipelineStatus.Creating:\n    case PipelineStatus.Updating:\n    case PipelineStatus.Deleting:\n    case PipelineStatus.Resetting:\n    case PipelineStatus.Pending:\n      return {\n        disabled: true,\n        button: 'stop',\n      }\n    default:\n      return {\n        disabled: true,\n        button: null,\n      }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/rdi-config-file-action-menu/RdiConfigFileActionMenu.spec.tsx",
    "content": "import { useFormikContext } from 'formik'\nimport React from 'react'\n\nimport { render, screen, userEvent, waitFor } from 'uiSrc/utils/test-utils'\nimport RdiConfigFileActionMenu from './RdiConfigFileActionMenu'\n\njest.mock('formik')\n\ndescribe('RdiConfigFileActionMenu', () => {\n  beforeEach(() => {\n    const mockUseFormikContext = {\n      handleSubmit: jest.fn(),\n      resetForm: jest.fn(),\n      // values: MOCK_RDI_PIPELINE_DATA,\n    }\n    ;(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)\n  })\n\n  it('should render', () => {\n    expect(render(<RdiConfigFileActionMenu />)).toBeTruthy()\n  })\n\n  it('should show menu with file actions when clicked', async () => {\n    render(<RdiConfigFileActionMenu />)\n    const actionBtn = screen.getByTestId(\n      'rdi-config-file-action-menu-trigger',\n    ) as HTMLElement\n    expect(actionBtn).toBeInTheDocument()\n\n    await userEvent.click(\n      screen.getByTestId('rdi-config-file-action-menu-trigger'),\n    )\n\n    await waitFor(() => screen.getByTestId('upload-file-btn'))\n    expect(screen.getByTestId('upload-file-btn')).toBeInTheDocument()\n    expect(screen.getByTestId('upload-pipeline-btn')).toBeInTheDocument()\n    expect(screen.getByTestId('download-pipeline-btn')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/rdi-config-file-action-menu/RdiConfigFileActionMenu.tsx",
    "content": "import React, { useState } from 'react'\nimport UploadModal from 'uiSrc/pages/rdi/pipeline-management/components/upload-modal/UploadModal'\nimport Download from 'uiSrc/pages/rdi/instance/components/download'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  UploadIcon,\n  MoreactionsIcon,\n  DownloadIcon,\n  SaveIcon,\n} from 'uiSrc/components/base/icons'\n\nimport { Menu } from '@redis-ui/components'\nimport DownloadFromServerModal from 'uiSrc/pages/rdi/pipeline-management/components/download-from-server-modal/DownloadFromServerModal'\n\nconst RdiConfigFileActionMenu = () => {\n  const [isOpen, setIsOpen] = useState(false)\n\n  const closeMenu = () => setIsOpen(false)\n\n  const button = (\n    <IconButton\n      role=\"button\"\n      icon={MoreactionsIcon}\n      data-testid=\"rdi-config-file-action-menu-trigger\"\n      aria-label=\"rdi-config-file-action-menu-trigger\"\n    />\n  )\n\n  return (\n    <Menu open={isOpen} onOpenChange={setIsOpen}>\n      <Menu.Trigger withButton>{button}</Menu.Trigger>\n      <Menu.Content>\n        <DownloadFromServerModal\n          onClose={closeMenu}\n          trigger={\n            <Menu.Content.Item\n              text=\"Download deployed pipeline\"\n              icon={DownloadIcon}\n              onClick={(e) => e.preventDefault()}\n              aria-labelledby=\"Upload pipeline button\"\n              data-testid=\"upload-pipeline-btn\"\n            />\n          }\n        />\n        <UploadModal\n          onClose={closeMenu}\n          trigger={\n            <Menu.Content.Item\n              text=\"Import pipeline from ZIP file\"\n              icon={UploadIcon}\n              onClick={(e) => e.preventDefault()}\n              aria-labelledby=\"Upload file button\"\n              data-testid=\"upload-file-btn\"\n            />\n          }\n        />\n        <Download\n          trigger={\n            <Menu.Content.Item\n              text=\"Save pipeline to ZIP file\"\n              icon={SaveIcon}\n              aria-labelledby=\"Download pipeline button\"\n              data-testid=\"download-pipeline-btn\"\n            />\n          }\n        />\n      </Menu.Content>\n    </Menu>\n  )\n}\n\nexport default RdiConfigFileActionMenu\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/components/rdi-config-file-action-menu/index.ts",
    "content": "import RdiConfigFileActionMenu from './RdiConfigFileActionMenu'\n\nexport default RdiConfigFileActionMenu\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/header/index.ts",
    "content": "import RdiPipelineHeader from './RdiPipelineHeader'\n\nexport default RdiPipelineHeader\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/components/index.ts",
    "content": "import Download from './download/Download'\nimport RdiPipelineHeader from './header'\n\nexport { Download, RdiPipelineHeader }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/index.ts",
    "content": "import RdiInstancePage from './InstancePage'\n\nexport default RdiInstancePage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/instance/styles.module.scss",
    "content": ".page {\n  height: 100%;\n  padding-bottom: 16px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/PipelineManagementPage.spec.tsx",
    "content": "import React from 'react'\n\nimport reactRouterDom, { BrowserRouter } from 'react-router-dom'\nimport { instance, mock } from 'ts-mockito'\nimport { useFormikContext } from 'formik'\nimport {\n  render,\n  cleanup,\n  mockedStore,\n  createMockedStore,\n  expectActionsToContain,\n  expectActionsToNotContain,\n} from 'uiSrc/utils/test-utils'\nimport {\n  appContextPipelineManagement,\n  setLastPageContext,\n  setLastPipelineManagementPage,\n} from 'uiSrc/slices/app/context'\nimport { PageNames, Pages } from 'uiSrc/constants'\nimport { MOCK_RDI_PIPELINE_DATA } from 'uiSrc/mocks/data/rdi'\nimport { getPipeline } from 'uiSrc/slices/rdi/pipeline'\nimport PipelineManagementPage, { Props } from './PipelineManagementPage'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextPipelineManagement: jest.fn().mockReturnValue({\n    lastViewedPage: '',\n  }),\n}))\n\njest.mock('formik')\n\nconst MOCK_RDI_ID = 'id1'\nconst MOCK_RDI_ID2 = 'id2'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n})\n\nconst renderPipelineManagement = (props: Props) =>\n  render(\n    <BrowserRouter>\n      <PipelineManagementPage {...props} />\n    </BrowserRouter>,\n    { store },\n  )\n\ndescribe('PipelineManagementPage', () => {\n  beforeEach(() => {\n    const mockUseFormikContext = {\n      setFieldValue: jest.fn,\n      values: MOCK_RDI_PIPELINE_DATA,\n    }\n    ;(useFormikContext as jest.Mock).mockReturnValue(mockUseFormikContext)\n  })\n\n  it('should render', () => {\n    expect(renderPipelineManagement(instance(mockedProps))).toBeTruthy()\n  })\n\n  it('should redirect to the config tab by default', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    reactRouterDom.useLocation = jest.fn().mockReturnValue({\n      pathname: Pages.rdiPipelineManagement('rdiInstanceId'),\n    })\n\n    renderPipelineManagement(instance(mockedProps))\n\n    expect(pushMock).toBeCalledWith(Pages.rdiPipelineConfig('rdiInstanceId'))\n  })\n\n  it('should redirect to the prev page from context', () => {\n    ;(appContextPipelineManagement as jest.Mock).mockReturnValueOnce({\n      lastViewedPage: Pages.rdiPipelineConfig('rdiInstanceId'),\n    })\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    reactRouterDom.useLocation = jest.fn().mockReturnValue({\n      pathname: Pages.rdiPipelineManagement('rdiInstanceId'),\n    })\n\n    renderPipelineManagement(instance(mockedProps))\n\n    expect(pushMock).toBeCalledWith(Pages.rdiPipelineConfig('rdiInstanceId'))\n  })\n\n  it('should save proper page on unmount', () => {\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdiPipelineConfig('rdiInstanceId') })\n\n    const { unmount } = renderPipelineManagement(instance(mockedProps))\n\n    unmount()\n    const expectedActions = [\n      getPipeline(),\n      setLastPageContext(PageNames.rdiPipelineManagement),\n      setLastPipelineManagementPage(Pages.rdiPipelineConfig('rdiInstanceId')),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n\n  describe('pipeline state', () => {\n    it('should fetch pipeline when context is empty', () => {\n      ;(appContextPipelineManagement as jest.Mock).mockReturnValueOnce({\n        lastViewedPage: '',\n      })\n      reactRouterDom.useParams = jest.fn().mockReturnValue({\n        rdiInstanceId: MOCK_RDI_ID,\n      })\n\n      renderPipelineManagement(instance(mockedProps))\n\n      expectActionsToContain(store.getActions(), [getPipeline()])\n    })\n\n    it('should fetch pipeline when context stores different visited RDI instance', () => {\n      ;(appContextPipelineManagement as jest.Mock).mockReturnValueOnce({\n        lastViewedPage: '',\n      })\n      reactRouterDom.useParams = jest.fn().mockReturnValue({\n        rdiInstanceId: MOCK_RDI_ID2,\n      })\n\n      renderPipelineManagement(instance(mockedProps))\n\n      expectActionsToContain(store.getActions(), [getPipeline()])\n    })\n\n    it('should not fetch pipeline when context stores the same visited RDI instance', () => {\n      ;(appContextPipelineManagement as jest.Mock).mockReturnValueOnce({\n        lastViewedPage: Pages.rdiPipelineConfig(MOCK_RDI_ID),\n      })\n      reactRouterDom.useParams = jest.fn().mockReturnValue({\n        rdiInstanceId: MOCK_RDI_ID,\n      })\n\n      renderPipelineManagement(instance(mockedProps))\n\n      expectActionsToNotContain(store.getActions(), [getPipeline()])\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/PipelineManagementPage.styles.tsx",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\n// Ratio for Navigation/Content is 1*:4\n// * - with some min/max limits\nexport const NavigationContainer = styled(FlexItem).attrs({\n  grow: 1,\n})`\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n  border-right: 1px solid\n    ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.border.informative100};\n  min-width: 300px;\n  max-width: 450px;\n`\n\nexport const ContentContainer = styled(FlexItem).attrs({\n  grow: 4,\n})``\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/PipelineManagementPage.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { useHistory, useLocation, useParams } from 'react-router-dom'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { IRoute, PageNames, Pages } from 'uiSrc/constants'\nimport { connectedInstanceSelector } from 'uiSrc/slices/rdi/instances'\nimport {\n  fetchRdiPipeline,\n  fetchRdiPipelineJobFunctions,\n  fetchRdiPipelineSchema,\n} from 'uiSrc/slices/rdi/pipeline'\nimport {\n  appContextPipelineManagement,\n  setLastPageContext,\n  setLastPipelineManagementPage,\n} from 'uiSrc/slices/app/context'\nimport { formatLongName, setTitle } from 'uiSrc/utils'\nimport SourcePipelineDialog from 'uiSrc/pages/rdi/pipeline-management/components/source-pipeline-dialog'\nimport Navigation from 'uiSrc/pages/rdi/pipeline-management/components/navigation'\n\nimport { removeInfiniteNotification } from 'uiSrc/slices/app/notifications'\nimport { InfiniteMessagesIds } from 'uiSrc/components/notifications/components'\nimport PipelinePageRouter from './PipelineManagementPageRouter'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport {\n  ContentContainer,\n  NavigationContainer,\n} from './PipelineManagementPage.styles'\n\nexport interface Props {\n  routes: IRoute[]\n}\n\nconst PipelineManagementPage = ({ routes = [] }: Props) => {\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const { lastViewedPage } = useSelector(appContextPipelineManagement)\n  const { name: connectedRdiInstanceName } = useSelector(\n    connectedInstanceSelector,\n  )\n\n  const pathnameRef = useRef<string>('')\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { pathname } = useLocation()\n\n  const rdiInstanceName = formatLongName(connectedRdiInstanceName, 33, 0, '...')\n  setTitle(`${rdiInstanceName} - Pipeline Management`)\n\n  useEffect(() => {\n    if (\n      !lastViewedPage?.startsWith(Pages.rdiPipelineManagement(rdiInstanceId))\n    ) {\n      dispatch(fetchRdiPipeline(rdiInstanceId))\n    }\n    dispatch(fetchRdiPipelineSchema(rdiInstanceId))\n    dispatch(fetchRdiPipelineJobFunctions(rdiInstanceId))\n  }, [])\n\n  useEffect(\n    () => () => {\n      dispatch(setLastPageContext(PageNames.rdiPipelineManagement))\n      dispatch(setLastPipelineManagementPage(pathnameRef.current))\n      dispatch(\n        removeInfiniteNotification(InfiniteMessagesIds.pipelineDeploySuccess),\n      )\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (pathname === Pages.rdiPipelineManagement(rdiInstanceId)) {\n      if (pathnameRef.current && pathnameRef.current !== lastViewedPage) {\n        history.push(pathnameRef.current)\n        return\n      }\n\n      // restore from context\n      if (lastViewedPage) {\n        history.push(lastViewedPage)\n        return\n      }\n\n      history.push(Pages.rdiPipelineConfig(rdiInstanceId))\n    }\n\n    pathnameRef.current =\n      pathname === Pages.rdiPipelineManagement(rdiInstanceId) ? '' : pathname\n  }, [pathname, lastViewedPage])\n\n  return (\n    <Row>\n      <NavigationContainer>\n        <Navigation />\n      </NavigationContainer>\n      <SourcePipelineDialog />\n      <ContentContainer>\n        <PipelinePageRouter routes={routes} />\n      </ContentContainer>\n    </Row>\n  )\n}\n\nexport default PipelineManagementPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/PipelineManagementPageRouter.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { IRoute } from 'uiSrc/constants'\nimport PipelinePageRouter from './PipelineManagementPageRouter'\n\nconst mockedRoutes = [\n  {\n    path: '/page',\n  },\n]\n\ndescribe('PipelinePageRouter', () => {\n  it('should render', () => {\n    expect(\n      render(<PipelinePageRouter routes={mockedRoutes as IRoute[]} />, {\n        withRouter: true,\n      }),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/PipelineManagementPageRouter.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { IRoute } from 'uiSrc/constants'\n\nexport interface Props {\n  routes: IRoute[]\n}\nconst PipelineManagementPageRouter = ({ routes }: Props) => (\n  <Switch>\n    {routes.map((route, i) => (\n      <RouteWithSubRoutes key={`pipeline-management-route-${i}`} {...route} />\n    ))}\n  </Switch>\n)\n\nexport default React.memo(PipelineManagementPageRouter)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/download-from-server-modal/DownloadFromServerModal.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  expectActionsToContain,\n  mockedStore,\n  render,\n  screen,\n  userEvent,\n} from 'uiSrc/utils/test-utils'\nimport { getPipeline, rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport DownloadFromServerModal from './DownloadFromServerModal'\nimport { Button } from 'uiSrc/components/base/forms/buttons'\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: {\n      jobs: [{ name: 'job1', value: 'value' }],\n    },\n  }),\n}))\n\njest.mock('formik', () => ({\n  ...jest.requireActual('formik'),\n  useFormikContext: jest.fn().mockReturnValue({\n    values: {\n      config: 'value',\n      jobs: [\n        { name: 'job1', value: 'value' },\n        { name: 'job2', value: 'value' },\n      ],\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst trigger = <Button data-testid=\"trigger-btn\">trigger</Button>\nconst renderDownloadFromServerModal = () =>\n  render(<DownloadFromServerModal trigger={trigger} />)\n\ndescribe('FetchPipelinePopover', () => {\n  it('should render', () => {\n    expect(renderDownloadFromServerModal()).toBeTruthy()\n  })\n\n  it('should open confirmation message', async () => {\n    renderDownloadFromServerModal()\n\n    expect(screen.queryByTestId('upload-confirm-btn')).not.toBeInTheDocument()\n\n    await userEvent.click(screen.getByTestId('trigger-btn'))\n\n    expect(screen.queryByTestId('upload-confirm-btn')).toBeInTheDocument()\n  })\n\n  it('should call proper actions', async () => {\n    renderDownloadFromServerModal()\n\n    await userEvent.click(screen.getByTestId('trigger-btn'))\n\n    await userEvent.click(screen.getByTestId('upload-confirm-btn'))\n\n    const expectedActions = [getPipeline()]\n    expectActionsToContain(store.getActions(), expectedActions)\n  })\n\n  it('should call proper telemetry event', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    renderDownloadFromServerModal()\n\n    await userEvent.click(screen.getByTestId('trigger-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_UPLOAD_FROM_SERVER_CLICKED,\n      eventData: {\n        id: 'rdiInstanceId',\n        jobsNumber: 1,\n      },\n    })\n  })\n\n  it('should render disabled trigger btn', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(() => ({\n      loading: true,\n    }))\n\n    renderDownloadFromServerModal()\n\n    expect(screen.getByTestId('trigger-btn')).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/download-from-server-modal/DownloadFromServerModal.tsx",
    "content": "import React, { useState } from 'react'\nimport { Button, Modal, TextButton } from '@redis-ui/components'\nimport { SaveIcon } from '@redis-ui/icons'\nimport { Download } from 'uiSrc/pages/rdi/instance/components'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  fetchRdiPipeline,\n  rdiPipelineSelector,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { useParams } from 'react-router-dom'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nexport interface Props {\n  trigger?: React.ReactElement\n  onClose?: () => void\n}\n\nconst DownloadFromServerModal = (props: Props) => {\n  const { trigger, onClose } = props\n\n  const { loading, data } = useSelector(rdiPipelineSelector)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const [isOpen, setIsOpen] = useState(false)\n\n  const dispatch = useDispatch()\n\n  const handleDownloadFromServer = () => {\n    dispatch(\n      fetchRdiPipeline(rdiInstanceId, () => {\n        onClose?.()\n      }),\n    )\n  }\n\n  const onOpenChangeHandler = (open: boolean) => {\n    if (!open) {\n      onClose?.()\n    }\n\n    setIsOpen(open)\n  }\n\n  const handleTrigger = (e: React.MouseEvent) => {\n    setIsOpen(true)\n    trigger?.props?.onClick?.(e)\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_UPLOAD_FROM_SERVER_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n        jobsNumber: data?.jobs?.length || 'none',\n      },\n    })\n  }\n\n  const button = trigger\n    ? React.cloneElement(trigger, {\n        disabled: loading,\n        onClick: handleTrigger,\n      })\n    : null\n\n  return (\n    <Modal.Compose open={isOpen} onOpenChange={onOpenChangeHandler}>\n      {button && <Modal.Trigger>{button}</Modal.Trigger>}\n      <Modal.Content.Compose persistent>\n        <Modal.Content.Close />\n        <Modal.Content.Header title=\"Download a pipeline from the server\" />\n        <Modal.Content.Body.Compose>\n          When downloading the pipeline configuration from the server, it will\n          overwrite the existing one displayed in Redis Insight.\n        </Modal.Content.Body.Compose>\n        <Modal.Content.Footer.Compose>\n          <Download\n            onClose={onClose}\n            trigger={\n              <TextButton>\n                <Button.Icon icon={SaveIcon} />\n                Save to file\n              </TextButton>\n            }\n          />\n          <Modal.Content.Footer.Group>\n            <SecondaryButton size=\"l\" onClick={onClose}>\n              Cancel\n            </SecondaryButton>\n            <PrimaryButton\n              size=\"l\"\n              onClick={handleDownloadFromServer}\n              loading={loading}\n              data-testid=\"upload-confirm-btn\"\n            >\n              Download from server\n            </PrimaryButton>\n          </Modal.Content.Footer.Group>\n        </Modal.Content.Footer.Compose>\n      </Modal.Content.Compose>\n    </Modal.Compose>\n  )\n}\n\nexport default DownloadFromServerModal\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/dry-run-job-commands/DryRunJobCommands.spec.tsx",
    "content": "import React from 'react'\nimport { act, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { rdiDryRunJobSelector } from 'uiSrc/slices/rdi/dryRun'\nimport DryRunJobCommands from './DryRunJobCommands'\n\njest.mock('uiSrc/slices/rdi/dryRun', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/dryRun'),\n  rdiDryRunJobSelector: jest.fn().mockReturnValue({\n    results: null,\n  }),\n}))\n\ndescribe('DryRunJobCommands', () => {\n  it('should render', () => {\n    expect(render(<DryRunJobCommands />)).toBeTruthy()\n  })\n\n  it('should render no commands message', async () => {\n    const rdiDryRunJobSelectorMock = jest.fn().mockReturnValue({\n      results: { output: {} },\n    })\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementationOnce(\n      rdiDryRunJobSelectorMock,\n    )\n\n    await act(async () => {\n      render(<DryRunJobCommands />)\n    })\n\n    expect(screen.getByTestId('commands-output')).toHaveTextContent(\n      'No Redis commands provided by the server.',\n    )\n  })\n\n  it('should render no commands message if there is no commands', async () => {\n    const rdiDryRunJobSelectorMock = jest.fn().mockReturnValue({\n      results: {\n        output: [{ name: 1 }],\n      },\n    })\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementationOnce(\n      rdiDryRunJobSelectorMock,\n    )\n\n    await act(async () => {\n      render(<DryRunJobCommands />)\n    })\n\n    expect(screen.getByTestId('commands-output')).toHaveTextContent(\n      'No Redis commands provided by the server.',\n    )\n  })\n\n  it('should render transformations', async () => {\n    const rdiDryRunJobSelectorMock = jest.fn().mockReturnValue({\n      results: {\n        output: [\n          {\n            connection: 'target',\n            commands: [\n              'HSET person:Yossi:Shirizli FNAME Yossi LAST_NAME Shirizli COUNTRY IL',\n            ],\n          },\n        ],\n      },\n    })\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementationOnce(\n      rdiDryRunJobSelectorMock,\n    )\n\n    await act(async () => {\n      render(<DryRunJobCommands target=\"target\" />)\n    })\n    expect(screen.getByTestId('commands-output')).toHaveTextContent(\n      'HSET person:Yossi:Shirizli FNAME Yossi LAST_NAME Shirizli COUNTRY IL',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/dry-run-job-commands/DryRunJobCommands.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport parse from 'html-react-parser'\nimport { monaco } from 'react-monaco-editor'\n\nimport { CodeBlock } from 'uiSrc/components'\nimport { rdiDryRunJobSelector } from 'uiSrc/slices/rdi/dryRun'\nimport { MonacoLanguage } from 'uiSrc/constants'\n\nexport interface Props {\n  target?: string\n}\n\nconst NO_COMMANDS_MESSAGE = 'No Redis commands provided by the server.'\n\nconst DryRunJobCommands = ({ target }: Props) => {\n  const { results } = useSelector(rdiDryRunJobSelector)\n  const [commands, setCommands] = useState<string>('')\n\n  useEffect(() => {\n    if (!results) {\n      return\n    }\n\n    try {\n      const targetCommands = results?.output?.find(\n        (el) => el.connection === target,\n      )?.commands\n\n      if (!targetCommands) {\n        setCommands(NO_COMMANDS_MESSAGE)\n        return\n      }\n      monaco.editor\n        .colorize(\n          (targetCommands ?? []).join('\\n').trim(),\n          MonacoLanguage.Redis,\n          {},\n        )\n        .then((data) => {\n          setCommands(data)\n        })\n    } catch (e) {\n      setCommands(NO_COMMANDS_MESSAGE)\n    }\n  }, [results, target])\n\n  return (\n    <div className=\"rdi-dry-run__codeBlock\" data-testid=\"commands-output\">\n      <CodeBlock className=\"rdi-dry-run__code\">{parse(commands)}</CodeBlock>\n    </div>\n  )\n}\n\nexport default DryRunJobCommands\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/dry-run-job-commands/index.ts",
    "content": "import DryRunJobCommands from './DryRunJobCommands'\n\nexport default DryRunJobCommands\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/dry-run-job-transformations/DryRunJobTransformations.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport { rdiDryRunJobSelector } from 'uiSrc/slices/rdi/dryRun'\nimport DryRunJobTransformations from './DryRunJobTransformations'\n\njest.mock('uiSrc/slices/rdi/dryRun', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/dryRun'),\n  rdiDryRunJobSelector: jest.fn().mockReturnValue({\n    results: null,\n  }),\n}))\n\ndescribe('DryRunJobTransformations', () => {\n  it('should render', () => {\n    expect(render(<DryRunJobTransformations />)).toBeTruthy()\n  })\n\n  it('should render no transformation message', () => {\n    const rdiDryRunJobSelectorMock = jest.fn().mockReturnValue({\n      results: {},\n    })\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementationOnce(\n      rdiDryRunJobSelectorMock,\n    )\n\n    render(<DryRunJobTransformations />)\n    expect(screen.getByTestId('transformations-output')).toHaveTextContent(\n      'No transformation results provided by the server.',\n    )\n  })\n\n  it('should render transformations', () => {\n    const rdiDryRunJobSelectorMock = jest.fn().mockReturnValue({\n      results: { transformation: [] },\n    })\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementationOnce(\n      rdiDryRunJobSelectorMock,\n    )\n\n    render(<DryRunJobTransformations />)\n    expect(screen.getByTestId('transformations-output')).toHaveTextContent('[]')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/dry-run-job-transformations/DryRunJobTransformations.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { rdiDryRunJobSelector } from 'uiSrc/slices/rdi/dryRun'\nimport MonacoJson from 'uiSrc/components/monaco-editor/components/monaco-json'\n\nconst NO_TRANSFORMATION_MESSAGE =\n  'No transformation results provided by the server.'\n\nconst DryRunJobTransformations = () => {\n  const { results } = useSelector(rdiDryRunJobSelector)\n\n  const [transformations, setTransformations] = useState('')\n\n  useEffect(() => {\n    if (!results) {\n      return\n    }\n\n    try {\n      const transformations = JSON.stringify(results?.transformation, null, 2)\n      setTransformations(transformations || NO_TRANSFORMATION_MESSAGE)\n    } catch (e) {\n      setTransformations(NO_TRANSFORMATION_MESSAGE)\n    }\n  }, [results])\n\n  return (\n    <>\n      <MonacoJson\n        readOnly\n        value={transformations}\n        wrapperClassName=\"rdi-dry-run__transformationsCode\"\n        data-testid=\"transformations-output\"\n        fullHeight\n      />\n    </>\n  )\n}\n\nexport default DryRunJobTransformations\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/dry-run-job-transformations/index.ts",
    "content": "import DryRunJobTransformations from './DryRunJobTransformations'\n\nexport default DryRunJobTransformations\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/jobs-panel/Panel.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\n\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { Text } from 'uiSrc/components/base/text'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { dryRunJob, rdiDryRunJobSelector } from 'uiSrc/slices/rdi/dryRun'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport JobsPanel, { Props } from './Panel'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/dryRun', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/dryRun'),\n  rdiDryRunJobSelector: jest.fn().mockReturnValue({\n    loading: false,\n    results: null,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('JobsPanel', () => {\n  it('should render', () => {\n    expect(render(<JobsPanel {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should have default value', () => {\n    render(<JobsPanel {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('input-value')).toHaveValue('{\\n}')\n  })\n\n  it('should call onClose', () => {\n    const mockOnClose = jest.fn()\n    render(<JobsPanel {...instance(mockedProps)} onClose={mockOnClose} />)\n\n    fireEvent.click(screen.getByTestId('close-dry-run-btn'))\n    expect(mockOnClose).toBeCalled()\n  })\n\n  it('should render run btn with proper properties', () => {\n    render(<JobsPanel {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('dry-run-btn')).not.toBeDisabled()\n\n    // set invalid json value\n    fireEvent.change(screen.getByTestId('input-value'), {\n      target: { value: 'test' },\n    })\n\n    expect(screen.getByTestId('dry-run-btn')).toBeDisabled()\n\n    // set valid json value\n    fireEvent.change(screen.getByTestId('input-value'), {\n      target: { value: '[]' },\n    })\n\n    expect(screen.getByTestId('dry-run-btn')).not.toBeDisabled()\n  })\n\n  it('should call proper telemetry events', () => {\n    render(<JobsPanel {...instance(mockedProps)} />)\n\n    fireEvent.change(screen.getByTestId('input-value'), {\n      target: { value: '[]' },\n    })\n    fireEvent.click(screen.getByTestId('dry-run-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_TEST_JOB_RUN,\n      eventData: {\n        id: 'rdiInstanceId',\n      },\n    })\n  })\n\n  it('should render proper tab', () => {\n    const { queryByTestId } = render(<JobsPanel {...instance(mockedProps)} />)\n\n    expect(queryByTestId('transformations-output')).toBeInTheDocument()\n    expect(queryByTestId('commands-output')).not.toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText('Job output'))\n\n    expect(queryByTestId('transformations-output')).not.toBeInTheDocument()\n    expect(queryByTestId('commands-output')).toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText('Transformation output'))\n\n    expect(queryByTestId('transformations-output')).toBeInTheDocument()\n    expect(queryByTestId('commands-output')).not.toBeInTheDocument()\n  })\n\n  it('should fetch dry run job results', () => {\n    render(<JobsPanel {...instance(mockedProps)} />)\n\n    fireEvent.change(screen.getByTestId('input-value'), {\n      target: { value: '[]' },\n    })\n    fireEvent.click(screen.getByTestId('dry-run-btn'))\n\n    const expectedActions = [dryRunJob()]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should not render target select if there is no results', async () => {\n    const { queryByTestId } = render(<JobsPanel {...instance(mockedProps)} />)\n\n    expect(queryByTestId('target-select')).not.toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText('Job output'))\n\n    expect(queryByTestId('target-select')).not.toBeInTheDocument()\n  })\n\n  it('should not render target select if there is one target', async () => {\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementation(() => ({\n      loading: false,\n      results: {\n        transformation: {},\n        output: [{ connection: 'target', commands: ['some command'] }],\n      },\n    }))\n    const { queryByTestId } = render(<JobsPanel {...instance(mockedProps)} />)\n\n    expect(queryByTestId('target-select')).not.toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText('Job output'))\n\n    expect(queryByTestId('target-select')).not.toBeInTheDocument()\n  })\n\n  it('should render target select if there is more then one target', async () => {\n    ;(rdiDryRunJobSelector as jest.Mock).mockImplementation(() => ({\n      loading: false,\n      results: {\n        transformation: {},\n        output: [\n          { connection: 'target', commands: ['some command'] },\n          { connection: 'target2', commands: ['some command'] },\n        ],\n      },\n    }))\n    const { queryByTestId } = render(<JobsPanel {...instance(mockedProps)} />)\n\n    expect(queryByTestId('target-select')).not.toBeInTheDocument()\n\n    fireEvent.mouseDown(screen.getByText('Job output'))\n\n    expect(queryByTestId('target-select')).toBeInTheDocument()\n  })\n\n  it('should render error notification', () => {\n    render(\n      <JobsPanel\n        {...instance(mockedProps)}\n        name=\"jobName\"\n        job={'hsources:incorrect\\n target:'}\n      />,\n    )\n\n    fireEvent.change(screen.getByTestId('input-value'), {\n      target: { value: '[]' },\n    })\n\n    fireEvent.click(screen.getByTestId('dry-run-btn'))\n\n    const expectedActions = [\n      addErrorNotification({\n        response: {\n          data: {\n            message: (\n              <>\n                <Text>JobName has an invalid structure.</Text>\n                <Text>\n                  end of the stream or a document separator is expected\n                </Text>\n              </>\n            ),\n          },\n        },\n      } as AxiosError),\n    ]\n\n    expect(store.getActions().slice(0 - expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/jobs-panel/Panel.tsx",
    "content": "import React, { useEffect, useState } from 'react'\n\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { isArray, upperFirst } from 'lodash'\n\nimport * as keys from 'uiSrc/constants/keys'\nimport { PipelineJobsTabs } from 'uiSrc/slices/interfaces/rdi'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  rdiDryRunJob,\n  rdiDryRunJobSelector,\n  setInitialDryRunJob,\n} from 'uiSrc/slices/rdi/dryRun'\nimport MonacoJson from 'uiSrc/components/monaco-editor/components/monaco-json'\nimport DryRunJobCommands from 'uiSrc/pages/rdi/pipeline-management/components/dry-run-job-commands'\nimport DryRunJobTransformations from 'uiSrc/pages/rdi/pipeline-management/components/dry-run-job-transformations'\nimport { createAxiosError, formatLongName, yamlToJson } from 'uiSrc/utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  PlayFilledIcon,\n  CancelSlimIcon,\n  ExtendIcon,\n  ShrinkIcon,\n} from 'uiSrc/components/base/icons'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { RiTooltip } from 'uiSrc/components'\nimport {\n  RiSelect,\n  RiSelectOption,\n  defaultValueRender,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { DryRunPanelContainer } from 'uiSrc/pages/rdi/pipeline-management/components/jobs-panel/styles'\nimport { Button, TextButton } from '@redis-ui/components'\n\nexport interface Props {\n  job: string\n  name: string\n  onClose: () => void\n}\n\nconst getTargetOption = (value: string) => {\n  const formattedValue = formatLongName(value)\n\n  return {\n    value,\n    inputDisplay: formattedValue,\n    dropdownDisplay: formattedValue,\n    'data-test-subj': `target-option-${value}`,\n  }\n}\n\nconst DryRunJobPanel = (props: Props) => {\n  const { job, name, onClose } = props\n  const { loading: isDryRunning, results } = useSelector(rdiDryRunJobSelector)\n\n  const [isFullScreen, setIsFullScreen] = useState<boolean>(false)\n  const [selectedTab, changeSelectedTab] = useState<PipelineJobsTabs>(\n    PipelineJobsTabs.Transformations,\n  )\n  const [input, setInput] = useState<string>('{\\n}')\n  const [isFormValid, setIsFormValid] = useState<boolean>(false)\n  const [targetOptions, setTargetOptions] = useState<RiSelectOption[]>([])\n  const [selectedTarget, setSelectedTarget] = useState<string>()\n\n  const dispatch = useDispatch()\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  useEffect(() => {\n    window.addEventListener('keydown', handleEscFullScreen)\n    return () => {\n      window.removeEventListener('keydown', handleEscFullScreen)\n    }\n  }, [isFullScreen])\n\n  useEffect(() => {\n    try {\n      JSON.parse(input)\n      setIsFormValid(true)\n    } catch (e) {\n      setIsFormValid(false)\n    }\n  }, [input])\n\n  // componentWillUnmount\n  useEffect(\n    () => () => {\n      dispatch(setInitialDryRunJob())\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (!results?.output || !isArray(results.output)) return\n\n    const targets = results.output\n      .filter(({ connection }) => connection)\n      .map(({ connection }) => getTargetOption(connection))\n    setTargetOptions(targets)\n    setSelectedTarget(targets[0]?.value)\n  }, [results])\n\n  const handleEscFullScreen = (event: KeyboardEvent) => {\n    if (event.key === keys.ESCAPE && isFullScreen) {\n      handleFullScreen()\n    }\n  }\n\n  const handleFullScreen = () => {\n    setIsFullScreen((value) => !value)\n  }\n\n  const handleDryRun = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_TEST_JOB_RUN,\n      eventData: {\n        id: rdiInstanceId,\n      },\n    })\n    const JSONJob = yamlToJson(job, (msg) => {\n      dispatch(\n        addErrorNotification(\n          createAxiosError({\n            message: (\n              <>\n                <Text>{`${upperFirst(name)} has an invalid structure.`}</Text>\n                <Text>{msg}</Text>\n              </>\n            ),\n          }),\n        ),\n      )\n    })\n    if (!JSONJob) {\n      return\n    }\n    const JSONInput = JSON.parse(input)\n    const formattedValue = isArray(JSONInput) ? JSONInput : [JSONInput]\n\n    dispatch(rdiDryRunJob(rdiInstanceId, formattedValue, JSONJob))\n  }\n\n  const isSelectAvailable =\n    selectedTab === PipelineJobsTabs.Output &&\n    !!results?.output &&\n    results?.output?.length > 1 &&\n    !!targetOptions.length\n\n  const tabs: TabInfo[] = [\n    {\n      value: PipelineJobsTabs.Transformations,\n      label: (\n        <RiTooltip\n          content={\n            <Text>\n              Displays the results of the transformations you defined. The data\n              is presented in JSON format.\n              <br />\n              No data is written to the target database.\n            </Text>\n          }\n          data-testid=\"transformation-output-tooltip\"\n        >\n          <Text>Transformation output</Text>\n        </RiTooltip>\n      ),\n      content: null,\n    },\n    {\n      value: PipelineJobsTabs.Output,\n      label: (\n        <RiTooltip\n          content={\n            <Text>\n              Displays the list of Redis commands that will be generated based\n              on your job details.\n              <br />\n              No data is written to the target database.\n            </Text>\n          }\n          data-testid=\"job-output-tooltip\"\n        >\n          <Text>Job output</Text>\n        </RiTooltip>\n      ),\n      content: null,\n    },\n  ]\n\n  const handleTabChange = (name: string) => {\n    if (selectedTab === name) return\n    changeSelectedTab(name as PipelineJobsTabs)\n  }\n\n  return (\n    <DryRunPanelContainer\n      grow\n      gap=\"l\"\n      data-testid=\"dry-run-panel\"\n      isFullScreen={isFullScreen}\n    >\n      {/* Header */}\n      <FlexItem>\n        <Row align=\"center\" justify=\"between\">\n          <Title size=\"L\" color=\"primary\">\n            Test transformation logic\n          </Title>\n          <FlexItem>\n            <Row gap=\"s\">\n              <IconButton\n                icon={isFullScreen ? ShrinkIcon : ExtendIcon}\n                aria-label=\"toggle fullscrenn dry run panel\"\n                onClick={handleFullScreen}\n                data-testid=\"fullScreen-dry-run-btn\"\n              />\n              <IconButton\n                icon={CancelSlimIcon}\n                aria-label=\"close dry run panel\"\n                onClick={onClose}\n                data-testid=\"close-dry-run-btn\"\n              />\n            </Row>\n          </FlexItem>\n        </Row>\n        <Text>Add input data to test the transformation logic.</Text>\n      </FlexItem>\n      {/* Input section */}\n      <FlexItem>\n        <Col gap=\"s\">\n          <Title size=\"S\" color=\"primary\">\n            Input\n          </Title>\n          <MonacoJson\n            value={input}\n            onChange={setInput}\n            disabled={false}\n            data-testid=\"input-value\"\n            fullHeight\n          />\n          <Row responsive justify=\"end\">\n            <FlexItem>\n              <RiTooltip\n                content={isFormValid ? null : 'Input should have JSON format'}\n                position=\"top\"\n              >\n                <TextButton\n                  variant=\"primary-inline\"\n                  onClick={handleDryRun}\n                  disabled={isDryRunning || !isFormValid}\n                  data-testid=\"dry-run-btn\"\n                >\n                  <Button.Icon icon={PlayFilledIcon} customSize=\"12\" />\n                  Dry run\n                </TextButton>\n              </RiTooltip>\n            </FlexItem>\n          </Row>\n        </Col>\n      </FlexItem>\n      {/* Results section */}\n      <FlexItem grow>\n        <Col gap=\"m\">\n          {isSelectAvailable && (\n            <RiSelect\n              options={targetOptions}\n              valueRender={defaultValueRender}\n              value={selectedTarget}\n              onChange={(value) => setSelectedTarget(value)}\n              data-testid=\"target-select\"\n            />\n          )}\n          <FlexItem grow>\n            <Tabs\n              tabs={tabs}\n              value={selectedTab}\n              onChange={handleTabChange}\n              data-testid=\"pipeline-jobs-tabs\"\n            />\n            {selectedTab === PipelineJobsTabs.Transformations && (\n              <DryRunJobTransformations />\n            )}\n            {selectedTab === PipelineJobsTabs.Output && (\n              <DryRunJobCommands target={selectedTarget} />\n            )}\n          </FlexItem>\n        </Col>\n      </FlexItem>\n    </DryRunPanelContainer>\n  )\n}\n\nexport default DryRunJobPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/jobs-panel/index.ts",
    "content": "import DryRunJobPanel from './Panel'\n\nexport default DryRunJobPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/jobs-panel/styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport React from 'react'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const DryRunPanelContainer = styled(Col)<\n  React.ComponentProps<typeof Col> & { isFullScreen?: boolean }\n>`\n  border-left: 1px solid\n    ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.border.informative100};\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.section.bgColor};\n  ${({ isFullScreen }) =>\n    isFullScreen\n      ? css`\n          position: absolute;\n          top: 0;\n          left: 0;\n          width: 100%;\n          height: 100%;\n          z-index: 15;\n        `\n      : css`\n          width: 524px;\n          overflow: auto;\n        `}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/Navigation.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\n\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport Navigation from './Navigation'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n}))\n\njest.mock('formik', () => ({\n  ...jest.requireActual('formik'),\n  useFormikContext: jest.fn().mockReturnValue({\n    values: {\n      config: 'value',\n      jobs: [\n        { name: 'job1', value: 'value' },\n        { name: 'job2', value: 'value' },\n      ],\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(rdiPipelineSelector as jest.Mock).mockReturnValue(\n    initialStateDefault.rdi.pipeline,\n  )\n})\n\ndescribe('Navigation', () => {\n  it('should render', () => {\n    expect(render(<Navigation />)).toBeTruthy()\n  })\n\n  it('should not show nav when pipeline is loading', () => {\n    render(<Navigation />)\n\n    expect(\n      screen.queryByTestId(`rdi-nav-btn-${RdiPipelineTabs.Config}`),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should show nav when pipeline is not loading', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n      ...initialStateDefault.rdi.pipeline,\n      loading: false,\n    })\n\n    render(<Navigation />)\n\n    expect(\n      screen.queryByTestId(`rdi-nav-btn-${RdiPipelineTabs.Config}`),\n    ).toBeInTheDocument()\n  })\n\n  it('should call proper history push after click on tabs', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n      ...initialStateDefault.rdi.pipeline,\n      loading: false,\n    })\n\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    render(<Navigation />)\n\n    fireEvent.click(screen.getByTestId('rdi-nav-btn-config'))\n    expect(pushMock).toBeCalledWith(\n      '/integrate/rdiInstanceId/pipeline-management/config',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/Navigation.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory, useLocation, useParams } from 'react-router-dom'\n\nimport { Nullable } from 'uiSrc/utils'\nimport { PageNames, Pages } from 'uiSrc/constants'\nimport { Title } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { LoadingContent } from 'uiSrc/components/base'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces/rdi'\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\n\nimport { ConfigurationCard, JobsCard } from './cards'\n\nconst getSelectedTab = (path: string, rdiInstanceId: string) => {\n  const tabsPath = path?.replace(\n    `${Pages.rdiPipelineManagement(rdiInstanceId)}/`,\n    '',\n  )\n\n  if (tabsPath.startsWith(PageNames.rdiPipelineConfig))\n    return RdiPipelineTabs.Config\n  if (tabsPath.startsWith(PageNames.rdiPipelineJobs))\n    return RdiPipelineTabs.Jobs\n\n  return null\n}\n\nconst Navigation = () => {\n  const [selectedTab, setSelectedTab] =\n    useState<Nullable<RdiPipelineTabs>>(null)\n\n  const history = useHistory()\n  const { pathname } = useLocation()\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const { loading } = useSelector(rdiPipelineSelector)\n\n  const onSelectedTabChanged = (id: string | RdiPipelineTabs) => {\n    if (id === RdiPipelineTabs.Config) {\n      history.push(Pages.rdiPipelineConfig(rdiInstanceId))\n      return\n    }\n\n    history.push(Pages.rdiPipelineJobs(rdiInstanceId, encodeURIComponent(id)))\n  }\n\n  useEffect(() => {\n    const activeTab = getSelectedTab(pathname, rdiInstanceId)\n    setSelectedTab(activeTab)\n  }, [pathname, rdiInstanceId])\n\n  return (\n    <Col gap=\"l\">\n      <Title size=\"S\" color=\"primary\">\n        Pipeline management\n      </Title>\n\n      {loading && <LoadingContent lines={4} />}\n\n      {!loading && (\n        <>\n          <ConfigurationCard\n            onSelect={onSelectedTabChanged}\n            isSelected={selectedTab === RdiPipelineTabs.Config}\n          />\n\n          <JobsCard\n            onSelect={onSelectedTabChanged}\n            isSelected={selectedTab === RdiPipelineTabs.Jobs}\n          />\n        </>\n      )}\n    </Col>\n  )\n}\n\nexport default Navigation\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/BaseCard.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\ntype BaseCardContainerProps = React.HTMLAttributes<HTMLDivElement> & {\n  isSelected?: boolean\n}\n\nexport const BaseCardContainer = styled.div<BaseCardContainerProps>`\n  border: 1px solid\n    ${({ isSelected, theme }) =>\n      isSelected\n        ? theme.semantic.color.text.informative400\n        : theme.semantic.color.border.neutral500};\n  background-color: ${({ isSelected, theme }) =>\n    isSelected ? theme.semantic.color.background.neutral200 : 'inherit'};\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/BaseCard.tsx",
    "content": "import React, { ReactNode } from 'react'\nimport { Title } from 'uiSrc/components/base/text'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { BaseCardContainer } from './BaseCard.styles'\n\nexport type BaseCardProps = {\n  title: string\n  children: ReactNode\n  titleActions?: ReactNode\n  isSelected: boolean\n} & React.HTMLAttributes<HTMLDivElement>\n\nconst BaseCard = ({\n  title,\n  children,\n  titleActions,\n  isSelected,\n  ...restProps\n}: BaseCardProps) => (\n  <BaseCardContainer {...restProps} role=\"button\" isSelected={isSelected}>\n    <Col gap=\"s\">\n      <Row align=\"center\" justify=\"between\">\n        <Title size=\"S\" color=\"primary\">\n          {title}\n        </Title>\n\n        {titleActions && <FlexItem>{titleActions}</FlexItem>}\n      </Row>\n\n      {children}\n    </Col>\n  </BaseCardContainer>\n)\n\nexport default BaseCard\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\n\nimport ConfigurationCard, { ConfigurationCardProps } from './ConfigurationCard'\n\nconst mockedProps = mock<ConfigurationCardProps>()\n\nconst mockUseConfigurationState = jest.fn()\njest.mock('./hooks/useConfigurationState', () => ({\n  useConfigurationState: () => mockUseConfigurationState(),\n}))\n\ndescribe('ConfigurationCard', () => {\n  beforeEach(() => {\n    mockUseConfigurationState.mockReturnValue({\n      hasChanges: false,\n      isValid: true,\n      configValidationErrors: [],\n    })\n  })\n\n  it('should render with correct title', () => {\n    render(<ConfigurationCard {...instance(mockedProps)} />)\n\n    expect(screen.getByText('Configuration')).toBeInTheDocument()\n    expect(screen.getByText('Configuration file')).toBeInTheDocument()\n  })\n\n  it('should render with correct test id', () => {\n    render(<ConfigurationCard {...instance(mockedProps)} />)\n\n    expect(\n      screen.getByTestId(`rdi-nav-btn-${RdiPipelineTabs.Config}`),\n    ).toBeInTheDocument()\n  })\n\n  it('should call onSelect with Config tab when clicked', () => {\n    const mockOnSelect = jest.fn()\n\n    render(\n      <ConfigurationCard {...instance(mockedProps)} onSelect={mockOnSelect} />,\n    )\n\n    fireEvent.click(screen.getByTestId(`rdi-nav-btn-${RdiPipelineTabs.Config}`))\n\n    expect(mockOnSelect).toHaveBeenCalledWith(RdiPipelineTabs.Config)\n  })\n\n  it('should render as selected when isSelected is true', () => {\n    render(<ConfigurationCard {...instance(mockedProps)} isSelected={true} />)\n\n    const card = screen.getByTestId(`rdi-nav-btn-${RdiPipelineTabs.Config}`)\n    expect(card).toBeInTheDocument()\n  })\n\n  it('should have proper accessibility attributes', () => {\n    render(<ConfigurationCard {...instance(mockedProps)} />)\n\n    const card = screen.getByTestId(`rdi-nav-btn-${RdiPipelineTabs.Config}`)\n    expect(card).toHaveAttribute('tabIndex', '0')\n    expect(card).toHaveAttribute('role', 'button')\n  })\n\n  describe('Changes indicator', () => {\n    it('should not show changes indicator when no changes', () => {\n      mockUseConfigurationState.mockReturnValue({\n        hasChanges: false,\n        isValid: true,\n        configValidationErrors: [],\n      })\n\n      render(<ConfigurationCard {...instance(mockedProps)} />)\n\n      expect(\n        screen.queryByTestId('updated-configuration-highlight'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should show changes indicator when config has changes', () => {\n      mockUseConfigurationState.mockReturnValue({\n        hasChanges: true,\n        isValid: true,\n        configValidationErrors: [],\n      })\n\n      render(<ConfigurationCard {...instance(mockedProps)} />)\n\n      expect(\n        screen.getByTestId('updated-configuration-highlight'),\n      ).toBeInTheDocument()\n    })\n  })\n\n  describe('Validation errors', () => {\n    it('should not show error icon when config is valid', () => {\n      mockUseConfigurationState.mockReturnValue({\n        hasChanges: false,\n        isValid: true,\n        configValidationErrors: [],\n      })\n\n      render(<ConfigurationCard {...instance(mockedProps)} />)\n\n      expect(\n        screen.queryByTestId('rdi-pipeline-nav__error-configuration'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should show error icon when config has validation errors', () => {\n      mockUseConfigurationState.mockReturnValue({\n        hasChanges: false,\n        isValid: false,\n        configValidationErrors: [\n          'Invalid configuration',\n          'Missing required field',\n        ],\n      })\n\n      render(<ConfigurationCard {...instance(mockedProps)} />)\n\n      expect(\n        screen.getByTestId('rdi-pipeline-nav__error-configuration'),\n      ).toBeInTheDocument()\n    })\n\n    it('should handle single validation error', () => {\n      mockUseConfigurationState.mockReturnValue({\n        hasChanges: false,\n        isValid: false,\n        configValidationErrors: ['Single error'],\n      })\n\n      render(<ConfigurationCard {...instance(mockedProps)} />)\n\n      expect(\n        screen.getByTestId('rdi-pipeline-nav__error-configuration'),\n      ).toBeInTheDocument()\n    })\n  })\n\n  it('should show both changes indicator and error icon when config has changes and errors', () => {\n    mockUseConfigurationState.mockReturnValue({\n      hasChanges: true,\n      isValid: false,\n      configValidationErrors: ['Invalid configuration'],\n    })\n\n    render(<ConfigurationCard {...instance(mockedProps)} />)\n\n    expect(\n      screen.getByTestId('updated-configuration-highlight'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByTestId('rdi-pipeline-nav__error-configuration'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/ConfigurationCard.tsx",
    "content": "import React from 'react'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { Indicator } from 'uiSrc/components/base/text/text.styles'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Icon, ToastNotificationIcon } from 'uiSrc/components/base/icons'\nimport { useConfigurationState } from './hooks'\n\nimport BaseCard, { BaseCardProps } from './BaseCard'\nimport ValidationErrorsList from '../../validation-errors-list/ValidationErrorsList'\n\nexport type ConfigurationCardProps = Omit<\n  BaseCardProps,\n  'title' | 'children' | 'onSelect'\n> & {\n  onSelect: (id: string | RdiPipelineTabs) => void\n}\n\nconst ConfigurationCard = ({\n  onSelect,\n  isSelected,\n}: ConfigurationCardProps) => {\n  const { hasChanges, isValid, configValidationErrors } =\n    useConfigurationState()\n\n  const handleClick = () => {\n    onSelect(RdiPipelineTabs.Config)\n  }\n\n  return (\n    <BaseCard\n      title=\"Configuration\"\n      isSelected={isSelected}\n      tabIndex={0}\n      onClick={handleClick}\n      data-testid={`rdi-nav-btn-${RdiPipelineTabs.Config}`}\n    >\n      <Row gap=\"s\" align=\"center\">\n        {!hasChanges && <Indicator $color=\"transparent\" />}\n\n        {hasChanges && (\n          <RiTooltip\n            content=\"This file contains undeployed changes.\"\n            position=\"top\"\n          >\n            <Indicator\n              $color=\"informative\"\n              data-testid={`updated-configuration-highlight`}\n            />\n          </RiTooltip>\n        )}\n\n        <Text>Configuration file</Text>\n\n        {!isValid && (\n          <RiTooltip\n            position=\"right\"\n            content={\n              <ValidationErrorsList validationErrors={configValidationErrors} />\n            }\n          >\n            <Icon\n              icon={ToastNotificationIcon}\n              color=\"danger500\"\n              size=\"M\"\n              data-testid={`rdi-pipeline-nav__error-configuration`}\n            />\n          </RiTooltip>\n        )}\n      </Row>\n    </BaseCard>\n  )\n}\n\nexport default ConfigurationCard\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/index.ts",
    "content": "export * from './useConfigurationState'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.spec.ts",
    "content": "import { renderHook } from 'uiSrc/utils/test-utils'\nimport { rdiPipelineSelector, initialState } from 'uiSrc/slices/rdi/pipeline'\nimport { IStateRdiPipeline, FileChangeType } from 'uiSrc/slices/interfaces'\nimport { useConfigurationState } from './useConfigurationState'\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn(),\n}))\n\nconst mockRdiPipelineSelector = rdiPipelineSelector as jest.MockedFunction<\n  typeof rdiPipelineSelector\n>\n\nconst createMockState = (\n  overrides: Partial<IStateRdiPipeline> = {},\n): IStateRdiPipeline => ({\n  ...initialState,\n  ...overrides,\n})\n\ndescribe('useConfigurationState', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should return correct state when no changes and no validation errors', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: {},\n        configValidationErrors: [],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current).toEqual({\n      hasChanges: false,\n      isValid: true,\n      configValidationErrors: [],\n    })\n  })\n\n  it('should return hasChanges as true when config has changes', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: { config: FileChangeType.Modified },\n        configValidationErrors: [],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current).toEqual({\n      hasChanges: true,\n      isValid: true,\n      configValidationErrors: [],\n    })\n  })\n\n  it('should return isValid as false when config has validation errors', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: {},\n        configValidationErrors: [\n          'Invalid configuration',\n          'Missing required field',\n        ],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current).toEqual({\n      hasChanges: false,\n      isValid: false,\n      configValidationErrors: [\n        'Invalid configuration',\n        'Missing required field',\n      ],\n    })\n  })\n\n  it('should handle both changes and validation errors', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: { config: FileChangeType.Added },\n        configValidationErrors: ['Configuration error'],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current).toEqual({\n      hasChanges: true,\n      isValid: false,\n      configValidationErrors: ['Configuration error'],\n    })\n  })\n\n  it('should handle empty validation errors array', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: {},\n        configValidationErrors: [],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current.isValid).toBe(true)\n  })\n\n  it('should handle single validation error', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: {},\n        configValidationErrors: ['Single error'],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current).toEqual({\n      hasChanges: false,\n      isValid: false,\n      configValidationErrors: ['Single error'],\n    })\n  })\n\n  it('should handle multiple validation errors', () => {\n    const errors = ['Error 1', 'Error 2', 'Error 3']\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: {},\n        configValidationErrors: errors,\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current).toEqual({\n      hasChanges: false,\n      isValid: false,\n      configValidationErrors: errors,\n    })\n  })\n\n  it('should handle changes in other files without affecting config state', () => {\n    mockRdiPipelineSelector.mockReturnValue(\n      createMockState({\n        changes: {\n          job1: FileChangeType.Modified,\n          job2: FileChangeType.Added,\n          // no config changes\n        },\n        configValidationErrors: [],\n      }),\n    )\n\n    const { result } = renderHook(() => useConfigurationState())\n\n    expect(result.current.hasChanges).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/hooks/useConfigurationState.ts",
    "content": "import { useSelector } from 'react-redux'\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\n\nexport interface ConfigurationState {\n  hasChanges: boolean\n  isValid: boolean\n  configValidationErrors: string[]\n}\n\nexport const useConfigurationState = (): ConfigurationState => {\n  const { changes, configValidationErrors } = useSelector(rdiPipelineSelector)\n\n  const hasChanges = !!changes.config\n  const isValid = configValidationErrors.length === 0\n\n  return {\n    hasChanges,\n    isValid,\n    configValidationErrors,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/index.ts",
    "content": "import ConfigurationCard from './ConfigurationCard'\nimport JobsCard from './jobs/JobsCard'\n\nexport { ConfigurationCard, JobsCard }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/jobs/JobNameForm.tsx",
    "content": "import React from 'react'\n\nimport InlineItemEditor from 'uiSrc/components/inline-item-editor'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { IRdiPipelineJob } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils'\n\ntype JobNameFormProps = {\n  name: string\n  idx?: number\n  currentJobName: Nullable<string>\n  jobs: IRdiPipelineJob[]\n  isLoading: boolean\n  onApply: (value: string, idx?: number) => void\n  onDecline: () => void\n}\n\nconst buildValidationMessage = (text: string) => ({\n  // Validation messages are displayed by RiTooltip\n  // and we don't want them to have a title\n  // TODO: refactor this (inline editor should be responsible for displaying errors)\n  // only the message should be provided from this component\n  title: '',\n  content: (\n    <Row align=\"center\" gap=\"s\">\n      <FlexItem>\n        <RiIcon type=\"InfoIcon\" />\n      </FlexItem>\n      <FlexItem grow>{text}</FlexItem>\n    </Row>\n  ),\n})\n\nconst validateJobName = (\n  jobName: string,\n  currentJobName: Nullable<string>,\n  jobs: IRdiPipelineJob[],\n) => {\n  if (!jobName) {\n    return buildValidationMessage('Job name is required')\n  }\n\n  if (jobName === currentJobName) return undefined\n\n  if (jobs.some((job) => job.name === jobName)) {\n    return buildValidationMessage('Job name is already in use')\n  }\n\n  return undefined\n}\n\nconst JobNameForm = ({\n  name,\n  idx,\n  currentJobName,\n  jobs,\n  isLoading,\n  onApply,\n  onDecline,\n}: JobNameFormProps) => (\n  <FlexItem grow data-testid={`rdi-nav-job-edit-${name}`}>\n    <InlineItemEditor\n      controlsPosition=\"bottom\"\n      onApply={(value: string) => onApply(value, idx)}\n      onDecline={onDecline}\n      disableByValidation={(value) =>\n        !!validateJobName(value, currentJobName, jobs)\n      }\n      getError={(value) => validateJobName(value, currentJobName, jobs)}\n      isLoading={isLoading}\n      declineOnUnmount={false}\n      initialValue={currentJobName || ''}\n      placeholder=\"Enter job name\"\n      maxLength={250}\n      viewChildrenMode={false}\n      disableEmpty\n      variant=\"underline\"\n      styles={{\n        input: {\n          height: '32px',\n        },\n        actionsContainer: {\n          width: '64px',\n        },\n      }}\n    />\n  </FlexItem>\n)\n\nexport default JobNameForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/jobs/JobsCard.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n  act,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\n\nimport { rdiPipelineSelector as rdiPipelineSelectorMock } from 'uiSrc/slices/rdi/pipeline'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport JobsCard, { JobsCardProps } from './JobsCard'\n\nconst mockedProps = mock<JobsCardProps>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: {},\n    jobs: [\n      { name: 'job1', value: 'value1' },\n      { name: 'job2', value: 'value2' },\n    ],\n    jobsValidationErrors: {},\n    changes: {},\n  }),\n}))\n\nconst mockedRdiPipelineSelector = rdiPipelineSelectorMock as jest.Mock\nconst mockedSendEventTelemetry = sendEventTelemetry as jest.Mock\n\ndescribe('JobsCard', () => {\n  it('should render with correct title', () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(screen.getByText('Transform and Validate')).toBeInTheDocument()\n  })\n\n  it('should render with correct test id', () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('rdi-pipeline-jobs-nav')).toBeInTheDocument()\n  })\n\n  it('should render as selected when isSelected is true', () => {\n    render(<JobsCard {...instance(mockedProps)} isSelected={true} />)\n\n    const card = screen.getByTestId('rdi-pipeline-jobs-nav')\n    expect(card).toBeInTheDocument()\n  })\n\n  it('should render add job button', () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('add-new-job')).toBeInTheDocument()\n    expect(screen.getByLabelText('add new job file')).toBeInTheDocument()\n  })\n\n  it('should render job actions', () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('rdi-nav-job-actions-job1')).toBeInTheDocument()\n    expect(screen.getByTestId('edit-job-name-job1')).toBeInTheDocument()\n    expect(screen.getByTestId('delete-job-job1')).toBeInTheDocument()\n  })\n\n  it('should delete job', async () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-job-job1'))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-confirm-btn'))\n    })\n\n    waitFor(() => {\n      expect(screen.queryByTestId('delete-job-job1')).not.toBeInTheDocument()\n    })\n  })\n\n  it('should not delete job when dismissed', async () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-job-job1'))\n    })\n\n    await act(() => {\n      fireEvent.click(document)\n    })\n\n    waitFor(() => {\n      expect(screen.queryByTestId('delete-job-job1')).toBeInTheDocument()\n    })\n  })\n\n  it('should edit job name', async () => {\n    render(<JobsCard {...instance(mockedProps)} onSelect={jest.fn()} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('edit-job-name-job1'))\n    })\n\n    waitFor(() => {\n      expect(screen.getByTestId('rdi-nav-job-edit-job1')).toBeInTheDocument()\n    })\n  })\n\n  it('should not edit job name when dismissed', async () => {\n    render(<JobsCard {...instance(mockedProps)} onSelect={jest.fn()} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('edit-job-name-job1'))\n    })\n\n    await act(() => {\n      fireEvent.click(document)\n    })\n\n    waitFor(() => {\n      expect(\n        screen.queryByTestId('rdi-nav-job-edit-job1'),\n      ).not.toBeInTheDocument()\n    })\n  })\n\n  it('should show new job form when add button is clicked', () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(screen.queryByTestId('new-job-file')).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('add-new-job'))\n\n    expect(screen.getByTestId('new-job-file')).toBeInTheDocument()\n  })\n\n  it('should disable add button when new job form is open', () => {\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('add-new-job'))\n\n    expect(screen.getByTestId('add-new-job')).toBeDisabled()\n  })\n\n  it('should handle empty jobs list', () => {\n    mockedRdiPipelineSelector.mockReturnValue({\n      loading: false,\n      data: {},\n      jobs: [],\n      jobsValidationErrors: {},\n      changes: {},\n    })\n\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(screen.queryByTestId('job-file-job1')).not.toBeInTheDocument()\n    expect(screen.getByTestId('add-new-job')).toBeInTheDocument()\n  })\n\n  it('should show validation errors when present', () => {\n    mockedRdiPipelineSelector.mockReturnValue({\n      loading: false,\n      data: {},\n      jobs: [\n        { name: 'job1', value: 'value1' },\n        { name: 'job2', value: 'value2' },\n      ],\n      jobsValidationErrors: {\n        job1: ['Error message'],\n      },\n      changes: {},\n    })\n\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(\n      screen.getByTestId('rdi-pipeline-nav__error-job1'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('rdi-pipeline-nav__error-job2'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should disable apply button when job name is invalid', async () => {\n    mockedRdiPipelineSelector.mockReturnValue({\n      loading: false,\n      error: '',\n      jobs: [{ name: 'job1', value: 'value' }],\n      jobsValidationErrors: { job1: ['Invalid name'] },\n      changes: {},\n    })\n\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('edit-job-name-job1'))\n    })\n\n    const input = screen.getByTestId('inline-item-editor')\n    fireEvent.change(input, { target: { value: '' } }) // Invalid name\n\n    expect(screen.getByTestId('apply-btn')).toBeDisabled()\n  })\n\n  it('should show changes indicator when job has changes', () => {\n    mockedRdiPipelineSelector.mockReturnValue({\n      loading: false,\n      data: {},\n      jobs: [\n        { name: 'job1', value: 'value1' },\n        { name: 'job2', value: 'value2' },\n      ],\n      jobsValidationErrors: {},\n      changes: {\n        job1: 'modified',\n      },\n    })\n\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    expect(\n      screen.getByTestId('updated-file-job1-highlight'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('updated-file-job2-highlight'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should call proper telemetry event when adding new job', async () => {\n    mockedSendEventTelemetry.mockImplementation(() => jest.fn())\n\n    mockedRdiPipelineSelector.mockReturnValue({\n      loading: false,\n      data: {\n        jobs: [\n          { name: 'job1', value: 'value1' },\n          { name: 'job2', value: 'value2' },\n        ],\n      },\n      jobs: [\n        { name: 'job1', value: 'value1' },\n        { name: 'job2', value: 'value2' },\n      ],\n      jobsValidationErrors: {},\n      changes: {},\n    })\n\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('add-new-job'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('inline-item-editor'), {\n        target: { value: 'job3' },\n      })\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('apply-btn'))\n    })\n\n    expect(mockedSendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_JOB_CREATED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        jobName: 'job3',\n      },\n    })\n  })\n\n  it('should call proper telemetry event when deleting job', async () => {\n    mockedSendEventTelemetry.mockImplementation(() => jest.fn())\n\n    render(<JobsCard {...instance(mockedProps)} />)\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-job-job1'))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-confirm-btn'))\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_JOB_DELETED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        jobName: 'job1',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/jobs/JobsCard.tsx",
    "content": "import React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useLocation, useParams } from 'react-router-dom'\nimport { isNumber } from 'lodash'\n\nimport { PageNames } from 'uiSrc/constants'\nimport { FileChangeType, IRdiPipelineJob } from 'uiSrc/slices/interfaces'\nimport {\n  deleteChangedFile,\n  deletePipelineJob,\n  rdiPipelineSelector,\n  setChangedFile,\n  setPipelineJobs,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { isEqualPipelineFile, Nullable } from 'uiSrc/utils'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { RiTooltip } from 'uiSrc/components'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { PlusIcon } from 'uiSrc/components/base/icons'\n\nimport BaseCard, { BaseCardProps } from '../BaseCard'\nimport JobNameForm from './JobNameForm'\nimport JobItem from './JobsItem'\n\nexport type JobsCardProps = Omit<\n  BaseCardProps,\n  'title' | 'children' | 'onSelect'\n> & {\n  onSelect: (id: string) => void\n}\n\nconst JobsCard = (props: JobsCardProps) => {\n  const { onSelect, isSelected } = props\n\n  const [currentJobName, setCurrentJobName] = useState<Nullable<string>>(null)\n  const [isNewJob, setIsNewJob] = useState(false)\n  const [hideTooltip, setHideTooltip] = useState(false)\n\n  const {\n    loading,\n    data,\n    jobs = [],\n    jobsValidationErrors,\n    changes = {},\n  } = useSelector(rdiPipelineSelector)\n\n  const dispatch = useDispatch()\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const { pathname } = useLocation()\n\n  const path = decodeURIComponent(pathname?.split('/').pop() || '')\n\n  const handleDeleteClick = (name: string) => {\n    dispatch(deletePipelineJob(name))\n\n    const newJobs = jobs.filter((el) => el.name !== name)\n    dispatch(setPipelineJobs(newJobs))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_JOB_DELETED,\n      eventData: {\n        rdiInstanceId,\n        jobName: name,\n      },\n    })\n\n    // if the last job is deleted, select the pipeline config tab\n    if (path === name) {\n      onSelect(newJobs.length ? newJobs[0].name : PageNames.rdiPipelineConfig)\n    }\n  }\n\n  const handleDeclineJobName = () => {\n    setCurrentJobName(null)\n\n    if (isNewJob) {\n      setIsNewJob(false)\n    }\n  }\n\n  const handleApplyJobName = (value: string, idx?: number) => {\n    const isJobExists = isNumber(idx)\n    const updatedJobs: IRdiPipelineJob[] = isJobExists\n      ? [\n          ...jobs.slice(0, idx),\n          { ...jobs[idx], name: value },\n          ...jobs.slice(idx + 1),\n        ]\n      : [...jobs, { name: value, value: '' }]\n\n    dispatch(setPipelineJobs(updatedJobs))\n\n    const deployedJob = data?.jobs.find((el) => el.name === value)\n\n    if (!deployedJob) {\n      dispatch(setChangedFile({ name: value, status: FileChangeType.Added }))\n    }\n\n    if (\n      deployedJob &&\n      isJobExists &&\n      isEqualPipelineFile(jobs[idx].value, deployedJob.value)\n    ) {\n      dispatch(deleteChangedFile(deployedJob.value))\n    }\n\n    setCurrentJobName(null)\n    setIsNewJob(false)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_JOB_CREATED,\n      eventData: {\n        rdiInstanceId,\n        jobName: value,\n      },\n    })\n\n    if (path === currentJobName) {\n      onSelect(value)\n    }\n  }\n\n  const isJobValid = (jobName: string) =>\n    jobsValidationErrors[jobName]\n      ? jobsValidationErrors[jobName].length === 0\n      : true\n\n  const getJobValidationErrors = (jobName: string) =>\n    jobsValidationErrors[jobName] || []\n\n  return (\n    <BaseCard\n      title=\"Transform and Validate\"\n      titleActions={\n        <RiTooltip\n          content={!hideTooltip ? 'Add a job file' : null}\n          position=\"top\"\n          anchorClassName=\"flex-row\"\n        >\n          <IconButton\n            icon={PlusIcon}\n            onClick={() => {\n              setIsNewJob(true)\n            }}\n            onMouseEnter={() => {\n              setHideTooltip(false)\n            }}\n            onMouseLeave={() => {\n              setHideTooltip(true)\n            }}\n            disabled={isNewJob}\n            aria-label=\"add new job file\"\n            data-testid=\"add-new-job\"\n          />\n        </RiTooltip>\n      }\n      isSelected={isSelected}\n      data-testid=\"rdi-pipeline-jobs-nav\"\n    >\n      {isNewJob && (\n        <Row align=\"center\" justify=\"between\" data-testid=\"new-job-file\">\n          <Row align=\"center\">\n            <JobNameForm\n              name=\"\"\n              idx={undefined}\n              currentJobName={currentJobName}\n              jobs={jobs}\n              isLoading={loading}\n              onApply={handleApplyJobName}\n              onDecline={handleDeclineJobName}\n            />\n          </Row>\n        </Row>\n      )}\n\n      {jobs.map(({ name }, idx) => (\n        <Row\n          key={name}\n          align=\"center\"\n          justify=\"between\"\n          data-testid={`job-file-${name}`}\n        >\n          {currentJobName === name ? (\n            <JobNameForm\n              name={name}\n              idx={idx}\n              currentJobName={currentJobName}\n              jobs={jobs}\n              isLoading={loading}\n              onApply={handleApplyJobName}\n              onDecline={handleDeclineJobName}\n            />\n          ) : (\n            <JobItem\n              name={name}\n              isValid={isJobValid(name)}\n              validationErrors={getJobValidationErrors(name)}\n              isActive={path === name}\n              hasChanges={!!changes[name]}\n              onSelect={onSelect}\n              onEdit={(jobName) => {\n                setCurrentJobName(jobName)\n                setIsNewJob(false)\n              }}\n              onDelete={handleDeleteClick}\n            />\n          )}\n        </Row>\n      ))}\n    </BaseCard>\n  )\n}\n\nexport default JobsCard\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/cards/jobs/JobsItem.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiTooltip } from 'uiSrc/components'\nimport { DeleteIcon, EditIcon, Icon } from 'uiSrc/components/base/icons'\nimport {\n  DestructiveButton,\n  IconButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport ConfirmationPopover from 'uiSrc/pages/rdi/components/confirmation-popover/ConfirmationPopover'\nimport ValidationErrorsList from 'uiSrc/pages/rdi/pipeline-management/components/validation-errors-list/ValidationErrorsList'\nimport { Indicator } from 'uiSrc/components/base/text/text.styles'\nimport { ToastNotificationIcon } from '@redis-ui/icons'\nimport { truncateText } from 'uiSrc/utils'\n\ntype JobItemProps = {\n  name: string\n  isValid: boolean\n  validationErrors: string[]\n  isActive: boolean\n  hasChanges: boolean\n  onSelect: (name: string) => void\n  onEdit: (name: string) => void\n  onDelete: (name: string) => void\n}\n\nconst JobItem = ({\n  name,\n  isValid,\n  validationErrors,\n  isActive,\n  hasChanges,\n  onSelect,\n  onEdit,\n  onDelete,\n}: JobItemProps) => (\n  <Row align=\"center\" gap=\"s\">\n    <FlexItem>\n      {!hasChanges && <Indicator $color=\"transparent\" />}\n\n      {hasChanges && (\n        <RiTooltip\n          content=\"This file contains undeployed changes.\"\n          position=\"top\"\n        >\n          <Indicator\n            $color=\"informative\"\n            data-testid={`updated-file-${name}-highlight`}\n          />\n        </RiTooltip>\n      )}\n    </FlexItem>\n\n    <FlexItem\n      onClick={() => onSelect(name)}\n      data-testid={`rdi-nav-job-${name}`}\n      grow\n    >\n      <Row align=\"center\" gap=\"m\">\n        <RiTooltip content={truncateText(name, 200)}>\n          <Text\n            style={{ textDecoration: isActive ? 'underline' : 'none' }}\n            color={isActive ? 'primary' : 'secondary'}\n          >\n            {truncateText(name, 20)}\n          </Text>\n        </RiTooltip>\n\n        {!isValid && (\n          <RiTooltip\n            position=\"right\"\n            content={\n              <ValidationErrorsList validationErrors={validationErrors} />\n            }\n          >\n            <Icon\n              icon={ToastNotificationIcon}\n              color=\"danger500\"\n              size=\"M\"\n              data-testid={`rdi-pipeline-nav__error-${name}`}\n            />\n          </RiTooltip>\n        )}\n      </Row>\n    </FlexItem>\n\n    <FlexItem>\n      <Row data-testid={`rdi-nav-job-actions-${name}`} align=\"center\">\n        <RiTooltip content=\"Edit job file name\" position=\"top\">\n          <IconButton\n            icon={EditIcon}\n            onClick={() => onEdit(name)}\n            aria-label=\"edit job file name\"\n            data-testid={`edit-job-name-${name}`}\n          />\n        </RiTooltip>\n\n        <RiTooltip\n          content=\"Delete job\"\n          position=\"top\"\n          anchorClassName=\"flex-row\"\n        >\n          <ConfirmationPopover\n            title={`Delete ${name}`}\n            body={\n              <Text size=\"s\">\n                Changes will not be applied until the pipeline is deployed.\n              </Text>\n            }\n            submitBtn={\n              <DestructiveButton\n                size=\"s\"\n                color=\"secondary\"\n                data-testid=\"delete-confirm-btn\"\n              >\n                Delete\n              </DestructiveButton>\n            }\n            onConfirm={() => onDelete(name)}\n            button={\n              <IconButton\n                icon={DeleteIcon}\n                aria-label=\"delete job\"\n                data-testid={`delete-job-${name}`}\n              />\n            }\n          />\n        </RiTooltip>\n      </Row>\n    </FlexItem>\n  </Row>\n)\n\nexport default JobItem\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/navigation/index.ts",
    "content": "import Navigation from './Navigation'\n\nexport default Navigation\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/source-pipeline-dialog/SourcePipelineModal.spec.tsx",
    "content": "import React from 'react'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  fireEvent,\n  screen,\n  initialStateDefault,\n  createMockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { rdiPipelineSelector, setChangedFile } from 'uiSrc/slices/rdi/pipeline'\nimport {\n  appContextPipelineManagement,\n  setPipelineDialogState,\n} from 'uiSrc/slices/app/context'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport SourcePipelineDialog, {\n  PipelineSourceOptions,\n} from './SourcePipelineModal'\n\njest.mock('formik', () => ({\n  ...jest.requireActual('formik'),\n  useFormikContext: jest.fn().mockReturnValue({\n    values: {\n      config: '',\n      jobs: [],\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/context', () => ({\n  ...jest.requireActual('uiSrc/slices/app/context'),\n  appContextPipelineManagement: jest.fn(),\n}))\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n  ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n    ...initialStateDefault.rdi.pipeline,\n  })\n  ;(appContextPipelineManagement as jest.Mock).mockReturnValue({\n    ...initialStateDefault.app.context.pipelineManagement,\n  })\n})\n\nconst renderSourcePipelineDialog = () =>\n  render(<SourcePipelineDialog />, { store })\n\ndescribe('SourcePipelineDialog', () => {\n  it('should not show dialog by default and not set isOpenDialog to true', () => {\n    renderSourcePipelineDialog()\n\n    expect(\n      screen.queryByTestId('file-source-pipeline-dialog'),\n    ).not.toBeInTheDocument()\n\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should show dialog when isOpenDialog flag is true', () => {\n    ;(appContextPipelineManagement as jest.Mock).mockReturnValue({\n      ...initialStateDefault.app.context.pipelineManagement,\n      isOpenDialog: true,\n    })\n\n    renderSourcePipelineDialog()\n\n    expect(\n      screen.queryByTestId('file-source-pipeline-dialog'),\n    ).toBeInTheDocument()\n  })\n\n  it('should not show dialog when there is deployed pipeline on a server', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n      ...initialStateDefault.rdi.pipeline,\n      loading: false,\n      data: { config: 'some config' },\n    })\n\n    renderSourcePipelineDialog()\n\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should not show dialog when config is fetching', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n      ...initialStateDefault.rdi.pipeline,\n      loading: true,\n      data: null,\n    })\n\n    renderSourcePipelineDialog()\n\n    expect(store.getActions()).toEqual([])\n  })\n\n  it('should show dialog when there is no pipeline on a server', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n      ...initialStateDefault.rdi.pipeline,\n      loading: false,\n      data: { config: '' },\n    })\n\n    renderSourcePipelineDialog()\n\n    expect(store.getActions()).toEqual([setPipelineDialogState(true)])\n  })\n\n  describe('Telemetry events', () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    beforeEach(() => {\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n      ;(appContextPipelineManagement as jest.Mock).mockReturnValue({\n        ...initialStateDefault.app.context.pipelineManagement,\n        isOpenDialog: true,\n      })\n    })\n\n    it('should call proper actions after select empty pipeline  option', () => {\n      renderSourcePipelineDialog()\n\n      fireEvent.click(screen.getByTestId('empty-source-pipeline-dialog'))\n\n      const expectedActions = [\n        setChangedFile({ name: 'config', status: FileChangeType.Added }),\n        setPipelineDialogState(false),\n      ]\n\n      expect(store.getActions()).toEqual(expectedActions)\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.RDI_START_OPTION_SELECTED,\n        eventData: {\n          id: 'rdiInstanceId',\n          option: PipelineSourceOptions.NEW,\n        },\n      })\n    })\n\n    it('should call proper telemetry event after select empty pipeline option', () => {\n      const sendEventTelemetryMock = jest.fn()\n      ;(sendEventTelemetry as jest.Mock).mockImplementation(\n        () => sendEventTelemetryMock,\n      )\n      ;(appContextPipelineManagement as jest.Mock).mockReturnValue({\n        ...initialStateDefault.app.context.pipelineManagement,\n        isOpenDialog: true,\n      })\n\n      renderSourcePipelineDialog()\n\n      fireEvent.click(screen.getByTestId('file-source-pipeline-dialog'))\n\n      expect(sendEventTelemetry).toBeCalledWith({\n        event: TelemetryEvent.RDI_START_OPTION_SELECTED,\n        eventData: {\n          id: 'rdiInstanceId',\n          option: PipelineSourceOptions.FILE,\n        },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/source-pipeline-dialog/SourcePipelineModal.styles.ts",
    "content": "import styled from 'styled-components'\n\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const ButtonWrapper = styled(Col)`\n  flex: 1;\n  align-items: center;\n  padding: ${({ theme }) =>\n    `${theme.core.space.space150} ${theme.core.space.space100}`};\n  border-radius: ${({ theme }) => theme.core.space.space050};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.secondary300};\n\n  &:hover,\n  &:focus {\n    border: 1px solid ${({ theme }) => theme.semantic.color.border.secondary400};\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/source-pipeline-dialog/SourcePipelineModal.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { keys } from '@elastic/eui'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { rdiPipelineSelector, setChangedFile } from 'uiSrc/slices/rdi/pipeline'\nimport {\n  appContextPipelineManagement,\n  setPipelineDialogState,\n} from 'uiSrc/slices/app/context'\nimport UploadModal from 'uiSrc/pages/rdi/pipeline-management/components/upload-modal/UploadModal'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { ContractsIcon, UploadIcon } from 'uiSrc/components/base/icons'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport { Modal } from 'uiSrc/components/base/display'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\nimport { ButtonWrapper } from './SourcePipelineModal.styles'\n\nexport const EMPTY_PIPELINE = {\n  config: '',\n  jobs: [],\n}\n\nexport enum PipelineSourceOptions {\n  FILE = 'upload from file',\n  NEW = 'new pipeline',\n}\n\nconst SourcePipelineDialog = () => {\n  const [isShowDownloadDialog, setIsShowDownloadDialog] = useState(false)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const { isOpenDialog } = useSelector(appContextPipelineManagement)\n\n  // data is original response from the server converted to config and jobs yaml strings\n  // since by default it is null we can determine if it was fetched and it's content\n  const { data } = useSelector(rdiPipelineSelector)\n\n  useEffect(() => {\n    if (data?.config === '') {\n      dispatch(setPipelineDialogState(true))\n    }\n  }, [data])\n\n  const dispatch = useDispatch()\n\n  const onSelect = (option: PipelineSourceOptions) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_START_OPTION_SELECTED,\n      eventData: {\n        id: rdiInstanceId,\n        option,\n      },\n    })\n  }\n\n  const onStartNewPipeline = () => {\n    onSelect(PipelineSourceOptions.NEW)\n    dispatch(setChangedFile({ name: 'config', status: FileChangeType.Added }))\n    dispatch(setPipelineDialogState(false))\n  }\n\n  const handleCloseDialog = () => {\n    dispatch(setChangedFile({ name: 'config', status: FileChangeType.Added }))\n    dispatch(setPipelineDialogState(false))\n  }\n\n  const onUploadClick = () => {\n    setIsShowDownloadDialog(true)\n    onSelect(PipelineSourceOptions.FILE)\n  }\n\n  const onEnter = (\n    event: React.KeyboardEvent<HTMLDivElement>,\n    callback: () => void,\n  ) => {\n    if (event.key === keys.ENTER) callback()\n  }\n\n  if (isShowDownloadDialog) {\n    return (\n      <UploadModal\n        onClose={() => dispatch(setPipelineDialogState(false))}\n        visible={isShowDownloadDialog}\n      />\n    )\n  }\n\n  if (!isOpenDialog) {\n    return null\n  }\n\n  return (\n    <Modal.Compose open onOpenChange={(open) => !open && handleCloseDialog()}>\n      <Modal.Content.Compose>\n        <Modal.Content.Body.Compose>\n          <Spacer size=\"xl\" />\n          <Col gap=\"xxl\">\n            <Col align=\"center\" justify=\"center\">\n              <Title size=\"L\" color=\"primary\">\n                Select an option\n              </Title>\n              <Title size=\"L\" color=\"primary\">\n                to start with your pipeline\n              </Title>\n            </Col>\n            <Row gap=\"xxl\">\n              <ButtonWrapper\n                gap=\"s\"\n                role=\"button\"\n                tabIndex={0}\n                onKeyDown={(event) => onEnter(event, onUploadClick)}\n                onClick={onUploadClick}\n                data-testid=\"file-source-pipeline-dialog\"\n              >\n                <UploadIcon size=\"XL\" />\n                <Text color=\"primary\" size=\"S\" textAlign=\"center\">\n                  Import pipeline from ZIP file\n                </Text>\n              </ButtonWrapper>\n              <ButtonWrapper\n                gap=\"s\"\n                role=\"button\"\n                tabIndex={0}\n                onKeyDown={(event) => onEnter(event, onStartNewPipeline)}\n                onClick={onStartNewPipeline}\n                data-testid=\"empty-source-pipeline-dialog\"\n              >\n                <ContractsIcon size=\"XL\" />\n                <Text color=\"primary\" size=\"S\" textAlign=\"center\">\n                  Create new pipeline\n                </Text>\n              </ButtonWrapper>\n            </Row>\n          </Col>\n          <Spacer size=\"xl\" />\n        </Modal.Content.Body.Compose>\n      </Modal.Content.Compose>\n    </Modal.Compose>\n  )\n}\n\nexport default SourcePipelineDialog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/source-pipeline-dialog/index.ts",
    "content": "import SourcePipelineDialog from './SourcePipelineModal'\n\nexport default SourcePipelineDialog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-button/TemplateButton.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\n\nimport {\n  fireEvent,\n  render,\n  cleanup,\n  mockedStore,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport { rdiPipelineStrategiesSelector } from 'uiSrc/slices/rdi/pipeline'\nimport TemplateButton, { TemplateButtonProps } from './TemplateButton'\n\nconst mockedProps = mock<TemplateButtonProps>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineStrategiesSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: [\n      {\n        strategy: 'test',\n      },\n    ],\n  }),\n}))\n\ndescribe('TemplateForm', () => {\n  it('should render', () => {\n    expect(render(<TemplateButton {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should be disabled if no templateOption', () => {\n    ;(rdiPipelineStrategiesSelector as jest.Mock).mockImplementationOnce(\n      () => ({\n        loading: false,\n        data: [],\n      }),\n    )\n\n    render(<TemplateButton {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('template-btn')).toBeDisabled()\n  })\n\n  it('should send telemetry on Click', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<TemplateButton {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('template-btn')).toBeEnabled()\n\n    fireEvent.click(screen.getByTestId('template-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_TEMPLATE_CLICKED,\n      eventData: {\n        id: 'rdiInstanceId',\n        page: RdiPipelineTabs.Jobs,\n        mode: 'test',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-button/TemplateButton.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { useParams } from 'react-router-dom'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  fetchJobTemplate,\n  rdiPipelineStrategiesSelector,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { RiTooltip } from 'uiSrc/components'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { getTooltipContent } from '../template-form/TemplateForm'\nimport { INGEST_OPTION } from '../template-form/constants'\n\nexport interface TemplateButtonProps {\n  value: string\n  setFieldValue: (template: string) => void\n}\n\nconst TemplateButton = ({ setFieldValue, value }: TemplateButtonProps) => {\n  const dispatch = useDispatch()\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const { loading, data } = useSelector(rdiPipelineStrategiesSelector)\n\n  const templateOption = data?.length\n    ? data.find((strategy) => strategy.strategy === INGEST_OPTION)?.strategy ||\n      data[0].strategy\n    : ''\n\n  const handleApply = () => {\n    dispatch(\n      fetchJobTemplate(rdiInstanceId, templateOption, (template: string) => {\n        setFieldValue(template)\n      }),\n    )\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_TEMPLATE_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n        page: RdiPipelineTabs.Jobs,\n        mode: templateOption,\n      },\n    })\n  }\n\n  return (\n    <RiTooltip\n      content={getTooltipContent(value, !templateOption)}\n      position=\"bottom\"\n      anchorClassName=\"flex-row\"\n    >\n      <SecondaryButton\n        inverted\n        size=\"s\"\n        aria-label=\"Insert template\"\n        loading={loading}\n        disabled={!templateOption || !!value}\n        onClick={handleApply}\n        data-testid=\"template-btn\"\n      >\n        Insert template\n      </SecondaryButton>\n    </RiTooltip>\n  )\n}\n\nexport default TemplateButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-button/index.ts",
    "content": "import TemplateButton from './TemplateButton'\n\nexport default TemplateButton\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-form/TemplateForm.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\n\nimport {\n  act,\n  cleanup,\n  expectActionsToContain,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  getPipelineStrategies,\n  rdiPipelineStrategiesSelector,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport { INGEST_OPTION, NO_TEMPLATE_LABEL } from './constants'\nimport TemplateForm, { Props } from './TemplateForm'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineStrategiesSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: [],\n  }),\n}))\n\ndescribe('TemplateForm', () => {\n  it('should render', () => {\n    expect(render(<TemplateForm {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call closePopover', async () => {\n    const mockClosePopover = jest.fn()\n\n    render(\n      <TemplateForm\n        {...instance(mockedProps)}\n        closePopover={mockClosePopover}\n      />,\n    )\n\n    fireEvent.click(screen.getByTestId('template-cancel-btn'))\n\n    expect(mockClosePopover).toHaveBeenCalled()\n  })\n\n  it('should fetch rdi strategies on initial', async () => {\n    await act(async () => {\n      render(<TemplateForm {...instance(mockedProps)} />)\n    })\n\n    const expectedActions = [getPipelineStrategies()]\n\n    expectActionsToContain(store.getActions(), expectedActions)\n  })\n\n  it('apply btn should be disabled if there is any value', () => {\n    render(<TemplateForm {...instance(mockedProps)} value=\"some value\" />)\n\n    expect(screen.getByTestId('template-apply-btn')).toBeDisabled()\n  })\n\n  it('apply btn should be disabled if there is any value', () => {\n    render(<TemplateForm {...instance(mockedProps)} value=\"some value\" />)\n\n    expect(screen.getByTestId('template-apply-btn')).toBeDisabled()\n  })\n\n  it('should display db type select when source is \"config\"', () => {\n    render(\n      <TemplateForm\n        {...instance(mockedProps)}\n        source={RdiPipelineTabs.Config}\n      />,\n    )\n\n    expect(screen.getByTestId('db-type-select')).toBeInTheDocument()\n  })\n\n  it('should not render db type select when source is \"jobs\"', () => {\n    render(\n      <TemplateForm {...instance(mockedProps)} source={RdiPipelineTabs.Jobs} />,\n    )\n\n    const dbTypeSelect = screen.queryByTestId('db-type-select')\n\n    expect(dbTypeSelect).toBeNull()\n  })\n\n  it('should select \"No template\" value', () => {\n    render(\n      <TemplateForm\n        {...instance(mockedProps)}\n        source={RdiPipelineTabs.Config}\n      />,\n    )\n\n    expect(screen.getByTestId('db-type-select')).toHaveTextContent(\n      NO_TEMPLATE_LABEL,\n    )\n  })\n\n  it('should select ingest option and first db option', async () => {\n    ;(rdiPipelineStrategiesSelector as jest.Mock).mockImplementation(() => ({\n      loading: false,\n      data: [\n        {\n          strategy: 'any',\n          databases: ['any'],\n        },\n        {\n          strategy: INGEST_OPTION,\n          databases: ['first_db_option', 'second_db_option'],\n        },\n      ],\n    }))\n\n    await act(() =>\n      render(\n        <TemplateForm\n          {...instance(mockedProps)}\n          source={RdiPipelineTabs.Config}\n        />,\n      ),\n    )\n\n    expect(screen.getByTestId('pipeline-type-select')).toHaveTextContent(\n      INGEST_OPTION,\n    )\n    expect(screen.getByTestId('db-type-select')).toHaveTextContent(\n      'first_db_option',\n    )\n  })\n\n  it('should select first db and pipeline options', async () => {\n    ;(rdiPipelineStrategiesSelector as jest.Mock).mockImplementation(() => ({\n      loading: false,\n      data: [\n        {\n          strategy: 'foo',\n          databases: ['any'],\n        },\n        {\n          strategy: 'bar',\n          databases: ['first_db_option', 'second_db_option'],\n        },\n      ],\n    }))\n\n    await act(() =>\n      render(\n        <TemplateForm\n          {...instance(mockedProps)}\n          source={RdiPipelineTabs.Config}\n        />,\n      ),\n    )\n\n    expect(screen.getByTestId('pipeline-type-select')).toHaveTextContent('foo')\n    expect(screen.getByTestId('db-type-select')).toHaveTextContent('any')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-form/TemplateForm.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  fetchPipelineStrategies,\n  fetchJobTemplate,\n  fetchConfigTemplate,\n  rdiPipelineStrategiesSelector,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces/rdi'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Title } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport {\n  RiSelectOption,\n  RiSelect,\n  defaultValueRender,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { NO_TEMPLATE_VALUE, NO_OPTIONS, INGEST_OPTION } from './constants'\n\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  setTemplate: (template: string) => void\n  closePopover: () => void\n  source: RdiPipelineTabs\n  value: string\n}\n\nexport const getTooltipContent = (\n  value: string,\n  isNoTemplateOptions: boolean,\n) => {\n  if (isNoTemplateOptions) {\n    return (\n      <>\n        No template is available.\n        <br />\n        Close the form and try again.\n      </>\n    )\n  }\n\n  if (value) {\n    return 'Templates can be accessed only with the empty Editor to prevent potential data loss.'\n  }\n\n  return null\n}\n\nconst TemplateForm = (props: Props) => {\n  const { closePopover, setTemplate, source, value } = props\n\n  const { loading, data } = useSelector(rdiPipelineStrategiesSelector)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const [pipelineTypeOptions, setPipelineTypeOptions] = useState<\n    RiSelectOption[]\n  >([])\n  const [dbTypeOptions, setDbTypeOptions] =\n    useState<RiSelectOption[]>(NO_OPTIONS)\n  const [selectedDbType, setSelectedDbType] = useState<string>('')\n  const [selectedPipelineType, setSelectedPipelineType] = useState<string>('')\n\n  const dispatch = useDispatch()\n\n  const handleCancel = () => {\n    closePopover()\n  }\n\n  const onSuccess = (template: string) => {\n    setTemplate(template)\n    closePopover()\n  }\n\n  const handleApply = () => {\n    if (source === RdiPipelineTabs.Config) {\n      dispatch(\n        fetchConfigTemplate(\n          rdiInstanceId,\n          selectedPipelineType,\n          selectedDbType,\n          onSuccess,\n        ),\n      )\n    }\n    if (source === RdiPipelineTabs.Jobs) {\n      dispatch(fetchJobTemplate(rdiInstanceId, selectedPipelineType, onSuccess))\n    }\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_TEMPLATE_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n        page: source,\n        mode: selectedPipelineType,\n      },\n    })\n  }\n\n  const isNoTemplateOptions =\n    source === RdiPipelineTabs.Config\n      ? selectedDbType === NO_TEMPLATE_VALUE ||\n        selectedPipelineType === NO_TEMPLATE_VALUE\n      : selectedPipelineType === NO_TEMPLATE_VALUE\n\n  useEffect(() => {\n    if (!selectedPipelineType || !data.length) {\n      setDbTypeOptions(NO_OPTIONS)\n      setSelectedDbType(NO_OPTIONS[0].value)\n\n      return\n    }\n\n    const selectedStrategy = data.find(\n      ({ strategy }) => strategy === selectedPipelineType,\n    )\n\n    const newDbTypeOptions = selectedStrategy?.databases?.map((db) => ({\n      value: db,\n      inputDisplay: db,\n    }))\n\n    if (newDbTypeOptions?.length) {\n      setDbTypeOptions(newDbTypeOptions)\n      setSelectedDbType(newDbTypeOptions[0].value)\n    } else {\n      setDbTypeOptions(NO_OPTIONS)\n      setSelectedDbType(NO_OPTIONS[0].value)\n    }\n  }, [data, selectedPipelineType])\n\n  useEffect(() => {\n    const newPipelineTypeOptions = data.map((strategy) => ({\n      value: strategy.strategy,\n      inputDisplay: strategy.strategy,\n    }))\n\n    setPipelineTypeOptions(\n      newPipelineTypeOptions.length ? newPipelineTypeOptions : NO_OPTIONS,\n    )\n\n    if (data?.length) {\n      const initialSelectedOption =\n        newPipelineTypeOptions.find(\n          (strategy) => strategy.value === INGEST_OPTION,\n        ) || newPipelineTypeOptions[0]\n      setSelectedPipelineType(initialSelectedOption.value)\n    } else {\n      setSelectedPipelineType(NO_OPTIONS[0].value)\n    }\n  }, [data])\n\n  useEffect(() => {\n    dispatch(fetchPipelineStrategies(rdiInstanceId))\n  }, [])\n\n  return (\n    <Col gap=\"l\">\n      <Title size=\"M\" color=\"primary\">\n        Select a template\n      </Title>\n      <form>\n        <Spacer size=\"xs\" />\n        {pipelineTypeOptions?.length > 1 && (\n          <FormField label=\"Pipeline type\">\n            <RiSelect\n              options={pipelineTypeOptions}\n              valueRender={defaultValueRender}\n              value={selectedPipelineType}\n              onChange={(value) => setSelectedPipelineType(value)}\n              data-testid=\"pipeline-type-select\"\n            />\n          </FormField>\n        )}\n        {source === RdiPipelineTabs.Config && (\n          <FormField label=\"Database type\">\n            <RiSelect\n              options={dbTypeOptions}\n              valueRender={defaultValueRender}\n              value={selectedDbType}\n              onChange={(value) => setSelectedDbType(value)}\n              data-testid=\"db-type-select\"\n            />\n          </FormField>\n        )}\n      </form>\n      <Row gap=\"m\" justify=\"end\">\n        <SecondaryButton\n          onClick={handleCancel}\n          size=\"m\"\n          data-testid=\"template-cancel-btn\"\n        >\n          Cancel\n        </SecondaryButton>\n        <RiTooltip\n          content={getTooltipContent(value, isNoTemplateOptions)}\n          position=\"bottom\"\n          anchorClassName=\"flex-row\"\n        >\n          <PrimaryButton\n            disabled={isNoTemplateOptions || !!value}\n            onClick={handleApply}\n            loading={loading}\n            size=\"m\"\n            data-testid=\"template-apply-btn\"\n          >\n            Apply\n          </PrimaryButton>\n        </RiTooltip>\n      </Row>\n    </Col>\n  )\n}\n\nexport default TemplateForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-form/constants.ts",
    "content": "export const NO_TEMPLATE_LABEL = 'No template'\n\nexport const NO_TEMPLATE_VALUE = 'no_template'\n\nexport const NO_OPTIONS = [\n  {\n    value: NO_TEMPLATE_VALUE,\n    label: NO_TEMPLATE_LABEL,\n  },\n]\n\nexport const INGEST_OPTION = 'ingest'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-form/index.ts",
    "content": "import TemplateForm from './TemplateForm'\n\nexport default TemplateForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/TemplatePopover.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\n\nimport {\n  fireEvent,\n  render,\n  cleanup,\n  mockedStore,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { getPipelineStrategies } from 'uiSrc/slices/rdi/pipeline'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport TemplatePopover, { Props } from './TemplatePopover'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('TemplatePopover', () => {\n  it('should render', () => {\n    expect(render(<TemplatePopover {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call proper actions on open popover', () => {\n    const mockSetIsPopoverOpen = jest.fn()\n\n    render(\n      <TemplatePopover\n        {...instance(mockedProps)}\n        source={RdiPipelineTabs.Config}\n        setIsPopoverOpen={mockSetIsPopoverOpen}\n      />,\n    )\n\n    fireEvent.click(\n      screen.getByTestId(`template-trigger-${RdiPipelineTabs.Config}`),\n    )\n\n    const expectedActions = [getPipelineStrategies()]\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n    expect(mockSetIsPopoverOpen).toBeCalledWith(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/TemplatePopover.tsx",
    "content": "import React from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport TemplateForm from 'uiSrc/pages/rdi/pipeline-management/components/template-form'\nimport { fetchPipelineStrategies } from 'uiSrc/slices/rdi/pipeline'\nimport { RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport { OutsideClickDetector } from 'uiSrc/components/base/utils'\n\nimport { SecondaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiPopover } from 'uiSrc/components/base'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  isPopoverOpen: boolean\n  setIsPopoverOpen: (value: boolean) => void\n  value: string\n  setFieldValue: (template: string) => void\n  loading: boolean\n  source: RdiPipelineTabs\n}\n\nconst TemplatePopover = (props: Props) => {\n  const {\n    isPopoverOpen,\n    setIsPopoverOpen,\n    value,\n    loading,\n    setFieldValue,\n    source,\n  } = props\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  const handleOpen = () => {\n    dispatch(fetchPipelineStrategies(rdiInstanceId))\n    setIsPopoverOpen(true)\n  }\n\n  const handleClose = () => {\n    setIsPopoverOpen(false)\n  }\n\n  return (\n    <OutsideClickDetector onOutsideClick={handleClose}>\n      <RiPopover\n        ownFocus\n        anchorPosition=\"downRight\"\n        isOpen={isPopoverOpen}\n        closePopover={handleClose}\n        panelClassName={styles.popoverWrapper}\n        button={\n          <SecondaryButton\n            inverted\n            size=\"s\"\n            className={styles.btn}\n            aria-label=\"Insert template\"\n            disabled={loading}\n            onClick={handleOpen}\n            data-testid={`template-trigger-${source}`}\n          >\n            Insert template\n          </SecondaryButton>\n        }\n      >\n        <TemplateForm\n          closePopover={handleClose}\n          setTemplate={setFieldValue}\n          source={source}\n          value={value}\n        />\n      </RiPopover>\n    </OutsideClickDetector>\n  )\n}\n\nexport default TemplatePopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/index.ts",
    "content": "import TemplatePopover from './TemplatePopover'\n\nexport default TemplatePopover\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/styles.module.scss",
    "content": ".popoverWrapper {\n  width: 299px;\n\n  &:global(.euiPanel) {\n    padding: 16px !important;\n  }\n}\n\n.anchor {\n  .btn {\n    display: flex;\n    align-items: center;\n    background-color: var(--insightsTriggerBgColor) !important;\n    color: var(--euiTooltipTextColor) !important;\n    border-radius: 4px;\n    border-color: var(--tableRowSelectedColor) !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/TestConnectionsLog.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { TransformGroupResult } from 'uiSrc/slices/interfaces'\n\nimport TestConnectionsLog from './TestConnectionsLog'\n\ndescribe('TestConnectionsLog', () => {\n  it('should render', () => {\n    const mockedData: TransformGroupResult = { fail: [], success: [] }\n    render(<TestConnectionsLog data={mockedData} />)\n  })\n\n  it('should render the correct status when only failed connections exist', () => {\n    const mockedData: TransformGroupResult = {\n      fail: [{ target: 'localhost:1233', error: 'some error' }],\n      success: [],\n    }\n    render(<TestConnectionsLog data={mockedData} />)\n\n    expect(screen.queryByText('Successful')).not.toBeInTheDocument()\n    expect(screen.queryByText('some error')).toBeInTheDocument()\n  })\n\n  it('should render all results', () => {\n    const mockedData: TransformGroupResult = {\n      fail: [\n        { target: 'localhost:1233', error: 'some error' },\n        { target: 'localhost:1234', error: 'timeout' },\n      ],\n      success: [{ target: 'localhost:1235' }],\n    }\n\n    render(<TestConnectionsLog data={mockedData} />)\n\n    expect(screen.queryAllByText('Successful').length).toEqual(1)\n    expect(screen.queryAllByText('some error').length).toEqual(1)\n    expect(screen.queryAllByText('timeout').length).toEqual(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/TestConnectionsLog.tsx",
    "content": "import React from 'react'\nimport {\n  IRdiConnectionResult,\n  TransformGroupResult,\n} from 'uiSrc/slices/interfaces'\nimport { StyledRdiAnalyticsTable } from 'uiSrc/pages/rdi/statistics/styles'\nimport { ColumnDefinition, Table } from 'uiSrc/components/base/layout/table'\nimport { RiTooltip } from 'uiSrc/components'\n\nconst columns: ColumnDefinition<IRdiConnectionResult>[] = [\n  {\n    header: 'Endpoint',\n    id: 'endpoint',\n    accessorKey: 'target',\n  },\n  {\n    header: 'Results',\n    id: 'results',\n    accessorKey: 'error',\n    cell: ({\n      row: {\n        original: { error: error },\n      },\n    }) => {\n      if (error) {\n        return <RiTooltip content={error}>{error}</RiTooltip>\n      }\n      return 'Successful'\n    },\n  },\n]\n\nexport interface Props {\n  data: TransformGroupResult\n}\n\nconst TestConnectionsLog = (props: Props) => {\n  const { data } = props\n  const statusData = [...data.success, ...data.fail]\n\n  return (\n    <>\n      <StyledRdiAnalyticsTable columns={columns} data={statusData} stripedRows>\n        <Table.Root></Table.Root>\n        <Table.Header />\n        <Table.Body />\n      </StyledRdiAnalyticsTable>\n    </>\n  )\n}\n\nexport default TestConnectionsLog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/index.ts",
    "content": "import TestConnectionsLog from './TestConnectionsLog'\n\nexport default TestConnectionsLog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-panel/TestConnectionsPanel.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { rdiTestConnectionsSelector } from 'uiSrc/slices/rdi/testConnections'\n\nimport TestConnectionsPanel, { Props } from './TestConnectionsPanel'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/rdi/testConnections', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/testConnections'),\n  rdiTestConnectionsSelector: jest.fn().mockReturnValue({\n    loading: false,\n  }),\n}))\n\ndescribe('TestConnectionsPanel', () => {\n  it('should render', () => {\n    render(<TestConnectionsPanel {...instance(mockedProps)} />)\n  })\n\n  it('should call onClose', () => {\n    const mockOnClose = jest.fn()\n    render(<TestConnectionsPanel onClose={mockOnClose} />)\n\n    fireEvent.click(screen.getByTestId('close-test-connections-btn'))\n\n    expect(mockOnClose).toBeCalled()\n  })\n\n  it('should render loading', () => {\n    const rdiTestConnectionsSelectorMock = jest.fn().mockReturnValue({\n      loading: true,\n    })\n    ;(rdiTestConnectionsSelector as jest.Mock).mockImplementation(\n      rdiTestConnectionsSelectorMock,\n    )\n\n    render(<TestConnectionsPanel {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('test-connections-loader')).toBeInTheDocument()\n  })\n\n  it('should show \"No results found\" when results are null', () => {\n    ;(rdiTestConnectionsSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      results: null,\n    })\n\n    render(<TestConnectionsPanel {...instance(mockedProps)} />)\n\n    expect(\n      screen.getByText('No results found. Please try again.'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render TestConnectionsLog for source and target connections', async () => {\n    const mockResults = {\n      source: {\n        success: [],\n        fail: [\n          {\n            target: 'source',\n            error: 'Something bad happened',\n          },\n        ],\n      },\n      target: {\n        success: [\n          {\n            target: 'Test-target-connection',\n          },\n        ],\n        fail: [],\n      },\n    }\n\n    ;(rdiTestConnectionsSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      results: mockResults,\n    })\n\n    render(<TestConnectionsPanel {...instance(mockedProps)} />)\n\n    expect(screen.getByText('Source connections')).toBeInTheDocument()\n    expect(screen.getByText('source')).toBeInTheDocument()\n    expect(screen.getByText('Something bad happened')).toBeInTheDocument()\n\n    expect(screen.getByText('Target connections')).toBeInTheDocument()\n    expect(screen.getByText('Test-target-connection')).toBeInTheDocument()\n    expect(screen.getByText('Successful')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-panel/TestConnectionsPanel.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport TestConnectionsLog from 'uiSrc/pages/rdi/pipeline-management/components/test-connections-log'\nimport { rdiTestConnectionsSelector } from 'uiSrc/slices/rdi/testConnections'\n\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { Loader } from 'uiSrc/components/base/display'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { TestConnectionContainer } from 'uiSrc/pages/rdi/pipeline-management/components/test-connections-panel/styles'\n\ninterface TestConnectionPanelWrapperProps {\n  onClose: () => void\n  children?: React.ReactNode\n}\n\nconst TestConnectionPanelWrapper = ({\n  children,\n  onClose,\n}: TestConnectionPanelWrapperProps) => (\n  <TestConnectionContainer grow data-testid=\"test-connection-panel\" gap=\"xxl\">\n    <FlexItem>\n      <Row align=\"center\" justify=\"between\">\n        <Title size=\"L\" color=\"primary\">\n          Test connection\n        </Title>\n        <IconButton\n          icon={CancelSlimIcon}\n          aria-label=\"close test connections panel\"\n          onClick={onClose}\n          data-testid=\"close-test-connections-btn\"\n        />\n      </Row>\n    </FlexItem>\n    <FlexItem />\n    <FlexItem grow>{children}</FlexItem>\n  </TestConnectionContainer>\n)\n\nexport interface Props {\n  onClose: () => void\n}\n\nconst TestConnectionsPanel = (props: Props) => {\n  const { onClose } = props\n  const { loading, results } = useSelector(rdiTestConnectionsSelector)\n\n  if (loading) {\n    return (\n      <TestConnectionPanelWrapper onClose={onClose}>\n        <Col centered>\n          <FlexItem>\n            <Text>Loading results...</Text>\n          </FlexItem>\n          <FlexItem>\n            <Loader\n              data-testid=\"test-connections-loader\"\n              color=\"secondary\"\n              size=\"xl\"\n            />\n          </FlexItem>\n        </Col>\n      </TestConnectionPanelWrapper>\n    )\n  }\n\n  if (!results) {\n    return (\n      <TestConnectionPanelWrapper onClose={onClose}>\n        <Col centered>\n          <Text>No results found. Please try again.</Text>\n        </Col>\n      </TestConnectionPanelWrapper>\n    )\n  }\n\n  return (\n    <TestConnectionPanelWrapper onClose={onClose}>\n      <Col gap=\"xxl\">\n        <FlexItem>\n          <Text color=\"primary\">Source connections</Text>\n          <TestConnectionsLog data={results.source} />\n        </FlexItem>\n        <FlexItem>\n          <Divider colorVariable=\"separatorColor\" />\n        </FlexItem>\n        <FlexItem>\n          <Text color=\"primary\">Target connections</Text>\n          <TestConnectionsLog data={results.target} />\n        </FlexItem>\n      </Col>\n    </TestConnectionPanelWrapper>\n  )\n}\n\nexport default TestConnectionsPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-panel/index.ts",
    "content": "import TestConnectionsPanel from './TestConnectionsPanel'\n\nexport default TestConnectionsPanel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-panel/styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const TestConnectionContainer = styled(Col)`\n  border-left: 1px solid\n    ${({ theme }: { theme: Theme }) =>\n      theme.semantic.color.border.informative100};\n  width: 440px;\n  padding: 2.4rem;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/upload-modal/UploadModal.spec.tsx",
    "content": "import React from 'react'\nimport { loadAsync } from 'jszip'\nimport { cloneDeep } from 'lodash'\n\nimport {\n  rdiPipelineSelector,\n  setChangedFiles,\n  setConfigValidationErrors,\n  setIsPipelineValid,\n  setJobsValidationErrors,\n  setPipelineConfig,\n  setPipelineJobs,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  userEvent,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport { validatePipeline } from 'uiSrc/components/yaml-validator'\nimport UploadModal, { Props } from './UploadModal'\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    config: 'value',\n    jobs: [\n      { name: 'job1', value: 'value' },\n      { name: 'job2', value: 'value' },\n    ],\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('jszip', () => ({\n  ...jest.requireActual('jszip'),\n  loadAsync: jest.fn().mockReturnValue({\n    file: jest.fn().mockReturnValue({\n      async: jest.fn().mockReturnValue('config'),\n    }),\n    files: {\n      'jobs/': {\n        async: jest.fn(),\n      },\n      'jobs/job1.yaml': {\n        async: jest.fn().mockReturnValue('value1'),\n      },\n      'jobs/job2.yaml': {\n        async: jest.fn().mockReturnValue('value2'),\n      },\n    },\n  }),\n}))\n\njest.mock('uiSrc/components/yaml-validator', () => ({\n  validatePipeline: jest.fn(),\n}))\n\nconst button = (\n  <button type=\"button\" data-testid=\"btn\">\n    test\n  </button>\n)\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  jest.clearAllMocks()\n})\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\nconst renderUploadModal = (props: Partial<Props> = {}) => {\n  const { trigger = button, ...rest } = props\n  return render(<UploadModal trigger={trigger} {...rest} />, { store })\n}\n\ndescribe('UploadModal', () => {\n  it('should render', () => {\n    expect(renderUploadModal()).toBeTruthy()\n  })\n\n  it('should call proper telemetry event when button is clicked', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    renderUploadModal()\n\n    await userEvent.click(screen.getByTestId('btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_UPLOAD_FROM_FILE_CLICKED,\n      eventData: {\n        id: 'rdiInstanceId',\n      },\n    })\n  })\n\n  it('should call proper telemetry event when file upload is successful', async () => {\n    renderUploadModal()\n\n    await userEvent.click(screen.getByTestId('btn'))\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n    })\n\n    await userEvent.click(screen.getByTestId('submit-btn'))\n\n    const expectedActions = [\n      setChangedFiles({\n        config: FileChangeType.Added,\n        job1: FileChangeType.Added,\n        job2: FileChangeType.Added,\n      }),\n      setPipelineConfig('config'),\n      setPipelineJobs([\n        { name: 'job1', value: 'value1' },\n        { name: 'job2', value: 'value2' },\n      ]),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper telemetry event when file upload is successful', async () => {\n    const onUploadedPipelineMock = jest.fn()\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    renderUploadModal({ onUploadedPipeline: onUploadedPipelineMock })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n      fireEvent.click(screen.getByTestId('submit-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_UPLOAD_SUCCEEDED,\n      eventData: {\n        id: 'rdiInstanceId',\n        jobsNumber: 2,\n        source: 'file',\n      },\n    })\n    expect(onUploadedPipelineMock).toBeCalled()\n  })\n\n  it('should call proper telemetry event when file upload has failed', async () => {\n    ;(loadAsync as jest.Mock).mockImplementation(() => {\n      throw new Error('error')\n    })\n\n    const onUploadedPipelineMock = jest.fn()\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    renderUploadModal({ onUploadedPipeline: onUploadedPipelineMock })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n      fireEvent.click(screen.getByTestId('submit-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_PIPELINE_UPLOAD_FAILED,\n      eventData: {\n        id: 'rdiInstanceId',\n        errorMessage: 'error',\n        source: 'file',\n      },\n    })\n    expect(onUploadedPipelineMock).not.toBeCalled()\n  })\n\n  it('should call onClose when close button is clicked', async () => {\n    const onCloseMock = jest.fn()\n\n    renderUploadModal({ onClose: onCloseMock })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('import-file-modal-close-btn'))\n    })\n\n    expect(onCloseMock).toBeCalled()\n  })\n\n  it('should render disabled upload button when loading', () => {\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(() => ({\n      loading: true,\n    }))\n\n    renderUploadModal()\n\n    expect(screen.getByTestId('btn')).toBeDisabled()\n  })\n\n  it('should open modal when upload button is clicked', async () => {\n    renderUploadModal()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    waitFor(() => {\n      expect(screen.getByTestId('import-file-modal')).toBeInTheDocument()\n    })\n  })\n\n  it('should call validatePipeline and dispatch validation actions on successful upload', async () => {\n    ;(validatePipeline as jest.Mock).mockReturnValue({\n      result: true,\n      configValidationErrors: [],\n      jobsValidationErrors: {},\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockReturnValue({\n      loading: false,\n      schema: 'mockSchema',\n    })\n    ;(loadAsync as jest.Mock).mockReturnValue({\n      file: jest.fn().mockReturnValue({\n        async: jest.fn().mockReturnValue('mockConfig'),\n      }),\n      files: {\n        'jobs/': {\n          async: jest.fn(),\n        },\n        'jobs/job1.yaml': {\n          async: jest.fn().mockReturnValue('value'),\n        },\n      },\n    })\n\n    renderUploadModal()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n      fireEvent.click(screen.getByTestId('submit-btn'))\n    })\n\n    expect(validatePipeline).toHaveBeenCalledWith({\n      config: 'mockConfig',\n      schema: 'mockSchema',\n      jobs: [{ name: 'job1', value: 'value' }],\n    })\n\n    expect(store.getActions()).toEqual([\n      setChangedFiles({\n        config: FileChangeType.Added,\n        job1: FileChangeType.Added,\n      }),\n      setPipelineConfig('mockConfig'),\n      setPipelineJobs([{ name: 'job1', value: 'value' }]),\n      setConfigValidationErrors([]),\n      setJobsValidationErrors({}),\n      setIsPipelineValid(true),\n    ])\n  })\n\n  it('should NOT call validatePipeline if schema is missing', async () => {\n    const rdiPipelineSelectorMock = rdiPipelineSelector as jest.Mock\n    rdiPipelineSelectorMock.mockReturnValueOnce({\n      loading: false,\n      schema: null,\n    })\n\n    renderUploadModal()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n      fireEvent.click(screen.getByTestId('submit-btn'))\n    })\n\n    expect(validatePipeline).not.toHaveBeenCalled()\n  })\n\n  it('should NOT call validatePipeline if jobs are missing', async () => {\n    ;(loadAsync as jest.Mock).mockReturnValue({\n      file: jest.fn().mockReturnValue({\n        async: jest.fn().mockReturnValue('config'),\n      }),\n      files: {\n        'jobs/': {\n          async: jest.fn(),\n        },\n        // No jobs\n      },\n    })\n\n    renderUploadModal()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n      fireEvent.click(screen.getByTestId('submit-btn'))\n    })\n\n    expect(validatePipeline).not.toHaveBeenCalled()\n  })\n\n  it('should NOT call validatePipeline if config is missing', async () => {\n    ;(loadAsync as jest.Mock).mockReturnValue({\n      file: jest.fn().mockReturnValue({\n        async: jest.fn().mockReturnValue(null),\n      }),\n      files: {\n        'jobs/': {\n          async: jest.fn(),\n        },\n        'jobs/job1.yaml': {\n          async: jest.fn().mockReturnValue('value1'),\n        },\n        'jobs/job2.yaml': {\n          async: jest.fn().mockReturnValue('value2'),\n        },\n      },\n    })\n\n    renderUploadModal()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('btn'))\n    })\n\n    await act(() => {\n      fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n        target: { files: ['file'] },\n      })\n      fireEvent.click(screen.getByTestId('submit-btn'))\n    })\n\n    expect(validatePipeline).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/upload-modal/UploadModal.tsx",
    "content": "import JSZip from 'jszip'\nimport React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { validatePipeline } from 'uiSrc/components/yaml-validator'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport {\n  rdiPipelineSelector,\n  setChangedFiles,\n  setConfigValidationErrors,\n  setIsPipelineValid,\n  setJobsValidationErrors,\n  setPipelineConfig,\n  setPipelineJobs,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport UploadDialog from './components/upload-dialog/UploadDialog'\n\nexport interface Props {\n  trigger?: React.ReactElement\n  onUploadedPipeline?: () => void\n  visible?: boolean\n  onClose?: () => void\n}\n\nconst UploadModal = (props: Props) => {\n  const { trigger, visible, onUploadedPipeline, onClose } = props\n\n  const [isModalVisible, setIsModalVisible] = useState(visible)\n  const [file, setFile] = useState<File>()\n  const [isUploaded, setIsUploaded] = useState(false)\n  const [error, setError] = useState<string>()\n\n  const {\n    loading,\n    config: pipelineConfig,\n    jobs: pipelineJobs,\n    schema,\n    monacoJobsSchema,\n    jobNameSchema,\n  } = useSelector(rdiPipelineSelector)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n\n  const dispatch = useDispatch()\n\n  const validateZip = (zip: JSZip) => {\n    // check if config.yaml exists\n    if (zip.file('config.yaml') === null) {\n      throw new Error('config.yaml is missing')\n    }\n\n    // check if job files exist\n    const jobFiles = Object.keys(zip.files).filter((filename) =>\n      filename.startsWith('jobs/'),\n    )\n    if (!jobFiles.length) {\n      throw new Error('No jobs folder found')\n    }\n  }\n\n  const handleUploadClick = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_PIPELINE_UPLOAD_FROM_FILE_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n      },\n    })\n\n    setIsModalVisible(true)\n    trigger?.props?.onClick?.()\n  }\n\n  const handleConfirmModal = async () => {\n    if (!file) {\n      return\n    }\n\n    // unzip config and job contents and set form values\n    try {\n      const zip = await JSZip.loadAsync(file)\n\n      validateZip(zip)\n\n      const config = await zip.file('config.yaml')?.async('string')\n      const jobs = await Promise.all(\n        Object.keys(zip.files)\n          .filter(\n            (filename) =>\n              filename.startsWith('jobs/') && filename.endsWith('.yaml'),\n          )\n          .map(async (filename) => ({\n            name: filename.split('/')[1].split('.')[0],\n            value: await zip.files[filename].async('string'),\n          })),\n      )\n\n      const uploadFiles = {\n        config: FileChangeType.Added,\n        ...jobs.reduce(\n          (acc, { name }) => {\n            acc[name] = FileChangeType.Added\n            return acc\n          },\n          {} as Record<string, FileChangeType>,\n        ),\n      }\n\n      dispatch(setChangedFiles(uploadFiles))\n\n      dispatch(setPipelineConfig(config || ''))\n      dispatch(setPipelineJobs(jobs))\n\n      if (config && schema && jobs?.length) {\n        const { result, configValidationErrors, jobsValidationErrors } =\n          validatePipeline({\n            config,\n            schema,\n            jobs,\n            monacoJobsSchema,\n            jobNameSchema,\n          })\n\n        dispatch(setConfigValidationErrors(configValidationErrors))\n        dispatch(setJobsValidationErrors(jobsValidationErrors))\n        dispatch(setIsPipelineValid(result))\n      }\n\n      sendEventTelemetry({\n        event: TelemetryEvent.RDI_PIPELINE_UPLOAD_SUCCEEDED,\n        eventData: {\n          id: rdiInstanceId,\n          jobsNumber: jobs.length,\n          source: 'file',\n        },\n      })\n\n      setIsUploaded(true)\n      onUploadedPipeline?.()\n    } catch (err) {\n      const errorMessage = (err as Error).message\n\n      sendEventTelemetry({\n        event: TelemetryEvent.RDI_PIPELINE_UPLOAD_FAILED,\n        eventData: {\n          id: rdiInstanceId,\n          errorMessage,\n          source: 'file',\n        },\n      })\n\n      setError(errorMessage)\n    }\n  }\n\n  const handleCloseModal = () => {\n    setIsModalVisible(false)\n    setIsUploaded(false)\n    setError(undefined)\n    onClose?.()\n  }\n\n  const handleFileChangeModal = (file: File) => {\n    setFile(file)\n  }\n\n  const button = trigger\n    ? React.cloneElement(trigger, {\n        disabled: loading,\n        onClick: handleUploadClick,\n      })\n    : null\n\n  return (\n    <>\n      {isModalVisible && (\n        <UploadDialog\n          onClose={handleCloseModal}\n          onConfirm={handleConfirmModal}\n          onFileChange={handleFileChangeModal}\n          isUploaded={isUploaded}\n          showWarning={\n            (!!pipelineConfig || !!pipelineJobs?.length) &&\n            !isUploaded &&\n            !error\n          }\n          error={error}\n          loading={loading}\n        />\n      )}\n      {button}\n    </>\n  )\n}\n\nexport default UploadModal\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/upload-modal/components/upload-dialog/UploadDialog.spec.tsx",
    "content": "import React from 'react'\n\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport UploadDialog, { Props } from './UploadDialog'\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n  }),\n}))\n\njest.mock('formik', () => ({\n  ...jest.requireActual('formik'),\n  useFormikContext: jest.fn().mockReturnValue({\n    values: {\n      config: 'value',\n      jobs: [\n        { name: 'job1', value: 'value' },\n        { name: 'job2', value: 'value' },\n      ],\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/components/base/display', () => {\n  const actual = jest.requireActual('uiSrc/components/base/display')\n\n  return {\n    ...actual,\n    Modal: {\n      ...actual.Modal,\n      Content: {\n        ...actual.Modal.Content,\n        Header: {\n          ...actual.Modal.Content.Header,\n          Title: jest.fn().mockReturnValue(null),\n        },\n      },\n    },\n  }\n})\n\nconst mockedProps: Props = {\n  onClose: jest.fn(),\n  onConfirm: jest.fn(),\n  onFileChange: jest.fn(),\n  isUploaded: false,\n  showWarning: false,\n  loading: false,\n}\n\ndescribe('UploadDialog', () => {\n  it('should render', () => {\n    expect(render(<UploadDialog {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should disable upload button when no file is selected', () => {\n    render(<UploadDialog {...mockedProps} />)\n\n    expect(screen.getByTestId('submit-btn')).toBeDisabled()\n  })\n\n  it('should enable upload button when file is selected', () => {\n    render(<UploadDialog {...mockedProps} />)\n\n    fireEvent.change(screen.getByTestId('import-file-modal-filepicker'), {\n      target: { files: ['file'] },\n    })\n\n    expect(screen.getByTestId('submit-btn')).not.toBeDisabled()\n  })\n\n  it('should render warning message', () => {\n    render(<UploadDialog {...mockedProps} showWarning />)\n\n    expect(screen.getByTestId('input-file-warning')).toBeInTheDocument()\n  })\n\n  it('should only allow .zip files', () => {\n    render(<UploadDialog {...mockedProps} />)\n\n    expect(screen.getByTestId('import-file-modal-filepicker')).toHaveAttribute(\n      'accept',\n      '.zip',\n    )\n  })\n\n  // Skipping until the title issue in the modal is fixed\n  it.skip('should show custom results success title after submit', () => {\n    render(<UploadDialog {...mockedProps} isUploaded />)\n\n    expect(screen.getByTestId('import-file-modal-title')).toHaveTextContent(\n      'Pipeline has been uploaded',\n    )\n  })\n\n  // Skipping until the title issue in the modal is fixed\n  it.skip('should show custom results failed title after submit', () => {\n    render(<UploadDialog {...mockedProps} isUploaded error=\"error\" />)\n\n    expect(screen.getByTestId('import-file-modal-title')).toHaveTextContent(\n      'Failed to upload pipeline',\n    )\n  })\n\n  it('should show results after submit', () => {\n    render(<UploadDialog {...mockedProps} isUploaded />)\n\n    expect(screen.getByTestId('result-succeeded')).toHaveTextContent(\n      'A new pipeline has been successfully uploaded.',\n    )\n  })\n\n  it('should show error message when error is present', () => {\n    render(<UploadDialog {...mockedProps} error=\"error\" />)\n\n    expect(screen.getByTestId('result-failed')).toHaveTextContent(\n      'There was a problem with the .zip file',\n    )\n    expect(screen.getByTestId('result-failed')).toHaveTextContent('error')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/upload-modal/components/upload-dialog/UploadDialog.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport ImportFileModal from 'uiSrc/components/import-file-modal'\n\nexport interface Props {\n  onClose: () => void\n  onConfirm: () => void\n  onFileChange: (file: File) => void\n  isUploaded: boolean\n  showWarning: boolean\n  error?: string\n  loading: boolean\n}\n\nconst warningMessage =\n  'If a new pipeline is uploaded, existing pipeline configuration and transformation' +\n  'jobs will be overwritten. Changes will not be applied until the pipeline is deployed.'\n\nconst UploadDialog = ({\n  onClose,\n  onConfirm,\n  onFileChange,\n  isUploaded,\n  showWarning,\n  error,\n  loading,\n}: Props) => {\n  const [isSubmitDisabled, setIsSubmitDisabled] = useState<boolean>(true)\n\n  const handleFileChange = (files: FileList | null) => {\n    if (!files?.length) {\n      setIsSubmitDisabled(true)\n    } else {\n      onFileChange(files[0])\n      setIsSubmitDisabled(false)\n    }\n  }\n\n  return (\n    <ImportFileModal\n      onClose={onClose}\n      onFileChange={handleFileChange}\n      onSubmit={onConfirm}\n      title={\n        showWarning\n          ? 'Upload a new pipeline'\n          : 'Upload an archive with an RDI pipeline'\n      }\n      resultsTitle={\n        !error ? 'Pipeline has been uploaded' : 'Failed to upload pipeline'\n      }\n      submitResults={\n        <Text>A new pipeline has been successfully uploaded.</Text>\n      }\n      loading={loading}\n      data={isUploaded}\n      warning={\n        showWarning ? (\n          <Text data-testid=\"input-file-warning\">{warningMessage}</Text>\n        ) : null\n      }\n      error={error}\n      errorMessage=\"There was a problem with the .zip file\"\n      isInvalid={false}\n      isSubmitDisabled={isSubmitDisabled}\n      submitBtnText=\"Upload\"\n      acceptedFileExtension=\".zip\"\n    />\n  )\n}\n\nexport default UploadDialog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/validation-errors-list/ValidationErrorsList.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ValidationErrorsList, { Props } from './ValidationErrorsList'\n\ndescribe('ValidationErrorsList', () => {\n  it('should render', () => {\n    const props: Props = {\n      validationErrors: [],\n    }\n    expect(render(<ValidationErrorsList {...props} />)).toBeTruthy()\n  })\n\n  it('should not render anything when validationErrors is undefined', () => {\n    const props: Props = {\n      validationErrors: undefined as any,\n    }\n    const { container } = render(<ValidationErrorsList {...props} />)\n\n    expect(container.firstChild).toBeNull()\n  })\n\n  it('should render a single validation error', () => {\n    const props: Props = {\n      validationErrors: ['Invalid configuration format'],\n    }\n    render(<ValidationErrorsList {...props} />)\n\n    expect(screen.getByText('Invalid configuration format')).toBeInTheDocument()\n    expect(screen.getByRole('list')).toBeInTheDocument()\n    expect(screen.getAllByRole('listitem')).toHaveLength(1)\n  })\n\n  it('should render multiple validation errors', () => {\n    const props: Props = {\n      validationErrors: [\n        'Missing required field: name',\n        'Invalid data type for age',\n        'Email format is incorrect',\n      ],\n    }\n    render(<ValidationErrorsList {...props} />)\n\n    expect(screen.getByText('Missing required field: name')).toBeInTheDocument()\n    expect(screen.getByText('Invalid data type for age')).toBeInTheDocument()\n    expect(screen.getByText('Email format is incorrect')).toBeInTheDocument()\n    expect(screen.getByRole('list')).toBeInTheDocument()\n    expect(screen.getAllByRole('listitem')).toHaveLength(3)\n  })\n\n  it('should render validation errors as list items within a Text component', () => {\n    const props: Props = {\n      validationErrors: ['Error message 1', 'Error message 2'],\n    }\n    render(<ValidationErrorsList {...props} />)\n\n    const list = screen.getByRole('list')\n    expect(list.tagName).toBe('UL')\n    expect(list.parentElement?.tagName).toBe('DIV')\n\n    const listItems = screen.getAllByRole('listitem')\n    expect(listItems).toHaveLength(2)\n    expect(listItems[0]).toHaveTextContent('Error message 1')\n    expect(listItems[1]).toHaveTextContent('Error message 2')\n  })\n\n  it('should handle special characters and HTML in error messages', () => {\n    const props: Props = {\n      validationErrors: [\n        'Error with <script>alert(\"xss\")</script>',\n        'Error with & special characters',\n        'Error with \"quotes\" and \\'apostrophes\\'',\n      ],\n    }\n    render(<ValidationErrorsList {...props} />)\n\n    expect(\n      screen.getByText('Error with <script>alert(\"xss\")</script>'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByText('Error with & special characters'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByText('Error with \"quotes\" and \\'apostrophes\\''),\n    ).toBeInTheDocument()\n  })\n\n  it('should handle empty string errors', () => {\n    const props: Props = {\n      validationErrors: ['', 'Valid error message', ''],\n    }\n    render(<ValidationErrorsList {...props} />)\n\n    const listItems = screen.getAllByRole('listitem')\n    expect(listItems).toHaveLength(3)\n    expect(listItems[0]).toHaveTextContent('')\n    expect(listItems[1]).toHaveTextContent('Valid error message')\n    expect(listItems[2]).toHaveTextContent('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/components/validation-errors-list/ValidationErrorsList.tsx",
    "content": "import React from 'react'\n\nexport interface Props {\n  validationErrors: string[]\n}\n\nconst ValidationErrorsList = (props: Props) => {\n  const { validationErrors } = props\n\n  if (!validationErrors?.length) {\n    return null\n  }\n\n  return (\n    <ul>\n      {validationErrors.map((err, index) => (\n        // eslint-disable-next-line react/no-array-index-key\n        <li key={index}>{err}</li>\n      ))}\n    </ul>\n  )\n}\n\nexport default ValidationErrorsList\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/index.ts",
    "content": "import PipelineManagementPage from './PipelineManagementPage'\n\nexport default PipelineManagementPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  rdiPipelineSelector,\n  setChangedFile,\n  deleteChangedFile,\n  setPipelineConfig,\n  getPipelineStrategies,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { rdiTestConnectionsSelector } from 'uiSrc/slices/rdi/testConnections'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  sendPageViewTelemetry,\n  TelemetryPageView,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport {\n  addErrorNotification,\n  type IAddInstanceErrorPayload,\n} from 'uiSrc/slices/app/notifications'\nimport Config from './Config'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    schema: { config: { test: {} } },\n    data: null,\n    config: `connections:\n            target:\n              type: redis\n          `,\n    jobs: [\n      {\n        name: 'jobName',\n        value: `job:\n      transform:\n        type: sql\n    `,\n      },\n      {\n        name: 'job2',\n        value: `job2:\n      transform:\n        type: redis\n    `,\n      },\n    ],\n  }),\n}))\n\njest.mock('uiSrc/slices/rdi/testConnections', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/testConnections'),\n  rdiTestConnectionsSelector: jest.fn().mockReturnValue({\n    loading: false,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('Config', () => {\n  it('should render', () => {\n    expect(render(<Config />)).toBeTruthy()\n  })\n\n  it('should call proper sendPageViewTelemetry', () => {\n    const sendPageViewTelemetryMock = jest.fn()\n    ;(sendPageViewTelemetry as jest.Mock).mockImplementation(\n      () => sendPageViewTelemetryMock,\n    )\n\n    render(<Config />)\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: TelemetryPageView.RDI_CONFIG,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n      },\n    })\n  })\n\n  it('should call proper actions', () => {\n    render(<Config />)\n    const fieldName = screen.getByTestId('rdi-monaco-config')\n    fireEvent.change(fieldName, { target: { value: '123' } })\n\n    const expectedActions = [\n      setPipelineConfig('123'),\n      setChangedFile({ name: 'config', status: FileChangeType.Added }),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper actions when value equal to deployed pipeline', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      schema: { config: { test: {} } },\n      data: { config: '123' },\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    render(<Config />)\n\n    const fieldName = screen.getByTestId('rdi-monaco-config')\n    fireEvent.change(fieldName, { target: { value: '123' } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      setPipelineConfig('123'),\n      deleteChangedFile('config'),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper actions when value not equal to deployed pipeline', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      schema: { config: { test: {} } },\n      data: { config: '11' },\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    render(<Config />)\n\n    const fieldName = screen.getByTestId('rdi-monaco-config')\n    fireEvent.change(fieldName, { target: { value: '123' } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      setPipelineConfig('123'),\n      setChangedFile({ name: 'config', status: FileChangeType.Modified }),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should open right panel', async () => {\n    const { queryByTestId } = render(<Config />)\n\n    expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('rdi-test-connection-btn'))\n    })\n\n    expect(queryByTestId('test-connection-panel')).toBeInTheDocument()\n  })\n\n  it('should close right panel', async () => {\n    const { queryByTestId } = render(<Config />)\n\n    expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('rdi-test-connection-btn'))\n    })\n\n    expect(queryByTestId('test-connection-panel')).toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('close-test-connections-btn'))\n    })\n\n    expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()\n  })\n\n  it('should render error notification', async () => {\n    ;(rdiPipelineSelector as jest.Mock).mockImplementationOnce(() => ({\n      config: 'sources:incorrect\\n target:',\n    }))\n\n    const { queryByTestId } = render(<Config />)\n\n    expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('rdi-test-connection-btn'))\n    })\n\n    const expectedActions = [\n      addErrorNotification({\n        response: {\n          data: {\n            message: (\n              <>\n                Config has an invalid structure.\n                <br />\n                end of the stream or a document separator is expected\n              </>\n            ),\n          },\n        },\n      } as IAddInstanceErrorPayload),\n    ]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n    expect(queryByTestId('test-connection-panel')).not.toBeInTheDocument()\n  })\n\n  it('should render loading spinner', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: true,\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    render(<Config />)\n\n    expect(screen.getByTestId('rdi-config-loading')).toBeInTheDocument()\n  })\n\n  it('should not render loader on btn for pipeline loading', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: true,\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    const { getByTestId } = render(<Config />)\n\n    // check is btn has loader\n    const child = getByTestId('rdi-test-connection-btn')\n    const icon = child.querySelector('svg')\n    expect(icon).not.toBeInTheDocument()\n  })\n\n  it('should render loader on btn when testing connection', () => {\n    const rdiTestConnectionsSelectorMock = jest.fn().mockReturnValue({\n      loading: true,\n    })\n    ;(rdiTestConnectionsSelector as jest.Mock).mockImplementation(\n      rdiTestConnectionsSelectorMock,\n    )\n\n    const { getByTestId } = render(<Config />)\n\n    // check is btn has loader\n    const child = getByTestId('rdi-test-connection-btn')\n    expect(child.querySelector('svg')).toBeTruthy()\n  })\n\n  it('should send telemetry event when clicking Test Connection button', async () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      sendEventTelemetryMock,\n    )\n\n    render(<Config />)\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId('rdi-test-connection-btn'))\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.RDI_TEST_CONNECTIONS_CLICKED,\n      eventData: {\n        id: 'rdiInstanceId',\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { get, throttle } from 'lodash'\n\nimport {\n  sendPageViewTelemetry,\n  sendEventTelemetry,\n  TelemetryPageView,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { EXTERNAL_LINKS, UTM_MEDIUMS } from 'uiSrc/constants/links'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport {\n  rdiPipelineSelector,\n  setChangedFile,\n  deleteChangedFile,\n  setPipelineConfig,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { FileChangeType, RdiPipelineTabs } from 'uiSrc/slices/interfaces'\nimport MonacoYaml from 'uiSrc/components/monaco-editor/components/monaco-yaml'\nimport TestConnectionsPanel from 'uiSrc/pages/rdi/pipeline-management/components/test-connections-panel'\nimport TemplatePopover from 'uiSrc/pages/rdi/pipeline-management/components/template-popover'\nimport { rdiErrorMessages } from 'uiSrc/pages/rdi/constants'\nimport {\n  testConnectionsAction,\n  rdiTestConnectionsSelector,\n  testConnectionsController,\n} from 'uiSrc/slices/rdi/testConnections'\nimport { appContextPipelineManagement } from 'uiSrc/slices/app/context'\nimport { createAxiosError, isEqualPipelineFile, yamlToJson } from 'uiSrc/utils'\n\nimport {\n  addErrorNotification,\n  type IAddInstanceErrorPayload,\n} from 'uiSrc/slices/app/notifications'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text, Title } from 'uiSrc/components/base/text'\n\nimport { Loader } from 'uiSrc/components/base/display'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Link } from '@redis-ui/components'\nimport { StyledRdiDatabaseConfigContainer } from 'uiSrc/pages/rdi/pipeline-management/pages/config/styles'\n\nconst Config = () => {\n  const [isPanelOpen, setIsPanelOpen] = useState<boolean>(false)\n  const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false)\n\n  const {\n    loading: pipelineLoading,\n    schema,\n    data,\n    config,\n  } = useSelector(rdiPipelineSelector)\n  const { loading: testingConnections } = useSelector(\n    rdiTestConnectionsSelector,\n  )\n  const { isOpenDialog } = useSelector(appContextPipelineManagement)\n\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.RDI_CONFIG,\n      eventData: {\n        rdiInstanceId,\n      },\n    })\n\n    return () => {\n      testConnectionsController?.abort()\n    }\n  }, [])\n\n  useEffect(() => {\n    if (isOpenDialog || pipelineLoading) return\n\n    if (!config) {\n      setIsPopoverOpen(true)\n    }\n\n    if (config) {\n      setIsPopoverOpen(false)\n    }\n  }, [isOpenDialog, config, pipelineLoading])\n\n  const testConnections = () => {\n    const JSONValue = yamlToJson(config, (msg) => {\n      dispatch(\n        addErrorNotification(\n          createAxiosError({\n            message: rdiErrorMessages.invalidStructure('config', msg),\n          }) as IAddInstanceErrorPayload,\n        ),\n      )\n    })\n    if (!JSONValue) {\n      return\n    }\n    setIsPanelOpen(true)\n    dispatch(testConnectionsAction(rdiInstanceId, JSONValue))\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_TEST_CONNECTIONS_CLICKED,\n      eventData: {\n        id: rdiInstanceId,\n      },\n    })\n  }\n\n  const checkIsFileUpdated = useCallback(\n    throttle((value) => {\n      if (!data) {\n        dispatch(\n          setChangedFile({ name: 'config', status: FileChangeType.Added }),\n        )\n        return\n      }\n\n      if (isEqualPipelineFile(value, data?.config)) {\n        dispatch(deleteChangedFile('config'))\n        return\n      }\n\n      dispatch(\n        setChangedFile({ name: 'config', status: FileChangeType.Modified }),\n      )\n    }, 2000),\n    [data],\n  )\n\n  const handleChange = useCallback(\n    (value: string) => {\n      dispatch(setPipelineConfig(value))\n\n      checkIsFileUpdated(value)\n    },\n    [data],\n  )\n\n  const handleClosePanel = () => {\n    testConnectionsController?.abort()\n    setIsPanelOpen(false)\n  }\n\n  return (\n    <Row>\n      <StyledRdiDatabaseConfigContainer grow>\n        <Col gap=\"m\">\n          <Row grow={false} align=\"center\" justify=\"between\">\n            <Title size=\"S\" color=\"primary\">\n              Target database configuration\n            </Title>\n            <TemplatePopover\n              isPopoverOpen={isPopoverOpen && !isOpenDialog}\n              setIsPopoverOpen={setIsPopoverOpen}\n              value={config}\n              setFieldValue={(template) =>\n                dispatch(setPipelineConfig(template))\n              }\n              loading={pipelineLoading}\n              source={RdiPipelineTabs.Config}\n            />\n          </Row>\n          <FlexItem>\n            <Text>\n              {'Configure target instance '}\n              <Link\n                data-testid=\"rdi-pipeline-config-link\"\n                target=\"_blank\"\n                href={getUtmExternalLink(EXTERNAL_LINKS.rdiPipeline, {\n                  medium: UTM_MEDIUMS.Rdi,\n                  campaign: 'config_file',\n                })}\n                variant=\"inline\"\n              >\n                connection details\n              </Link>\n              {' and applier settings.'}\n            </Text>\n          </FlexItem>\n          <FlexItem grow>\n            {pipelineLoading ? (\n              <Col grow data-testid=\"rdi-config-loading\">\n                <Loader color=\"secondary\" size=\"l\" loaderText=\"Loading...\" />\n              </Col>\n            ) : (\n              <MonacoYaml\n                schema={get(schema, 'config', null)}\n                value={config}\n                onChange={handleChange}\n                disabled={pipelineLoading}\n                data-testid=\"rdi-monaco-config\"\n                fullHeight\n              />\n            )}\n          </FlexItem>\n          <Row grow={false} justify=\"end\">\n            <PrimaryButton\n              onClick={testConnections}\n              loading={testingConnections}\n              disabled={pipelineLoading}\n              aria-labelledby=\"test target connections\"\n              data-testid=\"rdi-test-connection-btn\"\n            >\n              Test Connection\n            </PrimaryButton>\n          </Row>\n        </Col>\n      </StyledRdiDatabaseConfigContainer>\n      {isPanelOpen && (\n        <FlexItem>\n          <TestConnectionsPanel onClose={handleClosePanel} />\n        </FlexItem>\n      )}\n    </Row>\n  )\n}\n\nexport default React.memo(Config)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/index.ts",
    "content": "import Config from './Config'\n\nexport default Config\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledRdiDatabaseConfigContainer = styled(FlexItem)`\n  padding: 2.4rem;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/Job.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  getPipelineStrategies,\n  rdiPipelineSelector,\n  setChangedFile,\n  deleteChangedFile,\n  updatePipelineJob,\n} from 'uiSrc/slices/rdi/pipeline'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport Job, { Props } from './Job'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    monacoJobsSchema: { jobs: { test: {} } },\n    config: `connections:\n            target:\n              type: redis\n          `,\n    jobs: [\n      {\n        name: 'jobName',\n        value: `job:\n      transform:\n        type: sql\n    `,\n      },\n      {\n        name: 'job2',\n        value: `job2:\n      transform:\n        type: redis\n    `,\n      },\n    ],\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('Job', () => {\n  it('should render', () => {\n    expect(render(<Job {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should not push to config page', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      monacoJobsSchema: { jobs: { test: {} } },\n      error: '',\n      config: `connections:\n      target:\n        type: redis\n    `,\n      jobs: [\n        {\n          name: 'jobName',\n          value: `job:\n                    transform:\n                      type: sql\n                `,\n        },\n        {\n          name: 'job2',\n          value: `job2:\n                    transform:\n                      type: redis\n                    `,\n        },\n      ],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n\n    render(<Job {...instance(mockedProps)} />)\n\n    expect(pushMock).not.toBeCalled()\n  })\n\n  it('should render Panel and disable dry run btn', () => {\n    const { queryByTestId } = render(<Job {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('rdi-job-dry-run')).not.toBeDisabled()\n    expect(queryByTestId('dry-run-panel')).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('rdi-job-dry-run'))\n\n    expect(screen.getByTestId('rdi-job-dry-run')).toBeDisabled()\n    expect(queryByTestId('dry-run-panel')).toBeInTheDocument()\n  })\n\n  it('should not call any updated file action if there is no deployed job', () => {\n    const mockJob = {\n      name: 'jobName',\n      value: '123',\n    }\n\n    render(<Job {...instance(mockedProps)} name={mockJob.name} />)\n\n    const fieldName = screen.getByTestId('rdi-monaco-job')\n    fireEvent.change(fieldName, { target: { value: mockJob.value } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      updatePipelineJob(mockJob),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should set modified file', () => {\n    const mockJob = {\n      name: 'jobName',\n      value: '123',\n    }\n\n    render(\n      <Job\n        {...instance(mockedProps)}\n        deployedJobValue={mockJob.value}\n        name={mockJob.name}\n      />,\n    )\n\n    const fieldName = screen.getByTestId('rdi-monaco-job')\n    fireEvent.change(fieldName, { target: { value: 'updated' } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      updatePipelineJob({ name: mockJob.name, value: 'updated' }),\n      setChangedFile({ name: mockJob.name, status: FileChangeType.Modified }),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should remove job from modified files', () => {\n    const mockJob = {\n      name: 'jobName',\n      value: '123',\n    }\n\n    render(\n      <Job\n        {...instance(mockedProps)}\n        deployedJobValue={mockJob.value}\n        name={mockJob.name}\n      />,\n    )\n\n    const fieldName = screen.getByTestId('rdi-monaco-job')\n    fireEvent.change(fieldName, { target: { value: mockJob.value } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      updatePipelineJob(mockJob),\n      deleteChangedFile(mockJob.name),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should open dedicated editor', () => {\n    render(\n      <Job\n        {...instance(mockedProps)}\n        deployedJobValue=\"value\"\n        name=\"jobName\"\n      />,\n    )\n\n    expect(screen.queryByTestId('draggable-area')).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('open-dedicated-editor-btn'))\n\n    expect(screen.getByTestId('draggable-area')).toBeInTheDocument()\n  })\n\n  it('should call proper telemetry events on open dedicated editor', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<Job {...instance(mockedProps)} value=\"value\" rdiInstanceId=\"id\" />)\n\n    fireEvent.click(screen.getByTestId('open-dedicated-editor-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_OPENED,\n      eventData: {\n        rdiInstanceId: 'id',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper telemetry events on cancel dedicated editor', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<Job {...instance(mockedProps)} value=\"value\" rdiInstanceId=\"id\" />)\n\n    fireEvent.click(screen.getByTestId('open-dedicated-editor-btn'))\n    fireEvent.click(screen.getByTestId('cancel-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_CANCELLED,\n      eventData: {\n        rdiInstanceId: 'id',\n        selectedLanguageSyntax: 'sqliteFunctions',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper telemetry events on submit dedicated editor', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<Job {...instance(mockedProps)} value=\"value\" rdiInstanceId=\"id\" />)\n\n    fireEvent.click(screen.getByTestId('open-dedicated-editor-btn'))\n    await act(() => {\n      fireEvent.click(screen.getByTestId('apply-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_SAVED,\n      eventData: {\n        rdiInstanceId: 'id',\n        selectedLanguageSyntax: 'sqliteFunctions',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should render loading spinner', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: true,\n      monacoJobsSchema: { jobs: { test: {} } },\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    render(<Job {...instance(mockedProps)} />)\n\n    expect(screen.getByTestId('rdi-job-loading')).toBeInTheDocument()\n  })\n\n  describe('monacoJobsSchema integration', () => {\n    it('should pass monacoJobsSchema to MonacoYaml when available', () => {\n      const mockMonacoJobsSchema = {\n        type: 'object',\n        properties: {\n          source: { type: 'object' },\n          transform: { type: 'object' },\n          output: { type: 'object' },\n        },\n      }\n\n      const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n        loading: false,\n        schema: { jobs: { test: {} } },\n        monacoJobsSchema: mockMonacoJobsSchema,\n        config: 'test-config',\n        jobs: [\n          {\n            name: 'testJob',\n            value: 'test-value',\n          },\n        ],\n      })\n      ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n        rdiPipelineSelectorMock,\n      )\n\n      render(\n        <Job {...instance(mockedProps)} name=\"testJob\" value=\"test-value\" />,\n      )\n\n      // Verify the component renders and doesn't crash with schema\n      expect(screen.getByTestId('rdi-monaco-job')).toBeInTheDocument()\n      expect(screen.queryByTestId('rdi-job-loading')).not.toBeInTheDocument()\n    })\n\n    it('should handle empty monacoJobsSchema gracefully', () => {\n      const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n        loading: false,\n        monacoJobsSchema: {},\n        config: 'test-config',\n        jobs: [\n          {\n            name: 'testJob',\n            value: 'test-value',\n          },\n        ],\n      })\n      ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n        rdiPipelineSelectorMock,\n      )\n\n      render(\n        <Job {...instance(mockedProps)} name=\"testJob\" value=\"test-value\" />,\n      )\n\n      // Verify the component renders without issues when schema is empty\n      expect(screen.getByTestId('rdi-monaco-job')).toBeInTheDocument()\n      expect(screen.queryByTestId('rdi-job-loading')).not.toBeInTheDocument()\n    })\n\n    it('should handle undefined monacoJobsSchema gracefully', () => {\n      const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n        loading: false,\n        monacoJobsSchema: undefined,\n        config: 'test-config',\n        jobs: [\n          {\n            name: 'testJob',\n            value: 'test-value',\n          },\n        ],\n      })\n      ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n        rdiPipelineSelectorMock,\n      )\n\n      render(\n        <Job {...instance(mockedProps)} name=\"testJob\" value=\"test-value\" />,\n      )\n\n      // Verify the component renders without issues when schema is undefined\n      expect(screen.getByTestId('rdi-monaco-job')).toBeInTheDocument()\n      expect(screen.queryByTestId('rdi-job-loading')).not.toBeInTheDocument()\n    })\n\n    it('should pass complex monacoJobsSchema structure to MonacoYaml', () => {\n      const complexSchema = {\n        type: 'object',\n        properties: {\n          source: {\n            type: 'object',\n            properties: {\n              server_name: { type: 'string' },\n              schema: { type: 'string' },\n              table: { type: 'string' },\n            },\n            required: ['server_name', 'schema', 'table'],\n          },\n          transform: {\n            type: 'array',\n            items: {\n              type: 'object',\n              properties: {\n                uses: { type: 'string' },\n                with: { type: 'object' },\n              },\n            },\n          },\n          output: {\n            type: 'array',\n            items: {\n              type: 'object',\n              properties: {\n                uses: { type: 'string' },\n                with: { type: 'object' },\n              },\n            },\n          },\n        },\n        required: ['source'],\n      }\n\n      const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n        loading: false,\n        monacoJobsSchema: complexSchema,\n        config: 'test-config',\n        jobs: [\n          {\n            name: 'complexJob',\n            value: 'source:\\n  server_name: test',\n          },\n        ],\n      })\n      ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n        rdiPipelineSelectorMock,\n      )\n\n      render(\n        <Job\n          {...instance(mockedProps)}\n          name=\"complexJob\"\n          value=\"source:\\n  server_name: test\"\n        />,\n      )\n\n      // Verify the component renders with complex schema structure\n      expect(screen.getByTestId('rdi-monaco-job')).toBeInTheDocument()\n      expect(screen.queryByTestId('rdi-job-loading')).not.toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/Job.tsx",
    "content": "import React, { useState, useEffect, useRef, useCallback } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { throttle } from 'lodash'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { EXTERNAL_LINKS, UTM_MEDIUMS } from 'uiSrc/constants/links'\nimport {\n  deleteChangedFile,\n  fetchPipelineStrategies,\n  rdiPipelineSelector,\n  setChangedFile,\n  setPipelineJobs,\n  updatePipelineJob,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport MonacoYaml from 'uiSrc/components/monaco-editor/components/monaco-yaml'\nimport DryRunJobPanel from 'uiSrc/pages/rdi/pipeline-management/components/jobs-panel'\nimport { rdiErrorMessages } from 'uiSrc/pages/rdi/constants'\nimport { DSL, KEYBOARD_SHORTCUTS } from 'uiSrc/constants'\nimport {\n  createAxiosError,\n  isEqualPipelineFile,\n  Maybe,\n  yamlToJson,\n} from 'uiSrc/utils'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { KeyboardShortcut, RiTooltip } from 'uiSrc/components'\n\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Loader } from 'uiSrc/components/base/display'\nimport TemplateButton from '../../components/template-button'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Link, TextButton } from '@redis-ui/components'\nimport { StyledRdiJobConfigContainer } from 'uiSrc/pages/rdi/pipeline-management/pages/job/styles'\n\nexport interface Props {\n  name: string\n  value: string\n  deployedJobValue: Maybe<string>\n  jobIndex: number\n  rdiInstanceId: string\n}\n\nconst Job = (props: Props) => {\n  const { name, value = '', deployedJobValue, jobIndex, rdiInstanceId } = props\n\n  const [isPanelOpen, setIsPanelOpen] = useState<boolean>(false)\n  const [shouldOpenDedicatedEditor, setShouldOpenDedicatedEditor] =\n    useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  const jobIndexRef = useRef<number>(jobIndex)\n  const deployedJobValueRef = useRef<Maybe<string>>(deployedJobValue)\n  const jobNameRef = useRef<string>(name)\n\n  const { loading, monacoJobsSchema, jobFunctions, jobs } =\n    useSelector(rdiPipelineSelector)\n\n  useEffect(() => {\n    dispatch(fetchPipelineStrategies(rdiInstanceId))\n  }, [])\n\n  useEffect(() => {\n    setIsPanelOpen(false)\n  }, [name])\n\n  useEffect(() => {\n    deployedJobValueRef.current = deployedJobValue\n  }, [deployedJobValue])\n\n  useEffect(() => {\n    jobIndexRef.current = jobIndex\n  }, [jobIndex])\n\n  useEffect(() => {\n    jobNameRef.current = name\n  }, [name])\n\n  const handleDryRunJob = () => {\n    const JSONValue = yamlToJson(value, (msg) => {\n      dispatch(\n        addErrorNotification(\n          createAxiosError({\n            message: rdiErrorMessages.invalidStructure(name, msg),\n          }),\n        ),\n      )\n    })\n    if (!JSONValue) {\n      return\n    }\n    setIsPanelOpen(true)\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_TEST_JOB_OPENED,\n      eventData: {\n        id: rdiInstanceId,\n      },\n    })\n  }\n\n  const checkIsFileUpdated = useCallback(\n    throttle((value) => {\n      if (!deployedJobValueRef.current) {\n        return\n      }\n\n      if (isEqualPipelineFile(value, deployedJobValueRef.current)) {\n        dispatch(deleteChangedFile(jobNameRef.current))\n        return\n      }\n      dispatch(\n        setChangedFile({\n          name: jobNameRef.current,\n          status: FileChangeType.Modified,\n        }),\n      )\n    }, 2000),\n    [deployedJobValue, jobNameRef.current],\n  )\n\n  const handleChange = (value: string) => {\n    dispatch(updatePipelineJob({ name: jobNameRef.current, value }))\n    checkIsFileUpdated(value)\n  }\n\n  const handleChangeLanguage = (langId: DSL) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_LANGUAGE_CHANGED,\n      eventData: {\n        rdiInstanceId,\n        selectedLanguageSyntax: langId,\n      },\n    })\n  }\n\n  const handleOpenDedicatedEditor = () => {\n    setShouldOpenDedicatedEditor(false)\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_OPENED,\n      eventData: {\n        rdiInstanceId,\n      },\n    })\n  }\n\n  const handleCloseDedicatedEditor = (langId: DSL) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_CANCELLED,\n      eventData: {\n        rdiInstanceId,\n        selectedLanguageSyntax: langId,\n      },\n    })\n  }\n\n  const handleSubmitDedicatedEditor = (langId: DSL) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_DEDICATED_EDITOR_SAVED,\n      eventData: {\n        rdiInstanceId,\n        selectedLanguageSyntax: langId,\n      },\n    })\n  }\n\n  return (\n    <Row>\n      <StyledRdiJobConfigContainer grow>\n        <Col gap=\"m\">\n          <Row grow={false} align=\"center\" justify=\"between\">\n            <Title size=\"S\" color=\"primary\">\n              {name}\n            </Title>\n            <FlexItem>\n              <Row gap=\"l\">\n                <RiTooltip\n                  position=\"top\"\n                  content={\n                    KEYBOARD_SHORTCUTS?.rdi?.openDedicatedEditor && (\n                      <div>\n                        <Text size=\"s\">{`${KEYBOARD_SHORTCUTS.rdi.openDedicatedEditor?.description}\\u00A0\\u00A0`}</Text>\n                        <KeyboardShortcut\n                          separator={KEYBOARD_SHORTCUTS?._separator}\n                          items={\n                            KEYBOARD_SHORTCUTS.rdi.openDedicatedEditor.keys\n                          }\n                        />\n                      </div>\n                    )\n                  }\n                  data-testid=\"open-dedicated-editor-tooltip\"\n                >\n                  <TextButton\n                    onClick={() => setShouldOpenDedicatedEditor(true)}\n                    data-testid=\"open-dedicated-editor-btn\"\n                    variant=\"primary-inline\"\n                  >\n                    SQL and JMESPath Editor\n                  </TextButton>\n                </RiTooltip>\n                <TemplateButton\n                  value={value}\n                  setFieldValue={(template) => {\n                    const newJobs = jobs.map((job, index) => {\n                      if (index === jobIndexRef.current) {\n                        return {\n                          ...job,\n                          value: template,\n                        }\n                      }\n                      return job\n                    })\n                    dispatch(setPipelineJobs(newJobs))\n                  }}\n                />\n              </Row>\n            </FlexItem>\n          </Row>\n          <Text color=\"primary\">\n            {'Create a job per source table to filter, transform, and '}\n            <Link\n              data-testid=\"rdi-pipeline-transformation-link\"\n              target=\"_blank\"\n              href={getUtmExternalLink(EXTERNAL_LINKS.rdiPipelineTransforms, {\n                medium: UTM_MEDIUMS.Rdi,\n                campaign: 'job_file',\n              })}\n              variant=\"inline\"\n            >\n              map data\n            </Link>\n            {' to Redis.'}\n          </Text>\n          {loading ? (\n            <div data-testid=\"rdi-job-loading\">\n              <Loader color=\"secondary\" size=\"l\" loaderText=\"Loading...\" />\n            </div>\n          ) : (\n            <MonacoYaml\n              schema={monacoJobsSchema}\n              value={value}\n              onChange={handleChange}\n              disabled={loading}\n              dedicatedEditorLanguages={[DSL.sqliteFunctions, DSL.jmespath]}\n              dedicatedEditorFunctions={\n                jobFunctions as monacoEditor.languages.CompletionItem[]\n              }\n              dedicatedEditorOptions={{\n                suggest: {\n                  preview: false,\n                  showIcons: true,\n                  showStatusBar: true,\n                },\n              }}\n              onChangeLanguage={handleChangeLanguage}\n              shouldOpenDedicatedEditor={shouldOpenDedicatedEditor}\n              onOpenDedicatedEditor={handleOpenDedicatedEditor}\n              onCloseDedicatedEditor={handleCloseDedicatedEditor}\n              onSubmitDedicatedEditor={handleSubmitDedicatedEditor}\n              data-testid=\"rdi-monaco-job\"\n              fullHeight\n            />\n          )}\n          <Row grow={false} justify=\"end\">\n            <PrimaryButton\n              color=\"secondary\"\n              onClick={handleDryRunJob}\n              disabled={isPanelOpen}\n              data-testid=\"rdi-job-dry-run\"\n            >\n              Dry Run\n            </PrimaryButton>\n          </Row>\n        </Col>\n      </StyledRdiJobConfigContainer>\n      {isPanelOpen && (\n        <FlexItem>\n          <DryRunJobPanel\n            onClose={() => setIsPanelOpen(false)}\n            job={value}\n            name={name}\n          />\n        </FlexItem>\n      )}\n    </Row>\n  )\n}\n\nexport default Job\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/JobWrapper.tsx",
    "content": "import React, { useState, useEffect } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\nimport { findIndex } from 'lodash'\n\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport { rdiPipelineSelector } from 'uiSrc/slices/rdi/pipeline'\nimport { Pages } from 'uiSrc/constants'\nimport { Maybe } from 'uiSrc/utils'\nimport Job from './Job'\n\nconst JobWrapper = () => {\n  const { rdiInstanceId, jobName } = useParams<{\n    rdiInstanceId: string\n    jobName: string\n  }>()\n\n  const [decodedJobName, setDecodedJobName] = useState<string>(\n    decodeURIComponent(jobName),\n  )\n  const [jobIndex, setJobIndex] = useState<number>(-1)\n  const [deployedJobValue, setDeployedJobValue] = useState<Maybe<string>>()\n\n  const history = useHistory()\n\n  const { data, jobs } = useSelector(rdiPipelineSelector)\n\n  useEffect(() => {\n    const jobIndex = findIndex(jobs, ({ name }) => name === decodedJobName)\n    setJobIndex(jobIndex)\n\n    if (jobIndex === -1) {\n      history.push(Pages.rdiPipelineConfig(rdiInstanceId))\n    }\n  }, [decodedJobName, rdiInstanceId, jobs?.length])\n\n  useEffect(() => {\n    setDecodedJobName(decodeURIComponent(jobName))\n  }, [jobName])\n\n  useEffect(() => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.RDI_JOBS,\n      eventData: {\n        rdiInstanceId,\n      },\n    })\n  }, [])\n\n  useEffect(() => {\n    const newDeployedJob = data?.jobs.find((el) => el.name === decodedJobName)\n\n    setDeployedJobValue(newDeployedJob ? newDeployedJob.value : undefined)\n  }, [data, decodedJobName])\n\n  return (\n    <Job\n      name={decodedJobName}\n      value={jobs[jobIndex]?.value ?? ''}\n      deployedJobValue={deployedJobValue}\n      jobIndex={jobIndex}\n      rdiInstanceId={rdiInstanceId}\n    />\n  )\n}\n\nexport default React.memo(JobWrapper)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/JobsWrapper.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\n\nimport {\n  deleteChangedFile,\n  getPipelineStrategies,\n  rdiPipelineSelector,\n  setChangedFile,\n  updatePipelineJob,\n} from 'uiSrc/slices/rdi/pipeline'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport {\n  sendPageViewTelemetry,\n  TelemetryPageView,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  MOCK_RDI_PIPELINE_CONFIG,\n  MOCK_RDI_PIPELINE_JOB2,\n} from 'uiSrc/mocks/data/rdi'\nimport { FileChangeType } from 'uiSrc/slices/interfaces'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport JobWrapper from './JobWrapper'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineSelector: jest.fn().mockReturnValue({\n    loading: false,\n    schema: { jobs: { test: {} } },\n    config: `connections:\n        target:\n          type: redis\n      `,\n    jobs: [\n      {\n        name: 'jobName',\n        value: `job:\n    transform:\n    type: sql\n    `,\n      },\n      {\n        name: 'job2',\n        value: `job2:\n    transform:\n    type: redis\n    `,\n      },\n    ],\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('JobWrapper', () => {\n  it('should render', () => {\n    expect(render(<JobWrapper />)).toBeTruthy()\n  })\n\n  it('should call proper sendPageViewTelemetry', () => {\n    const sendPageViewTelemetryMock = jest.fn()\n    ;(sendPageViewTelemetry as jest.Mock).mockImplementation(\n      () => sendPageViewTelemetryMock,\n    )\n\n    render(<JobWrapper />)\n\n    expect(sendPageViewTelemetry).toBeCalledWith({\n      name: TelemetryPageView.RDI_JOBS,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n      },\n    })\n  })\n\n  it('should push to config page', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      config: MOCK_RDI_PIPELINE_CONFIG,\n      jobs: [MOCK_RDI_PIPELINE_JOB2],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementationOnce(\n      rdiPipelineSelectorMock,\n    )\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n\n    render(<JobWrapper />)\n\n    expect(pushMock).toBeCalledWith(\n      '/integrate/rdiInstanceId/pipeline-management/config',\n    )\n  })\n\n  it('should not push to config page', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      error: '',\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementationOnce(\n      rdiPipelineSelectorMock,\n    )\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest\n      .fn()\n      .mockReturnValueOnce({ push: pushMock })\n\n    render(<JobWrapper />)\n\n    expect(pushMock).not.toBeCalled()\n  })\n\n  it('should render proper link', () => {\n    render(<JobWrapper />)\n\n    expect(\n      screen.getByTestId('rdi-pipeline-transformation-link'),\n    ).toHaveAttribute(\n      'href',\n      'https://redis.io/docs/latest/integrate/redis-data-integration/ingest/data-pipelines/transform-examples/?utm_source=redisinsight&utm_medium=rdi&utm_campaign=job_file',\n    )\n  })\n\n  it('should send telemetry event with proper data', () => {\n    render(<JobWrapper />)\n\n    fireEvent.click(screen.getByTestId('rdi-job-dry-run'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_TEST_JOB_OPENED,\n      eventData: {\n        id: 'rdiInstanceId',\n      },\n    })\n  })\n\n  it('should render Panel and disable dry run btn', () => {\n    const { queryByTestId } = render(<JobWrapper />)\n\n    expect(screen.getByTestId('rdi-job-dry-run')).not.toBeDisabled()\n    expect(queryByTestId('dry-run-panel')).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('rdi-job-dry-run'))\n\n    expect(screen.getByTestId('rdi-job-dry-run')).toBeDisabled()\n    expect(queryByTestId('dry-run-panel')).toBeInTheDocument()\n  })\n\n  it('should call proper actions when change monaco editor', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      schema: { jobs: { test: {} } },\n      data: { jobs: [{ name: 'jobName', value: 'value' }] },\n      jobs: [{ name: 'jobName', value: 'value' }],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementationOnce(\n      rdiPipelineSelectorMock,\n    )\n\n    render(<JobWrapper />)\n\n    const fieldName = screen.getByTestId('rdi-monaco-job')\n    fireEvent.change(fieldName, { target: { value: '123' } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      updatePipelineJob({ name: 'jobName', value: '123' }),\n      setChangedFile({ name: 'jobName', status: FileChangeType.Modified }),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should call proper actions when value is equal with deployed job', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      schema: { jobs: { test: {} } },\n      data: { jobs: [{ name: 'jobName', value: '123' }] },\n      jobs: [{ name: 'jobName' }],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    render(<JobWrapper />)\n\n    const fieldName = screen.getByTestId('rdi-monaco-job')\n    fireEvent.change(fieldName, { target: { value: '123' } })\n\n    const expectedActions = [\n      getPipelineStrategies(),\n      updatePipelineJob({ name: 'jobName', value: '123' }),\n      deleteChangedFile('jobName'),\n    ]\n\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('should render error notification', () => {\n    const rdiPipelineSelectorMock = jest.fn().mockReturnValue({\n      loading: false,\n      schema: { jobs: { test: {} } },\n      data: {\n        jobs: [{ name: 'jobName', value: 'sources:incorrect\\n target:' }],\n      },\n      config: MOCK_RDI_PIPELINE_CONFIG,\n      jobs: [{ name: 'jobName', value: 'sources:incorrect\\n target:' }],\n    })\n    ;(rdiPipelineSelector as jest.Mock).mockImplementation(\n      rdiPipelineSelectorMock,\n    )\n\n    const { queryByTestId } = render(<JobWrapper />)\n\n    fireEvent.click(screen.getByTestId('rdi-job-dry-run'))\n\n    const expectedActions = [\n      addErrorNotification({\n        response: {\n          data: {\n            message: (\n              <>\n                JobName has an invalid structure.\n                <br />\n                end of the stream or a document separator is expected\n              </>\n            ),\n          },\n        },\n      } as AxiosError),\n    ]\n\n    expect(store.getActions().slice(0 - expectedActions.length)).toEqual(\n      expectedActions,\n    )\n\n    expect(queryByTestId('dry-run-panel')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/index.ts",
    "content": "import JobWrapper from './JobWrapper'\n\nexport default JobWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/pipeline-management/pages/job/styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const StyledRdiJobConfigContainer = styled(FlexItem)`\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space300};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/StatisticsPage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport reactRouterDom from 'react-router-dom'\n\nimport { getPipelineStatus } from 'uiSrc/slices/rdi/pipeline'\nimport {\n  getStatistics,\n  rdiStatisticsSelector,\n} from 'uiSrc/slices/rdi/statistics'\nimport {\n  TelemetryEvent,\n  TelemetryPageView,\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n} from 'uiSrc/telemetry'\nimport {\n  cleanup,\n  userEvent,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\nimport { PageNames, Pages } from 'uiSrc/constants'\nimport { setLastPageContext } from 'uiSrc/slices/app/context'\nimport { RdiPipelineStatus } from 'uiSrc/slices/interfaces'\n\nimport StatisticsPage from './StatisticsPage'\n\nconst CONNECTIONS_DATA = {\n  connections: {\n    Connection1: {\n      status: 'good',\n      type: 'type1',\n      host: 'Redis-Cloud',\n      port: 12000,\n      database: 'admin',\n      user: 'admin',\n    },\n  },\n  dataStreams: {\n    Stream1: {\n      total: 35,\n      pending: 2,\n      inserted: 2530,\n      updated: 65165,\n      deleted: 1,\n      filtered: 0,\n      rejected: 5,\n      deduplicated: 0,\n      lastArrival: '1 Hour',\n    },\n  },\n  processingPerformance: {\n    totalBatches: 3427,\n    batchSizeAvg: '0.93',\n    readTimeAvg: '13',\n    processTimeAvg: '24',\n    ackTimeAvg: '6.2',\n    totalTimeAvg: '6.1',\n    recPerSecAvg: 110,\n  },\n  rdiPipelineStatus: {\n    rdiVersion: '2.0',\n    address: '172.17.0.2:12006',\n    runStatus: 'Started',\n    syncMode: 'Streaming',\n  },\n  clients: {\n    9875: {\n      addr: '172.16.0.2:62356',\n      name: 'redis-di-cli',\n      ageSec: 100,\n      idleSec: 2,\n      user: 'default',\n    },\n  },\n}\n\njest.mock('uiSrc/slices/rdi/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    name: 'name',\n  }),\n}))\n\njest.mock('uiSrc/slices/rdi/pipeline', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/pipeline'),\n  rdiPipelineStatusSelector: jest.fn().mockReturnValue({\n    loading: false,\n    data: {\n      components: { processor: 'ready' },\n      pipelines: {\n        default: {\n          status: 'ready',\n          state: 'some',\n          tasks: 'none',\n        },\n      },\n    },\n  }),\n}))\n\njest.mock('uiSrc/slices/rdi/statistics', () => ({\n  ...jest.requireActual('uiSrc/slices/rdi/statistics'),\n  rdiStatisticsSelector: jest.fn().mockReturnValue({\n    loading: false,\n    results: {\n      status: 'success',\n      data: CONNECTIONS_DATA,\n    },\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n  sendEventTelemetry: jest.fn(),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n  ;(sendEventTelemetry as jest.Mock).mockRestore()\n})\n\ndescribe('StatisticsPage', () => {\n  it('should render', () => {\n    expect(render(<StatisticsPage />)).toBeTruthy()\n  })\n\n  it('renders the page with correct title', () => {\n    render(<StatisticsPage />)\n    expect(document.title).toBe('name - Pipeline Status')\n  })\n\n  it('renders null when statisticsResults is not available', () => {\n    ;(rdiStatisticsSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      results: null,\n    })\n    const { container } = render(<StatisticsPage />)\n    expect(container.firstChild).toBeNull()\n  })\n\n  it('renders the empty state when statistics status is not success', () => {\n    ;(rdiStatisticsSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      results: {\n        status: null,\n        data: CONNECTIONS_DATA,\n      },\n    })\n    render(<StatisticsPage />)\n    expect(screen.getByTestId('empty-pipeline')).toBeInTheDocument()\n  })\n\n  it('renders the empty state when statistics status is success but data is missing', () => {\n    ;(rdiStatisticsSelector as jest.Mock).mockReturnValueOnce({\n      loading: false,\n      results: {\n        status: RdiPipelineStatus.Success,\n        data: null,\n      },\n    })\n    render(<StatisticsPage />)\n    expect(screen.getByTestId('empty-pipeline')).toBeInTheDocument()\n  })\n\n  it('renders statistics sections when status is success and data exists', async () => {\n    render(<StatisticsPage />)\n\n    // Check that statistics sections are rendered instead of empty state\n    expect(screen.queryByTestId('empty-pipeline')).not.toBeInTheDocument()\n    expect(\n      await screen.findByTestId('processing-performance-info-refresh-btn'),\n    ).toBeInTheDocument()\n  })\n\n  it('should call proper telemetry on page view', () => {\n    render(<StatisticsPage />)\n\n    expect(sendPageViewTelemetry).toBeCalledWith({\n      name: TelemetryPageView.RDI_STATUS,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n      },\n    })\n  })\n\n  it('should call proper telemetry event when refresh is clicked for processing performance section', async () => {\n    render(<StatisticsPage />)\n\n    fireEvent.click(\n      await screen.findByTestId('processing-performance-info-refresh-btn'),\n    )\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_REFRESH_CLICKED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'processing_performance',\n      },\n    })\n  })\n\n  xit('should call proper telemetry event when refresh is clicked for data streams section', () => {\n    render(<StatisticsPage />)\n\n    fireEvent.click(screen.getByTestId('data-streams-refresh-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_REFRESH_CLICKED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'data_streams',\n      },\n    })\n  })\n\n  xit('should call proper telemetry event when refresh is clicked for clients section', () => {\n    render(<StatisticsPage />)\n\n    fireEvent.click(screen.getByTestId('clients-refresh-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_REFRESH_CLICKED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'clients',\n      },\n    })\n  })\n\n  it('should call proper telemetry event when auto refresh is disabled for processing performance section', async () => {\n    render(<StatisticsPage />)\n\n    const testid = 'processing-performance-info'\n\n    await userEvent.click(\n      await screen.findByTestId(`${testid}-auto-refresh-config-btn`),\n    )\n    await waitForRiPopoverVisible()\n    await userEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // disabled\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_DISABLED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'processing_performance',\n        enableAutoRefresh: false,\n        refreshRate: '5.0',\n      },\n    })\n  })\n\n  it('should call proper telemetry event when auto refresh is enabled for processing performance section', async () => {\n    render(<StatisticsPage />)\n\n    const testid = 'processing-performance-info'\n\n    await userEvent.click(\n      await screen.findByTestId(`${testid}-auto-refresh-config-btn`),\n    )\n    await waitForRiPopoverVisible()\n    await userEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // disabled\n    await userEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // enabled\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_ENABLED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'processing_performance',\n        enableAutoRefresh: true,\n        refreshRate: '5.0',\n      },\n    })\n  })\n\n  xit('should call proper telemetry event when auto refresh is enabled for data streams section', async () => {\n    render(<StatisticsPage />)\n\n    const testid = 'data-streams'\n\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-config-btn`))\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // enabled\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_ENABLED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'data_streams',\n        enableAutoRefresh: true,\n        refreshRate: '5.0',\n      },\n    })\n  })\n\n  xit('should call proper telemetry event when auto refresh is disabled for data streams section', async () => {\n    render(<StatisticsPage />)\n\n    const testid = 'data-streams'\n\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-config-btn`))\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // enabled\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // disabled\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_DISABLED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'data_streams',\n        enableAutoRefresh: false,\n        refreshRate: '5.0',\n      },\n    })\n  })\n\n  xit('should call proper telemetry event when auto refresh is enabled for clients section', async () => {\n    render(<StatisticsPage />)\n\n    const testid = 'clients'\n\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-config-btn`))\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // enabled\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_ENABLED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'clients',\n        enableAutoRefresh: true,\n        refreshRate: '5.0',\n      },\n    })\n  })\n\n  xit('should call proper telemetry event when auto refresh is disabled for clients section', async () => {\n    render(<StatisticsPage />)\n\n    const testid = 'clients'\n\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-config-btn`))\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // enabled\n    fireEvent.click(screen.getByTestId(`${testid}-auto-refresh-switch`)) // disabled\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_DISABLED,\n      eventData: {\n        rdiInstanceId: 'rdiInstanceId',\n        section: 'clients',\n        enableAutoRefresh: false,\n        refreshRate: '5.0',\n      },\n    })\n  })\n\n  it('should get statistics on mount', () => {\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.rdiPipelineConfig('rdiInstanceId') })\n\n    render(<StatisticsPage />)\n\n    const expectedActions = [getPipelineStatus(), getStatistics()]\n\n    expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n\n  it('should save proper page on unmount', () => {\n    const { unmount } = render(<StatisticsPage />)\n\n    unmount()\n    const expectedActions = [setLastPageContext(PageNames.rdiStatistics)]\n\n    expect(store.getActions().slice(0 - expectedActions.length)).toEqual(\n      expectedActions,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/StatisticsPage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Col)`\n  overflow: auto;\n  position: relative;\n`\nexport const ContentWrapper = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space200};\n`\n\nexport const LoadingState = styled(Col)`\n  z-index: 2;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/StatisticsPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { connectedInstanceSelector } from 'uiSrc/slices/rdi/instances'\nimport { getPipelineStatusAction } from 'uiSrc/slices/rdi/pipeline'\nimport {\n  fetchRdiStatistics,\n  rdiStatisticsSelector,\n} from 'uiSrc/slices/rdi/statistics'\nimport {\n  TelemetryEvent,\n  TelemetryPageView,\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n} from 'uiSrc/telemetry'\nimport { formatLongName, Nullable, setTitle } from 'uiSrc/utils'\nimport { setLastPageContext } from 'uiSrc/slices/app/context'\nimport { PageNames } from 'uiSrc/constants'\nimport { Loader } from 'uiSrc/components/base/display'\nimport {\n  type IRdiStatistics,\n  type IStatisticsSection,\n  RdiPipelineStatus,\n  RdiStatisticsViewType,\n} from 'uiSrc/slices/interfaces'\n\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { AutoRefresh } from 'uiSrc/components'\nimport Empty from './empty'\nimport StatisticsTable from './components/statistics-table'\nimport StatisticsBlocks from './components/statistics-blocks'\nimport StatisticsInfo from './components/statistics-info'\nimport * as S from './StatisticsPage.styles'\n\nconst shouldShowStatistics = (data: Nullable<IRdiStatistics>) =>\n  data?.status === RdiPipelineStatus.Success && !!data?.data\n\nconst renderStatisticsSection = (section: IStatisticsSection) => {\n  switch (section.view) {\n    case RdiStatisticsViewType.Info:\n      return <StatisticsInfo key={section.name} data={section} />\n    case RdiStatisticsViewType.Table:\n      return <StatisticsTable key={section.name} data={section} />\n    case RdiStatisticsViewType.Blocks:\n      return <StatisticsBlocks key={section.name} data={section} />\n    default:\n      return null\n  }\n}\n\nconst StatisticsPage = () => {\n  const [pageLoading, setPageLoading] = useState(true)\n  const { rdiInstanceId } = useParams<{ rdiInstanceId: string }>()\n  const [lastRefreshTime, setLastRefreshTime] = React.useState(Date.now())\n\n  const dispatch = useDispatch()\n\n  const { loading: isStatisticsLoading, results: statisticsResults } =\n    useSelector(rdiStatisticsSelector)\n  const { name: connectedRdiInstanceName } = useSelector(\n    connectedInstanceSelector,\n  )\n  const rdiInstanceName = formatLongName(connectedRdiInstanceName, 33, 0, '...')\n  setTitle(`${rdiInstanceName} - Pipeline Status`)\n\n  const onRefresh = (section: string) => {\n    dispatch(fetchRdiStatistics(rdiInstanceId, section))\n  }\n\n  const onRefreshClicked = (section: string) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.RDI_STATISTICS_REFRESH_CLICKED,\n      eventData: {\n        rdiInstanceId,\n        section,\n      },\n    })\n  }\n\n  const onChangeAutoRefresh = (\n    section: string,\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    sendEventTelemetry({\n      event: enableAutoRefresh\n        ? TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_ENABLED\n        : TelemetryEvent.RDI_STATISTICS_AUTO_REFRESH_DISABLED,\n      eventData: {\n        rdiInstanceId,\n        section,\n        enableAutoRefresh,\n        refreshRate,\n      },\n    })\n  }\n\n  const hideSpinner = () => {\n    setPageLoading(false)\n  }\n\n  useEffect(() => {\n    dispatch(getPipelineStatusAction(rdiInstanceId))\n    dispatch(\n      fetchRdiStatistics(rdiInstanceId, undefined, hideSpinner, hideSpinner),\n    )\n\n    sendPageViewTelemetry({\n      name: TelemetryPageView.RDI_STATUS,\n      eventData: {\n        rdiInstanceId,\n      },\n    })\n  }, [])\n\n  useEffect(\n    () => () => {\n      // unmount\n      dispatch(setLastPageContext(PageNames.rdiStatistics))\n    },\n    [],\n  )\n\n  if (!statisticsResults) {\n    return null\n  }\n\n  // todo add interface\n  if (statisticsResults.status === 'failed') {\n    return (\n      <Text style={{ margin: '20px auto' }}>\n        Unexpected error in your RDI endpoint, please refresh the page\n      </Text>\n    )\n  }\n\n  const sections = statisticsResults.data?.sections || []\n  return (\n    <S.Container>\n      <S.ContentWrapper gap=\"xxl\" grow={false}>\n        {pageLoading && (\n          <S.LoadingState centered>\n            <Loader size=\"xl\" />\n          </S.LoadingState>\n        )}\n        {!shouldShowStatistics(statisticsResults) ? (\n          // TODO add loader\n          <Empty rdiInstanceId={rdiInstanceId} />\n        ) : (\n          !pageLoading && (\n            <>\n              <Row justify=\"end\">\n                <FlexItem>\n                  <AutoRefresh\n                    postfix=\"processing-performance-info\"\n                    displayText\n                    loading={isStatisticsLoading}\n                    lastRefreshTime={lastRefreshTime}\n                    enableAutoRefreshDefault\n                    testid=\"processing-performance-info\"\n                    onRefresh={() => {\n                      setLastRefreshTime(Date.now())\n                      onRefresh('processing_performance')\n                    }}\n                    onRefreshClicked={() =>\n                      onRefreshClicked('processing_performance')\n                    }\n                    onEnableAutoRefresh={(\n                      enableAutoRefresh: boolean,\n                      refreshRate: string,\n                    ) =>\n                      onChangeAutoRefresh(\n                        'processing_performance',\n                        enableAutoRefresh,\n                        refreshRate,\n                      )\n                    }\n                  />\n                </FlexItem>\n              </Row>\n              {sections.map((section) => renderStatisticsSection(section))}\n            </>\n          )\n        )}\n      </S.ContentWrapper>\n    </S.Container>\n  )\n}\n\nexport default StatisticsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/clients/Clients.tsx",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/accordion/Accordion.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\nimport Accordion from './Accordion'\n\nconst mockedProps = {\n  id: 'accordion',\n  title: 'Accordion Title',\n  children: <div>Accordion Content</div>,\n  onRefresh: jest.fn(),\n  onRefreshClicked: jest.fn(),\n}\n\ndescribe('Accordion', () => {\n  it('renders the title and children', () => {\n    render(<Accordion {...mockedProps} />)\n\n    expect(screen.getByText('Accordion Title')).toBeInTheDocument()\n    expect(screen.getByText('Accordion Content')).toBeInTheDocument()\n  })\n\n  it('calls the onRefresh callback when the refresh button is clicked', () => {\n    render(<Accordion {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId('accordion-refresh-btn'))\n\n    expect(mockedProps.onRefresh).toHaveBeenCalled()\n  })\n\n  it('calls the onRefreshClicked callback when the refresh button is clicked', () => {\n    render(<Accordion {...mockedProps} />)\n\n    fireEvent.click(screen.getByTestId('accordion-refresh-btn'))\n\n    expect(mockedProps.onRefreshClicked).toHaveBeenCalled()\n  })\n\n  it('does not render the auto refresh button when hideAutoRefresh prop is true', () => {\n    render(<Accordion {...mockedProps} hideAutoRefresh />)\n\n    expect(screen.queryByTestId('accordion-refresh-btn')).toBeNull()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/accordion/Accordion.tsx",
    "content": "import React from 'react'\nimport { EuiAccordion } from '@elastic/eui'\n\nimport { AutoRefresh } from 'uiSrc/components'\n\nimport styles from './styles.module.scss'\n\ninterface Props {\n  id: string\n  title: string\n  children: JSX.Element\n  loading?: boolean\n  onRefresh?: () => void\n  onRefreshClicked?: () => void\n  onChangeAutoRefresh?: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  hideAutoRefresh?: boolean\n  enableAutoRefreshDefault?: boolean\n}\n\nconst Accordion = ({\n  id,\n  title,\n  children,\n  loading = false,\n  onRefresh,\n  onRefreshClicked,\n  onChangeAutoRefresh,\n  hideAutoRefresh = false,\n  enableAutoRefreshDefault,\n}: Props) => {\n  const [lastRefreshTime, setLastRefreshTime] = React.useState(Date.now())\n\n  return (\n    <EuiAccordion\n      id={id}\n      className={styles.wrapper}\n      buttonContent={title}\n      paddingSize=\"m\"\n      initialIsOpen\n      extraAction={\n        !hideAutoRefresh && (\n          <AutoRefresh\n            postfix={id}\n            displayText\n            loading={loading}\n            lastRefreshTime={lastRefreshTime}\n            onRefresh={() => {\n              setLastRefreshTime(Date.now())\n              onRefresh?.()\n            }}\n            onRefreshClicked={onRefreshClicked}\n            onEnableAutoRefresh={onChangeAutoRefresh}\n            enableAutoRefreshDefault={enableAutoRefreshDefault}\n            testid={id}\n          />\n        )\n      }\n    >\n      {children}\n    </EuiAccordion>\n  )\n}\n\nexport default Accordion\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/accordion/index.ts",
    "content": "import Accordion from './Accordion'\n\nexport default Accordion\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/accordion/styles.module.scss",
    "content": ".wrapper {\n  :global {\n    .euiAccordion__triggerWrapper,\n    .euiAccordion__childWrapper {\n      background-color: transparent;\n    }\n\n    .euiAccordion__icon.euiAccordion__icon-isOpen {\n      transform: rotate(90deg) !important;\n    }\n\n    .euiAccordion__icon {\n      transform: rotate(0) !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/panel/Panel.spec.tsx",
    "content": "import React from 'react'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport Panel from './Panel'\n\ndescribe('Panel', () => {\n  it('renders children correctly', () => {\n    const { getByText } = render(<Panel>Test Content</Panel>)\n    expect(getByText('Test Content')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/panel/Panel.tsx",
    "content": "import React from 'react'\n\nimport styles from './styles.module.scss'\n\ninterface Props {\n  children: string | JSX.Element\n}\n\nconst Panel = ({ children }: Props) => (\n  <div className={styles.panel}>{children}</div>\n)\n\nexport default Panel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/panel/index.ts",
    "content": "import Panel from './Panel'\n\nexport default Panel\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/panel/styles.module.scss",
    "content": ".panel {\n  border-radius: 8px !important;\n  padding: 18px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-blocks/StatisticsBlocks.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport {\n  StatisticsBlocksSectionFactory,\n  StatisticsBlockItemFactory,\n} from 'uiSrc/mocks/factories/rdi/RdiStatistics.factory'\nimport StatisticsBlocks from './StatisticsBlocks'\n\ndescribe('StatisticsBlocks', () => {\n  it('should render section with correct name', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      name: 'Processing performance information',\n    })\n\n    render(<StatisticsBlocks data={mockData} />)\n\n    expect(\n      screen.getByText('Processing performance information'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render all block labels, values, and units', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      data: [\n        StatisticsBlockItemFactory.build({\n          label: 'Total batches',\n          value: 100,\n          units: 'Total',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Batch size average',\n          value: 1.5,\n          units: 'MB',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Process time average',\n          value: 50,\n          units: 'ms',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'ACK time average',\n          value: 0.5,\n          units: 'sec',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Read time average',\n          value: 10,\n          units: 'ms',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Records per second average',\n          value: 1000,\n          units: 'sec',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Total time average',\n          value: 60,\n          units: 'ms',\n        }),\n      ],\n    })\n\n    render(<StatisticsBlocks data={mockData} />)\n\n    // Labels\n    expect(screen.getByText('Total batches')).toBeInTheDocument()\n    expect(screen.getByText('Batch size average')).toBeInTheDocument()\n    expect(screen.getByText('Process time average')).toBeInTheDocument()\n    expect(screen.getByText('ACK time average')).toBeInTheDocument()\n    expect(screen.getByText('Read time average')).toBeInTheDocument()\n    expect(screen.getByText('Records per second average')).toBeInTheDocument()\n    expect(screen.getByText('Total time average')).toBeInTheDocument()\n\n    // Values\n    expect(screen.getByText('100')).toBeInTheDocument()\n    expect(screen.getByText('1.5')).toBeInTheDocument()\n    expect(screen.getByText('50')).toBeInTheDocument()\n    expect(screen.getByText('0.5')).toBeInTheDocument()\n    expect(screen.getByText('10')).toBeInTheDocument()\n    expect(screen.getByText('1000')).toBeInTheDocument()\n    expect(screen.getByText('60')).toBeInTheDocument()\n\n    // Units\n    expect(screen.getByText('Total')).toBeInTheDocument()\n    expect(screen.getByText('MB')).toBeInTheDocument()\n    expect(screen.getAllByText('ms')).toHaveLength(3)\n    expect(screen.getAllByText('sec')).toHaveLength(2)\n  })\n\n  it('should render empty section when data is empty', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      name: 'Empty Section',\n      data: [],\n    })\n\n    render(<StatisticsBlocks data={mockData} />)\n\n    expect(screen.getByText('Empty Section')).toBeInTheDocument()\n  })\n\n  it('should render single block in one column', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      name: 'Single Block',\n      data: [\n        StatisticsBlockItemFactory.build({\n          label: 'Test Label',\n          value: 42,\n          units: 'items',\n        }),\n      ],\n    })\n\n    render(<StatisticsBlocks data={mockData} />)\n\n    expect(screen.getByText('Single Block')).toBeInTheDocument()\n    expect(screen.getByText('Test Label')).toBeInTheDocument()\n    expect(screen.getByText('42')).toBeInTheDocument()\n    expect(screen.getByText('items')).toBeInTheDocument()\n  })\n\n  it('should distribute blocks across columns correctly', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      name: 'Six Blocks',\n      data: [\n        StatisticsBlockItemFactory.build({\n          label: 'Block 1',\n          value: 1,\n          units: 'u1',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Block 2',\n          value: 2,\n          units: 'u2',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Block 3',\n          value: 3,\n          units: 'u3',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Block 4',\n          value: 4,\n          units: 'u4',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Block 5',\n          value: 5,\n          units: 'u5',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Block 6',\n          value: 6,\n          units: 'u6',\n        }),\n      ],\n    })\n\n    render(<StatisticsBlocks data={mockData} />)\n\n    // All blocks should be rendered\n    expect(screen.getByText('Block 1')).toBeInTheDocument()\n    expect(screen.getByText('Block 2')).toBeInTheDocument()\n    expect(screen.getByText('Block 3')).toBeInTheDocument()\n    expect(screen.getByText('Block 4')).toBeInTheDocument()\n    expect(screen.getByText('Block 5')).toBeInTheDocument()\n    expect(screen.getByText('Block 6')).toBeInTheDocument()\n  })\n\n  it('should handle decimal and zero values', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      name: 'Decimal Values',\n      data: [\n        StatisticsBlockItemFactory.build({\n          label: 'Zero value',\n          value: 0,\n          units: 'count',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Small decimal',\n          value: 0.001,\n          units: 'sec',\n        }),\n        StatisticsBlockItemFactory.build({\n          label: 'Large number',\n          value: 999999,\n          units: 'records',\n        }),\n      ],\n    })\n\n    render(<StatisticsBlocks data={mockData} />)\n\n    expect(screen.getByText('0')).toBeInTheDocument()\n    expect(screen.getByText('0.001')).toBeInTheDocument()\n    expect(screen.getByText('999999')).toBeInTheDocument()\n  })\n\n  it('should render section id based on name', () => {\n    const mockData = StatisticsBlocksSectionFactory.build({\n      name: 'Processing performance information',\n    })\n\n    const { container } = render(<StatisticsBlocks data={mockData} />)\n\n    // id is name.toLowerCase() which keeps spaces\n    const section = container.querySelector(\n      '[id=\"processing performance information\"]',\n    )\n    expect(section).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-blocks/StatisticsBlocks.styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const InfoPanel = styled(FlexItem)`\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.section.header.backgroundColor};\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space050};\n  padding: ${({ theme }: { theme: Theme }) =>\n    `${theme.core.space.space100} ${theme.core.space.space200}`};\n  border: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.components.section.separator.color};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-blocks/StatisticsBlocks.tsx",
    "content": "import React from 'react'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Section } from '@redis-ui/components'\nimport { Text } from 'uiSrc/components/base/text'\nimport { IStatisticsBlocksSection } from 'uiSrc/slices/interfaces'\nimport VerticalDivider from '../../components/vertical-divider'\n\nimport * as S from './StatisticsBlocks.styles'\n\ninterface Props {\n  data: IStatisticsBlocksSection\n}\n\nconst StatisticsBlocks = ({ data }: Props) => {\n  const { name, data: blocks } = data\n\n  // Split blocks into 3 columns, only showing columns that have items\n  const itemsPerColumn = Math.ceil(blocks.length / 3)\n  const columns = [\n    blocks.slice(0, itemsPerColumn),\n    blocks.slice(itemsPerColumn, itemsPerColumn * 2),\n    blocks.slice(itemsPerColumn * 2),\n  ].filter((col) => col.length > 0)\n\n  return (\n    <Section\n      collapsible\n      defaultOpen\n      id={name.toLowerCase()}\n      label={name}\n      content={\n        <Row responsive gap=\"s\" align=\"start\">\n          {columns.flatMap((columnBlocks, columnIndex) => [\n            columnIndex > 0 && (\n              <VerticalDivider key={`divider-${columnIndex}`} />\n            ),\n            <FlexItem key={columnIndex} grow>\n              <Col gap=\"s\">\n                {columnBlocks.map((block) => (\n                  <S.InfoPanel key={block.label} grow>\n                    <Row gap=\"m\" responsive align=\"center\">\n                      <FlexItem grow>\n                        <Text>{block.label}</Text>\n                      </FlexItem>\n                      <FlexItem>\n                        <Text color=\"primary\">{block.value}</Text>\n                      </FlexItem>\n                      <FlexItem style={{ minWidth: 40 }}>\n                        <Text size=\"s\" color=\"ghost\">\n                          {block.units}\n                        </Text>\n                      </FlexItem>\n                    </Row>\n                  </S.InfoPanel>\n                ))}\n              </Col>\n            </FlexItem>,\n          ])}\n        </Row>\n      }\n    />\n  )\n}\n\nexport default StatisticsBlocks\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-blocks/index.ts",
    "content": "export { default } from './StatisticsBlocks'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-info/StatisticsInfo.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport {\n  StatisticsInfoSectionFactory,\n  StatisticsInfoItemFactory,\n} from 'uiSrc/mocks/factories/rdi/RdiStatistics.factory'\nimport StatisticsInfo from './StatisticsInfo'\n\ndescribe('StatisticsInfo', () => {\n  it('should render section title', () => {\n    const mockData = StatisticsInfoSectionFactory.build({\n      name: 'General info',\n    })\n\n    render(<StatisticsInfo data={mockData} />)\n\n    expect(screen.getByText('General info')).toBeInTheDocument()\n  })\n\n  it('should render all info items with labels and values', () => {\n    const mockData = StatisticsInfoSectionFactory.build({\n      data: [\n        { label: 'RDI version', value: '1.0.0' },\n        { label: 'RDI database address', value: 'redis://localhost:6379' },\n        { label: 'Run status', value: 'running' },\n        { label: 'Sync mode', value: 'streaming' },\n      ],\n    })\n\n    render(<StatisticsInfo data={mockData} />)\n\n    // Labels\n    expect(screen.getByText('RDI version')).toBeInTheDocument()\n    expect(screen.getByText('RDI database address')).toBeInTheDocument()\n    expect(screen.getByText('Run status')).toBeInTheDocument()\n    expect(screen.getByText('Sync mode')).toBeInTheDocument()\n\n    // Values\n    expect(screen.getByText('1.0.0')).toBeInTheDocument()\n    expect(screen.getByText('redis://localhost:6379')).toBeInTheDocument()\n    expect(screen.getByText('running')).toBeInTheDocument()\n    expect(screen.getByText('streaming')).toBeInTheDocument()\n  })\n\n  it('should render empty section when data is empty', () => {\n    const mockData = StatisticsInfoSectionFactory.build({\n      name: 'Empty Info',\n      data: [],\n    })\n\n    render(<StatisticsInfo data={mockData} />)\n\n    expect(screen.getByText('Empty Info')).toBeInTheDocument()\n  })\n\n  it('should render single info item correctly', () => {\n    const mockData = StatisticsInfoSectionFactory.build({\n      name: 'Single Item',\n      data: [\n        StatisticsInfoItemFactory.build({ label: 'Status', value: 'active' }),\n      ],\n    })\n\n    render(<StatisticsInfo data={mockData} />)\n\n    expect(screen.getByText('Single Item')).toBeInTheDocument()\n    expect(screen.getByText('Status')).toBeInTheDocument()\n    expect(screen.getByText('active')).toBeInTheDocument()\n  })\n\n  it('should handle empty string values', () => {\n    const mockData = StatisticsInfoSectionFactory.build({\n      name: 'Empty Values',\n      data: [\n        StatisticsInfoItemFactory.build({ label: 'Version', value: '' }),\n        StatisticsInfoItemFactory.build({ label: 'Address', value: '' }),\n      ],\n    })\n\n    render(<StatisticsInfo data={mockData} />)\n\n    expect(screen.getByText('Empty Values')).toBeInTheDocument()\n    expect(screen.getByText('Version')).toBeInTheDocument()\n    expect(screen.getByText('Address')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-info/StatisticsInfo.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Panel as BasePanel } from 'uiSrc/components/panel'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Panel = styled(BasePanel)`\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.section.header.backgroundColor};\n  border-radius: ${({ theme }: { theme: Theme }) =>\n    theme.components.section.borderRadius};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-info/StatisticsInfo.tsx",
    "content": "import React from 'react'\n\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { IStatisticsInfoSection } from 'uiSrc/slices/interfaces'\nimport VerticalDivider from '../../components/vertical-divider'\n\nimport * as S from './StatisticsInfo.styles'\n\ninterface Props {\n  data: IStatisticsInfoSection\n}\n\nconst StatisticsInfo = ({ data }: Props) => {\n  const { name, data: items } = data\n\n  return (\n    <S.Panel>\n      <Col gap=\"l\">\n        <Title size=\"S\" color=\"primary\">\n          {name}\n        </Title>\n        <Row gap=\"m\" responsive>\n          {items.map((item, index) => (\n            <React.Fragment key={item.label}>\n              {index > 0 && <VerticalDivider />}\n              <Row gap=\"m\" responsive>\n                <FlexItem>\n                  <Text>{item.label}</Text>\n                </FlexItem>\n                <FlexItem>\n                  <Text color=\"primary\">{item.value}</Text>\n                </FlexItem>\n              </Row>\n            </React.Fragment>\n          ))}\n        </Row>\n      </Col>\n    </S.Panel>\n  )\n}\n\nexport default StatisticsInfo\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-info/index.ts",
    "content": "export { default } from './StatisticsInfo'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-table/StatisticsTable.spec.tsx",
    "content": "import React from 'react'\n\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { StatisticsCellType } from 'uiSrc/slices/interfaces'\nimport { StatisticsTableSectionFactory } from 'uiSrc/mocks/factories/rdi/RdiStatistics.factory'\nimport StatisticsTable from './StatisticsTable'\n\ndescribe('StatisticsTable', () => {\n  it('should render section with correct name', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      name: 'Target connections',\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    expect(screen.getByText('Target connections')).toBeInTheDocument()\n  })\n\n  it('should render all column headers', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [\n        { id: 'status', header: 'Status', type: StatisticsCellType.Status },\n        { id: 'name', header: 'Name' },\n        { id: 'host', header: 'Host' },\n        { id: 'type', header: 'Type' },\n      ],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    expect(screen.getByText('Status')).toBeInTheDocument()\n    expect(screen.getByText('Name')).toBeInTheDocument()\n    expect(screen.getByText('Host')).toBeInTheDocument()\n    expect(screen.getByText('Type')).toBeInTheDocument()\n  })\n\n  it('should render all data rows', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [\n        { id: 'status', header: 'Status', type: StatisticsCellType.Status },\n        { id: 'name', header: 'Name' },\n        { id: 'host', header: 'Host' },\n        { id: 'type', header: 'Type' },\n      ],\n      data: [\n        {\n          status: 'connected',\n          name: 'target1',\n          host: 'localhost:6379',\n          type: 'redis',\n        },\n        {\n          status: 'not yet used',\n          name: 'target2',\n          host: 'localhost:6380',\n          type: 'redis',\n        },\n      ],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    expect(screen.getByText('target1')).toBeInTheDocument()\n    expect(screen.getByText('target2')).toBeInTheDocument()\n    expect(screen.getByText('localhost:6379')).toBeInTheDocument()\n    expect(screen.getByText('localhost:6380')).toBeInTheDocument()\n  })\n\n  it('should render correct number of columns and rows for target connections', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [\n        { id: 'status', header: 'Status', type: StatisticsCellType.Status },\n        { id: 'name', header: 'Name' },\n        { id: 'host', header: 'Host' },\n        { id: 'type', header: 'Type' },\n      ],\n      data: [\n        {\n          status: 'connected',\n          name: 'target1',\n          host: 'localhost:6379',\n          type: 'redis',\n        },\n        {\n          status: 'not yet used',\n          name: 'target2',\n          host: 'localhost:6380',\n          type: 'redis',\n        },\n      ],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    const columnHeaders = screen.getAllByRole('columnheader')\n    expect(columnHeaders).toHaveLength(4)\n\n    const dataRows = screen.getAllByRole('row')\n    expect(dataRows).toHaveLength(3) // 2 data rows + 1 header row\n  })\n\n  it('should render correct number of columns and rows for clients', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      name: 'Clients',\n      columns: [\n        { id: 'id', header: 'ID' },\n        { id: 'addr', header: 'Address' },\n        { id: 'name', header: 'Name' },\n        { id: 'ageSec', header: 'Age (sec)' },\n        { id: 'idleSec', header: 'Idle (sec)' },\n        { id: 'user', header: 'User' },\n      ],\n      data: [\n        {\n          id: 'client1',\n          addr: '127.0.0.1',\n          name: 'Client 1',\n          ageSec: 11,\n          idleSec: 5,\n          user: 'user1',\n        },\n        {\n          id: 'client2',\n          addr: '127.0.0.2',\n          name: 'Client 2',\n          ageSec: 20,\n          idleSec: 12,\n          user: 'user2',\n        },\n      ],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    const columnHeaders = screen.getAllByRole('columnheader')\n    expect(columnHeaders).toHaveLength(6)\n\n    const dataRows = screen.getAllByRole('row')\n    expect(dataRows).toHaveLength(3) // 2 data rows + 1 header row\n  })\n\n  it('should render correct number of columns and rows for data streams with footer', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      name: 'Data streams',\n      columns: [\n        { id: 'name', header: 'Name' },\n        { id: 'total', header: 'Total' },\n        { id: 'pending', header: 'Pending' },\n        { id: 'inserted', header: 'Inserted' },\n        { id: 'updated', header: 'Updated' },\n        { id: 'deleted', header: 'Deleted' },\n        { id: 'filtered', header: 'Filtered' },\n        { id: 'rejected', header: 'Rejected' },\n        { id: 'deduplicated', header: 'Deduplicated' },\n        {\n          id: 'last_arrival',\n          header: 'Last arrival',\n          type: StatisticsCellType.Date,\n        },\n      ],\n      data: [\n        {\n          name: 'stream1',\n          total: 11,\n          pending: 5,\n          inserted: 3,\n          updated: 2,\n          deleted: 1,\n          filtered: 0,\n          rejected: 0,\n          deduplicated: 0,\n          last_arrival: '2022-01-01',\n        },\n        {\n          name: 'stream2',\n          total: 20,\n          pending: 10,\n          inserted: 6,\n          updated: 4,\n          deleted: 2,\n          filtered: 0,\n          rejected: 0,\n          deduplicated: 0,\n          last_arrival: '2022-01-02',\n        },\n      ],\n      footer: {\n        name: 'Totals',\n        total: 31,\n        pending: 15,\n        inserted: 9,\n        updated: 6,\n        deleted: 3,\n        filtered: 0,\n        rejected: 0,\n        deduplicated: 0,\n        last_arrival: '',\n      },\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    const columnHeaders = screen.getAllByRole('columnheader')\n    expect(columnHeaders).toHaveLength(10)\n\n    const dataRows = screen.getAllByRole('row')\n    expect(dataRows).toHaveLength(4) // 2 data rows + 1 header row + 1 footer row\n  })\n\n  it('should render empty table when data is empty', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      name: 'Empty Table',\n      columns: [{ id: 'name', header: 'Name' }],\n      data: [],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    expect(screen.getByText('Empty Table')).toBeInTheDocument()\n    expect(screen.getByText('Name')).toBeInTheDocument()\n  })\n\n  it('should render footer row when provided', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [\n        { id: 'name', header: 'Name' },\n        { id: 'total', header: 'Total' },\n        { id: 'pending', header: 'Pending' },\n      ],\n      data: [\n        { name: 'stream1', total: 11, pending: 5 },\n        { name: 'stream2', total: 20, pending: 10 },\n      ],\n      footer: { name: 'Totals', total: 31, pending: 15 },\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    expect(screen.getByText('Totals')).toBeInTheDocument()\n    expect(screen.getByText('31')).toBeInTheDocument()\n    expect(screen.getByText('15')).toBeInTheDocument()\n  })\n\n  it('should render section id based on name', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      name: 'Target connections',\n    })\n\n    const { container } = render(<StatisticsTable data={mockData} />)\n\n    expect(\n      container.querySelector('[id=\"target connections\"]'),\n    ).toBeInTheDocument()\n  })\n\n  it('should truncate long values and show tooltip', () => {\n    const longValue = 'this_is_a_very_long_stream_name_that_should_be_truncated'\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [{ id: 'name', header: 'Name' }],\n      data: [{ name: longValue }],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    // Should show truncated value (formatLongName with maxLength=30, endPartLength=0, separator='...')\n    expect(\n      screen.getByText('this_is_a_very_long_stream_...'),\n    ).toBeInTheDocument()\n    // Full value should not be visible as text (only in tooltip)\n    expect(screen.queryByText(longValue)).not.toBeInTheDocument()\n  })\n\n  it('should not truncate short values', () => {\n    const shortValue = 'short_name'\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [{ id: 'name', header: 'Name' }],\n      data: [{ name: shortValue }],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    expect(screen.getByText(shortValue)).toBeInTheDocument()\n  })\n\n  it('should format date values using FormatedDate component when type is date', () => {\n    const mockData = StatisticsTableSectionFactory.build({\n      columns: [\n        {\n          id: 'last_arrival',\n          header: 'Last arrival',\n          type: StatisticsCellType.Date,\n        },\n      ],\n      data: [{ last_arrival: '2024-01-15T10:30:00Z' }],\n    })\n\n    render(<StatisticsTable data={mockData} />)\n\n    // FormatedDate formats the date according to user settings\n    // The exact format depends on user config, but it should render something\n    expect(screen.getByRole('cell')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-table/StatisticsTable.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Section } from '@redis-ui/components'\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nexport const SectionBody = styled(Section.Body)`\n  padding: 1px 0 0 0;\n`\n\nexport const StatisticsTable = styled(Table.Compose)`\n  box-shadow: none;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-table/StatisticsTable.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport { Section } from '@redis-ui/components'\nimport { IStatisticsTableSection } from 'uiSrc/slices/interfaces'\nimport useStatisticsTableColumns from './hooks/useStatisticsTableColumns'\n\nimport * as S from './StatisticsTable.styles'\n\ninterface Props {\n  data: IStatisticsTableSection\n}\n\nconst StatisticsTable = ({ data }: Props) => {\n  const { name, columns, data: tableData, footer } = data\n\n  const tableColumns = useStatisticsTableColumns(columns)\n\n  const dataWithFooter = useMemo(\n    () => (footer ? [...tableData, footer] : tableData),\n    [tableData, footer],\n  )\n\n  return (\n    <Section.Compose collapsible defaultOpen id={name.toLowerCase()}>\n      <Section.Header label={name} />\n      <S.SectionBody>\n        <S.StatisticsTable columns={tableColumns} data={dataWithFooter}>\n          <Table.Root>\n            <Table.Header />\n            <Table.Body />\n          </Table.Root>\n        </S.StatisticsTable>\n      </S.SectionBody>\n    </Section.Compose>\n  )\n}\n\nexport default StatisticsTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-table/hooks/useStatisticsTableColumns.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { FormatedDate, RiTooltip } from 'uiSrc/components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Indicator } from 'uiSrc/components/base/text/text.styles'\nimport { formatLongName } from 'uiSrc/utils'\nimport {\n  IStatisticsColumn,\n  StatisticsCellType,\n  StatisticsConnectionStatus,\n} from 'uiSrc/slices/interfaces'\n\nconst MAX_LENGTH = 30\n\nconst getStatusColor = (status: string) => {\n  switch (status) {\n    case StatisticsConnectionStatus.Connected:\n      return 'success'\n    case StatisticsConnectionStatus.NotYetUsed:\n      return 'warning'\n    default:\n      return 'danger'\n  }\n}\n\nconst useStatisticsTableColumns = (\n  columns: IStatisticsColumn[],\n): ColumnDef<Record<string, unknown>>[] =>\n  useMemo(\n    () =>\n      columns.map((column) => {\n        const baseColumn: ColumnDef<Record<string, unknown>> = {\n          header: column.header,\n          id: column.id,\n          accessorKey: column.id,\n        }\n\n        if (column.type === StatisticsCellType.Status) {\n          return {\n            ...baseColumn,\n            size: 40,\n            cell: ({ row: { original } }) => {\n              const status = original[column.id] as string\n              return (\n                <Row align=\"center\" justify=\"center\">\n                  <RiTooltip content={status}>\n                    <Indicator $color={getStatusColor(status)} />\n                  </RiTooltip>\n                </Row>\n              )\n            },\n          }\n        }\n\n        if (column.type === StatisticsCellType.Date) {\n          return {\n            ...baseColumn,\n            cell: ({ row: { original } }) => {\n              const value = original[column.id] as string\n              return <FormatedDate date={value} />\n            },\n          }\n        }\n\n        return {\n          ...baseColumn,\n          cell: ({ row: { original } }) => {\n            const value = original[column.id]\n            const stringValue = String(value ?? '')\n\n            if (stringValue.length > MAX_LENGTH) {\n              return (\n                <RiTooltip content={stringValue}>\n                  {formatLongName(stringValue, MAX_LENGTH, 0, '...')}\n                </RiTooltip>\n              )\n            }\n\n            return stringValue\n          },\n        }\n      }),\n    [columns],\n  )\n\nexport default useStatisticsTableColumns\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/statistics-table/index.ts",
    "content": "export { default } from './StatisticsTable'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/vertical-divider/VerticalDivider.spec.tsx",
    "content": "import React from 'react'\n\nimport { render } from 'uiSrc/utils/test-utils'\nimport VerticalDivider from './VerticalDivider'\n\ndescribe('VerticalDivider', () => {\n  it('should render', () => {\n    expect(render(<VerticalDivider />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/vertical-divider/VerticalDivider.tsx",
    "content": "import React from 'react'\n\nimport Divider from 'uiSrc/components/divider/Divider'\n\nimport styles from './styles.module.scss'\n\nconst VerticalDivider = (props: any) => (\n  <Divider className={styles.divider} orientation=\"vertical\" {...props} />\n)\n\nexport default VerticalDivider\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/vertical-divider/index.ts",
    "content": "import VerticalDivider from './VerticalDivider'\n\nexport default VerticalDivider\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/components/vertical-divider/styles.module.scss",
    "content": ".divider {\n  margin: 0 12px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/empty/Empty.spec.tsx",
    "content": "import { createMemoryHistory } from 'history'\nimport React from 'react'\nimport { Router } from 'react-router-dom'\n\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport Empty from './Empty'\n\ndescribe('Empty', () => {\n  test('renders empty pipeline message', () => {\n    render(<Empty rdiInstanceId=\"123\" />)\n    expect(screen.getByText('No pipeline deployed yet')).toBeInTheDocument()\n    expect(\n      screen.getByText('Create your first pipeline to get started!'),\n    ).toBeInTheDocument()\n  })\n\n  test('navigates to pipeline config page when \"Add Pipeline\" button is clicked', () => {\n    const history = createMemoryHistory()\n    render(\n      <Router history={history}>\n        <Empty rdiInstanceId=\"123\" />\n      </Router>,\n    )\n\n    const addPipelineButton = screen.getByTestId('add-pipeline-btn')\n    fireEvent.click(addPipelineButton)\n\n    waitFor(() => {\n      expect(history.location.pathname).toBe('/rdi/pipeline-config/123')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/empty/Empty.tsx",
    "content": "import React from 'react'\nimport { useHistory } from 'react-router-dom'\n\nimport EmptyPipelineIcon from 'uiSrc/assets/img/rdi/empty_pipeline.svg'\nimport { Pages } from 'uiSrc/constants'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiImage } from 'uiSrc/components/base/display'\nimport Panel from '../components/panel'\n\nimport styles from './styles.module.scss'\n\ninterface Props {\n  rdiInstanceId: string\n}\n\nconst Empty = ({ rdiInstanceId }: Props) => {\n  const history = useHistory()\n\n  return (\n    <Panel>\n      <div\n        className={styles.emptyPipelineContainer}\n        data-testid=\"empty-pipeline\"\n      >\n        <RiImage src={EmptyPipelineIcon} alt=\"empty\" $size=\"s\" />\n        <Spacer size=\"xl\" />\n        <Text>No pipeline deployed yet</Text>\n        <Text className={styles.subTitle}>\n          Create your first pipeline to get started!\n        </Text>\n        <Spacer size=\"l\" />\n        <PrimaryButton\n          data-testid=\"add-pipeline-btn\"\n          size=\"s\"\n          onClick={() => {\n            history.push(Pages.rdiPipelineConfig(rdiInstanceId))\n          }}\n        >\n          Add Pipeline\n        </PrimaryButton>\n      </div>\n    </Panel>\n  )\n}\n\nexport default Empty\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/empty/index.ts",
    "content": "import Empty from './Empty'\n\nexport default Empty\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/empty/styles.module.scss",
    "content": ".emptyPipelineContainer {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-direction: column;\n  margin: auto;\n  height: calc(100vh - 115px);\n}\n\n.subTitle {\n  font-size: 18px !important;\n  font-weight: 500 !important;\n  margin-top: 6px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/index.ts",
    "content": "import StatisticsPage from './StatisticsPage'\n\nexport default StatisticsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/status/styles.ts",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/pages/rdi/statistics/styles.ts",
    "content": "import styled from 'styled-components'\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nexport const StyledRdiAnalyticsTable = styled(Table.Compose)`\n  box-shadow: none;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport { RedisClusterInstanceFactory } from 'uiSrc/mocks/factories/cluster/RedisClusterInstance.factory'\nimport RedisClusterDatabases from './RedisClusterDatabases'\nimport { colFactory } from './useClusterDatabasesConfig'\n\nconst emptyInstances: [] = []\nconst mockInstances = RedisClusterInstanceFactory.buildList(5)\nconst mockManyInstances = RedisClusterInstanceFactory.buildList(15)\n\nconst [emptyColumns] = colFactory(emptyInstances)\nconst [columnsWithData] = colFactory(mockInstances)\nconst [columnsWithManyData] = colFactory(mockManyInstances)\n\nconst meta: Meta<typeof RedisClusterDatabases> = {\n  component: RedisClusterDatabases,\n  args: {\n    columns: emptyColumns,\n    instances: emptyInstances,\n    loading: false,\n    onClose: fn(),\n    onBack: fn(),\n    onSubmit: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Empty: Story = {}\n\nexport const WithDatabases: Story = {\n  args: {\n    instances: mockInstances,\n    columns: columnsWithData,\n  },\n}\n\nexport const WithManyDatabases: Story = {\n  args: {\n    instances: mockManyInstances,\n    columns: columnsWithManyData,\n  },\n}\n\nexport const Loading: Story = {\n  args: {\n    instances: emptyInstances,\n    columns: emptyColumns,\n    loading: true,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabases.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport type { Maybe } from 'uiSrc/utils'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport type { InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport validationErrors from 'uiSrc/constants/validationErrors'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  type ColumnDef,\n  type RowSelectionState,\n  Table,\n} from 'uiSrc/components/base/layout/table'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { CancelButton } from './components'\n\ninterface Props {\n  columns: ColumnDef<InstanceRedisCluster>[]\n  onClose: () => void\n  onBack: () => void\n  onSubmit: (uids: Maybe<number>[]) => void\n  instances: InstanceRedisCluster[]\n  loading: boolean\n}\n\nconst loadingMsg = 'loading...'\nconst notFoundMsg = 'Not found'\nconst noResultsMessage =\n  'Your Redis Enterprise Cluster has no databases available.'\n\nfunction getSubtitle(items: InstanceRedisCluster[]) {\n  if (!items.length) {\n    return null\n  }\n\n  return `These are the ${items.length > 1 ? 'databases ' : 'database '}\nin your Redis Enterprise Cluster. Select the\n${items.length > 1 ? ' databases ' : ' database '} that you want\nto add.`\n}\n\nconst hasSelection = (selection: RowSelectionState) =>\n  Object.values(selection).some(Boolean)\nconst RedisClusterDatabases = ({\n  columns,\n  onClose,\n  onBack,\n  onSubmit,\n  instances,\n  loading,\n}: Props) => {\n  const [items, setItems] = useState<InstanceRedisCluster[]>([])\n  const [message, setMessage] = useState(loadingMsg)\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false)\n\n  const [selection, setSelection] = useState<RowSelectionState>({})\n\n  useEffect(() => {\n    if (instances !== null) {\n      setItems(instances)\n    }\n  }, [instances])\n\n  useEffect(() => {\n    if (instances?.length === 0) {\n      setMessage(noResultsMessage)\n    }\n  }, [instances])\n\n  const handleSubmit = () => {\n    // Map rowSelection state to the selected items list using uid as row id\n    const selected = Object.entries(selection)\n      .filter(([_uid, isSelected]) => Boolean(isSelected))\n      .map(([uid]) => Number(uid))\n    onSubmit(selected)\n  }\n\n  const showPopover = () => {\n    setIsPopoverOpen(true)\n  }\n\n  const closePopover = () => {\n    setIsPopoverOpen(false)\n  }\n\n  const isSubmitDisabled = () => !hasSelection(selection)\n\n  const onSelectionChange = (selection: RowSelectionState) => {\n    setSelection(selection)\n  }\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n    const itemsTemp =\n      instances?.filter(\n        (item: InstanceRedisCluster) =>\n          item.name?.toLowerCase().indexOf(value) !== -1 ||\n          item.dnsName?.toLowerCase().indexOf(value) !== -1 ||\n          item.port?.toString().toLowerCase().indexOf(value) !== -1,\n      ) ?? []\n\n    if (!itemsTemp?.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer>\n        <Header\n          title=\"Auto-Discover Redis Enterprise Databases\"\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n          subTitle={getSubtitle(items)}\n        />\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            columns={columns}\n            data={items}\n            rowSelectionMode=\"multiple\"\n            getRowId={(row) => `${row.uid}`}\n            onRowSelectionChange={onSelectionChange}\n            defaultSorting={[{ id: 'name', desc: false }]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() => <EmptyState message={message} />}\n          />\n        </DatabaseWrapper>\n      </DatabaseContainer>\n      <Footer>\n        <Row justify=\"end\" gap=\"m\">\n          <CancelButton\n            isPopoverOpen={isPopoverOpen}\n            onShowPopover={showPopover}\n            onClosePopover={closePopover}\n            onProceed={onClose}\n          />\n          <RiTooltip\n            position=\"top\"\n            anchorClassName=\"euiToolTip__btn-disabled\"\n            title={\n              isSubmitDisabled()\n                ? validationErrors.SELECT_AT_LEAST_ONE('database')\n                : null\n            }\n            content={\n              isSubmitDisabled() ? (\n                <span>{validationErrors.NO_DBS_SELECTED}</span>\n              ) : null\n            }\n          >\n            <PrimaryButton\n              size=\"m\"\n              disabled={isSubmitDisabled()}\n              onClick={handleSubmit}\n              loading={loading}\n              color=\"secondary\"\n              icon={isSubmitDisabled() ? InfoIcon : undefined}\n              data-testid=\"btn-add-databases\"\n            >\n              Add selected Databases\n            </PrimaryButton>\n          </RiTooltip>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default RedisClusterDatabases\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport { clusterSelector } from 'uiSrc/slices/instances/cluster'\nimport RedisClusterDatabasesPage from './RedisClusterDatabasesPage'\n\njest.mock('uiSrc/slices/instances/cluster', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/cluster'),\n  clusterSelector: jest.fn().mockReturnValue({\n    data: [\n      {\n        id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff',\n        host: 'localhost',\n        port: 6379,\n        name: 'localhost',\n        username: null,\n        password: null,\n        connectionType: 'STANDALONE',\n        nameFromProvider: null,\n        new: true,\n        lastConnection: new Date('2021-04-22T09:03:56.917Z'),\n      },\n      {\n        id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4',\n        host: 'localhost',\n        port: 12000,\n        name: 'oea123123',\n        username: null,\n        password: null,\n        connectionType: 'STANDALONE',\n        nameFromProvider: null,\n        lastConnection: null,\n        tls: {\n          verifyServerCert: true,\n          caCertId: '70b95d32-c19d-4311-bb24-e684af12cf15',\n          clientCertPairId: '70b95d32-c19d-4311-b23b24-e684af12cf15',\n        },\n      },\n    ],\n    dataAdded: [],\n  }),\n}))\n\n/**\n * RedisClusterDatabasesPage tests\n *\n * @group component\n */\ndescribe('RedisClusterDatabasesPage', () => {\n  it('should render with added column', () => {\n    expect(render(<RedisClusterDatabasesPage />)).toBeTruthy()\n  })\n  it('should render table', () => {\n    const { queryByTestId } = render(<RedisClusterDatabasesPage />)\n    expect(queryByTestId('db_name_localhost')).toBeInTheDocument()\n  })\n  it('should render', () => {\n    ;(clusterSelector as jest.Mock).mockReturnValueOnce({\n      data: [],\n      dataAdded: [],\n    })\n\n    expect(render(<RedisClusterDatabasesPage />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx",
    "content": "import React from 'react'\nimport RedisClusterDatabases from './RedisClusterDatabases'\nimport RedisClusterDatabasesResult from './RedisClusterDatabasesResult'\nimport { useClusterDatabasesConfig } from './useClusterDatabasesConfig'\n\nconst RedisClusterDatabasesPage = () => {\n  const {\n    columns,\n    columnsResult,\n    instancesAdded,\n    instances,\n    loading,\n    handleClose,\n    handleBackAdding,\n    handleAddInstances,\n  } = useClusterDatabasesConfig()\n\n  if (instancesAdded.length) {\n    return (\n      <RedisClusterDatabasesResult\n        instances={instancesAdded || []}\n        onView={handleClose}\n        onBack={handleBackAdding}\n        columns={columnsResult}\n      />\n    )\n  }\n\n  return (\n    <RedisClusterDatabases\n      instances={instances || []}\n      loading={loading}\n      onClose={handleClose}\n      onBack={handleBackAdding}\n      onSubmit={handleAddInstances}\n      columns={columns}\n    />\n  )\n}\n\nexport default RedisClusterDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RedisClusterDatabasesResult from './RedisClusterDatabasesResult'\n\ndescribe('RedisClusterDatabasesResult', () => {\n  it('should render', () => {\n    const columnsMock = [\n      {\n        header: 'Subscription ID',\n        id: 'subscriptionId',\n        accessorKey: 'subscriptionId',\n        enableSorting: true,\n      },\n    ]\n    expect(\n      render(\n        <RedisClusterDatabasesResult\n          columns={columnsMock}\n          instances={[]}\n          onView={jest.fn()}\n          onBack={jest.fn()}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { fn } from 'storybook/test'\n\nimport { RedisClusterInstanceAddedFactory } from 'uiSrc/mocks/factories/cluster/RedisClusterInstance.factory'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\nimport RedisClusterDatabasesResult from './RedisClusterDatabasesResult'\nimport { colFactory } from './useClusterDatabasesConfig'\n\nconst mockInstancesSuccess = RedisClusterInstanceAddedFactory.buildList(3, {\n  statusAdded: AddRedisDatabaseStatus.Success,\n})\nconst mockInstancesFailed = RedisClusterInstanceAddedFactory.buildList(2, {\n  statusAdded: AddRedisDatabaseStatus.Fail,\n})\nconst mockInstancesMixed = [\n  ...RedisClusterInstanceAddedFactory.buildList(3, {\n    statusAdded: AddRedisDatabaseStatus.Success,\n  }),\n  ...RedisClusterInstanceAddedFactory.buildList(2, {\n    statusAdded: AddRedisDatabaseStatus.Fail,\n  }),\n]\n\nconst [, colMock] = colFactory(mockInstancesSuccess)\n\nconst meta: Meta<typeof RedisClusterDatabasesResult> = {\n  component: RedisClusterDatabasesResult,\n  args: {\n    columns: colMock,\n    instances: [],\n    onBack: fn(),\n    onView: fn(),\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Empty: Story = {}\n\nexport const AllSuccess: Story = {\n  args: {\n    instances: mockInstancesSuccess,\n    columns: colMock,\n  },\n}\n\nexport const AllFailed: Story = {\n  args: {\n    instances: mockInstancesFailed,\n    columns: colMock,\n  },\n}\n\nexport const Mixed: Story = {\n  args: {\n    instances: mockInstancesMixed,\n    columns: colMock,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesResult.tsx",
    "content": "import React, { useEffect, useState } from 'react'\n\nimport type { InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport { AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\nimport { setTitle } from 'uiSrc/utils'\nimport MessageBar from 'uiSrc/components/message-bar/MessageBar'\nimport { riToast } from 'uiSrc/components/base/display/toast'\nimport { AutodiscoveryPageTemplate } from 'uiSrc/templates'\n\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { type ColumnDef, Table } from 'uiSrc/components/base/layout/table'\nimport {\n  DatabaseContainer,\n  DatabaseWrapper,\n  EmptyState,\n  Footer,\n  Header,\n} from 'uiSrc/components/auto-discover'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { SummaryText } from './components'\n\nexport interface Props {\n  columns: ColumnDef<InstanceRedisCluster>[]\n  instances: InstanceRedisCluster[]\n  onView: (sendEvent?: boolean) => void\n  onBack: (sendEvent?: boolean) => void\n}\n\nconst loadingMsg = 'loading...'\nconst notFoundMsg = 'Not found'\n\nconst RedisClusterDatabasesResult = ({\n  columns,\n  instances,\n  onBack,\n  onView,\n}: Props) => {\n  const [items, setItems] = useState<InstanceRedisCluster[]>([])\n  const [message, setMessage] = useState(loadingMsg)\n\n  useEffect(() => {\n    setTitle('Redis Enterprise Databases Added')\n  }, [])\n\n  useEffect(() => {\n    setItems(instances)\n  }, [instances])\n\n  const countSuccessAdded = instances.filter(\n    ({ statusAdded }) => statusAdded === AddRedisDatabaseStatus.Success,\n  )?.length\n\n  const countFailAdded = instances.filter(\n    ({ statusAdded }) => statusAdded === AddRedisDatabaseStatus.Fail,\n  )?.length\n\n  const onQueryChange = (term: string) => {\n    const value = term?.toLowerCase()\n    const itemsTemp = instances.filter(\n      (item: InstanceRedisCluster) =>\n        item.name?.toLowerCase().indexOf(value) !== -1 ||\n        item.dnsName?.toLowerCase().indexOf(value) !== -1 ||\n        item.port?.toString().indexOf(value) !== -1,\n    )\n\n    if (!itemsTemp.length) {\n      setMessage(notFoundMsg)\n    }\n    setItems(itemsTemp)\n  }\n\n  return (\n    <AutodiscoveryPageTemplate>\n      <DatabaseContainer justify=\"start\">\n        <Header\n          title={`\n          Redis Enterprise\n          ${\n            countSuccessAdded + countFailAdded > 1\n              ? ' Databases '\n              : ' Database '\n          }\n          Added\n          `}\n          onBack={onBack}\n          onQueryChange={onQueryChange}\n        />\n        <MessageBar\n          opened={!!countSuccessAdded || !!countFailAdded}\n          variant={\n            !!countFailAdded\n              ? riToast.Variant.Attention\n              : riToast.Variant.Success\n          }\n        >\n          <SummaryText\n            countSuccessAdded={countSuccessAdded}\n            countFailAdded={countFailAdded}\n          />\n        </MessageBar>\n        <Spacer size=\"m\" />\n        <DatabaseWrapper>\n          <Table\n            columns={columns}\n            data={items}\n            defaultSorting={[{ id: 'name', desc: false }]}\n            paginationEnabled={items.length > 10}\n            stripedRows\n            emptyState={() => <EmptyState message={message} />}\n          />\n        </DatabaseWrapper>\n      </DatabaseContainer>\n      <Footer>\n        <Row justify=\"end\">\n          <PrimaryButton\n            size=\"m\"\n            onClick={() => onView(false)}\n            data-testid=\"btn-view-databases\"\n          >\n            View Databases\n          </PrimaryButton>\n        </Row>\n      </Footer>\n    </AutodiscoveryPageTemplate>\n  )\n}\n\nexport default RedisClusterDatabasesResult\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/capabilities.tsx",
    "content": "import React from 'react'\nimport { DatabaseListModules } from 'uiSrc/components'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport {\n  RedisClusterIds,\n  RedisClusterTitles,\n} from 'uiSrc/pages/redis-cluster/constants/constants'\n\nexport const capabilitiesColumn = (): ColumnDef<InstanceRedisCluster> => {\n  return {\n    header: RedisClusterTitles.Capabilities,\n    id: RedisClusterIds.Capabilities,\n    accessorKey: RedisClusterIds.Capabilities,\n    enableSorting: true,\n    maxSize: 150,\n    cell: function Modules({ row: { original: instance } }) {\n      return (\n        <DatabaseListModules\n          modules={instance?.modules?.map((name) => ({ name }))}\n        />\n      )\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/database.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\n\nimport { DatabaseCell } from '../components/DatabaseCell'\nimport {\n  RedisClusterIds,\n  RedisClusterTitles,\n} from 'uiSrc/pages/redis-cluster/constants/constants'\n\nexport const databaseColumn = (): ColumnDef<InstanceRedisCluster> => {\n  return {\n    header: RedisClusterTitles.Database,\n    id: RedisClusterIds.Name,\n    accessorKey: RedisClusterIds.Name,\n    minSize: 180,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { name },\n      },\n    }) => <DatabaseCell name={name} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/endpoint.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\n\nimport { EndpointCell } from '../components/EndpointCell'\nimport {\n  RedisClusterIds,\n  RedisClusterTitles,\n} from 'uiSrc/pages/redis-cluster/constants/constants'\n\nexport const endpointColumn = (): ColumnDef<InstanceRedisCluster> => {\n  return {\n    header: RedisClusterTitles.Endpoint,\n    id: RedisClusterIds.Endpoint,\n    accessorKey: RedisClusterIds.Endpoint,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { dnsName, port },\n      },\n    }) => <EndpointCell dnsName={dnsName} port={port} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/options.tsx",
    "content": "import React from 'react'\nimport { DatabaseListOptions } from 'uiSrc/components'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport { parseInstanceOptionsCluster } from 'uiSrc/utils'\nimport {\n  RedisClusterIds,\n  RedisClusterTitles,\n} from 'uiSrc/pages/redis-cluster/constants/constants'\n\nexport const optionsColumn = (\n  instances: InstanceRedisCluster[],\n): ColumnDef<InstanceRedisCluster> => {\n  return {\n    header: RedisClusterTitles.Options,\n    id: RedisClusterIds.Options,\n    accessorKey: RedisClusterIds.Options,\n    enableSorting: true,\n    maxSize: 180,\n    cell: ({ row: { original: instance } }) => {\n      const options = parseInstanceOptionsCluster(instance?.uid, instances)\n      return <DatabaseListOptions options={options} />\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/result.tsx",
    "content": "import React from 'react'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\n\nimport { ResultCell } from '../components/ResultCell'\nimport {\n  RedisClusterIds,\n  RedisClusterTitles,\n} from 'uiSrc/pages/redis-cluster/constants/constants'\n\nexport const resultColumn = (): ColumnDef<InstanceRedisCluster> => {\n  return {\n    header: RedisClusterTitles.Result,\n    id: RedisClusterIds.Result,\n    accessorKey: RedisClusterIds.Result,\n    enableSorting: true,\n    cell: ({\n      row: {\n        original: { statusAdded, messageAdded },\n      },\n    }) => <ResultCell statusAdded={statusAdded} messageAdded={messageAdded} />,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/selection.ts",
    "content": "import { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport { getSelectionColumn } from 'uiSrc/pages/autodiscover-cloud/utils'\n\nexport const selectionColumn = () => {\n  return getSelectionColumn<InstanceRedisCluster>()\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/columns/status.ts",
    "content": "import { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport {\n  RedisClusterIds,\n  RedisClusterTitles,\n} from 'uiSrc/pages/redis-cluster/constants/constants'\n\nexport const statusColumn = (): ColumnDef<InstanceRedisCluster> => {\n  return {\n    header: RedisClusterTitles.Status,\n    id: RedisClusterIds.Status,\n    accessorKey: RedisClusterIds.Status,\n    enableSorting: true,\n    size: 100,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/components/DatabaseCell.tsx",
    "content": "import React from 'react'\nimport { RiTooltip } from 'uiSrc/components'\nimport { formatLongName } from 'uiSrc/utils'\nimport { CellText } from 'uiSrc/components/auto-discover'\n\nimport styles from '../../styles.module.scss'\n\nexport interface DatabaseCellProps {\n  name: string\n}\n\nexport const DatabaseCell = ({ name }: DatabaseCellProps) => {\n  const cellContent = (name || '')\n    .substring(0, 200)\n    .replace(/\\s\\s/g, '\\u00a0\\u00a0')\n\n  return (\n    <div role=\"presentation\" data-testid={`db_name_${name}`}>\n      <RiTooltip\n        position=\"bottom\"\n        title=\"Database\"\n        className={styles.tooltipColumnName}\n        anchorClassName=\"truncateText\"\n        content={formatLongName(name || '')}\n      >\n        <CellText>{cellContent}</CellText>\n      </RiTooltip>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/components/EndpointCell.tsx",
    "content": "import React from 'react'\nimport { RiTooltip } from 'uiSrc/components'\nimport { formatLongName } from 'uiSrc/utils'\nimport {\n  CopyPublicEndpointText,\n  CopyTextContainer,\n  CopyBtnWrapper,\n} from 'uiSrc/components/auto-discover'\n\nexport interface EndpointCellProps {\n  dnsName: string\n  port: number\n}\n\nexport const EndpointCell = ({ dnsName, port }: EndpointCellProps) => {\n  if (!dnsName) {\n    return null\n  }\n  const text = `${dnsName}:${port}`\n\n  return (\n    <CopyTextContainer>\n      <RiTooltip\n        position=\"bottom\"\n        title=\"Endpoint\"\n        content={formatLongName(text)}\n      >\n        <CopyPublicEndpointText>{text}</CopyPublicEndpointText>\n      </RiTooltip>\n\n      <CopyBtnWrapper\n        copy={text}\n        aria-label=\"Copy public endpoint\"\n        successLabel=\"\"\n      />\n    </CopyTextContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/components/ResultCell.tsx",
    "content": "import React from 'react'\nimport { type AddRedisDatabaseStatus } from 'uiSrc/slices/interfaces'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nexport interface ResultCellProps {\n  statusAdded: AddRedisDatabaseStatus | undefined\n  messageAdded: string | undefined\n}\n\nexport const ResultCell = ({ statusAdded, messageAdded }: ResultCellProps) => {\n  if (statusAdded === 'success') {\n    return <Text>{messageAdded}</Text>\n  }\n\n  return (\n    <RiTooltip position=\"left\" title=\"Error\" content={messageAdded}>\n      <Row align=\"center\" gap=\"s\">\n        <FlexItem>\n          <RiIcon type=\"ToastDangerIcon\" color=\"danger600\" />\n        </FlexItem>\n\n        <FlexItem>\n          <ColorText color=\"danger\" className=\"flex-row euiTextAlign--center\">\n            Error\n          </ColorText>\n        </FlexItem>\n      </Row>\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/components/index.ts",
    "content": "export * from './DatabaseCell'\nexport * from './EndpointCell'\nexport * from './ResultCell'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/column-definitions/index.ts",
    "content": "export * from './columns/result'\nexport * from './columns/database'\nexport * from './columns/endpoint'\nexport * from './columns/status'\nexport * from './columns/options'\nexport * from './columns/capabilities'\nexport * from './columns/selection'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.style.ts",
    "content": "export default {\n  panelCancelBtn: 'panelCancelBtn',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.tsx",
    "content": "import React from 'react'\n\nimport { RiPopover } from 'uiSrc/components/base'\nimport {\n  DestructiveButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport styles from './CancelButton.style'\n\nimport type { CancelButtonProps } from './CancelButton.types'\n\nexport const CancelButton = ({\n  isPopoverOpen,\n  onShowPopover,\n  onClosePopover,\n  onProceed,\n}: CancelButtonProps) => (\n  <RiPopover\n    anchorPosition=\"upCenter\"\n    isOpen={isPopoverOpen}\n    closePopover={onClosePopover}\n    panelClassName={styles.panelCancelBtn}\n    panelPaddingSize=\"l\"\n    button={\n      <SecondaryButton\n        onClick={onShowPopover}\n        className=\"btn-cancel\"\n        data-testid=\"btn-back\"\n      >\n        Cancel\n      </SecondaryButton>\n    }\n  >\n    <Text size=\"m\">\n      Your changes have not been saved.&#10;&#13; Do you want to proceed to the\n      list of databases?\n    </Text>\n    <br />\n    <div>\n      <DestructiveButton\n        size=\"s\"\n        onClick={onProceed}\n        data-testid=\"btn-back-proceed\"\n      >\n        Proceed\n      </DestructiveButton>\n    </div>\n  </RiPopover>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/components/CancelButton/CancelButton.types.ts",
    "content": "export interface CancelButtonProps {\n  isPopoverOpen: boolean\n  onShowPopover: () => void\n  onClosePopover: () => void\n  onProceed: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.tsx",
    "content": "import React from 'react'\n\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport type { SummaryTextProps } from './SummaryText.types'\n\nexport const SummaryText = ({\n  countSuccessAdded,\n  countFailAdded,\n}: SummaryTextProps) => (\n  <Text>\n    <ColorText variant=\"semiBold\">Summary: </ColorText>\n    {countSuccessAdded ? (\n      <span>\n        Successfully added {countSuccessAdded} database(s)\n        {countFailAdded ? '. ' : '.'}\n      </span>\n    ) : null}\n    {countFailAdded ? (\n      <span>Failed to add {countFailAdded} database(s).</span>\n    ) : null}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/components/SummaryText/SummaryText.types.ts",
    "content": "export interface SummaryTextProps {\n  countSuccessAdded: number\n  countFailAdded: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/components/index.ts",
    "content": "export { CancelButton } from './CancelButton/CancelButton'\nexport { SummaryText } from './SummaryText/SummaryText'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/constants/constants.ts",
    "content": "export enum RedisClusterIds {\n  Capabilities = 'modules',\n  Name = 'name',\n  Endpoint = 'dnsName',\n  Options = 'options',\n  Result = 'messageAdded',\n  Status = 'status',\n}\n\nexport enum RedisClusterTitles {\n  Capabilities = 'Capabilities',\n  Database = 'Database',\n  Endpoint = 'Endpoint',\n  Options = 'Options',\n  Result = 'Result',\n  Status = 'Status',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/index.ts",
    "content": "import RedisClusterDatabasesPage from './RedisClusterDatabasesPage'\n\nexport { RedisClusterDatabasesPage }\n\nexport default RedisClusterDatabasesPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/styles.module.scss",
    "content": "$breakpoint-to-wrap-buttons: 660px;\n\n.footer {\n  padding: 15px 14px 10px 30px !important;\n  @media only screen and (max-width: $breakpoint-to-wrap-buttons) {\n    padding: 15px 14px 10px 14px !important;\n  }\n}\n\n\n.footerClusterDatabases {\n  @media only screen and (max-width: $breakpoint-to-wrap-buttons) {\n    display: flex;\n    flex-wrap: wrap;\n  }\n}\n\n.footerButtonsGroup {\n  @media only screen and (max-width: $breakpoint-to-wrap-buttons) {\n    display: flex;\n    justify-content: space-between;\n    width: 100%;\n    margin-top: 10px;\n  }\n}\n\n.title {\n  padding-bottom: 5px !important;\n}\n\n.searchForm {\n  width: 266px !important;\n}\n\n.panelCancelBtn {\n  max-width: 350px !important;\n  margin-left: -10px;\n}\n\n.tooltipColumnName {\n  max-width: 370px !important;\n  * {\n    line-height: 1.19;\n    font-size: 14px !important;\n\n    &:not(:global(.euiToolTip__title)) {\n      font-weight: 300 !important;\n    }\n  }\n}\n\n.errorBtn {\n  height: 100% !important;\n  > span {\n    padding: 0 !important;\n  }\n}\n\n.tooltipColumnNameText {\n  text-decoration: underline;\n  display: inline-block;\n  width: 100%;\n  overflow: hidden;\n  vertical-align: top;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.noDatabases {\n  color: var(--euiTextSubduedColor) !important;\n  margin: 50px auto 0;\n  text-align: center;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-cluster/useClusterDatabasesConfig.tsx",
    "content": "import { useCallback, useEffect, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  addInstancesRedisCluster,\n  clusterSelector,\n  resetDataRedisCluster,\n  resetInstancesRedisCluster,\n} from 'uiSrc/slices/instances/cluster'\nimport { Maybe, Nullable, setTitle } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { Pages } from 'uiSrc/constants'\nimport { type InstanceRedisCluster } from 'uiSrc/slices/interfaces'\nimport { type ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  capabilitiesColumn,\n  databaseColumn,\n  endpointColumn,\n  optionsColumn,\n  resultColumn,\n  selectionColumn,\n  statusColumn,\n} from './column-definitions'\n\nexport const colFactory = (instances: Nullable<InstanceRedisCluster[]>) => {\n  let columns: ColumnDef<InstanceRedisCluster>[] = [\n    databaseColumn(),\n    statusColumn(),\n    endpointColumn(),\n    capabilitiesColumn(),\n    optionsColumn(instances || []),\n  ]\n  if (instances && instances.length > 0) {\n    columns.unshift(selectionColumn())\n  }\n\n  const columnsResult: ColumnDef<InstanceRedisCluster>[] = [\n    ...columns,\n    resultColumn(),\n  ]\n  // remove selection column from result columns\n  columnsResult.shift()\n  return [columns, columnsResult]\n}\nconst sendCancelEvent = () => {\n  sendEventTelemetry({\n    event:\n      TelemetryEvent.CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_CANCELLED,\n  })\n}\nexport const useClusterDatabasesConfig = () => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n\n  const {\n    credentials,\n    data: instances,\n    dataAdded: instancesAdded,\n    loading,\n  } = useSelector(clusterSelector)\n\n  useEffect(() => {\n    setTitle('Auto-Discover Redis Enterprise Databases')\n  }, [])\n\n  const handleClose = useCallback(\n    (sendEvent = true) => {\n      sendEvent && sendCancelEvent()\n      dispatch(resetDataRedisCluster())\n      history.push(Pages.home)\n    },\n    [dispatch, history],\n  )\n  const handleBackAdding = useCallback(\n    (sendEvent = true) => {\n      sendEvent && sendCancelEvent()\n      dispatch(resetInstancesRedisCluster())\n      history.push(Pages.home)\n    },\n    [dispatch, history],\n  )\n\n  const handleAddInstances = useCallback(\n    (uids: Maybe<number>[]) => {\n      dispatch(addInstancesRedisCluster({ uids, credentials }))\n    },\n    [dispatch],\n  )\n\n  const [columns, columnsResult] = useMemo(\n    () => colFactory(instances),\n    [instances],\n  )\n\n  return {\n    columns,\n    columnsResult,\n    instances,\n    instancesAdded,\n    loading,\n    handleClose,\n    handleBackAdding,\n    handleAddInstances,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/edit-connection/EditConnection.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport EditConnection from './EditConnection'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('EditConnection', () => {\n  it('should render', () => {\n    expect(render(<EditConnection />)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/edit-connection/EditConnection.tsx",
    "content": "import React, { useContext, useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\n\nimport {\n  getApiErrorMessage,\n  isStatusSuccessful,\n  Nullable,\n  setTitle,\n} from 'uiSrc/utils'\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { PageHeader, PagePlaceholder } from 'uiSrc/components'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { setConnectedInstanceId } from 'uiSrc/slices/instances/instances'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { ContentCreateRedis } from 'uiSrc/slices/interfaces/content'\nimport PromoLink from 'uiSrc/components/promo-link/PromoLink'\nimport { getPathToResource } from 'uiSrc/services/resourcesService'\nimport { HELP_LINKS } from 'uiSrc/pages/home/constants'\nimport { sendEventTelemetry } from 'uiSrc/telemetry'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { contentSelector } from 'uiSrc/slices/content/create-redis-buttons'\nimport DatabasePanelDialog from 'uiSrc/pages/home/components/database-panel-dialog'\nimport { Page, PageBody } from 'uiSrc/components/base/layout/page'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\n\nimport './styles.scss'\nimport styles from './styles.module.scss'\n\ninterface IState {\n  loading: boolean\n  error: string\n  data: Nullable<Instance>\n}\nconst DEFAULT_STATE = { loading: true, error: '', data: null }\n\nconst EditConnection = () => {\n  const history = useHistory()\n  const dispatch = useDispatch()\n  const { server } = useSelector(appInfoSelector)\n  const { data: createDbContent } = useSelector(contentSelector)\n  const [state, setState] = useState<IState>(DEFAULT_STATE)\n  const { theme } = useContext(ThemeContext)\n\n  let isApiSubscribed = false\n\n  setTitle('Redis Stack')\n\n  useEffect(() => {\n    getInstanceInfo()\n    return () => {\n      isApiSubscribed = false\n    }\n  }, [])\n\n  const onClose = () => history.goBack()\n\n  const getInstanceInfo = async () => {\n    try {\n      setState(DEFAULT_STATE)\n      isApiSubscribed = true\n      const { data, status } = await apiService.get<Instance>(\n        `${ApiEndpoints.DATABASES}/${server?.fixedDatabaseId}`,\n      )\n      if (isStatusSuccessful(status) && isApiSubscribed) {\n        setState({ ...state, loading: false, data })\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      if (isApiSubscribed) {\n        setState({ ...state, loading: false, error: errorMessage })\n      }\n      dispatch(addErrorNotification(error))\n    }\n  }\n  const onInstanceChanged = () => {\n    if (server?.fixedDatabaseId) {\n      dispatch(setConnectedInstanceId(server.fixedDatabaseId))\n      history.goBack()\n    }\n  }\n\n  const CreateCloudBtn = ({ content }: { content: ContentCreateRedis }) => {\n    const { title, description, styles, links } = content\n\n    // @ts-ignore\n    const linkStyles = styles ? styles[theme] : {}\n    return (\n      <PromoLink\n        title={title}\n        description={description}\n        url={links?.main?.url}\n        testId=\"promo-btn\"\n        styles={{\n          ...linkStyles,\n          backgroundImage: linkStyles?.backgroundImage\n            ? `url(${getPathToResource(linkStyles.backgroundImage)})`\n            : undefined,\n        }}\n        onClick={() =>\n          sendEventTelemetry({\n            event: HELP_LINKS.cloud.event,\n            eventData: { source: 'Redis Stack' },\n          })\n        }\n      />\n    )\n  }\n\n  return state.loading ? (\n    <PagePlaceholder />\n  ) : (\n    <>\n      <PageHeader title=\"Redis Stack\" />\n      <div />\n      <Page className=\"homePage redisStackConnection\">\n        <PageBody component=\"div\" className={styles.container}>\n          {createDbContent?.cloud && (\n            <FlexItem style={{ margin: '20px 0' }}>\n              <CreateCloudBtn content={createDbContent.cloud} />\n            </FlexItem>\n          )}\n          <div className={styles.formContainer}>\n            <div className={styles.form}>\n              <DatabasePanelDialog\n                editMode\n                editedInstance={state.data}\n                onDbEdited={onInstanceChanged}\n                onClose={onClose}\n              />\n            </div>\n          </div>\n        </PageBody>\n      </Page>\n    </>\n  )\n}\n\nexport default EditConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/edit-connection/index.ts",
    "content": "import EditConnection from './EditConnection'\n\nexport default EditConnection\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/edit-connection/styles.module.scss",
    "content": ".container {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center !important;\n  height: 1px;\n  min-height: 100%;\n  padding-bottom: 16px;\n}\n\n.formContainer {\n  position: relative;\n  max-width: 600px;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n}\n\n.form {\n  display: flex;\n  flex: 1;\n  flex-direction: column;\n}\n\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/edit-connection/styles.scss",
    "content": ".redisStackConnection {\n  .container {\n    margin-right: 0;\n    height: 1px;\n    min-height: 100%;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/protected-route/ProtectedRoute.spec.tsx",
    "content": "import React from 'react'\nimport Router from 'uiSrc/Router'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { Pages } from 'uiSrc/constants'\nimport { SettingsPage } from 'uiSrc/pages'\n\nimport ProtectedRoute from './ProtectedRoute'\n\ndescribe('ProtectedRoute', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <Router>\n          <ProtectedRoute>\n            <RouteWithSubRoutes\n              key={1}\n              path={Pages.settings}\n              component={SettingsPage}\n            />\n          </ProtectedRoute>\n        </Router>,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/protected-route/ProtectedRoute.tsx",
    "content": "import React from 'react'\nimport { Redirect, Route } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { Pages } from 'uiSrc/constants'\n\ninterface IProps {\n  children: React.ReactNode\n}\n\nconst ProtectedRoute = ({ children, ...rest }: IProps) => {\n  const { id: connected } = useSelector(connectedInstanceSelector)\n  return (\n    <Route\n      {...rest}\n      render={() => (connected ? children : <Redirect to={Pages.home} />)}\n    />\n  )\n}\n\nexport default ProtectedRoute\n"
  },
  {
    "path": "redisinsight/ui/src/pages/redis-stack/components/protected-route/index.ts",
    "content": "import ProtectedRoute from './ProtectedRoute'\n\nexport default ProtectedRoute\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/SettingsPage.spec.tsx",
    "content": "import React from 'react'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  render,\n  userEvent,\n  screen,\n  toggleAccordion,\n} from 'uiSrc/utils/test-utils'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport SettingsPage from './SettingsPage'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({\n    cloudSso: {\n      flag: false,\n    },\n  }),\n}))\n\n/**\n * SettingsPage tests\n *\n * @group component\n */\ndescribe('SettingsPage', () => {\n  it('should render', () => {\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n\n  it('Accordion \"Appearance\" should render', () => {\n    const { container } = render(<SettingsPage />)\n\n    expect(\n      container.querySelector('[data-test-subj=\"accordion-appearance\"]'),\n    ).toBeInTheDocument()\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n\n  it('Accordion \"Privacy settings\" should render', () => {\n    const { container } = render(<SettingsPage />)\n\n    expect(\n      container.querySelector('[data-test-subj=\"accordion-privacy-settings\"]'),\n    ).toBeInTheDocument()\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n\n  it('Accordion \"Advanced settings\" should render', () => {\n    const { container } = render(<SettingsPage />)\n\n    expect(\n      container.querySelector('[data-test-subj=\"accordion-advanced-settings\"]'),\n    ).toBeInTheDocument()\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n\n  it('Accordion \"Workbench settings\" should render', () => {\n    const { container } = render(<SettingsPage />)\n\n    expect(\n      container.querySelector(\n        '[data-test-subj=\"accordion-workbench-settings\"]',\n      ),\n    ).toBeInTheDocument()\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n\n  it('should not render cloud accordion without feature flag', () => {\n    const { container } = render(<SettingsPage />)\n\n    expect(\n      container.querySelector('[data-test-subj=\"accordion-cloud-settings\"]'),\n    ).not.toBeInTheDocument()\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n\n  it('should render cloud accordion with feature flag', () => {\n    ;(appFeatureFlagsFeaturesSelector as jest.Mock).mockReturnValue({\n      cloudSso: {\n        flag: true,\n      },\n    })\n    const { container } = render(<SettingsPage />)\n\n    expect(\n      container.querySelector('[data-test-subj=\"accordion-cloud-settings\"]'),\n    ).toBeInTheDocument()\n    expect(render(<SettingsPage />)).toBeTruthy()\n  })\n})\n\ndescribe('Telemetry', () => {\n  it('change Cleanup setting', async () => {\n    const sendEventTelemetryMock = jest.fn()\n\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    sendEventTelemetry.mockReset()\n\n    render(<SettingsPage />)\n    await toggleAccordion('accordion-workbench-settings')\n    await userEvent.click(screen.getByTestId('switch-workbench-cleanup'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED,\n      eventData: {\n        currentValue: true,\n        newValue: false,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/SettingsPage.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport SettingsPage from './SettingsPage'\n\nconst meta: Meta<typeof SettingsPage> = {\n  component: SettingsPage,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/SettingsPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport cx from 'classnames'\n\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { setTitle } from 'uiSrc/utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { useDebouncedEffect } from 'uiSrc/services'\nimport {\n  ConsentsNotifications,\n  ConsentsPrivacy,\n  FeatureFlagComponent,\n} from 'uiSrc/components'\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport {\n  fetchUserConfigSettings,\n  fetchUserSettingsSpec,\n  userSettingsSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport Divider from 'uiSrc/components/divider/Divider'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  Page,\n  PageBody,\n  PageContentBody,\n  PageHeader,\n} from 'uiSrc/components/base/layout/page'\nimport { CallOut } from 'uiSrc/components/base/display/call-out/CallOut'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Loader, RICollapsibleNavGroup } from 'uiSrc/components/base/display'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport {\n  AdvancedSettings,\n  AppVersion,\n  CloudSettings,\n  ThemeSettings,\n  WorkbenchSettings,\n} from './components'\nimport { DateTimeFormatter } from './components/general-settings'\nimport styles from './styles.module.scss'\n\nconst SettingsPage = () => {\n  const [loading, setLoading] = useState(false)\n  const { loading: settingsLoading } = useSelector(userSettingsSelector)\n\n  const initialOpenSection = globalThis.location.hash || ''\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    // componentDidMount\n    // fetch config settings, after that take spec\n    dispatch(fetchUserConfigSettings(() => dispatch(fetchUserSettingsSpec())))\n  }, [])\n\n  useEffect(() => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.SETTINGS_PAGE,\n    })\n  }, [])\n\n  useDebouncedEffect(() => setLoading(settingsLoading), 100, [settingsLoading])\n  setTitle('Settings')\n\n  const Appearance = () => (\n    <>\n      <ThemeSettings />\n      <ConsentsNotifications />\n      <Divider />\n      <Spacer />\n      <DateTimeFormatter />\n    </>\n  )\n\n  const PrivacySettings = () => (\n    <div>\n      {loading && (\n        <div className={styles.cover}>\n          <Loader size=\"xl\" />\n        </div>\n      )}\n      <ConsentsPrivacy />\n    </div>\n  )\n\n  const WorkbenchSettingsGroup = () => (\n    <div>\n      {loading && (\n        <div className={styles.cover}>\n          <Loader size=\"xl\" />\n        </div>\n      )}\n      <WorkbenchSettings />\n    </div>\n  )\n\n  const CloudSettingsGroup = () => (\n    <div>\n      {loading && (\n        <div className={styles.cover}>\n          <Loader size=\"xl\" />\n        </div>\n      )}\n      <CloudSettings />\n    </div>\n  )\n\n  const AdvancedSettingsGroup = () => (\n    <div>\n      {loading && (\n        <div className={styles.cover}>\n          <Loader size=\"xl\" />\n        </div>\n      )}\n      <CallOut className={styles.warning}>\n        <Text size=\"s\" className={styles.smallText}>\n          Advanced settings should only be changed if you understand their\n          impact.\n        </Text>\n      </CallOut>\n      <AdvancedSettings />\n    </div>\n  )\n\n  return (\n    <Page className={styles.container}>\n      <PageBody component=\"div\">\n        <PageHeader>\n          <Title size=\"XXL\" className={styles.title}>\n            Settings\n          </Title>\n        </PageHeader>\n\n        <PageContentBody style={{ maxWidth: 792 }}>\n          <Col gap=\"s\">\n            <RICollapsibleNavGroup\n              isCollapsible\n              className={styles.accordion}\n              title=\"General\"\n              initialIsOpen={initialOpenSection === '#general'}\n              data-test-subj=\"accordion-appearance\"\n            >\n              {Appearance()}\n            </RICollapsibleNavGroup>{' '}\n            <RICollapsibleNavGroup\n              isCollapsible\n              className={styles.accordion}\n              title=\"Privacy\"\n              initialIsOpen={initialOpenSection === '#privacy'}\n              data-test-subj=\"accordion-privacy-settings\"\n            >\n              {PrivacySettings()}\n            </RICollapsibleNavGroup>\n            <RICollapsibleNavGroup\n              isCollapsible\n              className={styles.accordion}\n              title=\"Workbench\"\n              initialIsOpen={initialOpenSection === '#workbench'}\n              data-test-subj=\"accordion-workbench-settings\"\n              data-testid=\"accordion-workbench-settings\"\n              id=\"accordion-workbench-settings\"\n            >\n              {WorkbenchSettingsGroup()}\n            </RICollapsibleNavGroup>\n            <FeatureFlagComponent name={FeatureFlags.cloudSso}>\n              <RICollapsibleNavGroup\n                isCollapsible\n                className={cx(styles.accordion, styles.accordionWithSubTitle)}\n                title=\"Redis Cloud\"\n                initialIsOpen={initialOpenSection === '#cloud'}\n                data-test-subj=\"accordion-cloud-settings\"\n              >\n                {CloudSettingsGroup()}\n              </RICollapsibleNavGroup>\n            </FeatureFlagComponent>\n            <RICollapsibleNavGroup\n              isCollapsible\n              className={cx(styles.accordion, styles.accordionWithSubTitle)}\n              title=\"Advanced\"\n              initialIsOpen={initialOpenSection === '#advanced'}\n              data-test-subj=\"accordion-advanced-settings\"\n            >\n              {AdvancedSettingsGroup()}\n            </RICollapsibleNavGroup>\n          </Col>\n          <AppVersion />\n        </PageContentBody>\n      </PageBody>\n    </Page>\n  )\n}\n\nexport default SettingsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport AdvancedSettings from './AdvancedSettings'\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsSelector: jest.fn().mockReturnValue({\n    config: {\n      scanThreshold: 10000,\n      batchSize: 5,\n    },\n  }),\n  updateUserConfigSettingsAction: () => jest.fn,\n}))\n\ndescribe('AdvancedSettings', () => {\n  it('should render', () => {\n    expect(render(<AdvancedSettings />)).toBeTruthy()\n  })\n\n  it('should Keys-to-scan-value render ', () => {\n    render(<AdvancedSettings />)\n\n    expect(screen.getByTestId(/keys-to-scan-value/)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { validateCountNumber } from 'uiSrc/utils'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { SettingItem } from 'uiSrc/components'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsConfigSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nconst AdvancedSettings = () => {\n  const { scanThreshold = '' } = useSelector(userSettingsConfigSelector) ?? {}\n\n  const dispatch = useDispatch()\n\n  const handleApplyKeysToScanChanges = (value: string) => {\n    // eslint-disable-next-line no-nested-ternary\n    const data = value\n      ? +value < SCAN_COUNT_DEFAULT\n        ? SCAN_COUNT_DEFAULT\n        : +value\n      : null\n\n    dispatch(updateUserConfigSettingsAction({ scanThreshold: data }))\n  }\n\n  return (\n    <>\n      <SettingItem\n        initValue={scanThreshold.toString()}\n        onApply={handleApplyKeysToScanChanges}\n        validation={validateCountNumber}\n        title=\"Keys to Scan in List view\"\n        summary=\"Sets the amount of keys to scan per one iteration. Filtering by pattern per a large number of keys may decrease performance.\"\n        testid=\"keys-to-scan\"\n        placeholder=\"10 000\"\n        label=\"Keys to Scan:\"\n      />\n      <Spacer size=\"m\" />\n    </>\n  )\n}\n\nexport default AdvancedSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/advanced-settings/index.ts",
    "content": "import AdvancedSettings from './AdvancedSettings'\n\nexport default AdvancedSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/app-version/AppVersion.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { appServerInfoSelector } from 'uiSrc/slices/app/info'\nimport AppVersion from './AppVersion'\n\njest.mock('uiSrc/slices/app/info', () => ({\n  ...jest.requireActual('uiSrc/slices/app/info'),\n  appServerInfoSelector: jest.fn().mockReturnValue(null),\n}))\n\ndescribe('AppVersion', () => {\n  it('should render app version when available', () => {\n    const mockVersion = faker.system.semver()\n    ;(appServerInfoSelector as jest.Mock).mockReturnValue({\n      appVersion: mockVersion,\n    })\n\n    render(<AppVersion />)\n\n    expect(screen.getByTestId('settings-app-version')).toHaveTextContent(\n      `Redis Insight v${mockVersion}`,\n    )\n  })\n\n  it('should not render when server info is null', () => {\n    ;(appServerInfoSelector as jest.Mock).mockReturnValue(null)\n\n    render(<AppVersion />)\n\n    expect(screen.queryByTestId('settings-app-version')).not.toBeInTheDocument()\n  })\n\n  it('should not render when appVersion is missing', () => {\n    ;(appServerInfoSelector as jest.Mock).mockReturnValue({})\n\n    render(<AppVersion />)\n\n    expect(screen.queryByTestId('settings-app-version')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/app-version/AppVersion.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { appServerInfoSelector } from 'uiSrc/slices/app/info'\nimport { Text } from 'uiSrc/components/base/text'\n\nconst AppVersion = () => {\n  const server = useSelector(appServerInfoSelector)\n\n  if (!server?.appVersion) return null\n\n  return (\n    <Text\n      size=\"s\"\n      color=\"subdued\"\n      data-testid=\"settings-app-version\"\n      style={{ marginTop: 16, textAlign: 'center' }}\n    >\n      Redis Insight v{server.appVersion}\n    </Text>\n  )\n}\n\nexport default AppVersion\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/app-version/index.ts",
    "content": "import AppVersion from './AppVersion'\n\nexport default AppVersion\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/CloudSettings.spec.tsx",
    "content": "import React from 'react'\nimport {\n  act,\n  cleanup,\n  createMockedStore,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  userEvent,\n  waitForRiPopoverVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  getCapiKeys,\n  getCapiKeysSuccess,\n  oauthCapiKeysSelector,\n  removeAllCapiKeys,\n} from 'uiSrc/slices/oauth/cloud'\nimport { apiService } from 'uiSrc/services'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAUTH_CLOUD_CAPI_KEYS_DATA } from 'uiSrc/mocks/data/oauth'\n\nimport CloudSettings from './CloudSettings'\n\njest.mock('uiSrc/slices/oauth/cloud', () => ({\n  ...jest.requireActual('uiSrc/slices/oauth/cloud'),\n  oauthCapiKeysSelector: jest.fn().mockReturnValue({\n    data: null,\n    loading: false,\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = createMockedStore()\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst renderCloudSettings = () => {\n  return render(<CloudSettings />, { store })\n}\n\ndescribe('CloudSettings', () => {\n  it('should show delete popover and call proper action on delete', async () => {\n    ;(oauthCapiKeysSelector as jest.Mock).mockReturnValue({\n      data: OAUTH_CLOUD_CAPI_KEYS_DATA,\n      loading: false,\n    })\n    renderCloudSettings()\n\n    await userEvent.click(screen.getByTestId('delete-key-btn'))\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(screen.getByTestId('delete-key-confirm-btn'))\n\n    expect(store.getActions()).toEqual([\n      getCapiKeys(),\n      getCapiKeysSuccess(OAUTH_CLOUD_CAPI_KEYS_DATA),\n      removeAllCapiKeys(),\n    ])\n  })\n\n  it('should render', () => {\n    expect(renderCloudSettings()).toBeTruthy()\n  })\n\n  it('should get api keys after render', () => {\n    renderCloudSettings()\n\n    expect(store.getActions()).toEqual([getCapiKeys()])\n  })\n\n  it('should be disabled delete all button', () => {\n    ;(oauthCapiKeysSelector as jest.Mock).mockReturnValue({\n      data: [],\n      loading: false,\n    })\n\n    renderCloudSettings()\n\n    expect(screen.getByTestId('delete-key-btn')).toBeDisabled()\n  })\n\n  it('should call proper telemetry events', async () => {\n    apiService.delete = jest.fn().mockResolvedValueOnce({ status: 200 })\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n    ;(oauthCapiKeysSelector as jest.Mock).mockReturnValue({\n      data: OAUTH_CLOUD_CAPI_KEYS_DATA,\n      loading: false,\n    })\n    renderCloudSettings()\n\n    fireEvent.click(screen.getByTestId('delete-key-btn'))\n    await waitForRiPopoverVisible()\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEYS_REMOVE_CLICKED,\n    })\n\n    sendEventTelemetry.mockRestore()\n\n    await act(() => {\n      fireEvent.click(screen.getByTestId('delete-key-confirm-btn'))\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEYS_REMOVED,\n    })\n\n    sendEventTelemetry.mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/CloudSettings.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport {\n  getCapiKeysAction,\n  oauthCapiKeysSelector,\n  removeAllCapiKeysAction,\n} from 'uiSrc/slices/oauth/cloud'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  DestructiveButton,\n  PrimaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { EXTERNAL_LINKS, UTM_MEDIUMS } from 'uiSrc/constants/links'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport UserApiKeysTable from './components/user-api-keys-table'\n\nimport styles from './styles.module.scss'\n\nconst CloudSettings = () => {\n  const { loading, data } = useSelector(oauthCapiKeysSelector)\n  const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(getCapiKeysAction())\n  }, [])\n\n  const handleClickDelete = () => {\n    setIsDeleteOpen(true)\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEYS_REMOVE_CLICKED,\n    })\n  }\n\n  const handleDeleteAllKeys = () => {\n    setIsDeleteOpen(false)\n    dispatch(\n      removeAllCapiKeysAction(() => {\n        sendEventTelemetry({\n          event: TelemetryEvent.SETTINGS_CLOUD_API_KEYS_REMOVED,\n        })\n      }),\n    )\n  }\n\n  return (\n    <div className={styles.container}>\n      <Title className={styles.title} size=\"XS\">\n        API user keys\n      </Title>\n      <Spacer size=\"s\" />\n      <Row gap=\"m\" responsive>\n        <FlexItem grow>\n          <Text size=\"m\" className={styles.smallText} color=\"primary\">\n            The list of API user keys that are stored locally in Redis Insight.\n          </Text>\n          <Spacer size=\"xs\" />\n          <Text size=\"m\" className={styles.smallText} color=\"primary\">\n            API user keys grant programmatic access to Redis Cloud.\n          </Text>\n          <Text size=\"m\" className={styles.smallText} color=\"primary\">\n            To delete API keys from Redis Cloud,\n            <Link\n              color=\"primary\"\n              target=\"_blank\"\n              href={getUtmExternalLink(EXTERNAL_LINKS.redisEnterpriseCloud, {\n                medium: UTM_MEDIUMS.Settings,\n                campaign: 'clear_keys',\n              })}\n            >\n              sign in to Redis Cloud\n            </Link>\n            and delete them manually.\n          </Text>\n        </FlexItem>\n        <FlexItem grow={false}>\n          <RiPopover\n            anchorPosition=\"downCenter\"\n            ownFocus\n            isOpen={isDeleteOpen}\n            closePopover={() => setIsDeleteOpen(false)}\n            panelPaddingSize=\"l\"\n            panelClassName={styles.deletePopover}\n            button={\n              <PrimaryButton\n                size=\"small\"\n                onClick={handleClickDelete}\n                disabled={loading || !data?.length}\n                data-testid=\"delete-key-btn\"\n              >\n                Remove all API keys\n              </PrimaryButton>\n            }\n          >\n            <div className={styles.popoverDeleteContainer}>\n              <Text size=\"m\" component=\"div\">\n                <h4>All API user keys will be removed from Redis Insight.</h4>\n                {'To delete API keys from Redis Cloud, '}\n                <Link\n                  target=\"_blank\"\n                  color=\"text\"\n                  tabIndex={-1}\n                  href=\"https://redis.io/redis-enterprise-cloud/overview/?utm_source=redisinsight&utm_medium=settings&utm_campaign=clear_keys\"\n                >\n                  sign in to Redis Cloud\n                </Link>\n                {' and delete them manually.'}\n              </Text>\n              <Spacer />\n              <div className={styles.popoverFooter}>\n                <DestructiveButton\n                  size=\"small\"\n                  icon={DeleteIcon}\n                  onClick={handleDeleteAllKeys}\n                  className={styles.popoverDeleteBtn}\n                  data-testid=\"delete-key-confirm-btn\"\n                >\n                  Remove all API keys\n                </DestructiveButton>\n              </div>\n            </div>\n          </RiPopover>\n        </FlexItem>\n      </Row>\n      <Spacer />\n      <UserApiKeysTable items={data} loading={loading} />\n    </div>\n  )\n}\n\nexport default CloudSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/UserApiKeysTable.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  fireEvent,\n  waitForRiPopoverVisible,\n  act,\n} from 'uiSrc/utils/test-utils'\n\nimport { removeCapiKey } from 'uiSrc/slices/oauth/cloud'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { apiService } from 'uiSrc/services'\nimport UserApiKeysTable, { Props } from './UserApiKeysTable'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockedCapiKeys = [\n  {\n    id: '1',\n    name: 'RedisInsight-f4868252-a128-4a02-af75-bd3c99898267-2020-11-01T-123',\n    createdAt: '2023-08-02T09:07:41.680Z',\n    lastUsed: '2023-08-02T09:07:41.680Z',\n    valid: true,\n  },\n  {\n    id: '2',\n    name: 'RedisInsight-dawdaw68252-a128-4a02-af75-bd3c99898267-2020-11-01T-123',\n    createdAt: '2023-08-02T09:07:41.680Z',\n    lastUsed: '2023-08-02T09:07:41.680Z',\n    valid: false,\n  },\n  {\n    id: '3',\n    name: 'RedisInsight-d4543wdaw68252-a128-4a02-af75-bd3c99898267-2020-11-01T-123',\n    createdAt: '2023-08-02T09:07:41.680Z',\n    lastUsed: '2023-08-02T09:07:41.680Z',\n    valid: false,\n  },\n]\n\ndescribe('UserApiKeysTable', () => {\n  it('should render', () => {\n    expect(render(<UserApiKeysTable {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should render message when there are no keys', () => {\n    render(<UserApiKeysTable {...mockedProps} items={[]} />)\n\n    expect(screen.getByTestId('no-api-keys-message')).toBeInTheDocument()\n  })\n\n  it('should render row content properly', () => {\n    render(<UserApiKeysTable {...mockedProps} items={mockedCapiKeys} />)\n\n    expect(screen.getByText(mockedCapiKeys[0].name)).toBeVisible()\n  })\n\n  it('should show delete popover and call proper action on delete', async () => {\n    render(<UserApiKeysTable {...mockedProps} items={mockedCapiKeys} />)\n\n    fireEvent.click(\n      screen.getByTestId(`remove-key-button-${mockedCapiKeys[0].name}-icon`),\n    )\n    await waitForRiPopoverVisible()\n\n    fireEvent.click(\n      screen.getByTestId(`remove-key-button-${mockedCapiKeys[0].name}`),\n    )\n\n    expect(store.getActions()).toEqual([removeCapiKey()])\n  })\n\n  it('should call proper telemetry events', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    apiService.delete = jest.fn().mockResolvedValue({ status: 200 })\n\n    render(<UserApiKeysTable {...mockedProps} items={mockedCapiKeys} />)\n\n    sendEventTelemetry.mockRestore()\n\n    fireEvent.click(\n      screen.getByTestId(`remove-key-button-${mockedCapiKeys[0].name}-icon`),\n    )\n    await waitForRiPopoverVisible()\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEY_REMOVE_CLICKED,\n      eventData: {\n        source: OAuthSocialSource.SettingsPage,\n      },\n    })\n\n    sendEventTelemetry.mockRestore()\n\n    await act(() => {\n      fireEvent.click(\n        screen.getByTestId(`remove-key-button-${mockedCapiKeys[0].name}`),\n      )\n    })\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.CLOUD_API_KEY_REMOVED,\n      eventData: {\n        source: OAuthSocialSource.SettingsPage,\n      },\n    })\n\n    sendEventTelemetry.mockRestore()\n\n    fireEvent.click(\n      screen.getByTestId(`copy-api-key-${mockedCapiKeys[0].name}-btn`),\n    )\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEY_NAME_COPIED,\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/UserApiKeysTable.tsx",
    "content": "import React, { useCallback, useState } from 'react'\nimport { format } from 'date-fns'\nimport { useDispatch } from 'react-redux'\nimport { isNull } from 'lodash'\n\nimport { formatLongName, Nullable } from 'uiSrc/utils'\nimport PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { OAuthSsoHandlerDialog, RiTooltip } from 'uiSrc/components'\nimport {\n  CloudCapiKey,\n  OAuthSocialAction,\n  OAuthSocialSource,\n} from 'uiSrc/slices/interfaces'\nimport { removeCapiKeyAction } from 'uiSrc/slices/oauth/cloud'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { EmptyButton, PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Table, ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport styles from './styles.module.scss'\n\nexport interface Props {\n  items: Nullable<CloudCapiKey[]>\n  loading: boolean\n}\n\nconst UserApiKeysTable = ({ items, loading }: Props) => {\n  const [deleting, setDeleting] = useState('')\n  const dispatch = useDispatch()\n\n  const handleCopy = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEY_NAME_COPIED,\n    })\n  }\n\n  const showPopover = useCallback((id = '') => {\n    setDeleting(id)\n  }, [])\n\n  const handleClickDeleteApiKey = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_CLOUD_API_KEY_REMOVE_CLICKED,\n      eventData: {\n        source: OAuthSocialSource.SettingsPage,\n      },\n    })\n  }\n\n  const handleDeleteApiKey = (id: string, name: string) => {\n    setDeleting('')\n    dispatch(\n      removeCapiKeyAction({ id, name }, () => {\n        sendEventTelemetry({\n          event: TelemetryEvent.CLOUD_API_KEY_REMOVED,\n          eventData: {\n            source: OAuthSocialSource.SettingsPage,\n          },\n        })\n      }),\n    )\n  }\n\n  const columns: ColumnDef<CloudCapiKey>[] = [\n    {\n      header: 'API Key Name',\n      id: 'name',\n      accessorKey: 'name',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { name, valid },\n        },\n      }) => {\n        const tooltipContent = formatLongName(name)\n        return (\n          <div className={styles.nameField}>\n            {!valid && (\n              <RiTooltip\n                content=\"This API key is invalid. Remove it from Redis Cloud and create a new one instead.\"\n                anchorClassName={styles.invalidIconAnchor}\n              >\n                <RiIcon\n                  type=\"ToastDangerIcon\"\n                  color=\"danger600\"\n                  className={styles.invalidIcon}\n                />\n              </RiTooltip>\n            )}\n            <RiTooltip title=\"API Key Name\" content={tooltipContent}>\n              <>{name}</>\n            </RiTooltip>\n          </div>\n        )\n      },\n    },\n    {\n      header: 'Created',\n      id: 'createdAt',\n      accessorKey: 'createdAt',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { createdAt },\n        },\n      }) => (\n        <RiTooltip content={format(new Date(createdAt), 'HH:mm:ss d LLL yyyy')}>\n          <>{format(new Date(createdAt), 'd MMM yyyy')}</>\n        </RiTooltip>\n      ),\n    },\n    {\n      header: 'Last used',\n      id: 'lastUsed',\n      accessorKey: 'lastUsed',\n      enableSorting: true,\n      cell: ({\n        row: {\n          original: { lastUsed },\n        },\n      }) => (\n        <>\n          {lastUsed ? (\n            <RiTooltip\n              content={format(new Date(lastUsed), 'HH:mm:ss d LLL yyyy')}\n            >\n              <>{format(new Date(lastUsed), 'd MMM yyyy')}</>\n            </RiTooltip>\n          ) : (\n            'Never'\n          )}\n        </>\n      ),\n    },\n    {\n      header: '',\n      id: 'actions',\n      accessorKey: 'id',\n      cell: ({\n        row: {\n          original: { id, name },\n        },\n      }) => (\n        <Row align=\"center\" justify=\"start\" grow={false} gap=\"s\">\n          <CopyButton\n            copy={name || ''}\n            onCopy={handleCopy}\n            aria-label=\"Copy API key\"\n            successLabel=\"\"\n            tooltipConfig={{\n              content: 'Copy API Key Name',\n            }}\n            data-testid={`copy-api-key-${name}`}\n          />\n          <PopoverDelete\n            header={\n              <>\n                {formatLongName(name)} <br /> will be removed from Redis\n                Insight.\n              </>\n            }\n            text={\n              <>\n                {'To delete this API key from Redis Cloud, '}\n                <Link\n                  target=\"_blank\"\n                  variant=\"inline\"\n                  color=\"text\"\n                  tabIndex={-1}\n                  href=\"https://redis.io/redis-enterprise-cloud/overview/?utm_source=redisinsight&utm_medium=settings&utm_campaign=clear_keys\"\n                >\n                  sign in to Redis Cloud\n                </Link>\n                {' and delete it manually.'}\n              </>\n            }\n            item={id}\n            suffix=\"\"\n            deleting={deleting}\n            closePopover={() => setDeleting('')}\n            updateLoading={loading}\n            showPopover={showPopover}\n            testid={`remove-key-button-${name}`}\n            handleDeleteItem={() => handleDeleteApiKey(id, name)}\n            handleButtonClick={handleClickDeleteApiKey}\n          />\n        </Row>\n      ),\n    },\n  ]\n\n  if (isNull(items)) return null\n\n  if (!items?.length) {\n    return (\n      <>\n        <div className={styles.noKeysMessage} data-testid=\"no-api-keys-message\">\n          <Row align=\"center\">\n            <RiIcon\n              className={styles.starsIcon}\n              type=\"StarsIcon\"\n              color=\"attention300\"\n            />\n            <Title size=\"XS\">The ultimate Redis starting point</Title>\n          </Row>\n          <Spacer size=\"s\" />\n          <Text size=\"s\" className={styles.smallText} color=\"primary\">\n            Cloud API keys will be created and stored when you connect to Redis\n            Cloud to create a free Redis Cloud database or autodiscover your\n            Cloud database.\n          </Text>\n          <Spacer />\n          <div className={styles.actions}>\n            <OAuthSsoHandlerDialog>\n              {(socialCloudHandlerClick) => (\n                <EmptyButton\n                  size=\"small\"\n                  color=\"ghost\"\n                  className={styles.autodiscoverBtn}\n                  onClick={(e: React.MouseEvent) =>\n                    socialCloudHandlerClick(e, {\n                      source: OAuthSocialSource.SettingsPage,\n                      action: OAuthSocialAction.Import,\n                    })\n                  }\n                  data-testid=\"autodiscover-btn\"\n                >\n                  Autodiscover\n                </EmptyButton>\n              )}\n            </OAuthSsoHandlerDialog>\n            <OAuthSsoHandlerDialog>\n              {(ssoCloudHandlerClick) => (\n                <PrimaryButton\n                  size=\"small\"\n                  onClick={(e: React.MouseEvent) =>\n                    ssoCloudHandlerClick(e, {\n                      source: OAuthSocialSource.SettingsPage,\n                      action: OAuthSocialAction.Create,\n                    })\n                  }\n                  data-testid=\"create-cloud-db-btn\"\n                >\n                  Create Redis Cloud database\n                </PrimaryButton>\n              )}\n            </OAuthSsoHandlerDialog>\n          </div>\n        </div>\n        <Spacer />\n      </>\n    )\n  }\n\n  return (\n    <Table\n      columns={columns}\n      data={items}\n      defaultSorting={[\n        {\n          id: 'createdAt',\n          desc: true,\n        },\n      ]}\n      data-testid=\"api-keys-table\"\n    />\n  )\n}\n\nexport default UserApiKeysTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/index.ts",
    "content": "import UserApiKeysTable from './UserApiKeysTable'\n\nexport default UserApiKeysTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/components/user-api-keys-table/styles.module.scss",
    "content": ".invalidIconAnchor {\n  flex-shrink: 0;\n}\n\n.invalidIcon {\n  width: 14px !important;\n  height: 14px !important;\n\n  margin-right: 4px;\n  margin-bottom: 1px;\n}\n\n.nameField {\n  display: flex;\n  align-items: center;\n\n  width: 100%;\n}\n\n.noKeysMessage {\n  padding: 18px 30px;\n\n  border: 1px solid var(--separatorColorLight);\n  border-radius: 4px;\n}\n\n.starsIcon {\n  width: 24px !important;\n  height: 24px !important;\n  margin-right: 8px;\n}\n\n.actions {\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n}\n\n.autodiscoverBtn {\n  font-size: 13px !important;\n  margin-right: 8px;\n}\n\n.copyBtnAnchor {\n  display: inline !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/index.ts",
    "content": "import CloudSettings from './CloudSettings'\n\nexport default CloudSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/cloud-settings/styles.module.scss",
    "content": ".title {\n  font-size:16px !important;\n}\n\n.deletePopover {\n  max-width: 420px !important;\n}\n\n.popoverFooter {\n  display: flex;\n  justify-content: flex-end;\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/DateTimeFormatter.spec.tsx",
    "content": "import React from 'react'\nimport { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { dateTimeOptions } from 'uiSrc/constants'\nimport { Nullable } from 'uiSrc/utils'\nimport DateTimeFormatter from './DateTimeFormatter'\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsConfigSelector: jest.fn().mockReturnValue({\n    dateFormat: null,\n    timezone: null,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('DateTimeFormatter', () => {\n  it('should render', () => {\n    expect(render(<DateTimeFormatter />)).toBeTruthy()\n  })\n\n  it('should not show custom btn and input unless custom radio btn is clicked ', async () => {\n    const { container } = render(<DateTimeFormatter />)\n    expect(screen.getByText('Pre-selected formats')).toBeInTheDocument()\n    expect(\n      container.querySelector('[data-test-subj=\"select-datetime\"]'),\n    ).toBeTruthy()\n    expect(screen.queryByTestId('datetime-custom-btn')).not.toBeInTheDocument()\n    fireEvent.click(screen.getByText('Custom'))\n    expect(screen.queryByTestId('datetime-custom-btn')).toBeInTheDocument()\n  })\n\n  it('should display invalid format when wrong format is typed in a custom input', async () => {\n    const { getByDisplayValue } = render(<DateTimeFormatter />)\n\n    await act(() => fireEvent.click(screen.getByText('Custom')))\n    const customInput: Nullable<HTMLElement> = screen.getByTestId(\n      'custom-datetime-input',\n    )\n\n    await act(async () =>\n      fireEvent.change(customInput, { target: { value: 'fffffinvalid' } }),\n    )\n\n    expect(getByDisplayValue('Invalid Format')).toBeInTheDocument()\n  })\n\n  it('should call proper telemetry events', async () => {\n    render(<DateTimeFormatter />)\n\n    await act(async () => fireEvent.click(screen.getByText('Custom')))\n    const customInput: Nullable<HTMLElement> = screen.getByTestId(\n      'custom-datetime-input',\n    )\n\n    await act(async () =>\n      fireEvent.change(customInput, {\n        target: { value: dateTimeOptions[1].value },\n      }),\n    )\n    await act(() => fireEvent.click(screen.getByTestId('datetime-custom-btn')))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n      eventData: {\n        currentFormat: dateTimeOptions[1].value,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/DateTimeFormatter.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport DateTimeFormatter from './DateTimeFormatter'\n\nconst meta: Meta<typeof DateTimeFormatter> = {\n  component: DateTimeFormatter,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/DateTimeFormatter.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { formatTimestamp } from 'uiSrc/utils'\nimport { DATETIME_FORMATTER_DEFAULT, TimezoneOption } from 'uiSrc/constants'\nimport { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport TimezoneForm from './components/timezone-form/TimezoneForm'\nimport DatetimeForm from './components/datetime-form/DatetimeForm'\nimport { TextInput } from 'uiSrc/components/base/inputs'\n\nconst DateTimeFormatter = () => {\n  const [preview, setPreview] = useState('')\n  const config = useSelector(userSettingsConfigSelector)\n\n  useEffect(() => {\n    setPreview(\n      formatTimestamp(\n        new Date(),\n        config?.dateFormat || DATETIME_FORMATTER_DEFAULT,\n        config?.timezone || TimezoneOption.Local,\n      ),\n    )\n  }, [config?.dateFormat, config?.timezone])\n\n  return (\n    <>\n      <Title size=\"M\">Date and Time Format</Title>\n      <Spacer size=\"m\" />\n      <Text color=\"primary\">\n        Specifies the date and time format to be used in Redis Insight:\n      </Text>\n      <Spacer size=\"m\" />\n      <DatetimeForm onFormatChange={(newPreview) => setPreview(newPreview)} />\n      <Spacer size=\"m\" />\n      <Text color=\"primary\">\n        Specifies the time zone to be used in Redis Insight:\n      </Text>\n      <Spacer size=\"s\" />\n      <Row align=\"center\" justify=\"between\" gap=\"m\">\n        <FlexItem grow={1}>\n          <TimezoneForm />\n        </FlexItem>\n        <Row align=\"center\" gap=\"m\" grow={false}>\n          <Text color=\"primary\" size=\"m\">\n            Preview:\n          </Text>\n          <TextInput\n            variant=\"outline\"\n            value={preview}\n            disabled\n            data-testid=\"data-preview\"\n          />\n        </Row>\n      </Row>\n    </>\n  )\n}\n\nexport default DateTimeFormatter\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/components/datetime-form/DatetimeForm.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { dateTimeOptions } from 'uiSrc/constants'\nimport { Nullable } from 'uiSrc/utils'\nimport DatetimeForm, { Props } from './DatetimeForm'\n\nconst mockedProps = mock<Props>()\n\njest.mock('uiSrc/slices/user/user-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/user/user-settings'),\n  userSettingsConfigSelector: jest.fn().mockReturnValue({\n    dateFormat: null,\n    timezone: null,\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('DatetimeForm', () => {\n  it('should render', () => {\n    expect(render(<DatetimeForm {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should not show custom btn and input unless custom radio btn is clicked ', async () => {\n    const { container } = render(<DatetimeForm {...instance(mockedProps)} />)\n    expect(screen.getByText('Pre-selected formats')).toBeTruthy()\n    expect(\n      container.querySelector('[data-test-subj=\"select-datetime\"]'),\n    ).toBeTruthy()\n    expect(screen.queryByTestId('datetime-custom-btn')).toBeFalsy()\n    await act(() => fireEvent.click(screen.getByText('Custom')))\n    expect(screen.queryByTestId('datetime-custom-btn')).toBeTruthy()\n  })\n\n  it('should display invalid format when wrong format is typed in a custom input', async () => {\n    const onFormatChange = jest.fn()\n    render(\n      <DatetimeForm\n        {...instance(mockedProps)}\n        onFormatChange={onFormatChange}\n      />,\n    )\n\n    await act(() => fireEvent.click(screen.getByText('Custom')))\n    const customInput: Nullable<HTMLElement> = screen.getByTestId(\n      'custom-datetime-input',\n    )\n\n    await act(() =>\n      fireEvent.change(customInput, { target: { value: 'fffffinvalid' } }),\n    )\n\n    expect(onFormatChange).toBeCalledWith('Invalid Format')\n  })\n\n  it('should call proper telemetry events when custom format is saved', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<DatetimeForm {...instance(mockedProps)} />)\n\n    await act(() => fireEvent.click(screen.getByText('Custom')))\n    const customInput: Nullable<HTMLElement> = screen.getByTestId(\n      'custom-datetime-input',\n    )\n\n    await act(() =>\n      fireEvent.change(customInput, {\n        target: { value: dateTimeOptions[1].value },\n      }),\n    )\n    await act(() => fireEvent.click(screen.getByTestId('datetime-custom-btn')))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n      eventData: {\n        currentFormat: dateTimeOptions[1].value,\n      },\n    })\n  })\n\n  it('should call proper telemetry events when radio option is changed to common', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<DatetimeForm {...instance(mockedProps)} />)\n\n    await act(() => fireEvent.click(screen.getByText('Custom')))\n    await act(() => fireEvent.click(screen.getByText('Pre-selected formats')))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n      eventData: {\n        currentFormat: dateTimeOptions[0].value,\n      },\n    })\n  })\n\n  it('should call sendEventTelemetry when common formats is changed', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock)\n\n    render(<DatetimeForm {...instance(mockedProps)} />)\n\n    fireEvent.click(screen.getByTestId('select-datetime-testid'))\n    await act(() =>\n      fireEvent.click(screen.queryByText(dateTimeOptions[1].value) || document),\n    )\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n      eventData: {\n        currentFormat: dateTimeOptions[1].value,\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/components/datetime-form/DatetimeForm.tsx",
    "content": "import React, { useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useFormik } from 'formik'\nimport { checkDateTimeFormat, formatTimestamp } from 'uiSrc/utils'\nimport {\n  DATETIME_FORMATTER_DEFAULT,\n  dateTimeOptions,\n  DatetimeRadioOption,\n  TimezoneOption,\n} from 'uiSrc/constants'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsConfigSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { InfoIcon, CheckBoldIcon } from 'uiSrc/components/base/icons'\nimport { RiRadioGroup } from 'uiSrc/components/base/forms/radio-group/RadioGroup'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { RiTooltip } from 'uiSrc/components'\nimport {\n  defaultValueRender,\n  RiSelect,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { TextInput } from 'uiSrc/components/base/inputs'\n\ninterface InitialValuesType {\n  format: string\n  customFormat: string\n  commonFormat: string\n  selectedRadioOption: DatetimeRadioOption\n}\n\nexport interface Props {\n  onFormatChange: (newPreview: string) => void\n}\n\nconst DatetimeForm = ({ onFormatChange }: Props) => {\n  const [error, setError] = useState('')\n  const [saveFormatSucceed, setSaveFormatSucceed] = useState(false)\n  const config = useSelector(userSettingsConfigSelector)\n  const dispatch = useDispatch()\n\n  const getInitialDateTime = (): InitialValuesType => {\n    const format = config?.dateFormat || DATETIME_FORMATTER_DEFAULT\n    const selectedRadioOption = dateTimeOptions.some(\n      (opt) => opt.value === format,\n    )\n      ? DatetimeRadioOption.Common\n      : DatetimeRadioOption.Custom\n\n    return {\n      selectedRadioOption,\n      format,\n      customFormat:\n        selectedRadioOption === DatetimeRadioOption.Custom ? format : '',\n      commonFormat:\n        selectedRadioOption === DatetimeRadioOption.Common\n          ? format\n          : dateTimeOptions[0].value,\n    }\n  }\n\n  const getInitialValues = useMemo(() => getInitialDateTime(), [config])\n\n  const formik = useFormik({\n    initialValues: getInitialValues,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      submitForm(values)\n    },\n  })\n\n  const submitForm = async (values: InitialValuesType) => {\n    if (checkDateTimeFormat(values.format).valid) {\n      dispatch(\n        updateUserConfigSettingsAction(\n          { dateFormat: values.format.trim() },\n          () => {\n            formik.setSubmitting(false)\n            setTimeout(() => setSaveFormatSucceed(false), 5000)\n          },\n          () => {\n            formik.setSubmitting(false)\n          },\n        ),\n      )\n    } else {\n      setError('This format is not supported.')\n      formik.setSubmitting(false)\n    }\n  }\n\n  const showError = !!error || !formik.values.customFormat\n  const getBtnIconType = () =>\n    !showError\n      ? InfoIcon\n      : !formik.isSubmitting && saveFormatSucceed\n        ? CheckBoldIcon\n        : undefined\n\n  const handleFormatCheck = (format = formik.values.format) => {\n    const { valid, error: errorMsg } = checkDateTimeFormat(format)\n    if (format.length > 50) {\n      setError('Format should not exceed 50 characters')\n    } else if (!valid) {\n      setError(errorMsg || 'This format is not supported')\n      onFormatChange?.('Invalid Format')\n    } else {\n      setError('')\n      const newPreview = formatTimestamp(\n        new Date(),\n        format,\n        config?.timezone || TimezoneOption.Local,\n      )\n      onFormatChange?.(newPreview)\n    }\n    return valid\n  }\n\n  const onRadioOptionChange = (id: string) => {\n    formik.setFieldValue('selectedRadioOption', id)\n    if (id === DatetimeRadioOption.Custom) {\n      formik.setFieldValue('customFormat', formik.values.format)\n    } else {\n      formik.setFieldValue('format', formik.values.commonFormat)\n      sendEventTelemetry({\n        event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n        eventData: {\n          currentFormat: formik.values.commonFormat,\n        },\n      })\n      formik.handleSubmit()\n    }\n  }\n\n  const onCustomFormatChange = (value: string) => {\n    formik.setFieldValue('customFormat', value)\n    formik.setFieldValue('format', value)\n    handleFormatCheck(value)\n  }\n\n  const onCommonFormatChange = (value: string) => {\n    formik.setFieldValue('commonFormat', value)\n    formik.setFieldValue('format', value)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n      eventData: {\n        currentFormat: value,\n      },\n    })\n    formik.handleSubmit()\n  }\n\n  const onCustomFormatSubmit = () => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_DATE_TIME_FORMAT_CHANGED,\n      eventData: {\n        currentFormat: formik.values.customFormat,\n      },\n    })\n    setSaveFormatSucceed(true)\n    formik.handleSubmit()\n  }\n\n  const dateTimeFormatOptions = [\n    {\n      value: DatetimeRadioOption.Common,\n      label: 'Pre-selected formats',\n    },\n    {\n      value: DatetimeRadioOption.Custom,\n      label: 'Custom',\n    },\n  ]\n\n  return (\n    <form onSubmit={formik.handleSubmit} data-testid=\"format-timestamp-form\">\n      <RiRadioGroup\n        items={dateTimeFormatOptions}\n        id=\"datetime-format\"\n        data-testid=\"format-timestamp-form-radio-group\"\n        value={formik.values.selectedRadioOption}\n        onChange={(id) => onRadioOptionChange(id)}\n      />\n      <Row gap=\"m\" style={{ height: 50 }}>\n        {formik.values.selectedRadioOption === DatetimeRadioOption.Common && (\n          <RiSelect\n            style={{ width: 240 }}\n            options={dateTimeOptions.map((option) => ({\n              ...option,\n              'data-test-subj': `date-option-${option.value}`,\n            }))}\n            valueRender={defaultValueRender}\n            value={formik.values.commonFormat}\n            onChange={(option) => onCommonFormatChange(option)}\n            disabled={\n              formik.values.selectedRadioOption !== DatetimeRadioOption.Common\n            }\n            data-test-subj=\"select-datetime\"\n            data-testid=\"select-datetime-testid\"\n          />\n        )}\n        {formik.values.selectedRadioOption === DatetimeRadioOption.Custom && (\n          <>\n            <FlexItem grow={false}>\n              <TextInput\n                style={{ width: 240 }}\n                id=\"customFormat\"\n                name=\"customFormat\"\n                value={formik.values.customFormat}\n                onChange={(value) => onCustomFormatChange(value)}\n                data-testid=\"custom-datetime-input\"\n              />\n            </FlexItem>\n            <RiTooltip\n              position=\"top\"\n              anchorClassName=\"euiToolTip__btn-disabled\"\n              content={\n                showError ? error || 'This format is not supported' : null\n              }\n            >\n              <PrimaryButton\n                aria-label=\"Save\"\n                loading={formik.isSubmitting}\n                onClick={onCustomFormatSubmit}\n                data-testid=\"datetime-custom-btn\"\n                icon={getBtnIconType()}\n                disabled={showError}\n              >\n                Save\n              </PrimaryButton>\n            </RiTooltip>\n          </>\n        )}\n      </Row>\n    </form>\n  )\n}\n\nexport default DatetimeForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/components/timezone-form/TimezoneForm.spec.tsx",
    "content": "import React from 'react'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport TimezoneForm from './TimezoneForm'\n\ndescribe('TimezoneForm', () => {\n  it('should render', () => {\n    expect(render(<TimezoneForm />)).toBeTruthy()\n  })\n\n  it('should include timezone select', () => {\n    const { container } = render(<TimezoneForm />)\n    expect(\n      container.querySelector('[data-test-subj=\"select-timezone\"]'),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/datetime-formatter/components/timezone-form/TimezoneForm.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useFormik } from 'formik'\n\nimport { TimezoneOption, timezoneOptions } from 'uiSrc/constants'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsConfigSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport {\n  defaultValueRender,\n  RiSelect,\n} from 'uiSrc/components/base/forms/select/RiSelect'\n\ninterface InitialValuesType {\n  timezone: TimezoneOption\n}\n\nconst TimezoneForm = () => {\n  const config = useSelector(userSettingsConfigSelector)\n  const dispatch = useDispatch()\n\n  const getInitialValues: InitialValuesType = useMemo(\n    () => ({ timezone: config?.timezone || TimezoneOption.Local }),\n    [config?.timezone],\n  )\n\n  const formik = useFormik({\n    initialValues: getInitialValues,\n    enableReinitialize: true,\n    onSubmit: (values) => {\n      submitForm(values)\n    },\n  })\n\n  const submitForm = async (values: InitialValuesType) => {\n    dispatch(\n      updateUserConfigSettingsAction(\n        { timezone: values.timezone.trim() },\n        () => {\n          formik.setSubmitting(false)\n        },\n        () => {\n          formik.setSubmitting(false)\n        },\n      ),\n    )\n  }\n\n  const onTimezoneChange = (value: string) => {\n    formik.setFieldValue('timezone', value)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_TIME_ZONE_CHANGED,\n      eventData: {\n        currentTimezone: value,\n      },\n    })\n    formik.handleSubmit()\n  }\n\n  return (\n    <form onSubmit={formik.handleSubmit} data-testid=\"format-timezone-form\">\n      <RiSelect\n        style={{ width: 240 }}\n        options={timezoneOptions.map((option) => ({\n          ...option,\n          'data-test-subj': `zone-option-${option.value}`,\n        }))}\n        value={formik.values.timezone}\n        valueRender={defaultValueRender}\n        onChange={(option) => onTimezoneChange(option)}\n        data-test-subj=\"select-timezone\"\n      />\n    </form>\n  )\n}\n\nexport default TimezoneForm\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/general-settings/index.ts",
    "content": "import DateTimeFormatter from './datetime-formatter/DateTimeFormatter'\n\nexport { DateTimeFormatter }\n\nexport default DateTimeFormatter\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/index.ts",
    "content": "import AdvancedSettings from './advanced-settings'\nimport AppVersion from './app-version'\nimport WorkbenchSettings from './workbench-settings'\nimport CloudSettings from './cloud-settings'\nimport GeneralSettings from './general-settings'\nimport ThemeSettings from './theme-settings'\n\nexport {\n  AdvancedSettings,\n  AppVersion,\n  WorkbenchSettings,\n  CloudSettings,\n  GeneralSettings,\n  ThemeSettings,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/theme-settings/ThemeSettings.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  mockedStore,\n  render,\n  screen,\n  userEvent,\n  waitFor,\n  waitForRedisUiSelectVisible,\n} from 'uiSrc/utils/test-utils'\nimport { DEFAULT_THEME, Theme, THEMES } from 'uiSrc/constants'\nimport { TelemetryEvent } from 'uiSrc/telemetry'\nimport { updateUserConfigSettingsAction } from 'uiSrc/slices/user/user-settings'\n\nimport ThemeSettings from './ThemeSettings'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/user/user-settings', () => {\n  const original = jest.requireActual('uiSrc/slices/user/user-settings')\n  return {\n    ...original,\n    updateUserConfigSettingsAction: jest.fn(() => ({ type: 'TEST_ACTION' })),\n  }\n})\n\nconst { sendEventTelemetry } = require('uiSrc/telemetry')\n\nlet store: typeof mockedStore\n\ndescribe('ThemeSettings', () => {\n  beforeEach(() => {\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n  })\n\n  it('should render', () => {\n    expect(render(<ThemeSettings />)).toBeTruthy()\n  })\n\n  it('should render the default theme option selected when there is no previous config', async () => {\n    render(<ThemeSettings />)\n\n    const selectedTheme = THEMES.find((t) => t.value === DEFAULT_THEME)\n    expect(selectedTheme).not.toBeUndefined()\n\n    await waitFor(() => {\n      expect(\n        screen.getByText(selectedTheme?.inputDisplay as string),\n      ).toBeInTheDocument()\n    })\n  })\n\n  it('should update the selected theme and dispatch telemetry on change', async () => {\n    const initialTheme = Theme.Dark\n    const newTheme = Theme.Light\n\n    // @ts-ignore-next-line\n    store.getState().user.settings.config = {\n      ...store.getState().user.settings.config,\n      theme: initialTheme,\n    }\n\n    render(<ThemeSettings />, { store })\n    const dropdownButton = screen.getByTestId('select-theme')\n    await userEvent.click(dropdownButton)\n\n    await waitForRedisUiSelectVisible()\n\n    await waitFor(() => {\n      expect(screen.getByText('Light Theme')).toBeInTheDocument()\n    })\n\n    await userEvent.click(screen.getByText('Light Theme'))\n\n    expect(updateUserConfigSettingsAction).toHaveBeenCalledWith({\n      theme: newTheme,\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SETTINGS_COLOR_THEME_CHANGED,\n      eventData: {\n        previousColorTheme: initialTheme,\n        currentColorTheme: newTheme,\n      },\n    })\n  })\n\n  it('should display all theme options from THEMES', async () => {\n    // @ts-ignore-next-line\n    store.getState().user.settings.config = {\n      ...store.getState().user.settings.config,\n      theme: Theme.Dark,\n    }\n\n    render(<ThemeSettings />, { store })\n\n    const dropdownButton = screen.getByTestId('select-theme')\n    await userEvent.click(dropdownButton)\n\n    await waitForRedisUiSelectVisible()\n\n    const darkTheme = THEMES.find((theme) => theme.value === Theme.Dark)\n\n    const rest = THEMES.filter((theme) => theme.value !== Theme.Dark)\n\n    await waitFor(() => {\n      // Selected theme name appears 2 times in the dropdown\n      // once in the list and once in the selected value\n      expect(\n        screen.queryAllByText(darkTheme?.inputDisplay as string).length,\n      ).toEqual(2)\n\n      rest.forEach((theme) => {\n        expect(\n          screen.getByText(theme.inputDisplay as string),\n        ).toBeInTheDocument()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/theme-settings/ThemeSettings.tsx",
    "content": "import React, { useContext, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  updateUserConfigSettingsAction,\n  userSettingsSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { ThemeContext } from 'uiSrc/contexts/themeContext'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { DEFAULT_THEME, THEMES } from 'uiSrc/constants'\nimport {\n  defaultValueRender,\n  RiSelect,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { Title } from 'uiSrc/components/base/text'\n\nconst ThemeSettings = () => {\n  const dispatch = useDispatch()\n  const [selectedTheme, setSelectedTheme] = useState<string>(DEFAULT_THEME)\n  const options = THEMES\n  const themeContext = useContext(ThemeContext)\n  const { theme, changeTheme } = themeContext\n  const { config } = useSelector(userSettingsSelector)\n  const previousThemeRef = useRef<string>(theme)\n\n  useEffect(() => {\n    if (config && config.theme) {\n      const configTheme = config.theme\n      setSelectedTheme(configTheme)\n      previousThemeRef.current = configTheme\n    }\n  }, [config])\n\n  const onChange = (value: string) => {\n    const previousValue = previousThemeRef.current\n    if (previousValue === value) {\n      return\n    }\n    changeTheme(value)\n    dispatch(updateUserConfigSettingsAction({ theme: value }))\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_COLOR_THEME_CHANGED,\n      eventData: {\n        previousColorTheme: previousValue,\n        currentColorTheme: value,\n      },\n    })\n  }\n\n  return (\n    <form>\n      <Title size=\"XS\">Color Theme</Title>\n      <Spacer size=\"m\" />\n      <FormField label=\"Specifies the color theme to be used in Redis Insight:\">\n        <RiSelect\n          valueRender={defaultValueRender}\n          options={options}\n          value={selectedTheme}\n          onChange={onChange}\n          style={{ marginTop: '12px' }}\n          data-test-subj=\"select-theme\"\n          data-testid=\"select-theme\"\n        />\n      </FormField>\n      <Spacer size=\"xl\" />\n    </form>\n  )\n}\n\nexport default ThemeSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/theme-settings/index.ts",
    "content": "import ThemeSettings from './ThemeSettings'\n\nexport default ThemeSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { setWorkbenchCleanUp } from 'uiSrc/slices/user/user-settings'\nimport {\n  cleanup,\n  userEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport WorkbenchSettings from './WorkbenchSettings'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('WorkbenchSettings', () => {\n  it('should render', () => {\n    expect(render(<WorkbenchSettings />)).toBeTruthy()\n  })\n\n  it('should call proper actions after click on switch wb clear mode', async () => {\n    render(<WorkbenchSettings />)\n\n    const afterRenderActions = [...store.getActions()]\n\n    await userEvent.click(screen.getByTestId('switch-workbench-cleanup'))\n\n    expect(store.getActions()).toEqual([\n      ...afterRenderActions,\n      setWorkbenchCleanUp(false),\n    ])\n  })\n\n  it('should pipeline-bunch render ', () => {\n    render(<WorkbenchSettings />)\n\n    expect(screen.getByTestId(/pipeline-bunch/)).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.tsx",
    "content": "import { toNumber } from 'lodash'\nimport React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { SettingItem } from 'uiSrc/components'\nimport { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport {\n  setWorkbenchCleanUp,\n  updateUserConfigSettingsAction,\n  userSettingsConfigSelector,\n  userSettingsWBSelector,\n} from 'uiSrc/slices/user/user-settings'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { validateNumber } from 'uiSrc/utils'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport { SwitchInput } from 'uiSrc/components/base/inputs'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nconst WorkbenchSettings = () => {\n  const { cleanup } = useSelector(userSettingsWBSelector)\n  const { batchSize = PIPELINE_COUNT_DEFAULT } =\n    useSelector(userSettingsConfigSelector) ?? {}\n\n  const dispatch = useDispatch()\n\n  const onSwitchWbCleanUp = (val: boolean) => {\n    dispatch(setWorkbenchCleanUp(val))\n    sendEventTelemetry({\n      event: TelemetryEvent.SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED,\n      eventData: {\n        currentValue: !val,\n        newValue: val,\n      },\n    })\n  }\n\n  const handleApplyPipelineCountChanges = (value: string) => {\n    dispatch(updateUserConfigSettingsAction({ batchSize: toNumber(value) }))\n  }\n\n  return (\n    <>\n      <Title size=\"M\">Editor Cleanup</Title>\n      <Spacer size=\"m\" />\n      <FormField>\n        <SwitchInput\n          checked={cleanup}\n          onCheckedChange={onSwitchWbCleanUp}\n          title=\"Clear the Editor after running commands\"\n          data-testid=\"switch-workbench-cleanup\"\n        />\n      </FormField>\n      <Spacer size=\"xl\" />\n      <SettingItem\n        initValue={batchSize.toString()}\n        onApply={handleApplyPipelineCountChanges}\n        validation={(value) => validateNumber(value)}\n        title=\"Pipeline Mode\"\n        testid=\"pipeline-bunch\"\n        placeholder={`${PIPELINE_COUNT_DEFAULT}`}\n        label=\"Commands in pipeline:\"\n        summary={\n          <>\n            {'Sets the size of a command batch for the '}\n            <Link\n              variant=\"inline\"\n              href=\"https://redis.io/docs/latest/develop/use/pipelining/\"\n              target=\"_blank\"\n              data-testid=\"pipelining-link\"\n              style={{ padding: 0 }}\n            >\n              pipeline\n            </Link>\n            {' mode in Workbench. 0 or 1 pipelines every command.'}\n          </>\n        }\n      />\n    </>\n  )\n}\n\nexport default WorkbenchSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/components/workbench-settings/index.ts",
    "content": "import WorkbenchSettings from './WorkbenchSettings'\n\nexport default WorkbenchSettings\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/index.ts",
    "content": "import SettingsPage from './SettingsPage'\n\nexport { SettingsPage }\n\nexport default SettingsPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/settings/styles.module.scss",
    "content": ".cover {\n  position: absolute;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  z-index: 2;\n  opacity: 0.8;\n  background-color: var(--euiColorLightestShade);\n}\n\n.title {\n  padding-bottom: 16px;\n  font:\n    normal normal 500 24px/29px Graphik,\n    sans-serif !important;\n}\n\n.accordion {\n  margin-top: 0 !important;\n\n  :global(.RI-collapsible-nav-group-content) {\n    padding: 24px 30px 12px !important;\n  }\n}\n\n.accordionWithSubTitle {\n  :global(.euiAccordion__triggerWrapper) {\n    padding: 8px 16px !important;\n    height: 60px;\n    max-height: 60px;\n  }\n  :global(.euiCollapsibleNavGroup__title) {\n    height: auto;\n  }\n\n  .subtitle {\n    font-size: 12px !important;\n    margin-top: -2px;\n  }\n}\n\n.container {\n  @include eui.scrollBar;\n  position: relative;\n  height: 100%;\n  overflow: auto;\n\n  :global {\n    .euiAccordion {\n      &:not(:last-of-type) {\n        .euiAccordion__triggerWrapper {\n          border-bottom: 0;\n        }\n      }\n    }\n    .euiAccordion__triggerWrapper {\n      background-color: var(--euiColorEmptyShade);\n      margin-top: -2px;\n    }\n    .euiAccordion-isOpen {\n      .euiAccordion__triggerWrapper {\n        background-color: var(--tableRowSelectedColor) !important;\n        border-color: var(--euiColorPrimary);\n        border-bottom: 0 !important;\n      }\n      .euiAccordion__childWrapper {\n        border-color: var(--euiColorPrimary);\n        border-width: 2px !important;\n      }\n    }\n\n    .euiAccordion__childWrapper {\n      background-color: var(--browserTableRowEven) !important;\n      border: 1px solid var(--euiColorLightestShade);\n      border-top: 0 !important;\n\n      .euiFieldText,\n      .euiFieldNumber,\n      .euiSelect,\n      .euiSuperSelectControl {\n        background-color: var(--browserTableRowEven) !important;\n      }\n    }\n  }\n}\n\n.warning {\n  margin: 6px 0 30px;\n}\n\n.smallText {\n  font:\n    normal normal normal 14px/24px Graphik,\n    sans-serif !important;\n  letter-spacing: -0.14px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/SlowLogPage.spec.tsx",
    "content": "import React from 'react'\nimport { slowLogSelector } from 'uiSrc/slices/analytics/slowlog'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport SlowLogPage from './SlowLogPage'\n\njest.mock('uiSrc/slices/analytics/slowlog', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/slowlog'),\n  slowLogSelector: jest.fn().mockReturnValue({\n    data: [],\n    config: null,\n    loading: false,\n  }),\n}))\n\nconst mockedData = [\n  {\n    id: 0,\n    time: 1652429583,\n    durationUs: 56,\n    args: 'info',\n    source: '0.0.0.1:50834',\n    client: 'redisinsight-common-0',\n  },\n  {\n    id: 1,\n    time: 1652429583,\n    durationUs: 11,\n    args: 'config get slowlog*',\n    source: '0.0.0.1:50834',\n    client: 'redisinsight-common-0',\n  },\n]\n\n/**\n * SlowLogPage tests\n *\n * @group component\n */\ndescribe('SlowLogPage', () => {\n  it('should render', () => {\n    expect(render(<SlowLogPage />)).toBeTruthy()\n  })\n\n  it('should render empty slow log with empty data', () => {\n    ;(slowLogSelector as jest.Mock).mockImplementation(() => ({\n      data: [],\n      config: null,\n      loading: false,\n    }))\n\n    render(<SlowLogPage />)\n    expect(screen.getByTestId('empty-slow-log')).toBeTruthy()\n  })\n\n  it('should render slow log table with mocked data', () => {\n    ;(slowLogSelector as jest.Mock).mockImplementation(() => ({\n      data: mockedData,\n      config: null,\n      loading: false,\n    }))\n\n    render(<SlowLogPage />)\n    expect(screen.getByTestId('slowlog-table')).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/SlowLogPage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\n\nexport const StyledSelect = styled(RiSelect)`\n  border: none;\n`\n\nexport const ContentWrapper = styled(Col).attrs({\n  grow: true,\n})`\n  padding-top: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/SlowLogPage.tsx",
    "content": "import { minBy, toNumber } from 'lodash'\nimport React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { AutoSizer } from 'react-virtualized'\n\nimport { DEFAULT_SLOWLOG_MAX_LEN, DurationUnits } from 'uiSrc/constants'\nimport { convertNumberByUnits } from 'uiSrc/pages/slow-log/utils'\nimport { appContextDbConfig } from 'uiSrc/slices/app/context'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport {\n  clearSlowLogAction,\n  fetchSlowLogsAction,\n  getSlowLogConfigAction,\n  slowLogConfigSelector,\n  slowLogSelector,\n} from 'uiSrc/slices/analytics/slowlog'\nimport {\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n  TelemetryEvent,\n  TelemetryPageView,\n} from 'uiSrc/telemetry'\nimport { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport {\n  analyticsSettingsSelector,\n  setAnalyticsViewTab,\n} from 'uiSrc/slices/analytics/settings'\nimport { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics'\n\nimport { FormatedDate } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { defaultValueRender } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { SlowLog } from 'apiSrc/modules/slow-log/models'\nimport { AnalysisPageContainer } from 'uiSrc/pages/database-analysis/components/analysis-page-container'\nimport { AnalyticsPageHeader } from 'uiSrc/pages/database-analysis/components/analytics-page-header'\n\nimport { Actions, EmptySlowLog, SlowLogTable } from './components'\n\nimport { StyledSelect, ContentWrapper } from './SlowLogPage.styles'\nimport { Container } from '../database-analysis/components/header/Header.styles'\n\nconst HIDE_TIMESTAMP_FROM_WIDTH = 850\nconst DEFAULT_COUNT_VALUE = '50'\nconst MAX_COUNT_VALUE = '-1'\nconst countOptions = [\n  { value: '10', inputDisplay: '10' },\n  { value: '25', inputDisplay: '25' },\n  { value: '50', inputDisplay: '50' },\n  { value: '100', inputDisplay: '100' },\n  { value: MAX_COUNT_VALUE, inputDisplay: 'Max available' },\n]\n\nconst SlowLogPage = () => {\n  const {\n    connectionType,\n    name: connectedInstanceName,\n    db,\n  } = useSelector(connectedInstanceSelector)\n  const { data, loading, config } = useSelector(slowLogSelector)\n  const { slowLogDurationUnit: durationUnit } = useSelector(appContextDbConfig)\n  const { slowlogLogSlowerThan = 0, slowlogMaxLen } = useSelector(\n    slowLogConfigSelector,\n  )\n  const { viewTab } = useSelector(analyticsSettingsSelector)\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const [count, setCount] = useState<string>(DEFAULT_COUNT_VALUE)\n  const [isPageViewSent, setIsPageViewSent] = useState(false)\n\n  const dispatch = useDispatch()\n\n  const lastTimestamp = minBy(data, 'time')?.time\n  const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}`\n  setTitle(`${dbName} - Slow Log`)\n\n  useEffect(() => {\n    getConfig()\n    if (viewTab !== AnalyticsViewTab.SlowLog) {\n      dispatch(setAnalyticsViewTab(AnalyticsViewTab.SlowLog))\n    }\n  }, [])\n\n  useEffect(() => {\n    getSlowLogs()\n  }, [count])\n\n  useEffect(() => {\n    if (connectedInstanceName && !isPageViewSent) {\n      sendPageView(instanceId)\n    }\n  }, [connectedInstanceName, isPageViewSent])\n\n  const sendPageView = (instanceId: string) => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.SLOWLOG_PAGE,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  const getSlowLogs = (maxLen?: number) => {\n    const countToSend =\n      count === MAX_COUNT_VALUE\n        ? maxLen || slowlogMaxLen || DEFAULT_SLOWLOG_MAX_LEN\n        : toNumber(count)\n\n    dispatch(\n      fetchSlowLogsAction(instanceId, countToSend, (data: SlowLog[]) => {\n        sendEventTelemetry({\n          event: TelemetryEvent.SLOWLOG_LOADED,\n          eventData: {\n            databaseId: instanceId,\n            numberOfCommands: data.length,\n          },\n        })\n      }),\n    )\n  }\n\n  const getConfig = () => {\n    dispatch(getSlowLogConfigAction(instanceId))\n  }\n\n  const onClearSlowLogs = () => {\n    dispatch(\n      clearSlowLogAction(instanceId, () => {\n        sendEventTelemetry({\n          event: TelemetryEvent.SLOWLOG_CLEARED,\n          eventData: {\n            databaseId: instanceId,\n          },\n        })\n      }),\n    )\n  }\n\n  const isEmptySlowLog = !data.length\n\n  return (\n    <AnalysisPageContainer data-testid=\"slow-log-page\">\n      <AutoSizer disableHeight>\n        {({ width }) => (\n          <div style={{ width }}>\n            <AnalyticsPageHeader\n              actions={\n                <Container align=\"center\" gap=\"xl\">\n                  {connectionType !== ConnectionType.Cluster && config && (\n                    <Text size=\"s\" color=\"secondary\" data-testid=\"config-info\">\n                      Execution time:{' '}\n                      {numberWithSpaces(\n                        convertNumberByUnits(\n                          slowlogLogSlowerThan,\n                          durationUnit,\n                        ),\n                      )}\n                      &nbsp;\n                      {durationUnit === DurationUnits.milliSeconds\n                        ? DurationUnits.mSeconds\n                        : DurationUnits.microSeconds}\n                      , Max length: {numberWithSpaces(slowlogMaxLen)}\n                    </Text>\n                  )}\n\n                  <Actions\n                    width={width}\n                    isEmptySlowLog={isEmptySlowLog}\n                    durationUnit={durationUnit}\n                    onClear={onClearSlowLogs}\n                    onRefresh={getSlowLogs}\n                  />\n                </Container>\n              }\n            />\n          </div>\n        )}\n      </AutoSizer>\n      <ContentWrapper>\n        <AutoSizer disableHeight>\n          {({ width }) => (\n            <div style={{ width }}>\n              <Row align=\"center\" justify=\"between\">\n                <FlexItem>\n                  <Title size=\"L\" color=\"primary\">\n                    Slow Log\n                  </Title>\n                </FlexItem>\n                <FlexItem>\n                  <Row align=\"center\" gap=\"xs\">\n                    <FlexItem>\n                      <Text size=\"s\" color=\"primary\">\n                        {connectionType === ConnectionType.Cluster\n                          ? 'Display per node:'\n                          : 'Display up to:'}\n                      </Text>\n                    </FlexItem>\n                    <FlexItem>\n                      <StyledSelect\n                        options={countOptions}\n                        valueRender={defaultValueRender}\n                        value={count}\n                        onChange={(value) => setCount(value)}\n                        data-testid=\"count-select\"\n                      />\n                    </FlexItem>\n                    {width > HIDE_TIMESTAMP_FROM_WIDTH && (\n                      <FlexItem style={{ marginLeft: 12 }}>\n                        <Text\n                          size=\"s\"\n                          color=\"secondary\"\n                          data-testid=\"entries-from-timestamp\"\n                        >\n                          ({data.length} entries\n                          {lastTimestamp && (\n                            <>\n                              <span>&nbsp;from &nbsp;</span>\n                              <FormatedDate date={lastTimestamp * 1000} />\n                            </>\n                          )}\n                          )\n                        </Text>\n                      </FlexItem>\n                    )}\n                  </Row>\n                </FlexItem>\n              </Row>\n            </div>\n          )}\n        </AutoSizer>\n        {isEmptySlowLog ? (\n          <EmptySlowLog\n            slowlogLogSlowerThan={slowlogLogSlowerThan}\n            durationUnit={durationUnit}\n          />\n        ) : (\n          <SlowLogTable\n            items={data}\n            loading={loading}\n            durationUnit={durationUnit}\n          />\n        )}\n      </ContentWrapper>\n    </AnalysisPageContainer>\n  )\n}\n\nexport default SlowLogPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/Actions/Actions.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport Actions, { Props } from './Actions'\n\nconst mockedProps = mock<Props>()\n\ndescribe('Actions', () => {\n  it('should render', () => {\n    expect(render(<Actions {...mockedProps} />)).toBeTruthy()\n  })\n\n  it('should call onClear after submit clear btn', () => {\n    const onClear = jest.fn()\n    render(\n      <Actions {...mockedProps} onClear={onClear} isEmptySlowLog={false} />,\n    )\n\n    fireEvent.click(screen.getByTestId('clear-btn'))\n    fireEvent.click(screen.getByTestId('reset-confirm-btn'))\n\n    expect(onClear).toBeCalled()\n  })\n\n  it('should call onRefresh after submit refresh btn', () => {\n    const onRefresh = jest.fn()\n    render(\n      <Actions {...mockedProps} onRefresh={onRefresh} isEmptySlowLog={false} />,\n    )\n\n    fireEvent.click(screen.getByTestId('slowlog-refresh-btn'))\n\n    expect(onRefresh).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/Actions/Actions.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const StyledInfoIconWrapper = styled.span`\n  display: flex;\n  align-self: center;\n  cursor: pointer;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/Actions/Actions.tsx",
    "content": "import React, { useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { DurationUnits } from 'uiSrc/constants'\nimport { slowLogSelector } from 'uiSrc/slices/analytics/slowlog'\nimport { AutoRefresh } from 'uiSrc/components'\nimport { RiPopover, RiTooltip } from 'uiSrc/components/base'\nimport { Nullable } from 'uiSrc/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { EraserIcon, SettingsIcon } from 'uiSrc/components/base/icons'\nimport { IconButton, PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { RiIcon } from 'uiSrc/components/base/icons/RiIcon'\n\nimport SlowLogConfig from '../SlowLogConfig'\nimport { StyledInfoIconWrapper } from './Actions.styles'\nimport { ClearSlowLogModal } from '../ClearSlowLogModal/ClearSlowLogModal'\n\nexport interface Props {\n  width: number\n  isEmptySlowLog: boolean\n  durationUnit: Nullable<DurationUnits>\n  onClear: () => void\n  onRefresh: (maxLen?: number) => void\n}\n\nconst HIDE_REFRESH_LABEL_WIDTH = 850\n\nconst Actions = (props: Props) => {\n  const {\n    isEmptySlowLog,\n    durationUnit,\n    width,\n    onClear = () => {},\n    onRefresh,\n  } = props\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { name = '' } = useSelector(connectedInstanceSelector)\n  const { loading, lastRefreshTime } = useSelector(slowLogSelector)\n\n  const [isClearModalOpen, setIsClearModalOpen] = useState(false)\n  const [isPopoverConfigOpen, setIsPopoverConfigOpen] = useState(false)\n\n  const showClearModal = () => {\n    setIsClearModalOpen((isClearModalOpen) => !isClearModalOpen)\n  }\n\n  const closeClearModal = () => {\n    setIsClearModalOpen(false)\n  }\n  const showConfigPopover = () => {\n    setIsPopoverConfigOpen((isPopoverConfigOpen) => !isPopoverConfigOpen)\n  }\n\n  const closePopoverConfig = () => {\n    setIsPopoverConfigOpen(false)\n  }\n\n  const handleEnableAutoRefresh = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    sendEventTelemetry({\n      event: enableAutoRefresh\n        ? TelemetryEvent.SLOWLOG_AUTO_REFRESH_ENABLED\n        : TelemetryEvent.SLOWLOG_AUTO_REFRESH_DISABLED,\n      eventData: {\n        databaseId: instanceId,\n        refreshRate: enableAutoRefresh ? +refreshRate : undefined,\n      },\n    })\n  }\n\n  const handleChangeAutoRefreshRate = (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => {\n    if (enableAutoRefresh) {\n      sendEventTelemetry({\n        event: TelemetryEvent.SLOWLOG_AUTO_REFRESH_ENABLED,\n        eventData: {\n          databaseId: instanceId,\n          refreshRate: +refreshRate,\n        },\n      })\n    }\n  }\n\n  return (\n    <Row gap=\"s\" align=\"center\">\n      <FlexItem>\n        <AutoRefresh\n          postfix=\"slowlog\"\n          loading={loading}\n          displayText={width > HIDE_REFRESH_LABEL_WIDTH}\n          lastRefreshTime={lastRefreshTime}\n          onRefresh={() => onRefresh()}\n          onEnableAutoRefresh={handleEnableAutoRefresh}\n          onChangeAutoRefreshRate={handleChangeAutoRefreshRate}\n          testid=\"slowlog\"\n        />\n      </FlexItem>\n\n      <FlexItem>\n        <RiPopover\n          ownFocus\n          anchorPosition=\"downRight\"\n          isOpen={isPopoverConfigOpen}\n          panelPaddingSize=\"m\"\n          closePopover={() => {}}\n          button={\n            <PrimaryButton\n              size=\"small\"\n              icon={SettingsIcon}\n              aria-label=\"Configure\"\n              onClick={() => showConfigPopover()}\n              data-testid=\"configure-btn\"\n            >\n              Configure\n            </PrimaryButton>\n          }\n        >\n          <SlowLogConfig\n            closePopover={closePopoverConfig}\n            onRefresh={onRefresh}\n          />\n        </RiPopover>\n      </FlexItem>\n\n      {!isEmptySlowLog && (\n        <>\n          <IconButton\n            icon={EraserIcon}\n            aria-label=\"Clear Slow Log\"\n            onClick={() => showClearModal()}\n            data-testid=\"clear-btn\"\n          />\n\n          <ClearSlowLogModal\n            name={name}\n            isOpen={isClearModalOpen}\n            onClose={closeClearModal}\n            onClear={onClear}\n          />\n        </>\n      )}\n\n      <FlexItem>\n        <RiTooltip\n          title=\"Slow Log\"\n          position=\"bottom\"\n          content={\n            <span data-testid=\"slowlog-tooltip-text\">\n              Slow Log is a list of slow operations for your Redis instance.\n              These can be used to troubleshoot performance issues.\n              <Spacer size=\"xs\" />\n              Each entry in the list displays the command, duration and\n              timestamp. Any transaction that exceeds{' '}\n              <b>slowlog-log-slower-than</b> {durationUnit} are recorded up to a\n              maximum of <b>slowlog-max-len</b> after which older entries are\n              discarded.\n            </span>\n          }\n        >\n          <StyledInfoIconWrapper>\n            <RiIcon type=\"InfoIcon\" data-testid=\"slow-log-tooltip-icon\" />\n          </StyledInfoIconWrapper>\n        </RiTooltip>\n      </FlexItem>\n    </Row>\n  )\n}\n\nexport default Actions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/Actions/index.ts",
    "content": "import Actions from './Actions'\n\nexport default Actions\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/ClearSlowLogModal/ClearSlowLogModal.spec.tsx",
    "content": "import React from 'react'\n\nimport { ClearSlowLogModal, ClearSlowLogModalProps } from './ClearSlowLogModal'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nconst renderClearSlowLogModal = (props?: Partial<ClearSlowLogModalProps>) => {\n  const defaultProps: ClearSlowLogModalProps = {\n    name: 'test-database',\n    isOpen: true,\n    onClose: jest.fn(),\n    onClear: jest.fn(),\n  }\n\n  return render(<ClearSlowLogModal {...defaultProps} {...props} />)\n}\n\ndescribe('ClearSlowLogModal', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render the modal when isOpen is true', () => {\n    const props: Partial<ClearSlowLogModalProps> = {\n      name: 'test-database',\n      isOpen: true,\n    }\n\n    renderClearSlowLogModal(props)\n\n    const title = screen.getByText('Clear slow log')\n    expect(title).toBeInTheDocument()\n\n    const message = screen.getByText(\n      /Slow Log will be cleared for\\s+test-database/,\n    )\n    expect(message).toBeInTheDocument()\n\n    const note = screen.getByText('NOTE: This is server configuration')\n    expect(note).toBeInTheDocument()\n\n    const cancelButton = screen.getByTestId('reset-cancel-btn')\n    expect(cancelButton).toBeInTheDocument()\n    expect(cancelButton).toHaveTextContent('Cancel')\n\n    const clearButton = screen.getByTestId('reset-confirm-btn')\n    expect(clearButton).toBeInTheDocument()\n    expect(clearButton).toHaveTextContent('Clear')\n  })\n\n  it('should not render the modal when isOpen is false', () => {\n    renderClearSlowLogModal({ isOpen: false })\n\n    const modal = screen.queryByTestId('clear-slow-log-modal')\n    expect(modal).not.toBeInTheDocument()\n  })\n\n  it('should call onClose when Cancel button is clicked', () => {\n    const props: Partial<ClearSlowLogModalProps> = {\n      onClose: jest.fn(),\n      onClear: jest.fn(),\n    }\n\n    renderClearSlowLogModal(props)\n\n    const cancelButton = screen.getByTestId('reset-cancel-btn')\n    fireEvent.click(cancelButton)\n\n    expect(props.onClose).toHaveBeenCalledTimes(1)\n    expect(props.onClear).not.toHaveBeenCalled()\n  })\n\n  it('should call onClear and onClose when Clear button is clicked', () => {\n    const props: Partial<ClearSlowLogModalProps> = {\n      onClose: jest.fn(),\n      onClear: jest.fn(),\n    }\n\n    renderClearSlowLogModal(props)\n\n    const clearButton = screen.getByTestId('reset-confirm-btn')\n    fireEvent.click(clearButton)\n\n    expect(props.onClear).toHaveBeenCalledTimes(1)\n    expect(props.onClose).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/ClearSlowLogModal/ClearSlowLogModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport FormDialog from 'uiSrc/components/form-dialog/FormDialog'\n\nexport const StyledFormDialog = styled(FormDialog)`\n  width: 402px;\n  height: auto;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/ClearSlowLogModal/ClearSlowLogModal.tsx",
    "content": "import React from 'react'\n\nimport { Button, DestructiveButton } from 'uiSrc/components/base/forms/buttons'\nimport { Col, FlexGroup, Row } from 'uiSrc/components/base/layout/flex'\nimport { Title, Text } from 'uiSrc/components/base/text'\nimport { EraserIcon } from 'uiSrc/components/base/icons'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\nimport { StyledFormDialog } from './ClearSlowLogModal.styles'\n\nexport interface ClearSlowLogModalProps {\n  name: string\n  isOpen: boolean\n  onClose: () => void\n  onClear: () => void\n}\n\nexport const ClearSlowLogModal = ({\n  name,\n  isOpen,\n  onClose,\n  onClear,\n}: ClearSlowLogModalProps) => {\n  const handleClearClick = () => {\n    onClear()\n    onClose()\n  }\n\n  return (\n    <StyledFormDialog\n      isOpen={isOpen}\n      onClose={onClose}\n      data-testid=\"clear-slow-log-modal\"\n      header={<Title size=\"XL\">Clear slow log</Title>}\n      footer={\n        <Row justify=\"end\" gap=\"m\">\n          <Button\n            variant=\"secondary-ghost\"\n            size=\"large\"\n            onClick={onClose}\n            data-testid=\"reset-cancel-btn\"\n          >\n            Cancel\n          </Button>\n          <DestructiveButton\n            size=\"large\"\n            icon={EraserIcon}\n            onClick={() => handleClearClick()}\n            data-testid=\"reset-confirm-btn\"\n          >\n            Clear\n          </DestructiveButton>\n        </Row>\n      }\n    >\n      <Spacer size=\"l\" />\n      <FlexGroup direction=\"column\" gap=\"l\">\n        <Col>\n          <Text size=\"m\" color=\"primary\">\n            Slow Log will be cleared for&nbsp;{name}\n          </Text>\n          <Text size=\"m\" color=\"secondary\">\n            NOTE: This is server configuration\n          </Text>\n        </Col>\n      </FlexGroup>\n    </StyledFormDialog>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/EmptySlowLog/EmptySlowLog.spec.tsx",
    "content": "import React from 'react'\nimport { DurationUnits } from 'uiSrc/constants'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport EmptySlowLog from './EmptySlowLog'\n\ndescribe('EmptySlowLog', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <EmptySlowLog\n          durationUnit={DurationUnits.milliSeconds}\n          slowlogLogSlowerThan={100}\n        />,\n      ),\n    ).toBeTruthy()\n  })\n  it('should contain msec instead of ms', () => {\n    const { container } = render(\n      <EmptySlowLog\n        durationUnit={DurationUnits.milliSeconds}\n        slowlogLogSlowerThan={10000000}\n      />,\n    )\n\n    expect(container).toHaveTextContent('10 000 msec')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/EmptySlowLog/EmptySlowLog.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const StyledImage = styled.img`\n  max-width: 120px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/EmptySlowLog/EmptySlowLog.tsx",
    "content": "import React from 'react'\nimport { useTheme } from '@redis-ui/styles'\nimport { DurationUnits } from 'uiSrc/constants'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { convertNumberByUnits } from 'uiSrc/pages/slow-log/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport NoQueryResultsIcon from 'uiSrc/assets/img/vector-search/no-query-results.svg'\nimport NoQueryResultsIconDark from 'uiSrc/assets/img/vector-search/no-query-results-dark.svg'\n\nimport { StyledImage } from './EmptySlowLog.styles'\n\nexport interface Props {\n  durationUnit: DurationUnits\n  slowlogLogSlowerThan: number\n}\n\nconst EmptySlowLog = (props: Props) => {\n  const { durationUnit, slowlogLogSlowerThan } = props\n  const theme = useTheme()\n  const icon =\n    theme.name === 'dark' ? NoQueryResultsIconDark : NoQueryResultsIcon\n\n  return (\n    <Col justify=\"center\" grow data-testid=\"empty-slow-log\">\n      <Col align=\"center\" justify=\"center\" gap=\"xxl\">\n        <StyledImage as=\"img\" src={icon} alt=\"No Slow Logs\" />\n        <Col align=\"center\" gap=\"m\" grow={false}>\n          <Title size=\"M\" color=\"primary\">\n            No Slow Logs found\n          </Title>\n          <Text color=\"primary\">\n            Either no commands exceeding&nbsp;\n            {numberWithSpaces(\n              convertNumberByUnits(slowlogLogSlowerThan, durationUnit),\n            )}\n            &nbsp;\n            {durationUnit === DurationUnits.milliSeconds\n              ? DurationUnits.mSeconds\n              : DurationUnits.microSeconds}\n            &nbsp;were found or Slow Log is disabled on the server.\n          </Text>\n        </Col>\n      </Col>\n    </Col>\n  )\n}\n\nexport default EmptySlowLog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/EmptySlowLog/index.ts",
    "content": "import EmptySlowLog from './EmptySlowLog'\n\nexport default EmptySlowLog\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogConfig/SlowLogConfig.spec.tsx",
    "content": "import React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { cloneDeep } from 'lodash'\nimport {\n  render,\n  screen,\n  fireEvent,\n  mockedStore,\n  cleanup,\n} from 'uiSrc/utils/test-utils'\nimport {\n  DEFAULT_SLOWLOG_MAX_LEN,\n  DEFAULT_SLOWLOG_SLOWER_THAN,\n} from 'uiSrc/constants'\n\nimport SlowLogConfig, { Props } from './SlowLogConfig'\n\nconst mockedProps = mock<Props>()\n\nconst slowlogMaxLenMock = 123\nconst slowlogLogSlowerThanMock = 1000\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/slices/analytics/slowlog', () => ({\n  ...jest.requireActual('uiSrc/slices/analytics/slowlog'),\n  slowLogConfigSelector: jest.fn().mockReturnValue({\n    slowlogMaxLen: slowlogMaxLenMock,\n    slowlogLogSlowerThan: slowlogLogSlowerThanMock,\n  }),\n}))\n\ndescribe('SlowLogConfig', () => {\n  it('should render', () => {\n    expect(render(<SlowLogConfig {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should change \"slower-than-input\" value properly', () => {\n    render(<SlowLogConfig {...instance(mockedProps)} />)\n    fireEvent.change(screen.getByTestId('slower-than-input'), {\n      target: { value: '123' },\n    })\n    expect(screen.getByTestId('slower-than-input')).toHaveValue('123')\n  })\n\n  it('should change \"max-len-input\" value properly', () => {\n    render(<SlowLogConfig {...instance(mockedProps)} />)\n    fireEvent.change(screen.getByTestId('max-len-input'), {\n      target: { value: '123' },\n    })\n    expect(screen.getByTestId('max-len-input')).toHaveValue('123')\n  })\n\n  it('btn Cancel should call \"closePopover\" and do not call \"onRefresh\"', () => {\n    const onRefresh = jest.fn()\n    const closePopover = jest.fn()\n    render(<SlowLogConfig onRefresh={onRefresh} closePopover={closePopover} />)\n    fireEvent.change(screen.getByTestId('max-len-input'), {\n      target: { value: '123' },\n    })\n    fireEvent.change(screen.getByTestId('slower-than-input'), {\n      target: { value: '123' },\n    })\n\n    fireEvent.click(screen.getByTestId('slowlog-config-cancel-btn'))\n    expect(closePopover).toBeCalled()\n    expect(onRefresh).not.toBeCalled()\n  })\n\n  it('btn Default should do not call \"closePopover\" and \"onRefresh\"', () => {\n    const onRefresh = jest.fn()\n    const closePopover = jest.fn()\n    render(<SlowLogConfig onRefresh={onRefresh} closePopover={closePopover} />)\n    fireEvent.change(screen.getByTestId('max-len-input'), {\n      target: { value: '123' },\n    })\n    fireEvent.change(screen.getByTestId('slower-than-input'), {\n      target: { value: '123' },\n    })\n\n    fireEvent.click(screen.getByTestId('slowlog-config-default-btn'))\n    expect(closePopover).not.toBeCalled()\n    expect(onRefresh).not.toBeCalled()\n  })\n\n  it('btn Default should reset form\"', () => {\n    const onRefresh = jest.fn()\n    const closePopover = jest.fn()\n    render(<SlowLogConfig onRefresh={onRefresh} closePopover={closePopover} />)\n    fireEvent.change(screen.getByTestId('max-len-input'), {\n      target: { value: '12323' },\n    })\n    fireEvent.change(screen.getByTestId('slower-than-input'), {\n      target: { value: '123223' },\n    })\n\n    fireEvent.click(screen.getByTestId('slowlog-config-default-btn'))\n    expect(screen.getByTestId('max-len-input')).toHaveValue(\n      `${DEFAULT_SLOWLOG_MAX_LEN}`,\n    )\n    expect(screen.getByTestId('slower-than-input')).toHaveValue(\n      `${DEFAULT_SLOWLOG_SLOWER_THAN}`,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogConfig/SlowLogConfig.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { TextInput } from 'uiSrc/components/base/inputs'\nimport { theme } from '@redis-ui/styles'\n\nexport const StyledContainer = styled(Col)<{ $isCluster?: boolean }>`\n  width: ${({ $isCluster }) => ($isCluster ? '394px' : '550px')};\n  padding: ${theme.core.space.space200};\n  border-radius: ${theme.core.space.space050};\n`\n\nexport const StyledInput = styled(TextInput)`\n  width: 160px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogConfig/SlowLogConfig.tsx",
    "content": "import { toNumber } from 'lodash'\nimport React, { useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport {\n  DEFAULT_SLOWLOG_DURATION_UNIT,\n  DEFAULT_SLOWLOG_MAX_LEN,\n  DEFAULT_SLOWLOG_SLOWER_THAN,\n  DURATION_UNITS,\n  DurationUnits,\n  MINUS_ONE,\n} from 'uiSrc/constants'\nimport { appContextDbConfig } from 'uiSrc/slices/app/context'\nimport { ConnectionType } from 'uiSrc/slices/interfaces'\nimport {\n  patchSlowLogConfigAction,\n  slowLogConfigSelector,\n  slowLogSelector,\n} from 'uiSrc/slices/analytics/slowlog'\nimport { errorValidateNegativeInteger, validateNumber } from 'uiSrc/utils'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { useConnectionType } from 'uiSrc/components/hooks/useConnectionType'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport {\n  EmptyButton,\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport {\n  defaultValueRender,\n  RiSelect,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { convertNumberByUnits } from '../../utils'\nimport { StyledContainer, StyledInput } from './SlowLogConfig.styles'\n\nexport interface Props {\n  closePopover: () => void\n  onRefresh: (maxLen?: number) => void\n}\n\nconst SlowLogConfig = ({ closePopover, onRefresh }: Props) => {\n  const options = DURATION_UNITS\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const connectionType = useConnectionType()\n  const { loading } = useSelector(slowLogSelector)\n  const { slowLogDurationUnit } = useSelector(appContextDbConfig)\n  const {\n    slowlogMaxLen = DEFAULT_SLOWLOG_MAX_LEN,\n    slowlogLogSlowerThan = DEFAULT_SLOWLOG_SLOWER_THAN,\n  } = useSelector(slowLogConfigSelector)\n\n  const [durationUnit, setDurationUnit] = useState(\n    slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT,\n  )\n  const [maxLen, setMaxLen] = useState(`${slowlogMaxLen}`)\n\n  const [slowerThan, setSlowerThan] = useState(\n    slowlogLogSlowerThan !== MINUS_ONE\n      ? `${convertNumberByUnits(slowlogLogSlowerThan, durationUnit)}`\n      : `${MINUS_ONE}`,\n  )\n\n  const dispatch = useDispatch()\n\n  const onChangeUnit = (value: DurationUnits) => {\n    setDurationUnit(value)\n  }\n\n  const handleDefault = () => {\n    setMaxLen(`${DEFAULT_SLOWLOG_MAX_LEN}`)\n    setSlowerThan(`${DEFAULT_SLOWLOG_SLOWER_THAN}`)\n    setDurationUnit(DEFAULT_SLOWLOG_DURATION_UNIT)\n  }\n\n  const handleCancel = () => {\n    closePopover()\n  }\n\n  const calculateSlowlogLogSlowerThan = (initSlowerThan: string) => {\n    if (initSlowerThan === '') {\n      return DEFAULT_SLOWLOG_SLOWER_THAN\n    }\n    if (initSlowerThan === `${MINUS_ONE}`) {\n      return MINUS_ONE\n    }\n    if (initSlowerThan === `${MINUS_ONE}`) {\n      return MINUS_ONE\n    }\n    return durationUnit === DurationUnits.microSeconds\n      ? +initSlowerThan\n      : +initSlowerThan * 1000\n  }\n\n  const handleSave = () => {\n    const slowlogLogSlowerThan = calculateSlowlogLogSlowerThan(slowerThan)\n    dispatch(\n      patchSlowLogConfigAction(\n        instanceId,\n        {\n          slowlogMaxLen: maxLen ? toNumber(maxLen) : DEFAULT_SLOWLOG_MAX_LEN,\n          slowlogLogSlowerThan,\n        },\n        durationUnit,\n        onSuccess,\n      ),\n    )\n  }\n\n  const onSuccess = () => {\n    onRefresh(maxLen ? toNumber(maxLen) : DEFAULT_SLOWLOG_MAX_LEN)\n    closePopover()\n  }\n\n  const disabledApplyBtn = () =>\n    (errorValidateNegativeInteger(`${slowerThan}`) && !!slowerThan) || loading\n\n  const clusterContent = () => (\n    <>\n      <Text color=\"primary\">\n        Each node can have different Slow Log configuration in a clustered\n        database.\n        <Spacer size=\"s\" />\n        {'Use '}\n        <code>CONFIG SET slowlog-log-slower-than</code>\n        {' or '}\n        <code>CONFIG SET slowlog-max-len</code>\n        {' for a specific node in redis-cli to configure it.'}\n      </Text>\n      <Row justify=\"end\">\n        <PrimaryButton\n          onClick={closePopover}\n          data-testid=\"slowlog-config-ok-btn\"\n        >\n          Ok\n        </PrimaryButton>\n      </Row>\n    </>\n  )\n\n  const unitConverter = () => {\n    if (Number.isNaN(toNumber(slowerThan))) {\n      return `- ${DurationUnits.mSeconds}`\n    }\n\n    if (slowerThan === `${MINUS_ONE}`) {\n      return `-1 ${DurationUnits.mSeconds}`\n    }\n\n    if (durationUnit === DurationUnits.microSeconds) {\n      const value = numberWithSpaces(\n        convertNumberByUnits(toNumber(slowerThan), DurationUnits.milliSeconds),\n      )\n      return `${value} ${DurationUnits.mSeconds}`\n    }\n\n    if (durationUnit === DurationUnits.milliSeconds) {\n      const value = numberWithSpaces(toNumber(slowerThan) * 1000)\n      return `${value} ${DurationUnits.microSeconds}`\n    }\n    return null\n  }\n\n  return (\n    <StyledContainer\n      $isCluster={connectionType === ConnectionType.Cluster}\n      gap=\"xxl\"\n    >\n      {connectionType === ConnectionType.Cluster && clusterContent()}\n      {connectionType !== ConnectionType.Cluster && (\n        <>\n          <form>\n            <Col gap=\"xxl\">\n              <FormField\n                layout=\"vertical\"\n                label={<Text color=\"primary\">slowlog-log-slower-than</Text>}\n                additionalText={\n                  <FlexItem $gap=\"s\">\n                    <Text\n                      color=\"secondary\"\n                      size=\"s\"\n                      data-testid=\"unit-converter\"\n                    >\n                      {unitConverter()}\n                    </Text>\n                    <Text\n                      color=\"secondary\"\n                      size=\"s\"\n                      data-testid=\"unit-converter\"\n                    >\n                      Execution time to exceed in order to log the command.\n                      <br />\n                      -1 disables Slow Log. 0 logs each command.\n                    </Text>\n                  </FlexItem>\n                }\n              >\n                <Row grow={false} align=\"center\" justify=\"start\" gap=\"s\">\n                  <StyledInput\n                    name=\"slowerThan\"\n                    id=\"slowerThan\"\n                    value={slowerThan}\n                    onChange={(value) => {\n                      setSlowerThan(validateNumber(value.trim(), -1, Infinity))\n                    }}\n                    placeholder={`${convertNumberByUnits(DEFAULT_SLOWLOG_SLOWER_THAN, durationUnit)}`}\n                    autoComplete=\"off\"\n                    data-testid=\"slower-than-input\"\n                  />\n                  <RiSelect\n                    style={{ maxWidth: 100 }}\n                    options={options}\n                    value={durationUnit}\n                    valueRender={defaultValueRender}\n                    onChange={onChangeUnit}\n                    data-test-subj=\"select-default-unit\"\n                  />\n                </Row>\n              </FormField>\n              <FormField\n                layout=\"vertical\"\n                label={<Text color=\"primary\">slowlog-max-len</Text>}\n                additionalText={\n                  <Text color=\"secondary\" size=\"s\">\n                    The length of the Slow Log. When a new command is logged the\n                    oldest\n                    <br />\n                    one is removed from the queue of logged commands.\n                  </Text>\n                }\n              >\n                <StyledInput\n                  name=\"maxLen\"\n                  id=\"maxLen\"\n                  placeholder={`${DEFAULT_SLOWLOG_MAX_LEN}`}\n                  value={maxLen}\n                  onChange={(value) => {\n                    setMaxLen(validateNumber(value.trim()))\n                  }}\n                  autoComplete=\"off\"\n                  data-testid=\"max-len-input\"\n                />\n              </FormField>\n            </Col>\n          </form>\n\n          <Row justify=\"between\" align=\"center\">\n            <FlexItem>\n              <Text size=\"s\" color=\"secondary\">\n                NOTE: This is server configuration\n              </Text>\n            </FlexItem>\n            <Row align=\"center\" gap=\"m\" grow={false}>\n              <EmptyButton\n                size=\"large\"\n                onClick={handleDefault}\n                data-testid=\"slowlog-config-default-btn\"\n              >\n                Default\n              </EmptyButton>\n              <SecondaryButton\n                onClick={handleCancel}\n                data-testid=\"slowlog-config-cancel-btn\"\n              >\n                Cancel\n              </SecondaryButton>\n              <PrimaryButton\n                disabled={disabledApplyBtn()}\n                onClick={handleSave}\n                data-testid=\"slowlog-config-save-btn\"\n              >\n                Save\n              </PrimaryButton>\n            </Row>\n          </Row>\n        </>\n      )}\n    </StyledContainer>\n  )\n}\n\nexport default SlowLogConfig\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogConfig/index.ts",
    "content": "import SlowLogConfig from './SlowLogConfig'\n\nexport default SlowLogConfig\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogTable/SlowLogTable.spec.tsx",
    "content": "import React from 'react'\nimport { mock } from 'ts-mockito'\nimport { render } from 'uiSrc/utils/test-utils'\n\nimport SlowLogTable, { Props } from './SlowLogTable'\n\nconst mockedProps = mock<Props>()\n\nconst mockedData = [\n  {\n    id: 0,\n    time: 1652429583,\n    durationUs: 56,\n    args: 'info',\n    source: '0.0.0.1:50834',\n    client: 'redisinsight-common-0',\n  },\n  {\n    id: 1,\n    time: 1652429583,\n    durationUs: 11,\n    args: 'config get slowlog*',\n    source: '0.0.0.1:50834',\n    client: 'redisinsight-common-0',\n  },\n]\n\ndescribe('SlowLogTable', () => {\n  it('should render', () => {\n    expect(render(<SlowLogTable {...mockedProps} items={[]} />)).toBeTruthy()\n  })\n\n  it('should render data', () => {\n    const { container } = render(\n      <SlowLogTable {...mockedProps} items={mockedData} />,\n    )\n\n    expect(container).toBeTruthy()\n\n    const rows = container.querySelectorAll('[data-row-type=\"regular\"]')\n    expect(rows).toHaveLength(mockedData.length)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogTable/SlowLogTable.styles.ts",
    "content": "import { Table } from '@redis-ui/table'\nimport styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const StyledTableWrapper = styled(Col)`\n  height: calc(100% - 100px);\n`\n\nexport const StyledTable = styled(Table)`\n  max-height: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogTable/SlowLogTable.tsx",
    "content": "import React from 'react'\nimport { useParams } from 'react-router-dom'\nimport { DURATION_UNITS, DurationUnits, SortOrder } from 'uiSrc/constants'\nimport { convertNumberByUnits } from 'uiSrc/pages/slow-log/utils'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  Table,\n  ColumnDef,\n  SortingState,\n} from 'uiSrc/components/base/layout/table'\n\nimport { FormatedDate, RiTooltip } from 'uiSrc/components'\n\nimport { SlowLog } from 'apiSrc/modules/slow-log/models'\nimport { StyledTableWrapper } from './SlowLogTable.styles'\n\nexport const DATE_FORMAT = 'HH:mm:ss d LLL yyyy'\n\nexport interface Props {\n  items: SlowLog[]\n  loading: boolean\n  durationUnit: DurationUnits\n}\n\nconst SlowLogTable = (props: Props) => {\n  const { items = [], durationUnit } = props\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const columns: ColumnDef<SlowLog>[] = [\n    {\n      id: 'time',\n      header: 'Timestamp',\n      accessorKey: 'time',\n      size: 15,\n      cell: ({ getValue }) => {\n        const date = (getValue() as number) * 1000\n\n        return <FormatedDate date={date} />\n      },\n    },\n    {\n      id: 'durationUs',\n      header: `Duration, ${DURATION_UNITS.find(({ value }) => value === durationUnit)?.inputDisplay}`,\n      accessorKey: 'durationUs',\n      size: 15,\n      cell: ({ getValue }) => {\n        const duration = getValue() as number\n\n        return (\n          <Text size=\"s\" data-testid=\"duration-value\">\n            {numberWithSpaces(convertNumberByUnits(duration, durationUnit))}\n          </Text>\n        )\n      },\n    },\n    {\n      id: 'args',\n      header: 'Command',\n      accessorKey: 'args',\n      cell: ({ getValue }) => {\n        const command = getValue() as string\n\n        return (\n          <RiTooltip position=\"bottom\" content={command}>\n            <span data-testid=\"command-value\">{command}</span>\n          </RiTooltip>\n        )\n      },\n    },\n  ]\n\n  const handleSortingChange = (state: SortingState) => {\n    const { desc } = state[0] || { desc: true }\n    const order = desc ? SortOrder.DESC : SortOrder.ASC\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SLOWLOG_SORTED,\n      eventData: {\n        databaseId: instanceId,\n        timestamp: order,\n      },\n    })\n  }\n\n  return (\n    <StyledTableWrapper data-testid=\"slowlog-table\">\n      <Table\n        columns={columns}\n        data={items}\n        onSortingChange={handleSortingChange}\n        maxHeight=\"60vh\"\n        stripedRows\n      />\n    </StyledTableWrapper>\n  )\n}\n\nexport default SlowLogTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/SlowLogTable/index.ts",
    "content": "import SlowLogTable from './SlowLogTable'\n\nexport default SlowLogTable\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/components/index.ts",
    "content": "import SlowLogTable from './SlowLogTable'\nimport EmptySlowLog from './EmptySlowLog'\nimport Actions from './Actions'\n\nexport { SlowLogTable, EmptySlowLog, Actions }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/index.ts",
    "content": "import SlowLogPage from './SlowLogPage'\n\nexport default SlowLogPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/slow-log/utils.ts",
    "content": "import { DurationUnits } from 'uiSrc/constants'\n\n// convert from microSeconds\nexport const convertNumberByUnits = (\n  number: number,\n  unit: DurationUnits,\n): number => {\n  if (unit === DurationUnits.milliSeconds) {\n    return number / 1000\n  }\n  return number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/VectorSearchPageRouter.spec.tsx",
    "content": "import React from 'react'\nimport { BrowserRouter } from 'react-router-dom'\nimport { cleanup, render, screen } from 'uiSrc/utils/test-utils'\nimport { IRoute, Pages } from 'uiSrc/constants'\nimport { VectorSearchPageRouter } from './VectorSearchPageRouter'\nimport { useRedisInstanceCompatibility } from './hooks/useRedisInstanceCompatibility'\n\njest.mock('./hooks/useRedisInstanceCompatibility', () => ({\n  useRedisInstanceCompatibility: jest.fn(),\n}))\n\njest.mock('./hooks/useLastViewedPage', () => ({\n  useLastViewedPage: jest.fn(),\n}))\n\njest.mock('./context/vector-search', () => ({\n  VectorSearchProvider: ({ children }: { children: unknown }) => children,\n}))\n\nconst DummyPage = () => <div data-testid=\"dummy-page\">Dummy</div>\n\nconst routes: IRoute[] = [\n  {\n    path: Pages.vectorSearch(':instanceId'),\n    component: DummyPage,\n  },\n]\n\nconst renderComponent = () =>\n  render(\n    <BrowserRouter>\n      <VectorSearchPageRouter routes={routes} />\n    </BrowserRouter>,\n  )\n\ndescribe('VectorSearchPageRouter', () => {\n  const mockUseRedisInstanceCompatibility =\n    useRedisInstanceCompatibility as jest.Mock\n\n  beforeEach(() => {\n    cleanup()\n    mockUseRedisInstanceCompatibility.mockReturnValue({\n      loading: false,\n      hasRedisearch: true,\n      hasMinimumRedisearchVersion: true,\n      hasSupportedVersion: true,\n    })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render child routes when RediSearch is available', () => {\n    renderComponent()\n\n    expect(\n      screen.queryByTestId('vector-search-page--rqe-not-available'),\n    ).not.toBeInTheDocument()\n    expect(screen.queryByTestId('vector-search-loader')).not.toBeInTheDocument()\n  })\n\n  it('should render loader while compatibility is loading', () => {\n    mockUseRedisInstanceCompatibility.mockReturnValue({\n      loading: true,\n      hasRedisearch: undefined,\n      hasMinimumRedisearchVersion: undefined,\n      hasSupportedVersion: undefined,\n    })\n\n    renderComponent()\n\n    expect(screen.getByTestId('vector-search-loader')).toBeInTheDocument()\n  })\n\n  it('should render loader when compatibility is uninitialized', () => {\n    mockUseRedisInstanceCompatibility.mockReturnValue({\n      loading: undefined,\n      hasRedisearch: undefined,\n      hasMinimumRedisearchVersion: undefined,\n      hasSupportedVersion: undefined,\n    })\n\n    renderComponent()\n\n    expect(screen.getByTestId('vector-search-loader')).toBeInTheDocument()\n  })\n\n  it('should render version not supported when RediSearch version < 2.0', () => {\n    mockUseRedisInstanceCompatibility.mockReturnValue({\n      loading: false,\n      hasRedisearch: true,\n      hasMinimumRedisearchVersion: false,\n      hasSupportedVersion: false,\n    })\n\n    renderComponent()\n\n    expect(\n      screen.getByTestId('vector-search-page--version-not-supported'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('version-not-supported')).toBeInTheDocument()\n  })\n\n  it('should render version not supported when RediSearch is present but version < 2.0', () => {\n    mockUseRedisInstanceCompatibility.mockReturnValue({\n      loading: false,\n      hasRedisearch: true,\n      hasMinimumRedisearchVersion: false,\n      hasSupportedVersion: false,\n    })\n\n    renderComponent()\n\n    expect(\n      screen.getByTestId('vector-search-page--version-not-supported'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('vector-search-page--rqe-not-available'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render RQE not available when RediSearch module is missing but version is supported', () => {\n    mockUseRedisInstanceCompatibility.mockReturnValue({\n      loading: false,\n      hasRedisearch: false,\n      hasMinimumRedisearchVersion: true,\n      hasSupportedVersion: true,\n    })\n\n    renderComponent()\n\n    expect(\n      screen.getByTestId('vector-search-page--rqe-not-available'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('rqe-not-available')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/VectorSearchPageRouter.tsx",
    "content": "import React from 'react'\nimport { Switch } from 'react-router-dom'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\nimport { Loader } from 'uiSrc/components/base/display'\n\nimport { VectorSearchPageRouterProps } from './VectorSearchPageRouter.types'\nimport { VectorSearchProvider } from './context/vector-search'\nimport { useRedisInstanceCompatibility, useLastViewedPage } from './hooks'\nimport { RqeNotAvailable } from './components/rqe-not-available'\nimport { VersionNotSupported } from './components/version-not-supported'\nimport * as S from './pages/styles'\n\n/**\n * Router component for Vector Search pages.\n * Handles routing between main page, create index, and query pages.\n * Guards against unsupported Redis versions (< 6) and missing RediSearch module.\n * Wrapped with VectorSearchProvider to supply global context (modal, shared actions).\n */\nexport const VectorSearchPageRouter = ({\n  routes,\n}: VectorSearchPageRouterProps) => {\n  const { hasRedisearch, hasMinimumRedisearchVersion, loading } =\n    useRedisInstanceCompatibility()\n\n  useLastViewedPage()\n\n  if (loading !== false) {\n    return (\n      <S.PageWrapper\n        data-testid=\"vector-search-page--loading\"\n        align=\"center\"\n        justify=\"center\"\n      >\n        <Loader size=\"xl\" data-testid=\"vector-search-loader\" />\n      </S.PageWrapper>\n    )\n  }\n\n  if (hasMinimumRedisearchVersion === false) {\n    return (\n      <S.PageWrapper data-testid=\"vector-search-page--version-not-supported\">\n        <VersionNotSupported />\n      </S.PageWrapper>\n    )\n  }\n\n  if (hasRedisearch === false) {\n    return (\n      <S.PageWrapper data-testid=\"vector-search-page--rqe-not-available\">\n        <RqeNotAvailable />\n      </S.PageWrapper>\n    )\n  }\n\n  return (\n    <VectorSearchProvider>\n      <Switch>\n        {routes.map((route) => (\n          <RouteWithSubRoutes key={route.path} {...route} />\n        ))}\n      </Switch>\n    </VectorSearchProvider>\n  )\n}\n\nexport default React.memo(VectorSearchPageRouter)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/VectorSearchPageRouter.types.ts",
    "content": "import { IRoute } from 'uiSrc/constants'\n\nexport interface VectorSearchPageRouterProps {\n  routes: IRoute[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/.gitkeep",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/CommandView.constants.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\n/**\n * Monaco editor options for CommandView component.\n * Configures the editor to be read-only but allow text selection for copying.\n */\nexport const COMMAND_VIEW_EDITOR_OPTIONS: Partial<monacoEditor.editor.IStandaloneEditorConstructionOptions> =\n  {\n    // Make editor read-only to prevent editing\n    readOnly: true,\n\n    // Allow DOM interactions (text selection, copy) despite being read-only\n    domReadOnly: false,\n\n    // Disable right-click context menu\n    contextmenu: false,\n\n    // Hide the minimap (code overview on the right)\n    minimap: { enabled: false },\n\n    // Prevent scrolling past the last line\n    scrollBeyondLastLine: false,\n\n    // Disable sticky scroll header\n    stickyScroll: { enabled: false },\n\n    // Hide the overview ruler (selection/highlight indicators on the right edge)\n    overviewRulerLanes: 0,\n\n    // Don't highlight the current line where cursor is placed\n    renderLineHighlight: 'none',\n\n    // Disable code folding\n    folding: false,\n\n    // Hide vertical guide lines (indentation and bracket pair guides)\n    guides: {\n      indentation: false,\n      bracketPairs: false,\n    },\n\n    // Show scrollbars only when content overflows\n    scrollbar: {\n      vertical: 'auto' as const,\n      horizontal: 'auto' as const,\n    },\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/CommandView.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\n\nimport { render, screen, fireEvent, waitFor } from 'uiSrc/utils/test-utils'\n\nimport { CommandViewProps } from './CommandView.types'\n\njest.mock('uiSrc/components/base/code-editor', () => {\n  const React = require('react')\n  return {\n    __esModule: true,\n    CodeEditor: (props: any) =>\n      React.createElement(\n        'div',\n        {\n          'data-testid': props['data-testid'],\n          'data-language': props.language,\n          'data-theme': props.theme,\n          'data-readonly': props.options?.readOnly,\n          'data-linenumbers': props.options?.lineNumbers,\n        },\n        props.value,\n      ),\n  }\n})\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  handleCopy: jest.fn(),\n}))\n\nimport { handleCopy } from 'uiSrc/utils'\nimport { CommandView } from './CommandView'\n\nconst mockedHandleCopy = jest.mocked(handleCopy)\n\ndescribe('CommandView', () => {\n  const defaultProps: CommandViewProps = {\n    command: 'FT.CREATE idx:test ON HASH SCHEMA field TEXT',\n  }\n\n  const renderComponent = (propsOverride?: Partial<CommandViewProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(<CommandView {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render component with command and copy button', () => {\n    const command = faker.lorem.sentence()\n    renderComponent({ command })\n\n    const editor = screen.getByTestId('command-view--editor')\n    expect(editor).toBeInTheDocument()\n    expect(editor).toHaveTextContent(command)\n    expect(editor).toHaveAttribute('data-readonly', 'true')\n\n    const copyButton = screen.getByTestId('command-view--copy-button-btn')\n    expect(copyButton).toBeInTheDocument()\n  })\n\n  it('should copy command to clipboard and call onCopy callback when copy button is clicked', async () => {\n    const command = faker.lorem.sentence()\n    const onCopyMock = jest.fn()\n\n    renderComponent({ command, onCopy: onCopyMock })\n\n    const copyButton = screen.getByTestId('command-view--copy-button-btn')\n    fireEvent.click(copyButton)\n\n    expect(mockedHandleCopy).toHaveBeenCalledWith(command)\n    await waitFor(() => {\n      expect(onCopyMock).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  describe('language prop', () => {\n    it('should use default Redis language when not provided', () => {\n      renderComponent()\n\n      const editor = screen.getByTestId('command-view--editor')\n      expect(editor).toHaveAttribute('data-language', 'redisLanguage')\n    })\n\n    it('should use custom language when provided', () => {\n      const customLanguage = 'plaintext'\n      renderComponent({ language: customLanguage })\n\n      const editor = screen.getByTestId('command-view--editor')\n      expect(editor).toHaveAttribute('data-language', customLanguage)\n    })\n  })\n\n  it('should apply custom className', () => {\n    const customClassName = `test-${faker.string.alphanumeric(10)}`\n    const { container } = renderComponent({ className: customClassName })\n\n    const commandView = container.firstChild\n    expect(commandView).toHaveClass(customClassName)\n  })\n\n  it('should use custom data-testid when provided', () => {\n    const customTestId = faker.string.alphanumeric(10)\n    renderComponent({ dataTestId: customTestId })\n\n    const commandView = screen.getByTestId(customTestId)\n    expect(commandView).toBeInTheDocument()\n\n    const editor = screen.getByTestId(`${customTestId}--editor`)\n    expect(editor).toBeInTheDocument()\n\n    const copyButton = screen.getByTestId(`${customTestId}--copy-button-btn`)\n    expect(copyButton).toBeInTheDocument()\n  })\n\n  describe('showLineNumbers prop', () => {\n    it('should hide line numbers by default', () => {\n      renderComponent()\n\n      const editor = screen.getByTestId('command-view--editor')\n      expect(editor).toHaveAttribute('data-linenumbers', 'off')\n    })\n\n    it('should show line numbers when set to true', () => {\n      renderComponent({ showLineNumbers: true })\n\n      const editor = screen.getByTestId('command-view--editor')\n      expect(editor).toHaveAttribute('data-linenumbers', 'on')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/CommandView.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { useDispatch } from 'react-redux'\nimport { CommandView } from './index'\nimport { MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport { getRedisCommandsSuccess } from 'uiSrc/slices/app/redis-commands'\nimport { RiAccordion } from 'uiSrc/components/base/display/accordion/RiAccordion'\nimport MonacoEnvironmentInitializer from 'uiSrc/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer'\nimport MonacoLanguages from 'uiSrc/components/monaco-laguages'\n\n// Decorator to initialize Monaco environment and Redis commands for syntax highlighting\nconst withMonacoSetup = (Story: React.ComponentType) => {\n  const MonacoSetup = () => {\n    const dispatch = useDispatch()\n\n    React.useEffect(() => {\n      // Initialize Redis commands with mock data so MonacoLanguages can register the language\n      // @ts-ignore - MOCK_COMMANDS_SPEC has some type differences but works for Monaco language registration\n      dispatch(getRedisCommandsSuccess(MOCK_COMMANDS_SPEC))\n    }, [dispatch])\n\n    return (\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <MonacoEnvironmentInitializer />\n        <MonacoLanguages />\n        <Story />\n      </div>\n    )\n  }\n\n  return <MonacoSetup />\n}\n\nconst meta: Meta<typeof CommandView> = {\n  component: CommandView,\n  parameters: {},\n  tags: ['autodocs'],\n  decorators: [withMonacoSetup],\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 350,\n      },\n    },\n  },\n  args: {\n    command: `FT.CREATE idx:bikes_vss ON HASH PREFIX 1 \"bikes:\"\n    SCHEMA \"model\" TEXT NOSTEM SORTABLE \"brand\" TEXT NOSTEM SORTABLE\n    \"price\" NUMERIC SORTABLE \"type\" TAG`,\n  },\n}\n\nexport const WithLineNumbers: Story = {\n  name: 'With line numbers enabled',\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 350,\n      },\n    },\n  },\n  args: {\n    command: `FT.CREATE idx:bikes_vss\n    ON HASH\n        PREFIX 1 \"bikes:\"\n    SCHEMA\n      \"model\" TEXT NOSTEM SORTABLE\n      \"brand\" TEXT NOSTEM SORTABLE\n      \"price\" NUMERIC SORTABLE\n      \"type\" TAG\n      \"material\" TAG\n      \"weight\" NUMERIC SORTABLE\n      \"description_embeddings\" VECTOR \"FLAT\" 10\n        \"TYPE\" FLOAT32\n        \"DIM\" 768\n        \"DISTANCE_METRIC\" \"L2\"\n        \"INITIAL_CAP\" 111\n        \"BLOCK_SIZE\"  111`,\n    showLineNumbers: true,\n  },\n}\n\nexport const WithCopyCallback: Story = {\n  name: 'With onCopy callback',\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 350,\n      },\n    },\n  },\n  args: {\n    command: 'FT.CREATE idx:test ON HASH SCHEMA field TEXT',\n    onCopy: () => alert('Command copied to clipboard!'),\n  },\n}\n\nexport const FixedHeightWithScrolling: Story = {\n  name: 'Fixed height with inline scrolling (composition with an accordion)',\n  render: (args) => (\n    <div style={{ padding: '16px', width: '100%' }}>\n      <style>\n        {`\n          #ri-accordion-command-view-example [data-testid=\"ri-accordion-body-command-view-example\"] {\n            padding: 0;\n          }\n        `}\n      </style>\n      <RiAccordion\n        id=\"command-view-example\"\n        label=\"Create Index command\"\n        defaultOpen={true}\n      >\n        <div style={{ width: '100%', height: '200px' }}>\n          <CommandView {...args} />\n        </div>\n      </RiAccordion>\n    </div>\n  ),\n  args: {\n    command: `FT.CREATE idx:bikes_vss\n    ON HASH\n        PREFIX 1 \"bikes:\"\n    SCHEMA\n      \"model\" TEXT NOSTEM SORTABLE\n      \"brand\" TEXT NOSTEM SORTABLE\n      \"price\" NUMERIC SORTABLE\n      \"type\" TAG\n      \"material\" TAG\n      \"weight\" NUMERIC SORTABLE\n      \"description_embeddings\" VECTOR \"FLAT\" 10\n        \"TYPE\" FLOAT32\n        \"DIM\" 768\n        \"DISTANCE_METRIC\" \"L2\"\n        \"INITIAL_CAP\" 111\n        \"BLOCK_SIZE\"  111`,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/CommandView.styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const EditorWrapper = styled(FlexGroup)`\n  position: relative;\n  width: 100%;\n  height: 100%;\n  border: none;\n\n  /*\n   * Override Monaco CSS variables so the editor blends with the parent card.\n   * The global _monaco.scss applies \"background-color: var(--monacoBgColor) !important\"\n   * so redefining the variable here is the cleanest override.\n   */\n  --monacoBgColor: transparent;\n\n  /*\n   * Monaco injects --vscode-focusBorder directly on .monaco-editor via its\n   * theme stylesheet, so a parent-level override is ignored. We must target\n   * the .monaco-editor element itself to win the specificity battle.\n   */\n  .monaco-editor {\n    --vscode-focusBorder: transparent;\n  }\n`\n\nexport const CopyButtonWrapper = styled(FlexGroup)`\n  position: absolute;\n  top: ${({ theme }) => theme.core.space.space100};\n  right: ${({ theme }) => theme.core.space.space250};\n  z-index: 10;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/CommandView.tsx",
    "content": "import React, { useMemo } from 'react'\nimport { merge } from 'lodash'\n\nimport { MonacoLanguage } from 'uiSrc/constants'\nimport { defaultMonacoOptions } from 'uiSrc/constants/monaco/monaco'\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { CodeEditor } from 'uiSrc/components/base/code-editor'\n\nimport { CommandViewProps } from './CommandView.types'\nimport { COMMAND_VIEW_EDITOR_OPTIONS } from './CommandView.constants'\nimport * as S from './CommandView.styles'\n\nexport const CommandView = ({\n  command,\n  language = MonacoLanguage.Redis,\n  className,\n  dataTestId,\n  onCopy,\n  showLineNumbers = false,\n}: CommandViewProps) => {\n  const editorOptions = useMemo(\n    () =>\n      merge({}, defaultMonacoOptions, COMMAND_VIEW_EDITOR_OPTIONS, {\n        lineNumbers: showLineNumbers ? 'on' : 'off',\n      }),\n    [showLineNumbers],\n  )\n\n  return (\n    <S.EditorWrapper className={className} data-testid={dataTestId}>\n      <CodeEditor\n        language={language}\n        value={command}\n        options={editorOptions}\n        data-testid={`${dataTestId ?? 'command-view'}--editor`}\n      />\n      <S.CopyButtonWrapper>\n        <CopyButton\n          copy={command}\n          successLabel=\"Copied\"\n          onCopy={onCopy}\n          data-testid={`${dataTestId ?? 'command-view'}--copy-button`}\n          aria-label=\"Copy command\"\n        />\n      </S.CopyButtonWrapper>\n    </S.EditorWrapper>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/CommandView.types.ts",
    "content": "export interface CommandViewProps {\n  command: string\n  language?: string\n  showLineNumbers?: boolean\n\n  className?: string\n  dataTestId?: string\n\n  onCopy?: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/command-view/index.ts",
    "content": "export { CommandView } from './CommandView'\nexport type { CommandViewProps } from './CommandView.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/create-index-onboarding/CreateIndexOnboarding.constants.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { IndexingTypeContent } from '../field-type-list'\n\nexport enum CreateIndexOnboardingStep {\n  DefineIndex = 'defineIndex',\n  IndexPrefix = 'indexPrefix',\n  FieldName = 'fieldName',\n  SampleValue = 'sampleValue',\n  IndexingType = 'indexingType',\n  CommandView = 'commandView',\n}\n\nexport const ONBOARDING_STEPS = [\n  CreateIndexOnboardingStep.DefineIndex,\n  CreateIndexOnboardingStep.IndexPrefix,\n  CreateIndexOnboardingStep.FieldName,\n  CreateIndexOnboardingStep.SampleValue,\n  CreateIndexOnboardingStep.IndexingType,\n  CreateIndexOnboardingStep.CommandView,\n] as const\n\nexport const TOTAL_STEPS = ONBOARDING_STEPS.length\n\nexport interface StepContent {\n  title: string\n  body: React.ReactNode\n}\n\nexport const STEP_CONTENT: Record<CreateIndexOnboardingStep, StepContent> = {\n  [CreateIndexOnboardingStep.DefineIndex]: {\n    title: 'Review and adjust the indexing schema',\n    body: (\n      <>\n        <Text size=\"m\" color=\"secondary\">\n          An index defines how Redis searches and queries your data. The schema\n          controls which fields are indexed, their types, and other\n          configuration options.\n        </Text>\n        <Text size=\"m\" color=\"secondary\">\n          Review the suggested index name. You{'\\u2019'}ll use it when building\n          queries.\n        </Text>\n        <Text size=\"m\" color=\"secondary\">\n          Tip: Index only fields you plan to search or filter on.\n        </Text>\n      </>\n    ),\n  },\n  [CreateIndexOnboardingStep.IndexPrefix]: {\n    title: 'Index prefix',\n    body: (\n      <>\n        <Text size=\"m\" color=\"secondary\">\n          Controls which keys are included in the index. All keys starting with\n          this prefix will be indexed.\n        </Text>\n        <Text size=\"m\" color=\"secondary\">\n          Example: <strong>bike:</strong> will index <strong>bike:1</strong>,{' '}\n          <strong>bike:road:3</strong>.\n        </Text>\n      </>\n    ),\n  },\n  [CreateIndexOnboardingStep.FieldName]: {\n    title: 'Field name',\n    body: (\n      <Text size=\"m\" color=\"secondary\">\n        Represents a searchable attribute in your data. Only selected fields\n        will be searchable.\n      </Text>\n    ),\n  },\n  [CreateIndexOnboardingStep.SampleValue]: {\n    title: 'Sample value',\n    body: (\n      <Text size=\"m\" color=\"secondary\">\n        A sample value from the data to be indexed. Use it to verify the field\n        type and indexing choice.\n      </Text>\n    ),\n  },\n  [CreateIndexOnboardingStep.IndexingType]: {\n    title: 'Indexing type & options',\n    body: <IndexingTypeContent />,\n  },\n  [CreateIndexOnboardingStep.CommandView]: {\n    title: 'Create index command',\n    body: (\n      <Text size=\"m\" color=\"secondary\">\n        This is the FT.CREATE command Redis will run. Once executed, your data\n        becomes searchable.\n      </Text>\n    ),\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/create-index-onboarding/CreateIndexOnboardingPopover.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport {\n  CreateIndexOnboardingStep,\n  TOTAL_STEPS,\n} from './CreateIndexOnboarding.constants'\nimport {\n  CreateIndexOnboardingContext,\n  CreateIndexOnboardingContextValue,\n} from '../../context/create-index-onboarding'\nimport { CreateIndexOnboardingPopover } from './CreateIndexOnboardingPopover'\n\nconst mockNextStep = jest.fn()\nconst mockPrevStep = jest.fn()\nconst mockSkipOnboarding = jest.fn()\n\nconst defaultContextValue: CreateIndexOnboardingContextValue = {\n  currentStep: null,\n  isActive: false,\n  totalSteps: TOTAL_STEPS,\n  startOnboarding: jest.fn(),\n  nextStep: mockNextStep,\n  prevStep: mockPrevStep,\n  skipOnboarding: mockSkipOnboarding,\n}\n\ndescribe('CreateIndexOnboardingPopover', () => {\n  const renderComponent = (\n    step: CreateIndexOnboardingStep,\n    contextOverrides?: Partial<CreateIndexOnboardingContextValue>,\n  ) => {\n    const value = { ...defaultContextValue, ...contextOverrides }\n    return render(\n      <CreateIndexOnboardingContext.Provider value={value}>\n        <CreateIndexOnboardingPopover step={step}>\n          <button type=\"button\" data-testid=\"trigger\">\n            Trigger\n          </button>\n        </CreateIndexOnboardingPopover>\n      </CreateIndexOnboardingContext.Provider>,\n    )\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render only children when onboarding is not active', () => {\n    renderComponent(CreateIndexOnboardingStep.DefineIndex)\n\n    expect(screen.getByTestId('trigger')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId(\n        `create-index-onboarding-content-${CreateIndexOnboardingStep.DefineIndex}`,\n      ),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should show title, step counter, Next, Skip tour, and no Back on first step', () => {\n    renderComponent(CreateIndexOnboardingStep.DefineIndex, {\n      isActive: true,\n      currentStep: CreateIndexOnboardingStep.DefineIndex,\n    })\n\n    expect(\n      screen.getByText('Review and adjust the indexing schema'),\n    ).toBeInTheDocument()\n    expect(screen.getByText(`1/${TOTAL_STEPS}`)).toBeInTheDocument()\n\n    expect(\n      screen.queryByTestId('create-index-onboarding-back'),\n    ).not.toBeInTheDocument()\n\n    const nextButton = screen.getByTestId(\n      `create-index-onboarding-action-${CreateIndexOnboardingStep.DefineIndex}`,\n    )\n    expect(nextButton).toHaveTextContent('Next')\n    fireEvent.click(nextButton)\n    expect(mockNextStep).toHaveBeenCalledTimes(1)\n\n    expect(\n      screen.getByTestId('create-index-onboarding-skip'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('create-index-onboarding-close'),\n    ).not.toBeInTheDocument()\n\n    fireEvent.click(screen.getByTestId('create-index-onboarding-skip'))\n    expect(mockSkipOnboarding).toHaveBeenCalledTimes(1)\n  })\n\n  it('should show Back button and step counter on intermediate step', () => {\n    renderComponent(CreateIndexOnboardingStep.IndexPrefix, {\n      isActive: true,\n      currentStep: CreateIndexOnboardingStep.IndexPrefix,\n    })\n\n    expect(screen.getByText(`2/${TOTAL_STEPS}`)).toBeInTheDocument()\n\n    const backButton = screen.getByTestId('create-index-onboarding-back')\n    expect(backButton).toHaveTextContent('Back')\n    fireEvent.click(backButton)\n    expect(mockPrevStep).toHaveBeenCalledTimes(1)\n  })\n\n  it('should show Got it, close button, Back, and no Skip tour on last step', () => {\n    renderComponent(CreateIndexOnboardingStep.CommandView, {\n      isActive: true,\n      currentStep: CreateIndexOnboardingStep.CommandView,\n    })\n\n    const gotItButton = screen.getByTestId(\n      `create-index-onboarding-action-${CreateIndexOnboardingStep.CommandView}`,\n    )\n    expect(gotItButton).toHaveTextContent('Got it')\n    fireEvent.click(gotItButton)\n    expect(mockSkipOnboarding).toHaveBeenCalledTimes(1)\n\n    expect(\n      screen.getByTestId('create-index-onboarding-close'),\n    ).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('create-index-onboarding-skip'),\n    ).not.toBeInTheDocument()\n\n    expect(\n      screen.getByTestId('create-index-onboarding-back'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render indexing type content for IndexingType step', () => {\n    renderComponent(CreateIndexOnboardingStep.IndexingType, {\n      isActive: true,\n      currentStep: CreateIndexOnboardingStep.IndexingType,\n    })\n\n    expect(\n      screen.getByTestId('create-index-onboarding-indexing-types'),\n    ).toBeInTheDocument()\n    expect(screen.getByText('TEXT')).toBeInTheDocument()\n    expect(screen.getByText('VECTOR')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/create-index-onboarding/CreateIndexOnboardingPopover.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Content = styled(Col)`\n  max-width: 340px;\n`\n\nexport const StepCounter = styled(Row)`\n  min-width: 24px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/create-index-onboarding/CreateIndexOnboardingPopover.tsx",
    "content": "import React from 'react'\n\nimport { AnchorPosition, RiPopover } from 'uiSrc/components/base'\nimport {\n  Button,\n  EmptyButton,\n  IconButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { useCreateIndexOnboarding } from '../../context/create-index-onboarding'\nimport {\n  CreateIndexOnboardingStep,\n  ONBOARDING_STEPS,\n  STEP_CONTENT,\n  TOTAL_STEPS,\n} from './CreateIndexOnboarding.constants'\nimport * as S from './CreateIndexOnboardingPopover.styles'\n\nexport interface CreateIndexOnboardingPopoverProps {\n  step: CreateIndexOnboardingStep\n  children: React.ReactNode\n  anchorPosition?: AnchorPosition\n}\n\nexport const CreateIndexOnboardingPopover = ({\n  step,\n  children,\n  anchorPosition = 'rightCenter',\n}: CreateIndexOnboardingPopoverProps) => {\n  const { currentStep, isActive, nextStep, prevStep, skipOnboarding } =\n    useCreateIndexOnboarding()\n\n  const isCurrentStep = isActive && currentStep === step\n\n  if (!isCurrentStep) {\n    return <>{children}</>\n  }\n\n  const content = STEP_CONTENT[step]\n\n  if (!content) {\n    return <>{children}</>\n  }\n\n  const stepIndex = ONBOARDING_STEPS.indexOf(\n    step as (typeof ONBOARDING_STEPS)[number],\n  )\n  const isFirstStep = stepIndex === 0\n  const isLastStep = stepIndex === TOTAL_STEPS - 1\n  const stepNumber = stepIndex + 1\n\n  const handleAction = isLastStep ? skipOnboarding : nextStep\n  const actionLabel = isLastStep ? 'Got it' : 'Next'\n\n  return (\n    <div onClick={(e) => e.stopPropagation()} role=\"presentation\">\n      <RiPopover\n        isOpen\n        anchorPosition={anchorPosition}\n        data-testid={`create-index-onboarding-popover-${step}`}\n        trigger={children}\n      >\n        <S.Content\n          gap=\"l\"\n          data-testid={`create-index-onboarding-content-${step}`}\n        >\n          <Col gap=\"s\">\n            <Row justify=\"end\">\n              {isLastStep ? (\n                <IconButton\n                  icon={CancelSlimIcon}\n                  onClick={skipOnboarding}\n                  size=\"S\"\n                  aria-label=\"close-onboarding\"\n                  data-testid=\"create-index-onboarding-close\"\n                />\n              ) : (\n                <EmptyButton\n                  onClick={skipOnboarding}\n                  data-testid=\"create-index-onboarding-skip\"\n                >\n                  <Text size=\"m\" color=\"primary\">\n                    Skip tour\n                  </Text>\n                </EmptyButton>\n              )}\n            </Row>\n            <Text size=\"L\" variant=\"semiBold\" color=\"primary\">\n              {content.title}\n            </Text>\n          </Col>\n\n          <Col gap=\"s\">{content.body}</Col>\n\n          <Row justify=\"between\" align=\"center\">\n            <S.StepCounter>\n              <Text size=\"s\" color=\"secondary\">\n                {stepNumber}/{TOTAL_STEPS}\n              </Text>\n            </S.StepCounter>\n\n            <Row gap=\"m\" grow={false}>\n              {!isFirstStep && (\n                <SecondaryButton\n                  size=\"small\"\n                  onClick={prevStep}\n                  data-testid=\"create-index-onboarding-back\"\n                >\n                  Back\n                </SecondaryButton>\n              )}\n              <Button\n                size=\"small\"\n                onClick={handleAction}\n                data-testid={`create-index-onboarding-action-${step}`}\n              >\n                {actionLabel}\n              </Button>\n            </Row>\n          </Row>\n        </S.Content>\n      </RiPopover>\n    </div>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/create-index-onboarding/index.ts",
    "content": "export { CreateIndexOnboardingPopover } from './CreateIndexOnboardingPopover'\nexport type { CreateIndexOnboardingPopoverProps } from './CreateIndexOnboardingPopover'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/delete-confirmation-modal/DeleteConfirmationModal.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { DeleteConfirmationModal } from './DeleteConfirmationModal'\nimport { DeleteConfirmationModalProps } from './DeleteConfirmationModal.types'\n\ndescribe('DeleteConfirmationModal', () => {\n  const defaultProps: DeleteConfirmationModalProps = {\n    isOpen: true,\n    title: faker.lorem.words(2),\n    question: faker.lorem.sentence(),\n    message: faker.lorem.sentence(),\n    cancelLabel: faker.lorem.word(),\n    confirmLabel: faker.lorem.word(),\n    onConfirm: jest.fn(),\n    onCancel: jest.fn(),\n    testId: 'test-modal',\n  }\n\n  const renderComponent = (\n    propsOverride?: Partial<DeleteConfirmationModalProps>,\n  ) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<DeleteConfirmationModal {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should not render when isOpen is false', () => {\n    renderComponent({ isOpen: false })\n\n    const message = screen.queryByTestId('test-modal-message')\n    expect(message).not.toBeInTheDocument()\n  })\n\n  it('should render modal with title, question, message and action buttons', () => {\n    renderComponent()\n\n    const dialog = screen.getByRole('dialog', { name: defaultProps.title })\n    expect(dialog).toBeInTheDocument()\n\n    const question = screen.getByText(defaultProps.question)\n    expect(question).toBeInTheDocument()\n\n    const message = screen.getByText(defaultProps.message)\n    expect(message).toBeInTheDocument()\n\n    const cancelBtn = screen.getByTestId('test-modal-cancel')\n    expect(cancelBtn).toHaveTextContent(defaultProps.cancelLabel)\n\n    const confirmBtn = screen.getByTestId('test-modal-confirm')\n    expect(confirmBtn).toHaveTextContent(defaultProps.confirmLabel)\n  })\n\n  it('should call onConfirm when confirm button is clicked', () => {\n    renderComponent()\n\n    const confirmBtn = screen.getByTestId('test-modal-confirm')\n    fireEvent.click(confirmBtn)\n\n    expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when cancel button is clicked', () => {\n    renderComponent()\n\n    const cancelBtn = screen.getByTestId('test-modal-cancel')\n    fireEvent.click(cancelBtn)\n\n    expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when close icon is clicked', () => {\n    renderComponent()\n\n    const closeBtn = screen.getByTestId('test-modal-close')\n    fireEvent.click(closeBtn)\n\n    expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)\n  })\n\n  it('should use default testId when none is provided', () => {\n    renderComponent({ testId: undefined })\n\n    const message = screen.getByTestId('delete-confirmation-modal-message')\n    expect(message).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/delete-confirmation-modal/DeleteConfirmationModal.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { DeleteConfirmationModal } from './DeleteConfirmationModal'\n\nconst meta: Meta<typeof DeleteConfirmationModal> = {\n  component: DeleteConfirmationModal,\n  tags: ['autodocs'],\n  parameters: {\n    docs: {\n      description: {\n        component:\n          'Reusable confirmation modal for destructive actions such as deleting an index or a saved query. All text, labels, and callbacks are configurable via props.',\n      },\n    },\n  },\n  argTypes: {\n    isOpen: {\n      description: 'Controls whether the modal is visible.',\n      control: 'boolean',\n    },\n    title: {\n      description: 'Modal heading text.',\n      control: 'text',\n    },\n    question: {\n      description: 'Secondary-colored question shown below the title.',\n      control: 'text',\n    },\n    message: {\n      description: 'Bold primary-colored message explaining consequences.',\n      control: 'text',\n    },\n    cancelLabel: {\n      description: 'Label for the secondary cancel button.',\n      control: 'text',\n    },\n    confirmLabel: {\n      description: 'Label for the destructive confirm button.',\n      control: 'text',\n    },\n    testId: {\n      description: 'Prefix for data-testid attributes.',\n      control: 'text',\n    },\n    onConfirm: {\n      description: 'Called when the confirm button is clicked.',\n      action: 'onConfirm',\n    },\n    onCancel: {\n      description:\n        'Called when cancel button, close icon, or backdrop is clicked.',\n      action: 'onCancel',\n    },\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const DeleteIndex: Story = {\n  args: {\n    isOpen: true,\n    title: 'Delete Index',\n    question: 'Are you sure you want to delete this index?',\n    message:\n      'Deleting the index will remove it from Search and Vector Search, but will not delete your underlying data.',\n    cancelLabel: 'Keep index',\n    confirmLabel: 'Delete index',\n    testId: 'delete-index-modal',\n  },\n  parameters: {\n    docs: {\n      description: {\n        story: 'Used when confirming deletion of a search index.',\n      },\n    },\n  },\n}\n\nexport const DeleteQuery: Story = {\n  args: {\n    isOpen: true,\n    title: 'Delete query',\n    question: 'Are you sure you want to delete this query?',\n    message:\n      \"This action will remove the saved query, but won't affect your index or data.\",\n    cancelLabel: 'Keep query',\n    confirmLabel: 'Delete query',\n    testId: 'query-library-delete-modal',\n  },\n  parameters: {\n    docs: {\n      description: {\n        story:\n          'Used when confirming deletion of a saved query from the library.',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/delete-confirmation-modal/DeleteConfirmationModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display/modal'\n\nexport const ModalContent = styled(Modal.Content.Compose)`\n  width: 600px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/delete-confirmation-modal/DeleteConfirmationModal.tsx",
    "content": "import React from 'react'\n\nimport { Modal } from 'uiSrc/components/base/display'\nimport { Text } from 'uiSrc/components/base/text'\nimport {\n  SecondaryButton,\n  DestructiveButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nimport { DeleteConfirmationModalProps } from './DeleteConfirmationModal.types'\nimport * as S from './DeleteConfirmationModal.styles'\n\nconst DEFAULT_TEST_ID = 'delete-confirmation-modal'\n\nexport const DeleteConfirmationModal = ({\n  isOpen,\n  title,\n  question,\n  message,\n  cancelLabel,\n  confirmLabel,\n  onConfirm,\n  onCancel,\n  testId = DEFAULT_TEST_ID,\n}: DeleteConfirmationModalProps) => {\n  if (!isOpen) return null\n\n  return (\n    <Modal.Compose open={isOpen}>\n      <S.ModalContent persistent onCancel={onCancel}>\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={onCancel}\n          data-testid={`${testId}-close`}\n        />\n\n        <Modal.Content.Header.Compose>\n          <Modal.Content.Header.Title>{title}</Modal.Content.Header.Title>\n        </Modal.Content.Header.Compose>\n\n        <Col gap=\"m\" data-testid={`${testId}-message`}>\n          <Text color=\"secondary\">{question}</Text>\n          <Text color=\"primary\" variant=\"semiBold\">\n            {message}\n          </Text>\n          <Spacer size=\"xl\" />\n        </Col>\n\n        <Row justify=\"end\" gap=\"m\">\n          <SecondaryButton\n            size=\"large\"\n            onClick={onCancel}\n            data-testid={`${testId}-cancel`}\n          >\n            {cancelLabel}\n          </SecondaryButton>\n          <DestructiveButton\n            size=\"large\"\n            onClick={onConfirm}\n            data-testid={`${testId}-confirm`}\n          >\n            {confirmLabel}\n          </DestructiveButton>\n        </Row>\n      </S.ModalContent>\n    </Modal.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/delete-confirmation-modal/DeleteConfirmationModal.types.ts",
    "content": "export interface DeleteConfirmationModalProps {\n  isOpen: boolean\n  title: string\n  question: string\n  message: string\n  cancelLabel: string\n  confirmLabel: string\n  onConfirm: () => void\n  onCancel: () => void\n  testId?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/delete-confirmation-modal/index.ts",
    "content": "export { DeleteConfirmationModal } from './DeleteConfirmationModal'\nexport type { DeleteConfirmationModalProps } from './DeleteConfirmationModal.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-tag/FieldTag.tsx",
    "content": "import React from 'react'\n\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { FIELD_TYPE_OPTIONS } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport { FieldTagProps } from './FieldTag.types'\nimport { FIELD_TYPE_BADGE_VARIANT_MAP } from './constants'\n\nexport const FieldTag = ({ tag, dataTestId }: FieldTagProps) => {\n  const tagLabel = FIELD_TYPE_OPTIONS.find(\n    (option) => option.value === tag,\n  )?.text\n\n  return tagLabel ? (\n    <RiBadge\n      label={tagLabel}\n      data-testid={dataTestId ?? 'field-tag'}\n      variant={FIELD_TYPE_BADGE_VARIANT_MAP[tag]}\n    />\n  ) : null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-tag/FieldTag.types.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nexport interface FieldTagProps {\n  tag: FieldTypes\n  dataTestId?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-tag/constants.ts",
    "content": "import { BadgeVariants } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nexport const FIELD_TYPE_BADGE_VARIANT_MAP: Record<FieldTypes, BadgeVariants> = {\n  [FieldTypes.TAG]: 'notice',\n  [FieldTypes.TEXT]: 'informative',\n  [FieldTypes.NUMERIC]: 'attention',\n  [FieldTypes.VECTOR]: 'success',\n  [FieldTypes.GEO]: 'default',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-list/FieldTypeList.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const FieldTypeRow = styled(Row)`\n  white-space: nowrap;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-list/FieldTypeList.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { FieldTag } from 'uiSrc/pages/vector-search/components/field-tag/FieldTag'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport * as S from './FieldTypeList.styles'\n\nconst FIELD_TYPE_DESCRIPTIONS: { type: FieldTypes; description: string }[] = [\n  {\n    type: FieldTypes.TEXT,\n    description: 'Full-text search and relevance scoring',\n  },\n  { type: FieldTypes.TAG, description: 'Exact matching and filtering' },\n  { type: FieldTypes.NUMERIC, description: 'Range queries and sorting' },\n  {\n    type: FieldTypes.GEO,\n    description: 'Geographic distance and radius queries',\n  },\n  { type: FieldTypes.VECTOR, description: 'Similarity and semantic search' },\n]\n\nexport const IndexingTypeContent = () => (\n  <Col gap=\"m\" data-testid=\"create-index-onboarding-indexing-types\">\n    <Text size=\"m\" color=\"secondary\">\n      Defines how Redis searches this field and how it behaves at query time.\n      Available indexing types:\n    </Text>\n\n    {FIELD_TYPE_DESCRIPTIONS.map(({ type, description }) => (\n      <S.FieldTypeRow key={type} gap=\"s\">\n        <FieldTag tag={type} />\n        <Text>{description}</Text>\n      </S.FieldTypeRow>\n    ))}\n\n    <Text size=\"m\" color=\"secondary\">\n      Optional settings may affect performance, storage, or ranking.\n    </Text>\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-list/index.ts",
    "content": "export { IndexingTypeContent } from './FieldTypeList'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.constants.ts",
    "content": "import {\n  VectorAlgorithm,\n  VectorDataType,\n  VectorDistanceMetric,\n} from '../index-details/IndexDetails.types'\n\nexport const VECTOR_CONSTRAINTS = {\n  DIMENSIONS_MIN: 1,\n  DIMENSIONS_MAX: 32768,\n  DIMENSIONS_DEFAULT: 384,\n  MAX_EDGES_MIN: 1,\n  MAX_EDGES_MAX: 512,\n  MAX_EDGES_DEFAULT: 16,\n  MAX_NEIGHBORS_MIN: 1,\n  MAX_NEIGHBORS_MAX: 4096,\n  MAX_NEIGHBORS_DEFAULT: 200,\n  CANDIDATE_LIMIT_MIN: 1,\n  CANDIDATE_LIMIT_MAX: 4096,\n  CANDIDATE_LIMIT_DEFAULT: 10,\n  EPSILON_MIN: 0,\n  EPSILON_DEFAULT: 0.01,\n}\n\nexport const TEXT_CONSTRAINTS = {\n  WEIGHT_DEFAULT: 1,\n  WEIGHT_MIN: 0,\n}\n\nexport const VECTOR_ALGORITHM_DEFAULT = VectorAlgorithm.FLAT\nexport const VECTOR_DISTANCE_METRIC_DEFAULT = VectorDistanceMetric.COSINE\nexport const VECTOR_DATA_TYPE_DEFAULT = VectorDataType.FLOAT32\n\nexport const MAX_SAMPLE_VALUE_LENGTH = 500\n\nexport const MIN_RQE_VERSION_FLOAT16 = '2.10.0'\n\nexport const VECTOR_DATA_TYPE_BASE_OPTIONS = [\n  { value: VectorDataType.FLOAT32, label: 'FLOAT32' },\n  { value: VectorDataType.FLOAT64, label: 'FLOAT64' },\n]\n\nexport const VECTOR_DATA_TYPE_FLOAT16_OPTIONS = [\n  { value: VectorDataType.BFLOAT16, label: 'BFLOAT16' },\n  { value: VectorDataType.FLOAT16, label: 'FLOAT16' },\n]\n\nexport const PHONETIC_NONE = 'none'\n\nexport const PHONETIC_OPTIONS = [\n  { value: PHONETIC_NONE, label: 'None' },\n  { value: 'dm:en', label: 'English (dm:en)' },\n  { value: 'dm:fr', label: 'French (dm:fr)' },\n  { value: 'dm:pt', label: 'Portuguese (dm:pt)' },\n  { value: 'dm:es', label: 'Spanish (dm:es)' },\n]\n\nexport const VECTOR_ALGORITHM_OPTIONS = [\n  { value: VectorAlgorithm.FLAT, label: 'FLAT' },\n  { value: VectorAlgorithm.HNSW, label: 'HNSW' },\n]\n\nexport const VECTOR_DISTANCE_METRIC_OPTIONS = [\n  { value: VectorDistanceMetric.L2, label: 'L2' },\n  { value: VectorDistanceMetric.IP, label: 'IP' },\n  { value: VectorDistanceMetric.COSINE, label: 'COSINE' },\n]\n\nexport const VALIDATION_MESSAGES = {\n  FIELD_NAME_REQUIRED: 'Field name is required.',\n  FIELD_NAME_DUPLICATE: 'A field with this name already exists.',\n  DIMENSIONS_REQUIRED: 'Dimensions value is required.',\n  DIMENSIONS_RANGE:\n    `Dimensions must be between` +\n    ` ${VECTOR_CONSTRAINTS.DIMENSIONS_MIN}` +\n    ` and ${VECTOR_CONSTRAINTS.DIMENSIONS_MAX}.`,\n  MAX_EDGES_RANGE:\n    `Max edges must be between` +\n    ` ${VECTOR_CONSTRAINTS.MAX_EDGES_MIN}` +\n    ` and ${VECTOR_CONSTRAINTS.MAX_EDGES_MAX}.`,\n  MAX_NEIGHBORS_RANGE:\n    `Max neighbors must be between` +\n    ` ${VECTOR_CONSTRAINTS.MAX_NEIGHBORS_MIN}` +\n    ` and ${VECTOR_CONSTRAINTS.MAX_NEIGHBORS_MAX}.`,\n  CANDIDATE_LIMIT_RANGE:\n    `Candidate limit must be between` +\n    ` ${VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_MIN}` +\n    ` and ${VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_MAX}.`,\n  EPSILON_MIN: `Epsilon must be ${VECTOR_CONSTRAINTS.EPSILON_MIN} or greater.`,\n  WEIGHT_MIN: 'Weight must be greater than 0.',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.spec.tsx",
    "content": "import React from 'react'\nimport { act } from 'react-dom/test-utils'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, fireEvent, waitFor } from 'uiSrc/utils/test-utils'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  IndexField,\n  VectorAlgorithm,\n  VectorDistanceMetric,\n} from '../index-details/IndexDetails.types'\nimport { FieldTypeModal } from './FieldTypeModal'\nimport { FieldTypeModalMode, FieldTypeModalProps } from './FieldTypeModal.types'\n\nconst mockOnSubmit = jest.fn()\nconst mockOnClose = jest.fn()\n\nconst existingFields: IndexField[] = [\n  {\n    id: 'title',\n    name: 'title',\n    value: faker.lorem.words(3),\n    type: FieldTypes.TEXT,\n  },\n  {\n    id: 'embedding',\n    name: 'embedding',\n    value: '[0.1, 0.2]',\n    type: FieldTypes.VECTOR,\n    options: {\n      algorithm: VectorAlgorithm.HNSW,\n      dimensions: 768,\n      distanceMetric: VectorDistanceMetric.COSINE,\n    },\n  },\n]\n\nconst defaultProps: FieldTypeModalProps = {\n  isOpen: true,\n  mode: FieldTypeModalMode.Create,\n  fields: existingFields,\n  onSubmit: mockOnSubmit,\n  onClose: mockOnClose,\n}\n\nconst renderComponent = async (\n  propsOverride?: Partial<FieldTypeModalProps>,\n) => {\n  const props = { ...defaultProps, ...propsOverride }\n\n  let result: ReturnType<typeof render>\n  await act(async () => {\n    result = render(<FieldTypeModal {...props} />)\n  })\n\n  return result!\n}\n\ndescribe('FieldTypeModal', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render nothing when isOpen is false', async () => {\n    await renderComponent({ isOpen: false })\n\n    expect(\n      screen.queryByTestId('field-type-modal-form'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render modal in create mode with field name input', async () => {\n    await renderComponent()\n\n    expect(\n      screen.getByTestId('field-type-modal-field-name'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('field-type-modal-save')).toBeInTheDocument()\n    expect(screen.getByTestId('field-type-modal-cancel')).toBeInTheDocument()\n  })\n\n  it('should render modal in edit mode with readonly field info', async () => {\n    await renderComponent({\n      mode: FieldTypeModalMode.Edit,\n      field: existingFields[0],\n    })\n\n    expect(\n      screen.getByTestId('field-type-modal-field-name-readonly'),\n    ).toBeInTheDocument()\n    expect(screen.getByText('title')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('field-type-modal-field-name'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should disable Add button when field name is empty in create mode', async () => {\n    await renderComponent()\n\n    await waitFor(() => {\n      expect(screen.getByTestId('field-type-modal-save')).toBeDisabled()\n    })\n  })\n\n  it('should call onClose when Cancel button is clicked', async () => {\n    await renderComponent()\n\n    fireEvent.click(screen.getByTestId('field-type-modal-cancel'))\n\n    expect(mockOnClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should show Add button text in create mode and Save in edit mode', async () => {\n    const { unmount } = await renderComponent()\n    expect(screen.getByTestId('field-type-modal-save')).toHaveTextContent('Add')\n    unmount()\n\n    await renderComponent({\n      mode: FieldTypeModalMode.Edit,\n      field: existingFields[0],\n    })\n    expect(screen.getByTestId('field-type-modal-save')).toHaveTextContent(\n      'Save',\n    )\n  })\n\n  it('should show description text', async () => {\n    await renderComponent()\n\n    expect(\n      screen.getByText(/You can change the field type/),\n    ).toBeInTheDocument()\n  })\n\n  it('should render section header for TEXT type by default', async () => {\n    await renderComponent()\n\n    expect(\n      screen.getByTestId('field-type-modal-section-header'),\n    ).toHaveTextContent('TEXT options')\n  })\n\n  it('should enable Add button after entering a valid field name', async () => {\n    await renderComponent()\n\n    const input = screen.getByTestId('field-type-modal-field-name')\n    fireEvent.change(input, { target: { value: 'newField' } })\n\n    await waitFor(() => {\n      expect(screen.getByTestId('field-type-modal-save')).not.toBeDisabled()\n    })\n  })\n\n  it('should show sample value in edit mode', async () => {\n    const field = { ...existingFields[0], value: 'sample text' }\n    await renderComponent({\n      mode: FieldTypeModalMode.Edit,\n      field,\n    })\n\n    expect(\n      screen.getByTestId('field-type-modal-sample-value'),\n    ).toBeInTheDocument()\n    expect(screen.getByText('sample text')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  IndexField,\n  VectorAlgorithm,\n  VectorDistanceMetric,\n} from '../index-details/IndexDetails.types'\nimport { FieldTypeModal } from './FieldTypeModal'\nimport { FieldTypeModalMode, FieldTypeModalProps } from './FieldTypeModal.types'\n\nconst sampleVectorField: IndexField = {\n  id: 'embedding',\n  name: 'embedding',\n  value: '[0.12, 0.34, ...]',\n  type: FieldTypes.VECTOR,\n  options: {\n    algorithm: VectorAlgorithm.HNSW,\n    dimensions: 768,\n    distanceMetric: VectorDistanceMetric.COSINE,\n    maxEdges: 16,\n    maxNeighbors: 200,\n    candidateLimit: 10,\n    epsilon: 0.01,\n  },\n}\n\nconst sampleTextField: IndexField = {\n  id: 'title',\n  name: 'title',\n  value: 'Sample product title',\n  type: FieldTypes.TEXT,\n  options: {\n    weight: 2,\n    phonetic: 'dm:en',\n  },\n}\n\nconst existingFields: IndexField[] = [sampleVectorField, sampleTextField]\n\nconst FieldTypeModalWithState = (\n  props: Omit<FieldTypeModalProps, 'isOpen' | 'onSubmit' | 'onClose'>,\n) => {\n  const [isOpen, setIsOpen] = useState(true)\n\n  return (\n    <>\n      {!isOpen && (\n        <button type=\"button\" onClick={() => setIsOpen(true)}>\n          Open modal\n        </button>\n      )}\n      <FieldTypeModal\n        {...props}\n        isOpen={isOpen}\n        onSubmit={(field) => {\n          // eslint-disable-next-line no-console\n          console.log('Submitted field:', field)\n          setIsOpen(false)\n        }}\n        onClose={() => setIsOpen(false)}\n      />\n    </>\n  )\n}\n\nconst meta: Meta<typeof FieldTypeModal> = {\n  component: FieldTypeModal,\n  argTypes: {\n    isOpen: { description: 'Controls modal visibility' },\n    mode: {\n      description: 'Create or Edit mode',\n      control: 'select',\n      options: Object.values(FieldTypeModalMode),\n    },\n    field: { description: 'Field to edit (edit mode only)' },\n    fields: { description: 'Existing fields for duplicate name validation' },\n    onSubmit: { description: 'Called with the new/updated IndexField' },\n    onClose: { description: 'Called when modal is closed' },\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const CreateMode: Story = {\n  render: () => (\n    <FieldTypeModalWithState\n      mode={FieldTypeModalMode.Create}\n      fields={existingFields}\n    />\n  ),\n}\n\nexport const EditVectorField: Story = {\n  name: 'Edit — Vector (HNSW)',\n  render: () => (\n    <FieldTypeModalWithState\n      mode={FieldTypeModalMode.Edit}\n      field={sampleVectorField}\n      fields={existingFields}\n    />\n  ),\n}\n\nexport const EditTextField: Story = {\n  name: 'Edit — Text',\n  render: () => (\n    <FieldTypeModalWithState\n      mode={FieldTypeModalMode.Edit}\n      field={sampleTextField}\n      fields={existingFields}\n    />\n  ),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display/modal'\nimport { Text } from 'uiSrc/components/base/text'\nimport { CopyButton } from 'uiSrc/components/copy-button'\n\nexport const ModalContent = styled(Modal.Content.Compose)`\n  width: 640px;\n  max-width: calc(100vw - 120px);\n  max-height: calc(100vh - 120px);\n`\n\nexport const ModalBody = styled(Modal.Content.Body)`\n  flex: 1;\n  min-height: 0;\n  overflow-y: auto;\n`\n\nexport const FieldValue = styled(Text)`\n  overflow-wrap: anywhere;\n`\n\nexport const InlineCopyButton = styled(CopyButton)`\n  display: inline-flex;\n  vertical-align: middle;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.tsx",
    "content": "import React, { useCallback, useMemo } from 'react'\nimport { useFormik } from 'formik'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { Modal } from 'uiSrc/components/base/display'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { truncateText } from 'uiSrc/utils'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport TextInput from 'uiSrc/components/base/inputs/TextInput'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\n\nimport { MAX_SAMPLE_VALUE_LENGTH } from './FieldTypeModal.constants'\nimport { FieldTypeModalMode, FieldTypeModalProps } from './FieldTypeModal.types'\nimport { FieldTypeSelect } from './components/FieldTypeSelect/FieldTypeSelect'\nimport { FieldTypeForm } from './components/FieldTypeForm/FieldTypeForm'\nimport { FieldTypeFormValues } from './components/FieldTypeForm/FieldTypeForm.types'\nimport { useFieldTypeValidation } from './hooks/useFieldTypeValidation'\nimport { getInitialValues, buildFieldFromValues } from './FieldTypeModal.utils'\nimport * as S from './FieldTypeModal.styles'\nimport { Spacer } from 'uiSrc/components/base/layout'\n\nexport const FieldTypeModal = ({\n  isOpen,\n  mode,\n  field,\n  fields,\n  onSubmit,\n  onClose,\n}: FieldTypeModalProps) => {\n  const validate = useFieldTypeValidation(mode, fields, field)\n\n  const initialValues = useMemo(\n    () => getInitialValues(mode, field),\n    [mode, field],\n  )\n\n  const formik = useFormik<FieldTypeFormValues>({\n    initialValues,\n    validate,\n    enableReinitialize: true,\n    validateOnMount: true,\n    onSubmit: (values, { resetForm }) => {\n      onSubmit(buildFieldFromValues(values, mode, field))\n      resetForm()\n    },\n  })\n\n  const handleFieldTypeChange = useCallback(\n    (newType: FieldTypes) => {\n      formik.setFieldValue('fieldType', newType)\n    },\n    [formik],\n  )\n\n  const handleClose = useCallback(() => {\n    formik.resetForm()\n    onClose()\n  }, [formik, onClose])\n\n  const isCreateMode = mode === FieldTypeModalMode.Create\n  const title = isCreateMode ? 'Add field' : 'Edit field'\n\n  if (!isOpen) return null\n\n  return (\n    <Modal.Compose open={isOpen}>\n      <S.ModalContent persistent onCancel={handleClose}>\n        <Modal.Content.Close icon={CancelIcon} onClick={handleClose} />\n        <Modal.Content.Header.Compose>\n          <Modal.Content.Header.Title>{title}</Modal.Content.Header.Title>\n        </Modal.Content.Header.Compose>\n\n        <S.ModalBody\n          content={\n            <Col gap=\"l\" data-testid=\"field-type-modal-form\">\n              {isCreateMode ? (\n                <FormField label=\"Field name\" required>\n                  <TextInput\n                    value={formik.values.fieldName}\n                    onChange={(value: string) =>\n                      formik.setFieldValue('fieldName', value)\n                    }\n                    onBlur={formik.handleBlur}\n                    name=\"fieldName\"\n                    placeholder=\"Enter field name\"\n                    error={\n                      formik.touched.fieldName\n                        ? formik.errors.fieldName\n                        : undefined\n                    }\n                    data-testid=\"field-type-modal-field-name\"\n                  />\n                </FormField>\n              ) : (\n                <>\n                  <Col\n                    gap=\"s\"\n                    data-testid=\"field-type-modal-field-name-readonly\"\n                  >\n                    <Text color=\"secondary\" component=\"span\">\n                      Field name:\n                    </Text>\n                    <S.FieldValue color=\"primary\" component=\"span\">\n                      {field?.name}\n                    </S.FieldValue>\n                  </Col>\n                  <Col gap=\"s\" data-testid=\"field-type-modal-sample-value\">\n                    <Text color=\"secondary\" component=\"span\">\n                      Field sample value:\n                    </Text>\n                    <S.FieldValue color=\"primary\" component=\"span\">\n                      {truncateText(\n                        String(field?.value ?? ''),\n                        MAX_SAMPLE_VALUE_LENGTH,\n                      )}\n                      {field?.value != null &&\n                        String(field.value).length >=\n                          MAX_SAMPLE_VALUE_LENGTH && (\n                          <S.InlineCopyButton\n                            copy={String(field.value)}\n                            data-testid=\"field-type-modal-sample-value-copy\"\n                          />\n                        )}\n                    </S.FieldValue>\n                  </Col>\n                </>\n              )}\n\n              <Text color=\"secondary\">\n                You can change the field type for this field. Keep in mind that\n                changing the field type will affect how the field is indexed and\n                queried.\n              </Text>\n\n              <FieldTypeSelect\n                value={formik.values.fieldType}\n                onChange={handleFieldTypeChange}\n                dataTestId=\"field-type-modal-field-type\"\n              />\n\n              <Spacer size=\"xs\" />\n              <FieldTypeForm formik={formik} />\n            </Col>\n          }\n        />\n\n        <Modal.Content.Footer.Compose>\n          <Row gap=\"m\" justify=\"end\">\n            <SecondaryButton\n              size=\"l\"\n              onClick={handleClose}\n              data-testid=\"field-type-modal-cancel\"\n            >\n              Cancel\n            </SecondaryButton>\n            <PrimaryButton\n              size=\"l\"\n              onClick={() => formik.handleSubmit()}\n              disabled={!formik.isValid}\n              data-testid=\"field-type-modal-save\"\n            >\n              {isCreateMode ? 'Add' : 'Save'}\n            </PrimaryButton>\n          </Row>\n        </Modal.Content.Footer.Compose>\n      </S.ModalContent>\n    </Modal.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.types.ts",
    "content": "import { IndexField } from '../index-details/IndexDetails.types'\n\nexport enum FieldTypeModalMode {\n  Create = 'create',\n  Edit = 'edit',\n}\n\nexport interface FieldTypeModalProps {\n  isOpen: boolean\n  mode: FieldTypeModalMode\n  field?: IndexField\n  fields: IndexField[]\n  onSubmit: (field: IndexField) => void\n  onClose: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/FieldTypeModal.utils.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  IndexField,\n  VectorAlgorithm,\n  VectorFieldOptions as VectorFieldOptionsType,\n  TextFieldOptions as TextFieldOptionsType,\n} from '../index-details/IndexDetails.types'\nimport { FieldTypeModalMode } from './FieldTypeModal.types'\nimport {\n  VECTOR_ALGORITHM_DEFAULT,\n  VECTOR_DATA_TYPE_DEFAULT,\n  VECTOR_DISTANCE_METRIC_DEFAULT,\n  VECTOR_CONSTRAINTS,\n  TEXT_CONSTRAINTS,\n} from './FieldTypeModal.constants'\nimport { FieldTypeFormValues } from './components/FieldTypeForm/FieldTypeForm.types'\n\nexport const getInitialValues = (\n  mode: FieldTypeModalMode,\n  field?: IndexField,\n): FieldTypeFormValues => {\n  if (mode === FieldTypeModalMode.Edit && field) {\n    const vectorOptions = field.options as VectorFieldOptionsType | undefined\n    const textOptions = field.options as TextFieldOptionsType | undefined\n\n    return {\n      fieldName: field.name,\n      fieldType: field.type,\n      algorithm:\n        field.type === FieldTypes.VECTOR &&\n        vectorOptions &&\n        'algorithm' in vectorOptions\n          ? vectorOptions.algorithm\n          : VECTOR_ALGORITHM_DEFAULT,\n      dataType:\n        field.type === FieldTypes.VECTOR && vectorOptions\n          ? (vectorOptions.dataType ?? VECTOR_DATA_TYPE_DEFAULT)\n          : VECTOR_DATA_TYPE_DEFAULT,\n      dimensions:\n        field.type === FieldTypes.VECTOR && vectorOptions\n          ? vectorOptions.dimensions\n          : VECTOR_CONSTRAINTS.DIMENSIONS_DEFAULT,\n      distanceMetric:\n        field.type === FieldTypes.VECTOR && vectorOptions\n          ? (vectorOptions.distanceMetric ?? VECTOR_DISTANCE_METRIC_DEFAULT)\n          : VECTOR_DISTANCE_METRIC_DEFAULT,\n      maxEdges:\n        field.type === FieldTypes.VECTOR &&\n        vectorOptions &&\n        'maxEdges' in vectorOptions\n          ? vectorOptions.maxEdges\n          : VECTOR_CONSTRAINTS.MAX_EDGES_DEFAULT,\n      maxNeighbors:\n        field.type === FieldTypes.VECTOR &&\n        vectorOptions &&\n        'maxNeighbors' in vectorOptions\n          ? vectorOptions.maxNeighbors\n          : VECTOR_CONSTRAINTS.MAX_NEIGHBORS_DEFAULT,\n      candidateLimit:\n        field.type === FieldTypes.VECTOR &&\n        vectorOptions &&\n        'candidateLimit' in vectorOptions\n          ? vectorOptions.candidateLimit\n          : VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_DEFAULT,\n      epsilon:\n        field.type === FieldTypes.VECTOR &&\n        vectorOptions &&\n        'epsilon' in vectorOptions\n          ? vectorOptions.epsilon\n          : VECTOR_CONSTRAINTS.EPSILON_DEFAULT,\n      weight:\n        field.type === FieldTypes.TEXT && textOptions\n          ? (textOptions.weight ?? TEXT_CONSTRAINTS.WEIGHT_DEFAULT)\n          : TEXT_CONSTRAINTS.WEIGHT_DEFAULT,\n      phonetic:\n        field.type === FieldTypes.TEXT && textOptions\n          ? textOptions.phonetic\n          : undefined,\n    }\n  }\n\n  return {\n    fieldName: '',\n    fieldType: FieldTypes.TEXT,\n    algorithm: VECTOR_ALGORITHM_DEFAULT,\n    dataType: VECTOR_DATA_TYPE_DEFAULT,\n    dimensions: VECTOR_CONSTRAINTS.DIMENSIONS_DEFAULT,\n    distanceMetric: VECTOR_DISTANCE_METRIC_DEFAULT,\n    maxEdges: VECTOR_CONSTRAINTS.MAX_EDGES_DEFAULT,\n    maxNeighbors: VECTOR_CONSTRAINTS.MAX_NEIGHBORS_DEFAULT,\n    candidateLimit: VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_DEFAULT,\n    epsilon: VECTOR_CONSTRAINTS.EPSILON_DEFAULT,\n    weight: TEXT_CONSTRAINTS.WEIGHT_DEFAULT,\n    phonetic: undefined,\n  } as FieldTypeFormValues\n}\n\nexport const buildFieldFromValues = (\n  values: FieldTypeFormValues,\n  mode: FieldTypeModalMode,\n  existingField?: IndexField,\n): IndexField => {\n  const id =\n    mode === FieldTypeModalMode.Edit && existingField\n      ? existingField.id\n      : values.fieldName.trim()\n\n  const base: IndexField = {\n    id,\n    name:\n      mode === FieldTypeModalMode.Edit && existingField\n        ? existingField.name\n        : values.fieldName.trim(),\n    value:\n      mode === FieldTypeModalMode.Edit && existingField\n        ? existingField.value\n        : '',\n    type: values.fieldType,\n  }\n\n  if (values.fieldType === FieldTypes.VECTOR) {\n    const shared = {\n      dimensions: values.dimensions,\n      distanceMetric: values.distanceMetric,\n      dataType: values.dataType,\n    }\n\n    base.options =\n      values.algorithm === VectorAlgorithm.HNSW\n        ? {\n            algorithm: VectorAlgorithm.HNSW,\n            ...shared,\n            maxEdges: values.maxEdges,\n            maxNeighbors: values.maxNeighbors,\n            candidateLimit: values.candidateLimit,\n            epsilon: values.epsilon,\n          }\n        : {\n            algorithm: VectorAlgorithm.FLAT,\n            ...shared,\n          }\n  } else if (values.fieldType === FieldTypes.TEXT) {\n    base.options = {\n      weight: values.weight,\n      phonetic: values.phonetic,\n    }\n  }\n\n  return base\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/components/FieldTypeForm/FieldTypeForm.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport { VectorFieldOptions } from '../VectorFieldOptions/VectorFieldOptions'\nimport { TextFieldOptions } from '../TextFieldOptions/TextFieldOptions'\nimport { FieldTypeFormValues } from './FieldTypeForm.types'\n\nexport interface FieldTypeFormProps {\n  formik: FormikProps<FieldTypeFormValues>\n}\n\nconst SECTION_LABELS: Partial<Record<FieldTypes, string>> = {\n  [FieldTypes.VECTOR]: 'VECTOR options',\n  [FieldTypes.TEXT]: 'TEXT options',\n}\n\nexport const FieldTypeForm = ({ formik }: FieldTypeFormProps) => {\n  const { fieldType } = formik.values\n  const sectionLabel = SECTION_LABELS[fieldType]\n\n  if (!sectionLabel) return null\n\n  return (\n    <Col gap=\"l\">\n      <Text color=\"primary\" data-testid=\"field-type-modal-section-header\">\n        {sectionLabel}\n      </Text>\n      {fieldType === FieldTypes.VECTOR && (\n        <VectorFieldOptions formik={formik} />\n      )}\n      {fieldType === FieldTypes.TEXT && <TextFieldOptions formik={formik} />}\n    </Col>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/components/FieldTypeForm/FieldTypeForm.types.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  VectorFlatFieldOptions,\n  VectorHnswFieldOptions,\n  TextFieldOptions,\n} from '../../../index-details/IndexDetails.types'\n\nexport interface BaseFieldFormValues {\n  fieldName: string\n  fieldType: FieldTypes\n}\n\nexport type TextFieldFormValues = TextFieldOptions\n\nexport type VectorFlatFieldFormValues = VectorFlatFieldOptions\n\nexport type VectorHnswFieldFormValues = VectorHnswFieldOptions\n\nexport type VectorFieldFormValues =\n  | VectorFlatFieldFormValues\n  | VectorHnswFieldFormValues\n\nexport type FieldTypeFormValues = BaseFieldFormValues &\n  TextFieldFormValues &\n  VectorFieldFormValues\n\nexport type AllFieldTypeFormFields = BaseFieldFormValues &\n  TextFieldFormValues &\n  VectorHnswFieldFormValues\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/components/FieldTypeSelect/FieldTypeSelect.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const DropdownOption = styled(Row)`\n  white-space: normal;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/components/FieldTypeSelect/FieldTypeSelect.tsx",
    "content": "import React, { useCallback } from 'react'\nimport {\n  FieldTypes,\n  FIELD_TYPE_OPTIONS,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  RiSelect,\n  SelectValueRenderParams,\n} from 'uiSrc/components/base/forms/select/RiSelect'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FieldTag } from 'uiSrc/pages/vector-search/components/field-tag/FieldTag'\n\nimport * as S from './FieldTypeSelect.styles'\n\nexport interface FieldTypeSelectProps {\n  value: FieldTypes\n  onChange: (value: FieldTypes) => void\n  dataTestId?: string\n}\n\nconst fieldTypeDescriptions: Record<FieldTypes, string> = Object.fromEntries(\n  FIELD_TYPE_OPTIONS.map((option) => [option.value, option.description]),\n) as Record<FieldTypes, string>\n\nconst fieldTypeSelectOptions = FIELD_TYPE_OPTIONS.map((option) => ({\n  value: option.value,\n  label: option.text,\n}))\n\nexport const FieldTypeSelect = ({\n  value,\n  onChange,\n  dataTestId = 'field-type-select',\n}: FieldTypeSelectProps) => {\n  const valueRender = useCallback(\n    ({ option, isOptionValue }: SelectValueRenderParams) => {\n      const fieldType = option.value as FieldTypes\n\n      if (isOptionValue) {\n        return (\n          <S.DropdownOption align=\"center\" gap=\"m\">\n            <FieldTag tag={fieldType} />\n            <Text color=\"ghost\">{fieldTypeDescriptions[fieldType]}</Text>\n          </S.DropdownOption>\n        )\n      }\n\n      return <FieldTag tag={fieldType} />\n    },\n    [],\n  )\n\n  const handleChange = useCallback(\n    (selectedValue: string) => {\n      onChange(selectedValue as FieldTypes)\n    },\n    [onChange],\n  )\n\n  return (\n    <RiSelect\n      options={fieldTypeSelectOptions}\n      value={value}\n      onChange={handleChange}\n      valueRender={valueRender}\n      data-testid={dataTestId}\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/components/TextFieldOptions/TextFieldOptions.tsx",
    "content": "import React from 'react'\nimport { FormikProps } from 'formik'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport NumericInput from 'uiSrc/components/base/inputs/NumericInput'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\n\nimport { PHONETIC_NONE, PHONETIC_OPTIONS } from '../../FieldTypeModal.constants'\nimport { FieldTypeFormValues } from '../FieldTypeForm/FieldTypeForm.types'\n\nexport interface TextFieldOptionsProps {\n  formik: FormikProps<FieldTypeFormValues>\n}\n\nexport const TextFieldOptions = ({ formik }: TextFieldOptionsProps) => {\n  const { values, errors, setFieldValue } = formik\n\n  return (\n    <Row gap=\"l\">\n      <FlexItem grow>\n        <FormField\n          label=\"Weight\"\n          infoIconProps={{\n            content:\n              'Declares the importance of this attribute when calculating result accuracy.',\n          }}\n        >\n          <NumericInput\n            value={values.weight ?? null}\n            onChange={(val: number | null) =>\n              setFieldValue('weight', val ?? undefined)\n            }\n            error={errors.weight}\n            min={0}\n            step={0.1}\n            data-testid=\"field-type-modal-text-weight\"\n          />\n        </FormField>\n      </FlexItem>\n      <FlexItem grow>\n        <FormField\n          label=\"Phonetic matcher\"\n          infoIconProps={{\n            content: 'Performs phonetic matching in searches.',\n          }}\n        >\n          <RiSelect\n            options={PHONETIC_OPTIONS}\n            value={values.phonetic ?? PHONETIC_NONE}\n            onChange={(val: string) =>\n              setFieldValue('phonetic', val === PHONETIC_NONE ? undefined : val)\n            }\n            data-testid=\"field-type-modal-text-phonetic\"\n          />\n        </FormField>\n      </FlexItem>\n    </Row>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/components/VectorFieldOptions/VectorFieldOptions.tsx",
    "content": "import React from 'react'\nimport { FormikErrors, FormikProps } from 'formik'\nimport { Row, Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { FormField } from 'uiSrc/components/base/forms/FormField'\nimport NumericInput from 'uiSrc/components/base/inputs/NumericInput'\nimport { RiSelect } from 'uiSrc/components/base/forms/select/RiSelect'\n\nimport {\n  VectorAlgorithm,\n  VectorDataType,\n  VectorDistanceMetric,\n} from '../../../index-details/IndexDetails.types'\nimport {\n  VECTOR_CONSTRAINTS,\n  VECTOR_ALGORITHM_OPTIONS,\n  VECTOR_DISTANCE_METRIC_OPTIONS,\n} from '../../FieldTypeModal.constants'\nimport { useVectorDataTypeOptions } from '../../hooks/useVectorDataTypeOptions'\nimport {\n  AllFieldTypeFormFields,\n  FieldTypeFormValues,\n} from '../FieldTypeForm/FieldTypeForm.types'\n\nexport interface VectorFieldOptionsProps {\n  formik: FormikProps<FieldTypeFormValues>\n}\n\nexport const VectorFieldOptions = ({ formik }: VectorFieldOptionsProps) => {\n  const { values, setFieldValue } = formik\n  const errors = formik.errors as FormikErrors<AllFieldTypeFormFields>\n  const isHnsw = values.algorithm === VectorAlgorithm.HNSW\n  const vectorDataTypeOptions = useVectorDataTypeOptions()\n\n  return (\n    <Col gap=\"l\">\n      <Row gap=\"l\">\n        <FlexItem grow>\n          <FormField\n            label=\"Algorithm\"\n            infoIconProps={{\n              content:\n                'Use FLAT for small datasets or when exact accuracy matters. Use HNSW for larger datasets or when fast search is important.',\n            }}\n          >\n            <RiSelect\n              options={VECTOR_ALGORITHM_OPTIONS}\n              value={values.algorithm}\n              onChange={(val: string) =>\n                setFieldValue('algorithm', val as VectorAlgorithm)\n              }\n              data-testid=\"field-type-modal-vector-algorithm\"\n            />\n          </FormField>\n        </FlexItem>\n\n        <FlexItem grow>\n          <FormField label=\"Vector type\">\n            <RiSelect\n              options={vectorDataTypeOptions}\n              value={values.dataType}\n              onChange={(val: string) =>\n                setFieldValue('dataType', val as VectorDataType)\n              }\n              data-testid=\"field-type-modal-vector-data-type\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n\n      <Row gap=\"l\">\n        <FlexItem grow>\n          <FormField\n            label=\"Dimensions\"\n            infoIconProps={{\n              content:\n                'Number of dimensions in each vector. Query vectors must match this size.',\n            }}\n          >\n            <NumericInput\n              value={values.dimensions ?? null}\n              onChange={(val: number | null) =>\n                setFieldValue('dimensions', val ?? undefined)\n              }\n              error={errors.dimensions}\n              min={VECTOR_CONSTRAINTS.DIMENSIONS_MIN}\n              max={VECTOR_CONSTRAINTS.DIMENSIONS_MAX}\n              data-testid=\"field-type-modal-vector-dimensions\"\n            />\n          </FormField>\n        </FlexItem>\n\n        <FlexItem grow>\n          <FormField\n            label=\"Distance metric\"\n            infoIconProps={{\n              content: 'Distance metric for vector comparison.',\n            }}\n          >\n            <RiSelect\n              options={VECTOR_DISTANCE_METRIC_OPTIONS}\n              value={values.distanceMetric}\n              onChange={(val: string) =>\n                setFieldValue('distanceMetric', val as VectorDistanceMetric)\n              }\n              data-testid=\"field-type-modal-vector-distance-metric\"\n            />\n          </FormField>\n        </FlexItem>\n      </Row>\n\n      {isHnsw && (\n        <>\n          <Row gap=\"l\">\n            <FlexItem grow>\n              <FormField\n                label=\"Max Edges\"\n                infoIconProps={{\n                  content:\n                    'Maximum outgoing edges per node. Higher values improve accuracy but increase memory.',\n                }}\n              >\n                <NumericInput\n                  value={values.maxEdges ?? null}\n                  onChange={(val: number | null) =>\n                    setFieldValue('maxEdges', val ?? undefined)\n                  }\n                  error={errors.maxEdges}\n                  min={VECTOR_CONSTRAINTS.MAX_EDGES_MIN}\n                  max={VECTOR_CONSTRAINTS.MAX_EDGES_MAX}\n                  data-testid=\"field-type-modal-vector-max-edges\"\n                />\n              </FormField>\n            </FlexItem>\n\n            <FlexItem grow>\n              <FormField\n                label=\"Max Neighbors\"\n                infoIconProps={{\n                  content:\n                    'Maximum neighbors considered during graph build. Higher values improve accuracy but slow indexing.',\n                }}\n              >\n                <NumericInput\n                  value={values.maxNeighbors ?? null}\n                  onChange={(val: number | null) =>\n                    setFieldValue('maxNeighbors', val ?? undefined)\n                  }\n                  error={errors.maxNeighbors}\n                  min={VECTOR_CONSTRAINTS.MAX_NEIGHBORS_MIN}\n                  max={VECTOR_CONSTRAINTS.MAX_NEIGHBORS_MAX}\n                  data-testid=\"field-type-modal-vector-max-neighbors\"\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n\n          <Row gap=\"l\">\n            <FlexItem grow>\n              <FormField\n                label=\"Candidate Limit\"\n                infoIconProps={{\n                  content:\n                    'Max top candidates considered during KNN search. Higher values improve accuracy but increase latency.',\n                }}\n              >\n                <NumericInput\n                  value={values.candidateLimit ?? null}\n                  onChange={(val: number | null) =>\n                    setFieldValue('candidateLimit', val ?? undefined)\n                  }\n                  error={errors.candidateLimit}\n                  min={VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_MIN}\n                  max={VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_MAX}\n                  data-testid=\"field-type-modal-vector-candidate-limit\"\n                />\n              </FormField>\n            </FlexItem>\n\n            <FlexItem grow>\n              <FormField\n                label=\"Epsilon\"\n                infoIconProps={{\n                  content:\n                    'Relative factor for range query boundaries. Higher values widen the search.',\n                }}\n              >\n                <NumericInput\n                  value={values.epsilon ?? null}\n                  onChange={(val: number | null) =>\n                    setFieldValue('epsilon', val ?? undefined)\n                  }\n                  error={errors.epsilon}\n                  min={VECTOR_CONSTRAINTS.EPSILON_MIN}\n                  step={0.01}\n                  data-testid=\"field-type-modal-vector-epsilon\"\n                />\n              </FormField>\n            </FlexItem>\n          </Row>\n        </>\n      )}\n    </Col>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/hooks/useFieldTypeValidation.ts",
    "content": "import { useCallback } from 'react'\nimport { FormikErrors } from 'formik'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  IndexField,\n  VectorAlgorithm,\n} from '../../index-details/IndexDetails.types'\nimport {\n  VECTOR_CONSTRAINTS,\n  TEXT_CONSTRAINTS,\n  VALIDATION_MESSAGES,\n} from '../FieldTypeModal.constants'\nimport {\n  FieldTypeFormValues,\n  AllFieldTypeFormFields,\n} from '../components/FieldTypeForm/FieldTypeForm.types'\nimport { FieldTypeModalMode } from '../FieldTypeModal.types'\n\nconst isInRange = (\n  value: number | undefined,\n  min: number,\n  max: number,\n): boolean => value !== undefined && value >= min && value <= max\n\nexport const useFieldTypeValidation = (\n  mode: FieldTypeModalMode,\n  fields: IndexField[],\n  _editingField?: IndexField,\n) =>\n  useCallback(\n    (values: FieldTypeFormValues): FormikErrors<AllFieldTypeFormFields> => {\n      const errors: FormikErrors<AllFieldTypeFormFields> = {}\n\n      if (mode === FieldTypeModalMode.Create) {\n        if (!values.fieldName?.trim()) {\n          errors.fieldName = VALIDATION_MESSAGES.FIELD_NAME_REQUIRED\n        } else if (fields.some((f) => f.name === values.fieldName.trim())) {\n          errors.fieldName = VALIDATION_MESSAGES.FIELD_NAME_DUPLICATE\n        }\n      }\n\n      if (values.fieldType === FieldTypes.VECTOR) {\n        if (values.dimensions === undefined) {\n          errors.dimensions = VALIDATION_MESSAGES.DIMENSIONS_REQUIRED\n        } else if (\n          !isInRange(\n            values.dimensions,\n            VECTOR_CONSTRAINTS.DIMENSIONS_MIN,\n            VECTOR_CONSTRAINTS.DIMENSIONS_MAX,\n          )\n        ) {\n          errors.dimensions = VALIDATION_MESSAGES.DIMENSIONS_RANGE\n        }\n\n        if (values.algorithm === VectorAlgorithm.HNSW) {\n          if (\n            values.maxEdges !== undefined &&\n            !isInRange(\n              values.maxEdges,\n              VECTOR_CONSTRAINTS.MAX_EDGES_MIN,\n              VECTOR_CONSTRAINTS.MAX_EDGES_MAX,\n            )\n          ) {\n            errors.maxEdges = VALIDATION_MESSAGES.MAX_EDGES_RANGE\n          }\n\n          if (\n            values.maxNeighbors !== undefined &&\n            !isInRange(\n              values.maxNeighbors,\n              VECTOR_CONSTRAINTS.MAX_NEIGHBORS_MIN,\n              VECTOR_CONSTRAINTS.MAX_NEIGHBORS_MAX,\n            )\n          ) {\n            errors.maxNeighbors = VALIDATION_MESSAGES.MAX_NEIGHBORS_RANGE\n          }\n\n          if (\n            values.candidateLimit !== undefined &&\n            !isInRange(\n              values.candidateLimit,\n              VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_MIN,\n              VECTOR_CONSTRAINTS.CANDIDATE_LIMIT_MAX,\n            )\n          ) {\n            errors.candidateLimit = VALIDATION_MESSAGES.CANDIDATE_LIMIT_RANGE\n          }\n\n          if (\n            values.epsilon !== undefined &&\n            values.epsilon <= VECTOR_CONSTRAINTS.EPSILON_MIN\n          ) {\n            errors.epsilon = VALIDATION_MESSAGES.EPSILON_MIN\n          }\n        }\n      }\n\n      if (values.fieldType === FieldTypes.TEXT) {\n        if (\n          values.weight !== undefined &&\n          values.weight <= TEXT_CONSTRAINTS.WEIGHT_MIN\n        ) {\n          errors.weight = VALIDATION_MESSAGES.WEIGHT_MIN\n        }\n      }\n\n      return errors\n    },\n    [mode, fields, _editingField],\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/hooks/useVectorDataTypeOptions.ts",
    "content": "import { useMemo } from 'react'\nimport { useSelector } from 'react-redux'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { REDISEARCH_MODULES } from 'uiSrc/slices/interfaces'\nimport { isRedisVersionSupported } from 'uiSrc/utils/comparisons/compareVersions'\n\nimport {\n  MIN_RQE_VERSION_FLOAT16,\n  VECTOR_DATA_TYPE_BASE_OPTIONS,\n  VECTOR_DATA_TYPE_FLOAT16_OPTIONS,\n} from '../FieldTypeModal.constants'\n\nconst REDISEARCH_MODULE_SET = new Set(REDISEARCH_MODULES)\n\nexport const useVectorDataTypeOptions = () => {\n  const { modules = [] } = useSelector(connectedInstanceSelector)\n\n  return useMemo(() => {\n    const rqeModule = modules.find((m) => REDISEARCH_MODULE_SET.has(m.name))\n    const rqeVersion = rqeModule?.semanticVersion || ''\n\n    const supportsFloat16 =\n      !!rqeVersion &&\n      isRedisVersionSupported(rqeVersion, MIN_RQE_VERSION_FLOAT16)\n\n    return supportsFloat16\n      ? [...VECTOR_DATA_TYPE_BASE_OPTIONS, ...VECTOR_DATA_TYPE_FLOAT16_OPTIONS]\n      : VECTOR_DATA_TYPE_BASE_OPTIONS\n  }, [modules])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/field-type-modal/index.ts",
    "content": "export { FieldTypeModal } from './FieldTypeModal'\nexport { FieldTypeModalMode } from './FieldTypeModal.types'\nexport type { FieldTypeModalProps } from './FieldTypeModal.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.columns.tsx",
    "content": "import React from 'react'\nimport { ColumnDef, Row, Table } from 'uiSrc/components/base/layout/table'\nimport { IndexDetailsColumn, IndexField } from './IndexDetails.types'\n\nimport { FieldNameCell } from './components/FieldNameCell/FieldNameCell'\nimport { FieldValueCell } from './components/FieldValueCell/FieldValueCell'\nimport { FieldTypeCell } from './components/FieldTypeCell/FieldTypeCell'\nimport { FieldActionsCell } from './components/FieldActionsCell/FieldActionsCell'\nimport { ColumnHeader } from './components/ColumnHeader/ColumnHeader'\nimport { FieldNameTooltip } from './components/FieldNameCell/FieldNameTooltip'\nimport { FieldValueTooltip } from './components/FieldValueCell/FieldValueTooltip'\nimport { FieldTypeTooltip } from './components/FieldTypeCell/FieldTypeTooltip'\nimport { CreateIndexOnboardingPopover } from '../create-index-onboarding'\nimport { CreateIndexOnboardingStep } from '../create-index-onboarding/CreateIndexOnboarding.constants'\n\nexport const SELECTION_COLUMN: ColumnDef<IndexField> = {\n  id: IndexDetailsColumn.Selection,\n  size: 50,\n  sizeUnit: 'px',\n  enableSorting: false,\n  header: Table.HeaderMultiRowSelectionButton,\n  cell: ({ row }) => <Table.RowSelectionButton row={row} />,\n}\n\nexport const NAME_COLUMN: ColumnDef<IndexField> = {\n  id: IndexDetailsColumn.Name,\n  accessorKey: IndexDetailsColumn.Name,\n  enableSorting: false,\n  header: () => (\n    <CreateIndexOnboardingPopover\n      step={CreateIndexOnboardingStep.FieldName}\n      anchorPosition=\"upCenter\"\n    >\n      <ColumnHeader label=\"Field name\" tooltip={<FieldNameTooltip />} />\n    </CreateIndexOnboardingPopover>\n  ),\n  cell: ({ row }: { row: Row<IndexField> }) => (\n    <FieldNameCell field={row.original} />\n  ),\n}\n\nexport const VALUE_COLUMN: ColumnDef<IndexField> = {\n  id: IndexDetailsColumn.Value,\n  accessorKey: IndexDetailsColumn.Value,\n  enableSorting: false,\n  header: () => (\n    <CreateIndexOnboardingPopover\n      step={CreateIndexOnboardingStep.SampleValue}\n      anchorPosition=\"upCenter\"\n    >\n      <ColumnHeader\n        label=\"Field sample value\"\n        tooltip={<FieldValueTooltip />}\n      />\n    </CreateIndexOnboardingPopover>\n  ),\n  cell: ({ row }: { row: Row<IndexField> }) => (\n    <FieldValueCell field={row.original} />\n  ),\n}\n\nexport const TYPE_COLUMN_READONLY: ColumnDef<IndexField> = {\n  id: IndexDetailsColumn.Type,\n  accessorKey: IndexDetailsColumn.Type,\n  size: 270,\n  sizeUnit: 'px',\n  enableSorting: false,\n  header: () => (\n    <CreateIndexOnboardingPopover\n      step={CreateIndexOnboardingStep.IndexingType}\n      anchorPosition=\"downCenter\"\n    >\n      <ColumnHeader label=\"Indexing type\" tooltip={<FieldTypeTooltip />} />\n    </CreateIndexOnboardingPopover>\n  ),\n  cell: ({ row }: { row: Row<IndexField> }) => (\n    <FieldTypeCell field={row.original} />\n  ),\n}\n\nexport const TYPE_COLUMN_EDITABLE: ColumnDef<IndexField> = {\n  id: IndexDetailsColumn.Type,\n  accessorKey: IndexDetailsColumn.Type,\n  size: 270,\n  sizeUnit: 'px',\n  enableSorting: false,\n  header: () => (\n    <CreateIndexOnboardingPopover\n      step={CreateIndexOnboardingStep.IndexingType}\n      anchorPosition=\"downCenter\"\n    >\n      <ColumnHeader\n        label=\"Suggested indexing type\"\n        tooltip={<FieldTypeTooltip />}\n      />\n    </CreateIndexOnboardingPopover>\n  ),\n  cell: ({ row }: { row: Row<IndexField> }) => (\n    <FieldTypeCell field={row.original} />\n  ),\n}\n\nexport const createActionsColumn = (\n  onFieldEdit?: (field: IndexField) => void,\n): ColumnDef<IndexField> => ({\n  id: IndexDetailsColumn.Actions,\n  enableSorting: false,\n  size: 50,\n  sizeUnit: 'px',\n  header: '',\n  cell: ({ row }: { row: Row<IndexField> }) => (\n    <FieldActionsCell field={row.original} onEdit={onFieldEdit} />\n  ),\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.config.tsx",
    "content": "import { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport {\n  GetColumnsOptions,\n  IndexDetailsMode,\n  IndexField,\n} from './IndexDetails.types'\nimport {\n  SELECTION_COLUMN,\n  NAME_COLUMN,\n  VALUE_COLUMN,\n  TYPE_COLUMN_READONLY,\n  TYPE_COLUMN_EDITABLE,\n  createActionsColumn,\n} from './IndexDetails.columns'\n\nconst READONLY_COLUMNS: ColumnDef<IndexField>[] = [\n  NAME_COLUMN,\n  VALUE_COLUMN,\n  TYPE_COLUMN_READONLY,\n]\n\nconst EDITABLE_BASE_COLUMNS: ColumnDef<IndexField>[] = [\n  SELECTION_COLUMN,\n  NAME_COLUMN,\n  VALUE_COLUMN,\n  TYPE_COLUMN_EDITABLE,\n]\n\nexport const getIndexDetailsColumns = ({\n  mode,\n  onFieldEdit,\n}: GetColumnsOptions): ColumnDef<IndexField>[] => {\n  if (mode === IndexDetailsMode.Readonly) {\n    return READONLY_COLUMNS\n  }\n\n  return [...EDITABLE_BASE_COLUMNS, createActionsColumn(onFieldEdit)]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.spec.tsx",
    "content": "import React from 'react'\nimport { act } from '@testing-library/react'\nimport {\n  cleanup,\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { indexFieldFactory } from 'uiSrc/mocks/factories/redisearch/IndexField.factory'\nimport { IndexDetails } from './IndexDetails'\nimport { IndexDetailsMode, IndexDetailsProps } from './IndexDetails.types'\n\nconst mockFields = indexFieldFactory.buildList(3)\n\nconst defaultProps: IndexDetailsProps = {\n  fields: mockFields,\n  mode: IndexDetailsMode.Editable,\n  rowSelection: {},\n  onRowSelectionChange: jest.fn(),\n  onFieldEdit: jest.fn(),\n}\n\nconst renderComponent = (props: Partial<IndexDetailsProps> = {}) =>\n  render(<IndexDetails {...defaultProps} {...props} />)\n\ndescribe('IndexDetails', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  afterEach(() => {\n    cleanup()\n  })\n\n  describe('Readonly mode', () => {\n    it('should render table with columns and field values', () => {\n      renderComponent({ mode: IndexDetailsMode.Readonly })\n\n      const table = screen.getByTestId('index-details-table')\n      expect(table).toBeInTheDocument()\n\n      // Verify the headers are present\n      const nameHeader = screen.getByText('Field name')\n      const valueHeader = screen.getByText('Field sample value')\n      const typeHeader = screen.getByText('Indexing type')\n\n      expect(nameHeader).toBeInTheDocument()\n      expect(valueHeader).toBeInTheDocument()\n      expect(typeHeader).toBeInTheDocument()\n\n      // Verify the field values are present and edit buttons are not\n      mockFields.forEach((field) => {\n        const nameCell = screen.getByTestId(\n          `index-details-field-name-${field.id}`,\n        )\n        const valueCell = screen.getByTestId(\n          `index-details-field-value-${field.id}`,\n        )\n        const typeCell = screen.getByTestId(\n          `index-details-field-type-${field.id}`,\n        )\n        const editBtn = screen.queryByTestId(\n          `index-details-field-edit-btn-${field.id}`,\n        )\n\n        expect(nameCell).toBeInTheDocument()\n        expect(valueCell).toBeInTheDocument()\n        expect(typeCell).toBeInTheDocument()\n        expect(editBtn).not.toBeInTheDocument()\n      })\n    })\n\n    it('should show column header tooltips on hover', async () => {\n      renderComponent({ mode: IndexDetailsMode.Readonly })\n\n      const verifyTooltip = async (\n        headerText: string,\n        tooltipPattern: RegExp,\n      ) => {\n        const header = screen.getByText(headerText)\n        const infoIcon = header.parentElement?.querySelector('svg') as Element\n\n        await act(async () => {\n          fireEvent.focus(infoIcon)\n        })\n        await waitForRiTooltipVisible()\n\n        const tooltipContent = screen.getAllByText(tooltipPattern)[0]\n        expect(tooltipContent).toBeInTheDocument()\n      }\n\n      // Field name tooltip\n      await verifyTooltip(\n        'Field name',\n        /Represents a searchable attribute in your data/,\n      )\n\n      // Field sample value tooltip\n      await verifyTooltip(\n        'Field sample value',\n        /A sample value from the data to be indexed/,\n      )\n\n      // Indexing type tooltip\n      await verifyTooltip(\n        'Indexing type',\n        /Defines how Redis searches this field/,\n      )\n    })\n  })\n\n  describe('Editable mode', () => {\n    it('should render selection and actions columns', () => {\n      renderComponent({ mode: IndexDetailsMode.Editable })\n\n      // Verify the column headers are present\n      const nameHeader = screen.getByText('Field name')\n      const valueHeader = screen.getByText('Field sample value')\n      const typeHeader = screen.getByText('Suggested indexing type')\n\n      expect(nameHeader).toBeInTheDocument()\n      expect(valueHeader).toBeInTheDocument()\n      expect(typeHeader).toBeInTheDocument()\n\n      // Verify field columns and actions are present\n      mockFields.forEach((field) => {\n        const nameCell = screen.getByTestId(\n          `index-details-field-name-${field.id}`,\n        )\n        const valueCell = screen.getByTestId(\n          `index-details-field-value-${field.id}`,\n        )\n        const typeCell = screen.getByTestId(\n          `index-details-field-type-${field.id}`,\n        )\n        const editBtn = screen.getByTestId(\n          `index-details-field-edit-btn-${field.id}`,\n        )\n\n        expect(nameCell).toBeInTheDocument()\n        expect(valueCell).toBeInTheDocument()\n        expect(typeCell).toBeInTheDocument()\n        expect(editBtn).toBeInTheDocument()\n      })\n    })\n\n    it('should call onFieldEdit when edit button is clicked', () => {\n      const onFieldEdit = jest.fn()\n      renderComponent({ onFieldEdit, mode: IndexDetailsMode.Editable })\n\n      const editBtn = screen.getByTestId(\n        `index-details-field-edit-btn-${mockFields[0].id}`,\n      )\n      fireEvent.click(editBtn)\n\n      expect(onFieldEdit).toHaveBeenCalledWith(mockFields[0])\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\nimport { indexFieldFactory } from 'uiSrc/mocks/factories/redisearch/IndexField.factory'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Card, Spacer } from 'uiSrc/components/base/layout'\nimport { IndexDetails } from './IndexDetails'\nimport {\n  IndexField,\n  IndexDetailsMode,\n  IndexDetailsProps,\n} from './IndexDetails.types'\n\nconst SAMPLE_FIELDS: IndexField[] = [\n  indexFieldFactory.build({ id: '1', type: FieldTypes.TEXT }),\n  indexFieldFactory.build({ id: '2', type: FieldTypes.NUMERIC }),\n  indexFieldFactory.build({ id: '3', type: FieldTypes.TAG }),\n  indexFieldFactory.build({ id: '4', type: FieldTypes.GEO }),\n  indexFieldFactory.build({ id: '5', type: FieldTypes.VECTOR }),\n]\n\nconst handleFieldEdit = (field: IndexField) => {\n  // eslint-disable-next-line no-alert\n  alert(\n    `Edit field: \"${field.name}\"\\n\\n` +\n      `Current type: ${field.type.toUpperCase()}\\n` +\n      `Sample value: ${field.value}\\n\\n` +\n      `In the real flow, a modal will appear here to edit the field type and settings.`,\n  )\n}\n\n// Stateful wrapper for interactive stories (controlled component)\nconst IndexDetailsWrapper = (props: IndexDetailsProps) => {\n  const [rowSelection, setRowSelection] = useState<RowSelectionState>(\n    props.rowSelection || {},\n  )\n\n  const selectedFields = props.fields.filter((field) => rowSelection[field.id])\n  const isEditable = props.mode === IndexDetailsMode.Editable\n\n  return (\n    <Col>\n      <IndexDetails\n        {...props}\n        rowSelection={rowSelection}\n        onRowSelectionChange={setRowSelection}\n        onFieldEdit={handleFieldEdit}\n      />\n\n      {isEditable && (\n        <>\n          <Spacer size=\"l\" />\n          <Card>\n            <Col gap=\"s\">\n              <Text size=\"L\">Selected Fields ({selectedFields.length}):</Text>\n              <Text\n                size=\"S\"\n                color=\"secondary\"\n                style={{ maxHeight: '150px', overflow: 'auto' }}\n              >\n                <pre style={{ margin: 0 }}>\n                  {JSON.stringify(\n                    selectedFields.map((f) => ({\n                      id: f.id,\n                      name: f.name,\n                      type: f.type,\n                    })),\n                    null,\n                    2,\n                  )}\n                </pre>\n              </Text>\n              <Text size=\"S\" color=\"ghost\">\n                Row Selection State: {JSON.stringify(rowSelection)}\n              </Text>\n            </Col>\n          </Card>\n        </>\n      )}\n    </Col>\n  )\n}\n\nconst meta: Meta<typeof IndexDetails> = {\n  component: IndexDetails,\n  tags: ['autodocs'],\n  decorators: [\n    (Story) => (\n      <Col style={{ maxWidth: '800px' }}>\n        <Story />\n      </Col>\n    ),\n  ],\n  render: (args) => <IndexDetailsWrapper {...args} />,\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const ReadonlyMode: Story = {\n  args: {\n    fields: SAMPLE_FIELDS,\n    mode: IndexDetailsMode.Readonly,\n  },\n}\n\nexport const EditableMode: Story = {\n  args: {\n    fields: SAMPLE_FIELDS,\n    mode: IndexDetailsMode.Editable,\n  },\n}\n\nexport const EmptyState: Story = {\n  args: {\n    fields: [],\n    mode: IndexDetailsMode.Readonly,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.styles.tsx",
    "content": "import styled, { css } from 'styled-components'\nimport { IndexDetailsContainerProps } from './IndexDetails.types'\n\nexport const IndexDetailsContainer = styled.div<IndexDetailsContainerProps>`\n  width: 100%;\n\n  ${({ $showBorder }) =>\n    !$showBorder &&\n    css`\n      > div {\n        box-shadow: none;\n      }\n    `}\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.tsx",
    "content": "import React from 'react'\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport { getIndexDetailsColumns } from './IndexDetails.config'\nimport * as S from './IndexDetails.styles'\nimport {\n  IndexDetailsMode,\n  IndexDetailsProps,\n  IndexField,\n} from './IndexDetails.types'\n\nexport const IndexDetails = ({\n  fields,\n  mode = IndexDetailsMode.Editable,\n  showBorder = false,\n  rowSelection,\n  onRowSelectionChange,\n  onFieldEdit,\n}: IndexDetailsProps) => {\n  const isEditable = mode === IndexDetailsMode.Editable\n  const columns = getIndexDetailsColumns({ mode, onFieldEdit })\n\n  return (\n    <S.IndexDetailsContainer\n      as=\"div\"\n      $showBorder={showBorder}\n      data-testid=\"index-details-container\"\n    >\n      <Table\n        columns={columns}\n        data={fields}\n        getRowId={(row: IndexField) => row.id}\n        rowSelectionMode={isEditable ? 'multiple' : undefined}\n        rowSelection={rowSelection}\n        onRowSelectionChange={onRowSelectionChange}\n        data-testid=\"index-details-table\"\n      />\n    </S.IndexDetailsContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/IndexDetails.types.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\n\nexport type IndexFieldValue = string | number\n\nexport enum VectorAlgorithm {\n  FLAT = 'FLAT',\n  HNSW = 'HNSW',\n}\n\nexport enum VectorDistanceMetric {\n  COSINE = 'COSINE',\n  L2 = 'L2',\n  IP = 'IP',\n}\n\nexport enum VectorDataType {\n  FLOAT32 = 'FLOAT32',\n  FLOAT64 = 'FLOAT64',\n  FLOAT16 = 'FLOAT16',\n  BFLOAT16 = 'BFLOAT16',\n}\n\nexport interface VectorFieldOptionsBase {\n  dimensions?: number\n  distanceMetric?: VectorDistanceMetric\n  dataType?: VectorDataType\n}\n\nexport interface VectorFlatFieldOptions extends VectorFieldOptionsBase {\n  algorithm: VectorAlgorithm.FLAT\n}\n\nexport interface VectorHnswFieldOptions extends VectorFieldOptionsBase {\n  algorithm: VectorAlgorithm.HNSW\n  maxEdges?: number\n  maxNeighbors?: number\n  candidateLimit?: number\n  epsilon?: number\n}\n\nexport type VectorFieldOptions = VectorFlatFieldOptions | VectorHnswFieldOptions\n\nexport interface TextFieldOptions {\n  weight?: number\n  phonetic?: string\n}\n\nexport type IndexFieldOptions = VectorFieldOptions | TextFieldOptions\n\nexport interface IndexField {\n  id: string\n  name: string\n  value: IndexFieldValue\n  type: FieldTypes\n  options?: IndexFieldOptions\n}\n\nexport enum IndexDetailsMode {\n  Readonly = 'readonly',\n  Editable = 'editable',\n}\n\nexport enum IndexDetailsColumn {\n  Selection = 'selection',\n  Name = 'name',\n  Value = 'value',\n  Type = 'type',\n  Actions = 'actions',\n}\n\nexport interface IndexDetailsProps {\n  fields: IndexField[]\n  mode?: IndexDetailsMode\n  showBorder?: boolean\n  rowSelection?: RowSelectionState\n  onRowSelectionChange?: (selection: RowSelectionState) => void\n  onFieldEdit?: (field: IndexField) => void\n}\n\nexport interface IndexDetailsContainerProps {\n  $showBorder?: boolean\n}\n\nexport interface GetColumnsOptions {\n  mode: IndexDetailsMode\n  onFieldEdit?: (field: IndexField) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/ColumnHeader/ColumnHeader.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { ColumnHeader } from './ColumnHeader'\nimport { ColumnHeaderProps } from './ColumnHeader.types'\n\nconst defaultProps: ColumnHeaderProps = {\n  label: 'Test label',\n  tooltip: 'Tooltip content',\n}\n\nconst renderComponent = (props: Partial<ColumnHeaderProps> = {}) =>\n  render(<ColumnHeader {...defaultProps} {...props} />)\n\ndescribe('ColumnHeader', () => {\n  it('should render the label', () => {\n    renderComponent({ label: 'Field name' })\n\n    const label = screen.getByText('Field name')\n    expect(label).toBeInTheDocument()\n  })\n\n  it('should render with tooltip content', () => {\n    const TooltipContent = () => <div>Custom tooltip</div>\n    renderComponent({ label: 'Test label', tooltip: <TooltipContent /> })\n\n    const label = screen.getByText('Test label')\n    expect(label).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/ColumnHeader/ColumnHeader.tsx",
    "content": "export { ColumnHeader } from 'uiSrc/components/column-header'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/ColumnHeader/ColumnHeader.types.ts",
    "content": "import { ReactNode } from 'react'\n\nexport interface ColumnHeaderProps {\n  label: string\n  tooltip: ReactNode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldActionsCell/FieldActionsCell.spec.tsx",
    "content": "import React from 'react'\nimport { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { indexFieldFactory } from 'uiSrc/mocks/factories/redisearch/IndexField.factory'\nimport { FieldActionsCell } from './FieldActionsCell'\n\nconst mockField = indexFieldFactory.build()\n\ndescribe('FieldActionsCell', () => {\n  beforeEach(() => {\n    cleanup()\n  })\n\n  it('should render edit button', () => {\n    render(<FieldActionsCell field={mockField} />)\n\n    const editBtn = screen.getByTestId(\n      `index-details-field-edit-btn-${mockField.id}`,\n    )\n    expect(editBtn).toBeInTheDocument()\n  })\n\n  it('should call onEdit with field when clicked', () => {\n    const onEdit = jest.fn()\n    render(<FieldActionsCell field={mockField} onEdit={onEdit} />)\n\n    const editBtn = screen.getByTestId(\n      `index-details-field-edit-btn-${mockField.id}`,\n    )\n    fireEvent.click(editBtn)\n\n    expect(onEdit).toHaveBeenCalledWith(mockField)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldActionsCell/FieldActionsCell.tsx",
    "content": "import React from 'react'\nimport { EditIcon } from 'uiSrc/components/base/icons'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons/IconButton'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\nimport { FieldActionsCellProps } from './FieldActionsCell.types'\n\nexport const FieldActionsCell = ({ field, onEdit }: FieldActionsCellProps) => {\n  const handleClick = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    onEdit?.(field)\n  }\n\n  return (\n    <RiTooltip content=\"Edit field type\">\n      <IconButton\n        icon={EditIcon}\n        aria-label=\"Edit field\"\n        onClick={handleClick}\n        data-testid={`index-details-field-edit-btn-${field.id}`}\n      />\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldActionsCell/FieldActionsCell.types.ts",
    "content": "import { IndexField } from '../../IndexDetails.types'\n\nexport interface FieldActionsCellProps {\n  field: IndexField\n  onEdit?: (field: IndexField) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldNameCell/FieldNameCell.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FieldNameCellProps } from './FieldNameCell.types'\n\nexport const FieldNameCell = ({ field }: FieldNameCellProps) => (\n  <Text\n    size=\"M\"\n    ellipsis\n    tooltipOnEllipsis\n    data-testid={`index-details-field-name-${field.id}`}\n  >\n    {field.name}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldNameCell/FieldNameCell.types.ts",
    "content": "import { IndexField } from '../../IndexDetails.types'\n\nexport interface FieldNameCellProps {\n  field: IndexField\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldNameCell/FieldNameTooltip.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const FieldNameTooltip = () => (\n  <Col gap=\"m\">\n    <Text size=\"L\" color=\"primary\">\n      Field name\n    </Text>\n    <Text color=\"secondary\">\n      Represents a searchable attribute in your data. Only selected fields will\n      be searchable.\n    </Text>\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldTypeCell/FieldTypeCell.tsx",
    "content": "import React from 'react'\nimport { FieldTag } from 'uiSrc/pages/vector-search/components/field-tag/FieldTag'\nimport { FieldTypeCellProps } from './FieldTypeCell.types'\n\nexport const FieldTypeCell = ({ field }: FieldTypeCellProps) => (\n  <FieldTag\n    tag={field.type}\n    dataTestId={`index-details-field-type-${field.id}`}\n  />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldTypeCell/FieldTypeCell.types.ts",
    "content": "import { IndexField } from '../../IndexDetails.types'\n\nexport interface FieldTypeCellProps {\n  field: IndexField\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldTypeCell/FieldTypeTooltip.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { IndexingTypeContent } from '../../../../components/field-type-list'\n\nexport const FieldTypeTooltip = () => (\n  <Col gap=\"m\">\n    <Text size=\"L\" color=\"primary\">\n      Indexing type & options\n    </Text>\n    <IndexingTypeContent />\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldValueCell/FieldValueCell.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { FieldValueCellProps } from './FieldValueCell.types'\n\nexport const FieldValueCell = ({ field }: FieldValueCellProps) => (\n  <Text\n    size=\"M\"\n    color=\"secondary\"\n    ellipsis\n    tooltipOnEllipsis\n    data-testid={`index-details-field-value-${field.id}`}\n  >\n    {field.value}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldValueCell/FieldValueCell.types.ts",
    "content": "import { IndexField } from '../../IndexDetails.types'\n\nexport interface FieldValueCellProps {\n  field: IndexField\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/components/FieldValueCell/FieldValueTooltip.tsx",
    "content": "import React from 'react'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const FieldValueTooltip = () => (\n  <Col gap=\"m\">\n    <Text size=\"L\" color=\"primary\">\n      Field sample value\n    </Text>\n    <Text color=\"secondary\">\n      A sample value from the data to be indexed. Use it to verify the field\n      type and indexing choice.\n    </Text>\n  </Col>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-details/index.ts",
    "content": "export { IndexDetails } from './IndexDetails'\nexport { IndexDetailsMode } from './IndexDetails.types'\nexport type {\n  IndexDetailsProps,\n  IndexField,\n  IndexFieldValue,\n} from './IndexDetails.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.constants.tsx",
    "content": "import React from 'react'\n\nimport { ColumnDef } from 'uiSrc/components/base/layout/table'\nimport { FieldTag } from 'uiSrc/pages/vector-search/components/field-tag/FieldTag'\n\nimport { IndexInfoTableData } from './IndexInfo.types'\n\nexport enum IndexInfoTableColumn {\n  Identifier = 'identifier',\n  Attribute = 'attribute',\n  Type = 'type',\n  Weight = 'weight',\n}\n\n/**\n * Table columns for displaying index attributes.\n */\nexport const TABLE_COLUMNS: ColumnDef<IndexInfoTableData>[] = [\n  {\n    id: IndexInfoTableColumn.Identifier,\n    accessorKey: IndexInfoTableColumn.Identifier,\n    header: 'Identifier',\n  },\n  {\n    id: IndexInfoTableColumn.Attribute,\n    accessorKey: IndexInfoTableColumn.Attribute,\n    header: 'Attribute',\n  },\n  {\n    id: IndexInfoTableColumn.Type,\n    accessorKey: IndexInfoTableColumn.Type,\n    header: 'Type',\n    enableSorting: false,\n    cell: ({ row }) => <FieldTag tag={row.original.type} />,\n  },\n  {\n    id: IndexInfoTableColumn.Weight,\n    accessorKey: IndexInfoTableColumn.Weight,\n    header: 'Weight',\n    enableSorting: false,\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { indexInfoFactory } from 'uiSrc/mocks/factories/vector-search/indexInfo.factory'\n\nimport { IndexInfo } from './IndexInfo'\nimport { IndexInfoProps } from './IndexInfo.types'\n\nconst renderComponent = (props?: Partial<IndexInfoProps>) => {\n  const defaultProps: IndexInfoProps = {\n    indexInfo: indexInfoFactory.build(),\n    dataTestId: 'index-info',\n  }\n\n  return render(<IndexInfo {...defaultProps} {...props} />)\n}\n\ndescribe('IndexInfo', () => {\n  it('should render all sections', () => {\n    renderComponent()\n\n    const indexInfo = screen.getByTestId('index-info')\n    const definition = screen.getByTestId('index-info--definition')\n    const options = screen.getByTestId('index-info--options')\n    const summary = screen.getByTestId('index-info--summary')\n\n    expect(indexInfo).toBeInTheDocument()\n    expect(definition).toBeInTheDocument()\n    expect(options).toBeInTheDocument()\n    expect(summary).toBeInTheDocument()\n  })\n\n  it('should render loader when indexInfo is undefined', () => {\n    renderComponent({ indexInfo: undefined })\n\n    const loader = screen.getByTestId('index-info--loader')\n\n    expect(loader).toBeInTheDocument()\n  })\n\n  it('should render table with columns and data', () => {\n    const mockIndexInfo = indexInfoFactory.build()\n\n    renderComponent({ indexInfo: mockIndexInfo })\n\n    // Column headers\n    const identifierCol = screen.getByText('Identifier')\n    const attributeCol = screen.getByText('Attribute')\n    const typeCol = screen.getByText('Type')\n    const weightCol = screen.getByText('Weight')\n\n    expect(identifierCol).toBeInTheDocument()\n    expect(attributeCol).toBeInTheDocument()\n    expect(typeCol).toBeInTheDocument()\n    expect(weightCol).toBeInTheDocument()\n\n    // First row data\n    const firstAttr = mockIndexInfo.attributes[0]\n    const identifierValue = screen.getByText(firstAttr.identifier)\n    const attributeValue = screen.getByText(firstAttr.attribute)\n\n    expect(identifierValue).toBeInTheDocument()\n    expect(attributeValue).toBeInTheDocument()\n  })\n\n  it('should use custom dataTestId', () => {\n    renderComponent({ dataTestId: 'custom-id' })\n\n    const indexInfo = screen.getByTestId('custom-id')\n    const definition = screen.getByTestId('custom-id--definition')\n\n    expect(indexInfo).toBeInTheDocument()\n    expect(definition).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport {\n  indexInfoFactory,\n  indexAttributeFactory,\n} from 'uiSrc/mocks/factories/vector-search/indexInfo.factory'\n\nimport { IndexInfo } from './index'\n\nconst meta: Meta<typeof IndexInfo> = {\n  component: IndexInfo,\n  tags: ['autodocs'],\n  argTypes: {\n    indexInfo: {\n      description: 'The index information object',\n    },\n    dataTestId: {\n      description: 'Custom data-testid prefix for testing',\n    },\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n// Simple default story\nexport const Default: Story = {\n  args: {\n    indexInfo: indexInfoFactory.build(),\n  },\n}\n\n// HASH index type\nexport const HashIndex: Story = {\n  name: 'Hash index',\n  args: {\n    indexInfo: indexInfoFactory.build({\n      indexDefinition: {\n        keyType: 'hash',\n        prefixes: ['bike:'],\n      },\n      attributes: [\n        indexAttributeFactory.build({\n          identifier: 'brand',\n          attribute: 'brand',\n          type: 'text',\n          weight: '1',\n        }),\n        indexAttributeFactory.build({\n          identifier: 'model',\n          attribute: 'model',\n          type: 'text',\n          weight: '1',\n        }),\n        indexAttributeFactory.build({\n          identifier: 'price',\n          attribute: 'price',\n          type: 'numeric',\n        }),\n        indexAttributeFactory.build({\n          identifier: 'condition',\n          attribute: 'condition',\n          type: 'tag',\n        }),\n      ],\n    }),\n  },\n}\n\n// JSON index type with $.path notation\nexport const JsonIndex: Story = {\n  name: 'JSON index',\n  args: {\n    indexInfo: indexInfoFactory.build({\n      indexDefinition: {\n        keyType: 'json',\n        prefixes: ['json:bikes:'],\n      },\n      attributes: [\n        indexAttributeFactory.build({\n          identifier: '$.name',\n          attribute: 'name',\n          type: 'text',\n          weight: '1',\n        }),\n        indexAttributeFactory.build({\n          identifier: '$.category',\n          attribute: 'category',\n          type: 'tag',\n        }),\n        indexAttributeFactory.build({\n          identifier: '$.price',\n          attribute: 'price',\n          type: 'numeric',\n        }),\n      ],\n    }),\n  },\n}\n\n// With filter and language options\nexport const WithOptions: Story = {\n  name: 'With options',\n  args: {\n    indexInfo: indexInfoFactory.build({\n      indexDefinition: {\n        keyType: 'hash',\n        prefixes: ['product:'],\n      },\n      indexOptions: {\n        filter: '@status == \"active\"',\n        defaultLang: 'german',\n      },\n      attributes: [\n        indexAttributeFactory.build({\n          identifier: 'name',\n          attribute: 'name',\n          type: 'text',\n        }),\n        indexAttributeFactory.build({\n          identifier: 'status',\n          attribute: 'status',\n          type: 'tag',\n        }),\n      ],\n    }),\n  },\n}\n\n// Empty attributes\nexport const EmptyState: Story = {\n  name: 'Empty state',\n  args: {\n    indexInfo: indexInfoFactory.build({\n      attributes: [],\n    }),\n  },\n}\n\n// Loading state\nexport const Loading: Story = {\n  name: 'Loading state',\n  args: {\n    indexInfo: undefined,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const IndexInfoContainer = styled(Col)`\n  width: 100%;\n  height: fit-content;\n  flex-grow: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.tsx",
    "content": "import React from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\nimport { Loader } from 'uiSrc/components/base/display'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { GroupBadge } from 'uiSrc/components'\n\nimport { IndexInfoProps } from './IndexInfo.types'\nimport { TABLE_COLUMNS } from './IndexInfo.constants'\nimport {\n  parseIndexAttributes,\n  formatOptions,\n  hasIndexOptions,\n} from './IndexInfo.utils'\nimport { IndexInfoContainer } from './IndexInfo.styles'\nimport { formatPrefixes } from 'uiSrc/pages/vector-search/utils'\n\nexport const IndexInfo = ({ indexInfo, dataTestId }: IndexInfoProps) => {\n  if (!indexInfo) {\n    return (\n      <Loader size=\"xl\" data-testid={`${dataTestId ?? 'index-info'}--loader`} />\n    )\n  }\n\n  const { indexDefinition, indexOptions } = indexInfo\n  const prefixes = formatPrefixes(indexDefinition.prefixes)\n  const keyType = indexDefinition.keyType\n  const showOptions = hasIndexOptions(indexOptions)\n\n  return (\n    <IndexInfoContainer gap=\"s\" data-testid={dataTestId ?? 'index-info'}>\n      {/* Index Definition Header */}\n      <Row\n        gap=\"s\"\n        align=\"center\"\n        data-testid={`${dataTestId ?? 'index-info'}--definition`}\n      >\n        <Text size=\"s\" color=\"secondary\">\n          Indexing\n        </Text>\n        <GroupBadge type={keyType} />\n        <Text size=\"s\" color=\"secondary\">\n          documents{prefixes && ` prefixed by ${prefixes}`}.\n        </Text>\n      </Row>\n\n      {/* Index Options */}\n      <Text\n        size=\"s\"\n        color=\"secondary\"\n        data-testid={`${dataTestId ?? 'index-info'}--options`}\n      >\n        Options:{' '}\n        {showOptions ? formatOptions(indexOptions!) : 'no options found'}\n      </Text>\n\n      {/* Attributes Table */}\n      <Table columns={TABLE_COLUMNS} data={parseIndexAttributes(indexInfo)} />\n\n      {/* Summary Info */}\n      <Text\n        size=\"xs\"\n        color=\"secondary\"\n        data-testid={`${dataTestId ?? 'index-info'}--summary`}\n      >\n        Number of docs: {indexInfo.numDocs} (max {indexInfo.maxDocId}) | Number\n        of records: {indexInfo.numRecords} | Number of terms:{' '}\n        {indexInfo.numTerms}\n      </Text>\n    </IndexInfoContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.types.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { IndexInfo } from 'uiSrc/pages/vector-search/hooks/useIndexInfo'\n\nexport interface IndexInfoProps {\n  indexInfo: IndexInfo | undefined\n  dataTestId?: string\n}\n\nexport interface IndexInfoTableData {\n  identifier: string\n  attribute: string\n  type: FieldTypes\n  weight?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.utils.spec.ts",
    "content": "import {\n  indexInfoFactory,\n  indexAttributeFactory,\n} from 'uiSrc/mocks/factories/vector-search/indexInfo.factory'\n\nimport {\n  parseIndexAttributes,\n  formatOptions,\n  hasIndexOptions,\n} from './IndexInfo.utils'\n\ndescribe('IndexInfo.utils', () => {\n  describe('parseIndexAttributes', () => {\n    it('should parse attributes to table-friendly format', () => {\n      const indexInfo = indexInfoFactory.build()\n\n      const result = parseIndexAttributes(indexInfo)\n\n      expect(result).toHaveLength(indexInfo.attributes.length)\n      expect(result[0].identifier).toBe(indexInfo.attributes[0].identifier)\n      expect(result[0].attribute).toBe(indexInfo.attributes[0].attribute)\n      expect(result[0].type).toBe(indexInfo.attributes[0].type)\n    })\n\n    it('should handle empty attributes array', () => {\n      const indexInfo = indexInfoFactory.build({ attributes: [] })\n\n      const result = parseIndexAttributes(indexInfo)\n\n      expect(result).toEqual([])\n    })\n\n    it('should handle attributes without weight', () => {\n      const indexInfo = indexInfoFactory.build({\n        attributes: [\n          indexAttributeFactory.build(\n            { type: 'tag' },\n            { transient: { includeWeight: false } },\n          ),\n        ],\n      })\n\n      const result = parseIndexAttributes(indexInfo)\n\n      expect(result[0].weight).toBeUndefined()\n    })\n  })\n\n  describe('formatOptions', () => {\n    it('should format filter option', () => {\n      const options = { filter: '@status == \"active\"' }\n\n      expect(formatOptions(options)).toBe('filter: @status == \"active\"')\n    })\n\n    it('should format language option', () => {\n      const options = { defaultLang: 'german' }\n\n      expect(formatOptions(options)).toBe('language: german')\n    })\n\n    it('should format both options with comma separator', () => {\n      const options = {\n        filter: '@status == \"active\"',\n        defaultLang: 'german',\n      }\n\n      expect(formatOptions(options)).toBe(\n        'filter: @status == \"active\", language: german',\n      )\n    })\n\n    it('should return empty string when no options', () => {\n      const options = {}\n\n      expect(formatOptions(options)).toBe('')\n    })\n  })\n\n  describe('hasIndexOptions', () => {\n    it('should return true when filter is present', () => {\n      expect(hasIndexOptions({ filter: '@status == \"active\"' })).toBe(true)\n    })\n\n    it('should return true when defaultLang is present', () => {\n      expect(hasIndexOptions({ defaultLang: 'german' })).toBe(true)\n    })\n\n    it('should return true when both options are present', () => {\n      expect(\n        hasIndexOptions({\n          filter: '@status == \"active\"',\n          defaultLang: 'german',\n        }),\n      ).toBe(true)\n    })\n\n    it('should return false for empty object', () => {\n      expect(hasIndexOptions({})).toBe(false)\n    })\n\n    it('should return false for undefined', () => {\n      expect(hasIndexOptions(undefined)).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/IndexInfo.utils.ts",
    "content": "import {\n  IndexInfo,\n  IndexOptions,\n} from 'uiSrc/pages/vector-search/hooks/useIndexInfo'\n\nimport { IndexInfoTableData } from './IndexInfo.types'\n\n/**\n * Parses index attributes to table-friendly format.\n * Expects field types to already be normalized (lowercase).\n */\nexport const parseIndexAttributes = (\n  indexInfo: IndexInfo,\n): IndexInfoTableData[] =>\n  indexInfo.attributes.map((field) => ({\n    identifier: field.identifier,\n    attribute: field.attribute,\n    type: field.type,\n    weight: field.weight,\n  }))\n\n/**\n * Formats index options for display.\n * Returns a comma-separated string of option key-value pairs.\n */\nexport const formatOptions = (options: IndexOptions): string => {\n  const optionParts: string[] = []\n\n  if (options.filter) {\n    optionParts.push(`filter: ${options.filter}`)\n  }\n\n  if (options.defaultLang) {\n    optionParts.push(`language: ${options.defaultLang}`)\n  }\n\n  return optionParts.join(', ')\n}\n\n/**\n * Checks if index options object has any meaningful options.\n */\nexport const hasIndexOptions = (options: IndexOptions | undefined): boolean => {\n  if (!options) {\n    return false\n  }\n\n  return Boolean(options.filter || options.defaultLang)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info/index.ts",
    "content": "export { IndexInfo } from './IndexInfo'\nexport type { IndexInfoProps } from './IndexInfo.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info-side-panel/IndexInfoSidePanel.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { IndexInfoSidePanel } from './IndexInfoSidePanel'\nimport { IndexInfoSidePanelProps } from './IndexInfoSidePanel.types'\n\nconst mockUseIndexInfo = jest.fn().mockReturnValue({\n  indexInfo: null,\n  loading: false,\n  error: null,\n  refetch: jest.fn(),\n})\n\njest.mock('../../hooks', () => ({\n  useIndexInfo: (...args: unknown[]) => mockUseIndexInfo(...args),\n}))\n\ndescribe('IndexInfoSidePanel', () => {\n  const defaultProps: IndexInfoSidePanelProps = {\n    onClose: jest.fn(),\n  }\n\n  const renderComponent = (\n    propsOverride?: Partial<IndexInfoSidePanelProps>,\n  ) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<IndexInfoSidePanel {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render panel with header and close button', () => {\n    const indexName = faker.string.alphanumeric(10)\n    renderComponent({ indexName })\n\n    const panel = screen.getByTestId('view-index-panel')\n    const title = screen.getByText(indexName)\n    const closeBtn = screen.getByTestId('close-index-panel-btn')\n\n    expect(panel).toBeInTheDocument()\n    expect(title).toBeInTheDocument()\n    expect(closeBtn).toBeInTheDocument()\n  })\n\n  it('should call onClose when close button is clicked', () => {\n    const mockOnClose = jest.fn()\n    renderComponent({ onClose: mockOnClose })\n\n    const closeBtn = screen.getByTestId('close-index-panel-btn')\n    fireEvent.click(closeBtn)\n\n    expect(mockOnClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should use indexName prop when provided', () => {\n    const indexName = faker.string.alphanumeric(10)\n    renderComponent({ indexName })\n\n    expect(mockUseIndexInfo).toHaveBeenCalledWith({ indexName })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info-side-panel/IndexInfoSidePanel.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Panel = styled(Col)`\n  width: 100%;\n  height: 100%;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.components.card.borderRadius};\n  background-color: ${({ theme }) =>\n    theme.semantic?.color.background.neutral100};\n  overflow: auto;\n`\n\nexport const PanelHeader = styled(Row).attrs({\n  align: 'center',\n  justify: 'between',\n  grow: false,\n})`\n  padding: ${({ theme }) =>\n    `${theme.core.space.space150} ${theme.core.space.space200}`};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n  flex-shrink: 0;\n`\n\nexport const PanelBody = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space200};\n  overflow: auto;\n  flex: 1;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info-side-panel/IndexInfoSidePanel.tsx",
    "content": "import React from 'react'\nimport { useParams } from 'react-router-dom'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\n\nimport { useIndexInfo } from '../../hooks'\nimport { decodeIndexNameFromUrl } from '../../utils'\nimport { IndexInfo } from '../index-info'\n\nimport { IndexInfoSidePanelProps } from './IndexInfoSidePanel.types'\nimport * as S from './IndexInfoSidePanel.styles'\n\nexport const IndexInfoSidePanel = ({\n  onClose,\n  indexName: indexNameProp,\n}: IndexInfoSidePanelProps) => {\n  const { indexName: indexNameParam } = useParams<{ indexName?: string }>()\n  const resolvedName =\n    indexNameProp ?? decodeIndexNameFromUrl(indexNameParam ?? '')\n\n  const { indexInfo } = useIndexInfo({\n    indexName: resolvedName,\n  })\n\n  return (\n    <S.Panel data-testid=\"view-index-panel\">\n      <S.PanelHeader>\n        <Text size=\"L\" color=\"primary\">\n          {resolvedName}\n        </Text>\n        <IconButton\n          icon={CancelIcon}\n          aria-label=\"Close panel\"\n          onClick={onClose}\n          data-testid=\"close-index-panel-btn\"\n        />\n      </S.PanelHeader>\n      <S.PanelBody>\n        <IndexInfo\n          indexInfo={indexInfo ?? undefined}\n          dataTestId=\"view-index-info\"\n        />\n      </S.PanelBody>\n    </S.Panel>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info-side-panel/IndexInfoSidePanel.types.ts",
    "content": "export interface IndexInfoSidePanelProps {\n  onClose: () => void\n  indexName?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-info-side-panel/index.ts",
    "content": "export { IndexInfoSidePanel } from './IndexInfoSidePanel'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/IndexList.config.tsx",
    "content": "import React from 'react'\n\nimport { ColumnDef, Row } from 'uiSrc/components/base/layout/table'\n\nimport {\n  IndexListRow,\n  IndexListColumn,\n  IndexListAction,\n} from './IndexList.types'\nimport {\n  INDEX_LIST_COLUMN_HEADERS,\n  INDEX_LIST_COLUMN_TOOLTIPS,\n} from './constants'\nimport { NameCell } from './components/NameCell/NameCell'\nimport { PrefixCell } from './components/PrefixCell/PrefixCell'\nimport { FieldTypesCell } from './components/FieldTypesCell/FieldTypesCell'\nimport { NumericCell } from './components/NumericCell/NumericCell'\nimport { ActionsCell } from './components/ActionsCell/ActionsCell'\nimport { ColumnHeader } from './components/ColumnHeader/ColumnHeader'\n\nconst createActionsColumn = (\n  onQueryClick?: (indexName: string) => void,\n  actions?: IndexListAction[],\n): ColumnDef<IndexListRow> => ({\n  id: IndexListColumn.Actions,\n  header: INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Actions],\n  enableSorting: false,\n  size: 110,\n  sizeUnit: 'px',\n  cell: ({ row }: { row: Row<IndexListRow> }) => (\n    <ActionsCell\n      row={row.original}\n      onQueryClick={onQueryClick}\n      actions={actions}\n    />\n  ),\n})\n\nconst INDEX_LIST_COLUMNS_BASE: ColumnDef<IndexListRow>[] = [\n  {\n    id: IndexListColumn.Name,\n    accessorKey: IndexListColumn.Name,\n    header: INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Name],\n    enableSorting: true,\n    size: 240,\n    cell: ({ row }: { row: Row<IndexListRow> }) => (\n      <NameCell row={row.original} />\n    ),\n    sortingFn: (rowA, rowB) =>\n      rowA.original.name\n        .toLowerCase()\n        .localeCompare(rowB.original.name.toLowerCase()),\n  },\n  {\n    id: IndexListColumn.Prefix,\n    accessorKey: IndexListColumn.Prefix,\n    header: () => (\n      <ColumnHeader\n        label={INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Prefix]}\n        tooltip={INDEX_LIST_COLUMN_TOOLTIPS[IndexListColumn.Prefix]}\n      />\n    ),\n    enableSorting: false,\n    cell: ({ row }: { row: Row<IndexListRow> }) => (\n      <PrefixCell row={row.original} />\n    ),\n    size: 200,\n  },\n  {\n    id: IndexListColumn.FieldTypes,\n    accessorKey: IndexListColumn.FieldTypes,\n    header: INDEX_LIST_COLUMN_HEADERS[IndexListColumn.FieldTypes],\n    enableSorting: false,\n    size: 220,\n    cell: ({ row }: { row: Row<IndexListRow> }) => (\n      <FieldTypesCell row={row.original} />\n    ),\n  },\n  {\n    id: IndexListColumn.Docs,\n    accessorKey: IndexListColumn.Docs,\n    header: () => (\n      <ColumnHeader\n        label={INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Docs]}\n        tooltip={INDEX_LIST_COLUMN_TOOLTIPS[IndexListColumn.Docs]}\n      />\n    ),\n    enableSorting: true,\n    size: 110,\n    cell: ({ row }) => (\n      <NumericCell\n        value={row.original.numDocs}\n        testId={`index-docs-${row.original.id}`}\n      />\n    ),\n    sortingFn: (rowA, rowB) => rowA.original.numDocs - rowB.original.numDocs,\n  },\n  {\n    id: IndexListColumn.Records,\n    accessorKey: IndexListColumn.Records,\n    header: () => (\n      <ColumnHeader\n        label={INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Records]}\n        tooltip={INDEX_LIST_COLUMN_TOOLTIPS[IndexListColumn.Records]}\n      />\n    ),\n    enableSorting: true,\n    size: 130,\n    cell: ({ row }) => (\n      <NumericCell\n        value={row.original.numRecords}\n        testId={`index-records-${row.original.id}`}\n      />\n    ),\n    sortingFn: (rowA, rowB) =>\n      rowA.original.numRecords - rowB.original.numRecords,\n  },\n  {\n    id: IndexListColumn.Terms,\n    accessorKey: IndexListColumn.Terms,\n    header: () => (\n      <ColumnHeader\n        label={INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Terms]}\n        tooltip={INDEX_LIST_COLUMN_TOOLTIPS[IndexListColumn.Terms]}\n      />\n    ),\n    enableSorting: true,\n    size: 120,\n    cell: ({ row }) => (\n      <NumericCell\n        value={row.original.numTerms}\n        testId={`index-terms-${row.original.id}`}\n      />\n    ),\n    sortingFn: (rowA, rowB) => rowA.original.numTerms - rowB.original.numTerms,\n  },\n  {\n    id: IndexListColumn.Fields,\n    accessorKey: IndexListColumn.Fields,\n    header: () => (\n      <ColumnHeader\n        label={INDEX_LIST_COLUMN_HEADERS[IndexListColumn.Fields]}\n        tooltip={INDEX_LIST_COLUMN_TOOLTIPS[IndexListColumn.Fields]}\n      />\n    ),\n    enableSorting: true,\n    size: 120,\n    cell: ({ row }) => (\n      <NumericCell\n        value={row.original.numFields}\n        testId={`index-fields-${row.original.id}`}\n      />\n    ),\n    sortingFn: (rowA, rowB) =>\n      rowA.original.numFields - rowB.original.numFields,\n  },\n]\n\nexport const getIndexListColumns = (options?: {\n  onQueryClick?: (indexName: string) => void\n  actions?: IndexListAction[]\n}): ColumnDef<IndexListRow>[] => {\n  const actions = options?.actions ?? []\n  return [\n    ...INDEX_LIST_COLUMNS_BASE,\n    createActionsColumn(options?.onQueryClick, actions),\n  ]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/IndexList.spec.tsx",
    "content": "import React from 'react'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  render,\n  screen,\n  userEvent,\n  waitForRiTooltipVisible,\n  within,\n} from 'uiSrc/utils/test-utils'\nimport { mockIndexListData } from 'uiSrc/mocks/factories/vector-search/indexList.factory'\nimport { IndexList } from './IndexList'\nimport { IndexListProps } from './IndexList.types'\n\nconst defaultProps: IndexListProps = {\n  data: mockIndexListData,\n  dataTestId: 'index-list',\n}\n\nconst renderComponent = (props: Partial<IndexListProps> = {}) =>\n  render(<IndexList {...defaultProps} {...props} />)\n\ndescribe('IndexList', () => {\n  beforeEach(() => {\n    cleanup()\n    jest.clearAllMocks()\n  })\n\n  describe('Rendering', () => {\n    it('should render list with correct columns', () => {\n      renderComponent()\n\n      expect(screen.getByTestId('index-list')).toBeInTheDocument()\n      expect(screen.getByText('Index name')).toBeInTheDocument()\n      expect(screen.getByText('Index prefix')).toBeInTheDocument()\n      expect(screen.getByText('Index types')).toBeInTheDocument()\n      expect(screen.getByText('Docs')).toBeInTheDocument()\n      expect(screen.getByText('Records')).toBeInTheDocument()\n      expect(screen.getByText('Terms')).toBeInTheDocument()\n      expect(screen.getByText('Fields')).toBeInTheDocument()\n    })\n\n    it('should render index names correctly', () => {\n      renderComponent()\n\n      expect(screen.getByText(mockIndexListData[0].name)).toBeInTheDocument()\n      expect(screen.getByText(mockIndexListData[1].name)).toBeInTheDocument()\n      expect(screen.getByText(mockIndexListData[2].name)).toBeInTheDocument()\n    })\n\n    it('should render prefixes correctly', () => {\n      renderComponent()\n\n      mockIndexListData.forEach((row) => {\n        if (row.prefixes.length > 0) {\n          const formattedPrefixes = row.prefixes.map((p) => `\"${p}\"`).join(', ')\n          expect(screen.getByText(formattedPrefixes)).toBeInTheDocument()\n        }\n      })\n    })\n\n    it('should render field type badges correctly', () => {\n      renderComponent()\n\n      const firstRow = mockIndexListData[0]\n      const firstIndexTypes = screen.getByTestId(\n        `index-field-types-${firstRow.id}`,\n      )\n      expect(firstIndexTypes).toBeInTheDocument()\n\n      // Verify badges exist for each field type\n      firstRow.fieldTypes.forEach((type) => {\n        expect(\n          screen.getByTestId(`index-field-types-${firstRow.id}--tag-${type}`),\n        ).toBeInTheDocument()\n      })\n    })\n\n    it('should render docs correctly', () => {\n      renderComponent()\n\n      mockIndexListData.forEach((row) => {\n        expect(screen.getByTestId(`index-docs-${row.id}`)).toHaveTextContent(\n          row.numDocs.toString(),\n        )\n      })\n    })\n\n    it('should render records correctly', () => {\n      renderComponent()\n\n      mockIndexListData.forEach((row) => {\n        expect(screen.getByTestId(`index-records-${row.id}`)).toHaveTextContent(\n          row.numRecords.toString(),\n        )\n      })\n    })\n\n    it('should render terms correctly', () => {\n      renderComponent()\n\n      mockIndexListData.forEach((row) => {\n        expect(screen.getByTestId(`index-terms-${row.id}`)).toHaveTextContent(\n          row.numTerms.toString(),\n        )\n      })\n    })\n\n    it('should render fields count correctly', () => {\n      renderComponent()\n\n      mockIndexListData.forEach((row) => {\n        expect(screen.getByTestId(`index-fields-${row.id}`)).toHaveTextContent(\n          row.numFields.toString(),\n        )\n      })\n    })\n\n    it('should render actions column with query buttons when onQueryClick is provided', () => {\n      renderComponent({ onQueryClick: () => {} })\n\n      mockIndexListData.forEach((row) => {\n        expect(\n          screen.getByTestId(`index-actions-${row.id}`),\n        ).toBeInTheDocument()\n        expect(\n          screen.getByTestId(`index-query-btn-${row.id}`),\n        ).toBeInTheDocument()\n      })\n    })\n\n    it('should not render Query button when onQueryClick is omitted', () => {\n      renderComponent()\n\n      mockIndexListData.forEach((row) => {\n        expect(\n          screen.getByTestId(`index-actions-${row.id}`),\n        ).toBeInTheDocument()\n        expect(\n          screen.queryByTestId(`index-query-btn-${row.id}`),\n        ).not.toBeInTheDocument()\n      })\n    })\n  })\n\n  describe('Loading and empty state', () => {\n    it('should show Loading... when loading is true and data is empty', () => {\n      renderComponent({ data: [], loading: true })\n\n      expect(screen.getByText('Loading...')).toBeInTheDocument()\n    })\n\n    it('should show No indexes found when loading is false and data is empty', () => {\n      renderComponent({ data: [], loading: false })\n\n      expect(screen.getByText('No indexes found')).toBeInTheDocument()\n    })\n\n    it('should show index data when loading is true but data is provided', () => {\n      renderComponent({ data: mockIndexListData, loading: true })\n\n      expect(screen.getByText(mockIndexListData[0].name)).toBeInTheDocument()\n    })\n  })\n\n  describe('Column header tooltips', () => {\n    it('shows tooltip when info icon is focused', async () => {\n      renderComponent()\n      const header = screen.getByText('Index prefix')\n      const infoIcon = header.parentElement?.querySelector('svg') as Element\n\n      await act(async () => {\n        fireEvent.focus(infoIcon)\n      })\n      await waitForRiTooltipVisible()\n\n      const tooltip = screen.getAllByText(\n        /Keys matching this prefix are automatically indexed/,\n      )[0]\n      expect(tooltip).toBeInTheDocument()\n    })\n  })\n\n  describe('Actions column callbacks', () => {\n    it('calls onQueryClick with index name when Query is clicked', async () => {\n      const onQueryClick = jest.fn()\n      renderComponent({ data: mockIndexListData, onQueryClick })\n\n      await userEvent.click(\n        screen.getByTestId(`index-query-btn-${mockIndexListData[0].id}`),\n      )\n\n      expect(onQueryClick).toHaveBeenCalledWith(mockIndexListData[0].name)\n    })\n\n    it('calls action callback with index name when menu item is clicked', async () => {\n      const onEdit = jest.fn()\n      renderComponent({\n        data: mockIndexListData,\n        actions: [{ name: 'Edit', callback: onEdit }],\n      })\n\n      const actionsCell = screen.getByTestId(\n        `index-actions-${mockIndexListData[0].id}`,\n      )\n      const [menuTrigger] = within(actionsCell).getAllByRole('button')\n      await userEvent.click(menuTrigger)\n      await userEvent.click(\n        screen.getByTestId(`index-actions-edit-btn-${mockIndexListData[0].id}`),\n      )\n\n      expect(onEdit).toHaveBeenCalledWith(mockIndexListData[0].name)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/IndexList.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport {\n  indexListRowFactory,\n  mockIndexListData,\n} from 'uiSrc/mocks/factories/vector-search/indexList.factory'\nimport { IndexList } from './IndexList'\nimport { IndexListProps, IndexListAction } from './IndexList.types'\n\n// Simple wrapper for stories\nconst IndexListWrapper = (props: IndexListProps) => (\n  <Col style={{ maxWidth: '1200px' }}>\n    <IndexList {...props} />\n  </Col>\n)\n\nconst meta: Meta<typeof IndexList> = {\n  component: IndexList,\n  tags: ['autodocs'],\n  render: (args) => <IndexListWrapper {...args} />,\n  parameters: {\n    docs: {\n      description: {\n        component:\n          'Table of vector search indexes. Pass `onQueryClick` and `actions` to handle the Query button and the row actions menu (e.g. Edit, Delete). Column headers for Index prefix, Docs, Records, Terms, and Fields show an info icon; hover or focus the icon to see a tooltip.',\n      },\n    },\n  },\n  argTypes: {\n    loading: {\n      description:\n        'When true, empty state shows \"Loading...\" instead of \"No indexes found\".',\n      control: 'boolean',\n    },\n    onQueryClick: {\n      description:\n        'Called with the index name when the Query button is clicked.',\n      action: 'onQueryClick',\n    },\n    actions: {\n      description:\n        'Array of { name, callback } for menu items. name is the display name of the action and callback is the function to call when the action is clicked.',\n    },\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    data: mockIndexListData,\n  },\n}\n\n/**\n * Custom `onQueryClick` and `actions` are passed. Use the Actions panel to see\n * callbacks when you click Query or open the menu and choose Edit or Delete.\n */\nexport const WithActionsCallbacks: Story = {\n  args: {\n    data: mockIndexListData,\n    onQueryClick: (_indexName: string) => {},\n    actions: [\n      {\n        name: 'Edit',\n        callback: (_indexName: string) => {\n          // eslint-disable-next-line no-console\n          console.log('Edit')\n        },\n      },\n      {\n        name: 'Delete',\n        callback: (_indexName: string) => {\n          // eslint-disable-next-line no-console\n          console.log('Delete')\n        },\n      },\n    ] satisfies IndexListAction[],\n  },\n  parameters: {\n    docs: {\n      description: {\n        story:\n          'Pass `onQueryClick` and `actions` to handle row actions. Check the Actions panel when clicking Query (if connected) or menu items.',\n      },\n    },\n  },\n}\n\nexport const Empty: Story = {\n  args: {\n    data: [],\n  },\n}\n\nexport const Loading: Story = {\n  args: {\n    data: [],\n    loading: true,\n  },\n  parameters: {\n    docs: {\n      description: {\n        story:\n          'Empty list while loading. The table shows \"Loading...\" in the empty state.',\n      },\n    },\n  },\n}\n\nexport const SingleIndex: Story = {\n  args: {\n    data: [mockIndexListData[0]],\n  },\n}\n\nexport const ManyIndexes: Story = {\n  args: {\n    data: indexListRowFactory.buildList(20),\n  },\n}\n\nexport const AllFieldTypes: Story = {\n  args: {\n    data: [\n      indexListRowFactory.build({\n        fieldTypes: [\n          FieldTypes.TEXT,\n          FieldTypes.TAG,\n          FieldTypes.NUMERIC,\n          FieldTypes.GEO,\n          FieldTypes.VECTOR,\n        ],\n      }),\n    ],\n  },\n}\n\nexport const LongNames: Story = {\n  args: {\n    data: [\n      indexListRowFactory.build({\n        name: 'this-is-a-very-long-index-name-that-should-be-truncated-in-the-ui',\n        prefixes: [\n          'prefix-one:',\n          'prefix-two:',\n          'prefix-three:',\n          'prefix-four:',\n        ],\n      }),\n    ],\n  },\n}\n\nexport const NoPrefixes: Story = {\n  args: {\n    data: [\n      indexListRowFactory.build({\n        prefixes: [],\n      }),\n    ],\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/IndexList.tsx",
    "content": "import React, { memo, useMemo } from 'react'\n\nimport { Table } from 'uiSrc/components/base/layout/table'\n\nimport { IndexListProps } from './IndexList.types'\nimport { getIndexListColumns } from './IndexList.config'\n\nexport const IndexList = memo(\n  ({\n    data,\n    loading,\n    dataTestId = 'index-list',\n    onQueryClick,\n    actions,\n  }: IndexListProps) => {\n    const columns = useMemo(\n      () => getIndexListColumns({ onQueryClick, actions }),\n      [onQueryClick, actions],\n    )\n\n    const hasIndexes = useMemo(() => !!data?.length, [data])\n\n    const emptyMessage = useMemo(() => {\n      if (loading) {\n        return 'Loading...'\n      }\n      if (!hasIndexes) {\n        return 'No indexes found'\n      }\n      return 'No results found'\n    }, [loading, hasIndexes])\n\n    return (\n      <Table\n        data={data}\n        columns={columns}\n        stripedRows\n        minWidth=\"920px\"\n        emptyState={emptyMessage}\n        data-testid={dataTestId}\n      />\n    )\n  },\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/IndexList.types.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { IconType } from 'uiSrc/components/base/icons'\n\n/**\n * Enum representing the column identifiers for the IndexList.\n */\nexport enum IndexListColumn {\n  Name = 'name',\n  Prefix = 'prefixes',\n  FieldTypes = 'fieldTypes',\n  Docs = 'numDocs',\n  Records = 'numRecords',\n  Terms = 'numTerms',\n  Fields = 'numFields',\n  Actions = 'actions',\n}\n\n/**\n * Represents a single row in the IndexList.\n * Contains all the data needed to display an index in the list.\n */\nexport interface IndexListRow {\n  /** Unique identifier for the index */\n  id: string\n  /** Name of the index */\n  name: string\n  /** Key prefixes that this index covers */\n  prefixes: string[]\n  /** Types of fields in this index */\n  fieldTypes: FieldTypes[]\n  /** Number of documents in the index */\n  numDocs: number\n  /** Number of records in the index */\n  numRecords: number\n  /** Number of distinct terms in the index */\n  numTerms: number\n  /** Number of fields/attributes in the index */\n  numFields: number\n}\n\n/**\n * Props for the IndexList component.\n */\nexport interface IndexListProps {\n  /** Array of index data to display in the list */\n  data: IndexListRow[]\n  /** Whether the list data is currently loading */\n  loading?: boolean\n  /** Test ID for the list container */\n  dataTestId?: string\n  /** Callback when the Query button is clicked (index name is passed) */\n  onQueryClick?: (indexName: string) => void\n  /** Actions to render in the row actions menu (e.g. Edit, Delete) */\n  actions?: IndexListAction[]\n}\n\n/**\n * Action item for the index actions menu (e.g. Edit, Delete).\n */\nexport interface IndexListAction {\n  /** Display name in the menu */\n  name: string\n  /** Optional icon displayed next to the menu item */\n  icon?: IconType\n  /** Visual variant for the menu item (e.g. 'destructive' for delete actions) */\n  variant?: 'primary' | 'destructive'\n  /** Callback invoked with the index name when the action is clicked */\n  callback: (indexName: string) => void\n}\n\n/**\n * Props for the actions column cell.\n */\nexport interface ActionsCellProps {\n  row: IndexListRow\n  onQueryClick?: (indexName: string) => void\n  actions?: IndexListAction[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/ActionsCell/ActionsCell.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, userEvent, within } from 'uiSrc/utils/test-utils'\nimport { ActionsCell } from './ActionsCell'\nimport { ActionsCellProps, IndexListRow } from '../../IndexList.types'\n\nconst mockRow = {\n  id: 'idx-products',\n  name: 'products-idx',\n} as IndexListRow\n\nconst renderComponent = (props: Partial<Omit<ActionsCellProps, 'row'>> = {}) =>\n  render(<ActionsCell row={mockRow} {...props} />)\n\ndescribe('ActionsCell', () => {\n  it('renders the actions container', () => {\n    renderComponent()\n    expect(screen.getByTestId('index-actions-idx-products')).toBeInTheDocument()\n  })\n\n  it('renders Query button when onQueryClick is provided', () => {\n    renderComponent({ onQueryClick: () => {} })\n    expect(\n      screen.getByTestId('index-query-btn-idx-products'),\n    ).toBeInTheDocument()\n  })\n\n  it('calls onQueryClick with index name when Query is clicked', async () => {\n    const onQueryClick = jest.fn()\n    renderComponent({ onQueryClick })\n    await userEvent.click(screen.getByTestId('index-query-btn-idx-products'))\n    expect(onQueryClick).toHaveBeenCalledWith('products-idx')\n  })\n\n  it('calls action callback with index name when menu item is clicked', async () => {\n    const onEdit = jest.fn()\n    renderComponent({\n      actions: [{ name: 'Edit', callback: onEdit }],\n    })\n    const actionsCell = screen.getByTestId('index-actions-idx-products')\n    const [menuTrigger] = within(actionsCell).getAllByRole('button')\n    await userEvent.click(menuTrigger)\n    await userEvent.click(\n      screen.getByTestId('index-actions-edit-btn-idx-products'),\n    )\n    expect(onEdit).toHaveBeenCalledWith('products-idx')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/ActionsCell/ActionsCell.tsx",
    "content": "import React, { useCallback } from 'react'\n\nimport { Button } from 'uiSrc/components/base/forms/buttons'\nimport { IconButton } from '@redis-ui/components'\n\nimport { ActionsCellProps } from '../../IndexList.types'\nimport {\n  Menu,\n  MenuContent,\n  MenuDropdownArrow,\n  MenuItem,\n  MenuTrigger,\n} from 'uiSrc/components/base/layout/menu'\nimport { MoreactionsIcon } from 'uiSrc/components/base/icons'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const ActionsCell = ({\n  row,\n  onQueryClick,\n  actions = [],\n}: ActionsCellProps) => {\n  const { id, name } = row\n\n  const handleQueryClick = useCallback(\n    (e: React.MouseEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n\n      onQueryClick?.(name)\n    },\n    [onQueryClick, name],\n  )\n\n  return (\n    <Row\n      wrap\n      data-testid={`index-actions-${id}`}\n      gap=\"m\"\n      align=\"center\"\n      justify=\"center\"\n    >\n      {onQueryClick && (\n        <Button\n          size=\"small\"\n          onClick={handleQueryClick}\n          data-testid={`index-query-btn-${id}`}\n        >\n          Query\n        </Button>\n      )}\n      {actions.length > 0 && (\n        <Menu data-testid={`index-actions-menu-${id}`}>\n          <MenuTrigger>\n            <IconButton\n              icon={MoreactionsIcon}\n              size=\"L\"\n              data-testid={`index-actions-menu-trigger-${id}`}\n            />\n          </MenuTrigger>\n          <MenuContent placement=\"bottom\" align=\"end\">\n            {actions.map((action) => {\n              const handleActionClick = (e: React.MouseEvent) => {\n                e.stopPropagation()\n                action.callback(name)\n              }\n              return (\n                <MenuItem\n                  key={action.name}\n                  icon={action.icon}\n                  variant={action.variant}\n                  text={action.name}\n                  onClick={handleActionClick}\n                  data-testid={`index-actions-${action.name.toLowerCase()}-btn-${id}`}\n                />\n              )\n            })}\n            <MenuDropdownArrow />\n          </MenuContent>\n        </Menu>\n      )}\n    </Row>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/ColumnHeader/ColumnHeader.spec.tsx",
    "content": "import React from 'react'\nimport { act } from '@testing-library/react'\nimport {\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport { ColumnHeader } from './ColumnHeader'\nimport { ColumnHeaderProps } from './ColumnHeader.types'\n\nconst defaultProps: ColumnHeaderProps = {\n  label: 'Test label',\n  tooltip: 'Tooltip content',\n}\n\nconst renderComponent = (props: Partial<ColumnHeaderProps> = {}) =>\n  render(<ColumnHeader {...defaultProps} {...props} />)\n\ndescribe('ColumnHeader', () => {\n  it('should render the label', () => {\n    renderComponent({ label: 'Index name' })\n\n    const label = screen.getByText('Index name')\n    expect(label).toBeInTheDocument()\n  })\n\n  it('should render with tooltip content', () => {\n    const TooltipContent = () => <div>Custom tooltip</div>\n    renderComponent({ label: 'Test label', tooltip: <TooltipContent /> })\n\n    const label = screen.getByText('Test label')\n    expect(label).toBeInTheDocument()\n  })\n\n  it('should show tooltip content when info icon is focused', async () => {\n    renderComponent({\n      label: 'Index prefix',\n      tooltip: 'Keys matching this prefix are automatically indexed.',\n    })\n\n    const header = screen.getByText('Index prefix')\n    const infoIcon = header.parentElement?.querySelector('svg') as Element\n\n    await act(async () => {\n      fireEvent.focus(infoIcon)\n    })\n    await waitForRiTooltipVisible()\n\n    const tooltipContent = screen.getAllByText(\n      'Keys matching this prefix are automatically indexed.',\n    )[0]\n    expect(tooltipContent).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/ColumnHeader/ColumnHeader.tsx",
    "content": "export { ColumnHeader } from 'uiSrc/components/column-header'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/ColumnHeader/ColumnHeader.types.ts",
    "content": "import { ReactNode } from 'react'\n\nexport interface ColumnHeaderProps {\n  label: string\n  tooltip: ReactNode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/FieldTypesCell/FieldTypesCell.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport { FieldTag } from 'uiSrc/pages/vector-search/components/field-tag/FieldTag'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\nimport { IndexListRow } from '../../IndexList.types'\n\nexport const FieldTypesCell = ({ row }: { row: IndexListRow }) => {\n  const fieldTags = useMemo(\n    () =>\n      row.fieldTypes.map((type) => (\n        <FieldTag\n          key={`index-field-types-${row.id}--tag-${type}`}\n          tag={type}\n          dataTestId={`index-field-types-${row.id}--tag-${type}`}\n        />\n      )),\n    [row.fieldTypes, row.id],\n  )\n\n  return (\n    <FlexGroup wrap gap=\"s\" data-testid={`index-field-types-${row.id}`}>\n      {fieldTags}\n    </FlexGroup>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/NameCell/NameCell.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { IndexListRow } from '../../IndexList.types'\n\nexport const NameCell = ({ row }: { row: IndexListRow }) => (\n  <RiTooltip content={row.name} position=\"bottom\">\n    <Text size=\"s\" ellipsis data-testid={`index-name-${row.id}`}>\n      {row.name}\n    </Text>\n  </RiTooltip>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/NumericCell/NumericCell.tsx",
    "content": "import React from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { NumericCellProps } from './NumericCell.types'\n\nexport const NumericCell = ({ value, testId }: NumericCellProps) => (\n  <Text size=\"s\" data-testid={testId}>\n    {value}\n  </Text>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/NumericCell/NumericCell.types.ts",
    "content": "export interface NumericCellProps {\n  /** The numeric value to display */\n  value: number\n  /** Test ID for the cell */\n  testId: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/components/PrefixCell/PrefixCell.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport { Text } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport { IndexListRow } from '../../IndexList.types'\nimport { formatPrefixes } from 'uiSrc/pages/vector-search/utils'\n\nexport const PrefixCell = ({ row }: { row: IndexListRow }) => {\n  const formattedPrefixes = useMemo(\n    () => formatPrefixes(row.prefixes),\n    [row.prefixes],\n  )\n\n  return (\n    <RiTooltip content={formattedPrefixes} position=\"bottom\">\n      <Text\n        size=\"s\"\n        ellipsis\n        data-testid={`index-prefix-${row.id}`}\n        style={{ overflow: 'hidden' }}\n      >\n        {formattedPrefixes}\n      </Text>\n    </RiTooltip>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/constants.ts",
    "content": "import { IndexListColumn } from './IndexList.types'\n\n/**\n * Column header labels for the IndexList component.\n */\nexport const INDEX_LIST_COLUMN_HEADERS: Record<IndexListColumn, string> = {\n  [IndexListColumn.Name]: 'Index name',\n  [IndexListColumn.Prefix]: 'Index prefix',\n  [IndexListColumn.FieldTypes]: 'Index types',\n  [IndexListColumn.Docs]: 'Docs',\n  [IndexListColumn.Records]: 'Records',\n  [IndexListColumn.Terms]: 'Terms',\n  [IndexListColumn.Fields]: 'Fields',\n  [IndexListColumn.Actions]: '',\n}\n\n/**\n * Column header tooltips for the IndexList component.\n */\nexport const INDEX_LIST_COLUMN_TOOLTIPS: Partial<\n  Record<IndexListColumn, string>\n> = {\n  [IndexListColumn.Prefix]:\n    'Keys matching this prefix are automatically indexed.',\n  [IndexListColumn.Docs]: 'Number of documents currently indexed.',\n  [IndexListColumn.Records]:\n    'Total indexed field-value pairs across all documents. One document with 5 fields = 5 records.',\n  [IndexListColumn.Terms]:\n    'Unique words extracted from TEXT fields for full-text search.',\n  [IndexListColumn.Fields]:\n    'Total number of fields defined in the index schema.',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index-list/index.ts",
    "content": "export { IndexList } from './IndexList'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/index.ts",
    "content": "export * from './index-details'\nexport * from './keys-browser'\nexport * from './pick-sample-data-modal'\nexport * from './rqe-not-available'\nexport * from './index-list'\nexport * from './no-search-results'\nexport * from './query-editor'\nexport * from './upgrade-redis-banner'\nexport * from './index-info-side-panel'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/KeysBrowser.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport KeysBrowser from './KeysBrowser'\n\njest.mock('./contexts/Context', () => {\n  const MockReact = require('react')\n  const actual = jest.requireActual('./contexts/Context')\n  return {\n    ...actual,\n    Provider: ({ children }: { children: React.ReactNode }) =>\n      MockReact.createElement(\n        'div',\n        { 'data-testid': 'mock-provider' },\n        children,\n      ),\n  }\n})\n\njest.mock('./components/Header', () => {\n  const MockReact = require('react')\n  return () =>\n    MockReact.createElement('div', { 'data-testid': 'mock-header' }, 'Header')\n})\n\njest.mock('./components/Content', () => {\n  const MockReact = require('react')\n  return () =>\n    MockReact.createElement('div', { 'data-testid': 'mock-content' }, 'Content')\n})\n\njest.mock('./components/Footer', () => {\n  const MockReact = require('react')\n  return () =>\n    MockReact.createElement('div', { 'data-testid': 'mock-footer' }, 'Footer')\n})\n\ndescribe('KeysBrowser (vector-search)', () => {\n  const onSelectKey = jest.fn()\n\n  it('should render the composed layout', () => {\n    render(<KeysBrowser onSelectKey={onSelectKey} />)\n\n    expect(screen.getByTestId('vs-keys-browser')).toBeInTheDocument()\n  })\n\n  it('should render header, content, and footer slots', () => {\n    render(<KeysBrowser onSelectKey={onSelectKey} />)\n\n    expect(screen.getByTestId('mock-header')).toBeInTheDocument()\n    expect(screen.getByTestId('mock-content')).toBeInTheDocument()\n    expect(screen.getByTestId('mock-footer')).toBeInTheDocument()\n  })\n\n  it('should wrap content in the Provider', () => {\n    render(<KeysBrowser onSelectKey={onSelectKey} />)\n\n    expect(screen.getByTestId('mock-provider')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/KeysBrowser.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { Route } from 'react-router-dom'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Card, Spacer } from 'uiSrc/components/base/layout'\nimport { bufferToString } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport KeysBrowser from './KeysBrowser'\nimport { StorePopulator } from './__stories__/StorePopulator'\n\nconst KeysBrowserContent = () => {\n  const [selectedKey, setSelectedKey] = useState<string | null>(null)\n\n  const handleSelectKey = (key: RedisResponseBuffer) => {\n    setSelectedKey(bufferToString(key))\n  }\n\n  return (\n    <Col>\n      <div style={{ width: 300, height: 500, border: '1px solid #ccc' }}>\n        <KeysBrowser onSelectKey={handleSelectKey} />\n      </div>\n\n      <Spacer size=\"l\" />\n      <Card style={{ padding: '10px' }}>\n        <Text size=\"s\" color=\"secondary\">\n          Selected key: {selectedKey ?? '(none)'}\n        </Text>\n      </Card>\n    </Col>\n  )\n}\n\nconst meta: Meta<typeof KeysBrowser> = {\n  component: KeysBrowser,\n  tags: ['autodocs'],\n  parameters: {\n    layout: 'centered',\n  },\n  decorators: [\n    () => (\n      <StorePopulator>\n        <Route path=\"/:instanceId/vector-search\">\n          <KeysBrowserContent />\n        </Route>\n      </StorePopulator>\n    ),\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/KeysBrowser.styles.ts",
    "content": "import styled from 'styled-components'\nimport Tabs from 'uiSrc/components/base/layout/tabs'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { KeysBrowser } from 'uiSrc/components/browser'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const Container = styled(Col)`\n  height: 100%;\n  overflow: hidden;\n`\n\nexport const HeaderWrapper = styled(KeysBrowser.Header).attrs({\n  align: 'center',\n  justify: 'between',\n})`\n  padding: ${({ theme }) => theme.core?.space?.space200};\n  border-bottom: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic?.color?.border.neutral500};\n`\n\nexport const TabBar = styled(Tabs.TabBar.Compose)`\n  padding: ${({ theme }) => theme.core?.space?.space200};\n  padding-bottom: 0;\n`\n\nexport const InfoIconWrapper = styled(FlexItem).attrs({ grow: false })`\n  margin-left: auto;\n  align-items: center;\n`\n\nexport const TreeWrapper = styled(Col)`\n  overflow: hidden;\n`\n\nexport const ErrorWrapper = styled(Col).attrs({\n  contentCentered: true,\n})`\n  overflow: auto;\n  padding: ${({ theme }) => theme.core?.space?.space200};\n`\n\nexport const FooterContainer = styled(KeysBrowser.Footer)`\n  padding: ${({ theme }) =>\n    `${theme.core?.space?.space100} ${theme.core?.space?.space150}`};\n  border-top: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic?.color?.border.neutral500};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic?.color?.background.neutral100};\n`\n\nexport const Separator = styled.hr`\n  border: none;\n  width: 1px;\n  height: ${({ theme }) => theme.core?.space?.space150};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic?.color?.background?.neutral500};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/KeysBrowser.tsx",
    "content": "import React from 'react'\n\nimport { KeysBrowser as KeysBrowserLayout } from 'uiSrc/components/browser'\n\nimport { Provider } from './contexts/Context'\nimport Header from './components/Header'\nimport Content from './components/Content'\nimport Footer from './components/Footer'\nimport { KeysBrowserProps } from './KeysBrowser.types'\nimport * as S from './KeysBrowser.styles'\n\nconst KeysBrowserInner = () => (\n  <S.Container>\n    <KeysBrowserLayout.Compose data-testid=\"vs-keys-browser\">\n      <Header />\n\n      <KeysBrowserLayout.Content>\n        <Content />\n      </KeysBrowserLayout.Content>\n\n      <Footer />\n    </KeysBrowserLayout.Compose>\n  </S.Container>\n)\n\nconst KeysBrowser = (props: KeysBrowserProps) => (\n  <Provider {...props}>\n    <KeysBrowserInner />\n  </Provider>\n)\n\nexport default KeysBrowser\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/KeysBrowser.types.ts",
    "content": "import React from 'react'\n\nimport { KeysStoreData } from 'uiSrc/slices/interfaces/keys'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { KeyTypes } from 'uiSrc/constants'\n\nexport interface KeysBrowserProps {\n  onSelectKey: (key: RedisResponseBuffer, keyType: KeyTypes) => void\n  initialKey?: RedisResponseBuffer\n  initialKeyType?: KeyTypes\n}\n\nexport interface KeysBrowserContextValue {\n  loading: boolean\n  headerLoading: boolean\n\n  keysState: KeysStoreData\n  keysError: string\n  commonFilterType: Nullable<KeyTypes>\n  scrollTopPosition: number\n\n  activeTab: KeyTypes\n\n  isSearched: boolean\n  isFiltered: boolean\n\n  keyListRef: React.RefObject<KeyTreeHandle | null>\n\n  selectKey: ({ rowData }: { rowData: { name: RedisResponseBuffer } }) => void\n\n  handleRefreshKeys: () => void\n  handleEnableAutoRefresh: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  handleChangeAutoRefreshRate: (\n    enableAutoRefresh: boolean,\n    refreshRate: string,\n  ) => void\n  handleTabChange: (tab: KeyTypes) => void\n  loadMoreItems: (\n    oldKeys: IKeyPropTypes[],\n    range: { startIndex: number; stopIndex: number },\n  ) => void\n  handleScanMore: (config: { startIndex: number; stopIndex: number }) => void\n}\n\nexport interface KeyTreeHandle {\n  handleLoadMoreItems: (config: {\n    startIndex: number\n    stopIndex: number\n  }) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/__stories__/StorePopulator.tsx",
    "content": "import React, { useLayoutEffect, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useHistory } from 'react-router-dom'\nimport { faker } from '@faker-js/faker'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { loadKeysSuccess, setFilter } from 'uiSrc/slices/browser/keys'\nimport {\n  setAppContextConnectedInstanceId,\n  setBrowserKeyListDataLoaded,\n} from 'uiSrc/slices/app/context'\nimport { setConnectedInstanceId } from 'uiSrc/slices/instances/instances'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { apiService } from 'uiSrc/services'\n\nconst MOCK_INSTANCE_ID = faker.string.uuid()\n\nconst generateMockKeys = (count: number, type: KeyTypes) =>\n  Array.from({ length: count }, (_, i) => {\n    const name = `${type}:${faker.word.noun()}:${i}`\n    return {\n      nameString: name,\n      name: stringToBuffer(name),\n      type,\n      ttl: faker.helpers.arrayElement([\n        -1,\n        faker.number.int({ min: 60, max: 86400 }),\n      ]),\n      size: faker.number.int({ min: 32, max: 8192 }),\n      length: faker.number.int({ min: 1, max: 500 }),\n    }\n  })\n\nconst MOCK_KEYS_BY_TYPE: Record<string, ReturnType<typeof generateMockKeys>> = {\n  [KeyTypes.Hash]: generateMockKeys(25, KeyTypes.Hash),\n  [KeyTypes.ReJSON]: generateMockKeys(15, KeyTypes.ReJSON),\n}\n\nconst buildMockKeysResponse = (keys: ReturnType<typeof generateMockKeys>) => ({\n  data: [\n    {\n      cursor: 0,\n      total: keys.length,\n      scanned: keys.length,\n      keys,\n    },\n  ],\n  status: 200,\n})\n\nexport const StorePopulator = ({ children }: { children: React.ReactNode }) => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n  const [ready, setReady] = useState(false)\n\n  useLayoutEffect(() => {\n    const interceptorId = apiService.interceptors.response.use(\n      undefined,\n      (error) => {\n        const url = error?.config?.url ?? ''\n\n        if (url.includes('/get-metadata')) {\n          const body = JSON.parse(error.config?.data ?? '{}')\n          const requestedKeys = body.keys || []\n          const keyType = body.type || KeyTypes.Hash\n          const responseData = requestedKeys.map((nameObj: any) => {\n            const bufferData = nameObj?.data ?? {}\n            const dataArray = Array.isArray(bufferData)\n              ? bufferData\n              : Object.keys(bufferData)\n                  .sort((a, b) => Number(a) - Number(b))\n                  .map((k) => bufferData[k])\n            return {\n              name: { type: 'Buffer', data: dataArray },\n              type: keyType,\n              ttl: faker.helpers.arrayElement([\n                -1,\n                faker.number.int({ min: 60, max: 86400 }),\n              ]),\n              size: faker.number.int({ min: 32, max: 8192 }),\n            }\n          })\n          return Promise.resolve({ data: responseData, status: 200 })\n        }\n\n        if (url.includes('/keys')) {\n          const body = JSON.parse(error.config?.data ?? '{}')\n          const keys =\n            MOCK_KEYS_BY_TYPE[body.type] ?? MOCK_KEYS_BY_TYPE[KeyTypes.Hash]\n          return Promise.resolve(buildMockKeysResponse(keys))\n        }\n\n        return Promise.reject(error)\n      },\n    )\n\n    const hashKeys = MOCK_KEYS_BY_TYPE[KeyTypes.Hash]\n\n    dispatch(setConnectedInstanceId(MOCK_INSTANCE_ID))\n    dispatch(setAppContextConnectedInstanceId(MOCK_INSTANCE_ID))\n    dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, true))\n    dispatch(setFilter(KeyTypes.Hash))\n    dispatch(\n      loadKeysSuccess({\n        data: {\n          total: hashKeys.length,\n          scanned: hashKeys.length,\n          nextCursor: '0',\n          keys: hashKeys,\n          shardsMeta: {},\n        },\n        isSearched: false,\n        isFiltered: true,\n      }),\n    )\n\n    history.push(`/${MOCK_INSTANCE_ID}/vector-search`)\n    setReady(true)\n\n    return () => {\n      apiService.interceptors.response.eject(interceptorId)\n    }\n  }, [])\n\n  if (!ready) return null\n  return <>{children}</>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/__stories__/contextMock.ts",
    "content": "import { KeyTypes } from 'uiSrc/constants'\nimport { KeysBrowserContextValue } from '../KeysBrowser.types'\n\nexport const createMockKeysBrowserContext = (\n  overrides: Partial<KeysBrowserContextValue> = {},\n): KeysBrowserContextValue => ({\n  loading: false,\n  headerLoading: false,\n  keysState: {\n    keys: [],\n    nextCursor: '0',\n    total: 0,\n    scanned: 0,\n    lastRefreshTime: null,\n    previousResultCount: 0,\n    shardsMeta: {},\n  },\n  keysError: '',\n  commonFilterType: null,\n  scrollTopPosition: 0,\n  activeTab: KeyTypes.Hash,\n  isSearched: false,\n  isFiltered: false,\n  keyListRef: { current: null },\n  selectKey: jest.fn(),\n  handleRefreshKeys: jest.fn(),\n  handleEnableAutoRefresh: jest.fn(),\n  handleChangeAutoRefreshRate: jest.fn(),\n  handleTabChange: jest.fn(),\n  loadMoreItems: jest.fn(),\n  handleScanMore: jest.fn(),\n  ...overrides,\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Content.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport Content from './Content'\nimport { createMockKeysBrowserContext } from '../__stories__/contextMock'\n\nlet mockContextValue = createMockKeysBrowserContext()\n\njest.mock('../hooks/useKeysBrowser', () => ({\n  useKeysBrowser: () => mockContextValue,\n}))\n\njest.mock('uiSrc/pages/browser/components/key-tree', () => {\n  const MockReact = require('react')\n  return {\n    __esModule: true,\n    default: MockReact.forwardRef(() =>\n      MockReact.createElement(\n        'div',\n        { 'data-testid': 'mock-key-tree' },\n        'KeyTree',\n      ),\n    ),\n  }\n})\n\ndescribe('Content (vector-search keys-browser)', () => {\n  it('should render type tabs', () => {\n    render(<Content />)\n\n    expect(screen.getByTestId('vs-keys-type-tabs')).toBeInTheDocument()\n  })\n\n  it('should render HASH and JSON tab labels', () => {\n    render(<Content />)\n\n    expect(screen.getByText('HASH')).toBeInTheDocument()\n    expect(screen.getByText('JSON')).toBeInTheDocument()\n  })\n\n  it('should render the info icon', () => {\n    render(<Content />)\n\n    expect(screen.getByTestId('vs-keys-info-icon')).toBeInTheDocument()\n  })\n\n  it('should render KeyTree', () => {\n    render(<Content />)\n\n    expect(screen.getByTestId('mock-key-tree')).toBeInTheDocument()\n  })\n\n  it('should render error when keysError is set', () => {\n    mockContextValue.keysError = 'Connection failed'\n\n    render(<Content />)\n\n    expect(screen.getByTestId('vs-keys-error')).toBeInTheDocument()\n    expect(screen.getByText('Connection failed')).toBeInTheDocument()\n\n    mockContextValue.keysError = ''\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Content.tsx",
    "content": "import React from 'react'\n\nimport { KeyTypes } from 'uiSrc/constants'\nimport { Nullable } from 'uiSrc/utils'\nimport { CallOut } from 'uiSrc/components/base/display'\nimport KeyTree from 'uiSrc/pages/browser/components/key-tree'\n\nimport TypeTabs from './TypeTabs'\nimport { useKeysBrowser } from '../hooks/useKeysBrowser'\nimport * as S from '../KeysBrowser.styles'\n\nconst noop = () => {}\n\nconst Content = () => {\n  const {\n    keysState,\n    keysError,\n    loading,\n    commonFilterType,\n    keyListRef,\n    selectKey,\n    loadMoreItems,\n  } = useKeysBrowser()\n\n  return (\n    <>\n      <TypeTabs />\n      <S.TreeWrapper>\n        {keysError && (\n          <S.ErrorWrapper>\n            <CallOut variant=\"danger\" data-testid=\"vs-keys-error\">\n              {keysError}\n            </CallOut>\n          </S.ErrorWrapper>\n        )}\n        {!keysError && (\n          <KeyTree\n            ref={keyListRef}\n            keysState={keysState}\n            loading={loading}\n            deleting={false}\n            commonFilterType={commonFilterType as Nullable<KeyTypes>}\n            selectKey={selectKey}\n            loadMoreItems={loadMoreItems}\n            onDelete={noop}\n            onAddKeyPanel={noop}\n            onBulkActionsPanel={noop}\n            visibleColumns={[]}\n            showFolderMetadata={false}\n            showDeleteAction={false}\n            showSelectedIndicator\n          />\n        )}\n      </S.TreeWrapper>\n    </>\n  )\n}\n\nexport default React.memo(Content)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Footer.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport Footer from './Footer'\nimport { createMockKeysBrowserContext } from '../__stories__/contextMock'\n\nlet mockContextValue = createMockKeysBrowserContext({\n  keysState: {\n    keys: [],\n    nextCursor: '0',\n    total: 100,\n    scanned: 0,\n    lastRefreshTime: null,\n    previousResultCount: 0,\n    shardsMeta: {},\n  },\n})\n\njest.mock('../hooks/useKeysBrowser', () => ({\n  useKeysBrowser: () => mockContextValue,\n}))\n\ndescribe('Footer (vector-search keys-browser)', () => {\n  it('should render summary container', () => {\n    render(<Footer />)\n\n    expect(screen.getByTestId('vs-keys-summary')).toBeInTheDocument()\n  })\n\n  it('should display total when not scanned', () => {\n    render(<Footer />)\n\n    expect(screen.getByText(/Total:/)).toBeInTheDocument()\n  })\n\n  it('should display results when scanned', () => {\n    mockContextValue.isSearched = true\n    mockContextValue.keysState = {\n      ...mockContextValue.keysState,\n      keys: Array.from({ length: 50 }, (_, i) => ({\n        name: { data: [i], type: 'Buffer' },\n        nameString: `key:${i}`,\n        type: 'hash',\n        ttl: -1,\n        size: 100,\n      })) as any,\n      scanned: 3000,\n      total: 126339,\n    }\n\n    render(<Footer />)\n\n    expect(screen.getByTestId('vs-keys-number-of-results')).toHaveTextContent(\n      '50',\n    )\n    expect(screen.getByTestId('vs-keys-number-of-scanned')).toHaveTextContent(\n      '3 000',\n    )\n    expect(screen.getByTestId('vs-keys-total')).toHaveTextContent('126 339')\n\n    mockContextValue.isSearched = false\n  })\n\n  it('should display scanning text when loading with no total', () => {\n    mockContextValue.headerLoading = true\n    mockContextValue.keysState = {\n      ...mockContextValue.keysState,\n      keys: [],\n      total: 0,\n      scanned: 0,\n    }\n\n    render(<Footer />)\n\n    expect(screen.getByTestId('vs-scanning-text')).toBeInTheDocument()\n\n    mockContextValue.headerLoading = false\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Footer.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { Route } from 'react-router-dom'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport Footer from './Footer'\nimport { StorePopulator } from '../__stories__/StorePopulator'\nimport { Provider } from '../contexts/Context'\nconst FooterContent = () => {\n  return (\n    <Col style={{ width: 300, height: 500, border: '1px solid transparent' }}>\n      <Footer />\n    </Col>\n  )\n}\nconst meta = {\n  component: Footer,\n  decorators: [\n    () => (\n      <StorePopulator>\n        <Route path=\"/:instanceId/vector-search\">\n          <Provider onSelectKey={() => {}}>\n            <FooterContent />\n          </Provider>\n        </Route>\n      </StorePopulator>\n    ),\n  ],\n} satisfies Meta<typeof Footer>\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Footer.tsx",
    "content": "import React from 'react'\nimport { isNull } from 'lodash'\n\nimport ScanMore from 'uiSrc/components/scan-more'\nimport { numberWithSpaces, nullableNumberWithSpaces } from 'uiSrc/utils/numbers'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport * as S from '../KeysBrowser.styles'\n\nimport { useKeysBrowser } from '../hooks/useKeysBrowser'\n\nconst Footer = () => {\n  const { keysState, headerLoading, isSearched, isFiltered, handleScanMore } =\n    useKeysBrowser()\n\n  const footerScanned = isSearched || isFiltered ? keysState.scanned : 0\n\n  const footerScannedDisplay =\n    keysState.keys.length > (footerScanned ?? 0)\n      ? keysState.keys.length\n      : (footerScanned ?? 0)\n\n  const footerNotAccurateScanned =\n    keysState.total &&\n    (footerScanned ?? 0) >= keysState.total &&\n    keysState.nextCursor &&\n    keysState.nextCursor !== '0'\n      ? '~'\n      : ''\n\n  return (\n    <S.FooterContainer data-testid=\"keys-browser-footer\">\n      <Row align=\"center\" justify=\"between\" grow data-testid=\"vs-keys-summary\">\n        <Row gap=\"s\" align=\"center\" grow={false}>\n          {headerLoading && !keysState.total && !isNull(keysState.total) && (\n            <ColorText\n              size=\"xs\"\n              color=\"secondary\"\n              data-testid=\"vs-scanning-text\"\n            >\n              Scanning...\n            </ColorText>\n          )}\n          {!!footerScanned && (\n            <>\n              <ColorText size=\"xs\" color=\"secondary\" component=\"span\">\n                {'Results: '}\n                <span data-testid=\"vs-keys-number-of-results\">\n                  {numberWithSpaces(keysState.keys.length)}\n                </span>\n                {' keys'}\n              </ColorText>\n              <S.Separator />\n              <ColorText size=\"xs\" color=\"secondary\" component=\"span\">\n                {'Scanned '}\n                <span data-testid=\"vs-keys-number-of-scanned\">\n                  {footerNotAccurateScanned}\n                  {numberWithSpaces(footerScannedDisplay)}\n                </span>\n                {'/'}\n                <span data-testid=\"vs-keys-total\">\n                  {nullableNumberWithSpaces(keysState.total)}\n                </span>\n              </ColorText>\n            </>\n          )}\n          {!footerScanned && (!!keysState.total || isNull(keysState.total)) && (\n            <ColorText size=\"xs\" color=\"secondary\" component=\"span\">\n              {'Total: '}\n              {nullableNumberWithSpaces(keysState.total)}\n            </ColorText>\n          )}\n        </Row>\n        <FlexItem grow={false}>\n          <ScanMore\n            withAlert={false}\n            scanned={footerScanned}\n            totalItemsCount={keysState.total}\n            loading={headerLoading}\n            loadMoreItems={handleScanMore}\n            nextCursor={keysState.nextCursor}\n          />\n        </FlexItem>\n      </Row>\n    </S.FooterContainer>\n  )\n}\n\nexport default React.memo(Footer)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Header.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport Header from './Header'\nimport { createMockKeysBrowserContext } from '../__stories__/contextMock'\n\nlet mockContextValue = createMockKeysBrowserContext()\n\njest.mock('../hooks/useKeysBrowser', () => ({\n  useKeysBrowser: () => mockContextValue,\n}))\n\njest.mock('uiSrc/pages/browser/components/key-tree', () => {\n  const MockReact = require('react')\n  return {\n    KeyTreeSettings: () =>\n      MockReact.createElement(\n        'div',\n        { 'data-testid': 'tree-view-settings-btn' },\n        'Settings',\n      ),\n  }\n})\n\ndescribe('Header (vector-search keys-browser)', () => {\n  it('should render \"Select key\" title', () => {\n    render(<Header />)\n\n    expect(screen.getByText('Select key')).toBeInTheDocument()\n  })\n\n  it('should render AutoRefresh controls', () => {\n    render(<Header />)\n\n    expect(screen.getByTestId('vs-keys-refresh-btn')).toBeInTheDocument()\n  })\n\n  it('should render KeyTreeSettings', () => {\n    render(<Header />)\n\n    expect(screen.getByTestId('tree-view-settings-btn')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/Header.tsx",
    "content": "import React from 'react'\n\nimport { AutoRefresh } from 'uiSrc/components'\nimport { FlexItem, Row } from 'uiSrc/components/base/layout/flex'\nimport { Title } from 'uiSrc/components/base/text'\nimport { KeyTreeSettings } from 'uiSrc/pages/browser/components/key-tree'\n\nimport { SelectKeyOnboardingPopover } from '../../select-key-onboarding-popover'\nimport { useKeysBrowser } from '../hooks/useKeysBrowser'\nimport * as S from '../KeysBrowser.styles'\n\nconst Header = () => {\n  const {\n    loading,\n    keysState,\n    handleRefreshKeys,\n    handleEnableAutoRefresh,\n    handleChangeAutoRefreshRate,\n  } = useKeysBrowser()\n\n  return (\n    <S.HeaderWrapper>\n      <Row align=\"center\" justify=\"between\">\n        <SelectKeyOnboardingPopover>\n          <FlexItem grow={false}>\n            <Title size=\"S\" variant=\"semiBold\" color=\"primary\">\n              Select key\n            </Title>\n          </FlexItem>\n        </SelectKeyOnboardingPopover>\n        <Row gap=\"m\" align=\"center\" grow={false}>\n          <FlexItem>\n            <AutoRefresh\n              iconSize=\"S\"\n              postfix=\"vs-keys\"\n              loading={loading}\n              lastRefreshTime={keysState.lastRefreshTime}\n              displayLastRefresh={false}\n              onRefresh={handleRefreshKeys}\n              onEnableAutoRefresh={handleEnableAutoRefresh}\n              onChangeAutoRefreshRate={handleChangeAutoRefreshRate}\n              testid=\"vs-keys\"\n            />\n          </FlexItem>\n          <FlexItem>\n            <KeyTreeSettings loading={loading} />\n          </FlexItem>\n        </Row>\n      </Row>\n    </S.HeaderWrapper>\n  )\n}\n\nexport default React.memo(Header)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/TypeTabs.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { KeyTypes } from 'uiSrc/constants'\n\nimport TypeTabs from './TypeTabs'\nimport { createMockKeysBrowserContext } from '../__stories__/contextMock'\n\nlet mockContextValue = createMockKeysBrowserContext()\n\njest.mock('../hooks/useKeysBrowser', () => ({\n  useKeysBrowser: () => mockContextValue,\n}))\n\ndescribe('TypeTabs (vector-search keys-browser)', () => {\n  beforeEach(() => {\n    mockContextValue = createMockKeysBrowserContext()\n  })\n\n  it('should render tabs container', () => {\n    render(<TypeTabs />)\n\n    expect(screen.getByTestId('vs-keys-type-tabs')).toBeInTheDocument()\n  })\n\n  it('should render HASH and JSON tab labels', () => {\n    render(<TypeTabs />)\n\n    expect(screen.getByText('HASH')).toBeInTheDocument()\n    expect(screen.getByText('JSON')).toBeInTheDocument()\n  })\n\n  it('should render info icon with tooltip', () => {\n    render(<TypeTabs />)\n\n    expect(screen.getByTestId('vs-keys-info-icon')).toBeInTheDocument()\n  })\n\n  it('should render with HASH as the default active tab', () => {\n    render(<TypeTabs />)\n\n    expect(screen.getByTestId('vs-keys-type-tabs')).toBeInTheDocument()\n    expect(screen.getByText('HASH')).toBeInTheDocument()\n  })\n\n  it('should render with JSON as the active tab', () => {\n    mockContextValue = createMockKeysBrowserContext({\n      activeTab: KeyTypes.ReJSON,\n    })\n\n    render(<TypeTabs />)\n\n    expect(screen.getByTestId('vs-keys-type-tabs')).toBeInTheDocument()\n    expect(screen.getByText('JSON')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/components/TypeTabs.tsx",
    "content": "import React from 'react'\n\nimport { KeyTypes } from 'uiSrc/constants'\nimport Tabs from 'uiSrc/components/base/layout/tabs'\nimport { RiTooltip } from 'uiSrc/components/base'\nimport { RiIcon } from 'uiSrc/components/base/icons'\n\nimport { useKeysBrowser } from '../hooks/useKeysBrowser'\nimport * as S from '../KeysBrowser.styles'\n\nconst TABS = [\n  { value: KeyTypes.Hash, label: 'HASH' },\n  { value: KeyTypes.ReJSON, label: 'JSON' },\n]\n\nconst TypeTabs = () => {\n  const { activeTab, handleTabChange } = useKeysBrowser()\n\n  return (\n    <Tabs.Compose\n      value={activeTab}\n      onChange={(value: string) => handleTabChange(value as KeyTypes)}\n      data-testid=\"vs-keys-type-tabs\"\n    >\n      <S.TabBar>\n        {TABS.map((tab) => (\n          <Tabs.TabBar.Trigger value={tab.value} key={tab.value}>\n            {tab.label}\n          </Tabs.TabBar.Trigger>\n        ))}\n        <S.InfoIconWrapper>\n          <RiTooltip\n            content=\"Only HASH and JSON key types are supported for index creation.\"\n            position=\"top\"\n            anchorClassName=\"flex-row\"\n          >\n            <RiIcon\n              type=\"InfoIcon\"\n              size=\"m\"\n              style={{ cursor: 'pointer' }}\n              data-testid=\"vs-keys-info-icon\"\n            />\n          </RiTooltip>\n        </S.InfoIconWrapper>\n      </S.TabBar>\n    </Tabs.Compose>\n  )\n}\n\nexport default React.memo(TypeTabs)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/contexts/Context.tsx",
    "content": "import React, {\n  createContext,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  appContextBrowser,\n  resetBrowserTree,\n  setBrowserKeyListDataLoaded,\n  setBrowserSelectedKey,\n} from 'uiSrc/slices/app/context'\nimport {\n  fetchKeys,\n  fetchMoreKeys,\n  keysDataSelector,\n  keysSelector,\n  loadKeyInfoSuccess,\n  resetKeyInfo,\n  resetKeysData,\n  setFilter,\n} from 'uiSrc/slices/browser/keys'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport {\n  setConnectedInstanceId,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport { SCAN_TREE_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { Nullable } from 'uiSrc/utils'\nimport { KeyTypes } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport {\n  KeysBrowserContextValue,\n  KeysBrowserProps,\n  KeyTreeHandle,\n} from '../KeysBrowser.types'\n\nexport const KeysBrowserContext = createContext<KeysBrowserContextValue | null>(\n  null,\n)\n\nconst SUPPORTED_TABS = [KeyTypes.Hash, KeyTypes.ReJSON] as const\n\nexport const Provider = ({\n  onSelectKey,\n  initialKey,\n  initialKeyType,\n  children,\n}: KeysBrowserProps & { children: React.ReactNode }) => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const keysState = useSelector(keysDataSelector)\n  const {\n    loading,\n    isSearched,\n    isFiltered,\n    filter,\n    error: keysError,\n  } = useSelector(keysSelector)\n  const { id: connectedInstanceId } = useSelector(connectedInstanceSelector)\n  const {\n    keyList: { scrollPatternTopPosition },\n  } = useSelector(appContextBrowser)\n  const dispatch = useDispatch()\n\n  const [activeTab, setActiveTab] = useState<KeyTypes>(\n    initialKeyType ?? SUPPORTED_TABS[0],\n  )\n\n  const keyListRef = useRef<KeyTreeHandle | null>(null)\n\n  const loadKeys = useCallback(() => {\n    dispatch(\n      fetchKeys(\n        {\n          searchMode: SearchMode.Pattern,\n          cursor: '0',\n          count: SCAN_TREE_COUNT_DEFAULT,\n        },\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, true)),\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false)),\n      ),\n    )\n  }, [instanceId])\n\n  useEffect(() => {\n    if (connectedInstanceId !== instanceId) {\n      dispatch(setConnectedInstanceId(instanceId))\n    }\n\n    dispatch(resetKeysData(SearchMode.Pattern))\n    dispatch(resetBrowserTree())\n    dispatch(setFilter(activeTab))\n    loadKeys()\n\n    return () => {\n      dispatch(setFilter(null))\n      dispatch(resetKeyInfo())\n      dispatch(resetKeysData(SearchMode.Pattern))\n      dispatch(resetBrowserTree())\n      dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false))\n    }\n  }, [dispatch, loadKeys])\n\n  const initialKeyAppliedRef = useRef(false)\n\n  useEffect(() => {\n    if (initialKey && !initialKeyAppliedRef.current && keysState.keys.length) {\n      initialKeyAppliedRef.current = true\n      dispatch(loadKeyInfoSuccess({ name: initialKey }))\n      onSelectKey(initialKey, activeTab)\n    }\n  }, [initialKey, keysState.keys.length, onSelectKey, activeTab])\n\n  const loadMoreItems = useCallback(\n    (\n      oldKeys: IKeyPropTypes[],\n      { startIndex, stopIndex }: { startIndex: number; stopIndex: number },\n    ) => {\n      if (keysState.nextCursor !== '0') {\n        dispatch(\n          fetchMoreKeys(\n            SearchMode.Pattern,\n            oldKeys,\n            keysState.nextCursor,\n            stopIndex - startIndex + 1,\n          ),\n        )\n      }\n    },\n    [keysState.nextCursor],\n  )\n\n  const handleRefreshKeys = useCallback(() => {\n    dispatch(\n      fetchKeys(\n        {\n          searchMode: SearchMode.Pattern,\n          cursor: '0',\n          count: SCAN_TREE_COUNT_DEFAULT,\n        },\n        (data) => {\n          const keys = Array.isArray(data) ? data[0].keys : data.keys\n\n          if (!keys.length) {\n            dispatch(resetKeyInfo())\n            dispatch(setBrowserSelectedKey(null))\n          }\n\n          dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, true))\n          dispatch(setConnectivityError(null))\n        },\n        () => dispatch(setBrowserKeyListDataLoaded(SearchMode.Pattern, false)),\n      ),\n    )\n  }, [])\n\n  const handleEnableAutoRefresh = useCallback(\n    (enableAutoRefresh: boolean, refreshRate: string) => {\n      sendEventTelemetry({\n        event: enableAutoRefresh\n          ? TelemetryEvent.TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED\n          : TelemetryEvent.TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED,\n        eventData: {\n          databaseId: instanceId,\n          refreshRate: +refreshRate,\n        },\n      })\n    },\n    [instanceId],\n  )\n\n  const handleChangeAutoRefreshRate = useCallback(\n    (enableAutoRefresh: boolean, refreshRate: string) => {\n      if (enableAutoRefresh) {\n        handleEnableAutoRefresh(enableAutoRefresh, refreshRate)\n      }\n    },\n    [handleEnableAutoRefresh],\n  )\n\n  const handleTabChange = useCallback(\n    (tab: KeyTypes) => {\n      setActiveTab(tab)\n      dispatch(resetBrowserTree())\n      dispatch(resetKeysData(SearchMode.Pattern))\n      dispatch(setFilter(tab))\n      loadKeys()\n    },\n    [loadKeys],\n  )\n\n  const selectKey = useCallback(\n    ({ rowData }: { rowData: { name: RedisResponseBuffer } }) => {\n      dispatch(loadKeyInfoSuccess({ name: rowData.name }))\n      onSelectKey(rowData.name, activeTab)\n    },\n    [onSelectKey, activeTab],\n  )\n\n  const handleScanMore = useCallback(\n    (config: { startIndex: number; stopIndex: number }) => {\n      keyListRef.current?.handleLoadMoreItems?.({\n        ...config,\n        stopIndex: SCAN_TREE_COUNT_DEFAULT - 1,\n      })\n    },\n    [],\n  )\n\n  const value: KeysBrowserContextValue = useMemo(\n    () => ({\n      loading,\n      headerLoading: loading,\n\n      keysState,\n      keysError,\n      commonFilterType: filter as Nullable<KeyTypes>,\n      scrollTopPosition: scrollPatternTopPosition,\n\n      activeTab,\n\n      isSearched,\n      isFiltered,\n\n      keyListRef,\n\n      selectKey,\n\n      handleRefreshKeys,\n      handleEnableAutoRefresh,\n      handleChangeAutoRefreshRate,\n      handleTabChange,\n      loadMoreItems,\n      handleScanMore,\n    }),\n    [\n      loading,\n      keysState,\n      keysError,\n      filter,\n      scrollPatternTopPosition,\n      activeTab,\n      isSearched,\n      isFiltered,\n      selectKey,\n      handleRefreshKeys,\n      handleEnableAutoRefresh,\n      handleChangeAutoRefreshRate,\n      handleTabChange,\n      loadMoreItems,\n      handleScanMore,\n    ],\n  )\n\n  return (\n    <KeysBrowserContext.Provider value={value}>\n      {children}\n    </KeysBrowserContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/hooks/useKeysBrowser.ts",
    "content": "import { useContext } from 'react'\n\nimport { KeysBrowserContextValue } from '../KeysBrowser.types'\nimport { KeysBrowserContext } from '../contexts/Context'\n\nexport const useKeysBrowser = (): KeysBrowserContextValue => {\n  const ctx = useContext(KeysBrowserContext)\n  if (!ctx) {\n    throw new Error('useKeysBrowser must be used within keys-browser Provider')\n  }\n  return ctx\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/keys-browser/index.ts",
    "content": "export { default as KeysBrowser } from './KeysBrowser'\nexport type {\n  KeysBrowserProps,\n  KeysBrowserContextValue,\n} from './KeysBrowser.types'\nexport { useKeysBrowser } from './hooks/useKeysBrowser'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/no-search-results/NoSearchResults.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { NoSearchResults } from './NoSearchResults'\n\ndescribe('NoSearchResults', () => {\n  it('should render correctly with default title', () => {\n    render(<NoSearchResults />)\n\n    expect(screen.getByTestId('no-search-results')).toBeInTheDocument()\n    expect(\n      screen.getByText(\n        'Your query results will appear here once you run a query.',\n      ),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/no-search-results/NoSearchResults.stories.tsx",
    "content": "import React from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { NoSearchResults } from './NoSearchResults'\n\nconst meta: Meta<typeof NoSearchResults> = {\n  component: NoSearchResults,\n  tags: ['autodocs'],\n  decorators: [\n    (Story) => (\n      <Row>\n        <Story />\n      </Row>\n    ),\n  ],\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {},\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/no-search-results/NoSearchResults.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Col)`\n  margin: auto;\n`\n\nexport const Image = styled.img`\n  max-width: 120px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/no-search-results/NoSearchResults.tsx",
    "content": "import React from 'react'\nimport { useTheme } from '@redis-ui/styles'\nimport { Text } from 'uiSrc/components/base/text'\nimport NoQueryResultsIcon from 'uiSrc/assets/img/vector-search/no-query-results.svg'\nimport NoQueryResultsIconDark from 'uiSrc/assets/img/vector-search/no-query-results-dark.svg'\n\nimport * as S from './NoSearchResults.styles'\n\nexport const NoSearchResults = () => {\n  const theme = useTheme()\n  const icon =\n    theme.name === 'dark' ? NoQueryResultsIconDark : NoQueryResultsIcon\n\n  return (\n    <S.Container\n      gap=\"xxl\"\n      data-testid=\"no-search-results\"\n      align=\"center\"\n      justify=\"center\"\n    >\n      <S.Image as=\"img\" src={icon} alt=\"No search results\" />\n      <Text size=\"M\">\n        Your query results will appear here once you run a query.\n      </Text>\n    </S.Container>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/no-search-results/index.ts",
    "content": "export { NoSearchResults } from './NoSearchResults'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/PickSampleDataModal.constants.ts",
    "content": "import {\n  SampleDataContent,\n  SampleDataOption,\n} from './PickSampleDataModal.types'\n\nexport const MODAL_TITLE = 'Getting your sample data ready for Search'\nexport const MODAL_SUBTITLE_LINE_1 = 'Select a sample dataset.'\nexport const MODAL_SUBTITLE_LINE_2 =\n  \"We'll load the data and generate the index needed for search.\"\nexport const CANCEL_BUTTON_TEXT = 'Cancel'\nexport const SEE_INDEX_DEFINITION_BUTTON_TEXT = 'See index definition'\nexport const START_QUERYING_BUTTON_TEXT = 'Start querying'\n\nexport const SAMPLE_DATA_OPTIONS: SampleDataOption[] = [\n  {\n    value: SampleDataContent.E_COMMERCE_DISCOVERY,\n    label: 'E-commerce Discovery',\n    description: 'Discover products that match intent, not just text',\n  },\n  {\n    value: SampleDataContent.CONTENT_RECOMMENDATIONS,\n    label: 'Content recommendations',\n    description: 'Discover content by theme or plot.',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/PickSampleDataModal.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { PickSampleDataModal } from './PickSampleDataModal'\nimport {\n  PickSampleDataModalProps,\n  SampleDataContent,\n} from './PickSampleDataModal.types'\nimport { SAMPLE_DATA_OPTIONS } from './PickSampleDataModal.constants'\n\nconst mockedOnSelectDataset = jest.fn()\nconst mockedOnCancel = jest.fn()\nconst mockedOnSeeIndexDefinition = jest.fn()\nconst mockedOnStartQuerying = jest.fn()\n\nconst renderComponent = (props?: Partial<PickSampleDataModalProps>) => {\n  const defaultProps: PickSampleDataModalProps = {\n    isOpen: true,\n    selectedDataset: null,\n    onSelectDataset: mockedOnSelectDataset,\n    onCancel: mockedOnCancel,\n    onSeeIndexDefinition: mockedOnSeeIndexDefinition,\n    onStartQuerying: mockedOnStartQuerying,\n  }\n\n  return render(<PickSampleDataModal {...defaultProps} {...props} />)\n}\n\ndescribe('PickSampleDataModal', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render nothing when isOpen is false', () => {\n    renderComponent({ isOpen: false })\n\n    expect(\n      screen.queryByTestId('pick-sample-data-modal--heading'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render modal content when isOpen is true', () => {\n    renderComponent()\n\n    expect(\n      screen.getByTestId('pick-sample-data-modal--heading'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByTestId('pick-sample-data-modal--subtitle'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByTestId('pick-sample-data-modal--illustration'),\n    ).toBeInTheDocument()\n  })\n\n  it('should render all sample data option cards', () => {\n    renderComponent()\n\n    SAMPLE_DATA_OPTIONS.forEach((option) => {\n      expect(\n        screen.getByTestId(`pick-sample-data-modal--option-${option.value}`),\n      ).toBeInTheDocument()\n      expect(screen.getByText(option.label)).toBeInTheDocument()\n      expect(screen.getByText(option.description)).toBeInTheDocument()\n    })\n  })\n\n  it('should call onSelectDataset when a card is clicked', () => {\n    renderComponent()\n\n    const ecommerceOption = screen.getByTestId(\n      `pick-sample-data-modal--option-${SampleDataContent.E_COMMERCE_DISCOVERY}`,\n    )\n    fireEvent.click(ecommerceOption)\n\n    expect(mockedOnSelectDataset).toHaveBeenCalledWith(\n      SampleDataContent.E_COMMERCE_DISCOVERY,\n    )\n  })\n\n  it('should disable \"Start querying\" button when no dataset is selected', () => {\n    renderComponent({ selectedDataset: null })\n\n    const startButton = screen.getByTestId(\n      'pick-sample-data-modal--start-querying',\n    )\n    expect(startButton).toBeDisabled()\n  })\n\n  it('should disable \"See index definition\" button when no dataset is selected', () => {\n    renderComponent({ selectedDataset: null })\n\n    const seeIndexButton = screen.getByTestId(\n      'pick-sample-data-modal--see-index-definition',\n    )\n    expect(seeIndexButton).toBeDisabled()\n  })\n\n  it('should enable action buttons when a dataset is selected', () => {\n    renderComponent({\n      selectedDataset: SampleDataContent.E_COMMERCE_DISCOVERY,\n    })\n\n    const startButton = screen.getByTestId(\n      'pick-sample-data-modal--start-querying',\n    )\n    const seeIndexButton = screen.getByTestId(\n      'pick-sample-data-modal--see-index-definition',\n    )\n\n    expect(startButton).not.toBeDisabled()\n    expect(seeIndexButton).not.toBeDisabled()\n  })\n\n  it('should call onStartQuerying with selected dataset when \"Start querying\" is clicked', () => {\n    renderComponent({\n      selectedDataset: SampleDataContent.CONTENT_RECOMMENDATIONS,\n    })\n\n    const startButton = screen.getByTestId(\n      'pick-sample-data-modal--start-querying',\n    )\n    fireEvent.click(startButton)\n\n    expect(mockedOnStartQuerying).toHaveBeenCalledWith(\n      SampleDataContent.CONTENT_RECOMMENDATIONS,\n    )\n  })\n\n  it('should call onSeeIndexDefinition with selected dataset when \"See index definition\" is clicked', () => {\n    renderComponent({\n      selectedDataset: SampleDataContent.E_COMMERCE_DISCOVERY,\n    })\n\n    const seeIndexButton = screen.getByTestId(\n      'pick-sample-data-modal--see-index-definition',\n    )\n    fireEvent.click(seeIndexButton)\n\n    expect(mockedOnSeeIndexDefinition).toHaveBeenCalledWith(\n      SampleDataContent.E_COMMERCE_DISCOVERY,\n    )\n  })\n\n  it('should call onCancel when \"Cancel\" button is clicked', () => {\n    renderComponent()\n\n    const cancelButton = screen.getByTestId('pick-sample-data-modal--cancel')\n    fireEvent.click(cancelButton)\n\n    expect(mockedOnCancel).toHaveBeenCalled()\n  })\n\n  it('should call onCancel when close (X) button is clicked', () => {\n    renderComponent()\n\n    const closeButton = screen.getByTestId('pick-sample-data-modal--close')\n    fireEvent.click(closeButton)\n\n    expect(mockedOnCancel).toHaveBeenCalled()\n  })\n\n  it('should disable \"Start querying\" button when loading', () => {\n    renderComponent({\n      loading: true,\n      selectedDataset: SampleDataContent.E_COMMERCE_DISCOVERY,\n    })\n\n    const startButton = screen.getByTestId(\n      'pick-sample-data-modal--start-querying',\n    )\n    expect(startButton).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/PickSampleDataModal.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { PickSampleDataModal } from './PickSampleDataModal'\nimport {\n  SampleDataContent,\n  PickSampleDataModalProps,\n} from './PickSampleDataModal.types'\n\n/**\n * Example parent wrapper that demonstrates how to wire the modal\n * with local state. This is the pattern RI-7920 should follow\n * when integrating into the Create Index flow.\n */\nconst PickSampleDataModalWithState = (\n  props: Omit<\n    PickSampleDataModalProps,\n    'isOpen' | 'selectedDataset' | 'onSelectDataset' | 'onCancel'\n  >,\n) => {\n  const [isOpen, setIsOpen] = useState(true)\n  const [selectedDataset, setSelectedDataset] =\n    useState<SampleDataContent | null>(null)\n\n  return (\n    <>\n      {!isOpen && (\n        <button type=\"button\" onClick={() => setIsOpen(true)}>\n          Open modal\n        </button>\n      )}\n      <PickSampleDataModal\n        isOpen={isOpen}\n        selectedDataset={selectedDataset}\n        onSelectDataset={setSelectedDataset}\n        onCancel={() => {\n          setIsOpen(false)\n          setSelectedDataset(null)\n        }}\n        onSeeIndexDefinition={(dataset) => {\n          // eslint-disable-next-line no-console\n          console.log('See index definition for:', dataset)\n          props.onSeeIndexDefinition(dataset)\n        }}\n        onStartQuerying={(dataset) => {\n          // eslint-disable-next-line no-console\n          console.log('Start querying with:', dataset)\n          props.onStartQuerying(dataset)\n        }}\n      />\n    </>\n  )\n}\n\nconst meta: Meta<typeof PickSampleDataModal> = {\n  component: PickSampleDataModal,\n  argTypes: {\n    isOpen: {\n      description: 'Controls modal visibility',\n    },\n    selectedDataset: {\n      description: 'Currently selected sample dataset',\n      control: 'select',\n      options: [null, ...Object.values(SampleDataContent)],\n    },\n    onSelectDataset: {\n      description: 'Called when user selects a dataset option',\n    },\n    onCancel: {\n      description: 'Called when user cancels (Cancel button or X)',\n    },\n    onSeeIndexDefinition: {\n      description: 'Called when user clicks \"See index definition\"',\n    },\n    onStartQuerying: {\n      description: 'Called when user clicks \"Start querying\"',\n    },\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n/**\n * Interactive example with local state management.\n * This demonstrates how a parent component should wire the modal.\n */\nexport const Interactive: Story = {\n  render: (args) => (\n    <PickSampleDataModalWithState\n      onSeeIndexDefinition={args.onSeeIndexDefinition}\n      onStartQuerying={args.onStartQuerying}\n    />\n  ),\n}\n\n/** Modal with no selection — action buttons are disabled. */\nexport const NoSelection: Story = {\n  args: {\n    isOpen: true,\n    selectedDataset: null,\n  },\n}\n\n/** Modal with E-commerce Discovery pre-selected. */\nexport const EcommerceSelected: Story = {\n  name: 'E-commerce Discovery selected',\n  args: {\n    isOpen: true,\n    selectedDataset: SampleDataContent.E_COMMERCE_DISCOVERY,\n  },\n}\n\n/** Modal with Content Recommendations pre-selected. */\nexport const ContentRecommendationsSelected: Story = {\n  name: 'Content recommendations selected',\n  args: {\n    isOpen: true,\n    selectedDataset: SampleDataContent.CONTENT_RECOMMENDATIONS,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/PickSampleDataModal.styles.ts",
    "content": "import React from 'react'\n\nimport styled, { css } from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display/modal'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\nexport const ModalContent = styled(Modal.Content.Compose)`\n  width: 660px;\n  max-height: calc(100vh - 100px);\n  border-radius: ${({ theme }: { theme: Theme }) => theme.core.space.space100};\n`\n\nexport const VisuallyHiddenTitle = styled(Modal.Content.Header.Title)`\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n`\n\nexport const ModalBody = styled(Col)`\n  padding: ${({ theme }: { theme: Theme }) => theme.core.space.space500};\n  overflow-y: auto;\n  flex: 1 1 auto;\n`\n\nexport const Illustration = styled(Row)`\n  height: 110px;\n`\n\nexport const Heading = styled(Title)`\n  max-width: 360px;\n  text-align: center;\n  align-self: center;\n  margin: 0 auto;\n`\n\nexport const Subtitle = styled(Text)`\n  line-height: 1.5;\n`\n\nexport const RadioCardList = styled(Col)`\n  width: 100%;\n`\n\nexport const RadioCard = styled.label<\n  React.LabelHTMLAttributes<HTMLLabelElement> & { $selected: boolean }\n>`\n  display: flex;\n  align-items: center;\n  gap: ${({ theme }: { theme: Theme }) => theme.core.space.space150};\n  padding: ${({ theme }: { theme: Theme }) =>\n    `${theme.core.space.space150} ${theme.core.space.space200}`};\n  border: 1px solid;\n  border-color: ${({ theme }: { theme: Theme }) =>\n    theme.components.boxSelectionGroup.item.states.default?.normal\n      ?.borderColor ?? theme.semantic.color.border.neutral300};\n  border-radius: ${({ theme }: { theme: Theme }) =>\n    theme.components.boxSelectionGroup.item.borderRadius};\n  background: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral100};\n  cursor: pointer;\n  transition: border-color 0.15s ease;\n\n  &:hover {\n    border-color: ${({ theme }: { theme: Theme }) =>\n      theme.components.boxSelectionGroup.item.states.default?.hover\n        ?.borderColor ?? theme.semantic.color.border.neutral400};\n  }\n\n  ${({ $selected, theme }) =>\n    $selected &&\n    css`\n      border-color: ${theme.components.boxSelectionGroup.item.states.checked\n        ?.normal?.borderColor ?? theme.semantic.color.border.informative400};\n    `}\n`\n\nexport const RadioCardContent = styled(Col)``\n\nexport const RadioCardTitle = styled(Text)`\n  font-weight: 500;\n`\n\nexport const RadioCardDescription = styled(Text)``\n\nexport const Footer = styled(Row)`\n  width: 100%;\n  padding: 0 ${({ theme }: { theme: Theme }) => theme.core.space.space400};\n  padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space400};\n`\n\nexport const FooterActions = styled(Row)``\n\nexport const ContentSection = styled(Col)`\n  width: 100%;\n`\n\nexport const DatasetSection = styled(Col)`\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/PickSampleDataModal.tsx",
    "content": "import React from 'react'\n\nimport { Modal } from 'uiSrc/components/base/display'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport {\n  RiRadioGroupRoot,\n  RiRadioGroupItemRoot,\n  RiRadioGroupItemIndicator,\n} from 'uiSrc/components/base/forms/radio-group/RadioGroup'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n  EmptyButton,\n} from 'uiSrc/components/base/forms/buttons'\n\nimport SampleDataModalImg from 'uiSrc/assets/img/vector-search/sample-data-modal-img.svg?react'\n\nimport {\n  PickSampleDataModalProps,\n  SampleDataContent,\n} from './PickSampleDataModal.types'\nimport {\n  SAMPLE_DATA_OPTIONS,\n  MODAL_TITLE,\n  MODAL_SUBTITLE_LINE_1,\n  MODAL_SUBTITLE_LINE_2,\n  CANCEL_BUTTON_TEXT,\n  SEE_INDEX_DEFINITION_BUTTON_TEXT,\n  START_QUERYING_BUTTON_TEXT,\n} from './PickSampleDataModal.constants'\nimport * as S from './PickSampleDataModal.styles'\n\nconst PickSampleDataModal = ({\n  isOpen,\n  loading,\n  selectedDataset,\n  onSelectDataset,\n  onCancel,\n  onSeeIndexDefinition,\n  onStartQuerying,\n}: PickSampleDataModalProps) => {\n  if (!isOpen) return null\n\n  const hasSelection = selectedDataset !== null\n\n  return (\n    <Modal.Compose open={isOpen}>\n      <S.ModalContent persistent onCancel={onCancel}>\n        <S.VisuallyHiddenTitle>{MODAL_TITLE}</S.VisuallyHiddenTitle>\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={onCancel}\n          data-testid=\"pick-sample-data-modal--close\"\n        />\n        <S.ModalBody align=\"center\" gap=\"xxl\">\n          <S.Illustration data-testid=\"pick-sample-data-modal--illustration\">\n            <SampleDataModalImg />\n          </S.Illustration>\n\n          <S.ContentSection gap=\"xxl\">\n            <S.Heading\n              size=\"XL\"\n              color=\"primary\"\n              data-testid=\"pick-sample-data-modal--heading\"\n            >\n              {MODAL_TITLE}\n            </S.Heading>\n\n            <S.DatasetSection gap=\"l\">\n              <S.Subtitle\n                size=\"M\"\n                color=\"primary\"\n                data-testid=\"pick-sample-data-modal--subtitle\"\n              >\n                {MODAL_SUBTITLE_LINE_1}\n                <br />\n                {MODAL_SUBTITLE_LINE_2}\n              </S.Subtitle>\n\n              <RiRadioGroupRoot\n                value={selectedDataset ?? ''}\n                onChange={(value) =>\n                  onSelectDataset(value as SampleDataContent)\n                }\n                data-testid=\"pick-sample-data-modal--radio-group\"\n              >\n                <S.RadioCardList gap=\"m\">\n                  {SAMPLE_DATA_OPTIONS.map((option) => (\n                    <S.RadioCard\n                      key={option.value}\n                      $selected={selectedDataset === option.value}\n                      data-testid={`pick-sample-data-modal--option-${option.value}`}\n                    >\n                      <RiRadioGroupItemRoot value={option.value}>\n                        <RiRadioGroupItemIndicator />\n                      </RiRadioGroupItemRoot>\n                      <S.RadioCardContent gap=\"xs\">\n                        <S.RadioCardTitle size=\"M\" color=\"primary\">\n                          {option.label}\n                        </S.RadioCardTitle>\n                        <S.RadioCardDescription size=\"XS\" color=\"secondary\">\n                          {option.description}\n                        </S.RadioCardDescription>\n                      </S.RadioCardContent>\n                    </S.RadioCard>\n                  ))}\n                </S.RadioCardList>\n              </RiRadioGroupRoot>\n            </S.DatasetSection>\n          </S.ContentSection>\n        </S.ModalBody>\n\n        <S.Footer align=\"center\" justify=\"between\">\n          <EmptyButton\n            onClick={onCancel}\n            data-testid=\"pick-sample-data-modal--cancel\"\n          >\n            {CANCEL_BUTTON_TEXT}\n          </EmptyButton>\n\n          <S.FooterActions align=\"center\" gap=\"m\" justify=\"end\" grow={false}>\n            <SecondaryButton\n              size=\"large\"\n              disabled={!hasSelection}\n              onClick={() =>\n                hasSelection && onSeeIndexDefinition(selectedDataset)\n              }\n              data-testid=\"pick-sample-data-modal--see-index-definition\"\n            >\n              {SEE_INDEX_DEFINITION_BUTTON_TEXT}\n            </SecondaryButton>\n\n            <PrimaryButton\n              size=\"large\"\n              loading={loading}\n              disabled={!hasSelection}\n              onClick={() => hasSelection && onStartQuerying(selectedDataset)}\n              data-testid=\"pick-sample-data-modal--start-querying\"\n            >\n              {START_QUERYING_BUTTON_TEXT}\n            </PrimaryButton>\n          </S.FooterActions>\n        </S.Footer>\n      </S.ModalContent>\n    </Modal.Compose>\n  )\n}\n\nexport { PickSampleDataModal }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/PickSampleDataModal.types.ts",
    "content": "export enum SampleDataContent {\n  E_COMMERCE_DISCOVERY = 'e-commerce-discovery',\n  CONTENT_RECOMMENDATIONS = 'content-recommendations',\n}\n\nexport interface SampleDataOption {\n  value: SampleDataContent\n  label: string\n  description: string\n}\n\nexport interface PickSampleDataModalProps {\n  isOpen: boolean\n  loading?: boolean\n  selectedDataset: SampleDataContent | null\n  onSelectDataset: (value: SampleDataContent) => void\n  onCancel: () => void\n  onSeeIndexDefinition: (selectedDataset: SampleDataContent) => void\n  onStartQuerying: (selectedDataset: SampleDataContent) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/pick-sample-data-modal/index.ts",
    "content": "export { PickSampleDataModal } from './PickSampleDataModal'\nexport { SampleDataContent } from './PickSampleDataModal.types'\nexport type {\n  PickSampleDataModalProps,\n  SampleDataOption,\n} from './PickSampleDataModal.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/EditorLibraryToggle.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { EditorLibraryToggle } from './EditorLibraryToggle'\nimport { EditorTab } from './QueryEditor.types'\n\ndescribe('EditorLibraryToggle', () => {\n  const defaultProps = {\n    activeTab: EditorTab.Editor,\n    onChangeTab: jest.fn(),\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render toggle buttons', () => {\n    render(<EditorLibraryToggle {...defaultProps} />)\n\n    const editorButton = screen.getByRole('button', { name: /Query editor/i })\n    const libraryButton = screen.getByRole('button', {\n      name: /Query library/i,\n    })\n\n    expect(editorButton).toBeInTheDocument()\n    expect(libraryButton).toBeInTheDocument()\n  })\n\n  it('should call onChangeTab with Library when Query library toggle is clicked', () => {\n    render(<EditorLibraryToggle {...defaultProps} />)\n\n    const libraryButton = screen.getByRole('button', {\n      name: /Query library/i,\n    })\n    fireEvent.click(libraryButton)\n\n    expect(defaultProps.onChangeTab).toHaveBeenCalledWith(EditorTab.Library)\n  })\n\n  it('should call onChangeTab with Editor when Query editor toggle is clicked', () => {\n    render(\n      <EditorLibraryToggle {...defaultProps} activeTab={EditorTab.Library} />,\n    )\n\n    const editorButton = screen.getByRole('button', { name: /Query editor/i })\n    fireEvent.click(editorButton)\n\n    expect(defaultProps.onChangeTab).toHaveBeenCalledWith(EditorTab.Editor)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/EditorLibraryToggle.tsx",
    "content": "import React from 'react'\n\nimport { ButtonGroup } from 'uiSrc/components/base/forms/button-group/ButtonGroup'\nimport { Icon, KnowledgeBaseIcon } from 'uiSrc/components/base/icons'\nimport { EditorTab, EditorLibraryToggleProps } from './QueryEditor.types'\nimport { QueryOnboardingPopover } from './components/query-onboarding-popover'\nimport * as S from './QueryEditor.styles'\n\nconst tabs = [\n  { value: EditorTab.Editor, label: 'Query editor' },\n  { value: EditorTab.Library, label: 'Query library', icon: KnowledgeBaseIcon },\n]\n\nexport const EditorLibraryToggle = ({\n  activeTab,\n  onChangeTab,\n}: EditorLibraryToggleProps) => (\n  <S.ToggleBar data-testid=\"editor-library-toggle\">\n    <QueryOnboardingPopover>\n      <ButtonGroup data-testid=\"editor-library-tabs\">\n        {tabs.map((tab) => (\n          <ButtonGroup.Button\n            key={tab.value}\n            isSelected={activeTab === tab.value}\n            onClick={() => onChangeTab(tab.value)}\n            data-testid={`editor-library-tab-${tab.value}`}\n          >\n            {tab.icon && <Icon icon={tab.icon} size=\"M\" color=\"currentColor\" />}{' '}\n            {tab.label}\n          </ButtonGroup.Button>\n        ))}\n      </ButtonGroup>\n    </QueryOnboardingPopover>\n  </S.ToggleBar>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditor.constants.ts",
    "content": "import { merge } from 'lodash'\nimport { defaultMonacoOptions } from 'uiSrc/constants'\n\nexport const EDITOR_OPTIONS = merge({}, defaultMonacoOptions, {\n  suggest: {\n    showWords: false,\n    showIcons: true,\n    insertMode: 'replace',\n    filterGraceful: false,\n    matchOnWordStartOnly: true,\n  },\n})\n\nexport const EDITOR_PLACEHOLDER =\n  'Start typing FT. to access search commands or switch to Query Library to access saved commands.'\n\n/** Commands that support FT.EXPLAIN and FT.PROFILE. */\nexport const EXPLAINABLE_COMMANDS = ['FT.SEARCH', 'FT.AGGREGATE'] as const\n\nexport const TOOLTIP_EXPLAIN =\n  \"Shows how your query will run (execution plan) to understand what's used.\"\nexport const TOOLTIP_PROFILE =\n  'Profiles your query to show where time is spent and spot bottlenecks.'\nexport const TOOLTIP_DISABLED_NO_QUERY = 'Disabled: no query identified.'\nexport const TOOLTIP_DISABLED_LOADING = 'Disabled: query is running.'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditor.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const EditorWrapper = styled(Col)`\n  height: 100%;\n  width: 100%;\n  overflow: hidden;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.components.card.borderRadius};\n`\n\nexport const ToggleBar = styled(Row).attrs({\n  align: 'center',\n  justify: 'start',\n  gap: 'l',\n  grow: false,\n})`\n  padding: ${({ theme }) =>\n    `${theme.core.space.space100} ${theme.core.space.space200}`};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral500};\n  flex-shrink: 0;\n`\n\nexport const EditorContainer = styled.div`\n  flex: 1;\n  min-height: 0;\n  position: relative;\n  background-color: ${({ theme }) =>\n    theme.semantic?.color.background.neutral200};\n\n  --monaco-color-bg: ${({ theme }) =>\n    theme.semantic?.color.background.neutral200};\n`\n\n/**\n * Placeholder overlay for the Monaco editor.\n * The `left` offset is set via inline style from `editor.getLayoutInfo().contentLeft`\n * so it always aligns with the actual content area regardless of glyph margin\n * or line-number gutter width. Top padding matches `defaultMonacoOptions.padding.top`.\n * `pointer-events: none` lets clicks pass through to the editor underneath.\n */\nexport const EditorPlaceholder = styled.div<{\n  $contentLeft?: number\n}>`\n  position: absolute;\n  top: 10px;\n  left: ${({ $contentLeft }) => $contentLeft ?? 0}px;\n  right: 0;\n  pointer-events: none;\n  z-index: 1;\n  color: ${({ theme }) => theme.semantic.color.text.neutral500};\n  font-family: Menlo, Monaco, 'Courier New', monospace;\n  font-size: 12px;\n  line-height: 18px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  opacity: 0.7;\n`\n\nexport const ActionsBar = styled(Row).attrs({\n  align: 'center',\n  justify: 'end',\n  gap: 'l',\n  grow: false,\n})`\n  padding: ${({ theme }) =>\n    `${theme.core.space.space100} ${theme.core.space.space200}`};\n  border-top: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  flex-shrink: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditor.types.ts",
    "content": "import { EXPLAINABLE_COMMANDS } from './QueryEditor.constants'\n\nexport type ExplainableCommand = (typeof EXPLAINABLE_COMMANDS)[number]\n\nexport interface OnboardingTemplate {\n  /** The Redis command name (used as the suggestion label). */\n  command: string\n  /** Short description shown as the suggestion detail. */\n  detail: string\n  /** Whether the template includes an index argument placeholder. */\n  usesIndex: boolean\n}\n\nexport interface QueryEditorWrapperProps {\n  query: string\n  setQuery: (script: string) => void\n  onSubmit: (value?: string) => void\n  isLoading?: boolean\n}\n\nexport enum EditorTab {\n  Editor = 'editor',\n  Library = 'library',\n}\n\nexport interface EditorLibraryToggleProps {\n  activeTab: EditorTab\n  onChangeTab: (tab: EditorTab) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditor.utils.spec.ts",
    "content": "import {\n  parseExplainableCommand,\n  buildExplainQuery,\n  buildProfileQuery,\n  extractFirstToken,\n} from './QueryEditor.utils'\n\ndescribe('parseExplainableCommand', () => {\n  it('detects FT.SEARCH', () => {\n    const result = parseExplainableCommand('FT.SEARCH idx \"*\"')\n    expect(result).toEqual({ command: 'FT.SEARCH', afterCommand: ' idx \"*\"' })\n  })\n\n  it('detects FT.AGGREGATE', () => {\n    const result = parseExplainableCommand('FT.AGGREGATE idx \"*\"')\n    expect(result).toEqual({\n      command: 'FT.AGGREGATE',\n      afterCommand: ' idx \"*\"',\n    })\n  })\n\n  it('is case-insensitive', () => {\n    const result = parseExplainableCommand('ft.search idx \"*\"')\n    expect(result).toEqual({ command: 'FT.SEARCH', afterCommand: ' idx \"*\"' })\n  })\n\n  it('returns null for non-matching commands', () => {\n    expect(parseExplainableCommand('GET key')).toBeNull()\n  })\n\n  it('returns null for empty string', () => {\n    expect(parseExplainableCommand('')).toBeNull()\n  })\n\n  it('returns null for command with no args', () => {\n    expect(parseExplainableCommand('FT.SEARCH')).toBeNull()\n  })\n\n  it('returns null for command with only whitespace after', () => {\n    expect(parseExplainableCommand('FT.SEARCH   ')).toBeNull()\n    expect(parseExplainableCommand('FT.AGGREGATE  ')).toBeNull()\n  })\n\n  it('ignores leading comment lines from sample queries', () => {\n    const query = '// Some description\\nFT.SEARCH idx \"*\"'\n    const result = parseExplainableCommand(query)\n    expect(result).toEqual({ command: 'FT.SEARCH', afterCommand: ' idx \"*\"' })\n  })\n\n  it('ignores multiple leading comment lines', () => {\n    const query = '// Line 1\\n// Line 2\\nFT.AGGREGATE idx \"*\"'\n    const result = parseExplainableCommand(query)\n    expect(result).toEqual({\n      command: 'FT.AGGREGATE',\n      afterCommand: ' idx \"*\"',\n    })\n  })\n\n  it('ignores indented comment lines', () => {\n    const query = '  // indented comment\\nFT.SEARCH idx \"*\"'\n    const result = parseExplainableCommand(query)\n    expect(result).toEqual({ command: 'FT.SEARCH', afterCommand: ' idx \"*\"' })\n  })\n\n  it('returns null when only comments remain', () => {\n    expect(parseExplainableCommand('// just a comment')).toBeNull()\n  })\n})\n\ndescribe('buildExplainQuery', () => {\n  it('builds FT.EXPLAIN from FT.SEARCH', () => {\n    const result = buildExplainQuery({\n      command: 'FT.SEARCH',\n      afterCommand: ' idx \"*\"',\n    })\n    expect(result).toBe('FT.EXPLAIN idx \"*\"')\n  })\n\n  it('builds FT.EXPLAIN from FT.AGGREGATE', () => {\n    const result = buildExplainQuery({\n      command: 'FT.AGGREGATE',\n      afterCommand: ' idx \"*\" GROUPBY 1 @field',\n    })\n    expect(result).toBe('FT.EXPLAIN idx \"*\" GROUPBY 1 @field')\n  })\n})\n\ndescribe('buildProfileQuery', () => {\n  it('builds FT.PROFILE from FT.SEARCH with unquoted index', () => {\n    const result = buildProfileQuery({\n      command: 'FT.SEARCH',\n      afterCommand: ' idx \"*\" LIMIT 0 10',\n    })\n    expect(result).toBe('FT.PROFILE idx SEARCH QUERY \"*\" LIMIT 0 10')\n  })\n\n  it('builds FT.PROFILE from FT.AGGREGATE with unquoted index', () => {\n    const result = buildProfileQuery({\n      command: 'FT.AGGREGATE',\n      afterCommand: ' idx \"*\" GROUPBY 1 @field',\n    })\n    expect(result).toBe('FT.PROFILE idx AGGREGATE QUERY \"*\" GROUPBY 1 @field')\n  })\n\n  it('handles index-only (no query args)', () => {\n    const result = buildProfileQuery({\n      command: 'FT.SEARCH',\n      afterCommand: ' idx',\n    })\n    expect(result).toBe('FT.PROFILE idx SEARCH QUERY')\n  })\n\n  it('handles double-quoted index with spaces', () => {\n    const result = buildProfileQuery({\n      command: 'FT.SEARCH',\n      afterCommand: ' \"my idx\" \"*\"',\n    })\n    expect(result).toBe('FT.PROFILE \"my idx\" SEARCH QUERY \"*\"')\n  })\n\n  it('handles single-quoted index with spaces', () => {\n    const result = buildProfileQuery({\n      command: 'FT.SEARCH',\n      afterCommand: \" 'my idx' '*'\",\n    })\n    expect(result).toBe(\"FT.PROFILE 'my idx' SEARCH QUERY '*'\")\n  })\n\n  it('handles quoted index with escaped quotes inside', () => {\n    const result = buildProfileQuery({\n      command: 'FT.SEARCH',\n      afterCommand: ' \"my\\\\\"idx\" \"*\"',\n    })\n    expect(result).toBe('FT.PROFILE \"my\\\\\"idx\" SEARCH QUERY \"*\"')\n  })\n\n  it('handles quoted index-only (no query args)', () => {\n    const result = buildProfileQuery({\n      command: 'FT.SEARCH',\n      afterCommand: ' \"my idx\"',\n    })\n    expect(result).toBe('FT.PROFILE \"my idx\" SEARCH QUERY')\n  })\n\n  it('handles FT.AGGREGATE with quoted index', () => {\n    const result = buildProfileQuery({\n      command: 'FT.AGGREGATE',\n      afterCommand: ' \"my idx\" \"*\" GROUPBY 1 @f',\n    })\n    expect(result).toBe('FT.PROFILE \"my idx\" AGGREGATE QUERY \"*\" GROUPBY 1 @f')\n  })\n})\n\ndescribe('extractFirstToken', () => {\n  it('extracts unquoted token', () => {\n    expect(extractFirstToken('idx \"*\"')).toEqual({\n      index: 'idx',\n      remainder: ' \"*\"',\n    })\n  })\n\n  it('extracts double-quoted token with spaces', () => {\n    expect(extractFirstToken('\"my idx\" \"*\"')).toEqual({\n      index: '\"my idx\"',\n      remainder: ' \"*\"',\n    })\n  })\n\n  it('extracts single-quoted token with spaces', () => {\n    expect(extractFirstToken(\"'my idx' '*'\")).toEqual({\n      index: \"'my idx'\",\n      remainder: \" '*'\",\n    })\n  })\n\n  it('handles token with escaped quote inside', () => {\n    expect(extractFirstToken('\"my\\\\\"idx\" rest')).toEqual({\n      index: '\"my\\\\\"idx\"',\n      remainder: ' rest',\n    })\n  })\n\n  it('handles single token (no remainder)', () => {\n    expect(extractFirstToken('idx')).toEqual({\n      index: 'idx',\n      remainder: null,\n    })\n  })\n\n  it('handles quoted single token (no remainder)', () => {\n    expect(extractFirstToken('\"my idx\"')).toEqual({\n      index: '\"my idx\"',\n      remainder: null,\n    })\n  })\n\n  it('handles empty string', () => {\n    expect(extractFirstToken('')).toEqual({ index: '', remainder: null })\n  })\n\n  it('handles unterminated quote', () => {\n    expect(extractFirstToken('\"unterminated')).toEqual({\n      index: '\"unterminated',\n      remainder: null,\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditor.utils.ts",
    "content": "import { EXPLAINABLE_COMMANDS } from './QueryEditor.constants'\nimport { ExplainableCommand } from './QueryEditor.types'\n\n/**\n * Parses the query to detect a single FT.SEARCH or FT.AGGREGATE command.\n *\n * Returns the matched command and its position so that Explain / Profile\n * can transform the query without a second regex pass.\n */\nexport const stripCommentLines = (text: string): string =>\n  text\n    .split('\\n')\n    .filter((line) => !/^\\s*\\/\\//.test(line))\n    .join('\\n')\n\nexport const parseExplainableCommand = (\n  query: string,\n): { command: ExplainableCommand; afterCommand: string } | null => {\n  const trimmed = stripCommentLines(query).trim()\n  if (!trimmed) return null\n\n  const upper = trimmed.toUpperCase()\n  for (const cmd of EXPLAINABLE_COMMANDS) {\n    if (upper.startsWith(cmd)) {\n      const rest = trimmed.slice(cmd.length)\n      // Must be followed by whitespace and at least one argument\n      // (FT.SEARCH/FT.AGGREGATE always require an index name)\n      if (/^\\s/.test(rest) && rest.trim().length > 0) {\n        return { command: cmd, afterCommand: rest }\n      }\n    }\n  }\n  return null\n}\n\n/**\n * Builds an FT.EXPLAIN command from the current query.\n *\n * `FT.SEARCH idx \"query\" LIMIT 0 10` → `FT.EXPLAIN idx \"query\" LIMIT 0 10`\n */\nexport const buildExplainQuery = (parsed: {\n  command: ExplainableCommand\n  afterCommand: string\n}): string => `FT.EXPLAIN${parsed.afterCommand}`\n\n/**\n * Builds an FT.PROFILE command from the current query.\n *\n * `FT.SEARCH idx \"query\" LIMIT 0 10`\n *   → `FT.PROFILE idx SEARCH QUERY \"query\" LIMIT 0 10`\n *\n * `FT.AGGREGATE idx \"query\" GROUPBY ...`\n *   → `FT.PROFILE idx AGGREGATE QUERY \"query\" GROUPBY ...`\n */\nexport const buildProfileQuery = (parsed: {\n  command: ExplainableCommand\n  afterCommand: string\n}): string => {\n  const rest = parsed.afterCommand.trimStart()\n  const subcommand = parsed.command === 'FT.SEARCH' ? 'SEARCH' : 'AGGREGATE'\n\n  // Extract the first token (index name), respecting quotes.\n  const { index, remainder } = extractFirstToken(rest)\n\n  if (remainder === null) {\n    // Only index, no query args\n    return `FT.PROFILE ${index} ${subcommand} QUERY`\n  }\n\n  return `FT.PROFILE ${index} ${subcommand} QUERY${remainder}`\n}\n\n/**\n * Extracts the first whitespace-delimited token from a string,\n * respecting single and double-quoted tokens that may contain spaces.\n *\n * Returns the token (including its quotes) and the remainder of the\n * string (starting from the whitespace after the token), or\n * `{ index: input, remainder: null }` when there is no second token.\n */\nexport const extractFirstToken = (\n  input: string,\n): { index: string; remainder: string | null } => {\n  if (!input) return { index: '', remainder: null }\n\n  const quote = input[0]\n  if (quote === '\"' || quote === \"'\") {\n    // Find the matching closing quote (skip escaped quotes)\n    let i = 1\n    while (i < input.length) {\n      if (input[i] === '\\\\') {\n        i += 2 // skip escaped character\n        continue\n      }\n      if (input[i] === quote) {\n        // Token ends at closing quote\n        const endIdx = i + 1\n        const index = input.slice(0, endIdx)\n        const after = input.slice(endIdx)\n        return after.length === 0 || !/\\s/.test(after[0])\n          ? { index, remainder: after || null }\n          : { index, remainder: after }\n      }\n      i++\n    }\n    // Unterminated quote – treat entire input as the token\n    return { index: input, remainder: null }\n  }\n\n  // Unquoted token – split on first whitespace\n  const spaceIdx = input.search(/\\s/)\n  if (spaceIdx === -1) {\n    return { index: input, remainder: null }\n  }\n  return { index: input.slice(0, spaceIdx), remainder: input.slice(spaceIdx) }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditorWrapper.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, fireEvent, waitFor } from 'uiSrc/utils/test-utils'\nimport { QUERY_LIBRARY_ITEMS_MOCK } from 'uiSrc/mocks/handlers/browser/queryLibraryHandlers'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\nimport { buildLoadQuery } from '../query-library-view/QueryLibraryView.utils'\n\nimport { QueryEditorWrapper } from './QueryEditorWrapper'\nimport { EditorTab } from './QueryEditor.types'\n\njest.mock('uiSrc/components/base/code-editor', () => {\n  const ReactMock = require('react')\n  return {\n    __esModule: true,\n    CodeEditor: (props: any) =>\n      ReactMock.createElement(\n        'div',\n        { 'data-testid': 'code-editor' },\n        props.value,\n      ),\n  }\n})\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst routerDom = require('react-router-dom')\n\nconst defaultProps = {\n  query: '',\n  setQuery: jest.fn(),\n  onSubmit: jest.fn(),\n}\n\nconst renderComponent = (props = {}) =>\n  render(<QueryEditorWrapper {...defaultProps} {...props} />)\n\ndescribe('QueryEditorWrapper', () => {\n  const originalUseParams = routerDom.useParams\n  const originalUseLocation = routerDom.useLocation\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    routerDom.useParams = () => ({\n      instanceId: 'instanceId',\n      indexName: 'test-index',\n    })\n\n    routerDom.useLocation = () => ({\n      pathname: 'pathname',\n      search: '',\n      state: null,\n    })\n  })\n\n  afterAll(() => {\n    routerDom.useParams = originalUseParams\n    routerDom.useLocation = originalUseLocation\n  })\n\n  it('should render the editor wrapper', () => {\n    renderComponent()\n\n    const editorWrapper = screen.getByTestId('vector-search-query-editor')\n    expect(editorWrapper).toBeInTheDocument()\n  })\n\n  it('should render editor/library toggle', () => {\n    renderComponent()\n\n    const toggle = screen.getByTestId('editor-library-toggle')\n    expect(toggle).toBeInTheDocument()\n\n    const editorBtn = screen.getByRole('button', { name: /Query editor/i })\n    expect(editorBtn).toBeInTheDocument()\n\n    const libraryBtn = screen.getByRole('button', { name: /Query library/i })\n    expect(libraryBtn).toBeInTheDocument()\n  })\n\n  it('should render editor view by default', () => {\n    renderComponent()\n\n    const editor = screen.getByTestId('vector-search-editor')\n    expect(editor).toBeInTheDocument()\n\n    const actions = screen.getByTestId('vector-search-actions')\n    expect(actions).toBeInTheDocument()\n  })\n\n  it('should switch to library view when Library toggle is clicked', async () => {\n    renderComponent()\n\n    const libraryBtn = screen.getByRole('button', { name: /Query library/i })\n    fireEvent.click(libraryBtn)\n\n    await waitFor(() => {\n      const libraryView = screen.getByTestId('query-library-view')\n      expect(libraryView).toBeInTheDocument()\n    })\n\n    const editor = screen.queryByTestId('vector-search-editor')\n    expect(editor).not.toBeInTheDocument()\n  })\n\n  it('should switch back to editor view when Editor toggle is clicked', async () => {\n    renderComponent()\n\n    const libraryBtn = screen.getByRole('button', { name: /Query library/i })\n    fireEvent.click(libraryBtn)\n\n    await waitFor(() => {\n      const editor = screen.queryByTestId('vector-search-editor')\n      expect(editor).not.toBeInTheDocument()\n    })\n\n    const editorBtn = screen.getByRole('button', { name: /Query editor/i })\n    fireEvent.click(editorBtn)\n\n    const editor = screen.getByTestId('vector-search-editor')\n    expect(editor).toBeInTheDocument()\n  })\n\n  it('should call onSubmit when Run is clicked on a library item', async () => {\n    renderComponent()\n\n    const libraryBtn = screen.getByRole('button', { name: /Query library/i })\n    fireEvent.click(libraryBtn)\n\n    await waitFor(() => {\n      const item = screen.getByTestId(\n        `query-library-item-${QUERY_LIBRARY_ITEMS_MOCK[0].id}`,\n      )\n      expect(item).toBeInTheDocument()\n    })\n\n    const runBtn = screen.getByTestId(\n      `query-library-item-${QUERY_LIBRARY_ITEMS_MOCK[0].id}-run-btn`,\n    )\n    fireEvent.click(runBtn)\n\n    expect(defaultProps.onSubmit).toHaveBeenCalledWith(\n      QUERY_LIBRARY_ITEMS_MOCK[0].query,\n    )\n  })\n\n  it('should switch to Editor tab and set query when Load is clicked', async () => {\n    renderComponent()\n\n    const libraryBtn = screen.getByRole('button', { name: /Query library/i })\n    fireEvent.click(libraryBtn)\n\n    await waitFor(() => {\n      const item = screen.getByTestId(\n        `query-library-item-${QUERY_LIBRARY_ITEMS_MOCK[0].id}`,\n      )\n      expect(item).toBeInTheDocument()\n    })\n\n    const loadBtn = screen.getByTestId(\n      `query-library-item-${QUERY_LIBRARY_ITEMS_MOCK[0].id}-load-btn`,\n    )\n    fireEvent.click(loadBtn)\n\n    expect(defaultProps.setQuery).toHaveBeenCalledWith(\n      buildLoadQuery(QUERY_LIBRARY_ITEMS_MOCK[0]),\n    )\n\n    const editor = screen.getByTestId('vector-search-editor')\n    expect(editor).toBeInTheDocument()\n  })\n\n  it('should open Library tab when location state has activeTab Library', async () => {\n    routerDom.useLocation = () => ({\n      pathname: 'pathname',\n      search: '',\n      state: { activeTab: EditorTab.Library },\n    })\n\n    renderComponent()\n\n    await waitFor(() => {\n      const searchInput = screen.getByPlaceholderText('Search query')\n      expect(searchInput).toBeInTheDocument()\n    })\n\n    const editor = screen.queryByTestId('vector-search-editor')\n    expect(editor).not.toBeInTheDocument()\n  })\n\n  it('should default to Editor tab when location state has no activeTab', () => {\n    routerDom.useLocation = () => ({\n      pathname: 'pathname',\n      search: '',\n      state: null,\n    })\n\n    renderComponent()\n\n    const editor = screen.getByTestId('vector-search-editor')\n    expect(editor).toBeInTheDocument()\n\n    const searchInput = screen.queryByPlaceholderText('Search query')\n    expect(searchInput).not.toBeInTheDocument()\n  })\n\n  it('should disable run button when isLoading is true', () => {\n    renderComponent({ query: 'FT.SEARCH idx \"*\"', isLoading: true })\n\n    const submitBtn = screen.getByTestId('btn-submit') as HTMLButtonElement\n    expect(submitBtn.disabled).toBe(true)\n  })\n\n  it('should enable run button when isLoading is false', () => {\n    renderComponent({ query: 'FT.SEARCH idx \"*\"', isLoading: false })\n\n    const submitBtn = screen.getByTestId('btn-submit') as HTMLButtonElement\n    expect(submitBtn.disabled).toBe(false)\n  })\n\n  describe('Save query flow', () => {\n    it('should render save button in actions bar', () => {\n      renderComponent()\n\n      expect(screen.getByTestId('btn-save-query')).toBeInTheDocument()\n    })\n\n    it('should open save modal when save button is clicked', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      fireEvent.click(screen.getByTestId('btn-save-query'))\n\n      expect(\n        screen.getByRole('dialog', { name: 'Save query' }),\n      ).toBeInTheDocument()\n      expect(\n        screen.getByTestId('save-query-modal-name-input'),\n      ).toBeInTheDocument()\n    })\n\n    it('should close save modal when cancel is clicked', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      fireEvent.click(screen.getByTestId('btn-save-query'))\n      expect(\n        screen.getByRole('dialog', { name: 'Save query' }),\n      ).toBeInTheDocument()\n\n      fireEvent.click(screen.getByTestId('save-query-modal-cancel'))\n\n      expect(\n        screen.queryByTestId('save-query-modal-body'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should call QueryLibraryService.create and close modal on successful save', async () => {\n      const queryName = faker.lorem.words(3)\n      const mockCreate = jest\n        .spyOn(QueryLibraryService.prototype, 'create')\n        .mockResolvedValueOnce({\n          id: faker.string.uuid(),\n          databaseId: 'instanceId',\n          indexName: 'test-index',\n          type: 'SAVED' as any,\n          name: queryName,\n          query: 'FT.SEARCH idx \"*\"',\n          createdAt: new Date().toISOString(),\n          updatedAt: new Date().toISOString(),\n        })\n\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      fireEvent.click(screen.getByTestId('btn-save-query'))\n\n      const input = screen.getByTestId('save-query-modal-name-input')\n      fireEvent.change(input, { target: { value: queryName } })\n\n      fireEvent.click(screen.getByTestId('save-query-modal-confirm'))\n\n      await waitFor(() => {\n        expect(mockCreate).toHaveBeenCalledWith('instanceId', {\n          indexName: 'test-index',\n          name: queryName,\n          query: 'FT.SEARCH idx \"*\"',\n        })\n      })\n\n      await waitFor(() => {\n        expect(\n          screen.queryByTestId('save-query-modal-body'),\n        ).not.toBeInTheDocument()\n      })\n\n      mockCreate.mockRestore()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditorWrapper.stories.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport type { Meta, StoryObj, StoryContext } from '@storybook/react-vite'\nimport { useDispatch } from 'react-redux'\nimport { fn } from 'storybook/test'\n\nimport { MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport {\n  getRedisCommands,\n  getRedisCommandsSuccess,\n} from 'uiSrc/slices/app/redis-commands'\nimport MonacoEnvironmentInitializer from 'uiSrc/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer'\nimport MonacoLanguages from 'uiSrc/components/monaco-laguages'\n\nimport { QueryEditorWrapper } from './QueryEditorWrapper'\n\n/**\n * Decorator to initialize Monaco environment and Redis commands.\n * When `parameters.loadingState` is true, dispatches the loading action\n * instead of loading commands to simulate the loading state.\n */\nconst WithMonacoSetup = (Story: React.ComponentType, context: StoryContext) => {\n  const isLoading = context.parameters?.loadingState === true\n\n  const MonacoSetup = () => {\n    const dispatch = useDispatch()\n\n    useEffect(() => {\n      if (isLoading) {\n        dispatch(getRedisCommands())\n      } else {\n        // @ts-ignore - MOCK_COMMANDS_SPEC type differences are fine for Monaco\n        dispatch(getRedisCommandsSuccess(MOCK_COMMANDS_SPEC))\n      }\n    }, [dispatch])\n\n    return (\n      <div\n        style={{\n          width: '1000px',\n          height: '400px',\n          display: 'flex',\n          margin: '0 auto',\n        }}\n      >\n        <MonacoEnvironmentInitializer />\n        <MonacoLanguages />\n        <Story />\n      </div>\n    )\n  }\n\n  return <MonacoSetup />\n}\n\nconst meta: Meta<typeof QueryEditorWrapper> = {\n  component: QueryEditorWrapper,\n  tags: ['autodocs'],\n  decorators: [WithMonacoSetup],\n  args: {\n    onSubmit: fn(),\n  },\n  parameters: {\n    layout: 'centered',\n    docs: {\n      description: {\n        component: `Vector Search Query Editor with Editor/Library toggle, Monaco editor\nwith RQE autocomplete, and Run action button.\n\n### Onboarding suggestions\n\nWhen the editor is **empty** and receives **focus**, a suggestions panel\nis shown with a predefined list of RQE query templates:\n\n| Command | Description |\n|---------|-------------|\n| \\`FT.SEARCH\\` | Find documents by text or filters |\n| \\`FT.AGGREGATE\\` | Group and summarize results |\n| \\`FT.SUGGET\\` | Retrieve autocomplete suggestions |\n| \\`FT.SPELLCHECK\\` | Suggest corrections for typos |\n| \\`FT.EXPLAIN\\` | See execution plan |\n| \\`FT.PROFILE\\` | Analyze performance |\n| \\`FT._LIST\\` | View index schema and stats |\n\nEach suggestion shows the **query detail first** (\\`detail\\` property),\nand users can expand the **full documentation** via the Monaco details\npanel.\n\n### Index-aware autocomplete\n\nTemplates are **index-aware**: when available indexes exist, the first\nindex name is **preselected** in snippet tab-stop placeholders. As the\nuser types, the selected index is used in suggestions.\n\n### How it works\n\nThe behaviour is driven by \\`VectorSearchEditor\\`'s \\`onSetup\\` callback:\n\n1. On mount (and on every subsequent focus of an empty editor),\n   \\`getOnboardingSuggestions()\\` builds the 7 template completion items.\n2. The items are set via \\`completions.setSuggestionsData()\\` and the\n   suggest widget is triggered.\n3. Once the user picks a template or starts typing, the normal\n   autocomplete flow takes over with all Redis commands available.\n\nThis behaviour is **unique to Vector Search** — the Workbench editor\ndoes not auto-show suggestions.`,\n      },\n      story: {\n        inline: false,\n        iframeHeight: 400,\n      },\n    },\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n/**\n * Default empty editor — demonstrates the **onboarding** flow.\n *\n * 1. Click into the editor area.\n * 2. A suggestions panel appears with 7 predefined FT.* templates,\n *    each showing its description as the detail text.\n * 3. Expand the documentation panel to see the full docs for each\n *    command.\n * 4. Pick a template — the snippet is inserted with the preselected\n *    index (if available) and autocomplete continues normally.\n * 5. All Redis commands remain available if you start typing\n *    something else.\n */\nexport const Default: Story = {\n  render: (args) => {\n    const [query, setQuery] = useState('')\n    return <QueryEditorWrapper {...args} query={query} setQuery={setQuery} />\n  },\n}\n\n/**\n * Editor pre-populated with a KNN search query.\n *\n * Because the editor is not blank, the onboarding suggestions popup\n * does **not** appear automatically — regular code completion is used\n * instead.\n */\nexport const WithQuery: Story = {\n  name: 'With pre-filled query',\n  render: (args) => {\n    const [query, setQuery] = useState(\n      'FT.SEARCH idx:bikes \"*=>[KNN 10 @vector $blob]\" PARAMS 2 blob \"...\" DIALECT 2',\n    )\n    return <QueryEditorWrapper {...args} query={query} setQuery={setQuery} />\n  },\n}\n\n/**\n * Loading state while Redis commands are being fetched.\n */\nexport const Loading: Story = {\n  parameters: {\n    loadingState: true,\n  },\n  render: (args) => {\n    const [query, setQuery] = useState('')\n    return <QueryEditorWrapper {...args} query={query} setQuery={setQuery} />\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/QueryEditorWrapper.tsx",
    "content": "import React, { useCallback, useMemo, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useLocation, useParams } from 'react-router-dom'\n\nimport { IRedisCommand } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport { redisearchListSelector } from 'uiSrc/slices/browser/redisearch'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands'\nimport SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json'\nimport {\n  QueryEditorContextProvider,\n  LoadingContainer,\n} from 'uiSrc/components/query'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\nimport { queryLibraryNotifications } from 'uiSrc/pages/vector-search/constants'\n\nimport { decodeIndexNameFromUrl } from '../../utils'\nimport { EditorTab, QueryEditorWrapperProps } from './QueryEditor.types'\nimport { EditorLibraryToggle } from './EditorLibraryToggle'\nimport { VectorSearchEditor } from './VectorSearchEditor'\nimport { VectorSearchActions } from './VectorSearchActions'\nimport { QueryLibraryView } from '../query-library-view'\nimport { SaveQueryModal } from '../save-query-modal'\nimport * as S from './QueryEditor.styles'\n\n/**\n * Wrapper for the Vector Search Query Editor.\n * Fetches commands + indexes, provides QueryEditorContext,\n * and composes the toggle header, editor, and actions bar.\n */\nexport const QueryEditorWrapper = ({\n  query,\n  setQuery,\n  onSubmit,\n  isLoading = false,\n}: QueryEditorWrapperProps) => {\n  const { instanceId, indexName } = useParams<{\n    instanceId: string\n    indexName?: string\n  }>()\n  const location = useLocation<{ activeTab?: EditorTab }>()\n  const [activeTab, setActiveTab] = useState<EditorTab>(\n    location.state?.activeTab ?? EditorTab.Editor,\n  )\n  const [isSaveModalOpen, setIsSaveModalOpen] = useState(false)\n  const [isSaving, setIsSaving] = useState(false)\n\n  const changeActiveTab = useCallback(\n    (tab: EditorTab) => {\n      setActiveTab(tab)\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_EDITOR_TAB_CHANGED,\n        eventData: { databaseId: instanceId, tab },\n      })\n    },\n    [instanceId],\n  )\n\n  const handleLibraryLoad = useCallback(\n    (queryText: string) => {\n      setQuery(queryText)\n      setActiveTab(EditorTab.Editor)\n    },\n    [setQuery],\n  )\n\n  const handleSaveClick = useCallback(() => {\n    setIsSaveModalOpen(true)\n  }, [])\n\n  const handleSaveClose = useCallback(() => {\n    setIsSaveModalOpen(false)\n  }, [])\n\n  const decodedIndexName = indexName ? decodeURIComponent(indexName) : ''\n\n  const dispatch = useDispatch()\n  const queryLibraryService = useRef(new QueryLibraryService()).current\n\n  const handleSaveSubmit = useCallback(\n    async (name: string) => {\n      if (!instanceId || !decodedIndexName) return\n\n      setIsSaving(true)\n      try {\n        const result = await queryLibraryService.create(instanceId, {\n          indexName: decodedIndexName,\n          name,\n          query,\n        })\n\n        if (result) {\n          sendEventTelemetry({\n            event: TelemetryEvent.SEARCH_QUERY_SAVED,\n            eventData: { databaseId: instanceId },\n          })\n          dispatch(\n            addMessageNotification(\n              queryLibraryNotifications.querySaved(() => {\n                setActiveTab(EditorTab.Library)\n              }),\n            ),\n          )\n          setIsSaveModalOpen(false)\n        }\n      } catch {\n        dispatch(addMessageNotification(queryLibraryNotifications.saveFailed()))\n      } finally {\n        setIsSaving(false)\n      }\n    },\n    [instanceId, decodedIndexName, query, dispatch],\n  )\n\n  const { loading: isCommandsLoading, spec: COMMANDS_SPEC } = useSelector(\n    appRedisCommandsSelector,\n  )\n  const { data: indexes = [] } = useSelector(redisearchListSelector)\n\n  const REDIS_COMMANDS = useMemo(\n    () =>\n      mergeRedisCommandsSpecs(\n        COMMANDS_SPEC,\n        SEARCH_COMMANDS_SPEC,\n      ) as IRedisCommand[],\n    [COMMANDS_SPEC, SEARCH_COMMANDS_SPEC],\n  )\n\n  if (isCommandsLoading) {\n    return (\n      <S.EditorWrapper>\n        <LoadingContainer>\n          <LoadingContent lines={2} className=\"fluid\" />\n        </LoadingContainer>\n      </S.EditorWrapper>\n    )\n  }\n\n  return (\n    <QueryEditorContextProvider\n      value={{\n        query,\n        setQuery,\n        commands: REDIS_COMMANDS,\n        indexes,\n        activeIndexName: indexName\n          ? decodeIndexNameFromUrl(indexName)\n          : undefined,\n        isLoading,\n        onSubmit,\n      }}\n    >\n      <S.EditorWrapper data-testid=\"vector-search-query-editor\">\n        <EditorLibraryToggle\n          activeTab={activeTab}\n          onChangeTab={changeActiveTab}\n        />\n        {activeTab === EditorTab.Editor && (\n          <>\n            <VectorSearchEditor />\n            <VectorSearchActions onSaveClick={handleSaveClick} />\n          </>\n        )}\n        {activeTab === EditorTab.Library && (\n          <QueryLibraryView onRun={onSubmit} onLoad={handleLibraryLoad} />\n        )}\n      </S.EditorWrapper>\n\n      <SaveQueryModal\n        isOpen={isSaveModalOpen}\n        isSaving={isSaving}\n        onSave={handleSaveSubmit}\n        onClose={handleSaveClose}\n      />\n    </QueryEditorContextProvider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/VectorSearchActions.spec.tsx",
    "content": "import React from 'react'\nimport {\n  act,\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport { QueryEditorContextProvider } from 'uiSrc/components/query'\nimport {\n  TOOLTIP_EXPLAIN,\n  TOOLTIP_PROFILE,\n  TOOLTIP_DISABLED_NO_QUERY,\n  TOOLTIP_DISABLED_LOADING,\n} from './QueryEditor.constants'\nimport { VectorSearchActions } from './VectorSearchActions'\n\nconst mockOnSubmit = jest.fn()\nconst mockOnSaveClick = jest.fn()\n\nconst renderComponent = ({\n  query = '',\n  isLoading = false,\n  onSaveClick = mockOnSaveClick,\n}: { query?: string; isLoading?: boolean; onSaveClick?: () => void } = {}) =>\n  render(\n    <QueryEditorContextProvider\n      value={{\n        query,\n        setQuery: jest.fn(),\n        isLoading,\n        commands: [],\n        indexes: [],\n        onSubmit: mockOnSubmit,\n      }}\n    >\n      <VectorSearchActions onSaveClick={onSaveClick} />\n    </QueryEditorContextProvider>,\n  )\n\ndescribe('VectorSearchActions', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render actions bar with save, run, explain, and profile buttons', () => {\n    renderComponent()\n\n    expect(screen.getByTestId('vector-search-actions')).toBeInTheDocument()\n    expect(screen.getByTestId('btn-save-query')).toBeInTheDocument()\n    expect(screen.getByTestId('btn-submit')).toBeInTheDocument()\n    expect(screen.getByTestId('btn-explain')).toBeInTheDocument()\n    expect(screen.getByTestId('btn-profile')).toBeInTheDocument()\n  })\n\n  it('should call onSubmit when run button is clicked', () => {\n    renderComponent()\n\n    fireEvent.click(screen.getByTestId('btn-submit'))\n    expect(mockOnSubmit).toHaveBeenCalled()\n  })\n\n  it('should disable run button when loading', () => {\n    renderComponent({ isLoading: true })\n\n    expect(screen.getByTestId('btn-submit')).toBeDisabled()\n  })\n\n  describe('Save button', () => {\n    it('should be disabled when query is empty', () => {\n      renderComponent({ query: '' })\n\n      expect(screen.getByTestId('btn-save-query')).toBeDisabled()\n    })\n\n    it('should be disabled when query is whitespace only', () => {\n      renderComponent({ query: '   ' })\n\n      expect(screen.getByTestId('btn-save-query')).toBeDisabled()\n    })\n\n    it('should be enabled when query has content', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      expect(screen.getByTestId('btn-save-query')).not.toBeDisabled()\n    })\n\n    it('should be disabled when loading', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"', isLoading: true })\n\n      expect(screen.getByTestId('btn-save-query')).toBeDisabled()\n    })\n\n    it('should call onSaveClick when clicked', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      fireEvent.click(screen.getByTestId('btn-save-query'))\n\n      expect(mockOnSaveClick).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  describe('Explain button', () => {\n    it('should be disabled when query is empty', () => {\n      renderComponent({ query: '' })\n\n      expect(screen.getByTestId('btn-explain')).toBeDisabled()\n    })\n\n    it('should be disabled for non-explainable commands', () => {\n      renderComponent({ query: 'GET mykey' })\n\n      expect(screen.getByTestId('btn-explain')).toBeDisabled()\n    })\n\n    it('should be enabled for FT.SEARCH query', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      expect(screen.getByTestId('btn-explain')).not.toBeDisabled()\n    })\n\n    it('should be enabled for FT.AGGREGATE query', () => {\n      renderComponent({ query: 'FT.AGGREGATE idx \"*\"' })\n\n      expect(screen.getByTestId('btn-explain')).not.toBeDisabled()\n    })\n\n    it('should be enabled for lowercase ft.search', () => {\n      renderComponent({ query: 'ft.search idx \"*\"' })\n\n      expect(screen.getByTestId('btn-explain')).not.toBeDisabled()\n    })\n\n    it('should be disabled when loading even with valid query', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"', isLoading: true })\n\n      expect(screen.getByTestId('btn-explain')).toBeDisabled()\n    })\n\n    it('should submit FT.EXPLAIN query when clicked', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\" LIMIT 0 10' })\n\n      fireEvent.click(screen.getByTestId('btn-explain'))\n      expect(mockOnSubmit).toHaveBeenCalledWith('FT.EXPLAIN idx \"*\" LIMIT 0 10')\n    })\n\n    it('should show disabled reason in tooltip when no valid query', async () => {\n      renderComponent({ query: '' })\n\n      await act(async () => {\n        fireEvent.focus(screen.getByTestId('btn-explain'))\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getAllByText(TOOLTIP_EXPLAIN)[0]).toBeInTheDocument()\n      expect(\n        screen.getAllByText(TOOLTIP_DISABLED_NO_QUERY)[0],\n      ).toBeInTheDocument()\n    })\n\n    it('should show loading reason in tooltip when query is running', async () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"', isLoading: true })\n\n      await act(async () => {\n        fireEvent.focus(screen.getByTestId('btn-explain'))\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getAllByText(TOOLTIP_EXPLAIN)[0]).toBeInTheDocument()\n      expect(\n        screen.getAllByText(TOOLTIP_DISABLED_LOADING)[0],\n      ).toBeInTheDocument()\n    })\n\n    it('should show only base tooltip when enabled', async () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      await act(async () => {\n        fireEvent.focus(screen.getByTestId('btn-explain'))\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getAllByText(TOOLTIP_EXPLAIN)[0]).toBeInTheDocument()\n      expect(\n        screen.queryByText(TOOLTIP_DISABLED_NO_QUERY),\n      ).not.toBeInTheDocument()\n      expect(\n        screen.queryByText(TOOLTIP_DISABLED_LOADING),\n      ).not.toBeInTheDocument()\n    })\n  })\n\n  describe('Profile button', () => {\n    it('should be disabled when query is empty', () => {\n      renderComponent({ query: '' })\n\n      expect(screen.getByTestId('btn-profile')).toBeDisabled()\n    })\n\n    it('should be disabled for non-explainable commands', () => {\n      renderComponent({ query: 'SET foo bar' })\n\n      expect(screen.getByTestId('btn-profile')).toBeDisabled()\n    })\n\n    it('should be enabled for FT.SEARCH query', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      expect(screen.getByTestId('btn-profile')).not.toBeDisabled()\n    })\n\n    it('should submit FT.PROFILE SEARCH query when clicked on FT.SEARCH', () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\" LIMIT 0 10' })\n\n      fireEvent.click(screen.getByTestId('btn-profile'))\n      expect(mockOnSubmit).toHaveBeenCalledWith(\n        'FT.PROFILE idx SEARCH QUERY \"*\" LIMIT 0 10',\n      )\n    })\n\n    it('should submit FT.PROFILE AGGREGATE query when clicked on FT.AGGREGATE', () => {\n      renderComponent({ query: 'FT.AGGREGATE idx \"*\" GROUPBY 1 @field' })\n\n      fireEvent.click(screen.getByTestId('btn-profile'))\n      expect(mockOnSubmit).toHaveBeenCalledWith(\n        'FT.PROFILE idx AGGREGATE QUERY \"*\" GROUPBY 1 @field',\n      )\n    })\n\n    it('should show disabled reason in tooltip when no valid query', async () => {\n      renderComponent({ query: '' })\n\n      await act(async () => {\n        fireEvent.focus(screen.getByTestId('btn-profile'))\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getAllByText(TOOLTIP_PROFILE)[0]).toBeInTheDocument()\n      expect(\n        screen.getAllByText(TOOLTIP_DISABLED_NO_QUERY)[0],\n      ).toBeInTheDocument()\n    })\n\n    it('should show only base tooltip when enabled', async () => {\n      renderComponent({ query: 'FT.SEARCH idx \"*\"' })\n\n      await act(async () => {\n        fireEvent.focus(screen.getByTestId('btn-profile'))\n      })\n      await waitForRiTooltipVisible()\n\n      expect(screen.getAllByText(TOOLTIP_PROFILE)[0]).toBeInTheDocument()\n      expect(\n        screen.queryByText(TOOLTIP_DISABLED_NO_QUERY),\n      ).not.toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/VectorSearchActions.tsx",
    "content": "import React, { useMemo } from 'react'\n\nimport { RiTooltip } from 'uiSrc/components'\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport RunButton from 'uiSrc/components/query/components/RunButton'\nimport { useQueryEditorContext } from 'uiSrc/components/query'\n\nimport {\n  TOOLTIP_EXPLAIN,\n  TOOLTIP_PROFILE,\n  TOOLTIP_DISABLED_NO_QUERY,\n  TOOLTIP_DISABLED_LOADING,\n} from './QueryEditor.constants'\nimport {\n  parseExplainableCommand,\n  buildExplainQuery,\n  buildProfileQuery,\n} from './QueryEditor.utils'\nimport * as S from './QueryEditor.styles'\nimport { SaveIcon } from 'uiSrc/components/base/icons'\n\ninterface VectorSearchActionsProps {\n  onSaveClick?: () => void\n}\n\n/**\n * Actions bar for Vector Search editor.\n *\n * Contains:\n * - **Save** – opens a modal to save the current query to the Query Library\n * - **Explain** – submits the query wrapped in FT.EXPLAIN\n * - **Profile** – submits the query wrapped in FT.PROFILE\n * - **Run** – submits the query as-is\n *\n * Explain and Profile are enabled only when the editor contains\n * a single FT.SEARCH or FT.AGGREGATE command.\n */\nexport const VectorSearchActions = ({\n  onSaveClick,\n}: VectorSearchActionsProps) => {\n  const { query, isLoading, onSubmit } = useQueryEditorContext()\n\n  const parsed = useMemo(() => parseExplainableCommand(query), [query])\n  const hasValidCommand = !!parsed\n  const isExplainEnabled = hasValidCommand && !isLoading\n\n  const hasQuery = !!query.trim()\n  const isSaveEnabled = hasQuery && !isLoading\n\n  const disabledReason = useMemo(() => {\n    if (!hasValidCommand) return TOOLTIP_DISABLED_NO_QUERY\n    if (isLoading) return TOOLTIP_DISABLED_LOADING\n    return undefined\n  }, [hasValidCommand, isLoading])\n\n  const handleExplain = () => {\n    if (!parsed) return\n    onSubmit(buildExplainQuery(parsed))\n  }\n\n  const handleProfile = () => {\n    if (!parsed) return\n    onSubmit(buildProfileQuery(parsed))\n  }\n\n  return (\n    <S.ActionsBar data-testid=\"vector-search-actions\">\n      <RiTooltip\n        position=\"top\"\n        title={TOOLTIP_EXPLAIN}\n        content={disabledReason}\n        data-testid=\"explain-tooltip\"\n      >\n        <EmptyButton\n          onClick={handleExplain}\n          disabled={!isExplainEnabled}\n          aria-label=\"explain\"\n          data-testid=\"btn-explain\"\n        >\n          Explain\n        </EmptyButton>\n      </RiTooltip>\n      <RiTooltip\n        position=\"top\"\n        title={TOOLTIP_PROFILE}\n        content={disabledReason}\n        data-testid=\"profile-tooltip\"\n      >\n        <EmptyButton\n          onClick={handleProfile}\n          disabled={!isExplainEnabled}\n          aria-label=\"profile\"\n          data-testid=\"btn-profile\"\n        >\n          Profile\n        </EmptyButton>\n      </RiTooltip>\n      <EmptyButton\n        icon={SaveIcon}\n        onClick={onSaveClick}\n        disabled={!isSaveEnabled}\n        aria-label=\"save\"\n        data-testid=\"btn-save-query\"\n      >\n        Save\n      </EmptyButton>\n      <RunButton isLoading={isLoading} onSubmit={() => onSubmit()} />\n    </S.ActionsBar>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/VectorSearchEditor.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { MonacoLanguage } from 'uiSrc/constants'\nimport { CodeEditor } from 'uiSrc/components/base/code-editor'\nimport { useQueryEditorContext, useQueryEditor } from 'uiSrc/components/query'\nimport { UseRedisCompletionsReturn } from 'uiSrc/components/query/hooks/useRedisCompletions.types'\n\nimport { EDITOR_OPTIONS, EDITOR_PLACEHOLDER } from './QueryEditor.constants'\nimport { getOnboardingSuggestions } from './onboardingSuggestions'\nimport * as S from './QueryEditor.styles'\n\n/**\n * Auto-opens the suggestion widget when the editor is empty.\n *\n * This only **triggers** the widget; the actual FT.* onboarding data\n * is provided by a dedicated completion provider registered in `onSetup`.\n * That provider is independent of `suggestionsRef`, so the templates are\n * always present regardless of whether cursor-change handlers overwrite\n * the main suggestions list.\n */\nconst triggerEmptySuggestions = (\n  editor: monacoEditor.editor.IStandaloneCodeEditor,\n  completions: UseRedisCompletionsReturn,\n) => {\n  if (editor.getValue()?.trim()) return\n\n  completions.setEscapedSuggestions(false)\n\n  setTimeout(() => {\n    if (editor.getValue()?.trim()) return\n    editor.trigger('', 'editor.action.triggerSuggest', { auto: false })\n  })\n}\n\n/**\n * Vector Search editor component.\n * Uses the shared useQueryEditor hook (no DSL syntax, no command history).\n *\n * **Onboarding flow** (Vector Search–specific):\n *\n * When the editor is empty and receives focus, a suggestions panel is\n * shown with a predefined list of RQE query templates (FT.SEARCH,\n * FT.AGGREGATE, FT.SUGGET, FT.SPELLCHECK, FT.EXPLAIN, FT.PROFILE,\n * FT._LIST).  Each template shows its description first; full\n * documentation is expandable via the Monaco details panel.\n *\n * Templates are **index-aware**: when available indexes exist, the\n * first index name is pre-filled in snippet placeholders.\n *\n * Once the user picks a template or starts typing, the normal\n * autocomplete behaviour takes over with all Redis commands available.\n */\nexport const VectorSearchEditor = () => {\n  const { query, onSubmit, indexes, activeIndexName } = useQueryEditorContext()\n  // Start as true because useMonacoRedisEditor auto-focuses the editor on mount\n  const [focused, setFocused] = useState(true)\n  const [contentLeft, setContentLeft] = useState(0)\n  const disposeOnboardingRef = useRef<(() => void) | null>(null)\n  // Keep refs to always read the latest values inside mount-time closures\n  const indexesRef = useRef(indexes)\n  indexesRef.current = indexes\n  const activeIndexNameRef = useRef(activeIndexName)\n  activeIndexNameRef.current = activeIndexName\n  // Track whether the user has interacted with the editor\n  const hasInteractedRef = useRef(false)\n\n  // Dispose the onboarding completion provider on unmount\n  useEffect(\n    () => () => {\n      disposeOnboardingRef.current?.()\n    },\n    [],\n  )\n\n  const { editorDidMount, onChange } = useQueryEditor({\n    onSubmit,\n    onSetup: (editor, monaco, completions) => {\n      // Handle \"No indexes\" suggestion interaction\n      completions.setupSuggestionWidgetListener(editor)\n\n      // Read Monaco's layout so the placeholder aligns with the content area\n      setContentLeft(editor.getLayoutInfo().contentLeft)\n      editor.onDidLayoutChange((info) => setContentLeft(info.contentLeft))\n\n      // Register an additional completion provider that returns FT.*\n      // onboarding templates when the editor is empty.  Monaco merges\n      // results from all providers; the templates sort first via sortText.\n      disposeOnboardingRef.current?.()\n      disposeOnboardingRef.current =\n        monaco.languages.registerCompletionItemProvider(\n          MonacoLanguage.Redis as string,\n          {\n            provideCompletionItems: (model) => {\n              if (model.getValue().trim()) return { suggestions: [] }\n              return {\n                suggestions: getOnboardingSuggestions(\n                  indexesRef.current,\n                  activeIndexNameRef.current,\n                ),\n              }\n            },\n          },\n        ).dispose\n\n      // Re-open when content is deleted back to empty (only after first interaction)\n      editor.onDidChangeModelContent(() => {\n        hasInteractedRef.current = true\n        if (!editor.getValue()?.trim()) {\n          triggerEmptySuggestions(editor, completions)\n        }\n      })\n\n      // Open suggestions on click when the editor is empty\n      editor.onMouseDown(() => {\n        if (!hasInteractedRef.current) {\n          hasInteractedRef.current = true\n          triggerEmptySuggestions(editor, completions)\n        }\n      })\n\n      // Re-open when the editor regains focus while still empty (only after first interaction)\n      editor.onDidFocusEditorWidget(() => {\n        setFocused(true)\n        if (hasInteractedRef.current) {\n          triggerEmptySuggestions(editor, completions)\n        }\n      })\n\n      editor.onDidBlurEditorWidget(() => {\n        setFocused(false)\n      })\n    },\n  })\n\n  return (\n    <S.EditorContainer as=\"div\" data-testid=\"vector-search-editor\">\n      {!query && !focused && (\n        <S.EditorPlaceholder\n          as=\"div\"\n          $contentLeft={contentLeft}\n          data-testid=\"editor-placeholder\"\n        >\n          {EDITOR_PLACEHOLDER}\n        </S.EditorPlaceholder>\n      )}\n      <CodeEditor\n        language={MonacoLanguage.Redis as string}\n        value={query}\n        options={EDITOR_OPTIONS}\n        className={`${MonacoLanguage.Redis}-editor`}\n        onChange={onChange}\n        editorDidMount={editorDidMount}\n      />\n    </S.EditorContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/components/query-onboarding-popover/QueryOnboardingPopover.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport BrowserStorageItem from 'uiSrc/constants/storage'\nimport { localStorageService } from 'uiSrc/services'\n\nimport { QueryOnboardingPopover } from './QueryOnboardingPopover'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('QueryOnboardingPopover', () => {\n  const renderComponent = () =>\n    render(\n      <QueryOnboardingPopover>\n        <button type=\"button\" data-testid=\"trigger\">\n          Trigger\n        </button>\n      </QueryOnboardingPopover>,\n    )\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should show onboarding popover on first visit', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(null)\n\n    renderComponent()\n\n    const onboardingContent = screen.getByTestId(\n      'query-library-onboarding-content',\n    )\n    const title = screen.getByText('Index created successfully.')\n\n    expect(onboardingContent).toBeInTheDocument()\n    expect(title).toBeInTheDocument()\n  })\n\n  it('should not show onboarding popover when already seen', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(true)\n\n    renderComponent()\n\n    const onboardingContent = screen.queryByTestId(\n      'query-library-onboarding-content',\n    )\n\n    expect(onboardingContent).not.toBeInTheDocument()\n  })\n\n  it('should render children when popover is not shown', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(true)\n\n    renderComponent()\n\n    const trigger = screen.getByTestId('trigger')\n\n    expect(trigger).toBeInTheDocument()\n  })\n\n  it('should mark as seen in localStorage when Got it is clicked', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(null)\n\n    renderComponent()\n\n    const dismissButton = screen.getByTestId('query-library-onboarding-dismiss')\n    fireEvent.click(dismissButton)\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.vectorSearchQueryOnboarding,\n      true,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/components/query-onboarding-popover/QueryOnboardingPopover.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Content = styled(Col)`\n  max-width: 340px;\n`\n\nexport const Section = styled(Col)`\n  gap: ${({ theme }) => theme.core.space.space025};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/components/query-onboarding-popover/QueryOnboardingPopover.tsx",
    "content": "import React, { useCallback, useState } from 'react'\n\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Button } from 'uiSrc/components/base/forms/buttons'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport BrowserStorageItem from 'uiSrc/constants/storage'\nimport { localStorageService } from 'uiSrc/services'\nimport { QueryOnboardingPopoverProps } from './QueryOnboardingPopover.types'\nimport * as S from './QueryOnboardingPopover.styles'\n\nexport const QueryOnboardingPopover = ({\n  children,\n}: QueryOnboardingPopoverProps) => {\n  const [isOpen, setIsOpen] = useState(\n    () =>\n      localStorageService.get(\n        BrowserStorageItem.vectorSearchQueryOnboarding,\n      ) !== true,\n  )\n\n  const handleDismiss = useCallback(() => {\n    localStorageService.set(\n      BrowserStorageItem.vectorSearchQueryOnboarding,\n      true,\n    )\n    setIsOpen(false)\n  }, [])\n\n  if (!isOpen) {\n    return <>{children}</>\n  }\n\n  return (\n    <RiPopover\n      isOpen\n      anchorPosition=\"rightUp\"\n      data-testid=\"query-library-onboarding-popover\"\n      trigger={children}\n    >\n      <S.Content gap=\"l\" data-testid=\"query-library-onboarding-content\">\n        <Text size=\"L\" variant=\"semiBold\" color=\"primary\">\n          Index created successfully.\n        </Text>\n        <Text size=\"m\" color=\"secondary\">\n          Your data is now searchable. Choose how you&apos;d like to search\n        </Text>\n\n        <S.Section>\n          <Text size=\"m\" variant=\"semiBold\" color=\"primary\">\n            Query editor\n          </Text>\n          <Text size=\"s\" color=\"secondary\">\n            write search queries directly using Redis commands.\n          </Text>\n        </S.Section>\n\n        <S.Section>\n          <Text size=\"m\" variant=\"semiBold\" color=\"primary\">\n            Query library\n          </Text>\n          <Text size=\"s\" color=\"secondary\">\n            reuse saved queries or use prebuilt examples for the sample data.\n          </Text>\n        </S.Section>\n\n        <Row justify=\"end\">\n          <Button\n            size=\"small\"\n            onClick={handleDismiss}\n            data-testid=\"query-library-onboarding-dismiss\"\n          >\n            Got it\n          </Button>\n        </Row>\n      </S.Content>\n    </RiPopover>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/components/query-onboarding-popover/QueryOnboardingPopover.types.ts",
    "content": "import React from 'react'\n\nexport interface QueryOnboardingPopoverProps {\n  children: React.ReactNode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/components/query-onboarding-popover/index.ts",
    "content": "export { QueryOnboardingPopover } from './QueryOnboardingPopover'\nexport type { QueryOnboardingPopoverProps } from './QueryOnboardingPopover.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/index.ts",
    "content": "export { QueryEditorWrapper } from './QueryEditorWrapper'\nexport { VectorSearchEditor } from './VectorSearchEditor'\nexport { VectorSearchActions } from './VectorSearchActions'\nexport { EditorLibraryToggle } from './EditorLibraryToggle'\nexport { EditorTab } from './QueryEditor.types'\nexport type { QueryEditorWrapperProps } from './QueryEditor.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/onboardingSuggestions.spec.ts",
    "content": "import {\n  getOnboardingSuggestions,\n  ONBOARDING_TEMPLATES,\n} from './onboardingSuggestions'\n\nconst EXPECTED_COMMANDS = [\n  'FT.SEARCH',\n  'FT.AGGREGATE',\n  'FT.SUGGET',\n  'FT.SPELLCHECK',\n  'FT.EXPLAIN',\n  'FT.PROFILE',\n  'FT._LIST',\n]\n\ndescribe('getOnboardingSuggestions', () => {\n  it('should return one suggestion per onboarding template', () => {\n    const suggestions = getOnboardingSuggestions()\n\n    expect(suggestions).toHaveLength(ONBOARDING_TEMPLATES.length)\n  })\n\n  it('should contain the expected FT.* commands', () => {\n    const suggestions = getOnboardingSuggestions()\n    const labels = suggestions.map((s) => s.label)\n\n    expect(labels).toEqual(EXPECTED_COMMANDS)\n  })\n\n  it('should include a detail description for every suggestion', () => {\n    const suggestions = getOnboardingSuggestions()\n\n    suggestions.forEach((s) => {\n      expect(s.detail).toBeTruthy()\n    })\n  })\n\n  it('should include documentation for every suggestion', () => {\n    const suggestions = getOnboardingSuggestions()\n\n    suggestions.forEach((s) => {\n      expect(s.documentation).toBeTruthy()\n    })\n  })\n\n  it('should use generic index placeholder when no indexes provided', () => {\n    const suggestions = getOnboardingSuggestions()\n    const ftSearch = suggestions.find((s) => s.label === 'FT.SEARCH')\n\n    expect(ftSearch?.insertText).toContain('${1:index}')\n  })\n\n  it('should preselect first available index in snippet when indexes exist', () => {\n    const mockIndexes = [{ data: Buffer.from('my-idx'), type: 'Buffer' }] as any\n\n    const suggestions = getOnboardingSuggestions(mockIndexes)\n    const ftSearch = suggestions.find((s) => s.label === 'FT.SEARCH')\n\n    expect(ftSearch?.insertText).toContain('my-idx')\n  })\n\n  it('should prefer activeIndexName over the first index in the list', () => {\n    const mockIndexes = [\n      { data: Buffer.from('first-idx'), type: 'Buffer' },\n    ] as any\n\n    const suggestions = getOnboardingSuggestions(mockIndexes, 'active-idx')\n    const ftSearch = suggestions.find((s) => s.label === 'FT.SEARCH')\n\n    expect(ftSearch?.insertText).toContain('active-idx')\n    expect(ftSearch?.insertText).not.toContain('first-idx')\n  })\n\n  it('should use activeIndexName even when no indexes array is provided', () => {\n    const suggestions = getOnboardingSuggestions([], 'active-idx')\n    const ftSearch = suggestions.find((s) => s.label === 'FT.SEARCH')\n\n    expect(ftSearch?.insertText).toContain('active-idx')\n  })\n\n  it('should insert activeIndexName as fixed text so Tab jumps to query', () => {\n    const suggestions = getOnboardingSuggestions([], 'active-idx')\n    const ftSearch = suggestions.find((s) => s.label === 'FT.SEARCH')\n\n    // Index is plain text, not a tab-stop\n    expect(ftSearch?.insertText).toContain(\"'active-idx'\")\n    expect(ftSearch?.insertText).not.toContain('${1:active-idx}')\n    // Query placeholder is ${1:...} (first tab-stop)\n    expect(ftSearch?.insertText).toContain('\"${1:*}\"')\n  })\n\n  it('should use editable tab-stop for index when no activeIndexName', () => {\n    const mockIndexes = [{ data: Buffer.from('my-idx'), type: 'Buffer' }] as any\n    const suggestions = getOnboardingSuggestions(mockIndexes)\n    const ftSearch = suggestions.find((s) => s.label === 'FT.SEARCH')\n\n    // Index is tab-stop 1, query is tab-stop 2\n    expect(ftSearch?.insertText).toContain('${1:my-idx}')\n    expect(ftSearch?.insertText).toContain('\"${2:*}\"')\n  })\n\n  it('should not include index placeholder for non-index commands', () => {\n    const suggestions = getOnboardingSuggestions()\n    const ftList = suggestions.find((s) => s.label === 'FT._LIST')\n\n    expect(ftList?.insertText).toBe('FT._LIST')\n  })\n\n  it('should sort templates before regular commands via sortText', () => {\n    const suggestions = getOnboardingSuggestions()\n\n    suggestions.forEach((s) => {\n      expect(s.sortText).toMatch(/^!/)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-editor/onboardingSuggestions.ts",
    "content": "import * as monacoEditor from 'monaco-editor'\n\nimport { bufferToString, formatLongName } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { OnboardingTemplate } from './QueryEditor.types'\n\n/**\n * Predefined RQE query templates for the Vector Search onboarding panel.\n * Shown when the editor is empty and receives focus.\n *\n * List per RI-7928 ticket (exact list TBD with Product).\n */\nexport const ONBOARDING_TEMPLATES: OnboardingTemplate[] = [\n  {\n    command: 'FT.SEARCH',\n    detail: 'Find documents by text or filters',\n    usesIndex: true,\n  },\n  {\n    command: 'FT.AGGREGATE',\n    detail: 'Group and summarize results',\n    usesIndex: true,\n  },\n  {\n    command: 'FT.SUGGET',\n    detail: 'Retrieve autocomplete suggestions',\n    usesIndex: false,\n  },\n  {\n    command: 'FT.SPELLCHECK',\n    detail: 'Suggest corrections for typos',\n    usesIndex: true,\n  },\n  {\n    command: 'FT.EXPLAIN',\n    detail: 'See execution plan',\n    usesIndex: true,\n  },\n  {\n    command: 'FT.PROFILE',\n    detail: 'Analyze performance',\n    usesIndex: true,\n  },\n  {\n    command: 'FT._LIST',\n    detail: 'View index schema and stats',\n    usesIndex: false,\n  },\n]\n\n/** Default range for an empty editor (cursor at 1:1). */\nconst EMPTY_EDITOR_RANGE: monacoEditor.IRange = {\n  startLineNumber: 1,\n  startColumn: 1,\n  endLineNumber: 1,\n  endColumn: 1,\n}\n\nconst getDocUrl = (command: string): string =>\n  getUtmExternalLink(`https://redis.io/commands/${command.toLowerCase()}/`, {\n    campaign: 'vector_search',\n  })\n\n/**\n * Resolves the preferred index into a snippet string.\n *\n * When `activeIndexName` is provided (the index from the current URL),\n * it is inserted as **fixed text** so that Tab skips straight to the\n * query placeholder.  Otherwise the index is an editable tab-stop.\n *\n * Returns `{ snippet, isFixed }` so that `getInsertText` can number\n * subsequent tab-stops correctly.\n */\nconst getIndexSnippet = (\n  indexes: RedisResponseBuffer[],\n  activeIndexName?: string,\n): { snippet: string; isFixed: boolean } => {\n  if (activeIndexName !== undefined) {\n    return { snippet: `'${activeIndexName}'`, isFixed: true }\n  }\n  if (indexes.length === 0) {\n    return { snippet: '${1:index}', isFixed: false }\n  }\n\n  const name = formatLongName(bufferToString(indexes[0]))\n  return { snippet: `'\\${1:${name}}'`, isFixed: false }\n}\n\n/**\n * Builds the snippet insert-text for a given command.\n *\n * When the index is fixed (active index from the URL), subsequent\n * tab-stops start at `$1`.  When the index itself is a tab-stop,\n * they start at `$2`.\n */\nconst getInsertText = (\n  command: string,\n  indexSnippet: string,\n  isIndexFixed: boolean,\n): string => {\n  const n = isIndexFixed ? 1 : 2\n\n  switch (command) {\n    case 'FT.SEARCH':\n      return `FT.SEARCH ${indexSnippet} \"\\${${n}:*}\"`\n    case 'FT.AGGREGATE':\n      return `FT.AGGREGATE ${indexSnippet} \"\\${${n}:*}\"`\n    case 'FT.SUGGET':\n      return 'FT.SUGGET ${1:key} ${2:prefix}'\n    case 'FT.SPELLCHECK':\n      return `FT.SPELLCHECK ${indexSnippet} \"\\${${n}:query}\"`\n    case 'FT.EXPLAIN':\n      return `FT.EXPLAIN ${indexSnippet} \"\\${${n}:*}\"`\n    case 'FT.PROFILE':\n      return `FT.PROFILE ${indexSnippet} SEARCH QUERY \"\\${${n}:*}\"`\n    case 'FT._LIST':\n      return 'FT._LIST'\n    default:\n      return command\n  }\n}\n\n/**\n * Builds the predefined RQE query-template suggestions shown when the\n * Vector Search editor is empty and receives focus (\"onboarding\").\n *\n * - Shows query details first (via `detail`).\n * - Full documentation is expandable in the Monaco details panel.\n * - Templates are **index-aware**: when available indexes exist, the\n *   first index name is pre-filled in snippet tab-stop placeholders.\n * - Uses `sortText` starting with `!` so templates sort before any\n *   regular command suggestions.\n */\nexport const getOnboardingSuggestions = (\n  indexes: RedisResponseBuffer[] = [],\n  activeIndexName?: string,\n): monacoEditor.languages.CompletionItem[] => {\n  const { snippet, isFixed } = getIndexSnippet(indexes, activeIndexName)\n\n  return ONBOARDING_TEMPLATES.map((t, i) => ({\n    label: t.command,\n    kind: monacoEditor.languages.CompletionItemKind.Snippet,\n    detail: t.detail,\n    documentation: {\n      value: `**${t.command}** — ${t.detail}\\n\\n[Documentation](${getDocUrl(t.command)})`,\n    },\n    insertText: getInsertText(\n      t.command,\n      t.usesIndex ? snippet : '',\n      t.usesIndex && isFixed,\n    ),\n    insertTextRules:\n      monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    range: EMPTY_EDITOR_RANGE,\n    sortText: `!${String(i).padStart(2, '0')}`,\n  })) as monacoEditor.languages.CompletionItem[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/QueryLibraryItem.constants.ts",
    "content": "import {\n  QueryLibraryItemType,\n  QueryTypeBadgeConfig,\n} from './QueryLibraryItem.types'\n\nexport const QUERY_TYPE_BADGE_MAP: Record<\n  QueryLibraryItemType,\n  QueryTypeBadgeConfig\n> = {\n  [QueryLibraryItemType.Sample]: {\n    label: 'Sample query',\n    variant: 'default',\n  },\n  [QueryLibraryItemType.Saved]: {\n    label: 'Saved query',\n    variant: 'white',\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/QueryLibraryItem.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\n\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport {\n  QueryLibraryItemProps,\n  QueryLibraryItemType,\n} from './QueryLibraryItem.types'\n\njest.mock('uiSrc/components/base/code-editor', () => {\n  const React = require('react')\n  return {\n    __esModule: true,\n    CodeEditor: (props: any) =>\n      React.createElement(\n        'div',\n        {\n          'data-testid': props['data-testid'],\n        },\n        props.value,\n      ),\n  }\n})\n\nimport { QueryLibraryItem } from './QueryLibraryItem'\n\ndescribe('QueryLibraryItem', () => {\n  const defaultProps: QueryLibraryItemProps = {\n    id: faker.string.uuid(),\n    name: faker.lorem.words(3),\n    type: QueryLibraryItemType.Sample,\n    query: 'FT.SEARCH idx \"@field:{value}\" RETURN 3 f1 f2 f3',\n    description: faker.lorem.sentence(),\n    onRun: jest.fn(),\n    onLoad: jest.fn(),\n    onDelete: jest.fn(),\n    onToggleOpen: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<QueryLibraryItemProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    return render(<QueryLibraryItem {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  describe('rendering', () => {\n    it('should render component', () => {\n      renderComponent()\n\n      expect(screen.getByTestId('query-library-item')).toBeInTheDocument()\n    })\n\n    it('should render query name and description', () => {\n      renderComponent()\n\n      expect(screen.getByTestId('query-library-item-name')).toHaveTextContent(\n        defaultProps.name,\n      )\n      expect(\n        screen.getByTestId('query-library-item-description'),\n      ).toHaveTextContent(defaultProps.description!)\n    })\n\n    it('should not render description when not provided', () => {\n      renderComponent({ description: undefined })\n\n      expect(\n        screen.queryByTestId('query-library-item-description'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should render type badge for sample queries', () => {\n      renderComponent({ type: QueryLibraryItemType.Sample })\n\n      expect(\n        screen.getByTestId('query-library-item-type-badge'),\n      ).toHaveTextContent('Sample')\n    })\n\n    it('should render type badge for saved queries', () => {\n      renderComponent({ type: QueryLibraryItemType.Saved })\n\n      expect(\n        screen.getByTestId('query-library-item-type-badge'),\n      ).toHaveTextContent('Saved')\n    })\n\n    it('should render action buttons', () => {\n      renderComponent()\n\n      expect(\n        screen.getByTestId('query-library-item-run-btn'),\n      ).toBeInTheDocument()\n      expect(\n        screen.getByTestId('query-library-item-load-btn'),\n      ).toBeInTheDocument()\n      expect(\n        screen.getByTestId('query-library-item-delete-btn'),\n      ).toBeInTheDocument()\n    })\n\n    it('should not render action buttons when callbacks are not provided', () => {\n      renderComponent({\n        onRun: undefined,\n        onLoad: undefined,\n        onDelete: undefined,\n      })\n\n      expect(\n        screen.queryByTestId('query-library-item-run-btn'),\n      ).not.toBeInTheDocument()\n      expect(\n        screen.queryByTestId('query-library-item-load-btn'),\n      ).not.toBeInTheDocument()\n      expect(\n        screen.queryByTestId('query-library-item-delete-btn'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should render copy button', () => {\n      renderComponent()\n\n      expect(\n        screen.getByTestId('query-library-item-copy-btn'),\n      ).toBeInTheDocument()\n    })\n  })\n\n  describe('expand/collapse', () => {\n    it('should be collapsed by default', () => {\n      renderComponent()\n\n      expect(\n        screen.queryByTestId('query-library-item-body'),\n      ).not.toBeInTheDocument()\n    })\n\n    it('should show body with command view when open', () => {\n      renderComponent({ isOpen: true })\n\n      expect(screen.getByTestId('query-library-item-body')).toBeInTheDocument()\n      expect(\n        screen.getByTestId('query-library-item-command-view'),\n      ).toBeInTheDocument()\n    })\n\n    it('should display query text in command view when expanded', () => {\n      const query = 'FT.SEARCH myIndex \"*\"'\n      renderComponent({ isOpen: true, query })\n\n      expect(\n        screen.getByTestId('query-library-item-command-view--editor'),\n      ).toHaveTextContent(query)\n    })\n\n    it('should toggle open state and update aria-expanded on header click', () => {\n      const onToggleOpen = jest.fn()\n      const { rerender } = renderComponent({ onToggleOpen, isOpen: false })\n\n      const header = screen.getByTestId('query-library-item-header')\n      expect(header).toHaveAttribute('aria-expanded', 'false')\n\n      fireEvent.click(header)\n      expect(onToggleOpen).toHaveBeenCalledTimes(1)\n      expect(onToggleOpen).toHaveBeenCalledWith(defaultProps.id)\n\n      rerender(\n        <QueryLibraryItem\n          {...defaultProps}\n          onToggleOpen={onToggleOpen}\n          isOpen\n        />,\n      )\n      expect(header).toHaveAttribute('aria-expanded', 'true')\n    })\n  })\n\n  describe('action callbacks', () => {\n    it('should call action callbacks with id when buttons are clicked', () => {\n      const onRun = jest.fn()\n      const onLoad = jest.fn()\n      const onDelete = jest.fn()\n      renderComponent({ onRun, onLoad, onDelete })\n\n      fireEvent.click(screen.getByTestId('query-library-item-run-btn'))\n      expect(onRun).toHaveBeenCalledTimes(1)\n      expect(onRun).toHaveBeenCalledWith(defaultProps.id)\n\n      fireEvent.click(screen.getByTestId('query-library-item-load-btn'))\n      expect(onLoad).toHaveBeenCalledTimes(1)\n      expect(onLoad).toHaveBeenCalledWith(defaultProps.id)\n\n      fireEvent.click(screen.getByTestId('query-library-item-delete-btn'))\n      expect(onDelete).toHaveBeenCalledTimes(1)\n      expect(onDelete).toHaveBeenCalledWith(defaultProps.id)\n    })\n\n    it('should not trigger onToggleOpen when action buttons are clicked', () => {\n      const onToggleOpen = jest.fn()\n      renderComponent({ onToggleOpen })\n\n      fireEvent.click(screen.getByTestId('query-library-item-run-btn'))\n      fireEvent.click(screen.getByTestId('query-library-item-load-btn'))\n      fireEvent.click(screen.getByTestId('query-library-item-delete-btn'))\n\n      expect(onToggleOpen).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/QueryLibraryItem.stories.tsx",
    "content": "import React, { useState } from 'react'\nimport type { Meta, StoryObj } from '@storybook/react-vite'\nimport { faker } from '@faker-js/faker'\nimport styled from 'styled-components'\nimport { useDispatch } from 'react-redux'\nimport { MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport { getRedisCommandsSuccess } from 'uiSrc/slices/app/redis-commands'\nimport { Col } from 'uiSrc/components/base/layout/flex'\nimport MonacoEnvironmentInitializer from 'uiSrc/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer'\nimport MonacoLanguages from 'uiSrc/components/monaco-laguages'\n\nimport { QueryLibraryItem } from './QueryLibraryItem'\nimport {\n  QueryLibraryItemProps,\n  QueryLibraryItemType,\n} from './QueryLibraryItem.types'\n\nconst withMonacoSetup = (Story: React.ComponentType) => {\n  const MonacoSetup = () => {\n    const dispatch = useDispatch()\n\n    React.useEffect(() => {\n      // @ts-ignore\n      dispatch(getRedisCommandsSuccess(MOCK_COMMANDS_SPEC))\n    }, [dispatch])\n\n    return (\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <MonacoEnvironmentInitializer />\n        <MonacoLanguages />\n        <Story />\n      </div>\n    )\n  }\n\n  return <MonacoSetup />\n}\n\nconst InteractiveWrapper = (props: QueryLibraryItemProps) => {\n  const [openId, setOpenId] = useState<string | null>(null)\n\n  return (\n    <Col style={{ maxWidth: '900px', gap: '8px' }}>\n      <QueryLibraryItem\n        {...props}\n        isOpen={openId === props.id}\n        onToggleOpen={(id) => setOpenId((prev) => (prev === id ? null : id))}\n      />\n    </Col>\n  )\n}\n\nconst ListWrapper = styled(Col)`\n  width: 100%;\n  height: 100%;\n  overflow: auto;\n  justify-content: flex-start;\n\n  & > * {\n    flex-grow: 0;\n    flex-shrink: 0;\n  }\n\n  & > *:not(:first-child) {\n    margin-top: -1px;\n  }\n`\n\nconst MultiItemWrapper = ({ items }: { items: QueryLibraryItemProps[] }) => {\n  const [openId, setOpenId] = useState<string | null>(null)\n\n  return (\n    <ListWrapper justify=\"start\">\n      {items.map((item) => (\n        <QueryLibraryItem\n          key={item.id}\n          {...item}\n          isOpen={openId === item.id}\n          onToggleOpen={(id) => setOpenId((prev) => (prev === id ? null : id))}\n        />\n      ))}\n    </ListWrapper>\n  )\n}\n\nconst meta: Meta<typeof QueryLibraryItem> = {\n  component: QueryLibraryItem,\n  tags: ['autodocs'],\n  decorators: [withMonacoSetup],\n  parameters: {\n    docs: {\n      description: {\n        component:\n          'A reusable Query Library item component with a header (name, description, type badge), action buttons (Run, Load, Delete), and an expandable read-only code preview.',\n      },\n    },\n  },\n  argTypes: {\n    type: {\n      control: 'select',\n      options: [QueryLibraryItemType.Sample, QueryLibraryItemType.Saved],\n    },\n    onRun: { action: 'onRun' },\n    onLoad: { action: 'onLoad' },\n    onDelete: { action: 'onDelete' },\n    onToggleOpen: { action: 'onToggleOpen' },\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Interactive: Story = {\n  name: 'Interactive (click to expand)',\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 350,\n      },\n    },\n  },\n  render: (args) => <InteractiveWrapper {...args} />,\n  args: {\n    id: 'interactive-1',\n    name: 'Aggregation pipeline',\n    description: 'Group bikes by brand and calculate averages',\n    type: QueryLibraryItemType.Sample,\n    query: `FT.AGGREGATE idx:bikes \"*\"\n  GROUPBY 1 @brand\n    REDUCE COUNT 0 AS count\n    REDUCE AVG 1 @price AS avg_price\n  SORTBY 2 @count DESC\n  LIMIT 0 5`,\n    onRun: () => {},\n    onLoad: () => {},\n    onDelete: () => {},\n  },\n}\n\nexport const LongText: Story = {\n  name: 'Long name and description (overflow)',\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 350,\n      },\n    },\n  },\n  render: (args) => <InteractiveWrapper {...args} />,\n  args: {\n    id: 'long-text-1',\n    name: 'This is a very long query name that should be truncated with an ellipsis when it overflows the available space in the header',\n    description:\n      'This is an equally long description that explains in great detail what this query does including filtering by multiple fields sorting by relevance and returning paginated results with vector similarity scores',\n    type: QueryLibraryItemType.Saved,\n    query: `FT.SEARCH idx:products\n  \"(@category:{electronics} @brand:{Samsung|Apple|Sony})\n  =>[KNN 10 @embedding $BLOB AS score]\"\n  PARAMS 2 BLOB \"\\\\x00\\\\x01\\\\x02\\\\x03\"\n  SORTBY score ASC\n  RETURN 5 name brand price category score\n  LIMIT 0 20`,\n    onRun: () => {},\n    onLoad: () => {},\n    onDelete: () => {},\n  },\n}\n\nexport const ExtremelyLongName: Story = {\n  name: 'Extremely long name (exceeds container)',\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 350,\n      },\n    },\n  },\n  render: (args) => <InteractiveWrapper {...args} />,\n  args: {\n    id: 'extreme-name-1',\n    name: [\n      'Product catalog aggregation by category and brand',\n      'with average price and maximum rating sorted by',\n      'count descending and price ascending limited to',\n      'top fifty results for the electronics department',\n      'including subcategories accessories and refurbished',\n      'items filtered by availability region warehouse',\n      'stock levels and seasonal promotional discounts',\n      'applied to premium membership tiers gold silver',\n      'and platinum with loyalty points multiplier active',\n      'for the current fiscal quarter ending March',\n      'thirty first across all participating retail',\n      'locations in North America and Western Europe',\n    ].join(' '),\n    type: QueryLibraryItemType.Sample,\n    query: 'FT.SEARCH idx:products \"*\" RETURN 1 name',\n    onRun: () => {},\n    onLoad: () => {},\n    onDelete: () => {},\n  },\n}\n\nexport const MultipleItems: Story = {\n  name: 'Multiple items (list)',\n  parameters: {\n    docs: {\n      story: {\n        inline: false,\n        iframeHeight: 800,\n      },\n    },\n  },\n  render: () => {\n    const types = [QueryLibraryItemType.Sample, QueryLibraryItemType.Saved]\n    const items = Array.from({ length: 10 }, (_, i) => ({\n      id: `list-${i + 1}`,\n      name: faker.lorem.words(3),\n      description: i % 3 === 0 ? undefined : faker.lorem.sentence(),\n      type: types[i % 2],\n      query: `FT.SEARCH idx:${faker.word.noun()} \"${faker.lorem.words(2)}\"`,\n      onRun: () => {},\n      onLoad: () => {},\n      onDelete: () => {},\n    }))\n\n    return <MultiItemWrapper items={items} />\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/QueryLibraryItem.styles.ts",
    "content": "import styled, { css } from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const Container = styled(Col)`\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral400};\n  min-height: 45px;\n  flex-grow: 0;\n  flex-shrink: 0;\n`\n\nconst HEADER_HEIGHT = '45px'\n\nexport const Header = styled(Row)`\n  height: ${HEADER_HEIGHT};\n  max-height: ${HEADER_HEIGHT};\n  min-height: ${HEADER_HEIGHT};\n  padding: ${({ theme }) => `0 ${theme.core.space.space200}`};\n  cursor: pointer;\n  user-select: none;\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral200};\n\n  &:hover {\n    background-color: ${({ theme }) =>\n      theme.semantic.color.background.neutral300};\n  }\n`\n\nexport const HeaderInfo = styled(Row)`\n  min-width: 0;\n  flex: 1;\n  overflow: hidden;\n`\n\nconst truncatedText = css`\n  display: flex;\n  align-items: center;\n\n  span {\n    display: inline-block;\n    max-width: 100%;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n  }\n`\n\nexport const CopyButtonWrapper = styled.span`\n  padding-left: ${({ theme }) => theme.core.space.space025};\n  opacity: 0;\n  flex-shrink: 0;\n  transition: opacity 250ms ease-in-out;\n`\n\nexport const Name = styled(Text)`\n  flex-shrink: 0;\n  max-width: 80%;\n  ${truncatedText}\n\n  &:hover {\n    ${CopyButtonWrapper} {\n      opacity: 1;\n    }\n  }\n`\n\nexport const Description = styled(Text)`\n  min-width: 0;\n  flex-shrink: 1;\n  ${truncatedText}\n`\n\nexport const BadgeWrapper = styled.span`\n  flex-shrink: 0;\n`\n\nexport const Body = styled(Col)`\n  max-height: 400px;\n  min-height: 200px;\n  overflow: auto;\n  border-top: 1px solid ${({ theme }) => theme.semantic.color.border.neutral400};\n`\n\nexport const ChevronWrapper = styled(Row)`\n  padding-left: ${({ theme }) => theme.core.space.space100};\n  border-left: 2px solid\n    ${({ theme }) => theme.semantic.color.border.neutral400};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/QueryLibraryItem.tsx",
    "content": "import React, { useCallback } from 'react'\n\nimport { RiBadge } from 'uiSrc/components/base/display/badge/RiBadge'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\nimport { CopyButton } from 'uiSrc/components/copy-button'\nimport { EmptyButton, IconButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  ChevronDownIcon,\n  DeleteIcon,\n  PlayFilledIcon,\n  ChevronRightIcon,\n} from 'uiSrc/components/base/icons'\nimport { truncateText } from 'uiSrc/utils'\nimport { CommandView } from 'uiSrc/pages/vector-search/components/command-view'\n\nimport { QueryLibraryItemProps } from './QueryLibraryItem.types'\nimport { QUERY_TYPE_BADGE_MAP } from './QueryLibraryItem.constants'\nimport * as S from './QueryLibraryItem.styles'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const QueryLibraryItem = ({\n  id,\n  name,\n  type,\n  query,\n  description,\n  isOpen = false,\n  onToggleOpen,\n  onRun,\n  onLoad,\n  onDelete,\n  dataTestId = 'query-library-item',\n}: QueryLibraryItemProps) => {\n  const badgeConfig = QUERY_TYPE_BADGE_MAP[type]\n\n  const handleToggle = useCallback(() => {\n    onToggleOpen?.(id)\n  }, [id, onToggleOpen])\n\n  const handleRun = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation()\n      onRun?.(id)\n    },\n    [id, onRun],\n  )\n\n  const handleLoad = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation()\n      onLoad?.(id)\n    },\n    [id, onLoad],\n  )\n\n  const handleDelete = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation()\n      onDelete?.(id)\n    },\n    [id, onDelete],\n  )\n\n  return (\n    <S.Container data-testid={dataTestId}>\n      <S.Header\n        onClick={handleToggle}\n        aria-expanded={isOpen}\n        data-testid={`${dataTestId}-header`}\n        align=\"center\"\n        justify=\"between\"\n        gap=\"m\"\n        grow\n      >\n        <S.HeaderInfo gap=\"m\" align=\"center\">\n          <S.Name color=\"primary\" data-testid={`${dataTestId}-name`}>\n            <RiTooltip position=\"bottom\" content={truncateText(name, 500)}>\n              {name}\n            </RiTooltip>\n            <S.CopyButtonWrapper>\n              <CopyButton\n                copy={name || ''}\n                aria-label=\"Copy query name\"\n                data-testid={`${dataTestId}-copy`}\n                withTooltip={false}\n              />\n            </S.CopyButtonWrapper>\n          </S.Name>\n          {description && (\n            <S.Description\n              color=\"informative\"\n              size=\"s\"\n              data-testid={`${dataTestId}-description`}\n            >\n              <RiTooltip\n                position=\"bottom\"\n                content={truncateText(description, 500)}\n              >\n                {description}\n              </RiTooltip>\n            </S.Description>\n          )}\n          <S.BadgeWrapper>\n            <RiBadge\n              label={badgeConfig.label}\n              variant={badgeConfig.variant}\n              data-testid={`${dataTestId}-type-badge`}\n            />\n          </S.BadgeWrapper>\n        </S.HeaderInfo>\n\n        <Row gap=\"s\" align=\"center\" grow={false}>\n          {onDelete && (\n            <IconButton\n              icon={DeleteIcon}\n              onClick={handleDelete}\n              aria-label=\"Delete query\"\n              data-testid={`${dataTestId}-delete-btn`}\n            />\n          )}\n          {onLoad && (\n            <EmptyButton\n              onClick={handleLoad}\n              aria-label=\"Load query\"\n              data-testid={`${dataTestId}-load-btn`}\n            >\n              Load\n            </EmptyButton>\n          )}\n          {onRun && (\n            <EmptyButton\n              icon={PlayFilledIcon}\n              onClick={handleRun}\n              aria-label=\"Run query\"\n              data-testid={`${dataTestId}-run-btn`}\n            >\n              Run\n            </EmptyButton>\n          )}\n        </Row>\n        <S.ChevronWrapper grow={false}>\n          {isOpen ? (\n            <ChevronDownIcon size=\"M\" />\n          ) : (\n            <ChevronRightIcon size=\"M\" />\n          )}\n        </S.ChevronWrapper>\n      </S.Header>\n\n      {isOpen && (\n        <S.Body data-testid={`${dataTestId}-body`}>\n          <CommandView\n            command={query}\n            dataTestId={`${dataTestId}-command-view`}\n          />\n        </S.Body>\n      )}\n    </S.Container>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/QueryLibraryItem.types.ts",
    "content": "import { BadgeVariants } from 'uiSrc/components/base/display/badge/RiBadge'\n\nexport enum QueryLibraryItemType {\n  Sample = 'sample',\n  Saved = 'saved',\n}\n\nexport interface QueryLibraryItemProps {\n  id: string\n  name: string\n  type: QueryLibraryItemType\n  query: string\n  description?: string\n\n  isOpen?: boolean\n  onToggleOpen?: (id: string) => void\n\n  onRun?: (id: string) => void\n  onLoad?: (id: string) => void\n  onDelete?: (id: string) => void\n\n  dataTestId?: string\n}\n\nexport interface QueryTypeBadgeConfig {\n  label: string\n  variant: BadgeVariants\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-item/index.ts",
    "content": "export { QueryLibraryItem } from './QueryLibraryItem'\nexport { QueryLibraryItemType } from './QueryLibraryItem.types'\nexport type { QueryLibraryItemProps } from './QueryLibraryItem.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/QueryLibraryView.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent, waitFor } from 'uiSrc/utils/test-utils'\nimport { queryLibraryItemFactory } from 'uiSrc/mocks/factories/query-library/queryLibraryItem.factory'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { QueryLibraryView } from './QueryLibraryView'\nimport { QueryLibraryViewProps } from './QueryLibraryView.types'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockItems = queryLibraryItemFactory.buildList(2)\n\nconst mockUseQueryLibrary = {\n  items: mockItems,\n  hasItemsBeforeSearch: true,\n  loading: false,\n  error: null as string | null,\n  search: '',\n  openItemId: null,\n  onSearchChange: jest.fn(),\n  deleteItem: jest.fn(),\n  toggleItemOpen: jest.fn(),\n  getItemById: jest.fn((id: string) => mockItems.find((i) => i.id === id)),\n}\n\njest.mock('./hooks/useQueryLibrary', () => ({\n  useQueryLibrary: () => mockUseQueryLibrary,\n}))\n\njest.mock('uiSrc/components/base/code-editor', () => {\n  const ReactMock = require('react')\n  return {\n    __esModule: true,\n    CodeEditor: (props: any) =>\n      ReactMock.createElement(\n        'div',\n        { 'data-testid': props['data-testid'] },\n        props.value,\n      ),\n  }\n})\n\ndescribe('QueryLibraryView', () => {\n  const defaultProps: QueryLibraryViewProps = {\n    onRun: jest.fn(),\n    onLoad: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<QueryLibraryViewProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<QueryLibraryView {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockUseQueryLibrary.items = mockItems\n    mockUseQueryLibrary.hasItemsBeforeSearch = true\n    mockUseQueryLibrary.loading = false\n    mockUseQueryLibrary.error = null\n    mockUseQueryLibrary.search = ''\n    mockUseQueryLibrary.openItemId = null\n    mockUseQueryLibrary.getItemById.mockImplementation((id: string) =>\n      mockItems.find((i) => i.id === id),\n    )\n  })\n\n  describe('rendering', () => {\n    it('should render the view container', () => {\n      renderComponent()\n\n      const container = screen.getByTestId('query-library-view')\n      expect(container).toBeInTheDocument()\n    })\n\n    it('should render search input when items exist', () => {\n      renderComponent()\n\n      const searchInput = screen.getByTestId('query-library-search')\n      expect(searchInput).toBeInTheDocument()\n    })\n\n    it('should not render search bar when there are no items and no search', () => {\n      mockUseQueryLibrary.items = []\n      mockUseQueryLibrary.hasItemsBeforeSearch = false\n      renderComponent()\n\n      const searchInput = screen.queryByTestId('query-library-search')\n      expect(searchInput).not.toBeInTheDocument()\n    })\n\n    it('should render search bar when search is active even with no items', () => {\n      mockUseQueryLibrary.items = []\n      mockUseQueryLibrary.hasItemsBeforeSearch = false\n      mockUseQueryLibrary.search = 'test'\n      renderComponent()\n\n      const searchInput = screen.getByTestId('query-library-search')\n      expect(searchInput).toBeInTheDocument()\n    })\n\n    it('should render query library items', () => {\n      renderComponent()\n\n      const firstItem = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}`,\n      )\n      expect(firstItem).toBeInTheDocument()\n\n      const secondItem = screen.getByTestId(\n        `query-library-item-${mockItems[1].id}`,\n      )\n      expect(secondItem).toBeInTheDocument()\n    })\n  })\n\n  describe('loading state', () => {\n    it('should show loading state when loading with no items', () => {\n      mockUseQueryLibrary.loading = true\n      mockUseQueryLibrary.items = []\n      renderComponent()\n\n      const loading = screen.getByTestId('query-library-loading')\n      expect(loading).toBeInTheDocument()\n    })\n\n    it('should show items when loading with existing items', () => {\n      mockUseQueryLibrary.loading = true\n      renderComponent()\n\n      const loading = screen.queryByTestId('query-library-loading')\n      expect(loading).not.toBeInTheDocument()\n\n      const item = screen.getByTestId(`query-library-item-${mockItems[0].id}`)\n      expect(item).toBeInTheDocument()\n    })\n  })\n\n  describe('error state', () => {\n    it('should show error message', () => {\n      mockUseQueryLibrary.error = 'Failed to load query library'\n      mockUseQueryLibrary.items = []\n      renderComponent()\n\n      const errorEl = screen.getByTestId('query-library-error')\n      expect(errorEl).toHaveTextContent('Failed to load query library')\n    })\n  })\n\n  describe('empty state', () => {\n    it('should show empty message when no items and no search', () => {\n      mockUseQueryLibrary.items = []\n      renderComponent()\n\n      const emptyState = screen.getByTestId('query-library-empty')\n      expect(emptyState).toHaveTextContent(\n        'No saved queries yet. Create your query in editor and click Save to add it here.',\n      )\n    })\n\n    it('should show search empty message when no items with search', () => {\n      mockUseQueryLibrary.items = []\n      mockUseQueryLibrary.search = 'nonexistent'\n      renderComponent()\n\n      const emptyState = screen.getByTestId('query-library-empty')\n      expect(emptyState).toHaveTextContent('No queries match your search')\n    })\n  })\n\n  describe('actions', () => {\n    it('should call onRun with query text when Run is clicked', () => {\n      renderComponent()\n\n      const runBtn = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}-run-btn`,\n      )\n      fireEvent.click(runBtn)\n\n      expect(defaultProps.onRun).toHaveBeenCalledWith(mockItems[0].query)\n      expect(sendEventTelemetry).toHaveBeenCalledWith(\n        expect.objectContaining({\n          event: TelemetryEvent.SEARCH_QUERY_LIBRARY_RUN,\n        }),\n      )\n    })\n\n    it('should call onLoad and send telemetry when Load is clicked', () => {\n      renderComponent()\n\n      const loadBtn = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}-load-btn`,\n      )\n      fireEvent.click(loadBtn)\n\n      expect(defaultProps.onLoad).toHaveBeenCalled()\n      expect(sendEventTelemetry).toHaveBeenCalledWith(\n        expect.objectContaining({\n          event: TelemetryEvent.SEARCH_QUERY_LIBRARY_LOADED,\n        }),\n      )\n    })\n\n    it('should show delete confirmation modal when Delete is clicked', () => {\n      renderComponent()\n\n      const deleteBtn = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}-delete-btn`,\n      )\n      fireEvent.click(deleteBtn)\n\n      const modalMessage = screen.getByTestId(\n        'query-library-delete-modal-message',\n      )\n      expect(modalMessage).toBeInTheDocument()\n    })\n\n    it('should call deleteItem and send telemetry on successful delete', async () => {\n      mockUseQueryLibrary.deleteItem.mockResolvedValue(true)\n      renderComponent()\n\n      const deleteBtn = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}-delete-btn`,\n      )\n      fireEvent.click(deleteBtn)\n\n      const confirmBtn = screen.getByTestId(\n        'query-library-delete-modal-confirm',\n      )\n      fireEvent.click(confirmBtn)\n\n      await waitFor(() => {\n        expect(mockUseQueryLibrary.deleteItem).toHaveBeenCalledWith(\n          mockItems[0].id,\n        )\n      })\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith(\n        expect.objectContaining({\n          event: TelemetryEvent.SEARCH_QUERY_DELETED,\n        }),\n      )\n    })\n\n    it('should not send telemetry when delete fails', async () => {\n      mockUseQueryLibrary.deleteItem.mockResolvedValue(false)\n      renderComponent()\n\n      const deleteBtn = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}-delete-btn`,\n      )\n      fireEvent.click(deleteBtn)\n\n      const confirmBtn = screen.getByTestId(\n        'query-library-delete-modal-confirm',\n      )\n      fireEvent.click(confirmBtn)\n\n      await waitFor(() => {\n        expect(mockUseQueryLibrary.deleteItem).toHaveBeenCalledWith(\n          mockItems[0].id,\n        )\n      })\n\n      expect(sendEventTelemetry).not.toHaveBeenCalledWith(\n        expect.objectContaining({\n          event: TelemetryEvent.SEARCH_QUERY_DELETED,\n        }),\n      )\n    })\n\n    it('should close delete modal on cancel', () => {\n      renderComponent()\n\n      const deleteBtn = screen.getByTestId(\n        `query-library-item-${mockItems[0].id}-delete-btn`,\n      )\n      fireEvent.click(deleteBtn)\n\n      const modalMessage = screen.getByTestId(\n        'query-library-delete-modal-message',\n      )\n      expect(modalMessage).toBeInTheDocument()\n\n      const cancelBtn = screen.getByTestId('query-library-delete-modal-cancel')\n      fireEvent.click(cancelBtn)\n\n      const dismissedModal = screen.queryByTestId(\n        'query-library-delete-modal-message',\n      )\n      expect(dismissedModal).not.toBeInTheDocument()\n    })\n  })\n\n  describe('search', () => {\n    it('should call onSearchChange when search input changes', () => {\n      renderComponent()\n\n      const searchInput = screen.getByTestId('query-library-search')\n      fireEvent.change(searchInput, { target: { value: 'test' } })\n\n      expect(mockUseQueryLibrary.onSearchChange).toHaveBeenCalledWith('test')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/QueryLibraryView.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Container = styled(Col)`\n  flex: 1;\n  min-height: 0;\n  overflow: hidden;\n`\n\nexport const SearchBar = styled(Row)`\n  padding: ${({ theme }) =>\n    `${theme.core.space.space150} ${theme.core.space.space200}`};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic.color.border.neutral400};\n  flex-grow: 0;\n  flex-shrink: 0;\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral200};\n\n  & > * {\n    flex: 1;\n    min-width: 0;\n  }\n`\n\nexport const ListContainer = styled(Col)`\n  flex: 1;\n  min-height: 0;\n  overflow-y: auto;\n`\n\nexport const EmptyState = styled(Col)`\n  flex: 1;\n  padding: ${({ theme }) => theme.core.space.space500};\n  color: ${({ theme }) => theme.semantic.color.text.neutral700};\n  text-align: center;\n`\n\nexport const LoadingWrapper = styled(Col)`\n  padding: ${({ theme }) => theme.core.space.space500};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/QueryLibraryView.tsx",
    "content": "import React, { useCallback, useState } from 'react'\nimport { useParams } from 'react-router-dom'\n\nimport { SearchInput } from 'uiSrc/components/base/inputs'\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { Text } from 'uiSrc/components/base/text'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { QueryLibraryType } from 'uiSrc/services/query-library/types'\n\nimport { QueryLibraryItem, QueryLibraryItemType } from '../query-library-item'\nimport { DeleteQueryModal } from './components/delete-query-modal'\nimport { useQueryLibrary } from './hooks/useQueryLibrary'\nimport { QueryLibraryViewProps } from './QueryLibraryView.types'\nimport { buildLoadQuery } from './QueryLibraryView.utils'\nimport * as S from './QueryLibraryView.styles'\n\nconst SERVICE_TYPE_TO_UI_TYPE: Record<QueryLibraryType, QueryLibraryItemType> =\n  {\n    [QueryLibraryType.Sample]: QueryLibraryItemType.Sample,\n    [QueryLibraryType.Saved]: QueryLibraryItemType.Saved,\n  }\n\nexport const QueryLibraryView = ({ onRun, onLoad }: QueryLibraryViewProps) => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const {\n    items,\n    hasItemsBeforeSearch,\n    loading,\n    error,\n    search,\n    openItemId,\n    onSearchChange,\n    deleteItem,\n    toggleItemOpen,\n    getItemById,\n  } = useQueryLibrary()\n\n  const [deletingId, setDeletingId] = useState<string | null>(null)\n\n  const handleRun = useCallback(\n    (id: string) => {\n      const item = getItemById(id)\n      if (item) {\n        sendEventTelemetry({\n          event: TelemetryEvent.SEARCH_QUERY_LIBRARY_RUN,\n          eventData: {\n            databaseId: instanceId,\n            query_type: SERVICE_TYPE_TO_UI_TYPE[item.type],\n          },\n        })\n        onRun(item.query)\n      }\n    },\n    [getItemById, onRun, instanceId],\n  )\n\n  const handleLoad = useCallback(\n    (id: string) => {\n      const item = getItemById(id)\n      if (item) {\n        sendEventTelemetry({\n          event: TelemetryEvent.SEARCH_QUERY_LIBRARY_LOADED,\n          eventData: {\n            databaseId: instanceId,\n            query_type: SERVICE_TYPE_TO_UI_TYPE[item.type],\n          },\n        })\n        onLoad(buildLoadQuery(item))\n      }\n    },\n    [getItemById, onLoad, instanceId],\n  )\n\n  const handleDeleteRequest = useCallback((id: string) => {\n    setDeletingId(id)\n  }, [])\n\n  const handleDeleteConfirm = useCallback(async () => {\n    if (!deletingId) return\n    const deleted = await deleteItem(deletingId)\n    if (deleted) {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_QUERY_DELETED,\n        eventData: { databaseId: instanceId },\n      })\n    }\n    setDeletingId(null)\n  }, [deletingId, deleteItem, instanceId])\n\n  const handleDeleteCancel = useCallback(() => {\n    setDeletingId(null)\n  }, [])\n\n  const isInitialLoading = loading === true && items.length === 0\n  const isEmpty = loading === false && !error && items.length === 0\n  const showSearchBar = hasItemsBeforeSearch || !!search\n\n  return (\n    <S.Container data-testid=\"query-library-view\">\n      {showSearchBar && (\n        <S.SearchBar>\n          <SearchInput\n            placeholder=\"Search query\"\n            value={search}\n            onChange={onSearchChange}\n            data-testid=\"query-library-search\"\n          />\n        </S.SearchBar>\n      )}\n\n      <S.ListContainer>\n        {isInitialLoading && (\n          <S.LoadingWrapper data-testid=\"query-library-loading\">\n            <LoadingContent lines={3} className=\"fluid\" />\n          </S.LoadingWrapper>\n        )}\n\n        {error && (\n          <S.EmptyState align=\"center\" justify=\"center\">\n            <Text color=\"danger\" data-testid=\"query-library-error\">\n              {error}\n            </Text>\n          </S.EmptyState>\n        )}\n\n        {isEmpty && (\n          <S.EmptyState align=\"center\" justify=\"center\">\n            <Text data-testid=\"query-library-empty\">\n              {search\n                ? 'No queries match your search'\n                : 'No saved queries yet. Create your query in editor and click Save to add it here.'}\n            </Text>\n          </S.EmptyState>\n        )}\n\n        {items.map((item) => (\n          <QueryLibraryItem\n            key={item.id}\n            id={item.id}\n            name={item.name}\n            type={SERVICE_TYPE_TO_UI_TYPE[item.type]}\n            query={item.query}\n            description={item.description}\n            isOpen={openItemId === item.id}\n            onToggleOpen={toggleItemOpen}\n            onRun={handleRun}\n            onLoad={handleLoad}\n            onDelete={handleDeleteRequest}\n            dataTestId={`query-library-item-${item.id}`}\n          />\n        ))}\n      </S.ListContainer>\n\n      {deletingId && (\n        <DeleteQueryModal\n          onConfirm={handleDeleteConfirm}\n          onCancel={handleDeleteCancel}\n        />\n      )}\n    </S.Container>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/QueryLibraryView.types.ts",
    "content": "export interface QueryLibraryViewProps {\n  onRun: (queryText: string) => void\n  onLoad: (queryText: string) => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/QueryLibraryView.utils.spec.ts",
    "content": "import { queryLibraryItemFactory } from 'uiSrc/mocks/factories/query-library/queryLibraryItem.factory'\nimport { QueryLibraryType } from 'uiSrc/services/query-library/types'\n\nimport { buildLoadQuery } from './QueryLibraryView.utils'\n\ndescribe('buildLoadQuery', () => {\n  it('should prepend description as comment for sample query with description', () => {\n    const item = queryLibraryItemFactory.build({\n      type: QueryLibraryType.Sample,\n    })\n\n    expect(buildLoadQuery(item)).toBe(`// ${item.description}\\n${item.query}`)\n  })\n\n  it('should return query as-is for sample query without description', () => {\n    const item = queryLibraryItemFactory.build({\n      type: QueryLibraryType.Sample,\n      description: undefined,\n    })\n\n    expect(buildLoadQuery(item)).toBe(item.query)\n  })\n\n  it('should return query as-is for sample query with empty description', () => {\n    const item = queryLibraryItemFactory.build({\n      type: QueryLibraryType.Sample,\n      description: '',\n    })\n\n    expect(buildLoadQuery(item)).toBe(item.query)\n  })\n\n  it('should return query as-is for saved query', () => {\n    const item = queryLibraryItemFactory.build({\n      type: QueryLibraryType.Saved,\n      description: undefined,\n    })\n\n    expect(buildLoadQuery(item)).toBe(item.query)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/QueryLibraryView.utils.ts",
    "content": "import {\n  QueryLibraryItem,\n  QueryLibraryType,\n} from 'uiSrc/services/query-library/types'\n\nexport const buildLoadQuery = (item: QueryLibraryItem): string => {\n  if (item.type === QueryLibraryType.Sample && item.description) {\n    return `// ${item.description}\\n${item.query}`\n  }\n\n  return item.query\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/components/delete-query-modal/DeleteQueryModal.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { DeleteQueryModal } from './DeleteQueryModal'\nimport { DeleteQueryModalProps } from './DeleteQueryModal.types'\n\ndescribe('DeleteQueryModal', () => {\n  const defaultProps: DeleteQueryModalProps = {\n    onConfirm: jest.fn(),\n    onCancel: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<DeleteQueryModalProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<DeleteQueryModal {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render modal with title, confirmation message and action buttons', () => {\n    renderComponent()\n\n    const dialog = screen.getByRole('dialog', { name: 'Delete query' })\n    expect(dialog).toBeInTheDocument()\n\n    const message = screen.getByTestId('query-library-delete-modal-message')\n    expect(message).toBeInTheDocument()\n\n    const question = screen.getByText(\n      'Are you sure you want to delete this query?',\n    )\n    expect(question).toBeInTheDocument()\n\n    const disclaimer = screen.getByText(\n      \"This action will remove the saved query, but won't affect your index or data.\",\n    )\n    expect(disclaimer).toBeInTheDocument()\n\n    const cancelBtn = screen.getByTestId('query-library-delete-modal-cancel')\n    expect(cancelBtn).toHaveTextContent('Keep query')\n\n    const confirmBtn = screen.getByTestId('query-library-delete-modal-confirm')\n    expect(confirmBtn).toHaveTextContent('Delete query')\n  })\n\n  it('should call onConfirm when Delete query button is clicked', () => {\n    renderComponent()\n\n    const confirmBtn = screen.getByTestId('query-library-delete-modal-confirm')\n    fireEvent.click(confirmBtn)\n\n    expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when Keep query button is clicked', () => {\n    renderComponent()\n\n    const cancelBtn = screen.getByTestId('query-library-delete-modal-cancel')\n    fireEvent.click(cancelBtn)\n\n    expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when close icon is clicked', () => {\n    renderComponent()\n\n    const closeBtn = screen.getByTestId('query-library-delete-modal-close')\n    fireEvent.click(closeBtn)\n\n    expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/components/delete-query-modal/DeleteQueryModal.tsx",
    "content": "import React from 'react'\n\nimport { DeleteConfirmationModal } from 'uiSrc/pages/vector-search/components/delete-confirmation-modal'\n\nimport { DeleteQueryModalProps } from './DeleteQueryModal.types'\n\nexport const DeleteQueryModal = ({\n  onConfirm,\n  onCancel,\n}: DeleteQueryModalProps) => (\n  <DeleteConfirmationModal\n    isOpen\n    title=\"Delete query\"\n    question=\"Are you sure you want to delete this query?\"\n    message=\"This action will remove the saved query, but won't affect your index or data.\"\n    cancelLabel=\"Keep query\"\n    confirmLabel=\"Delete query\"\n    onConfirm={onConfirm}\n    onCancel={onCancel}\n    testId=\"query-library-delete-modal\"\n  />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/components/delete-query-modal/DeleteQueryModal.types.ts",
    "content": "export interface DeleteQueryModalProps {\n  onConfirm: () => void\n  onCancel: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/components/delete-query-modal/index.ts",
    "content": "export { DeleteQueryModal } from './DeleteQueryModal'\nexport type { DeleteQueryModalProps } from './DeleteQueryModal.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/hooks/useQueryLibrary.spec.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { act, renderHook, waitFor, getMswURL } from 'uiSrc/utils/test-utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl } from 'uiSrc/utils'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { QUERY_LIBRARY_ITEMS_MOCK } from 'uiSrc/mocks/handlers/browser/queryLibraryHandlers'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\n\nimport { useQueryLibrary } from './useQueryLibrary'\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst routerDom = require('react-router-dom')\n\nconst mockIndexName = 'test-index'\n\ndescribe('useQueryLibrary hook', () => {\n  const originalUseParams = routerDom.useParams\n\n  beforeEach(() => {\n    routerDom.useParams = () => ({\n      instanceId: INSTANCE_ID_MOCK,\n      indexName: mockIndexName,\n    })\n  })\n\n  afterAll(() => {\n    routerDom.useParams = originalUseParams\n  })\n\n  it('should load items on mount', async () => {\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.items).toEqual(QUERY_LIBRARY_ITEMS_MOCK)\n  })\n\n  it('should be loading after mount', () => {\n    const { result } = renderHook(() => useQueryLibrary())\n\n    expect(result.current.loading).toBe(true)\n  })\n\n  it('should handle empty result from service', async () => {\n    mswServer.use(\n      http.get(\n        getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.QUERY_LIBRARY)),\n        async () => HttpResponse.json([], { status: 200 }),\n      ),\n    )\n\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.items).toEqual([])\n  })\n\n  it('should handle service errors and set error state', async () => {\n    jest\n      .spyOn(QueryLibraryService.prototype, 'getList')\n      .mockRejectedValueOnce(new Error('Unexpected failure'))\n\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.error).toBe('Failed to load query library')\n    expect(result.current.items).toEqual([])\n  })\n\n  it('should delete item and return true on success', async () => {\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    const idToDelete = QUERY_LIBRARY_ITEMS_MOCK[0].id\n    let deleted: boolean | undefined\n\n    await act(async () => {\n      deleted = await result.current.deleteItem(idToDelete)\n    })\n\n    expect(deleted).toBe(true)\n    expect(result.current.items).toHaveLength(1)\n    expect(result.current.items[0].id).toBe(QUERY_LIBRARY_ITEMS_MOCK[1].id)\n  })\n\n  it('should return false when delete fails', async () => {\n    jest\n      .spyOn(QueryLibraryService.prototype, 'delete')\n      .mockRejectedValueOnce(new Error('Delete failed'))\n\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    const idToDelete = QUERY_LIBRARY_ITEMS_MOCK[0].id\n    let deleted: boolean | undefined\n\n    await act(async () => {\n      deleted = await result.current.deleteItem(idToDelete)\n    })\n\n    expect(deleted).toBe(false)\n    expect(result.current.items).toHaveLength(QUERY_LIBRARY_ITEMS_MOCK.length)\n  })\n\n  it('should toggle item open state', async () => {\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    act(() => {\n      result.current.toggleItemOpen(QUERY_LIBRARY_ITEMS_MOCK[0].id)\n    })\n\n    expect(result.current.openItemId).toBe(QUERY_LIBRARY_ITEMS_MOCK[0].id)\n\n    act(() => {\n      result.current.toggleItemOpen(QUERY_LIBRARY_ITEMS_MOCK[0].id)\n    })\n\n    expect(result.current.openItemId).toBeNull()\n  })\n\n  it('should get item by id', async () => {\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.getItemById(QUERY_LIBRARY_ITEMS_MOCK[0].id)).toEqual(\n      QUERY_LIBRARY_ITEMS_MOCK[0],\n    )\n    expect(result.current.getItemById('nonexistent')).toBeUndefined()\n  })\n\n  it('should update search state when onSearchChange is called', async () => {\n    const { result } = renderHook(() => useQueryLibrary())\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    act(() => {\n      result.current.onSearchChange('test query')\n    })\n\n    expect(result.current.search).toBe('test query')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/hooks/useQueryLibrary.ts",
    "content": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { debounce } from 'lodash'\n\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\nimport { QueryLibraryItem } from 'uiSrc/services/query-library/types'\nimport { queryLibraryNotifications } from 'uiSrc/pages/vector-search/constants'\n\nconst SEARCH_DEBOUNCE_MS = 300\n\nexport const useQueryLibrary = () => {\n  const dispatch = useDispatch()\n  const { instanceId: databaseId, indexName: rawIndexName } = useParams<{\n    instanceId: string\n    indexName?: string\n  }>()\n\n  const indexName = rawIndexName ? decodeURIComponent(rawIndexName) : ''\n\n  const [items, setItems] = useState<QueryLibraryItem[]>([])\n  // Whether the library contains any items at all, ignoring the active search filter.\n  // Updated only on unfiltered fetches and deletions, so it stays stable\n  // during debounced search transitions and avoids UI flicker.\n  const [hasItemsBeforeSearch, setHasItems] = useState(false)\n  const [loading, setLoading] = useState<boolean>()\n  const [error, setError] = useState<string | null>(null)\n  const [search, setSearch] = useState('')\n  const [openItemId, setOpenItemId] = useState<string | null>(null)\n\n  const serviceRef = useRef(new QueryLibraryService())\n\n  const fetchItems = useCallback(\n    async (searchTerm?: string) => {\n      if (!databaseId || !indexName) return\n\n      setLoading(true)\n      setError(null)\n\n      try {\n        const data = await serviceRef.current.getList(databaseId, {\n          indexName,\n          search: searchTerm || undefined,\n        })\n        setItems(data)\n        if (!searchTerm) {\n          setHasItems(data.length > 0)\n        }\n      } catch {\n        setItems([])\n        setError('Failed to load query library')\n      } finally {\n        setLoading(false)\n      }\n    },\n    [databaseId, indexName],\n  )\n\n  const debouncedFetch = useMemo(\n    () => debounce((term: string) => fetchItems(term), SEARCH_DEBOUNCE_MS),\n    [fetchItems],\n  )\n\n  useEffect(\n    () => () => {\n      debouncedFetch.cancel()\n    },\n    [debouncedFetch],\n  )\n\n  useEffect(() => {\n    fetchItems()\n  }, [fetchItems])\n\n  const handleSearchChange = useCallback(\n    (value: string) => {\n      setSearch(value)\n      debouncedFetch(value)\n    },\n    [debouncedFetch],\n  )\n\n  const deleteItem = useCallback(\n    async (id: string): Promise<boolean> => {\n      if (!databaseId) {\n        return false\n      }\n\n      try {\n        await serviceRef.current.delete(databaseId, id)\n        setItems((prev) => {\n          const remaining = prev.filter((item) => item.id !== id)\n          if (remaining.length === 0 && !search) {\n            setHasItems(false)\n          }\n          return remaining\n        })\n        setOpenItemId((prev) => (prev === id ? null : prev))\n        dispatch(\n          addMessageNotification(queryLibraryNotifications.queryDeleted()),\n        )\n        return true\n      } catch {\n        return false\n      }\n    },\n    [databaseId, dispatch, search],\n  )\n\n  const toggleItemOpen = useCallback((id: string) => {\n    setOpenItemId((prev) => (prev === id ? null : id))\n  }, [])\n\n  const getItemById = useCallback(\n    (id: string) => items.find((item) => item.id === id),\n    [items],\n  )\n\n  return {\n    items,\n    hasItemsBeforeSearch,\n    loading,\n    error,\n    search,\n    openItemId,\n    onSearchChange: handleSearchChange,\n    deleteItem,\n    toggleItemOpen,\n    getItemById,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/query-library-view/index.ts",
    "content": "export { QueryLibraryView } from './QueryLibraryView'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/rqe-not-available/RqeNotAvailable.spec.tsx",
    "content": "import React from 'react'\nimport { configureStore, combineReducers } from '@reduxjs/toolkit'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport cloudReducer from 'uiSrc/slices/instances/cloud'\nimport instancesReducer from 'uiSrc/slices/instances/instances'\nimport appOauthReducer from 'uiSrc/slices/oauth/cloud'\nimport appFeaturesReducer from 'uiSrc/slices/app/features'\nimport { RqeNotAvailable } from './RqeNotAvailable'\n\nconst createTestStore = () =>\n  configureStore({\n    reducer: combineReducers({\n      connections: combineReducers({\n        cloud: cloudReducer,\n        instances: instancesReducer,\n      }),\n      oauth: combineReducers({ cloud: appOauthReducer }),\n      app: combineReducers({ features: appFeaturesReducer }),\n    }),\n    preloadedState: {\n      app: {\n        features: {\n          featureFlags: {\n            features: {\n              [FeatureFlags.cloudSso]: { flag: true },\n              [FeatureFlags.cloudAds]: { flag: true },\n              [FeatureFlags.envDependent]: { flag: true },\n            },\n          },\n        },\n      },\n    },\n    middleware: (getDefaultMiddleware) =>\n      getDefaultMiddleware({ serializableCheck: false }),\n  })\n\ndescribe('RqeNotAvailable', () => {\n  it('should render with RQE not available content', () => {\n    render(<RqeNotAvailable />, { store: createTestStore() })\n\n    expect(screen.getByTestId('rqe-not-available')).toBeInTheDocument()\n    expect(screen.getByTestId('rqe-not-available-title')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/rqe-not-available/RqeNotAvailable.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { RqeNotAvailable } from './RqeNotAvailable'\n\nconst meta: Meta<typeof RqeNotAvailable> = {\n  component: RqeNotAvailable,\n  parameters: {\n    layout: 'fullscreen',\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof RqeNotAvailable>\n\nexport const Default: Story = {}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/rqe-not-available/RqeNotAvailable.tsx",
    "content": "import React from 'react'\n\nimport {\n  SearchPageFallback,\n  RQE_NOT_AVAILABLE_CONTENT,\n} from '../search-page-fallback'\n\nexport const RqeNotAvailable = () => (\n  <SearchPageFallback content={RQE_NOT_AVAILABLE_CONTENT} />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/rqe-not-available/index.ts",
    "content": "export { RqeNotAvailable } from './RqeNotAvailable'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/save-query-modal/SaveQueryModal.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { SaveQueryModal } from './SaveQueryModal'\nimport { SaveQueryModalProps } from './SaveQueryModal.types'\n\ndescribe('SaveQueryModal', () => {\n  const defaultProps: SaveQueryModalProps = {\n    isOpen: true,\n    isSaving: false,\n    onSave: jest.fn(),\n    onClose: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<SaveQueryModalProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<SaveQueryModal {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should not render when isOpen is false', () => {\n    renderComponent({ isOpen: false })\n\n    expect(\n      screen.queryByTestId('save-query-modal-body'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render modal with title, input, and buttons', () => {\n    renderComponent()\n\n    const dialog = screen.getByRole('dialog', { name: 'Save query' })\n    expect(dialog).toBeInTheDocument()\n\n    expect(\n      screen.getByTestId('save-query-modal-name-input'),\n    ).toBeInTheDocument()\n    expect(screen.getByTestId('save-query-modal-cancel')).toBeInTheDocument()\n    expect(screen.getByTestId('save-query-modal-confirm')).toBeInTheDocument()\n  })\n\n  it('should disable save button when name is empty', () => {\n    renderComponent()\n\n    const saveBtn = screen.getByTestId('save-query-modal-confirm')\n    expect(saveBtn).toBeDisabled()\n  })\n\n  it('should enable save button when name is entered', () => {\n    renderComponent()\n\n    const input = screen.getByTestId('save-query-modal-name-input')\n    fireEvent.change(input, { target: { value: faker.lorem.words(2) } })\n\n    const saveBtn = screen.getByTestId('save-query-modal-confirm')\n    expect(saveBtn).not.toBeDisabled()\n  })\n\n  it('should disable save button when name is whitespace only', () => {\n    renderComponent()\n\n    const input = screen.getByTestId('save-query-modal-name-input')\n    fireEvent.change(input, { target: { value: '   ' } })\n\n    const saveBtn = screen.getByTestId('save-query-modal-confirm')\n    expect(saveBtn).toBeDisabled()\n  })\n\n  it('should call onSave with trimmed name when save is clicked', () => {\n    const queryName = faker.lorem.words(3)\n    renderComponent()\n\n    const input = screen.getByTestId('save-query-modal-name-input')\n    fireEvent.change(input, { target: { value: `  ${queryName}  ` } })\n\n    const saveBtn = screen.getByTestId('save-query-modal-confirm')\n    fireEvent.click(saveBtn)\n\n    expect(defaultProps.onSave).toHaveBeenCalledWith(queryName)\n  })\n\n  it('should call onClose when cancel button is clicked', () => {\n    renderComponent()\n\n    const cancelBtn = screen.getByTestId('save-query-modal-cancel')\n    fireEvent.click(cancelBtn)\n\n    expect(defaultProps.onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onClose when close icon is clicked', () => {\n    renderComponent()\n\n    const closeBtn = screen.getByTestId('save-query-modal-close')\n    fireEvent.click(closeBtn)\n\n    expect(defaultProps.onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should disable save button when isSaving is true', () => {\n    renderComponent({ isSaving: true })\n\n    const input = screen.getByTestId('save-query-modal-name-input')\n    fireEvent.change(input, { target: { value: faker.lorem.words(2) } })\n\n    const saveBtn = screen.getByTestId('save-query-modal-confirm')\n    expect(saveBtn).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/save-query-modal/SaveQueryModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display/modal'\n\nexport const ModalContent = styled(Modal.Content.Compose)`\n  width: 600px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/save-query-modal/SaveQueryModal.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\n\nimport { Modal } from 'uiSrc/components/base/display'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Text } from 'uiSrc/components/base/text'\nimport TextInput from 'uiSrc/components/base/inputs/TextInput'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\n\nimport { SaveQueryModalProps } from './SaveQueryModal.types'\nimport * as S from './SaveQueryModal.styles'\n\nconst TEST_ID = 'save-query-modal'\n\nexport const SaveQueryModal = ({\n  isOpen,\n  isSaving,\n  onSave,\n  onClose,\n}: SaveQueryModalProps) => {\n  const [name, setName] = useState('')\n\n  useEffect(() => {\n    if (isOpen) {\n      setName('')\n    }\n  }, [isOpen])\n\n  const isSaveDisabled = !name.trim() || isSaving\n\n  const handleSubmit = useCallback(async () => {\n    const trimmed = name.trim()\n    if (!trimmed) return\n    await onSave(trimmed)\n  }, [name, onSave])\n\n  if (!isOpen) return null\n\n  return (\n    <Modal.Compose open={isOpen}>\n      <S.ModalContent persistent onCancel={onClose}>\n        <Modal.Content.Close\n          icon={CancelIcon}\n          onClick={onClose}\n          data-testid={`${TEST_ID}-close`}\n        />\n\n        <Modal.Content.Header.Compose>\n          <Modal.Content.Header.Title>Save query</Modal.Content.Header.Title>\n        </Modal.Content.Header.Compose>\n\n        <Col gap=\"l\" data-testid={`${TEST_ID}-body`}>\n          <Text color=\"secondary\">\n            Name your query to add it to your saved queries list for quick\n            reuse.\n          </Text>\n\n          <TextInput\n            value={name}\n            onChange={setName}\n            placeholder=\"Enter command name\"\n            name=\"queryName\"\n            autoFocus\n            data-testid={`${TEST_ID}-name-input`}\n          />\n          <Spacer size=\"s\" />\n        </Col>\n\n        <Row justify=\"end\" gap=\"m\">\n          <SecondaryButton\n            size=\"large\"\n            onClick={onClose}\n            data-testid={`${TEST_ID}-cancel`}\n          >\n            Cancel\n          </SecondaryButton>\n          <PrimaryButton\n            size=\"large\"\n            loading={isSaving}\n            onClick={handleSubmit}\n            disabled={isSaveDisabled}\n            data-testid={`${TEST_ID}-confirm`}\n          >\n            Save query\n          </PrimaryButton>\n        </Row>\n      </S.ModalContent>\n    </Modal.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/save-query-modal/SaveQueryModal.types.ts",
    "content": "export interface SaveQueryModalProps {\n  isOpen: boolean\n  isSaving: boolean\n  onSave: (name: string) => Promise<void>\n  onClose: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/save-query-modal/index.ts",
    "content": "export { SaveQueryModal } from './SaveQueryModal'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/search-page-fallback/SearchPageFallback.spec.tsx",
    "content": "import React from 'react'\nimport { configureStore, combineReducers } from '@reduxjs/toolkit'\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport { OAuthSsoDialog } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport cloudReducer from 'uiSrc/slices/instances/cloud'\nimport instancesReducer from 'uiSrc/slices/instances/instances'\nimport appOauthReducer from 'uiSrc/slices/oauth/cloud'\nimport appFeaturesReducer from 'uiSrc/slices/app/features'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { SearchPageFallback } from './SearchPageFallback'\nimport { SearchPageFallbackContent } from './SearchPageFallback.types'\n\nconst createTestStore = (featureFlagsEnabled = true) =>\n  configureStore({\n    reducer: combineReducers({\n      connections: combineReducers({\n        cloud: cloudReducer,\n        instances: instancesReducer,\n      }),\n      oauth: combineReducers({ cloud: appOauthReducer }),\n      app: combineReducers({ features: appFeaturesReducer }),\n    }),\n    preloadedState: {\n      app: {\n        features: {\n          featureFlags: {\n            features: {\n              [FeatureFlags.cloudSso]: { flag: featureFlagsEnabled },\n              [FeatureFlags.cloudAds]: { flag: featureFlagsEnabled },\n              [FeatureFlags.envDependent]: { flag: featureFlagsEnabled },\n            },\n          },\n        },\n      },\n    },\n    middleware: (getDefaultMiddleware) =>\n      getDefaultMiddleware({ serializableCheck: false }),\n  })\n\nconst CONTENT_WITH_FEATURES: SearchPageFallbackContent = {\n  testId: 'test-fallback',\n  title: 'Test Title',\n  subtitle: 'Test Subtitle',\n  features: ['Feature A', 'Feature B'],\n  description: 'Test description',\n  ctaText: 'Test CTA',\n  oauthSource: OAuthSocialSource.BrowserSearch,\n}\n\nconst CONTENT_WITHOUT_FEATURES: SearchPageFallbackContent = {\n  testId: 'minimal-fallback',\n  title: 'Minimal Title',\n  description: 'Minimal description',\n  ctaText: 'Minimal CTA',\n  oauthSource: OAuthSocialSource.BrowserSearch,\n}\n\nconst renderFallback = (\n  content: SearchPageFallbackContent,\n  featureFlagsEnabled = true,\n) => {\n  const store = createTestStore(featureFlagsEnabled)\n  return render(\n    <>\n      <SearchPageFallback content={content} />\n      <OAuthSsoDialog />\n    </>,\n    { store },\n  )\n}\n\ndescribe('SearchPageFallback', () => {\n  it('should render all content sections', () => {\n    renderFallback(CONTENT_WITH_FEATURES)\n\n    expect(screen.getByTestId('test-fallback')).toBeInTheDocument()\n    expect(screen.getByTestId('test-fallback-title')).toHaveTextContent(\n      'Test Title',\n    )\n    expect(screen.getByTestId('test-fallback-description')).toBeInTheDocument()\n    expect(screen.getByTestId('test-fallback-cta-text')).toBeInTheDocument()\n    expect(screen.getByTestId('test-fallback-illustration')).toBeInTheDocument()\n  })\n\n  it('should render feature list when features are provided', () => {\n    renderFallback(CONTENT_WITH_FEATURES)\n\n    expect(screen.getByText('Feature A')).toBeInTheDocument()\n    expect(screen.getByText('Feature B')).toBeInTheDocument()\n  })\n\n  it('should not render subtitle or feature list when not provided', () => {\n    renderFallback(CONTENT_WITHOUT_FEATURES)\n\n    expect(screen.getByTestId('minimal-fallback')).toBeInTheDocument()\n    expect(\n      screen.queryByTestId('minimal-fallback-feature-list'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render CTA buttons when feature flags are enabled', () => {\n    renderFallback(CONTENT_WITH_FEATURES)\n\n    expect(\n      screen.getByTestId('test-fallback-get-started-button'),\n    ).toBeInTheDocument()\n    expect(\n      screen.getByTestId('test-fallback-learn-more-link'),\n    ).toBeInTheDocument()\n  })\n\n  it('should not render CTA wrapper when feature flags are disabled', () => {\n    renderFallback(CONTENT_WITH_FEATURES, false)\n\n    expect(\n      screen.queryByTestId('test-fallback-cta-wrapper'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should open OAuth modal when clicking get started button', async () => {\n    renderFallback(CONTENT_WITH_FEATURES)\n\n    fireEvent.click(screen.getByTestId('test-fallback-get-started-button'))\n\n    await waitFor(() => {\n      expect(screen.getByTestId('social-oauth-dialog')).toBeInTheDocument()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/search-page-fallback/SearchPageFallback.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Card } from 'uiSrc/components/base/layout'\nimport { Row, Col } from 'uiSrc/components/base/layout/flex'\nimport { Group, Item } from 'uiSrc/components/base/layout/list'\nimport { Text } from 'uiSrc/components/base/text'\n\nexport const StyledCard = styled(Card)`\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n`\n\nexport const ScrollArea = styled(Col)`\n  position: relative;\n  overflow-y: auto;\n  padding: clamp(2rem, 8vh, 9rem) clamp(7rem, 8vw, 14rem);\n`\n\nexport const StyledCardBody = styled(Row).attrs({\n  align: 'center',\n  gap: 'xxl',\n})`\n  flex: 0 0 auto;\n  max-width: 1000px;\n  width: 100%;\n  height: fit-content;\n`\n\nexport const ContentSection = styled(Col).attrs({\n  gap: 'xl',\n})`\n  flex: 1;\n  max-width: 550px;\n`\n\nexport const CtaText = styled(Text).attrs({\n  color: 'primary',\n})`\n  max-width: 350px;\n`\n\nexport const DescriptionText = styled(Text).attrs({\n  color: 'primary',\n})`\n  max-width: 450px;\n`\n\nexport const IllustrationSection = styled(Row).attrs({\n  align: 'end',\n  justify: 'center',\n  grow: false,\n})`\n  height: 100%;\n  flex-shrink: 0;\n\n  svg {\n    width: 400px;\n    height: auto;\n  }\n`\n\nexport const FeatureList = styled(Group).attrs({\n  gap: 'm',\n})`\n  list-style: none;\n  padding: 0;\n  margin: 0;\n`\n\nexport const FeatureListItem = styled(Item)`\n  & > button,\n  & > span {\n    gap: ${({ theme }) => theme.core.space.space100};\n    justify-content: start;\n  }\n`\n\nexport const ButtonWrapper = styled(Col).attrs({\n  gap: 'xl',\n  align: 'start',\n})``\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/search-page-fallback/SearchPageFallback.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport RqeIllustration from 'uiSrc/assets/img/vector-search/rqe-not-available.svg?react'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { EXTERNAL_LINKS, UTM_CAMPAINGS } from 'uiSrc/constants/links'\nimport { OAuthSocialAction } from 'uiSrc/slices/interfaces'\nimport { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { Link } from 'uiSrc/components/base/link/Link'\n\nimport { SearchPageFallbackContent } from './SearchPageFallback.types'\nimport * as S from './SearchPageFallback.styles'\n\ninterface SearchPageFallbackProps {\n  content: SearchPageFallbackContent\n}\n\nexport const SearchPageFallback = ({ content }: SearchPageFallbackProps) => {\n  const { [FeatureFlags.envDependent]: envDependentFeature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n\n  const utmCampaign = UTM_CAMPAINGS[content.oauthSource]\n\n  return (\n    <S.StyledCard data-testid={content.testId}>\n      <S.ScrollArea>\n        <S.StyledCardBody>\n          <S.ContentSection>\n            <Title\n              color=\"primary\"\n              size=\"XL\"\n              data-testid={`${content.testId}-title`}\n            >\n              {content.title}\n            </Title>\n\n            {content.subtitle && (\n              <ColorText color=\"primary\" variant=\"semiBold\">\n                {content.subtitle}\n              </ColorText>\n            )}\n\n            {content.features && (\n              <S.FeatureList data-testid={`${content.testId}-feature-list`}>\n                {content.features.map((feature) => (\n                  <S.FeatureListItem\n                    key={feature}\n                    iconType=\"ToastCheckIcon\"\n                    color=\"primary\"\n                    label={<ColorText color=\"primary\">{feature}</ColorText>}\n                  />\n                ))}\n              </S.FeatureList>\n            )}\n\n            <S.DescriptionText\n              color=\"primary\"\n              data-testid={`${content.testId}-description`}\n            >\n              {content.description}\n            </S.DescriptionText>\n\n            <S.CtaText data-testid={`${content.testId}-cta-text`}>\n              {content.ctaText}\n            </S.CtaText>\n\n            {envDependentFeature?.flag && (\n              <S.ButtonWrapper data-testid={`${content.testId}-cta-wrapper`}>\n                <FeatureFlagComponent name={FeatureFlags.cloudAds}>\n                  <OAuthSsoHandlerDialog>\n                    {(ssoCloudHandlerClick) => (\n                      <Link\n                        variant=\"inline\"\n                        target=\"_blank\"\n                        href={getUtmExternalLink(EXTERNAL_LINKS.tryFree, {\n                          campaign: utmCampaign,\n                        })}\n                        onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                          ssoCloudHandlerClick(e as React.MouseEvent, {\n                            source: content.oauthSource,\n                            action: OAuthSocialAction.Create,\n                          })\n                        }}\n                        data-testid={`${content.testId}-get-started-button`}\n                      >\n                        <PrimaryButton size=\"m\">\n                          Get started for free\n                        </PrimaryButton>\n                      </Link>\n                    )}\n                  </OAuthSsoHandlerDialog>\n                </FeatureFlagComponent>\n\n                <Link\n                  variant=\"inline\"\n                  target=\"_blank\"\n                  href={getUtmExternalLink(EXTERNAL_LINKS.redisQueryEngine, {\n                    campaign: utmCampaign,\n                  })}\n                  data-testid={`${content.testId}-learn-more-link`}\n                >\n                  Learn more\n                </Link>\n              </S.ButtonWrapper>\n            )}\n          </S.ContentSection>\n\n          <S.IllustrationSection data-testid={`${content.testId}-illustration`}>\n            <RqeIllustration />\n          </S.IllustrationSection>\n        </S.StyledCardBody>\n      </S.ScrollArea>\n    </S.StyledCard>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/search-page-fallback/SearchPageFallback.types.ts",
    "content": "import { OAuthSocialSource } from 'uiSrc/slices/interfaces'\n\nexport interface SearchPageFallbackContent {\n  testId: string\n  title: string\n  subtitle?: string\n  features?: string[]\n  description: string\n  ctaText: string\n  oauthSource: OAuthSocialSource\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/search-page-fallback/constants.ts",
    "content": "import { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { SearchPageFallbackContent } from './SearchPageFallback.types'\n\nexport const RQE_NOT_AVAILABLE_CONTENT: SearchPageFallbackContent = {\n  testId: 'rqe-not-available',\n  title: 'Redis Query Engine is not available for this database',\n  subtitle: 'Redis Query Engine allows to:',\n  features: ['Query', 'Secondary index', 'Full-text search'],\n  description:\n    'These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, ' +\n    'geo filtering and vector similarity semantic search on top of text queries.',\n  ctaText:\n    'Use your free trial all-in-one Redis Cloud database to start exploring these capabilities',\n  oauthSource: OAuthSocialSource.BrowserSearch,\n}\n\nexport const VERSION_NOT_SUPPORTED_CONTENT: SearchPageFallbackContent = {\n  testId: 'version-not-supported',\n  title: 'Redis Query Engine 2.0+ required',\n  description:\n    'This page requires Redis Query Engine 2.0 or later (included with Redis 6+). ' +\n    'Older versions of the query engine are not compatible with the commands used here.',\n  ctaText:\n    'Create a free Redis Cloud database to start exploring these capabilities.',\n  oauthSource: OAuthSocialSource.BrowserFiltering,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/search-page-fallback/index.ts",
    "content": "export { SearchPageFallback } from './SearchPageFallback'\nexport type { SearchPageFallbackContent } from './SearchPageFallback.types'\nexport {\n  RQE_NOT_AVAILABLE_CONTENT,\n  VERSION_NOT_SUPPORTED_CONTENT,\n} from './constants'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/select-key-onboarding-popover/SelectKeyOnboardingPopover.spec.tsx",
    "content": "import React from 'react'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport BrowserStorageItem from 'uiSrc/constants/storage'\nimport { localStorageService } from 'uiSrc/services'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\n\nimport { SelectKeyOnboardingPopover } from './SelectKeyOnboardingPopover'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/browser/keys', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/keys'),\n  selectedKeyDataSelector: jest.fn().mockReturnValue(null),\n}))\n\ndescribe('SelectKeyOnboardingPopover', () => {\n  const renderComponent = () =>\n    render(\n      <SelectKeyOnboardingPopover>\n        <button type=\"button\" data-testid=\"trigger\">\n          Trigger\n        </button>\n      </SelectKeyOnboardingPopover>,\n    )\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    ;(selectedKeyDataSelector as jest.Mock).mockReturnValue(null)\n  })\n\n  it('should show popover on first visit', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(null)\n\n    renderComponent()\n\n    expect(\n      screen.getByTestId('select-key-onboarding-content'),\n    ).toBeInTheDocument()\n    expect(screen.getByText('Select a key to get started')).toBeInTheDocument()\n  })\n\n  it('should not show popover when already seen', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(true)\n\n    renderComponent()\n\n    expect(\n      screen.queryByTestId('select-key-onboarding-content'),\n    ).not.toBeInTheDocument()\n    expect(screen.getByTestId('trigger')).toBeInTheDocument()\n  })\n\n  it('should dismiss and persist on Got it click', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(null)\n\n    renderComponent()\n\n    fireEvent.click(screen.getByTestId('select-key-onboarding-dismiss'))\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.vectorSearchSelectKeyOnboarding,\n      true,\n    )\n    expect(\n      screen.queryByTestId('select-key-onboarding-content'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should dismiss and persist on X close click', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(null)\n\n    renderComponent()\n\n    fireEvent.click(screen.getByTestId('select-key-onboarding-close'))\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.vectorSearchSelectKeyOnboarding,\n      true,\n    )\n    expect(\n      screen.queryByTestId('select-key-onboarding-content'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should auto-dismiss when a key is selected', () => {\n    ;(localStorageService.get as jest.Mock).mockReturnValue(null)\n    ;(selectedKeyDataSelector as jest.Mock).mockReturnValue({\n      name: 'user:1',\n    })\n\n    renderComponent()\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.vectorSearchSelectKeyOnboarding,\n      true,\n    )\n    expect(\n      screen.queryByTestId('select-key-onboarding-content'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/select-key-onboarding-popover/SelectKeyOnboardingPopover.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const Content = styled(Col)`\n  max-width: 340px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/select-key-onboarding-popover/SelectKeyOnboardingPopover.tsx",
    "content": "import React, { useCallback, useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Button, IconButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelSlimIcon } from 'uiSrc/components/base/icons'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport BrowserStorageItem from 'uiSrc/constants/storage'\nimport { localStorageService } from 'uiSrc/services'\nimport { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys'\n\nimport * as S from './SelectKeyOnboardingPopover.styles'\n\nexport interface SelectKeyOnboardingPopoverProps {\n  children: React.ReactNode\n}\n\ninterface PopoverContentProps {\n  children: React.ReactNode\n  onDismiss: () => void\n}\n\nconst PopoverContent = ({ children, onDismiss }: PopoverContentProps) => {\n  const selectedKey = useSelector(selectedKeyDataSelector)\n\n  useEffect(() => {\n    if (selectedKey?.name) {\n      onDismiss()\n    }\n  }, [selectedKey?.name, onDismiss])\n\n  return (\n    <RiPopover\n      isOpen\n      anchorPosition=\"rightCenter\"\n      data-testid=\"select-key-onboarding-popover\"\n      trigger={children}\n    >\n      <S.Content gap=\"l\" data-testid=\"select-key-onboarding-content\">\n        <Col gap=\"s\">\n          <Row justify=\"end\">\n            <IconButton\n              icon={CancelSlimIcon}\n              onClick={onDismiss}\n              size=\"S\"\n              aria-label=\"close-onboarding\"\n              data-testid=\"select-key-onboarding-close\"\n            />\n          </Row>\n          <Text size=\"L\" variant=\"semiBold\" color=\"primary\">\n            Select a key to get started\n          </Text>\n        </Col>\n        <Col gap=\"m\">\n          <Text size=\"m\" color=\"secondary\">\n            We&apos;ll use the selected key to generate a suggested indexing\n            schema. Redis will index all keys with the same prefix, not just\n            this single key.\n          </Text>\n          <Text size=\"m\" color=\"secondary\">\n            Indexing available for Hash and JSON data structures.\n          </Text>\n        </Col>\n        <Row justify=\"end\">\n          <Button\n            size=\"small\"\n            onClick={onDismiss}\n            data-testid=\"select-key-onboarding-dismiss\"\n          >\n            Got it\n          </Button>\n        </Row>\n      </S.Content>\n    </RiPopover>\n  )\n}\n\nexport const SelectKeyOnboardingPopover = ({\n  children,\n}: SelectKeyOnboardingPopoverProps) => {\n  const [isOpen, setIsOpen] = useState(\n    () =>\n      localStorageService.get(\n        BrowserStorageItem.vectorSearchSelectKeyOnboarding,\n      ) !== true,\n  )\n\n  const handleDismiss = useCallback(() => {\n    localStorageService.set(\n      BrowserStorageItem.vectorSearchSelectKeyOnboarding,\n      true,\n    )\n    setIsOpen(false)\n  }, [])\n\n  if (!isOpen) {\n    return <>{children}</>\n  }\n\n  return <PopoverContent onDismiss={handleDismiss}>{children}</PopoverContent>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/select-key-onboarding-popover/index.ts",
    "content": "export { SelectKeyOnboardingPopover } from './SelectKeyOnboardingPopover'\nexport type { SelectKeyOnboardingPopoverProps } from './SelectKeyOnboardingPopover'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/upgrade-redis-banner/UpgradeRedisBanner.spec.tsx",
    "content": "import React from 'react'\nimport { configureStore, combineReducers } from '@reduxjs/toolkit'\nimport { fireEvent, render, screen, waitFor } from 'uiSrc/utils/test-utils'\nimport { OAuthSsoDialog } from 'uiSrc/components'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport cloudReducer from 'uiSrc/slices/instances/cloud'\nimport appOauthReducer from 'uiSrc/slices/oauth/cloud'\nimport appFeaturesReducer from 'uiSrc/slices/app/features'\n\nimport { UpgradeRedisBanner } from './UpgradeRedisBanner'\n\nconst createTestStore = (featureFlagsEnabled = true) =>\n  configureStore({\n    reducer: combineReducers({\n      connections: combineReducers({ cloud: cloudReducer }),\n      oauth: combineReducers({ cloud: appOauthReducer }),\n      app: combineReducers({ features: appFeaturesReducer }),\n    }),\n    preloadedState: {\n      app: {\n        features: {\n          featureFlags: {\n            features: {\n              [FeatureFlags.cloudSso]: { flag: featureFlagsEnabled },\n              [FeatureFlags.cloudAds]: { flag: featureFlagsEnabled },\n            },\n          },\n        },\n      },\n    },\n    middleware: (getDefaultMiddleware) =>\n      getDefaultMiddleware({ serializableCheck: false }),\n  })\n\nconst renderComponent = (featureFlagsEnabled = true) => {\n  const store = createTestStore(featureFlagsEnabled)\n  return render(\n    <>\n      <UpgradeRedisBanner />\n      <OAuthSsoDialog />\n    </>,\n    { store },\n  )\n}\n\ndescribe('UpgradeRedisBanner', () => {\n  it('should render correctly', () => {\n    renderComponent()\n\n    const banner = screen.getByTestId('upgrade-redis-banner')\n    expect(banner).toBeInTheDocument()\n  })\n\n  it('should display the upgrade message', () => {\n    renderComponent()\n\n    expect(screen.getByText(/Upgrade to Redis 7\\.2\\+/)).toBeInTheDocument()\n  })\n\n  it('should open \"Cloud Login\" modal when clicking on \"Free Redis Cloud DB\" button', async () => {\n    renderComponent()\n\n    const button = screen.getByRole('button', { name: /Free Redis Cloud DB/i })\n    expect(button).toBeInTheDocument()\n\n    fireEvent.click(button)\n\n    await waitFor(() => {\n      const modal = screen.getByTestId('social-oauth-dialog')\n      expect(modal).toBeInTheDocument()\n    })\n  })\n\n  it('should not render \"Free Redis Cloud DB\" button if feature flags are disabled', () => {\n    renderComponent(false)\n\n    const button = screen.queryByRole('button', {\n      name: /Free Redis Cloud DB/i,\n    })\n    expect(button).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/upgrade-redis-banner/UpgradeRedisBanner.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { OAuthSsoHandlerDialog } from 'uiSrc/components'\nimport { CallOut } from 'uiSrc/components/base/display/call-out/CallOut'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  OAuthSocialAction,\n  OAuthSocialSource,\n} from 'uiSrc/slices/interfaces/cloud'\n\nexport const UpgradeRedisBanner = () => {\n  const {\n    [FeatureFlags.cloudSso]: featureFlagCloudSsl,\n    [FeatureFlags.cloudAds]: featureFlagCloudAds,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n\n  const isCloudSsoEnabled =\n    featureFlagCloudSsl?.flag && featureFlagCloudAds?.flag\n\n  return (\n    <OAuthSsoHandlerDialog>\n      {(ssoCloudHandlerClick) => (\n        <CallOut\n          variant=\"notice\"\n          {...(isCloudSsoEnabled && {\n            actions: {\n              primary: {\n                label: 'Free Redis Cloud DB',\n                onClick: () =>\n                  // @ts-ignore: We don't have the event arg here\n                  ssoCloudHandlerClick(null, {\n                    source: OAuthSocialSource.BrowserFiltering,\n                    action: OAuthSocialAction.Create,\n                  }),\n              },\n            },\n          })}\n          data-testid=\"upgrade-redis-banner\"\n        >\n          Upgrade to Redis 7.2+ to unlock fast, real-time semantic AI search\n          with vector search\n        </CallOut>\n      )}\n    </OAuthSsoHandlerDialog>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/upgrade-redis-banner/index.ts",
    "content": "export { UpgradeRedisBanner } from './UpgradeRedisBanner'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/version-not-supported/VersionNotSupported.spec.tsx",
    "content": "import React from 'react'\nimport { configureStore, combineReducers } from '@reduxjs/toolkit'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport cloudReducer from 'uiSrc/slices/instances/cloud'\nimport instancesReducer from 'uiSrc/slices/instances/instances'\nimport appOauthReducer from 'uiSrc/slices/oauth/cloud'\nimport appFeaturesReducer from 'uiSrc/slices/app/features'\nimport { VersionNotSupported } from './VersionNotSupported'\n\nconst createTestStore = () =>\n  configureStore({\n    reducer: combineReducers({\n      connections: combineReducers({\n        cloud: cloudReducer,\n        instances: instancesReducer,\n      }),\n      oauth: combineReducers({ cloud: appOauthReducer }),\n      app: combineReducers({ features: appFeaturesReducer }),\n    }),\n    preloadedState: {\n      app: {\n        features: {\n          featureFlags: {\n            features: {\n              [FeatureFlags.cloudSso]: { flag: true },\n              [FeatureFlags.cloudAds]: { flag: true },\n              [FeatureFlags.envDependent]: { flag: true },\n            },\n          },\n        },\n      },\n    },\n    middleware: (getDefaultMiddleware) =>\n      getDefaultMiddleware({ serializableCheck: false }),\n  })\n\ndescribe('VersionNotSupported', () => {\n  it('should render with version not supported content', () => {\n    render(<VersionNotSupported />, { store: createTestStore() })\n\n    expect(screen.getByTestId('version-not-supported')).toBeInTheDocument()\n    expect(\n      screen.getByTestId('version-not-supported-title'),\n    ).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/version-not-supported/VersionNotSupported.tsx",
    "content": "import React from 'react'\n\nimport {\n  SearchPageFallback,\n  VERSION_NOT_SUPPORTED_CONTENT,\n} from '../search-page-fallback'\n\nexport const VersionNotSupported = () => (\n  <SearchPageFallback content={VERSION_NOT_SUPPORTED_CONTENT} />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/version-not-supported/index.ts",
    "content": "export { VersionNotSupported } from './VersionNotSupported'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/WelcomeScreen.constants.ts",
    "content": "import type { Feature } from './WelcomeScreen.types'\n\nexport const FEATURES: Feature[] = [\n  {\n    icon: 'VectorSearchIcon',\n    title: 'Full-text search',\n    description:\n      'Find and filter your data instantly using powerful keyword and field-based queries.',\n  },\n  {\n    icon: 'WorkbenchIcon',\n    title: 'Vector search',\n    description:\n      'Retrieve results by meaning, not just words. Ideal for AI, semantic, and recommendation apps.',\n  },\n  {\n    icon: 'MindmapIcon',\n    title: 'Hybrid search',\n    description:\n      'Combine vector and keyword search for higher accuracy and more relevant results.',\n  },\n  {\n    icon: 'RocketIcon',\n    title: 'High performance, low effort',\n    description:\n      'Built-in quantization and compression deliver blazing speed and efficiency at any scale.',\n  },\n]\n\nexport const TITLE = 'Search your data at in-memory speed'\nexport const SUBTITLE =\n  'Discover how Redis enables full-text and vector search. Fast, simple, and production-ready.'\n\nexport const TRY_SAMPLE_DATA_LABEL = 'Try with sample data'\nexport const USE_MY_DATABASE_LABEL = 'Use data from my database'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/WelcomeScreen.spec.tsx",
    "content": "import React from 'react'\nimport { act } from '@testing-library/react'\nimport {\n  fireEvent,\n  render,\n  screen,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport { WelcomeScreen } from './WelcomeScreen'\nimport type { WelcomeScreenProps } from './WelcomeScreen.types'\nimport { TITLE, SUBTITLE, FEATURES } from './WelcomeScreen.constants'\n\nconst defaultProps: WelcomeScreenProps = {\n  onTrySampleDataClick: jest.fn(),\n  onUseMyDatabaseClick: jest.fn(),\n}\n\nconst renderComponent = (props: Partial<WelcomeScreenProps> = {}) =>\n  render(<WelcomeScreen {...defaultProps} {...props} />)\n\ndescribe('WelcomeScreen', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render all sections', () => {\n    renderComponent()\n\n    const welcomeScreen = screen.getByTestId('welcome-screen')\n    expect(welcomeScreen).toBeInTheDocument()\n\n    const title = screen.getByTestId('welcome-screen--title')\n    expect(title).toHaveTextContent(TITLE)\n\n    const subtitle = screen.getByTestId('welcome-screen--subtitle')\n    expect(subtitle).toHaveTextContent(SUBTITLE)\n\n    const features = screen.getByTestId('welcome-screen--features')\n    expect(features).toBeInTheDocument()\n\n    FEATURES.forEach((feature) => {\n      const featureTitle = screen.getByText(feature.title)\n      expect(featureTitle).toBeInTheDocument()\n    })\n\n    const trySampleDataBtn = screen.getByTestId(\n      'welcome-screen--try-sample-data-btn',\n    )\n    expect(trySampleDataBtn).toBeInTheDocument()\n\n    const useMyDatabaseBtn = screen.getByTestId(\n      'welcome-screen--use-my-database-btn',\n    )\n    expect(useMyDatabaseBtn).toBeInTheDocument()\n\n    const background = screen.getByTestId('welcome-screen--background')\n    expect(background).toBeInTheDocument()\n  })\n\n  it('should call onTrySampleDataClick when primary button is clicked', () => {\n    const onTrySampleDataClick = jest.fn()\n    renderComponent({ onTrySampleDataClick })\n\n    const trySampleDataBtn = screen.getByTestId(\n      'welcome-screen--try-sample-data-btn',\n    )\n    fireEvent.click(trySampleDataBtn)\n\n    expect(onTrySampleDataClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onUseMyDatabaseClick when secondary button is clicked', () => {\n    const onUseMyDatabaseClick = jest.fn()\n    renderComponent({ onUseMyDatabaseClick })\n\n    const useMyDatabaseBtn = screen.getByTestId(\n      'welcome-screen--use-my-database-btn',\n    )\n    fireEvent.click(useMyDatabaseBtn)\n\n    expect(onUseMyDatabaseClick).toHaveBeenCalledTimes(1)\n  })\n\n  it('should disable secondary button when useMyDatabaseDisabled is provided', async () => {\n    const onUseMyDatabaseClick = jest.fn()\n    renderComponent({\n      onUseMyDatabaseClick,\n      useMyDatabaseDisabled: { tooltip: 'Feature disabled' },\n    })\n\n    const button = screen.getByTestId('welcome-screen--use-my-database-btn')\n    expect(button).toBeDisabled()\n\n    fireEvent.click(button)\n    expect(onUseMyDatabaseClick).not.toHaveBeenCalled()\n\n    await act(async () => {\n      fireEvent.focus(button)\n    })\n    await waitForRiTooltipVisible()\n\n    const tooltipText = screen.getAllByText('Feature disabled')[0]\n    expect(tooltipText).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/WelcomeScreen.stories.tsx",
    "content": "import type { Meta, StoryObj } from '@storybook/react-vite'\n\nimport { WelcomeScreen } from './WelcomeScreen'\n\nconst meta: Meta<typeof WelcomeScreen> = {\n  component: WelcomeScreen,\n  tags: ['autodocs'],\n  parameters: {\n    layout: 'fullscreen',\n    docs: {\n      description: {\n        component:\n          'Welcome screen for Vector Search feature. Displays feature cards and action buttons.',\n      },\n    },\n  },\n}\n\nexport default meta\n\ntype Story = StoryObj<typeof meta>\n\nexport const Default: Story = {\n  args: {\n    // eslint-disable-next-line no-alert\n    onTrySampleDataClick: () => alert('Try with sample data clicked!'),\n    // eslint-disable-next-line no-alert\n    onUseMyDatabaseClick: () => alert('Use data from my database clicked!'),\n  },\n}\n\nexport const UseMyDatabaseDisabled: Story = {\n  name: '\"Use data from my database\" disabled',\n  args: {\n    // eslint-disable-next-line no-alert\n    onTrySampleDataClick: () => alert('Try with sample data clicked!'),\n    useMyDatabaseDisabled: {\n      tooltip: \"You don't have any data in your database yet\",\n    },\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/WelcomeScreen.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, FlexGroup } from 'uiSrc/components/base/layout/flex'\nimport Eyeglass from 'uiSrc/assets/img/vector-search/eyeglass.svg'\nimport EyeglassDark from 'uiSrc/assets/img/vector-search/eyeglass-dark.svg'\n\nexport const Container = styled(Col)`\n  position: relative;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n`\n\nexport const ScrollArea = styled(Col)`\n  position: relative;\n  z-index: 1;\n  overflow-y: auto;\n  padding: clamp(2rem, 8vh, 9rem) clamp(7rem, 8vw, 14rem);\n`\n\nexport const Content = styled(Col)`\n  position: relative;\n  z-index: 1;\n  flex-grow: 0;\n`\n\nexport const FeaturesContainer = styled(FlexGroup)`\n  max-width: 704px; /* Two columns */\n`\n\nexport const FeatureItem = styled(Col)`\n  max-width: 320px;\n`\n\nexport const BackgroundImage = styled.div`\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  width: 584px;\n  height: 567px;\n  pointer-events: none;\n  z-index: 0;\n  background-image: url(${({ theme }) =>\n    theme.name === 'dark' ? EyeglassDark : Eyeglass});\n  background-repeat: no-repeat;\n  background-position: bottom right;\n  background-size: contain;\n\n  @media (max-width: 900px), ((max-height: 700px) and (max-width: 1200px)) {\n    opacity: 0.1;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/WelcomeScreen.tsx",
    "content": "import React from 'react'\n\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { Text, Title } from 'uiSrc/components/base/text'\nimport { RiIcon, AllIconsType } from 'uiSrc/components/base/icons'\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\n\nimport {\n  FEATURES,\n  TITLE,\n  SUBTITLE,\n  TRY_SAMPLE_DATA_LABEL,\n  USE_MY_DATABASE_LABEL,\n} from './WelcomeScreen.constants'\nimport type { WelcomeScreenProps } from './WelcomeScreen.types'\nimport * as S from './WelcomeScreen.styles'\n\nexport const WelcomeScreen = ({\n  onTrySampleDataClick,\n  onUseMyDatabaseClick,\n  useMyDatabaseDisabled,\n}: WelcomeScreenProps) => {\n  const isUseMyDatabaseDisabled = !!useMyDatabaseDisabled\n  const useMyDatabaseTooltip = useMyDatabaseDisabled?.tooltip\n\n  return (\n    <S.Container data-testid=\"welcome-screen\">\n      <S.ScrollArea>\n        <S.Content>\n          <Col gap=\"m\">\n            <Title\n              size=\"XL\"\n              color=\"primary\"\n              data-testid=\"welcome-screen--title\"\n            >\n              {TITLE}\n            </Title>\n            <Text\n              size=\"L\"\n              color=\"primary\"\n              data-testid=\"welcome-screen--subtitle\"\n            >\n              {SUBTITLE}\n            </Text>\n          </Col>\n\n          <Spacer size=\"7.2rem\" />\n\n          <S.FeaturesContainer\n            wrap\n            gap=\"xl\"\n            data-testid=\"welcome-screen--features\"\n          >\n            {FEATURES.map((feature) => (\n              <S.FeatureItem\n                key={feature.title}\n                gap=\"xs\"\n                data-testid={`welcome-screen--feature-${feature.icon}`}\n              >\n                <RiIcon\n                  type={feature.icon as AllIconsType}\n                  size=\"xl\"\n                  color=\"neutral800\"\n                />\n                <Spacer size=\"space050\" />\n                <Text size=\"M\" variant=\"semiBold\" color=\"primary\">\n                  {feature.title}\n                </Text>\n                <Text size=\"S\" color=\"secondary\">\n                  {feature.description}\n                </Text>\n              </S.FeatureItem>\n            ))}\n          </S.FeaturesContainer>\n\n          <Spacer size=\"11.2rem\" />\n\n          <Row gap=\"l\" data-testid=\"welcome-screen--actions\">\n            <PrimaryButton\n              size=\"l\"\n              onClick={onTrySampleDataClick}\n              data-testid=\"welcome-screen--try-sample-data-btn\"\n            >\n              {TRY_SAMPLE_DATA_LABEL}\n            </PrimaryButton>\n\n            <RiTooltip\n              content={isUseMyDatabaseDisabled ? useMyDatabaseTooltip : null}\n              anchorClassName=\"euiToolTip__btn-disabled\"\n            >\n              <SecondaryButton\n                filled\n                size=\"l\"\n                onClick={onUseMyDatabaseClick}\n                disabled={isUseMyDatabaseDisabled}\n                data-testid=\"welcome-screen--use-my-database-btn\"\n              >\n                {USE_MY_DATABASE_LABEL}\n              </SecondaryButton>\n            </RiTooltip>\n          </Row>\n        </S.Content>\n      </S.ScrollArea>\n\n      <S.BackgroundImage data-testid=\"welcome-screen--background\" />\n    </S.Container>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/WelcomeScreen.types.ts",
    "content": "export interface WelcomeScreenProps {\n  /**\n   * Callback when \"Try with sample data\" button is clicked.\n   */\n  onTrySampleDataClick?: () => void\n\n  /**\n   * Callback when \"Use data from my database\" button is clicked.\n   */\n  onUseMyDatabaseClick?: () => void\n\n  /**\n   * Disable \"Use data from my database\" button and show tooltip.\n   * Tooltip text is required when button is disabled.\n   */\n  useMyDatabaseDisabled?: {\n    tooltip: string\n  }\n}\n\nexport interface Feature {\n  icon: string\n  title: string\n  description: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/components/welcome-screen/index.ts",
    "content": "export { WelcomeScreen } from './WelcomeScreen'\nexport type { WelcomeScreenProps, Feature } from './WelcomeScreen.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/index.ts",
    "content": "export { SAMPLE_DATASETS, BIKES_DATASET, MOVIES_DATASET } from './sample-data'\nexport type { SampleDatasetConfig } from './sample-data'\nexport {\n  createIndexNotifications,\n  queryLibraryNotifications,\n} from './notifications'\nexport { KEY_TYPE_MAP, REVERSE_KEY_TYPE_MAP } from './key-type-maps'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/key-type-maps.ts",
    "content": "import { KeyTypes } from 'uiSrc/constants'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nexport const KEY_TYPE_MAP: Partial<Record<KeyTypes, RedisearchIndexKeyType>> = {\n  [KeyTypes.Hash]: RedisearchIndexKeyType.HASH,\n  [KeyTypes.ReJSON]: RedisearchIndexKeyType.JSON,\n}\n\nexport const REVERSE_KEY_TYPE_MAP: Record<RedisearchIndexKeyType, KeyTypes> = {\n  [RedisearchIndexKeyType.HASH]: KeyTypes.Hash,\n  [RedisearchIndexKeyType.JSON]: KeyTypes.ReJSON,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/notifications.ts",
    "content": "import {\n  RiToastType,\n  ToastVariant,\n} from 'uiSrc/components/base/display/toast/RiToast'\n\ninterface NotificationMessage {\n  title: string\n  message: string\n  variant?: ToastVariant\n  showCloseButton?: boolean\n  actions?: RiToastType['actions']\n}\n\n/**\n * Toast notifications for the Vector Search index creation flow.\n */\nexport const createIndexNotifications = {\n  /** Shown after a new index is successfully created from sample data. */\n  sampleDataCreated: (): NotificationMessage => ({\n    title: 'Your sample data is now searchable.',\n    message:\n      'Start building queries or explore sample ones under Query library.',\n    showCloseButton: false,\n    actions: {},\n  }),\n\n  /**\n   * Shown when the index already exists for the chosen sample dataset.\n   * Variant: notice – the data is usable but nothing new was created.\n   */\n  sampleDataAlreadyExists: (): NotificationMessage => ({\n    title: 'Your sample data is already searchable using an existing index.',\n    message:\n      'You can start building new queries or explore existing ones in the Query Library.',\n    variant: 'notice' as ToastVariant,\n    showCloseButton: false,\n    actions: {},\n  }),\n\n  /** Shown when the index creation request fails. */\n  createFailed: (details?: string): NotificationMessage => ({\n    title: 'Failed to create index',\n    message:\n      details ||\n      'An error occurred while creating the index. Please try again.',\n    variant: 'danger' as ToastVariant,\n  }),\n\n  // TODO: Use when creating an index from existing database keys (not sample data).\n  /** Shown after a new index is successfully created from database data. */\n  indexCreated: (): NotificationMessage => ({\n    title: 'Index created successfully.',\n    message: 'Your data is now searchable. You can start running queries.',\n    showCloseButton: false,\n    actions: {},\n  }),\n}\n\nexport const queryLibraryNotifications = {\n  querySaved: (onGoToLibrary?: VoidFunction): NotificationMessage => ({\n    title: 'Query saved to your library.',\n    message: 'You can find it anytime in the Query Library.',\n    showCloseButton: false,\n    actions: {\n      primary: {\n        label: 'Go to Query Library',\n        onClick: onGoToLibrary ?? (() => {}),\n        closes: true,\n      },\n    },\n  }),\n\n  saveFailed: (): NotificationMessage => ({\n    title: 'Failed to save query',\n    message: 'An error occurred while saving the query. Please try again.',\n    variant: 'error' as ToastVariant,\n  }),\n\n  queryDeleted: (): NotificationMessage => ({\n    title: 'Query has been deleted.',\n    message: '',\n  }),\n\n  cleanupFailed: (): NotificationMessage => ({\n    title: 'Failed to clean up query library',\n    message:\n      'An error occurred while removing saved queries for the deleted index.',\n    variant: 'error' as ToastVariant,\n  }),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/sample-data/bikes.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { SampleDatasetConfig } from './types'\n\nexport const BIKES_DATASET: SampleDatasetConfig = {\n  displayName: 'E-commerce discovery',\n  indexName: 'idx:bikes_vss',\n  indexPrefix: 'bikes:',\n  collectionName: 'bikes',\n  fields: [\n    { id: 'model', name: 'model', value: 'Varuna', type: FieldTypes.TEXT },\n    { id: 'brand', name: 'brand', value: 'Eva', type: FieldTypes.TEXT },\n    { id: 'price', name: 'price', value: 1398, type: FieldTypes.NUMERIC },\n    { id: 'type', name: 'type', value: 'Road bikes', type: FieldTypes.TAG },\n    {\n      id: 'material',\n      name: 'material',\n      value: 'aluminium',\n      type: FieldTypes.TAG,\n    },\n    { id: 'weight', name: 'weight', value: 7.5, type: FieldTypes.NUMERIC },\n    {\n      id: 'description_embeddings',\n      name: 'description_embeddings',\n      value: 'FLAT, FLOAT32, 768, L2',\n      type: FieldTypes.VECTOR,\n    },\n  ],\n  sampleQueries: [\n    {\n      name: 'Basic semantic search',\n      description:\n        'Performs a simple K-nearest neighbors (KNN) vector search to find the 3 bikes most semantically similar to \"Comfortable commuter bike.\" Returns the similarity score along with brand, type, and description fields.',\n      query:\n        'FT.SEARCH idx:bikes_vss ' +\n        '\"*=>[KNN 3 @description_embeddings $my_blob AS score ]\" ' +\n        'RETURN 4 score brand type description ' +\n        'PARAMS 2 my_blob \"\\\\xecNN<\\\\xec\\\\xc78=`\\\\xbd\\\\x87\\\\xbc\\\\xd0\\\\xad\\\\xdc<\\\\xb9\\\\x8fn\\\\xbd\\\\xc81Q<\\\\xcc\\\\x0c\\\\x03=\\\\x11\\\\x9a\\\\x96\\\\xbd\\\\xf0\\\\x15\\\\x9c<k\\\\x9e\\\\x04=7\\\\xde(\\\\xb9\\\\xdev\\\\x9c9\\\\xd2C\\\\x95=\\\\x01\\\\x12\\\\x149Z\\\\xaf\\\\x91=\\\\x95\\\\x80\\\\x98\\\\xbc\\\\xf6\\\\xbf\\\\xd7\\\\xbap6\\\\xd3;n\\\\x03i\\\\xbc\\\\x86\\\\xfcM\\\\xbd\\\\x9ccx<\\\\xbd\\\\x12\\\\x97\\\\xbd\\\\x9aH\\\\xa2\\\\xbb\\\\xbf9\\\\xd8\\\\xbc\\\\xe5\\\\xa2\\\\xf6\\\\xbbR\\\\xf7\\\\xd7\\\\xbc\\\\xed@^<\\\\x16\\\\xce%\\\\xbckCo<S\\\\xdd\\\\xaa<\\\\xd2\\\\xa6\\\\xe7\\\\xbbC\\\\xf4U\\\\xbb7]\\\\xc3\\\\xbc\\\\xa0\\\\x93/\\\\xbd\\\\xfc\\\\x8fj<+\\\\xe2X\\\\xbd6\\\\xad\\\\x8b<\\\\x85\\\\xfd1\\\\xbc\\\\x9d\\\\x14\\\\xe7<\\\\xcb\\\\xa9\\\\xe7<[\\\\x17:=\\\\xdc\\\\xd2\\\\x97<\\\\xcc\\\\x86\\\\xff<ss\\\\xe5<B\\\\x00\\\\xd1\\\\xbc\\\\xf6\\\\xddH\\\\xbc!\\\\x0c\\\\x14<\\\\xef\\\\x91\\\\x09=\\\\xda\\\\x9f\\\\xa5\\\\xba\\\\xdb\\\\xc0\\\\xe4<\\\\x89\\\\x0e\\\\xbd;\\\\xda\\\\xcep\\\\xbdL\\\\xcd\\\\x9d\\\\xbd\\\\x17AZ<\\\\x1cf\\\\xea;\\\\xd5\\\\xf1\\\\xe1\\\\xbc>\\\\xc8d;\\\\x87f:\\\\xbc<\\\\x0e\\\\xb5<\\\\x9a\\\\x8eL=\\\\xa1\\\\x90\\\\xcb<y, =\\\\x1f\\\\x08\\\\xbf\\\\xbb\\\\xd1\\\\xaa\\\\xab\\\\xbc\\\\xd6\\\\xe1B=QW\\\\xf0<\\\\x1e\\\\x1e ;>cJ=wu\\\\x0d=@W\\\\xa5:\\\\x96a\\\\xcc<\\\\x0a.\\\\xdf\\\\xbaI\\\\xad\\\\x0b\\\\xbc\\\\x04\\\\x1f\\\\x8c\\\\xbdHh\\\\xc8\\\\xbd\\\\xfd\\\\xd9#\\\\xba65\\\\x90\\\\xba2\\\\x90\\\\xc7;\\\\xf9;X\\\\xbd\\\\xb19\\\\x1e<\\\\x8c\\\\xc8\\\\x02\\\\xbd\\\\x19\\\\x17\\\\xa4\\\\xbdIV\\\\xbe\\\\xbc\\\\x1bR\\\\x01=l\\\\xa1\\\\xc7\\\\xbc;* =(\\\\x08a<2\\\\xa1s\\\\xbc\\\\xd1\\\\xa5\\\\xbe<\\\\x84e\\\\xbb\\\\xbc\\\\xf1\\\\xe2\\\\xc8<U\\\\xf6\\\\xad<\\\\x13\\\\xdb@\\\\xbd\\\\x84~G\\\\xbd\\\\x97\\\\xaa\\\\xbf<\\\\x85\\\\x04\\\\xc4\\\\xbc9\\\\xe6e<\\\\x9b\\\\xda\\\\xf8<\\\\xbf\\\\x00\\\\xdc<\\\\xd0Q@<\\\\x16\\\\x08\\\\xef<\\\\xf2\\\\x9e\\\\x12=\\\\xae\\\\xe2\\\\xab\\\\xbc\\\\xc1\\\\x9d\\\\xe5\\\\xbb;\\\\xb8\\\\xba;\\\\x86\\\\x12h=\\\\x84\\\\x11\\\\xe4<\\\\xb2\\\\xce\\\\xb0<!\\\\xa2n\\\\xbdK\\\\xf9\\\\xd4<\\\\xf3\\\\xed\\\\x1e=\\\\xa8c\\\\x19<\\\\xa2`\\\\x00=\\\\x023\\\\x8c\\\\xbb\\\\xc7.\\\\x94\\\\xbd?\\\\x11\\\\x95=\\\\xb4\\\\x9f\\\\xd3<[\\\\x9f\\\\x95<3\\\\x18\\\\x9f<a6i\\\\xbd\\\\x01i\\\\xff\\\\xbbe\\\\xc2\\\\xd6\\\\xbbC\\\\x95W\\\\xbd\\\\x0fNx\\\\xbb\\\\x03\\\\x0d&=j+3\\\\xbc\\\\xf2\\\\xfb\\\\x9a<\\\\xc21G<C:\\\\xf3\\\\xbc8m\\\\xd3\\\\xbc`\\\\xee\\\\x95;}\\\\x7fO\\\\xbd\\\\xcda\\\\xca<\\\\x09\\\\x909<\\\\x8d\\\\xaa\\\\x00\\\\xbdG\\\\xb4\\\\xd3<\\\\xcc\\\\xcey\\\\xbc\\\\xa0\\\\x8f\\\\xaa\\\\xbd\\\\x08\\\\x9a\\\\xa3<=n\\\\x80\\\\xbdm\\\\xf3\\\\x8d<T\\\\xccM\\\\xbc\\\\xf2\\\\xfa\\\\xf6\\\\xba\\\\xaaL8;{\\\\x0a)\\\\xbdC\\\\xbc\\\\x03\\\\xbd\\\\xc9\\\\xe22;gn\\\\xb9<\\\\x8e\\\\xdc2\\\\xbd\\\\x92\\\\xdcI\\\\xbc\\\\x12\\\\x0d\\\\xe7\\\\xbbW\\\\xcf\\\\x82=n\\\\xf3\\\\x08\\\\xbc@.\\\\x1a=\\\\xa2\\\\xb9\\\\xeb\\\\xbc\\\\xca\\\\xdb\\\\xf7;\\\\x22\\\\xac\\\\xaa<\\\\xa57\\\\xda<\\\\xfe5\\\\x90<r|\\\\xa0<\\\\x8d\\\\x80>=\\\\xc7\\\\xd0I\\\\xbd_\\\\x9ft\\\\xbc\\\\xc6J\\\\xe7<\\\\xc8X\\\\x84=\\\\xa4(\\\\x08=\\\\xf8\\\\x98\\\\x08\\\\xbd\\\\x08\\\\xf1\\\\x07<\\\\xd8\\\\xce\\\\xda\\\\xbca\\\\xee3\\\\xbc\\\\xbd1\\\\xea;\\\\xd4i\\\\x96=\\\\x99\\\\xfa\\\\x8d\\\\xbcI\\\\x1e\\\\x19\\\\xbd\\\\xd7S\\\\x81\\\\xbb\\\\x16\\\\xfd=<\\\\x00n1\\\\xbd\\\\xc2\\\\xa6\\\\xd3;\\\\x1e\\\\x12\\\\x11<\\\\x10X\\\\x86;\\\\x87\\\\xfd\\\\xb5<w^\\\\xbd\\\\xbc~;\\\\x1f<\\\\xd8\\\\xbd\\\\x95=ax\\\\x90<h\\\\xf3\\\\x94\\\\xbb\\\\xf4\\\\xf46=3zU<\\\\xf9\\\\xb1\\\\xe3\\\\xbc\\\\xe2\\\\x0d\\\\x22=\\\\xd1\\\\x9d\\\\xd6;c\\\\x15\\\\xd0<<\\\\x80\\\\xcf\\\\xbc\\\\x06i\\\\xaf\\\\xbc\\\\xe6z\\\\xd7;\\\\xc9\\\\xe7<=\\\\x15\\\\x5c\\\\x02\\\\xbd\\\\xf1\\\\xa0$\\\\xbd*\\\\xb9\\\\xbe;\\\\xe9\\\\x82p=\\\\xf6$\\\\xb3<\\\\x89\\\\xf2y=\\\\xad\\\\xc7\\\\xaa\\\\xbd\\\\xe6 \\\\xae\\\\xbc\\\\xa0\\\\x06\\\\x1b\\\\xbdq\\\\x03\\\\xb0=\\\\x22E\\\\x8f\\\\xbc\\\\xf7\\\\xb5\\\\x00\\\\xbd\\\\xa7:\\\\x16=\\\\xdeYl\\\\xbd\\\\xaf\\\\x7f&:\\\\x05\\\\xc4\\\\xaa\\\\xbc\\\\xb1\\\\xdbo<a\\\\xdcJ\\\\xbd\\\\xaf\\\\x01:9\\\\x14y\\\\xf5\\\\xbc\\\\xba\\\\xe5\\\\xb5<n\\\\x22\\\\x87;c|P=\\\\xee\\\\x8e\\\\x9a\\\\xbd\\\\x93\\\\xca\\\\xbb\\\\xbb\\\\xba\\\\x1a\\\\xcd\\\\xbc\\\\x82\\\\xb3\\\\xa9\\\\xbcg\\\\xd8\\\\xc7\\\\xbc\\\\xde\\\\xa5\\\\x1a\\\\xbd\\\\xf2Zq\\\\xbd\\\\xb0)\\\\x5c=d=\\\\xaa\\\\xbc\\\\x13J\\\\x16=!\\\\xdaA\\\\xbd\\\\xc4\\\\x00\\\\x80<\\\\x85b\\\\x95\\\\xbb\\\\xb1\\\\xbc!=\\\\xff\\\\x91|<\\\\xc8\\\\xc2x=>P\\\\xd8\\\\xbb\\\\xa5/T=\\\\x98K6=\\\\xdc\\\\xb8W<\\\\xc9\\\\x1a\\\\xba\\\\xbc3\\\\x15\\\\x07<\\\\xbf\\\\x97\\\\x85\\\\xbd\\\\xf5\\\\x22\\\\xcb\\\\xbcB\\\\xbe^<\\\\x8c\\\\x17\\\\x7f<\\\\x98\\\\xee\\\\xc8<\\\\xc8\\\\xa2\\\\xcc<\\\\x0e\\\\x8bl<o\\\\xdd\\\\x19\\\\xbd\\\\x9d7f<\\\\x9e\\\\x0d?\\\\xbdr\\\\xa58\\\\xbd\\\\xe8\\\\xc6/\\\\xbd\\\\x1dIL:\\\\xca\\\\xaf<\\\\xbd\\\\x99`\\\\x86<\\\\x9d\\\\x5c\\\\x00\\\\xbc\\\\xa3\\\\xd8X\\\\xbc\\\\xd3\\\\xc4^;Q\\\\x03\\\\xa2<\\\\xb6\\\\x05\\\\xde<)\\\\xe1O\\\\xbd\\\\x02\\\\x83\\\\x88\\\\xbd\\\\xb8qF\\\\xbd4H\\\\x04\\\\xbd\\\\xe2>*=\\\\x93*\\\\x1c=\\\\xe4|\\\\xa2\\\\xbcHE\\\\x1f=\\\\x9d\\\\xf2=;\\\\xa7iM;\\\\x15\\\\xa0\\\\x22\\\\xbd\\\\x81\\\\x1a\\\\x84\\\\xbdZY\\\\x9b\\\\xbc4z\\\\x9c\\\\xbcs\\\\xaa8\\\\xbd]\\\\xdb\\\\x0b=\\\\xa9S\\\\x80\\\\xbcx\\\\x81U\\\\xbd\\\\x01~\\\\x98\\\\xbc\\\\x9e\\\\xa2\\\\x88\\\\xbc\\\\xc9\\\\xb3t=Z)9=|\\\\xc2u<\\\\xaaE\\\\x9b<a\\\\x1b:\\\\xbb~\\\\xacZ\\\\xbd\\\\xc8\\\\xd5\\\\xd7<;\\\\x85\\\\x80\\\\xbd=\\\\x187\\\\xbcu\\\\x99\\\\x86\\\\xbc\\\\x01\\\\x99r\\\\xbd\\\\xee\\\\xf0\\\\xd8<\\\\x9a\\\\xb1\\\\x83\\\\xbdMO\\\\xa2<\\\\xc8\\\\x9b\\\\x10<aQ\\\\xa2\\\\xbb\\\\xfa#\\\\xc0;\\\\xcb\\\\x8a\\\\x9b;\\\\x8c\\\\xb5,\\\\xbd\\\\xed4\\\\xfe\\\\xbc\\\\xbd\\\\x19\\\\xb7<\\\\x87-\\\\xb0=y\\\\x14\\\\x00\\\\xbcD\\\\x81\\\\xa6\\\\xbba\\\\xa2\\\\x81<&\\\\xdc\\\\x85\\\\xbc\\\\x8c\\\\xaf\\\\x9e\\\\xbb\\\\xeda\\\\x14=\\\\xec\\\\x22R=\\\\x9bj\\\\x19\\\\xbd\\\\x05\\\\xac\\\\x16\\\\xbd\\\\xb3\\\\xc7\\\\x15\\\\xbd\\\\xb7\\\\x8fa\\\\xbc\\\\x8f\\\\xd3D<\\\\xf6\\\\xffm<}\\\\xef\\\\x03;\\\\xc1X&=\\\\xd5\\\\xf1z:\\\\xdd\\\\xe20\\\\xbd%\\\\xd6@<\\\\x8f\\\\xbd3\\\\xbd\\\\xd3\\\\xc9*\\\\xbdu\\\\xc24\\\\xbd\\\\x92\\\\xbd\\\\xba\\\\xbb\\\\xcb\\\\xfa;\\\\xbd\\\\xb3\\\\xd3\\\\xe9<t\\\\xad\\\\xce\\\\xbdHnq9\\\\x9d\\\\xa2\\\\x84<\\\\xc5\\\\x08\\\\xba\\\\xbd\\\\xad+r\\\\xbd\\\\xf5\\\\xd26\\\\xbd\\\\x87\\\\x97\\\\x8e\\\\xbb\\\\x87\\\\xf3\\\\xd7:\\\\x87\\\\xeam\\\\xbc:\\\\xbc\\\\x12\\\\xbd.M\\\\xb0<\\\\xefw\\\\xd2\\\\xbb\\\\xda\\\\xed\\\\xe7;\\\\xdc\\\\xf1\\\\x10<\\\\x10P\\\\x98\\\\xbb\\\\xf6\\\\xaf[\\\\xba\\\\x11\\\\x86b\\\\xbdN\\\\xe9\\\\x05<y\\\\x8f\\\\xaa=<JE=\\\\xe1\\\\xeb\\\\xfe\\\\xbc\\\\xf1\\\\xa5\\\\xfe<kg\\\\xe6; \\\\xed,=\\\\xcd[!\\\\xbdV3\\\\x06=U\\\\x87\\\\xf5\\\\xbc\\\\xdf\\\\x90t\\\\xbc\\\\xe9\\\\x9f\\\\x9b</\\\\xca\\\\x8a<\\\\xed\\\\xbe\\\\xb3;@2\\\\xef\\\\xbc\\\\x88\\\\xe8\\\\xce<Y\\\\x8e\\\\xf7<G\\\\xfe\\\\x95<\\\\xe5\\\\xe2\\\\xe6;\\\\xe5q\\\\xa0:9\\\\x0bH<0\\\\x9f\\\\x9b\\\\xbc6\\\\x91\\\\xc1\\\\xbc\\\\xe7\\\\x96s=\\\\x9b\\\\xb6\\\\xfd\\\\xbc\\\\xd4\\\\xa8\\\\x0e\\\\xbd\\\\xadA9=\\\\xcbZ\\\\xd8\\\\xbb\\\\x06\\\\xbdC=\\\\xa5\\\\xf4\\\\xb3\\\\xbd\\\\x08\\\\x22\\\\x00=\\\\x1c\\\\x17\\\\x11=\\\\xe05\\\\x97\\\\xbd\\\\xc6W\\\\xa6\\\\xbbV\\\\x1cR;\\\\xf2\\\\x1c\\\\x22\\\\xbc\\\\xef+\\\\x07\\\\xbdm\\\\xfa\\\\x07\\\\xbc|\\\\xf7\\\\xe3<t\\\\xce.\\\\xbc\\\\xc2\\\\x80i\\\\xbd\\\\x22P\\\\x13\\\\xbc?\\\\xeeS=\\\\x14\\\\x83\\\\x8e<f\\\\xb9\\\\xf1\\\\xbaT\\\\xdb2\\\\xbdP\\\\x7f\\\\xe7<\\\\xb0eB=\\\\x82\\\\xb0\\\\x9a=r\\\\xaa!<5\\\\x81\\\\xa291$\\\\x11=\\\\xc5\\\\xc8\\\\x98\\\\xbc\\\\xe3\\\\x89W;\\\\xfff <\\\\xff\\\\xe3:\\\\xbd\\\\xa3\\\\xed\\\\xe2\\\\xbc\\\\xe0!\\\\xa2\\\\xbc\\\\xf5\\\\x0f\\\\xbe=9\\\\xd0[=\\\\xcf\\\\xcc\\\\xb1;`Hd\\\\xbc\\\\x03|9=M\\\\xf5\\\\xb2<m|\\\\xf2;\\\\x14\\\\xac.=~\\\\xd5B=\\\\xd0_\\\\xf9<k\\\\xa8\\\\xb1<\\\\xfb!,=&\\\\x15Y\\\\xbdHk\\\\xf6<\\\\xebx\\\\xa2=*\\\\xb3\\\\xbe\\\\xbd\\\\x5cpM\\\\xbd\\\\xb1\\\\x04\\\\x15\\\\xbd,\\\\x03\\\\x18\\\\xbd\\\\xef\\\\x5cS;F\\\\xcd\\\\xe7<|z\\\\xbe<\\\\xa6i\\\\x89=\\\\x90a\\\\x02<\\\\xb0/\\\\x8c\\\\xbd\\\\xbf\\\\x9f\\\\xb0=\\\\x1aB\\\\x08=\\\\x84\\\\xdb\\\\x9d\\\\xbc\\\\xe5\\\\xeb:=`w\\\\x9b\\\\xbc\\\\xc7\\\\x06\\\\xe5\\\\xbc\\\\xa7\\\\x97\\\\x9d\\\\xbd\\\\x0d\\\\x7f\\\\xc1<\\\\xf9\\\\xben=\\\\xf8\\\\xee\\\\xe1<\\\\xda)d\\\\xbd\\\\x04\\\\xa1\\\\x0d\\\\xbb#.0=l\\\\xb6\\\\x9d:]\\\\xac\\\\x95\\\\xbc\\\\xa2.!\\\\xbdP\\\\xbf\\\\x8c\\\\xbcE*\\\\x5c\\\\xbd\\\\x10\\\\x09\\\\x81=2?n;:\\\\x82\\\\xe4;\\\\xee(\\\\x04\\\\xbd=\\\\xbe\\\\xd3;#v7\\\\xbd\\\\xe7\\\\xaaR:\\\\xe6\\\\xc4\\\\xca\\\\xba\\\\xc0\\\\x5c\\\\x1f\\\\xbd7.\\\\xc3<\\\\x1d\\\\xe9N=8m\\\\xa6;\\\\xabe`:B\\\\x9d\\\\xd6\\\\xbc\\\\xcb`\\\\x8b<~\\\\x9e\\\\x16=\\\\x88C\\\\xcb\\\\xbc\\\\xad_\\\\x0f=6\\\\x0f\\\\x04\\\\xbd\\\\xc3vM\\\\xbc\\\\xa2\\\\x90C\\\\xbc2\\\\xdb\\\\x1b<Z!9\\\\xbc\\\\xde\\\\xc6-=N\\\\xee\\\\xa6\\\\xbb\\\\x16(\\\\xde\\\\xba\\\\x17bH=e-z=\\\\xd7\\\\xef\\\\xa3<al\\\\x8c\\\\xbci\\\\xd7`=6cW\\\\xbb\\\\xcdx\\\\xa3<\\\\xa8\\\\x83:=\\\\xbc\\\\xfa\\\\xa39\\\\xeb\\\\xb3\\\\x92<Y\\\\xc2\\\\xfb:v@\\\\x06=D\\\\x8c\\\\xc5<\\\\xec\\\\x800\\\\xbba\\\\xef\\\\x18\\\\xbd\\\\x95\\\\xd6\\\\x9b\\\\xbc/\\\\xb0\\\\xa9;\\\\x8b\\\\x98F\\\\xbd\\\\xea\\\\x7f\\\\x8f\\\\xbcQ#\\\\x87<D8\\\\x04=\\\\xb86\\\\xf6<\\\\x8dW@=L\\\\x17)\\\\x09_\\\\xab\\\\x0d\\\\xbc\\\\xb2M\\\\xae\\\\xbc&\\\\x898<\\\\xa9\\\\x12\\\\x08\\\\xbd^bN=<\\\\xe4\\\\xff;\\\\x05{|\\\\xbd\\\\xf3\\\\xacD<1\\\\x82\\\\x8a<\\\\xc6\\\\x8d \\\\xbdD\\\\xbe\\\\xa5\\\\xbc^\\\\xcdk=w}9\\\\xbd\\\\x18\\\\xa9#=\\\\xa0\\\\xa5\\\\x1c\\\\xbd1\\\\x9f\\\\x1b<.Nu\\\\xbd\\\\x01\\\\xf03\\\\xbc\\\\x03\\\\xf2\\\\x7f<DuK<WX\\\\xc4\\\\xbaql\\\\x1b=\\\\x94\\\\xb9\\\\x8c\\\\xbc\\\\xc0l\\\\xdb\\\\xbc0!%=\\\\xe3\\\\x16F\\\\xbd{\\\\x93`\\\\xbdY\\\\xb2\\\\xc6\\\\xbc\\\\xaa\\\\xc8<=<\\\\xe4<=\\\\x0eL\\\\xcb\\\\xbc\\\\xe6\\\\x18\\\\xa3<X\\\\x95\\\\xbe\\\\xbc#\\\\x837\\\\xbd\\\\xe5\\\\xb2\\\\xc1;\\\\x1aT[\\\\xbd\\\\x00K\\\\xaa\\\\xb9\\\\xe3\\\\xb2\\\\xd7;\\\\xfd\\\\xb21=\\\\xd5\\\\x83\\\\x02\\\\xbd\\\\xeci\\\\xb7\\\\xbaE\\\\x05\\\\xf8<\\\\x22Xi\\\\xbc\\\\xe2\\\\x8d\\\\x0d\\\\xbcW\\\\x1b\\\\x5c\\\\xbc\\\\xf7^\\\\xa4<8\\\\x22\\\\xbe=U\\\\xdf!\\\\xba\\\\xaa\\\\x839\\\\xbdy\\\\x09A\\\\xbdVoD\\\\xbd;~[\\\\xbdO\\\\x0c\\\\xb5\\\\xbc\\\\xe5$\\\\x82=\\\\xd9z\\\\xbf\\\\xbc\\\\x10\\\\xc5O=T\\\\xcd6\\\\xbd\\\\x07\\\\x981\\\\xbd\\\\xd2\\\\xb4K<C#\\\\x9f\\\\xbc8e\\\\x0e=T\\\\xf6\\\\xf89\\\\xc9Pk=T\\\\xce\\\\xfe<\\\\xae\\\\x17 \\\\xbc\\\\x1c\\\\xd9M\\\\xbc\\\\x85\\\\x08\\\\x83<\\\\x15U\\\\xb0<\\\\x96\\\\xdd\\\\x81;Yq\\\\x0b9\\\\x8d\\\\xf0p=\\\\xfb]\\\\x99<\\\\xf6\\\\xd9+=Z\\\\x84\\\\x8f=\\\\xb5\\\\x98\\\\x04\\\\xbd\\\\xbf\\\\x22\\\\x10=M\\\\xed\\\\xbd\\\\xbd\\\\xd1\\\\x08<=\\\\xc8|\\\\x83\\\\xbc\\\\x11\\\\xae\\\\xdb\\\\xbc\\\\x91\\\\xb1\\\\xa0\\\\xbd4=[=\\\\x1f\\\\x10\\\\x19<\\\\x8c\\\\x22\\\\x8f:d\\\\x96\\\\xa6\\\\xbc\\\\xec\\\\xf7\\\\xe3<}\\\\x06\\\\x02=\\\\x12X\\\\xee\\\\xb8Y!\\\\xf4<\\\\x07\\\\xf7m<\\\\x1b\\\\x7fs<A\\\\xfb\\\\xf4\\\\xbb\\\\xcc\\\\x16\\\\xe5<\\\\xc7\\\\xe4\\\\xa7:\\\\xdb!\\\\x05=\\\\xc7_G<\\\\x91\\\\x11y\\\\xbd\\\\xcc\\\\xd69\\\\xbd\\\\x0f\\\\xdb\\\\x88<D3\\\\xdf\\\\xbc\\\\xdbx\\\\xc9\\\\xbd\\\\xc1I\\\\x8f\\\\xbd\\\\x12\\\\x11\\\\xc0\\\\xba\\\\xef\\\\x15I\\\\xbd\\\\xab\\\\xb3?<!\\\\xe0\\\\x08\\\\xbd\\\\xa6\\\\xfa\\\\xd89[\\\\xa3\\\\x8e\\\\xbc\\\\xc2Q\\\\x1b\\\\xbd;\\\\x96\\\\xd3\\\\xbb6v.=\\\\x19W\\\\x8e\\\\xbb\\\\x98+H\\\\xbd\\\\x92h\\\\xa8=\\\\xef\\\\xb4\\\\x1f=\\\\xd0\\\\x22\\\\xf3<\\\\xfd\\\\x10\\\\xe5\\\\xbc\\\\x94b\\\\x02<\\\\xa6\\\\x22\\\\x87<\\\\x82\\\\xd7\\\\xfe\\\\xbc4z*=VTA\\\\xbc*\\\\x19\\\\x8b\\\\xbbA\\\\xf5\\\\x04=f\\\\x1a\\\\x14\\\\xbc\\\\x1bN\\\\x95<`\\\\xef\\\\xd4<\\\\xbcA3=~c\\\\xac=\\\\x84\\\\xa7J\\\\xbd\\\\x98\\\\xa0\\\\xe7\\\\xbc\\\\xb5mk\\\\xbd\\\\xb4B\\\\x13=J\\\\xedk=H\\\\xc2\\\\xc5=\\\\xac\\\\x0d\\\\x92;\\\\xabb!=\\\\x800\\\\x1a\\\\xbc\\\\xa6D\\\\xdb<\\\\xaa\\\\xaf\\\\xf4<s\\\\x83\\\\x1a\\\\xbc\\\\x9d\\\\x09W\\\\xbd\\\\xab:\\\\x89\\\\xbd\\\\x81\\\\x02\\\\xcf\\\\xbb\\\\x80\\\\xf1\\\\x5c\\\\xbd:\\\\x1a\\\\xcf<\\\\xed\\\\xc2p\\\\xbc\\\\xa0+5<T\\\\x14\\\\x85\\\\xbd/\\\\xd1\\\\x9c\\\\xba\\\\xebE#\\\\xbd\\\\x17\\\\xde\\\\xb1\\\\xbc\\\\xbc\\\\x1e|\\\\xbd\\\\x9d\\\\x9b9\\\\xbc\\\\x81\\\\x0f\\\\xef<\\\\xfc\\\\xda\\\\x15\\\\xbd\\\\x98Wt\\\\xbd\\\\x97\\\\x94\\\\xf7<q?\\\\xbc\\\\xba\\\\xde\\\\x0f\\\\x04<\\\\xd6\\\\xb2\\\\xd6\\\\xbc\\\\xa8\\\\xa7\\\\x11=\\\\xd3\\\\xfe\\\\x11=\\\\xdd\\\\xae\\\\x82\\\\xba\\\\x0e\\\\x0f\\\\x0a<{Np\\\\xbcc\\\\xa8\\\\xc6<R\\\\x15\\\\xe4\\\\xbc\\\\xf9?p=\\\\xe5\\\\x11a\\\\xbd\\\\xa7\\\\x9e;\\\\xbb#9\\\\x88=\\\\x80\\\\x84%<l!};\\\\xfb\\\\xb9\\\\x02\\\\xbd\\\\xacm$=\\\\x02\\\\xe6\\\\xb1;\\\\x8f\\\\x82\\\\x00<\\\\x9d\\\\xf90=\\\\x05\\\\xf2\\\\xbc\\\\xba\\\\xba\\\\xbb\\\\x94<+\\\\xcc?\\\\xbc\\\\xe4A\\\\x93\\\\xbb\\\\x02R\\\\x08\\\\xbd\\\\xde\\\\xcf|\\\\xbcD\\\\x0cq\\\\xba\\\\x01l1\\\\xbd@\\\\xb1K\\\\xbdC0>=\\\\x8c\\\\xd8^<\\\\xba\\\\x8d\\\\xb2=\\\\x98\\\\xb1\\\\x84\\\\xbc\\\\xf9\\\\x93J<\\\\xba\\\\x93#\\\\xbd\\\\x9dX\\\\x0a\\\\xbdz\\\\xe6G\\\\xbd\\\\xc5\\\\xa5\\\\xa2\\\\xbd\\\\xec\\\\xf1\\\\xb1<-9L=\\\\xbe\\\\x86K=\\\\xb6\\\\x91\\\\x1a:z(\\\\xa2;\\\\xd6\\\\x16?<\\\\xcd\\\\x22&\\\\xbd:\\\\x05\\\\xb9<\\\\xa1\\\\xf13\\\\xbd\\\\x10\\\\x9f\\\\xc4\\\\xba\\\\x09\\\\x1c\\\\x85\\\\xbd<=a\\\\xbaCg\\\\x85\\\\xbc\\\\xe6\\\\x85\\\\xe1;\\\\x11\\\\xc2\\\\x8b\\\\xbdu\\\\xaf\\\\xde:\\\\xe2\\\\x14\\\\x84\\\\xbb\\\\xc7\\\\x0b\\\\x99\\\\xbc\\\\xde\\\\x03\\\\x1e\\\\xbd\\\\xbaO\\\\xdf<\\\\xa7\\\\x9dl\\\\xbc\\\\x1e*=\\\\xbd\\\\xdc\\\\xc8\\\\x1f\\\\xbc\\\\x5c\\\\x16^;\\\\x97\\\\x87K\\\\xbd\\\\x9f\\\\x8e\\\\x9a\\\\xbc\\\\xbfH\\\\x09\\\\xbd\\\\xf1+(=\\\\xeeF\\\\x16=u\\\\xfev<c8\\\\x04\\\\xbd\\\\xaa>A\\\\xbd\\\\x05,)<Y\\\\x22\\\\xb5\\\\xb8\\\\x1a\\\\xd4;\\\\xbc\\\\x9d\\\\x0a\\\\xea\\\\xbc\\\\x1fk7\\\\xbd\\\\x08\\\\x07\\\\x88\\\\xbc\\\\x92\\\\xad\\\\x22=\\\\x05$\\\\xe8;\\\\xa9-w;\\\\x0dY\\\\xa6=\\\\xd1<\\\\xa9\\\\xba\\\\xa47\\\\xd1;\\\\xf3Y\\\\xb4=\\\\xac|\\\\x84\\\\xbb\\\\x0eS\\\\x03\\\\xbdY\\\\xc1\\\\xe8<\\\\xb5t\\\\xaa\\\\xbaR\\\\x81\\\\xac\\\\xbc\\\\xb7\\\\xf0\\\\xa0\\\\xbb\\\\xf056\\\\xbd\\\\x1drc\\\\xbd\\\\xda\\\\xf3\\\\x83;|\\\\x08\\\\x22=\\\\x80\\\\x01\\\\xb8<\\\\xe8\\\\xb6\\\\x94\\\\xbc\\\\xe6\\\\xccF=\\\\xeb\\\\xfc\\\\x0a=\\\\xc4\\\\xe7y\\\\xbd&\\\\x82\\\\x95;\\\\x7f\\\\x0e\\\\x1b=\\\\xaf~\\\\x0d\\\\xbd\\\\x0ad\\\\xc2\\\\xbbN\\\\xd1\\\\x7f\\\\xbc\\\\xbd,J=[\\\\x99:\\\\xb9\\\\xfcy\\\\x91=\\\\x9e!\\\\xfb;\\\\xb7\\\\xab\\\\x9b=\\\\xbe\\\\xcd\\\\xe4<\\\\x82\\\\xb0\\\\xab<\\\\xcc\\\\x8fC;\\\\xcdR@<\\\\x1f2\\\\xa7=7\\\\xd4\\\\x89\\\\xbd\" ' +\n        'SORTBY score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Age-targeted semantic search',\n      description:\n        'Searches for bikes matching the natural language query \"Commuter bike for people over 60.\" Demonstrates how vector search can understand intent and context beyond keyword matching, finding bikes suited for older riders prioritizing comfort and ease of use.',\n      query:\n        'FT.SEARCH idx:bikes_vss ' +\n        '\"*=>[KNN 3 @description_embeddings $my_blob AS score ]\" ' +\n        'RETURN 4 score brand type description ' +\n        'PARAMS 2 my_blob \"A=\\\\xe1\\\\xbb\\\\x8a\\\\xad\\\\x9b<&7R\\\\xbd=!\\\\xaf\\\\xbbN\\\\x0b|\\\\xbd\\\\x1c\\\\xaf\\\\xaf<@\\\\x96\\\\xa3;\\\\x7fN8\\\\xbd\\\\xf8\\\\x8d\\\\x19=&:\\\\x94\\\\xbc\\\\xb1\\\\x19\\\\xbe<\\\\xef\\\\x8e\\\\xe5;\\\\xac\\\\xc2\\\\x93\\\\xbc\\\\xe1\\\\xbd\\\\x9c\\\\xbc\\\\xc1\\\\xf1)=\\\\xcfE\\\\x85;\\\\x8aq\\\\xae:z3\\\\xb1;\\\\x95\\\\xf9\\\\xf4<\\\\xa7\\\\xc6\\\\x8f\\\\xbc\\\\xc6\\\\xc1\\\\xc3<-\\\\x02\\\\xdd\\\\xbc\\\\xd3\\\\x16\\\\xe5<\\\\x06`\\\\xd7<\\\\x1eB\\\\xc4\\\\xba\\\\xd4D\\\\xba;O\\\\xb2+=\\\\xff\\\\x83N=|zy;\\\\xdeL\\\\x01=&\\\\xf1\\\\x9f\\\\xbc\\\\xcc\\\\x02\\\\xe0\\\\xbc\\\\xc8\\\\x22\\\\xb4\\\\xbc\\\\xcf\\\\xe3V\\\\xbc\\\\x9e\\\\x03\\\\xef:vJ\\\\x13\\\\xbd]\\\\xc2F=\\\\xed\\\\xa8\\\\xaf\\\\xbb\\\\xd8?\\\\xc4\\\\xbc\\\\x06\\\\x01\\\\x05=\\\\x84\\\\x83P=\\\\x0bT,<\\\\x0c\\\\x9c\\\\x1d<H\\\\x1b\\\\xf2<\\\\xf8Z\\\\x88\\\\xbc\\\\xde\\\\x81\\\\x97\\\\xbaV\\\\x03Y=\\\\xc8!-\\\\xbdL\\\\x0f\\\\xe0\\\\xbc\\\\xbe\\\\x93\\\\xd6<h\\\\xb4O<\\\\x07\\\\xdd\\\\x13=\\\\x1d]\\\\xef:\\\\x90\\\\xa0\\\\xbb\\\\xbc\\\\x1e\\\\x82G<\\\\xc0\\\\x1d\\\\x0b\\\\xbdO\\\\x0f\\\\x9d\\\\xba\\\\xef\\\\xa0\\\\x80\\\\xbd&\\\\xc2\\\\xf6:u\\\\x19\\\\x86=\\\\x9e\\\\xdc\\\\xa3<\\\\xa7\\\\x9a\\\\xb2< \\\\xbff<\\\\x08\\\\x9bu\\\\xba\\\\xad\\\\x1d\\\\xb5=R\\\\x1b\\\\xf9<\\\\xfcJ\\\\x9c\\\\xbc\\\\x9d>\\\\x0b=\\\\xae1>=|\\\\x8f\\\\x81;\\\\xac\\\\xdc\\\\xbc<3k\\\\xe6\\\\xbcU\\\\xdd\\\\xcb\\\\xbcs\\\\xf4\\\\xa3\\\\xbd\\\\xc7\\\\x06\\\\x1c\\\\xbd\\\\xa8\\\\xf4m\\\\xbc\\\\x02\\\\x92R;\\\\xda\\\\xc0\\\\x85\\\\xbb\\\\xee\\\\x04\\\\x19\\\\xbd\\\\xfd\\\\x97F\\\\xbcD\\\\x03\\\\xc6;YM\\\\xc2\\\\xbd\\\\x82\\\\xd0b<\\\\xcdt\\\\x15=\\\\xde\\\\x8az<u\\\\xa8\\\\xce=\\\\xf4\\\\xf92\\\\xbbn\\\\xe8\\\\xbc\\\\xbbgX\\\\x9b={\\\\xd5\\\\x1d<\\\\x15io<-&\\\\x88\\\\xbd\\\\xd8\\\\xc9\\\\x1d\\\\xbd\\\\x01\\\\xfe\\\\xf0\\\\xbb\\\\x9de)\\\\xbd\\\\xd20G\\\\xbd|A\\\\xbc\\\\xbc\\\\xd9\\\\xc5\\\\xa7<\\\\xaa+h=4\\\\xec\\\\x02\\\\xbc\\\\xe2gs=\\\\x00\\\\x91N<K\\\\x88\\\\xee<\\\\xf7\\\\xae\\\\x1a=]\\\\xe8\\\\x04\\\\xbcA\\\\xb2\\\\xcf<\\\\x88\\\\xa1\\\\x06=\\\\x82\\\\x80\\\\x11\\\\xbb\\\\x09\\\\xd0[\\\\xbdo\\\\x06\\\\xf5<\\\\xc4!\\\\xc8<5\\\\xdea<\\\\xef\\\\xffG<W\\\\x0c4\\\\xbd\\\\xb7H\\\\x85\\\\xbd^\\\\x01\\\\xc9<[\\\\xf2\\\\x1f=\\\\x86\\\\x0a\\\\x14=OE#<n\\\\xb1\\\\x80\\\\xbd\\\\x08\\\\xc3\\\\x01\\\\xbc){\\\\xd9;\\\\xfa\\\\x1c;\\\\xbd8\\\\xafJ=YW\\\\x8e=\\\\xfb\\\\x11\\\\xf1:Q\\\\x93\\\\xb5\\\\xbc\\\\xc1\\\\xbb\\\\x8a\\\\xbcy$\\\\x19<\\\\xe2\\\\xafP\\\\xbd\\\\xdc\\\\x8ef\\\\xbc\\\\xa7o/\\\\xbd\\\\xc3\\\\xdc\\\\x04=-\\\\xd7\\\\xf1\\\\xbc\\\\xeb3\\\\x0a\\\\xbd\\\\x92\\\\x9dS<\\\\x8d\\\\xffV\\\\xbd\\\\xe1DI\\\\xbd\\\\xc8g\\\\xa1\\\\xbcZk\\\\x5c\\\\xbcG\\\\xe5\\\\xd5\\\\xbcK-\\\\xef;\\\\xc6j\\\\x1e\\\\xbd\\\\x06\\\\x17\\\\x90=\\\\x97\\\\xe2\\\\x00\\\\xbdbv|\\\\xbd\\\\xf6U\\\\xcc;\\\\x0bw\\\\x8e=d\\\\x80\\\\xed\\\\xbbb\\\\xcaN<\\\\xf2\\\\xc1\\\\xed<\\\\x22o\\\\x18=\\\\xb1\\\\x8dC\\\\xbd\\\\xfa\\\\x06\\\\xc7<\\\\x88\\\\x18\\\\x7f\\\\xbbG\\\\x13&=\\\\x1bS\\\\x08\\\\xbc>.\\\\xe3\\\\xba\\\\xf5\\\\xa6(=n\\\\xa7,\\\\xbd%\\\\xfb\\\\xed;\\\\xaf\\\\x82n\\\\xbd\\\\xff\\\\xeb\\\\xd5\\\\xbc\\\\x19\\\\x0f\\\\xec;\\\\xbe\\\\xbb\\\\xad=ex\\\\xcd<\\\\x8fS\\\\xa3<O?\\\\xd5\\\\xbc\\\\xa8S\\\\x15<j(A\\\\xbbh\\\\x8f\\\\x90\\\\xbc]\\\\xc3\\\\xf3<\\\\xa8\\\\xe3#\\\\xbc\\\\xaek5\\\\xbd\\\\x817\\\\xb1\\\\xbd\\\\x0f^v\\\\xbb6\\\\xf4\\\\xbc<\\\\x82#\\\\xf4<\\\\x01\\\\x1a\\\\xe9\\\\xbc\\\\xd7q\\\\xdc\\\\xbc.\\\\x8b\\\\x15=+\\\\xb8\\\\x1b<\\\\x00&\\\\x1e\\\\xbd\\\\xad\\\\xba\\\\x22;\\\\xefX\\\\xf0<\\\\xfa\\\\x1e\\\\xd5\\\\xbb%\\\\x1a\\\\xa2=4\\\\xb1\\\\xb0\\\\xbd&\\\\x9f\\\\x8a\\\\xbbS\\\\x1fk\\\\xbc\\\\xbf\\\\x93\\\\x9e\\\\xbd\\\\xa2\\\\xc9\\\\xa3<\\\\xc1\\\\xc6\\\\x8d;\\\\x9d\\\\xfb\\\\xea\\\\xba\\\\xcc\\\\xe4s\\\\xbd\\\\x88\\\\xa1\\\\x1b=2\\\\x1f\\\\x9a<\\\\x1e/\\\\x02\\\\xbd\\\\xa1\\\\x99\\\\x1f\\\\xbd\\\\xc9\\\\xba\\\\xf8:Z\\\\x04-\\\\xbc\\\\xca\\\\xe5k<~\\\\xcd(\\\\xbc\\\\x81\\\\x13H<\\\\xef\\\\x03?\\\\xbd=\\\\xab6=\\\\x17\\\\xcc\\\\x88<\\\\x89\\\\x09\\\\xde\\\\xbc\\\\xa3\\\\x19!<\\\\x22N\\\\xae\\\\xbc\\\\xad\\\\x96\\\\xa4<JI\\\\xc9<\\\\xcbU\\\\x9c=\\\\x8a\\\\xd08<\\\\xca\\\\x1f\\\\xa6=j\\\\x9b\\\\xf9\\\\xbc\\\\x8fW\\\\xd4\\\\xbcs\\\\xa6\\\\x95=\\\\xed]\\\\x9e=w?\\\\x81\\\\xbc[\\\\x22\\\\xd4\\\\xbc\\\\xe4\\\\x22T<\\\\xc2\\\\x8fi\\\\xbd1&!\\\\xbd\\\\xef%\\\\xbb\\\\xbd0\\\\xfbu\\\\xbd\\\\xe7\\\\xe2\\\\x1e\\\\xbd\\\\x5c\\\\xa3\\\\xea\\\\xbc\\\\x1b\\\\x83\\\\x06={G|\\\\xbd\\\\x8a\\\\xf8 =\\\\xb8\\\\x82?\\\\xbd\\\\x07\\\\xdc\\\\x01;\\\\x04[\\\\x83<B\\\\xaft=RA\\\\xff\\\\xbb@\\\\xb42=\\\\x97d\\\\x85=\\\\x06\\\\xce\\\\xb0;\\\\x95\\\\xfd\\\\x18\\\\xbd\\\\x83\\\\x18\\\\xf5\\\\xbc\\\\x1c\\\\x1f/\\\\xbc\\\\xda\\\\xc7k\\\\xbd\\\\x83mS=)[\\\\xcb\\\\xba\\\\x80\\\\xa0\\\\xf7\\\\xbb\\\\x9a\\\\x12\\\\x16=\\\\x12\\\\xc0\\\\xc2<\\\\xa2?\\\\xd6\\\\xbd\\\\x06Kh=\\\\xcb\\\\x92\\\\x03\\\\xbc\\\\xcf\\\\xae\\\\x89\\\\xbdE\\\\x8b\\\\x0e\\\\xbd]\\\\x02\\\\x86</1\\\\x1a\\\\xbd\\\\xd0\\\\x17\\\\x0d<:\\\\x1a\\\\x86\\\\xbc\\\\xd2{\\\\x00\\\\xbd\\\\x1b#\\\\x22=\\\\xb1[\\\\xa2<\\\\x8dg\\\\x83\\\\xbc\\\\xed\\\\x9b<<\\\\xd7\\\\x84\\\\x98\\\\xbd)\\\\xba\\\\xd0\\\\xbc\\\\xa6]\\\\xef\\\\xbc\\\\x9eON=S\\\\x92\\\\xea<[R\\\\xb3<$IT=\\\\x5c\\\\xe9\\\\xb5;\\\\xe1\\\\x08\\\\x15<\\\\x12\\\\x8e\\\\xed:\\\\xf6\\\\xc5\\\\x87:d-\\\\xd0<t\\\\xf7\\\\xa7\\\\xbb\\\\x8f\\\\x1c\\\\xcd\\\\xbcI\\\\xb8\\\\xc9;j\\\\xadR=\\\\xae\\\\x1d\\\\x5c\\\\xbdj\\\\xc8=\\\\xbd\\\\xed\\\\xb3\\\\x86\\\\xbc\\\\xb1\\\\xb8\\\\xec<\\\\x07H\\\\x07=\\\\xb9\\\\x87\\\\xe0<\\\\x0c\\\\xd5<=g\\\\xfd`\\\\xbd\\\\xa3`\\\\xf9\\\\xbcm\\\\xe8\\\\x08\\\\xbd\\\\xad}t\\\\xbdq\\\\xaa\\\\x03=\\\\x08\\\\x18D\\\\xbdl\\\\x9cV\\\\xbdFKv\\\\xbc8\\\\x0c\\\\x00\\\\xbd\\\\x14v\\\\x22\\\\xbc\\\\xbfF\\\\x06\\\\xbd\\\\xb1-r\\\\xbb\\\\x8f\\\\xae\\\\xe0<\\\\xf6a\\\\x11\\\\xbd\\\\x8d\\\\xb9\\\\xec<\\\\xccV\\\\xfb\\\\xba&\\\\xfa\\\\x95=W\\\\xd8\\\\x84=Sm\\\\xad\\\\xbc\\\\xe5CH\\\\xbdu\\\\x5c\\\\x83<\\\\xb0l\\\\xd9\\\\xbb\\\\x93g\\\\x19<A\\\\x8b5<r\\\\x05\\\\x83=\\\\x0e6;\\\\xbd^\\\\x98\\\\xfd\\\\xbc\\\\x1e\\\\x99\\\\xe6\\\\xbcQ\\\\xdeC\\\\xbd\\\\xdeS\\\\x1d;6O\\\\xd4<\\\\xc3\\\\x8a_\\\\xbdm>\\\\x15=Ij\\\\xee\\\\xbc\\\\xca\\\\x8e\\\\x1e\\\\xbd\\\\x87m\\\\xc4<~\\\\x22\\\\x1d\\\\xbc\\\\xc9m\\\\x0b\\\\xbc\\\\x07\\\\xa4\\\\x18\\\\xbdqg\\\\xb4=\\\\xa8\\\\xcb\\\\xf1\\\\xbcn_\\\\x03=\\\\xf3\\\\xb3\\\\x07\\\\xbd\\\\xb2\\\\xea\\\\x9e\\\\xbcm\\\\xe1/;\\\\xc0F\\\\x8d\\\\xbd\\\\x04\\\\xca\\\\xaf<\\\\x10\\\\x98\\\\x8e\\\\xbdc\\\\xe4\\\\xb2\\\\xbb\\\\xd5K;;\\\\xc3\\\\x8c\\\\x94=\\\\xfe\\\\xa0\\\\x13<f\\\\xb5\\\\xd4\\\\xbb\\\\x8d\\\\x8e\\\\xa8;\\\\xc3e\\\\x0c\\\\xbc\\\\xd3\\\\xbbp\\\\xbc\\\\xdd\\\\xb9\\\\xab\\\\xbc\\\\xd0\\\\xcb\\\\xe3<\\\\x1bV\\\\xa1\\\\xbb\\\\x83\\\\x96\\\\x0a\\\\xbd\\\\x93\\\\xb2z=L\\\\xf7Z=\\\\x1bz\\\\x03<p\\\\xdb^\\\\xbbD}<\\\\xbd1\\\\xf5x\\\\xbcpz};>&\\\\x88=@\\\\xf9\\\\xd3\\\\xbd\\\\x94G\\\\x1c=\\\\xca\\\\xf0&\\\\xbd\\\\xd6\\\\x97\\\\xfc\\\\xbb\\\\xbf\\\\x80\\\\xc9\\\\xbc\\\\xeek\\\\xef<\\\\xc6\\\\x96\\\\x94\\\\xbc\\\\x0b\\\\x8a\\\\x01=W\\\\x81i<\\\\xfdJy=OFo=\\\\xad[]<\\\\x9b\\\\x9bA\\\\xbd\\\\xa3N\\\\xae<\\\\xcb\\\\x88\\\\x15=l\\\\x11k\\\\xba\\\\xbe,\\\\x91\\\\xbc\\\\xaf\\\\xff%\\\\xbcM\\\\xe0\\\\xde<l\\\\x9e7=\\\\xf8P\\\\xd5\\\\xbdY \\\\xae<\\\\x9b\\\\x0b\\\\x04=!\\\\xbf\\\\xa7<\\\\x9d\\\\x5c\\\\x9f\\\\xbb\\\\x82\\\\xf01<\\\\xfdZ\\\\x0f=\\\\xc1\\\\xf5\\\\xc7\\\\xbb\\\\x98;\\\\xc2<\\\\xbbj\\\\x1c=\\\\xef\\\\x1a\\\\xd4\\\\xbb,\\\\xdf\\\\x0e\\\\xbd\\\\xbf\\\\xe9\\\\x85<\\\\x84\\\\xb2\\\\x1d<Jw\\\\xdf<.\\\\xd22=M\\\\xb4\\\\x0d\\\\xbd\\\\x19\\\\xb5\\\\xb4<\\\\xe8>l=J\\\\xaa\\\\x0e=\\\\xef\\\\x16\\\\x0d=\\\\xa9xX\\\\xb9\\\\xb8\\\\xac\\\\xdf\\\\xbc\\\\xe5J\\\\x02\\\\xbd\\\\xa7_\\\\x8a=:\\\\xd8/=W8(\\\\xbd\\\\x0c\\\\x9a\\\\x93\\\\xbd\\\\xbd~\\\\x8d\\\\xba&\\\\x0a`=\\\\x13\\\\x1e&<\\\\xe3t\\\\xb3<Z\\\\xb7c\\\\xbdi\\\\xe2\\\\x8e=\\\\x0bK\\\\xd6<]b.=:\\\\x99\\\\x91<m\\\\x99*=\\\\xed=e\\\\xbd4\\\\xd2\\\\xcc<T\\\\x92\\\\xb8<\\\\x8a*\\\\xa9\\\\xbc\\\\x9f\\\\x81\\\\x9f=\\\\xa7\\\\xb5\\\\xc3\\\\xb9>\\\\x04\\\\x91\\\\xbd\\\\x921]\\\\xbd\\\\x8f6H< \\\\x0c\\\\x8e\\\\xbd\\\\x80\\\\xd0\\\\xc2\\\\xbc\\\\x8ewC\\\\xbd\\\\xd5\\\\xbf\\\\xd0<qu\\\\xb6=\\\\x01\\\\xa7\\\\x22;P\\\\x13\\\\xa1\\\\xbc\\\\xcb\\\\xc2\\\\x83=\\\\xe5\\\\x11\\\\x9b;\\\\xc3\\\\x0b<\\\\xbd\\\\x80\\\\xf3\\\\x04<\\\\xdd[\\\\x90;\\\\xd4H\\\\x0d\\\\xbd{\\\\xdd\\\\x0e\\\\xbd\\\\xb6\\\\x89v\\\\xbd\\\\xe4:\\\\xb4=\\\\xf2\\\\xbf\\\\xf7<\\\\x92Sh\\\\xbc\\\\x8a\\\\xa5@<\\\\xbd\\\\x0c~<\\\\x22o\\\\xd5\\\\xbb\\\\x0b/9\\\\xbc\\\\x96Jr\\\\xbcAT_<\\\\x1f\\\\x83\\\\xc7\\\\xbc\\\\xf5\\\\xb21=\\\\x06G\\\\xb6=sh\\\\xce\\\\xbb\\\\xe0\\\\xee:\\\\xbd\\\\xa2\\\\xe6\\\\xb2<\\\\x970J\\\\xbd8\\\\xfda\\\\xbc\\\\xee\\\\x0a \\\\xbd\\\\xa2>\\\\x82<\\\\xbf^\\\\xc2\\\\xbb\\\\xed5b;\\\\xd4\\\\x18`\\\\xbaQ\\\\xe6E<,\\\\x13\\\\xb9\\\\xbb\\\\x7f\\\\xa2\\\\xbb<(\\\\x09\\\\xad=\\\\xc4\\\\x00\\\\xba<;\\\\xb3G=4\\\\xd5\\\\xc7\\\\xbcV\\\\xean\\\\xbc\\\\x05`T=\\\\x00\\\\x09d<\\\\xcc\\\\xe8\\\\xd0<9\\\\xdc\\\\xdc\\\\xbc\\\\xff\\\\x9f <g\\\\xcc\\\\x98=@\\\\xdd\\\\xb4\\\\xba\\\\x09@\\\\xbd\\\\xbc,\\\\xc5\\\\xbd\\\\xbc\\\\xeew/\\\\xbd~\\\\x8c#\\\\xbbk\\\\xc5f=O\\\\x97I<\\\\x99i5=m-\\\\x5c<4>\\\\x04=\\\\x81\\\\x84=\\\\xbd\\\\xff_\\\\x8b<\\\\xc4\\\\x9d\\\\x06<|\\\\xf5\\\\xc4\\\\xbb\\\\x8ey\\\\xcc\\\\xbdU}a;H\\\\xbe\\\\xe8\\\\xbb\\\\xf2\\\\x02\\\\xb2;\\\\xd7L\\\\x13\\\\xb9\\\\xafb0<[Y\\\\x8e<\\\\xa8\\\\xf2\\\\x8f\\\\xbbl\\\\xd8\\\\xee<r1\\\\x17\\\\x09\\\\xf3\\\\x0a\\\\xc2\\\\xbc\\\\x94\\\\xd6f\\\\xbc\\\\xce\\\\xec\\\\x94<3\\\\x0f\\\\xad:\\\\x87\\\\xa1R=\\\\xba$\\\\xb2<2Z\\\\xa7\\\\xbb=\\\\xa2\\\\x15\\\\xbd\\\\xcej\\\\x1b;\\\\x8b\\\\xc5\\\\xe5\\\\xbc\\\\x0fp\\\\xf8\\\\xbc\\\\x03\\\\xd65\\\\xbc\\\\x93\\\\xe1\\\\xd0\\\\xbb\\\\x02$\\\\x85\\\\xbc\\\\x1f\\\\xa2\\\\x94\\\\xbc\\\\xdf_]=\\\\x06A\\\\x1c=\\\\xc9\\\\xf4F=\\\\x8a3\\\\x1a;\\\\xf7 \\\\x8b<\\\\x80\\\\xca\\\\xab<\\\\xefa\\\\x80<\\\\x8c\\\\xe7\\\\xca\\\\xbcD\\\\xdaX\\\\xbd3\\\\xd4\\\\x85\\\\xbc\\\\x22Dd<\\\\x98\\\\x19!\\\\xbd\\\\x94*\\\\xc5\\\\xbc\\\\x01x\\\\xb6\\\\xbc&<\\\\x9f;^d\\\\xf5<\\\\xc2%\\\\xa8<\\\\x1a\\\\xaa!<\\\\x9d\\\\xc5\\\\xdd\\\\xbc\\\\x0d\\\\xd9+=\\\\xec\\\\xf0+\\\\xbc\\\\xaf\\\\xa81\\\\xbbd\\\\x8e]\\\\xbc\\\\xd3\\\\x01\\\\x11\\\\xbd\\\\xf6\\\\x95\\\\xc1\\\\xbdg\\\\xfd\\\\x07\\\\xbc|\\\\xd0\\\\xe8;\\\\xcd|\\\\xaa<\\\\x9c\\\\x83\\\\x22=\\\\x8aj\\\\xeb\\\\xbcn\\\\x0f\\\\x8a\\\\xbd\\\\xb1\\\\x08\\\\x96=h\\\\x81k\\\\xbd\\\\xbd\\\\xaa\\\\x0f\\\\xba?U\\\\x90\\\\xbc,\\\\xc1\\\\x8e\\\\xbc\\\\xa03\\\\x88\\\\xbcD\\\\xce\\\\x85<(9\\\\x0f=\\\\xa4\\\\xb1\\\\x22\\\\xbd\\\\xedP\\\\x22=\\\\xe8\\\\xdc\\\\xc1;/D\\\\xc3\\\\xbcfx\\\\x00\\\\xbaO\\\\x8c\\\\xa6\\\\xbb\\\\x922\\\\xfb<\\\\x8488\\\\xbd\\\\x0eN,\\\\xbd_:A=\\\\x94\\\\xbc\\\\xa4<\\\\xb9\\\\xd2\\\\xb8\\\\xbc^\\\\x16a<\\\\x85\\\\x1b\\\\x94\\\\xbd\\\\x82~;=\\\\x07qp<S#\\\\x9a<\\\\x88\\\\xaab\\\\xbbKy\\\\xac;\\\\xb4\\\\xaf\\\\xda<\\\\xcf\\\\x08<\\\\xbd\\\\xd8\\\\xf6m=\\\\x80\\\\x17T\\\\xbc\\\\x11`4=\\\\xc7P\\\\xbe;\\\\x0e\\\\x81u<C\\\\x08\\\\xf3\\\\xbcT\\\\xb1\\\\xb8;_a\\\\x9e\\\\xbc\\\\xff\\\\xf0\\\\x5c<6\\\\x9d1\\\\xbd\\\\xb1\\\\x0e\\\\x12=\\\\xa5T\\\\x08\\\\xbaT\\\\xa9\\\\x8f\\\\xbc\\\\x22\\\\xf5\\\\x1c=\\\\xfdpW<\\\\x0em\\\\xa3<\\\\xf5E\\\\x01;-\\\\xc5}=\\\\xf16\\\\xa5\\\\xb8q\\\\xa3\\\\xfa<HE\\\\xa9\\\\xbb\\\\x08pv\\\\xbdV\\\\xa81<p0t\\\\xbc\\\\x02}\\\\xe2:\\\\x9ase\\\\xbd\\\\x0a\\\\xfc\\\\x1b\\\\xbd\\\\xf2\\\\x18N<\\\\xb2U\\\\xba\\\\xbb\\\\x94\\\\x8b}=\\\\xc3\\\\x89B\\\\xbc\\\\xf5J\\\\xbf\\\\xbcqcn\\\\xbd\\\\xf5\\\\x8b\\\\xa1\\\\xbc\\\\xb3\\\\xf3\\\\x02\\\\xbd\\\\xcd\\\\x1b.<\\\\xb5\\\\x1e\\\\xfc\\\\xbc*g\\\\x9f< \\\\xd5\\\\xab=\\\\xbf6\\\\x90\\\\xbdV_\\\\x00\\\\xbd\\\\xeb\\\\x93G<\\\\xdc&\\\\x83<\\\\xc1Y4<\\\\xf5\\\\x8b$\\\\xbd\\\\xb6\\\\x1b\\\\xae<\\\\xe2/E<\\\\xf9\\\\x8e\\\\xd4<H\\\\x9d6\\\\xbc=/w\\\\xbd\\\\xaf\\\\xe4\\\\x0b\\\\xbc\\\\xad\\\\xd8\\\\xf1\\\\xbc\\\\x1b\\\\xc2\\\\x0b\\\\xbdjMS\\\\xbd\\\\x144\\\\xa5\\\\xbc\\\\x0e\\\\x07\\\\xaa\\\\xbc\\\\x82\\\\xbaX\\\\xbd\\\\xdf\\\\xd8\\\\xd1<\\\\x83P\\\\xc5<f\\\\x18V\\\\xbc\\\\x0b\\\\x90\\\\x22=\\\\x8cp6=I\\\\xa4\\\\xcd\\\\xbc\\\\xdb;\\\\x8d:\\\\xa1\\\\x89\\\\x03\\\\xbdtM\\\\xf5\\\\xbcJ<\\\\x0f\\\\xbb\\\\xbe\\\\xc93\\\\xbc\\\\xa0\\\\x0b\\\\x1d<\\\\xc6{\\\\x8d\\\\xbd}8\\\\xa9<\\\\xbeo\\\\x82\\\\xbd\\\\xb9n&=\\\\xa0L\\\\xcd<\\\\x07\\\\x196=\\\\x0b\\\\x1cV;\\\\xa1S\\\\xef\\\\xbb\\\\x04\\\\xa3\\\\x0b;\\\\xc6BJ\\\\xbc\\\\x94_\\\\x9b<\\\\xb3R\\\\xf4\\\\xbc\\\\xdf\\\\xe3:<j\\\\xcb\\\\x8e\\\\xbc\\\\xcf\\\\x16\\\\x08=[\\\\x97\\\\xa3;\\\\x1e\\\\xee\\\\xb9\\\\xbc\\\\x16\\\\x89$=W@\\\\x09=\\\\xde\\\\xfc\\\\x88\\\\xbca\\\\xf1\\\\xf2<z+\\\\x88\\\\xbdy,<\\\\xbd=\\\\x19\\\\x14\\\\xbd}\\\\x97\\\\x91<\\\\x90\\\\xe9:\\\\xbd\\\\x96tt=\\\\xc5\\\\xebk<\\\\xce\\\\x9en=\\\\x18_Z\\\\xbc\\\\xab:\\\\x85<\\\\xb8\\\\x08\\\\xa2<\\\\xc0A\\\\xd5;\\\\x8f\\\\x9f\\\\xb6<\\\\x0f;g=iQ\\\\xfb<\\\\xba\\\\xcc\\\\xf8\\\\xbb\\\\xaf\\\\x99\\\\xe9\\\\xbc\\\\xf7\\\\xd6\\\\xf4\\\\xbc\\\\xb4\\\\x96\\\\xc7\\\\xbc>\\\\xd9l\\\\xbc\\\\xcf\\\\x00\\\\xfe:*J\\\\xd0;\\\\x9e\\\\x0en\\\\xbcH\\\\xdee=S$9<u\\\\x19\\\\x9d=\\\\x17\\\\xc9\\\\xba\\\\xbav\\\\xc6\\\\xd4\\\\xbc>\\\\xce\\\\xa9\\\\xbb\\\\x81Mr\\\\xbd\\\\x90b\\\\x0a\\\\xbd\\\\x8d\\\\xbf\\\\xaf\\\\xbd\\\\x9c\\\\xfb\\\\xc5<\\\\x99\\\\xce\\\\x18=\\\\x5c\\\\xe9\\\\xd9\\\\xbb\\\\xa4\\\\xab\\\\x87\\\\xbcs\\\\x8b\\\\xa0<\\\\xdem[<\\\\xf2\\\\xac\\\\x92\\\\xbcZ,*=Wa2\\\\xbc\\\\xfeD\\\\x83\\\\xbb_\\\\x85M\\\\xbd\\\\xfbi\\\\xca<\\\\x09!\\\\xd5\\\\xbb\\\\xb1\\\\x02\\\\xee<\\\\x05\\\\x13\\\\x8c;\\\\x22\\\\x8e.\\\\xbd\\\\xd5s\\\\x08<d\\\\x8c\\\\x0e\\\\xbde_\\\\xa1<d\\\\xb8\\\\x1c\\\\xbc\\\\x92N=\\\\xbc\\\\xb5\\\\xae+=2{\\\\xb8:BP\\\\xb3\\\\xbc\\\\xbb\\\\xd7\\\\x8c\\\\xbd\\\\xcc|\\\\x89=\\\\x0c\\\\xde\\\\xe6\\\\xb7i\\\\x9c\\\\xcf<\\\\xb8\\\\xf2:=Oj\\\\xb3\\\\xbcM\\\\xe3D\\\\xbd\\\\xefZ\\\\xc7<\\\\x14\\\\xcd\\\\x86=V]\\\\xa5<\\\\xe5\\\\xbf\\\\xc1\\\\xbc\\\\x17\\\\x8cl\\\\xbdT\\\\x0f\\\\x0d=\\\\xd4/\\\\xb0\\\\xbc\\\\xb6\\\\x842<\\\\x02\\\\xa8\\\\x05=\\\\xe4\\\\xeeV\\\\xbd\\\\xe7\\\\x8d\\\\x9c=V\\\\xaf\\\\xbe\\\\xbcn\\\\xc8;<\\\\xdd\\\\x98\\\\xfe<p\\\\xdb\\\\xf0\\\\xbb\\\\x06\\\\xd0\\\\xa7\\\\xbc\\\\x1d\\\\xbf\\\\xf1<\\\\xf7g/<\\\\xc2\\\\x9f\\\\x01\\\\xbd\\\\xf3\\\\xb5\\\\x00=$\\\\xf4\\\\xee\\\\xbcu\\\\x09<\\\\xbb\\\\xf6a\\\\x8e<l\\\\x06\\\\x04=\\\\xe9\\\\xfe\\\\xe0\\\\xbcU\\\\x98h=]V\\\\x17=\\\\x22\\\\xb0\\\\xcf<\\\\xba\\\\x22+\\\\xbd\\\\xbf\\\\xb8\\\\xba\\\\xbcB5\\\\xf4<\\\\xc6\\\\xa2\\\\xe8\\\\xbc\\\\x91v0\\\\xbd\\\\xf3\\\\xb2\\\\xae<_Q\\\\xb8=\\\\xf9\\\\x9d\\\\xd3<\\\\x02\\\\x00\\\\xb6;\\\\x98=\\\\xd0\\\\xbb\\\\x94\\\\xeb\\\\xdb<C\\\\x85\\\\xf3:@\\\\xdf$=\\\\xd8\\\\xf5\\\\xbf\\\\xbc\\\\xa2\\\\xdb\\\\x04=3\\\\x10\\\\x22<\\\\x8f\\\\xc0)\\\\xbd\" ' +\n        'SORTBY score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Gender-specific product search',\n      description:\n        'Finds mountain bikes semantically similar to \"Female specific mountain bike.\" Shows how embeddings can capture product attributes like gender-specific geometry, sizing, and design features without requiring exact keyword matches.',\n      query:\n        'FT.SEARCH idx:bikes_vss ' +\n        '\"*=>[KNN 3 @description_embeddings $my_blob AS score ]\" ' +\n        'RETURN 4 score brand type description ' +\n        'PARAMS 2 my_blob \"\\\\xf5\\\\x1e\\\\xaf:7\\\\xd2\\\\x03=\\\\x90\\\\x07\\\\xd9\\\\xbc_\\\\xdf\\\\x93=\\\\x13U\\\\xb7<2\\\\xf7\\\\x14\\\\xbc\\\\xd6q\\\\x1a=^\\\\x8d\\\\xa8<\\\\xe5\\\\x9bR=\\\\x8c\\\\xc7\\\\xd2<\\\\x96\\\\xd9\\\\x0c\\\\xbb\\\\x9b/8\\\\xbd\\\\xbe\\\\x14\\\\x94=\\\\x93\\\\x05\\\\x8e\\\\xbck\\\\x02V=\\\\x09\\\\x1a<<\\\\x1a\\\\xff\\\\xe5\\\\xbb4F\\\\xbe;\\\\x10\\\\x81\\\\x9b=\\\\xb6\\\\xa0\\\\xcc\\\\xbd\\\\xaa\\\\xe9\\\\xfd\\\\xbb\\\\x8b\\\\x02\\\\xde<\\\\xcc\\\\x96>\\\\xbd!\\\\xc9G<\\\\xe5H\\\\x92<m\\\\xab|\\\\xbc\\\\xa8\\\\x7f\\\\x22=\\\\xe5\\\\xd9\\\\x17\\\\xbdFT\\\\xcf<k}\\\\x90:\\\\xf7\\\\x0a\\\\x07\\\\xbc\\\\x18R\\\\x86\\\\xbcO\\\\xfbm\\\\xbd^=j\\\\xbct\\\\x95!=\\\\xad\\\\x12\\\\xc2\\\\xbc$\\\\x03\\\\xc4\\\\xbb\\\\xf4\\\\xcb\\\\xad\\\\xbcl\\\\x9d\\\\x1e\\\\xbd\\\\xe5u\\\\xe3;0\\\\xd1\\\\xc0=\\\\x08\\\\xe5\\\\xaf\\\\xb9\\\\xe6\\\\x84\\\\xf5\\\\xbch\\\\x19\\\\x91=K+\\\\xf2\\\\xbcz\\\\x10\\\\xcf\\\\xbb\\\\xb2\\\\xd5y<\\\\xe7\\\\xfcO<\\\\x09\\\\xfc\\\\xe3\\\\xbc\\\\x80\\\\xa0\\\\xcc<\\\\x87)h;\\\\xe1\\\\xf6~\\\\xbc\\\\x7f\\\\x5c\\\\xc8\\\\xba\\\\xfc\\\\xf5\\\\x22\\\\xbd\\\\xaf<\\\\x97\\\\xbc\\\\x16\\\\xed\\\\xe0<2K\\\\x8a\\\\xbb^\\\\xfd\\\\x92\\\\xbdI\\\\x9a2\\\\xbc\\\\x83AY=\\\\xf9\\\\x10_;y\\\\xb2^=\\\\x13\\\\xb2\\\\xe8;\\\\xf8\\\\x8b\\\\x8b=\\\\xb7\\\\x8a\\\\x87=\\\\xc3\\\\x85\\\\x9f\\\\xba\\\\xaf\\\\xef!=\\\\xd9\\\\x94\\\\xcb\\\\xbb\\\\xab\\\\x96<=mrY\\\\xbc~\\\\x83\\\\x87\\\\xbb\\\\x08\\\\xc64\\\\xbd\\\\xc9\\\\xb7\\\\xd2\\\\xbcx\\\\xf6\\\\xcd;\\\\xb4$\\\\xb0\\\\xbb\\\\x11\\\\x0e\\\\xa8\\\\xbc\\\\xd5\\\\xff\\\\xbc<\\\\x86\\\\x9e\\\\xc8:\\\\x93$\\\\x93\\\\xbcDH\\\\xe2\\\\xbbQ8\\\\x8d;+\\\\xb85\\\\xbd\\\\xf4%W\\\\xb9\\\\xa0\\\\x06W\\\\xbb-\\\\xf1\\\\xad\\\\xbd\\\\xfc\\\\xb0\\\\xdd<\\\\xa6\\\\x1f\\\\x9f<\\\\x22\\\\x85 <\\\\x9d\\\\x99,=C\\\\xad\\\\x8b<Na\\\\x9d\\\\xbc\\\\xb0B\\\\xc4<\\\\xebK\\\\xf1<\\\\x1c5L=\\\\x18\\\\xd6\\\\xf7\\\\xbcb\\\\x0b\\\\x15=Y\\\\xcc \\\\xbd\\\\x80\\\\xc9};\\\\xb7\\\\xf1\\\\xe7</W+\\\\xbd?\\\\xd4=<{\\\\x80\\\\x08=^U\\\\xfd<9*\\\\xd2\\\\xbc\\\\xd9\\\\xd0\\\\x98\\\\xbc}|C=\\\\xf9,L\\\\xbd\\\\xb6_\\\\x22=\\\\xcb\\\\xb5\\\\x8d\\\\xbc\\\\xbcR\\\\x11<\\\\xd9>\\\\x80</VW=\\\\x19-\\\\x85=x\\\\x7f\\\\xdb\\\\xbc_w\\\\x8b\\\\xbd\\\\x91\\\\x0a\\\\xa1<mjL<\\\\x1c\\\\xf8\\\\x00=i\\\\xd1\\\\xc7<\\\\x0e\\\\xa6\\\\xe5\\\\xbc\\\\x1a\\\\x96\\\\xb3\\\\xbbD\\\\x81\\\\xa3;HM\\\\x82\\\\xbd\\\\x84ei<c\\\\x9b-=\\\\xa4\\\\xc4t\\\\xbd\\\\xff\\\\xc1\\\\xf8\\\\xbb\\\\xad\\\\xabE\\\\xbd\\\\x12,\\\\xa0\\\\xbc\\\\x86\\\\xa9\\\\xd2\\\\xbc\\\\xc4\\\\x83\\\\x82<\\\\x99\\\\x7f\\\\xed\\\\xbd\\\\x1de!=\\\\xa8\\\\xdca\\\\xbd\\\\x0d\\\\xa2D\\\\xbd>es=\\\\xecDN\\\\xbdm\\\\xb9i\\\\xbd\\\\x8f\\\\xdd\\\\x85\\\\xbc)|\\\\x06=\\\\xf7\\\\x13l\\\\xbd\\\\xdc\\\\x98\\\\x90\\\\xbd\\\\x86MQ\\\\xbdV\\\\xc5\\\\xbe<\\\\xc7\\\\x0cB\\\\xbb\\\\xec#\\\\x90<\\\\xdf&\\\\xe0\\\\xbc) \\\\xac:/\\\\xff\\\\x8a<z,\\\\x06\\\\xbd\\\\xe6\\\\xf5G<\\\\xc7\\\\x06\\\\xbd=\\\\xe4\\\\xe3O\\\\xbd\\\\x8f\\\\xc0\\\\x0d=\\\\xbf\\\\xef\\\\xb0<1\\\\xba\\\\xba\\\\xbc\\\\xba\\\\x17\\\\x1e\\\\xbc1\\\\xa5\\\\xc8:3\\\\x0f\\\\x16=\\\\xaf\\\\x22e:\\\\xf9\\\\x18o=NY\\\\xa1\\\\xbd\\\\x91x\\\\xa1\\\\xbc.C\\\\xb7\\\\xbc@s:=\\\\xb7\\\\xa8\\\\xf5<\\\\x1c\\\\x04&<\\\\xa3\\\\xc1\\\\xb1<L{\\\\x18=\\\\xeb\\\\x7f\\\\x03\\\\xbb\\\\x1e\\\\xfe%\\\\xbc\\\\xd8\\\\xb7\\\\x93=\\\\xa9\\\\xc7\\\\xe3\\\\xbc5\\\\x92.\\\\xbd+t\\\\xb5<\\\\xc0\\\\xa4\\\\x10=\\\\x94\\\\xf8\\\\x10\\\\xbd\\\\xbcc\\\\x88=\\\\x97\\\\xe9\\\\xb2\\\\xbb\\\\x9f\\\\xfa\\\\xc9\\\\xbc\\\\x1bW\\\\xf3\\\\xbc\\\\xc4ER\\\\xbdNI==\\\\xc5o\\\\x90\\\\xbc\\\\x22\\\\xcd\\\\x14=\\\\x7f\\\\xe4\\\\xe5<>\\\\xdf\\\\x89=\\\\xa2\\\\xb5}<\\\\xf2\\\\x16\\\\x85=\\\\xc6P\\\\x1e\\\\xbc\\\\x17!\\\\x92<\\\\xa5m\\\\xbd<qX\\\\x87\\\\xbdO\\\\x03\\\\x9e\\\\xbc\\\\xc6,N=B\\\\x9c\\\\x04\\\\xbd\\\\xcfP\\\\xce<\\\\xd7?\\\\xec\\\\xbc0\\\\xd9\\\\xb3\\\\xbc\\\\x85>6<\\\\x1e(4\\\\xbc|\\\\x8b\\\\xe2\\\\xbbp\\\\xea\\\\xca\\\\xbd\\\\xe7\\\\x999;J\\\\xc2b\\\\xbb\\\\x97\\\\xf92\\\\xbb\\\\x9d/J=n\\\\x81\\\\xce\\\\xba\\\\xd9\\\\x12/=\\\\xaf\\\\xb6\\\\x8d\\\\xbd\\\\xfd\\\\xbe\\\\x22\\\\xbdV\\\\x1d\\\\xef<\\\\xb9\\\\xaaO<\\\\xf0`\\\\x81\\\\xbdi\\\\xdc\\\\x12\\\\xbb\\\\xc3\\\\xbf\\\\x91<\\\\xc7\\\\xeb3\\\\xbckr\\\\xa0=\\\\xee\\\\xec\\\\xcd:\\\\xf3\\\\x93\\\\x80<\\\\x96\\\\x0aa;A\\\\x8a\\\\xdc;\\\\x10\\\\xdb\\\\xb1<\\\\xbat\\\\xb5\\\\xbb\\\\x1e\\\\xbbM<\\\\x1f)\\\\x0e\\\\xbc\\\\x15\\\\xfa\\\\x1d\\\\xbb\\\\xd3`\\\\xe8<\\\\xf6\\\\xc1)=\\\\xd8\\\\xd7F\\\\xbdl}+=\\\\x92\\\\x80X<\\\\xa7\\\\xbc\\\\x8f<\\\\x1e$\\\\x14\\\\xbc\\\\x8a\\\\x1b\\\\xbc<\\\\xec\\\\x1e\\\\xda\\\\xbc\\\\xa2\\\\xac\\\\x91\\\\xbc\\\\x84\\\\xcf\\\\x02=`\\\\x87\\\\xf5<\\\\xeb=\\\\xde\\\\xbc>\\\\xe5@\\\\xbdn\\\\x90B;\\\\x92\\\\xd9\\\\xd0\\\\xbc\\\\x1fn\\\\xfd<\\\\x86\\\\xde\\\\x0d=\\\\x80^v<\\\\xb0\\\\x10\\\\xfc<\\\\x16T\\\\xa0<\\\\xa1gh\\\\xbdf\\\\x1c\\\\x04<L\\\\x90\\\\x9a\\\\xbc[ow\\\\xbdI\\\\xccr\\\\xbcnl\\\\xff<>i\\\\x0a\\\\xbdZ\\\\x93\\\\xf5<]\\\\x1e\\\\x9b;dme\\\\xbc}B\\\\xb8<\\\\x03~\\\\xe7<\\\\xf0\\\\xa5z\\\\xbc\\\\xab\\\\xe5\\\\x8f:\\\\x0b\\\\x98\\\\xd2\\\\xbc\\\\x801\\\\xf1<)\\\\x83\\\\xd1\\\\xbccN\\\\xe2=\\\\x9a\\\\x11\\\\xbc<\\\\x1c\\\\xbe\\\\xd9\\\\xbc\\\\x98\\\\x0f\\\\xf6< \\\\xc9\\\\x15=&\\\\xbe\\\\xdd\\\\xbc\\\\x1c0\\\\x09\\\\xbd\\\\xb5\\\\x8e\\\\x92\\\\xbdXa\\\\x8e\\\\xba>D.\\\\xbdNBE\\\\xbd\\\\xbdq\\\\xbc;\\\\x8a\\\\x03\\\\x86\\\\xbc\\\\xe6\\\\xa2\\\\x12\\\\xbd\\\\xd9\\\\x5c\\\\x15\\\\xbd\\\\x83\\\\xca\\\\xd4\\\\xbc\\\\x91\\\\x0dV<Ov\\\\x93=\\\\x10_M\\\\xbcu\\\\xfa\\\\x00\\\\xbcj_\\\\x11=\\\\xe6dR\\\\xbd%\\\\x01\\\\x94<\\\\xac\\\\xc3:\\\\xbdN\\\\xbb\\\\x11=_\\\\xcd\\\\x81<\\\\x18\\\\x8b\\\\x90\\\\xbd\\\\x9e\\\\x86\\\\x22<s}\\\\x97<\\\\xb0\\\\x80\\\\xd2\\\\xbb\\\\x88\\\\xa5\\\\xf9\\\\xbc\\\\x8a\\\\x8a\\\\xa1;\\\\x89\\\\xd40=\\\\xea}\\\\xdb\\\\xb9\\\\xd1\\\\x08\\\\xee\\\\xbc\\\\xe4\\\\xcd\\\\xe8\\\\xbb\\\\x13^=\\\\xbc\\\\x91\\\\x81\\\\x01\\\\xbd\\\\x0f\\\\xad\\\\x04\\\\xbd\\\\xf5\\\\xef@=\\\\xa0\\\\x95\\\\x1e<\\\\xc74\\\\xea\\\\xbc\\\\x9f\\\\xd2\\\\xfc<\\\\xbb\\\\xc8\\\\xb6<gO\\\\x0d<\\\\xc7a\\\\xbb\\\\xbc/\\\\xdf4\\\\xbd\\\\xfe>\\\\xa9\\\\xbcTB\\\\xe1<\\\\xb6\\\\xd7\\\\xb4<\\\\xb9\\\\xb9b=\\\\x5c\\\\xa4l\\\\xbc\\\\xeb\\\\xea:=\\\\x98\\\\x84\\\\xed;*\\\\x01\\\\xef\\\\xbc\\\\xf5$a;\\\\xaa\\\\xe8+=\\\\xf1\\\\xa0 =\\\\xd8\\\\x8b\\\\x03\\\\xbd\\\\x96\\\\xd5#=g\\\\x1f\\\\x91\\\\xbd\\\\xc4\\\\x02\\\\x05\\\\xbc\\\\x94\\\\xd4\\\\x92\\\\xbdS^\\\\x92\\\\xbcs\\\\xe6\\\\xe4<e\\\\xbeT\\\\xbbF\\\\xda\\\\x1a\\\\xbd\\\\x18.O<[\\\\xd1\\\\x89=kt\\\\xe6\\\\xb9L\\\\xa6\\\\x09=\\\\xca\\\\x137\\\\xbc\\\\x8d\\\\xf3\\\\x00\\\\xbdX\\\\xad\\\\x04=fOC\\\\xbcf\\\\xabp\\\\xbc/\\\\x0e2\\\\xbc\\\\x04\\\\x80a=o\\\\x224\\\\xbaqT\\\\x0a\\\\xbd?\\\\xf7e=\\\\x80\\\\x97\\\\xc2<e\\\\xfed\\\\xbd\\\\x144\\\\x0b=kj\\\\x04\\\\xbc\\\\xc3\\\\xbb\\\\xdc\\\\xbc\\\\xb3\\\\xbc\\\\x93\\\\xbd\\\\x94>J;L\\\\xd8\\\\xd5\\\\xbc\\\\xda$0\\\\xbc\\\\x15#H\\\\xbd\\\\xd9\\\\x7fI=V5\\\\x00;\\\\xd4\\\\x80J;\\\\x86R\\\\x9c\\\\xbdI\\\\xb6\\\\xa6:P\\\\xb8\\\\xe2\\\\xbc\\\\x95tN\\\\xbd\\\\x0f\\\\x19\\\\x85=\\\\xa8\\\\xa4\\\\x86=^\\\\x10\\\\x81<\\\\x9fO\\\\x0d=]\\\\x01\\\\x1c=\\\\x0f\\\\x1d\\\\x02\\\\xbd&)\\\\xe4\\\\xbc\\\\xfb\\\\xfc\\\\x02=\\\\xec\\\\x11\\\\xdb<8\\\\x83d\\\\xbc\\\\x8dFC\\\\xbd\\\\xfd0`\\\\xbd\\\\x22*\\\\x8d;\\\\xd0\\\\x09m\\\\xbc\\\\xac\\\\x0b\\\\x7f\\\\xbdn\\\\xf3\\\\xd3\\\\xbc\\\\xad1\\\\x03<JPG;A\\\\xcf\\\\x85;\\\\xf5\\\\xeb\\\\x05=5M\\\\xe8\\\\xbc\\\\xa6\\\\x1c\\\\x1c\\\\xbd\\\\xa1?\\\\x22=+B\\\\xdf<,\\\\xd0\\\\x8c<Y\\\\x0e\\\\xc6=3\\\\xf9\\\\xb4\\\\xbc\\\\x22\\\\x93<\\\\xbd\\\\xf3\\\\x96-=7\\\\x95\\\\x9b<\\\\xa8T\\\\xec\\\\xbc\\\\xc6\\\\xc36<\\\\x12\\\\x0e\\\\x1c;A\\\\xc4\\\\xbd\\\\xbc\\\\xd7\\\\x09Q;+\\\\xf67\\\\xbb\\\\xa3\\\\xa0\\\\x92\\\\xbd\\\\x84\\\\x94\\\\x1f=X3\\\\xa9\\\\xbc\\\\xc4\\\\x8d\\\\x88\\\\xbc\\\\x05\\\\xd9~=\\\\xddo\\\\xb09lN\\\\xd1\\\\xbc\\\\x95-9<\\\\x00\\\\xcd\\\\x5c<\\\\x1c\\\\x95l\\\\xbbk\\\\x91.<=\\\\xe2\\\\x9f=aX]\\\\xbc\\\\xb4\\\\xf2\\\\x80=AT6<\\\\x1b\\\\x99A\\\\xbc\\\\x9c\\\\xe4q\\\\xbc\\\\xe1\\\\xf4\\\\x93;\\\\x11\\\\xb65\\\\xbd\\\\xe2/\\\\xaa\\\\xbc\\\\xbdQ\\\\xaa\\\\xbd\\\\x06\\\\x81\\\\x0c=={\\\\xe8\\\\xbc\\\\xb4\\\\xe3\\\\x90<\\\\xd3F\\\\x1e=H\\\\xfd\\\\xb0=\\\\x1ef\\\\xc1;o\\\\xc1\\\\x90<\\\\xdd\\\\xbe\\\\x93=~m\\\\x89=UTw\\\\xbc\\\\xe1y\\\\x9e=\\\\xe5b\\\\xa1\\\\xbc\\\\x1f\\\\xf4\\\\xa6\\\\xbd\\\\x9a\\\\xd1\\\\xd1\\\\xbc\\\\x82\\\\xf1\\\\x02<Hff=\\\\x12p\\\\x8d\\\\xbb=kH\\\\xbb\\\\xde\\\\xb4\\\\xc4\\\\xb8P\\\\x93E=\\\\x89Y\\\\x14=G\\\\xcer:_\\\\xa4$\\\\xbb,\\\\x8f2\\\\xbda\\\\xd7\\\\x15\\\\xbd%M@;P\\\\xd6h;\\\\xc3\\\\x8aC\\\\xbd\\\\xeePQ\\\\xbd\\\\xc8m\\\\x9f\\\\xbc\\\\xd7p\\\\x9a<\\\\xe8\\\\xf6\\\\xab;\\\\x11\\\\xa7\\\\xab<\\\\x96j\\\\xac\\\\xbc/S\\\\x7f<<\\\\x9f\\\\xc1<E\\\\xf7\\\\xa1\\\\xbc`xo<\\\\xd8\\\\x81\\\\x22\\\\xbc\\\\x8f\\\\x12k=\\\\x88\\\\xab==\\\\xff1\\\\xb1<\\\\x95\\\\x8a>\\\\xbb|M\\\\x98\\\\xbcZq\\\\x16\\\\xbd/\\\\xd3\\\\x97<(;\\\\x0c\\\\xbd\\\\x0a\\\\xff0=\\\\x93\\\\xb7\\\\xb2\\\\xbc\\\\x8f\\\\x90\\\\xb8;\\\\x92(\\\\x98\\\\xbc\\\\xf3\\\\x1f\\\\xe7<9=\\\\x13\\\\xbcQ\\\\x83\\\\x8d\\\\xbdF\\\\xf1e<\\\\xb2\\\\xc6V=\\\\xbdR};\\\\xbd\\\\x12\\\\xb3\\\\xbcu\\\\x0b\\\\x99=\\\\x82\\\\xd4\\\\xbf\\\\xbc\\\\xcd\\\\xf3\\\\xf0<\\\\xdd\\\\x8a\\\\xc7;\\\\x02\\\\xab\\\\x8e\\\\xbb\\\\xb3GO=\\\\xb280=\\\\x8bw\\\\x15\\\\xbd\\\\xa8\\\\xf7<\\\\xbc\\\\x16v\\\\x1d\\\\xbdX\\\\x99\\\\xbb\\\\xbc\\\\xa9K\\\\x03=\\\\x9dh.\\\\xbd[U\\\\x92<\\\\xd3\\\\xe1\\\\xab\\\\xbb\\\\xc1W\\\\xb2:\\\\x0fl\\\\x0d\\\\x09\\\\x17\\\\x80\\\\xb6\\\\xbdK\\\\xe7\\\\x0a\\\\xbdl\\\\xd0\\\\xb9<\\\\x83x\\\\xd5\\\\xba\\\\xd9\\\\xb2 \\\\xbcm\\\\xbf\\\\xcf\\\\xbc\\\\xe5,@\\\\xbcL\\\\xad\\\\x18=\\\\x1e\\\\xe9\\\\x0a\\\\xbc\\\\xed\\\\xbc\\\\xa4<\\\\xa7KS\\\\xbb\\\\xa5\\\\xe3O=\\\\xf8*X;\\\\x0e\\\\x9b\\\\x95=\\\\xb72\\\\x84\\\\xbc\\\\xf9\\\\xff\\\\x5c<\\\\xf1\\\\xc5\\\\x93\\\\xbaCk>\\\\xbdAw\\\\xe0<\\\\xa2\\\\xd6\\\\xe3<\\\\xde\\\\xe0\\\\x86=~}\\\\x81=\\\\xbe~u\\\\xbdH\\\\xcb6\\\\xbd\\\\x12\\\\x9f2=\\\\xc6\\\\xf16=a^\\\\xd3\\\\xbc\\\\xed\\\\x04\\\\x809\\\\xe7\\\\x8f\\\\x07<\\\\xb0\\\\x9fW\\\\xbb<\\\\xf3\\\\x16\\\\xbd\\\\x9a\\\\x98\\\\xfc<\\\\xf8\\\\xaf\\\\xe1\\\\xbc\\\\xee\\\\xb6!\\\\xbd\\\\xda\\\\xeb\\\\x96\\\\xbcn\\\\x22\\\\x7f<\\\\xf8j\\\\x8c<z\\\\xb5\\\\x80<\\\\xda\\\\x0e\\\\xa2<\\\\x9d\\\\xeco<O \\\\xbb<Ic\\\\x18\\\\xbc\\\\x0a\\\\xd4\\\\x0d\\\\xbd\\\\xc8\\\\x95\\\\xf7<\\\\xc2\\\\x92\\\\x10=\\\\xbai\\\\xd6\\\\xbd\\\\x9d\\\\xd7@=\\\\xf7w\\\\x01\\\\xbc\\\\x14mW\\\\xbc\\\\xb7&\\\\x84\\\\xbc0t\\\\x099\\\\xb7V\\\\x18\\\\xbe\\\\x87!\\\\xa7<\\\\xa59D=\\\\x1b2\\\\x01\\\\xbdE-b\\\\xbc\\\\xd2KW\\\\xbd\\\\x0a\\\\xa3.\\\\xbd\\\\xe9-\\\\x04\\\\xbd8u\\\\x1f<\\\\xe7\\\\xbe\\\\x8d\\\\xbc\\\\xff\\\\xca\\\\x7f\\\\xbc\\\\xb8\\\\x03\\\\x92\\\\xba\\\\x22\\\\xe6\\\\xa5\\\\xbbU[Q\\\\xbc6\\\\xec\\\\x1e\\\\xb9\\\\x93\\\\x80\\\\xbe\\\\xbc\\\\xa4>\\\\xc29j\\\\x9fc=L\\\\xb5\\\\x00\\\\xbbI\\\\x90\\\\xcb\\\\xbc\\\\xfdi\\\\x81<\\\\xbbx\\\\x02\\\\xbc\\\\xfb\\\\x0f\\\\xa1\\\\xbb\\\\x13\\\\xdf\\\\x97\\\\xbc\\\\xa2t\\\\x8f\\\\xbc\\\\x99jS\\\\xbd\\\\xdf\\\\xb6\\\\xbf<t\\\\xf3+\\\\xbdxN\\\\xa0<\\\\x18\\\\xe2\\\\x1d<t\\\\xf6\\\\xb4\\\\xba9\\\\xb1\\\\xd8\\\\xbc\\\\xac\\\\x82;=\\\\x91o\\\\x0e\\\\xbd\\\\xe7\\\\xa4\\\\x19=\\\\x11s3=\\\\x93@\\\\xbc\\\\xbb\\\\xec\\\\xda\\\\xe3;\\\\xbe\\\\xf8\\\\xff;k\\\\x9a\\\\xac\\\\xba\\\\xc0\\\\xb4W\\\\xbd\\\\xf3*\\\\xf4<Hl\\\\x8f\\\\xba],7=\\\\xef\\\\xce\\\\xfa<\\\\xa8\\\\xe6\\\\xe1\\\\xbc\\\\xe0oT\\\\xbd\\\\x1fx\\\\x8c=p\\\\xa2\\\\xaa;\\\\x15\\\\x87\\\\xe1\\\\xbd\\\\xcf\\\\xc1\\\\xbb\\\\xbcQ\\\\xacg<5\\\\x22 \\\\xbdpr\\\\xfb\\\\xbc@\\\\xc0\\\\x95\\\\xbcd\\\\xe9\\\\xff\\\\xbb_\\\\xa4\\\\x80\\\\xbc\\\\xe5W\\\\xb4\\\\xbb\\\\xe8\\\\xcf\\\\xd8\\\\xbc\\\\x7f\\\\xf7 =\\\\x8b\\\\x1a\\\\x0b\\\\xbd|A\\\\x81\\\\xbd\\\\x1blN=r\\\\x1d\\\\xbb<|\\\\x02\\\\xb6\\\\xbb\\\\xeaQ4<\\\\x15\\\\xd7\\\\xa0<i\\\\xb7\\\\x89\\\\xbca\\\\xe4\\\\xc9\\\\xbb\\\\x22\\\\x8e]=\\\\xf2\\\\xbd3=\\\\xc5*-=\\\\xa1F\\\\xab<\\\\xf7Z\\\\xc7<+|:<@g\\\\xfe\\\\xbb8+\\\\xc7<\\\\xd2\\\\xf5%=\\\\x86\\\\x8d(\\\\xbcq\\\\xd8y<\\\\xb4\\\\xf0\\\\x95\\\\xbd\\\\xb8\\\\xf0Y=\\\\x8c+\\\\x82<\\\\xd6i\\\\xa6\\\\xbc\\\\x95q3=\\\\xcacL<\\\\x8f\\\\x80]\\\\xbd\\\\x8f\\\\x8e\\\\x94;w\\\\x84\\\\x8e<\\\\xa2\\\\xad\\\\x8e<\\\\xad\\\\xe6z\\\\xbd\\\\x17/\\\\x08\\\\xbd0\\\\xf3\\\\x84<\\\\xdb\\\\xb9\\\\xd9:P@\\\\x18<?NZ\\\\xbc\\\\xf6xX\\\\xbbcU\\\\x22=(Z\\\\xaa\\\\xbcMX\\\\x98\\\\xbc\\\\xebL\\\\x01\\\\xbc\\\\xa3\\\\xea\\\\xd7\\\\xbc\\\\xf4h4\\\\xbc\\\\xe0C\\\\xdb<\\\\xe56#\\\\xbc\\\\xe3\\\\xe4\\\\x06\\\\xbd\\\\xe49\\\\x85\\\\xbc\\\\x16\\\\x1d\\\\xb1=\\\\xd6Xv=\\\\x7f\\\\x94\\\\x85\\\\xbd\\\\xed\\\\xa42<[\\\\x1b\\\\x1f\\\\xbdv\\\\x83C\\\\xbc\\\\xc0\\\\x99c=\\\\xf1t~\\\\xbdgMe\\\\xbd\\\\x11j\\\\xef\\\\xbc\\\\x1d\\\\x9c\\\\xe0:\\\\x9d\\\\xae\\\\xb2\\\\xbc\\\\xf6|\\\\x1a=;\\\\xe0\\\\x01=T\\\\xb1\\\\xad\\\\xba\\\\xba{1\\\\xbc\\\\x09\\\\xc2\\\\x1a\\\\xbb=\\\\xe1\\\\x86;\\\\x22He=gb\\\\xb9\\\\xbb2G:=o\\\\xd2\\\\xe9\\\\xbb0-j\\\\xbd\\\\xf1\\\\x81.\\\\xbd\\\\xfb\\\\x8a\\\\xf8\\\\xbac\\\\xe6\\\\xaf\\\\xbc\\\\x1e\\\\xb7\\\\x0d\\\\xbd\\\\x04j\\\\xc1\\\\xbc\\\\xf1\\\\x12\\\\x96\\\\xbd:\\\\xc0G<v\\\\x13\\\\xe5<\\\\x88\\\\xdd\\\\x8d\\\\xbc\\\\xfc\\\\x16Y=\\\\xc9]u=\\\\xe4\\\\x1b0\\\\xbb\\\\xd8D\\\\xd0<\\\\x844t;7\\\\xd9\\\\x16\\\\xbdp\\\\x84\\\\xf9\\\\xbcv\\\\xc9j<\\\\xbbG\\\\xe4\\\\xbc\\\\x22N\\\\x8a=/\\\\xaa-\\\\xbd\\\\xd5l\\\\xca\\\\xbb\\\\xe6\\\\xce\\\\xb8;\\\\x0a\\\\x02+9\\\\xcb\\\\xcf\\\\xa5\\\\xbd\\\\xa0\\\\x93\\\\xa0\\\\xbcK-\\\\xc1<<L*\\\\xbd*\\\\x03\\\\xd6;\\\\x86\\\\xa3:\\\\xbd\\\\x87\\\\xa4\\\\x9d\\\\xbc\\\\xf1\\\\xd6\\\\xfb\\\\xbcB\\\\x9b\\\\xea\\\\xbc\\\\xbe\\\\x81a\\\\xbc\\\\xa5\\\\xcd\\\\xfc\\\\xbb\\\\x0d\\\\x0c\\\\xab\\\\xbc\\\\xb2\\\\xceT=*-\\\\xd4<\\\\xd3zd<\\\\xda\\\\xa9\\\\xa2\\\\xbc\\\\x11K\\\\x11<\\\\xe8\\\\xdf\\\\xca\\\\xbbtV&=\\\\x1a\\\\xea\\\\x11=\\\\xf3\\\\xd8\\\\x80=\\\\x95\\\\xdf\\\\x03=\\\\x03\\\\xccS<u4\\\\xa19\\\\xc0K.\\\\xbd!\\\\x04j=\\\\x08\\\\xdb=\\\\xbd\\\\x7f&|\\\\xbd\\\\xa3}\\\\x96<\\\\xf0\\\\xccb\\\\xbct\\\\xf9\\\\xb7\\\\xbb\\\\x00}\\\\x1c\\\\xbd(\\\\x9cI;\\\\x10OM<\\\\xd3\\\\xe7==\\\\xbc@\\\\xa1\\\\xbc/l\\\\xde\\\\xbc\\\\x8b\\\\xaaB=\\\\xa5\\\\x22\\\\xe8;\\\\x1b\\\\x0f\\\\x05<\\\\xd6\\\\x84\\\\x0b=\\\\xa3\\\\xe9g\\\\xbc\\\\x1e\\\\xcb#\\\\xbc_\\\\xca\\\\x89\\\\xbcz\\\\x9a\\\\x90\\\\xbcB\\\\x81o<zS\\\\x98=ar\\\\x06;\\\\xb7l\\\\xc7<3&\\\\x11<\\\\x93\\\\x1c\\\\xc5\\\\xbb f\\\\x1c\\\\xbb\\\\xa2\\\\xd8i\\\\xbd\\\\xcfm\\\\xd0\\\\xbc,1\\\\x80\\\\xbd 0\\\\xed\\\\xbc\\\\x98\\\\x1f.=\\\\x8f\\\\xd7\\\\xe6<\\\\x8dD\\\\xe6<\\\\x80$\\\\x8d\\\\xbc[L`=\\\\x84gE;\\\\xb5\\\\x88\\\\xa5<\\\\xcfTL=\\\\x1e\\\\xef\\\\xc5\\\\xbcz\\\\x06\\\\x8c=\\\\x91\\\\xea\\\\xef<\\\\xa9D\\\\x89=\\\\xa2\\\\xe9W\\\\xbd\" ' +\n        'SORTBY score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Hybrid search (vector + filters)',\n      description:\n        'Combines semantic vector search with traditional attribute filtering. Searches for \"Female specific mountain bike\" but restricts results to bikes of type \"Mountain Bikes\" with prices between $3,000–$3,500. Demonstrates pre-filtering before KNN to narrow the candidate set.',\n      query:\n        'FT.SEARCH idx:bikes_vss ' +\n        '\"(@type:{Mountain Bikes} @price:[3000 3500])=>[KNN 3 @description_embeddings $my_blob AS score ]\" ' +\n        'RETURN 5 score brand type price description ' +\n        'PARAMS 2 my_blob \"\\\\xf5\\\\x1e\\\\xaf:7\\\\xd2\\\\x03=\\\\x90\\\\x07\\\\xd9\\\\xbc_\\\\xdf\\\\x93=\\\\x13U\\\\xb7<2\\\\xf7\\\\x14\\\\xbc\\\\xd6q\\\\x1a=^\\\\x8d\\\\xa8<\\\\xe5\\\\x9bR=\\\\x8c\\\\xc7\\\\xd2<\\\\x96\\\\xd9\\\\x0c\\\\xbb\\\\x9b/8\\\\xbd\\\\xbe\\\\x14\\\\x94=\\\\x93\\\\x05\\\\x8e\\\\xbck\\\\x02V=\\\\x09\\\\x1a<<\\\\x1a\\\\xff\\\\xe5\\\\xbb4F\\\\xbe;\\\\x10\\\\x81\\\\x9b=\\\\xb6\\\\xa0\\\\xcc\\\\xbd\\\\xaa\\\\xe9\\\\xfd\\\\xbb\\\\x8b\\\\x02\\\\xde<\\\\xcc\\\\x96>\\\\xbd!\\\\xc9G<\\\\xe5H\\\\x92<m\\\\xab|\\\\xbc\\\\xa8\\\\x7f\\\\x22=\\\\xe5\\\\xd9\\\\x17\\\\xbdFT\\\\xcf<k}\\\\x90:\\\\xf7\\\\x0a\\\\x07\\\\xbc\\\\x18R\\\\x86\\\\xbcO\\\\xfbm\\\\xbd^=j\\\\xbct\\\\x95!=\\\\xad\\\\x12\\\\xc2\\\\xbc$\\\\x03\\\\xc4\\\\xbb\\\\xf4\\\\xcb\\\\xad\\\\xbcl\\\\x9d\\\\x1e\\\\xbd\\\\xe5u\\\\xe3;0\\\\xd1\\\\xc0=\\\\x08\\\\xe5\\\\xaf\\\\xb9\\\\xe6\\\\x84\\\\xf5\\\\xbch\\\\x19\\\\x91=K+\\\\xf2\\\\xbcz\\\\x10\\\\xcf\\\\xbb\\\\xb2\\\\xd5y<\\\\xe7\\\\xfcO<\\\\x09\\\\xfc\\\\xe3\\\\xbc\\\\x80\\\\xa0\\\\xcc<\\\\x87)h;\\\\xe1\\\\xf6~\\\\xbc\\\\x7f\\\\x5c\\\\xc8\\\\xba\\\\xfc\\\\xf5\\\\x22\\\\xbd\\\\xaf<\\\\x97\\\\xbc\\\\x16\\\\xed\\\\xe0<2K\\\\x8a\\\\xbb^\\\\xfd\\\\x92\\\\xbdI\\\\x9a2\\\\xbc\\\\x83AY=\\\\xf9\\\\x10_;y\\\\xb2^=\\\\x13\\\\xb2\\\\xe8;\\\\xf8\\\\x8b\\\\x8b=\\\\xb7\\\\x8a\\\\x87=\\\\xc3\\\\x85\\\\x9f\\\\xba\\\\xaf\\\\xef!=\\\\xd9\\\\x94\\\\xcb\\\\xbb\\\\xab\\\\x96<=mrY\\\\xbc~\\\\x83\\\\x87\\\\xbb\\\\x08\\\\xc64\\\\xbd\\\\xc9\\\\xb7\\\\xd2\\\\xbcx\\\\xf6\\\\xcd;\\\\xb4$\\\\xb0\\\\xbb\\\\x11\\\\x0e\\\\xa8\\\\xbc\\\\xd5\\\\xff\\\\xbc<\\\\x86\\\\x9e\\\\xc8:\\\\x93$\\\\x93\\\\xbcDH\\\\xe2\\\\xbbQ8\\\\x8d;+\\\\xb85\\\\xbd\\\\xf4%W\\\\xb9\\\\xa0\\\\x06W\\\\xbb-\\\\xf1\\\\xad\\\\xbd\\\\xfc\\\\xb0\\\\xdd<\\\\xa6\\\\x1f\\\\x9f<\\\\x22\\\\x85 <\\\\x9d\\\\x99,=C\\\\xad\\\\x8b<Na\\\\x9d\\\\xbc\\\\xb0B\\\\xc4<\\\\xebK\\\\xf1<\\\\x1c5L=\\\\x18\\\\xd6\\\\xf7\\\\xbcb\\\\x0b\\\\x15=Y\\\\xcc \\\\xbd\\\\x80\\\\xc9};\\\\xb7\\\\xf1\\\\xe7</W+\\\\xbd?\\\\xd4=<{\\\\x80\\\\x08=^U\\\\xfd<9*\\\\xd2\\\\xbc\\\\xd9\\\\xd0\\\\x98\\\\xbc}|C=\\\\xf9,L\\\\xbd\\\\xb6_\\\\x22=\\\\xcb\\\\xb5\\\\x8d\\\\xbc\\\\xbcR\\\\x11<\\\\xd9>\\\\x80</VW=\\\\x19-\\\\x85=x\\\\x7f\\\\xdb\\\\xbc_w\\\\x8b\\\\xbd\\\\x91\\\\x0a\\\\xa1<mjL<\\\\x1c\\\\xf8\\\\x00=i\\\\xd1\\\\xc7<\\\\x0e\\\\xa6\\\\xe5\\\\xbc\\\\x1a\\\\x96\\\\xb3\\\\xbbD\\\\x81\\\\xa3;HM\\\\x82\\\\xbd\\\\x84ei<c\\\\x9b-=\\\\xa4\\\\xc4t\\\\xbd\\\\xff\\\\xc1\\\\xf8\\\\xbb\\\\xad\\\\xabE\\\\xbd\\\\x12,\\\\xa0\\\\xbc\\\\x86\\\\xa9\\\\xd2\\\\xbc\\\\xc4\\\\x83\\\\x82<\\\\x99\\\\x7f\\\\xed\\\\xbd\\\\x1de!=\\\\xa8\\\\xdca\\\\xbd\\\\x0d\\\\xa2D\\\\xbd>es=\\\\xecDN\\\\xbdm\\\\xb9i\\\\xbd\\\\x8f\\\\xdd\\\\x85\\\\xbc)|\\\\x06=\\\\xf7\\\\x13l\\\\xbd\\\\xdc\\\\x98\\\\x90\\\\xbd\\\\x86MQ\\\\xbdV\\\\xc5\\\\xbe<\\\\xc7\\\\x0cB\\\\xbb\\\\xec#\\\\x90<\\\\xdf&\\\\xe0\\\\xbc) \\\\xac:/\\\\xff\\\\x8a<z,\\\\x06\\\\xbd\\\\xe6\\\\xf5G<\\\\xc7\\\\x06\\\\xbd=\\\\xe4\\\\xe3O\\\\xbd\\\\x8f\\\\xc0\\\\x0d=\\\\xbf\\\\xef\\\\xb0<1\\\\xba\\\\xba\\\\xbc\\\\xba\\\\x17\\\\x1e\\\\xbc1\\\\xa5\\\\xc8:3\\\\x0f\\\\x16=\\\\xaf\\\\x22e:\\\\xf9\\\\x18o=NY\\\\xa1\\\\xbd\\\\x91x\\\\xa1\\\\xbc.C\\\\xb7\\\\xbc@s:=\\\\xb7\\\\xa8\\\\xf5<\\\\x1c\\\\x04&<\\\\xa3\\\\xc1\\\\xb1<L{\\\\x18=\\\\xeb\\\\x7f\\\\x03\\\\xbb\\\\x1e\\\\xfe%\\\\xbc\\\\xd8\\\\xb7\\\\x93=\\\\xa9\\\\xc7\\\\xe3\\\\xbc5\\\\x92.\\\\xbd+t\\\\xb5<\\\\xc0\\\\xa4\\\\x10=\\\\x94\\\\xf8\\\\x10\\\\xbd\\\\xbcc\\\\x88=\\\\x97\\\\xe9\\\\xb2\\\\xbb\\\\x9f\\\\xfa\\\\xc9\\\\xbc\\\\x1bW\\\\xf3\\\\xbc\\\\xc4ER\\\\xbdNI==\\\\xc5o\\\\x90\\\\xbc\\\\x22\\\\xcd\\\\x14=\\\\x7f\\\\xe4\\\\xe5<>\\\\xdf\\\\x89=\\\\xa2\\\\xb5}<\\\\xf2\\\\x16\\\\x85=\\\\xc6P\\\\x1e\\\\xbc\\\\x17!\\\\x92<\\\\xa5m\\\\xbd<qX\\\\x87\\\\xbdO\\\\x03\\\\x9e\\\\xbc\\\\xc6,N=B\\\\x9c\\\\x04\\\\xbd\\\\xcfP\\\\xce<\\\\xd7?\\\\xec\\\\xbc0\\\\xd9\\\\xb3\\\\xbc\\\\x85>6<\\\\x1e(4\\\\xbc|\\\\x8b\\\\xe2\\\\xbbp\\\\xea\\\\xca\\\\xbd\\\\xe7\\\\x999;J\\\\xc2b\\\\xbb\\\\x97\\\\xf92\\\\xbb\\\\x9d/J=n\\\\x81\\\\xce\\\\xba\\\\xd9\\\\x12/=\\\\xaf\\\\xb6\\\\x8d\\\\xbd\\\\xfd\\\\xbe\\\\x22\\\\xbdV\\\\x1d\\\\xef<\\\\xb9\\\\xaaO<\\\\xf0`\\\\x81\\\\xbdi\\\\xdc\\\\x12\\\\xbb\\\\xc3\\\\xbf\\\\x91<\\\\xc7\\\\xeb3\\\\xbckr\\\\xa0=\\\\xee\\\\xec\\\\xcd:\\\\xf3\\\\x93\\\\x80<\\\\x96\\\\x0aa;A\\\\x8a\\\\xdc;\\\\x10\\\\xdb\\\\xb1<\\\\xbat\\\\xb5\\\\xbb\\\\x1e\\\\xbbM<\\\\x1f)\\\\x0e\\\\xbc\\\\x15\\\\xfa\\\\x1d\\\\xbb\\\\xd3`\\\\xe8<\\\\xf6\\\\xc1)=\\\\xd8\\\\xd7F\\\\xbdl}+=\\\\x92\\\\x80X<\\\\xa7\\\\xbc\\\\x8f<\\\\x1e$\\\\x14\\\\xbc\\\\x8a\\\\x1b\\\\xbc<\\\\xec\\\\x1e\\\\xda\\\\xbc\\\\xa2\\\\xac\\\\x91\\\\xbc\\\\x84\\\\xcf\\\\x02=`\\\\x87\\\\xf5<\\\\xeb=\\\\xde\\\\xbc>\\\\xe5@\\\\xbdn\\\\x90B;\\\\x92\\\\xd9\\\\xd0\\\\xbc\\\\x1fn\\\\xfd<\\\\x86\\\\xde\\\\x0d=\\\\x80^v<\\\\xb0\\\\x10\\\\xfc<\\\\x16T\\\\xa0<\\\\xa1gh\\\\xbdf\\\\x1c\\\\x04<L\\\\x90\\\\x9a\\\\xbc[ow\\\\xbdI\\\\xccr\\\\xbcnl\\\\xff<>i\\\\x0a\\\\xbdZ\\\\x93\\\\xf5<]\\\\x1e\\\\x9b;dme\\\\xbc}B\\\\xb8<\\\\x03~\\\\xe7<\\\\xf0\\\\xa5z\\\\xbc\\\\xab\\\\xe5\\\\x8f:\\\\x0b\\\\x98\\\\xd2\\\\xbc\\\\x801\\\\xf1<)\\\\x83\\\\xd1\\\\xbccN\\\\xe2=\\\\x9a\\\\x11\\\\xbc<\\\\x1c\\\\xbe\\\\xd9\\\\xbc\\\\x98\\\\x0f\\\\xf6< \\\\xc9\\\\x15=&\\\\xbe\\\\xdd\\\\xbc\\\\x1c0\\\\x09\\\\xbd\\\\xb5\\\\x8e\\\\x92\\\\xbdXa\\\\x8e\\\\xba>D.\\\\xbdNBE\\\\xbd\\\\xbdq\\\\xbc;\\\\x8a\\\\x03\\\\x86\\\\xbc\\\\xe6\\\\xa2\\\\x12\\\\xbd\\\\xd9\\\\x5c\\\\x15\\\\xbd\\\\x83\\\\xca\\\\xd4\\\\xbc\\\\x91\\\\x0dV<Ov\\\\x93=\\\\x10_M\\\\xbcu\\\\xfa\\\\x00\\\\xbcj_\\\\x11=\\\\xe6dR\\\\xbd%\\\\x01\\\\x94<\\\\xac\\\\xc3:\\\\xbdN\\\\xbb\\\\x11=_\\\\xcd\\\\x81<\\\\x18\\\\x8b\\\\x90\\\\xbd\\\\x9e\\\\x86\\\\x22<s}\\\\x97<\\\\xb0\\\\x80\\\\xd2\\\\xbb\\\\x88\\\\xa5\\\\xf9\\\\xbc\\\\x8a\\\\x8a\\\\xa1;\\\\x89\\\\xd40=\\\\xea}\\\\xdb\\\\xb9\\\\xd1\\\\x08\\\\xee\\\\xbc\\\\xe4\\\\xcd\\\\xe8\\\\xbb\\\\x13^=\\\\xbc\\\\x91\\\\x81\\\\x01\\\\xbd\\\\x0f\\\\xad\\\\x04\\\\xbd\\\\xf5\\\\xef@=\\\\xa0\\\\x95\\\\x1e<\\\\xc74\\\\xea\\\\xbc\\\\x9f\\\\xd2\\\\xfc<\\\\xbb\\\\xc8\\\\xb6<gO\\\\x0d<\\\\xc7a\\\\xbb\\\\xbc/\\\\xdf4\\\\xbd\\\\xfe>\\\\xa9\\\\xbcTB\\\\xe1<\\\\xb6\\\\xd7\\\\xb4<\\\\xb9\\\\xb9b=\\\\x5c\\\\xa4l\\\\xbc\\\\xeb\\\\xea:=\\\\x98\\\\x84\\\\xed;*\\\\x01\\\\xef\\\\xbc\\\\xf5$a;\\\\xaa\\\\xe8+=\\\\xf1\\\\xa0 =\\\\xd8\\\\x8b\\\\x03\\\\xbd\\\\x96\\\\xd5#=g\\\\x1f\\\\x91\\\\xbd\\\\xc4\\\\x02\\\\x05\\\\xbc\\\\x94\\\\xd4\\\\x92\\\\xbdS^\\\\x92\\\\xbcs\\\\xe6\\\\xe4<e\\\\xbeT\\\\xbbF\\\\xda\\\\x1a\\\\xbd\\\\x18.O<[\\\\xd1\\\\x89=kt\\\\xe6\\\\xb9L\\\\xa6\\\\x09=\\\\xca\\\\x137\\\\xbc\\\\x8d\\\\xf3\\\\x00\\\\xbdX\\\\xad\\\\x04=fOC\\\\xbcf\\\\xabp\\\\xbc/\\\\x0e2\\\\xbc\\\\x04\\\\x80a=o\\\\x224\\\\xbaqT\\\\x0a\\\\xbd?\\\\xf7e=\\\\x80\\\\x97\\\\xc2<e\\\\xfed\\\\xbd\\\\x144\\\\x0b=kj\\\\x04\\\\xbc\\\\xc3\\\\xbb\\\\xdc\\\\xbc\\\\xb3\\\\xbc\\\\x93\\\\xbd\\\\x94>J;L\\\\xd8\\\\xd5\\\\xbc\\\\xda$0\\\\xbc\\\\x15#H\\\\xbd\\\\xd9\\\\x7fI=V5\\\\x00;\\\\xd4\\\\x80J;\\\\x86R\\\\x9c\\\\xbdI\\\\xb6\\\\xa6:P\\\\xb8\\\\xe2\\\\xbc\\\\x95tN\\\\xbd\\\\x0f\\\\x19\\\\x85=\\\\xa8\\\\xa4\\\\x86=^\\\\x10\\\\x81<\\\\x9fO\\\\x0d=]\\\\x01\\\\x1c=\\\\x0f\\\\x1d\\\\x02\\\\xbd&)\\\\xe4\\\\xbc\\\\xfb\\\\xfc\\\\x02=\\\\xec\\\\x11\\\\xdb<8\\\\x83d\\\\xbc\\\\x8dFC\\\\xbd\\\\xfd0`\\\\xbd\\\\x22*\\\\x8d;\\\\xd0\\\\x09m\\\\xbc\\\\xac\\\\x0b\\\\x7f\\\\xbdn\\\\xf3\\\\xd3\\\\xbc\\\\xad1\\\\x03<JPG;A\\\\xcf\\\\x85;\\\\xf5\\\\xeb\\\\x05=5M\\\\xe8\\\\xbc\\\\xa6\\\\x1c\\\\x1c\\\\xbd\\\\xa1?\\\\x22=+B\\\\xdf<,\\\\xd0\\\\x8c<Y\\\\x0e\\\\xc6=3\\\\xf9\\\\xb4\\\\xbc\\\\x22\\\\x93<\\\\xbd\\\\xf3\\\\x96-=7\\\\x95\\\\x9b<\\\\xa8T\\\\xec\\\\xbc\\\\xc6\\\\xc36<\\\\x12\\\\x0e\\\\x1c;A\\\\xc4\\\\xbd\\\\xbc\\\\xd7\\\\x09Q;+\\\\xf67\\\\xbb\\\\xa3\\\\xa0\\\\x92\\\\xbd\\\\x84\\\\x94\\\\x1f=X3\\\\xa9\\\\xbc\\\\xc4\\\\x8d\\\\x88\\\\xbc\\\\x05\\\\xd9~=\\\\xddo\\\\xb09lN\\\\xd1\\\\xbc\\\\x95-9<\\\\x00\\\\xcd\\\\x5c<\\\\x1c\\\\x95l\\\\xbbk\\\\x91.<=\\\\xe2\\\\x9f=aX]\\\\xbc\\\\xb4\\\\xf2\\\\x80=AT6<\\\\x1b\\\\x99A\\\\xbc\\\\x9c\\\\xe4q\\\\xbc\\\\xe1\\\\xf4\\\\x93;\\\\x11\\\\xb65\\\\xbd\\\\xe2/\\\\xaa\\\\xbc\\\\xbdQ\\\\xaa\\\\xbd\\\\x06\\\\x81\\\\x0c=={\\\\xe8\\\\xbc\\\\xb4\\\\xe3\\\\x90<\\\\xd3F\\\\x1e=H\\\\xfd\\\\xb0=\\\\x1ef\\\\xc1;o\\\\xc1\\\\x90<\\\\xdd\\\\xbe\\\\x93=~m\\\\x89=UTw\\\\xbc\\\\xe1y\\\\x9e=\\\\xe5b\\\\xa1\\\\xbc\\\\x1f\\\\xf4\\\\xa6\\\\xbd\\\\x9a\\\\xd1\\\\xd1\\\\xbc\\\\x82\\\\xf1\\\\x02<Hff=\\\\x12p\\\\x8d\\\\xbb=kH\\\\xbb\\\\xde\\\\xb4\\\\xc4\\\\xb8P\\\\x93E=\\\\x89Y\\\\x14=G\\\\xcer:_\\\\xa4$\\\\xbb,\\\\x8f2\\\\xbda\\\\xd7\\\\x15\\\\xbd%M@;P\\\\xd6h;\\\\xc3\\\\x8aC\\\\xbd\\\\xeePQ\\\\xbd\\\\xc8m\\\\x9f\\\\xbc\\\\xd7p\\\\x9a<\\\\xe8\\\\xf6\\\\xab;\\\\x11\\\\xa7\\\\xab<\\\\x96j\\\\xac\\\\xbc/S\\\\x7f<<\\\\x9f\\\\xc1<E\\\\xf7\\\\xa1\\\\xbc`xo<\\\\xd8\\\\x81\\\\x22\\\\xbc\\\\x8f\\\\x12k=\\\\x88\\\\xab==\\\\xff1\\\\xb1<\\\\x95\\\\x8a>\\\\xbb|M\\\\x98\\\\xbcZq\\\\x16\\\\xbd/\\\\xd3\\\\x97<(;\\\\x0c\\\\xbd\\\\x0a\\\\xff0=\\\\x93\\\\xb7\\\\xb2\\\\xbc\\\\x8f\\\\x90\\\\xb8;\\\\x92(\\\\x98\\\\xbc\\\\xf3\\\\x1f\\\\xe7<9=\\\\x13\\\\xbcQ\\\\x83\\\\x8d\\\\xbdF\\\\xf1e<\\\\xb2\\\\xc6V=\\\\xbdR};\\\\xbd\\\\x12\\\\xb3\\\\xbcu\\\\x0b\\\\x99=\\\\x82\\\\xd4\\\\xbf\\\\xbc\\\\xcd\\\\xf3\\\\xf0<\\\\xdd\\\\x8a\\\\xc7;\\\\x02\\\\xab\\\\x8e\\\\xbb\\\\xb3GO=\\\\xb280=\\\\x8bw\\\\x15\\\\xbd\\\\xa8\\\\xf7<\\\\xbc\\\\x16v\\\\x1d\\\\xbdX\\\\x99\\\\xbb\\\\xbc\\\\xa9K\\\\x03=\\\\x9dh.\\\\xbd[U\\\\x92<\\\\xd3\\\\xe1\\\\xab\\\\xbb\\\\xc1W\\\\xb2:\\\\x0fl\\\\x0d\\\\x09\\\\x17\\\\x80\\\\xb6\\\\xbdK\\\\xe7\\\\x0a\\\\xbdl\\\\xd0\\\\xb9<\\\\x83x\\\\xd5\\\\xba\\\\xd9\\\\xb2 \\\\xbcm\\\\xbf\\\\xcf\\\\xbc\\\\xe5,@\\\\xbcL\\\\xad\\\\x18=\\\\x1e\\\\xe9\\\\x0a\\\\xbc\\\\xed\\\\xbc\\\\xa4<\\\\xa7KS\\\\xbb\\\\xa5\\\\xe3O=\\\\xf8*X;\\\\x0e\\\\x9b\\\\x95=\\\\xb72\\\\x84\\\\xbc\\\\xf9\\\\xff\\\\x5c<\\\\xf1\\\\xc5\\\\x93\\\\xbaCk>\\\\xbdAw\\\\xe0<\\\\xa2\\\\xd6\\\\xe3<\\\\xde\\\\xe0\\\\x86=~}\\\\x81=\\\\xbe~u\\\\xbdH\\\\xcb6\\\\xbd\\\\x12\\\\x9f2=\\\\xc6\\\\xf16=a^\\\\xd3\\\\xbc\\\\xed\\\\x04\\\\x809\\\\xe7\\\\x8f\\\\x07<\\\\xb0\\\\x9fW\\\\xbb<\\\\xf3\\\\x16\\\\xbd\\\\x9a\\\\x98\\\\xfc<\\\\xf8\\\\xaf\\\\xe1\\\\xbc\\\\xee\\\\xb6!\\\\xbd\\\\xda\\\\xeb\\\\x96\\\\xbcn\\\\x22\\\\x7f<\\\\xf8j\\\\x8c<z\\\\xb5\\\\x80<\\\\xda\\\\x0e\\\\xa2<\\\\x9d\\\\xeco<O \\\\xbb<Ic\\\\x18\\\\xbc\\\\x0a\\\\xd4\\\\x0d\\\\xbd\\\\xc8\\\\x95\\\\xf7<\\\\xc2\\\\x92\\\\x10=\\\\xbai\\\\xd6\\\\xbd\\\\x9d\\\\xd7@=\\\\xf7w\\\\x01\\\\xbc\\\\x14mW\\\\xbc\\\\xb7&\\\\x84\\\\xbc0t\\\\x099\\\\xb7V\\\\x18\\\\xbe\\\\x87!\\\\xa7<\\\\xa59D=\\\\x1b2\\\\x01\\\\xbdE-b\\\\xbc\\\\xd2KW\\\\xbd\\\\x0a\\\\xa3.\\\\xbd\\\\xe9-\\\\x04\\\\xbd8u\\\\x1f<\\\\xe7\\\\xbe\\\\x8d\\\\xbc\\\\xff\\\\xca\\\\x7f\\\\xbc\\\\xb8\\\\x03\\\\x92\\\\xba\\\\x22\\\\xe6\\\\xa5\\\\xbbU[Q\\\\xbc6\\\\xec\\\\x1e\\\\xb9\\\\x93\\\\x80\\\\xbe\\\\xbc\\\\xa4>\\\\xc29j\\\\x9fc=L\\\\xb5\\\\x00\\\\xbbI\\\\x90\\\\xcb\\\\xbc\\\\xfdi\\\\x81<\\\\xbbx\\\\x02\\\\xbc\\\\xfb\\\\x0f\\\\xa1\\\\xbb\\\\x13\\\\xdf\\\\x97\\\\xbc\\\\xa2t\\\\x8f\\\\xbc\\\\x99jS\\\\xbd\\\\xdf\\\\xb6\\\\xbf<t\\\\xf3+\\\\xbdxN\\\\xa0<\\\\x18\\\\xe2\\\\x1d<t\\\\xf6\\\\xb4\\\\xba9\\\\xb1\\\\xd8\\\\xbc\\\\xac\\\\x82;=\\\\x91o\\\\x0e\\\\xbd\\\\xe7\\\\xa4\\\\x19=\\\\x11s3=\\\\x93@\\\\xbc\\\\xbb\\\\xec\\\\xda\\\\xe3;\\\\xbe\\\\xf8\\\\xff;k\\\\x9a\\\\xac\\\\xba\\\\xc0\\\\xb4W\\\\xbd\\\\xf3*\\\\xf4<Hl\\\\x8f\\\\xba],7=\\\\xef\\\\xce\\\\xfa<\\\\xa8\\\\xe6\\\\xe1\\\\xbc\\\\xe0oT\\\\xbd\\\\x1fx\\\\x8c=p\\\\xa2\\\\xaa;\\\\x15\\\\x87\\\\xe1\\\\xbd\\\\xcf\\\\xc1\\\\xbb\\\\xbcQ\\\\xacg<5\\\\x22 \\\\xbdpr\\\\xfb\\\\xbc@\\\\xc0\\\\x95\\\\xbcd\\\\xe9\\\\xff\\\\xbb_\\\\xa4\\\\x80\\\\xbc\\\\xe5W\\\\xb4\\\\xbb\\\\xe8\\\\xcf\\\\xd8\\\\xbc\\\\x7f\\\\xf7 =\\\\x8b\\\\x1a\\\\x0b\\\\xbd|A\\\\x81\\\\xbd\\\\x1blN=r\\\\x1d\\\\xbb<|\\\\x02\\\\xb6\\\\xbb\\\\xeaQ4<\\\\x15\\\\xd7\\\\xa0<i\\\\xb7\\\\x89\\\\xbca\\\\xe4\\\\xc9\\\\xbb\\\\x22\\\\x8e]=\\\\xf2\\\\xbd3=\\\\xc5*-=\\\\xa1F\\\\xab<\\\\xf7Z\\\\xc7<+|:<@g\\\\xfe\\\\xbb8+\\\\xc7<\\\\xd2\\\\xf5%=\\\\x86\\\\x8d(\\\\xbcq\\\\xd8y<\\\\xb4\\\\xf0\\\\x95\\\\xbd\\\\xb8\\\\xf0Y=\\\\x8c+\\\\x82<\\\\xd6i\\\\xa6\\\\xbc\\\\x95q3=\\\\xcacL<\\\\x8f\\\\x80]\\\\xbd\\\\x8f\\\\x8e\\\\x94;w\\\\x84\\\\x8e<\\\\xa2\\\\xad\\\\x8e<\\\\xad\\\\xe6z\\\\xbd\\\\x17/\\\\x08\\\\xbd0\\\\xf3\\\\x84<\\\\xdb\\\\xb9\\\\xd9:P@\\\\x18<?NZ\\\\xbc\\\\xf6xX\\\\xbbcU\\\\x22=(Z\\\\xaa\\\\xbcMX\\\\x98\\\\xbc\\\\xebL\\\\x01\\\\xbc\\\\xa3\\\\xea\\\\xd7\\\\xbc\\\\xf4h4\\\\xbc\\\\xe0C\\\\xdb<\\\\xe56#\\\\xbc\\\\xe3\\\\xe4\\\\x06\\\\xbd\\\\xe49\\\\x85\\\\xbc\\\\x16\\\\x1d\\\\xb1=\\\\xd6Xv=\\\\x7f\\\\x94\\\\x85\\\\xbd\\\\xed\\\\xa42<[\\\\x1b\\\\x1f\\\\xbdv\\\\x83C\\\\xbc\\\\xc0\\\\x99c=\\\\xf1t~\\\\xbdgMe\\\\xbd\\\\x11j\\\\xef\\\\xbc\\\\x1d\\\\x9c\\\\xe0:\\\\x9d\\\\xae\\\\xb2\\\\xbc\\\\xf6|\\\\x1a=;\\\\xe0\\\\x01=T\\\\xb1\\\\xad\\\\xba\\\\xba{1\\\\xbc\\\\x09\\\\xc2\\\\x1a\\\\xbb=\\\\xe1\\\\x86;\\\\x22He=gb\\\\xb9\\\\xbb2G:=o\\\\xd2\\\\xe9\\\\xbb0-j\\\\xbd\\\\xf1\\\\x81.\\\\xbd\\\\xfb\\\\x8a\\\\xf8\\\\xbac\\\\xe6\\\\xaf\\\\xbc\\\\x1e\\\\xb7\\\\x0d\\\\xbd\\\\x04j\\\\xc1\\\\xbc\\\\xf1\\\\x12\\\\x96\\\\xbd:\\\\xc0G<v\\\\x13\\\\xe5<\\\\x88\\\\xdd\\\\x8d\\\\xbc\\\\xfc\\\\x16Y=\\\\xc9]u=\\\\xe4\\\\x1b0\\\\xbb\\\\xd8D\\\\xd0<\\\\x844t;7\\\\xd9\\\\x16\\\\xbdp\\\\x84\\\\xf9\\\\xbcv\\\\xc9j<\\\\xbbG\\\\xe4\\\\xbc\\\\x22N\\\\x8a=/\\\\xaa-\\\\xbd\\\\xd5l\\\\xca\\\\xbb\\\\xe6\\\\xce\\\\xb8;\\\\x0a\\\\x02+9\\\\xcb\\\\xcf\\\\xa5\\\\xbd\\\\xa0\\\\x93\\\\xa0\\\\xbcK-\\\\xc1<<L*\\\\xbd*\\\\x03\\\\xd6;\\\\x86\\\\xa3:\\\\xbd\\\\x87\\\\xa4\\\\x9d\\\\xbc\\\\xf1\\\\xd6\\\\xfb\\\\xbcB\\\\x9b\\\\xea\\\\xbc\\\\xbe\\\\x81a\\\\xbc\\\\xa5\\\\xcd\\\\xfc\\\\xbb\\\\x0d\\\\x0c\\\\xab\\\\xbc\\\\xb2\\\\xceT=*-\\\\xd4<\\\\xd3zd<\\\\xda\\\\xa9\\\\xa2\\\\xbc\\\\x11K\\\\x11<\\\\xe8\\\\xdf\\\\xca\\\\xbbtV&=\\\\x1a\\\\xea\\\\x11=\\\\xf3\\\\xd8\\\\x80=\\\\x95\\\\xdf\\\\x03=\\\\x03\\\\xccS<u4\\\\xa19\\\\xc0K.\\\\xbd!\\\\x04j=\\\\x08\\\\xdb=\\\\xbd\\\\x7f&|\\\\xbd\\\\xa3}\\\\x96<\\\\xf0\\\\xccb\\\\xbct\\\\xf9\\\\xb7\\\\xbb\\\\x00}\\\\x1c\\\\xbd(\\\\x9cI;\\\\x10OM<\\\\xd3\\\\xe7==\\\\xbc@\\\\xa1\\\\xbc/l\\\\xde\\\\xbc\\\\x8b\\\\xaaB=\\\\xa5\\\\x22\\\\xe8;\\\\x1b\\\\x0f\\\\x05<\\\\xd6\\\\x84\\\\x0b=\\\\xa3\\\\xe9g\\\\xbc\\\\x1e\\\\xcb#\\\\xbc_\\\\xca\\\\x89\\\\xbcz\\\\x9a\\\\x90\\\\xbcB\\\\x81o<zS\\\\x98=ar\\\\x06;\\\\xb7l\\\\xc7<3&\\\\x11<\\\\x93\\\\x1c\\\\xc5\\\\xbb f\\\\x1c\\\\xbb\\\\xa2\\\\xd8i\\\\xbd\\\\xcfm\\\\xd0\\\\xbc,1\\\\x80\\\\xbd 0\\\\xed\\\\xbc\\\\x98\\\\x1f.=\\\\x8f\\\\xd7\\\\xe6<\\\\x8dD\\\\xe6<\\\\x80$\\\\x8d\\\\xbc[L`=\\\\x84gE;\\\\xb5\\\\x88\\\\xa5<\\\\xcfTL=\\\\x1e\\\\xef\\\\xc5\\\\xbcz\\\\x06\\\\x8c=\\\\x91\\\\xea\\\\xef<\\\\xa9D\\\\x89=\\\\xa2\\\\xe9W\\\\xbd\" ' +\n        'SORTBY score ' +\n        'DIALECT 2',\n    },\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/sample-data/index.ts",
    "content": "import { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { SampleDatasetConfig } from './types'\nimport { BIKES_DATASET } from './bikes'\nimport { MOVIES_DATASET } from './movies'\n\n/**\n * Central registry of all sample datasets.\n * To add a new dataset:\n * 1. Create a new file in this directory (e.g. `my-dataset.ts`)\n *    exporting a `SampleDatasetConfig`.\n * 2. Add a new entry to `SampleDataContent` enum.\n * 3. Register the config in this map.\n */\nexport const SAMPLE_DATASETS: Record<SampleDataContent, SampleDatasetConfig> = {\n  [SampleDataContent.E_COMMERCE_DISCOVERY]: BIKES_DATASET,\n  [SampleDataContent.CONTENT_RECOMMENDATIONS]: MOVIES_DATASET,\n}\n\nexport { BIKES_DATASET, MOVIES_DATASET }\nexport type { SampleDatasetConfig, SampleQuery } from './types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/sample-data/movies.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { SampleDatasetConfig } from './types'\n\nexport const MOVIES_DATASET: SampleDatasetConfig = {\n  displayName: 'Content recommendations',\n  indexName: 'idx:movies_vss',\n  indexPrefix: 'movie:',\n  collectionName: 'movies',\n  fields: [\n    { id: 'title', name: 'title', value: 'Toy Story', type: FieldTypes.TEXT },\n    {\n      id: 'genres',\n      name: 'genres',\n      value: 'Animation, Comedy, Family',\n      type: FieldTypes.TAG,\n    },\n    {\n      id: 'plot',\n      name: 'plot',\n      value: 'Toys come to life when humans arent around.',\n      type: FieldTypes.TEXT,\n    },\n    { id: 'year', name: 'year', value: 1995, type: FieldTypes.NUMERIC },\n    {\n      id: 'embedding',\n      name: 'embedding',\n      value: 'FLAT, FLOAT32, 8, COSINE',\n      type: FieldTypes.VECTOR,\n    },\n  ],\n  sampleQueries: [\n    {\n      name: 'Basic plot similarity search',\n      description:\n        'Performs a K-nearest neighbors search to find movies with plot embeddings most similar to the query vector. Returns the top 3 matches with title, plot, and similarity score. Demonstrates pure semantic search—Toy Story ranks first based on meaning, not keyword matches.',\n      query:\n        'FT.SEARCH idx:movies_vss \"*=>[KNN 3 @embedding $vec AS score]\" ' +\n        'PARAMS 2 vec \"\\\\x9a\\\\x99\\\\x19\\\\x3f\\\\xcd\\\\xcc\\\\xcc\\\\x3d\\\\x9a\\\\x99\\\\x4c\\\\x3f\\\\x9a\\\\x99\\\\x33\\\\x3e\\\\x9a\\\\x99\\\\x33\\\\x3f\\\\xcd\\\\xcc\\\\x66\\\\x3e\\\\xcd\\\\xcc\\\\xcc\\\\x3d\\\\xcd\\\\xcc\\\\x4c\\\\x3e\" ' +\n        'SORTBY score ' +\n        'RETURN 3 title plot score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Genre-filtered semantic search',\n      description:\n        'Combines a genre tag filter with vector similarity to find music-related movies matching \"A feel-good film about music and students.\" Pre-filters to the Music genre before running KNN, showing how hybrid search improves relevance by narrowing candidates.',\n      query:\n        'FT.SEARCH idx:movies_vss \"@genres:{Music} =>[KNN 5 @embedding $vec AS score]\" ' +\n        'PARAMS 2 vec \"\\\\x9a\\\\x99\\\\x1d\\\\x3e\\\\xcd\\\\xcc\\\\x4c\\\\xbd\\\\x9a\\\\x99\\\\x99\\\\x3e\\\\x9a\\\\x99\\\\x19\\\\x3e\\\\x9a\\\\x99\\\\x19\\\\xbe\\\\x9a\\\\x99\\\\x1d\\\\x3e\\\\xcd\\\\xcc\\\\x0c\\\\x3e\\\\x9a\\\\x99\\\\xf1\\\\xbc\" ' +\n        'SORTBY score ' +\n        'RETURN 4 title year genres score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Retrieve document embedding',\n      description:\n        'Extracts the stored embedding vector from an existing movie document (Inception). This vector can then be used as input for a \"more like this\" recommendation query, enabling content-based recommendations without regenerating embeddings.',\n      query:\n        'FT.SEARCH idx:movies_vss \"*=>[KNN 5 @embedding $vec AS score]\" ' +\n        'PARAMS 2 vec \"\\\\xCD\\\\xCC\\\\x56\\\\x3E\\\\x9A\\\\x99\\\\xF3\\\\xBC\\\\xCD\\\\xCC\\\\x00\\\\x3F\\\\x66\\\\x66\\\\x34\\\\x3E\\\\xC6\\\\xF5\\\\x1B\\\\xBE\\\\x9A\\\\x99\\\\x4D\\\\x3E\\\\x9A\\\\x99\\\\x99\\\\x3D\\\\x9A\\\\x99\\\\xB5\\\\xBD\" ' +\n        'SORTBY score ' +\n        'RETURN 2 title score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Multi-filter hybrid search',\n      description:\n        \"Combines multiple metadata filters (genre: Music, year: 1970–1979) with vector similarity search. Finds classic 70s music films matching the query's semantic intent, showing how numeric ranges and tag filters work seamlessly with KNN.\",\n      query:\n        'FT.SEARCH idx:movies_vss \"(@genres:{Music} @year:[1970 1979]) =>[KNN 5 @embedding $vec AS score]\" ' +\n        'PARAMS 2 vec \"\\\\x9a\\\\x99\\\\x1d\\\\x3e\\\\xcd\\\\xcc\\\\x4c\\\\xbd\\\\x9a\\\\x99\\\\x99\\\\x3e\\\\x9a\\\\x99\\\\x19\\\\x3e\\\\x9a\\\\x99\\\\x19\\\\xbe\\\\x9a\\\\x99\\\\x1d\\\\x3e\\\\xcd\\\\xcc\\\\x0c\\\\x3e\\\\x9a\\\\x99\\\\xf1\\\\xbc\" ' +\n        'SORTBY score ' +\n        'RETURN 4 title year genres score ' +\n        'DIALECT 2',\n    },\n    {\n      name: 'Personalized multi-genre search',\n      description:\n        'Filters results to user-preferred genres (Animated OR Sci-Fi) before running vector similarity. Demonstrates personalization—narrowing recommendations to categories the user enjoys while still ranking by semantic relevance.',\n      query:\n        'FT.SEARCH idx:movies_vss \"@genres:{\\\\\"Animated\\\\\"|\\\\\"Sci-Fi\\\\\"} =>[KNN 5 @embedding $vec AS score]\" ' +\n        'PARAMS 2 vec \"\\\\x9a\\\\x99\\\\x1d\\\\x3e\\\\xcd\\\\xcc\\\\x4c\\\\xbd\\\\x9a\\\\x99\\\\x99\\\\x3e\\\\x9a\\\\x99\\\\x19\\\\x3e\\\\x9a\\\\x99\\\\x19\\\\xbe\\\\x9a\\\\x99\\\\x1d\\\\x3e\\\\xcd\\\\xcc\\\\x0c\\\\x3e\\\\x9a\\\\x99\\\\xf1\\\\xbc\" ' +\n        'SORTBY score ' +\n        'RETURN 3 title genres score ' +\n        'DIALECT 2',\n    },\n  ],\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/constants/sample-data/types.ts",
    "content": "import { IndexField } from '../../components/index-details/IndexDetails.types'\n\nexport interface SampleQuery {\n  name: string\n  description?: string\n  query: string\n}\n\n/**\n * Full configuration for a sample dataset.\n * To add a new dataset, create a new file in this directory implementing this interface,\n * then register it in the SAMPLE_DATASETS map in `index.ts`.\n */\nexport interface SampleDatasetConfig {\n  /** Human-readable display name (e.g. \"E-commerce discovery\"). */\n  displayName: string\n\n  /** Index name used in the FT.CREATE command (e.g. \"idx:bikes_vss\"). */\n  indexName: string\n\n  /** Key prefix used in the FT.CREATE command (e.g. \"bikes:\"). */\n  indexPrefix: string\n\n  /** Collection name used for bulk data import via the API. */\n  collectionName: string\n\n  /** Index field definitions shown in the IndexDetails table. */\n  fields: IndexField[]\n\n  /** Pre-defined sample queries seeded into the Query Library after index creation. */\n  sampleQueries: SampleQuery[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-onboarding/CreateIndexOnboardingContext.tsx",
    "content": "import { createContext, useContext } from 'react'\n\nimport { CreateIndexOnboardingStep } from '../../components/create-index-onboarding/CreateIndexOnboarding.constants'\n\nexport interface CreateIndexOnboardingContextValue {\n  currentStep: CreateIndexOnboardingStep | null\n  isActive: boolean\n  totalSteps: number\n  startOnboarding: () => void\n  nextStep: () => void\n  prevStep: () => void\n  skipOnboarding: () => void\n}\n\nexport const CreateIndexOnboardingContext =\n  createContext<CreateIndexOnboardingContextValue>({\n    currentStep: null,\n    isActive: false,\n    totalSteps: 0,\n    startOnboarding: () => {},\n    nextStep: () => {},\n    prevStep: () => {},\n    skipOnboarding: () => {},\n  })\n\nexport const useCreateIndexOnboarding = () =>\n  useContext(CreateIndexOnboardingContext)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-onboarding/CreateIndexOnboardingProvider.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport BrowserStorageItem from 'uiSrc/constants/storage'\nimport { localStorageService } from 'uiSrc/services'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { CreateIndexOnboardingStep } from '../../components/create-index-onboarding/CreateIndexOnboarding.constants'\nimport { SearchOnboardingAction } from '../../telemetry.constants'\nimport { CreateIndexOnboardingProvider } from './CreateIndexOnboardingProvider'\nimport { useCreateIndexOnboarding } from './CreateIndexOnboardingContext'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst TestConsumer = () => {\n  const {\n    currentStep,\n    isActive,\n    startOnboarding,\n    nextStep,\n    prevStep,\n    skipOnboarding,\n  } = useCreateIndexOnboarding()\n\n  return (\n    <div>\n      <span data-testid=\"current-step\">{currentStep ?? 'null'}</span>\n      <span data-testid=\"is-active\">{String(isActive)}</span>\n      <button type=\"button\" data-testid=\"start\" onClick={startOnboarding}>\n        Start\n      </button>\n      <button type=\"button\" data-testid=\"next\" onClick={nextStep}>\n        Next\n      </button>\n      <button type=\"button\" data-testid=\"prev\" onClick={prevStep}>\n        Prev\n      </button>\n      <button type=\"button\" data-testid=\"skip\" onClick={skipOnboarding}>\n        Skip\n      </button>\n    </div>\n  )\n}\n\nconst mockInstanceId = faker.string.uuid()\nconst mockLocalStorageGet = localStorageService.get as jest.Mock\nconst mockSendEventTelemetry = sendEventTelemetry as jest.Mock\n\ndescribe('CreateIndexOnboardingProvider', () => {\n  const renderComponent = (instanceId = mockInstanceId) =>\n    render(\n      <CreateIndexOnboardingProvider instanceId={instanceId}>\n        <TestConsumer />\n      </CreateIndexOnboardingProvider>,\n    )\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockLocalStorageGet.mockReturnValue(null)\n  })\n\n  it('should start inactive with no current step', () => {\n    renderComponent()\n\n    const currentStep = screen.getByTestId('current-step')\n    const isActive = screen.getByTestId('is-active')\n\n    expect(currentStep).toHaveTextContent('null')\n    expect(isActive).toHaveTextContent('false')\n  })\n\n  it('should start onboarding at DefineIndex step', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    fireEvent.click(startButton)\n\n    const currentStep = screen.getByTestId('current-step')\n    const isActive = screen.getByTestId('is-active')\n\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.DefineIndex)\n    expect(isActive).toHaveTextContent('true')\n  })\n\n  it('should not start onboarding if already seen', () => {\n    mockLocalStorageGet.mockReturnValue(true)\n\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    fireEvent.click(startButton)\n\n    const currentStep = screen.getByTestId('current-step')\n    const isActive = screen.getByTestId('is-active')\n\n    expect(currentStep).toHaveTextContent('null')\n    expect(isActive).toHaveTextContent('false')\n  })\n\n  it('should not start onboarding twice', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    fireEvent.click(startButton)\n    fireEvent.click(startButton)\n\n    const currentStep = screen.getByTestId('current-step')\n\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.DefineIndex)\n  })\n\n  it('should advance through steps with nextStep', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    const nextButton = screen.getByTestId('next')\n    const currentStep = screen.getByTestId('current-step')\n\n    fireEvent.click(startButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.DefineIndex)\n\n    fireEvent.click(nextButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.IndexPrefix)\n\n    fireEvent.click(nextButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.FieldName)\n  })\n\n  it('should complete and persist after last step', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    const nextButton = screen.getByTestId('next')\n    const isActive = screen.getByTestId('is-active')\n\n    fireEvent.click(startButton)\n\n    for (let i = 0; i < 6; i++) {\n      fireEvent.click(nextButton)\n    }\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.vectorSearchCreateIndexOnboarding,\n      true,\n    )\n    expect(isActive).toHaveTextContent('false')\n  })\n\n  it('should skip and persist on skipOnboarding', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    const skipButton = screen.getByTestId('skip')\n    const isActive = screen.getByTestId('is-active')\n\n    fireEvent.click(startButton)\n    fireEvent.click(skipButton)\n\n    expect(localStorageService.set).toHaveBeenCalledWith(\n      BrowserStorageItem.vectorSearchCreateIndexOnboarding,\n      true,\n    )\n    expect(isActive).toHaveTextContent('false')\n  })\n\n  it('should go back to previous step with prevStep', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    const nextButton = screen.getByTestId('next')\n    const prevButton = screen.getByTestId('prev')\n    const currentStep = screen.getByTestId('current-step')\n\n    fireEvent.click(startButton)\n    fireEvent.click(nextButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.IndexPrefix)\n\n    fireEvent.click(prevButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.DefineIndex)\n  })\n\n  it('should stay on first step when prevStep is called', () => {\n    renderComponent()\n\n    const startButton = screen.getByTestId('start')\n    const prevButton = screen.getByTestId('prev')\n    const currentStep = screen.getByTestId('current-step')\n\n    fireEvent.click(startButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.DefineIndex)\n\n    fireEvent.click(prevButton)\n    expect(currentStep).toHaveTextContent(CreateIndexOnboardingStep.DefineIndex)\n  })\n\n  describe('edge cases', () => {\n    it('should no-op nextStep when not active', () => {\n      renderComponent()\n\n      const nextButton = screen.getByTestId('next')\n      const isActive = screen.getByTestId('is-active')\n\n      fireEvent.click(nextButton)\n\n      expect(isActive).toHaveTextContent('false')\n      expect(localStorageService.set).not.toHaveBeenCalled()\n    })\n\n    it('should no-op prevStep when not active', () => {\n      renderComponent()\n\n      const prevButton = screen.getByTestId('prev')\n      const isActive = screen.getByTestId('is-active')\n\n      fireEvent.click(prevButton)\n\n      expect(isActive).toHaveTextContent('false')\n    })\n\n    it('should no-op skipOnboarding when not active', () => {\n      renderComponent()\n\n      const skipButton = screen.getByTestId('skip')\n      const isActive = screen.getByTestId('is-active')\n\n      fireEvent.click(skipButton)\n\n      expect(isActive).toHaveTextContent('false')\n      expect(localStorageService.set).not.toHaveBeenCalled()\n    })\n  })\n\n  describe('telemetry', () => {\n    it('should send STARTED event when onboarding begins', () => {\n      renderComponent()\n\n      const startButton = screen.getByTestId('start')\n      fireEvent.click(startButton)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STARTED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n\n    it('should not send STARTED event if already seen', () => {\n      mockLocalStorageGet.mockReturnValue(true)\n\n      renderComponent()\n\n      const startButton = screen.getByTestId('start')\n      fireEvent.click(startButton)\n\n      expect(sendEventTelemetry).not.toHaveBeenCalled()\n    })\n\n    it('should send STEP_CLICKED with action next on nextStep', () => {\n      renderComponent()\n\n      const startButton = screen.getByTestId('start')\n      const nextButton = screen.getByTestId('next')\n\n      fireEvent.click(startButton)\n      mockSendEventTelemetry.mockClear()\n\n      fireEvent.click(nextButton)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED,\n        eventData: {\n          databaseId: mockInstanceId,\n          step: CreateIndexOnboardingStep.DefineIndex,\n          action: SearchOnboardingAction.Next,\n        },\n      })\n    })\n\n    it('should send STEP_CLICKED with action back on prevStep', () => {\n      renderComponent()\n\n      const startButton = screen.getByTestId('start')\n      const nextButton = screen.getByTestId('next')\n      const prevButton = screen.getByTestId('prev')\n\n      fireEvent.click(startButton)\n      fireEvent.click(nextButton)\n      mockSendEventTelemetry.mockClear()\n\n      fireEvent.click(prevButton)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED,\n        eventData: {\n          databaseId: mockInstanceId,\n          step: CreateIndexOnboardingStep.IndexPrefix,\n          action: SearchOnboardingAction.Back,\n        },\n      })\n    })\n\n    it('should send STEP_CLICKED with action skip when skipping mid-tour', () => {\n      renderComponent()\n\n      const startButton = screen.getByTestId('start')\n      const nextButton = screen.getByTestId('next')\n      const skipButton = screen.getByTestId('skip')\n\n      fireEvent.click(startButton)\n      fireEvent.click(nextButton)\n      mockSendEventTelemetry.mockClear()\n\n      fireEvent.click(skipButton)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED,\n        eventData: {\n          databaseId: mockInstanceId,\n          step: CreateIndexOnboardingStep.IndexPrefix,\n          action: SearchOnboardingAction.Skip,\n        },\n      })\n    })\n\n    it('should send COMPLETED event when finishing the last step', () => {\n      renderComponent()\n\n      const startButton = screen.getByTestId('start')\n      const nextButton = screen.getByTestId('next')\n      const skipButton = screen.getByTestId('skip')\n\n      fireEvent.click(startButton)\n\n      for (let i = 0; i < 5; i++) {\n        fireEvent.click(nextButton)\n      }\n\n      mockSendEventTelemetry.mockClear()\n\n      fireEvent.click(skipButton)\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_COMPLETED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n\n    it('should not send telemetry when actions are no-ops', () => {\n      renderComponent()\n\n      const nextButton = screen.getByTestId('next')\n      const prevButton = screen.getByTestId('prev')\n      const skipButton = screen.getByTestId('skip')\n\n      fireEvent.click(nextButton)\n      fireEvent.click(prevButton)\n      fireEvent.click(skipButton)\n\n      expect(sendEventTelemetry).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-onboarding/CreateIndexOnboardingProvider.tsx",
    "content": "import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n\nimport BrowserStorageItem from 'uiSrc/constants/storage'\nimport { localStorageService } from 'uiSrc/services'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport {\n  CreateIndexOnboardingStep,\n  ONBOARDING_STEPS,\n} from '../../components/create-index-onboarding/CreateIndexOnboarding.constants'\nimport { SearchOnboardingAction } from '../../telemetry.constants'\nimport {\n  CreateIndexOnboardingContext,\n  CreateIndexOnboardingContextValue,\n} from './CreateIndexOnboardingContext'\n\nconst isOnboardingSeen = (): boolean =>\n  localStorageService.get(\n    BrowserStorageItem.vectorSearchCreateIndexOnboarding,\n  ) === true\n\nconst markOnboardingSeen = () => {\n  localStorageService.set(\n    BrowserStorageItem.vectorSearchCreateIndexOnboarding,\n    true,\n  )\n}\n\nconst isLastStep = (step: CreateIndexOnboardingStep | null): boolean =>\n  step === ONBOARDING_STEPS[ONBOARDING_STEPS.length - 1]\n\nexport interface CreateIndexOnboardingProviderProps {\n  instanceId: string\n  children: React.ReactNode\n}\n\nexport const CreateIndexOnboardingProvider = ({\n  instanceId,\n  children,\n}: CreateIndexOnboardingProviderProps) => {\n  const [currentStep, setCurrentStep] =\n    useState<CreateIndexOnboardingStep | null>(null)\n  const [isActive, setIsActive] = useState(false)\n\n  const isActiveRef = useRef(isActive)\n  isActiveRef.current = isActive\n\n  const currentStepRef = useRef(currentStep)\n  currentStepRef.current = currentStep\n\n  useEffect(() => {\n    if (isActive && currentStep === null) {\n      markOnboardingSeen()\n      setIsActive(false)\n    }\n  }, [isActive, currentStep])\n\n  const startOnboarding = useCallback(() => {\n    if (isActiveRef.current) return\n    if (isOnboardingSeen()) return\n\n    setCurrentStep(ONBOARDING_STEPS[0])\n    setIsActive(true)\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STARTED,\n      eventData: { databaseId: instanceId },\n    })\n  }, [instanceId])\n\n  const skipOnboarding = useCallback(() => {\n    if (!isActiveRef.current) return\n\n    const step = currentStepRef.current\n\n    if (isLastStep(step)) {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_COMPLETED,\n        eventData: { databaseId: instanceId },\n      })\n    } else {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED,\n        eventData: {\n          databaseId: instanceId,\n          step,\n          action: SearchOnboardingAction.Skip,\n        },\n      })\n    }\n\n    markOnboardingSeen()\n    setIsActive(false)\n    setCurrentStep(null)\n  }, [instanceId])\n\n  const nextStep = useCallback(() => {\n    if (!isActiveRef.current) return\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        step: currentStepRef.current,\n        action: SearchOnboardingAction.Next,\n      },\n    })\n\n    setCurrentStep((prev) => {\n      if (!prev) return null\n\n      const currentIndex = ONBOARDING_STEPS.findIndex((step) => step === prev)\n\n      if (currentIndex === -1) return null\n\n      const nextIndex = currentIndex + 1\n\n      if (nextIndex >= ONBOARDING_STEPS.length) return null\n\n      return ONBOARDING_STEPS[nextIndex]\n    })\n  }, [instanceId])\n\n  const prevStep = useCallback(() => {\n    if (!isActiveRef.current) return\n\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n        step: currentStepRef.current,\n        action: SearchOnboardingAction.Back,\n      },\n    })\n\n    setCurrentStep((prev) => {\n      if (!prev) return null\n\n      const currentIndex = ONBOARDING_STEPS.findIndex((step) => step === prev)\n\n      if (currentIndex <= 0) return prev\n\n      return ONBOARDING_STEPS[currentIndex - 1]\n    })\n  }, [instanceId])\n\n  const value: CreateIndexOnboardingContextValue = useMemo(\n    () => ({\n      currentStep,\n      isActive,\n      totalSteps: ONBOARDING_STEPS.length,\n      startOnboarding,\n      nextStep,\n      prevStep,\n      skipOnboarding,\n    }),\n    [\n      currentStep,\n      isActive,\n      startOnboarding,\n      nextStep,\n      prevStep,\n      skipOnboarding,\n    ],\n  )\n\n  return (\n    <CreateIndexOnboardingContext.Provider value={value}>\n      {children}\n    </CreateIndexOnboardingContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-onboarding/index.ts",
    "content": "export {\n  CreateIndexOnboardingContext,\n  useCreateIndexOnboarding,\n} from './CreateIndexOnboardingContext'\nexport type { CreateIndexOnboardingContextValue } from './CreateIndexOnboardingContext'\nexport { CreateIndexOnboardingProvider } from './CreateIndexOnboardingProvider'\nexport type { CreateIndexOnboardingProviderProps } from './CreateIndexOnboardingProvider'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-page/CreateIndexPageContext.tsx",
    "content": "import { createContext, useContext } from 'react'\n\nimport { CreateIndexPageContextValue } from './CreateIndexPageContext.types'\n\nexport const CreateIndexPageContext =\n  createContext<CreateIndexPageContextValue | null>(null)\n\nexport const useCreateIndexPage = (): CreateIndexPageContextValue => {\n  const ctx = useContext(CreateIndexPageContext)\n  if (!ctx) {\n    throw new Error(\n      'useCreateIndexPage must be used within CreateIndexPageProvider',\n    )\n  }\n  return ctx\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-page/CreateIndexPageContext.types.ts",
    "content": "import { RowSelectionState } from 'uiSrc/components/base/layout/table'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nimport { IndexField } from '../../components/index-details/IndexDetails.types'\nimport { FieldTypeModalMode } from '../../components/field-type-modal'\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport {\n  CreateIndexTab,\n  CreateIndexMode,\n} from '../../pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\n\nexport interface FieldModalState {\n  isOpen: boolean\n  mode: FieldTypeModalMode\n  field?: IndexField\n}\n\n// TODO: Think about splitting this into multiple contexts, one for each mode.\nexport interface CreateIndexPageContextValue {\n  /** Which mode the page is operating in. */\n  mode: CreateIndexMode\n\n  /** Currently active tab (table or command). */\n  activeTab: CreateIndexTab\n  setActiveTab: (tab: CreateIndexTab) => void\n\n  /**\n   * Whether the page is in read-only mode.\n   * true for SampleData; false for ExistingData.\n   */\n  isReadonly: boolean\n\n  /** Whether the KeysBrowser panel should be shown (browse mode). */\n  showBrowser: boolean\n\n  /** Pre-selected key from navigation (triggers auto-selection on mount). */\n  initialKey?: RedisResponseBuffer\n\n  /** Pre-selected key type from navigation. */\n  initialKeyType?: RedisearchIndexKeyType\n\n  /** Display title for the page header. */\n  displayName: string\n\n  /** Editable index name (ExistingData) or derived name (SampleData). */\n  indexName: string\n  setIndexName: (name: string) => void\n\n  /** Editable index prefix. */\n  indexPrefix: string\n  setIndexPrefix: (prefix: string) => void\n\n  /** Key type for existing data (HASH or JSON). */\n  keyType: RedisearchIndexKeyType\n  setKeyType: (type: RedisearchIndexKeyType) => void\n\n  /** Current index fields. */\n  fields: IndexField[]\n  setFields: (fields: IndexField[], skippedFields?: string[]) => void\n\n  /** Field names skipped during inference (e.g. complex nested JSON objects). */\n  skippedFields: string[]\n\n  /** Row selection state for field include/exclude. */\n  rowSelection: RowSelectionState\n  onRowSelectionChange: (selection: RowSelectionState) => void\n\n  /** Generated FT.CREATE command. */\n  command: string\n\n  /** Validation error for index name (null when valid). */\n  indexNameError: string | null\n\n  /** Whether the fields have been modified since the last key load. */\n  isFieldsDirty: boolean\n\n  /** Reset the dirty flag (e.g. after confirming key change). */\n  resetFieldsDirty: () => void\n\n  /** Whether the \"Create index\" button should be disabled. */\n  isCreateDisabled: boolean\n\n  /** Human-readable reason why \"Create index\" is disabled (null when enabled). */\n  createDisabledReason: string | null\n\n  /** Action state. */\n  loading: boolean\n  handleCreateIndex: () => void\n  handleCancel: () => void\n\n  /** Field modal state and handlers. */\n  fieldModal: FieldModalState\n  openAddFieldModal: () => void\n  openEditFieldModal: (field: IndexField) => void\n  closeFieldModal: () => void\n  handleFieldSubmit: (field: IndexField) => void\n}\n\nexport interface CreateIndexPageProviderProps {\n  instanceId: string\n  mode?: CreateIndexMode\n  sampleData?: SampleDataContent\n  showBrowser?: boolean\n  initialKey?: RedisResponseBuffer\n  initialKeyType?: RedisearchIndexKeyType\n  initialPrefix?: string\n  children: React.ReactNode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-page/CreateIndexPageProvider.tsx",
    "content": "import React, { useCallback, useMemo, useRef, useState } from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useDispatch } from 'react-redux'\n\nimport { Pages } from 'uiSrc/constants'\nimport { RowSelectionState } from 'uiSrc/components/base/layout/table'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  CommandExecutionType,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport { fetchRedisearchListAction } from 'uiSrc/slices/browser/redisearch'\nimport CommandsHistoryService from 'uiSrc/services/commands-history/commandsHistoryService'\n\nimport { IndexField } from '../../components/index-details/IndexDetails.types'\nimport { FieldTypeModalMode } from '../../components/field-type-modal'\nimport {\n  useCreateIndexCommand,\n  useCreateIndexFlow,\n  useIndexNameValidation,\n} from '../../hooks'\nimport {\n  getFieldsBySampleData,\n  getDisplayNameBySampleData,\n  getIndexPrefixBySampleData,\n  getIndexNameBySampleData,\n} from '../../utils/sampleData'\nimport { generateDynamicFtCreateCommand } from '../../utils/generateDynamicFtCreateCommand'\nimport { deriveIndexName, encodeIndexNameForUrl } from '../../utils'\nimport {\n  CreateIndexTab,\n  CreateIndexMode,\n} from '../../pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\nimport { createIndexNotifications } from '../../constants'\nimport {\n  SearchTelemetryCancelStep,\n  SearchTelemetryFieldEditAction,\n} from '../../telemetry.constants'\nimport { getFieldTypeSummary } from '../../utils/telemetry.utils'\n\nimport {\n  CreateIndexPageProviderProps,\n  FieldModalState,\n} from './CreateIndexPageContext.types'\nimport { CreateIndexPageContext } from './CreateIndexPageContext'\n\nconst INITIAL_FIELD_MODAL_STATE: FieldModalState = {\n  isOpen: false,\n  mode: FieldTypeModalMode.Create,\n  field: undefined,\n}\n\nconst DEFAULT_INDEX_PREFIX = ''\n\nexport const CreateIndexPageProvider = ({\n  instanceId,\n  sampleData,\n  mode: modeProp,\n  showBrowser: showBrowserProp = true,\n  initialKey: initialKeyProp,\n  initialKeyType: initialKeyTypeProp,\n  initialPrefix: initialPrefixProp,\n  children,\n}: CreateIndexPageProviderProps) => {\n  const mode = modeProp ?? CreateIndexMode.SampleData\n  const isSampleData = mode === CreateIndexMode.SampleData\n\n  const [activeTab, setActiveTab] = useState<CreateIndexTab>(\n    CreateIndexTab.Table,\n  )\n\n  const changeActiveTab = useCallback(\n    (tab: CreateIndexTab) => {\n      setActiveTab(tab)\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_TAB_CHANGED,\n        eventData: { databaseId: instanceId, tab },\n      })\n    },\n    [instanceId],\n  )\n  const [fieldModal, setFieldModal] = useState<FieldModalState>(\n    INITIAL_FIELD_MODAL_STATE,\n  )\n\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  // --- Sample data mode hooks (only meaningful when isSampleData) ---\n  const { command: sampleCommand } = useCreateIndexCommand(sampleData)\n  const { run: createSampleIndexFlow, loading: sampleLoading } =\n    useCreateIndexFlow()\n\n  // --- Fields ---\n  const sampleFields = useMemo(\n    () => (sampleData ? getFieldsBySampleData(sampleData) : []),\n    [sampleData],\n  )\n  const [editableFields, setEditableFields] = useState<IndexField[] | null>(\n    null,\n  )\n  const fields = isSampleData\n    ? (editableFields ?? sampleFields)\n    : (editableFields ?? [])\n\n  const [skippedFields, setSkippedFields] = useState<string[]>([])\n\n  const setFields = useCallback(\n    (newFields: IndexField[], skipped?: string[]) => {\n      setEditableFields(newFields)\n      setSkippedFields(skipped ?? [])\n      const initialSelection: RowSelectionState = {}\n      newFields.forEach((f) => {\n        initialSelection[f.id] = true\n      })\n      setRowSelection(initialSelection)\n      setIsFieldsDirty(false)\n\n      if (newFields.length > 0) {\n        sendEventTelemetry({\n          event: TelemetryEvent.SEARCH_INDEX_AUTO_SUGGESTION_VIEWED,\n          eventData: {\n            databaseId: instanceId,\n            data_source: mode,\n            field_types: getFieldTypeSummary(newFields),\n            number_of_fields: newFields.length,\n            number_of_skipped: skipped?.length ?? 0,\n          },\n        })\n      }\n    },\n    [instanceId, mode],\n  )\n\n  // --- Index name ---\n  const [indexName, setIndexName] = useState<string>(() => {\n    if (isSampleData && sampleData) return getIndexNameBySampleData(sampleData)\n    if (initialPrefixProp) return deriveIndexName(initialPrefixProp)\n    return deriveIndexName('')\n  })\n\n  // --- Index prefix ---\n  const [indexPrefix, setIndexPrefix] = useState<string>(() => {\n    if (isSampleData && sampleData)\n      return getIndexPrefixBySampleData(sampleData)\n    if (initialPrefixProp) return initialPrefixProp\n    return DEFAULT_INDEX_PREFIX\n  })\n\n  // --- Key type (only for existing data) ---\n  const [keyType, setKeyType] = useState<RedisearchIndexKeyType>(\n    initialKeyTypeProp ?? RedisearchIndexKeyType.HASH,\n  )\n\n  // --- Row selection ---\n  const [rowSelection, setRowSelection] = useState<RowSelectionState>({})\n\n  // --- Dirty tracking ---\n  const [isFieldsDirty, setIsFieldsDirty] = useState(false)\n  const resetFieldsDirty = useCallback(() => setIsFieldsDirty(false), [])\n\n  // --- Validation ---\n  const indexNameError = useIndexNameValidation(!isSampleData ? indexName : '')\n\n  // --- Derived values ---\n  const isReadonly = isSampleData\n\n  const displayName = useMemo(() => {\n    if (isSampleData && sampleData)\n      return getDisplayNameBySampleData(sampleData)\n    return 'existing data'\n  }, [isSampleData, sampleData])\n\n  const showBrowser = !isSampleData && showBrowserProp\n\n  const selectedFields = useMemo(() => {\n    if (isSampleData) return fields\n    return fields.filter((f) => rowSelection[f.id])\n  }, [isSampleData, fields, rowSelection])\n\n  const dynamicCommand = useMemo(() => {\n    if (isSampleData) return sampleCommand\n    if (selectedFields.length === 0) return ''\n    return generateDynamicFtCreateCommand({\n      indexName: indexName.trim(),\n      keyType,\n      prefix: indexPrefix,\n      fields: selectedFields,\n    })\n  }, [\n    isSampleData,\n    sampleCommand,\n    selectedFields,\n    indexName,\n    keyType,\n    indexPrefix,\n  ])\n\n  const createDisabledReason = useMemo((): string | null => {\n    if (isSampleData) return null\n    if (selectedFields.length === 0)\n      return 'Select a key and at least one field to index.'\n    if (indexNameError !== null) return indexNameError\n    return null\n  }, [isSampleData, indexNameError, selectedFields])\n\n  const isCreateDisabled = createDisabledReason !== null\n\n  // --- Command execution for existing data ---\n  const commandsHistoryService = useRef(\n    new CommandsHistoryService(CommandExecutionType.Search),\n  ).current\n  const [existingDataLoading, setExistingDataLoading] = useState(false)\n\n  const handleCreateExistingDataIndex = useCallback(async () => {\n    if (!dynamicCommand || isCreateDisabled) return\n\n    setExistingDataLoading(true)\n    try {\n      const results = await commandsHistoryService.addCommandsToHistory(\n        instanceId,\n        [dynamicCommand],\n        {\n          activeRunQueryMode: RunQueryMode.Raw,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      const failedResult = results[0]?.result?.find(\n        (r) => r.status === CommandExecutionStatus.Fail,\n      )\n\n      if (failedResult) {\n        const errorMessage =\n          typeof failedResult.response === 'string'\n            ? failedResult.response\n            : undefined\n        dispatch(\n          addMessageNotification(\n            createIndexNotifications.createFailed(errorMessage),\n          ),\n        )\n        sendEventTelemetry({\n          event: TelemetryEvent.SEARCH_CREATE_INDEX_ERROR,\n          eventData: { databaseId: instanceId, data_source: mode },\n        })\n        return\n      }\n\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_CREATED,\n        eventData: {\n          databaseId: instanceId,\n          data_source: mode,\n          number_of_indexed_fields: selectedFields.length,\n          field_types: getFieldTypeSummary(selectedFields),\n          fields_modified: isFieldsDirty,\n          key_type: keyType,\n        },\n      })\n\n      dispatch(fetchRedisearchListAction())\n      dispatch(addMessageNotification(createIndexNotifications.indexCreated()))\n      history.push(\n        Pages.vectorSearchQuery(\n          instanceId,\n          encodeIndexNameForUrl(indexName.trim()),\n        ),\n      )\n    } catch {\n      dispatch(addMessageNotification(createIndexNotifications.createFailed()))\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_ERROR,\n        eventData: { databaseId: instanceId, data_source: mode },\n      })\n    } finally {\n      setExistingDataLoading(false)\n    }\n  }, [\n    dynamicCommand,\n    isCreateDisabled,\n    isFieldsDirty,\n    instanceId,\n    indexName,\n    commandsHistoryService,\n    dispatch,\n    history,\n    mode,\n    selectedFields,\n    keyType,\n  ])\n\n  // --- Telemetry callbacks for sample data index creation ---\n  const onSampleIndexCreated = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_INDEX_CREATED,\n      eventData: {\n        databaseId: instanceId,\n        data_source: mode,\n        number_of_indexed_fields: selectedFields.length,\n        field_types: getFieldTypeSummary(selectedFields),\n        fields_modified: isFieldsDirty,\n      },\n    })\n  }, [instanceId, mode, selectedFields, isFieldsDirty])\n\n  const onSampleIndexError = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_ERROR,\n      eventData: { databaseId: instanceId, data_source: mode },\n    })\n  }, [instanceId, mode])\n\n  // --- Actions ---\n  const loading = isSampleData ? sampleLoading : existingDataLoading\n\n  const handleCreateIndex = useCallback(() => {\n    if (isSampleData && sampleData) {\n      createSampleIndexFlow(instanceId, sampleData, {\n        onSuccess: onSampleIndexCreated,\n        onError: onSampleIndexError,\n      })\n      return\n    }\n    handleCreateExistingDataIndex()\n  }, [\n    isSampleData,\n    sampleData,\n    createSampleIndexFlow,\n    instanceId,\n    onSampleIndexCreated,\n    onSampleIndexError,\n    handleCreateExistingDataIndex,\n  ])\n\n  const handleCancel = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_CANCELLED,\n      eventData: {\n        databaseId: instanceId,\n        data_source: mode,\n        step: SearchTelemetryCancelStep.IndexDefinition,\n      },\n    })\n\n    history.push(Pages.vectorSearch(instanceId))\n  }, [history, instanceId, mode])\n\n  // --- Field modal ---\n  const openAddFieldModal = useCallback(() => {\n    setFieldModal({\n      isOpen: true,\n      mode: FieldTypeModalMode.Create,\n      field: undefined,\n    })\n  }, [])\n\n  const openEditFieldModal = useCallback((field: IndexField) => {\n    setFieldModal({\n      isOpen: true,\n      mode: FieldTypeModalMode.Edit,\n      field,\n    })\n  }, [])\n\n  const closeFieldModal = useCallback(() => {\n    setFieldModal(INITIAL_FIELD_MODAL_STATE)\n  }, [])\n\n  const handleFieldSubmit = useCallback(\n    (updatedField: IndexField) => {\n      const action =\n        fieldModal.mode === FieldTypeModalMode.Create\n          ? SearchTelemetryFieldEditAction.Add\n          : SearchTelemetryFieldEditAction.Edit\n\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_CREATE_INDEX_FIELD_EDITED,\n        eventData: {\n          databaseId: instanceId,\n          field_type: updatedField.type,\n          action,\n        },\n      })\n\n      setEditableFields((prev) => {\n        const currentFields = isSampleData\n          ? (prev ?? sampleFields)\n          : (prev ?? [])\n\n        if (fieldModal.mode === FieldTypeModalMode.Create) {\n          return [...currentFields, updatedField]\n        }\n\n        return currentFields.map((f) =>\n          f.id === updatedField.id ? updatedField : f,\n        )\n      })\n\n      if (fieldModal.mode === FieldTypeModalMode.Create) {\n        setRowSelection((prev) => ({ ...prev, [updatedField.id]: true }))\n      }\n\n      setFieldModal(INITIAL_FIELD_MODAL_STATE)\n      setIsFieldsDirty(true)\n    },\n    [fieldModal.mode, sampleFields, isSampleData, instanceId],\n  )\n\n  const onRowSelectionChange = useCallback((selection: RowSelectionState) => {\n    setRowSelection(selection)\n    setIsFieldsDirty(true)\n  }, [])\n\n  // --- Context value ---\n  const value = useMemo(\n    () => ({\n      mode,\n      activeTab,\n      setActiveTab: changeActiveTab,\n      isReadonly,\n      showBrowser,\n      initialKey: initialKeyProp,\n      initialKeyType: initialKeyTypeProp,\n      displayName,\n      indexName,\n      setIndexName,\n      indexPrefix,\n      setIndexPrefix,\n      keyType,\n      setKeyType,\n      fields,\n      setFields,\n      skippedFields,\n      rowSelection,\n      onRowSelectionChange,\n      command: dynamicCommand,\n      indexNameError,\n      isFieldsDirty,\n      resetFieldsDirty,\n      isCreateDisabled,\n      createDisabledReason,\n      loading,\n      handleCreateIndex,\n      handleCancel,\n      fieldModal,\n      openAddFieldModal,\n      openEditFieldModal,\n      closeFieldModal,\n      handleFieldSubmit,\n    }),\n    [\n      mode,\n      activeTab,\n      changeActiveTab,\n      isReadonly,\n      showBrowser,\n      initialKeyProp,\n      initialKeyTypeProp,\n      displayName,\n      indexName,\n      indexPrefix,\n      keyType,\n      fields,\n      setFields,\n      skippedFields,\n      rowSelection,\n      onRowSelectionChange,\n      dynamicCommand,\n      indexNameError,\n      isFieldsDirty,\n      resetFieldsDirty,\n      isCreateDisabled,\n      createDisabledReason,\n      loading,\n      handleCreateIndex,\n      handleCancel,\n      fieldModal,\n      openAddFieldModal,\n      openEditFieldModal,\n      closeFieldModal,\n      handleFieldSubmit,\n    ],\n  )\n\n  return (\n    <CreateIndexPageContext.Provider value={value}>\n      {children}\n    </CreateIndexPageContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/create-index-page/index.ts",
    "content": "export { useCreateIndexPage } from './CreateIndexPageContext'\nexport { CreateIndexPageProvider } from './CreateIndexPageProvider'\nexport type {\n  CreateIndexPageContextValue,\n  CreateIndexPageProviderProps,\n  FieldModalState,\n} from './CreateIndexPageContext.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/vector-search/VectorSearchContext.tsx",
    "content": "import { createContext, useContext } from 'react'\n\nimport { VectorSearchContextValue } from './VectorSearchContext.types'\n\nexport const VectorSearchContext =\n  createContext<VectorSearchContextValue | null>(null)\n\nexport const useVectorSearch = (): VectorSearchContextValue => {\n  const ctx = useContext(VectorSearchContext)\n  if (!ctx) {\n    throw new Error('useVectorSearch must be used within VectorSearchProvider')\n  }\n  return ctx\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/vector-search/VectorSearchContext.types.ts",
    "content": "import { SearchTelemetrySource } from '../../telemetry.constants'\n\nexport interface VectorSearchContextValue {\n  openPickSampleDataModal: (source: SearchTelemetrySource) => void\n  navigateToExistingDataFlow: (source: SearchTelemetrySource) => void\n  hasExistingKeys: boolean\n  hasExistingKeysLoading: boolean\n}\n\nexport interface VectorSearchProviderProps {\n  children: React.ReactNode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/vector-search/VectorSearchProvider.tsx",
    "content": "import React, { useCallback, useMemo, useState } from 'react'\nimport { useHistory, useParams } from 'react-router-dom'\n\nimport { Pages } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { IndexField } from '../../components/index-details/IndexDetails.types'\nimport { PickSampleDataModal } from '../../components/pick-sample-data-modal'\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { useCreateIndexFlow, useHasExistingKeys } from '../../hooks'\nimport { CreateIndexMode } from '../../pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\nimport {\n  SearchTelemetryCancelStep,\n  SearchTelemetryDemoDataNextStep,\n  SearchTelemetrySource,\n} from '../../telemetry.constants'\nimport { getFieldTypeSummary } from '../../utils/telemetry.utils'\nimport { getFieldsBySampleData } from '../../utils/sampleData'\nimport { VectorSearchProviderProps } from './VectorSearchContext.types'\nimport { VectorSearchContext } from './VectorSearchContext'\n\nexport const VectorSearchProvider = ({\n  children,\n}: VectorSearchProviderProps) => {\n  const [isSampleDataModalOpen, setIsSampleDataModalOpen] = useState(false)\n  const [selectedDataset, setSelectedDataset] =\n    useState<SampleDataContent | null>(null)\n\n  const history = useHistory()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const { run: createIndexFlow, loading: createIndexLoading } =\n    useCreateIndexFlow()\n  const { hasKeys: hasExistingKeys, loading: hasExistingKeysLoading } =\n    useHasExistingKeys()\n\n  const openPickSampleDataModal = useCallback(\n    (source: SearchTelemetrySource) => {\n      setSelectedDataset(null)\n      setIsSampleDataModalOpen(true)\n\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_DEMO_ONBOARDING_TRIGGERED,\n        eventData: { databaseId: instanceId, source },\n      })\n    },\n    [instanceId],\n  )\n\n  const dismissSampleDataModal = useCallback(() => {\n    setIsSampleDataModalOpen(false)\n    setSelectedDataset(null)\n  }, [])\n\n  const cancelSampleDataModal = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_CANCELLED,\n      eventData: {\n        databaseId: instanceId,\n        step: SearchTelemetryCancelStep.SampleDataModal,\n      },\n    })\n\n    dismissSampleDataModal()\n  }, [instanceId, dismissSampleDataModal])\n\n  const handleSelectDataset = useCallback((value: SampleDataContent) => {\n    setSelectedDataset(value)\n  }, [])\n\n  const handleSeeIndexDefinition = useCallback(\n    (dataset: SampleDataContent) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_DEMO_DATA_SELECTED,\n        eventData: {\n          databaseId: instanceId,\n          dataset,\n          next_step: SearchTelemetryDemoDataNextStep.IndexDefinition,\n        },\n      })\n\n      dismissSampleDataModal()\n      history.push({\n        pathname: Pages.vectorSearchCreateIndex(instanceId),\n        search: `?sampleData=${dataset}`,\n      })\n    },\n    [dismissSampleDataModal, history, instanceId],\n  )\n\n  const onStartQueryingIndexCreated = useCallback(\n    (fields: IndexField[]) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_CREATED,\n        eventData: {\n          databaseId: instanceId,\n          data_source: CreateIndexMode.SampleData,\n          number_of_indexed_fields: fields.length,\n          field_types: getFieldTypeSummary(fields),\n          fields_modified: false,\n        },\n      })\n    },\n    [instanceId],\n  )\n\n  const onStartQueryingIndexError = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CREATE_INDEX_ERROR,\n      eventData: {\n        databaseId: instanceId,\n        data_source: CreateIndexMode.SampleData,\n      },\n    })\n  }, [instanceId])\n\n  const handleStartQuerying = useCallback(\n    (dataset: SampleDataContent) => {\n      const fields = getFieldsBySampleData(dataset)\n\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_DEMO_DATA_SELECTED,\n        eventData: {\n          databaseId: instanceId,\n          dataset,\n          next_step: SearchTelemetryDemoDataNextStep.StartQuerying,\n        },\n      })\n\n      createIndexFlow(instanceId, dataset, {\n        onSuccess: () => {\n          dismissSampleDataModal()\n          onStartQueryingIndexCreated(fields)\n        },\n        onError: () => {\n          dismissSampleDataModal()\n          onStartQueryingIndexError()\n        },\n      })\n    },\n    [\n      dismissSampleDataModal,\n      createIndexFlow,\n      instanceId,\n      onStartQueryingIndexCreated,\n      onStartQueryingIndexError,\n    ],\n  )\n\n  const navigateToExistingDataFlow = useCallback(\n    (source: SearchTelemetrySource) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_OWN_DATA_INDEX_TRIGGERED,\n        eventData: { databaseId: instanceId, source },\n      })\n\n      history.push({\n        pathname: Pages.vectorSearchCreateIndex(instanceId),\n        search: `?mode=${CreateIndexMode.ExistingData}`,\n      })\n    },\n    [history, instanceId],\n  )\n\n  const contextValue = useMemo(\n    () => ({\n      openPickSampleDataModal,\n      navigateToExistingDataFlow,\n      hasExistingKeys,\n      hasExistingKeysLoading,\n    }),\n    [\n      openPickSampleDataModal,\n      navigateToExistingDataFlow,\n      hasExistingKeys,\n      hasExistingKeysLoading,\n    ],\n  )\n\n  return (\n    <VectorSearchContext.Provider value={contextValue}>\n      {children}\n      <PickSampleDataModal\n        isOpen={isSampleDataModalOpen}\n        loading={createIndexLoading}\n        selectedDataset={selectedDataset}\n        onSelectDataset={handleSelectDataset}\n        onCancel={cancelSampleDataModal}\n        onSeeIndexDefinition={handleSeeIndexDefinition}\n        onStartQuerying={handleStartQuerying}\n      />\n    </VectorSearchContext.Provider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/context/vector-search/index.ts",
    "content": "export { useVectorSearch } from './VectorSearchContext'\nexport { VectorSearchProvider } from './VectorSearchProvider'\nexport type {\n  VectorSearchContextValue,\n  VectorSearchProviderProps,\n} from './VectorSearchContext.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/index.ts",
    "content": "export { useIndexInfo } from './useIndexInfo'\nexport type {\n  IndexInfo,\n  IndexAttribute,\n  IndexDefinition,\n  IndexOptions,\n  UseIndexInfoOptions,\n  UseIndexInfoResult,\n} from './useIndexInfo'\n\nexport { useIndexListData } from './useIndexListData'\nexport type { UseIndexListDataResult } from './useIndexListData'\n\nexport { useRedisInstanceCompatibility } from './useRedisInstanceCompatibility'\nexport type { UseRedisInstanceCompatibilityReturn } from './useRedisInstanceCompatibility'\n\nexport { useRedisearchListData } from './useRedisearchListData'\nexport type { UseRedisearchListDataReturn } from './useRedisearchListData'\n\nexport { useCreateIndex } from './useCreateIndex'\nexport type { CreateIndexParams, UseCreateIndexResult } from './useCreateIndex'\n\nexport { useCreateIndexCommand } from './useCreateIndexCommand'\nexport type { UseCreateIndexCommandResult } from './useCreateIndexCommand'\n\nexport { useCreateIndexFlow } from './useCreateIndexFlow'\nexport type { UseCreateIndexFlowResult } from './useCreateIndexFlow'\n\nexport { useIsKeyIndexed, UseIsKeyIndexedStatus } from './useIsKeyIndexed'\nexport type { UseIsKeyIndexedResult } from './useIsKeyIndexed'\n\nexport {\n  useIndexNameValidation,\n  INDEX_NAME_ERRORS,\n} from './useIndexNameValidation'\n\nexport { useLoadKeyData } from './useLoadKeyData'\nexport type { UseLoadKeyDataResult } from './useLoadKeyData'\n\nexport { useHasExistingKeys } from './useHasExistingKeys'\nexport type { UseHasExistingKeysResult } from './useHasExistingKeys'\n\nexport { useListContent } from './useListContent'\n\nexport { useLastViewedPage } from './useLastViewedPage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndex/index.ts",
    "content": "export { useCreateIndex } from './useCreateIndex'\nexport type { CreateIndexParams, UseCreateIndexResult } from './useCreateIndex'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndex/useCreateIndex.spec.ts",
    "content": "import { renderHook, act } from '@testing-library/react-hooks'\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { useCreateIndex, CreateIndexParams } from './useCreateIndex'\n\nconst mockLoad = jest.fn()\nconst mockAddCommandsToHistory = jest.fn()\n\njest.mock('uiSrc/services/hooks', () => ({\n  useLoadData: () => ({\n    load: mockLoad,\n  }),\n}))\n\njest.mock('../../utils/generateFtCreateCommand', () => ({\n  generateFtCreateCommand: () => 'FT.CREATE idx:bikes_vss ...',\n}))\n\njest.mock('uiSrc/services/commands-history/commandsHistoryService', () => ({\n  __esModule: true,\n  default: jest.fn().mockImplementation(() => ({\n    addCommandsToHistory: mockAddCommandsToHistory,\n  })),\n}))\n\nconst mockOnSuccess = jest.fn(async () => {})\nconst mockOnError = jest.fn(async () => {})\n\ndescribe('useCreateIndex', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  const defaultParams: CreateIndexParams = {\n    instanceId: 'test-instance-id',\n    dataContent: SampleDataContent.E_COMMERCE_DISCOVERY,\n    indexName: 'bikes',\n  }\n\n  it('should complete flow successfully', async () => {\n    mockLoad.mockResolvedValue(undefined)\n    mockAddCommandsToHistory.mockResolvedValue([])\n\n    const { result } = renderHook(() => useCreateIndex())\n\n    await act(async () => {\n      await result.current.run(defaultParams, mockOnSuccess, mockOnError)\n    })\n\n    expect(mockLoad).toHaveBeenCalledWith('test-instance-id', 'bikes')\n    expect(mockAddCommandsToHistory).toHaveBeenCalledWith(\n      'test-instance-id',\n      ['FT.CREATE idx:bikes_vss ...'],\n      {\n        activeRunQueryMode: 'RAW',\n        resultsMode: 'DEFAULT',\n      },\n    )\n    expect(result.current.success).toBe(true)\n    expect(result.current.error).toBeNull()\n    expect(result.current.loading).toBe(false)\n    expect(mockOnSuccess).toHaveBeenCalled()\n    expect(mockOnError).not.toHaveBeenCalled()\n  })\n\n  it('should handle error if instanceId is missing', async () => {\n    const { result } = renderHook(() => useCreateIndex())\n\n    await act(async () => {\n      await result.current.run({ ...defaultParams, instanceId: '' })\n    })\n\n    expect(result.current.success).toBe(false)\n    expect(result.current.error?.message).toMatch(/Instance ID is required/)\n    expect(result.current.loading).toBe(false)\n    expect(mockLoad).not.toHaveBeenCalled()\n    expect(mockAddCommandsToHistory).not.toHaveBeenCalled()\n  })\n\n  it('should handle failure in data loading', async () => {\n    const error = new Error('Failed to load')\n    mockLoad.mockRejectedValue(error)\n\n    const { result } = renderHook(() => useCreateIndex())\n\n    await act(async () => {\n      await result.current.run(defaultParams, mockOnSuccess, mockOnError)\n    })\n\n    expect(mockLoad).toHaveBeenCalled()\n    expect(result.current.success).toBe(false)\n    expect(result.current.error).toBe(error)\n    expect(result.current.loading).toBe(false)\n    expect(mockAddCommandsToHistory).not.toHaveBeenCalled()\n    expect(mockOnSuccess).not.toHaveBeenCalled()\n    expect(mockOnError).toHaveBeenCalled()\n  })\n\n  it('should handle command history service failure', async () => {\n    mockLoad.mockResolvedValue(undefined)\n    mockAddCommandsToHistory.mockRejectedValue(\n      new Error('Command history service failed'),\n    )\n\n    const { result } = renderHook(() => useCreateIndex())\n\n    await act(async () => {\n      await result.current.run(defaultParams)\n    })\n\n    expect(mockAddCommandsToHistory).toHaveBeenCalled()\n    expect(result.current.success).toBe(false)\n    expect(result.current.error).toBeInstanceOf(Error)\n    expect(result.current.error?.message).toBe('Command history service failed')\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should await async onSuccess before setting loading to false', async () => {\n    mockLoad.mockResolvedValue(undefined)\n    mockAddCommandsToHistory.mockResolvedValue([])\n\n    const order: string[] = []\n    const asyncOnSuccess = jest.fn(async () => {\n      order.push('onSuccess:start')\n      await Promise.resolve()\n      order.push('onSuccess:end')\n    })\n\n    const { result } = renderHook(() => useCreateIndex())\n\n    await act(async () => {\n      await result.current.run(defaultParams, asyncOnSuccess)\n    })\n\n    expect(asyncOnSuccess).toHaveBeenCalled()\n    expect(order).toEqual(['onSuccess:start', 'onSuccess:end'])\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should handle movies data content correctly', async () => {\n    mockLoad.mockResolvedValue(undefined)\n    mockAddCommandsToHistory.mockResolvedValue([])\n\n    const { result } = renderHook(() => useCreateIndex())\n\n    const moviesParams: CreateIndexParams = {\n      ...defaultParams,\n      dataContent: SampleDataContent.CONTENT_RECOMMENDATIONS,\n    }\n\n    await act(async () => {\n      await result.current.run(moviesParams)\n    })\n\n    expect(mockLoad).toHaveBeenCalledWith('test-instance-id', 'movies')\n    expect(mockAddCommandsToHistory).toHaveBeenCalledWith(\n      'test-instance-id',\n      ['FT.CREATE idx:bikes_vss ...'],\n      {\n        activeRunQueryMode: 'RAW',\n        resultsMode: 'DEFAULT',\n      },\n    )\n    expect(result.current.success).toBe(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndex/useCreateIndex.ts",
    "content": "import { useCallback, useRef, useState } from 'react'\nimport { useLoadData } from 'uiSrc/services/hooks'\nimport { generateFtCreateCommand } from '../../utils/generateFtCreateCommand'\nimport {\n  CommandExecutionType,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport CommandsHistoryService from 'uiSrc/services/commands-history/commandsHistoryService'\n\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { getCollectionNameBySampleData } from '../../utils/sampleData'\n\nexport interface CreateIndexParams {\n  instanceId: string\n  indexName: string\n  dataContent: SampleDataContent\n}\n\nexport interface UseCreateIndexResult {\n  run: (\n    params: CreateIndexParams,\n    onSuccess?: () => Promise<void>,\n    onError?: () => Promise<void>,\n  ) => Promise<void>\n  loading: boolean\n  error: Error | null\n  success: boolean\n}\n\nexport const useCreateIndex = (): UseCreateIndexResult => {\n  const [success, setSuccess] = useState(false)\n  const [loading, setLoading] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n\n  const commandsHistoryService = useRef(\n    new CommandsHistoryService(CommandExecutionType.Search),\n  ).current\n\n  const { load } = useLoadData()\n\n  const run = useCallback(\n    async (\n      { instanceId, indexName, dataContent }: CreateIndexParams,\n      onSuccess?: () => Promise<void>,\n      onError?: () => Promise<void>,\n    ) => {\n      setSuccess(false)\n      setError(null)\n      setLoading(true)\n\n      try {\n        const collectionName = getCollectionNameBySampleData(dataContent)\n\n        if (!instanceId) {\n          throw new Error('Instance ID is required')\n        }\n\n        // Step 1: Load the vector collection data\n        await load(instanceId, collectionName)\n\n        // Step 2: Create the search index command\n        const cmd = generateFtCreateCommand({\n          indexName,\n          dataContent,\n        })\n\n        // Step 3: Persist results so Vector Search history shows it\n        await commandsHistoryService.addCommandsToHistory(instanceId, [cmd], {\n          activeRunQueryMode: RunQueryMode.Raw,\n          resultsMode: ResultsMode.Default,\n        })\n\n        setSuccess(true)\n        await onSuccess?.()\n      } catch (e) {\n        setError(e instanceof Error ? e : new Error(String(e)))\n        await onError?.()\n      } finally {\n        setLoading(false)\n      }\n    },\n    [load],\n  )\n\n  return {\n    run,\n    loading,\n    error,\n    success,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndexCommand/index.ts",
    "content": "export { useCreateIndexCommand } from './useCreateIndexCommand'\nexport type { UseCreateIndexCommandResult } from './useCreateIndexCommand'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndexCommand/useCreateIndexCommand.spec.ts",
    "content": "import { renderHook } from '@testing-library/react-hooks'\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { useCreateIndexCommand } from './useCreateIndexCommand'\nimport { PresetIndexName } from '../../utils/sampleData'\n\ndescribe('useCreateIndexCommand', () => {\n  it('should return command and indexName for e-commerce discovery', () => {\n    const { result } = renderHook(() =>\n      useCreateIndexCommand(SampleDataContent.E_COMMERCE_DISCOVERY),\n    )\n\n    expect(result.current.indexName).toBe(PresetIndexName.BIKES)\n    expect(result.current.command).toContain('FT.CREATE')\n    expect(result.current.command).toContain(PresetIndexName.BIKES)\n  })\n\n  it('should return command and indexName for content recommendations', () => {\n    const { result } = renderHook(() =>\n      useCreateIndexCommand(SampleDataContent.CONTENT_RECOMMENDATIONS),\n    )\n\n    expect(result.current.indexName).toBe(PresetIndexName.MOVIES)\n    expect(result.current.command).toContain('FT.CREATE')\n    expect(result.current.command).toContain(PresetIndexName.MOVIES)\n  })\n\n  it('should return empty command and indexName when sampleData is undefined', () => {\n    const { result } = renderHook(() => useCreateIndexCommand(undefined))\n\n    expect(result.current.indexName).toBe('')\n    expect(result.current.command).toBe('')\n  })\n\n  it('should allow overriding the index name', () => {\n    const customName = 'my-custom-index'\n    const { result } = renderHook(() =>\n      useCreateIndexCommand(SampleDataContent.E_COMMERCE_DISCOVERY, customName),\n    )\n\n    expect(result.current.indexName).toBe(customName)\n    expect(result.current.command).toContain(customName)\n  })\n\n  it('should return empty defaults when sampleData is undefined', () => {\n    const { result } = renderHook(() => useCreateIndexCommand(undefined))\n\n    expect(result.current.indexName).toBe('')\n    expect(result.current.command).toBe('')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndexCommand/useCreateIndexCommand.ts",
    "content": "import { useMemo } from 'react'\nimport { generateFtCreateCommand } from '../../utils/generateFtCreateCommand'\n\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { getIndexNameBySampleData } from '../../utils/sampleData'\n\nexport interface UseCreateIndexCommandResult {\n  command: string\n  indexName: string\n}\n\n/**\n * Hook that derives the FT.CREATE command for a given sample data choice.\n * Returns the command string and the resolved index name.\n *\n * When sampleData is undefined (e.g. ExistingData mode), returns empty defaults.\n *\n * Designed to be extensible: later, this can accept a key-based definition\n * and generate the command dynamically based on existing key fields.\n */\nexport const useCreateIndexCommand = (\n  sampleData?: SampleDataContent,\n  indexNameOverride?: string,\n): UseCreateIndexCommandResult => {\n  const indexName =\n    indexNameOverride ??\n    (sampleData ? getIndexNameBySampleData(sampleData) : '')\n\n  const command = useMemo(() => {\n    if (!sampleData) {\n      return ''\n    }\n\n    return generateFtCreateCommand({\n      indexName,\n      dataContent: sampleData,\n    })\n  }, [indexName, sampleData])\n\n  return { command, indexName }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndexFlow/index.ts",
    "content": "export { useCreateIndexFlow } from './useCreateIndexFlow'\nexport type { UseCreateIndexFlowResult } from './useCreateIndexFlow'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndexFlow/useCreateIndexFlow.spec.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { renderHook, act, mockedStore, getMswURL } from 'uiSrc/utils/test-utils'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl } from 'uiSrc/utils'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { EditorTab } from '../../components/query-editor/QueryEditor.types'\nimport { createIndexNotifications } from '../../constants'\nimport { useCreateIndexFlow } from './useCreateIndexFlow'\n\nconst mockPush = jest.fn()\nconst mockCreateIndexRun = jest.fn()\n\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst routerDom = require('react-router-dom')\n\njest.mock('../useCreateIndex', () => ({\n  useCreateIndex: () => ({\n    run: mockCreateIndexRun,\n  }),\n}))\n\nlet mockExistingIndexes: string[] = []\njest.mock('../useRedisearchListData', () => ({\n  useRedisearchListData: () => ({\n    stringData: mockExistingIndexes,\n  }),\n}))\n\ndescribe('useCreateIndexFlow', () => {\n  const originalUseHistory = routerDom.useHistory\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockedStore.clearActions()\n    mockExistingIndexes = []\n\n    routerDom.useHistory = () => ({ push: mockPush })\n  })\n\n  afterAll(() => {\n    routerDom.useHistory = originalUseHistory\n  })\n\n  describe('when index does not exist', () => {\n    it('should invoke onSuccess callback after successful index creation', async () => {\n      const onSuccess = jest.fn()\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n          { onSuccess },\n        )\n      })\n\n      expect(onSuccess).not.toHaveBeenCalled()\n\n      const createOnSuccess = mockCreateIndexRun.mock.calls[0][1]\n      await act(async () => {\n        await createOnSuccess()\n      })\n\n      expect(onSuccess).toHaveBeenCalledTimes(1)\n    })\n\n    it('should invoke onError callback when index creation fails', async () => {\n      const onSuccess = jest.fn()\n      const onError = jest.fn()\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n          { onSuccess, onError },\n        )\n      })\n\n      const createOnError = mockCreateIndexRun.mock.calls[0][2]\n      await act(async () => {\n        await createOnError()\n      })\n\n      expect(onError).toHaveBeenCalledTimes(1)\n      expect(onSuccess).not.toHaveBeenCalled()\n    })\n\n    it('should call createIndex and seed sample queries on success', async () => {\n      const seedSpy = jest.spyOn(QueryLibraryService.prototype, 'seed')\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n        )\n      })\n\n      expect(mockCreateIndexRun).toHaveBeenCalledWith(\n        {\n          instanceId: INSTANCE_ID_MOCK,\n          indexName: 'idx:bikes_vss',\n          dataContent: SampleDataContent.E_COMMERCE_DISCOVERY,\n        },\n        expect.any(Function),\n        expect.any(Function),\n      )\n\n      const onSuccess = mockCreateIndexRun.mock.calls[0][1]\n      await act(async () => {\n        await onSuccess()\n      })\n\n      expect(seedSpy).toHaveBeenCalledWith(\n        INSTANCE_ID_MOCK,\n        expect.arrayContaining([\n          expect.objectContaining({\n            indexName: 'idx:bikes_vss',\n            name: expect.any(String),\n            query: expect.any(String),\n          }),\n        ]),\n      )\n\n      expect(mockPush).toHaveBeenCalledWith({\n        pathname: expect.stringContaining('idx%3Abikes_vss'),\n        state: { activeTab: EditorTab.Library },\n      })\n\n      expect(mockedStore.getActions()).toContainEqual(\n        addMessageNotification(createIndexNotifications.sampleDataCreated()),\n      )\n    })\n\n    it('should dispatch createFailed notification on error', async () => {\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n        )\n      })\n\n      const onError = mockCreateIndexRun.mock.calls[0][2]\n      act(() => {\n        onError()\n      })\n\n      expect(mockedStore.getActions()).toContainEqual(\n        addMessageNotification(createIndexNotifications.createFailed()),\n      )\n      expect(mockPush).not.toHaveBeenCalled()\n    })\n\n    it('should navigate only after seed completes', async () => {\n      const seedUrl = getMswURL(\n        getUrl(INSTANCE_ID_MOCK, ApiEndpoints.QUERY_LIBRARY_SEED),\n      )\n      let seedResponseSent = false\n      mswServer.use(\n        http.post(seedUrl, async () => {\n          await new Promise((resolve) => {\n            setTimeout(resolve, 0)\n          })\n          seedResponseSent = true\n          return HttpResponse.json([], { status: 200 })\n        }),\n      )\n      mockPush.mockImplementation(() => {\n        expect(seedResponseSent).toBe(true)\n      })\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n        )\n      })\n\n      const onSuccess = mockCreateIndexRun.mock.calls[0][1]\n      await act(async () => {\n        await onSuccess()\n      })\n\n      expect(mockPush).toHaveBeenCalled()\n    })\n  })\n\n  describe('when index already exists', () => {\n    beforeEach(() => {\n      mockExistingIndexes = ['idx:bikes_vss']\n    })\n\n    it('should seed sample queries and navigate to query page', async () => {\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        await result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n        )\n      })\n\n      expect(mockCreateIndexRun).not.toHaveBeenCalled()\n\n      expect(mockPush).toHaveBeenCalledWith({\n        pathname: expect.stringContaining('idx%3Abikes_vss'),\n        state: { activeTab: EditorTab.Library },\n      })\n\n      expect(mockedStore.getActions()).toContainEqual(\n        addMessageNotification(\n          createIndexNotifications.sampleDataAlreadyExists(),\n        ),\n      )\n    })\n\n    it('should invoke onSuccess callback when index already exists', async () => {\n      const onSuccess = jest.fn()\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        await result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n          { onSuccess },\n        )\n      })\n\n      expect(onSuccess).toHaveBeenCalledTimes(1)\n      expect(mockCreateIndexRun).not.toHaveBeenCalled()\n    })\n\n    it('should set loading to true while seeding sample queries', async () => {\n      let resolveSeed!: () => void\n      const seedPromise = new Promise<void>((resolve) => {\n        resolveSeed = resolve\n      })\n      jest\n        .spyOn(QueryLibraryService.prototype, 'seed')\n        .mockReturnValue(seedPromise)\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      act(() => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n        )\n      })\n\n      expect(result.current.loading).toBe(true)\n\n      await act(async () => {\n        resolveSeed()\n      })\n\n      expect(result.current.loading).toBe(false)\n    })\n\n    it('should invoke onError callback when seeding fails', async () => {\n      const onSuccess = jest.fn()\n      const onError = jest.fn()\n      const seedSpy = jest\n        .spyOn(QueryLibraryService.prototype, 'seed')\n        .mockRejectedValue(new Error('seed failed'))\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        await result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.E_COMMERCE_DISCOVERY,\n          { onSuccess, onError },\n        )\n      })\n\n      expect(onError).toHaveBeenCalledTimes(1)\n      expect(onSuccess).not.toHaveBeenCalled()\n      expect(mockPush).not.toHaveBeenCalled()\n      expect(result.current.loading).toBe(false)\n\n      seedSpy.mockRestore()\n    })\n  })\n\n  describe('movies dataset', () => {\n    it('should seed movie sample queries with correct index name', async () => {\n      const seedSpy = jest.spyOn(QueryLibraryService.prototype, 'seed')\n\n      const { result } = renderHook(() => useCreateIndexFlow())\n\n      await act(async () => {\n        result.current.run(\n          INSTANCE_ID_MOCK,\n          SampleDataContent.CONTENT_RECOMMENDATIONS,\n        )\n      })\n\n      const onSuccess = mockCreateIndexRun.mock.calls[0][1]\n      await act(async () => {\n        await onSuccess()\n      })\n\n      expect(seedSpy).toHaveBeenCalledWith(\n        INSTANCE_ID_MOCK,\n        expect.arrayContaining([\n          expect.objectContaining({\n            indexName: 'idx:movies_vss',\n            name: expect.any(String),\n            query: expect.any(String),\n          }),\n        ]),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useCreateIndexFlow/useCreateIndexFlow.ts",
    "content": "import { useCallback, useRef, useState } from 'react'\nimport { useHistory } from 'react-router-dom'\nimport { useDispatch } from 'react-redux'\n\nimport { Pages } from 'uiSrc/constants'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport { fetchRedisearchListAction } from 'uiSrc/slices/browser/redisearch'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\nimport { SeedQueryLibraryItem } from 'uiSrc/services/query-library/types'\n\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { EditorTab } from '../../components/query-editor/QueryEditor.types'\nimport { createIndexNotifications } from '../../constants'\nimport { useCreateIndex } from '../useCreateIndex'\nimport { useRedisearchListData } from '../useRedisearchListData'\nimport {\n  getIndexNameBySampleData,\n  getSampleQueriesBySampleData,\n} from '../../utils/sampleData'\nimport { encodeIndexNameForUrl } from '../../utils'\n\nexport interface CreateIndexFlowCallbacks {\n  onSuccess?: () => void\n  onError?: () => void\n}\n\nexport interface UseCreateIndexFlowResult {\n  /** Trigger index creation; navigates to query page on completion. */\n  run: (\n    instanceId: string,\n    dataset: SampleDataContent,\n    callbacks?: CreateIndexFlowCallbacks,\n  ) => void\n  loading: boolean\n}\n\n/**\n * Shared hook that encapsulates the \"create-index-or-navigate-if-exists\"\n * flow used by both VectorSearchContext (modal \"Start querying\") and\n * CreateIndexPageContext (\"Create index\" button).\n */\nexport const useCreateIndexFlow = (): UseCreateIndexFlowResult => {\n  const history = useHistory()\n  const dispatch = useDispatch()\n\n  const { run: createIndex } = useCreateIndex()\n  const { stringData: existingIndexes } = useRedisearchListData()\n  const queryLibraryService = useRef(new QueryLibraryService()).current\n  const [loading, setLoading] = useState(false)\n\n  const seedSampleQueries = useCallback(\n    async (\n      instanceId: string,\n      indexName: string,\n      dataset: SampleDataContent,\n    ) => {\n      const seedItems: SeedQueryLibraryItem[] = getSampleQueriesBySampleData(\n        dataset,\n      ).map((sq) => ({\n        indexName,\n        name: sq.name,\n        description: sq.description,\n        query: sq.query,\n      }))\n\n      await queryLibraryService.seed(instanceId, seedItems)\n    },\n    [queryLibraryService],\n  )\n\n  const navigateToLibrary = useCallback(\n    (instanceId: string, indexName: string) => {\n      history.push({\n        pathname: Pages.vectorSearchQuery(\n          instanceId,\n          encodeIndexNameForUrl(indexName),\n        ),\n        state: { activeTab: EditorTab.Library },\n      })\n    },\n    [history],\n  )\n\n  const run = useCallback(\n    async (\n      instanceId: string,\n      dataset: SampleDataContent,\n      callbacks?: CreateIndexFlowCallbacks,\n    ) => {\n      const indexName = getIndexNameBySampleData(dataset)\n      const indexAlreadyExists = existingIndexes.includes(indexName)\n\n      setLoading(true)\n      try {\n        if (indexAlreadyExists) {\n          dispatch(\n            addMessageNotification(\n              createIndexNotifications.sampleDataAlreadyExists(),\n            ),\n          )\n          await seedSampleQueries(instanceId, indexName, dataset)\n          callbacks?.onSuccess?.()\n          navigateToLibrary(instanceId, indexName)\n          return\n        }\n\n        await createIndex(\n          { instanceId, indexName, dataContent: dataset },\n          async () => {\n            dispatch(\n              addMessageNotification(\n                createIndexNotifications.sampleDataCreated(),\n              ),\n            )\n            dispatch(fetchRedisearchListAction())\n            await seedSampleQueries(instanceId, indexName, dataset)\n            callbacks?.onSuccess?.()\n            navigateToLibrary(instanceId, indexName)\n          },\n          async () => {\n            dispatch(\n              addMessageNotification(createIndexNotifications.createFailed()),\n            )\n            callbacks?.onError?.()\n          },\n        )\n      } catch {\n        callbacks?.onError?.()\n      } finally {\n        setLoading(false)\n      }\n    },\n    [\n      existingIndexes,\n      createIndex,\n      dispatch,\n      seedSampleQueries,\n      navigateToLibrary,\n    ],\n  )\n\n  return { run, loading }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useHasExistingKeys/index.ts",
    "content": "export { useHasExistingKeys } from './useHasExistingKeys'\nexport type { UseHasExistingKeysResult } from './useHasExistingKeys'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useHasExistingKeys/useHasExistingKeys.spec.ts",
    "content": "import { renderHook } from '@testing-library/react-hooks'\n\nimport { apiService } from 'uiSrc/services'\n\nimport { useHasExistingKeys } from './useHasExistingKeys'\n\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useLocation: jest.fn(() => ({ pathname: '/test' })),\n}))\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn((selector) => {\n    const state = {\n      connections: {\n        instances: {\n          connectedInstance: { id: 'test-instance' },\n        },\n      },\n      app: {\n        info: { encoding: 'utf-8' },\n      },\n    }\n    return selector(state)\n  }),\n}))\n\njest.mock('uiSrc/services', () => ({\n  apiService: {\n    post: jest.fn(),\n  },\n}))\n\nconst mockApiPost = apiService.post as jest.Mock\n\ndescribe('useHasExistingKeys', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should return hasKeys=true when Hash keys exist', async () => {\n    mockApiPost.mockResolvedValue({\n      status: 200,\n      data: [{ keys: [{ name: 'key:1' }], total: 1 }],\n    })\n\n    const { result, waitForNextUpdate } = renderHook(() => useHasExistingKeys())\n\n    await waitForNextUpdate()\n\n    expect(result.current.hasKeys).toBe(true)\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should return hasKeys=false when no keys exist', async () => {\n    mockApiPost.mockResolvedValue({\n      status: 200,\n      data: [{ keys: [], total: 0 }],\n    })\n\n    const { result, waitForNextUpdate } = renderHook(() => useHasExistingKeys())\n\n    await waitForNextUpdate()\n\n    expect(result.current.hasKeys).toBe(false)\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should return hasKeys=false on API error', async () => {\n    mockApiPost.mockRejectedValue(new Error('Network error'))\n\n    const { result, waitForNextUpdate } = renderHook(() => useHasExistingKeys())\n\n    await waitForNextUpdate()\n\n    expect(result.current.hasKeys).toBe(false)\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should be loading initially', () => {\n    mockApiPost.mockReturnValue(new Promise(() => {}))\n\n    const { result } = renderHook(() => useHasExistingKeys())\n\n    expect(result.current.loading).toBe(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useHasExistingKeys/useHasExistingKeys.ts",
    "content": "import { useCallback, useEffect, useState } from 'react'\nimport { useLocation } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints, KeyTypes } from 'uiSrc/constants'\nimport { getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\n\ninterface ScanResponse {\n  keys: unknown[]\n  total: number\n}\n\nexport interface UseHasExistingKeysResult {\n  hasKeys: boolean\n  loading: boolean\n}\n\nexport const useHasExistingKeys = (): UseHasExistingKeysResult => {\n  const [hasKeys, setHasKeys] = useState(false)\n  const [loading, setLoading] = useState(true)\n\n  const { pathname } = useLocation()\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { encoding } = useSelector(appInfoSelector)\n\n  const checkForKeys = useCallback(\n    async (signal?: AbortSignal) => {\n      if (!instanceId) {\n        setLoading(false)\n        return\n      }\n\n      setLoading(true)\n\n      try {\n        const types = [KeyTypes.Hash, KeyTypes.ReJSON]\n\n        const results = await Promise.all(\n          types.map((type) =>\n            apiService.post<ScanResponse[]>(\n              getUrl(instanceId, ApiEndpoints.KEYS),\n              {\n                cursor: '0',\n                count: 1,\n                type,\n                match: '*',\n                keysInfo: false,\n                scanThreshold: 1,\n              },\n              { params: { encoding }, signal },\n            ),\n          ),\n        )\n\n        if (signal?.aborted) return\n\n        const foundAny = results.some(({ data, status }) => {\n          if (!isStatusSuccessful(status)) return false\n          const keys = Array.isArray(data)\n            ? data[0]?.keys\n            : (data as unknown as ScanResponse)?.keys\n          return keys && keys.length > 0\n        })\n\n        setHasKeys(foundAny)\n      } catch (error) {\n        if (signal?.aborted) return\n        console.error('Failed to check for existing keys', error)\n        setHasKeys(false)\n      } finally {\n        if (!signal?.aborted) {\n          setLoading(false)\n        }\n      }\n    },\n    [instanceId, encoding],\n  )\n\n  useEffect(() => {\n    // Abort in-flight requests on unmount to prevent state updates after cleanup\n    const controller = new AbortController()\n    checkForKeys(controller.signal)\n\n    return () => {\n      controller.abort()\n    }\n  }, [checkForKeys, pathname])\n\n  return { hasKeys, loading }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexInfo/index.ts",
    "content": "export { useIndexInfo } from './useIndexInfo'\nexport { transformIndexInfo } from './useIndexInfo.utils'\nexport type {\n  IndexInfo,\n  IndexAttribute,\n  IndexDefinition,\n  IndexOptions,\n  UseIndexInfoOptions,\n  UseIndexInfoResult,\n} from './useIndexInfo.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexInfo/useIndexInfo.spec.ts",
    "content": "import { act, waitFor } from '@testing-library/react'\nimport { http, HttpResponse } from 'msw'\nimport { cloneDeep } from 'lodash'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport {\n  getMswURL,\n  mockStore,\n  renderHook,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { indexInfoFactory } from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory'\n\nimport { useIndexInfo } from './useIndexInfo'\n\nconst instanceId = 'test-instance-id'\nconst indexName = 'test-index'\n\nconst getMockedStore = () => {\n  const state = cloneDeep(initialStateDefault)\n  state.connections.instances.connectedInstance = {\n    ...state.connections.instances.connectedInstance,\n    id: instanceId,\n  }\n  return mockStore(state)\n}\n\ndescribe('useIndexInfo', () => {\n  beforeEach(() => {\n    mswServer.resetHandlers()\n  })\n\n  it('should return initial state correctly', () => {\n    const { result } = renderHook(() => useIndexInfo({ indexName: '' }), {\n      store: getMockedStore(),\n    })\n\n    expect(result.current.loading).toBe(true)\n    expect(result.current.error).toBeNull()\n    expect(result.current.indexInfo).toBeNull()\n  })\n\n  it('should fetch index info successfully', async () => {\n    const mockApiResponse = indexInfoFactory.build()\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async ({ request }) => {\n          const body = await request.json()\n          expect(body).toEqual({ index: indexName })\n          return HttpResponse.json(mockApiResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useIndexInfo({ indexName }), {\n      store: getMockedStore(),\n    })\n\n    expect(result.current.loading).toBe(true)\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.error).toBeNull()\n    expect(result.current.indexInfo).not.toBeNull()\n\n    // Verify transformation applied correctly\n    expect(result.current.indexInfo?.indexDefinition?.keyType).toBe(\n      mockApiResponse.index_definition?.key_type,\n    )\n    expect(result.current.indexInfo?.numDocs).toBe(\n      Number(mockApiResponse.num_docs),\n    )\n    expect(result.current.indexInfo?.attributes[0].type).toBe(\n      mockApiResponse.attributes[0].type.toLowerCase(),\n    )\n  })\n\n  it('should handle API errors correctly', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async () =>\n          HttpResponse.json({ message: 'Server error' }, { status: 500 }),\n      ),\n    )\n\n    const { result } = renderHook(() => useIndexInfo({ indexName }), {\n      store: getMockedStore(),\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.error).toBe('Failed to fetch index info')\n    expect(result.current.indexInfo).toBeNull()\n  })\n\n  it('should refetch when refetch is called', async () => {\n    let callCount = 0\n    const mockApiResponse = indexInfoFactory.build()\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async () => {\n          callCount++\n          return HttpResponse.json(mockApiResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useIndexInfo({ indexName }), {\n      store: getMockedStore(),\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(callCount).toBe(1)\n\n    // Call refetch\n    await act(async () => {\n      result.current.refetch()\n    })\n\n    await waitFor(() => {\n      expect(callCount).toBe(2)\n    })\n  })\n\n  it('should ignore stale responses when indexName changes rapidly', async () => {\n    const firstIndexResponse = indexInfoFactory.build({\n      index_name: 'idx:first',\n      num_docs: '100',\n    })\n    const secondIndexResponse = indexInfoFactory.build({\n      index_name: 'idx:second',\n      num_docs: '200',\n    })\n\n    let resolveFirst: () => void\n    const firstRequestPromise = new Promise<void>((resolve) => {\n      resolveFirst = resolve\n    })\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async ({ request }) => {\n          const body = (await request.json()) as { index: string }\n\n          if (body.index === 'first-index') {\n            // First request is delayed\n            await firstRequestPromise\n            return HttpResponse.json(firstIndexResponse, { status: 200 })\n          }\n          // Second request returns immediately\n          return HttpResponse.json(secondIndexResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result, rerender } = renderHook(\n      (initialProps) =>\n        useIndexInfo({\n          indexName: (initialProps as { name: string })?.name ?? '',\n        }),\n      {\n        store: getMockedStore(),\n        initialProps: { name: 'first-index' },\n      },\n    )\n\n    // First fetch starts\n    expect(result.current.loading).toBe(true)\n\n    // Change indexName before first request completes\n    rerender({ name: 'second-index' })\n\n    // Wait for second request to complete\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    // Should have data from second request\n    expect(result.current.indexInfo?.numDocs).toBe(200)\n\n    // Now complete the first (stale) request\n    await act(async () => {\n      resolveFirst!()\n    })\n\n    // Should still have data from second request (stale response ignored)\n    expect(result.current.indexInfo?.numDocs).toBe(200)\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should not get stuck in loading state when indexName changes while fetch is in progress', async () => {\n    let resolveFirstRequest: () => void\n    const firstRequestPromise = new Promise<void>((resolve) => {\n      resolveFirstRequest = resolve\n    })\n    const mockApiResponse = indexInfoFactory.build()\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async ({ request }) => {\n          const body = (await request.json()) as { index: string }\n\n          if (body.index === indexName) {\n            await firstRequestPromise\n          }\n          return HttpResponse.json(mockApiResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result, rerender } = renderHook(\n      (initialProps) =>\n        useIndexInfo({\n          indexName: (initialProps as { name: string })?.name ?? '',\n        }),\n      {\n        store: getMockedStore(),\n        initialProps: { name: indexName },\n      },\n    )\n\n    expect(result.current.loading).toBe(true)\n\n    // Change indexName while first fetch is in progress\n    rerender({ name: 'other-index' })\n\n    // Wait for second request to complete\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    // Complete the first (stale) request\n    await act(async () => {\n      resolveFirstRequest!()\n    })\n\n    // Should remain stable (stale response ignored)\n    expect(result.current.loading).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexInfo/useIndexInfo.ts",
    "content": "import { useState, useEffect, useCallback, useRef } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { IndexInfoDto } from 'apiSrc/modules/browser/redisearch/dto/index.info.dto'\n\nimport {\n  IndexInfo,\n  UseIndexInfoOptions,\n  UseIndexInfoResult,\n} from './useIndexInfo.types'\nimport { transformIndexInfo } from './useIndexInfo.utils'\n\n/**\n * Hook for fetching index information.\n *\n * @param options.indexName - Name of the index to fetch info for\n * @returns { indexInfo, loading, error, refetch }\n */\nexport const useIndexInfo = ({\n  indexName,\n}: UseIndexInfoOptions): UseIndexInfoResult => {\n  const [indexInfo, setIndexInfo] = useState<IndexInfo | null>(null)\n  const [loading, setLoading] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n\n  // Only selector needed - get instance ID for API URL\n  const connectedInstance = useSelector(connectedInstanceSelector)\n  const instanceId = connectedInstance?.id\n\n  // Track the current fetch request to ignore stale responses\n  const fetchIdRef = useRef(0)\n\n  const fetchIndexInfo = useCallback(async () => {\n    if (!instanceId) {\n      return\n    }\n\n    // Increment fetch ID to invalidate any in-flight requests\n    const currentFetchId = ++fetchIdRef.current\n\n    setLoading(true)\n    setError(null)\n\n    try {\n      const { data, status } = await apiService.post<IndexInfoDto>(\n        getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO),\n        { index: indexName },\n      )\n\n      // Ignore response if a newer fetch has been initiated\n      if (currentFetchId !== fetchIdRef.current) {\n        return\n      }\n\n      if (isStatusSuccessful(status)) {\n        setIndexInfo(transformIndexInfo(data))\n      }\n    } catch (err) {\n      // Ignore error if a newer fetch has been initiated\n      if (currentFetchId !== fetchIdRef.current) {\n        return\n      }\n      setError('Failed to fetch index info')\n    } finally {\n      // Only update loading state if this is still the current fetch\n      if (currentFetchId === fetchIdRef.current) {\n        setLoading(false)\n      }\n    }\n  }, [instanceId, indexName])\n\n  useEffect(() => {\n    fetchIndexInfo()\n\n    return () => {\n      fetchIdRef.current++\n    }\n  }, [indexName, fetchIndexInfo])\n\n  return { indexInfo, loading, error, refetch: fetchIndexInfo }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexInfo/useIndexInfo.types.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\n/**\n * Frontend types for index information.\n * The hook transforms API response (DTO) to these types.\n */\n\nexport interface IndexAttribute {\n  identifier: string\n  attribute: string\n  type: FieldTypes\n  weight?: string\n}\n\nexport interface IndexDefinition {\n  keyType: string\n  prefixes: string[]\n}\n\nexport interface IndexOptions {\n  filter?: string\n  defaultLang?: string\n}\n\n/**\n * Index information as used by the frontend.\n * This is the single source of truth for index data on the FE.\n */\nexport interface IndexInfo {\n  indexDefinition: IndexDefinition\n  indexOptions?: IndexOptions\n  attributes: IndexAttribute[]\n  numDocs: number\n  maxDocId: number\n  numRecords: number\n  numTerms: number\n}\n\n/**\n * Hook options and result types.\n */\n\nexport interface UseIndexInfoOptions {\n  indexName: string\n}\n\nexport interface UseIndexInfoResult {\n  indexInfo: IndexInfo | null\n  loading: boolean\n  error: string | null\n  refetch: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexInfo/useIndexInfo.utils.spec.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  indexInfoFactory,\n  indexInfoAttributeFactory,\n} from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory'\n\nimport { normalizeFieldType, transformIndexInfo } from './useIndexInfo.utils'\n\ndescribe('useIndexInfo.utils', () => {\n  describe('normalizeFieldType', () => {\n    it.each([\n      ['TEXT', FieldTypes.TEXT],\n      ['TAG', FieldTypes.TAG],\n      ['NUMERIC', FieldTypes.NUMERIC],\n      ['GEO', FieldTypes.GEO],\n      ['VECTOR', FieldTypes.VECTOR],\n      ['text', FieldTypes.TEXT],\n    ])('should convert %s to %s', (input, expected) => {\n      expect(normalizeFieldType(input)).toBe(expected)\n    })\n  })\n\n  describe('transformIndexInfo', () => {\n    it('should transform API response to frontend model', () => {\n      const apiResponse = indexInfoFactory.build()\n\n      const result = transformIndexInfo(apiResponse)\n\n      // Verify snake_case → camelCase transformation\n      expect(result.indexDefinition?.keyType).toBe(\n        apiResponse.index_definition?.key_type,\n      )\n      expect(result.indexDefinition?.prefixes).toEqual(\n        apiResponse.index_definition?.prefixes,\n      )\n      expect(result.indexOptions?.filter).toBe(\n        apiResponse.index_options?.filter,\n      )\n      expect(result.indexOptions?.defaultLang).toBe(\n        apiResponse.index_options?.default_lang,\n      )\n\n      // Verify string → number transformation\n      expect(result.numDocs).toBe(Number(apiResponse.num_docs))\n      expect(result.maxDocId).toBe(Number(apiResponse.max_doc_id))\n      expect(result.numRecords).toBe(Number(apiResponse.num_records))\n      expect(result.numTerms).toBe(Number(apiResponse.num_terms))\n\n      // Verify attributes transformation\n      expect(result.attributes).toHaveLength(apiResponse.attributes.length)\n      expect(result.attributes[0].identifier).toBe(\n        apiResponse.attributes[0].identifier,\n      )\n      expect(result.attributes[0].attribute).toBe(\n        apiResponse.attributes[0].attribute,\n      )\n      expect(result.attributes[0].type).toBe(\n        apiResponse.attributes[0].type.toLowerCase(),\n      )\n    })\n\n    it('should handle missing index options', () => {\n      const apiResponse = indexInfoFactory.build({\n        index_options: undefined,\n      })\n\n      const result = transformIndexInfo(apiResponse)\n\n      expect(result.indexOptions).toBeUndefined()\n    })\n\n    it('should handle empty attributes array', () => {\n      const apiResponse = indexInfoFactory.build({\n        attributes: [],\n      })\n\n      const result = transformIndexInfo(apiResponse)\n\n      expect(result.attributes).toEqual([])\n    })\n\n    it('should handle attributes without weight', () => {\n      const apiResponse = indexInfoFactory.build({\n        attributes: [\n          indexInfoAttributeFactory.build(\n            { type: 'TAG' },\n            { transient: { includeWeight: false } },\n          ),\n        ],\n      })\n\n      const result = transformIndexInfo(apiResponse)\n\n      expect(result.attributes[0].weight).toBeUndefined()\n    })\n\n    it('should normalize all field types to lowercase', () => {\n      const apiResponse = indexInfoFactory.build({\n        attributes: [\n          indexInfoAttributeFactory.build({ type: 'TEXT' }),\n          indexInfoAttributeFactory.build({ type: 'TAG' }),\n          indexInfoAttributeFactory.build({ type: 'NUMERIC' }),\n        ],\n      })\n\n      const result = transformIndexInfo(apiResponse)\n\n      expect(result.attributes[0].type).toBe('text')\n      expect(result.attributes[1].type).toBe('tag')\n      expect(result.attributes[2].type).toBe('numeric')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexInfo/useIndexInfo.utils.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { IndexInfoDto } from 'apiSrc/modules/browser/redisearch/dto/index.info.dto'\n\nimport { IndexInfo } from './useIndexInfo.types'\n\n/**\n * Converts API field type (uppercase) to FieldTypes enum (lowercase).\n */\nexport const normalizeFieldType = (type: string): FieldTypes =>\n  type.toLowerCase() as FieldTypes\n\n/**\n * Transforms API response (DTO) to frontend model (IndexInfo).\n * Normalizes field types and converts snake_case to camelCase.\n */\nexport const transformIndexInfo = (data: IndexInfoDto): IndexInfo => ({\n  indexDefinition: {\n    keyType: data.index_definition.key_type,\n    prefixes: data.index_definition.prefixes,\n  },\n  indexOptions: data.index_options\n    ? {\n        filter: data.index_options.filter,\n        defaultLang: data.index_options.default_lang,\n      }\n    : undefined,\n  attributes: data.attributes.map((attr) => ({\n    identifier: attr.identifier,\n    attribute: attr.attribute,\n    type: normalizeFieldType(attr.type),\n    weight: attr.WEIGHT,\n  })),\n  numDocs: Number(data.num_docs) || 0,\n  maxDocId: Number(data.max_doc_id) || 0,\n  numRecords: Number(data.num_records) || 0,\n  numTerms: Number(data.num_terms) || 0,\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexListData/index.ts",
    "content": "export { useIndexListData } from './useIndexListData'\nexport type { UseIndexListDataResult } from './useIndexListData'\nexport { transformIndexListRow } from './useIndexListData.utils'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexListData/useIndexListData.spec.ts",
    "content": "import { act, waitFor } from '@testing-library/react'\nimport { http, HttpResponse } from 'msw'\nimport { AxiosResponse } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport {\n  getMswURL,\n  mockStore,\n  renderHook,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { indexInfoFactory } from 'uiSrc/mocks/factories/redisearch/IndexInfo.factory'\n\nimport { useIndexListData } from './useIndexListData'\nimport * as utils from './useIndexListData.utils'\n\nconst instanceId = 'test-instance-id'\n\nconst getMockedStore = () => {\n  const state = cloneDeep(initialStateDefault)\n  state.connections.instances.connectedInstance = {\n    ...state.connections.instances.connectedInstance,\n    id: instanceId,\n  }\n  return mockStore(state)\n}\n\ndescribe('useIndexListData', () => {\n  beforeEach(() => {\n    mswServer.resetHandlers()\n  })\n\n  it('should return initial state with empty data and no loading', () => {\n    const { result } = renderHook(() => useIndexListData([]), {\n      store: getMockedStore(),\n    })\n\n    expect(result.current.data).toEqual([])\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should not fetch when indexNames is empty', async () => {\n    const requestSpy = jest.fn()\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async () => {\n          requestSpy()\n          return HttpResponse.json({}, { status: 200 })\n        },\n      ),\n    )\n\n    renderHook(() => useIndexListData([]), {\n      store: getMockedStore(),\n    })\n\n    expect(requestSpy).not.toHaveBeenCalled()\n  })\n\n  it('should fetch and return rows for all provided index names', async () => {\n    const mockResponse = indexInfoFactory.build()\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async () => HttpResponse.json(mockResponse, { status: 200 }),\n      ),\n    )\n\n    const indexNames = ['idx-a', 'idx-b']\n\n    const { result } = renderHook(() => useIndexListData(indexNames), {\n      store: getMockedStore(),\n    })\n\n    expect(result.current.loading).toBe(true)\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.data).toHaveLength(2)\n    expect(result.current.data[0].name).toBe('idx-a')\n    expect(result.current.data[1].name).toBe('idx-b')\n    expect(result.current.data[0].numDocs).toBe(Number(mockResponse.num_docs))\n  })\n\n  it('should skip failed requests and return only successful rows', async () => {\n    const mockResponse = indexInfoFactory.build()\n    let callCount = 0\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async () => {\n          callCount++\n          if (callCount === 1) {\n            return HttpResponse.json(mockResponse, { status: 200 })\n          }\n          return HttpResponse.json({ message: 'Not found' }, { status: 404 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(\n      () => useIndexListData(['idx-ok', 'idx-fail']),\n      { store: getMockedStore() },\n    )\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.data).toHaveLength(1)\n    expect(result.current.data[0].name).toBe('idx-ok')\n  })\n\n  it('should reset data when indexNames becomes empty', async () => {\n    const mockResponse = indexInfoFactory.build()\n\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO)),\n        async () => HttpResponse.json(mockResponse, { status: 200 }),\n      ),\n    )\n\n    const { result, rerender } = renderHook(\n      (initialProps) =>\n        useIndexListData((initialProps as { names: string[] })?.names ?? []),\n      {\n        store: getMockedStore(),\n        initialProps: { names: ['idx-a'] },\n      },\n    )\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.data).toHaveLength(1)\n\n    rerender({ names: [] })\n\n    expect(result.current.data).toEqual([])\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should abort stale requests and use latest data when indexNames changes', async () => {\n    const secondResponse = indexInfoFactory.build({ num_docs: '200' })\n\n    let resolveSecond: (v: PromiseSettledResult<AxiosResponse>[]) => void\n    let firstSignal: AbortSignal | undefined\n\n    const fetchSpy = jest\n      .spyOn(utils, 'fetchAllIndexesInfo')\n      .mockImplementation((_id, _names, signal) => {\n        if (!firstSignal) {\n          firstSignal = signal\n          return new Promise(() => {})\n        }\n\n        return new Promise((resolve) => {\n          resolveSecond = resolve\n        })\n      })\n\n    const { result, rerender } = renderHook(\n      (initialProps) =>\n        useIndexListData((initialProps as { names: string[] })?.names ?? []),\n      {\n        store: getMockedStore(),\n        initialProps: { names: ['idx-first'] },\n      },\n    )\n\n    expect(result.current.loading).toBe(true)\n    expect(fetchSpy).toHaveBeenCalledTimes(1)\n\n    rerender({ names: ['idx-second'] })\n\n    expect(firstSignal?.aborted).toBe(true)\n    expect(fetchSpy).toHaveBeenCalledTimes(2)\n\n    await act(async () => {\n      resolveSecond!([\n        {\n          status: 'fulfilled',\n          value: { data: secondResponse, status: 200 } as AxiosResponse,\n        },\n      ])\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.data).toHaveLength(1)\n    expect(result.current.data[0].numDocs).toBe(Number(secondResponse.num_docs))\n\n    fetchSpy.mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexListData/useIndexListData.ts",
    "content": "import { useState, useEffect } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nimport { IndexListRow } from '../../components/index-list/IndexList.types'\nimport {\n  fetchAllIndexesInfo,\n  transformIndexListRows,\n} from './useIndexListData.utils'\n\nexport interface UseIndexListDataResult {\n  data: IndexListRow[]\n  loading: boolean\n}\n\n/**\n * Hook for fetching index information for multiple indexes.\n * Multi-index counterpart of useIndexInfo — fetches info for all\n * provided index names in parallel and returns IndexListRow[].\n */\nexport const useIndexListData = (\n  indexNames: string[],\n): UseIndexListDataResult => {\n  const [data, setData] = useState<IndexListRow[]>([])\n  const [loading, setLoading] = useState(false)\n\n  const connectedInstance = useSelector(connectedInstanceSelector)\n  const instanceId = connectedInstance?.id\n\n  const indexNamesKey = JSON.stringify(indexNames)\n\n  useEffect(() => {\n    const names: string[] = JSON.parse(indexNamesKey)\n\n    if (!instanceId || names.length === 0) {\n      setData([])\n      setLoading(false)\n      return undefined\n    }\n\n    const controller = new AbortController()\n\n    setLoading(true)\n\n    fetchAllIndexesInfo(instanceId, names, controller.signal).then(\n      (results) => {\n        if (controller.signal.aborted) return\n\n        setData(transformIndexListRows(names, results))\n        setLoading(false)\n      },\n    )\n\n    return () => {\n      controller.abort()\n    }\n  }, [instanceId, indexNamesKey])\n\n  return { loading, data }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexListData/useIndexListData.utils.spec.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  indexInfoFactory,\n  indexAttributeFactory,\n} from 'uiSrc/mocks/factories/vector-search/indexInfo.factory'\n\nimport { transformIndexListRow } from './useIndexListData.utils'\n\ndescribe('useIndexListData.utils', () => {\n  describe('transformIndexListRow', () => {\n    it('should transform IndexInfo to IndexListRow', () => {\n      const info = indexInfoFactory.build()\n\n      const result = transformIndexListRow('my-index', info)\n\n      expect(result).toEqual({\n        id: 'my-index',\n        name: 'my-index',\n        prefixes: info.indexDefinition.prefixes,\n        fieldTypes: [...new Set(info.attributes.map((a) => a.type))],\n        numDocs: info.numDocs,\n        numRecords: info.numRecords,\n        numTerms: info.numTerms,\n        numFields: info.attributes.length,\n      })\n    })\n\n    it('should deduplicate field types', () => {\n      const info = indexInfoFactory.build({\n        attributes: [\n          indexAttributeFactory.build({ type: FieldTypes.TEXT }),\n          indexAttributeFactory.build({ type: FieldTypes.TEXT }),\n          indexAttributeFactory.build({ type: FieldTypes.TAG }),\n        ],\n      })\n\n      const result = transformIndexListRow('idx', info)\n\n      expect(result.fieldTypes).toEqual([FieldTypes.TEXT, FieldTypes.TAG])\n    })\n\n    it('should handle empty attributes', () => {\n      const info = indexInfoFactory.build({ attributes: [] })\n\n      const result = transformIndexListRow('idx', info)\n\n      expect(result.fieldTypes).toEqual([])\n      expect(result.numFields).toBe(0)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexListData/useIndexListData.utils.ts",
    "content": "import { AxiosResponse } from 'axios'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { IndexInfoDto } from 'apiSrc/modules/browser/redisearch/dto/index.info.dto'\n\nimport { transformIndexInfo } from '../useIndexInfo/useIndexInfo.utils'\nimport { IndexInfo } from '../useIndexInfo/useIndexInfo.types'\nimport { IndexListRow } from '../../components/index-list/IndexList.types'\n\n/**\n * Fetches index info for all given index names in parallel.\n */\nexport const fetchAllIndexesInfo = (\n  instanceId: string,\n  indexNames: string[],\n  signal?: AbortSignal,\n): Promise<PromiseSettledResult<AxiosResponse<IndexInfoDto>>[]> =>\n  Promise.allSettled(\n    indexNames.map((name) =>\n      apiService.post<IndexInfoDto>(\n        getUrl(instanceId, ApiEndpoints.REDISEARCH_INFO),\n        { index: name },\n        { signal },\n      ),\n    ),\n  )\n\n/**\n * Maps settled API results to IndexListRow[], skipping failed requests.\n */\nexport const transformIndexListRows = (\n  indexNames: string[],\n  results: PromiseSettledResult<AxiosResponse<IndexInfoDto>>[],\n): IndexListRow[] =>\n  results.reduce<IndexListRow[]>((rows, result, i) => {\n    if (\n      result.status === 'fulfilled' &&\n      isStatusSuccessful(result.value.status)\n    ) {\n      rows.push(\n        transformIndexListRow(\n          indexNames[i],\n          transformIndexInfo(result.value.data),\n        ),\n      )\n    }\n    return rows\n  }, [])\n\n/**\n * Converts an IndexInfo into an IndexListRow for the list table.\n */\nexport const transformIndexListRow = (\n  name: string,\n  info: IndexInfo,\n): IndexListRow => ({\n  id: name,\n  name,\n  prefixes: info.indexDefinition.prefixes,\n  fieldTypes: [...new Set(info.attributes.map((attr) => attr.type))],\n  numDocs: info.numDocs,\n  numRecords: info.numRecords,\n  numTerms: info.numTerms,\n  numFields: info.attributes.length,\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexNameValidation/index.ts",
    "content": "export {\n  useIndexNameValidation,\n  INDEX_NAME_ERRORS,\n} from './useIndexNameValidation'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexNameValidation/useIndexNameValidation.spec.ts",
    "content": "import { renderHook } from '@testing-library/react-hooks'\n\nimport {\n  useIndexNameValidation,\n  INDEX_NAME_ERRORS,\n} from './useIndexNameValidation'\n\nconst mockExistingIndexes: string[] = []\n\njest.mock('../useRedisearchListData', () => ({\n  useRedisearchListData: () => ({\n    stringData: mockExistingIndexes,\n  }),\n}))\n\ndescribe('useIndexNameValidation', () => {\n  beforeEach(() => {\n    mockExistingIndexes.length = 0\n  })\n\n  it('should return null for empty string', () => {\n    const { result } = renderHook(() => useIndexNameValidation(''))\n    expect(result.current).toBeNull()\n  })\n\n  it('should return null for whitespace-only string', () => {\n    const { result } = renderHook(() => useIndexNameValidation('   '))\n    expect(result.current).toBeNull()\n  })\n\n  it('should return DUPLICATE error when index name already exists', () => {\n    mockExistingIndexes.push('idx:existing')\n    const { result } = renderHook(() => useIndexNameValidation('idx:existing'))\n    expect(result.current).toBe(INDEX_NAME_ERRORS.DUPLICATE)\n  })\n\n  it('should return null for valid unique name', () => {\n    mockExistingIndexes.push('idx:other')\n    const { result } = renderHook(() => useIndexNameValidation('idx:newindex'))\n    expect(result.current).toBeNull()\n  })\n\n  it('should trim name before duplicate check', () => {\n    mockExistingIndexes.push('idx:test')\n    const { result } = renderHook(() => useIndexNameValidation('  idx:test  '))\n    expect(result.current).toBe(INDEX_NAME_ERRORS.DUPLICATE)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIndexNameValidation/useIndexNameValidation.ts",
    "content": "import { useMemo } from 'react'\n\nimport { useRedisearchListData } from '../useRedisearchListData'\n\nexport const INDEX_NAME_ERRORS = {\n  DUPLICATE: 'An index with this name already exists.',\n} as const\n\nexport const useIndexNameValidation = (indexName: string): string | null => {\n  const { stringData: existingIndexes } = useRedisearchListData()\n\n  return useMemo(() => {\n    if (existingIndexes.includes(indexName.trim())) {\n      return INDEX_NAME_ERRORS.DUPLICATE\n    }\n    return null\n  }, [indexName, existingIndexes])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIsKeyIndexed/index.ts",
    "content": "export { useIsKeyIndexed } from './useIsKeyIndexed'\nexport { UseIsKeyIndexedStatus } from './useIsKeyIndexed.types'\nexport type { UseIsKeyIndexedResult } from './useIsKeyIndexed.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIsKeyIndexed/useIsKeyIndexed.spec.ts",
    "content": "import { useSelector, useDispatch } from 'react-redux'\nimport { renderHook, act } from 'uiSrc/utils/test-utils'\n\nimport { fetchKeyIndexesAction } from 'uiSrc/slices/browser/redisearch'\n\nimport { useIsKeyIndexed } from './useIsKeyIndexed'\nimport { UseIsKeyIndexedStatus } from './useIsKeyIndexed.types'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  keyIndexesSelector: jest.fn(),\n  fetchKeyIndexesAction: jest.fn((keyName: string, force?: boolean) => ({\n    type: 'FETCH_KEY_INDEXES',\n    payload: { keyName, force },\n  })),\n}))\n\ndescribe('useIsKeyIndexed', () => {\n  const mockDispatch = jest.fn()\n  const mockUseSelector = useSelector as jest.Mock\n  const mockUseDispatch = useDispatch as jest.Mock\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockUseDispatch.mockReturnValue(mockDispatch)\n  })\n\n  const setupKeyIndexes = (keyName: string, entry: unknown) => {\n    mockUseSelector.mockReturnValue(entry ? { [keyName]: entry } : {})\n  }\n\n  it('should return idle status when keyName is empty', () => {\n    mockUseSelector.mockReturnValue({})\n\n    const { result } = renderHook(() => useIsKeyIndexed(''))\n\n    expect(result.current.status).toBe(UseIsKeyIndexedStatus.Idle)\n    expect(result.current.isIndexed).toBe(false)\n    expect(result.current.indexes).toEqual([])\n  })\n\n  it('should not dispatch when keyName is empty', () => {\n    mockUseSelector.mockReturnValue({})\n\n    renderHook(() => useIsKeyIndexed(''))\n\n    expect(mockDispatch).not.toHaveBeenCalled()\n  })\n\n  it('should dispatch fetchKeyIndexesAction on mount with a key', () => {\n    mockUseSelector.mockReturnValue({})\n\n    renderHook(() => useIsKeyIndexed('movie:1'))\n\n    expect(mockDispatch).toHaveBeenCalledWith(fetchKeyIndexesAction('movie:1'))\n  })\n\n  it('should return loading status when entry is loading', () => {\n    setupKeyIndexes('movie:1', { loading: true, data: [], error: '' })\n\n    const { result } = renderHook(() => useIsKeyIndexed('movie:1'))\n\n    expect(result.current.status).toBe(UseIsKeyIndexedStatus.Loading)\n    expect(result.current.isIndexed).toBe(false)\n    expect(result.current.indexes).toEqual([])\n  })\n\n  it('should return ready status with matching indexes', () => {\n    const mockIndexes = [\n      { name: 'idx:movie', prefixes: ['movie:'], keyType: 'HASH' },\n    ]\n    setupKeyIndexes('movie:1', { loading: false, data: mockIndexes, error: '' })\n\n    const { result } = renderHook(() => useIsKeyIndexed('movie:1'))\n\n    expect(result.current.status).toBe(UseIsKeyIndexedStatus.Ready)\n    expect(result.current.isIndexed).toBe(true)\n    expect(result.current.indexes).toEqual(mockIndexes)\n  })\n\n  it('should return ready with isIndexed false when no indexes match', () => {\n    setupKeyIndexes('session:abc', { loading: false, data: [], error: '' })\n\n    const { result } = renderHook(() => useIsKeyIndexed('session:abc'))\n\n    expect(result.current.status).toBe(UseIsKeyIndexedStatus.Ready)\n    expect(result.current.isIndexed).toBe(false)\n    expect(result.current.indexes).toEqual([])\n  })\n\n  it('should return error status on failure', () => {\n    setupKeyIndexes('movie:1', {\n      loading: false,\n      data: [],\n      error: 'Failed to fetch',\n    })\n\n    const { result } = renderHook(() => useIsKeyIndexed('movie:1'))\n\n    expect(result.current.status).toBe(UseIsKeyIndexedStatus.Error)\n    expect(result.current.isIndexed).toBe(false)\n  })\n\n  it('should return idle when entry does not exist yet', () => {\n    mockUseSelector.mockReturnValue({})\n\n    const { result } = renderHook(() => useIsKeyIndexed('movie:1'))\n\n    expect(result.current.status).toBe(UseIsKeyIndexedStatus.Idle)\n  })\n\n  it('should dispatch with force=true on refresh', async () => {\n    setupKeyIndexes('movie:1', { loading: false, data: [], error: '' })\n\n    const { result } = renderHook(() => useIsKeyIndexed('movie:1'))\n\n    await act(async () => {\n      await result.current.refresh()\n    })\n\n    expect(mockDispatch).toHaveBeenCalledWith(\n      fetchKeyIndexesAction('movie:1', true),\n    )\n  })\n\n  it('should not dispatch refresh when keyName is empty', async () => {\n    mockUseSelector.mockReturnValue({})\n\n    const { result } = renderHook(() => useIsKeyIndexed(''))\n\n    mockDispatch.mockClear()\n\n    await act(async () => {\n      await result.current.refresh()\n    })\n\n    expect(mockDispatch).not.toHaveBeenCalled()\n  })\n\n  it('should return multiple matching indexes', () => {\n    const mockIndexes = [\n      { name: 'idx:movie', prefixes: ['movie:'], keyType: 'HASH' },\n      { name: 'idx:global', prefixes: [], keyType: 'HASH' },\n    ]\n    setupKeyIndexes('movie:1', { loading: false, data: mockIndexes, error: '' })\n\n    const { result } = renderHook(() => useIsKeyIndexed('movie:1'))\n\n    expect(result.current.isIndexed).toBe(true)\n    expect(result.current.indexes).toHaveLength(2)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIsKeyIndexed/useIsKeyIndexed.ts",
    "content": "import { useEffect, useCallback } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\n\nimport {\n  keyIndexesSelector,\n  fetchKeyIndexesAction,\n} from 'uiSrc/slices/browser/redisearch'\nimport { AppDispatch } from 'uiSrc/slices/store'\n\nimport {\n  UseIsKeyIndexedResult,\n  UseIsKeyIndexedStatus,\n} from './useIsKeyIndexed.types'\n\n/**\n * Hook to determine if a given key is covered by one or more search indexes.\n * Thin wrapper around Redux state -- dispatches a thunk on mount and reads from the store.\n *\n * @param keyName - The key name to check\n * @returns { isIndexed, indexes, status, refresh }\n */\nexport const useIsKeyIndexed = (keyName: string): UseIsKeyIndexedResult => {\n  const dispatch = useDispatch<AppDispatch>()\n  const keyIndexes = useSelector(keyIndexesSelector)\n  const entry = keyName ? keyIndexes[keyName] : undefined\n\n  useEffect(() => {\n    if (keyName) {\n      dispatch(fetchKeyIndexesAction(keyName))\n    }\n  }, [keyName, dispatch])\n\n  const refresh = useCallback(async () => {\n    if (keyName) {\n      await dispatch(fetchKeyIndexesAction(keyName, true))\n    }\n  }, [keyName, dispatch])\n\n  if (!keyName || !entry) {\n    return {\n      isIndexed: false,\n      indexes: [],\n      status: UseIsKeyIndexedStatus.Idle,\n      refresh,\n    }\n  }\n\n  let status: UseIsKeyIndexedStatus\n  if (entry.loading) {\n    status = UseIsKeyIndexedStatus.Loading\n  } else if (entry.error) {\n    status = UseIsKeyIndexedStatus.Error\n  } else {\n    status = UseIsKeyIndexedStatus.Ready\n  }\n\n  return {\n    isIndexed: entry.data.length > 0,\n    indexes: entry.data,\n    status,\n    refresh,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useIsKeyIndexed/useIsKeyIndexed.types.ts",
    "content": "import { IndexSummary } from 'uiSrc/slices/interfaces/redisearch'\n\nexport enum UseIsKeyIndexedStatus {\n  Idle = 'idle',\n  Loading = 'loading',\n  Ready = 'ready',\n  Error = 'error',\n}\n\nexport interface UseIsKeyIndexedResult {\n  isIndexed: boolean\n  indexes: IndexSummary[]\n  status: UseIsKeyIndexedStatus\n  refresh: () => Promise<void>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLastViewedPage/index.ts",
    "content": "export { useLastViewedPage } from './useLastViewedPage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLastViewedPage/useLastViewedPage.spec.ts",
    "content": "import reactRouterDom from 'react-router-dom'\nimport { renderHook } from 'uiSrc/utils/test-utils'\nimport { Pages } from 'uiSrc/constants'\n\nimport { useLastViewedPage } from './useLastViewedPage'\n\ndescribe('useLastViewedPage', () => {\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should redirect to the last viewed page and clear it', () => {\n    const savedPage = Pages.vectorSearchCreateIndex('instanceId')\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: savedPage })\n    const { unmount } = renderHook(() => useLastViewedPage())\n    unmount()\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.vectorSearch('instanceId') })\n    renderHook(() => useLastViewedPage())\n\n    expect(pushMock).toHaveBeenCalledWith(savedPage)\n  })\n\n  it('should not redirect when there is no saved page', () => {\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n    reactRouterDom.useLocation = jest.fn().mockReturnValue({\n      pathname: Pages.vectorSearch('instanceId'),\n    })\n\n    renderHook(() => useLastViewedPage())\n\n    expect(pushMock).not.toHaveBeenCalled()\n  })\n\n  it('should not redirect when saved page belongs to a different instance', () => {\n    const savedPage = Pages.vectorSearchCreateIndex('instanceA')\n    const pushMock = jest.fn()\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock })\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: savedPage })\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: 'instanceA' })\n    const { unmount } = renderHook(() => useLastViewedPage())\n    unmount()\n\n    reactRouterDom.useLocation = jest\n      .fn()\n      .mockReturnValue({ pathname: Pages.vectorSearch('instanceB') })\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: 'instanceB' })\n    renderHook(() => useLastViewedPage())\n\n    expect(pushMock).not.toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLastViewedPage/useLastViewedPage.ts",
    "content": "import { useEffect, useRef } from 'react'\nimport { useHistory, useLocation, useParams } from 'react-router-dom'\nimport { Pages } from 'uiSrc/constants'\n\n// Persists the last active sub-page (create-index or query) across mount/unmount cycles,\n// so navigating away from Vector Search and back restores the previous sub-page.\n// Scoped per instance to prevent cross-database navigation.\n// Cleared after each restore to allow explicit navigation to the index list.\ninterface SavedPage {\n  instanceId: string\n  pathname: string\n}\n\nlet lastViewedPage: SavedPage | null = null\n\nexport const useLastViewedPage = () => {\n  const history = useHistory()\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { pathname } = useLocation()\n  const pathnameRef = useRef<string>('')\n  const instanceIdRef = useRef<string>(instanceId)\n\n  useEffect(() => {\n    instanceIdRef.current = instanceId\n  }, [instanceId])\n\n  useEffect(\n    () => () => {\n      if (pathnameRef.current) {\n        lastViewedPage = {\n          instanceId: instanceIdRef.current,\n          pathname: pathnameRef.current,\n        }\n      }\n    },\n    [],\n  )\n\n  useEffect(() => {\n    if (\n      pathname === Pages.vectorSearch(instanceId) &&\n      lastViewedPage?.instanceId === instanceId\n    ) {\n      const savedPage = lastViewedPage.pathname\n      lastViewedPage = null\n      history.push(savedPage)\n      return\n    }\n\n    pathnameRef.current =\n      pathname === Pages.vectorSearch(instanceId) ? '' : pathname\n  }, [pathname])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useListContent/index.ts",
    "content": "export { useListContent } from './useListContent'\nexport type { UseListContentReturn } from './useListContent'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useListContent/useListContent.spec.ts",
    "content": "import { useDispatch, useSelector } from 'react-redux'\nimport reactRouterDom from 'react-router-dom'\nimport { faker } from '@faker-js/faker'\nimport { renderHook, act } from 'uiSrc/utils/test-utils'\n\nimport { Pages } from 'uiSrc/constants'\nimport {\n  deleteRedisearchIndexAction,\n  redisearchListSelector,\n} from 'uiSrc/slices/browser/redisearch'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { useListContent } from './useListContent'\nimport { useIndexListData } from '../useIndexListData'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n}))\n\njest.mock('../useIndexListData', () => ({\n  useIndexListData: jest.fn(() => ({\n    data: [],\n    loading: false,\n  })),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/redisearch'),\n  redisearchListSelector: jest.fn(),\n  deleteRedisearchIndexAction: jest.fn().mockReturnValue({ type: 'delete' }),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  connectedInstanceSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/services', () => ({\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/services/query-library/QueryLibraryService', () => ({\n  QueryLibraryService: jest.fn().mockImplementation(() => ({\n    deleteByIndex: jest.fn(),\n  })),\n}))\n\nconst mockDispatch = jest.fn()\nconst mockPush = jest.fn()\nconst mockInstanceId = faker.string.uuid()\nconst mockDatabaseId = faker.string.uuid()\n\ndescribe('useListContent', () => {\n  const mockUseSelector = useSelector as jest.Mock\n  const mockUseDispatch = useDispatch as jest.Mock\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockUseDispatch.mockReturnValue(mockDispatch)\n    reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: mockPush })\n    reactRouterDom.useParams = jest\n      .fn()\n      .mockReturnValue({ instanceId: mockInstanceId })\n\n    mockUseSelector.mockImplementation((selector: any) => {\n      if (selector === redisearchListSelector) {\n        return { data: [] }\n      }\n      if (selector === connectedInstanceSelector) {\n        return { id: mockDatabaseId }\n      }\n      return {}\n    })\n    ;(useIndexListData as jest.Mock).mockReturnValue({\n      data: [],\n      loading: false,\n    })\n  })\n\n  it('should return data and loading from useIndexListData', () => {\n    const mockData = [{ id: 'idx', name: 'idx' }]\n    ;(useIndexListData as jest.Mock).mockReturnValue({\n      data: mockData,\n      loading: true,\n    })\n\n    const { result } = renderHook(() => useListContent())\n\n    expect(result.current.data).toBe(mockData)\n    expect(result.current.loading).toBe(true)\n  })\n\n  it('should return three actions', () => {\n    const { result } = renderHook(() => useListContent())\n\n    expect(result.current.actions).toHaveLength(3)\n    expect(result.current.actions[0].name).toBe('View index')\n    expect(result.current.actions[1].name).toBe('Browse dataset')\n    expect(result.current.actions[2].name).toBe('Delete')\n    expect(result.current.actions[2].variant).toBe('destructive')\n  })\n\n  describe('onQueryClick', () => {\n    it('should navigate to vector search query page', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.onQueryClick(indexName)\n      })\n\n      expect(mockPush).toHaveBeenCalledWith(\n        Pages.vectorSearchQuery(mockInstanceId, indexName),\n      )\n    })\n\n    it('should send SEARCH_INDEX_QUERY_CLICKED telemetry', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.onQueryClick(indexName)\n      })\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_INDEX_QUERY_CLICKED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n  })\n\n  describe('View index action', () => {\n    it('should set viewingIndexName when callback is invoked', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      expect(result.current.viewingIndexName).toBeNull()\n\n      act(() => {\n        result.current.actions[0].callback(indexName)\n      })\n\n      expect(result.current.viewingIndexName).toBe(indexName)\n    })\n\n    it('should send telemetry when viewing index details', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.actions[0].callback(indexName)\n      })\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_INDEX_DETAILS_VIEWED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n\n    it('should clear viewingIndexName when onCloseViewPanel is called', () => {\n      const { result } = renderHook(() => useListContent())\n\n      act(() => {\n        result.current.actions[0].callback('my-index')\n      })\n      expect(result.current.viewingIndexName).toBe('my-index')\n\n      act(() => {\n        result.current.onCloseViewPanel()\n      })\n      expect(result.current.viewingIndexName).toBeNull()\n    })\n  })\n\n  describe('Browse dataset action', () => {\n    it('should dispatch changeSearchMode and navigate to browser with browseIndex', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.actions[1].callback(indexName)\n      })\n\n      expect(mockDispatch).toHaveBeenCalled()\n      expect(mockPush).toHaveBeenCalledWith({\n        pathname: Pages.browser(mockInstanceId),\n        search: `browseIndex=${indexName}`,\n      })\n    })\n\n    it('should send SEARCH_INDEX_BROWSE_DATASET_CLICKED telemetry', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.actions[1].callback(indexName)\n      })\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_INDEX_BROWSE_DATASET_CLICKED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n  })\n\n  describe('Delete action', () => {\n    it('should set pendingDeleteIndex when callback is invoked', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      expect(result.current.pendingDeleteIndex).toBeNull()\n\n      act(() => {\n        result.current.actions[2].callback(indexName)\n      })\n\n      expect(result.current.pendingDeleteIndex).toBe(indexName)\n    })\n\n    it('should dispatch delete action on confirm', () => {\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.actions[2].callback(indexName)\n      })\n\n      act(() => {\n        result.current.onConfirmDelete()\n      })\n\n      expect(deleteRedisearchIndexAction).toHaveBeenCalled()\n      expect(result.current.pendingDeleteIndex).toBeNull()\n    })\n\n    it('should send correct telemetry on successful delete', async () => {\n      ;(deleteRedisearchIndexAction as jest.Mock).mockImplementation(\n        (_payload: any, onSuccess: () => Promise<void>) => {\n          onSuccess()\n          return { type: 'delete' }\n        },\n      )\n\n      const { result } = renderHook(() => useListContent())\n      const indexName = faker.string.alpha(10)\n\n      act(() => {\n        result.current.actions[2].callback(indexName)\n      })\n\n      act(() => {\n        result.current.onConfirmDelete()\n      })\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_INDEX_DELETED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n\n    it('should clear pendingDeleteIndex on close', () => {\n      const { result } = renderHook(() => useListContent())\n\n      act(() => {\n        result.current.actions[2].callback('some-index')\n      })\n      expect(result.current.pendingDeleteIndex).toBe('some-index')\n\n      act(() => {\n        result.current.onCloseDelete()\n      })\n      expect(result.current.pendingDeleteIndex).toBeNull()\n    })\n\n    it('should not dispatch when confirming without pending index', () => {\n      const { result } = renderHook(() => useListContent())\n\n      act(() => {\n        result.current.onConfirmDelete()\n      })\n\n      expect(deleteRedisearchIndexAction).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useListContent/useListContent.ts",
    "content": "import { useCallback, useMemo, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useHistory, useParams } from 'react-router-dom'\n\nimport { BrowserStorageItem, Pages } from 'uiSrc/constants'\nimport { bufferToString, stringToBuffer } from 'uiSrc/utils'\nimport { encodeIndexNameForUrl } from 'uiSrc/pages/vector-search/utils'\nimport {\n  deleteRedisearchIndexAction,\n  redisearchListSelector,\n} from 'uiSrc/slices/browser/redisearch'\nimport { SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { changeSearchMode } from 'uiSrc/slices/browser/keys'\nimport {\n  ShowIcon,\n  DeleteIcon,\n  VectorSearchKeyIcon,\n} from 'uiSrc/components/base/icons'\nimport { addMessageNotification } from 'uiSrc/slices/app/notifications'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { QueryLibraryService } from 'uiSrc/services/query-library/QueryLibraryService'\nimport { queryLibraryNotifications } from 'uiSrc/pages/vector-search/constants'\nimport { localStorageService } from 'uiSrc/services'\n\nimport { IndexListAction } from '../../components/index-list/IndexList.types'\nimport { useIndexListData } from '../useIndexListData'\n\nexport const useListContent = () => {\n  const dispatch = useDispatch()\n  const history = useHistory()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const { data: rawIndexes } = useSelector(redisearchListSelector)\n  const { id: databaseId } = useSelector(connectedInstanceSelector)\n  const indexes = useMemo(\n    () => rawIndexes.map((index) => bufferToString(index)),\n    [rawIndexes],\n  )\n\n  const { data, loading } = useIndexListData(indexes)\n\n  const [pendingDeleteIndex, setPendingDeleteIndex] = useState<string | null>(\n    null,\n  )\n  const [viewingIndexName, setViewingIndexName] = useState<string | null>(null)\n\n  const handleQueryClick = useCallback(\n    (indexName: string) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_QUERY_CLICKED,\n        eventData: { databaseId: instanceId },\n      })\n      history.push(\n        Pages.vectorSearchQuery(instanceId, encodeIndexNameForUrl(indexName)),\n      )\n    },\n    [history, instanceId],\n  )\n\n  const handleViewIndex = useCallback(\n    (indexName: string) => {\n      setViewingIndexName(indexName)\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_DETAILS_VIEWED,\n        eventData: { databaseId: instanceId },\n      })\n    },\n    [instanceId],\n  )\n\n  const handleCloseViewPanel = useCallback(() => {\n    setViewingIndexName(null)\n  }, [])\n\n  const handleBrowseDataset = useCallback(\n    (indexName: string) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_BROWSE_DATASET_CLICKED,\n        eventData: { databaseId: instanceId },\n      })\n      localStorageService.set(\n        BrowserStorageItem.browserSearchMode,\n        SearchMode.Redisearch,\n      )\n      dispatch(changeSearchMode(SearchMode.Redisearch))\n      const search = new URLSearchParams()\n      search.set('browseIndex', indexName)\n      history.push({\n        pathname: Pages.browser(instanceId),\n        search: search.toString(),\n      })\n    },\n    [dispatch, history, instanceId],\n  )\n\n  const cleanupQueryLibrary = useCallback(\n    async (indexName: string) => {\n      try {\n        if (databaseId) {\n          const queryLibraryService = new QueryLibraryService()\n          await queryLibraryService.deleteByIndex(databaseId, indexName)\n        }\n      } catch {\n        dispatch(\n          addMessageNotification(queryLibraryNotifications.cleanupFailed()),\n        )\n      }\n    },\n    [databaseId, dispatch],\n  )\n\n  const handleDelete = useCallback((indexName: string) => {\n    setPendingDeleteIndex(indexName)\n  }, [])\n\n  const handleConfirmDelete = useCallback(() => {\n    if (!pendingDeleteIndex) return\n\n    const indexName = pendingDeleteIndex\n\n    dispatch(\n      deleteRedisearchIndexAction(\n        { index: stringToBuffer(indexName) },\n        async () => {\n          sendEventTelemetry({\n            event: TelemetryEvent.SEARCH_INDEX_DELETED,\n            eventData: { databaseId: instanceId },\n          })\n          await cleanupQueryLibrary(indexName)\n        },\n      ),\n    )\n    setPendingDeleteIndex(null)\n  }, [dispatch, cleanupQueryLibrary, instanceId, pendingDeleteIndex])\n\n  const handleCloseDelete = useCallback(() => {\n    setPendingDeleteIndex(null)\n  }, [])\n\n  const actions: IndexListAction[] = useMemo(\n    () => [\n      { name: 'View index', icon: ShowIcon, callback: handleViewIndex },\n      {\n        name: 'Browse dataset',\n        icon: VectorSearchKeyIcon,\n        callback: handleBrowseDataset,\n      },\n      {\n        name: 'Delete',\n        icon: DeleteIcon,\n        variant: 'destructive',\n        callback: handleDelete,\n      },\n    ],\n    [handleViewIndex, handleBrowseDataset, handleDelete],\n  )\n\n  return {\n    data,\n    loading,\n    actions,\n    onQueryClick: handleQueryClick,\n    viewingIndexName,\n    onCloseViewPanel: handleCloseViewPanel,\n    pendingDeleteIndex,\n    onConfirmDelete: handleConfirmDelete,\n    onCloseDelete: handleCloseDelete,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLoadKeyData/helpers.spec.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  VectorAlgorithm,\n  VectorDataType,\n  VectorFlatFieldOptions,\n} from '../../components/index-details/IndexDetails.types'\nimport {\n  isIndexableJsonValue,\n  filterJsonData,\n  parseHashFields,\n  parseJsonValue,\n} from './helpers'\n\ndescribe('isIndexableJsonValue', () => {\n  it.each([\n    { desc: 'null', value: null, expected: true },\n    { desc: 'undefined', value: undefined, expected: true },\n    { desc: 'string', value: 'hello', expected: true },\n    { desc: 'number', value: 42, expected: true },\n    { desc: 'boolean', value: true, expected: true },\n    { desc: 'geo coordinates array', value: [12.5, 45.3], expected: true },\n    { desc: 'vector array', value: [0.1, 0.2, 0.3], expected: true },\n    { desc: 'geo object', value: { lon: 12.5, lat: 45.3 }, expected: true },\n    { desc: 'string array', value: ['a', 'b'], expected: false },\n    { desc: 'nested object', value: { a: 1, b: 2 }, expected: false },\n    { desc: 'mixed array', value: [1, 'two'], expected: false },\n  ])('should return $expected for $desc', ({ value, expected }) => {\n    const result = isIndexableJsonValue(value)\n    expect(result).toBe(expected)\n  })\n})\n\ndescribe('filterJsonData', () => {\n  it('should separate indexable fields from complex nested ones', () => {\n    const data = {\n      name: 'test',\n      count: 5,\n      nested: { a: 1, b: 2 },\n      tags: ['tag1', 'tag2'],\n      location: [12.5, 45.3],\n    }\n\n    const { indexable, skippedFields } = filterJsonData(data)\n\n    expect(indexable).toEqual({\n      name: 'test',\n      count: 5,\n      location: [12.5, 45.3],\n    })\n    expect(skippedFields).toEqual(['nested', 'tags'])\n  })\n\n  it('should return all fields as indexable when none are complex', () => {\n    const data = { a: 'hello', b: 42, c: true }\n\n    const { indexable, skippedFields } = filterJsonData(data)\n\n    expect(Object.keys(indexable)).toHaveLength(3)\n    expect(skippedFields).toEqual([])\n  })\n\n  it('should return empty results for empty object', () => {\n    const { indexable, skippedFields } = filterJsonData({})\n\n    expect(indexable).toEqual({})\n    expect(skippedFields).toEqual([])\n  })\n})\n\ndescribe('parseHashFields', () => {\n  it('should convert API hash fields into IndexField[]', () => {\n    const apiFields = [\n      {\n        field: { data: [110, 97, 109, 101], type: 'Buffer' as const },\n        value: { data: [65, 108, 105, 99, 101], type: 'Buffer' as const },\n      },\n      {\n        field: { data: [97, 103, 101], type: 'Buffer' as const },\n        value: { data: [51, 48], type: 'Buffer' as const },\n      },\n    ]\n\n    const result = parseHashFields(apiFields)\n\n    expect(result.fields).toHaveLength(2)\n    expect(result.fields[0].name).toBe('name')\n    expect(result.fields[1].name).toBe('age')\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should return empty fields for empty input', () => {\n    const result = parseHashFields([])\n\n    expect(result.fields).toEqual([])\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should detect binary float32 vector and return VECTOR type with options', () => {\n    // Two float32 values: 1.0 and 2.0 in little-endian\n    const float1 = [0x00, 0x00, 0x80, 0x3f] // 1.0\n    const float2 = [0x00, 0x00, 0x00, 0x40] // 2.0\n    const apiFields = [\n      {\n        field: { data: [101, 109, 98], type: 'Buffer' as const }, // \"emb\"\n        value: { data: [...float1, ...float2], type: 'Buffer' as const },\n      },\n    ]\n\n    const result = parseHashFields(apiFields)\n\n    expect(result.fields).toHaveLength(1)\n    const vectorField = result.fields[0]\n    expect(vectorField.type).toBe(FieldTypes.VECTOR)\n    expect(vectorField.name).toBe('emb')\n    expect(vectorField.value).toContain('(2 dims)')\n    const options = vectorField.options as VectorFlatFieldOptions\n    expect(options.algorithm).toBe(VectorAlgorithm.FLAT)\n    expect(options.dimensions).toBe(2)\n    expect(options.dataType).toBe(VectorDataType.FLOAT32)\n  })\n\n  it('should mix binary vector fields with regular text fields', () => {\n    const float1 = [0x00, 0x00, 0x80, 0x3f]\n    const float2 = [0x00, 0x00, 0x00, 0x40]\n    const apiFields = [\n      {\n        field: { data: [110, 97, 109, 101], type: 'Buffer' as const }, // \"name\"\n        value: { data: [65, 108, 105, 99, 101], type: 'Buffer' as const }, // \"Alice\"\n      },\n      {\n        field: { data: [118, 101, 99], type: 'Buffer' as const }, // \"vec\"\n        value: { data: [...float1, ...float2], type: 'Buffer' as const },\n      },\n    ]\n\n    const result = parseHashFields(apiFields)\n\n    expect(result.fields).toHaveLength(2)\n\n    const nameField = result.fields.find((f) => f.name === 'name')\n    expect(nameField?.type).not.toBe(FieldTypes.VECTOR)\n\n    const vecField = result.fields.find((f) => f.name === 'vec')\n    expect(vecField?.type).toBe(FieldTypes.VECTOR)\n  })\n})\n\ndescribe('parseJsonValue', () => {\n  it('should parse a JSON string and infer fields', () => {\n    const raw = JSON.stringify([{ title: 'hello', count: 42 }])\n\n    const result = parseJsonValue(raw)\n\n    expect(result.fields).toHaveLength(2)\n    expect(result.fields.map((f) => f.name).sort()).toEqual(['count', 'title'])\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should handle a plain object (not wrapped in array)', () => {\n    const raw = { name: 'test', price: 99 }\n\n    const result = parseJsonValue(raw)\n\n    expect(result.fields).toHaveLength(2)\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should skip non-indexable nested fields', () => {\n    const raw = { simple: 'value', nested: { deep: true } }\n\n    const result = parseJsonValue(raw)\n\n    expect(result.fields).toHaveLength(1)\n    expect(result.fields[0].name).toBe('simple')\n    expect(result.skippedFields).toEqual(['nested'])\n  })\n\n  it('should return empty result for invalid JSON string', () => {\n    const result = parseJsonValue('not valid json')\n\n    expect(result.fields).toEqual([])\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should return empty result for null', () => {\n    const result = parseJsonValue(null)\n\n    expect(result.fields).toEqual([])\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should return empty result for a primitive', () => {\n    const result = parseJsonValue(42)\n\n    expect(result.fields).toEqual([])\n    expect(result.skippedFields).toEqual([])\n  })\n\n  it('should return empty result for an empty array', () => {\n    const result = parseJsonValue([])\n\n    expect(result.fields).toEqual([])\n    expect(result.skippedFields).toEqual([])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLoadKeyData/helpers.ts",
    "content": "import {\n  bufferToString,\n  isBinaryVector,\n  bufferToFloat32Array,\n} from 'uiSrc/utils'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  FieldTypes,\n  RedisearchIndexKeyType,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  inferKeyFields,\n  isGeoCoordinates,\n  isVector,\n  HashKeyData,\n  JsonKeyData,\n} from '../../utils/inferFieldType'\nimport {\n  IndexField,\n  VectorAlgorithm,\n  VectorDataType,\n  VectorFlatFieldOptions,\n} from '../../components/index-details/IndexDetails.types'\nimport { InferredFieldsResult } from './useLoadKeyData.types'\n\nconst EMPTY_RESULT: InferredFieldsResult = { fields: [], skippedFields: [] }\n\n// ---------------------------------------------------------------------------\n// JSON helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true when a JSON field value can be mapped to a RediSearch field type.\n * Objects/arrays that aren't geo coordinates or vectors are not indexable directly.\n */\nexport const isIndexableJsonValue = (value: unknown): boolean => {\n  if (value === null || value === undefined) {\n    return true\n  }\n\n  if (\n    typeof value === 'string' ||\n    typeof value === 'number' ||\n    typeof value === 'boolean'\n  ) {\n    return true\n  }\n\n  if (Array.isArray(value)) {\n    return isGeoCoordinates(value) || isVector(value)\n  }\n\n  if (typeof value === 'object') {\n    return isGeoCoordinates(value)\n  }\n\n  return true\n}\n\nexport interface FilterJsonDataResult {\n  indexable: JsonKeyData\n  skippedFields: string[]\n}\n\n/**\n * Partitions JSON key entries into indexable fields and skipped fields.\n * Complex nested values (objects/arrays that aren't geo or vectors) are skipped.\n */\nexport const filterJsonData = (data: JsonKeyData): FilterJsonDataResult => {\n  const indexable: JsonKeyData = {}\n  const skippedFields: string[] = []\n\n  Object.entries(data).forEach(([key, value]) => {\n    if (isIndexableJsonValue(value)) {\n      indexable[key] = value\n    } else {\n      skippedFields.push(key)\n    }\n  })\n\n  return { indexable, skippedFields }\n}\n\n// ---------------------------------------------------------------------------\n// Response → IndexField[] converters\n// ---------------------------------------------------------------------------\n\nconst MAX_VECTOR_PREVIEW_ELEMENTS = 4\n\nconst formatVectorPreview = (floats: Float32Array): string => {\n  const items = Array.from(floats.slice(0, MAX_VECTOR_PREVIEW_ELEMENTS), (v) =>\n    v.toPrecision(6),\n  )\n  const suffix = floats.length > MAX_VECTOR_PREVIEW_ELEMENTS ? ', ...' : ''\n  return `[${items.join(', ')}${suffix}] (${floats.length} dims)`\n}\n\n/**\n * Builds an IndexField for a binary float32 vector buffer.\n * Pre-populates the VECTOR field options with inferred dimensions and data type.\n */\nconst buildBinaryVectorField = (\n  fieldName: string,\n  valueBuf: RedisResponseBuffer,\n): IndexField => {\n  const floats = bufferToFloat32Array(new Uint8Array(valueBuf.data))\n  const options: VectorFlatFieldOptions = {\n    algorithm: VectorAlgorithm.FLAT,\n    dimensions: floats.length,\n    dataType: VectorDataType.FLOAT32,\n  }\n  return {\n    id: fieldName,\n    name: fieldName,\n    value: formatVectorPreview(floats),\n    type: FieldTypes.VECTOR,\n    options,\n  }\n}\n\n/**\n * Converts raw hash field API response into inferred IndexField[].\n * Detects binary float32 vectors before falling back to string-based inference.\n */\nexport const parseHashFields = (\n  apiFields: Array<{ field: RedisResponseBuffer; value: RedisResponseBuffer }>,\n): InferredFieldsResult => {\n  const binaryVectorFields: IndexField[] = []\n  const textEntries: Array<[string, string]> = []\n\n  apiFields.forEach(({ field, value }) => {\n    const fieldName = bufferToString(field)\n    if (isBinaryVector(value)) {\n      binaryVectorFields.push(buildBinaryVectorField(fieldName, value))\n    } else {\n      textEntries.push([fieldName, bufferToString(value)])\n    }\n  })\n\n  const hashData: HashKeyData = Object.fromEntries(textEntries)\n  const inferred = inferKeyFields(hashData, RedisearchIndexKeyType.HASH)\n\n  return {\n    fields: [...inferred, ...binaryVectorFields],\n    skippedFields: [],\n  }\n}\n\n/**\n * Extracts the root JSON object from a parsed value.\n * ReJSON GET wraps the result in an array (e.g. `[{...}]`), so we unwrap the first element.\n * Plain objects are returned as-is; anything else yields an empty object.\n */\nconst extractRootObject = (value: object): JsonKeyData => {\n  if (!Array.isArray(value)) {\n    return value as JsonKeyData\n  }\n\n  const first = value[0]\n  if (first && typeof first === 'object' && !Array.isArray(first)) {\n    return first as JsonKeyData\n  }\n\n  return {} as JsonKeyData\n}\n\n/**\n * Unwraps the raw ReJSON GET response, filters out non-indexable fields,\n * and returns inferred IndexField[] together with skipped field names.\n */\nexport const parseJsonValue = (rawData: unknown): InferredFieldsResult => {\n  let jsonValue = rawData\n  if (typeof jsonValue === 'string') {\n    try {\n      jsonValue = JSON.parse(jsonValue)\n    } catch {\n      return EMPTY_RESULT\n    }\n  }\n\n  if (!jsonValue || typeof jsonValue !== 'object') {\n    return EMPTY_RESULT\n  }\n\n  const rootObject = extractRootObject(jsonValue)\n\n  const { indexable, skippedFields } = filterJsonData(rootObject)\n\n  return {\n    fields: inferKeyFields(indexable, RedisearchIndexKeyType.JSON),\n    skippedFields,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLoadKeyData/index.ts",
    "content": "export { useLoadKeyData } from './useLoadKeyData'\nexport type { UseLoadKeyDataResult } from './useLoadKeyData.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLoadKeyData/useLoadKeyData.spec.ts",
    "content": "import { waitFor } from '@testing-library/react'\nimport { act } from 'react-dom/test-utils'\nimport { http, HttpResponse, delay } from 'msw'\nimport { cloneDeep } from 'lodash'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport {\n  getMswURL,\n  mockStore,\n  renderHook,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport {\n  FieldTypes,\n  RedisearchIndexKeyType,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport { useLoadKeyData } from './useLoadKeyData'\n\nconst instanceId = 'test-instance-id'\n\nconst getMockedStore = () => {\n  const state = cloneDeep(initialStateDefault)\n  state.connections.instances.connectedInstance = {\n    ...state.connections.instances.connectedInstance,\n    id: instanceId,\n  }\n  return mockStore(state)\n}\n\nconst hashKey = { data: [116, 101, 115, 116], type: 'Buffer' }\n\ndescribe('useLoadKeyData', () => {\n  beforeEach(() => {\n    mswServer.resetHandlers()\n  })\n\n  it('should load hash key fields', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.HASH_GET_FIELDS)),\n        async () =>\n          HttpResponse.json(\n            {\n              fields: [\n                {\n                  field: { data: [110, 97, 109, 101], type: 'Buffer' },\n                  value: { data: [74, 111, 104, 110], type: 'Buffer' },\n                },\n              ],\n            },\n            { status: 200 },\n          ),\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadKeyData(), {\n      store: getMockedStore(),\n    })\n\n    act(() => {\n      result.current.loadKeyData(hashKey as any, RedisearchIndexKeyType.HASH)\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.error).toBeNull()\n    expect(result.current.fields.length).toBeGreaterThan(0)\n  })\n\n  it('should load JSON key fields from string response', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REJSON_GET)),\n        async () =>\n          HttpResponse.json(\n            {\n              downloaded: true,\n              path: '$',\n              data: JSON.stringify({ title: 'Test', year: 2024 }),\n            },\n            { status: 200 },\n          ),\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadKeyData(), {\n      store: getMockedStore(),\n    })\n\n    act(() => {\n      result.current.loadKeyData(hashKey as any, RedisearchIndexKeyType.JSON)\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.error).toBeNull()\n    expect(result.current.fields).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({ name: 'title', type: FieldTypes.TAG }),\n        expect.objectContaining({ name: 'year', type: FieldTypes.NUMERIC }),\n      ]),\n    )\n  })\n\n  it('should load JSON key fields from array response', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REJSON_GET)),\n        async () =>\n          HttpResponse.json(\n            {\n              downloaded: true,\n              path: '$',\n              data: [{ title: 'Test', year: 2024 }],\n            },\n            { status: 200 },\n          ),\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadKeyData(), {\n      store: getMockedStore(),\n    })\n\n    act(() => {\n      result.current.loadKeyData(hashKey as any, RedisearchIndexKeyType.JSON)\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.fields).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({ name: 'title', type: FieldTypes.TAG }),\n        expect.objectContaining({ name: 'year', type: FieldTypes.NUMERIC }),\n      ]),\n    )\n  })\n\n  it('should skip nested objects for JSON keys and report skippedFields', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.REJSON_GET)),\n        async () =>\n          HttpResponse.json(\n            {\n              downloaded: true,\n              path: '$',\n              data: JSON.stringify({\n                name: 'test',\n                nested: { a: 1, b: 2 },\n                count: 5,\n              }),\n            },\n            { status: 200 },\n          ),\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadKeyData(), {\n      store: getMockedStore(),\n    })\n\n    act(() => {\n      result.current.loadKeyData(hashKey as any, RedisearchIndexKeyType.JSON)\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.fields).toHaveLength(2)\n    expect(result.current.skippedFields).toEqual(['nested'])\n  })\n\n  it('should set loading state during fetch', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.HASH_GET_FIELDS)),\n        async () => {\n          await delay(200)\n          return HttpResponse.json({ fields: [] }, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadKeyData(), {\n      store: getMockedStore(),\n    })\n\n    act(() => {\n      result.current.loadKeyData(hashKey as any, RedisearchIndexKeyType.HASH)\n    })\n\n    expect(result.current.loading).toBe(true)\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n  })\n\n  it('should handle API errors', async () => {\n    mswServer.use(\n      http.post(\n        getMswURL(getUrl(instanceId, ApiEndpoints.HASH_GET_FIELDS)),\n        async () =>\n          HttpResponse.json(\n            { message: 'Internal Server Error' },\n            { status: 500 },\n          ),\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadKeyData(), {\n      store: getMockedStore(),\n    })\n\n    act(() => {\n      result.current.loadKeyData(hashKey as any, RedisearchIndexKeyType.HASH)\n    })\n\n    await waitFor(() => {\n      expect(result.current.loading).toBe(false)\n    })\n\n    expect(result.current.error).toBeTruthy()\n    expect(result.current.fields).toEqual([])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLoadKeyData/useLoadKeyData.ts",
    "content": "import { useCallback, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { AxiosError } from 'axios'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport { IndexField } from '../../components/index-details/IndexDetails.types'\nimport { parseHashFields, parseJsonValue } from './helpers'\nimport {\n  InferredFieldsResult,\n  UseLoadKeyDataResult,\n} from './useLoadKeyData.types'\n\nexport const useLoadKeyData = (): UseLoadKeyDataResult => {\n  const [fields, setFields] = useState<IndexField[]>([])\n  const [skippedFields, setSkippedFields] = useState<string[]>([])\n  const [loading, setLoading] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n  const { encoding } = useSelector(appInfoSelector)\n\n  const loadHashData = useCallback(\n    async (key: RedisResponseBuffer): Promise<InferredFieldsResult> => {\n      const { data, status } = await apiService.post<{\n        fields: Array<{\n          field: RedisResponseBuffer\n          value: RedisResponseBuffer\n        }>\n      }>(\n        getUrl(instanceId ?? '', ApiEndpoints.HASH_GET_FIELDS),\n        { keyName: key, cursor: 0, count: SCAN_COUNT_DEFAULT, match: '*' },\n        { params: { encoding } },\n      )\n\n      if (!isStatusSuccessful(status)) {\n        throw new Error('Failed to load hash fields')\n      }\n\n      return parseHashFields(data.fields)\n    },\n    [instanceId, encoding],\n  )\n\n  const loadJsonData = useCallback(\n    async (key: RedisResponseBuffer): Promise<InferredFieldsResult> => {\n      const { data, status } = await apiService.post<{ data: unknown }>(\n        getUrl(instanceId ?? '', ApiEndpoints.REJSON_GET),\n        { keyName: key, path: '$', forceRetrieve: true, encoding },\n      )\n\n      if (!isStatusSuccessful(status)) {\n        throw new Error('Failed to load JSON data')\n      }\n\n      return parseJsonValue(data?.data)\n    },\n    [instanceId, encoding],\n  )\n\n  const loadKeyData = useCallback(\n    async (key: RedisResponseBuffer, keyType: RedisearchIndexKeyType) => {\n      setLoading(true)\n      setError(null)\n      setFields([])\n      setSkippedFields([])\n\n      try {\n        const result =\n          keyType === RedisearchIndexKeyType.HASH\n            ? await loadHashData(key)\n            : await loadJsonData(key)\n\n        setFields(result.fields)\n        setSkippedFields(result.skippedFields)\n      } catch (e) {\n        setError(getApiErrorMessage(e as AxiosError))\n        setFields([])\n        setSkippedFields([])\n      } finally {\n        setLoading(false)\n      }\n    },\n    [loadHashData, loadJsonData],\n  )\n\n  return { loadKeyData, fields, skippedFields, loading, error }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useLoadKeyData/useLoadKeyData.types.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport { IndexField } from '../../components/index-details/IndexDetails.types'\n\nexport interface InferredFieldsResult {\n  fields: IndexField[]\n  skippedFields: string[]\n}\n\nexport interface UseLoadKeyDataResult {\n  loadKeyData: (\n    key: RedisResponseBuffer,\n    keyType: RedisearchIndexKeyType,\n  ) => void\n  fields: IndexField[]\n  skippedFields: string[]\n  loading: boolean\n  error: string | null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisInstanceCompatibility/index.ts",
    "content": "export { useRedisInstanceCompatibility } from './useRedisInstanceCompatibility'\nexport type { UseRedisInstanceCompatibilityReturn } from './useRedisInstanceCompatibility.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisInstanceCompatibility/useRedisInstanceCompatibility.spec.ts",
    "content": "import { renderHook } from 'uiSrc/utils/test-utils'\nimport {\n  connectedInstanceInfoSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { useRedisInstanceCompatibility } from './useRedisInstanceCompatibility'\nimport { UseRedisInstanceCompatibilityReturn } from './useRedisInstanceCompatibility.types'\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn(),\n  connectedInstanceInfoSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/interfaces', () => ({\n  REDISEARCH_MODULES: ['search', 'searchlight', 'ft'],\n}))\n\nconst renderUseRedisInstanceCompatibility = () => {\n  const { result } = renderHook(() => useRedisInstanceCompatibility())\n  return result.current as UseRedisInstanceCompatibilityReturn\n}\n\ndescribe('useRedisInstanceCompatibility', () => {\n  const mockConnectedInstanceSelector = connectedInstanceSelector as jest.Mock\n  const mockConnectedInstanceInfoSelector =\n    connectedInstanceInfoSelector as jest.Mock\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: '7.2.0',\n    })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('returns undefineds when loading is undefined (not initialized yet)', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: undefined,\n      modules: [{ name: 'search', semanticVersion: '2.6.0' }],\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.loading).toBeUndefined()\n    expect(hookResult.hasRedisearch).toBeUndefined()\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n    expect(hookResult.hasSupportedVersion).toBeUndefined()\n  })\n\n  it('still returns hasRedisearch=undefined when modules is null even after init', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: null,\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.loading).toBe(false)\n    expect(hookResult.hasRedisearch).toBeUndefined()\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n    expect(hookResult.hasSupportedVersion).toBe(true)\n  })\n\n  it('returns loading=true when connectedInstanceSelector returns loading=true', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: true,\n      modules: null,\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({})\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n\n    expect(hookResult.loading).toBe(true)\n    expect(hookResult.hasRedisearch).toBeUndefined()\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n    expect(hookResult.hasSupportedVersion).toBeUndefined()\n  })\n\n  it('detects RediSearch module + supported version', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [\n        { name: 'search', semanticVersion: '2.6.0' },\n        { name: 'other' },\n      ],\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n\n    expect(hookResult.loading).toBe(false)\n    expect(hookResult.hasRedisearch).toBe(true)\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(true)\n    expect(hookResult.hasSupportedVersion).toBe(true)\n  })\n\n  it('returns hasRedisearch=false when modules is an empty array (defaulted)', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.loading).toBe(false)\n    expect(hookResult.hasRedisearch).toBe(false)\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n    expect(hookResult.hasSupportedVersion).toBe(true)\n  })\n\n  it('returns hasRedisearch=undefined when modules are missing', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: null,\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasRedisearch).toBeUndefined()\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n    expect(hookResult.hasSupportedVersion).toBe(true)\n  })\n\n  it('handles unsupported Redis version (below 7.2) with RediSearch 2.x', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', semanticVersion: '2.4.0' }],\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: '7.1.9',\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasRedisearch).toBe(true)\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(true)\n    expect(hookResult.hasSupportedVersion).toBe(false)\n  })\n\n  it('returns hasMinimumRedisearchVersion=false for RediSearch < 2.0', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', semanticVersion: '1.6.14' }],\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: '5.0.14',\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasRedisearch).toBe(true)\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(false)\n    expect(hookResult.hasSupportedVersion).toBe(false)\n  })\n\n  it('returns hasMinimumRedisearchVersion=true for RediSearch 2.0.0', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', semanticVersion: '2.0.0' }],\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: '6.0.0',\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(true)\n    expect(hookResult.hasSupportedVersion).toBe(false)\n  })\n\n  it('returns hasMinimumRedisearchVersion=true for RediSearch 2.6.3', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', semanticVersion: '2.6.3' }],\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(true)\n    expect(hookResult.hasSupportedVersion).toBe(true)\n  })\n\n  it('returns hasMinimumRedisearchVersion=undefined when no RediSearch module present', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'ReJSON', semanticVersion: '2.4.0' }],\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasRedisearch).toBe(false)\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n  })\n\n  it('falls back to integer version when semanticVersion is missing (2.x)', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', version: 20600 }],\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasRedisearch).toBe(true)\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(true)\n  })\n\n  it('falls back to integer version when semanticVersion is missing (1.x)', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', version: 10614 }],\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: '5.0.14',\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasRedisearch).toBe(true)\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(false)\n    expect(hookResult.hasSupportedVersion).toBe(false)\n  })\n\n  it('handles unparsable Redis version -> false for hasSupportedVersion', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'something else' }],\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: 'not a version',\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasMinimumRedisearchVersion).toBeUndefined()\n    expect(hookResult.hasSupportedVersion).toBe(false)\n  })\n\n  it('handles absent Redis version -> undefined for hasSupportedVersion', () => {\n    mockConnectedInstanceSelector.mockReturnValue({\n      loading: false,\n      modules: [{ name: 'search', semanticVersion: '2.6.0' }],\n    })\n\n    mockConnectedInstanceInfoSelector.mockReturnValue({\n      version: undefined,\n    })\n\n    const hookResult = renderUseRedisInstanceCompatibility()\n    expect(hookResult.hasMinimumRedisearchVersion).toBe(true)\n    expect(hookResult.hasSupportedVersion).toBeUndefined()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisInstanceCompatibility/useRedisInstanceCompatibility.ts",
    "content": "import { useMemo } from 'react'\nimport { useSelector } from 'react-redux'\nimport {\n  connectedInstanceInfoSelector,\n  connectedInstanceSelector,\n} from 'uiSrc/slices/instances/instances'\nimport { REDISEARCH_MODULES } from 'uiSrc/slices/interfaces'\nimport { isRedisVersionSupported } from 'uiSrc/utils/comparisons/compareVersions'\n\nimport { UseRedisInstanceCompatibilityReturn } from './useRedisInstanceCompatibility.types'\n\nconst MIN_REDISEARCH_VERSION = '2.0.0'\nconst MIN_SUPPORTED_REDIS_VERSION = '7.2.0'\nconst REDISEARCH_MODULE_SET = new Set(REDISEARCH_MODULES)\n\n/**\n * Redis modules encode versions as integers in MMmmpp format:\n * major * 10000 + minor * 100 + patch (e.g. 10614 → 1.6.14, 20600 → 2.6.0).\n */\nconst decodeModuleVersion = (version: number): string => {\n  const major = Math.floor(version / 10000)\n  const minor = Math.floor((version % 10000) / 100)\n  const patch = version % 100\n  return `${major}.${minor}.${patch}`\n}\n\nexport const useRedisInstanceCompatibility =\n  (): UseRedisInstanceCompatibilityReturn => {\n    const { version } = useSelector(connectedInstanceInfoSelector)\n\n    const { loading, modules = [] } = useSelector(connectedInstanceSelector)\n\n    const isInitialized = loading !== undefined\n\n    const redisearchModule = useMemo(\n      () => modules?.find((m) => REDISEARCH_MODULE_SET.has(m.name)),\n      [modules],\n    )\n\n    const redisearchPresent = isInitialized\n      ? modules\n        ? !!redisearchModule\n        : undefined\n      : undefined\n\n    const redisearchVersion =\n      redisearchModule?.semanticVersion ??\n      (redisearchModule?.version != null\n        ? decodeModuleVersion(redisearchModule.version)\n        : undefined)\n    const hasMinRedisearch =\n      isInitialized && redisearchVersion\n        ? isRedisVersionSupported(redisearchVersion, MIN_REDISEARCH_VERSION)\n        : undefined\n\n    const hasVersion = isInitialized && version\n\n    return {\n      loading,\n      hasRedisearch: redisearchPresent,\n      hasMinimumRedisearchVersion: hasMinRedisearch,\n      hasSupportedVersion: hasVersion\n        ? isRedisVersionSupported(version, MIN_SUPPORTED_REDIS_VERSION)\n        : undefined,\n    }\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisInstanceCompatibility/useRedisInstanceCompatibility.types.ts",
    "content": "export type UseRedisInstanceCompatibilityReturn = {\n  loading: boolean | undefined\n  hasRedisearch: boolean | undefined\n  hasMinimumRedisearchVersion: boolean | undefined\n  hasSupportedVersion: boolean | undefined\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisearchListData/index.ts",
    "content": "export { useRedisearchListData } from './useRedisearchListData'\nexport type { UseRedisearchListDataReturn } from './useRedisearchListData.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisearchListData/useRedisearchListData.spec.ts",
    "content": "import { renderHook, waitFor } from 'uiSrc/utils/test-utils'\nimport { useSelector, useDispatch } from 'react-redux'\nimport {\n  redisearchListSelector,\n  fetchRedisearchListAction,\n} from 'uiSrc/slices/browser/redisearch'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { isRedisearchAvailable, bufferToString } from 'uiSrc/utils'\nimport { useRedisearchListData } from './useRedisearchListData'\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useDispatch: jest.fn(),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  redisearchListSelector: jest.fn(),\n  fetchRedisearchListAction: jest.fn(() => ({ type: 'FETCH_REDISEARCH_LIST' })),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  connectedInstanceSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  isRedisearchAvailable: jest.fn(),\n  bufferToString: jest.fn((str) => `string-${str.data}`),\n}))\n\ndescribe('useRedisearchListData', () => {\n  const mockDispatch = jest.fn()\n  const mockUseSelector = useSelector as jest.Mock\n  const mockUseDispatch = useDispatch as jest.Mock\n  const mockIsRedisearchAvailable = isRedisearchAvailable as jest.Mock\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockUseDispatch.mockReturnValue(mockDispatch)\n  })\n\n  const setupMocks = (\n    redisearchState: { loading: boolean | undefined; data: any[] },\n    instanceState: { modules?: any[]; host?: string },\n  ) => {\n    mockUseSelector.mockImplementation((selector: any) => {\n      if (selector === redisearchListSelector) {\n        return redisearchState\n      }\n      if (selector === connectedInstanceSelector) {\n        return instanceState\n      }\n      return {}\n    })\n  }\n\n  it('should return undefined loading when Redux initial state has not been updated', () => {\n    setupMocks(\n      { loading: undefined, data: [] },\n      { modules: [], host: 'localhost' },\n    )\n    mockIsRedisearchAvailable.mockReturnValue(false)\n\n    const { result } = renderHook(() => useRedisearchListData())\n\n    expect(result.current.loading).toBeUndefined()\n    expect(result.current.data).toEqual([])\n    expect(result.current.stringData).toEqual([])\n  })\n\n  it('should dispatch fetchRedisearchListAction when redisearch module is available', async () => {\n    setupMocks(\n      { loading: false, data: [] },\n      { modules: [{ name: 'search' }], host: 'localhost' },\n    )\n    mockIsRedisearchAvailable.mockReturnValue(true)\n\n    renderHook(() => useRedisearchListData())\n\n    await waitFor(() => {\n      expect(mockDispatch).toHaveBeenCalledWith(fetchRedisearchListAction())\n    })\n  })\n\n  it('should not dispatch action when host is not available', () => {\n    setupMocks(\n      { loading: false, data: [] },\n      { modules: [{ name: 'search' }], host: undefined },\n    )\n    mockIsRedisearchAvailable.mockReturnValue(true)\n\n    renderHook(() => useRedisearchListData())\n\n    expect(mockDispatch).not.toHaveBeenCalled()\n  })\n\n  it('should not dispatch action when redisearch module is not available', () => {\n    setupMocks({ loading: false, data: [] }, { modules: [], host: 'localhost' })\n    mockIsRedisearchAvailable.mockReturnValue(false)\n\n    renderHook(() => useRedisearchListData())\n\n    expect(mockDispatch).not.toHaveBeenCalled()\n  })\n\n  it('should convert data to string data', () => {\n    const mockData = [{ data: 'index1' }, { data: 'index2' }]\n    setupMocks(\n      { loading: false, data: mockData },\n      { modules: [], host: 'localhost' },\n    )\n    mockIsRedisearchAvailable.mockReturnValue(false)\n\n    const { result } = renderHook(() => useRedisearchListData())\n\n    expect(result.current.stringData).toEqual([\n      'string-index1',\n      'string-index2',\n    ])\n    expect(bufferToString).toHaveBeenCalledTimes(2)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisearchListData/useRedisearchListData.ts",
    "content": "import { useEffect, useMemo } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\n\nimport {\n  redisearchListSelector,\n  fetchRedisearchListAction,\n} from 'uiSrc/slices/browser/redisearch'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { bufferToString, isRedisearchAvailable } from 'uiSrc/utils'\n\nexport const useRedisearchListData = () => {\n  const dispatch = useDispatch()\n  const { loading, data } = useSelector(redisearchListSelector)\n  const { modules, host: instanceHost } = useSelector(connectedInstanceSelector)\n\n  const stringData = useMemo(\n    () => data.map((index) => bufferToString(index)),\n    [data],\n  )\n\n  useEffect(() => {\n    if (!instanceHost) {\n      return\n    }\n\n    const moduleExists = isRedisearchAvailable(modules)\n    if (moduleExists) {\n      dispatch(fetchRedisearchListAction())\n    }\n  }, [dispatch, instanceHost, modules])\n\n  return {\n    loading,\n    data,\n    stringData,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/hooks/useRedisearchListData/useRedisearchListData.types.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\n\nexport interface UseRedisearchListDataReturn {\n  loading: boolean | undefined\n  data: RedisResponseBuffer[]\n  stringData: string[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/index.ts",
    "content": "export { VectorSearchPage } from './pages/VectorSearchPage'\nexport { VectorSearchListPage } from './pages/VectorSearchListPage'\nexport { VectorSearchQueryPage } from './pages/VectorSearchQueryPage'\nexport { VectorSearchCreateIndexPage } from './pages/VectorSearchCreateIndexPage'\nexport { default as VectorSearchPageRouter } from './VectorSearchPageRouter'\nexport type { VectorSearchPageRouterProps } from './VectorSearchPageRouter.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.spec.tsx",
    "content": "import React from 'react'\nimport reactRouterDom from 'react-router-dom'\nimport { cleanup, render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { VectorSearchCreateIndexPage } from './VectorSearchCreateIndexPage'\n\njest.mock('../../components/index-details', () => {\n  const MockReact = require('react')\n  return {\n    IndexDetails: () =>\n      MockReact.createElement(\n        'div',\n        { 'data-testid': 'index-details-container' },\n        'IndexDetails',\n      ),\n  }\n})\n\njest.mock('../../components/command-view', () => {\n  const MockReact = require('react')\n  return {\n    CommandView: (props: any) =>\n      MockReact.createElement(\n        'div',\n        { 'data-testid': props.dataTestId },\n        'CommandView',\n      ),\n  }\n})\n\nconst mockPush = jest.fn()\n\nconst setupRouterMocks = (sampleData?: string) => {\n  reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: mockPush })\n  reactRouterDom.useParams = jest\n    .fn()\n    .mockReturnValue({ instanceId: 'test-instance' })\n  reactRouterDom.useLocation = jest.fn().mockReturnValue({\n    pathname: '/test-instance/vector-search/create-index',\n    search: sampleData ? `?sampleData=${sampleData}` : '',\n    hash: '',\n  })\n}\n\ndescribe('VectorSearchCreateIndexPage', () => {\n  beforeEach(() => {\n    cleanup()\n    jest.clearAllMocks()\n  })\n\n  it('should render all page elements', () => {\n    setupRouterMocks('e-commerce-discovery')\n\n    render(<VectorSearchCreateIndexPage />)\n\n    const page = screen.getByTestId('vector-search--create-index--page')\n    const card = screen.getByTestId('vector-search--create-index--card')\n    const title = screen.getByTestId('vector-search--create-index--title')\n    const viewToggle = screen.getByTestId(\n      'vector-search--create-index--view-toggle',\n    )\n    const tableViewBtn = screen.getByTestId(\n      'vector-search--create-index--table-view-btn',\n    )\n    const commandViewBtn = screen.getByTestId(\n      'vector-search--create-index--command-view-btn',\n    )\n    const addFieldBtn = screen.getByTestId(\n      'vector-search--create-index--add-field-btn',\n    )\n    const prefixValue = screen.getByTestId(\n      'vector-search--create-index--prefix-value',\n    )\n    const indexDetails = screen.getByTestId('index-details-container')\n    const submitBtn = screen.getByTestId(\n      'vector-search--create-index--submit-btn',\n    )\n    const cancelBtn = screen.getByTestId(\n      'vector-search--create-index--cancel-btn',\n    )\n\n    expect(page).toBeInTheDocument()\n    expect(card).toBeInTheDocument()\n    expect(title).toHaveTextContent(\n      'View sample data index: E-commerce discovery',\n    )\n    expect(viewToggle).toBeInTheDocument()\n    expect(tableViewBtn).toBeInTheDocument()\n    expect(commandViewBtn).toBeInTheDocument()\n    expect(addFieldBtn).toBeInTheDocument()\n    expect(addFieldBtn).toBeDisabled()\n    expect(prefixValue).toHaveTextContent('bikes:')\n    expect(indexDetails).toBeInTheDocument()\n    expect(submitBtn).toBeInTheDocument()\n    expect(cancelBtn).toBeInTheDocument()\n  })\n\n  it('should switch to command view when clicking Command view button', () => {\n    setupRouterMocks('e-commerce-discovery')\n\n    render(<VectorSearchCreateIndexPage />)\n\n    const commandViewBtn = screen.getByTestId(\n      'vector-search--create-index--command-view-btn',\n    )\n\n    fireEvent.click(commandViewBtn)\n\n    const commandView = screen.getByTestId(\n      'vector-search--create-index--command-view',\n    )\n    const indexDetails = screen.queryByTestId('index-details-container')\n\n    expect(commandView).toBeInTheDocument()\n    expect(indexDetails).not.toBeInTheDocument()\n  })\n\n  it('should navigate back on cancel', () => {\n    setupRouterMocks('e-commerce-discovery')\n\n    render(<VectorSearchCreateIndexPage />)\n\n    const cancelBtn = screen.getByTestId(\n      'vector-search--create-index--cancel-btn',\n    )\n\n    fireEvent.click(cancelBtn)\n\n    expect(mockPush).toHaveBeenCalledWith(\n      expect.stringContaining('vector-search'),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport TextInput from 'uiSrc/components/base/inputs/TextInput'\n\nexport const PageWrapper = styled(Row)`\n  background-color: ${({ theme }) =>\n    theme.semantic?.color.background.neutral100};\n  padding: ${({ theme }) => theme.core?.space.space100}\n    ${({ theme }) => theme.core?.space.space200};\n  height: 100%;\n  width: 100%;\n  gap: ${({ theme }) => theme.core?.space.space200};\n`\n\nexport const BrowserPanel = styled(Col).attrs({ grow: false })`\n  width: 280px;\n  flex-shrink: 0;\n  overflow: hidden;\n  border: 1px solid ${({ theme }) => theme.semantic?.color?.border?.neutral500};\n  border-radius: ${({ theme }) => theme.components?.card?.borderRadius};\n`\n\nexport const RightPanel = styled(Col)`\n  flex: 1;\n  min-height: 0;\n  min-width: 0;\n`\n\nexport const TitleRow = styled(Row).attrs({ grow: false, align: 'center' })`\n  height: 52px;\n  flex-shrink: 0;\n  gap: ${({ theme }) => theme.core?.space.space100};\n  line-height: 1;\n`\n\nexport const CardContainer = styled(Col)`\n  flex: 1;\n  min-height: 0;\n  min-width: 0;\n  background: ${({ theme }) => theme.semantic?.color.background.neutral100};\n  border: 1px solid ${({ theme }) => theme.semantic?.color?.border?.neutral500};\n  border-radius: ${({ theme }) => theme.components?.card?.borderRadius};\n  overflow: hidden;\n`\n\nexport const ToolbarRow = styled(Row).attrs({ grow: false })`\n  padding: ${({ theme }) => theme.core?.space.space100}\n    ${({ theme }) => theme.core?.space.space100};\n  border-bottom: 1px solid\n    ${({ theme }) => theme.semantic?.color?.border?.neutral500};\n`\n\nexport const ToolbarRight = styled(Row).attrs({ grow: false })`\n  margin-left: auto;\n  gap: ${({ theme }) => theme.core?.space.space200};\n`\n\nexport const VerticalSeparator = styled.div`\n  width: 1px;\n  height: 24px;\n  background: ${({ theme }) => theme.semantic?.color?.border?.neutral500};\n  flex-shrink: 0;\n`\n\nexport const IndexPrefixRow = styled(Row).attrs({ grow: false })`\n  gap: ${({ theme }) => theme.core?.space.space200};\n`\n\nexport const ContentArea = styled(Col)`\n  flex: 1;\n  overflow: auto;\n  min-height: 0;\n`\n\nexport const EmptyState = styled(Col)`\n  flex: 1;\n  gap: ${({ theme }) => theme.core?.space.space300};\n  white-space: pre-line;\n  text-align: center;\n`\n\nexport const FooterRow = styled(Row).attrs({ grow: false })`\n  border-top: 1px solid\n    ${({ theme }) => theme.semantic?.color?.border?.neutral500};\n  padding: ${({ theme }) => theme.core?.space.space100}\n    ${({ theme }) => theme.core?.space.space100};\n`\n\nexport const IndexPrefixInput = styled(TextInput)`\n  width: 120px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.tsx",
    "content": "import React from 'react'\nimport { useLocation, useParams, Redirect } from 'react-router-dom'\n\nimport { Pages } from 'uiSrc/constants'\n\nimport { CreateIndexMode } from './VectorSearchCreateIndexPage.types'\nimport {\n  isExistingDataState,\n  isSampleDataState,\n  hasPreselectedKey,\n  parseCreateIndexSearchParams,\n} from '../../utils'\nimport { CreateIndexPageProvider } from '../../context/create-index-page'\nimport { CreateIndexOnboardingProvider } from '../../context/create-index-onboarding'\nimport { CreateIndexHeader } from './components/CreateIndexHeader'\nimport { CreateIndexContent } from './components/CreateIndexContent'\nimport { CreateIndexBrowser } from './components/CreateIndexBrowser'\nimport * as S from './VectorSearchCreateIndexPage.styles'\n\nexport const VectorSearchCreateIndexPage = () => {\n  const { search } = useLocation()\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const state = parseCreateIndexSearchParams(search)\n  const mode = isExistingDataState(state)\n    ? CreateIndexMode.ExistingData\n    : CreateIndexMode.SampleData\n\n  const sampleData = isSampleDataState(state) ? state.sampleData : undefined\n\n  if (mode === CreateIndexMode.SampleData && !sampleData) {\n    return <Redirect to={Pages.vectorSearch(instanceId)} />\n  }\n\n  const existingState = isExistingDataState(state) ? state : undefined\n  const preselected = hasPreselectedKey(state)\n  const showBrowser = mode === CreateIndexMode.ExistingData && !preselected\n\n  return (\n    <CreateIndexPageProvider\n      instanceId={instanceId}\n      mode={mode}\n      sampleData={sampleData}\n      showBrowser={showBrowser}\n      initialKey={preselected ? existingState?.initialKey : undefined}\n      initialKeyType={preselected ? existingState?.initialKeyType : undefined}\n      initialPrefix={preselected ? existingState?.initialPrefix : undefined}\n    >\n      <CreateIndexOnboardingProvider instanceId={instanceId}>\n        <S.PageWrapper data-testid=\"vector-search--create-index--page\">\n          <CreateIndexBrowser />\n\n          <S.RightPanel>\n            <CreateIndexHeader />\n            <CreateIndexContent />\n          </S.RightPanel>\n        </S.PageWrapper>\n      </CreateIndexOnboardingProvider>\n    </CreateIndexPageProvider>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport { SampleDataContent } from '../../components/pick-sample-data-modal/PickSampleDataModal.types'\n\nexport enum CreateIndexTab {\n  Table = 'table',\n  Command = 'command',\n}\n\nexport enum CreateIndexMode {\n  SampleData = 'sampleData',\n  ExistingData = 'existingData',\n}\n\nexport interface SampleDataLocationState {\n  sampleData: SampleDataContent\n  mode?: CreateIndexMode.SampleData\n}\n\nexport interface ExistingDataLocationState {\n  mode: CreateIndexMode.ExistingData\n  initialKey?: RedisResponseBuffer\n  initialKeyType?: RedisearchIndexKeyType\n  initialPrefix?: string\n}\n\nexport type CreateIndexLocationState =\n  | SampleDataLocationState\n  | ExistingDataLocationState\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/ConfirmKeyChangeModal/ConfirmKeyChangeModal.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { ConfirmKeyChangeModal } from './ConfirmKeyChangeModal'\nimport { ConfirmKeyChangeModalProps } from './ConfirmKeyChangeModal.types'\n\ndescribe('ConfirmKeyChangeModal', () => {\n  const defaultProps: ConfirmKeyChangeModalProps = {\n    onConfirm: jest.fn(),\n    onCancel: jest.fn(),\n  }\n\n  const renderComponent = (\n    propsOverride?: Partial<ConfirmKeyChangeModalProps>,\n  ) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<ConfirmKeyChangeModal {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render the modal with title and message', () => {\n    renderComponent()\n\n    const title = screen.getByText('Unsaved changes')\n    const message = screen.getByTestId('change-key-modal-message')\n\n    expect(title).toBeInTheDocument()\n    expect(message).toBeInTheDocument()\n  })\n\n  it('should call onCancel when \"Keep editing\" button is clicked', () => {\n    const onCancel = jest.fn()\n    renderComponent({ onCancel })\n\n    const cancelBtn = screen.getByTestId('change-key-modal-cancel')\n    fireEvent.click(cancelBtn)\n\n    expect(onCancel).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onConfirm when \"Discard and load\" button is clicked', () => {\n    const onConfirm = jest.fn()\n    renderComponent({ onConfirm })\n\n    const confirmBtn = screen.getByTestId('change-key-modal-confirm')\n    fireEvent.click(confirmBtn)\n\n    expect(onConfirm).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onCancel when close button is clicked', () => {\n    const onCancel = jest.fn()\n    renderComponent({ onCancel })\n\n    const closeBtn = screen.getByTestId('change-key-modal-close')\n    fireEvent.click(closeBtn)\n\n    expect(onCancel).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/ConfirmKeyChangeModal/ConfirmKeyChangeModal.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Modal } from 'uiSrc/components/base/display'\n\nexport const ConfirmModalContent = styled(Modal.Content.Compose)`\n  width: 500px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/ConfirmKeyChangeModal/ConfirmKeyChangeModal.tsx",
    "content": "import React from 'react'\n\nimport { Modal } from 'uiSrc/components/base/display'\nimport { Button, PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { CancelIcon } from 'uiSrc/components/base/icons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nimport { ConfirmKeyChangeModalProps } from './ConfirmKeyChangeModal.types'\nimport * as S from './ConfirmKeyChangeModal.styles'\n\nexport const ConfirmKeyChangeModal = ({\n  onConfirm,\n  onCancel,\n}: ConfirmKeyChangeModalProps) => (\n  <Modal.Compose open>\n    <S.ConfirmModalContent persistent onCancel={onCancel}>\n      <Modal.Content.Close\n        icon={CancelIcon}\n        onClick={onCancel}\n        data-testid=\"change-key-modal-close\"\n      />\n\n      <Modal.Content.Header.Compose>\n        <Modal.Content.Header.Title>Unsaved changes</Modal.Content.Header.Title>\n      </Modal.Content.Header.Compose>\n\n      <Col gap=\"m\">\n        <Text color=\"secondary\" data-testid=\"change-key-modal-message\">\n          You have modified the index types. Selecting a different key will\n          discard your changes and load fields from the new key.\n        </Text>\n        <Spacer size=\"xl\" />\n      </Col>\n\n      <Row justify=\"end\" gap=\"m\">\n        <Button\n          size=\"large\"\n          variant=\"secondary-ghost\"\n          onClick={onCancel}\n          data-testid=\"change-key-modal-cancel\"\n        >\n          Keep editing\n        </Button>\n        <PrimaryButton\n          size=\"large\"\n          onClick={onConfirm}\n          data-testid=\"change-key-modal-confirm\"\n        >\n          Discard and load\n        </PrimaryButton>\n      </Row>\n    </S.ConfirmModalContent>\n  </Modal.Compose>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/ConfirmKeyChangeModal/ConfirmKeyChangeModal.types.ts",
    "content": "export interface ConfirmKeyChangeModalProps {\n  onConfirm: () => void\n  onCancel: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/ConfirmKeyChangeModal/index.ts",
    "content": "export { ConfirmKeyChangeModal } from './ConfirmKeyChangeModal'\nexport type { ConfirmKeyChangeModalProps } from './ConfirmKeyChangeModal.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/CreateIndexBrowser.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { KeyTypes } from 'uiSrc/constants'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  KEY_TYPE_MAP,\n  REVERSE_KEY_TYPE_MAP,\n} from 'uiSrc/pages/vector-search/constants'\nimport { bufferToString } from 'uiSrc/utils'\n\nimport KeysBrowser from '../../../components/keys-browser/KeysBrowser'\nimport { useLoadKeyData } from '../../../hooks'\nimport { extractNamespace, deriveIndexName } from '../../../utils'\n\nimport { useCreateIndexPage } from '../../../context/create-index-page'\nimport { ConfirmKeyChangeModal } from './ConfirmKeyChangeModal'\nimport * as S from '../VectorSearchCreateIndexPage.styles'\n\nexport const CreateIndexBrowser = () => {\n  const {\n    showBrowser,\n    initialKey,\n    initialKeyType,\n    isFieldsDirty,\n    resetFieldsDirty,\n    setFields,\n    setKeyType,\n    setIndexPrefix,\n    setIndexName,\n  } = useCreateIndexPage()\n\n  const {\n    loadKeyData,\n    fields: loadedFields,\n    skippedFields: loadedSkippedFields,\n  } = useLoadKeyData()\n  const prevLoadedFieldsRef = useRef(loadedFields)\n  const initialKeyLoadedRef = useRef(false)\n\n  const [pendingKey, setPendingKey] = useState<{\n    key: RedisResponseBuffer\n    keyType: KeyTypes\n  } | null>(null)\n\n  useEffect(() => {\n    if (loadedFields !== prevLoadedFieldsRef.current) {\n      setFields(loadedFields, loadedSkippedFields)\n    }\n    prevLoadedFieldsRef.current = loadedFields\n  }, [loadedFields, loadedSkippedFields, setFields])\n\n  const applyKeySelection = useCallback(\n    (key: RedisResponseBuffer, keyType: KeyTypes) => {\n      const indexKeyType = KEY_TYPE_MAP[keyType] ?? RedisearchIndexKeyType.HASH\n      setKeyType(indexKeyType)\n\n      const keyName = bufferToString(key)\n      const namespace = extractNamespace(keyName)\n      setIndexPrefix(namespace)\n      setIndexName(deriveIndexName(namespace))\n\n      loadKeyData(key, indexKeyType)\n    },\n    [loadKeyData, setKeyType, setIndexPrefix, setIndexName],\n  )\n\n  const handleSelectKey = useCallback(\n    (key: RedisResponseBuffer, keyType: KeyTypes) => {\n      if (isFieldsDirty) {\n        setPendingKey({ key, keyType })\n        return\n      }\n      applyKeySelection(key, keyType)\n    },\n    [isFieldsDirty, applyKeySelection],\n  )\n\n  const handleConfirmKeyChange = useCallback(() => {\n    resetFieldsDirty()\n    if (pendingKey) {\n      applyKeySelection(pendingKey.key, pendingKey.keyType)\n    }\n    setPendingKey(null)\n  }, [pendingKey, applyKeySelection, resetFieldsDirty])\n\n  const handleCancelKeyChange = useCallback(() => {\n    setPendingKey(null)\n  }, [])\n\n  useEffect(() => {\n    if (\n      !showBrowser &&\n      initialKey &&\n      initialKeyType &&\n      !initialKeyLoadedRef.current\n    ) {\n      initialKeyLoadedRef.current = true\n      loadKeyData(initialKey, initialKeyType)\n    }\n  }, [showBrowser, initialKey, initialKeyType, loadKeyData])\n\n  if (!showBrowser) return null\n\n  return (\n    <>\n      <S.BrowserPanel data-testid=\"vector-search--create-index--browser-panel\">\n        <KeysBrowser\n          onSelectKey={handleSelectKey}\n          initialKey={initialKey}\n          initialKeyType={\n            initialKeyType ? REVERSE_KEY_TYPE_MAP[initialKeyType] : undefined\n          }\n        />\n      </S.BrowserPanel>\n\n      {pendingKey && (\n        <ConfirmKeyChangeModal\n          onConfirm={handleConfirmKeyChange}\n          onCancel={handleCancelKeyChange}\n        />\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/CreateIndexContent.tsx",
    "content": "import React from 'react'\nimport { useTheme } from '@redis-ui/styles'\n\nimport { Text } from 'uiSrc/components/base/text'\n\nimport { IndexDetails } from '../../../components/index-details'\nimport { IndexDetailsMode } from '../../../components/index-details/IndexDetails.types'\nimport { CommandView } from '../../../components/command-view'\nimport { FieldTypeModal } from '../../../components/field-type-modal'\nimport SelectDataImg from 'uiSrc/assets/img/vector-search/vector-search-browser.svg?react'\nimport SelectDataImgDark from 'uiSrc/assets/img/vector-search/vector-search-browser-dark.svg?react'\n\nimport {\n  CreateIndexTab,\n  CreateIndexMode,\n} from '../VectorSearchCreateIndexPage.types'\nimport { useCreateIndexPage } from '../../../context/create-index-page'\nimport { CreateIndexToolbar } from './CreateIndexToolbar'\nimport { CreateIndexFooter } from './CreateIndexFooter'\nimport * as S from '../VectorSearchCreateIndexPage.styles'\n\nexport const CreateIndexContent = () => {\n  const theme = useTheme()\n  const {\n    mode,\n    activeTab,\n    fields,\n    command,\n    isReadonly,\n    rowSelection,\n    onRowSelectionChange,\n    fieldModal,\n    openEditFieldModal,\n    closeFieldModal,\n    handleFieldSubmit,\n  } = useCreateIndexPage()\n\n  const isExistingData = mode === CreateIndexMode.ExistingData\n  const EmptyStateImg =\n    theme.name === 'dark' ? SelectDataImgDark : SelectDataImg\n\n  if (isExistingData && fields.length === 0) {\n    return (\n      <S.CardContainer data-testid=\"vector-search--create-index--card\">\n        <S.ContentArea data-testid=\"vector-search--create-index--content\">\n          <S.EmptyState\n            align=\"center\"\n            justify=\"center\"\n            data-testid=\"vector-search--create-index--empty-state\"\n          >\n            <EmptyStateImg />\n            <Text size=\"M\" color=\"secondary\">\n              The indexing schema will appear here once you{'\\n'}\n              select a key from the browser on the left.\n            </Text>\n          </S.EmptyState>\n        </S.ContentArea>\n        <CreateIndexFooter />\n      </S.CardContainer>\n    )\n  }\n\n  return (\n    <S.CardContainer data-testid=\"vector-search--create-index--card\">\n      <CreateIndexToolbar />\n\n      <S.ContentArea data-testid=\"vector-search--create-index--content\">\n        {activeTab === CreateIndexTab.Table && (\n          <IndexDetails\n            fields={fields}\n            mode={\n              isReadonly ? IndexDetailsMode.Readonly : IndexDetailsMode.Editable\n            }\n            rowSelection={isExistingData ? rowSelection : undefined}\n            onRowSelectionChange={\n              isExistingData ? onRowSelectionChange : undefined\n            }\n            onFieldEdit={openEditFieldModal}\n          />\n        )}\n\n        {activeTab === CreateIndexTab.Command && (\n          <CommandView\n            command={command}\n            dataTestId=\"vector-search--create-index--command-view\"\n          />\n        )}\n\n        <FieldTypeModal\n          isOpen={fieldModal.isOpen}\n          mode={fieldModal.mode}\n          field={fieldModal.field}\n          fields={fields}\n          onSubmit={handleFieldSubmit}\n          onClose={closeFieldModal}\n        />\n      </S.ContentArea>\n\n      <CreateIndexFooter />\n    </S.CardContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/CreateIndexFooter.tsx",
    "content": "import React from 'react'\n\nimport {\n  PrimaryButton,\n  SecondaryButton,\n} from 'uiSrc/components/base/forms/buttons'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\nimport { CallOut } from 'uiSrc/components/base/display'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nimport { useCreateIndexPage } from '../../../context/create-index-page'\nimport * as S from '../VectorSearchCreateIndexPage.styles'\n\nexport const CreateIndexFooter = () => {\n  const {\n    loading,\n    isCreateDisabled,\n    createDisabledReason,\n    skippedFields,\n    handleCreateIndex,\n    handleCancel,\n  } = useCreateIndexPage()\n\n  return (\n    <S.FooterRow\n      align=\"center\"\n      justify=\"between\"\n      data-testid=\"vector-search--create-index--footer\"\n    >\n      {skippedFields.length > 0 ? (\n        <CallOut\n          variant=\"notice\"\n          data-testid=\"vector-search--create-index--skipped-fields-banner\"\n        >\n          <Row align=\"center\" gap=\"s\">\n            <RiIcon type=\"InfoIcon\" size=\"s\" />\n            <span>\n              {skippedFields.length === 1\n                ? `Field \"${skippedFields[0]}\" was removed — nested objects and arrays cannot be indexed directly.`\n                : `${skippedFields.length} fields were removed (${skippedFields.join(', ')}) — nested objects and arrays cannot be indexed directly.`}\n            </span>\n          </Row>\n        </CallOut>\n      ) : (\n        <span />\n      )}\n\n      <Row gap=\"s\" grow={false}>\n        <SecondaryButton\n          onClick={handleCancel}\n          data-testid=\"vector-search--create-index--cancel-btn\"\n        >\n          Cancel\n        </SecondaryButton>\n\n        <RiTooltip\n          content={createDisabledReason}\n          data-testid=\"vector-search--create-index--submit-tooltip\"\n        >\n          <PrimaryButton\n            loading={loading}\n            disabled={isCreateDisabled}\n            onClick={handleCreateIndex}\n            data-testid=\"vector-search--create-index--submit-btn\"\n          >\n            Create index\n          </PrimaryButton>\n        </RiTooltip>\n      </Row>\n    </S.FooterRow>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/CreateIndexHeader.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\n\nimport { Title } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nimport { CreateIndexMode } from '../VectorSearchCreateIndexPage.types'\nimport { useCreateIndexPage } from '../../../context/create-index-page'\nimport { useCreateIndexOnboarding } from '../../../context/create-index-onboarding'\nimport { CreateIndexOnboardingPopover } from '../../../components/create-index-onboarding'\nimport { CreateIndexOnboardingStep } from '../../../components/create-index-onboarding/CreateIndexOnboarding.constants'\nimport { IndexNameEditor } from './IndexNameEditor'\nimport * as S from '../VectorSearchCreateIndexPage.styles'\n\nconst INFO_TOOLTIP =\n  'Select a key from the left panel to auto-detect the indexing schema.'\n\nexport const CreateIndexHeader = () => {\n  const { mode, displayName, indexName, setIndexName, indexNameError, fields } =\n    useCreateIndexPage()\n  const { startOnboarding } = useCreateIndexOnboarding()\n  const onboardingTriggeredRef = useRef(false)\n\n  const isSampleData = mode === CreateIndexMode.SampleData\n  const hasFields = fields.length > 0\n\n  useEffect(() => {\n    if (onboardingTriggeredRef.current) return\n    if (!isSampleData && hasFields) {\n      onboardingTriggeredRef.current = true\n      startOnboarding()\n    }\n  }, [isSampleData, hasFields, startOnboarding])\n\n  return (\n    <S.TitleRow data-testid=\"vector-search--create-index--header\">\n      <CreateIndexOnboardingPopover\n        step={CreateIndexOnboardingStep.DefineIndex}\n        anchorPosition=\"rightCenter\"\n      >\n        <Row align=\"center\" gap=\"s\" grow={false}>\n          <Title\n            size=\"M\"\n            color=\"primary\"\n            data-testid=\"vector-search--create-index--title\"\n          >\n            {isSampleData\n              ? `View sample data index: ${displayName}`\n              : 'Define search index:'}\n          </Title>\n\n          {!isSampleData && !hasFields && (\n            <RiTooltip\n              position=\"bottom\"\n              content={INFO_TOOLTIP}\n              data-testid=\"index-name-info-tooltip\"\n            >\n              <Row align=\"center\" grow={false}>\n                <RiIcon\n                  type=\"InfoIcon\"\n                  size=\"l\"\n                  data-testid=\"index-name-info-icon\"\n                />\n              </Row>\n            </RiTooltip>\n          )}\n        </Row>\n      </CreateIndexOnboardingPopover>\n\n      {!isSampleData && hasFields && (\n        <IndexNameEditor\n          indexName={indexName}\n          onNameChange={setIndexName}\n          validationError={indexNameError}\n        />\n      )}\n    </S.TitleRow>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/CreateIndexToolbar.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\n\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { Text } from 'uiSrc/components/base/text'\nimport { ButtonGroup } from 'uiSrc/components/base/forms/button-group/ButtonGroup'\n\nimport {\n  CreateIndexTab,\n  CreateIndexMode,\n} from '../VectorSearchCreateIndexPage.types'\nimport { useCreateIndexPage } from '../../../context/create-index-page'\nimport { useCreateIndexOnboarding } from '../../../context/create-index-onboarding'\nimport { CreateIndexOnboardingPopover } from '../../../components/create-index-onboarding'\nimport { CreateIndexOnboardingStep } from '../../../components/create-index-onboarding/CreateIndexOnboarding.constants'\nimport * as S from '../VectorSearchCreateIndexPage.styles'\n\nexport const CreateIndexToolbar = () => {\n  const {\n    mode,\n    activeTab,\n    setActiveTab,\n    indexPrefix,\n    setIndexPrefix,\n    isReadonly,\n    openAddFieldModal,\n  } = useCreateIndexPage()\n\n  const { currentStep, isActive } = useCreateIndexOnboarding()\n  const tabBeforeOnboardingRef = useRef<CreateIndexTab | null>(null)\n\n  useEffect(() => {\n    if (isActive && currentStep === CreateIndexOnboardingStep.CommandView) {\n      tabBeforeOnboardingRef.current = activeTab\n      setActiveTab(CreateIndexTab.Command)\n    } else if (tabBeforeOnboardingRef.current !== null) {\n      setActiveTab(tabBeforeOnboardingRef.current)\n      tabBeforeOnboardingRef.current = null\n    }\n  }, [currentStep, isActive])\n\n  const isExistingData = mode === CreateIndexMode.ExistingData\n\n  return (\n    <S.ToolbarRow\n      align=\"center\"\n      justify=\"between\"\n      data-testid=\"vector-search--create-index--toolbar\"\n    >\n      <CreateIndexOnboardingPopover\n        step={CreateIndexOnboardingStep.CommandView}\n        anchorPosition=\"rightCenter\"\n      >\n        <ButtonGroup data-testid=\"vector-search--create-index--view-toggle\">\n          <ButtonGroup.Button\n            isSelected={activeTab === CreateIndexTab.Table}\n            onClick={() => setActiveTab(CreateIndexTab.Table)}\n            data-testid=\"vector-search--create-index--table-view-btn\"\n          >\n            Table view\n          </ButtonGroup.Button>\n          <ButtonGroup.Button\n            isSelected={activeTab === CreateIndexTab.Command}\n            onClick={() => setActiveTab(CreateIndexTab.Command)}\n            data-testid=\"vector-search--create-index--command-view-btn\"\n          >\n            Command view\n          </ButtonGroup.Button>\n        </ButtonGroup>\n      </CreateIndexOnboardingPopover>\n\n      <S.ToolbarRight\n        align=\"center\"\n        data-testid=\"vector-search--create-index--toolbar-right\"\n      >\n        <EmptyButton\n          disabled={isReadonly}\n          onClick={openAddFieldModal}\n          data-testid=\"vector-search--create-index--add-field-btn\"\n        >\n          + Add field\n        </EmptyButton>\n\n        <S.VerticalSeparator />\n\n        <CreateIndexOnboardingPopover\n          step={CreateIndexOnboardingStep.IndexPrefix}\n          anchorPosition=\"downCenter\"\n        >\n          <S.IndexPrefixRow align=\"center\">\n            <Text size=\"S\" color=\"secondary\">\n              Index prefix:\n            </Text>\n            {isExistingData ? (\n              <S.IndexPrefixInput\n                value={indexPrefix}\n                onChange={(value: string) => setIndexPrefix(value)}\n                data-testid=\"vector-search--create-index--prefix-input\"\n              />\n            ) : (\n              <Text\n                size=\"S\"\n                color=\"default\"\n                data-testid=\"vector-search--create-index--prefix-value\"\n              >\n                {indexPrefix}\n              </Text>\n            )}\n          </S.IndexPrefixRow>\n        </CreateIndexOnboardingPopover>\n      </S.ToolbarRight>\n    </S.ToolbarRow>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/IndexNameEditor/IndexNameEditor.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { useIndexNameValidation } from '../../../../hooks'\nimport { IndexNameEditor } from './IndexNameEditor'\nimport { IndexNameEditorProps } from './IndexNameEditor.types'\n\njest.mock('../../../../hooks', () => ({\n  ...jest.requireActual('../../../../hooks'),\n  useIndexNameValidation: jest.fn(() => null),\n}))\n\nconst mockUseIndexNameValidation = useIndexNameValidation as jest.Mock\n\ndescribe('IndexNameEditor', () => {\n  const defaultProps: IndexNameEditorProps = {\n    indexName: faker.string.alphanumeric(10),\n    onNameChange: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<IndexNameEditorProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<IndexNameEditor {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should show index name with edit button by default', () => {\n    renderComponent()\n\n    const display = screen.getByTestId('index-name-display')\n    const nameText = screen.getByText(defaultProps.indexName)\n    const editBtn = screen.getByTestId('index-name-edit-btn')\n\n    expect(display).toBeInTheDocument()\n    expect(nameText).toBeInTheDocument()\n    expect(editBtn).toBeInTheDocument()\n  })\n\n  it('should enter edit mode when clicking the display row', () => {\n    renderComponent()\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const editInput = screen.getByTestId('index-name-edit-input')\n    const cancelBtn = screen.getByTestId('index-name-cancel-btn')\n    const confirmBtn = screen.getByTestId('index-name-confirm-btn')\n\n    expect(editInput).toBeInTheDocument()\n    expect(cancelBtn).toBeInTheDocument()\n    expect(confirmBtn).toBeInTheDocument()\n  })\n\n  it('should call onNameChange and exit edit mode on confirm', () => {\n    const onNameChange = jest.fn()\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const confirmBtn = screen.getByTestId('index-name-confirm-btn')\n    fireEvent.click(confirmBtn)\n\n    const editInput = screen.queryByTestId('index-name-edit-input')\n\n    expect(onNameChange).toHaveBeenCalledWith(defaultProps.indexName)\n    expect(editInput).not.toBeInTheDocument()\n  })\n\n  it('should exit edit mode without calling onNameChange on cancel', () => {\n    const onNameChange = jest.fn()\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const cancelBtn = screen.getByTestId('index-name-cancel-btn')\n    fireEvent.click(cancelBtn)\n\n    const editInput = screen.queryByTestId('index-name-edit-input')\n\n    expect(onNameChange).not.toHaveBeenCalled()\n    expect(editInput).not.toBeInTheDocument()\n  })\n\n  it('should confirm on Enter key', () => {\n    const onNameChange = jest.fn()\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const editInput = screen.getByTestId('index-name-edit-input')\n    fireEvent.keyDown(editInput, { key: 'Enter' })\n\n    const editInputAfter = screen.queryByTestId('index-name-edit-input')\n\n    expect(onNameChange).toHaveBeenCalledWith(defaultProps.indexName)\n    expect(editInputAfter).not.toBeInTheDocument()\n  })\n\n  it('should cancel on Escape key', () => {\n    const onNameChange = jest.fn()\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const editInput = screen.getByTestId('index-name-edit-input')\n    fireEvent.keyDown(editInput, { key: 'Escape' })\n\n    const editInputAfter = screen.queryByTestId('index-name-edit-input')\n\n    expect(onNameChange).not.toHaveBeenCalled()\n    expect(editInputAfter).not.toBeInTheDocument()\n  })\n\n  it('should submit updated draft value on confirm', () => {\n    const onNameChange = jest.fn()\n    const newName = faker.string.alphanumeric(8)\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const editInput = screen.getByTestId('index-name-edit-input')\n    fireEvent.change(editInput, { target: { value: newName } })\n\n    const confirmBtn = screen.getByTestId('index-name-confirm-btn')\n    fireEvent.click(confirmBtn)\n\n    expect(onNameChange).toHaveBeenCalledWith(newName)\n  })\n\n  it('should validate the draft value, not the committed indexName', () => {\n    const errorMsg = 'An index with this name already exists.'\n    mockUseIndexNameValidation.mockReturnValue(errorMsg)\n    renderComponent()\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const errorText = screen.getByText(errorMsg)\n    expect(errorText).toBeInTheDocument()\n  })\n\n  it('should not confirm when draft has a validation error', () => {\n    const errorMsg = 'An index with this name already exists.'\n    mockUseIndexNameValidation.mockReturnValue(errorMsg)\n    const onNameChange = jest.fn()\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const confirmBtn = screen.getByTestId('index-name-confirm-btn')\n    fireEvent.click(confirmBtn)\n\n    const editInput = screen.getByTestId('index-name-edit-input')\n\n    expect(onNameChange).not.toHaveBeenCalled()\n    expect(editInput).toBeInTheDocument()\n  })\n\n  it('should not confirm on Enter key when draft has a validation error', () => {\n    const errorMsg = 'Index name is required.'\n    mockUseIndexNameValidation.mockReturnValue(errorMsg)\n    const onNameChange = jest.fn()\n    renderComponent({ onNameChange })\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const editInput = screen.getByTestId('index-name-edit-input')\n    fireEvent.keyDown(editInput, { key: 'Enter' })\n\n    expect(onNameChange).not.toHaveBeenCalled()\n    expect(editInput).toBeInTheDocument()\n  })\n\n  it('should disable confirm button when draft has a validation error', () => {\n    mockUseIndexNameValidation.mockReturnValue('Index name is required.')\n    renderComponent()\n\n    const indexNameDisplay = screen.getByTestId('index-name-display')\n    fireEvent.click(indexNameDisplay)\n\n    const confirmBtn = screen.getByTestId('index-name-confirm-btn')\n    expect(confirmBtn).toBeDisabled()\n  })\n\n  it('should show error icon when validationError is provided', () => {\n    const errorMsg = 'An index with this name already exists.'\n    renderComponent({ validationError: errorMsg })\n\n    const errorIcon = screen.getByTestId('index-name-error-icon')\n    expect(errorIcon).toBeInTheDocument()\n  })\n\n  it('should not show error icon when validationError is null', () => {\n    renderComponent({ validationError: null })\n\n    const errorIcon = screen.queryByTestId('index-name-error-icon')\n    expect(errorIcon).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/IndexNameEditor/IndexNameEditor.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport TextInput from 'uiSrc/components/base/inputs/TextInput'\n\nexport const DisplayRow = styled(Row).attrs({\n  grow: false,\n  align: 'center',\n})`\n  cursor: pointer;\n  gap: ${({ theme }) => theme.core?.space.space050};\n`\n\nexport const EditRow = styled(Row).attrs({ grow: false })`\n  gap: ${({ theme }) => theme.core?.space.space050};\n`\n\nexport const NameInput = styled(TextInput)`\n  width: 200px;\n`\n\nexport const ErrorIcon = styled(Row).attrs({ align: 'center', grow: false })`\n  color: ${({ theme }) => theme.semantic?.color?.text?.danger500};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/IndexNameEditor/IndexNameEditor.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { Title } from 'uiSrc/components/base/text'\nimport { IconButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  RiIcon,\n  PencilIcon,\n  CancelSlimIcon,\n  CheckThinIcon,\n} from 'uiSrc/components/base/icons'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\n\nimport { useIndexNameValidation } from '../../../../hooks'\nimport { IndexNameEditorProps } from './IndexNameEditor.types'\nimport * as S from './IndexNameEditor.styles'\n\nexport const IndexNameEditor = ({\n  indexName,\n  onNameChange,\n  validationError,\n}: IndexNameEditorProps) => {\n  const [isEditing, setIsEditing] = useState(false)\n  const [draft, setDraft] = useState(indexName)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const draftError = useIndexNameValidation(draft)\n  const hasError = !!draftError\n\n  useEffect(() => {\n    if (isEditing) {\n      inputRef.current?.focus()\n      inputRef.current?.select()\n    }\n  }, [isEditing])\n\n  useEffect(() => {\n    if (isEditing) {\n      inputRef.current?.focus()\n    }\n  }, [isEditing, hasError])\n\n  const startEditing = useCallback(() => {\n    setDraft(indexName)\n    setIsEditing(true)\n  }, [indexName])\n\n  const cancelEditing = useCallback(() => {\n    setIsEditing(false)\n    setDraft(indexName)\n  }, [indexName])\n\n  const confirmEditing = useCallback(() => {\n    if (draftError) {\n      return\n    }\n\n    onNameChange(draft)\n    setIsEditing(false)\n  }, [draft, draftError, onNameChange])\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === 'Enter') {\n        e.preventDefault()\n        confirmEditing()\n      }\n      if (e.key === 'Escape') {\n        e.preventDefault()\n        cancelEditing()\n      }\n    },\n    [confirmEditing, cancelEditing],\n  )\n\n  if (isEditing) {\n    return (\n      <S.EditRow align=\"center\">\n        <S.NameInput\n          value={draft}\n          onChange={(value: string) => setDraft(value)}\n          onKeyDown={handleKeyDown}\n          ref={inputRef}\n          error={draftError || undefined}\n          data-testid=\"index-name-edit-input\"\n        />\n        <IconButton\n          icon={CancelSlimIcon}\n          size=\"S\"\n          aria-label=\"Cancel editing\"\n          onClick={cancelEditing}\n          data-testid=\"index-name-cancel-btn\"\n        />\n        <IconButton\n          icon={CheckThinIcon}\n          size=\"S\"\n          color=\"primary\"\n          aria-label=\"Confirm index name\"\n          onClick={confirmEditing}\n          disabled={hasError}\n          data-testid=\"index-name-confirm-btn\"\n        />\n      </S.EditRow>\n    )\n  }\n\n  return (\n    <S.DisplayRow onClick={startEditing} data-testid=\"index-name-display\">\n      {validationError && (\n        <RiTooltip\n          position=\"bottom\"\n          content={validationError}\n          data-testid=\"index-name-error-tooltip\"\n        >\n          <S.ErrorIcon>\n            <RiIcon\n              type=\"ToastDangerIcon\"\n              size=\"l\"\n              data-testid=\"index-name-error-icon\"\n            />\n          </S.ErrorIcon>\n        </RiTooltip>\n      )}\n\n      <Title size=\"M\" color=\"primary\">\n        {indexName}\n      </Title>\n      <IconButton\n        icon={PencilIcon}\n        size=\"S\"\n        aria-label=\"Edit index name\"\n        data-testid=\"index-name-edit-btn\"\n      />\n    </S.DisplayRow>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/IndexNameEditor/IndexNameEditor.types.ts",
    "content": "export interface IndexNameEditorProps {\n  indexName: string\n  onNameChange: (name: string) => void\n  validationError?: string | null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/components/IndexNameEditor/index.ts",
    "content": "export { IndexNameEditor } from './IndexNameEditor'\nexport type { IndexNameEditorProps } from './IndexNameEditor.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchCreateIndexPage/index.ts",
    "content": "export { VectorSearchCreateIndexPage } from './VectorSearchCreateIndexPage'\nexport { VectorSearchCreateIndexPage as default } from './VectorSearchCreateIndexPage'\nexport { CreateIndexTab } from './VectorSearchCreateIndexPage.types'\nexport type { CreateIndexLocationState } from './VectorSearchCreateIndexPage.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/VectorSearchListPage.spec.tsx",
    "content": "import React from 'react'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { RootState } from 'uiSrc/slices/store'\nimport {\n  INSTANCE_ID_MOCK,\n  INSTANCES_MOCK,\n} from 'uiSrc/mocks/handlers/instances/instancesHandlers'\n\nimport { mockIndexListData } from 'uiSrc/mocks/factories/vector-search/indexList.factory'\n\nimport { VectorSearchListPage } from './VectorSearchListPage'\n\njest.mock('../../hooks/useIndexListData', () => ({\n  useIndexListData: jest.fn(() => ({\n    data: mockIndexListData,\n    loading: false,\n  })),\n}))\n\njest.mock('../../context/vector-search', () => ({\n  useVectorSearch: jest.fn(() => ({\n    openPickSampleDataModal: jest.fn(),\n  })),\n}))\n\nconst mockHistoryPush = jest.fn()\nconst mockInstanceId = INSTANCE_ID_MOCK\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: mockHistoryPush,\n  }),\n  useParams: () => ({\n    instanceId: mockInstanceId,\n  }),\n}))\n\nconst getTestState = (): RootState => ({\n  ...initialStateDefault,\n  connections: {\n    ...initialStateDefault.connections,\n    instances: {\n      ...initialStateDefault.connections.instances,\n      connectedInstance: {\n        ...initialStateDefault.connections.instances.connectedInstance,\n        ...INSTANCES_MOCK[0],\n      },\n    },\n  },\n})\n\nconst renderComponent = () => {\n  const store = mockStore(getTestState())\n  return render(<VectorSearchListPage />, { store })\n}\n\ndescribe('VectorSearchListPage', () => {\n  beforeEach(() => {\n    cleanup()\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render the page layout with header, title, info icon, create button, and table', () => {\n    renderComponent()\n\n    const page = screen.getByTestId('vector-search--list--page')\n    expect(page).toBeInTheDocument()\n\n    const header = screen.getByTestId('vector-search--list--header')\n    expect(header).toBeInTheDocument()\n\n    const title = screen.getByTestId('vector-search--list--title')\n    expect(title).toBeInTheDocument()\n    expect(screen.getByText('Search indexes')).toBeInTheDocument()\n\n    const infoIcon = screen.getByTestId('vector-search--list--info-icon')\n    expect(infoIcon).toBeInTheDocument()\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    expect(btn).toBeInTheDocument()\n    expect(screen.getByText('+ Create search index')).toBeInTheDocument()\n\n    const table = screen.getByTestId('vector-search--list--table')\n    expect(table).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/VectorSearchListPage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const HeaderRow = styled(Row).attrs({ grow: false })`\n  align-items: center;\n`\n\nexport const PageLayout = styled(Col).attrs({ gap: 'l' })`\n  min-height: 0;\n  min-width: 0;\n  overflow: hidden;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/VectorSearchListPage.tsx",
    "content": "import React from 'react'\n\nimport { useRedisInstanceCompatibility } from '../../hooks'\nimport { UpgradeRedisBanner } from '../../components/upgrade-redis-banner'\nimport { ListHeader } from './components/ListHeader'\nimport { ListContent } from './components/list-content'\n\nimport * as S from './VectorSearchListPage.styles'\n\n/**\n * Vector Search List Page.\n * Displays the list of search indexes with a header (title, info popover,\n * create-index dropdown) and a table of index data.\n * Shows an upgrade banner when the Redis version is below the minimum supported.\n */\nexport const VectorSearchListPage = () => {\n  const { hasSupportedVersion } = useRedisInstanceCompatibility()\n\n  return (\n    <S.PageLayout data-testid=\"vector-search--list--page\">\n      {hasSupportedVersion === false && <UpgradeRedisBanner />}\n\n      <ListHeader />\n      <ListContent />\n    </S.PageLayout>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/ListHeader.tsx",
    "content": "import React from 'react'\n\nimport { HeaderTitle } from './header-title'\nimport { CreateIndexMenu } from './create-index-menu'\n\nimport * as S from '../VectorSearchListPage.styles'\n\nexport const ListHeader = () => (\n  <S.HeaderRow justify=\"between\" data-testid=\"vector-search--list--header\">\n    <HeaderTitle />\n    <CreateIndexMenu />\n  </S.HeaderRow>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/create-index-menu/CreateIndexMenu.spec.tsx",
    "content": "import React from 'react'\nimport { act } from '@testing-library/react'\nimport {\n  fireEvent,\n  render,\n  screen,\n  userEvent,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\n\nimport { useVectorSearch } from '../../../../context/vector-search'\nimport { CreateIndexMenu } from './CreateIndexMenu'\n\njest.mock('../../../../context/vector-search', () => ({\n  useVectorSearch: jest.fn(),\n}))\n\nconst mockUseVectorSearch = jest.mocked(useVectorSearch)\n\nconst renderComponent = ({\n  hasExistingKeys = false,\n  hasExistingKeysLoading = false,\n} = {}) => {\n  mockUseVectorSearch.mockReturnValue({\n    hasExistingKeys,\n    hasExistingKeysLoading,\n    openPickSampleDataModal: jest.fn(),\n    navigateToExistingDataFlow: jest.fn(),\n  } as unknown as ReturnType<typeof useVectorSearch>)\n\n  return render(<CreateIndexMenu />)\n}\n\ndescribe('CreateIndexMenu', () => {\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render the create index button', () => {\n    renderComponent()\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    expect(btn).toBeInTheDocument()\n    expect(screen.getByText('+ Create search index')).toBeInTheDocument()\n  })\n\n  it('should open the menu and show options when button is clicked', async () => {\n    renderComponent()\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    await userEvent.click(btn)\n\n    const sampleDataItem = screen.getByTestId(\n      'vector-search--list--create-index--sample-data',\n    )\n    expect(sampleDataItem).toBeInTheDocument()\n\n    const existingDataItem = screen.getByTestId(\n      'vector-search--list--create-index--existing-data',\n    )\n    expect(existingDataItem).toBeInTheDocument()\n  })\n\n  it('should call openPickSampleDataModal when \"Use sample data\" is clicked', async () => {\n    renderComponent()\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    await userEvent.click(btn)\n\n    const sampleDataItem = screen.getByTestId(\n      'vector-search--list--create-index--sample-data',\n    )\n    await userEvent.click(sampleDataItem)\n\n    expect(mockUseVectorSearch().openPickSampleDataModal).toHaveBeenCalled()\n  })\n\n  it('should have \"Use existing data\" option disabled when no keys exist', async () => {\n    renderComponent({ hasExistingKeys: false })\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    await userEvent.click(btn)\n\n    const existingDataItem = screen.getByTestId(\n      'vector-search--list--create-index--existing-data',\n    )\n    expect(existingDataItem).toHaveAttribute('aria-disabled', 'true')\n  })\n\n  it('should show tooltip when no keys exist', async () => {\n    renderComponent({ hasExistingKeys: false })\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    await userEvent.click(btn)\n\n    const existingDataItem = screen.getByTestId(\n      'vector-search--list--create-index--existing-data',\n    )\n\n    await act(async () => {\n      fireEvent.focus(existingDataItem)\n    })\n    await waitForRiTooltipVisible()\n\n    expect(\n      screen.getAllByText('No Hash or JSON keys found in your database')[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should show loading tooltip when keys are being checked', async () => {\n    renderComponent({ hasExistingKeysLoading: true })\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    await userEvent.click(btn)\n\n    const existingDataItem = screen.getByTestId(\n      'vector-search--list--create-index--existing-data',\n    )\n\n    await act(async () => {\n      fireEvent.focus(existingDataItem)\n    })\n    await waitForRiTooltipVisible()\n\n    expect(\n      screen.getAllByText('Checking for existing keys…')[0],\n    ).toBeInTheDocument()\n  })\n\n  it('should enable \"Use existing data\" when keys exist', async () => {\n    renderComponent({ hasExistingKeys: true })\n\n    const btn = screen.getByTestId('vector-search--list--create-index-btn')\n    await userEvent.click(btn)\n\n    const existingDataItem = screen.getByTestId(\n      'vector-search--list--create-index--existing-data',\n    )\n    expect(existingDataItem).not.toHaveAttribute('aria-disabled', 'true')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/create-index-menu/CreateIndexMenu.tsx",
    "content": "import React, { useCallback, useMemo } from 'react'\n\nimport { ToggleButton } from 'uiSrc/components/base/forms/buttons'\nimport {\n  Menu,\n  MenuContent,\n  MenuItem,\n  MenuTrigger,\n  MenuDropdownArrow,\n} from 'uiSrc/components/base/layout/menu'\nimport { RiTooltip } from 'uiSrc/components/base/tooltip'\n\nimport { useVectorSearch } from '../../../../context/vector-search'\nimport { SearchTelemetrySource } from '../../../../telemetry.constants'\n\nexport const CreateIndexMenu = () => {\n  const {\n    openPickSampleDataModal,\n    navigateToExistingDataFlow,\n    hasExistingKeys,\n    hasExistingKeysLoading,\n  } = useVectorSearch()\n\n  const isExistingDataDisabled = hasExistingKeysLoading || !hasExistingKeys\n\n  const existingDataTooltip = useMemo(() => {\n    if (hasExistingKeysLoading) {\n      return 'Checking for existing keys…'\n    }\n\n    if (!hasExistingKeys) {\n      return 'No Hash or JSON keys found in your database'\n    }\n\n    return null\n  }, [hasExistingKeysLoading, hasExistingKeys])\n\n  const handleSampleData = useCallback(\n    () => openPickSampleDataModal(SearchTelemetrySource.List),\n    [openPickSampleDataModal],\n  )\n\n  const handleExistingData = useCallback(\n    () => navigateToExistingDataFlow(SearchTelemetrySource.List),\n    [navigateToExistingDataFlow],\n  )\n\n  return (\n    <Menu>\n      <MenuTrigger>\n        <ToggleButton data-testid=\"vector-search--list--create-index-btn\">\n          + Create search index\n        </ToggleButton>\n      </MenuTrigger>\n      <MenuContent align=\"end\">\n        <MenuItem\n          text=\"Use sample data\"\n          onClick={handleSampleData}\n          data-testid=\"vector-search--list--create-index--sample-data\"\n        />\n        <RiTooltip\n          content={isExistingDataDisabled ? existingDataTooltip : null}\n        >\n          <MenuItem\n            text=\"Use existing data\"\n            disabled={isExistingDataDisabled}\n            onClick={handleExistingData}\n            data-testid=\"vector-search--list--create-index--existing-data\"\n          />\n        </RiTooltip>\n        <MenuDropdownArrow />\n      </MenuContent>\n    </Menu>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/create-index-menu/index.ts",
    "content": "export { CreateIndexMenu } from './CreateIndexMenu'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/delete-index-confirmation/DeleteIndexConfirmation.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, userEvent } from 'uiSrc/utils/test-utils'\n\nimport {\n  DeleteIndexConfirmation,\n  DeleteIndexConfirmationProps,\n} from './DeleteIndexConfirmation'\n\nconst defaultProps: DeleteIndexConfirmationProps = {\n  isOpen: true,\n  onConfirm: jest.fn(),\n  onClose: jest.fn(),\n}\n\nconst renderComponent = (\n  propsOverride?: Partial<DeleteIndexConfirmationProps>,\n) => {\n  const props = { ...defaultProps, ...propsOverride }\n  return render(<DeleteIndexConfirmation {...props} />)\n}\n\ndescribe('DeleteIndexConfirmation', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should not render when isOpen is false', () => {\n    renderComponent({ isOpen: false })\n\n    expect(\n      screen.queryByTestId('delete-index-modal-message'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render modal when isOpen is true', () => {\n    renderComponent()\n\n    expect(screen.getByTestId('delete-index-modal-message')).toBeInTheDocument()\n    expect(screen.getByTestId('delete-index-modal-confirm')).toBeInTheDocument()\n    expect(screen.getByTestId('delete-index-modal-cancel')).toBeInTheDocument()\n  })\n\n  it('should call onConfirm when Delete index button is clicked', async () => {\n    const user = userEvent.setup({ pointerEventsCheck: 0 })\n    const onConfirm = jest.fn()\n    renderComponent({ onConfirm })\n\n    await user.click(screen.getByTestId('delete-index-modal-confirm'))\n\n    expect(onConfirm).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onClose when Keep index button is clicked', async () => {\n    const user = userEvent.setup({ pointerEventsCheck: 0 })\n    const onClose = jest.fn()\n    renderComponent({ onClose })\n\n    await user.click(screen.getByTestId('delete-index-modal-cancel'))\n\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n\n  it('should call onClose when close icon is clicked', async () => {\n    const user = userEvent.setup({ pointerEventsCheck: 0 })\n    const onClose = jest.fn()\n    renderComponent({ onClose })\n\n    await user.click(screen.getByTestId('delete-index-modal-close'))\n\n    expect(onClose).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/delete-index-confirmation/DeleteIndexConfirmation.tsx",
    "content": "import React from 'react'\n\nimport { DeleteConfirmationModal } from 'uiSrc/pages/vector-search/components/delete-confirmation-modal'\n\nexport interface DeleteIndexConfirmationProps {\n  isOpen: boolean\n  onConfirm: () => void\n  onClose: () => void\n}\n\nexport const DeleteIndexConfirmation = ({\n  isOpen,\n  onConfirm,\n  onClose,\n}: DeleteIndexConfirmationProps) => (\n  <DeleteConfirmationModal\n    isOpen={isOpen}\n    title=\"Delete Index\"\n    question=\"Are you sure you want to delete this index?\"\n    message=\"Deleting the index will remove it from Search and Vector Search, but will not delete your underlying data.\"\n    cancelLabel=\"Keep index\"\n    confirmLabel=\"Delete index\"\n    onConfirm={onConfirm}\n    onCancel={onClose}\n    testId=\"delete-index-modal\"\n  />\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/header-title/HeaderTitle.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen, fireEvent } from 'uiSrc/utils/test-utils'\n\nimport { HeaderTitle } from './HeaderTitle'\n\nconst renderComponent = () => render(<HeaderTitle />)\n\ndescribe('HeaderTitle', () => {\n  it('should render the title', () => {\n    renderComponent()\n\n    const title = screen.getByTestId('vector-search--list--title')\n    expect(title).toBeInTheDocument()\n    expect(screen.getByText('Search indexes')).toBeInTheDocument()\n  })\n\n  it('should render the info icon', () => {\n    renderComponent()\n\n    const infoIcon = screen.getByTestId('vector-search--list--info-icon')\n    expect(infoIcon).toBeInTheDocument()\n  })\n\n  it('should open the popover when info icon is clicked', () => {\n    renderComponent()\n\n    const infoIcon = screen.getByTestId('vector-search--list--info-icon')\n    fireEvent.click(infoIcon)\n\n    const popoverText = screen.getByText(/A search index organizes your data/)\n    expect(popoverText).toBeInTheDocument()\n  })\n\n  it('should render learn more link inside the popover', () => {\n    renderComponent()\n\n    const infoIcon = screen.getByTestId('vector-search--list--info-icon')\n    fireEvent.click(infoIcon)\n\n    const link = screen.getByTestId('vector-search--list--learn-more-link')\n    expect(link).toBeInTheDocument()\n    expect(link).toHaveAttribute(\n      'href',\n      'https://redis.io/docs/latest/develop/ai/search-and-query/query/vector-search/?utm_source=redisinsight&utm_medium=app&utm_campaign=vector_search',\n    )\n  })\n\n  it('should close the popover when info icon is clicked again', () => {\n    renderComponent()\n\n    const infoIcon = screen.getByTestId('vector-search--list--info-icon')\n\n    fireEvent.click(infoIcon)\n    const popoverText = screen.getByText(/A search index organizes your data/)\n    expect(popoverText).toBeInTheDocument()\n\n    fireEvent.click(infoIcon)\n    const popoverTextAfterClose = screen.queryByText(\n      /A search index organizes your data/,\n    )\n    expect(popoverTextAfterClose).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/header-title/HeaderTitle.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const InfoIconWrapper = styled(FlexItem)`\n  cursor: pointer;\n`\n\nexport const PopoverContent = styled(Col)`\n  max-width: 280px;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/header-title/HeaderTitle.tsx",
    "content": "import React, { useState } from 'react'\n\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { InfoIcon } from 'uiSrc/components/base/icons'\nimport { RiPopover } from 'uiSrc/components/base'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\n\nimport * as S from './HeaderTitle.styles'\n\nexport const HeaderTitle = () => {\n  const [isInfoPopoverOpen, setIsInfoPopoverOpen] = useState(false)\n\n  return (\n    <Row align=\"center\" gap=\"xs\">\n      <Title size=\"M\" color=\"primary\" data-testid=\"vector-search--list--title\">\n        Search indexes\n      </Title>\n      <RiPopover\n        anchorPosition=\"downCenter\"\n        isOpen={isInfoPopoverOpen}\n        closePopover={() => setIsInfoPopoverOpen(false)}\n        panelPaddingSize=\"l\"\n        button={\n          <S.InfoIconWrapper\n            data-testid=\"vector-search--list--info-icon\"\n            onClick={() => setIsInfoPopoverOpen((prev) => !prev)}\n          >\n            <InfoIcon />\n          </S.InfoIconWrapper>\n        }\n      >\n        <S.PopoverContent>\n          <Text>\n            A search index organizes your data to enable fast Vector, full-text,\n            hybrid, and numeric searches in Redis.\n          </Text>\n          <Spacer size=\"s\" />\n          <Link\n            href={getUtmExternalLink(EXTERNAL_LINKS.searchIndexes, {\n              campaign: 'vector_search',\n            })}\n            target=\"_blank\"\n            variant=\"inline\"\n            external\n            data-testid=\"vector-search--list--learn-more-link\"\n          >\n            Learn more\n          </Link>\n        </S.PopoverContent>\n      </RiPopover>\n    </Row>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/header-title/index.ts",
    "content": "export { HeaderTitle } from './HeaderTitle'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/list-content/ListContent.spec.tsx",
    "content": "import React from 'react'\nimport { faker } from '@faker-js/faker'\nimport { cleanup, render, screen, userEvent } from 'uiSrc/utils/test-utils'\nimport { ShowIcon, DeleteIcon } from 'uiSrc/components/base/icons'\n\nimport { useListContent } from 'uiSrc/pages/vector-search/hooks/useListContent'\nimport { ListContent } from './ListContent'\n\ntype UseListContentReturn = ReturnType<typeof useListContent>\n\njest.mock('uiSrc/pages/vector-search/hooks/useListContent', () => ({\n  useListContent: jest.fn(),\n}))\n\njest.mock('uiSrc/pages/vector-search/hooks/useIndexInfo/useIndexInfo', () => ({\n  useIndexInfo: jest.fn().mockReturnValue({\n    indexInfo: null,\n    loading: false,\n    error: null,\n    refetch: jest.fn(),\n  }),\n}))\n\nconst mockOnQueryClick = jest.fn()\nconst mockOnCloseViewPanel = jest.fn()\nconst mockOnConfirmDelete = jest.fn()\nconst mockOnCloseDelete = jest.fn()\nconst mockViewIndexCallback = jest.fn()\nconst mockBrowseDatasetCallback = jest.fn()\nconst mockDeleteCallback = jest.fn()\n\nconst defaultHookReturn: UseListContentReturn = {\n  data: [],\n  loading: false,\n  actions: [\n    { name: 'View index', icon: ShowIcon, callback: mockViewIndexCallback },\n    {\n      name: 'Browse dataset',\n      callback: mockBrowseDatasetCallback,\n    },\n    {\n      name: 'Delete',\n      icon: DeleteIcon,\n      variant: 'destructive',\n      callback: mockDeleteCallback,\n    },\n  ],\n  onQueryClick: mockOnQueryClick,\n  viewingIndexName: null,\n  onCloseViewPanel: mockOnCloseViewPanel,\n  pendingDeleteIndex: null,\n  onConfirmDelete: mockOnConfirmDelete,\n  onCloseDelete: mockOnCloseDelete,\n}\n\nconst mockIndexRow = {\n  id: faker.string.alpha(8),\n  name: faker.string.alpha(8),\n  prefixes: [],\n  fieldTypes: [],\n  numDocs: 0,\n  numRecords: 0,\n  numTerms: 0,\n  numFields: 0,\n}\n\nconst setupHook = (overrides?: Partial<typeof defaultHookReturn>) => {\n  ;(useListContent as jest.Mock).mockReturnValue({\n    ...defaultHookReturn,\n    ...overrides,\n  })\n}\n\nconst renderComponent = () => render(<ListContent />)\n\ndescribe('ListContent', () => {\n  beforeEach(() => {\n    cleanup()\n    setupHook()\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render the index list table', () => {\n    setupHook({ data: [mockIndexRow] })\n    renderComponent()\n\n    expect(screen.getByTestId('vector-search--list--table')).toBeInTheDocument()\n  })\n\n  it('should render empty state when no data', () => {\n    renderComponent()\n\n    expect(screen.getByText('No indexes found')).toBeInTheDocument()\n  })\n\n  it('should not render view index panel when viewingIndexName is null', () => {\n    renderComponent()\n\n    expect(screen.queryByTestId('view-index-panel')).not.toBeInTheDocument()\n  })\n\n  it('should render view index panel when viewingIndexName is set', () => {\n    setupHook({ viewingIndexName: 'my-index' })\n    renderComponent()\n\n    expect(screen.getByTestId('view-index-panel')).toBeInTheDocument()\n  })\n\n  it('should not render delete confirmation when pendingDeleteIndex is null', () => {\n    renderComponent()\n\n    expect(\n      screen.queryByText('Are you sure you want to delete this index?'),\n    ).not.toBeInTheDocument()\n  })\n\n  it('should render delete confirmation when pendingDeleteIndex is set', () => {\n    setupHook({ pendingDeleteIndex: 'some-index' })\n    renderComponent()\n\n    expect(\n      screen.getByText('Are you sure you want to delete this index?'),\n    ).toBeInTheDocument()\n  })\n\n  it('should call onQueryClick when query button is clicked', async () => {\n    setupHook({ data: [mockIndexRow] })\n    renderComponent()\n\n    const queryBtn = screen.getByTestId(`index-query-btn-${mockIndexRow.id}`)\n    await userEvent.click(queryBtn)\n\n    expect(mockOnQueryClick).toHaveBeenCalledWith(mockIndexRow.name)\n  })\n\n  it('should call action callback when menu item is clicked', async () => {\n    const user = userEvent.setup({ pointerEventsCheck: 0 })\n    setupHook({ data: [mockIndexRow] })\n    renderComponent()\n\n    const menuTrigger = screen.getByTestId(\n      `index-actions-menu-trigger-${mockIndexRow.id}`,\n    )\n    await user.click(menuTrigger)\n\n    const browseOption = screen.getByText('Browse dataset')\n    await user.click(browseOption)\n\n    expect(mockBrowseDatasetCallback).toHaveBeenCalledWith(mockIndexRow.name)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/list-content/ListContent.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const ContentArea = styled(Row)`\n  min-height: 0;\n  flex: 1;\n`\n\nexport const TableWrapper = styled(Col)`\n  padding: 2px;\n  min-width: 0;\n  overflow: auto;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/list-content/ListContent.tsx",
    "content": "import React from 'react'\n\nimport {\n  ResizableContainer,\n  ResizablePanel,\n  ResizablePanelHandle,\n} from 'uiSrc/components/base/layout'\n\nimport { IndexList } from 'uiSrc/pages/vector-search/components/index-list'\nimport { IndexInfoSidePanel } from 'uiSrc/pages/vector-search/components/index-info-side-panel'\nimport { useListContent } from 'uiSrc/pages/vector-search/hooks/useListContent'\n\nimport * as S from './ListContent.styles'\nimport { DeleteIndexConfirmation } from '../delete-index-confirmation/DeleteIndexConfirmation'\n\nexport const ListContent = () => {\n  const {\n    data,\n    loading,\n    actions,\n    onQueryClick,\n    viewingIndexName,\n    onCloseViewPanel,\n    pendingDeleteIndex,\n    onConfirmDelete,\n    onCloseDelete,\n  } = useListContent()\n\n  return (\n    <S.ContentArea>\n      <ResizableContainer direction=\"horizontal\">\n        <ResizablePanel\n          id=\"index-list-panel\"\n          order={1}\n          minSize={30}\n          defaultSize={viewingIndexName !== null ? 70 : 100}\n        >\n          <S.TableWrapper>\n            <IndexList\n              data={data}\n              loading={loading}\n              onQueryClick={onQueryClick}\n              actions={actions}\n              dataTestId=\"vector-search--list--table\"\n            />\n          </S.TableWrapper>\n        </ResizablePanel>\n\n        {viewingIndexName !== null && (\n          <>\n            <ResizablePanelHandle\n              direction=\"vertical\"\n              data-test-subj=\"resize-btn-view-index-panel\"\n            />\n\n            <ResizablePanel\n              id=\"view-index-panel\"\n              order={2}\n              minSize={15}\n              defaultSize={30}\n            >\n              <IndexInfoSidePanel\n                indexName={viewingIndexName}\n                onClose={onCloseViewPanel}\n              />\n            </ResizablePanel>\n          </>\n        )}\n      </ResizableContainer>\n\n      <DeleteIndexConfirmation\n        isOpen={!!pendingDeleteIndex}\n        onConfirm={onConfirmDelete}\n        onClose={onCloseDelete}\n      />\n    </S.ContentArea>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/components/list-content/index.ts",
    "content": "export { ListContent } from './ListContent'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchListPage/index.ts",
    "content": "export { VectorSearchListPage } from './VectorSearchListPage'\nexport { VectorSearchListPage as default } from './VectorSearchListPage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchPage/VectorSearchPage.spec.tsx",
    "content": "import React from 'react'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { sendPageViewTelemetry } from 'uiSrc/telemetry'\nimport { TelemetryPageView } from 'uiSrc/telemetry/pageViews'\nimport { RootState } from 'uiSrc/slices/store'\nimport {\n  INSTANCE_ID_MOCK,\n  INSTANCES_MOCK,\n} from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { VectorSearchPage } from './VectorSearchPage'\nimport { useRedisearchListData } from '../../hooks/useRedisearchListData'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n}))\n\njest.mock('../../hooks/useRedisearchListData', () => ({\n  useRedisearchListData: jest.fn(),\n}))\n\njest.mock('../../context/vector-search', () => ({\n  useVectorSearch: jest.fn(() => ({\n    openPickSampleDataModal: jest.fn(),\n  })),\n}))\n\nconst getTestState = (): RootState => ({\n  ...initialStateDefault,\n  connections: {\n    ...initialStateDefault.connections,\n    instances: {\n      ...initialStateDefault.connections.instances,\n      connectedInstance: {\n        ...initialStateDefault.connections.instances.connectedInstance,\n        ...INSTANCES_MOCK[0],\n      },\n    },\n  },\n})\n\nconst renderComponent = () => {\n  const store = mockStore(getTestState())\n  return render(<VectorSearchPage />, { store })\n}\n\ndescribe('VectorSearchPage', () => {\n  const mockUseRedisearchListData = useRedisearchListData as jest.Mock\n\n  beforeEach(() => {\n    cleanup()\n\n    mockUseRedisearchListData.mockReturnValue({\n      loading: false,\n      data: [],\n      stringData: [],\n    })\n  })\n\n  afterEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render vector search page', () => {\n    const { container } = renderComponent()\n\n    expect(container).toBeTruthy()\n  })\n\n  it('should render loader while loading indexes', () => {\n    mockUseRedisearchListData.mockReturnValue({\n      loading: true,\n      data: [],\n      stringData: [],\n    })\n\n    renderComponent()\n\n    const loader = screen.getByTestId('vector-search-loader')\n    expect(loader).toBeInTheDocument()\n  })\n\n  it('should render loader when indexes loading is undefined (not yet fetched)', () => {\n    mockUseRedisearchListData.mockReturnValue({\n      loading: undefined,\n      data: [],\n      stringData: [],\n    })\n\n    renderComponent()\n\n    const loader = screen.getByTestId('vector-search-loader')\n    expect(loader).toBeInTheDocument()\n\n    const welcomeScreen = screen.queryByTestId('welcome-screen')\n    expect(welcomeScreen).not.toBeInTheDocument()\n  })\n\n  it('should render welcome screen when no indexes exist', () => {\n    mockUseRedisearchListData.mockReturnValue({\n      loading: false,\n      data: [],\n      stringData: [],\n    })\n\n    renderComponent()\n\n    const welcomeScreen = screen.getByTestId('welcome-screen')\n    expect(welcomeScreen).toBeInTheDocument()\n  })\n\n  it('should render index list screen when indexes exist', () => {\n    mockUseRedisearchListData.mockReturnValue({\n      loading: false,\n      data: [Buffer.from('index1'), Buffer.from('index2')],\n      stringData: ['index1', 'index2'],\n    })\n\n    renderComponent()\n\n    const listPage = screen.getByTestId('vector-search--list--page')\n    expect(listPage).toBeInTheDocument()\n  })\n\n  it('should send page view telemetry with enhanced data when ready', () => {\n    mockUseRedisearchListData.mockReturnValue({\n      loading: false,\n      data: [Buffer.from('index1')],\n      stringData: ['index1'],\n    })\n\n    renderComponent()\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: TelemetryPageView.VECTOR_SEARCH_PAGE,\n      eventData: expect.objectContaining({\n        databaseId: INSTANCE_ID_MOCK,\n        number_of_indexes: 1,\n        welcome_page_enabled: false,\n      }),\n    })\n  })\n\n  it('should send welcome_page_enabled=true when no indexes', () => {\n    renderComponent()\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: TelemetryPageView.VECTOR_SEARCH_PAGE,\n      eventData: expect.objectContaining({\n        databaseId: INSTANCE_ID_MOCK,\n        number_of_indexes: 0,\n        welcome_page_enabled: true,\n      }),\n    })\n  })\n\n  it('should not send page view telemetry while still loading', () => {\n    mockUseRedisearchListData.mockReturnValue({\n      loading: true,\n      data: [],\n      stringData: [],\n    })\n\n    renderComponent()\n\n    expect(sendPageViewTelemetry).not.toHaveBeenCalled()\n  })\n\n  it('should set page title correctly', () => {\n    renderComponent()\n\n    expect(document.title).toBe(\n      `${INSTANCES_MOCK[0].name} [db${INSTANCES_MOCK[0].db}] - Vector Search`,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchPage/VectorSearchPage.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { TelemetryPageView } from 'uiSrc/telemetry'\nimport { usePageViewTelemetry } from 'uiSrc/telemetry/usePageViewTelemetry'\nimport { Loader } from 'uiSrc/components/base/display'\nimport {\n  formatLongName,\n  getDbIndex,\n  getRedisearchVersion,\n  setTitle,\n} from 'uiSrc/utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\nimport { useRedisearchListData } from '../../hooks'\nimport { VectorSearchWelcomePage } from '../VectorSearchWelcomePage'\nimport { VectorSearchListPage } from '../VectorSearchListPage'\nimport * as S from '../styles'\n\n/**\n * Main Vector Search page component.\n * Acts as the entry point that selects and renders the appropriate screen\n * based on the current state (indexes availability).\n * RediSearch module availability is guarded at the router level (VectorSearchPageRouter).\n */\nexport const VectorSearchPage = () => {\n  const { stringData: indexes, loading: indexesLoading } =\n    useRedisearchListData()\n\n  const {\n    name: connectedInstanceName,\n    db,\n    provider,\n    modules,\n  } = useSelector(connectedInstanceSelector)\n\n  const isReady = indexesLoading === false\n\n  usePageViewTelemetry({\n    page: TelemetryPageView.VECTOR_SEARCH_PAGE,\n    ready: isReady,\n    eventData: {\n      rqe_version: getRedisearchVersion(modules),\n      provider,\n      number_of_indexes: indexes.length,\n      welcome_page_enabled: indexes.length === 0,\n    },\n  })\n\n  setTitle(\n    `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Vector Search`,\n  )\n\n  if (indexesLoading !== false) {\n    return (\n      <S.PageWrapper\n        data-testid=\"vector-search-page--loading\"\n        align=\"center\"\n        justify=\"center\"\n      >\n        <Loader size=\"xl\" data-testid=\"vector-search-loader\" />\n      </S.PageWrapper>\n    )\n  }\n\n  if (indexes.length === 0) {\n    return (\n      <S.PageWrapper data-testid=\"vector-search-page--welcome\">\n        <VectorSearchWelcomePage />\n      </S.PageWrapper>\n    )\n  }\n\n  // Show index list when indexes exist\n  return (\n    <S.PageWrapper data-testid=\"vector-search-page--list\">\n      <VectorSearchListPage />\n    </S.PageWrapper>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchPage/index.ts",
    "content": "export { VectorSearchPage } from './VectorSearchPage'\nexport { VectorSearchPage as default } from './VectorSearchPage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/VectorSearchQueryPage.spec.tsx",
    "content": "import React from 'react'\nimport { act, cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils'\nimport { sendEventTelemetry } from 'uiSrc/telemetry'\nimport { TelemetryEvent } from 'uiSrc/telemetry/events'\nimport { commandExecutionUIFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\nimport { VectorSearchQueryPage } from './VectorSearchQueryPage'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\nconst mockInstanceId = 'instanceId'\nconst mockPush = jest.fn()\n\njest.mock('react-router-dom', () => {\n  const actual = jest.requireActual('react-router-dom')\n  return {\n    ...actual,\n    useParams: () => ({\n      instanceId: mockInstanceId,\n      indexName: 'test-index',\n    }),\n    useHistory: () => ({\n      push: mockPush,\n    }),\n  }\n})\n\njest.mock('uiSrc/slices/browser/redisearch', () => ({\n  ...jest.requireActual('uiSrc/slices/browser/redisearch'),\n  redisearchListSelector: jest.fn().mockReturnValue({\n    data: [],\n    loading: false,\n    error: '',\n  }),\n  fetchRedisearchListAction: jest\n    .fn()\n    .mockReturnValue({ type: 'FETCH_REDISEARCH_LIST' }),\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [],\n  }),\n}))\n\nconst mockGetCommandsHistory = jest.fn()\nconst mockAddCommandsToHistory = jest.fn()\nconst mockDeleteCommandFromHistory = jest.fn()\nconst mockClearCommandsHistory = jest.fn()\nconst mockGetCommandHistory = jest.fn()\n\njest.mock('uiSrc/services/commands-history/commandsHistoryService', () => ({\n  CommandsHistoryService: jest.fn().mockImplementation(() => ({\n    getCommandsHistory: mockGetCommandsHistory,\n    getCommandHistory: mockGetCommandHistory,\n    addCommandsToHistory: mockAddCommandsToHistory,\n    deleteCommandFromHistory: mockDeleteCommandFromHistory,\n    clearCommandsHistory: mockClearCommandsHistory,\n  })),\n}))\n\nconst mockHistoryItems = commandExecutionUIFactory.buildList(2)\n\nconst renderComponent = async () => {\n  let result: ReturnType<typeof render>\n\n  await act(async () => {\n    result = render(<VectorSearchQueryPage />)\n  })\n\n  return result!\n}\n\ndescribe('VectorSearchQueryPage', () => {\n  beforeEach(() => {\n    cleanup()\n    jest.clearAllMocks()\n    mockGetCommandsHistory.mockResolvedValue([])\n    mockAddCommandsToHistory.mockResolvedValue([])\n    mockDeleteCommandFromHistory.mockResolvedValue(undefined)\n    mockClearCommandsHistory.mockResolvedValue(undefined)\n    mockGetCommandHistory.mockResolvedValue(null)\n  })\n\n  it('should render page with header, editor, and query results', async () => {\n    await renderComponent()\n\n    const page = screen.getByTestId('vector-search-query-page')\n    const viewIndexBtn = screen.getByTestId('view-index-btn')\n    const editor = screen.getByTestId('vector-search-query-editor')\n    const results = screen.getByTestId('query-results')\n\n    expect(page).toBeInTheDocument()\n    expect(viewIndexBtn).toBeInTheDocument()\n    expect(editor).toBeInTheDocument()\n    expect(results).toBeInTheDocument()\n  })\n\n  it('should render no results placeholder when there are no items', async () => {\n    mockGetCommandsHistory.mockResolvedValueOnce([])\n\n    await renderComponent()\n\n    const placeholder = screen.getByTestId('no-search-results')\n    expect(placeholder).toBeInTheDocument()\n  })\n\n  it('should render query cards and hide placeholder when history items exist', async () => {\n    mockGetCommandsHistory.mockResolvedValueOnce(mockHistoryItems)\n\n    await renderComponent()\n\n    // should render query cards when history items exist\n    const card1 = screen.getByTestId(\n      `query-card-container-${mockHistoryItems[0].id}`,\n    )\n    const card2 = screen.getByTestId(\n      `query-card-container-${mockHistoryItems[1].id}`,\n    )\n\n    expect(card1).toBeInTheDocument()\n    expect(card2).toBeInTheDocument()\n\n    // should hide no results placeholder when items exist\n    const placeholder = screen.queryByTestId('no-search-results')\n    expect(placeholder).not.toBeInTheDocument()\n\n    // should render clear results button when items exist\n    const clearBtn = screen.getByTestId('clear-history-btn')\n    expect(clearBtn).toBeInTheDocument()\n  })\n\n  describe('Telemetry', () => {\n    it('should send telemetry on clear all results', async () => {\n      mockGetCommandsHistory.mockResolvedValueOnce(mockHistoryItems)\n      mockClearCommandsHistory.mockResolvedValueOnce(undefined)\n\n      await renderComponent()\n\n      const clearResultsButton = screen.getByTestId('clear-history-btn')\n\n      await act(async () => {\n        fireEvent.click(clearResultsButton)\n      })\n\n      expect(sendEventTelemetry).toHaveBeenCalledWith({\n        event: TelemetryEvent.SEARCH_CLEAR_ALL_RESULTS_CLICKED,\n        eventData: { databaseId: mockInstanceId },\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/VectorSearchQueryPage.styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const PageContainer = styled(FlexGroup)`\n  flex-direction: column;\n  height: 100%;\n  width: 100%;\n  padding: ${({ theme }) =>\n    `${theme.core.space.space100} ${theme.core.space.space200}`};\n  gap: ${({ theme }) => theme.core.space.space100};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/VectorSearchQueryPage.tsx",
    "content": "import React, { useCallback, useMemo, useState } from 'react'\nimport { useHistory, useParams } from 'react-router-dom'\n\nimport { RiSelectOption } from 'uiSrc/components/base/forms/select/RiSelect'\nimport { Pages } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport {\n  getIndexDisplayName,\n  resolveIndexName,\n  encodeIndexNameForUrl,\n  decodeIndexNameFromUrl,\n} from '../../utils'\nimport { useRedisearchListData } from '../../hooks'\nimport { VectorSearchQueryPageParams } from './VectorSearchQueryPage.types'\nimport { PageHeader, PageContent } from './components'\n\nimport * as S from './VectorSearchQueryPage.styles'\n\nexport const VectorSearchQueryPage = () => {\n  const { instanceId, indexName } = useParams<VectorSearchQueryPageParams>()\n  const history = useHistory()\n\n  const [isIndexPanelOpen, setIsIndexPanelOpen] = useState(false)\n\n  const { stringData: indexes } = useRedisearchListData()\n\n  const indexOptions: RiSelectOption[] = useMemo(\n    () =>\n      indexes.map((name) => {\n        const displayName = getIndexDisplayName(name)\n        return { value: displayName, label: displayName }\n      }),\n    [indexes],\n  )\n\n  const handleIndexChange = useCallback(\n    (value: string) => {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_CHANGED,\n        eventData: { databaseId: instanceId },\n      })\n      history.push(\n        Pages.vectorSearchQuery(\n          instanceId,\n          encodeIndexNameForUrl(resolveIndexName(value)),\n        ),\n      )\n    },\n    [instanceId, history],\n  )\n\n  const toggleIndexPanel = useCallback(() => {\n    if (!isIndexPanelOpen) {\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_INDEX_DETAILS_VIEWED,\n        eventData: { databaseId: instanceId },\n      })\n    }\n    setIsIndexPanelOpen((prev) => !prev)\n  }, [instanceId, isIndexPanelOpen])\n\n  const closeIndexPanel = useCallback(() => {\n    setIsIndexPanelOpen(false)\n  }, [])\n\n  return (\n    <S.PageContainer data-testid=\"vector-search-query-page\">\n      <PageHeader\n        indexName={getIndexDisplayName(decodeIndexNameFromUrl(indexName))}\n        indexOptions={indexOptions}\n        onIndexChange={handleIndexChange}\n        onToggleIndexPanel={toggleIndexPanel}\n      />\n\n      <PageContent\n        isIndexPanelOpen={isIndexPanelOpen}\n        onCloseIndexPanel={closeIndexPanel}\n      />\n    </S.PageContainer>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/VectorSearchQueryPage.types.ts",
    "content": "export type VectorSearchQueryPageParams = {\n  instanceId: string\n  indexName: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/header-title/HeaderTitle.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { faker } from '@faker-js/faker'\n\nimport { HeaderTitle, HeaderTitleProps } from './HeaderTitle'\n\ndescribe('HeaderTitle', () => {\n  const defaultProps: HeaderTitleProps = {\n    indexName: faker.string.alpha(10),\n    indexOptions: [\n      { value: 'index-1', label: 'index-1' },\n      { value: 'index-2', label: 'index-2' },\n    ],\n    onIndexChange: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<HeaderTitleProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<HeaderTitle {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render breadcrumb with labels and index select', () => {\n    renderComponent()\n\n    const breadcrumb = screen.getByTestId('breadcrumb-search-indexes')\n    const link = screen.getByTestId('breadcrumb-search-indexes-link')\n    const searchLabel = screen.getByText('Indexes')\n    const trigger = screen.getByTestId('index-select-trigger')\n\n    expect(breadcrumb).toBeInTheDocument()\n    expect(link).toBeInTheDocument()\n    expect(searchLabel).toBeInTheDocument()\n    expect(trigger).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/header-title/HeaderTitle.styles.ts",
    "content": "import styled from 'styled-components'\n\nexport const BreadcrumbLink = styled.button`\n  display: inline-flex;\n  align-items: center;\n  gap: ${({ theme }) => theme.core.space.space050};\n  background: none;\n  border: none;\n  padding: 0;\n  cursor: pointer;\n  outline: none;\n\n  &:hover {\n    text-decoration: underline;\n  }\n`\n\nexport const SlashSeparator = styled.span`\n  color: ${({ theme }) => theme.semantic.color.text.neutral500};\n  font-size: ${({ theme }) => theme.core.font.fontSize200};\n  font-weight: 400;\n  line-height: 1;\n`\n\nexport const IndexSelectTrigger = styled.button`\n  display: inline-flex;\n  align-items: center;\n  gap: ${({ theme }) => theme.core.space.space050};\n  background: none;\n  border: none;\n  padding: 0;\n  cursor: pointer;\n  color: ${({ theme }) => theme.semantic.color.text.primary500};\n  font-weight: 700;\n\n  &:hover {\n    text-decoration: underline;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/header-title/HeaderTitle.tsx",
    "content": "import React from 'react'\nimport { Breadcrumbs } from 'uiSrc/components/base/navigation/breadcrumbs'\nimport { useHistory, useParams } from 'react-router-dom'\n\nimport { Title } from 'uiSrc/components/base/text'\nimport { Pages } from 'uiSrc/constants'\nimport { RiIcon } from 'uiSrc/components/base/icons'\nimport {\n  RiSelect,\n  RiSelectOption,\n} from 'uiSrc/components/base/forms/select/RiSelect'\n\nimport * as S from './HeaderTitle.styles'\n\nexport interface HeaderTitleProps {\n  indexName: string\n  indexOptions: RiSelectOption[]\n  onIndexChange: (value: string) => void\n}\n\nexport const HeaderTitle = ({\n  indexName,\n  indexOptions,\n  onIndexChange,\n}: HeaderTitleProps) => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const history = useHistory()\n\n  const handleNavigateToList = () => {\n    history.push(Pages.vectorSearch(instanceId))\n  }\n\n  return (\n    <Breadcrumbs.Compose\n      aria-label=\"Breadcrumb\"\n      data-testid=\"breadcrumb-search-indexes\"\n    >\n      <Breadcrumbs.List>\n        <Breadcrumbs.Item>\n          <S.BreadcrumbLink\n            as=\"button\"\n            onClick={handleNavigateToList}\n            data-testid=\"breadcrumb-search-indexes-link\"\n          >\n            <RiIcon type=\"ChevronLeftIcon\" size=\"S\" />\n            <Title size=\"M\" color=\"primary\">\n              Indexes\n            </Title>\n          </S.BreadcrumbLink>\n        </Breadcrumbs.Item>\n        <Breadcrumbs.Item>\n          <Breadcrumbs.Separator>\n            <S.SlashSeparator>/</S.SlashSeparator>\n          </Breadcrumbs.Separator>\n        </Breadcrumbs.Item>\n\n        <Breadcrumbs.Item>\n          <RiSelect.Compose\n            options={indexOptions}\n            value={indexName}\n            onChange={onIndexChange}\n          >\n            <RiSelect.Trigger.Compose customContainer>\n              <S.IndexSelectTrigger\n                as=\"button\"\n                data-testid=\"index-select-trigger\"\n              >\n                <Title size=\"M\" color=\"primary\">\n                  <RiSelect.Trigger.Value />\n                </Title>\n                <RiSelect.Trigger.Arrow />\n              </S.IndexSelectTrigger>\n            </RiSelect.Trigger.Compose>\n            <RiSelect.Content\n              searchable\n              data-testid=\"breadcrumb-index-select\"\n            />\n          </RiSelect.Compose>\n        </Breadcrumbs.Item>\n      </Breadcrumbs.List>\n    </Breadcrumbs.Compose>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/header-title/index.ts",
    "content": "export { HeaderTitle } from './HeaderTitle'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/index.ts",
    "content": "export { PageHeader } from './page-header'\nexport { PageContent } from './page-content'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-content/PageContent.spec.tsx",
    "content": "import React from 'react'\nimport { act, render, screen } from 'uiSrc/utils/test-utils'\n\nimport { PageContent } from './PageContent'\nimport { PageContentProps } from './PageContent.types'\n\njest.mock('../../../../hooks/useIndexInfo/useIndexInfo', () => ({\n  useIndexInfo: jest.fn().mockReturnValue({\n    indexInfo: null,\n    loading: false,\n    error: null,\n    refetch: jest.fn(),\n  }),\n}))\n\njest.mock('uiSrc/services/commands-history/commandsHistoryService', () => ({\n  CommandsHistoryService: jest.fn().mockImplementation(() => ({\n    getCommandsHistory: jest.fn().mockResolvedValue([]),\n    getCommandHistory: jest.fn().mockResolvedValue(null),\n    addCommandsToHistory: jest.fn().mockResolvedValue([]),\n    deleteCommandFromHistory: jest.fn().mockResolvedValue(undefined),\n    clearCommandsHistory: jest.fn().mockResolvedValue(undefined),\n  })),\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [],\n  }),\n}))\n\ndescribe('PageContent', () => {\n  const defaultProps: PageContentProps = {\n    isIndexPanelOpen: false,\n    onCloseIndexPanel: jest.fn(),\n  }\n\n  const renderComponent = async (propsOverride?: Partial<PageContentProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n\n    let result: ReturnType<typeof render>\n    await act(async () => {\n      result = render(<PageContent {...props} />)\n    })\n    return result!\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render editor and query results', async () => {\n    await renderComponent()\n\n    const editor = screen.getByTestId('vector-search-query-editor')\n    const results = screen.getByTestId('query-results')\n\n    expect(editor).toBeInTheDocument()\n    expect(results).toBeInTheDocument()\n  })\n\n  it('should not render index panel when closed', async () => {\n    await renderComponent({ isIndexPanelOpen: false })\n\n    const panel = screen.queryByTestId('view-index-panel')\n    expect(panel).not.toBeInTheDocument()\n  })\n\n  it('should render index panel when open', async () => {\n    await renderComponent({ isIndexPanelOpen: true })\n\n    const panel = screen.getByTestId('view-index-panel')\n    expect(panel).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-content/PageContent.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const ContentArea = styled(Row)`\n  min-height: 0;\n`\n\nexport const EditorResultsArea = styled(Col)`\n  flex: 1;\n  min-width: 0;\n  min-height: 0;\n  height: 100%;\n`\n\nexport const NoResultsWrapper = styled(Col)`\n  height: 100%;\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-content/PageContent.tsx",
    "content": "import React, { useCallback } from 'react'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  ResizableContainer,\n  ResizablePanel,\n  ResizablePanelHandle,\n} from 'uiSrc/components/base/layout'\nimport { QueryResultsProvider } from 'uiSrc/components/query/context/query-results.context'\nimport { QueryResults } from 'uiSrc/components/query/query-results'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport {\n  getSearchCommandType,\n  searchResultsTelemetry,\n} from '../../../../utils/telemetry.utils'\nimport { useQuery } from '../../hooks/useQuery'\nimport { QueryEditorWrapper } from '../../../../components/query-editor'\nimport { NoSearchResults } from '../../../../components/no-search-results'\nimport { IndexInfoSidePanel } from '../../../../components/index-info-side-panel'\n\nimport { PageContentProps } from './PageContent.types'\nimport * as S from './PageContent.styles'\n\nexport const PageContent = ({\n  isIndexPanelOpen,\n  onCloseIndexPanel,\n}: PageContentProps) => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const {\n    query,\n    setQuery,\n    items,\n    clearing,\n    processing,\n    isResultsLoaded,\n    activeMode,\n    resultsMode,\n    scrollDivRef,\n    onSubmit,\n    onToggleOpen,\n    onQueryDelete,\n    onAllQueriesDelete,\n    onQueryReRun,\n    onQueryProfile,\n  } = useQuery()\n\n  const handleSubmit = useCallback(\n    (value?: string) => {\n      const command = value ?? query\n      sendEventTelemetry({\n        event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED,\n        eventData: {\n          databaseId: instanceId,\n          command_type: getSearchCommandType(command),\n          commands: [command],\n        },\n      })\n      onSubmit(value)\n    },\n    [instanceId, query, onSubmit],\n  )\n\n  const handleAllQueriesDelete = useCallback(() => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CLEAR_ALL_RESULTS_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    onAllQueriesDelete()\n  }, [instanceId, onAllQueriesDelete])\n\n  return (\n    <S.ContentArea>\n      <ResizableContainer direction=\"horizontal\">\n        <ResizablePanel\n          id=\"editor-results-panel\"\n          order={1}\n          minSize={30}\n          defaultSize={isIndexPanelOpen ? 70 : 100}\n        >\n          <S.EditorResultsArea>\n            <ResizableContainer direction=\"vertical\">\n              <ResizablePanel\n                id=\"query-editor-panel\"\n                minSize={10}\n                defaultSize={50}\n              >\n                <QueryEditorWrapper\n                  query={query}\n                  setQuery={setQuery}\n                  onSubmit={handleSubmit}\n                  isLoading={processing}\n                />\n              </ResizablePanel>\n\n              <ResizablePanelHandle\n                direction=\"horizontal\"\n                data-test-subj=\"resize-btn-scripting-area-and-results\"\n              />\n\n              <ResizablePanel\n                id=\"query-results-panel\"\n                minSize={10}\n                maxSize={90}\n                defaultSize={50}\n              >\n                <QueryResultsProvider telemetry={searchResultsTelemetry}>\n                  <QueryResults\n                    items={items}\n                    clearing={clearing}\n                    processing={processing}\n                    isResultsLoaded={isResultsLoaded}\n                    activeMode={activeMode}\n                    activeResultsMode={resultsMode}\n                    scrollDivRef={scrollDivRef}\n                    onToggleOpen={onToggleOpen}\n                    onQueryReRun={onQueryReRun}\n                    onQueryProfile={onQueryProfile}\n                    onQueryDelete={onQueryDelete}\n                    onAllQueriesDelete={handleAllQueriesDelete}\n                    noResultsPlaceholder={\n                      <S.NoResultsWrapper align=\"center\" justify=\"center\">\n                        <NoSearchResults />\n                      </S.NoResultsWrapper>\n                    }\n                  />\n                </QueryResultsProvider>\n              </ResizablePanel>\n            </ResizableContainer>\n          </S.EditorResultsArea>\n        </ResizablePanel>\n\n        {isIndexPanelOpen && (\n          <>\n            <ResizablePanelHandle\n              direction=\"vertical\"\n              data-test-subj=\"resize-btn-index-panel\"\n            />\n\n            <ResizablePanel\n              id=\"index-info-panel\"\n              order={2}\n              minSize={15}\n              defaultSize={30}\n            >\n              <IndexInfoSidePanel onClose={onCloseIndexPanel} />\n            </ResizablePanel>\n          </>\n        )}\n      </ResizableContainer>\n    </S.ContentArea>\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-content/PageContent.types.ts",
    "content": "export interface PageContentProps {\n  isIndexPanelOpen: boolean\n  onCloseIndexPanel: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-content/index.ts",
    "content": "export { PageContent } from './PageContent'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-header/PageHeader.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { faker } from '@faker-js/faker'\n\nimport { PageHeader } from './PageHeader'\nimport { PageHeaderProps } from './PageHeader.types'\n\ndescribe('PageHeader', () => {\n  const defaultProps: PageHeaderProps = {\n    indexName: faker.string.alpha(10),\n    indexOptions: [\n      { value: 'index-1', label: 'index-1' },\n      { value: 'index-2', label: 'index-2' },\n    ],\n    onIndexChange: jest.fn(),\n    onToggleIndexPanel: jest.fn(),\n  }\n\n  const renderComponent = (propsOverride?: Partial<PageHeaderProps>) => {\n    const props = { ...defaultProps, ...propsOverride }\n    return render(<PageHeader {...props} />)\n  }\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should render header with breadcrumb and view index button', () => {\n    renderComponent()\n\n    const breadcrumb = screen.getByTestId('breadcrumb-search-indexes')\n    const viewIndexBtn = screen.getByTestId('view-index-btn')\n\n    expect(breadcrumb).toBeInTheDocument()\n    expect(viewIndexBtn).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-header/PageHeader.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\n\nexport const HeaderRow = styled(Row).attrs({\n  align: 'center',\n  justify: 'between',\n  grow: false,\n})`\n  width: 100%;\n  flex-shrink: 0;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-header/PageHeader.tsx",
    "content": "import React from 'react'\n\nimport { HeaderTitle } from '../header-title'\nimport { ViewIndexButton } from '../view-index-button'\n\nimport { PageHeaderProps } from './PageHeader.types'\nimport * as S from './PageHeader.styles'\n\nexport const PageHeader = ({\n  indexName,\n  indexOptions,\n  onIndexChange,\n  onToggleIndexPanel,\n}: PageHeaderProps) => (\n  <S.HeaderRow>\n    <HeaderTitle\n      indexName={indexName}\n      indexOptions={indexOptions}\n      onIndexChange={onIndexChange}\n    />\n    <ViewIndexButton onClick={onToggleIndexPanel} />\n  </S.HeaderRow>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-header/PageHeader.types.ts",
    "content": "import { RiSelectOption } from 'uiSrc/components/base/forms/select/RiSelect'\n\nexport interface PageHeaderProps {\n  indexName: string\n  indexOptions: RiSelectOption[]\n  onIndexChange: (value: string) => void\n  onToggleIndexPanel: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/page-header/index.ts",
    "content": "export { PageHeader } from './PageHeader'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/view-index-button/ViewIndexButton.tsx",
    "content": "import React from 'react'\n\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\n\nexport interface ViewIndexButtonProps {\n  onClick: () => void\n}\n\nexport const ViewIndexButton = ({ onClick }: ViewIndexButtonProps) => (\n  <EmptyButton size=\"small\" onClick={onClick} data-testid=\"view-index-btn\">\n    View index\n  </EmptyButton>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/components/view-index-button/index.ts",
    "content": "export { ViewIndexButton } from './ViewIndexButton'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/hooks/useQuery.spec.ts",
    "content": "import type { RenderHookResult } from '@testing-library/react'\nimport { act, renderHook, waitFor } from 'uiSrc/utils/test-utils'\n\nimport * as hookUtils from './useQuery.utils'\nimport * as sharedUtils from 'uiSrc/utils'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { commandExecutionUIFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\n\nimport { useQuery } from './useQuery'\n\ntype UseQueryReturn = ReturnType<typeof useQuery>\ntype UseQueryHookResult = RenderHookResult<UseQueryReturn, unknown>\n\njest.mock('./useQuery.utils', () => ({\n  sortCommandsByDate: jest.fn((items) => items),\n  prepareNewItems: jest.fn(),\n  generateCommandId: jest.fn(() => 'cmd-123'),\n  createErrorResult: jest.fn((msg: string) => ({ error: msg })),\n  scrollToElement: jest.fn(),\n  limitHistoryLength: jest.fn((items) => items),\n  createGroupItem: jest.fn((count: number, id: string) => ({\n    id,\n    result: `group-${count}`,\n    loading: false,\n    isOpen: false,\n    error: '',\n  })),\n}))\n\nconst mockGetCommandsHistory = jest.fn()\nconst mockGetCommandHistory = jest.fn()\nconst mockAddCommandsToHistory = jest.fn()\nconst mockDeleteCommandFromHistory = jest.fn()\nconst mockClearCommandsHistory = jest.fn()\n\njest.mock('uiSrc/services/commands-history/commandsHistoryService', () => ({\n  CommandsHistoryService: jest.fn().mockImplementation(() => ({\n    getCommandsHistory: mockGetCommandsHistory,\n    getCommandHistory: mockGetCommandHistory,\n    addCommandsToHistory: mockAddCommandsToHistory,\n    deleteCommandFromHistory: mockDeleteCommandFromHistory,\n    clearCommandsHistory: mockClearCommandsHistory,\n  })),\n}))\n\njest.mock('uiSrc/utils', () => ({\n  ...jest.requireActual('uiSrc/utils'),\n  getCommandsForExecution: jest.fn(),\n  getExecuteParams: jest.fn((_, current) => ({ ...current })),\n  isGroupResults: jest.fn(() => false),\n  isSilentMode: jest.fn(() => false),\n}))\n\nconst mockedHookUtils = jest.mocked(hookUtils)\nconst mockedSharedUtils = jest.mocked(sharedUtils)\n\ndescribe('useQuery hook', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n    mockGetCommandsHistory.mockResolvedValue([])\n    mockGetCommandHistory.mockResolvedValue(null)\n    mockAddCommandsToHistory.mockResolvedValue([])\n    mockDeleteCommandFromHistory.mockResolvedValue(undefined)\n    mockClearCommandsHistory.mockResolvedValue(undefined)\n  })\n\n  it('should initialize with default state', async () => {\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n    expect(result.current.query).toBe('')\n    expect(result.current.items).toEqual([])\n    expect(result.current.clearing).toBe(false)\n    expect(result.current.processing).toBe(false)\n  })\n\n  it('should load history on mount', async () => {\n    const historyItems = [commandExecutionUIFactory.build()]\n    mockGetCommandsHistory.mockResolvedValue(historyItems)\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n    expect(result.current.items).toEqual(historyItems)\n    expect(mockGetCommandsHistory).toHaveBeenCalledWith('instanceId')\n  })\n\n  it('should set isLoaded on history load error', async () => {\n    mockGetCommandsHistory.mockRejectedValueOnce(new Error('error'))\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n    expect(result.current.items).toEqual([])\n  })\n\n  it('should submit a command and update items on success', async () => {\n    const apiData = [{ id: 'cmd-1230', result: 'PONG' }] as any\n\n    mockGetCommandsHistory.mockResolvedValue([])\n    mockAddCommandsToHistory.mockResolvedValue(apiData)\n\n    mockedSharedUtils.getCommandsForExecution.mockReturnValueOnce(['PING'])\n    mockedHookUtils.prepareNewItems.mockImplementationOnce(\n      (cmds: string[], id: string) =>\n        cmds.map((_, i) => ({\n          id: `${id}${i}`,\n          loading: true,\n          isOpen: false,\n          error: '',\n        })),\n    )\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n    await act(async () => {\n      await result.current.onSubmit('PING')\n    })\n\n    expect(result.current.items[0]).toMatchObject({\n      id: 'cmd-1230',\n      result: 'PONG',\n      loading: false,\n      error: '',\n      isOpen: true,\n    })\n    expect(mockedHookUtils.scrollToElement).toHaveBeenCalled()\n    expect(result.current.processing).toBe(false)\n  })\n\n  it('should handle API error on submit', async () => {\n    mockGetCommandsHistory.mockResolvedValue([])\n    mockAddCommandsToHistory.mockRejectedValue(new Error('api failed'))\n    mockedSharedUtils.getCommandsForExecution.mockReturnValueOnce(['PING'])\n    mockedHookUtils.prepareNewItems.mockImplementationOnce(\n      (cmds: string[], id: string) =>\n        cmds.map((_, i) => ({\n          id: `${id}${i}`,\n          loading: true,\n          isOpen: false,\n          error: '',\n        })),\n    )\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n    await act(async () => {\n      await result.current.onSubmit('PING')\n    })\n\n    expect(result.current.items[0]).toMatchObject({\n      loading: false,\n      isOpen: true,\n      error: 'api failed',\n      result: { error: 'api failed' },\n    })\n    expect(result.current.processing).toBe(false)\n  })\n\n  it('should not submit empty command', async () => {\n    mockGetCommandsHistory.mockResolvedValue([])\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n    await act(async () => {\n      await result.current.onSubmit('')\n    })\n\n    expect(mockAddCommandsToHistory).not.toHaveBeenCalled()\n  })\n\n  it('should delete a query', async () => {\n    const historyItems = [commandExecutionUIFactory.build({ id: 'to-delete' })]\n    mockGetCommandsHistory.mockResolvedValue(historyItems)\n    mockDeleteCommandFromHistory.mockResolvedValue(undefined)\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n    await act(async () => {\n      await result.current.onQueryDelete('to-delete')\n    })\n\n    expect(mockDeleteCommandFromHistory).toHaveBeenCalledWith(\n      'instanceId',\n      'to-delete',\n    )\n    expect(result.current.items).toEqual([])\n  })\n\n  it('should clear all queries', async () => {\n    const historyItems = commandExecutionUIFactory.buildList(3)\n    mockGetCommandsHistory.mockResolvedValue(historyItems)\n    mockClearCommandsHistory.mockResolvedValue(undefined)\n\n    const { result } = renderHook(() =>\n      useQuery(),\n    ) as unknown as UseQueryHookResult\n    await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n    await act(async () => {\n      await result.current.onAllQueriesDelete()\n    })\n\n    expect(mockClearCommandsHistory).toHaveBeenCalledWith('instanceId')\n    expect(result.current.items).toEqual([])\n    expect(result.current.clearing).toBe(false)\n  })\n\n  describe('onToggleOpen', () => {\n    it('should toggle item closed when isOpen is false', async () => {\n      const historyItems = [\n        commandExecutionUIFactory.build({\n          id: 'item-1',\n          isOpen: true,\n          result: [\n            { response: 'data', status: CommandExecutionStatus.Success },\n          ],\n        }),\n      ]\n      mockGetCommandsHistory.mockResolvedValue(historyItems)\n\n      const { result } = renderHook(() =>\n        useQuery(),\n      ) as unknown as UseQueryHookResult\n      await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n      await act(async () => {\n        await result.current.onToggleOpen('item-1', false)\n      })\n\n      expect(result.current.items[0]).toMatchObject({\n        id: 'item-1',\n        isOpen: false,\n      })\n      expect(mockGetCommandHistory).not.toHaveBeenCalled()\n    })\n\n    it('should fetch and open item when opening and result is missing', async () => {\n      const historyItems = [\n        commandExecutionUIFactory.build({\n          id: 'item-2',\n          isOpen: false,\n          result: undefined,\n        }),\n      ]\n      mockGetCommandsHistory.mockResolvedValue(historyItems)\n      mockGetCommandHistory.mockResolvedValueOnce({\n        id: 'item-2',\n        result: 'fetched-data',\n        error: '',\n        loading: false,\n        isOpen: false,\n      })\n\n      const { result } = renderHook(() =>\n        useQuery(),\n      ) as unknown as UseQueryHookResult\n      await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n      await act(async () => {\n        await result.current.onToggleOpen('item-2', true)\n      })\n\n      expect(mockGetCommandHistory).toHaveBeenCalledWith('instanceId', 'item-2')\n      expect(result.current.items[0]).toMatchObject({\n        id: 'item-2',\n        loading: false,\n        isOpen: true,\n        result: 'fetched-data',\n      })\n    })\n\n    it('should open item directly when result already exists', async () => {\n      const historyItems = [\n        commandExecutionUIFactory.build({\n          id: 'item-3',\n          isOpen: false,\n          result: [\n            { response: 'existing', status: CommandExecutionStatus.Success },\n          ],\n        }),\n      ]\n      mockGetCommandsHistory.mockResolvedValue(historyItems)\n\n      const { result } = renderHook(() =>\n        useQuery(),\n      ) as unknown as UseQueryHookResult\n      await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n      await act(async () => {\n        await result.current.onToggleOpen('item-3', true)\n      })\n\n      expect(mockGetCommandHistory).not.toHaveBeenCalled()\n      expect(result.current.items[0]).toMatchObject({\n        id: 'item-3',\n        isOpen: true,\n      })\n    })\n\n    it('should set error when fetch fails on toggle open', async () => {\n      const historyItems = [\n        commandExecutionUIFactory.build({\n          id: 'item-4',\n          isOpen: false,\n          result: undefined,\n        }),\n      ]\n      mockGetCommandsHistory.mockResolvedValue(historyItems)\n      mockGetCommandHistory.mockRejectedValueOnce(new Error('load failed'))\n\n      const { result } = renderHook(() =>\n        useQuery(),\n      ) as unknown as UseQueryHookResult\n      await waitFor(() => expect(result.current.isResultsLoaded).toBe(true))\n\n      await act(async () => {\n        await result.current.onToggleOpen('item-4', true)\n      })\n\n      expect(result.current.items[0]).toMatchObject({\n        id: 'item-4',\n        loading: false,\n        error: 'Failed to load command details',\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/hooks/useQuery.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react'\nimport { useParams } from 'react-router-dom'\nimport { chunk } from 'lodash'\nimport {\n  Nullable,\n  getCommandsForExecution,\n  getExecuteParams,\n  isGroupResults,\n  isSilentMode,\n} from 'uiSrc/utils'\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport {\n  RunQueryMode,\n  ResultsMode,\n  CommandExecutionUI,\n  CommandExecutionType,\n} from 'uiSrc/slices/interfaces'\nimport { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { CommandsHistoryService } from 'uiSrc/services/commands-history/commandsHistoryService'\nimport {\n  createErrorResult,\n  createGroupItem,\n  generateCommandId,\n  limitHistoryLength,\n  prepareNewItems,\n  scrollToElement,\n  sortCommandsByDate,\n} from './useQuery.utils'\n\nexport const useQuery = () => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const scrollDivRef = useRef<HTMLDivElement>(null)\n\n  const [query, setQuery] = useState('')\n  const [items, setItems] = useState<CommandExecutionUI[]>([])\n  const [clearing, setClearing] = useState(false)\n  const [processing, setProcessing] = useState(false)\n  const [isLoaded, setIsLoaded] = useState(false)\n\n  const commandsHistoryService = useRef(\n    new CommandsHistoryService(CommandExecutionType.Search),\n  ).current\n\n  const [activeRunQueryMode] = useState(RunQueryMode.ASCII)\n  const [resultsMode] = useState(ResultsMode.Default)\n\n  useEffect(() => {\n    const loadHistory = async () => {\n      try {\n        const historyData =\n          await commandsHistoryService.getCommandsHistory(instanceId)\n        setItems(historyData)\n      } catch {\n        // Silently handle error — history may be unavailable\n      } finally {\n        setIsLoaded(true)\n      }\n    }\n\n    loadHistory()\n  }, [instanceId])\n\n  const handleApiSuccess = useCallback(\n    (data: CommandExecutionUI[], commandId: string, isNewCommand: boolean) => {\n      setItems((prevItems) => {\n        const updatedItems = prevItems.map((item) => {\n          const result = data.find((_, i) => item.id === commandId + i)\n          if (result) {\n            return {\n              ...result,\n              loading: false,\n              error: '',\n              isOpen: !isSilentMode(resultsMode),\n            }\n          }\n          return item\n        })\n        return sortCommandsByDate(updatedItems)\n      })\n\n      if (isNewCommand) {\n        scrollToElement(scrollDivRef.current, 'start')\n      }\n    },\n    [resultsMode],\n  )\n\n  const handleApiError = useCallback((error: unknown) => {\n    const message =\n      error instanceof Error ? error.message : 'Failed to execute command'\n\n    setItems((prevItems) =>\n      prevItems.map((item) => {\n        if (item.loading) {\n          return {\n            ...item,\n            loading: false,\n            error: message,\n            result: createErrorResult(message),\n            isOpen: true,\n          }\n        }\n        return item\n      }),\n    )\n    setProcessing(false)\n  }, [])\n\n  const executeCommandBatch = useCallback(\n    async (\n      commandInit: string,\n      commandId: Nullable<string> | undefined,\n      executeParams: CodeButtonParams,\n    ) => {\n      const currentExecuteParams = {\n        activeRunQueryMode,\n        resultsMode,\n        batchSize: PIPELINE_COUNT_DEFAULT,\n      }\n\n      const { batchSize } = getExecuteParams(\n        executeParams,\n        currentExecuteParams,\n      )\n      const commandsForExecuting = getCommandsForExecution(commandInit)\n      const chunkSize = isGroupResults(resultsMode)\n        ? commandsForExecuting.length\n        : batchSize > 1\n          ? batchSize\n          : 1\n\n      const [commands, ...restCommands] = chunk(commandsForExecuting, chunkSize)\n\n      if (!commands?.length) {\n        setProcessing(false)\n        return\n      }\n\n      const newCommandId = commandId || generateCommandId()\n      const newItems = prepareNewItems(commands, newCommandId)\n\n      setItems((prevItems) => {\n        const updatedItems = isGroupResults(resultsMode)\n          ? [createGroupItem(newItems.length, newCommandId), ...prevItems]\n          : [...newItems, ...prevItems]\n        return limitHistoryLength(updatedItems)\n      })\n\n      const data = await commandsHistoryService.addCommandsToHistory(\n        instanceId,\n        commands,\n        {\n          activeRunQueryMode,\n          resultsMode,\n        },\n      )\n\n      handleApiSuccess(data, newCommandId, !commandId)\n\n      if (restCommands.length > 0) {\n        const remainingCommands = restCommands\n          .map((cmds) => cmds.join('\\n'))\n          .join('\\n')\n        if (remainingCommands) {\n          await executeCommandBatch(remainingCommands, undefined, executeParams)\n        }\n      } else {\n        setProcessing(false)\n      }\n    },\n    [activeRunQueryMode, resultsMode, instanceId, handleApiSuccess],\n  )\n\n  const onSubmit = useCallback(\n    async (\n      commandInit: string = query,\n      commandId?: Nullable<string>,\n      executeParams: CodeButtonParams = {},\n    ) => {\n      if (!commandInit?.length) return\n\n      setProcessing(true)\n\n      try {\n        await executeCommandBatch(commandInit, commandId, executeParams)\n      } catch (error) {\n        handleApiError(error)\n      }\n    },\n    [query, executeCommandBatch, handleApiError],\n  )\n\n  const handleQueryDelete = useCallback(\n    async (commandId: string) => {\n      try {\n        await commandsHistoryService.deleteCommandFromHistory(\n          instanceId,\n          commandId,\n        )\n        setItems((prevItems) =>\n          prevItems.filter((item) => item.id !== commandId),\n        )\n      } catch {\n        // Silently handle error\n      }\n    },\n    [instanceId],\n  )\n\n  const handleAllQueriesDelete = useCallback(async () => {\n    try {\n      setClearing(true)\n      await commandsHistoryService.clearCommandsHistory(instanceId)\n      setItems([])\n    } catch {\n      // Silently handle error\n    } finally {\n      setClearing(false)\n    }\n  }, [instanceId])\n\n  const handleToggleOpen = useCallback(\n    async (id: string, isOpen: boolean) => {\n      if (isOpen) {\n        const item = items.find((i) => i.id === id)\n        if (item && !item.result) {\n          setItems((prev) =>\n            prev.map((i) => (i.id === id ? { ...i, loading: true } : i)),\n          )\n\n          try {\n            const command = await commandsHistoryService.getCommandHistory(\n              instanceId,\n              id,\n            )\n\n            setItems((prev) =>\n              prev.map((i) => {\n                if (i.id !== id) return i\n                if (command) {\n                  return {\n                    ...i,\n                    ...command,\n                    isOpen: true,\n                    loading: false,\n                    error: '',\n                  }\n                }\n                return { ...i, loading: false }\n              }),\n            )\n            return\n          } catch {\n            setItems((prev) =>\n              prev.map((i) =>\n                i.id === id\n                  ? {\n                      ...i,\n                      loading: false,\n                      error: 'Failed to load command details',\n                    }\n                  : i,\n              ),\n            )\n            return\n          }\n        }\n      }\n\n      setItems((prev) => prev.map((i) => (i.id === id ? { ...i, isOpen } : i)))\n    },\n    [items, instanceId],\n  )\n\n  return {\n    query,\n    setQuery,\n    items,\n    clearing,\n    processing,\n    isResultsLoaded: isLoaded,\n    activeMode: activeRunQueryMode,\n    resultsMode,\n    scrollDivRef,\n    onSubmit,\n    onToggleOpen: handleToggleOpen,\n    onQueryDelete: handleQueryDelete,\n    onAllQueriesDelete: handleAllQueriesDelete,\n    onQueryReRun: onSubmit,\n    onQueryProfile: onSubmit,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/hooks/useQuery.utils.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { WORKBENCH_HISTORY_MAX_LENGTH } from 'uiSrc/pages/workbench/constants'\nimport {\n  sortCommandsByDate,\n  prepareNewItems,\n  createGroupItem,\n  createErrorResult,\n  limitHistoryLength,\n  generateCommandId,\n} from './useQuery.utils'\n\ndescribe('useQuery.utils', () => {\n  describe('sortCommandsByDate', () => {\n    it('should sort commands by date in descending order (newest first)', () => {\n      const older = new Date('2025-01-01')\n      const newer = new Date('2025-06-01')\n\n      const items: CommandExecutionUI[] = [\n        { id: '1', command: 'SET a b', createdAt: older },\n        { id: '2', command: 'GET a', createdAt: newer },\n      ]\n\n      const result = sortCommandsByDate(items)\n      expect(result[0].id).toBe('2')\n      expect(result[1].id).toBe('1')\n    })\n\n    it('should handle items without createdAt', () => {\n      const items: CommandExecutionUI[] = [\n        { id: '1', command: 'SET a b' },\n        { id: '2', command: 'GET a', createdAt: new Date('2025-01-01') },\n      ]\n\n      const result = sortCommandsByDate(items)\n      expect(result[0].id).toBe('2')\n      expect(result[1].id).toBe('1')\n    })\n\n    it('should return empty array for empty input', () => {\n      expect(sortCommandsByDate([])).toEqual([])\n    })\n\n    it('should handle single item', () => {\n      const items: CommandExecutionUI[] = [\n        { id: '1', command: 'SET a b', createdAt: new Date() },\n      ]\n      expect(sortCommandsByDate(items)).toHaveLength(1)\n    })\n  })\n\n  describe('prepareNewItems', () => {\n    it('should create loading items for each command', () => {\n      const commands = ['SET key value', 'GET key']\n      const commandId = faker.string.numeric(10)\n\n      const result = prepareNewItems(commands, commandId)\n\n      expect(result).toHaveLength(2)\n      expect(result[0]).toEqual({\n        command: 'SET key value',\n        id: `${commandId}0`,\n        loading: true,\n        isOpen: true,\n        error: '',\n      })\n      expect(result[1]).toEqual({\n        command: 'GET key',\n        id: `${commandId}1`,\n        loading: true,\n        isOpen: true,\n        error: '',\n      })\n    })\n\n    it('should return empty array for empty commands', () => {\n      expect(prepareNewItems([], 'id')).toEqual([])\n    })\n  })\n\n  describe('createGroupItem', () => {\n    it('should create a group item with correct command text', () => {\n      const commandId = faker.string.numeric(10)\n\n      const result = createGroupItem(5, commandId)\n\n      expect(result).toEqual({\n        command: '5 - Command(s)',\n        id: commandId,\n        loading: true,\n        isOpen: true,\n        error: '',\n      })\n    })\n  })\n\n  describe('createErrorResult', () => {\n    it('should create an error result array', () => {\n      const message = faker.lorem.sentence()\n\n      const result = createErrorResult(message)\n\n      expect(result).toEqual([\n        {\n          response: message,\n          status: CommandExecutionStatus.Fail,\n        },\n      ])\n    })\n  })\n\n  describe('limitHistoryLength', () => {\n    it('should return items unchanged when under max length', () => {\n      const items: CommandExecutionUI[] = Array.from({ length: 5 }, (_, i) => ({\n        id: `${i}`,\n        command: faker.lorem.word(),\n      }))\n\n      expect(limitHistoryLength(items)).toHaveLength(5)\n    })\n\n    it('should trim items to max length when over limit', () => {\n      const items: CommandExecutionUI[] = Array.from(\n        { length: WORKBENCH_HISTORY_MAX_LENGTH + 10 },\n        (_, i) => ({\n          id: `${i}`,\n          command: faker.lorem.word(),\n        }),\n      )\n\n      const result = limitHistoryLength(items)\n      expect(result).toHaveLength(WORKBENCH_HISTORY_MAX_LENGTH)\n    })\n\n    it('should return items unchanged when at exact max length', () => {\n      const items: CommandExecutionUI[] = Array.from(\n        { length: WORKBENCH_HISTORY_MAX_LENGTH },\n        (_, i) => ({\n          id: `${i}`,\n          command: faker.lorem.word(),\n        }),\n      )\n\n      expect(limitHistoryLength(items)).toHaveLength(\n        WORKBENCH_HISTORY_MAX_LENGTH,\n      )\n    })\n\n    it('should handle empty array', () => {\n      expect(limitHistoryLength([])).toEqual([])\n    })\n  })\n\n  describe('generateCommandId', () => {\n    it('should return a string', () => {\n      expect(typeof generateCommandId()).toBe('string')\n    })\n\n    it('should return a timestamp-based id', () => {\n      const before = Date.now()\n      const id = generateCommandId()\n      const after = Date.now()\n\n      const idNum = Number(id)\n      expect(idNum).toBeGreaterThanOrEqual(before)\n      expect(idNum).toBeLessThanOrEqual(after)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/hooks/useQuery.utils.ts",
    "content": "import { scrollIntoView } from 'uiSrc/utils'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { WORKBENCH_HISTORY_MAX_LENGTH } from 'uiSrc/pages/workbench/constants'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\n\nexport const sortCommandsByDate = (\n  commands: CommandExecutionUI[],\n): CommandExecutionUI[] =>\n  commands.sort((a, b) => {\n    const dateA = new Date(a.createdAt || 0).getTime()\n    const dateB = new Date(b.createdAt || 0).getTime()\n    return dateB - dateA\n  })\n\nexport const prepareNewItems = (\n  commands: string[],\n  commandId: string,\n): CommandExecutionUI[] =>\n  commands.map((command, i) => ({\n    command,\n    id: commandId + i,\n    loading: true,\n    isOpen: true,\n    error: '',\n  }))\n\nexport const createGroupItem = (\n  itemCount: number,\n  commandId: string,\n): CommandExecutionUI => ({\n  command: `${itemCount} - Command(s)`,\n  id: commandId,\n  loading: true,\n  isOpen: true,\n  error: '',\n})\n\nexport const createErrorResult = (message: string) => [\n  {\n    response: message,\n    status: CommandExecutionStatus.Fail,\n  },\n]\n\nexport const scrollToElement = (\n  element: HTMLDivElement | null,\n  inline: ScrollLogicalPosition = 'start',\n) => {\n  if (!element) return\n\n  requestAnimationFrame(() => {\n    scrollIntoView(element, {\n      behavior: 'smooth',\n      block: 'nearest',\n      inline,\n    })\n  })\n}\n\nexport const limitHistoryLength = (\n  items: CommandExecutionUI[],\n): CommandExecutionUI[] =>\n  items.length > WORKBENCH_HISTORY_MAX_LENGTH\n    ? items.slice(0, WORKBENCH_HISTORY_MAX_LENGTH)\n    : items\n\nexport const generateCommandId = (): string => `${Date.now()}`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchQueryPage/index.ts",
    "content": "export { VectorSearchQueryPage } from './VectorSearchQueryPage'\nexport { VectorSearchQueryPage as default } from './VectorSearchQueryPage'\nexport type { VectorSearchQueryPageParams } from './VectorSearchQueryPage.types'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchWelcomePage/VectorSearchWelcomePage.tsx",
    "content": "import React, { useCallback } from 'react'\n\nimport { WelcomeScreen } from '../../components/welcome-screen'\nimport { useVectorSearch } from '../../context/vector-search'\nimport { SearchTelemetrySource } from '../../telemetry.constants'\n\n/**\n * Vector Search Welcome page.\n * Connects the WelcomeScreen presentational component to the application\n * context, providing callbacks and configuration.\n */\nexport const VectorSearchWelcomePage = () => {\n  const {\n    openPickSampleDataModal,\n    navigateToExistingDataFlow,\n    hasExistingKeys,\n    hasExistingKeysLoading,\n  } = useVectorSearch()\n\n  const useMyDatabaseDisabled = hasExistingKeysLoading\n    ? { tooltip: 'Checking for existing keys…' }\n    : !hasExistingKeys\n      ? { tooltip: 'No Hash or JSON keys found in your database' }\n      : undefined\n\n  const handleTrySampleData = useCallback(\n    () => openPickSampleDataModal(SearchTelemetrySource.Welcome),\n    [openPickSampleDataModal],\n  )\n\n  const handleUseMyDatabase = useCallback(\n    () => navigateToExistingDataFlow(SearchTelemetrySource.Welcome),\n    [navigateToExistingDataFlow],\n  )\n\n  return (\n    <WelcomeScreen\n      onTrySampleDataClick={handleTrySampleData}\n      onUseMyDatabaseClick={handleUseMyDatabase}\n      useMyDatabaseDisabled={useMyDatabaseDisabled}\n    />\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/VectorSearchWelcomePage/index.ts",
    "content": "export { VectorSearchWelcomePage } from './VectorSearchWelcomePage'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/pages/styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexGroup } from 'uiSrc/components/base/layout/flex'\n\nexport const PageWrapper = styled(FlexGroup)`\n  background-color: ${({ theme }) =>\n    theme.semantic?.color.background.neutral100};\n  padding: ${({ theme }) => theme.core?.space.space100}\n    ${({ theme }) => theme.core?.space.space200};\n  height: 100%;\n  width: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/telemetry.constants.ts",
    "content": "export enum SearchTelemetrySource {\n  Welcome = 'welcome',\n  List = 'list',\n  Browser = 'browser',\n}\n\nexport enum SearchBrowserSource {\n  KeyDetails = 'key_details',\n  TreeView = 'tree_view',\n}\n\nexport enum SearchTelemetryCancelStep {\n  SampleDataModal = 'sample_data_modal',\n  IndexDefinition = 'index_definition',\n}\n\nexport enum SearchTelemetryDemoDataNextStep {\n  IndexDefinition = 'index_definition',\n  StartQuerying = 'start_querying',\n}\n\nexport enum SearchTelemetryFieldEditAction {\n  Add = 'add',\n  Edit = 'edit',\n}\n\nexport enum SearchOnboardingAction {\n  Next = 'next',\n  Back = 'back',\n  Skip = 'skip',\n}\n\nexport enum SearchCommandType {\n  Search = 'search',\n  Aggregate = 'aggregate',\n  Explain = 'explain',\n  Profile = 'profile',\n  Other = 'other',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/generateDynamicFtCreateCommand.spec.ts",
    "content": "import {\n  FieldTypes,\n  RedisearchIndexKeyType,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  IndexField,\n  VectorAlgorithm,\n  VectorDataType,\n  VectorDistanceMetric,\n} from '../components/index-details/IndexDetails.types'\nimport {\n  generateDynamicFtCreateCommand,\n  DynamicFtCreateParams,\n} from './generateDynamicFtCreateCommand'\n\nconst buildParams = (\n  overrides?: Partial<DynamicFtCreateParams>,\n): DynamicFtCreateParams => ({\n  indexName: 'idx:test',\n  keyType: RedisearchIndexKeyType.HASH,\n  prefix: 'test:',\n  fields: [],\n  ...overrides,\n})\n\ndescribe('generateDynamicFtCreateCommand', () => {\n  describe('basic structure', () => {\n    it('should generate command with HASH key type', () => {\n      const result = generateDynamicFtCreateCommand(\n        buildParams({\n          indexName: 'idx:myindex',\n          keyType: RedisearchIndexKeyType.HASH,\n          prefix: 'myprefix:',\n        }),\n      )\n\n      expect(result).toContain('FT.CREATE \"idx:myindex\"')\n      expect(result).toContain('ON HASH')\n      expect(result).toContain('PREFIX 1 \"myprefix:\"')\n      expect(result).toContain('SCHEMA')\n    })\n\n    it('should generate command with JSON key type', () => {\n      const result = generateDynamicFtCreateCommand(\n        buildParams({\n          keyType: RedisearchIndexKeyType.JSON,\n          prefix: 'doc:',\n        }),\n      )\n\n      expect(result).toContain('ON JSON')\n      expect(result).toContain('PREFIX 1 \"doc:\"')\n    })\n\n    it('should generate command with empty fields', () => {\n      const result = generateDynamicFtCreateCommand(buildParams())\n\n      expect(result).toContain('FT.CREATE \"idx:test\"')\n      expect(result).toContain('SCHEMA')\n    })\n\n    it('should generate command with empty index name', () => {\n      const result = generateDynamicFtCreateCommand(\n        buildParams({ indexName: '' }),\n      )\n\n      expect(result).toContain('FT.CREATE \"\"')\n      expect(result).toContain('ON HASH')\n    })\n  })\n\n  describe('simple field types', () => {\n    it.each([\n      { type: FieldTypes.NUMERIC, expected: 'NUMERIC' },\n      { type: FieldTypes.GEO, expected: 'GEO' },\n    ])('should generate $expected field for HASH key', ({ type, expected }) => {\n      const fields: IndexField[] = [\n        { id: 'f1', name: 'myfield', value: 'val', type },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain(`\"myfield\" ${expected}`)\n    })\n\n    it('should generate TAG field for HASH key', () => {\n      const fields: IndexField[] = [\n        { id: 'f1', name: 'myfield', value: 'val', type: FieldTypes.TAG },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"myfield\" TAG')\n    })\n\n    it.each([\n      { type: FieldTypes.NUMERIC, expected: 'NUMERIC' },\n      { type: FieldTypes.GEO, expected: 'GEO' },\n    ])(\n      'should generate $expected field with JSONPath alias for JSON key',\n      ({ type, expected }) => {\n        const fields: IndexField[] = [\n          { id: 'f1', name: 'myfield', value: 'val', type },\n        ]\n        const result = generateDynamicFtCreateCommand(\n          buildParams({ keyType: RedisearchIndexKeyType.JSON, fields }),\n        )\n\n        expect(result).toContain(`$.myfield AS \"myfield\" ${expected}`)\n      },\n    )\n\n    it('should generate TAG field for JSON key', () => {\n      const fields: IndexField[] = [\n        { id: 'f1', name: 'myfield', value: 'val', type: FieldTypes.TAG },\n      ]\n      const result = generateDynamicFtCreateCommand(\n        buildParams({ keyType: RedisearchIndexKeyType.JSON, fields }),\n      )\n\n      expect(result).toContain('$.myfield AS \"myfield\" TAG')\n    })\n  })\n\n  describe('TEXT fields', () => {\n    it('should generate TEXT field without WEIGHT when using default', () => {\n      const fields: IndexField[] = [\n        { id: 'title', name: 'title', value: 'hello', type: FieldTypes.TEXT },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"title\" TEXT')\n      expect(result).not.toContain('WEIGHT')\n      expect(result).not.toContain('PHONETIC')\n    })\n\n    it('should generate TEXT field with non-default weight', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'title',\n          name: 'title',\n          value: 'hello',\n          type: FieldTypes.TEXT,\n          options: { weight: 2.5 },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"title\" TEXT WEIGHT 2.5')\n    })\n\n    it('should generate TEXT field with phonetic', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'title',\n          name: 'title',\n          value: 'hello',\n          type: FieldTypes.TEXT,\n          options: { phonetic: 'dm:en' },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"title\" TEXT PHONETIC dm:en')\n    })\n\n    it('should omit PHONETIC when set to none', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'title',\n          name: 'title',\n          value: 'hello',\n          type: FieldTypes.TEXT,\n          options: { phonetic: 'none' },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).not.toContain('PHONETIC')\n    })\n\n    it('should generate TEXT field with weight and phonetic', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'title',\n          name: 'title',\n          value: 'hello',\n          type: FieldTypes.TEXT,\n          options: { weight: 3, phonetic: 'dm:fr' },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"title\" TEXT WEIGHT 3 PHONETIC dm:fr')\n    })\n  })\n\n  describe('VECTOR FLAT fields', () => {\n    it('should generate VECTOR FLAT with default options when none provided', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"embedding\" VECTOR FLAT 6')\n      expect(result).toContain('TYPE FLOAT32')\n      expect(result).toContain('DIM 384')\n      expect(result).toContain('DISTANCE_METRIC COSINE')\n    })\n\n    it('should generate VECTOR FLAT with explicit options', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n          options: {\n            algorithm: VectorAlgorithm.FLAT,\n            dataType: VectorDataType.FLOAT64,\n            dimensions: 768,\n            distanceMetric: VectorDistanceMetric.L2,\n          },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"embedding\" VECTOR FLAT 6')\n      expect(result).toContain('TYPE FLOAT64')\n      expect(result).toContain('DIM 768')\n      expect(result).toContain('DISTANCE_METRIC L2')\n    })\n\n    it('should generate VECTOR FLAT for JSON key type', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n          options: {\n            algorithm: VectorAlgorithm.FLAT,\n            dimensions: 8,\n            distanceMetric: VectorDistanceMetric.COSINE,\n          },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(\n        buildParams({ keyType: RedisearchIndexKeyType.JSON, fields }),\n      )\n\n      expect(result).toContain('$.embedding AS \"embedding\" VECTOR FLAT 6')\n      expect(result).toContain('TYPE FLOAT32')\n      expect(result).toContain('DIM 8')\n      expect(result).toContain('DISTANCE_METRIC COSINE')\n    })\n  })\n\n  describe('VECTOR HNSW fields', () => {\n    it('should generate VECTOR HNSW with all options', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n          options: {\n            algorithm: VectorAlgorithm.HNSW,\n            dataType: VectorDataType.FLOAT32,\n            dimensions: 512,\n            distanceMetric: VectorDistanceMetric.IP,\n            maxEdges: 32,\n            maxNeighbors: 400,\n            candidateLimit: 20,\n            epsilon: 0.05,\n          },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"embedding\" VECTOR HNSW 14')\n      expect(result).toContain('TYPE FLOAT32')\n      expect(result).toContain('DIM 512')\n      expect(result).toContain('DISTANCE_METRIC IP')\n      expect(result).toContain('M 32')\n      expect(result).toContain('EF_CONSTRUCTION 400')\n      expect(result).toContain('EF_RUNTIME 20')\n      expect(result).toContain('EPSILON 0.05')\n    })\n\n    it('should generate VECTOR HNSW with only base options', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n          options: {\n            algorithm: VectorAlgorithm.HNSW,\n            dimensions: 256,\n            distanceMetric: VectorDistanceMetric.COSINE,\n          },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"embedding\" VECTOR HNSW 6')\n      expect(result).toContain('TYPE FLOAT32')\n      expect(result).toContain('DIM 256')\n      expect(result).toContain('DISTANCE_METRIC COSINE')\n      expect(result).not.toMatch(/\\bM \\d/)\n      expect(result).not.toContain('EF_CONSTRUCTION')\n      expect(result).not.toContain('EF_RUNTIME')\n      expect(result).not.toContain('EPSILON')\n    })\n\n    it('should generate VECTOR HNSW with partial options', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n          options: {\n            algorithm: VectorAlgorithm.HNSW,\n            dimensions: 128,\n            distanceMetric: VectorDistanceMetric.L2,\n            maxEdges: 16,\n          },\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"embedding\" VECTOR HNSW 8')\n      expect(result).toContain('M 16')\n      expect(result).not.toContain('EF_CONSTRUCTION')\n    })\n  })\n\n  describe('mixed fields', () => {\n    it('should generate command with multiple field types', () => {\n      const fields: IndexField[] = [\n        { id: 'title', name: 'title', value: 'test', type: FieldTypes.TEXT },\n        { id: 'genre', name: 'genre', value: 'comedy', type: FieldTypes.TAG },\n        { id: 'year', name: 'year', value: 2024, type: FieldTypes.NUMERIC },\n        { id: 'loc', name: 'location', value: '1,2', type: FieldTypes.GEO },\n        {\n          id: 'emb',\n          name: 'embedding',\n          value: '[1,2,3]',\n          type: FieldTypes.VECTOR,\n          options: {\n            algorithm: VectorAlgorithm.FLAT,\n            dimensions: 8,\n            distanceMetric: VectorDistanceMetric.COSINE,\n          },\n        },\n      ]\n\n      const result = generateDynamicFtCreateCommand(\n        buildParams({\n          indexName: 'idx:mixed',\n          prefix: 'item:',\n          fields,\n        }),\n      )\n\n      expect(result).toContain('FT.CREATE \"idx:mixed\"')\n      expect(result).toContain('PREFIX 1 \"item:\"')\n      expect(result).toContain('\"title\" TEXT')\n      expect(result).toContain('\"genre\" TAG')\n      expect(result).toContain('\"year\" NUMERIC')\n      expect(result).toContain('\"location\" GEO')\n      expect(result).toContain('\"embedding\" VECTOR FLAT 6')\n    })\n\n    it('should reorder fields by type to avoid keyword clashes', () => {\n      const fields: IndexField[] = [\n        { id: 'title', name: 'title', value: 'test', type: FieldTypes.TEXT },\n        { id: 'genre', name: 'genre', value: 'comedy', type: FieldTypes.TAG },\n        { id: 'year', name: 'year', value: 2024, type: FieldTypes.NUMERIC },\n      ]\n\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      const numericIdx = result.indexOf('\"year\" NUMERIC')\n      const tagIdx = result.indexOf('\"genre\" TAG')\n      const textIdx = result.indexOf('\"title\" TEXT')\n\n      expect(numericIdx).toBeLessThan(tagIdx)\n      expect(tagIdx).toBeLessThan(textIdx)\n    })\n\n    it('should generate JSON command with multiple field types', () => {\n      const fields: IndexField[] = [\n        { id: 'title', name: 'title', value: 'test', type: FieldTypes.TEXT },\n        { id: 'tags', name: 'tags', value: 'a,b', type: FieldTypes.TAG },\n        { id: 'year', name: 'year', value: 2024, type: FieldTypes.NUMERIC },\n      ]\n\n      const result = generateDynamicFtCreateCommand(\n        buildParams({\n          keyType: RedisearchIndexKeyType.JSON,\n          prefix: 'doc:',\n          fields,\n        }),\n      )\n\n      expect(result).toContain('ON JSON')\n      expect(result).toContain('$.title AS \"title\" TEXT')\n      expect(result).toContain('$.tags AS \"tags\" TAG')\n      expect(result).toContain('$.year AS \"year\" NUMERIC')\n    })\n  })\n\n  describe('reserved keyword field names', () => {\n    it('should place NUMERIC \"weight\" before TEXT fields via reordering', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'desc',\n          name: 'description',\n          value: 'hello',\n          type: FieldTypes.TEXT,\n        },\n        {\n          id: 'w',\n          name: 'weight',\n          value: '10',\n          type: FieldTypes.NUMERIC,\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      const weightIdx = result.indexOf('\"weight\" NUMERIC')\n      const descIdx = result.indexOf('\"description\" TEXT')\n\n      expect(weightIdx).toBeLessThan(descIdx)\n    })\n\n    it('should place TAG fields before TEXT to avoid \"separator\" clash', () => {\n      const fields: IndexField[] = [\n        {\n          id: 's',\n          name: 'separator',\n          value: ';',\n          type: FieldTypes.TEXT,\n        },\n        { id: 't', name: 'tags', value: 'a,b', type: FieldTypes.TAG },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      const tagIdx = result.indexOf('\"tags\" TAG')\n      const textIdx = result.indexOf('\"separator\" TEXT')\n\n      expect(tagIdx).toBeLessThan(textIdx)\n    })\n  })\n\n  describe('field name handling', () => {\n    it('should quote HASH field names with special characters', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'f1',\n          name: 'my-field',\n          value: 'val',\n          type: FieldTypes.TEXT,\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(buildParams({ fields }))\n\n      expect(result).toContain('\"my-field\" TEXT')\n    })\n\n    it('should generate JSONPath for JSON field names with special characters', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'f1',\n          name: 'my_field',\n          value: 'val',\n          type: FieldTypes.TAG,\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(\n        buildParams({ keyType: RedisearchIndexKeyType.JSON, fields }),\n      )\n\n      expect(result).toContain('$.my_field AS \"my_field\" TAG')\n    })\n\n    it('should double-quote the AS alias for JSON fields with reserved word names', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'f1',\n          name: 'WEIGHT',\n          value: '10',\n          type: FieldTypes.NUMERIC,\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(\n        buildParams({ keyType: RedisearchIndexKeyType.JSON, fields }),\n      )\n\n      expect(result).toContain('$.WEIGHT AS \"WEIGHT\" NUMERIC')\n    })\n\n    it('should double-quote the AS alias for JSON fields with colons and hyphens', () => {\n      const fields: IndexField[] = [\n        {\n          id: 'f1',\n          name: 'geo:location-data',\n          value: '1,2',\n          type: FieldTypes.GEO,\n        },\n      ]\n      const result = generateDynamicFtCreateCommand(\n        buildParams({ keyType: RedisearchIndexKeyType.JSON, fields }),\n      )\n\n      expect(result).toContain('$.geo:location-data AS \"geo:location-data\" GEO')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/generateDynamicFtCreateCommand.ts",
    "content": "import {\n  FieldTypes,\n  RedisearchIndexKeyType,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nimport {\n  IndexField,\n  VectorAlgorithm,\n  VectorFieldOptions,\n  VectorHnswFieldOptions,\n  TextFieldOptions,\n} from '../components/index-details/IndexDetails.types'\nimport {\n  VECTOR_ALGORITHM_DEFAULT,\n  VECTOR_DATA_TYPE_DEFAULT,\n  VECTOR_DISTANCE_METRIC_DEFAULT,\n  VECTOR_CONSTRAINTS,\n  PHONETIC_NONE,\n} from '../components/field-type-modal/FieldTypeModal.constants'\n\nexport interface DynamicFtCreateParams {\n  indexName: string\n  keyType: RedisearchIndexKeyType\n  prefix: string\n  fields: IndexField[]\n}\n\nexport const generateDynamicFtCreateCommand = ({\n  indexName,\n  keyType,\n  prefix,\n  fields,\n}: DynamicFtCreateParams): string => {\n  const onClause = keyType === RedisearchIndexKeyType.JSON ? 'JSON' : 'HASH'\n\n  const fieldSchemas = sortFieldsByType(fields)\n    .map((field) => `    ${buildFieldSchema(field, keyType)}`)\n    .join('\\n')\n\n  const parts = [\n    `FT.CREATE \"${indexName}\"`,\n    `  ON ${onClause}`,\n    `    PREFIX 1 \"${prefix}\"`,\n    '  SCHEMA',\n  ]\n\n  if (fieldSchemas) {\n    parts.push(fieldSchemas)\n  }\n\n  return parts.join('\\n')\n}\n\nconst buildFieldSchema = (\n  field: IndexField,\n  keyType: RedisearchIndexKeyType,\n): string => {\n  const isJson = keyType === RedisearchIndexKeyType.JSON\n  const fieldRef = isJson\n    ? `$.${field.name} AS \"${field.name}\"`\n    : `\"${field.name}\"`\n\n  if (field.type === FieldTypes.VECTOR) {\n    const vectorOptions = field.options as VectorFieldOptions | undefined\n    const algorithm = vectorOptions?.algorithm ?? VECTOR_ALGORITHM_DEFAULT\n    const params = getVectorParams(vectorOptions)\n    const numArgs = params.length * 2\n\n    const paramLines = params\n      .map(([key, value]) => `      ${key} ${value}`)\n      .join('\\n')\n\n    return `${fieldRef} VECTOR ${algorithm} ${numArgs}\\n${paramLines}`\n  }\n\n  if (field.type === FieldTypes.TEXT) {\n    const textOptions = field.options as TextFieldOptions | undefined\n    const parts = [fieldRef, 'TEXT']\n\n    if (textOptions?.weight !== undefined && textOptions.weight !== 1) {\n      parts.push('WEIGHT', String(textOptions.weight))\n    }\n\n    if (textOptions?.phonetic && textOptions.phonetic !== PHONETIC_NONE) {\n      parts.push('PHONETIC', textOptions.phonetic)\n    }\n\n    return parts.join(' ')\n  }\n\n  return `${fieldRef} ${field.type.toUpperCase()}`\n}\n\nconst getVectorParams = (options?: VectorFieldOptions): [string, string][] => {\n  const algorithm = options?.algorithm ?? VECTOR_ALGORITHM_DEFAULT\n  const dataType = options?.dataType ?? VECTOR_DATA_TYPE_DEFAULT\n  const dimensions =\n    options?.dimensions ?? VECTOR_CONSTRAINTS.DIMENSIONS_DEFAULT\n  const distanceMetric =\n    options?.distanceMetric ?? VECTOR_DISTANCE_METRIC_DEFAULT\n\n  const params: [string, string][] = [\n    ['TYPE', dataType],\n    ['DIM', String(dimensions)],\n    ['DISTANCE_METRIC', distanceMetric],\n  ]\n\n  if (algorithm === VectorAlgorithm.HNSW) {\n    const hnsw = options as VectorHnswFieldOptions | undefined\n    if (hnsw?.maxEdges !== undefined) {\n      params.push(['M', String(hnsw.maxEdges)])\n    }\n    if (hnsw?.maxNeighbors !== undefined) {\n      params.push(['EF_CONSTRUCTION', String(hnsw.maxNeighbors)])\n    }\n    if (hnsw?.candidateLimit !== undefined) {\n      params.push(['EF_RUNTIME', String(hnsw.candidateLimit)])\n    }\n    if (hnsw?.epsilon !== undefined) {\n      params.push(['EPSILON', String(hnsw.epsilon)])\n    }\n  }\n\n  return params\n}\n\n/**\n * RediSearch parses field modifiers greedily: after a field type, it keeps\n * consuming tokens that match known modifier keywords (case-insensitive).\n * A field *name* like \"weight\" after a TEXT field is consumed as the WEIGHT\n * modifier, causing a parsing error.\n *\n * Ordering fields so that types with fewer/simpler modifiers come first\n * (NUMERIC, GEO → TAG → TEXT → VECTOR) avoids most real-world clashes.\n */\nconst FIELD_TYPE_ORDER: Record<string, number> = {\n  [FieldTypes.NUMERIC]: 0,\n  [FieldTypes.GEO]: 1,\n  [FieldTypes.TAG]: 2,\n  [FieldTypes.TEXT]: 3,\n  [FieldTypes.VECTOR]: 4,\n}\n\nconst sortFieldsByType = (fields: IndexField[]): IndexField[] =>\n  [...fields].sort(\n    (a, b) => (FIELD_TYPE_ORDER[a.type] ?? 3) - (FIELD_TYPE_ORDER[b.type] ?? 3),\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/generateFtCreateCommand.spec.ts",
    "content": "import { SampleDataContent } from '../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { generateFtCreateCommand } from './generateFtCreateCommand'\n\ndescribe('generateFtCreateCommand', () => {\n  it('returns the expected hardcoded FT.CREATE command for e-commerce discovery', () => {\n    const result = generateFtCreateCommand({\n      indexName: 'idx:bikes_vss',\n      dataContent: SampleDataContent.E_COMMERCE_DISCOVERY,\n    })\n\n    expect(result).toBe(`FT.CREATE idx:bikes_vss\n    ON HASH\n        PREFIX 1 \"bikes:\"\n    SCHEMA\n      \"model\" TEXT NOSTEM SORTABLE\n      \"brand\" TEXT NOSTEM SORTABLE\n      \"price\" NUMERIC SORTABLE\n      \"type\" TAG\n      \"material\" TAG\n      \"weight\" NUMERIC SORTABLE\n      \"description_embeddings\" VECTOR FLAT 10\n        TYPE FLOAT32\n        DIM 768\n        DISTANCE_METRIC L2\n        INITIAL_CAP 111\n        BLOCK_SIZE 111`)\n  })\n\n  it('returns the expected hardcoded FT.CREATE command for content recommendations', () => {\n    const result = generateFtCreateCommand({\n      indexName: 'idx:movies',\n      dataContent: SampleDataContent.CONTENT_RECOMMENDATIONS,\n    })\n\n    expect(result).toBe(`FT.CREATE idx:movies\n    ON JSON\n      PREFIX 1 \"movie:\"\n    SCHEMA\n      $.title AS title TEXT\n      $.genres[*] AS genres TAG\n      $.plot AS plot TEXT\n      $.year AS year NUMERIC\n      $.embedding AS embedding VECTOR FLAT 6\n        TYPE FLOAT32\n        DIM 8\n        DISTANCE_METRIC COSINE`)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/generateFtCreateCommand.ts",
    "content": "// TODO: Since v1 would use predefined data, return a hardcoded command\n// instead of generating it dynamically.\n\nimport { SampleDataContent } from '../components/pick-sample-data-modal/PickSampleDataModal.types'\n\ntype FtCreateCommandParams = {\n  indexName: string\n  dataContent: SampleDataContent\n  // TODO: this would eventually need to generate schema based on selected fields\n  // indexFields: any[]\n}\n\nexport const generateFtCreateCommand = ({\n  indexName,\n  dataContent,\n}: FtCreateCommandParams): string => {\n  if (dataContent === SampleDataContent.CONTENT_RECOMMENDATIONS) {\n    return `FT.CREATE ${indexName}\n    ON JSON\n      PREFIX 1 \"movie:\"\n    SCHEMA\n      $.title AS title TEXT\n      $.genres[*] AS genres TAG\n      $.plot AS plot TEXT\n      $.year AS year NUMERIC\n      $.embedding AS embedding VECTOR FLAT 6\n        TYPE FLOAT32\n        DIM 8\n        DISTANCE_METRIC COSINE`\n  }\n\n  return `FT.CREATE ${indexName}\n    ON HASH\n        PREFIX 1 \"bikes:\"\n    SCHEMA\n      \"model\" TEXT NOSTEM SORTABLE\n      \"brand\" TEXT NOSTEM SORTABLE\n      \"price\" NUMERIC SORTABLE\n      \"type\" TAG\n      \"material\" TAG\n      \"weight\" NUMERIC SORTABLE\n      \"description_embeddings\" VECTOR FLAT 10\n        TYPE FLOAT32\n        DIM 768\n        DISTANCE_METRIC L2\n        INITIAL_CAP 111\n        BLOCK_SIZE 111`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/helpers.spec.ts",
    "content": "import { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  RedisResponseBuffer,\n  RedisResponseBufferType,\n} from 'uiSrc/slices/interfaces'\n\nimport {\n  CreateIndexMode,\n  ExistingDataLocationState,\n} from '../pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\nimport { SampleDataContent } from '../components/pick-sample-data-modal/PickSampleDataModal.types'\n\nimport {\n  extractNamespace,\n  deriveIndexName,\n  formatPrefixes,\n  isExistingDataState,\n  isSampleDataState,\n  hasPreselectedKey,\n  parseCreateIndexSearchParams,\n  EMPTY_INDEX_NAME_LABEL,\n  getIndexDisplayName,\n  resolveIndexName,\n  encodeIndexNameForUrl,\n  decodeIndexNameFromUrl,\n} from './helpers'\n\ndescribe('formatPrefixes', () => {\n  it('should format single prefix with quotes', () => {\n    const result = formatPrefixes(['user:'])\n\n    expect(result).toBe('\"user:\"')\n  })\n\n  it('should format multiple prefixes with comma separator', () => {\n    const result = formatPrefixes(['user:', 'customer:'])\n\n    expect(result).toBe('\"user:\", \"customer:\"')\n  })\n\n  it('should return empty string for undefined', () => {\n    const result = formatPrefixes(undefined)\n\n    expect(result).toBe('')\n  })\n\n  it('should return empty string for empty array', () => {\n    const result = formatPrefixes([])\n\n    expect(result).toBe('')\n  })\n})\n\ndescribe('isExistingDataState', () => {\n  it('should return true for existing data state', () => {\n    const state = { mode: CreateIndexMode.ExistingData as const }\n\n    const result = isExistingDataState(state)\n\n    expect(result).toBe(true)\n  })\n\n  it('should return false for sample data state', () => {\n    const state = { sampleData: SampleDataContent.E_COMMERCE_DISCOVERY }\n\n    const result = isExistingDataState(state)\n\n    expect(result).toBe(false)\n  })\n\n  it('should return false for undefined', () => {\n    const result = isExistingDataState(undefined)\n\n    expect(result).toBe(false)\n  })\n})\n\ndescribe('isSampleDataState', () => {\n  it('should return true for sample data state', () => {\n    const state = { sampleData: SampleDataContent.E_COMMERCE_DISCOVERY }\n\n    const result = isSampleDataState(state)\n\n    expect(result).toBe(true)\n  })\n\n  it('should return false for existing data state', () => {\n    const state = { mode: CreateIndexMode.ExistingData as const }\n\n    const result = isSampleDataState(state)\n\n    expect(result).toBe(false)\n  })\n\n  it('should return false for undefined', () => {\n    const result = isSampleDataState(undefined)\n\n    expect(result).toBe(false)\n  })\n})\n\ndescribe('hasPreselectedKey', () => {\n  it('should return true when existing data state has initialKey', () => {\n    const state: ExistingDataLocationState = {\n      mode: CreateIndexMode.ExistingData,\n      initialKey: {\n        data: [98, 105, 107, 101, 115],\n        type: RedisResponseBufferType.Buffer,\n      } as RedisResponseBuffer,\n      initialKeyType: RedisearchIndexKeyType.HASH,\n    }\n\n    const result = hasPreselectedKey(state)\n\n    expect(result).toBe(true)\n  })\n\n  it('should return false when existing data state has no initialKey', () => {\n    const state = { mode: CreateIndexMode.ExistingData as const }\n\n    const result = hasPreselectedKey(state)\n\n    expect(result).toBe(false)\n  })\n\n  it('should return false for sample data state', () => {\n    const state = { sampleData: SampleDataContent.E_COMMERCE_DISCOVERY }\n\n    const result = hasPreselectedKey(state)\n\n    expect(result).toBe(false)\n  })\n\n  it('should return false for undefined', () => {\n    const result = hasPreselectedKey(undefined)\n\n    expect(result).toBe(false)\n  })\n})\n\ndescribe('parseCreateIndexSearchParams', () => {\n  it('should return sampleData state for sampleData param', () => {\n    const result = parseCreateIndexSearchParams(\n      `?sampleData=${SampleDataContent.E_COMMERCE_DISCOVERY}`,\n    )\n\n    expect(result).toEqual({\n      sampleData: SampleDataContent.E_COMMERCE_DISCOVERY,\n    })\n  })\n\n  it('should return existing data state for mode=existingData', () => {\n    const result = parseCreateIndexSearchParams('?mode=existingData')\n\n    expect(result).toEqual({\n      mode: CreateIndexMode.ExistingData,\n      initialKey: undefined,\n      initialKeyType: undefined,\n      initialPrefix: undefined,\n    })\n  })\n\n  it('should parse initialKeyType and initialPrefix', () => {\n    const result = parseCreateIndexSearchParams(\n      `?mode=existingData&initialKeyType=${RedisearchIndexKeyType.JSON}&initialPrefix=bikes:`,\n    )\n\n    expect(result).toEqual({\n      mode: CreateIndexMode.ExistingData,\n      initialKey: undefined,\n      initialKeyType: RedisearchIndexKeyType.JSON,\n      initialPrefix: 'bikes:',\n    })\n  })\n\n  it('should preserve empty-string initialPrefix', () => {\n    const result = parseCreateIndexSearchParams(\n      `?mode=existingData&initialKey=simplekey&initialKeyType=${RedisearchIndexKeyType.HASH}&initialPrefix=`,\n    )\n\n    expect(result).toEqual(\n      expect.objectContaining({\n        mode: CreateIndexMode.ExistingData,\n        initialPrefix: '',\n      }),\n    )\n  })\n\n  it('should parse initialKey as RedisResponseBuffer', () => {\n    const result = parseCreateIndexSearchParams(\n      '?mode=existingData&initialKey=bikes:1001',\n    )\n\n    expect(result).toEqual(\n      expect.objectContaining({\n        mode: CreateIndexMode.ExistingData,\n        initialKey: expect.objectContaining({\n          type: RedisResponseBufferType.Buffer,\n        }),\n      }),\n    )\n  })\n\n  it('should return undefined for unknown params', () => {\n    const result = parseCreateIndexSearchParams('?unknown=value')\n\n    expect(result).toBeUndefined()\n  })\n\n  it('should return undefined for empty search', () => {\n    const result = parseCreateIndexSearchParams('')\n\n    expect(result).toBeUndefined()\n  })\n})\n\ndescribe('extractNamespace', () => {\n  it('should extract namespace from a simple key', () => {\n    const namespace = extractNamespace('bikes:10002')\n    expect(namespace).toBe('bikes:')\n  })\n\n  it('should extract namespace from a key with multiple separators', () => {\n    const namespace = extractNamespace('schools:bmx:large')\n    expect(namespace).toBe('schools:bmx:')\n  })\n\n  it('should return empty string when key has no separator', () => {\n    const namespace = extractNamespace('singlekey')\n    expect(namespace).toBe('')\n  })\n\n  it('should handle key ending with colon', () => {\n    const namespace = extractNamespace('prefix:')\n    expect(namespace).toBe('prefix:')\n  })\n\n  it('should handle empty string', () => {\n    const namespace = extractNamespace('')\n    expect(namespace).toBe('')\n  })\n})\n\ndescribe('deriveIndexName', () => {\n  it('should derive index name from namespace', () => {\n    const result = deriveIndexName('bikes:')\n    expect(result).toBe('idx:bikes')\n  })\n\n  it('should derive index name from multi-level namespace', () => {\n    const result = deriveIndexName('schools:bmx:')\n    expect(result).toBe('idx:schools:bmx')\n  })\n\n  it('should fall back to default when namespace is empty', () => {\n    const result = deriveIndexName('')\n    expect(result).toBe('idx:myindex')\n  })\n})\n\ndescribe('getIndexDisplayName', () => {\n  it('should return label for empty string', () => {\n    expect(getIndexDisplayName('')).toBe(EMPTY_INDEX_NAME_LABEL)\n  })\n\n  it('should return the name as-is for non-empty string', () => {\n    expect(getIndexDisplayName('idx:test')).toBe('idx:test')\n  })\n})\n\ndescribe('resolveIndexName', () => {\n  it('should return empty string for the empty-name label', () => {\n    expect(resolveIndexName(EMPTY_INDEX_NAME_LABEL)).toBe('')\n  })\n\n  it('should return the name as-is for a regular name', () => {\n    expect(resolveIndexName('idx:test')).toBe('idx:test')\n  })\n})\n\ndescribe('encodeIndexNameForUrl / decodeIndexNameFromUrl', () => {\n  it('should round-trip a regular index name', () => {\n    const name = 'idx:bikes'\n    expect(decodeIndexNameFromUrl(encodeIndexNameForUrl(name))).toBe(name)\n  })\n\n  it('should round-trip an empty index name', () => {\n    expect(decodeIndexNameFromUrl(encodeIndexNameForUrl(''))).toBe('')\n  })\n\n  it('should produce a non-empty URL segment for empty name', () => {\n    const encoded = encodeIndexNameForUrl('')\n    expect(encoded).not.toBe('')\n  })\n\n  it('should round-trip a name with special characters', () => {\n    const name = 'idx:special chars/&?#'\n    expect(decodeIndexNameFromUrl(encodeIndexNameForUrl(name))).toBe(name)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/helpers.ts",
    "content": "import { RedisearchIndexKeyType } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { stringToBuffer } from 'uiSrc/utils'\n\nimport { SampleDataContent } from '../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport {\n  CreateIndexLocationState,\n  CreateIndexMode,\n  ExistingDataLocationState,\n  SampleDataLocationState,\n} from '../pages/VectorSearchCreateIndexPage/VectorSearchCreateIndexPage.types'\n\n/**\n * Parses create-index navigation params from a query string.\n * HashRouter (Electron) does not support location.state, so callers\n * encode params in the search string instead.\n */\nexport const parseCreateIndexSearchParams = (\n  search: string,\n): CreateIndexLocationState | undefined => {\n  const params = new URLSearchParams(search)\n  const sampleData = params.get('sampleData') as SampleDataContent | null\n  if (sampleData) return { sampleData }\n\n  const mode = params.get('mode')\n  if (mode === CreateIndexMode.ExistingData) {\n    const initialKeyStr = params.get('initialKey')\n    return {\n      mode: CreateIndexMode.ExistingData,\n      initialKey: initialKeyStr ? stringToBuffer(initialKeyStr) : undefined,\n      initialKeyType:\n        (params.get('initialKeyType') as RedisearchIndexKeyType) || undefined,\n      initialPrefix: params.get('initialPrefix') ?? undefined,\n    }\n  }\n\n  return undefined\n}\n\n/** Narrows location state to ExistingData mode (user-provided key). */\nexport const isExistingDataState = (\n  state: CreateIndexLocationState | undefined,\n): state is ExistingDataLocationState =>\n  state?.mode === CreateIndexMode.ExistingData\n\n/** Narrows location state to SampleData mode (pre-built dataset). */\nexport const isSampleDataState = (\n  state: CreateIndexLocationState | undefined,\n): state is SampleDataLocationState =>\n  state != null && !isExistingDataState(state)\n\n/** Returns true when the ExistingData state already carries a pre-selected key. */\nexport const hasPreselectedKey = (\n  state: CreateIndexLocationState | undefined,\n): boolean => isExistingDataState(state) && !!state.initialKey\n\n/**\n * Formats prefixes array for display.\n * Joins prefixes with comma and wraps each in quotes.\n */\nexport const formatPrefixes = (prefixes: string[] | undefined): string => {\n  if (!prefixes || prefixes.length === 0) {\n    return ''\n  }\n\n  return prefixes.map((prefix) => `\"${prefix}\"`).join(', ')\n}\n\n/**\n * Extracts the namespace prefix from a key name.\n * e.g. \"bikes:10002\" → \"bikes:\", \"schools:bmx:large\" → \"schools:bmx:\"\n * If no separator is found, returns an empty string.\n */\nexport const extractNamespace = (keyName: string): string => {\n  const lastColonIdx = keyName.lastIndexOf(':')\n\n  if (lastColonIdx === -1) {\n    return ''\n  }\n\n  return keyName.slice(0, lastColonIdx + 1)\n}\n\n/**\n * Derives a default index name from a namespace prefix.\n * e.g. \"bikes:\" → \"idx:bikes\", \"\" → \"idx:myindex\"\n */\nexport const deriveIndexName = (namespace: string): string => {\n  const stripped = namespace.replace(/:$/, '')\n  return stripped ? `idx:${stripped}` : 'idx:myindex'\n}\n\n/**\n * Display label used in place of an empty index name.\n * Also doubles as the RiSelect value and URL segment, since both\n * RiSelect and React Router cannot handle empty strings.\n */\nexport const EMPTY_INDEX_NAME_LABEL = '(empty name)'\n\nexport const getIndexDisplayName = (name: string): string =>\n  name === '' ? EMPTY_INDEX_NAME_LABEL : name\n\nexport const resolveIndexName = (displayName: string): string =>\n  displayName === EMPTY_INDEX_NAME_LABEL ? '' : displayName\n\nexport const encodeIndexNameForUrl = (name: string): string =>\n  encodeURIComponent(getIndexDisplayName(name))\n\nexport const decodeIndexNameFromUrl = (urlSegment: string): string =>\n  resolveIndexName(decodeURIComponent(urlSegment))\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/index.ts",
    "content": "export * from './helpers'\nexport * from './inferFieldType'\nexport * from './sampleData'\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/inferFieldType.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\nimport {\n  FieldTypes,\n  RedisearchIndexKeyType,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport {\n  isGeoString,\n  isGeoCoordinates,\n  isVector,\n  isVectorLikeString,\n  isNumeric,\n  inferFieldType,\n  inferHashKeyFields,\n  inferKeyFields,\n} from './inferFieldType'\n\ndescribe('isGeoString', () => {\n  it.each([\n    { value: '-122.4194,37.7749', desc: 'lon,lat' },\n    { value: '0,0', desc: 'origin' },\n    { value: '-180,-90', desc: 'min bounds' },\n    { value: '180,90', desc: 'max bounds' },\n    { value: ' -122.4194 , 37.7749 ', desc: 'whitespace around values' },\n    { value: '0.0,0.0', desc: 'decimal zeros' },\n  ])('should return true for $desc', ({ value }) => {\n    expect(isGeoString(value)).toBe(true)\n  })\n\n  it.each([\n    { value: '181,0', desc: 'lon out of range (> 180)' },\n    { value: '0,91', desc: 'lat out of range (> 90)' },\n    { value: '-181,0', desc: 'lon out of range (< -180)' },\n    { value: '0,-91', desc: 'lat out of range (< -90)' },\n    { value: 'hello,world', desc: 'non-numeric values' },\n    { value: '1,2,3', desc: 'three components' },\n    { value: '42', desc: 'single number' },\n    { value: '', desc: 'empty string' },\n    { value: ',', desc: 'just comma' },\n  ])('should return false for $desc', ({ value }) => {\n    expect(isGeoString(value)).toBe(false)\n  })\n})\n\ndescribe('isGeoCoordinates', () => {\n  it.each([\n    { value: [-122.4194, 37.7749], desc: '[lon, lat] array' },\n    { value: [0, 0], desc: 'origin array' },\n    { value: [-180, -90], desc: 'min bounds array' },\n    { value: [180, 90], desc: 'max bounds array' },\n    {\n      value: { lon: -122.4194, lat: 37.7749 },\n      desc: 'object with lon, lat',\n    },\n    {\n      value: { lat: 37.7749, lon: -122.4194 },\n      desc: 'object with lat, lon',\n    },\n    {\n      value: { longitude: 0, latitude: 0 },\n      desc: 'object with longitude, latitude',\n    },\n  ])('should return true for $desc', ({ value }) => {\n    expect(isGeoCoordinates(value)).toBe(true)\n  })\n\n  it.each([\n    { value: [181, 0], desc: 'lon out of range in array' },\n    { value: [0, 91], desc: 'lat out of range in array' },\n    { value: [1, 2, 3], desc: 'three elements' },\n    { value: [1], desc: 'single element' },\n    { value: ['a', 'b'], desc: 'non-numeric array' },\n    { value: {}, desc: 'empty object' },\n    { value: { x: 1, y: 2 }, desc: 'object without lat/lon' },\n    { value: { lon: 1 }, desc: 'object missing lat' },\n    { value: null, desc: 'null' },\n    { value: '-122.4,37.7', desc: 'string' },\n  ])('should return false for $desc', ({ value }) => {\n    expect(isGeoCoordinates(value)).toBe(false)\n  })\n})\n\ndescribe('isVector', () => {\n  it.each([\n    { value: [1, 2, 3], desc: 'integer array' },\n    { value: [1.5, -2.0, 3.14], desc: 'float array' },\n    { value: [0, 0], desc: 'minimal length array' },\n  ])('should return true for $desc', ({ value }) => {\n    expect(isVector(value)).toBe(true)\n  })\n\n  it.each([\n    { value: [], desc: 'empty array' },\n    { value: [1], desc: 'single element (too short)' },\n    { value: [1, 'a', 3], desc: 'mixed types' },\n    { value: ['a', 'b'], desc: 'string array' },\n    { value: [1, Number.NaN, 3], desc: 'NaN in array' },\n    { value: [1, Number.POSITIVE_INFINITY], desc: 'Infinity in array' },\n    { value: null, desc: 'null' },\n    { value: 'not an array', desc: 'string' },\n  ])('should return false for $desc', ({ value }) => {\n    expect(isVector(value)).toBe(false)\n  })\n})\n\ndescribe('isVectorLikeString', () => {\n  it.each([\n    { value: '[1,2,3]', desc: 'integer array string' },\n    { value: '[1.5, -2.0, 3.14]', desc: 'float array string' },\n    { value: '[0, 0]', desc: 'minimal length array string' },\n    { value: ' [1, 2, 3] ', desc: 'surrounding whitespace' },\n  ])('should return true for $desc', ({ value }) => {\n    expect(isVectorLikeString(value)).toBe(true)\n  })\n\n  it.each([\n    { value: '[]', desc: 'empty array' },\n    { value: '[1]', desc: 'single element (too short)' },\n    { value: '[1, \"a\", 3]', desc: 'mixed types' },\n    { value: '[\"a\", \"b\"]', desc: 'string array' },\n    { value: 'not an array', desc: 'plain string' },\n    { value: '{1, 2, 3}', desc: 'curly braces' },\n    { value: '', desc: 'empty string' },\n    { value: '[1, NaN, 3]', desc: 'NaN in array' },\n    { value: '[1, Infinity]', desc: 'Infinity in array' },\n  ])('should return false for $desc', ({ value }) => {\n    expect(isVectorLikeString(value)).toBe(false)\n  })\n})\n\ndescribe('isNumeric', () => {\n  it.each([\n    { value: '42', desc: 'integer' },\n    { value: '-3.14', desc: 'negative float' },\n    { value: '+100', desc: 'positive sign' },\n    { value: '0', desc: 'zero' },\n    { value: '.5', desc: 'leading dot' },\n    { value: '1e10', desc: 'scientific notation' },\n    { value: '2.5E-3', desc: 'scientific with capital E' },\n    { value: '1.0', desc: 'float with trailing zero' },\n    { value: ' 42 ', desc: 'whitespace padded' },\n  ])('should return true for $desc', ({ value }) => {\n    expect(isNumeric(value)).toBe(true)\n  })\n\n  it.each([\n    { value: '12abc', desc: 'alphanumeric' },\n    { value: '', desc: 'empty string' },\n    { value: '   ', desc: 'whitespace only' },\n    { value: 'hello', desc: 'word' },\n    { value: '1,000', desc: 'thousands separator' },\n    { value: '1.2.3', desc: 'multiple dots' },\n  ])('should return false for $desc', ({ value }) => {\n    expect(isNumeric(value)).toBe(false)\n  })\n})\n\ndescribe('inferFieldType', () => {\n  describe('string-based detection', () => {\n    it('should return TAG for WKT patterns (GEOSHAPE not supported)', () => {\n      expect(inferFieldType('POINT(1 2)')).toBe(FieldTypes.TAG)\n    })\n\n    it('should return GEO for coordinate patterns', () => {\n      expect(inferFieldType('-122.4194,37.7749')).toBe(FieldTypes.GEO)\n    })\n\n    it('should return VECTOR for numeric arrays', () => {\n      expect(inferFieldType('[1, 2, 3, 4]')).toBe(FieldTypes.VECTOR)\n    })\n\n    it('should return NUMERIC for numeric strings', () => {\n      expect(inferFieldType('42')).toBe(FieldTypes.NUMERIC)\n    })\n\n    it('should return TAG for short strings', () => {\n      const shortValue = faker.string.alpha(49)\n      expect(inferFieldType(shortValue)).toBe(FieldTypes.TAG)\n    })\n\n    it('should return TEXT for long strings', () => {\n      const longValue = faker.string.alpha(50)\n      expect(inferFieldType(longValue)).toBe(FieldTypes.TEXT)\n    })\n\n    it('should return TAG for empty string', () => {\n      expect(inferFieldType('')).toBe(FieldTypes.TAG)\n    })\n  })\n\n  describe('priority order', () => {\n    it('should return TAG for WKT with coordinates (GEOSHAPE not supported)', () => {\n      expect(inferFieldType('POINT(-122.4 37.7)')).toBe(FieldTypes.TAG)\n    })\n\n    it('should prefer GEO over NUMERIC for coordinate-like values', () => {\n      expect(inferFieldType('10,20')).toBe(FieldTypes.GEO)\n    })\n\n    it('should prefer VECTOR over TAG for short numeric arrays', () => {\n      expect(inferFieldType('[1,2]')).toBe(FieldTypes.VECTOR)\n    })\n\n    it('documents inconsistency: 2D numeric array in lon/lat range is GEO, same as string is VECTOR', () => {\n      expect(inferFieldType([1, 2])).toBe(FieldTypes.GEO)\n      expect(inferFieldType('[1,2]')).toBe(FieldTypes.VECTOR)\n    })\n\n    it('should prefer NUMERIC over TAG for short numeric strings', () => {\n      expect(inferFieldType('42')).toBe(FieldTypes.NUMERIC)\n    })\n  })\n\n  describe('value-based detection (number, boolean, null, array, object)', () => {\n    it.each([\n      { value: 42, desc: 'integer' },\n      { value: 99.9, desc: 'float' },\n    ])('should return NUMERIC for $desc', ({ value }) => {\n      expect(inferFieldType(value)).toBe(FieldTypes.NUMERIC)\n    })\n\n    it('should return TAG for boolean', () => {\n      expect(inferFieldType(true)).toBe(FieldTypes.TAG)\n      expect(inferFieldType(false)).toBe(FieldTypes.TAG)\n    })\n\n    it('should return TAG for null and undefined', () => {\n      expect(inferFieldType(null)).toBe(FieldTypes.TAG)\n      expect(inferFieldType(undefined)).toBe(FieldTypes.TAG)\n    })\n\n    it('should return VECTOR for array of numbers', () => {\n      expect(inferFieldType([1, 2, 3])).toBe(FieldTypes.VECTOR)\n    })\n\n    it('should return TEXT for array of non-numbers (structured data, not a single tag)', () => {\n      expect(inferFieldType(['a', 'b'])).toBe(FieldTypes.TEXT)\n    })\n\n    it('should return TEXT for object', () => {\n      expect(inferFieldType({})).toBe(FieldTypes.TEXT)\n      expect(inferFieldType({ foo: 1 })).toBe(FieldTypes.TEXT)\n    })\n\n    it('should return GEO for [lon, lat] array', () => {\n      expect(inferFieldType([-122.4194, 37.7749])).toBe(FieldTypes.GEO)\n      expect(inferFieldType([0, 0])).toBe(FieldTypes.GEO)\n    })\n\n    it('should return GEO for { lat, lon } object', () => {\n      expect(inferFieldType({ lon: -122.4194, lat: 37.7749 })).toBe(\n        FieldTypes.GEO,\n      )\n      expect(inferFieldType({ lat: 0, lon: 0 })).toBe(FieldTypes.GEO)\n    })\n\n    it('should return VECTOR for 3+ element numeric array not GEO', () => {\n      expect(inferFieldType([1, 2, 3])).toBe(FieldTypes.VECTOR)\n    })\n\n    it('should use string detection when value is string', () => {\n      expect(inferFieldType('42')).toBe(FieldTypes.NUMERIC)\n      expect(inferFieldType('short')).toBe(FieldTypes.TAG)\n    })\n  })\n})\n\ndescribe('inferHashKeyFields', () => {\n  it('should infer types for all hash fields', () => {\n    const fields = [\n      { field: 'price', value: '1398' },\n      { field: 'brand', value: 'Eva' },\n      { field: 'coords', value: '-122.4194,37.7749' },\n      { field: 'embedding', value: '[0.1, 0.2, 0.3]' },\n      { field: 'description', value: faker.string.alpha(100) },\n    ]\n\n    const result = inferHashKeyFields(fields)\n\n    expect(result).toHaveLength(5)\n    expect(result[0]).toEqual({\n      id: 'price',\n      name: 'price',\n      value: '1398',\n      type: FieldTypes.NUMERIC,\n    })\n    expect(result[1]).toEqual({\n      id: 'brand',\n      name: 'brand',\n      value: 'Eva',\n      type: FieldTypes.TAG,\n    })\n    expect(result[2].type).toBe(FieldTypes.GEO)\n    expect(result[3].type).toBe(FieldTypes.VECTOR)\n    expect(result[4].type).toBe(FieldTypes.TEXT)\n  })\n\n  it('should return empty array for empty input', () => {\n    expect(inferHashKeyFields([])).toEqual([])\n  })\n})\n\ndescribe('inferKeyFields', () => {\n  it('should walk Hash key object and infer types when keyType is hash', () => {\n    const hashObject = {\n      price: '99',\n      name: 'bike',\n    }\n\n    const fields = inferKeyFields(hashObject, RedisearchIndexKeyType.HASH)\n\n    expect(fields).toHaveLength(2)\n    expect(fields).toEqual(\n      inferHashKeyFields([\n        { field: 'price', value: '99' },\n        { field: 'name', value: 'bike' },\n      ]),\n    )\n    expect(fields[0].type).toBe(FieldTypes.NUMERIC)\n    expect(fields[1].type).toBe(FieldTypes.TAG)\n  })\n\n  it('should walk JSON key object and infer types when keyType is json', () => {\n    const jsonObject = {\n      count: 10,\n      label: 'active',\n    }\n\n    const fields = inferKeyFields(jsonObject, RedisearchIndexKeyType.JSON)\n\n    expect(fields).toHaveLength(2)\n    expect(fields[0]).toEqual({\n      id: 'count',\n      name: 'count',\n      value: '10',\n      type: FieldTypes.NUMERIC,\n    })\n    expect(fields[1]).toEqual({\n      id: 'label',\n      name: 'label',\n      value: 'active',\n      type: FieldTypes.TAG,\n    })\n  })\n\n  it('should treat numeric strings as TAG/TEXT for JSON (only actual numbers as NUMERIC)', () => {\n    const jsonObject = {\n      actualNumber: 42,\n      numericString: '1398',\n    }\n\n    const fields = inferKeyFields(jsonObject, RedisearchIndexKeyType.JSON)\n\n    expect(fields).toHaveLength(2)\n    expect(fields[0].type).toBe(FieldTypes.NUMERIC)\n    expect(fields[1].type).toBe(FieldTypes.TAG)\n  })\n\n  it('should return empty array for empty object', () => {\n    expect(inferKeyFields({}, RedisearchIndexKeyType.HASH)).toEqual([])\n    expect(inferKeyFields({}, RedisearchIndexKeyType.JSON)).toEqual([])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/inferFieldType.ts",
    "content": "import {\n  FieldTypes,\n  RedisearchIndexKeyType,\n} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { IndexField } from '../components/index-details/IndexDetails.types'\n\nconst TAG_MAX_LENGTH = 50\nconst MIN_VECTOR_LENGTH = 2\n\nconst LON_MIN = -180\nconst LON_MAX = 180\nconst LAT_MIN = -90\nconst LAT_MAX = 90\n\nconst isLonLatInRange = (lon: number, lat: number): boolean =>\n  Number.isFinite(lon) &&\n  Number.isFinite(lat) &&\n  lon >= LON_MIN &&\n  lon <= LON_MAX &&\n  lat >= LAT_MIN &&\n  lat <= LAT_MAX\n\n/** Returns true if value is [lon, lat] or { lat, lon } with valid coordinate ranges. */\nexport const isGeoCoordinates = (value: unknown): boolean => {\n  if (Array.isArray(value)) {\n    if (value.length !== 2) {\n      return false\n    }\n    const [a, b] = value\n    if (typeof a !== 'number' || typeof b !== 'number') {\n      return false\n    }\n    return isLonLatInRange(a, b)\n  }\n  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n    const o = value as Record<string, unknown>\n    const lon = o.lon ?? o.longitude\n    const lat = o.lat ?? o.latitude\n    if (typeof lon !== 'number' || typeof lat !== 'number') {\n      return false\n    }\n    return isLonLatInRange(lon, lat)\n  }\n  return false\n}\n\nconst GEO_REGEX = /^\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*$/\n\nexport const isGeoString = (value: string): boolean => {\n  const match = GEO_REGEX.exec(value)\n  if (!match) {\n    return false\n  }\n  const lon = parseFloat(match[1])\n  const lat = parseFloat(match[2])\n  return isGeoCoordinates([lon, lat])\n}\n\n/** Returns true if value is an array of at least MIN_VECTOR_LENGTH finite numbers. */\nexport const isVector = (value: unknown): boolean =>\n  Array.isArray(value) &&\n  value.length >= MIN_VECTOR_LENGTH &&\n  value.every((item) => typeof item === 'number' && Number.isFinite(item))\n\n/** Returns true if string parses as JSON array that passes isVector (e.g. hash field \"[1,2,3]\"). */\nexport const isVectorLikeString = (value: string): boolean => {\n  const trimmed = value.trim()\n  if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {\n    return false\n  }\n  try {\n    const parsed = JSON.parse(trimmed)\n    return isVector(parsed)\n  } catch {\n    return false\n  }\n}\n\nexport const isNumeric = (value: string): boolean => {\n  if (value.trim() === '') {\n    return false\n  }\n  const n = Number(value)\n  return !Number.isNaN(n) && Number.isFinite(n)\n}\n\nexport interface InferFieldTypeOptions {\n  /** When true, only typeof value === 'number' yields NUMERIC; numeric strings become TAG/TEXT. Use for JSON. */\n  strictNumbers?: boolean\n}\n\n/**\n * Infers the index field type from a raw value (string, number, boolean, null, array, or object).\n */\nexport const inferFieldType = (\n  value: unknown,\n  options?: InferFieldTypeOptions,\n): FieldTypes => {\n  if (value === null || value === undefined) {\n    return FieldTypes.TAG\n  }\n  if (typeof value === 'number') {\n    return FieldTypes.NUMERIC\n  }\n  if (typeof value === 'boolean') {\n    return FieldTypes.TAG\n  }\n  if (Array.isArray(value)) {\n    if (isGeoCoordinates(value)) {\n      return FieldTypes.GEO\n    }\n    return isVector(value) ? FieldTypes.VECTOR : FieldTypes.TEXT\n  }\n  if (typeof value === 'object') {\n    if (isGeoCoordinates(value)) {\n      return FieldTypes.GEO\n    }\n    return FieldTypes.TEXT\n  }\n\n  const str = String(value)\n  if (isGeoString(str)) {\n    return FieldTypes.GEO\n  }\n  if (isVectorLikeString(str)) {\n    return FieldTypes.VECTOR\n  }\n  if (!options?.strictNumbers && isNumeric(str)) {\n    return FieldTypes.NUMERIC\n  }\n\n  if (str.length < TAG_MAX_LENGTH) {\n    return FieldTypes.TAG\n  }\n  return FieldTypes.TEXT\n}\n\nexport interface HashFieldInput {\n  field: string\n  value: string\n}\n\n/** Converts a value to string for IndexField.value (display/storage). */\nconst valueToString = (value: unknown): string => {\n  if (value == null) {\n    return ''\n  }\n  if (typeof value === 'object') {\n    return JSON.stringify(value)\n  }\n  return String(value)\n}\n\nexport const inferHashKeyFields = (fields: HashFieldInput[]): IndexField[] =>\n  fields.map((entry) => ({\n    id: entry.field,\n    name: entry.field,\n    value: entry.value,\n    type: inferFieldType(entry.value),\n  }))\n\n/**\n * Top-level hash key value: field name → string value.\n * This is the key's value as loaded when the user clicks the key in the keys browser (Hash).\n */\nexport type HashKeyData = Record<string, string>\n\n/**\n * Top-level JSON key value: property name → value (string, number, boolean, null, array, or object).\n * This is the key's value as loaded when the user clicks the key in the keys browser (JSON).\n */\nexport type JsonKeyData = Record<string, unknown>\n\n/**\n * Infers index field types for all top-level fields of a key.\n * - For Hash keys: infers from string values directly.\n * - For JSON keys: infers from typed values with strict number handling.\n *\n * Note: callers should pre-filter complex JSON values via filterJsonData\n * before passing data to this function.\n */\nexport const inferKeyFields = (\n  data: HashKeyData | JsonKeyData,\n  keyType: RedisearchIndexKeyType,\n): IndexField[] => {\n  const entries = Object.entries(data)\n\n  if (keyType === RedisearchIndexKeyType.HASH) {\n    return inferHashKeyFields(\n      entries.map(([field, value]) => ({ field, value: value as string })),\n    )\n  }\n\n  return entries.map(([key, value]) => ({\n    id: key,\n    name: key,\n    value: valueToString(value),\n    type: inferFieldType(value, { strictNumbers: true }),\n  }))\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/sampleData.ts",
    "content": "import { SampleDataContent } from '../components/pick-sample-data-modal/PickSampleDataModal.types'\nimport { IndexField } from '../components/index-details/IndexDetails.types'\nimport { SampleQuery } from '../constants/sample-data/types'\nimport { SAMPLE_DATASETS } from '../constants'\n\n/**\n * Preset index names for sample datasets.\n */\nexport enum PresetIndexName {\n  BIKES = 'idx:bikes_vss',\n  MOVIES = 'idx:movies_vss',\n}\n\nexport const getIndexNameBySampleData = (\n  sampleData: SampleDataContent,\n): string => SAMPLE_DATASETS[sampleData].indexName\n\nexport const getFieldsBySampleData = (\n  sampleData: SampleDataContent,\n): IndexField[] => SAMPLE_DATASETS[sampleData].fields\n\nexport const getCollectionNameBySampleData = (\n  sampleData: SampleDataContent,\n): string => SAMPLE_DATASETS[sampleData].collectionName\n\nexport const getDisplayNameBySampleData = (\n  sampleData: SampleDataContent,\n): string => SAMPLE_DATASETS[sampleData].displayName\n\nexport const getIndexPrefixBySampleData = (\n  sampleData: SampleDataContent,\n): string => SAMPLE_DATASETS[sampleData].indexPrefix\n\nexport const getSampleQueriesBySampleData = (\n  sampleData: SampleDataContent,\n): SampleQuery[] => SAMPLE_DATASETS[sampleData].sampleQueries\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/telemetry.utils.spec.ts",
    "content": "import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { IndexField } from '../components/index-details/IndexDetails.types'\nimport {\n  getFieldTypeSummary,\n  getSearchCommandType,\n  searchResultsTelemetry,\n} from './telemetry.utils'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('getFieldTypeSummary', () => {\n  it('should return empty object for empty fields', () => {\n    const result = getFieldTypeSummary([])\n    expect(result).toEqual({})\n  })\n\n  it('should count field types correctly', () => {\n    const fields: IndexField[] = [\n      { id: '1', name: 'title', value: 'title', type: FieldTypes.TEXT },\n      { id: '2', name: 'tag', value: 'tag', type: FieldTypes.TAG },\n      { id: '3', name: 'desc', value: 'desc', type: FieldTypes.TEXT },\n      { id: '4', name: 'vec', value: 'vec', type: FieldTypes.VECTOR },\n      { id: '5', name: 'tag2', value: 'tag2', type: FieldTypes.TAG },\n    ]\n\n    const result = getFieldTypeSummary(fields)\n    expect(result).toEqual({\n      [FieldTypes.TEXT]: 2,\n      [FieldTypes.TAG]: 2,\n      [FieldTypes.VECTOR]: 1,\n    })\n  })\n\n  it('should handle single field type', () => {\n    const fields: IndexField[] = [\n      { id: '1', name: 'vec', value: 'vec', type: FieldTypes.VECTOR },\n    ]\n\n    const result = getFieldTypeSummary(fields)\n    expect(result).toEqual({ [FieldTypes.VECTOR]: 1 })\n  })\n})\n\ndescribe('getSearchCommandType', () => {\n  it.each([\n    { input: 'FT.SEARCH idx *', expected: 'search' },\n    { input: 'FT.AGGREGATE idx *', expected: 'aggregate' },\n    { input: 'FT.EXPLAIN idx \"*\" LIMIT 0 10', expected: 'explain' },\n    { input: 'FT.PROFILE idx SEARCH QUERY \"*\"', expected: 'profile' },\n    { input: 'FT.INFO idx', expected: 'other' },\n    { input: 'SET key value', expected: 'other' },\n    { input: '', expected: 'other' },\n  ])('should return \"$expected\" for \"$input\"', ({ input, expected }) => {\n    expect(getSearchCommandType(input)).toBe(expected)\n  })\n})\n\ndescribe('searchResultsTelemetry', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should send SEARCH_COMMAND_COPIED event on onCommandCopied', () => {\n    searchResultsTelemetry.onCommandCopied?.({\n      command: 'FT.SEARCH idx query',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_COMMAND_COPIED,\n      eventData: { databaseId: 'db-123', command: 'FT.SEARCH idx query' },\n    })\n  })\n\n  it('should send SEARCH_CLEAR_RESULT_CLICKED event on onResultCleared', () => {\n    searchResultsTelemetry.onResultCleared?.({\n      command: 'FT.SEARCH idx query',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_CLEAR_RESULT_CLICKED,\n      eventData: { databaseId: 'db-123', command: 'FT.SEARCH idx query' },\n    })\n  })\n\n  it('should send SEARCH_RESULTS_COLLAPSED event on onResultCollapsed', () => {\n    searchResultsTelemetry.onResultCollapsed?.({\n      command: 'FT.SEARCH idx query',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_RESULTS_COLLAPSED,\n      eventData: { databaseId: 'db-123', command: 'FT.SEARCH idx query' },\n    })\n  })\n\n  it('should send SEARCH_RESULTS_EXPANDED event on onResultExpanded', () => {\n    searchResultsTelemetry.onResultExpanded?.({\n      command: 'FT.SEARCH idx query',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_RESULTS_EXPANDED,\n      eventData: { databaseId: 'db-123', command: 'FT.SEARCH idx query' },\n    })\n  })\n\n  it('should send SEARCH_RESULT_VIEW_CHANGED event on onResultViewChanged', () => {\n    const params = {\n      databaseId: 'db-123',\n      command: 'FT.SEARCH',\n      rawMode: false,\n      group: false,\n      previousView: 'Text',\n      currentView: 'Plugin',\n    }\n\n    searchResultsTelemetry.onResultViewChanged?.(params)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_RESULT_VIEW_CHANGED,\n      eventData: params,\n    })\n  })\n\n  it('should send SEARCH_RESULTS_IN_FULL_SCREEN event on onFullScreenToggled', () => {\n    searchResultsTelemetry.onFullScreenToggled?.({\n      state: 'Open',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_RESULTS_IN_FULL_SCREEN,\n      eventData: { databaseId: 'db-123', state: 'Open' },\n    })\n  })\n\n  it('should send SEARCH_COMMAND_RUN_AGAIN event on onQueryReRun', () => {\n    searchResultsTelemetry.onQueryReRun?.({\n      command: 'FT.SEARCH idx query',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.SEARCH_COMMAND_RUN_AGAIN,\n      eventData: { databaseId: 'db-123', commands: ['FT.SEARCH idx query'] },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/vector-search/utils/telemetry.utils.ts",
    "content": "import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { QueryResultsTelemetry } from 'uiSrc/components/query/context/query-results.context'\nimport { IndexField } from '../components/index-details/IndexDetails.types'\nimport { SearchCommandType } from '../telemetry.constants'\n\nexport const getSearchCommandType = (command: string): SearchCommandType => {\n  const trimmed = command.trimStart().toUpperCase()\n\n  if (trimmed.startsWith('FT.SEARCH')) {\n    return SearchCommandType.Search\n  }\n\n  if (trimmed.startsWith('FT.AGGREGATE')) {\n    return SearchCommandType.Aggregate\n  }\n\n  if (trimmed.startsWith('FT.EXPLAIN')) {\n    return SearchCommandType.Explain\n  }\n\n  if (trimmed.startsWith('FT.PROFILE')) {\n    return SearchCommandType.Profile\n  }\n\n  return SearchCommandType.Other\n}\n\nexport const getFieldTypeSummary = (\n  fields: IndexField[],\n): Record<string, number> =>\n  fields.reduce(\n    (acc, field) => {\n      acc[field.type] = (acc[field.type] || 0) + 1\n      return acc\n    },\n    {} as Record<string, number>,\n  )\n\nexport const searchResultsTelemetry: QueryResultsTelemetry = {\n  onCommandCopied: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_COMMAND_COPIED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultCleared: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_CLEAR_RESULT_CLICKED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultCollapsed: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_RESULTS_COLLAPSED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultExpanded: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_RESULTS_EXPANDED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultViewChanged: (params) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_RESULT_VIEW_CHANGED,\n      eventData: params,\n    })\n  },\n  onFullScreenToggled: ({ state, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_RESULTS_IN_FULL_SCREEN,\n      eventData: { databaseId, state },\n    })\n  },\n  onQueryReRun: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.SEARCH_COMMAND_RUN_AGAIN,\n      eventData: { databaseId, commands: [command] },\n    })\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/WorkbenchPage.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { useSelector } from 'react-redux'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport { RunQueryMode } from 'uiSrc/slices/interfaces'\nimport { RootState } from 'uiSrc/slices/store'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport {\n  act,\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  userEvent,\n  waitForRiTooltipVisible,\n} from 'uiSrc/utils/test-utils'\nimport WorkbenchPage from './WorkbenchPage'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('react-redux', () => ({\n  ...jest.requireActual('react-redux'),\n  useSelector: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [],\n  }),\n}))\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\n/**\n * WorkbenchPage tests\n *\n * @group component\n */\ndescribe('WorkbenchPage', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          workbench: {\n            ...state.workbench,\n            results: {\n              ...state.workbench.results,\n              items: [\n                {\n                  mode: 'RAW',\n                  resultsMode: 'DEFAULT',\n                  id: '9dda0f6d-9265-4b15-b627-82d2eb867605',\n                  databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946',\n                  command: 'info',\n                  summary: null,\n                  createdAt: '2022-09-28T18:04:46.000Z',\n                  emptyCommand: false,\n                },\n              ],\n            },\n          },\n        }),\n    )\n  })\n\n  it('should render', () => {\n    expect(render(<WorkbenchPage />)).toBeTruthy()\n  })\n})\n\n/**\n * WorkbenchPage tests\n *\n * @group component\n */\n\ndescribe('Telemetry', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          app: {\n            ...state.app,\n            context: {\n              ...state.app.context,\n              workbench: {\n                ...state.app.context.workbench,\n                script: 'info',\n              },\n            },\n          },\n          workbench: {\n            ...state.workbench,\n            results: {\n              ...state.workbench.results,\n              items: [\n                {\n                  mode: 'RAW',\n                  resultsMode: 'DEFAULT',\n                  id: '9dda0f6d-9265-4b15-b627-82d2eb867605',\n                  databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946',\n                  command: 'info',\n                  summary: null,\n                  createdAt: '2022-09-28T18:04:46.000Z',\n                  emptyCommand: false,\n                },\n              ],\n            },\n          },\n        }),\n    )\n  })\n\n  it('should send proper eventData after changing Raw mode', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WorkbenchPage />)\n\n    // turn on Raw mode\n    fireEvent.click(screen.getByTestId('btn-change-mode'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_MODE_CHANGED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        changedFromMode: RunQueryMode.ASCII,\n        changedToMode: RunQueryMode.Raw,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should send proper eventData without Raw mode', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WorkbenchPage />)\n\n    // send command without Raw mode\n    fireEvent.click(screen.getByTestId('btn-submit'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED,\n      eventData: {\n        command: 'INFO',\n        databaseId: INSTANCE_ID_MOCK,\n        results: 'single',\n        multiple: 'Single',\n        pipeline: true,\n        rawMode: false,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should send proper eventData with Raw mode', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WorkbenchPage />)\n\n    // send command with Raw mode\n    fireEvent.click(screen.getByTestId('btn-submit'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED,\n      eventData: {\n        command: 'INFO',\n        databaseId: INSTANCE_ID_MOCK,\n        results: 'single',\n        multiple: 'Single',\n        pipeline: true,\n        rawMode: false,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('Results: should send telemetry on re-run', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WorkbenchPage />)\n\n    fireEvent.click(screen.getByTestId('re-run-command'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_COMMAND_RUN_AGAIN,\n      eventData: {\n        auto: undefined,\n        pipeline: undefined,\n        command: 'INFO',\n        databaseId: INSTANCE_ID_MOCK,\n        multiple: 'Single',\n        results: 'single',\n        rawMode: true,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n\n  it('should call proper telemetry on delete', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WorkbenchPage />)\n\n    fireEvent.click(screen.getByTestId('delete-command'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n        command: 'info',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n\n    fireEvent.click(screen.getByTestId('clear-history-btn'))\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_CLEAR_ALL_RESULTS_CLICKED,\n      eventData: {\n        databaseId: 'instanceId',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\ndescribe('Raw mode', () => {\n  beforeEach(() => {\n    const state: any = store.getState()\n\n    ;(useSelector as jest.Mock).mockImplementation(\n      (callback: (arg0: RootState) => RootState) =>\n        callback({\n          ...state,\n          workbench: {\n            ...state.workbench,\n            results: {\n              ...state.workbench.results,\n              items: [\n                {\n                  mode: 'RAW',\n                  resultsMode: 'DEFAULT',\n                  id: '9dda0f6d-9265-4b15-b627-82d2eb867605',\n                  databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946',\n                  command: 'info',\n                  summary: null,\n                  createdAt: '2022-09-28T18:04:46.000Z',\n                  emptyCommand: false,\n                },\n              ],\n            },\n          },\n        }),\n    )\n  })\n\n  it('Verify tooltips', async () => {\n    render(<WorkbenchPage />)\n\n    await act(async () => {\n      fireEvent.focus(screen.getByTestId('btn-change-mode'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.queryByTestId('change-mode-tooltip')).toBeInTheDocument()\n  })\n\n  it('Verify tooltips2 ', async () => {\n    render(<WorkbenchPage />)\n\n    await act(() => {\n      fireEvent.focus(screen.getByTestId('parameters-anchor'))\n    })\n    await waitForRiTooltipVisible()\n\n    expect(screen.getByTestId('parameters-tooltip')).toBeInTheDocument()\n  })\n\n  it('check button clickable and selected', async () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WorkbenchPage />)\n\n    const btn = screen.getByTestId(/btn-change-mode/)\n    expect(btn).not.toBeDisabled()\n\n    await userEvent.click(btn)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_MODE_CHANGED,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        changedFromMode: RunQueryMode.ASCII,\n        changedToMode: RunQueryMode.Raw,\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport WBViewWrapper from './components/wb-view'\n\nconst WorkbenchPage = () => {\n  const [isPageViewSent, setIsPageViewSent] = useState(false)\n\n  const { name: connectedInstanceName, db } = useSelector(\n    connectedInstanceSelector,\n  )\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  setTitle(\n    `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Workbench`,\n  )\n\n  useEffect(() => {\n    if (connectedInstanceName && !isPageViewSent) {\n      sendPageView(instanceId)\n    }\n  }, [connectedInstanceName, isPageViewSent])\n\n  const sendPageView = (instanceId: string) => {\n    sendPageViewTelemetry({\n      name: TelemetryPageView.WORKBENCH_PAGE,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  return <WBViewWrapper />\n}\n\nexport default WorkbenchPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/Query.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport { QueryEditorContextProvider } from 'uiSrc/components/query'\nimport Query from './Query'\nimport { Props } from './Query.types'\n\nconst mockedProps = mock<Props>()\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\nconst defaultContextValue = {\n  query: '',\n  setQuery: jest.fn(),\n  isLoading: false,\n  commands: [],\n  indexes: [],\n  onSubmit: jest.fn(),\n}\n\nconst renderWithContext = (props: Props, contextOverrides = {}) =>\n  render(\n    <QueryEditorContextProvider\n      value={{ ...defaultContextValue, ...contextOverrides }}\n    >\n      <Query {...props} />\n    </QueryEditorContextProvider>,\n  )\n\ndescribe('Query', () => {\n  it('should render', () => {\n    expect(renderWithContext(instance(mockedProps))).toBeTruthy()\n  })\n\n  it('should call onClear when clear button is clicked', async () => {\n    const setQueryMock = jest.fn()\n    const onClearMock = jest.fn()\n\n    const props: Props = {\n      ...instance(mockedProps),\n      useLiteActions: true,\n      onClear: onClearMock,\n    }\n\n    renderWithContext(props, {\n      query: 'test query',\n      setQuery: setQueryMock,\n    })\n\n    // Ensure we start with the query input populated\n    const queryInput = screen.getByTestId('monaco')\n    expect(queryInput).toHaveValue('test query')\n\n    // Find the clear button and click it\n    const clearButton = screen.getByTestId('btn-clear')\n    expect(clearButton).toBeInTheDocument()\n\n    fireEvent.click(clearButton)\n\n    // Verify that the onClear function was called and the query input is cleared\n    expect(setQueryMock).toHaveBeenCalled()\n    expect(onClearMock).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/Query.styles.ts",
    "content": "import { ComponentPropsWithRef } from 'react'\nimport styled, { css } from 'styled-components'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport const Wrapper = styled.div<ComponentPropsWithRef<'div'>>`\n  position: relative;\n  width: 100%;\n  height: 100%;\n\n  .editorBounder {\n    bottom: 6px;\n    left: 18px;\n    right: 46px;\n  }\n`\n\nexport const Container = styled(Col)<{\n  $disabled?: boolean\n}>`\n  padding: ${({ theme }) => theme.core.space.space200};\n  width: 100%;\n  height: 100%;\n  word-break: break-word;\n  text-align: left;\n  letter-spacing: 0;\n  background-color: ${({ theme }) => theme.components.card.bgColor};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.components.card.borderRadius};\n\n  ${({ $disabled }) =>\n    $disabled &&\n    css`\n      opacity: 0.8;\n    `}\n`\n\nexport const InputContainer = styled.div<ComponentPropsWithRef<'div'>>`\n  max-height: calc(100% - 32px);\n  flex-grow: 1;\n  width: 100%;\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  background-color: ${({ theme }) =>\n    theme.semantic.color.background.neutral300};\n\n  --monaco-color-bg: ${({ theme }) =>\n    theme.semantic.color.background.neutral300};\n`\n\nexport const QueryFooter = styled(Row).attrs({\n  align: 'center',\n  justify: 'between',\n})`\n  margin-top: ${({ theme }) => theme.core.space.space200};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx",
    "content": "import React, { useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport { MonacoLanguage } from 'uiSrc/constants'\nimport { CodeEditor } from 'uiSrc/components/base/code-editor'\nimport {\n  stopProcessing,\n  workbenchResultsSelector,\n} from 'uiSrc/slices/workbench/wb-results'\nimport DedicatedEditor from 'uiSrc/components/monaco-editor/components/dedicated-editor'\nimport {\n  QueryActions,\n  QueryTutorials,\n  QueryLiteActions,\n  useQueryEditorContext,\n  useCommandHistory,\n  useDslSyntax,\n  useQueryEditor,\n} from 'uiSrc/components/query'\nimport { aroundQuotesRegExp, options, TUTORIALS } from './constants'\nimport { Props } from './Query.types'\nimport * as S from './Query.styles'\n\nconst Query = (props: Props) => {\n  const {\n    activeMode,\n    resultsMode,\n    useLiteActions = false,\n    setQueryEl = () => {},\n    onKeyDown = () => {},\n    onQueryChangeMode = () => {},\n    onChangeGroupMode = () => {},\n    onClear = () => {},\n  } = props\n\n  const { monacoObjects, query, setQuery, isLoading, onSubmit } =\n    useQueryEditorContext()\n\n  const {\n    items: execHistoryItems,\n    loading,\n    processing,\n  } = useSelector(workbenchResultsSelector)\n\n  const input = useRef<HTMLDivElement>(null)\n  const dispatch = useDispatch()\n\n  // Command history\n  const { onQuickHistoryAccess, resetHistoryPos, isHistoryScrolled } =\n    useCommandHistory({\n      monacoObjects,\n      historyItems: execHistoryItems,\n    })\n\n  function handleSubmit(value?: string) {\n    resetHistoryPos()\n    onSubmit(value)\n  }\n\n  const handleClear = () => {\n    setQuery('')\n    onClear?.()\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    onKeyDown?.(e, query)\n  }\n\n  // DSL syntax widget (Workbench-only) — called before useQueryEditor\n  // so that dsl values are available synchronously in callbacks below.\n  const dsl = useDslSyntax({ monacoObjects })\n\n  // Shared editor lifecycle with Workbench-specific extensions.\n  const { editorDidMount, onChange } = useQueryEditor({\n    onSubmit: handleSubmit,\n\n    shouldTriggerParameterHints: () =>\n      !dsl.isDedicatedEditorOpenRef.current && !dsl.isWidgetOpen.current,\n\n    isDedicatedEditorOpen: () => dsl.isDedicatedEditorOpenRef.current ?? false,\n\n    beforeCursorChange: () => dsl.hideWidget(),\n\n    onSetup: (editor, monaco, completionsRef) => {\n      setQueryEl(editor)\n      dsl.setupDslCommands(editor, monaco)\n      completionsRef.setupSuggestionWidgetListener(editor)\n    },\n\n    onKeyDown: (e) => {\n      if (e.keyCode === monacoEditor.KeyCode.UpArrow) {\n        onQuickHistoryAccess()\n      }\n    },\n\n    onCursorChange: (e, command) => {\n      dsl.handleDslSyntax(e, command)\n    },\n\n    onQueryChange: (value) => {\n      if (value === '' && isHistoryScrolled()) {\n        resetHistoryPos()\n      }\n    },\n\n    onCleanup: () => dispatch(stopProcessing()),\n  })\n\n  const combinedIsLoading = isLoading || loading || processing\n\n  return (\n    <S.Wrapper>\n      <S.Container\n        $disabled={dsl.isDedicatedEditorOpen}\n        onKeyDown={handleKeyDown}\n        role=\"textbox\"\n        tabIndex={0}\n        data-testid=\"main-input-container-area\"\n      >\n        <S.InputContainer data-testid=\"query-input-container\" ref={input}>\n          <CodeEditor\n            language={MonacoLanguage.Redis as string}\n            value={query}\n            options={options}\n            className={`${MonacoLanguage.Redis}-editor`}\n            onChange={onChange}\n            editorDidMount={editorDidMount}\n          />\n        </S.InputContainer>\n        <S.QueryFooter>\n          {useLiteActions ? (\n            <QueryLiteActions\n              isLoading={combinedIsLoading}\n              onSubmit={handleSubmit}\n              onClear={handleClear}\n            />\n          ) : (\n            <>\n              <QueryTutorials\n                tutorials={TUTORIALS}\n                source=\"advanced_workbench_editor\"\n              />\n              <QueryActions\n                isLoading={combinedIsLoading}\n                activeMode={activeMode}\n                resultsMode={resultsMode}\n                onChangeGroupMode={onChangeGroupMode}\n                onChangeMode={onQueryChangeMode}\n                onSubmit={handleSubmit}\n              />\n            </>\n          )}\n        </S.QueryFooter>\n      </S.Container>\n      {dsl.isDedicatedEditorOpen && (\n        <DedicatedEditor\n          initialHeight={input?.current?.scrollHeight || 0}\n          langId={dsl.syntaxCommand.current.lang}\n          query={(dsl.selectedArg.current || '').replace(\n            aroundQuotesRegExp,\n            '',\n          )}\n          onSubmit={dsl.updateArgFromDedicatedEditor}\n          onCancel={dsl.onCancelDedicatedEditor}\n        />\n      )}\n    </S.Wrapper>\n  )\n}\n\nexport default React.memo(Query)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/Query.types.ts",
    "content": "import React from 'react'\n\nimport { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench'\n\nexport interface Props {\n  activeMode: RunQueryMode\n  resultsMode?: ResultsMode\n  useLiteActions?: boolean\n  setQueryEl?: Function\n  onKeyDown?: (e: React.KeyboardEvent, script: string) => void\n  onQueryChangeMode: () => void\n  onChangeGroupMode: () => void\n  onClear?: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/constants.ts",
    "content": "import { merge } from 'lodash'\nimport { defaultMonacoOptions, TutorialsIds } from 'uiSrc/constants'\n\nexport const aroundQuotesRegExp = /(^[\"']|[\"']$)/g\n\nexport const options = merge({}, defaultMonacoOptions, {\n  suggest: {\n    showWords: false,\n    showIcons: true,\n    insertMode: 'replace',\n    filterGraceful: false,\n    matchOnWordStartOnly: true,\n  },\n})\n\nexport const TUTORIALS = [\n  {\n    id: TutorialsIds.IntroToSearch,\n    title: 'Intro to search',\n  },\n  {\n    id: TutorialsIds.BasicRedisUseCases,\n    title: 'Basic use cases',\n  },\n  {\n    id: TutorialsIds.IntroVectorSearch,\n    title: 'Intro to vector search',\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/index.ts",
    "content": "import Query from './Query'\n\nexport default Query\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/Query/styles.module.scss",
    "content": ".wrapper {\n  position: relative;\n  height: 100%;\n\n  :global(.editorBounder) {\n    bottom: 6px;\n    left: 18px;\n    right: 46px;\n  }\n}\n\n.container {\n  display: flex;\n  flex-direction: column;\n  padding: 16px;\n  width: 100%;\n  height: 100%;\n  word-break: break-word;\n  text-align: left;\n  letter-spacing: 0;\n  background-color: var(--rsInputWrapperColor);\n  color: var(--euiTextSubduedColor) !important;\n  border: 1px solid var(--euiColorLightShade);\n  border-radius: var(--border-radius-medium);\n}\n\n.disabled {\n  opacity: 0.8;\n}\n\n.disabledActions {\n  pointer-events: none;\n  user-select: none;\n}\n\n.containerPlaceholder {\n  display: flex;\n  padding: 8px 16px 8px 16px;\n  width: 100%;\n  height: 100%;\n  background-color: var(--rsInputWrapperColor);\n  color: var(--euiTextSubduedColor) !important;\n  border: 1px solid var(--euiColorLightShade);\n  > div {\n    border: 1px solid var(--euiColorLightShade);\n    background-color: var(--euiColorEmptyShade);\n    padding: 8px 20px;\n    width: 100%;\n  }\n}\n\n.input {\n  // cannot use overflow since suggestions are absolute\n  max-height: calc(100% - 32px);\n  flex-grow: 1;\n  width: 100%;\n  border: 1px solid var(--euiColorLightShade);\n  background-color: var(--rsInputColor);\n}\n\n.queryFooter {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  margin-top: 16px;\n  flex-shrink: 0;\n}\n\n#script {\n  font: normal normal bold 14px/17px Inconsolata !important;\n  color: var(--textColorShade);\n  caret-color: var(--euiColorFullShade);\n  min-width: 5px;\n  display: inline;\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\nimport QueryWrapper from './QueryWrapper'\nimport { type Props } from './QueryWrapper.types'\n\nconst mockedProps = mock<Props>()\nconst redisCommandsPath = 'uiSrc/slices/app/redis-commands'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock(redisCommandsPath, () => {\n  const defaultState = jest.requireActual(redisCommandsPath).initialState\n  return {\n    ...jest.requireActual(redisCommandsPath),\n    appRedisCommandsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('QueryWrapper', () => {\n  it('should render', () => {\n    // connectedInstanceSelector.mockImplementation(() => ({\n    //   id: '123',\n    //   connectionType: 'CLUSTER',\n    // }));\n\n    // const sendCliClusterActionMock = jest.fn();\n\n    // sendCliClusterCommandAction.mockImplementation(() => sendCliClusterActionMock);\n\n    expect(render(<QueryWrapper {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  it('should call onClear callback when clear button is clicked', () => {\n    const props: Props = {\n      ...instance(mockedProps),\n      queryProps: {\n        useLiteActions: true,\n      },\n      query: 'test query',\n      onClear: jest.fn(),\n    }\n\n    render(<QueryWrapper {...props} />)\n\n    // Ensure we start with the query input populated\n    const queryInput = screen.getByTestId('monaco')\n    expect(queryInput).toHaveValue(props.query)\n\n    // Find the clear button and click it\n    const clearButton = screen.getByTestId('btn-clear')\n    fireEvent.click(clearButton)\n\n    // Verify that the onClear function was called and the query input is cleared\n    expect(props.onClear).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.stories.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport type { Meta, StoryObj, StoryContext } from '@storybook/react-vite'\nimport { useDispatch } from 'react-redux'\nimport { fn } from 'storybook/test'\n\nimport { MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport {\n  getRedisCommands,\n  getRedisCommandsSuccess,\n} from 'uiSrc/slices/app/redis-commands'\nimport MonacoEnvironmentInitializer from 'uiSrc/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer'\nimport MonacoLanguages from 'uiSrc/components/monaco-laguages'\nimport { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench'\n\nimport QueryWrapper from './QueryWrapper'\n\n/**\n * Decorator to initialize Monaco environment and Redis commands.\n * When `parameters.loadingState` is true, dispatches the loading action\n * instead of loading commands to simulate the loading state.\n */\nconst WithMonacoSetup = (Story: React.ComponentType, context: StoryContext) => {\n  const isLoading = context.parameters?.loadingState === true\n\n  const MonacoSetup = () => {\n    const dispatch = useDispatch()\n\n    useEffect(() => {\n      if (isLoading) {\n        dispatch(getRedisCommands())\n      } else {\n        // @ts-ignore - MOCK_COMMANDS_SPEC type differences are fine for Monaco\n        dispatch(getRedisCommandsSuccess(MOCK_COMMANDS_SPEC))\n      }\n    }, [dispatch])\n\n    return (\n      <div\n        style={{\n          width: '1000px',\n          height: '400px',\n          display: 'flex',\n          margin: '0 auto',\n        }}\n      >\n        <MonacoEnvironmentInitializer />\n        <MonacoLanguages />\n        <Story />\n      </div>\n    )\n  }\n\n  return <MonacoSetup />\n}\n\nconst meta: Meta<typeof QueryWrapper> = {\n  component: QueryWrapper,\n  tags: ['autodocs'],\n  decorators: [WithMonacoSetup],\n  args: {\n    setQueryEl: fn(),\n    onSubmit: fn(),\n    onQueryChangeMode: fn(),\n    onChangeGroupMode: fn(),\n    onClear: fn(),\n  },\n  parameters: {\n    layout: 'fullscreen',\n    docs: {\n      description: {\n        component:\n          'Workbench Query Editor with full autocomplete, command history, DSL syntax widget, raw/group mode toggle, and tutorials.',\n      },\n      story: {\n        // inline: false,\n        // iframeHeight: 400,\n      },\n    },\n  },\n}\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\n/**\n * Default empty editor with full actions (tutorials + raw/group/run).\n */\nexport const Default: Story = {\n  render: (args) => {\n    const [query, setQuery] = useState('')\n    return (\n      <QueryWrapper\n        {...args}\n        query={query}\n        setQuery={setQuery}\n        activeMode={RunQueryMode.ASCII}\n        resultsMode={ResultsMode.Default}\n      />\n    )\n  },\n}\n\n/**\n * Editor with lite actions (Run + Clear only, no tutorials/mode toggles).\n */\nexport const LiteActions: Story = {\n  name: 'Lite actions mode',\n  render: (args) => {\n    const [query, setQuery] = useState('')\n    return (\n      <QueryWrapper\n        {...args}\n        query={query}\n        setQuery={setQuery}\n        activeMode={RunQueryMode.ASCII}\n        queryProps={{ useLiteActions: true }}\n      />\n    )\n  },\n}\n\n/**\n * Editor pre-populated with a multi-line query.\n */\nexport const WithQuery: Story = {\n  name: 'With pre-filled query',\n  render: (args) => {\n    const [query, setQuery] = useState(\n      'FT.CREATE idx:bikes_vss ON HASH PREFIX 1 \"bikes:\"\\n' +\n        'SCHEMA \"model\" TEXT NOSTEM SORTABLE\\n' +\n        '\"brand\" TEXT NOSTEM SORTABLE\\n' +\n        '\"price\" NUMERIC SORTABLE',\n    )\n    return (\n      <QueryWrapper\n        {...args}\n        query={query}\n        setQuery={setQuery}\n        activeMode={RunQueryMode.ASCII}\n        resultsMode={ResultsMode.Default}\n      />\n    )\n  },\n}\n\n/**\n * Editor pre-populated with a GRAPH.QUERY that triggers the DSL syntax widget.\n * Place the cursor inside the quoted Cypher expression to see the\n * \"Use Cypher Editor  Shift+Space\" tooltip.\n */\nexport const DslSyntaxWidget: Story = {\n  name: 'DSL syntax widget (Cypher)',\n  render: (args) => {\n    const [query, setQuery] = useState('GRAPH.QUERY graph \"MATCH (n) RETURN n\"')\n    return (\n      <QueryWrapper\n        {...args}\n        query={query}\n        setQuery={setQuery}\n        activeMode={RunQueryMode.ASCII}\n        resultsMode={ResultsMode.Default}\n      />\n    )\n  },\n}\n\n/**\n * Loading state while Redis commands are being fetched.\n */\nexport const Loading: Story = {\n  parameters: {\n    loadingState: true,\n  },\n  render: (args) => (\n    <QueryWrapper {...args} query=\"\" activeMode={RunQueryMode.ASCII} />\n  ),\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport const ContainerPlaceholder = styled(Col)`\n  width: 100%;\n  height: 100%;\n  background-color: ${({ theme }) => theme.components.card.bgColor};\n  border: 1px solid ${({ theme }) => theme.semantic.color.border.neutral500};\n  border-radius: ${({ theme }) => theme.components.card.borderRadius};\n`\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx",
    "content": "import React, { useEffect, useMemo } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { LoadingContent } from 'uiSrc/components/base/layout'\nimport { IRedisCommand } from 'uiSrc/constants'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport {\n  fetchRedisearchListAction,\n  redisearchListSelector,\n} from 'uiSrc/slices/browser/redisearch'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands'\nimport SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json'\nimport {\n  QueryEditorContextProvider,\n  LoadingContainer,\n} from 'uiSrc/components/query'\n\nimport Query from './Query'\nimport { Props } from './QueryWrapper.types'\nimport * as S from './QueryWrapper.styles'\n\nconst QueryWrapper = (props: Props) => {\n  const {\n    query = '',\n    activeMode,\n    resultsMode,\n    setQuery = () => {},\n    setQueryEl,\n    onKeyDown,\n    onSubmit = () => {},\n    onQueryChangeMode,\n    onChangeGroupMode,\n    onClear,\n    queryProps = {},\n  } = props\n  const { loading: isCommandsLoading } = useSelector(appRedisCommandsSelector)\n  const { id: connectedInstanceId } = useSelector(connectedInstanceSelector)\n  const { data: indexes = [] } = useSelector(redisearchListSelector)\n  const { spec: COMMANDS_SPEC } = useSelector(appRedisCommandsSelector)\n\n  const REDIS_COMMANDS = useMemo(\n    () =>\n      mergeRedisCommandsSpecs(\n        COMMANDS_SPEC,\n        SEARCH_COMMANDS_SPEC,\n      ) as IRedisCommand[],\n    [COMMANDS_SPEC, SEARCH_COMMANDS_SPEC],\n  )\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    if (!connectedInstanceId) return\n\n    // fetch indexes\n    dispatch(fetchRedisearchListAction(undefined, undefined, false))\n  }, [connectedInstanceId])\n\n  if (isCommandsLoading) {\n    return (\n      <S.ContainerPlaceholder>\n        <LoadingContainer>\n          <LoadingContent lines={2} className=\"fluid\" />\n        </LoadingContainer>\n      </S.ContainerPlaceholder>\n    )\n  }\n\n  return (\n    <QueryEditorContextProvider\n      value={{\n        query,\n        setQuery,\n        commands: REDIS_COMMANDS,\n        indexes,\n        isLoading: false,\n        onSubmit,\n      }}\n    >\n      <Query\n        activeMode={activeMode}\n        resultsMode={resultsMode}\n        setQueryEl={setQueryEl}\n        onKeyDown={onKeyDown}\n        onQueryChangeMode={onQueryChangeMode}\n        onChangeGroupMode={onChangeGroupMode}\n        onClear={onClear}\n        {...queryProps}\n      />\n    </QueryEditorContextProvider>\n  )\n}\n\nexport default React.memo(QueryWrapper)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.types.ts",
    "content": "import React from 'react'\n\nimport { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench'\n\nimport { Props as BaseQueryProps } from './Query/Query.types'\n\ntype QueryProps = Pick<BaseQueryProps, 'useLiteActions'>\n\nexport interface Props {\n  query: string\n  activeMode: RunQueryMode\n  resultsMode?: ResultsMode\n  queryProps?: QueryProps\n  setQuery: (script: string) => void\n  setQueryEl: Function\n  onKeyDown?: (e: React.KeyboardEvent, script: string) => void\n  onSubmit: (value?: string) => void\n  onQueryChangeMode: () => void\n  onChangeGroupMode: () => void\n  onClear?: () => void\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/query/index.ts",
    "content": "import QueryWrapper from './QueryWrapper'\n\nexport default QueryWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-no-results-message/WbNoResultsMessage.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport WbNoResultsMessage from './WbNoResultsMessage'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    provider: 'REDIS_CLOUD',\n  }),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('WbNoResultsMessage', () => {\n  it('should render', () => {\n    expect(render(<WbNoResultsMessage />)).toBeTruthy()\n  })\n\n  it('should call proper actions after click on insight btn', () => {\n    render(<WbNoResultsMessage />)\n\n    fireEvent.click(screen.getByTestId('no-results-explore-btn'))\n\n    expect(store.getActions()).toEqual([\n      changeSelectedTab(InsightsPanelTabs.Explore),\n      changeSidePanel(SidePanels.Insights),\n    ])\n  })\n\n  it('should call proper telemetry events after click on insights', () => {\n    const sendEventTelemetryMock = jest.fn()\n    ;(sendEventTelemetry as jest.Mock).mockImplementation(\n      () => sendEventTelemetryMock,\n    )\n\n    render(<WbNoResultsMessage />)\n\n    fireEvent.click(screen.getByTestId('no-results-explore-btn'))\n\n    expect(sendEventTelemetry).toBeCalledWith({\n      event: TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      eventData: {\n        databaseId: 'instanceId',\n        provider: 'REDIS_CLOUD',\n        source: 'workbench',\n      },\n    })\n    ;(sendEventTelemetry as jest.Mock).mockRestore()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-no-results-message/WbNoResultsMessage.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\n\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport BulbImg from 'uiSrc/assets/img/workbench/bulb.svg'\nimport ArrowToGuidesIcon from 'uiSrc/assets/img/workbench/arrow-to-guides.svg?react'\n\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { PrimaryButton } from 'uiSrc/components/base/forms/buttons'\nimport { LightBulbIcon } from 'uiSrc/components/base/icons'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Card } from 'uiSrc/components/base/layout'\n\nimport styles from './styles.module.scss'\nimport { Panel } from 'uiSrc/components/panel'\n\nconst WbNoResultsMessage = () => {\n  const { provider } = useSelector(connectedInstanceSelector)\n\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const dispatch = useDispatch()\n\n  const handleOpenInsights = () => {\n    dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n    dispatch(changeSidePanel(SidePanels.Insights))\n\n    sendEventTelemetry({\n      event: TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      eventData: {\n        databaseId: instanceId,\n        provider,\n        source: 'workbench',\n      },\n    })\n  }\n\n  return (\n    <div className={styles.noResults} data-testid=\"wb_no-results\">\n      <Text\n        className={styles.noResultsTitle}\n        data-testid=\"wb_no-results__title\"\n      >\n        No results to display yet\n      </Text>\n      <Title style={{ marginTop: 12, fontSize: 28 }}>\n        This is our advanced CLI\n      </Title>\n      <Title style={{ marginTop: 6, fontSize: 20, lineHeight: 1.2 }}>\n        for Redis commands.\n      </Title>\n      <Spacer />\n\n      <Card className={styles.noResultsPanel}>\n        <ArrowToGuidesIcon className={styles.arrowToGuides} />\n        <Panel gap=\"m\" responsive>\n          <FlexItem>\n            <img\n              className={styles.noResultsIcon}\n              src={BulbImg}\n              alt=\"no results\"\n              data-testid=\"wb_no-results__icon\"\n            />\n          </FlexItem>\n          <FlexItem grow>\n            <Text\n              className={styles.noResultsText}\n              data-testid=\"wb_no-results__summary\"\n            >\n              Try Workbench with our interactive Tutorials to learn how Redis\n              can solve your use cases.\n            </Text>\n            <Spacer size=\"xl\" />\n            <div>\n              <PrimaryButton\n                icon={LightBulbIcon}\n                onClick={() => handleOpenInsights()}\n                className={styles.exploreBtn}\n                data-testid=\"no-results-explore-btn\"\n              >\n                Explore\n              </PrimaryButton>\n            </div>\n            <Spacer size=\"s\" />\n            <Text textAlign=\"left\" size=\"xs\">\n              Or click the icon in the top right corner.\n            </Text>\n          </FlexItem>\n        </Panel>\n      </Card>\n    </div>\n  )\n}\n\nexport default WbNoResultsMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-no-results-message/index.ts",
    "content": "import WbNoResultsMessage from './WbNoResultsMessage'\n\nexport default WbNoResultsMessage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-no-results-message/styles.module.scss",
    "content": ".noResults {\n  max-width: 540px;\n  height: 100%;\n  min-height: 400px;\n  display: flex;\n  margin: 0 auto;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n\n  .noResultsPanel {\n    width: 100%;\n    position: relative;\n\n    border-radius: 8px !important;\n    border-color: var(--separatorColor) !important;\n    background-color: var(--tableLightBorderColor) !important;\n  }\n}\n\n.noResultsTitle {\n  font: normal normal 400 12px/24px 'Graphik', sans-serif !important;\n}\n\n.noResultsText {\n  font: normal normal normal 14px/18px 'Graphik', sans-serif !important;\n  letter-spacing: -0.13px;\n  text-align: left;\n}\n\n.arrowToGuides {\n  fill: none !important;\n  position: absolute;\n  top: 0;\n  right: 0;\n  transform: translate(75%, -33%);\n}\n\n.noResultsIcon {\n  width: 94px !important;\n  height: auto !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'\nimport WBResults, { Props } from './WBResults'\nimport {\n  ViewMode,\n  ViewModeContextProvider,\n} from 'uiSrc/components/query/context/view-mode.context'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  sessionStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\nconst renderWBResultsComponent = (props: Partial<Props> = {}) => {\n  return render(\n    <ViewModeContextProvider viewMode={ViewMode.Workbench}>\n      <WBResults {...instance(mockedProps)} {...props} />\n    </ViewModeContextProvider>,\n    {\n      store,\n    },\n  )\n}\n\ndescribe('WBResults', () => {\n  it('should render', () => {\n    const { container } = renderWBResultsComponent()\n    expect(container).toBeTruthy()\n  })\n\n  it('should render NoResults component with empty items', () => {\n    const { getByTestId } = renderWBResultsComponent({\n      items: [],\n      isResultsLoaded: true,\n    })\n\n    expect(getByTestId('wb_no-results')).toBeInTheDocument()\n  })\n\n  it('should not render NoResults component with empty items and loading state', () => {\n    renderWBResultsComponent({\n      items: [],\n      isResultsLoaded: false,\n    })\n\n    expect(screen.queryByTestId('wb_no-results')).not.toBeInTheDocument()\n  })\n\n  it('should render with custom props', () => {\n    renderWBResultsComponent({\n      ...instance(mockedProps),\n      items: [],\n      isResultsLoaded: false,\n    })\n  })\n\n  it('should render with custom props', () => {\n    const itemsMock: CommandExecutionUI[] = [\n      {\n        id: '1',\n        command: 'query1',\n        result: [\n          {\n            response: 'data1',\n            status: CommandExecutionStatus.Success,\n          },\n        ],\n      },\n      {\n        id: '2',\n        command: 'query2',\n        result: [\n          {\n            response: 'data2',\n            status: CommandExecutionStatus.Success,\n          },\n        ],\n      },\n    ]\n\n    const { container } = renderWBResultsComponent({\n      ...instance(mockedProps),\n      items: itemsMock,\n      isResultsLoaded: true,\n    })\n\n    expect(container).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\n\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport { ProfileQueryType } from 'uiSrc/pages/workbench/constants'\nimport { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils/profile'\nimport { Nullable } from 'uiSrc/utils'\nimport { QueryCard } from 'uiSrc/components/query'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench'\n\nimport { EmptyButton } from 'uiSrc/components/base/forms/buttons'\nimport { DeleteIcon } from 'uiSrc/components/base/icons'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport WbNoResultsMessage from '../../wb-no-results-message'\n\nimport styles from './styles.module.scss'\n\n/** @deprecated Use QueryResultsProps from 'uiSrc/components/query/query-results' instead. */\nexport interface Props {\n  isResultsLoaded: boolean\n  items: CommandExecutionUI[]\n  clearing: boolean\n  processing: boolean\n  activeMode: RunQueryMode\n  activeResultsMode?: ResultsMode\n  scrollDivRef: React.Ref<HTMLDivElement>\n  onQueryReRun: (\n    query: string,\n    commandId?: Nullable<string>,\n    executeParams?: CodeButtonParams,\n  ) => void\n  onQueryDelete: (commandId: string) => void\n  onAllQueriesDelete: () => void\n  onQueryOpen: (commandId: string) => void\n  onQueryProfile: (\n    query: string,\n    commandId?: Nullable<string>,\n    executeParams?: CodeButtonParams,\n  ) => void\n}\n\n/** @deprecated Use QueryResults from 'uiSrc/components/query/query-results' instead. */\nconst WBResults = (props: Props) => {\n  const {\n    isResultsLoaded,\n    items = [],\n    clearing,\n    processing,\n    activeMode,\n    activeResultsMode,\n    onQueryReRun,\n    onQueryProfile,\n    onQueryDelete,\n    onAllQueriesDelete,\n    onQueryOpen,\n    scrollDivRef,\n  } = props\n\n  const handleQueryProfile = (\n    profileType: ProfileQueryType,\n    commandExecution: {\n      command: string\n      mode?: RunQueryMode\n      resultsMode?: ResultsMode\n    },\n  ) => {\n    const { command, mode, resultsMode } = commandExecution\n    const profileQuery = generateProfileQueryForCommand(command, profileType)\n    if (profileQuery) {\n      onQueryProfile(profileQuery, null, {\n        mode,\n        results: resultsMode,\n        clearEditor: false,\n      })\n    }\n  }\n\n  return (\n    <div className={styles.wrapper}>\n      {!isResultsLoaded && (\n        <ProgressBarLoader color=\"primary\" data-testid=\"progress-wb-history\" />\n      )}\n      {!!items?.length && (\n        <div className={styles.header}>\n          <EmptyButton\n            size=\"small\"\n            icon={DeleteIcon}\n            className={styles.clearAllBtn}\n            onClick={() => onAllQueriesDelete?.()}\n            disabled={clearing || processing}\n            data-testid=\"clear-history-btn\"\n          >\n            Clear Results\n          </EmptyButton>\n        </div>\n      )}\n      <div className={cx(styles.container)}>\n        <div ref={scrollDivRef} />\n        {items?.length\n          ? items.map(\n              ({\n                command = '',\n                isOpen = false,\n                result = undefined,\n                summary = undefined,\n                id = '',\n                loading,\n                createdAt,\n                mode,\n                resultsMode,\n                emptyCommand,\n                isNotStored,\n                executionTime,\n                db,\n              }) => (\n                <QueryCard\n                  id={id}\n                  key={id}\n                  isOpen={isOpen}\n                  result={result}\n                  summary={summary}\n                  clearing={clearing}\n                  loading={loading}\n                  command={command}\n                  createdAt={createdAt}\n                  activeMode={activeMode}\n                  emptyCommand={emptyCommand}\n                  isNotStored={isNotStored}\n                  executionTime={executionTime}\n                  mode={mode}\n                  activeResultsMode={activeResultsMode}\n                  resultsMode={resultsMode}\n                  db={db}\n                  onQueryOpen={() => onQueryOpen(id)}\n                  onQueryProfile={(profileType) =>\n                    handleQueryProfile(profileType, {\n                      command,\n                      mode,\n                      resultsMode,\n                    })\n                  }\n                  onQueryReRun={() =>\n                    onQueryReRun(command, null, {\n                      mode,\n                      results: resultsMode,\n                      clearEditor: false,\n                    })\n                  }\n                  onQueryDelete={() => onQueryDelete(id)}\n                />\n              ),\n            )\n          : null}\n        {isResultsLoaded && !items.length && <WbNoResultsMessage />}\n      </div>\n    </div>\n  )\n}\n\nexport default React.memo(WBResults)\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/index.ts",
    "content": "import WBResults from './WBResults'\n\nexport default WBResults\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/styles.module.scss",
    "content": ".wrapper {\n  flex: 1;\n  width: 100%;\n  height: 100%;\n  background-color: var(--euiColorEmptyShade);\n  border: 1px solid var(--euiColorLightShade);\n  border-radius: var(--border-radius-medium);\n\n  display: flex;\n  flex-direction: column;\n\n  position: relative;\n}\n\n.container {\n  @include eui.scrollBar;\n  color: var(--euiTextSubduedColor) !important;\n\n  flex: 1;\n  width: 100%;\n  overflow: auto;\n}\n\n.header {\n  height: 42px;\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  padding: 0 12px;\n\n  flex-shrink: 0;\n  border-bottom: 1px solid var(--tableDarkestBorderColor);\n}\n\n.clearAllBtn {\n  font-size: 14px !important;\n\n  :global {\n    .euiIcon {\n      width: 14px !important;\n      height: 14px !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\nimport { instance, mock } from 'ts-mockito'\n// import MonacoEditor from 'react-monaco-editor'\nimport { cleanup, fireEvent, mockedStore, render } from 'uiSrc/utils/test-utils'\nimport WBView, { Props } from './WBView'\n\nconst mockedProps = mock<Props>()\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\n// jest.mock('react-monaco-editor', () => jest.fn().mockImplementation(() =>\n//   <div data-testid=\"monaco\">Monaco</div>))\n\njest.mock('uiSrc/utils/workbench', () => ({\n  ...jest.requireActual('uiSrc/utils/workbench'),\n  updateWBHistoryStorage: jest.fn(),\n}))\n\ndescribe('WBView', () => {\n  it('should render', () => {\n    expect(render(<WBView {...instance(mockedProps)} />)).toBeTruthy()\n  })\n\n  describe('Workbench input keyboard cases', () => {\n    it.skip('\"Enter\" keydown should call \"onSubmit\"', () => {\n      const command = 'info'\n      const onSubmitMock = jest.fn()\n\n      const { queryByTestId } = render(\n        <WBView\n          {...instance(mockedProps)}\n          script={command}\n          onSubmit={onSubmitMock}\n        />,\n      )\n\n      const monacoEl = queryByTestId('monaco')\n\n      fireEvent.keyDown(monacoEl, {\n        code: 'Enter',\n        ctrlKey: true,\n      })\n\n      expect(onSubmitMock).toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx",
    "content": "import React, { Ref, useCallback, useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport cx from 'classnames'\nimport { isEmpty } from 'lodash'\nimport { useParams } from 'react-router-dom'\n\nimport {\n  Maybe,\n  Nullable,\n  getParsedParamsInQuery,\n  getCommandsFromQuery,\n} from 'uiSrc/utils'\nimport {\n  setWorkbenchVerticalPanelSizes,\n  appContextWorkbench,\n} from 'uiSrc/slices/app/context'\nimport { CommandExecutionUI } from 'uiSrc/slices/interfaces'\nimport { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench'\nimport { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'\nimport { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'\nimport { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { CodeButtonParams } from 'uiSrc/constants'\n\nimport {\n  ResizableContainer,\n  ResizablePanel,\n  ResizablePanelHandle,\n} from 'uiSrc/components/base/layout'\nimport { QueryResultsProvider } from 'uiSrc/components/query/context/query-results.context'\nimport { QueryResults } from 'uiSrc/components/query/query-results'\nimport { toggleOpenWBResult } from 'uiSrc/slices/workbench/wb-results'\nimport QueryWrapper from '../../query'\nimport { workbenchResultsTelemetry } from '../../../telemetry.constants'\nimport WbNoResultsMessage from '../../wb-no-results-message'\n\nimport styles from './styles.module.scss'\n\nconst verticalPanelIds = {\n  firstPanelId: 'scriptingArea',\n  secondPanelId: 'resultsArea',\n}\n\nexport interface Props {\n  script: string\n  items: CommandExecutionUI[]\n  clearing: boolean\n  processing: boolean\n  isResultsLoaded: boolean\n  setScript: (script: string) => void\n  setScriptEl: Function\n  scrollDivRef: Ref<HTMLDivElement>\n  activeMode: RunQueryMode\n  resultsMode: ResultsMode\n  onSubmit: (\n    query?: string,\n    commandId?: Nullable<string>,\n    executeParams?: CodeButtonParams,\n  ) => void\n  onQueryOpen: (commandId?: string) => void\n  onQueryDelete: (commandId: string) => void\n  onAllQueriesDelete: () => void\n  onQueryChangeMode: () => void\n  onChangeGroupMode: () => void\n}\n\ninterface IState {\n  activeMode: RunQueryMode\n  resultsMode?: ResultsMode\n}\n\nlet state: IState = {\n  activeMode: RunQueryMode.ASCII,\n  resultsMode: ResultsMode.Default,\n}\n\n/** @deprecated Use PageContent from 'pages/vector-search/pages/VectorSearchQueryPage/components/page-content' instead. */\nconst WBView = (props: Props) => {\n  const {\n    script = '',\n    items,\n    clearing,\n    processing,\n    setScript,\n    setScriptEl,\n    activeMode,\n    resultsMode,\n    isResultsLoaded,\n    onSubmit,\n    onQueryOpen,\n    onQueryDelete,\n    onAllQueriesDelete,\n    onQueryChangeMode,\n    onChangeGroupMode,\n    scrollDivRef,\n  } = props\n\n  state = {\n    activeMode,\n    resultsMode,\n  }\n\n  const { instanceId = '' } = useParams<{ instanceId: string }>()\n  const { panelSizes } = useSelector(appContextWorkbench)\n  const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(\n    appRedisCommandsSelector,\n  )\n  const { batchSize = PIPELINE_COUNT_DEFAULT } =\n    useSelector(userSettingsConfigSelector) ?? {}\n  const verticalPanelSizesRef = useRef(panelSizes)\n\n  const dispatch = useDispatch()\n\n  useEffect(\n    () => () => {\n      dispatch(setWorkbenchVerticalPanelSizes(verticalPanelSizesRef.current))\n    },\n    [],\n  )\n\n  const onVerticalPanelWidthChange = useCallback((newSizes: any) => {\n    verticalPanelSizesRef.current = newSizes\n  }, [])\n\n  const handleToggleOpen = (id: string, isOpen: boolean) => {\n    dispatch(toggleOpenWBResult(id))\n\n    const item = items.find((i) => i.id === id)\n    if (isOpen && !item?.result) {\n      onQueryOpen(id)\n    }\n  }\n\n  const handleSubmit = (value?: string) => {\n    sendEventSubmitTelemetry(TelemetryEvent.WORKBENCH_COMMAND_SUBMITTED, value)\n    onSubmit(value)\n  }\n\n  const handleReRun = (\n    query?: string,\n    commandId?: Nullable<string>,\n    executeParams: CodeButtonParams = {},\n  ) => {\n    sendEventSubmitTelemetry(\n      TelemetryEvent.WORKBENCH_COMMAND_RUN_AGAIN,\n      query,\n      executeParams,\n    )\n    onSubmit(query, commandId, executeParams)\n  }\n\n  const handleProfile = (\n    query?: string,\n    commandId?: Nullable<string>,\n    executeParams: CodeButtonParams = {},\n  ) => {\n    sendEventSubmitTelemetry(\n      TelemetryEvent.WORKBENCH_COMMAND_PROFILE,\n      query,\n      executeParams,\n    )\n    onSubmit(query, commandId, executeParams)\n  }\n\n  const sendEventSubmitTelemetry = (\n    event: TelemetryEvent,\n    commandInit = script,\n    executeParams?: CodeButtonParams,\n  ) => {\n    const eventData = (() => {\n      const parsedParams: Maybe<CodeButtonParams> = isEmpty(executeParams)\n        ? getParsedParamsInQuery(commandInit)\n        : executeParams\n\n      const command =\n        getCommandsFromQuery(commandInit, REDIS_COMMANDS_ARRAY) || ''\n      const pipeline =\n        TelemetryEvent.WORKBENCH_COMMAND_RUN_AGAIN !== event\n          ? (parsedParams?.pipeline || batchSize) > 1\n          : undefined\n      const isMultiple = command.includes(';')\n\n      return {\n        command: command?.toUpperCase(),\n        pipeline,\n        databaseId: instanceId,\n        multiple: isMultiple ? 'Multiple' : 'Single',\n        rawMode:\n          (parsedParams?.mode?.toUpperCase() || state.activeMode) ===\n          RunQueryMode.Raw,\n        results: ResultsMode.GroupMode.startsWith?.(\n          parsedParams?.results?.toUpperCase() || state.resultsMode || 'GROUP',\n        )\n          ? 'group'\n          : parsedParams?.results?.toLowerCase() === 'silent'\n            ? 'silent'\n            : 'single',\n      }\n    })()\n\n    if (eventData.command) {\n      sendEventTelemetry({\n        event,\n        eventData,\n      })\n    }\n  }\n\n  return (\n    <div className={cx('workbenchPage', styles.container)}>\n      <div className={styles.main}>\n        <div className={styles.content}>\n          <ResizableContainer\n            onLayout={onVerticalPanelWidthChange}\n            direction=\"vertical\"\n          >\n            <ResizablePanel\n              id={verticalPanelIds.firstPanelId}\n              minSize={30}\n              className={styles.queryPanel}\n              defaultSize={panelSizes && panelSizes[0] ? panelSizes[0] : 20}\n            >\n              <QueryWrapper\n                query={script}\n                activeMode={activeMode}\n                resultsMode={resultsMode}\n                setQuery={setScript}\n                setQueryEl={setScriptEl}\n                onSubmit={handleSubmit}\n                onQueryChangeMode={onQueryChangeMode}\n                onChangeGroupMode={onChangeGroupMode}\n              />\n            </ResizablePanel>\n\n            <ResizablePanelHandle\n              direction=\"horizontal\"\n              data-test-subj=\"resize-btn-scripting-area-and-results\"\n            />\n\n            <ResizablePanel\n              id={verticalPanelIds.secondPanelId}\n              minSize={10}\n              maxSize={70}\n              defaultSize={panelSizes && panelSizes[1] ? panelSizes[1] : 80}\n              className={cx(styles.queryResults, styles.queryResultsPanel)}\n            >\n              <QueryResultsProvider telemetry={workbenchResultsTelemetry}>\n                <QueryResults\n                  items={items}\n                  clearing={clearing}\n                  processing={processing}\n                  isResultsLoaded={isResultsLoaded}\n                  activeMode={activeMode}\n                  activeResultsMode={resultsMode}\n                  scrollDivRef={scrollDivRef}\n                  onToggleOpen={handleToggleOpen}\n                  onQueryReRun={handleReRun}\n                  onQueryProfile={handleProfile}\n                  onQueryDelete={onQueryDelete}\n                  onAllQueriesDelete={onAllQueriesDelete}\n                  noResultsPlaceholder={<WbNoResultsMessage />}\n                />\n              </QueryResultsProvider>\n            </ResizablePanel>\n          </ResizableContainer>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport default WBView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/WBView/index.ts",
    "content": "import WBView from './WBView'\n\nexport default WBView\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss",
    "content": ".container {\n  display: flex;\n  flex-grow: 1;\n  flex-direction: column;\n  max-height: 100%;\n}\n\n.main {\n  display: flex;\n  flex: 1;\n  padding: 0 16px 0;\n  height: 100%;\n  width: 100%;\n}\n\n.content {\n  display: flex;\n  flex-grow: 1;\n  width: 100%;\n}\n\n:global(.show-cli) {\n  .queryResults {\n    & > div {\n      border-bottom-width: 1px;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport React from 'react'\n\nimport {\n  cleanup,\n  clearStoreActions,\n  fireEvent,\n  mockedStore,\n  render,\n  screen,\n  act,\n} from 'uiSrc/utils/test-utils'\nimport QueryWrapper from 'uiSrc/pages/workbench/components/query'\nimport { type Props as QueryProps } from 'uiSrc/pages/workbench/components/query/QueryWrapper.types'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\nimport {\n  clearWbResults,\n  loadWBHistory,\n  processWBCommand,\n  sendWBCommandAction,\n  workbenchResultsSelector,\n} from 'uiSrc/slices/workbench/wb-results'\n\nimport WBViewWrapper from './WBViewWrapper'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/pages/workbench/components/query', () => ({\n  __esModule: true,\n  namedExport: jest.fn(),\n  default: jest.fn(),\n}))\n\nconst QueryWrapperMock = (props: QueryProps) => (\n  <div\n    onKeyDown={(e: any) => props.onKeyDown?.(e, 'get')}\n    data-testid=\"query\"\n    aria-label=\"query\"\n    role=\"textbox\"\n    tabIndex={0}\n  />\n)\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\njest.mock('uiSrc/slices/instances/instances', () => ({\n  ...jest.requireActual('uiSrc/slices/instances/instances'),\n  connectedInstanceSelector: jest.fn().mockReturnValue({\n    id: '123',\n    connectionType: 'STANDALONE',\n  }),\n}))\n\njest.mock('uiSrc/slices/app/plugins', () => ({\n  ...jest.requireActual('uiSrc/slices/app/plugins'),\n  appPluginsSelector: jest.fn().mockReturnValue({\n    visualizations: [],\n  }),\n}))\n\njest.mock('uiSrc/slices/workbench/wb-results', () => ({\n  ...jest.requireActual('uiSrc/slices/workbench/wb-results'),\n  sendWBCommandClusterAction: jest.fn(),\n  processUnsupportedCommand: jest.fn(),\n  updateCliCommandHistory: jest.fn,\n  workbenchResultsSelector: jest.fn().mockReturnValue({\n    loading: false,\n    items: [],\n  }),\n}))\n\njest.mock('uiSrc/slices/workbench/wb-tutorials', () => {\n  const defaultState = jest.requireActual(\n    'uiSrc/slices/workbench/wb-tutorials',\n  ).initialState\n  return {\n    ...jest.requireActual('uiSrc/slices/workbench/wb-tutorials'),\n    workbenchTutorialsSelector: jest.fn().mockReturnValue({\n      ...defaultState,\n    }),\n  }\n})\n\ndescribe('WBViewWrapper', () => {\n  beforeAll(() => {\n    QueryWrapper.mockImplementation(QueryWrapperMock)\n  })\n\n  it('should render', () => {\n    expect(render(<WBViewWrapper />)).toBeTruthy()\n  })\n\n  it('should render with SessionStorage', () => {\n    render(<WBViewWrapper />)\n\n    const expectedActions = [loadWBHistory()]\n    expect(\n      clearStoreActions(store.getActions().slice(0, expectedActions.length)),\n    ).toEqual(clearStoreActions(expectedActions))\n  })\n\n  it('should call delete command', () => {\n    ;(workbenchResultsSelector as jest.Mock).mockImplementation(() => ({\n      items: [{ id: '1' }],\n    }))\n    render(<WBViewWrapper />)\n\n    fireEvent.click(screen.getByTestId('delete-command'))\n    expect(clearStoreActions(store.getActions().slice(-1))).toEqual(\n      clearStoreActions([processWBCommand('1')]),\n    )\n  })\n\n  it('should call delete all command', () => {\n    ;(workbenchResultsSelector as jest.Mock).mockImplementation(() => ({\n      items: [{ id: '1' }],\n    }))\n    render(<WBViewWrapper />)\n\n    fireEvent.click(screen.getByTestId('clear-history-btn'))\n    expect(clearStoreActions(store.getActions().slice(-1))).toEqual(\n      clearStoreActions([clearWbResults()]),\n    )\n  })\n\n  it('should be disabled button when commands are processing', () => {\n    ;(workbenchResultsSelector as jest.Mock).mockImplementation(() => ({\n      items: [{ id: '1' }],\n      processing: true,\n    }))\n    render(<WBViewWrapper />)\n\n    expect(screen.getByTestId('clear-history-btn')).toBeDisabled()\n  })\n\n  it('should not display clear results when with empty history', () => {\n    ;(workbenchResultsSelector as jest.Mock).mockImplementation(() => ({\n      items: [],\n    }))\n    render(<WBViewWrapper />)\n\n    expect(screen.queryByTestId('clear-history-btn')).not.toBeInTheDocument()\n  })\n\n  it.skip('\"onSubmit\" for Cluster connection should call \"sendWBCommandClusterAction\"', async () => {\n    ;(connectedInstanceSelector as jest.Mock).mockImplementation(() => ({\n      id: '123',\n      connectionType: 'CLUSTER',\n    }))\n\n    const sendWBCommandClusterActionMock = jest.fn()\n\n    ;(sendWBCommandAction as jest.Mock).mockImplementation(\n      () => sendWBCommandClusterActionMock,\n    )\n\n    const { queryAllByTestId } = render(<WBViewWrapper />)\n\n    // Act\n    await act(() => {\n      fireEvent.click(queryAllByTestId(/preselect-/)[0])\n    })\n\n    const monacoEl = screen.getByTestId('query')\n\n    fireEvent.keyDown(monacoEl, {\n      code: 'Enter',\n      ctrlKey: true,\n    })\n\n    expect(sendWBCommandClusterActionMock).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx",
    "content": "import React, { Ref, useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { useParams } from 'react-router-dom'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\n\nimport {\n  getMonacoLines,\n  getParsedParamsInQuery,\n  isGroupMode,\n  Maybe,\n  Nullable,\n  scrollIntoView,\n} from 'uiSrc/utils'\nimport {\n  changeActiveRunQueryMode,\n  changeResultsMode,\n  clearWbResultsAction,\n  deleteWBCommandAction,\n  fetchWBCommandAction,\n  fetchWBHistoryAction,\n  resetWBHistoryItems,\n  sendWbQueryAction,\n  workbenchResultsSelector,\n} from 'uiSrc/slices/workbench/wb-results'\nimport { Instance, IPluginVisualization } from 'uiSrc/slices/interfaces'\nimport {\n  connectedInstanceSelector,\n  initialState as instanceInitState,\n} from 'uiSrc/slices/instances/instances'\nimport { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench'\nimport {\n  cliSettingsSelector,\n  fetchBlockingCliCommandsAction,\n} from 'uiSrc/slices/cli/cli-settings'\nimport {\n  appContextWorkbench,\n  setWorkbenchScript,\n} from 'uiSrc/slices/app/context'\nimport { appPluginsSelector } from 'uiSrc/slices/app/plugins'\nimport { userSettingsWBSelector } from 'uiSrc/slices/user/user-settings'\nimport { CodeButtonParams } from 'uiSrc/constants'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { incrementOnboardStepAction } from 'uiSrc/slices/app/features'\n\nimport { OnboardingStepName, OnboardingSteps } from 'uiSrc/constants/onboarding'\n\nimport {\n  changeSelectedTab,\n  changeSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\nimport WBView from './WBView'\n\ninterface IState {\n  loading: boolean\n  instance: Instance\n  unsupportedCommands: string[]\n  blockingCommands: string[]\n  visualizations: IPluginVisualization[]\n  scriptEl: Nullable<monacoEditor.editor.IStandaloneCodeEditor>\n}\n\nlet state: IState = {\n  loading: false,\n  instance: instanceInitState?.connectedInstance,\n  unsupportedCommands: [],\n  blockingCommands: [],\n  visualizations: [],\n  scriptEl: null,\n}\n\n/** @deprecated Use useQuery hook from 'pages/vector-search/pages/VectorSearchQueryPage/hooks/useQuery' instead. */\nconst WBViewWrapper = () => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n\n  const {\n    isLoaded,\n    loading,\n    items,\n    clearing,\n    processing,\n    activeRunQueryMode,\n    resultsMode,\n  } = useSelector(workbenchResultsSelector)\n  const { unsupportedCommands, blockingCommands } =\n    useSelector(cliSettingsSelector)\n  const { cleanup: cleanupWB } = useSelector(userSettingsWBSelector)\n  const { script: scriptContext } = useSelector(appContextWorkbench)\n\n  const [script, setScript] = useState(scriptContext)\n  const [scriptEl, setScriptEl] =\n    useState<Nullable<monacoEditor.editor.IStandaloneCodeEditor>>(null)\n\n  const instance = useSelector(connectedInstanceSelector)\n  const { visualizations = [] } = useSelector(appPluginsSelector)\n  state = {\n    scriptEl,\n    loading,\n    instance,\n    blockingCommands,\n    unsupportedCommands,\n    visualizations,\n  }\n  const scrollDivRef: Ref<HTMLDivElement> = useRef(null)\n  const scriptRef = useRef(script)\n\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(fetchWBHistoryAction(instanceId))\n\n    return () => {\n      dispatch(resetWBHistoryItems())\n      dispatch(setWorkbenchScript(scriptRef.current))\n    }\n  }, [])\n\n  useEffect(() => {\n    scriptRef.current = script\n  }, [script])\n\n  useEffect(() => {\n    if (scriptContext) {\n      setScript(scriptContext)\n      setTimeout(() => {\n        scriptEl?.setSelection(new monacoEditor.Selection(0, 0, 0, 0))\n      }, 0)\n      dispatch(setWorkbenchScript(''))\n    }\n  }, [scriptContext])\n\n  useEffect(() => {\n    if (!blockingCommands.length) {\n      dispatch(fetchBlockingCliCommandsAction())\n    }\n  }, [blockingCommands])\n\n  const handleChangeQueryRunMode = () => {\n    dispatch(\n      changeActiveRunQueryMode(\n        activeRunQueryMode === RunQueryMode.ASCII\n          ? RunQueryMode.Raw\n          : RunQueryMode.ASCII,\n      ),\n    )\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_MODE_CHANGED,\n      eventData: {\n        databaseId: instanceId,\n        changedFromMode: activeRunQueryMode,\n        changedToMode:\n          activeRunQueryMode === RunQueryMode.ASCII\n            ? RunQueryMode.Raw\n            : RunQueryMode.ASCII,\n      },\n    })\n  }\n\n  const handleChangeGroupMode = () => {\n    dispatch(\n      changeResultsMode(\n        isGroupMode(resultsMode) ? ResultsMode.Default : ResultsMode.GroupMode,\n      ),\n    )\n  }\n\n  const updateOnboardingOnSubmit = () =>\n    dispatch(\n      incrementOnboardStepAction(\n        OnboardingSteps.WorkbenchPage,\n        undefined,\n        () => {\n          dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n          dispatch(changeSidePanel(SidePanels.Insights))\n          sendEventTelemetry({\n            event: TelemetryEvent.ONBOARDING_TOUR_ACTION_MADE,\n            eventData: {\n              databaseId: instanceId,\n              step: OnboardingStepName.WorkbenchIntro,\n            },\n          })\n        },\n      ),\n    )\n\n  const handleSubmit = (\n    commandInit: string = script,\n    commandId?: Nullable<string>,\n    executeParams: CodeButtonParams = {},\n  ) => {\n    if (!commandInit?.length) return\n\n    dispatch(\n      sendWbQueryAction(commandInit, commandId, executeParams, {\n        afterEach: () => {\n          const isNewCommand = !commandId\n          isNewCommand && scrollResults('start')\n        },\n        afterAll: updateOnboardingOnSubmit,\n      }),\n    )\n  }\n\n  const scrollResults = (inline: ScrollLogicalPosition = 'start') => {\n    requestAnimationFrame(() => {\n      scrollIntoView(scrollDivRef?.current, {\n        behavior: 'smooth',\n        block: 'nearest',\n        inline,\n      })\n    })\n  }\n\n  const handleQueryDelete = (commandId: string) => {\n    dispatch(deleteWBCommandAction(commandId))\n  }\n\n  const handleAllQueriesDelete = () => {\n    dispatch(clearWbResultsAction())\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_CLEAR_ALL_RESULTS_CLICKED,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n  }\n\n  const handleQueryOpen = (commandId: string = '') => {\n    dispatch(fetchWBCommandAction(commandId))\n  }\n\n  const resetCommand = () => {\n    state?.scriptEl?.getAction('editor.action.insertLineAfter')?.run() // HACK: to reset completion snippets\n    setScript('')\n  }\n\n  const sourceValueSubmit = (\n    value: string = script,\n    commandId?: Nullable<string>,\n    executeParams: CodeButtonParams = { clearEditor: true },\n  ) => {\n    if (state.loading || (!value && !script)) return\n\n    const lines = getMonacoLines(value)\n    const parsedParams: Maybe<CodeButtonParams> = getParsedParamsInQuery(value)\n\n    const { clearEditor } = executeParams\n    handleSubmit(value, commandId, { ...executeParams, ...parsedParams })\n\n    if (cleanupWB && clearEditor && lines.length) {\n      resetCommand()\n    }\n  }\n\n  return (\n    <WBView\n      items={items}\n      clearing={clearing}\n      processing={processing}\n      isResultsLoaded={isLoaded}\n      script={script}\n      setScript={setScript}\n      setScriptEl={setScriptEl}\n      scrollDivRef={scrollDivRef}\n      activeMode={activeRunQueryMode}\n      onSubmit={sourceValueSubmit}\n      onQueryOpen={handleQueryOpen}\n      onQueryDelete={handleQueryDelete}\n      onAllQueriesDelete={handleAllQueriesDelete}\n      onQueryChangeMode={handleChangeQueryRunMode}\n      resultsMode={resultsMode}\n      onChangeGroupMode={handleChangeGroupMode}\n    />\n  )\n}\n\nexport default WBViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/components/wb-view/index.ts",
    "content": "import WBViewWrapper from './WBViewWrapper'\n\nexport default WBViewWrapper\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/constants.ts",
    "content": "import { AllIconsType } from 'uiSrc/components/base/icons/RiIcon'\n\nexport const WORKBENCH_HISTORY_WRAPPER_NAME = 'WORKBENCH'\nexport const WORKBENCH_HISTORY_MAX_LENGTH = 30\n\nexport enum WBQueryType {\n  Text = 'Text',\n  Plugin = 'Plugin',\n}\n\nexport const DEFAULT_TEXT_VIEW_TYPE = {\n  id: 'default__Text',\n  text: 'Text',\n  name: 'default__Text',\n  value: WBQueryType.Text,\n  iconDark: 'TextViewIconDarkIcon' as AllIconsType,\n  iconLight: 'TextViewIconLightIcon' as AllIconsType,\n  internal: true,\n}\n\nexport const VIEW_TYPE_OPTIONS = [DEFAULT_TEXT_VIEW_TYPE]\n\nexport const getViewTypeOptions = () => [...VIEW_TYPE_OPTIONS]\n\nexport const SEARCH_COMMANDS = ['ft.search', 'ft.aggregate']\nexport const GRAPH_COMMANDS = ['graph.query']\n\nconst ALLOWED_PROFILE_COMMANDS = [...SEARCH_COMMANDS, ...GRAPH_COMMANDS]\n\nexport const isCommandAllowedForProfile = (query: string) =>\n  ALLOWED_PROFILE_COMMANDS.includes(query?.split(' ')?.[0]?.toLowerCase())\n\nexport enum ProfileQueryType {\n  Profile = 'Profile',\n  Explain = 'Explain',\n}\n\nconst PROFILE_VIEW_TYPE_OPTIONS = [\n  {\n    id: ProfileQueryType.Profile,\n    text: 'Profile the command',\n    name: 'Profile',\n    value: WBQueryType.Text,\n  },\n  {\n    id: ProfileQueryType.Explain,\n    text: 'Explain the command',\n    name: 'Explain',\n    value: WBQueryType.Text,\n  },\n]\n\nexport const getProfileViewTypeOptions = () => [...PROFILE_VIEW_TYPE_OPTIONS]\n\nexport enum ModuleCommandPrefix {\n  RediSearch = 'FT.',\n  JSON = 'JSON.',\n  TimeSeries = 'TS.',\n  Graph = 'GRAPH.',\n  BF = 'BF.',\n  CF = 'CF.',\n  CMS = 'CMS.',\n  TOPK = 'TOPK.',\n  TDIGEST = 'TDIGEST.',\n}\n\nexport const COMMANDS_TO_GET_INDEX_INFO = [\n  'FT.SEARCH',\n  'FT.AGGREGATE',\n  'FT.EXPLAIN',\n  'FT.EXPLAINCLI',\n  'FT.PROFILE',\n  'FT.SPELLCHECK',\n  'FT.TAGVALS',\n  'FT.ALTER',\n]\n\nexport const COMMANDS_WITHOUT_INDEX_PROPOSE = ['FT.CREATE']\n\nexport const COMPOSITE_ARGS = ['LOAD *']\n\nexport enum DefinedArgumentName {\n  index = 'index',\n  query = 'query',\n  field = 'field',\n  expression = 'expression',\n}\n\nexport const FIELD_START_SYMBOL = '@'\nexport enum EmptySuggestionsIds {\n  NoIndexes = 'no-indexes',\n}\n\nexport const SORTED_SEARCH_COMMANDS = [\n  'FT.SEARCH',\n  'FT.AGGREGATE',\n  'FT.CREATE',\n  'FT.EXPLAIN',\n  'FT.PROFILE',\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/contexts/enablementAreaContext.tsx",
    "content": "import React from 'react'\nimport { CodeButtonParams } from 'uiSrc/constants/workbench'\nimport { Nullable } from 'uiSrc/utils'\n\ninterface IContext {\n  setScript: (\n    script: string,\n    params?: CodeButtonParams,\n    onFinish?: () => void,\n  ) => void\n  openPage: (page: IInternalPage, fromUser?: boolean) => void\n  isCodeBtnDisabled?: boolean\n}\nexport interface IInternalPage {\n  path: string\n  manifestPath?: Nullable<string>\n  label?: string\n}\nexport const defaultValue = {\n  setScript: (script: string) => script,\n  openPage: (page: IInternalPage) => page,\n  isCodeBtnDisabled: false,\n}\nconst EnablementAreaContext = React.createContext<IContext>(defaultValue)\nexport const EnablementAreaProvider = EnablementAreaContext.Provider\nexport default EnablementAreaContext\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/data/supported_commands.json",
    "content": "{\n  \"FT.AGGREGATE\": {\n    \"summary\": \"Run a search query on an index and perform aggregate transformations on the results\",\n    \"complexity\": \"O(1)\",\n    \"arguments\": [\n      {\n        \"name\": \"index\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"query\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"verbatim\",\n        \"type\": \"pure-token\",\n        \"token\": \"VERBATIM\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"load\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"count\",\n            \"type\": \"string\",\n            \"token\": \"LOAD\"\n          },\n          {\n            \"name\": \"field\",\n            \"type\": \"string\",\n            \"multiple\": true\n          }\n        ]\n      },\n      {\n        \"name\": \"timeout\",\n        \"type\": \"integer\",\n        \"optional\": true,\n        \"token\": \"TIMEOUT\"\n      },\n      {\n        \"name\": \"loadall\",\n        \"type\": \"pure-token\",\n        \"token\": \"LOAD *\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"groupby\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"multiple\": true,\n        \"arguments\": [\n          {\n            \"name\": \"nargs\",\n            \"type\": \"integer\",\n            \"token\": \"GROUPBY\"\n          },\n          {\n            \"name\": \"property\",\n            \"type\": \"string\",\n            \"multiple\": true\n          },\n          {\n            \"name\": \"reduce\",\n            \"type\": \"block\",\n            \"optional\": true,\n            \"multiple\": true,\n            \"arguments\": [\n              {\n                \"name\": \"reduce\",\n                \"token\": \"REDUCE\",\n                \"type\": \"pure-token\"\n              },\n              {\n                \"name\": \"function\",\n                \"type\": \"oneof\",\n                \"arguments\": [\n                  {\n                    \"name\": \"count\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"COUNT\"\n                  },\n                  {\n                    \"name\": \"count_distinct\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"COUNT_DISTINCT\"\n                  },\n                  {\n                    \"name\": \"count_distinctish\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"COUNT_DISTINCTISH\"\n                  },\n                  {\n                    \"name\": \"sum\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"SUM\"\n                  },\n                  {\n                    \"name\": \"min\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"MIN\"\n                  },\n                  {\n                    \"name\": \"max\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"MAX\"\n                  },\n                  {\n                    \"name\": \"avg\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"AVG\"\n                  },\n                  {\n                    \"name\": \"stddev\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"STDDEV\"\n                  },\n                  {\n                    \"name\": \"quantile\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"QUANTILE\"\n                  },\n                  {\n                    \"name\": \"tolist\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"TOLIST\"\n                  },\n                  {\n                    \"name\": \"first_value\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"FIRST_VALUE\"\n                  },\n                  {\n                    \"name\": \"random_sample\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"RANDOM_SAMPLE\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"nargs\",\n                \"type\": \"integer\"\n              },\n              {\n                \"name\": \"arg\",\n                \"type\": \"string\",\n                \"multiple\": true\n              },\n              {\n                \"name\": \"name\",\n                \"type\": \"string\",\n                \"token\": \"AS\",\n                \"optional\": true\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"sortby\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"nargs\",\n            \"type\": \"integer\",\n            \"token\": \"SORTBY\"\n          },\n          {\n            \"name\": \"fields\",\n            \"type\": \"block\",\n            \"optional\": true,\n            \"multiple\": true,\n            \"arguments\": [\n              {\n                \"name\": \"property\",\n                \"type\": \"string\"\n              },\n              {\n                \"name\": \"order\",\n                \"type\": \"oneof\",\n                \"arguments\": [\n                  {\n                    \"name\": \"asc\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"ASC\"\n                  },\n                  {\n                    \"name\": \"desc\",\n                    \"type\": \"pure-token\",\n                    \"token\": \"DESC\"\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"name\": \"num\",\n            \"type\": \"integer\",\n            \"token\": \"MAX\",\n            \"optional\": true\n          }\n        ]\n      },\n      {\n        \"name\": \"apply\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"multiple\": true,\n        \"arguments\": [\n          {\n            \"name\": \"expression\",\n            \"type\": \"string\",\n            \"expression\": true,\n            \"token\": \"APPLY\",\n            \"arguments\": [\n              {\n                \"name\": \"exists\",\n                \"token\": \"exists\",\n                \"type\": \"function\",\n                \"summary\": \"Checks whether a field exists in a document.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"log\",\n                \"token\": \"log\",\n                \"type\": \"function\",\n                \"summary\": \"Return the logarithm of a number, property or subexpression\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"abs\",\n                \"token\": \"abs\",\n                \"type\": \"function\",\n                \"summary\": \"Return the absolute number of a numeric expression\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"ceil\",\n                \"token\": \"ceil\",\n                \"type\": \"function\",\n                \"summary\": \"Round to the smallest value not less than x\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"floor\",\n                \"token\": \"floor\",\n                \"type\": \"function\",\n                \"summary\": \"Round to largest value not greater than x\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"log2\",\n                \"token\": \"log2\",\n                \"type\": \"function\",\n                \"summary\": \"Return the logarithm of x to base 2\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"exp\",\n                \"token\": \"exp\",\n                \"type\": \"function\",\n                \"summary\": \"Return the exponent of x, e.g., e^x\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"sqrt\",\n                \"token\": \"sqrt\",\n                \"type\": \"function\",\n                \"summary\": \"Return the square root of x\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"upper\",\n                \"token\": \"upper\",\n                \"type\": \"function\",\n                \"summary\": \"Return the uppercase conversion of s\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"lower\",\n                \"token\": \"lower\",\n                \"type\": \"function\",\n                \"summary\": \"Return the lowercase conversion of s\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"startswith\",\n                \"token\": \"startswith\",\n                \"type\": \"function\",\n                \"summary\": \"Return 1 if s2 is the prefix of s1, 0 otherwise.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s1\"\n                  },\n                  {\n                    \"token\": \"s2\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"contains\",\n                \"token\": \"contains\",\n                \"type\": \"function\",\n                \"summary\": \"Return the number of occurrences of s2 in s1, 0 otherwise. If s2 is an empty string, return length(s1) + 1.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s1\"\n                  },\n                  {\n                    \"token\": \"s2\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"strlen\",\n                \"token\": \"strlen\",\n                \"type\": \"function\",\n                \"summary\": \"Return the length of s\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"substr\",\n                \"token\": \"substr\",\n                \"type\": \"function\",\n                \"summary\": \"Return the substring of s, starting at offset and having count characters.If offset is negative, it represents the distance from the end of the string.If count is -1, it means \\\"the rest of the string starting at offset\\\".\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s\"\n                  },\n                  {\n                    \"token\": \"offset\"\n                  },\n                  {\n                    \"token\": \"count\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"format\",\n                \"token\": \"format\",\n                \"type\": \"function\",\n                \"summary\": \"Use the arguments following fmt to format a string.Currently the only format argument supported is %s and it applies to all types of arguments.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"fmt\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"matched_terms\",\n                \"token\": \"matched_terms\",\n                \"type\": \"function\",\n                \"summary\": \"Return the query terms that matched for each record (up to 100), as a list. If a limit is specified, Redis will return the first N matches found, based on query order.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"max_terms=100\",\n                    \"optional\": true\n                  }\n                ]\n              },\n              {\n                \"name\": \"split\",\n                \"token\": \"split\",\n                \"type\": \"function\",\n                \"summary\": \"Split a string by any character in the string sep, and strip any characters in strip. If only s is specified, it is split by commas and spaces are stripped. The output is an array.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"s\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"timefmt\",\n                \"token\": \"timefmt\",\n                \"type\": \"function\",\n                \"summary\": \"Return a formatted time string based on a numeric timestamp value x.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"x\"\n                  },\n                  {\n                    \"token\": \"fmt\",\n                    \"optional\": true\n                  }\n                ]\n              },\n              {\n                \"name\": \"parsetime\",\n                \"token\": \"parsetime\",\n                \"type\": \"function\",\n                \"summary\": \"The opposite of timefmt() - parse a time format using a given format string\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timesharing\"\n                  },\n                  {\n                    \"token\": \"fmt\",\n                    \"optional\": true\n                  }\n                ]\n              },\n              {\n                \"name\": \"day\",\n                \"token\": \"day\",\n                \"type\": \"function\",\n                \"summary\": \"Round a Unix timestamp to midnight (00:00) start of the current day.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"hour\",\n                \"token\": \"hour\",\n                \"type\": \"function\",\n                \"summary\": \"Round a Unix timestamp to the beginning of the current hour.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"minute\",\n                \"token\": \"minute\",\n                \"type\": \"function\",\n                \"summary\": \"Round a Unix timestamp to the beginning of the current minute.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"month\",\n                \"token\": \"month\",\n                \"type\": \"function\",\n                \"summary\": \"Round a unix timestamp to the beginning of the current month.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"dayofweek\",\n                \"token\": \"dayofweek\",\n                \"type\": \"function\",\n                \"summary\": \"Convert a Unix timestamp to the day number (Sunday = 0).\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"dayofmonth\",\n                \"token\": \"dayofmonth\",\n                \"type\": \"function\",\n                \"summary\": \"Convert a Unix timestamp to the day of month number (1 .. 31).\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"dayofyear\",\n                \"token\": \"dayofyear\",\n                \"type\": \"function\",\n                \"summary\": \"Convert a Unix timestamp to the day of year number (0 .. 365).\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"year\",\n                \"token\": \"year\",\n                \"type\": \"function\",\n                \"summary\": \"Convert a Unix timestamp to the current year (e.g. 2018).\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"monthofyear\",\n                \"token\": \"monthofyear\",\n                \"type\": \"function\",\n                \"summary\": \"Convert a Unix timestamp to the current month (0 .. 11).\",\n                \"arguments\": [\n                  {\n                    \"token\": \"timestamp\"\n                  }\n                ]\n              },\n              {\n                \"name\": \"geodistance\",\n                \"token\": \"geodistance\",\n                \"type\": \"function\",\n                \"summary\": \"Return distance in meters.\",\n                \"arguments\": [\n                  {\n                    \"token\": \"\"\n                  }\n                ]\n              }\n            ]\n          },\n          {\n            \"name\": \"name\",\n            \"type\": \"string\",\n            \"token\": \"AS\"\n          }\n        ]\n      },\n      {\n        \"name\": \"limit\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"limit\",\n            \"type\": \"pure-token\",\n            \"token\": \"LIMIT\"\n          },\n          {\n            \"name\": \"offset\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"num\",\n            \"type\": \"integer\"\n          }\n        ]\n      },\n      {\n        \"name\": \"filter\",\n        \"type\": \"string\",\n        \"optional\": true,\n        \"expression\": true,\n        \"token\": \"FILTER\"\n      },\n      {\n        \"name\": \"cursor\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"withcursor\",\n            \"type\": \"pure-token\",\n            \"token\": \"WITHCURSOR\"\n          },\n          {\n            \"name\": \"read_size\",\n            \"type\": \"integer\",\n            \"optional\": true,\n            \"token\": \"COUNT\"\n          },\n          {\n            \"name\": \"idle_time\",\n            \"type\": \"integer\",\n            \"optional\": true,\n            \"token\": \"MAXIDLE\"\n          }\n        ]\n      },\n      {\n        \"name\": \"params\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"params\",\n            \"type\": \"pure-token\",\n            \"token\": \"PARAMS\"\n          },\n          {\n            \"name\": \"nargs\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"values\",\n            \"type\": \"block\",\n            \"multiple\": true,\n            \"arguments\": [\n              {\n                \"name\": \"name\",\n                \"type\": \"string\"\n              },\n              {\n                \"name\": \"value\",\n                \"type\": \"string\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"dialect\",\n        \"type\": \"integer\",\n        \"optional\": true,\n        \"token\": \"DIALECT\",\n        \"since\": \"2.4.3\"\n      }\n    ],\n    \"since\": \"1.1.0\",\n    \"group\": \"search\",\n    \"provider\": \"redisearch\"\n  },\n  \"FT.EXPLAIN\": {\n    \"summary\": \"Returns the execution plan for a complex query\",\n    \"complexity\": \"O(1)\",\n    \"arguments\": [\n      {\n        \"name\": \"index\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"query\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"dialect\",\n        \"type\": \"integer\",\n        \"optional\": true,\n        \"token\": \"DIALECT\",\n        \"since\": \"2.4.3\"\n      }\n    ],\n    \"since\": \"1.0.0\",\n    \"group\": \"search\",\n    \"provider\": \"redisearch\"\n  },\n  \"FT.PROFILE\": {\n    \"summary\": \"Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information\",\n    \"complexity\": \"O(N)\",\n    \"arguments\": [\n      {\n        \"name\": \"index\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"querytype\",\n        \"type\": \"oneof\",\n        \"arguments\": [\n          {\n            \"name\": \"search\",\n            \"type\": \"pure-token\",\n            \"token\": \"SEARCH\"\n          },\n          {\n            \"name\": \"aggregate\",\n            \"type\": \"pure-token\",\n            \"token\": \"AGGREGATE\"\n          }\n        ]\n      },\n      {\n        \"name\": \"limited\",\n        \"type\": \"pure-token\",\n        \"token\": \"LIMITED\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"query\",\n        \"type\": \"token\",\n        \"token\": \"QUERY\",\n        \"expression\": true\n      }\n    ],\n    \"since\": \"2.2.0\",\n    \"group\": \"search\",\n    \"provider\": \"redisearch\"\n  },\n  \"FT.SEARCH\": {\n    \"summary\": \"Searches the index with a textual query, returning either documents or just ids\",\n    \"complexity\": \"O(N)\",\n    \"history\": [[\"2.0.0\", \"Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments\"]],\n    \"arguments\": [\n      {\n        \"name\": \"index\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"query\",\n        \"type\": \"string\"\n      },\n      {\n        \"name\": \"nocontent\",\n        \"type\": \"pure-token\",\n        \"token\": \"NOCONTENT\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"verbatim\",\n        \"type\": \"pure-token\",\n        \"token\": \"VERBATIM\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"nostopwords\",\n        \"type\": \"pure-token\",\n        \"token\": \"NOSTOPWORDS\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"withscores\",\n        \"type\": \"pure-token\",\n        \"token\": \"WITHSCORES\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"withpayloads\",\n        \"type\": \"pure-token\",\n        \"token\": \"WITHPAYLOADS\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"withsortkeys\",\n        \"type\": \"pure-token\",\n        \"token\": \"WITHSORTKEYS\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"filter\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"multiple\": true,\n        \"arguments\": [\n          {\n            \"name\": \"numeric_field\",\n            \"type\": \"string\",\n            \"token\": \"FILTER\"\n          },\n          {\n            \"name\": \"min\",\n            \"type\": \"double\"\n          },\n          {\n            \"name\": \"max\",\n            \"type\": \"double\"\n          }\n        ]\n      },\n      {\n        \"name\": \"geo_filter\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"multiple\": true,\n        \"arguments\": [\n          {\n            \"name\": \"geo_field\",\n            \"type\": \"string\",\n            \"token\": \"GEOFILTER\"\n          },\n          {\n            \"name\": \"lon\",\n            \"type\": \"double\"\n          },\n          {\n            \"name\": \"lat\",\n            \"type\": \"double\"\n          },\n          {\n            \"name\": \"radius\",\n            \"type\": \"double\"\n          },\n          {\n            \"name\": \"radius_type\",\n            \"type\": \"oneof\",\n            \"arguments\": [\n              {\n                \"name\": \"m\",\n                \"type\": \"pure-token\",\n                \"token\": \"m\"\n              },\n              {\n                \"name\": \"km\",\n                \"type\": \"pure-token\",\n                \"token\": \"km\"\n              },\n              {\n                \"name\": \"mi\",\n                \"type\": \"pure-token\",\n                \"token\": \"mi\"\n              },\n              {\n                \"name\": \"ft\",\n                \"type\": \"pure-token\",\n                \"token\": \"ft\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"in_keys\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"count\",\n            \"type\": \"string\",\n            \"token\": \"INKEYS\"\n          },\n          {\n            \"name\": \"key\",\n            \"type\": \"string\",\n            \"multiple\": true\n          }\n        ]\n      },\n      {\n        \"name\": \"in_fields\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"count\",\n            \"type\": \"string\",\n            \"token\": \"INFIELDS\"\n          },\n          {\n            \"name\": \"field\",\n            \"type\": \"string\",\n            \"multiple\": true\n          }\n        ]\n      },\n      {\n        \"name\": \"return\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"count\",\n            \"type\": \"string\",\n            \"token\": \"RETURN\"\n          },\n          {\n            \"name\": \"identifiers\",\n            \"type\": \"block\",\n            \"multiple\": true,\n            \"arguments\": [\n              {\n                \"name\": \"identifier\",\n                \"type\": \"string\"\n              },\n              {\n                \"name\": \"property\",\n                \"type\": \"string\",\n                \"token\": \"AS\",\n                \"optional\": true\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"summarize\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"summarize\",\n            \"type\": \"pure-token\",\n            \"token\": \"SUMMARIZE\"\n          },\n          {\n            \"name\": \"fields\",\n            \"type\": \"block\",\n            \"optional\": true,\n            \"arguments\": [\n              {\n                \"name\": \"count\",\n                \"type\": \"string\",\n                \"token\": \"FIELDS\"\n              },\n              {\n                \"name\": \"field\",\n                \"type\": \"string\",\n                \"multiple\": true\n              }\n            ]\n          },\n          {\n            \"name\": \"num\",\n            \"type\": \"integer\",\n            \"token\": \"FRAGS\",\n            \"optional\": true\n          },\n          {\n            \"name\": \"fragsize\",\n            \"type\": \"integer\",\n            \"token\": \"LEN\",\n            \"optional\": true\n          },\n          {\n            \"name\": \"separator\",\n            \"type\": \"string\",\n            \"token\": \"SEPARATOR\",\n            \"optional\": true\n          }\n        ]\n      },\n      {\n        \"name\": \"highlight\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"highlight\",\n            \"type\": \"pure-token\",\n            \"token\": \"HIGHLIGHT\"\n          },\n          {\n            \"name\": \"fields\",\n            \"type\": \"block\",\n            \"optional\": true,\n            \"arguments\": [\n              {\n                \"name\": \"count\",\n                \"type\": \"string\",\n                \"token\": \"FIELDS\"\n              },\n              {\n                \"name\": \"field\",\n                \"type\": \"string\",\n                \"multiple\": true\n              }\n            ]\n          },\n          {\n            \"name\": \"tags\",\n            \"type\": \"block\",\n            \"optional\": true,\n            \"arguments\": [\n              {\n                \"name\": \"tags\",\n                \"type\": \"pure-token\",\n                \"token\": \"TAGS\"\n              },\n              {\n                \"name\": \"open\",\n                \"type\": \"string\"\n              },\n              {\n                \"name\": \"close\",\n                \"type\": \"string\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"slop\",\n        \"type\": \"integer\",\n        \"optional\": true,\n        \"token\": \"SLOP\"\n      },\n      {\n        \"name\": \"timeout\",\n        \"type\": \"integer\",\n        \"optional\": true,\n        \"token\": \"TIMEOUT\"\n      },\n      {\n        \"name\": \"inorder\",\n        \"type\": \"pure-token\",\n        \"token\": \"INORDER\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"language\",\n        \"type\": \"string\",\n        \"optional\": true,\n        \"token\": \"LANGUAGE\"\n      },\n      {\n        \"name\": \"expander\",\n        \"type\": \"string\",\n        \"optional\": true,\n        \"token\": \"EXPANDER\"\n      },\n      {\n        \"name\": \"scorer\",\n        \"type\": \"string\",\n        \"optional\": true,\n        \"token\": \"SCORER\"\n      },\n      {\n        \"name\": \"explainscore\",\n        \"type\": \"pure-token\",\n        \"token\": \"EXPLAINSCORE\",\n        \"optional\": true\n      },\n      {\n        \"name\": \"payload\",\n        \"type\": \"string\",\n        \"optional\": true,\n        \"token\": \"PAYLOAD\"\n      },\n      {\n        \"name\": \"sortby\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"sortby\",\n            \"type\": \"string\",\n            \"token\": \"SORTBY\"\n          },\n          {\n            \"name\": \"order\",\n            \"type\": \"oneof\",\n            \"optional\": true,\n            \"arguments\": [\n              {\n                \"name\": \"asc\",\n                \"type\": \"pure-token\",\n                \"token\": \"ASC\"\n              },\n              {\n                \"name\": \"desc\",\n                \"type\": \"pure-token\",\n                \"token\": \"DESC\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"limit\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"limit\",\n            \"type\": \"pure-token\",\n            \"token\": \"LIMIT\"\n          },\n          {\n            \"name\": \"offset\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"num\",\n            \"type\": \"integer\"\n          }\n        ]\n      },\n      {\n        \"name\": \"params\",\n        \"type\": \"block\",\n        \"optional\": true,\n        \"arguments\": [\n          {\n            \"name\": \"params\",\n            \"type\": \"pure-token\",\n            \"token\": \"PARAMS\"\n          },\n          {\n            \"name\": \"nargs\",\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"values\",\n            \"type\": \"block\",\n            \"multiple\": true,\n            \"arguments\": [\n              {\n                \"name\": \"name\",\n                \"type\": \"string\"\n              },\n              {\n                \"name\": \"value\",\n                \"type\": \"string\"\n              }\n            ]\n          }\n        ]\n      },\n      {\n        \"name\": \"dialect\",\n        \"type\": \"integer\",\n        \"optional\": true,\n        \"token\": \"DIALECT\",\n        \"since\": \"2.4.3\"\n      }\n    ],\n    \"since\": \"1.0.0\",\n    \"group\": \"search\",\n    \"provider\": \"redisearch\"\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/index.ts",
    "content": "import WorkbenchPage from './WorkbenchPage'\n\nexport default WorkbenchPage\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/interfaces.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport interface IEditorMount {\n  editor: monacoEditor.editor.IStandaloneCodeEditor\n  monaco: typeof monacoEditor\n}\n\nexport interface ISnippetController\n  extends monacoEditor.editor.IEditorContribution {\n  isInSnippet: () => boolean\n  finish: () => boolean\n  cancel: () => boolean\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/telemetry.constants.spec.ts",
    "content": "import { TelemetryEvent, sendEventTelemetry } from 'uiSrc/telemetry'\nimport { workbenchResultsTelemetry } from './telemetry.constants'\n\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendEventTelemetry: jest.fn(),\n}))\n\ndescribe('workbenchResultsTelemetry', () => {\n  beforeEach(() => {\n    jest.clearAllMocks()\n  })\n\n  it('should send WORKBENCH_COMMAND_COPIED event on onCommandCopied', () => {\n    workbenchResultsTelemetry.onCommandCopied?.({\n      command: 'GET key',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_COMMAND_COPIED,\n      eventData: { databaseId: 'db-123', command: 'GET key' },\n    })\n  })\n\n  it('should send WORKBENCH_CLEAR_RESULT_CLICKED event on onResultCleared', () => {\n    workbenchResultsTelemetry.onResultCleared?.({\n      command: 'SET key value',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED,\n      eventData: { databaseId: 'db-123', command: 'SET key value' },\n    })\n  })\n\n  it('should send WORKBENCH_RESULTS_COLLAPSED event on onResultCollapsed', () => {\n    workbenchResultsTelemetry.onResultCollapsed?.({\n      command: 'info',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_RESULTS_COLLAPSED,\n      eventData: { databaseId: 'db-123', command: 'info' },\n    })\n  })\n\n  it('should send WORKBENCH_RESULTS_EXPANDED event on onResultExpanded', () => {\n    workbenchResultsTelemetry.onResultExpanded?.({\n      command: 'info',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_RESULTS_EXPANDED,\n      eventData: { databaseId: 'db-123', command: 'info' },\n    })\n  })\n\n  it('should send WORKBENCH_RESULT_VIEW_CHANGED event on onResultViewChanged', () => {\n    const params = {\n      databaseId: 'db-123',\n      command: 'FT.SEARCH',\n      rawMode: false,\n      group: false,\n      previousView: 'Text',\n      currentView: 'Plugin',\n    }\n\n    workbenchResultsTelemetry.onResultViewChanged?.(params)\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_RESULT_VIEW_CHANGED,\n      eventData: params,\n    })\n  })\n\n  it('should send WORKBENCH_RESULTS_IN_FULL_SCREEN event on onFullScreenToggled', () => {\n    workbenchResultsTelemetry.onFullScreenToggled?.({\n      state: 'Open',\n      databaseId: 'db-123',\n    })\n\n    expect(sendEventTelemetry).toHaveBeenCalledWith({\n      event: TelemetryEvent.WORKBENCH_RESULTS_IN_FULL_SCREEN,\n      eventData: { databaseId: 'db-123', state: 'Open' },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/telemetry.constants.ts",
    "content": "import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\nimport { QueryResultsTelemetry } from 'uiSrc/components/query/context/query-results.context'\n\nexport const workbenchResultsTelemetry: QueryResultsTelemetry = {\n  onCommandCopied: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_COMMAND_COPIED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultCleared: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultCollapsed: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_RESULTS_COLLAPSED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultExpanded: ({ command, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_RESULTS_EXPANDED,\n      eventData: { databaseId, command },\n    })\n  },\n  onResultViewChanged: (params) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_RESULT_VIEW_CHANGED,\n      eventData: params,\n    })\n  },\n  onFullScreenToggled: ({ state, databaseId }) => {\n    sendEventTelemetry({\n      event: TelemetryEvent.WORKBENCH_RESULTS_IN_FULL_SCREEN,\n      eventData: { databaseId, state },\n    })\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/types.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { Maybe } from 'uiSrc/utils'\nimport { IRedisCommand, IRedisCommandTree } from 'uiSrc/constants'\n\nexport enum ArgName {\n  NArgs = 'nargs',\n  Count = 'count',\n}\n\nexport interface FoundCommandArgument {\n  isComplete?: boolean\n  stopArg: Maybe<IRedisCommand>\n  isBlocked: boolean\n  append: Maybe<Array<IRedisCommandTree[]>>\n  parent: Maybe<IRedisCommand>\n  token?: Maybe<IRedisCommand>\n}\n\nexport interface CursorContext {\n  prevCursorChar: string\n  nextCursorChar: string\n  isCursorInQuotes: boolean\n  currentOffsetArg: string\n  offset: number\n  argLeftOffset: number\n  argRightOffset: number\n  range: monacoEditor.IRange\n}\n\nexport interface BlockTokensTree {\n  queryArgs: string[]\n  command?: IRedisCommand\n  parent?: BlockTokensTree\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/helpers.ts",
    "content": "import { ICommandTokenType, IRedisCommand } from 'uiSrc/constants'\n\nexport const isStringsEqual = (str1?: string, str2?: string) =>\n  str1?.toLowerCase() === str2?.toLowerCase()\nexport const isPureTokenType = (command?: IRedisCommand) =>\n  command?.type === ICommandTokenType.PureToken\nexport const isBlockType = (command?: IRedisCommand) =>\n  command?.type === ICommandTokenType.Block\nexport const isOneOfType = (command?: IRedisCommand) =>\n  command?.type === ICommandTokenType.OneOf\n\nexport const isPureToken = (command?: IRedisCommand, arg?: string) =>\n  isPureTokenType(command) && isStringsEqual(arg, command?.token)\n\nexport const isArgInOneOf = (command?: IRedisCommand, arg?: string) =>\n  command?.arguments?.some(({ token }) => isStringsEqual(arg, token))\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/monaco.ts",
    "content": "import { monaco } from 'react-monaco-editor'\nimport * as monacoEditor from 'monaco-editor'\nimport { isString } from 'lodash'\nimport { generateDetail } from 'uiSrc/pages/workbench/utils/query'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport { IRedisCommand, ICommandTokenType } from 'uiSrc/constants'\n\nexport const setCursorPositionAtTheEnd = (\n  editor: monacoEditor.editor.IStandaloneCodeEditor,\n) => {\n  if (!editor) return\n\n  const rows = editor.getValue().split('\\n')\n\n  editor.setPosition({\n    column: rows[rows.length - 1].trimEnd().length + 1,\n    lineNumber: rows.length,\n  })\n\n  editor.focus()\n}\n\nexport const getRange = (\n  position: monaco.Position,\n  word: monaco.editor.IWordAtPosition,\n): monaco.IRange => ({\n  startLineNumber: position.lineNumber,\n  endLineNumber: position.lineNumber,\n  endColumn: word.endColumn,\n  startColumn: word.startColumn,\n})\n\nexport const buildSuggestion = (\n  arg: IRedisCommand,\n  range: monaco.IRange,\n  options: any = {},\n) => {\n  const extraQuotes = arg.expression ? \"'$1'\" : ''\n  return {\n    label: isString(arg)\n      ? arg\n      : arg.token || arg.arguments?.[0].token || arg.name || '',\n    insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} ${extraQuotes}`,\n    insertTextRules:\n      monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    range,\n    kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function,\n    ...options,\n  }\n}\n\nexport const getRediSearchSignutureProvider = (\n  options: Maybe<{\n    isOpen: boolean\n    data: {\n      currentArg: IRedisCommand\n      parent: Maybe<IRedisCommand>\n      token: Maybe<IRedisCommand>\n    }\n  }>,\n) => {\n  const { isOpen, data } = options || {}\n  const { currentArg, parent, token } = data || {}\n  if (!isOpen) return null\n\n  const label = generateDetail(parent)\n  let signaturePosition: Nullable<[number, number]> = null\n  const arg =\n    currentArg?.type === ICommandTokenType.Block\n      ? currentArg?.arguments?.[0]?.name || currentArg?.token || ''\n      : currentArg?.name || currentArg?.type || ''\n\n  // we may have several the same args inside documentation, so we get proper arg after token\n  const numberOfArgsInside = label.split(arg).length - 1\n  if (token && numberOfArgsInside > 1) {\n    const parentToken = token.token || token.arguments?.[0]?.token\n    const parentTokenPosition = parentToken ? label.indexOf(parentToken) : 0\n    const wordRegex = new RegExp(`\\\\b${arg}\\\\b`, 'g')\n    const startPosition =\n      (wordRegex.exec(label.slice(parentTokenPosition))?.index || 0) +\n      parentTokenPosition\n    signaturePosition = [startPosition, startPosition + arg.length]\n  }\n\n  return {\n    dispose: () => {},\n    value: {\n      activeParameter: 0,\n      activeSignature: 0,\n      signatures: [\n        {\n          label: label || '',\n          parameters: [{ label: signaturePosition || arg }],\n        },\n      ],\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/profile.ts",
    "content": "import { ProfileQueryType, SEARCH_COMMANDS, GRAPH_COMMANDS } from '../constants'\n\nexport const generateGraphProfileQuery = (\n  query: string,\n  type: ProfileQueryType,\n) => {\n  const q = query?.split(' ')?.slice(1)\n\n  if (q) {\n    return [`graph.${type.toLowerCase()}`, ...q].join(' ')\n  }\n\n  return null\n}\n\nexport const generateSearchProfileQuery = (\n  query: string,\n  type: ProfileQueryType,\n) => {\n  const commandSplit = query?.split(' ')\n  const cmd = commandSplit?.[0]\n\n  if (!commandSplit || !cmd) {\n    return null\n  }\n\n  if (type === ProfileQueryType.Explain) {\n    return [`ft.${type.toLowerCase()}`, ...commandSplit?.slice(1)].join(' ')\n  }\n  const index = commandSplit?.[1]\n\n  const queryType = cmd.split('.')?.[1] // SEARCH / AGGREGATE\n  return [\n    `ft.${type.toLowerCase()}`,\n    index,\n    queryType,\n    'QUERY',\n    ...commandSplit?.slice(2),\n  ].join(' ')\n}\n\nexport const generateProfileQueryForCommand = (\n  query: string,\n  type: ProfileQueryType,\n) => {\n  const cmd = query?.split(' ')?.[0]?.toLowerCase()\n\n  if (GRAPH_COMMANDS.includes(cmd)) {\n    return generateGraphProfileQuery(query, type)\n  }\n  if (SEARCH_COMMANDS.includes(cmd)) {\n    return generateSearchProfileQuery(query, type)\n  }\n\n  return null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/query.ts",
    "content": "/* eslint-disable no-continue */\nimport { findLastIndex, isNumber, toNumber } from 'lodash'\nimport {\n  CommandProvider,\n  ICommandTokenType,\n  IRedisCommand,\n  IRedisCommandTree,\n} from 'uiSrc/constants'\nimport { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils'\nimport { ArgName, FoundCommandArgument } from 'uiSrc/pages/workbench/types'\nimport {\n  isBlockType,\n  isStringsEqual,\n  isOneOfType,\n  isPureToken,\n  isArgInOneOf,\n} from './helpers'\n\ninterface BlockTokensTree {\n  queryArgs: string[]\n  command?: IRedisCommand\n  currentArg?: IRedisCommand\n  parent?: BlockTokensTree\n}\n\nexport const findSuggestionsByQueryArgs = (\n  commands: IRedisCommand[],\n  queryArgs: string[],\n): Nullable<FoundCommandArgument> => {\n  const firstQueryArg = queryArgs[0]\n  const scopeCommand = firstQueryArg\n    ? commands.find((command) => isStringsEqual(command.token, firstQueryArg))\n    : undefined\n\n  const nextArgs = queryArgs.slice(scopeCommand ? 1 : 0)\n  const blockToken: BlockTokensTree = {\n    queryArgs: nextArgs,\n    command: scopeCommand,\n  }\n\n  return findStopArgumentWithSuggestions(blockToken)\n}\n\nconst skipOptionalArguments = (\n  queryArg: string,\n  commandIndex: number,\n  commandArguments: IRedisCommand[],\n  withLastMandatory = false,\n): {\n  currentArgument: Maybe<IRedisCommand>\n  optionalArguments: IRedisCommand[]\n  index: number\n} => {\n  const optionalArguments = []\n  let index = commandIndex + 1\n  let currentArgument = commandArguments[index]\n  while (index < commandArguments.length) {\n    const argumentToken =\n      currentArgument.token || currentArgument.arguments?.[0]?.token\n    const isOptionalWithInput = isStringsEqual(queryArg, argumentToken)\n\n    if (isOptionalWithInput || (currentArgument && !currentArgument.optional)) {\n      return {\n        optionalArguments: withLastMandatory\n          ? [...optionalArguments, currentArgument]\n          : optionalArguments,\n        currentArgument,\n        index,\n      }\n    }\n\n    optionalArguments.push(currentArgument)\n    index++\n    currentArgument = commandArguments[index]\n  }\n\n  return { optionalArguments, currentArgument, index }\n}\n\nconst isCountArg = (arg?: Nullable<IRedisCommand>) =>\n  arg?.name === ArgName.NArgs || arg?.name === ArgName.Count\nconst isCountPositive = (count?: Maybe<number>) => isNumber(count) && count > 0\nexport const findStopArgument = (\n  queryArgs: string[],\n  command?: IRedisCommandTree,\n  count?: Maybe<number>,\n  forceReturn = false,\n  parentSkippedArguments: IRedisCommand[][] = [],\n) => {\n  let commandIndex = 0\n  let isBlocked = true\n  let argsCount = count\n\n  if (!command?.arguments) return null\n\n  for (let i = 0; i < queryArgs.length; i++) {\n    const queryArg = queryArgs[i]\n    let currentArgument: Maybe<IRedisCommand> = command?.arguments[commandIndex]\n    const prevMandatoryIndex = findLastIndex(\n      command.arguments.slice(0, commandIndex),\n      (arg) => !arg.optional,\n    )\n\n    // handle optional arguments, iterate until we get token or non optional arguments\n    // we check if we not blocked on optional token argument\n    if (!isBlocked && (currentArgument?.optional || !forceReturn)) {\n      const arg = skipOptionalArguments(\n        queryArg,\n        prevMandatoryIndex,\n        command.arguments,\n      )\n      const { index, currentArgument: nextCurrentArgument } = arg\n\n      currentArgument = nextCurrentArgument\n      commandIndex = index\n    }\n\n    // handle case when we proceed all arguments from command but it is multiple and should return to 0 index\n    if (!currentArgument && isCountPositive(argsCount)) {\n      commandIndex = 0\n      currentArgument = command?.arguments[commandIndex]\n    }\n\n    if (!currentArgument && forceReturn) {\n      const arg = skipOptionalArguments(\n        queryArg,\n        prevMandatoryIndex,\n        command.arguments,\n      )\n      const { optionalArguments: allRestArguments } = skipOptionalArguments(\n        '',\n        prevMandatoryIndex,\n        command.arguments,\n      )\n      const { index, currentArgument: nextCurrentArgument } = arg\n      const restOptional = removeSuggestedArgs(queryArgs, allRestArguments)\n\n      if (!nextCurrentArgument) {\n        return {\n          queryArgsIterated: i,\n          stopArgument: currentArgument,\n          skippedArguments: allRestArguments,\n          isCompleteByNArgs: argsCount === 0,\n        }\n      }\n\n      // Continue iterating through optional arguments\n      currentArgument = nextCurrentArgument\n\n      if (restOptional?.length) {\n        commandIndex = index\n      }\n    }\n\n    // check count to understand that we in block nargs scope, if completed all arguments then return\n    if (isCountPositive(count) && argsCount === 0) {\n      return {\n        queryArgsIterated: i,\n        stopArgument: currentArgument,\n        skippedArguments: parentSkippedArguments,\n        isBlocked: false,\n        isCompleteByNArgs: true,\n      }\n    }\n\n    // handle pure token, if arg equals, then move to next command\n    if (isPureToken(currentArgument, queryArg)) {\n      isBlocked = false\n      commandIndex++\n      isNumber(argsCount) && argsCount--\n      continue\n    }\n\n    // handle first iteration of token, if arg equals - stay on it and move on to check itself\n    if (currentArgument?.token) {\n      if (isStringsEqual(queryArg, currentArgument.token)) {\n        isBlocked = true\n        continue\n      }\n\n      if (isBlocked) isBlocked = false\n\n      // token can be on block type\n      if (!isBlockType(currentArgument) && !isCountArg(currentArgument)) {\n        commandIndex++\n        isNumber(argsCount) && argsCount--\n        continue\n      }\n    }\n\n    // handle block, we call the same function for block arguments\n    if (isBlockType(currentArgument)) {\n      const { optionalArguments } = skipOptionalArguments(\n        '',\n        prevMandatoryIndex,\n        command.arguments,\n      )\n      const filteredArguments = removeSuggestedArgs(\n        queryArgs,\n        optionalArguments,\n      )\n      const parentArgs = [filteredArguments, ...parentSkippedArguments]\n      const restQeuryArgs = queryArgs.slice(i)\n      const blockResult: any =\n        findStopArgument(\n          restQeuryArgs,\n          currentArgument,\n          argsCount,\n          true,\n          parentArgs,\n        ) || {}\n      const {\n        stopArgument,\n        isCompleteByNArgs,\n        queryArgsIterated,\n        lastArgument,\n        skippedArguments,\n      } = blockResult\n\n      const isCountCompleted = isCountPositive(argsCount) && isCompleteByNArgs\n      i += (queryArgsIterated || 1) - 1\n\n      // entered all multiple counted arguments, move to next command arg\n      if (isCountCompleted) {\n        argsCount = undefined\n        isBlocked = false\n        commandIndex++\n        continue\n      }\n\n      // we found an argument which is needed to be inserted\n      if (stopArgument) {\n        return {\n          ...blockResult,\n          blockParent: currentArgument,\n        }\n      }\n\n      if (lastArgument?.optional && skippedArguments?.[0]?.length) {\n        return {\n          ...blockResult,\n        }\n      }\n\n      if (currentArgument && !currentArgument.multiple) commandIndex++\n\n      isBlocked = false\n      argsCount = undefined\n      continue\n    }\n\n    // handle one-of argument\n    if (isOneOfType(currentArgument)) {\n      const isArgOneOf = isArgInOneOf(currentArgument, queryArg)\n      if (isArgOneOf) {\n        commandIndex++\n        isNumber(argsCount) && argsCount--\n        isBlocked = false\n        continue\n      }\n\n      isBlocked = true\n    }\n\n    // handle multiple arguments, argsCount - number of arguments which have to be inserted\n    if (currentArgument?.multiple) {\n      isNumber(argsCount) && argsCount--\n\n      if (argsCount === 0) {\n        commandIndex++\n        isBlocked = false\n      }\n\n      continue\n    }\n\n    // handle count argument with next multiple\n    const nextArgument = command?.arguments?.[commandIndex + 1]\n    if (isCountArg(currentArgument) && nextArgument?.multiple) {\n      argsCount = toNumber(queryArg) || 0\n\n      if (argsCount === 0) {\n        // skip next argument\n        commandIndex += 2\n        continue\n      }\n\n      commandIndex++\n      isBlocked = true\n      continue\n    }\n\n    commandIndex++\n    isNumber(argsCount) && argsCount--\n\n    if (isCountPositive(argsCount)) {\n      continue\n    }\n\n    isBlocked = false\n  }\n\n  const stopArgument: Maybe<IRedisCommand> = command?.arguments?.[commandIndex]\n  const prevMandatoryIndex = findLastIndex(\n    command.arguments.slice(0, commandIndex),\n    (arg) => !arg.optional,\n  )\n\n  const data = skipOptionalArguments(\n    '',\n    prevMandatoryIndex,\n    command.arguments,\n    true,\n  )\n  const currentLvlArgs = removeSuggestedArgs(queryArgs, data.optionalArguments)\n\n  return {\n    skippedArguments:\n      stopArgument && !stopArgument.optional\n        ? [currentLvlArgs]\n        : [currentLvlArgs, ...parentSkippedArguments],\n    lastArgument: command?.arguments?.[commandIndex - 1],\n    stopArgument,\n    isBlocked,\n    queryArgsIterated: queryArgs.length,\n    isCompleteByNArgs: argsCount === 0,\n  }\n}\n\nexport const findStopArgumentWithSuggestions = (\n  currentBlock: BlockTokensTree,\n): Nullable<FoundCommandArgument> => {\n  const { queryArgs, command } = currentBlock\n  const { isBlocked, stopArgument, skippedArguments, blockParent } =\n    findStopArgument(queryArgs, command)\n\n  if (isBlocked) {\n    const isBlockedWithSuggestions =\n      stopArgument.type === ICommandTokenType.OneOf\n\n    return {\n      stopArg: stopArgument,\n      append: [\n        fillArgsByType(\n          isBlockedWithSuggestions\n            ? stopArgument.arguments\n            : [stopArgument.arguments?.[0]] || [stopArgument],\n        ),\n      ],\n      isBlocked: !isBlockedWithSuggestions,\n      parent: blockParent || command,\n    }\n  }\n\n  const append = skippedArguments.map((args: IRedisCommand[]) =>\n    fillArgsByType(args, true),\n  )\n\n  return {\n    append,\n    stopArg: stopArgument,\n    isBlocked,\n    parent: blockParent || command,\n  }\n}\n\nexport const removeSuggestedArgs = (\n  args: string[],\n  commandArgs: IRedisCommandTree[],\n) =>\n  commandArgs.filter((arg) => {\n    if (arg.token && arg.multiple) return true\n\n    if (arg.type === ICommandTokenType.OneOf) {\n      return !args.some((queryArg) =>\n        arg.arguments?.some((oneOfArg) =>\n          isStringsEqual(oneOfArg.token, queryArg),\n        ),\n      )\n    }\n\n    if (arg.type === ICommandTokenType.Block) {\n      if (arg.token) return !args.includes(arg.token) || arg.multiple\n      return (\n        arg.arguments?.[0]?.token &&\n        (!args.includes(arg.arguments?.[0]?.token?.toUpperCase()) ||\n          arg.multiple)\n      )\n    }\n\n    return arg.token && !args.includes(arg.token)\n  })\n\nexport const fillArgsByType = (\n  args: IRedisCommand[],\n  expandBlock = true,\n): IRedisCommandTree[] => {\n  const result: IRedisCommandTree[] = []\n\n  for (let i = 0; i < args.length; i++) {\n    const currentArg = args[i]\n\n    if (\n      expandBlock &&\n      currentArg?.type === ICommandTokenType.OneOf &&\n      !currentArg?.token\n    ) {\n      result.push(\n        ...(currentArg?.arguments?.map((arg) => ({\n          ...arg,\n          parent: currentArg,\n        })) || []),\n      )\n    }\n\n    if (currentArg?.token) {\n      result.push(currentArg)\n      continue\n    }\n\n    if (currentArg?.type === ICommandTokenType.Block) {\n      result.push({\n        multiple: currentArg.multiple,\n        optional: currentArg.optional,\n        parent: currentArg,\n        ...((currentArg?.arguments?.[0] as IRedisCommand) || {}),\n      })\n    }\n  }\n\n  return result\n}\n\nexport const generateDetail = (command: Maybe<IRedisCommand>) => {\n  if (!command) return ''\n  if (command.arguments) {\n    const isTokenInArguemnts = command.token === command.arguments?.[0]?.token\n    const args = generateArgsNames(\n      CommandProvider.Main,\n      command.arguments.slice(isTokenInArguemnts ? 1 : 0),\n    ).join(' ')\n    return command.token ? `${command.token} ${args}` : args\n  }\n  if (command.token) {\n    if (command.type === ICommandTokenType.PureToken) return command.token\n    return `${command.token}`\n  }\n\n  return ''\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/query_old.ts",
    "content": "/* eslint-disable no-continue */\n\nimport { findLastIndex, isNumber, toNumber } from 'lodash'\nimport { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils'\nimport {\n  CommandProvider,\n  IRedisCommand,\n  IRedisCommandTree,\n  ICommandTokenType,\n} from 'uiSrc/constants'\nimport { isStringsEqual } from './helpers'\nimport { ArgName, FoundCommandArgument } from '../types'\n\nexport const findCurrentArgument = (\n  args: IRedisCommand[],\n  prev: string[],\n  untilTokenArgs: string[] = [],\n  parent?: IRedisCommandTree,\n): Nullable<FoundCommandArgument> => {\n  for (let i = prev.length - 1; i >= 0; i--) {\n    const arg = prev[i]\n    const currentArg = findArgByToken(args, arg)\n    const currentWithParent: IRedisCommandTree = { ...currentArg, parent }\n\n    if (currentArg?.arguments && currentArg?.type === ICommandTokenType.Block) {\n      return findCurrentArgument(\n        currentArg.arguments,\n        prev.slice(i),\n        prev,\n        currentWithParent,\n      )\n    }\n\n    const tokenIndex = args.findIndex((cArg) => isStringsEqual(cArg.token, arg))\n    const token = args[tokenIndex]\n\n    if (token) {\n      const pastArgs = prev.slice(i)\n      const commandArgs = parent ? args.slice(tokenIndex, args.length) : [token]\n\n      // getArgByRest - here we preparing the list of arguments which can be inserted,\n      // this is the main function which creates the list of arguments\n      return {\n        ...getArgumentSuggestions(\n          { tokenArgs: pastArgs, untilTokenArgs },\n          commandArgs,\n          parent,\n        ),\n        token,\n        parent: parent || token,\n      }\n    }\n  }\n\n  return null\n}\n\nconst findStopArgumentInQuery = (\n  queryArgs: string[],\n  restCommandArgs: Maybe<IRedisCommand[]> = [],\n): {\n  restArguments: IRedisCommand[]\n  stopArgIndex: number\n  argumentsIntered?: number\n  isBlocked: boolean\n  parent?: IRedisCommand\n} => {\n  let currentCommandArgIndex = 0\n  let argumentsIntered = 0\n  let isBlockedOnCommand = false\n  let multipleIndexStart = 0\n  let multipleCountNumber = 0\n\n  const moveToNextCommandArg = () => {\n    currentCommandArgIndex++\n    argumentsIntered++\n  }\n  const blockCommand = () => {\n    isBlockedOnCommand = true\n  }\n  const unBlockCommand = () => {\n    isBlockedOnCommand = false\n  }\n\n  const skipArg = () => {\n    argumentsIntered -= 1\n    moveToNextCommandArg()\n    unBlockCommand()\n  }\n\n  for (let i = 0; i < queryArgs.length; i++) {\n    const arg = queryArgs[i]\n    const currentCommandArg = restCommandArgs[currentCommandArgIndex]\n\n    if (currentCommandArg?.type === ICommandTokenType.PureToken) {\n      skipArg()\n      continue\n    }\n\n    if (!isBlockedOnCommand && currentCommandArg?.optional) {\n      const isNotToken =\n        currentCommandArg?.token &&\n        !isStringsEqual(currentCommandArg.token, arg)\n      const isNotOneOfToken =\n        !currentCommandArg?.token &&\n        currentCommandArg?.type === ICommandTokenType.OneOf &&\n        currentCommandArg?.arguments?.every(\n          ({ token }) => !isStringsEqual(token, arg),\n        )\n\n      if (isNotToken || isNotOneOfToken) {\n        moveToNextCommandArg()\n        skipArg()\n        continue\n      }\n    }\n\n    if (currentCommandArg?.type === ICommandTokenType.Block) {\n      let blockArguments = currentCommandArg.arguments\n        ? [...currentCommandArg.arguments]\n        : []\n      const nArgs = toNumber(queryArgs[i - 1]) || 0\n\n      // if block is multiple - we duplicate nArgs inner arguments\n      if (currentCommandArg?.multiple && nArgs) {\n        blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat()\n      }\n\n      const currentQueryArg = queryArgs.slice(i)?.[0]\n      const isBlockHasToken = isStringsEqual(\n        blockArguments?.[0]?.token,\n        currentQueryArg,\n      )\n\n      if (currentCommandArg.token && !isBlockHasToken && currentQueryArg) {\n        blockArguments.unshift({\n          type: ICommandTokenType.PureToken,\n          token: currentQueryArg,\n        })\n      }\n\n      const blockSuggestion = findStopArgumentInQuery(\n        queryArgs.slice(i),\n        blockArguments,\n      )\n      const stopArg =\n        blockSuggestion.restArguments?.[blockSuggestion.stopArgIndex]\n      const { argumentsIntered } = blockSuggestion\n\n      if (\n        nArgs &&\n        currentCommandArg?.multiple &&\n        isNumber(argumentsIntered) &&\n        argumentsIntered >= nArgs\n      ) {\n        i += queryArgs.slice(i).length - 1\n        skipArg()\n        continue\n      }\n\n      if (blockSuggestion.isBlocked || stopArg) {\n        return {\n          ...blockSuggestion,\n          parent: currentCommandArg,\n        }\n      }\n\n      i += queryArgs.slice(i).length - 1\n      skipArg()\n      continue\n    }\n\n    // if we are on token - that requires one more argument\n    if (isStringsEqual(currentCommandArg?.token, arg)) {\n      blockCommand()\n      continue\n    }\n\n    if (\n      currentCommandArg?.name === ArgName.NArgs ||\n      currentCommandArg?.name === ArgName.Count\n    ) {\n      const numberOfArgs = toNumber(arg)\n\n      if (numberOfArgs === 0) {\n        moveToNextCommandArg()\n        skipArg()\n        continue\n      }\n\n      moveToNextCommandArg()\n      blockCommand()\n      continue\n    }\n\n    if (\n      currentCommandArg?.type === ICommandTokenType.OneOf &&\n      currentCommandArg?.optional\n    ) {\n      // if oneof is optional then we can switch to another argument\n      if (\n        !currentCommandArg?.arguments?.some(({ token }) =>\n          isStringsEqual(token, arg),\n        )\n      ) {\n        moveToNextCommandArg()\n      }\n\n      skipArg()\n      continue\n    }\n\n    if (currentCommandArg?.multiple) {\n      if (!multipleIndexStart) {\n        multipleCountNumber = toNumber(queryArgs[i - 1])\n        multipleIndexStart = i - 1\n      }\n\n      if (i - multipleIndexStart >= multipleCountNumber) {\n        skipArg()\n        multipleIndexStart = 0\n        continue\n      }\n\n      blockCommand()\n      continue\n    }\n\n    moveToNextCommandArg()\n\n    isBlockedOnCommand = false\n  }\n\n  return {\n    restArguments: restCommandArgs,\n    stopArgIndex: currentCommandArgIndex,\n    argumentsIntered,\n    isBlocked: isBlockedOnCommand,\n  }\n}\n\nexport const getArgumentSuggestions = (\n  {\n    tokenArgs,\n    untilTokenArgs,\n  }: {\n    tokenArgs: string[]\n    untilTokenArgs: string[]\n  },\n  pastCommandArgs: IRedisCommand[],\n  current?: IRedisCommandTree,\n): {\n  isComplete: boolean\n  stopArg: Maybe<IRedisCommand>\n  isBlocked: boolean\n  append: Array<IRedisCommand[]>\n} => {\n  const {\n    restArguments,\n    stopArgIndex,\n    isBlocked: isWasBlocked,\n    parent,\n  } = findStopArgumentInQuery(tokenArgs, pastCommandArgs)\n\n  const prevArg = restArguments[stopArgIndex - 1]\n  const stopArgument = restArguments[stopArgIndex]\n  const restNotFilledArgs = restArguments.slice(stopArgIndex)\n\n  const isOneOfArgument =\n    stopArgument?.type === ICommandTokenType.OneOf ||\n    (stopArgument?.type === ICommandTokenType.PureToken &&\n      current?.parent?.type === ICommandTokenType.OneOf)\n\n  if (isWasBlocked) {\n    return {\n      isComplete: false,\n      stopArg: stopArgument,\n      isBlocked: !isOneOfArgument,\n      append: isOneOfArgument ? [stopArgument.arguments!] : [],\n    }\n  }\n\n  const isPrevArgWasMandatory = prevArg && !prevArg.optional\n  if (isPrevArgWasMandatory && stopArgument && !stopArgument.optional) {\n    const isCanAppend = stopArgument?.token || isOneOfArgument\n    const append = isCanAppend\n      ? [[isOneOfArgument ? stopArgument.arguments! : stopArgument].flat()]\n      : []\n\n    return {\n      isComplete: false,\n      stopArg: stopArgument,\n      isBlocked: !isCanAppend,\n      append,\n    }\n  }\n\n  // if we finished argument - stopArgument will be undefined, then we get it as token\n  const lastArgument = stopArgument ?? restArguments[0]\n  const isBlockHasParent = current?.arguments?.some(\n    ({ name }) => parent?.name && name === parent?.name,\n  )\n  const foundParent = isBlockHasParent\n    ? { ...parent, parent: current }\n    : parent || current\n\n  const isBlockComplete = !stopArgument && isPrevArgWasMandatory\n  const beforeMandatoryOptionalArgs = getAllRestArguments(\n    foundParent,\n    lastArgument,\n    untilTokenArgs,\n    isBlockComplete,\n  )\n  const requiredArgsLength = restNotFilledArgs.filter(\n    (arg) => !arg.optional,\n  ).length\n\n  return {\n    isComplete: requiredArgsLength === 0,\n    stopArg: stopArgument,\n    isBlocked: false,\n    append: beforeMandatoryOptionalArgs,\n  }\n}\n\nexport const getRestArguments = (\n  current: Maybe<IRedisCommandTree>,\n  stopArgument: Nullable<IRedisCommand>,\n): IRedisCommandTree[] => {\n  const argumentIndexInArg = current?.arguments?.findIndex(\n    ({ name }) => name === stopArgument?.name,\n  )\n  const nextMandatoryIndex =\n    stopArgument && !stopArgument.optional\n      ? argumentIndexInArg\n      : argumentIndexInArg && argumentIndexInArg > -1\n        ? current?.arguments?.findIndex(\n            ({ optional }, i) => !optional && i > argumentIndexInArg,\n          )\n        : -1\n\n  const prevMandatory = current?.arguments\n    ?.slice(0, argumentIndexInArg)\n    .reverse()\n    .find(({ optional }) => !optional)\n  const prevMandatoryIndex = current?.arguments?.findIndex(\n    ({ name }) => name === prevMandatory?.name,\n  )\n\n  const beforeMandatoryOptionalArgs =\n    (nextMandatoryIndex && nextMandatoryIndex > -1\n      ? current?.arguments?.slice(prevMandatoryIndex, nextMandatoryIndex)\n      : current?.arguments?.slice((prevMandatoryIndex || 0) + 1)) || []\n\n  const nextMandatoryArg =\n    nextMandatoryIndex && nextMandatoryIndex > -1\n      ? current?.arguments?.[nextMandatoryIndex]\n      : undefined\n\n  if (nextMandatoryArg?.token) {\n    beforeMandatoryOptionalArgs.unshift(nextMandatoryArg)\n  }\n\n  if (nextMandatoryArg?.type === ICommandTokenType.OneOf) {\n    beforeMandatoryOptionalArgs.unshift(...(nextMandatoryArg.arguments || []))\n  }\n\n  return beforeMandatoryOptionalArgs.map((arg) => ({ ...arg, parent: current }))\n}\n\nexport const getAllRestArguments = (\n  current: Maybe<IRedisCommandTree>,\n  stopArgument: Nullable<IRedisCommand>,\n  untilTokenArgs: string[] = [],\n  skipLevel = false,\n) => {\n  const appendArgs: Array<IRedisCommand[]> = []\n\n  const currentToken =\n    current?.type === ICommandTokenType.Block\n      ? current?.arguments?.[0].token\n      : current?.token\n  const lastTokenIndex = findLastIndex(untilTokenArgs, (arg) =>\n    isStringsEqual(arg, currentToken),\n  )\n  const currentLvlNextArgs = removeNotSuggestedArgs(\n    untilTokenArgs.slice(lastTokenIndex > 0 ? lastTokenIndex : 0),\n    getRestArguments(current, stopArgument),\n  )\n\n  if (!skipLevel) {\n    appendArgs.push(fillArgsByType(currentLvlNextArgs))\n  }\n\n  if (current?.parent) {\n    const parentArgs = getAllRestArguments(\n      current.parent,\n      current,\n      untilTokenArgs,\n    )\n    if (parentArgs?.length) {\n      appendArgs.push(...parentArgs)\n    }\n  }\n\n  return appendArgs\n}\n\nexport const removeNotSuggestedArgs = (\n  args: string[],\n  commandArgs: IRedisCommandTree[],\n) =>\n  commandArgs.filter((arg) => {\n    if (arg.token && arg.multiple) return true\n\n    if (arg.type === ICommandTokenType.OneOf) {\n      return !args.some((queryArg) =>\n        arg.arguments?.some((oneOfArg) =>\n          isStringsEqual(oneOfArg.token, queryArg),\n        ),\n      )\n    }\n\n    if (arg.type === ICommandTokenType.Block) {\n      if (arg.token)\n        return (\n          !args.some((queryArg) => isStringsEqual(queryArg, arg.token)) ||\n          arg.multiple\n        )\n      return (\n        arg.arguments?.[0]?.token &&\n        (!args.some((queryArg) =>\n          isStringsEqual(queryArg, arg.arguments?.[0]?.token),\n        ) ||\n          arg.multiple)\n      )\n    }\n\n    return (\n      arg.token && !args.some((queryArg) => isStringsEqual(queryArg, arg.token))\n    )\n  })\n\nexport const fillArgsByType = (\n  args: IRedisCommand[],\n  expandBlock = true,\n): IRedisCommandTree[] => {\n  const result: IRedisCommandTree[] = []\n\n  for (let i = 0; i < args.length; i++) {\n    const currentArg = args[i]\n\n    if (\n      expandBlock &&\n      currentArg.type === ICommandTokenType.OneOf &&\n      !currentArg.token\n    ) {\n      result.push(\n        ...(currentArg?.arguments?.map((arg) => ({\n          ...arg,\n          parent: currentArg,\n        })) || []),\n      )\n    }\n\n    if (currentArg.token) {\n      result.push(currentArg)\n      continue\n    }\n\n    if (currentArg.type === ICommandTokenType.Block) {\n      result.push({\n        multiple: currentArg.multiple,\n        optional: currentArg.optional,\n        parent: currentArg,\n        ...((currentArg?.arguments?.[0] as IRedisCommand) || {}),\n      })\n    }\n  }\n\n  return result\n}\n\nexport const findArgByToken = (\n  list: IRedisCommand[],\n  arg: string,\n): Maybe<IRedisCommand> =>\n  list.find((cArg) =>\n    cArg.type === ICommandTokenType.OneOf\n      ? cArg.arguments?.some((oneOfArg: IRedisCommand) =>\n          isStringsEqual(oneOfArg?.token, arg),\n        )\n      : isStringsEqual(cArg.arguments?.[0].token, arg),\n  )\n\nexport const generateDetail = (command: Maybe<IRedisCommand>) => {\n  if (!command) return ''\n  if (command.arguments) {\n    const isTokenInArguemnts = command.token === command.arguments?.[0]?.token\n    const args = generateArgsNames(\n      CommandProvider.Main,\n      command.arguments.slice(isTokenInArguemnts ? 1 : 0),\n    ).join(' ')\n    return command.token ? `${command.token} ${args}` : args\n  }\n  if (command.token) {\n    if (command.type === ICommandTokenType.PureToken) return command.token\n    return `${command.token}`\n  }\n\n  return ''\n}\n\nexport const addOwnTokenToArgs = (token: string, command: IRedisCommand) => {\n  if (command.arguments) {\n    return {\n      ...command,\n      arguments: [\n        { token, type: ICommandTokenType.PureToken },\n        ...command.arguments,\n      ],\n    }\n  }\n  return command\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { isNumber } from 'lodash'\nimport { IMonacoQuery, Nullable, splitQueryByArgs } from 'uiSrc/utils'\nimport {\n  CursorContext,\n  FoundCommandArgument,\n} from 'uiSrc/pages/workbench/types'\nimport { IRedisCommand } from 'uiSrc/constants'\nimport {\n  asSuggestionsRef,\n  getFieldsSuggestions,\n  getFunctionsSuggestions,\n  getGeneralSuggestions,\n  getIndexesSuggestions,\n  getNoIndexesSuggestion,\n} from 'uiSrc/pages/workbench/utils/suggestions'\nimport {\n  COMMANDS_WITHOUT_INDEX_PROPOSE,\n  DefinedArgumentName,\n  FIELD_START_SYMBOL,\n  ModuleCommandPrefix,\n} from 'uiSrc/pages/workbench/constants'\nimport { findSuggestionsByQueryArgs } from 'uiSrc/pages/workbench/utils/query'\n\nexport const findSuggestionsByArg = (\n  listOfCommands: IRedisCommand[],\n  command: IMonacoQuery,\n  cursorContext: CursorContext,\n  additionData: {\n    indexes?: any[]\n    fields?: any[]\n    activeIndexName?: string\n  },\n  isEscaped: boolean = false,\n): {\n  suggestions: any\n  helpWidget?: any\n} => {\n  const { allArgs, args, cursor } = command\n  const { prevCursorChar } = cursor\n  const [beforeOffsetArgs, [currentOffsetArg]] = args\n\n  const startCommentIndex = beforeOffsetArgs.findIndex((el) =>\n    el.startsWith('//'),\n  )\n  if (startCommentIndex > -1 || currentOffsetArg?.startsWith('//')) {\n    return {\n      suggestions: asSuggestionsRef([]),\n      helpWidget: { isOpen: false },\n    }\n  }\n\n  const foundArg = findSuggestionsByQueryArgs(listOfCommands, beforeOffsetArgs)\n\n  if (!command.name.startsWith(ModuleCommandPrefix.RediSearch)) {\n    return {\n      helpWidget: {\n        isOpen: !!foundArg,\n        data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg },\n      },\n      suggestions: asSuggestionsRef([]),\n    }\n  }\n\n  if (\n    prevCursorChar === FIELD_START_SYMBOL ||\n    currentOffsetArg?.startsWith(FIELD_START_SYMBOL)\n  ) {\n    return handleFieldSuggestions(\n      additionData.fields || [],\n      foundArg,\n      cursorContext.range,\n    )\n  }\n\n  if (foundArg?.stopArg?.token && !foundArg?.isBlocked) {\n    return handleCommonSuggestions(\n      command.commandQuery,\n      foundArg,\n      allArgs,\n      additionData.fields || [],\n      cursorContext,\n      isEscaped,\n    )\n  }\n\n  const { indexes, fields, activeIndexName } = additionData\n  switch (foundArg?.stopArg?.name) {\n    case DefinedArgumentName.index: {\n      return handleIndexSuggestions(\n        indexes,\n        command,\n        foundArg,\n        currentOffsetArg,\n        cursorContext,\n        activeIndexName,\n      )\n    }\n    case DefinedArgumentName.query: {\n      return handleQuerySuggestions(foundArg)\n    }\n    default: {\n      return handleCommonSuggestions(\n        command.commandQuery,\n        foundArg,\n        allArgs,\n        fields,\n        cursorContext,\n        isEscaped,\n      )\n    }\n  }\n}\n\nconst handleFieldSuggestions = (\n  fields: any[],\n  foundArg: Nullable<FoundCommandArgument>,\n  range: monacoEditor.IRange,\n) => {\n  const isInQuery = foundArg?.stopArg?.name === DefinedArgumentName.query\n  const fieldSuggestions = getFieldsSuggestions(fields, range, true, isInQuery)\n  return {\n    suggestions: asSuggestionsRef(fieldSuggestions, true),\n  }\n}\n\nconst handleIndexSuggestions = (\n  indexes: any[] = [],\n  command: IMonacoQuery,\n  foundArg: FoundCommandArgument,\n  currentOffsetArg: Nullable<string>,\n  cursorContext: CursorContext,\n  activeIndexName?: string,\n) => {\n  const isIndex = indexes.length > 0\n  const helpWidget = {\n    isOpen: isIndex,\n    data: { parent: foundArg.parent, currentArg: foundArg?.stopArg },\n  }\n  const currentCommand = command.info\n\n  if (COMMANDS_WITHOUT_INDEX_PROPOSE.includes(command.name || '')) {\n    return {\n      suggestions: asSuggestionsRef([]),\n      helpWidget,\n    }\n  }\n\n  if (!isIndex) {\n    helpWidget.isOpen = !!currentOffsetArg\n\n    return {\n      suggestions: asSuggestionsRef(\n        !currentOffsetArg ? getNoIndexesSuggestion(cursorContext.range) : [],\n        true,\n      ),\n      helpWidget,\n    }\n  }\n\n  if (!isIndex || currentOffsetArg) {\n    return {\n      suggestions: asSuggestionsRef([], !currentOffsetArg),\n      helpWidget,\n    }\n  }\n\n  const argumentIndex = currentCommand?.arguments?.findIndex(\n    ({ name }) => foundArg?.stopArg?.name === name,\n  )\n  const isNextArgQuery =\n    isNumber(argumentIndex) &&\n    currentCommand?.arguments?.[argumentIndex + 1]?.name ===\n      DefinedArgumentName.query\n\n  return {\n    suggestions: asSuggestionsRef(\n      getIndexesSuggestions(\n        indexes,\n        cursorContext.range,\n        isNextArgQuery,\n        activeIndexName,\n      ),\n    ),\n    helpWidget,\n  }\n}\n\nconst handleQuerySuggestions = (foundArg: FoundCommandArgument) => ({\n  helpWidget: {\n    isOpen: true,\n    data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg },\n  },\n  suggestions: asSuggestionsRef([], false),\n})\n\nconst handleExpressionSuggestions = (\n  value: string,\n  foundArg: FoundCommandArgument,\n  cursorContext: CursorContext,\n) => {\n  const helpWidget = {\n    isOpen: true,\n    data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg },\n  }\n\n  const { isCursorInQuotes, offset, argLeftOffset } = cursorContext\n  if (!isCursorInQuotes) {\n    return {\n      suggestions: asSuggestionsRef([]),\n      helpWidget,\n    }\n  }\n\n  const stringBeforeCursor = value.substring(argLeftOffset, offset) || ''\n  const expression = stringBeforeCursor.replace(/^[\"']|[\"']$/g, '')\n  const { args } = splitQueryByArgs(expression, offset - argLeftOffset)\n  const [, [currentArg]] = args\n\n  const functions = foundArg?.stopArg?.arguments ?? []\n  const suggestions = getFunctionsSuggestions(functions, cursorContext.range)\n  const isStartsWithFunction = functions.some(({ token }) =>\n    token?.startsWith(currentArg),\n  )\n\n  return {\n    suggestions: asSuggestionsRef(suggestions, true, isStartsWithFunction),\n    helpWidget,\n  }\n}\n\nconst handleCommonSuggestions = (\n  value: string,\n  foundArg: Nullable<FoundCommandArgument>,\n  allArgs: string[],\n  fields: any[] = [],\n  cursorContext: CursorContext,\n  isEscaped: boolean,\n) => {\n  if (foundArg?.stopArg?.expression && foundArg.isBlocked) {\n    return handleExpressionSuggestions(value, foundArg, cursorContext)\n  }\n\n  const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext\n  const shouldHideSuggestions =\n    isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscaped)\n  if (shouldHideSuggestions) {\n    return {\n      helpWidget: {\n        isOpen: !!foundArg,\n        data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg },\n      },\n      suggestions: asSuggestionsRef([]),\n    }\n  }\n\n  const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions(\n    foundArg,\n    allArgs,\n    cursorContext.range,\n    fields,\n  )\n\n  return {\n    suggestions: asSuggestionsRef(suggestions, forceHide),\n    helpWidget: helpWidgetData,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/suggestions.ts",
    "content": "import { monaco } from 'react-monaco-editor'\nimport * as monacoEditor from 'monaco-editor'\nimport { findIndex } from 'lodash'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  bufferToString,\n  formatLongName,\n  generateArgsForInsertText,\n  getCommandMarkdown,\n  getDocUrlForCommand,\n  Nullable,\n} from 'uiSrc/utils'\nimport { FoundCommandArgument } from 'uiSrc/pages/workbench/types'\nimport {\n  DefinedArgumentName,\n  EmptySuggestionsIds,\n  ModuleCommandPrefix,\n  SORTED_SEARCH_COMMANDS,\n} from 'uiSrc/pages/workbench/constants'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport { IRedisCommand } from 'uiSrc/constants'\nimport { generateDetail } from './query'\nimport { buildSuggestion } from './monaco'\n\nexport const asSuggestionsRef = (\n  suggestions: monacoEditor.languages.CompletionItem[],\n  forceHide = true,\n  forceShow = true,\n) => ({\n  data: suggestions,\n  forceHide,\n  forceShow,\n})\n\nconst NO_INDEXES_DOC_LINK = getUtmExternalLink(\n  'https://redis.io/docs/latest/commands/ft.create/',\n  { campaign: 'workbench' },\n)\nexport const getNoIndexesSuggestion = (range: monaco.IRange) => [\n  {\n    id: EmptySuggestionsIds.NoIndexes,\n    label: 'No indexes to display',\n    kind: monacoEditor.languages.CompletionItemKind.Issue,\n    insertText: '',\n    insertTextRules:\n      monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    range,\n    detail: 'Create an index',\n    documentation: {\n      value: `See the [documentation](${NO_INDEXES_DOC_LINK}) for detailed instructions on how to create an index.`,\n    },\n  },\n]\n\nexport const getIndexesSuggestions = (\n  indexes: RedisResponseBuffer[],\n  range: monaco.IRange,\n  isNextArgQuery = true,\n  activeIndexName?: string,\n) =>\n  indexes.map((index) => {\n    const value = formatLongName(bufferToString(index))\n    const insertQueryQuotes = isNextArgQuery ? \" '\\${1:query to search}'\" : ''\n    const isActive = activeIndexName !== undefined && value === activeIndexName\n\n    return {\n      label: value || ' ',\n      kind: monacoEditor.languages.CompletionItemKind.Snippet,\n      insertText: `'${value}'${insertQueryQuotes} `,\n      insertTextRules:\n        monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      range,\n      detail: value || ' ',\n      preselect: isActive,\n      sortText: isActive ? `0_${value}` : `1_${value}`,\n    }\n  })\n\nexport const addFieldAttribute = (attribute: string, type: string) => {\n  switch (type) {\n    case 'TAG':\n      return `${attribute}:{\\${1:tag}}`\n    case 'TEXT':\n      return `${attribute}:(\\${1:term})`\n    case 'NUMERIC':\n      return `${attribute}:[\\${1:range}]`\n    case 'GEO':\n      return `${attribute}:[\\${1:lon} \\${2:lat} \\${3:radius} \\${4:unit}]`\n    case 'VECTOR':\n      return `${attribute} \\\\$\\${1:vector}`\n    default:\n      return attribute\n  }\n}\n\nexport const getFieldsSuggestions = (\n  fields: any[],\n  range: monaco.IRange,\n  spaceAfter = false,\n  withType = false,\n) =>\n  fields.map((field) => {\n    const { attribute, type } = field\n    const attibuteText = attribute.trim() ? attribute : `\\\\'${attribute}\\\\'`\n    const insertText = withType\n      ? addFieldAttribute(attibuteText, type)\n      : attibuteText\n\n    return {\n      label: attribute || ' ',\n      kind: monacoEditor.languages.CompletionItemKind.Reference,\n      insertText: `${insertText}${spaceAfter ? ' ' : ''}`,\n      insertTextRules:\n        monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      range,\n      detail: attribute || ' ',\n    }\n  })\n\nconst insertFunctionArguments = (args: IRedisCommand[]) =>\n  generateArgsForInsertText(\n    args.map(({ token, optional }) =>\n      optional ? `[${token}]` : token || '',\n    ) as string[],\n    ', ',\n  )\n\nexport const getFunctionsSuggestions = (\n  functions: IRedisCommand[],\n  range: monaco.IRange,\n) =>\n  functions.map(({ token, summary, arguments: args }) => ({\n    label: token || '',\n    insertText: `${token}(${insertFunctionArguments(args || [])})`,\n    insertTextRules:\n      monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    range,\n    kind: monacoEditor.languages.CompletionItemKind.Function,\n    detail: summary,\n  }))\n\nexport const getSortingForCommand = (command: IRedisCommand) => {\n  if (!command.token?.startsWith(ModuleCommandPrefix.RediSearch))\n    return command.token\n  if (!SORTED_SEARCH_COMMANDS.includes(command.token)) return command.token\n\n  const index = findIndex(\n    SORTED_SEARCH_COMMANDS,\n    (token) => token === command.token,\n  )\n  return `${ModuleCommandPrefix.RediSearch}_${index}`\n}\n\nexport const getCommandsSuggestions = (\n  commands: IRedisCommand[],\n  range: monaco.IRange,\n) =>\n  commands.map((command) =>\n    buildSuggestion(command, range, {\n      detail: generateDetail(command),\n      insertTextRules:\n        monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      documentation: {\n        value: getCommandMarkdown(\n          command as any,\n          command.name ? getDocUrlForCommand(command.name) : '',\n        ),\n      },\n      sortText: getSortingForCommand(command),\n    }),\n  )\n\nexport const getMandatoryArgumentSuggestions = (\n  foundArg: FoundCommandArgument,\n  fields: any[],\n  range: monaco.IRange,\n): monacoEditor.languages.CompletionItem[] => {\n  if (foundArg.stopArg?.name === DefinedArgumentName.field) {\n    if (!fields.length) return []\n    return getFieldsSuggestions(fields, range, true)\n  }\n\n  if (foundArg.isBlocked) return []\n  if (foundArg.append?.length) {\n    return foundArg.append[0].map((arg: any) =>\n      buildSuggestion(arg, range, {\n        kind: monacoEditor.languages.CompletionItemKind.Property,\n        detail: generateDetail(foundArg?.parent),\n      }),\n    )\n  }\n\n  return []\n}\n\nexport const getCommandSuggestions = (\n  foundArg: Nullable<FoundCommandArgument>,\n  allArgs: string[],\n  range: monaco.IRange,\n) => {\n  const appendCommands = foundArg?.append ?? []\n  const suggestions = []\n\n  for (let i = 0; i < appendCommands.length; i++) {\n    const isLastLevel = i === appendCommands.length - 1\n    const filteredFileldArgs = appendCommands[i]\n\n    const leveledSuggestions = filteredFileldArgs.map((arg) =>\n      buildSuggestion(arg, range, {\n        sortText: `${i}`,\n        kind: isLastLevel\n          ? monacoEditor.languages.CompletionItemKind.Reference\n          : monacoEditor.languages.CompletionItemKind.Property,\n        detail: generateDetail(arg?.parent),\n      }),\n    )\n\n    suggestions.push(leveledSuggestions)\n  }\n\n  return suggestions.flat()\n}\n\nexport const getGeneralSuggestions = (\n  foundArg: Nullable<FoundCommandArgument>,\n  allArgs: string[],\n  range: monacoEditor.IRange,\n  fields: any[],\n): {\n  suggestions: monacoEditor.languages.CompletionItem[]\n  forceHide?: boolean\n  helpWidgetData?: any\n} => {\n  if (foundArg) {\n    // TODO: check result\n    return {\n      // TODO: hope I need to recive proper append\n      suggestions:\n        foundArg?.stopArg && !foundArg?.stopArg.optional\n          ? getMandatoryArgumentSuggestions(foundArg, fields, range)\n          : getCommandSuggestions(foundArg, allArgs, range),\n      helpWidgetData: {\n        isOpen: !!foundArg?.stopArg,\n        data: {\n          parent: foundArg?.parent,\n          currentArg: foundArg?.stopArg,\n          token: foundArg?.token,\n        },\n      },\n    }\n  }\n\n  return {\n    suggestions: getCommandSuggestions(foundArg, allArgs, range),\n    helpWidgetData: { isOpen: false },\n  }\n}\n\nexport const isIndexComplete = (index: string) => {\n  if (index.length === 0) return false\n\n  const firstChar = index[0]\n  const lastChar = index[index.length - 1]\n\n  if (firstChar !== '\"' && firstChar !== \"'\") return true\n  if (index.length === 1 && (firstChar === '\"' || firstChar === \"'\"))\n    return false\n  if (firstChar !== lastChar) return false\n\n  let escape = false\n  for (let i = 1; i < index.length - 1; i++) {\n    escape = index[i] === '\\\\' && !escape\n  }\n\n  return !escape\n}\n\nexport const getParentWithOwnToken = (command?: IRedisCommand) => {\n  if (command?.token) {\n    return {\n      ...command,\n      arguments: command?.arguments\n        ? [{ name: command.token }, ...command.arguments]\n        : undefined,\n    }\n  }\n\n  return command\n}\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts",
    "content": "import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands'\nimport { getRediSearchSignutureProvider } from 'uiSrc/pages/workbench/utils/monaco'\n\nconst ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE']\n\nconst getRediSearchSignatureProviderTests = [\n  {\n    input: {\n      isOpen: false,\n      data: {\n        currentArg: {},\n        parent: {},\n      },\n    },\n    result: null,\n  },\n  {\n    input: {\n      isOpen: true,\n      data: {\n        currentArg: ftAggregateCommand.arguments.find(\n          ({ name }) => name === 'groupby',\n        ),\n        parent: null,\n      },\n    },\n    result: {\n      dispose: expect.any(Function),\n      value: {\n        activeParameter: 0,\n        activeSignature: 0,\n        signatures: [\n          {\n            label: '',\n            parameters: [{ label: 'nargs' }],\n          },\n        ],\n      },\n    },\n  },\n  {\n    input: {\n      isOpen: true,\n      data: {\n        currentArg: { name: 'expression' },\n        parent: ftAggregateCommand.arguments.find(\n          ({ name }) => name === 'apply',\n        ),\n      },\n    },\n    result: {\n      dispose: expect.any(Function),\n      value: {\n        activeParameter: 0,\n        activeSignature: 0,\n        signatures: [\n          {\n            label: 'APPLY expression AS name',\n            parameters: [{ label: 'expression' }],\n          },\n        ],\n      },\n    },\n  },\n]\n\ndescribe('getRediSearchSignatureProvider', () => {\n  it.each(getRediSearchSignatureProviderTests)(\n    'should properly return result',\n    ({ input, result }) => {\n      const testResult = getRediSearchSignutureProvider(input)\n\n      expect(result).toEqual(testResult)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/profile.spec.ts",
    "content": "import { ProfileQueryType } from '../../constants'\n\nimport {\n  generateGraphProfileQuery,\n  generateSearchProfileQuery,\n  generateProfileQueryForCommand,\n} from '../profile'\n\nconst generateGraphProfileQueryTests: Record<string, any>[] = [\n  {\n    input: 'GRAPH.QUERY key \"MATCH (n) RETURN n\"',\n    output: 'graph.profile key \"MATCH (n) RETURN n\"',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'GRAPH.QUERY key \"MATCH (n) RETURN n\"',\n    output: 'graph.explain key \"MATCH (n) RETURN n\"',\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'graph.query key \"MATCH (n) RETURN n\"',\n    output: 'graph.profile key \"MATCH (n) RETURN n\"',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'graph.query key \"MATCH (n) RETURN n\"',\n    output: 'graph.explain key \"MATCH (n) RETURN n\"',\n    type: ProfileQueryType.Explain,\n  },\n  { input: null, output: null, type: ProfileQueryType.Profile },\n  { input: null, output: null, type: ProfileQueryType.Explain },\n]\n\ndescribe('generateGraphProfileQuery', () => {\n  generateGraphProfileQueryTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} and type: ${test.type}`, () => {\n      const result = generateGraphProfileQuery(test.input, test.type)\n      expect(result).toEqual(test.output)\n    })\n  })\n})\n\nconst generateSearchProfileQueryTests: Record<string, any>[] = [\n  {\n    input: 'FT.SEARCH index tomatoes',\n    output: 'ft.profile index SEARCH QUERY tomatoes',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'FT.AGGREGATE index tomatoes',\n    output: 'ft.profile index AGGREGATE QUERY tomatoes',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'FT.SEARCH index tomatoes',\n    output: 'ft.explain index tomatoes',\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'FT.AGGREGATE index tomatoes',\n    output: 'ft.explain index tomatoes',\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'ft.search index tomatoes',\n    output: 'ft.profile index search QUERY tomatoes',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'ft.aggregate index tomatoes',\n    output: 'ft.profile index aggregate QUERY tomatoes',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'ft.search index tomatoes',\n    output: 'ft.explain index tomatoes',\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'ft.aggregate index tomatoes',\n    output: 'ft.explain index tomatoes',\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'ft.SEARCH index tomatoes',\n    output: 'ft.profile index SEARCH QUERY tomatoes',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'ft.AGGREGATE index tomatoes',\n    output: 'ft.profile index AGGREGATE QUERY tomatoes',\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'ft.SEARCH index tomatoes',\n    output: 'ft.explain index tomatoes',\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'ft.AGGREGATE index tomatoes',\n    output: 'ft.explain index tomatoes',\n    type: ProfileQueryType.Explain,\n  },\n  { input: null, output: null, type: ProfileQueryType.Profile },\n  { input: null, output: null, type: ProfileQueryType.Explain },\n]\n\ndescribe('generateSearchProfileQuery', () => {\n  generateSearchProfileQueryTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} and type: ${test.type}`, () => {\n      const result = generateSearchProfileQuery(test.input, test.type)\n      expect(result).toEqual(test.output)\n    })\n  })\n})\n\nconst generateProfileQueryForCommandTests: Record<string, any>[] = [\n  ...generateGraphProfileQueryTests,\n  ...generateSearchProfileQueryTests,\n  { input: 'GRAPH.LIST', output: null, type: ProfileQueryType.Profile },\n  { input: 'GRAPH.LIST', output: null, type: ProfileQueryType.Explain },\n  {\n    input: 'GRAPH.PROFILE key \"MATCH (n) RETURN n\"',\n    output: null,\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'GRAPH.PROFILE key \"MATCH (n) RETURN n\"',\n    output: null,\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'GRAPH.EXPLAIN key \"MATCH (n) RETURN n\"',\n    output: null,\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'GRAPH.EXPLAIN key \"MATCH (n) RETURN n\"',\n    output: null,\n    type: ProfileQueryType.Explain,\n  },\n  { input: 'ft._LIST', output: null, type: ProfileQueryType.Profile },\n  { input: 'ft._LIST', output: null, type: ProfileQueryType.Explain },\n  {\n    input: 'ft.profile index SEARCH QUERY tomatoes',\n    output: null,\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'ft.profile index AGGREGATE QUERY tomatoes',\n    output: null,\n    type: ProfileQueryType.Explain,\n  },\n  {\n    input: 'ft.explain index tomatoes',\n    output: null,\n    type: ProfileQueryType.Profile,\n  },\n  {\n    input: 'ft.explain index tomatoes',\n    output: null,\n    type: ProfileQueryType.Explain,\n  },\n]\ndescribe('generateProfileQueryForCommand', () => {\n  generateProfileQueryForCommandTests.forEach((test) => {\n    it(`should be output: ${test.output} for input: ${test.input} and type: ${test.type}`, () => {\n      const result = generateProfileQueryForCommand(test.input, test.type)\n\n      expect(result).toEqual(test.output)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts",
    "content": "import { Maybe, splitQueryByArgs } from 'uiSrc/utils'\nimport { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands'\nimport { IRedisCommand, ICommandTokenType } from 'uiSrc/constants'\nimport {\n  findSuggestionsByQueryArgs,\n  generateDetail,\n} from 'uiSrc/pages/workbench/utils/query'\nimport { commonfindCurrentArgumentCases } from './test-cases'\n\nconst ftSearchCommand = MOCKED_REDIS_COMMANDS['FT.SEARCH']\nconst ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE']\nconst COMMANDS = Object.keys(MOCKED_REDIS_COMMANDS).map((name) => ({\n  name,\n  ...MOCKED_REDIS_COMMANDS[name],\n}))\nconst COMPOSITE_ARGS = COMMANDS.filter(\n  (command) => command.name && command.name.includes(' '),\n).map(({ name }) => name)\n\ndescribe('findSuggestionsByQueryArgs', () => {\n  describe('with list of commands', () => {\n    commonfindCurrentArgumentCases.forEach(\n      ({ input, result, appendIncludes, appendNotIncludes }) => {\n        it(`should return proper suggestions for ${input}`, () => {\n          const { args } = splitQueryByArgs(\n            input,\n            0,\n            COMPOSITE_ARGS.concat('LOAD *'),\n          )\n          const COMMANDS_LIST = COMMANDS.map((command) => ({\n            ...command,\n            token: command.name!,\n            type: ICommandTokenType.Block,\n          }))\n\n          const testResult = findSuggestionsByQueryArgs(\n            COMMANDS_LIST,\n            args.flat(),\n          )\n          expect(result.stopArg ? testResult?.stopArg : undefined).toEqual(\n            result.stopArg,\n          )\n          expect(testResult?.append?.flat()?.map((arg) => arg.token)).toEqual(\n            expect.arrayContaining(appendIncludes),\n          )\n\n          if (appendNotIncludes) {\n            appendNotIncludes.forEach((token) => {\n              expect(\n                testResult?.append?.flat()?.map((arg) => arg.token),\n              ).not.toEqual(expect.arrayContaining([token]))\n            })\n          }\n        })\n      },\n    )\n  })\n})\n\nconst generateDetailTests: Array<{ input: Maybe<IRedisCommand>; result: any }> =\n  [\n    {\n      input: ftSearchCommand.arguments.find(\n        ({ name }) => name === 'nocontent',\n      ) as IRedisCommand,\n      result: 'NOCONTENT',\n    },\n    {\n      input: ftSearchCommand.arguments.find(\n        ({ name }) => name === 'filter',\n      ) as IRedisCommand,\n      result: 'FILTER numeric_field min max',\n    },\n    {\n      input: ftSearchCommand.arguments.find(\n        ({ name }) => name === 'geo_filter',\n      ) as IRedisCommand,\n      result: 'GEOFILTER geo_field lon lat radius m | km | mi | ft',\n    },\n    {\n      input: ftAggregateCommand.arguments.find(\n        ({ name }) => name === 'groupby',\n      ) as IRedisCommand,\n      result:\n        'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]',\n    },\n  ]\n\ndescribe('generateDetail', () => {\n  it.each(generateDetailTests)(\n    'should return for %input proper result',\n    ({ input, result }) => {\n      const testResult = generateDetail(input)\n      expect(testResult).toEqual(result)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts",
    "content": "// Common test cases\nexport const commonfindCurrentArgumentCases = [\n  {\n    input: 'FT.SEARCH index \"\" DIALECT 1',\n    result: {\n      stopArg: undefined,\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'],\n    appendNotIncludes: ['DIALECT'],\n  },\n  {\n    input: 'FT.AGGREGATE \"idx:schools\" \"\" GROUPBY 1 p REDUCE AVG 1 a1 AS name ',\n    result: {\n      stopArg: undefined,\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'],\n    appendNotIncludes: ['AS'],\n  },\n  {\n    input:\n      'FT.AGGREGATE \\'idx1:vd\\' \"*\" GROUPBY 1 @location REDUCE COUNT 0 AS item_count REDUCE SUM 1 @students ',\n    result: {\n      stopArg: {\n        name: 'name',\n        optional: true,\n        token: 'AS',\n        type: 'string',\n      },\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['AS', 'REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'],\n  },\n  {\n    input: 'FT.SEARCH \"idx:bicycle\" \"*\" ',\n    result: {\n      stopArg: expect.any(Object),\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['DIALECT', 'EXPANDER', 'INKEYS', 'LIMIT'],\n    appendNotIncludes: ['ASC'],\n  },\n  {\n    input: 'FT.SEARCH \"idx:bicycle\" \"*\" DIALECT 2',\n    result: expect.any(Object),\n    appendIncludes: ['EXPANDER', 'INKEYS', 'LIMIT'],\n    appendNotIncludes: ['DIALECT'],\n  },\n  {\n    input: \"FT.PROFILE 'idx:schools' SEARCH \",\n    result: expect.any(Object),\n    appendIncludes: ['LIMITED', 'QUERY'],\n    appendNotIncludes: ['AGGREGATE', 'SEARCH'],\n  },\n  {\n    input: 'FT.PROFILE idx AGGREGATE LIMITED ',\n    result: expect.any(Object),\n    appendIncludes: ['QUERY'],\n    appendNotIncludes: ['LIMITED', 'SEARCH'],\n  },\n  {\n    input: \"FT.PROFILE 'idx:schools' SEARCH QUERY 'q' \",\n    result: expect.any(Object),\n    appendIncludes: [],\n    appendNotIncludes: ['LIMITED'],\n  },\n  {\n    input: 'FT.CREATE \"idx:schools\" ',\n    result: expect.any(Object),\n    appendIncludes: ['FILTER', 'ON', 'SCHEMA', 'SCORE', 'NOHL', 'STOPWORDS'],\n    appendNotIncludes: ['HASH', 'JSON'],\n  },\n  {\n    input: 'FT.CREATE \"idx:schools\" ON',\n    result: expect.any(Object),\n    appendIncludes: ['HASH', 'JSON'],\n    appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'],\n  },\n  {\n    input: 'FT.CREATE \"idx:schools\" ON JSON NOFREQS',\n    result: expect.any(Object),\n    appendIncludes: [\n      'TEMPORARY',\n      'NOFIELDS',\n      'PAYLOAD_FIELD',\n      'MAXTEXTFIELDS',\n      'PREFIX',\n      'SKIPINITIALSCAN',\n    ],\n    appendNotIncludes: ['ON', 'JSON', 'NOFREQS'],\n  },\n  {\n    input: 'FT.CREATE \"idx:schools\" ON JSON NOFREQS SKIPINITIALSCAN',\n    result: expect.any(Object),\n    appendIncludes: [\n      'TEMPORARY',\n      'NOFIELDS',\n      'PAYLOAD_FIELD',\n      'MAXTEXTFIELDS',\n      'PREFIX',\n    ],\n    appendNotIncludes: ['ON', 'JSON', 'NOFREQS', 'SKIPINITIALSCAN'],\n  },\n  {\n    input: 'FT.CREATE \"idx:schools\" ON JSON SCHEMA address ',\n    result: {\n      stopArg: expect.any(Object),\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['AS', 'GEO', 'TEXT', 'VECTOR'],\n    appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'],\n  },\n  // TODO: need to investigte the case when we have NOINDEX 'FT.CREATE \"idx:schools\" ON JSON SCHEMA address TEXT NOINDEX '\n  // TODO: in this case we switch to field, but need to check?(or maybe not) all previous optional tokens\n  {\n    input: 'FT.CREATE \"idx:schools\" ON JSON SCHEMA address TEXT INDEXMISSING ',\n    result: {\n      stopArg: expect.any(Object),\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['INDEXEMPTY', 'SORTABLE', 'WITHSUFFIXTRIE', 'NOINDEX'],\n    appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'],\n  },\n  {\n    input:\n      'FT.CREATE \"idx:schools\" ON JSON SCHEMA address TEXT INDEXMISSING SORTABLE ',\n    result: {\n      stopArg: expect.any(Object),\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['INDEXEMPTY', 'UNF', 'WITHSUFFIXTRIE'],\n    appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'],\n  },\n  {\n    input: 'FT.ALTER \"idx:schools\" ',\n    result: {\n      stopArg: expect.any(Object),\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: ['SCHEMA', 'SKIPINITIALSCAN'],\n    appendNotIncludes: ['ADD'],\n  },\n  {\n    input: 'FT.ALTER \"idx:schools\" SCHEMA',\n    result: expect.any(Object),\n    appendIncludes: ['ADD'],\n    appendNotIncludes: ['SKIPINITIALSCAN'],\n  },\n  {\n    input: 'FT.CONFIG SET ',\n    result: {\n      stopArg: {\n        name: 'option',\n        type: 'string',\n      },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n    appendIncludes: [],\n    appendNotIncludes: [expect.any(String)],\n  },\n  {\n    input: 'FT.CURSOR READ \"idx:schools\" 1 ',\n    result: expect.any(Object),\n    appendIncludes: ['COUNT'],\n  },\n  {\n    input: 'FT.DICTADD dict term1 ',\n    result: {\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n      stopArg: {\n        multiple: true,\n        name: 'term',\n        type: 'string',\n      },\n    },\n    appendIncludes: [],\n  },\n  {\n    input: 'FT.SUGADD key string ',\n    result: {\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      stopArg: {\n        name: 'score',\n        type: 'double',\n      },\n      token: expect.any(Object),\n    },\n    appendIncludes: [],\n  },\n  {\n    input: 'FT.SUGADD key string 1.0 ',\n    result: expect.any(Object),\n    appendIncludes: ['INCR', 'PAYLOAD'],\n  },\n  {\n    input: 'FT.SUGADD key string 1.0 PAYLOAD 1 ',\n    result: expect.any(Object),\n    appendIncludes: ['INCR'],\n    appendNotIncludes: ['PAYLOAD'],\n  },\n  {\n    input: 'FT.SUGGET k p FUZZY MAX 2 ',\n    result: expect.any(Object),\n    appendIncludes: ['WITHPAYLOADS', 'WITHSCORES'],\n    appendNotIncludes: ['FUZZY', 'MAX'],\n  },\n  {\n    input: 'FT.ALTER index SKIPINITIALSCAN ',\n    result: expect.any(Object),\n    appendIncludes: ['SCHEMA'],\n    appendNotIncludes: ['ADD'],\n  },\n  {\n    input: 'FT.SPELLCHECK idx \"\" ',\n    result: expect.any(Object),\n    appendIncludes: ['DIALECT', 'DISTANCE', 'TERMS'],\n    appendNotIncludes: ['EXCLUDE', 'INCLUDE'],\n  },\n  {\n    input: 'FT.SEARCH index \"\" HIGHLIGHT FIELDS 1 f1 ',\n    result: expect.any(Object),\n    appendIncludes: [\n      'TAGS',\n      'SUMMARIZE',\n      'DIALECT',\n      'FILTER',\n      'WITHSCORES',\n      'INKEYS',\n    ],\n    appendNotIncludes: ['FIELDS'],\n  },\n  {\n    input: 'FT.SEARCH index \"*\" SORTBY price ',\n    result: expect.any(Object),\n    appendIncludes: [\n      'ASC',\n      'DESC',\n      'FILTER',\n      'LIMIT',\n      'DIALECT',\n      'WITHSCORES',\n      'INFIELDS',\n    ],\n    appendNotIncludes: ['SORTBY'],\n  },\n  {\n    input: 'FT.SEARCH textVehicles \"(-@make:Toyota)\" FILTER @year 2021 2022 ',\n    result: expect.any(Object),\n    appendIncludes: ['FILTER', 'GEOFILTER', 'TIMEOUT', 'WITHSORTKEYS'],\n    appendNotIncludes: ['AS', 'ASC'],\n  },\n  {\n    input: 'FT.SEARCH textVehicles \"*\" GEOFILTER geo_field lon lat radius ',\n    result: expect.any(Object),\n    appendIncludes: ['ft', 'km', 'm', 'mi'],\n    appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'AS', 'ASC'],\n  },\n  // skip\n  // {\n  //   input: 'FT.SEARCH textVehicles \"*\" RETURN 2 test ',\n  //   result: expect.any(Object),\n  //   appendIncludes: ['AS'],\n  //   appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'ASC'],\n  // },\n  {\n    input: 'FT.CREATE textVehicles ON ',\n    result: expect.any(Object),\n    appendIncludes: ['HASH', 'JSON'],\n    appendNotIncludes: [\n      'SORTBY',\n      'FILTER',\n      'LIMIT',\n      'DIALECT',\n      'WITHSCORES',\n      'INFIELDS',\n    ],\n  },\n  {\n    input: 'FT.CREATE textVehicles SCHEMA make ',\n    result: expect.any(Object),\n    appendIncludes: ['AS', 'GEO', 'NUMERIC', 'TAG', 'TEXT', 'VECTOR'],\n    appendNotIncludes: ['FILTER', 'LIMIT', 'DIALECT', 'WITHSCORES', 'INFIELDS'],\n  },\n  {\n    input: \"FT.AGGREGATE 'idx:articles' '@body:(term) ' APPLY 'test' \",\n    result: expect.any(Object),\n    appendIncludes: ['AS'],\n    appendNotIncludes: ['REDUCE', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'],\n  },\n  {\n    input: \"FT.AGGREGATE 'idx:articles' '@body:(term) ' APPLY 'test' AS test1\",\n    result: expect.any(Object),\n    appendIncludes: ['APPLY', 'LOAD', 'SORTBY', 'GROUPBY'],\n  },\n  {\n    input: \"FT.AGGREGATE 'idx:articles' '@body:(term) ' LOAD * \",\n    result: expect.any(Object),\n    appendIncludes: ['APPLY', 'LOAD', 'SORTBY', 'GROUPBY'],\n  },\n  {\n    input: \"FT.AGGREGATE 'idx:articles' '@body:(term) ' SORTBY 1 property \",\n    result: expect.any(Object),\n    appendIncludes: ['MAX', 'APPLY', 'GROUPBY'],\n    appendNotIncludes: ['REDUCE', 'ASC', 'DESC'],\n  },\n  {\n    input: \"FT.AGGREGATE 'idx:articles' '@body:(term) ' SORTBY 2 property ASC \",\n    result: expect.any(Object),\n    appendIncludes: ['MAX', 'APPLY', 'LOAD', 'GROUPBY'],\n    appendNotIncludes: ['SORTBY'],\n  },\n  {\n    input:\n      \"FT.AGGREGATE 'idx:articles' '@body:(term) ' PARAMS 4 name1 value1 name2 value2 \",\n    result: expect.any(Object),\n    appendIncludes: ['APPLY', 'LOAD', 'SORTBY', 'GROUPBY'],\n    appendNotIncludes: ['PARAMS', 'REDUCE'],\n  },\n  {\n    input: 'FT.ALTER index SCHEMA ADD sdfsd fsdfsd ',\n    result: expect.any(Object),\n    appendIncludes: [],\n    appendNotIncludes: ['SKIPINITIALSCAN', 'ADD', 'SCHEMA'],\n  },\n  {\n    input: \"FT.DROPINDEX 'vd' \",\n    result: expect.any(Object),\n    appendIncludes: ['DD'],\n  },\n  {\n    input: 'FT.EXPLAIN index query ',\n    result: expect.any(Object),\n    appendIncludes: ['DIALECT'],\n    appendNotIncludes: [\n      'SKIPINITIALSCAN',\n      'ADD',\n      'SCHEMA',\n      'APPLY',\n      'LOAD',\n      'SORTBY',\n      'GROUPBY',\n    ],\n  },\n  {\n    input: 'FT.EXPLAINCLI index query ',\n    result: expect.any(Object),\n    appendIncludes: ['DIALECT'],\n    appendNotIncludes: [\n      'SKIPINITIALSCAN',\n      'ADD',\n      'SCHEMA',\n      'APPLY',\n      'LOAD',\n      'SORTBY',\n      'GROUPBY',\n    ],\n  },\n  {\n    input: 'FT.INFO index ',\n    result: expect.any(Object),\n    appendIncludes: [],\n    appendNotIncludes: ['ADD', 'SCHEMA', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'],\n  },\n  {\n    input: \"FT.PROFILE 'idx:schools' \",\n    result: expect.any(Object),\n    appendIncludes: ['AGGREGATE', 'SEARCH'],\n    appendNotIncludes: ['LIMITED'],\n  },\n  {\n    input: \"FT.SPELLCHECK 'idx:articles' 'test' DIALECT d DISTANCE d TERMS \",\n    result: expect.any(Object),\n    appendIncludes: ['EXCLUDE', 'INCLUDE'],\n    appendNotIncludes: ['DIALECT', 'DISTANCE', 'TERMS'],\n  },\n  {\n    input: \"FT.SYNUPDATE 'idx:products' synonym_group_id \",\n    result: expect.any(Object),\n    appendIncludes: ['SKIPINITIALSCAN'],\n    appendNotIncludes: [\n      'DIALECT',\n      'DISTANCE',\n      'TERMS',\n      'INCLUDE',\n      'SCHEMA',\n      'APPLY',\n      'LOAD',\n      'SORTBY',\n      'GROUPBY',\n    ],\n  },\n  {\n    input:\n      \"FT.SEARCH 'idx' 'query to search' PARAMS 2 p1 p2 RETURN 3 p1 p2 p3 DIALECT \",\n    result: {\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n      stopArg: {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    },\n    appendIncludes: [],\n  },\n  // TODO: fix this case\n  {\n    input:\n      \"FT.SEARCH 'idx' 'query to search' SORTBY a ASC PARAMS 3 a a2 a3 DIALECT \",\n    result: {\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n      stopArg: {\n        name: 'dialect',\n        type: 'integer',\n        optional: true,\n        token: 'DIALECT',\n        since: '2.4.3',\n      },\n    },\n    appendIncludes: [],\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts",
    "content": "export const findArgumentftAggreageTests = [\n  {\n    args: ['index', '\"query\"', 'APPLY'],\n    result: {\n      stopArg: { name: 'expression', token: 'APPLY', type: 'string' },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'APPLY', 'expression'],\n    result: {\n      stopArg: { name: 'name', token: 'AS', type: 'string' },\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'APPLY', 'expression', 'AS'],\n    result: {\n      stopArg: { name: 'name', token: 'AS', type: 'string' },\n      append: expect.any(Array),\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'APPLY', 'expression', 'AS', 'name'],\n    result: {\n      stopArg: undefined,\n      append: expect.any(Array),\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['\"\"', '\"\"', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f'],\n    result: {\n      stopArg: { name: 'nargs', type: 'integer' },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['\"\"', '\"\"', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '0'],\n    result: {\n      stopArg: {\n        name: 'name',\n        type: 'string',\n        token: 'AS',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'name',\n            type: 'string',\n            token: 'AS',\n            optional: true,\n            parent: {\n              name: 'reduce',\n              type: 'block',\n              optional: true,\n              multiple: true,\n              arguments: [\n                {\n                  name: 'function',\n                  token: 'REDUCE',\n                  type: 'string',\n                },\n                {\n                  name: 'nargs',\n                  type: 'integer',\n                },\n                {\n                  name: 'arg',\n                  type: 'string',\n                  multiple: true,\n                },\n                {\n                  name: 'name',\n                  type: 'string',\n                  token: 'AS',\n                  optional: true,\n                },\n              ],\n              parent: expect.any(Object),\n            },\n          },\n        ],\n        [\n          {\n            name: 'function',\n            token: 'REDUCE',\n            type: 'string',\n            multiple: true,\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: [\n      '\"\"',\n      '\"\"',\n      'GROUPBY',\n      '2',\n      'p1',\n      'p2',\n      'REDUCE',\n      'f',\n      '1',\n      'AS',\n      'name',\n    ],\n    result: {\n      stopArg: undefined,\n      append: [\n        [],\n        [\n          {\n            name: 'function',\n            token: 'REDUCE',\n            type: 'string',\n            multiple: true,\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'SORTBY'],\n    result: {\n      stopArg: { name: 'nargs', token: 'SORTBY', type: 'integer' },\n      append: expect.any(Array),\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'SORTBY', '1', 'p1'],\n    result: {\n      stopArg: {\n        name: 'num',\n        type: 'integer',\n        token: 'MAX',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'num',\n            type: 'integer',\n            token: 'MAX',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'SORTBY', '2', 'p1', 'ASC'],\n    result: {\n      stopArg: {\n        name: 'num',\n        type: 'integer',\n        token: 'MAX',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'num',\n            type: 'integer',\n            token: 'MAX',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'SORTBY', '0'],\n    result: {\n      stopArg: {\n        name: 'num',\n        type: 'integer',\n        token: 'MAX',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'num',\n            type: 'integer',\n            token: 'MAX',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'SORTBY', '2', 'p1', 'ASC', 'MAX'],\n    result: {\n      stopArg: {\n        name: 'num',\n        type: 'integer',\n        token: 'MAX',\n        optional: true,\n      },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'LOAD', '4'],\n    result: {\n      stopArg: { multiple: true, name: 'field', type: 'string' },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'LOAD', '4', '1', '2', '3'],\n    result: {\n      stopArg: { multiple: true, name: 'field', type: 'string' },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['index', '\"query\"', 'LOAD', '4', '1', '2', '3', '4'],\n    result: {\n      stopArg: undefined,\n      append: [],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts",
    "content": "export const findArgumentftSearchTests = [\n  {\n    args: ['', '', 'SUMMARIZE'],\n    result: {\n      stopArg: {\n        name: 'fields',\n        type: 'block',\n        optional: true,\n        arguments: [\n          {\n            name: 'count',\n            type: 'string',\n            token: 'FIELDS',\n          },\n          {\n            name: 'field',\n            type: 'string',\n            multiple: true,\n          },\n        ],\n      },\n      append: [\n        [\n          {\n            name: 'count',\n            type: 'string',\n            token: 'FIELDS',\n            optional: true,\n            parent: expect.any(Object),\n          },\n          {\n            name: 'num',\n            type: 'integer',\n            token: 'FRAGS',\n            optional: true,\n            parent: expect.any(Object),\n          },\n          {\n            name: 'fragsize',\n            type: 'integer',\n            token: 'LEN',\n            optional: true,\n            parent: expect.any(Object),\n          },\n          {\n            name: 'separator',\n            type: 'string',\n            token: 'SEPARATOR',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'SUMMARIZE', 'FIELDS'],\n    result: {\n      stopArg: {\n        name: 'count',\n        type: 'string',\n        token: 'FIELDS',\n      },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'SUMMARIZE', 'FIELDS', '1'],\n    result: {\n      stopArg: {\n        name: 'field',\n        type: 'string',\n        multiple: true,\n      },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS'],\n    result: {\n      stopArg: {\n        name: 'num',\n        type: 'integer',\n        token: 'FRAGS',\n        optional: true,\n      },\n      append: [],\n      isBlocked: true,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS', '10'],\n    result: {\n      stopArg: {\n        name: 'fragsize',\n        type: 'integer',\n        token: 'LEN',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'fragsize',\n            type: 'integer',\n            token: 'LEN',\n            optional: true,\n            parent: expect.any(Object),\n          },\n          {\n            name: 'separator',\n            type: 'string',\n            token: 'SEPARATOR',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'RETURN', '1', 'iden'],\n    result: {\n      stopArg: expect.any(Object),\n      append: [],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'RETURN', '2', 'iden'],\n    result: {\n      stopArg: {\n        name: 'property',\n        type: 'string',\n        token: 'AS',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'property',\n            type: 'string',\n            token: 'AS',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n        [],\n      ],\n      isBlocked: false,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'RETURN', '2', 'iden', 'iden'],\n    result: {\n      stopArg: undefined,\n      append: [],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'RETURN', '3', 'iden', 'iden'],\n    result: {\n      stopArg: {\n        name: 'property',\n        type: 'string',\n        token: 'AS',\n        optional: true,\n      },\n      append: [\n        [\n          {\n            name: 'property',\n            type: 'string',\n            token: 'AS',\n            optional: true,\n            parent: expect.any(Object),\n          },\n        ],\n        [],\n      ],\n      isBlocked: false,\n      isComplete: false,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'],\n    result: {\n      stopArg: undefined,\n      append: [],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'SORTBY', 'f'],\n    result: {\n      stopArg: {\n        name: 'order',\n        type: 'oneof',\n        optional: true,\n        arguments: [\n          {\n            name: 'asc',\n            type: 'pure-token',\n            token: 'ASC',\n          },\n          {\n            name: 'desc',\n            type: 'pure-token',\n            token: 'DESC',\n          },\n        ],\n      },\n      append: [\n        [\n          {\n            name: 'asc',\n            type: 'pure-token',\n            token: 'ASC',\n            parent: expect.any(Object),\n          },\n          {\n            name: 'desc',\n            type: 'pure-token',\n            token: 'DESC',\n            parent: expect.any(Object),\n          },\n        ],\n      ],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'SORTBY', 'f', 'DESC'],\n    result: {\n      stopArg: undefined,\n      append: [[]],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n  {\n    args: ['', '', 'DIALECT', '1'],\n    result: {\n      stopArg: undefined,\n      append: [[]],\n      isBlocked: false,\n      isComplete: true,\n      parent: expect.any(Object),\n      token: expect.any(Object),\n    },\n  },\n]\n"
  },
  {
    "path": "redisinsight/ui/src/pages/workbench/utils/tests/test-cases/index.ts",
    "content": "export * from './ft-aggregate'\nexport * from './ft-search'\nexport * from './common'\n"
  },
  {
    "path": "redisinsight/ui/src/plugins/pluginEvents.spec.ts",
    "content": "import { fireEvent } from 'uiSrc/utils/test-utils'\nimport { pluginApi } from 'uiSrc/services/PluginAPI'\nimport { listenPluginsEvents, PluginEvents } from './pluginEvents'\n\njest.mock('uiSrc/services/PluginAPI', () => ({\n  pluginApi: {\n    sendEvent: jest.fn(),\n  },\n}))\n\nconst createEvent = (data = {}): [string, MessageEventInit] => [\n  'message',\n  { data: { iframeId: 'id', ...data } },\n]\n\ndescribe('listenPluginsEvents', () => {\n  it('should call proper options', () => {\n    const sendEventMock = jest.fn()\n\n    ;(pluginApi.sendEvent as jest.Mock).mockImplementation(sendEventMock)\n\n    listenPluginsEvents()\n\n    fireEvent(\n      window,\n      new MessageEvent(...createEvent({ event: PluginEvents.loaded })),\n    )\n    expect(sendEventMock).toBeCalledWith('id', PluginEvents.loaded)\n    sendEventMock.mockRestore()\n\n    fireEvent(\n      window,\n      new MessageEvent(\n        ...createEvent({ event: PluginEvents.error, error: 'Some error' }),\n      ),\n    )\n    expect(sendEventMock).toBeCalledWith('id', PluginEvents.error, 'Some error')\n    sendEventMock.mockRestore()\n\n    fireEvent(\n      window,\n      new MessageEvent(\n        ...createEvent({ event: PluginEvents.heightChanged, height: 100 }),\n      ),\n    )\n    expect(sendEventMock).toBeCalledWith('id', PluginEvents.heightChanged, 100)\n    sendEventMock.mockRestore()\n\n    fireEvent(\n      window,\n      new MessageEvent(\n        ...createEvent({\n          event: PluginEvents.executeRedisCommand,\n          command: 'c',\n          requestId: 1,\n        }),\n      ),\n    )\n    expect(sendEventMock).toBeCalledWith(\n      'id',\n      PluginEvents.executeRedisCommand,\n      { command: 'c', requestId: 1 },\n    )\n    sendEventMock.mockRestore()\n\n    fireEvent(\n      window,\n      new MessageEvent(\n        ...createEvent({ event: PluginEvents.setHeaderText, text: 'text' }),\n      ),\n    )\n    expect(sendEventMock).toBeCalledWith(\n      'id',\n      PluginEvents.setHeaderText,\n      'text',\n    )\n    sendEventMock.mockRestore()\n\n    fireEvent(\n      window,\n      new MessageEvent(\n        ...createEvent({ event: PluginEvents.getState, requestId: 'id' }),\n      ),\n    )\n    expect(sendEventMock).toBeCalledWith('id', PluginEvents.getState, {\n      requestId: 'id',\n    })\n    sendEventMock.mockRestore()\n\n    fireEvent(\n      window,\n      new MessageEvent(\n        ...createEvent({\n          event: PluginEvents.setState,\n          requestId: 'id',\n          state: {},\n        }),\n      ),\n    )\n    expect(sendEventMock).toBeCalledWith('id', PluginEvents.setState, {\n      requestId: 'id',\n      state: {},\n    })\n    sendEventMock.mockRestore()\n\n    fireEvent(window, new MessageEvent(...createEvent({ event: 'click' })))\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/plugins/pluginEvents.ts",
    "content": "import { pluginApi } from 'uiSrc/services/PluginAPI'\n\nconst dispatchBodyEvent = (mouseEventType: string) => {\n  document.querySelector('body')?.dispatchEvent(\n    new MouseEvent(mouseEventType, {\n      view: window,\n      bubbles: true,\n      cancelable: true,\n      buttons: 1,\n    }),\n  )\n}\n\nexport enum PluginEvents {\n  loaded = 'loaded',\n  error = 'error',\n  heightChanged = 'heightChanged',\n  setHeaderText = 'setHeaderText',\n  executeRedisCommand = 'executeRedisCommand',\n  getState = 'getState',\n  setState = 'setState',\n  formatRedisReply = 'formatRedisReply',\n}\n\nexport const listenPluginsEvents = () => {\n  globalThis.onmessage = (e: MessageEvent) => {\n    switch (e.data?.event) {\n      case PluginEvents.loaded: {\n        pluginApi.sendEvent(e.data.iframeId, PluginEvents.loaded)\n        break\n      }\n      case PluginEvents.error: {\n        pluginApi.sendEvent(e.data.iframeId, PluginEvents.error, e.data.error)\n        break\n      }\n      case PluginEvents.heightChanged: {\n        pluginApi.sendEvent(\n          e.data.iframeId,\n          PluginEvents.heightChanged,\n          e.data.height,\n        )\n        break\n      }\n      case PluginEvents.executeRedisCommand: {\n        pluginApi.sendEvent(e.data.iframeId, PluginEvents.executeRedisCommand, {\n          command: e.data.command,\n          requestId: e.data.requestId,\n        })\n        break\n      }\n      case PluginEvents.setHeaderText: {\n        pluginApi.sendEvent(\n          e.data.iframeId,\n          PluginEvents.setHeaderText,\n          e.data.text,\n        )\n        break\n      }\n      case PluginEvents.getState: {\n        pluginApi.sendEvent(e.data.iframeId, PluginEvents.getState, {\n          requestId: e.data.requestId,\n        })\n        break\n      }\n      case PluginEvents.setState: {\n        pluginApi.sendEvent(e.data.iframeId, PluginEvents.setState, {\n          requestId: e.data.requestId,\n          state: e.data.state,\n        })\n        break\n      }\n      case PluginEvents.formatRedisReply: {\n        pluginApi.sendEvent(e.data.iframeId, PluginEvents.formatRedisReply, {\n          requestId: e.data.requestId,\n          data: e.data.data,\n        })\n        break\n      }\n      case 'click': {\n        // Simulate bubbling from iframe\n        ;['mousedown', 'click', 'mouseup'].forEach(dispatchBodyEvent)\n        break\n      }\n      default:\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/plugins/pluginImport.spec.ts",
    "content": "import {\n  prepareIframeHtml,\n  importPluginScript,\n} from 'uiSrc/plugins/pluginImport'\n\ndescribe('pluginImport', () => {\n  it('should render html with required tags', () => {\n    const html = prepareIframeHtml({ stylesSrc: [] })\n    const div = document.createElement('div')\n\n    expect(html).toContain('<body')\n    expect(html).toContain('<head')\n    expect(html).toContain('globalThis.plugin = {}')\n\n    div.innerHTML = html\n\n    expect(div.querySelector('#app')).toBeTruthy()\n  })\n})\n\ndescribe('importPluginScript', () => {\n  it('should set proper methods to Plugin SDK', () => {\n    globalThis.ResizeObserver = jest.fn(() => ({\n      observe: jest.fn(),\n    }))\n\n    importPluginScript()(JSON.stringify({}))\n    expect(globalThis.PluginSDK).toEqual({\n      setPluginLoadFailed: expect.any(Function),\n      setPluginLoadSucceed: expect.any(Function),\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/plugins/pluginImport.ts",
    "content": "/* eslint-disable sonarjs/no-nested-template-literals */\n/* eslint-disable no-restricted-globals */\n// @ts-nocheck\nexport const importPluginScript = () => (config) => {\n  const { scriptSrc, stylesSrc, iframeId, modules, baseUrl, appVersion } =\n    JSON.parse(config)\n  const events = {\n    ERROR: 'error',\n    LOADED: 'loaded',\n    EXECUTE_COMMAND: 'executeCommand',\n    SET_HEADER_TEXT: 'setHeaderText',\n    EXECUTE_REDIS_COMMAND: 'executeRedisCommand',\n    GET_STATE: 'getState',\n    SET_STATE: 'setState',\n    FORMAT_REDIS_REPLY: 'formatRedisReply',\n  }\n\n  Object.defineProperty(globalThis, 'state', {\n    value: {\n      callbacks: { counter: 0 },\n      pluginState: {},\n      config: { scriptSrc, stylesSrc, iframeId, baseUrl, appVersion },\n      modules,\n    },\n    writable: false,\n  })\n\n  const { callbacks } = globalThis.state\n\n  const sendMessageToMain = (data = {}) => {\n    const event = document.createEvent('Event')\n    event.initEvent('message', false, false)\n    event.data = data\n    event.origin = '*'\n    parent.dispatchEvent(event)\n  }\n\n  const providePluginSDK = () => {\n    globalThis.PluginSDK = {\n      setPluginLoadSucceed: () => {\n        sendMessageToMain({\n          event: events.LOADED,\n          iframeId,\n        })\n      },\n      setPluginLoadFailed: (error) => {\n        sendMessageToMain({\n          event: events.ERROR,\n          iframeId,\n          error,\n        })\n      },\n    }\n  }\n\n  const listenEvents = () => {\n    const promiseEvents = [\n      events.EXECUTE_REDIS_COMMAND,\n      events.GET_STATE,\n      events.SET_STATE,\n      events.FORMAT_REDIS_REPLY,\n    ]\n    globalThis.onmessage = (e) => {\n      // eslint-disable-next-line sonarjs/no-collapsible-if\n      if (e.data.event === events.EXECUTE_COMMAND) {\n        const { plugin } = globalThis\n        // eslint-disable-next-line no-prototype-builtins\n        if (!plugin.hasOwnProperty(e.data.method)) {\n          return\n        }\n        const action = plugin[e.data.method]\n        if (typeof action === 'function') {\n          action(e.data.data)\n        }\n      }\n\n      // eslint-disable-next-line sonarjs/no-collapsible-if\n      if (promiseEvents.includes(e.data.event)) {\n        // eslint-disable-next-line no-prototype-builtins\n        if (callbacks.hasOwnProperty(e.data.requestId)) {\n          const actions = callbacks[e.data.requestId]\n          // eslint-disable-next-line no-prototype-builtins\n          if (actions && actions.hasOwnProperty(e.data.actionType)) {\n            const action = actions[e.data.actionType]\n            if (typeof action === 'function') {\n              action(e.data.data)\n            }\n            delete callbacks[e.data.requestId]\n          }\n        }\n      }\n    }\n\n    const resizeObserver = new ResizeObserver(() => {\n      sendMessageToMain({\n        event: 'heightChanged',\n        iframeId,\n        height: document.body.offsetHeight,\n      })\n    })\n\n    resizeObserver.observe(document.body)\n\n    document.addEventListener('click', () => {\n      sendMessageToMain({\n        event: 'click',\n        iframeId,\n      })\n    })\n  }\n\n  providePluginSDK()\n  listenEvents()\n}\n\nexport const prepareIframeHtml = (config) => {\n  const importPluginScriptInner: string = importPluginScript().toString()\n  const { scriptSrc, scriptPath, stylesSrc, bodyClass } = config\n  const stylesLinks = stylesSrc\n    .map((styleSrc: string) => `<link rel=\"stylesheet\" href=${styleSrc} />`)\n    .join('')\n  const configString = JSON.stringify(config)\n\n  return `\n      <head>\n        ${stylesLinks}\n        <!-- Forbid XMLHttpRequest (AJAX), WebSocket, fetch(), <a ping> or EventSource -->\n        <meta http-equiv=\"Content-Security-Policy\" content=\"connect-src 'none';\">\n      </head>\n      <body class=\"${bodyClass}\" style=\"height: fit-content\">\n        <script>\n          try {\n            document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n          } catch {\n            document.createElementNS = window.parent.document.createElementNS\n          }\n        </script>\n        <div id=\"app\"></div>\n        <script>\n          globalThis.plugin = {}\n          ;(${importPluginScriptInner})(\\`${configString}\\`);\n          import(\\`${scriptSrc}\\`)\n              .then((module) => {\n                  globalThis.plugin = { ...module.default };\n                  globalThis.PluginSDK.setPluginLoadSucceed();\n              })\n              .catch((e) => {\n                  var error = \\`${scriptPath} not found. Check if it has been renamed or deleted and try again.\\`\n                  globalThis.PluginSDK.setPluginLoadFailed(error)\n              })\n        </script>\n        <script src=\"${scriptSrc}\" type=\"module\"></script>\n      </body>\n`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/resourses/en-EN.ts",
    "content": "const RESOURCES = {\n  Pro: 'Pro',\n  Fixed: 'Fixed',\n  Annual: 'Annual',\n}\n\nexport default RESOURCES\n"
  },
  {
    "path": "redisinsight/ui/src/services/PluginAPI.ts",
    "content": "class PluginAPIService {\n  private subscriptions: any = {}\n\n  onEvent(iframeId = '', event = '', callback: any) {\n    this.subscriptions[iframeId] = {\n      ...this.subscriptions[iframeId],\n      [event]: callback,\n    }\n  }\n\n  sendEvent(iframeId = '', event = '', data?: any) {\n    this.subscriptions[iframeId]?.[event]?.(data)\n  }\n\n  unregisterSubscriptions() {\n    this.subscriptions = {}\n  }\n}\n\nconst pluginApi = new PluginAPIService()\n\nexport { pluginApi }\n"
  },
  {
    "path": "redisinsight/ui/src/services/apiService.ts",
    "content": "import axios, {\n  AxiosInstance,\n  AxiosError,\n  InternalAxiosRequestConfig,\n} from 'axios'\nimport { isNumber } from 'lodash'\nimport { sessionStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem, CustomErrorCodes } from 'uiSrc/constants'\nimport { CLOUD_AUTH_API_ENDPOINTS, CustomHeaders } from 'uiSrc/constants/api'\nimport { store } from 'uiSrc/slices/store'\nimport { logoutUserAction } from 'uiSrc/slices/oauth/cloud'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport { getConfig } from 'uiSrc/config'\nimport ApiErrors from 'uiSrc/constants/apiErrors'\n\nconst riConfig = getConfig()\n\nconst { apiPort } = window.app?.config || { apiPort: riConfig.api.port }\nconst isDevelopment = riConfig.app.env === 'development'\nconst isWebApp = riConfig.app.type === 'web'\nconst hostedApiBaseUrl = riConfig.api.hostedBaseUrl\n\nlet apiPrefix = riConfig.api.prefix\n\nif (window.__RI_PROXY_PATH__) {\n  apiPrefix = `${window.__RI_PROXY_PATH__}/${apiPrefix}`\n}\n\nexport const getBaseUrl = () =>\n  !isDevelopment && isWebApp\n    ? `${window.location.origin}/${apiPrefix}/`\n    : `${riConfig.api.baseUrl}:${apiPort}/${apiPrefix}/`\n\nconst mutableAxiosInstance: AxiosInstance = axios.create({\n  baseURL: hostedApiBaseUrl || getBaseUrl(),\n  withCredentials: !!hostedApiBaseUrl,\n})\n\nexport const setApiCsrfHeader = (token: string) => {\n  mutableAxiosInstance.defaults.headers.common[CustomHeaders.CsrfToken] = token\n}\n\nexport const requestInterceptor = (config: InternalAxiosRequestConfig) => {\n  if (config?.headers) {\n    const instanceId = /databases\\/([\\w-]+)\\/?.*/.exec(config.url || '')?.[1]\n\n    if (instanceId) {\n      const dbIndex = sessionStorageService.get(\n        `${BrowserStorageItem.dbIndex}${instanceId}`,\n      )\n\n      if (isNumber(dbIndex)) {\n        config.headers[CustomHeaders.DbIndex] = dbIndex\n      }\n    }\n\n    if (window.windowId) {\n      config.headers[CustomHeaders.WindowId] = window.windowId\n    }\n  }\n\n  return config\n}\n\nexport const cloudAuthInterceptor = (error: AxiosError) => {\n  const { response, config } = error\n  if (\n    response?.status === 401 &&\n    config?.url &&\n    CLOUD_AUTH_API_ENDPOINTS.includes(config.url as any)\n  ) {\n    store?.dispatch<any>(logoutUserAction?.())\n  }\n\n  return Promise.reject(error)\n}\n\nexport const hostedAuthInterceptor = (error: AxiosError) => {\n  const { response } = error\n  if (response?.status === 401 && hostedApiBaseUrl) {\n    const noPermission =\n      (response?.data as any)?.message === 'Insufficient permissions'\n    // provide the current path to redirect back to the same location after login\n    window.location.href = noPermission\n      ? riConfig.app.returnUrlBase!\n      : `${riConfig.app.unauthenticatedRedirect}${window.location.pathname}`\n  }\n  return Promise.reject(error)\n}\n\nexport const isConnectivityError = (\n  status?: number,\n  data?: { code?: string; error?: string },\n): boolean => {\n  if (!status || !data) {\n    return false\n  }\n\n  switch (status) {\n    case 424:\n      return !!data.error?.startsWith?.('RedisConnection')\n    case 503:\n      return (\n        data.code === 'serviceUnavailable' ||\n        data.error === 'Service Unavailable'\n      )\n    default:\n      return false\n  }\n}\n\nexport const connectivityErrorsInterceptor = (error: AxiosError) => {\n  const { response } = error\n  const responseUrl = response?.request?.responseURL || ''\n  const responseData = response?.data as {\n    message?: string\n    code?: string\n    error?: string\n    errorCode?: number\n  }\n\n  if (isConnectivityError(response?.status, responseData)) {\n    let message\n\n    if (\n      responseData?.errorCode ===\n      CustomErrorCodes.RedisConnectionDefaultUserDisabled\n    ) {\n      message = responseData?.message\n    }\n\n    const state = store.getState()\n    const isConnectedToDatabase =\n      !!state.connections.instances.connectedInstance?.id\n    const isErrorTargetsConnectedDatabase = responseUrl.includes(\n      state.connections.instances.connectedInstance?.id,\n    )\n\n    if (isConnectedToDatabase && isErrorTargetsConnectedDatabase) {\n      store?.dispatch<any>(\n        setConnectivityError(message || ApiErrors.ConnectionLost),\n      )\n    }\n  }\n\n  return Promise.reject(error)\n}\n\nmutableAxiosInstance.interceptors.request.use(requestInterceptor, (error) =>\n  Promise.reject(error),\n)\n\nmutableAxiosInstance.interceptors.response.use(undefined, cloudAuthInterceptor)\n\nmutableAxiosInstance.interceptors.response.use(undefined, hostedAuthInterceptor)\n\nmutableAxiosInstance.interceptors.response.use(\n  undefined,\n  connectivityErrorsInterceptor,\n)\n\nexport default mutableAxiosInstance\n"
  },
  {
    "path": "redisinsight/ui/src/services/capability.ts",
    "content": "import { CapabilityStorageItem } from 'uiSrc/constants/storage'\nimport {\n  getCapabilityStorageField,\n  setCapabilityStorageField,\n} from 'uiSrc/services'\nimport { getTutorialCapability } from 'uiSrc/utils'\n\nconst TIME_TO_READ_POPOVER_TEXT = 1_000\n\nexport const isShowCapabilityTutorialPopover = (isFree = false) =>\n  !!isFree &&\n  !getCapabilityStorageField(CapabilityStorageItem.tutorialPopoverShown) &&\n  getTutorialCapability(getCapabilityStorageField(CapabilityStorageItem.source))\n    ?.name\n\nexport const setCapabilityPopoverShown = () => {\n  setTimeout(() => {\n    setCapabilityStorageField(CapabilityStorageItem.tutorialPopoverShown, true)\n  }, TIME_TO_READ_POPOVER_TEXT)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/commandsHistoryService.spec.ts",
    "content": "import { merge } from 'lodash'\nimport { faker } from '@faker-js/faker'\n\nimport { RootState, store } from 'uiSrc/slices/store'\nimport {\n  CommandExecutionType,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { commandExecutionUIFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\n\nimport { CommandsHistoryService } from './commandsHistoryService'\nimport { CommandsHistorySQLite } from './database/CommandsHistorySQLite'\nimport { CommandsHistoryIndexedDB } from './database/CommandsHistoryIndexedDB'\nimport { initialState as appFeaturesInitialState } from 'uiSrc/slices/app/features'\n\n// Mock the database classes\njest.mock('./database/CommandsHistorySQLite')\njest.mock('./database/CommandsHistoryIndexedDB', () => ({\n  CommandsHistoryIndexedDB: jest.fn().mockImplementation(() => ({\n    getCommandsHistory: jest\n      .fn()\n      .mockResolvedValue({ success: true, data: [] }),\n    getCommandHistory: jest\n      .fn()\n      .mockResolvedValue({ success: true, data: null }),\n    addCommandsToHistory: jest\n      .fn()\n      .mockResolvedValue({ success: true, data: [] }),\n    deleteCommandFromHistory: jest.fn().mockResolvedValue({ success: true }),\n    clearCommandsHistory: jest.fn().mockResolvedValue({ success: true }),\n  })),\n}))\n\n// Mock the notification action\njest.mock('uiSrc/slices/app/notifications', () => ({\n  addErrorNotification: jest.fn((error) => ({\n    type: 'app/notifications/addErrorNotification',\n    payload: error,\n  })),\n}))\n\n// Mock the store module\njest.mock('uiSrc/slices/store', () => ({\n  store: {\n    getState: jest.fn(),\n    dispatch: jest.fn(),\n  },\n}))\n\nconst mockedCommandsHistorySQLite = jest.mocked(CommandsHistorySQLite)\nconst mockedCommandsHistoryIndexedDB = jest.mocked(CommandsHistoryIndexedDB)\n\ndescribe('CommandsHistoryService', () => {\n  let commandsHistoryService: CommandsHistoryService\n  const mockedStore = jest.mocked(store)\n\n  const mockInstanceId = faker.string.uuid()\n  const mockCommandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n\n  const mockCommandHistoryData = commandExecutionUIFactory.buildList(3)\n\n  // Helper function to create default database mock\n  const createDefaultDatabaseMock = (overrides = {}) => ({\n    getCommandsHistory: jest.fn().mockResolvedValue({\n      success: true,\n      data: [],\n    }),\n    getCommandHistory: jest.fn().mockResolvedValue({\n      success: true,\n      data: null,\n    }),\n    addCommandsToHistory: jest.fn().mockResolvedValue({\n      success: true,\n      data: [],\n    }),\n    deleteCommandFromHistory: jest.fn().mockResolvedValue({\n      success: true,\n    }),\n    clearCommandsHistory: jest.fn().mockResolvedValue({\n      success: true,\n    }),\n    ...overrides,\n  })\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    // Reset mock store using the initial state\n    mockedStore.getState.mockReturnValue({\n      app: {\n        features: merge({}, appFeaturesInitialState, {\n          featureFlags: {\n            features: {\n              [FeatureFlags.envDependent]: { flag: false },\n            },\n          },\n        }),\n      },\n    } as RootState)\n    mockedStore.dispatch.mockClear()\n\n    // Set up default database mock\n    mockedCommandsHistoryIndexedDB.mockImplementation(\n      () => createDefaultDatabaseMock() as any,\n    )\n\n    // Create a new instance for each test\n    commandsHistoryService = new CommandsHistoryService(\n      mockCommandExecutionType,\n    )\n  })\n\n  describe('getCommandsHistory', () => {\n    it('should initialize with IndexedDB when envDependent feature is disabled', async () => {\n      const mockGetCommandsHistory = jest.fn().mockResolvedValue({\n        success: true,\n        data: mockCommandHistoryData,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandsHistory: mockGetCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const indexedDBService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n      const result = await indexedDBService.getCommandsHistory(mockInstanceId)\n\n      expect(result).toEqual(mockCommandHistoryData)\n      expect(mockedCommandsHistoryIndexedDB).toHaveBeenCalled()\n    })\n\n    it('should initialize with SQLite when envDependent feature is enabled', async () => {\n      // Update store state for this test using initial state\n      mockedStore.getState.mockReturnValue({\n        app: {\n          features: merge({}, appFeaturesInitialState, {\n            featureFlags: {\n              features: {\n                [FeatureFlags.envDependent]: { flag: true },\n              },\n            },\n          }),\n        },\n      } as any)\n\n      const mockGetCommandsHistory = jest.fn().mockResolvedValue({\n        success: true,\n        data: mockCommandHistoryData,\n      })\n\n      mockedCommandsHistorySQLite.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandsHistory: mockGetCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the updated store state\n      const sqliteService = new CommandsHistoryService(mockCommandExecutionType)\n      const result = await sqliteService.getCommandsHistory(mockInstanceId)\n\n      expect(result).toEqual(mockCommandHistoryData)\n      expect(mockedCommandsHistorySQLite).toHaveBeenCalled()\n    })\n\n    it('should dispatch error notification when database returns error', async () => {\n      const mockError = { message: 'Database error' } as any\n      const mockGetCommandsHistory = jest.fn().mockResolvedValue({\n        success: false,\n        error: mockError,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandsHistory: mockGetCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const errorService = new CommandsHistoryService(mockCommandExecutionType)\n      const result = await errorService.getCommandsHistory(mockInstanceId)\n\n      expect(result).toEqual([])\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        addErrorNotification(mockError),\n      )\n    })\n  })\n\n  describe('getCommandHistory', () => {\n    const mockCommandId = faker.string.uuid()\n    const mockCommandData = commandExecutionUIFactory.build()\n\n    it('should initialize with IndexedDB when envDependent feature is disabled', async () => {\n      const mockGetCommandHistory = jest.fn().mockResolvedValue({\n        success: true,\n        data: mockCommandData,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandHistory: mockGetCommandHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const indexedDBService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n      const result = await indexedDBService.getCommandHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(result).toEqual(mockCommandData)\n      expect(mockedCommandsHistoryIndexedDB).toHaveBeenCalled()\n    })\n\n    it('should initialize with SQLite when envDependent feature is enabled', async () => {\n      // Update store state for this test using initial state\n      mockedStore.getState.mockReturnValue({\n        app: {\n          features: merge({}, appFeaturesInitialState, {\n            featureFlags: {\n              features: {\n                [FeatureFlags.envDependent]: { flag: true },\n              },\n            },\n          }),\n        },\n      } as any)\n\n      const mockGetCommandHistory = jest.fn().mockResolvedValue({\n        success: true,\n        data: mockCommandData,\n      })\n\n      mockedCommandsHistorySQLite.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandHistory: mockGetCommandHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the updated store state\n      const sqliteService = new CommandsHistoryService(mockCommandExecutionType)\n      const result = await sqliteService.getCommandHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(result).toEqual(mockCommandData)\n      expect(mockedCommandsHistorySQLite).toHaveBeenCalled()\n    })\n\n    it('should dispatch error notification when database returns error', async () => {\n      const mockError = { message: 'Database error' } as any\n      const mockGetCommandHistory = jest.fn().mockResolvedValue({\n        success: false,\n        error: mockError,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandHistory: mockGetCommandHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const errorService = new CommandsHistoryService(mockCommandExecutionType)\n      const result = await errorService.getCommandHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(result).toEqual(null)\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        addErrorNotification(mockError),\n      )\n    })\n\n    it('should return null when data is not available', async () => {\n      const mockGetCommandHistory = jest.fn().mockResolvedValue({\n        success: true,\n        data: null,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandHistory: mockGetCommandHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const nullDataService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n      const result = await nullDataService.getCommandHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(result).toEqual(null)\n    })\n\n    it('should handle different instance IDs and command IDs', async () => {\n      const instanceId1 = faker.string.uuid()\n      const instanceId2 = faker.string.uuid()\n      const commandId1 = faker.string.uuid()\n      const commandId2 = faker.string.uuid()\n\n      const mockCommand1 = commandExecutionUIFactory.build()\n      const mockCommand2 = commandExecutionUIFactory.build()\n\n      const mockGetCommandHistory = jest\n        .fn()\n        .mockResolvedValueOnce({ success: true, data: mockCommand1 })\n        .mockResolvedValueOnce({ success: true, data: mockCommand2 })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            getCommandHistory: mockGetCommandHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const multiCommandService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n\n      const result1 = await multiCommandService.getCommandHistory(\n        instanceId1,\n        commandId1,\n      )\n      const result2 = await multiCommandService.getCommandHistory(\n        instanceId2,\n        commandId2,\n      )\n\n      expect(result1).toEqual(mockCommand1)\n      expect(result2).toEqual(mockCommand2)\n      expect(mockGetCommandHistory).toHaveBeenCalledTimes(2)\n      expect(mockGetCommandHistory).toHaveBeenNthCalledWith(\n        1,\n        instanceId1,\n        commandId1,\n      )\n      expect(mockGetCommandHistory).toHaveBeenNthCalledWith(\n        2,\n        instanceId2,\n        commandId2,\n      )\n    })\n  })\n\n  describe('addCommandsToHistory', () => {\n    const mockCommands = [faker.string.alphanumeric(10)]\n    const mockOptions = {\n      activeRunQueryMode: RunQueryMode.ASCII,\n      resultsMode: ResultsMode.Default,\n    }\n\n    it('should add commands to history successfully', async () => {\n      const mockAddCommandsToHistory = jest.fn().mockResolvedValue({\n        success: true,\n        data: mockCommandHistoryData,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            addCommandsToHistory: mockAddCommandsToHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const addCommandsService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n      const result = await addCommandsService.addCommandsToHistory(\n        mockInstanceId,\n        mockCommands,\n        mockOptions,\n      )\n\n      expect(result).toEqual(mockCommandHistoryData)\n    })\n\n    it('should dispatch error notification when database returns error', async () => {\n      const mockError = { message: 'Database error' } as any\n      const mockAddCommandsToHistory = jest.fn().mockResolvedValue({\n        success: false,\n        error: mockError,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            addCommandsToHistory: mockAddCommandsToHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const errorService = new CommandsHistoryService(mockCommandExecutionType)\n      const result = await errorService.addCommandsToHistory(\n        mockInstanceId,\n        mockCommands,\n        mockOptions,\n      )\n\n      expect(result).toEqual([])\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        addErrorNotification(mockError),\n      )\n    })\n\n    it('should return empty array when success is false', async () => {\n      const mockAddCommandsToHistory = jest.fn().mockResolvedValue({\n        success: false,\n        data: mockCommandHistoryData,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            addCommandsToHistory: mockAddCommandsToHistory,\n          }) as any,\n      )\n\n      const result = await commandsHistoryService.addCommandsToHistory(\n        mockInstanceId,\n        mockCommands,\n        mockOptions,\n      )\n\n      expect(result).toEqual([])\n    })\n  })\n\n  describe('deleteCommandFromHistory', () => {\n    const mockCommandId = faker.string.uuid()\n\n    it('should delete command from history successfully', async () => {\n      const mockDeleteCommandFromHistory = jest.fn().mockResolvedValue({\n        success: true,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            deleteCommandFromHistory: mockDeleteCommandFromHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const deleteCommandService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n      await deleteCommandService.deleteCommandFromHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(mockDeleteCommandFromHistory).toHaveBeenCalledWith(\n        mockInstanceId,\n        mockCommandId,\n      )\n      expect(mockedStore.dispatch).not.toHaveBeenCalled()\n    })\n\n    it('should dispatch error notification when database returns error', async () => {\n      const mockError = { message: 'Database error' } as any\n      const mockDeleteCommandFromHistory = jest.fn().mockResolvedValue({\n        success: false,\n        error: mockError,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            deleteCommandFromHistory: mockDeleteCommandFromHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const errorService = new CommandsHistoryService(mockCommandExecutionType)\n      await errorService.deleteCommandFromHistory(mockInstanceId, mockCommandId)\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        addErrorNotification(mockError),\n      )\n    })\n\n    it('should work with SQLite database when envDependent feature is enabled', async () => {\n      // Update store state for this test using initial state\n      mockedStore.getState.mockReturnValue({\n        app: {\n          features: merge({}, appFeaturesInitialState, {\n            featureFlags: {\n              features: {\n                [FeatureFlags.envDependent]: { flag: true },\n              },\n            },\n          }),\n        },\n      } as RootState)\n\n      const mockDeleteCommandFromHistory = jest.fn().mockResolvedValue({\n        success: true,\n      })\n\n      mockedCommandsHistorySQLite.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            deleteCommandFromHistory: mockDeleteCommandFromHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the updated store state\n      const sqliteService = new CommandsHistoryService(mockCommandExecutionType)\n      await sqliteService.deleteCommandFromHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(mockDeleteCommandFromHistory).toHaveBeenCalledWith(\n        mockInstanceId,\n        mockCommandId,\n      )\n      expect(mockedCommandsHistorySQLite).toHaveBeenCalled()\n    })\n\n    it('should handle different command IDs', async () => {\n      const commandId1 = faker.string.uuid()\n      const commandId2 = faker.string.uuid()\n\n      const mockDeleteCommandFromHistory = jest.fn().mockResolvedValue({\n        success: true,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            deleteCommandFromHistory: mockDeleteCommandFromHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const deleteCommandService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n\n      await deleteCommandService.deleteCommandFromHistory(\n        mockInstanceId,\n        commandId1,\n      )\n      await deleteCommandService.deleteCommandFromHistory(\n        mockInstanceId,\n        commandId2,\n      )\n\n      expect(mockDeleteCommandFromHistory).toHaveBeenCalledTimes(2)\n      expect(mockDeleteCommandFromHistory).toHaveBeenNthCalledWith(\n        1,\n        mockInstanceId,\n        commandId1,\n      )\n      expect(mockDeleteCommandFromHistory).toHaveBeenNthCalledWith(\n        2,\n        mockInstanceId,\n        commandId2,\n      )\n    })\n  })\n\n  describe('clearCommandsHistory', () => {\n    it('should clear command history successfully', async () => {\n      const mockClearCommandsHistory = jest.fn().mockResolvedValue({\n        success: true,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            clearCommandsHistory: mockClearCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const clearCommandsService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n      await clearCommandsService.clearCommandsHistory(mockInstanceId)\n\n      expect(mockClearCommandsHistory).toHaveBeenCalledWith(\n        mockInstanceId,\n        mockCommandExecutionType,\n      )\n      expect(mockedStore.dispatch).not.toHaveBeenCalled()\n    })\n\n    it('should dispatch error notification when database returns error', async () => {\n      const mockError = { message: 'Database error' } as any\n      const mockClearCommandsHistory = jest.fn().mockResolvedValue({\n        success: false,\n        error: mockError,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            clearCommandsHistory: mockClearCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const errorService = new CommandsHistoryService(mockCommandExecutionType)\n      await errorService.clearCommandsHistory(mockInstanceId)\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        addErrorNotification(mockError),\n      )\n    })\n\n    it('should work with SQLite database when envDependent feature is enabled', async () => {\n      // Update store state for this test using initial state\n      mockedStore.getState.mockReturnValue({\n        app: {\n          features: merge({}, appFeaturesInitialState, {\n            featureFlags: {\n              features: {\n                [FeatureFlags.envDependent]: { flag: true },\n              },\n            },\n          }),\n        },\n      } as RootState)\n\n      const mockClearCommandsHistory = jest.fn().mockResolvedValue({\n        success: true,\n      })\n\n      mockedCommandsHistorySQLite.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            clearCommandsHistory: mockClearCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the updated store state\n      const sqliteService = new CommandsHistoryService(mockCommandExecutionType)\n      await sqliteService.clearCommandsHistory(mockInstanceId)\n\n      expect(mockClearCommandsHistory).toHaveBeenCalledWith(\n        mockInstanceId,\n        mockCommandExecutionType,\n      )\n      expect(mockedCommandsHistorySQLite).toHaveBeenCalled()\n    })\n\n    it('should handle different instance IDs', async () => {\n      const instanceId1 = 'instance-1'\n      const instanceId2 = 'instance-2'\n\n      const mockClearCommandsHistory = jest.fn().mockResolvedValue({\n        success: true,\n      })\n\n      mockedCommandsHistoryIndexedDB.mockImplementation(\n        () =>\n          createDefaultDatabaseMock({\n            clearCommandsHistory: mockClearCommandsHistory,\n          }) as any,\n      )\n\n      // Create a new service instance with the mocked database\n      const clearCommandsService = new CommandsHistoryService(\n        mockCommandExecutionType,\n      )\n\n      await clearCommandsService.clearCommandsHistory(instanceId1)\n      await clearCommandsService.clearCommandsHistory(instanceId2)\n\n      expect(mockClearCommandsHistory).toHaveBeenCalledTimes(2)\n      expect(mockClearCommandsHistory).toHaveBeenNthCalledWith(\n        1,\n        instanceId1,\n        mockCommandExecutionType,\n      )\n      expect(mockClearCommandsHistory).toHaveBeenNthCalledWith(\n        2,\n        instanceId2,\n        mockCommandExecutionType,\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/commandsHistoryService.ts",
    "content": "import { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { store } from 'uiSrc/slices/store'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  CommandExecutionType,\n  CommandExecutionUI,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\n\nimport { CommandsHistorySQLite } from './database/CommandsHistorySQLite'\nimport { CommandsHistoryDatabase } from './database/interface'\nimport { CommandsHistoryIndexedDB } from './database/CommandsHistoryIndexedDB'\n\nexport class CommandsHistoryService {\n  private commandsHistoryDatabase: CommandsHistoryDatabase\n\n  private commandExecutionType: CommandExecutionType\n\n  constructor(commandExecutionType: CommandExecutionType) {\n    this.commandExecutionType = commandExecutionType\n    this.commandsHistoryDatabase = this.initializeDatabase()\n  }\n\n  private initializeDatabase(): CommandsHistoryDatabase {\n    const state = store.getState()\n    const { [FeatureFlags.envDependent]: envDependentFeature } =\n      appFeatureFlagsFeaturesSelector(state)\n\n    if (envDependentFeature?.flag) {\n      return new CommandsHistorySQLite()\n    } else {\n      return new CommandsHistoryIndexedDB(this.commandExecutionType)\n    }\n  }\n\n  async getCommandsHistory(instanceId: string): Promise<CommandExecutionUI[]> {\n    const { data, error } =\n      await this.commandsHistoryDatabase.getCommandsHistory(\n        instanceId,\n        this.commandExecutionType,\n      )\n\n    if (error) {\n      store.dispatch(addErrorNotification(error))\n    }\n\n    return data || []\n  }\n\n  async getCommandHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<CommandExecutionUI | null> {\n    const { data, error } =\n      await this.commandsHistoryDatabase.getCommandHistory(\n        instanceId,\n        commandId,\n      )\n\n    if (error) {\n      store.dispatch(addErrorNotification(error))\n    }\n\n    return data || null\n  }\n\n  async addCommandsToHistory(\n    instanceId: string,\n    commands: string[],\n    options: {\n      activeRunQueryMode: RunQueryMode\n      resultsMode: ResultsMode\n    },\n  ): Promise<CommandExecutionUI[]> {\n    const { success, error, data } =\n      await this.commandsHistoryDatabase.addCommandsToHistory(\n        instanceId,\n        this.commandExecutionType,\n        commands,\n        options,\n      )\n\n    if (error) {\n      store.dispatch(addErrorNotification(error))\n    }\n\n    return success && data ? data : []\n  }\n\n  async deleteCommandFromHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<void> {\n    const { error } =\n      await this.commandsHistoryDatabase.deleteCommandFromHistory(\n        instanceId,\n        commandId,\n      )\n\n    if (error) {\n      store.dispatch(addErrorNotification(error))\n    }\n  }\n\n  async clearCommandsHistory(instanceId: string): Promise<void> {\n    const { error } = await this.commandsHistoryDatabase.clearCommandsHistory(\n      instanceId,\n      this.commandExecutionType,\n    )\n\n    if (error) {\n      store.dispatch(addErrorNotification(error))\n    }\n  }\n}\n\nexport default CommandsHistoryService\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/database/CommandsHistoryIndexedDB.spec.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { faker } from '@faker-js/faker'\nimport {\n  CommandExecutionType,\n  CommandExecutionUI,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport {\n  commandExecutionFactory,\n  commandExecutionUIFactory,\n} from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\nimport {\n  addCommands,\n  clearCommands,\n  findCommand,\n  getLocalWbHistory,\n  removeCommand,\n} from 'uiSrc/services/workbenchStorage'\nimport { getUrl } from 'uiSrc/utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { CommandsHistoryIndexedDB } from './CommandsHistoryIndexedDB'\n\n// Mock dependencies\njest.mock('uiSrc/services/workbenchStorage', () => ({\n  addCommands: jest.fn(),\n  clearCommands: jest.fn(),\n  findCommand: jest.fn(),\n  getLocalWbHistory: jest.fn(),\n  removeCommand: jest.fn(),\n  wbHistoryStorage: {},\n}))\n\njest.mock('uiSrc/services/vectorSearchHistoryStorage', () => ({\n  vectorSearchCommandsHistoryStorage: {},\n}))\n\nconst mockedAddCommands = jest.mocked(addCommands)\nconst mockedClearCommands = jest.mocked(clearCommands)\nconst mockedFindCommand = jest.mocked(findCommand)\nconst mockedGetLocalWbHistory = jest.mocked(getLocalWbHistory)\nconst mockedRemoveCommand = jest.mocked(removeCommand)\n\ndescribe('CommandsHistoryIndexedDB', () => {\n  let commandsHistoryIndexedDB: CommandsHistoryIndexedDB\n  const mockInstanceId = INSTANCE_ID_MOCK\n  const mockCommandId = faker.string.uuid()\n  const mockCommandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n  const mockCommandHistoryData = commandExecutionFactory.buildList(3)\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    commandsHistoryIndexedDB = new CommandsHistoryIndexedDB(\n      mockCommandExecutionType,\n    )\n  })\n\n  describe('getCommandsHistory', () => {\n    it('should return successful result with data from storage', async () => {\n      mockedGetLocalWbHistory.mockResolvedValue(mockCommandHistoryData)\n      const expectedResultCommands = mockCommandHistoryData.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n      })) as CommandExecutionUI[]\n\n      const result = await commandsHistoryIndexedDB.getCommandsHistory(\n        mockInstanceId,\n        mockCommandExecutionType,\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: expectedResultCommands,\n      })\n    })\n\n    it('should return empty array when no data in storage', async () => {\n      mockedGetLocalWbHistory.mockResolvedValue([])\n\n      const result = await commandsHistoryIndexedDB.getCommandsHistory(\n        mockInstanceId,\n        mockCommandExecutionType,\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: [],\n      })\n    })\n\n    it.each([\n      ['Workbench', CommandExecutionType.Workbench],\n      ['Search', CommandExecutionType.Search],\n    ])(\n      'should handle %s command execution type',\n      async (_, commandExecutionType) => {\n        const service = new CommandsHistoryIndexedDB(commandExecutionType)\n        mockedGetLocalWbHistory.mockResolvedValue([])\n\n        const result = await service.getCommandsHistory(\n          mockInstanceId,\n          commandExecutionType,\n        )\n\n        expect(result).toEqual({\n          success: true,\n          data: [],\n        })\n      },\n    )\n\n    it('should handle different instance IDs', async () => {\n      const instanceId1 = faker.string.uuid()\n      const instanceId2 = faker.string.uuid()\n      mockedGetLocalWbHistory.mockResolvedValue([])\n\n      const result1 = await commandsHistoryIndexedDB.getCommandsHistory(\n        instanceId1,\n        mockCommandExecutionType,\n      )\n      const result2 = await commandsHistoryIndexedDB.getCommandsHistory(\n        instanceId2,\n        mockCommandExecutionType,\n      )\n\n      expect(mockedGetLocalWbHistory).toHaveBeenCalledWith(\n        expect.any(Object),\n        instanceId1,\n      )\n      expect(mockedGetLocalWbHistory).toHaveBeenCalledWith(\n        expect.any(Object),\n        instanceId2,\n      )\n      expect(result1).toEqual({\n        success: true,\n        data: [],\n      })\n      expect(result2).toEqual({\n        success: true,\n        data: [],\n      })\n    })\n  })\n\n  describe('getCommandHistory', () => {\n    it('should successfully fetch and map single command history from IndexedDB', async () => {\n      const commandId = faker.string.uuid()\n      const mockCommand = commandExecutionFactory.build({\n        id: commandId,\n        command: 'GET key1',\n      })\n      const expectedResultCommand = {\n        ...mockCommand,\n        emptyCommand: false,\n      }\n\n      mockedFindCommand.mockResolvedValue(mockCommand)\n\n      const result = await commandsHistoryIndexedDB.getCommandHistory(\n        mockInstanceId,\n        commandId,\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: expectedResultCommand,\n      })\n    })\n\n    it('should handle command not found in IndexedDB', async () => {\n      const commandId = faker.string.uuid()\n\n      mockedFindCommand.mockResolvedValue(undefined)\n\n      const result = await commandsHistoryIndexedDB.getCommandHistory(\n        mockInstanceId,\n        commandId,\n      )\n\n      expect(result).toEqual({\n        success: false,\n      })\n    })\n\n    it('should handle different instance IDs and command IDs', async () => {\n      const instanceId1 = faker.string.uuid()\n      const instanceId2 = faker.string.uuid()\n      const commandId1 = faker.string.uuid()\n      const commandId2 = faker.string.uuid()\n\n      const mockCommand1 = commandExecutionFactory.build({\n        id: commandId1,\n        command: 'GET key1',\n      })\n      const mockCommand2 = commandExecutionFactory.build({\n        id: commandId2,\n        command: 'SET key2 value',\n      })\n\n      const expectedResultCommand1 = {\n        ...mockCommand1,\n        emptyCommand: false,\n      }\n      const expectedResultCommand2 = {\n        ...mockCommand2,\n        emptyCommand: false,\n      }\n\n      mockedFindCommand\n        .mockResolvedValueOnce(mockCommand1)\n        .mockResolvedValueOnce(mockCommand2)\n\n      const result1 = await commandsHistoryIndexedDB.getCommandHistory(\n        instanceId1,\n        commandId1,\n      )\n      const result2 = await commandsHistoryIndexedDB.getCommandHistory(\n        instanceId2,\n        commandId2,\n      )\n\n      expect(result1).toEqual({\n        success: true,\n        data: expectedResultCommand1,\n      })\n      expect(result2).toEqual({\n        success: true,\n        data: expectedResultCommand2,\n      })\n    })\n  })\n\n  describe('addCommandsToHistory', () => {\n    const mockCommands = [faker.lorem.word(), faker.lorem.word()]\n    const mockOptions = {\n      activeRunQueryMode: faker.lorem.word(),\n      resultsMode: faker.lorem.word(),\n    }\n    const mockCommandExecutions = commandExecutionUIFactory.buildList(2)\n\n    it('should successfully add commands to history', async () => {\n      const expectedResultCommands = mockCommandExecutions.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt?.toISOString(),\n      })) as unknown as CommandExecutionUI[]\n\n      // Override the MSW handler to return our mock commands\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(mockInstanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.json(mockCommandExecutions, { status: 200 }),\n        ),\n      )\n\n      mockedAddCommands.mockResolvedValue(undefined)\n\n      const result = await commandsHistoryIndexedDB.addCommandsToHistory(\n        mockInstanceId,\n        mockCommandExecutionType,\n        mockCommands,\n        mockOptions,\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: expectedResultCommands,\n      })\n    })\n\n    it('should handle unsuccessful status code 400', async () => {\n      const statusCode = 400\n      const commandExecutionType = CommandExecutionType.Workbench\n      const mockCommandsStrings = commandExecutionFactory\n        .buildList(2)\n        .map((cmd) => cmd.command)\n\n      // Override the MSW handler to return an error status\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(mockInstanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandsHistoryIndexedDB.addCommandsToHistory(\n        mockInstanceId,\n        commandExecutionType,\n        mockCommandsStrings,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      const mockError = 'Network Error'\n      const commandExecutionType = CommandExecutionType.Workbench\n      const mockCommandsStrings = commandExecutionFactory\n        .buildList(2)\n        .map((cmd) => cmd.command)\n\n      // Override the MSW handler to simulate a network error\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(mockInstanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await commandsHistoryIndexedDB.addCommandsToHistory(\n        mockInstanceId,\n        commandExecutionType,\n        mockCommandsStrings,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error.message).toBe(mockError)\n    })\n  })\n\n  describe('deleteCommandFromHistory', () => {\n    it('should successfully delete command from history', async () => {\n      mockedRemoveCommand.mockResolvedValue(undefined)\n\n      const result = await commandsHistoryIndexedDB.deleteCommandFromHistory(\n        mockInstanceId,\n        mockCommandId,\n      )\n\n      expect(mockedRemoveCommand).toHaveBeenCalledWith(\n        expect.any(Object),\n        mockInstanceId,\n        mockCommandId,\n      )\n      expect(result).toEqual({\n        success: true,\n      })\n    })\n  })\n\n  describe('clearCommandsHistory', () => {\n    it('should successfully clear commands history', async () => {\n      mockedClearCommands.mockResolvedValue(undefined)\n\n      const result =\n        await commandsHistoryIndexedDB.clearCommandsHistory(mockInstanceId)\n\n      expect(mockedClearCommands).toHaveBeenCalledWith(\n        expect.any(Object),\n        mockInstanceId,\n      )\n      expect(result).toEqual({\n        success: true,\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/database/CommandsHistoryIndexedDB.ts",
    "content": "import { AxiosError } from 'axios'\nimport {\n  CommandExecution,\n  CommandExecutionType,\n  CommandExecutionUI,\n} from 'uiSrc/slices/interfaces'\nimport {\n  CommandHistoryResult,\n  CommandsHistoryDatabase,\n  CommandsHistoryResult,\n} from './interface'\nimport { vectorSearchCommandsHistoryStorage } from 'uiSrc/services/vectorSearchHistoryStorage'\nimport { getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport apiService from 'uiSrc/services/apiService'\nimport { mapCommandExecutionToUI } from '../utils/command-execution.mapper'\nimport {\n  addCommands,\n  clearCommands,\n  findCommand,\n  getLocalWbHistory,\n  removeCommand,\n  wbHistoryStorage,\n  WorkbenchStorage,\n} from 'uiSrc/services/workbenchStorage'\n\nexport class CommandsHistoryIndexedDB implements CommandsHistoryDatabase {\n  private readonly dbStorage: WorkbenchStorage\n\n  constructor(commandExecutionType: CommandExecutionType) {\n    this.dbStorage =\n      commandExecutionType === CommandExecutionType.Search\n        ? vectorSearchCommandsHistoryStorage\n        : wbHistoryStorage\n  }\n\n  async getCommandsHistory(instanceId: string): Promise<CommandsHistoryResult> {\n    const data = await getLocalWbHistory(this.dbStorage, instanceId)\n    const results: CommandExecutionUI[] = data.map(mapCommandExecutionToUI)\n\n    return Promise.resolve({\n      success: true,\n      data: results,\n    })\n  }\n\n  async getCommandHistory(\n    _instanceId: string,\n    commandId: string,\n  ): Promise<CommandHistoryResult> {\n    const command = await findCommand(this.dbStorage, commandId)\n\n    if (!command) {\n      return {\n        success: false,\n      }\n    }\n\n    return {\n      success: true,\n      data: mapCommandExecutionToUI(command as CommandExecution),\n    }\n  }\n\n  async addCommandsToHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n    commands: string[],\n    options: {\n      activeRunQueryMode: string\n      resultsMode: string\n    },\n  ): Promise<CommandsHistoryResult> {\n    const { activeRunQueryMode, resultsMode } = options\n\n    try {\n      const url = getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS)\n\n      const { data, status } = await apiService.post<CommandExecution[]>(url, {\n        commands,\n\n        type: commandExecutionType,\n        activeRunQueryMode,\n        resultsMode,\n      })\n\n      if (isStatusSuccessful(status)) {\n        const results: CommandExecutionUI[] = data.map(mapCommandExecutionToUI)\n\n        await addCommands(this.dbStorage, data)\n\n        return { success: true, data: results }\n      }\n\n      return {\n        success: false,\n        error: new Error(`Request failed with status ${status}`),\n      }\n    } catch (exception) {\n      return {\n        success: false,\n        error: exception as AxiosError,\n      }\n    }\n  }\n\n  async deleteCommandFromHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<CommandsHistoryResult> {\n    await removeCommand(this.dbStorage, instanceId, commandId)\n\n    return Promise.resolve({\n      success: true,\n    })\n  }\n\n  async clearCommandsHistory(\n    instanceId: string,\n  ): Promise<CommandsHistoryResult> {\n    await clearCommands(this.dbStorage, instanceId)\n\n    return Promise.resolve({\n      success: true,\n    })\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/database/CommandsHistorySQLite.spec.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { faker } from '@faker-js/faker'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl } from 'uiSrc/utils'\nimport {\n  CommandExecutionType,\n  CommandExecutionUI,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport { commandExecutionFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport { CommandsHistorySQLite } from './CommandsHistorySQLite'\n\ndescribe('CommandHistorySQLite', () => {\n  let commandHistorySQLite: CommandsHistorySQLite\n  const instanceId = INSTANCE_ID_MOCK\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    commandHistorySQLite = new CommandsHistorySQLite()\n  })\n\n  describe('getCommandsHistory', () => {\n    it('should successfully fetch and map command history', async () => {\n      const commandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n      const mockCommands = [\n        commandExecutionFactory.build({ id: '1', command: 'GET key1' }),\n        commandExecutionFactory.build({ id: '2', command: 'SET key2 value' }),\n      ]\n      const expectedResultCommands = mockCommands.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt.toISOString(),\n      }))\n\n      // Override the MSW handler to return our mock commands\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.json(mockCommands, { status: 200 }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandsHistory(\n        instanceId,\n        commandExecutionType,\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: expectedResultCommands,\n      })\n    })\n\n    it.each([\n      ['Workbench', CommandExecutionType.Workbench],\n      ['Search', CommandExecutionType.Search],\n    ])(\n      'should fetch command history for %s type',\n      async (_, commandExecutionType) => {\n        const result = await commandHistorySQLite.getCommandsHistory(\n          instanceId,\n          commandExecutionType,\n        )\n\n        expect(result.success).toBe(true)\n        expect(result.data?.length).toBeGreaterThan(0)\n      },\n    )\n\n    it('should handle unsuccessful status code 400', async () => {\n      const statusCode = 400\n\n      // Override the MSW handler to return an error status\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandsHistory(\n        instanceId,\n        CommandExecutionType.Workbench,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      const mockError = 'Network Error'\n\n      // Override the MSW handler to simulate a network error\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandsHistory(\n        instanceId,\n        CommandExecutionType.Workbench,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error.message).toBe(mockError)\n    })\n\n    it('should handle different instance IDs', async () => {\n      const instanceId1 = 'instance-1'\n      const instanceId2 = 'instance-2'\n\n      const mockCommandExecutions1 = commandExecutionFactory.buildList(3)\n      const mockCommandExecutions2 = commandExecutionFactory.buildList(3)\n\n      const expectedResultCommands1 = mockCommandExecutions1.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt.toISOString(),\n      }))\n      const expectedResultCommands2 = mockCommandExecutions2.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt.toISOString(),\n      }))\n\n      // Override the MSW handler to return different data based on instance ID\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(instanceId1, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () =>\n            HttpResponse.json(mockCommandExecutions1, { status: 200 }),\n        ),\n        http.get(\n          getMswURL(\n            getUrl(instanceId2, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () =>\n            HttpResponse.json(mockCommandExecutions2, { status: 200 }),\n        ),\n      )\n\n      const result1 = await commandHistorySQLite.getCommandsHistory(\n        instanceId1,\n        CommandExecutionType.Workbench,\n      )\n      const result2 = await commandHistorySQLite.getCommandsHistory(\n        instanceId2,\n        CommandExecutionType.Search,\n      )\n\n      expect(result1).toEqual({\n        success: true,\n        data: expectedResultCommands1,\n      })\n      expect(result2).toEqual({\n        success: true,\n        data: expectedResultCommands2,\n      })\n    })\n  })\n\n  describe('getCommandHistory', () => {\n    it('should successfully fetch and map single command history', async () => {\n      const commandId = faker.string.uuid()\n      const mockCommand = commandExecutionFactory.build({\n        id: commandId,\n        command: 'GET key1',\n      })\n      const expectedResultCommand = {\n        ...mockCommand,\n        emptyCommand: false,\n        createdAt: mockCommand.createdAt.toISOString(),\n      }\n\n      // Override the MSW handler to return our mock command\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.json(mockCommand, { status: 200 }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: expectedResultCommand,\n      })\n    })\n\n    it('should handle unsuccessful status code 400', async () => {\n      const statusCode = 400\n      const commandId = faker.string.uuid()\n\n      // Override the MSW handler to return an error status\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      const mockError = 'Network Error'\n      const commandId = faker.string.uuid()\n\n      // Override the MSW handler to simulate a network error\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error.message).toBe(mockError)\n    })\n\n    it('should handle different instance IDs and command IDs', async () => {\n      const instanceId1 = 'instance-1'\n      const instanceId2 = 'instance-2'\n      const commandId1 = faker.string.uuid()\n      const commandId2 = faker.string.uuid()\n\n      const mockCommand1 = commandExecutionFactory.build({\n        id: commandId1,\n        command: 'GET key1',\n      })\n      const mockCommand2 = commandExecutionFactory.build({\n        id: commandId2,\n        command: 'SET key2 value',\n      })\n\n      const expectedResultCommand1 = {\n        ...mockCommand1,\n        emptyCommand: false,\n        createdAt: mockCommand1.createdAt.toISOString(),\n      }\n      const expectedResultCommand2 = {\n        ...mockCommand2,\n        emptyCommand: false,\n        createdAt: mockCommand2.createdAt.toISOString(),\n      }\n\n      // Override the MSW handler to return different data based on instance ID and command ID\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(\n              instanceId1,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId1,\n            ),\n          ),\n          async () => HttpResponse.json(mockCommand1, { status: 200 }),\n        ),\n        http.get(\n          getMswURL(\n            getUrl(\n              instanceId2,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId2,\n            ),\n          ),\n          async () => HttpResponse.json(mockCommand2, { status: 200 }),\n        ),\n      )\n\n      const result1 = await commandHistorySQLite.getCommandHistory(\n        instanceId1,\n        commandId1,\n      )\n      const result2 = await commandHistorySQLite.getCommandHistory(\n        instanceId2,\n        commandId2,\n      )\n\n      expect(result1).toEqual({\n        success: true,\n        data: expectedResultCommand1,\n      })\n      expect(result2).toEqual({\n        success: true,\n        data: expectedResultCommand2,\n      })\n    })\n\n    it('should handle 404 not found error', async () => {\n      const statusCode = 404\n      const commandId = faker.string.uuid()\n\n      // Override the MSW handler to return 404 status\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.getCommandHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n  })\n\n  describe('addCommandsToHistory', () => {\n    it('should successfully add commands to history and return mapped results', async () => {\n      const commandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n      const mockCommands = [\n        commandExecutionFactory.build({ id: '1', command: 'GET key1' }),\n        commandExecutionFactory.build({ id: '2', command: 'SET key2 value' }),\n      ]\n      const mockCommandsStrings = mockCommands.map((cmd) => cmd.command)\n      const expectedResultCommands = mockCommands.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt.toISOString(),\n      })) as unknown as CommandExecutionUI[]\n\n      // Override the MSW handler to return our mock commands\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.json(mockCommands, { status: 200 }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.addCommandsToHistory(\n        instanceId,\n        commandExecutionType,\n        mockCommandsStrings,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result).toEqual({\n        success: true,\n        data: expectedResultCommands,\n      })\n    })\n\n    it.each([\n      ['Workbench', CommandExecutionType.Workbench],\n      ['Search', CommandExecutionType.Search],\n    ])(\n      'should add commands to history for %s type',\n      async (_, commandExecutionType) => {\n        const mockCommands = commandExecutionFactory.buildList(2)\n        const mockCommandsStrings = mockCommands.map((cmd) => cmd.command)\n        const expectedResultCommands = mockCommands.map((cmd) => ({\n          ...cmd,\n          emptyCommand: false,\n          createdAt: cmd.createdAt.toISOString(),\n        })) as unknown as CommandExecutionUI[]\n\n        mswServer.use(\n          http.post(\n            getMswURL(\n              getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n            ),\n            async () => HttpResponse.json(mockCommands, { status: 200 }),\n          ),\n        )\n\n        const result = await commandHistorySQLite.addCommandsToHistory(\n          instanceId,\n          commandExecutionType,\n          mockCommandsStrings,\n          {\n            activeRunQueryMode: RunQueryMode.ASCII,\n            resultsMode: ResultsMode.Default,\n          },\n        )\n\n        expect(result.success).toBe(true)\n        expect(result.data).toEqual(expectedResultCommands)\n      },\n    )\n\n    it('should handle unsuccessful status code 400', async () => {\n      const statusCode = 400\n      const commandExecutionType = CommandExecutionType.Workbench\n      const mockCommandsStrings = commandExecutionFactory\n        .buildList(2)\n        .map((cmd) => cmd.command)\n\n      // Override the MSW handler to return an error status\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.addCommandsToHistory(\n        instanceId,\n        commandExecutionType,\n        mockCommandsStrings,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      const mockError = 'Network Error'\n      const commandExecutionType = CommandExecutionType.Workbench\n      const mockCommandsStrings = commandExecutionFactory\n        .buildList(2)\n        .map((cmd) => cmd.command)\n\n      // Override the MSW handler to simulate a network error\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await commandHistorySQLite.addCommandsToHistory(\n        instanceId,\n        commandExecutionType,\n        mockCommandsStrings,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error.message).toBe(mockError)\n    })\n\n    it('should handle empty commands array', async () => {\n      const commandExecutionType = CommandExecutionType.Workbench\n      const emptyCommands: string[] = []\n\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.json(emptyCommands, { status: 200 }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.addCommandsToHistory(\n        instanceId,\n        commandExecutionType,\n        emptyCommands,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result.success).toBe(true)\n      expect(result.data).toEqual([])\n    })\n\n    it('should handle different instance IDs', async () => {\n      const instanceId1 = 'instance-1'\n      const instanceId2 = 'instance-2'\n      const commandExecutionType = CommandExecutionType.Workbench\n\n      const mockCommandExecutions1 = commandExecutionFactory.buildList(2)\n      const mockCommandExecutions2 = commandExecutionFactory.buildList(2)\n\n      const mockCommandsStrings1 = mockCommandExecutions1.map(\n        (cmd) => cmd.command,\n      )\n      const mockCommandsStrings2 = mockCommandExecutions2.map(\n        (cmd) => cmd.command,\n      )\n\n      const expectedResultCommands1 = mockCommandExecutions1.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt.toISOString(),\n      })) as unknown as CommandExecutionUI[]\n      const expectedResultCommands2 = mockCommandExecutions2.map((cmd) => ({\n        ...cmd,\n        emptyCommand: false,\n        createdAt: cmd.createdAt.toISOString(),\n      })) as unknown as CommandExecutionUI[]\n\n      // Override the MSW handler to return different data based on instance ID\n      mswServer.use(\n        http.post(\n          getMswURL(\n            getUrl(instanceId1, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () =>\n            HttpResponse.json(mockCommandExecutions1, { status: 200 }),\n        ),\n        http.post(\n          getMswURL(\n            getUrl(instanceId2, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () =>\n            HttpResponse.json(mockCommandExecutions2, { status: 200 }),\n        ),\n      )\n\n      const result1 = await commandHistorySQLite.addCommandsToHistory(\n        instanceId1,\n        commandExecutionType,\n        mockCommandsStrings1,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n      const result2 = await commandHistorySQLite.addCommandsToHistory(\n        instanceId2,\n        commandExecutionType,\n        mockCommandsStrings2,\n        {\n          activeRunQueryMode: RunQueryMode.ASCII,\n          resultsMode: ResultsMode.Default,\n        },\n      )\n\n      expect(result1).toEqual({\n        success: true,\n        data: expectedResultCommands1,\n      })\n      expect(result2).toEqual({\n        success: true,\n        data: expectedResultCommands2,\n      })\n    })\n  })\n\n  describe('deleteCommandFromHistory', () => {\n    it('should successfully delete command from history', async () => {\n      const commandId = faker.string.uuid()\n\n      // Override the MSW handler to return success status\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.deleteCommandFromHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result).toEqual({\n        success: true,\n      })\n    })\n\n    it('should handle unsuccessful status code 400', async () => {\n      const statusCode = 400\n      const commandId = faker.string.uuid()\n\n      // Override the MSW handler to return an error status\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.deleteCommandFromHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      const mockError = 'Network Error'\n      const commandId = faker.string.uuid()\n\n      // Override the MSW handler to simulate a network error\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(\n              instanceId,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId,\n            ),\n          ),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await commandHistorySQLite.deleteCommandFromHistory(\n        instanceId,\n        commandId,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error.message).toBe(mockError)\n    })\n\n    it('should handle different instance IDs and command IDs', async () => {\n      const instanceId1 = 'instance-1'\n      const instanceId2 = 'instance-2'\n      const commandId1 = faker.string.uuid()\n      const commandId2 = faker.string.uuid()\n\n      // Override the MSW handler to return success for both requests\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(\n              instanceId1,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId1,\n            ),\n          ),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n        http.delete(\n          getMswURL(\n            getUrl(\n              instanceId2,\n              ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n              commandId2,\n            ),\n          ),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n      )\n\n      const result1 = await commandHistorySQLite.deleteCommandFromHistory(\n        instanceId1,\n        commandId1,\n      )\n      const result2 = await commandHistorySQLite.deleteCommandFromHistory(\n        instanceId2,\n        commandId2,\n      )\n\n      expect(result1).toEqual({\n        success: true,\n      })\n      expect(result2).toEqual({\n        success: true,\n      })\n    })\n  })\n\n  describe('clearCommandsHistory', () => {\n    it('should successfully clear command history', async () => {\n      const commandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n\n      // Override the MSW handler to return success status\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.clearCommandsHistory(\n        instanceId,\n        commandExecutionType,\n      )\n\n      expect(result).toEqual({\n        success: true,\n      })\n    })\n\n    it('should handle unsuccessful status code 400', async () => {\n      const statusCode = 400\n      const commandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n\n      // Override the MSW handler to return an error status\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await commandHistorySQLite.clearCommandsHistory(\n        instanceId,\n        commandExecutionType,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      const mockError = 'Network Error'\n      const commandExecutionType = faker.helpers.enumValue(CommandExecutionType)\n\n      // Override the MSW handler to simulate a network error\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await commandHistorySQLite.clearCommandsHistory(\n        instanceId,\n        commandExecutionType,\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error.message).toBe(mockError)\n    })\n\n    it('should handle different instance IDs and command execution types', async () => {\n      const instanceId1 = 'instance-1'\n      const instanceId2 = 'instance-2'\n      const commandExecutionType1 = CommandExecutionType.Workbench\n      const commandExecutionType2 = CommandExecutionType.Search\n\n      // Override the MSW handler to return success for both requests\n      mswServer.use(\n        http.delete(\n          getMswURL(\n            getUrl(instanceId1, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n        http.delete(\n          getMswURL(\n            getUrl(instanceId2, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n          ),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n      )\n\n      const result1 = await commandHistorySQLite.clearCommandsHistory(\n        instanceId1,\n        commandExecutionType1,\n      )\n      const result2 = await commandHistorySQLite.clearCommandsHistory(\n        instanceId2,\n        commandExecutionType2,\n      )\n\n      expect(result1).toEqual({\n        success: true,\n      })\n      expect(result2).toEqual({\n        success: true,\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/database/CommandsHistorySQLite.ts",
    "content": "import { AxiosError } from 'axios'\n\nimport apiService from 'uiSrc/services/apiService'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport {\n  CommandExecution,\n  CommandExecutionType,\n  CommandExecutionUI,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\n\nimport { mapCommandExecutionToUI } from '../utils/command-execution.mapper'\nimport {\n  CommandHistoryResult,\n  CommandsHistoryDatabase,\n  CommandsHistoryResult,\n} from './interface'\n\nexport class CommandsHistorySQLite implements CommandsHistoryDatabase {\n  async getCommandsHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n  ): Promise<CommandsHistoryResult> {\n    try {\n      const url = getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS)\n      const config = { params: { type: commandExecutionType } }\n\n      const { data, status } = await apiService.get<CommandExecution[]>(\n        url,\n        config,\n      )\n\n      if (isStatusSuccessful(status)) {\n        const results: CommandExecutionUI[] = data.map(mapCommandExecutionToUI)\n\n        return { success: true, data: results }\n      }\n\n      return {\n        success: false,\n        error: new Error(`Request failed with status ${status}`),\n      }\n    } catch (exception) {\n      return {\n        success: false,\n        error: exception as AxiosError,\n      }\n    }\n  }\n\n  async getCommandHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<CommandHistoryResult> {\n    try {\n      const url = getUrl(\n        instanceId,\n        ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n        commandId,\n      )\n      const { data, status } = await apiService.get<CommandExecution>(url)\n\n      if (isStatusSuccessful(status)) {\n        return { success: true, data: mapCommandExecutionToUI(data) }\n      }\n\n      return {\n        success: false,\n        error: new Error(`Request failed with status ${status}`),\n      }\n    } catch (exception) {\n      return {\n        success: false,\n        error: exception as AxiosError,\n      }\n    }\n  }\n\n  async addCommandsToHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n    commands: string[],\n    options: {\n      activeRunQueryMode: RunQueryMode\n      resultsMode: ResultsMode\n    },\n  ): Promise<CommandsHistoryResult> {\n    const { activeRunQueryMode, resultsMode } = options\n    try {\n      const url = getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS)\n\n      const { data, status } = await apiService.post<CommandExecution[]>(url, {\n        commands,\n\n        type: commandExecutionType,\n        activeRunQueryMode,\n        resultsMode,\n      })\n\n      if (isStatusSuccessful(status)) {\n        const results: CommandExecutionUI[] = data.map(mapCommandExecutionToUI)\n        return { success: true, data: results }\n      }\n\n      return {\n        success: false,\n        error: new Error(`Request failed with status ${status}`),\n      }\n    } catch (exception) {\n      return {\n        success: false,\n        error: exception as AxiosError,\n      }\n    }\n  }\n\n  async deleteCommandFromHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<CommandsHistoryResult> {\n    try {\n      const url = getUrl(\n        instanceId,\n        ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS,\n        commandId,\n      )\n\n      const { status } = await apiService.delete<CommandExecution>(url)\n\n      if (isStatusSuccessful(status)) {\n        return { success: true }\n      }\n\n      return {\n        success: false,\n        error: new Error(`Request failed with status ${status}`),\n      }\n    } catch (exception) {\n      return {\n        success: false,\n        error: exception as AxiosError,\n      }\n    }\n  }\n\n  async clearCommandsHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n  ): Promise<CommandsHistoryResult> {\n    try {\n      const url = getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS)\n\n      const { status } = await apiService.delete<CommandExecution>(url, {\n        data: { type: commandExecutionType },\n      })\n\n      if (isStatusSuccessful(status)) {\n        return { success: true }\n      }\n\n      return {\n        success: false,\n        error: new Error(`Request failed with status ${status}`),\n      }\n    } catch (exception) {\n      return {\n        success: false,\n        error: exception as AxiosError,\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/database/interface.ts",
    "content": "import { AxiosError } from 'axios'\nimport {\n  CommandExecutionType,\n  CommandExecutionUI,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\n\nexport interface CommandsHistoryDatabase {\n  getCommandsHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n  ): Promise<CommandsHistoryResult>\n\n  getCommandHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<CommandHistoryResult>\n\n  addCommandsToHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n    commands: string[],\n    options: {\n      activeRunQueryMode: RunQueryMode\n      resultsMode: ResultsMode\n    },\n  ): Promise<CommandsHistoryResult>\n\n  deleteCommandFromHistory(\n    instanceId: string,\n    commandId: string,\n  ): Promise<CommandsHistoryResult>\n\n  clearCommandsHistory(\n    instanceId: string,\n    commandExecutionType: CommandExecutionType,\n  ): Promise<CommandsHistoryResult>\n}\n\nexport interface CommandsHistoryResult {\n  success: boolean\n  data?: CommandExecutionUI[]\n  error?: Error | AxiosError | any\n}\n\nexport interface CommandHistoryResult {\n  success: boolean\n  data?: CommandExecutionUI\n  error?: Error | AxiosError | any\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/utils/command-execution.mapper.spec.ts",
    "content": "import { CommandExecution } from 'uiSrc/slices/interfaces'\nimport { mapCommandExecutionToUI } from './command-execution.mapper'\nimport { EMPTY_COMMAND } from 'uiSrc/constants'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { commandExecutionFactory } from 'uiSrc/mocks/factories/workbench/commandExectution.factory'\n\ndescribe('mapCommandExecutionToUI', () => {\n  it('should map command execution with valid command', () => {\n    const mockCommandExecution: CommandExecution =\n      commandExecutionFactory.build()\n\n    const result = mapCommandExecutionToUI(mockCommandExecution)\n\n    expect(result).toEqual({\n      ...mockCommandExecution,\n      command: mockCommandExecution.command,\n      emptyCommand: false,\n    })\n  })\n\n  it.each([\n    ['empty string', ''],\n    ['null', null],\n    ['undefined', undefined],\n  ])('should map command execution with %s command', (_, commandValue) => {\n    const mockCommandExecution = commandExecutionFactory.build({\n      command: commandValue as any,\n    })\n\n    const result = mapCommandExecutionToUI(mockCommandExecution)\n\n    expect(result).toEqual({\n      ...mockCommandExecution,\n      command: EMPTY_COMMAND,\n      emptyCommand: true,\n    })\n  })\n\n  it('should preserve all original properties', () => {\n    const mockCommandExecution = commandExecutionFactory.build({\n      id: '5',\n      databaseId: 'db5',\n      command: 'PING',\n      result: [{ response: 'OK', status: CommandExecutionStatus.Success }],\n      createdAt: new Date('2023-01-01'),\n    })\n\n    const result = mapCommandExecutionToUI(mockCommandExecution)\n\n    expect(result).toMatchObject({\n      id: mockCommandExecution.id,\n      databaseId: mockCommandExecution.databaseId,\n      command: mockCommandExecution.command,\n      result: mockCommandExecution.result,\n      createdAt: mockCommandExecution.createdAt,\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/commands-history/utils/command-execution.mapper.ts",
    "content": "import { EMPTY_COMMAND } from 'uiSrc/constants'\nimport { CommandExecution, CommandExecutionUI } from 'uiSrc/slices/interfaces'\n\nexport const mapCommandExecutionToUI = (\n  item: CommandExecution,\n): CommandExecutionUI => {\n  return {\n    ...item,\n    command: item.command || EMPTY_COMMAND,\n    emptyCommand: !item.command,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/database/instancesService.ts",
    "content": "import { RedisNodeInfoResponse } from 'src/modules/database/dto/redis-info.dto'\nimport { Database as DatabaseInstanceResponse } from 'src/modules/database/models/database'\nimport { ExportDatabase } from 'src/modules/database/models/export-database'\nimport axios, { CancelTokenSource } from 'axios'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { isStatusSuccessful, Nullable } from 'uiSrc/utils'\nimport { Instance } from 'uiSrc/slices/interfaces'\nimport { PartialInstance } from 'uiSrc/slices/instances/instances'\n\nconst endpoint = ApiEndpoints.DATABASES\n\nexport async function getInstanceInfo(id: string) {\n  const { data, status } = await apiService.get<RedisNodeInfoResponse>(\n    `${endpoint}/${id}/info`,\n  )\n  return isStatusSuccessful(status) ? data : null\n}\nexport async function getInstanceOverview(id: string) {\n  const { data, status } = await apiService.get<RedisNodeInfoResponse>(\n    `${endpoint}/${id}/overview`,\n  )\n\n  return isStatusSuccessful(status) ? data : null\n}\n\nexport const sourceInstance: {\n  source: Nullable<CancelTokenSource>\n} = {\n  source: null,\n}\n\nexport async function updateInstanceAlias(id: string, name: string) {\n  sourceInstance.source?.cancel?.()\n  const { CancelToken } = axios\n  sourceInstance.source = CancelToken.source()\n\n  const { status } = await apiService.patch(\n    `${endpoint}/${id}`,\n    { name },\n    { cancelToken: sourceInstance.source.token },\n  )\n\n  sourceInstance.source = null\n\n  return isStatusSuccessful(status)\n}\n\nexport async function checkInstanceDbIndex(id: string, index: number) {\n  const { status } = await apiService.get(`${endpoint}/${id}/db/${index}`)\n\n  return isStatusSuccessful(status)\n}\n\nexport async function importInstances(importData: FormData) {\n  const { status, data } = await apiService.post(\n    ApiEndpoints.DATABASES_IMPORT,\n    importData,\n    {\n      headers: {\n        Accept: 'application/json',\n        'Content-Type': 'multipart/form-data',\n      },\n    },\n  )\n\n  return isStatusSuccessful(status) ? data : null\n}\n\nexport async function testInstanceConnection(\n  id?: string,\n  payload?: PartialInstance,\n) {\n  const url = id\n    ? `${ApiEndpoints.DATABASES_TEST_CONNECTION}/${id}`\n    : `${ApiEndpoints.DATABASES_TEST_CONNECTION}`\n  const { status } = await apiService.post(url, payload)\n\n  return isStatusSuccessful(status)\n}\n\nexport async function getInstance(id: string) {\n  const { data, status } = await apiService.get<Instance>(`${endpoint}/${id}`)\n\n  return isStatusSuccessful(status) ? data : null\n}\n\nexport async function connectInstance(id: string) {\n  const { status } = await apiService.get(`${endpoint}/${id}/connect`)\n\n  return isStatusSuccessful(status)\n}\n\nexport async function listDatabases() {\n  const { data, status } =\n    await apiService.get<DatabaseInstanceResponse[]>(endpoint)\n\n  return isStatusSuccessful(status) ? data : null\n}\n\nexport async function createInstance(instance: Instance) {\n  const { data, status } = await apiService.post(endpoint, instance)\n\n  return isStatusSuccessful(status) ? data : null\n}\n\nexport async function updateInstance(\n  id: string,\n  instance: Partial<Instance> | PartialInstance,\n) {\n  const { status } = await apiService.patch(`${endpoint}/${id}`, instance)\n\n  return isStatusSuccessful(status)\n}\n\nexport async function cloneInstance(id: string, instance: Partial<Instance>) {\n  const { status, data } = await apiService.post(\n    `${endpoint}/clone/${id}`,\n    instance,\n  )\n\n  return isStatusSuccessful(status) ? data : null\n}\n\nexport async function deleteInstances(databasesIds: string[]) {\n  const { status } = await apiService.delete(endpoint, {\n    data: { ids: databasesIds },\n  })\n  return isStatusSuccessful(status)\n}\n\nexport async function exportInstances(ids: string[], withSecrets: boolean) {\n  const { data, status } = await apiService.post<ExportDatabase>(\n    ApiEndpoints.DATABASES_EXPORT,\n    {\n      ids,\n      withSecrets,\n    },\n  )\n  return isStatusSuccessful(status) ? data : null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/databaseSettingsService.ts",
    "content": "import { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { DatabaseSettingsData } from 'uiSrc/slices/interfaces'\n\nexport const getDbSettingsUrl = (instanceId: String) =>\n  `${ApiEndpoints.DATABASES}/${instanceId}/settings`\n\nexport const getDbSettings = async (instanceId: String) => {\n  const url = getDbSettingsUrl(instanceId)\n  const {\n    data: { data },\n    status,\n  } = await apiService.get<{ data: DatabaseSettingsData }>(url)\n\n  return { data, status }\n}\n\nexport const updateDbSettings = async (\n  instanceId: String,\n  updateData: DatabaseSettingsData,\n) => {\n  const url = getDbSettingsUrl(instanceId)\n  const {\n    data: { data },\n    status,\n  } = await apiService.post<{\n    data: DatabaseSettingsData\n  }>(url, { data: updateData })\n\n  return { data, status }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/formatter/FormatSelector.spec.ts",
    "content": "import FormatSelector from './FormatSelector'\nimport HtmlToJsxString from './HtmlToJsxString'\nimport MarkdownToJsxString from './MarkdownToJsxString'\n\ndescribe('FormatSelector', () => {\n  it('should select correct formatter for html ', () => {\n    expect(FormatSelector.selectFor('html')).toBeInstanceOf(HtmlToJsxString)\n  })\n  it('should select correct formatter for markdown ', () => {\n    expect(FormatSelector.selectFor('md')).toBeInstanceOf(MarkdownToJsxString)\n  })\n  it('should throw unsupported format error', () => {\n    try {\n      FormatSelector.selectFor('mp4')\n    } catch (e) {\n      expect(e.message).toBe('Unsupported format')\n    }\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/formatter/FormatSelector.ts",
    "content": "import { IFormatter } from './formatter.interfaces'\nimport MarkdownToJsxString from './MarkdownToJsxString'\nimport HtmlToJsxString from './HtmlToJsxString'\n\nexport enum FileExtension {\n  Html = 'html',\n  Markdown = 'md',\n}\n\nclass FormatSelector {\n  private static formatters = {\n    [FileExtension.Html]: new HtmlToJsxString(),\n    [FileExtension.Markdown]: new MarkdownToJsxString(),\n  }\n\n  static selectFor(extension: string): IFormatter {\n    const FormatterFactory =\n      FormatSelector.formatters[extension as FileExtension]\n    if (FormatterFactory) {\n      return FormatterFactory\n    }\n    throw new Error('Unsupported format')\n  }\n}\n\nexport default FormatSelector\n"
  },
  {
    "path": "redisinsight/ui/src/services/formatter/HtmlToJsxString.ts",
    "content": "import { IFormatter } from './formatter.interfaces'\n\nclass HtmlToJsxString implements IFormatter {\n  format(data: any): Promise<string> {\n    return new Promise((resolve) => {\n      resolve(String(data))\n    })\n  }\n}\n\nexport default HtmlToJsxString\n"
  },
  {
    "path": "redisinsight/ui/src/services/formatter/MarkdownToJsxString.ts",
    "content": "import { unified } from 'unified'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport remarkGfm from 'remark-gfm'\nimport rehypeStringify from 'rehype-stringify'\nimport { visit } from 'unist-util-visit'\n\nimport {\n  remarkRedisUpload,\n  remarkLink,\n  rehypeLinks,\n  remarkImage,\n  remarkCode,\n  remarkSanitize,\n} from 'uiSrc/utils/formatters/markdown'\nimport { IFormatter, IFormatterConfig } from './formatter.interfaces'\n\nclass MarkdownToJsxString implements IFormatter {\n  format(input: any, config?: IFormatterConfig): Promise<string> {\n    const { data, path = '', codeOptions = {} } = input\n    return new Promise((resolve, reject) => {\n      unified()\n        .use(remarkParse)\n        .use(remarkSanitize)\n        .use(remarkGfm) // support GitHub Flavored Markdown\n        .use(remarkRedisUpload, path) // Add custom component for redis-upload code block\n        .use(remarkCode, codeOptions) // Add custom component for Redis code block\n        .use(remarkImage, path) // Add custom component for Redis code block\n        .use(remarkLink) // Customise links\n        .use(remarkRehype, { allowDangerousHtml: true }) // Pass raw HTML strings through.\n        .use(rehypeLinks, config ? { history: config.history } : undefined) // Customise links\n        .use(MarkdownToJsxString.rehypeWrapSymbols) // Wrap special symbols inside curly braces for JSX parse\n        .use(rehypeStringify, { allowDangerousHtml: true }) // Serialize the raw HTML strings\n        .process(data)\n        .then((file) => {\n          resolve(String(file))\n        })\n        .catch((error) => reject(error))\n    })\n  }\n\n  private static rehypeWrapSymbols(\n    symbols: string[] = ['{', '}', '>'],\n  ): (tree: Node) => void {\n    return (tree: any) => {\n      visit(tree, 'text', (node) => {\n        const { value } = node\n        if (value) {\n          node.value = value.replace(\n            new RegExp(`[${symbols.join()}]`, 'g'),\n            '{\"$&\"}',\n          )\n        }\n      })\n    }\n  }\n}\n\nexport default MarkdownToJsxString\n"
  },
  {
    "path": "redisinsight/ui/src/services/formatter/formatter.interfaces.ts",
    "content": "import { History } from 'history'\n\nexport interface IFormatterConfig {\n  history: History\n}\n\nexport interface IFormatter {\n  format(data: any, config?: IFormatterConfig): Promise<string>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/hooks.ts",
    "content": "import { RefObject, useCallback, useEffect, useState } from 'react'\n\nexport const useResizableFormField = (\n  formRef: RefObject<HTMLDivElement>,\n  width: number,\n) => {\n  const flexFormWidth = 700\n  const flexGroupResponsiveForm = 'flexGroupResponsiveForm'\n  const flexItemResponsiveForm = 'flexItemResponsiveForm'\n\n  const [flexGroupClassName, setFlexGroupClassName] = useState('')\n  const [flexItemClassName, setFlexItemClassName] = useState('')\n\n  useEffect(() => {\n    if (formRef.current) {\n      const { offsetWidth } = formRef.current\n\n      setFlexItemClassName(\n        offsetWidth < flexFormWidth ? flexItemResponsiveForm : '',\n      )\n      setFlexGroupClassName(\n        offsetWidth < flexFormWidth ? flexGroupResponsiveForm : '',\n      )\n    }\n  }, [width])\n\n  return [flexGroupClassName, flexItemClassName]\n}\n\nexport const useDebouncedEffect = (\n  effect: () => void,\n  delay: number,\n  deps: any[],\n) => {\n  const callback = useCallback(effect, deps)\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      callback()\n    }, delay)\n\n    return () => {\n      clearTimeout(handler)\n    }\n  }, [callback, delay])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/index.ts",
    "content": "export * from './hooks'\nexport * from './useWebworkers'\nexport * from './useCabability'\nexport * from './useStateWithContext'\nexport * from './useLoadData'\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useCabability.ts",
    "content": "import { useEffect } from 'react'\nimport { useDispatch } from 'react-redux'\nimport { setCapability } from 'uiSrc/slices/app/context'\n\nexport const useCapability = (source = '') => {\n  const dispatch = useDispatch()\n\n  useEffect(() => {\n    dispatch(setCapability({ source, tutorialPopoverShown: false }))\n  }, [source])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useIoConnection.ts",
    "content": "import { useSelector } from 'react-redux'\nimport { useCallback } from 'react'\nimport { WsParams, wsService } from 'uiSrc/services/wsService'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\n\nexport const useIoConnection = (url: string, params: WsParams) => {\n  const { [FeatureFlags.envDependent]: envDependent } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n  return useCallback(\n    () => wsService(url, params, envDependent?.flag),\n    [url, params, envDependent],\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useLoadData.spec.ts",
    "content": "import { act, renderHook } from '@testing-library/react-hooks'\nimport { http, HttpResponse } from 'msw'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { getUrl } from 'uiSrc/utils'\nimport { useLoadData } from './useLoadData'\n\ndescribe('useLoadData', () => {\n  const instanceId = 'test-instance-id'\n  const collectionName = 'test-collection'\n\n  beforeEach(() => {\n    mswServer.resetHandlers()\n  })\n\n  it('should return initial state correctly', () => {\n    const { result } = renderHook(() => useLoadData())\n\n    expect(result.current.loading).toBe(false)\n    expect(result.current.error).toBeNull()\n  })\n\n  it('should successfully load data and return response', async () => {\n    const mockResponse = {\n      id: 'bulk-action-123',\n      summary: {\n        processed: 100,\n        succeed: 95,\n        failed: 5,\n      },\n      status: 'completed',\n    }\n\n    mswServer.use(\n      http.post(\n        getMswURL(\n          getUrl(\n            instanceId,\n            ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n          ),\n        ),\n        async ({ request }) => {\n          const body = await request.json()\n          expect(body).toEqual({ collectionName })\n          return HttpResponse.json(mockResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadData())\n\n    let returnedData\n    await act(async () => {\n      returnedData = await result.current.load(instanceId, collectionName)\n    })\n\n    expect(returnedData).toEqual(mockResponse)\n    expect(result.current.loading).toBe(false)\n    expect(result.current.error).toBeNull()\n  })\n\n  it('should set loading state correctly during API call', async () => {\n    const mockResponse = { id: '123' }\n    let resolveRequest: () => void\n    const requestPromise = new Promise<void>((resolve) => {\n      resolveRequest = resolve\n    })\n\n    mswServer.use(\n      http.post(\n        getMswURL(\n          getUrl(\n            instanceId,\n            ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n          ),\n        ),\n        async () => {\n          await requestPromise\n          return HttpResponse.json(mockResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadData())\n\n    expect(result.current.loading).toBe(false)\n\n    // Start the request without awaiting\n    act(() => {\n      result.current.load(instanceId, collectionName)\n    })\n\n    // Loading should be true while request is pending\n    expect(result.current.loading).toBe(true)\n    expect(result.current.error).toBeNull()\n\n    // Complete the request\n    await act(async () => {\n      resolveRequest()\n    })\n\n    // Loading should be false after completion\n    expect(result.current.loading).toBe(false)\n    expect(result.current.error).toBeNull()\n  })\n\n  it('should handle API errors correctly when error is an Error instance', async () => {\n    const errorMessage = 'Network error occurred'\n\n    mswServer.use(\n      http.post(\n        getMswURL(\n          getUrl(\n            instanceId,\n            ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n          ),\n        ),\n        async () => {\n          return HttpResponse.json({ message: errorMessage }, { status: 500 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadData())\n\n    await act(async () => {\n      try {\n        await result.current.load(instanceId, collectionName)\n      } catch (err) {\n        expect(err).toBeInstanceOf(Error)\n      }\n    })\n\n    expect(result.current.loading).toBe(false)\n    expect(result.current.error).toBeInstanceOf(Error)\n  })\n\n  it('should reset error state on new load attempt', async () => {\n    const mockResponse = { id: '123' }\n    let callCount = 0\n\n    // Mock first call to fail, second to succeed\n    mswServer.use(\n      http.post(\n        getMswURL(\n          getUrl(\n            instanceId,\n            ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n          ),\n        ),\n        async () => {\n          callCount++\n          if (callCount === 1) {\n            return HttpResponse.json(\n              { message: 'Server error' },\n              { status: 500 },\n            )\n          }\n          return HttpResponse.json(mockResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadData())\n\n    // First call fails\n    await act(async () => {\n      try {\n        await result.current.load(instanceId, collectionName)\n      } catch {\n        // Expected to fail\n      }\n    })\n\n    expect(result.current.error).toBeInstanceOf(Error)\n\n    // Second call succeeds\n    await act(async () => {\n      await result.current.load(instanceId, collectionName)\n    })\n\n    expect(result.current.error).toBeNull()\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should handle multiple concurrent calls correctly', async () => {\n    const mockResponse1 = { id: '123' }\n    const mockResponse2 = { id: '456' }\n    let callCount = 0\n\n    mswServer.use(\n      http.post(\n        getMswURL(\n          getUrl(\n            instanceId,\n            ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n          ),\n        ),\n        async () => {\n          callCount++\n          const response = callCount === 1 ? mockResponse1 : mockResponse2\n          return HttpResponse.json(response, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadData())\n\n    let result1: any\n    let result2: any\n\n    await act(async () => {\n      const [promise1, promise2] = await Promise.all([\n        result.current.load(instanceId, 'collection1'),\n        result.current.load(instanceId, 'collection2'),\n      ])\n      result1 = promise1\n      result2 = promise2\n    })\n\n    expect(result1).toEqual(mockResponse1)\n    expect(result2).toEqual(mockResponse2)\n    expect(result.current.loading).toBe(false)\n  })\n\n  it('should call API with correct parameters for different collections', async () => {\n    const mockResponse = { id: '123' }\n    const requestBodies: any[] = []\n\n    mswServer.use(\n      http.post(\n        '*/bulk-actions/import/vector-collection',\n        async ({ request }) => {\n          const body = await request.json()\n          requestBodies.push(body)\n          return HttpResponse.json(mockResponse, { status: 200 })\n        },\n      ),\n    )\n\n    const { result } = renderHook(() => useLoadData())\n\n    await act(async () => {\n      await result.current.load('instance-1', 'bikes')\n    })\n\n    await act(async () => {\n      await result.current.load('instance-2', 'cars')\n    })\n\n    expect(requestBodies).toHaveLength(2)\n    expect(requestBodies[0]).toEqual({ collectionName: 'bikes' })\n    expect(requestBodies[1]).toEqual({ collectionName: 'cars' })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useLoadData.ts",
    "content": "import { useState, useCallback } from 'react'\nimport { apiService } from 'uiSrc/services'\nimport { getUrl } from 'uiSrc/utils'\nimport { IBulkActionOverview } from 'uiSrc/slices/interfaces'\nimport { ApiEndpoints } from 'uiSrc/constants'\n\ninterface UseLoadDataResult {\n  load: (instanceId: string, collection: string) => Promise<IBulkActionOverview>\n  loading: boolean\n  error: Error | null\n}\n\nexport const useLoadData = (): UseLoadDataResult => {\n  const [loading, setLoading] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n\n  const loadData = useCallback(\n    async (\n      instanceId: string,\n      collectionName: string,\n    ): Promise<IBulkActionOverview> => {\n      setLoading(true)\n      setError(null)\n\n      try {\n        const { data } = await apiService.post(\n          getUrl(\n            instanceId,\n            ApiEndpoints.BULK_ACTIONS_IMPORT_VECTOR_COLLECTION,\n          ),\n          { collectionName },\n        )\n\n        return data\n      } catch (err) {\n        const error =\n          err instanceof Error\n            ? err\n            : new Error('Failed to import vector collection')\n        setError(error)\n        throw error\n      } finally {\n        setLoading(false)\n      }\n    },\n    [],\n  )\n\n  return {\n    load: loadData,\n    loading,\n    error,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useStateWithContext.ts",
    "content": "import { useSelector } from 'react-redux'\nimport { useState } from 'react'\nimport { useParams } from 'react-router-dom'\nimport { appContextSelector } from 'uiSrc/slices/app/context'\n\nexport const useStateWithContext = <T>(value: T, initialValue: T) => {\n  const { instanceId } = useParams<{ instanceId: string }>()\n  const { contextInstanceId } = useSelector(appContextSelector)\n\n  return useState<T>(instanceId === contextInstanceId ? value : initialValue)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useSystemThemeListener.ts",
    "content": "import { useEffect } from 'react'\nimport { Theme, THEME_MATCH_MEDIA_DARK } from 'uiSrc/constants'\nimport { useThemeContext } from 'uiSrc/contexts/themeContext'\n\n/**\n * Hook that listens to OS system theme changes\n * and updates the theme context when user has System theme selected.\n */\nexport const useSystemThemeListener = () => {\n  const { usingSystemTheme, changeTheme } = useThemeContext()\n\n  useEffect(() => {\n    const handleSystemThemeChange = () => {\n      changeTheme(Theme.System)\n    }\n    const mediaQuery = window.matchMedia?.(THEME_MATCH_MEDIA_DARK)\n    if (usingSystemTheme && mediaQuery) {\n      mediaQuery.addEventListener('change', handleSystemThemeChange)\n    }\n\n    return () => {\n      mediaQuery?.removeEventListener('change', handleSystemThemeChange)\n    }\n  }, [usingSystemTheme, changeTheme])\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/hooks/useWebworkers.ts",
    "content": "import { useState } from 'react'\nimport { Nullable } from 'uiSrc/utils'\n\nconst workerHandler = (fn: (...args: any) => any) => {\n  onmessage = (event) => {\n    postMessage(fn(event.data))\n  }\n}\n\nexport const useDisposableWebworker = (fn: (...args: any) => any) => {\n  const [result, setResult] = useState<Nullable<any>>(null)\n  const [error, setError] = useState<Nullable<ErrorEvent>>(null)\n\n  const run = (value: any) => {\n    const worker = new Worker(\n      URL.createObjectURL(new Blob([`(${workerHandler})(${fn})`])),\n    )\n    worker.onmessage = (event) => {\n      setResult(event.data)\n      worker.terminate()\n    }\n    worker.onerror = (error: ErrorEvent) => {\n      setError(error)\n      worker.terminate()\n    }\n    worker.postMessage(value)\n  }\n\n  return {\n    result,\n    error,\n    run,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/index.ts",
    "content": "/* eslint-disable import/first */\nexport * from './storage'\nexport * from './migrateStorageData'\n\nimport apiService from './apiService'\nimport resourcesService from './resourcesService'\n\nexport * from './routing'\nexport * from './theme'\n\nexport * from './hooks'\nexport * from './capability'\nexport { apiService, resourcesService }\nexport { WorkbenchStorage } from 'uiSrc/services/workbenchStorage'\nexport * as instancesService from 'uiSrc/services/database/instancesService'\n"
  },
  {
    "path": "redisinsight/ui/src/services/keys.ts",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/services/migrateStorageData.ts",
    "content": "import { isString } from 'lodash'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  getDBConfigStorageField,\n  localStorageService,\n  setDBConfigStorageField,\n} from './storage'\n\nexport const migrateLocalStorageData = () => {\n  migrateDelimiterTreeView()\n}\n\nconst migrateDelimiterTreeView = () => {\n  const prefix = 'dbConfig_'\n  const storage = localStorageService.getAll()\n\n  // Iterate over all keys and filter for the dbConfig_ prefix\n  Object.keys(storage).forEach((key) => {\n    if (key.startsWith(prefix)) {\n      const instanceId = key.replace(prefix, '')\n\n      const treeViewDelimiter = getDBConfigStorageField(\n        instanceId,\n        BrowserStorageItem.treeViewDelimiter,\n      )\n\n      // Check if treeViewDelimiter is a string and needs transform to array\n      if (isString(treeViewDelimiter)) {\n        setDBConfigStorageField(\n          instanceId,\n          BrowserStorageItem.treeViewDelimiter,\n          [{ label: treeViewDelimiter }],\n        )\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/QueryLibraryService.spec.ts",
    "content": "import { merge } from 'lodash'\nimport { faker } from '@faker-js/faker'\n\nimport { RootState, store } from 'uiSrc/slices/store'\nimport { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { initialState as appFeaturesInitialState } from 'uiSrc/slices/app/features'\nimport {\n  queryLibraryItemFactory,\n  createQueryLibraryItemFactory,\n  seedQueryLibraryItemFactory,\n} from 'uiSrc/mocks/factories/query-library/queryLibraryItem.factory'\n\nimport { QueryLibraryService } from './QueryLibraryService'\nimport { QueryLibraryDatabase } from './database/interface'\nimport { QueryLibrarySQLite } from './database/QueryLibrarySQLite'\nimport { QueryLibraryIndexedDB } from './database/QueryLibraryIndexedDB'\n\njest.mock('./database/QueryLibrarySQLite')\njest.mock('./database/QueryLibraryIndexedDB', () => ({\n  QueryLibraryIndexedDB: jest.fn().mockImplementation(() => ({\n    getList: jest.fn().mockResolvedValue({ success: true, data: [] }),\n    getOne: jest.fn().mockResolvedValue({ success: true, data: null }),\n    create: jest.fn().mockResolvedValue({ success: true, data: null }),\n    update: jest.fn().mockResolvedValue({ success: true, data: null }),\n    delete: jest.fn().mockResolvedValue({ success: true }),\n    seed: jest.fn().mockResolvedValue({ success: true, data: [] }),\n  })),\n}))\n\njest.mock('uiSrc/slices/app/notifications', () => ({\n  addErrorNotification: jest.fn((error) => ({\n    type: 'app/notifications/addErrorNotification',\n    payload: error,\n  })),\n}))\n\njest.mock('uiSrc/slices/store', () => ({\n  store: {\n    getState: jest.fn(),\n    dispatch: jest.fn(),\n  },\n}))\n\nconst mockedSQLite = jest.mocked(QueryLibrarySQLite)\nconst mockedIndexedDB = jest.mocked(QueryLibraryIndexedDB)\nconst mockedAddErrorNotification = addErrorNotification as unknown as jest.Mock\n\ndescribe('QueryLibraryService', () => {\n  const mockedStore = jest.mocked(store)\n\n  const mockDatabaseId = faker.string.uuid()\n  const mockIndexName = `idx:${faker.word.noun()}`\n  const mockFilter = { indexName: mockIndexName }\n  const mockItems = queryLibraryItemFactory.buildList(3, {\n    databaseId: mockDatabaseId,\n    indexName: mockIndexName,\n  })\n  const mockItem = queryLibraryItemFactory.build({\n    databaseId: mockDatabaseId,\n    indexName: mockIndexName,\n  })\n\n  const createDefaultDatabaseMock = (\n    overrides: Partial<Record<keyof QueryLibraryDatabase, jest.Mock>> = {},\n  ): QueryLibraryDatabase => ({\n    getList: jest.fn().mockResolvedValue({ success: true, data: [] }),\n    getOne: jest.fn().mockResolvedValue({ success: true, data: null }),\n    create: jest.fn().mockResolvedValue({ success: true, data: null }),\n    update: jest.fn().mockResolvedValue({ success: true, data: null }),\n    delete: jest.fn().mockResolvedValue({ success: true }),\n    seed: jest.fn().mockResolvedValue({ success: true, data: [] }),\n    ...overrides,\n  })\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    mockedStore.getState.mockReturnValue({\n      app: {\n        features: merge({}, appFeaturesInitialState, {\n          featureFlags: {\n            features: {\n              [FeatureFlags.envDependent]: { flag: false },\n            },\n          },\n        }),\n      },\n    } as RootState)\n    mockedStore.dispatch.mockClear()\n\n    mockedIndexedDB.mockImplementation(() => createDefaultDatabaseMock())\n  })\n\n  describe('initializeDatabase', () => {\n    it('should use IndexedDB when envDependent is disabled', () => {\n      const service = new QueryLibraryService()\n      expect(mockedIndexedDB).toHaveBeenCalled()\n      expect(service).toBeDefined()\n    })\n\n    it('should use SQLite when envDependent is enabled', () => {\n      mockedStore.getState.mockReturnValue({\n        app: {\n          features: merge({}, appFeaturesInitialState, {\n            featureFlags: {\n              features: {\n                [FeatureFlags.envDependent]: { flag: true },\n              },\n            },\n          }),\n        },\n      } as RootState)\n\n      const service = new QueryLibraryService()\n      expect(mockedSQLite).toHaveBeenCalled()\n      expect(service).toBeDefined()\n    })\n  })\n\n  describe('getList', () => {\n    it('should return items on success', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          getList: jest.fn().mockResolvedValue({\n            success: true,\n            data: mockItems,\n          }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.getList(mockDatabaseId, mockFilter)\n\n      expect(result).toEqual(mockItems)\n    })\n\n    it('should return empty array when data is undefined', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          getList: jest\n            .fn()\n            .mockResolvedValue({ success: true, data: undefined }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.getList(mockDatabaseId, mockFilter)\n\n      expect(result).toEqual([])\n    })\n\n    it('should dispatch error notification on error', async () => {\n      const mockError = new Error('Request failed')\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          getList: jest\n            .fn()\n            .mockResolvedValue({ success: false, error: mockError }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.getList(mockDatabaseId, mockFilter)\n\n      expect(result).toEqual([])\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        mockedAddErrorNotification(mockError),\n      )\n    })\n  })\n\n  describe('getOne', () => {\n    it('should return item on success', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          getOne: jest.fn().mockResolvedValue({\n            success: true,\n            data: mockItem,\n          }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.getOne(mockDatabaseId, mockItem.id)\n\n      expect(result).toEqual(mockItem)\n    })\n\n    it('should return null when item not found', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          getOne: jest.fn().mockResolvedValue({ success: false }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.getOne(mockDatabaseId, mockItem.id)\n\n      expect(result).toEqual(null)\n    })\n\n    it('should dispatch error notification on error', async () => {\n      const mockError = new Error('Not found')\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          getOne: jest\n            .fn()\n            .mockResolvedValue({ success: false, error: mockError }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      await service.getOne(mockDatabaseId, mockItem.id)\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        mockedAddErrorNotification(mockError),\n      )\n    })\n  })\n\n  describe('create', () => {\n    const createDto = createQueryLibraryItemFactory.build({\n      indexName: mockIndexName,\n    })\n\n    it('should return created item on success', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          create: jest.fn().mockResolvedValue({\n            success: true,\n            data: mockItem,\n          }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.create(mockDatabaseId, createDto)\n\n      expect(result).toEqual(mockItem)\n    })\n\n    it('should return null on failure', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          create: jest.fn().mockResolvedValue({ success: false }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.create(mockDatabaseId, createDto)\n\n      expect(result).toEqual(null)\n    })\n\n    it('should dispatch error notification on error', async () => {\n      const mockError = new Error('Create failed')\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          create: jest\n            .fn()\n            .mockResolvedValue({ success: false, error: mockError }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      await service.create(mockDatabaseId, createDto)\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        mockedAddErrorNotification(mockError),\n      )\n    })\n  })\n\n  describe('update', () => {\n    const updateDto = { name: faker.lorem.words(2) }\n\n    it('should return updated item on success', async () => {\n      const updated = { ...mockItem, ...updateDto }\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          update: jest.fn().mockResolvedValue({\n            success: true,\n            data: updated,\n          }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.update(\n        mockDatabaseId,\n        mockItem.id,\n        updateDto,\n      )\n\n      expect(result).toEqual(updated)\n    })\n\n    it('should return null on failure', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          update: jest.fn().mockResolvedValue({ success: false }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.update(\n        mockDatabaseId,\n        mockItem.id,\n        updateDto,\n      )\n\n      expect(result).toEqual(null)\n    })\n\n    it('should dispatch error notification on error', async () => {\n      const mockError = new Error('Update failed')\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          update: jest\n            .fn()\n            .mockResolvedValue({ success: false, error: mockError }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      await service.update(mockDatabaseId, mockItem.id, updateDto)\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        mockedAddErrorNotification(mockError),\n      )\n    })\n  })\n\n  describe('delete', () => {\n    it('should delete successfully without dispatching errors', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          delete: jest.fn().mockResolvedValue({ success: true }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      await service.delete(mockDatabaseId, mockItem.id)\n\n      expect(mockedStore.dispatch).not.toHaveBeenCalled()\n    })\n\n    it('should dispatch error notification and throw on error', async () => {\n      const mockError = new Error('Delete failed')\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          delete: jest\n            .fn()\n            .mockResolvedValue({ success: false, error: mockError }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      await expect(service.delete(mockDatabaseId, mockItem.id)).rejects.toThrow(\n        mockError,\n      )\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        mockedAddErrorNotification(mockError),\n      )\n    })\n  })\n\n  describe('seed', () => {\n    const seedItems = seedQueryLibraryItemFactory.buildList(1, {\n      indexName: mockIndexName,\n    })\n\n    it('should return seeded items on success', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          seed: jest.fn().mockResolvedValue({\n            success: true,\n            data: mockItems,\n          }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.seed(mockDatabaseId, seedItems)\n\n      expect(result).toEqual(mockItems)\n    })\n\n    it('should return empty array on failure', async () => {\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          seed: jest.fn().mockResolvedValue({ success: false }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      const result = await service.seed(mockDatabaseId, seedItems)\n\n      expect(result).toEqual([])\n    })\n\n    it('should dispatch error notification on error', async () => {\n      const mockError = new Error('Seed failed')\n\n      mockedIndexedDB.mockImplementation(() =>\n        createDefaultDatabaseMock({\n          seed: jest\n            .fn()\n            .mockResolvedValue({ success: false, error: mockError }),\n        }),\n      )\n\n      const service = new QueryLibraryService()\n      await service.seed(mockDatabaseId, seedItems)\n\n      expect(mockedStore.dispatch).toHaveBeenCalledWith(\n        mockedAddErrorNotification(mockError),\n      )\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/QueryLibraryService.ts",
    "content": "import { FeatureFlags } from 'uiSrc/constants/featureFlags'\nimport { store } from 'uiSrc/slices/store'\nimport {\n  addErrorNotification,\n  IAddInstanceErrorPayload,\n} from 'uiSrc/slices/app/notifications'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport {\n  QueryLibraryItem,\n  CreateQueryLibraryItem,\n  UpdateQueryLibraryItem,\n  SeedQueryLibraryItem,\n  QueryLibraryFilter,\n} from './types'\nimport { QueryLibraryDatabase } from './database/interface'\nimport { QueryLibrarySQLite } from './database/QueryLibrarySQLite'\nimport { QueryLibraryIndexedDB } from './database/QueryLibraryIndexedDB'\n\nexport class QueryLibraryService {\n  private database: QueryLibraryDatabase\n\n  constructor() {\n    this.database = this.initializeDatabase()\n  }\n\n  private initializeDatabase(): QueryLibraryDatabase {\n    const state = store.getState()\n    const { [FeatureFlags.envDependent]: envDependentFeature } =\n      appFeatureFlagsFeaturesSelector(state)\n\n    if (envDependentFeature?.flag) {\n      return new QueryLibrarySQLite()\n    }\n\n    return new QueryLibraryIndexedDB()\n  }\n\n  async getList(\n    databaseId: string,\n    filter: QueryLibraryFilter,\n  ): Promise<QueryLibraryItem[]> {\n    const { data, error } = await this.database.getList(databaseId, filter)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n    }\n\n    return data || []\n  }\n\n  async getOne(\n    databaseId: string,\n    id: string,\n  ): Promise<QueryLibraryItem | null> {\n    const { data, error } = await this.database.getOne(databaseId, id)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n    }\n\n    return data || null\n  }\n\n  async create(\n    databaseId: string,\n    item: CreateQueryLibraryItem,\n  ): Promise<QueryLibraryItem | null> {\n    const { data, error } = await this.database.create(databaseId, item)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n    }\n\n    return data || null\n  }\n\n  async update(\n    databaseId: string,\n    id: string,\n    item: UpdateQueryLibraryItem,\n  ): Promise<QueryLibraryItem | null> {\n    const { data, error } = await this.database.update(databaseId, id, item)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n    }\n\n    return data || null\n  }\n\n  async delete(databaseId: string, id: string): Promise<void> {\n    const { error } = await this.database.delete(databaseId, id)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n      throw error\n    }\n  }\n\n  async deleteByIndex(databaseId: string, indexName: string): Promise<void> {\n    const { error } = await this.database.deleteByIndex(databaseId, indexName)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n    }\n  }\n\n  async seed(\n    databaseId: string,\n    items: SeedQueryLibraryItem[],\n  ): Promise<QueryLibraryItem[]> {\n    const { data, error } = await this.database.seed(databaseId, items)\n\n    if (error) {\n      store.dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n    }\n\n    return data || []\n  }\n}\n\nexport default QueryLibraryService\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/database/QueryLibraryIndexedDB.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport {\n  QueryLibraryType,\n  QueryLibraryItem,\n} from 'uiSrc/services/query-library/types'\nimport {\n  queryLibraryItemFactory,\n  createQueryLibraryItemFactory,\n  seedQueryLibraryItemFactory,\n} from 'uiSrc/mocks/factories/query-library/queryLibraryItem.factory'\n\nimport { queryLibraryStorage } from './QueryLibraryStorage'\nimport { QueryLibraryIndexedDB } from './QueryLibraryIndexedDB'\n\njest.mock('./QueryLibraryStorage', () => ({\n  queryLibraryStorage: {\n    getAllByIndex: jest.fn(),\n    getById: jest.fn(),\n    put: jest.fn(),\n    remove: jest.fn(),\n  },\n}))\n\nconst mockedStorage = jest.mocked(queryLibraryStorage)\n\ndescribe('QueryLibraryIndexedDB', () => {\n  let indexedDB: QueryLibraryIndexedDB\n  const mockDatabaseId = faker.string.uuid()\n  const mockIndexName = `idx:${faker.word.noun()}`\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    indexedDB = new QueryLibraryIndexedDB()\n  })\n\n  describe('getList', () => {\n    it('should return items from storage', async () => {\n      const mockItems = queryLibraryItemFactory.buildList(3, {\n        databaseId: mockDatabaseId,\n        indexName: mockIndexName,\n      })\n      mockedStorage.getAllByIndex.mockResolvedValue(mockItems)\n\n      const result = await indexedDB.getList(mockDatabaseId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result.success).toBe(true)\n      expect(result.data).toHaveLength(3)\n      expect(mockedStorage.getAllByIndex).toHaveBeenCalledWith(\n        mockDatabaseId,\n        mockIndexName,\n      )\n    })\n\n    it('should filter items by search term across name, description, and query fields', async () => {\n      const items: QueryLibraryItem[] = [\n        queryLibraryItemFactory.build({\n          name: 'Find all bikes',\n          description: '',\n          query: '',\n        }),\n        queryLibraryItemFactory.build({\n          name: '',\n          description: 'bikes query',\n          query: '',\n        }),\n        queryLibraryItemFactory.build({\n          name: '',\n          description: '',\n          query: 'FT.SEARCH bikes \"*\"',\n        }),\n        queryLibraryItemFactory.build({\n          name: 'Count documents',\n          description: 'unrelated',\n          query: 'FT.SEARCH products \"*\"',\n        }),\n      ]\n      mockedStorage.getAllByIndex.mockResolvedValue(items)\n\n      const result = await indexedDB.getList(mockDatabaseId, {\n        indexName: mockIndexName,\n        search: 'bikes',\n      })\n\n      expect(result.success).toBe(true)\n      expect(result.data).toHaveLength(3)\n    })\n\n    it('should sort items by createdAt ascending', async () => {\n      const items: QueryLibraryItem[] = [\n        queryLibraryItemFactory.build({\n          createdAt: '2026-01-03T00:00:00.000Z',\n        }),\n        queryLibraryItemFactory.build({\n          createdAt: '2026-01-01T00:00:00.000Z',\n        }),\n        queryLibraryItemFactory.build({\n          createdAt: '2026-01-02T00:00:00.000Z',\n        }),\n      ]\n      mockedStorage.getAllByIndex.mockResolvedValue(items)\n\n      const result = await indexedDB.getList(mockDatabaseId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result.data![0].createdAt).toBe('2026-01-01T00:00:00.000Z')\n      expect(result.data![1].createdAt).toBe('2026-01-02T00:00:00.000Z')\n      expect(result.data![2].createdAt).toBe('2026-01-03T00:00:00.000Z')\n    })\n\n    it('should return empty array when storage is empty', async () => {\n      mockedStorage.getAllByIndex.mockResolvedValue([])\n\n      const result = await indexedDB.getList(mockDatabaseId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result).toEqual({ success: true, data: [] })\n    })\n\n    it('should return error on storage failure', async () => {\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.getAllByIndex.mockRejectedValue(mockError)\n\n      const result = await indexedDB.getList(mockDatabaseId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n    })\n  })\n\n  describe('getOne', () => {\n    it('should return item when found and databaseId matches', async () => {\n      const mockItem = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      })\n      mockedStorage.getById.mockResolvedValue(mockItem)\n\n      const result = await indexedDB.getOne(mockDatabaseId, mockItem.id)\n\n      expect(result).toEqual({ success: true, data: mockItem })\n      expect(mockedStorage.getById).toHaveBeenCalledWith(mockItem.id)\n    })\n\n    it('should return success false when item not found', async () => {\n      mockedStorage.getById.mockResolvedValue(undefined)\n\n      const result = await indexedDB.getOne(mockDatabaseId, faker.string.uuid())\n\n      expect(result).toEqual({ success: false })\n    })\n\n    it('should return success false when databaseId does not match', async () => {\n      const mockItem = queryLibraryItemFactory.build({\n        databaseId: 'other-db-id',\n      })\n      mockedStorage.getById.mockResolvedValue(mockItem)\n\n      const result = await indexedDB.getOne(mockDatabaseId, mockItem.id)\n\n      expect(result).toEqual({ success: false })\n    })\n\n    it('should return error on storage failure', async () => {\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.getById.mockRejectedValue(mockError)\n\n      const result = await indexedDB.getOne(mockDatabaseId, faker.string.uuid())\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n    })\n  })\n\n  describe('create', () => {\n    it('should create item with Saved type and store it', async () => {\n      mockedStorage.put.mockResolvedValue(undefined)\n      const input = createQueryLibraryItemFactory.build({\n        indexName: mockIndexName,\n      })\n\n      const result = await indexedDB.create(mockDatabaseId, input)\n\n      expect(result.success).toBe(true)\n      expect(result.data).toMatchObject({\n        databaseId: mockDatabaseId,\n        indexName: input.indexName,\n        type: QueryLibraryType.Saved,\n        name: input.name,\n        query: input.query,\n      })\n      expect(result.data?.id).toBeDefined()\n      expect(result.data?.createdAt).toBeDefined()\n      expect(result.data?.updatedAt).toBeDefined()\n      expect(mockedStorage.put).toHaveBeenCalledTimes(1)\n    })\n\n    it('should return error on storage failure', async () => {\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.put.mockRejectedValue(mockError)\n      const input = createQueryLibraryItemFactory.build()\n\n      const result = await indexedDB.create(mockDatabaseId, input)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n    })\n  })\n\n  describe('update', () => {\n    it('should update only provided fields', async () => {\n      const existing = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n        name: 'Original',\n        query: 'FT.SEARCH idx \"*\"',\n        description: 'Original desc',\n      })\n      mockedStorage.getById.mockResolvedValue(existing)\n      mockedStorage.put.mockResolvedValue(undefined)\n\n      const result = await indexedDB.update(mockDatabaseId, existing.id, {\n        name: 'Updated',\n      })\n\n      expect(result.success).toBe(true)\n      expect(result.data?.name).toBe('Updated')\n      expect(result.data?.query).toBe('FT.SEARCH idx \"*\"')\n      expect(result.data?.description).toBe('Original desc')\n    })\n\n    it('should return success false when item not found', async () => {\n      mockedStorage.getById.mockResolvedValue(undefined)\n\n      const result = await indexedDB.update(\n        mockDatabaseId,\n        faker.string.uuid(),\n        { name: 'Updated' },\n      )\n\n      expect(result).toEqual({ success: false })\n    })\n\n    it('should return error on storage failure', async () => {\n      const existing = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      })\n      mockedStorage.getById.mockResolvedValue(existing)\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.put.mockRejectedValue(mockError)\n\n      const result = await indexedDB.update(mockDatabaseId, existing.id, {\n        name: 'Updated',\n      })\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n    })\n  })\n\n  describe('delete', () => {\n    it('should remove item from storage when databaseId matches', async () => {\n      const mockItem = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      })\n      mockedStorage.getById.mockResolvedValue(mockItem)\n      mockedStorage.remove.mockResolvedValue(undefined)\n\n      const result = await indexedDB.delete(mockDatabaseId, mockItem.id)\n\n      expect(result).toEqual({ success: true })\n      expect(mockedStorage.remove).toHaveBeenCalledWith(mockItem.id)\n    })\n\n    it('should return success false when item not found', async () => {\n      mockedStorage.getById.mockResolvedValue(undefined)\n\n      const result = await indexedDB.delete(mockDatabaseId, faker.string.uuid())\n\n      expect(result).toEqual({ success: false })\n      expect(mockedStorage.remove).not.toHaveBeenCalled()\n    })\n\n    it('should return success false when databaseId does not match', async () => {\n      const mockItem = queryLibraryItemFactory.build({\n        databaseId: 'other-db-id',\n      })\n      mockedStorage.getById.mockResolvedValue(mockItem)\n\n      const result = await indexedDB.delete(mockDatabaseId, mockItem.id)\n\n      expect(result).toEqual({ success: false })\n      expect(mockedStorage.remove).not.toHaveBeenCalled()\n    })\n\n    it('should return error on storage failure', async () => {\n      const mockItem = queryLibraryItemFactory.build({\n        databaseId: mockDatabaseId,\n      })\n      mockedStorage.getById.mockResolvedValue(mockItem)\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.remove.mockRejectedValue(mockError)\n\n      const result = await indexedDB.delete(mockDatabaseId, mockItem.id)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n    })\n  })\n\n  describe('seed', () => {\n    it('should create items with Sample type', async () => {\n      mockedStorage.getAllByIndex.mockResolvedValue([])\n      mockedStorage.put.mockResolvedValue(undefined)\n\n      const seedItems = seedQueryLibraryItemFactory.buildList(2, {\n        indexName: mockIndexName,\n      })\n\n      const result = await indexedDB.seed(mockDatabaseId, seedItems)\n\n      expect(result.success).toBe(true)\n      expect(result.data).toHaveLength(2)\n      expect(result.data![0].type).toBe(QueryLibraryType.Sample)\n      expect(result.data![0].name).toBe(seedItems[0].name)\n      expect(result.data![1].type).toBe(QueryLibraryType.Sample)\n      expect(result.data![1].name).toBe(seedItems[1].name)\n      expect(mockedStorage.put).toHaveBeenCalledTimes(2)\n    })\n\n    it('should skip items that already exist by name', async () => {\n      const existingSample = queryLibraryItemFactory.build({\n        type: QueryLibraryType.Sample,\n        indexName: mockIndexName,\n        name: 'Existing Query',\n      })\n      mockedStorage.getAllByIndex.mockResolvedValue([existingSample])\n\n      const seedItems = [\n        seedQueryLibraryItemFactory.build({\n          indexName: mockIndexName,\n          name: 'Existing Query',\n        }),\n        seedQueryLibraryItemFactory.build({\n          indexName: mockIndexName,\n          name: 'New Query',\n        }),\n      ]\n      mockedStorage.put.mockResolvedValue(undefined)\n\n      const result = await indexedDB.seed(mockDatabaseId, seedItems)\n\n      expect(result.success).toBe(true)\n      expect(result.data).toHaveLength(2)\n      expect(mockedStorage.put).toHaveBeenCalledTimes(1)\n      expect(mockedStorage.put).toHaveBeenCalledWith(\n        expect.objectContaining({ name: 'New Query' }),\n      )\n    })\n\n    it('should skip all items when all already exist', async () => {\n      const existingItems = [\n        queryLibraryItemFactory.build({\n          type: QueryLibraryType.Sample,\n          indexName: mockIndexName,\n          name: 'Query A',\n        }),\n        queryLibraryItemFactory.build({\n          type: QueryLibraryType.Sample,\n          indexName: mockIndexName,\n          name: 'Query B',\n        }),\n      ]\n      mockedStorage.getAllByIndex.mockResolvedValue(existingItems)\n\n      const seedItems = [\n        seedQueryLibraryItemFactory.build({\n          indexName: mockIndexName,\n          name: 'Query A',\n        }),\n        seedQueryLibraryItemFactory.build({\n          indexName: mockIndexName,\n          name: 'Query B',\n        }),\n      ]\n\n      const result = await indexedDB.seed(mockDatabaseId, seedItems)\n\n      expect(result.success).toBe(true)\n      expect(result.data).toEqual(existingItems)\n      expect(mockedStorage.put).not.toHaveBeenCalled()\n    })\n\n    it('should not treat Saved items as existing samples', async () => {\n      const savedItem = queryLibraryItemFactory.build({\n        type: QueryLibraryType.Saved,\n        indexName: mockIndexName,\n        name: 'Same Name',\n      })\n      mockedStorage.getAllByIndex.mockResolvedValue([savedItem])\n      mockedStorage.put.mockResolvedValue(undefined)\n\n      const seedItems = [\n        seedQueryLibraryItemFactory.build({\n          indexName: mockIndexName,\n          name: 'Same Name',\n        }),\n      ]\n\n      const result = await indexedDB.seed(mockDatabaseId, seedItems)\n\n      expect(result.success).toBe(true)\n      expect(mockedStorage.put).toHaveBeenCalledTimes(1)\n    })\n\n    it('should return empty array for empty seed list', async () => {\n      const result = await indexedDB.seed(mockDatabaseId, [])\n\n      expect(result).toEqual({ success: true, data: [] })\n      expect(mockedStorage.getAllByIndex).not.toHaveBeenCalled()\n    })\n\n    it('should propagate getList failure instead of bypassing deduplication', async () => {\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.getAllByIndex.mockRejectedValue(mockError)\n\n      const result = await indexedDB.seed(mockDatabaseId, [\n        seedQueryLibraryItemFactory.build({ indexName: mockIndexName }),\n      ])\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n      expect(mockedStorage.put).not.toHaveBeenCalled()\n    })\n\n    it('should return error on storage failure during put', async () => {\n      const mockError = new Error('IndexedDB error')\n      mockedStorage.getAllByIndex.mockResolvedValue([])\n      mockedStorage.put.mockRejectedValue(mockError)\n\n      const result = await indexedDB.seed(mockDatabaseId, [\n        seedQueryLibraryItemFactory.build({ indexName: mockIndexName }),\n      ])\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBe(mockError)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/database/QueryLibraryIndexedDB.ts",
    "content": "import { v4 as uuidv4 } from 'uuid'\nimport {\n  QueryLibraryItem,\n  QueryLibraryType,\n  CreateQueryLibraryItem,\n  UpdateQueryLibraryItem,\n  SeedQueryLibraryItem,\n  QueryLibraryFilter,\n} from '../types'\nimport {\n  QueryLibraryDatabase,\n  QueryLibraryResult,\n  QueryLibraryItemResult,\n} from './interface'\nimport { queryLibraryStorage } from './QueryLibraryStorage'\n\nexport class QueryLibraryIndexedDB implements QueryLibraryDatabase {\n  async getList(\n    databaseId: string,\n    filter: QueryLibraryFilter,\n  ): Promise<QueryLibraryResult> {\n    try {\n      const raw = await queryLibraryStorage.getAllByIndex(\n        databaseId,\n        filter.indexName,\n      )\n\n      let items = raw || []\n\n      if (filter.search) {\n        const term = filter.search.toLowerCase()\n        items = items.filter(\n          (item) =>\n            item.name?.toLowerCase().includes(term) ||\n            item.description?.toLowerCase().includes(term) ||\n            item.query?.toLowerCase().includes(term),\n        )\n      }\n\n      items.sort(\n        (a, b) =>\n          new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),\n      )\n\n      return { success: true, data: items }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async getOne(\n    databaseId: string,\n    id: string,\n  ): Promise<QueryLibraryItemResult> {\n    try {\n      const item = await queryLibraryStorage.getById(id)\n\n      if (!item || item.databaseId !== databaseId) {\n        return { success: false }\n      }\n\n      return { success: true, data: item }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async create(\n    databaseId: string,\n    data: CreateQueryLibraryItem,\n  ): Promise<QueryLibraryItemResult> {\n    try {\n      const now = new Date().toISOString()\n      const item: QueryLibraryItem = {\n        id: uuidv4(),\n        databaseId,\n        indexName: data.indexName,\n        type: QueryLibraryType.Saved,\n        name: data.name,\n        query: data.query,\n        createdAt: now,\n        updatedAt: now,\n      }\n\n      await queryLibraryStorage.put(item)\n\n      return { success: true, data: item }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async update(\n    databaseId: string,\n    id: string,\n    data: UpdateQueryLibraryItem,\n  ): Promise<QueryLibraryItemResult> {\n    try {\n      const getResult = await this.getOne(databaseId, id)\n\n      if (!getResult.success || !getResult.data) {\n        return { success: false, error: getResult.error }\n      }\n\n      const updated: QueryLibraryItem = {\n        ...getResult.data,\n        ...(data.name !== undefined && { name: data.name }),\n        ...(data.description !== undefined && {\n          description: data.description,\n        }),\n        ...(data.query !== undefined && { query: data.query }),\n        updatedAt: new Date().toISOString(),\n      }\n\n      await queryLibraryStorage.put(updated)\n\n      return { success: true, data: updated }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async delete(databaseId: string, id: string): Promise<QueryLibraryResult> {\n    try {\n      const getResult = await this.getOne(databaseId, id)\n\n      if (!getResult.success || !getResult.data) {\n        return { success: false, error: getResult.error }\n      }\n\n      await queryLibraryStorage.remove(id)\n      return { success: true }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async deleteByIndex(\n    databaseId: string,\n    indexName: string,\n  ): Promise<QueryLibraryResult> {\n    try {\n      const listResult = await this.getList(databaseId, { indexName })\n\n      if (!listResult.success || !listResult.data) {\n        return { success: false, error: listResult.error }\n      }\n\n      await Promise.all(\n        listResult.data.map((item) => queryLibraryStorage.remove(item.id)),\n      )\n\n      return { success: true }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async seed(\n    databaseId: string,\n    items: SeedQueryLibraryItem[],\n  ): Promise<QueryLibraryResult> {\n    try {\n      if (!items.length) {\n        return { success: true, data: [] }\n      }\n\n      const indexName = items[0].indexName\n      const listResult = await this.getList(databaseId, { indexName })\n\n      if (!listResult.success) {\n        return { success: false, error: listResult.error }\n      }\n\n      const existing = listResult.data || []\n\n      const existingSampleNames = new Set(\n        existing\n          .filter((item) => item.type === QueryLibraryType.Sample)\n          .map((item) => item.name),\n      )\n\n      const newItems = items.filter(\n        (item) => !existingSampleNames.has(item.name),\n      )\n\n      const now = new Date().toISOString()\n      const created: QueryLibraryItem[] = []\n\n      for (const seedItem of newItems) {\n        const item: QueryLibraryItem = {\n          id: uuidv4(),\n          databaseId,\n          indexName: seedItem.indexName,\n          type: QueryLibraryType.Sample,\n          name: seedItem.name,\n          description: seedItem.description,\n          query: seedItem.query,\n          createdAt: now,\n          updatedAt: now,\n        }\n\n        await queryLibraryStorage.put(item)\n        created.push(item)\n      }\n\n      return { success: true, data: [...existing, ...created] }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/database/QueryLibrarySQLite.spec.ts",
    "content": "import { http, HttpResponse } from 'msw'\nimport { faker } from '@faker-js/faker'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl } from 'uiSrc/utils'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { getMswURL } from 'uiSrc/utils/test-utils'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport {\n  queryLibraryItemFactory,\n  createQueryLibraryItemFactory,\n  seedQueryLibraryItemFactory,\n} from 'uiSrc/mocks/factories/query-library/queryLibraryItem.factory'\n\nimport { QueryLibrarySQLite } from './QueryLibrarySQLite'\n\ndescribe('QueryLibrarySQLite', () => {\n  let sqlite: QueryLibrarySQLite\n  const instanceId = INSTANCE_ID_MOCK\n  const mockIndexName = `idx:${faker.word.noun()}`\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n    sqlite = new QueryLibrarySQLite()\n  })\n\n  describe('getList', () => {\n    it('should return items on successful response', async () => {\n      const mockItems = queryLibraryItemFactory.buildList(3)\n\n      mswServer.use(\n        http.get(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY)),\n          async () => HttpResponse.json(mockItems, { status: 200 }),\n        ),\n      )\n\n      const result = await sqlite.getList(instanceId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result).toEqual({ success: true, data: mockItems })\n    })\n\n    it('should handle 400 error', async () => {\n      const statusCode = 400\n\n      mswServer.use(\n        http.get(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY)),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await sqlite.getList(instanceId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n      expect(result.error?.message).toBe(\n        `Request failed with status code ${statusCode}`,\n      )\n    })\n\n    it('should handle network errors', async () => {\n      mswServer.use(\n        http.get(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY)),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await sqlite.getList(instanceId, {\n        indexName: mockIndexName,\n      })\n\n      expect(result.success).toBe(false)\n      expect(result.error?.message).toBe('Network Error')\n    })\n  })\n\n  describe('getOne', () => {\n    it('should return item on successful response', async () => {\n      const mockItem = queryLibraryItemFactory.build()\n\n      mswServer.use(\n        http.get(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, mockItem.id),\n          ),\n          async () => HttpResponse.json(mockItem, { status: 200 }),\n        ),\n      )\n\n      const result = await sqlite.getOne(instanceId, mockItem.id)\n\n      expect(result).toEqual({ success: true, data: mockItem })\n    })\n\n    it('should handle 404 error', async () => {\n      const statusCode = 404\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.get(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await sqlite.getOne(instanceId, itemId)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n    })\n\n    it('should handle network errors', async () => {\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.get(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await sqlite.getOne(instanceId, itemId)\n\n      expect(result.success).toBe(false)\n      expect(result.error?.message).toBe('Network Error')\n    })\n  })\n\n  describe('create', () => {\n    it('should return created item on success', async () => {\n      const mockItem = queryLibraryItemFactory.build()\n\n      mswServer.use(\n        http.post(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY)),\n          async () => HttpResponse.json(mockItem, { status: 201 }),\n        ),\n      )\n\n      const result = await sqlite.create(instanceId, {\n        indexName: mockItem.indexName,\n        name: mockItem.name,\n        query: mockItem.query,\n      })\n\n      expect(result).toEqual({ success: true, data: mockItem })\n    })\n\n    it('should handle 400 error', async () => {\n      const statusCode = 400\n\n      mswServer.use(\n        http.post(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY)),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await sqlite.create(\n        instanceId,\n        createQueryLibraryItemFactory.build(),\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n    })\n\n    it('should handle network errors', async () => {\n      mswServer.use(\n        http.post(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY)),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await sqlite.create(\n        instanceId,\n        createQueryLibraryItemFactory.build(),\n      )\n\n      expect(result.success).toBe(false)\n      expect(result.error?.message).toBe('Network Error')\n    })\n  })\n\n  describe('update', () => {\n    it('should return updated item on success', async () => {\n      const mockItem = queryLibraryItemFactory.build()\n\n      mswServer.use(\n        http.patch(\n          getMswURL(\n            getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, mockItem.id),\n          ),\n          async () => HttpResponse.json(mockItem, { status: 200 }),\n        ),\n      )\n\n      const result = await sqlite.update(instanceId, mockItem.id, {\n        name: mockItem.name,\n      })\n\n      expect(result).toEqual({ success: true, data: mockItem })\n    })\n\n    it('should handle 400 error', async () => {\n      const statusCode = 400\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.patch(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await sqlite.update(instanceId, itemId, {\n        name: faker.lorem.words(2),\n      })\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n    })\n\n    it('should handle network errors', async () => {\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.patch(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await sqlite.update(instanceId, itemId, {\n        name: faker.lorem.words(2),\n      })\n\n      expect(result.success).toBe(false)\n      expect(result.error?.message).toBe('Network Error')\n    })\n  })\n\n  describe('delete', () => {\n    it('should return success on successful delete', async () => {\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.delete(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.text('', { status: 200 }),\n        ),\n      )\n\n      const result = await sqlite.delete(instanceId, itemId)\n\n      expect(result).toEqual({ success: true })\n    })\n\n    it('should handle 400 error', async () => {\n      const statusCode = 400\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.delete(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await sqlite.delete(instanceId, itemId)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n    })\n\n    it('should handle network errors', async () => {\n      const itemId = faker.string.uuid()\n\n      mswServer.use(\n        http.delete(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY, itemId)),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await sqlite.delete(instanceId, itemId)\n\n      expect(result.success).toBe(false)\n      expect(result.error?.message).toBe('Network Error')\n    })\n  })\n\n  describe('seed', () => {\n    const seedInput = seedQueryLibraryItemFactory.buildList(2)\n\n    it('should return seeded items on success', async () => {\n      const mockItems = queryLibraryItemFactory.buildList(2)\n\n      mswServer.use(\n        http.post(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY_SEED)),\n          async () => HttpResponse.json(mockItems, { status: 201 }),\n        ),\n      )\n\n      const result = await sqlite.seed(instanceId, seedInput)\n\n      expect(result).toEqual({ success: true, data: mockItems })\n    })\n\n    it('should handle 400 error', async () => {\n      const statusCode = 400\n\n      mswServer.use(\n        http.post(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY_SEED)),\n          async () => HttpResponse.text('', { status: statusCode }),\n        ),\n      )\n\n      const result = await sqlite.seed(instanceId, seedInput)\n\n      expect(result.success).toBe(false)\n      expect(result.error).toBeInstanceOf(Error)\n    })\n\n    it('should handle network errors', async () => {\n      mswServer.use(\n        http.post(\n          getMswURL(getUrl(instanceId, ApiEndpoints.QUERY_LIBRARY_SEED)),\n          async () => HttpResponse.error(),\n        ),\n      )\n\n      const result = await sqlite.seed(instanceId, seedInput)\n\n      expect(result.success).toBe(false)\n      expect(result.error?.message).toBe('Network Error')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/database/QueryLibrarySQLite.ts",
    "content": "import apiService from 'uiSrc/services/apiService'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport {\n  QueryLibraryItem,\n  CreateQueryLibraryItem,\n  UpdateQueryLibraryItem,\n  SeedQueryLibraryItem,\n  QueryLibraryFilter,\n} from '../types'\nimport {\n  QueryLibraryDatabase,\n  QueryLibraryResult,\n  QueryLibraryItemResult,\n} from './interface'\n\nexport class QueryLibrarySQLite implements QueryLibraryDatabase {\n  async getList(\n    databaseId: string,\n    filter: QueryLibraryFilter,\n  ): Promise<QueryLibraryResult> {\n    try {\n      const url = getUrl(databaseId, ApiEndpoints.QUERY_LIBRARY)\n      const { data, status } = await apiService.get<QueryLibraryItem[]>(url, {\n        params: filter,\n      })\n\n      if (isStatusSuccessful(status)) {\n        return { success: true, data }\n      }\n\n      return { success: false }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async getOne(\n    databaseId: string,\n    id: string,\n  ): Promise<QueryLibraryItemResult> {\n    try {\n      const url = getUrl(databaseId, ApiEndpoints.QUERY_LIBRARY, id)\n      const { data, status } = await apiService.get<QueryLibraryItem>(url)\n\n      if (isStatusSuccessful(status)) {\n        return { success: true, data }\n      }\n\n      return { success: false }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async create(\n    databaseId: string,\n    item: CreateQueryLibraryItem,\n  ): Promise<QueryLibraryItemResult> {\n    try {\n      const url = getUrl(databaseId, ApiEndpoints.QUERY_LIBRARY)\n      const { data, status } = await apiService.post<QueryLibraryItem>(\n        url,\n        item,\n      )\n\n      if (isStatusSuccessful(status)) {\n        return { success: true, data }\n      }\n\n      return { success: false }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async update(\n    databaseId: string,\n    id: string,\n    item: UpdateQueryLibraryItem,\n  ): Promise<QueryLibraryItemResult> {\n    try {\n      const url = getUrl(databaseId, ApiEndpoints.QUERY_LIBRARY, id)\n      const { data, status } = await apiService.patch<QueryLibraryItem>(\n        url,\n        item,\n      )\n\n      if (isStatusSuccessful(status)) {\n        return { success: true, data }\n      }\n\n      return { success: false }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  async delete(databaseId: string, id: string): Promise<QueryLibraryResult> {\n    try {\n      const url = getUrl(databaseId, ApiEndpoints.QUERY_LIBRARY, id)\n      const { status } = await apiService.delete(url)\n\n      if (isStatusSuccessful(status)) {\n        return { success: true }\n      }\n\n      return { success: false }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n\n  // No-op: for SQLite, query library cleanup is handled automatically by the backend\n  // when an index is deleted (see redisearch.service.ts → deleteIndex).\n  async deleteByIndex(\n    _databaseId: string,\n    _indexName: string,\n  ): Promise<QueryLibraryResult> {\n    return { success: true }\n  }\n\n  async seed(\n    databaseId: string,\n    items: SeedQueryLibraryItem[],\n  ): Promise<QueryLibraryResult> {\n    try {\n      const url = getUrl(databaseId, ApiEndpoints.QUERY_LIBRARY_SEED)\n      const { data, status } = await apiService.post<QueryLibraryItem[]>(url, {\n        items,\n      })\n\n      if (isStatusSuccessful(status)) {\n        return { success: true, data }\n      }\n\n      return { success: false }\n    } catch (exception) {\n      return { success: false, error: exception as Error }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/database/QueryLibraryStorage.ts",
    "content": "import { getConfig } from 'uiSrc/config'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { QueryLibraryItem } from '../types'\n\nconst riConfig = getConfig()\nconst DB_NAME = riConfig.app.queryLibraryIndexedDbName\nconst STORE_NAME = BrowserStorageItem.queryLibrary\n\nexport class QueryLibraryStorage {\n  private db?: IDBDatabase\n\n  private initPromise?: Promise<IDBDatabase>\n\n  private initDb(): Promise<IDBDatabase> {\n    return new Promise((resolve, reject) => {\n      if (!window.indexedDB) {\n        reject(new Error('indexedDB is not supported'))\n        return\n      }\n\n      const request = window.indexedDB.open(DB_NAME, 1)\n\n      request.onerror = (event) => {\n        event.preventDefault()\n        reject(request.error)\n      }\n\n      request.onsuccess = () => {\n        this.db = request.result\n        this.db.onversionchange = (e) => {\n          ;(e.target as IDBDatabase)?.close()\n          this.db = undefined\n          this.initPromise = undefined\n        }\n        resolve(this.db)\n      }\n\n      request.onupgradeneeded = () => {\n        const db = request.result\n\n        db.onerror = (event) => {\n          event.preventDefault()\n          reject(request.error)\n        }\n\n        const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })\n        store.createIndex('databaseId_indexName', ['databaseId', 'indexName'], {\n          unique: false,\n        })\n      }\n    })\n  }\n\n  private async getDb(): Promise<IDBDatabase> {\n    if (!this.db) {\n      this.initPromise ??= this.initDb().catch((err) => {\n        this.initPromise = undefined\n        throw err\n      })\n      await this.initPromise\n    }\n    return this.db!\n  }\n\n  async getAllByIndex(\n    databaseId: string,\n    indexName: string,\n  ): Promise<QueryLibraryItem[]> {\n    const db = await this.getDb()\n    return new Promise((resolve, reject) => {\n      const tx = db.transaction(STORE_NAME, 'readonly')\n      const index = tx.objectStore(STORE_NAME).index('databaseId_indexName')\n      const req = index.getAll([databaseId, indexName])\n\n      req.onsuccess = () => resolve(req.result || [])\n      req.onerror = () => reject(req.error)\n    })\n  }\n\n  async getById(id: string): Promise<QueryLibraryItem | undefined> {\n    const db = await this.getDb()\n    return new Promise((resolve, reject) => {\n      const tx = db.transaction(STORE_NAME, 'readonly')\n      const req = tx.objectStore(STORE_NAME).get(id)\n\n      req.onsuccess = () => resolve(req.result)\n      req.onerror = () => reject(req.error)\n    })\n  }\n\n  async put(item: QueryLibraryItem): Promise<void> {\n    const db = await this.getDb()\n    return new Promise((resolve, reject) => {\n      const tx = db.transaction(STORE_NAME, 'readwrite')\n      tx.objectStore(STORE_NAME).put(item)\n\n      tx.oncomplete = () => resolve()\n      tx.onerror = () => reject(tx.error)\n    })\n  }\n\n  async remove(id: string): Promise<void> {\n    const db = await this.getDb()\n    return new Promise((resolve, reject) => {\n      const tx = db.transaction(STORE_NAME, 'readwrite')\n      tx.objectStore(STORE_NAME).delete(id)\n\n      tx.oncomplete = () => resolve()\n      tx.onerror = () => reject(tx.error)\n    })\n  }\n}\n\nexport const queryLibraryStorage = new QueryLibraryStorage()\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/database/interface.ts",
    "content": "import {\n  QueryLibraryItem,\n  CreateQueryLibraryItem,\n  UpdateQueryLibraryItem,\n  SeedQueryLibraryItem,\n  QueryLibraryFilter,\n} from '../types'\n\nexport interface QueryLibraryResult {\n  success: boolean\n  data?: QueryLibraryItem[]\n  error?: Error\n}\n\nexport interface QueryLibraryItemResult {\n  success: boolean\n  data?: QueryLibraryItem\n  error?: Error\n}\n\nexport interface QueryLibraryDatabase {\n  getList(\n    databaseId: string,\n    filter: QueryLibraryFilter,\n  ): Promise<QueryLibraryResult>\n\n  getOne(databaseId: string, id: string): Promise<QueryLibraryItemResult>\n\n  create(\n    databaseId: string,\n    data: CreateQueryLibraryItem,\n  ): Promise<QueryLibraryItemResult>\n\n  update(\n    databaseId: string,\n    id: string,\n    data: UpdateQueryLibraryItem,\n  ): Promise<QueryLibraryItemResult>\n\n  delete(databaseId: string, id: string): Promise<QueryLibraryResult>\n\n  seed(\n    databaseId: string,\n    items: SeedQueryLibraryItem[],\n  ): Promise<QueryLibraryResult>\n\n  deleteByIndex(\n    databaseId: string,\n    indexName: string,\n  ): Promise<QueryLibraryResult>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/index.ts",
    "content": "export { QueryLibraryService } from './QueryLibraryService'\nexport * from './types'\n"
  },
  {
    "path": "redisinsight/ui/src/services/query-library/types.ts",
    "content": "export enum QueryLibraryType {\n  Sample = 'SAMPLE',\n  Saved = 'SAVED',\n}\n\nexport interface QueryLibraryItem {\n  id: string\n  databaseId: string\n  indexName: string\n  type: QueryLibraryType\n  name: string\n  description?: string\n  query: string\n  createdAt: string\n  updatedAt: string\n}\n\nexport type CreateQueryLibraryItem = Pick<\n  QueryLibraryItem,\n  'indexName' | 'name' | 'query'\n>\n\nexport type UpdateQueryLibraryItem = Partial<\n  Pick<QueryLibraryItem, 'name' | 'description' | 'query'>\n>\n\nexport type SeedQueryLibraryItem = Pick<\n  QueryLibraryItem,\n  'indexName' | 'name' | 'query'\n> & { description?: string }\n\nexport interface QueryLibraryFilter {\n  indexName: string\n  search?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/resourcesService.ts",
    "content": "import axios from 'axios'\nimport { CustomHeaders } from 'uiSrc/constants/api'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\nimport { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\n\nconst { apiPort } = window.app?.config || { apiPort: riConfig.api.port }\nconst isDevelopment = riConfig.app.env === 'development'\nconst isWebApp = riConfig.app.type === 'web'\nconst hostedApiBaseUrl = riConfig.api.hostedBaseUrl\n\nlet BASE_URL =\n  !isDevelopment && isWebApp ? '/' : `${riConfig.api.baseUrl}:${apiPort}/`\n\nif (window.__RI_PROXY_PATH__) {\n  BASE_URL = `${BASE_URL}${window.__RI_PROXY_PATH__}/`\n}\n\nexport const RESOURCES_BASE_URL = BASE_URL\n\nconst resourcesService = axios.create({\n  baseURL: hostedApiBaseUrl || RESOURCES_BASE_URL,\n  withCredentials: !!hostedApiBaseUrl,\n})\n\nexport const setResourceCsrfHeader = (token: string) => {\n  resourcesService.defaults.headers.common[CustomHeaders.CsrfToken] = token\n}\n\n// TODO: it seems it's shoudn't be location.origin\n// TODO: check all cases and rename this to getResourcesUrl\n// TODO: also might be helpful create function which returns origin url\nexport const getOriginUrl = () =>\n  IS_ABSOLUTE_PATH.test(RESOURCES_BASE_URL)\n    ? RESOURCES_BASE_URL\n    : window?.location?.origin || RESOURCES_BASE_URL\n\nexport const getPathToResource = (url: string = ''): string => {\n  try {\n    return IS_ABSOLUTE_PATH.test(url)\n      ? url\n      : new URL(url, getOriginUrl()).toString()\n  } catch {\n    return ''\n  }\n}\n\nexport const checkResourse = async (url: string = '') =>\n  resourcesService.head(url)\n\nconst localResourcesService = axios.create({\n  baseURL: riConfig.app.localResourcesBaseUrl,\n  withCredentials: false,\n})\n\nexport default riConfig.app.useLocalResources\n  ? localResourcesService\n  : resourcesService\n"
  },
  {
    "path": "redisinsight/ui/src/services/routing.ts",
    "content": "import { createLocation } from 'history'\n\nexport const isModifiedEvent = (event: any) =>\n  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)\n\nconst isLeftClickEvent = (event: any) => event.button === 0\n\nlet router: any\nexport const registerRouter = (reactRouter: any): any => {\n  router = reactRouter\n}\n\nexport const getRouterLinkProps = (to: any, customOnClick?: () => void) => {\n  const location =\n    typeof to === 'string'\n      ? createLocation(to, null, '', router?.history?.location)\n      : to\n\n  const href = router?.history?.createHref(location)\n\n  const onClick = (event: any) => {\n    customOnClick?.()\n\n    if (event.defaultPrevented) {\n      return\n    }\n\n    // If target prop is set (e.g. to \"_blank\"), let browser handle link.\n    if (event?.target?.getAttribute('target')) {\n      return\n    }\n\n    if (isModifiedEvent(event) || !isLeftClickEvent(event)) {\n      return\n    }\n\n    // Prevent regular link behavior, which causes a browser refresh.\n    event.preventDefault()\n    router?.history?.push(location)\n  }\n\n  return { href, onClick }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/storage.ts",
    "content": "import { isObjectLike } from 'lodash'\nimport { Maybe } from 'uiSrc/utils'\nimport { updateDbSettings } from 'uiSrc/services/databaseSettingsService'\nimport BrowserStorageItem from '../constants/storage'\n\nclass StorageService {\n  private storage: Storage\n\n  private envKey: Maybe<string>\n\n  constructor(storage: Storage, envKey?: string) {\n    this.storage = storage\n    this.envKey = envKey\n  }\n\n  private getKey(itemName: string): string {\n    return this.envKey ? `${this.envKey}_${itemName}` : itemName\n  }\n\n  get(itemName: string = '') {\n    const key = this.getKey(itemName)\n    let item\n    try {\n      item = this.storage.getItem(key)\n    } catch (error) {\n      console.error(`getItem from storage error: ${error}`)\n    }\n\n    if (item) {\n      try {\n        return JSON.parse(item)\n      } catch (e) {\n        return item\n      }\n    }\n    return null\n  }\n\n  set(itemName: string = '', item: any) {\n    try {\n      const key = this.getKey(itemName)\n      if (isObjectLike(item)) {\n        this.storage.setItem(key, JSON.stringify(item))\n      } else {\n        this.storage.setItem(key, item)\n      }\n    } catch (error) {\n      console.error(`setItem to storage error: ${error}`)\n    }\n  }\n\n  remove(itemName: string = '') {\n    const key = this.getKey(itemName)\n    this.storage.removeItem(key)\n  }\n\n  getAll() {\n    return this.storage\n  }\n}\nconst envKey = window.__RI_PROXY_PATH__\n\nexport const localStorageService = new StorageService(localStorage, envKey)\nexport const sessionStorageService = new StorageService(sessionStorage, envKey)\n\nexport const getObjectStorageField = (itemName = '', field = '') => {\n  try {\n    return localStorageService?.get(itemName)?.[field]\n  } catch (e) {\n    return null\n  }\n}\nexport const getObjectStorage = (itemName = '') => {\n  try {\n    return localStorageService?.get(itemName)\n  } catch (e) {\n    return null\n  }\n}\n\nexport const setObjectStorageField = (\n  itemName = '',\n  field = '',\n  value?: any,\n) => {\n  try {\n    const config = localStorageService?.get(itemName) || {}\n\n    if (value === undefined) {\n      delete config[field]\n      localStorageService?.set(itemName, config)\n      return\n    }\n\n    localStorageService?.set(itemName, {\n      ...config,\n      [field]: value,\n    })\n  } catch (e) {\n    console.error(e)\n  }\n}\n\nexport const setObjectStorage = (itemName = '', obj?: Record<string, any>) => {\n  try {\n    const config = localStorageService?.get(itemName) || {}\n\n    if (obj === undefined) {\n      localStorageService?.remove(itemName)\n      return\n    }\n\n    localStorageService?.set(itemName, {\n      ...config,\n      ...obj,\n    })\n  } catch (e) {\n    console.error(e)\n  }\n}\n\nexport const getDBConfigStorageField = (\n  instanceId: string,\n  field: string = '',\n) => getObjectStorageField(BrowserStorageItem.dbConfig + instanceId, field)\n\nexport const setDBConfigStorageField = (\n  instanceId: string,\n  field: string = '',\n  value?: any,\n) => {\n  const itemName = BrowserStorageItem.dbConfig + instanceId\n  setObjectStorageField(itemName, field, value)\n  // on each update of config value, update db settings via the API\n  const config = getObjectStorage(itemName)\n  if (config) {\n    updateDbSettings(instanceId, config)\n  }\n}\n\nexport const getCapabilityStorageField = (field: string = '') =>\n  getObjectStorageField(BrowserStorageItem.capability, field)\n\nexport const setCapabilityStorageField = (field: string = '', value?: any) => {\n  setObjectStorageField(BrowserStorageItem.capability, field, value)\n}\n\nexport default StorageService\n"
  },
  {
    "path": "redisinsight/ui/src/services/tests/PluguinApi.spec.ts",
    "content": "import { pluginApi } from 'uiSrc/services/PluginAPI'\n\ndescribe('PluginApi', () => {\n  it('should subscribe on event and receive data after emit', () => {\n    const mockCallback = jest.fn()\n    const data = { data: 'some data' }\n\n    pluginApi.onEvent('id1', 'someEvent', mockCallback)\n    pluginApi.sendEvent('id1', 'someEvent', data)\n\n    expect(mockCallback).toBeCalledWith(data)\n  })\n\n  it('should subscribe on event and not receive data after unregister all subscriptions and emit', () => {\n    const mockCallback = jest.fn()\n    const data = { data: 'some data' }\n\n    pluginApi.onEvent('id1', 'someEvent', mockCallback)\n    pluginApi.unregisterSubscriptions()\n    pluginApi.sendEvent('id1', 'someEvent', data)\n\n    expect(mockCallback).not.toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/tests/apiService.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { sessionStorageService } from 'uiSrc/services'\nimport {\n  cloudAuthInterceptor,\n  connectivityErrorsInterceptor,\n  isConnectivityError,\n  requestInterceptor,\n} from 'uiSrc/services/apiService'\nimport { ApiEndpoints, CustomErrorCodes } from 'uiSrc/constants'\nimport { cleanup, mockedStore } from 'uiSrc/utils/test-utils'\nimport { logoutUser } from 'uiSrc/slices/oauth/cloud'\nimport { store } from 'uiSrc/slices/store'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport ApiErrors from 'uiSrc/constants/apiErrors'\nimport { INSTANCES_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\n\ndescribe('requestInterceptor', () => {\n  it('should properly set db-index to headers', () => {\n    sessionStorageService.get = jest.fn().mockReturnValue(5)\n\n    const config: any = {\n      headers: {},\n      url: 'http://localhost:8080/databases/123-215gg-23/endpoint',\n    }\n\n    requestInterceptor(config)\n    expect(config?.headers?.['ri-db-index']).toEqual(5)\n  })\n\n  it('should not set db-index to headers with url not related to database', () => {\n    sessionStorageService.get = jest.fn().mockReturnValue(5)\n\n    const config: any = {\n      headers: {},\n      url: 'http://localhost:8080/settings/123-215gg-23/endpoint',\n    }\n\n    requestInterceptor(config)\n    expect(config?.headers?.['ri-db-index']).toEqual(undefined)\n  })\n})\n\ndescribe('connectivityErrorsInterceptor', () => {\n  let mockedTestStore: typeof mockedStore\n  beforeEach(() => {\n    cleanup()\n    mockedTestStore = cloneDeep(mockedStore)\n    mockedTestStore.clearActions()\n  })\n\n  it('should properly handle non-connectivity error', async () => {\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: {\n        status: 500,\n        data: {\n          error: 'Internal server error',\n        },\n      },\n    }\n\n    try {\n      await connectivityErrorsInterceptor(response)\n    } catch {\n      expect(mockedTestStore.getActions()).toEqual([])\n    }\n  })\n\n  it('should properly handle 424 error and store default error message', async () => {\n    // Set up connected instance for interceptor to work\n    mockedTestStore.getState().connections.instances.connectedInstance = {\n      ...INSTANCES_MOCK[0],\n      id: 'test-instance-id',\n    }\n\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: {\n        status: 424,\n        data: {\n          error: 'RedisConnectionFailedException',\n        },\n        request: {\n          responseURL:\n            'http://localhost:5001/databases/test-instance-id/overview',\n        },\n      },\n    }\n\n    try {\n      await connectivityErrorsInterceptor(response)\n    } catch {\n      expect(mockedTestStore.getActions()).toEqual([\n        setConnectivityError(ApiErrors.ConnectionLost),\n      ])\n    }\n  })\n\n  it('should properly handle specific 424 error and store custom error message', async () => {\n    // Set up connected instance for interceptor to work\n    mockedTestStore.getState().connections.instances.connectedInstance = {\n      ...INSTANCES_MOCK[0],\n      id: 'test-instance-id',\n    }\n\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: {\n        status: 424,\n        data: {\n          message: 'custom message',\n          error: 'RedisConnectionFailedException',\n          errorCode: CustomErrorCodes.RedisConnectionDefaultUserDisabled,\n        },\n        request: {\n          responseURL:\n            'http://localhost:5001/databases/test-instance-id/overview',\n        },\n      },\n    }\n\n    try {\n      await connectivityErrorsInterceptor(response)\n    } catch {\n      expect(mockedTestStore.getActions()).toEqual([\n        setConnectivityError('custom message'),\n      ])\n    }\n  })\n\n  it('should properly handle specific 424 error and store default error message when no message available', async () => {\n    // Set up connected instance for interceptor to work\n    mockedTestStore.getState().connections.instances.connectedInstance = {\n      ...INSTANCES_MOCK[0],\n      id: 'test-instance-id',\n    }\n\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: {\n        status: 424,\n        data: {\n          error: 'RedisConnectionFailedException',\n          errorCode: CustomErrorCodes.RedisConnectionDefaultUserDisabled,\n        },\n        request: {\n          responseURL:\n            'http://localhost:5001/databases/test-instance-id/overview',\n        },\n      },\n    }\n\n    try {\n      await connectivityErrorsInterceptor(response)\n    } catch {\n      expect(mockedTestStore.getActions()).toEqual([\n        setConnectivityError(ApiErrors.ConnectionLost),\n      ])\n    }\n  })\n\n  it('should not dispatch connectivity error when instance ID does not match', async () => {\n    // Set up connected instance with different ID than the response URL\n    mockedTestStore.getState().connections.instances.connectedInstance = {\n      ...INSTANCES_MOCK[0],\n      id: 'different-instance-id',\n    }\n\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: {\n        status: 424,\n        data: {\n          error: 'RedisConnectionFailedException',\n        },\n        request: {\n          responseURL:\n            'http://localhost:5001/databases/test-instance-id/overview', // Different ID\n        },\n      },\n    }\n\n    try {\n      await connectivityErrorsInterceptor(response)\n    } catch {\n      // Should not dispatch any connectivity error actions\n      expect(mockedTestStore.getActions()).toEqual([])\n    }\n  })\n})\n\ndescribe('cloudAuthInterceptor', () => {\n  let mockedTestStore: typeof mockedStore\n  beforeEach(() => {\n    cleanup()\n    mockedTestStore = cloneDeep(mockedStore)\n    mockedTestStore.clearActions()\n  })\n\n  it('should properly handle 401 error, call logogut', async () => {\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: { status: 401 },\n      config: { url: ApiEndpoints.CLOUD_CAPI_KEYS },\n    }\n\n    try {\n      await cloudAuthInterceptor(response)\n    } catch {\n      expect(mockedTestStore.getActions()).toEqual([logoutUser(), setSSOFlow()])\n    }\n  })\n\n  it('should properly handle 401 error, do not call logout', async () => {\n    jest\n      .spyOn(store, 'dispatch')\n      .mockImplementation(mockedTestStore.dispatch as any)\n    jest.spyOn(store, 'getState').mockImplementation(mockedTestStore.getState)\n\n    const response: any = {\n      response: { status: 401 },\n      config: { url: ApiEndpoints.BULK_ACTIONS_IMPORT },\n    }\n\n    try {\n      await cloudAuthInterceptor(response)\n    } catch {\n      expect(mockedTestStore.getActions()).toEqual([])\n    }\n  })\n})\n\ndescribe('isConnectivityError', () => {\n  it.each<{ apiResponse: any; result: boolean }>([\n    {\n      apiResponse: undefined,\n      result: false,\n    },\n    {\n      apiResponse: {\n        status: 424,\n        data: {\n          error: 'RedisConnectionFailedException',\n        },\n      },\n      result: true,\n    },\n    {\n      apiResponse: {\n        status: 500,\n        data: {\n          error: 'RedisConnectionFailedException',\n        },\n      },\n      result: false,\n    },\n    {\n      apiResponse: {\n        status: 503,\n        data: {\n          error: 'Service Unavailable',\n        },\n      },\n      result: true,\n    },\n    {\n      apiResponse: {\n        status: 401,\n        data: {\n          error: 'Service Unavailable',\n        },\n      },\n      result: false,\n    },\n    {\n      apiResponse: {\n        status: 503,\n        data: {\n          code: 'serviceUnavailable',\n        },\n      },\n      result: true,\n    },\n    {\n      apiResponse: {\n        status: 400,\n        data: {\n          code: 'serviceUnavailable',\n        },\n      },\n      result: false,\n    },\n  ])('test %j', ({ apiResponse, result }) => {\n    expect(isConnectivityError(apiResponse?.status, apiResponse?.data)).toEqual(\n      result,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/tests/formatter/HtmlToJsxString.spec.tsx",
    "content": "import React from 'react'\nimport HtmlToJsxString from '../../formatter/HtmlToJsxString'\n\ndescribe('HtmlToJsxString', () => {\n  it('should return proper string', async () => {\n    const div = <div />\n    const formatter = new HtmlToJsxString()\n    const result = await formatter.format(div)\n    expect(result).toEqual(String(div))\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/tests/formatter/MarkdownToJsxString.spec.ts",
    "content": "import { unified } from 'unified'\nimport MarkdownToJsxString from '../../formatter/MarkdownToJsxString'\n\njest.mock('unified')\ndescribe('MarkdownToJsxString', () => {\n  it('should call process', async () => {\n    // mock implementation\n    const useProcessMock = jest.fn().mockImplementation(() => Promise.resolve())\n    function useMock() {\n      return { use: useMock, process: useProcessMock }\n    }\n    ;(unified as jest.Mock).mockImplementation(() => ({\n      use: useMock,\n    }))\n\n    await new MarkdownToJsxString().format('')\n    expect(useProcessMock).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/tests/resourcesService.spec.ts",
    "content": "/*\n  eslint-disable global-require\n*/\n\nimport { cloneDeep } from 'lodash'\n\nObject.defineProperty(window, 'location', {\n  value: {\n    origin: 'http://localhost',\n  },\n  writable: true,\n})\n\nconst OLD_ENV = cloneDeep(riConfig)\n\nbeforeEach(() => {\n  jest.resetModules()\n  riConfig = cloneDeep(OLD_ENV)\n})\nafterAll(() => {\n  riConfig = cloneDeep(OLD_ENV)\n})\n\ndescribe('getOriginUrl', () => {\n  test('shoud return url with absolute path', () => {\n    const { getOriginUrl } = require('../resourcesService')\n\n    expect(getOriginUrl()).toEqual('http://localhost:5001/')\n  })\n\n  test('shoud return origin with not absolute path', () => {\n    riConfig.app.type = 'web'\n    riConfig.app.env = 'production'\n\n    const { getOriginUrl } = require('../resourcesService')\n    expect(getOriginUrl()).toEqual('http://localhost')\n  })\n})\n\ndescribe('getPathToResource', () => {\n  test('shoud return url with absolute path', () => {\n    const { getPathToResource } = require('../resourcesService')\n\n    expect(getPathToResource('data/file.txt')).toEqual(\n      'http://localhost:5001/data/file.txt',\n    )\n  })\n\n  test('shoud return origin with not absolute path', () => {\n    riConfig.app.type = 'web'\n    riConfig.app.env = 'production'\n\n    const { getPathToResource } = require('../resourcesService')\n    expect(getPathToResource('data/file.txt')).toEqual(\n      'http://localhost/data/file.txt',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/tests/routing.spec.tsx",
    "content": "import React from 'react'\nimport { Link } from 'uiSrc/components/base/link/Link'\nimport { Pages } from 'uiSrc/constants'\nimport { getRouterLinkProps } from 'uiSrc/services'\nimport { render, fireEvent, screen } from 'uiSrc/utils/test-utils'\n\ndescribe('getRouterLinkProps', () => {\n  it('should call click callback', () => {\n    const mockOnClick = jest.fn()\n\n    render(\n      <Link\n        {...getRouterLinkProps(Pages.browser, mockOnClick)}\n        data-testid=\"link\"\n      >\n        Text\n      </Link>,\n    )\n    fireEvent.click(screen.getByTestId('link'))\n\n    expect(mockOnClick).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/services/theme.ts",
    "content": "import {\n  BrowserStorageItem,\n  Theme,\n  THEME_MATCH_MEDIA_DARK,\n} from 'uiSrc/constants'\nimport { localStorageService } from './storage'\n\ninterface Themes {\n  [theme: string]: string\n}\n\nclass ThemeService {\n  readonly themes: Themes = {}\n\n  registerTheme(theme: Theme, cssFiles: any) {\n    this.themes[theme] = cssFiles\n  }\n\n  applyTheme(newTheme: Theme) {\n    let actualTheme = newTheme\n\n    if (newTheme === Theme.System) {\n      actualTheme = window.matchMedia?.(THEME_MATCH_MEDIA_DARK)?.matches\n        ? Theme.Dark\n        : Theme.Light\n    }\n\n    const sheet = new CSSStyleSheet()\n    sheet?.replaceSync(this.themes[actualTheme])\n\n    document.adoptedStyleSheets = [sheet]\n\n    localStorageService.set(BrowserStorageItem.theme, newTheme)\n    document.body.classList.value = `theme_${actualTheme}`\n  }\n\n  static getTheme() {\n    return localStorageService.get(BrowserStorageItem.theme)\n  }\n}\nexport const themeService = new ThemeService()\nexport default ThemeService\n"
  },
  {
    "path": "redisinsight/ui/src/services/utils/index.ts",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/services/vectorSearchHistoryStorage.ts",
    "content": "import { getConfig } from 'uiSrc/config'\nimport { WorkbenchStorage } from './workbenchStorage'\n\nconst riConfig = getConfig()\n\nexport const vectorSearchCommandsHistoryStorage = new WorkbenchStorage(\n  riConfig.app.vectorSearchIndexedDbName,\n  1,\n)\n"
  },
  {
    "path": "redisinsight/ui/src/services/workbenchStorage.ts",
    "content": "import { flatten } from 'lodash'\nimport { CommandExecution } from 'uiSrc/slices/interfaces'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { getConfig } from 'uiSrc/config'\nimport { formatBytes } from 'uiSrc/utils'\nimport { WORKBENCH_HISTORY_MAX_LENGTH } from 'uiSrc/pages/workbench/constants'\n\nconst riConfig = getConfig()\n\nexport class WorkbenchStorage {\n  private db?: IDBDatabase\n\n  constructor(\n    private readonly dbName: string,\n    private readonly version = 1,\n  ) {}\n\n  private initDb(storeName: string) {\n    return new Promise((resolve, reject) => {\n      if (!window.indexedDB) {\n        reject(new Error('indexedDB is not supported'))\n        return\n      }\n      // Let us open our database\n      const DBOpenRequest = window.indexedDB.open(this.dbName, this.version)\n\n      DBOpenRequest.onerror = (event) => {\n        event.preventDefault()\n        reject(DBOpenRequest.error)\n        console.error('indexedDB open error')\n      }\n\n      DBOpenRequest.onsuccess = () => {\n        this.db = DBOpenRequest.result\n        this.db.onversionchange = (e) => {\n          // Triggered when the database is modified (e.g. adding an objectStore) or\n          // deleted (even when initiated by other sessions in different tabs).\n          // Closing the connection here prevents those operations from being blocked.\n          // If the database is accessed again later by this instance, the connection\n          // will be reopened or the database recreated as needed.\n          ;(e.target as IDBDatabase)?.close()\n        }\n        resolve(this.db)\n      }\n\n      // This event handles the event whereby a new version of the database needs to be created\n      // Either one has not been created before, or a new version number has been submitted via the\n      // window.indexedDB.open line above\n      // it is only implemented in recent browsers\n      DBOpenRequest.onupgradeneeded = (event) => {\n        this.db = DBOpenRequest.result\n\n        this.db.onerror = (event) => {\n          event.preventDefault()\n          reject(DBOpenRequest.error)\n        }\n\n        try {\n          if (\n            event.newVersion &&\n            event.newVersion > event.oldVersion &&\n            event.oldVersion > 0 &&\n            this.db.objectStoreNames.contains(storeName)\n          ) {\n            // if there is need to update\n            this.db.deleteObjectStore(storeName)\n          }\n          // Create an objectStore for this database\n          const objectStore = this.db.createObjectStore(storeName, {\n            keyPath: ['id', 'databaseId'],\n          })\n          objectStore.createIndex('dbId', 'databaseId', { unique: false })\n          objectStore.createIndex('commandId', 'id', { unique: true })\n        } catch (ex) {\n          if (ex instanceof DOMException && ex?.name === 'ConstraintError') {\n            console.warn(\n              `The database \"${this.dbName}\" has been upgraded from version ${event.oldVersion} to version \n              ${event.newVersion}, but the storage \"${storeName}\" already exists.`,\n            )\n          } else {\n            throw ex\n          }\n        }\n      }\n    })\n  }\n\n  async getDb(storeName: string) {\n    if (!this.db) {\n      await this.initDb(storeName)\n    }\n    return this.db\n  }\n\n  getItem(\n    storeName: string,\n    commandId: string,\n    onSuccess?: () => void,\n    onError?: () => void,\n  ) {\n    return new Promise((resolve, reject) => {\n      try {\n        this.getDb(storeName).then((db) => {\n          if (db === undefined) {\n            reject(new Error('Failed to retrieve item from IndexedDB'))\n            return\n          }\n          const objectStore = db\n            .transaction(storeName, 'readonly')\n            ?.objectStore(storeName)\n          const idbIndex = objectStore?.index('commandId')\n          const indexReq = idbIndex?.get(commandId)\n          indexReq.onsuccess = () => {\n            const value = indexReq.result\n            onSuccess?.()\n            resolve(value)\n          }\n          indexReq.onerror = () => {\n            onError?.()\n            reject(indexReq.error)\n          }\n        })\n      } catch (e) {\n        onError?.()\n        reject(e)\n      }\n    })\n  }\n\n  getItems(\n    storeName: string,\n    dbId: string,\n    onSuccess?: () => void,\n    onError?: () => void,\n  ) {\n    return new Promise((resolve, reject) => {\n      try {\n        this.getDb(storeName).then((db) => {\n          if (db === undefined) {\n            reject(new Error('Failed to retrieve item from IndexedDB'))\n            return\n          }\n          const objectStore = db\n            .transaction(storeName, 'readonly')\n            ?.objectStore(storeName)\n          const idbIndex = objectStore?.index('dbId')\n          const indexReq = idbIndex?.getAll(dbId)\n          indexReq.onsuccess = () => {\n            const values = indexReq.result\n            onSuccess?.()\n            if (values && values.length > 0) {\n              resolve(values)\n            } else {\n              resolve([])\n            }\n          }\n          indexReq.onerror = () => {\n            onError?.()\n            reject(indexReq.error)\n          }\n        })\n      } catch (e) {\n        onError?.()\n        reject(e)\n      }\n    })\n  }\n\n  setItem(\n    storeName: string,\n    value: any,\n    onSuccess?: () => void,\n    onError?: () => void,\n  ): Promise<void> {\n    return new Promise((resolve, reject) => {\n      try {\n        this.getDb(storeName).then((db) => {\n          if (db === undefined) {\n            reject(new Error('Failed to set item in IndexedDB'))\n            return\n          }\n          const transaction = db.transaction(storeName, 'readwrite')\n          const req = transaction?.objectStore(storeName)?.put(value)\n          transaction.oncomplete = () => {\n            onSuccess?.()\n            resolve()\n          }\n          transaction.onerror = () => {\n            onError?.()\n            reject(req?.error)\n          }\n        })\n      } catch (e) {\n        reject(e)\n      }\n    })\n  }\n\n  removeItem(\n    storeName: string,\n    dbId: string,\n    commandId: string,\n    onSuccess?: () => void,\n    onError?: () => void,\n  ): Promise<string | void> {\n    return new Promise((resolve, reject) => {\n      try {\n        this.getDb(storeName).then((db) => {\n          if (db === undefined) {\n            reject(new Error('Failed to remove item from IndexedDB'))\n            return\n          }\n          const transaction = db.transaction(storeName, 'readwrite')\n          const req = transaction\n            .objectStore(storeName)\n            ?.delete([commandId, dbId])\n\n          transaction.oncomplete = () => {\n            onSuccess?.()\n            resolve(commandId)\n          }\n\n          transaction.onerror = () => {\n            onError?.()\n            reject(req?.error)\n          }\n        })\n      } catch (e) {\n        onError?.()\n        reject(e)\n      }\n    })\n  }\n\n  clear(\n    storeName: string,\n    dbId: string,\n    onSuccess?: () => void,\n    onError?: () => void,\n  ): Promise<void> {\n    return new Promise((resolve, reject) => {\n      try {\n        this.getDb(storeName).then((db) => {\n          if (db === undefined) {\n            reject(new Error('Failed to clear items in IndexedDB'))\n            return\n          }\n\n          const objectStore = db\n            .transaction(storeName, 'readwrite')\n            ?.objectStore(storeName)\n          const idbIndex = objectStore?.index('dbId')\n          const indexReq = idbIndex?.openCursor(dbId)\n          indexReq.onsuccess = () => {\n            const cursor = indexReq.result\n            onSuccess?.()\n            if (cursor) {\n              cursor.delete()\n              cursor.continue()\n            } else {\n              // either deleted all items or there were none\n              resolve()\n            }\n          }\n          indexReq.onerror = () => {\n            onError?.()\n            reject(indexReq.error)\n          }\n        })\n      } catch (e) {\n        onError?.()\n        reject(e)\n      }\n    })\n  }\n}\n\nexport const wbHistoryStorage = new WorkbenchStorage(\n  riConfig.app.indexedDbName,\n  2,\n)\n\ntype CommandHistoryType = CommandExecution[]\n\nexport async function getLocalWbHistory(\n  dbStorage: WorkbenchStorage,\n  dbId: string,\n) {\n  try {\n    const history = (await dbStorage.getItems(\n      BrowserStorageItem.wbCommandsHistory,\n      dbId,\n    )) as CommandHistoryType\n\n    return history || []\n  } catch (e) {\n    console.error(e)\n    return []\n  }\n}\n\nexport function saveLocalWbHistory(\n  dbStorage: WorkbenchStorage,\n  commandsHistory: CommandHistoryType,\n) {\n  try {\n    const key = BrowserStorageItem.wbCommandsHistory\n    return Promise.all(\n      flatten(commandsHistory.map((chItem) => dbStorage.setItem(key, chItem))),\n    )\n  } catch (e) {\n    console.error(e)\n    return null\n  }\n}\n\nasync function cleanupDatabaseHistory(\n  dbStorage: WorkbenchStorage,\n  dbId: string,\n) {\n  const commandsHistory: CommandHistoryType = await getLocalWbHistory(\n    dbStorage,\n    dbId,\n  )\n  let size = 0\n  // collect items up to maxItemsPerDb\n  const update = commandsHistory\n    .reverse()\n    .reduce((acc, commandsHistoryElement) => {\n      if (size >= WORKBENCH_HISTORY_MAX_LENGTH) {\n        return acc\n      }\n      size++\n      acc.push(commandsHistoryElement)\n      return acc\n    }, [] as CommandHistoryType)\n  // clear old items\n  await clearCommands(dbStorage, dbId)\n  // save\n  await saveLocalWbHistory(dbStorage, update)\n}\n\nexport async function addCommands(\n  dbStorage: WorkbenchStorage,\n  data: CommandExecution[],\n) {\n  // Store command results in local storage!\n  const storedData = data.map((item) => {\n    // Do not store command execution result that exceeded limitation\n    if (JSON.stringify(item.result).length > riConfig.workbench.maxResultSize) {\n      item.result = [\n        {\n          status: CommandExecutionStatus.Success,\n          response: `Results have been deleted since they exceed ${formatBytes(riConfig.workbench.maxResultSize)}. \n          Re-run the command to see new results.`,\n        },\n      ]\n    }\n    return item\n  })\n  await saveLocalWbHistory(dbStorage, storedData)\n  const [{ databaseId }] = storedData\n  return cleanupDatabaseHistory(dbStorage, databaseId)\n}\n\nexport async function removeCommand(\n  dbStorage: WorkbenchStorage,\n  dbId: string,\n  commandId: string,\n) {\n  // Delete command from local storage?!\n  await dbStorage.removeItem(\n    BrowserStorageItem.wbCommandsHistory,\n    dbId,\n    commandId,\n  )\n}\n\nexport async function clearCommands(dbStorage: WorkbenchStorage, dbId: string) {\n  await dbStorage.clear(BrowserStorageItem.wbCommandsHistory, dbId)\n}\n\nexport async function findCommand(\n  dbStorage: WorkbenchStorage,\n  commandId: string,\n) {\n  // Fetch command from local storage\n  return dbStorage.getItem(BrowserStorageItem.wbCommandsHistory, commandId)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/services/wsService.ts",
    "content": "import { io } from 'socket.io-client'\nimport { getProxyPath } from 'uiSrc/utils'\nimport { CustomHeaders } from 'uiSrc/constants/api'\nimport { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\n\nexport type WsParams = {\n  forceNew?: boolean\n  token?: string\n  reconnection?: boolean\n  query?: Record<string, any>\n  extraHeaders?: Record<string, any>\n  path?: string\n}\n\nexport function wsService(\n  wsUrl: string,\n  {\n    forceNew = true,\n    token,\n    reconnection,\n    query,\n    extraHeaders,\n    path = getProxyPath(),\n  }: WsParams,\n  passTokenViaHeaders: boolean = true,\n) {\n  const tokenObj = { [CustomHeaders.CsrfToken.toLowerCase()]: token }\n  const queryParams = {\n    ...(passTokenViaHeaders ? {} : tokenObj),\n    ...(query || {}),\n  }\n\n  const headers = {\n    [CustomHeaders.WindowId]: window.windowId || '',\n    ...(passTokenViaHeaders ? tokenObj : {}),\n    ...(extraHeaders || {}),\n  }\n\n  const transports = riConfig.api.socketTransports?.split(',')\n  const withCredentials = riConfig.api.socketCredentials\n\n  const ioOptions = {\n    path,\n    forceNew,\n    reconnection,\n    query: queryParams,\n    extraHeaders: headers,\n    rejectUnauthorized: false,\n    transports,\n    withCredentials,\n  }\n\n  return io(wsUrl, ioOptions)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/setup-env.ts",
    "content": "import { defaultConfig } from 'uiSrc/config/default'\n\nriConfig = defaultConfig\n\nwindow.app = {\n  ...window.app,\n  config: {\n    apiPort: `${defaultConfig.api.port}`,\n  },\n}\n"
  },
  {
    "path": "redisinsight/ui/src/setup-tests.ts",
    "content": "import '@testing-library/jest-dom'\nimport 'whatwg-fetch'\n\nimport { mswServer } from 'uiSrc/mocks/server'\n\nexport const URL = 'URL'\nwindow.URL.revokeObjectURL = () => {}\nwindow.URL.createObjectURL = () => URL\n\nclass ResizeObserver {\n  observe() {}\n\n  unobserve() {}\n\n  disconnect() {}\n}\n\nclass File extends Blob {\n  constructor(fileBits: any[], fileName: string, options?: any) {\n    super(fileBits, options)\n    this.name = fileName\n  }\n\n  lastModified = Date.now()\n\n  name = 'test-file'\n\n  webkitRelativePath = ''\n}\n\nObject.defineProperty(window, 'ResizeObserver', {\n  writable: true,\n  configurable: true,\n  value: ResizeObserver,\n})\n\nObject.defineProperty(window, 'File', {\n  writable: true,\n  configurable: true,\n  value: File,\n})\n\nbeforeAll(() => {\n  mswServer.listen({\n    onUnhandledRequest: 'bypass',\n  })\n})\n\nafterEach(() => {\n  mswServer.resetHandlers()\n})\n\nafterAll(() => {\n  // server.printHandlers()\n  mswServer.close()\n})\n\nglobal.ResizeObserver = jest.fn().mockImplementation(() => ({\n  observe: jest.fn(),\n  unobserve: jest.fn(),\n  disconnect: jest.fn(),\n}))\n\n// we need this since jsdom doesn't support PointerEvent\nwindow.HTMLElement.prototype.hasPointerCapture = jest.fn()\n\n// Mock window.indexedDB for test environments (jsdom/Node)\nif (!window.indexedDB) {\n  window.indexedDB = {\n    open: jest.fn(() => ({\n      onerror: jest.fn(),\n      onsuccess: jest.fn(),\n      onupgradeneeded: jest.fn(),\n      result: {},\n    })),\n  } as any\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/analytics/clusterDetails.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { StateClusterDetails } from 'uiSrc/slices/interfaces/analytics'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\n\nimport { ClusterDetails } from 'apiSrc/modules/cluster-monitor/models/cluster-details'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateClusterDetails = {\n  loading: false,\n  error: '',\n  data: null,\n}\n\nconst clusterDetailsSlice = createSlice({\n  name: 'clusterDetails',\n  initialState,\n  reducers: {\n    setClusterDetailsInitialState: () => initialState,\n    getClusterDetails: (state) => {\n      state.loading = true\n    },\n    getClusterDetailsSuccess: (\n      state,\n      { payload }: PayloadAction<ClusterDetails>,\n    ) => {\n      state.loading = false\n      state.data = payload\n    },\n    getClusterDetailsError: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\nexport const clusterDetailsSelector = (state: RootState) =>\n  state.analytics.clusterDetails\n\nexport const {\n  setClusterDetailsInitialState,\n  getClusterDetails,\n  getClusterDetailsSuccess,\n  getClusterDetailsError,\n} = clusterDetailsSlice.actions\n\n// The reducer\nexport default clusterDetailsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchClusterDetailsAction(\n  instanceId: string,\n  onSuccessAction?: (data: ClusterDetails) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getClusterDetails())\n\n      const { data, status } = await apiService.get<ClusterDetails>(\n        getUrl(instanceId, ApiEndpoints.CLUSTER_DETAILS),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getClusterDetailsSuccess(data))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getClusterDetailsError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/analytics/dbAnalysis.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  StateDatabaseAnalysis,\n  DatabaseAnalysisViewTab,\n} from 'uiSrc/slices/interfaces/analytics'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport {\n  DatabaseAnalysis,\n  ShortDatabaseAnalysis,\n} from 'apiSrc/modules/database-analysis/models'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateDatabaseAnalysis = {\n  loading: false,\n  error: '',\n  data: null,\n  selectedViewTab: DatabaseAnalysisViewTab.DataSummary,\n  history: {\n    loading: false,\n    error: '',\n    data: [],\n    showNoExpiryGroup: false,\n    selectedAnalysis: null,\n  },\n}\n\nconst databaseAnalysisSlice = createSlice({\n  name: 'databaseAnalysis',\n  initialState,\n  reducers: {\n    setDatabaseAnalysisInitialState: () => initialState,\n    getDBAnalysis: (state) => {\n      state.loading = true\n    },\n    getDBAnalysisSuccess: (\n      state,\n      { payload }: PayloadAction<DatabaseAnalysis>,\n    ) => {\n      state.loading = false\n      state.data = payload\n    },\n    getDBAnalysisError: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    setRecommendationVote: () => {\n      // we don't have any loading here\n    },\n    setRecommendationVoteSuccess: (\n      state,\n      { payload }: PayloadAction<DatabaseAnalysis>,\n    ) => {\n      state.data = payload\n    },\n    setRecommendationVoteError: (state, { payload }) => {\n      state.error = payload\n    },\n    loadDBAnalysisReports: (state) => {\n      state.history.loading = true\n    },\n    loadDBAnalysisReportsSuccess: (\n      state,\n      { payload }: PayloadAction<ShortDatabaseAnalysis[]>,\n    ) => {\n      state.history.loading = false\n      state.history.data = payload\n    },\n    loadDBAnalysisReportsError: (state, { payload }) => {\n      state.history.loading = false\n      state.history.error = payload\n    },\n    setSelectedAnalysisId: (state, { payload }: PayloadAction<string>) => {\n      state.history.selectedAnalysis = payload\n    },\n    setShowNoExpiryGroup: (state, { payload }: PayloadAction<boolean>) => {\n      state.history.showNoExpiryGroup = payload\n    },\n    setDatabaseAnalysisViewTab: (\n      state,\n      { payload }: PayloadAction<DatabaseAnalysisViewTab>,\n    ) => {\n      state.selectedViewTab = payload\n    },\n  },\n})\n\nexport const dbAnalysisSelector = (state: RootState) =>\n  state.analytics.databaseAnalysis\nexport const dbAnalysisReportsSelector = (state: RootState) =>\n  state.analytics.databaseAnalysis.history\nexport const dbAnalysisViewTabSelector = (state: RootState) =>\n  state.analytics.databaseAnalysis.selectedViewTab\n\nexport const {\n  setDatabaseAnalysisInitialState,\n  getDBAnalysis,\n  getDBAnalysisSuccess,\n  getDBAnalysisError,\n  loadDBAnalysisReports,\n  loadDBAnalysisReportsSuccess,\n  loadDBAnalysisReportsError,\n  setSelectedAnalysisId,\n  setShowNoExpiryGroup,\n  setDatabaseAnalysisViewTab,\n  setRecommendationVote,\n  setRecommendationVoteSuccess,\n  setRecommendationVoteError,\n} = databaseAnalysisSlice.actions\n\n// The reducer\nexport default databaseAnalysisSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchDBAnalysisAction(\n  instanceId: string,\n  id: string,\n  onSuccessAction?: (data: DatabaseAnalysis) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getDBAnalysis())\n\n      const { data, status } = await apiService.get<DatabaseAnalysis>(\n        getUrl(instanceId, ApiEndpoints.DATABASE_ANALYSIS, id),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getDBAnalysisSuccess(data))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getDBAnalysisError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function putRecommendationVote(\n  name: string,\n  vote: Vote,\n  onSuccessAction?: (recommendation: { name: string; vote: Vote }) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      dispatch(setRecommendationVote())\n      const state = stateInit()\n      const instanceId = state.connections.instances.connectedInstance?.id\n\n      const { data, status } = await apiService.patch(\n        getUrl(\n          instanceId,\n          ApiEndpoints.DATABASE_ANALYSIS,\n          state.analytics.databaseAnalysis.history.selectedAnalysis ?? '',\n        ),\n        { name, vote },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setRecommendationVoteSuccess(data))\n\n        onSuccessAction?.({ name, vote })\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(setRecommendationVoteError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchDBAnalysisReportsHistory(\n  instanceId: string,\n  onSuccessAction?: (data: ShortDatabaseAnalysis[]) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(loadDBAnalysisReports())\n\n      const { data, status } = await apiService.get<ShortDatabaseAnalysis[]>(\n        getUrl(instanceId, ApiEndpoints.DATABASE_ANALYSIS),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadDBAnalysisReportsSuccess(data))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadDBAnalysisReportsError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function createNewAnalysis(\n  instanceId: string,\n  delimiters: string[],\n  onSuccessAction?: (data: DatabaseAnalysis) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getDBAnalysis())\n\n      const { data, status } = await apiService.post<DatabaseAnalysis>(\n        getUrl(instanceId, ApiEndpoints.DATABASE_ANALYSIS),\n        {\n          delimiter: delimiters?.[0],\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getDBAnalysisSuccess(data))\n        dispatch<any>(fetchDBAnalysisReportsHistory(instanceId))\n        dispatch(setSelectedAnalysisId(data.id))\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getDBAnalysisError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/analytics/settings.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport {\n  AnalyticsViewTab,\n  StateAnalyticsSettings,\n} from 'uiSrc/slices/interfaces/analytics'\nimport { RootState } from 'uiSrc/slices/store'\n\nexport const initialState: StateAnalyticsSettings = {\n  viewTab: AnalyticsViewTab.ClusterDetails,\n}\n\nconst analyticsSettings = createSlice({\n  name: 'analyticsSettings',\n  initialState,\n  reducers: {\n    setInitialAnalyticsSettings: () => initialState,\n\n    setAnalyticsViewTab: (\n      state,\n      { payload }: PayloadAction<AnalyticsViewTab>,\n    ) => {\n      state.viewTab = payload\n    },\n  },\n})\n\nexport const { setInitialAnalyticsSettings, setAnalyticsViewTab } =\n  analyticsSettings.actions\n\nexport const analyticsSettingsSelector = (state: RootState) =>\n  state.analytics.settings\n\nexport default analyticsSettings.reducer\n"
  },
  {
    "path": "redisinsight/ui/src/slices/analytics/slowlog.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { ApiEndpoints, DurationUnits } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { setSlowLogUnits } from 'uiSrc/slices/app/context'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { StateSlowLog } from 'uiSrc/slices/interfaces/analytics'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateSlowLog = {\n  loading: false,\n  error: '',\n  data: [],\n  lastRefreshTime: null,\n  config: null,\n}\n\nconst slowLogSlice = createSlice({\n  name: 'slowlog',\n  initialState,\n  reducers: {\n    setSlowLogInitialState: () => initialState,\n    getSlowLogs: (state) => {\n      state.loading = true\n    },\n    getSlowLogsSuccess: (\n      state,\n      { payload: data }: PayloadAction<SlowLog[]>,\n    ) => {\n      state.loading = false\n      state.data = data\n      state.lastRefreshTime = Date.now()\n    },\n    getSlowLogsError: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    deleteSlowLogs: (state) => {\n      state.loading = true\n    },\n    deleteSlowLogsSuccess: (state) => {\n      state.loading = false\n      state.data = []\n    },\n    deleteSlowLogsError: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    getSlowLogConfig: (state) => {\n      state.loading = true\n    },\n    getSlowLogConfigSuccess: (\n      state,\n      { payload: data }: PayloadAction<SlowLogConfig>,\n    ) => {\n      state.loading = false\n      state.config = data\n    },\n    getSlowLogConfigError: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\nexport const slowLogSelector = (state: RootState) => state.analytics.slowlog\nexport const slowLogConfigSelector = (state: RootState) =>\n  state.analytics.slowlog.config || {}\n\nexport const {\n  setSlowLogInitialState,\n  getSlowLogs,\n  getSlowLogsSuccess,\n  getSlowLogsError,\n  deleteSlowLogs,\n  deleteSlowLogsSuccess,\n  deleteSlowLogsError,\n  getSlowLogConfig,\n  getSlowLogConfigSuccess,\n  getSlowLogConfigError,\n} = slowLogSlice.actions\n\n// The reducer\nexport default slowLogSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchSlowLogsAction(\n  instanceId: string,\n  count: number,\n  onSuccessAction?: (data: SlowLog[]) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getSlowLogs())\n\n      const { data, status } = await apiService.get<SlowLog[]>(\n        getUrl(instanceId, ApiEndpoints.SLOW_LOGS),\n        {\n          params: { count },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getSlowLogsSuccess(data))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getSlowLogsError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function clearSlowLogAction(\n  instanceId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(deleteSlowLogs())\n\n      const { status } = await apiService.delete<any>(\n        getUrl(instanceId, ApiEndpoints.SLOW_LOGS),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteSlowLogsSuccess())\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(deleteSlowLogsError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function getSlowLogConfigAction(\n  instanceId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getSlowLogConfig())\n\n      const { data, status } = await apiService.get<SlowLogConfig>(\n        getUrl(instanceId, ApiEndpoints.SLOW_LOGS_CONFIG),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getSlowLogConfigSuccess(data))\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getSlowLogConfigError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function patchSlowLogConfigAction(\n  instanceId: string,\n  config: SlowLogConfig,\n  durationUnit: DurationUnits,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getSlowLogConfig())\n\n      const { data, status } = await apiService.patch<SlowLogConfig>(\n        getUrl(instanceId, ApiEndpoints.SLOW_LOGS_CONFIG),\n        config,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getSlowLogConfigSuccess(data))\n        dispatch(setSlowLogUnits(durationUnit))\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getSlowLogConfigError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/connectivity.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { getDatabaseConfigInfoAction } from 'uiSrc/slices/instances/instances'\n\nimport { AppDispatch, RootState } from '../store'\nimport { StateAppConnectivity } from '../interfaces'\n\nexport const initialState: StateAppConnectivity = {\n  loading: false,\n  error: undefined,\n}\n\nconst appConnectivitySlice = createSlice({\n  name: 'appConnectivity',\n  initialState,\n  reducers: {\n    setConnectivityLoading: (state, { payload }) => {\n      state.loading = payload\n    },\n    setConnectivityError: (state, { payload }) => {\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const { setConnectivityLoading, setConnectivityError } =\n  appConnectivitySlice.actions\n\n// A selector\nexport const appConnectivity = (state: RootState) => state.app.connectivity\nexport const appConnectivityError = (state: RootState) =>\n  state.app.connectivity.error\n\n// The reducer\nexport default appConnectivitySlice.reducer\n\n// Asynchronous thunk action\nexport const retryConnection =\n  (\n    connectionInstanceId: string,\n    onSuccessAction?: () => void,\n    onFailAction?: () => void,\n  ) =>\n  (dispatch: AppDispatch) => {\n    dispatch(setConnectivityLoading(true))\n\n    return dispatch(\n      getDatabaseConfigInfoAction(\n        connectionInstanceId,\n        () => {\n          dispatch(setConnectivityError(null))\n          dispatch(setConnectivityLoading(false))\n          onSuccessAction?.()\n        },\n        () => {\n          dispatch(setConnectivityLoading(false))\n          onFailAction?.()\n        },\n      ),\n    )\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/context.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { EuiComboBoxOptionOption } from '@elastic/eui'\nimport { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces'\nimport {\n  CapabilityStorageItem,\n  ConfigDBStorageItem,\n} from 'uiSrc/constants/storage'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport {\n  BrowserStorageItem,\n  DEFAULT_DELIMITER,\n  DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS,\n  DEFAULT_SLOWLOG_DURATION_UNIT,\n  DEFAULT_TREE_SORTING,\n  KeyTypes,\n  Pages,\n  SortOrder,\n  BrowserColumns,\n  DEFAULT_SHOWN_COLUMNS,\n} from 'uiSrc/constants'\nimport {\n  localStorageService,\n  setCapabilityStorageField,\n  setDBConfigStorageField,\n} from 'uiSrc/services'\nimport { clearExpertChatHistory } from 'uiSrc/slices/panels/aiAssistant'\nimport { resetKeys, resetPatternKeysData } from 'uiSrc/slices/browser/keys'\nimport { setMonitorInitialState } from 'uiSrc/slices/cli/monitor'\nimport { setInitialPubSubState } from 'uiSrc/slices/pubsub/pubsub'\nimport { resetBulkActions } from 'uiSrc/slices/browser/bulkActions'\nimport {\n  resetCliHelperSettings,\n  resetCliSettingsAction,\n} from 'uiSrc/slices/cli/cli-settings'\nimport {\n  resetRedisearchKeysData,\n  setRedisearchInitialState,\n} from 'uiSrc/slices/browser/redisearch'\nimport { setClusterDetailsInitialState } from 'uiSrc/slices/analytics/clusterDetails'\nimport { setDatabaseAnalysisInitialState } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { setInitialAnalyticsSettings } from 'uiSrc/slices/analytics/settings'\nimport { setInitialRecommendationsState } from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  setPipelineConfig,\n  setPipelineInitialState,\n  setPipelineJobs,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { resetOutput } from 'uiSrc/slices/cli/cli-output'\nimport { setConnectivityError } from 'uiSrc/slices/app/connectivity'\nimport { SearchMode } from '../interfaces/keys'\nimport {\n  AppWorkspace,\n  RedisResponseBuffer,\n  StateAppContext,\n} from '../interfaces'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateAppContext = {\n  workspace:\n    localStorageService.get(BrowserStorageItem.homePage) === Pages.rdi\n      ? AppWorkspace.RDI\n      : AppWorkspace.Databases,\n  contextInstanceId: '',\n  contextRdiInstanceId: '',\n  lastPage: '',\n  dbConfig: {\n    treeViewDelimiter: [DEFAULT_DELIMITER],\n    treeViewSort: DEFAULT_TREE_SORTING,\n    slowLogDurationUnit: DEFAULT_SLOWLOG_DURATION_UNIT,\n    showHiddenRecommendations: DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS,\n    shownColumns: DEFAULT_SHOWN_COLUMNS,\n  },\n  dbIndex: {\n    disabled: false,\n  },\n  browser: {\n    keyList: {\n      isDataPatternLoaded: false,\n      isDataRedisearchLoaded: false,\n      scrollPatternTopPosition: 0,\n      scrollRedisearchTopPosition: 0,\n      isNotRendered: true,\n      selectedKey: null,\n    },\n    panelSizes: [],\n    tree: {\n      openNodes: {},\n      selectedLeaf: null,\n    },\n    bulkActions: {\n      opened: false,\n    },\n    keyDetailsSizes: {\n      [KeyTypes.Hash]:\n        localStorageService?.get(BrowserStorageItem.keyDetailSizes)?.hash ??\n        null,\n      [KeyTypes.List]:\n        localStorageService?.get(BrowserStorageItem.keyDetailSizes)?.list ??\n        null,\n      [KeyTypes.ZSet]:\n        localStorageService?.get(BrowserStorageItem.keyDetailSizes)?.zset ??\n        null,\n    },\n  },\n  workbench: {\n    script: '',\n    panelSizes: [],\n  },\n  searchAndQuery: {\n    script: '',\n    panelSizes: {\n      vertical: {},\n    },\n  },\n  pubsub: {\n    channel: '',\n    message: '',\n  },\n  analytics: {\n    lastViewedPage: '',\n  },\n  capability: {\n    source: '',\n  },\n  pipelineManagement: {\n    lastViewedPage: '',\n    isOpenDialog: false,\n  },\n}\n\n// A slice for recipes\nconst appContextSlice = createSlice({\n  name: 'appContext',\n  initialState,\n  reducers: {\n    // don't need to reset instanceId\n    setAppContextInitialState: (state) => ({\n      ...initialState,\n      workspace: state.workspace,\n      browser: {\n        ...initialState.browser,\n        keyDetailsSizes: state.browser.keyDetailsSizes,\n      },\n      contextInstanceId: state.contextInstanceId,\n      contextRdiInstanceId: state.contextRdiInstanceId,\n      capability: state.capability,\n      pipelineManagement: state.pipelineManagement,\n    }),\n    // set connected instance\n    setAppContextConnectedInstanceId: (\n      state,\n      { payload }: { payload: string },\n    ) => {\n      state.contextInstanceId = payload\n    },\n    // set connected rdi instance\n    setAppContextConnectedRdiInstanceId: (\n      state,\n      { payload }: { payload: string },\n    ) => {\n      state.contextRdiInstanceId = payload\n    },\n    setCurrentWorkspace: (\n      state,\n      { payload }: PayloadAction<Maybe<AppWorkspace>>,\n    ) => {\n      state.workspace = payload || AppWorkspace.Databases\n    },\n    setDbConfig: (state, { payload }) => {\n      state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? [\n        DEFAULT_DELIMITER,\n      ]\n      state.dbConfig.treeViewSort =\n        payload?.treeViewSort ?? DEFAULT_TREE_SORTING\n      state.dbConfig.slowLogDurationUnit =\n        payload?.slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT\n      state.dbConfig.showHiddenRecommendations =\n        payload?.showHiddenRecommendations\n      state.dbConfig.shownColumns =\n        payload?.shownColumns ?? DEFAULT_SHOWN_COLUMNS\n    },\n    setSlowLogUnits: (state, { payload }) => {\n      state.dbConfig.slowLogDurationUnit = payload\n      setDBConfigStorageField(\n        state.contextInstanceId,\n        ConfigDBStorageItem.slowLogDurationUnit,\n        payload,\n      )\n    },\n    setBrowserTreeDelimiter: (\n      state,\n      { payload }: { payload: EuiComboBoxOptionOption[] },\n    ) => {\n      state.dbConfig.treeViewDelimiter = payload as any\n      setDBConfigStorageField(\n        state.contextInstanceId,\n        BrowserStorageItem.treeViewDelimiter,\n        payload,\n      )\n    },\n    setBrowserTreeSort: (state, { payload }: PayloadAction<SortOrder>) => {\n      state.dbConfig.treeViewSort = payload\n      setDBConfigStorageField(\n        state.contextInstanceId,\n        BrowserStorageItem.treeViewSort,\n        payload,\n      )\n    },\n    setBrowserShownColumns: (\n      state,\n      { payload }: PayloadAction<BrowserColumns[]>,\n    ) => {\n      state.dbConfig.shownColumns = payload\n      setDBConfigStorageField(\n        state.contextInstanceId,\n        BrowserStorageItem.browserShownColumns,\n        payload,\n      )\n    },\n    setRecommendationsShowHidden: (\n      state,\n      { payload }: { payload: boolean },\n    ) => {\n      state.dbConfig.showHiddenRecommendations = payload\n      setDBConfigStorageField(\n        state.contextInstanceId,\n        BrowserStorageItem.showHiddenRecommendations,\n        payload,\n      )\n    },\n    setBrowserSelectedKey: (\n      state,\n      { payload }: { payload: Nullable<RedisResponseBuffer> },\n    ) => {\n      state.browser.keyList.selectedKey = payload\n    },\n    setBrowserPatternKeyListDataLoaded: (\n      state,\n      { payload }: { payload: boolean },\n    ) => {\n      state.browser.keyList.isDataPatternLoaded = payload\n    },\n    setBrowserRedisearchKeyListDataLoaded: (\n      state,\n      { payload }: { payload: boolean },\n    ) => {\n      state.browser.keyList.isDataRedisearchLoaded = payload\n    },\n    setBrowserPatternScrollPosition: (\n      state,\n      { payload }: { payload: number },\n    ) => {\n      state.browser.keyList.scrollPatternTopPosition = payload\n    },\n    setBrowserRedisearchScrollPosition: (\n      state,\n      { payload }: { payload: number },\n    ) => {\n      state.browser.keyList.scrollRedisearchTopPosition = payload\n    },\n    setBrowserIsNotRendered: (state, { payload }: { payload: boolean }) => {\n      state.browser.keyList.isNotRendered = payload\n    },\n    clearBrowserKeyListData: (state) => {\n      state.browser.keyList = {\n        ...initialState.browser.keyList,\n        selectedKey: state.browser.keyList.selectedKey,\n      }\n    },\n    setBrowserPanelSizes: (state, { payload }: { payload: any }) => {\n      state.browser.panelSizes = payload\n    },\n    setBrowserTreeNodesOpen: (\n      state,\n      { payload }: { payload: { [key: string]: boolean } },\n    ) => {\n      state.browser.tree.openNodes = payload\n    },\n    setWorkbenchScript: (state, { payload }: { payload: string }) => {\n      state.workbench.script = payload\n    },\n    setWorkbenchVerticalPanelSizes: (state, { payload }: { payload: any }) => {\n      state.workbench.panelSizes = payload\n    },\n    setSQVerticalPanelSizes: (state, { payload }: { payload: any }) => {\n      state.searchAndQuery.panelSizes.vertical = payload\n    },\n    setSQScript: (state, { payload }: { payload: any }) => {\n      state.searchAndQuery.script = payload\n    },\n    setLastPageContext: (state, { payload }: { payload: string }) => {\n      state.lastPage = payload\n    },\n    resetBrowserTree: (state) => {\n      state.browser.tree.selectedLeaf = null\n      state.browser.tree.openNodes = {}\n    },\n    setPubSubFieldsContext: (\n      state,\n      { payload }: { payload: { channel: string; message: string } },\n    ) => {\n      state.pubsub.channel = payload.channel\n      state.pubsub.message = payload.message\n    },\n    setBrowserBulkActionOpen: (state, { payload }: PayloadAction<boolean>) => {\n      state.browser.bulkActions.opened = payload\n    },\n    setLastAnalyticsPage: (state, { payload }: { payload: string }) => {\n      state.analytics.lastViewedPage = payload\n    },\n    updateKeyDetailsSizes: (\n      state,\n      { payload }: { payload: { type: KeyTypes; sizes: RelativeWidthSizes } },\n    ) => {\n      const { type, sizes } = payload\n      state.browser.keyDetailsSizes[type] = sizes\n      localStorageService?.set(\n        BrowserStorageItem.keyDetailSizes,\n        state.browser.keyDetailsSizes,\n      )\n    },\n    setDbIndexState: (state, { payload }: { payload: boolean }) => {\n      state.dbIndex.disabled = payload\n    },\n    setCapability: (\n      state,\n      {\n        payload,\n      }: PayloadAction<\n        Maybe<{ source: string; tutorialPopoverShown: boolean }>\n      >,\n    ) => {\n      const source = payload?.source ?? ''\n      const tutorialPopoverShown = !!payload?.tutorialPopoverShown\n\n      state.capability.source = source\n\n      setCapabilityStorageField(CapabilityStorageItem.source, source)\n      setCapabilityStorageField(\n        CapabilityStorageItem.tutorialPopoverShown,\n        tutorialPopoverShown,\n      )\n    },\n    setLastPipelineManagementPage: (\n      state,\n      { payload }: { payload: string },\n    ) => {\n      state.pipelineManagement.lastViewedPage = payload\n    },\n    setPipelineDialogState: (state, { payload }: { payload: boolean }) => {\n      state.pipelineManagement.isOpenDialog = payload\n    },\n    resetPipelineManagement: (state) => {\n      state.pipelineManagement.lastViewedPage = ''\n      state.pipelineManagement.isOpenDialog = false\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setAppContextInitialState,\n  setAppContextConnectedInstanceId,\n  setAppContextConnectedRdiInstanceId,\n  setCurrentWorkspace,\n  setDbConfig,\n  setSlowLogUnits,\n  setBrowserPatternKeyListDataLoaded,\n  setBrowserRedisearchKeyListDataLoaded,\n  setBrowserSelectedKey,\n  setBrowserPatternScrollPosition,\n  setBrowserRedisearchScrollPosition,\n  setBrowserIsNotRendered,\n  setBrowserPanelSizes,\n  setBrowserTreeNodesOpen,\n  setBrowserTreeDelimiter,\n  resetBrowserTree,\n  setWorkbenchScript,\n  setWorkbenchVerticalPanelSizes,\n  setSQVerticalPanelSizes,\n  setSQScript,\n  setLastPageContext,\n  setPubSubFieldsContext,\n  setBrowserBulkActionOpen,\n  setLastAnalyticsPage,\n  updateKeyDetailsSizes,\n  clearBrowserKeyListData,\n  setDbIndexState,\n  setRecommendationsShowHidden,\n  setBrowserTreeSort,\n  setCapability,\n  setLastPipelineManagementPage,\n  setPipelineDialogState,\n  resetPipelineManagement,\n  setBrowserShownColumns,\n} = appContextSlice.actions\n\n// Selectors\nexport const appContextSelector = (state: RootState) => state.app.context\nexport const appContextDbConfig = (state: RootState) =>\n  state.app.context.dbConfig\nexport const appContextBrowser = (state: RootState) => state.app.context.browser\nexport const appContextBrowserTree = (state: RootState) =>\n  state.app.context.browser.tree\nexport const appContextBrowserKeyDetails = (state: RootState) =>\n  state.app.context.browser.keyDetailsSizes\nexport const appContextWorkbench = (state: RootState) =>\n  state.app.context.workbench\nexport const appContextSearchAndQuery = (state: RootState) =>\n  state.app.context.searchAndQuery\nexport const appContextSelectedKey = (state: RootState) =>\n  state.app.context.browser.keyList.selectedKey\nexport const appContextPubSub = (state: RootState) => state.app.context.pubsub\nexport const appContextAnalytics = (state: RootState) =>\n  state.app.context.analytics\nexport const appContextDbIndex = (state: RootState) => state.app.context.dbIndex\nexport const appContextCapability = (state: RootState) =>\n  state.app.context.capability\nexport const appContextPipelineManagement = (state: RootState) =>\n  state.app.context.pipelineManagement\n\n// The reducer\nexport default appContextSlice.reducer\n\n// Asynchronous thunk action\nexport function setBrowserKeyListDataLoaded(\n  searchMode: SearchMode,\n  value: boolean,\n) {\n  return searchMode === SearchMode.Pattern\n    ? setBrowserPatternKeyListDataLoaded(value)\n    : setBrowserRedisearchKeyListDataLoaded(value)\n}\n\nexport function resetDatabaseContext() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(resetKeys())\n    dispatch(setMonitorInitialState())\n    dispatch(setInitialPubSubState())\n    dispatch(resetBulkActions())\n    dispatch(setAppContextInitialState())\n    dispatch(resetPatternKeysData())\n    dispatch(resetCliHelperSettings())\n    dispatch(resetCliSettingsAction())\n    dispatch(resetRedisearchKeysData())\n    dispatch(setClusterDetailsInitialState())\n    dispatch(setDatabaseAnalysisInitialState())\n    dispatch(setInitialAnalyticsSettings())\n    dispatch(setRedisearchInitialState())\n    dispatch(setInitialRecommendationsState())\n    dispatch(clearExpertChatHistory())\n    dispatch(setConnectivityError(null))\n    setTimeout(() => {\n      dispatch(resetOutput())\n    }, 0)\n  }\n}\n\nexport function resetRdiContext() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setAppContextConnectedRdiInstanceId(''))\n    dispatch(setPipelineInitialState())\n    dispatch(setPipelineConfig(''))\n    dispatch(setPipelineJobs([]))\n    dispatch(resetPipelineManagement())\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/csrf.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport apiService, { setApiCsrfHeader } from 'uiSrc/services/apiService'\nimport { setResourceCsrfHeader } from 'uiSrc/services/resourcesService'\nimport { getConfig } from 'uiSrc/config'\nimport { AppDispatch, RootState } from '../store'\n\nconst riConfig = getConfig()\nexport const getCsrfEndpoint = () => riConfig.api.csrfEndpoint\n\nexport interface CSRFTokenResponse {\n  token: string\n}\n\nexport const initialState: {\n  csrfEndpoint: string\n  loading: boolean\n  token: string\n  error: string\n} = {\n  csrfEndpoint: getCsrfEndpoint(),\n  loading: false,\n  token: '',\n  error: '',\n}\n\nconst appCsrfSlice = createSlice({\n  name: 'appCsrf',\n  initialState,\n  reducers: {\n    fetchCsrfToken: (state) => {\n      state.loading = true\n    },\n    fetchCsrfTokenSuccess: (\n      state,\n      { payload }: { payload: { token: string } },\n    ) => {\n      state.token = payload.token\n      state.loading = false\n    },\n    fetchCsrfTokenFail: (\n      state,\n      { payload }: { payload: { error: string } },\n    ) => {\n      state.loading = false\n      state.token = ''\n      state.error = payload.error\n    },\n  },\n})\n\nexport const { fetchCsrfToken, fetchCsrfTokenSuccess, fetchCsrfTokenFail } =\n  appCsrfSlice.actions\n\nexport const appCsrfSelector = (state: RootState) => state.app.csrf\n\nexport default appCsrfSlice.reducer\n\nexport function fetchCsrfTokenAction(\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      if (getCsrfEndpoint()) {\n        dispatch(fetchCsrfToken())\n\n        const { data } =\n          await apiService.get<CSRFTokenResponse>(getCsrfEndpoint())\n\n        setApiCsrfHeader(data.token)\n        setResourceCsrfHeader(data.token)\n        dispatch(fetchCsrfTokenSuccess({ token: data.token }))\n        onSuccessAction?.(data)\n      }\n    } catch (error: any) {\n      console.error('Error fetching CSRF token: ', error)\n      dispatch(fetchCsrfTokenFail({ error: error?.message || '' }))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/db-settings.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { getDbSettings } from 'uiSrc/services/databaseSettingsService'\nimport { DatabaseSettings, DatabaseSettingsData } from '../interfaces'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: DatabaseSettings = {\n  loading: false,\n  error: '',\n  data: {},\n}\n\n// A slice for recipes\nconst appDbSettingsSlice = createSlice({\n  name: 'appDbSettings',\n  initialState,\n  reducers: {\n    getDBSettings: (state) => {\n      state.loading = true\n    },\n    getDBSettingsSuccess: (\n      state,\n      {\n        payload: { data, id },\n      }: {\n        payload: {\n          id: string\n          data: DatabaseSettingsData\n        }\n      },\n    ) => {\n      state.loading = false\n      state.data = { ...state.data, [id]: data }\n    },\n    getDBSettingsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const { getDBSettings, getDBSettingsSuccess, getDBSettingsFailure } =\n  appDbSettingsSlice.actions\n\n// A selector\nexport const appDBSettingsSelector = (state: RootState) => state.app.dbSettings\n\n// The reducer\nexport default appDbSettingsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchDBSettings(\n  id: string,\n  onSuccessAction?: (payload: {\n    id: string\n    data: DatabaseSettingsData\n  }) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getDBSettings())\n    if (!id) {\n      dispatch(getDBSettingsFailure('DB not connected'))\n      onFailAction?.()\n      return\n    }\n    try {\n      const { data, status } = await getDbSettings(id)\n      if (isStatusSuccessful(status)) {\n        dispatch(getDBSettingsSuccess({ id, data }))\n        onSuccessAction?.({\n          id,\n          data,\n        })\n      } else {\n        dispatch(getDBSettingsFailure(data))\n        onFailAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(getDBSettingsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/features.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { remove } from 'lodash'\nimport { ApiEndpoints, BrowserStorageItem, FeatureFlags } from 'uiSrc/constants'\nimport { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting'\nimport { apiService, localStorageService } from 'uiSrc/services'\nimport { StateAppFeatures } from 'uiSrc/slices/interfaces'\nimport { AppDispatch, RootState } from 'uiSrc/slices/store'\nimport { getPagesForFeatures } from 'uiSrc/utils/features'\nimport { OnboardingSteps } from 'uiSrc/constants/onboarding'\nimport { isStatusSuccessful, Maybe } from 'uiSrc/utils'\nimport { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\n\nexport const initialState: StateAppFeatures = {\n  highlighting: {\n    version: '',\n    features: [],\n    pages: {},\n  },\n  onboarding: {\n    currentStep: 0,\n    totalSteps: 0,\n    isActive: false,\n  },\n  featureFlags: {\n    loading: false,\n    features: {\n      [FeatureFlags.insightsRecommendations]: {\n        flag: false,\n      },\n      [FeatureFlags.cloudSso]: {\n        flag: false,\n      },\n      [FeatureFlags.cloudSsoRecommendedSettings]: {\n        flag: false,\n      },\n      [FeatureFlags.documentationChat]: {\n        flag: false,\n      },\n      [FeatureFlags.databaseChat]: {\n        flag: false,\n      },\n      [FeatureFlags.hashFieldExpiration]: {\n        flag: false,\n      },\n      [FeatureFlags.rdi]: {\n        flag: false,\n      },\n      [FeatureFlags.enhancedCloudUI]: {\n        flag: false,\n      },\n      [FeatureFlags.databaseManagement]: {\n        flag: true,\n      },\n      [FeatureFlags.envDependent]: {\n        flag: riConfig.features.envDependent.defaultFlag,\n      },\n      [FeatureFlags.cloudAds]: {\n        flag: riConfig.features.cloudAds.defaultFlag,\n      },\n      [FeatureFlags.vectorSearchV2]: {\n        flag: false,\n      },\n      [FeatureFlags.azureEntraId]: {\n        flag: false,\n      },\n      [FeatureFlags.devBrowser]: {\n        flag: false,\n      },\n    },\n  },\n}\n\nconst appFeaturesSlice = createSlice({\n  name: 'appFeatures',\n  initialState,\n  reducers: {\n    setFeaturesInitialState: () => initialState,\n    setFeaturesToHighlight: (\n      state,\n      { payload }: { payload: { version: string; features: string[] } },\n    ) => {\n      state.highlighting.features = payload.features\n      state.highlighting.version = payload.version\n      state.highlighting.pages = getPagesForFeatures(payload.features)\n    },\n    removeFeatureFromHighlighting: (\n      state,\n      { payload }: { payload: string },\n    ) => {\n      remove(state.highlighting.features, (f) => f === payload)\n\n      const pageName = BUILD_FEATURES[payload].page\n      if (pageName && pageName in state.highlighting.pages) {\n        remove(state.highlighting.pages[pageName], (f) => f === payload)\n      }\n\n      const { version, features } = state.highlighting\n      localStorageService.set(BrowserStorageItem.featuresHighlighting, {\n        version,\n        features,\n      })\n    },\n    setOnboarding: (state, { payload }) => {\n      const enabledByEnv =\n        state.featureFlags.features[FeatureFlags.envDependent]?.flag ?? true\n      if (payload.currentStep > payload.totalSteps || !enabledByEnv) {\n        state.onboarding.isActive = false\n        localStorageService.set(BrowserStorageItem.onboardingStep, null)\n        return\n      }\n\n      state.onboarding.currentStep = payload.currentStep ?? 0\n      state.onboarding.totalSteps = payload.totalSteps\n      state.onboarding.isActive = true\n      localStorageService.set(\n        BrowserStorageItem.onboardingStep,\n        payload.currentStep ?? 0,\n      )\n    },\n    skipOnboarding: (state) => {\n      state.onboarding.isActive = false\n      localStorageService.set(BrowserStorageItem.onboardingStep, null)\n    },\n    setOnboardPrevStep: (state) => {\n      const { currentStep, isActive } = state.onboarding\n      if (!isActive) return\n\n      const step = currentStep > 0 ? currentStep - 1 : 0\n      state.onboarding.currentStep = step\n\n      localStorageService.set(BrowserStorageItem.onboardingStep, step)\n    },\n    setOnboardNextStep: (\n      state,\n      { payload = 0 }: PayloadAction<Maybe<number>>,\n    ) => {\n      const { currentStep, isActive } = state.onboarding\n      if (!isActive) return\n\n      const step = currentStep + 1 + payload\n      state.onboarding.currentStep = step\n\n      if (state.onboarding.currentStep > state.onboarding.totalSteps) {\n        state.onboarding.isActive = false\n        localStorageService.set(BrowserStorageItem.onboardingStep, null)\n        return\n      }\n\n      localStorageService.set(BrowserStorageItem.onboardingStep, step)\n    },\n    getFeatureFlags: (state) => {\n      state.featureFlags.loading = true\n    },\n    getFeatureFlagsSuccess: (state, { payload }) => {\n      state.featureFlags.loading = false\n\n      // make sure certain features are defined and enabled by default\n      if (!payload.features[FeatureFlags.envDependent]) {\n        payload.features[FeatureFlags.envDependent] = {\n          flag: riConfig.features.envDependent.defaultFlag,\n        }\n      }\n      if (!payload.features[FeatureFlags.cloudAds]) {\n        payload.features[FeatureFlags.cloudAds] = {\n          flag: riConfig.features.cloudAds.defaultFlag,\n        }\n      }\n\n      state.featureFlags.features = payload.features\n    },\n    getFeatureFlagsFailure: (state) => {\n      state.featureFlags.loading = false\n    },\n  },\n})\n\nexport const {\n  setFeaturesInitialState,\n  setFeaturesToHighlight,\n  removeFeatureFromHighlighting,\n  skipOnboarding,\n  setOnboardPrevStep,\n  setOnboardNextStep,\n  setOnboarding,\n  getFeatureFlags,\n  getFeatureFlagsSuccess,\n  getFeatureFlagsFailure,\n} = appFeaturesSlice.actions\n\nexport const appFeatureSelector = (state: RootState) => state.app.features\nexport const appFeatureHighlightingSelector = (state: RootState) =>\n  state.app.features.highlighting\nexport const appFeaturePagesHighlightingSelector = (state: RootState) =>\n  state.app.features.highlighting.pages\n\nexport const appFeatureOnboardingSelector = (state: RootState) =>\n  state.app.features.onboarding\nexport const appFeatureFlagsSelector = (state: RootState) =>\n  state.app.features.featureFlags\nexport const appFeatureFlagsFeaturesSelector = (state: RootState) =>\n  state.app.features.featureFlags.features\n\nconst isDevelopment = riConfig.app.env === 'development'\n\nexport const isAzureEntraIdEnabledSelector = (state: RootState): boolean => {\n  if (isDevelopment) {\n    return true\n  }\n\n  const features = state.app.features.featureFlags.features\n  const azureEntraIdEnabled = features[FeatureFlags.azureEntraId]?.flag ?? false\n  const envDependentEnabled = features[FeatureFlags.envDependent]?.flag ?? false\n\n  return azureEntraIdEnabled && envDependentEnabled\n}\n\nexport default appFeaturesSlice.reducer\n\nexport function incrementOnboardStepAction(\n  step: OnboardingSteps,\n  skipCount = 0,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { currentStep, isActive } = state.app.features.onboarding\n    if (isActive && currentStep === step) {\n      dispatch(setOnboardNextStep(skipCount))\n      onSuccess?.()\n    }\n  }\n}\n\nexport function fetchFeatureFlags(\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getFeatureFlags())\n\n    try {\n      const { data, status } = await apiService.get(ApiEndpoints.FEATURES)\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getFeatureFlagsSuccess(data))\n        onSuccessAction?.(data)\n      }\n    } catch (error) {\n      dispatch(getFeatureFlagsFailure())\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/info.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport { RedisResponseEncoding, StateAppInfo } from '../interfaces'\n\nexport const initialState: StateAppInfo = {\n  loading: true,\n  error: '',\n  server: null,\n  encoding: RedisResponseEncoding.Buffer,\n  electron: {\n    isUpdateAvailable: null,\n    updateDownloadedVersion: '',\n    isReleaseNotesViewed: null,\n  },\n  isShortcutsFlyoutOpen: false,\n}\n\n// A slice for recipes\nconst appInfoSlice = createSlice({\n  name: 'appInfo',\n  initialState,\n  reducers: {\n    setServerInfoInitialState: () => initialState,\n    setElectronInfo: (state, { payload }) => {\n      state.electron.isUpdateAvailable = payload.isUpdateAvailable\n      state.electron.updateDownloadedVersion = payload.updateDownloadedVersion\n    },\n    setReleaseNotesViewed: (state, { payload }) => {\n      state.electron.isReleaseNotesViewed = payload\n    },\n    getServerInfo: (state) => {\n      state.loading = true\n    },\n    getServerInfoSuccess: (state, { payload }) => {\n      state.loading = false\n      state.server = payload\n    },\n    getServerInfoFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    setShortcutsFlyoutState: (state, { payload }) => {\n      state.isShortcutsFlyoutOpen = payload\n    },\n    setEncoding: (state, { payload }: PayloadAction<RedisResponseEncoding>) => {\n      state.encoding = payload\n    },\n    setServerLoaded: (state) => {\n      state.loading = false\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setElectronInfo,\n  setReleaseNotesViewed,\n  getServerInfo,\n  getServerInfoSuccess,\n  getServerInfoFailure,\n  setShortcutsFlyoutState,\n  setEncoding,\n  setServerLoaded,\n} = appInfoSlice.actions\n\n// A selector\nexport const appInfoSelector = (state: RootState) => state.app.info\nexport const appServerInfoSelector = (state: RootState) => state.app.info.server\nexport const appElectronInfoSelector = (state: RootState) =>\n  state.app.info.electron\n\n// The reducer\nexport default appInfoSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchServerInfo(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getServerInfo())\n\n    try {\n      const { data, status } = await apiService.get<GetServerInfoResponse>(\n        ApiEndpoints.INFO,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getServerInfoSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getServerInfoFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/init.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { fetchCsrfTokenAction } from 'uiSrc/slices/app/csrf'\nimport { fetchFeatureFlags } from 'uiSrc/slices/app/features'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { fetchCloudUserProfile } from 'uiSrc/slices/user/cloud-user-profile'\nimport { AppDispatch, RootState } from '../store'\n\nexport const STATUS_INITIAL = 'initial'\nexport const STATUS_LOADING = 'loading'\nexport const STATUS_SUCCESS = 'success'\nexport const STATUS_FAIL = 'fail'\nconst appStatus = [\n  STATUS_INITIAL,\n  STATUS_LOADING,\n  STATUS_SUCCESS,\n  STATUS_FAIL,\n] as const\n\nexport const FAILED_TO_FETCH_CSRF_TOKEN_ERROR = 'Failed to fetch CSRF token'\nexport const FAILED_TO_FETCH_FEATURE_FLAGS_ERROR =\n  'Failed to fetch feature flags'\nexport const FAILED_TO_FETCH_USER_PROFILE_ERROR = 'Failed to fetch user profile'\n\nexport const initialState: {\n  status: (typeof appStatus)[number]\n  error?: string\n} = {\n  status: STATUS_INITIAL,\n}\n\nconst appInitSlice = createSlice({\n  name: 'init',\n  initialState,\n  reducers: {\n    initializeAppState: (state) => {\n      state.status = STATUS_LOADING\n    },\n    initializeAppStateSuccess: (state) => {\n      state.status = STATUS_SUCCESS\n    },\n    initializeAppStateFail: (\n      state,\n      {\n        payload,\n      }: {\n        payload: {\n          error: string\n        }\n      },\n    ) => {\n      state.status = STATUS_FAIL\n      state.error = payload.error\n    },\n  },\n})\n\nexport const {\n  initializeAppState,\n  initializeAppStateSuccess,\n  initializeAppStateFail,\n} = appInitSlice.actions\n\nexport const appInitSelector = (state: RootState) => state.app.init\n\nexport default appInitSlice.reducer\n\n/**\n * Initialize the app by fetching REQUIRED data.\n *\n * @param onSuccessAction - Called when the app is successfully initialized.\n * @param onFailAction - Called when there is an error while initializing the app.\n *\n */\nexport function initializeAppAction(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(initializeAppState())\n      await dispatch(\n        fetchCsrfTokenAction(undefined, () => {\n          throw new Error(FAILED_TO_FETCH_CSRF_TOKEN_ERROR)\n        }),\n      )\n\n      await dispatch(\n        fetchFeatureFlags(\n          async (flagsData) => {\n            const { [FeatureFlags.envDependent]: envDependent } =\n              flagsData.features\n            if (!envDependent?.flag) {\n              await dispatch(\n                fetchCloudUserProfile(undefined, () => {\n                  throw new Error(FAILED_TO_FETCH_USER_PROFILE_ERROR)\n                }),\n              )\n            }\n\n            dispatch(initializeAppStateSuccess())\n            onSuccessAction?.()\n          },\n          () => {\n            throw new Error(FAILED_TO_FETCH_FEATURE_FLAGS_ERROR)\n          },\n        ),\n      )\n    } catch (error: any) {\n      dispatch(initializeAppStateFail({ error: error?.message || '' }))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/notifications.ts",
    "content": "import { createSlice, current, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { findIndex, isUndefined } from 'lodash'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getApiErrorMessage,\n  getApiErrorName,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\nimport { NotificationsDto } from 'apiSrc/modules/notification/dto'\nimport {\n  IError,\n  IGlobalNotification,\n  InfiniteMessage,\n  StateAppNotifications,\n} from '../interfaces'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateAppNotifications = {\n  errors: [],\n  messages: [],\n  infiniteMessages: [],\n  notificationCenter: {\n    loading: false,\n    lastReceivedNotification: null,\n    notifications: [],\n    isNotificationOpen: false,\n    isCenterOpen: false,\n    totalUnread: 0,\n    shouldDisplayToast: false,\n  },\n}\n\nexport interface IAddInstanceErrorPayload extends AxiosError {\n  instanceId?: string\n  response?: AxiosError['response'] & {\n    data: {\n      message?: string | JSX.Element\n      title?: string\n      additionalInfo?: Record<string, any>\n    }\n  }\n}\n// A slice for recipes\nconst notificationsSlice = createSlice({\n  name: 'notifications',\n  initialState,\n  reducers: {\n    addErrorNotification: (\n      state,\n      { payload }: { payload: IAddInstanceErrorPayload },\n    ) => {\n      const { instanceId } = payload\n      const title = payload?.response?.data?.title\n      const errorName = getApiErrorName(payload)\n      const message = getApiErrorMessage(payload)\n      const additionalInfo = payload?.response?.data?.additionalInfo\n      const errorExistedId = state.errors.findIndex(\n        (err) => err.message === message,\n      )\n\n      if (errorExistedId !== -1) {\n        notificationsSlice.caseReducers.removeError(state, {\n          payload: current(state.errors[errorExistedId])?.id ?? '',\n        })\n      }\n\n      const error: IError = {\n        ...payload,\n        title,\n        instanceId,\n        id: `${Date.now()}`,\n        name: errorName,\n        message,\n      }\n\n      if (additionalInfo) {\n        error.additionalInfo = additionalInfo\n      }\n\n      state.errors.push(error)\n    },\n    removeError: (state, { payload = '' }: { payload: string }) => {\n      if (state.errors.find((error) => error.id === payload)) {\n        state.errors = state.errors.filter((error) => error.id !== payload)\n      }\n    },\n    resetErrors: (state) => {\n      state.errors = []\n    },\n    addMessageNotification: (state, { payload }) => {\n      state.messages.push({\n        ...payload,\n        id: `${Date.now()}`,\n        group: payload.group,\n      })\n    },\n    removeMessage: (state, { payload = '' }: { payload: string }) => {\n      if (state.messages.find((message) => message.id === payload)) {\n        state.messages = state.messages.filter(\n          (message) => message.id !== payload,\n        )\n      }\n      if (state.errors.find((error) => error.id === payload)) {\n        state.errors = state.errors.filter((error) => error.id !== payload)\n      }\n    },\n    resetMessages: (state) => {\n      state.messages = []\n    },\n    setIsCenterOpen: (state, { payload }: { payload: Maybe<boolean> }) => {\n      if (isUndefined(payload)) {\n        state.notificationCenter.isCenterOpen =\n          !state.notificationCenter.isCenterOpen\n        return\n      }\n      state.notificationCenter.isCenterOpen = payload\n    },\n    setIsNotificationOpen: (\n      state,\n      { payload }: { payload: Maybe<boolean> },\n    ) => {\n      if (isUndefined(payload)) {\n        state.notificationCenter.isNotificationOpen =\n          !state.notificationCenter.isNotificationOpen\n        return\n      }\n      state.notificationCenter.isNotificationOpen = payload\n    },\n    setNewNotificationReceived: (\n      state,\n      { payload }: { payload: NotificationsDto },\n    ) => {\n      state.notificationCenter.totalUnread = payload.totalUnread\n      state.notificationCenter.isNotificationOpen = true\n    },\n    setLastReceivedNotification: (\n      state,\n      { payload }: { payload: Nullable<IGlobalNotification> },\n    ) => {\n      state.notificationCenter.lastReceivedNotification = payload\n    },\n    getNotifications: (state) => {\n      state.notificationCenter.loading = true\n    },\n    getNotificationsSuccess: (\n      state,\n      { payload }: { payload: NotificationsDto },\n    ) => {\n      state.notificationCenter.loading = false\n      state.notificationCenter.notifications = payload.notifications\n      state.notificationCenter.totalUnread = payload.totalUnread\n    },\n    getNotificationsFailed: (state) => {\n      state.notificationCenter.loading = false\n    },\n    unreadNotifications: (state, { payload }) => {\n      state.notificationCenter.totalUnread = payload\n    },\n    addInfiniteNotification: (\n      state,\n      { payload }: PayloadAction<InfiniteMessage>,\n    ) => {\n      const index = findIndex(state.infiniteMessages, { id: payload.id })\n      if (index === -1) {\n        state.infiniteMessages.push(payload)\n      } else {\n        const currentNotification = state.infiniteMessages[index]\n        // check if existing notification is exactly the same as the new one, if yes, do not update\n        if (\n          currentNotification.variation &&\n          payload.variation === currentNotification.variation\n        ) {\n          return\n        }\n        state.infiniteMessages[index] = payload\n      }\n    },\n    removeInfiniteNotification: (state, { payload }: PayloadAction<string>) => {\n      if (state.infiniteMessages.find((message) => message.id === payload)) {\n        state.infiniteMessages = state.infiniteMessages.filter(\n          (message) => message.id !== payload,\n        )\n      }\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  addErrorNotification,\n  removeError,\n  resetErrors,\n  addMessageNotification,\n  removeMessage,\n  resetMessages,\n  setIsCenterOpen,\n  setIsNotificationOpen,\n  setNewNotificationReceived,\n  setLastReceivedNotification,\n  getNotifications,\n  getNotificationsSuccess,\n  getNotificationsFailed,\n  unreadNotifications,\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} = notificationsSlice.actions\n\n// Selectors\nexport const errorsSelector = (state: RootState) =>\n  state.app.notifications.errors\nexport const messagesSelector = (state: RootState) =>\n  state.app.notifications.messages\nexport const infiniteNotificationsSelector = (state: RootState) =>\n  state.app.notifications.infiniteMessages\nexport const notificationCenterSelector = (state: RootState) =>\n  state.app.notifications.notificationCenter\n\n// The reducer\nexport default notificationsSlice.reducer\n\nexport function fetchNotificationsAction(\n  onSuccessAction?: (totalCount: number, numberOfNotifications: number) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getNotifications())\n\n    try {\n      const { data, status } = await apiService.get<NotificationsDto>(\n        ApiEndpoints.NOTIFICATIONS,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getNotificationsSuccess(data))\n        onSuccessAction?.(data.totalUnread, data.notifications?.length)\n      }\n    } catch (error) {\n      dispatch(getNotificationsFailed())\n      onFailAction?.()\n    }\n  }\n}\n\nexport function unreadNotificationsAction(notification?: {\n  timestamp: number\n  type: string\n}) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { data, status } = await apiService.patch(\n        ApiEndpoints.NOTIFICATIONS_READ,\n        notification,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(unreadNotifications(data.totalUnread))\n      }\n    } catch (error) {\n      //\n    }\n  }\n}\n\nexport function setNewNotificationAction(data: NotificationsDto) {\n  return (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    dispatch(setNewNotificationReceived(data))\n    const toastNotification = state.user.settings.config?.agreements\n      ?.notifications\n      ? data.notifications[0]\n      : null\n    dispatch(setLastReceivedNotification(toastNotification))\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/plugins.ts",
    "content": "import { flatMap, isEmpty, reject } from 'lodash'\nimport { createSlice } from '@reduxjs/toolkit'\n\nimport {\n  getApiErrorMessage,\n  getUrl,\n  isStatusSuccessful,\n  multilineCommandToOneLine,\n} from 'uiSrc/utils'\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport {\n  CommandExecutionType,\n  IPlugin,\n  PluginsResponse,\n  StateAppPlugins,\n} from 'uiSrc/slices/interfaces'\nimport { SendCommandResponse } from 'apiSrc/modules/cli/dto/cli.dto'\nimport { PluginState } from 'apiSrc/modules/workbench/models/plugin-state'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateAppPlugins = {\n  loading: false,\n  error: '',\n  staticPath: '',\n  plugins: [],\n  visualizations: [],\n}\n\n// A slice for recipes\nconst appPluginsSlice = createSlice({\n  name: 'appPlugins',\n  initialState,\n  reducers: {\n    setAppPluginsInitialState: () => initialState,\n    getAllPlugins: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    getAllPluginsSuccess: (\n      state,\n      { payload }: { payload: PluginsResponse },\n    ) => {\n      state.loading = false\n      state.staticPath = payload?.static\n      state.plugins = reject(payload?.plugins, isEmpty)\n      state.visualizations = flatMap(\n        reject(payload?.plugins, isEmpty),\n        (plugin: IPlugin) =>\n          plugin.visualizations.map((view) => ({\n            ...view,\n            plugin: {\n              name: plugin.name,\n              baseUrl: plugin.baseUrl,\n              internal: plugin.internal,\n              stylesSrc: plugin.styles,\n              scriptSrc: plugin.main,\n            },\n            uniqId: `${plugin.name}__${view.id}`,\n          })),\n      )\n    },\n    getAllPluginsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setAppPluginsInitialState,\n  getAllPlugins,\n  getAllPluginsSuccess,\n  getAllPluginsFailure,\n} = appPluginsSlice.actions\n\n// Selectors\nexport const appPluginsSelector = (state: RootState) => state.app.plugins\n\n// The reducer\nexport default appPluginsSlice.reducer\n\n// Asynchronous thunk action\nexport function loadPluginsAction() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getAllPlugins())\n\n    try {\n      const { data, status } = await apiService.get(`${ApiEndpoints.PLUGINS}`)\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getAllPluginsSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getAllPluginsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function sendPluginCommandAction({\n  command = '',\n  executionType = CommandExecutionType.Workbench,\n  onSuccessAction,\n  onFailAction,\n}: {\n  command: string\n  executionType?: CommandExecutionType\n  onSuccessAction?: (responseData: any) => void\n  onFailAction?: (error: any) => void\n}) {\n  return async (_dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      const { data, status } = await apiService.post<SendCommandResponse>(\n        getUrl(id, ApiEndpoints.PLUGINS, ApiEndpoints.COMMAND_EXECUTIONS),\n        {\n          command: multilineCommandToOneLine(command),\n          type: executionType,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.(data)\n      }\n    } catch (error) {\n      onFailAction?.(error)\n    }\n  }\n}\n\nexport function getPluginStateAction({\n  visualizationId = '',\n  commandId = '',\n  onSuccessAction,\n  onFailAction,\n}: {\n  visualizationId: string\n  commandId: string\n  onSuccessAction?: (responseData: any) => void\n  onFailAction?: (error: any) => void\n}) {\n  return async (_dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      const { data, status } = await apiService.get<PluginState>(\n        getUrl(\n          id,\n          ApiEndpoints.PLUGINS,\n          visualizationId,\n          ApiEndpoints.COMMAND_EXECUTIONS,\n          commandId,\n          ApiEndpoints.STATE,\n        ),\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.(data)\n      }\n    } catch (error) {\n      onFailAction?.(error)\n    }\n  }\n}\n\nexport function setPluginStateAction({\n  visualizationId = '',\n  commandId = '',\n  pluginState,\n  onSuccessAction,\n  onFailAction,\n}: {\n  visualizationId: string\n  commandId: string\n  pluginState: any\n  onSuccessAction?: (responseData: any) => void\n  onFailAction?: (error: any) => void\n}) {\n  return async (_dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      const { data, status } = await apiService.post(\n        getUrl(\n          id,\n          ApiEndpoints.PLUGINS,\n          visualizationId,\n          ApiEndpoints.COMMAND_EXECUTIONS,\n          commandId,\n          ApiEndpoints.STATE,\n        ),\n        {\n          state: pluginState,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.(data)\n      }\n    } catch (error) {\n      onFailAction?.(error)\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/redis-commands.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { isString, uniqBy } from 'lodash'\nimport { apiService, resourcesService } from 'uiSrc/services'\nimport { ApiEndpoints, ICommand, ICommands } from 'uiSrc/constants'\nimport {\n  getApiErrorMessage,\n  isStatusSuccessful,\n  checkDeprecatedCommandGroup,\n} from 'uiSrc/utils'\nimport { getConfig } from 'uiSrc/config'\nimport { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport { StateAppRedisCommands } from '../interfaces'\n\nexport const commands = [\n  'main',\n  'redisearch',\n  'redisjson',\n  'redistimeseries',\n  'redisgraph',\n  'redisgears',\n  'redisbloom',\n]\n\nexport const initialState: StateAppRedisCommands = {\n  loading: false,\n  error: '',\n  spec: {},\n  commandsArray: [],\n  commandGroups: [],\n}\n\n// A slice for recipes\nconst appRedisCommandsSlice = createSlice({\n  name: 'appRedisCommands',\n  initialState,\n  reducers: {\n    getRedisCommands: (state) => {\n      state.loading = true\n    },\n    getRedisCommandsSuccess: (state, { payload }: { payload: ICommands }) => {\n      state.loading = false\n      state.spec = payload\n      state.commandsArray = Object.keys(state.spec).sort()\n      state.commandGroups = uniqBy(Object.values(payload), 'group')\n        .map((item: ICommand) => item.group)\n        .filter((group: string) => isString(group))\n        .filter((group: string) => !checkDeprecatedCommandGroup(group))\n    },\n    getRedisCommandsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  getRedisCommands,\n  getRedisCommandsSuccess,\n  getRedisCommandsFailure,\n} = appRedisCommandsSlice.actions\n\n// A selector\nexport const appRedisCommandsSelector = (state: RootState) =>\n  state.app.redisCommands\n\n// The reducer\nexport default appRedisCommandsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchRedisCommandsInfo(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getRedisCommands())\n\n    try {\n      const riConfig = getConfig()\n\n      if (riConfig.app.useLocalResources) {\n        const results = await Promise.all(\n          commands.map((command) =>\n            resourcesService.get<ICommand>(`/static/commands/${command}.json`),\n          ),\n        )\n        if (results.every(({ status }) => isStatusSuccessful(status))) {\n          const data: ICommands = results.reduce(\n            (obj, result) => ({\n              ...obj,\n              ...result.data,\n            }),\n            {},\n          )\n\n          dispatch(getRedisCommandsSuccess(data))\n          onSuccessAction?.()\n        }\n      } else {\n        const { data, status } = await apiService.get<GetServerInfoResponse>(\n          ApiEndpoints.REDIS_COMMANDS,\n        )\n        if (isStatusSuccessful(status)) {\n          dispatch(getRedisCommandsSuccess(data))\n          onSuccessAction?.()\n        }\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getRedisCommandsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/socket-connection.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { RootState } from '../store'\nimport { StateAppSocketConnection } from '../interfaces'\n\nexport const initialState: StateAppSocketConnection = {\n  isConnected: false,\n}\n\n// A slice for recipes\nconst appSocketConnectionSlice = createSlice({\n  name: 'appSocketConnection',\n  initialState,\n  reducers: {\n    setAppSocketConnectionInitialState: () => initialState,\n    setIsConnected: (state, { payload }) => {\n      state.isConnected = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const { setAppSocketConnectionInitialState, setIsConnected } =\n  appSocketConnectionSlice.actions\n\n// Selectors\nexport const appSocketConnectionSelector = (state: RootState) =>\n  state.app.socketConnection\n\n// The reducer\nexport default appSocketConnectionSlice.reducer\n"
  },
  {
    "path": "redisinsight/ui/src/slices/app/url-handling.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { RootState } from 'uiSrc/slices/store'\nimport { StateUrlHandling } from 'uiSrc/slices/interfaces/urlHandling'\nimport { localStorageService } from 'uiSrc/services'\nimport { AppStorageItem } from 'uiSrc/constants/storage'\n\nexport const initialState: StateUrlHandling = {\n  fromUrl: null,\n  returnUrl: localStorageService.get(AppStorageItem.returnUrl),\n  action: null,\n  dbConnection: null,\n  properties: {},\n}\n\nconst appUrlHandlingSlice = createSlice({\n  name: 'appUrlHandling',\n  initialState,\n  reducers: {\n    setUrlHandlingInitialState: () => initialState,\n    setFromUrl: (state, { payload }) => {\n      state.fromUrl = payload\n    },\n    setReturnUrl: (state, { payload }) => {\n      state.returnUrl = payload\n    },\n    setUrlDbConnection: (state, { payload }) => {\n      state.action = payload.action\n      state.dbConnection = payload.dbConnection\n    },\n    setUrlProperties: (state, { payload }) => {\n      state.properties = payload\n    },\n  },\n})\n\nexport const {\n  setUrlHandlingInitialState,\n  setFromUrl,\n  setReturnUrl,\n  setUrlDbConnection,\n  setUrlProperties,\n} = appUrlHandlingSlice.actions\n\nexport const appRedirectionSelector = (state: RootState) =>\n  state.app.urlHandling\n\nexport const appReturnUrlSelector = (state: RootState) =>\n  state.app.urlHandling.returnUrl\n\nexport default appUrlHandlingSlice.reducer\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/bulkActions.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport axios, { AxiosError } from 'axios'\nimport {\n  ApiEndpoints,\n  BulkActionsType,\n  KeyTypes,\n  MAX_BULK_ACTION_ERRORS_LENGTH,\n} from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getApiErrorMessage,\n  getUrl,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\n\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { AppDispatch, RootState } from '../store'\nimport { StateBulkActions, IBulkActionOverview } from '../interfaces'\n\nexport const initialState: StateBulkActions = {\n  isShowBulkActions: false,\n  loading: false,\n  error: '',\n  isConnected: false,\n  bulkDelete: {\n    isActionTriggered: false,\n    loading: false,\n    error: '',\n    overview: null,\n    generateReport: false,\n    filter: null,\n    search: '',\n    keyCount: null,\n  },\n  bulkUpload: {\n    loading: false,\n    error: '',\n    overview: null,\n    fileName: '',\n  },\n  selectedBulkAction: {\n    id: '',\n    type: null,\n  },\n}\n\n// A slice for recipes\nconst bulkActionsSlice = createSlice({\n  name: 'bulkActions',\n  initialState,\n  reducers: {\n    setBulkActionsInitialState: () => initialState,\n    setBulkDeleteStartAgain: (state) => {\n      state.bulkDelete = initialState.bulkDelete\n      state.isConnected = false\n    },\n    setBulkUploadStartAgain: (state) => {\n      state.bulkUpload = initialState.bulkUpload\n      state.isConnected = false\n    },\n    toggleBulkActions: (state) => {\n      state.isShowBulkActions = !state.isShowBulkActions\n    },\n    setBulkActionConnected: (state, { payload }: PayloadAction<boolean>) => {\n      state.isConnected = payload\n    },\n    setLoading: (state, { payload }: PayloadAction<boolean>) => {\n      state.loading = payload\n    },\n    setBulkDeleteLoading: (state, { payload }: PayloadAction<boolean>) => {\n      state.bulkDelete.loading = payload\n    },\n    setBulkActionType: (state, { payload }: PayloadAction<BulkActionsType>) => {\n      state.selectedBulkAction.type = payload\n    },\n    toggleBulkDeleteActionTriggered: (state) => {\n      state.bulkDelete.isActionTriggered = !state.bulkDelete.isActionTriggered\n    },\n    setBulkDeleteGenerateReport: (\n      state,\n      { payload }: PayloadAction<boolean>,\n    ) => {\n      state.bulkDelete.generateReport = payload\n    },\n    setBulkDeleteFilter: (\n      state,\n      { payload }: PayloadAction<Nullable<KeyTypes>>,\n    ) => {\n      state.bulkDelete.filter = payload\n    },\n    setBulkDeleteSearch: (state, { payload }: PayloadAction<string>) => {\n      state.bulkDelete.search = payload\n    },\n    setBulkDeleteKeyCount: (\n      state,\n      { payload }: PayloadAction<Nullable<number>>,\n    ) => {\n      state.bulkDelete.keyCount = payload\n    },\n    setDeleteOverview: (\n      state,\n      { payload }: PayloadAction<IBulkActionOverview>,\n    ) => {\n      let errors = state.bulkDelete.overview?.summary?.errors || []\n\n      errors = payload.summary?.errors\n        ?.concat(errors)\n        .slice(0, MAX_BULK_ACTION_ERRORS_LENGTH)\n      state.bulkDelete.overview = {\n        ...payload,\n        summary: {\n          ...payload.summary,\n          errors,\n        },\n      }\n    },\n    setDeleteOverviewStatus: (state, { payload }) => {\n      if (state.bulkDelete.overview) {\n        state.bulkDelete.overview.status = payload\n      }\n    },\n    disconnectBulkDeleteAction: (state) => {\n      state.bulkDelete.loading = false\n      state.bulkDelete.isActionTriggered = false\n      state.isConnected = false\n    },\n    // bulk delete\n    bulkDeleteSuccess: (state) => {\n      state.bulkDelete.loading = false\n    },\n    bulkUpload: (state) => {\n      state.bulkUpload.loading = true\n      state.bulkUpload.error = ''\n    },\n    bulkUploadSuccess: (\n      state,\n      {\n        payload,\n      }: PayloadAction<{ data: IBulkActionOverview; fileName?: string }>,\n    ) => {\n      state.bulkUpload.loading = false\n      state.bulkUpload.overview = payload.data\n      state.bulkUpload.fileName = payload.fileName\n    },\n    bulkUploadFailed: (state, { payload }: PayloadAction<Maybe<string>>) => {\n      state.bulkUpload.loading = false\n\n      if (payload) {\n        state.bulkUpload.error = payload\n      }\n    },\n    bulkImportDefaultData: (state) => {\n      state.loading = true\n    },\n    bulkImportDefaultDataSuccess: (state) => {\n      state.loading = false\n    },\n    bulkImportDefaultDataFailed: (state) => {\n      state.loading = false\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setLoading,\n  setBulkDeleteLoading,\n  setBulkDeleteStartAgain,\n  setBulkUploadStartAgain,\n  setBulkActionType,\n  setBulkActionConnected,\n  toggleBulkActions,\n  disconnectBulkDeleteAction,\n  toggleBulkDeleteActionTriggered,\n  setBulkDeleteGenerateReport,\n  setBulkDeleteFilter,\n  setBulkDeleteSearch,\n  setBulkDeleteKeyCount,\n  setDeleteOverview,\n  setDeleteOverviewStatus,\n  setBulkActionsInitialState,\n  bulkDeleteSuccess,\n  bulkUpload,\n  bulkUploadFailed,\n  bulkUploadSuccess,\n  bulkImportDefaultData,\n  bulkImportDefaultDataSuccess,\n  bulkImportDefaultDataFailed,\n} = bulkActionsSlice.actions\n\n// Selectors\nexport const bulkActionsSelector = (state: RootState) =>\n  state.browser.bulkActions\nexport const selectedBulkActionsSelector = (state: RootState) =>\n  state.browser.bulkActions?.selectedBulkAction\nexport const bulkActionsDeleteSelector = (state: RootState) =>\n  state.browser.bulkActions.bulkDelete\nexport const bulkActionsDeleteOverviewSelector = (state: RootState) =>\n  state.browser.bulkActions.bulkDelete.overview\nexport const bulkActionsDeleteSummarySelector = (state: RootState) =>\n  state.browser.bulkActions.bulkDelete.overview?.summary\n\nexport const bulkActionsUploadSelector = (state: RootState) =>\n  state.browser.bulkActions.bulkUpload\nexport const bulkActionsUploadOverviewSelector = (state: RootState) =>\n  state.browser.bulkActions.bulkUpload.overview\nexport const bulkActionsUploadSummarySelector = (state: RootState) =>\n  state.browser.bulkActions.bulkUpload.overview?.summary\n\n// The reducer\nexport default bulkActionsSlice.reducer\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let uploadController: Nullable<AbortController> = null\n\n// Thunk actions\n// Asynchronous thunk action\nexport function bulkUploadDataAction(\n  id: string,\n  uploadFile: { file: FormData; fileName: string },\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(bulkUpload())\n\n    try {\n      uploadController?.abort()\n      uploadController = new AbortController()\n\n      const { status, data } = await apiService.post(\n        getUrl(id, ApiEndpoints.BULK_ACTIONS_IMPORT),\n        uploadFile.file,\n        {\n          headers: {\n            Accept: 'application/json',\n            'Content-Type': 'multipart/form-data',\n          },\n          signal: uploadController.signal,\n        },\n      )\n\n      uploadController = null\n\n      if (isStatusSuccessful(status)) {\n        dispatch(bulkUploadSuccess({ data, fileName: uploadFile.fileName }))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      // show error when request wasn't aborted\n      if (!axios.isCancel(error)) {\n        const errorMessage = getApiErrorMessage(error as AxiosError)\n        dispatch(addErrorNotification(error as AxiosError))\n        dispatch(bulkUploadFailed(errorMessage))\n        onFailAction?.()\n      } else {\n        dispatch(bulkUploadFailed())\n      }\n    }\n  }\n}\n\nexport function bulkImportDefaultDataAction(\n  id: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(bulkImportDefaultData())\n\n    try {\n      const { status, data } = await apiService.post(\n        getUrl(id, ApiEndpoints.BULK_ACTIONS_IMPORT_DEFAULT_DATA),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(bulkImportDefaultDataSuccess())\n        dispatch(\n          addMessageNotification(\n            successMessages.UPLOAD_DATA_BULK(data as IBulkActionOverview),\n          ),\n        )\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      dispatch(addErrorNotification(error as AxiosError))\n      dispatch(bulkImportDefaultDataFailed())\n      onFailAction?.()\n    }\n  }\n}\n\nexport function resetBulkActions() {\n  return async (dispatch: AppDispatch) => {\n    uploadController?.abort()\n    dispatch(setBulkActionsInitialState())\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/hash.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { cloneDeep, remove, isNull } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints, KeyTypes } from 'uiSrc/constants'\nimport {\n  bufferToString,\n  getApiErrorMessage,\n  getUrl,\n  isEqualBuffers,\n  isStatusSuccessful,\n  Maybe,\n} from 'uiSrc/utils'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport {\n  GetHashFieldsResponse,\n  AddFieldsToHashDto,\n  UpdateHashFieldsTtlDto,\n} from 'apiSrc/modules/browser/hash/dto'\nimport {\n  deleteKeyFromList,\n  deleteSelectedKeySuccess,\n  fetchKeyInfo,\n  refreshKeyInfoAction,\n  updateSelectedKeyRefreshTime,\n} from './keys'\nimport { AppDispatch, RootState } from '../store'\nimport { HashField, RedisResponseBuffer, StateHash } from '../interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\n\nexport const initialState: StateHash = {\n  loading: false,\n  error: '',\n  data: {\n    total: 0,\n    key: undefined,\n    keyName: '',\n    fields: [],\n    nextCursor: 0,\n    match: '*',\n  },\n  updateValue: {\n    loading: false,\n    error: '',\n  },\n}\n\n// A slice for recipes\nconst hashSlice = createSlice({\n  name: 'hash',\n  initialState,\n  reducers: {\n    setHashInitialState: () => initialState,\n\n    setHashFields: (state, { payload }: PayloadAction<HashField[]>) => {\n      state.data.fields = payload\n    },\n\n    // load Hash fields\n    loadHashFields: (\n      state,\n      {\n        payload: [match = '', resetData = true],\n      }: PayloadAction<[string, Maybe<boolean>]>,\n    ) => {\n      state.loading = true\n      state.error = ''\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n\n      state.data = {\n        ...state.data,\n        match: match || '*',\n      }\n    },\n    loadHashFieldsSuccess: (\n      state,\n      { payload }: PayloadAction<GetHashFieldsResponse>,\n    ) => {\n      state.data = {\n        ...state.data,\n        ...payload,\n      }\n      state.data.key = payload.keyName\n      state.loading = false\n    },\n    loadHashFieldsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    // load more Hash fields for infinity scroll\n    loadMoreHashFields: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadMoreHashFieldsSuccess: (\n      state,\n      { payload: { fields, ...rest } }: PayloadAction<GetHashFieldsResponse>,\n    ) => {\n      state.loading = false\n      state.data = {\n        ...state.data,\n        ...rest,\n        fields: state.data?.fields?.concat(fields),\n      }\n    },\n    loadMoreHashFieldsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // Update Hash Value\n    updateValue: (state) => {\n      state.updateValue = {\n        ...state.updateValue,\n        loading: true,\n        error: '',\n      }\n    },\n    updateValueSuccess: (state) => {\n      state.updateValue = {\n        ...state.updateValue,\n        loading: false,\n      }\n    },\n    updateValueFailure: (state, { payload }) => {\n      state.updateValue = {\n        ...state.updateValue,\n        loading: false,\n        error: payload,\n      }\n    },\n    resetUpdateValue: (state) => {\n      state.updateValue = cloneDeep(initialState.updateValue)\n    },\n\n    // delete Hash fields\n    removeHashFields: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    removeHashFieldsSuccess: (state) => {\n      state.loading = false\n    },\n    removeHashFieldsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    removeFieldsFromList: (\n      state,\n      { payload }: { payload: RedisResponseBuffer[] },\n    ) => {\n      remove(\n        state.data?.fields,\n        ({ field }) =>\n          payload.findIndex((item) => isEqualBuffers(item, field)) > -1,\n      )\n\n      state.data = {\n        ...state.data,\n        total: state.data.total - 1,\n      }\n    },\n    updateFieldsInList: (state, { payload }: { payload: HashField[] }) => {\n      const newFieldsState = state.data.fields.map((listItem) => {\n        const index = payload.findIndex((item) =>\n          isEqualBuffers(item.field, listItem.field),\n        )\n        if (index > -1) {\n          return {\n            ...listItem,\n            ...payload[index],\n          }\n        }\n        return listItem\n      })\n\n      state.data = {\n        ...state.data,\n        fields: newFieldsState,\n      }\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setHashInitialState,\n  loadHashFields,\n  loadHashFieldsSuccess,\n  loadHashFieldsFailure,\n  loadMoreHashFields,\n  loadMoreHashFieldsSuccess,\n  loadMoreHashFieldsFailure,\n  removeHashFields,\n  removeHashFieldsSuccess,\n  removeHashFieldsFailure,\n  removeFieldsFromList,\n  updateValue,\n  updateValueSuccess,\n  updateValueFailure,\n  resetUpdateValue,\n  updateFieldsInList,\n  setHashFields,\n} = hashSlice.actions\n\n// Selectors\nexport const hashSelector = (state: RootState) => state.browser.hash\nexport const hashDataSelector = (state: RootState) => state.browser.hash?.data\nexport const updateHashValueStateSelector = (state: RootState) =>\n  state.browser.hash.updateValue\n\n// The reducer\nexport default hashSlice.reducer\n\n// Asynchronous thunk actions\nexport function fetchHashFields(\n  key: RedisResponseBuffer,\n  cursor: number,\n  count: number,\n  match: string,\n  resetData: boolean = true,\n  onSuccess?: (data: GetHashFieldsResponse) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadHashFields([isNull(match) ? '*' : match, resetData]))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetHashFieldsResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH_GET_FIELDS,\n        ),\n        {\n          keyName: key,\n          cursor,\n          count,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadHashFieldsSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n        onSuccess?.(data)\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadHashFieldsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function refreshHashFieldsAction(\n  key: RedisResponseBuffer,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { match } = state.browser.hash.data\n    const { encoding } = state.app.info\n    dispatch(loadHashFields([match || '*', resetData]))\n\n    try {\n      const { data, status } = await apiService.post<GetHashFieldsResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH_GET_FIELDS,\n        ),\n        {\n          keyName: key,\n          cursor: 0,\n          count: SCAN_COUNT_DEFAULT,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadHashFieldsSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadHashFieldsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function fetchMoreHashFields(\n  key: RedisResponseBuffer,\n  cursor: number,\n  count: number,\n  match: string,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreHashFields())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH_GET_FIELDS,\n        ),\n        {\n          keyName: key,\n          cursor,\n          count,\n          match: isNull(match) ? '*' : match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadMoreHashFieldsSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadMoreHashFieldsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function deleteHashFields(\n  key: RedisResponseBuffer,\n  fields: RedisResponseBuffer[],\n  onSuccessAction?: (newTotal?: number) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(removeHashFields())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status, data } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH_FIELDS,\n        ),\n        {\n          data: {\n            keyName: key,\n            fields,\n          },\n          params: { encoding },\n        },\n      )\n      const newTotalValue = state.browser.hash.data.total - data.affected\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.(newTotalValue)\n        dispatch(removeHashFieldsSuccess())\n        dispatch(removeFieldsFromList(fields))\n        if (newTotalValue > 0) {\n          dispatch<any>(refreshKeyInfoAction(key))\n          dispatch(\n            addMessageNotification(\n              successMessages.REMOVED_KEY_VALUE(\n                key,\n                fields.map((field) => bufferToString(field)).join(''),\n                'Field',\n              ),\n            ),\n          )\n        } else {\n          dispatch(deleteSelectedKeySuccess())\n          dispatch(deleteKeyFromList(key))\n          dispatch(addMessageNotification(successMessages.DELETED_KEY(key)))\n        }\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(removeHashFieldsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function addHashFieldsAction(\n  data: AddFieldsToHashDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateValue())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        if (onSuccessAction) {\n          onSuccessAction()\n        }\n        dispatch(updateValueSuccess())\n        dispatch<any>(fetchKeyInfo(data.keyName))\n      }\n    } catch (error) {\n      if (onFailAction) {\n        onFailAction()\n      }\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateValueFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateHashFieldsAction(\n  data: AddFieldsToHashDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateValue())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_EDITED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_EDITED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyType: KeyTypes.Hash,\n          },\n        })\n        dispatch(updateValueSuccess())\n        dispatch(updateFieldsInList(data.fields))\n        dispatch<any>(refreshKeyInfoAction(data.keyName))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateValueFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateHashTTLAction(\n  data: UpdateHashFieldsTtlDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateValue())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.patch(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HASH_TTL,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_FIELD_TTL_EDITED,\n            TelemetryEvent.TREE_VIEW_FIELD_TTL_EDITED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n          },\n        })\n        onSuccessAction?.()\n        dispatch(updateValueSuccess())\n\n        const key = data.keyName as RedisResponseBuffer\n        const isLastFieldAffected =\n          state.browser.hash.data.total - data.fields.length === 0\n        const isSetToZero =\n          data.fields.reduce((prev, current) => prev + current.expire, 0) === 0\n\n        if (isLastFieldAffected && isSetToZero) {\n          dispatch(deleteSelectedKeySuccess())\n          dispatch(deleteKeyFromList(key))\n          dispatch(addMessageNotification(successMessages.DELETED_KEY(key)))\n          return\n        }\n\n        if (isSetToZero) {\n          dispatch(\n            removeFieldsFromList(data.fields.map(({ field }) => field) as any),\n          )\n          return\n        }\n\n        dispatch(updateFieldsInList(data.fields as any))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateValueFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/keys.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { cloneDeep, remove, get, isUndefined } from 'lodash'\nimport axios, { AxiosError, CancelTokenSource } from 'axios'\nimport { apiService, localStorageService } from 'uiSrc/services'\nimport { getConfig } from 'uiSrc/config'\nimport {\n  ApiEndpoints,\n  BrowserStorageItem,\n  KeyTypes,\n  KeyValueFormat,\n  EndpointBasedOnKeyType,\n  ENDPOINT_BASED_ON_KEY_TYPE,\n  SearchHistoryMode,\n  SortOrder,\n  STRING_MAX_LENGTH,\n  ModulesKeyTypes,\n  BrowserColumns,\n} from 'uiSrc/constants'\nimport {\n  getApiErrorMessage,\n  isStatusNotFoundError,\n  Nullable,\n  parseKeysListResponse,\n  getUrl,\n  isStatusSuccessful,\n  Maybe,\n  bufferToString,\n  isEqualBuffers,\n  isRedisearchAvailable,\n} from 'uiSrc/utils'\nimport { DEFAULT_SEARCH_MATCH, SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n  getAdditionalAddedEventData,\n  getMatchType,\n} from 'uiSrc/telemetry'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { IFetchKeyArgs, IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport {\n  resetBrowserTree,\n  setBrowserSelectedKey,\n} from 'uiSrc/slices/app/context'\nimport { NamespaceSearchableResult } from 'uiSrc/slices/interfaces/keys'\n\nimport { CreateListWithExpireDto } from 'apiSrc/modules/browser/list/dto'\nimport { SetStringWithExpireDto } from 'apiSrc/modules/browser/string/dto'\nimport { CreateZSetWithExpireDto } from 'apiSrc/modules/browser/z-set/dto'\nimport { CreateHashWithExpireDto } from 'apiSrc/modules/browser/hash/dto'\nimport { CreateRejsonRlWithExpireDto } from 'apiSrc/modules/browser/rejson-rl/dto'\nimport { CreateSetWithExpireDto } from 'apiSrc/modules/browser/set/dto'\nimport {\n  GetKeyInfoResponse,\n  GetKeysWithDetailsResponse,\n} from 'apiSrc/modules/browser/keys/dto'\nimport { CreateStreamDto } from 'apiSrc/modules/browser/stream/dto'\n\nimport { fetchString } from './string'\nimport {\n  setZsetInitialState,\n  fetchZSetMembers,\n  refreshZsetMembersAction,\n} from './zset'\nimport { fetchSetMembers, refreshSetMembersAction } from './set'\nimport { fetchReJSON, setEditorType, setIsWithinThreshold } from './rejson'\nimport {\n  setHashInitialState,\n  fetchHashFields,\n  refreshHashFieldsAction,\n} from './hash'\nimport {\n  setListInitialState,\n  fetchListElements,\n  refreshListElementsAction,\n} from './list'\nimport {\n  fetchStreamEntries,\n  refreshStream,\n  setStreamInitialState,\n} from './stream'\nimport {\n  deleteRedisearchHistoryAction,\n  deleteRedisearchKeyFromList,\n  editRedisearchKeyFromList,\n  editRedisearchKeyTTLFromList,\n  fetchMoreRedisearchKeysAction,\n  fetchRedisearchHistoryAction,\n  fetchRedisearchKeysAction,\n  resetRedisearchKeysData,\n  setLastBatchRedisearchKeys,\n  setQueryRedisearch,\n} from './redisearch'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\nimport {\n  KeysStore,\n  KeyViewType,\n  SearchHistoryItem,\n  SearchMode,\n} from '../interfaces/keys'\nimport { AppDispatch, RootState } from '../store'\nimport { StreamViewType } from '../interfaces/stream'\nimport { EditorType, RedisResponseBuffer, RedisString } from '../interfaces'\n\nconst riConfig = getConfig()\n\nconst defaultViewFormat = KeyValueFormat.Unicode\n\nexport const initialState: KeysStore = {\n  loading: false,\n  deleting: false,\n  error: '',\n  filter: null,\n  search: '',\n  isSearched: false,\n  isFiltered: false,\n  isBrowserFullScreen: false,\n  searchMode:\n    localStorageService?.get(BrowserStorageItem.browserSearchMode) ??\n    SearchMode.Pattern,\n  viewType:\n    localStorageService?.get(BrowserStorageItem.browserViewType) ??\n    KeyViewType.Tree,\n  data: {\n    total: 0,\n    scanned: 0,\n    nextCursor: '0',\n    keys: [],\n    shardsMeta: {},\n    previousResultCount: 0,\n    lastRefreshTime: null,\n  },\n  selectedKey: {\n    loading: false,\n    refreshing: false,\n    isRefreshDisabled: false,\n    lastRefreshTime: null,\n    error: '',\n    data: null,\n    length: 0,\n    viewFormat:\n      localStorageService?.get(BrowserStorageItem.viewFormat) ??\n      defaultViewFormat,\n    compressor: null,\n  },\n  addKey: {\n    loading: false,\n    error: '',\n  },\n  searchHistory: {\n    data: null,\n    loading: false,\n  },\n}\n\nexport const initialKeyInfo = {\n  ttl: -1,\n  name: null,\n  nameString: null,\n  type: KeyTypes.String,\n  size: 1,\n  length: 0,\n}\n\nconst getInitialSelectedKeyState = (state: KeysStore) => ({\n  ...initialState.selectedKey,\n  viewFormat: state.selectedKey.viewFormat,\n})\n\n// A slice for recipes\nconst keysSlice = createSlice({\n  name: 'keys',\n  initialState,\n  reducers: {\n    // load Keys\n    loadKeys: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadKeysSuccess: (state, { payload: { data, isSearched, isFiltered } }) => {\n      state.data = {\n        ...data,\n        previousResultCount: data.keys?.length,\n      }\n      state.loading = false\n      state.isSearched = isSearched\n      state.isFiltered = isFiltered\n      state.data.lastRefreshTime = Date.now()\n    },\n    loadKeysFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // load more Keys for infinity scroll\n    loadMoreKeys: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadMoreKeysSuccess: (\n      state,\n      { payload: { total, scanned, nextCursor, keys, shardsMeta } },\n    ) => {\n      state.data.keys = keys\n      state.data.total = total\n      state.data.scanned = scanned\n      state.data.nextCursor = nextCursor\n      state.data.shardsMeta = shardsMeta\n      state.data.previousResultCount = keys.length\n\n      state.loading = false\n    },\n    loadMoreKeysFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    setLastBatchPatternKeys: (state, { payload }) => {\n      const newKeys = state.data.keys\n      newKeys.splice(-payload.length, payload.length, ...payload)\n      state.data.keys = newKeys\n    },\n\n    loadKeyInfoSuccess: (state, { payload }) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: false,\n        isRefreshDisabled: false,\n        data: {\n          ...payload,\n          nameString: bufferToString(payload.name),\n        },\n      }\n    },\n    refreshKeyInfo: (state) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        refreshing: true,\n      }\n    },\n    refreshKeyInfoSuccess: (state, { payload }) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        data: { ...state.selectedKey.data, ...payload },\n        refreshing: false,\n      }\n    },\n    refreshKeyInfoFail: (state) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        refreshing: false,\n      }\n    },\n    updateSelectedKeyRefreshTime: (state, { payload }) => {\n      state.selectedKey.lastRefreshTime = payload\n    },\n    // delete selected Key\n    deleteSelectedKey: (state) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: true,\n        error: '',\n      }\n    },\n    deleteSelectedKeySuccess: (state) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: false,\n        data: null,\n      }\n    },\n    deleteSelectedKeyFailure: (state, { payload }) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: false,\n        error: payload,\n      }\n    },\n    // delete Key\n    deleteKey: (state) => {\n      state.deleting = true\n    },\n    deleteKeySuccess: (state) => {\n      state.deleting = false\n    },\n    deleteKeyFailure: (state) => {\n      state.deleting = false\n    },\n    deletePatternKeyFromList: (state, { payload }) => {\n      remove(state.data?.keys, (key) =>\n        isEqualBuffers(key.name as RedisResponseBuffer, payload),\n      )\n\n      state.data = {\n        ...state.data,\n        total: Math.max(state.data.total - 1, 0),\n        scanned: state.data.scanned - 1,\n      }\n    },\n    deleteKeysByPattern: (\n      state,\n      {\n        payload,\n      }: PayloadAction<{\n        pattern: string\n        deletedCount: number\n        filterType?: Nullable<KeyTypes>\n      }>,\n    ) => {\n      const { pattern, deletedCount, filterType } = payload\n\n      // Only handle simple prefix patterns (e.g., \"folder:*\")\n      const isSimplePrefix =\n        pattern.endsWith('*') &&\n        !pattern.slice(0, -1).includes('*') &&\n        !pattern.includes('?')\n\n      if (!isSimplePrefix) return\n\n      const prefix = pattern.slice(0, -1)\n\n      // Skip local key removal for '*' pattern (all keys) - let user refresh instead\n      if (!prefix) {\n        state.data.total = Math.max(state.data.total - deletedCount, 0)\n        return\n      }\n      const encoder = new TextEncoder()\n      const prefixBytes = encoder.encode(prefix)\n\n      const bufferStartsWith = (\n        bufferData: number[],\n        prefixBytes: Uint8Array,\n      ): boolean => {\n        if (bufferData.length < prefixBytes.length) return false\n        for (let i = 0; i < prefixBytes.length; i++) {\n          if (bufferData[i] !== prefixBytes[i]) return false\n        }\n        return true\n      }\n\n      state.data.keys = state.data.keys.filter((key: any) => {\n        const bufferData = key.name?.data\n        if (!bufferData) return true\n        if (!bufferStartsWith(bufferData, prefixBytes)) return true\n        if (filterType && key.type !== filterType) return true\n        return false\n      })\n\n      state.data.total = Math.max(state.data.total - deletedCount, 0)\n    },\n\n    // edit TTL or Key actions\n    defaultSelectedKeyAction: (state) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: true,\n        error: '',\n      }\n    },\n    defaultSelectedKeyActionSuccess: (state) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: false,\n        // data: null,\n      }\n    },\n    defaultSelectedKeyActionFailure: (state, { payload }) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        loading: false,\n        error: payload,\n      }\n    },\n    editPatternKeyFromList: (state, { payload }) => {\n      const keys = state.data.keys.map((key) => {\n        if (isEqualBuffers(key.name as RedisResponseBuffer, payload?.key)) {\n          return {\n            ...key,\n            name: payload?.newKey,\n            nameString: bufferToString(payload?.newKey),\n          }\n        }\n        return key\n      })\n\n      state.data = {\n        ...state.data,\n        keys,\n      }\n    },\n\n    editPatternKeyTTLFromList: (\n      state,\n      { payload: [key, ttl] }: PayloadAction<[RedisResponseBuffer, number]>,\n    ) => {\n      const keys = state.data.keys.map((keyData) => {\n        if (isEqualBuffers(keyData.name as RedisResponseBuffer, key)) {\n          keyData.ttl = ttl\n        }\n        return keyData\n      })\n\n      state.data = {\n        ...state.data,\n        keys,\n      }\n    },\n\n    // update length for Selected Key\n    updateSelectedKeyLength: (state, { payload }) => {\n      state.selectedKey = {\n        ...state.selectedKey,\n        data: {\n          ...state.selectedKey.data,\n          length: payload,\n        },\n      }\n    },\n\n    // Add Key\n    addKey: (state) => {\n      state.addKey = {\n        ...state.addKey,\n        loading: true,\n        error: '',\n      }\n    },\n    addKeySuccess: (state) => {\n      state.addKey = {\n        ...state.addKey,\n        loading: false,\n        error: '',\n      }\n    },\n    updateKeyList: (state, { payload }) => {\n      state.data?.keys.unshift({ name: payload.keyName })\n\n      state.data = {\n        ...state.data,\n        total: state.data.total + 1,\n        scanned: state.data.scanned + 1,\n      }\n    },\n    addKeyFailure: (state, { payload }) => {\n      state.addKey = {\n        ...state.addKey,\n        loading: false,\n        error: payload,\n      }\n    },\n\n    setPatternSearchMatch: (state, { payload }) => {\n      state.search = payload\n    },\n    setFilter: (state, { payload }) => {\n      state.filter = payload\n    },\n\n    changeKeyViewType: (state, { payload }: { payload: KeyViewType }) => {\n      state.viewType = payload\n      localStorageService?.set(BrowserStorageItem.browserViewType, payload)\n    },\n\n    changeSearchMode: (state, { payload }: { payload: SearchMode }) => {\n      state.searchMode = payload\n    },\n\n    resetAddKey: (state) => {\n      state.addKey = cloneDeep(initialState.addKey)\n    },\n\n    resetKeyInfo: (state) => {\n      state.selectedKey = cloneDeep(\n        getInitialSelectedKeyState(state as KeysStore),\n      )\n    },\n\n    // reset keys for keys slice\n    resetKeys: (state) =>\n      cloneDeep({\n        ...initialState,\n        viewType:\n          localStorageService?.get(BrowserStorageItem.browserViewType) ??\n          KeyViewType.Tree,\n        searchMode:\n          localStorageService?.get(BrowserStorageItem.browserSearchMode) ??\n          SearchMode.Pattern,\n        selectedKey: getInitialSelectedKeyState(state as KeysStore),\n      }),\n\n    resetPatternKeysData: (state) => {\n      // state.data.keys = []\n      state.data.total = 0\n      state.data.scanned = 0\n      state.data.keys.length = 0\n    },\n\n    toggleBrowserFullScreen: (\n      state,\n      { payload }: { payload: Maybe<boolean> },\n    ) => {\n      if (!isUndefined(payload)) {\n        state.isBrowserFullScreen = payload\n        return\n      }\n      state.isBrowserFullScreen = !state.isBrowserFullScreen\n    },\n\n    setViewFormat: (state, { payload }: PayloadAction<KeyValueFormat>) => {\n      state.selectedKey.viewFormat = payload\n      localStorageService?.set(BrowserStorageItem.viewFormat, payload)\n    },\n    loadSearchHistory: (state) => {\n      state.searchHistory.loading = true\n    },\n    loadSearchHistorySuccess: (\n      state,\n      { payload }: PayloadAction<SearchHistoryItem[]>,\n    ) => {\n      state.searchHistory.loading = false\n      state.searchHistory.data = payload\n    },\n    loadSearchHistoryFailure: (state) => {\n      state.searchHistory.loading = false\n    },\n    deleteSearchHistory: (state) => {\n      state.searchHistory.loading = true\n    },\n    deleteSearchHistorySuccess: (state, { payload }: { payload: string[] }) => {\n      state.searchHistory.loading = false\n      if (state.searchHistory.data) {\n        remove(state.searchHistory.data, (item) => payload.includes(item.id))\n      }\n    },\n    deleteSearchHistoryFailure: (state) => {\n      state.searchHistory.loading = false\n    },\n    setSelectedKeyRefreshDisabled: (\n      state,\n      { payload }: PayloadAction<boolean>,\n    ) => {\n      state.selectedKey.isRefreshDisabled = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadKeys,\n  loadKeysSuccess,\n  loadKeysFailure,\n  loadMoreKeys,\n  loadMoreKeysSuccess,\n  loadMoreKeysFailure,\n  loadKeyInfoSuccess,\n  updateSelectedKeyRefreshTime,\n  refreshKeyInfo,\n  refreshKeyInfoSuccess,\n  refreshKeyInfoFail,\n  defaultSelectedKeyAction,\n  defaultSelectedKeyActionSuccess,\n  defaultSelectedKeyActionFailure,\n  setLastBatchPatternKeys,\n  addKey,\n  updateKeyList,\n  addKeyFailure,\n  addKeySuccess,\n  resetAddKey,\n  deleteSelectedKey,\n  deleteSelectedKeySuccess,\n  deleteSelectedKeyFailure,\n  deletePatternKeyFromList,\n  deleteKeysByPattern,\n  deleteKey,\n  deleteKeySuccess,\n  deleteKeyFailure,\n  editPatternKeyFromList,\n  editPatternKeyTTLFromList,\n  setPatternSearchMatch,\n  setFilter,\n  changeKeyViewType,\n  resetKeyInfo,\n  resetKeys,\n  resetPatternKeysData,\n  toggleBrowserFullScreen,\n  setViewFormat,\n  changeSearchMode,\n  loadSearchHistory,\n  loadSearchHistorySuccess,\n  loadSearchHistoryFailure,\n  deleteSearchHistory,\n  deleteSearchHistorySuccess,\n  deleteSearchHistoryFailure,\n  setSelectedKeyRefreshDisabled,\n} = keysSlice.actions\n\n// A selector\nexport const keysSelector = (state: RootState) => state.browser.keys\nexport const keysDataSelector = (state: RootState) => state.browser.keys.data\nexport const selectedKeySelector = (state: RootState) =>\n  state.browser.keys?.selectedKey\nexport const selectedKeyDataSelector = (state: RootState) =>\n  state.browser.keys?.selectedKey?.data\nexport const addKeyStateSelector = (state: RootState) =>\n  state.browser.keys?.addKey\nexport const keysSearchHistorySelector = (state: RootState) =>\n  state.browser.keys.searchHistory\n\n// The reducer\nexport default keysSlice.reducer\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let sourceKeysFetch: Nullable<CancelTokenSource> = null\n\nexport function setInitialStateByType(type: string) {\n  return (dispatch: AppDispatch) => {\n    if (type === KeyTypes.Hash) {\n      dispatch(setHashInitialState())\n    }\n    if (type === KeyTypes.List) {\n      dispatch(setListInitialState())\n    }\n    if (type === KeyTypes.ZSet) {\n      dispatch(setZsetInitialState())\n    }\n    if (type === KeyTypes.Stream) {\n      dispatch(setStreamInitialState())\n    }\n  }\n}\n// Asynchronous thunk action\nexport function fetchPatternKeysAction(\n  cursor: string,\n  count: number,\n  telemetryProperties: { [key: string]: any } = {},\n  onSuccess?: (data: GetKeysWithDetailsResponse[]) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadKeys())\n\n    try {\n      sourceKeysFetch?.cancel?.()\n\n      const { CancelToken } = axios\n      sourceKeysFetch = CancelToken.source()\n\n      const state = stateInit()\n      const scanThreshold =\n        state.user.settings.config?.scanThreshold || count || SCAN_COUNT_DEFAULT\n      const { search: match, filter: type } = state.browser.keys\n      const { encoding } = state.app.info\n\n      const { data, status } = await apiService.post<\n        GetKeysWithDetailsResponse[]\n      >(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEYS,\n        ),\n        {\n          cursor,\n          count,\n          type,\n          match: match || DEFAULT_SEARCH_MATCH,\n          keysInfo: false,\n          scanThreshold,\n        },\n        {\n          params: { encoding },\n          cancelToken: sourceKeysFetch.token,\n        },\n      )\n\n      sourceKeysFetch = null\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          loadKeysSuccess({\n            data: parseKeysListResponse({}, data as never[]),\n            isSearched: !!match,\n            isFiltered: !!type,\n          }),\n        )\n        if (!!type || !!match) {\n          let matchValue = '*'\n          if (match !== '*' && !!match) {\n            matchValue = getMatchType(match)\n          }\n          sendEventTelemetry({\n            event: getBasedOnViewTypeEvent(\n              localStorageService?.get(BrowserStorageItem.browserViewType),\n              TelemetryEvent.BROWSER_KEYS_SCANNED_WITH_FILTER_ENABLED,\n              TelemetryEvent.TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED,\n            ),\n            eventData: {\n              databaseId: state.connections.instances?.connectedInstance?.id,\n              keyType: type,\n              match: matchValue,\n              databaseSize: data[0].total,\n              numberOfKeysScanned: data[0].scanned,\n              scanCount: count,\n              source: telemetryProperties.source ?? 'manual',\n              ...telemetryProperties,\n            },\n          })\n        }\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      if (!axios.isCancel(error)) {\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadKeysFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchMorePatternKeysAction(\n  oldKeys: IKeyPropTypes[] = [],\n  cursor: string,\n  count: number,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreKeys())\n\n    try {\n      sourceKeysFetch?.cancel?.()\n\n      const { CancelToken } = axios\n      sourceKeysFetch = CancelToken.source()\n\n      const state = stateInit()\n      const scanThreshold =\n        state.user.settings.config?.scanThreshold ?? SCAN_COUNT_DEFAULT\n      const { search: match, filter: type } = state.browser.keys\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEYS,\n        ),\n        {\n          cursor,\n          count,\n          type,\n          match: match || DEFAULT_SEARCH_MATCH,\n          keysInfo: false,\n          scanThreshold,\n        },\n        {\n          params: { encoding },\n          cancelToken: sourceKeysFetch.token,\n        },\n      )\n\n      sourceKeysFetch = null\n      if (isStatusSuccessful(status)) {\n        const newKeysData = parseKeysListResponse(\n          state.browser.keys.data.shardsMeta,\n          data,\n        )\n        dispatch(\n          loadMoreKeysSuccess({\n            ...newKeysData,\n            keys: oldKeys.concat(newKeysData.keys),\n          }),\n        )\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEYS_ADDITIONALLY_SCANNED,\n            TelemetryEvent.TREE_VIEW_KEYS_ADDITIONALLY_SCANNED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            databaseSize: data[0].total,\n            numberOfKeysScanned:\n              state.browser.keys.data.scanned + data[0].scanned,\n            scanCount: count,\n          },\n        })\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      if (!axios.isCancel(error)) {\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadMoreKeysFailure(errorMessage))\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchKeyInfo(\n  key: RedisResponseBuffer,\n  resetData?: boolean,\n  onSuccess?: (data: Nullable<IKeyPropTypes>) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(defaultSelectedKeyAction())\n    const { shownColumns } = stateInit()?.app?.context?.dbConfig\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEY_INFO,\n        ),\n        {\n          keyName: key,\n          includeSize: shownColumns.includes(BrowserColumns.Size),\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadKeyInfoSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n        onSuccess?.(data)\n      }\n\n      if (data.type === KeyTypes.Hash) {\n        dispatch<any>(\n          fetchHashFields(key, 0, SCAN_COUNT_DEFAULT, '*', resetData),\n        )\n      }\n      if (data.type === KeyTypes.List) {\n        dispatch<any>(fetchListElements(key, 0, SCAN_COUNT_DEFAULT, resetData))\n      }\n      if (data.type === KeyTypes.String) {\n        dispatch<any>(\n          fetchString(key, {\n            resetData,\n            end: STRING_MAX_LENGTH,\n          }),\n        )\n      }\n      if (data.type === KeyTypes.ZSet) {\n        dispatch<any>(\n          fetchZSetMembers(\n            key,\n            0,\n            SCAN_COUNT_DEFAULT,\n            SortOrder.ASC,\n            resetData,\n          ),\n        )\n      }\n      if (data.type === KeyTypes.Set) {\n        dispatch<any>(\n          fetchSetMembers(key, 0, SCAN_COUNT_DEFAULT, '*', resetData),\n        )\n      }\n      if (data.type === KeyTypes.ReJSON) {\n        dispatch<any>(fetchReJSON(key, '$', data.length, resetData))\n        dispatch<any>(setEditorType(EditorType.Default))\n        dispatch<any>(\n          setIsWithinThreshold(\n            data.size <= riConfig.browser.rejsonMonacoEditorMaxThreshold,\n          ),\n        )\n      }\n      if (data.type === KeyTypes.Stream) {\n        const { viewType } = state.browser.stream\n\n        if (viewType === StreamViewType.Data) {\n          dispatch<any>(\n            fetchStreamEntries(\n              key,\n              SCAN_COUNT_DEFAULT,\n              SortOrder.DESC,\n              resetData,\n            ),\n          )\n        }\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(defaultSelectedKeyActionFailure(errorMessage))\n      const status = get(error, ['response', 'status'])\n      if (status && isStatusNotFoundError(status)) {\n        dispatch(resetKeyInfo())\n        dispatch(setBrowserSelectedKey(null))\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function refreshKeyInfoAction(key: RedisResponseBuffer) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const { shownColumns } = stateInit()?.app?.context?.dbConfig\n    dispatch(refreshKeyInfo())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEY_INFO,\n        ),\n        {\n          keyName: key,\n          includeSize: shownColumns.includes(BrowserColumns.Size),\n        },\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(refreshKeyInfoSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(refreshKeyInfoFail())\n      dispatch(addErrorNotification(error))\n      const status = get(error, ['response', 'status'])\n      if (status && isStatusNotFoundError(status)) {\n        dispatch(resetKeyInfo())\n        dispatch(deleteKeyFromList(key))\n        dispatch(setBrowserSelectedKey(null))\n      }\n    }\n  }\n}\n\nfunction addTypedKey(\n  data: any,\n  keyType: KeyTypes,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(addKey())\n    const endpoint =\n      ENDPOINT_BASED_ON_KEY_TYPE[keyType as EndpointBasedOnKeyType]\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.post(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          endpoint,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        const additionalData = getAdditionalAddedEventData(endpoint, data)\n        if (onSuccessAction) {\n          onSuccessAction()\n        }\n        dispatch(addKeySuccess())\n        dispatch<any>(addKeyIntoList({ key: data.keyName, keyType }))\n        dispatch(\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        )\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_ADDED,\n            TelemetryEvent.TREE_VIEW_KEY_ADDED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            ...additionalData,\n          },\n        })\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      if (onFailAction) {\n        onFailAction()\n      }\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(addKeyFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function addHashKey(\n  data: CreateHashWithExpireDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.Hash, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function addZsetKey(\n  data: CreateZSetWithExpireDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.ZSet, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function addSetKey(\n  data: CreateSetWithExpireDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.Set, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function addStringKey(\n  data: SetStringWithExpireDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.String, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function addListKey(\n  data: CreateListWithExpireDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.List, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function addReJSONKey(\n  data: CreateRejsonRlWithExpireDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.ReJSON, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function addStreamKey(\n  data: CreateStreamDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return addTypedKey(data, KeyTypes.Stream, onSuccessAction, onFailAction)\n}\n\n// Asynchronous thunk action\nexport function deleteSelectedKeyAction(\n  key: RedisResponseBuffer,\n  onSuccessAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteSelectedKey())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEYS,\n        ),\n        {\n          data: { keyNames: [key] },\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteSelectedKeySuccess())\n        dispatch<any>(deleteKeyFromList(key))\n        onSuccessAction?.()\n        dispatch(addMessageNotification(successMessages.DELETED_KEY(key)))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(addErrorNotification(error as AxiosError))\n      dispatch(deleteSelectedKeyFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteKeyAction(\n  key: RedisResponseBuffer,\n  onSuccessAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteKey())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEYS,\n        ),\n        {\n          data: { keyNames: [key] },\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteKeySuccess())\n        dispatch<any>(deleteKeyFromList(key))\n        onSuccessAction?.()\n        dispatch(addMessageNotification(successMessages.DELETED_KEY(key)))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(addErrorNotification(error as AxiosError))\n      dispatch(deleteKeyFailure())\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function editKey(\n  key: RedisResponseBuffer,\n  newKey: RedisResponseBuffer,\n  onSuccess?: () => void,\n  onFailure?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(defaultSelectedKeyAction())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.patch(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEY_NAME,\n        ),\n        { keyName: key, newKeyName: newKey },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch<any>(editKeyFromList({ key, newKey }))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(defaultSelectedKeyActionFailure(errorMessage))\n      onFailure?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function editKeyTTL(key: RedisResponseBuffer, ttl: number) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(defaultSelectedKeyAction())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.patch(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id ?? '',\n          ApiEndpoints.KEY_TTL,\n        ),\n        { keyName: key, ttl },\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_TTL_CHANGED,\n            TelemetryEvent.TREE_VIEW_KEY_TTL_CHANGED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            ttl: ttl >= 0 ? ttl : -1,\n            previousTTL: state.browser.keys.selectedKey?.data?.ttl,\n          },\n        })\n        if (ttl !== 0) {\n          dispatch<any>(editKeyTTLFromList([key, ttl]))\n          dispatch<any>(fetchKeyInfo(key))\n        } else {\n          dispatch(deleteSelectedKeySuccess())\n          dispatch<any>(deleteKeyFromList(key))\n        }\n        dispatch(defaultSelectedKeyActionSuccess())\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(defaultSelectedKeyActionFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchKeysMetadata(\n  keys: RedisString[],\n  filter: Nullable<KeyTypes>,\n  signal?: AbortSignal,\n  onSuccessAction?: (data: GetKeyInfoResponse[]) => void,\n  onFailAction?: () => void,\n) {\n  return async (_dispatch: AppDispatch, stateInit: () => RootState) => {\n    const { shownColumns } = stateInit()?.app?.context?.dbConfig\n    try {\n      const state = stateInit()\n      const { data } = await apiService.post<GetKeyInfoResponse[]>(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id,\n          ApiEndpoints.KEYS_METADATA,\n        ),\n        {\n          keys,\n          type: filter || undefined,\n          includeSize: shownColumns.includes(BrowserColumns.Size),\n          includeTTL: shownColumns.includes(BrowserColumns.TTL),\n        },\n        { params: { encoding: state.app.info.encoding }, signal },\n      )\n\n      onSuccessAction?.(data)\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        onFailAction?.()\n        console.error(error)\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchKeysMetadataTree(\n  keys: RedisString[],\n  filter: Nullable<KeyTypes>,\n  signal?: AbortSignal,\n  onSuccessAction?: (data: GetKeyInfoResponse[]) => void,\n  onFailAction?: () => void,\n) {\n  return async (_dispatch: AppDispatch, stateInit: () => RootState) => {\n    const { shownColumns } = stateInit()?.app?.context?.dbConfig\n    try {\n      const state = stateInit()\n      const { data } = await apiService.post<GetKeyInfoResponse[]>(\n        getUrl(\n          state.connections.instances?.connectedInstance?.id,\n          ApiEndpoints.KEYS_METADATA,\n        ),\n        {\n          keys: keys.map(([, nameBuffer]) => nameBuffer),\n          type: filter || undefined,\n          includeSize: shownColumns.includes(BrowserColumns.Size),\n          includeTTL: shownColumns.includes(BrowserColumns.TTL),\n        },\n        { params: { encoding: state.app.info.encoding }, signal },\n      )\n\n      const newData = data.map((key, i) => ({ ...key, path: keys[i][0] }))\n\n      onSuccessAction?.(newData)\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        onFailAction?.()\n        console.error(error)\n      }\n    }\n  }\n}\n\nexport function fetchNamespaceSearchable(\n  prefixes: [string, string][],\n  signal?: AbortSignal,\n  onSuccessAction?: (data: NamespaceSearchableResult[]) => void,\n  onFailAction?: () => void,\n) {\n  return async (_dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n\n    try {\n      const { data, status } = await apiService.post<\n        NamespaceSearchableResult[]\n      >(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.KEYS_NAMESPACE_SEARCHABLE,\n        ),\n        { prefixes: prefixes.map(([, prefix]) => prefix) },\n        { signal },\n      )\n\n      if (isStatusSuccessful(status)) {\n        const results = data.map((item, i) => ({\n          ...item,\n          path: prefixes[i][0],\n        }))\n\n        onSuccessAction?.(results)\n      }\n    } catch (_err) {\n      if (axios.isCancel(_err)) return\n\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchPatternHistoryAction(\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadSearchHistory())\n\n    try {\n      const state = stateInit()\n      const { data, status } = await apiService.get<SearchHistoryItem[]>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HISTORY,\n        ),\n        {\n          params: {\n            mode: SearchHistoryMode.Pattern,\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadSearchHistorySuccess(data))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      dispatch(loadSearchHistoryFailure())\n      onFailed?.()\n    }\n  }\n}\n\nexport function deletePatternHistoryAction(\n  ids: string[],\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteSearchHistory())\n\n    try {\n      const state = stateInit()\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HISTORY,\n        ),\n        {\n          data: {\n            ids,\n          },\n          params: {\n            mode: SearchHistoryMode.Pattern,\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteSearchHistorySuccess(ids))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      dispatch(deleteSearchHistoryFailure())\n      onFailed?.()\n    }\n  }\n}\n\nexport function fetchSearchHistoryAction(\n  searchMode: SearchMode,\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch) =>\n    searchMode === SearchMode.Pattern\n      ? dispatch<any>(fetchPatternHistoryAction(onSuccess, onFailed))\n      : dispatch<any>(fetchRedisearchHistoryAction(onSuccess, onFailed))\n}\n\nexport function deleteSearchHistoryAction(\n  searchMode: SearchMode,\n  ids: string[],\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch) =>\n    searchMode === SearchMode.Pattern\n      ? dispatch<any>(deletePatternHistoryAction(ids, onSuccess, onFailed))\n      : dispatch<any>(deleteRedisearchHistoryAction(ids, onSuccess, onFailed))\n}\n\n// Asynchronous thunk action\nexport function fetchKeys(\n  options: {\n    searchMode: SearchMode\n    cursor: string\n    count: number\n    telemetryProperties?: {}\n  },\n  onSuccess?: (\n    data: GetKeysWithDetailsResponse[] | GetKeysWithDetailsResponse,\n  ) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const isRedisearchExists = isRedisearchAvailable(\n      state.connections.instances.connectedInstance.modules,\n    )\n    const { searchMode, count, cursor, telemetryProperties } = options\n\n    return searchMode === SearchMode.Pattern || !isRedisearchExists\n      ? dispatch<any>(\n          fetchPatternKeysAction(\n            cursor,\n            count,\n            telemetryProperties,\n            onSuccess,\n            onFailed,\n          ),\n        )\n      : dispatch<any>(\n          fetchRedisearchKeysAction(\n            cursor,\n            count,\n            telemetryProperties,\n            onSuccess,\n            onFailed,\n          ),\n        )\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchMoreKeys(\n  searchMode: SearchMode,\n  oldKeys: IKeyPropTypes[] = [],\n  cursor: string,\n  count: number,\n) {\n  return searchMode === SearchMode.Pattern\n    ? fetchMorePatternKeysAction(oldKeys, cursor, count)\n    : fetchMoreRedisearchKeysAction(oldKeys, cursor, count)\n}\n\nexport function setLastBatchKeys(\n  keys: GetKeyInfoResponse[],\n  searchMode: SearchMode,\n) {\n  return searchMode === SearchMode.Pattern\n    ? setLastBatchPatternKeys(keys)\n    : setLastBatchRedisearchKeys(keys)\n}\n\nexport function setSearchMatch(query: string, searchMode: SearchMode) {\n  return searchMode === SearchMode.Pattern\n    ? setPatternSearchMatch(query)\n    : setQueryRedisearch(query)\n}\n\nexport function resetKeysData(searchMode: SearchMode) {\n  return searchMode === SearchMode.Pattern\n    ? resetPatternKeysData()\n    : resetRedisearchKeysData()\n}\n\nexport function deleteKeyFromList(key: RedisResponseBuffer) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n\n    return state.browser.keys?.searchMode === SearchMode.Pattern\n      ? dispatch(deletePatternKeyFromList(key))\n      : dispatch(deleteRedisearchKeyFromList(key))\n  }\n}\n\nexport function editKeyFromList(data: {\n  key: RedisResponseBuffer\n  newKey: RedisResponseBuffer\n}) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n\n    return state.browser.keys?.searchMode === SearchMode.Pattern\n      ? dispatch(editPatternKeyFromList(data))\n      : dispatch(editRedisearchKeyFromList(data))\n  }\n}\n\nexport function addKeyIntoList({\n  key,\n  keyType,\n}: {\n  key: RedisString\n  keyType: KeyTypes\n}) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { viewType, filter, search } = state.browser.keys\n\n    if (search && search !== '*') {\n      return null\n    }\n\n    if (!filter || filter === keyType) {\n      if (viewType !== KeyViewType.Tree) {\n        dispatch(resetBrowserTree())\n      }\n      return dispatch(updateKeyList({ keyName: key, keyType }))\n    }\n    return null\n  }\n}\n\nexport function editKeyTTLFromList(data: [RedisResponseBuffer, number]) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n\n    return state.browser.keys?.searchMode === SearchMode.Pattern\n      ? dispatch(editPatternKeyTTLFromList(data))\n      : dispatch(editRedisearchKeyTTLFromList(data))\n  }\n}\n\nexport function refreshKey(\n  key: RedisResponseBuffer,\n  type: KeyTypes | ModulesKeyTypes,\n  args?: IFetchKeyArgs,\n  length?: number,\n) {\n  return async (dispatch: AppDispatch) => {\n    const resetData = false\n    dispatch(refreshKeyInfoAction(key))\n    switch (type) {\n      case KeyTypes.Hash: {\n        dispatch(refreshHashFieldsAction(key, resetData))\n        break\n      }\n      case KeyTypes.ZSet: {\n        dispatch(refreshZsetMembersAction(key, resetData))\n        break\n      }\n      case KeyTypes.Set: {\n        dispatch(refreshSetMembersAction(key, resetData))\n        break\n      }\n      case KeyTypes.List: {\n        dispatch(refreshListElementsAction(key, resetData))\n        break\n      }\n      case KeyTypes.String: {\n        dispatch(\n          fetchString(key, { resetData, end: args?.end || STRING_MAX_LENGTH }),\n        )\n        break\n      }\n      case KeyTypes.ReJSON: {\n        dispatch(fetchReJSON(key, '$', length, true))\n        break\n      }\n      case KeyTypes.Stream: {\n        dispatch(refreshStream(key, resetData))\n        break\n      }\n      default:\n        dispatch(fetchKeyInfo(key, resetData))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/list.ts",
    "content": "import { cloneDeep, isNull } from 'lodash'\nimport { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints, KeyTypes } from 'uiSrc/constants'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport {\n  getUrl,\n  Nullable,\n  getApiErrorMessage,\n  isStatusSuccessful,\n  Maybe,\n} from 'uiSrc/utils'\n\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport {\n  SetListElementDto,\n  GetListElementsResponse,\n  GetListElementResponse,\n  SetListElementResponse,\n  PushElementToListDto,\n  DeleteListElementsDto,\n  DeleteListElementsResponse,\n} from 'apiSrc/modules/browser/list/dto'\nimport {\n  refreshKeyInfoAction,\n  fetchKeyInfo,\n  deleteKeyFromList,\n  deleteSelectedKeySuccess,\n  updateSelectedKeyRefreshTime,\n} from './keys'\nimport { StateList } from '../interfaces/list'\nimport { AppDispatch, RootState } from '../store'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\nimport { RedisResponseBuffer } from '../interfaces'\n\nexport const initialState: StateList = {\n  loading: false,\n  error: '',\n  data: {\n    total: 0,\n    key: '',\n    keyName: '',\n    elements: [],\n    count: 0,\n    offset: 0,\n    searchedIndex: null,\n  },\n  updateValue: {\n    loading: false,\n    error: '',\n  },\n}\n\n// A slice for recipes\nconst listSlice = createSlice({\n  name: 'list',\n  initialState,\n  reducers: {\n    setListInitialState: () => initialState,\n\n    // load List elements\n    loadListElements: (\n      state,\n      { payload: resetData = true }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      state.loading = true\n      state.error = ''\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n    },\n    loadListElementsSuccess: (\n      state,\n      { payload }: PayloadAction<GetListElementsResponse>,\n    ) => {\n      state.data = {\n        ...state.data,\n        ...payload,\n        elements: payload.elements.map((element, i) => ({ index: i, element })),\n        key: payload.keyName,\n      }\n      state.loading = false\n    },\n    loadListElementsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // load more List elements for infinity scroll\n    loadMoreListElements: (state) => {\n      state.loading = true\n      state.error = ''\n      state.data.searchedIndex = initialState.data.searchedIndex\n    },\n    loadMoreListElementsSuccess: (\n      state,\n      { payload: { elements } }: PayloadAction<GetListElementsResponse>,\n    ) => {\n      state.loading = false\n      const listIndex = state.data?.elements?.length\n\n      state.data.elements = state.data?.elements?.concat(\n        elements.map((element, i) => ({ index: listIndex + i, element })),\n      )\n    },\n    loadMoreListElementsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // load searching List element by Index\n    loadSearchingListElement: (\n      state,\n      { payload }: { payload: Nullable<number> },\n    ) => {\n      state.loading = true\n      state.error = ''\n      state.data = {\n        ...state.data,\n        elements: initialState.data.elements,\n        searchedIndex: payload,\n      }\n    },\n    loadSearchingListElementSuccess: (\n      state,\n      {\n        payload: [index, data],\n      }: PayloadAction<[number, GetListElementResponse]>,\n    ) => {\n      state.loading = false\n\n      state.data.elements = [{ index, element: data?.value }]\n    },\n    loadSearchingListElementFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // Update List element\n    updateValue: (state) => {\n      state.updateValue = {\n        ...state.updateValue,\n        loading: true,\n        error: '',\n      }\n    },\n    updateValueSuccess: (state) => {\n      state.updateValue = {\n        ...state.updateValue,\n        loading: false,\n      }\n    },\n    updateValueFailure: (state, { payload }) => {\n      state.updateValue = {\n        ...state.updateValue,\n        loading: false,\n        error: payload,\n      }\n    },\n    resetUpdateValue: (state) => {\n      state.updateValue = cloneDeep(initialState.updateValue)\n    },\n    updateElementInList: (\n      state,\n      { payload }: { payload: SetListElementDto },\n    ) => {\n      state.data.elements[\n        state.data.elements.length === 1 ? 0 : payload.index\n      ] = payload\n    },\n    insertListElements: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    insertListElementsSuccess: (state) => {\n      state.loading = false\n      state.error = ''\n    },\n    insertListElementsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    deleteListElements: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    deleteListElementsSuccess: (state) => {\n      state.loading = false\n      state.error = ''\n    },\n    deleteListElementsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setListInitialState,\n  loadListElements,\n  loadListElementsSuccess,\n  loadListElementsFailure,\n  loadMoreListElements,\n  loadMoreListElementsSuccess,\n  loadMoreListElementsFailure,\n  updateValue,\n  updateValueSuccess,\n  updateValueFailure,\n  resetUpdateValue,\n  updateElementInList,\n  loadSearchingListElement,\n  loadSearchingListElementSuccess,\n  loadSearchingListElementFailure,\n  insertListElements,\n  insertListElementsFailure,\n  insertListElementsSuccess,\n  deleteListElements,\n  deleteListElementsSuccess,\n  deleteListElementsFailure,\n} = listSlice.actions\n\n// A selector\nexport const listSelector = (state: RootState) => state.browser.list\nexport const listDataSelector = (state: RootState) => state.browser.list?.data\nexport const updateListValueStateSelector = (state: RootState) =>\n  state.browser.list?.updateValue\n\n// The reducer\nexport default listSlice.reducer\n\n// Asynchronous thunk actions\nexport function fetchListElements(\n  key: RedisResponseBuffer,\n  offset: number,\n  count: number,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadListElements(resetData))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetListElementsResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.LIST_GET_ELEMENTS,\n        ),\n        {\n          keyName: key,\n          offset,\n          count,\n        },\n        {\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadListElementsSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadListElementsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function fetchMoreListElements(\n  key: RedisResponseBuffer,\n  offset: number,\n  count: number,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreListElements())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetListElementsResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.LIST_GET_ELEMENTS,\n        ),\n        {\n          keyName: key,\n          offset,\n          count,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadMoreListElementsSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadMoreListElementsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function fetchSearchingListElementAction(\n  key: RedisResponseBuffer,\n  index: Nullable<number>,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadSearchingListElement(index))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetListElementResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          `${ApiEndpoints.LIST_GET_ELEMENTS}/${index}`,\n        ),\n        {\n          keyName: key,\n        },\n        {\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadSearchingListElementSuccess([index, data]))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n        onSuccess?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadSearchingListElementFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function refreshListElementsAction(\n  key: RedisResponseBuffer,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { searchedIndex } = state.browser.list.data\n\n    if (isNull(searchedIndex)) {\n      dispatch<any>(fetchListElements(key, 0, SCAN_COUNT_DEFAULT, resetData))\n    } else {\n      dispatch<any>(fetchSearchingListElementAction(key, searchedIndex))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateListElementAction(\n  data: SetListElementDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateValue())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.patch<SetListElementResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.LIST,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(updateValueSuccess())\n        dispatch(updateElementInList(data))\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_EDITED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_EDITED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyType: KeyTypes.List,\n          },\n        })\n        dispatch<any>(refreshKeyInfoAction(data.keyName))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateValueFailure(errorMessage))\n\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function insertListElementsAction(\n  data: PushElementToListDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(insertListElements())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put<PushElementToListDto>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.LIST,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(insertListElementsSuccess())\n        dispatch<any>(fetchKeyInfo(data.keyName))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(insertListElementsFailure(errorMessage))\n\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteListElementsAction(\n  data: DeleteListElementsDto,\n  onSuccessAction?: (newTotal: number) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteListElements())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status, data: responseData } =\n        await apiService.delete<DeleteListElementsResponse>(\n          getUrl(\n            state.connections.instances.connectedInstance?.id,\n            ApiEndpoints.LIST_DELETE_ELEMENTS,\n          ),\n          { data, params: { encoding } },\n        )\n      if (isStatusSuccessful(status)) {\n        const newTotal = state.browser.list.data?.total - data.count\n\n        onSuccessAction?.(newTotal)\n        dispatch(deleteListElementsSuccess())\n        if (newTotal > 0) {\n          dispatch<any>(fetchKeyInfo(data.keyName))\n          dispatch(\n            addMessageNotification(\n              successMessages.REMOVED_LIST_ELEMENTS(\n                data.keyName,\n                data.count,\n                responseData.elements,\n              ),\n            ),\n          )\n        } else {\n          dispatch(deleteSelectedKeySuccess())\n          dispatch(deleteKeyFromList(data.keyName))\n          dispatch(\n            addMessageNotification(successMessages.DELETED_KEY(data.keyName)),\n          )\n        }\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(deleteListElementsFailure(errorMessage))\n\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/redisearch.ts",
    "content": "import axios, { AxiosError } from 'axios'\nimport { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { remove } from 'lodash'\n\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { ApiEndpoints, SearchHistoryMode } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  bufferToString,\n  getApiErrorMessage,\n  getUrl,\n  isEqualBuffers,\n  isRedisearchAvailable,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport ApiErrors from 'uiSrc/constants/apiErrors'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nimport { SearchHistoryItem } from 'uiSrc/slices/interfaces/keys'\nimport { IndexSummary } from 'uiSrc/slices/interfaces/redisearch'\nimport { GetKeysWithDetailsResponse } from 'apiSrc/modules/browser/keys/dto'\nimport {\n  CreateRedisearchIndexDto,\n  IndexDeleteRequestBodyDto,\n  ListRedisearchIndexesResponse,\n} from 'apiSrc/modules/browser/redisearch/dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport {\n  RedisResponseBuffer,\n  StateRedisearch,\n  KeyIndexesApiResponse,\n} from '../interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\n\nexport const initialState: StateRedisearch = {\n  loading: false,\n  error: '',\n  search: '',\n  isSearched: false,\n  selectedIndex: null,\n  data: {\n    total: 0,\n    scanned: 0,\n    nextCursor: '0',\n    keys: [],\n    shardsMeta: {},\n    previousResultCount: 0,\n    lastRefreshTime: null,\n  },\n  list: {\n    loading: undefined,\n    error: '',\n    data: [],\n  },\n  createIndex: {\n    loading: false,\n    error: '',\n  },\n  searchHistory: {\n    data: null,\n    loading: false,\n  },\n  keyIndexes: {},\n}\n\n// A slice for recipes\nconst redisearchSlice = createSlice({\n  name: 'redisearch',\n  initialState,\n  reducers: {\n    setRedisearchInitialState: () => initialState,\n\n    // load redisearch keys\n    loadKeys: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadKeysSuccess: (\n      state,\n      {\n        payload: [data, isSearched],\n      }: PayloadAction<[GetKeysWithDetailsResponse, boolean]>,\n    ) => {\n      state.data = {\n        ...state.data,\n        ...data,\n        nextCursor: `${data.cursor}`,\n        previousResultCount: data.keys?.length,\n      }\n      state.loading = false\n      state.isSearched = isSearched\n      state.data.lastRefreshTime = Date.now()\n    },\n    loadKeysFailure: (state, { payload }: PayloadAction<Maybe<string>>) => {\n      if (payload) {\n        state.error = payload\n      }\n      state.loading = false\n    },\n\n    // load more redisearch keys\n    loadMoreKeys: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadMoreKeysSuccess: (\n      state,\n      { payload }: PayloadAction<GetKeysWithDetailsResponse>,\n    ) => {\n      state.data.keys = payload.keys\n      state.data.total = payload.total\n      state.data.scanned = payload.scanned\n      state.data.nextCursor = `${payload.cursor}`\n      state.data.previousResultCount = payload.keys.length\n\n      state.loading = false\n    },\n    loadMoreKeysFailure: (state, { payload }: PayloadAction<Maybe<string>>) => {\n      if (payload) {\n        state.error = payload\n      }\n      state.loading = false\n    },\n\n    // load list of indexes\n    loadList: (state) => {\n      state.list = {\n        ...state.list,\n        loading: true,\n        error: '',\n      }\n    },\n    loadListSuccess: (\n      state,\n      { payload }: PayloadAction<RedisResponseBuffer[]>,\n    ) => {\n      state.list = {\n        ...state.list,\n        loading: false,\n        data: payload,\n      }\n    },\n    loadListFailure: (state, { payload }) => {\n      state.list = {\n        ...state.list,\n        loading: false,\n        error: payload,\n      }\n    },\n    createIndex: (state) => {\n      state.createIndex = {\n        ...state.createIndex,\n        loading: true,\n        error: '',\n      }\n    },\n    createIndexSuccess: (state) => {\n      state.createIndex = {\n        ...state.createIndex,\n        loading: false,\n      }\n    },\n    createIndexFailure: (state, { payload }: PayloadAction<string>) => {\n      state.createIndex = {\n        ...state.createIndex,\n        loading: false,\n        error: payload,\n      }\n    },\n\n    // create an index\n    setSelectedIndex: (\n      state,\n      { payload }: PayloadAction<RedisResponseBuffer>,\n    ) => {\n      state.selectedIndex = payload\n    },\n\n    setLastBatchRedisearchKeys: (state, { payload }) => {\n      const newKeys = state.data.keys\n      newKeys.splice(-payload.length, payload.length, ...payload)\n      state.data.keys = newKeys\n    },\n\n    setQueryRedisearch: (state, { payload }: PayloadAction<string>) => {\n      state.search = payload\n    },\n\n    resetRedisearchKeysData: (state) => {\n      state.data.total = 0\n      state.data.scanned = 0\n      state.data.keys.length = 0\n    },\n\n    deleteRedisearchKeyFromList: (state, { payload }) => {\n      remove(state.data?.keys, (key) => isEqualBuffers(key.name, payload))\n\n      state.data = {\n        ...state.data,\n        total: state.data.total - 1,\n        scanned: state.data.scanned - 1,\n      }\n    },\n\n    editRedisearchKeyFromList: (state, { payload }) => {\n      const keys = state.data.keys.map((key) => {\n        if (isEqualBuffers(key.name, payload?.key)) {\n          key.name = payload?.newKey\n          key.nameString = bufferToString(payload?.newKey)\n        }\n        return key\n      })\n\n      state.data = {\n        ...state.data,\n        keys,\n      }\n    },\n\n    editRedisearchKeyTTLFromList: (\n      state,\n      { payload: [key, ttl] }: PayloadAction<[RedisResponseBuffer, number]>,\n    ) => {\n      const keys = state.data.keys.map((keyData) => {\n        if (isEqualBuffers(keyData.name, key)) {\n          keyData.ttl = ttl\n        }\n        return keyData\n      })\n\n      state.data = {\n        ...state.data,\n        keys,\n      }\n    },\n    loadRediSearchHistory: (state) => {\n      state.searchHistory.loading = true\n    },\n    loadRediSearchHistorySuccess: (\n      state,\n      { payload }: PayloadAction<SearchHistoryItem[]>,\n    ) => {\n      state.searchHistory.loading = false\n      state.searchHistory.data = payload\n    },\n    loadRediSearchHistoryFailure: (state) => {\n      state.searchHistory.loading = false\n    },\n    deleteRediSearchHistory: (state) => {\n      state.searchHistory.loading = true\n    },\n    deleteRediSearchHistorySuccess: (\n      state,\n      { payload }: { payload: string[] },\n    ) => {\n      state.searchHistory.loading = false\n      if (state.searchHistory.data) {\n        remove(state.searchHistory.data, (item) => payload.includes(item.id))\n      }\n    },\n    deleteRediSearchHistoryFailure: (state) => {\n      state.searchHistory.loading = false\n    },\n\n    loadKeyIndexes: (state, { payload }: PayloadAction<string>) => {\n      state.keyIndexes[payload] = {\n        loading: true,\n        data: [],\n        error: '',\n      }\n    },\n    loadKeyIndexesSuccess: (\n      state,\n      { payload: [key, data] }: PayloadAction<[string, IndexSummary[]]>,\n    ) => {\n      state.keyIndexes[key] = {\n        loading: false,\n        data,\n        error: '',\n      }\n    },\n    loadKeyIndexesFailure: (\n      state,\n      { payload: [key, error] }: PayloadAction<[string, string]>,\n    ) => {\n      state.keyIndexes[key] = {\n        loading: false,\n        data: [],\n        error,\n      }\n    },\n    resetKeyIndexes: (state) => {\n      state.keyIndexes = {}\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadKeys,\n  loadKeysSuccess,\n  loadKeysFailure,\n  loadMoreKeys,\n  loadMoreKeysSuccess,\n  loadMoreKeysFailure,\n  loadList,\n  loadListSuccess,\n  loadListFailure,\n  createIndex,\n  createIndexSuccess,\n  createIndexFailure,\n  setRedisearchInitialState,\n  setSelectedIndex,\n  setLastBatchRedisearchKeys,\n  setQueryRedisearch,\n  resetRedisearchKeysData,\n  deleteRedisearchKeyFromList,\n  editRedisearchKeyFromList,\n  editRedisearchKeyTTLFromList,\n  loadRediSearchHistory,\n  loadRediSearchHistorySuccess,\n  loadRediSearchHistoryFailure,\n  deleteRediSearchHistory,\n  deleteRediSearchHistorySuccess,\n  deleteRediSearchHistoryFailure,\n  loadKeyIndexes,\n  loadKeyIndexesSuccess,\n  loadKeyIndexesFailure,\n  resetKeyIndexes,\n} = redisearchSlice.actions\n\n// Selectors\nexport const redisearchSelector = (state: RootState) => state.browser.redisearch\nexport const redisearchDataSelector = (state: RootState) =>\n  state.browser.redisearch.data\nexport const redisearchListSelector = (state: RootState) =>\n  state.browser.redisearch.list\nexport const createIndexStateSelector = (state: RootState) =>\n  state.browser.redisearch.createIndex\nexport const redisearchHistorySelector = (state: RootState) =>\n  state.browser.redisearch.searchHistory\nexport const keyIndexesSelector = (state: RootState) =>\n  state.browser.redisearch.keyIndexes\n\n// The reducer\nexport default redisearchSlice.reducer\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let controller: Nullable<AbortController> = null\n\n// Asynchronous thunk action\nexport function fetchRedisearchKeysAction(\n  cursor: string,\n  count: number,\n  telemetryProperties: { [key: string]: any } = {},\n  onSuccess?: (value: GetKeysWithDetailsResponse) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadKeys())\n\n    try {\n      controller?.abort()\n      controller = new AbortController()\n\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { selectedIndex: index, search: query } = state.browser.redisearch\n      const { data, status } =\n        await apiService.post<GetKeysWithDetailsResponse>(\n          getUrl(\n            state.connections.instances.connectedInstance?.id,\n            ApiEndpoints.REDISEARCH_SEARCH,\n          ),\n          {\n            offset: +cursor,\n            limit: count,\n            query: query || DEFAULT_SEARCH_MATCH,\n            index,\n          },\n          {\n            params: { encoding },\n            signal: controller.signal,\n          },\n        )\n\n      controller = null\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadKeysSuccess([data, !!query]))\n\n        if (query) {\n          sendEventTelemetry({\n            event: TelemetryEvent.SEARCH_KEYS_SEARCHED,\n            eventData: {\n              view: state.browser.keys?.viewType,\n              databaseId: state.connections.instances?.connectedInstance?.id,\n              scanCount: data.scanned,\n              source: telemetryProperties.source ?? 'manual',\n              ...telemetryProperties,\n            },\n          })\n        }\n\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadKeysFailure(errorMessage))\n\n        if (\n          error?.response?.data?.message\n            ?.toString()\n            .endsWith(ApiErrors.RedisearchIndexNotFound)\n        ) {\n          dispatch(setRedisearchInitialState())\n          dispatch(fetchRedisearchListAction())\n        }\n        onFailed?.()\n      } else {\n        dispatch(loadKeysFailure())\n      }\n    }\n  }\n}\n// Asynchronous thunk action\nexport function fetchMoreRedisearchKeysAction(\n  oldKeys: IKeyPropTypes[] = [],\n  cursor: string,\n  count: number,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreKeys())\n\n    try {\n      controller?.abort()\n      controller = new AbortController()\n\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { selectedIndex: index, search: query } = state.browser.redisearch\n      const { data, status } =\n        await apiService.post<GetKeysWithDetailsResponse>(\n          getUrl(\n            state.connections.instances.connectedInstance?.id,\n            ApiEndpoints.REDISEARCH_SEARCH,\n          ),\n          {\n            offset: +cursor,\n            limit: count,\n            query: query || DEFAULT_SEARCH_MATCH,\n            index,\n          },\n          {\n            params: { encoding },\n            signal: controller.signal,\n          },\n        )\n\n      controller = null\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          loadMoreKeysSuccess({\n            ...data,\n            keys: oldKeys.concat(data.keys),\n          }),\n        )\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadMoreKeysFailure(errorMessage))\n      } else {\n        dispatch(loadMoreKeysFailure())\n      }\n    }\n  }\n}\n\nexport function fetchRedisearchListAction(\n  onSuccess?: (value: RedisResponseBuffer[]) => void,\n  onFailed?: () => void,\n  showError = true,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadList())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } =\n        await apiService.get<ListRedisearchIndexesResponse>(\n          getUrl(\n            state.connections.instances.connectedInstance?.id,\n            ApiEndpoints.REDISEARCH,\n          ),\n          {\n            params: { encoding },\n          },\n        )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadListSuccess(data.indexes))\n        onSuccess?.(data.indexes)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      showError && dispatch(addErrorNotification(error))\n      dispatch(loadListFailure(errorMessage))\n      onFailed?.()\n    }\n  }\n}\nexport function createRedisearchIndexAction(\n  data: CreateRedisearchIndexDto,\n  onSuccess?: (data: CreateRedisearchIndexDto) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(createIndex())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.post<void>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REDISEARCH,\n        ),\n        {\n          ...data,\n        },\n        {\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(createIndexSuccess())\n        dispatch(addMessageNotification(successMessages.CREATE_INDEX()))\n        dispatch(fetchRedisearchListAction())\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(createIndexFailure(errorMessage))\n      onFailed?.()\n    }\n  }\n}\n\nexport function deleteRedisearchIndexAction(\n  data: IndexDeleteRequestBodyDto,\n  onSuccess?: (data: IndexDeleteRequestBodyDto) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.delete<void>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REDISEARCH,\n        ),\n        {\n          data,\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          addMessageNotification(\n            successMessages.DELETE_INDEX(bufferToString(data.index as string)),\n          ),\n        )\n        dispatch(fetchRedisearchListAction())\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(addErrorNotification(error))\n      onFailed?.()\n    }\n  }\n}\n\nexport function fetchRedisearchHistoryAction(\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadRediSearchHistory())\n\n    try {\n      const state = stateInit()\n      const { data, status } = await apiService.get<SearchHistoryItem[]>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HISTORY,\n        ),\n        {\n          params: {\n            mode: SearchHistoryMode.Redisearch,\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadRediSearchHistorySuccess(data))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      dispatch(loadRediSearchHistoryFailure())\n      onFailed?.()\n    }\n  }\n}\n\nexport function deleteRedisearchHistoryAction(\n  ids: string[],\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteRediSearchHistory())\n\n    try {\n      const state = stateInit()\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.HISTORY,\n        ),\n        {\n          data: {\n            ids,\n          },\n          params: {\n            mode: SearchHistoryMode.Redisearch,\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteRediSearchHistorySuccess(ids))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      dispatch(deleteRediSearchHistoryFailure())\n      onFailed?.()\n    }\n  }\n}\n\nconst transformKeyIndexesResponse = (\n  data: KeyIndexesApiResponse,\n): IndexSummary[] =>\n  (data.indexes || []).map((idx) => ({\n    name: idx.name,\n    prefixes: idx.prefixes,\n    keyType: idx.key_type,\n  }))\n\nexport function fetchKeyIndexesAction(keyName: string, force = false) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { modules } = state.connections.instances.connectedInstance\n\n    if (!isRedisearchAvailable(modules)) {\n      dispatch(loadKeyIndexesSuccess([keyName, []]))\n      return\n    }\n\n    const existing = state.browser.redisearch.keyIndexes[keyName]\n\n    if (!force && existing && (existing.loading || !existing.error)) {\n      return\n    }\n\n    dispatch(loadKeyIndexes(keyName))\n\n    try {\n      const { data, status } = await apiService.post<KeyIndexesApiResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REDISEARCH_KEY_INDEXES,\n        ),\n        { key: keyName },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          loadKeyIndexesSuccess([keyName, transformKeyIndexesResponse(data)]),\n        )\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadKeyIndexesFailure([keyName, errorMessage]))\n    }\n  }\n}\n\nexport function fetchRedisearchInfoAction(\n  index: string,\n  onSuccess?: (value: RedisResponseBuffer[]) => void,\n  onFailed?: () => void,\n) {\n  return async (_: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REDISEARCH_INFO,\n        ),\n        {\n          index,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      onFailed?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/rejson.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport axios, { AxiosError, CancelTokenSource } from 'axios'\n\nimport { isNumber } from 'lodash'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n  getJsonPathLevel,\n} from 'uiSrc/telemetry'\nimport {\n  getApiErrorMessage,\n  getUrl,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { parseJsonData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/utils'\nimport {\n  GetRejsonRlResponseDto,\n  RemoveRejsonRlResponse,\n} from 'apiSrc/modules/browser/rejson-rl/dto'\n\nimport { refreshKeyInfoAction } from './keys'\nimport {\n  EditorType,\n  InitialStateRejson,\n  RedisResponseBuffer,\n} from '../interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\n\nexport const JSON_LENGTH_TO_FORCE_RETRIEVE = 200\nconst TELEMETRY_KEY_LEVEL_ENTIRE_KEY = 'entireKey'\n\nexport const initialState: InitialStateRejson = {\n  loading: false,\n  error: null,\n  data: {\n    downloaded: false,\n    data: undefined,\n    type: '',\n  },\n  editorType: EditorType.Default,\n  isWithinThreshold: false,\n}\n\n// A slice for recipes\nconst rejsonSlice = createSlice({\n  name: 'rejson',\n  initialState,\n  reducers: {\n    // load reJSON part\n    loadRejsonBranch: (\n      state,\n      { payload: resetData = true }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      state.loading = true\n      state.error = null\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n    },\n    loadRejsonBranchSuccess: (state, { payload }) => {\n      state.data = payload\n      state.loading = false\n    },\n    loadRejsonBranchFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    appendReJSONArrayItem: (state) => {\n      state.loading = true\n      state.error = null\n    },\n    appendReJSONArrayItemSuccess: (state) => {\n      state.loading = false\n      state.error = null\n    },\n    appendReJSONArrayItemFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    setReJSONData: (state) => {\n      state.loading = true\n      state.error = null\n    },\n    setReJSONDataSuccess: (state) => {\n      state.loading = false\n      state.error = null\n    },\n    setReJSONDataFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    removeRejsonKey: (state) => {\n      state.loading = true\n      state.error = null\n    },\n    removeRejsonKeySuccess: (state) => {\n      state.loading = false\n      state.error = null\n    },\n    removeRejsonKeyFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    setEditorType: (state, { payload }: PayloadAction<EditorType>) => {\n      state.editorType = payload\n    },\n    setIsWithinThreshold: (state, { payload }: PayloadAction<boolean>) => {\n      state.isWithinThreshold = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadRejsonBranch,\n  loadRejsonBranchSuccess,\n  loadRejsonBranchFailure,\n  appendReJSONArrayItem,\n  appendReJSONArrayItemSuccess,\n  appendReJSONArrayItemFailure,\n  setReJSONData,\n  setReJSONDataSuccess,\n  setReJSONDataFailure,\n  removeRejsonKey,\n  removeRejsonKeySuccess,\n  removeRejsonKeyFailure,\n  setEditorType,\n  setIsWithinThreshold,\n} = rejsonSlice.actions\n\n// A selector\nexport const rejsonSelector = (state: RootState) => state.browser.rejson\nexport const rejsonDataSelector = (state: RootState) =>\n  state.browser.rejson?.data\n\n// The reducer\nexport default rejsonSlice.reducer\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let sourceRejson: Nullable<CancelTokenSource> = null\n\n// Asynchronous thunk action\nexport function fetchReJSON(\n  key: RedisResponseBuffer,\n  path = '$',\n  length?: number,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadRejsonBranch(resetData))\n\n    try {\n      sourceRejson?.cancel?.()\n\n      const { CancelToken } = axios\n      sourceRejson = CancelToken.source()\n\n      const state = stateInit()\n      const { editorType } = state.browser.rejson\n      const { encoding } = state.app.info\n\n      // \"Force retrieve\" means fetching the entire JSON value without any optimization.\n      // Normally, the optimized approach retrieves only the necessary portion —\n      // typically just the top-level properties currently visible.\n      const shouldForceRetrieve =\n        editorType === EditorType.Text ||\n        !isNumber(length) ||\n        length <= JSON_LENGTH_TO_FORCE_RETRIEVE\n\n      const { data, status } = await apiService.post<GetRejsonRlResponseDto>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REJSON_GET,\n        ),\n        {\n          keyName: key,\n          path,\n          forceRetrieve: shouldForceRetrieve,\n          encoding,\n        },\n        { cancelToken: sourceRejson.token },\n      )\n\n      sourceRejson = null\n      if (isStatusSuccessful(status)) {\n        dispatch(loadRejsonBranchSuccess(data))\n      }\n    } catch (error) {\n      if (!axios.isCancel(error)) {\n        const errorMessage = getApiErrorMessage(error as AxiosError)\n        dispatch(loadRejsonBranchFailure(errorMessage))\n        dispatch(addErrorNotification(error))\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function setReJSONDataAction(\n  key: RedisResponseBuffer,\n  path: string,\n  data: string,\n  isEditMode: boolean,\n  length?: number,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(setReJSONData())\n\n    try {\n      const state = stateInit()\n\n      const { status } = await apiService.patch<GetRejsonRlResponseDto>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REJSON_SET,\n        ),\n        {\n          keyName: key,\n          path,\n          data,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        try {\n          const { editorType } = state.browser.rejson\n          const keyLevel =\n            editorType === EditorType.Text\n              ? TELEMETRY_KEY_LEVEL_ENTIRE_KEY\n              : getJsonPathLevel(path)\n          sendEventTelemetry({\n            event: getBasedOnViewTypeEvent(\n              state.browser.keys?.viewType,\n              isEditMode\n                ? TelemetryEvent.BROWSER_JSON_PROPERTY_EDITED\n                : TelemetryEvent.BROWSER_JSON_PROPERTY_ADDED,\n              isEditMode\n                ? TelemetryEvent.TREE_VIEW_JSON_PROPERTY_EDITED\n                : TelemetryEvent.TREE_VIEW_JSON_PROPERTY_ADDED,\n            ),\n            eventData: {\n              databaseId: state.connections.instances?.connectedInstance?.id,\n              keyLevel,\n            },\n          })\n        } catch (error) {\n          // console.log(error)\n        }\n\n        dispatch(setReJSONDataSuccess())\n        dispatch<any>(fetchReJSON(key, '$', length))\n        dispatch<any>(refreshKeyInfoAction(key))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setReJSONDataFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function appendReJSONArrayItemAction(\n  key: RedisResponseBuffer,\n  path: string,\n  data: string,\n  length?: number,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(appendReJSONArrayItem())\n\n    try {\n      const state = stateInit()\n      const { status } = await apiService.patch<GetRejsonRlResponseDto>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REJSON_ARRAPPEND,\n        ),\n        {\n          keyName: key,\n          path,\n          data: [data],\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        const keyLevel = path === '$' ? '0' : getJsonPathLevel(`${path}[0]`)\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_JSON_PROPERTY_ADDED,\n            TelemetryEvent.TREE_VIEW_JSON_PROPERTY_ADDED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyLevel,\n          },\n        })\n        dispatch(appendReJSONArrayItemSuccess())\n        dispatch<any>(fetchReJSON(key, '$', length))\n        dispatch<any>(refreshKeyInfoAction(key))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(appendReJSONArrayItemFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function removeReJSONKeyAction(\n  key: RedisResponseBuffer,\n  path = '$',\n  jsonKeyName = '',\n  length?: number,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(removeRejsonKey())\n\n    try {\n      const state = stateInit()\n      const { status } = await apiService.delete<RemoveRejsonRlResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REJSON,\n        ),\n        {\n          data: {\n            keyName: key,\n            path,\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_JSON_PROPERTY_DELETED,\n            TelemetryEvent.TREE_VIEW_JSON_PROPERTY_DELETED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyLevel: getJsonPathLevel(path),\n          },\n        })\n        dispatch(removeRejsonKeySuccess())\n        dispatch<any>(fetchReJSON(key, '$', length))\n        dispatch<any>(refreshKeyInfoAction(key))\n        dispatch(\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(key, jsonKeyName, 'JSON key'),\n          ),\n        )\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(removeRejsonKeyFailure(errorMessage))\n      dispatch(addErrorNotification(error as AxiosError))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchVisualisationResults(path = '$', forceRetrieve = false) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const key = state.browser.keys?.selectedKey?.data?.name\n      const { data, status } = await apiService.post<GetRejsonRlResponseDto>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.REJSON_GET,\n        ),\n        {\n          keyName: key,\n          path,\n          forceRetrieve,\n          encoding,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        return {\n          ...data,\n          data: parseJsonData(data?.data),\n        }\n      }\n      throw new Error(data.toString())\n    } catch (_err) {\n      const error = _err as AxiosError\n      if (!axios.isCancel(error)) {\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(loadRejsonBranchFailure(errorMessage))\n        dispatch(addErrorNotification(error))\n      }\n    }\n\n    return null\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/set.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { remove } from 'lodash'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport {\n  bufferToString,\n  getApiErrorMessage,\n  getUrl,\n  isEqualBuffers,\n  isStatusSuccessful,\n  Maybe,\n} from 'uiSrc/utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\n\nimport {\n  AddMembersToSetDto,\n  GetSetMembersResponse,\n} from 'apiSrc/modules/browser/set/dto'\nimport {\n  deleteKeyFromList,\n  deleteSelectedKeySuccess,\n  fetchKeyInfo,\n  refreshKeyInfoAction,\n  updateSelectedKeyRefreshTime,\n} from './keys'\nimport { AppDispatch, RootState } from '../store'\nimport { InitialStateSet, RedisResponseBuffer } from '../interfaces'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\n\nexport const initialState: InitialStateSet = {\n  loading: false,\n  error: '',\n  data: {\n    total: 0,\n    key: undefined,\n    keyName: '',\n    members: [],\n    nextCursor: 0,\n    match: '*',\n  },\n}\n\n// A slice for recipes\nconst setSlice = createSlice({\n  name: 'set',\n  initialState,\n  reducers: {\n    setSetMembers: (\n      state,\n      { payload }: PayloadAction<RedisResponseBuffer[]>,\n    ) => {\n      state.data.members = payload\n    },\n    // load Set members\n    loadSetMembers: (\n      state,\n      {\n        payload: [match, resetData = true],\n      }: PayloadAction<[string, Maybe<boolean>]>,\n    ) => {\n      state.loading = true\n      state.error = ''\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n\n      state.data = {\n        ...state.data,\n        match: match || '*',\n      }\n    },\n    loadSetMembersSuccess: (\n      state,\n      { payload }: PayloadAction<GetSetMembersResponse>,\n    ) => {\n      state.data = {\n        ...state.data,\n        ...payload,\n      }\n      state.data.key = payload.keyName\n      state.loading = false\n    },\n    loadSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // load more Set members for infinity scroll\n    loadMoreSetMembers: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadMoreSetMembersSuccess: (\n      state,\n      { payload: { members, ...rest } }: PayloadAction<GetSetMembersResponse>,\n    ) => {\n      state.loading = false\n      state.data = {\n        ...state.data,\n        ...rest,\n        members: state.data?.members?.concat(members),\n      }\n    },\n    loadMoreSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    addSetMembers: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    addSetMembersSuccess: (state) => {\n      state.loading = false\n    },\n    addSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    // delete Set members\n    removeSetMembers: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    removeSetMembersSuccess: (state) => {\n      state.loading = false\n    },\n    removeSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    removeMembersFromList: (\n      state,\n      { payload }: { payload: RedisResponseBuffer[] },\n    ) => {\n      remove(\n        state.data?.members,\n        (member: { data: any[] }) =>\n          payload.findIndex((item) => isEqualBuffers(item, member)) > -1,\n      )\n\n      state.data = {\n        ...state.data,\n        total: state.data.total - 1,\n      }\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadSetMembers,\n  loadSetMembersSuccess,\n  loadSetMembersFailure,\n  loadMoreSetMembers,\n  loadMoreSetMembersSuccess,\n  loadMoreSetMembersFailure,\n  addSetMembers,\n  addSetMembersSuccess,\n  addSetMembersFailure,\n  removeSetMembers,\n  removeSetMembersSuccess,\n  removeSetMembersFailure,\n  removeMembersFromList,\n  setSetMembers,\n} = setSlice.actions\n\n// A selector\nexport const setSelector = (state: RootState) => state.browser.set\nexport const setDataSelector = (state: RootState) => state.browser.set?.data\n\n// The reducer\nexport default setSlice.reducer\n\n// Asynchronous thunk actions\nexport function fetchSetMembers(\n  key: RedisResponseBuffer,\n  cursor: number,\n  count: number,\n  match: string,\n  resetData?: boolean,\n  onSuccess?: (data: GetSetMembersResponse) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadSetMembers([match, resetData]))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetSetMembersResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.SET_GET_MEMBERS,\n        ),\n        {\n          keyName: key,\n          cursor,\n          count,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadSetMembersSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n        onSuccess?.(data)\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadSetMembersFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function fetchMoreSetMembers(\n  key: RedisResponseBuffer,\n  cursor: number,\n  count: number,\n  match: string,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreSetMembers())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetSetMembersResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.SET_GET_MEMBERS,\n        ),\n        {\n          keyName: key,\n          cursor,\n          count,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadMoreSetMembersSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadMoreSetMembersFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function refreshSetMembersAction(\n  key: RedisResponseBuffer,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { match } = state.browser.set.data\n    const { encoding } = state.app.info\n\n    dispatch(loadSetMembers([match || '*', resetData]))\n\n    try {\n      const { data, status } = await apiService.post<GetSetMembersResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.SET_GET_MEMBERS,\n        ),\n        {\n          keyName: key,\n          cursor: 0,\n          count: SCAN_COUNT_DEFAULT,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadSetMembersSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadSetMembersFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function addSetMembersAction(\n  data: AddMembersToSetDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(addSetMembers())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.SET,\n        ),\n        data,\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(addSetMembersSuccess())\n        dispatch<any>(fetchKeyInfo(data.keyName))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(addSetMembersFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function deleteSetMembers(\n  key: RedisResponseBuffer,\n  members: RedisResponseBuffer[],\n  onSuccessAction?: (newTotal: number) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(removeSetMembers())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.SET_MEMBERS,\n        ),\n        {\n          data: {\n            keyName: key,\n            members,\n          },\n          params: { encoding },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        const newTotalValue = state.browser.set.data.total - data.affected\n\n        onSuccessAction?.(newTotalValue)\n        dispatch(removeSetMembersSuccess())\n        dispatch(removeMembersFromList(members))\n        if (newTotalValue > 0) {\n          dispatch<any>(refreshKeyInfoAction(key))\n          dispatch(\n            addMessageNotification(\n              successMessages.REMOVED_KEY_VALUE(\n                key,\n                members.map((member) => bufferToString(member)).join(''),\n                'Member',\n              ),\n            ),\n          )\n        } else {\n          dispatch(deleteSelectedKeySuccess())\n          dispatch(deleteKeyFromList(key))\n          dispatch(addMessageNotification(successMessages.DELETED_KEY(key)))\n        }\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(removeSetMembersFailure(errorMessage))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/stream.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { remove } from 'lodash'\nimport axios, { AxiosError, CancelTokenSource } from 'axios'\n\nimport { apiService } from 'uiSrc/services'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport { ApiEndpoints, SortOrder } from 'uiSrc/constants'\nimport { refreshKeyInfoAction } from 'uiSrc/slices/browser/keys'\nimport {\n  getUrl,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n  getApiErrorMessage,\n  bufferToString,\n} from 'uiSrc/utils'\nimport {\n  getStreamRangeStart,\n  getStreamRangeEnd,\n  updateConsumerGroups,\n  updateConsumers,\n} from 'uiSrc/utils/streamUtils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport {\n  AddStreamEntriesDto,\n  AddStreamEntriesResponse,\n  ConsumerDto,\n  ConsumerGroupDto,\n  CreateConsumerGroupsDto,\n  GetStreamEntriesResponse,\n  PendingEntryDto,\n  UpdateConsumerGroupDto,\n  ClaimPendingEntryDto,\n  ClaimPendingEntriesResponse,\n  AckPendingEntriesResponse,\n} from 'apiSrc/modules/browser/stream/dto'\nimport { AppDispatch, RootState } from '../store'\nimport { StateStream, StreamViewType } from '../interfaces/stream'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\nimport { RedisResponseBuffer } from '../interfaces'\n\nexport const initialState: StateStream = {\n  loading: false,\n  error: '',\n  sortOrder: SortOrder.DESC,\n  range: { start: '', end: '' },\n  viewType: StreamViewType.Data,\n  data: {\n    total: 0,\n    entries: [],\n    keyName: '',\n    keyNameString: '',\n    lastGeneratedId: '',\n    firstEntry: {\n      id: '',\n      fields: [],\n    },\n    lastEntry: {\n      id: '',\n      fields: [],\n    },\n    lastRefreshTime: null,\n  },\n  groups: {\n    loading: false,\n    error: '',\n    data: [],\n    selectedGroup: null,\n    lastRefreshTime: null,\n  },\n}\n\n// A slice for recipes\nconst streamSlice = createSlice({\n  name: 'stream',\n  initialState,\n  reducers: {\n    setStreamInitialState: () => initialState,\n    // load stream entries\n    loadEntries: (\n      state,\n      { payload: resetData = true }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      state.loading = true\n      state.error = ''\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n    },\n    loadEntriesSuccess: (\n      state,\n      {\n        payload: [data, sortOrder],\n      }: PayloadAction<[GetStreamEntriesResponse, SortOrder]>,\n    ) => {\n      state.data = {\n        ...state.data,\n        ...data,\n      }\n      state.data.keyName = data?.keyName\n      state.data.keyNameString = bufferToString(data?.keyName)\n      state.data.lastRefreshTime = Date.now()\n      state.sortOrder = sortOrder\n      state.loading = false\n    },\n    loadEntriesFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    // load more stream entries\n    loadMoreEntries: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadMoreEntriesSuccess: (\n      state,\n      {\n        payload: { entries, ...rest },\n      }: PayloadAction<GetStreamEntriesResponse>,\n    ) => {\n      state.data = {\n        ...state.data,\n        ...rest,\n        entries: state.data?.entries?.concat(entries),\n      }\n      state.loading = false\n    },\n    loadMoreEntriesFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    addNewEntries: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    addNewEntriesSuccess: (state) => {\n      state.loading = false\n      state.data.entries = []\n    },\n    addNewEntriesFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    addNewGroup: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    addNewGroupSuccess: (state) => {\n      state.loading = false\n    },\n    addNewGroupFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    // delete Stream entries\n    removeStreamEntries: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    removeStreamEntriesSuccess: (state) => {\n      state.loading = false\n      state.data.entries = []\n    },\n    removeStreamEntriesFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    updateStart: (state, { payload }: PayloadAction<string>) => {\n      state.range.start = payload\n    },\n    updateEnd: (state, { payload }: PayloadAction<string>) => {\n      state.range.end = payload\n    },\n    cleanRangeFilter: (state) => {\n      state.range = {\n        start: '',\n        end: '',\n      }\n    },\n\n    setStreamViewType: (state, { payload }: PayloadAction<StreamViewType>) => {\n      state.viewType = payload\n    },\n\n    // load stream consumer groups entries\n    loadConsumerGroups: (\n      state,\n      { payload: resetData = true }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      state.groups.loading = true\n      state.groups.error = ''\n\n      if (resetData) {\n        state.groups.data = initialState.groups.data\n      }\n    },\n    loadConsumerGroupsSuccess: (\n      state,\n      { payload }: PayloadAction<ConsumerGroupDto[]>,\n    ) => {\n      state.groups.loading = false\n      state.groups.data = payload\n      state.groups.lastRefreshTime = Date.now()\n    },\n    loadConsumerGroupsFailure: (state, { payload }) => {\n      state.groups.loading = false\n      state.groups.error = payload\n    },\n\n    deleteConsumerGroups: (state) => {\n      state.groups.loading = true\n      state.groups.error = ''\n    },\n\n    deleteConsumerGroupsSuccess: (state) => {\n      state.groups.loading = false\n    },\n\n    deleteConsumerGroupsFailure: (state, { payload }) => {\n      state.groups.loading = false\n      state.groups.error = payload\n    },\n\n    setSelectedGroup: (state, { payload }: PayloadAction<ConsumerGroupDto>) => {\n      state.groups.selectedGroup = {\n        ...payload,\n        nameString: bufferToString(payload.name),\n      }\n    },\n\n    modifyLastDeliveredId: (state) => {\n      state.groups.loading = true\n    },\n\n    modifyLastDeliveredIdSuccess: (state) => {\n      state.groups.loading = false\n    },\n\n    modifyLastDeliveredIdFailure: (state, { payload }) => {\n      state.groups.loading = false\n      state.groups.error = payload\n    },\n\n    setSelectedConsumer: (state, { payload }) => {\n      state.groups.selectedGroup = {\n        ...state.groups.selectedGroup,\n        selectedConsumer: {\n          ...payload,\n          nameString: bufferToString(payload.name),\n        },\n      }\n    },\n\n    loadConsumersSuccess: (\n      state,\n      { payload }: PayloadAction<ConsumerDto[]>,\n    ) => {\n      state.groups.loading = false\n\n      const groups = updateConsumerGroups(\n        state.groups.data,\n        state.groups.selectedGroup?.name,\n        payload,\n      )\n      const consumers = payload.map((consumer) => ({\n        ...consumer,\n        name: {\n          ...consumer.name,\n          viewValue: bufferToString(consumer.name),\n        },\n      }))\n\n      state.groups.data = groups\n      state.groups.selectedGroup = {\n        ...state.groups.selectedGroup,\n        lastRefreshTime: Date.now(),\n        data: consumers,\n      }\n    },\n\n    loadConsumersFailure: (state, { payload }: PayloadAction<string>) => {\n      state.groups.loading = false\n      state.groups.error = payload\n      state.viewType = StreamViewType.Groups\n    },\n\n    deleteConsumers: (state) => {\n      state.groups.loading = true\n      state.groups.error = ''\n    },\n\n    deleteConsumersSuccess: (state) => {\n      state.groups.loading = false\n    },\n\n    deleteConsumersFailure: (state, { payload }) => {\n      state.groups.loading = false\n      state.groups.error = payload\n    },\n\n    loadConsumerMessagesSuccess: (\n      state,\n      { payload }: PayloadAction<PendingEntryDto[]>,\n    ) => {\n      state.groups.loading = false\n\n      const consumers = updateConsumers(\n        state.groups.selectedGroup?.data,\n        state.groups.selectedGroup?.selectedConsumer?.name,\n        payload,\n      )\n      const groups = updateConsumerGroups(\n        state.groups.data,\n        state.groups.selectedGroup?.name,\n        consumers,\n      )\n\n      state.groups.data = groups\n      state.groups.selectedGroup = {\n        ...state.groups.selectedGroup,\n        data: consumers,\n        selectedConsumer: {\n          ...state.groups.selectedGroup?.selectedConsumer,\n          lastRefreshTime: Date.now(),\n          data: payload,\n        },\n      }\n    },\n\n    loadConsumerMessagesFailure: (\n      state,\n      { payload }: PayloadAction<string>,\n    ) => {\n      state.groups.loading = false\n      state.groups.error = payload\n      state.viewType = StreamViewType.Consumers\n    },\n\n    loadMoreConsumerMessagesSuccess: (\n      state,\n      { payload }: PayloadAction<PendingEntryDto[]>,\n    ) => {\n      state.groups.loading = false\n\n      state.groups.selectedGroup = {\n        ...state.groups.selectedGroup,\n        selectedConsumer: {\n          ...state.groups.selectedGroup?.selectedConsumer,\n          lastRefreshTime: Date.now(),\n          data: (\n            state.groups.selectedGroup?.selectedConsumer?.data ?? []\n          ).concat(payload),\n        },\n      }\n    },\n\n    setConsumerGroupsSortOrder: (\n      state,\n      { payload }: PayloadAction<SortOrder>,\n    ) => {\n      state.groups.sortOrder = payload\n    },\n    loadMoreConsumerGroupsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    claimConsumerMessages: (state) => {\n      state.groups.loading = true\n    },\n    claimConsumerMessagesSuccess: (state) => {\n      state.groups.loading = false\n    },\n    claimConsumerMessagesFailure: (state, { payload }) => {\n      state.groups.loading = false\n      state.groups.error = payload\n    },\n    ackPendingEntries: (state) => {\n      state.groups.loading = true\n    },\n    ackPendingEntriesSuccess: (state) => {\n      state.groups.loading = false\n    },\n    ackPendingEntriesFailure: (state, { payload }) => {\n      state.groups.loading = false\n      state.groups.error = payload\n    },\n    deleteMessageFromList: (state, { payload }) => {\n      remove(\n        state.groups?.selectedGroup?.selectedConsumer?.data!,\n        (message) => message?.id === payload,\n      )\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setStreamInitialState,\n  loadEntries,\n  loadEntriesSuccess,\n  loadEntriesFailure,\n  loadMoreEntries,\n  loadMoreEntriesSuccess,\n  loadMoreEntriesFailure,\n  addNewEntries,\n  addNewEntriesSuccess,\n  addNewEntriesFailure,\n  addNewGroup,\n  addNewGroupSuccess,\n  addNewGroupFailure,\n  removeStreamEntries,\n  removeStreamEntriesSuccess,\n  removeStreamEntriesFailure,\n  updateStart,\n  updateEnd,\n  cleanRangeFilter,\n  setStreamViewType,\n  loadConsumerGroups,\n  loadConsumerGroupsSuccess,\n  loadConsumerGroupsFailure,\n  deleteConsumerGroups,\n  deleteConsumerGroupsSuccess,\n  deleteConsumerGroupsFailure,\n  modifyLastDeliveredId,\n  modifyLastDeliveredIdSuccess,\n  modifyLastDeliveredIdFailure,\n  loadConsumersSuccess,\n  loadConsumersFailure,\n  deleteConsumers,\n  deleteConsumersSuccess,\n  deleteConsumersFailure,\n  loadConsumerMessagesSuccess,\n  loadConsumerMessagesFailure,\n  loadMoreConsumerMessagesSuccess,\n  setSelectedGroup,\n  setSelectedConsumer,\n  claimConsumerMessages,\n  claimConsumerMessagesSuccess,\n  claimConsumerMessagesFailure,\n  ackPendingEntries,\n  ackPendingEntriesSuccess,\n  ackPendingEntriesFailure,\n  deleteMessageFromList,\n} = streamSlice.actions\n\n// A selector\nexport const streamSelector = (state: RootState) => state.browser.stream\nexport const streamDataSelector = (state: RootState) =>\n  state.browser.stream?.data\nexport const streamRangeSelector = (state: RootState) =>\n  state.browser.stream?.range\nexport const streamGroupsSelector = (state: RootState) =>\n  state.browser.stream?.groups\nexport const streamGroupsDataSelector = (state: RootState) =>\n  state.browser.stream?.groups?.data || []\nexport const selectedGroupSelector = (state: RootState) =>\n  state.browser.stream?.groups?.selectedGroup\nexport const selectedConsumerSelector = (state: RootState) =>\n  state.browser.stream?.groups?.selectedGroup?.selectedConsumer\n\n// The reducer\nexport default streamSlice.reducer\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let sourceStreamFetch: Nullable<CancelTokenSource> = null\n\n// Asynchronous thunk action\nexport function fetchStreamEntries(\n  key: RedisResponseBuffer,\n  count: number,\n  sortOrder: SortOrder,\n  resetData?: boolean,\n  onSuccess?: (data: GetStreamEntriesResponse) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadEntries(resetData))\n\n    try {\n      sourceStreamFetch?.cancel?.()\n\n      const { CancelToken } = axios\n      sourceStreamFetch = CancelToken.source()\n\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const start = getStreamRangeStart(\n        state.browser.stream.range.start,\n        state.browser.stream.data.firstEntry?.id,\n      )\n      const end = getStreamRangeEnd(\n        state.browser.stream.range.end,\n        state.browser.stream.data.lastEntry?.id,\n      )\n      const { data, status } = await apiService.post<GetStreamEntriesResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_ENTRIES_GET,\n        ),\n        {\n          keyName: key,\n          start,\n          end,\n          count,\n          sortOrder,\n        },\n        {\n          params: { encoding },\n          cancelToken: sourceStreamFetch.token,\n        },\n      )\n\n      sourceStreamFetch = null\n      if (isStatusSuccessful(status)) {\n        dispatch(loadEntriesSuccess([data, sortOrder]))\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadEntriesFailure(errorMessage))\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function refreshStream(\n  key: RedisResponseBuffer,\n  resetData: boolean = false,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const streamViewType = state.browser.stream.viewType\n\n    if (streamViewType === StreamViewType.Data) {\n      dispatch(refreshStreamEntries(key, resetData))\n    }\n    if (streamViewType === StreamViewType.Groups) {\n      dispatch<any>(fetchConsumerGroups(resetData))\n    }\n    if (streamViewType === StreamViewType.Consumers) {\n      dispatch<any>(\n        fetchConsumers(\n          resetData,\n          () => {},\n          () => dispatch(setStreamViewType(StreamViewType.Groups)),\n        ),\n      )\n    }\n    if (streamViewType === StreamViewType.Messages) {\n      dispatch<any>(\n        fetchConsumerMessages(\n          resetData,\n          () => {},\n          () => dispatch(setStreamViewType(StreamViewType.Consumers)),\n        ),\n      )\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function refreshStreamEntries(\n  key: RedisResponseBuffer,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadEntries(resetData))\n\n    try {\n      sourceStreamFetch?.cancel?.()\n\n      const { CancelToken } = axios\n      sourceStreamFetch = CancelToken.source()\n\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { sortOrder } = state.browser.stream\n      const start = getStreamRangeStart(\n        state.browser.stream.range.start,\n        state.browser.stream.data.firstEntry?.id,\n      )\n      const end = getStreamRangeEnd(\n        state.browser.stream.range.end,\n        state.browser.stream.data.lastEntry?.id,\n      )\n      const { data, status } = await apiService.post<GetStreamEntriesResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_ENTRIES_GET,\n        ),\n        {\n          keyName: key,\n          start,\n          end,\n          sortOrder,\n          count: SCAN_COUNT_DEFAULT,\n          encoding,\n        },\n        {\n          params: { encoding },\n          cancelToken: sourceStreamFetch.token,\n        },\n      )\n\n      sourceStreamFetch = null\n      if (isStatusSuccessful(status)) {\n        dispatch(loadEntriesSuccess([data, sortOrder]))\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadEntriesFailure(errorMessage))\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchMoreStreamEntries(\n  key: RedisResponseBuffer,\n  start: string,\n  end: string,\n  count: number,\n  sortOrder: SortOrder,\n  onSuccess?: (data: GetStreamEntriesResponse) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreEntries())\n\n    try {\n      sourceStreamFetch?.cancel?.()\n\n      const { CancelToken } = axios\n      sourceStreamFetch = CancelToken.source()\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetStreamEntriesResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_ENTRIES_GET,\n        ),\n        {\n          keyName: key,\n          start,\n          end,\n          count,\n          sortOrder,\n        },\n        {\n          params: { encoding },\n          cancelToken: sourceStreamFetch.token,\n        },\n      )\n\n      sourceStreamFetch = null\n      if (isStatusSuccessful(status)) {\n        dispatch(loadMoreEntriesSuccess(data))\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadMoreEntriesFailure(errorMessage))\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function addNewEntriesAction(\n  data: AddStreamEntriesDto,\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(addNewEntries())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.post<AddStreamEntriesResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_ENTRIES,\n        ),\n        data,\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(addNewEntriesSuccess())\n        dispatch<any>(refreshStreamEntries(data.keyName, false))\n        dispatch<any>(refreshKeyInfoAction(data.keyName))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(addNewEntriesFailure(errorMessage))\n      onFailed?.()\n    }\n  }\n}\n// Asynchronous thunk actions\nexport function deleteStreamEntry(\n  key: RedisResponseBuffer,\n  entries: string[],\n  onSuccessAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(removeStreamEntries())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_ENTRIES,\n        ),\n        {\n          data: {\n            keyName: key,\n            entries,\n          },\n          params: { encoding },\n        },\n      )\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(removeStreamEntriesSuccess())\n        dispatch<any>(refreshStreamEntries(key, false))\n        dispatch<any>(refreshKeyInfoAction(key))\n        dispatch(\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(key, entries.join(''), 'Entry'),\n          ),\n        )\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(removeStreamEntriesFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function addNewGroupAction(\n  data: CreateConsumerGroupsDto,\n  onSuccess?: () => void,\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(addNewGroup())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.post(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMER_GROUPS,\n        ),\n        data,\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(addNewGroupSuccess())\n        dispatch<any>(fetchConsumerGroups(false))\n        dispatch<any>(refreshKeyInfoAction(data.keyName))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(addNewGroupFailure(errorMessage))\n      onFail?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchConsumerGroups(\n  resetData?: boolean,\n  onSuccess?: (data: ConsumerGroupDto[]) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadConsumerGroups(resetData))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const keyName = state.browser.keys?.selectedKey?.data?.name\n      const { data, status } = await apiService.post<ConsumerGroupDto[]>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMER_GROUPS_GET,\n        ),\n        {\n          keyName,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadConsumerGroupsSuccess(data))\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadConsumerGroupsFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n\nexport function deleteConsumerGroupsAction(\n  keyName: RedisResponseBuffer,\n  consumerGroups: RedisResponseBuffer[],\n  onSuccessAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteConsumerGroups())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMER_GROUPS,\n        ),\n        {\n          data: {\n            keyName,\n            consumerGroups,\n          },\n          params: { encoding },\n        },\n      )\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(deleteConsumerGroupsSuccess())\n        dispatch<any>(fetchConsumerGroups(false))\n        dispatch<any>(refreshKeyInfoAction(keyName))\n        dispatch(\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(\n              keyName,\n              consumerGroups.map((group) => bufferToString(group)).join(''),\n              'Group',\n            ),\n          ),\n        )\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(deleteConsumerGroupsFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchConsumers(\n  resetData?: boolean,\n  onSuccess?: (data: ConsumerDto[]) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadConsumerGroups(resetData))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const keyName = state.browser.keys?.selectedKey?.data?.name\n      const groupName = state.browser.stream.groups.selectedGroup?.name\n      const { data, status } = await apiService.post<ConsumerDto[]>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMERS_GET,\n        ),\n        {\n          keyName,\n          groupName,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadConsumersSuccess(data))\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadConsumersFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteConsumersAction(\n  keyName: RedisResponseBuffer,\n  groupName: RedisResponseBuffer,\n  consumerNames: RedisResponseBuffer[],\n  onSuccessAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(deleteConsumers())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMERS,\n        ),\n        {\n          data: {\n            keyName,\n            groupName,\n            consumerNames,\n          },\n          params: { encoding },\n        },\n      )\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(deleteConsumersSuccess())\n        dispatch<any>(fetchConsumers(false))\n        dispatch<any>(refreshKeyInfoAction(keyName))\n        dispatch(\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(\n              keyName,\n              consumerNames\n                .map((consumer) => bufferToString(consumer))\n                .join(''),\n              'Consumer',\n            ),\n          ),\n        )\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(deleteConsumersFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchConsumerMessages(\n  resetData?: boolean,\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadConsumerGroups(resetData))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const keyName = state.browser.keys?.selectedKey?.data?.name\n      const groupName = state.browser.stream.groups.selectedGroup?.name\n      const consumerName =\n        state.browser.stream.groups.selectedGroup?.selectedConsumer?.name\n      const { data, status } = await apiService.post<PendingEntryDto[]>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMERS_MESSAGES_GET,\n        ),\n        {\n          keyName,\n          groupName,\n          consumerName,\n        },\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(loadConsumerMessagesSuccess(data))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadConsumerMessagesFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchMoreConsumerMessages(\n  count: number,\n  start: string = '-',\n  end: string = '+',\n  resetData?: boolean,\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadConsumerGroups(resetData))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const keyName = state.browser.keys?.selectedKey?.data?.name\n      const groupName = state.browser.stream.groups.selectedGroup?.name\n      const consumerName =\n        state.browser.stream.groups.selectedGroup?.selectedConsumer?.name\n      const { data, status } = await apiService.post<PendingEntryDto[]>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMERS_MESSAGES_GET,\n        ),\n        {\n          start,\n          end,\n          count,\n          keyName,\n          groupName,\n          consumerName,\n        },\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(loadMoreConsumerMessagesSuccess(data))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(loadConsumerMessagesFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function modifyLastDeliveredIdAction(\n  data: UpdateConsumerGroupDto,\n  onSuccess?: () => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(modifyLastDeliveredId())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.patch<any>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAMS_CONSUMER_GROUPS,\n        ),\n        data,\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(modifyLastDeliveredIdSuccess())\n        dispatch<any>(fetchConsumerGroups(false))\n        dispatch<any>(refreshKeyInfoAction(data.keyName))\n        onSuccess?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(modifyLastDeliveredIdFailure(errorMessage))\n      onFailed?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function claimPendingMessages(\n  payload: Partial<ClaimPendingEntryDto>,\n  onSuccess?: (data: ClaimPendingEntriesResponse) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(claimConsumerMessages())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } =\n        await apiService.post<ClaimPendingEntriesResponse>(\n          getUrl(\n            state.connections.instances.connectedInstance?.id,\n            ApiEndpoints.STREAM_CLAIM_PENDING_MESSAGES,\n          ),\n          {\n            keyName: state.browser.keys?.selectedKey?.data?.name,\n            groupName: state.browser.stream.groups.selectedGroup?.name,\n            consumerName:\n              state.browser.stream.groups.selectedGroup?.selectedConsumer?.name,\n            ...payload,\n          },\n          { params: { encoding } },\n        )\n      if (isStatusSuccessful(status)) {\n        dispatch(claimConsumerMessagesSuccess())\n        dispatch<any>(fetchConsumers())\n        dispatch<any>(fetchConsumerGroups())\n        if (data.affected.length) {\n          dispatch(deleteMessageFromList(data.affected[0]))\n          dispatch(\n            addMessageNotification(\n              successMessages.MESSAGE_ACTION(data.affected[0], 'claimed'),\n            ),\n          )\n        } else {\n          dispatch(\n            addMessageNotification(successMessages.NO_CLAIMED_MESSAGES()),\n          )\n        }\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(claimConsumerMessagesFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function ackPendingEntriesAction(\n  key: RedisResponseBuffer,\n  group: string,\n  entries: string[],\n  onSuccess?: (data: AckPendingEntriesResponse) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(ackPendingEntries())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<AckPendingEntriesResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STREAM_ACK_PENDING_ENTRIES,\n        ),\n        {\n          keyName: key,\n          groupName: group,\n          entries,\n        },\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(ackPendingEntriesSuccess())\n        dispatch(deleteMessageFromList(entries[0]))\n        dispatch<any>(fetchConsumers())\n        dispatch<any>(fetchConsumerGroups())\n        dispatch(\n          addMessageNotification(\n            successMessages.MESSAGE_ACTION(entries[0], 'acknowledged'),\n          ),\n        )\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(ackPendingEntriesFailure(errorMessage))\n        onFailed?.()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/string.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosResponseHeaders } from 'axios'\nimport { ApiEndpoints, KeyTypes } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getApiErrorMessage,\n  getUrl,\n  isStatusSuccessful,\n  Maybe,\n} from 'uiSrc/utils'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { IFetchKeyArgs } from 'uiSrc/constants/prop-types/keys'\n\nimport { refreshKeyInfoAction } from './keys'\nimport { addErrorNotification } from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\nimport { StringState } from '../interfaces/string'\nimport { RedisResponseBuffer } from '../interfaces'\n\nexport const initialState: StringState = {\n  loading: false,\n  error: '',\n  data: {\n    key: '',\n    value: null,\n  },\n}\n\n// A slice for recipes\nconst stringSlice = createSlice({\n  name: 'string',\n  initialState,\n  reducers: {\n    // load String value\n    getString: (\n      state,\n      { payload: resetData = true }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      state.loading = true\n      state.error = ''\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n    },\n    getStringSuccess: (state, { payload }) => {\n      state.data.key = payload.keyName\n      state.data.value = payload.value\n      state.loading = false\n    },\n    getStringFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    downloadString: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    downloadStringSuccess: (state) => {\n      state.loading = false\n      state.error = ''\n    },\n    downloadStringFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    updateValue: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    updateValueSuccess: (state, { payload }) => {\n      state.loading = false\n      state.data.value = payload\n    },\n    updateValueFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    resetStringValue: (state) => {\n      state.data.key = ''\n      state.data.value = null\n    },\n    setIsStringCompressed: (state, { payload }: PayloadAction<boolean>) => {\n      state.isCompressed = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  getString,\n  getStringSuccess,\n  getStringFailure,\n  downloadString,\n  downloadStringSuccess,\n  downloadStringFailure,\n  updateValue,\n  updateValueSuccess,\n  updateValueFailure,\n  resetStringValue,\n  setIsStringCompressed,\n} = stringSlice.actions\n\n// A selector\nexport const stringSelector = (state: RootState) => state.browser.string\nexport const stringDataSelector = (state: RootState) =>\n  state.browser.string?.data\n\n// The reducer\nexport default stringSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchString(\n  key: RedisResponseBuffer,\n  args: IFetchKeyArgs = {},\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const { resetData, end: endString } = args\n    dispatch(getString(resetData))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STRING_VALUE,\n        ),\n        {\n          keyName: key,\n          end: endString,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getStringSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getStringFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchDownloadStringValue(\n  key: RedisResponseBuffer,\n  onSuccessAction?: (data: string, headers: AxiosResponseHeaders) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(downloadString())\n\n    try {\n      const state = stateInit()\n      const { data, status, headers } = await apiService.post(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          `${ApiEndpoints.STRING_VALUE_DOWNLOAD}`,\n        ),\n        {\n          keyName: key,\n        },\n        { responseType: 'arraybuffer' },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(downloadStringSuccess())\n        onSuccessAction?.(data, headers)\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(downloadStringFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateStringValueAction(\n  key: RedisResponseBuffer,\n  value: RedisResponseBuffer,\n  onSuccess?: (value: RedisResponseBuffer) => void,\n  onFailed?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateValue())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.STRING,\n        ),\n        {\n          keyName: key,\n          value,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_EDITED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_EDITED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyType: KeyTypes.String,\n          },\n        })\n        dispatch(updateValueSuccess(value))\n        dispatch<any>(refreshKeyInfoAction(key))\n        onSuccess?.(value)\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateValueFailure(errorMessage))\n      onFailed?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/browser/zset.ts",
    "content": "import { cloneDeep, isNull, remove } from 'lodash'\nimport { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints, SortOrder, KeyTypes } from 'uiSrc/constants'\nimport {\n  bufferToString,\n  getApiErrorMessage,\n  getUrl,\n  isEqualBuffers,\n  isStatusSuccessful,\n  Maybe,\n} from 'uiSrc/utils'\nimport {\n  getBasedOnViewTypeEvent,\n  sendEventTelemetry,\n  TelemetryEvent,\n} from 'uiSrc/telemetry'\nimport { StateZset } from 'uiSrc/slices/interfaces/zset'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport {\n  AddMembersToZSetDto,\n  SearchZSetMembersResponse,\n  ZSetMemberDto,\n  GetZSetResponse,\n} from 'apiSrc/modules/browser/z-set/dto'\nimport {\n  deleteKeyFromList,\n  deleteSelectedKeySuccess,\n  fetchKeyInfo,\n  refreshKeyInfoAction,\n  updateSelectedKeyRefreshTime,\n} from './keys'\nimport { AppDispatch, RootState } from '../store'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\nimport { RedisResponseBuffer } from '../interfaces'\n\nexport const initialState: StateZset = {\n  loading: false,\n  searching: false,\n  error: '',\n  data: {\n    total: 0,\n    key: undefined,\n    keyName: '',\n    members: [],\n    nextCursor: 0,\n    match: '',\n    sortOrder: SortOrder.ASC,\n  },\n  updateScore: {\n    loading: false,\n    error: '',\n  },\n}\n\n// A slice for recipes\nconst zsetSlice = createSlice({\n  name: 'zset',\n  initialState,\n  reducers: {\n    setZsetInitialState: () => initialState,\n\n    setZSetMembers: (state, { payload }: PayloadAction<ZSetMemberDto[]>) => {\n      state.data.members = payload\n    },\n    // load ZSet members\n    loadZSetMembers: (\n      state,\n      {\n        payload: [sortOrder, resetData = true],\n      }: PayloadAction<[SortOrder, Maybe<boolean>]>,\n    ) => {\n      state.loading = true\n      state.searching = false\n      state.error = ''\n\n      if (resetData) {\n        state.data = initialState.data\n      }\n      state.data = {\n        ...state.data,\n        sortOrder,\n      }\n    },\n    loadZSetMembersSuccess: (state, { payload }) => {\n      state.data.keyName = payload.keyName\n      state.data.members = payload.members\n      state.data.total = payload.total\n      state.loading = false\n    },\n    loadZSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    searchZSetMembers: (state, { payload }) => {\n      state.loading = true\n      state.searching = true\n      state.error = ''\n      state.data = {\n        ...initialState.data,\n        match: payload,\n      }\n    },\n    searchZSetMembersSuccess: (state, { payload }) => {\n      state.data = {\n        ...state.data,\n        ...payload,\n      }\n      state.data.key = payload.keyName\n      state.loading = false\n    },\n    searchZSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    searchMoreZSetMembers: (state, { payload }) => {\n      state.loading = true\n      state.searching = true\n      state.error = ''\n      state.data.match = payload\n    },\n    searchMoreZSetMembersSuccess: (state, { payload }) => {\n      state.loading = false\n      state.data.nextCursor = payload.nextCursor\n      state.data.members = state.data.members.concat(payload.members)\n    },\n    searchMoreZSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    // load more ZSet members for infinity scroll\n    loadMoreZSetMembers: (state) => {\n      state.loading = true\n      state.searching = false\n      state.error = ''\n    },\n    loadMoreZSetMembersSuccess: (state, { payload: { members } }) => {\n      state.loading = false\n      state.data.members = state.data.members.concat(members)\n    },\n    loadMoreZSetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    removeZsetMembers: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    removeZsetMembersSuccess: (state) => {\n      state.loading = false\n    },\n    removeZsetMembersFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    removeMembersFromList: (\n      state,\n      { payload }: { payload: RedisResponseBuffer[] },\n    ) => {\n      remove(\n        state.data?.members,\n        (member) =>\n          payload.findIndex((item) => isEqualBuffers(item, member.name)) > -1,\n      )\n\n      state.data = {\n        ...state.data,\n        total: state.data.total - 1,\n      }\n    },\n    updateScore: (state) => {\n      state.updateScore = {\n        ...state.updateScore,\n        loading: true,\n        error: '',\n      }\n    },\n    updateScoreSuccess: (state) => {\n      state.updateScore = {\n        ...state.updateScore,\n        loading: false,\n      }\n    },\n    updateScoreFailure: (state, { payload }) => {\n      state.updateScore = {\n        ...state.updateScore,\n        loading: false,\n        error: payload,\n      }\n    },\n    resetUpdateScore: (state) => {\n      state.updateScore = cloneDeep(initialState.updateScore)\n    },\n    updateMembersInList: (state, { payload }: { payload: ZSetMemberDto[] }) => {\n      const newMembersState = state.data.members.map((listItem) => {\n        const index = payload.findIndex((item) =>\n          isEqualBuffers(item.name, listItem.name),\n        )\n        if (index > -1) {\n          return payload[index]\n        }\n        return listItem\n      })\n\n      state.data = {\n        ...state.data,\n        members: newMembersState,\n      }\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setZsetInitialState,\n  loadZSetMembers,\n  loadZSetMembersSuccess,\n  loadZSetMembersFailure,\n  loadMoreZSetMembers,\n  loadMoreZSetMembersSuccess,\n  loadMoreZSetMembersFailure,\n  searchZSetMembers,\n  searchZSetMembersSuccess,\n  searchZSetMembersFailure,\n  searchMoreZSetMembers,\n  searchMoreZSetMembersFailure,\n  searchMoreZSetMembersSuccess,\n  removeZsetMembers,\n  removeZsetMembersSuccess,\n  removeZsetMembersFailure,\n  removeMembersFromList,\n  updateScore,\n  updateScoreSuccess,\n  updateScoreFailure,\n  resetUpdateScore,\n  updateMembersInList,\n  setZSetMembers,\n} = zsetSlice.actions\n\n// A selector\nexport const zsetSelector = (state: RootState) => state.browser.zset\nexport const zsetDataSelector = (state: RootState) => state.browser.zset?.data\nexport const updateZsetScoreStateSelector = (state: RootState) =>\n  state.browser.zset?.updateScore\n\n// The reducer\nexport default zsetSlice.reducer\n\n// Asynchronous thunk actions\nexport function fetchZSetMembers(\n  key: RedisResponseBuffer,\n  offset: number,\n  count: number,\n  sortOrder: SortOrder,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadZSetMembers([sortOrder, resetData]))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetZSetResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET_GET_MEMBERS,\n        ),\n        {\n          keyName: key,\n          offset,\n          count,\n          sortOrder,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadZSetMembersSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadZSetMembersFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk actions\nexport function fetchMoreZSetMembers(\n  key: RedisResponseBuffer,\n  offset: number,\n  count: number,\n  sortOrder: SortOrder,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadMoreZSetMembers())\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<GetZSetResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET_GET_MEMBERS,\n        ),\n        {\n          keyName: key,\n          offset,\n          count,\n          sortOrder,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadMoreZSetMembersSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadMoreZSetMembersFailure(errorMessage))\n    }\n  }\n}\n\nexport function fetchAddZSetMembers(\n  data: AddMembersToZSetDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateScore())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_ADDED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_ADDED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyType: KeyTypes.ZSet,\n            numberOfAdded: data.members.length,\n          },\n        })\n        onSuccessAction?.()\n        dispatch(updateScoreSuccess())\n        dispatch<any>(fetchKeyInfo(data.keyName))\n      }\n    } catch (error) {\n      onFailAction?.()\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateScoreFailure(errorMessage))\n    }\n  }\n}\n\nexport function deleteZSetMembers(\n  key: RedisResponseBuffer,\n  members: RedisResponseBuffer[],\n  onSuccessAction?: (newTotal: number) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(removeZsetMembers())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status, data } = await apiService.delete(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET_MEMBERS,\n        ),\n        {\n          data: {\n            keyName: key,\n            members,\n            params: { encoding },\n          },\n        },\n      )\n      if (isStatusSuccessful(status)) {\n        const newTotalValue = state.browser.zset.data.total - data.affected\n\n        onSuccessAction?.(newTotalValue)\n        dispatch(removeZsetMembersSuccess())\n        dispatch(removeMembersFromList(members))\n        if (newTotalValue > 0) {\n          dispatch<any>(refreshKeyInfoAction(key))\n          dispatch(\n            addMessageNotification(\n              successMessages.REMOVED_KEY_VALUE(\n                key,\n                members.map((member) => bufferToString(member)).join(''),\n                'Member',\n              ),\n            ),\n          )\n        } else {\n          dispatch(deleteSelectedKeySuccess())\n          dispatch(deleteKeyFromList(key))\n          dispatch(addMessageNotification(successMessages.DELETED_KEY(key)))\n        }\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(removeZsetMembersFailure(errorMessage))\n    }\n  }\n}\n\nexport function updateZSetMembers(\n  data: AddMembersToZSetDto,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(updateScore())\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { status } = await apiService.put(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET,\n        ),\n        data,\n        { params: { encoding } },\n      )\n      if (isStatusSuccessful(status)) {\n        sendEventTelemetry({\n          event: getBasedOnViewTypeEvent(\n            state.browser.keys?.viewType,\n            TelemetryEvent.BROWSER_KEY_VALUE_EDITED,\n            TelemetryEvent.TREE_VIEW_KEY_VALUE_EDITED,\n          ),\n          eventData: {\n            databaseId: state.connections.instances?.connectedInstance?.id,\n            keyType: KeyTypes.ZSet,\n          },\n        })\n        onSuccessAction?.()\n        dispatch(updateScoreSuccess())\n        dispatch(updateMembersInList(data.members))\n        dispatch<any>(refreshKeyInfoAction(data.keyName))\n      }\n    } catch (error) {\n      onFailAction?.()\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateScoreFailure(errorMessage))\n    }\n  }\n}\n\nexport function fetchSearchZSetMembers(\n  key: RedisResponseBuffer,\n  cursor: number,\n  count: number,\n  match: string,\n  onSuccess?: (data: SearchZSetMembersResponse) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(searchZSetMembers(isNull(match) ? '*' : match))\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<SearchZSetMembersResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET_MEMBERS_SEARCH,\n        ),\n        {\n          keyName: key,\n          cursor,\n          count,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(searchZSetMembersSuccess(data))\n        dispatch(updateSelectedKeyRefreshTime(Date.now()))\n        onSuccess?.(data)\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(searchZSetMembersFailure(errorMessage))\n    }\n  }\n}\n\nexport function fetchSearchMoreZSetMembers(\n  key: RedisResponseBuffer,\n  cursor: number,\n  count: number,\n  match: string,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(searchMoreZSetMembers(match))\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post<SearchZSetMembersResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET_MEMBERS_SEARCH,\n        ),\n        {\n          keyName: key,\n          cursor,\n          count,\n          match,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(searchMoreZSetMembersSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(searchMoreZSetMembersFailure(errorMessage))\n    }\n  }\n}\n\nexport function refreshZsetMembersAction(\n  key: RedisResponseBuffer,\n  resetData?: boolean,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { searching } = state.browser.zset\n    const { match } = state.browser.zset.data\n    if (searching) {\n      dispatch(searchZSetMembers(isNull(match) ? '*' : match))\n      try {\n        const state = stateInit()\n        const { encoding } = state.app.info\n        const { data, status } =\n          await apiService.post<SearchZSetMembersResponse>(\n            getUrl(\n              state.connections.instances.connectedInstance?.id,\n              ApiEndpoints.ZSET_MEMBERS_SEARCH,\n            ),\n            {\n              keyName: key,\n              cursor: 0,\n              count: SCAN_COUNT_DEFAULT,\n              match,\n            },\n            { params: { encoding } },\n          )\n\n        if (isStatusSuccessful(status)) {\n          dispatch(searchZSetMembersSuccess(data))\n        }\n      } catch (error) {\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(addErrorNotification(error))\n        dispatch(searchZSetMembersFailure(errorMessage))\n      }\n      return\n    }\n    const { sortOrder } = state.browser.zset.data\n    dispatch(loadZSetMembers([sortOrder, resetData]))\n\n    try {\n      const state = stateInit()\n      const { encoding } = state.app.info\n      const { data, status } = await apiService.post(\n        getUrl(\n          state.connections.instances.connectedInstance?.id,\n          ApiEndpoints.ZSET_GET_MEMBERS,\n        ),\n        {\n          keyName: key,\n          offset: 0,\n          count: SCAN_COUNT_DEFAULT,\n          sortOrder,\n        },\n        { params: { encoding } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadZSetMembersSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadZSetMembersFailure(errorMessage))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/cli/cli-output.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\n\nimport { AxiosError, AxiosResponseHeaders } from 'axios'\nimport { apiService, localStorageService } from 'uiSrc/services'\nimport { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  cliParseTextResponseWithOffset,\n  getDbIndexFromSelectQuery,\n} from 'uiSrc/utils/cliHelper'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport {\n  SelectCommand,\n  CliOutputFormatterType,\n} from 'uiSrc/constants/cliOutput'\nimport { SendCommandResponse } from 'apiSrc/modules/cli/dto/cli.dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport { CommandExecutionStatus, StateCliOutput } from '../interfaces/cli'\nimport { addErrorNotification } from '../app/notifications'\n\nexport const initialState: StateCliOutput = {\n  data: [],\n  loading: false,\n  error: '',\n  db: 0,\n  commandHistory:\n    localStorageService?.get(BrowserStorageItem.cliInputHistory) ?? [],\n}\n\n// A slice for recipes\nconst outputSlice = createSlice({\n  name: 'output',\n  initialState,\n  reducers: {\n    setOutputInitialState: () => initialState,\n\n    // Concat text to Output\n    concatToOutput: (state, { payload }: { payload: any[] }) => {\n      state.data = state.data.concat(payload)\n    },\n\n    // Update Cli command History\n    updateCliCommandHistory: (state, { payload }: { payload: string[] }) => {\n      state.commandHistory = payload\n    },\n\n    // Send CLI command to API\n    sendCliCommand: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    sendCliCommandSuccess: (state) => {\n      state.loading = false\n\n      state.error = ''\n    },\n    sendCliCommandFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    resetOutput: (state) => {\n      state.data = []\n      state.loading = false\n    },\n    resetOutputLoading: (state) => {\n      state.loading = false\n    },\n\n    setCliDbIndex: (state, { payload }) => {\n      state.db = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  concatToOutput,\n  setOutputInitialState,\n  resetOutput,\n  resetOutputLoading,\n  updateCliCommandHistory,\n  sendCliCommand,\n  sendCliCommandSuccess,\n  sendCliCommandFailure,\n  setCliDbIndex,\n} = outputSlice.actions\n\n// A selector\nexport const outputSelector = (state: RootState) => state.cli.output\n\n// The reducer\nexport default outputSlice.reducer\n\n// Asynchronous thunk action\nexport function sendCliCommandAction(\n  command: string = '',\n  onSuccessAction?: () => void,\n  onFailAction?: (error: AxiosError) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections?.instances?.connectedInstance\n\n      if (command === '') {\n        onSuccessAction?.()\n        return\n      }\n\n      dispatch(sendCliCommand())\n\n      const {\n        data: { response, status: dataStatus },\n        status,\n      } = await apiService.post<SendCommandResponse>(\n        getUrl(\n          id,\n          ApiEndpoints.CLI,\n          state.cli.settings?.cliClientUuid,\n          ApiEndpoints.SEND_COMMAND,\n        ),\n        { command, outputFormat: CliOutputFormatterType.Raw },\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(sendCliCommandSuccess())\n        dispatch(\n          concatToOutput(\n            cliParseTextResponseWithOffset(response, command, dataStatus),\n          ),\n        )\n        if (\n          dataStatus === CommandExecutionStatus.Success &&\n          command.toLowerCase().startsWith(SelectCommand.toLowerCase())\n        ) {\n          try {\n            const dbIndex = getDbIndexFromSelectQuery(command)\n            dispatch(setCliDbIndex(dbIndex))\n          } catch (e) {\n            // continue regardless of error\n          }\n        }\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(sendCliCommandFailure(errorMessage))\n\n      onFailAction?.(error as AxiosError)\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function sendCliClusterCommandAction(\n  command: string = '',\n  onSuccessAction?: () => void,\n  onFailAction?: (error: AxiosError) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const outputFormat = CliOutputFormatterType.Raw\n      const state = stateInit()\n      const { id = '' } = state.connections.instances?.connectedInstance\n\n      if (command === '') {\n        onSuccessAction?.()\n        return\n      }\n\n      dispatch(sendCliCommand())\n\n      const {\n        data: { response, status: dataStatus },\n        status,\n      } = await apiService.post<SendCommandResponse>(\n        getUrl(\n          id,\n          ApiEndpoints.CLI,\n          state.cli.settings?.cliClientUuid,\n          ApiEndpoints.SEND_CLUSTER_COMMAND,\n        ),\n        { command, outputFormat },\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.()\n        dispatch(sendCliCommandSuccess())\n        dispatch(\n          concatToOutput(\n            cliParseTextResponseWithOffset(response, command, dataStatus),\n          ),\n        )\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(sendCliCommandFailure(errorMessage))\n\n      onFailAction?.(error as AxiosError)\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchMonitorLog(\n  logFileId: string = '',\n  onSuccessAction?: (data: string, headers: AxiosResponseHeaders) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(sendCliCommand())\n\n    try {\n      const { data, status, headers } = await apiService.get<string>(\n        `${ApiEndpoints.PROFILER_LOGS}/${logFileId}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(sendCliCommandSuccess())\n        onSuccessAction?.(data, headers)\n      }\n    } catch (err) {\n      const error = err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(sendCliCommandFailure(errorMessage))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/cli/cli-settings.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\n\nimport { apiService, sessionStorageService } from 'uiSrc/services'\nimport { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { setCliDbIndex } from 'uiSrc/slices/cli/cli-output'\nimport {\n  CreateCliClientResponse,\n  DeleteClientResponse,\n} from 'apiSrc/modules/cli/dto/cli.dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport { StateCliSettings } from '../interfaces/cli'\n\nexport const initialState: StateCliSettings = {\n  isMinimizedHelper: false,\n  isShowHelper: false,\n  isShowCli: false,\n  loading: false,\n  errorClient: '',\n  cliClientUuid: '',\n  matchedCommand: '',\n  searchedCommand: '',\n  searchingCommand: '',\n  searchingCommandFilter: '',\n  isEnteringCommand: false,\n  isSearching: false,\n  unsupportedCommands: [],\n  blockingCommands: [],\n}\n\n// A slice for recipes\nconst cliSettingsSlice = createSlice({\n  name: 'cliSettings',\n  initialState,\n  reducers: {\n    setCliSettingsInitialState: () => initialState,\n    // collapse / uncollapse CLI\n    openCli: (state) => {\n      state.isShowCli = true\n    },\n\n    toggleCli: (state) => {\n      state.isShowCli = !state.isShowCli\n    },\n    openCliHelper: (state) => {\n      state.isShowHelper = true\n    },\n    // collapse / uncollapse CLI Helper\n    toggleCliHelper: (state) => {\n      state.isShowHelper = !state.isShowHelper\n      state.isMinimizedHelper = !state.isMinimizedHelper\n    },\n    // hide / unhide CLI Helper\n    toggleHideCliHelper: (state) => {\n      state.isMinimizedHelper = !state.isMinimizedHelper\n    },\n\n    setMatchedCommand: (state, { payload }: { payload: string }) => {\n      state.matchedCommand = payload\n      state.isSearching = false\n    },\n\n    setCliEnteringCommand: (state) => {\n      state.isEnteringCommand = true\n    },\n\n    setSearchedCommand: (state, { payload }: { payload: string }) => {\n      state.searchedCommand = payload\n      state.isSearching = false\n    },\n\n    setSearchingCommand: (state, { payload }: { payload: string }) => {\n      state.searchingCommand = payload\n      state.isSearching = true\n      state.isEnteringCommand = false\n    },\n\n    setSearchingCommandFilter: (state, { payload }: { payload: string }) => {\n      state.searchingCommandFilter = payload\n      state.isSearching = true\n      state.isEnteringCommand = false\n    },\n\n    clearSearchingCommand: (state) => {\n      state.searchingCommand = ''\n      state.searchedCommand = ''\n      state.searchingCommandFilter = ''\n      state.isSearching = false\n    },\n\n    // create, update, delete CLI Client\n    processCliClient: (state) => {\n      state.loading = true\n    },\n    processCliClientSuccess: (state, { payload }: { payload: string }) => {\n      state.loading = false\n      state.cliClientUuid = payload\n\n      state.errorClient = ''\n    },\n    processCliClientFailure: (state, { payload }) => {\n      state.loading = false\n      state.errorClient = payload\n    },\n\n    deleteCliClientSuccess: (state) => {\n      state.loading = false\n      state.cliClientUuid = ''\n    },\n\n    getUnsupportedCommandsSuccess: (\n      state,\n      { payload }: { payload: string[] },\n    ) => {\n      state.loading = false\n      state.unsupportedCommands = payload.map((command) =>\n        command.toLowerCase(),\n      )\n    },\n\n    getBlockingCommandsSuccess: (state, { payload }: { payload: string[] }) => {\n      state.loading = false\n      state.blockingCommands = payload.map((command) => command.toLowerCase())\n    },\n\n    // reset cli client uuid\n    resetCliClientUuid: (state) => {\n      state.cliClientUuid = ''\n    },\n\n    // reset to collapse CLI\n    resetCliSettings: (state) => {\n      state.isShowCli = false\n      state.cliClientUuid = ''\n      state.loading = false\n    },\n\n    // reset to collapse CLI Helper\n    resetCliHelperSettings: (state) => {\n      state.isShowHelper = false\n      state.isSearching = false\n      state.isEnteringCommand = false\n      state.isMinimizedHelper = false\n      state.matchedCommand = ''\n      state.searchingCommand = ''\n      state.searchedCommand = ''\n      state.searchingCommandFilter = ''\n    },\n\n    goBackFromCommand: (state) => {\n      state.matchedCommand = ''\n      state.searchedCommand = ''\n      state.isSearching = true\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setCliSettingsInitialState,\n  openCli,\n  toggleCli,\n  openCliHelper,\n  toggleCliHelper,\n  toggleHideCliHelper,\n  setMatchedCommand,\n  setSearchedCommand,\n  setSearchingCommand,\n  setSearchingCommandFilter,\n  clearSearchingCommand,\n  setCliEnteringCommand,\n  processCliClient,\n  processCliClientSuccess,\n  processCliClientFailure,\n  deleteCliClientSuccess,\n  resetCliClientUuid,\n  resetCliSettings,\n  resetCliHelperSettings,\n  getUnsupportedCommandsSuccess,\n  getBlockingCommandsSuccess,\n  goBackFromCommand,\n} = cliSettingsSlice.actions\n\n// A selector\nexport const cliSettingsSelector = (state: RootState) => state.cli.settings\nexport const cliUnsupportedCommandsSelector = (\n  state: RootState,\n  exclude: string[] = [],\n): string[] =>\n  state.cli.settings.unsupportedCommands.filter(\n    (command: string) => !exclude.includes(command.toLowerCase()),\n  )\n\n// The reducer\nexport default cliSettingsSlice.reducer\n\n// Asynchronous thunk action\nexport function createCliClientAction(\n  instanceId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: (message: string) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    dispatch(processCliClient())\n\n    try {\n      const { data, status } = await apiService.post<CreateCliClientResponse>(\n        getUrl(instanceId ?? '', ApiEndpoints.CLI),\n      )\n\n      if (isStatusSuccessful(status)) {\n        sessionStorageService.set(BrowserStorageItem.cliClientUuid, data?.uuid)\n        dispatch(processCliClientSuccess(data?.uuid))\n        dispatch(\n          setCliDbIndex(\n            state.connections?.instances?.connectedInstance?.db || 0,\n          ),\n        )\n\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(processCliClientFailure(errorMessage))\n      onFailAction?.(errorMessage)\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateCliClientAction(\n  uuid: string,\n  onSuccessAction?: () => void,\n  onFailAction?: (message: string) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(processCliClient())\n\n    try {\n      const state = stateInit()\n      const { data, status } = await apiService.patch<CreateCliClientResponse>(\n        getUrl(\n          state.connections.instances.connectedInstance?.id ?? '',\n          ApiEndpoints.CLI,\n          uuid,\n        ),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(processCliClientSuccess(data?.uuid))\n        dispatch(\n          setCliDbIndex(\n            state.connections?.instances?.connectedInstance?.db || 0,\n          ),\n        )\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(processCliClientFailure(errorMessage))\n      onFailAction?.(errorMessage)\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteCliClientAction(\n  instanceId: string,\n  uuid: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(processCliClient())\n\n    try {\n      const { status } = await apiService.delete<DeleteClientResponse>(\n        getUrl(instanceId, ApiEndpoints.CLI, uuid),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteCliClientSuccess())\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(processCliClientFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function resetCliSettingsAction(onSuccessAction?: () => void) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { contextInstanceId } = state.app.context\n    const cliClientUuid =\n      sessionStorageService.get(BrowserStorageItem.cliClientUuid) ?? ''\n\n    dispatch(resetCliSettings())\n    cliClientUuid &&\n      dispatch(\n        deleteCliClientAction(\n          contextInstanceId,\n          cliClientUuid,\n          onSuccessAction,\n        ),\n      )\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchBlockingCliCommandsAction(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(processCliClient())\n\n    try {\n      const { data, status } = await apiService.get<string[]>(\n        ApiEndpoints.CLI_BLOCKING_COMMANDS,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getBlockingCommandsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(processCliClientFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchUnsupportedCliCommandsAction(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(processCliClient())\n\n    try {\n      const { data, status } = await apiService.get<string[]>(\n        ApiEndpoints.CLI_UNSUPPORTED_COMMANDS,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUnsupportedCommandsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(processCliClientFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/cli/monitor.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { MonitorEvent } from 'uiSrc/constants'\n\nimport { IMonitorDataPayload, StateMonitor } from '../interfaces'\nimport { RootState } from '../store'\n\nexport const initialState: StateMonitor = {\n  loading: false,\n  loadingPause: false,\n  isShowMonitor: false,\n  isRunning: false,\n  isStarted: false,\n  isPaused: false,\n  isResumeLocked: false,\n  isSaveToFile: false,\n  isMinimizedMonitor: false,\n  socket: null,\n  error: '',\n  items: [],\n  logFileId: null,\n  timestamp: {\n    start: 0,\n    paused: 0,\n    unPaused: 0,\n    duration: 0,\n  },\n}\n\nexport const MONITOR_ITEMS_MAX_COUNT = 10_000\n\n// A slice for recipes\nconst monitorSlice = createSlice({\n  name: 'monitor',\n  initialState,\n  reducers: {\n    setMonitorInitialState: (state) => {\n      state.socket?.emit(MonitorEvent.FlushLogs)\n      state.socket?.removeAllListeners()\n      state.socket?.disconnect()\n      return { ...initialState }\n    },\n    // collapse / uncollapse Monitor\n    toggleMonitor: (state) => {\n      state.isShowMonitor = !state.isShowMonitor\n      state.isMinimizedMonitor = !state.isMinimizedMonitor\n    },\n\n    // uncollapse Monitor\n    showMonitor: (state) => {\n      state.isShowMonitor = true\n    },\n\n    // hide / unhide CLI Helper\n    toggleHideMonitor: (state) => {\n      state.isMinimizedMonitor = !state.isMinimizedMonitor\n    },\n\n    setSocket: (state, { payload }) => {\n      state.socket = payload\n      state.isStarted = true\n    },\n\n    startMonitor: (state, { payload }) => {\n      state.isRunning = true\n      state.error = ''\n      state.isSaveToFile = payload\n    },\n\n    setLogFileId: (state, { payload }) => {\n      state.logFileId = payload\n    },\n\n    setStartTimestamp: (state, { payload }) => {\n      state.timestamp.start = payload\n      state.timestamp.unPaused = state.timestamp.start\n    },\n\n    togglePauseMonitor: (state) => {\n      state.isPaused = !state.isPaused\n      if (!state.isPaused) {\n        state.timestamp.unPaused = Date.now()\n      }\n      if (state.isPaused) {\n        state.timestamp.paused = Date.now()\n        state.timestamp.duration +=\n          state.timestamp.paused - state.timestamp.unPaused\n      }\n    },\n\n    pauseMonitor: (state) => {\n      state.isPaused = true\n      state.timestamp.paused = Date.now()\n      state.timestamp.duration +=\n        state.timestamp.paused - state.timestamp.unPaused\n    },\n\n    setMonitorLoadingPause: (state, { payload }) => {\n      state.loadingPause = payload\n    },\n\n    stopMonitor: (state) => {\n      state.isRunning = false\n    },\n\n    resetProfiler: (state) => {\n      state.socket?.emit(MonitorEvent.FlushLogs)\n      state.socket?.removeAllListeners()\n      state.socket?.disconnect()\n      return {\n        ...initialState,\n        isShowMonitor: state.isShowMonitor,\n        isMinimizedMonitor: state.isMinimizedMonitor,\n      }\n    },\n\n    concatMonitorItems: (\n      state,\n      { payload }: { payload: IMonitorDataPayload[] },\n    ) => {\n      // small optimization to not unnecessary concat big arrays since we know max logs to show limitations\n      if (payload.length >= MONITOR_ITEMS_MAX_COUNT) {\n        state.items = payload.slice(-MONITOR_ITEMS_MAX_COUNT)\n        return\n      }\n\n      if (state.items.length + payload.length >= MONITOR_ITEMS_MAX_COUNT) {\n        // concat is faster for arrays\n        state.items = state.items\n          .slice(payload.length - MONITOR_ITEMS_MAX_COUNT)\n          .concat(payload)\n        return\n      }\n\n      state.items = state.items.concat(payload)\n    },\n\n    resetMonitorItems: (state) => {\n      state.items = []\n    },\n    setError: (state, { payload }) => {\n      state.error = payload\n    },\n\n    lockResume: (state) => {\n      state.isResumeLocked = true\n      state.isPaused = true\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setMonitorInitialState,\n  showMonitor,\n  toggleMonitor,\n  toggleHideMonitor,\n  setSocket,\n  togglePauseMonitor,\n  pauseMonitor,\n  lockResume,\n  startMonitor,\n  setStartTimestamp,\n  setLogFileId,\n  setMonitorLoadingPause,\n  stopMonitor,\n  resetProfiler,\n  concatMonitorItems,\n  resetMonitorItems,\n  setError,\n} = monitorSlice.actions\n\n// A selector\nexport const monitorSelector = (state: RootState) => state.cli.monitor\n\n// The reducer\nexport default monitorSlice.reducer\n"
  },
  {
    "path": "redisinsight/ui/src/slices/content/create-redis-buttons.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport {\n  ContentCreateRedis as IContentItem,\n  StateContentCreateRedis as IState,\n} from 'uiSrc/slices/interfaces/content'\nimport { resourcesService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: IState = {\n  data: {},\n  loading: false,\n  error: '',\n}\n\n// A slice for recipes\nconst createRedisButtonsSlice = createSlice({\n  name: 'createRedisButtons',\n  initialState,\n  reducers: {\n    getContent: (state) => {\n      state.loading = true\n    },\n    getContentSuccess: (\n      state,\n      { payload }: { payload: Record<string, any> },\n    ) => {\n      state.loading = false\n      state.data = payload\n    },\n    getContentFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const { getContent, getContentFailure, getContentSuccess } =\n  createRedisButtonsSlice.actions\n\n// A selector\nexport const contentSelector = (state: RootState) =>\n  state.content.createRedisButtons\n\n// The reducer\nexport default createRedisButtonsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchContentAction() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getContent())\n\n    try {\n      const { data, status } = await resourcesService.get<\n        Record<string, IContentItem>\n      >(ApiEndpoints.CONTENT_CREATE_DATABASE)\n      if (isStatusSuccessful(status)) {\n        dispatch(getContentSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getContentFailure(errorMessage))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/content/guide-links.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { StateContentGuideLinks } from 'uiSrc/slices/interfaces/content'\nimport { resourcesService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: StateContentGuideLinks = {\n  data: [],\n  loading: false,\n  error: '',\n}\n\n// A slice for recipes\nconst guideLinksContentSlice = createSlice({\n  name: 'createRedisButtons',\n  initialState,\n  reducers: {\n    getGuideLinks: (state) => {\n      state.loading = true\n    },\n    getGuideLinksSuccess: (state, { payload }) => {\n      state.loading = false\n      state.data = payload\n    },\n    getGuideLinksFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const { getGuideLinks, getGuideLinksSuccess, getGuideLinksFailure } =\n  guideLinksContentSlice.actions\n\n// A selector\nexport const guideLinksSelector = (state: RootState) => state.content.guideLinks\n\n// The reducer\nexport default guideLinksContentSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchGuideLinksAction() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getGuideLinks())\n\n    try {\n      const { data, status } = await resourcesService.get(\n        ApiEndpoints.CONTENT_GUIDE_LINKS,\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(getGuideLinksSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(getGuideLinksFailure(errorMessage))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/azure.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { cloneDeep } from 'lodash'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getApiErrorMessage,\n  getAxiosError,\n  isStatusSuccessful,\n} from 'uiSrc/utils'\nimport {\n  AzureRedisDatabase,\n  AzureRedisType,\n  AzureSubscription,\n  EnhancedAxiosError,\n  ImportAzureDatabaseResponse,\n  InitialStateAzure,\n  LoadedAzure,\n} from '../interfaces'\nimport {\n  addErrorNotification,\n  IAddInstanceErrorPayload,\n} from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: InitialStateAzure = {\n  loading: false,\n  error: '',\n  subscriptions: null,\n  selectedSubscription: null,\n  databases: null,\n  databasesAdded: [],\n  loaded: {\n    [LoadedAzure.Subscriptions]: false,\n    [LoadedAzure.Databases]: false,\n    [LoadedAzure.DatabasesAdded]: false,\n  },\n}\n\nconst azureSlice = createSlice({\n  name: 'azure',\n  initialState,\n  reducers: {\n    // Load subscriptions\n    loadSubscriptionsAzure: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadSubscriptionsAzureSuccess: (\n      state,\n      { payload }: PayloadAction<AzureSubscription[]>,\n    ) => {\n      state.loading = false\n      state.loaded[LoadedAzure.Subscriptions] = true\n      state.subscriptions = payload\n    },\n    loadSubscriptionsAzureFailure: (\n      state,\n      { payload }: PayloadAction<string>,\n    ) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // Select subscription\n    setSelectedSubscriptionAzure: (\n      state,\n      { payload }: PayloadAction<AzureSubscription | null>,\n    ) => {\n      // Only reset databases if subscription actually changed\n      const subscriptionChanged =\n        state.selectedSubscription?.subscriptionId !== payload?.subscriptionId\n\n      state.selectedSubscription = payload\n\n      if (subscriptionChanged) {\n        state.databases = null\n        state.databasesAdded = []\n        state.loaded[LoadedAzure.Databases] = false\n        state.loaded[LoadedAzure.DatabasesAdded] = false\n      }\n    },\n\n    // Load databases\n    loadDatabasesAzure: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadDatabasesAzureSuccess: (\n      state,\n      { payload }: PayloadAction<AzureRedisDatabase[]>,\n    ) => {\n      state.loading = false\n      state.loaded[LoadedAzure.Databases] = true\n      state.databases = payload\n    },\n    loadDatabasesAzureFailure: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // Add databases\n    addDatabasesAzure: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    addDatabasesAzureSuccess: (\n      state,\n      { payload }: PayloadAction<ImportAzureDatabaseResponse[]>,\n    ) => {\n      state.loading = false\n      state.loaded[LoadedAzure.DatabasesAdded] = true\n\n      // Map responses to databases with status\n      // Use databaseDetails from response, fallback to local state lookup\n      state.databasesAdded = payload.map((response) => {\n        const database =\n          response.databaseDetails ??\n          state.databases?.find((db) => db.id === response.id)\n\n        if (!database) {\n          // Should not happen, but handle gracefully\n          return {\n            id: response.id,\n            name: response.id,\n            subscriptionId: '',\n            resourceGroup: '',\n            location: '',\n            type: AzureRedisType.Standard,\n            host: '',\n            port: 0,\n            provisioningState: '',\n            statusAdded: response.status,\n            messageAdded: response.message,\n          }\n        }\n\n        return {\n          ...database,\n          statusAdded: response.status,\n          messageAdded: response.message,\n        }\n      })\n    },\n    addDatabasesAzureFailure: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // Reset\n    resetDataAzure: () => cloneDeep(initialState),\n\n    clearSubscriptionsAzure: (state) => {\n      state.subscriptions = null\n      state.selectedSubscription = null\n      state.databases = null\n      state.databasesAdded = []\n      state.loaded[LoadedAzure.Subscriptions] = false\n      state.loaded[LoadedAzure.Databases] = false\n      state.loaded[LoadedAzure.DatabasesAdded] = false\n    },\n    clearDatabasesAzure: (state) => {\n      state.databases = null\n      state.databasesAdded = []\n      state.loaded[LoadedAzure.Databases] = false\n      state.loaded[LoadedAzure.DatabasesAdded] = false\n    },\n  },\n})\n\nexport const {\n  loadSubscriptionsAzure,\n  loadSubscriptionsAzureSuccess,\n  loadSubscriptionsAzureFailure,\n  setSelectedSubscriptionAzure,\n  loadDatabasesAzure,\n  loadDatabasesAzureSuccess,\n  loadDatabasesAzureFailure,\n  addDatabasesAzure,\n  addDatabasesAzureSuccess,\n  addDatabasesAzureFailure,\n  resetDataAzure,\n  clearSubscriptionsAzure,\n  clearDatabasesAzure,\n} = azureSlice.actions\n\n// Selectors\nexport const azureSelector = (state: RootState) => state.connections.azure\n\nexport default azureSlice.reducer\n\n// Thunk actions\nexport function fetchSubscriptionsAzure(accountId: string) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadSubscriptionsAzure())\n\n    try {\n      const { data, status } = await apiService.get<AzureSubscription[]>(\n        ApiEndpoints.AZURE_SUBSCRIPTIONS,\n        { params: { accountId } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadSubscriptionsAzureSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(loadSubscriptionsAzureFailure(errorMessage))\n      dispatch(addErrorNotification(err as IAddInstanceErrorPayload))\n    }\n  }\n}\n\nexport function fetchDatabasesAzure(accountId: string, subscriptionId: string) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadDatabasesAzure())\n\n    try {\n      const { data, status } = await apiService.get<AzureRedisDatabase[]>(\n        `${ApiEndpoints.AZURE_SUBSCRIPTIONS}/${subscriptionId}/databases`,\n        { params: { accountId } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadDatabasesAzureSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(loadDatabasesAzureFailure(errorMessage))\n      dispatch(addErrorNotification(err as IAddInstanceErrorPayload))\n    }\n  }\n}\n\nexport function addDatabasesAzureAction(\n  accountId: string,\n  databaseIds: string[],\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(addDatabasesAzure())\n\n    try {\n      const { data, status } = await apiService.post<\n        ImportAzureDatabaseResponse[]\n      >(ApiEndpoints.AZURE_AUTODISCOVERY_DATABASES, {\n        accountId,\n        databases: databaseIds.map((id) => ({ id })),\n      })\n\n      if (isStatusSuccessful(status)) {\n        dispatch(addDatabasesAzureSuccess(data))\n        return data\n      }\n      return []\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addDatabasesAzureFailure(errorMessage))\n      dispatch(addErrorNotification(err as IAddInstanceErrorPayload))\n      return []\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/caCerts.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { addErrorNotification } from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState = {\n  loading: false,\n  error: '',\n  data: [],\n}\n\n// A slice for recipes\nconst caCertsSlice = createSlice({\n  name: 'caCerts',\n  initialState,\n  reducers: {\n    // load ca certificates\n    loadCaCerts: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadCaCertsSuccess: (state, { payload }) => {\n      state.data = payload\n      state.loading = false\n    },\n    loadCaCertsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // delete ca certificate\n    deleteCaCertificate: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    deleteCaCertificateSuccess: (state) => {\n      state.loading = false\n    },\n    deleteCaCertificateFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadCaCerts,\n  loadCaCertsSuccess,\n  loadCaCertsFailure,\n\n  deleteCaCertificate,\n  deleteCaCertificateSuccess,\n  deleteCaCertificateFailure,\n} = caCertsSlice.actions\n\n// A selector\nexport const caCertsSelector = (state: RootState) => state.connections.caCerts\n\n// The reducer\nexport default caCertsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchCaCerts() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadCaCerts())\n\n    try {\n      const { data, status } = await apiService.get(\n        `${ApiEndpoints.CA_CERTIFICATES}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadCaCertsSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadCaCertsFailure(errorMessage))\n    }\n  }\n}\n\nexport function deleteCaCertificateAction(\n  id: string,\n  onSuccessAction?: (id: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(deleteCaCertificate(id))\n\n    try {\n      const { status } = await apiService.delete(\n        `${ApiEndpoints.CA_CERTIFICATES}/${id}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteCaCertificateSuccess())\n        onSuccessAction?.(id)\n        dispatch(fetchCaCerts())\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(deleteCaCertificateFailure(errorMessage))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/clientCerts.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { addErrorNotification } from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState = {\n  loading: false,\n  error: '',\n  data: [],\n}\n\n// A slice for recipes\nconst clientCertsSlice = createSlice({\n  name: 'clientCerts',\n  initialState,\n  reducers: {\n    // load client certificates\n    loadClientCerts: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadClientCertsSuccess: (state, { payload }) => {\n      state.data = payload\n      state.loading = false\n    },\n    loadClientCertsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // delete client certificate\n    deleteClientCert: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    deleteClientCertSuccess: (state) => {\n      state.loading = false\n    },\n    deleteClientCertFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadClientCerts,\n  loadClientCertsSuccess,\n  loadClientCertsFailure,\n\n  deleteClientCert,\n  deleteClientCertSuccess,\n  deleteClientCertFailure,\n} = clientCertsSlice.actions\n\n// A selector\nexport const clientCertsSelector = (state: RootState) =>\n  state.connections.clientCerts\n\n// The reducer\nexport default clientCertsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchClientCerts() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadClientCerts())\n\n    try {\n      const { data, status } = await apiService.get(\n        `${ApiEndpoints.CLIENT_CERTIFICATES}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadClientCertsSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadClientCertsFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\nexport function deleteClientCertAction(\n  id: string,\n  onSuccessAction?: (id: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { status } = await apiService.delete(\n        `${ApiEndpoints.CLIENT_CERTIFICATES}/${id}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteClientCertSuccess())\n        onSuccessAction?.(id)\n        dispatch(fetchClientCerts())\n      }\n    } catch (error) {\n      dispatch(addErrorNotification(error))\n      dispatch(deleteClientCertFailure(getApiErrorMessage(error)))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/cloud.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { cloneDeep, find, map } from 'lodash'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getApiErrorMessage,\n  getApiErrorsFromBulkOperation,\n  getAxiosError,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\nimport { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors'\nimport {\n  EnhancedAxiosError,\n  ICredentialsRedisCloud,\n  InitialStateCloud,\n  InstanceRedisCloud,\n  LoadedCloud,\n  OAuthSocialAction,\n} from '../interfaces'\nimport { addErrorNotification } from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: InitialStateCloud = {\n  loading: false,\n  error: '',\n  data: null,\n  dataAdded: [],\n  subscriptions: null,\n  credentials: null,\n  ssoFlow: undefined,\n  isRecommendedSettings: undefined,\n  account: {\n    error: '',\n    data: null,\n  },\n  loaded: {\n    [LoadedCloud.Subscriptions]: false,\n    [LoadedCloud.Instances]: false,\n    [LoadedCloud.InstancesAdded]: false,\n  },\n}\n\n// A slice for recipes\nconst cloudSlice = createSlice({\n  name: 'cloud',\n  initialState,\n  reducers: {\n    // load redis cloud subscriptions\n    loadSubscriptionsRedisCloud: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadSubscriptionsRedisCloudSuccess: (state, { payload }) => {\n      state.loading = false\n      state.loaded[LoadedCloud.Subscriptions] = true\n\n      state.subscriptions = payload?.data\n      state.credentials = payload?.credentials\n    },\n    loadSubscriptionsRedisCloudFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // load redis cloud account\n    loadAccountRedisCloud: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadAccountRedisCloudSuccess: (state, { payload }) => {\n      state.loading = false\n      state.account = {\n        data: payload?.data,\n        error: '',\n      }\n    },\n    loadAccountRedisCloudFailure: (state, { payload }) => {\n      state.loading = false\n      state.account = {\n        data: null,\n        error: payload,\n      }\n    },\n\n    // load redis cloud instances\n    loadInstancesRedisCloud: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadInstancesRedisCloudSuccess: (state, { payload }) => {\n      state.loading = false\n      state.loaded[LoadedCloud.Instances] = true\n\n      state.data = map(payload?.data, (instance) => ({\n        ...instance,\n        free: !!instance?.cloudDetails?.free,\n        subscriptionName:\n          find(\n            state.subscriptions,\n            (subscription) => subscription.id === instance.subscriptionId,\n          )?.name ?? '',\n      }))\n    },\n    loadInstancesRedisCloudFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // add  redis cloud instances\n    createInstancesRedisCloud: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    createInstancesRedisCloudSuccess: (state, { payload }) => {\n      state.loading = false\n\n      state.loaded[LoadedCloud.InstancesAdded] = true\n\n      state.dataAdded = payload?.map((instance: InstanceRedisCloud) => ({\n        ...(instance.databaseDetails || {}),\n        databaseIdAdded: instance.databaseId,\n        subscriptionIdAdded: instance.subscriptionId,\n        statusAdded: instance.status,\n        messageAdded: instance.message,\n        subscriptionName:\n          find(\n            state.subscriptions,\n            (subscription) => subscription.id === instance.subscriptionId,\n          )?.name ?? '',\n      }))\n    },\n    createInstancesRedisCloudFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // reset data for cloud slice\n    resetDataRedisCloud: () => cloneDeep(initialState),\n    // reset data for cloud slice\n    resetSubscriptionsRedisCloud: (state) => {\n      state.subscriptions = null\n      state.data = null\n      state.dataAdded = []\n    },\n\n    // reset loaded field by LoadedCloud for cloud slice\n    resetLoadedRedisCloud: (state, { payload }: PayloadAction<LoadedCloud>) => {\n      state.loaded[payload] = false\n    },\n    setSSOFlow: (\n      state,\n      { payload }: PayloadAction<Maybe<OAuthSocialAction>>,\n    ) => {\n      state.ssoFlow = payload\n    },\n    setIsRecommendedSettingsSSO: (\n      state,\n      { payload }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      state.isRecommendedSettings = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadSubscriptionsRedisCloud,\n  loadSubscriptionsRedisCloudSuccess,\n  loadSubscriptionsRedisCloudFailure,\n  loadAccountRedisCloud,\n  loadAccountRedisCloudSuccess,\n  loadAccountRedisCloudFailure,\n  loadInstancesRedisCloud,\n  loadInstancesRedisCloudSuccess,\n  loadInstancesRedisCloudFailure,\n  createInstancesRedisCloud,\n  createInstancesRedisCloudSuccess,\n  createInstancesRedisCloudFailure,\n  resetDataRedisCloud,\n  resetSubscriptionsRedisCloud,\n  resetLoadedRedisCloud,\n  setSSOFlow,\n  setIsRecommendedSettingsSSO,\n} = cloudSlice.actions\n\n// A selector\nexport const cloudSelector = (state: RootState) => state.connections.cloud\n\n// The reducer\nexport default cloudSlice.reducer\n\nconst generateAuthHeaders = (\n  credentials: Nullable<ICredentialsRedisCloud>,\n) => ({\n  'x-cloud-api-key': credentials?.accessKey || '',\n  'x-cloud-api-secret': credentials?.secretKey || '',\n})\n\n// Asynchronous thunk action\nexport function fetchSubscriptionsRedisCloud(\n  credentials: Nullable<ICredentialsRedisCloud>,\n  isWithinOauth?: boolean,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadSubscriptionsRedisCloud())\n\n    try {\n      const { data, status } = await apiService.get(\n        isWithinOauth\n          ? `${ApiEndpoints.CLOUD_ME_AUTODISCOVERY_SUBSCRIPTIONS}`\n          : `${ApiEndpoints.REDIS_CLOUD_SUBSCRIPTIONS}`,\n        {\n          headers: {\n            ...(!isWithinOauth ? generateAuthHeaders(credentials) : {}),\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          loadSubscriptionsRedisCloudSuccess({\n            data,\n            credentials,\n          }),\n        )\n        onSuccessAction?.()\n        dispatch<any>(fetchAccountRedisCloud(credentials, isWithinOauth))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(loadSubscriptionsRedisCloudFailure(errorMessage))\n      dispatch(addErrorNotification(err))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchAccountRedisCloud(\n  credentials: Nullable<ICredentialsRedisCloud>,\n  isWithinOauth?: boolean,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadAccountRedisCloud())\n\n    try {\n      const { data, status } = await apiService.get(\n        isWithinOauth\n          ? `${ApiEndpoints.CLOUD_ME_AUTODISCOVERY_ACCOUNT}`\n          : `${ApiEndpoints.REDIS_CLOUD_ACCOUNT}`,\n        {\n          headers: {\n            ...(!isWithinOauth ? generateAuthHeaders(credentials) : {}),\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadAccountRedisCloudSuccess({ data }))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(loadAccountRedisCloudFailure(errorMessage))\n      dispatch(addErrorNotification(err))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchInstancesRedisCloud(\n  payload: {\n    subscriptions: Maybe<\n      Pick<InstanceRedisCloud, 'subscriptionId' | 'subscriptionType' | 'free'>\n    >[]\n    credentials: Nullable<ICredentialsRedisCloud>\n  },\n  isWithinOauth?: boolean,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadInstancesRedisCloud())\n\n    try {\n      const { data, status } = await apiService.post(\n        isWithinOauth\n          ? `${ApiEndpoints.CLOUD_ME_AUTODISCOVERY_GET_DATABASES}`\n          : `${ApiEndpoints.REDIS_CLOUD_GET_DATABASES}`,\n        {\n          subscriptions: payload.subscriptions,\n        },\n        {\n          headers: {\n            ...(!isWithinOauth ? generateAuthHeaders(payload.credentials) : {}),\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadInstancesRedisCloudSuccess({ data, credentials: payload }))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(loadInstancesRedisCloudFailure(errorMessage))\n      dispatch(addErrorNotification(err))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function addInstancesRedisCloud(\n  payload: {\n    databases: Pick<\n      InstanceRedisCloud,\n      'subscriptionId' | 'databaseId' | 'free'\n    >[]\n    credentials: Nullable<ICredentialsRedisCloud>\n  },\n  isWithinOauth?: boolean,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(createInstancesRedisCloud())\n\n    try {\n      const { data, status } = await apiService.post(\n        isWithinOauth\n          ? `${ApiEndpoints.CLOUD_ME_AUTODISCOVERY_DATABASES}`\n          : `${ApiEndpoints.REDIS_CLOUD_DATABASES}`,\n        {\n          databases: payload.databases,\n        },\n        {\n          headers: {\n            ...(!isWithinOauth ? generateAuthHeaders(payload.credentials) : {}),\n          },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        const encryptionErrors = getApiErrorsFromBulkOperation(\n          data,\n          ...ApiEncryptionErrors,\n        )\n        if (encryptionErrors.length) {\n          dispatch(addErrorNotification(encryptionErrors[0]))\n        }\n        dispatch(createInstancesRedisCloudSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as EnhancedAxiosError)\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(createInstancesRedisCloudFailure(errorMessage))\n      dispatch(addErrorNotification(err))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/cluster.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  getApiErrorMessage,\n  getApiErrorsFromBulkOperation,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\nimport { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors'\nimport {\n  ICredentialsRedisCluster,\n  InitialStateCluster,\n  InstanceRedisCluster,\n} from '../interfaces'\nimport { addErrorNotification } from '../app/notifications'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: InitialStateCluster = {\n  loading: false,\n  error: '',\n  data: null,\n  dataAdded: [],\n  credentials: null,\n}\n\n// A slice for recipes\nconst clusterSlice = createSlice({\n  name: 'cluster',\n  initialState,\n  reducers: {\n    // load  redis cluster instances\n    loadInstancesRedisCluster: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadInstancesRedisClusterSuccess: (state, { payload }) => {\n      state.data = payload?.data\n      state.loading = false\n      state.credentials = payload?.credentials\n    },\n    loadInstancesRedisClusterFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // add  redis cluster instances\n    createInstancesRedisCluster: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    createInstancesRedisClusterSuccess: (state, { payload }) => {\n      state.loading = false\n\n      state.dataAdded = payload?.map((instance: InstanceRedisCluster) => ({\n        ...(instance.databaseDetails || {}),\n        uidAdded: instance.uid,\n        statusAdded: instance.status,\n        messageAdded: instance.message,\n      }))\n    },\n    createInstancesRedisClusterFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // reset data for cluster slice\n    resetDataRedisCluster: () => initialState,\n\n    // reset instances for cluster slice\n    resetInstancesRedisCluster: (state) => ({\n      ...initialState,\n      credentials: state.credentials,\n    }),\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadInstancesRedisCluster,\n  loadInstancesRedisClusterSuccess,\n  loadInstancesRedisClusterFailure,\n  createInstancesRedisCluster,\n  createInstancesRedisClusterSuccess,\n  createInstancesRedisClusterFailure,\n  resetDataRedisCluster,\n  resetInstancesRedisCluster,\n} = clusterSlice.actions\n\n// A selector\nexport const clusterSelector = (state: RootState) => state.connections.cluster\n\n// The reducer\nexport default clusterSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchInstancesRedisCluster(\n  payload: ICredentialsRedisCluster,\n  onSuccessAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadInstancesRedisCluster())\n\n    try {\n      const { data, status } = await apiService.post(\n        `${ApiEndpoints.REDIS_CLUSTER_GET_DATABASES}`,\n        { ...payload },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          loadInstancesRedisClusterSuccess({ data, credentials: payload }),\n        )\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadInstancesRedisClusterFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function addInstancesRedisCluster(payload: {\n  uids: Maybe<number>[]\n  credentials: Nullable<ICredentialsRedisCluster>\n}) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(createInstancesRedisCluster())\n\n    try {\n      const { data, status } = await apiService.post(\n        `${ApiEndpoints.REDIS_CLUSTER_DATABASES}`,\n        { uids: payload.uids, ...payload.credentials },\n      )\n\n      if (isStatusSuccessful(status)) {\n        const encryptionErrors = getApiErrorsFromBulkOperation(\n          data,\n          ...ApiEncryptionErrors,\n        )\n        if (encryptionErrors.length) {\n          dispatch(addErrorNotification(encryptionErrors[0]))\n        }\n        dispatch(createInstancesRedisClusterSuccess(data))\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(createInstancesRedisClusterFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/instances.ts",
    "content": "import { filter, first, get, isNull, map, orderBy } from 'lodash'\nimport { createSlice } from '@reduxjs/toolkit'\nimport axios, { AxiosError } from 'axios'\n\nimport ApiErrors from 'uiSrc/constants/apiErrors'\nimport {\n  instancesService,\n  localStorageService,\n  sessionStorageService,\n} from 'uiSrc/services'\nimport {\n  BrowserStorageItem,\n  COLUMN_FIELD_NAME_MAP,\n  CustomErrorCodes,\n  DatabaseListColumn,\n} from 'uiSrc/constants'\nimport { setAppContextInitialState } from 'uiSrc/slices/app/context'\nimport { resetKeys } from 'uiSrc/slices/browser/keys'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport {\n  checkRediStack,\n  ensureSemanticVersion,\n  getApiErrorMessage,\n  Nullable,\n} from 'uiSrc/utils'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { Database as DatabaseInstanceResponse } from 'apiSrc/modules/database/models/database'\nimport { RedisNodeInfoResponse } from 'apiSrc/modules/database/dto/redis-info.dto'\nimport { ExportDatabase } from 'apiSrc/modules/database/models/export-database'\n\nimport { fetchMastersSentinelAction } from './sentinel'\nimport { fetchTags } from './tags'\nimport { AppDispatch, RootState } from '../store'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  addMessageNotification,\n  removeInfiniteNotification,\n} from '../app/notifications'\nimport { ConnectionType, InitialStateInstances, Instance } from '../interfaces'\n\nconst HIDE_CREATING_DB_DELAY_MS = 500\n\nexport const initialState: InitialStateInstances = {\n  loading: false,\n  error: '',\n  data: [],\n  loadingChanging: false,\n  errorChanging: '',\n  changedSuccessfully: false,\n  deletedSuccessfully: false,\n  freeInstances: [],\n  connectedInstance: {\n    id: '',\n    name: '',\n    host: '',\n    port: 0,\n    version: '',\n    nameFromProvider: '',\n    lastConnection: new Date(),\n    connectionType: ConnectionType.Standalone,\n    isRediStack: false,\n    modules: [],\n    loading: undefined,\n  },\n  editedInstance: {\n    loading: false,\n    error: '',\n    data: null,\n  },\n  instanceOverview: {\n    version: '',\n  },\n  instanceInfo: {\n    version: '',\n    server: {},\n  },\n  importInstances: {\n    loading: false,\n    error: '',\n    data: null,\n  },\n  shownColumns: [...COLUMN_FIELD_NAME_MAP.keys()],\n}\n\n// A slice for recipes\nconst instancesSlice = createSlice({\n  name: 'instances',\n  initialState,\n  reducers: {\n    // load instances\n    loadInstances: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadInstancesSuccess: (\n      state,\n      { payload }: { payload: DatabaseInstanceResponse[] },\n    ) => {\n      const data = checkRediStack(payload)\n      state.data = data.map(ensureSemanticVersion)\n      state.loading = false\n      state.freeInstances =\n        (filter(\n          [...orderBy(payload, 'lastConnection', 'desc')],\n          'cloudDetails.free',\n        ) as unknown as Instance[]) || null\n      if (state.connectedInstance.id) {\n        const isRediStack = state.data.find(\n          (db) => db.id === state.connectedInstance.id,\n        )?.isRediStack\n        state.connectedInstance.isRediStack = isRediStack || false\n      }\n    },\n    loadInstancesFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // add/edit instance\n    defaultInstanceChanging: (state) => {\n      state.loadingChanging = true\n      state.changedSuccessfully = false\n      state.errorChanging = ''\n    },\n    defaultInstanceChangingSuccess: (state) => {\n      state.changedSuccessfully = true\n      state.loadingChanging = false\n    },\n    defaultInstanceChangingFailure: (state, { payload = '' }) => {\n      state.loadingChanging = false\n      state.changedSuccessfully = false\n      state.errorChanging = payload.toString()\n    },\n\n    // test database connection\n    testConnection: (state) => {\n      state.loadingChanging = true\n      state.errorChanging = ''\n    },\n    testConnectionSuccess: (state) => {\n      state.loadingChanging = false\n    },\n    testConnectionFailure: (state, { payload = '' }) => {\n      state.loadingChanging = false\n      state.errorChanging = payload.toString()\n    },\n\n    changeInstanceAlias: (state) => {\n      state.loadingChanging = true\n      state.errorChanging = ''\n    },\n    changeInstanceAliasSuccess: (state, { payload }) => {\n      const { id, name } = payload\n      state.data = state.data.map((item: Instance) => {\n        if (item.id === id) {\n          item.name = name\n        }\n        return item\n      })\n      state.loadingChanging = false\n    },\n    changeInstanceAliasFailure: (state, { payload = '' }) => {\n      state.loadingChanging = false\n      state.errorChanging = payload.toString()\n    },\n\n    resetInstanceUpdate: (state) => {\n      state.loadingChanging = false\n    },\n\n    // delete instances\n    setDefaultInstance: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    setDefaultInstanceSuccess: (state) => {\n      state.loading = false\n    },\n    setDefaultInstanceFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    getDatabaseConfigInfo: (state) => {\n      state.error = ''\n    },\n    getDatabaseConfigInfoSuccess: (state, { payload }) => {\n      state.instanceOverview = {\n        ...payload,\n        version: payload?.version || state.instanceOverview.version || '',\n      }\n    },\n    getDatabaseConfigInfoFailure: (state, { payload }) => {\n      state.error = payload\n    },\n\n    // set connected instance id\n    setConnectedInstanceId: (state, { payload }: { payload: string }) => {\n      state.connectedInstance = {\n        ...state.connectedInstance,\n        id: payload,\n      }\n    },\n\n    // set connected instance\n    setConnectedInstance: (state) => {\n      state.connectedInstance.loading = true\n    },\n\n    // set connected instance success\n    setConnectedInstanceSuccess: (\n      state,\n      { payload }: { payload: Instance },\n    ) => {\n      const isRediStack = state.data?.find(\n        (db) => db.id === state.connectedInstance.id,\n      )?.isRediStack\n      state.connectedInstance = ensureSemanticVersion(payload)\n      state.connectedInstance.loading = false\n      state.connectedInstance.isRediStack = isRediStack || false\n      state.connectedInstance.isFreeDb = payload.cloudDetails?.free || false\n      state.connectedInstance.db =\n        sessionStorageService.get(\n          `${BrowserStorageItem.dbIndex}${payload.id}`,\n        ) ?? payload.db\n    },\n\n    setConnectedInfoInstance: (state) => {\n      state.instanceInfo = initialState.instanceInfo\n    },\n\n    setConnectedInfoInstanceSuccess: (\n      state,\n      { payload }: { payload: RedisNodeInfoResponse },\n    ) => {\n      state.instanceInfo = payload\n    },\n\n    // set edited instance\n    setEditedInstance: (\n      state,\n      { payload }: { payload: Nullable<Instance> },\n    ) => {\n      state.editedInstance.data = payload\n    },\n\n    updateEditedInstance: (\n      state,\n      { payload }: { payload: Nullable<Instance> },\n    ) => {\n      if (isNull(state.editedInstance.data)) {\n        state.editedInstance.data = payload\n      } else {\n        state.editedInstance.data = {\n          ...state.editedInstance.data,\n          ...payload,\n        }\n      }\n    },\n\n    setConnectedInstanceFailure: (state) => {\n      state.connectedInstance.loading = false\n    },\n\n    // reset connected instance\n    resetConnectedInstance: (state) => {\n      state.connectedInstance = initialState.connectedInstance\n    },\n\n    importInstancesFromFile: (state) => {\n      state.importInstances.loading = true\n      state.importInstances.error = ''\n    },\n\n    importInstancesFromFileSuccess: (state, { payload }) => {\n      state.importInstances.loading = false\n      state.importInstances.data = payload\n    },\n\n    importInstancesFromFileFailure: (state, { payload }) => {\n      state.importInstances.loading = false\n      state.importInstances.error = payload\n    },\n\n    resetImportInstances: (state) => {\n      state.importInstances = initialState.importInstances\n    },\n\n    checkDatabaseIndex: (state) => {\n      state.connectedInstance.loading = true\n    },\n    checkDatabaseIndexSuccess: (state, { payload }) => {\n      state.connectedInstance.db = payload\n      state.connectedInstance.loading = false\n\n      sessionStorageService.set(\n        `${BrowserStorageItem.dbIndex}${state.connectedInstance.id}`,\n        payload,\n      )\n    },\n    checkDatabaseIndexFailure: (state) => {\n      state.connectedInstance.loading = false\n    },\n    setShownColumns: (\n      state,\n      { payload }: { payload: DatabaseListColumn[] },\n    ) => {\n      state.shownColumns = [...payload]\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadInstances,\n  loadInstancesSuccess,\n  loadInstancesFailure,\n  defaultInstanceChanging,\n  defaultInstanceChangingSuccess,\n  defaultInstanceChangingFailure,\n  testConnection,\n  testConnectionSuccess,\n  testConnectionFailure,\n  setDefaultInstance,\n  setDefaultInstanceSuccess,\n  setDefaultInstanceFailure,\n  setConnectedInstanceSuccess,\n  setConnectedInstanceFailure,\n  setConnectedInstance,\n  setConnectedInstanceId,\n  resetConnectedInstance,\n  getDatabaseConfigInfo,\n  getDatabaseConfigInfoSuccess,\n  getDatabaseConfigInfoFailure,\n  changeInstanceAlias,\n  changeInstanceAliasSuccess,\n  changeInstanceAliasFailure,\n  resetInstanceUpdate,\n  setEditedInstance,\n  updateEditedInstance,\n  importInstancesFromFile,\n  importInstancesFromFileSuccess,\n  importInstancesFromFileFailure,\n  resetImportInstances,\n  checkDatabaseIndex,\n  checkDatabaseIndexSuccess,\n  checkDatabaseIndexFailure,\n  setConnectedInfoInstance,\n  setConnectedInfoInstanceSuccess,\n  setShownColumns,\n} = instancesSlice.actions\n\n// selectors\nexport const instancesSelector = (state: RootState) =>\n  state.connections.instances\nexport const freeInstancesSelector = (state: RootState) =>\n  state.connections.instances.freeInstances\nexport const connectedInstanceSelector = (state: RootState) =>\n  state.connections.instances.connectedInstance\nexport const connectedInstanceCDSelector = (state: RootState) =>\n  state.connections.instances.connectedInstance.cloudDetails\nexport const connectedInstanceInfoSelector = (state: RootState) =>\n  state.connections.instances.instanceInfo\nexport const editedInstanceSelector = (state: RootState) =>\n  state.connections.instances.editedInstance\nexport const connectedInstanceOverviewSelector = (state: RootState) =>\n  state.connections.instances.instanceOverview\nexport const importInstancesSelector = (state: RootState) =>\n  state.connections.instances.importInstances\n\n// The reducer\nexport default instancesSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchInstancesAction(onSuccess?: (data: Instance[]) => void) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const envDependentFeature = get(stateInit(), [\n      'app',\n      'features',\n      'featureFlags',\n      'features',\n      'envDependent',\n    ])\n\n    if (!envDependentFeature?.flag) {\n      return\n    }\n\n    dispatch(loadInstances())\n\n    try {\n      const data = await instancesService.listDatabases()\n\n      if (data !== null) {\n        localStorageService.set(BrowserStorageItem.instancesCount, data?.length)\n        onSuccess?.(data as Instance[])\n        dispatch(loadInstancesSuccess(data))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadInstancesFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n\n      localStorageService.set(BrowserStorageItem.instancesCount, '0')\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function createInstanceStandaloneAction(\n  payload: Instance,\n  onRedirectToSentinel?: () => void,\n  onSuccess?: (id: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(defaultInstanceChanging())\n\n    try {\n      const data = await instancesService.createInstance(payload)\n\n      if (data !== null) {\n        dispatch(defaultInstanceChangingSuccess())\n        dispatch<any>(fetchInstancesAction())\n\n        dispatch(\n          addMessageNotification(\n            successMessages.ADDED_NEW_INSTANCE(payload.name ?? ''),\n          ),\n        )\n        onSuccess?.(data.id)\n      }\n    } catch (_error) {\n      const error = _error as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      const errorCode = get(\n        error,\n        'response.data.errorCode',\n        0,\n      ) as CustomErrorCodes\n\n      if (errorCode === CustomErrorCodes.DatabaseAlreadyExists) {\n        const databaseId: string = get(\n          error,\n          'response.data.resource.databaseId',\n          '',\n        )\n\n        dispatch(\n          autoCreateAndConnectToInstanceActionSuccess(\n            databaseId,\n            successMessages.DATABASE_ALREADY_EXISTS(),\n            () => {\n              dispatch(defaultInstanceChangingSuccess())\n              onSuccess?.(databaseId)\n            },\n            () => {\n              dispatch(defaultInstanceChangingFailure(errorMessage))\n            },\n          ),\n        )\n        return\n      }\n\n      dispatch(defaultInstanceChangingFailure(errorMessage))\n\n      if (error?.response?.data?.error === ApiErrors.SentinelParamsRequired) {\n        checkoutToSentinelFlow(payload, dispatch, onRedirectToSentinel)\n        return\n      }\n\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function autoCreateAndConnectToInstanceAction(\n  payload: Instance,\n  onSuccess?: (id: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(\n      addInfiniteNotification(INFINITE_MESSAGES.AUTO_CREATING_DATABASE()),\n    )\n\n    try {\n      const data = await instancesService.createInstance(payload)\n\n      if (data !== null) {\n        await dispatch(\n          autoCreateAndConnectToInstanceActionSuccess(\n            data?.id,\n            successMessages.ADDED_NEW_INSTANCE(data?.name),\n            onSuccess,\n          ),\n        )\n      }\n    } catch (error) {\n      const errorCode = get(\n        error,\n        'response.data.errorCode',\n        0,\n      ) as CustomErrorCodes\n\n      if (errorCode === CustomErrorCodes.DatabaseAlreadyExists) {\n        const databaseId = get(error, 'response.data.resource.databaseId', '')\n\n        dispatch(\n          autoCreateAndConnectToInstanceActionSuccess(\n            databaseId,\n            successMessages.DATABASE_ALREADY_EXISTS(),\n            onSuccess,\n          ),\n        )\n        return\n      }\n      dispatch(addErrorNotification(error as AxiosError))\n      dispatch(removeInfiniteNotification(InfiniteMessagesIds.autoCreateDb))\n    }\n  }\n}\n\nfunction autoCreateAndConnectToInstanceActionSuccess(\n  id: string,\n  message: any,\n  onSuccess?: (id: string) => void,\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const isConnectedId = state.app?.context?.contextInstanceId === id\n      if (!isConnectedId) {\n        dispatch(resetKeys())\n        dispatch(setAppContextInitialState())\n        dispatch(setConnectedInstanceId(id ?? ''))\n      }\n      dispatch(\n        checkConnectToInstanceAction(\n          id,\n          (id) => {\n            setTimeout(() => {\n              dispatch(\n                removeInfiniteNotification(InfiniteMessagesIds.autoCreateDb),\n              )\n              dispatch(addMessageNotification(message))\n              onSuccess?.(id)\n            }, HIDE_CREATING_DB_DELAY_MS)\n          },\n          () => {\n            dispatch(\n              removeInfiniteNotification(InfiniteMessagesIds.autoCreateDb),\n            )\n            onFail?.()\n          },\n          !isConnectedId,\n        ),\n      )\n    } catch (error) {\n      // process error if needed\n    }\n  }\n}\n\nexport type PartialInstance = Partial<Omit<Instance, 'tags'>> & {\n  tags?: {\n    key: string\n    value: string\n  }[]\n}\n\n// Asynchronous thunk action\nexport function updateInstanceAction(\n  { id, ...payload }: PartialInstance,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(defaultInstanceChanging())\n\n    try {\n      const result = await instancesService.updateInstance(id!, payload)\n\n      if (result) {\n        dispatch(defaultInstanceChangingSuccess())\n        dispatch<any>(fetchInstancesAction())\n        if (payload.tags) {\n          dispatch(fetchTags())\n        }\n        onSuccess?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(defaultInstanceChangingFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function cloneInstanceAction(\n  { id, ...payload }: Partial<Instance>,\n  onSuccess?: (id?: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(defaultInstanceChanging())\n\n    try {\n      const data = await instancesService.cloneInstance(id!, payload)\n\n      if (data !== null) {\n        dispatch(defaultInstanceChangingSuccess())\n        dispatch<any>(fetchInstancesAction())\n\n        dispatch(\n          addMessageNotification(\n            successMessages.ADDED_NEW_INSTANCE(data.name ?? ''),\n          ),\n        )\n        onSuccess?.(id)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(defaultInstanceChangingFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteInstancesAction(\n  instances: Instance[],\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(setDefaultInstance())\n\n    try {\n      const state = stateInit()\n      const databasesIds = map(instances, 'id')\n      const result = await instancesService.deleteInstances(databasesIds)\n\n      if (result) {\n        dispatch(setDefaultInstanceSuccess())\n        dispatch<any>(fetchInstancesAction())\n        dispatch(fetchTags())\n\n        if (databasesIds.includes(state.app.context.contextInstanceId)) {\n          dispatch(resetConnectedInstance())\n          dispatch(resetKeys())\n          dispatch(setAppContextInitialState())\n        }\n        onSuccess?.()\n\n        if (instances.length === 1) {\n          dispatch(\n            addMessageNotification(\n              successMessages.DELETE_INSTANCE(first(instances)?.name ?? ''),\n            ),\n          )\n        } else {\n          dispatch(\n            addMessageNotification(\n              successMessages.DELETE_INSTANCES(map(instances, 'name')),\n            ),\n          )\n        }\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function exportInstancesAction(\n  ids: string[],\n  withSecrets: boolean,\n  onSuccess?: (data: ExportDatabase) => void,\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setDefaultInstance())\n\n    try {\n      const data = await instancesService.exportInstances(ids, withSecrets)\n\n      if (data !== null) {\n        dispatch(setDefaultInstanceSuccess())\n\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n      onFail?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchConnectedInstanceAction(\n  id: string,\n  onSuccess?: () => void,\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setDefaultInstance())\n    dispatch(setConnectedInstance())\n\n    try {\n      const data = await instancesService.getInstance(id)\n\n      if (data !== null) {\n        dispatch(setConnectedInstanceSuccess(data))\n        dispatch(setDefaultInstanceSuccess())\n      }\n      onSuccess?.()\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n      onFail?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchConnectedInstanceInfoAction(\n  id: string,\n  onSuccess?: () => void,\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setConnectedInfoInstance())\n\n    try {\n      const data = await instancesService.getInstanceInfo(id)\n\n      if (data !== null) {\n        dispatch(setConnectedInfoInstanceSuccess(data))\n        onSuccess?.()\n      } else {\n        onFail?.()\n      }\n    } catch (error) {\n      onFail?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchEditedInstanceAction(\n  instance: Instance,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setDefaultInstance())\n    dispatch(setEditedInstance(instance))\n\n    try {\n      const data = await instancesService.getInstance(instance.id)\n\n      if (data !== null) {\n        dispatch(updateEditedInstance(data))\n        dispatch(setDefaultInstanceSuccess())\n      }\n      onSuccess?.()\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setEditedInstance(null))\n      dispatch(setConnectedInstanceFailure())\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function checkConnectToInstanceAction(\n  id: string = '',\n  onSuccessAction?: (id: string) => void,\n  onFailAction?: () => void,\n  resetInstance: boolean = true,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setDefaultInstance())\n    resetInstance && dispatch(resetConnectedInstance())\n    try {\n      const result = await instancesService.connectInstance(id)\n\n      if (result) {\n        dispatch(setDefaultInstanceSuccess())\n        onSuccessAction?.(id)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification({ ...error, instanceId: id }))\n      onFailAction?.()\n    }\n  }\n}\n\nconst checkoutToSentinelFlow = (\n  payload: Instance,\n  dispatch: AppDispatch,\n  onRedirectToSentinel?: () => void,\n) => {\n  const payloadSentinel = { ...payload }\n  delete payloadSentinel.name\n  delete payloadSentinel.db\n\n  dispatch<any>(\n    fetchMastersSentinelAction(payloadSentinel, onRedirectToSentinel),\n  )\n}\n\n// Asynchronous thunk action\nexport function getDatabaseConfigInfoAction(\n  id: string,\n  onSuccessAction?: (id: string) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getDatabaseConfigInfo())\n    try {\n      const data = await instancesService.getInstanceOverview(id)\n\n      if (data !== null) {\n        dispatch(getDatabaseConfigInfoSuccess(data))\n        onSuccessAction?.(id)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getDatabaseConfigInfoFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function changeInstanceAliasAction(\n  id: string = '',\n  name: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(changeInstanceAlias())\n\n    try {\n      const result = await instancesService.updateInstanceAlias(id, name)\n      if (result) {\n        dispatch(changeInstanceAliasSuccess({ id, name }))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      if (!axios.isCancel(error)) {\n        const errorMessage = getApiErrorMessage(error)\n        dispatch(changeInstanceAliasFailure(errorMessage))\n        dispatch(addErrorNotification(error))\n        onFailAction?.()\n      }\n    }\n  }\n}\n\nexport function checkDatabaseIndexAction(\n  id: string,\n  index: number,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(checkDatabaseIndex())\n\n    try {\n      const result = await instancesService.checkInstanceDbIndex(id, index)\n\n      if (result) {\n        dispatch(checkDatabaseIndexSuccess(index))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(checkDatabaseIndexFailure())\n      dispatch(addErrorNotification(error))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function resetInstanceUpdateAction() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(resetInstanceUpdate())\n    instancesService.sourceInstance?.source?.cancel?.()\n  }\n}\n\n// Asynchronous thunk action\nexport function uploadInstancesFile(\n  file: FormData,\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(importInstancesFromFile())\n\n    try {\n      const data = await instancesService.importInstances(file)\n\n      if (data !== null) {\n        dispatch(fetchTags())\n        dispatch(importInstancesFromFileSuccess(data))\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(importInstancesFromFileFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function testInstanceStandaloneAction(\n  { id, ...payload }: Partial<Instance>,\n  onRedirectToSentinel?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(testConnection())\n    try {\n      const result = await instancesService.testInstanceConnection(id, payload)\n      if (result) {\n        dispatch(testConnectionSuccess())\n\n        dispatch(addMessageNotification(successMessages.TEST_CONNECTION()))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n\n      dispatch(testConnectionFailure(errorMessage))\n\n      if (error?.response?.data?.error === ApiErrors.SentinelParamsRequired) {\n        checkoutToSentinelFlow(\n          { id, ...payload },\n          dispatch,\n          onRedirectToSentinel,\n        )\n        return\n      }\n\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/sentinel.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\n\nimport {\n  getApiErrorMessage,\n  getApiErrorsFromBulkOperation,\n  isStatusSuccessful,\n  parseAddedMastersSentinel,\n  parseMastersSentinel,\n} from 'uiSrc/utils'\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors'\n\nimport { SentinelMaster } from 'apiSrc/modules/redis-sentinel/models/sentinel-master'\nimport { CreateSentinelDatabasesDto } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.databases.dto'\nimport { CreateSentinelDatabaseResponse } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.response'\nimport {\n  AddRedisDatabaseStatus,\n  InitialStateSentinel,\n  Instance,\n  LoadedSentinel,\n  ModifiedSentinelMaster,\n} from '../interfaces'\nimport { AppDispatch, RootState } from '../store'\nimport { addErrorNotification } from '../app/notifications'\nimport type { ActionStatus } from 'src/common/models'\n\nexport const initialState: InitialStateSentinel = {\n  loading: false,\n  error: '',\n  instance: null,\n  data: [],\n  statuses: [],\n  loaded: {\n    [LoadedSentinel.Masters]: false,\n  },\n}\n\nexport const initialStateSentinelStatus: CreateSentinelDatabaseResponse = {\n  name: '',\n  status: AddRedisDatabaseStatus.Success as unknown as ActionStatus,\n  message: '',\n}\n\n// A slice for recipes\nconst sentinelSlice = createSlice({\n  name: 'sentinel',\n  initialState,\n  reducers: {\n    setInstanceSentinel: (state, { payload }: { payload: Instance }) => {\n      state.instance = payload\n    },\n    // load redis sentinel subscriptions\n    loadMastersSentinel: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadMastersSentinelSuccess: (\n      state,\n      { payload }: { payload: SentinelMaster[] },\n    ) => {\n      state.loading = false\n      state.loaded[LoadedSentinel.Masters] = true\n      state.data = parseMastersSentinel(payload)\n    },\n    loadMastersSentinelFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    updateMastersSentinel: (\n      state,\n      { payload }: { payload: ModifiedSentinelMaster[] },\n    ) => {\n      state.data = payload\n    },\n\n    // add redis master sentinel\n    createMastersSentinel: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    createMastersSentinelSuccess: (\n      state,\n      { payload }: { payload: CreateSentinelDatabasesDto[] },\n    ) => {\n      state.loading = false\n\n      state.loaded[LoadedSentinel.MastersAdded] = true\n\n      state.statuses = payload\n      state.data = parseAddedMastersSentinel(state.data, payload)\n    },\n    createMastersSentinelFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // reset data for sentinel slice\n    resetDataSentinel: () => cloneDeep(initialState),\n\n    // reset loaded field by LoadedSentinel for sentinel slice\n    resetLoadedSentinel: (\n      state,\n      { payload }: PayloadAction<LoadedSentinel>,\n    ) => {\n      state.loaded[payload] = false\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setInstanceSentinel,\n  loadMastersSentinel,\n  loadMastersSentinelSuccess,\n  loadMastersSentinelFailure,\n  updateMastersSentinel,\n  createMastersSentinel,\n  createMastersSentinelSuccess,\n  createMastersSentinelFailure,\n  resetDataSentinel,\n  resetLoadedSentinel,\n} = sentinelSlice.actions\n\n// A selector\nexport const sentinelSelector = (state: RootState) => state.connections.sentinel\n\n// The reducer\nexport default sentinelSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchMastersSentinelAction(\n  payload: Instance,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadMastersSentinel())\n\n    try {\n      const { status, data } = await apiService.post<SentinelMaster[]>(\n        `${ApiEndpoints.SENTINEL_GET_DATABASES}`,\n        payload,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setInstanceSentinel(payload))\n        dispatch(loadMastersSentinelSuccess(data))\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadMastersSentinelFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function createMastersSentinelAction(\n  payload: {\n    alias: string\n    name: string\n    username?: string\n    password?: string\n    db?: number\n  }[],\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(createMastersSentinel())\n\n    const state = stateInit()\n    try {\n      const { instance } = state.connections.sentinel\n      const { data, status } = await apiService.post<\n        CreateSentinelDatabaseResponse[]\n      >(`${ApiEndpoints.SENTINEL_DATABASES}`, {\n        ...instance,\n        masters: payload,\n      })\n\n      if (isStatusSuccessful(status)) {\n        const encryptionErrors = getApiErrorsFromBulkOperation(\n          data,\n          ...ApiEncryptionErrors,\n        )\n        if (encryptionErrors.length) {\n          dispatch(addErrorNotification(encryptionErrors[0]))\n        }\n        dispatch(createMastersSentinelSuccess(data))\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(createMastersSentinelFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/instances/tags.ts",
    "content": "import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { RootState } from '../store'\nimport { InitialTagsState } from '../interfaces/tag'\n\nexport const initialState: InitialTagsState = {\n  data: [],\n  selectedTags: new Set<string>(),\n  loading: false,\n  error: null,\n}\n\nexport const fetchTags = createAsyncThunk('tags/fetchTags', async () => {\n  const response = await apiService.get(ApiEndpoints.TAGS)\n  return response.data\n})\n\nconst tagsSlice = createSlice({\n  name: 'tags',\n  initialState,\n  reducers: {\n    setSelectedTags: (\n      state,\n      action: PayloadAction<InitialTagsState['selectedTags']>,\n    ) => {\n      state.selectedTags = action.payload\n    },\n  },\n  extraReducers: (builder) => {\n    builder\n      .addCase(fetchTags.pending, (state) => {\n        state.loading = true\n        state.error = null\n      })\n      .addCase(fetchTags.fulfilled, (state, action) => {\n        state.loading = false\n        state.data = action.payload\n      })\n      .addCase(fetchTags.rejected, (state, action) => {\n        state.loading = false\n        state.error = action.error.message || 'Failed to fetch tags'\n      })\n  },\n})\n\nexport const { setSelectedTags } = tagsSlice.actions\n\nexport const tagsSelector = (state: RootState) => state.connections.tags\n\nexport default tagsSlice.reducer\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/aiAssistant.ts",
    "content": "export enum AiChatType {\n  Assistance = 'document',\n  Query = 'database',\n}\n\nexport enum AiChatMessageType {\n  AIMessage = 'AIMessage',\n  HumanMessage = 'HumanMessage',\n}\n\nexport interface AiChatMessage {\n  id: string\n  type: AiChatMessageType\n  content: string\n  error?: {\n    statusCode: number\n    errorCode?: number\n  }\n  context?: {\n    [key: string]: {\n      title: string\n      category: string\n    }\n  }\n}\n\nexport interface StateAiAssistant {\n  activeTab: AiChatType\n  assistant: {\n    loading: boolean\n    agreements: boolean\n    id: string\n    messages: Array<AiChatMessage>\n  }\n  expert: {\n    loading: boolean\n    agreements: string[]\n    messages: Array<AiChatMessage>\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/analytics.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models'\nimport { ClusterDetails } from 'apiSrc/modules/cluster-monitor/models/cluster-details'\nimport {\n  DatabaseAnalysis,\n  ShortDatabaseAnalysis,\n} from 'apiSrc/modules/database-analysis/models'\n\nexport interface StateSlowLog {\n  loading: boolean\n  error: string\n  data: SlowLog[]\n  lastRefreshTime: Nullable<number>\n  config: Nullable<SlowLogConfig>\n}\n\nexport interface StateClusterDetails {\n  loading: boolean\n  error: string\n  data: Nullable<ClusterDetails>\n}\n\nexport interface StateDatabaseAnalysis {\n  loading: boolean\n  error: string\n  data: Nullable<DatabaseAnalysis>\n  selectedViewTab: DatabaseAnalysisViewTab\n  history: {\n    loading: boolean\n    error: string\n    data: ShortDatabaseAnalysis[]\n    selectedAnalysis: Nullable<string>\n    showNoExpiryGroup: boolean\n  }\n}\n\nexport interface StateAnalyticsSettings {\n  viewTab: AnalyticsViewTab\n}\n\nexport enum AnalyticsViewTab {\n  ClusterDetails = 'ClusterDetails',\n  DatabaseAnalysis = 'DatabaseAnalysis',\n  SlowLog = 'SlowLog',\n}\n\nexport enum DatabaseAnalysisViewTab {\n  DataSummary = 'DataSummary',\n  Recommendations = 'Recommendations',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/api.ts",
    "content": "import { CreateCommandExecutionDto as CreateCommandExecutionDtoAPI } from 'apiSrc/modules/workbench/dto/create-command-execution.dto'\nimport { CommandExecution as CommandExecutionAPI } from 'apiSrc/modules/workbench/models/command-execution'\nimport { CommandExecutionResult as CommandExecutionResultAPI } from 'apiSrc/modules/workbench/models/command-execution-result'\n\ninterface CreateCommandExecutionDto extends CreateCommandExecutionDtoAPI {}\ninterface CommandExecution extends CommandExecutionAPI {}\ninterface CommandExecutionResult extends CommandExecutionResultAPI {}\n\nexport { CommandExecution, CommandExecutionResult, CreateCommandExecutionDto }\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/app.ts",
    "content": "import { AxiosError } from 'axios'\nimport { EuiComboBoxOptionOption } from '@elastic/eui'\nimport { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  BrowserColumns,\n  BrowserStorageItem,\n  DurationUnits,\n  FeatureFlags,\n  ICommands,\n  SortOrder,\n} from 'uiSrc/constants'\nimport { ConfigDBStorageItem } from 'uiSrc/constants/storage'\nimport { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto'\nimport { RedisString as RedisStringAPI } from 'apiSrc/common/constants/redis-string'\nimport { RiToastType } from 'uiSrc/components/base/display/toast/RiToast'\nimport { ToastVariant } from '@redis-ui/components'\n\nexport interface CustomError {\n  details?: any[]\n  error: string\n  message: string\n  statusCode: number\n  errorCode?: number\n  resourceId?: string\n}\n\nexport interface ErrorOptions {\n  message: string | JSX.Element\n  code?: string\n  config?: object\n  request?: object\n  response?: object\n}\n\nexport interface EnhancedAxiosError extends AxiosError<CustomError> {}\n\nexport interface IError extends AxiosError {\n  id: string\n  instanceId?: string\n  title?: string\n  additionalInfo?: Record<string, any>\n  /** If true, the error toast won't auto-dismiss and requires manual close */\n  persistent?: boolean\n}\n\nexport interface IMessage {\n  id: string\n  title: string\n  message: string | JSX.Element\n  variant?: ToastVariant\n  group?: string\n  className?: string\n  showCloseButton?: boolean\n  actions?: RiToastType['actions']\n}\n\nexport enum AppWorkspace {\n  Databases = 'databases',\n  RDI = 'redisDataIntegration',\n}\n\nexport interface StateAppInfo {\n  loading: boolean\n  error: string\n  server: Nullable<GetServerInfoResponse>\n  encoding: RedisResponseEncoding\n  electron: {\n    isUpdateAvailable: Nullable<boolean>\n    updateDownloadedVersion: string\n    isReleaseNotesViewed: Nullable<boolean>\n  }\n  isShortcutsFlyoutOpen: boolean\n}\n\nexport interface StateAppConnectivity {\n  loading: boolean\n  error?: string\n}\n\nexport interface StateAppContext {\n  workspace: AppWorkspace\n  contextInstanceId: string\n  contextRdiInstanceId: string\n  lastPage: string\n  dbConfig: {\n    treeViewDelimiter: EuiComboBoxOptionOption[]\n    treeViewSort: SortOrder\n    slowLogDurationUnit: DurationUnits\n    showHiddenRecommendations: boolean\n    shownColumns: BrowserColumns[]\n  }\n  dbIndex: {\n    disabled: boolean\n  }\n  browser: {\n    keyList: {\n      isDataPatternLoaded: boolean\n      isDataRedisearchLoaded: boolean\n      scrollPatternTopPosition: number\n      scrollRedisearchTopPosition: number\n      isNotRendered: boolean\n      selectedKey: Nullable<RedisResponseBuffer>\n    }\n    panelSizes: number[]\n    tree: {\n      openNodes: {\n        [key: string]: boolean\n      }\n      selectedLeaf: Nullable<string>\n    }\n    bulkActions: {\n      opened: boolean\n    }\n    keyDetailsSizes: {\n      [key: string]: Nullable<RelativeWidthSizes>\n    }\n  }\n  workbench: {\n    script: string\n    panelSizes: number[]\n  }\n  searchAndQuery: {\n    script: string\n    panelSizes: {\n      vertical: {\n        [key: string]: number\n      }\n    }\n  }\n  pubsub: {\n    channel: string\n    message: string\n  }\n  analytics: {\n    lastViewedPage: string\n  }\n  capability: {\n    source: string\n  }\n  pipelineManagement: {\n    lastViewedPage: string\n    isOpenDialog: boolean\n  }\n}\n\nexport interface StateAppRedisCommands {\n  loading: boolean\n  error: string\n  spec: ICommands\n  commandsArray: string[]\n  commandGroups: string[]\n}\n\nexport interface DatabaseSettingsData {\n  [ConfigDBStorageItem.notShowConfirmationRunTutorial]?: boolean\n  [BrowserStorageItem.treeViewDelimiter]?: {\n    label: string\n  }[]\n  [BrowserStorageItem.treeViewSort]?: SortOrder\n  [BrowserStorageItem.showHiddenRecommendations]?: boolean\n\n  [key: string]: any\n}\n\nexport interface DatabaseSettings {\n  loading: boolean\n  error: string\n  data: {\n    [instanceId: string]: DatabaseSettingsData\n  }\n}\n\nexport interface IPluginVisualization {\n  id: string\n  uniqId: string\n  name: string\n  plugin: any\n  activationMethod: string\n  matchCommands: string[]\n  default?: boolean\n  iconDark?: string\n  iconLight?: string\n}\n\nexport interface PluginsResponse {\n  static: string\n  plugins: IPlugin[]\n}\nexport interface IPlugin {\n  name: string\n  main: string\n  styles: string | string[]\n  baseUrl: string\n  visualizations: any[]\n  internal?: boolean\n}\n\nexport interface StateAppPlugins {\n  loading: boolean\n  error: string\n  staticPath: string\n  plugins: IPlugin[]\n  visualizations: IPluginVisualization[]\n}\n\nexport interface StateAppSocketConnection {\n  isConnected: boolean\n}\n\nexport interface FeatureFlagComponent {\n  flag: boolean\n  variant?: string\n  data?: any\n}\n\nexport interface StateAppFeatures {\n  highlighting: {\n    version: string\n    features: string[]\n    pages: {\n      [key: string]: string[]\n    }\n  }\n  onboarding: {\n    currentStep: number\n    totalSteps: number\n    isActive: boolean\n  }\n  featureFlags: {\n    loading: boolean\n    features: {\n      [key in FeatureFlags]?: FeatureFlagComponent\n    }\n  }\n}\nexport enum NotificationType {\n  Global = 'global',\n}\n\nexport interface IGlobalNotification {\n  type: string\n  timestamp: number\n  title: string\n  body: string\n  read?: boolean\n  category?: string\n  categoryColor?: string\n}\n\nexport interface InfiniteMessage {\n  id: string\n  variation?: string\n  variant?: ToastVariant\n  className?: string\n  message?: RiToastType['message']\n  description?: RiToastType['description']\n  actions?: RiToastType['actions']\n  customIcon?: RiToastType['customIcon']\n  showCloseButton?: boolean\n  onClose?: () => void\n}\n\nexport interface StateAppNotifications {\n  errors: IError[]\n  messages: IMessage[]\n  infiniteMessages: InfiniteMessage[]\n  notificationCenter: {\n    loading: boolean\n    lastReceivedNotification: Nullable<IGlobalNotification>\n    notifications: IGlobalNotification[]\n    isNotificationOpen: boolean\n    isCenterOpen: boolean\n    totalUnread: number\n    shouldDisplayToast: boolean\n  }\n}\n\nexport enum ActionBarStatus {\n  Progress = 'progress',\n  Success = 'success',\n  Default = 'default',\n  Close = 'close',\n}\n\nexport enum RedisResponseEncoding {\n  UTF8 = 'utf8',\n  ASCII = 'ascii',\n  Buffer = 'buffer',\n}\n\nexport enum RedisResponseBufferType {\n  Buffer = 'Buffer',\n}\n\nexport type RedisResponseBuffer = {\n  type: RedisResponseBufferType\n  data: UintArray\n} & Exclude<RedisStringAPI, string>\n\nexport type RedisString = string | RedisResponseBuffer\n\nexport type UintArray = number[] | Uint8Array\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/azure.ts",
    "content": "export enum AzureLoginSource {\n  Autodiscovery = 'autodiscovery',\n  TokenRefresh = 'token-refresh',\n}\n\nexport enum AzureRedisType {\n  Standard = 'standard',\n  Enterprise = 'enterprise',\n}\n\nexport enum AzureAccessKeysStatus {\n  Enabled = 'Enabled',\n  Disabled = 'Disabled',\n}\n\nexport interface AzureSubscription {\n  subscriptionId: string\n  displayName: string\n  state: string\n}\n\nexport interface AzureRedisDatabase {\n  id: string\n  name: string\n  subscriptionId: string\n  resourceGroup: string\n  location: string\n  type: AzureRedisType\n  host: string\n  port: number\n  sslPort?: number\n  provisioningState: string\n  accessKeysAuthentication?: AzureAccessKeysStatus\n}\n\nexport enum ActionStatus {\n  Success = 'success',\n  Fail = 'fail',\n}\n\nexport interface ImportAzureDatabaseResponse {\n  id: string\n  status: ActionStatus\n  message?: string\n  databaseDetails?: AzureRedisDatabase\n  error?: string | object\n}\n\n// Azure autodiscovery slice interfaces\nexport enum LoadedAzure {\n  Subscriptions = 'subscriptions',\n  Databases = 'databases',\n  DatabasesAdded = 'databasesAdded',\n}\n\nexport interface AzureDatabaseWithStatus extends AzureRedisDatabase {\n  statusAdded?: ActionStatus\n  messageAdded?: string\n}\n\nexport interface InitialStateAzure {\n  loading: boolean\n  error: string\n  subscriptions: AzureSubscription[] | null\n  selectedSubscription: AzureSubscription | null\n  databases: AzureRedisDatabase[] | null\n  databasesAdded: AzureDatabaseWithStatus[]\n  loaded: {\n    [LoadedAzure.Subscriptions]: boolean\n    [LoadedAzure.Databases]: boolean\n    [LoadedAzure.DatabasesAdded]: boolean\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/bulkActions.ts",
    "content": "import { BulkActionsStatus, BulkActionsType, KeyTypes } from 'uiSrc/constants'\nimport { Nullable } from 'uiSrc/utils'\nimport { IBulkActionOverview as IBulkActionOverviewBE } from 'apiSrc/modules/bulk-actions/interfaces/bulk-action-overview.interface'\n\nexport interface IBulkActionOverview\n  extends Omit<IBulkActionOverviewBE, 'status'> {\n  status: BulkActionsStatus\n}\n\nexport interface StateBulkActions {\n  isShowBulkActions: boolean\n  loading: boolean\n  error: string\n  isConnected: boolean\n  selectedBulkAction: SelectedBulkAction\n  bulkDelete: {\n    isActionTriggered: boolean\n    loading: boolean\n    error: string\n    overview: Nullable<IBulkActionOverview>\n    generateReport: boolean\n    filter: Nullable<KeyTypes>\n    search: string\n    keyCount: Nullable<number>\n  }\n  bulkUpload: {\n    loading: boolean\n    error: string\n    overview: Nullable<IBulkActionOverview>\n    fileName?: string\n  }\n}\n\nexport interface SelectedBulkAction {\n  id: string\n  type: Nullable<BulkActionsType>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/cli.ts",
    "content": "export enum CommandExecutionStatus {\n  Success = 'success',\n  Fail = 'fail',\n}\n\nexport enum ClusterNodeRole {\n  All = 'ALL',\n  Master = 'MASTER',\n  Slave = 'SLAVE',\n}\n\nexport interface StateCliSettings {\n  isMinimizedHelper: boolean\n  isShowCli: boolean\n  isShowHelper: boolean\n  cliClientUuid: string\n  loading: boolean\n  errorClient: string\n  matchedCommand: string\n  searchedCommand: string\n  isSearching: boolean\n  isEnteringCommand: boolean\n  searchingCommand: string\n  searchingCommandFilter: string\n  unsupportedCommands: string[]\n  blockingCommands: string[]\n}\n\nexport interface StateCliOutput {\n  data: (string | JSX.Element)[]\n  commandHistory: string[]\n  loading: boolean\n  error: string\n  db: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/cloud.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { Instance } from 'uiSrc/slices/interfaces/instances'\n\nimport { OAuthProvider } from 'uiSrc/components/oauth/oauth-select-plan/constants'\nimport { CloudJobInfo, CloudJobStatus } from 'apiSrc/modules/cloud/job/models'\nimport { CloudUser } from 'apiSrc/modules/cloud/user/models'\nimport { CloudSubscriptionPlanResponse } from 'apiSrc/modules/cloud/subscription/dto'\n\nexport interface CloudJobInfoState extends Omit<CloudJobInfo, 'status'> {\n  status: '' | CloudJobStatus\n}\n\nexport interface StateAppOAuth {\n  loading: boolean\n  error: string\n  message: string\n  source: Nullable<OAuthSocialSource>\n  job: Nullable<CloudJobInfoState>\n  user: {\n    initialLoading: boolean\n    error: string\n    loading: boolean\n    data: Nullable<CloudUser>\n    freeDb: CloudUserFreeDbState\n  }\n  plan: {\n    isOpenDialog: boolean\n    data: CloudSubscriptionPlanResponse[]\n    loading: boolean\n  }\n  isOpenSocialDialog: boolean\n  isOpenSelectAccountDialog: boolean\n  showProgress: boolean\n  capiKeys: {\n    loading: boolean\n    data: Nullable<CloudCapiKey[]>\n  }\n  agreement: boolean\n}\n\nexport interface CloudImportDatabaseResources {\n  subscriptionId: number\n  databaseId?: number\n  region: string\n  provider?: string\n}\n\nexport interface Region {\n  provider: string\n  regions: string[]\n}\n\nexport interface CloudCapiKey {\n  id: string\n  name: string\n  valid: boolean\n  createdAt: string\n  lastUsed?: string\n}\n\nexport interface CloudUserFreeDbState {\n  loading: boolean\n  error: string\n  data: Nullable<Instance>\n}\n\nexport interface CloudSuccessResult {\n  resourceId: string\n  provider?: OAuthProvider\n  region?: string\n}\n\nexport enum OAuthSocialSource {\n  Browser = 'browser',\n  ListOfDatabases = 'list of databases',\n  DatabaseConnectionList = 'database connection list',\n  WelcomeScreen = 'welcome screen',\n  BrowserContentMenu = 'browser content menu',\n  BrowserFiltering = 'browser filtering',\n  BrowserSearch = 'browser search',\n  BrowserRedisJSON = 'browser RedisJSON',\n  RediSearch = 'workbench RediSearch',\n  RedisJSON = 'workbench RedisJSON',\n  RedisTimeSeries = 'workbench RedisTimeSeries',\n  RedisGraph = 'workbench RedisGraph',\n  RedisBloom = 'workbench RedisBloom',\n  Autodiscovery = 'autodiscovery',\n  SettingsPage = 'settings',\n  ConfirmationMessage = 'confirmation message',\n  Workbench = 'workbench',\n  Tutorials = 'tutorials',\n  EmptyDatabasesList = 'empty_db_list',\n  DatabasesList = 'db_list',\n  DiscoveryForm = 'discovery form',\n  UserProfile = 'user profile',\n  AiChat = 'ai chat',\n  NavigationMenu = 'navigation menu',\n  AddDbForm = 'add db form',\n}\n\nexport enum OAuthSocialAction {\n  Create = 'create',\n  Import = 'import',\n  SignIn = 'signIn',\n}\n\nexport enum OAuthStrategy {\n  Google = 'google',\n  GitHub = 'github',\n  SSO = 'sso',\n}\n\nexport enum CloudSsoUtmCampaign {\n  ListOfDatabases = 'list_of_databases',\n  Workbench = 'redisinsight_workbench',\n  WelcomeScreen = 'welcome_screen',\n  BrowserSearch = 'redisinsight_browser_search',\n  BrowserOverview = 'redisinsight_browser_overview',\n  BrowserFilter = 'browser_filter',\n  Tutorial = 'tutorial',\n  AutoDiscovery = 'auto_discovery',\n  Copilot = 'copilot',\n  UserProfile = 'user_account',\n  Settings = 'settings',\n  NavigationMenu = 'navigation_menu',\n  AddDbForm = 'add_db_form',\n  Unknown = 'other',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/content.ts",
    "content": "import { Theme } from 'uiSrc/constants'\n\nexport interface StateContentCreateRedis {\n  data: Record<string, ContentCreateRedis | Record<string, any>>\n  loading: boolean\n  error: string\n}\n\nexport interface ContentFeatureCreateRedis {\n  title: string\n  description?: string\n  links: Record<\n    string,\n    {\n      altText: string\n      url: string\n      event?: string\n    }\n  >\n  styles?: {\n    [Theme.Dark]: Record<string, any>\n    [Theme.Light]: Record<string, any>\n  }\n}\n\nexport interface ContentCreateRedis extends ContentFeatureCreateRedis {\n  features?: {\n    [key: string]: ContentFeatureCreateRedis\n  }\n}\n\nexport interface StateContentGuideLinks {\n  data: ContentGuideLinks[]\n  loading: boolean\n  error: string\n}\n\nexport interface ContentGuideLinks {\n  title: string\n  tutorialId: string\n  icon: string\n  description?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/hash.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces/app'\nimport { ModifiedGetHashMembersResponse } from 'uiSrc/slices/interfaces/instances'\n\nexport interface HashField {\n  field: RedisResponseBuffer\n  value: RedisResponseBuffer\n}\n\nexport interface StateHashData extends ModifiedGetHashMembersResponse {\n  fields: HashField[]\n}\n\nexport interface StateHash {\n  loading: boolean\n  error: string\n  data: StateHashData\n  updateValue: {\n    loading: boolean\n    error: string\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/index.ts",
    "content": "export * from './instances'\nexport * from './hash'\nexport * from './app'\nexport * from './workbench'\nexport * from './redisearch'\nexport * from './monitor'\nexport * from './api'\nexport * from './bulkActions'\nexport * from './searchAndQuery'\nexport * from './cloud'\nexport * from './azure'\nexport * from './rdi'\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/insights.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { IEnablementAreaItem } from 'uiSrc/slices/interfaces/workbench'\n\nexport enum SidePanels {\n  AiAssistant = 'ai',\n  Insights = 'insights',\n}\n\nexport enum InsightsPanelTabs {\n  Explore = 'explore',\n  Recommendations = 'tips',\n}\n\nexport interface SidePanelsState {\n  openedPanel: Nullable<SidePanels>\n  insights: {\n    tabSelected: InsightsPanelTabs\n  }\n  explore: {\n    search: string\n    itemScrollTop: number\n    data: Nullable<string>\n    url: Nullable<string>\n    manifest: Nullable<IEnablementAreaItem[]>\n    isPageOpen: boolean\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/instances.ts",
    "content": "import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces/app'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport { OAuthSocialAction } from 'uiSrc/slices/interfaces/cloud'\nimport { DatabaseListColumn } from 'uiSrc/constants'\nimport { GetHashFieldsResponse } from 'apiSrc/modules/browser/hash/dto'\nimport { GetSetMembersResponse } from 'apiSrc/modules/browser/set/dto'\nimport {\n  GetRejsonRlResponseDto,\n  SafeRejsonRlDataDto,\n} from 'apiSrc/modules/browser/rejson-rl/dto'\nimport {\n  GetListElementsDto,\n  GetListElementsResponse,\n} from 'apiSrc/modules/browser/list/dto'\nimport { Database as DatabaseInstanceResponse } from 'apiSrc/modules/database/models/database'\nimport { SearchZSetMembersResponse } from 'apiSrc/modules/browser/z-set/dto'\nimport { SentinelMaster } from 'apiSrc/modules/redis-sentinel/models/sentinel-master'\nimport { CreateSentinelDatabaseDto } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.dto'\nimport { CreateSentinelDatabaseResponse } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.response'\nimport { RedisNodeInfoResponse } from 'apiSrc/modules/database/dto/redis-info.dto'\nimport { Tag } from './tag'\n\nexport interface Instance extends Partial<DatabaseInstanceResponse> {\n  host: string\n  port: number\n  nameFromProvider?: Nullable<string>\n  provider?: string\n  id: string\n  endpoints?: Nullable<Endpoints[]>\n  connectionType?: ConnectionType\n  lastConnection?: Date\n  password?: Nullable<string>\n  username?: Nullable<string>\n  name?: string\n  db?: number\n  tls?: boolean\n  ssh?: boolean\n  sshOptions?: {\n    host: string\n    port: number\n    username?: string\n    password?: string | true\n    privateKey?: string\n    passphrase?: string | true\n  }\n  tlsClientAuthRequired?: boolean\n  verifyServerCert?: boolean\n  caCert?: CaCertificate\n  clientCert?: ClientCertificate\n  authUsername?: Nullable<string>\n  authPass?: Nullable<string>\n  isDeleting?: boolean\n  sentinelMaster?: SentinelMaster\n  modules: AdditionalRedisModule[]\n  version: Nullable<string>\n  isRediStack?: boolean\n  visible?: boolean\n  loading?: boolean\n  isFreeDb?: boolean\n  tags?: Tag[]\n}\n\nexport interface AdditionalRedisModule {\n  name: string\n  version: number\n  semanticVersion: string\n}\n\ninterface CaCertificate {\n  id?: string\n  name?: string\n  certificate?: string\n}\n\ninterface ClientCertificate {\n  id?: string\n  name?: string\n  key?: string\n  certificate?: string\n}\n\nexport enum ConnectionType {\n  Standalone = 'STANDALONE',\n  Cluster = 'CLUSTER',\n  Sentinel = 'SENTINEL',\n}\n\nexport enum ConnectionProvider {\n  UNKNOWN = 'UNKNOWN',\n  LOCALHOST = 'LOCALHOST',\n  REDIS_SOFTWARE = 'REDIS_SOFTWARE',\n  REDIS_CLOUD = 'REDIS_CLOUD',\n  AZURE = 'AZURE',\n  AWS = 'AWS',\n  GOOGLE = 'GOOGLE',\n}\n\nexport const CONNECTION_TYPE_DISPLAY = Object.freeze({\n  [ConnectionType.Standalone]: 'Standalone',\n  [ConnectionType.Cluster]: 'OSS Cluster',\n  [ConnectionType.Sentinel]: 'Sentinel',\n})\n\nexport interface Endpoints {\n  host: string\n  port: number\n}\n\nexport interface InstanceRedisCluster {\n  host: string\n  port: number\n  uid: number\n  name: string\n  id?: number\n  dnsName: string\n  address: string\n  status: InstanceRedisClusterStatus\n  modules: RedisDefaultModules[]\n  tls: boolean\n  options: any\n  message?: string\n  uidAdded?: number\n  statusAdded?: AddRedisDatabaseStatus\n  messageAdded?: string\n  databaseDetails?: InstanceRedisCluster\n}\n\nexport interface InstanceRedisCloud {\n  accessKey: string\n  secretKey: string\n  credentials: Nullable<ICredentialsRedisCluster>\n  account: Nullable<RedisCloudAccount>\n  host: string\n  port: number\n  uid: number\n  name: string\n  id?: number\n  dnsName: string\n  address: string\n  status: InstanceRedisClusterStatus\n  modules: RedisDefaultModules[]\n  tls: boolean\n  options: any\n  message?: string\n  publicEndpoint?: string\n  databaseId: number\n  databaseIdAdded?: number\n  subscriptionId?: number\n  subscriptionType?: RedisCloudSubscriptionType\n  subscriptionName: string\n  subscriptionIdAdded?: number\n  statusAdded?: AddRedisDatabaseStatus\n  messageAdded?: string\n  databaseDetails?: InstanceRedisCluster\n  free: boolean\n}\n\nexport interface IBulkOperationResult {\n  status: AddRedisDatabaseStatus\n  message: string\n  error?: any\n}\n\nexport enum AddRedisDatabaseStatus {\n  Success = 'success',\n  Fail = 'fail',\n}\n\nexport enum RedisDefaultModules {\n  AI = 'ai',\n  Graph = 'graph',\n  Gears = 'rg',\n  Bloom = 'bf',\n  ReJSON = 'ReJSON',\n  Search = 'search',\n  SearchLight = 'searchlight',\n  TimeSeries = 'timeseries',\n  FT = 'ft',\n  FTL = 'ftl',\n  RedisGears = 'redisgears',\n  RedisGears2 = 'redisgears_2',\n  VectorSet = 'vectorset',\n}\n\nexport enum RedisCustomModulesName {\n  Proto = 'PB',\n  IpTables = 'iptables-input-filter',\n}\n\nexport const REDISEARCH_MODULES: string[] = [\n  RedisDefaultModules.Search,\n  RedisDefaultModules.SearchLight,\n  RedisDefaultModules.FT,\n  RedisDefaultModules.FTL,\n]\n\nexport const COMMAND_MODULES = {\n  [RedisDefaultModules.Search]: REDISEARCH_MODULES,\n  [RedisDefaultModules.ReJSON]: [RedisDefaultModules.ReJSON],\n  [RedisDefaultModules.TimeSeries]: [RedisDefaultModules.TimeSeries],\n  [RedisDefaultModules.Bloom]: [RedisDefaultModules.Bloom],\n}\n\n// Enums don't allow to use dynamic key\nexport const DATABASE_LIST_MODULES_TEXT = Object.freeze({\n  [RedisDefaultModules.AI]: 'AI',\n  [RedisDefaultModules.Graph]: 'Graph',\n  [RedisDefaultModules.Gears]: 'Gears',\n  [RedisDefaultModules.RedisGears]: 'Gears',\n  [RedisDefaultModules.RedisGears2]: 'Gears',\n  [RedisDefaultModules.Bloom]: 'Probabilistic',\n  [RedisDefaultModules.ReJSON]: 'JSON',\n  [RedisDefaultModules.TimeSeries]: 'Time Series',\n  [RedisCustomModulesName.Proto]: 'redis-protobuf',\n  [RedisCustomModulesName.IpTables]: 'RedisPushIpTables',\n  [RedisDefaultModules.Search]: 'Redis Query Engine',\n  [RedisDefaultModules.SearchLight]: 'Redis Query Engine',\n  [RedisDefaultModules.FT]: 'Redis Query Engine',\n  [RedisDefaultModules.FTL]: 'Redis Query Engine',\n  [RedisDefaultModules.VectorSet]: 'Vector Set',\n})\n\nexport enum AddRedisClusterDatabaseOptions {\n  ActiveActive = 'enabledActiveActive',\n  Backup = 'enabledBackup',\n  Clustering = 'enabledClustering',\n  PersistencePolicy = 'persistencePolicy',\n  Flash = 'enabledRedisFlash',\n  Replication = 'enabledReplication',\n  ReplicaDestination = 'isReplicaDestination',\n  ReplicaSource = 'isReplicaSource',\n}\n\n// Enums don't allow to use dynamic key\nexport const DATABASE_LIST_OPTIONS_TEXT = Object.freeze({\n  [AddRedisClusterDatabaseOptions.ActiveActive]: 'Active-Active',\n  [AddRedisClusterDatabaseOptions.Backup]: 'Backup',\n  [AddRedisClusterDatabaseOptions.Clustering]: 'Clustering',\n  [AddRedisClusterDatabaseOptions.PersistencePolicy]: 'Persistence',\n  [AddRedisClusterDatabaseOptions.Flash]: 'Flash',\n  [AddRedisClusterDatabaseOptions.Replication]: 'Replication',\n  [AddRedisClusterDatabaseOptions.ReplicaDestination]: 'Replica Destination',\n  [AddRedisClusterDatabaseOptions.ReplicaSource]: 'Replica Source',\n})\n\nexport enum PersistencePolicy {\n  'none' = 'none',\n  'aof-every-1-second' = 'Append-only file (AOF) every 1 second',\n  'aof-every-write' = 'Append-only file (AOF) every write',\n  'snapshot-every-1-hour' = 'Redis database backup (RDB) every 1 hour',\n  'snapshot-every-6-hours' = 'Redis database backup (RDB) every 6 hours',\n  'snapshot-every-12-hours' = 'Redis database backup (RDB) every 12 hours',\n}\n\nexport enum InstanceRedisClusterStatus {\n  Pending = 'pending',\n  CreationFailed = 'creation-failed',\n  Active = 'active',\n  ActiveChangePending = 'active-change-pending',\n  ImportPending = 'import-pending',\n  DeletePending = 'delete-pending',\n  Recovery = 'recovery',\n}\n\nexport interface TlsSettings {\n  caCertId?: string\n  clientCertPairId?: string\n  verifyServerCert?: boolean\n}\n\nexport interface ClusterNode {\n  host: string\n  port: number\n  role?: 'slave' | 'master'\n  slot?: number\n}\n\nexport enum RedisCloudSubscriptionStatus {\n  Active = 'active',\n  NotActivated = 'not_activated',\n  Deleting = 'deleting',\n  Pending = 'pending',\n  Error = 'error',\n}\n\nexport const RedisCloudSubscriptionStatusText = Object.freeze({\n  [RedisCloudSubscriptionStatus.Active]: 'Active',\n  [RedisCloudSubscriptionStatus.NotActivated]: 'Not Activated',\n  [RedisCloudSubscriptionStatus.Deleting]: 'Deleting',\n  [RedisCloudSubscriptionStatus.Pending]: 'Pending',\n  [RedisCloudSubscriptionStatus.Error]: 'Error',\n})\n\nexport enum RedisCloudSubscriptionType {\n  Flexible = 'flexible',\n  Fixed = 'fixed',\n}\n\nexport const RedisCloudSubscriptionTypeText = Object.freeze({\n  [RedisCloudSubscriptionType.Fixed]: 'Fixed',\n  [RedisCloudSubscriptionType.Flexible]: 'Flexible',\n})\n\nexport interface RedisCloudSubscription {\n  id: number\n  name: string\n  type: RedisCloudSubscriptionType\n  numberOfDatabases: number\n  provider: string\n  region: string\n  status: RedisCloudSubscriptionStatus\n  free: boolean\n}\n\nexport interface DatabaseConfigInfo {\n  version: string\n  totalKeys?: Nullable<number>\n  usedMemory?: Nullable<number>\n  connectedClients?: Nullable<number>\n  opsPerSecond?: Nullable<number>\n  networkInKbps?: Nullable<number>\n  networkOutKbps?: Nullable<number>\n  cpuUsagePercentage?: Nullable<number>\n  maxCpuUsagePercentage?: Nullable<number>\n  serverName?: Nullable<string>\n  cloudDetails?: {\n    cloudId: number\n    subscriptionId: number\n    subscriptionType: 'fixed' | 'flexible'\n    planMemoryLimit: number\n    memoryLimitMeasurementUnit: string\n    isBdbPackages: boolean\n  }\n}\n\nexport interface InitialStateInstances {\n  loading: boolean\n  error: string\n  data: Instance[]\n  loadingChanging: boolean\n  errorChanging: string\n  changedSuccessfully: boolean\n  deletedSuccessfully: boolean\n  connectedInstance: Instance\n  editedInstance: InitialStateEditedInstances\n  instanceOverview: DatabaseConfigInfo\n  instanceInfo: RedisNodeInfoResponse\n  freeInstances: Nullable<Instance[]>\n  importInstances: {\n    loading: boolean\n    error: string\n    data: Nullable<ImportDatabasesData>\n  }\n  shownColumns: DatabaseListColumn[]\n}\n\nexport interface ErrorImportResult {\n  statusCode: number\n  message: string\n  error: string\n}\n\nexport interface ImportDatabasesData {\n  fail: Array<FailedImportStatusResult>\n  partial: Array<FailedImportStatusResult>\n  success: Array<SuccessImportStatusResult>\n  total: number\n}\n\nexport interface FailedImportStatusResult {\n  host?: string\n  port?: number\n  index: number\n  errors: Array<ErrorImportResult>\n  status: string\n}\n\nexport interface SuccessImportStatusResult {\n  host: string\n  port: number\n  index: number\n  status: string\n}\n\nexport interface InitialStateEditedInstances {\n  loading: boolean\n  error: string\n  data: Nullable<Instance>\n}\n\nexport interface InitialStateCluster {\n  loading: boolean\n  data: Nullable<InstanceRedisCluster[]>\n  dataAdded: InstanceRedisCluster[]\n  error: string\n  credentials: Nullable<ICredentialsRedisCluster>\n}\n\nexport interface InitialStateCloud {\n  loading: boolean\n  data: Nullable<InstanceRedisCloud[]>\n  dataAdded: InstanceRedisCloud[]\n  error: string\n  credentials: Nullable<ICredentialsRedisCloud>\n  subscriptions: Nullable<RedisCloudSubscription[]>\n  ssoFlow: Maybe<OAuthSocialAction>\n  isRecommendedSettings: Maybe<boolean>\n  account: {\n    data: Nullable<RedisCloudAccount>\n    error: string\n  }\n  loaded: ILoadedCloud\n}\n\nexport interface InitialStateSentinel {\n  loading: boolean\n  instance: Nullable<Instance>\n  data: ModifiedSentinelMaster[]\n  statuses: CreateSentinelDatabaseResponse[]\n  error: string\n  loaded: ILoadedSentinel\n}\n\nexport enum LoadedCloud {\n  Subscriptions = 'subscriptions',\n  Instances = 'instances',\n  InstancesAdded = 'instancesAdded',\n}\n\nexport enum LoadedSentinel {\n  Masters = 'masters',\n  MastersAdded = 'mastersAdded',\n}\n\nexport interface ILoadedCloud {\n  [LoadedCloud.Subscriptions]?: boolean\n  [LoadedCloud.Instances]?: boolean\n  [LoadedCloud.InstancesAdded]?: boolean\n}\n\nexport interface ILoadedSentinel {\n  [LoadedSentinel.Masters]?: boolean\n  [LoadedSentinel.MastersAdded]?: boolean\n}\n\nexport interface ModifiedGetSetMembersResponse extends GetSetMembersResponse {\n  key?: RedisResponseBuffer\n  match?: string\n}\n\nexport interface ModifiedZsetMembersResponse extends SearchZSetMembersResponse {\n  key?: RedisResponseBuffer\n  match?: string\n}\n\nexport interface ModifiedGetHashMembersResponse extends GetHashFieldsResponse {\n  key?: RedisResponseBuffer\n  match?: string\n}\n\nexport interface ModifiedSentinelMaster extends CreateSentinelDatabaseDto {\n  id?: string\n  alias: string\n  host?: string\n  port?: string\n  username?: string\n  password?: string\n  loading?: boolean\n  message?: string\n  status?: AddRedisDatabaseStatus\n  error?:\n    | string\n    | {\n        statusCode?: number\n        name?: string\n      }\n  numberOfSlaves?: number\n}\n\nexport interface ModifiedGetListElementsResponse\n  extends GetListElementsDto,\n    GetListElementsResponse {\n  elements: { index: number; element: RedisResponseBuffer }[]\n  key?: RedisString\n  searchedIndex: Nullable<number>\n}\n\nexport interface InitialStateSet {\n  loading: boolean\n  error: string\n  data: ModifiedGetSetMembersResponse\n}\n\nexport interface GetRejsonRlResponse extends GetRejsonRlResponseDto {\n  data: Maybe<SafeRejsonRlDataDto[] | string | number | boolean | null>\n}\n\nexport enum EditorType {\n  Default = 'default',\n  Text = 'text',\n}\n\nexport interface InitialStateRejson {\n  loading: boolean\n  error: Nullable<string>\n  data: GetRejsonRlResponse\n  editorType: EditorType\n  isWithinThreshold: boolean\n}\n\nexport interface ICredentialsRedisCluster {\n  host: string\n  port: number\n  username: string\n  password: string\n}\n\nexport interface RedisCloudAccount {\n  accountId: Nullable<number>\n  accountName: Nullable<string>\n  ownerEmail: Nullable<string>\n  ownerName: Nullable<string>\n}\n\nexport interface ICredentialsRedisCloud {\n  accessKey: Nullable<string>\n  secretKey: Nullable<string>\n}\n\nexport enum InstanceType {\n  Standalone = 'Redis Database',\n  RedisCloudPro = 'Redis Cloud',\n  RedisEnterpriseCluster = 'Enterprise Software',\n  AWSElasticache = 'AWS Elasticache',\n  Sentinel = 'Sentinel',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/keys.ts",
    "content": "import {\n  BrowserColumns,\n  KeyTypes,\n  KeyValueCompressor,\n  KeyValueFormat,\n} from 'uiSrc/constants'\nimport { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport { GetKeyInfoResponse } from 'apiSrc/modules/browser/keys/dto'\n\nexport interface Key {\n  name: string\n  type: KeyTypes\n  ttl: number\n  size: number\n  length: number\n}\n\nexport enum KeyViewType {\n  Browser = 'Browser',\n  Tree = 'Tree',\n}\n\nexport enum SearchMode {\n  Pattern = 'Pattern',\n  Redisearch = 'Redisearch',\n}\n\nexport interface KeysStore {\n  loading: boolean\n  deleting: boolean\n  error: string\n  search: string\n  filter: Nullable<KeyTypes>\n  isFiltered: boolean\n  isSearched: boolean\n  isBrowserFullScreen: boolean\n  viewType: KeyViewType\n  searchMode: SearchMode\n  data: KeysStoreData\n  selectedKey: {\n    loading: boolean\n    refreshing: boolean\n    isRefreshDisabled: boolean\n    lastRefreshTime: Nullable<number>\n    error: string\n    data: Nullable<IKeyPropTypes>\n    length: Maybe<number>\n    viewFormat: KeyValueFormat\n    compressor: Nullable<KeyValueCompressor>\n  }\n  addKey: {\n    loading: boolean\n    error: string\n  }\n  searchHistory: {\n    data: null | Array<SearchHistoryItem>\n    loading: boolean\n  }\n  shownColumns: BrowserColumns[]\n}\n\nexport interface SearchHistoryItem {\n  id: string\n  filter: {\n    type: string\n    match: string\n  }\n  mode: string\n}\n\nexport interface KeysStoreData {\n  total: number\n  scanned: number\n  nextCursor: string\n  keys: GetKeyInfoResponse[]\n  shardsMeta: Record<\n    string,\n    {\n      cursor: number\n      scanned: number\n      total: number\n      host?: string\n      port?: number\n    }\n  >\n  previousResultCount: number\n  lastRefreshTime: Nullable<number>\n  maxResults?: Nullable<number>\n}\n\nexport interface NamespaceSearchableResult {\n  prefix: string\n  key?: {\n    name: string\n    type: string\n  }\n  path?: string\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/list.ts",
    "content": "import { ModifiedGetListElementsResponse } from 'uiSrc/slices/interfaces/instances'\n\nexport interface StateList {\n  loading: boolean\n  error: string\n  data: ModifiedGetListElementsResponse\n  updateValue: {\n    loading: boolean\n    error: string\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/monitor.ts",
    "content": "import { Socket } from 'socket.io-client'\nimport { Nullable } from 'uiSrc/utils'\nimport { IMonitorData } from 'apiSrc/modules/profiler/interfaces/monitor-data.interface'\n\nexport interface IMonitorDataPayload extends Partial<IMonitorData> {\n  isError?: boolean\n  message?: string\n}\n\nexport interface StateMonitor {\n  loading: boolean\n  loadingPause: boolean\n  isShowMonitor: boolean\n  isMinimizedMonitor: boolean\n  isRunning: boolean\n  isStarted: boolean\n  isPaused: boolean\n  isResumeLocked: boolean\n  isSaveToFile: boolean\n  socket: Nullable<Socket>\n  items: IMonitorDataPayload[]\n  error: string\n  logFileId: Nullable<string>\n  timestamp: {\n    start: number\n    paused: number\n    unPaused: number\n    duration: number\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/pubsub.ts",
    "content": "import { IMessage } from 'apiSrc/modules/pub-sub/interfaces/message.interface'\nimport { SubscriptionDto } from 'apiSrc/modules/pub-sub/dto/subscription.dto'\n\nexport interface PubSubSubscription {\n  channel: string\n  type: string\n}\n\nexport interface PubSubMessage {\n  channel: string\n  message: string\n  time: number\n}\n\nexport interface StatePubSub {\n  loading: boolean\n  publishing: boolean\n  error: string\n  subscriptions: SubscriptionDto[]\n  isSubscribeTriggered: boolean\n  isConnected: boolean\n  isSubscribed: boolean\n  messages: IMessage[]\n  count: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/rdi.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { Nullable } from 'uiSrc/utils'\nimport { ICommand, RdiListColumn } from 'uiSrc/constants'\nimport { Rdi as RdiInstanceResponse } from 'apiSrc/modules/rdi/models/rdi'\n\n// tabs for dry run job panel\nexport enum PipelineJobsTabs {\n  Transformations = 'transformations',\n  Output = 'output',\n}\n\n// pipeline management page tabs\nexport enum RdiPipelineTabs {\n  Config = 'config',\n  Jobs = 'jobs',\n}\n\nexport interface IRdiPipelineJob {\n  name: string\n  value: string\n}\n\nexport interface IPipeline {\n  config: string\n  jobs: IRdiPipelineJob[]\n}\n\nexport interface IPipelineJSON {\n  config: object\n  jobs: { [key: string]: any }\n}\n\ninterface IDryRunJobOutput {\n  connection: string\n  commands: string[]\n}\n\nexport interface IDryRunJobResults {\n  transformation: object\n  output: IDryRunJobOutput[]\n}\n\nexport interface IRdiPipelineStrategy {\n  strategy: string\n  databases: string[]\n}\n\nexport interface IRdiPipelineStrategies {\n  loading: boolean\n  error: string\n  data: IRdiPipelineStrategy[]\n}\n\nexport enum StatisticsConnectionStatus {\n  NotYetUsed = 'not yet used',\n  Connected = 'connected',\n}\n\nexport enum RdiPipelineStatus {\n  Success = 'success',\n  Failed = 'failed',\n}\n\nexport enum RdiStatisticsViewType {\n  Table = 'table',\n  Blocks = 'blocks',\n  Info = 'info',\n}\n\nexport enum StatisticsCellType {\n  Status = 'status',\n  Date = 'date',\n}\n\nexport interface IStatisticsColumn {\n  id: string\n  header: string\n  type?: StatisticsCellType\n}\n\nexport interface IStatisticsTableSection {\n  name: string\n  view: RdiStatisticsViewType.Table\n  columns: IStatisticsColumn[]\n  data: Record<string, unknown>[]\n  footer?: Record<string, unknown>\n}\n\nexport interface IStatisticsBlockItem {\n  label: string\n  value: number\n  units: string\n}\n\nexport interface IStatisticsBlocksSection {\n  name: string\n  view: RdiStatisticsViewType.Blocks\n  data: IStatisticsBlockItem[]\n}\n\nexport interface IStatisticsInfoItem {\n  label: string\n  value: string\n}\n\nexport interface IStatisticsInfoSection {\n  name: string\n  view: RdiStatisticsViewType.Info\n  data: IStatisticsInfoItem[]\n}\n\nexport type IStatisticsSection =\n  | IStatisticsTableSection\n  | IStatisticsBlocksSection\n  | IStatisticsInfoSection\n\nexport interface IRdiStatisticsData {\n  sections: IStatisticsSection[]\n}\n\nexport interface IRdiStatistics {\n  status: RdiPipelineStatus\n  data?: IRdiStatisticsData\n  error?: string\n}\n\nexport enum FileChangeType {\n  Added = 'added',\n  Modified = 'modified',\n  Removed = 'removed',\n}\n\nexport enum PipelineStatus {\n  // v1 statuses\n  Ready = 'ready',\n  NotReady = 'not-ready',\n  // v1/v2 intersection\n  Stopping = 'stopping',\n  // v2 statuses\n  Started = 'started',\n  Stopped = 'stopped',\n  Error = 'error',\n  Creating = 'creating',\n  Updating = 'updating',\n  Deleting = 'deleting',\n  Starting = 'starting',\n  Resetting = 'resetting',\n  Pending = 'pending',\n  Unknown = 'unknown',\n}\n\nexport enum PipelineState {\n  InitialSync = 'initial-sync',\n  CDC = 'cdc',\n  NotRunning = 'not-running',\n}\n\nexport interface IComponentStatus {\n  name: string\n  type: string\n  status: string\n  version: string\n  errors: string[]\n  metric_collections?: string[]\n}\n\n// Flexible interface - supports both V1 and V2 formats\nexport interface IPipelineStatus {\n  status: PipelineStatus\n  state?: PipelineState\n  errors?: string[]\n  components?: IComponentStatus[]\n}\n\nexport enum PipelineAction {\n  Start = 'start',\n  Stop = 'stop',\n  Reset = 'reset',\n}\n\nexport interface IStateRdiPipeline {\n  loading: boolean\n  error: string\n  data: Nullable<IPipeline>\n  config: string\n  jobs: IRdiPipelineJob[]\n  isPipelineValid: boolean\n  configValidationErrors: string[]\n  jobsValidationErrors: Record<string, string[]>\n  resetChecked: boolean\n  schema: Nullable<object>\n  jobNameSchema: Nullable<object>\n  monacoJobsSchema: Nullable<object>\n  strategies: IRdiPipelineStrategies\n  changes: Record<string, FileChangeType>\n  jobFunctions: monacoEditor.languages.CompletionItem[]\n  status: {\n    loading: boolean\n    error: string\n    data: Nullable<IPipelineStatus>\n  }\n  pipelineAction: {\n    loading: boolean\n    error: string\n    action: Nullable<PipelineAction>\n  }\n}\n\nexport interface IStateRdiDryRunJob {\n  loading: boolean\n  error: string\n  results: Nullable<IDryRunJobResults>\n}\n\nexport interface IStateRdiStatistics {\n  loading: boolean\n  error: string\n  results: Nullable<IRdiStatistics>\n}\n\nexport interface RdiInstance extends RdiInstanceResponse {\n  visible?: boolean\n  loading: boolean\n  error: string\n  // not really present, but used in InstancesList.tsx:142\n  db?: number\n}\n\nexport interface IErrorData {\n  message: string\n  statusCode: number\n  error: string\n  errorCode?: number\n  errors?: string[]\n}\n\nexport interface InitialStateRdiInstances {\n  loading: boolean\n  error: string\n  data: RdiInstance[]\n  connectedInstance: RdiInstance\n  loadingChanging: boolean\n  errorChanging: string\n  changedSuccessfully: boolean\n  shownColumns: RdiListColumn[]\n}\n\n// Rdi test target connections\nexport enum TestConnectionStatus {\n  Fail = 'failed',\n  Success = 'success',\n}\n\ninterface IErrorDetail {\n  code: string\n  message: string\n}\n\ninterface ITargetDetail {\n  status: TestConnectionStatus\n  error?: IErrorDetail\n}\n\ninterface ISourcesDetail {\n  connected: boolean\n  error?: string\n}\n\nexport interface IConnectionResult {\n  targets: ITargets\n  sources: ISourcesDetail\n}\n\nexport interface ITargets {\n  [key: string]: ITargetDetail\n}\n\nexport interface TestConnectionsResponse {\n  targets: ITargets\n  sources: ISourcesDetail\n}\n\nexport interface IRdiConnectionResult {\n  target: string\n  error?: string\n}\n\nexport interface TransformGroupResult {\n  success: IRdiConnectionResult[]\n  fail: IRdiConnectionResult[]\n}\n\nexport interface TransformResult {\n  target: TransformGroupResult\n  source: TransformGroupResult\n}\n\nexport interface IStateRdiTestConnections {\n  loading: boolean\n  error: string\n  results: Nullable<TransformResult>\n}\n\nexport type TJMESPathFunctions = {\n  [key: string]: Pick<ICommand, 'summary'> &\n    Required<Pick<ICommand, 'arguments'>>\n}\n\nexport interface IYamlFormatError {\n  filename: string\n  msg: string\n}\n\nexport interface IActionPipelineResultProps {\n  success: boolean\n  error: Nullable<string>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/recommendations.ts",
    "content": "import { Vote } from 'uiSrc/constants/recommendations'\nimport { Nullable } from 'uiSrc/utils'\n\nexport interface IRecommendation {\n  id: string\n  name: string\n  read: boolean\n  hide: boolean\n  tutorial: string\n  vote: Nullable<Vote>\n  params: IRecommendationParams\n}\n\nexport interface IRecommendations {\n  recommendations: IRecommendation[]\n  totalUnread: number\n}\n\nexport interface StateRecommendations {\n  data: IRecommendations\n  loading: boolean\n  error: string\n  isHighlighted: boolean\n  content: IRecommendationsStatic\n}\n\nexport interface IRecommendationContent {\n  type?: string\n  value?: any\n  parameter?: any\n}\n\nexport interface IRecommendationsStatic {\n  [key: string]: {\n    id: string\n    title: string\n    liveTitle?: string\n    telemetryEvent?: string\n    redisStack?: boolean\n    tutorialId?: string\n    content?: IRecommendationContent[]\n    badges?: string[]\n    deprecated?: boolean\n  }\n}\n\nexport interface IRecommendationParams {\n  keys: string[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/redisearch.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from './app'\nimport { KeysStoreData, SearchHistoryItem } from './keys'\n\nexport interface IndexSummary {\n  name: string\n  prefixes: string[]\n  keyType: string\n}\n\nexport interface KeyIndexesResponseItem {\n  name: string\n  prefixes: string[]\n  key_type: string\n}\n\nexport interface KeyIndexesApiResponse {\n  indexes: KeyIndexesResponseItem[]\n}\n\nexport interface KeyIndexesEntry {\n  loading: boolean\n  data: IndexSummary[]\n  error: string\n}\n\nexport interface StateRedisearch {\n  loading: boolean\n  error: string\n  search: string\n  isSearched: boolean\n  data: KeysStoreData\n  selectedIndex: Nullable<RedisResponseBuffer>\n  list: {\n    loading: boolean | undefined\n    error: string\n    data: RedisResponseBuffer[]\n  }\n  createIndex: {\n    loading: boolean\n    error: string\n  }\n  searchHistory: {\n    data: null | Array<SearchHistoryItem>\n    loading: boolean\n  }\n  keyIndexes: Record<string, KeyIndexesEntry>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/searchAndQuery.ts",
    "content": "import {\n  CommandExecutionUI,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces/workbench'\n\nexport interface StateSearchAndQuery {\n  isLoaded: boolean\n  loading: boolean\n  processing: boolean\n  clearing: boolean\n  error: string\n  items: CommandExecutionUI[]\n  activeRunQueryMode: RunQueryMode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/stream.ts",
    "content": "import { SortOrder } from 'uiSrc/constants'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  ConsumerDto,\n  ConsumerGroupDto,\n  GetStreamEntriesResponse,\n  PendingEntryDto,\n} from 'apiSrc/modules/browser/stream/dto'\nimport { RedisResponseBuffer } from './app'\n\ntype Range = {\n  start: string\n  end: string\n}\n\nexport enum StreamViewType {\n  Data = 'Data',\n  Groups = 'Groups',\n  Consumers = 'Consumers',\n  Messages = 'Messages',\n}\n\nexport interface StateStream {\n  loading: boolean\n  error: string\n  sortOrder: SortOrder\n  range: Range\n  data: StateStreamData\n  viewType: StreamViewType\n  groups: StateConsumerGroups\n}\n\nexport interface StateStreamData extends GetStreamEntriesResponse {\n  lastRefreshTime: Nullable<number>\n  keyNameString: string\n}\n\nexport interface StateConsumerGroups {\n  loading: boolean\n  error: string\n  data: ConsumerGroupDto[]\n  selectedGroup: Nullable<StateSelectedGroup>\n  lastRefreshTime: Nullable<number>\n}\n\nexport interface StateSelectedGroup {\n  name: RedisResponseBuffer\n  nameString: string\n  data: ConsumerDto[]\n  selectedConsumer: Nullable<StateSelectedConsumer>\n  lastRefreshTime: Nullable<number>\n}\n\nexport interface StateSelectedConsumer {\n  name: RedisResponseBuffer\n  nameString: string\n  pending: number\n  idle: number\n  data: PendingEntryDto[]\n  lastRefreshTime: Nullable<number>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/string.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { RedisResponseBuffer } from './app'\n\nexport interface StringState {\n  loading: boolean\n  error: string\n  isCompressed: boolean\n  data: {\n    key: string\n    value: Nullable<RedisResponseBuffer>\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/tag.ts",
    "content": "export interface Tag {\n  id: string\n  key: string\n  value: string\n  createdAt: string\n  updatedAt: string\n}\n\nexport interface InitialTagsState {\n  data: Tag[]\n  selectedTags: Set<string>\n  loading: boolean\n  error: string | null\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/urlHandling.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\n\nexport enum UrlHandlingActions {\n  Connect = 'databases/connect',\n  Open = 'open',\n}\nexport interface StateUrlHandling {\n  fromUrl: Nullable<string>\n  returnUrl: Nullable<string>\n  action: Nullable<UrlHandlingActions>\n  dbConnection: Nullable<any>\n  properties: Record<string, any>\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/user.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\nimport { CloudUser } from 'apiSrc/modules/cloud/user/models'\nimport {\n  GetAgreementsSpecResponse,\n  GetAppSettingsResponse,\n} from 'apiSrc/modules/settings/dto/settings.dto'\n\nexport interface StateUserSettings {\n  loading: boolean\n  error: string\n  isShowConceptsPopup: Nullable<boolean>\n  config: Nullable<GetAppSettingsResponse>\n  spec: Nullable<GetAgreementsSpecResponse>\n  workbench: {\n    cleanup: boolean\n  }\n}\n\nexport interface StateUserProfile {\n  loading: boolean\n  error: string\n  data?: CloudUser\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/workbench.ts",
    "content": "import { CommandExecution } from './api'\n\nexport interface StateWorkbenchSettings {\n  wbClientUuid: string\n  loading: boolean\n  error: string\n  errorClient: string\n  unsupportedCommands: string[]\n}\n\nexport interface StateWorkbenchResults {\n  isLoaded: boolean\n  loading: boolean\n  processing: boolean\n  clearing: boolean\n  error: string\n  items: CommandExecutionUI[]\n  resultsMode: ResultsMode\n  activeRunQueryMode: RunQueryMode\n}\n\nexport enum EnablementAreaComponent {\n  CodeButton = 'code-button',\n  Group = 'group',\n  InternalLink = 'internal-link',\n}\n\nexport interface IEnablementAreaItem {\n  id: string\n  type: EnablementAreaComponent\n  label: string\n  summary?: string\n  children?: IEnablementAreaItem[]\n  args?: Record<string, any>\n  _actions?: string[]\n  _path?: string\n  _key?: string\n  _groupPath?: string\n}\n\nexport interface StateWorkbenchEnablementArea {\n  loading: boolean\n  deleting?: boolean\n  error: string\n  items: IEnablementAreaItem[]\n}\nexport interface StateWorkbenchCustomTutorials\n  extends StateWorkbenchEnablementArea {\n  bulkUpload: {\n    pathsInProgress: string[]\n  }\n}\n\nexport interface CommandExecutionUI extends Partial<CommandExecution> {\n  id?: string\n  loading?: boolean\n  isOpen?: boolean\n  error?: string\n  emptyCommand?: boolean\n}\n\nexport enum RunQueryMode {\n  Raw = 'RAW',\n  ASCII = 'ASCII',\n}\n\nexport enum ResultsMode {\n  Silent = 'SILENT',\n  Default = 'DEFAULT',\n  GroupMode = 'GROUP_MODE',\n}\n\nexport enum CommandExecutionType {\n  Workbench = 'WORKBENCH',\n  Search = 'SEARCH',\n}\n\nexport interface ResultsSummary {\n  total: number\n  success: number\n  fail: number\n}\n\nexport interface ExecuteQueryParams {\n  batchSize: number\n  activeRunQueryMode: RunQueryMode\n  resultsMode: ResultsMode\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/interfaces/zset.ts",
    "content": "import { RedisResponseBuffer } from 'uiSrc/slices/interfaces/app'\nimport { ModifiedZsetMembersResponse } from 'uiSrc/slices/interfaces/instances'\n\nexport interface ZsetMember {\n  name: RedisResponseBuffer\n  score: number\n}\n\nexport interface StateZsetData extends ModifiedZsetMembersResponse {\n  sortOrder?: string\n  members: ZsetMember[]\n}\n\nexport interface StateZset {\n  loading: boolean\n  searching: boolean\n  error: string\n  data: StateZsetData\n  updateScore: {\n    loading: boolean\n    error: string\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/oauth/azure.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { AppDispatch, RootState } from 'uiSrc/slices/store'\nimport {\n  addErrorNotification,\n  IAddInstanceErrorPayload,\n} from 'uiSrc/slices/app/notifications'\nimport { resetDataAzure } from 'uiSrc/slices/instances/azure'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'\n\nconst OAUTH_TIMEOUT_MS = 60 * 1000\nlet oauthTimeoutId: ReturnType<typeof setTimeout> | null = null\n\nexport interface AzureAccount {\n  id: string\n  username: string\n  name?: string\n}\n\nexport interface AzureAuthLoginResponse {\n  url: string\n}\n\nexport interface StateAzureAuth {\n  loading: boolean\n  account: AzureAccount | null\n  error: string\n  source: AzureLoginSource | null\n}\n\nexport enum AzureOAuthPrompt {\n  /**\n   * Force the account picker to appear, allowing the user to select a different account.\n   */\n  SelectAccount = 'select_account',\n\n  /**\n   * Force re-authentication, even if the user has a valid session.\n   */\n  Login = 'login',\n\n  /**\n   * Force the consent dialog to appear, even if consent was previously granted.\n   */\n  Consent = 'consent',\n}\n\nexport enum AzureOAuthRedirectType {\n  /**\n   * Uses custom protocol (redisinsight://) for Electron app deep linking.\n   */\n  Deeplink = 'deeplink',\n\n  /**\n   * Uses HTTP localhost callback for web/Docker deployments with localStorage polling.\n   */\n  Web = 'web',\n}\n\nexport const initialState: StateAzureAuth = {\n  loading: false,\n  account: null,\n  error: '',\n  source: null,\n}\n\nconst clearOAuthTimeout = () => {\n  if (oauthTimeoutId) {\n    clearTimeout(oauthTimeoutId)\n    oauthTimeoutId = null\n  }\n}\n\nconst azureAuthSlice = createSlice({\n  name: 'azureAuth',\n  initialState,\n  reducers: {\n    setAzureAuthInitialState: () => initialState,\n    setAzureLoginSource: (\n      state,\n      { payload }: PayloadAction<AzureLoginSource | null>,\n    ) => {\n      state.source = payload\n    },\n    azureAuthLogin: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    azureAuthLoginSuccess: (state) => {\n      // Keep loading true - waiting for the OAuth callback\n      // Loading will be set to false by azureOAuthCallbackSuccess or azureOAuthCallbackFailure\n      state.error = ''\n    },\n    azureAuthLoginFailure: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n      state.source = null\n    },\n    azureOAuthCallbackSuccess: (\n      state,\n      { payload }: PayloadAction<AzureAccount>,\n    ) => {\n      state.loading = false\n      state.account = payload\n      state.error = ''\n    },\n    azureOAuthCallbackFailure: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n      state.source = null\n    },\n    azureAuthLogout: (state) => {\n      state.account = null\n      state.error = ''\n      state.source = null\n    },\n  },\n})\n\nexport const {\n  setAzureAuthInitialState,\n  setAzureLoginSource,\n  azureAuthLogin,\n  azureAuthLoginSuccess,\n  azureAuthLoginFailure,\n  azureOAuthCallbackSuccess,\n  azureOAuthCallbackFailure,\n  azureAuthLogout,\n} = azureAuthSlice.actions\n\n// Selectors\nexport const azureAuthSelector = (state: RootState) => state.oauth.azure\nexport const azureAuthAccountSelector = (state: RootState) =>\n  state.oauth.azure?.account\nexport const azureAuthLoadingSelector = (state: RootState) =>\n  state.oauth.azure?.loading\nexport const azureAuthSourceSelector = (state: RootState) =>\n  state.oauth.azure?.source\n\n// The reducer\nexport default azureAuthSlice.reducer\n\nexport interface InitiateAzureLoginOptions {\n  source: AzureLoginSource\n  prompt?: AzureOAuthPrompt\n  redirectType?: AzureOAuthRedirectType\n  onSuccess?: (url: string) => void\n  onFail?: () => void\n}\n\n// Thunk action to initiate Azure login\nexport function initiateAzureLoginAction(options: InitiateAzureLoginOptions) {\n  const { source, prompt, redirectType, onSuccess, onFail } = options\n\n  return async (dispatch: AppDispatch) => {\n    dispatch(setAzureLoginSource(source))\n    sendEventTelemetry({\n      event: TelemetryEvent.AZURE_SIGN_IN_CLICKED,\n    })\n    dispatch(azureAuthLogin())\n\n    try {\n      const params: Record<string, string> = {}\n      if (prompt) params.prompt = prompt\n      if (redirectType) params.redirectType = redirectType\n\n      const { data, status } = await apiService.get<AzureAuthLoginResponse>(\n        ApiEndpoints.AZURE_AUTH_LOGIN,\n        { params: Object.keys(params).length > 0 ? params : undefined },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(azureAuthLoginSuccess())\n        onSuccess?.(data.url)\n\n        // Start timeout to reset loading state if OAuth flow is abandoned\n        clearOAuthTimeout()\n        oauthTimeoutId = setTimeout(() => {\n          dispatch(setAzureAuthInitialState())\n        }, OAUTH_TIMEOUT_MS)\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(azureAuthLoginFailure(errorMessage))\n      dispatch(addErrorNotification(error as IAddInstanceErrorPayload))\n      onFail?.()\n    }\n  }\n}\n\nexport function handleAzureOAuthSuccess(account: AzureAccount) {\n  return (dispatch: AppDispatch) => {\n    clearOAuthTimeout()\n    // Clear stale subscriptions/databases data from previous account\n    dispatch(resetDataAzure())\n    dispatch(azureOAuthCallbackSuccess(account))\n  }\n}\n\nexport function handleAzureOAuthFailure(errorMessage: string) {\n  return (dispatch: AppDispatch) => {\n    clearOAuthTimeout()\n    dispatch(azureOAuthCallbackFailure(errorMessage))\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/oauth/cloud.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { remove } from 'lodash'\nimport { apiService, localStorageService } from 'uiSrc/services'\nimport { ApiEndpoints, BrowserStorageItem, Pages } from 'uiSrc/constants'\nimport {\n  getApiErrorCode,\n  getApiErrorMessage,\n  getAxiosError,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\n\nimport { CloudJobName, CloudJobStatus } from 'uiSrc/electron/constants'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { getCloudSsoUtmParams } from 'uiSrc/utils/oauth/cloudSsoUtm'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport { CloudUser } from 'apiSrc/modules/cloud/user/models'\nimport { CloudJobInfo } from 'apiSrc/modules/cloud/job/models'\nimport { CloudSubscriptionPlanResponse } from 'apiSrc/modules/cloud/subscription/dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport {\n  CloudCapiKey,\n  CloudJobInfoState,\n  CloudSuccessResult,\n  EnhancedAxiosError,\n  Instance,\n  OAuthSocialAction,\n  OAuthSocialSource,\n  StateAppOAuth,\n} from '../interfaces'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  addMessageNotification,\n  removeInfiniteNotification,\n} from '../app/notifications'\nimport {\n  checkConnectToInstanceAction,\n  setConnectedInstanceId,\n} from '../instances/instances'\nimport ApiStatusCode from '../../constants/apiStatusCode'\n\nexport const initialState: StateAppOAuth = {\n  loading: false,\n  error: '',\n  message: '',\n  job: {\n    id: localStorageService.get(BrowserStorageItem.OAuthJobId) ?? '',\n    name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n    status: '',\n  },\n  source: null,\n  agreement:\n    localStorageService.get(BrowserStorageItem.OAuthAgreement) ?? false,\n  isOpenSocialDialog: false,\n  isOpenSelectAccountDialog: false,\n  showProgress: true,\n  user: {\n    initialLoading: true,\n    loading: false,\n    error: '',\n    data: null,\n    freeDb: {\n      loading: false,\n      error: '',\n      data: null,\n    },\n  },\n  plan: {\n    loading: false,\n    isOpenDialog: false,\n    data: [],\n  },\n  capiKeys: {\n    loading: false,\n    data: null,\n  },\n}\n\n// A slice for recipes\nconst oauthCloudSlice = createSlice({\n  name: 'oauthCloud',\n  initialState,\n  reducers: {\n    setOAuthInitialState: () => initialState,\n\n    signIn: (state) => {\n      state.loading = true\n    },\n    signInSuccess: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = ''\n      state.message = payload\n    },\n    signInFailure: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n    },\n    getUserInfo: (state) => {\n      state.user.loading = true\n    },\n    getUserInfoSuccess: (state, { payload }: PayloadAction<CloudUser>) => {\n      state.user.loading = false\n      state.user.data = payload\n      state.user.error = ''\n    },\n    getUserInfoFailure: (state, { payload }: PayloadAction<string>) => {\n      state.user.loading = false\n      state.user.error = payload\n    },\n    addFreeDb: (state) => {\n      state.user.freeDb.loading = true\n    },\n    addFreeDbSuccess: (state, { payload }: PayloadAction<Instance>) => {\n      state.user.freeDb.loading = false\n      state.user.freeDb.data = payload\n    },\n    addFreeDbFailure: (state, { payload }: PayloadAction<string>) => {\n      state.user.freeDb.loading = false\n      state.user.freeDb.error = payload\n    },\n    setSocialDialogState: (\n      state,\n      { payload }: PayloadAction<Nullable<OAuthSocialSource>>,\n    ) => {\n      if (payload) {\n        state.source = payload\n      }\n      state.isOpenSocialDialog = !!payload\n    },\n    setOAuthCloudSource: (\n      state,\n      { payload }: PayloadAction<Nullable<OAuthSocialSource>>,\n    ) => {\n      state.source = payload\n    },\n    setSelectAccountDialogState: (\n      state,\n      { payload }: PayloadAction<boolean>,\n    ) => {\n      state.isOpenSelectAccountDialog = payload\n    },\n    setJob: (state, { payload }: PayloadAction<CloudJobInfoState>) => {\n      state.job = payload\n    },\n\n    // Select Plan\n    setIsOpenSelectPlanDialog: (state, { payload }: PayloadAction<boolean>) => {\n      state.plan.isOpenDialog = payload\n    },\n    getPlans: (state) => {\n      state.plan.loading = true\n    },\n    getPlansSuccess: (state, { payload }: PayloadAction<any[]>) => {\n      state.plan.loading = false\n      state.plan.data = payload\n    },\n    getPlansFailure: (state) => {\n      state.plan.loading = false\n    },\n    showOAuthProgress: (state, { payload }: PayloadAction<boolean>) => {\n      state.showProgress = payload\n    },\n    setAgreement: (state, { payload }: PayloadAction<boolean>) => {\n      state.agreement = payload\n    },\n    getCapiKeys: (state) => {\n      state.capiKeys.loading = true\n    },\n    getCapiKeysSuccess: (state, { payload }: PayloadAction<CloudCapiKey[]>) => {\n      state.capiKeys.loading = false\n      state.capiKeys.data = payload\n    },\n    getCapiKeysFailure: (state) => {\n      state.capiKeys.loading = false\n    },\n    removeCapiKey: (state) => {\n      state.capiKeys.loading = true\n    },\n    removeCapiKeySuccess: (state, { payload }: PayloadAction<string>) => {\n      state.capiKeys.loading = false\n      if (state.capiKeys.data) {\n        remove(state.capiKeys.data, (item) => item.id === payload)\n      }\n    },\n    removeCapiKeyFailure: (state) => {\n      state.capiKeys.loading = false\n    },\n    removeAllCapiKeys: (state) => {\n      state.capiKeys.loading = true\n    },\n    removeAllCapiKeysSuccess: (state) => {\n      state.capiKeys.loading = false\n      state.capiKeys.data = []\n    },\n    removeAllCapiKeysFailure: (state) => {\n      state.capiKeys.loading = false\n    },\n    logoutUser: (state) => {\n      state.user.loading = true\n    },\n    logoutUserSuccess: (state) => {\n      state.user.loading = false\n      state.user.data = null\n    },\n    logoutUserFailure: (state) => {\n      state.user.loading = false\n      state.user.data = null\n    },\n    setInitialLoadingState: (state, { payload }: PayloadAction<boolean>) => {\n      state.user.initialLoading = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setOAuthInitialState,\n  signIn,\n  signInSuccess,\n  signInFailure,\n  getUserInfo,\n  getUserInfoSuccess,\n  getUserInfoFailure,\n  addFreeDb,\n  addFreeDbSuccess,\n  addFreeDbFailure,\n  setSocialDialogState,\n  setOAuthCloudSource,\n  setSelectAccountDialogState,\n  setJob,\n  setIsOpenSelectPlanDialog,\n  getPlans,\n  getPlansSuccess,\n  getPlansFailure,\n  showOAuthProgress,\n  setAgreement,\n  getCapiKeys,\n  getCapiKeysSuccess,\n  getCapiKeysFailure,\n  removeCapiKey,\n  removeCapiKeySuccess,\n  removeCapiKeyFailure,\n  removeAllCapiKeys,\n  removeAllCapiKeysSuccess,\n  removeAllCapiKeysFailure,\n  logoutUser,\n  logoutUserSuccess,\n  logoutUserFailure,\n  setInitialLoadingState,\n} = oauthCloudSlice.actions\n\n// A selector\nexport const oauthCloudSelector = (state: RootState) => state.oauth.cloud\nexport const oauthCloudJobSelector = (state: RootState) => state.oauth.cloud.job\nexport const oauthCloudUserSelector = (state: RootState) =>\n  state.oauth.cloud.user\nexport const oauthCloudUserDataSelector = (state: RootState) =>\n  state.oauth.cloud.user.data\nexport const oauthCloudPlanSelector = (state: RootState) =>\n  state.oauth.cloud.plan\nexport const oauthCloudPAgreementSelector = (state: RootState) =>\n  state.oauth.cloud.agreement\nexport const oauthCapiKeysSelector = (state: RootState) =>\n  state.oauth.cloud.capiKeys\n\n// The reducer\nexport default oauthCloudSlice.reducer\n\nexport function createFreeDbSuccess(\n  result: CloudSuccessResult,\n  history: any,\n  jobName: Maybe<CloudJobName>,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const { resourceId: id, ...details } = result\n    try {\n      const onConnect = () => {\n        const state = stateInit()\n        const isConnected = state.app?.context?.contextInstanceId === id\n\n        dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthSuccess))\n\n        if (!isConnected) {\n          dispatch(setConnectedInstanceId(id ?? ''))\n          dispatch(checkConnectToInstanceAction(id))\n        }\n\n        history.push(Pages.browser(id))\n      }\n\n      dispatch(showOAuthProgress(true))\n      dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress))\n      dispatch(\n        addInfiniteNotification(\n          INFINITE_MESSAGES.SUCCESS_CREATE_DB(details, onConnect, jobName),\n        ),\n      )\n      dispatch(setSelectAccountDialogState(false))\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setOAuthCloudSource(null))\n      dispatch(addErrorNotification(error))\n      dispatch(addFreeDbFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchProfile(\n  onSuccessAction?: (isMultiAccount?: boolean) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getUserInfo())\n\n    try {\n      const { data, status } = await apiService.get<CloudUser>(\n        ApiEndpoints.CLOUD_ME,\n        {\n          // params: getCloudSsoUtmParams(getState().oauth?.cloud?.source),\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUserInfoSuccess(data))\n\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(getUserInfoFailure(errorMessage))\n\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchUserInfo(\n  onSuccessAction?: (isSelectAccout: boolean) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, getState: () => RootState) => {\n    dispatch(getUserInfo())\n\n    try {\n      const { data, status } = await apiService.get<CloudUser>(\n        ApiEndpoints.CLOUD_ME,\n        {\n          params: getCloudSsoUtmParams(getState().oauth?.cloud?.source),\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        const isSignInFlow =\n          getState().connections?.cloud.ssoFlow === OAuthSocialAction.SignIn\n        const isSelectAccout =\n          !isSignInFlow && (data?.accounts?.length ?? 0) > 1\n\n        if (isSelectAccout) {\n          dispatch(setSelectAccountDialogState(true))\n          dispatch(\n            removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n          )\n        }\n\n        dispatch(getUserInfoSuccess(data))\n        dispatch(setSocialDialogState(null))\n\n        onSuccessAction?.(isSelectAccout)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getUserInfoFailure(errorMessage))\n      dispatch(setOAuthCloudSource(null))\n\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function createFreeDbJob({\n  name,\n  resources = {},\n  onSuccessAction,\n  onFailAction,\n}: {\n  name: CloudJobName\n  resources?: {\n    planId?: number\n    databaseId?: number\n    subscriptionId?: number\n    region?: string\n    provider?: string\n    isRecommendedSettings?: boolean\n  }\n  onSuccessAction?: () => void\n  onFailAction?: () => void\n}) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(addFreeDb())\n\n    try {\n      const { data, status } = await apiService.post<CloudJobInfo>(\n        ApiEndpoints.CLOUD_ME_JOBS,\n        {\n          name,\n          runMode: 'async',\n          data: resources,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        localStorageService.set(BrowserStorageItem.OAuthJobId, data.id)\n        dispatch(setJob({ id: data.id, name, status: CloudJobStatus.Running }))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const err = getAxiosError(error as EnhancedAxiosError)\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n\n      dispatch(addErrorNotification(err))\n      dispatch(addFreeDbFailure(errorMessage))\n      dispatch(setOAuthCloudSource(null))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function activateAccount(\n  id: string,\n  onSuccessAction?: () => void,\n  onFailAction?: (error: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getUserInfo())\n\n    try {\n      const { data, status } = await apiService.put<CloudUser>(\n        [ApiEndpoints.CLOUD_ME_ACCOUNTS, id, ApiEndpoints.CLOUD_CURRENT].join(\n          '/',\n        ),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUserInfoSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const err = getAxiosError(error as EnhancedAxiosError)\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      const errorCode = getApiErrorCode(error as AxiosError)\n\n      if (errorCode === ApiStatusCode.Unauthorized) {\n        dispatch<any>(logoutUserAction())\n      }\n\n      dispatch(addErrorNotification(err))\n      dispatch(getUserInfoFailure(errorMessage))\n      dispatch(setOAuthCloudSource(null))\n      onFailAction?.(errorMessage)\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchPlans(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getPlans())\n\n    try {\n      const { data, status } = await apiService.get<\n        CloudSubscriptionPlanResponse[]\n      >(ApiEndpoints.CLOUD_SUBSCRIPTION_PLANS)\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getPlansSuccess(data))\n        dispatch(setIsOpenSelectPlanDialog(true))\n        dispatch(setSocialDialogState(null))\n        dispatch(setSelectAccountDialogState(false))\n        dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress))\n\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(err))\n      dispatch(getPlansFailure())\n      dispatch(removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress))\n      dispatch(setOAuthCloudSource(null))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function getCapiKeysAction(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getCapiKeys())\n\n    try {\n      const { data, status } = await apiService.get<CloudCapiKey[]>(\n        ApiEndpoints.CLOUD_CAPI_KEYS,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getCapiKeysSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(addErrorNotification(error))\n      dispatch(getCapiKeysFailure())\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function removeAllCapiKeysAction(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(removeAllCapiKeys())\n\n    try {\n      const { status } = await apiService.delete(ApiEndpoints.CLOUD_CAPI_KEYS)\n\n      if (isStatusSuccessful(status)) {\n        dispatch(removeAllCapiKeysSuccess())\n        dispatch(\n          addMessageNotification(successMessages.REMOVED_ALL_CAPI_KEYS()),\n        )\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(addErrorNotification(error))\n      dispatch(removeAllCapiKeysFailure())\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function removeCapiKeyAction(\n  { id, name }: { id: string; name: string },\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(removeCapiKey())\n\n    try {\n      const { status } = await apiService.delete(\n        `${ApiEndpoints.CLOUD_CAPI_KEYS}/${id}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(removeCapiKeySuccess(id))\n        dispatch(addMessageNotification(successMessages.REMOVED_CAPI_KEY(name)))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(addErrorNotification(error))\n      dispatch(removeCapiKeyFailure())\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function logoutUserAction(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(logoutUser())\n    dispatch(setSSOFlow())\n\n    try {\n      const { status } = await apiService.get(ApiEndpoints.CLOUD_ME_LOGOUT)\n\n      if (isStatusSuccessful(status)) {\n        dispatch(logoutUserSuccess())\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const err = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(err))\n      dispatch(logoutUserFailure())\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/panels/aiAssistant.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { v4 as uuidv4 } from 'uuid'\n\nimport { AxiosError } from 'axios'\nimport {\n  apiService,\n  localStorageService,\n  sessionStorageService,\n} from 'uiSrc/services'\nimport { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  AiChatMessage,\n  AiChatType,\n  StateAiAssistant,\n} from 'uiSrc/slices/interfaces/aiAssistant'\nimport {\n  getApiErrorCode,\n  getAxiosError,\n  isStatusSuccessful,\n  Maybe,\n  parseCustomError,\n} from 'uiSrc/utils'\nimport { getBaseUrl } from 'uiSrc/services/apiService'\nimport { getStreamedAnswer } from 'uiSrc/utils/api'\nimport ApiStatusCode from 'uiSrc/constants/apiStatusCode'\nimport {\n  generateAiMessage,\n  generateHumanMessage,\n} from 'uiSrc/utils/transformers/chatbot'\nimport { logoutUserAction } from 'uiSrc/slices/oauth/cloud'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { EnhancedAxiosError } from 'uiSrc/slices/interfaces'\nimport { AppDispatch, RootState } from '../store'\n\nconst getTabSelected = (tab?: string): AiChatType => {\n  if (Object.values(AiChatType).includes(tab as unknown as AiChatType))\n    return tab as AiChatType\n  return AiChatType.Assistance\n}\n\nexport const initialState: StateAiAssistant = {\n  activeTab: getTabSelected(\n    sessionStorageService.get(BrowserStorageItem.selectedAiChat),\n  ),\n  assistant: {\n    loading: false,\n    agreements:\n      localStorageService.get(BrowserStorageItem.generalChatAgreements) ??\n      false,\n    id: sessionStorageService.get(BrowserStorageItem.aiChatSession) ?? '',\n    messages: [],\n  },\n  expert: {\n    loading: false,\n    agreements: [],\n    messages: [],\n  },\n}\n\n// A slice for recipes\nconst aiAssistantSlice = createSlice({\n  name: 'aiAssistant',\n  initialState,\n  reducers: {\n    setSelectedTab: (state, { payload }: PayloadAction<AiChatType>) => {\n      state.activeTab = payload\n      sessionStorageService.set(BrowserStorageItem.selectedAiChat, payload)\n    },\n    updateAssistantChatAgreements: (\n      state,\n      { payload }: PayloadAction<boolean>,\n    ) => {\n      state.assistant.agreements = payload\n      localStorageService.set(BrowserStorageItem.generalChatAgreements, payload)\n    },\n    updateExpertChatAgreements: (state, { payload }: PayloadAction<string>) => {\n      state.expert.agreements.push(payload)\n    },\n    createAssistantChat: (state) => {\n      state.assistant.loading = true\n      state.assistant.id = ''\n      sessionStorageService.remove(BrowserStorageItem.aiChatSession)\n    },\n    createAssistantSuccess: (state, { payload }: PayloadAction<string>) => {\n      state.assistant.id = payload\n      state.assistant.loading = false\n\n      sessionStorageService.set(BrowserStorageItem.aiChatSession, payload)\n    },\n    clearAssistantChatId: (state) => {\n      state.assistant.id = ''\n      sessionStorageService.remove(BrowserStorageItem.aiChatSession)\n    },\n    createAssistantFailed: (state) => {\n      state.assistant.loading = false\n    },\n    getAssistantChatHistory: (state) => {\n      state.assistant.loading = true\n    },\n    getAssistantChatHistorySuccess: (\n      state,\n      { payload }: PayloadAction<Array<AiChatMessage>>,\n    ) => {\n      state.assistant.loading = false\n      state.assistant.messages =\n        payload?.map((m) => ({ ...m, id: `ai_${uuidv4()}` })) || []\n    },\n    getAssistantChatHistoryFailed: (state) => {\n      state.assistant.loading = false\n    },\n    removeAssistantChatHistory: (state) => {\n      state.assistant.loading = true\n    },\n    removeAssistantChatHistorySuccess: (state) => {\n      state.assistant.loading = false\n      state.assistant.messages = []\n      state.assistant.id = ''\n      sessionStorageService.remove(BrowserStorageItem.aiChatSession)\n    },\n    removeAssistantChatHistoryFailed: (state) => {\n      state.assistant.loading = false\n    },\n    sendQuestion: (state, { payload }: PayloadAction<AiChatMessage>) => {\n      state.assistant.messages.push(payload)\n    },\n    setQuestionError: (\n      state,\n      {\n        payload,\n      }: PayloadAction<{\n        id: string\n        error: Maybe<{\n          statusCode: number\n          errorCode?: number\n        }>\n      }>,\n    ) => {\n      state.assistant.messages = state.assistant.messages.map((item) =>\n        item.id === payload.id\n          ? {\n              ...item,\n              error: payload.error,\n            }\n          : item,\n      )\n    },\n    sendAnswer: (state, { payload }: PayloadAction<AiChatMessage>) => {\n      state.assistant.messages.push(payload)\n    },\n    getExpertChatHistory: (state) => {\n      state.expert.loading = true\n    },\n    getExpertChatHistorySuccess: (\n      state,\n      { payload }: PayloadAction<Array<AiChatMessage>>,\n    ) => {\n      state.expert.loading = false\n      state.expert.messages =\n        payload?.map((m) => ({ ...m, id: `ai_${uuidv4()}` })) || []\n    },\n    getExpertChatHistoryFailed: (state) => {\n      state.expert.loading = false\n    },\n    sendExpertQuestion: (state, { payload }: PayloadAction<AiChatMessage>) => {\n      state.expert.messages.push(payload)\n    },\n    setExpertQuestionError: (\n      state,\n      {\n        payload,\n      }: PayloadAction<{\n        id: string\n        error: Maybe<{\n          statusCode: number\n          errorCode?: number\n          details?: Record<string, any>\n        }>\n      }>,\n    ) => {\n      state.expert.messages = state.expert.messages.map((item) =>\n        item.id === payload.id\n          ? {\n              ...item,\n              error: payload.error,\n            }\n          : item,\n      )\n    },\n    sendExpertAnswer: (state, { payload }: PayloadAction<AiChatMessage>) => {\n      state.expert.messages.push(payload)\n    },\n    clearExpertChatHistory: (state) => {\n      state.expert.messages = []\n    },\n  },\n})\n\n// A selector\nexport const aiChatSelector = (state: RootState) => state.panels.aiAssistant\nexport const aiAssistantChatSelector = (state: RootState) =>\n  state.panels.aiAssistant.assistant\nexport const aiExpertChatSelector = (state: RootState) =>\n  state.panels.aiAssistant.expert\n\n// Actions generated from the slice\nexport const {\n  createAssistantChat,\n  updateAssistantChatAgreements,\n  updateExpertChatAgreements,\n  clearAssistantChatId,\n  setSelectedTab,\n  createAssistantSuccess,\n  createAssistantFailed,\n  getAssistantChatHistory,\n  getAssistantChatHistorySuccess,\n  getAssistantChatHistoryFailed,\n  removeAssistantChatHistory,\n  removeAssistantChatHistorySuccess,\n  removeAssistantChatHistoryFailed,\n  sendQuestion,\n  setQuestionError,\n  sendAnswer,\n  getExpertChatHistory,\n  getExpertChatHistorySuccess,\n  getExpertChatHistoryFailed,\n  sendExpertQuestion,\n  setExpertQuestionError,\n  sendExpertAnswer,\n  clearExpertChatHistory,\n} = aiAssistantSlice.actions\n\n// The reducer\nexport default aiAssistantSlice.reducer\n\nexport function createAssistantChatAction(\n  onSuccess?: (chatId: string) => void,\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(createAssistantChat())\n\n    try {\n      const { status, data } = await apiService.post<any>(\n        ApiEndpoints.AI_ASSISTANT_CHATS,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(createAssistantSuccess(data.id))\n        onSuccess?.(data.id)\n      }\n    } catch (e) {\n      dispatch(createAssistantFailed())\n      onFail?.()\n    }\n  }\n}\n\nexport function askAssistantChatbot(\n  id: string,\n  message: string,\n  {\n    onMessage,\n    onFinish,\n    onError,\n  }: {\n    onMessage?: (message: AiChatMessage) => void\n    onError?: (errorCode: number) => void\n    onFinish?: () => void\n  },\n) {\n  return async (dispatch: AppDispatch) => {\n    const humanMessage = generateHumanMessage(message)\n    const aiMessageProgressed = generateAiMessage()\n\n    dispatch(sendQuestion(humanMessage))\n\n    onMessage?.(aiMessageProgressed)\n\n    const baseUrl = getBaseUrl()\n    const url = `${baseUrl}${ApiEndpoints.AI_ASSISTANT_CHATS}/${id}/messages`\n\n    await getStreamedAnswer(url, message, {\n      onMessage: (value: string) => {\n        aiMessageProgressed.content += value\n        onMessage?.(aiMessageProgressed)\n      },\n      onFinish: () => {\n        dispatch(sendAnswer(aiMessageProgressed))\n        onFinish?.()\n      },\n      onError: (error: any) => {\n        dispatch(\n          setQuestionError({\n            id: humanMessage.id,\n            error: {\n              statusCode: error?.status ?? 500,\n              errorCode: error?.errorCode,\n            },\n          }),\n        )\n        onError?.(error?.status ?? 500)\n        onFinish?.()\n      },\n    })\n  }\n}\n\nexport function getAssistantChatHistoryAction(\n  id: string,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getAssistantChatHistory())\n\n    try {\n      const { status, data } = await apiService.get<any>(\n        `${ApiEndpoints.AI_ASSISTANT_CHATS}/${id}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getAssistantChatHistorySuccess(data.messages))\n        onSuccess?.()\n      }\n    } catch (e) {\n      dispatch(getAssistantChatHistoryFailed())\n      const error = e as AxiosError\n      if (error?.response?.status === ApiStatusCode.Unauthorized) {\n        dispatch(clearAssistantChatId())\n      }\n    }\n  }\n}\n\nexport function removeAssistantChatAction(id: string) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(removeAssistantChatHistory())\n\n    try {\n      const { status } = await apiService.delete<any>(\n        `${ApiEndpoints.AI_ASSISTANT_CHATS}/${id}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(removeAssistantChatHistorySuccess())\n      }\n    } catch (e) {\n      dispatch(removeAssistantChatHistoryFailed())\n    }\n  }\n}\n\nexport function getExpertChatHistoryAction(\n  instanceId: string,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getExpertChatHistory())\n\n    try {\n      const { status, data } = await apiService.get<any>(\n        `${ApiEndpoints.AI_EXPERT}/${instanceId}/messages`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getExpertChatHistorySuccess(data))\n        onSuccess?.()\n      }\n    } catch (error) {\n      const err = getAxiosError(error as EnhancedAxiosError)\n      const errorCode = getApiErrorCode(error as AxiosError)\n\n      if (errorCode === ApiStatusCode.Unauthorized) {\n        dispatch<any>(logoutUserAction())\n      }\n\n      dispatch(addErrorNotification(err))\n      dispatch(getExpertChatHistoryFailed())\n    }\n  }\n}\n\nexport function askExpertChatbotAction(\n  databaseId: string,\n  message: string,\n  {\n    onMessage,\n    onError,\n    onFinish,\n  }: {\n    onMessage?: (message: AiChatMessage) => void\n    onError?: (errorCode: number) => void\n    onFinish?: () => void\n  },\n) {\n  return async (dispatch: AppDispatch) => {\n    const humanMessage = generateHumanMessage(message)\n    const aiMessageProgressed: AiChatMessage = generateAiMessage()\n\n    dispatch(sendExpertQuestion(humanMessage))\n\n    onMessage?.(aiMessageProgressed)\n\n    const baseUrl = getBaseUrl()\n    const url = `${baseUrl}${ApiEndpoints.AI_EXPERT}/${databaseId}/messages`\n\n    await getStreamedAnswer(url, message, {\n      onMessage: (value: string) => {\n        aiMessageProgressed.content += value\n        onMessage?.(aiMessageProgressed)\n      },\n      onFinish: () => {\n        dispatch(sendExpertAnswer(aiMessageProgressed))\n        onFinish?.()\n      },\n      onError: (error: any) => {\n        if (error?.status === ApiStatusCode.Unauthorized) {\n          const err = parseCustomError(error)\n          dispatch(addErrorNotification(err))\n          dispatch(logoutUserAction())\n        } else {\n          dispatch(\n            setExpertQuestionError({\n              id: humanMessage.id,\n              error: {\n                statusCode: error?.status ?? 500,\n                errorCode: error?.errorCode,\n                details: error?.details,\n              },\n            }),\n          )\n        }\n\n        onError?.(error?.status ?? 500)\n        onFinish?.()\n      },\n    })\n  }\n}\n\nexport function removeExpertChatHistoryAction(\n  instanceId: string,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    // dispatch(getExpertChatHistory())\n\n    try {\n      const { status } = await apiService.delete<any>(\n        `${ApiEndpoints.AI_EXPERT}/${instanceId}/messages`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(clearExpertChatHistory())\n        onSuccess?.()\n      }\n    } catch (error) {\n      const err = getAxiosError(error as EnhancedAxiosError)\n      const errorCode = getApiErrorCode(error as AxiosError)\n\n      if (errorCode === ApiStatusCode.Unauthorized) {\n        dispatch<any>(logoutUserAction())\n      }\n\n      dispatch(addErrorNotification(err))\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/panels/sidePanels.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { useHistory } from 'react-router-dom'\nimport { Nullable } from 'uiSrc/utils'\nimport {\n  SidePanelsState,\n  InsightsPanelTabs,\n  SidePanels,\n} from 'uiSrc/slices/interfaces/insights'\nimport { sessionStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem, EAManifestFirstKey } from 'uiSrc/constants'\nimport { AppDispatch, RootState } from '../store'\n\nconst getInsightsTabSelected = (tab?: string): InsightsPanelTabs => {\n  if (\n    Object.values(InsightsPanelTabs).includes(\n      tab as unknown as InsightsPanelTabs,\n    )\n  )\n    return tab as InsightsPanelTabs\n  return InsightsPanelTabs.Explore\n}\n\nexport const initialState: SidePanelsState = {\n  openedPanel: sessionStorageService.get(BrowserStorageItem.sidePanel) ?? null,\n  insights: {\n    tabSelected: getInsightsTabSelected(\n      sessionStorageService.get(BrowserStorageItem.insightsPanel),\n    ),\n  },\n  explore: {\n    search: '',\n    itemScrollTop: 0,\n    data: null,\n    url: null,\n    manifest: null,\n    isPageOpen: false,\n  },\n}\n\n// A slice for recipes\nconst insightsPanelSlice = createSlice({\n  name: 'insightsPanel',\n  initialState,\n  reducers: {\n    changeSidePanel: (\n      state,\n      { payload }: { payload: Nullable<SidePanels> },\n    ) => {\n      state.openedPanel = payload\n      sessionStorageService.set(BrowserStorageItem.sidePanel, payload)\n    },\n    toggleSidePanel: (\n      state,\n      { payload }: { payload: Nullable<SidePanels> },\n    ) => {\n      state.openedPanel = payload === state.openedPanel ? null : payload\n      sessionStorageService.set(BrowserStorageItem.sidePanel, state.openedPanel)\n    },\n    changeSelectedTab: (\n      state,\n      { payload }: PayloadAction<InsightsPanelTabs>,\n    ) => {\n      state.insights.tabSelected = payload\n      sessionStorageService.set(BrowserStorageItem.insightsPanel, payload)\n    },\n    setExplorePanelSearch: (state, { payload }: { payload: string }) => {\n      const prevValue = state.explore.search\n      state.explore.search = payload\n      if (prevValue !== payload) {\n        state.explore.itemScrollTop = 0\n      }\n    },\n    setExplorePanelScrollTop: (state, { payload }: { payload: number }) => {\n      state.explore.itemScrollTop = payload || 0\n    },\n    resetExplorePanelSearch: (state) => {\n      state.explore.search = ''\n      state.explore.itemScrollTop = 0\n    },\n    setExplorePanelContent: (state, { payload }) => {\n      state.explore.data = payload.data\n      state.explore.url = payload.url\n    },\n    setExplorePanelIsPageOpen: (state, { payload }) => {\n      state.explore.isPageOpen = payload\n    },\n    setExplorePanelManifest: (state, { payload }) => {\n      state.explore.manifest = payload\n    },\n  },\n})\n\n// A selector\nexport const sidePanelsSelector = (state: RootState) => state.panels.sidePanels\nexport const insightsPanelSelector = (state: RootState) =>\n  state.panels.sidePanels.insights\nexport const explorePanelSelector = (state: RootState) =>\n  state.panels.sidePanels.explore\n\n// Actions generated from the slice\nexport const {\n  changeSidePanel,\n  toggleSidePanel,\n  changeSelectedTab,\n  setExplorePanelContent,\n  setExplorePanelSearch,\n  setExplorePanelScrollTop,\n  resetExplorePanelSearch,\n  setExplorePanelIsPageOpen,\n  setExplorePanelManifest,\n} = insightsPanelSlice.actions\n\nexport function openTutorialByPath(\n  path: Nullable<string>,\n  history: ReturnType<typeof useHistory>,\n  openList = false,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(changeSelectedTab(InsightsPanelTabs.Explore))\n    dispatch(changeSidePanel(SidePanels.Insights))\n\n    if (path) {\n      history.push({\n        search: `path=${EAManifestFirstKey.TUTORIALS}/${path}`,\n      })\n      return\n    }\n\n    if (openList) {\n      dispatch(resetExplorePanelSearch())\n      history.push({\n        search: '',\n      })\n    }\n  }\n}\n\n// The reducer\nexport default insightsPanelSlice.reducer\n"
  },
  {
    "path": "redisinsight/ui/src/slices/pubsub/pubsub.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { StatePubSub } from 'uiSrc/slices/interfaces/pubsub'\nimport { AppDispatch, RootState } from 'uiSrc/slices/store'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { DEFAULT_SEARCH_MATCH } from 'uiSrc/constants/api'\nimport { SubscriptionType } from 'uiSrc/constants/pubSub'\nimport { MessagesResponse } from 'apiSrc/modules/pub-sub/dto/messages.response'\nimport { PublishResponse } from 'apiSrc/modules/pub-sub/dto/publish.response'\n\nexport const initialState: StatePubSub = {\n  loading: false,\n  publishing: false,\n  error: '',\n  subscriptions: [],\n  isSubscribeTriggered: false,\n  isConnected: false,\n  isSubscribed: false,\n  messages: [],\n  count: 0,\n}\n\nexport const PUB_SUB_ITEMS_MAX_COUNT = 5_000\n\nconst pubSubSlice = createSlice({\n  name: 'pubsub',\n  initialState,\n  reducers: {\n    setInitialPubSubState: () => initialState,\n    setPubSubConnected: (state, { payload }: PayloadAction<boolean>) => {\n      state.isConnected = payload\n    },\n    toggleSubscribeTriggerPubSub: (\n      state,\n      { payload }: PayloadAction<string>,\n    ) => {\n      const channels = payload.trim() || DEFAULT_SEARCH_MATCH\n      const subs = channels.split(' ').map((channel) => ({\n        channel,\n        type: SubscriptionType.PSubscribe,\n      }))\n\n      state.isSubscribeTriggered = !state.isSubscribeTriggered\n      state.subscriptions = subs\n    },\n    setIsPubSubSubscribed: (state) => {\n      state.isSubscribed = true\n    },\n    setIsPubSubUnSubscribed: (state) => {\n      state.isSubscribed = false\n    },\n    concatPubSubMessages: (\n      state,\n      { payload }: PayloadAction<MessagesResponse>,\n    ) => {\n      state.count += payload.count\n      if (payload.messages.length >= PUB_SUB_ITEMS_MAX_COUNT) {\n        state.messages = [...payload.messages.slice(-PUB_SUB_ITEMS_MAX_COUNT)]\n        return\n      }\n\n      let newItems = [...state.messages, ...payload.messages]\n\n      if (newItems.length > PUB_SUB_ITEMS_MAX_COUNT) {\n        newItems = newItems.slice(newItems.length - PUB_SUB_ITEMS_MAX_COUNT)\n      }\n\n      state.messages = newItems\n    },\n    clearPubSubMessages: (state) => {\n      state.messages = []\n      state.count = 0\n    },\n    setLoading: (state, { payload }: PayloadAction<boolean>) => {\n      state.loading = payload\n    },\n    disconnectPubSub: (state) => {\n      state.loading = false\n      state.isSubscribed = false\n      state.isSubscribeTriggered = false\n      state.isConnected = false\n    },\n    publishMessage: (state) => {\n      state.publishing = true\n    },\n    publishMessageSuccess: (state) => {\n      state.publishing = false\n      state.error = ''\n    },\n    publishMessageError: (state, { payload }) => {\n      state.publishing = false\n      state.error = payload\n    },\n  },\n})\n\nexport const {\n  setInitialPubSubState,\n  setPubSubConnected,\n  toggleSubscribeTriggerPubSub,\n  setIsPubSubSubscribed,\n  setIsPubSubUnSubscribed,\n  concatPubSubMessages,\n  clearPubSubMessages,\n  setLoading,\n  disconnectPubSub,\n  publishMessage,\n  publishMessageSuccess,\n  publishMessageError,\n} = pubSubSlice.actions\n\nexport const pubSubSelector = (state: RootState) => state.pubsub\n\nexport default pubSubSlice.reducer\n\n// Asynchronous thunk action\nexport function publishMessageAction(\n  instanceId: string,\n  channel: string,\n  message: string,\n  onSuccessAction?: (affected: number) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(publishMessage())\n      const { data, status } = await apiService.post<PublishResponse>(\n        getUrl(instanceId, ApiEndpoints.PUB_SUB_MESSAGES),\n        {\n          channel,\n          message,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(publishMessageSuccess())\n        onSuccessAction?.(data.affected)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(publishMessageError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/rdi/dryRun.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  getApiErrorMessage,\n  getAxiosError,\n  isStatusSuccessful,\n} from 'uiSrc/utils'\nimport {\n  EnhancedAxiosError,\n  IDryRunJobResults,\n  IStateRdiDryRunJob,\n} from 'uiSrc/slices/interfaces'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: IStateRdiDryRunJob = {\n  loading: false,\n  error: '',\n  results: null,\n}\n\nconst rdiPipelineSlice = createSlice({\n  name: 'dryRunJob',\n  initialState,\n  reducers: {\n    setInitialDryRunJob: () => initialState,\n    dryRunJob: (state) => {\n      state.loading = true\n      state.results = null\n    },\n    dryRunJobSuccess: (\n      state,\n      { payload }: PayloadAction<IDryRunJobResults>,\n    ) => {\n      state.loading = false\n      state.results = payload\n      state.error = ''\n    },\n    dryRunJobFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n      state.results = null\n    },\n  },\n})\n\nexport const rdiDryRunJobSelector = (state: RootState) => state.rdi.dryRun\n\nexport const {\n  dryRunJob,\n  dryRunJobSuccess,\n  dryRunJobFailure,\n  setInitialDryRunJob,\n} = rdiPipelineSlice.actions\n\n// The reducer\nexport default rdiPipelineSlice.reducer\n\n// Asynchronous thunk action\nexport function rdiDryRunJob(\n  rdiInstanceId: string,\n  input_data: object,\n  job: unknown,\n  onSuccessAction?: (data: IDryRunJobResults) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(dryRunJob())\n      const { data, status } = await apiService.post<IDryRunJobResults>(\n        `rdi/${rdiInstanceId}/pipeline/dry-run-job`,\n        {\n          input_data,\n          job,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(dryRunJobSuccess(data))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n      const errorMessage = getApiErrorMessage(error)\n\n      dispatch(addErrorNotification(parsedError))\n      dispatch(dryRunJobFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/rdi/instances.ts",
    "content": "import { first, map } from 'lodash'\nimport { createSlice } from '@reduxjs/toolkit'\n\nimport { AxiosError } from 'axios'\nimport {\n  ApiEndpoints,\n  DEFAULT_RDI_SHOWN_COLUMNS,\n  RdiListColumn,\n} from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport {\n  getApiErrorMessage,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n} from 'uiSrc/utils'\nimport { Rdi as RdiInstanceResponse } from 'apiSrc/modules/rdi/models/rdi'\n\nimport { AppDispatch, RootState } from '../store'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../app/notifications'\nimport {\n  IErrorData,\n  InitialStateRdiInstances,\n  RdiInstance,\n} from '../interfaces/rdi'\n\nexport const initialState: InitialStateRdiInstances = {\n  loading: false,\n  error: '',\n  data: [],\n  shownColumns: DEFAULT_RDI_SHOWN_COLUMNS,\n  connectedInstance: {\n    id: '',\n    name: '',\n    url: '',\n    version: '',\n    lastConnection: new Date(),\n    loading: false,\n    error: '',\n  },\n  loadingChanging: false,\n  errorChanging: '',\n  changedSuccessfully: false,\n}\n\n// A slice for recipes\nconst instancesSlice = createSlice({\n  name: 'rdiInstances',\n  initialState,\n  reducers: {\n    // load instances\n    loadInstances: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    loadInstancesSuccess: (\n      state,\n      { payload }: { payload: RdiInstanceResponse[] },\n    ) => {\n      state.data = payload\n      state.loading = false\n    },\n    loadInstancesFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // add/edit instance\n    defaultInstanceChanging: (state) => {\n      state.loadingChanging = true\n      state.changedSuccessfully = false\n      state.errorChanging = ''\n    },\n    defaultInstanceChangingSuccess: (state) => {\n      state.changedSuccessfully = true\n      state.loadingChanging = false\n    },\n    defaultInstanceChangingFailure: (state, { payload = '' }) => {\n      state.loadingChanging = false\n      state.changedSuccessfully = false\n      state.errorChanging = payload.toString()\n    },\n\n    // test instance connection\n    testConnection: (state) => {\n      state.loadingChanging = true\n      state.errorChanging = ''\n    },\n    testConnectionSuccess: (state) => {\n      state.loadingChanging = false\n    },\n    testConnectionFailure: (state, { payload = '' }) => {\n      state.loadingChanging = false\n      state.errorChanging = payload.toString()\n    },\n\n    // delete instances\n    setDefaultInstance: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    setDefaultInstanceSuccess: (state) => {\n      state.loading = false\n      state.error = ''\n    },\n    setDefaultInstanceFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    // set connected instance id\n    setConnectedInstanceId: (state, { payload }: { payload: string }) => {\n      state.connectedInstance = {\n        ...state.connectedInstance,\n        id: payload,\n      }\n    },\n\n    // set edited instance\n    setEditedInstance: (\n      state,\n      { payload }: { payload: Nullable<RdiInstance> },\n    ) => {\n      state.editedInstance.data = payload\n    },\n\n    // set connected instance\n    setConnectedInstance: (state) => {\n      state.connectedInstance = {\n        ...initialState.connectedInstance,\n        loading: true,\n      }\n    },\n\n    // set connected instance success\n    setConnectedInstanceSuccess: (\n      state,\n      { payload }: { payload: RdiInstance },\n    ) => {\n      state.connectedInstance = payload\n      state.connectedInstance.loading = false\n    },\n\n    // set connected instance failed\n    setConnectedInstanceFailure: (state, { payload }) => {\n      state.connectedInstance.error = payload\n      state.connectedInstance.loading = false\n    },\n\n    // reset connected instance\n    resetConnectedInstance: (state) => {\n      state.connectedInstance = initialState.connectedInstance\n    },\n\n    updateConnectedInstance: (state, { payload }: { payload: RdiInstance }) => {\n      state.connectedInstance = { ...state.connectedInstance, ...payload }\n    },\n\n    setShownColumns: (state, { payload }: { payload: RdiListColumn[] }) => {\n      state.shownColumns = [...payload]\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  loadInstances,\n  loadInstancesSuccess,\n  loadInstancesFailure,\n  defaultInstanceChanging,\n  defaultInstanceChangingSuccess,\n  defaultInstanceChangingFailure,\n  testConnection,\n  testConnectionSuccess,\n  testConnectionFailure,\n  setDefaultInstance,\n  setDefaultInstanceSuccess,\n  setDefaultInstanceFailure,\n  setConnectedInstanceId,\n  setEditedInstance,\n  setConnectedInstance,\n  setConnectedInstanceSuccess,\n  setConnectedInstanceFailure,\n  resetConnectedInstance,\n  updateConnectedInstance,\n  setShownColumns,\n} = instancesSlice.actions\n\n// selectors\nexport const instancesSelector = (state: RootState) => state.rdi.instances\nexport const connectedInstanceSelector = (state: RootState) =>\n  state.rdi.instances.connectedInstance\n\n// The reducer\nexport default instancesSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchInstancesAction(\n  onSuccess?: (data: RdiInstance[]) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(loadInstances())\n\n    try {\n      const { data, status } = await apiService.get<RdiInstanceResponse[]>(\n        ApiEndpoints.RDI_INSTANCES,\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccess?.(data as RdiInstance[])\n        dispatch(loadInstancesSuccess(data))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(loadInstancesFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function createInstanceAction(\n  payload: Partial<RdiInstance>,\n  onSuccess?: (data: RdiInstanceResponse) => void,\n  onFail?: (error: Maybe<string | number>) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(defaultInstanceChanging())\n\n    try {\n      const { status, data } = await apiService.post<RdiInstanceResponse>(\n        `${ApiEndpoints.RDI_INSTANCES}`,\n        payload,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(defaultInstanceChangingSuccess())\n        dispatch(fetchInstancesAction())\n\n        dispatch(\n          addMessageNotification(\n            successMessages.ADDED_NEW_RDI_INSTANCE(payload.name ?? ''),\n          ),\n        )\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(defaultInstanceChangingFailure(errorMessage))\n      const errorData = error?.response?.data as IErrorData\n      onFail?.(errorData?.errorCode || errorData?.error)\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function editInstanceAction(\n  id: string,\n  payload: Partial<RdiInstance>,\n  onSuccess?: (data: RdiInstanceResponse) => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(defaultInstanceChanging())\n\n    try {\n      const { status, data } = await apiService.patch<RdiInstanceResponse>(\n        `${ApiEndpoints.RDI_INSTANCES}/${id}`,\n        payload,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(defaultInstanceChangingSuccess())\n        dispatch(fetchInstancesAction())\n\n        const state = stateInit()\n        if (state.rdi.instances.connectedInstance?.id === data.id) {\n          dispatch(updateConnectedInstance(data))\n        }\n\n        onSuccess?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(defaultInstanceChangingFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteInstancesAction(\n  instances: RdiInstance[],\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(setDefaultInstance())\n\n    try {\n      const state = stateInit()\n      const instancesIds = map(instances, 'id')\n\n      const { status } = await apiService.delete(ApiEndpoints.RDI_INSTANCES, {\n        data: { ids: instancesIds },\n      })\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setDefaultInstanceSuccess())\n        dispatch<any>(fetchInstancesAction())\n\n        if (instancesIds.includes(state.app.context.contextInstanceId)) {\n          dispatch(resetConnectedInstance())\n        }\n        onSuccess?.()\n\n        if (instances.length === 1) {\n          dispatch(\n            addMessageNotification(\n              successMessages.DELETE_RDI_INSTANCE(first(instances)?.name ?? ''),\n            ),\n          )\n        } else {\n          dispatch(\n            addMessageNotification(\n              successMessages.DELETE_RDI_INSTANCES(map(instances, 'name')),\n            ),\n          )\n        }\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchConnectedInstanceAction(\n  id: string,\n  onSuccess?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setConnectedInstance())\n\n    try {\n      const { data, status } = await apiService.get<RdiInstanceResponse>(\n        `${ApiEndpoints.RDI_INSTANCES}/${id}`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setConnectedInstanceSuccess(data))\n      }\n      onSuccess?.()\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(setConnectedInstanceFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function checkConnectToRdiInstanceAction(\n  id: string = '',\n  onSuccessAction?: (id: string) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(setDefaultInstance())\n    try {\n      const { status } = await apiService.get(\n        `${ApiEndpoints.RDI_INSTANCES}/${id}/connect`,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setDefaultInstanceSuccess())\n        onSuccessAction?.(id)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n\n      dispatch(setDefaultInstanceFailure(errorMessage))\n      dispatch(addErrorNotification({ ...error, instanceId: id }))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/rdi/pipeline.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { get, omit } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport {\n  IStateRdiPipeline,\n  IPipeline,\n  FileChangeType,\n  IPipelineJSON,\n  IRdiPipelineStrategy,\n  TJMESPathFunctions,\n  IPipelineStatus,\n  IActionPipelineResultProps,\n  PipelineAction,\n  IRdiPipelineJob,\n} from 'uiSrc/slices/interfaces/rdi'\nimport {\n  getApiErrorMessage,\n  getAxiosError,\n  getRdiUrl,\n  isStatusSuccessful,\n  Nullable,\n  parseJMESPathFunctions,\n  pipelineToYaml,\n} from 'uiSrc/utils'\nimport { EnhancedAxiosError } from 'uiSrc/slices/interfaces'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: IStateRdiPipeline = {\n  loading: true,\n  error: '',\n  data: null,\n  config: '',\n  jobs: [],\n  // pipeline validation is based on combination of config + job/s definitions\n  isPipelineValid: false,\n  configValidationErrors: [],\n  jobsValidationErrors: {},\n  resetChecked: false,\n  schema: null,\n  jobNameSchema: null,\n  monacoJobsSchema: null,\n  strategies: {\n    loading: false,\n    error: '',\n    data: [],\n  },\n  changes: {},\n  jobFunctions: [],\n  status: {\n    loading: false,\n    data: null,\n    error: '',\n  },\n  pipelineAction: {\n    loading: false,\n    action: null,\n    error: '',\n  },\n}\n\nconst rdiPipelineSlice = createSlice({\n  name: 'rdiPipeline',\n  initialState,\n  reducers: {\n    setPipelineInitialState: () => initialState,\n    resetPipelineChecked: (state, { payload }: PayloadAction<boolean>) => {\n      state.resetChecked = payload\n    },\n    getPipeline: (state) => {\n      state.loading = true\n    },\n    getPipelineSuccess: (state, { payload }: PayloadAction<IPipeline>) => {\n      state.loading = false\n      state.data = payload\n      state.config = payload?.config || ''\n      state.jobs = payload?.jobs || []\n    },\n    getPipelineFailure: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n    },\n    setPipelineConfig: (state, { payload }: PayloadAction<string>) => {\n      state.config = payload\n    },\n    setPipelineJobs: (state, { payload }: PayloadAction<IRdiPipelineJob[]>) => {\n      state.jobs = payload\n    },\n    updatePipelineJob: (\n      state,\n      { payload }: PayloadAction<{ name: string; value: string }>,\n    ) => {\n      const jobIndex = state.jobs.findIndex((job) => job.name === payload.name)\n      if (jobIndex !== -1) {\n        state.jobs[jobIndex].value = payload.value\n      }\n    },\n    deployPipeline: (state) => {\n      state.loading = true\n    },\n    deployPipelineSuccess: (state) => {\n      state.loading = false\n    },\n    deployPipelineFailure: (state) => {\n      state.loading = false\n    },\n    triggerPipelineAction: (\n      state,\n      { payload }: PayloadAction<PipelineAction>,\n    ) => {\n      state.pipelineAction.loading = true\n      state.pipelineAction.action = payload\n      state.pipelineAction.error = ''\n    },\n    triggerPipelineActionSuccess: (state) => {\n      state.pipelineAction.loading = false\n      state.pipelineAction.action = null\n    },\n    triggerPipelineActionFailure: (\n      state,\n      { payload }: PayloadAction<string>,\n    ) => {\n      state.pipelineAction.loading = false\n      state.pipelineAction.error = payload\n    },\n    setPipelineSchema: (\n      state,\n      { payload }: PayloadAction<Nullable<object>>,\n    ) => {\n      state.schema = payload\n    },\n    setMonacoJobsSchema: (\n      state,\n      { payload }: PayloadAction<Nullable<object>>,\n    ) => {\n      state.monacoJobsSchema = payload\n    },\n    setJobNameSchema: (state, { payload }: PayloadAction<Nullable<object>>) => {\n      state.jobNameSchema = payload\n    },\n    getPipelineStrategies: (state) => {\n      state.strategies.loading = true\n    },\n    getPipelineStrategiesSuccess: (\n      state,\n      { payload }: PayloadAction<IRdiPipelineStrategy[]>,\n    ) => {\n      state.strategies = {\n        loading: false,\n        error: '',\n        data: payload,\n      }\n    },\n    getPipelineStrategiesFailure: (state, { payload }) => {\n      state.strategies = {\n        loading: false,\n        error: payload,\n        data: [],\n      }\n    },\n    setChangedFile: (\n      state,\n      { payload }: PayloadAction<{ name: string; status: FileChangeType }>,\n    ) => {\n      state.changes[payload.name] = payload.status\n    },\n    setChangedFiles: (\n      state,\n      { payload }: PayloadAction<Record<string, FileChangeType>>,\n    ) => {\n      state.changes = payload\n    },\n    deleteChangedFile: (state, { payload }: PayloadAction<string>) => {\n      delete state.changes[payload]\n    },\n    getPipelineStatus: (state) => {\n      state.status.loading = true\n    },\n    getPipelineStatusSuccess: (\n      state,\n      { payload }: PayloadAction<IPipelineStatus>,\n    ) => {\n      state.status = {\n        loading: false,\n        error: '',\n        data: payload,\n      }\n    },\n    getPipelineStatusFailure: (state, { payload }: PayloadAction<string>) => {\n      state.status = {\n        loading: false,\n        error: payload,\n        data: null,\n      }\n    },\n    setJobFunctions: (\n      state,\n      { payload }: PayloadAction<TJMESPathFunctions>,\n    ) => {\n      state.jobFunctions = parseJMESPathFunctions(payload)\n    },\n    setIsPipelineValid: (state, { payload }: PayloadAction<boolean>) => {\n      state.isPipelineValid = payload\n    },\n    setConfigValidationErrors: (\n      state,\n      { payload }: PayloadAction<string[]>,\n    ) => {\n      state.configValidationErrors = payload\n    },\n    setJobsValidationErrors: (\n      state,\n      { payload }: PayloadAction<Record<string, string[]>>,\n    ) => {\n      state.jobsValidationErrors = payload\n    },\n  },\n})\n\nexport const rdiPipelineSelector = (state: RootState) => state.rdi.pipeline\nexport const rdiPipelineActionSelector = (state: RootState) =>\n  state.rdi.pipeline.pipelineAction\nexport const rdiPipelineStrategiesSelector = (state: RootState) =>\n  state.rdi.pipeline.strategies\nexport const rdiPipelineStatusSelector = (state: RootState) =>\n  state.rdi.pipeline.status\n\nexport const {\n  resetPipelineChecked,\n  getPipeline,\n  getPipelineSuccess,\n  getPipelineFailure,\n  deployPipeline,\n  deployPipelineSuccess,\n  deployPipelineFailure,\n  setPipelineSchema,\n  setMonacoJobsSchema,\n  setJobNameSchema,\n  getPipelineStrategies,\n  getPipelineStrategiesSuccess,\n  getPipelineStrategiesFailure,\n  setPipelineConfig,\n  setPipelineJobs,\n  updatePipelineJob,\n  setPipelineInitialState,\n  setChangedFile,\n  setChangedFiles,\n  deleteChangedFile,\n  setJobFunctions,\n  getPipelineStatus,\n  getPipelineStatusSuccess,\n  getPipelineStatusFailure,\n  triggerPipelineAction,\n  triggerPipelineActionSuccess,\n  triggerPipelineActionFailure,\n  setIsPipelineValid,\n  setConfigValidationErrors,\n  setJobsValidationErrors,\n} = rdiPipelineSlice.actions\n\n// The reducer\nexport default rdiPipelineSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchRdiPipeline(\n  rdiInstanceId: string,\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getPipeline())\n      const { data, status } = await apiService.get<IPipelineJSON>(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE),\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(getPipelineSuccess(pipelineToYaml(data)))\n        dispatch(setChangedFiles({}))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getPipelineFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function deployPipelineAction(\n  rdiInstanceId: string,\n  pipeline: IPipelineJSON,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(deployPipeline())\n      const { status } = await apiService.post(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_DEPLOY_PIPELINE),\n        pipeline,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deployPipelineSuccess())\n        dispatch(setChangedFiles({}))\n        dispatch(\n          addInfiniteNotification(INFINITE_MESSAGES.SUCCESS_DEPLOY_PIPELINE()),\n        )\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(parsedError))\n      dispatch(deployPipelineFailure())\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchPipelineStrategies(\n  rdiInstanceId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getPipelineStrategies())\n      const { status, data } = await apiService.get<{\n        strategies: IRdiPipelineStrategy[]\n      }>(getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_STRATEGIES))\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getPipelineStrategiesSuccess(data.strategies))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const parsedError = getApiErrorMessage(error as EnhancedAxiosError)\n\n      dispatch(getPipelineStrategiesFailure(parsedError))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchJobTemplate(\n  rdiInstanceId: string,\n  pipelineType: string,\n  onSuccessAction?: (template: string) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { status, data } = await apiService.get(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_JOB_TEMPLATE, pipelineType),\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.(data.template)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(parsedError))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchConfigTemplate(\n  rdiInstanceId: string,\n  pipelineType: string,\n  dbType: string,\n  onSuccessAction?: (template: string) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { status, data } = await apiService.get(\n        getRdiUrl(\n          rdiInstanceId,\n          ApiEndpoints.RDI_CONFIG_TEMPLATE,\n          pipelineType,\n          dbType,\n        ),\n      )\n\n      if (isStatusSuccessful(status)) {\n        onSuccessAction?.(data.template)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(parsedError))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchRdiPipelineSchema(\n  rdiInstanceId: string,\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { data, status } = await apiService.get<Nullable<object>>(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_SCHEMA),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setPipelineSchema(data))\n        dispatch(\n          setMonacoJobsSchema({\n            ...omit(get(data, ['jobs'], {}), ['properties.name']),\n            required: get(data, ['jobs', 'required'], []).filter(\n              (val: string) => val !== 'name',\n            ),\n          }),\n        )\n        dispatch(\n          setJobNameSchema(get(data, ['jobs', 'properties', 'name'], null)),\n        )\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      dispatch(setPipelineSchema(null))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function fetchRdiPipelineJobFunctions(\n  rdiInstanceId: string,\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { data, status } = await apiService.get<TJMESPathFunctions>(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_JOB_FUNCTIONS),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(setJobFunctions(data))\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      onFailAction?.()\n    }\n  }\n}\n\nexport function deletePipelineJob(name: string) {\n  return (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const { data } = state.rdi.pipeline\n    if (data?.jobs?.find((el) => el.name === name)) {\n      dispatch(setChangedFile({ name, status: FileChangeType.Removed }))\n    } else {\n      dispatch(deleteChangedFile(name))\n    }\n  }\n}\n\nexport function getPipelineStatusAction(\n  rdiInstanceId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getPipelineStatus())\n      const { data, status } = await apiService.get<IPipelineStatus>(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_STATUS),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getPipelineStatusSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getPipelineStatusFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function stopPipelineAction(\n  rdiInstanceId: string,\n  onSuccessAction?: (result: IActionPipelineResultProps) => void,\n  onErrorAction?: (result: IActionPipelineResultProps) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(triggerPipelineAction(PipelineAction.Stop))\n      const { status } = await apiService.post(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_STOP),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(triggerPipelineActionSuccess())\n        onSuccessAction?.({ success: true, error: null })\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(parsedError))\n      dispatch(triggerPipelineActionFailure(errorMessage))\n      onErrorAction?.({ success: false, error: errorMessage })\n    }\n  }\n}\n\nexport function startPipelineAction(\n  rdiInstanceId: string,\n  onSuccessAction?: (result: IActionPipelineResultProps) => void,\n  onErrorAction?: (result: IActionPipelineResultProps) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(triggerPipelineAction(PipelineAction.Start))\n      const { status } = await apiService.post(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_START),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(triggerPipelineActionSuccess())\n        onSuccessAction?.({ success: true, error: null })\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(parsedError))\n      dispatch(triggerPipelineActionFailure(errorMessage))\n      onErrorAction?.({ success: false, error: errorMessage })\n    }\n  }\n}\n\nexport function resetPipelineAction(\n  rdiInstanceId: string,\n  onSuccessAction?: (result: IActionPipelineResultProps) => void,\n  onErrorAction?: (result: IActionPipelineResultProps) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(triggerPipelineAction(PipelineAction.Reset))\n      const { status } = await apiService.post(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_PIPELINE_RESET),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(triggerPipelineActionSuccess())\n        dispatch(\n          addMessageNotification(successMessages.SUCCESS_RESET_PIPELINE()),\n        )\n        onSuccessAction?.({ success: true, error: null })\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      const parsedError = getAxiosError(error as EnhancedAxiosError)\n\n      dispatch(addErrorNotification(parsedError))\n      dispatch(triggerPipelineActionFailure(errorMessage))\n      onErrorAction?.({ success: false, error: errorMessage })\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/rdi/statistics.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\n\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  IRdiStatistics,\n  IStateRdiStatistics,\n} from 'uiSrc/slices/interfaces/rdi'\nimport { getApiErrorMessage, getRdiUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: IStateRdiStatistics = {\n  loading: true,\n  error: '',\n  results: null,\n}\n\nconst rdiStatisticsSlice = createSlice({\n  name: 'rdiStatistics',\n  initialState,\n  reducers: {\n    getStatistics: (state) => {\n      state.loading = true\n    },\n    getStatisticsSuccess: (\n      state,\n      { payload }: PayloadAction<IRdiStatistics>,\n    ) => {\n      state.loading = false\n      state.error = ''\n      state.results = {\n        data: payload.data,\n        status: payload.status,\n      }\n    },\n    getStatisticsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\nexport const rdiStatisticsSelector = (state: RootState) => state.rdi.statistics\n\nexport const { getStatistics, getStatisticsSuccess, getStatisticsFailure } =\n  rdiStatisticsSlice.actions\n\n// The reducer\nexport default rdiStatisticsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchRdiStatistics(\n  rdiInstanceId: string,\n  section?: string,\n  onSuccessAction?: (data: any) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getStatistics())\n      const { data, status } = await apiService.get<IRdiStatistics>(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_STATISTICS),\n        section ? { params: { sections: section } } : undefined,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getStatisticsSuccess(data))\n\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getStatisticsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/rdi/testConnections.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport axios, { AxiosError } from 'axios'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  getApiErrorMessage,\n  getAxiosError,\n  getRdiUrl,\n  isStatusSuccessful,\n  Maybe,\n  Nullable,\n  transformConnectionResults,\n} from 'uiSrc/utils'\nimport {\n  EnhancedAxiosError,\n  IStateRdiTestConnections,\n  TestConnectionsResponse,\n  TransformResult,\n} from 'uiSrc/slices/interfaces'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { AppDispatch, RootState } from '../store'\n\nexport const initialState: IStateRdiTestConnections = {\n  loading: false,\n  error: '',\n  results: null,\n}\n\nconst rdiTestConnectionsSlice = createSlice({\n  name: 'testConnections',\n  initialState,\n  reducers: {\n    setInitialDryRunJob: () => initialState,\n    testConnections: (state) => {\n      state.loading = true\n      state.results = null\n    },\n    testConnectionsSuccess: (\n      state,\n      { payload }: PayloadAction<TransformResult>,\n    ) => {\n      state.loading = false\n      state.results = payload\n      state.error = ''\n    },\n    testConnectionsFailure: (\n      state,\n      { payload }: PayloadAction<Maybe<string>>,\n    ) => {\n      state.loading = false\n      state.results = null\n\n      if (payload) {\n        state.error = payload\n      }\n    },\n  },\n})\n\nexport const rdiTestConnectionsSelector = (state: RootState) =>\n  state.rdi.testConnections\n\nexport const {\n  testConnections,\n  testConnectionsSuccess,\n  testConnectionsFailure,\n} = rdiTestConnectionsSlice.actions\n\n// The reducer\nexport default rdiTestConnectionsSlice.reducer\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let testConnectionsController: Nullable<AbortController> = null\n\n// Asynchronous thunk action\nexport function testConnectionsAction(\n  rdiInstanceId: string,\n  config: unknown,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(testConnections())\n\n    try {\n      testConnectionsController?.abort()\n      testConnectionsController = new AbortController()\n\n      const { status, data } = await apiService.post<TestConnectionsResponse>(\n        getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_TEST_CONNECTIONS),\n        config,\n        {\n          signal: testConnectionsController.signal,\n        },\n      )\n\n      testConnectionsController = null\n\n      if (isStatusSuccessful(status)) {\n        dispatch(testConnectionsSuccess(transformConnectionResults(data)))\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      if (!axios.isCancel(_err)) {\n        const error = _err as AxiosError\n        const parsedError = getAxiosError(error as EnhancedAxiosError)\n        const errorMessage = getApiErrorMessage(error)\n\n        dispatch(addErrorNotification(parsedError))\n        dispatch(testConnectionsFailure(errorMessage))\n        onFailAction?.()\n      } else {\n        dispatch(testConnectionsFailure())\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/recommendations/recommendations.ts",
    "content": "import { PayloadAction, createSlice } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\n\nimport { remove, some } from 'lodash'\nimport { apiService, resourcesService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils'\nimport {\n  DeleteDatabaseRecommendationResponse,\n  ModifyDatabaseRecommendationDto,\n} from 'apiSrc/modules/database-recommendation/dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport {\n  StateRecommendations,\n  IRecommendations,\n  IRecommendation,\n  IRecommendationsStatic,\n} from '../interfaces/recommendations'\n\nexport const initialState: StateRecommendations = {\n  data: {\n    recommendations: [],\n    totalUnread: 0,\n  },\n  content: {},\n  loading: false,\n  error: '',\n  isHighlighted: false,\n}\n\n// A slice for recipes\nconst recommendationsSlice = createSlice({\n  name: 'recommendations',\n  initialState,\n  reducers: {\n    setInitialRecommendationsState: (state) => ({\n      ...initialState,\n      content: state.content,\n    }),\n    resetRecommendationsHighlighting: (state) => {\n      state.isHighlighted = false\n    },\n    getRecommendations: (state) => {\n      state.loading = true\n      state.error = ''\n    },\n    getRecommendationsSuccess: (\n      state,\n      { payload }: { payload: IRecommendations },\n    ) => {\n      state.loading = false\n      state.data = payload\n      state.error = ''\n    },\n    getRecommendationsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    setIsHighlighted: (state, { payload }) => {\n      state.isHighlighted = payload\n    },\n    setTotalUnread: (state, { payload }) => {\n      state.data.totalUnread = payload\n      state.isHighlighted = !!payload\n    },\n    addUnreadRecommendations: (state, { payload }) => {\n      payload.recommendations?.forEach((r: IRecommendation) => {\n        const isRecommendationExists = some(\n          state.data.recommendations,\n          (stateR) => r.id === stateR.id,\n        )\n        if (!isRecommendationExists) {\n          state.data.recommendations?.unshift(r)\n        }\n      })\n      state.data.totalUnread = payload.totalUnread\n      state.isHighlighted = !!payload.totalUnread\n    },\n    readRecommendations: (state, { payload }) => {\n      state.data = {\n        ...state.data,\n        totalUnread: payload,\n      }\n    },\n    updateRecommendation: () => {\n      // we don't have any loading here\n    },\n    updateRecommendationSuccess: (\n      state,\n      { payload }: PayloadAction<IRecommendation>,\n    ) => {\n      state.data.recommendations = [\n        ...state.data.recommendations.map((recommendation) =>\n          payload.id === recommendation.id ? payload : recommendation,\n        ),\n      ]\n    },\n    updateRecommendationError: (state, { payload }) => {\n      state.error = payload\n    },\n    deleteRecommendations: (\n      state,\n      { payload }: PayloadAction<Array<{ id: string; isRead: boolean }>>,\n    ) => {\n      remove(state.data.recommendations, (r) =>\n        some(payload, (pR) => pR.id === r.id),\n      )\n      const countUnread = payload.filter((r) => !r.isRead).length\n      state.data.totalUnread -= countUnread\n    },\n\n    getContentRecommendations: (state) => {\n      state.loading = true\n    },\n    getContentRecommendationsSuccess: (\n      state,\n      { payload }: PayloadAction<IRecommendationsStatic>,\n    ) => {\n      state.loading = false\n      state.content = payload\n    },\n    getContentRecommendationsFailure: (state) => {\n      state.loading = false\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setInitialRecommendationsState,\n  resetRecommendationsHighlighting,\n  getRecommendations,\n  getRecommendationsSuccess,\n  getRecommendationsFailure,\n  setIsHighlighted,\n  readRecommendations,\n  updateRecommendation,\n  updateRecommendationSuccess,\n  updateRecommendationError,\n  setTotalUnread,\n  addUnreadRecommendations,\n  deleteRecommendations,\n  getContentRecommendations,\n  getContentRecommendationsSuccess,\n  getContentRecommendationsFailure,\n} = recommendationsSlice.actions\n\n// A selector\nexport const recommendationsSelector = (state: RootState) =>\n  state.recommendations\n\n// The reducer\nexport default recommendationsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchRecommendationsAction(\n  instanceId: string,\n  onSuccessAction?: (recommendations: IRecommendation[]) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      dispatch(getRecommendations())\n\n      const { data, status } = await apiService.get<IRecommendations>(\n        getUrl(instanceId, ApiEndpoints.RECOMMENDATIONS),\n      )\n\n      if (isStatusSuccessful(status)) {\n        if (data.totalUnread) {\n          dispatch(setIsHighlighted(true))\n        }\n        dispatch(getRecommendationsSuccess(data))\n        onSuccessAction?.(data.recommendations)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(getRecommendationsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function readRecommendationsAction(instanceId: string) {\n  return async (dispatch: AppDispatch) => {\n    try {\n      const { data, status } = await apiService.patch(\n        getUrl(instanceId, ApiEndpoints.RECOMMENDATIONS_READ),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(readRecommendations(data.totalUnread))\n        dispatch(setIsHighlighted(false))\n      }\n    } catch (error) {\n      // ignore\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateLiveRecommendation(\n  id: string,\n  dto: ModifyDatabaseRecommendationDto,\n  onSuccessAction?: (recommendation: IRecommendation) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      dispatch(updateRecommendation())\n      const state = stateInit()\n      const instanceId = state.connections.instances.connectedInstance?.id\n\n      const { data, status } = await apiService.patch<IRecommendation>(\n        getUrl(instanceId, ApiEndpoints.RECOMMENDATIONS, id),\n        dto,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(updateRecommendationSuccess(data))\n        onSuccessAction?.(data)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateRecommendationError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteLiveRecommendations(\n  recommendations: Array<{ id: string; isRead: boolean }>,\n  onSuccessAction?: (instanceId: string) => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      dispatch(updateRecommendation())\n      const state = stateInit()\n      const instanceId = state.connections.instances.connectedInstance?.id\n\n      const { status } =\n        await apiService.delete<DeleteDatabaseRecommendationResponse>(\n          getUrl(instanceId, ApiEndpoints.RECOMMENDATIONS),\n          { data: { ids: recommendations.map(({ id }) => id) } },\n        )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteRecommendations(recommendations))\n        onSuccessAction?.(instanceId)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(updateRecommendationError(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchContentRecommendations() {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getContentRecommendations())\n\n    try {\n      const { data, status } =\n        await resourcesService.get<IRecommendationsStatic>(\n          ApiEndpoints.CONTENT_RECOMMENDATIONS,\n        )\n      if (isStatusSuccessful(status)) {\n        dispatch(getContentRecommendationsSuccess(data))\n      }\n    } catch (_err) {\n      dispatch(getContentRecommendationsFailure())\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/search/searchAndQuery.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { RunQueryMode, StateSearchAndQuery } from 'uiSrc/slices/interfaces'\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport { RootState } from 'uiSrc/slices/store'\n\nexport const initialState: StateSearchAndQuery = {\n  isLoaded: false,\n  loading: false,\n  processing: false,\n  clearing: false,\n  error: '',\n  items: [],\n  activeRunQueryMode:\n    localStorageService?.get(BrowserStorageItem.SQRunQueryMode) ??\n    RunQueryMode.ASCII,\n}\n\nconst searchAndQuerySlice = createSlice({\n  name: 'searchAndQuery',\n  initialState,\n  reducers: {\n    changeSQActiveRunQueryMode: (state, { payload }) => {\n      state.activeRunQueryMode = payload\n      localStorageService.set(BrowserStorageItem.SQRunQueryMode, payload)\n    },\n  },\n})\n\nexport const searchAndQuerySelector = (state: RootState) => state.search.query\n\nexport default searchAndQuerySlice.reducer\n\nexport const { changeSQActiveRunQueryMode } = searchAndQuerySlice.actions\n"
  },
  {
    "path": "redisinsight/ui/src/slices/store.ts",
    "content": "import { configureStore, combineReducers } from '@reduxjs/toolkit'\n\nimport { getConfig } from 'uiSrc/config'\nimport instancesReducer from './instances/instances'\nimport caCertsReducer from './instances/caCerts'\nimport clientCertsReducer from './instances/clientCerts'\nimport clusterReducer from './instances/cluster'\nimport cloudReducer from './instances/cloud'\nimport sentinelReducer from './instances/sentinel'\nimport azureReducer from './instances/azure'\nimport keysReducer from './browser/keys'\nimport stringReducer from './browser/string'\nimport zsetReducer from './browser/zset'\nimport setReducer from './browser/set'\nimport hashReducer from './browser/hash'\nimport listReducer from './browser/list'\nimport rejsonReducer from './browser/rejson'\nimport streamReducer from './browser/stream'\nimport bulkActionsReducer from './browser/bulkActions'\nimport notificationsReducer from './app/notifications'\nimport cliSettingsReducer from './cli/cli-settings'\nimport outputReducer from './cli/cli-output'\nimport monitorReducer from './cli/monitor'\nimport userSettingsReducer from './user/user-settings'\nimport cloudUserProfile from './user/cloud-user-profile'\nimport appInfoReducer from './app/info'\nimport appInitReducer from './app/init'\nimport appConnectivityReducer from './app/connectivity'\nimport appContextReducer from './app/context'\nimport appCsrfReducer from './app/csrf'\nimport appRedisCommandsReducer from './app/redis-commands'\nimport appPluginsReducer from './app/plugins'\nimport appsSocketConnectionReducer from './app/socket-connection'\nimport appFeaturesReducer from './app/features'\nimport appUrlHandlingReducer from './app/url-handling'\nimport appOauthReducer from './oauth/cloud'\nimport azureAuthReducer from './oauth/azure'\nimport workbenchResultsReducer from './workbench/wb-results'\nimport workbenchTutorialsReducer from './workbench/wb-tutorials'\nimport workbenchCustomTutorialsReducer from './workbench/wb-custom-tutorials'\nimport searchAndQueryReducer from './search/searchAndQuery'\nimport contentCreateRedisButtonReducer from './content/create-redis-buttons'\nimport contentGuideLinksReducer from './content/guide-links'\nimport pubSubReducer from './pubsub/pubsub'\nimport slowLogReducer from './analytics/slowlog'\nimport analyticsSettingsReducer from './analytics/settings'\nimport clusterDetailsReducer from './analytics/clusterDetails'\nimport databaseAnalysisReducer from './analytics/dbAnalysis'\nimport redisearchReducer from './browser/redisearch'\nimport recommendationsReducer from './recommendations/recommendations'\nimport sidePanelsReducer from './panels/sidePanels'\nimport rdiInstancesReducer from './rdi/instances'\nimport rdiPipelineReducer from './rdi/pipeline'\nimport rdiDryRunJobReducer from './rdi/dryRun'\nimport rdiTestConnectionsReducer from './rdi/testConnections'\nimport rdiStatisticsReducer from './rdi/statistics'\nimport aiAssistantReducer from './panels/aiAssistant'\nimport appDbSettingsReducer from './app/db-settings'\nimport tagsReducer from './instances/tags'\n\nconst riConfig = getConfig()\n\nexport const rootReducer = combineReducers({\n  app: combineReducers({\n    info: appInfoReducer,\n    notifications: notificationsReducer,\n    context: appContextReducer,\n    redisCommands: appRedisCommandsReducer,\n    plugins: appPluginsReducer,\n    socketConnection: appsSocketConnectionReducer,\n    features: appFeaturesReducer,\n    urlHandling: appUrlHandlingReducer,\n    csrf: appCsrfReducer,\n    init: appInitReducer,\n    connectivity: appConnectivityReducer,\n    dbSettings: appDbSettingsReducer,\n  }),\n  connections: combineReducers({\n    instances: instancesReducer,\n    caCerts: caCertsReducer,\n    clientCerts: clientCertsReducer,\n    cluster: clusterReducer,\n    cloud: cloudReducer,\n    sentinel: sentinelReducer,\n    azure: azureReducer,\n    tags: tagsReducer,\n  }),\n  browser: combineReducers({\n    keys: keysReducer,\n    string: stringReducer,\n    zset: zsetReducer,\n    set: setReducer,\n    hash: hashReducer,\n    list: listReducer,\n    rejson: rejsonReducer,\n    stream: streamReducer,\n    bulkActions: bulkActionsReducer,\n    redisearch: redisearchReducer,\n  }),\n  cli: combineReducers({\n    settings: cliSettingsReducer,\n    output: outputReducer,\n    monitor: monitorReducer,\n  }),\n  user: combineReducers({\n    settings: userSettingsReducer,\n    cloudProfile: cloudUserProfile,\n  }),\n  workbench: combineReducers({\n    results: workbenchResultsReducer,\n    tutorials: workbenchTutorialsReducer,\n    customTutorials: workbenchCustomTutorialsReducer,\n  }),\n  search: combineReducers({\n    query: searchAndQueryReducer,\n  }),\n  content: combineReducers({\n    createRedisButtons: contentCreateRedisButtonReducer,\n    guideLinks: contentGuideLinksReducer,\n  }),\n  analytics: combineReducers({\n    settings: analyticsSettingsReducer,\n    slowlog: slowLogReducer,\n    clusterDetails: clusterDetailsReducer,\n    databaseAnalysis: databaseAnalysisReducer,\n  }),\n  pubsub: pubSubReducer,\n  recommendations: recommendationsReducer,\n  oauth: combineReducers({\n    cloud: appOauthReducer,\n    azure: azureAuthReducer,\n  }),\n  panels: combineReducers({\n    sidePanels: sidePanelsReducer,\n    aiAssistant: aiAssistantReducer,\n  }),\n  rdi: combineReducers({\n    instances: rdiInstancesReducer,\n    pipeline: rdiPipelineReducer,\n    dryRun: rdiDryRunJobReducer,\n    testConnections: rdiTestConnectionsReducer,\n    statistics: rdiStatisticsReducer,\n  }),\n})\n\nconst store = configureStore({\n  reducer: rootReducer,\n  middleware: (getDefaultMiddleware) =>\n    getDefaultMiddleware({ serializableCheck: false }),\n  devTools: riConfig.app.env !== 'production',\n})\n\nconst dispatch = store.dispatch\n\nexport { store, dispatch }\n\nexport type ReduxStore = typeof store\nexport type RootState = ReturnType<typeof rootReducer>\nexport type AppDispatch = typeof store.dispatch\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/analytics/clusterDetails.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  mockedStore,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nimport { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils'\nimport {\n  CLUSTER_DETAILS_DATA_MOCK,\n  INSTANCE_ID_MOCK,\n} from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers'\nimport reducer, {\n  initialState,\n  getClusterDetails,\n  getClusterDetailsSuccess,\n  getClusterDetailsError,\n  clusterDetailsSelector,\n  setClusterDetailsInitialState,\n  fetchClusterDetailsAction,\n} from '../../analytics/clusterDetails'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('clusterDetails slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setUserSettingsInitialState', () => {\n    it('should properly set the initial state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setClusterDetailsInitialState())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { clusterDetails: nextState },\n      })\n      expect(clusterDetailsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getClusterDetails', () => {\n    it('should properly set state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getClusterDetails())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { clusterDetails: nextState },\n      })\n      expect(clusterDetailsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getClusterDetailsSuccess', () => {\n    it('should properly set state after success fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        data: CLUSTER_DETAILS_DATA_MOCK,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getClusterDetailsSuccess(CLUSTER_DETAILS_DATA_MOCK),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { clusterDetails: nextState },\n      })\n      expect(clusterDetailsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getClusterDetailsError', () => {\n    it('should properly set state after failed fetch data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getClusterDetailsError(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { clusterDetails: nextState },\n      })\n      expect(clusterDetailsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('fetchClusterDetailsAction', () => {\n      it('succeed to fetch data', async () => {\n        // Act\n\n        const responsePayload = {\n          status: 200,\n          data: CLUSTER_DETAILS_DATA_MOCK,\n        }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(fetchClusterDetailsAction(INSTANCE_ID_MOCK))\n\n        // Assert\n        const expectedActions = [\n          getClusterDetails(),\n          getClusterDetailsSuccess(CLUSTER_DETAILS_DATA_MOCK),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: DEFAULT_ERROR_MESSAGE },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchClusterDetailsAction(INSTANCE_ID_MOCK))\n\n        // Assert\n        const expectedActions = [\n          getClusterDetails(),\n          addErrorNotification(responsePayload as AxiosError),\n          getClusterDetailsError(DEFAULT_ERROR_MESSAGE),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport reducer, {\n  initialState,\n  setDatabaseAnalysisInitialState,\n  getDBAnalysis,\n  getDBAnalysisSuccess,\n  getDBAnalysisError,\n  loadDBAnalysisReports,\n  loadDBAnalysisReportsSuccess,\n  loadDBAnalysisReportsError,\n  setSelectedAnalysisId,\n  fetchDBAnalysisAction,\n  createNewAnalysis,\n  fetchDBAnalysisReportsHistory,\n  dbAnalysisReportsSelector,\n  dbAnalysisSelector,\n  setShowNoExpiryGroup,\n  setRecommendationVote,\n  setRecommendationVoteSuccess,\n  setRecommendationVoteError,\n  putRecommendationVote,\n} from 'uiSrc/slices/analytics/dbAnalysis'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nlet store: typeof mockedStore\n\nconst mockAnalysis = {\n  id: '071ce08e-00b5-4356-b261-b666f4fbbbd9',\n  databaseId: '528332fb-3fad-4392-a1e3-8eb919a76680',\n  filter: { type: null, match: '*', count: 10000 },\n  delimiter: ':',\n  progress: { total: 14209, scanned: 80000, processed: 14209 },\n  createdAt: new Date('2021-04-22T09:03:56.917Z'),\n  totalKeys: {\n    total: 14209,\n    types: [{ type: 'zset', total: 1 }],\n  },\n  totalMemory: {\n    total: 1026830,\n    types: [{ type: 'zset', total: 96 }],\n  },\n  topKeysNsp: [\n    {\n      nsp: 'delimiter_4',\n      memory: 1399,\n      keys: 7,\n      types: [{ type: 'zset', memory: 96, keys: 1 }],\n    },\n  ],\n  topMemoryNsp: [\n    {\n      nsp: 'delimiter_3',\n      memory: 114768,\n      keys: 1,\n      types: [{ type: 'string', memory: 114768, keys: 1 }],\n    },\n  ],\n  topKeysMemory: [\n    {\n      name: 'delimiter_3:big',\n      type: 'string',\n      memory: 114768,\n      length: 100331,\n      ttl: -1,\n    },\n  ],\n}\n\nconst mockHistoryReport = {\n  id: 'id',\n  created: new Date('2021-04-22T09:03:56.917Z'),\n}\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('db analysis slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n\n    describe('setDatabaseAnalysisInitialState', () => {\n      it('should properly set initial state', () => {\n        const nextState = reducer(\n          initialState,\n          setDatabaseAnalysisInitialState(),\n        )\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(initialState)\n      })\n    })\n\n    describe('setSelectedAnalysisId', () => {\n      it('should properly set payload to selectedAnalysis', () => {\n        const payload = 'id'\n\n        // Arrange\n        const stateHistory = {\n          ...initialState.history,\n          selectedAnalysis: 'id',\n        }\n\n        // Act\n        const nextState = reducer(initialState, setSelectedAnalysisId(payload))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisReportsSelector(rootState)).toEqual(stateHistory)\n      })\n    })\n\n    describe('loadDBAnalysisReportsError', () => {\n      it('should properly set error to history', () => {\n        // Arrange\n        const error = 'Some error'\n        const state = {\n          ...initialState,\n          history: {\n            loading: false,\n            error,\n            data: [],\n            selectedAnalysis: null,\n            showNoExpiryGroup: false,\n          },\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          loadDBAnalysisReportsError(error),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('getDBAnalysisError', () => {\n      it('should properly set error', () => {\n        // Arrange\n        const error = 'Some error'\n        const state = {\n          ...initialState,\n          error,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getDBAnalysisError(error))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('setRecommendationVoteError', () => {\n      it('should properly set error', () => {\n        // Arrange\n        const error = 'Some error'\n        const state = {\n          ...initialState,\n          error,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setRecommendationVoteError(error),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('getDBAnalysis', () => {\n      it('should properly set loading: true', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getDBAnalysis())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('loadDBAnalysisReports', () => {\n      it('should properly set loading: true', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          history: {\n            ...initialState.history,\n            loading: true,\n          },\n        }\n\n        // Act\n        const nextState = reducer(initialState, loadDBAnalysisReports())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('getDBAnalysisSuccess', () => {\n      it('should properly set loading: true', () => {\n        const payload = mockAnalysis\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: false,\n          data: mockAnalysis,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getDBAnalysisSuccess(payload))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('setRecommendationVoteSuccess', () => {\n      it('should properly set data', () => {\n        const payload = mockAnalysis\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: false,\n          data: mockAnalysis,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setRecommendationVoteSuccess(payload),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisSelector(rootState)).toEqual(state)\n      })\n    })\n    describe('loadDBAnalysisReportsSuccess', () => {\n      it('should properly set data to history', () => {\n        const payload = [mockHistoryReport]\n        // Arrange\n        const stateHistory = {\n          ...initialState.history,\n          loading: false,\n          data: [{ id: 'id', created: new Date('2021-04-22T09:03:56.917Z') }],\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          loadDBAnalysisReportsSuccess(payload),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisReportsSelector(rootState)).toEqual(stateHistory)\n      })\n    })\n    describe('setShowNoExpiryGroup', () => {\n      it('should properly set data to history', () => {\n        const payload = true\n        // Arrange\n        const stateHistory = {\n          ...initialState.history,\n          showNoExpiryGroup: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setShowNoExpiryGroup(payload))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          analytics: { databaseAnalysis: nextState },\n        })\n        expect(dbAnalysisReportsSelector(rootState)).toEqual(stateHistory)\n      })\n    })\n  })\n\n  // thunks\n  describe('thunks', () => {\n    describe('fetchDBAnalysisAction', () => {\n      it('succeed to fetch analysis data', async () => {\n        const data = mockAnalysis\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchDBAnalysisAction('instanceId', 'id'))\n\n        // Assert\n        const expectedActions = [getDBAnalysis(), getDBAnalysisSuccess(data)]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchDBAnalysisAction('instanceId', 'id'))\n\n        // Assert\n        const expectedActions = [\n          getDBAnalysis(),\n          addErrorNotification(responsePayload as AxiosError),\n          getDBAnalysisError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n    describe('createNewAnalysis', () => {\n      it('succeed to create new analysis', async () => {\n        const data = mockAnalysis\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadGet = {\n          data: [{ id: data.id, createdAt: data.createdAt }, mockHistoryReport],\n          status: 200,\n        }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayloadGet)\n\n        // Act\n        await store.dispatch<any>(\n          createNewAnalysis('instanceId', ['delimiter']),\n        )\n\n        // Assert\n        const expectedActions = [\n          getDBAnalysis(),\n          getDBAnalysisSuccess(data),\n          loadDBAnalysisReports(),\n          setSelectedAnalysisId(data.id),\n          loadDBAnalysisReportsSuccess([\n            {\n              createdAt: mockAnalysis.createdAt,\n              id: mockAnalysis.id,\n            },\n            mockHistoryReport,\n          ]),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to create new analysis', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createNewAnalysis('instanceId', ['delimiter']),\n        )\n\n        // Assert\n        const expectedActions = [\n          getDBAnalysis(),\n          addErrorNotification(responsePayload as AxiosError),\n          getDBAnalysisError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n    describe('fetchDBAnalysisReportsHistory', () => {\n      it('succeed to fetch analysis reports', async () => {\n        const data = [mockHistoryReport]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchDBAnalysisReportsHistory('instanceId'))\n\n        // Assert\n        const expectedActions = [\n          loadDBAnalysisReports(),\n          loadDBAnalysisReportsSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to create new analysis', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchDBAnalysisReportsHistory('instanceId'))\n\n        // Assert\n        const expectedActions = [\n          loadDBAnalysisReports(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadDBAnalysisReportsError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n    describe('putRecommendationVote', () => {\n      it('succeed to put recommendation vote', async () => {\n        const data = mockAnalysis\n        const responsePayload = { data, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(putRecommendationVote('name', Vote.Like))\n\n        // Assert\n        const expectedActions = [\n          setRecommendationVote(),\n          setRecommendationVoteSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to put recommendation vote', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(putRecommendationVote('name', Vote.Like))\n\n        // Assert\n        const expectedActions = [\n          setRecommendationVote(),\n          addErrorNotification(responsePayload as AxiosError),\n          setRecommendationVoteError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/analytics/settings.spec.ts",
    "content": "import { initialStateDefault } from 'uiSrc/utils/test-utils'\nimport { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics'\n\nimport reducer, {\n  analyticsSettingsSelector,\n  initialState,\n  setAnalyticsViewTab,\n} from '../../analytics/settings'\n\ndescribe('analytics settings slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setAnalyticsViewTab', () => {\n    it('should properly set the AnalyticsViewTab.SlowLog', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        viewTab: AnalyticsViewTab.SlowLog,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setAnalyticsViewTab(AnalyticsViewTab.SlowLog),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { settings: nextState },\n      })\n      expect(analyticsSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { DEFAULT_SLOWLOG_DURATION_UNIT } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { setSlowLogUnits } from 'uiSrc/slices/app/context'\nimport {\n  cleanup,\n  mockedStore,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\n\nimport { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models'\n\nimport reducer, {\n  initialState,\n  getSlowLogConfig,\n  getSlowLogConfigSuccess,\n  getSlowLogConfigError,\n  getSlowLogs,\n  getSlowLogsSuccess,\n  getSlowLogsError,\n  clearSlowLogAction,\n  deleteSlowLogs,\n  deleteSlowLogsError,\n  deleteSlowLogsSuccess,\n  fetchSlowLogsAction,\n  getSlowLogConfigAction,\n  patchSlowLogConfigAction,\n  setSlowLogInitialState,\n  slowLogSelector,\n} from '../../analytics/slowlog'\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slowLog slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setUserSettingsInitialState', () => {\n    it('should properly set the initial state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSlowLogInitialState())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getSlowLogs', () => {\n    it('should properly set state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogs())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getSlowLogsSuccess', () => {\n    it('should properly set state after success fetch data', () => {\n      // Arrange\n      const data: SlowLog[] = [\n        {\n          id: 1,\n          time: 1652265051,\n          durationUs: 199,\n          args: 'SET foo bar',\n          source: '127.17.0.1:46922',\n        },\n      ]\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n        lastRefreshTime: MOCK_TIMESTAMP,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getSlowLogsError', () => {\n    it('should properly set state after failed fetch data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogsError(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSlowLogs', () => {\n    it('should properly set state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogs())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSlowLogsSuccess', () => {\n    it('should properly set state after success fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteSlowLogsSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSlowLogsError', () => {\n    it('should properly set state after failed fetch data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteSlowLogsError(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getSlowLogConfig', () => {\n    it('should properly set state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogConfig())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getSlowLogConfigSuccess', () => {\n    it('should properly set state after success fetch data', () => {\n      // Arrange\n      const config: SlowLogConfig = {\n        slowlogMaxLen: 100,\n        slowlogLogSlowerThan: 300,\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        config,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogConfigSuccess(config))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getSlowLogConfigError', () => {\n    it('should properly set state after failed fetch data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getSlowLogConfigError(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        analytics: { slowlog: nextState },\n      })\n      expect(slowLogSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('fetchSlowLogsAction', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const data: SlowLog[] = [\n          {\n            id: 1,\n            time: 1652265051,\n            durationUs: 199,\n            args: 'SET foo bar',\n            source: '127.17.0.1:46922',\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSlowLogsAction('123', 100))\n\n        // Assert\n        const expectedActions = [getSlowLogs(), getSlowLogsSuccess(data)]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSlowLogsAction('123', 100))\n\n        // Assert\n        const expectedActions = [\n          getSlowLogs(),\n          addErrorNotification(responsePayload as AxiosError),\n          getSlowLogsError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('clearSlowLogAction', () => {\n      it('succeed to fetch data', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(clearSlowLogAction('123'))\n\n        // Assert\n        const expectedActions = [deleteSlowLogs(), deleteSlowLogsSuccess()]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(clearSlowLogAction('123'))\n\n        // Assert\n        const expectedActions = [\n          deleteSlowLogs(),\n          addErrorNotification(responsePayload as AxiosError),\n          deleteSlowLogsError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('getSlowLogConfigAction', () => {\n      it('succeed to fetch data', async () => {\n        const data = {\n          slowlogMaxLen: 100,\n          slowlogLogSlowerThan: 1200,\n        }\n        const responsePayload = { status: 200, data }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getSlowLogConfigAction('123'))\n\n        // Assert\n        const expectedActions = [\n          getSlowLogConfig(),\n          getSlowLogConfigSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getSlowLogConfigAction('123'))\n\n        // Assert\n        const expectedActions = [\n          getSlowLogConfig(),\n          addErrorNotification(responsePayload as AxiosError),\n          getSlowLogConfigError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('patchSlowLogConfigAction', () => {\n      it('succeed to fetch data', async () => {\n        const data = {\n          slowlogMaxLen: 100,\n          slowlogLogSlowerThan: 1200,\n        }\n        const config = {\n          ...data,\n        }\n        const responsePayload = { status: 200, data }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          patchSlowLogConfigAction(\n            '123',\n            config,\n            DEFAULT_SLOWLOG_DURATION_UNIT,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          getSlowLogConfig(),\n          getSlowLogConfigSuccess(data),\n          setSlowLogUnits(DEFAULT_SLOWLOG_DURATION_UNIT),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          patchSlowLogConfigAction(\n            '123',\n            {\n              slowlogMaxLen: 100,\n              slowlogLogSlowerThan: 1200,\n            },\n            DEFAULT_SLOWLOG_DURATION_UNIT,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          getSlowLogConfig(),\n          addErrorNotification(responsePayload as AxiosError),\n          getSlowLogConfigError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/connectivity.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { http, HttpResponse } from 'msw'\nimport { waitFor } from '@testing-library/react'\nimport {\n  cleanup,\n  getMswURL,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { store } from 'uiSrc/slices/store'\nimport reducer, {\n  appConnectivity,\n  appConnectivityError,\n  initialState,\n  retryConnection,\n  setConnectivityError,\n  setConnectivityLoading,\n} from 'uiSrc/slices/app/connectivity'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport {\n  getDatabaseConfigInfo,\n  getDatabaseConfigInfoFailure,\n  getDatabaseConfigInfoSuccess,\n} from 'uiSrc/slices/instances/instances'\nimport { INSTANCES_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\n\nlet testStore: typeof mockedStore\nconst onSuccessAction = jest.fn()\nconst onFailAction = jest.fn()\n\nbeforeEach(() => {\n  cleanup()\n  jest.resetAllMocks()\n  testStore = cloneDeep(mockedStore)\n  testStore.clearActions()\n})\n\ndescribe('app connectivity slice', () => {\n  describe('setConnectivityLoading reducer', () => {\n    it('should set loading to true', () => {\n      const nextState = reducer(initialState, setConnectivityLoading(true))\n      const expectedState = {\n        loading: true,\n        error: undefined,\n      }\n      expect(nextState).toEqual(expectedState)\n    })\n\n    it('should set loading to false', () => {\n      const nextState = reducer(initialState, setConnectivityLoading(false))\n      const expectedState = {\n        loading: false,\n        error: undefined,\n      }\n      expect(nextState).toEqual(expectedState)\n    })\n  })\n\n  describe('setConnectivityError reducer', () => {\n    it('should set connectivity error to a value', () => {\n      const nextState = reducer(\n        initialState,\n        setConnectivityError('Test error message'),\n      )\n      const expectedState = {\n        loading: false,\n        error: 'Test error message',\n      }\n      expect(nextState).toEqual(expectedState)\n    })\n\n    it('should set connectivity error to null', () => {\n      const nextState = reducer(initialState, setConnectivityError(null))\n      const expectedState = {\n        loading: false,\n        error: null,\n      }\n      expect(nextState).toEqual(expectedState)\n    })\n  })\n\n  describe('retryConnection', () => {\n    it('should handle success path', async () => {\n      // Set connected instance for interceptor to work\n      testStore.getState().connections.instances.connectedInstance = {\n        ...INSTANCES_MOCK[0],\n        id: '123', // Match the test database ID\n      }\n\n      const getDbOverviewMock = jest.fn(() => {\n        return HttpResponse.json({})\n      })\n      mswServer.use(\n        http.get(\n          getMswURL(`${ApiEndpoints.DATABASES}/123/overview`),\n          getDbOverviewMock,\n        ),\n      )\n\n      testStore.dispatch<any>(\n        retryConnection('123', onSuccessAction, onFailAction),\n      )\n\n      await waitFor(() => {\n        expect(getDbOverviewMock).toHaveBeenCalledTimes(1)\n\n        const expectedActions = [\n          setConnectivityLoading(true),\n          getDatabaseConfigInfo(),\n          getDatabaseConfigInfoSuccess({}),\n          setConnectivityError(null),\n          setConnectivityLoading(false),\n        ]\n\n        expect(testStore.getActions()).toEqual(expectedActions)\n      })\n\n      expect(onSuccessAction).toHaveBeenCalledTimes(1)\n      expect(onFailAction).not.toHaveBeenCalled()\n    })\n\n    it('should handle failure path', async () => {\n      // Set connected instance for interceptor to work\n      testStore.getState().connections.instances.connectedInstance = {\n        ...INSTANCES_MOCK[0],\n        id: '123', // Match the test database ID\n      }\n\n      jest.spyOn(store, 'dispatch').mockImplementation((action: any) => {\n        testStore.dispatch(action)\n      })\n      const getDbOverviewMock = jest.fn(() => {\n        return HttpResponse.json(\n          { code: 'serviceUnavailable', message: 'Test error' },\n          { status: 503 },\n        )\n      })\n      mswServer.use(\n        http.get(\n          getMswURL(`${ApiEndpoints.DATABASES}/123/overview`),\n          getDbOverviewMock,\n        ),\n      )\n\n      testStore.dispatch<any>(\n        retryConnection('123', onSuccessAction, onFailAction),\n      )\n\n      await waitFor(() => {\n        expect(getDbOverviewMock).toHaveBeenCalledTimes(1)\n\n        const expectedActions = [\n          setConnectivityLoading(true),\n          getDatabaseConfigInfo(),\n          setConnectivityError('The connection to the server has been lost.'),\n          getDatabaseConfigInfoFailure('Test error'),\n          setConnectivityLoading(false),\n        ]\n\n        expect(testStore.getActions()).toEqual(expectedActions)\n      })\n\n      expect(onSuccessAction).not.toHaveBeenCalled()\n      expect(onFailAction).toHaveBeenCalledTimes(1)\n    })\n\n    it('should not dispatch connectivity error when instance ID does not match', async () => {\n      // Set connected instance with different ID than the request\n      testStore.getState().connections.instances.connectedInstance = {\n        ...INSTANCES_MOCK[0],\n        id: 'different-instance-id', // Different from request URL\n      }\n\n      jest.spyOn(store, 'dispatch').mockImplementation((action: any) => {\n        testStore.dispatch(action)\n      })\n\n      const getDbOverviewMock = jest.fn(async () => {\n        return HttpResponse.json(\n          { code: 'serviceUnavailable', message: 'Test error' },\n          { status: 503 },\n        )\n      })\n      mswServer.use(\n        http.get(\n          getMswURL(`${ApiEndpoints.DATABASES}/123/overview`), // ID 123 in URL\n          getDbOverviewMock,\n        ),\n      )\n\n      testStore.dispatch<any>(\n        retryConnection('123', onSuccessAction, onFailAction),\n      )\n\n      await waitFor(() => {\n        expect(getDbOverviewMock).toHaveBeenCalledTimes(1)\n\n        const expectedActions = [\n          setConnectivityLoading(true),\n          getDatabaseConfigInfo(),\n          // Note: NO setConnectivityError action should be dispatched\n          getDatabaseConfigInfoFailure('Test error'),\n          setConnectivityLoading(false),\n        ]\n\n        expect(testStore.getActions()).toEqual(expectedActions)\n      })\n\n      expect(onSuccessAction).not.toHaveBeenCalled()\n      expect(onFailAction).toHaveBeenCalledTimes(1)\n    })\n  })\n\n  describe('selectors', () => {\n    it('should get state after error is set', async () => {\n      testStore.dispatch<any>(setConnectivityError('Test error'))\n      testStore.dispatch<any>(setConnectivityLoading(true))\n\n      const expectedState = { error: 'Test error', loading: true }\n      const rootState = Object.assign(initialStateDefault, {\n        app: { connectivity: expectedState },\n      })\n\n      await waitFor(() => {\n        const actualValue = appConnectivity(rootState)\n        expect(actualValue).toStrictEqual(expectedState)\n        expect(appConnectivityError(rootState)).toStrictEqual(\n          expectedState.error,\n        )\n      })\n    })\n\n    it('should get state after error is cleared', async () => {\n      testStore.dispatch<any>(setConnectivityError('Test error'))\n      testStore.dispatch<any>(setConnectivityLoading(true))\n      testStore.dispatch<any>(setConnectivityError(null))\n      testStore.dispatch<any>(setConnectivityLoading(false))\n\n      const expectedState = { error: null, loading: false }\n      const rootState = Object.assign(initialStateDefault, {\n        app: { connectivity: expectedState },\n      })\n\n      await waitFor(() => {\n        const actualValue = appConnectivity(rootState)\n        expect(actualValue).toStrictEqual(expectedState)\n        expect(appConnectivityError(rootState)).toStrictEqual(\n          expectedState.error,\n        )\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/context.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { KeyTypes, SortOrder } from 'uiSrc/constants'\nimport { stringToBuffer } from 'uiSrc/utils'\n\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  setAppContextInitialState,\n  setAppContextConnectedInstanceId,\n  setAppContextConnectedRdiInstanceId,\n  setBrowserPatternKeyListDataLoaded,\n  setBrowserRedisearchKeyListDataLoaded,\n  setBrowserSelectedKey,\n  setBrowserPatternScrollPosition,\n  setBrowserPanelSizes,\n  setBrowserTreeSort,\n  setWorkbenchScript,\n  setWorkbenchVerticalPanelSizes,\n  setLastPageContext,\n  appContextSelector,\n  appContextBrowser,\n  appContextWorkbench,\n  setBrowserTreeNodesOpen,\n  resetBrowserTree,\n  appContextBrowserTree,\n  setBrowserTreeDelimiter,\n  setBrowserIsNotRendered,\n  setBrowserRedisearchScrollPosition,\n  updateKeyDetailsSizes,\n  appContextBrowserKeyDetails,\n  appContextDbConfig,\n  setSlowLogUnits,\n  setDbConfig,\n  setDbIndexState,\n  appContextDbIndex,\n  setRecommendationsShowHidden,\n  appContextCapability,\n  setCapability,\n  setPipelineDialogState,\n  setLastPipelineManagementPage,\n  resetPipelineManagement,\n  appContextPipelineManagement,\n} from '../../app/context'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('setAppContextInitialState', () => {\n    it('should properly set initial state', () => {\n      const nextState = reducer(initialState, setAppContextInitialState())\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n      expect(appContextSelector(rootState)).toEqual(initialState)\n    })\n\n    it('should properly set initial state with existing contextId and capability and contextRdiInstanceId', () => {\n      // Arrange\n      const contextInstanceId = '12312-3123'\n      const contextRdiInstanceId = 'rdi-123'\n      const capability = { source: '123123' }\n      const prevState = {\n        ...initialState,\n        contextInstanceId,\n        contextRdiInstanceId,\n        capability,\n        browser: {\n          ...initialState.browser,\n          keyList: {\n            ...initialState.browser.keyList,\n            isDataLoaded: true,\n            scrollTopPosition: 100,\n            selectedKey: stringToBuffer('some key'),\n          },\n          tree: {\n            ...initialState.browser.tree,\n            delimiter: '-',\n          },\n          bulkActions: {\n            ...initialState.browser.bulkActions,\n            opened: true,\n          },\n        },\n        workbench: {\n          ...initialState.workbench,\n          script: '123123',\n        },\n        pubsub: {\n          ...initialState.pubsub,\n          channel: '123123',\n          message: '123123',\n        },\n        analytics: {\n          ...initialState.analytics,\n          lastViewedPage: 'zxczxc',\n        },\n      }\n      const state = {\n        ...initialState,\n        contextInstanceId,\n        contextRdiInstanceId,\n        capability,\n      }\n\n      // Act\n      const nextState = reducer(prevState, setAppContextInitialState())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setAppContextConnectedInstanceId', () => {\n    it('should properly set id', () => {\n      // Arrange\n      const contextInstanceId = '12312-3123'\n      const state = {\n        ...initialState,\n        contextInstanceId,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setAppContextConnectedInstanceId(contextInstanceId),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setAppContextConnectedRdiInstanceId', () => {\n    it('should properly set id', () => {\n      // Arrange\n      const contextRdiInstanceId = 'rdi-123'\n      const state = {\n        ...initialState,\n        contextRdiInstanceId,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setAppContextConnectedRdiInstanceId(contextRdiInstanceId),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserPatternKeyListDataLoaded', () => {\n    it('should properly set context is data loaded', () => {\n      // Arrange\n      const isDataPatternLoaded = true\n      const state = {\n        ...initialState.browser,\n        keyList: {\n          ...initialState.browser.keyList,\n          isDataPatternLoaded,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserPatternKeyListDataLoaded(isDataPatternLoaded),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserRedisearchKeyListDataLoaded', () => {\n    it('should properly set context is data loaded', () => {\n      // Arrange\n      const isDataRedisearchLoaded = true\n      const state = {\n        ...initialState.browser,\n        keyList: {\n          ...initialState.browser.keyList,\n          isDataRedisearchLoaded,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserRedisearchKeyListDataLoaded(isDataRedisearchLoaded),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserSelectedKey', () => {\n    it('should properly set selectedKey', () => {\n      // Arrange\n      const selectedKey = stringToBuffer('nameOfKey')\n      const state = {\n        ...initialState.browser,\n        keyList: {\n          ...initialState.browser.keyList,\n          selectedKey,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserSelectedKey(selectedKey),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserPatternScrollPosition', () => {\n    it('should properly set scroll position of keyList', () => {\n      // Arrange\n      const scrollPatternTopPosition = 530\n      const state = {\n        ...initialState.browser,\n        keyList: {\n          ...initialState.browser.keyList,\n          scrollPatternTopPosition,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserPatternScrollPosition(scrollPatternTopPosition),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserRedisearchScrollPosition', () => {\n    it('should properly set scroll position of keyList', () => {\n      // Arrange\n      const scrollRedisearchTopPosition = 530\n      const state = {\n        ...initialState.browser,\n        keyList: {\n          ...initialState.browser.keyList,\n          scrollRedisearchTopPosition,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserRedisearchScrollPosition(scrollRedisearchTopPosition),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserPanelSizes', () => {\n    it('should properly set browser panel widths', () => {\n      // Arrange\n      const panelSizes = {\n        first: 100,\n        second: 200,\n      }\n      const state = {\n        ...initialState.browser,\n        panelSizes,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setBrowserPanelSizes(panelSizes))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setWorkbenchScript', () => {\n    it('should properly set workbench script', () => {\n      // Arrange\n      const script = 'set 1 1 // 215 hset 5 21'\n      const state = {\n        ...initialState.workbench,\n        script,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setWorkbenchScript(script))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextWorkbench(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setWorkbenchVerticalPanelSizes', () => {\n    it('should properly set wb panel sizes', () => {\n      // Arrange\n      const panelSizes = [100, 200]\n      const state = {\n        ...initialState.workbench,\n        panelSizes,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setWorkbenchVerticalPanelSizes(panelSizes),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextWorkbench(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setLastPageContext', () => {\n    it('should properly set last page', () => {\n      // Arrange\n      const lastPage = 'workbench'\n      const state = {\n        ...initialState,\n        lastPage,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setLastPageContext(lastPage))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserTreeNodesOpen', () => {\n    it('should properly set open nodes in the tree', () => {\n      // Arrange\n      const openNodes = {\n        '1o2313': true,\n        eu12313: false,\n      }\n      const prevState = {\n        ...initialState,\n        browser: {\n          ...initialState.browser,\n          tree: {\n            ...initialState.browser.tree,\n            openNodes,\n          },\n        },\n      }\n\n      const state = {\n        ...initialState.browser.tree,\n        openNodes,\n      }\n\n      // Act\n      const nextState = reducer(prevState, setBrowserTreeNodesOpen(openNodes))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowserTree(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserIsNotRendered', () => {\n    it('should properly set browser is not rendered value', () => {\n      // Arrange\n      const isNotRendered = false\n      const state = {\n        ...initialState.browser,\n        keyList: {\n          ...initialState.browser.keyList,\n          isNotRendered,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserIsNotRendered(isNotRendered),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowser(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setDbConfig', () => {\n    it('should properly set db config', () => {\n      // Arrange\n      const data = {\n        slowLogDurationUnit: 'msec',\n        treeViewDelimiter: [{ label: ':-' }],\n        treeViewSort: SortOrder.DESC,\n        showHiddenRecommendations: true,\n      }\n\n      const state = {\n        ...initialState.dbConfig,\n        ...data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setDbConfig(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextDbConfig(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSlowLogUnits', () => {\n    it('should properly set slow log units', () => {\n      // Arrange\n      const slowLogDurationUnit = 'msec'\n\n      const state = {\n        ...initialState.dbConfig,\n        slowLogDurationUnit,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setSlowLogUnits(slowLogDurationUnit),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextDbConfig(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserTreeDelimiter', () => {\n    it('should properly set browser tree delimiter', () => {\n      // Arrange\n      const delimiter = [{ label: '_' }]\n\n      const state = {\n        ...initialState.dbConfig,\n        treeViewDelimiter: delimiter,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setBrowserTreeDelimiter(delimiter),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextDbConfig(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setBrowserTreeSort', () => {\n    it('should properly set browser tree sorting', () => {\n      // Arrange\n      const sorting = SortOrder.DESC\n\n      const state = {\n        ...initialState.dbConfig,\n        treeViewSort: sorting,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setBrowserTreeSort(sorting))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextDbConfig(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setRecommendationsShowHidden', () => {\n    it('should properly set is show hidden live recommendations', () => {\n      // Arrange\n      const value = true\n\n      const state = {\n        ...initialState.dbConfig,\n        showHiddenRecommendations: value,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setRecommendationsShowHidden(value),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextDbConfig(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetBrowserTree', () => {\n    it('should properly set last page', () => {\n      // Arrange\n      const prevState = {\n        ...initialState,\n        browser: {\n          ...initialState.browser,\n          tree: {\n            ...initialState.browser.tree,\n            openNodes: {\n              test: true,\n            },\n            selectedLeaf: 'test',\n          },\n        },\n      }\n      const state = {\n        ...initialState.browser.tree,\n        openNodes: {},\n        selectedLeaf: null,\n      }\n\n      // Act\n      const nextState = reducer(prevState, resetBrowserTree())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowserTree(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateKeyDetailsSizes', () => {\n    it('should properly update sizes', () => {\n      // Arrange\n      const payload = {\n        type: KeyTypes.Hash,\n        sizes: {\n          field: 50,\n        },\n      }\n\n      const state = {\n        ...initialState.browser.keyDetailsSizes,\n        [KeyTypes.Hash]: { ...payload.sizes },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateKeyDetailsSizes(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextBrowserKeyDetails(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setDbIndexState', () => {\n    it('should properly set state for db index', () => {\n      // Arrange\n      const state = {\n        ...initialState.dbIndex,\n        disabled: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setDbIndexState(true))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextDbIndex(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setCapability', () => {\n    it('should properly set db config', () => {\n      // Arrange\n      const data = {\n        source: '123123',\n        tutorialPopoverShown: false,\n      }\n\n      const state = {\n        ...initialState.capability,\n        source: data.source,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setCapability(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextCapability(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setPipelineDialogState', () => {\n    it('should properly set pipeline dialog state', () => {\n      // Arrange\n      const state = {\n        ...initialState.pipelineManagement,\n        isOpenDialog: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setPipelineDialogState(false))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextPipelineManagement(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setLastPipelineManagementPage', () => {\n    it('should properly set last viewed page', () => {\n      // Arrange\n      const mockLastPage = 'name'\n      const state = {\n        ...initialState.pipelineManagement,\n        lastViewedPage: mockLastPage,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setLastPipelineManagementPage(mockLastPage),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextPipelineManagement(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetPipelineManagement', () => {\n    it('should properly set last page', () => {\n      // Arrange\n      const prevState = {\n        ...initialState,\n        pipelineManagement: {\n          lastViewedPage: 'some value',\n          isOpenDialog: false,\n        },\n      }\n      const state = {\n        lastViewedPage: '',\n        isOpenDialog: false,\n      }\n\n      // Act\n      const nextState = reducer(prevState, resetPipelineManagement())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { context: nextState },\n      })\n\n      expect(appContextPipelineManagement(rootState)).toEqual(state)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/csrf.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport reducer, {\n  appCsrfSelector,\n  fetchCsrfTokenAction,\n  initialState,\n  fetchCsrfToken,\n  fetchCsrfTokenSuccess,\n  fetchCsrfTokenFail,\n} from 'uiSrc/slices/app/csrf'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { apiService } from 'uiSrc/services'\nimport { getConfig } from 'uiSrc/config'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst riConfig = getConfig()\n\ndescribe('slices', () => {\n  const OLD_ENV_CONFIG = cloneDeep(riConfig)\n  beforeEach(() => {\n    riConfig.api.csrfEndpoint = OLD_ENV_CONFIG.api.csrfEndpoint\n  })\n  afterAll(() => {\n    riConfig.api.csrfEndpoint = OLD_ENV_CONFIG.api.csrfEndpoint\n  })\n\n  it('fetch token reducer should properly set the token', () => {\n    const nextState = reducer(initialState, fetchCsrfToken())\n\n    const newState = {\n      ...initialStateDefault,\n      app: {\n        ...initialStateDefault.app,\n        csrf: nextState,\n      },\n    }\n\n    expect(appCsrfSelector(newState).loading).toEqual(true)\n  })\n\n  it('fetch token success reducer should properly set the state', () => {\n    const nextState = reducer(\n      initialState,\n      fetchCsrfTokenSuccess({ token: 'xyz-456' }),\n    )\n\n    const newState = {\n      ...initialStateDefault,\n      app: {\n        ...initialStateDefault.app,\n        csrf: nextState,\n      },\n    }\n\n    expect(appCsrfSelector(newState).token).toEqual('xyz-456')\n    expect(appCsrfSelector(newState).loading).toEqual(false)\n  })\n\n  it('fetch token failure reducer should properly set the state', () => {\n    const nextState = reducer(\n      initialState,\n      fetchCsrfTokenFail({ error: 'something went wrong' }),\n    )\n\n    const newState = {\n      ...initialStateDefault,\n      app: {\n        ...initialStateDefault.app,\n        csrf: nextState,\n      },\n    }\n\n    expect(appCsrfSelector(newState).token).toEqual('')\n    expect(appCsrfSelector(newState).loading).toEqual(false)\n    expect(appCsrfSelector(newState).error).toEqual('something went wrong')\n  })\n\n  it('fetchCsrfToken should fetch the token', async () => {\n    riConfig.api.csrfEndpoint = 'http://localhost'\n\n    apiService.get = jest.fn().mockResolvedValueOnce({\n      data: {\n        token: 'xyz-456',\n      },\n    })\n    const successFn = jest.fn()\n    const failFn = jest.fn()\n    await store.dispatch<any>(fetchCsrfTokenAction(successFn, failFn))\n\n    expect(store.getActions()).toEqual([\n      fetchCsrfToken(),\n      fetchCsrfTokenSuccess({ token: 'xyz-456' }),\n    ])\n    expect(successFn).toHaveBeenCalledWith({ token: 'xyz-456' })\n    expect(failFn).not.toHaveBeenCalled()\n  })\n\n  it('fetchCsrfToken should handle failure', async () => {\n    riConfig.api.csrfEndpoint = 'http://localhost'\n\n    apiService.get = jest\n      .fn()\n      .mockRejectedValueOnce(new Error('something went wrong'))\n    const successFn = jest.fn()\n    const failFn = jest.fn()\n    await store.dispatch<any>(fetchCsrfTokenAction(successFn, failFn))\n\n    expect(store.getActions()).toEqual([\n      fetchCsrfToken(),\n      fetchCsrfTokenFail({ error: 'something went wrong' }),\n    ])\n    expect(successFn).not.toHaveBeenCalled()\n    expect(failFn).toHaveBeenCalled()\n  })\n\n  it('fetchCsrfToken should not fetch the token when endpoint is not provided', async () => {\n    await store.dispatch<any>(fetchCsrfTokenAction())\n\n    expect(store.getActions()).toEqual([])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/db-settings.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport reducer, {\n  initialState,\n  fetchDBSettings,\n  getDBSettingsSuccess,\n  getDBSettingsFailure,\n  getDBSettings,\n  appDBSettingsSelector,\n} from 'uiSrc/slices/app/db-settings'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { apiService } from 'uiSrc/services'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('DB Settings slices', () => {\n  beforeEach(() => {})\n  afterAll(() => {})\n\n  it('get db settings reducer should properly set the loading state', () => {\n    const nextState = reducer(initialState, getDBSettings())\n\n    const newState = {\n      ...initialStateDefault,\n      app: {\n        ...initialStateDefault.app,\n        dbSettings: nextState,\n      },\n    }\n\n    expect(appDBSettingsSelector(newState).loading).toEqual(true)\n  })\n\n  it('get db settings success reducer should properly set the state', () => {\n    const nextState = reducer(\n      initialState,\n      getDBSettingsSuccess({ data: { key: 'test' }, id: 'testDb' }),\n    )\n\n    const newState = {\n      ...initialStateDefault,\n      app: {\n        ...initialStateDefault.app,\n        dbSettings: nextState,\n      },\n    }\n\n    expect(appDBSettingsSelector(newState).data).toEqual({\n      testDb: {\n        key: 'test',\n      },\n    })\n    expect(appDBSettingsSelector(newState).loading).toEqual(false)\n  })\n\n  it('get db settings failure reducer should properly set the state', () => {\n    const nextState = reducer(\n      initialState,\n      getDBSettingsFailure({ error: 'something went wrong' }),\n    )\n\n    const newState = {\n      ...initialStateDefault,\n      app: {\n        ...initialStateDefault.app,\n        dbSettings: nextState,\n      },\n    }\n\n    expect(appDBSettingsSelector(newState).data).toEqual({})\n    expect(appDBSettingsSelector(newState).loading).toEqual(false)\n    expect(appDBSettingsSelector(newState).error).toEqual({\n      error: 'something went wrong',\n    })\n  })\n\n  it('fetchDBSettings should fetch database settings', async () => {\n    apiService.get = jest.fn().mockResolvedValueOnce({\n      status: 200,\n      data: {\n        data: {\n          key: 'test',\n        },\n      },\n    })\n    const successFn = jest.fn()\n    const failFn = jest.fn()\n    await store.dispatch<any>(fetchDBSettings('testDb', successFn, failFn))\n\n    expect(store.getActions()).toEqual([\n      getDBSettings(),\n      getDBSettingsSuccess({\n        data: { key: 'test' },\n        id: 'testDb',\n      }),\n    ])\n    expect(successFn).toHaveBeenCalledWith({\n      data: { key: 'test' },\n      id: 'testDb',\n    })\n    expect(failFn).not.toHaveBeenCalled()\n  })\n\n  it('fetchDbSettings should handle generic failure', async () => {\n    apiService.get = jest.fn().mockRejectedValueOnce(new Error())\n    const successFn = jest.fn()\n    const failFn = jest.fn()\n    await store.dispatch<any>(fetchDBSettings('testDb', successFn, failFn))\n\n    expect(store.getActions()).toEqual([\n      getDBSettings(),\n      getDBSettingsFailure('Something was wrong!'),\n    ])\n    expect(successFn).not.toHaveBeenCalled()\n    expect(failFn).toHaveBeenCalled()\n  })\n\n  it('fetchDbSettings should handle axios failure', async () => {\n    apiService.get = jest.fn().mockRejectedValueOnce({\n      response: { data: { message: 'something went wrong' } },\n    })\n    const successFn = jest.fn()\n    const failFn = jest.fn()\n    await store.dispatch<any>(fetchDBSettings('testDb', successFn, failFn))\n\n    expect(store.getActions()).toEqual([\n      getDBSettings(),\n      getDBSettingsFailure('something went wrong'),\n    ])\n    expect(successFn).not.toHaveBeenCalled()\n    expect(failFn).toHaveBeenCalled()\n  })\n\n  it('fetchDbSettings should handle missing db id failure', async () => {\n    const successFn = jest.fn()\n    const failFn = jest.fn()\n    await store.dispatch<any>(fetchDBSettings('', successFn, failFn))\n\n    expect(store.getActions()).toEqual([\n      getDBSettings(),\n      getDBSettingsFailure('DB not connected'),\n    ])\n    expect(successFn).not.toHaveBeenCalled()\n    expect(failFn).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/features.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport reducer, {\n  initialState,\n  setFeaturesInitialState,\n  appFeatureSelector,\n  setFeaturesToHighlight,\n  removeFeatureFromHighlighting,\n  setOnboarding,\n  skipOnboarding,\n  setOnboardPrevStep,\n  setOnboardNextStep,\n  incrementOnboardStepAction,\n  getFeatureFlags,\n  getFeatureFlagsSuccess,\n  getFeatureFlagsFailure,\n  fetchFeatureFlags,\n  isAzureEntraIdEnabledSelector,\n} from 'uiSrc/slices/app/features'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport {\n  cleanup,\n  initialStateDefault,\n  MOCKED_HIGHLIGHTING_FEATURES,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { apiService } from 'uiSrc/services'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockFeatures = MOCKED_HIGHLIGHTING_FEATURES\ndescribe('slices', () => {\n  describe('setFeaturesInitialState', () => {\n    it('should properly set initial state', () => {\n      const nextState = reducer(initialState, setFeaturesInitialState())\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n      expect(appFeatureSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('setFeaturesToHighlight', () => {\n    it('should properly set features to highlight', () => {\n      const payload = {\n        features: mockFeatures,\n        version: '2.0.0',\n      }\n      const state = {\n        ...initialState,\n        highlighting: {\n          ...initialState.highlighting,\n          features: payload.features,\n          version: payload.version,\n          pages: {\n            browser: payload.features,\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setFeaturesToHighlight(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeFeatureFromHighlighting', () => {\n    it('should properly remove feature to highlight', () => {\n      const prevState = {\n        ...initialState,\n        highlighting: {\n          ...initialState.highlighting,\n          features: mockFeatures,\n          version: '2.0.0',\n          pages: {\n            browser: mockFeatures,\n          },\n        },\n      }\n\n      const payload = mockFeatures[0]\n      const state = {\n        ...prevState,\n        highlighting: {\n          ...prevState.highlighting,\n          features: [mockFeatures[1]],\n          pages: {\n            browser: [mockFeatures[1]],\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        prevState,\n        removeFeatureFromHighlighting(payload),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setOnboarding', () => {\n    it('should properly set onboarding', () => {\n      const payload = {\n        currentStep: 0,\n        totalSteps: 14,\n      }\n      const state = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          currentStep: 0,\n          totalSteps: 14,\n          isActive: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setOnboarding(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n\n    it('should not set onboarding when currenStep > totalSteps', () => {\n      const payload = {\n        currentStep: 5,\n        totalSteps: 4,\n      }\n      const state = {\n        ...initialState,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setOnboarding(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('skipOnboarding', () => {\n    it('should properly set state', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: true,\n        },\n      }\n\n      const state = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, skipOnboarding())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setOnboardPrevStep', () => {\n    it('should properly set state', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: true,\n          currentStep: 3,\n          totalSteps: 10,\n        },\n      }\n\n      const state = {\n        ...currenState,\n        onboarding: {\n          ...currenState.onboarding,\n          currentStep: 2,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, setOnboardPrevStep())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set state with isActive = false', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: false,\n          currentStep: 3,\n          totalSteps: 10,\n        },\n      }\n\n      const state = {\n        ...currenState,\n        onboarding: {\n          ...currenState.onboarding,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, setOnboardPrevStep())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set state with currentStep === 0', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: true,\n          currentStep: 0,\n          totalSteps: 10,\n        },\n      }\n\n      const state = {\n        ...currenState,\n        onboarding: {\n          ...currenState.onboarding,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, setOnboardPrevStep())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setOnboardNextStep', () => {\n    it('should properly set state', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: true,\n          currentStep: 3,\n          totalSteps: 10,\n        },\n      }\n\n      const state = {\n        ...currenState,\n        onboarding: {\n          ...currenState.onboarding,\n          currentStep: 4,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, setOnboardNextStep())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set state with isActive = false', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: false,\n          currentStep: 3,\n          totalSteps: 10,\n        },\n      }\n\n      const state = {\n        ...currenState,\n        onboarding: {\n          ...currenState.onboarding,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, setOnboardNextStep())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set state with currentStep === totalSteps', () => {\n      const currenState = {\n        ...initialState,\n        onboarding: {\n          ...initialState.onboarding,\n          isActive: true,\n          currentStep: 10,\n          totalSteps: 10,\n        },\n      }\n\n      const state = {\n        ...currenState,\n        onboarding: {\n          ...currenState.onboarding,\n          currentStep: 11,\n          isActive: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currenState, setOnboardNextStep())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getFeatureFlags', () => {\n    it('should properly set state', () => {\n      const state = {\n        ...initialState,\n        featureFlags: {\n          ...initialState.featureFlags,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getFeatureFlags())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getFeatureFlagsSuccess', () => {\n    it('should properly set state', () => {\n      const payload = {\n        features: {\n          insightsRecommendations: {\n            flag: true,\n          },\n        },\n      }\n      const state = {\n        ...initialState,\n        featureFlags: {\n          ...initialState.featureFlags,\n          features: payload.features,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getFeatureFlagsSuccess(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getFeatureFlagsFailure', () => {\n    it('should properly set state', () => {\n      const currentState = {\n        ...initialState,\n        featureFlags: {\n          ...initialState.featureFlags,\n          loading: true,\n        },\n      }\n\n      const state = {\n        ...initialState,\n        featureFlags: {\n          ...initialState.featureFlags,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState, getFeatureFlagsFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { features: nextState },\n      })\n\n      expect(appFeatureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n  describe('incrementOnboardStepAction', () => {\n    it('should call setOnboardNextStep', async () => {\n      // Act\n      const nextState = Object.assign(initialStateDefault, {\n        app: {\n          features: {\n            ...initialState,\n            onboarding: {\n              isActive: true,\n              currentStep: 3,\n              totalSteps: 10,\n            },\n          },\n        },\n      })\n      const mockedStore = mockStore(nextState)\n      await mockedStore.dispatch<any>(incrementOnboardStepAction(3))\n      // Assert\n      const expectedActions = [setOnboardNextStep(0)]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n    it('should not call setOnboardNextStep with isActive == false', async () => {\n      // Act\n      const nextState = Object.assign(initialStateDefault, {\n        app: {\n          features: {\n            ...initialState,\n            onboarding: {\n              isActive: false,\n              currentStep: 3,\n              totalSteps: 10,\n            },\n          },\n        },\n      })\n      const mockedStore = mockStore(nextState)\n      await mockedStore.dispatch<any>(incrementOnboardStepAction(3))\n\n      expect(mockedStore.getActions()).toEqual([])\n    })\n\n    it('should not call setOnboardNextStep with different step', async () => {\n      // Act\n      const nextState = Object.assign(initialStateDefault, {\n        app: {\n          features: {\n            ...initialState,\n            onboarding: {\n              isActive: true,\n              currentStep: 4,\n              totalSteps: 10,\n            },\n          },\n        },\n      })\n      const mockedStore = mockStore(nextState)\n      await mockedStore.dispatch<any>(incrementOnboardStepAction(5))\n\n      expect(mockedStore.getActions()).toEqual([])\n    })\n  })\n\n  describe('fetchFeatureFlags', () => {\n    it('succeed to fetch data', async () => {\n      // Arrange\n      const data = { features: { insightsRecommendations: true } }\n      const responsePayload = { data, status: 200 }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchFeatureFlags())\n\n      // Assert\n      const expectedActions = [getFeatureFlags(), getFeatureFlagsSuccess(data)]\n\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch data', async () => {\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchFeatureFlags())\n\n      // Assert\n      const expectedActions = [getFeatureFlags(), getFeatureFlagsFailure()]\n\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('isAzureEntraIdEnabledSelector', () => {\n    const createRootState = (features: Record<string, { flag: boolean }>) => ({\n      ...initialStateDefault,\n      app: {\n        features: {\n          ...initialState,\n          featureFlags: {\n            ...initialState.featureFlags,\n            features,\n          },\n        },\n      },\n    })\n\n    it('should return true when both azureEntraId and envDependent flags are enabled', () => {\n      const rootState = createRootState({\n        [FeatureFlags.azureEntraId]: { flag: true },\n        [FeatureFlags.envDependent]: { flag: true },\n      })\n\n      expect(isAzureEntraIdEnabledSelector(rootState)).toBe(true)\n    })\n\n    it('should return false when azureEntraId is disabled', () => {\n      const rootState = createRootState({\n        [FeatureFlags.azureEntraId]: { flag: false },\n        [FeatureFlags.envDependent]: { flag: true },\n      })\n\n      expect(isAzureEntraIdEnabledSelector(rootState)).toBe(false)\n    })\n\n    it('should return false when envDependent is disabled', () => {\n      const rootState = createRootState({\n        [FeatureFlags.azureEntraId]: { flag: true },\n        [FeatureFlags.envDependent]: { flag: false },\n      })\n\n      expect(isAzureEntraIdEnabledSelector(rootState)).toBe(false)\n    })\n\n    it('should return false when both flags are disabled', () => {\n      const rootState = createRootState({\n        [FeatureFlags.azureEntraId]: { flag: false },\n        [FeatureFlags.envDependent]: { flag: false },\n      })\n\n      expect(isAzureEntraIdEnabledSelector(rootState)).toBe(false)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/info.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { mswServer } from 'uiSrc/mocks/server'\nimport { errorHandlers } from 'uiSrc/mocks/res/responseComposition'\nimport { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils'\nimport { APP_INFO_DATA_MOCK } from 'uiSrc/mocks/handlers/app/infoHandlers'\nimport reducer, {\n  initialState,\n  setElectronInfo,\n  setReleaseNotesViewed,\n  getServerInfo,\n  getServerInfoSuccess,\n  getServerInfoFailure,\n  appInfoSelector,\n  fetchServerInfo,\n} from '../../app/info'\n\njest.mock('uiSrc/services')\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setElectronInfo', () => {\n    it('should properly set electron info', () => {\n      // Arrange\n      const data = {\n        isUpdateAvailable: true,\n        updateDownloadedVersion: '1.2.0',\n      }\n      const state = {\n        ...initialState,\n        electron: {\n          ...initialState.electron,\n          ...data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setElectronInfo(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { info: nextState },\n      })\n\n      expect(appInfoSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setReleaseNotesViewed', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const isReleaseNotesViewed = true\n      const state = {\n        ...initialState,\n        electron: {\n          ...initialState.electron,\n          isReleaseNotesViewed,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setReleaseNotesViewed(isReleaseNotesViewed),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { info: nextState },\n      })\n\n      expect(appInfoSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getServerInfo', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getServerInfo())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { info: nextState },\n      })\n\n      expect(appInfoSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getServerInfoSuccess', () => {\n    it('should properly set state after success', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        server: APP_INFO_DATA_MOCK,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getServerInfoSuccess(APP_INFO_DATA_MOCK),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { info: nextState },\n      })\n\n      expect(appInfoSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getServerInfoFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getServerInfoFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { info: nextState },\n      })\n\n      expect(appInfoSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('fetchServerInfo', () => {\n    it('succeed to fetch server info', async () => {\n      // Act\n      await store.dispatch<any>(fetchServerInfo(jest.fn()))\n\n      // Assert\n      const expectedActions = [\n        getServerInfo(),\n        getServerInfoSuccess(APP_INFO_DATA_MOCK),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch server info', async () => {\n      mswServer.use(...errorHandlers)\n\n      // Act\n      await store.dispatch<any>(fetchServerInfo(jest.fn(), jest.fn()))\n\n      // Assert\n      const expectedActions = [\n        getServerInfo(),\n        getServerInfoFailure(DEFAULT_ERROR_MESSAGE),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/init.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { http, HttpResponse } from 'msw'\nimport reducer, {\n  appInitSelector,\n  FAILED_TO_FETCH_CSRF_TOKEN_ERROR,\n  FAILED_TO_FETCH_FEATURE_FLAGS_ERROR,\n  initializeAppAction,\n  initializeAppState,\n  initializeAppStateFail,\n  initializeAppStateSuccess,\n  initialState,\n  STATUS_FAIL,\n  STATUS_LOADING,\n  STATUS_SUCCESS,\n} from 'uiSrc/slices/app/init'\nimport {\n  cleanup,\n  getMswURL,\n  initialStateDefault,\n  mockedStore,\n  waitFor,\n} from 'uiSrc/utils/test-utils'\nimport {\n  getFeatureFlags,\n  getFeatureFlagsFailure,\n  getFeatureFlagsSuccess,\n} from 'uiSrc/slices/app/features'\nimport { getConfig } from 'uiSrc/config'\nimport {\n  CSRFTokenResponse,\n  fetchCsrfToken,\n  fetchCsrfTokenFail,\n} from 'uiSrc/slices/app/csrf'\nimport { FEATURES_DATA_MOCK } from 'uiSrc/mocks/handlers/app/featureHandlers'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { mswServer } from 'uiSrc/mocks/server'\nimport {\n  getUserProfile,\n  getUserProfileSuccess,\n} from 'uiSrc/slices/user/cloud-user-profile'\nimport { CLOUD_ME_DATA_MOCK } from 'uiSrc/mocks/handlers/oauth/cloud'\n\nconst riConfig = getConfig()\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('init slice', () => {\n  describe('initializeAppState', () => {\n    it('should properly initialize app state', () => {\n      const state = {\n        ...initialState,\n        status: STATUS_LOADING,\n      }\n\n      // Act\n      const nextState = reducer(initialState, initializeAppState())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { init: nextState },\n      })\n\n      expect(appInitSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('initializeAppStateSuccess', () => {\n    it('should have success state', () => {\n      const state = {\n        ...initialState,\n        status: STATUS_SUCCESS,\n      }\n\n      // Act\n      const nextState = reducer(initialState, initializeAppStateSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { init: nextState },\n      })\n\n      expect(appInitSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('initializeAppStateFail', () => {\n    it('should have fail state', () => {\n      const state = {\n        ...initialState,\n        status: STATUS_FAIL,\n        error: FAILED_TO_FETCH_CSRF_TOKEN_ERROR,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        initializeAppStateFail({\n          error: FAILED_TO_FETCH_CSRF_TOKEN_ERROR,\n        }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { init: nextState },\n      })\n\n      expect(appInitSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('initApp', () => {\n    it('succeed to init data', async () => {\n      // Act\n      await store.dispatch<any>(initializeAppAction())\n\n      // Assert\n      const expectedActions = [\n        initializeAppState(),\n        getFeatureFlags(),\n        getFeatureFlagsSuccess(FEATURES_DATA_MOCK),\n        initializeAppStateSuccess(),\n      ]\n\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to init data', async () => {\n      mswServer.use(\n        http.get<any, (typeof FEATURES_DATA_MOCK)[]>(\n          getMswURL(ApiEndpoints.FEATURES),\n          async () => {\n            return HttpResponse.text('', { status: 500 })\n          },\n        ),\n      )\n\n      // Act\n      await store.dispatch<any>(initializeAppAction())\n\n      // Assert\n      const expectedActions = [\n        initializeAppState(),\n        getFeatureFlags(),\n        getFeatureFlagsFailure(),\n        initializeAppStateFail({ error: FAILED_TO_FETCH_FEATURE_FLAGS_ERROR }),\n      ]\n\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to init csrf', async () => {\n      riConfig.api.csrfEndpoint = 'csrf'\n      mswServer.use(\n        http.get<any, CSRFTokenResponse | { message: string }>(\n          getMswURL(riConfig.api.csrfEndpoint),\n          async () => {\n            return HttpResponse.text('', { status: 500 })\n          },\n        ),\n      )\n\n      // Act\n      await store.dispatch<any>(initializeAppAction())\n\n      // Assert\n      const expectedActions = [\n        initializeAppState(),\n        fetchCsrfToken(),\n        fetchCsrfTokenFail({ error: 'Request failed with status code 500' }),\n        initializeAppStateFail({ error: FAILED_TO_FETCH_CSRF_TOKEN_ERROR }),\n      ]\n\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('fetches user profile if !envDependent', async () => {\n      riConfig.api.csrfEndpoint = ''\n\n      const newFeatureFlags = {\n        features: {\n          ...FEATURES_DATA_MOCK.features,\n          envDependent: {\n            name: 'envDependent',\n            flag: false,\n          },\n        },\n      }\n\n      // Arrange\n      mswServer.use(\n        http.get<any, (typeof FEATURES_DATA_MOCK)[]>(\n          getMswURL(ApiEndpoints.FEATURES),\n          async () => {\n            return HttpResponse.json(newFeatureFlags, { status: 200 })\n          },\n        ),\n      )\n\n      // Act\n      await store.dispatch<any>(initializeAppAction())\n\n      // Assert\n      const expectedActions = [\n        initializeAppState(),\n        getFeatureFlags(),\n        getFeatureFlagsSuccess(newFeatureFlags),\n        getUserProfile(),\n        getUserProfileSuccess(CLOUD_ME_DATA_MOCK),\n        initializeAppStateSuccess(),\n      ]\n\n      await waitFor(() => {\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/notifications.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\n\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { IError, IMessage } from 'uiSrc/slices/interfaces'\nimport reducer, {\n  initialState,\n  removeError,\n  removeMessage,\n  resetErrors,\n  resetMessages,\n  addMessageNotification,\n  addErrorNotification,\n  errorsSelector,\n  messagesSelector,\n  IAddInstanceErrorPayload,\n  setIsCenterOpen,\n  notificationCenterSelector,\n  setIsNotificationOpen,\n  setNewNotificationReceived,\n  setLastReceivedNotification,\n  getNotifications,\n  getNotificationsSuccess,\n  fetchNotificationsAction,\n  getNotificationsFailed,\n  unreadNotificationsAction,\n  unreadNotifications,\n  setNewNotificationAction,\n  addInfiniteNotification,\n  removeInfiniteNotification,\n} from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst notificationsResponse: any = {\n  notifications: [\n    {\n      timestamp: 123123125,\n      title: 'string',\n      body: 'string',\n      read: false,\n    },\n    {\n      timestamp: 123123123,\n      title: 'string',\n      body: 'string',\n      read: false,\n    },\n    {\n      timestamp: 123123121,\n      title: 'string',\n      body: 'string',\n      read: false,\n    },\n  ],\n  totalUnread: 3,\n}\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('addErrorNotification', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const errorMessage = 'some error'\n      const responsePayload = {\n        instanceId: undefined,\n        name: 'Error',\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        addErrorNotification(responsePayload as IAddInstanceErrorPayload),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      const state = {\n        ...initialState,\n        errors: [\n          {\n            ...responsePayload,\n            id: errorsSelector(rootState)[0].id,\n            message: responsePayload.response.data.message,\n          },\n        ],\n      }\n\n      expect(errorsSelector(rootState)).toEqual(state.errors)\n    })\n  })\n\n  describe('removeError', () => {\n    it('should properly remove the error', () => {\n      // Arrange\n      const stateWithErrors: IError[] = [\n        // @ts-ignore\n        { id: '1', message: '' },\n        // @ts-ignore\n        { id: '2', message: '' },\n      ]\n\n      // Act\n      const nextState = reducer(\n        {\n          ...initialState,\n          errors: stateWithErrors,\n        },\n        removeError('1'),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      const state = {\n        ...initialState,\n        errors: [{ id: '2', message: '' }],\n      }\n\n      expect(errorsSelector(rootState)).toEqual(state.errors)\n    })\n  })\n\n  describe('resetErrors', () => {\n    it('should properly reset errors', () => {\n      // Arrange\n      const stateWithErrors: IError[] = [\n        // @ts-ignore\n        { id: '1', message: '' },\n        // @ts-ignore\n        { id: '2', message: '' },\n      ]\n\n      // Act\n      const nextState = reducer(\n        {\n          ...initialState,\n          errors: stateWithErrors,\n        },\n        resetErrors(),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      const state = {\n        ...initialState,\n        errors: [],\n      }\n\n      expect(errorsSelector(rootState)).toEqual(state.errors)\n    })\n  })\n\n  describe('addMessageNotification', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const message = 'some message'\n      const responsePayload = {\n        response: {\n          status: 200,\n          data: { message },\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        addMessageNotification(responsePayload),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      const state = {\n        ...initialState,\n        messages: [\n          {\n            ...responsePayload,\n            id: messagesSelector(rootState)[0].id,\n          },\n        ],\n      }\n\n      expect(messagesSelector(rootState)).toEqual(state.messages)\n    })\n  })\n\n  describe('removeMessage', () => {\n    it('should properly remove the message', () => {\n      // Arrange\n      const stateWithMessages: IMessage[] = [\n        { id: '1', message: '', title: '' },\n        { id: '2', message: '', title: '' },\n      ]\n\n      // Act\n      const nextState = reducer(\n        {\n          ...initialState,\n          messages: stateWithMessages,\n        },\n        removeMessage('1'),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      const state = {\n        ...initialState,\n        messages: [{ id: '2', message: '', title: '' }],\n      }\n\n      expect(messagesSelector(rootState)).toEqual(state.messages)\n    })\n  })\n\n  describe('resetMessages', () => {\n    it('should properly reset errors', () => {\n      // Arrange\n      const stateWithMessages: IMessage[] = [\n        { id: '1', message: '', title: '' },\n        { id: '2', message: '', title: '' },\n      ]\n\n      // Act\n      const nextState = reducer(\n        {\n          ...initialState,\n          messages: stateWithMessages,\n        },\n        resetMessages(),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      const state = {\n        ...initialState,\n        messages: [],\n      }\n\n      expect(messagesSelector(rootState)).toEqual(state.messages)\n    })\n  })\n\n  describe('setIsCenterOpen', () => {\n    it('should properly toggle isCenterOpen', () => {\n      const state = {\n        ...initialState,\n        notificationCenter: {\n          ...initialState.notificationCenter,\n          isCenterOpen: true,\n        },\n      }\n      // Act\n      const nextState = reducer(initialState, setIsCenterOpen())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('setIsNotificationOpen', () => {\n    it('should properly toggle isNotificationOpen', () => {\n      const state = {\n        ...initialState,\n        notificationCenter: {\n          ...initialState.notificationCenter,\n          isNotificationOpen: true,\n        },\n      }\n      // Act\n      const nextState = reducer(initialState, setIsNotificationOpen())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('setNewNotificationReceived', () => {\n    it('should properly set new notification', () => {\n      const state = {\n        ...initialState,\n        notificationCenter: {\n          ...initialState.notificationCenter,\n          totalUnread: notificationsResponse.totalUnread,\n          isNotificationOpen: true,\n        },\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        setNewNotificationReceived(notificationsResponse),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('setLastReceivedNotification', () => {\n    it('should properly set lastReceivedNotification', () => {\n      const state = {\n        ...initialState,\n        notificationCenter: {\n          ...initialState.notificationCenter,\n          lastReceivedNotification: notificationsResponse.notifications[0],\n        },\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        setLastReceivedNotification(notificationsResponse.notifications[0]),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('getNotifications', () => {\n    it('should properly set state', () => {\n      const state = {\n        ...initialState,\n        notificationCenter: {\n          ...initialState.notificationCenter,\n          loading: true,\n        },\n      }\n      // Act\n      const nextState = reducer(initialState, getNotifications())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('getNotificationsSuccess', () => {\n    it('should properly set state', () => {\n      const state = {\n        ...initialState,\n        notificationCenter: {\n          ...initialState.notificationCenter,\n          loading: false,\n          notifications: notificationsResponse.notifications,\n          totalUnread: notificationsResponse.totalUnread,\n        },\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        getNotificationsSuccess(notificationsResponse),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('addInfiniteNotification', () => {\n    it('should properly set state with new notification', () => {\n      const notification = {\n        id: 'id',\n        Inner: 'message text',\n      }\n      const state = {\n        ...initialState,\n        infiniteMessages: [notification],\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        addInfiniteNotification(notification),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n\n    it('should properly set state with updated notification', () => {\n      const notification = {\n        id: 'id',\n        Inner: 'updated text',\n      }\n\n      const currentState = {\n        ...initialState,\n        infiniteMessages: [\n          {\n            id: 'id',\n            Inner: 'message text',\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        infiniteMessages: [notification],\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        addInfiniteNotification(notification),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  describe('removeInfiniteNotification', () => {\n    it('should properly remove notification', () => {\n      const notification = {\n        id: 'id',\n        Inner: 'message text',\n      }\n\n      const currentState = {\n        ...initialState,\n        infiniteMessages: [notification],\n      }\n\n      const state = {\n        ...initialState,\n      }\n      // Act\n      const nextState = reducer(\n        currentState,\n        removeInfiniteNotification(notification.id),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { notifications: nextState },\n      })\n\n      expect(notificationCenterSelector(rootState)).toEqual(\n        state.notificationCenter,\n      )\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('fetchNotificationsAction', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const data = notificationsResponse\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchNotificationsAction())\n\n        // Assert\n        const expectedActions = [\n          getNotifications(),\n          getNotificationsSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchNotificationsAction())\n\n        // Assert\n        const expectedActions = [getNotifications(), getNotificationsFailed()]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('unreadNotificationsAction', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const data = notificationsResponse\n        const responsePayload = { data, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(unreadNotificationsAction(data.totalUnread))\n\n        // Assert\n        const expectedActions = [unreadNotifications(data.totalUnread)]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('setNewNotificationAction', () => {\n      it('succeed to update notificationsCenter', () => {\n        const data = notificationsResponse\n        store.dispatch<any>(setNewNotificationAction(data))\n\n        const expectedActions = [\n          setNewNotificationReceived(data),\n          setLastReceivedNotification(null),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/plugins.spec.ts",
    "content": "import { cloneDeep, flatMap, isEmpty, reject } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { IPlugin, PluginsResponse } from 'uiSrc/slices/interfaces'\nimport reducer, {\n  appPluginsSelector,\n  getAllPlugins,\n  getAllPluginsFailure,\n  getAllPluginsSuccess,\n  getPluginStateAction,\n  initialState,\n  loadPluginsAction,\n  sendPluginCommandAction,\n  setPluginStateAction,\n} from '../../app/plugins'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst MOCK_PLUGINS_RESPONSE = {\n  static: '/static/resources/plugins',\n  plugins: [\n    {\n      styles: '/static/plugins/redisearch/dist/styles.css',\n      main: '/static/plugins/redisearch/dist/index.js',\n      name: 'redisearch',\n      visualizations: [\n        {\n          id: 'redisearch',\n          name: 'Table',\n          activationMethod: 'renderRediSearch',\n          matchCommands: ['FT.INFO', 'FT.SEARCH', 'FT.AGGREGATE'],\n          iconDark: './dist/table_view_icon_dark.svg',\n          iconLight: './dist/table_view_icon_light.svg',\n          default: true,\n        },\n      ],\n      internal: true,\n      baseUrl: '/static/plugins/redisearch/',\n    },\n  ],\n}\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getAllPlugins', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getAllPlugins())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { plugins: nextState },\n      })\n\n      expect(appPluginsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getAllPluginsSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const data: PluginsResponse = MOCK_PLUGINS_RESPONSE\n      const state = {\n        ...initialState,\n        staticPath: data.static,\n        plugins: reject(data?.plugins, isEmpty),\n        visualizations: flatMap(\n          reject(data?.plugins, isEmpty),\n          (plugin: IPlugin) =>\n            plugin.visualizations.map((view) => ({\n              ...view,\n              plugin: {\n                name: plugin.name,\n                baseUrl: plugin.baseUrl,\n                internal: plugin.internal,\n                stylesSrc: plugin.styles,\n                scriptSrc: plugin.main,\n              },\n              uniqId: `${plugin.name}__${view.id}`,\n            })),\n        ),\n      }\n\n      // Act\n      const nextState = reducer(initialState, getAllPluginsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { plugins: nextState },\n      })\n\n      expect(appPluginsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getAllPluginsFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getAllPluginsFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { plugins: nextState },\n      })\n\n      expect(appPluginsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('loadPluginsAction', () => {\n    it('succeed to fetch plugins', async () => {\n      // Arrange\n      const data = MOCK_PLUGINS_RESPONSE\n      const responsePayload = { status: 200, data }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(loadPluginsAction())\n\n      // Assert\n      const expectedActions = [getAllPlugins(), getAllPluginsSuccess(data)]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch plugins', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(loadPluginsAction())\n\n      // Assert\n      const expectedActions = [\n        getAllPlugins(),\n        getAllPluginsFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('sendPluginCommandAction', () => {\n    it('succeed to send command', async () => {\n      // Arrange\n      const data = 'response'\n      const onSuccess = jest.fn()\n      const responsePayload = { status: 200, data }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        sendPluginCommandAction({\n          command: 'info',\n          onSuccessAction: onSuccess,\n        }),\n      )\n\n      expect(onSuccess).toBeCalledWith(data)\n    })\n\n    it('failed to send command', async () => {\n      // Arrange\n      const onFailed = jest.fn()\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        sendPluginCommandAction({\n          command: 'info',\n          onFailAction: onFailed,\n        }),\n      )\n\n      expect(onFailed).toBeCalledWith(responsePayload)\n    })\n  })\n\n  describe('getPluginStateAction', () => {\n    it('succeed to get plugin state ', async () => {\n      // Arrange\n      const data = 'response'\n      const onSuccess = jest.fn()\n      const responsePayload = { status: 200, data }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        getPluginStateAction({\n          visualizationId: '1',\n          commandId: '1',\n          onSuccessAction: onSuccess,\n        }),\n      )\n\n      expect(onSuccess).toBeCalledWith(data)\n    })\n\n    it('failed to get plugin state', async () => {\n      // Arrange\n      const onFailed = jest.fn()\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        getPluginStateAction({\n          visualizationId: '1',\n          commandId: '1',\n          onFailAction: onFailed,\n        }),\n      )\n\n      expect(onFailed).toBeCalledWith(responsePayload)\n    })\n  })\n\n  describe('setPluginStateAction', () => {\n    it('succeed to set plugin state ', async () => {\n      // Arrange\n      const data = 'response'\n      const onSuccess = jest.fn()\n      const responsePayload = { status: 200, data }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        setPluginStateAction({\n          visualizationId: '1',\n          commandId: '1',\n          pluginState: { info: 'smth' },\n          onSuccessAction: onSuccess,\n        }),\n      )\n\n      expect(onSuccess).toBeCalledWith(data)\n    })\n\n    it('failed to set plugin state', async () => {\n      // Arrange\n      const onFailed = jest.fn()\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        setPluginStateAction({\n          visualizationId: '1',\n          commandId: '1',\n          pluginState: { info: 'smth' },\n          onFailAction: onFailed,\n        }),\n      )\n\n      expect(onFailed).toBeCalledWith(responsePayload)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/redis-commands.spec.ts",
    "content": "import { cloneDeep, uniqBy } from 'lodash'\nimport set from 'lodash/set'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { getConfig } from 'uiSrc/config'\nimport { apiService, resourcesService } from 'uiSrc/services'\nimport { ICommand, MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport reducer, {\n  initialState,\n  getRedisCommands,\n  getRedisCommandsFailure,\n  getRedisCommandsSuccess,\n  appRedisCommandsSelector,\n  fetchRedisCommandsInfo,\n  commands,\n} from '../../app/redis-commands'\n\nconst riConfig = getConfig()\n\nconst mockConfig = (useLocalResources = false) => {\n  set(riConfig, 'app.useLocalResources', useLocalResources)\n}\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  mockConfig()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getRedisCommands', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getRedisCommands())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { redisCommands: nextState },\n      })\n\n      expect(appRedisCommandsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getRedisCommandsSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const data = MOCK_COMMANDS_SPEC\n      const state = {\n        ...initialState,\n        spec: data,\n        commandsArray: Object.keys(data).sort(),\n        commandGroups: uniqBy(Object.values(data), 'group').map(\n          (item: ICommand) => item.group,\n        ),\n      }\n\n      // Act\n      const nextState = reducer(initialState, getRedisCommandsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { redisCommands: nextState },\n      })\n\n      expect(appRedisCommandsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getRedisCommandsFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getRedisCommandsFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { redisCommands: nextState },\n      })\n\n      expect(appRedisCommandsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('fetchRedisCommandsInfo', () => {\n    it('succeed to fetch redis commands', async () => {\n      // Arrange\n      const data = MOCK_COMMANDS_SPEC\n      const responsePayload = { status: 200, data }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchRedisCommandsInfo(jest.fn()))\n\n      // Assert\n      const expectedActions = [\n        getRedisCommands(),\n        getRedisCommandsSuccess(data),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch server info', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchRedisCommandsInfo(jest.fn(), jest.fn()))\n\n      // Assert\n      const expectedActions = [\n        getRedisCommands(),\n        getRedisCommandsFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('successfully fetches all local commands', async () => {\n      mockConfig(true)\n      let expectedResult = {}\n      const onSuccessAction = jest.fn()\n      const onFailAction = jest.fn()\n      const resourceGetSpy = jest.spyOn(resourcesService, 'get')\n\n      commands.forEach((command) => {\n        expectedResult = { ...expectedResult, [command]: {} }\n        resourceGetSpy.mockResolvedValueOnce({\n          status: 200,\n          data: { [command]: {} },\n        })\n      })\n\n      // Act\n      await store.dispatch<any>(\n        fetchRedisCommandsInfo(onSuccessAction, onFailAction),\n      )\n\n      // Assert\n      const expectedActions = [\n        getRedisCommands(),\n        getRedisCommandsSuccess(expectedResult),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n      expect(onSuccessAction).toHaveBeenCalledTimes(1)\n      expect(onFailAction).not.toHaveBeenCalled()\n    })\n\n    it('handles local commands fetch failures', async () => {\n      let expectedResult = {}\n      const onSuccessAction = jest.fn()\n      const onFailAction = jest.fn()\n      const resourceGetSpy = jest.spyOn(resourcesService, 'get')\n      const errorMessage = 'Something was wrong!'\n\n      commands.slice(0, -1).forEach((command) => {\n        expectedResult = { ...expectedResult, [command]: {} }\n        resourceGetSpy.mockResolvedValueOnce({\n          status: 200,\n          data: { [command]: {} },\n        })\n      })\n      resourceGetSpy.mockRejectedValueOnce({\n        status: 500,\n        data: { message: errorMessage },\n      })\n\n      // Act\n      await store.dispatch<any>(\n        fetchRedisCommandsInfo(onSuccessAction, onFailAction),\n      )\n\n      // Assert\n      const expectedActions = [\n        getRedisCommands(),\n        getRedisCommandsFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n      expect(onFailAction).toHaveBeenCalledTimes(1)\n      expect(onSuccessAction).not.toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/app/url-handling.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { UrlHandlingActions } from 'uiSrc/slices/interfaces/urlHandling'\nimport reducer, {\n  initialState,\n  setUrlHandlingInitialState,\n  setFromUrl,\n  setUrlDbConnection,\n  setUrlProperties,\n  appRedirectionSelector,\n} from '../../app/url-handling'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('urlHandling slice', () => {\n  describe('setUrlHandlingInitialState', () => {\n    it('should properly set initial state', () => {\n      const nextState = reducer(initialState, setUrlHandlingInitialState())\n      const rootState = Object.assign(initialStateDefault, {\n        app: { urlHandling: nextState },\n      })\n      expect(appRedirectionSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('setFromUrl', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const payload = 'any-string'\n\n      const state = {\n        ...initialState,\n        fromUrl: payload,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setFromUrl(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { urlHandling: nextState },\n      })\n      expect(appRedirectionSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setUrlDbConnection', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const payload = {\n        action: UrlHandlingActions.Connect,\n        dbConnection: {\n          name: 'dbName',\n          host: 'localhost',\n          port: 6379,\n        },\n      }\n\n      const state = {\n        ...initialState,\n        ...payload,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setUrlDbConnection(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { urlHandling: nextState },\n      })\n      expect(appRedirectionSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setUrlProperties', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const payload = {\n        property1: '123',\n        property2: 'zx',\n      }\n\n      const state = {\n        ...initialState,\n        properties: payload,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setUrlProperties(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        app: { urlHandling: nextState },\n      })\n      expect(appRedirectionSelector(rootState)).toEqual(state)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { BulkActionsStatus, BulkActionsType, KeyTypes } from 'uiSrc/constants'\nimport reducer, {\n  bulkActionsSelector,\n  initialState,\n  toggleBulkDeleteActionTriggered,\n  toggleBulkActions,\n  setBulkActionConnected,\n  setLoading,\n  setBulkActionType,\n  setDeleteOverview,\n  bulkActionsDeleteOverviewSelector,\n  bulkActionsDeleteSelector,\n  disconnectBulkDeleteAction,\n  bulkDeleteSuccess,\n  setBulkDeleteStartAgain,\n  setBulkUploadStartAgain,\n  setBulkDeleteLoading,\n  setBulkDeleteFilter,\n  setBulkDeleteSearch,\n  setBulkDeleteKeyCount,\n  bulkUpload,\n  bulkUploadSuccess,\n  bulkUploadFailed,\n  bulkUploadDataAction,\n  setDeleteOverviewStatus,\n  bulkImportDefaultData,\n  bulkImportDefaultDataSuccess,\n  bulkImportDefaultDataFailed,\n  bulkImportDefaultDataAction,\n} from 'uiSrc/slices/browser/bulkActions'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { apiService } from 'uiSrc/services'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { IBulkActionOverview } from 'uiSrc/slices/interfaces'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('bulkActions slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n\n    describe('setBulkDeleteStartAgain', () => {\n      it('should properly set state', () => {\n        const currentState = {\n          ...initialState,\n          isConnected: true,\n          bulkDelete: {\n            overview: {\n              id: '123',\n            },\n            isActionTriggered: true,\n          },\n        }\n\n        // Arrange\n        const state = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, setBulkDeleteStartAgain())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setBulkUploadStartAgain', () => {\n      it('should properly set state', () => {\n        const currentState = {\n          ...initialState,\n          isConnected: true,\n          bulkDelete: {\n            isActionTriggered: true,\n          },\n          bulkUpload: {\n            fileName: 'file.ts',\n            overview: {\n              id: '123123',\n            },\n          },\n        }\n\n        // Arrange\n        const state = {\n          ...initialState,\n          bulkDelete: {\n            isActionTriggered: true,\n          },\n          bulkUpload: {\n            ...initialState.bulkUpload,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, setBulkUploadStartAgain())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('toggleBulkActions', () => {\n      it('should properly set state', () => {\n        const currentState = {\n          ...initialState,\n          isShowBulkActions: true,\n        }\n\n        // Arrange\n        const state = {\n          ...initialState,\n          isShowBulkActions: false,\n        }\n\n        // Act\n        const nextState = reducer(currentState, toggleBulkActions())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setBulkActionConnected', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          isConnected: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setBulkActionConnected(true))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setLoading', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setLoading(true))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setBulkDeleteLoading', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            loading: true,\n          },\n        }\n\n        // Act\n        const nextState = reducer(initialState, setBulkDeleteLoading(true))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setBulkActionType', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          selectedBulkAction: {\n            ...initialState.selectedBulkAction,\n            type: BulkActionsType.Delete,\n          },\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setBulkActionType(BulkActionsType.Delete),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('toggleBulkDeleteActionTriggered', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            isActionTriggered: true,\n          },\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          toggleBulkDeleteActionTriggered(),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setBulkDeleteFilter', () => {\n      it('should properly set filter', () => {\n        // Arrange\n        const state = {\n          ...initialState.bulkDelete,\n          filter: KeyTypes.Hash,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setBulkDeleteFilter(KeyTypes.Hash),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteSelector(rootState)).toEqual(state)\n      })\n\n      it('should properly set filter to null', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            filter: KeyTypes.Hash,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, setBulkDeleteFilter(null))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteSelector(rootState).filter).toBeNull()\n      })\n    })\n\n    describe('setBulkDeleteSearch', () => {\n      it('should properly set search pattern', () => {\n        // Arrange\n        const searchPattern = 'user:session:*'\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setBulkDeleteSearch(searchPattern),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteSelector(rootState).search).toEqual(\n          searchPattern,\n        )\n      })\n    })\n\n    describe('setBulkDeleteKeyCount', () => {\n      it('should properly set key count', () => {\n        // Arrange\n        const keyCount = 42\n\n        // Act\n        const nextState = reducer(initialState, setBulkDeleteKeyCount(keyCount))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteSelector(rootState).keyCount).toEqual(keyCount)\n      })\n\n      it('should properly set key count to null', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            keyCount: 100,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, setBulkDeleteKeyCount(null))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteSelector(rootState).keyCount).toBeNull()\n      })\n    })\n\n    describe('setDeleteOverview', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const data = {\n          id: 1,\n          databaseId: '1',\n          duration: 300,\n          status: 'completed',\n          type: BulkActionsType.Delete,\n          summary: { processed: 1, succeed: 1, failed: 0, errors: [] },\n        }\n\n        const overview = {\n          ...data,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setDeleteOverview(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteOverviewSelector(rootState)).toEqual(overview)\n      })\n    })\n\n    describe('setDeleteOverviewStatus', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            overview: {\n              id: 1,\n              databaseId: '1',\n              duration: 300,\n              status: 'inprogress',\n              type: BulkActionsType.Delete,\n              summary: { processed: 1, succeed: 1, failed: 0, errors: [] },\n            },\n          },\n        }\n\n        const overviewState = {\n          ...currentState.bulkDelete.overview,\n          status: BulkActionsStatus.Disconnected,\n        }\n\n        // Act\n        const nextState = reducer(\n          currentState,\n          setDeleteOverviewStatus(BulkActionsStatus.Disconnected),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsDeleteOverviewSelector(rootState)).toEqual(\n          overviewState,\n        )\n      })\n    })\n\n    describe('disconnectBulkDeleteAction', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          isConnected: true,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            loading: true,\n            isActionTriggered: true,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, disconnectBulkDeleteAction())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(initialState)\n      })\n    })\n\n    describe('bulkDeleteSuccess', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          bulkDelete: {\n            ...initialState.bulkDelete,\n            loading: true,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, bulkDeleteSuccess())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(initialState)\n      })\n    })\n\n    describe('bulkUpload', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          bulkUpload: {\n            ...initialState.bulkUpload,\n            loading: true,\n          },\n        }\n\n        // Act\n        const nextState = reducer(initialState, bulkUpload())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n\n      describe('bulkUploadSuccess', () => {\n        it('should properly set state', () => {\n          // Arrange\n          const currentState = {\n            ...initialState,\n            bulkUpload: {\n              ...initialState.bulkUpload,\n              loading: true,\n            },\n          }\n\n          const state = {\n            ...initialState,\n            bulkUpload: {\n              ...initialState.bulkUpload,\n              loading: false,\n              overview: {},\n              fileName: 'file.txt',\n            },\n          }\n\n          // Act\n          const nextState = reducer(\n            currentState,\n            bulkUploadSuccess({ data: {}, fileName: 'file.txt' }),\n          )\n\n          // Assert\n          const rootState = Object.assign(initialStateDefault, {\n            browser: { bulkActions: nextState },\n          })\n          expect(bulkActionsSelector(rootState)).toEqual(state)\n        })\n      })\n\n      describe('bulkUploadFailed', () => {\n        it('should properly set state', () => {\n          // Arrange\n          const state = {\n            ...initialState,\n            bulkUpload: {\n              ...initialState.bulkUpload,\n              loading: false,\n              error: 'error',\n            },\n          }\n\n          // Act\n          const nextState = reducer(initialState, bulkUploadFailed('error'))\n\n          // Assert\n          const rootState = Object.assign(initialStateDefault, {\n            browser: { bulkActions: nextState },\n          })\n          expect(bulkActionsSelector(rootState)).toEqual(state)\n        })\n      })\n    })\n\n    describe('bulkImportDefaultData', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, bulkImportDefaultData())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { bulkActions: nextState },\n        })\n        expect(bulkActionsSelector(rootState)).toEqual(state)\n      })\n\n      describe('bulkImportDefaultDataSuccess', () => {\n        it('should properly set state', () => {\n          // Arrange\n          const currentState = {\n            ...initialState,\n            loading: true,\n          }\n\n          const state = {\n            ...initialState,\n            loading: false,\n          }\n\n          // Act\n          const nextState = reducer(\n            currentState,\n            bulkImportDefaultDataSuccess(),\n          )\n\n          // Assert\n          const rootState = Object.assign(initialStateDefault, {\n            browser: { bulkActions: nextState },\n          })\n          expect(bulkActionsSelector(rootState)).toEqual(state)\n        })\n      })\n\n      describe('bulkImportDefaultDataFailed', () => {\n        it('should properly set state', () => {\n          // Arrange\n          const currentState = {\n            ...initialState,\n            loading: true,\n          }\n\n          const state = {\n            ...initialState,\n            loading: false,\n          }\n\n          // Act\n          const nextState = reducer(currentState, bulkImportDefaultDataFailed())\n\n          // Assert\n          const rootState = Object.assign(initialStateDefault, {\n            browser: { bulkActions: nextState },\n          })\n          expect(bulkActionsSelector(rootState)).toEqual(state)\n        })\n      })\n    })\n  })\n\n  // thunks\n  describe('bulkUploadDataAction', () => {\n    it('should call proper actions on success', async () => {\n      // Arrange\n      const formData = new FormData()\n      formData.append('file', '')\n      const data = {}\n\n      const responsePayload = { data, status: 200 }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        bulkUploadDataAction('id', { file: formData, fileName: 'text.txt' }),\n      )\n\n      // Assert\n      const expectedActions = [\n        bulkUpload(),\n        bulkUploadSuccess({ data: responsePayload.data, fileName: 'text.txt' }),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('should call proper actions on fail', async () => {\n      // Arrange\n      const formData = new FormData()\n      const errorMessage = 'Some error'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        bulkUploadDataAction('id', { file: formData, fileName: 'text.txt' }),\n      )\n\n      // Assert\n      const expectedActions = [\n        bulkUpload(),\n        addErrorNotification(responsePayload as AxiosError),\n        bulkUploadFailed(errorMessage),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  // thunks\n  describe('bulkImportDefaultDataAction', () => {\n    it('should call proper actions on success', async () => {\n      const data = {}\n      const responsePayload = { data, status: 200 }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(bulkImportDefaultDataAction('id'))\n\n      // Assert\n      const expectedActions = [\n        bulkImportDefaultData(),\n        bulkImportDefaultDataSuccess(),\n        addMessageNotification(\n          successMessages.UPLOAD_DATA_BULK(data as IBulkActionOverview),\n        ),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('should call proper actions on fail', async () => {\n      // Arrange\n      const errorMessage = 'Some error'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(bulkImportDefaultDataAction('id'))\n\n      // Assert\n      const expectedActions = [\n        bulkImportDefaultData(),\n        addErrorNotification(responsePayload as AxiosError),\n        bulkImportDefaultDataFailed(),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/hash.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { bufferToString, stringToBuffer } from 'uiSrc/utils'\nimport { deleteRedisearchKeyFromList } from 'uiSrc/slices/browser/redisearch'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport {\n  defaultSelectedKeyAction,\n  refreshKeyInfo,\n  deleteSelectedKeySuccess,\n  updateSelectedKeyRefreshTime,\n  refreshKeyInfoSuccess,\n  loadKeyInfoSuccess,\n} from '../../browser/keys'\nimport reducer, {\n  deleteHashFields,\n  fetchMoreHashFields,\n  fetchHashFields,\n  initialState,\n  loadMoreHashFields,\n  loadMoreHashFieldsFailure,\n  loadMoreHashFieldsSuccess,\n  loadHashFields,\n  loadHashFieldsFailure,\n  loadHashFieldsSuccess,\n  removeFieldsFromList,\n  removeHashFields,\n  removeHashFieldsFailure,\n  removeHashFieldsSuccess,\n  hashSelector,\n  updateValue,\n  updateValueSuccess,\n  updateValueFailure,\n  resetUpdateValue,\n  addHashFieldsAction,\n  updateFieldsInList,\n  updateHashFieldsAction,\n  refreshHashFieldsAction,\n} from '../../browser/hash'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('hash slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadHashFields', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadHashFields(['*', undefined]))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadHashFieldsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        keyName: 'hash',\n        nextCursor: 0,\n        fields: [{ field: 'hash field', value: 'hash value' }],\n        total: 1,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...data,\n          key: data.keyName,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadHashFieldsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty fields', () => {\n      // Arrange\n      const data = {\n        keyName: 'hash',\n        nextCursor: 0,\n        fields: [],\n        total: 0,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...data,\n          key: data.keyName,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadHashFieldsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadHashFieldsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadHashFieldsFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreHashFields', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreHashFields())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreHashFieldsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        keyName: 'hash',\n        nextCursor: 0,\n        fields: [{ field: 'hash field', value: 'hash value' }],\n        total: 1,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          ...data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreHashFieldsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: 'hash',\n        nextCursor: 0,\n        fields: [],\n        total: 0,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          ...data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreHashFieldsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreHashFieldsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreHashFieldsFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeHashFields', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeHashFields())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeHashFieldsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const initailStateRemove = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          fields: [{ field: 'hash field', value: 'hash value' }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initailStateRemove, removeHashFieldsSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(initailStateRemove)\n    })\n  })\n\n  describe('removeHashFieldsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeHashFieldsFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeFieldsFromList', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const initialStateRemove = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          fields: [\n            { field: stringToBuffer('hash field'), value: 'hash value' },\n            { field: stringToBuffer('hash field2'), value: 'hash value' },\n            { field: stringToBuffer('hash field3'), value: 'hash value' },\n          ],\n        },\n      }\n\n      const data = [stringToBuffer('hash field'), stringToBuffer('hash field3')]\n\n      const state = {\n        ...initialStateRemove,\n        data: {\n          ...initialStateRemove.data,\n          total: initialStateRemove.data.total - 1,\n          fields: [\n            { field: stringToBuffer('hash field2'), value: 'hash value' },\n          ],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateRemove, removeFieldsFromList(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateValue', () => {\n    it('should properly set the state while updating a hash key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: true,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValue())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateValueSuccess', () => {\n    it('should properly set the state after successfully updated hash key', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateValueFailure', () => {\n    it('should properly set the state on update hash key failure', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetUpdateValue', () => {\n    it('should properly reset the state', () => {\n      // Arrange;\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: false,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetUpdateValue())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { hash: nextState },\n      }\n      expect(hashSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchHashFields', () => {\n      it('call fetchHashFields, loadHashFieldsSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          keyName: 'small set',\n          nextCursor: 0,\n          fields: [{ field: 'hash field', value: 'hash value' }],\n          total: 3,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchHashFields(data.keyName, 0, 20, '*'))\n\n        // Assert\n        const expectedActions = [\n          loadHashFields(['*', true]),\n          loadHashFieldsSuccess(responsePayload.data),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchMoreHashFields', () => {\n      it(\n        'call fetchMoreHashFields, loadMoreHashFieldsSuccess,' +\n          ' when fetch is successed',\n        async () => {\n          // Arrange\n          const data = {\n            keyName: 'hash',\n            nextCursor: 0,\n            fields: [{ field: 'hash field', value: 'hash value' }],\n            total: 3,\n          }\n          const responsePayload = { data, status: 200 }\n\n          apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n          // Act\n          await store.dispatch<any>(\n            fetchMoreHashFields(data.keyName, 0, 20, '*'),\n          )\n\n          // Assert\n          const expectedActions = [\n            loadMoreHashFields(),\n            loadMoreHashFieldsSuccess(responsePayload.data),\n          ]\n\n          expect(mockedStore.getActions()).toEqual(expectedActions)\n        },\n      )\n    })\n\n    describe('refreshHashFieldsAction', () => {\n      it('succeed to refresh hash data', async () => {\n        // Arrange\n        const data = {\n          keyName: 'small set',\n          nextCursor: 0,\n          fields: [{ field: 'hash field', value: 'hash value' }],\n          total: 3,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(refreshHashFieldsAction(data.keyName))\n\n        // Assert\n        const expectedActions = [\n          loadHashFields(['*', undefined]),\n          loadHashFieldsSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteHashFields', () => {\n      it(\n        'call removeHashFields, removeHashFieldsSuccess,' +\n          ' and removeFieldsFromList when fetch is successed',\n        async () => {\n          // Arrange\n\n          const key = stringToBuffer('key')\n          const fields = ['hash field', 'hash field 2'].map((field) =>\n            stringToBuffer(field),\n          )\n          const responsePayload = { status: 200, data: { affected: 2 } }\n          const nextState = {\n            ...initialStateDefault,\n            browser: {\n              keys: {\n                ...initialStateDefault.browser.keys,\n              },\n              hash: {\n                ...initialState,\n                data: {\n                  ...initialState.data,\n                  total: 10,\n                },\n              },\n            },\n          }\n\n          const mockedStore = mockStore(nextState)\n\n          apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n          apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n          // Act\n          await mockedStore.dispatch<any>(deleteHashFields(key, fields))\n\n          // Assert\n          const expectedActions = [\n            removeHashFields(),\n            removeHashFieldsSuccess(),\n            removeFieldsFromList(fields),\n            refreshKeyInfo(),\n            addMessageNotification(\n              successMessages.REMOVED_KEY_VALUE(\n                key,\n                fields.map((field) => bufferToString(field)).join(''),\n                'Field',\n              ),\n            ),\n            refreshKeyInfoSuccess({ affected: 2 }),\n            updateSelectedKeyRefreshTime(MOCK_TIMESTAMP),\n          ]\n\n          expect(mockedStore.getActions()).toEqual(expectedActions)\n        },\n      )\n\n      it('succeed to delete all fields from hast', async () => {\n        // Arrange\n\n        const key = 'key'\n        const fields = ['hash field', 'hash field 2']\n        const responsePayload = { status: 200, data: { affected: 2 } }\n        const nextState = {\n          ...initialStateDefault,\n          browser: {\n            hash: {\n              ...initialState,\n              data: {\n                ...initialState.data,\n                total: 2,\n              },\n            },\n          },\n        }\n\n        const mockedStore = mockStore(nextState)\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await mockedStore.dispatch<any>(deleteHashFields(key, fields))\n\n        // Assert\n        const expectedActions = [\n          removeHashFields(),\n          removeHashFieldsSuccess(),\n          removeFieldsFromList(fields),\n          deleteSelectedKeySuccess(),\n          deleteRedisearchKeyFromList(key),\n          addMessageNotification(successMessages.DELETED_KEY(key)),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addHashFields', () => {\n      const keyName = 'key'\n      const fields = [\n        { field: 'hash field', value: 'hash value' },\n        { field: 'hash field 2', value: 'hash value 2' },\n      ]\n      it('succeed to add fields to hash', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n        apiService.put = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await mockedStore.dispatch<any>(\n          addHashFieldsAction({ keyName, fields }),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          updateValueSuccess(),\n          defaultSelectedKeyAction(),\n          loadKeyInfoSuccess({ affected: 2 }),\n          updateSelectedKeyRefreshTime(MOCK_TIMESTAMP),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n      it('failed to add fields to hash', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addHashFieldsAction({ keyName, fields }))\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateValueFailure(errorMessage),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('updateHashFieldsAction', () => {\n      const keyName = 'key'\n      const fields = [\n        { field: 'hash field', value: 'hash value' },\n        { field: 'hash field 2', value: 'hash value 2' },\n      ]\n      it('succeed to update fields in hash', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n        apiService.put = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(updateHashFieldsAction({ keyName, fields }))\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          updateValueSuccess(),\n          updateFieldsInList(fields),\n          refreshKeyInfo(),\n          refreshKeyInfoSuccess({ affected: 2 }),\n          updateSelectedKeyRefreshTime(MOCK_TIMESTAMP),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n      it('failed to update fields in hash', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await mockedStore.dispatch<any>(\n          updateHashFieldsAction({ keyName, fields }),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateValueFailure(errorMessage),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/keys.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { configureStore } from '@reduxjs/toolkit'\nimport { getConfig } from 'uiSrc/config'\nimport {\n  BrowserColumns,\n  KeyTypes,\n  KeyValueFormat,\n  ModulesKeyTypes,\n} from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  parseKeysListResponse,\n  stringToBuffer,\n  UTF8ToBuffer,\n} from 'uiSrc/utils'\nimport {\n  cleanup,\n  clearStoreActions,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { SearchHistoryItem, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { setBrowserSelectedKey } from 'uiSrc/slices/app/context'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport {\n  setEditorType,\n  setIsWithinThreshold,\n} from 'uiSrc/slices/browser/rejson'\nimport { EditorType } from 'uiSrc/slices/interfaces'\nimport { CreateHashWithExpireDto } from 'apiSrc/modules/browser/hash/dto'\nimport {\n  CreateListWithExpireDto,\n  ListElementDestination,\n} from 'apiSrc/modules/browser/list/dto'\nimport { CreateRejsonRlWithExpireDto } from 'apiSrc/modules/browser/rejson-rl/dto'\nimport { CreateSetWithExpireDto } from 'apiSrc/modules/browser/set/dto'\nimport { CreateZSetWithExpireDto } from 'apiSrc/modules/browser/z-set/dto'\nimport { SetStringWithExpireDto } from 'apiSrc/modules/browser/string/dto'\nimport { rootReducer } from '../../store'\nimport { getString, getStringSuccess } from '../../browser/string'\nimport reducer, {\n  addHashKey,\n  addKey,\n  addKeyFailure,\n  addKeySuccess,\n  addListKey,\n  addReJSONKey,\n  addSetKey,\n  addStringKey,\n  addZsetKey,\n  defaultSelectedKeyAction,\n  defaultSelectedKeyActionFailure,\n  defaultSelectedKeyActionSuccess,\n  deleteSelectedKey,\n  deleteSelectedKeyAction,\n  deleteSelectedKeyFailure,\n  deleteSelectedKeySuccess,\n  deleteKey,\n  deleteKeySuccess,\n  deleteKeyFailure,\n  deleteKeyAction,\n  deletePatternHistoryAction,\n  deletePatternKeyFromList,\n  deleteKeysByPattern,\n  deleteSearchHistory,\n  deleteSearchHistoryAction,\n  deleteSearchHistoryFailure,\n  deleteSearchHistorySuccess,\n  editKey,\n  editKeyTTL,\n  editPatternKeyFromList,\n  editPatternKeyTTLFromList,\n  fetchKeyInfo,\n  fetchKeys,\n  fetchKeysMetadata,\n  fetchKeysMetadataTree,\n  fetchMoreKeys,\n  fetchPatternHistoryAction,\n  fetchSearchHistoryAction,\n  initialState,\n  keysSelector,\n  loadKeyInfoSuccess,\n  loadKeys,\n  loadKeysFailure,\n  loadKeysSuccess,\n  loadMoreKeys,\n  loadMoreKeysFailure,\n  loadMoreKeysSuccess,\n  loadSearchHistory,\n  loadSearchHistoryFailure,\n  loadSearchHistorySuccess,\n  refreshKeyInfo,\n  refreshKeyInfoAction,\n  updateKeyList,\n  addKeyIntoList,\n  refreshKeyInfoFail,\n  refreshKeyInfoSuccess,\n  resetAddKey,\n  resetKeyInfo,\n  resetKeys,\n  setLastBatchPatternKeys,\n  updateSelectedKeyRefreshTime,\n  refreshKey,\n  fetchNamespaceSearchable,\n} from '../../browser/keys'\n\nconst riConfig = getConfig()\nconst REJSON_THRESHOLD = riConfig.browser.rejsonMonacoEditorMaxThreshold\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('keys slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadKeys', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadKeysSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        total: 249,\n        nextCursor: '228',\n        keys: [\n          {\n            name: stringToBuffer('bull:mail-queue:155'),\n            type: 'hash',\n            ttl: 2147474450,\n            size: 3041,\n          },\n          {\n            name: stringToBuffer('bull:mail-queue:223'),\n            type: 'hash',\n            ttl: -1,\n            size: 3041,\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...data,\n          lastRefreshTime: Date.now(),\n          previousResultCount: data.keys.length,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadKeysSuccess({ data, isFiltered: false, isSearched: false }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = {}\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...data,\n          previousResultCount: data.keys?.length,\n          lastRefreshTime: Date.now(),\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadKeysSuccess({ data, isFiltered: false, isSearched: false }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadKeysFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeysFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreKeys', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreKeysSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        total: 0,\n        nextCursor: '0',\n        shardsMeta: {},\n        scanned: 0,\n        keys: [\n          {\n            name: stringToBuffer('bull:mail-queue:155'),\n            type: 'hash',\n            ttl: 2147474450,\n            size: 3041,\n          },\n          {\n            name: stringToBuffer('bull:mail-queue:223'),\n            type: 'hash',\n            ttl: -1,\n            size: 3041,\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...data,\n          previousResultCount: data.keys.length,\n          lastRefreshTime: initialState.data.lastRefreshTime,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        total: 0,\n        nextCursor: '0',\n        keys: [],\n        scanned: 0,\n        shardsMeta: {},\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('loadMoreKeysFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadKeyInfoSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = {\n        name: stringToBuffer('keyName'),\n        nameString: 'keyName',\n        type: 'hash',\n        ttl: -1,\n        size: 279,\n      }\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: false,\n          isRefreshDisabled: false,\n          data: {\n            ...data,\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeyInfoSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setLastBatchKeys', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const strToKey = (name: string) => ({\n        name,\n        nameString: name,\n        ttl: 1,\n        size: 1,\n        type: 'hash',\n      })\n      const data = ['44', '55', '66'].map(strToKey)\n\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '44', '55', '66'].map(strToKey),\n        },\n      }\n\n      const prevState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '4', '5', '6'].map(strToKey),\n        },\n      }\n\n      // Act\n      const nextState = reducer(prevState, setLastBatchPatternKeys(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('refreshKeyInfo', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          refreshing: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, refreshKeyInfo())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('refreshKeyInfoSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = {\n        name: 'keyName',\n        type: 'hash',\n        ttl: -1,\n        size: 279,\n        length: 3,\n      }\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          refreshing: false,\n          data: { ...initialState.selectedKey.data, ...data },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, refreshKeyInfoSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('refreshKeyInfoFail', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          refreshing: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, refreshKeyInfoFail())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addKey', () => {\n    it('should properly set the state while adding a key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        addKey: {\n          ...initialState.addKey,\n          loading: true,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, addKey())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateKeyList', () => {\n    it('should properly set the state after successfully added key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: [{ name: 'name' }],\n          scanned: 1,\n          total: 1,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        updateKeyList({ keyName: 'name', keyType: 'hash' }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addKeyFailure', () => {\n    it('should properly set the state on add key failure', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        addKey: {\n          ...initialState.addKey,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, addKeyFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetAddKey', () => {\n    it('should properly reset the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        addKey: {\n          ...initialState.addKey,\n          loading: false,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetAddKey())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSelectedKey', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: true,\n          data: null,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteSelectedKey())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSelectedKeySuccess', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: false,\n          data: null,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteSelectedKeySuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSelectedKeyFailure', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteSelectedKeyFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteKey', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        deleting: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteKey())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteKeySuccess', () => {\n    it('should properly set the state after the delete key', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        deleting: true,\n      }\n      const state = {\n        ...initialState,\n        deleting: false,\n      }\n\n      // Act\n      const nextState = reducer(currentState, deleteKeySuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteKeyFailure', () => {\n    it('should properly set the state after the delete key', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        deleting: true,\n      }\n      const state = {\n        ...initialState,\n        deleting: false,\n      }\n\n      // Act\n      const nextState = reducer(currentState, deleteKeyFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteKeysByPattern', () => {\n    it('should remove keys matching pattern using buffer comparison', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          total: 100,\n          scanned: 50,\n          keys: [\n            { name: stringToBuffer('user:session:1'), type: KeyTypes.String },\n            { name: stringToBuffer('user:session:2'), type: KeyTypes.String },\n            { name: stringToBuffer('user:profile:1'), type: KeyTypes.Hash },\n            { name: stringToBuffer('other:key'), type: KeyTypes.String },\n          ],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        deleteKeysByPattern({ pattern: 'user:session:*', deletedCount: 10 }),\n      )\n\n      // Assert - matching keys are removed\n      expect(nextState.data.keys).toHaveLength(2)\n      // Total is updated based on deletedCount from server\n      expect(nextState.data.total).toEqual(90) // 100 - 10 (deletedCount)\n    })\n\n    it('should not go below zero for total count', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          total: 5,\n          scanned: 2,\n          keys: [{ name: stringToBuffer('key:1'), type: KeyTypes.String }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        deleteKeysByPattern({ pattern: 'key:*', deletedCount: 100 }),\n      )\n\n      // Assert\n      expect(nextState.data.keys).toHaveLength(0) // Key removed\n      expect(nextState.data.total).toEqual(0) // Clamped to 0\n    })\n\n    it('should skip local key removal for \"*\" pattern (all keys) but update total', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          total: 100,\n          scanned: 50,\n          keys: [\n            { name: stringToBuffer('user:1'), type: KeyTypes.String },\n            { name: stringToBuffer('session:1'), type: KeyTypes.Hash },\n            { name: stringToBuffer('other:key'), type: KeyTypes.String },\n          ],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        deleteKeysByPattern({ pattern: '*', deletedCount: 50 }),\n      )\n\n      // Assert - keys should NOT be removed for '*' pattern\n      expect(nextState.data.keys).toHaveLength(3)\n      // Total should still be updated\n      expect(nextState.data.total).toEqual(50)\n    })\n  })\n\n  describe('defaultSelectedKeyAction', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: true,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, defaultSelectedKeyAction())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('defaultSelectedKeyActionSuccess', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, defaultSelectedKeyActionSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('defaultSelectedKeyActionFailure', () => {\n    it('should properly set the state before the delete key', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        defaultSelectedKeyActionFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('editPatternKeyFromList', () => {\n    it('should properly set the state before the edit key', () => {\n      // Arrange\n\n      const data = {\n        key: 'test',\n        newKey: 'test2',\n      }\n\n      const initialStateMock = {\n        ...initialState,\n        data: {\n          keys: [{ name: data.key }],\n        },\n      }\n      const state = {\n        ...initialState,\n        data: {\n          keys: [{ name: data.newKey, nameString: data.newKey }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateMock, editPatternKeyFromList(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('editPatternKeyTTLFromList', () => {\n    it('should properly set the state before the edit key ttl', () => {\n      // Arrange\n\n      const key = UTF8ToBuffer('key')\n      const ttl = 12000\n\n      const initialStateMock = {\n        ...initialState,\n        data: {\n          keys: [{ name: key }],\n        },\n      }\n      const state = {\n        ...initialState,\n        data: {\n          keys: [{ name: key, ttl }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateMock,\n        editPatternKeyTTLFromList([key, ttl]),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetKeyInfo', () => {\n    it('should properly save viewFormat', () => {\n      // Arrange\n      const viewFormat = KeyValueFormat.HEX\n      const initialStateMock = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          viewFormat,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateMock, resetKeyInfo())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(initialStateMock)\n    })\n  })\n\n  describe('resetKeys', () => {\n    it('should properly save viewFormat', () => {\n      // Arrange\n      const viewFormat = KeyValueFormat.HEX\n      const initialStateMock = {\n        ...initialState,\n        selectedKey: {\n          ...initialState.selectedKey,\n          viewFormat,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateMock, resetKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(initialStateMock)\n    })\n  })\n\n  describe('loadSearchHistory', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, loadSearchHistory())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSearchHistorySuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data: SearchHistoryItem[] = [\n        {\n          id: '1',\n          mode: SearchMode.Pattern,\n          filter: { type: 'list', match: '*' },\n        },\n      ]\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n          data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, loadSearchHistorySuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSearchHistoryFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, loadSearchHistoryFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSearchHistory', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, deleteSearchHistory())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSearchHistorySuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data: SearchHistoryItem[] = [\n        {\n          id: '1',\n          mode: SearchMode.Pattern,\n          filter: { type: 'list', match: '*' },\n        },\n        {\n          id: '2',\n          mode: SearchMode.Pattern,\n          filter: { type: 'list', match: '*' },\n        },\n      ]\n      const currentState = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n          data,\n        },\n      }\n\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n          data: [\n            {\n              id: '1',\n              mode: SearchMode.Pattern,\n              filter: { type: 'list', match: '*' },\n            },\n          ],\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState, deleteSearchHistorySuccess(['2']))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteSearchHistoryFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, deleteSearchHistoryFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { keys: nextState },\n      })\n      expect(keysSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('refreshKey', () => {\n    it('defaultSelectedKeyAction should be called by default', async () => {\n      const key = stringToBuffer('key')\n\n      // Act\n      await store.dispatch<any>(refreshKey(key, ModulesKeyTypes.Graph))\n\n      // Assert\n      const expectedActions = [refreshKeyInfo(), defaultSelectedKeyAction()]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchKeys', () => {\n      it('call both loadKeys and loadKeysSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          total: 10,\n          nextCursor: 20,\n          scanned: 20,\n          shardsMeta: {},\n          keys: [\n            {\n              name: stringToBuffer('bull:mail-queue:155'),\n              type: 'hash',\n              ttl: 2147474450,\n              size: 3041,\n            },\n            {\n              name: stringToBuffer('bull:mail-queue:223'),\n              type: 'hash',\n              ttl: -1,\n              size: 3041,\n            },\n          ],\n        }\n        const responsePayload = {\n          data: [\n            {\n              total: data.total,\n              scanned: data.scanned,\n              cursor: 20,\n              keys: [...data.keys],\n            },\n          ],\n          status: 200,\n        }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchKeys({ searchMode: SearchMode.Pattern, cursor: '0', count: 20 }),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadKeys(),\n          loadKeysSuccess({\n            data: parseKeysListResponse({}, responsePayload.data),\n            isFiltered: false,\n            isSearched: false,\n          }),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to load keys', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchKeys({ searchMode: SearchMode.Pattern, cursor: '0', count: 20 }),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadKeysFailure(errorMessage),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchMoreKeys', () => {\n      it('call both loadMoreKeys and loadMoreKeysSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          total: 0,\n          nextCursor: '0',\n          scanned: 20,\n          shardsMeta: {},\n          keys: [\n            {\n              name: stringToBuffer('bull:mail-queue:155'),\n              type: 'hash',\n              ttl: 2147474450,\n              size: 3041,\n            },\n            {\n              name: stringToBuffer('bull:mail-queue:223'),\n              type: 'hash',\n              ttl: -1,\n              size: 3041,\n            },\n          ],\n        }\n\n        const responsePayload = {\n          data: [\n            {\n              total: data.total,\n              scanned: data.scanned,\n              cursor: 20,\n              keys: [...data.keys],\n            },\n          ],\n          status: 200,\n        }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchMoreKeys(SearchMode.Pattern, [], '0', 20),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadMoreKeys(),\n          loadMoreKeysSuccess(parseKeysListResponse({}, responsePayload.data)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch more keys', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchMoreKeys(SearchMode.Pattern, [], '0', 20),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadMoreKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadMoreKeysFailure(errorMessage),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchKeyInfo', () => {\n      it('call both defaultSelectedKeyAction and loadKeyInfoSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('string'),\n          type: KeyTypes.String,\n          ttl: -1,\n          size: 10,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchKeyInfo(data.name))\n\n        // Assert\n        const expectedActions = [\n          defaultSelectedKeyAction(),\n          loadKeyInfoSuccess(responsePayload.data),\n          updateSelectedKeyRefreshTime(Date.now()),\n          // fetch keyInfo\n          getString(),\n          getStringSuccess(data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch key info', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchKeyInfo('keyName'))\n\n        // Assert\n        const expectedActions = [\n          defaultSelectedKeyAction(),\n          addErrorNotification(responsePayload as AxiosError),\n          defaultSelectedKeyActionFailure(errorMessage),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('key info is reset when key not found', async () => {\n        // Arrange\n        const errorMessage = 'resource not found error'\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchKeyInfo('keyName'))\n\n        // Assert\n        const expectedActions = [\n          defaultSelectedKeyAction(),\n          addErrorNotification(responsePayload as AxiosError),\n          defaultSelectedKeyActionFailure(errorMessage),\n          resetKeyInfo(),\n          setBrowserSelectedKey(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should set default JSON editor', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('rejson'),\n          type: KeyTypes.ReJSON,\n          ttl: -1,\n          size: 10,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchKeyInfo(data.name))\n\n        // Assert\n        expect(store.getActions()).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining(setEditorType(EditorType.Default)),\n          ]),\n        )\n      })\n\n      it('should set isWithinThreshold to true when length is within threshold', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('rejson'),\n          type: KeyTypes.ReJSON,\n          ttl: -1,\n          size: REJSON_THRESHOLD,\n          length: REJSON_THRESHOLD + 100, // just to make sure this isn't used instead of size\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchKeyInfo(data.name))\n\n        // Assert\n        expect(store.getActions()).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining(setIsWithinThreshold(true)),\n          ]),\n        )\n      })\n\n      it('should set isWithinThreshold to false when length exceeds threshold', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('rejson'),\n          type: KeyTypes.ReJSON,\n          ttl: -1,\n          size: REJSON_THRESHOLD + 1,\n          length: REJSON_THRESHOLD, // just to make sure this isn't used instead of size\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchKeyInfo(data.name))\n\n        // Assert\n        expect(store.getActions()).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining(setIsWithinThreshold(false)),\n          ]),\n        )\n      })\n    })\n\n    describe('refreshKeyInfoAction', () => {\n      it('success to refresh key info', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('keyName'),\n          type: 'hash',\n          ttl: -1,\n          size: 279,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          refreshKeyInfoAction(stringToBuffer('keyName')),\n        )\n\n        // Assert\n        const expectedActions = [\n          refreshKeyInfo(),\n          refreshKeyInfoSuccess(responsePayload.data),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to refresh key info', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(refreshKeyInfoAction('keyName'))\n\n        // Assert\n        const expectedActions = [\n          refreshKeyInfo(),\n          refreshKeyInfoFail(),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should reset key info and clear selected key when key not found (404)', async () => {\n        // Arrange\n        const keyName = stringToBuffer('deletedKey')\n        const errorMessage = 'Key with this name does not exist.'\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(refreshKeyInfoAction(keyName))\n\n        // Assert\n        const expectedActions = [\n          refreshKeyInfo(),\n          refreshKeyInfoFail(),\n          addErrorNotification(responsePayload as AxiosError),\n          resetKeyInfo(),\n          deletePatternKeyFromList(keyName),\n          setBrowserSelectedKey(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addHashKey', () => {\n      it('success to add key', async () => {\n        // Arrange\n        const data: CreateHashWithExpireDto = {\n          keyName: 'keyName',\n          fields: [{ field: '1', value: '1' }],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addHashKey(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          addKey(),\n          addKeySuccess(),\n          updateKeyList({ keyName: data.keyName, keyType: 'hash' }),\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addHashZset', () => {\n      it('success to add key', async () => {\n        // Arrange\n        const data: CreateZSetWithExpireDto = {\n          keyName: 'keyName',\n          members: [{ name: '1', score: 1 }],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addZsetKey(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          addKey(),\n          addKeySuccess(),\n          updateKeyList({ keyName: data.keyName, keyType: 'zset' }),\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addHashSet', () => {\n      it('success to add key', async () => {\n        // Arrange\n        const data: CreateSetWithExpireDto = {\n          keyName: 'keyName',\n          members: ['member'],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addSetKey(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          addKey(),\n          addKeySuccess(),\n          updateKeyList({ keyName: data.keyName, keyType: 'set' }),\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addStringKey', () => {\n      it('success to add key', async () => {\n        // Arrange\n        const data: SetStringWithExpireDto = {\n          keyName: 'keyName',\n          value: 'string',\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addStringKey(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          addKey(),\n          addKeySuccess(),\n          updateKeyList({ keyName: data.keyName, keyType: 'string' }),\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addListKey', () => {\n      it('success to add key', async () => {\n        // Arrange\n        const data: CreateListWithExpireDto = {\n          keyName: 'keyName',\n          destination: 'TAIL' as ListElementDestination,\n          elements: ['1'],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addListKey(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          addKey(),\n          addKeySuccess(),\n          updateKeyList({ keyName: data.keyName, keyType: 'list' }),\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addReJSONKey', () => {\n      it('success to add key', async () => {\n        // Arrange\n        const data: CreateRejsonRlWithExpireDto = {\n          keyName: 'keyName',\n          data: '{}',\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(addReJSONKey(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          addKey(),\n          addKeySuccess(),\n          updateKeyList({ keyName: data.keyName, keyType: 'ReJSON-RL' }),\n          addMessageNotification(successMessages.ADDED_NEW_KEY(data.keyName)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteSelectedKey', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('string'),\n          type: KeyTypes.String,\n          ttl: -1,\n          size: 10,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteSelectedKeyAction(data.name))\n\n        // Assert\n        const expectedActions = [\n          deleteSelectedKey(),\n          deleteSelectedKeySuccess(),\n          deletePatternKeyFromList(data.name),\n          addMessageNotification(successMessages.DELETED_KEY(data.name)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteKey', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const data = {\n          name: stringToBuffer('string'),\n          type: KeyTypes.String,\n          ttl: -1,\n          size: 10,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteKeyAction(data.name))\n\n        // Assert\n        const expectedActions = [\n          deleteKey(),\n          deleteKeySuccess(),\n          deletePatternKeyFromList(data.name),\n          addMessageNotification(successMessages.DELETED_KEY(data.name)),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('editKey', () => {\n      it('call both editKey, editKeySuccess and editPatternKeyFromList when editing is successed', async () => {\n        // Arrange\n        const key = 'string'\n        const newKey = 'string2'\n        const responsePayload = { data: newKey, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(editKey(key, newKey))\n\n        // Assert\n        const expectedActions = [\n          defaultSelectedKeyAction(),\n          editPatternKeyFromList({ key, newKey }),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('editKeyTTL', () => {\n      it('success editKeyTTL with positive ttl', async () => {\n        // Arrange\n        const key = 'string'\n        const ttl = 1200\n        const responsePayload = { status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(editKeyTTL(key, ttl))\n\n        // Assert\n        const expectedActions = [\n          defaultSelectedKeyAction(),\n          // fetch keyInfo\n          editPatternKeyTTLFromList([key, ttl]),\n          defaultSelectedKeyAction(),\n          defaultSelectedKeyActionSuccess(),\n          loadKeyInfoSuccess({ data: '{}', keyName: 'keyName' }),\n          updateSelectedKeyRefreshTime(MOCK_TIMESTAMP),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('success editKeyTTL with ttl = 0', async () => {\n        // Arrange\n        const key = 'string'\n        const ttl = 0\n        const responsePayload = { status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(editKeyTTL(key, ttl))\n\n        // Assert\n        const expectedActions = [\n          defaultSelectedKeyAction(),\n          deleteSelectedKeySuccess(),\n          deletePatternKeyFromList(key),\n          defaultSelectedKeyActionSuccess(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    const shownColumnsTestCases = [\n      {\n        name: 'size and ttl',\n        shownColumns: [BrowserColumns.Size, BrowserColumns.TTL],\n      },\n      {\n        name: 'size only',\n        shownColumns: [BrowserColumns.Size],\n      },\n      {\n        name: 'ttl only',\n        shownColumns: [BrowserColumns.TTL],\n      },\n      {\n        name: 'no columns',\n        shownColumns: [],\n      },\n    ]\n    describe('fetchKeysMetadata', () => {\n      shownColumnsTestCases.forEach(({ name, shownColumns }) => {\n        it(`success to fetch keys metadata with ${name}`, async () => {\n          const initialStateWithColumns = {\n            ...initialStateDefault,\n            app: {\n              context: {\n                dbConfig: {\n                  shownColumns,\n                },\n              },\n            },\n          }\n\n          const testStore = configureStore({\n            reducer: rootReducer,\n            preloadedState: initialStateWithColumns,\n          })\n\n          const data = [\n            {\n              name: stringToBuffer('key1'),\n              type: 'hash',\n              ttl: -1,\n              size: 100,\n              length: 100,\n            },\n            {\n              name: stringToBuffer('key2'),\n              type: 'hash',\n              ttl: -1,\n              size: 150,\n              length: 100,\n            },\n            {\n              name: stringToBuffer('key3'),\n              type: 'hash',\n              ttl: -1,\n              size: 110,\n              length: 100,\n            },\n          ]\n          const responsePayload = { data, status: 200 }\n\n          const apiServiceMock = jest.fn().mockResolvedValue(responsePayload)\n          const onSuccessMock = jest.fn()\n          apiService.post = apiServiceMock\n          const controller = new AbortController()\n\n          // Act\n          await testStore.dispatch<any>(\n            fetchKeysMetadata(\n              data.map(({ name }) => ({ name })),\n              null,\n              controller.signal,\n              onSuccessMock,\n            ),\n          )\n\n          const expectedData = {\n            keys: data.map(({ name }) => ({ name })),\n            type: undefined,\n          }\n\n          expectedData.includeTTL = shownColumns.includes(BrowserColumns.TTL)\n          expectedData.includeSize = shownColumns.includes(BrowserColumns.Size)\n\n          expect(apiServiceMock).toBeCalledWith(\n            '/databases//keys/get-metadata',\n            expectedData,\n            { params: { encoding: 'buffer' }, signal: controller.signal },\n          )\n\n          expect(onSuccessMock).toBeCalledWith(data)\n        })\n      })\n    })\n\n    describe('fetchKeysMetadataTree', () => {\n      shownColumnsTestCases.forEach(({ name, shownColumns }) => {\n        it(`success to fetch keys metadata with ${name}`, async () => {\n          const initialStateWithColumns = {\n            ...initialStateDefault,\n            app: {\n              context: {\n                dbConfig: {\n                  shownColumns,\n                },\n              },\n            },\n          }\n\n          const testStore = configureStore({\n            reducer: rootReducer,\n            preloadedState: initialStateWithColumns,\n          })\n\n          const data = [\n            {\n              name: stringToBuffer('key1'),\n              type: 'hash',\n              ttl: -1,\n              size: 100,\n              path: 0,\n              length: 100,\n            },\n            {\n              name: stringToBuffer('key2'),\n              type: 'hash',\n              ttl: -1,\n              size: 150,\n              path: 1,\n              length: 100,\n            },\n            {\n              name: stringToBuffer('key3'),\n              type: 'hash',\n              ttl: -1,\n              size: 110,\n              path: 2,\n              length: 100,\n            },\n          ]\n\n          const responsePayload = { data, status: 200 }\n          const apiServiceMock = jest.fn().mockResolvedValue(responsePayload)\n          const onSuccessMock = jest.fn()\n          apiService.post = apiServiceMock\n          const controller = new AbortController()\n\n          // Act\n          await testStore.dispatch<any>(\n            fetchKeysMetadataTree(\n              data.map(({ name }, i) => [i, name]),\n              null,\n              controller.signal,\n              onSuccessMock,\n            ),\n          )\n\n          const expectedData = {\n            keys: data.map(({ name }) => name),\n            type: undefined,\n          }\n\n          expectedData.includeTTL = shownColumns.includes(BrowserColumns.TTL)\n          expectedData.includeSize = shownColumns.includes(BrowserColumns.Size)\n\n          // Assert\n          expect(apiServiceMock).toBeCalledWith(\n            '/databases//keys/get-metadata',\n            expectedData,\n            { params: { encoding: 'buffer' }, signal: controller.signal },\n          )\n\n          expect(onSuccessMock).toBeCalledWith(data)\n        })\n      })\n    })\n\n    describe('addKeyIntoList', () => {\n      it('updateKeyList should be called', async () => {\n        // Act\n        await store.dispatch<any>(\n          addKeyIntoList({ key: 'key', keyType: 'hash' }),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateKeyList({ keyName: 'key', keyType: 'hash' }),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchPatternHistoryAction', () => {\n      it('success fetch history', async () => {\n        // Arrange\n        const data: SearchHistoryItem[] = [\n          {\n            id: '1',\n            mode: SearchMode.Pattern,\n            filter: { type: 'list', match: '*' },\n          },\n          {\n            id: '2',\n            mode: SearchMode.Pattern,\n            filter: { type: 'list', match: '*' },\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPatternHistoryAction())\n\n        // Assert\n        const expectedActions = [\n          loadSearchHistory(),\n          loadSearchHistorySuccess(data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('failed to load history', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPatternHistoryAction())\n\n        // Assert\n        const expectedActions = [\n          loadSearchHistory(),\n          loadSearchHistoryFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchSearchHistoryAction', () => {\n      it('success fetch history', async () => {\n        // Arrange\n        const data: SearchHistoryItem[] = [\n          {\n            id: '1',\n            mode: SearchMode.Pattern,\n            filter: { type: 'list', match: '*' },\n          },\n          {\n            id: '2',\n            mode: SearchMode.Pattern,\n            filter: { type: 'list', match: '*' },\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSearchHistoryAction(SearchMode.Pattern))\n\n        // Assert\n        const expectedActions = [\n          loadSearchHistory(),\n          loadSearchHistorySuccess(data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('failed to load history', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSearchHistoryAction(SearchMode.Pattern))\n\n        // Assert\n        const expectedActions = [\n          loadSearchHistory(),\n          loadSearchHistoryFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deletePatternHistoryAction', () => {\n      it('success delete history', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deletePatternHistoryAction(['1']))\n\n        // Assert\n        const expectedActions = [\n          deleteSearchHistory(),\n          deleteSearchHistorySuccess(['1']),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to delete history', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deletePatternHistoryAction(['1']))\n\n        // Assert\n        const expectedActions = [\n          deleteSearchHistory(),\n          deleteSearchHistoryFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchNamespaceSearchable', () => {\n      it('should call API with correct prefixes and invoke onSuccess', async () => {\n        const prefixes: [string, string][] = [\n          ['0.0', 'user:'],\n          ['0.1', 'session:'],\n        ]\n        const apiResponse = [\n          { prefix: 'user:', key: { name: 'user:1', type: 'hash' } },\n          { prefix: 'session:' },\n        ]\n        const responsePayload = { data: apiResponse, status: 200 }\n        const apiServiceMock = jest.fn().mockResolvedValue(responsePayload)\n        const onSuccessMock = jest.fn()\n        apiService.post = apiServiceMock\n\n        await store.dispatch<any>(\n          fetchNamespaceSearchable(prefixes, undefined, onSuccessMock),\n        )\n\n        expect(apiServiceMock).toBeCalledWith(\n          '/databases//keys/get-namespace-searchable',\n          { prefixes: ['user:', 'session:'] },\n          { signal: undefined },\n        )\n\n        expect(onSuccessMock).toBeCalledWith([\n          {\n            prefix: 'user:',\n            key: { name: 'user:1', type: 'hash' },\n            path: '0.0',\n          },\n          { prefix: 'session:', path: '0.1' },\n        ])\n      })\n\n      it('should call onFail on error', async () => {\n        const prefixes: [string, string][] = [['0.0', 'user:']]\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: 'Internal error' },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n        const onFailMock = jest.fn()\n\n        await store.dispatch<any>(\n          fetchNamespaceSearchable(prefixes, undefined, undefined, onFailMock),\n        )\n\n        expect(onFailMock).toHaveBeenCalled()\n      })\n\n      it('should not throw or call onFail on cancelled request', async () => {\n        const prefixes: [string, string][] = [['0.0', 'user:']]\n        const cancelError = { __CANCEL__: true }\n        Object.defineProperty(cancelError, '__CANCEL__', { value: true })\n        apiService.post = jest.fn().mockRejectedValue(cancelError)\n        const onFailMock = jest.fn()\n\n        await store.dispatch<any>(\n          fetchNamespaceSearchable(prefixes, undefined, undefined, onFailMock),\n        )\n\n        expect(onFailMock).not.toHaveBeenCalled()\n      })\n    })\n\n    describe('deleteSearchHistoryAction', () => {\n      it('success delete history', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          deleteSearchHistoryAction(SearchMode.Pattern, ['1']),\n        )\n\n        // Assert\n        const expectedActions = [\n          deleteSearchHistory(),\n          deleteSearchHistorySuccess(['1']),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to delete history', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          deleteSearchHistoryAction(SearchMode.Pattern, ['1']),\n        )\n\n        // Assert\n        const expectedActions = [\n          deleteSearchHistory(),\n          deleteSearchHistoryFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/list.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { deleteRedisearchKeyFromList } from 'uiSrc/slices/browser/redisearch'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport {\n  DeleteListElementsDto,\n  PushElementToListDto,\n} from 'apiSrc/modules/browser/list/dto'\nimport {\n  defaultSelectedKeyAction,\n  deleteSelectedKeySuccess,\n  refreshKeyInfo,\n  updateSelectedKeyRefreshTime,\n} from '../../browser/keys'\nimport reducer, {\n  initialState,\n  setListInitialState,\n  loadListElements,\n  loadListElementsSuccess,\n  loadListElementsFailure,\n  loadMoreListElements,\n  loadMoreListElementsSuccess,\n  loadMoreListElementsFailure,\n  updateValue,\n  updateValueSuccess,\n  updateValueFailure,\n  resetUpdateValue,\n  updateElementInList,\n  loadSearchingListElement,\n  loadSearchingListElementSuccess,\n  loadSearchingListElementFailure,\n  insertListElements,\n  insertListElementsSuccess,\n  insertListElementsFailure,\n  listSelector,\n  fetchListElements,\n  fetchMoreListElements,\n  fetchSearchingListElementAction,\n  refreshListElementsAction,\n  updateListElementAction,\n  insertListElementsAction,\n  deleteListElementsAction,\n  deleteListElements,\n  deleteListElementsSuccess,\n  deleteListElementsFailure,\n} from '../../browser/list'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('list slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('setListInitialState', () => {\n    it('should properly set initialState', () => {\n      // Arrange\n      const state = initialState\n\n      // Act\n      const nextState = reducer(initialState, setListInitialState())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadListElements', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadListElements())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadListElementsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        key: stringToBuffer('list'),\n        keyName: stringToBuffer('list'),\n        elements: ['1', '2', '3'].map((element) => stringToBuffer(element)),\n        total: 1,\n        count: 123,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...initialState.data,\n          ...data,\n          elements: data.elements.map((element, i) => ({ index: i, element })),\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadListElementsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadListElementsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadListElementsFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreListElements', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreListElements())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreListElementsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        keyName: 'list',\n        key: 'list',\n        elements: ['1', '23', '432'].map((element) => stringToBuffer(element)),\n        // elements: ['1', '23', '432'].map((element, i) => ({ element: stringToBuffer(element), index: i })),\n        total: 1,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          elements: data.elements\n            .concat(data.elements)\n            .map((element, i) => ({ element, index: i })),\n        },\n      }\n\n      const initialStateWithElements = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          elements: data.elements.map((element, i) => ({ element, index: i })),\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithElements,\n        loadMoreListElementsSuccess(data),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: 'hash',\n        elements: [],\n        total: 0,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreListElementsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('loadMoreListElementsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreListElementsFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateValue', () => {\n    it('should properly set the state while updating a list key', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: true,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValue())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateValueSuccess', () => {\n    it('should properly set the state after successfully updated list key', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateValueFailure', () => {\n    it('should properly set the state on update list key failure', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetUpdateValue', () => {\n    it('should properly reset the state', () => {\n      // Arrange;\n      const state = {\n        ...initialState,\n        updateValue: {\n          ...initialState.updateValue,\n          loading: false,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetUpdateValue())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSearchingListElement', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const searchedIndex = 10\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n        data: {\n          ...initialState.data,\n          searchedIndex,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSearchingListElement(searchedIndex),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSearchingListElementSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        keyName: stringToBuffer('list'),\n        value: stringToBuffer('12311'),\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          elements: [{ element: data.value, index: 0 }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSearchingListElementSuccess([0, data]),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: stringToBuffer('list'),\n        value: stringToBuffer(''),\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          elements: [{ element: data.value, index: 0 }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSearchingListElementSuccess([0, data]),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSearchingListElementFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSearchingListElementFailure(data),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('insertListElements', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n      // Act\n      const nextState = reducer(initialState, insertListElements())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('insertListElementsSuccess', () => {\n    it('should properly set the state after the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n      // Act\n      const nextState = reducer(initialState, insertListElementsSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('insertListElementsFailure', () => {\n    it('should properly set the error after the fetch data', () => {\n      // Arrange\n      const error = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, insertListElementsFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteListElements', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n      // Act\n      const nextState = reducer(initialState, deleteListElements())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteListElementsSuccess', () => {\n    it('should properly set the state after the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n      // Act\n      const nextState = reducer(initialState, deleteListElementsSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteListElementsFailure', () => {\n    it('should properly set the error after the fetch data', () => {\n      // Arrange\n      const error = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteListElementsFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { list: nextState },\n      }\n      expect(listSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchListElements', () => {\n      it('call fetchListElements, loadListElementsSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          key: 'small list',\n          keyName: 'small list',\n          elements: ['123', '123', '321'],\n          total: 3,\n        }\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchListElements(data.keyName, 0, 20))\n\n        // Assert\n        const expectedActions = [\n          loadListElements(),\n          loadListElementsSuccess(responsePayload.data),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchMoreListElements', () => {\n      it('call fetchMoreListElements, loadMoreListElementsSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          keyName: 'small list',\n          elements: ['123', '123', '321'],\n          total: 3,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchMoreListElements(data.keyName, 0, 20))\n\n        // Assert\n        const expectedActions = [\n          loadMoreListElements(),\n          loadMoreListElementsSuccess(responsePayload.data),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchSearchingListElementAction', () => {\n      it('call fetchSearchingListElementAction, loadSearchingListElementSuccess when search is successed', async () => {\n        // Arrange\n        const searchingIndex = 10\n        const data = {\n          keyName: stringToBuffer('small list'),\n          value: stringToBuffer('value'),\n        }\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchSearchingListElementAction(data.keyName, searchingIndex),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadSearchingListElement(searchingIndex),\n          loadSearchingListElementSuccess([\n            searchingIndex,\n            responsePayload.data,\n          ]),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('refreshListElementsAction', () => {\n      it('call refreshListElementsAction without searchingIndex, call loadListElements when fetch is successed', async () => {\n        // Act\n        await store.dispatch<any>(refreshListElementsAction())\n\n        // Assert\n        const expectedActions = [loadListElements()]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n    })\n\n    describe('updateListElementAction', () => {\n      const keyName = 'key'\n      const data = {\n        keyName,\n        index: 123,\n        element: 'value',\n      }\n      it('succeed to update element in list', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(updateListElementAction(data))\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          updateValueSuccess(),\n          updateElementInList(data),\n          refreshKeyInfo(),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n      it('failed to update element in list', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(updateListElementAction(data))\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateValueFailure(errorMessage),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('insertListElementsAction', () => {\n      const data = {\n        keyName: 'keyName',\n        destination: 'TAIL',\n        element: 'value',\n      } as PushElementToListDto\n      it('succeed to insert element in list', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n        apiService.put = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(insertListElementsAction(data))\n\n        // Assert\n        const expectedActions = [\n          insertListElements(),\n          insertListElementsSuccess(),\n          defaultSelectedKeyAction(),\n        ]\n\n        expect(\n          mockedStore.getActions().slice(0, expectedActions.length),\n        ).toEqual(expectedActions)\n      })\n      it('failed to insert element in list', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(insertListElementsAction(data))\n\n        // Assert\n        const expectedActions = [\n          insertListElements(),\n          addErrorNotification(responsePayload as AxiosError),\n          insertListElementsFailure(errorMessage),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteListElementsAction', () => {\n      const data = {\n        keyName: 'keyName',\n        destination: 'TAIL',\n        count: 2,\n      } as DeleteListElementsDto\n      it('succeed to delete elements from list', async () => {\n        // Arrange\n        const responsePayload = {\n          status: 200,\n          data: { elements: ['zx', 'zz'] },\n        }\n        const nextState = {\n          ...initialStateDefault,\n          browser: {\n            ...initialStateDefault.browser,\n            list: {\n              ...initialState,\n              data: {\n                ...initialState.data,\n                total: 10,\n              },\n            },\n          },\n        }\n\n        const mockedStore = mockStore(nextState)\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await mockedStore.dispatch<any>(deleteListElementsAction(data))\n\n        // Assert\n        const expectedActions = [\n          deleteListElements(),\n          deleteListElementsSuccess(),\n          defaultSelectedKeyAction(),\n          addMessageNotification(\n            successMessages.REMOVED_LIST_ELEMENTS(\n              data.keyName,\n              data.count,\n              responsePayload.data.elements,\n            ),\n          ),\n        ]\n\n        expect(\n          mockedStore.getActions().slice(0, expectedActions.length),\n        ).toEqual(expectedActions)\n      })\n\n      it('succeed to delete all elements from list', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n        const nextState = {\n          ...initialStateDefault,\n          browser: {\n            list: {\n              ...initialState,\n              data: {\n                ...initialState.data,\n                total: 2,\n              },\n            },\n          },\n        }\n\n        const mockedStore = mockStore(nextState)\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await mockedStore.dispatch<any>(deleteListElementsAction(data))\n\n        // Assert\n        const expectedActions = [\n          deleteListElements(),\n          deleteListElementsSuccess(),\n          deleteSelectedKeySuccess(),\n          deleteRedisearchKeyFromList(data.keyName),\n          addMessageNotification(successMessages.DELETED_KEY(data.keyName)),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to delete elements in list', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteListElementsAction(data))\n\n        // Assert\n        const expectedActions = [\n          deleteListElements(),\n          addErrorNotification(responsePayload as AxiosError),\n          deleteListElementsFailure(errorMessage),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep, omit } from 'lodash'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { stringToBuffer, UTF8ToBuffer } from 'uiSrc/utils'\nimport { REDISEARCH_LIST_DATA_MOCK } from 'uiSrc/mocks/handlers/browser/redisearchHandlers'\nimport { SearchHistoryItem, SearchMode } from 'uiSrc/slices/interfaces/keys'\nimport { fetchKeys, fetchMoreKeys } from 'uiSrc/slices/browser/keys'\nimport { initialState as initialStateInstances } from 'uiSrc/slices/instances/instances'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport { IndexDeleteRequestBodyDto } from 'apiSrc/modules/browser/redisearch/dto'\nimport reducer, {\n  initialState,\n  loadKeys,\n  loadKeysSuccess,\n  loadKeysFailure,\n  loadMoreKeys,\n  loadMoreKeysSuccess,\n  loadMoreKeysFailure,\n  loadList,\n  loadListSuccess,\n  loadListFailure,\n  setSelectedIndex,\n  setLastBatchRedisearchKeys,\n  setQueryRedisearch,\n  createIndex,\n  createIndexSuccess,\n  createIndexFailure,\n  fetchRedisearchListAction,\n  createRedisearchIndexAction,\n  redisearchDataSelector,\n  redisearchSelector,\n  setRedisearchInitialState,\n  resetRedisearchKeysData,\n  deleteRedisearchKeyFromList,\n  editRedisearchKeyFromList,\n  editRedisearchKeyTTLFromList,\n  loadRediSearchHistory,\n  loadRediSearchHistorySuccess,\n  loadRediSearchHistoryFailure,\n  deleteRediSearchHistory,\n  deleteRediSearchHistorySuccess,\n  deleteRediSearchHistoryFailure,\n  fetchRedisearchHistoryAction,\n  deleteRedisearchHistoryAction,\n  fetchRedisearchInfoAction,\n  deleteRedisearchIndexAction,\n  loadKeyIndexes,\n  loadKeyIndexesSuccess,\n  loadKeyIndexesFailure,\n  resetKeyIndexes,\n  keyIndexesSelector,\n  fetchKeyIndexesAction,\n} from '../../browser/redisearch'\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\ndescribe('redisearch slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadKeys', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: {\n          redisearch: nextState,\n        },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n      expect(redisearchDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('loadKeysSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const data = {\n        total: 249,\n        cursor: 228,\n        scanned: 228,\n        keys: [\n          {\n            name: stringToBuffer('bull:mail-queue:155'),\n            type: 'hash',\n            ttl: 2147474450,\n            size: 3041,\n          },\n          {\n            name: stringToBuffer('bull:mail-queue:223'),\n            type: 'hash',\n            ttl: -1,\n            size: 3041,\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...data,\n          shardsMeta: {},\n          nextCursor: '228',\n          lastRefreshTime: Date.now(),\n          previousResultCount: data.keys.length,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeysSuccess([data, false]))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadKeysFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeysFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n\n    it('should not properly set the error if no payload', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        error: initialState.error,\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadKeysFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreKeys', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreKeysSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        total: 0,\n        cursor: 0,\n        shardsMeta: {},\n        scanned: 0,\n        keys: [\n          {\n            name: stringToBuffer('bull:mail-queue:155'),\n            type: 'hash',\n            ttl: 2147474450,\n            size: 3041,\n          },\n          {\n            name: stringToBuffer('bull:mail-queue:223'),\n            type: 'hash',\n            ttl: -1,\n            size: 3041,\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...omit(data, 'cursor'),\n          nextCursor: `${data.cursor}`,\n          previousResultCount: data.keys.length,\n          lastRefreshTime: initialState.data.lastRefreshTime,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        total: 0,\n        cursor: '0',\n        keys: [],\n        scanned: 0,\n        shardsMeta: {},\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('loadMoreKeysFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n\n    it('should not properly set the error if no payload', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        error: initialState.error,\n        data: {\n          ...initialState.data,\n          keys: [],\n          nextCursor: '0',\n          total: 0,\n          scanned: 0,\n          shardsMeta: {},\n          previousResultCount: 0,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreKeysFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadList', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        list: {\n          ...initialState.list,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadList())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: {\n          redisearch: nextState,\n        },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n      expect(redisearchDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('loadListSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const data = REDISEARCH_LIST_DATA_MOCK\n      const state = {\n        ...initialState,\n        list: {\n          data,\n          error: '',\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadListSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: {\n          redisearch: nextState,\n        },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n      expect(redisearchDataSelector(rootState)).toEqual(state.data)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any[] = []\n\n      const state = {\n        ...initialState,\n        list: {\n          data,\n          error: '',\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadListSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: {\n          redisearch: nextState,\n        },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n      expect(redisearchDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('loadListFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        list: {\n          data: [],\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadListFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: {\n          redisearch: nextState,\n        },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n      expect(redisearchDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('setSelectedIndex', () => {\n    it('should properly set the selected index', () => {\n      // Arrange\n\n      const index = stringToBuffer('idx')\n      const state = {\n        ...initialState,\n        selectedIndex: index,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSelectedIndex(index))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setQueryRedisearch', () => {\n    it('should properly set the selected index', () => {\n      // Arrange\n\n      const query = 'query'\n      const state = {\n        ...initialState,\n        search: query,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setQueryRedisearch(query))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setLastBatchKeys', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const strToKey = (name: string) => ({\n        name,\n        nameString: name,\n        ttl: 1,\n        size: 1,\n        type: 'hash',\n      })\n      const data = ['44', '55', '66'].map(strToKey)\n\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '44', '55', '66'].map(strToKey),\n        },\n      }\n\n      const prevState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '4', '5', '6'].map(strToKey),\n        },\n      }\n\n      // Act\n      const nextState = reducer(prevState, setLastBatchRedisearchKeys(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createIndex', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        createIndex: {\n          ...initialState.createIndex,\n          loading: true,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, createIndex())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: {\n          redisearch: nextState,\n        },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createIndexSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        createIndex: {\n          ...initialState.createIndex,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, createIndexSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadKeysFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        createIndex: {\n          ...initialState.createIndex,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, createIndexFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetRedisearchKeysData', () => {\n    it('should reset keys data', () => {\n      const strToKey = (name: string) => ({\n        name,\n        nameString: name,\n        ttl: 1,\n        size: 1,\n        type: 'hash',\n      })\n\n      // Arrange\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: [],\n        },\n      }\n\n      const prevState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '4', '5', '6'].map(strToKey),\n        },\n      }\n\n      // Act\n      const nextState = reducer(prevState, resetRedisearchKeysData())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteRedisearchKeyFromList', () => {\n    it('should delete keys from list', () => {\n      const scanned = 5\n      const total = 5\n      const strToKey = (name: string) => ({\n        name: stringToBuffer(name),\n        nameString: name,\n        ttl: 1,\n        size: 1,\n        type: 'hash',\n      })\n\n      // Arrange\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '5', '6'].map(strToKey),\n          scanned: scanned - 1,\n          total: total - 1,\n        },\n      }\n\n      const prevState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          scanned,\n          total,\n          keys: ['1', '2', '3', '4', '5', '6'].map(strToKey),\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        prevState,\n        deleteRedisearchKeyFromList(strToKey('4')?.name),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('editRedisearchKeyFromList', () => {\n    it('should rename key in the list', () => {\n      const strToKey = (name: string) => ({\n        name: stringToBuffer(name),\n        nameString: name,\n        ttl: 1,\n        size: 1,\n        type: 'hash',\n      })\n\n      // Arrange\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '44', '5', '6'].map(strToKey),\n        },\n      }\n\n      const prevState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: ['1', '2', '3', '4', '5', '6'].map(strToKey),\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        prevState,\n        editRedisearchKeyFromList({\n          key: strToKey('4')?.name,\n          newKey: strToKey('44')?.name,\n        }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('editRedisearchKeyTTLFromList', () => {\n    it('should properly set the state before the edit key ttl', () => {\n      // Arrange\n\n      const key = UTF8ToBuffer('key')\n      const ttl = 12000\n\n      const prevState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: [{ name: key }],\n        },\n      }\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          keys: [{ name: key, ttl }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        prevState,\n        editRedisearchKeyTTLFromList([key, ttl]),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadRediSearchHistory', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, loadRediSearchHistory())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadRediSearchHistorySuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data: SearchHistoryItem[] = [\n        {\n          id: '1',\n          mode: SearchMode.Redisearch,\n          filter: { type: 'list', match: '*' },\n        },\n      ]\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n          data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, loadRediSearchHistorySuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadRediSearchHistoryFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, loadRediSearchHistoryFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteRediSearchHistory', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, deleteRediSearchHistory())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteRediSearchHistorySuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data: SearchHistoryItem[] = [\n        {\n          id: '1',\n          mode: SearchMode.Redisearch,\n          filter: { type: 'list', match: '*' },\n        },\n        {\n          id: '2',\n          mode: SearchMode.Redisearch,\n          filter: { type: 'list', match: '*' },\n        },\n      ]\n      const currentState = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n          data,\n        },\n      }\n\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n          data: [\n            {\n              id: '1',\n              mode: SearchMode.Redisearch,\n              filter: { type: 'list', match: '*' },\n            },\n          ],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        deleteRediSearchHistorySuccess(['2']),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteRediSearchHistoryFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        searchHistory: {\n          ...initialState.searchHistory,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, deleteRediSearchHistoryFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        browser: { redisearch: nextState },\n      })\n      expect(redisearchSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchRedisearchListAction', () => {\n      it('call both fetchRedisearchListAction, loadListSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = REDISEARCH_LIST_DATA_MOCK\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRedisearchListAction())\n\n        // Assert\n        const expectedActions = [\n          loadList(),\n          loadListSuccess(responsePayload.data.indexes),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchRedisearchKeysAction', () => {\n      it('call both loadKeys and loadKeysSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          total: 10,\n          cursor: 20,\n          scanned: 20,\n          keys: [\n            {\n              name: stringToBuffer('bull:mail-queue:155'),\n              type: 'hash',\n              ttl: 2147474450,\n              size: 3041,\n            },\n            {\n              name: stringToBuffer('bull:mail-queue:223'),\n              type: 'hash',\n              ttl: -1,\n              size: 3041,\n            },\n          ],\n        }\n        const responsePayload = {\n          data: {\n            total: data.total,\n            scanned: data.scanned,\n            cursor: 20,\n            keys: [...data.keys],\n          },\n          status: 200,\n        }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        const newStore = mockStore({\n          ...initialStateDefault,\n          connections: {\n            instances: {\n              ...cloneDeep(initialStateInstances),\n              connectedInstance: {\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n        })\n\n        // Act\n        await newStore.dispatch<any>(\n          fetchKeys({\n            searchMode: SearchMode.Redisearch,\n            cursor: '0',\n            count: 20,\n          }),\n        )\n\n        // Assert\n        const expectedActions = [loadKeys(), loadKeysSuccess([data, false])]\n        expect(newStore.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to load keys', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        const newStore = mockStore({\n          ...initialStateDefault,\n          connections: {\n            instances: {\n              ...cloneDeep(initialStateInstances),\n              connectedInstance: {\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n        })\n\n        // Act\n        await newStore.dispatch<any>(\n          fetchKeys({\n            searchMode: SearchMode.Redisearch,\n            cursor: '0',\n            count: 20,\n          }),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadKeysFailure(errorMessage),\n        ]\n        expect(newStore.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to load keys: Index not found', async () => {\n        // Arrange\n        const errorMessage = 'idx: no such index'\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        const newStore = mockStore({\n          ...initialStateDefault,\n          connections: {\n            instances: {\n              ...cloneDeep(initialStateInstances),\n              connectedInstance: {\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n        })\n\n        // Act\n        await newStore.dispatch<any>(\n          fetchKeys({\n            searchMode: SearchMode.Redisearch,\n            cursor: '0',\n            count: 20,\n          }),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadKeysFailure(errorMessage),\n          setRedisearchInitialState(),\n          loadList(),\n          loadListSuccess(REDISEARCH_LIST_DATA_MOCK.indexes),\n        ]\n        expect(newStore.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchMoreRedisearchKeysAction', () => {\n      it('call both loadMoreKeys and loadMoreKeysSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = {\n          total: 0,\n          nextCursor: '0',\n          scanned: 20,\n          shardsMeta: {},\n          keys: [\n            {\n              name: stringToBuffer('bull:mail-queue:155'),\n              type: 'hash',\n              ttl: 2147474450,\n              size: 3041,\n            },\n            {\n              name: stringToBuffer('bull:mail-queue:223'),\n              type: 'hash',\n              ttl: -1,\n              size: 3041,\n            },\n          ],\n        }\n\n        const responsePayload = {\n          data: {\n            total: data.total,\n            scanned: data.scanned,\n            cursor: 20,\n            keys: [...data.keys],\n          },\n          status: 200,\n        }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchMoreKeys(SearchMode.Redisearch, [], '0', 20),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadMoreKeys(),\n          loadMoreKeysSuccess(responsePayload.data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch more keys', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchMoreKeys(SearchMode.Redisearch, [], '0', 20),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadMoreKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadMoreKeysFailure(errorMessage),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('createRedisearchIndexAction', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const data = {\n          index: stringToBuffer('index'),\n          type: 'hash',\n          prefixes: ['prefix1', 'prefix 2'].map((p) => stringToBuffer(p)),\n          fields: [{ name: stringToBuffer('field'), type: 'numeric' }],\n        }\n\n        const responsePayload = { status: 200 }\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(createRedisearchIndexAction(data))\n\n        // Assert\n        const expectedActions = [\n          createIndex(),\n          createIndexSuccess(),\n          addMessageNotification(successMessages.CREATE_INDEX()),\n          loadList(),\n          loadListSuccess(REDISEARCH_LIST_DATA_MOCK.indexes),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to create index', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(createRedisearchIndexAction({}))\n\n        // Assert\n        const expectedActions = [\n          createIndex(),\n          addErrorNotification(responsePayload as AxiosError),\n          createIndexFailure(errorMessage),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchRedisearchHistoryAction', () => {\n      it('success fetch history', async () => {\n        // Arrange\n        const data: SearchHistoryItem[] = [\n          {\n            id: '1',\n            mode: SearchMode.Redisearch,\n            filter: { type: 'list', match: '*' },\n          },\n          {\n            id: '2',\n            mode: SearchMode.Redisearch,\n            filter: { type: 'list', match: '*' },\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRedisearchHistoryAction())\n\n        // Assert\n        const expectedActions = [\n          loadRediSearchHistory(),\n          loadRediSearchHistorySuccess(data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('failed to load history', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRedisearchHistoryAction())\n\n        // Assert\n        const expectedActions = [\n          loadRediSearchHistory(),\n          loadRediSearchHistoryFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteRedisearchHistoryAction', () => {\n      it('success delete history', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteRedisearchHistoryAction(['1']))\n\n        // Assert\n        const expectedActions = [\n          deleteRediSearchHistory(),\n          deleteRediSearchHistorySuccess(['1']),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to delete history', async () => {\n        // Arrange\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteRedisearchHistoryAction(['1']))\n\n        // Assert\n        const expectedActions = [\n          deleteRediSearchHistory(),\n          deleteRediSearchHistoryFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchRedisearchInfoAction', () => {\n      it('success fetch info', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n        const onSuccess = jest.fn()\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRedisearchInfoAction('index', onSuccess))\n\n        expect(onSuccess).toBeCalled()\n      })\n\n      it('failed to delete history', async () => {\n        // Arrange\n        const onFailed = jest.fn()\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchRedisearchInfoAction('index', undefined, onFailed),\n        )\n\n        // Assert\n        expect(onFailed).toBeCalled()\n      })\n    })\n\n    describe('deleteRedisearchIndexAction', () => {\n      const indexName = 'index'\n      const deleteIndexRequestPayload: IndexDeleteRequestBodyDto = {\n        index: indexName,\n      }\n\n      const mockSuccessCallback = jest.fn()\n      const mockErrorCallback = jest.fn()\n\n      beforeEach(() => {\n        store.clearActions()\n        jest.clearAllMocks()\n      })\n\n      it('should delete index successfully', async () => {\n        // Arrange\n        const responsePayload = { status: 204 }\n        const listResponsePayload = {\n          data: REDISEARCH_LIST_DATA_MOCK,\n          status: 200,\n        }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n        apiService.get = jest.fn().mockResolvedValue(listResponsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          deleteRedisearchIndexAction(\n            deleteIndexRequestPayload,\n            mockSuccessCallback,\n            mockErrorCallback,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          addMessageNotification(successMessages.DELETE_INDEX(indexName)),\n          loadList(),\n          loadListSuccess(REDISEARCH_LIST_DATA_MOCK.indexes),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n        expect(mockSuccessCallback).toHaveBeenCalled()\n        expect(mockErrorCallback).not.toHaveBeenCalled()\n      })\n\n      it('should fail to delete index', async () => {\n        // Arrange\n        const errorMessage = 'Mock error message'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          deleteRedisearchIndexAction(\n            deleteIndexRequestPayload,\n            mockSuccessCallback,\n            mockErrorCallback,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          {\n            type: 'notifications/addErrorNotification',\n            payload: responsePayload,\n          },\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n        expect(mockSuccessCallback).not.toHaveBeenCalled()\n        expect(mockErrorCallback).toHaveBeenCalled()\n      })\n    })\n\n    describe('fetchKeyIndexesAction', () => {\n      const storeWithSearchModule = () =>\n        mockStore({\n          ...initialStateDefault,\n          connections: {\n            instances: {\n              ...cloneDeep(initialStateInstances),\n              connectedInstance: {\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n        })\n\n      it('should dispatch loadKeyIndexes and loadKeyIndexesSuccess on success', async () => {\n        const keyName = 'movie:1'\n        const responseData = {\n          indexes: [\n            { name: 'idx:movie', prefixes: ['movie:'], key_type: 'HASH' },\n          ],\n        }\n        const responsePayload = { data: responseData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        const searchStore = storeWithSearchModule()\n        await searchStore.dispatch<any>(fetchKeyIndexesAction(keyName))\n\n        const expectedActions = [\n          loadKeyIndexes(keyName),\n          loadKeyIndexesSuccess([\n            keyName,\n            [{ name: 'idx:movie', prefixes: ['movie:'], keyType: 'HASH' }],\n          ]),\n        ]\n\n        expect(searchStore.getActions()).toEqual(expectedActions)\n      })\n\n      it('should dispatch loadKeyIndexesFailure on error', async () => {\n        const keyName = 'movie:1'\n        const errorMessage = 'Request failed with status code 500'\n        apiService.post = jest\n          .fn()\n          .mockRejectedValue(new AxiosError(errorMessage))\n\n        const searchStore = storeWithSearchModule()\n        await searchStore.dispatch<any>(fetchKeyIndexesAction(keyName))\n\n        const actions = searchStore.getActions()\n        expect(actions[0]).toEqual(loadKeyIndexes(keyName))\n        expect(actions[1].type).toEqual(loadKeyIndexesFailure.type)\n        expect(actions[1].payload[0]).toEqual(keyName)\n      })\n\n      it('should dispatch loadKeyIndexesSuccess with empty data when redisearch module is not available', async () => {\n        const keyName = 'movie:1'\n\n        await store.dispatch<any>(fetchKeyIndexesAction(keyName))\n\n        const actions = store.getActions()\n        expect(actions).toEqual([loadKeyIndexesSuccess([keyName, []])])\n      })\n\n      it('should not dispatch when entry is already loaded', async () => {\n        const keyName = 'movie:1'\n        const storeWithEntry = mockStore({\n          ...initialStateDefault,\n          connections: {\n            instances: {\n              ...cloneDeep(initialStateInstances),\n              connectedInstance: {\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n          browser: {\n            ...initialStateDefault.browser,\n            redisearch: {\n              ...initialState,\n              keyIndexes: {\n                [keyName]: {\n                  loading: false,\n                  data: [\n                    {\n                      name: 'idx:movie',\n                      prefixes: ['movie:'],\n                      keyType: 'HASH',\n                    },\n                  ],\n                  error: '',\n                },\n              },\n            },\n          },\n        })\n\n        await storeWithEntry.dispatch<any>(fetchKeyIndexesAction(keyName))\n\n        expect(storeWithEntry.getActions()).toEqual([])\n      })\n\n      it('should re-fetch when force=true even if already loaded', async () => {\n        const keyName = 'movie:1'\n        const responseData = {\n          indexes: [\n            { name: 'idx:movie', prefixes: ['movie:'], key_type: 'HASH' },\n          ],\n        }\n        apiService.post = jest\n          .fn()\n          .mockResolvedValue({ data: responseData, status: 200 })\n\n        const storeWithEntry = mockStore({\n          ...initialStateDefault,\n          connections: {\n            instances: {\n              ...cloneDeep(initialStateInstances),\n              connectedInstance: {\n                modules: [{ name: RedisDefaultModules.Search }],\n              },\n            },\n          },\n          browser: {\n            ...initialStateDefault.browser,\n            redisearch: {\n              ...initialState,\n              keyIndexes: {\n                [keyName]: {\n                  loading: false,\n                  data: [\n                    {\n                      name: 'idx:movie',\n                      prefixes: ['movie:'],\n                      keyType: 'HASH',\n                    },\n                  ],\n                  error: '',\n                },\n              },\n            },\n          },\n        })\n\n        await storeWithEntry.dispatch<any>(fetchKeyIndexesAction(keyName, true))\n\n        expect(storeWithEntry.getActions().length).toBeGreaterThan(0)\n        expect(storeWithEntry.getActions()[0]).toEqual(loadKeyIndexes(keyName))\n      })\n    })\n  })\n\n  describe('keyIndexes reducers', () => {\n    describe('loadKeyIndexes', () => {\n      it('should set loading state for a key', () => {\n        const nextState = reducer(initialState, loadKeyIndexes('movie:1'))\n\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { redisearch: nextState },\n        })\n        const entry = keyIndexesSelector(rootState)['movie:1']\n        expect(entry.loading).toBe(true)\n        expect(entry.data).toEqual([])\n        expect(entry.error).toBe('')\n      })\n    })\n\n    describe('loadKeyIndexesSuccess', () => {\n      it('should set data for a key', () => {\n        const indexes = [\n          { name: 'idx:movie', prefixes: ['movie:'], keyType: 'HASH' },\n        ]\n        const nextState = reducer(\n          initialState,\n          loadKeyIndexesSuccess(['movie:1', indexes]),\n        )\n\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { redisearch: nextState },\n        })\n        const entry = keyIndexesSelector(rootState)['movie:1']\n        expect(entry.loading).toBe(false)\n        expect(entry.data).toEqual(indexes)\n        expect(entry.error).toBe('')\n      })\n    })\n\n    describe('loadKeyIndexesFailure', () => {\n      it('should set error for a key', () => {\n        const nextState = reducer(\n          initialState,\n          loadKeyIndexesFailure(['movie:1', 'Failed to fetch']),\n        )\n\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { redisearch: nextState },\n        })\n        const entry = keyIndexesSelector(rootState)['movie:1']\n        expect(entry.loading).toBe(false)\n        expect(entry.data).toEqual([])\n        expect(entry.error).toBe('Failed to fetch')\n      })\n    })\n\n    describe('resetKeyIndexes', () => {\n      it('should clear all key indexes', () => {\n        const stateWithData = reducer(\n          initialState,\n          loadKeyIndexesSuccess([\n            'movie:1',\n            [{ name: 'idx:movie', prefixes: ['movie:'], keyType: 'HASH' }],\n          ]),\n        )\n        const nextState = reducer(stateWithData, resetKeyIndexes())\n\n        const rootState = Object.assign(initialStateDefault, {\n          browser: { redisearch: nextState },\n        })\n        expect(keyIndexesSelector(rootState)).toEqual({})\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/rejson.setJsonDataAction.spec.ts",
    "content": "import thunk from 'redux-thunk'\nimport configureStore from 'redux-mock-store'\nimport { EditorType } from 'uiSrc/slices/interfaces'\n\nconst mockStore = configureStore([thunk])\n\ndescribe('setReJSONDataAction', () => {\n  let store: any\n  let sendEventTelemetryMock: jest.Mock\n  let setReJSONDataAction: any\n  let apiService: any\n\n  beforeEach(async () => {\n    jest.resetModules()\n\n    sendEventTelemetryMock = jest.fn()\n\n    jest.doMock('uiSrc/telemetry', () => {\n      const actual = jest.requireActual('uiSrc/telemetry')\n      return {\n        ...actual,\n        sendEventTelemetry: sendEventTelemetryMock,\n        getBasedOnViewTypeEvent: jest.fn(() => 'mocked_event'),\n      }\n    })\n\n    jest.doMock('uiSrc/slices/browser/keys', () => {\n      const actual = jest.requireActual('uiSrc/slices/browser/keys')\n      return {\n        ...actual,\n        refreshKeyInfoAction: () => ({ type: 'DUMMY_REFRESH' }),\n      }\n    })\n\n    const rejson = await import('uiSrc/slices/browser/rejson')\n    setReJSONDataAction = rejson.setReJSONDataAction\n    apiService = (await import('uiSrc/services')).apiService\n\n    store = mockStore({\n      browser: {\n        rejson: { editorType: 'Default' },\n        keys: { viewType: 'Browser' },\n      },\n      app: {\n        info: { encoding: 'utf8' },\n      },\n      connections: {\n        instances: {\n          connectedInstance: {\n            id: 'instance-id',\n          },\n        },\n      },\n    })\n\n    apiService.patch = jest.fn().mockResolvedValue({ status: 200 })\n    apiService.post = jest.fn().mockResolvedValue({ status: 200, data: {} })\n\n    jest.clearAllMocks()\n  })\n\n  it('should call sendEventTelemetry with correct args', async () => {\n    await store.dispatch(setReJSONDataAction('key', '$', '{}', true, 100))\n\n    expect(sendEventTelemetryMock).toHaveBeenCalledWith({\n      event: 'mocked_event',\n      eventData: {\n        databaseId: 'instance-id',\n        keyLevel: 0,\n      },\n    })\n  })\n\n  it('should set entireKey: true when editor is Text', async () => {\n    store = mockStore({\n      ...store.getState(),\n      browser: {\n        ...store.getState().browser,\n        rejson: { editorType: EditorType.Text },\n      },\n    })\n\n    await store.dispatch(setReJSONDataAction('key', '$', '{}', true, 100))\n\n    expect(sendEventTelemetryMock).toHaveBeenCalledWith(\n      expect.objectContaining({\n        eventData: expect.objectContaining({\n          keyLevel: 'entireKey',\n        }),\n      }),\n    )\n  })\n\n  it('should compute keyLevel from nested path', async () => {\n    const nestedPath = '$.foo.bar[1].nested.key' // 5 levels of nesting\n\n    await store.dispatch(\n      setReJSONDataAction('key', nestedPath, '{}', true, 100),\n    )\n\n    expect(sendEventTelemetryMock).toHaveBeenCalledWith(\n      expect.objectContaining({\n        eventData: expect.objectContaining({\n          keyLevel: 5,\n        }),\n      }),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/rejson.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { GetRejsonRlResponseDto } from 'apiSrc/modules/browser/rejson-rl/dto'\nimport reducer, {\n  initialState,\n  loadRejsonBranch,\n  loadRejsonBranchSuccess,\n  loadRejsonBranchFailure,\n  appendReJSONArrayItem,\n  appendReJSONArrayItemSuccess,\n  appendReJSONArrayItemFailure,\n  setReJSONData,\n  setReJSONDataSuccess,\n  setReJSONDataFailure,\n  removeRejsonKey,\n  removeRejsonKeySuccess,\n  removeRejsonKeyFailure,\n  rejsonSelector,\n  fetchReJSON,\n  fetchVisualisationResults,\n  setReJSONDataAction,\n  appendReJSONArrayItemAction,\n  removeReJSONKeyAction,\n  JSON_LENGTH_TO_FORCE_RETRIEVE,\n} from '../../browser/rejson'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../../app/notifications'\nimport { refreshKeyInfo } from '../../browser/keys'\nimport { EditorType } from 'uiSrc/slices/interfaces'\nimport { stringToBuffer } from 'uiSrc/utils'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet storeWithSelectedKey: typeof mockedStore\nlet defaultData: GetRejsonRlResponseDto\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  defaultData = {\n    downloaded: false,\n    path: '$',\n    data: [\n      { key: 'glossary', path: \"['glossary']\", cardinality: 2, type: 'object' },\n    ],\n    type: 'object',\n  }\n\n  const rootStateWithSelectedKey = {\n    ...initialStateDefault,\n    browser: {\n      keys: {\n        selectedKey: {\n          data: {\n            name: 'selectedKey',\n          },\n        },\n      },\n    },\n  }\n\n  storeWithSelectedKey = mockStore(rootStateWithSelectedKey)\n})\n\ndescribe('rejson slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadRejsonBranch', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadRejsonBranch())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadRejsonBranchSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: defaultData,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadRejsonBranchSuccess(defaultData),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadRejsonBranchSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadRejsonBranchFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadRejsonBranchFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('appendReJSONArrayItem', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, appendReJSONArrayItem())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('appendReJSONArrayItemSuccess', () => {\n    it('should properly set the state after append', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, appendReJSONArrayItemSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('appendReJSONArrayItemFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        appendReJSONArrayItemFailure(data),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setReJSONData', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setReJSONData())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setReJSONDataSuccess', () => {\n    it('should properly set the state after append', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setReJSONDataSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setReJSONDataFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setReJSONDataFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeRejsonKey', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeRejsonKey())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeRejsonKeySuccess', () => {\n    it('should properly set the state after append', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeRejsonKeySuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeRejsonKeyFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeRejsonKeyFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          rejson: nextState,\n        },\n      }\n      expect(rejsonSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchReJSON', () => {\n      it('call both fetchReJSON and loadRejsonBranchSuccess when fetch is successed', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n\n        const responsePayload = { data: defaultData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchReJSON(key, path))\n\n        // Assert\n        const expectedActions = [\n          loadRejsonBranch(),\n          loadRejsonBranchSuccess(responsePayload.data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both fetchReJSON and loadRejsonBranchFailure when fetch is fail', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchReJSON(key, path))\n\n        // Assert\n        const expectedActions = [\n          loadRejsonBranch(),\n          loadRejsonBranchFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should set forceRetrieve to true when editorType is Text and the JSON is big enough', async () => {\n        const key = stringToBuffer('key')\n        const path = '$'\n        const responsePayload = { data: {}, status: 200 }\n\n        const apiServicePostMock = jest.fn()\n        apiService.post = apiServicePostMock.mockResolvedValue(responsePayload)\n\n        const customState = {\n          ...store.getState(),\n          browser: {\n            ...store.getState().browser,\n            rejson: {\n              ...store.getState().browser.rejson,\n              editorType: EditorType.Text,\n            },\n          },\n        }\n\n        const storeWithCustomState = mockStore(customState)\n\n        const lengthAboveThreshold = JSON_LENGTH_TO_FORCE_RETRIEVE + 1\n        await storeWithCustomState.dispatch<any>(\n          fetchReJSON(key, path, lengthAboveThreshold),\n        )\n\n        const postPayload = apiServicePostMock.mock.calls[0][1]\n\n        expect(postPayload.forceRetrieve).toBe(true)\n      })\n\n      it('should set forceRetrieve to true when length is undefined and editorType is Default', async () => {\n        const key = stringToBuffer('key')\n        const path = '$'\n        const responsePayload = { data: {}, status: 200 }\n\n        const apiServicePostMock = jest.fn()\n        apiService.post = apiServicePostMock.mockResolvedValue(responsePayload)\n\n        const customState = {\n          ...store.getState(),\n          browser: {\n            ...store.getState().browser,\n            rejson: {\n              ...store.getState().browser.rejson,\n              editorType: EditorType.Default,\n            },\n          },\n        }\n\n        const storeWithCustomState = mockStore(customState)\n\n        await storeWithCustomState.dispatch<any>(fetchReJSON(key, path)) // no length\n\n        const postPayload = apiServicePostMock.mock.calls[0][1]\n        expect(postPayload.forceRetrieve).toBe(true)\n      })\n\n      it('should set forceRetrieve to true when length is below threshold and editorType is Default', async () => {\n        const key = stringToBuffer('key')\n        const path = '$'\n        const responsePayload = { data: {}, status: 200 }\n\n        const apiServicePostMock = jest.fn()\n        apiService.post = apiServicePostMock.mockResolvedValue(responsePayload)\n\n        const customState = {\n          ...store.getState(),\n          browser: {\n            ...store.getState().browser,\n            rejson: {\n              ...store.getState().browser.rejson,\n              editorType: EditorType.Default,\n            },\n          },\n        }\n\n        const storeWithCustomState = mockStore(customState)\n\n        const smallLength = 1\n        expect(smallLength).toBeLessThan(JSON_LENGTH_TO_FORCE_RETRIEVE)\n\n        await storeWithCustomState.dispatch<any>(\n          fetchReJSON(key, path, smallLength),\n        )\n\n        const postPayload = apiServicePostMock.mock.calls[0][1]\n        expect(postPayload.forceRetrieve).toBe(true)\n      })\n\n      it('should set forceRetrieve to false when length is above threshold and editorType is Default', async () => {\n        const key = stringToBuffer('key')\n        const path = '$'\n        const responsePayload = { data: {}, status: 200 }\n\n        const apiServicePostMock = jest.fn()\n        apiService.post = apiServicePostMock.mockResolvedValue(responsePayload)\n\n        const customState = {\n          ...store.getState(),\n          browser: {\n            ...store.getState().browser,\n            rejson: {\n              ...store.getState().browser.rejson,\n              editorType: EditorType.Default,\n            },\n          },\n        }\n\n        const storeWithCustomState = mockStore(customState)\n\n        const lengthAboveThreshold = JSON_LENGTH_TO_FORCE_RETRIEVE + 1\n        await storeWithCustomState.dispatch<any>(\n          fetchReJSON(key, path, lengthAboveThreshold),\n        )\n\n        const postPayload = apiServicePostMock.mock.calls[0][1]\n        expect(postPayload.forceRetrieve).toBe(false)\n      })\n    })\n\n    describe('setReJSONDataAction', () => {\n      it('succeed to fetch set json data', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const data = '{}'\n\n        const responsePayload = { status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayload2 = { data: defaultData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload2)\n\n        // Act\n        await store.dispatch<any>(setReJSONDataAction(key, path, data))\n\n        // Assert\n        const expectedActions = [\n          setReJSONData(),\n          setReJSONDataSuccess(),\n          loadRejsonBranch(),\n          refreshKeyInfo(),\n        ]\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch set json data', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(setReJSONDataAction(key, path, '{'))\n\n        // Assert\n        const expectedActions = [\n          setReJSONData(),\n          setReJSONDataFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('appendReJSONArrayItemAction', () => {\n      it('succeed to fetch append array data', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const data = '123'\n\n        const responsePayload = { status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayload2 = { data: defaultData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload2)\n\n        // Act\n        await store.dispatch<any>(appendReJSONArrayItemAction(key, path, data))\n\n        // Assert\n        const expectedActions = [\n          appendReJSONArrayItem(),\n          appendReJSONArrayItemSuccess(),\n          loadRejsonBranch(),\n          refreshKeyInfo(),\n        ]\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch append array data', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(appendReJSONArrayItemAction(key, path, '{'))\n\n        // Assert\n        const expectedActions = [\n          appendReJSONArrayItem(),\n          appendReJSONArrayItemFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('removeReJSONKeyAction', () => {\n      it('succeed to fetch remove json key', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const jsonKeyName = 'jsonKeyName'\n\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayload2 = { data: defaultData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload2)\n\n        // Act\n        await store.dispatch<any>(removeReJSONKeyAction(key, path, jsonKeyName))\n\n        // Assert\n        const expectedActions = [\n          removeRejsonKey(),\n          removeRejsonKeySuccess(),\n          loadRejsonBranch(),\n          refreshKeyInfo(),\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(key, jsonKeyName, 'JSON key'),\n          ),\n        ]\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch remove json key', async () => {\n        // Arrange\n        const key = 'key'\n        const path = '$'\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeReJSONKeyAction(key, path))\n\n        // Assert\n        const expectedActions = [\n          removeRejsonKey(),\n          removeRejsonKeyFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchVisualisationResults', () => {\n      it('call both fetchVisualisationResults and loadRejsonBranchSuccess when fetch is successed', async () => {\n        // Arrange\n        const path = '$'\n\n        const responsePayload = { data: defaultData, status: 200 }\n\n        const expectedResult = defaultData\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        const result = await storeWithSelectedKey.dispatch<any>(\n          fetchVisualisationResults(path),\n        )\n\n        // Assert\n        expect(result).toEqual(expectedResult)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/set.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { deleteRedisearchKeyFromList } from 'uiSrc/slices/browser/redisearch'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport {\n  defaultSelectedKeyAction,\n  deleteSelectedKeySuccess,\n  refreshKeyInfo,\n  updateSelectedKeyRefreshTime,\n} from '../../browser/keys'\nimport reducer, {\n  initialState,\n  loadMoreSetMembers,\n  loadMoreSetMembersFailure,\n  loadMoreSetMembersSuccess,\n  loadSetMembers,\n  loadSetMembersFailure,\n  loadSetMembersSuccess,\n  addSetMembers,\n  addSetMembersSuccess,\n  addSetMembersFailure,\n  removeMembersFromList,\n  removeSetMembers,\n  removeSetMembersFailure,\n  removeSetMembersSuccess,\n  setSelector,\n  fetchSetMembers,\n  fetchMoreSetMembers,\n  addSetMembersAction,\n  deleteSetMembers,\n} from '../../browser/set'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('set slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadSetMembers(['', undefined]))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        key: 'test set',\n        keyName: 'test set',\n        ttl: 2147284147,\n        total: 1,\n        nextCursor: 67,\n        members: ['1', '2'],\n        match: '*1*',\n      }\n\n      const state = {\n        loading: false,\n        error: '',\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = {\n        keyName: 'key',\n      }\n\n      const state = {\n        loading: false,\n        error: '',\n        data: {\n          ...initialState.data,\n          ...data,\n          key: data.keyName,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        loading: false,\n        error: data,\n        data: {\n          total: 0,\n          key: undefined,\n          keyName: '',\n          members: [],\n          nextCursor: 0,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadSetMembersFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        loading: true,\n        error: '',\n        data: {\n          total: 0,\n          key: undefined,\n          keyName: '',\n          members: [],\n          nextCursor: 0,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreSetMembers())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        key: undefined,\n        keyName: '',\n        nextCursor: 0,\n        total: 0,\n        members: ['2', '3'],\n        match: '*2*',\n      }\n\n      const state = {\n        loading: false,\n        error: '',\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: 'test set',\n        ttl: -1,\n        total: 10,\n        nextCursor: 67,\n        members: [],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          ...data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        loading: false,\n        error: data,\n        data: {\n          total: 0,\n          key: undefined,\n          keyName: '',\n          members: [],\n          nextCursor: 0,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreSetMembersFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, addSetMembers())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, addSetMembersSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, addSetMembersFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeSetMembers())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const initailStateRemove = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          members: ['1', '2', '3'],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initailStateRemove, removeSetMembersSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(initailStateRemove)\n    })\n  })\n\n  describe('removeSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        loading: false,\n        error: data,\n        data: {\n          total: 0,\n          key: undefined,\n          keyName: '',\n          members: [],\n          nextCursor: 0,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeSetMembersFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeMembersFromList', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const initialStateRemove = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          members: ['1', '2', '3'].map((member) => stringToBuffer(member)),\n        },\n      }\n\n      const data = ['1', '3'].map((member) => stringToBuffer(member))\n\n      const state = {\n        ...initialStateRemove,\n        data: {\n          ...initialStateRemove.data,\n          total: initialStateRemove.data.total - 1,\n          members: [stringToBuffer('2')],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateRemove, removeMembersFromList(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { set: nextState },\n      }\n      expect(setSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchSetMembers', () => {\n      // Arrange\n      const data = {\n        keyName: 'small set',\n        nextCursor: 0,\n        members: ['123', '123', '1'],\n        total: 3,\n        match: '*',\n      }\n      it('call fetchSetMembers, loadSetMembersSuccess when fetch is successed', async () => {\n        // Arrange\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSetMembers(data.keyName, 0, 20, '*'))\n\n        // Assert\n        const expectedActions = [\n          loadSetMembers([data.match, undefined]),\n          loadSetMembersSuccess(responsePayload.data),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('failed to fetch Set members', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSetMembers(data.keyName, 0, 20, '*'))\n\n        // Assert\n        const expectedActions = [\n          loadSetMembers(['*', undefined]),\n          addErrorNotification(responsePayload as AxiosError),\n          loadSetMembersFailure(errorMessage),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n\n  describe('fetchMoreSetMembers', () => {\n    // Arrange\n    const data = {\n      keyName: 'small set',\n      nextCursor: 0,\n      members: ['123', '123', '1'],\n      total: 3,\n    }\n    it('call fetchMoreSetMembers, loadMoreSetMembersSuccess when fetch is successed', async () => {\n      const responsePayload = { data, status: 200 }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchMoreSetMembers(data.keyName, 0, 20, '*'))\n\n      // Assert\n      const expectedActions = [\n        loadMoreSetMembers(),\n        loadMoreSetMembersSuccess(responsePayload.data),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n    it('failed to fetch more Set members', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchMoreSetMembers(data.keyName, 0, 20, '*'))\n\n      // Assert\n      const expectedActions = [\n        loadMoreSetMembers(),\n        addErrorNotification(responsePayload as AxiosError),\n        loadMoreSetMembersFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('addSetMembersAction', () => {\n    const keyName = 'key'\n    const members = ['member1', 'member2']\n    it('succeed to add members to set', async () => {\n      // Arrange\n      const responsePayload = { status: 200 }\n      apiService.put = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        addSetMembersAction({ keyName, members }, jest.fn),\n      )\n\n      // Assert\n      const expectedActions = [\n        addSetMembers(),\n        addSetMembersSuccess(),\n        defaultSelectedKeyAction(),\n      ]\n\n      expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n        expectedActions,\n      )\n    })\n    it('failed to add members to set', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        addSetMembersAction({ keyName, members }, jest.fn(), jest.fn()),\n      )\n\n      // Assert\n      const expectedActions = [\n        addSetMembers(),\n        addErrorNotification(responsePayload as AxiosError),\n        addSetMembersFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('deleteSetMembers', () => {\n    const key = 'key'\n    const members = ['123', '123', '1']\n    it('call removeSetMembers, removeSetMembersSuccess, and removeMembersFromList when fetch is successed', async () => {\n      // Arrange\n      const responsePayload = { status: 200, data: { affected: 3 } }\n\n      apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n      const nextState = {\n        ...initialStateDefault,\n        browser: {\n          ...initialStateDefault.browser,\n          set: {\n            ...initialState,\n            data: {\n              ...initialState.data,\n              total: 10,\n            },\n          },\n        },\n      }\n\n      const mockedStore = mockStore(nextState)\n\n      // Act\n      await mockedStore.dispatch<any>(deleteSetMembers(key, members))\n\n      // Assert\n      const expectedActions = [\n        removeSetMembers(),\n        removeSetMembersSuccess(),\n        removeMembersFromList(members),\n        refreshKeyInfo(),\n        addMessageNotification(\n          successMessages.REMOVED_KEY_VALUE(key, members.join(''), 'Member'),\n        ),\n      ]\n\n      expect(mockedStore.getActions().slice(0, expectedActions.length)).toEqual(\n        expectedActions,\n      )\n    })\n\n    it('succeed to delete all members from set', async () => {\n      // Arrange\n      const responsePayload = { status: 200, data: { affected: 3 } }\n\n      apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n      const nextState = {\n        ...initialStateDefault,\n        browser: {\n          set: {\n            ...initialState,\n            data: {\n              ...initialState.data,\n              total: 2,\n            },\n          },\n        },\n      }\n\n      const mockedStore = mockStore(nextState)\n\n      // Act\n      await mockedStore.dispatch<any>(deleteSetMembers(key, members))\n\n      // Assert\n      const expectedActions = [\n        removeSetMembers(),\n        removeSetMembersSuccess(),\n        removeMembersFromList(members),\n        deleteSelectedKeySuccess(),\n        deleteRedisearchKeyFromList(key),\n        addMessageNotification(successMessages.DELETED_KEY(key)),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to delete member from set', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(deleteSetMembers(key, members))\n\n      // Assert\n      const expectedActions = [\n        removeSetMembers(),\n        addErrorNotification(responsePayload as AxiosError),\n        removeSetMembersFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/stream.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep, omit } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { SortOrder } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport { refreshKeyInfo } from 'uiSrc/slices/browser/keys'\nimport reducer, {\n  initialState,\n  setStreamInitialState,\n  loadEntries,\n  loadEntriesSuccess,\n  loadEntriesFailure,\n  loadMoreEntries,\n  loadMoreEntriesSuccess,\n  loadMoreEntriesFailure,\n  addNewEntries,\n  addNewEntriesSuccess,\n  addNewEntriesFailure,\n  removeStreamEntries,\n  removeStreamEntriesSuccess,\n  removeStreamEntriesFailure,\n  updateStart,\n  updateEnd,\n  cleanRangeFilter,\n  streamSelector,\n  streamRangeSelector,\n  fetchStreamEntries,\n  refreshStreamEntries,\n  setStreamViewType,\n  loadConsumerGroups,\n  loadConsumerGroupsSuccess,\n  loadConsumerGroupsFailure,\n  loadConsumersSuccess,\n  loadConsumersFailure,\n  loadConsumerMessagesSuccess,\n  loadConsumerMessagesFailure,\n  setSelectedGroup,\n  setSelectedConsumer,\n  fetchConsumerGroups,\n  fetchConsumers,\n  fetchConsumerMessages,\n  deleteConsumerGroups,\n  deleteConsumerGroupsAction,\n  deleteConsumerGroupsSuccess,\n  deleteConsumerGroupsFailure,\n  deleteConsumersAction,\n  deleteConsumers,\n  deleteConsumersSuccess,\n  deleteConsumersFailure,\n  modifyLastDeliveredIdAction,\n  modifyLastDeliveredId,\n  modifyLastDeliveredIdSuccess,\n  modifyLastDeliveredIdFailure,\n  fetchMoreConsumerMessages,\n  loadMoreConsumerMessagesSuccess,\n  ackPendingEntriesAction,\n  ackPendingEntries,\n  ackPendingEntriesSuccess,\n  ackPendingEntriesFailure,\n  claimPendingMessages,\n  claimConsumerMessages,\n  claimConsumerMessagesSuccess,\n  claimConsumerMessagesFailure,\n  deleteMessageFromList,\n} from 'uiSrc/slices/browser/stream'\nimport { StreamViewType } from 'uiSrc/slices/interfaces/stream'\nimport { bufferToString, stringToBuffer } from 'uiSrc/utils'\nimport {\n  ConsumerDto,\n  ConsumerGroupDto,\n  ClaimPendingEntryDto,\n  PendingEntryDto,\n  UpdateConsumerGroupDto,\n} from 'apiSrc/modules/browser/stream/dto'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\n\nconst mockedEntryData = {\n  keyName: bufferToString('stream_example'),\n  keyNameString: 'stream_example',\n  total: 1,\n  lastGeneratedId: '1652942518810-0',\n  firstEntry: {\n    id: '1652942518810-0',\n    fields: { field: stringToBuffer('1'), name: stringToBuffer('2') },\n  },\n  lastEntry: {\n    id: '1652942518810-0',\n    fields: { field: stringToBuffer('1'), name: stringToBuffer('2') },\n  },\n  entries: [\n    {\n      id: '1652942518810-0',\n      fields: { field: stringToBuffer('1'), name: stringToBuffer('2') },\n    },\n  ],\n}\n\nconst mockGroups: ConsumerGroupDto[] = [\n  {\n    name: {\n      ...stringToBuffer('test'),\n      viewValue: 'test',\n    },\n    consumers: 123,\n    pending: 321,\n    smallestPendingId: '123',\n    greatestPendingId: '123',\n    lastDeliveredId: '123',\n  },\n  {\n    name: {\n      ...stringToBuffer('test2'),\n      viewValue: 'test2',\n    },\n    consumers: 13,\n    pending: 31,\n    smallestPendingId: '3',\n    greatestPendingId: '23',\n    lastDeliveredId: '12',\n  },\n]\n\nconst mockConsumers: ConsumerDto[] = [\n  {\n    name: stringToBuffer('test'),\n    nameString: 'test',\n    idle: 123,\n    pending: 321,\n  },\n  {\n    name: stringToBuffer('test2'),\n    nameString: 'test2',\n    idle: 13,\n    pending: 31,\n  },\n]\n\nconst mockMessages: PendingEntryDto[] = [\n  {\n    id: '123',\n    consumerName: stringToBuffer('test'),\n    idle: 321,\n    delivered: 321,\n  },\n  {\n    id: '1234',\n    consumerName: stringToBuffer('test2'),\n    idle: 3213,\n    delivered: 1321,\n  },\n]\n\nDate.now = jest.fn(() => Date.parse('2021-05-27'))\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('stream slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setStreamInitialState', () => {\n    it('should properly set initial state', () => {\n      const nextState = reducer(initialState, setStreamInitialState())\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('loadEntries', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadEntries(true))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadEntriesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...mockedEntryData,\n        },\n      }\n\n      // Act\n      const tempState = reducer(\n        initialState,\n        loadEntriesSuccess([mockedEntryData, SortOrder.DESC]),\n      )\n\n      const nextState = omit({ ...tempState }, 'data.lastRefreshTime')\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadEntriesFailure', () => {\n    it('should properly set the state after failed fetched data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadEntriesFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreEntries', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreEntries())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreEntriesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...mockedEntryData,\n        },\n      }\n\n      // Act\n      const tempState = reducer(\n        initialState,\n        loadMoreEntriesSuccess(mockedEntryData),\n      )\n      const nextState = omit({ ...tempState }, 'data.lastRefreshTime')\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreEntriesFailure', () => {\n    it('should properly set the state after failed fetched data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreEntriesFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addNewEntries', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, addNewEntries())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addNewEntriesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, addNewEntriesSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addNewEntriesFailure', () => {\n    it('should properly set the state after failed fetched data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, addNewEntriesFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeStreamEntries', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeStreamEntries())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeStreamEntriesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeStreamEntriesSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeStreamEntriesFailure', () => {\n    it('should properly set the state after failed fetched data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeStreamEntriesFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateStart', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        range: {\n          ...initialState.range,\n          start: '10',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateStart('10'))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateEnd', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        range: {\n          ...initialState.range,\n          end: '100',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateEnd('100'))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('cleanRangeFilter', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const startState = {\n        ...initialState,\n        range: {\n          ...initialState.range,\n          start: '100',\n          end: '200',\n        },\n      }\n      const stateRange = {\n        ...initialState.range,\n      }\n\n      // Act\n      const nextState = reducer(startState, cleanRangeFilter())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamRangeSelector(rootState)).toEqual(stateRange)\n    })\n  })\n\n  describe('setStreamViewType', () => {\n    it('should properly set stream view type', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        viewType: StreamViewType.Messages,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setStreamViewType(StreamViewType.Messages),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumerGroups', () => {\n    it('should properly set groups.loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        groups: {\n          ...initialState.groups,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadConsumerGroups())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumerGroupsSuccess', () => {\n    it('should properly set groups.data = payload', () => {\n      // Arrange\n      const data: ConsumerGroupDto[] = [\n        {\n          name: stringToBuffer('123'),\n          consumers: 123,\n          pending: 123,\n          smallestPendingId: '123',\n          greatestPendingId: '123',\n          lastDeliveredId: '123',\n        },\n      ]\n      const state = {\n        ...initialState,\n        loading: false,\n        groups: {\n          ...initialState.groups,\n          data,\n          lastRefreshTime: Date.now(),\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadConsumerGroupsSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumersSuccess', () => {\n    it('should properly set groups.selectedGroup.data = payload', () => {\n      // Arrange\n      const data: ConsumerDto[] = [\n        {\n          name: {\n            ...stringToBuffer('123'),\n            viewValue: '123',\n          },\n          pending: 123,\n          idle: 123,\n        },\n      ]\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          selectedGroup: {\n            data,\n            lastRefreshTime: Date.now(),\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadConsumersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumerMessagesSuccess', () => {\n    it('should properly set groups.selectedGroup.selectedConsumer.data = payload', () => {\n      // Arrange\n      const data: PendingEntryDto[] = [\n        {\n          id: '123',\n          consumerName: '123',\n          idle: 123,\n          delivered: 123,\n        },\n      ]\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          selectedGroup: {\n            selectedConsumer: {\n              data,\n              lastRefreshTime: Date.now(),\n            },\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadConsumerMessagesSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumerMessagesFailure', () => {\n    it('should properly set error to groups and set viewType = Consumers payload', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        viewType: StreamViewType.Consumers,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n          error,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadConsumerMessagesFailure(error),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumersFailure', () => {\n    it('should properly set error to groups and set viewType = Groups payload', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        viewType: StreamViewType.Groups,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n          error,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadConsumersFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadConsumerGroupsFailure', () => {\n    it('should properly set error to groups payload', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n          error,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadConsumerGroupsFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSelectedGroup', () => {\n    it('should properly set selectedGroups', () => {\n      // Arrange\n      const group = {\n        name: stringToBuffer('group name'),\n        nameString: 'group name',\n      }\n\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          selectedGroup: group,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSelectedGroup(group))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSelectedConsumer', () => {\n    it('should properly set selectedConsumer', () => {\n      // Arrange\n      const consumer = {\n        name: stringToBuffer('consumer name'),\n        nameString: 'consumer name',\n      }\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          selectedGroup: {\n            selectedConsumer: consumer,\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSelectedConsumer(consumer))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreConsumerMessagesSuccess', () => {\n    it('should properly concat more messages', () => {\n      // Arrange\n      const data: PendingEntryDto[] = [\n        {\n          id: '123',\n          consumerName: '123',\n          idle: 123,\n          delivered: 123,\n        },\n      ]\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          selectedGroup: {\n            selectedConsumer: {\n              lastRefreshTime: Date.now(),\n              data: [\n                ...(initialState.groups.selectedGroup?.selectedConsumer?.data ??\n                  []),\n                ...data,\n              ],\n            },\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadMoreConsumerMessagesSuccess(data),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('ackPendingEntries', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, ackPendingEntries())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('ackPendingEntriesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, ackPendingEntriesSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('ackPendingEntriesFailure', () => {\n    it('should properly set the state after failed fetched data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n          error,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, ackPendingEntriesFailure(error))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('claimConsumerMessages', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, claimConsumerMessages())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('claimConsumerMessagesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, claimConsumerMessagesSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('claimConsumerMessagesFailure', () => {\n    it('should properly set the state after failed fetched data', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        groups: {\n          ...initialState.groups,\n          loading: false,\n          error,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        claimConsumerMessagesFailure(error),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { stream: nextState },\n      }\n      expect(streamSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchStreamEntries', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const responsePayload = { data: mockedEntryData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchStreamEntries(\n            mockedEntryData.keyName,\n            500,\n            SortOrder.DESC,\n            true,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadEntries(true),\n          loadEntriesSuccess([mockedEntryData, SortOrder.DESC]),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchStreamEntries(\n            mockedEntryData.keyName,\n            500,\n            SortOrder.DESC,\n            true,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadEntries(true),\n          addErrorNotification(responsePayload as AxiosError),\n          loadEntriesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('refreshStreamEntries', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const responsePayload = { data: mockedEntryData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          refreshStreamEntries(mockedEntryData.keyName, true),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadEntries(true),\n          loadEntriesSuccess([mockedEntryData, SortOrder.DESC]),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          refreshStreamEntries(mockedEntryData.keyName, true),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadEntries(true),\n          addErrorNotification(responsePayload as AxiosError),\n          loadEntriesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchConsumerGroups', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const responsePayload = { data: mockGroups, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConsumerGroups())\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          loadConsumerGroupsSuccess(mockGroups),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConsumerGroups())\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadConsumerGroupsFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchConsumers', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const responsePayload = { data: mockConsumers, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConsumers())\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          loadConsumersSuccess(mockConsumers),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConsumers())\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadConsumersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchConsumerMessages', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const responsePayload = { data: mockMessages, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConsumerMessages())\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          loadConsumerMessagesSuccess(mockMessages),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConsumerMessages())\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadConsumerMessagesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('modifyLastDeliveredIdAction', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const data: UpdateConsumerGroupDto = {\n          keyName: 'key',\n          name: 'name',\n          lastDeliveredId: '0-1',\n        }\n        const responsePayload = { data: mockMessages, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadPost = { data: mockConsumers, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)\n\n        // Act\n        await store.dispatch<any>(modifyLastDeliveredIdAction(data))\n\n        // Assert\n        const expectedActions = [\n          modifyLastDeliveredId(),\n          modifyLastDeliveredIdSuccess(),\n          loadConsumerGroups(false),\n          refreshKeyInfo(),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch data', async () => {\n        const data: UpdateConsumerGroupDto = {\n          keyName: 'key',\n          name: 'name',\n          lastDeliveredId: '0-1',\n        }\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(modifyLastDeliveredIdAction(data))\n\n        // Assert\n        const expectedActions = [\n          modifyLastDeliveredId(),\n          addErrorNotification(responsePayload as AxiosError),\n          modifyLastDeliveredIdFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteConsumerGroupsAction', () => {\n      it('succeed to delete data', async () => {\n        // Arrange\n        const keyName = 'key'\n        const groups = ['group']\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadPost = { data: mockConsumers, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)\n\n        // Act\n        await store.dispatch<any>(deleteConsumerGroupsAction(keyName, groups))\n\n        // Assert\n        const expectedActions = [\n          deleteConsumerGroups(),\n          deleteConsumerGroupsSuccess(),\n          loadConsumerGroups(false),\n          refreshKeyInfo(),\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(\n              keyName,\n              groups.join(''),\n              'Group',\n            ),\n          ),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to delete data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const keyName = 'key'\n        const groups = ['group']\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteConsumerGroupsAction(keyName, groups))\n\n        // Assert\n        const expectedActions = [\n          deleteConsumerGroups(),\n          addErrorNotification(responsePayload as AxiosError),\n          deleteConsumerGroupsFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteConsumersAction', () => {\n      it('succeed to delete data', async () => {\n        // Arrange\n        const keyName = stringToBuffer('key')\n        const groupName = stringToBuffer('group')\n        const consumerNames = ['consumer'].map((name) => stringToBuffer(name))\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadPost = { data: mockConsumers, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)\n\n        // Act\n        await store.dispatch<any>(\n          deleteConsumersAction(keyName, groupName, consumerNames),\n        )\n\n        // Assert\n        const expectedActions = [\n          deleteConsumers(),\n          deleteConsumersSuccess(),\n          loadConsumerGroups(false),\n          refreshKeyInfo(),\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(\n              keyName,\n              consumerNames.map((name) => bufferToString(name)).join(''),\n              'Consumer',\n            ),\n          ),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to delete data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const keyName = 'key'\n        const groupName = 'group'\n        const consumerNames = ['consumer']\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          deleteConsumersAction(keyName, groupName, consumerNames),\n        )\n\n        // Assert\n        const expectedActions = [\n          deleteConsumers(),\n          addErrorNotification(responsePayload as AxiosError),\n          deleteConsumersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchMoreConsumerMessages', () => {\n      it('succeed to fetch more data', async () => {\n        // Arrange\n        const pendingMessages = [\n          {\n            idle: 123,\n            id: '123',\n            consumerName: 'name',\n            delivered: 1,\n          },\n        ]\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadPost = { data: pendingMessages, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)\n\n        // Act\n        await store.dispatch<any>(fetchMoreConsumerMessages(500))\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          loadMoreConsumerMessagesSuccess(pendingMessages),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch more data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchMoreConsumerMessages(500))\n\n        // Assert\n        const expectedActions = [\n          loadConsumerGroups(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadConsumerMessagesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('ackPendingEntriesAction', () => {\n      it('succeed to acknowledge data', async () => {\n        // Arrange\n        const keyName = 'key'\n        const groupName = 'group'\n        const entries = ['0-1']\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          ackPendingEntriesAction(keyName, groupName, entries),\n        )\n\n        // Assert\n        const expectedActions = [\n          ackPendingEntries(),\n          ackPendingEntriesSuccess(),\n          deleteMessageFromList('0-1'),\n          loadConsumerGroups(),\n          loadConsumerGroups(),\n          addMessageNotification(\n            successMessages.MESSAGE_ACTION(entries.join(''), 'acknowledged'),\n          ),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to acknowledge message', async () => {\n        const errorMessage = 'Something was wrong!'\n        const keyName = 'key'\n        const groupName = 'group'\n        const entries = ['0-1']\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          ackPendingEntriesAction(keyName, groupName, entries),\n        )\n\n        // Assert\n        const expectedActions = [\n          ackPendingEntries(),\n          addErrorNotification(responsePayload as AxiosError),\n          ackPendingEntriesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('claimPendingMessagesAction', () => {\n      it('succeed to claim message', async () => {\n        // Arrange\n        const data: Partial<ClaimPendingEntryDto> = {\n          keyName: 'key',\n          groupName: 'group',\n          consumerName: 'name',\n          minIdleTime: 0,\n          entries: ['0-1'],\n        }\n\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadPost = { data: { affected: ['0-1'] }, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)\n\n        // Act\n        await store.dispatch<any>(claimPendingMessages(data))\n\n        // Assert\n        const expectedActions = [\n          claimConsumerMessages(),\n          claimConsumerMessagesSuccess(),\n          loadConsumerGroups(),\n          loadConsumerGroups(),\n          deleteMessageFromList('0-1'),\n          addMessageNotification(\n            successMessages.MESSAGE_ACTION('0-1', 'claimed'),\n          ),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('succeed to claim message with 0 affected', async () => {\n        // Arrange\n        const data: Partial<ClaimPendingEntryDto> = {\n          keyName: 'key',\n          groupName: 'group',\n          consumerName: 'name',\n          minIdleTime: 0,\n          entries: ['0-1'],\n        }\n\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        const responsePayloadPost = { data: { affected: [] }, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayloadPost)\n\n        // Act\n        await store.dispatch<any>(claimPendingMessages(data))\n\n        // Assert\n        const expectedActions = [\n          claimConsumerMessages(),\n          claimConsumerMessagesSuccess(),\n          loadConsumerGroups(),\n          loadConsumerGroups(),\n          addMessageNotification(successMessages.NO_CLAIMED_MESSAGES()),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch data', async () => {\n        const data: Partial<ClaimPendingEntryDto> = {\n          keyName: 'key',\n          groupName: 'group',\n          consumerName: 'name',\n          minIdleTime: 0,\n          entries: ['0-1'],\n        }\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(claimPendingMessages(data))\n\n        // Assert\n        const expectedActions = [\n          claimConsumerMessages(),\n          addErrorNotification(responsePayload as AxiosError),\n          claimConsumerMessagesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/string.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport {\n  refreshKeyInfo,\n  refreshKeyInfoSuccess,\n  updateSelectedKeyRefreshTime,\n} from '../../browser/keys'\nimport reducer, {\n  initialState,\n  getString,\n  getStringSuccess,\n  getStringFailure,\n  stringSelector,\n  stringDataSelector,\n  fetchString,\n  updateValue,\n  updateValueSuccess,\n  updateValueFailure,\n  resetStringValue,\n  updateStringValueAction,\n  setIsStringCompressed,\n  fetchDownloadStringValue,\n  downloadString,\n  downloadStringSuccess,\n  downloadStringFailure,\n} from '../../browser/string'\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\ndescribe('string slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getString', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getString())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('getStringSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const data = {\n        keyName: 'zxc',\n        value: 'val',\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          key: data.keyName,\n          value: data.value,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getStringSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: '',\n        value: '',\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          key: data.keyName,\n          value: data.value,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getStringSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('getStringFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getStringFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('downloadString', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, downloadString())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('downloadStringSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(initialState, downloadStringSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, downloadStringSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('downloadStringFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, downloadStringFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('updateValue', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValue())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('updateValueSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = 'test test'\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          value: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = ''\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          value: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('updateValueFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateValueFailure(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('resetStringValue', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetStringValue())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('setIsStringCompressed', () => {\n    it('should properly set the state with isCompressed=true', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        isCompressed: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setIsStringCompressed(true))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: {\n          string: nextState,\n        },\n      }\n      expect(stringSelector(rootState)).toEqual(state)\n      expect(stringDataSelector(rootState)).toEqual(state.data)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchString', () => {\n      it('call both fetchString, getStringSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = 'test'\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchString(''))\n\n        // Assert\n        const expectedActions = [\n          getString(),\n          getStringSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetchString', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchString(''))\n\n        // Assert\n        const expectedActions = [\n          getString(),\n          addErrorNotification(responsePayload as AxiosError),\n          getStringFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchDownloadStringValue', () => {\n      it('call both fetchDownloadStringValue, downloadStringSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = 'test'\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchDownloadStringValue(''))\n\n        // Assert\n        const expectedActions = [downloadString(), downloadStringSuccess()]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetchDownloadStringValue', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchDownloadStringValue(''))\n\n        // Assert\n        const expectedActions = [\n          downloadString(),\n          addErrorNotification(responsePayload as AxiosError),\n          downloadStringFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('updateStringValueAction', () => {\n      it('succeed to fetch update string value', async () => {\n        // Arrange\n        const data = {\n          keyName: 'stringKey',\n          value: 'string value',\n        }\n        const responsePayload = { status: 200 }\n\n        apiService.put = jest.fn().mockResolvedValue(responsePayload)\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          updateStringValueAction(data.keyName, data.value, jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          updateValueSuccess(data.value),\n          refreshKeyInfo(),\n          refreshKeyInfoSuccess(),\n          updateSelectedKeyRefreshTime(MOCK_TIMESTAMP),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch update string value', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          updateStringValueAction('', '', jest.fn(), jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateValue(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateValueFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/browser/zset.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { SortOrder } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { stringToBuffer } from 'uiSrc/utils'\nimport { deleteRedisearchKeyFromList } from 'uiSrc/slices/browser/redisearch'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport {\n  AddMembersToZSetDto,\n  ZSetMemberDto,\n} from 'apiSrc/modules/browser/z-set/dto'\nimport {\n  defaultSelectedKeyAction,\n  deleteSelectedKeySuccess,\n  refreshKeyInfo,\n  updateSelectedKeyRefreshTime,\n} from '../../browser/keys'\nimport reducer, {\n  initialState,\n  setZsetInitialState,\n  loadZSetMembers,\n  loadZSetMembersSuccess,\n  loadZSetMembersFailure,\n  searchZSetMembers,\n  searchZSetMembersSuccess,\n  searchZSetMembersFailure,\n  searchMoreZSetMembers,\n  searchMoreZSetMembersSuccess,\n  searchMoreZSetMembersFailure,\n  loadMoreZSetMembers,\n  loadMoreZSetMembersSuccess,\n  loadMoreZSetMembersFailure,\n  removeZsetMembers,\n  removeZsetMembersSuccess,\n  removeZsetMembersFailure,\n  removeMembersFromList,\n  resetUpdateScore,\n  updateScore,\n  updateScoreSuccess,\n  updateScoreFailure,\n  updateMembersInList,\n  zsetSelector,\n  fetchZSetMembers,\n  fetchMoreZSetMembers,\n  fetchAddZSetMembers,\n  deleteZSetMembers,\n  updateZSetMembers,\n  fetchSearchZSetMembers,\n  fetchSearchMoreZSetMembers,\n  refreshZsetMembersAction,\n} from '../../browser/zset'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet dateNow: jest.SpyInstance<number>\nconst errorMessage = 'some error'\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('zset slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setZsetInitialState', () => {\n    it('should properly set initial state', () => {\n      const nextState = reducer(initialState, setZsetInitialState())\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('loadZSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadZSetMembers([initialState.data.sortOrder, undefined]),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadZSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        keyName: 'small zset',\n        ttl: 2147284147,\n        total: 1,\n        size: 67,\n        members: [{ name: '1', score: '1' }],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          keyName: data.keyName,\n          members: data.members,\n          total: data.total,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadZSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: '',\n        members: [],\n        total: 0,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          ...data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadZSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadZSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const state = {\n        loading: false,\n        searching: false,\n        error: errorMessage,\n        data: initialState.data,\n        updateScore: {\n          loading: false,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadZSetMembersFailure(errorMessage),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('searchZSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        searching: true,\n        data: {\n          ...initialState.data,\n          match: '*',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, searchZSetMembers('*'))\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('searchZSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      const data = {\n        keyName: 'zset',\n        nextCursor: 0,\n        members: [{ name: 'member name', score: 10 }],\n        total: 1,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...initialState.data,\n          ...data,\n          key: data.keyName,\n          match: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, searchZSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty members', () => {\n      const data = {\n        keyName: 'zset',\n        nextCursor: 0,\n        members: [],\n        total: 1,\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        error: '',\n        data: {\n          ...initialState.data,\n          ...data,\n          key: data.keyName,\n          match: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, searchZSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('searchZSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        searchZSetMembersFailure(errorMessage),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('searchMoreZSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      const data = '*'\n      const state = {\n        ...initialState,\n        loading: true,\n        searching: true,\n        data: {\n          ...initialState.data,\n          match: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, searchMoreZSetMembers(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('searchMoreZSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      const data = {\n        nextCursor: 0,\n        members: [{ name: 'member name', score: 10 }],\n        total: 1,\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          members: data.members,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        searchMoreZSetMembersSuccess(data),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      const data = {\n        keyName: 'zset',\n        nextCursor: 0,\n        members: [],\n        total: 0,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        searchMoreZSetMembersSuccess(data),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('searchMoreZSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        searchMoreZSetMembersFailure(errorMessage),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreZSetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreZSetMembers())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMoreZSetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = {\n        key: undefined,\n        keyName: '',\n        match: '',\n        nextCursor: 0,\n        total: 0,\n        members: [\n          { name: '1', score: '1' },\n          { name: '12', score: '12' },\n          { name: '2', score: '2' },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: {\n          ...initialState.data,\n          ...data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreZSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = {\n        keyName: 'small zset',\n        ttl: -1,\n        total: 10,\n        size: 67,\n        members: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMoreZSetMembersSuccess(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('loadMoreZSetMembersFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadMoreZSetMembersFailure(errorMessage),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeZsetMembers', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeZsetMembers())\n\n      // Assert\n      const rootSate = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootSate)).toEqual(state)\n    })\n  })\n\n  describe('removeZsetMembersSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      const initialStateRemove = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          members: [{ name: 'member name', score: 10 }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateRemove, removeZsetMembersSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(initialStateRemove)\n    })\n  })\n\n  describe('removeZsetMembersFailure', () => {\n    it('should properly set the error', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        removeZsetMembersFailure(errorMessage),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeMembersFromList', () => {\n    it('should properly remove members from list', () => {\n      // Arrange\n      const initialStateRemove = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          members: [\n            { name: stringToBuffer('member name'), score: 1 },\n            { name: stringToBuffer('member name1'), score: 2 },\n            { name: stringToBuffer('member name2'), score: 3 },\n          ],\n        },\n      }\n\n      const data = [\n        stringToBuffer('member name'),\n        stringToBuffer('member name1'),\n      ]\n\n      const state = {\n        ...initialStateRemove,\n        data: {\n          ...initialStateRemove.data,\n          total: initialStateRemove.data.total - 1,\n          members: [{ name: stringToBuffer('member name2'), score: 3 }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateRemove, removeMembersFromList(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateScore', () => {\n    it('should properly set the state while updating a zset score', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        updateScore: {\n          ...initialState.updateScore,\n          loading: true,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateScore())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateScoreSuccess', () => {\n    it('should properly set the state after successfully updated zset score', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        updateScore: {\n          ...initialState.updateScore,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateScoreSuccess())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateScoreFailure', () => {\n    it('should properly set the state on update zset score failure', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        updateScore: {\n          ...initialState.updateScore,\n          loading: false,\n          error: errorMessage,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateScoreFailure(errorMessage))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetUpdateScore', () => {\n    it('should properly reset the state', () => {\n      // Arrange;\n      const state = {\n        ...initialState,\n        updateScore: {\n          ...initialState.updateScore,\n          loading: false,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetUpdateScore())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateMembersInList', () => {\n    it('should properly update members in list', () => {\n      // Arrange\n      const initialStateToUpdate = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          members: [\n            {\n              name: stringToBuffer('name'),\n              score: 1,\n            },\n            {\n              name: stringToBuffer('name2'),\n              score: 2,\n            },\n          ],\n        },\n      }\n\n      const data: ZSetMemberDto[] = [\n        {\n          name: stringToBuffer('name2'),\n          score: 3,\n        },\n      ]\n\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          members: [\n            {\n              name: stringToBuffer('name'),\n              score: 1,\n            },\n            {\n              name: stringToBuffer('name2'),\n              score: 3,\n            },\n          ],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialStateToUpdate, updateMembersInList(data))\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        browser: { zset: nextState },\n      }\n      expect(zsetSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchZSetMembers', () => {\n      it('succeed to fetch zset members', async () => {\n        // Arrange\n        const data = {\n          keyName: 'small zset',\n          ttl: 2146616656,\n          total: 1,\n          size: 67,\n          members: [{ name: '1', score: '1' }],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchZSetMembers(data.keyName, 0, 20, SortOrder.ASC),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadZSetMembers([SortOrder.ASC, undefined]),\n          loadZSetMembersSuccess(responsePayload.data),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch zset members', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchZSetMembers('', 0, 20, SortOrder.ASC))\n\n        // Assert\n        const expectedActions = [\n          loadZSetMembers([SortOrder.ASC, undefined]),\n          addErrorNotification(responsePayload as AxiosError),\n          loadZSetMembersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchMoreZSetMembers', () => {\n      it('succeed to fetch more zset members', async () => {\n        // Arrange\n        const data = {\n          keyName: 'small zset',\n          ttl: 2146616656,\n          total: 1,\n          size: 67,\n          members: [{ name: '1', score: '1' }],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchMoreZSetMembers(data.keyName, 0, 20, SortOrder.ASC),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadMoreZSetMembers(),\n          loadMoreZSetMembersSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch more zset members', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchMoreZSetMembers('', 0, 20, SortOrder.ASC),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadMoreZSetMembers(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadMoreZSetMembersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchAddZSetMembers', () => {\n      it('succeed to fetch add zset members', async () => {\n        // Arrange\n        const data: AddMembersToZSetDto = {\n          keyName: 'small zset',\n          members: [{ name: '1', score: 1 }],\n        }\n        const responsePayload = { status: 200 }\n\n        apiService.put = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchAddZSetMembers(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          updateScore(),\n          updateScoreSuccess(),\n          defaultSelectedKeyAction(),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch add zset members', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchAddZSetMembers(\n            { keyName: '', members: [] },\n            jest.fn(),\n            jest.fn(),\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateScore(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateScoreFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteZSetMembers', () => {\n      it('succeed to fetch delete zset members', async () => {\n        // Arrange\n        const key = 'zset key'\n        const members = ['member#1', 'member#2']\n        const responsePayload = { status: 200, data: { affected: 2 } }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n        const nextState = {\n          ...initialStateDefault,\n          browser: {\n            ...initialStateDefault.browser,\n            zset: {\n              ...initialState,\n              data: {\n                ...initialState.data,\n                total: 10,\n              },\n            },\n          },\n        }\n\n        const mockedStore = mockStore(nextState)\n\n        // Act\n        await mockedStore.dispatch<any>(deleteZSetMembers(key, members))\n\n        // Assert\n        const expectedActions = [\n          removeZsetMembers(),\n          removeZsetMembersSuccess(),\n          removeMembersFromList(members),\n          refreshKeyInfo(),\n          addMessageNotification(\n            successMessages.REMOVED_KEY_VALUE(key, members.join(''), 'Member'),\n          ),\n        ]\n\n        expect(\n          mockedStore.getActions().slice(0, expectedActions.length),\n        ).toEqual(expectedActions)\n      })\n\n      it('succeed to fetch delete all zset members', async () => {\n        // Arrange\n        const key = 'zset key'\n        const members = ['member#1', 'member#2']\n        const responsePayload = { status: 200, data: { affected: 2 } }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n        const nextState = {\n          ...initialStateDefault,\n          browser: {\n            zset: {\n              ...initialState,\n              data: {\n                ...initialState.data,\n                total: 2,\n              },\n            },\n          },\n        }\n\n        const mockedStore = mockStore(nextState)\n\n        // Act\n        await mockedStore.dispatch<any>(deleteZSetMembers(key, members))\n\n        // Assert\n        const expectedActions = [\n          removeZsetMembers(),\n          removeZsetMembersSuccess(),\n          removeMembersFromList(members),\n          deleteSelectedKeySuccess(),\n          deleteRedisearchKeyFromList(key),\n          addMessageNotification(successMessages.DELETED_KEY(key)),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch delete zset members', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteZSetMembers('key', []))\n\n        // Assert\n        const expectedActions = [\n          removeZsetMembers(),\n          addErrorNotification(responsePayload as AxiosError),\n          removeZsetMembersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('updateZSetMembers', () => {\n      it('succeed to fetch update zset members', async () => {\n        // Arrange\n        const data: AddMembersToZSetDto = {\n          keyName: 'small zset',\n          members: [{ name: '1', score: 1 }],\n        }\n        const responsePayload = { status: 200 }\n\n        apiService.put = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(updateZSetMembers(data, jest.fn()))\n\n        // Assert\n        const expectedActions = [\n          updateScore(),\n          updateScoreSuccess(),\n          updateMembersInList(data.members),\n          refreshKeyInfo(),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('failed to fetch update zset members', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.put = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          updateZSetMembers({ keyName: '', members: [] }, jest.fn(), jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateScore(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateScoreFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchSearchZSetMembers', () => {\n      it('succeed to fetch search zset members', async () => {\n        // Arrange\n        const data = { members: [] }\n        const responsePayload = { status: 200, data }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSearchZSetMembers('key', 0, 20, 'zz'))\n\n        // Assert\n        const expectedActions = [\n          searchZSetMembers('zz'),\n          searchZSetMembersSuccess(data),\n          updateSelectedKeyRefreshTime(Date.now()),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch search zset members', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSearchZSetMembers('key', 0, 20, 'zz'))\n\n        // Assert\n        const expectedActions = [\n          searchZSetMembers('zz'),\n          addErrorNotification(responsePayload as AxiosError),\n          searchZSetMembersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchSearchMoreZSetMembers', () => {\n      it('succeed to fetch search more zset members', async () => {\n        // Arrange\n        const responseData = { members: [] }\n        const responsePayload = { status: 200, data: responseData }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchSearchMoreZSetMembers('key', 0, 20, 'zz'),\n        )\n\n        // Assert\n        const expectedActions = [\n          searchMoreZSetMembers('zz'),\n          searchMoreZSetMembersSuccess(responseData),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch search more zset members', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchSearchMoreZSetMembers('key', 0, 20, 'zz'),\n        )\n\n        // Assert\n        const expectedActions = [\n          searchMoreZSetMembers('zz'),\n          addErrorNotification(responsePayload as AxiosError),\n          searchMoreZSetMembersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('refreshStringMembersAction', () => {\n      it('succeed to fetch zset members after refresh', async () => {\n        // Arrange\n        const data = {\n          keyName: 'small zset',\n          ttl: 2146616656,\n          total: 1,\n          size: 67,\n          members: [{ name: '1', score: '1' }],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(refreshZsetMembersAction(data.keyName))\n\n        // Assert\n        const expectedActions = [\n          loadZSetMembers([SortOrder.ASC, undefined]),\n          loadZSetMembersSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch zset members after refresh', async () => {\n        // Arrange\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(refreshZsetMembersAction())\n\n        // Assert\n        const expectedActions = [\n          loadZSetMembers([SortOrder.ASC, undefined]),\n          addErrorNotification(responsePayload as AxiosError),\n          loadZSetMembersFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('succeed to search zset members after refresh', async () => {\n        // Arrange\n        const data = { members: [] }\n        const responsePayload = { status: 200, data }\n        // Act\n        const nextState = {\n          ...initialStateDefault,\n          browser: {\n            zset: {\n              ...initialState,\n              searching: true,\n            },\n          },\n        }\n        const mockedStore = mockStore(nextState)\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        await mockedStore.dispatch<any>(refreshZsetMembersAction())\n\n        // Assert\n        const expectedActions = [\n          searchZSetMembers(''),\n          searchZSetMembersSuccess(data),\n        ]\n\n        expect(mockedStore.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport { AxiosError } from 'axios'\nimport { SendCommandResponse } from 'src/modules/cli/dto/cli.dto'\nimport { AppDispatch, RootState } from 'uiSrc/slices/store'\nimport {\n  cleanup,\n  clearStoreActions,\n  initialStateDefault,\n  mockedStore,\n  mockStore,\n} from 'uiSrc/utils/test-utils'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { apiService } from 'uiSrc/services'\nimport { cliTexts } from 'uiSrc/components/messages/cli-output/cliOutput'\nimport { cliParseTextResponseWithOffset } from 'uiSrc/utils/cliHelper'\nimport ApiErrors from 'uiSrc/constants/apiErrors'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport reducer, {\n  concatToOutput,\n  fetchMonitorLog,\n  initialState,\n  outputSelector,\n  sendCliClusterCommandAction,\n  sendCliCommand,\n  sendCliCommandAction,\n  sendCliCommandFailure,\n  sendCliCommandSuccess,\n  setCliDbIndex,\n  updateCliCommandHistory,\n} from '../../cli/cli-output'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\njest.mock('uiSrc/slices/cli/cli-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/cli-settings'),\n  updateCliClientAction: jest\n    .fn()\n    .mockImplementation((_dispatch: AppDispatch, stateInit: () => RootState) =>\n      stateInit(),\n    ),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('cliOutput slice', () => {\n  describe('concatToOutput', () => {\n    it('should properly concat a new array to existed output', () => {\n      const data = ['\\n\\n', 'tatata']\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, concatToOutput(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          output: nextState,\n        },\n      })\n      expect(outputSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateCliCommandHistory', () => {\n    it('should properly updated cli history output', () => {\n      const data = ['lalal', 'tatata']\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        commandHistory: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateCliCommandHistory(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          output: nextState,\n        },\n      })\n      expect(outputSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('sendCliCommand', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, sendCliCommand())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          output: nextState,\n        },\n      })\n      expect(outputSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('sendCliCommandSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, sendCliCommandSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          output: nextState,\n        },\n      })\n      expect(outputSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('sendCliCommandFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, sendCliCommandFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          output: nextState,\n        },\n      })\n      expect(outputSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setCliDbIndex', () => {\n    it('should set correct value', () => {\n      // Arrange\n      const db = 1\n      const state: typeof initialState = { ...initialState, db }\n\n      // Act\n      const nextState = reducer(initialState, setCliDbIndex(db))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          output: nextState,\n        },\n      })\n      expect(outputSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('Standalone Cli command', () => {\n      it('call both sendCliStandaloneCommandAction and sendCliCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const command = 'keys *'\n        const data = {\n          response: 'tatata',\n          status: CommandExecutionStatus.Success,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandSuccess(),\n          concatToOutput(\n            cliParseTextResponseWithOffset(data.response, command, data.status),\n          ),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call setCliDbIndex when response status is successed', async () => {\n        // Arrange\n        const dbIndex = 1\n        const command = `SELECT ${dbIndex}`\n        const data = { response: 'OK', status: CommandExecutionStatus.Success }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandSuccess(),\n          concatToOutput(\n            cliParseTextResponseWithOffset(data.response, command, data.status),\n          ),\n          setCliDbIndex(dbIndex),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('should not call setCliDbIndex when response status is failed', async () => {\n        // Arrange\n        const dbIndex = 1\n        const command = `SELECT ${dbIndex}`\n        const data = { response: 'OK', status: CommandExecutionStatus.Fail }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandSuccess(),\n          concatToOutput(\n            cliParseTextResponseWithOffset(data.response, command, data.status),\n          ),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendCliStandaloneCommandAction and sendCliCommandSuccess when response status is fail', async () => {\n        // Arrange\n        const command = 'keys *'\n        const data = {\n          response: '(err) tatata',\n          status: CommandExecutionStatus.Fail,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandSuccess(),\n          concatToOutput(\n            cliParseTextResponseWithOffset(data.response, command, data.status),\n          ),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendCliStandaloneCommandAction and sendCliCommandFailure when fetch is fail', async () => {\n        // Arrange\n        const command = 'keys *'\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandFailure(responsePayload.response.data.message),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both updateCliClientAction on ClientNotFound error', async () => {\n        // Arrange\n        const command = 'keys *'\n        const errorMessage = cliTexts.CONNECTION_CLOSED\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: errorMessage, name: ApiErrors.ClientNotFound },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n        const rootState = Object.assign(initialStateDefault, {\n          cli: {\n            settings: {\n              ...initialStateDefault.cli.settings,\n              cliClientUuid: '123',\n            },\n          },\n        })\n        const tempStore = mockStore(rootState)\n\n        // Act\n        await tempStore.dispatch<any>(sendCliCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandFailure(responsePayload.response.data.message),\n        ]\n        expect(clearStoreActions(tempStore.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('Single Node Cluster Cli command', () => {\n      it('call both sendCliClusterCommandAction and sendCliCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const command = 'keys *'\n        const data: SendCommandResponse = {\n          response: '(nil)',\n          status: CommandExecutionStatus.Success,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliClusterCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandSuccess(),\n          concatToOutput(\n            cliParseTextResponseWithOffset(data.response, command, data.status),\n          ),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendCliClusterCommandAction and sendCliCommandSuccess when response status is fail', async () => {\n        // Arrange\n        const command = 'keys *'\n        const data: SendCommandResponse = {\n          response: null,\n          status: CommandExecutionStatus.Success,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliClusterCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandSuccess(),\n          concatToOutput(\n            cliParseTextResponseWithOffset(data.response, command, data.status),\n          ),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendCliClusterCommandAction and sendCliCommandFailure when fetch is fail', async () => {\n        // Arrange\n        const command = 'keys *'\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendCliClusterCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandFailure(responsePayload.response.data.message),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both updateCliClientAction on ClientNotFound error', async () => {\n        // Arrange\n        const command = 'keys *'\n        const errorMessage = cliTexts.CONNECTION_CLOSED\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: errorMessage, name: ApiErrors.ClientNotFound },\n          },\n        }\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n        const rootState = Object.assign(initialStateDefault, {\n          cli: {\n            settings: {\n              ...initialStateDefault.cli.settings,\n              cliClientUuid: '123',\n            },\n          },\n        })\n        const tempStore = mockStore(rootState)\n\n        // Act\n        await tempStore.dispatch<any>(sendCliClusterCommandAction(command))\n\n        // Assert\n        const expectedActions = [\n          sendCliCommand(),\n          sendCliCommandFailure(responsePayload.response.data.message),\n        ]\n        expect(clearStoreActions(tempStore.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n  })\n\n  describe('fetchMonitorLog', () => {\n    it('call both sendCliCommand and sendCliCommandSuccess when fetch is successed', async () => {\n      // Arrange\n      const fileIdMock = 'fileId'\n      const onSuccessActionMock = jest.fn()\n      const data = 'test'\n      const responsePayload = { data, status: 200 }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        fetchMonitorLog(fileIdMock, onSuccessActionMock),\n      )\n\n      // Assert\n      const expectedActions = [sendCliCommand(), sendCliCommandSuccess()]\n      expect(store.getActions()).toEqual(expectedActions)\n      expect(onSuccessActionMock).toBeCalled()\n    })\n\n    it('call both sendCliCommand and sendCliCommandFailure when fetch is fail', async () => {\n      // Arrange\n      const fileIdMock = 'fileId'\n      const onSuccessActionMock = jest.fn()\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        fetchMonitorLog(fileIdMock, onSuccessActionMock),\n      )\n\n      // Assert\n      const expectedActions = [\n        sendCliCommand(),\n        addErrorNotification(responsePayload as AxiosError),\n        sendCliCommandFailure(responsePayload.response.data.message),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n      expect(onSuccessActionMock).not.toBeCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/cli/cli-settings.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  mockedStore,\n  initialStateDefault,\n  clearStoreActions,\n} from 'uiSrc/utils/test-utils'\nimport { setCliDbIndex } from 'uiSrc/slices/cli/cli-output'\nimport { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers'\nimport reducer, {\n  initialState,\n  toggleCli,\n  resetCliSettings,\n  processCliClient,\n  processCliClientSuccess,\n  processCliClientFailure,\n  cliSettingsSelector,\n  createCliClientAction,\n  updateCliClientAction,\n  deleteCliClientAction,\n  deleteCliClientSuccess,\n  getUnsupportedCommandsSuccess,\n  fetchUnsupportedCliCommandsAction,\n  toggleCliHelper,\n  setMatchedCommand,\n  setCliEnteringCommand,\n  setSearchedCommand,\n  setSearchingCommandFilter,\n  setSearchingCommand,\n  clearSearchingCommand,\n  resetCliClientUuid,\n  resetCliHelperSettings,\n  goBackFromCommand,\n} from '../../cli/cli-settings'\n\nlet mathRandom: jest.SpyInstance<number>\nconst random = 0.91911\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n  localStorageService: {\n    set: jest.fn(),\n    get: jest.fn(),\n  },\n}))\n\ndescribe('cliSettings slice', () => {\n  beforeAll(() => {\n    mathRandom = jest.spyOn(Math, 'random').mockImplementation(() => random)\n  })\n\n  afterAll(() => {\n    mathRandom.mockRestore()\n  })\n\n  describe('toggleCliHelper', () => {\n    it('default state.isShowHelper should be falsy', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowHelper: false,\n        isMinimizedHelper: false,\n      }\n\n      expect(cliSettingsSelector(initialStateDefault)).toEqual(state)\n    })\n\n    it('should properly set !isShowHelper', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowHelper: true,\n        isMinimizedHelper: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, toggleCliHelper())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('toggleCli', () => {\n    it('should properly set !isShowCli', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowCli: true,\n        isMinimizedHelper: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, toggleCli())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setMatchedCommand', () => {\n    it('should properly set matchedCommand', () => {\n      // Arrange\n      const matchedCommand = 'get'\n      const state: typeof initialState = {\n        ...initialState,\n        matchedCommand,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setMatchedCommand(matchedCommand))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setCliEnteringCommand', () => {\n    it('should properly set isEnteringCommand = true', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isEnteringCommand: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setCliEnteringCommand())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSearchedCommand', () => {\n    it('should properly set searched command', () => {\n      // Arrange\n      const searchedCommand = 'SET'\n      const state: typeof initialState = {\n        ...initialState,\n        searchedCommand,\n        isSearching: false,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setSearchedCommand(searchedCommand),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSearchingCommand', () => {\n    it('should properly set searching command', () => {\n      // Arrange\n      const searchingCommand = 'se'\n      const state: typeof initialState = {\n        ...initialState,\n        searchingCommand,\n        isEnteringCommand: false,\n        isSearching: true,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setSearchingCommand(searchingCommand),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSearchingCommandFilter', () => {\n    it('should properly set searching command filter', () => {\n      // Arrange\n      const searchingCommandFilter = 'server'\n      const state: typeof initialState = {\n        ...initialState,\n        searchingCommandFilter,\n        isEnteringCommand: false,\n        isSearching: true,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setSearchingCommandFilter(searchingCommandFilter),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('clearSearchingCommand', () => {\n    it('should properly clear search', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        searchingCommand: '',\n        searchedCommand: '',\n        searchingCommandFilter: '',\n        isSearching: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, clearSearchingCommand())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetCliSettings', () => {\n    it('should properly set isShowCli = false', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowCli: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetCliSettings())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetCliClientUuid', () => {\n    it('should properly set cliClientUuid = \"\"', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        cliClientUuid: '',\n      }\n\n      // Act\n      const nextState = reducer(\n        { ...initialState, cliClientUuid: '123' },\n        resetCliClientUuid(),\n      )\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        cli: {\n          settings: nextState,\n        },\n      }\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetCliHelperSettings', () => {\n    it('should properly set Cli Helper settings to default', () => {\n      // Arrange\n      const initState: typeof initialState = {\n        ...initialState,\n        isShowHelper: true,\n        isSearching: true,\n        isEnteringCommand: true,\n        isMinimizedHelper: true,\n        matchedCommand: '123',\n        searchingCommand: '123',\n        searchedCommand: '123',\n        searchingCommandFilter: '123',\n      }\n\n      const state: typeof initialState = {\n        ...initialState,\n        isShowHelper: false,\n        isSearching: false,\n        isEnteringCommand: false,\n        isMinimizedHelper: false,\n        matchedCommand: '',\n        searchingCommand: '',\n        searchedCommand: '',\n        searchingCommandFilter: '',\n      }\n\n      // Act\n      const nextState = reducer(initState, resetCliHelperSettings())\n\n      // Assert\n      const rootState = {\n        ...initialStateDefault,\n        cli: {\n          settings: nextState,\n        },\n      }\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('goBackFromCommand', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const initState: typeof initialState = {\n        ...initialState,\n        isShowHelper: true,\n        isSearching: true,\n        isEnteringCommand: true,\n        isMinimizedHelper: true,\n        matchedCommand: '123',\n        searchingCommand: '123',\n        searchedCommand: '123',\n        searchingCommandFilter: '123',\n      }\n\n      const state: typeof initialState = {\n        ...initState,\n        matchedCommand: '',\n        searchedCommand: '',\n        isSearching: true,\n      }\n\n      // Act\n      const nextState = reducer(initState, goBackFromCommand())\n\n      // Assert\n      const rootState = { ...initialStateDefault, cli: { settings: nextState } }\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('processCliClient', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, processCliClient())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('processCliClientSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = { uuid: '70b95d32-c19d-4311-bb24-e684af12cf15' }\n      const state = {\n        ...initialState,\n        loading: false,\n        cliClientUuid: data.uuid,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        processCliClientSuccess(data?.uuid),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data = { uuid: '' }\n\n      const state = {\n        ...initialState,\n        loading: false,\n        cliClientUuid: data.uuid,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        processCliClientSuccess(data?.uuid),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('processCliClientFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        errorClient: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, processCliClientFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUnsupportedCommandsSuccess', () => {\n    it('should properly set unsupportedCommands', () => {\n      // Arrange\n      const data = ['sync', 'subscribe']\n      const state = {\n        ...initialState,\n        loading: false,\n        unsupportedCommands: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getUnsupportedCommandsSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          settings: nextState,\n        },\n      })\n      expect(cliSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    it('call both createCliClientAction and processCliClientSuccess when fetch is successed', async () => {\n      // Arrange\n      const data = { uuid: '70b95d32-c19d-4311-bb24-e684af12cf15' }\n      const responsePayload = { data, status: 200 }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        createCliClientAction(INSTANCE_ID_MOCK, () => {}),\n      )\n\n      // Assert\n      const expectedActions = [\n        processCliClient(),\n        processCliClientSuccess(responsePayload.data?.uuid),\n        setCliDbIndex(0),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('call both createCliClientAction and processCliClientFailure when fetch is fail', async () => {\n      // Arrange\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(\n        createCliClientAction(INSTANCE_ID_MOCK, () => {}),\n      )\n\n      // Assert\n      const expectedActions = [\n        processCliClient(),\n        processCliClientFailure(responsePayload.response.data.message),\n      ]\n      expect(clearStoreActions(store.getActions())).toEqual(\n        clearStoreActions(expectedActions),\n      )\n    })\n\n    it('call both updateCliClientAction and processCliClientSuccess when fetch is successed', async () => {\n      // Arrange\n      const uuid = '70b95d32-c19d-4311-bb24-e684af12cf15'\n      const data = { uuid }\n      const responsePayload = { data, status: 200 }\n\n      apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(updateCliClientAction(uuid))\n\n      // Assert\n      const expectedActions = [\n        processCliClient(),\n        processCliClientSuccess(responsePayload.data?.uuid),\n        setCliDbIndex(0),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both updateCliClientAction and processCliClientFailure when fetch is fail', async () => {\n      // Arrange\n      const uuid = '70b95d32-c19d-4311-bb24-e684af12cf15'\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.patch = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(updateCliClientAction(uuid))\n\n      // Assert\n      const expectedActions = [\n        processCliClient(),\n        processCliClientFailure(responsePayload.response.data.message),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both deleteCliClientAction and deleteCliClientSuccess when fetch is successed', async () => {\n      // Arrange\n      const uuid = '70b95d32-c19d-4311-bb24-e684af12cf15'\n      const data = { uuid }\n      const responsePayload = { data, status: 200 }\n\n      apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(deleteCliClientAction(uuid))\n\n      // Assert\n      const expectedActions = [processCliClient(), deleteCliClientSuccess()]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both deleteCliClientAction and processCliClientFailure when fetch is fail', async () => {\n      // Arrange\n      const uuid = '70b95d32-c19d-4311-bb24-e684af12cf15'\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(deleteCliClientAction(uuid))\n\n      // Assert\n      const expectedActions = [\n        processCliClient(),\n        processCliClientFailure(responsePayload.response.data.message),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchUnsupportedCliCommandsAction and getUnsupportedCommandsSuccess when fetch is successed', async () => {\n      // Arrange\n      const data = ['sync', 'subscribe']\n      const responsePayload = { data, status: 200 }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchUnsupportedCliCommandsAction())\n\n      // Assert\n      const expectedActions = [\n        processCliClient(),\n        getUnsupportedCommandsSuccess(data),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/cli/monitor.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport MockedSocket from 'socket.io-mock'\nimport {\n  cleanup,\n  mockedStore,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { MOCK_TIMESTAMP } from 'uiSrc/mocks/data/dateNow'\nimport reducer, {\n  initialState,\n  resetMonitorItems,\n  monitorSelector,\n  toggleMonitor,\n  showMonitor,\n  toggleHideMonitor,\n  stopMonitor,\n  setError,\n  togglePauseMonitor,\n  startMonitor,\n  setStartTimestamp,\n  setSocket,\n  concatMonitorItems,\n  MONITOR_ITEMS_MAX_COUNT,\n} from '../../cli/monitor'\n\nlet store: typeof mockedStore\nlet socket: typeof MockedSocket\nlet dateNow: jest.SpyInstance<number>\n\nbeforeEach(() => {\n  cleanup()\n  socket = new MockedSocket()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('monitor slice', () => {\n  beforeAll(() => {\n    dateNow = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_TIMESTAMP)\n  })\n\n  afterAll(() => {\n    dateNow.mockRestore()\n  })\n\n  describe('toggleMonitor', () => {\n    it('default state.isShowMonitor should be falsy', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowMonitor: false,\n        isMinimizedMonitor: false,\n      }\n\n      expect(monitorSelector(initialStateDefault)).toEqual(state)\n    })\n\n    it('should properly set isShowMonitor = true', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowMonitor: true,\n        isMinimizedMonitor: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, toggleMonitor())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('showMonitor', () => {\n    it('should properly set !isShowMonitor', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowMonitor: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, showMonitor())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('toggleHideMonitor', () => {\n    it('should properly set !isShowMonitor', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isMinimizedMonitor: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, toggleHideMonitor())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('startMonitor', () => {\n    it('should properly set new state', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isRunning: true,\n        isSaveToFile: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, startMonitor(true))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setStartTimestamp', () => {\n    it('should properly set new state', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        timestamp: {\n          ...initialState.timestamp,\n          start: MOCK_TIMESTAMP,\n          unPaused: MOCK_TIMESTAMP,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setStartTimestamp(MOCK_TIMESTAMP))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('togglePauseMonitor', () => {\n    it('should properly set new state', () => {\n      // Arrange\n      const diffTimestamp = 5\n      const intermediateState = reducer(\n        initialState,\n        setStartTimestamp(MOCK_TIMESTAMP - diffTimestamp),\n      )\n      const state: typeof intermediateState = {\n        ...intermediateState,\n        isPaused: true,\n        timestamp: {\n          ...intermediateState.timestamp,\n          paused: MOCK_TIMESTAMP,\n          duration: diffTimestamp,\n        },\n      }\n\n      // Act\n      const nextState = reducer(intermediateState, togglePauseMonitor())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('stopMonitor', () => {\n    it('should properly set new state', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isRunning: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, stopMonitor())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSocket', () => {\n    it('should properly set setSocket = mockedSocket', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        socket,\n        isStarted: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSocket(socket))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('concatMonitorItems', () => {\n    it('should properly set payload to items', () => {\n      const payload = [\n        {\n          time: '1',\n          args: ['monitor'],\n          source: 'source',\n          database: 0,\n          shardOptions: { host: '127.0.0.1', port: 6379 },\n        },\n        {\n          time: '2',\n          args: ['get'],\n          source: 'source',\n          database: 0,\n          shardOptions: { host: '127.0.0.1', port: 6379 },\n        },\n      ]\n\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        items: payload,\n      }\n\n      // Act\n      const nextState = reducer(initialState, concatMonitorItems(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set items no more than MONITOR_ITEMS_MAX_COUNT', () => {\n      const payload = new Array(MONITOR_ITEMS_MAX_COUNT + 10)\n\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        items: new Array(MONITOR_ITEMS_MAX_COUNT),\n      }\n\n      // Act\n      const nextState = reducer(initialState, concatMonitorItems(payload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetMonitorItems', () => {\n    it('should properly set isShowMonitor = false', () => {\n      // Arrange\n      const state: typeof initialState = {\n        ...initialState,\n        isShowMonitor: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, resetMonitorItems())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setError', () => {\n    it('should properly set an Error', () => {\n      // Arrange\n      const error = 'Some error'\n      const state: typeof initialState = {\n        ...initialState,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setError(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        cli: {\n          monitor: nextState,\n        },\n      })\n      expect(monitorSelector(rootState)).toEqual(state)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/content/create-redis-buttons.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { resourcesService } from 'uiSrc/services'\nimport reducer, {\n  initialState,\n  getContent,\n  getContentSuccess,\n  getContentFailure,\n  contentSelector,\n  fetchContentAction,\n} from '../../content/create-redis-buttons'\n\nconst MOCK_CONTENT = {\n  cloud: {\n    title: 'Limited offer.',\n    description: 'Try Redis Cloud.',\n    links: {\n      main: {\n        altText: 'Try Redis Cloud.',\n        url: 'url',\n      },\n    },\n  },\n}\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getContent', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getContent())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        content: { createRedisButtons: nextState },\n      })\n\n      expect(contentSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getContentSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const data = MOCK_CONTENT\n      const state = {\n        ...initialState,\n        data,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getContentSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        content: { createRedisButtons: nextState },\n      })\n\n      expect(contentSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getContentFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getContentFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        content: { createRedisButtons: nextState },\n      })\n\n      expect(contentSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('fetchContentAction', () => {\n    it('succeed to fetch content', async () => {\n      // Arrange\n      const data = MOCK_CONTENT\n      const responsePayload = { status: 200, data }\n\n      resourcesService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchContentAction())\n\n      // Assert\n      const expectedActions = [getContent(), getContentSuccess(data)]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch content', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      resourcesService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchContentAction())\n\n      // Assert\n      const expectedActions = [getContent(), getContentFailure(errorMessage)]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/content/guide-links.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { resourcesService } from 'uiSrc/services'\nimport { MOCK_EXPLORE_GUIDES } from 'uiSrc/constants/mocks/mock-explore-guides'\nimport reducer, {\n  initialState,\n  getGuideLinks,\n  getGuideLinksSuccess,\n  getGuideLinksFailure,\n  fetchGuideLinksAction,\n  guideLinksSelector,\n} from '../../content/guide-links'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getGuideLinks', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getGuideLinks())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        content: { guideLinks: nextState },\n      })\n\n      expect(guideLinksSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getGuideLinksSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const data = MOCK_EXPLORE_GUIDES\n      const state = {\n        ...initialState,\n        data,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getGuideLinksSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        content: { guideLinks: nextState },\n      })\n\n      expect(guideLinksSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getGuideLinksFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getGuideLinksFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        content: { guideLinks: nextState },\n      })\n\n      expect(guideLinksSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('fetchGuideLinksAction', () => {\n    it('succeed to fetch content', async () => {\n      // Arrange\n      const data = MOCK_EXPLORE_GUIDES\n      const responsePayload = { status: 200, data }\n\n      resourcesService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchGuideLinksAction())\n\n      // Assert\n      const expectedActions = [getGuideLinks(), getGuideLinksSuccess(data)]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch content', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      resourcesService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchGuideLinksAction())\n\n      // Assert\n      const expectedActions = [\n        getGuideLinks(),\n        getGuideLinksFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/azure.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { faker } from '@faker-js/faker'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  azureSelector,\n  loadSubscriptionsAzure,\n  loadSubscriptionsAzureSuccess,\n  loadSubscriptionsAzureFailure,\n  setSelectedSubscriptionAzure,\n  loadDatabasesAzure,\n  loadDatabasesAzureSuccess,\n  loadDatabasesAzureFailure,\n  addDatabasesAzure,\n  addDatabasesAzureSuccess,\n  addDatabasesAzureFailure,\n  resetDataAzure,\n  clearSubscriptionsAzure,\n  clearDatabasesAzure,\n  fetchSubscriptionsAzure,\n  fetchDatabasesAzure,\n  addDatabasesAzureAction,\n} from '../../instances/azure'\nimport {\n  ActionStatus,\n  AzureAccessKeysStatus,\n  AzureRedisDatabase,\n  AzureRedisType,\n  AzureSubscription,\n  LoadedAzure,\n} from '../../interfaces'\nimport {\n  addErrorNotification,\n  IAddInstanceErrorPayload,\n} from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet subscriptions: AzureSubscription[]\nlet databases: AzureRedisDatabase[]\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  subscriptions = [\n    {\n      subscriptionId: faker.string.uuid(),\n      displayName: faker.company.name(),\n      state: 'Enabled',\n    },\n    {\n      subscriptionId: faker.string.uuid(),\n      displayName: faker.company.name(),\n      state: 'Enabled',\n    },\n  ]\n\n  databases = [\n    {\n      id: faker.string.uuid(),\n      name: faker.internet.domainWord(),\n      host: faker.internet.domainName(),\n      port: 6379,\n      location: 'eastus',\n      type: AzureRedisType.Standard,\n      accessKeysAuthentication: AzureAccessKeysStatus.Enabled,\n      provisioningState: 'Succeeded',\n      resourceGroup: faker.string.alphanumeric(10),\n      subscriptionId: subscriptions[0].subscriptionId,\n    },\n    {\n      id: faker.string.uuid(),\n      name: faker.internet.domainWord(),\n      host: faker.internet.domainName(),\n      port: 6380,\n      location: 'westus',\n      type: AzureRedisType.Enterprise,\n      accessKeysAuthentication: AzureAccessKeysStatus.Disabled,\n      provisioningState: 'Succeeded',\n      resourceGroup: faker.string.alphanumeric(10),\n      subscriptionId: subscriptions[0].subscriptionId,\n    },\n  ]\n})\n\ndescribe('azure slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      const nextState = initialState\n      const result = reducer(undefined, { type: '' })\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadSubscriptionsAzure', () => {\n    it('should properly set the state before the fetch data', () => {\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      const nextState = reducer(initialState, loadSubscriptionsAzure())\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSubscriptionsAzureSuccess', () => {\n    it('should properly set the state with fetched subscriptions', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedAzure.Subscriptions]: true,\n        },\n        subscriptions,\n      }\n\n      const nextState = reducer(\n        initialState,\n        loadSubscriptionsAzureSuccess(subscriptions),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedAzure.Subscriptions]: true,\n        },\n        subscriptions: [],\n      }\n\n      const nextState = reducer(initialState, loadSubscriptionsAzureSuccess([]))\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSubscriptionsAzureFailure', () => {\n    it('should properly set the error', () => {\n      const errorMessage = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      const nextState = reducer(\n        initialState,\n        loadSubscriptionsAzureFailure(errorMessage),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSelectedSubscriptionAzure', () => {\n    it('should set selected subscription and reset databases when subscription changes', () => {\n      const stateWithData = {\n        ...initialState,\n        databases,\n        databasesAdded: databases,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedAzure.Databases]: true,\n          [LoadedAzure.DatabasesAdded]: true,\n        },\n      }\n\n      const nextState = reducer(\n        stateWithData,\n        setSelectedSubscriptionAzure(subscriptions[0]),\n      )\n\n      expect(nextState.selectedSubscription).toEqual(subscriptions[0])\n      expect(nextState.databases).toBeNull()\n      expect(nextState.databasesAdded).toEqual([])\n      expect(nextState.loaded[LoadedAzure.Databases]).toBe(false)\n      expect(nextState.loaded[LoadedAzure.DatabasesAdded]).toBe(false)\n    })\n\n    it('should not reset databases when same subscription is selected', () => {\n      const stateWithData = {\n        ...initialState,\n        selectedSubscription: subscriptions[0],\n        databases,\n        databasesAdded: databases,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedAzure.Databases]: true,\n          [LoadedAzure.DatabasesAdded]: true,\n        },\n      }\n\n      const nextState = reducer(\n        stateWithData,\n        setSelectedSubscriptionAzure(subscriptions[0]),\n      )\n\n      expect(nextState.selectedSubscription).toEqual(subscriptions[0])\n      expect(nextState.databases).toEqual(databases)\n      expect(nextState.databasesAdded).toEqual(databases)\n      expect(nextState.loaded[LoadedAzure.Databases]).toBe(true)\n      expect(nextState.loaded[LoadedAzure.DatabasesAdded]).toBe(true)\n    })\n  })\n\n  describe('loadDatabasesAzure', () => {\n    it('should properly set the state before the fetch data', () => {\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      const nextState = reducer(initialState, loadDatabasesAzure())\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadDatabasesAzureSuccess', () => {\n    it('should properly set the state with fetched databases', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedAzure.Databases]: true,\n        },\n        databases,\n      }\n\n      const nextState = reducer(\n        initialState,\n        loadDatabasesAzureSuccess(databases),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadDatabasesAzureFailure', () => {\n    it('should properly set the error', () => {\n      const errorMessage = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      const nextState = reducer(\n        initialState,\n        loadDatabasesAzureFailure(errorMessage),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addDatabasesAzure', () => {\n    it('should properly set the state before adding databases', () => {\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      const nextState = reducer(initialState, addDatabasesAzure())\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addDatabasesAzureSuccess', () => {\n    it('should properly set the state with added databases', () => {\n      const stateWithDatabases = {\n        ...initialState,\n        databases,\n      }\n\n      const addResults = [\n        { id: databases[0].id, status: ActionStatus.Success, message: '' },\n        {\n          id: databases[1].id,\n          status: ActionStatus.Fail,\n          message: 'Connection failed',\n        },\n      ]\n\n      const nextState = reducer(\n        stateWithDatabases,\n        addDatabasesAzureSuccess(addResults),\n      )\n\n      expect(nextState.loading).toBe(false)\n      expect(nextState.loaded[LoadedAzure.DatabasesAdded]).toBe(true)\n      expect(nextState.databasesAdded).toHaveLength(2)\n      expect(nextState.databasesAdded[0].statusAdded).toBe(ActionStatus.Success)\n      expect(nextState.databasesAdded[1].statusAdded).toBe(ActionStatus.Fail)\n      expect(nextState.databasesAdded[1].messageAdded).toBe('Connection failed')\n    })\n  })\n\n  describe('addDatabasesAzureFailure', () => {\n    it('should properly set the error', () => {\n      const errorMessage = 'Failed to add databases'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      const nextState = reducer(\n        initialState,\n        addDatabasesAzureFailure(errorMessage),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: { azure: nextState },\n      })\n      expect(azureSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetDataAzure', () => {\n    it('should reset state to initial state', () => {\n      const stateWithData = {\n        ...initialState,\n        loading: true,\n        error: 'some error',\n        subscriptions,\n        selectedSubscription: subscriptions[0],\n        databases,\n        databasesAdded: databases,\n        loaded: {\n          [LoadedAzure.Subscriptions]: true,\n          [LoadedAzure.Databases]: true,\n          [LoadedAzure.DatabasesAdded]: true,\n        },\n      }\n\n      const nextState = reducer(stateWithData, resetDataAzure())\n\n      expect(nextState).toEqual(initialState)\n    })\n  })\n\n  describe('clearSubscriptionsAzure', () => {\n    it('should clear subscriptions and all dependent data', () => {\n      const stateWithData = {\n        ...initialState,\n        subscriptions,\n        selectedSubscription: subscriptions[0],\n        databases,\n        databasesAdded: databases,\n        loaded: {\n          [LoadedAzure.Subscriptions]: true,\n          [LoadedAzure.Databases]: true,\n          [LoadedAzure.DatabasesAdded]: true,\n        },\n      }\n\n      const nextState = reducer(stateWithData, clearSubscriptionsAzure())\n\n      expect(nextState.subscriptions).toBeNull()\n      expect(nextState.selectedSubscription).toBeNull()\n      expect(nextState.databases).toBeNull()\n      expect(nextState.databasesAdded).toEqual([])\n      expect(nextState.loaded[LoadedAzure.Subscriptions]).toBe(false)\n      expect(nextState.loaded[LoadedAzure.Databases]).toBe(false)\n      expect(nextState.loaded[LoadedAzure.DatabasesAdded]).toBe(false)\n    })\n  })\n\n  describe('clearDatabasesAzure', () => {\n    it('should clear only databases and preserve subscriptions', () => {\n      const stateWithData = {\n        ...initialState,\n        subscriptions,\n        databases,\n        databasesAdded: databases,\n        loaded: {\n          [LoadedAzure.Subscriptions]: true,\n          [LoadedAzure.Databases]: true,\n          [LoadedAzure.DatabasesAdded]: true,\n        },\n      }\n\n      const nextState = reducer(stateWithData, clearDatabasesAzure())\n\n      expect(nextState.subscriptions).toEqual(subscriptions)\n      expect(nextState.databases).toBeNull()\n      expect(nextState.databasesAdded).toEqual([])\n      expect(nextState.loaded[LoadedAzure.Subscriptions]).toBe(true)\n      expect(nextState.loaded[LoadedAzure.Databases]).toBe(false)\n      expect(nextState.loaded[LoadedAzure.DatabasesAdded]).toBe(false)\n    })\n  })\n\n  // Thunk actions\n  describe('thunk actions', () => {\n    describe('fetchSubscriptionsAzure', () => {\n      it('should dispatch success action when fetch succeeds', async () => {\n        const accountId = faker.string.uuid()\n        const responsePayload = { data: subscriptions, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(fetchSubscriptionsAzure(accountId))\n\n        const expectedActions = [\n          loadSubscriptionsAzure(),\n          loadSubscriptionsAzureSuccess(subscriptions),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should dispatch failure action when fetch fails', async () => {\n        const accountId = faker.string.uuid()\n        const errorMessage = 'Failed to fetch subscriptions'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        await store.dispatch<any>(fetchSubscriptionsAzure(accountId))\n\n        const expectedActions = [\n          loadSubscriptionsAzure(),\n          loadSubscriptionsAzureFailure(errorMessage),\n          addErrorNotification(\n            responsePayload as unknown as IAddInstanceErrorPayload,\n          ),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchDatabasesAzure', () => {\n      it('should dispatch success action when fetch succeeds', async () => {\n        const accountId = faker.string.uuid()\n        const subscriptionId = subscriptions[0].subscriptionId\n        const responsePayload = { data: databases, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(\n          fetchDatabasesAzure(accountId, subscriptionId),\n        )\n\n        const expectedActions = [\n          loadDatabasesAzure(),\n          loadDatabasesAzureSuccess(databases),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should dispatch failure action when fetch fails', async () => {\n        const accountId = faker.string.uuid()\n        const subscriptionId = subscriptions[0].subscriptionId\n        const errorMessage = 'Failed to fetch databases'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        await store.dispatch<any>(\n          fetchDatabasesAzure(accountId, subscriptionId),\n        )\n\n        const expectedActions = [\n          loadDatabasesAzure(),\n          loadDatabasesAzureFailure(errorMessage),\n          addErrorNotification(\n            responsePayload as unknown as IAddInstanceErrorPayload,\n          ),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addDatabasesAzureAction', () => {\n      it('should dispatch success action when add succeeds', async () => {\n        const accountId = faker.string.uuid()\n        const databaseIds = [databases[0].id, databases[1].id]\n        const addResults = [\n          { id: databases[0].id, status: ActionStatus.Success, message: '' },\n          { id: databases[1].id, status: ActionStatus.Success, message: '' },\n        ]\n        const responsePayload = { data: addResults, status: 201 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(\n          addDatabasesAzureAction(accountId, databaseIds),\n        )\n\n        const expectedActions = [\n          addDatabasesAzure(),\n          addDatabasesAzureSuccess(addResults),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should dispatch failure action when add fails', async () => {\n        const accountId = faker.string.uuid()\n        const databaseIds = [databases[0].id]\n        const errorMessage = 'Failed to add databases'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        const result = await store.dispatch<any>(\n          addDatabasesAzureAction(accountId, databaseIds),\n        )\n\n        const expectedActions = [\n          addDatabasesAzure(),\n          addDatabasesAzureFailure(errorMessage),\n          addErrorNotification(\n            responsePayload as unknown as IAddInstanceErrorPayload,\n          ),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n        expect(result).toEqual([])\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/caCerts.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  loadCaCerts,\n  loadCaCertsSuccess,\n  loadCaCertsFailure,\n  caCertsSelector,\n  fetchCaCerts,\n  deleteCaCertificate,\n  deleteCaCertificateSuccess,\n  deleteCaCertificateFailure,\n  deleteCaCertificateAction,\n} from '../../instances/caCerts'\nimport { addErrorNotification } from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('caCerts slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('deleteCaCertificate', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteCaCertificate())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteCaCertificateSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      const state = {\n        ...initialState,\n        loading: false,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteCaCertificateSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteCaCertificateFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteCaCertificateFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadCaCerts', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadCaCerts())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadCaCertsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = [\n        { id: '70b95d32-c19d-4311-bb24-e684af12cf15', name: 'ca cert' },\n      ]\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadCaCertsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadCaCertsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadCaCertsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadCaCertsFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          caCerts: nextState,\n        },\n      })\n      expect(caCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    it('call both fetchCaCerts and loadCaCertsSuccess when fetch is successed', async () => {\n      // Arrange\n      const data = [\n        { id: '70b95d32-c19d-4311-bb24-e684af12cf15', name: 'ca cert' },\n      ]\n      const responsePayload = { data, status: 200 }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchCaCerts())\n\n      // Assert\n      const expectedActions = [\n        loadCaCerts(),\n        loadCaCertsSuccess(responsePayload.data),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchCaCerts and deleteCaCertificateSuccess when delete ca certificate action is successed', async () => {\n      // Arrange\n      const responsePayload = { status: 200 }\n      apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n      // Arrange\n      const data = [\n        { id: '70b95d32-c19d-4311-bb24-e684af12cf15', name: 'ca cert' },\n      ]\n      const fetchResponsePayload = { data, status: 200 }\n\n      apiService.get = jest.fn().mockResolvedValue(fetchResponsePayload)\n\n      // mock function for onSuccessAction\n      const onSuccessAction = jest.fn()\n\n      const id = '70b95d32-c19d-4311-bb24-e684af12cf15'\n\n      // Act\n      await store.dispatch<any>(deleteCaCertificateAction(id, onSuccessAction))\n\n      // assert that onSuccessAction is called\n      expect(onSuccessAction).toBeCalled()\n\n      // Assert\n      const expectedActions = [\n        deleteCaCertificate(id),\n        deleteCaCertificateSuccess(),\n        loadCaCerts(),\n        loadCaCertsSuccess(fetchResponsePayload.data),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchCaCerts and deleteCaCertificateFailure when delete ca certificate action is failed', async () => {\n      // Arrange\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // mock function for onSuccessAction\n      const onSuccessAction = jest.fn()\n\n      const id = '70b95d32-c19d-4311-bb24-e684af12cf15'\n\n      // Act\n      await store.dispatch<any>(deleteCaCertificateAction(id, onSuccessAction))\n\n      // assert that onSuccessAction is not called\n      expect(onSuccessAction).not.toBeCalled()\n\n      // Assert\n      const expectedActions = [\n        deleteCaCertificate(id),\n        addErrorNotification(responsePayload as AxiosError),\n        deleteCaCertificateFailure(responsePayload.response.data.message),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchCaCerts and loadCaCertsFailure when fetch is fail', async () => {\n      // Arrange\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchCaCerts())\n\n      // Assert\n      const expectedActions = [\n        loadCaCerts(),\n        addErrorNotification(responsePayload as AxiosError),\n        loadCaCertsFailure(responsePayload.response.data.message),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/clientCerts.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  loadClientCerts,\n  loadClientCertsSuccess,\n  loadClientCertsFailure,\n  clientCertsSelector,\n  fetchClientCerts,\n  deleteClientCert,\n  deleteClientCertSuccess,\n  deleteClientCertFailure,\n  deleteClientCertAction,\n} from '../../instances/clientCerts'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('clientCerts slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('deleteClientCert', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteClientCert())\n\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadClientCerts', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadClientCerts())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteClientCertSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteClientCertSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadClientCertsSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const data = [\n        { id: '70b95d32-c19d-4311-bb24-e684af12cf15', name: 'client cert' },\n      ]\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadClientCertsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadClientCertsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteClientCertFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteClientCertFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadClientCertsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadClientCertsFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          clientCerts: nextState,\n        },\n      })\n      expect(clientCertsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    it('call both fetchClientCerts and loadClientCertsSuccess when fetch is successed', async () => {\n      // Arrange\n      const data = [\n        { id: '70b95d32-c19d-4311-bb24-e684af12cf15', name: 'ca cert' },\n      ]\n      const responsePayload = { data, status: 200 }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchClientCerts())\n\n      // Assert\n      const expectedActions = [\n        loadClientCerts(),\n        loadClientCertsSuccess(responsePayload.data),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchClientCerts and deleteClientCertSuccess when delete is successed', async () => {\n      // Arrange delete\n      const responsePayload = { status: 200 }\n      apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n      // Arrange fetch\n      const data = [\n        { id: '70b95d32-c19d-4311-bb24-e684af12cf15', name: 'client cert' },\n      ]\n      const fetchResponsePayload = { data, status: 200 }\n      apiService.get = jest.fn().mockResolvedValue(fetchResponsePayload)\n\n      // mock function for onSuccessAction\n      const onSuccessAction = jest.fn()\n\n      const id = '70b95d32-c19d-4311-bb24-e684af12cf15'\n\n      // Act\n      await store.dispatch<any>(deleteClientCertAction(id, onSuccessAction))\n\n      // Assert onSuccessAction\n      expect(onSuccessAction).toBeCalled()\n\n      // Assert\n      const expectedActions = [\n        deleteClientCertSuccess(),\n        loadClientCerts(),\n        loadClientCertsSuccess(fetchResponsePayload.data),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchClientCerts and deleteClientCertFailure when delete is failed', async () => {\n      // Arrange delete\n      const error = 'some error'\n      const responsePayload = {\n        response: { data: { message: error }, status: 500 },\n      }\n      apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      const onSuccessAction = jest.fn()\n      const id = '70b95d32-c19d-4311-bb24-e684af12cf15'\n\n      // Act\n      await store.dispatch<any>(deleteClientCertAction(id, onSuccessAction))\n\n      // assert that onSuccessAction is not called\n      expect(onSuccessAction).not.toBeCalled()\n\n      // Assert\n      const expectedActions = [\n        addErrorNotification(responsePayload as AxiosError),\n        deleteClientCertFailure(responsePayload.response.data.message),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/cloud.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  GetCloudAccountShortInfoResponse,\n  RedisCloudDatabase,\n} from 'apiSrc/modules/redis-enterprise/dto/cloud.dto'\nimport reducer, {\n  loadSubscriptionsRedisCloud,\n  initialState,\n  loadSubscriptionsRedisCloudSuccess,\n  cloudSelector,\n  loadSubscriptionsRedisCloudFailure,\n  loadAccountRedisCloudFailure,\n  loadAccountRedisCloudSuccess,\n  loadAccountRedisCloud,\n  setSSOFlow,\n  setIsRecommendedSettingsSSO,\n  fetchSubscriptionsRedisCloud,\n  fetchAccountRedisCloud,\n  loadInstancesRedisCloud,\n  fetchInstancesRedisCloud,\n  loadInstancesRedisCloudSuccess,\n  loadInstancesRedisCloudFailure,\n  addInstancesRedisCloud,\n  createInstancesRedisCloudFailure,\n  createInstancesRedisCloud,\n  createInstancesRedisCloudSuccess,\n} from '../../instances/cloud'\nimport { LoadedCloud, RedisCloudSubscriptionType } from '../../interfaces'\nimport { addErrorNotification } from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet account: GetCloudAccountShortInfoResponse\nlet instances: RedisCloudDatabase[]\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  account = {\n    accountId: 40131,\n    accountName: 'Redis Labs',\n    ownerName: 'Danny Cohen',\n    ownerEmail: 'danny.cohen@redis.com',\n  }\n\n  instances = [\n    {\n      subscriptionId: 113214,\n      databaseId: 51121677,\n      name: 'New-DB-without-SSL',\n      publicEndpoint:\n        'redis-11759.c176718.us-east-1-1.ec2.qa-cloud.rlrcp.com:11759',\n      status: 'active',\n      sslClientAuthentication: false,\n      modules: [],\n      options: {\n        enabledDataPersistence: false,\n        persistencePolicy: 'none',\n        enabledRedisFlash: false,\n        enabledReplication: true,\n        enabledBackup: false,\n        enabledClustering: false,\n        isReplicaDestination: false,\n        isReplicaSource: false,\n      },\n    },\n  ]\n})\n\ndescribe('cloud slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadSubscriptionsRedisCloud', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadSubscriptionsRedisCloud())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSubscriptionsRedisCloudSuccess', () => {\n    it('should properly set the state with fetched subscriptions', () => {\n      // Arrange\n\n      const data = [\n        {\n          id: 110358,\n          name: 'Subscription test example with',\n          numberOfDatabases: 1,\n          status: 'active',\n        },\n      ]\n\n      const credentials = {\n        accessKey: '123',\n        secretKey: '123',\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n\n        loaded: {\n          ...initialState.loaded,\n          [LoadedCloud.Subscriptions]: true,\n        },\n        subscriptions: data,\n        credentials,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSubscriptionsRedisCloudSuccess({ data, credentials }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any[] = []\n\n      const credentials = {\n        accessKey: '123',\n        secretKey: '123',\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n\n        loaded: {\n          ...initialState.loaded,\n          [LoadedCloud.Subscriptions]: true,\n        },\n        subscriptions: data,\n        credentials,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSubscriptionsRedisCloudSuccess({ data, credentials }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadSubscriptionsRedisCloudFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadSubscriptionsRedisCloudFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadAccountRedisCloud', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadAccountRedisCloud())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadAccountRedisCloudSuccess', () => {\n    it('should properly set the state with fetched subscriptions', () => {\n      // Arrange\n\n      const data = {\n        accountId: 40131,\n        accountName: 'Redis Labs',\n        ownerName: 'Danny Cohen',\n        ownerEmail: 'danny.cohen@redis.com',\n      }\n\n      const state = {\n        ...initialState,\n        loading: false,\n\n        account: {\n          data,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadAccountRedisCloudSuccess({ data }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = {}\n\n      const credentials = {\n        accessKey: '123',\n        secretKey: '123',\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        account: {\n          data,\n          error: '',\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadAccountRedisCloudSuccess({ data, credentials }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadAccountRedisCloudFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        account: {\n          data: null,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadAccountRedisCloudFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesRedisCloud', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadInstancesRedisCloud())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesRedisCloudSuccess', () => {\n    it('should properly set the state with fetched instances', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: [],\n        loaded: {\n          ...initialState.loaded,\n          [LoadedCloud.Instances]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadInstancesRedisCloudSuccess({ instances }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedCloud.Instances]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadInstancesRedisCloudSuccess({ data }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesRedisCloudFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadInstancesRedisCloudFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createInstancesRedisCloud', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, createInstancesRedisCloud())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createInstancesRedisCloudSuccess', () => {\n    it('should properly set the state with created instances', () => {\n      // Arrange\n\n      const dataAdded = [\n        {\n          databaseIdAdded: 51121677,\n          messageAdded: undefined,\n          statusAdded: 'active',\n          subscriptionIdAdded: 113214,\n          subscriptionName: '',\n        },\n      ]\n\n      const state = {\n        ...initialState,\n        loading: false,\n        dataAdded,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedCloud.InstancesAdded]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createInstancesRedisCloudSuccess(instances),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        dataAdded: data,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedCloud.InstancesAdded]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createInstancesRedisCloudSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createInstancesRedisCloudFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createInstancesRedisCloudFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSSOFlow', () => {\n    it('should properly set setSSOFlow', () => {\n      // Arrange\n      const data = 'import'\n      const state = {\n        ...initialState,\n        ssoFlow: 'import',\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSSOFlow(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setIsRecommendedSettingsSSO', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data = true\n      const state = {\n        ...initialState,\n        isRecommendedSettings: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setIsRecommendedSettingsSSO(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cloud: nextState,\n        },\n      })\n      expect(cloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchSubscriptionsRedisCloud', () => {\n      it('call fetchSubscriptionsRedisCloud, loadSubscriptionsRedisCloud, and loadSubscriptionsRedisCloudSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = [\n          {\n            id: 110358,\n            type: RedisCloudSubscriptionType.Flexible,\n            name: 'Subscription test example with',\n            numberOfDatabases: 1,\n            status: 'active',\n          },\n        ]\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSubscriptionsRedisCloud(credentials))\n\n        // Assert\n        const expectedActions = [\n          loadSubscriptionsRedisCloud(),\n          loadSubscriptionsRedisCloudSuccess({\n            data: responsePayload.data,\n            credentials,\n          }),\n          loadAccountRedisCloud(),\n          loadAccountRedisCloudSuccess({ data }),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call fetchSubscriptionsRedisCloud, loadSubscriptionsRedisCloud, and loadSubscriptionsRedisCloudFailure when fetch is failure', async () => {\n        // Arrange\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchSubscriptionsRedisCloud(credentials))\n\n        // Assert\n        const expectedActions = [\n          loadSubscriptionsRedisCloud(),\n          loadSubscriptionsRedisCloudFailure(\n            responsePayload.response.data.message,\n          ),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchAccountRedisCloud', () => {\n      it('call fetchAccountRedisCloud and loadAccountRedisCloudSuccess when fetch is successed', async () => {\n        // Arrange\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n        const responsePayload = { data: account, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchAccountRedisCloud(credentials))\n\n        // Assert\n        const expectedActions = [\n          loadAccountRedisCloud(),\n          loadAccountRedisCloudSuccess({\n            data: responsePayload.data,\n          }),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call fetchAccountRedisCloud and loadAccountRedisCloudFailure when fetch is failure', async () => {\n        // Arrange\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchAccountRedisCloud(credentials))\n\n        // Assert\n        const expectedActions = [\n          loadAccountRedisCloud(),\n          loadAccountRedisCloudFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchInstancesRedisCloud', () => {\n      it('call fetchInstancesRedisCloud and loadInstancesRedisCloudSuccess when fetch is successed', async () => {\n        // Arrange\n        const subscriptions = [\n          {\n            subscriptionId: 1,\n            subscriptionType: RedisCloudSubscriptionType.Flexible,\n            free: false,\n          },\n          {\n            subscriptionId: 2,\n            subscriptionType: RedisCloudSubscriptionType.Flexible,\n            free: false,\n          },\n          {\n            subscriptionId: 3,\n            subscriptionType: RedisCloudSubscriptionType.Fixed,\n            free: true,\n          },\n        ]\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n        const responsePayload = { data: instances, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchInstancesRedisCloud({ subscriptions, credentials }),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadInstancesRedisCloud(),\n          loadInstancesRedisCloudSuccess({\n            data: responsePayload.data,\n            credentials: { subscriptions, credentials },\n          }),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call fetchInstancesRedisCloud and loadInstancesRedisCloudFailure when fetch is failure', async () => {\n        // Arrange\n        const ids = [1, 2, 3]\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchInstancesRedisCloud({ ids, credentials }),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadInstancesRedisCloud(),\n          loadInstancesRedisCloudFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addInstancesRedisCloud', () => {\n      it('call addInstancesRedisCloud and createInstancesRedisCloudSuccess when fetch is successed', async () => {\n        // Arrange\n        const databasesPicked = [\n          { subscriptionId: '1231', databaseId: '123', free: false },\n        ]\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n        const responsePayload = { data: instances, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          addInstancesRedisCloud({ databases: databasesPicked, credentials }),\n        )\n\n        // Assert\n        const expectedActions = [\n          createInstancesRedisCloud(),\n          createInstancesRedisCloudSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call addInstancesRedisCloud and createInstancesRedisCloudFailure when fetch is failure', async () => {\n        // Arrange\n        const databasesPicked = [\n          { subscriptionId: '1231', databaseId: '123', free: false },\n        ]\n        const credentials = {\n          accessKey: '123',\n          secretKey: '123',\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          addInstancesRedisCloud({ databases: databasesPicked, credentials }),\n        )\n\n        // Assert\n        const expectedActions = [\n          createInstancesRedisCloud(),\n          createInstancesRedisCloudFailure(\n            responsePayload.response.data.message,\n          ),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/cluster.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  ClusterConnectionDetailsDto,\n  RedisEnterpriseDatabase,\n} from 'apiSrc/modules/redis-enterprise/dto/cluster.dto'\nimport { AddRedisEnterpriseDatabaseResponse } from 'apiSrc/modules/redis-enterprise/dto/redis-enterprise-cluster.dto'\nimport reducer, {\n  initialState,\n  loadInstancesRedisCluster,\n  loadInstancesRedisClusterSuccess,\n  loadInstancesRedisClusterFailure,\n  clusterSelector,\n  fetchInstancesRedisCluster,\n  createInstancesRedisCluster,\n  createInstancesRedisClusterSuccess,\n  createInstancesRedisClusterFailure,\n  addInstancesRedisCluster,\n} from '../../instances/cluster'\n\nimport { addErrorNotification } from '../../app/notifications'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet defaultCredentials: ClusterConnectionDetailsDto\nlet defaultData: RedisEnterpriseDatabase[]\nlet defaultDataAdded: AddRedisEnterpriseDatabaseResponse[]\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  defaultCredentials = {\n    host: 'localhost',\n    port: 9443,\n    username: 'e@e.com',\n    password: '1',\n  }\n\n  defaultData = [\n    {\n      uid: 2,\n      name: 'test2',\n      dnsName: 'redis-14249.enterprise',\n      address: '172.17.0.2',\n      port: 14249,\n      status: 'active',\n      tls: false,\n      modules: [],\n      tags: [],\n      options: {\n        enabledDataPersistence: false,\n        persistencePolicy: 'none',\n        enabledRedisFlash: false,\n        enabledReplication: false,\n        enabledBackup: false,\n        enabledActiveActive: false,\n        enabledClustering: false,\n        isReplicaDestination: false,\n        isReplicaSource: false,\n      },\n    },\n    {\n      uid: 1,\n      name: 'test',\n      dnsName: 'redis-12000.enterprise',\n      address: '172.17.0.2',\n      port: 12000,\n      status: 'active',\n      tls: false,\n      modules: ['graph', 'search', 'ReJSON', 'bf', 'timeseries'],\n      tags: [],\n      options: {\n        enabledDataPersistence: true,\n        persistencePolicy: 'aof-every-write',\n        enabledRedisFlash: false,\n        enabledReplication: false,\n        enabledBackup: false,\n        enabledActiveActive: false,\n        enabledClustering: false,\n        isReplicaDestination: false,\n        isReplicaSource: false,\n      },\n    },\n  ]\n\n  defaultDataAdded = [\n    {\n      uid: 1,\n      status: 'success',\n      message: 'Added',\n      databaseDetails: {\n        uid: 1,\n        name: 'test',\n        dnsName: 'redis-12000.enterprise',\n        address: '172.17.0.2',\n        port: 12000,\n        status: 'active',\n        tls: false,\n        modules: ['graph', 'search', 'ReJSON', 'bf', 'timeseries'],\n        tags: [],\n        options: {\n          enabledDataPersistence: true,\n          persistencePolicy: 'aof-every-write',\n          enabledRedisFlash: false,\n          enabledReplication: false,\n          enabledBackup: false,\n          enabledActiveActive: false,\n          enabledClustering: false,\n          isReplicaDestination: false,\n          isReplicaSource: false,\n        },\n      },\n    },\n    {\n      uid: 2,\n      status: 'fail',\n      message:\n        'Could not connect to localhost:11243, please check the connection details.',\n      databaseDetails: {\n        uid: 2,\n        name: 'test2',\n        dnsName: 'redis-14249.enterprise',\n        address: '172.17.0.2',\n        port: 14249,\n        status: 'active',\n        tls: false,\n        modules: [],\n        tags: [],\n        options: {\n          enabledDataPersistence: false,\n          persistencePolicy: 'none',\n          enabledRedisFlash: false,\n          enabledReplication: false,\n          enabledBackup: false,\n          enabledActiveActive: false,\n          enabledClustering: false,\n          isReplicaDestination: false,\n          isReplicaSource: false,\n        },\n      },\n    },\n  ]\n})\n\ndescribe('cluster slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadInstancesRedisCluster', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadInstancesRedisCluster())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesRedisClusterSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        data: defaultData,\n        credentials: defaultCredentials,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadInstancesRedisClusterSuccess({\n          data: defaultData,\n          credentials: defaultCredentials,\n        }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n        credentials: defaultCredentials,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadInstancesRedisClusterSuccess({\n          data,\n          credentials: defaultCredentials,\n        }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesRedisClusterFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadInstancesRedisClusterFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createInstancesRedisCluster', () => {\n    it('should properly set the state before the fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, createInstancesRedisCluster())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createInstancesRedisClusterSuccess', () => {\n    it('should properly set the state with created instances', () => {\n      // Arrange\n\n      const dataAdded = [\n        {\n          uidAdded: 2,\n          statusAdded: 'active',\n          messageAdded: undefined,\n        },\n        {\n          uidAdded: 1,\n          statusAdded: 'active',\n          messageAdded: undefined,\n        },\n      ]\n\n      const state = {\n        ...initialState,\n        loading: false,\n        dataAdded,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createInstancesRedisClusterSuccess(defaultData),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        dataAdded: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createInstancesRedisClusterSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createInstancesRedisClusterFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createInstancesRedisClusterFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          cluster: nextState,\n        },\n      })\n      expect(clusterSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchInstancesRedisCluster', () => {\n      it('call both fetchInstancesRedisCluster and loadInstancesRedisClusterSuccess when fetch is successed', async () => {\n        // Arrange\n        const responsePayload = { data: defaultData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchInstancesRedisCluster(defaultCredentials),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadInstancesRedisCluster(),\n          loadInstancesRedisClusterSuccess({\n            data: responsePayload.data,\n            credentials: defaultCredentials,\n          }),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both fetchInstancesRedisCluster and loadInstancesRedisClusterFailure when fetch is fail', async () => {\n        // Arrange\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchInstancesRedisCluster(defaultCredentials),\n        )\n\n        // Assert\n        const expectedActions = [\n          loadInstancesRedisCluster(),\n          loadInstancesRedisClusterFailure(\n            responsePayload.response.data.message,\n          ),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('addInstancesRedisCluster', () => {\n      it('call both addInstancesRedisCluster and createInstancesRedisClusterSuccess when fetch is successed', async () => {\n        // Arrange\n\n        const uids = [1, 2]\n        const responsePayload = { data: defaultDataAdded, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          addInstancesRedisCluster({ uids, credentials: defaultCredentials }),\n        )\n\n        // Assert\n        const expectedActions = [\n          createInstancesRedisCluster(),\n          createInstancesRedisClusterSuccess(responsePayload.data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both addInstancesRedisCluster and createInstancesRedisClusterFailure when fetch is fail', async () => {\n        // Arrange\n        const uids = [1, 2]\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          addInstancesRedisCluster({ uids, credentials: defaultCredentials }),\n        )\n\n        // Assert\n        const expectedActions = [\n          createInstancesRedisCluster(),\n          createInstancesRedisClusterFailure(\n            responsePayload.response.data.message,\n          ),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/instances.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep, map, omit } from 'lodash'\n\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { CustomErrorCodes, apiErrors } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { checkRediStack } from 'uiSrc/utils'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { setAppContextInitialState } from 'uiSrc/slices/app/context'\nimport { resetKeys } from 'uiSrc/slices/browser/keys'\nimport reducer, {\n  initialState,\n  instancesSelector,\n  loadInstances,\n  loadInstancesSuccess,\n  loadInstancesFailure,\n  fetchInstancesAction,\n  createInstanceStandaloneAction,\n  defaultInstanceChanging,\n  defaultInstanceChangingSuccess,\n  defaultInstanceChangingFailure,\n  testConnection,\n  testConnectionSuccess,\n  testConnectionFailure,\n  updateInstanceAction,\n  deleteInstancesAction,\n  setDefaultInstance,\n  setDefaultInstanceSuccess,\n  setDefaultInstanceFailure,\n  checkConnectToInstanceAction,\n  getDatabaseConfigInfo,\n  getDatabaseConfigInfoSuccess,\n  getDatabaseConfigInfoFailure,\n  getDatabaseConfigInfoAction,\n  changeInstanceAlias,\n  changeInstanceAliasFailure,\n  changeInstanceAliasSuccess,\n  changeInstanceAliasAction,\n  resetConnectedInstance,\n  setEditedInstance,\n  fetchEditedInstanceAction,\n  setConnectedInstanceId,\n  setConnectedInstance,\n  setConnectedInstanceFailure,\n  setConnectedInstanceSuccess,\n  importInstancesFromFile,\n  importInstancesFromFileSuccess,\n  importInstancesFromFileFailure,\n  resetImportInstances,\n  importInstancesSelector,\n  uploadInstancesFile,\n  checkDatabaseIndexFailure,\n  checkDatabaseIndexSuccess,\n  checkDatabaseIndex,\n  checkDatabaseIndexAction,\n  setConnectedInfoInstance,\n  setConnectedInfoInstanceSuccess,\n  fetchConnectedInstanceInfoAction,\n  testInstanceStandaloneAction,\n  updateEditedInstance,\n  exportInstancesAction,\n  autoCreateAndConnectToInstanceAction,\n  cloneInstanceAction,\n} from '../../instances/instances'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  addMessageNotification,\n  IAddInstanceErrorPayload,\n} from '../../app/notifications'\nimport {\n  ConnectionType,\n  InitialStateInstances,\n  Instance,\n} from '../../interfaces'\nimport { loadMastersSentinel } from '../../instances/sentinel'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet instances: Instance[]\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  instances = [\n    {\n      id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff',\n      version: '6.2.6',\n      host: 'localhost',\n      port: 6379,\n      name: 'localhost',\n      username: null,\n      password: null,\n      connectionType: ConnectionType.Standalone,\n      nameFromProvider: null,\n      modules: [],\n      lastConnection: new Date('2021-04-22T09:03:56.917Z'),\n    },\n    {\n      id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4',\n      host: 'localhost',\n      port: 12000,\n      name: 'oea123123',\n      username: null,\n      password: null,\n      connectionType: ConnectionType.Standalone,\n      nameFromProvider: null,\n      modules: [],\n      tls: {\n        verifyServerCert: true,\n        caCertId: '70b95d32-c19d-4311-bb24-e684af12cf15',\n        clientCertPairId: '70b95d32-c19d-4311-b23b24-e684af12cf15',\n      },\n    },\n    {\n      id: 'b83a3932-e95f-4f09-9d8a-55079f400186',\n      version: '6.2.6',\n      host: 'localhost',\n      port: 5005,\n      name: 'sentinel',\n      username: null,\n      password: null,\n      connectionType: ConnectionType.Sentinel,\n      nameFromProvider: null,\n      lastConnection: new Date('2021-04-22T18:40:44.031Z'),\n      modules: [],\n      endpoints: [\n        {\n          host: 'localhost',\n          port: 5005,\n        },\n        {\n          host: '127.0.0.1',\n          port: 5006,\n        },\n      ],\n      sentinelMaster: {\n        name: 'mymaster',\n      },\n    },\n  ]\n})\n\ndescribe('instances slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadInstances', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadInstances())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('defaultInstanceChanging', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loadingChanging: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, defaultInstanceChanging())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('testConnection', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loadingChanging: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, testConnection())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('testConnectionSuccess', () => {\n    it('should properly set loading = false', () => {\n      // Arrange\n      const prevState: InitialStateInstances = {\n        ...initialState,\n        loadingChanging: true,\n      }\n      const state = {\n        ...initialState,\n        loadingChanging: false,\n      }\n\n      // Act\n      const nextState = reducer(prevState, testConnectionSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('testConnectionFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loadingChanging: false,\n        errorChanging: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, testConnectionFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('changeInstanceAlias', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loadingChanging: true,\n        errorChanging: '',\n      }\n\n      // Act\n      const nextState = reducer(initialState, changeInstanceAlias())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('changeInstanceAliasSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const prevState: InitialStateInstances = {\n        ...initialState,\n        data: [instances[0], instances[1]],\n      }\n      const state = {\n        ...initialState,\n        loadingChanging: false,\n        data: [{ ...instances[0], name: 'newAlias' }, instances[1]],\n      }\n\n      // Act\n      const nextState = reducer(\n        prevState,\n        changeInstanceAliasSuccess({ id: instances[0].id, name: 'newAlias' }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('changeInstanceAliasFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loadingChanging: false,\n        errorChanging: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, changeInstanceAliasFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: checkRediStack(instances),\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadInstancesSuccess(instances))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadInstancesSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadInstancesFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadInstancesFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getDatabaseConfigInfo', () => {\n    it('should properly set state before fetch data', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getDatabaseConfigInfo())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getDatabaseConfigInfoSuccess', () => {\n    it('should properly set state after fetch data', () => {\n      // Arrange\n      const data = {\n        version: '6.2',\n        totalKeys: 10,\n        usedMemory: 5,\n        connectedClients: 1,\n        opsPerSecond: 2,\n        networkInKbps: 0,\n        networkOutKbps: 0,\n        cpuUsagePercentage: null,\n      }\n      const state = {\n        ...initialState,\n        instanceOverview: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getDatabaseConfigInfoSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getDatabaseConfigInfoFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'some error'\n      const state = {\n        ...initialState,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getDatabaseConfigInfoFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInstanceId', () => {\n    it('should properly set \"id\"', () => {\n      // Arrange\n      const id = 'id'\n      const state: InitialStateInstances = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          id,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setConnectedInstanceId(id))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInstance', () => {\n    it('should properly set loading = \"true\"', () => {\n      // Arrange\n      const state: InitialStateInstances = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setConnectedInstance())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInstanceSuccess', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const instance = { ...instances[1] }\n      const state: InitialStateInstances = {\n        ...initialState,\n        connectedInstance: {\n          ...instance,\n          loading: false,\n          isRediStack: false,\n          isFreeDb: false,\n          db: undefined,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setConnectedInstanceSuccess(instance),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInstanceFailure', () => {\n    it('should properly set loading = \"false\"', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setConnectedInstanceFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInfoInstance', () => {\n    it('should properly set initial state', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        instanceInfo: {\n          version: '6.12.0',\n          databases: 12,\n          server: {},\n        },\n      }\n      const state: InitialStateInstances = {\n        ...initialState,\n        instanceInfo: initialState.instanceInfo,\n      }\n\n      // Act\n      const nextState = reducer(currentState, setConnectedInfoInstance())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInfoInstanceSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const payload = {\n        version: '6.12.0',\n        databases: 12,\n        server: {},\n      }\n      const state: InitialStateInstances = {\n        ...initialState,\n        instanceInfo: payload,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setConnectedInfoInstanceSuccess(payload),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setEditedInstance', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data = instances[1]\n      const state = {\n        ...initialState,\n        editedInstance: {\n          ...initialState.editedInstance,\n          data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setEditedInstance(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateEditedInstance', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data = instances[1]\n      const state = {\n        ...initialState,\n        editedInstance: {\n          ...initialState.editedInstance,\n          data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateEditedInstance(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('importInstancesFromFile', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState.importInstances,\n        loading: true,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(initialState, importInstancesFromFile())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(importInstancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('importInstancesFromFileSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const mockedError = {\n        statusCode: 400,\n        message: 'message',\n        error: 'error',\n      }\n      const data = {\n        total: 3,\n        fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n        partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n        success: [\n          { index: 1, status: 'success', port: 1233, host: 'localhost' },\n        ],\n      }\n      const state = {\n        ...initialState.importInstances,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        importInstancesFromFileSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(importInstancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('importInstancesFromFileFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const error = 'Some error'\n      const state = {\n        ...initialState.importInstances,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        importInstancesFromFileFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(importInstancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetImportInstances', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const mockedError = {\n        statusCode: 400,\n        message: 'message',\n        error: 'error',\n      }\n      const currentState = {\n        ...initialState,\n        importInstances: {\n          ...initialState.importInstances,\n          data: {\n            total: 3,\n            fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n            partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n            success: [\n              { index: 1, status: 'success', port: 1233, host: 'localhost' },\n            ],\n          },\n        },\n      }\n\n      const state = {\n        ...initialState.importInstances,\n      }\n\n      // Act\n      const nextState = reducer(currentState, resetImportInstances())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(importInstancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('checkDatabaseIndex', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, checkDatabaseIndex())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('checkDatabaseIndexSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          loading: false,\n          db: 5,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, checkDatabaseIndexSuccess(5))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('checkDatabaseIndexFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, checkDatabaseIndexFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchInstances', () => {\n      it('call both fetchInstances and loadInstancesSuccess when fetch is successed', async () => {\n        // Arrange\n        const responsePayload = { data: instances, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchInstancesAction())\n\n        // Assert\n        const expectedActions = [\n          loadInstances(),\n          loadInstancesSuccess(responsePayload.data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both fetchInstances and loadInstancesFailure when fetch is fail', async () => {\n        // Arrange\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchInstancesAction())\n\n        // Assert\n        const expectedActions = [\n          loadInstances(),\n          loadInstancesFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('createInstanceStandaloneAction', () => {\n      it('call both createInstanceStandaloneAction and defaultInstanceChangingSuccess when fetch is successed', async () => {\n        // Arrange\n        const requestData = {\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const responsePayload = { status: 201 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createInstanceStandaloneAction(requestData, jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingSuccess(),\n          loadInstances(),\n          addMessageNotification(\n            successMessages.ADDED_NEW_INSTANCE(requestData.name),\n          ),\n        ]\n\n        expect(store.getActions().splice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('call both createInstanceStandaloneAction and defaultInstanceChangingFailure when fetch is fail', async () => {\n        // Arrange\n        const requestData = {\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createInstanceStandaloneAction(requestData, () => ({})),\n        )\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('redirect to sentinel flow when user try connect to Sentinel like Stanalone createInstanceStandaloneAction', async () => {\n        // Arrange\n        const requestData = {\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 400,\n            data: {\n              message: errorMessage,\n              error: apiErrors.SentinelParamsRequired,\n            },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createInstanceStandaloneAction(requestData, () => ({})),\n        )\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingFailure(responsePayload.response.data.message),\n          loadMastersSentinel(),\n        ]\n\n        expect(store.getActions().splice(0, 3)).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on fail with errorCode=11_200 (Database already exists)', async () => {\n        // Arrange\n        const mockId = '123'\n        const mockName = 'name'\n        const requestData = {\n          name: mockName,\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: {\n              message: errorMessage,\n              errorCode: CustomErrorCodes.DatabaseAlreadyExists,\n              resource: {\n                databaseId: mockId,\n              },\n            },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(createInstanceStandaloneAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          resetKeys(),\n          setAppContextInitialState(),\n          setConnectedInstanceId(mockId),\n          setDefaultInstance(),\n          resetConnectedInstance(),\n        ]\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n    })\n\n    describe('deleteInstances', () => {\n      it('call both deleteInstances and setDefaultInstanceSuccess when fetch is successed for only one instance', async () => {\n        // Arrange\n        const requestData = [instances[0]]\n\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteInstancesAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceSuccess(),\n          loadInstances(),\n          addMessageNotification(\n            successMessages.DELETE_INSTANCE(requestData[0].name ?? ''),\n          ),\n        ]\n\n        const actions = store.getActions()\n        // skip thunk action\n        const testActions = [\n          ...actions.slice(0, expectedActions.length - 1),\n          actions[expectedActions.length],\n        ]\n\n        expect(testActions).toEqual(expectedActions)\n      })\n\n      it('call both deleteInstances and setDefaultInstanceSuccess when fetch is successed for several instances', async () => {\n        // Arrange\n        const requestData = instances.slice(0, 3)\n\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteInstancesAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceSuccess(),\n          loadInstances(),\n          addMessageNotification(\n            successMessages.DELETE_INSTANCES(map(requestData, 'name')),\n          ),\n        ]\n\n        const actions = store.getActions()\n        // skip thunk action\n        const testActions = [\n          ...actions.slice(0, expectedActions.length - 1),\n          actions[expectedActions.length],\n        ]\n\n        expect(testActions).toEqual(expectedActions)\n      })\n\n      it('call both deleteInstances and setDefaultInstanceFailure when fetch is fail', async () => {\n        // Arrange\n        const requestData = instances.slice(0, 3)\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteInstancesAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('exportInstancesAction', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          exportInstancesAction(map(instances, 'id'), true),\n        )\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceSuccess(),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on fail', async () => {\n        // Arrange\n        const errorMessage = 'Some Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          exportInstancesAction(map(instances, 'id'), false),\n        )\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceFailure(errorMessage),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('updateInstance', () => {\n      it('call both updateInstance and defaultInstanceChangingSuccess when fetch is successed', async () => {\n        // Arrange\n        const requestData = {\n          id: '123',\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const responsePayload = { status: 201 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(updateInstanceAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingSuccess(),\n        ]\n\n        expect(store.getActions().splice(0, 2)).toEqual(expectedActions)\n      })\n\n      it('call both updateInstance and defaultInstanceChangingFailure when fetch is fail', async () => {\n        // Arrange\n        const requestData = {\n          id: '123',\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(updateInstanceAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('checkConnectToInstance', () => {\n      it('call both checkConnectToInstance and setDefaultInstanceSuccess when connection is successed', async () => {\n        // Arrange\n        const requestId = '123'\n\n        const onSuccessAction = jest.fn()\n        const onFailAction = jest.fn()\n\n        const responsePayload = { status: 201 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          checkConnectToInstanceAction(\n            requestId,\n            onSuccessAction,\n            onFailAction,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          resetConnectedInstance(),\n          setDefaultInstanceSuccess(),\n        ]\n\n        expect(store.getActions().splice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('call both checkConnectToInstance and setDefaultInstanceFailure when fetch is fail', async () => {\n        // Arrange\n        const requestId = '123'\n\n        const onSuccessAction = jest.fn()\n        const onFailAction = jest.fn()\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          instanceId: requestId,\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          checkConnectToInstanceAction(\n            requestId,\n            onSuccessAction,\n            onFailAction,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          resetConnectedInstance(),\n          setDefaultInstanceFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as IAddInstanceErrorPayload),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('getDatabaseConfigInfoAction', () => {\n      it('succeed to get database config info', async () => {\n        // Arrange\n        const requestId = '123'\n        const data = {\n          databases: 1,\n          server: {},\n          modules: [],\n          version: '6.52',\n        }\n        const responsePayload = { status: 200, data }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          getDatabaseConfigInfoAction(requestId, jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          getDatabaseConfigInfo(),\n          getDatabaseConfigInfoSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to get database config info', async () => {\n        // Arrange\n        const requestId = '123'\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          getDatabaseConfigInfoAction(requestId, jest.fn(), jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          getDatabaseConfigInfo(),\n          getDatabaseConfigInfoFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchConnectedInstanceInfoAction', () => {\n      it('succeed to get database instance info', async () => {\n        // Arrange\n        const requestId = '123'\n        const data = {\n          databases: 12,\n          server: {},\n          modules: [],\n          version: '6.52',\n        }\n        const responsePayload = { status: 200, data }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchConnectedInstanceInfoAction(requestId, jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [\n          setConnectedInfoInstance(),\n          setConnectedInfoInstanceSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to get database config info', async () => {\n        // Arrange\n        const requestId = '123'\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchConnectedInstanceInfoAction(requestId, jest.fn()),\n        )\n\n        // Assert\n        const expectedActions = [setConnectedInfoInstance()]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('changeInstanceAliasAction', () => {\n      const requestPayload = {\n        id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff',\n        name: 'newAlias',\n      }\n      it('succeed to change database alias', async () => {\n        // Arrange\n        const data = {\n          oldName: 'databaseAlias',\n          newName: 'newAlias',\n        }\n        const responsePayload = { status: 200, data }\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          changeInstanceAliasAction(requestPayload.id, requestPayload.name),\n        )\n\n        // Assert\n        const expectedActions = [\n          changeInstanceAlias(),\n          changeInstanceAliasSuccess(requestPayload),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('failed to change database alias', async () => {\n        // Arrange\n        const errorMessage = 'Not Found!'\n        const responsePayload = {\n          response: {\n            status: 404,\n            data: { message: errorMessage },\n          },\n        }\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          changeInstanceAliasAction(requestPayload.id, requestPayload.name),\n        )\n\n        // Assert\n        const expectedActions = [\n          changeInstanceAlias(),\n          changeInstanceAliasFailure(errorMessage),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchEditedInstanceAction', () => {\n      it('call both setEditedInstance and setDefaultInstanceSuccess when fetch is successed', async () => {\n        // Arrange\n        const editedInstance = {\n          id: 'instanceId',\n          host: '1',\n          port: 1,\n          modules: [],\n        }\n        const data = instances[1]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchEditedInstanceAction(editedInstance))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setEditedInstance(editedInstance),\n          updateEditedInstance(responsePayload.data),\n          setDefaultInstanceSuccess(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both setDefaultInstance and setDefaultInstanceFailure when fetch is fail', async () => {\n        // Arrange\n        const editedInstance = {\n          id: 'instanceId',\n          host: '1',\n          port: 1,\n          modules: [],\n        }\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchEditedInstanceAction(editedInstance))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setEditedInstance(editedInstance),\n          setEditedInstance(null),\n          setConnectedInstanceFailure(),\n          setDefaultInstanceFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('checkDatabaseIndexAction', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const id = 'instanceId'\n        const index = 3\n        const responsePayload = { status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(checkDatabaseIndexAction(id, index))\n\n        // Assert\n        const expectedActions = [\n          checkDatabaseIndex(),\n          checkDatabaseIndexSuccess(index),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on fail', async () => {\n        // Arrange\n        const id = 'instanceId'\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(checkDatabaseIndexAction(id, 3))\n\n        // Assert\n        const expectedActions = [\n          checkDatabaseIndex(),\n          checkDatabaseIndexFailure(),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('uploadInstancesFile', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const formData = new FormData()\n        const mockedError = {\n          statusCode: 400,\n          message: 'message',\n          error: 'error',\n        }\n        const data = {\n          total: 3,\n          fail: [{ index: 0, status: 'fail', errors: [mockedError] }],\n          partial: [{ index: 2, status: 'fail', errors: [mockedError] }],\n          success: [\n            { index: 1, status: 'success', port: 1233, host: 'localhost' },\n          ],\n        }\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(uploadInstancesFile(formData))\n\n        // Assert\n        const expectedActions = [\n          importInstancesFromFile(),\n          importInstancesFromFileSuccess(responsePayload.data),\n        ]\n        const actions = store.getActions()\n        // skip thunk action\n        const testActions = [\n          ...actions.slice(0, expectedActions.length - 1),\n          actions[expectedActions.length],\n        ]\n\n        expect(testActions).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on fail', async () => {\n        // Arrange\n        const formData = new FormData()\n        const errorMessage = 'Some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(uploadInstancesFile(formData))\n\n        // Assert\n        const expectedActions = [\n          importInstancesFromFile(),\n          importInstancesFromFileFailure(responsePayload.response.data.message),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('testInstanceStandaloneAction', () => {\n      it('call axios with proper url with id', async () => {\n        // Arrange\n        const requestData = {\n          id: '123',\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        apiService.post = jest.fn()\n\n        // Act\n        await store.dispatch<any>(testInstanceStandaloneAction(requestData))\n\n        expect(apiService.post).toBeCalledWith(\n          'databases/test/123',\n          omit(requestData, 'id'),\n        )\n      })\n\n      it('call axios with proper url with id', async () => {\n        // Arrange\n        const requestData = {\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        apiService.post = jest.fn()\n\n        // Act\n        await store.dispatch<any>(testInstanceStandaloneAction(requestData))\n\n        expect(apiService.post).toBeCalledWith('databases/test', requestData)\n      })\n\n      it('call proper actions on success', async () => {\n        // Arrange\n        const requestData = {\n          id: '123',\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const responsePayload = { status: 201 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(testInstanceStandaloneAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          testConnection(),\n          testConnectionSuccess(),\n          addMessageNotification(successMessages.TEST_CONNECTION()),\n        ]\n\n        expect(store.getActions().splice(0, 3)).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on fail', async () => {\n        // Arrange\n        const requestData = {\n          id: '123',\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(testInstanceStandaloneAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          testConnection(),\n          testConnectionFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('autoCreateAndConnectToInstanceAction', () => {\n      it('call proper actions on success', async () => {\n        // Arrange\n        const mockId = '123'\n        const mockName = 'name'\n        const requestData = {\n          name: mockName,\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const responsePayload = {\n          status: 201,\n          data: { id: mockId, name: mockName },\n        }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          autoCreateAndConnectToInstanceAction(requestData),\n        )\n\n        // Assert\n        const expectedActions = [\n          addInfiniteNotification(INFINITE_MESSAGES.AUTO_CREATING_DATABASE()),\n          resetKeys(),\n          setAppContextInitialState(),\n          setConnectedInstanceId(mockId),\n          setDefaultInstance(),\n          resetConnectedInstance(),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n\n      it('should call proper actions on fail with errorCode=11_200 (Database already exists)', async () => {\n        // Arrange\n        const mockId = '123'\n        const mockName = 'name'\n        const requestData = {\n          name: mockName,\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage = 'some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: {\n              message: errorMessage,\n              errorCode: CustomErrorCodes.DatabaseAlreadyExists,\n              resource: {\n                databaseId: mockId,\n              },\n            },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          autoCreateAndConnectToInstanceAction(requestData),\n        )\n\n        // Assert\n        const expectedActions = [\n          addInfiniteNotification(INFINITE_MESSAGES.AUTO_CREATING_DATABASE()),\n          resetKeys(),\n          setAppContextInitialState(),\n          setConnectedInstanceId(mockId),\n          setDefaultInstance(),\n          resetConnectedInstance(),\n        ]\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n    })\n\n    describe('cloneInstanceAction', () => {\n      it('call proper actions on success', async () => {\n        // Arrange\n        const mockId = '123'\n        const mockName = 'name'\n        const requestData = {\n          id: mockId,\n          name: mockName,\n          timeout: 45_000,\n        }\n\n        const responsePayload = { status: 201, data: { name: mockName } }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(cloneInstanceAction(requestData))\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingSuccess(),\n          loadInstances(),\n          addMessageNotification(\n            successMessages.ADDED_NEW_INSTANCE(requestData.name),\n          ),\n        ]\n\n        expect(store.getActions().slice(0, expectedActions.length)).toEqual(\n          expectedActions,\n        )\n      })\n      it('call both createInstanceStandaloneAction and defaultInstanceChangingFailure when fetch is fail', async () => {\n        // Arrange\n        const requestData = {\n          name: 'db',\n          host: 'localhost',\n          port: 6379,\n        }\n\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(cloneInstanceAction(requestData, () => ({})))\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingFailure(responsePayload.response.data.message),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/instances/sentinel.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { apiService } from 'uiSrc/services'\nimport { parseAddedMastersSentinel, parseMastersSentinel } from 'uiSrc/utils'\n\nimport { SentinelMaster } from 'apiSrc/modules/redis-sentinel/models/sentinel-master'\nimport { CreateSentinelDatabaseResponse } from 'apiSrc/modules/redis-sentinel/dto/create.sentinel.database.response'\n\nimport reducer, {\n  initialState,\n  sentinelSelector,\n  loadMastersSentinel,\n  loadMastersSentinelSuccess,\n  loadMastersSentinelFailure,\n  fetchMastersSentinelAction,\n  setInstanceSentinel,\n  createMastersSentinelAction,\n  createMastersSentinelSuccess,\n  createMastersSentinel,\n  createMastersSentinelFailure,\n  updateMastersSentinel,\n} from '../../instances/sentinel'\nimport { addErrorNotification } from '../../app/notifications'\nimport { LoadedSentinel, ModifiedSentinelMaster } from '../../interfaces'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nlet masters: SentinelMaster[]\nlet parsedMasters: ModifiedSentinelMaster[]\nlet parsedAddedMasters: ModifiedSentinelMaster[]\nlet addedMastersStatuses: CreateSentinelDatabaseResponse[]\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n\n  masters = [\n    {\n      host: '127.0.0.1',\n      port: 6379,\n      name: 'mymaster-2',\n      numberOfSlaves: 1,\n      nodes: [{ host: 'localhost', port: 5005 }],\n    },\n    {\n      host: '127.0.0.1',\n      port: 6379,\n      name: 'mymaster',\n      numberOfSlaves: 0,\n      nodes: [\n        { host: 'localhost', port: 5005 },\n        { host: '127.0.0.1', port: 5006 },\n      ],\n    },\n  ]\n\n  addedMastersStatuses = [\n    {\n      id: 'ce935f36-057a-40a6-a796-4045e4b123bd',\n      name: 'mymaster',\n      status: 'success',\n      message: 'Added',\n    },\n    {\n      name: 'mymaster-2',\n      status: 'fail',\n      message: 'Failed to authenticate, please check the username or password.',\n      error: {\n        statusCode: 401,\n        message:\n          'Failed to authenticate, please check the username or password.',\n        error: 'Unauthorized',\n      },\n    },\n  ]\n\n  parsedMasters = parseMastersSentinel(masters)\n  parsedAddedMasters = parseAddedMastersSentinel(\n    parsedMasters,\n    addedMastersStatuses,\n  )\n})\n\ndescribe('sentinel slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('loadMastersSentinel', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMastersSentinel())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateMastersSentinel', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n\n      const data: ModifiedSentinelMaster[] = [\n        { name: 'mymaster', host: 'localhost', port: 0, numberOfSlaves: 10 },\n      ]\n\n      const state = {\n        ...initialState,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateMastersSentinel(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMastersSentinelSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: parsedMasters,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedSentinel.Masters]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        loadMastersSentinelSuccess(masters),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedSentinel.Masters]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMastersSentinelSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadMastersSentinelFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(initialState, loadMastersSentinelFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createMastersSentinel', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, createMastersSentinel())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createMastersSentinelSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data: parsedAddedMasters,\n        statuses: addedMastersStatuses,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedSentinel.MastersAdded]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        { ...initialState, data: parsedMasters },\n        createMastersSentinelSuccess(addedMastersStatuses),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with empty data', () => {\n      // Arrange\n      const data: any = []\n\n      const state = {\n        ...initialState,\n        loading: false,\n        data,\n        loaded: {\n          ...initialState.loaded,\n          [LoadedSentinel.MastersAdded]: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createMastersSentinelSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('createMastersSentinelFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n        data: [],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        createMastersSentinelFailure(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        connections: {\n          sentinel: nextState,\n        },\n      })\n      expect(sentinelSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    it('call both fetchMastersSentinelAction and loadMastersSentinelSuccess when fetch is successed', async () => {\n      // Arrange\n      const requestData = {\n        host: 'localhost',\n        port: 5005,\n      }\n\n      const responsePayload = { data: masters, status: 200 }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchMastersSentinelAction(requestData))\n\n      // Assert\n      const expectedActions = [\n        loadMastersSentinel(),\n        setInstanceSentinel(requestData),\n        loadMastersSentinelSuccess(responsePayload.data),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n\n    it('call both fetchMastersSentinelAction and loadMastersSentinelFailure when fetch is fail', async () => {\n      // Arrange\n      const requestData = {\n        host: 'localhost',\n        port: 5005,\n      }\n\n      const errorMessage =\n        'Could not connect to aoeu:123, please check the connection details.'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n\n      apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchMastersSentinelAction(requestData))\n\n      // Assert\n      const expectedActions = [\n        loadMastersSentinel(),\n        loadMastersSentinelFailure(responsePayload.response.data.message),\n        addErrorNotification(responsePayload as AxiosError),\n      ]\n      expect(store.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  it('call both createMastersSentinelAction and createMastersSentinelSuccess when fetch is successed', async () => {\n    // Arrange\n    const requestData = [\n      {\n        alias: 'db test1',\n        name: 'mymaster',\n      },\n      {\n        alias: 'db test2',\n        name: 'mymaster-2',\n        username: 'egor',\n        password: '123',\n      },\n    ]\n\n    const responsePayload = { data: addedMastersStatuses, status: 200 }\n\n    apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n    // Act\n    await store.dispatch<any>(createMastersSentinelAction(requestData))\n\n    // Assert\n    const expectedActions = [\n      createMastersSentinel(),\n      createMastersSentinelSuccess(responsePayload.data),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n\n  it('call both createMastersSentinelAction and createMastersSentinelFailure when fetch is fail', async () => {\n    // Arrange\n    const requestData = [\n      {\n        alias: 'db test1',\n        name: 'mymaster',\n      },\n      {\n        alias: 'db test2',\n        name: 'mymaster-2',\n        username: 'egor',\n        password: '123',\n      },\n    ]\n\n    const errorMessage =\n      'Could not connect to aoeu:123, please check the connection details.'\n    const responsePayload = {\n      response: {\n        status: 500,\n        data: { message: errorMessage },\n      },\n    }\n\n    apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n    // Act\n    await store.dispatch<any>(createMastersSentinelAction(requestData))\n\n    // Assert\n    const expectedActions = [\n      createMastersSentinel(),\n      createMastersSentinelFailure(responsePayload.response.data.message),\n      addErrorNotification(responsePayload as AxiosError),\n    ]\n    expect(store.getActions()).toEqual(expectedActions)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/oauth/azure.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { faker } from '@faker-js/faker'\nimport { AxiosError } from 'axios'\n\nimport reducer, {\n  initialState,\n  azureAuthLogin,\n  azureAuthLoginSuccess,\n  azureAuthLoginFailure,\n  azureOAuthCallbackSuccess,\n  azureOAuthCallbackFailure,\n  azureAuthLogout,\n  setAzureAuthInitialState,\n  setAzureLoginSource,\n  azureAuthSelector,\n  azureAuthAccountSelector,\n  azureAuthLoadingSelector,\n  initiateAzureLoginAction,\n  handleAzureOAuthSuccess,\n} from 'uiSrc/slices/oauth/azure'\nimport { AzureLoginSource } from 'uiSrc/slices/interfaces'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { resetDataAzure } from 'uiSrc/slices/instances/azure'\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { AzureAccountFactory } from 'uiSrc/mocks/factories/cloud/AzureAccount.factory'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockAccount = AzureAccountFactory.build()\n\ndescribe('azure auth slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      const nextState = initialState\n      const result = reducer(undefined, { type: '' })\n      expect(result).toEqual(nextState)\n    })\n\n    it('should reset to initial state with setAzureAuthInitialState', () => {\n      const modifiedState = {\n        ...initialState,\n        loading: true,\n        error: 'some error',\n      }\n      const result = reducer(modifiedState, setAzureAuthInitialState())\n      expect(result).toEqual(initialState)\n    })\n  })\n\n  describe('setAzureLoginSource', () => {\n    it('should set source to autodiscovery', () => {\n      const nextState = reducer(\n        initialState,\n        setAzureLoginSource(AzureLoginSource.Autodiscovery),\n      )\n      expect(nextState.source).toEqual(AzureLoginSource.Autodiscovery)\n    })\n\n    it('should set source to token-refresh', () => {\n      const nextState = reducer(\n        initialState,\n        setAzureLoginSource(AzureLoginSource.TokenRefresh),\n      )\n      expect(nextState.source).toEqual(AzureLoginSource.TokenRefresh)\n    })\n\n    it('should reset source to null', () => {\n      const prevState = {\n        ...initialState,\n        source: AzureLoginSource.Autodiscovery,\n      }\n      const nextState = reducer(prevState, setAzureLoginSource(null))\n      expect(nextState.source).toBeNull()\n    })\n  })\n\n  describe('azureAuthLogin', () => {\n    it('should set loading = true and clear error', () => {\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      const nextState = reducer(initialState, azureAuthLogin())\n\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { azure: nextState },\n      })\n      expect(azureAuthSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('azureAuthLoginSuccess', () => {\n    it('should keep loading true and clear error', () => {\n      const prevState = { ...initialState, loading: true }\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n\n      const nextState = reducer(prevState, azureAuthLoginSuccess())\n\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { azure: nextState },\n      })\n      expect(azureAuthSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('azureAuthLoginFailure', () => {\n    it('should set loading = false and set error', () => {\n      const errorMessage = faker.lorem.sentence()\n      const prevState = { ...initialState, loading: true }\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      const nextState = reducer(prevState, azureAuthLoginFailure(errorMessage))\n\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { azure: nextState },\n      })\n      expect(azureAuthSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('azureOAuthCallbackSuccess', () => {\n    it('should set loading = false, set account, and clear error', () => {\n      const prevState = { ...initialState, loading: true }\n      const state = {\n        ...initialState,\n        loading: false,\n        account: mockAccount,\n        error: '',\n      }\n\n      const nextState = reducer(\n        prevState,\n        azureOAuthCallbackSuccess(mockAccount),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { azure: nextState },\n      })\n      expect(azureAuthSelector(rootState)).toEqual(state)\n    })\n\n    it('should not reset source (ConfigAzureAuth needs it for redirect decision)', () => {\n      const prevState = {\n        ...initialState,\n        loading: true,\n        source: AzureLoginSource.Autodiscovery,\n      }\n\n      const nextState = reducer(\n        prevState,\n        azureOAuthCallbackSuccess(mockAccount),\n      )\n\n      expect(nextState.source).toEqual(AzureLoginSource.Autodiscovery)\n    })\n  })\n\n  describe('azureOAuthCallbackFailure', () => {\n    it('should set loading = false and set error', () => {\n      const errorMessage = faker.lorem.sentence()\n      const prevState = { ...initialState, loading: true }\n      const state = {\n        ...initialState,\n        loading: false,\n        error: errorMessage,\n      }\n\n      const nextState = reducer(\n        prevState,\n        azureOAuthCallbackFailure(errorMessage),\n      )\n\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { azure: nextState },\n      })\n      expect(azureAuthSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('azureAuthLogout', () => {\n    it('should clear account and error', () => {\n      const prevState = {\n        ...initialState,\n        account: mockAccount,\n        error: 'error',\n      }\n      const state = {\n        ...initialState,\n        account: null,\n        error: '',\n      }\n\n      const nextState = reducer(prevState, azureAuthLogout())\n\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { azure: nextState },\n      })\n      expect(azureAuthSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('selectors', () => {\n    it('azureAuthAccountSelector should return account', () => {\n      const rootState = {\n        ...initialStateDefault,\n        oauth: {\n          ...initialStateDefault.oauth,\n          azure: { ...initialState, account: mockAccount },\n        },\n      }\n      expect(azureAuthAccountSelector(rootState)).toEqual(mockAccount)\n    })\n\n    it('azureAuthLoadingSelector should return loading', () => {\n      const rootState = {\n        ...initialStateDefault,\n        oauth: {\n          ...initialStateDefault.oauth,\n          azure: { ...initialState, loading: true },\n        },\n      }\n      expect(azureAuthLoadingSelector(rootState)).toEqual(true)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('initiateAzureLoginAction', () => {\n      it('should dispatch login actions and call onSuccess on success', async () => {\n        const authUrl = faker.internet.url()\n        const responsePayload = { data: { url: authUrl }, status: 200 }\n        const onSuccess = jest.fn()\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(\n          initiateAzureLoginAction({\n            source: AzureLoginSource.Autodiscovery,\n            onSuccess,\n          }),\n        )\n\n        const expectedActions = [\n          setAzureLoginSource(AzureLoginSource.Autodiscovery),\n          azureAuthLogin(),\n          azureAuthLoginSuccess(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n        expect(onSuccess).toHaveBeenCalledWith(authUrl)\n      })\n\n      it('should dispatch failure and error notification on API error', async () => {\n        const errorMessage = 'Failed to get auth URL'\n        const error = {\n          response: { data: { message: errorMessage } },\n        } as AxiosError\n\n        apiService.get = jest.fn().mockRejectedValue(error)\n\n        await store.dispatch<any>(\n          initiateAzureLoginAction({\n            source: AzureLoginSource.Autodiscovery,\n            onSuccess: jest.fn(),\n          }),\n        )\n\n        const actions = store.getActions()\n        expect(actions[0]).toEqual(\n          setAzureLoginSource(AzureLoginSource.Autodiscovery),\n        )\n        expect(actions[1]).toEqual(azureAuthLogin())\n        expect(actions[2]).toEqual(azureAuthLoginFailure(errorMessage))\n        expect(actions[3].type).toEqual(addErrorNotification({} as any).type)\n      })\n\n      it('should set source to token-refresh when initiated from error notification', async () => {\n        const authUrl = faker.internet.url()\n        const responsePayload = { data: { url: authUrl }, status: 200 }\n        const onSuccess = jest.fn()\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(\n          initiateAzureLoginAction({\n            source: AzureLoginSource.TokenRefresh,\n            onSuccess,\n          }),\n        )\n\n        const expectedActions = [\n          setAzureLoginSource(AzureLoginSource.TokenRefresh),\n          azureAuthLogin(),\n          azureAuthLoginSuccess(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should pass prompt parameter as query param to API', async () => {\n        const authUrl = faker.internet.url()\n        const responsePayload = { data: { url: authUrl }, status: 200 }\n        const onSuccess = jest.fn()\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(\n          initiateAzureLoginAction({\n            source: AzureLoginSource.Autodiscovery,\n            onSuccess,\n            prompt: 'select_account',\n          }),\n        )\n\n        expect(apiService.get).toHaveBeenCalledWith(expect.any(String), {\n          params: { prompt: 'select_account' },\n        })\n      })\n\n      it('should not pass params when prompt is not provided', async () => {\n        const authUrl = faker.internet.url()\n        const responsePayload = { data: { url: authUrl }, status: 200 }\n        const onSuccess = jest.fn()\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        await store.dispatch<any>(\n          initiateAzureLoginAction({\n            source: AzureLoginSource.Autodiscovery,\n            onSuccess,\n          }),\n        )\n\n        expect(apiService.get).toHaveBeenCalledWith(expect.any(String), {\n          params: undefined,\n        })\n      })\n    })\n\n    describe('handleAzureOAuthSuccess', () => {\n      it('should dispatch resetDataAzure to clear stale data when switching accounts', () => {\n        store.dispatch<any>(handleAzureOAuthSuccess(mockAccount))\n\n        const actions = store.getActions()\n        expect(actions).toContainEqual(resetDataAzure())\n        expect(actions).toContainEqual(azureOAuthCallbackSuccess(mockAccount))\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/oauth/cloud.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\n\nimport {\n  cleanup,\n  clearStoreActions,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { apiService } from 'uiSrc/services'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  addMessageNotification,\n  removeInfiniteNotification,\n} from 'uiSrc/slices/app/notifications'\nimport {\n  INFINITE_MESSAGES,\n  InfiniteMessagesIds,\n} from 'uiSrc/components/notifications/components'\nimport { CloudJobStatus, CloudJobName } from 'uiSrc/electron/constants'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { setSSOFlow } from 'uiSrc/slices/instances/cloud'\nimport reducer, {\n  initialState,\n  oauthCloudSelector,\n  signIn,\n  signInSuccess,\n  signInFailure,\n  getUserInfo,\n  getUserInfoSuccess,\n  getUserInfoFailure,\n  fetchUserInfo,\n  addFreeDbFailure,\n  addFreeDbSuccess,\n  addFreeDb,\n  createFreeDbJob,\n  setSelectAccountDialogState,\n  createFreeDbSuccess,\n  activateAccount,\n  setJob,\n  fetchPlans,\n  getPlans,\n  getPlansSuccess,\n  getPlansFailure,\n  setIsOpenSelectPlanDialog,\n  showOAuthProgress,\n  setAgreement,\n  getCapiKeys,\n  getCapiKeysSuccess,\n  getCapiKeysFailure,\n  removeCapiKey,\n  removeCapiKeySuccess,\n  removeCapiKeyFailure,\n  removeAllCapiKeysSuccess,\n  removeAllCapiKeysFailure,\n  removeAllCapiKeys,\n  getCapiKeysAction,\n  removeAllCapiKeysAction,\n  removeCapiKeyAction,\n  setOAuthCloudSource,\n  setSocialDialogState,\n  logoutUser,\n  logoutUserSuccess,\n  logoutUserFailure,\n  logoutUserAction,\n  oauthCloudUserSelector,\n  setInitialLoadingState,\n} from '../../oauth/cloud'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('oauth cloud slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('signIn', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, signIn())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('signInSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = 'message'\n      const state = {\n        ...initialState,\n        message: data,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, signInSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('signInFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, signInFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserInfo', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserInfo())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserInfoSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      const data = { id: 12 }\n      // Arrange\n      const state = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          loading: false,\n          data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserInfoSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserInfoFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          loading: false,\n          error: data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserInfoFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addFreeDb', () => {\n    it('should properly set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          freeDb: {\n            ...initialState.user.freeDb,\n            loading: true,\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, addFreeDb())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addFreeDbSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      const data = {\n        host: 'localhost',\n        port: 6379,\n        id: 'id',\n        modules: [],\n        version: '1',\n      }\n      // Arrange\n      const state = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          freeDb: {\n            ...initialState.user.freeDb,\n            data,\n            loading: false,\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, addFreeDbSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('addFreeDbFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'some error'\n      const state = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          freeDb: {\n            ...initialState.user.freeDb,\n            loading: false,\n            error: data,\n          },\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, addFreeDbFailure(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSelectAccountDialogState', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = true\n      const state = {\n        ...initialState,\n        isOpenSelectAccountDialog: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSelectAccountDialogState(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setIsOpenSelectPlanDialog', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = true\n      const state = {\n        ...initialState,\n        plan: {\n          ...initialState.plan,\n          isOpenDialog: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setIsOpenSelectPlanDialog(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPlans', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        plan: {\n          ...initialState.plan,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPlans())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPlansSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = [\n        {\n          id: 12148,\n          type: 'fixed',\n          name: 'Cache 30MB',\n          provider: 'AWS',\n          region: 'eu-west-1',\n          price: 0,\n          details: {\n            countryName: 'Poland',\n            cityName: 'Warsaw',\n            id: 12148,\n            region: 'eu-west-1',\n          },\n        },\n      ]\n      const state = {\n        ...initialState,\n        plan: {\n          ...initialState.plan,\n          loading: false,\n          data,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPlansSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPlansFailure', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        plan: {\n          ...initialState.plan,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPlansFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSocialDialogState', () => {\n    it('should properly set the source=SignInDialogSource.BrowserSearch and isOpenSocialDialog=true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        isOpenSocialDialog: true,\n        source: OAuthSocialSource.BrowserSearch,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setSocialDialogState(OAuthSocialSource.BrowserSearch),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { cloud: nextState },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n\n    it('should not set source=null and set and isOpenSocialDialog=false', () => {\n      // Arrange\n      const prevState = {\n        ...initialState,\n        isOpenSocialDialog: true,\n        source: OAuthSocialSource.BrowserSearch,\n      }\n      const state = {\n        ...initialState,\n        isOpenSocialDialog: false,\n        source: OAuthSocialSource.BrowserSearch,\n      }\n\n      // Act\n      const nextState = reducer(prevState, setSocialDialogState(null))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { cloud: nextState },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setAgreement', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const prevInitialState = {\n        ...initialState,\n        agreement: false,\n      }\n      const state = {\n        ...initialState,\n        agreement: true,\n      }\n\n      // Act\n      const nextState = reducer(prevInitialState, setAgreement(true))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: { cloud: nextState },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getCapiKeys', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getCapiKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getCapiKeysSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = [\n        {\n          id: '1',\n        },\n      ]\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          data,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getCapiKeysSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getCapiKeysFailure', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getCapiKeysFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeCapiKey', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeCapiKey())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeCapiKeySuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        capiKeys: {\n          data: [{ id: '1' }, { id: '2' }, { id: '3' }],\n        },\n      }\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: false,\n          data: [{ id: '1' }, { id: '3' }],\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState, removeCapiKeySuccess('2'))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeCapiKeyFailure', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeCapiKeyFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeAllCapiKeys', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeAllCapiKeys())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeAllCapiKeysSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        capiKeys: {\n          data: [{ id: '1' }, { id: '2' }, { id: '3' }],\n        },\n      }\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: false,\n          data: [],\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState, removeAllCapiKeysSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('removeAllCapiKeysFailure', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        capiKeys: {\n          ...initialState.capiKeys,\n          loading: false,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, removeAllCapiKeysFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setOAuthCloudSource', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const data = OAuthSocialSource.Autodiscovery\n      const state = {\n        ...initialState,\n        source: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setOAuthCloudSource(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('logoutUser', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState.user,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, logoutUser())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudUserSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('logoutUserSuccess', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          loading: true,\n          data: {},\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState as any, logoutUserSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudUserSelector(rootState)).toEqual(initialState.user)\n    })\n  })\n\n  describe('logoutUserFailure', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        user: {\n          ...initialState.user,\n          loading: true,\n          data: {},\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState as any, logoutUserFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudUserSelector(rootState)).toEqual(initialState.user)\n    })\n  })\n\n  describe('setInitialLoadingState', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const userState = {\n        ...initialState.user,\n        initialLoading: false,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState as any,\n        setInitialLoadingState(false),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        oauth: {\n          cloud: nextState,\n        },\n      })\n      expect(oauthCloudUserSelector(rootState)).toEqual(userState)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('fetchUserInfo', () => {\n      it('call both fetchUserInfo and getUserInfoSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = { id: 123123 }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserInfo())\n\n        // Assert\n        const expectedActions = [\n          getUserInfo(),\n          getUserInfoSuccess(responsePayload.data),\n          setSocialDialogState(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('call setSelectAccountDialogState and setSocialDialogState when fetch is successed and accounts > 1', async () => {\n        // Arrange\n        const data = { id: 123123, accounts: [{}, {}] }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserInfo())\n\n        // Assert\n        const expectedActions = [\n          getUserInfo(),\n          setSelectAccountDialogState(true),\n          removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n          getUserInfoSuccess(responsePayload.data),\n          setSocialDialogState(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both fetchAccountInfo and getUserInfoFailure when fetch is fail', async () => {\n        // Arrange\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserInfo())\n\n        // Assert\n        const expectedActions = [\n          getUserInfo(),\n          addErrorNotification(responsePayload as AxiosError),\n          getUserInfoFailure(responsePayload.response.data.message),\n          setOAuthCloudSource(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('createFreeDb', () => {\n      it('call both addFreeDb and setJob when post is successed', async () => {\n        // Arrange\n        const data = {\n          id: '123123',\n          name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          status: CloudJobStatus.Running,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createFreeDbJob({\n            name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          }),\n        )\n\n        // Assert\n        const expectedActions = [addFreeDb(), setJob(data)]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both fetchAccountInfo and addFreeDbFailure when post is fail', async () => {\n        // Arrange\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createFreeDbJob({\n            name: CloudJobName.CreateFreeSubscriptionAndDatabase,\n          }),\n        )\n\n        // Assert\n        const expectedActions = [\n          addFreeDb(),\n          addErrorNotification(responsePayload as AxiosError),\n          addFreeDbFailure(responsePayload.response.data.message),\n          setOAuthCloudSource(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('activateAccount', () => {\n      it('call both getUserInfo and getUserInfoSuccess when put is successed', async () => {\n        // Arrange\n        const data = {\n          id: 3,\n          accounts: [\n            { id: 3, name: 'name' },\n            { id: 4, name: 'name' },\n          ],\n        }\n        const responseAccountPayload = { data, status: 200 }\n\n        apiService.put = jest.fn().mockResolvedValue(responseAccountPayload)\n\n        // Act\n        await store.dispatch<any>(activateAccount('123'))\n\n        // Assert\n        const expectedActions = [getUserInfo(), getUserInfoSuccess(data)]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both getUserInfo and getUserInfoFailure when put is fail', async () => {\n        // Arrange\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.put = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(activateAccount('3'))\n\n        // Assert\n        const expectedActions = [\n          getUserInfo(),\n          addErrorNotification(responsePayload as AxiosError),\n          getUserInfoFailure(responsePayload.response.data.message),\n          setOAuthCloudSource(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('createFreeDbSuccess', () => {\n      it('should call proper actions without error', async () => {\n        // Arrange\n        const result = {\n          resourceId: '123',\n        }\n        const onConnect = () => {}\n\n        // Act\n        await store.dispatch<any>(\n          createFreeDbSuccess(result, {}, CloudJobName.CreateFreeDatabase),\n        )\n\n        // Assert\n        const expectedActions = [\n          showOAuthProgress(true),\n          removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n          addInfiniteNotification(\n            INFINITE_MESSAGES.SUCCESS_CREATE_DB(\n              {},\n              onConnect,\n              CloudJobName.CreateFreeDatabase,\n            ),\n          ),\n          setSelectAccountDialogState(false),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('fetchPlans', () => {\n      it('call both fetchPlans and getPlansSuccess when fetch is successed', async () => {\n        // Arrange\n        const data = [\n          {\n            id: 12148,\n            type: 'fixed',\n            name: 'Cache 30MB',\n            provider: 'AWS',\n            region: 'eu-west-1',\n            price: 0,\n            details: {\n              countryName: 'Poland',\n              cityName: 'Warsaw',\n              id: 12148,\n              region: 'eu-west-1',\n            },\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPlans())\n\n        // Assert\n        const expectedActions = [\n          getPlans(),\n          getPlansSuccess(responsePayload.data),\n          setIsOpenSelectPlanDialog(true),\n          setSocialDialogState(null),\n          setSelectAccountDialogState(false),\n          removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n      it('call setIsOpenSelectPlanDialog and setSocialDialogState when fetch is successed and accounts > 1', async () => {\n        // Arrange\n        const data = [\n          {\n            id: 12148,\n            type: 'fixed',\n            name: 'Cache 30MB',\n            provider: 'AWS',\n            region: 'eu-west-1',\n            price: 0,\n            details: {\n              countryName: 'Poland',\n              cityName: 'Warsaw',\n              id: 12148,\n              region: 'eu-west-1',\n            },\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPlans())\n\n        // Assert\n        const expectedActions = [\n          getPlans(),\n          getPlansSuccess(responsePayload.data),\n          setIsOpenSelectPlanDialog(true),\n          setSocialDialogState(null),\n          setSelectAccountDialogState(false),\n          removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('call both getPlans and getPlansFailure when fetch is fail', async () => {\n        // Arrange\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPlans())\n\n        // Assert\n        const expectedActions = [\n          getPlans(),\n          addErrorNotification(responsePayload as AxiosError),\n          getPlansFailure(),\n          removeInfiniteNotification(InfiniteMessagesIds.oAuthProgress),\n          setOAuthCloudSource(null),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('getCapiKeysAction', () => {\n      it('should call proper actions on succeed', async () => {\n        const data = [{ id: '1' }, { id: '2' }]\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getCapiKeysAction())\n\n        // Assert\n        const expectedActions = [getCapiKeys(), getCapiKeysSuccess(data)]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on failed', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getCapiKeysAction())\n\n        // Assert\n        const expectedActions = [\n          getCapiKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          getCapiKeysFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('removeAllCapiKeysAction', () => {\n      it('should call proper actions on succeed', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeAllCapiKeysAction())\n\n        // Assert\n        const expectedActions = [\n          removeAllCapiKeys(),\n          removeAllCapiKeysSuccess(),\n          addMessageNotification(successMessages.REMOVED_ALL_CAPI_KEYS()),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on failed', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeAllCapiKeysAction())\n\n        // Assert\n        const expectedActions = [\n          removeAllCapiKeys(),\n          addErrorNotification(responsePayload as AxiosError),\n          removeAllCapiKeysFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('removeCapiKeyAction', () => {\n      it('should call proper actions on succeed', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeCapiKeyAction({ id: '1', name: 'Key' }))\n\n        // Assert\n        const expectedActions = [\n          removeCapiKey(),\n          removeCapiKeySuccess('1'),\n          addMessageNotification(successMessages.REMOVED_CAPI_KEY('Key')),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on failed', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeCapiKeyAction({ id: '1', name: 'Key' }))\n\n        // Assert\n        const expectedActions = [\n          removeCapiKey(),\n          addErrorNotification(responsePayload as AxiosError),\n          removeCapiKeyFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('logoutUserAction', () => {\n      it('should call proper actions on succeed', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(logoutUserAction())\n\n        // Assert\n        const expectedActions = [\n          logoutUser(),\n          setSSOFlow(),\n          logoutUserSuccess(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions on failed', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(logoutUserAction())\n\n        // Assert\n        const expectedActions = [\n          logoutUser(),\n          setSSOFlow(),\n          addErrorNotification(responsePayload as AxiosError),\n          logoutUserFailure(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/panels/aiAssistant.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport reducer, {\n  initialState,\n  getAssistantChatHistoryFailed,\n  removeAssistantChatHistory,\n  removeAssistantChatHistorySuccess,\n  removeAssistantChatHistoryFailed,\n  sendQuestion,\n  updateAssistantChatAgreements,\n  updateExpertChatAgreements,\n  clearAssistantChatId,\n  setQuestionError,\n  getExpertChatHistory,\n  getExpertChatHistorySuccess,\n  getExpertChatHistoryFailed,\n  sendAnswer,\n  sendExpertQuestion,\n  setExpertQuestionError,\n  sendExpertAnswer,\n  clearExpertChatHistory,\n  getAssistantChatHistorySuccess,\n  aiAssistantChatSelector,\n  createAssistantFailed,\n  getAssistantChatHistory,\n  aiChatSelector,\n  createAssistantChat,\n  setSelectedTab,\n  createAssistantSuccess,\n  createAssistantChatAction,\n  getAssistantChatHistoryAction,\n  removeAssistantChatAction,\n  getExpertChatHistoryAction,\n  removeExpertChatHistoryAction,\n  aiExpertChatSelector,\n} from 'uiSrc/slices/panels/aiAssistant'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  AiChatMessage,\n  AiChatMessageType,\n  AiChatType,\n} from 'uiSrc/slices/interfaces/aiAssistant'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('ai assistant slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {\n        type: undefined,\n      })\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n\n    describe('setSelectedTab', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          activeTab: AiChatType.Query,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setSelectedTab(AiChatType.Query),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('createAssistantChat', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, createAssistantChat())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('createAssistantSuccess', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: false,\n          id: '1',\n        }\n\n        // Act\n        const nextState = reducer(initialState, createAssistantSuccess('1'))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('createAssistantFailed', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(initialState, createAssistantFailed())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getAssistantChatHistory', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getAssistantChatHistory())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getAssistantChatHistorySuccess', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: false,\n          messages: expect.any(Array),\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          getAssistantChatHistorySuccess([]),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getAssistantChatHistoryFailed', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getAssistantChatHistoryFailed())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getAssistantChatHistoryFailed', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getAssistantChatHistoryFailed())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('removeAssistantChatHistory', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, removeAssistantChatHistory())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('removeAssistantChatHistorySuccess', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          removeAssistantChatHistorySuccess(),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('removeAssistantChatHistoryFailed', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          removeAssistantChatHistoryFailed(),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('sendQuestion', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const humanMessage = {\n          id: '1',\n          type: AiChatMessageType.HumanMessage,\n          content: 'message',\n          context: {},\n        }\n        const state = {\n          ...initialState.assistant,\n          messages: [humanMessage],\n        }\n\n        // Act\n        const nextState = reducer(initialState, sendQuestion(humanMessage))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('sendAnswer', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const data: AiChatMessage = {\n          id: '1',\n          type: AiChatMessageType.AIMessage,\n          content: 'message',\n          context: {},\n        }\n        const state = {\n          ...initialState.assistant,\n          messages: [data],\n        }\n\n        // Act\n        const nextState = reducer(initialState, sendAnswer(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('updateAssistantChatAgreements', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.assistant,\n          agreements: true,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          updateAssistantChatAgreements(true),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('updateExpertChatAgreements', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.expert,\n          agreements: ['id'],\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          updateExpertChatAgreements('id'),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('clearAssistantChatId', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          assistant: {\n            ...initialState.assistant,\n            id: 'chatId',\n          },\n        }\n\n        const state = {\n          ...initialState.assistant,\n          id: '',\n        }\n\n        // Act\n        const nextState = reducer(currentState, clearAssistantChatId())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setQuestionError', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          assistant: {\n            ...initialState.assistant,\n            messages: [\n              {\n                id: '1',\n                content: '2',\n              },\n            ],\n          },\n        } as any\n\n        const data = {\n          id: '1',\n          error: {\n            statusCode: 500,\n            errorCode: 1,\n          },\n        }\n\n        const state = {\n          ...initialState.assistant,\n          messages: [\n            {\n              ...data,\n              content: '2',\n            },\n          ],\n        }\n\n        // Act\n        const nextState = reducer(currentState, setQuestionError(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiAssistantChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getExpertChatHistory', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.expert,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, getExpertChatHistory())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getExpertChatHistorySuccess', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          expert: {\n            ...initialState.expert,\n            loading: true,\n          },\n        }\n\n        const state = {\n          ...initialState.expert,\n          loading: false,\n          messages: [{ id: expect.any(String) }],\n        }\n\n        // Act\n        const nextState = reducer(\n          currentState,\n          getExpertChatHistorySuccess([{}] as any),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getExpertChatHistoryFailed', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          expert: {\n            ...initialState.expert,\n            loading: true,\n          },\n        }\n\n        const state = {\n          ...initialState.expert,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(currentState, getExpertChatHistoryFailed())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('sendExpertQuestion', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const data = { id: '1' } as any\n        const state = {\n          ...initialState.expert,\n          messages: [data],\n        }\n\n        // Act\n        const nextState = reducer(initialState, sendExpertQuestion(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setExpertQuestionError', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          expert: {\n            ...initialState.expert,\n            messages: [\n              {\n                id: '1',\n                content: '2',\n              },\n            ],\n          },\n        } as any\n\n        const data = {\n          id: '1',\n          error: {\n            statusCode: 500,\n            errorCode: 1,\n          },\n        }\n\n        const state = {\n          ...initialState.expert,\n          messages: [\n            {\n              ...data,\n              content: '2',\n            },\n          ],\n        }\n\n        // Act\n        const nextState = reducer(currentState, setExpertQuestionError(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('sendExpertAnswer', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const data: AiChatMessage = {\n          id: '1',\n          type: AiChatMessageType.AIMessage,\n          content: 'message',\n          context: {},\n        }\n        const state = {\n          ...initialState.expert,\n          messages: [data],\n        }\n\n        // Act\n        const nextState = reducer(initialState, sendExpertAnswer(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('clearExpertChatHistory', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          expert: {\n            ...initialState.expert,\n            messages: [\n              {\n                id: '1',\n                content: '2',\n              },\n            ],\n          },\n        } as any\n\n        const state = {\n          ...initialState.expert,\n          messages: [],\n        }\n\n        // Act\n        const nextState = reducer(currentState, clearExpertChatHistory())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { aiAssistant: nextState },\n        })\n        expect(aiExpertChatSelector(rootState)).toEqual(state)\n      })\n    })\n  })\n\n  describe('thunks', () => {\n    describe('createAssistantChatAction', () => {\n      it('should call proper actions with success result', async () => {\n        const data = { id: '1' }\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(createAssistantChatAction())\n\n        // Assert\n        const expectedActions = [\n          createAssistantChat(),\n          createAssistantSuccess(data.id),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions with failed result', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(createAssistantChatAction())\n\n        // Assert\n        const expectedActions = [createAssistantChat(), createAssistantFailed()]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('getAssistantChatHistoryAction', () => {\n      it('should call proper actions with success result', async () => {\n        const data = { messages: [] }\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getAssistantChatHistoryAction('1'))\n\n        // Assert\n        const expectedActions = [\n          getAssistantChatHistory(),\n          getAssistantChatHistorySuccess(data.messages),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions with failed result', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getAssistantChatHistoryAction('1'))\n\n        // Assert\n        const expectedActions = [\n          getAssistantChatHistory(),\n          getAssistantChatHistoryFailed(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('removeAssistantChatAction', () => {\n      it('should call proper actions with success result', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeAssistantChatAction('1'))\n\n        // Assert\n        const expectedActions = [\n          removeAssistantChatHistory(),\n          removeAssistantChatHistorySuccess(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions with failed result', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeAssistantChatAction('1'))\n\n        // Assert\n        const expectedActions = [\n          removeAssistantChatHistory(),\n          removeAssistantChatHistoryFailed(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('getExpertChatHistoryAction', () => {\n      it('should call proper actions with success result', async () => {\n        const data = [] as any\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getExpertChatHistoryAction('1'))\n\n        // Assert\n        const expectedActions = [\n          getExpertChatHistory(),\n          getExpertChatHistorySuccess(data),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('should call proper actions with failed result', async () => {\n        const errorMessage = 'Error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getExpertChatHistoryAction('1'))\n\n        // Assert\n        const expectedActions = [\n          getExpertChatHistory(),\n          addErrorNotification(responsePayload as AxiosError),\n          getExpertChatHistoryFailed(),\n        ]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('removeExpertChatHistoryAction', () => {\n      it('should call proper actions with success result', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(removeExpertChatHistoryAction('1'))\n\n        // Assert\n        const expectedActions = [clearExpertChatHistory()]\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/panels/sidePanels.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport reducer, {\n  changeSelectedTab,\n  explorePanelSelector,\n  initialState,\n  insightsPanelSelector,\n  resetExplorePanelSearch,\n  setExplorePanelContent,\n  setExplorePanelIsPageOpen,\n  setExplorePanelManifest,\n  setExplorePanelScrollTop,\n  setExplorePanelSearch,\n  changeSidePanel,\n  sidePanelsSelector,\n  toggleSidePanel,\n} from 'uiSrc/slices/panels/sidePanels'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('sidePanels slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n\n    describe('changeSidePanel', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          openedPanel: SidePanels.Insights,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          changeSidePanel(SidePanels.Insights),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n\n        expect(sidePanelsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('toggleSidePanel', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          openedPanel: SidePanels.Insights,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          toggleSidePanel(SidePanels.Insights),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n\n        expect(sidePanelsSelector(rootState)).toEqual(state)\n      })\n\n      it('should properly change state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          openedPanel: SidePanels.Insights,\n        }\n\n        const state = {\n          ...initialState,\n          openedPanel: null,\n        }\n\n        // Act\n        const nextState = reducer(\n          currentState,\n          toggleSidePanel(SidePanels.Insights),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n\n        expect(sidePanelsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('changeSelectedTab', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.insights,\n          tabSelected: InsightsPanelTabs.Recommendations,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          changeSelectedTab(InsightsPanelTabs.Recommendations),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(insightsPanelSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setExplorePanelSearch', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const search = 'path/0/1'\n        const currentState = {\n          ...initialState,\n          explore: {\n            ...initialState.explore,\n            itemScrollTop: 100,\n          },\n        }\n        const state = {\n          ...initialState,\n          explore: {\n            ...initialState.explore,\n            search,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, setExplorePanelSearch(search))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(sidePanelsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setExplorePanelScrollTop', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          explore: {\n            ...initialState.explore,\n            itemScrollTop: 100,\n          },\n        }\n\n        // Act\n        const nextState = reducer(initialState, setExplorePanelScrollTop(100))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(sidePanelsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('resetExplorePanelSearch', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          explore: {\n            ...initialState.explore,\n            search: 'path/1/1',\n            itemScrollTop: 100,\n          },\n        }\n\n        const state = {\n          ...initialState,\n          explore: {\n            ...initialState.explore,\n          },\n        }\n\n        // Act\n        const nextState = reducer(currentState, resetExplorePanelSearch())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(sidePanelsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setExplorePanelContent', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const data = {\n          data: 'any content',\n          url: 'url:123',\n        }\n        const state = {\n          ...initialState.explore,\n          ...data,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setExplorePanelContent(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(explorePanelSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setExplorePanelIsPageOpen', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState.explore,\n          isPageOpen: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setExplorePanelIsPageOpen(true))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(explorePanelSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setExplorePanelManifest', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const manifest = {\n          page1: '1',\n        }\n        const state = {\n          ...initialState.explore,\n          manifest,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          setExplorePanelManifest(manifest),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          panels: { sidePanels: nextState },\n        })\n        expect(explorePanelSelector(rootState)).toEqual(state)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep } from 'lodash'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport reducer, {\n  clearPubSubMessages,\n  concatPubSubMessages,\n  disconnectPubSub,\n  initialState,\n  PUB_SUB_ITEMS_MAX_COUNT,\n  publishMessage,\n  publishMessageAction,\n  publishMessageError,\n  publishMessageSuccess,\n  pubSubSelector,\n  setIsPubSubUnSubscribed,\n  setLoading,\n  setPubSubConnected,\n  toggleSubscribeTriggerPubSub,\n} from 'uiSrc/slices/pubsub/pubsub'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('pubsub slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n\n    describe('setPubSubConnected', () => {\n      it('should properly set state', () => {\n        const isConnected = true\n\n        // Arrange\n        const state = {\n          ...initialState,\n          isConnected,\n        }\n\n        // Act\n        const nextState = reducer(initialState, setPubSubConnected(isConnected))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('toggleSubscribeTriggerPubSub', () => {\n      it('should properly set state', () => {\n        const channels = '1 * 3'\n\n        // Arrange\n        const state = {\n          ...initialState,\n          isSubscribeTriggered: !initialState.isSubscribeTriggered,\n          subscriptions: [\n            { channel: '1', type: 'p' },\n            { channel: '*', type: 'p' },\n            { channel: '3', type: 'p' },\n          ],\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          toggleSubscribeTriggerPubSub(channels),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n\n      it('should properly set state for empty channels', () => {\n        const channels = ''\n\n        // Arrange\n        const state = {\n          ...initialState,\n          isSubscribeTriggered: !initialState.isSubscribeTriggered,\n          subscriptions: [{ channel: '*', type: 'p' }],\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          toggleSubscribeTriggerPubSub(channels),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setIsPubSubUnSubscribed', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          isSubscribed: true,\n        }\n        const state = {\n          ...currentState,\n          isSubscribed: false,\n        }\n\n        // Act\n        const nextState = reducer(currentState, setIsPubSubUnSubscribed())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('concatPubSubMessages', () => {\n      it('should properly set payload to items', () => {\n        const payload = {\n          count: 2,\n          messages: [\n            {\n              message: '1',\n              channel: '2',\n              time: 123123123,\n            },\n            {\n              message: '2',\n              channel: '2',\n              time: 123123123,\n            },\n          ],\n        }\n\n        // Arrange\n        const state: typeof initialState = {\n          ...initialState,\n          count: payload.count,\n          messages: payload.messages,\n        }\n\n        // Act\n        const nextState = reducer(initialState, concatPubSubMessages(payload))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n\n      it('should properly set items no more than MONITOR_ITEMS_MAX_COUNT', () => {\n        const payload = {\n          count: PUB_SUB_ITEMS_MAX_COUNT + 10,\n          messages: new Array(PUB_SUB_ITEMS_MAX_COUNT + 10),\n        }\n\n        // Arrange\n        const state: typeof initialState = {\n          ...initialState,\n          count: PUB_SUB_ITEMS_MAX_COUNT + 10,\n          messages: new Array(PUB_SUB_ITEMS_MAX_COUNT),\n        }\n\n        // Act\n        const nextState = reducer(initialState, concatPubSubMessages(payload))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('clearPubSubMessages', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          messages: ['a', 'b', 'c'],\n          count: 3,\n        }\n\n        const state = {\n          ...currentState,\n          messages: [],\n          count: 0,\n        }\n\n        // Act\n        const nextState = reducer(currentState, clearPubSubMessages())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setLoading', () => {\n      it('should properly set state', () => {\n        // Arrange\n\n        const state = {\n          ...initialState,\n          loading: true,\n        }\n\n        // Act\n        const nextState = reducer(state, setLoading(true))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('disconnectPubSub', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          loading: true,\n          isSubscribed: true,\n          isSubscribeTriggered: true,\n          isConnected: true,\n        }\n\n        const state = {\n          ...initialState,\n        }\n\n        // Act\n        const nextState = reducer(currentState, disconnectPubSub())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('publishMessage', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          publishing: true,\n        }\n\n        // Act\n        const nextState = reducer(initialState, publishMessage())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('publishMessageSuccess', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n        }\n\n        // Act\n        const nextState = reducer(initialState, publishMessageSuccess())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('publishMessageError', () => {\n      it('should properly set state', () => {\n        // Arrange\n        const error = 'Some error'\n        const state = {\n          ...initialState,\n          error,\n        }\n\n        // Act\n        const nextState = reducer(initialState, publishMessageError(error))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          pubsub: nextState,\n        })\n        expect(pubSubSelector(rootState)).toEqual(state)\n      })\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('publishMessageAction', () => {\n      it('succeed to fetch data', async () => {\n        const data = { affected: 1 }\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          publishMessageAction('123', 'channel', 'message'),\n        )\n\n        // Assert\n        const expectedActions = [publishMessage(), publishMessageSuccess()]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          publishMessageAction('123', 'channel', 'message'),\n        )\n\n        // Assert\n        const expectedActions = [\n          publishMessage(),\n          addErrorNotification(responsePayload as AxiosError),\n          publishMessageError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/rdi/dryRun.spec.tsx",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { AnyAction } from '@reduxjs/toolkit'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  dryRunJob,\n  dryRunJobSuccess,\n  dryRunJobFailure,\n  rdiDryRunJob,\n  rdiDryRunJobSelector,\n} from 'uiSrc/slices/rdi/dryRun'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('rdi dry run slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {} as AnyAction)\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('rdiDryRunJobSelector', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        results: null,\n      }\n\n      // Act\n      const nextState = reducer(initialState, dryRunJob())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          dryRun: nextState,\n        },\n      })\n      expect(rdiDryRunJobSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('dryRunJobSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const mockData = {\n        output: [{ connection: 'name', commands: ['HSET 1 1'] }],\n        transformation: { name: 'John' },\n      }\n\n      const state = {\n        ...initialState,\n        results: mockData,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, dryRunJobSuccess(mockData))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          dryRun: nextState,\n        },\n      })\n      expect(rdiDryRunJobSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('dryRunJobFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, dryRunJobFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          dryRun: nextState,\n        },\n      })\n      expect(rdiDryRunJobSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('rdiDryRunJob', () => {\n      it('succeed to fetch data', async () => {\n        const mockData = {\n          output: [\n            {\n              connection: 'target',\n              commands: ['HSET 1 1'],\n            },\n          ],\n          transformation: { name: 'John' },\n        }\n        const responsePayload = { data: mockData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(rdiDryRunJob('123', { name: 'Johny' }, {}))\n\n        // Assert\n        const expectedActions = [dryRunJob(), dryRunJobSuccess(mockData)]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(rdiDryRunJob('123', { name: 'Johny' }, {}))\n\n        // Assert\n        const expectedActions = [\n          dryRunJob(),\n          addErrorNotification(responsePayload as AxiosError),\n          dryRunJobFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/rdi/instances.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  setConnectedInstance,\n  setConnectedInstanceSuccess,\n  setConnectedInstanceFailure,\n  setDefaultInstance,\n  setDefaultInstanceSuccess,\n  setDefaultInstanceFailure,\n  instancesSelector,\n  fetchConnectedInstanceAction,\n  checkConnectToRdiInstanceAction,\n  createInstanceAction,\n  defaultInstanceChanging,\n  defaultInstanceChangingSuccess,\n  defaultInstanceChangingFailure,\n  editInstanceAction,\n  updateConnectedInstance,\n} from 'uiSrc/slices/rdi/instances'\nimport { apiService } from 'uiSrc/services'\nimport {\n  addErrorNotification,\n  addMessageNotification,\n  IAddInstanceErrorPayload,\n} from 'uiSrc/slices/app/notifications'\nimport { RdiInstance } from 'uiSrc/slices/interfaces'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { Rdi } from 'apiSrc/modules/rdi/models'\n\nlet store: typeof mockedStore\n\nconst mockRdiInstance = {\n  name: 'name',\n  version: '1.2',\n  url: 'http://localhost:4000',\n}\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('rdi instances slice', () => {\n  describe('setConnectedInstance', () => {\n    it('should properly set loading=true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n      }\n      // Act\n      const nextState = reducer(initialState, setConnectedInstance())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        instances: nextState,\n      })\n\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInstanceSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        connectedInstance: mockRdiInstance,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setConnectedInstanceSuccess(mockRdiInstance as RdiInstance),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setConnectedInstanceFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          loading: false,\n          error,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        setConnectedInstanceFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setDefaultInstance', () => {\n    it('should properly set loading=true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        error: '',\n      }\n      // Act\n      const nextState = reducer(initialState, setDefaultInstance())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          instances: nextState,\n        },\n      })\n\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setDefaultInstanceSuccess', () => {\n    it('should properly set loading=false', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setDefaultInstanceSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setDefaultInstanceFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setDefaultInstanceFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          instances: nextState,\n        },\n      })\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateConnectedInstance', () => {\n    it('should properly update connected instance', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        connectedInstance: {\n          ...initialState.connectedInstance,\n          ...mockRdiInstance,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        updateConnectedInstance(mockRdiInstance as Rdi),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          instances: nextState,\n        },\n      })\n\n      expect(instancesSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('fetchConnectedInstanceAction', () => {\n      it('succeed to fetch data', async () => {\n        const responsePayload = { data: mockRdiInstance, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConnectedInstanceAction('123'))\n\n        // Assert\n        const expectedActions = [\n          setConnectedInstance(),\n          setConnectedInstanceSuccess(mockRdiInstance as RdiInstance),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchConnectedInstanceAction('123'))\n\n        // Assert\n        const expectedActions = [\n          setConnectedInstance(),\n          setConnectedInstanceFailure(errorMessage),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('createInstanceAction', () => {\n      const onSuccess = jest.fn()\n      const onFail = jest.fn()\n      it('succeed to create data and call success callback', async () => {\n        const responsePayload = { data: mockRdiInstance, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n        apiService.get = jest.fn().mockResolvedValue({ status: 200, data: [] })\n\n        // Act\n        await store.dispatch<any>(\n          createInstanceAction(mockRdiInstance, onSuccess, onFail),\n        )\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingSuccess(),\n          addMessageNotification(\n            successMessages.ADDED_NEW_RDI_INSTANCE(mockRdiInstance.name),\n          ),\n        ]\n\n        expect(store.getActions()).toEqual(\n          expect.arrayContaining(expectedActions),\n        )\n        expect(onSuccess).toBeCalledWith(mockRdiInstance)\n      })\n\n      it('failed to create data and call onFail callback', async () => {\n        const errorMessage = 'Something was wrong!'\n        const errorCode = 11403\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage, errorCode },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          createInstanceAction(mockRdiInstance, onSuccess, onFail),\n        )\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingFailure(errorMessage),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n        expect(onFail).toBeCalledWith(errorCode)\n      })\n    })\n\n    describe('editInstanceAction', () => {\n      it('succeed to edit data and calls a success callback', async () => {\n        const onSuccess = jest.fn()\n        const responsePayload = { data: mockRdiInstance, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          editInstanceAction('123', mockRdiInstance, onSuccess),\n        )\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingSuccess(),\n        ]\n\n        expect(store.getActions()).toEqual(\n          expect.arrayContaining(expectedActions),\n        )\n        expect(onSuccess).toBeCalledWith(mockRdiInstance)\n      })\n\n      it('failed to edit data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(editInstanceAction('123', mockRdiInstance))\n\n        // Assert\n        const expectedActions = [\n          defaultInstanceChanging(),\n          defaultInstanceChangingFailure(errorMessage),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('checkConnectToRdiInstanceAction', () => {\n      it('succeed to fetch data', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(checkConnectToRdiInstanceAction('123'))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceSuccess(),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(checkConnectToRdiInstanceAction('123'))\n\n        // Assert\n        const expectedActions = [\n          setDefaultInstance(),\n          setDefaultInstanceFailure(errorMessage),\n          addErrorNotification({\n            ...responsePayload,\n            instanceId: '123',\n          } as IAddInstanceErrorPayload),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/rdi/pipeline.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { AnyAction } from '@reduxjs/toolkit'\nimport {\n  cleanup,\n  clearStoreActions,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  MOCK_RDI_PIPELINE_DATA,\n  MOCK_RDI_PIPELINE_JOB1,\n  MOCK_RDI_PIPELINE_JOB2,\n  MOCK_RDI_PIPELINE_JSON_DATA,\n  MOCK_RDI_PIPELINE_STATUS_DATA,\n} from 'uiSrc/mocks/data/rdi'\nimport reducer, {\n  initialState,\n  getPipeline,\n  getPipelineSuccess,\n  getPipelineFailure,\n  deployPipeline,\n  deployPipelineSuccess,\n  deployPipelineFailure,\n  getPipelineStrategies,\n  getPipelineStrategiesSuccess,\n  getPipelineStrategiesFailure,\n  setPipelineSchema,\n  setChangedFile,\n  setChangedFiles,\n  deleteChangedFile,\n  getPipelineStatus,\n  getPipelineStatusSuccess,\n  getPipelineStatusFailure,\n  fetchRdiPipeline,\n  deployPipelineAction,\n  fetchRdiPipelineSchema,\n  fetchPipelineStrategies,\n  fetchConfigTemplate,\n  fetchJobTemplate,\n  setJobFunctions,\n  fetchRdiPipelineJobFunctions,\n  getPipelineStatusAction,\n  rdiPipelineSelector,\n  rdiPipelineStatusSelector,\n  resetPipelineAction,\n  stopPipelineAction,\n  startPipelineAction,\n  triggerPipelineAction,\n  triggerPipelineActionSuccess,\n  triggerPipelineActionFailure,\n  rdiPipelineActionSelector,\n  setPipelineConfig,\n  setPipelineJobs,\n  setMonacoJobsSchema,\n  setJobNameSchema,\n  updatePipelineJob,\n} from 'uiSrc/slices/rdi/pipeline'\nimport { apiService } from 'uiSrc/services'\nimport {\n  addErrorNotification,\n  addInfiniteNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport { INFINITE_MESSAGES } from 'uiSrc/components/notifications/components'\nimport { FileChangeType, PipelineAction } from 'uiSrc/slices/interfaces'\nimport { parseJMESPathFunctions } from 'uiSrc/utils'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('rdi pipe slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {} as AnyAction)\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('rdiPipelineSelector', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPipeline())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n        data: MOCK_RDI_PIPELINE_DATA,\n        config: MOCK_RDI_PIPELINE_DATA.config,\n        jobs: MOCK_RDI_PIPELINE_DATA.jobs,\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        getPipelineSuccess(MOCK_RDI_PIPELINE_DATA),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setPipelineConfig', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        config: MOCK_RDI_PIPELINE_DATA.config,\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        setPipelineConfig(MOCK_RDI_PIPELINE_DATA.config),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setPipelineJobs', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        jobs: MOCK_RDI_PIPELINE_DATA.jobs,\n      }\n      // Act\n      const nextState = reducer(\n        initialState,\n        setPipelineJobs(MOCK_RDI_PIPELINE_DATA.jobs),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updatePipelineJob', () => {\n    it('should properly update job by name', () => {\n      // Arrange - preload state with existing jobs\n      const baseState = {\n        ...initialState,\n        jobs: MOCK_RDI_PIPELINE_DATA.jobs,\n      }\n\n      const expectedState = {\n        ...initialState,\n        jobs: [\n          MOCK_RDI_PIPELINE_JOB1,\n          { ...MOCK_RDI_PIPELINE_JOB2, value: 'newValue2' },\n        ],\n      }\n\n      // Act - update second job value\n      const nextState = reducer(\n        baseState,\n        updatePipelineJob({\n          name: MOCK_RDI_PIPELINE_JOB2.name,\n          value: 'newValue2',\n        }),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(expectedState)\n    })\n  })\n\n  describe('getPipelineFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPipelineFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deployPipeline', () => {\n    it('should set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deployPipeline())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deployPipelineSuccess', () => {\n    it('should set loading = false', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deployPipelineSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deployPipelineFailure', () => {\n    it('should set loading = false', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deployPipelineFailure())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineStrategies', () => {\n    it('should set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        strategies: {\n          ...initialState.strategies,\n          loading: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPipelineStrategies())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineStrategiesSuccess', () => {\n    it('should set loading = false', () => {\n      const mockData = [{ strategy: 'ingest', databases: ['oracle'] }]\n      // Arrange\n      const state = {\n        ...initialState,\n        strategies: {\n          ...initialState.strategies,\n          data: mockData,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getPipelineStrategiesSuccess(mockData),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineStrategiesFailure', () => {\n    it('should set loading = false', () => {\n      const mockError = 'some error'\n      // Arrange\n      const state = {\n        ...initialState,\n        strategies: {\n          ...initialState.strategies,\n          error: mockError,\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getPipelineStrategiesFailure(mockError),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setChangedFile', () => {\n    it('should set change file', () => {\n      const mockChangedFile = { name: 'name', status: FileChangeType.Added }\n      // Arrange\n      const state = {\n        ...initialState,\n        changes: {\n          name: FileChangeType.Added,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setChangedFile(mockChangedFile))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteChangedFile', () => {\n    it('should remove changed file', () => {\n      const mockChangedFile = { name: 'name', status: FileChangeType.Added }\n      // Arrange\n      const state = {\n        ...initialState,\n        changes: {\n          name: FileChangeType.Added,\n        },\n      }\n\n      // Act\n      const nextState = reducer(state, deleteChangedFile(mockChangedFile.name))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(initialState)\n    })\n  })\n\n  describe('setChangedFiles', () => {\n    it('should replace changed files', () => {\n      const mockChangedFiles1 = { name: FileChangeType.Modified }\n      const mockChangedFiles2 = { name: FileChangeType.Removed }\n\n      // Arrange\n      const state = {\n        ...initialState,\n        changes: mockChangedFiles2,\n      }\n\n      // Act\n      const nextState = reducer(\n        { ...initialState, changes: mockChangedFiles1 },\n        setChangedFiles(mockChangedFiles2),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setJobFunctions', () => {\n    it('should set job functions as monaco compilation items', () => {\n      const summaryValue = 'summary'\n      const argumentsValue = [\n        {\n          name: 'encoded',\n          type: 'string',\n          display_text: 'base64 encoded string',\n          optional: false,\n        },\n      ]\n      const mockData = {\n        function: {\n          summary: summaryValue,\n          arguments: argumentsValue,\n        },\n      }\n\n      // Arrange\n      const state = {\n        ...initialState,\n        jobFunctions: parseJMESPathFunctions(mockData),\n      }\n\n      // Act\n      const nextState = reducer(initialState, setJobFunctions(mockData))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineStatus', () => {\n    it('should set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState.status,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPipelineStatus())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineStatusSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineStatusSuccess', () => {\n    it('should proper data', () => {\n      const data = MOCK_RDI_PIPELINE_STATUS_DATA\n      // Arrange\n      const state = {\n        ...initialState.status,\n        loading: false,\n        data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPipelineStatusSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineStatusSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getPipelineStatusFailure', () => {\n    it('should set error', () => {\n      const error = 'some error'\n      // Arrange\n      const state = {\n        ...initialState.status,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getPipelineStatusFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineStatusSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('triggerPipelineAction', () => {\n    it('should set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState.pipelineAction,\n        loading: true,\n        action: PipelineAction.Start,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        triggerPipelineAction(PipelineAction.Start),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineActionSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('triggerPipelineActionSuccess', () => {\n    it('should set loading = true', () => {\n      // Arrange\n      const state = {\n        ...initialState.pipelineAction,\n        loading: false,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(initialState, triggerPipelineActionSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineActionSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('triggerPipelineActionFailure', () => {\n    it('should set loading = true', () => {\n      const error = 'Some reset error'\n      // Arrange\n      const state = {\n        ...initialState.pipelineAction,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        triggerPipelineActionFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          pipeline: nextState,\n        },\n      })\n      expect(rdiPipelineActionSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n  describe('thunks', () => {\n    describe('fetchRdiPipeline', () => {\n      it('succeed to fetch data', async () => {\n        const data = MOCK_RDI_PIPELINE_JSON_DATA\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipeline('123'))\n\n        // Assert\n        const expectedActions = [\n          getPipeline(),\n          getPipelineSuccess(MOCK_RDI_PIPELINE_DATA),\n          setChangedFiles({}),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipeline('123'))\n\n        // Assert\n        const expectedActions = [\n          getPipeline(),\n          addErrorNotification(responsePayload as AxiosError),\n          getPipelineFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deployPipelineAction', () => {\n      it('succeed to post data', async () => {\n        const mockData = { config: {}, jobs: [] }\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deployPipelineAction('123', mockData))\n\n        // Assert\n        const expectedActions = [\n          deployPipeline(),\n          deployPipelineSuccess(),\n          setChangedFiles({}),\n          addInfiniteNotification(INFINITE_MESSAGES.SUCCESS_DEPLOY_PIPELINE()),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('failed to post data', async () => {\n        const mockData = { config: {}, jobs: [] }\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deployPipelineAction('123', mockData))\n\n        // Assert\n        const expectedActions = [\n          deployPipeline(),\n          addErrorNotification(responsePayload as AxiosError),\n          deployPipelineFailure(),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('resetPipelineAction', () => {\n      it('succeed to post data', async () => {\n        const cb = jest.fn()\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(resetPipelineAction('123', cb, cb))\n\n        // Assert\n        const expectedActions = [\n          triggerPipelineAction(PipelineAction.Reset),\n          triggerPipelineActionSuccess(),\n          addMessageNotification(successMessages.SUCCESS_RESET_PIPELINE()),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('failed to post data', async () => {\n        const cb = jest.fn()\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(resetPipelineAction('123', cb, cb))\n\n        // Assert\n        const expectedActions = [\n          triggerPipelineAction(PipelineAction.Reset),\n          addErrorNotification(responsePayload as AxiosError),\n          triggerPipelineActionFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('stopPipelineAction', () => {\n      it('succeed to post data', async () => {\n        const cb = jest.fn()\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(stopPipelineAction('123', cb, cb))\n\n        // Assert\n        const expectedActions = [\n          triggerPipelineAction(PipelineAction.Stop),\n          triggerPipelineActionSuccess(),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('failed to post data', async () => {\n        const cb = jest.fn()\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(stopPipelineAction('123', cb, cb))\n\n        // Assert\n        const expectedActions = [\n          triggerPipelineAction(PipelineAction.Stop),\n          addErrorNotification(responsePayload as AxiosError),\n          triggerPipelineActionFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('startPipelineAction', () => {\n      it('succeed to post data', async () => {\n        const cb = jest.fn()\n        const responsePayload = { status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(startPipelineAction('123', cb, cb))\n\n        // Assert\n        const expectedActions = [\n          triggerPipelineAction(PipelineAction.Start),\n          triggerPipelineActionSuccess(),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('failed to post data', async () => {\n        const cb = jest.fn()\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(startPipelineAction('123', cb, cb))\n\n        // Assert\n        const expectedActions = [\n          triggerPipelineAction(PipelineAction.Start),\n          addErrorNotification(responsePayload as AxiosError),\n          triggerPipelineActionFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchRdiPipelineSchema', () => {\n      it('succeed to fetch data with minimal schema', async () => {\n        const data = { config: 'string' }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineSchema('123'))\n\n        // Assert\n        const expectedActions = [\n          setPipelineSchema(data),\n          setMonacoJobsSchema({ required: [] }),\n          setJobNameSchema(null),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('succeed to fetch data with complete jobs schema', async () => {\n        const data = {\n          config: 'string',\n          jobs: {\n            type: 'object',\n            properties: {\n              name: {\n                type: 'string',\n                pattern: '^[a-zA-Z][a-zA-Z0-9_]*$',\n              },\n              source: {\n                type: 'object',\n                properties: {\n                  server_name: { type: 'string' },\n                  schema: { type: 'string' },\n                  table: { type: 'string' },\n                },\n              },\n            },\n            required: ['name', 'source'],\n          },\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineSchema('123'))\n\n        // Assert\n        const expectedMonacoJobsSchema = {\n          type: 'object',\n          properties: {\n            source: {\n              type: 'object',\n              properties: {\n                server_name: { type: 'string' },\n                schema: { type: 'string' },\n                table: { type: 'string' },\n              },\n            },\n          },\n          required: ['source'], // 'name' is filtered out\n        }\n\n        const expectedJobNameSchema = {\n          type: 'string',\n          pattern: '^[a-zA-Z][a-zA-Z0-9_]*$',\n        }\n\n        const expectedActions = [\n          setPipelineSchema(data),\n          setMonacoJobsSchema(expectedMonacoJobsSchema),\n          setJobNameSchema(expectedJobNameSchema),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('succeed to fetch data with jobs schema but no name property', async () => {\n        const data = {\n          config: 'string',\n          jobs: {\n            type: 'object',\n            properties: {\n              source: { type: 'object' },\n              transform: { type: 'array' },\n            },\n            required: ['source', 'transform'],\n          },\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineSchema('123'))\n\n        // Assert\n        const expectedMonacoJobsSchema = {\n          type: 'object',\n          properties: {\n            source: { type: 'object' },\n            transform: { type: 'array' },\n          },\n          required: ['source', 'transform'],\n        }\n\n        const expectedActions = [\n          setPipelineSchema(data),\n          setMonacoJobsSchema(expectedMonacoJobsSchema),\n          setJobNameSchema(null), // default fallback value\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('succeed to fetch data with empty jobs schema', async () => {\n        const data = {\n          config: 'string',\n          jobs: {},\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineSchema('123'))\n\n        // Assert\n        const expectedActions = [\n          setPipelineSchema(data),\n          setMonacoJobsSchema({ required: [] }),\n          setJobNameSchema(null),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineSchema('123'))\n\n        // Assert\n        const expectedActions = [setPipelineSchema(null)]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchRdiPipelineJobFunctions', () => {\n      it('succeed to fetch data', async () => {\n        const data = { function: { summary: 'summary', arguments: [] } }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineJobFunctions('123'))\n\n        // Assert\n        const expectedActions = [setJobFunctions(data)]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRdiPipelineJobFunctions('123'))\n\n        expect(store.getActions().length).toEqual(0)\n      })\n    })\n\n    describe('fetchPipelineStrategies', () => {\n      it('succeed to fetch data', async () => {\n        const data = {\n          strategies: [{ strategy: 'ingest', databases: ['oracle'] }],\n        }\n\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPipelineStrategies('123'))\n\n        // Assert\n        const expectedActions = [\n          getPipelineStrategies(),\n          getPipelineStrategiesSuccess(data.strategies),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchPipelineStrategies('123'))\n\n        // Assert\n        const expectedActions = [\n          getPipelineStrategies(),\n          getPipelineStrategiesFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchJobTemplate', () => {\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchJobTemplate('123', 'db_type'))\n\n        // Assert\n        const expectedActions = [\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchConfigTemplate', () => {\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          fetchConfigTemplate('123', 'ingest', 'db_type'),\n        )\n\n        // Assert\n        const expectedActions = [\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('getPipelineStatusAction', () => {\n      it('succeed to fetch data', async () => {\n        const data = MOCK_RDI_PIPELINE_STATUS_DATA\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getPipelineStatusAction('123'))\n\n        // Assert\n        const expectedActions = [\n          getPipelineStatus(),\n          getPipelineStatusSuccess(MOCK_RDI_PIPELINE_STATUS_DATA),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(getPipelineStatusAction('123'))\n\n        // Assert\n        const expectedActions = [\n          getPipelineStatus(),\n          getPipelineStatusFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/rdi/testConnections.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport reducer, {\n  initialState,\n  testConnections,\n  testConnectionsSuccess,\n  testConnectionsFailure,\n  rdiTestConnectionsSelector,\n  testConnectionsAction,\n} from 'uiSrc/slices/rdi/testConnections'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nlet store: typeof mockedStore\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('rdi test connections slice', () => {\n  describe('rdiTestConnectionsSelector', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n        results: null,\n      }\n\n      // Act\n      const nextState = reducer(initialState, testConnections())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          testConnections: nextState,\n        },\n      })\n      expect(rdiTestConnectionsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('testConnectionsSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const data = {\n        target: { success: [{ target: 'target' }], fail: [] },\n        source: { success: [], fail: [] },\n      }\n      const state = {\n        ...initialState,\n        results: data,\n        loading: false,\n      }\n\n      // Act\n      const nextState = reducer(initialState, testConnectionsSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          testConnections: nextState,\n        },\n      })\n      expect(rdiTestConnectionsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('testConnectionsFailure', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, testConnectionsFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        rdi: {\n          testConnections: nextState,\n        },\n      })\n      expect(rdiTestConnectionsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('testConnectionsAction', () => {\n      it('succeed to fetch data', async () => {\n        const mockData = {\n          targets: {\n            target: {\n              status: 'success',\n            },\n          },\n\n          sources: {\n            source: {\n              connected: true,\n              error: '',\n            },\n          },\n        }\n\n        const responsePayload = { data: mockData, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(testConnectionsAction('123', 'config'))\n\n        // Assert\n        const expectedActions = [\n          testConnections(),\n          testConnectionsSuccess({\n            target: { success: [{ target: 'target' }], fail: [] },\n            source: {\n              success: [\n                {\n                  target: 'source',\n                },\n              ],\n              fail: [],\n            },\n          }),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(testConnectionsAction('123', 'config'))\n\n        // Assert\n        const expectedActions = [\n          testConnections(),\n          addErrorNotification(responsePayload as AxiosError),\n          testConnectionsFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/recommendations/recommendations.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport { cloneDeep, set } from 'lodash'\nimport { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\nimport { Vote } from 'uiSrc/constants/recommendations'\nimport { apiService, resourcesService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport reducer, {\n  initialState,\n  getRecommendations,\n  getRecommendationsSuccess,\n  getRecommendationsFailure,\n  setIsHighlighted,\n  readRecommendations,\n  fetchRecommendationsAction,\n  readRecommendationsAction,\n  recommendationsSelector,\n  updateRecommendationSuccess,\n  updateRecommendationError,\n  updateLiveRecommendation,\n  updateRecommendation,\n  setTotalUnread,\n  deleteLiveRecommendations,\n  deleteRecommendations,\n  getContentRecommendations,\n  getContentRecommendationsSuccess,\n  getContentRecommendationsFailure,\n  fetchContentRecommendations,\n  addUnreadRecommendations,\n} from 'uiSrc/slices/recommendations/recommendations'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockStore,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nlet store: typeof mockedStore\n\nconst mockId = 'id'\nconst mockName = 'name'\nconst mockVote = Vote.Like\nconst mockRecommendations = {\n  recommendations: [\n    { id: mockId, name: mockName, read: false, vote: null, hide: false },\n  ],\n  totalUnread: 1,\n}\nconst mockRecommendationVoted = cloneDeep(mockRecommendations)\nset(mockRecommendationVoted, 'recommendations[0].vote', mockVote)\n\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('recommendations slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n\n    describe('getRecommendations', () => {\n      it('should properly set loading: true', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: true,\n          error: '',\n        }\n\n        // Act\n        const nextState = reducer(initialState, getRecommendations())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getRecommendationsFailure', () => {\n      it('should properly set error', () => {\n        // Arrange\n        const error = 'Some error'\n        const state = {\n          ...initialState,\n          error,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          getRecommendationsFailure(error),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getRecommendationsSuccess', () => {\n      it('should properly set loading: true', () => {\n        const payload = mockRecommendations\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: false,\n          data: mockRecommendations,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          getRecommendationsSuccess(payload),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('setTotalUnread', () => {\n      it('should properly set total unread', () => {\n        // Arrange\n        const data = 10\n        const state = {\n          ...initialState,\n          isHighlighted: true,\n          data: {\n            ...initialState.data,\n            totalUnread: data,\n          },\n        }\n\n        // Act\n        const nextState = reducer(initialState, setTotalUnread(data))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('addUnreadRecommendations', () => {\n      it('should properly set total unread', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          data: {\n            ...initialState.data,\n            recommendations: [{ id: '1' }, { id: '2' }],\n            totalUnread: 1,\n          },\n        }\n\n        const state = {\n          ...currentState,\n          isHighlighted: true,\n          data: {\n            ...initialState.data,\n            recommendations: [{ id: '3' }, { id: '1' }, { id: '2' }],\n            totalUnread: 2,\n          },\n        }\n\n        // Act\n        const nextState = reducer(\n          currentState,\n          addUnreadRecommendations({\n            recommendations: [{ id: '3' }],\n            totalUnread: 2,\n          }),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('readRecommendations', () => {\n      it('should properly set totalUnread', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          data: { ...initialState.data, totalUnread: 0 },\n          loading: false,\n          error: '',\n        }\n\n        // Act\n        const nextState = reducer(initialState, readRecommendations(0))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('updateRecommendationSuccess', () => {\n      it('should properly set data', () => {\n        const payload = mockRecommendationVoted.recommendations[0]\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: false,\n          data: mockRecommendationVoted,\n        }\n\n        // Act\n        const initialStateWithRecs = {\n          ...initialState,\n          data: mockRecommendations,\n        }\n        const nextState = reducer(\n          initialStateWithRecs,\n          updateRecommendationSuccess(payload),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('updateRecommendationError', () => {\n      it('should properly set an error', () => {\n        const error = 'Some error'\n        const state = {\n          ...initialState,\n          error,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          updateRecommendationError(error),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n  })\n\n  describe('deleteRecommendations', () => {\n    it('should properly delete recommendations', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          recommendations: [{ id: '1' }, { id: '2' }, { id: '3' }],\n          totalUnread: 1,\n        },\n        loading: false,\n        error: '',\n      }\n\n      const state = {\n        ...initialState,\n        data: {\n          ...initialState.data,\n          recommendations: [{ id: '1' }, { id: '3' }],\n          totalUnread: 0,\n        },\n        loading: false,\n        error: '',\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        deleteRecommendations([{ id: '2', isRead: false }]),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        recommendations: nextState,\n      })\n      expect(recommendationsSelector(rootState)).toEqual(state)\n    })\n\n    describe('getContentRecommendations', () => {\n      it('should properly set loading: true', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: true,\n          error: '',\n        }\n\n        // Act\n        const nextState = reducer(initialState, getContentRecommendations())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getContentRecommendationsFailure', () => {\n      it('should properly set error', () => {\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: false,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          getContentRecommendationsFailure(),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n\n    describe('getContentRecommendationsSuccess', () => {\n      it('should properly set loading: true', () => {\n        const payload = MOCK_RECOMMENDATIONS\n        // Arrange\n        const state = {\n          ...initialState,\n          loading: false,\n          content: MOCK_RECOMMENDATIONS,\n        }\n\n        // Act\n        const nextState = reducer(\n          initialState,\n          getContentRecommendationsSuccess(payload),\n        )\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: nextState,\n        })\n        expect(recommendationsSelector(rootState)).toEqual(state)\n      })\n    })\n  })\n\n  // thunks\n  describe('thunks', () => {\n    describe('fetchRecommendationsAction', () => {\n      it('succeed to fetch recommendations data', async () => {\n        const data = {\n          recommendations: [],\n          totalUnread: 0,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRecommendationsAction('instanceId'))\n\n        // Assert\n        const expectedActions = [\n          getRecommendations(),\n          getRecommendationsSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('succeed to fetch recommendations data and set highlighting', async () => {\n        const data = mockRecommendations\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n        const onSuccessActionMock = jest.fn()\n\n        const state = {\n          ...initialStateDefault.recommendations,\n        }\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          recommendations: state,\n        })\n\n        const tempStore = mockStore(rootState)\n\n        // Act\n        await tempStore.dispatch<any>(\n          fetchRecommendationsAction('instanceId', onSuccessActionMock),\n        )\n\n        // Assert\n        const expectedActions = [\n          getRecommendations(),\n          setIsHighlighted(true),\n          getRecommendationsSuccess(data),\n        ]\n\n        expect(tempStore.getActions()).toEqual(expectedActions)\n        expect(onSuccessActionMock).toBeCalledWith(\n          mockRecommendations.recommendations,\n        )\n        expect(onSuccessActionMock).toBeCalledTimes(1)\n      })\n\n      it('failed to fetch recommendations data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchRecommendationsAction('instanceId'))\n\n        // Assert\n        const expectedActions = [\n          getRecommendations(),\n          addErrorNotification(responsePayload as AxiosError),\n          getRecommendationsFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('readRecommendationsAction', () => {\n      it('succeed to read recommendations', async () => {\n        const data = {\n          recommendations: [],\n          totalUnread: 0,\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(readRecommendationsAction('instanceId'))\n\n        // Assert\n        const expectedActions = [\n          readRecommendations(data.totalUnread),\n          setIsHighlighted(false),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to read recommendations', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(readRecommendationsAction('instanceId'))\n\n        expect(store.getActions()).toEqual([])\n      })\n    })\n\n    describe('putLiveRecommendationVote', () => {\n      it('succeed to update recommendation', async () => {\n        // const data = mockRecommendations\n        const data = mockRecommendationVoted.recommendations[0]\n        const responsePayload = { data, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n        const onSuccessActionMock = jest.fn()\n\n        // Act\n        await store.dispatch<any>(\n          updateLiveRecommendation(\n            mockId,\n            { vote: mockVote },\n            onSuccessActionMock,\n          ),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateRecommendation(),\n          updateRecommendationSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n        expect(onSuccessActionMock).toBeCalledWith(data)\n      })\n\n      it('failed to put recommendation vote', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          updateLiveRecommendation(mockId, mockVote, mockName),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateRecommendation(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateRecommendationError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('deleteLiveRecommendations', () => {\n      it('succeed to delete recommendation', async () => {\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n        const onSuccessActionMock = jest.fn()\n\n        // Act\n        await store.dispatch<any>(\n          deleteLiveRecommendations([mockId], onSuccessActionMock),\n        )\n\n        // Assert\n        expect(onSuccessActionMock).toBeCalledWith('')\n        const expectedActions = [\n          updateRecommendation(),\n          deleteRecommendations([mockId]),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to delete recommendation', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteLiveRecommendations([mockId]))\n\n        // Assert\n        const expectedActions = [\n          updateRecommendation(),\n          addErrorNotification(responsePayload as AxiosError),\n          updateRecommendationError(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchContentRecommendations', () => {\n      it('succeed to get content recommendations', async () => {\n        const data = MOCK_RECOMMENDATIONS\n        const responsePayload = { status: 200, data }\n\n        resourcesService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchContentRecommendations())\n\n        // Assert\n        const expectedActions = [\n          getContentRecommendations(),\n          getContentRecommendationsSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to get content recommendations', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        resourcesService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchContentRecommendations())\n\n        // Assert\n        const expectedActions = [\n          getContentRecommendations(),\n          getContentRecommendationsFailure(),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/search/searchAndQuery.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\n\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\n\nimport { RunQueryMode } from 'uiSrc/slices/interfaces'\nimport reducer, {\n  initialState,\n  searchAndQuerySelector,\n  changeSQActiveRunQueryMode,\n} from '../../search/searchAndQuery'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {} as any)\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('changeSQActiveRunQueryMode', () => {\n    it('should properly set mode', () => {\n      const state = {\n        ...initialState,\n        activeRunQueryMode: RunQueryMode.Raw,\n      }\n\n      const nextState = reducer(\n        initialState,\n        changeSQActiveRunQueryMode(RunQueryMode.Raw),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        search: { query: nextState },\n      })\n\n      expect(searchAndQuerySelector(rootState)).toEqual(state)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/user/user-settings.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\n\nimport { apiService } from 'uiSrc/services'\nimport {\n  cleanup,\n  mockedStore,\n  initialStateDefault,\n} from 'uiSrc/utils/test-utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\n\nimport reducer, {\n  initialState,\n  setUserSettingsInitialState,\n  setSettingsPopupState,\n  getUserConfigSettings,\n  getUserConfigSettingsSuccess,\n  getUserConfigSettingsFailure,\n  updateUserConfigSettings,\n  updateUserConfigSettingsSuccess,\n  updateUserConfigSettingsFailure,\n  getUserSettingsSpec,\n  getUserSettingsSpecSuccess,\n  getUserSettingsSpecFailure,\n  fetchUserConfigSettings,\n  fetchUserSettingsSpec,\n  updateUserConfigSettingsAction,\n  userSettingsSelector,\n} from '../../user/user-settings'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('userSettings slice', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('setUserSettingsInitialState', () => {\n    it('should properly set the initial state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setUserSettingsInitialState())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n\n    it('Cleanup should properly be \"true\" in the initial state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        workbench: {\n          ...initialState.workbench,\n          cleanup: true,\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, setUserSettingsInitialState())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('setSettingsPopupState', () => {\n    it('should properly set the state', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        isShowConceptsPopup: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, setSettingsPopupState(true))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserConfigSettings', () => {\n    it('should properly set the state before fetch', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserConfigSettings())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserConfigSettingsSuccess', () => {\n    it('should properly set the state after fetch the data', () => {\n      // Arrange\n      const data = {\n        agreements: {\n          eula: true,\n        },\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        config: data,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getUserConfigSettingsSuccess(data),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserConfigSettingsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const error = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getUserConfigSettingsFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateUserConfigSettings', () => {\n    it('should properly set the state before fetch', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, updateUserConfigSettings())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateUserConfigSettingsSuccess', () => {\n    it('should properly set the state after fetch the data', () => {\n      // Arrange\n\n      const config = {\n        agreements: {\n          eula: true,\n        },\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        isShowConceptsPopup: false,\n        config,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        updateUserConfigSettingsSuccess(config),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('updateUserConfigSettingsFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const error = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        updateUserConfigSettingsFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserSettingsSpec', () => {\n    it('should properly set the state before fetch', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        loading: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserSettingsSpec())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserSettingsSpecSuccess', () => {\n    it('should properly set the state after fetch the data', () => {\n      // Arrange\n      const data = {\n        version: '1.0.0',\n        agreements: {\n          eula: {\n            defaultValue: false,\n            required: true,\n            editable: false,\n            since: '1.0.0',\n            title: 'Title',\n            label: '<a>Text</a>',\n          },\n        },\n      }\n      const state = {\n        ...initialState,\n        loading: false,\n        spec: data,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserSettingsSpecSuccess(data))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getUserSettingsSpecFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const error = 'some error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getUserSettingsSpecFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        user: { settings: nextState },\n      })\n      expect(userSettingsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('thunks', () => {\n    describe('fetchUserConfigSettings', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const data = {\n          agreements: {\n            eula: true,\n            analytics: false,\n          },\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserConfigSettings())\n\n        // Assert\n        const expectedActions = [\n          getUserConfigSettings(),\n          getUserConfigSettingsSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserConfigSettings())\n\n        // Assert\n        const expectedActions = [\n          getUserConfigSettings(),\n          getUserConfigSettingsFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('fetchUserSettingsSpec', () => {\n      it('succeed to fetch data', async () => {\n        // Arrange\n        const data = {\n          version: '1.0.0',\n          agreements: {\n            eula: {\n              defaultValue: false,\n              required: true,\n              editable: false,\n              since: '1.0.0',\n              title: 'Title',\n              label: '<a>Text</a>',\n            },\n          },\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserSettingsSpec())\n\n        // Assert\n        const expectedActions = [\n          getUserSettingsSpec(),\n          getUserSettingsSpecSuccess(responsePayload.data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to fetch data', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchUserSettingsSpec())\n\n        // Assert\n        const expectedActions = [\n          getUserSettingsSpec(),\n          getUserSettingsSpecFailure(errorMessage),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n\n    describe('updateUserConfigSettingsAction', () => {\n      it('succeed to update settings', async () => {\n        // Arrange\n        const data = { agreements: { eula: true } }\n        const responsePayload = { data, status: 200 }\n\n        apiService.patch = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          updateUserConfigSettingsAction({ agreements: { eula: true } }),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateUserConfigSettings(),\n          updateUserConfigSettingsSuccess(data),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n\n      it('failed to update settings', async () => {\n        const errorMessage = 'Something was wrong!'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.patch = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          updateUserConfigSettingsAction({ agreements: { eula: true } }),\n        )\n\n        // Assert\n        const expectedActions = [\n          updateUserConfigSettings(),\n          updateUserConfigSettingsFailure(errorMessage),\n          addErrorNotification(responsePayload as AxiosError),\n        ]\n\n        expect(store.getActions()).toEqual(expectedActions)\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport {\n  IBulkActionOverview,\n  IEnablementAreaItem,\n} from 'uiSrc/slices/interfaces'\nimport { MOCK_CUSTOM_TUTORIALS, MOCK_TUTORIALS_ITEMS } from 'uiSrc/constants'\nimport { apiService } from 'uiSrc/services'\n\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { getFileNameFromPath } from 'uiSrc/utils/pathUtil'\nimport reducer, {\n  initialState,\n  getWBCustomTutorials,\n  getWBCustomTutorialsSuccess,\n  getWBCustomTutorialsFailure,\n  uploadWbCustomTutorial,\n  uploadWBCustomTutorialSuccess,\n  uploadWBCustomTutorialFailure,\n  deleteWbCustomTutorial,\n  deleteWBCustomTutorialSuccess,\n  deleteWBCustomTutorialFailure,\n  uploadCustomTutorial,\n  fetchCustomTutorials,\n  deleteCustomTutorial,\n  workbenchCustomTutorialsSelector,\n  uploadDataBulk,\n  uploadDataBulkSuccess,\n  uploadDataBulkFailed,\n  uploadDataBulkAction,\n  defaultItems,\n  setWbCustomTutorialsState,\n} from '../../workbench/wb-custom-tutorials'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getWBCustomTutorials', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getWBCustomTutorials())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getWBCustomTutorialsSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const customTutorials: IEnablementAreaItem = MOCK_CUSTOM_TUTORIALS\n      const state = {\n        ...initialState,\n        items: [customTutorials],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getWBCustomTutorialsSuccess(customTutorials),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getWBCustomTutorialsFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        items: defaultItems,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        getWBCustomTutorialsFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('uploadWbCustomTutorial', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, uploadWbCustomTutorial())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('uploadWBCustomTutorialSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const items: IEnablementAreaItem[] = MOCK_TUTORIALS_ITEMS\n      const currentState = {\n        ...initialState,\n        items: defaultItems,\n      }\n      const state = {\n        ...initialState,\n        items: [\n          {\n            ...defaultItems[0],\n            children: [items[0], ...(defaultItems[0].children as [])],\n          },\n        ],\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        uploadWBCustomTutorialSuccess(items[0]),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('uploadWBCustomTutorialFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        uploadWBCustomTutorialFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteWbCustomTutorial', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        deleting: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, deleteWbCustomTutorial())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteWBCustomTutorialSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const id = 'tutorials'\n      const currentState = {\n        ...initialState,\n        items: [\n          {\n            ...defaultItems[0],\n            children: MOCK_TUTORIALS_ITEMS,\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        items: [\n          {\n            ...defaultItems[0],\n            children: currentState.items[0].children.slice(1),\n          },\n        ],\n      }\n\n      // Act\n      const nextState = reducer(currentState, deleteWBCustomTutorialSuccess(id))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('deleteWBCustomTutorialFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        deleting: false,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialState,\n        deleteWBCustomTutorialFailure(error),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('uploadDataBulk', () => {\n    it('should properly set loading for paths', () => {\n      // Arrange\n      const state = {\n        ...initialState,\n        bulkUpload: {\n          ...initialState.bulkUpload,\n          pathsInProgress: ['data/data'],\n        },\n      }\n\n      // Act\n      const nextState = reducer(initialState, uploadDataBulk('data/data'))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('uploadDataBulk', () => {\n    it('should properly remove path from loading', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        bulkUpload: {\n          ...initialState.bulkUpload,\n          pathsInProgress: ['data/data', 'data/another'],\n        },\n      }\n\n      const state = {\n        ...initialState,\n        bulkUpload: {\n          ...initialState.bulkUpload,\n          pathsInProgress: ['data/another'],\n        },\n      }\n\n      // Act\n      const nextState = reducer(\n        currentState,\n        uploadDataBulkSuccess('data/data'),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('uploadDataBulkFailed', () => {\n    it('should properly remove path from loading', () => {\n      // Arrange\n      const currentState = {\n        ...initialState,\n        bulkUpload: {\n          ...initialState.bulkUpload,\n          pathsInProgress: ['data/data'],\n        },\n      }\n\n      const state = {\n        ...initialState,\n        bulkUpload: {\n          ...initialState.bulkUpload,\n          pathsInProgress: [],\n        },\n      }\n\n      // Act\n      const nextState = reducer(currentState, uploadDataBulkFailed('data/data'))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          customTutorials: nextState,\n        },\n      })\n\n      expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n    })\n\n    describe('setWbCustomTutorialsState', () => {\n      it('should properly set open state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          items: [\n            {\n              ...defaultItems[0],\n              args: {\n                initialIsOpen: false,\n              },\n              children: MOCK_TUTORIALS_ITEMS,\n            },\n          ],\n        }\n\n        const state = {\n          ...initialState,\n          items: [\n            {\n              ...defaultItems[0],\n              args: {\n                defaultInitialIsOpen: false,\n                initialIsOpen: true,\n              },\n              children: MOCK_TUTORIALS_ITEMS,\n            },\n          ],\n        }\n\n        // Act\n        const nextState = reducer(currentState, setWbCustomTutorialsState(true))\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          workbench: {\n            customTutorials: nextState,\n          },\n        })\n\n        expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n      })\n\n      it('should properly return open state', () => {\n        // Arrange\n        const currentState = {\n          ...initialState,\n          items: [\n            {\n              ...defaultItems[0],\n              args: {\n                defaultInitialIsOpen: false,\n                initialIsOpen: true,\n              },\n              children: MOCK_TUTORIALS_ITEMS,\n            },\n          ],\n        }\n\n        const state = {\n          ...initialState,\n          items: [\n            {\n              ...defaultItems[0],\n              args: {\n                defaultInitialIsOpen: false,\n                initialIsOpen: false,\n              },\n              children: MOCK_TUTORIALS_ITEMS,\n            },\n          ],\n        }\n\n        // Act\n        const nextState = reducer(currentState, setWbCustomTutorialsState())\n\n        // Assert\n        const rootState = Object.assign(initialStateDefault, {\n          workbench: {\n            customTutorials: nextState,\n          },\n        })\n\n        expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state)\n      })\n    })\n  })\n\n  // thunks\n\n  describe('fetchCustomTutorials', () => {\n    it('succeed to fetch tutorials items', async () => {\n      // Arrange\n      const data = MOCK_CUSTOM_TUTORIALS\n      const responsePayload = { status: 200, data }\n\n      apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchCustomTutorials(jest.fn()))\n\n      // Assert\n      const expectedActions = [\n        getWBCustomTutorials(),\n        getWBCustomTutorialsSuccess(data),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch tutorials items', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchCustomTutorials())\n\n      // Assert\n      const expectedActions = [\n        getWBCustomTutorials(),\n        getWBCustomTutorialsFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('uploadCustomTutorial', () => {\n    it('succeed to upload tutorial', async () => {\n      // Arrange\n      const data = {}\n      const formData = new FormData()\n      formData.append('name', 'TutorialName')\n      formData.append('link', 'https://odkawokd.com')\n      const responsePayload = { status: 200, data }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(uploadCustomTutorial(formData, jest.fn()))\n\n      // Assert\n      const expectedActions = [\n        uploadWbCustomTutorial(),\n        uploadWBCustomTutorialSuccess(data),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch tutorials items', async () => {\n      // Arrange\n      const formData = new FormData()\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(uploadCustomTutorial(formData))\n\n      // Assert\n      const expectedActions = [\n        uploadWbCustomTutorial(),\n        uploadWBCustomTutorialFailure(errorMessage),\n        addErrorNotification(responsePayload as AxiosError),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('deleteCustomTutorial', () => {\n    it('succeed to delete tutorial', async () => {\n      // Arrange\n      const id = '213123-13123123-123'\n      const responsePayload = { status: 200 }\n\n      apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(deleteCustomTutorial(id))\n\n      // Assert\n      const expectedActions = [\n        deleteWbCustomTutorial(),\n        deleteWBCustomTutorialSuccess(id),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to delete tutorial', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(deleteCustomTutorial('1'))\n\n      // Assert\n      const expectedActions = [\n        deleteWbCustomTutorial(),\n        deleteWBCustomTutorialFailure(errorMessage),\n        addErrorNotification(responsePayload as AxiosError),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n\n  describe('uploadDataBulkAction', () => {\n    it('succeed to upload data', async () => {\n      // Arrange\n      const instanceId = '1'\n      const path = 'data/data'\n      const data = {}\n      const responsePayload = { status: 200, data }\n\n      apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(uploadDataBulkAction(instanceId, path))\n\n      // Assert\n      const expectedActions = [\n        uploadDataBulk(path),\n        uploadDataBulkSuccess(path),\n        addMessageNotification(\n          successMessages.UPLOAD_DATA_BULK(\n            data as IBulkActionOverview,\n            getFileNameFromPath(path),\n          ),\n        ),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to delete tutorial', async () => {\n      // Arrange\n      const instanceId = '1'\n      const path = 'data/data'\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      apiService.post = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(uploadDataBulkAction(instanceId, path))\n\n      // Assert\n      const expectedActions = [\n        uploadDataBulk(path),\n        uploadDataBulkFailed(path),\n        addErrorNotification(responsePayload as AxiosError),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\n\nimport {\n  cleanup,\n  mockedStore,\n  initialStateDefault,\n  clearStoreActions,\n  act,\n} from 'uiSrc/utils/test-utils'\nimport { apiService } from 'uiSrc/services'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  ClusterNodeRole,\n  CommandExecutionStatus,\n} from 'uiSrc/slices/interfaces/cli'\nimport { EMPTY_COMMAND } from 'uiSrc/constants'\nimport { CommandExecutionType, ResultsMode } from 'uiSrc/slices/interfaces'\nimport { setDbIndexState } from 'uiSrc/slices/app/context'\nimport { SendClusterCommandDto } from 'apiSrc/modules/cli/dto/cli.dto'\nimport reducer, {\n  initialState,\n  sendWBCommand,\n  sendWBCommandSuccess,\n  processWBCommandFailure,\n  processWBCommandsFailure,\n  workbenchResultsSelector,\n  sendWBCommandAction,\n  sendWBCommandClusterAction,\n  fetchWBCommandAction,\n  deleteWBCommandAction,\n  loadWBHistory,\n  loadWBHistorySuccess,\n  loadWBHistoryFailure,\n  processWBCommand,\n  fetchWBCommandSuccess,\n  toggleOpenWBResult,\n  deleteWBCommandSuccess,\n  resetWBHistoryItems,\n  fetchWBHistoryAction,\n  clearWbResultsAction,\n  clearWbResults,\n  clearWbResultsSuccess,\n  clearWbResultsFailed,\n  sendWbQueryAction,\n} from '../../workbench/wb-results'\n\njest.mock('uiSrc/services', () => ({\n  ...jest.requireActual('uiSrc/services'),\n}))\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\nconst mockItemId = '123'\nconst initialStateWithItems = {\n  ...initialState,\n  items: [\n    {\n      id: mockItemId + 0,\n    },\n  ],\n}\n\ndescribe('workbench results slice', () => {\n  describe('sendWBCommand', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const mockPayload = {\n        commands: ['command', 'command2'],\n        commandId: '123',\n        executionType: CommandExecutionType.Workbench,\n      }\n      const state = {\n        ...initialState,\n        loading: true,\n        processing: true,\n        items: mockPayload.commands.map((command, i) => ({\n          command,\n          id: mockPayload.commandId + i,\n          loading: true,\n          isOpen: true,\n          error: '',\n        })),\n      }\n\n      // Act\n      const nextState = reducer(initialState, sendWBCommand(mockPayload))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('toggleOpenWBResult', () => {\n    it('should properly set isOpen = true', () => {\n      // Arrange\n\n      const state = {\n        ...initialStateWithItems,\n        items: [\n          {\n            ...initialStateWithItems.items[0],\n            isOpen: true,\n          },\n        ],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        toggleOpenWBResult(mockItemId + 0),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('resetWBHistoryItems', () => {\n    it('should properly remove all items', () => {\n      // Arrange\n\n      const state = {\n        ...initialStateWithItems,\n        items: [],\n      }\n\n      // Act\n      const nextState = reducer(initialStateWithItems, resetWBHistoryItems())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('sendWBCommandSuccess', () => {\n    it('should properly set the state with fetched data', () => {\n      // Arrange\n\n      const mockedId = '123'\n\n      const mockCommandExecution = {\n        commandId: '123',\n        data: [\n          {\n            command: 'command',\n            databaseId: '123',\n            id: mockedId + 0,\n            createdAt: new Date(),\n            isOpen: true,\n            error: '',\n            loading: false,\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        items: [...mockCommandExecution.data],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        sendWBCommandSuccess(mockCommandExecution),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n\n    it('should properly set the state with fetched data and isOpen = false, for request silent mode', () => {\n      // Arrange\n\n      const mockedId = '123'\n\n      const mockCommandExecution = {\n        commandId: '123',\n        data: [\n          {\n            command: 'command',\n            databaseId: '123',\n            id: mockedId + 0,\n            createdAt: new Date(),\n            resultsMode: ResultsMode.Silent,\n            isOpen: false,\n            error: '',\n            loading: false,\n            summary: {\n              fail: 0,\n              success: 1,\n              total: 1,\n            },\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n        ],\n      }\n\n      const state = {\n        ...initialState,\n        items: [...mockCommandExecution.data],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        sendWBCommandSuccess(mockCommandExecution),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('processWBCommandFailure', () => {\n    it('should properly set the error', () => {\n      // Arrange\n      const data = 'error'\n      const mockCommandExecution = {\n        id: mockItemId + 0,\n        error: data,\n        loading: false,\n      }\n      const state = {\n        ...initialStateWithItems,\n        items: [\n          {\n            ...mockCommandExecution,\n          },\n        ],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        processWBCommandFailure(mockCommandExecution),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('processWBCommandsFailure', () => {\n    it('should properly remove from items', () => {\n      // Arrange\n      const data = 'error'\n      const mockCommandExecution = {\n        commandsId: [mockItemId + 0],\n        error: data,\n      }\n      const state = {\n        ...initialStateWithItems,\n        items: [\n          {\n            id: mockItemId + 0,\n            loading: false,\n            isOpen: true,\n            error: '',\n            result: [\n              {\n                response: data,\n                status: 'fail',\n              },\n            ],\n          },\n        ],\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        processWBCommandsFailure(mockCommandExecution),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('loadWBHistorySuccess', () => {\n    it('should properly set history items', () => {\n      // Arrange\n      const mockCommandExecution = [\n        {\n          mode: null,\n          id: 'e3553f5a-0fdf-4282-8406-8b377c2060d2',\n          databaseId: '3f795233-e26a-463b-a116-58cf620b18f2',\n          command: 'get test',\n          role: null,\n          nodeOptions: null,\n          createdAt: '2022-06-10T15:47:13.000Z',\n          emptyCommand: false,\n        },\n      ]\n      const state = {\n        ...initialStateWithItems,\n        items: mockCommandExecution,\n        isLoaded: true,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        loadWBHistorySuccess(mockCommandExecution),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n\n    it(`if command=null should properly set history items with command=${EMPTY_COMMAND}`, () => {\n      // Arrange\n      const mockCommandExecution = [\n        {\n          mode: null,\n          id: 'e3553f5a-0fdf-4282-8406-8b377c2060d2',\n          databaseId: '3f795233-e26a-463b-a116-58cf620b18f2',\n          command: null,\n          role: null,\n          nodeOptions: null,\n          createdAt: '2022-06-10T15:47:13.000Z',\n        },\n      ]\n\n      const state = {\n        ...initialStateWithItems,\n        items: [\n          {\n            mode: null,\n            id: 'e3553f5a-0fdf-4282-8406-8b377c2060d2',\n            databaseId: '3f795233-e26a-463b-a116-58cf620b18f2',\n            command: EMPTY_COMMAND,\n            role: null,\n            nodeOptions: null,\n            createdAt: '2022-06-10T15:47:13.000Z',\n            emptyCommand: true,\n          },\n        ],\n        isLoaded: true,\n      }\n\n      // Act\n      const nextState = reducer(\n        initialStateWithItems,\n        loadWBHistorySuccess(mockCommandExecution),\n      )\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('clearWbResults', () => {\n    it('should properly set state', () => {\n      // Arrange\n\n      const state = {\n        ...initialState,\n        clearing: true,\n      }\n\n      // Act\n      const nextState = reducer(initialState, clearWbResults())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('clearWbResultsSuccess', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const currentState = {\n        ...initialStateWithItems,\n        clearing: true,\n      }\n\n      const state = {\n        ...initialState,\n        clearing: false,\n      }\n\n      // Act\n      const nextState = reducer(currentState, clearWbResultsSuccess())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('clearWbResultsFailed', () => {\n    it('should properly set state', () => {\n      // Arrange\n      const currentState = {\n        ...initialStateWithItems,\n        clearing: true,\n      }\n\n      const state = {\n        ...initialStateWithItems,\n        clearing: false,\n      }\n\n      // Act\n      const nextState = reducer(currentState, clearWbResultsFailed())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          results: nextState,\n        },\n      })\n      expect(workbenchResultsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('thunks', () => {\n    describe('Standalone Cli commands', () => {\n      it('call both sendWBCommandAction and sendWBCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const commands = ['keys *', 'set 1 1']\n        const commandId = `${Date.now()}`\n        const data = [\n          {\n            command: 'keys *',\n            databaseId: '123',\n            id: commandId + (commands.length - 1),\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n          {\n            command: 'set 1 1',\n            databaseId: '123',\n            id: commandId + (commands.length - 1),\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendWBCommandAction({ commands, commandId }))\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          sendWBCommandSuccess({ data, commandId }),\n          setDbIndexState(false),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendWBCommandAction and sendWBCommandSuccess when response status is fail', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const commandId = `${Date.now()}`\n        const data = [\n          {\n            command: 'command',\n            databaseId: '123',\n            id: commandId,\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Fail,\n              },\n            ],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendWBCommandAction({ commands, commandId }))\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          sendWBCommandSuccess({ data, commandId }),\n          setDbIndexState(false),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendWBCommandAction and processWBCommandsFailure when fetch is fail', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const commandId = `${Date.now()}`\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(sendWBCommandAction({ commands, commandId }))\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          addErrorNotification(responsePayload as AxiosError),\n          processWBCommandsFailure({\n            commandsId: commands.map((_, i) => commandId + i),\n            error: responsePayload.response.data.message,\n          }),\n          setDbIndexState(false),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('Single Node Cluster Cli command', () => {\n      const commandId = `${Date.now()}`\n      const options: SendClusterCommandDto = {\n        command: 'keys *',\n        nodeOptions: {\n          host: 'localhost',\n          port: 7000,\n          enableRedirection: true,\n        },\n        role: ClusterNodeRole.All,\n      }\n\n      it('call both sendWBCommandClusterAction and sendWBCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const data = [\n          {\n            command: 'command',\n            databaseId: '123',\n            id: commandId + (commands.length - 1),\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          sendWBCommandClusterAction({ commands, commandId, options }),\n        )\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          sendWBCommandSuccess({ data, commandId }),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendWBCommandClusterAction and sendWBCommandSuccess when response status is fail', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const data = [\n          {\n            command: 'command',\n            databaseId: '123',\n            id: commandId,\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Fail,\n              },\n            ],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          sendWBCommandClusterAction({ commands, options, commandId }),\n        )\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          sendWBCommandSuccess({ data, commandId }),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both sendWBCommandClusterAction and processWBCommandFailure when fetch is fail', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(\n          sendWBCommandAction({ commands, options, commandId }),\n        )\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          addErrorNotification(responsePayload as AxiosError),\n          processWBCommandsFailure({\n            commandsId: commands.map((_, i) => commandId + i),\n            error: responsePayload.response.data.message,\n          }),\n          setDbIndexState(false),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('Fetch result for command', () => {\n      it('call both fetchWBCommandAction and fetchWBCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const data = {\n          command: 'command',\n          databaseId: '123',\n          id: mockItemId,\n          createdAt: new Date(),\n          result: [\n            {\n              response: 'test',\n              status: CommandExecutionStatus.Success,\n            },\n          ],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchWBCommandAction(mockItemId))\n\n        // Assert\n        const expectedActions = [\n          processWBCommand(mockItemId),\n          fetchWBCommandSuccess(data),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both fetchWBCommandAction and fetchWBCommandSuccess when response status is fail', async () => {\n        // Arrange\n        const commandId = `${Date.now()}`\n        const data = {\n          command: 'command',\n          databaseId: '123',\n          id: commandId,\n          createdAt: new Date(),\n          result: [\n            {\n              response: 'test',\n              status: CommandExecutionStatus.Fail,\n            },\n          ],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchWBCommandAction(commandId))\n\n        // Assert\n        const expectedActions = [\n          processWBCommand(commandId),\n          fetchWBCommandSuccess(data),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both fetchWBCommandAction and processWBCommandFailure when fetch is fail', async () => {\n        // Arrange\n        const command = 'keys *'\n        const commandId = `${Date.now()}`\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchWBCommandAction(commandId))\n\n        // Assert\n        const expectedActions = [\n          processWBCommand(commandId),\n          addErrorNotification(responsePayload as AxiosError),\n          processWBCommandFailure({\n            command,\n            error: responsePayload.response.data.message,\n          }),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('Delete command from the list', () => {\n      it('call both deleteWBCommandAction and fetchWBCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const data = {\n          command: 'command',\n          databaseId: '123',\n          id: mockItemId,\n          createdAt: new Date(),\n          result: [\n            {\n              response: 'test',\n              status: CommandExecutionStatus.Success,\n            },\n          ],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteWBCommandAction(mockItemId))\n\n        // Assert\n        const expectedActions = [\n          processWBCommand(mockItemId),\n          deleteWBCommandSuccess(mockItemId),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both deleteWBCommandAction and fetchWBCommandSuccess when response status is fail', async () => {\n        // Arrange\n        const commandId = `${Date.now()}`\n        const data = {\n          command: 'command',\n          databaseId: '123',\n          id: commandId,\n          createdAt: new Date(),\n          result: [\n            {\n              response: 'test',\n              status: CommandExecutionStatus.Fail,\n            },\n          ],\n        }\n        const responsePayload = { data, status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteWBCommandAction(commandId))\n\n        // Assert\n        const expectedActions = [\n          processWBCommand(commandId),\n          deleteWBCommandSuccess(commandId),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both deleteWBCommandAction and processWBCommandFailure when fetch is fail', async () => {\n        // Arrange\n        const command = 'keys *'\n        const commandId = `${Date.now()}`\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(deleteWBCommandAction(commandId))\n\n        // Assert\n        const expectedActions = [\n          processWBCommand(commandId),\n          addErrorNotification(responsePayload as AxiosError),\n          processWBCommandFailure({\n            command,\n            error: responsePayload.response.data.message,\n          }),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('clearWbResultsAction', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const responsePayload = { status: 200 }\n\n        apiService.delete = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(clearWbResultsAction())\n\n        // Assert\n        const expectedActions = [clearWbResults(), clearWbResultsSuccess()]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('should call proper actions on fail', async () => {\n        // Arrange\n        const errorMessage = 'Some error'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.delete = jest.fn().mockRejectedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(clearWbResultsAction())\n\n        // Assert\n        const expectedActions = [\n          clearWbResults(),\n          addErrorNotification(responsePayload as AxiosError),\n          clearWbResultsFailed(),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('Fetch list of commands', () => {\n      it('call both fetchWBHistoryAction and fetchWBCommandSuccess when response status is successed', async () => {\n        // Arrange\n        const data = [\n          {\n            command: 'command1',\n            id: '1',\n            databaseId: '1',\n            createdAt: new Date(),\n            result: [],\n          },\n          {\n            id: '2',\n            command: 'command2',\n            databaseId: '1',\n            createdAt: new Date(),\n            result: [],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.get = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchWBHistoryAction(mockItemId))\n\n        // Assert\n        const expectedActions = [loadWBHistory(), loadWBHistorySuccess(data)]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('call both fetchWBHistoryAction and processWBCommandFailure when fetch is fail', async () => {\n        // Arrange\n        const commandId = `${Date.now()}`\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.get = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await store.dispatch<any>(fetchWBHistoryAction(commandId))\n\n        // Assert\n        const expectedActions = [\n          loadWBHistory(),\n          addErrorNotification(responsePayload as AxiosError),\n          loadWBHistoryFailure(responsePayload.response.data.message),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n\n    describe('sendWbQueryAction', () => {\n      it('should call proper actions on success', async () => {\n        // Arrange\n        const queryInit = `\n        [auto=true]\n        keys * // comment\n        set 1 1\n        `\n        const commands = ['keys *', 'set 1 1']\n        const commandId = `${Date.now()}`\n        const data = [\n          {\n            command: 'keys *',\n            databaseId: '123',\n            id: commandId + (commands.length - 1),\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n          {\n            command: 'set 1 1',\n            databaseId: '123',\n            id: commandId + (commands.length - 1),\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Success,\n              },\n            ],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await act(() => {\n          store.dispatch<any>(sendWbQueryAction(queryInit, null))\n        })\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          sendWBCommandSuccess({ data, commandId }),\n          setDbIndexState(false),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('should call proper actions on fail', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const commandId = `${Date.now()}`\n        const data = [\n          {\n            command: 'command',\n            databaseId: '123',\n            id: commandId,\n            createdAt: new Date(),\n            result: [\n              {\n                response: 'test',\n                status: CommandExecutionStatus.Fail,\n              },\n            ],\n          },\n        ]\n        const responsePayload = { data, status: 200 }\n\n        apiService.post = jest.fn().mockResolvedValue(responsePayload)\n\n        // Act\n        await act(() => {\n          store.dispatch<any>(sendWbQueryAction(commands[0]))\n        })\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          sendWBCommandSuccess({ data, commandId }),\n          setDbIndexState(false),\n        ]\n\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n\n      it('should call proper actions on fetch fail', async () => {\n        // Arrange\n        const commands = ['keys *']\n        const commandId = `${Date.now()}`\n        const errorMessage =\n          'Could not connect to aoeu:123, please check the connection details.'\n        const responsePayload = {\n          response: {\n            status: 500,\n            data: { message: errorMessage },\n          },\n        }\n\n        apiService.post = jest.fn().mockRejectedValueOnce(responsePayload)\n\n        // Act\n        await act(() => {\n          store.dispatch<any>(sendWbQueryAction(commands[0]))\n        })\n\n        // Assert\n        const expectedActions = [\n          sendWBCommand({ commands, commandId }),\n          setDbIndexState(true),\n          addErrorNotification(responsePayload as AxiosError),\n          processWBCommandsFailure({\n            commandsId: commands.map((_, i) => commandId + i),\n            error: responsePayload.response.data.message,\n          }),\n          setDbIndexState(false),\n        ]\n        expect(clearStoreActions(store.getActions())).toEqual(\n          clearStoreActions(expectedActions),\n        )\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/tests/workbench/wb-tutorials.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport {\n  cleanup,\n  initialStateDefault,\n  mockedStore,\n} from 'uiSrc/utils/test-utils'\nimport { IEnablementAreaItem } from 'uiSrc/slices/interfaces'\nimport { MOCK_TUTORIALS } from 'uiSrc/constants'\nimport { resourcesService } from 'uiSrc/services'\n\nimport reducer, {\n  initialState,\n  workbenchTutorialsSelector,\n  getWBTutorials,\n  getWBTutorialsFailure,\n  getWBTutorialsSuccess,\n  fetchTutorials,\n  defaultItems,\n} from '../../workbench/wb-tutorials'\n\nlet store: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  store = cloneDeep(mockedStore)\n  store.clearActions()\n})\n\ndescribe('slices', () => {\n  describe('reducer, actions and selectors', () => {\n    it('should return the initial state on first run', () => {\n      // Arrange\n      const nextState = initialState\n\n      // Act\n      const result = reducer(undefined, {})\n\n      // Assert\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('getWBTutorials', () => {\n    it('should properly set loading', () => {\n      // Arrange\n      const loading = true\n      const state = {\n        ...initialState,\n        loading,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getWBTutorials())\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          tutorials: nextState,\n        },\n      })\n\n      expect(workbenchTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getWBTutorialsSuccess', () => {\n    it('should properly set state after success', () => {\n      // Arrange\n      const tutorials: IEnablementAreaItem = MOCK_TUTORIALS\n      const state = {\n        ...initialState,\n        items: [tutorials],\n      }\n\n      // Act\n      const nextState = reducer(initialState, getWBTutorialsSuccess(tutorials))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          tutorials: nextState,\n        },\n      })\n\n      expect(workbenchTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  describe('getWBTutorialsFailure', () => {\n    it('should properly set error', () => {\n      // Arrange\n      const error = 'error'\n      const state = {\n        ...initialState,\n        loading: false,\n        items: defaultItems,\n        error,\n      }\n\n      // Act\n      const nextState = reducer(initialState, getWBTutorialsFailure(error))\n\n      // Assert\n      const rootState = Object.assign(initialStateDefault, {\n        workbench: {\n          tutorials: nextState,\n        },\n      })\n\n      expect(workbenchTutorialsSelector(rootState)).toEqual(state)\n    })\n  })\n\n  // thunks\n\n  describe('fetchTutorials', () => {\n    it('succeed to fetch tutorials items', async () => {\n      // Arrange\n      const data = MOCK_TUTORIALS\n      const responsePayload = { status: 200, data }\n\n      resourcesService.get = jest.fn().mockResolvedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchTutorials(jest.fn()))\n\n      // Assert\n      const expectedActions = [getWBTutorials(), getWBTutorialsSuccess(data)]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n\n    it('failed to fetch tutorials items', async () => {\n      // Arrange\n      const errorMessage = 'Something was wrong!'\n      const responsePayload = {\n        response: {\n          status: 500,\n          data: { message: errorMessage },\n        },\n      }\n      resourcesService.get = jest.fn().mockRejectedValue(responsePayload)\n\n      // Act\n      await store.dispatch<any>(fetchTutorials())\n\n      // Assert\n      const expectedActions = [\n        getWBTutorials(),\n        getWBTutorialsFailure(errorMessage),\n      ]\n\n      expect(mockedStore.getActions()).toEqual(expectedActions)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/slices/user/cloud-user-profile.ts",
    "content": "import { AxiosError } from 'axios'\nimport { createSlice } from '@reduxjs/toolkit'\nimport { apiService } from 'uiSrc/services'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { CloudUser } from 'apiSrc/modules/cloud/user/models'\n\nimport { AppDispatch, RootState } from '../store'\nimport { StateUserProfile } from '../interfaces/user'\n\nexport const initialState: StateUserProfile = {\n  loading: false,\n  error: '',\n  data: undefined,\n}\n\nconst cloudUserProfileSlice = createSlice({\n  name: 'cloudUserProfile',\n  initialState,\n  reducers: {\n    setUserProfileInitialState: () => initialState,\n    getUserProfile: (state) => {\n      state.loading = true\n    },\n    getUserProfileSuccess: (state, { payload }) => {\n      state.loading = false\n      state.data = payload\n    },\n    getUserProfileFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setUserProfileInitialState,\n  getUserProfile,\n  getUserProfileSuccess,\n  getUserProfileFailure,\n} = cloudUserProfileSlice.actions\n\n// A selector\nexport const cloudUserProfileSelector = (state: RootState) =>\n  state.user.cloudProfile\n\n// The reducer\nexport default cloudUserProfileSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchCloudUserProfile(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getUserProfile())\n\n    try {\n      const { data, status } = await apiService.get<CloudUser>(\n        ApiEndpoints.CLOUD_ME,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUserProfileSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(getUserProfileFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/user/user-settings.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { apiService, localStorageService } from 'uiSrc/services'\nimport { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport {\n  GetAgreementsSpecResponse,\n  GetAppSettingsResponse,\n  UpdateSettingsDto,\n} from 'apiSrc/modules/settings/dto/settings.dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport { StateUserSettings } from '../interfaces/user'\n\nexport const initialState: StateUserSettings = {\n  loading: false,\n  error: '',\n  isShowConceptsPopup: null,\n  config: null,\n  spec: null,\n  workbench: {\n    cleanup: localStorageService?.get(BrowserStorageItem.wbCleanUp) ?? true,\n  },\n}\n\n// A slice for recipes\nconst userSettingsSlice = createSlice({\n  name: 'userSettings',\n  initialState,\n  reducers: {\n    setUserSettingsInitialState: () => initialState,\n    setSettingsPopupState: (state, { payload }) => {\n      state.isShowConceptsPopup = payload\n    },\n    getUserConfigSettings: (state) => {\n      state.loading = true\n    },\n    getUserConfigSettingsSuccess: (state, { payload }) => {\n      state.loading = false\n      state.config = payload\n    },\n    getUserConfigSettingsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    updateUserConfigSettings: (state) => {\n      state.loading = true\n    },\n    updateUserConfigSettingsSuccess: (state, { payload }) => {\n      state.loading = false\n      state.config = payload\n      state.isShowConceptsPopup = false\n    },\n    updateUserConfigSettingsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    getUserSettingsSpec: (state) => {\n      state.loading = true\n    },\n    getUserSettingsSpecSuccess: (state, { payload }) => {\n      state.loading = false\n      state.spec = payload\n    },\n    getUserSettingsSpecFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    setWorkbenchCleanUp: (state, { payload }) => {\n      localStorageService.set(BrowserStorageItem.wbCleanUp, payload)\n      state.workbench.cleanup = payload\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setUserSettingsInitialState,\n  setSettingsPopupState,\n  getUserConfigSettings,\n  getUserConfigSettingsSuccess,\n  getUserConfigSettingsFailure,\n  updateUserConfigSettings,\n  updateUserConfigSettingsSuccess,\n  updateUserConfigSettingsFailure,\n  getUserSettingsSpec,\n  getUserSettingsSpecSuccess,\n  getUserSettingsSpecFailure,\n  setWorkbenchCleanUp,\n} = userSettingsSlice.actions\n\n// A selector\nexport const userSettingsSelector = (state: RootState) => state.user.settings\nexport const userSettingsConfigSelector = (state: RootState) =>\n  state.user.settings.config\nexport const userSettingsWBSelector = (state: RootState) =>\n  state.user.settings.workbench\n\n// The reducer\nexport default userSettingsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchAppInfo(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getUserConfigSettings())\n\n    try {\n      const { data, status } = await apiService.get<GetAppSettingsResponse>(\n        ApiEndpoints.INFO,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUserConfigSettingsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as unknown as AxiosError)\n      dispatch(getUserConfigSettingsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchUserConfigSettings(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getUserConfigSettings())\n\n    try {\n      const { data, status } = await apiService.get<GetAppSettingsResponse>(\n        ApiEndpoints.SETTINGS,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUserConfigSettingsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as unknown as AxiosError)\n      dispatch(getUserConfigSettingsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchUserSettingsSpec(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getUserSettingsSpec())\n\n    try {\n      const { data, status } = await apiService.get<GetAgreementsSpecResponse>(\n        ApiEndpoints.SETTINGS_AGREEMENTS_SPEC,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(getUserSettingsSpecSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as unknown as AxiosError)\n      dispatch(getUserSettingsSpecFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function updateUserConfigSettingsAction(\n  settings: any,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(updateUserConfigSettings())\n\n    try {\n      const { status, data } = await apiService.patch<UpdateSettingsDto>(\n        ApiEndpoints.SETTINGS,\n        settings,\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(updateUserConfigSettingsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as unknown as AxiosError)\n      dispatch(updateUserConfigSettingsFailure(errorMessage))\n      dispatch(addErrorNotification(error as unknown as AxiosError))\n      onFailAction?.()\n    }\n  }\n}\n\ntype ToggleAnalyticsReasonType =\n  | 'none'\n  | 'oauth-agreement'\n  | 'google'\n  | 'github'\n  | 'sso'\n  | 'user'\n\nexport function enableUserAnalyticsAction(\n  reason: ToggleAnalyticsReasonType = 'none',\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n    const agreements = state?.user?.settings?.config?.agreements\n\n    if (agreements && !agreements.analytics) {\n      dispatch(\n        updateUserConfigSettingsAction({\n          agreements: { ...agreements, analytics: true },\n          analyticsReason: reason,\n        }),\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { isUndefined, remove } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport {\n  getApiErrorMessage,\n  getUrl,\n  isStatusSuccessful,\n  Maybe,\n} from 'uiSrc/utils'\nimport { apiService } from 'uiSrc/services'\nimport {\n  EnablementAreaComponent,\n  IBulkActionOverview,\n  IEnablementAreaItem,\n  StateWorkbenchCustomTutorials,\n} from 'uiSrc/slices/interfaces'\n\nimport {\n  addErrorNotification,\n  addMessageNotification,\n} from 'uiSrc/slices/app/notifications'\nimport successMessages from 'uiSrc/components/notifications/success-messages'\nimport { getFileNameFromPath } from 'uiSrc/utils/pathUtil'\nimport { AppDispatch, RootState } from '../store'\n\nexport const defaultItems: IEnablementAreaItem[] = [\n  {\n    id: 'custom-tutorials',\n    label: 'MY TUTORIALS',\n    type: EnablementAreaComponent.Group,\n    children: [],\n  },\n]\n\nexport const initialState: StateWorkbenchCustomTutorials = {\n  loading: false,\n  deleting: false,\n  error: '',\n  items: defaultItems,\n  bulkUpload: {\n    pathsInProgress: [],\n  },\n}\n\n// A slice for recipes\nconst workbenchCustomTutorialsSlice = createSlice({\n  name: 'workbenchCustomTutorials',\n  initialState,\n  reducers: {\n    getWBCustomTutorials: (state) => {\n      state.loading = true\n    },\n    getWBCustomTutorialsSuccess: (\n      state,\n      { payload }: { payload: IEnablementAreaItem },\n    ) => {\n      state.loading = false\n      state.items = [payload]\n    },\n    getWBCustomTutorialsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n      state.items = defaultItems\n    },\n    uploadWbCustomTutorial: (state) => {\n      state.loading = true\n    },\n    uploadWBCustomTutorialSuccess: (state, { payload }) => {\n      state.loading = false\n      if (state.items[0]?.children) {\n        state.items[0].children.unshift(payload)\n      }\n    },\n    uploadWBCustomTutorialFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n    },\n    deleteWbCustomTutorial: (state) => {\n      state.deleting = true\n    },\n    deleteWBCustomTutorialSuccess: (state, { payload }) => {\n      state.deleting = false\n      if (state.items[0].children) {\n        remove(state.items[0].children, (item) => item.id === payload)\n      }\n    },\n    deleteWBCustomTutorialFailure: (state, { payload }) => {\n      state.deleting = false\n      state.error = payload\n    },\n    uploadDataBulk: (state, { payload }) => {\n      state.bulkUpload.pathsInProgress.push(payload)\n    },\n    uploadDataBulkSuccess: (state, { payload }) => {\n      remove(state.bulkUpload.pathsInProgress, (p) => p === payload)\n    },\n    uploadDataBulkFailed: (state, { payload }) => {\n      remove(state.bulkUpload.pathsInProgress, (p) => p === payload)\n    },\n    setWbCustomTutorialsState: (\n      state,\n      { payload }: PayloadAction<Maybe<boolean>>,\n    ) => {\n      if (state.items[0].args) {\n        const { defaultInitialIsOpen, initialIsOpen } = state.items[0].args\n        if (isUndefined(payload)) {\n          state.items[0].args.initialIsOpen =\n            defaultInitialIsOpen ?? initialIsOpen\n          return\n        }\n\n        state.items[0].args.defaultInitialIsOpen = initialIsOpen\n        state.items[0].args.initialIsOpen = payload\n      }\n    },\n  },\n})\n\n// A selector\nexport const workbenchCustomTutorialsSelector = (state: RootState) =>\n  state.workbench.customTutorials\nexport const customTutorialsBulkUploadSelector = (state: RootState) =>\n  state.workbench.customTutorials.bulkUpload\n\n// Actions generated from the slice\nexport const {\n  getWBCustomTutorials,\n  getWBCustomTutorialsSuccess,\n  getWBCustomTutorialsFailure,\n  uploadWbCustomTutorial,\n  uploadWBCustomTutorialSuccess,\n  uploadWBCustomTutorialFailure,\n  deleteWbCustomTutorial,\n  deleteWBCustomTutorialSuccess,\n  deleteWBCustomTutorialFailure,\n  uploadDataBulk,\n  uploadDataBulkSuccess,\n  uploadDataBulkFailed,\n  setWbCustomTutorialsState,\n} = workbenchCustomTutorialsSlice.actions\n\n// The reducer\nexport default workbenchCustomTutorialsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchCustomTutorials(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getWBCustomTutorials())\n\n    try {\n      const { data, status } = await apiService.get(\n        ApiEndpoints.CUSTOM_TUTORIALS_MANIFEST,\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(getWBCustomTutorialsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(getWBCustomTutorialsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function uploadCustomTutorial(\n  formData: FormData,\n  onSuccessAction?: () => void,\n  onFailAction?: (error?: string) => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(uploadWbCustomTutorial())\n    try {\n      const { status, data } = await apiService.post(\n        ApiEndpoints.CUSTOM_TUTORIALS,\n        formData,\n        {\n          headers: {\n            Accept: 'application/json',\n            'Content-Type': 'multipart/form-data',\n          },\n        },\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(uploadWBCustomTutorialSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (_error) {\n      const error = _error as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(uploadWBCustomTutorialFailure(errorMessage))\n      dispatch(addErrorNotification(error))\n      onFailAction?.(error?.response?.data?.error)\n    }\n  }\n}\n\nexport function deleteCustomTutorial(\n  id: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(deleteWbCustomTutorial())\n    try {\n      const { status } = await apiService.delete(\n        `${ApiEndpoints.CUSTOM_TUTORIALS}/${id}`,\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteWBCustomTutorialSuccess(id))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error as AxiosError)\n      dispatch(deleteWBCustomTutorialFailure(errorMessage))\n      dispatch(addErrorNotification(error as AxiosError))\n      onFailAction?.()\n    }\n  }\n}\n\nexport function uploadDataBulkAction(\n  instanceId: string,\n  path: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(uploadDataBulk(path))\n    try {\n      const { status, data } = await apiService.post(\n        getUrl(instanceId, ApiEndpoints.BULK_ACTIONS_IMPORT_TUTORIAL_DATA),\n        { path },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(uploadDataBulkSuccess(path))\n        dispatch(\n          addMessageNotification(\n            successMessages.UPLOAD_DATA_BULK(\n              data as IBulkActionOverview,\n              getFileNameFromPath(path),\n            ),\n          ),\n        )\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      dispatch(uploadDataBulkFailed(path))\n      dispatch(addErrorNotification(error as AxiosError))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/workbench/wb-results.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { AxiosError } from 'axios'\nimport { chunk, reverse } from 'lodash'\nimport { apiService, localStorageService } from 'uiSrc/services'\nimport {\n  ApiEndpoints,\n  BrowserStorageItem,\n  CodeButtonParams,\n} from 'uiSrc/constants'\nimport { addErrorNotification } from 'uiSrc/slices/app/notifications'\nimport { CliOutputFormatterType } from 'uiSrc/constants/cliOutput'\nimport {\n  RunQueryMode,\n  ResultsMode,\n  CommandExecutionType,\n} from 'uiSrc/slices/interfaces/workbench'\nimport {\n  getApiErrorMessage,\n  getCommandsForExecution,\n  getExecuteParams,\n  getMultiCommands,\n  getUrl,\n  isGroupResults,\n  isSilentMode,\n  isStatusSuccessful,\n  Nullable,\n} from 'uiSrc/utils'\nimport { WORKBENCH_HISTORY_MAX_LENGTH } from 'uiSrc/pages/workbench/constants'\nimport {\n  ClusterNodeRole,\n  CommandExecutionStatus,\n} from 'uiSrc/slices/interfaces/cli'\nimport { setDbIndexState } from 'uiSrc/slices/app/context'\nimport { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api'\nimport {\n  addCommands,\n  clearCommands,\n  findCommand,\n  getLocalWbHistory,\n  removeCommand,\n  wbHistoryStorage,\n} from 'uiSrc/services/workbenchStorage'\nimport { CreateCommandExecutionsDto } from 'apiSrc/modules/workbench/dto/create-command-executions.dto'\n\nimport { AppDispatch, RootState } from '../store'\nimport {\n  CommandExecution,\n  ConnectionType,\n  StateWorkbenchResults,\n} from '../interfaces'\nimport { mapCommandExecutionToUI } from 'uiSrc/services/commands-history/utils/command-execution.mapper'\n\nexport const initialState: StateWorkbenchResults = {\n  isLoaded: false,\n  loading: false,\n  processing: false,\n  clearing: false,\n  error: '',\n  items: [],\n  resultsMode:\n    localStorageService?.get(BrowserStorageItem.wbGroupMode) ??\n    ResultsMode.Default,\n  activeRunQueryMode:\n    localStorageService?.get(BrowserStorageItem.RunQueryMode) ??\n    RunQueryMode.ASCII,\n}\n\n// A slice for recipes\nconst workbenchResultsSlice = createSlice({\n  name: 'workbenchResults',\n  initialState,\n  reducers: {\n    setWBResultsInitialState: () => initialState,\n\n    // Fetch Workbench history\n    loadWBHistory: (state) => {\n      state.loading = true\n    },\n\n    loadWBHistorySuccess: (\n      state,\n      { payload }: { payload: CommandExecution[] },\n    ) => {\n      state.items = payload.map(mapCommandExecutionToUI)\n      state.loading = false\n      state.isLoaded = true\n    },\n\n    loadWBHistoryFailure: (state, { payload }) => {\n      state.error = payload\n      state.loading = false\n      state.isLoaded = true\n    },\n\n    // Process Workbench command to API\n    processWBCommand: (state, { payload = '' }: { payload: string }) => {\n      if (!payload) return\n\n      state.items = [...state.items].map((item) => {\n        if (item.id === payload) {\n          return { ...item, loading: true }\n        }\n        return item\n      })\n    },\n\n    processWBCommandsFailure: (\n      state,\n      { payload }: { payload: { commandsId: string[]; error: string } },\n    ) => {\n      state.items = [...state.items].map((item) => {\n        let newItem = item\n        payload.commandsId.forEach(() => {\n          if (payload.commandsId.indexOf(item?.id as string) !== -1) {\n            newItem = {\n              ...item,\n              result: [\n                {\n                  response: payload.error,\n                  status: CommandExecutionStatus.Fail,\n                },\n              ],\n              loading: false,\n              isOpen: true,\n              error: '',\n            }\n          }\n        })\n        return newItem\n      })\n      state.loading = false\n      state.processing = false\n    },\n\n    processWBCommandFailure: (\n      state,\n      { payload }: { payload: { id: string; error: string } },\n    ) => {\n      state.items = [...state.items].map((item) => {\n        if (item.id === payload.id) {\n          return { ...item, loading: false, error: payload?.error }\n        }\n        return item\n      })\n      state.loading = false\n      state.processing = false\n    },\n\n    sendWBCommand: (\n      state,\n      {\n        payload: { commands, commandId },\n      }: { payload: { commands: string[]; commandId: string } },\n    ) => {\n      let newItems = [\n        ...commands.map((command, i) => ({\n          command,\n          id: commandId + i,\n          loading: true,\n          isOpen: true,\n          error: '',\n        })),\n        ...state.items,\n      ]\n\n      if (newItems?.length > WORKBENCH_HISTORY_MAX_LENGTH) {\n        newItems = newItems.slice(0, WORKBENCH_HISTORY_MAX_LENGTH)\n      }\n\n      state.items = newItems\n      state.loading = true\n      state.processing = true\n    },\n\n    sendWBCommandSuccess: (\n      state,\n      {\n        payload: { data, commandId, processing },\n      }: {\n        payload: {\n          data: CommandExecution[]\n          commandId: string\n          processing?: boolean\n        }\n      },\n    ) => {\n      state.items = [...state.items].map((item) => {\n        let newItem = item\n        data.forEach((command, i) => {\n          if (item.id === commandId + i) {\n            // don't open a card if silent mode and no errors\n            newItem = {\n              ...command,\n              loading: false,\n              error: '',\n              isOpen: !isSilentMode(command.resultsMode),\n            }\n          }\n        })\n        return newItem\n      })\n\n      state.loading = false\n      state.processing = (state.processing && processing) || false\n    },\n\n    fetchWBCommandSuccess: (\n      state,\n      { payload }: { payload: CommandExecution },\n    ) => {\n      state.items = [...state.items].map((item) => {\n        if (item.id === payload.id) {\n          return {\n            ...item,\n            ...payload,\n            loading: false,\n            isOpen: true,\n            error: '',\n          }\n        }\n        return item\n      })\n    },\n\n    deleteWBCommandSuccess: (state, { payload }: { payload: string }) => {\n      state.items = [...state.items.filter((item) => item.id !== payload)]\n    },\n\n    // toggle open card\n    toggleOpenWBResult: (state, { payload }: { payload: string }) => {\n      state.items = [...state.items].map((item) => {\n        if (item.id === payload) {\n          return { ...item, isOpen: !item.isOpen }\n        }\n        return item\n      })\n    },\n\n    resetWBHistoryItems: (state) => {\n      state.items = []\n      state.isLoaded = false\n    },\n\n    stopProcessing: (state) => {\n      state.processing = false\n    },\n\n    clearWbResults: (state) => {\n      state.clearing = true\n    },\n\n    clearWbResultsSuccess: (state) => {\n      state.clearing = false\n      state.items = []\n    },\n\n    clearWbResultsFailed: (state) => {\n      state.clearing = false\n    },\n\n    changeResultsMode: (state, { payload }) => {\n      state.resultsMode = payload\n      localStorageService.set(BrowserStorageItem.wbGroupMode, payload)\n    },\n\n    changeActiveRunQueryMode: (state, { payload }) => {\n      state.activeRunQueryMode = payload\n      localStorageService.set(BrowserStorageItem.RunQueryMode, payload)\n    },\n  },\n})\n\n// Actions generated from the slice\nexport const {\n  setWBResultsInitialState,\n  loadWBHistory,\n  loadWBHistorySuccess,\n  loadWBHistoryFailure,\n  processWBCommand,\n  fetchWBCommandSuccess,\n  processWBCommandFailure,\n  processWBCommandsFailure,\n  sendWBCommand,\n  sendWBCommandSuccess,\n  toggleOpenWBResult,\n  deleteWBCommandSuccess,\n  resetWBHistoryItems,\n  stopProcessing,\n  clearWbResults,\n  clearWbResultsSuccess,\n  clearWbResultsFailed,\n  changeResultsMode,\n  changeActiveRunQueryMode,\n} = workbenchResultsSlice.actions\n\n// A selector\nexport const workbenchResultsSelector = (state: RootState) =>\n  state.workbench.results\n\n// The reducer\nexport default workbenchResultsSlice.reducer\n\n// Asynchronous thunk actions\nexport function fetchWBHistoryAction(\n  instanceId: string,\n  executionType = CommandExecutionType.Workbench,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    dispatch(loadWBHistory())\n\n    try {\n      const state = stateInit()\n      const envDependentFlag =\n        state.app.features.featureFlags.features.envDependent?.flag\n      if (envDependentFlag === false) {\n        // Fetch commands from local storage\n        const commandsHistory = await getLocalWbHistory(\n          wbHistoryStorage,\n          instanceId,\n        )\n        if (Array.isArray(commandsHistory)) {\n          dispatch(loadWBHistorySuccess(reverse(commandsHistory)))\n        } else {\n          dispatch(loadWBHistorySuccess([]))\n        }\n        return\n      }\n      const { data, status } = await apiService.get<CommandExecution[]>(\n        getUrl(instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n        { params: { type: executionType } },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(loadWBHistorySuccess(data))\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(loadWBHistoryFailure(errorMessage))\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function sendWBCommandAction({\n  commands = [],\n  multiCommands = [],\n  mode = RunQueryMode.ASCII,\n  resultsMode = ResultsMode.Default,\n  commandId = `${Date.now()}`,\n  executionType = CommandExecutionType.Workbench,\n  onSuccessAction,\n  onFailAction,\n}: {\n  commands: string[]\n  multiCommands?: string[]\n  commandId?: string\n  mode: RunQueryMode\n  resultsMode?: ResultsMode\n  executionType?: CommandExecutionType\n  onSuccessAction?: (multiCommands: string[]) => void\n  onFailAction?: () => void\n}) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      dispatch(\n        sendWBCommand({\n          commands: isGroupResults(resultsMode)\n            ? [`${commands.length} - Command(s)`]\n            : commands,\n          commandId,\n        }),\n      )\n\n      dispatch(setDbIndexState(true))\n\n      const { data, status } = await apiService.post<CommandExecution[]>(\n        getUrl(id, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n        {\n          commands,\n          mode,\n          resultsMode,\n          type: executionType,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          sendWBCommandSuccess({\n            commandId,\n            data: reverse(data),\n            processing: !!multiCommands?.length,\n          }),\n        )\n        dispatch(setDbIndexState(!!multiCommands?.length))\n        const envDependentFlag =\n          state.app.features.featureFlags.features.envDependent?.flag\n        if (envDependentFlag === false) {\n          await addCommands(wbHistoryStorage, reverse(data))\n        }\n        onSuccessAction?.(multiCommands)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(\n        processWBCommandsFailure({\n          commandsId: commands.map((_, i) => commandId + i),\n          error: errorMessage,\n        }),\n      )\n      dispatch(setDbIndexState(false))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function sendWBCommandClusterAction({\n  commands = [],\n  multiCommands = [],\n  options,\n  mode = RunQueryMode.ASCII,\n  resultsMode = ResultsMode.Default,\n  commandId = `${Date.now()}`,\n  executionType = CommandExecutionType.Workbench,\n  onSuccessAction,\n  onFailAction,\n}: {\n  commands: string[]\n  options: CreateCommandExecutionsDto\n  commandId?: string\n  multiCommands?: string[]\n  mode?: RunQueryMode\n  resultsMode?: ResultsMode\n  executionType?: CommandExecutionType\n  onSuccessAction?: (multiCommands: string[]) => void\n  onFailAction?: () => void\n}) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      dispatch(\n        sendWBCommand({\n          commands: isGroupResults(resultsMode)\n            ? [`${commands.length} - Commands`]\n            : commands,\n          commandId,\n        }),\n      )\n\n      const { data, status } = await apiService.post<CommandExecution[]>(\n        getUrl(id, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n        {\n          ...options,\n          commands,\n          mode,\n          resultsMode,\n          type: executionType,\n          outputFormat: CliOutputFormatterType.Raw,\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(\n          sendWBCommandSuccess({\n            commandId,\n            data: reverse(data),\n            processing: !!multiCommands?.length,\n          }),\n        )\n        const envDependentFlag =\n          state.app.features.featureFlags.features.envDependent?.flag\n        if (envDependentFlag === false) {\n          await addCommands(wbHistoryStorage, reverse(data))\n        }\n        onSuccessAction?.(multiCommands)\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(\n        processWBCommandsFailure({\n          commandsId: commands.map((_, i) => commandId + i),\n          error: errorMessage,\n        }),\n      )\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function fetchWBCommandAction(\n  commandId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      dispatch(processWBCommand(commandId))\n      const envDependentFlag =\n        state.app.features.featureFlags.features.envDependent?.flag\n      if (envDependentFlag === false) {\n        const command = await findCommand(wbHistoryStorage, commandId)\n\n        dispatch(fetchWBCommandSuccess(command as CommandExecution))\n\n        onSuccessAction?.()\n        return\n      }\n      const { data, status } = await apiService.get<CommandExecution>(\n        getUrl(id, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, commandId),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(fetchWBCommandSuccess(data))\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(processWBCommandFailure({ id: commandId, error: errorMessage }))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function deleteWBCommandAction(\n  commandId: string,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      dispatch(processWBCommand(commandId))\n      const envDependentFlag =\n        state.app.features.featureFlags.features.envDependent?.flag\n      if (envDependentFlag === false) {\n        await removeCommand(wbHistoryStorage, id, commandId)\n\n        dispatch(deleteWBCommandSuccess(commandId))\n        onSuccessAction?.()\n        return\n      }\n\n      const { status } = await apiService.delete<CommandExecution>(\n        getUrl(id, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, commandId),\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(deleteWBCommandSuccess(commandId))\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(addErrorNotification(error))\n      dispatch(processWBCommandFailure({ id: commandId, error: errorMessage }))\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function clearWbResultsAction(\n  executionType = CommandExecutionType.Workbench,\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    try {\n      const state = stateInit()\n      const { id = '' } = state.connections.instances.connectedInstance\n\n      dispatch(clearWbResults())\n      const envDependentFlag =\n        state.app.features.featureFlags.features.envDependent?.flag\n      if (envDependentFlag === false) {\n        await clearCommands(wbHistoryStorage, id)\n        dispatch(clearWbResultsSuccess())\n        onSuccessAction?.()\n        return\n      }\n      const { status } = await apiService.delete(\n        getUrl(id, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS),\n        {\n          data: { type: executionType },\n        },\n      )\n\n      if (isStatusSuccessful(status)) {\n        dispatch(clearWbResultsSuccess())\n\n        onSuccessAction?.()\n      }\n    } catch (_err) {\n      const error = _err as AxiosError\n      dispatch(addErrorNotification(error))\n      dispatch(clearWbResultsFailed())\n      onFailAction?.()\n    }\n  }\n}\n\n// Asynchronous thunk action\nexport function sendWbQueryAction(\n  queryInit: string,\n  commandId?: Nullable<string>,\n  executeParams: CodeButtonParams = {},\n  onSuccessAction?: {\n    afterEach?: () => void\n    afterAll?: () => void\n  },\n  onFail?: () => void,\n) {\n  return async (dispatch: AppDispatch, stateInit: () => RootState) => {\n    const state = stateInit()\n\n    const {\n      resultsMode: resultsModeInitial,\n      activeRunQueryMode: activeRunQueryModeInitial,\n    } = state.workbench.results || {}\n    const { batchSize: batchSizeInitial = PIPELINE_COUNT_DEFAULT } =\n      state.user.settings?.config || {}\n    const currentExecuteParams = {\n      resultsMode: resultsModeInitial,\n      activeRunQueryMode: activeRunQueryModeInitial,\n      batchSize: batchSizeInitial,\n    }\n\n    const sendCommand = (\n      commands: string[],\n      multiCommands: string[] = [],\n      executeParams: any,\n      onSuccess: () => void,\n    ) => {\n      const { activeRunQueryMode, resultsMode } = executeParams\n      const { connectionType, host, port } =\n        state.connections.instances?.connectedInstance\n\n      if (connectionType !== ConnectionType.Cluster) {\n        dispatch(\n          sendWBCommandAction({\n            resultsMode,\n            commands,\n            multiCommands,\n            mode: activeRunQueryMode,\n            onSuccessAction: onSuccess,\n            onFailAction: onFail,\n          }),\n        )\n        return\n      }\n\n      const options: CreateCommandExecutionsDto = {\n        commands,\n        nodeOptions: {\n          host,\n          port,\n          enableRedirection: true,\n        },\n        role: ClusterNodeRole.All,\n      }\n      dispatch(\n        sendWBCommandClusterAction({\n          commands,\n          options,\n          mode: activeRunQueryMode,\n          resultsMode,\n          multiCommands,\n          onSuccessAction: onSuccess,\n          onFailAction: onFail,\n        }),\n      )\n    }\n\n    const prepareQueryToSend = (\n      commandInit: string,\n      commandId?: Nullable<string>,\n      executeParams: CodeButtonParams = {},\n    ) => {\n      if (!commandInit?.length) {\n        if (queryInit?.length) {\n          onSuccessAction?.afterAll?.()\n        }\n        return\n      }\n\n      const { batchSize, activeRunQueryMode, resultsMode } = getExecuteParams(\n        executeParams,\n        currentExecuteParams,\n      )\n      const commandsForExecuting = getCommandsForExecution(commandInit)\n      const chunkSize = isGroupResults(resultsMode)\n        ? commandsForExecuting.length\n        : batchSize > 1\n          ? batchSize\n          : 1\n      const [commands, ...rest] = chunk(commandsForExecuting, chunkSize)\n      const multiCommands = rest.map((command) => getMultiCommands(command))\n\n      if (!commands?.length) {\n        prepareQueryToSend(multiCommands.join('\\n'), commandId, executeParams)\n        return\n      }\n\n      sendCommand(\n        commands,\n        multiCommands,\n        { activeRunQueryMode, resultsMode },\n        () => {\n          prepareQueryToSend(multiCommands.join('\\n'), commandId, executeParams)\n          onSuccessAction?.afterEach?.()\n        },\n      )\n    }\n\n    prepareQueryToSend(queryInit, commandId, executeParams)\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/slices/workbench/wb-tutorials.ts",
    "content": "import { createSlice } from '@reduxjs/toolkit'\nimport { ApiEndpoints } from 'uiSrc/constants'\nimport { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils'\nimport { resourcesService } from 'uiSrc/services'\nimport {\n  IEnablementAreaItem,\n  StateWorkbenchEnablementArea,\n} from 'uiSrc/slices/interfaces'\n\nimport { AppDispatch, RootState } from '../store'\n\nexport const defaultItems: IEnablementAreaItem[] = []\nexport const initialState: StateWorkbenchEnablementArea = {\n  loading: false,\n  error: '',\n  items: [],\n}\n\n// A slice for recipes\nconst workbenchTutorialsSlice = createSlice({\n  name: 'workbenchTutorials',\n  initialState,\n  reducers: {\n    getWBTutorials: (state) => {\n      state.loading = true\n    },\n    getWBTutorialsSuccess: (\n      state,\n      { payload }: { payload: IEnablementAreaItem },\n    ) => {\n      state.loading = false\n      state.items = [payload]\n    },\n    getWBTutorialsFailure: (state, { payload }) => {\n      state.loading = false\n      state.error = payload\n      state.items = defaultItems\n    },\n  },\n})\n\n// A selector\nexport const workbenchTutorialsSelector = (state: RootState) =>\n  state.workbench.tutorials\n\n// Actions generated from the slice\nexport const { getWBTutorials, getWBTutorialsSuccess, getWBTutorialsFailure } =\n  workbenchTutorialsSlice.actions\n\n// The reducer\nexport default workbenchTutorialsSlice.reducer\n\n// Asynchronous thunk action\nexport function fetchTutorials(\n  onSuccessAction?: () => void,\n  onFailAction?: () => void,\n) {\n  return async (dispatch: AppDispatch) => {\n    dispatch(getWBTutorials())\n\n    try {\n      const { data, status } = await resourcesService.get(\n        ApiEndpoints.TUTORIALS,\n      )\n      if (isStatusSuccessful(status)) {\n        dispatch(getWBTutorialsSuccess(data))\n        onSuccessAction?.()\n      }\n    } catch (error) {\n      const errorMessage = getApiErrorMessage(error)\n      dispatch(getWBTutorialsFailure(errorMessage))\n      onFailAction?.()\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_base.scss",
    "content": "@use \"../mixins/_eui\";\n$sideBarSize: 60px;\n\n:root {\n  --base: 16px;\n  //2px\n  --size-xxs: calc(var(--base) * 0.125);\n  //4px\n  --size-xs: calc(var(--base) * 0.25);\n  //8px\n  --size-s: calc(var(--base) * 0.5);\n  //12px\n  --size-m: calc(var(--base) * 0.75);\n  //16px\n  --size-base: var(--base);\n  //24px\n  --size-l: calc(var(--base) * 1.5);\n  //32px\n  --size-xl: calc(var(--base) * 2);\n  //40px\n  --size-xxl: calc(var(--base) * 2.5);\n  //48px\n  --size-xxxl: calc(var(--base) * 3);\n  //64px\n  --size-xxxxl: calc(var(--base) * 4);\n\n  // to 574px\n  --bp-xs: 0;\n  // to 767px\n  --bp-s: 575px;\n  // to 991px\n  --bp-m: 768px;\n  // to 1199px\n  --bp-l: 992px;\n  // above 1200px\n  --bp-xl: 1200px;\n}\n\nhtml {\n  letter-spacing: normal !important;\n  background-color: var(--euiPageBackgroundColor);\n  color: var(--htmlColor);\n  // sass-lint:disable-block no-misspelled-properties\n  scrollbar-width: thin;\n\n  scrollbar-color: var(--scrollBackgroundColor) transparent;\n}\n\nbody {\n  @include eui.scrollBar;\n  -webkit-font-smoothing: antialiased;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n.euiPage.main {\n  color: var(--htmlColor);\n}\n\n.euiPage .euiPage {\n  padding-bottom: 0;\n}\n\n.euiPage {\n  background-color: var(--euiPageBackgroundColor) !important;\n}\n\nmain.euiPageBody {\n  height: calc(100vh - 60px);\n  @media only screen and (min-width: 768px) {\n    height: 100vh;\n  }\n}\n\n.euiPageSideBar {\n  margin-right: 0 !important;\n  width: $sideBarSize;\n  min-width: $sideBarSize !important;\n  overflow: hidden !important;\n  height: $sideBarSize !important;\n  max-height: $sideBarSize !important;\n  min-height: $sideBarSize !important;\n  padding: 0;\n  flex: 1 1 $sideBarSize !important;\n\n  @media only screen and (min-width: 768px) {\n    display: block;\n    height: 100% !important;\n    max-height: 100% !important;\n    flex: 0 1 0 !important;\n    overflow: auto !important;\n  }\n}\n\n.euiLoadingSpinner {\n  border-top-color: transparent;\n}\n\n.link-underline {\n  color: currentColor;\n  text-decoration: underline;\n\n  &:hover {\n    text-decoration: none;\n  }\n}\n\n.color-shade {\n  color: var(--textColorShade) !important;\n}\n\n.euiHealth .euiIcon {\n  width: 18px !important;\n  height: 18px !important;\n}\n\n.euiSpacer--m {\n  height: 12px !important;\n}\n\n.euiSpacer--xl {\n  height: 36px !important;\n}\n\n.euiProgress {\n  background-color: var(--euiColorLightShade);\n}\n.euiProgress--primary.euiProgress--indeterminate:before,\n.euiProgress--success.euiProgress--indeterminate:before {\n  background-color: var(--euiColorPrimary);\n}\n\n.euiLoadingContent__singleLineBackground {\n  background: linear-gradient(\n    137deg,\n    var(--loadingContentColor) 45%,\n    var(--euiColorLightestShade) 50%,\n    var(--loadingContentColor) 55%\n  );\n}\n\n.euiListGroupItem--subdued .euiListGroupItem__text:not(:disabled),\n.euiListGroupItem--subdued .euiListGroupItem__button:not(:disabled) {\n  color: var(--euiTextSubduedColor);\n}\n\n.euiText pre {\n  color: var(--euiTextColor);\n}\n\n// overrides\n:root {\n  // todo: take from theme\n  --backgroundPrimaryHover: var(--euiPageBackgroundColor);\n  --color-primary: #e8f1ff;\n  --color-primary-text: #006bb4;\n  --color-ghost-text: #fff;\n  --color-text-text: #343741;\n  --color-ghost: rgba(255, 255, 255, 0.1);\n  --color-subdued: #e8f1ff;\n  --background-hover: rgba(23 80 186 / 0.04);\n  --background-primary-hover: rgba(23 80 186 / 0.04);\n  --border-radius-small: 4px;\n  --border-radius-medium: 6px;\n  --font-weight-m: 500;\n  --font-size-xs: 12px;\n  --font-size-s: 14px;\n  --font-size-m: 16px;\n  --font-size-l: 18px;\n  --gap-s: var(--size-s);\n  --gap-m: var(--size-m);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_flex_groups.scss",
    "content": ".euiFlexGroup--gutterLarge {\n  @media only screen and (max-width: 767px) {\n    margin: 0 !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_fonts.scss",
    "content": "@font-face {\n  font-family: 'Inconsolata';\n  src: url('uiSrc/assets/fonts/inconsolata/Inconsolata-Regular.ttf') format('truetype');\n}\n\n@font-face {\n  font-family: 'Inconsolata';\n  font-weight: bold;\n  src: url('uiSrc/assets/fonts/inconsolata/Inconsolata-Bold.ttf') format('truetype');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 300;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-Light.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 300;\n  font-style: italic;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-LightItalic.woff2') format('woff2');\n}\n\n\n@font-face {\n  font-family: 'Graphik';\n  src: url('uiSrc/assets/fonts/graphik/Graphik-Regular.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-style: italic;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-RegularItalic.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 500;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-Medium.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 500;\n  font-style: italic;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-MediumItalic.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 600;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-Semibold.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'Graphik';\n  font-weight: 600;\n  font-style: italic;\n  src: url('uiSrc/assets/fonts/graphik/Graphik-SemiboldItalic.woff2') format('woff2');\n}\n\n@font-face {\n  font-family: 'SourceCodePro';\n  font-weight: 400;\n  font-style: normal;\n  src: url('uiSrc/assets/fonts/sourceCodePro/SourceCodePro-Regular.ttf') format('truetype');\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_functions.scss",
    "content": "@function vh($base) {\n  @return $base;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_functions_electron.scss",
    "content": "@function vh($base) {\n  $result: calc($base - 320px);\n  @return $result;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_helpers.scss",
    "content": ".text-capitalize {\n  text-transform: capitalize;\n}\n\n.text-uppercase {\n  text-transform: uppercase;\n}\n\n.fluid {\n  width: 100%;\n}\n\n.flex-row {\n  display: flex !important;\n}\n\n.flex-column {\n  display: flex;\n  flex-direction: column;\n}\n\n.space-between {\n  justify-content: space-between;\n}\n\n.line-clamp-2 {\n  line-clamp: 2;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: -webkit-box !important;\n}\n\n.relative {\n  position: relative;\n}\n\n.italic {\n  font-style: italic;\n}\n\n.truncateText {\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  width: 100%;\n  overflow: hidden;\n  & > div,\n  & > span,\n  & > p {\n    max-width: 100%;\n  }\n}\n\n.transparent {\n  opacity: 0;\n}\n\n.inline-flex {\n  display: inline-flex !important;\n}\n\n.copy-btn,\n.copy-near-btn {\n  display: flex;\n  margin-left: 25px;\n  opacity: 0;\n  height: 100% !important;\n  flex-shrink: 0;\n  transition: opacity 250ms ease-in-out;\n}\n\n.copy-near-btn {\n  margin-left: 5px;\n}\n\n.copy-btn-wrapper {\n  position: relative;\n  display: flex;\n  max-width: 100%;\n  justify-content: flex-start;\n  align-items: center;\n  gap: 0.5rem;\n\n  &:hover .copy-btn,\n  &:hover .copy-near-btn {\n    opacity: 1;\n  }\n}\n\n.popover-without-top-tail {\n  margin-top: -8px;\n\n  // removes top arrow (it is always first child)\n  & > span:nth-child(1) * {\n    display: none\n  }\n}\n\n.color-green {\n  color: var(--defaultGreenColor) !important;\n}\n\n.input-warning {\n  background-image: linear-gradient(\n    to top,\n    var(--euiColorWarningLight),\n    var(--euiColorWarningLight) 2px,\n    #0000 2px,\n    #0000 100%\n  ) !important;\n}\n\n// To be removed once global font styling is fixed\n.font-inconsolata {\n  font-family: 'Inconsolata', serif !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_inputs.scss",
    "content": ".euiFormControlLayout--group {\n  .euiFieldText,\n  .euiFieldNumber,\n  .euiSelect,\n  .euiSuperSelectControl {\n    box-shadow: none !important;\n    border: 0 !important;\n  }\n}\n\n.euiTableCellContent__text {\n  pointer-events: none !important;\n}\n\ninput[type='password'] ~ .euiFormControlLayoutIcons,\ninput[name='password'] ~ .euiFormControlLayoutIcons,\ninput[name='sentinelMasterPassword'] ~ .euiFormControlLayoutIcons,\ninput[name='sshPassword'] ~ .euiFormControlLayoutIcons,\ninput[name='sshPassphrase'] ~ .euiFormControlLayoutIcons {\n  display: none;\n}\n\n\n.euiComboBox.euiComboBox-isOpen .euiComboBox__inputWrap {\n  background-image: none !important;\n  border-bottom: solid 2px var(--euiColorPrimary) !important;\n}\n.euiComboBox .euiComboBox__inputWrap {\n  .euiComboBoxPill {\n    background-color: var(--buttonDarkenBgColor) !important;\n  }\n  .euiBadge__iconButton {\n    margin-top: 1px;\n    margin-left: 8px;\n    margin-right: 3px;\n    border-radius: 50%;\n    background-color: var(--euiColorPrimary);\n    color: var(--rdiSecondaryBgColor);\n    width: 13px;\n    height: 13px;\n\n    &:hover {\n      transform: translateY(-1px);\n    }\n    &:active {\n      transform: translateY(1px);\n    }\n\n    svg {\n      width: 10px;\n      height: 10px;\n\n      &:focus {\n        background: inherit;\n      }\n    }\n  }\n  .euiComboBox__input > input {\n    color: var(--inputTextColor) !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_overrides.scss",
    "content": ".flexItemNoFullWidth {\n  @media only screen and (max-width: 767px) {\n    .euiFlexGroup--responsive > .euiFlexItem {\n      width: inherit !important;\n      flex-basis: inherit !important;\n      margin-left: inherit !important;\n      margin-right: inherit !important;\n      margin-bottom: 8px !important;\n    }\n\n    .euiFlexGroup {\n      flex-wrap: nowrap;\n    }\n\n    .euiFlexGroup--gutterLarge > .euiFlexItem {\n      margin: 12px !important;\n    }\n\n    .action-buttons {\n      margin-left: 10px;\n    }\n  }\n}\n\n.euiTableHeaderButton-isSorted .euiTableSortIcon {\n  fill: var(--iconsDefaultColor) !important;\n  &:hover {\n    fill: var(--iconsDefaultHoverColor) !important;\n  }\n}\n\n.euiFormControlLayoutCustomIcon .euiFormControlLayoutCustomIcon__icon {\n  fill: var(--iconsDefaultColor);\n}\n\n.euiTableRowCell {\n  color: var(--textColorShade) !important;\n}\n\n.euiModal {\n  box-shadow: 0 3px 15px var(--controlsBoxShadowColor) !important;\n  border: 0 !important;\n}\n\n@media only screen and (min-width: 575px) and (max-width: 767px) {\n  .euiPageSideBar {\n    margin-bottom: 0 !important;\n  }\n}\n\n.euiOverlayMask {\n  padding-bottom: 0 !important;\n  background: var(--overlayMaskBgColor) !important;\n}\n\nbody .euiSuperSelect__listbox {\n  max-height: 350px;\n}\n\n.euiPanel {\n  border-color: var(--euiColorLightShade);\n}\n\n.euiFormControlLayout--group .euiFormLabel {\n  background-color: var(--tableDarkestBorderColor);\n  color: var(--htmlColor)\n}\n\n.euiFilePicker-hasFiles .euiFilePicker__promptText {\n  color: var(--euiTextColor);\n}\n\n.euiFilePicker__showDrop .euiFilePicker__prompt, .euiFilePicker__input:focus+.euiFilePicker__prompt {\n  background-image:\n    linear-gradient(to top, var(--euiColorPrimary), var(--euiColorPrimary) 2px, transparent 2px, transparent 100%);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_react_virtualized.scss",
    "content": "\n.ReactVirtualized__Table__headerRow {\n  font-weight: 700;\n  text-transform: uppercase;\n  display: -webkit-box;\n  display: flex;\n  -webkit-box-orient: horizontal;\n  -webkit-box-direction: normal;\n          flex-direction: row;\n  -webkit-box-align: center;\n          align-items: center;\n}\n.ReactVirtualized__Table__row {\n  display: -webkit-box;\n  display: flex;\n  -webkit-box-orient: horizontal;\n  -webkit-box-direction: normal;\n          flex-direction: row;\n  -webkit-box-align: center;\n          align-items: center;\n}\n\n.ReactVirtualized__Table__headerTruncatedText {\n  display: inline-block;\n  max-width: 100%;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n\n.ReactVirtualized__Table__headerColumn,\n.ReactVirtualized__Table__rowColumn {\n  margin-right: 10px;\n  min-width: 0px;\n}\n.ReactVirtualized__Table__rowColumn {\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.ReactVirtualized__Table__headerColumn:first-of-type,\n.ReactVirtualized__Table__rowColumn:first-of-type {\n  margin-left: 10px;\n}\n.ReactVirtualized__Table__sortableHeaderColumn {\n  cursor: pointer;\n}\n\n.ReactVirtualized__Table__sortableHeaderIconContainer {\n  display: -webkit-box;\n  display: flex;\n  -webkit-box-align: center;\n          align-items: center;\n}\n.ReactVirtualized__Table__sortableHeaderIcon {\n  -webkit-box-flex: 0;\n          flex: 0 0 24px;\n  height: 1em;\n  width: 1em;\n  fill: currentColor;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_selects.scss",
    "content": ".euiSuperSelect {\n  .euiFormControlLayoutCustomIcon {\n    transition: transform 0.1s linear;\n  }\n  &.euiPopover-isOpen {\n    .euiFormControlLayoutCustomIcon {\n      transform: rotateX(180deg);\n    }\n  }\n}\n\n.euiContextMenuItem.euiSuperSelect {\n  &__item {\n    min-height: 40px;\n    &:focus {\n      background: none;\n      text-decoration: none;\n    }\n    &:hover {\n      background: var(--hoverInListColorDarken);\n      text-decoration: none;\n      color: var(--euiColorGhost);\n    }\n    &.withColorDefinition {\n      min-height: auto;\n      &:hover,\n      &:focus {\n        background: var(--hoverInListColor);\n      }\n    }\n  }\n}\n.euiPanel.euiPanel--plain {\n  border-color: var(--euiColorLightShade);\n  background-color: var(--euiColorEmptyShade);\n}\n\n.euiResizableButton:focus:not(:disabled):after,\n.euiResizableButton:focus:not(:disabled):before {\n  background-color: var(--euiColorPrimary);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/base/_typography.scss",
    "content": "body {\n  letter-spacing: -0.14px;\n\n  b {\n    font-weight: 500;\n  }\n\n  .euiSuperSelect__item {\n    font-size: 13px;\n    line-height: 14px;\n    letter-spacing: -0.13px;\n  }\n\n  .euiSuperSelect__item .euiContextMenuItem__text {\n    line-height: 16px;\n  }\n\n  .euiText {\n    font-size: 0.875rem;\n    letter-spacing: -0.14px;\n  }\n\n  .euiText--small {\n    font-size: 0.8125rem;\n    letter-spacing: -0.13px;\n    line-height: 1.1875rem;\n  }\n\n  .euiText--extraSmall {\n    font-size: 0.75rem;\n    letter-spacing: -0.12px;\n  }\n\n  .euiTitle {\n    font-weight: 500;\n    letter-spacing: normal;\n  }\n\n  .euiHealth--textSizeS {\n    font-size: 0.8125rem;\n    letter-spacing: -0.13px;\n  }\n}\n\nbody {\n  input,\n  textarea,\n  select,\n  button {\n    font-family: 'Graphik', sans-serif;\n  }\n}\n\n.euiSuperSelectControl {\n  font-family: 'Graphik', sans-serif !important;\n}\n\n.euiText,\n.euiTitle {\n  color: var(--htmlColor) !important;\n}\n\n.euiTextColor.euiTextColor--default {\n  color: var(--htmlColor);\n}\n\n.euiText h4,\n.euiText dt {\n  color: var(--euiTextSubduedColor) !important;\n}\n\n.euiTextColor--danger {\n  color: var(--euiColorDangerText) !important;\n}\n\n.euiTextColor--warning,\n.euiTextColor--warning.warning--light,\n.euiIcon--warning.warning--light {\n  color: var(--euiColorWarningLight) !important;\n}\n\n.euiTextColor--subdued {\n  color: var(--euiTextSubduedColor) !important;\n}\n\n.euiIcon--ghost {\n  color: var(--euiColorGhost) !important;\n}\n\n.euiContextMenuItem,\n.euiSelect,\n.euiSuperSelectControl,\n.euiFieldText,\n.euiFieldNumber {\n  font-family: 'Graphik', sans-serif !important;\n  color: var(--htmlColor) !important;\n}\n\n.euiFieldText:disabled {\n  background-color: var(--inputDisabledBackgroundColor) !important;\n}\n\n.euiButtonIcon--success {\n  color: var(--euiColorSuccess) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_accordion.scss",
    "content": ".euiAccordion__triggerWrapper {\n  h3 {\n    font: normal normal 500 18px/24px Graphik, sans-serif !important;\n    letter-spacing: normal;\n  }\n}\n\n.euiAccordion__icon {\n  transform: rotate(90deg) !important;\n\n  &.euiAccordion__icon-isOpen {\n    transform: rotate(270deg) !important;\n  }\n}\n.euiAccordion__button .euiAccordion__iconWrapper {\n  color: var(--htmlColor) !important;\n}\n.euiAccordion__button:hover,\n.euiAccordion__button:focus {\n  text-decoration: none !important;\n}\n\n.euiCollapsibleNavGroup__title {\n  font: normal normal normal 14px/16px Graphik, sans-serif !important;\n  letter-spacing: -0.14px;\n  padding-bottom: 0 !important;\n  height: 26px;\n  line-height: 26px !important;\n}\n\n.euiCollapsibleNavGroup__children {\n  padding: 10px 20px 15px !important;\n}\n\n.euiCollapsibleNavGroup {\n  border: none !important;\n  margin-top: 20px;\n}\n\n.euiAccordion__childWrapper {\n  background-color: var(--euiColorLightestShade);\n  border: none !important;\n\n  .euiFieldText,\n  .euiFieldNumber,\n  .euiSelect,\n  .euiSuperSelectControl {\n    background-color: var(--euiColorLightestShade) !important;\n  }\n\n  .boxSection {\n    background-color: var(--euiColorLightShade) !important;\n\n    .euiSelect,\n    .euiSuperSelectControl,\n    .euiFieldText {\n      background-color: var(--euiColorLightShade) !important;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_badge.scss",
    "content": ".euiBadge {\n  border-radius: 4px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_buttons.scss",
    "content": "$euiBorderWidthThick: 2px !default;\n\n.euiButton {\n  text-decoration: none !important;\n  color: var(--euiColorGhost) !important;\n  font-weight: 500 !important;\n  letter-spacing: normal !important;\n  height: 42px !important;\n  line-height: 42px !important;\n\n  &.euiButton--secondary {\n    color: var(--buttonSecondaryTextColor) !important;\n    border-color: var(--euiColorSecondary);\n    &:not([class*=\"isDisabled\"]) {\n      &:hover {\n        background-color: var(--buttonSecondaryHoverColor);\n        border-color: var(--buttonSecondaryHoverColor);\n        color: var(--euiColorPrimaryText) !important;\n      }\n    }\n    &:focus:not(:hover),\n    &:focus-within:not(:hover) {\n      border: 1px solid var(--euiColorPrimary);\n      outline: 1px solid var(--euiColorPrimary);\n    }\n    .euiTextColor--default {\n      color: currentColor;\n    }\n  }\n  &.euiButton--primary {\n    &:not([class*=\"isDisabled\"]) {\n      box-shadow: none;\n    }\n  }\n  &.euiButton--fill {\n    color: var(--euiColorPrimaryText) !important;\n    &:hover {\n      color: var(--euiColorPrimaryText) !important;\n    }\n    &:focus:not(:hover),\n    &:focus-within:not(:hover) {\n      border: 1px solid var(--euiColorPrimary);\n      outline: 1px solid var(--euiColorPrimary);\n    }\n  }\n}\n.euiButton.euiButton--fill {\n  &.euiButton--secondary {\n    border-color: var(--euiColorSecondary);\n    background-color: var(--euiColorSecondary);\n\n    &:not([class*=\"isDisabled\"]) {\n      &:hover {\n        background-color: var(--buttonSecondaryHoverColor);\n        border-color: var(--buttonSecondaryHoverColor);\n      }\n      &:focus:not(:hover),\n      &:focus-within:not(:hover) {\n        border: 1px solid var(--euiColorPrimary);\n        outline: 1px solid var(--euiColorPrimary);\n      }\n    }\n  }\n}\n.euiButton.euiBorderWidthThick {\n  border-width: $euiBorderWidthThick;\n}\n\n.euiButton.euiButton-isDisabled.euiButton--fill {\n  opacity: 0.6;\n  &.euiButton--secondary {\n    border-color: var(--euiColorSecondary);\n    background-color: var(--euiColorSecondary);\n    color: var(--buttonSecondaryDisabledTextColor) !important;\n    &:hover {\n      background-color: var(--euiColorSecondary);\n    }\n  }\n  &.euiButton--primary {\n    border-color: var(--euiColorPrimary);\n    background-color: var(--euiColorPrimary);\n    &:hover {\n      background-color: var(--euiColorPrimary);\n    }\n  }\n  &.euiButton--warning {\n    border-color: var(--euiColorColorWarning);\n    background-color: var(--euiColorColorWarning);\n    &:hover {\n      background-color: var(--euiColorColorWarning);\n    }\n  }\n  &.euiButton--danger {\n    border-color: var(--euiColorColorDanger);\n    background-color: var(--euiColorColorWarning);\n    &:hover {\n      background-color: var(--euiColorColorDanger);\n    }\n  }\n}\n\n.euiButton__text {\n  font:\n    normal normal 500 15px/20px Graphik,\n    sans-serif !important;\n}\n\n.euiButtonEmpty__text {\n  font:\n    normal normal 400 14px/18px Graphik,\n    sans-serif;\n}\n\n.euiButton--small {\n  height: 30px !important;\n  line-height: 30px !important;\n  .euiButton__text {\n    font:\n      normal normal 500 13px/16px Graphik,\n      sans-serif !important;\n  }\n}\n\n.euiButton {\n  &.euiButton--warning.euiButton--fill:not([class*=\"isDisabled\"]) {\n    background-color: var(--buttonWarningColor);\n    border-color: var(--buttonWarningColor);\n\n    &:hover,\n    &:focus,\n    &:focus-within {\n      background-color: var(--buttonWarningHoverColor);\n      border-color: var(--buttonWarningHoverColor);\n      outline-color: var(--buttonWarningHoverColor);\n    }\n  }\n}\n\n.euiButtonEmpty.euiButtonEmpty--primary {\n  border-radius: 4px;\n  color: var(--euiTextSubduedColor);\n  background-color: transparent;\n  transition:\n    color ease 0.3s,\n    background-color ease 0.3s;\n\n  &:not([class*=\"isDisabled\"]) {\n    .euiButtonEmpty__text,\n    .euiText {\n      transition: color ease 0.3s;\n      color: var(--euiTextSubduedColor) !important;\n    }\n\n    &:hover,\n    &:focus {\n      text-decoration: none !important;\n      background-color: var(--hoverInListColorDarken);\n      color: var(--htmlColor);\n\n      .euiButtonEmpty__text,\n      .euiText {\n        color: var(--htmlColor) !important;\n      }\n    }\n  }\n}\n\n.euiButtonIcon.euiButtonIcon--primary {\n  color: var(--iconsDefaultColor);\n  &:hover {\n    color: var(--iconsDefaultHoverColor);\n  }\n  &[disabled] {\n    color: var(--controlsBorderColor);\n  }\n}\n\n.euiButtonIcon--text,\n.euiButtonEmpty--text {\n  color: var(--euiTextSubduedColorHover) !important;\n}\n\n.btnLikeLink {\n  font: inherit !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_callout.scss",
    "content": ".euiCallOut {\n  padding: 12px 24px !important;\n  &--primary {\n    background-color: var(--callOutBackgroundColor) !important;\n    border-color: var(--euiColorPrimary) !important;\n    .euiText {\n      color: var(--euiTooltipTextColor) !important;\n      line-height: 24px;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_cli_output.scss",
    "content": "@use \"../mixins/eui\";\n\n.cli-container {\n  @include eui.scrollBar;\n\n  flex: auto;\n  border-top: inherit;\n  padding: 9px 18px;\n  height: 100%;\n  word-break: break-all;\n\n  font: normal normal normal 14px/17px Inconsolata !important;\n  text-align: left;\n  letter-spacing: 0;\n  color: var(--textColorShade);\n\n  border-top: 1px solid var(--euiColorLightShade);\n\n  z-index: 10;\n\n  overflow-y: auto;\n  overflow-x: hidden;\n}\n\n.cli-command-wrapper {\n  font: normal normal bold 14px/15px Inconsolata !important;\n  white-space: pre-line;\n}\n\n.cli-output-response-success {\n  color: var(--cliOutputResponseColor) !important;\n}\n\n.cli-output-response-fail {\n  color: var(--cliOutputResponseFailColor) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_components.scss",
    "content": "// Import here all partials from current folder\n@import \"forms\";\n@import \"buttons\";\n@import \"toasts\";\n@import \"accordion\";\n@import \"popover\";\n@import \"table\";\n@import \"badge\";\n@import \"radio\";\n@import \"resizable_container\";\n@import \"database\";\n@import \"callout\";\n@import \"notificationBody\";\n@import \"json_view\";\n@import \"cli_output\";\n@import \"modal\";\n@import \"markdown/index\";\n@import \"homePage\";\n@import \"itemList\";\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_database.scss",
    "content": ".databaseContainer {\n  position: relative;\n  padding: 20px 14px 0 14px;\n  background-color: var(--euiColorEmptyShade);\n  @media only screen and (min-width: 768px) {\n    padding: 30px 14px 0 30px;\n    max-width: calc(100vw - 95px);\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_forms.scss",
    "content": "@use \"../mixins/eui\";\n\n.euiButton,\n.euiCollapsibleNav,\n.euiHeaderSectionItem__button {\n  backface-visibility: hidden;\n}\n\n.euiFormControlLayout--compressed {\n  height: 33px !important;\n}\n\n.formFooterBar {\n  position: absolute;\n  bottom: 0;\n  // background: var(--browserTableRowEven);\n  border-style: solid;\n  border-width: 0;\n  border-color: var(--euiColorLightShade);\n  border-top-width: 1px;\n  width: 100%;\n\n  z-index: 2;\n\n  max-height: 100%;\n  @include eui.scrollBar;\n  overflow-y: auto;\n\n  &.contentActive {\n    border-color: var(--euiColorPrimary) !important;\n    border-bottom-width: 1px !important;\n  }\n}\n\n.euiRadio .euiRadio__circle,\n.euiCheckbox .euiCheckbox__square {\n  border-width: 2px !important;\n}\n\n.euiFieldText,\n.euiFieldNumber,\n.euiSelect,\n.euiSuperSelectControl,\n.euiComboBox .euiComboBox__inputWrap {\n  background-color: var(--euiColorEmptyShade) !important;\n  max-width: 100% !important;\n  border: 1px solid var(--controlsBorderColor) !important;\n  box-shadow: none !important;\n  font-size: 14px;\n  line-height: 24px;\n  letter-spacing: -0.14px;\n}\n\n.euiFormHelpText {\n  padding-top: 2px;\n}\n\n.euiFormControlLayout--group {\n  box-shadow: none !important;\n  border: 1px solid var(--controlsBorderColor);\n  padding: 0 !important;\n}\n\n.euiCheckbox .euiCheckbox__input ~ .euiCheckbox__label,\n.euiRadio .euiRadio__input ~ .euiRadio__label {\n  color: var(--controlsLabelColor);\n}\n\n.euiCheckbox .euiCheckbox__input + .euiCheckbox__square,\n.euiRadio .euiRadio__input + .euiRadio__circle {\n  border-color: var(--iconsDefaultColor) !important;\n  background-color: var(--euiColorEmptyShade) !important;\n  &:hover {\n    background-color: var(--euiColorEmptyShade) !important;\n    border-color: var(--iconsDefaultHoverColor) !important;\n  }\n}\n\n.euiRadio .euiRadio__input:checked + .euiRadio__circle,\n.euiCheckbox .euiCheckbox__input:checked + .euiCheckbox__square {\n  border-color: var(--euiColorPrimary) !important;\n}\n\n.euiRadio:hover,\n.euiCheckbox:hover {\n  .euiRadio__label,\n  .euiCheckbox__label {\n    transition: color 150ms ease-in;\n    color: var(--controlsLabelHoverColor) !important;\n  }\n}\n.euiCheckbox .euiCheckbox__input:checked ~ .euiCheckbox__label,\n.euiRadio .euiRadio__input:checked ~ .euiRadio__label {\n  color: var(--controlsLabelHoverColor) !important;\n}\n\n.euiRadio:hover .euiRadio__circle,\n.euiRadio:hover .euiCheckbox__square,\n.euiCheckbox:hover .euiRadio__circle,\n.euiCheckbox:hover .euiCheckbox__square {\n  border-color: var(--iconsDefaultHoverColor) !important;\n}\n\n.euiCheckbox .euiCheckbox__input:checked + .euiCheckbox__square {\n  border-color: var(--euiColorPrimary) !important;\n  background-color: var(--euiColorPrimary) !important;\n}\n\n.theme_DARK {\n  .euiCheckbox .euiCheckbox__input:checked + .euiCheckbox__square {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='8' viewBox='0 0 10 8'%3E%3Cpath d='M.375 2.625L3.375 5.625M3.375 5.625L8.625.375' fill='none' fill-rule='evenodd' stroke='rgb%2832, 32, 32%29' stroke-linecap='round' stroke-width='1.5' transform='translate(.5 1)'/%3E%3C/svg%3E\") !important;\n  }\n}\n\n.theme_LIGHT {\n  .euiCheckbox .euiCheckbox__input:checked + .euiCheckbox__square {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='8' viewBox='0 0 10 8'%3E%3Cpath d='M.375 2.625L3.375 5.625M3.375 5.625L8.625.375' fill='none' fill-rule='evenodd' stroke='rgb%28255, 255, 255%29' stroke-linecap='round' stroke-width='1.5' transform='translate(.5 1)'/%3E%3C/svg%3E\") !important;\n  }\n}\n\n.inlineFieldsNoSpace {\n  .euiFlexGroup {\n    .euiFlexItem:not(:first-child) {\n      .euiFieldText,\n      .euiFormControlLayout--group,\n      .euiSuperSelectControl {\n        border-left: 0 !important;\n      }\n    }\n  }\n}\n\n.euiComboBox .euiComboBox__inputWrap {\n  padding-top: 5px !important;\n  padding-left: 8px !important;\n  min-height: 43px !important;\n  .euiBadge {\n    background-color: var(--comboBoxBadgeBgColor);\n    color: var(--euiTextSubduedColor);\n    border: 0;\n  }\n\n  .euiBadge__text {\n    line-height: 20px;\n  }\n\n  .euiComboBoxPlaceholder {\n    color: var(--inputPlaceholderColor) !important;\n  }\n}\n\n.euiSuperSelectControl:focus,\n.euiFieldNumber:focus,\n.euiFieldText:focus {\n  background-image: linear-gradient(\n    to top,\n    var(--euiColorPrimary),\n    var(--euiColorPrimary) 2px,\n    transparent 2px,\n    transparent 100%\n  ) !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_homePage.scss",
    "content": "@use \"../mixins/eui\";\n\n.homePage {\n  .addInstanceBtn :global {\n    .euiButton {\n      padding-left: 10px;\n      padding-right: 10px;\n      height: 43px;\n      margin: 0 auto;\n      text-decoration: none !important;\n    }\n\n    .euiButton__text {\n      font-weight: 500 !important;\n    }\n  }\n\n  .importDatabasesBtn :global(.euiButton) {\n    height: 43px;\n    min-width: auto !important;\n\n    :global(.euiButton__text) {\n      display: flex;\n      align-items: center;\n    }\n  }\n\n  .contentDL {\n    @include eui.euiBreakpoint(\"xs\", \"s\") {\n      & > div:first-of-type {\n        margin-left: 0 !important;\n      }\n      & > div:last-of-type {\n        margin-right: 0 !important;\n      }\n    }\n  }\n\n  .spacerDl {\n    @include eui.euiBreakpoint(\"xs\", \"s\") {\n      height: 6px !important;\n    }\n    @include eui.euiBreakpoint(\"m\", \"l\", \"xl\") {\n      height: 18px !important;\n    }\n  }\n\n  .searchContainer {\n    position: relative;\n    height: 42px;\n    max-width: 320px;\n    margin-left: auto !important;\n    padding-left: 12px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_itemList.scss",
    "content": "@use \"../mixins/eui\";\n\n.itemList {\n  @include eui.scrollBar;\n\n  height: 100%;\n  overflow: auto;\n  position: relative;\n\n  background-color: var(--euiColorEmptyShade);\n\n  .euiBasicTable {\n    border-top: none;\n  }\n\n  .hiddenColumn {\n    width: 0 !important;\n    display: none !important;\n  }\n\n  .euiTable {\n    position: relative;\n    background-color: transparent;\n  }\n\n  thead tr {\n    background-color: var(--euiColorEmptyShade);\n    height: 54px;\n\n    &:first-child {\n      border-left: 1px solid var(--euiColorLightShade);\n    }\n    &:last-child {\n      border-right: 1px solid var(--euiColorLightShade);\n    }\n  }\n\n  tbody tr {\n    &:last-child {\n      border-bottom: 1px solid var(--euiColorLightShade);\n    }\n  }\n\n  .euiTableHeaderCell,\n  .euiTableHeaderCellCheckbox {\n    padding-top: 3px;\n    position: sticky;\n    top: 0;\n    z-index: 1;\n    background-color: var(--euiColorEmptyShade);\n    border-bottom: none !important;\n\n    box-shadow: inset 0 1px 0 var(--euiColorLightShade), inset 0 -1px 0 var(--euiColorLightShade);\n  }\n\n  .euiTableRow {\n    font-size: 14px !important;\n    height: 48px;\n\n    .column_name {\n      cursor: pointer;\n      padding-top: 0;\n      padding-bottom: 0;\n\n      div span:nth-child(2) {\n        overflow: hidden;\n      }\n\n      :global(.euiToolTipAnchor) {\n        max-width: 100%;\n      }\n    }\n\n    .copyHostPortText,\n    .copyUrlText,\n    .copyPublicEndpointText,\n    .column_name,\n    .column_name .euiToolTipAnchor {\n      display: inline-block;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      overflow: hidden;\n      max-width: 100%;\n      vertical-align: top;\n    }\n\n    .euiIcon--medium {\n      width: 18px;\n      height: 18px;\n    }\n\n    .column_controls {\n      float: right;\n      width: 100%;\n      display: flex;\n      justify-content: flex-end;\n      gap: 8px;\n\n      > div, span {\n        display: flex;\n      }\n    }\n\n    .host_port,\n    .url,\n    .public_endpoint {\n      height: 24px;\n      line-height: 24px;\n      width: auto;\n      max-width: 100%;\n      padding-right: 34px;\n      position: relative;\n\n      * {\n        color: var(--textColorShade) !important;\n      }\n\n      &:hover .copyHostPortBtn,\n      &:hover .copyUrlBtn,\n      &:hover .copyPublicEndpointBtn {\n        opacity: 1;\n        height: auto;\n      }\n    }\n\n    .copyHostPortBtn,\n    .copyUrlBtn,\n    .copyPublicEndpointBtn {\n      margin-left: 25px;\n      opacity: 0;\n      height: 0;\n      transition: opacity 250ms ease-in-out;\n    }\n\n    .copyHostPortText,\n    .copyUrlText,\n    .copyPublicEndpointText {\n      display: inline-block;\n      width: auto;\n      max-width: 100%;\n    }\n\n    .copyPublicEndpointText {\n      max-width: calc(100% - 50px);\n    }\n\n    .copyHostPortTooltip,\n    .copyUrlTooltip,\n    .copyPublicEndpointTooltip {\n      position: absolute;\n      right: 0;\n    }\n\n    .column_copy {\n      padding-left: 50%;\n    }\n\n    .deleteInstancePopover {\n      width: 100%;\n    }\n\n    .deleteInstanceTooltip {\n      margin-right: 10%;\n    }\n\n    .editInstanceBtn {\n      position: absolute;\n      right: 50px;\n    }\n\n    &:nth-child(odd) {\n      background-color: var(--euiColorEmptyShade);\n      .options_icon {\n        border: 2px solid var(--euiColorEmptyShade);\n      }\n    }\n    &:nth-child(even) {\n      background-color: var(--browserTableRowEven);\n\n      .options_icon {\n        border: 2px solid var(--browserTableRowEven);\n      }\n    }\n\n    .euiTableRowCell,\n    .euiTableRowCellCheckbox {\n      border-bottom-width: 0;\n    }\n\n    @media only screen and (max-width: 767px) {\n      height: auto;\n    }\n  }\n\n  .euiTableCellContent {\n    @media only screen and (min-width: 767px) {\n      padding-left: 13px;\n    }\n  }\n\n  .euiTableFooterCell,\n  .euiTableHeaderCell {\n    color: var(--htmlColor);\n  }\n\n  .euiTableHeaderCell {\n    .euiTableCellContent__text {\n      font-size: 16px !important;\n      font-family: \"Graphik\", sans-serif !important;\n      font-weight: 500 !important;\n    }\n\n    .euiTableHeaderButton {\n      &:hover *,\n      &:active *,\n      &:focus * {\n        color: var(--euiTextColor) !important;\n        fill: var(--euiTextColor) !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_json_view.scss",
    "content": ".jsonViewer {\n  font: normal normal normal 13px/18px Inconsolata, monospace;\n  letter-spacing: 0.15px;\n  padding: 0;\n  background: transparent;\n  color: var(--euiTextSubduedColor);\n  margin-bottom: 0;\n  white-space: pre-wrap;\n\n  &-collapsed {\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    width: 100%;\n    overflow: hidden;\n    line-height: inherit;\n  }\n\n  .json-pretty__key {\n    color: var(--jsonKeyNameColor);\n  }\n\n  .json-pretty__number-value,\n  .json-pretty__bigint-value,\n  .json-pretty__null-value,\n  .json-pretty__other-value {\n    color: var(--jsonNumberColor);\n  }\n\n  .json-pretty__string-value {\n    color: var(--jsonStringColor);\n  }\n\n  .json-pretty__boolean-value {\n    color: var(--jsonBooleanColor);\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_modal.scss",
    "content": ".euiModal {\n  background-color: var(--euiColorLightestShade) !important;\n}\n\n.euiModal > .euiButtonIcon {\n  background-color: transparent !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_notificationBody.scss",
    "content": ".notificationHTMLBody {\n  a {\n    color: var(--euiTooltipTextColor) !important;\n    text-decoration: underline !important;\n\n    &:hover, &:focus {\n      text-decoration: none !important;\n    }\n  }\n\n  a[target=\"_blank\"] {\n    color: var(--externalLinkTooltipColor) !important\n  }\n\n  ul, ol {\n    margin-top: 0.5rem;\n    margin-left: 1rem !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_popover.scss",
    "content": ".euiPanel.euiPopover__panel {\n  box-shadow: 0 3px 15px var(--controlsBoxShadowColor) !important;\n}\n\n.euiPopover__panel .euiPopover__panelArrow.euiPopover__panelArrow {\n  border-color: transparent !important;\n\n  &--left:before {\n    border-left-color: var(--euiColorLightShade) !important;\n  }\n  &--left:after {\n    border-left-color: var(--euiColorEmptyShade) !important;\n  }\n\n  &--right:before {\n    border-right-color: var(--euiColorLightShade) !important;\n  }\n  &--right:after {\n    border-right-color: var(--euiColorEmptyShade) !important;\n  }\n\n  &--bottom:before {\n    border-bottom-color: var(--euiColorLightShade) !important;\n  }\n  &--bottom:after {\n    border-bottom-color: var(--euiColorEmptyShade) !important;\n  }\n\n  &--top:before {\n    border-top-color: var(--euiColorLightShade) !important;\n  }\n  &--top:after {\n    border-top-color: var(--euiColorEmptyShade) !important;\n  }\n}\n\n.popoverLikeTooltip {\n  max-width: 267px !important;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_radio.scss",
    "content": ".euiRadio .euiRadio__input + .euiRadio__circle {\n  width: 18px;\n  height: 18px;\n  padding: 6px;\n  border-radius: 100% !important;\n}\n\n.euiRadio .euiRadio__input:checked + .euiRadio__circle {\n  background-image: none !important;\n  background-color: var(--euiColorEmptyShade) !important;\n  box-shadow: inset 0 0 0 5px var(--euiColorPrimary) !important;\n  border: none !important;\n}\n\n.main-container, .euiModal {\n  .euiRadio .euiRadio__input:focus + .euiRadio__circle,\n  .euiRadio .euiRadio__input:active:not(:disabled) + .euiRadio__circle {\n    animation: none !important;\n  }\n}\n\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_resizable_container.scss",
    "content": ".euiResizableToggleButton {\n  &:not(.euiResizableToggleButton-isCollapsed) {\n    background-color: var(--euiColorPrimary)  !important;\n    color: var(--euiColorPrimaryText) !important;\n    border: none !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_table.scss",
    "content": "@use \"../mixins/_eui\";\nbody {\n  table .euiTableHeaderCellCheckbox {\n    width: 44px;\n  }\n  table {\n    &.euiTable {\n      background-color: var(--browserTableRowEven) !important;\n    }\n    &.euiTable--responsive .euiTableRow {\n      td {\n        @media only screen and (max-width: 767px) {\n          &:first-child {\n            border-left: none;\n          }\n          &:last-child {\n            border-right: none;\n          }\n        }\n      }\n    }\n    .euiTableHeaderCell {\n      color: var(--htmlColor) !important;\n    }\n    .euiTableRowCell,\n    .euiTableRowCellCheckbox {\n      border-color: var(--euiColorLightShade) !important;\n    }\n    .euiTableRow {\n      td {\n        &:first-child {\n          border-left: 1px solid var(--euiColorLightShade);\n        }\n        &:last-child {\n          border-right: 1px solid var(--euiColorLightShade);\n        }\n      }\n      &-isSelected {\n        border-color: var(--euiColorPrimary) !important;\n        background: var(--tableRowSelectedColor) !important;\n        td {\n          &:first-child {\n            border-left-color: var(--euiColorPrimary);\n          }\n          &:last-child {\n            border-right-color: var(--euiColorPrimary);\n          }\n          border-top: 1px solid var(--euiColorPrimary);\n          border-bottom: 1px solid var(--euiColorPrimary) !important;\n        }\n      }\n      &:hover {\n        background-color: var(--tableRowHoverColor);\n      }\n    }\n  }\n\n  @media only screen and (max-width: 767px) {\n    table .euiTableRow-isSelected td {\n      border-top-width: 0 !important;\n      border-bottom-width: 0 !important;\n    }\n  }\n\n  .euiBasicTable-loading tbody:before  {\n    background-color: var(--euiColorPrimary) !important;\n  }\n\n  .inMemoryTableDefault {\n    &:not(.stickyHeader) {\n      > div:first-child {\n        @include eui.scrollBar;\n        overflow-x: auto;\n      }\n    }\n\n    &.euiBasicTable-loading table {\n      overflow: hidden;\n    }\n\n    &.noHeaderBorders,\n    &.noBorders {\n      .euiTableRow {\n        &:not(:first-child) {\n          .euiTableRowCell {\n            border-top: 0 !important;\n          }\n        }\n      }\n      .euiTableRowCell {\n        &:not(:last-child) {\n          border-right: 0 !important;\n        }\n      }\n\n      .euiTableHeaderCell {\n        border: 0 !important;\n      }\n    }\n\n    &.noBorders {\n      .euiTableCaption {\n        height: 0;\n      }\n      .euiTableRowCell {\n        border-top: 0;\n      }\n      .euiTableRow:nth-child(odd) {\n        td {\n          &:first-child {\n            border-left: 1px solid var(--euiColorEmptyShade);\n          }\n          &:last-child {\n            border-right: 1px solid var(--euiColorEmptyShade);\n          }\n        }\n      }\n      .euiTableRow:nth-child(even) {\n        td {\n          &:first-child {\n            border-left: 1px solid var(--browserTableRowEven);\n          }\n          &:last-child {\n            border-right: 1px solid var(--browserTableRowEven);\n          }\n        }\n      }\n    }\n\n    &.stickyHeader {\n      .euiTableHeaderCell {\n        position: sticky;\n        top: 0;\n        z-index: 1;\n      }\n    }\n\n    table {\n      overflow: initial;\n      table-layout: auto;\n      border-bottom: 1px solid var(--tableDarkestBorderColor);\n    }\n\n    .euiTableCellContent {\n      white-space: nowrap !important;\n      font:\n        normal normal normal 13px/17px Graphik,\n        sans-serif;\n      letter-spacing: -0.13px;\n      padding: 10px 18px;\n    }\n\n    .euiTableRowCell {\n      max-width: 350px;\n\n      border-color: var(--tableDarkestBorderColor);\n      border-right: 1px solid var(--tableLightestBorderColor);\n      border-bottom: none;\n    }\n\n    .euiTableCellContent span {\n      color: var(--textColorShade);\n      padding-top: 1px;\n      max-width: 100%;\n      overflow: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n    }\n\n    .euiTableHeaderCell {\n      letter-spacing: 0;\n      border: 1px solid var(--tableLightestBorderColor);\n\n      .euiTableCellContent__text {\n        color: var(--htmlColor) !important;\n        font:\n          normal normal 500 14px/17px Graphik,\n          sans-serif;\n        letter-spacing: -0.14px;\n      }\n    }\n\n    .euiTableRowCell__mobileHeader {\n      padding: 10px 18px !important;\n      color: var(--htmlColor) !important;\n      font:\n        normal normal 500 14px/17px Graphik,\n        sans-serif !important;\n      letter-spacing: -0.14px;\n    }\n\n    .euiSpacer {\n      display: none;\n    }\n\n    .euiTableRow:nth-child(odd) {\n      background-color: var(--euiColorEmptyShade);\n    }\n    .euiTableRow:nth-child(even) {\n      background-color: var(--browserTableRowEven);\n    }\n\n    // pagination\n    .euiFlexGroup--justifyContentSpaceBetween {\n      justify-content: center;\n      margin: 0;\n\n      .euiFlexItem {\n        margin: 0;\n      }\n\n      .euiPagination {\n        padding: 8px 10px;\n      }\n    }\n\n    .euiTableHeaderMobile {\n      display: none;\n    }\n\n    &.imtd-multiLineCells {\n      .euiTableCellContent,\n      .euiTableCellContent span {\n        white-space: normal !important;\n      }\n\n      .euiTableCellContent {\n        flex-direction: column;\n        align-items: start;\n      }\n\n      .euiTableRowCell {\n        vertical-align: top;\n      }\n    }\n\n    @media only screen and (min-width: 575px) and (max-width: 767px) {\n      .euiTableRow {\n        margin-bottom: 0 !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/_toasts.scss",
    "content": ".euiToast {\n  border-width: 0 !important;\n  box-shadow: 0 3px 15px var(--controlsBoxShadowColor) !important;\n}\n.euiToast--danger,\n.euiToast--success {\n  border-top-width: 3px !important;\n}\n\n.euiToast--danger {\n  border-color: var(--euiToastDangerBorderColor) !important;\n  background-color: var(--euiToastDangerBgColor) !important;\n}\n\n.euiToast--success {\n  background-color: var(--euiToastBackgroundColor) !important;\n  border-color: var(--euiToastSuccessBorderColor) !important;\n}\n\n.euiToast__closeButton {\n  svg {\n    fill: #ffffff !important;\n  }\n\n  &:focus {\n    background-color: initial !important;\n  }\n}\n\n.euiToastHeader__title {\n  font-size: 18px !important;\n}\n\n.euiToastHeader__icon {\n  transform: translateY(3px) !important;\n  width: 20px !important;\n  height: 20px !important;\n  fill: #fff !important;\n}\n\n.toast-danger-btn {\n  box-shadow: none !important;\n  border: none !important;\n  float: right;\n  &.euiButton {\n    color: white !important;\n  }\n  &.euiButton--fill {\n    border: 1px solid var(--buttonDangerToastColor) !important;\n    background-color: transparent !important;\n  }\n  &.euiButton--fill:hover {\n    border: 1px solid var(--buttonDangerToastHoverColor) !important;\n    background-color: var(--buttonDangerToastHoverColor) !important;\n  }\n}\n\n.euiGlobalToastList {\n  width: auto !important;\n\n  .euiToast {\n    width: 340px !important;\n\n    &.dynamic {\n      width: auto !important;\n      max-width: 480px !important;\n    }\n  }\n}\n\n.euiGlobalToastList--right:not(:empty) {\n  align-items: flex-end !important;\n  right: 14px !important;\n}\n\n.euiGlobalToastList--left:not(:empty) {\n  left: 14px !important;\n}\n\n@media only screen and (max-width: 767px) {\n  .euiGlobalToastList {\n    width: 400px !important;\n    max-width: 90vw;\n  }\n  .euiGlobalToastList--right:not(:empty) {\n    left: auto !important;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/_blockquote.scss",
    "content": "@import './variables';\n\nblockquote {\n  padding: 0 10px;\n  border-left: 2px solid var(--controlsBorderColor);\n  margin-bottom: ($margin-size-pace * 6 + px);\n  margin-top: ($margin-size-pace * 2 + px);;\n  font-style: italic;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/_code.scss",
    "content": "@use \"../../mixins/_eui\";\n@import './variables';\n\ncode {\n  background-color: var(--euiColorLightestShade);\n  border-radius: 4px;\n  padding: 1px 4px;\n}\n\npre > code {\n  @include eui.scrollBar;\n\n  display: flex;\n  padding: 8px 16px;\n  width: 100%;\n\n  white-space: pre-wrap;\n  background-color: var(--browserTableRowEven);\n\n  border: 1px solid var(--separatorColor);\n  border-radius: 4px;\n  margin-bottom: ($margin-size-pace * 4 + px);\n  font-size: 11px;\n  overflow: auto;\n\n  @media only screen and (max-width: ($m-breakpoint + px)) {\n    padding: 6px;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/_list.scss",
    "content": "@import './variables';\n\nb + ul, strong + ul, b + ol, strong + ol {\n  margin-top: 10px;\n}\n\nul, ol {\n  &:not(.euiPagination__list) {\n    margin-top: ($margin-size-pace * 3 + px);\n    margin-bottom: ($margin-size-pace * 3 + px);\n    margin-left: 16px;\n    li {\n      &:not(:last-of-type) {\n        margin-bottom: ($margin-size-pace + px);\n      }\n    }\n    @media only screen and (max-width: ($l-breakpoint + px)) {\n      li {\n        line-height: 18px;\n        &:not(:last-of-type) {\n          margin-bottom: ($margin-size-pace + px);\n        }\n      }\n    }\n  }\n  + p {\n    margin-top: ($margin-size-pace * 3 + px);\n  }\n  li p {\n    margin-bottom: 0;\n  }\n}\n\nul:not(.euiPagination__list) {\n  list-style-type: disc;\n  list-style-position: outside;\n  ul, ol {\n    list-style-type: circle;\n    list-style-position: outside;\n    margin-left: 16px;\n  }\n}\n\nol {\n  list-style-type: decimal;\n  list-style-position: outside;\n  ol, ul {\n    list-style-type: circle;\n    list-style-position: outside;\n    margin-left: 16px;\n  }\n}\n\nul.contains-task-list {\n  list-style-type: none;\n  margin-left: 0;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/_table.scss",
    "content": "@import './variables';\n\ntable {\n  th {\n    color: var(--htmlColor);\n    font-weight: normal;\n  }\n  th, td {\n    border: 1px solid var(--euiColorLightShade);\n    padding: 8px 12px;\n  }\n  margin-bottom: ($margin-size-pace * 6 + px);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/_typography.scss",
    "content": "@import './variables';\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--euiColorGhost);\n  font: normal normal 500 14px/24px \"Nunito Sans\", sans-serif;\n  margin-bottom: ($margin-size-pace * 3 + px);\n  margin-top: ($margin-size-pace * 3 + px);\n  letter-spacing: 0;\n}\n\nhr + h1, hr + h2, hr + h3, hr + h4, hr + h5, hr + h6 {\n  margin-top: 0;\n}\n\np + {\n  p, pre, h1, h2, h3, h4, h5, h6 {\n    margin-top: ($margin-size-pace * 3 + px);\n  }\n}\n\n\na {\n  text-decoration: underline;\n  &:focus {\n    text-decoration: underline;\n  }\n}\n\nb, strong {\n  letter-spacing: -0.12px;\n  color: var(--htmlColor);\n}\n\nem, i {\n  b, strong {\n    font-style: italic;\n  }\n}\n\nsup, sub {\n  position: relative;\n  font-size: 75%;\n  line-height: 0;\n  vertical-align: baseline;\n}\nsup {\n  top: -0.5em;\n}\n\nsub {\n  bottom: -0.25em;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/_variables.scss",
    "content": "$margin-size-pace: 4;\n$l-breakpoint: 1440;\n$m-breakpoint: 992;\n\n"
  },
  {
    "path": "redisinsight/ui/src/styles/components/markdown/index.scss",
    "content": "@import './variables';\n\n.jsx-markdown {\n  @import './typography';\n  @import './code';\n  @import './list';\n  @import './table';\n  @import './blockquote';\n\n  font: normal normal normal 12px/15px \"Nunito Sans\", sans-serif;\n\n  hr {\n    border: none;\n    height: 1px;\n    width: 100%;\n    background-color: var(--separatorColor);\n    margin: ($margin-size-pace * 3 + px) 0;\n  }\n\n  img {\n    max-width: 100%;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/elastic.css",
    "content": "@layer app {\n  @keyframes euiAnimFadeIn {\n    0% {\n      opacity: 0;\n    }\n    100% {\n      opacity: 1;\n    }\n  }\n  @keyframes euiGrow {\n    0% {\n      opacity: 0;\n    }\n    1% {\n      opacity: 0;\n      transform: scale(0);\n    }\n    100% {\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n  @keyframes focusRingAnimate {\n    0% {\n      box-shadow: 0 0 0 6px rgba(0, 107, 180, 0);\n    }\n    100% {\n      box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n    }\n  }\n  @keyframes focusRingAnimateLarge {\n    0% {\n      box-shadow: 0 0 0 10px rgba(0, 107, 180, 0);\n    }\n    100% {\n      box-shadow: 0 0 0 4px rgba(0, 107, 180, 0.3);\n    }\n  }\n  @keyframes euiButtonActive {\n    50% {\n      transform: translateY(1px);\n    }\n  }\n  .eui-alignBaseline {\n    vertical-align: baseline !important;\n  }\n  .eui-alignBottom {\n    vertical-align: bottom !important;\n  }\n  .eui-alignMiddle {\n    vertical-align: middle !important;\n  }\n  .eui-alignTop {\n    vertical-align: top !important;\n  }\n  .eui-displayBlock {\n    display: block !important;\n  }\n  .eui-displayInline {\n    display: inline !important;\n  }\n  .eui-displayInlineBlock {\n    display: inline-block !important;\n  }\n  .eui-fullWidth {\n    display: block !important;\n    width: 100% !important;\n  }\n  .eui-textCenter {\n    text-align: center !important;\n  }\n  .eui-textLeft {\n    text-align: left !important;\n  }\n  .eui-textRight {\n    text-align: right !important;\n  }\n  .eui-textNoWrap {\n    white-space: nowrap !important;\n  }\n  .eui-textInheritColor {\n    color: inherit !important;\n  }\n  .eui-textBreakWord {\n    overflow-wrap: break-word !important;\n    word-wrap: break-word !important;\n    word-break: break-word;\n  }\n  .eui-textBreakAll {\n    overflow-wrap: break-word !important;\n    word-break: break-all !important;\n  }\n  .eui-textBreakNormal {\n    overflow-wrap: normal !important;\n    word-wrap: normal !important;\n    word-break: normal !important;\n  }\n  .eui-textOverflowWrap {\n    overflow-wrap: break-word !important;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .eui-textOverflowWrap {\n      word-break: break-all !important;\n    }\n  }\n  .eui-textTruncate {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n  }\n  [class*='eui-showFor'] {\n    display: none !important;\n  }\n  @media only screen and (max-width: 574px) {\n    .eui-hideFor--xs {\n      display: none !important;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .eui-showFor--xs {\n      display: inline !important;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .eui-showFor--xs--block {\n      display: block !important;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .eui-showFor--xs--inlineBlock {\n      display: inline-block !important;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .eui-showFor--xs--flex {\n      display: flex !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .eui-hideFor--s {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .eui-showFor--s {\n      display: inline !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .eui-showFor--s--block {\n      display: block !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .eui-showFor--s--inlineBlock {\n      display: inline-block !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .eui-showFor--s--flex {\n      display: flex !important;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .eui-hideFor--m {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .eui-showFor--m {\n      display: inline !important;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .eui-showFor--m--block {\n      display: block !important;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .eui-showFor--m--inlineBlock {\n      display: inline-block !important;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .eui-showFor--m--flex {\n      display: flex !important;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .eui-hideFor--l {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .eui-showFor--l {\n      display: inline !important;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .eui-showFor--l--block {\n      display: block !important;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .eui-showFor--l--inlineBlock {\n      display: inline-block !important;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .eui-showFor--l--flex {\n      display: flex !important;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .eui-hideFor--xl {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .eui-showFor--xl {\n      display: inline !important;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .eui-showFor--xl--block {\n      display: block !important;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .eui-showFor--xl--inlineBlock {\n      display: inline-block !important;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .eui-showFor--xl--flex {\n      display: flex !important;\n    }\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiIEFlexWrapFix {\n      flex-grow: 1;\n      flex-shrink: 1;\n      flex-basis: 0%;\n    }\n  }\n  .eui-yScroll {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n  }\n  .eui-yScroll::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .eui-yScroll::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .eui-yScroll::-webkit-scrollbar-corner,\n  .eui-yScroll::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .eui-yScroll:focus {\n    outline: none;\n  }\n  .eui-yScroll[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .eui-xScroll {\n    scrollbar-width: thin;\n    overflow-x: auto;\n  }\n  .eui-xScroll::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .eui-xScroll::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .eui-xScroll::-webkit-scrollbar-corner,\n  .eui-xScroll::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .eui-xScroll:focus {\n    outline: none;\n  }\n  .eui-xScroll[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .eui-yScrollWithShadows {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n  }\n  .eui-yScrollWithShadows::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .eui-yScrollWithShadows::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .eui-yScrollWithShadows::-webkit-scrollbar-corner,\n  .eui-yScrollWithShadows::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .eui-yScrollWithShadows:focus {\n    outline: none;\n  }\n  .eui-yScrollWithShadows[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .eui-xScrollWithShadows {\n    scrollbar-width: thin;\n    overflow-x: auto;\n    mask-image: linear-gradient(\n      to right,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n  }\n  .eui-xScrollWithShadows::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .eui-xScrollWithShadows::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .eui-xScrollWithShadows::-webkit-scrollbar-corner,\n  .eui-xScrollWithShadows::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .eui-xScrollWithShadows:focus {\n    outline: none;\n  }\n  .eui-xScrollWithShadows[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .euiYScrollWithShadows {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n  }\n  .euiYScrollWithShadows::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiYScrollWithShadows::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiYScrollWithShadows::-webkit-scrollbar-corner,\n  .euiYScrollWithShadows::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiYScrollWithShadows:focus {\n    outline: none;\n  }\n  .euiYScrollWithShadows[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .eui-isFocusable:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimateLarge !important;\n  }\n  .eui-fullHeight {\n    height: 100%;\n    flex: 1 1 auto;\n    overflow: hidden;\n  }\n  *,\n  *:before,\n  *:after {\n    box-sizing: border-box;\n  }\n  html,\n  body,\n  div,\n  span,\n  applet,\n  object,\n  iframe,\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6,\n  p,\n  blockquote,\n  pre,\n  a,\n  abbr,\n  acronym,\n  address,\n  big,\n  cite,\n  code,\n  del,\n  dfn,\n  em,\n  img,\n  ins,\n  kbd,\n  q,\n  s,\n  samp,\n  small,\n  strike,\n  strong,\n  sub,\n  sup,\n  tt,\n  var,\n  b,\n  u,\n  i,\n  center,\n  dl,\n  dt,\n  dd,\n  ol,\n  ul,\n  li,\n  fieldset,\n  form,\n  label,\n  legend,\n  table,\n  caption,\n  tbody,\n  tfoot,\n  /* thead,\n  tr,\n  th, */\n  td,\n  article,\n  aside,\n  canvas,\n  details,\n  embed,\n  figure,\n  figcaption,\n  footer,\n  header,\n  hgroup,\n  menu,\n  nav,\n  output,\n  ruby,\n  section,\n  summary,\n  time,\n  mark,\n  audio,\n  video {\n    margin: 0;\n    padding: 0;\n    border: none;\n    vertical-align: baseline;\n  }\n  td {\n    vertical-align: middle;\n  }\n  code,\n  pre,\n  kbd,\n  samp {\n    font-family: 'Roboto Mono', Consolas, Menlo, Courier, monospace;\n  }\n  h1,\n  h2,\n  h3,\n  h4,\n  h5,\n  h6,\n  p {\n    font-family: inherit;\n    font-weight: inherit;\n    font-size: inherit;\n  }\n  input,\n  textarea,\n  select,\n  button {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n  }\n  em {\n    font-style: italic;\n  }\n  strong {\n    font-weight: 700;\n  }\n  article,\n  aside,\n  details,\n  figcaption,\n  figure,\n  footer,\n  header,\n  hgroup,\n  menu,\n  nav,\n  section {\n    display: block;\n  }\n  html {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 62.5%;\n    color: #343741;\n    height: 100%;\n    background-color: var(--euiPageBackgroundColor);\n  }\n  body {\n    line-height: 1;\n  }\n  *:focus {\n    outline: none;\n  }\n  *:focus::-moz-focus-inner {\n    border: none;\n  }\n  *:focus:-moz-focusring {\n    outline: none;\n  }\n  a {\n    text-decoration: none;\n    color: var(--euiColorPrimary);\n  }\n  a:hover {\n    text-decoration: none;\n  }\n  a:focus {\n    text-decoration: none;\n    outline: none;\n  }\n  a:hover,\n  button,\n  [role='button'] {\n    cursor: pointer;\n  }\n  input {\n    margin: 0;\n    padding: 0;\n  }\n  input:disabled {\n    opacity: 1;\n  }\n  button {\n    background: none;\n    border: none;\n    padding: 0;\n    margin: 0;\n    outline: none;\n    font-size: inherit;\n    color: inherit;\n    border-radius: 0;\n  }\n  button:hover {\n    cursor: pointer;\n  }\n  ol,\n  ul {\n    list-style: none;\n  }\n  blockquote,\n  q {\n    quotes: none;\n  }\n  blockquote:before,\n  blockquote:after,\n  q:before,\n  q:after {\n    content: '';\n    content: none;\n  }\n  table {\n    border-collapse: collapse;\n    border-spacing: 0;\n  }\n  hr {\n    margin: 0;\n  }\n  fieldset {\n    min-inline-size: auto;\n  }\n  svg text {\n    letter-spacing: normal !important;\n  }\n  html {\n    scrollbar-width: thin;\n    scrollbar-color: transparent(var(--euiColorDarkShade), 0.5) rgba(0, 0, 0, 0);\n  }\n  .euiScreenReaderOnly,\n  .euiScreenReaderOnly--showOnFocus:not(:focus):not(:active) {\n    position: absolute;\n    left: -10000px;\n    top: auto;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n  }\n  .euiSkipLink {\n    transition: none !important;\n  }\n  .euiSkipLink:focus {\n    animation: none !important;\n  }\n  .euiSkipLink.euiSkipLink--absolute:focus {\n    position: absolute;\n  }\n  .euiSkipLink.euiSkipLink--fixed:focus {\n    position: fixed;\n    top: 4px;\n    left: 4px;\n    z-index: 1001;\n  }\n  .euiAccordion__triggerWrapper {\n    display: flex;\n    align-items: center;\n  }\n  .euiAccordion__button {\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    text-align: left;\n    width: 100%;\n    flex-grow: 1;\n    display: flex;\n    align-items: center;\n  }\n  .euiAccordion__button:hover,\n  .euiAccordion__button:focus {\n    text-decoration: underline;\n    cursor: pointer;\n  }\n  .euiAccordion__button:focus .euiAccordion__iconWrapper {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n    color: #006bb4;\n    outline: none;\n  }\n  .euiAccordion__buttonReverse {\n    flex-direction: row-reverse;\n    justify-content: space-between;\n  }\n  .euiAccordion__buttonReverse .euiAccordion__iconWrapper {\n    margin-left: 8px;\n    margin-right: 4px;\n  }\n  .euiAccordion__iconWrapper {\n    width: 16px;\n    height: 16px;\n    margin-left: 4px;\n    margin-right: 8px;\n    border-radius: 4px;\n    flex-shrink: 0;\n  }\n  .euiAccordion__iconWrapper .euiAccordion__icon {\n    vertical-align: top;\n    transition: transform 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiAccordion__iconWrapper .euiAccordion__icon-isOpen {\n    transform: rotate(90deg);\n  }\n  .euiAccordion__iconButton {\n    margin-left: 8px;\n    margin-right: 4px;\n  }\n  .euiAccordion__iconButton:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n    color: #006bb4;\n  }\n  .euiAccordion__optionalAction {\n    flex-shrink: 0;\n  }\n  .euiAccordion__childWrapper {\n    visibility: hidden;\n    height: 0;\n    opacity: 0;\n    overflow: hidden;\n    transform: translatez(0);\n    transition:\n      height 250ms cubic-bezier(0.694, 0.0482, 0.335, 1),\n      opacity 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiAccordion__childWrapper:focus {\n    outline: none;\n  }\n  .euiAccordion__padding--xs {\n    padding: 4px;\n  }\n  .euiAccordion__padding--s {\n    padding: 8px;\n  }\n  .euiAccordion__padding--m {\n    padding: 16px;\n  }\n  .euiAccordion__padding--l {\n    padding: 24px;\n  }\n  .euiAccordion__padding--xl {\n    padding: 32px;\n  }\n  .euiAccordion.euiAccordion-isOpen .euiAccordion__childWrapper {\n    visibility: visible;\n    opacity: 1;\n    height: auto;\n  }\n  .euiAccordion__children-isLoading {\n    line-height: 1.5;\n    display: flex;\n    align-items: center;\n  }\n  .euiAccordion__children-isLoading .euiAccordion__spinner {\n    margin-right: 4px;\n  }\n  .euiAccordionForm__extraAction {\n    opacity: 0;\n    transition: opacity 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiAccordionForm__extraAction:focus {\n    opacity: 1;\n  }\n  .euiAccordionForm__title {\n    display: inline-block;\n  }\n  .euiAccordionForm__button {\n    padding: 16px 16px 16px 0;\n  }\n  .euiAccordionForm__button:hover {\n    text-decoration: none;\n  }\n  .euiAccordionForm__button:hover .euiAccordionForm__title {\n    text-decoration: underline;\n  }\n  .euiAccordionForm {\n    border-top: 1px solid #d3dae6;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiAccordionForm + .euiAccordionForm {\n    border-top: none;\n  }\n  .euiAccordionForm:hover .euiAccordionForm__extraAction {\n    opacity: 1;\n    visibility: visible;\n  }\n  .euiAspectRatio {\n    position: relative;\n  }\n  .euiAspectRatio > * {\n    position: absolute !important;\n    top: 0 !important;\n    left: 0 !important;\n    width: 100% !important;\n    height: 100% !important;\n  }\n  .euiAvatar {\n    flex-shrink: 0;\n    display: inline-flex;\n    justify-content: center;\n    align-items: center;\n    background-size: cover;\n    text-align: center;\n    vertical-align: middle;\n    overflow-x: hidden;\n    font-weight: 500;\n  }\n  .euiAvatar--user {\n    position: relative;\n    border-radius: 50%;\n  }\n  .euiAvatar--user:after {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    border-radius: 50%;\n    content: '';\n    pointer-events: none;\n    border: 1px solid rgba(52, 55, 65, 0.05);\n  }\n  .euiAvatar--space {\n    position: relative;\n    border-radius: 4px;\n  }\n  .euiAvatar--space:after {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    border-radius: 4px;\n    content: '';\n    pointer-events: none;\n    border: 1px solid rgba(52, 55, 65, 0.05);\n  }\n  .euiAvatar-isDisabled {\n    cursor: not-allowed;\n    filter: grayscale(100%);\n  }\n  .euiAvatar--plain {\n    background-color: #fff;\n  }\n  .euiAvatar--s {\n    width: 24px;\n    height: 24px;\n    line-height: 24px;\n    font-size: 12px;\n  }\n  .euiAvatar--m {\n    width: 32px;\n    height: 32px;\n    line-height: 32px;\n    font-size: 14.4px;\n  }\n  .euiAvatar--l {\n    width: 40px;\n    height: 40px;\n    line-height: 40px;\n    font-size: 19.2px;\n  }\n  .euiAvatar--xl {\n    width: 64px;\n    height: 64px;\n    line-height: 64px;\n    font-size: 25.6px;\n  }\n  .euiBadge {\n    font-size: 12px;\n    font-weight: 500;\n    line-height: 18px;\n    padding: 0 8px;\n    display: inline-block;\n    text-decoration: none;\n    border-radius: 2px;\n    border: solid 1px rgba(0, 0, 0, 0);\n    background-color: rgba(0, 0, 0, 0);\n    white-space: nowrap;\n    vertical-align: middle;\n    cursor: default;\n    max-width: 100%;\n    text-align: left;\n  }\n  .euiBadge.euiBadge-isDisabled {\n    color: #88888b !important;\n    background-color: #c2c3c6 !important;\n  }\n  .euiBadge:focus-within {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiBadge + .euiBadge {\n    margin-left: 4px;\n  }\n  .euiBadge .euiBadge__content {\n    min-height: 18px;\n    display: flex;\n    align-items: center;\n    overflow: hidden;\n  }\n  .euiBadge .euiBadge__childButton {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    flex: 1 1 auto;\n    text-align: inherit;\n    font-weight: inherit;\n    line-height: inherit;\n    color: inherit;\n  }\n  .euiBadge .euiBadge__childButton:disabled {\n    cursor: not-allowed;\n  }\n  .euiBadge .euiBadge__childButton:not(:disabled):hover,\n  .euiBadge .euiBadge__childButton:not(:disabled):focus {\n    text-decoration: underline;\n  }\n  .euiBadge .euiBadge__iconButton {\n    flex: 0 0 auto;\n    font-size: 0;\n    margin-left: 4px;\n  }\n  .euiBadge .euiBadge__iconButton:focus {\n    background-color: rgba(255, 255, 255, 0.8);\n    color: #000;\n    border-radius: 2px;\n  }\n  .euiBadge .euiBadge__iconButton:disabled {\n    cursor: not-allowed;\n  }\n  .euiBadge .euiBadge__iconButton .euiBadge__icon {\n    margin: 0 !important;\n  }\n  .euiBadge .euiBadge__text {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    flex: 1 1 auto;\n    cursor: default;\n  }\n  .euiBadge .euiBadge__icon {\n    flex: 0 0 auto;\n  }\n  .euiBadge .euiBadge__icon:not(:only-child) {\n    margin-left: 4px;\n  }\n  .euiBadge.euiBadge--iconLeft .euiBadge__content {\n    flex-direction: row-reverse;\n  }\n  .euiBadge.euiBadge--iconLeft .euiBadge__iconButton,\n  .euiBadge.euiBadge--iconLeft .euiBadge__icon:not(:only-child) {\n    margin-right: 4px;\n    margin-left: 0;\n  }\n  .euiBadge-isClickable:not(:disabled):hover,\n  .euiBadge-isClickable:not(:disabled):focus {\n    text-decoration: underline;\n  }\n  .euiBadge-isClickable.euiBadge-isDisabled {\n    cursor: not-allowed;\n  }\n  .euiBadge-isClickable:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiBadge-isClickable .euiBadge__text {\n    cursor: inherit;\n  }\n  .euiBadge--hollow {\n    background-color: #fff;\n    border-color: #d3dae6;\n    color: #343741;\n  }\n  .euiBadgeGroup__item {\n    display: inline-block;\n    max-width: 100%;\n  }\n  .euiBadgeGroup--gutterExtraSmall {\n    margin: -2px;\n  }\n  .euiBadgeGroup--gutterExtraSmall > .euiBadgeGroup__item {\n    margin: 2px;\n    max-width: calc(100% - 4px);\n  }\n  .euiBadgeGroup--gutterSmall {\n    margin: -4px;\n  }\n  .euiBadgeGroup--gutterSmall > .euiBadgeGroup__item {\n    margin: 4px;\n    max-width: calc(100% - 8px);\n  }\n  .euiBetaBadge {\n    display: inline-block;\n    padding: 0 16px;\n    border-radius: 24px;\n    box-shadow: inset 0 0 0 1px #d3dae6;\n    vertical-align: super;\n    font-size: 12px;\n    font-weight: 700;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    line-height: 24px;\n    text-align: center;\n    white-space: nowrap;\n    cursor: default;\n  }\n  .euiBetaBadge:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n    outline-color: #000;\n    outline-offset: 2px;\n  }\n  .euiBetaBadge:not(.euiBetaBadge--hollow) {\n    box-shadow: none;\n  }\n  .euiBetaBadge.euiBetaBadge--small {\n    font-size: 10px;\n    font-size: 0.625rem;\n    line-height: 20px;\n    padding: 0 12px;\n  }\n  .euiBetaBadge--iconOnly {\n    padding: 0;\n    width: 24px;\n  }\n  .euiBetaBadge--iconOnly .euiBetaBadge__icon {\n    position: relative;\n    margin-top: -1px;\n  }\n  .euiBetaBadge--iconOnly.euiBetaBadge--small {\n    width: 20px;\n    padding: 0;\n  }\n  .euiBetaBadge--singleLetter {\n    padding: 0 0 0 1px;\n    width: 24px;\n  }\n  .euiBetaBadge--singleLetter.euiBetaBadge--small {\n    width: 20px;\n    padding: 0 0 0 1px;\n  }\n  .euiBetaBadge--subdued {\n    background: #e0e5ee;\n    color: #000;\n  }\n  .euiBetaBadge--subdued.euiBetaBadge-isClickable {\n    color: #000;\n  }\n  .euiBetaBadge--hollow.euiBetaBadge-isClickable {\n    color: #000;\n  }\n  .euiBetaBadge--accent {\n    background: #dd0a73;\n    color: #fff;\n  }\n  .euiBetaBadge--accent.euiBetaBadge-isClickable {\n    color: #fff;\n  }\n  .euiNotificationBadge {\n    flex-shrink: 0;\n    display: inline-block;\n    border-radius: 4px;\n    font-size: 12px;\n    font-weight: 500;\n    line-height: 16px;\n    height: 16px;\n    min-width: 16px;\n    padding-left: 4px;\n    padding-right: 4px;\n    vertical-align: middle;\n    text-align: center;\n    transition: all 150ms ease-in;\n    cursor: default;\n    background: #dd0a73;\n    color: #fff;\n  }\n  .euiNotificationBadge--medium {\n    line-height: 20px;\n    height: 20px;\n    min-width: 24px;\n  }\n  .euiNotificationBadge--subdued {\n    background: #e0e5ee;\n    color: #000;\n  }\n  .euiBasicTable-loading {\n    position: relative;\n  }\n  .euiBasicTable-loading tbody {\n    overflow: hidden;\n  }\n  .euiBasicTable-loading tbody:before {\n    position: absolute;\n    content: '';\n    width: 100%;\n    height: 2px;\n    background-color: #006bb4;\n    animation: euiBasicTableLoading 1000ms linear;\n    animation-iteration-count: infinite;\n  }\n  @keyframes euiBasicTableLoading {\n    from {\n      left: 0;\n      width: 0;\n    }\n    20% {\n      left: 0;\n      width: 40%;\n    }\n    80% {\n      left: 60%;\n      width: 40%;\n    }\n    100% {\n      left: 100%;\n      width: 0;\n    }\n  }\n  .euiBeacon {\n    position: relative;\n    background-color: #54b399;\n    border-radius: 50%;\n  }\n  .euiBeacon:before,\n  .euiBeacon:after {\n    position: absolute;\n    content: '';\n    height: 100%;\n    width: 100%;\n    left: 0;\n    top: 0;\n    background-color: rgba(0, 0, 0, 0);\n    border-radius: 50%;\n    box-shadow: 0 0 1px 1px #54b399;\n  }\n  .euiBeacon:before {\n    animation: euiBeaconPulseLarge 2.5s infinite ease-out;\n  }\n  .euiBeacon:after {\n    animation: euiBeaconPulseSmall 2.5s infinite ease-out 0.25s;\n  }\n  @keyframes euiBeaconPulseLarge {\n    0% {\n      transform: scale(0.1);\n      opacity: 1;\n    }\n    70% {\n      transform: scale(3);\n      opacity: 0;\n    }\n    100% {\n      opacity: 0;\n    }\n  }\n  @keyframes euiBeaconPulseSmall {\n    0% {\n      transform: scale(0.1);\n      opacity: 1;\n    }\n    70% {\n      transform: scale(2);\n      opacity: 0;\n    }\n    100% {\n      opacity: 0;\n    }\n  }\n  .euiBottomBar {\n    box-shadow:\n      0 0 12px -1px rgba(65, 78, 101, 0.2),\n      0 0 4px -1px rgba(65, 78, 101, 0.2),\n      0 0 2px 0 rgba(65, 78, 101, 0.2);\n    background: #25282f;\n    color: #fff;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiBottomBar {\n      animation: euiBottomBarAppear 350ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n  }\n  .euiBottomBar--fixed {\n    position: fixed;\n    z-index: 998;\n  }\n  .euiBottomBar--sticky {\n    position: sticky;\n    z-index: 998;\n  }\n  .euiBottomBar--paddingSmall {\n    padding: 8px;\n  }\n  .euiBottomBar--paddingMedium {\n    padding: 16px;\n  }\n  .euiBottomBar--paddingLarge {\n    padding: 24px;\n  }\n  @keyframes euiBottomBarAppear {\n    0% {\n      transform: translateY(100%);\n      opacity: 0;\n    }\n    100% {\n      transform: translateY(0%);\n      opacity: 1;\n    }\n  }\n  .euiButton {\n    display: inline-block;\n    appearance: none;\n    cursor: pointer;\n    height: 40px;\n    line-height: 40px;\n    text-align: center;\n    white-space: nowrap;\n    max-width: 100%;\n    vertical-align: middle;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    text-decoration: none;\n    border: solid 1px rgba(0, 0, 0, 0);\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    border-radius: 4px;\n    min-width: 112px;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiButton {\n      transition:\n        transform 250ms ease-in-out,\n        background 250ms ease-in-out;\n    }\n    .euiButton:hover:not([class*='isDisabled']) {\n      transform: translateY(-1px);\n    }\n    .euiButton:focus {\n      animation: euiButtonActive 250ms cubic-bezier(0.34, 1.61, 0.7, 1);\n    }\n    .euiButton:active:not([class*='isDisabled']) {\n      transform: translateY(1px);\n    }\n  }\n  .euiButton:hover:not([class*='isDisabled']),\n  .euiButton:focus {\n    text-decoration: underline;\n  }\n  .euiButton .euiButton__content {\n    padding: 0 12px;\n  }\n  .euiButton .euiButton__text {\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n  .euiButton.euiButton--small {\n    height: 32px;\n    line-height: 32px;\n  }\n  .euiButton:hover,\n  .euiButton:active {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 162, 179, 0.15),\n      0 2px 2px -1px rgba(152, 162, 179, 0.3);\n  }\n  .euiButton:not([class*='isDisabled']):hover,\n  .euiButton:not([class*='isDisabled']):focus,\n  .euiButton:not([class*='isDisabled']):focus-within {\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiButton.euiButton-isDisabled {\n    pointer-events: auto;\n    cursor: not-allowed;\n    color: #afb0b3;\n    border-color: #c2c3c6;\n  }\n  .euiButton.euiButton-isDisabled .euiButtonContent__icon {\n    fill: currentColor;\n  }\n  .euiButton.euiButton-isDisabled .euiButtonContent__spinner {\n    border-color: var(--euiColorPrimary) currentColor currentColor currentColor;\n  }\n  .euiButton.euiButton-isDisabled.euiButton--fill {\n    color: #88888b;\n    background-color: #c2c3c6;\n    border-color: #c2c3c6;\n  }\n  .euiButton.euiButton-isDisabled.euiButton--fill:hover,\n  .euiButton.euiButton-isDisabled.euiButton--fill:focus,\n  .euiButton.euiButton-isDisabled.euiButton--fill:focus-within {\n    background-color: #c2c3c6;\n    border-color: #c2c3c6;\n  }\n  .euiButton.euiButton-isDisabled:hover,\n  .euiButton.euiButton-isDisabled:focus,\n  .euiButton.euiButton-isDisabled:focus-within {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    text-decoration: none;\n  }\n  .euiButton--primary {\n    color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiButton--primary.euiButton--fill {\n    background-color: #006bb4;\n    border-color: #006bb4;\n    color: #fff;\n  }\n  .euiButton--primary.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--primary.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--primary.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #005c9b;\n    border-color: #005c9b;\n  }\n  .euiButton--primary:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(54, 97, 126, 0.3);\n  }\n  .euiButton--primary:not([class*='isDisabled']):hover,\n  .euiButton--primary:not([class*='isDisabled']):focus,\n  .euiButton--primary:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(54, 97, 126, 0.15),\n      0 2px 2px -1px rgba(54, 97, 126, 0.3);\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiButton--accent {\n    color: #dd0a73;\n    border-color: #dd0a73;\n  }\n  .euiButton--accent.euiButton--fill {\n    background-color: #dd0a73;\n    border-color: #dd0a73;\n    color: #fff;\n  }\n  .euiButton--accent.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--accent.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--accent.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #c50966;\n    border-color: #c50966;\n  }\n  .euiButton--accent:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(152, 79, 115, 0.3);\n  }\n  .euiButton--accent:not([class*='isDisabled']):hover,\n  .euiButton--accent:not([class*='isDisabled']):focus,\n  .euiButton--accent:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 79, 115, 0.15),\n      0 2px 2px -1px rgba(152, 79, 115, 0.3);\n    background-color: rgba(221, 10, 115, 0.1);\n  }\n  .euiButton--secondary {\n    color: #017d73;\n    border-color: #017d73;\n  }\n  .euiButton--secondary.euiButton--fill {\n    background-color: #017d73;\n    border-color: #017d73;\n    color: #fff;\n  }\n  .euiButton--secondary.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--secondary.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--secondary.euiButton--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #01645c;\n    border-color: #01645c;\n  }\n  .euiButton--secondary:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(39, 87, 83, 0.3);\n  }\n  .euiButton--secondary:not([class*='isDisabled']):hover,\n  .euiButton--secondary:not([class*='isDisabled']):focus,\n  .euiButton--secondary:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(39, 87, 83, 0.15),\n      0 2px 2px -1px rgba(39, 87, 83, 0.3);\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButton--success {\n    color: #017d73;\n    border-color: #017d73;\n  }\n  .euiButton--success.euiButton--fill {\n    background-color: #017d73;\n    border-color: #017d73;\n    color: #fff;\n  }\n  .euiButton--success.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--success.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--success.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #01645c;\n    border-color: #01645c;\n  }\n  .euiButton--success:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(39, 87, 83, 0.3);\n  }\n  .euiButton--success:not([class*='isDisabled']):hover,\n  .euiButton--success:not([class*='isDisabled']):focus,\n  .euiButton--success:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(39, 87, 83, 0.15),\n      0 2px 2px -1px rgba(39, 87, 83, 0.3);\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButton--warning {\n    color: #9b6900;\n    border-color: #f5a700;\n  }\n  .euiButton--warning.euiButton--fill {\n    background-color: #f5a700;\n    border-color: #f5a700;\n    color: #000;\n  }\n  .euiButton--warning.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--warning.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--warning.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #dc9600;\n    border-color: #dc9600;\n  }\n  .euiButton--warning:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(172, 140, 74, 0.3);\n  }\n  .euiButton--warning:not([class*='isDisabled']):hover,\n  .euiButton--warning:not([class*='isDisabled']):focus,\n  .euiButton--warning:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(172, 140, 74, 0.15),\n      0 2px 2px -1px rgba(172, 140, 74, 0.3);\n    background-color: rgba(245, 167, 0, 0.1);\n  }\n  .euiButton--danger {\n    color: #bd271e;\n    border-color: #bd271e;\n  }\n  .euiButton--danger.euiButton--fill {\n    background-color: #bd271e;\n    border-color: #bd271e;\n    color: #fff;\n  }\n  .euiButton--danger.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--danger.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--danger.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #a7221b;\n    border-color: #a7221b;\n  }\n  .euiButton--danger:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(123, 97, 96, 0.3);\n  }\n  .euiButton--danger:not([class*='isDisabled']):hover,\n  .euiButton--danger:not([class*='isDisabled']):focus,\n  .euiButton--danger:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(123, 97, 96, 0.15),\n      0 2px 2px -1px rgba(123, 97, 96, 0.3);\n    background-color: rgba(189, 39, 30, 0.1);\n  }\n  .euiButton--subdued {\n    color: #6a717d;\n    border-color: #6a717d;\n  }\n  .euiButton--subdued.euiButton--fill {\n    background-color: #6a717d;\n    border-color: #6a717d;\n    color: #fff;\n  }\n  .euiButton--subdued.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--subdued.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--subdued.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #5e656f;\n    border-color: #5e656f;\n  }\n  .euiButton--subdued:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(116, 116, 116, 0.3);\n  }\n  .euiButton--subdued:not([class*='isDisabled']):hover,\n  .euiButton--subdued:not([class*='isDisabled']):focus,\n  .euiButton--subdued:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(116, 116, 116, 0.15),\n      0 2px 2px -1px rgba(116, 116, 116, 0.3);\n    background-color: rgba(106, 113, 125, 0.1);\n  }\n  .euiButton--ghost {\n    color: #fff;\n    border-color: #fff;\n  }\n  .euiButton--ghost.euiButton--fill {\n    background-color: #fff;\n    border-color: #fff;\n    color: #000;\n  }\n  .euiButton--ghost.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--ghost.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--ghost.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #f2f2f2;\n    border-color: #f2f2f2;\n  }\n  .euiButton--ghost:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3);\n  }\n  .euiButton--ghost:not([class*='isDisabled']):hover,\n  .euiButton--ghost:not([class*='isDisabled']):focus,\n  .euiButton--ghost:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(0, 0, 0, 0.15),\n      0 2px 2px -1px rgba(0, 0, 0, 0.3);\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .euiButton--text {\n    color: #343741;\n    border-color: #69707d;\n  }\n  .euiButton--text.euiButton--fill {\n    background-color: #69707d;\n    border-color: #69707d;\n    color: #fff;\n  }\n  .euiButton--text.euiButton--fill:not([class*='isDisabled']):hover,\n  .euiButton--text.euiButton--fill:not([class*='isDisabled']):focus,\n  .euiButton--text.euiButton--fill:not([class*='isDisabled']):focus-within {\n    background-color: #5d646f;\n    border-color: #5d646f;\n  }\n  .euiButton--text:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(115, 115, 115, 0.3);\n  }\n  .euiButton--text:not([class*='isDisabled']):hover,\n  .euiButton--text:not([class*='isDisabled']):focus,\n  .euiButton--text:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(115, 115, 115, 0.15),\n      0 2px 2px -1px rgba(115, 115, 115, 0.3);\n    background-color: rgba(105, 112, 125, 0.1);\n  }\n  .euiButton.euiButton-isDisabled.euiButton--ghost,\n  .euiButton.euiButton-isDisabled.euiButton--ghost:hover,\n  .euiButton.euiButton-isDisabled.euiButton--ghost:focus,\n  .euiButton.euiButton-isDisabled.euiButton--ghost:focus-within {\n    box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3);\n    color: #69707d;\n    border-color: #69707d;\n  }\n  .euiButton.euiButton-isDisabled.euiButton--ghost.euiButton--fill {\n    background-color: #69707d;\n    color: #a1a5ae;\n  }\n  .euiButton--fullWidth {\n    display: block;\n    width: 100%;\n  }\n  .euiButtonContent {\n    height: 100%;\n    width: 100%;\n    vertical-align: middle;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n  .euiButtonContent .euiButtonContent__icon,\n  .euiButtonContent .euiButtonContent__spinner {\n    flex-shrink: 0;\n  }\n  .euiButtonContent > * + * {\n    margin-inline-start: 8px;\n  }\n  .euiButtonContent--iconRight {\n    height: 100%;\n    width: 100%;\n    vertical-align: middle;\n    flex-direction: row-reverse;\n  }\n  .euiButtonContent--iconRight .euiButtonContent__icon,\n  .euiButtonContent--iconRight .euiButtonContent__spinner {\n    flex-shrink: 0;\n  }\n  .euiButtonContent--iconRight > * + * {\n    margin-inline-start: 0;\n    margin-inline-end: 8px;\n  }\n  .euiButtonEmpty {\n    display: inline-block;\n    appearance: none;\n    cursor: pointer;\n    height: 40px;\n    line-height: 40px;\n    text-align: center;\n    white-space: nowrap;\n    max-width: 100%;\n    vertical-align: middle;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    text-decoration: none;\n    border: solid 1px rgba(0, 0, 0, 0);\n    border-color: rgba(0, 0, 0, 0);\n    background-color: rgba(0, 0, 0, 0);\n    box-shadow: none;\n    transform: none !important;\n    animation: none !important;\n    transition-timing-function: ease-in;\n    transition-duration: 150ms;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiButtonEmpty {\n      transition:\n        transform 250ms ease-in-out,\n        background 250ms ease-in-out;\n    }\n    .euiButtonEmpty:hover:not([class*='isDisabled']) {\n      transform: translateY(-1px);\n    }\n    .euiButtonEmpty:focus {\n      animation: euiButtonActive 250ms cubic-bezier(0.34, 1.61, 0.7, 1);\n    }\n    .euiButtonEmpty:active:not([class*='isDisabled']) {\n      transform: translateY(1px);\n    }\n  }\n  .euiButtonEmpty:hover:not([class*='isDisabled']),\n  .euiButtonEmpty:focus {\n    text-decoration: underline;\n  }\n  .euiButtonEmpty .euiButtonEmpty__content {\n    padding: 0 8px;\n  }\n  .euiButtonEmpty .euiButtonEmpty__text {\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n  .euiButtonEmpty.euiButtonEmpty--small {\n    height: 32px;\n  }\n  .euiButtonEmpty.euiButtonEmpty--xSmall {\n    height: 24px;\n    font-size: 14px;\n  }\n  .euiButtonEmpty:disabled {\n    pointer-events: auto;\n    cursor: not-allowed;\n    color: #afb0b3;\n  }\n  .euiButtonEmpty:disabled .euiButtonContent__icon {\n    fill: currentColor;\n  }\n  .euiButtonEmpty:disabled .euiButtonContent__spinner {\n    border-color: var(--euiColorPrimary) currentColor currentColor currentColor;\n  }\n  .euiButtonEmpty:disabled:focus {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiButtonEmpty:disabled:hover,\n  .euiButtonEmpty:disabled:focus {\n    text-decoration: none;\n  }\n  .euiButtonEmpty--flushLeft .euiButtonEmpty__content,\n  .euiButtonEmpty--flushRight .euiButtonEmpty__content,\n  .euiButtonEmpty--flushBoth .euiButtonEmpty__content {\n    padding-left: 0;\n    padding-right: 0;\n  }\n  .euiButtonEmpty--flushLeft {\n    margin-right: 8px;\n  }\n  .euiButtonEmpty--flushRight {\n    margin-left: 8px;\n  }\n  .euiButtonEmpty--primary {\n    color: #006bb4;\n  }\n  .euiButtonEmpty--primary:focus {\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiButtonEmpty--danger {\n    color: #bd271e;\n  }\n  .euiButtonEmpty--danger:focus {\n    background-color: rgba(189, 39, 30, 0.1);\n  }\n  .euiButtonEmpty--disabled {\n    color: #757577;\n  }\n  .euiButtonEmpty--disabled:focus {\n    background-color: rgba(175, 176, 179, 0.1);\n  }\n  .euiButtonEmpty--disabled:hover {\n    cursor: not-allowed;\n  }\n  .euiButtonEmpty--ghost {\n    color: #fff;\n  }\n  .euiButtonEmpty--ghost:focus {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .euiButtonEmpty--text {\n    color: #343741;\n  }\n  .euiButtonEmpty--text:focus {\n    background-color: rgba(52, 55, 65, 0.1);\n  }\n  .euiButtonEmpty--success {\n    color: #017d73;\n  }\n  .euiButtonEmpty--success:focus {\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButtonEmpty--warning {\n    color: #9b6900;\n  }\n  .euiButtonEmpty--warning:focus {\n    background-color: rgba(155, 105, 0, 0.1);\n  }\n  .euiButtonIcon {\n    display: inline-block;\n    appearance: none;\n    cursor: pointer;\n    height: 40px;\n    line-height: 40px;\n    text-align: center;\n    white-space: nowrap;\n    max-width: 100%;\n    vertical-align: middle;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    text-decoration: none;\n    border: solid 1px rgba(0, 0, 0, 0);\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    border-radius: 4px;\n    width: 40px;\n    display: inline-flex;\n    align-items: center;\n    justify-content: space-around;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiButtonIcon {\n      transition:\n        transform 250ms ease-in-out,\n        background 250ms ease-in-out;\n    }\n    .euiButtonIcon:hover:not([class*='isDisabled']) {\n      transform: translateY(-1px);\n    }\n    .euiButtonIcon:focus {\n      animation: euiButtonActive 250ms cubic-bezier(0.34, 1.61, 0.7, 1);\n    }\n    .euiButtonIcon:active:not([class*='isDisabled']) {\n      transform: translateY(1px);\n    }\n  }\n  .euiButtonIcon:hover:not([class*='isDisabled']),\n  .euiButtonIcon:focus {\n    text-decoration: underline;\n  }\n  .euiButtonIcon > svg {\n    pointer-events: none;\n  }\n  .euiButtonIcon.euiButtonIcon--empty {\n    box-shadow: none !important;\n    border: none;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled {\n    pointer-events: auto;\n    cursor: not-allowed;\n    color: #afb0b3;\n    border-color: #c2c3c6;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled .euiButtonContent__icon {\n    fill: currentColor;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled .euiButtonContent__spinner {\n    border-color: var(--euiColorPrimary) currentColor currentColor currentColor;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--fill {\n    color: #88888b;\n    background-color: #c2c3c6;\n    border-color: #c2c3c6;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--fill:hover,\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--fill:focus,\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--fill:focus-within {\n    background-color: #c2c3c6;\n    border-color: #c2c3c6;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled:hover,\n  .euiButtonIcon.euiButtonIcon-isDisabled:focus,\n  .euiButtonIcon.euiButtonIcon-isDisabled:focus-within {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    text-decoration: none;\n  }\n  .euiButtonIcon--xSmall {\n    height: 24px;\n    width: 24px;\n  }\n  .euiButtonIcon--small {\n    height: 32px;\n    width: 32px;\n  }\n  .euiButtonIcon--primary {\n    color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiButtonIcon--primary.euiButtonIcon--fill {\n    background-color: #006bb4;\n    border-color: #006bb4;\n    color: #fff;\n  }\n  .euiButtonIcon--primary.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--primary.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--primary.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #005c9b;\n    border-color: #005c9b;\n  }\n  .euiButtonIcon--primary:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(54, 97, 126, 0.3);\n  }\n  .euiButtonIcon--primary:not([class*='isDisabled']):hover,\n  .euiButtonIcon--primary:not([class*='isDisabled']):focus,\n  .euiButtonIcon--primary:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(54, 97, 126, 0.15),\n      0 2px 2px -1px rgba(54, 97, 126, 0.3);\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiButtonIcon--accent {\n    color: #dd0a73;\n    border-color: #dd0a73;\n  }\n  .euiButtonIcon--accent.euiButtonIcon--fill {\n    background-color: #dd0a73;\n    border-color: #dd0a73;\n    color: #fff;\n  }\n  .euiButtonIcon--accent.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--accent.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--accent.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #c50966;\n    border-color: #c50966;\n  }\n  .euiButtonIcon--accent:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(152, 79, 115, 0.3);\n  }\n  .euiButtonIcon--accent:not([class*='isDisabled']):hover,\n  .euiButtonIcon--accent:not([class*='isDisabled']):focus,\n  .euiButtonIcon--accent:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 79, 115, 0.15),\n      0 2px 2px -1px rgba(152, 79, 115, 0.3);\n    background-color: rgba(221, 10, 115, 0.1);\n  }\n  .euiButtonIcon--secondary {\n    color: #017d73;\n    border-color: #017d73;\n  }\n  .euiButtonIcon--secondary.euiButtonIcon--fill {\n    background-color: #017d73;\n    border-color: #017d73;\n    color: #fff;\n  }\n  .euiButtonIcon--secondary.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonIcon--secondary.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonIcon--secondary.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #01645c;\n    border-color: #01645c;\n  }\n  .euiButtonIcon--secondary:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(39, 87, 83, 0.3);\n  }\n  .euiButtonIcon--secondary:not([class*='isDisabled']):hover,\n  .euiButtonIcon--secondary:not([class*='isDisabled']):focus,\n  .euiButtonIcon--secondary:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(39, 87, 83, 0.15),\n      0 2px 2px -1px rgba(39, 87, 83, 0.3);\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButtonIcon--success {\n    color: #017d73;\n    border-color: #017d73;\n  }\n  .euiButtonIcon--success.euiButtonIcon--fill {\n    background-color: #017d73;\n    border-color: #017d73;\n    color: #fff;\n  }\n  .euiButtonIcon--success.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--success.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--success.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #01645c;\n    border-color: #01645c;\n  }\n  .euiButtonIcon--success:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(39, 87, 83, 0.3);\n  }\n  .euiButtonIcon--success:not([class*='isDisabled']):hover,\n  .euiButtonIcon--success:not([class*='isDisabled']):focus,\n  .euiButtonIcon--success:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(39, 87, 83, 0.15),\n      0 2px 2px -1px rgba(39, 87, 83, 0.3);\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButtonIcon--warning {\n    color: #9b6900;\n    border-color: #f5a700;\n  }\n  .euiButtonIcon--warning.euiButtonIcon--fill {\n    background-color: #f5a700;\n    border-color: #f5a700;\n    color: #000;\n  }\n  .euiButtonIcon--warning.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--warning.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--warning.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #dc9600;\n    border-color: #dc9600;\n  }\n  .euiButtonIcon--warning:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(172, 140, 74, 0.3);\n  }\n  .euiButtonIcon--warning:not([class*='isDisabled']):hover,\n  .euiButtonIcon--warning:not([class*='isDisabled']):focus,\n  .euiButtonIcon--warning:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(172, 140, 74, 0.15),\n      0 2px 2px -1px rgba(172, 140, 74, 0.3);\n    background-color: rgba(245, 167, 0, 0.1);\n  }\n  .euiButtonIcon--danger {\n    color: #bd271e;\n    border-color: #bd271e;\n  }\n  .euiButtonIcon--danger.euiButtonIcon--fill {\n    background-color: #bd271e;\n    border-color: #bd271e;\n    color: #fff;\n  }\n  .euiButtonIcon--danger.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--danger.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--danger.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #a7221b;\n    border-color: #a7221b;\n  }\n  .euiButtonIcon--danger:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(123, 97, 96, 0.3);\n  }\n  .euiButtonIcon--danger:not([class*='isDisabled']):hover,\n  .euiButtonIcon--danger:not([class*='isDisabled']):focus,\n  .euiButtonIcon--danger:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(123, 97, 96, 0.15),\n      0 2px 2px -1px rgba(123, 97, 96, 0.3);\n    background-color: rgba(189, 39, 30, 0.1);\n  }\n  .euiButtonIcon--subdued {\n    color: #6a717d;\n    border-color: #6a717d;\n  }\n  .euiButtonIcon--subdued.euiButtonIcon--fill {\n    background-color: #6a717d;\n    border-color: #6a717d;\n    color: #fff;\n  }\n  .euiButtonIcon--subdued.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--subdued.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--subdued.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #5e656f;\n    border-color: #5e656f;\n  }\n  .euiButtonIcon--subdued:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(116, 116, 116, 0.3);\n  }\n  .euiButtonIcon--subdued:not([class*='isDisabled']):hover,\n  .euiButtonIcon--subdued:not([class*='isDisabled']):focus,\n  .euiButtonIcon--subdued:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(116, 116, 116, 0.15),\n      0 2px 2px -1px rgba(116, 116, 116, 0.3);\n    background-color: rgba(106, 113, 125, 0.1);\n  }\n  .euiButtonIcon--ghost {\n    color: #fff;\n    border-color: #fff;\n  }\n  .euiButtonIcon--ghost.euiButtonIcon--fill {\n    background-color: #fff;\n    border-color: #fff;\n    color: #000;\n  }\n  .euiButtonIcon--ghost.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--ghost.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--ghost.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #f2f2f2;\n    border-color: #f2f2f2;\n  }\n  .euiButtonIcon--ghost:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3);\n  }\n  .euiButtonIcon--ghost:not([class*='isDisabled']):hover,\n  .euiButtonIcon--ghost:not([class*='isDisabled']):focus,\n  .euiButtonIcon--ghost:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(0, 0, 0, 0.15),\n      0 2px 2px -1px rgba(0, 0, 0, 0.3);\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .euiButtonIcon--text {\n    color: #343741;\n    border-color: #69707d;\n  }\n  .euiButtonIcon--text.euiButtonIcon--fill {\n    background-color: #69707d;\n    border-color: #69707d;\n    color: #fff;\n  }\n  .euiButtonIcon--text.euiButtonIcon--fill:not([class*='isDisabled']):hover,\n  .euiButtonIcon--text.euiButtonIcon--fill:not([class*='isDisabled']):focus,\n  .euiButtonIcon--text.euiButtonIcon--fill:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: #5d646f;\n    border-color: #5d646f;\n  }\n  .euiButtonIcon--text:not([class*='isDisabled']) {\n    box-shadow: 0 2px 2px -1px rgba(115, 115, 115, 0.3);\n  }\n  .euiButtonIcon--text:not([class*='isDisabled']):hover,\n  .euiButtonIcon--text:not([class*='isDisabled']):focus,\n  .euiButtonIcon--text:not([class*='isDisabled']):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(115, 115, 115, 0.15),\n      0 2px 2px -1px rgba(115, 115, 115, 0.3);\n    background-color: rgba(105, 112, 125, 0.1);\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--ghost,\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--ghost:hover,\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--ghost:focus,\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--ghost:focus-within {\n    box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3);\n    color: #69707d;\n    border-color: #69707d;\n  }\n  .euiButtonIcon.euiButtonIcon-isDisabled.euiButtonIcon--ghost.euiButton--fill {\n    background-color: #69707d;\n    color: #a1a5ae;\n  }\n  .euiButtonGroup {\n    display: inline-block;\n    max-width: 100%;\n    position: relative;\n  }\n  .euiButtonGroup--fullWidth {\n    display: block;\n  }\n  .euiButtonGroup--fullWidth .euiButtonGroup__buttons {\n    width: 100%;\n  }\n  .euiButtonGroup--fullWidth .euiButtonGroup__buttons .euiButtonGroupButton {\n    flex: 1;\n  }\n  .euiButtonGroup__buttons {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    border-radius: 5px;\n    max-width: 100%;\n    display: flex;\n    overflow: hidden;\n  }\n  .euiButtonGroup--isDisabled .euiButtonGroup__buttons {\n    box-shadow: none;\n  }\n  .euiButtonGroup--compressed .euiButtonGroup__buttons {\n    box-shadow: none !important;\n    border-radius: 2px;\n    background-color: #fbfcfd;\n    height: 32px;\n    border: 1px solid rgba(16, 38, 118, 0.1);\n    overflow: visible;\n  }\n  .euiButtonGroupButton {\n    display: inline-block;\n    appearance: none;\n    cursor: pointer;\n    height: 40px;\n    line-height: 40px;\n    text-align: center;\n    white-space: nowrap;\n    max-width: 100%;\n    vertical-align: middle;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    transition:\n      background-color 250ms ease-in-out,\n      border-color 250ms ease-in-out,\n      color 250ms ease-in-out;\n    min-width: 0;\n    flex-shrink: 1;\n    flex-grow: 0;\n  }\n  .euiButtonGroupButton .euiButton__content {\n    padding: 0 12px;\n  }\n  .euiButtonGroupButton-isIconOnly .euiButton__content {\n    padding: 0 8px;\n  }\n  .euiButtonGroupButton .euiButton__text {\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--small {\n    height: 32px;\n    line-height: 32px;\n  }\n  .euiButtonGroupButton:not([class*='isDisabled']):hover,\n  .euiButtonGroupButton:not([class*='isDisabled']):focus,\n  .euiButtonGroupButton:not([class*='isDisabled']):focus-within {\n    background-color: rgba(0, 107, 180, 0.1);\n    text-decoration: underline;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled {\n    pointer-events: auto;\n    cursor: not-allowed;\n    color: #afb0b3;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled\n    .euiButtonContent__icon {\n    fill: currentColor;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled\n    .euiButtonContent__spinner {\n    border-color: var(--euiColorPrimary) currentColor currentColor currentColor;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton-isSelected {\n    color: #88888b;\n    background-color: #c2c3c6;\n    border-color: #c2c3c6;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton-isSelected:focus-within {\n    background-color: #c2c3c6;\n    border-color: #c2c3c6;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ) {\n    color: #006bb4;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #006bb4;\n    border-color: #006bb4;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #005c9b;\n    border-color: #005c9b;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--primary:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ) {\n    color: #dd0a73;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #dd0a73;\n    border-color: #dd0a73;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #c50966;\n    border-color: #c50966;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--accent:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(221, 10, 115, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ) {\n    color: #017d73;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #017d73;\n    border-color: #017d73;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #01645c;\n    border-color: #01645c;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--secondary:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ) {\n    color: #017d73;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #017d73;\n    border-color: #017d73;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #01645c;\n    border-color: #01645c;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--success:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ) {\n    color: #9b6900;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #f5a700;\n    border-color: #f5a700;\n    color: #000;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #dc9600;\n    border-color: #dc9600;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--warning:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(245, 167, 0, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ) {\n    color: #bd271e;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #bd271e;\n    border-color: #bd271e;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #a7221b;\n    border-color: #a7221b;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--danger:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(189, 39, 30, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ) {\n    color: #6a717d;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #6a717d;\n    border-color: #6a717d;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #5e656f;\n    border-color: #5e656f;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--subdued:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(106, 113, 125, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not([class*='isDisabled']) {\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #fff;\n    border-color: #fff;\n    color: #000;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #f2f2f2;\n    border-color: #f2f2f2;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--ghost:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--text:not([class*='isDisabled']) {\n    color: #343741;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected {\n    background-color: #69707d;\n    border-color: #69707d;\n    color: #fff;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:hover,\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus,\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ).euiButtonGroupButton-isSelected:focus-within {\n    background-color: #5d646f;\n    border-color: #5d646f;\n  }\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ):hover,\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ):focus,\n  .euiButtonGroupButton.euiButtonGroupButton--text:not(\n      [class*='isDisabled']\n    ):focus-within {\n    background-color: rgba(105, 112, 125, 0.1);\n  }\n  .euiButtonGroupButton__textShift::after {\n    display: block;\n    content: attr(data-text);\n    font-weight: 700;\n    height: 0;\n    overflow: hidden;\n    visibility: hidden;\n  }\n  .euiButtonGroup--medium .euiButtonGroupButton,\n  .euiButtonGroup--small .euiButtonGroupButton {\n    border: 1px solid #d3dae6;\n  }\n  .euiButtonGroup--medium .euiButtonGroupButton:not(:first-child),\n  .euiButtonGroup--small .euiButtonGroupButton:not(:first-child) {\n    margin-left: -1px;\n  }\n  .euiButtonGroup--medium .euiButtonGroupButton:first-child,\n  .euiButtonGroup--small .euiButtonGroupButton:first-child {\n    border-radius: 4px 0 0 4px;\n  }\n  .euiButtonGroup--medium .euiButtonGroupButton:last-child,\n  .euiButtonGroup--small .euiButtonGroupButton:last-child {\n    border-radius: 0 4px 4px 0;\n  }\n  .euiButtonGroup--medium\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost,\n  .euiButtonGroup--medium\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost:hover,\n  .euiButtonGroup--medium\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost:focus,\n  .euiButtonGroup--medium\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost:focus-within,\n  .euiButtonGroup--small\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost,\n  .euiButtonGroup--small\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost:hover,\n  .euiButtonGroup--small\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost:focus,\n  .euiButtonGroup--small\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost:focus-within {\n    color: #69707d;\n  }\n  .euiButtonGroup--isDisabled\n    .euiButtonGroup--medium\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost,\n  .euiButtonGroup--isDisabled\n    .euiButtonGroup--small\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost {\n    border-color: #69707d;\n  }\n  .euiButtonGroup--medium\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost.euiButtonGroupButton-isSelected,\n  .euiButtonGroup--small\n    .euiButtonGroupButton.euiButtonGroupButton-isDisabled.euiButtonGroupButton--ghost.euiButtonGroupButton-isSelected {\n    background-color: #69707d;\n    color: #a1a5ae;\n  }\n  .euiButtonGroup--medium .euiButtonGroupButton-isSelected,\n  .euiButtonGroup--small .euiButtonGroupButton-isSelected {\n    z-index: 0;\n  }\n  .euiButtonGroup--medium\n    .euiButtonGroupButton-isSelected\n    + .euiButtonGroupButton-isSelected,\n  .euiButtonGroup--small\n    .euiButtonGroupButton-isSelected\n    + .euiButtonGroupButton-isSelected {\n    box-shadow: -1px 0 0 rgba(255, 255, 255, 0.1);\n  }\n  .euiButtonGroup--compressed .euiButtonGroupButton {\n    height: 30px;\n    line-height: 30px;\n    font-size: 14px;\n    border-radius: 4px;\n    padding: 2px;\n    background-clip: content-box;\n  }\n  .euiButtonGroup--compressed .euiButtonGroupButton .euiButton__content {\n    padding-left: 8px;\n    padding-right: 8px;\n  }\n  .euiButtonGroup--compressed\n    .euiButtonGroupButton.euiButtonGroupButton-isSelected {\n    font-weight: 600;\n  }\n  .euiButtonGroup--compressed\n    .euiButtonGroupButton:not([class*='isDisabled']):focus,\n  .euiButtonGroup--compressed\n    .euiButtonGroupButton:not([class*='isDisabled']):focus-within {\n    outline: 2px solid rgba(0, 107, 180, 0.3);\n  }\n  .euiBreadcrumbs {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    margin-bottom: -4px;\n    display: flex;\n    align-items: center;\n    flex-wrap: wrap;\n    min-width: 0;\n  }\n  .euiBreadcrumb {\n    display: inline-block;\n    margin-bottom: 4px;\n  }\n  .euiBreadcrumb:not(.euiBreadcrumb--last) {\n    margin-right: 8px;\n    color: #6a717d;\n  }\n  .euiBreadcrumb--last {\n    font-weight: 500;\n  }\n  .euiBreadcrumb--collapsed {\n    flex-shrink: 0;\n  }\n  .euiBreadcrumbSeparator {\n    flex-shrink: 0;\n    display: inline-block;\n    margin-right: 8px;\n    width: 1px;\n    height: 16px;\n    transform: translateY(-1px) rotate(15deg);\n    background: #d3dae6;\n  }\n  .euiBreadcrumbs__inPopover .euiBreadcrumb--last {\n    font-weight: 400;\n    color: #69707d !important;\n  }\n  .euiBreadcrumbs--truncate {\n    white-space: nowrap;\n    flex-wrap: nowrap;\n  }\n  .euiBreadcrumbs--truncate .euiBreadcrumb:not(.euiBreadcrumb--collapsed) {\n    max-width: 160px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .euiBreadcrumbs--truncate\n    .euiBreadcrumb:not(.euiBreadcrumb--collapsed).euiBreadcrumb--last {\n    max-width: none;\n  }\n  .euiBreadcrumb--truncate {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    max-width: 160px;\n    text-align: center;\n    vertical-align: top;\n  }\n  .euiCallOut {\n    padding: 16px;\n    border-left: 2px solid rgba(0, 0, 0, 0);\n  }\n  .euiCallOut.euiCallOut--small {\n    padding: 8px;\n  }\n  .euiCallOut .euiCallOutHeader__icon {\n    flex: 0 0 auto;\n    transform: translateY(2px);\n  }\n  .euiCallOut .euiCallOutHeader__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    font-weight: 400;\n    margin-bottom: 0;\n  }\n  .euiCallOut--small .euiCallOutHeader__title {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    font-weight: 400;\n  }\n  .euiCallOut--primary {\n    border-color: #006bb4;\n    background-color: #e6f0f8;\n  }\n  .euiCallOut--primary .euiCallOutHeader__icon {\n    fill: #006bb4;\n  }\n  .euiCallOut--primary .euiCallOutHeader__title {\n    color: #006bb4;\n  }\n  .euiCallOut--success {\n    border-color: #017d73;\n    background-color: #e6f2f1;\n  }\n  .euiCallOut--success .euiCallOutHeader__icon {\n    fill: #01776d;\n  }\n  .euiCallOut--success .euiCallOutHeader__title {\n    color: #01776d;\n  }\n  .euiCallOut--warning {\n    border-color: #f5a700;\n    background-color: #fef6e6;\n  }\n  .euiCallOut--warning .euiCallOutHeader__icon {\n    fill: #936400;\n  }\n  .euiCallOut--warning .euiCallOutHeader__title {\n    color: #936400;\n  }\n  .euiCallOut--danger {\n    border-color: #bd271e;\n    background-color: #f8e9e9;\n  }\n  .euiCallOut--danger .euiCallOutHeader__icon {\n    fill: #bd271e;\n  }\n  .euiCallOut--danger .euiCallOutHeader__title {\n    color: #bd271e;\n  }\n  .euiCallOutHeader {\n    display: flex;\n    align-items: baseline;\n  }\n  .euiCallOutHeader + * {\n    margin-top: 8px;\n  }\n  .euiCallOutHeader > * + * {\n    margin-left: 8px;\n  }\n  .euiCard {\n    display: flex;\n    flex-direction: column;\n    min-height: 1px;\n  }\n  .euiCard.euiCard-isDisabled {\n    cursor: not-allowed !important;\n    transform: none !important;\n    box-shadow: none !important;\n    text-decoration: none !important;\n    background-color: rgba(194, 195, 198, 0.1) !important;\n    color: #afb0b3;\n  }\n  .euiCard.euiCard-isDisabled .euiCard__top {\n    filter: grayscale(100%);\n  }\n  .euiCard.euiCard-isDisabled .euiCard__titleAnchor,\n  .euiCard.euiCard-isDisabled .euiCard__titleButton {\n    color: #afb0b3;\n    cursor: inherit;\n  }\n  .euiCard.euiCard-isDisabled\n    .euiCard__betaBadge:not(.euiBetaBadge-isClickable):not(\n      .euiBetaBadge--hollow\n    ) {\n    box-shadow: inset 0 0 0 1px #d3dae6;\n    background: rgba(0, 0, 0, 0);\n    color: inherit;\n  }\n  .euiCard.euiCard-isDisabled\n    .euiCard__betaBadge:not(.euiBetaBadge-isClickable).euiBetaBadge--hollow {\n    background-color: #fff;\n  }\n  .euiCard.euiCard--isClickable {\n    display: flex;\n    width: 100%;\n  }\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):focus-within {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 162, 179, 0.15),\n      0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimateLarge !important;\n  }\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):focus .euiCard__title,\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):focus\n    .euiCard__titleAnchor,\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):focus\n    .euiCard__titleButton,\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):hover .euiCard__title,\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):hover\n    .euiCard__titleAnchor,\n  .euiCard.euiCard--isClickable:not(.euiCard-isDisabled):hover\n    .euiCard__titleButton {\n    text-decoration: underline;\n  }\n  .euiCard .euiCard__top,\n  .euiCard .euiCard__content,\n  .euiCard .euiCard__footer {\n    width: 100%;\n  }\n  .euiCard.euiCard--leftAligned {\n    text-align: left;\n    align-items: flex-start;\n  }\n  .euiCard.euiCard--leftAligned .euiCard__titleButton {\n    text-align: left;\n  }\n  .euiCard.euiCard--centerAligned {\n    text-align: center;\n    align-items: center;\n  }\n  .euiCard.euiCard--rightAligned {\n    text-align: right;\n    align-items: flex-end;\n  }\n  .euiCard.euiCard--rightAligned .euiCard__titleButton {\n    text-align: right;\n  }\n  .euiCard.euiCard-isSelected {\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiCard--hasBetaBadge {\n    position: relative;\n    overflow: visible;\n  }\n  .euiCard--hasBetaBadge .euiCard__betaBadgeWrapper {\n    position: absolute;\n    top: -12px;\n    left: 50%;\n    transform: translateX(-50%);\n    z-index: 3;\n    min-width: 30%;\n    max-width: calc(100% - 32px);\n  }\n  .euiCard--hasBetaBadge .euiCard__betaBadgeWrapper .euiCard__betaBadge {\n    width: 100%;\n  }\n  .euiCard--hasBetaBadge .euiCard__betaBadgeWrapper .euiCard__betaBadge {\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .euiCard__betaBadge.euiBetaBadge--hollow {\n    background-color: #fff;\n  }\n  .euiCard--isSelectable {\n    position: relative;\n  }\n  .euiCard[class*='paddingSmall'] {\n    padding: 8px;\n  }\n  .euiCard[class*='paddingSmall'].euiCard--isSelectable {\n    padding-bottom: 48px;\n  }\n  .euiCard[class*='paddingSmall'] .euiCard__top .euiCard__image {\n    width: calc(100% + 8px * 2);\n    left: -8px;\n    top: -8px;\n    margin-bottom: -8px;\n  }\n  .euiCard[class*='paddingSmall']\n    .euiCard__top\n    .euiCard__image\n    + .euiCard__icon {\n    transform: translate(-50%, -75%);\n    transform: translate(-50%, calc(-50% + -8px));\n  }\n  .euiCard[class*='paddingMedium'] {\n    padding: 16px;\n  }\n  .euiCard[class*='paddingMedium'].euiCard--isSelectable {\n    padding-bottom: 56px;\n  }\n  .euiCard[class*='paddingMedium'] .euiCard__top .euiCard__image {\n    width: calc(100% + 16px * 2);\n    left: -16px;\n    top: -16px;\n    margin-bottom: -16px;\n  }\n  .euiCard[class*='paddingMedium']\n    .euiCard__top\n    .euiCard__image\n    + .euiCard__icon {\n    transform: translate(-50%, -75%);\n    transform: translate(-50%, calc(-50% + -16px));\n  }\n  .euiCard[class*='paddingLarge'] {\n    padding: 24px;\n  }\n  .euiCard[class*='paddingLarge'].euiCard--isSelectable {\n    padding-bottom: 64px;\n  }\n  .euiCard[class*='paddingLarge'] .euiCard__top .euiCard__image {\n    width: calc(100% + 24px * 2);\n    left: -24px;\n    top: -24px;\n    margin-bottom: -24px;\n  }\n  .euiCard[class*='paddingLarge']\n    .euiCard__top\n    .euiCard__image\n    + .euiCard__icon {\n    transform: translate(-50%, -75%);\n    transform: translate(-50%, calc(-50% + -24px));\n  }\n  .euiCard__top {\n    flex-grow: 0;\n    position: relative;\n    min-height: 1px;\n    font-size: 0;\n  }\n  .euiCard__top .euiCard__image {\n    position: relative;\n    border-top-left-radius: 3px;\n    border-top-right-radius: 3px;\n    overflow: hidden;\n  }\n  .euiCard__top .euiCard__image img {\n    width: 100%;\n  }\n  .euiCard__top .euiCard__image + .euiCard__icon {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n  }\n  .euiCard__top .euiCard__icon {\n    margin-top: 8px;\n  }\n  .euiCard__footer:not(:empty) {\n    flex-grow: 0;\n    margin-top: 16px;\n  }\n  .euiCard[class*='transparent'] .euiCard__image {\n    border-radius: 4px;\n  }\n  .euiCard--isSelectable--text.euiCard-isSelected:not(.euiCard-isDisabled) {\n    border-color: #017d73 !important;\n  }\n  .euiCard--isSelectable--primary.euiCard-isSelected:not(.euiCard-isDisabled) {\n    border-color: #006bb4 !important;\n  }\n  .euiCard--isSelectable--success.euiCard-isSelected:not(.euiCard-isDisabled) {\n    border-color: #017d73 !important;\n  }\n  .euiCard--isSelectable--danger.euiCard-isSelected:not(.euiCard-isDisabled) {\n    border-color: #bd271e !important;\n  }\n  .euiCard--isSelectable--ghost.euiCard-isSelected:not(.euiCard-isDisabled) {\n    border-color: #69707d !important;\n  }\n  .euiCard__top + .euiCard__content {\n    margin-top: 16px;\n  }\n  .euiCard__content {\n    flex-grow: 1;\n  }\n  .euiCard__content .euiCard__description,\n  .euiCard__content .euiCard__children {\n    margin-top: 8px;\n  }\n  .euiCard__content .euiCard__titleAnchor,\n  .euiCard__content .euiCard__titleButton {\n    font: inherit;\n    color: inherit;\n    letter-spacing: inherit;\n  }\n  .euiCard__content .euiCard__titleAnchor:focus,\n  .euiCard__content .euiCard__titleButton:focus {\n    text-decoration: underline;\n  }\n  .euiCard.euiCard--horizontal .euiCard__content,\n  .euiCard.euiCard--horizontal .euiCard__titleButton {\n    text-align: left;\n  }\n  .euiCard.euiCard--horizontal.euiCard--hasIcon {\n    flex-direction: row;\n    align-items: flex-start !important;\n  }\n  .euiCard.euiCard--horizontal.euiCard--hasIcon .euiCard__top,\n  .euiCard.euiCard--horizontal.euiCard--hasIcon .euiCard__content {\n    width: auto;\n    margin-top: 0;\n  }\n  .euiCard.euiCard--horizontal.euiCard--hasIcon .euiCard__top .euiCard__icon {\n    margin-top: 0;\n    margin-right: 16px;\n  }\n  .euiCardSelect {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    height: 40px !important;\n    width: 100%;\n    overflow: hidden;\n    border-bottom-left-radius: 3px;\n    border-bottom-right-radius: 3px;\n    font-weight: 700;\n  }\n  .euiCardSelect--text:enabled {\n    background-color: #f5f7fa;\n  }\n  .euiCardSelect--primary:enabled {\n    background-color: #e6f0f8;\n  }\n  .euiCardSelect--success:enabled {\n    background-color: #e6f2f1;\n    color: #01776d;\n  }\n  .euiCardSelect--danger:enabled {\n    background-color: #f8e9e9;\n  }\n  .euiCardSelect--ghost:enabled {\n    background-color: #69707d;\n  }\n  .euiCardSelect:disabled {\n    background-color: #fafbfd;\n  }\n  .euiCheckableCard {\n    transition: border-color 250ms ease-in;\n  }\n  .euiCheckableCard:not(\n      .euiCheckableCard-isDisabled\n    ).euiCheckableCard-isChecked {\n    border-color: #006bb4;\n  }\n  .euiCheckableCard__label {\n    cursor: pointer;\n    display: block;\n    width: calc(100% + 32px);\n    padding: 16px;\n    margin: -16px;\n  }\n  .euiCheckableCard__label-isDisabled {\n    color: #98a2b3;\n    cursor: not-allowed;\n  }\n  .euiCheckableCard__children {\n    margin-top: 16px;\n  }\n  .euiCodeBlock {\n    max-width: 100%;\n    display: block;\n    position: relative;\n    background: #f5f7fa;\n    color: #343741;\n  }\n  .euiCodeBlock .euiCodeBlock__pre {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow: auto;\n    display: block;\n  }\n  .euiCodeBlock .euiCodeBlock__pre::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiCodeBlock .euiCodeBlock__pre::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiCodeBlock .euiCodeBlock__pre::-webkit-scrollbar-corner,\n  .euiCodeBlock .euiCodeBlock__pre::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiCodeBlock .euiCodeBlock__pre--whiteSpacePre {\n    white-space: pre;\n  }\n  .euiCodeBlock .euiCodeBlock__pre--whiteSpacePreWrap {\n    white-space: pre-wrap;\n  }\n  .euiCodeBlock .euiCodeBlock__code {\n    font-family: 'Roboto Mono', Consolas, Menlo, Courier, monospace;\n    letter-spacing: normal;\n    display: block;\n    line-height: 1.5;\n    font-weight: 400;\n    font-size: inherit;\n  }\n  .euiCodeBlock .euiCodeBlock__controls {\n    position: absolute;\n    top: 0;\n    right: 0;\n  }\n  .euiCodeBlock .euiCodeBlock__fullScreenButton + .euiCodeBlock__copyButton {\n    margin-top: 4px;\n  }\n  .euiCodeBlock .euiCodeBlock__line {\n    display: block;\n  }\n  .euiCodeBlock.euiCodeBlock-isFullScreen {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n  }\n  .euiCodeBlock.euiCodeBlock-isFullScreen .euiCodeBlock__pre {\n    padding: 32px !important;\n  }\n  .euiCodeBlock.euiCodeBlock-isFullScreen .euiCodeBlock__controls {\n    top: 4px;\n    right: 4px;\n  }\n  .euiCodeBlock.euiCodeBlock--fontSmall {\n    font-size: 12px;\n  }\n  .euiCodeBlock.euiCodeBlock--fontMedium {\n    font-size: 14px;\n  }\n  .euiCodeBlock.euiCodeBlock--fontLarge {\n    font-size: 16px;\n  }\n  .euiCodeBlock.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePreWrap {\n    padding-right: 28px;\n  }\n  .euiCodeBlock.euiCodeBlock--hasControls .euiCodeBlock__pre--whiteSpacePre {\n    margin-right: 28px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingSmall .euiCodeBlock__pre {\n    padding: 8px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingSmall .euiCodeBlock__controls {\n    top: 8px;\n    right: 8px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingSmall.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePreWrap {\n    padding-right: 36px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingSmall.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePre {\n    margin-right: 36px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingMedium .euiCodeBlock__pre {\n    padding: 16px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingMedium .euiCodeBlock__controls {\n    top: 16px;\n    right: 16px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingMedium.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePreWrap {\n    padding-right: 44px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingMedium.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePre {\n    margin-right: 44px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingLarge .euiCodeBlock__pre {\n    padding: 24px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingLarge .euiCodeBlock__controls {\n    top: 24px;\n    right: 24px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingLarge.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePreWrap {\n    padding-right: 52px;\n  }\n  .euiCodeBlock.euiCodeBlock--paddingLarge.euiCodeBlock--hasControls\n    .euiCodeBlock__pre--whiteSpacePre {\n    margin-right: 52px;\n  }\n  .euiCodeBlock.euiCodeBlock--inline {\n    display: inline-block;\n    white-space: pre;\n    color: #343741;\n    font-size: 90%;\n    padding: 0 8px;\n    background: #f5f7fa;\n  }\n  .euiCodeBlock.euiCodeBlock--inline .euiCodeBlock__pre {\n    padding: 0 4px;\n  }\n  .euiCodeBlock.euiCodeBlock--inline .euiCodeBlock__code {\n    display: inline;\n    white-space: normal;\n  }\n  .euiCodeBlock.euiCodeBlock--transparentBackground {\n    background: rgba(0, 0, 0, 0);\n  }\n  .euiCodeBlock\n    .token.punctuation:not(.interpolation-punctuation):not([class*='attr-']) {\n    opacity: 0.7;\n  }\n  .euiCodeBlock .token.comment,\n  .euiCodeBlock .token.prolog,\n  .euiCodeBlock .token.doctype,\n  .euiCodeBlock .token.cdata,\n  .euiCodeBlock .token.coord,\n  .euiCodeBlock .token.blockquote {\n    color: #6a717d;\n    font-style: italic;\n  }\n  .euiCodeBlock .token.selector {\n    color: inherit;\n  }\n  .euiCodeBlock .token.string,\n  .euiCodeBlock .token.interpolation,\n  .euiCodeBlock .token.interpolation-punctuation,\n  .euiCodeBlock .token.doc-comment .token.keyword,\n  .euiCodeBlock .token.attr-value,\n  .euiCodeBlock .token.url .token.content {\n    color: #ac4e6d;\n  }\n  .euiCodeBlock .token.number,\n  .euiCodeBlock .token.boolean,\n  .euiCodeBlock .token.keyword.nil,\n  .euiCodeBlock .token.regex,\n  .euiCodeBlock .token.variable,\n  .euiCodeBlock .token.unit,\n  .euiCodeBlock .token.hexcode,\n  .euiCodeBlock .token.attr-name,\n  .euiCodeBlock .token.attr-equals {\n    color: #3b7d6a;\n  }\n  .euiCodeBlock .token.atrule .token.rule,\n  .euiCodeBlock .token.keyword {\n    color: #7c609e;\n  }\n  .euiCodeBlock .token.function {\n    color: inherit;\n  }\n  .euiCodeBlock .token.tag {\n    color: #4a7194;\n  }\n  .euiCodeBlock .token.class-name {\n    color: #4a7194;\n  }\n  .euiCodeBlock .token.property {\n    color: inherit;\n  }\n  .euiCodeBlock .token.console,\n  .euiCodeBlock .token.list-punctuation,\n  .euiCodeBlock .token.url-reference,\n  .euiCodeBlock .token.url .token.url {\n    color: #b34f3b;\n  }\n  .euiCodeBlock .token.paramater {\n    color: inherit;\n  }\n  .euiCodeBlock .token.meta,\n  .euiCodeBlock .token.important {\n    color: #6a717d;\n  }\n  .euiCodeBlock .token.title {\n    color: #996130;\n  }\n  .euiCodeBlock .token.section {\n    color: #b34f3b;\n  }\n  .euiCodeBlock .token.prefix.inserted,\n  .euiCodeBlock .token.prefix.deleted {\n    padding-left: 4px;\n    margin-left: -4px;\n  }\n  .euiCodeBlock .token.prefix.inserted {\n    box-shadow: -4px 0 #3b7d6a;\n    color: #3b7d6a;\n  }\n  .euiCodeBlock .token.prefix.deleted {\n    box-shadow: -4px 0 #bd271e;\n    color: #bd271e;\n  }\n  .euiCodeBlock .token.selector .token.class {\n    color: inherit;\n  }\n  .euiCodeBlock .token.selector .token.id {\n    color: inherit;\n  }\n  .euiCodeBlock .token.italic {\n    font-style: italic;\n  }\n  .euiCodeBlock .token.important,\n  .euiCodeBlock .token.bold {\n    font-weight: 700;\n  }\n  .euiCodeBlock .token.url-reference,\n  .euiCodeBlock .token.url .token.url {\n    text-decoration: underline;\n  }\n  .euiCodeBlock .token.entity {\n    cursor: help;\n  }\n  .euiCodeEditorWrapper {\n    position: relative;\n  }\n  .euiCodeEditorWrapper .ace_hidden-cursors {\n    opacity: 0;\n  }\n  .euiCodeEditorWrapper.euiCodeEditorWrapper-isEditing .ace_hidden-cursors {\n    opacity: 1;\n  }\n  .euiCodeEditorKeyboardHint {\n    position: absolute;\n    top: 0;\n    left: 0;\n    background: rgba(255, 255, 255, 0.7);\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    opacity: 0;\n    cursor: pointer;\n    height: 100%;\n    width: 100%;\n  }\n  .euiCodeEditorKeyboardHint:focus {\n    opacity: 1;\n    border: 2px solid #006bb4;\n    z-index: 1000;\n  }\n  .euiCodeEditorKeyboardHint.euiCodeEditorKeyboardHint-isInactive {\n    display: none;\n  }\n  .euiCollapsibleNav:not([class*='push']) {\n    z-index: 6000 !important;\n  }\n  .euiCollapsibleNavGroup:not(:first-child) {\n    border-top: 1px solid #d3dae6;\n  }\n  .euiCollapsibleNavGroup .euiAccordion__triggerWrapper {\n    padding: 16px;\n  }\n  .euiCollapsibleNavGroup--light {\n    background-color: #fafbfd;\n  }\n  .euiCollapsibleNavGroup--dark {\n    background-color: #2a2c34;\n    color: #fff;\n  }\n  .euiCollapsibleNavGroup--dark\n    .euiCollapsibleNavGroup__heading:focus\n    .euiAccordion__iconWrapper {\n    color: #2580bf;\n    animation-name: euiCollapsibleNavGroupDarkFocusRingAnimate !important;\n  }\n  .euiCollapsibleNavGroup--dark .euiCollapsibleNavGroup__title {\n    color: inherit;\n    line-height: inherit;\n  }\n  .euiCollapsibleNavGroup__heading {\n    font-weight: 600;\n  }\n  .euiCollapsibleNavGroup__heading:not(.euiAccordion__button) {\n    padding: 16px;\n  }\n  .euiCollapsibleNavGroup__children {\n    padding: 8px;\n  }\n  .euiCollapsibleNavGroup--withHeading .euiCollapsibleNavGroup__children {\n    padding-top: 0;\n  }\n  @keyframes euiCollapsibleNavGroupDarkFocusRingAnimate {\n    0% {\n      box-shadow: 0 0 0 6px rgba(0, 107, 180, 0);\n    }\n    100% {\n      box-shadow: 0 0 0 3px #2580bf;\n    }\n  }\n  .euiColorPicker {\n    position: relative;\n    width: 152px;\n  }\n  .euiColorPicker__popoverAnchor .euiColorPicker__input {\n    padding-right: 40px;\n  }\n  .euiColorPicker__popoverAnchor .euiColorPicker__input[class*='--compressed'] {\n    padding-right: 32px;\n  }\n  .euiColorPicker__popoverAnchor\n    .euiColorPicker__input\n    + .euiFormControlLayoutIcons {\n    color: inherit;\n  }\n  .euiSwatchInput__stroke {\n    fill: none;\n    stroke: rgba(0, 0, 0, 0.2);\n  }\n  .euiColorPicker__popoverPanel--pickerOnly {\n    padding-bottom: 0 !important;\n  }\n  .euiColorPicker__input--inGroup {\n    height: 38px !important;\n    box-shadow: none !important;\n    border-radius: 0;\n  }\n  .euiColorPicker__input--inGroup.euiFieldText--compressed {\n    height: 30px !important;\n    border-radius: 0;\n  }\n  .euiColorPicker__alphaRange .euiRangeInput {\n    min-width: 0;\n  }\n  .euiColorPickerSwatch {\n    display: inline-block;\n    height: 24px;\n    width: 24px;\n    border-radius: 2px;\n    cursor: pointer;\n    border: solid 1px rgba(0, 0, 0, 0.1);\n    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);\n  }\n  .euiColorPickerSwatch:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiHue {\n    background: linear-gradient(\n      to right,\n      #ff3232 0%,\n      #fff130 20%,\n      #45ff30 35%,\n      #28fff0 52%,\n      #282cff 71%,\n      #ff28fb 88%,\n      #ff0094 100%\n    );\n    height: 24px;\n    margin: 4px 0;\n    position: relative;\n  }\n  .euiHue:before,\n  .euiHue:after {\n    content: '';\n    left: 0;\n    position: absolute;\n    height: 8px;\n    background: #fff;\n    width: 100%;\n  }\n  .euiHue:after {\n    bottom: 0;\n  }\n  .euiHue__range {\n    position: relative;\n    height: 24px;\n    width: calc(100% + 2px);\n    margin: 0 -1px;\n    appearance: none;\n    background: rgba(0, 0, 0, 0);\n    z-index: 2;\n  }\n  .euiHue__range::-webkit-slider-thumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n  }\n  .euiHue__range::-moz-range-thumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n  }\n  .euiHue__range::-ms-thumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n  }\n  .euiHue__range::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    margin-top: 0;\n  }\n  .euiHue__range::-ms-thumb {\n    margin-top: 0;\n  }\n  .euiHue__range::-ms-track {\n    height: 24px;\n    background: rgba(0, 0, 0, 0);\n    border-color: rgba(0, 0, 0, 0);\n    color: rgba(0, 0, 0, 0);\n  }\n  .euiHue__range::-moz-focus-outer {\n    border: none;\n  }\n  .euiHue__range::-ms-fill-lower,\n  .euiHue__range::-ms-fill-upper {\n    background: rgba(0, 0, 0, 0);\n  }\n  .euiHue__range:focus {\n    outline: none;\n  }\n  .euiHue__range:focus::-webkit-slider-thumb {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n    border-color: #006bb4;\n  }\n  .euiHue__range:focus::-moz-range-thumb {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n    border-color: #006bb4;\n  }\n  .euiHue__range:focus::-ms-thumb {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n    border-color: #006bb4;\n  }\n  .euiSaturation {\n    position: relative;\n    width: 100%;\n    padding-bottom: 100%;\n    border-radius: 2px;\n    touch-action: none;\n    z-index: 3;\n  }\n  .euiSaturation .euiSaturation__lightness,\n  .euiSaturation .euiSaturation__saturation {\n    position: absolute;\n    top: -1px;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    border-radius: 2px;\n  }\n  .euiSaturation .euiSaturation__lightness {\n    background: linear-gradient(\n      to right,\n      rgb(255, 255, 255),\n      rgba(255, 255, 255, 0)\n    );\n  }\n  .euiSaturation .euiSaturation__saturation {\n    background: linear-gradient(to top, rgb(0, 0, 0), rgba(0, 0, 0, 0));\n  }\n  .euiSaturation .euiSaturation__indicator {\n    position: absolute;\n    height: 12px;\n    width: 12px;\n    border-radius: 100%;\n    margin-top: -6px;\n    margin-left: -6px;\n    border: 1px solid #343741;\n  }\n  .euiSaturation .euiSaturation__indicator:before {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    border-radius: 100%;\n    border: 1px solid #f5f7fa;\n  }\n  .euiSaturation:focus {\n    outline: none;\n  }\n  .euiSaturation:focus .euiSaturation__indicator {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n    border-color: #006bb4;\n  }\n  .euiColorStops:not(.euiColorStops-isDisabled):focus {\n    outline: 2px solid rgba(0, 107, 180, 0.3);\n  }\n  .euiColorStops__addContainer {\n    display: block;\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 50%;\n    height: 16px;\n    margin-top: -8px;\n  }\n  .euiColorStops__addContainer:hover:not(\n      .euiColorStops__addContainer-isDisabled\n    ) {\n    cursor: pointer;\n  }\n  .euiColorStops__addContainer:hover:not(\n      .euiColorStops__addContainer-isDisabled\n    )\n    .euiColorStops__addTarget {\n    opacity: 0.7;\n  }\n  .euiColorStops__addTarget {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n    position: absolute;\n    top: 0;\n    height: 16px;\n    width: 16px;\n    background-color: #f5f7fa;\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity 150ms;\n  }\n  .euiColorStop {\n    width: 152px;\n  }\n  .euiColorStopPopover.euiPopover {\n    position: absolute;\n    top: 50%;\n    width: 16px;\n    height: 16px;\n    margin-top: -8px;\n  }\n  .euiColorStopPopover-hasFocus {\n    z-index: 1;\n  }\n  .euiColorStopPopover__anchor {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n  }\n  .euiColorStopPopover__anchor:before {\n    content: '';\n    display: block;\n    position: absolute;\n    left: 0;\n    top: 0;\n    height: 16px;\n    width: 16px;\n    border-radius: 16px;\n    background: #fff;\n  }\n  .euiColorStopThumb.euiRangeThumb:not(:disabled) {\n    top: 0;\n    margin-top: 0;\n    pointer-events: auto;\n    cursor: grab;\n    border: solid 3px #fff;\n    box-shadow:\n      0 0 0 1px #98a2b3,\n      0 2px 2px -1px rgba(152, 162, 179, 0.2),\n      0 1px 5px -2px rgba(152, 162, 179, 0.2);\n  }\n  .euiColorStopThumb.euiRangeThumb:not(:disabled):active {\n    cursor: grabbing;\n  }\n  .euiColorStopPopover-isLoadingPanel {\n    visibility: hidden !important;\n  }\n  .euiColorStops.euiColorStops-isDragging:not(.euiColorStops-isDisabled):not(\n      .euiColorStops-isReadOnly\n    ) {\n    cursor: grabbing;\n  }\n  .euiColorStops__highlight {\n    color: #69707d;\n  }\n  .euiColorPalettePicker__itemTitle {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiColorPalettePicker__itemTitle + .euiColorPaletteDisplay {\n    margin-top: 4px;\n  }\n  .euiColorPaletteDisplay {\n    display: flex;\n    flex-direction: row;\n    overflow: hidden;\n    height: 8px;\n  }\n  .euiColorPaletteDisplay--sizeExtraSmall {\n    position: relative;\n    height: 4px;\n    border-radius: 4px;\n  }\n  .euiColorPaletteDisplay--sizeExtraSmall:after {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    border-radius: 4px;\n    content: '';\n    pointer-events: none;\n    border: 1px solid rgba(52, 55, 65, 0.2);\n  }\n  .euiColorPaletteDisplay--sizeExtraSmall\n    .euiColorPaletteDisplayFixed__bleedArea {\n    height: 4px;\n  }\n  .euiColorPaletteDisplay--sizeSmall {\n    position: relative;\n    height: 8px;\n    border-radius: 8px;\n  }\n  .euiColorPaletteDisplay--sizeSmall:after {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    border-radius: 8px;\n    content: '';\n    pointer-events: none;\n    border: 1px solid rgba(52, 55, 65, 0.2);\n  }\n  .euiColorPaletteDisplay--sizeSmall .euiColorPaletteDisplayFixed__bleedArea {\n    height: 8px;\n  }\n  .euiColorPaletteDisplay--sizeMedium {\n    position: relative;\n    height: 16px;\n    border-radius: 16px;\n  }\n  .euiColorPaletteDisplay--sizeMedium:after {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    border-radius: 16px;\n    content: '';\n    pointer-events: none;\n    border: 1px solid rgba(52, 55, 65, 0.2);\n  }\n  .euiColorPaletteDisplay--sizeMedium .euiColorPaletteDisplayFixed__bleedArea {\n    height: 16px;\n  }\n  .euiColorPaletteDisplayFixed__bleedArea {\n    position: absolute;\n    top: 0;\n    left: 0;\n    display: flex;\n    height: 8px;\n    width: calc(100% + 1px);\n  }\n  .euiComboBox {\n    max-width: 400px;\n    width: 100%;\n    height: auto;\n    position: relative;\n  }\n  .euiComboBox--fullWidth {\n    max-width: 100%;\n  }\n  .euiComboBox--compressed {\n    height: 32px;\n  }\n  .euiComboBox--inGroup {\n    height: 100%;\n  }\n  .euiComboBox--compressed,\n  .euiComboBox .euiFormControlLayout {\n    height: auto;\n  }\n  .euiComboBox .euiComboBox__inputWrap {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    border: none;\n    border-radius: 0;\n    padding: 12px;\n    max-width: 400px;\n    width: 100%;\n    height: auto;\n    padding: 4px 8px;\n    display: flex;\n    outline: none;\n    padding-right: 40px;\n  }\n  .euiComboBox .euiComboBox__inputWrap--fullWidth {\n    max-width: 100%;\n  }\n  .euiComboBox .euiComboBox__inputWrap--compressed {\n    height: 32px;\n  }\n  .euiComboBox .euiComboBox__inputWrap--inGroup {\n    height: 100%;\n  }\n  @supports (-moz-appearance: none) {\n    .euiComboBox .euiComboBox__inputWrap {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiComboBox .euiComboBox__inputWrap {\n      line-height: 1em;\n    }\n  }\n  .euiComboBox .euiComboBox__inputWrap::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiComboBox .euiComboBox__inputWrap::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiComboBox .euiComboBox__inputWrap:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiComboBox .euiComboBox__inputWrap:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiComboBox .euiComboBox__inputWrap::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiComboBox .euiComboBox__inputWrap--compressed {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding: 8px;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiComboBox .euiComboBox__inputWrap--compressed {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiComboBox .euiComboBox__inputWrap--inGroup {\n    box-shadow: none !important;\n    border-radius: 0;\n  }\n  .euiComboBox .euiComboBox__inputWrap--withIcon {\n    padding-left: 40px;\n  }\n  .euiComboBox .euiComboBox__inputWrap--fullWidth {\n    max-width: 100%;\n  }\n  .euiComboBox .euiComboBox__inputWrap--compressed {\n    height: 32px;\n  }\n  .euiComboBox .euiComboBox__inputWrap--inGroup {\n    height: 100%;\n  }\n  .euiComboBox .euiComboBox__inputWrap .euiComboBoxPill {\n    max-width: calc(100% - 2px - 16px);\n  }\n  .euiComboBox .euiComboBox__inputWrap:not(.euiComboBox__inputWrap--noWrap) {\n    padding-top: 4px;\n    padding-bottom: 4px;\n    padding-left: 4px;\n    height: auto;\n    flex-wrap: wrap;\n    align-content: flex-start;\n  }\n  .euiComboBox\n    .euiComboBox__inputWrap:not(.euiComboBox__inputWrap--noWrap):hover {\n    cursor: text;\n  }\n  .euiComboBox .euiComboBox__inputWrap.euiComboBox__inputWrap-isClearable {\n    padding-right: 62px;\n  }\n  .euiComboBox .euiComboBox__inputWrap.euiComboBox__inputWrap-isLoading {\n    padding-right: 62px;\n  }\n  .euiComboBox\n    .euiComboBox__inputWrap.euiComboBox__inputWrap-isLoading\n    .euiComboBoxPlaceholder {\n    padding-right: 62px;\n  }\n  .euiComboBox\n    .euiComboBox__inputWrap.euiComboBox__inputWrap-isLoading.euiComboBox__inputWrap-isClearable {\n    padding-right: 84px;\n  }\n  .euiComboBox .euiComboBox__input {\n    display: inline-flex !important;\n    height: 32px;\n    overflow: hidden;\n  }\n  .euiComboBox .euiComboBox__input > input {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    appearance: none;\n    padding: 0;\n    border: none;\n    background: rgba(0, 0, 0, 0);\n    font-size: 14px;\n    color: #343741;\n    margin: 4px;\n    line-height: 1.5;\n  }\n  .euiComboBox.euiComboBox-isOpen .euiComboBox__inputWrap {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiComboBox.euiComboBox-isOpen .euiComboBox__inputWrap--compressed {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiComboBox.euiComboBox-isInvalid .euiComboBox__inputWrap {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiComboBox.euiComboBox-isDisabled .euiComboBox__inputWrap {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    -webkit-text-fill-color: unset;\n  }\n  .euiComboBox.euiComboBox-isDisabled\n    .euiComboBox__inputWrap::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiComboBox.euiComboBox-isDisabled\n    .euiComboBox__inputWrap::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiComboBox.euiComboBox-isDisabled\n    .euiComboBox__inputWrap:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiComboBox.euiComboBox-isDisabled .euiComboBox__inputWrap:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiComboBox.euiComboBox-isDisabled .euiComboBox__inputWrap::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiComboBox.euiComboBox-isDisabled .euiComboBoxPlaceholder,\n  .euiComboBox.euiComboBox-isDisabled .euiComboBoxPill--plainText {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n  }\n  .euiComboBox.euiComboBox-isDisabled\n    .euiComboBox__inputWrap:not(.euiComboBox__inputWrap--noWrap):hover {\n    cursor: not-allowed;\n  }\n  .euiComboBox.euiComboBox--compressed .euiComboBox__inputWrap {\n    line-height: 32px;\n    padding-top: 0;\n    padding-bottom: 0;\n    padding-right: 32px;\n  }\n  .euiComboBox.euiComboBox--compressed\n    .euiComboBox__inputWrap.euiComboBox__inputWrap-isClearable {\n    padding-right: 54px;\n  }\n  .euiComboBox.euiComboBox--compressed\n    .euiComboBox__inputWrap.euiComboBox__inputWrap-isLoading {\n    padding-right: 54px;\n  }\n  .euiComboBox.euiComboBox--compressed\n    .euiComboBox__inputWrap.euiComboBox__inputWrap-isLoading\n    .euiComboBoxPlaceholder {\n    padding-right: 54px;\n  }\n  .euiComboBox.euiComboBox--compressed\n    .euiComboBox__inputWrap.euiComboBox__inputWrap-isLoading.euiComboBox__inputWrap-isClearable {\n    padding-right: 76px;\n  }\n  .euiComboBox .euiFormControlLayout__prepend,\n  .euiComboBox .euiFormControlLayout__append {\n    height: auto !important;\n  }\n  .euiComboBox__input {\n    max-width: 100%;\n  }\n  .euiComboBox__input input {\n    border: none !important;\n    box-shadow: none !important;\n    outline: none !important;\n  }\n  .euiComboBoxPill {\n    height: 22px;\n    line-height: 22px;\n    vertical-align: baseline;\n  }\n  .euiComboBoxPill,\n  .euiComboBoxPill + .euiComboBoxPill {\n    margin: 4px;\n  }\n  .euiComboBox--compressed .euiComboBoxPill,\n  .euiComboBox--compressed .euiComboBoxPill + .euiComboBoxPill {\n    margin: 5px 4px 0 0;\n  }\n  .euiComboBoxPill--plainText {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    line-height: 24px;\n    font-size: 14px;\n    padding: 0;\n    color: #343741;\n    vertical-align: middle;\n    display: inline-block;\n  }\n  .euiComboBoxPlaceholder {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    padding-right: 40px;\n    position: absolute;\n    pointer-events: none;\n    padding-left: 4px;\n    line-height: 32px;\n    color: #6a717d;\n    margin-bottom: 0 !important;\n  }\n  .euiComboBoxOptionsList {\n    transform: none !important;\n    top: 0;\n  }\n  .euiComboBoxOptionsList.euiPopover__panel-isAttached.euiComboBoxOptionsList--top {\n    box-shadow:\n      0 0 12px -1px rgba(152, 162, 179, 0.2),\n      0 0 4px -1px rgba(152, 162, 179, 0.2),\n      0 0 2px 0 rgba(152, 162, 179, 0.2);\n  }\n  .euiComboBoxOptionsList__empty {\n    overflow-wrap: break-word !important;\n    word-wrap: break-word !important;\n    word-break: break-word;\n    padding: 8px;\n    text-align: center;\n    word-wrap: break-word;\n  }\n  .euiComboBoxOptionsList__rowWrap {\n    padding: 0;\n    max-height: 200px;\n    overflow: hidden;\n  }\n  .euiComboBoxOptionsList__rowWrap > div {\n    scrollbar-width: thin;\n  }\n  .euiComboBoxOptionsList__rowWrap > div::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiComboBoxOptionsList__rowWrap > div::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiComboBoxOptionsList__rowWrap > div::-webkit-scrollbar-corner,\n  .euiComboBoxOptionsList__rowWrap > div::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiComboBoxOption {\n    font-size: 14px;\n    padding: 4px 8px 4px 16px;\n    width: 100%;\n    text-align: left;\n    border: 1px solid #d3dae6;\n    border-color: rgba(0, 0, 0, 0);\n    display: flex;\n    align-items: center;\n  }\n  .euiComboBoxOption:hover {\n    text-decoration: underline;\n  }\n  .euiComboBoxOption.euiComboBoxOption-isFocused {\n    cursor: pointer;\n    color: #006bb4;\n    background-color: #e6f0f8;\n  }\n  .euiComboBoxOption.euiComboBoxOption-isDisabled {\n    color: #98a2b3;\n    cursor: not-allowed;\n  }\n  .euiComboBoxOption.euiComboBoxOption-isDisabled:hover {\n    text-decoration: none;\n  }\n  .euiComboBoxOption__contentWrapper {\n    display: flex;\n  }\n  .euiComboBoxOption__contentWrapper .euiComboBoxOption__emptyStateText {\n    flex: 1;\n    text-align: left;\n    margin-bottom: 0;\n  }\n  .euiComboBoxOption__contentWrapper .euiComboBoxOption__enterBadge {\n    align-self: flex-start;\n    margin-left: 4px;\n  }\n  .euiComboBoxOption__content {\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n    flex: 1;\n    text-align: left;\n  }\n  .euiComboBoxTitle {\n    font-size: 12px;\n    padding: 11px 8px 4px;\n    width: 100%;\n    font-weight: 700;\n    color: #000;\n  }\n  .euiComment {\n    font-size: 14px;\n    display: flex;\n    padding-bottom: 16px;\n    min-height: 56px;\n  }\n  .euiComment .euiCommentEvent {\n    flex-grow: 1;\n  }\n  .euiComment .euiCommentTimeline {\n    position: relative;\n    flex-grow: 0;\n    margin-right: 16px;\n  }\n  .euiComment .euiCommentTimeline::before {\n    content: '';\n    position: absolute;\n    left: 20px;\n    top: 24px;\n    width: 2px;\n    background-color: #d3dae6;\n    height: calc(100% + 24px);\n  }\n  .euiComment:last-of-type .euiCommentTimeline::before {\n    display: none;\n  }\n  .euiComment--update:not(.euiComment--hasBody) {\n    align-items: center;\n  }\n  .euiCommentEvent--regular {\n    background-color: #fff;\n    border: 1px solid #d3dae6;\n    border-radius: 4px;\n    flex-grow: 1;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--flexGrowZero {\n    flex-grow: 0;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--noBorder {\n    border: none;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--hasShadow {\n    box-shadow:\n      0 2px 2px -1px rgba(152, 162, 179, 0.3),\n      0 1px 5px -2px rgba(152, 162, 179, 0.3);\n    border: 1px solid #d3dae6;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--isClickable {\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--isClickable:enabled {\n    display: block;\n    width: 100%;\n    text-align: left;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--isClickable:hover,\n  .euiCommentEvent--regular.euiCommentEvent--regular--isClickable:focus {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 162, 179, 0.15),\n      0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    transform: translateY(-2px);\n    cursor: pointer;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--borderRadiusNone {\n    border-radius: 0;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--borderRadiusMedium {\n    border-radius: 4px;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--transparent {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--plain {\n    background-color: #fff;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--subdued {\n    background-color: #fafbfd;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--accent {\n    background-color: #fce7f1;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--primary {\n    background-color: #e6f0f8;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--success {\n    background-color: #e6f2f1;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--warning {\n    background-color: #fef6e6;\n  }\n  .euiCommentEvent--regular.euiCommentEvent--regular--danger {\n    background-color: #f8e9e9;\n  }\n  .euiCommentEvent {\n    overflow: hidden;\n  }\n  .euiCommentEvent__header {\n    line-height: 1.5;\n    display: flex;\n    align-items: center;\n  }\n  .euiCommentEvent__headerData {\n    align-items: center;\n    display: flex;\n    flex-wrap: wrap;\n  }\n  .euiCommentEvent__headerData > div {\n    padding-right: 4px;\n  }\n  .euiCommentEvent__headerUsername {\n    font-weight: 600;\n  }\n  .euiCommentEvent--regular {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    border-radius: 4px;\n    border: 1px solid #d3dae6;\n  }\n  .euiCommentEvent--regular .euiCommentEvent__header {\n    min-height: 40px;\n    background-color: #f5f7fa;\n    border-bottom: 1px solid #d3dae6;\n    padding: 4px 8px;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiCommentEvent--regular .euiCommentEvent__header::after {\n      content: '';\n      min-height: 32px;\n      font-size: 0;\n      display: block;\n    }\n  }\n  .euiCommentEvent--regular .euiCommentEvent__headerData {\n    flex-grow: 1;\n  }\n  .euiCommentEvent--regular .euiCommentEvent__body {\n    padding: 8px;\n  }\n  .euiCommentEvent--update .euiCommentEvent__header {\n    justify-content: flex-start;\n    padding: 4px 0;\n  }\n  .euiCommentEvent--update .euiCommentEvent__headerData {\n    padding-right: 8px;\n  }\n  .euiCommentEvent--update .euiCommentEvent__body {\n    padding-top: 4px;\n  }\n  .euiCommentTimeline__content {\n    min-width: 40px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    position: relative;\n  }\n  .euiCommentTimeline__icon--default {\n    flex-shrink: 0;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    overflow-x: hidden;\n    border-radius: 50%;\n    background-color: #f5f7fa;\n  }\n  .euiCommentTimeline__icon--default.euiCommentTimeline__icon--regular {\n    width: 40px;\n    height: 40px;\n  }\n  .euiCommentTimeline__icon--default.euiCommentTimeline__icon--update {\n    width: 24px;\n    height: 24px;\n  }\n  .euiContextMenu {\n    width: 256px;\n    max-width: 100%;\n    position: relative;\n    overflow: hidden;\n    transition: height 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    border-radius: 4px;\n  }\n  .euiContextMenu .euiContextMenu__content {\n    padding: 8px;\n  }\n  .euiContextMenu__panel {\n    position: absolute;\n  }\n  .euiContextMenu__icon {\n    margin-right: 8px;\n  }\n  .euiContextMenuPanel {\n    width: 100%;\n    visibility: visible;\n    outline-offset: -3px;\n  }\n  .euiContextMenuPanel:focus {\n    outline: none;\n  }\n  .euiContextMenuPanel.euiContextMenuPanel-txInLeft {\n    pointer-events: none;\n    animation: euiContextMenuPanelTxInLeft 250ms\n      cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiContextMenuPanel.euiContextMenuPanel-txOutLeft {\n    pointer-events: none;\n    animation: euiContextMenuPanelTxOutLeft 250ms\n      cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiContextMenuPanel.euiContextMenuPanel-txInRight {\n    pointer-events: none;\n    animation: euiContextMenuPanelTxInRight 250ms\n      cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiContextMenuPanel.euiContextMenuPanel-txOutRight {\n    pointer-events: none;\n    animation: euiContextMenuPanelTxOutRight 250ms\n      cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiContextMenuPanel--next {\n    transform: translateX(256px);\n    visibility: hidden;\n  }\n  .euiContextMenuPanel--previous {\n    transform: translateX(-256px);\n    visibility: hidden;\n  }\n  .euiContextMenuPanelTitle {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    padding: 12px;\n    text-transform: uppercase;\n    border-bottom: 1px solid #d3dae6;\n    padding: 12px;\n    width: 100%;\n    text-align: left;\n    outline-offset: -3px;\n  }\n  .euiContextMenuPanelTitle:enabled:hover,\n  .euiContextMenuPanelTitle:enabled:focus {\n    text-decoration: underline;\n  }\n  .euiContextMenuPanelTitle--small {\n    color: #1a1c21;\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    padding: 12px;\n    text-transform: uppercase;\n    border-bottom: 1px solid #d3dae6;\n    padding: 6px 8px;\n  }\n  @keyframes euiContextMenuPanelTxInLeft {\n    0% {\n      transform: translateX(256px);\n    }\n    100% {\n      transform: translateX(0);\n    }\n  }\n  @keyframes euiContextMenuPanelTxOutLeft {\n    0% {\n      transform: translateX(0);\n    }\n    100% {\n      transform: translateX(-256px);\n    }\n  }\n  @keyframes euiContextMenuPanelTxInRight {\n    0% {\n      transform: translateX(-256px);\n    }\n    100% {\n      transform: translateX(0);\n    }\n  }\n  @keyframes euiContextMenuPanelTxOutRight {\n    0% {\n      transform: translateX(0);\n    }\n    100% {\n      transform: translateX(256px);\n    }\n  }\n  .euiContextMenuItem {\n    display: block;\n    padding: 12px;\n    width: 100%;\n    text-align: left;\n    color: #343741;\n    outline-offset: -3px;\n  }\n  .euiContextMenuItem:hover,\n  .euiContextMenuItem:focus {\n    text-decoration: underline;\n  }\n  .euiContextMenuItem:focus {\n    background-color: #e6f0f8;\n  }\n  .euiContextMenuItem.euiContextMenuItem-isDisabled {\n    color: #afb0b3;\n    cursor: default;\n  }\n  .euiContextMenuItem.euiContextMenuItem-isDisabled:hover,\n  .euiContextMenuItem.euiContextMenuItem-isDisabled:focus {\n    text-decoration: none;\n  }\n  .euiContextMenuItem--small {\n    padding: 6px 8px;\n  }\n  .euiContextMenuItem--small .euiContextMenuItem__text {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiContextMenuItem__inner {\n    display: flex;\n  }\n  .euiContextMenuItem__text {\n    flex-grow: 1;\n    overflow: hidden;\n  }\n  .euiContextMenuItem__arrow {\n    align-self: flex-end;\n  }\n  .euiContextMenu__itemLayout {\n    display: flex;\n    align-items: center;\n  }\n  .euiContextMenu__itemLayout.euiContextMenu__itemLayout--bottom {\n    align-items: flex-end;\n  }\n  .euiContextMenu__itemLayout.euiContextMenu__itemLayout--top {\n    align-items: flex-start;\n  }\n  .euiContextMenu__itemLayout .euiContextMenu__icon {\n    flex-shrink: 0;\n  }\n  .euiControlBar {\n    background: #343741;\n    color: #fff;\n    display: flex;\n    flex-direction: column;\n    box-shadow:\n      inset 0 40px 0 #343741,\n      inset 0 600rem 0 #fafbfd;\n    bottom: 0;\n    transform: translateY(0);\n    height: 40px;\n    max-height: calc(100vh - 80px);\n  }\n  .euiControlBar--fixed {\n    position: fixed;\n    z-index: 6000;\n  }\n  .euiControlBar--absolute {\n    position: absolute;\n    z-index: 1000;\n  }\n  .euiControlBar--relative {\n    position: relative;\n  }\n  .euiControlBar-isOpen {\n    animation-duration: 250ms;\n    animation-timing-function: cubic-bezier(0.694, 0.0482, 0.335, 1);\n    animation-fill-mode: forwards;\n  }\n  .euiControlBar-isOpen.euiControlBar--large {\n    animation-name: euiControlBarOpenPanelLarge;\n    height: calc(100vh - 80px);\n    bottom: -100vh;\n  }\n  .euiControlBar-isOpen.euiControlBar--medium {\n    animation-name: euiControlBarOpenPanelMedium;\n    height: 480px;\n    bottom: -480px;\n  }\n  .euiControlBar-isOpen.euiControlBar--small {\n    animation-name: euiControlBarOpenPanelSmall;\n    height: 240px;\n    bottom: -240px;\n  }\n  .euiControlBar__controls {\n    height: 40px;\n    width: 100%;\n    display: flex;\n    align-items: center;\n    overflow-y: hidden;\n    overflow-x: auto;\n    padding: 0 12px;\n  }\n  .euiControlBar__content {\n    scrollbar-width: thin;\n    overflow-y: auto;\n    width: 100%;\n    height: calc(100% - 40px);\n    background-color: #fafbfd;\n    animation-name: euiControlBarShowContent;\n    animation-duration: 350ms;\n    animation-iteration-count: 1;\n    animation-timing-function: cubic-bezier(0.694, 0.0482, 0.335, 1);\n    color: #343741;\n  }\n  .euiControlBar__content::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiControlBar__content::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiControlBar__content::-webkit-scrollbar-corner,\n  .euiControlBar__content::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiControlBar__icon {\n    flex-shrink: 0;\n    margin-left: 8px;\n    margin-right: 8px;\n  }\n  .euiControlBar__buttonIcon {\n    flex-shrink: 0;\n    min-width: 40px;\n    min-height: 40px;\n  }\n  .euiControlBar__button {\n    flex-shrink: 0;\n    border-radius: 2px;\n    margin-left: 4px;\n    font-size: 14px;\n  }\n  .euiControlBar__button:enabled:hover {\n    transform: none;\n    box-shadow: none;\n  }\n  .euiControlBar__button:last-child {\n    margin-right: 4px;\n  }\n  .euiControlBar__breadcrumbs .euiBreadcrumb:not(.euiBreadcrumb--last) {\n    color: #9ca1aa;\n  }\n  .euiControlBar__breadcrumbs .euiBreadcrumbSeparator {\n    background: rgba(255, 255, 255, 0.2);\n  }\n  .euiControlBar__spacer {\n    flex-grow: 1;\n    height: 100%;\n  }\n  .euiControlBar__divider {\n    flex-shrink: 0;\n    height: 100%;\n    width: 1px;\n    background-color: rgba(255, 255, 255, 0.2);\n  }\n  .euiControlBar__text {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    padding: 0 8px;\n    color: #fff;\n  }\n  .euiControlBar__text:last-child {\n    padding-right: 0;\n  }\n  .euiControlBar__tab {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    color: #fff;\n    padding: 0 16px;\n    text-align: center;\n    height: 100%;\n  }\n  .euiControlBar__tab:hover,\n  .euiControlBar__tab:focus {\n    text-decoration: underline;\n    cursor: pointer;\n  }\n  .euiControlBar__tab.euiControlBar__tab--active {\n    background-color: #fafbfd;\n    box-shadow: inset 0 4px 0 #006bb4;\n    color: #006bb4;\n  }\n  .euiControlBar__controls .euiLink.euiLink--primary {\n    color: #6eaad4;\n  }\n  .euiControlBar__controls .euiLink.euiLink--primary:hover {\n    color: #4d97cb;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--primary:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--primary:enabled:not(.euiButton--fill) {\n    color: #6eaad4;\n    border-color: #6eaad4;\n  }\n  .euiControlBar__controls .euiButtonIcon--primary {\n    color: #6eaad4;\n  }\n  .euiControlBar__controls .euiLink.euiLink--accent {\n    color: #ec7bb3;\n  }\n  .euiControlBar__controls .euiLink.euiLink--accent:hover {\n    color: #e7549d;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--accent:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--accent:enabled:not(.euiButton--fill) {\n    color: #ec7bb3;\n    border-color: #ec7bb3;\n  }\n  .euiControlBar__controls .euiButtonIcon--accent {\n    color: #ec7bb3;\n  }\n  .euiControlBar__controls .euiLink.euiLink--secondary {\n    color: #5eaea7;\n  }\n  .euiControlBar__controls .euiLink.euiLink--secondary:hover {\n    color: #4da49d;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--secondary:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--secondary:enabled:not(.euiButton--fill) {\n    color: #5eaea7;\n    border-color: #5eaea7;\n  }\n  .euiControlBar__controls .euiButtonIcon--secondary {\n    color: #5eaea7;\n  }\n  .euiControlBar__controls .euiLink.euiLink--success {\n    color: #5eaea7;\n  }\n  .euiControlBar__controls .euiLink.euiLink--success:hover {\n    color: #4da49d;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--success:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--success:enabled:not(.euiButton--fill) {\n    color: #5eaea7;\n    border-color: #5eaea7;\n  }\n  .euiControlBar__controls .euiButtonIcon--success {\n    color: #5eaea7;\n  }\n  .euiControlBar__controls .euiLink.euiLink--warning {\n    color: #f5a700;\n  }\n  .euiControlBar__controls .euiLink.euiLink--warning:hover {\n    color: #f8c14d;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--warning:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--warning:enabled:not(.euiButton--fill) {\n    color: #f5a700;\n    border-color: #f5a700;\n  }\n  .euiControlBar__controls .euiButtonIcon--warning {\n    color: #f5a700;\n  }\n  .euiControlBar__controls .euiLink.euiLink--danger {\n    color: #db8a85;\n  }\n  .euiControlBar__controls .euiLink.euiLink--danger:hover {\n    color: #d16862;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--danger:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--danger:enabled:not(.euiButton--fill) {\n    color: #db8a85;\n    border-color: #db8a85;\n  }\n  .euiControlBar__controls .euiButtonIcon--danger {\n    color: #db8a85;\n  }\n  .euiControlBar__controls .euiLink.euiLink--subdued {\n    color: #9ca1aa;\n  }\n  .euiControlBar__controls .euiLink.euiLink--subdued:hover {\n    color: #979ca4;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--subdued:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--subdued:enabled:not(.euiButton--fill) {\n    color: #9ca1aa;\n    border-color: #9ca1aa;\n  }\n  .euiControlBar__controls .euiButtonIcon--subdued {\n    color: #9ca1aa;\n  }\n  .euiControlBar__controls .euiLink.euiLink--ghost {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiLink.euiLink--ghost:hover {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--ghost:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--ghost:enabled:not(.euiButton--fill) {\n    color: #fff;\n    border-color: #fff;\n  }\n  .euiControlBar__controls .euiButtonIcon--ghost {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #9ca0aa;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text:hover {\n    color: #969ba4;\n  }\n  .euiControlBar__controls .euiLink.euiLink--text {\n    color: #fff;\n  }\n  .euiControlBar__controls .euiControlBar__button.euiButton--text:enabled {\n    box-shadow: none;\n  }\n  .euiControlBar__controls\n    .euiControlBar__button.euiButton--text:enabled:not(.euiButton--fill) {\n    color: #9ca0aa;\n    border-color: #9ca0aa;\n  }\n  .euiControlBar__controls .euiButtonIcon--text {\n    color: #9ca0aa;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiControlBar:not(.euiControlBar--showOnMobile) {\n      display: none;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiControlBar:not(.euiControlBar--showOnMobile) {\n      display: none;\n    }\n  }\n  @keyframes euiControlBarOpenPanelLarge {\n    0% {\n      transform: translateY(calc(40px * 3 * -1));\n    }\n    100% {\n      transform: translateY(-100vh);\n    }\n  }\n  @keyframes euiControlBarOpenPanelMedium {\n    0% {\n      transform: translateY(-40px);\n    }\n    100% {\n      transform: translateY(-480px);\n    }\n  }\n  @keyframes euiControlBarOpenPanelSmall {\n    0% {\n      transform: translateY(-40px);\n    }\n    100% {\n      transform: translateY(-240px);\n    }\n  }\n  @keyframes euiControlBarShowContent {\n    0% {\n      opacity: 0;\n    }\n    100% {\n      opacity: 1;\n    }\n  }\n  .euiDatePicker .euiFormControlLayout {\n    height: auto;\n  }\n  .euiDatePicker.euiDatePicker--shadow .react-datepicker-popper {\n    box-shadow:\n      0 6px 12px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -1px rgba(152, 162, 179, 0.2),\n      0 2px 2px 0 rgba(152, 162, 179, 0.2);\n    border: 1px solid #d3dae6;\n    background-color: #fff;\n    border-radius: 0 0 4px 4px;\n  }\n  .euiDatePicker.euiDatePicker--shadow.euiDatePicker--inline .react-datepicker {\n    box-shadow:\n      0 6px 12px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -1px rgba(152, 162, 179, 0.2),\n      0 2px 2px 0 rgba(152, 162, 179, 0.2);\n    border: 1px solid #d3dae6;\n    background-color: #fff;\n    border-radius: 4px;\n  }\n  .react-datepicker {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 12px;\n    color: #000;\n    display: flex;\n    position: relative;\n    border-radius: 4px;\n  }\n  .react-datepicker--time-only\n    .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box {\n    width: 100%;\n  }\n  .react-datepicker--time-only\n    .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    .react-datepicker__time-list\n    li.react-datepicker__time-list-item {\n    font-size: 14px;\n    text-align: left;\n    padding-left: 36px;\n    padding-right: 36px;\n    color: #343741;\n  }\n  .react-datepicker--time-only\n    .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    .react-datepicker__time-list\n    li.react-datepicker__time-list-item.react-datepicker__time-list-item--selected {\n    color: #fff;\n  }\n  .react-datepicker--time-only\n    .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    .react-datepicker__time-list\n    li.react-datepicker__time-list-item.react-datepicker__time-list-item--disabled {\n    color: #afb0b3;\n  }\n  .react-datepicker--time-only .react-datepicker__time-container {\n    border-left: 0;\n  }\n  .euiDatePicker.euiDatePicker--shadow .react-datepicker-popper {\n    z-index: 2000;\n    animation: euiAnimFadeIn 150ms ease-in;\n  }\n  .euiDatePicker.euiDatePicker--shadow\n    .react-datepicker-popper[data-placement^='top'] {\n    box-shadow:\n      0 0 12px -1px rgba(152, 162, 179, 0.2),\n      0 0 4px -1px rgba(152, 162, 179, 0.2),\n      0 0 2px 0 rgba(152, 162, 179, 0.2);\n    border-radius: 4px 4px 0 0;\n  }\n  .euiDatePicker.euiDatePicker--shadow\n    .react-datepicker-popper[data-placement^='right'] {\n    margin-left: 0;\n  }\n  .euiDatePicker.euiDatePicker--shadow\n    .react-datepicker-popper[data-placement^='left'] {\n    margin-right: 0;\n  }\n  .react-datepicker__header {\n    text-align: center;\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n  }\n  .react-datepicker__header--time {\n    display: none;\n  }\n  .react-datepicker__header__dropdown {\n    padding: 16px 0 8px 0;\n  }\n  .react-datepicker__year-dropdown-container--select,\n  .react-datepicker__month-dropdown-container--select,\n  .react-datepicker__month-year-dropdown-container--select,\n  .react-datepicker__year-dropdown-container--scroll,\n  .react-datepicker__month-dropdown-container--scroll,\n  .react-datepicker__month-year-dropdown-container--scroll {\n    display: inline-block;\n    margin: 0 4px;\n  }\n  .react-datepicker__current-month,\n  .react-datepicker-time__header {\n    display: none;\n  }\n  .react-datepicker-time__header {\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n  }\n  .react-datepicker__navigation {\n    cursor: pointer;\n    position: absolute;\n    top: 18px;\n    width: 0;\n    padding: 0;\n    z-index: 1;\n    text-indent: -999em;\n    overflow: hidden;\n  }\n  .react-datepicker__navigation--previous {\n    background-position: center;\n    background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiI+ICA8ZGVmcz4gICAgPHBhdGggaWQ9ImFycm93X2Rvd24tYSIgZD0iTTEzLjA2ODg1MDgsNS4xNTcyNTAzOCBMOC4zODQyMzk3NSw5Ljc2ODI3NDI4IEM4LjE3MDU0NDE1LDkuOTc4NjEzMDggNy44Mjk5OTIxNCw5Ljk3OTE0MDk1IDcuNjE1NzYwMjUsOS43NjgyNzQyOCBMMi45MzExNDkxNSw1LjE1NzI1MDM4IEMyLjcxODEzNTksNC45NDc1ODMyMSAyLjM3Mjc3MzE5LDQuOTQ3NTgzMjEgMi4xNTk3NTk5NCw1LjE1NzI1MDM4IEMxLjk0Njc0NjY5LDUuMzY2OTE3NTYgMS45NDY3NDY2OSw1LjcwNjg1NTIyIDIuMTU5NzU5OTQsNS45MTY1MjI0IEw2Ljg0NDM3MTA0LDEwLjUyNzU0NjMgQzcuNDg1MTc0MjQsMTEuMTU4MjgzNiA4LjUxNjQ0OTc5LDExLjE1NjY4NTEgOS4xNTU2Mjg5NiwxMC41Mjc1NDYzIEwxMy44NDAyNDAxLDUuOTE2NTIyNCBDMTQuMDUzMjUzMyw1LjcwNjg1NTIyIDE0LjA1MzI1MzMsNS4zNjY5MTc1NiAxMy44NDAyNDAxLDUuMTU3MjUwMzggQzEzLjYyNzIyNjgsNC45NDc1ODMyMSAxMy4yODE4NjQxLDQuOTQ3NTgzMjEgMTMuMDY4ODUwOCw1LjE1NzI1MDM4IFoiLz4gIDwvZGVmcz4gIDxnIGZpbGwtcnVsZT0iZXZlbm9kZCI+ICAgIDx1c2UgZmlsbC1ydWxlPSJub256ZXJvIiB4bGluazpocmVmPSIjYXJyb3dfZG93bi1hIi8+ICA8L2c+PC9zdmc+);\n    left: 20px;\n    height: 16px;\n    width: 16px;\n    transform: rotate(90deg);\n    transition: transform 90ms ease-in-out;\n  }\n  .react-datepicker__navigation--previous:hover,\n  .react-datepicker__navigation--previous:focus {\n    border-radius: 4px;\n    transform: scale(1.2) rotate(90deg);\n  }\n  .react-datepicker__navigation--previous:hover {\n    background-color: #f5f7fa;\n    box-shadow: 0 0 0 2px #f5f7fa;\n  }\n  .react-datepicker__navigation--previous:focus {\n    background-color: #e6f0f8;\n    box-shadow: 0 0 0 2px #e6f0f8;\n  }\n  .react-datepicker__navigation--previous--disabled,\n  .react-datepicker__navigation--previous--disabled:hover {\n    cursor: not-allowed;\n    opacity: 0.2;\n  }\n  .react-datepicker__navigation--next {\n    background-position: center;\n    background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiI+ICA8ZGVmcz4gICAgPHBhdGggaWQ9ImFycm93X2Rvd24tYSIgZD0iTTEzLjA2ODg1MDgsNS4xNTcyNTAzOCBMOC4zODQyMzk3NSw5Ljc2ODI3NDI4IEM4LjE3MDU0NDE1LDkuOTc4NjEzMDggNy44Mjk5OTIxNCw5Ljk3OTE0MDk1IDcuNjE1NzYwMjUsOS43NjgyNzQyOCBMMi45MzExNDkxNSw1LjE1NzI1MDM4IEMyLjcxODEzNTksNC45NDc1ODMyMSAyLjM3Mjc3MzE5LDQuOTQ3NTgzMjEgMi4xNTk3NTk5NCw1LjE1NzI1MDM4IEMxLjk0Njc0NjY5LDUuMzY2OTE3NTYgMS45NDY3NDY2OSw1LjcwNjg1NTIyIDIuMTU5NzU5OTQsNS45MTY1MjI0IEw2Ljg0NDM3MTA0LDEwLjUyNzU0NjMgQzcuNDg1MTc0MjQsMTEuMTU4MjgzNiA4LjUxNjQ0OTc5LDExLjE1NjY4NTEgOS4xNTU2Mjg5NiwxMC41Mjc1NDYzIEwxMy44NDAyNDAxLDUuOTE2NTIyNCBDMTQuMDUzMjUzMyw1LjcwNjg1NTIyIDE0LjA1MzI1MzMsNS4zNjY5MTc1NiAxMy44NDAyNDAxLDUuMTU3MjUwMzggQzEzLjYyNzIyNjgsNC45NDc1ODMyMSAxMy4yODE4NjQxLDQuOTQ3NTgzMjEgMTMuMDY4ODUwOCw1LjE1NzI1MDM4IFoiLz4gIDwvZGVmcz4gIDxnIGZpbGwtcnVsZT0iZXZlbm9kZCI+ICAgIDx1c2UgZmlsbC1ydWxlPSJub256ZXJvIiB4bGluazpocmVmPSIjYXJyb3dfZG93bi1hIi8+ICA8L2c+PC9zdmc+);\n    right: 20px;\n    height: 16px;\n    width: 16px;\n    transform: rotate(-90deg);\n  }\n  .react-datepicker__navigation--next--with-time:not(\n      .react-datepicker__navigation--next--with-today-button\n    ) {\n    left: 248px;\n  }\n  .react-datepicker__navigation--next:hover,\n  .react-datepicker__navigation--next:focus {\n    border-radius: 4px;\n    transform: scale(1.2) rotate(-90deg);\n  }\n  .react-datepicker__navigation--next:hover {\n    background-color: #f5f7fa;\n    box-shadow: 0 0 0 2px #f5f7fa;\n  }\n  .react-datepicker__navigation--next:focus {\n    background-color: #e6f0f8;\n    box-shadow: 0 0 0 2px #e6f0f8;\n  }\n  .react-datepicker__navigation--next--disabled,\n  .react-datepicker__navigation--next--disabled:hover {\n    cursor: not-allowed;\n    opacity: 0.2;\n  }\n  .react-datepicker__navigation--years {\n    position: relative;\n    top: 0;\n    display: block;\n    margin-left: auto;\n    margin-right: auto;\n  }\n  .react-datepicker__navigation--years-previous {\n    top: 4px;\n    border-top-color: #f5f7fa;\n  }\n  .react-datepicker__navigation--years-previous:hover {\n    border-top-color: #d3dce9;\n  }\n  .react-datepicker__navigation--years-upcoming {\n    top: -4px;\n    border-bottom-color: #f5f7fa;\n  }\n  .react-datepicker__navigation--years-upcoming:hover {\n    border-bottom-color: #d3dce9;\n  }\n  .react-datepicker__month {\n    margin: 0 16px 16px 16px;\n    text-align: center;\n    border-radius: 4px;\n  }\n  .react-datepicker__time-container {\n    border-left: #d3dae6;\n    width: auto;\n    display: flex;\n    padding: 16px 0;\n    border-radius: 0 4px 4px 0;\n    flex-grow: 1;\n  }\n  .react-datepicker__time-container .react-datepicker__time {\n    position: relative;\n    flex-grow: 1;\n    display: flex;\n    padding-left: 4px;\n    flex-direction: column;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box {\n    width: auto;\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list {\n    scrollbar-width: thin;\n    height: 204px !important;\n    display: flex;\n    flex-direction: column;\n    flex-grow: 1;\n    overflow-y: auto;\n    align-items: center;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list::-webkit-scrollbar-corner,\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item {\n    padding: 4px 8px;\n    margin-bottom: 4px;\n    text-align: right;\n    color: #69707d;\n    white-space: nowrap;\n    line-height: 12px;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item:hover,\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item:focus {\n    cursor: pointer;\n    text-decoration: underline;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item--selected {\n    background-color: #006bb4;\n    color: #fff;\n    border-radius: 2px;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item--selected:hover {\n    background-color: #006bb4;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item--disabled {\n    color: #d3dae6;\n  }\n  .react-datepicker__time-container\n    .react-datepicker__time\n    .react-datepicker__time-box\n    ul.react-datepicker__time-list\n    li.react-datepicker__time-list-item--disabled:hover {\n    cursor: not-allowed;\n    text-decoration: none;\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .react-datepicker__week-number {\n    color: #f5f7fa;\n    display: inline-block;\n    width: 32px;\n    line-height: 28px;\n    text-align: center;\n    margin: 0 4px;\n  }\n  .react-datepicker__week-number.react-datepicker__week-number--clickable {\n    cursor: pointer;\n  }\n  .react-datepicker__week-number.react-datepicker__week-number--clickable:hover {\n    border-radius: 4px;\n    background-color: #fff;\n  }\n  .react-datepicker__day-names,\n  .react-datepicker__week {\n    white-space: nowrap;\n  }\n  .react-datepicker__day-name,\n  .react-datepicker__day,\n  .react-datepicker__time-name {\n    color: #000;\n    display: inline-block;\n    width: 32px;\n    line-height: 28px;\n    text-align: center;\n    margin: 0 2px;\n  }\n  .react-datepicker__day-name {\n    color: #69707d;\n    text-transform: uppercase;\n  }\n  .react-datepicker__day {\n    cursor: pointer;\n    border: solid 2px rgba(0, 0, 0, 0);\n    transition: transform 90ms ease-in-out;\n  }\n  .react-datepicker__day:hover:not(.react-datepicker__day--disabled) {\n    text-decoration: underline;\n    font-weight: 700;\n    transform: scale(1.2);\n  }\n  .react-datepicker__day--today {\n    font-weight: bold;\n    color: #006bb4;\n  }\n  .react-datepicker__day--outside-month {\n    color: #69707d;\n  }\n  .react-datepicker__day--highlighted {\n    border-radius: 4px;\n    background-color: #017d73;\n    color: #fff;\n  }\n  .react-datepicker__day--highlighted:hover {\n    background-color: #01645c;\n  }\n  .react-datepicker__day--in-range {\n    background-color: rgba(0, 107, 180, 0.1);\n    color: #000;\n    border-radius: 0;\n    border-top: solid 6px #fff;\n    border-bottom: solid 6px #fff;\n    border-right: none;\n    border-left: none;\n    line-height: 20px;\n  }\n  .react-datepicker__day--selected,\n  .react-datepicker__day--in-selecting-range {\n    height: 32px;\n    margin: 0 2px;\n    border-radius: 4px;\n    background-color: #006bb4;\n    line-height: 28px;\n    border: solid 2px #006bb4;\n    color: #fff;\n  }\n  .react-datepicker__day--selected:hover,\n  .react-datepicker__day--in-selecting-range:hover {\n    background-color: #005c9b;\n  }\n  .react-datepicker__day--keyboard-selected {\n    border-radius: 4px;\n    border: solid 2px #006bb4;\n    font-weight: 700;\n  }\n  .react-datepicker__day--keyboard-selected:hover {\n    background-color: #005c9b;\n    color: #fff;\n  }\n  .react-datepicker__day--in-selecting-range:not(\n      .react-datepicker__day--in-range\n    ) {\n    background-color: rgba(0, 107, 180, 0.5);\n  }\n  .react-datepicker__month--selecting-range\n    .react-datepicker__day--in-range:not(\n      .react-datepicker__day--in-selecting-range\n    ) {\n    background-color: #fff;\n    color: #000;\n  }\n  .react-datepicker__day--disabled {\n    cursor: not-allowed;\n    color: #d3dae6;\n  }\n  .react-datepicker__day--disabled:hover {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .react-datepicker__input-container {\n    position: relative;\n  }\n  .react-datepicker__year-read-view {\n    font-weight: 300;\n    color: #69707d;\n  }\n  .react-datepicker__month-read-view {\n    font-weight: 500;\n  }\n  .react-datepicker__year-read-view,\n  .react-datepicker__month-read-view,\n  .react-datepicker__month-year-read-view {\n    font-size: 20px;\n  }\n  .react-datepicker__year-read-view:hover,\n  .react-datepicker__month-read-view:hover,\n  .react-datepicker__month-year-read-view:hover {\n    cursor: pointer;\n    color: #006bb4;\n  }\n  .react-datepicker__year-read-view:hover\n    .react-datepicker__year-read-view--down-arrow,\n  .react-datepicker__year-read-view:hover\n    .react-datepicker__month-read-view--down-arrow,\n  .react-datepicker__month-read-view:hover\n    .react-datepicker__year-read-view--down-arrow,\n  .react-datepicker__month-read-view:hover\n    .react-datepicker__month-read-view--down-arrow,\n  .react-datepicker__month-year-read-view:hover\n    .react-datepicker__year-read-view--down-arrow,\n  .react-datepicker__month-year-read-view:hover\n    .react-datepicker__month-read-view--down-arrow {\n    border-top-color: #d3dce9;\n  }\n  .react-datepicker__year-read-view--down-arrow,\n  .react-datepicker__month-read-view--down-arrow,\n  .react-datepicker__month-year-read-view--down-arrow {\n    display: none;\n  }\n  .react-datepicker__year-dropdown,\n  .react-datepicker__month-dropdown,\n  .react-datepicker__month-year-dropdown {\n    background-color: #fff;\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    right: 0;\n    z-index: 1;\n    text-align: center;\n    border-radius: 4px;\n    display: flex;\n    flex-wrap: wrap;\n    animation: euiAnimFadeIn 150ms ease-in;\n    align-content: space-around;\n    align-items: center;\n    padding: 8px;\n  }\n  .react-datepicker__year-dropdown:hover,\n  .react-datepicker__month-dropdown:hover,\n  .react-datepicker__month-year-dropdown:hover {\n    cursor: pointer;\n  }\n  .react-datepicker__year-dropdown {\n    flex-wrap: wrap-reverse;\n    flex-direction: row-reverse;\n    justify-content: flex-end;\n  }\n  .react-datepicker__year-option:first-of-type,\n  .react-datepicker__year-option:last-of-type {\n    display: none;\n  }\n  .react-datepicker__year-option,\n  .react-datepicker__month-option,\n  .react-datepicker__month-year-option {\n    font-size: 12px;\n    padding: 8px;\n    color: #343741;\n    flex-basis: 33.3%;\n  }\n  .react-datepicker__year-option:first-of-type,\n  .react-datepicker__month-option:first-of-type,\n  .react-datepicker__month-year-option:first-of-type {\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n  }\n  .react-datepicker__year-option:last-of-type,\n  .react-datepicker__month-option:last-of-type,\n  .react-datepicker__month-year-option:last-of-type {\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n  }\n  .react-datepicker__year-option:hover,\n  .react-datepicker__month-option:hover,\n  .react-datepicker__month-year-option:hover {\n    background-color: #f5f7fa;\n  }\n  .react-datepicker__year-option:hover\n    .react-datepicker__navigation--years-upcoming,\n  .react-datepicker__month-option:hover\n    .react-datepicker__navigation--years-upcoming,\n  .react-datepicker__month-year-option:hover\n    .react-datepicker__navigation--years-upcoming {\n    border-bottom-color: #d3dce9;\n  }\n  .react-datepicker__year-option:hover\n    .react-datepicker__navigation--years-previous,\n  .react-datepicker__month-option:hover\n    .react-datepicker__navigation--years-previous,\n  .react-datepicker__month-year-option:hover\n    .react-datepicker__navigation--years-previous {\n    border-top-color: #d3dce9;\n  }\n  .react-datepicker__year-option--selected,\n  .react-datepicker__month-option--selected,\n  .react-datepicker__month-year-option--selected {\n    display: none;\n  }\n  .react-datepicker__screenReaderOnly {\n    position: absolute;\n    left: -10000px;\n    top: auto;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n  }\n  .react-datepicker__year-option--preselected,\n  .react-datepicker__month-option--preselected {\n    background: #e6f0f8;\n  }\n  .react-datepicker__year-option--selected_year.react-datepicker__year-option--preselected,\n  .react-datepicker__month-option--selected_month.react-datepicker__month-option--preselected {\n    background: #006bb4;\n  }\n  .react-datepicker__time-list-item--preselected,\n  .react-datepicker__year-option--preselected,\n  .react-datepicker__month-option--preselected {\n    background: #d2e4f2;\n  }\n  .react-datepicker__time-container--focus {\n    background: #e6f0f8;\n  }\n  .react-datepicker__month-read-view:focus,\n  .react-datepicker__year-read-view:focus {\n    text-decoration: underline;\n  }\n  .react-datepicker__month--accessible:focus {\n    background: #e6f0f8;\n  }\n  .react-datepicker__month--accessible:focus\n    .react-datepicker__day--in-range:not(.react-datepicker__day--selected) {\n    border-top-color: #e6f0f8;\n    border-bottom-color: #e6f0f8;\n  }\n  .react-datepicker__navigation:focus {\n    background-color: #e6f0f8;\n  }\n  .react-datepicker__year-option--selected_year,\n  .react-datepicker__month-option--selected_month {\n    background: #006bb4;\n    color: #fff;\n    font-weight: 700;\n    border-radius: 4px;\n  }\n  .react-datepicker__focusTrap {\n    display: flex;\n  }\n  .euiDatePickerRange {\n    max-width: 400px;\n    width: 100%;\n    height: auto;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    display: flex;\n    align-items: center;\n    padding: 1px;\n  }\n  .euiDatePickerRange--fullWidth {\n    max-width: 100%;\n  }\n  .euiDatePickerRange--compressed {\n    height: 32px;\n  }\n  .euiDatePickerRange--inGroup {\n    height: 100%;\n  }\n  @supports (-moz-appearance: none) {\n    .euiDatePickerRange {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiDatePickerRange > * {\n    flex-grow: 1;\n  }\n  .euiDatePickerRange .euiFieldText.euiDatePicker {\n    box-shadow: none !important;\n    text-align: center;\n  }\n  .euiDatePickerRange .react-datepicker-popper .euiFieldText.euiDatePicker {\n    text-align: left;\n  }\n  .euiDatePickerRange--inGroup {\n    box-shadow: none;\n    padding: 0;\n  }\n  .euiDatePickerRange--inGroup .euiDatePicker {\n    height: 38px;\n  }\n  .euiDatePickerRange > .euiDatePickerRange__delimeter {\n    background-color: rgba(0, 0, 0, 0) !important;\n    line-height: 1 !important;\n    flex: 0 0 auto;\n    padding-left: 6px;\n    padding-right: 6px;\n  }\n  .euiDatePickerRange--readOnly {\n    background: #eef2f7;\n  }\n  .euiSuperDatePicker__absoluteDateFormRow {\n    padding: 0 8px 8px;\n  }\n  .euiDatePopoverButton {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    display: block;\n    width: 100%;\n    padding: 0 8px;\n    line-height: 38px;\n    height: 38px;\n    word-break: break-all;\n    transition: background 150ms ease-in;\n    background-size: 100%;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiDatePopoverButton {\n      line-height: 1em;\n    }\n  }\n  .euiDatePopoverButton::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiDatePopoverButton::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiDatePopoverButton:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiDatePopoverButton:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiDatePopoverButton::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiDatePopoverButton:focus,\n  .euiDatePopoverButton-isSelected {\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n  }\n  .euiDatePopoverButton-needsUpdating {\n    background-color: #e6f2f1;\n    color: #01776d;\n  }\n  .euiDatePopoverButton-needsUpdating:focus,\n  .euiDatePopoverButton-needsUpdating.euiDatePopoverButton-isSelected {\n    background-image: linear-gradient(\n      to top,\n      #017d73,\n      #017d73 2px,\n      transparent 2px,\n      transparent 100%\n    );\n  }\n  .euiDatePopoverButton-isInvalid {\n    background-color: #f8e9e9;\n    color: #bd271e;\n  }\n  .euiDatePopoverButton-isInvalid:focus,\n  .euiDatePopoverButton-isInvalid.euiDatePopoverButton-isSelected {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n  }\n  .euiDatePopoverButton:disabled {\n    background-color: #eef2f7;\n    color: #69707d;\n    cursor: default;\n  }\n  .euiDatePopoverButton--start {\n    text-align: right;\n  }\n  .euiDatePopoverButton--end {\n    text-align: left;\n  }\n  .euiDatePopoverContent {\n    width: 400px;\n    max-width: 100%;\n  }\n  .euiDatePopoverContent__padded {\n    padding: 8px;\n  }\n  .euiDatePopoverContent__padded--large {\n    padding: 16px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiDatePopoverContent {\n      width: 284px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiDatePopoverContent {\n      width: 284px;\n    }\n  }\n  .euiQuickSelectPopover__content {\n    width: 400px;\n    max-width: 100%;\n  }\n  .euiQuickSelectPopover__section {\n    scrollbar-width: thin;\n    max-height: 132px;\n    overflow: hidden;\n    overflow-y: auto;\n    padding: 8px 0 4px;\n  }\n  .euiQuickSelectPopover__section::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiQuickSelectPopover__section::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiQuickSelectPopover__section::-webkit-scrollbar-corner,\n  .euiQuickSelectPopover__section::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiQuickSelectPopover__buttonText {\n    margin-right: 4px !important;\n  }\n  .euiQuickSelectPopover__anchor {\n    height: 100%;\n  }\n  .euiQuickSelectPopover__sectionItem {\n    font-size: 14px;\n    line-height: 14px;\n  }\n  .euiQuickSelectPopover__sectionItem:not(:last-of-type) {\n    margin-bottom: 8px;\n  }\n  .euiQuickSelect__applyButton {\n    min-width: 0;\n  }\n  .euiRefreshInterval__startButton {\n    min-width: 90px;\n  }\n  .euiSuperDatePicker__flexWrapper {\n    max-width: calc(100% + 8px);\n    width: 606px;\n  }\n  .euiSuperDatePicker__flexWrapper--isAutoRefreshOnly {\n    width: 400px;\n  }\n  .euiSuperDatePicker__flexWrapper--noUpdateButton {\n    width: 480px;\n  }\n  .euiSuperDatePicker {\n    max-width: 100% !important;\n  }\n  .euiSuperDatePicker > .euiFormControlLayout__childrenWrapper {\n    flex: 1 1 100%;\n    overflow: hidden;\n  }\n  .euiSuperDatePicker\n    > .euiFormControlLayout__childrenWrapper\n    > .euiDatePickerRange {\n    max-width: none;\n    width: auto;\n    border-radius: 0 0 0 0;\n  }\n  .euiSuperDatePicker__startPopoverButton {\n    margin-right: -12px;\n  }\n  .euiSuperDatePicker__prettyFormat {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    display: block;\n    width: 100%;\n    padding: 0 8px;\n    line-height: 38px;\n    height: 38px;\n    word-break: break-all;\n    transition: background 150ms ease-in;\n    display: flex;\n    justify-content: space-between;\n    text-align: left;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiSuperDatePicker__prettyFormat {\n      line-height: 1em;\n    }\n  }\n  .euiSuperDatePicker__prettyFormat::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperDatePicker__prettyFormat::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperDatePicker__prettyFormat:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperDatePicker__prettyFormat:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperDatePicker__prettyFormat::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperDatePicker__prettyFormat:not(:disabled):hover,\n  .euiSuperDatePicker__prettyFormat:focus {\n    text-decoration: none;\n  }\n  .euiSuperDatePicker__prettyFormat:not(:disabled):hover\n    .euiSuperDatePicker__prettyFormatLink,\n  .euiSuperDatePicker__prettyFormat:focus\n    .euiSuperDatePicker__prettyFormatLink {\n    text-decoration: underline;\n  }\n  .euiSuperDatePicker__prettyFormat:disabled {\n    background-color: #eef2f7;\n    color: #69707d;\n    cursor: not-allowed;\n  }\n  .euiSuperDatePicker__prettyFormat:disabled\n    .euiSuperDatePicker__prettyFormatLink {\n    display: none;\n  }\n  .euiSuperDatePicker__prettyFormatLink {\n    color: #006bb4;\n    padding-left: 4px;\n    flex-shrink: 0;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiSuperDatePicker__flexWrapper {\n      width: calc(100% + 8px);\n    }\n    .euiSuperDatePicker__prettyFormatLink {\n      flex-shrink: 1;\n      min-width: 3em;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiSuperDatePicker__flexWrapper {\n      width: calc(100% + 8px);\n    }\n    .euiSuperDatePicker__prettyFormatLink {\n      flex-shrink: 1;\n      min-width: 3em;\n    }\n  }\n  .euiSuperUpdateButton {\n    min-width: 118px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiSuperUpdateButton {\n      min-width: 0;\n    }\n    .euiSuperUpdateButton .euiSuperUpdateButton__text {\n      display: none;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiSuperUpdateButton {\n      min-width: 0;\n    }\n    .euiSuperUpdateButton .euiSuperUpdateButton__text {\n      display: none;\n    }\n  }\n  .euiDataGrid {\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n    overflow: hidden;\n    height: 100%;\n  }\n  .euiDataGrid--fullScreen {\n    height: 100%;\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    z-index: 8000;\n    background: #fff;\n  }\n  .euiDataGrid--fullScreen .euiDataGrid__pagination {\n    padding-bottom: 4px;\n    background: #f5f7fa;\n    border-top: 1px solid #d3dae6;\n  }\n  .euiDataGrid--fullScreen .euiDataGrid__verticalScroll .euiDataGridRow {\n    will-change: transform;\n  }\n  .euiDataGrid__content {\n    height: 100%;\n    font-feature-settings: 'tnum' 1;\n    max-width: 100%;\n    width: 100%;\n    z-index: 2;\n    background: #fff;\n  }\n  .euiDataGrid__controls {\n    background: #fafbfd;\n    position: relative;\n    z-index: 3;\n    border: 1px solid #d3dae6;\n    padding: 4px;\n    flex-grow: 0;\n  }\n  .euiDataGrid__controls > * {\n    margin-left: 2px;\n  }\n  .euiDataGrid__controlBtn {\n    border-radius: 4px;\n  }\n  .euiDataGrid__controlBtn:focus {\n    background: #dddee1;\n  }\n  .euiDataGrid__controlBtn--active,\n  .euiDataGrid__controlBtn--active:focus {\n    font-weight: 600;\n    color: #000;\n  }\n  .euiDataGrid--bordersNone .euiDataGrid__controls {\n    border: none;\n    background: #fff;\n  }\n  .euiDataGrid--bordersHorizontal .euiDataGrid__controls {\n    border-right: none;\n    border-left: none;\n    border-top: none;\n    background: #fff;\n  }\n  .euiDataGrid__pagination {\n    padding-top: 4px;\n    flex-grow: 0;\n  }\n  .euiDataGrid__verticalScroll {\n    flex-grow: 1;\n    overflow-y: hidden;\n    height: 100%;\n  }\n  .euiDataGrid__overflow {\n    overflow-y: hidden;\n    height: 100%;\n    background: #fff;\n  }\n  .euiDataGrid__restrictBody {\n    height: 100vh;\n    overflow: hidden;\n  }\n  .euiDataGrid__controlScroll {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n    max-height: 400px;\n    padding: 8px;\n    margin: -8px;\n  }\n  .euiDataGrid__controlScroll::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiDataGrid__controlScroll::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiDataGrid__controlScroll::-webkit-scrollbar-corner,\n  .euiDataGrid__controlScroll::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiDataGrid__controlScroll:focus {\n    outline: none;\n  }\n  .euiDataGrid__controlScroll[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .euiDataGrid__focusWrap {\n    height: 100%;\n  }\n  .euiDataGrid__virtualized {\n    scrollbar-width: thin;\n    scroll-padding: 0;\n  }\n  .euiDataGrid__virtualized::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiDataGrid__virtualized::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid #fff;\n    background-clip: content-box;\n  }\n  .euiDataGrid__virtualized::-webkit-scrollbar-corner,\n  .euiDataGrid__virtualized::-webkit-scrollbar-track {\n    background-color: #fff;\n  }\n  .euiDataGridHeader {\n    display: flex;\n    z-index: 3;\n    background: #fff;\n    position: sticky;\n    top: 0;\n  }\n  .euiDataGridHeaderCell {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    font-weight: 700;\n    padding: 6px;\n    flex: 0 0 auto;\n    position: relative;\n    align-items: center;\n    display: flex;\n  }\n  .euiDataGridHeaderCell > * {\n    max-width: 100%;\n    width: 100%;\n  }\n  .euiDataGridHeaderCell.euiDataGridHeaderCell--numeric {\n    text-align: right;\n  }\n  .euiDataGridHeaderCell.euiDataGridHeaderCell--currency {\n    text-align: right;\n  }\n  .euiDataGridHeaderCell:focus {\n    border: 1px solid rgba(0, 0, 0, 0);\n    box-shadow: 0 0 0 2px rgba(0, 107, 180, 0.3);\n    border-radius: 1px;\n    z-index: 2;\n    outline: none;\n    border-top: none;\n  }\n  .euiDataGridHeaderCell:not(\n      .euiDataGridHeaderCell--controlColumn\n    ):focus-within {\n    border: 1px solid rgba(0, 0, 0, 0);\n    box-shadow: 0 0 0 2px rgba(0, 107, 180, 0.3);\n    border-radius: 1px;\n    z-index: 2;\n    outline: none;\n    border-top: none;\n  }\n  .euiDataGridHeaderCell:not(.euiDataGridHeaderCell--controlColumn)\n    .euiDataGridHeaderCell__sortingArrow {\n    margin-right: 4px;\n  }\n  .euiDataGridHeaderCell:not(.euiDataGridHeaderCell--controlColumn)\n    .euiDataGridHeaderCell__anchor {\n    width: 100%;\n  }\n  .euiDataGridHeaderCell:not(.euiDataGridHeaderCell--controlColumn)\n    .euiDataGridHeaderCell__button {\n    flex: 0 0 auto;\n    position: relative;\n    align-items: center;\n    display: flex;\n    width: 100%;\n    font-weight: 700;\n    outline: none;\n  }\n  .euiDataGridHeaderCell:not(.euiDataGridHeaderCell--controlColumn)\n    .euiDataGridHeaderCell__button\n    .euiDataGridHeaderCell__sortingArrow {\n    flex-grow: 0;\n  }\n  .euiDataGridHeaderCell:not(.euiDataGridHeaderCell--controlColumn)\n    .euiDataGridHeaderCell__content {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    overflow: hidden;\n    white-space: nowrap;\n    text-align: left;\n    flex-grow: 1;\n    align-self: baseline;\n  }\n  .euiDataGridHeaderCell:not(.euiDataGridHeaderCell--controlColumn)\n    .euiDataGridHeaderCell__icon {\n    flex-grow: 0;\n    flex-basis: auto;\n    width: auto;\n    padding-left: 4px;\n  }\n  .euiDataGridHeader__action--selected {\n    font-weight: 700 !important;\n  }\n  .euiDataGrid--bordersNone.euiDataGrid--bordersHorizontal .euiDataGridHeader {\n    background: #fff;\n  }\n  .euiDataGrid--headerUnderline .euiDataGridHeaderCell {\n    border-top: none;\n    border-left: none;\n    border-right: none;\n    border-bottom: 2px solid #d3dae6;\n    border-bottom-color: #343741;\n  }\n  .euiDataGrid--bordersNone.euiDataGrid--headerUnderline\n    .euiDataGridHeaderCell {\n    border-bottom: 2px solid #d3dae6;\n    border-color: #343741;\n  }\n  .euiDataGrid--headerShade .euiDataGridHeaderCell {\n    background: #f5f7fa;\n  }\n  .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell {\n    border-right: 1px solid #d3dae6;\n    border-bottom: 1px solid #d3dae6;\n    border-left: none;\n  }\n  .euiDataGrid--headerShade.euiDataGrid--bordersAll\n    .euiDataGridHeaderCell:first-of-type {\n    border-left: 1px solid #d3dae6;\n  }\n  .euiDataGrid--headerShade.euiDataGrid--bordersHorizontal\n    .euiDataGridHeaderCell {\n    border-top: none;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiDataGrid--bordersNone .euiDataGridHeaderCell {\n    border: none;\n  }\n  .euiDataGrid--borderhorizontal .euiDataGridHeaderCell {\n    border-top: none;\n    border-right: none;\n    border-left: none;\n  }\n  .euiDataGrid--fontSizeSmall .euiDataGridHeaderCell {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiDataGrid--fontSizeLarge .euiDataGridHeaderCell {\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n  }\n  .euiDataGrid--paddingSmall .euiDataGridHeaderCell {\n    padding: 4px;\n  }\n  .euiDataGrid--paddingLarge .euiDataGridHeaderCell {\n    padding: 8px;\n  }\n  .euiDataGrid--noControls.euiDataGrid--bordersAll .euiDataGridHeaderCell {\n    border-top: 1px solid #d3dae6;\n  }\n  .euiDataGrid--noControls.euiDataGrid--bordersHorizontal\n    .euiDataGridHeaderCell {\n    border-top: 1px solid #d3dae6;\n  }\n  .euiDataGridRowCell.euiDataGridFooterCell {\n    font-weight: 700;\n  }\n  .euiDataGrid--stickyFooter .euiDataGridFooter {\n    position: sticky;\n    bottom: 0;\n  }\n  .euiDataGrid--footerOverline .euiDataGridRowCell.euiDataGridFooterCell {\n    border-top: 2px solid #d3dae6;\n    border-top-color: #343741 !important;\n    background: #fff !important;\n  }\n  .euiDataGrid--bordersNone .euiDataGridRowCell.euiDataGridFooterCell {\n    border-left: none;\n    border-right: none;\n  }\n  .euiDataGrid--bordersHorizontal .euiDataGridRowCell.euiDataGridFooterCell {\n    border-left: none;\n    border-right: none;\n  }\n  .euiDataGrid--footerShade .euiDataGridRowCell.euiDataGridFooterCell {\n    background: #f5f7fa;\n  }\n  .euiDataGridColumnResizer {\n    position: absolute;\n    top: 0;\n    right: -8px;\n    height: 100%;\n    width: 16px;\n    cursor: ew-resize;\n    opacity: 0;\n    z-index: 2;\n  }\n  .euiDataGridColumnResizer:after {\n    content: '';\n    position: absolute;\n    left: 7px;\n    top: 0;\n    bottom: 0;\n    width: 3px;\n    background-color: #006bb4;\n  }\n  .euiDataGridColumnResizer:hover,\n  .euiDataGridColumnResizer:active {\n    opacity: 1;\n  }\n  .euiDataGridColumnResizer:hover ~ .euiDataGridHeaderCell__content,\n  .euiDataGridColumnResizer:active ~ .euiDataGridHeaderCell__content {\n    user-select: none;\n  }\n  .euiDataGridHeaderCell:last-child .euiDataGridColumnResizer {\n    right: 0;\n    width: 8px;\n  }\n  .euiDataGridHeaderCell:last-child .euiDataGridColumnResizer:after {\n    left: auto;\n    right: 0;\n  }\n  .euiDataGridRow {\n    display: flex;\n  }\n  .euiDataGridRowCell {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    padding: 6px;\n    border-right: solid 1px #edf0f5;\n    border-bottom: 1px solid #d3dae6;\n    flex: 0 0 auto;\n    background: #fff;\n    position: relative;\n    align-items: center;\n    display: flex;\n  }\n  .euiDataGridRowCell > * {\n    max-width: 100%;\n    width: 100%;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--firstColumn {\n    border-left: 1px solid #d3dae6;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--lastColumn {\n    border-right-color: #d3dae6;\n  }\n  .euiDataGridRowCell:focus {\n    border: 1px solid rgba(0, 0, 0, 0);\n    box-shadow: 0 0 0 2px rgba(0, 107, 180, 0.3);\n    border-radius: 1px;\n    z-index: 2;\n    outline: none;\n    margin-top: -1px;\n  }\n  .euiDataGridRowCell:hover .euiDataGridRowCell__expandButtonIcon {\n    animation-duration: 90ms;\n    animation-name: euiDataGridCellButtonSlideIn;\n    animation-iteration-count: 1;\n    animation-delay: 250ms;\n    animation-fill-mode: forwards;\n  }\n  .euiDataGridRowCell:hover .euiDataGridRowCell__actionButtonIcon {\n    animation-duration: 90ms;\n    animation-name: euiDataGridCellButtonSlideIn;\n    animation-iteration-count: 1;\n    animation-delay: 250ms;\n    animation-fill-mode: forwards;\n  }\n  .euiDataGridRowCell:not(:hover) .euiDataGridRowCell__expandButtonIcon,\n  .euiDataGridRowCell.euiDataGridRowCell--open\n    .euiDataGridRowCell__expandButtonIcon {\n    animation: none;\n    margin-left: 6px;\n    width: 12px;\n  }\n  .euiDataGridRowCell:not(:hover) .euiDataGridRowCell__actionButtonIcon,\n  .euiDataGridRowCell.euiDataGridRowCell--open\n    .euiDataGridRowCell__actionButtonIcon {\n    animation: none;\n    margin-left: 6px;\n    width: 12px;\n  }\n  .euiDataGridRowCell:focus .euiDataGridRowCell__actionButtonIcon {\n    margin-left: 6px;\n    width: 12px;\n  }\n  .euiDataGridRowCell:not(:hover):not(:focus):not(.euiDataGridRowCell--open)\n    .euiDataGridRowCell__expandButtonIcon,\n  .euiDataGridRowCell:not(:hover):not(:focus):not(.euiDataGridRowCell--open)\n    .euiDataGridRowCell__actionButtonIcon {\n    display: none;\n  }\n  .euiDataGridRowCell:focus:not(:first-of-type) {\n    padding-left: 5px;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--numeric {\n    text-align: right;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--currency {\n    text-align: right;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--uppercase {\n    text-transform: uppercase;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--lowercase {\n    text-transform: lowercase;\n  }\n  .euiDataGridRowCell.euiDataGridRowCell--capitalize {\n    text-transform: capitalize;\n  }\n  .euiDataGridRowCell:not(.euiDataGridRowCell--controlColumn)\n    .euiDataGridRowCell__content,\n  .euiDataGridRowCell:not(.euiDataGridRowCell--controlColumn)\n    .euiDataGridRowCell__truncate,\n  .euiDataGridRowCell:not(\n      .euiDataGridRowCell--controlColumn\n    ).euiDataGridRowCell__truncate,\n  .euiDataGridRowCell:not(.euiDataGridRowCell--controlColumn)\n    .euiDataGridRowCell__expandContent {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    overflow: hidden;\n    white-space: nowrap;\n  }\n  .euiDataGridRowCell__popover {\n    scrollbar-width: thin;\n    overflow: auto;\n    max-width: 400px !important;\n    max-height: 400px !important;\n  }\n  .euiDataGridRowCell__popover::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiDataGridRowCell__popover::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiDataGridRowCell__popover::-webkit-scrollbar-corner,\n  .euiDataGridRowCell__popover::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiDataGridRowCell__expand {\n    width: 100%;\n    max-width: 100%;\n  }\n  .euiDataGridRowCell__expandFlex {\n    display: flex;\n    align-items: center;\n    height: 100%;\n  }\n  .euiDataGridRowCell__expandContent {\n    flex-grow: 1;\n  }\n  .euiDataGridRowCell__contentByHeight {\n    flex-grow: 1;\n    height: 100%;\n  }\n  .euiDataGridRowCell__alignBaseLine {\n    align-items: baseline;\n  }\n  .euiDataGridRowCell__expandButton {\n    display: flex;\n    flex-grow: 0;\n  }\n  .euiDataGridRowCell__expandButtonIcon {\n    height: 12px;\n    border-radius: 2px;\n    width: 0;\n    overflow: hidden;\n    transition: none;\n    box-shadow: none !important;\n    border: none;\n  }\n  .euiDataGridRowCell__expandButtonIcon-isActive {\n    margin-left: 6px;\n    width: 12px;\n  }\n  .euiDataGridRowCell__actionButtonIcon {\n    height: 12px;\n    border-radius: 2px;\n    width: 0;\n    overflow: hidden;\n    transition: none;\n  }\n  .euiDataGrid--rowHoverHighlight .euiDataGridRow:hover .euiDataGridRowCell {\n    background-color: #fffcdd !important;\n  }\n  .euiDataGrid--stripes .euiDataGridRowCell.euiDataGridRowCell--stripe {\n    background: #f5f7fa;\n  }\n  .euiDataGrid--bordersNone .euiDataGridRowCell {\n    border-color: rgba(0, 0, 0, 0) !important;\n  }\n  .euiDataGrid--bordersHorizontal .euiDataGridRowCell {\n    border-right-color: rgba(0, 0, 0, 0);\n    border-left-color: rgba(0, 0, 0, 0);\n  }\n  .euiDataGrid--fontSizeSmall .euiDataGridRowCell {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiDataGridRowCell--fontSizeSmall {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiDataGrid--fontSizeLarge .euiDataGridRowCell {\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n  }\n  .euiDataGridRowCell--fontSizeLarge {\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n  }\n  .euiDataGrid--paddingSmall .euiDataGridRowCell {\n    padding: 4px;\n  }\n  .euiDataGrid--paddingSmall .euiDataGridRowCell:focus:not(:first-of-type) {\n    padding-left: 3px;\n  }\n  .euiDataGridRowCell--paddingSmall {\n    padding: 4px;\n  }\n  .euiDataGrid--paddingLarge .euiDataGridRowCell {\n    padding: 8px;\n  }\n  .euiDataGrid--paddingLarge .euiDataGridRowCell:focus:not(:first-of-type) {\n    padding-left: 7px;\n  }\n  .euiDataGridRowCell--paddingLarge {\n    padding: 8px;\n  }\n  @keyframes euiDataGridCellButtonSlideIn {\n    from {\n      margin-left: 0;\n      width: 0;\n    }\n    to {\n      margin-left: 6px;\n      width: 12px;\n    }\n  }\n  .euiDataGridColumnSelector__item {\n    padding: 4px;\n  }\n  .euiDataGridColumnSelector__item-isDragging {\n    box-shadow:\n      0 12px 24px 0 rgba(65, 78, 101, 0.1),\n      0 6px 12px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n    background: #fff;\n  }\n  .euiDataGridColumnSelector__columnList {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n    max-height: 400px;\n    margin: 0 -8px;\n  }\n  .euiDataGridColumnSelector__columnList::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiDataGridColumnSelector__columnList::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiDataGridColumnSelector__columnList::-webkit-scrollbar-corner,\n  .euiDataGridColumnSelector__columnList::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiDataGridColumnSelector__columnList:focus {\n    outline: none;\n  }\n  .euiDataGridColumnSelector__columnList[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .euiDataGridColumnSelector__itemLabel {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiDataGridColumnSelectorPopover {\n    transform: none !important;\n    transition: none !important;\n    margin-top: -8px;\n    min-width: 192px;\n  }\n  .euiDataGridColumnSorting__item-isDragging {\n    box-shadow:\n      0 12px 24px 0 rgba(65, 78, 101, 0.1),\n      0 6px 12px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n    background: #fff;\n  }\n  .euiDataGridColumnSortingPopover {\n    transform: none !important;\n    transition: none !important;\n    margin-top: -8px;\n    min-width: 192px;\n  }\n  .euiDataGridColumnSorting__fieldList {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n    padding-top: 4px;\n    padding-bottom: 4px;\n    max-height: 300px;\n  }\n  .euiDataGridColumnSorting__fieldList::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiDataGridColumnSorting__fieldList::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiDataGridColumnSorting__fieldList::-webkit-scrollbar-corner,\n  .euiDataGridColumnSorting__fieldList::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiDataGridColumnSorting__fieldList:focus {\n    outline: none;\n  }\n  .euiDataGridColumnSorting__fieldList[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .euiDataGridColumnSorting__field {\n    display: block;\n    padding: 4px 8px;\n    width: 100%;\n    outline-offset: -3px;\n  }\n  .euiDataGridColumnSorting__field:hover {\n    cursor: pointer;\n    text-decoration: underline;\n  }\n  .euiDataGridColumnSorting__field:focus {\n    cursor: pointer;\n    text-decoration: underline;\n    background-color: #e6f0f8;\n  }\n  .euiDataGridColumnSorting__field:disabled {\n    cursor: not-allowed;\n    text-decoration: none;\n    color: #afb0b3;\n  }\n  .euiDataGridColumnSorting__orderButtons {\n    padding-left: 24px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiDataGridColumnSorting__orderButtons {\n      padding-left: 4px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiDataGridColumnSorting__orderButtons {\n      padding-left: 4px;\n    }\n  }\n  .euiDataGridColumnSorting__orderButtons .euiDataGridColumnSorting__order {\n    min-width: 200px;\n    border: none;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiDataGridColumnSorting__orderButtons .euiDataGridColumnSorting__order {\n      min-width: unset;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiDataGridColumnSorting__orderButtons .euiDataGridColumnSorting__order {\n      min-width: unset;\n    }\n  }\n  .euiDataGridColumnSorting__orderButtons\n    .euiDataGridColumnSorting__order\n    button {\n    font-size: 12px !important;\n  }\n  .euiDescriptionList.euiDescriptionList--row .euiDescriptionList__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    line-height: 1.5;\n    margin-top: 16px;\n  }\n  .euiDescriptionList.euiDescriptionList--row\n    .euiDescriptionList__title:first-of-type {\n    margin-top: 0;\n  }\n  .euiDescriptionList.euiDescriptionList--row .euiDescriptionList__description {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--center {\n    text-align: center;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--reverse\n    .euiDescriptionList__title {\n    color: #343741;\n    font-weight: 400;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--reverse\n    .euiDescriptionList__description {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--compressed\n    .euiDescriptionList__title {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--compressed\n    .euiDescriptionList__description {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--compressed.euiDescriptionList--reverse\n    .euiDescriptionList__title {\n    color: #343741;\n    font-weight: 400;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--row.euiDescriptionList--compressed.euiDescriptionList--reverse\n    .euiDescriptionList__description {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--column,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn {\n    display: flex;\n    align-items: stretch;\n    flex-wrap: wrap;\n  }\n  .euiDescriptionList.euiDescriptionList--column > *,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn > * {\n    margin-top: 16px;\n  }\n  .euiDescriptionList.euiDescriptionList--column > *:first-child,\n  .euiDescriptionList.euiDescriptionList--column > :nth-child(2),\n  .euiDescriptionList.euiDescriptionList--responsiveColumn > *:first-child,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn > :nth-child(2) {\n    margin-top: 0;\n  }\n  .euiDescriptionList.euiDescriptionList--column .euiDescriptionList__title,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn\n    .euiDescriptionList__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    line-height: 1.5;\n    width: 50%;\n    padding-right: 8px;\n  }\n  .euiDescriptionList.euiDescriptionList--column\n    .euiDescriptionList__description,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn\n    .euiDescriptionList__description {\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    width: 50%;\n    padding-left: 8px;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--center\n    .euiDescriptionList__title,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--center\n    .euiDescriptionList__title {\n    text-align: right;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--reverse\n    .euiDescriptionList__title,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--reverse\n    .euiDescriptionList__title {\n    color: #343741;\n    font-weight: 400;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--reverse\n    .euiDescriptionList__description,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--reverse\n    .euiDescriptionList__description {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--compressed\n    .euiDescriptionList__title,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--compressed\n    .euiDescriptionList__title {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--compressed\n    .euiDescriptionList__description,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--compressed\n    .euiDescriptionList__description {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--compressed.euiDescriptionList--reverse\n    .euiDescriptionList__title,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--compressed.euiDescriptionList--reverse\n    .euiDescriptionList__title {\n    color: #343741;\n    font-weight: 400;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--column.euiDescriptionList--compressed.euiDescriptionList--reverse\n    .euiDescriptionList__description,\n  .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--compressed.euiDescriptionList--reverse\n    .euiDescriptionList__description {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    line-height: 1.5;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiDescriptionList.euiDescriptionList--responsiveColumn {\n      display: block;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn\n      .euiDescriptionList__title,\n    .euiDescriptionList.euiDescriptionList--responsiveColumn\n      .euiDescriptionList__description {\n      width: 100%;\n      padding: 0;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn\n      .euiDescriptionList__description {\n      font-size: 14px;\n      font-size: 0.875rem;\n      line-height: 1.5;\n      margin-top: 0;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--center\n      .euiDescriptionList__title,\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--center\n      .euiDescriptionList__description {\n      text-align: center;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--reverse\n      .euiDescriptionList__title {\n      font-size: 14px;\n      font-size: 0.875rem;\n      line-height: 1.5;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--reverse\n      .euiDescriptionList__description {\n      color: #1a1c21;\n      font-size: 16px;\n      font-size: 1rem;\n      line-height: 1.5rem;\n      font-weight: 600;\n      letter-spacing: -0.02em;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiDescriptionList.euiDescriptionList--responsiveColumn {\n      display: block;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn\n      .euiDescriptionList__title,\n    .euiDescriptionList.euiDescriptionList--responsiveColumn\n      .euiDescriptionList__description {\n      width: 100%;\n      padding: 0;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn\n      .euiDescriptionList__description {\n      font-size: 14px;\n      font-size: 0.875rem;\n      line-height: 1.5;\n      margin-top: 0;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--center\n      .euiDescriptionList__title,\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--center\n      .euiDescriptionList__description {\n      text-align: center;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--reverse\n      .euiDescriptionList__title {\n      font-size: 14px;\n      font-size: 0.875rem;\n      line-height: 1.5;\n    }\n    .euiDescriptionList.euiDescriptionList--responsiveColumn.euiDescriptionList--reverse\n      .euiDescriptionList__description {\n      color: #1a1c21;\n      font-size: 16px;\n      font-size: 1rem;\n      line-height: 1.5rem;\n      font-weight: 600;\n      letter-spacing: -0.02em;\n    }\n  }\n  .euiDescriptionList.euiDescriptionList--inline .euiDescriptionList__title {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    display: inline;\n    border-radius: 4px;\n    font-weight: 400;\n    background: #f5f7fa;\n    border: 1px solid #d3dae6;\n    padding: 0 4px;\n    margin: 0 4px;\n  }\n  .euiDescriptionList.euiDescriptionList--inline\n    .euiDescriptionList__title:first-of-type {\n    margin-left: 0;\n  }\n  .euiDescriptionList.euiDescriptionList--inline\n    .euiDescriptionList__description {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    display: inline;\n    word-break: break-all;\n  }\n  .euiDescriptionList.euiDescriptionList--inline.euiDescriptionList--compressed\n    .euiDescriptionList__title {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--inline.euiDescriptionList--compressed\n    .euiDescriptionList__description {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiDescriptionList.euiDescriptionList--inline.euiDescriptionList--center {\n    text-align: center;\n  }\n  .euiDraggable.euiDraggable--isDragging {\n    z-index: 9000 !important;\n  }\n  .euiDraggable.euiDraggable--hasClone:not(.euiDraggable--isDragging) {\n    transform: none !important;\n  }\n  .euiDraggable.euiDraggable--withoutDropAnimation {\n    transition-duration: 0.001s !important;\n  }\n  .euiDraggable:focus > .euiDraggable__item,\n  .euiDraggable.euiDraggable--hasCustomDragHandle\n    > .euiDraggable__item\n    [data-react-beautiful-dnd-drag-handle]:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiDraggable .euiDraggable__item.euiDraggable__item--isDisabled {\n    cursor: not-allowed;\n  }\n  .euiDraggable--s {\n    padding: 2px;\n  }\n  .euiDraggable--m {\n    padding: 4px;\n  }\n  .euiDraggable--l {\n    padding: 8px;\n  }\n  .euiDroppable {\n    transition: background-color 500ms ease;\n  }\n  .euiDroppable.euiDroppable--isDraggingType:not(.euiDroppable--isDisabled) {\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiDroppable.euiDroppable--isDraggingType:not(\n      .euiDroppable--isDisabled\n    ).euiDroppable--isDraggingOver {\n    background-color: rgba(1, 125, 115, 0.25);\n  }\n  .euiDroppable .euiDroppable__placeholder.euiDroppable__placeholder--isHidden {\n    display: none !important;\n  }\n  .euiDroppable--withPanel {\n    background-color: #fff;\n    border: 1px solid #d3dae6;\n    border-radius: 4px;\n    flex-grow: 1;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--flexGrowZero {\n    flex-grow: 0;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--noBorder {\n    border: none;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--hasShadow {\n    box-shadow:\n      0 2px 2px -1px rgba(152, 162, 179, 0.3),\n      0 1px 5px -2px rgba(152, 162, 179, 0.3);\n    border: 1px solid #d3dae6;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--isClickable {\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--isClickable:enabled {\n    display: block;\n    width: 100%;\n    text-align: left;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--isClickable:hover,\n  .euiDroppable--withPanel.euiDroppable--withPanel--isClickable:focus {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 162, 179, 0.15),\n      0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    transform: translateY(-2px);\n    cursor: pointer;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--borderRadiusNone {\n    border-radius: 0;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--borderRadiusMedium {\n    border-radius: 4px;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--transparent {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--plain {\n    background-color: #fff;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--subdued {\n    background-color: #fafbfd;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--accent {\n    background-color: #fce7f1;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--primary {\n    background-color: #e6f0f8;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--success {\n    background-color: #e6f2f1;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--warning {\n    background-color: #fef6e6;\n  }\n  .euiDroppable--withPanel.euiDroppable--withPanel--danger {\n    background-color: #f8e9e9;\n  }\n  .euiDroppable--withPanel {\n    box-shadow:\n      0 6px 12px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -1px rgba(152, 162, 179, 0.2),\n      0 2px 2px 0 rgba(152, 162, 179, 0.2);\n    border-radius: 4px;\n  }\n  .euiDroppable--noGrow {\n    flex-grow: 0;\n  }\n  .euiDroppable--grow {\n    flex-grow: 1;\n  }\n  .euiDroppable--s {\n    padding: 2px;\n  }\n  .euiDroppable--m {\n    padding: 4px;\n  }\n  .euiDroppable--l {\n    padding: 8px;\n  }\n  .euiEmptyPrompt {\n    max-width: 36em;\n    text-align: center;\n    padding: 24px;\n    margin: auto;\n  }\n  .euiErrorBoundary {\n    background: repeating-linear-gradient(\n      45deg,\n      rgba(189, 39, 30, 0.25),\n      rgba(189, 39, 30, 0.25) 1px,\n      rgba(189, 39, 30, 0.05) 1px,\n      rgba(189, 39, 30, 0.05) 20px\n    );\n    overflow: auto;\n    padding: 16px;\n  }\n  .euiErrorBoundary__text {\n    background-color: #fff;\n    padding: 8px;\n  }\n  .euiErrorBoundary__stack {\n    white-space: pre-wrap;\n  }\n  .euiExpression {\n    overflow-wrap: break-word !important;\n    word-wrap: break-word !important;\n    word-break: break-word;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    font-family: 'Roboto Mono', Consolas, Menlo, Courier, monospace;\n    letter-spacing: normal;\n    border-bottom: 2px solid rgba(0, 0, 0, 0);\n    display: inline-block;\n    text-align: left;\n    padding: 2px 0;\n    transition: all 250ms ease-in-out;\n    color: #343741;\n  }\n  .euiExpression:focus {\n    border-bottom-style: solid;\n  }\n  .euiExpression + .euiExpression {\n    margin-left: 8px;\n  }\n  .euiExpression.euiExpression--columns {\n    border-color: rgba(0, 0, 0, 0);\n    border-bottom-style: solid;\n    margin-bottom: 4px;\n  }\n  .euiExpression.euiExpression--truncate {\n    max-width: 100%;\n  }\n  .euiExpression.euiExpression--truncate .euiExpression__description,\n  .euiExpression.euiExpression--truncate .euiExpression__value {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    display: inline-block;\n    vertical-align: bottom;\n  }\n  .euiExpression-isUppercase .euiExpression__description {\n    text-transform: uppercase;\n  }\n  .euiExpression-isClickable {\n    cursor: pointer;\n    border-bottom: 2px dotted #d3dae6;\n  }\n  .euiExpression-isClickable:hover:not(:disabled) {\n    border-bottom-style: solid;\n    transform: translateY(-1px);\n  }\n  .euiExpression__icon {\n    margin-left: 4px;\n  }\n  .euiExpression-isActive {\n    border-bottom-style: solid;\n  }\n  .euiExpression--columns {\n    width: 100%;\n    display: flex;\n    padding: 4px;\n    border-radius: 4px;\n  }\n  .euiExpression--columns.euiExpression-isClickable {\n    background-color: #f5f7fa;\n  }\n  .euiExpression--columns.euiExpression-isClickable:focus\n    .euiExpression__description,\n  .euiExpression--columns.euiExpression-isClickable:focus .euiExpression__value,\n  .euiExpression--columns.euiExpression-isClickable:hover:not(:disabled)\n    .euiExpression__description,\n  .euiExpression--columns.euiExpression-isClickable:hover:not(:disabled)\n    .euiExpression__value {\n    text-decoration: underline;\n  }\n  .euiExpression--columns .euiExpression__description {\n    text-align: right;\n    margin-right: 8px;\n    flex-shrink: 0;\n  }\n  .euiExpression--columns .euiExpression__value {\n    flex-grow: 1;\n  }\n  .euiExpression--columns .euiExpression__icon {\n    margin-top: 4px;\n  }\n  .euiExpression--subdued:focus {\n    background-color: rgba(106, 113, 125, 0.1);\n  }\n  .euiExpression--subdued.euiExpression-isActive {\n    border-bottom-color: #6a717d;\n    border-color: #6a717d;\n  }\n  .euiExpression--subdued .euiExpression__description {\n    color: #6a717d;\n  }\n  .euiExpression--primary:focus {\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiExpression--primary.euiExpression-isActive {\n    border-bottom-color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiExpression--primary .euiExpression__description {\n    color: #006bb4;\n  }\n  .euiExpression--success:focus {\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiExpression--success.euiExpression-isActive {\n    border-bottom-color: #017d73;\n    border-color: #017d73;\n  }\n  .euiExpression--success .euiExpression__description {\n    color: #017d73;\n  }\n  .euiExpression--secondary:focus {\n    background-color: rgba(1, 125, 115, 0.1);\n  }\n  .euiExpression--secondary.euiExpression-isActive {\n    border-bottom-color: #017d73;\n    border-color: #017d73;\n  }\n  .euiExpression--secondary .euiExpression__description {\n    color: #017d73;\n  }\n  .euiExpression--warning:focus {\n    background-color: rgba(155, 105, 0, 0.1);\n  }\n  .euiExpression--warning.euiExpression-isActive {\n    border-bottom-color: #9b6900;\n    border-color: #9b6900;\n  }\n  .euiExpression--warning .euiExpression__description {\n    color: #9b6900;\n  }\n  .euiExpression--danger:focus {\n    background-color: rgba(189, 39, 30, 0.1);\n  }\n  .euiExpression--danger.euiExpression-isActive {\n    border-bottom-color: #bd271e;\n    border-color: #bd271e;\n  }\n  .euiExpression--danger .euiExpression__description {\n    color: #bd271e;\n  }\n  .euiExpression--accent:focus {\n    background-color: rgba(221, 10, 115, 0.1);\n  }\n  .euiExpression--accent.euiExpression-isActive {\n    border-bottom-color: #dd0a73;\n    border-color: #dd0a73;\n  }\n  .euiExpression--accent .euiExpression__description {\n    color: #dd0a73;\n  }\n  .euiFacetButton {\n    display: inline-block;\n    appearance: none;\n    cursor: pointer;\n    height: 40px;\n    line-height: 40px;\n    text-align: center;\n    white-space: nowrap;\n    max-width: 100%;\n    vertical-align: middle;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    height: 32px;\n    text-align: left;\n    text-decoration: none;\n    transition: all 150ms ease-in;\n  }\n  .euiFacetButton:hover:not(:disabled) .euiFacetButton__text,\n  .euiFacetButton:focus:not(:disabled) .euiFacetButton__text {\n    text-decoration: underline;\n  }\n  .euiFacetButton:focus {\n    background-color: #e6f0f8;\n    box-shadow:\n      -4px 0 #e6f0f8,\n      4px 0 #e6f0f8;\n  }\n  .euiFacetButton:disabled {\n    color: #afb0b3;\n    pointer-events: none;\n  }\n  .euiFacetButton:disabled .euiFacetButton__content {\n    pointer-events: auto;\n    cursor: not-allowed;\n  }\n  .euiFacetButton:disabled .euiFacetButton__icon,\n  .euiFacetButton:disabled .euiFacetButton__quantity {\n    opacity: 0.5;\n  }\n  .euiFacetButton:disabled:focus {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiFacetButton:disabled:hover,\n  .euiFacetButton:disabled:focus {\n    text-decoration: none;\n  }\n  .euiFacetButton__content {\n    height: 100%;\n    width: 100%;\n    vertical-align: middle;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n  .euiFacetButton__content .euiButtonContent__icon,\n  .euiFacetButton__content .euiButtonContent__spinner {\n    flex-shrink: 0;\n  }\n  .euiFacetButton__content > * + * {\n    margin-inline-start: 8px;\n  }\n  .euiFacetButton__text {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    flex-grow: 1;\n    vertical-align: middle;\n  }\n  .euiFacetButton__text::after {\n    display: block;\n    content: attr(data-text);\n    font-weight: 700;\n    height: 0;\n    overflow: hidden;\n    visibility: hidden;\n  }\n  .euiFacetButton--isSelected .euiFacetButton__text {\n    font-weight: 700;\n  }\n  .euiFacetButton__icon {\n    transition: all 150ms ease-in;\n  }\n  .euiFacetGroup--gutterNone .euiFacetButton {\n    margin-top: 0;\n    margin-bottom: 0;\n  }\n  .euiFacetGroup--gutterNone.euiFacetGroup--horizontal {\n    margin-left: -12px;\n  }\n  .euiFacetGroup--gutterNone.euiFacetGroup--horizontal .euiFacetButton {\n    margin-left: 12px;\n    max-width: calc(100% - 12px);\n  }\n  .euiFacetGroup--gutterSmall .euiFacetButton {\n    margin-top: 2px;\n    margin-bottom: 2px;\n  }\n  .euiFacetGroup--gutterSmall.euiFacetGroup--horizontal {\n    margin-left: -16px;\n  }\n  .euiFacetGroup--gutterSmall.euiFacetGroup--horizontal .euiFacetButton {\n    margin-left: 16px;\n    max-width: calc(100% - 16px);\n  }\n  .euiFacetGroup--gutterMedium .euiFacetButton {\n    margin-top: 4px;\n    margin-bottom: 4px;\n  }\n  .euiFacetGroup--gutterMedium.euiFacetGroup--horizontal {\n    margin-left: -20px;\n  }\n  .euiFacetGroup--gutterMedium.euiFacetGroup--horizontal .euiFacetButton {\n    margin-left: 20px;\n    max-width: calc(100% - 20px);\n  }\n  .euiFacetGroup--gutterLarge .euiFacetButton {\n    margin-top: 6px;\n    margin-bottom: 6px;\n  }\n  .euiFacetGroup--gutterLarge.euiFacetGroup--horizontal {\n    margin-left: -24px;\n  }\n  .euiFacetGroup--gutterLarge.euiFacetGroup--horizontal .euiFacetButton {\n    margin-left: 24px;\n    max-width: calc(100% - 24px);\n  }\n  .euiFilterGroup {\n    display: inline-flex;\n    max-width: 100%;\n    border-right: 1px solid rgba(16, 38, 118, 0.1);\n    box-shadow:\n      0 1px 2px -1px rgba(152, 162, 179, 0.2),\n      0 3px 3px -2px rgba(152, 162, 179, 0.2);\n    overflow: hidden;\n  }\n  .euiFilterGroup > * {\n    flex: 1 1 auto;\n    min-width: 48px;\n  }\n  .euiFilterGroup > .euiFilterButton--noGrow {\n    flex-grow: 0;\n  }\n  .euiFilterGroup > .euiFilterButton-hasNotification {\n    min-width: 96px;\n  }\n  .euiFilterGroup > .euiFilterButton--hasIcon {\n    min-width: 128px;\n  }\n  .euiFilterGroup .euiPopover__anchor {\n    display: block;\n  }\n  .euiFilterGroup .euiPopover__anchor .euiFilterButton {\n    width: 100%;\n  }\n  .euiFilterGroup--fullWidth {\n    display: flex;\n  }\n  .euiFilterGroup__popoverPanel {\n    width: 288px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiFilterGroup {\n      flex-wrap: wrap;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiFilterGroup {\n      flex-wrap: wrap;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .euiFilterGroup {\n      display: flex;\n    }\n    .euiFilterGroup .euiFilterButton {\n      flex-grow: 1 !important;\n    }\n  }\n  .euiFilterButton {\n    background-color: #fbfcfd;\n    height: 40px;\n    width: auto;\n    border: 1px solid rgba(16, 38, 118, 0.1);\n    border-right: none;\n    font-size: 14px;\n  }\n  .euiFilterButton:disabled {\n    color: #afb0b3;\n    pointer-events: none;\n  }\n  .euiFilterButton:disabled .euiFilterButton__notification {\n    opacity: 0.5;\n  }\n  .euiFilterButton:hover:not(:disabled),\n  .euiFilterButton:focus:not(:disabled) {\n    text-decoration: none;\n  }\n  .euiFilterButton:hover:not(:disabled) .euiFilterButton__textShift,\n  .euiFilterButton:focus:not(:disabled) .euiFilterButton__textShift {\n    text-decoration: underline;\n  }\n  .euiFilterButton-hasActiveFilters {\n    font-weight: 700;\n  }\n  .euiFilterButton--hasIcon .euiButtonEmpty__content {\n    justify-content: space-between;\n  }\n  .euiFilterButton--withNext + .euiFilterButton {\n    margin-left: -4px;\n    border-left: none;\n  }\n  .euiFilterButton-isSelected {\n    background-color: #f5f7fa;\n  }\n  .euiFilterButton__text-hasNotification {\n    display: flex;\n    align-items: center;\n  }\n  .euiFilterButton__notification {\n    margin-left: 8px;\n    vertical-align: text-bottom;\n  }\n  .euiFilterButton__textShift {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    min-width: 48px;\n  }\n  .euiFilterButton__textShift::after {\n    display: block;\n    content: attr(data-text);\n    font-weight: 700;\n    height: 0;\n    overflow: hidden;\n    visibility: hidden;\n  }\n  .euiFilterSelectItem {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    padding: 4px 12px;\n    display: block;\n    width: 100%;\n    text-align: left;\n    color: #343741;\n    border-bottom: 1px solid #d3dae6;\n    border-color: #eef2f7;\n    outline-offset: -3px;\n  }\n  .euiFilterSelectItem:hover {\n    cursor: pointer;\n    text-decoration: underline;\n  }\n  .euiFilterSelectItem:focus {\n    cursor: pointer;\n    text-decoration: underline;\n    background-color: #e6f0f8;\n  }\n  .euiFilterSelectItem:disabled {\n    cursor: not-allowed;\n    text-decoration: none;\n    color: #afb0b3;\n  }\n  .euiFilterSelectItem:focus,\n  .euiFilterSelectItem-isFocused {\n    background-color: #e6f0f8;\n    color: #006bb4;\n  }\n  .euiFilterSelectItem__content {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n  }\n  .euiFilterSelect__items {\n    scrollbar-width: thin;\n    overflow-y: auto;\n    max-height: 480px;\n  }\n  .euiFilterSelect__items::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiFilterSelect__items::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiFilterSelect__items::-webkit-scrollbar-corner,\n  .euiFilterSelect__items::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiFilterSelect__note {\n    height: 64px;\n    text-align: center;\n    display: flex;\n    align-items: center;\n    justify-content: space-around;\n  }\n  .euiFilterSelect__noteContent {\n    color: #69707d;\n    font-size: 14px;\n  }\n  .euiFlexGroup {\n    display: flex;\n    align-items: stretch;\n    flex-grow: 1;\n  }\n  .euiFlexGroup .euiFlexItem {\n    flex-grow: 1;\n    flex-basis: 0%;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiFlexGroup .euiFlexItem {\n      min-width: 1px;\n    }\n  }\n  .euiFlexGroup--gutterExtraSmall {\n    margin: -2px;\n  }\n  .euiFlexGroup--gutterExtraSmall > .euiFlexItem {\n    margin: 2px;\n  }\n  .euiFlexGroup--gutterSmall {\n    margin: -4px;\n  }\n  .euiFlexGroup--gutterSmall > .euiFlexItem {\n    margin: 4px;\n  }\n  .euiFlexGroup--gutterMedium {\n    margin: -8px;\n  }\n  .euiFlexGroup--gutterMedium > .euiFlexItem {\n    margin: 8px;\n  }\n  .euiFlexGroup--gutterLarge {\n    margin: -12px;\n  }\n  .euiFlexGroup--gutterLarge > .euiFlexItem {\n    margin: 12px;\n  }\n  .euiFlexGroup--gutterExtraLarge {\n    margin: -20px;\n  }\n  .euiFlexGroup--gutterExtraLarge > .euiFlexItem {\n    margin: 20px;\n  }\n  .euiFlexGroup--justifyContentSpaceEvenly {\n    justify-content: space-evenly;\n  }\n  .euiFlexGroup--justifyContentSpaceBetween {\n    justify-content: space-between;\n  }\n  .euiFlexGroup--justifyContentSpaceAround {\n    justify-content: space-around;\n  }\n  .euiFlexGroup--justifyContentCenter {\n    justify-content: center;\n  }\n  .euiFlexGroup--justifyContentFlexEnd {\n    justify-content: flex-end;\n  }\n  .euiFlexGroup--alignItemsFlexStart {\n    align-items: flex-start;\n  }\n  .euiFlexGroup--alignItemsCenter {\n    align-items: center;\n  }\n  .euiFlexGroup--alignItemsFlexEnd {\n    align-items: flex-end;\n  }\n  .euiFlexGroup--alignItemsBaseline {\n    align-items: baseline;\n  }\n  .euiFlexGroup--directionRow {\n    flex-direction: row;\n  }\n  .euiFlexGroup--directionRowReverse {\n    flex-direction: row-reverse;\n  }\n  .euiFlexGroup--directionColumn {\n    flex-direction: column;\n  }\n  .euiFlexGroup--directionColumnReverse {\n    flex-direction: column-reverse;\n  }\n  .euiFlexGroup--wrap {\n    flex-wrap: wrap;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiFlexGroup--responsive {\n      flex-wrap: wrap;\n      margin-left: 0;\n      margin-right: 0;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiFlexGroup--responsive {\n      flex-wrap: wrap;\n      margin-left: 0;\n      margin-right: 0;\n    }\n  }\n  .euiFlexGrid {\n    display: flex;\n    flex-wrap: wrap;\n    margin-bottom: 0;\n  }\n  .euiFlexGrid > .euiFlexItem {\n    flex-grow: 0;\n  }\n  .euiFlexGrid > .euiFlexItem.euiFlexItem--flexGrowZero {\n    flex-grow: 0 !important;\n    flex-basis: auto !important;\n  }\n  .euiFlexGrid--directionColumn {\n    display: block;\n    column-gap: 0;\n  }\n  .euiFlexGrid--directionColumn > .euiFlexItem {\n    display: inline-block;\n    line-height: initial;\n  }\n  .euiFlexGrid--gutterNone {\n    margin: 0px;\n    align-items: stretch;\n  }\n  .euiFlexGrid--gutterNone > .euiFlexItem {\n    margin: 0px;\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--fourths > .euiFlexItem {\n    flex-basis: calc(25% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--fourths.euiFlexGrid--directionColumn {\n    column-count: 4;\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--fourths.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--thirds > .euiFlexItem {\n    flex-basis: calc(33.3% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--thirds.euiFlexGrid--directionColumn {\n    column-count: 3;\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--thirds.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--halves > .euiFlexItem {\n    flex-basis: calc(50% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--halves.euiFlexGrid--directionColumn {\n    column-count: 2;\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--halves.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--single > .euiFlexItem {\n    flex-basis: calc(100% - 0px);\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--single.euiFlexGrid--directionColumn {\n    column-count: 1;\n  }\n  .euiFlexGrid--gutterNone.euiFlexGrid--single.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 0px);\n  }\n  .euiFlexGrid--gutterSmall {\n    margin: -4px;\n    align-items: stretch;\n  }\n  .euiFlexGrid--gutterSmall > .euiFlexItem {\n    margin: 4px;\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--fourths > .euiFlexItem {\n    flex-basis: calc(25% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--fourths.euiFlexGrid--directionColumn {\n    column-count: 4;\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--fourths.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--thirds > .euiFlexItem {\n    flex-basis: calc(33.3% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--thirds.euiFlexGrid--directionColumn {\n    column-count: 3;\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--thirds.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--halves > .euiFlexItem {\n    flex-basis: calc(50% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--halves.euiFlexGrid--directionColumn {\n    column-count: 2;\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--halves.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--single > .euiFlexItem {\n    flex-basis: calc(100% - 8px);\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--single.euiFlexGrid--directionColumn {\n    column-count: 1;\n  }\n  .euiFlexGrid--gutterSmall.euiFlexGrid--single.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 8px);\n  }\n  .euiFlexGrid--gutterMedium {\n    margin: -8px;\n    align-items: stretch;\n  }\n  .euiFlexGrid--gutterMedium > .euiFlexItem {\n    margin: 8px;\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--fourths > .euiFlexItem {\n    flex-basis: calc(25% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--fourths.euiFlexGrid--directionColumn {\n    column-count: 4;\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--fourths.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--thirds > .euiFlexItem {\n    flex-basis: calc(33.3% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--thirds.euiFlexGrid--directionColumn {\n    column-count: 3;\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--thirds.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--halves > .euiFlexItem {\n    flex-basis: calc(50% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--halves.euiFlexGrid--directionColumn {\n    column-count: 2;\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--halves.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--single > .euiFlexItem {\n    flex-basis: calc(100% - 16px);\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--single.euiFlexGrid--directionColumn {\n    column-count: 1;\n  }\n  .euiFlexGrid--gutterMedium.euiFlexGrid--single.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 16px);\n  }\n  .euiFlexGrid--gutterLarge {\n    margin: -12px;\n    align-items: stretch;\n  }\n  .euiFlexGrid--gutterLarge > .euiFlexItem {\n    margin: 12px;\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--fourths > .euiFlexItem {\n    flex-basis: calc(25% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--fourths.euiFlexGrid--directionColumn {\n    column-count: 4;\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--fourths.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--thirds > .euiFlexItem {\n    flex-basis: calc(33.3% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--thirds.euiFlexGrid--directionColumn {\n    column-count: 3;\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--thirds.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--halves > .euiFlexItem {\n    flex-basis: calc(50% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--halves.euiFlexGrid--directionColumn {\n    column-count: 2;\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--halves.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--single > .euiFlexItem {\n    flex-basis: calc(100% - 24px);\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--single.euiFlexGrid--directionColumn {\n    column-count: 1;\n  }\n  .euiFlexGrid--gutterLarge.euiFlexGrid--single.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 24px);\n  }\n  .euiFlexGrid--gutterXLarge {\n    margin: -16px;\n    align-items: stretch;\n  }\n  .euiFlexGrid--gutterXLarge > .euiFlexItem {\n    margin: 16px;\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--fourths > .euiFlexItem {\n    flex-basis: calc(25% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--fourths.euiFlexGrid--directionColumn {\n    column-count: 4;\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--fourths.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--thirds > .euiFlexItem {\n    flex-basis: calc(33.3% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--thirds.euiFlexGrid--directionColumn {\n    column-count: 3;\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--thirds.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--halves > .euiFlexItem {\n    flex-basis: calc(50% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--halves.euiFlexGrid--directionColumn {\n    column-count: 2;\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--halves.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--single > .euiFlexItem {\n    flex-basis: calc(100% - 32px);\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--single.euiFlexGrid--directionColumn {\n    column-count: 1;\n  }\n  .euiFlexGrid--gutterXLarge.euiFlexGrid--single.euiFlexGrid--directionColumn\n    > .euiFlexItem {\n    width: calc(100% - 32px);\n  }\n  @media only screen and (max-width: 574px) {\n    .euiFlexGrid.euiFlexGrid--responsive {\n      margin-left: 0 !important;\n      margin-right: 0 !important;\n      column-count: 1 !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiFlexGrid.euiFlexGrid--responsive {\n      margin-left: 0 !important;\n      margin-right: 0 !important;\n      column-count: 1 !important;\n    }\n  }\n  .euiFlexItem {\n    display: flex;\n    flex-direction: column;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiFlexItem {\n      min-width: 1px;\n    }\n  }\n  .euiFlexItem.euiFlexItem--flexGrowZero {\n    flex-grow: 0;\n    flex-basis: auto;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow1 {\n    flex-grow: 1;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow2 {\n    flex-grow: 2;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow3 {\n    flex-grow: 3;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow4 {\n    flex-grow: 4;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow5 {\n    flex-grow: 5;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow6 {\n    flex-grow: 6;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow7 {\n    flex-grow: 7;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow8 {\n    flex-grow: 8;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow9 {\n    flex-grow: 9;\n  }\n  .euiFlexItem.euiFlexItem--flexGrow10 {\n    flex-grow: 10;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiFlexGroup--responsive > .euiFlexItem,\n    .euiFlexGrid--responsive > .euiFlexItem {\n      width: 100% !important;\n      flex-basis: 100% !important;\n      margin-left: 0 !important;\n      margin-right: 0 !important;\n      margin-bottom: 16px !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiFlexGroup--responsive > .euiFlexItem,\n    .euiFlexGrid--responsive > .euiFlexItem {\n      width: 100% !important;\n      flex-basis: 100% !important;\n      margin-left: 0 !important;\n      margin-right: 0 !important;\n      margin-bottom: 16px !important;\n    }\n  }\n  .euiCheckbox {\n    position: relative;\n  }\n  .euiCheckbox .euiCheckbox__input {\n    position: absolute;\n    left: -10000px;\n    top: auto;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n  }\n  .euiCheckbox .euiCheckbox__input ~ .euiCheckbox__label {\n    display: inline-block;\n    padding-left: 24px;\n    line-height: 24px;\n    font-size: 14px;\n    position: relative;\n    z-index: 2;\n    cursor: pointer;\n  }\n  .euiCheckbox .euiCheckbox__input + .euiCheckbox__square {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 4px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    display: inline-block;\n    position: absolute;\n    left: 0;\n    top: 3px;\n  }\n  .euiCheckbox .euiCheckbox__input:checked + .euiCheckbox__square {\n    border-color: #006bb4;\n    background-color: #006bb4;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='8' viewBox='0 0 10 8'%3E%3Cpath d='M.375 2.625L3.375 5.625M3.375 5.625L8.625.375' fill='none' fill-rule='evenodd' stroke='rgb%28255, 255, 255%29' stroke-linecap='round' stroke-width='1.5' transform='translate(.5 1)'/%3E%3C/svg%3E\");\n  }\n  .euiCheckbox .euiCheckbox__input:indeterminate + .euiCheckbox__square {\n    border-color: #006bb4;\n    background-color: #006bb4;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Crect width='6' height='6' fill='rgb%28255, 255, 255%29' fill-rule='evenodd'/%3E%3C/svg%3E\");\n  }\n  .euiCheckbox .euiCheckbox__input[disabled] {\n    cursor: not-allowed !important;\n  }\n  .euiCheckbox .euiCheckbox__input[disabled] ~ .euiCheckbox__label {\n    color: #98a2b3;\n    cursor: not-allowed !important;\n  }\n  .euiCheckbox .euiCheckbox__input[disabled] + .euiCheckbox__square {\n    border-color: #d3dae6;\n    background-color: #d3dae6;\n    box-shadow: none;\n  }\n  .euiCheckbox .euiCheckbox__input:checked[disabled] + .euiCheckbox__square {\n    border-color: #d3dae6;\n    background-color: #d3dae6;\n    box-shadow: none;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='8' viewBox='0 0 10 8'%3E%3Cpath d='M.375 2.625L3.375 5.625M3.375 5.625L8.625.375' fill='none' fill-rule='evenodd' stroke='rgb%2894, 100, 111%29' stroke-linecap='round' stroke-width='1.5' transform='translate(.5 1)'/%3E%3C/svg%3E\");\n  }\n  .euiCheckbox\n    .euiCheckbox__input:indeterminate[disabled]\n    + .euiCheckbox__square {\n    border-color: #d3dae6;\n    background-color: #d3dae6;\n    box-shadow: none;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Ccircle cx='8' cy='11' r='3' fill='rgb%2894, 100, 111%29' fill-rule='evenodd' transform='translate(-5 -8)'/%3E%3C/svg%3E\");\n  }\n  .euiCheckbox .euiCheckbox__input:focus + .euiCheckbox__square,\n  .euiCheckbox\n    .euiCheckbox__input:active:not(:disabled)\n    + .euiCheckbox__square {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n    border-color: #006bb4;\n  }\n  .euiCheckbox.euiCheckbox--inList,\n  .euiCheckbox.euiCheckbox--noLabel {\n    min-height: 16px;\n    min-width: 16px;\n  }\n  .euiCheckbox.euiCheckbox--inList .euiCheckbox__square,\n  .euiCheckbox.euiCheckbox--noLabel .euiCheckbox__square {\n    top: 0;\n  }\n  .euiCheckbox.euiCheckbox--inList .euiCheckbox__input,\n  .euiCheckbox.euiCheckbox--noLabel .euiCheckbox__input {\n    width: 16px;\n    height: 16px;\n    position: absolute;\n    opacity: 0;\n    z-index: 1;\n    margin: 0;\n    left: 0;\n    cursor: pointer;\n  }\n  .euiCheckboxGroup__item + .euiCheckboxGroup__item {\n    margin-top: 4px;\n  }\n  .euiCheckboxGroup__item + .euiCheckboxGroup__item.euiCheckbox--compressed {\n    margin-top: 0;\n  }\n  .euiDescribedFormGroup {\n    max-width: 800px;\n  }\n  .euiDescribedFormGroup + * {\n    margin-top: 24px;\n  }\n  .euiDescribedFormGroup.euiDescribedFormGroup--fullWidth {\n    max-width: 100%;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__description {\n    padding-top: 8px;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fields {\n    min-width: 0;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fieldPadding--xxxsmall {\n    padding-top: 8px;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fieldPadding--xxsmall {\n    padding-top: 11px;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fieldPadding--xsmall {\n    padding-top: 14px;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fieldPadding--small {\n    padding-top: 20px;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fieldPadding--medium {\n    padding-top: 32px;\n  }\n  .euiDescribedFormGroup .euiDescribedFormGroup__fieldPadding--large {\n    padding-top: 44px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiDescribedFormGroup .euiDescribedFormGroup__fields {\n      padding-top: 0;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiDescribedFormGroup .euiDescribedFormGroup__fields {\n      padding-top: 0;\n    }\n  }\n  .euiFieldNumber {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    border: none;\n    border-radius: 0;\n    padding: 12px;\n  }\n  .euiFieldNumber--fullWidth {\n    max-width: 100%;\n  }\n  .euiFieldNumber--compressed {\n    height: 32px;\n  }\n  .euiFieldNumber--inGroup {\n    height: 100%;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFieldNumber {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiFieldNumber {\n      line-height: 1em;\n    }\n  }\n  .euiFieldNumber::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldNumber::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldNumber:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldNumber:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldNumber::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldNumber:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiFieldNumber:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldNumber:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldNumber:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldNumber:-webkit-autofill {\n    -webkit-text-fill-color: #343741;\n  }\n  .euiFieldNumber:-webkit-autofill ~ .euiFormControlLayoutIcons {\n    color: #343741;\n  }\n  .euiFieldNumber--compressed {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding: 8px;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFieldNumber--compressed {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFieldNumber--compressed:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiFieldNumber--compressed:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldNumber--compressed:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldNumber--compressed:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber--compressed:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber--compressed:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber--compressed:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber--compressed:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldNumber--compressed[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldNumber--inGroup {\n    box-shadow: none !important;\n    border-radius: 0;\n  }\n  .euiFieldNumber--withIcon {\n    padding-left: 40px;\n  }\n  .euiFieldNumber-isLoading {\n    padding-right: 40px;\n  }\n  .euiFieldNumber-isLoading.euiFieldNumber--compressed {\n    padding-right: 32px;\n  }\n  .euiFieldNumber--withIcon.euiFieldNumber--compressed {\n    padding-left: 32px;\n  }\n  .euiFieldText {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    border: none;\n    border-radius: 0;\n    padding: 12px;\n  }\n  .euiFieldText--fullWidth {\n    max-width: 100%;\n  }\n  .euiFieldText--compressed {\n    height: 32px;\n  }\n  .euiFieldText--inGroup {\n    height: 100%;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFieldText {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiFieldText {\n      line-height: 1em;\n    }\n  }\n  .euiFieldText::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldText::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldText:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldText:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldText::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiFieldText:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiFieldText:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldText:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldText:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldText:-webkit-autofill {\n    -webkit-text-fill-color: #343741;\n  }\n  .euiFieldText:-webkit-autofill ~ .euiFormControlLayoutIcons {\n    color: #343741;\n  }\n  .euiFieldText--compressed {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding: 8px;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFieldText--compressed {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFieldText--compressed:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiFieldText--compressed:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldText--compressed:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldText--compressed:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText--compressed:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText--compressed:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText--compressed:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText--compressed:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFieldText--compressed[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFieldText--inGroup {\n    box-shadow: none !important;\n    border-radius: 0;\n  }\n  .euiFieldText--withIcon {\n    padding-left: 40px;\n  }\n  .euiFieldText-isLoading {\n    padding-right: 40px;\n  }\n  .euiFieldText-isLoading.euiFieldText--compressed {\n    padding-right: 32px;\n  }\n  .euiFieldText.euiFieldText-isInvalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiFieldText--withIcon.euiFieldText--compressed {\n    padding-left: 32px;\n  }\n  .euiFilePicker {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    position: relative;\n  }\n  .euiFilePicker--fullWidth {\n    max-width: 100%;\n  }\n  .euiFilePicker--compressed {\n    height: 32px;\n  }\n  .euiFilePicker--inGroup {\n    height: 100%;\n  }\n  .euiFilePicker.euiFilePicker--large {\n    border-radius: 0;\n    overflow: hidden;\n    height: auto;\n  }\n  .euiFilePicker.euiFilePicker--large.euiFilePicker--compressed {\n    border-radius: 2px;\n  }\n  .euiFilePicker__input {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    opacity: 0;\n    overflow: hidden;\n  }\n  .euiFilePicker__input:hover {\n    cursor: pointer;\n  }\n  .euiFilePicker__input:hover:disabled {\n    cursor: not-allowed;\n  }\n  .euiFilePicker__input:disabled {\n    opacity: 0;\n  }\n  .euiFilePicker__input:disabled ~ .euiFilePicker__prompt {\n    color: #98a2b3;\n  }\n  .euiFilePicker__icon {\n    position: absolute;\n    left: 12px;\n    top: 12px;\n    transition: transform 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiFilePicker--compressed .euiFilePicker__icon {\n    top: 8px;\n    left: 8px;\n  }\n  .euiFilePicker--large .euiFilePicker__icon {\n    position: static;\n    margin-bottom: 16px;\n  }\n  .euiFilePicker__prompt {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding-left: 40px;\n    height: 40px;\n    padding-top: 12px;\n    padding-right: 12px;\n    padding-bottom: 12px;\n    pointer-events: none;\n    border-radius: 0;\n    transition:\n      box-shadow 150ms ease-in,\n      background-color 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in 150ms;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFilePicker__prompt {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFilePicker--compressed .euiFilePicker__prompt {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding: 8px;\n    border-radius: 2px;\n    padding-left: 32px;\n    height: 32px;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFilePicker--compressed .euiFilePicker__prompt {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFilePicker--large .euiFilePicker__prompt {\n    height: 128px;\n    padding: 0 24px;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n  }\n  .euiFilePicker--large.euiFilePicker--compressed .euiFilePicker__prompt {\n    height: 104px;\n  }\n  .euiFilePicker-isInvalid .euiFilePicker__prompt {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiFilePicker__promptText {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    line-height: 16px;\n  }\n  .euiFilePicker:not(.euiFilePicker--large):not(.euiFilePicker-hasFiles)\n    .euiFilePicker__promptText {\n    color: #98a2b3;\n  }\n  .euiFilePicker__clearButton,\n  .euiFilePicker__loadingSpinner {\n    position: absolute;\n    right: 12px;\n    top: 12px;\n  }\n  .euiFilePicker--compressed .euiFilePicker__clearButton,\n  .euiFilePicker--compressed .euiFilePicker__loadingSpinner {\n    top: 8px;\n  }\n  .euiFilePicker__clearButton {\n    pointer-events: auto;\n  }\n  .euiFilePicker:not(.euiFilePicker--large) .euiFilePicker__clearButton {\n    width: 16px;\n    height: 16px;\n    pointer-events: all;\n    background-color: #98a2b3;\n    border-radius: 16px;\n    line-height: 0;\n  }\n  .euiFilePicker:not(.euiFilePicker--large) .euiFilePicker__clearButton:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiFilePicker:not(.euiFilePicker--large)\n    .euiFilePicker__clearButton\n    .euiFilePicker__clearIcon {\n    width: 8px;\n    height: 8px;\n    fill: #fff;\n    stroke: #fff;\n    stroke-width: 2px;\n  }\n  .euiFilePicker--large .euiFilePicker__clearButton {\n    position: relative;\n    top: 0;\n    right: 0;\n  }\n  .euiFilePicker__showDrop .euiFilePicker__prompt,\n  .euiFilePicker__input:focus + .euiFilePicker__prompt {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFilePicker--compressed .euiFilePicker__showDrop .euiFilePicker__prompt,\n  .euiFilePicker--compressed\n    .euiFilePicker__input:focus\n    + .euiFilePicker__prompt {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFilePicker__input:disabled + .euiFilePicker__prompt {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFilePicker__input:disabled\n    + .euiFilePicker__prompt::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFilePicker__input:disabled + .euiFilePicker__prompt::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFilePicker__input:disabled\n    + .euiFilePicker__prompt:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFilePicker__input:disabled + .euiFilePicker__prompt:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFilePicker__input:disabled + .euiFilePicker__prompt::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFilePicker:not(.euiFilePicker--large).euiFilePicker-isLoading\n    .euiFilePicker__prompt,\n  .euiFilePicker:not(.euiFilePicker--large).euiFilePicker-hasFiles\n    .euiFilePicker__prompt {\n    padding-right: 40px;\n  }\n  .euiFilePicker-hasFiles .euiFilePicker__promptText {\n    color: #343741;\n  }\n  .euiFilePicker--large\n    .euiFilePicker__input:hover:not(:disabled)\n    + .euiFilePicker__prompt\n    .euiFilePicker__promptText,\n  .euiFilePicker--large\n    .euiFilePicker__input:focus\n    + .euiFilePicker__prompt\n    .euiFilePicker__promptText {\n    text-decoration: underline;\n  }\n  .euiFilePicker--large\n    .euiFilePicker__input:hover:not(:disabled)\n    + .euiFilePicker__prompt\n    .euiFilePicker__icon,\n  .euiFilePicker--large\n    .euiFilePicker__input:focus\n    + .euiFilePicker__prompt\n    .euiFilePicker__icon {\n    transform: scale(1.1);\n  }\n  .euiFilePicker--large.euiFilePicker__showDrop\n    .euiFilePicker__prompt\n    .euiFilePicker__promptText {\n    text-decoration: underline;\n  }\n  .euiFilePicker--large.euiFilePicker__showDrop\n    .euiFilePicker__prompt\n    .euiFilePicker__icon {\n    transform: scale(1.1);\n  }\n  .euiFilePicker--large.euiFilePicker-hasFiles .euiFilePicker__promptText {\n    font-weight: 700;\n  }\n  .euiForm__error {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    list-style: disc;\n  }\n  .euiForm__errors {\n    margin-bottom: 16px;\n  }\n  .euiFormControlLayout {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n  }\n  .euiFormControlLayout--fullWidth {\n    max-width: 100%;\n  }\n  .euiFormControlLayout--compressed {\n    height: 32px;\n  }\n  .euiFormControlLayout--inGroup {\n    height: 100%;\n  }\n  .euiFormControlLayout__childrenWrapper {\n    position: relative;\n  }\n  .euiFormControlLayout--group {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    display: flex;\n    align-items: stretch;\n    padding: 1px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFormControlLayout--group {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFormControlLayout--group > *,\n  .euiFormControlLayout--group .euiPopover__anchor,\n  .euiFormControlLayout--group .euiButtonEmpty,\n  .euiFormControlLayout--group .euiText,\n  .euiFormControlLayout--group .euiFormLabel,\n  .euiFormControlLayout--group .euiButtonIcon {\n    height: 100%;\n  }\n  .euiFormControlLayout--group .euiFormControlLayout__childrenWrapper {\n    flex-grow: 1;\n    overflow: hidden;\n  }\n  .euiFormControlLayout--group .euiFormControlLayout__prepend,\n  .euiFormControlLayout--group .euiFormControlLayout__append {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    flex-shrink: 0;\n    height: 100%;\n    border-radius: 0;\n  }\n  .euiFormControlLayout--group .euiFormControlLayout__prepend.euiIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__prepend .euiIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__append.euiIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__append .euiIcon {\n    padding: 0 8px;\n    width: 32px;\n    border-radius: 0;\n    background-color: #e9edf3;\n  }\n  .euiFormControlLayout--group .euiFormControlLayout__prepend.euiButtonIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__prepend.euiButtonEmpty,\n  .euiFormControlLayout--group .euiFormControlLayout__prepend .euiButtonIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__prepend .euiButtonEmpty,\n  .euiFormControlLayout--group .euiFormControlLayout__append.euiButtonIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__append.euiButtonEmpty,\n  .euiFormControlLayout--group .euiFormControlLayout__append .euiButtonIcon,\n  .euiFormControlLayout--group .euiFormControlLayout__append .euiButtonEmpty {\n    transform: none !important;\n  }\n  .euiFormControlLayout--group\n    .euiFormControlLayout__prepend.euiButtonIcon\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__prepend.euiButtonEmpty\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__prepend\n    .euiButtonIcon\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__prepend\n    .euiButtonEmpty\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__append.euiButtonIcon\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__append.euiButtonEmpty\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__append\n    .euiButtonIcon\n    .euiIcon,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__append\n    .euiButtonEmpty\n    .euiIcon {\n    background: none !important;\n    padding: 0;\n    width: 16px;\n  }\n  .euiFormControlLayout--group .euiButtonIcon {\n    padding: 0 8px;\n    width: 32px;\n    border-radius: 0;\n  }\n  .euiFormControlLayout--group .euiButtonIcon:not(:focus) {\n    background-color: #e9edf3;\n  }\n  .euiFormControlLayout--group .euiButtonIcon:focus-visible {\n    outline: 2px solid rgba(0, 107, 180, 0.3);\n    outline-offset: -2px;\n  }\n  .euiFormControlLayout--group > .euiFormControlLayout__prepend,\n  .euiFormControlLayout--group > .euiFormControlLayout__append {\n    max-width: 50%;\n  }\n  .euiFormControlLayout--group .euiFormLabel,\n  .euiFormControlLayout--group .euiText {\n    background-color: #e9edf3;\n    padding: 12px;\n    line-height: 16px !important;\n    cursor: default !important;\n  }\n  .euiFormControlLayout--group\n    .euiFormLabel\n    + *:not(.euiFormControlLayout__childrenWrapper):not(input),\n  .euiFormControlLayout--group\n    .euiText\n    + *:not(.euiFormControlLayout__childrenWrapper):not(input) {\n    margin-left: -12px;\n  }\n  .euiFormControlLayout--group\n    > *:not(.euiFormControlLayout__childrenWrapper)\n    + .euiFormLabel,\n  .euiFormControlLayout--group\n    > *:not(.euiFormControlLayout__childrenWrapper)\n    + .euiText {\n    margin-left: -12px;\n  }\n  .euiFormControlLayout--group .euiButtonEmpty {\n    border-right: 1px solid #e4e8ee;\n  }\n  .euiFormControlLayout--group\n    .euiFormControlLayout__childrenWrapper\n    ~ .euiButtonEmpty,\n  .euiFormControlLayout--group\n    .euiFormControlLayout__childrenWrapper\n    ~ *\n    .euiButtonEmpty {\n    border-right: none;\n    border-left: 1px solid #e4e8ee;\n  }\n  .euiFormControlLayout--group.euiFormControlLayout--compressed {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    border-radius: 2px;\n    overflow: hidden;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFormControlLayout--group.euiFormControlLayout--compressed {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFormControlLayout--group.euiFormControlLayout--compressed .euiFormLabel,\n  .euiFormControlLayout--group.euiFormControlLayout--compressed .euiText {\n    padding: 8px;\n  }\n  .euiFormControlLayout--group.euiFormControlLayout--compressed\n    .euiFormLabel\n    + *:not(.euiFormControlLayout__childrenWrapper),\n  .euiFormControlLayout--group.euiFormControlLayout--compressed\n    .euiText\n    + *:not(.euiFormControlLayout__childrenWrapper) {\n    margin-left: -8px;\n  }\n  .euiFormControlLayout--group.euiFormControlLayout--compressed\n    > *:not(.euiFormControlLayout__childrenWrapper)\n    + .euiFormLabel,\n  .euiFormControlLayout--group.euiFormControlLayout--compressed\n    > *:not(.euiFormControlLayout__childrenWrapper)\n    + .euiText {\n    margin-left: -8px;\n  }\n  .euiFormControlLayout--group.euiFormControlLayout--readOnly {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFormControlLayout--group.euiFormControlLayout--readOnly input {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiFormControlLayoutDelimited {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    display: flex;\n    align-items: stretch;\n    padding: 1px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFormControlLayoutDelimited {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFormControlLayoutDelimited .euiFormControlLayoutDelimited__delimeter {\n    background-color: #fbfcfd;\n  }\n  .euiFormControlLayoutDelimited > .euiFormControlLayout__childrenWrapper {\n    display: flex;\n    align-items: center;\n    width: 100%;\n  }\n  .euiFormControlLayoutDelimited[class*='--compressed'] {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiFormControlLayoutDelimited[class*='--compressed'] {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiFormControlLayoutDelimited[class*='--compressed']\n    .euiFormControlLayoutDelimited__input {\n    height: 100%;\n    padding-top: 0;\n    padding-bottom: 0;\n    padding-left: 8px;\n    padding-right: 8px;\n  }\n  .euiFormControlLayoutDelimited[class*='--compressed']\n    .euiFormControlLayoutIcons {\n    padding-left: 8px;\n    padding-right: 8px;\n  }\n  .euiFormControlLayoutDelimited[class*='--fullWidth']\n    .euiFormControlLayout__childrenWrapper,\n  .euiFormControlLayoutDelimited[class*='--fullWidth'] input {\n    width: 100%;\n    max-width: none;\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled'] {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled']::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled']::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled']:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled']:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled']::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiFormControlLayoutDelimited[class*='-isDisabled']\n    .euiFormControlLayoutDelimited__delimeter {\n    background-color: #eef2f7;\n  }\n  .euiFormControlLayoutDelimited[class*='--readOnly'] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiFormControlLayoutDelimited[class*='--readOnly'] input,\n  .euiFormControlLayoutDelimited[class*='--readOnly']\n    .euiFormControlLayoutDelimited__delimeter {\n    background-color: rgba(211, 218, 230, 0.05);\n  }\n  .euiFormControlLayoutDelimited .euiFormControlLayoutIcons {\n    position: static;\n    padding-left: 12px;\n    padding-right: 12px;\n    flex-shrink: 0;\n  }\n  .euiFormControlLayoutDelimited\n    .euiFormControlLayoutIcons:not(.euiFormControlLayoutIcons--right) {\n    order: -1;\n  }\n  .euiFormControlLayoutDelimited__input {\n    box-shadow: none !important;\n    border-radius: 0 !important;\n    text-align: center;\n    height: 100%;\n    min-width: 0;\n  }\n  .euiFormControlLayoutDelimited[class*='--compressed']\n    .euiFormControlLayoutDelimited__input {\n    max-width: none;\n  }\n  .euiFormControlLayoutDelimited__delimeter {\n    line-height: 1 !important;\n    flex: 0 0 auto;\n    padding-left: 6px;\n    padding-right: 6px;\n  }\n  .euiFormControlLayoutIcons {\n    pointer-events: none;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 12px;\n    display: flex;\n    align-items: center;\n  }\n  .euiFormControlLayoutIcons > * + * {\n    margin-left: 6px;\n  }\n  .euiFormControlLayout--compressed .euiFormControlLayoutIcons {\n    left: 8px;\n  }\n  .euiFormControlLayoutIcons--right {\n    left: auto;\n    right: 12px;\n  }\n  .euiFormControlLayout--compressed .euiFormControlLayoutIcons--right {\n    left: auto;\n    right: 8px;\n  }\n  *:disabled + .euiFormControlLayoutIcons {\n    cursor: not-allowed;\n    color: #98a2b3;\n  }\n  .euiFormControlLayoutClearButton {\n    width: 16px;\n    height: 16px;\n    pointer-events: all;\n    background-color: #98a2b3;\n    border-radius: 16px;\n    line-height: 0;\n  }\n  .euiFormControlLayoutClearButton:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiFormControlLayoutClearButton .euiFormControlLayoutClearButton__icon {\n    width: 8px;\n    height: 8px;\n    fill: #fff;\n    stroke: #fff;\n    stroke-width: 2px;\n  }\n  .euiFormControlLayoutClearButton--small {\n    width: 12px;\n    height: 12px;\n    pointer-events: all;\n    background-color: #98a2b3;\n    border-radius: 12px;\n    line-height: 0;\n  }\n  .euiFormControlLayoutClearButton--small:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiFormControlLayoutClearButton--small\n    .euiFormControlLayoutClearButton__icon {\n    width: 6px;\n    height: 6px;\n    fill: #fff;\n    stroke: #fff;\n    stroke-width: 4px;\n  }\n  .euiFormControlLayoutCustomIcon {\n    pointer-events: none;\n    font-size: 0;\n  }\n  .euiFormControlLayoutCustomIcon--clickable {\n    width: 16px;\n    height: 16px;\n    pointer-events: all;\n  }\n  .euiFormControlLayoutCustomIcon--clickable\n    .euiFormControlLayoutCustomIcon__icon {\n    vertical-align: baseline;\n    transform: none;\n  }\n  .euiFormControlLayoutCustomIcon--clickable:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiFormControlLayoutCustomIcon--clickable:disabled {\n    cursor: not-allowed;\n    color: #98a2b3;\n  }\n  .euiFormErrorText {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    padding-top: 4px;\n    color: #bd271e;\n  }\n  .euiFormLegend {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    color: #1a1c21;\n    font-weight: 600;\n  }\n  .euiFormLegend:not(.euiFormLegend-isHidden) {\n    margin-bottom: 8px;\n  }\n  .euiFormLegend:not(.euiFormLegend-isHidden).euiFormLegend--compressed {\n    margin-bottom: 4px;\n  }\n  .euiFormHelpText {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    padding-top: 4px;\n    color: #69707d;\n  }\n  .euiFormLabel {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    color: #1a1c21;\n    font-weight: 600;\n    display: inline-block;\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiFormLabel.euiFormLabel-isInvalid {\n    color: #bd271e;\n  }\n  .euiFormLabel.euiFormLabel-isFocused {\n    color: #006bb4;\n  }\n  .euiFormLabel[for] {\n    cursor: pointer;\n  }\n  .euiRadio {\n    position: relative;\n  }\n  .euiRadio .euiRadio__input {\n    position: absolute;\n    left: -10000px;\n    top: auto;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n  }\n  .euiRadio .euiRadio__input ~ .euiRadio__label {\n    display: inline-block;\n    padding-left: 24px;\n    line-height: 24px;\n    font-size: 14px;\n    position: relative;\n    z-index: 2;\n    cursor: pointer;\n  }\n  .euiRadio .euiRadio__input + .euiRadio__circle {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    display: inline-block;\n    position: absolute;\n    left: 0;\n    top: 3px;\n  }\n  .euiRadio .euiRadio__input:checked + .euiRadio__circle {\n    border-color: #006bb4;\n    background-color: #006bb4;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Ccircle cx='8' cy='11' r='3' fill='rgb%28255, 255, 255%29' fill-rule='evenodd' transform='translate(-5 -8)'/%3E%3C/svg%3E\");\n  }\n  .euiRadio .euiRadio__input[disabled] {\n    cursor: not-allowed !important;\n  }\n  .euiRadio .euiRadio__input[disabled] ~ .euiRadio__label {\n    color: #98a2b3;\n    cursor: not-allowed !important;\n  }\n  .euiRadio .euiRadio__input[disabled] + .euiRadio__circle {\n    border-color: #d3dae6;\n    background-color: #d3dae6;\n    box-shadow: none;\n  }\n  .euiRadio .euiRadio__input:checked[disabled] + .euiRadio__circle {\n    border-color: #d3dae6;\n    background-color: #d3dae6;\n    box-shadow: none;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Ccircle cx='8' cy='11' r='3' fill='rgb%2894, 100, 111%29' fill-rule='evenodd' transform='translate(-5 -8)'/%3E%3C/svg%3E\");\n  }\n  .euiRadio .euiRadio__input:focus + .euiRadio__circle,\n  .euiRadio .euiRadio__input:active:not(:disabled) + .euiRadio__circle {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n    border-color: #006bb4;\n  }\n  .euiRadio.euiRadio--inList,\n  .euiRadio.euiRadio--noLabel {\n    min-height: 16px;\n    min-width: 16px;\n  }\n  .euiRadio.euiRadio--inList .euiRadio__circle,\n  .euiRadio.euiRadio--noLabel .euiRadio__circle {\n    top: 0;\n  }\n  .euiRadio.euiRadio--inList .euiRadio__input,\n  .euiRadio.euiRadio--noLabel .euiRadio__input {\n    width: 16px;\n    height: 16px;\n    position: absolute;\n    opacity: 0;\n    z-index: 1;\n    margin: 0;\n    left: 0;\n    cursor: pointer;\n  }\n  .euiRadioGroup__item + .euiRadioGroup__item {\n    margin-top: 4px;\n  }\n  .euiRadioGroup__item + .euiRadioGroup__item.euiRadio--compressed {\n    margin-top: 0;\n  }\n  .euiRange__horizontalSpacer {\n    width: 16px;\n  }\n  .euiRange__slimHorizontalSpacer {\n    width: 8px;\n  }\n  .euiRangeDraggable {\n    height: 20px;\n    position: absolute;\n    top: 10px;\n    pointer-events: none;\n    z-index: 2;\n  }\n  .euiRangeDraggable.euiRangeDraggable--compressed {\n    height: 16px;\n    top: 8px;\n  }\n  .euiRangeDraggable.euiRangeDraggable--hasTicks {\n    top: 0;\n  }\n  .euiRangeDraggable .euiRangeDraggle__inner {\n    position: absolute;\n    left: 16px;\n    right: 16px;\n    top: 0;\n    bottom: 0;\n  }\n  .euiRangeDraggable:not(.euiRangeDraggable--disabled) .euiRangeDraggle__inner {\n    cursor: grab;\n    pointer-events: all;\n  }\n  .euiRangeDraggable:not(.euiRangeDraggable--disabled)\n    .euiRangeDraggle__inner:active {\n    cursor: grabbing;\n  }\n  .euiRangeHighlight {\n    position: absolute;\n    left: 0;\n    width: 100%;\n    top: calc(50% - 2px);\n    overflow: hidden;\n  }\n  .euiRangeHighlight__progress {\n    height: 4px;\n    border-radius: 4px;\n    background-color: #69707d;\n  }\n  .euiRangeHighlight__progress--hasFocus {\n    background-color: #006bb4;\n  }\n  .euiRangeHighlight--hasTicks {\n    top: 8px;\n  }\n  .euiRangeHighlight--hasTicks.euiRangeHighlight--compressed {\n    top: 6px;\n  }\n  .euiRangeInput {\n    width: auto;\n    min-width: 64px;\n  }\n  .euiRange__popover .euiRangeInput {\n    margin: 0 !important;\n    width: 100%;\n  }\n  .euiRangeLabel--min,\n  .euiRangeLabel--max {\n    font-size: 12px;\n  }\n  .euiRangeLabel--min {\n    margin-right: 8px;\n  }\n  .euiRangeLabel--max {\n    margin-left: 8px;\n  }\n  .euiRangeLabel--isDisabled {\n    opacity: 0.25;\n  }\n  .euiRangeLevels {\n    display: flex;\n    justify-content: stretch;\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 22px;\n  }\n  .euiRangeLevels--hasTicks {\n    top: 12px;\n  }\n  .euiRangeLevels--compressed {\n    top: 18px;\n  }\n  .euiRangeLevels--compressed.euiRangeLevels--hasTicks {\n    top: 10px;\n  }\n  .euiRangeLevel {\n    display: block;\n    height: 6px;\n    border-radius: 6px;\n    margin: 2px;\n  }\n  .euiRangeLevel--primary {\n    background-color: rgba(0, 107, 180, 0.3);\n  }\n  .euiRangeLevel--success {\n    background-color: rgba(1, 125, 115, 0.3);\n  }\n  .euiRangeLevel--warning {\n    background-color: rgba(245, 167, 0, 0.3);\n  }\n  .euiRangeLevel--danger {\n    background-color: rgba(189, 39, 30, 0.3);\n  }\n  .euiRangeSlider {\n    height: 40px;\n    appearance: none;\n    background: rgba(0, 0, 0, 0);\n    width: 100%;\n    position: relative;\n    cursor: pointer;\n    z-index: 1;\n  }\n  .euiRangeSlider:disabled {\n    cursor: not-allowed;\n  }\n  .euiRangeSlider:disabled::-webkit-slider-thumb {\n    cursor: not-allowed;\n    border-color: #69707d;\n    background-color: #69707d;\n    box-shadow: none;\n  }\n  .euiRangeSlider:disabled::-moz-range-thumb {\n    cursor: not-allowed;\n    border-color: #69707d;\n    background-color: #69707d;\n    box-shadow: none;\n  }\n  .euiRangeSlider:disabled::-ms-thumb {\n    cursor: not-allowed;\n    border-color: #69707d;\n    background-color: #69707d;\n    box-shadow: none;\n  }\n  .euiRangeSlider:disabled ~ .euiRangeThumb {\n    cursor: not-allowed;\n    border-color: #69707d;\n    background-color: #69707d;\n    box-shadow: none;\n  }\n  .euiRangeSlider::-webkit-slider-thumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n  }\n  .euiRangeSlider::-moz-range-thumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n  }\n  .euiRangeSlider::-ms-thumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n  }\n  .euiRangeSlider::-webkit-slider-runnable-track {\n    height: 2px;\n    transition: all 250ms ease-in;\n    width: 100%;\n    background: #69707d;\n    border: 0 solid #69707d;\n    border-radius: 4px;\n  }\n  .euiRangeSlider::-moz-range-track {\n    height: 2px;\n    transition: all 250ms ease-in;\n    width: 100%;\n    background: #69707d;\n    border: 0 solid #69707d;\n    border-radius: 4px;\n  }\n  .euiRangeSlider::-ms-fill-lower {\n    height: 2px;\n    transition: all 250ms ease-in;\n    width: 100%;\n    background: #69707d;\n    border: 0 solid #69707d;\n    border-radius: 4px;\n  }\n  .euiRangeSlider::-ms-fill-upper {\n    height: 2px;\n    transition: all 250ms ease-in;\n    width: 100%;\n    background: #69707d;\n    border: 0 solid #69707d;\n    border-radius: 4px;\n  }\n  .euiRangeSlider:focus {\n    outline: none;\n  }\n  .euiRangeSlider:focus-visible::-webkit-slider-thumb,\n  .euiRangeSlider--hasFocus::-webkit-slider-thumb {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n  }\n  .euiRangeSlider:focus-visible::-moz-range-thumb,\n  .euiRangeSlider--hasFocus::-moz-range-thumb {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n  }\n  .euiRangeSlider:focus-visible::-ms-thumb,\n  .euiRangeSlider--hasFocus::-ms-thumb {\n    box-shadow: 0 0 0 3px rgba(0, 107, 180, 0.3);\n  }\n  .euiRangeSlider:focus-visible ~ .euiRangeThumb,\n  .euiRangeSlider--hasFocus ~ .euiRangeThumb {\n    border-color: #69707d;\n  }\n  .euiRangeSlider:focus-visible::-webkit-slider-runnable-track,\n  .euiRangeSlider--hasFocus::-webkit-slider-runnable-track {\n    background-color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiRangeSlider:focus-visible::-moz-range-track,\n  .euiRangeSlider--hasFocus::-moz-range-track {\n    background-color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiRangeSlider:focus-visible::-ms-fill-lower,\n  .euiRangeSlider--hasFocus::-ms-fill-lower {\n    background-color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiRangeSlider:focus-visible::-ms-fill-upper,\n  .euiRangeSlider--hasFocus::-ms-fill-upper {\n    background-color: #006bb4;\n    border-color: #006bb4;\n  }\n  .euiRangeSlider:focus-visible\n    ~ .euiRangeHighlight\n    .euiRangeHighlight__progress,\n  .euiRangeSlider--hasFocus ~ .euiRangeHighlight .euiRangeHighlight__progress {\n    background-color: #006bb4;\n  }\n  .euiRangeSlider:focus-visible ~ .euiRangeTooltip .euiRangeTooltip__value,\n  .euiRangeSlider--hasFocus ~ .euiRangeTooltip .euiRangeTooltip__value {\n    box-shadow:\n      0 6px 12px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -1px rgba(152, 162, 179, 0.2),\n      0 2px 2px 0 rgba(152, 162, 179, 0.2);\n  }\n  .euiRangeSlider:focus-visible\n    ~ .euiRangeTooltip\n    .euiRangeTooltip__value.euiRangeTooltip__value--right,\n  .euiRangeSlider:focus-visible\n    ~ .euiRangeTooltip\n    .euiRangeTooltip__value.euiRangeTooltip__value--left,\n  .euiRangeSlider--hasFocus\n    ~ .euiRangeTooltip\n    .euiRangeTooltip__value.euiRangeTooltip__value--right,\n  .euiRangeSlider--hasFocus\n    ~ .euiRangeTooltip\n    .euiRangeTooltip__value.euiRangeTooltip__value--left {\n    transform: translateX(0) translateY(-50%) scale(1.1);\n  }\n  .euiRangeSlider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    margin-top: -7px;\n  }\n  .euiRangeSlider::-ms-thumb {\n    margin-top: 0;\n  }\n  .euiRangeSlider::-moz-focus-outer {\n    border: none;\n  }\n  .euiRangeSlider::-ms-track {\n    height: 2px;\n    transition: all 250ms ease-in;\n    width: 100%;\n    background: rgba(0, 0, 0, 0);\n    border-color: rgba(0, 0, 0, 0);\n    border-width: 8px 0;\n    color: rgba(0, 0, 0, 0);\n  }\n  .euiRangeSlider--hasTicks {\n    height: 20px;\n  }\n  .euiRangeSlider--compressed {\n    height: 32px;\n  }\n  .euiRangeSlider--compressed.euiRangeSlider--hasTicks {\n    height: 16px;\n  }\n  .euiRangeSlider--hasRange::-webkit-slider-runnable-track {\n    background-color: rgba(105, 112, 125, 0.4);\n    border-color: rgba(105, 112, 125, 0.4);\n  }\n  .euiRangeSlider--hasRange::-moz-range-track {\n    background-color: rgba(105, 112, 125, 0.4);\n    border-color: rgba(105, 112, 125, 0.4);\n  }\n  .euiRangeSlider--hasRange::-ms-fill-lower {\n    background-color: rgba(105, 112, 125, 0.4);\n    border-color: rgba(105, 112, 125, 0.4);\n  }\n  .euiRangeSlider--hasRange::-ms-fill-upper {\n    background-color: rgba(105, 112, 125, 0.4);\n    border-color: rgba(105, 112, 125, 0.4);\n  }\n  .euiRangeThumb {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    padding: 7px;\n    border: 1px solid #c9cbcd;\n    background: #fff no-repeat center;\n    border-radius: 14px;\n    transition:\n      background-color 150ms ease-in,\n      border-color 150ms ease-in;\n    cursor: pointer;\n    border-color: #69707d;\n    padding: 0;\n    height: 16px;\n    width: 16px;\n    content: '';\n    position: absolute;\n    left: 0;\n    top: 50%;\n    margin-top: -8px;\n    pointer-events: none;\n    z-index: 1;\n  }\n  .euiRangeThumb:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n    border-color: #006bb4;\n  }\n  .euiRangeThumb--hasTicks {\n    top: 25%;\n  }\n  .euiRangeTicks {\n    position: absolute;\n    left: 0;\n    right: 0;\n    top: 8px;\n    display: flex;\n  }\n  .euiRangeTicks--isCustom {\n    left: 2px;\n    right: 2px;\n  }\n  .euiRangeTick {\n    overflow-x: hidden;\n    text-overflow: ellipsis;\n    font-size: 12px;\n    position: absolute;\n    transform: translateX(-50%);\n    padding-top: 16px;\n  }\n  .euiRangeTick:not(.euiRangeTick--hasTickMark)::before {\n    width: 4px;\n    height: 4px;\n    background-color: #69707d;\n    border-radius: 100%;\n    position: absolute;\n    top: 0;\n    content: '';\n    left: calc(50% - 2px);\n  }\n  .euiRangeTick .euiRangeTick__pseudo {\n    width: 4px;\n    height: 4px;\n    background-color: #69707d;\n    border-radius: 100%;\n    position: absolute;\n    top: 0;\n  }\n  .euiRangeTick--isCustom {\n    overflow-x: visible;\n  }\n  .euiRangeTick--isMin,\n  .euiRangeTick--isMax {\n    transform: translateX(0);\n  }\n  .euiRangeTick--isMin .euiRangeTick__pseudo {\n    left: 0;\n  }\n  .euiRangeTick--isMax .euiRangeTick__pseudo {\n    right: 0;\n  }\n  .euiRangeTick:enabled:hover,\n  .euiRangeTick:focus,\n  .euiRangeTick--selected {\n    color: #006bb4;\n  }\n  .euiRangeTick--selected {\n    font-weight: 500;\n  }\n  .euiRangeTick:disabled {\n    cursor: not-allowed;\n  }\n  .euiRangeTicks--compressed {\n    top: 6px;\n  }\n  .euiRangeTicks--compressed .euiRangeTick {\n    padding-top: 14px;\n  }\n  .euiRangeTick__label {\n    pointer-events: none;\n  }\n  .euiRangeTooltip {\n    display: block;\n    position: absolute;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    width: calc(100% - 16px);\n    margin-left: 8px;\n    pointer-events: none;\n    z-index: 2;\n  }\n  .euiRangeTooltip__value {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    border: 1px solid #404040;\n    position: absolute;\n    border-radius: 4px;\n    padding: 2px 8px;\n    background-color: #404040;\n    color: #fff;\n    max-width: 256px;\n    top: 50%;\n    transition:\n      box-shadow 250ms cubic-bezier(0.694, 0.0482, 0.335, 1),\n      transform 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiRangeTooltip__value::after,\n  .euiRangeTooltip__value::before {\n    content: '';\n    position: absolute;\n    bottom: -6px;\n    left: 50%;\n    transform-origin: center;\n    background-color: #404040;\n    width: 12px;\n    height: 12px;\n    border-radius: 2px;\n  }\n  .euiRangeTooltip__value::before {\n    background-color: #404040;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--right {\n    margin-left: 24px;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--right:before,\n  .euiRangeTooltip__value.euiRangeTooltip__value--right:after {\n    left: -5px;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--right::before {\n    margin-left: -1px;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--left {\n    margin-right: 24px;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--left:before,\n  .euiRangeTooltip__value.euiRangeTooltip__value--left:after {\n    left: auto;\n    right: -5px;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--left::before {\n    margin-right: -1px;\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--right,\n  .euiRangeTooltip__value.euiRangeTooltip__value--left {\n    transform: translateX(0) translateY(-50%);\n  }\n  .euiRangeTooltip__value.euiRangeTooltip__value--right:before,\n  .euiRangeTooltip__value.euiRangeTooltip__value--right:after,\n  .euiRangeTooltip__value.euiRangeTooltip__value--left:before,\n  .euiRangeTooltip__value.euiRangeTooltip__value--left:after {\n    bottom: 50%;\n    transform: translateY(50%) rotateZ(45deg);\n  }\n  .euiRangeTooltip__value--hasTicks {\n    top: 10px;\n  }\n  .euiRangeTooltip--compressed .euiRangeTooltip__value--hasTicks {\n    top: 8px;\n  }\n  .euiRangeTrack {\n    height: 100%;\n    flex-grow: 1;\n    position: relative;\n    align-self: flex-start;\n  }\n  .euiRangeTrack--hasTicks {\n    margin-left: 1em;\n    margin-right: 1em;\n  }\n  .euiRangeTrack--disabled {\n    opacity: 0.25;\n  }\n  .euiRangeWrapper {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    display: flex;\n    align-items: center;\n  }\n  .euiRangeWrapper--fullWidth {\n    max-width: 100%;\n  }\n  .euiRangeWrapper--compressed {\n    height: 32px;\n  }\n  .euiRangeWrapper--inGroup {\n    height: 100%;\n  }\n  .euiRangeWrapper > .euiFormControlLayout {\n    width: auto;\n  }\n  .euiRangeWrapper > .euiFormControlLayout.euiFormControlLayout--group {\n    flex-shrink: 0;\n  }\n  .euiDualRange__slider::-webkit-slider-thumb {\n    visibility: hidden;\n  }\n  .euiDualRange__slider::-moz-range-thumb {\n    visibility: hidden;\n  }\n  .euiDualRange__slider::-ms-thumb {\n    visibility: hidden;\n  }\n  .euiSelect {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    border: none;\n    border-radius: 0;\n    padding: 12px;\n    padding-right: 40px;\n    appearance: none;\n    line-height: 40px;\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n  .euiSelect--fullWidth {\n    max-width: 100%;\n  }\n  .euiSelect--compressed {\n    height: 32px;\n  }\n  .euiSelect--inGroup {\n    height: 100%;\n  }\n  @supports (-moz-appearance: none) {\n    .euiSelect {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiSelect {\n      line-height: 1em;\n    }\n  }\n  .euiSelect::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSelect::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSelect:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSelect:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSelect::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSelect:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiSelect:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSelect:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSelect:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSelect:-webkit-autofill {\n    -webkit-text-fill-color: #343741;\n  }\n  .euiSelect:-webkit-autofill ~ .euiFormControlLayoutIcons {\n    color: #343741;\n  }\n  .euiSelect--compressed {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding: 8px;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiSelect--compressed {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiSelect--compressed:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiSelect--compressed:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSelect--compressed:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSelect--compressed:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect--compressed:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect--compressed:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect--compressed:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect--compressed:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSelect--compressed[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSelect--inGroup {\n    box-shadow: none !important;\n    border-radius: 0;\n  }\n  .euiSelect-isLoading {\n    padding-right: 62px;\n  }\n  .euiSelect-isLoading.euiSelect--compressed {\n    padding-right: 54px;\n  }\n  .euiSelect--compressed {\n    padding-right: 32px;\n    line-height: 32px;\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n  .euiSelect--inGroup {\n    line-height: 38px;\n  }\n  .euiSelect--inGroup.euiSelect--compressed {\n    line-height: 30px;\n  }\n  .euiSelect::-ms-expand {\n    display: none;\n  }\n  .euiSelect:focus::-ms-value {\n    color: #343741;\n    background: rgba(0, 0, 0, 0);\n  }\n  .euiSelect:-moz-focusring {\n    color: rgba(0, 0, 0, 0);\n    text-shadow: 0 0 0 #343741;\n  }\n  .euiSuperSelect__listbox {\n    scrollbar-width: thin;\n    max-height: 300px;\n    overflow: hidden;\n    overflow-y: auto;\n  }\n  .euiSuperSelect__listbox::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiSuperSelect__listbox::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiSuperSelect__listbox::-webkit-scrollbar-corner,\n  .euiSuperSelect__listbox::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiSuperSelect__item {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    padding: 8px;\n  }\n  .euiSuperSelect__item:hover {\n    cursor: pointer;\n    text-decoration: underline;\n  }\n  .euiSuperSelect__item:focus {\n    cursor: pointer;\n    text-decoration: underline;\n    background-color: #e6f0f8;\n  }\n  .euiSuperSelect__item:disabled {\n    cursor: not-allowed;\n    text-decoration: none;\n    color: #afb0b3;\n  }\n  .euiSuperSelect__item--hasDividers:not(:last-of-type) {\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiSuperSelectControl {\n    max-width: 400px;\n    width: 100%;\n    height: 40px;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 3px 2px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    border: none;\n    border-radius: 0;\n    padding: 12px;\n    padding-right: 40px;\n    display: block;\n    text-align: left;\n    line-height: 40px;\n    padding-top: 0;\n    padding-bottom: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .euiSuperSelectControl--fullWidth {\n    max-width: 100%;\n  }\n  .euiSuperSelectControl--compressed {\n    height: 32px;\n  }\n  .euiSuperSelectControl--inGroup {\n    height: 100%;\n  }\n  @supports (-moz-appearance: none) {\n    .euiSuperSelectControl {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiSuperSelectControl {\n      line-height: 1em;\n    }\n  }\n  .euiSuperSelectControl::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperSelectControl::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperSelectControl::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiSuperSelectControl:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSuperSelectControl:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSuperSelectControl:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSuperSelectControl:-webkit-autofill {\n    -webkit-text-fill-color: #343741;\n  }\n  .euiSuperSelectControl:-webkit-autofill ~ .euiFormControlLayoutIcons {\n    color: #343741;\n  }\n  .euiSuperSelectControl--compressed {\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n    padding: 8px;\n    border-radius: 2px;\n  }\n  @supports (-moz-appearance: none) {\n    .euiSuperSelectControl--compressed {\n      transition-property: box-shadow, background-image, background-size;\n    }\n  }\n  .euiSuperSelectControl--compressed:invalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiSuperSelectControl--compressed:focus {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSuperSelectControl--compressed:disabled {\n    color: #98a2b3;\n    -webkit-text-fill-color: #98a2b3;\n    cursor: not-allowed;\n    background: #eef2f7;\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSuperSelectControl--compressed:disabled::-webkit-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl--compressed:disabled::-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl--compressed:disabled:-ms-input-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl--compressed:disabled:-moz-placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl--compressed:disabled::placeholder {\n    color: #98a2b3;\n    opacity: 1;\n  }\n  .euiSuperSelectControl--compressed[readOnly] {\n    cursor: default;\n    background: rgba(211, 218, 230, 0.05);\n    border-color: rgba(0, 0, 0, 0);\n    box-shadow: inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiSuperSelectControl--inGroup {\n    box-shadow: none !important;\n    border-radius: 0;\n  }\n  .euiSuperSelectControl-isLoading {\n    padding-right: 62px;\n  }\n  .euiSuperSelectControl-isLoading.euiSuperSelectControl--compressed {\n    padding-right: 54px;\n  }\n  .euiSuperSelectControl-isInvalid {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100%;\n  }\n  .euiSuperSelectControl--compressed {\n    padding-right: 32px;\n    line-height: 32px;\n    padding-top: 0;\n    padding-bottom: 0;\n  }\n  .euiSuperSelectControl.euiSuperSelect--isOpen__button {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n    box-shadow:\n      0 1px 1px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -2px rgba(152, 162, 179, 0.2),\n      inset 0 0 0 1px rgba(16, 38, 118, 0.1);\n  }\n  .euiHeader {\n    box-shadow:\n      0 2px 2px -1px rgba(152, 162, 179, 0.3),\n      0 1px 5px -2px rgba(152, 162, 179, 0.3);\n    height: 49px;\n    position: relative;\n    z-index: 999;\n    display: flex;\n    justify-content: space-between;\n    background: #fff;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiHeader--fixed {\n    z-index: 1000;\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n  }\n  .euiHeader--fixed + .euiHeader--fixed {\n    top: 49px;\n  }\n  .euiHeader--dark {\n    background-color: #25282f;\n    border-bottom-color: #25282f;\n  }\n  .euiHeader--dark .euiHeaderLogo__text,\n  .euiHeader--dark .euiHeaderLink,\n  .euiHeader--dark .euiHeaderSectionItemButton {\n    color: #fff;\n  }\n  .euiHeader--dark .euiHeaderLink-isActive {\n    color: #4d97cb;\n  }\n  .euiHeader--dark .euiHeaderSectionItem:after {\n    background: #69707d;\n  }\n  .euiHeader--dark .euiHeaderLogo:focus,\n  .euiHeader--dark .euiHeaderLink:focus,\n  .euiHeader--dark .euiHeaderSectionItemButton:focus {\n    background: #00365a;\n  }\n  .euiHeader--dark .euiHeaderSectionItemButton__notification--badge {\n    box-shadow: 0 0 0 1px #25282f;\n  }\n  .euiHeader--dark .euiHeaderSectionItemButton__notification--dot {\n    stroke: #25282f;\n  }\n  .euiHeaderProfile {\n    padding: 16px;\n  }\n  .euiHeaderLinks {\n    display: flex;\n  }\n  .euiHeaderLinks__list {\n    white-space: nowrap;\n    display: flex;\n    align-items: center;\n  }\n  .euiHeaderLinks__list--gutterXS > * {\n    margin: 0 4px;\n  }\n  .euiHeaderLinks__list--gutterS > * {\n    margin: 0 8px;\n  }\n  .euiHeaderLinks__list--gutterM > * {\n    margin: 0 12px;\n  }\n  .euiHeaderLinks__list--gutterL > * {\n    margin: 0 24px;\n  }\n  .euiHeaderLinks__mobileList .euiHeaderLink {\n    display: block;\n    width: 100%;\n    padding: 8px;\n  }\n  .euiHeaderLinks__mobileList .euiHeaderLink > span {\n    justify-content: flex-start;\n  }\n  .euiHeaderLogo {\n    text-align: left;\n    position: relative;\n    height: 48px;\n    line-height: 48px;\n    min-width: 49px;\n    padding: 0 13px 0 12px;\n    display: inline-flex;\n    align-items: center;\n    vertical-align: middle;\n    white-space: nowrap;\n  }\n  .euiHeaderLogo:hover {\n    text-decoration: underline;\n  }\n  .euiHeaderLogo:focus {\n    text-decoration: underline;\n    background: #e6f0f8;\n  }\n  .euiHeaderLogo:focus,\n  .euiHeaderLogo:hover {\n    text-decoration: none;\n  }\n  .euiHeaderLogo__text {\n    color: #1a1c21;\n    font-size: 20px;\n    font-size: 1.25rem;\n    line-height: 2rem;\n    font-weight: 500;\n    letter-spacing: -0.025em;\n    padding-left: 16px;\n    font-weight: 300;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiHeaderLogo {\n      padding: 0 12px;\n    }\n    .euiHeaderLogo__icon.euiIcon--xLarge {\n      width: 24px;\n      height: 24px;\n    }\n    .euiHeaderLogo__text {\n      color: #1a1c21;\n      font-size: 16px;\n      font-size: 1rem;\n      line-height: 1.5rem;\n      font-weight: 600;\n      letter-spacing: -0.02em;\n      font-weight: 400;\n    }\n  }\n  .euiHeaderAlert {\n    min-width: 300px;\n    position: relative;\n    margin-bottom: 24px;\n    padding: 0 8px 24px;\n    border-bottom: 1px solid #d3dae6;\n    border-top: none;\n  }\n  .euiHeaderAlert .euiHeaderAlert__dismiss {\n    opacity: 0;\n    position: absolute;\n    right: 12px;\n    top: 12px;\n    transition: opacity 250ms ease-in;\n  }\n  .euiHeaderAlert:hover .euiHeaderAlert__dismiss,\n  .euiHeaderAlert .euiHeaderAlert__dismiss:focus {\n    opacity: 1;\n  }\n  .euiHeaderAlert .euiHeaderAlert__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    margin-bottom: 8px;\n  }\n  .euiHeaderAlert .euiHeaderAlert__text {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    margin-bottom: 16px;\n  }\n  .euiHeaderAlert .euiHeaderAlert__action {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiHeaderAlert .euiHeaderAlert__date {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    color: #69707d;\n  }\n  .euiHeaderBreadcrumbs {\n    margin-left: 12px;\n    margin-right: 12px;\n    display: flex;\n    align-items: center;\n    flex-grow: 1;\n  }\n  .euiHeaderSection {\n    display: flex;\n    flex-grow: 0;\n    flex-shrink: 0;\n  }\n  .euiHeaderSection--grow,\n  .euiHeaderSection--left {\n    flex-grow: 1;\n  }\n  .euiHeaderSection--dontGrow {\n    flex-grow: 0;\n  }\n  .euiHeaderSectionItem {\n    position: relative;\n    display: flex;\n    align-items: center;\n  }\n  .euiHeaderSectionItem:after {\n    position: absolute;\n    content: '';\n    top: 16px;\n    bottom: 0;\n    background: #d3dae6;\n    left: 0;\n  }\n  .euiHeaderSectionItem--borderLeft:after {\n    left: 0;\n    width: 1px;\n  }\n  .euiHeaderSectionItem--borderRight:after {\n    width: 1px;\n    left: auto;\n    right: 0;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiHeaderSectionItem {\n      min-width: 36px;\n    }\n    .euiHeaderSectionItem--borderLeft:after,\n    .euiHeaderSectionItem--borderRight:after {\n      display: none;\n    }\n  }\n  .euiHeaderSectionItemButton {\n    position: relative;\n    height: 48px;\n    min-width: 48px;\n    text-align: center;\n    font-size: 0;\n  }\n  .euiHeaderSectionItemButton__notification {\n    position: absolute;\n  }\n  .euiHeaderSectionItemButton__notification--dot {\n    top: 0;\n    right: 0;\n    stroke: #fff;\n  }\n  .euiHeaderSectionItemButton__notification--badge {\n    top: 9%;\n    right: 9%;\n    box-shadow: 0 0 0 1px #fff;\n  }\n  .euiHeaderSectionItemButton__content {\n    display: inline-block;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiHeaderSectionItemButton {\n      min-width: 36px;\n    }\n    .euiHeaderSectionItemButton__notification.euiHeaderSectionItemButton__notification--dot {\n      width: 16px;\n      height: 16px;\n      top: 9%;\n    }\n  }\n  .euiHealth {\n    display: inline-block;\n  }\n  .euiHealth--textSizeXS {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n  }\n  .euiHealth--textSizeS {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiHealth--textSizeM {\n    font-size: 18px;\n    font-size: 1.125rem;\n    line-height: 1.5;\n  }\n  .euiHealth--textSizeInherit {\n    font-size: inherit;\n  }\n  .euiHorizontalRule {\n    border: none;\n    height: 1px;\n    background-color: #d3dae6;\n    flex-shrink: 0;\n    flex-grow: 0;\n  }\n  .euiHorizontalRule.euiHorizontalRule--full {\n    width: 100%;\n  }\n  .euiHorizontalRule.euiHorizontalRule--half {\n    width: 50%;\n    margin-left: auto;\n    margin-right: auto;\n  }\n  .euiHorizontalRule.euiHorizontalRule--quarter {\n    width: 25%;\n    margin-left: auto;\n    margin-right: auto;\n  }\n  .euiHorizontalRule--marginXSmall {\n    margin: 8px 0;\n  }\n  .euiHorizontalRule--marginSmall {\n    margin: 12px 0;\n  }\n  .euiHorizontalRule--marginMedium {\n    margin: 16px 0;\n  }\n  .euiHorizontalRule--marginLarge {\n    margin: 24px 0;\n  }\n  .euiHorizontalRule--marginXLarge {\n    margin: 32px 0;\n  }\n  .euiHorizontalRule--marginXXLarge {\n    margin: 40px 0;\n  }\n  .euiIcon {\n    flex-shrink: 0;\n    display: inline-block;\n    vertical-align: middle;\n    fill: currentColor;\n    transform: translate(0, 0);\n  }\n  .euiIcon:focus {\n    opacity: 1;\n    background: #e6f0f8;\n  }\n  .euiIcon--app {\n    fill: #343741;\n  }\n  .euiIcon--app .euiIcon__fillSecondary {\n    fill: #017d73;\n  }\n  .euiIcon-isLoading {\n    opacity: 0.05;\n    background-color: currentColor;\n    border-radius: 4px;\n  }\n  .euiIcon-isLoaded {\n    animation: euiIconLoading 250ms ease-in 0s 1 forwards;\n  }\n  .euiIcon--accent {\n    color: #dd0a73;\n  }\n  .euiIcon--danger {\n    color: #bd271e;\n  }\n  .euiIcon--ghost {\n    color: #fff;\n  }\n  .euiIcon--primary {\n    color: #006bb4;\n  }\n  .euiIcon--secondary {\n    color: #017d73;\n  }\n  .euiIcon--success {\n    color: #017d73;\n  }\n  .euiIcon--subdued {\n    color: #6a717d;\n  }\n  .euiIcon--text {\n    color: #343741;\n  }\n  .euiIcon--warning {\n    color: #be8100;\n  }\n  .euiIcon--inherit {\n    color: inherit;\n  }\n  .euiIcon--text,\n  .euiIcon--text .euiIcon__fillSecondary,\n  .euiIcon--subdued,\n  .euiIcon--subdued .euiIcon__fillSecondary,\n  .euiIcon--primary,\n  .euiIcon--primary .euiIcon__fillSecondary,\n  .euiIcon--customColor,\n  .euiIcon--customColor .euiIcon__fillSecondary {\n    fill: currentColor;\n  }\n  .euiIcon__fillNegative {\n    fill: #343741;\n  }\n  .euiIcon--small {\n    width: 12px;\n    height: 12px;\n  }\n  .euiIcon--medium {\n    width: 16px;\n    height: 16px;\n  }\n  .euiIcon--large {\n    width: 24px;\n    height: 24px;\n  }\n  .euiIcon--xLarge {\n    width: 32px;\n    height: 32px;\n  }\n  .euiIcon--xxLarge {\n    width: 40px;\n    height: 40px;\n  }\n  @keyframes euiIconLoading {\n    0% {\n      opacity: 0.05;\n    }\n    100% {\n      opacity: 1;\n    }\n  }\n  .euiImage {\n    display: inline-block;\n    max-width: 100%;\n    position: relative;\n    min-height: 1px;\n    line-height: 0;\n    flex-shrink: 0;\n  }\n  .euiImage .euiImage__img {\n    margin-bottom: 0;\n  }\n  .euiImage.euiImage--hasShadow .euiImage__img {\n    box-shadow:\n      0 6px 12px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -1px rgba(152, 162, 179, 0.2),\n      0 2px 2px 0 rgba(152, 162, 179, 0.2);\n  }\n  .euiImage .euiImage__button {\n    position: relative;\n    cursor: pointer;\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiImage .euiImage__button:focus {\n    outline: 2px solid rgba(0, 107, 180, 0.3);\n  }\n  .euiImage .euiImage__button:hover .euiImage__icon {\n    visibility: visible;\n    fill-opacity: 1;\n  }\n  .euiImage .euiImage__button--fullWidth {\n    width: 100%;\n  }\n  .euiImage.euiImage--allowFullScreen:hover .euiImage__caption {\n    text-decoration: underline;\n  }\n  .euiImage.euiImage--allowFullScreen:not(.euiImage--hasShadow)\n    .euiImage__button:hover,\n  .euiImage.euiImage--allowFullScreen:not(.euiImage--hasShadow)\n    .euiImage__button:focus {\n    box-shadow:\n      0 6px 12px -1px rgba(152, 162, 179, 0.2),\n      0 4px 4px -1px rgba(152, 162, 179, 0.2),\n      0 2px 2px 0 rgba(152, 162, 179, 0.2);\n  }\n  .euiImage.euiImage--allowFullScreen.euiImage--hasShadow\n    .euiImage__button:hover,\n  .euiImage.euiImage--allowFullScreen.euiImage--hasShadow\n    .euiImage__button:focus {\n    box-shadow:\n      0 12px 24px 0 rgba(65, 78, 101, 0.1),\n      0 6px 12px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n  }\n  .euiImage.euiImage--small .euiImage__img {\n    width: 7.5rem;\n  }\n  .euiImage.euiImage--medium .euiImage__img {\n    width: 12.5rem;\n  }\n  .euiImage.euiImage--large .euiImage__img {\n    width: 22.5rem;\n  }\n  .euiImage.euiImage--xlarge .euiImage__img {\n    width: 37.5rem;\n  }\n  .euiImage.euiImage--fullWidth {\n    width: 100%;\n  }\n  .euiImage.euiImage--original .euiImage__img {\n    width: auto;\n    max-width: 100%;\n  }\n  .euiImage.euiImage--floatLeft {\n    float: left;\n  }\n  .euiImage.euiImage--floatLeft[class*='euiImage--margin'] {\n    margin-left: 0;\n    margin-top: 0;\n  }\n  .euiImage.euiImage--floatRight {\n    float: right;\n  }\n  .euiImage.euiImage--floatRight[class*='euiImage--margin'] {\n    margin-right: 0;\n    margin-top: 0;\n  }\n  .euiImage.euiImage--marginSmall {\n    margin: 8px;\n  }\n  .euiImage.euiImage--marginMedium {\n    margin: 16px;\n  }\n  .euiImage.euiImage--marginLarge {\n    margin: 24px;\n  }\n  .euiImage.euiImage--marginXlarge {\n    margin: 32px;\n  }\n  .euiImage__img {\n    width: 100%;\n    vertical-align: middle;\n  }\n  .euiImage__caption {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    margin-top: 4px;\n    text-align: center;\n  }\n  .euiImage__icon {\n    visibility: hidden;\n    fill-opacity: 0;\n    position: absolute;\n    right: 16px;\n    top: 16px;\n    transition: fill-opacity 350ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    cursor: pointer;\n  }\n  .euiImage-isFullScreen {\n    position: relative;\n    max-height: 80vh;\n    max-width: 80vw;\n    animation: euiImageFullScreen 500ms cubic-bezier(0.34, 1.61, 0.7, 1);\n  }\n  .euiImage-isFullScreen:hover .euiImage__button {\n    box-shadow:\n      0 12px 24px 0 rgba(65, 78, 101, 0.1),\n      0 6px 12px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n  }\n  .euiImage-isFullScreen:hover .euiImage__caption {\n    text-decoration: underline;\n  }\n  .euiImage-isFullScreen__img {\n    max-height: 80vh;\n    max-width: 80vw;\n    vertical-align: middle;\n    cursor: pointer;\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiImage-isFullScreenCloseIcon {\n    position: absolute;\n    right: 16px;\n    top: 16px;\n    pointer-events: none;\n  }\n  @keyframes euiImageFullScreen {\n    0% {\n      opacity: 0;\n      transform: translateY(64px);\n    }\n    100% {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .euiImage.euiImage--floatLeft,\n    .euiImage.euiImage--floatRight {\n      float: none;\n    }\n    .euiImage.euiImage--floatLeft[class*='euiImage--margin'],\n    .euiImage.euiImage--floatRight[class*='euiImage--margin'] {\n      margin-top: inherit;\n      margin-right: inherit;\n      margin-bottom: inherit;\n      margin-left: inherit;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiImage.euiImage--floatLeft,\n    .euiImage.euiImage--floatRight {\n      float: none;\n    }\n    .euiImage.euiImage--floatLeft[class*='euiImage--margin'],\n    .euiImage.euiImage--floatRight[class*='euiImage--margin'] {\n      margin-top: inherit;\n      margin-right: inherit;\n      margin-bottom: inherit;\n      margin-left: inherit;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiImage.euiImage--floatLeft,\n    .euiImage.euiImage--floatRight {\n      float: none;\n    }\n    .euiImage.euiImage--floatLeft[class*='euiImage--margin'],\n    .euiImage.euiImage--floatRight[class*='euiImage--margin'] {\n      margin-top: inherit;\n      margin-right: inherit;\n      margin-bottom: inherit;\n      margin-left: inherit;\n    }\n  }\n  .euiKeyPadMenu {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    width: 288px;\n    max-width: 100%;\n  }\n  .euiKeyPadMenuItem {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    display: block;\n    padding: 4px;\n    height: 96px;\n    width: 96px;\n    color: #69707d;\n    border: 1px solid #d3dae6;\n    border-color: rgba(0, 0, 0, 0);\n    border-radius: 4px;\n    transition:\n      border-color 150ms ease-in,\n      box-shadow 150ms ease-in;\n  }\n  .euiKeyPadMenuItem:not(:disabled):hover,\n  .euiKeyPadMenuItem:not(:disabled):focus {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    border-color: #d3dae6;\n  }\n  .euiKeyPadMenuItem:not(:disabled):hover .euiKeyPadMenuItem__icon,\n  .euiKeyPadMenuItem:not(:disabled):focus .euiKeyPadMenuItem__icon {\n    transform: translateY(0);\n  }\n  .euiKeyPadMenuItem:disabled {\n    color: #afb0b3;\n    cursor: not-allowed;\n  }\n  .euiKeyPadMenuItem:disabled .euiKeyPadMenuItem__icon {\n    filter: grayscale(100%);\n  }\n  .euiKeyPadMenuItem:disabled .euiKeyPadMenuItem__icon svg * {\n    fill: #afb0b3;\n  }\n  .euiKeyPadMenuItem__inner {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n  }\n  .euiKeyPadMenuItem--hasBetaBadge .euiKeyPadMenuItem__inner {\n    position: relative;\n  }\n  .euiKeyPadMenuItem--hasBetaBadge\n    .euiKeyPadMenuItem__inner\n    .euiKeyPadMenuItem__betaBadgeWrapper {\n    position: absolute;\n    top: 4px;\n    right: 8px;\n    z-index: 3;\n  }\n  .euiKeyPadMenuItem--hasBetaBadge\n    .euiKeyPadMenuItem__inner\n    .euiKeyPadMenuItem__betaBadgeWrapper\n    .euiKeyPadMenuItem__betaBadge:not(.euiBetaBadge--iconOnly) {\n    padding: 0 6px;\n    overflow: hidden;\n    letter-spacing: 3rem;\n  }\n  .euiKeyPadMenuItem__betaBadge {\n    width: 20px;\n    height: 20px;\n    line-height: 20px;\n    color: #000;\n    background-color: #e9edf3;\n    box-shadow: none;\n  }\n  .euiKeyPadMenuItem__betaBadge .euiBetaBadge__icon {\n    width: 12px;\n    height: 12px;\n  }\n  .euiKeyPadMenuItem__icon {\n    transition: transform 250ms cubic-bezier(0.34, 1.61, 0.7, 1);\n    transform: translateY(2px);\n    margin-bottom: 12px;\n  }\n  .euiKeyPadMenuItem__label {\n    font-size: 12px;\n    font-weight: 500;\n    line-height: 16px;\n    text-align: center;\n  }\n  .euiLink {\n    text-align: left;\n  }\n  .euiLink:hover {\n    text-decoration: underline;\n  }\n  .euiLink:focus {\n    text-decoration: underline;\n    background: #e6f0f8;\n  }\n  .euiLink .euiLink__externalIcon {\n    margin-left: 4px;\n  }\n  .euiLink.euiLink-disabled {\n    text-decoration: none;\n    cursor: default;\n  }\n  .euiLink.euiLink--subdued {\n    color: #6a717d;\n  }\n  .euiLink.euiLink--subdued:hover,\n  .euiLink.euiLink--subdued:focus {\n    color: #535861;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--subdued:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--primary {\n    color: #006bb4;\n  }\n  .euiLink.euiLink--primary:hover,\n  .euiLink.euiLink--primary:focus {\n    color: #004d81;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--primary:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--secondary {\n    color: #017d73;\n  }\n  .euiLink.euiLink--secondary:hover,\n  .euiLink.euiLink--secondary:focus {\n    color: #014a44;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--secondary:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--success {\n    color: #017d73;\n  }\n  .euiLink.euiLink--success:hover,\n  .euiLink.euiLink--success:focus {\n    color: #014a44;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--success:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--accent {\n    color: #dd0a73;\n  }\n  .euiLink.euiLink--accent:hover,\n  .euiLink.euiLink--accent:focus {\n    color: #ac085a;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--accent:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--warning {\n    color: #9b6900;\n  }\n  .euiLink.euiLink--warning:hover,\n  .euiLink.euiLink--warning:focus {\n    color: #684600;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--warning:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--danger {\n    color: #bd271e;\n  }\n  .euiLink.euiLink--danger:hover,\n  .euiLink.euiLink--danger:focus {\n    color: #911e17;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--danger:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--text {\n    color: #343741;\n  }\n  .euiLink.euiLink--text:hover,\n  .euiLink.euiLink--text:focus {\n    color: #1d1f25;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--text:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiLink.euiLink--ghost {\n    color: #fff;\n  }\n  .euiLink.euiLink--ghost:hover,\n  .euiLink.euiLink--ghost:focus {\n    color: #e6e6e6;\n    text-decoration: underline;\n  }\n  .euiLink.euiLink--ghost:focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  button.euiLink {\n    user-select: text;\n  }\n  .euiListGroup.euiListGroup-flush {\n    padding: 0;\n    border: none;\n  }\n  .euiListGroup.euiListGroup-bordered {\n    border-radius: 4px;\n    border: 1px solid #d3dae6;\n  }\n  .euiListGroup-maxWidthDefault {\n    max-width: 400px;\n  }\n  .euiListGroup--gutterSmall {\n    padding: 8px;\n  }\n  .euiListGroup--gutterSmall .euiListGroupItem:not(:first-of-type) {\n    margin-top: 8px;\n  }\n  .euiListGroup--gutterMedium {\n    padding: 16px;\n  }\n  .euiListGroup--gutterMedium .euiListGroupItem:not(:first-of-type) {\n    margin-top: 16px;\n  }\n  .euiListGroupItem {\n    padding: 0;\n    border-radius: 4px;\n    display: flex;\n    align-items: center;\n    transition: background-color 150ms;\n    position: relative;\n  }\n  .euiListGroupItem.euiListGroupItem-isActive,\n  .euiListGroupItem.euiListGroupItem-isClickable:hover {\n    background-color: rgba(211, 218, 230, 0.25);\n  }\n  .euiListGroupItem.euiListGroupItem-isClickable:focus-within {\n    background-color: rgba(211, 218, 230, 0.25);\n  }\n  .euiListGroupItem.euiListGroupItem--ghost.euiListGroupItem-isClickable:hover {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .euiListGroupItem.euiListGroupItem--ghost.euiListGroupItem-isClickable:focus-within {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .euiListGroupItem.euiListGroupItem-isClickable:hover\n    .euiListGroupItem__button,\n  .euiListGroupItem .euiListGroupItem__button:hover,\n  .euiListGroupItem .euiListGroupItem__button:focus {\n    text-decoration: underline;\n  }\n  .euiListGroupItem.euiListGroupItem-isDisabled,\n  .euiListGroupItem.euiListGroupItem-isDisabled:hover,\n  .euiListGroupItem.euiListGroupItem-isDisabled:focus,\n  .euiListGroupItem.euiListGroupItem-isDisabled .euiListGroupItem__button:hover,\n  .euiListGroupItem.euiListGroupItem-isDisabled\n    .euiListGroupItem__button:focus {\n    color: #c2c3c6;\n    cursor: not-allowed;\n    background-color: rgba(0, 0, 0, 0);\n    text-decoration: none;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiListGroupItem__button:hover,\n    .euiListGroupItem__button:focus {\n      background-color: rgba(211, 218, 230, 0.25);\n      border-radius: 4px;\n    }\n    .euiListGroupItem__button:hover\n      .euiListGroupItem--ghost\n      .euiListGroupItem__button:hover,\n    .euiListGroupItem__button:hover\n      .euiListGroupItem--ghost\n      .euiListGroupItem__button:focus,\n    .euiListGroupItem__button:focus\n      .euiListGroupItem--ghost\n      .euiListGroupItem__button:hover,\n    .euiListGroupItem__button:focus\n      .euiListGroupItem--ghost\n      .euiListGroupItem__button:focus {\n      background-color: rgba(255, 255, 255, 0.1);\n    }\n  }\n  .euiListGroupItem__text,\n  .euiListGroupItem__button {\n    line-height: 24px;\n    padding: 4px 8px;\n    display: flex;\n    align-items: center;\n    flex: 1 0 auto;\n    text-align: left;\n    max-width: 100%;\n    font-weight: inherit;\n  }\n  .euiListGroupItem-hasExtraAction .euiListGroupItem__text,\n  .euiListGroupItem-hasExtraAction .euiListGroupItem__button {\n    max-width: calc(100% - 32px);\n  }\n  .euiListGroupItem--primary .euiListGroupItem__text:not(:disabled),\n  .euiListGroupItem--primary .euiListGroupItem__button:not(:disabled) {\n    color: #006bb4;\n  }\n  .euiListGroupItem--text .euiListGroupItem__text:not(:disabled),\n  .euiListGroupItem--text .euiListGroupItem__button:not(:disabled) {\n    color: #343741;\n  }\n  .euiListGroupItem--subdued .euiListGroupItem__text:not(:disabled),\n  .euiListGroupItem--subdued .euiListGroupItem__button:not(:disabled) {\n    color: #6a717d;\n  }\n  .euiListGroupItem--ghost .euiListGroupItem__text:not(:disabled),\n  .euiListGroupItem--ghost .euiListGroupItem__button:not(:disabled) {\n    color: #fff;\n  }\n  .euiListGroupItem-isActive:not(.euiListGroupItem--ghost)\n    .euiListGroupItem__text,\n  .euiListGroupItem-isActive:not(.euiListGroupItem--ghost)\n    .euiListGroupItem__button {\n    color: #343741;\n  }\n  .euiListGroupItem__label {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .euiListGroupItem__extraAction {\n    opacity: 0;\n    margin-right: 8px;\n    transition: opacity 150ms;\n  }\n  .euiListGroupItem:not(.euiListGroupItem-isDisabled):focus\n    .euiListGroupItem__extraAction,\n  .euiListGroupItem:not(.euiListGroupItem-isDisabled):hover\n    .euiListGroupItem__extraAction,\n  .euiListGroupItem__extraAction.euiListGroupItem__extraAction-alwaysShow,\n  .euiListGroupItem__extraAction:focus {\n    opacity: 1;\n  }\n  .euiListGroupItem__icon {\n    margin-right: 12px;\n    flex-grow: 0;\n    flex-shrink: 0;\n  }\n  .euiListGroupItem--xSmall {\n    font-size: 12px;\n  }\n  .euiListGroupItem--small {\n    font-size: 14px;\n  }\n  .euiListGroupItem--medium {\n    font-size: 16px;\n  }\n  .euiListGroupItem--large {\n    font-size: 20px;\n  }\n  .euiListGroupItem--xSmall,\n  .euiListGroupItem--small {\n    font-weight: 500;\n    letter-spacing: 0;\n  }\n  .euiListGroupItem--xSmall .euiListGroupItem__button,\n  .euiListGroupItem--xSmall .euiListGroupItem__text {\n    line-height: 16px;\n  }\n  .euiListGroupItem--large .euiListGroupItem__button,\n  .euiListGroupItem--large .euiListGroupItem__text {\n    line-height: 32px;\n  }\n  .euiListGroupItem--wrapText .euiListGroupItem__button,\n  .euiListGroupItem--wrapText .euiListGroupItem__text {\n    width: 100%;\n    word-break: break-word;\n  }\n  .euiListGroupItem--wrapText .euiListGroupItem__label {\n    white-space: inherit;\n  }\n  .euiListGroup-flush .euiListGroupItem {\n    border-radius: 0;\n  }\n  .euiListGroup-bordered .euiListGroupItem:first-child {\n    border-top-left-radius: 4px;\n    border-top-right-radius: 4px;\n  }\n  .euiListGroup-bordered .euiListGroupItem:last-child {\n    border-bottom-left-radius: 4px;\n    border-bottom-right-radius: 4px;\n  }\n  .euiListGroupItem__tooltip {\n    width: 100%;\n  }\n  .euiPinnableListGroup__itemExtraAction svg {\n    transform: rotate(45deg);\n  }\n  .euiPinnableListGroup__itemExtraAction-pinned:not(:hover):not(:focus) {\n    color: #8c919a;\n  }\n  .euiLoadingLogo,\n  .euiLoadingKibana {\n    position: relative;\n    display: inline-block;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiLoadingLogo:before,\n    .euiLoadingLogo:after,\n    .euiLoadingKibana:before,\n    .euiLoadingKibana:after {\n      position: absolute;\n      content: '';\n      width: 90%;\n      left: 5%;\n      border-radius: 50%;\n      opacity: 0.2;\n      z-index: 1;\n    }\n    .euiLoadingLogo:before,\n    .euiLoadingKibana:before {\n      box-shadow: 0 0 8px #000;\n      animation: 1s euiLoadingKibanaPulsateAndFade\n        cubic-bezier(0.694, 0.0482, 0.335, 1) infinite;\n    }\n    .euiLoadingLogo:after,\n    .euiLoadingKibana:after {\n      background-color: #000;\n      animation: 1s euiLoadingKibanaPulsate\n        cubic-bezier(0.694, 0.0482, 0.335, 1) infinite;\n    }\n  }\n  .euiLoadingLogo .euiLoadingLogo__icon,\n  .euiLoadingLogo .euiLoadingKibana__icon,\n  .euiLoadingKibana .euiLoadingLogo__icon,\n  .euiLoadingKibana .euiLoadingKibana__icon {\n    display: block;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiLoadingLogo .euiLoadingLogo__icon,\n    .euiLoadingLogo .euiLoadingKibana__icon,\n    .euiLoadingKibana .euiLoadingLogo__icon,\n    .euiLoadingKibana .euiLoadingKibana__icon {\n      animation: 1s euiLoadingKibanaBounceMedium\n        cubic-bezier(0.694, 0.0482, 0.335, 1) infinite;\n    }\n  }\n  .euiLoadingLogo--medium,\n  .euiLoadingKibana--medium {\n    width: 16px;\n  }\n  .euiLoadingLogo--medium:before,\n  .euiLoadingLogo--medium:after,\n  .euiLoadingKibana--medium:before,\n  .euiLoadingKibana--medium:after {\n    height: 3px;\n    bottom: -4px;\n  }\n  .euiLoadingLogo--medium .euiLoadingLogo__icon,\n  .euiLoadingLogo--medium .euiLoadingKibana__icon,\n  .euiLoadingKibana--medium .euiLoadingLogo__icon,\n  .euiLoadingKibana--medium .euiLoadingKibana__icon {\n    z-index: 999;\n    animation-name: euiLoadingKibanaBounceMedium;\n  }\n  .euiLoadingLogo--large,\n  .euiLoadingKibana--large {\n    width: 24px;\n  }\n  .euiLoadingLogo--large:before,\n  .euiLoadingLogo--large:after,\n  .euiLoadingKibana--large:before,\n  .euiLoadingKibana--large:after {\n    height: 6px;\n    bottom: -8px;\n  }\n  .euiLoadingLogo--large .euiLoadingLogo__icon,\n  .euiLoadingLogo--large .euiLoadingKibana__icon,\n  .euiLoadingKibana--large .euiLoadingLogo__icon,\n  .euiLoadingKibana--large .euiLoadingKibana__icon {\n    animation-name: euiLoadingKibanaBounceLarge;\n  }\n  .euiLoadingLogo--xLarge,\n  .euiLoadingKibana--xLarge {\n    width: 32px;\n  }\n  .euiLoadingLogo--xLarge:before,\n  .euiLoadingLogo--xLarge:after,\n  .euiLoadingKibana--xLarge:before,\n  .euiLoadingKibana--xLarge:after {\n    height: 8px;\n    bottom: -12px;\n  }\n  .euiLoadingLogo--xLarge .euiLoadingLogo__icon,\n  .euiLoadingLogo--xLarge .euiLoadingKibana__icon,\n  .euiLoadingKibana--xLarge .euiLoadingLogo__icon,\n  .euiLoadingKibana--xLarge .euiLoadingKibana__icon {\n    animation-name: euiLoadingKibanaBounceXLarge;\n  }\n  @keyframes euiLoadingKibanaBounceMedium {\n    50% {\n      transform: translateY(-8px);\n    }\n  }\n  @keyframes euiLoadingKibanaBounceLarge {\n    50% {\n      transform: translateY(-12px);\n    }\n  }\n  @keyframes euiLoadingKibanaBounceXLarge {\n    50% {\n      transform: translateY(-16px);\n    }\n  }\n  @keyframes euiLoadingKibanaPulsateAndFade {\n    0% {\n      opacity: 0;\n    }\n    50% {\n      transform: scale(0.5);\n      opacity: 0.1;\n    }\n    100% {\n      opacity: 0;\n    }\n  }\n  @keyframes euiLoadingKibanaPulsate {\n    0% {\n      opacity: 0.15;\n    }\n    50% {\n      transform: scale(0.5);\n      opacity: 0.05;\n    }\n    100% {\n      opacity: 0.15;\n    }\n  }\n  .euiLoadingElastic {\n    position: relative;\n    display: inline-block;\n  }\n  .euiLoadingElastic--medium {\n    width: 16px;\n  }\n  .euiLoadingElastic--large {\n    width: 24px;\n  }\n  .euiLoadingElastic--xLarge {\n    width: 32px;\n  }\n  .euiLoadingElastic--xxLarge {\n    width: 40px;\n  }\n  .euiLoadingElastic path {\n    animation-fill-mode: forwards;\n    animation-direction: alternate;\n    transform-style: preserve-3d;\n    animation-duration: 1s;\n    animation-timing-function: cubic-bezier(0, 0.63, 0.49, 1);\n    animation-iteration-count: infinite;\n    transform-origin: 50% 50%;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiLoadingElastic path {\n      animation-name: euiLoadingElastic;\n    }\n  }\n  .euiLoadingElastic path:nth-of-type(1) {\n    animation-delay: 0s;\n  }\n  .euiLoadingElastic path:nth-of-type(2) {\n    animation-delay: 0.035s;\n  }\n  .euiLoadingElastic path:nth-of-type(3) {\n    animation-delay: 0.125s;\n  }\n  .euiLoadingElastic path:nth-of-type(4) {\n    animation-delay: 0.155s;\n  }\n  .euiLoadingElastic path:nth-of-type(5) {\n    animation-delay: 0.075s;\n  }\n  .euiLoadingElastic path:nth-of-type(6) {\n    animation-delay: 0.06s;\n  }\n  @keyframes euiLoadingElastic {\n    0% {\n      transform: scale3d(0, 0, -0.7);\n      opacity: 0;\n    }\n    40% {\n      transform: scale3d(1, 1, 2);\n      opacity: 1;\n    }\n    50% {\n      transform: scale3d(0.99, 0.99, 2);\n    }\n    70% {\n      transform: scale3d(0.96, 0.96, -2.5);\n    }\n    100% {\n      transform: scale3d(0.98, 0.98, 2);\n    }\n  }\n  .euiLoadingChart {\n    height: 32px;\n    z-index: 500;\n    overflow: hidden;\n    display: inline-block;\n  }\n  .euiLoadingChart__bar {\n    height: 100%;\n    width: 8px;\n    display: inline-block;\n    margin-bottom: -16px;\n    margin-left: 2px;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiLoadingChart__bar {\n      animation: euiLoadingChart 1s infinite;\n    }\n  }\n  .euiLoadingChart__bar:nth-child(1) {\n    background-color: #54b399;\n  }\n  @media screen and (prefers-reduced-motion: reduce) {\n    .euiLoadingChart__bar:nth-child(1) {\n      transform: translateY(66%);\n    }\n  }\n  .euiLoadingChart__bar:nth-child(2) {\n    background-color: #6092c0;\n    animation-delay: 0.1s;\n  }\n  @media screen and (prefers-reduced-motion: reduce) {\n    .euiLoadingChart__bar:nth-child(2) {\n      transform: translateY(44%);\n    }\n  }\n  .euiLoadingChart__bar:nth-child(3) {\n    background-color: #d36086;\n    animation-delay: 0.2s;\n  }\n  @media screen and (prefers-reduced-motion: reduce) {\n    .euiLoadingChart__bar:nth-child(3) {\n      transform: translateY(22%);\n    }\n  }\n  .euiLoadingChart__bar:nth-child(4) {\n    background-color: #9170b8;\n    animation-delay: 0.3s;\n  }\n  .euiLoadingChart--mono .euiLoadingChart__bar:nth-child(1) {\n    background-color: #d3dae6;\n  }\n  .euiLoadingChart--mono .euiLoadingChart__bar:nth-child(2) {\n    background-color: #cbd1dd;\n  }\n  .euiLoadingChart--mono .euiLoadingChart__bar:nth-child(3) {\n    background-color: #c2c9d4;\n  }\n  .euiLoadingChart--mono .euiLoadingChart__bar:nth-child(4) {\n    background-color: #bac0ca;\n  }\n  .euiLoadingChart--medium {\n    height: 16px;\n  }\n  .euiLoadingChart--medium > span {\n    width: 2px;\n    margin-left: 2px;\n    margin-bottom: 8px;\n  }\n  .euiLoadingChart--large {\n    height: 24px;\n  }\n  .euiLoadingChart--large > span {\n    width: 4px;\n    margin-left: 2px;\n    margin-bottom: 12px;\n  }\n  .euiLoadingChart--xLarge {\n    height: 32px;\n  }\n  .euiLoadingChart--xLarge > span {\n    width: 8px;\n    margin-left: 4px;\n    margin-bottom: 16px;\n  }\n  @keyframes euiLoadingChart {\n    0% {\n      transform: translateY(0);\n    }\n    50% {\n      transform: translateY(66%);\n    }\n    100% {\n      transform: translateY(0);\n    }\n  }\n  .euiLoadingContent__loader {\n    display: block;\n    width: 100%;\n  }\n  .euiLoadingContent__singleLine {\n    display: block;\n    width: 100%;\n    height: 16px;\n    margin-bottom: 8px;\n    border-radius: 4px;\n    overflow: hidden;\n  }\n  .euiLoadingContent__singleLine:last-child:not(:only-child) {\n    width: 75%;\n  }\n  .euiLoadingContent__singleLineBackground {\n    display: block;\n    width: 220%;\n    height: 100%;\n    background: linear-gradient(137deg, #f0f2f6 45%, #f6f8fa 50%, #f0f2f6 55%);\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiLoadingContent__singleLineBackground {\n      animation: euiLoadingContentGradientLoad 1.5s\n        cubic-bezier(0.694, 0.0482, 0.335, 1) infinite;\n    }\n  }\n  @keyframes euiLoadingContentGradientLoad {\n    0% {\n      transform: translateX(-53%);\n    }\n    100% {\n      transform: translateX(0);\n    }\n  }\n  .euiLoadingSpinner {\n    flex-shrink: 0;\n    display: inline-block;\n    width: 32px;\n    height: 32px;\n    border-radius: 50%;\n    border: solid 2px #d3dae6;\n    border-color: var(--euiColorPrimary) var(--euiColorLightShade)\n      var(--euiColorLightShade) var(--euiColorLightShade);\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiLoadingSpinner {\n      animation: euiLoadingSpinner 0.6s infinite linear;\n    }\n  }\n  .euiLoadingSpinner--small {\n    width: 8px;\n    height: 8px;\n    border-width: 1px;\n  }\n  .euiLoadingSpinner--medium {\n    width: 16px;\n    height: 16px;\n    border-width: 1px;\n  }\n  .euiLoadingSpinner--large {\n    width: 24px;\n    height: 24px;\n  }\n  .euiLoadingSpinner--xLarge {\n    width: 32px;\n    height: 32px;\n  }\n  @keyframes euiLoadingSpinner {\n    from {\n      transform: rotate(0deg);\n    }\n    to {\n      transform: rotate(359deg);\n    }\n  }\n  .euiMarkdownEditor--isPreviewing .euiMarkdownEditor__toggleContainer {\n    display: none;\n  }\n  .euiMarkdownEditor--fullHeight {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n  }\n  .euiMarkdownEditor--fullHeight .euiMarkdownEditorTextArea {\n    resize: none;\n  }\n  .euiMarkdownEditor--fullHeight .euiMarkdownEditorDropZone {\n    height: 100%;\n  }\n  .euiMarkdownEditorDropZone {\n    display: flex;\n    position: relative;\n    flex-direction: column;\n    min-height: '150px';\n  }\n  .euiMarkdownEditorDropZone__input {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    opacity: 0;\n    overflow: hidden;\n  }\n  .euiMarkdownEditorDropZone__input:hover {\n    cursor: pointer;\n  }\n  .euiMarkdownEditorDropZone__input:hover:disabled {\n    cursor: not-allowed;\n  }\n  .euiMarkdownEditorDropZone--isDragging .euiMarkdownEditorFooter,\n  .euiMarkdownEditorDropZone--isDragging .euiMarkdownEditorTextArea,\n  .euiMarkdownEditorDropZone--isDragging .euiMarkdownEditorTextArea:focus,\n  .euiMarkdownEditorDropZone--isDragging\n    .euiMarkdownEditor:focus-within\n    .euiMarkdownEditorTextArea {\n    background-color: rgba(0, 107, 180, 0.1) !important;\n  }\n  .euiMarkdownEditorDropZone--isDragging .euiMarkdownEditorTextArea,\n  .euiMarkdownEditorDropZone--isDragging .euiMarkdownEditorTextArea:focus {\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    ) !important;\n  }\n  .euiMarkdownEditorDropZone--isDraggingError .euiMarkdownEditorFooter,\n  .euiMarkdownEditorDropZone--isDraggingError .euiMarkdownEditorTextArea,\n  .euiMarkdownEditorDropZone--isDraggingError .euiMarkdownEditorTextArea:focus,\n  .euiMarkdownEditorDropZone--isDraggingError\n    .euiMarkdownEditor:focus-within\n    .euiMarkdownEditorTextArea {\n    background-color: rgba(189, 39, 30, 0.1) !important;\n  }\n  .euiMarkdownEditorDropZone--hasError .euiMarkdownEditorTextArea,\n  .euiMarkdownEditorDropZone--hasError .euiMarkdownEditorTextArea:focus {\n    background-image: linear-gradient(\n      to top,\n      #bd271e,\n      #bd271e 2px,\n      transparent 2px,\n      transparent 100%\n    ) !important;\n  }\n  .euiMarkdownFormat {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    color: #343741;\n    font-weight: 400;\n  }\n  .euiMarkdownFormat--reversed {\n    color: #f5f7fa;\n  }\n  .euiMarkdownFormat > div > *:first-child {\n    margin-top: 0 !important;\n  }\n  .euiMarkdownFormat > div > * {\n    margin-top: 0;\n    margin-bottom: 1em;\n  }\n  .euiMarkdownFormat > div > *:last-child,\n  .euiMarkdownFormat .euiCheckbox {\n    margin-bottom: 0 !important;\n  }\n  .euiMarkdownFormat .euiCheckbox + *:not(.euiCheckbox) {\n    margin-top: 1em;\n  }\n  .euiMarkdownFormat p,\n  .euiMarkdownFormat blockquote,\n  .euiMarkdownFormat ul,\n  .euiMarkdownFormat ol,\n  .euiMarkdownFormat dl,\n  .euiMarkdownFormat pre,\n  .euiMarkdownFormat table {\n    margin-top: 0;\n    margin-bottom: 1em;\n    line-height: 1.5em;\n  }\n  .euiMarkdownFormat strong {\n    font-weight: 600;\n  }\n  .euiMarkdownFormat h1,\n  .euiMarkdownFormat h2,\n  .euiMarkdownFormat h3,\n  .euiMarkdownFormat h4,\n  .euiMarkdownFormat h5,\n  .euiMarkdownFormat h6 {\n    margin-top: 0;\n    margin-bottom: 0.5em;\n  }\n  .euiMarkdownFormat h1 {\n    font-size: 2.25em;\n    line-height: 1.333333em;\n    font-weight: 300;\n  }\n  .euiMarkdownFormat h2 {\n    font-size: 1.75em;\n    line-height: 1.428571em;\n    font-weight: 300;\n  }\n  .euiMarkdownFormat h3 {\n    font-size: 1.25em;\n    line-height: 1.6em;\n    font-weight: 600;\n  }\n  .euiMarkdownFormat h4 {\n    font-size: 1em;\n    line-height: 1.5em;\n    font-weight: 600;\n  }\n  .euiMarkdownFormat h5 {\n    font-size: 0.875em;\n    line-height: 1.142857em;\n    font-weight: 700;\n  }\n  .euiMarkdownFormat h6 {\n    font-size: 0.75em;\n    line-height: 1.333333em;\n    font-weight: 700;\n    text-transform: uppercase;\n  }\n  .euiMarkdownFormat img {\n    max-width: 100%;\n    box-sizing: content-box;\n    border-style: none;\n  }\n  .euiMarkdownFormat blockquote {\n    padding: 0 1em;\n    border-left: 0.25em solid rgba(0, 0, 0, 0.15);\n  }\n  .euiMarkdownFormat--reversed blockquote {\n    border-left-color: rgba(255, 255, 255, 0.15);\n  }\n  .euiMarkdownFormat hr {\n    border: none;\n    height: 1px;\n    background-color: rgba(0, 0, 0, 0.15);\n    margin: 1.5em 0;\n  }\n  .euiMarkdownFormat hr::before {\n    display: table;\n    content: '';\n  }\n  .euiMarkdownFormat hr::after {\n    display: table;\n    clear: both;\n    content: '';\n  }\n  .euiMarkdownFormat ul,\n  .euiMarkdownFormat ol {\n    padding-left: 1.5em;\n    margin-top: 0;\n    margin-bottom: 1em;\n  }\n  .euiMarkdownFormat ul {\n    list-style-type: disc;\n  }\n  .euiMarkdownFormat ol {\n    list-style-type: decimal;\n  }\n  .euiMarkdownFormat ul ul {\n    list-style-type: circle;\n  }\n  .euiMarkdownFormat ol ol,\n  .euiMarkdownFormat ul ol {\n    list-style-type: lower-roman;\n  }\n  .euiMarkdownFormat ul ul ol,\n  .euiMarkdownFormat ul ol ol,\n  .euiMarkdownFormat ol ul ol,\n  .euiMarkdownFormat ol ol ol {\n    list-style-type: lower-alpha;\n  }\n  .euiMarkdownFormat dd {\n    margin-left: 0;\n  }\n  .euiMarkdownFormat ul ul,\n  .euiMarkdownFormat ul ol,\n  .euiMarkdownFormat ol ol,\n  .euiMarkdownFormat ol ul {\n    margin-top: 0;\n    margin-bottom: 0;\n  }\n  .euiMarkdownFormat li > p {\n    margin-bottom: 0.5em;\n  }\n  .euiMarkdownFormat li + li {\n    margin-top: 0.25em;\n  }\n  .euiMarkdownFormat .task-list-item {\n    list-style-type: none;\n  }\n  .euiMarkdownFormat .task-list-item + .task-list-item {\n    margin-top: 0.25em;\n  }\n  .euiMarkdownFormat .task-list-item input {\n    margin: 0 0.2em 0.25em -1.6em;\n    vertical-align: middle;\n  }\n  .euiMarkdownFormat table {\n    display: block;\n    width: 100%;\n    overflow: auto;\n    border-left: 1px solid rgba(0, 0, 0, 0.15);\n    border-spacing: 0;\n    border-collapse: collapse;\n  }\n  .euiMarkdownFormat td,\n  .euiMarkdownFormat th {\n    padding: 0;\n  }\n  .euiMarkdownFormat table th,\n  .euiMarkdownFormat table td {\n    padding: 0.25em 0.5em;\n    border-top: 1px solid rgba(0, 0, 0, 0.15);\n    border-bottom: 1px solid rgba(0, 0, 0, 0.15);\n  }\n  .euiMarkdownFormat table th:last-child,\n  .euiMarkdownFormat table td:last-child {\n    border-right: 1px solid rgba(0, 0, 0, 0.15);\n  }\n  .euiMarkdownFormat table tr {\n    background-color: rgba(0, 0, 0, 0);\n    border-top: 1px solid rgba(0, 0, 0, 0.15);\n  }\n  .euiMarkdownEditorFooter {\n    display: inline-flex;\n    padding: 4px;\n    border: 1px solid #d3dae6;\n    align-items: center;\n    background: #fafbfd;\n  }\n  .euiMarkdownEditorFooter__popover {\n    width: 300px;\n  }\n  .euiMarkdownEditorFooter__actions {\n    flex: 1;\n    display: inline-flex;\n  }\n  .euiMarkdownEditorFooter__actions > button,\n  .euiMarkdownEditorFooter__actions > span {\n    margin-right: 4px;\n    align-self: center;\n  }\n  .euiMarkdownEditorFooter__actions .euiMarkdownEditorFooter__uploadError {\n    position: relative;\n    left: -1px;\n    line-height: 1;\n    border-radius: 4px;\n  }\n  .euiMarkdownEditorFooter__actions\n    .euiMarkdownEditorFooter__uploadError\n    > span {\n    padding: 0 4px;\n  }\n  .euiMarkdownEditorFooter__help {\n    justify-self: flex-end;\n  }\n  .euiMarkdownEditorFooter__help > svg {\n    width: 26px;\n  }\n  .euiMarkdownEditorFooter__errors > svg {\n    color: #6a717d;\n  }\n  .euiMarkdownEditorPreview {\n    scrollbar-width: thin;\n    background: #fff;\n    min-height: '150px';\n    overflow-y: auto;\n    border: 1px solid #d3dae6;\n    padding: 12px;\n  }\n  .euiMarkdownEditorPreview::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiMarkdownEditorPreview::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiMarkdownEditorPreview::-webkit-scrollbar-corner,\n  .euiMarkdownEditorPreview::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiMarkdownEditorTextArea {\n    font-family:\n      'Inter UI',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      Helvetica,\n      Arial,\n      sans-serif,\n      'Apple Color Emoji',\n      'Segoe UI Emoji',\n      'Segoe UI Symbol';\n    font-weight: 400;\n    letter-spacing: -0.005em;\n    -webkit-text-size-adjust: 100%;\n    -ms-text-size-adjust: 100%;\n    font-kerning: normal;\n    font-size: 14px;\n    color: #343741;\n    scrollbar-width: thin;\n    width: 100%;\n    height: 100%;\n    min-height: '150px';\n    padding: 12px;\n    border: 1px solid #d3dae6;\n    border-bottom: none;\n    line-height: 1.5;\n    resize: vertical;\n    background-color: #fbfcfd;\n    background-repeat: no-repeat;\n    background-size: 0% 100%;\n    margin: 0;\n    transition:\n      box-shadow 150ms ease-in,\n      background-image 150ms ease-in,\n      background-size 150ms ease-in,\n      background-color 150ms ease-in;\n  }\n  @media screen and (-ms-high-contrast: active),\n    screen and (-ms-high-contrast: none) {\n    .euiMarkdownEditorTextArea {\n      line-height: 1em;\n    }\n  }\n  .euiMarkdownEditorTextArea::-webkit-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiMarkdownEditorTextArea::-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiMarkdownEditorTextArea:-ms-input-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiMarkdownEditorTextArea:-moz-placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiMarkdownEditorTextArea::placeholder {\n    color: #6a717d;\n    opacity: 1;\n  }\n  .euiMarkdownEditorTextArea::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiMarkdownEditorTextArea::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiMarkdownEditorTextArea::-webkit-scrollbar-corner,\n  .euiMarkdownEditorTextArea::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiMarkdownEditorTextArea:focus,\n  .euiMarkdownEditor:focus-within .euiMarkdownEditorTextArea {\n    background-color: #fff;\n    background-image: linear-gradient(\n      to top,\n      #006bb4,\n      #006bb4 2px,\n      transparent 2px,\n      transparent 100%\n    );\n    background-size: 100% 100%;\n  }\n  .euiMarkdownEditorToolbar {\n    display: flex;\n    flex-wrap: wrap;\n    background: #f5f7fa;\n    border: 1px solid #d3dae6;\n    border-color: #d3dae6;\n    border-bottom: none;\n    padding: 4px;\n  }\n  .euiMarkdownEditorToolbar__buttons {\n    display: flex;\n    flex-wrap: wrap;\n    flex: 1;\n    align-items: center;\n  }\n  .euiMarkdownEditorToolbar__buttons > * {\n    margin-right: 4px;\n  }\n  .euiMarkdownEditorToolbar__divider {\n    content: '';\n    height: 24px;\n    display: block;\n    margin-left: 4px;\n    padding-right: 4px;\n    border-left: 1px solid #d3dae6;\n  }\n  .euiMarkdownTooltip__icon {\n    transform: translateY(-1px);\n  }\n  .euiMark {\n    background-color: rgba(0, 0, 0, 0);\n    font-weight: 700;\n    color: #343741;\n  }\n  .euiModal {\n    border: 1px solid #d3dae6;\n    box-shadow:\n      0 40px 64px 0 rgba(65, 78, 101, 0.1),\n      0 24px 32px 0 rgba(65, 78, 101, 0.1),\n      0 16px 16px 0 rgba(65, 78, 101, 0.1),\n      0 8px 8px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n    border-color: #c6cad1;\n    border-top-color: #e3e4e8;\n    border-bottom-color: #aaafba;\n    display: flex;\n    position: relative;\n    background-color: #fff;\n    border-radius: 4px;\n    z-index: 8000;\n    min-width: 400px;\n    animation: euiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1);\n    max-width: calc(100vw - 16px);\n  }\n  .euiModal:focus {\n    outline: none;\n  }\n  .euiModal .euiModal__flex {\n    flex: 1 1 auto;\n    display: flex;\n    flex-direction: column;\n    max-height: 75vh;\n    overflow: hidden;\n  }\n  .euiModal--maxWidth-default {\n    max-width: min(768px, 100vw - 16px);\n  }\n  .euiModal--confirmation {\n    min-width: 400px;\n  }\n  .euiModalHeader {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 24px 40px 16px 24px;\n    flex-grow: 0;\n    flex-shrink: 0;\n  }\n  .euiModalHeader__title {\n    color: #1a1c21;\n    font-size: 28px;\n    font-size: 1.75rem;\n    line-height: 2.5rem;\n    font-weight: 300;\n    letter-spacing: -0.04em;\n  }\n  .euiModalBody {\n    flex-grow: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n  }\n  .euiModalBody .euiModalBody__overflow {\n    scrollbar-width: thin;\n    height: 100%;\n    overflow-y: auto;\n    overflow-x: hidden;\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n    padding: 8px 24px;\n  }\n  .euiModalBody .euiModalBody__overflow::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiModalBody .euiModalBody__overflow::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiModalBody .euiModalBody__overflow::-webkit-scrollbar-corner,\n  .euiModalBody .euiModalBody__overflow::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiModalBody .euiModalBody__overflow:focus {\n    outline: none;\n  }\n  .euiModalBody .euiModalBody__overflow[tabindex='0']:focus:focus-visible {\n    outline-style: auto;\n  }\n  .euiModalFooter {\n    display: flex;\n    justify-content: flex-end;\n    padding: 16px 24px 24px;\n    flex-grow: 0;\n    flex-shrink: 0;\n  }\n  .euiModalFooter > * + * {\n    margin-left: 16px;\n  }\n  .euiModalHeader + .euiModalFooter {\n    padding-top: 8px;\n  }\n  .euiModalBody:last-of-type .euiModalBody__overflow {\n    padding-bottom: 24px;\n  }\n  .euiModal__closeIcon {\n    background-color: rgba(255, 255, 255, 0.9);\n    position: absolute;\n    right: 4px;\n    top: 4px;\n    z-index: 3;\n  }\n  @keyframes euiModal {\n    0% {\n      opacity: 0;\n      transform: translateY(32px);\n    }\n    100% {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .euiModal {\n      position: fixed;\n      width: 100vw !important;\n      max-width: none !important;\n      min-width: 0 !important;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      top: 0;\n      border-radius: 0;\n      border: none;\n    }\n    .euiModal.euiModal--confirmation {\n      box-shadow:\n        0 -40px 64px 0 rgba(65, 78, 101, 0.1),\n        0 -24px 32px 0 rgba(65, 78, 101, 0.1),\n        0 -16px 16px 0 rgba(65, 78, 101, 0.1),\n        0 -8px 8px 0 rgba(65, 78, 101, 0.1);\n      top: auto;\n    }\n    .euiModal .euiModal__flex {\n      max-height: 100vh;\n    }\n    .euiModalHeader {\n      width: 100%;\n    }\n    .euiModalFooter {\n      width: 100%;\n      background: #f5f7fa;\n      padding: 12px 24px !important;\n      justify-content: stretch;\n    }\n    .euiModalFooter > * {\n      flex: 1;\n    }\n    .euiModalFooter > * + * {\n      margin-left: 0;\n    }\n    .euiModalBody {\n      width: 100%;\n    }\n    .euiModalBody .euiModalBody__overflow {\n      padding-bottom: 24px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiModal {\n      position: fixed;\n      width: 100vw !important;\n      max-width: none !important;\n      min-width: 0 !important;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      top: 0;\n      border-radius: 0;\n      border: none;\n    }\n    .euiModal.euiModal--confirmation {\n      box-shadow:\n        0 -40px 64px 0 rgba(65, 78, 101, 0.1),\n        0 -24px 32px 0 rgba(65, 78, 101, 0.1),\n        0 -16px 16px 0 rgba(65, 78, 101, 0.1),\n        0 -8px 8px 0 rgba(65, 78, 101, 0.1);\n      top: auto;\n    }\n    .euiModal .euiModal__flex {\n      max-height: 100vh;\n    }\n    .euiModalHeader {\n      width: 100%;\n    }\n    .euiModalFooter {\n      width: 100%;\n      background: #f5f7fa;\n      padding: 12px 24px !important;\n      justify-content: stretch;\n    }\n    .euiModalFooter > * {\n      flex: 1;\n    }\n    .euiModalFooter > * + * {\n      margin-left: 0;\n    }\n    .euiModalBody {\n      width: 100%;\n    }\n    .euiModalBody .euiModalBody__overflow {\n      padding-bottom: 24px;\n    }\n  }\n  .euiNotificationEvent {\n    display: flex;\n    padding: 12px 0 12px 12px;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiNotificationEvent:last-child {\n    border-bottom: none;\n  }\n  .euiNotificationEvent--withReadState {\n    padding: 12px 0 12px 8px;\n  }\n  .euiNotificationEvent__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    display: flex;\n  }\n  .euiNotificationEvent__title.euiLink {\n    color: #006bb4;\n  }\n  .euiNotificationEvent__title--isRead {\n    color: #69707d !important;\n  }\n  .euiNotificationEvent__readButton {\n    margin-right: 8px;\n  }\n  .euiNotificationEvent__content {\n    flex: 1;\n  }\n  .euiNotificationEvent__content > * + * {\n    margin-top: 8px;\n    margin-right: 12px;\n  }\n  .euiNotificationEventMeta {\n    position: relative;\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n    align-items: center;\n    flex-wrap: wrap;\n    margin-right: 4px;\n    min-height: 24px;\n  }\n  .euiNotificationEventMeta--hasContextMenu {\n    padding-right: 24px;\n  }\n  .euiNotificationEventMeta__contextMenuWrapper {\n    position: absolute;\n    top: 0;\n    right: 0;\n  }\n  .euiNotificationEventMeta__section {\n    margin-right: 8px;\n  }\n  .euiNotificationEventMeta__section:first-child {\n    display: flex;\n    flex: 1;\n    align-items: center;\n  }\n  .euiNotificationEventMeta__icon {\n    margin-right: 8px;\n  }\n  .euiNotificationEventMeta__badge {\n    max-width: 100%;\n    display: inline-grid;\n  }\n  .euiNotificationEventMeta__time {\n    font-size: 12px;\n    color: #6a717d;\n  }\n  .euiNotificationEventMessages {\n    font-size: 14px;\n  }\n  .euiNotificationEventMessages__accordion {\n    color: #69707d;\n  }\n  .euiNotificationEventMessages__accordionButton {\n    color: #006bb4;\n  }\n  .euiNotificationEventMessages__accordionContent > * {\n    padding-top: 8px;\n  }\n  .euiNotificationEventReadButton--isRead svg {\n    fill: rgba(0, 0, 0, 0);\n    stroke-width: 1px;\n    stroke: #d3dae6;\n  }\n  .euiNotificationEventReadIcon {\n    display: flex;\n    align-items: center;\n    height: 24px;\n    margin: 0 4px;\n  }\n  .euiNotificationEventReadIcon--isRead svg {\n    fill: rgba(0, 0, 0, 0);\n    stroke-width: 1px;\n    stroke: #d3dae6;\n  }\n  .euiOverlayMask {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding-bottom: 10vh;\n    animation: euiAnimFadeIn 150ms ease-in;\n    background: rgba(255, 255, 255, 0.8);\n  }\n  .euiBody-hasOverlayMask {\n    overflow: hidden;\n  }\n  .euiOverlayMask--aboveHeader {\n    z-index: 6000;\n  }\n  .euiOverlayMask--belowHeader {\n    z-index: 1000;\n  }\n  .euiPagination {\n    display: flex;\n    align-items: center;\n  }\n  .euiPagination__compressedText {\n    display: inline-flex;\n    align-items: center;\n    line-height: 1 !important;\n  }\n  .euiPagination__compressedText > *:first-child {\n    margin-right: 4px;\n  }\n  .euiPagination__compressedText > *:last-child {\n    margin-left: 4px;\n  }\n  .euiPagination__list {\n    display: flex;\n    align-items: baseline;\n  }\n  .euiPaginationButton {\n    font-size: 14px;\n    padding: 0;\n    text-align: center;\n    border-radius: 4px;\n  }\n  .euiPaginationButton-isActive {\n    font-weight: 700;\n  }\n  .euiPaginationButton-isActive.euiPaginationButton-isActive {\n    color: #006bb4;\n  }\n  .euiPaginationButton-isActive.euiPaginationButton-isActive\n    .euiButtonEmpty__content {\n    cursor: default;\n  }\n  .euiPaginationButton-isActive.euiPaginationButton-isActive,\n  .euiPaginationButton-isActive.euiPaginationButton-isActive:hover {\n    text-decoration: underline;\n  }\n  .euiPaginationButton-isPlaceholder {\n    align-items: baseline;\n    color: #afb0b3;\n    font-size: 14px;\n    padding: 0 8px;\n    height: 24px;\n    padding-top: 6px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPaginationButton-isPlaceholder,\n    .euiPaginationButton--hideOnMobile {\n      display: none;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPaginationButton-isPlaceholder,\n    .euiPaginationButton--hideOnMobile {\n      display: none;\n    }\n  }\n  .euiPanel {\n    background-color: #fff;\n    border-radius: 4px;\n    flex-grow: 1;\n  }\n  .euiPanel.euiPanel--flexGrowZero {\n    flex-grow: 0;\n  }\n  .euiPanel.euiPanel--noBorder {\n    border: none;\n  }\n  .euiPanel.euiPanel--hasShadow {\n    box-shadow:\n      0 2px 2px -1px rgba(152, 162, 179, 0.3),\n      0 1px 5px -2px rgba(152, 162, 179, 0.3);\n    border: 1px solid #d3dae6;\n  }\n  .euiPanel.euiPanel--isClickable {\n    transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiPanel.euiPanel--isClickable:enabled {\n    display: block;\n    width: 100%;\n    text-align: left;\n  }\n  .euiPanel.euiPanel--isClickable:hover,\n  .euiPanel.euiPanel--isClickable:focus {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 162, 179, 0.15),\n      0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    transform: translateY(-2px);\n    cursor: pointer;\n  }\n  .euiPanel.euiPanel--borderRadiusNone {\n    border-radius: 0;\n  }\n  .euiPanel.euiPanel--borderRadiusMedium {\n    border-radius: 4px;\n  }\n  .euiPanel.euiPanel--transparent {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiPanel.euiPanel--plain {\n    background-color: #fff;\n  }\n  .euiPanel.euiPanel--subdued {\n    background-color: #fafbfd;\n  }\n  .euiPanel.euiPanel--accent {\n    background-color: #fce7f1;\n  }\n  .euiPanel.euiPanel--primary {\n    background-color: #e6f0f8;\n  }\n  .euiPanel.euiPanel--success {\n    background-color: #e6f2f1;\n  }\n  .euiPanel.euiPanel--warning {\n    background-color: #fef6e6;\n  }\n  .euiPanel.euiPanel--danger {\n    background-color: #f8e9e9;\n  }\n  .euiPanel--paddingSmall {\n    padding: 8px;\n  }\n  .euiPanel--paddingMedium {\n    padding: 16px;\n  }\n  .euiPanel--paddingLarge {\n    padding: 24px;\n  }\n  .euiSplitPanel {\n    display: flex;\n    flex-direction: column;\n    min-width: 0;\n  }\n  .euiSplitPanel .euiSplitPanel__inner {\n    flex-basis: 0%;\n    transform: none !important;\n    box-shadow: none !important;\n  }\n  .euiSplitPanel.euiSplitPanel-isResponsive.euiPanel--borderRadiusNone\n    .euiSplitPanel__inner:first-child,\n  .euiSplitPanel.euiPanel--borderRadiusNone .euiSplitPanel__inner:first-child {\n    border-radius: -1 -1 0 0;\n  }\n  .euiSplitPanel.euiSplitPanel-isResponsive.euiPanel--borderRadiusNone\n    .euiSplitPanel__inner:last-child,\n  .euiSplitPanel.euiPanel--borderRadiusNone .euiSplitPanel__inner:last-child {\n    border-radius: 0 0 -1 -1;\n  }\n  .euiSplitPanel.euiSplitPanel-isResponsive.euiPanel--borderRadiusMedium\n    .euiSplitPanel__inner:first-child,\n  .euiSplitPanel.euiPanel--borderRadiusMedium\n    .euiSplitPanel__inner:first-child {\n    border-radius: 3px 3px 0 0;\n  }\n  .euiSplitPanel.euiSplitPanel-isResponsive.euiPanel--borderRadiusMedium\n    .euiSplitPanel__inner:last-child,\n  .euiSplitPanel.euiPanel--borderRadiusMedium .euiSplitPanel__inner:last-child {\n    border-radius: 0 0 3px 3px;\n  }\n  .euiSplitPanel--row {\n    flex-direction: row;\n  }\n  .euiSplitPanel--row.euiSplitPanel-isResponsive {\n    flex-direction: column;\n  }\n  .euiSplitPanel--row.euiPanel--borderRadiusNone\n    .euiSplitPanel__inner:first-child {\n    border-radius: -1 0 0 -1;\n  }\n  .euiSplitPanel--row.euiPanel--borderRadiusNone\n    .euiSplitPanel__inner:last-child {\n    border-radius: 0 -1 -1 0;\n  }\n  .euiSplitPanel--row.euiPanel--borderRadiusMedium\n    .euiSplitPanel__inner:first-child {\n    border-radius: 3px 0 0 3px;\n  }\n  .euiSplitPanel--row.euiPanel--borderRadiusMedium\n    .euiSplitPanel__inner:last-child {\n    border-radius: 0 3px 3px 0;\n  }\n  .euiPage {\n    display: flex;\n    background-color: #fafbfd;\n    flex-shrink: 0;\n    max-width: 100%;\n  }\n  .euiPage--restrictWidth-default,\n  .euiPage--restrictWidth-custom {\n    margin-left: auto;\n    margin-right: auto;\n    width: 100%;\n  }\n  .euiPage--restrictWidth-default {\n    max-width: 1000px;\n  }\n  .euiPage--grow {\n    flex-grow: 1;\n  }\n  .euiPage--column {\n    flex-direction: column;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPage {\n      flex-direction: column;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPage {\n      flex-direction: column;\n    }\n  }\n  .euiPage--paddingSmall {\n    padding: 8px;\n  }\n  .euiPage--paddingSmall .euiPageSideBar {\n    min-width: 192px;\n    margin-right: 8px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPage--paddingSmall .euiPageSideBar {\n      margin-right: 0;\n      margin-bottom: 8px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPage--paddingSmall .euiPageSideBar {\n      margin-right: 0;\n      margin-bottom: 8px;\n    }\n  }\n  .euiPage--paddingSmall .euiPageBody > .euiPageHeader {\n    margin-bottom: 8px;\n  }\n  .euiPage--paddingMedium {\n    padding: 16px;\n  }\n  .euiPage--paddingMedium .euiPageSideBar {\n    min-width: 192px;\n    margin-right: 16px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPage--paddingMedium .euiPageSideBar {\n      margin-right: 0;\n      margin-bottom: 16px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPage--paddingMedium .euiPageSideBar {\n      margin-right: 0;\n      margin-bottom: 16px;\n    }\n  }\n  .euiPage--paddingMedium .euiPageBody > .euiPageHeader {\n    margin-bottom: 16px;\n  }\n  .euiPage--paddingLarge {\n    padding: 24px;\n  }\n  .euiPage--paddingLarge .euiPageSideBar {\n    min-width: 192px;\n    margin-right: 24px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPage--paddingLarge .euiPageSideBar {\n      margin-right: 0;\n      margin-bottom: 24px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPage--paddingLarge .euiPageSideBar {\n      margin-right: 0;\n      margin-bottom: 24px;\n    }\n  }\n  .euiPage--paddingLarge .euiPageBody > .euiPageHeader {\n    margin-bottom: 24px;\n  }\n  .euiPageBody {\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n    flex: 1 1 100%;\n    max-width: 100%;\n    min-width: 0;\n  }\n  .euiPageBody--restrictWidth-default,\n  .euiPageBody--restrictWidth-custom {\n    margin-left: auto;\n    margin-right: auto;\n    width: 100%;\n  }\n  .euiPageBody--restrictWidth-default {\n    max-width: 1000px;\n  }\n  .euiPageBody.euiPageBody--borderRadiusNone {\n    border-top-width: 0;\n    border-right-width: 0;\n    border-bottom-width: 0;\n  }\n  .euiPageBody--paddingSmall {\n    padding: 8px;\n  }\n  .euiPageBody--paddingSmall > .euiPageHeader:not([class*='--padding']) {\n    margin-bottom: 8px;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiPageBody--paddingSmall\n    > .euiPageHeader:not([class*='--padding']):not(\n      .euiPageHeader--tabsAtBottom\n    ) {\n    padding-bottom: 8px;\n  }\n  .euiPageBody--paddingMedium {\n    padding: 16px;\n  }\n  .euiPageBody--paddingMedium > .euiPageHeader:not([class*='--padding']) {\n    margin-bottom: 16px;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiPageBody--paddingMedium\n    > .euiPageHeader:not([class*='--padding']):not(\n      .euiPageHeader--tabsAtBottom\n    ) {\n    padding-bottom: 16px;\n  }\n  .euiPageBody--paddingLarge {\n    padding: 24px;\n  }\n  .euiPageBody--paddingLarge > .euiPageHeader:not([class*='--padding']) {\n    margin-bottom: 24px;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiPageBody--paddingLarge\n    > .euiPageHeader:not([class*='--padding']):not(\n      .euiPageHeader--tabsAtBottom\n    ) {\n    padding-bottom: 24px;\n  }\n  .euiPageContent {\n    width: 100%;\n    min-width: 0;\n  }\n  .euiPageContent.euiPageContent--borderRadiusNone {\n    border-left-width: 0;\n    border-right-width: 0;\n    border-bottom-width: 0;\n  }\n  .euiPageContent.euiPageContent--verticalCenter {\n    align-self: center;\n    margin-top: auto;\n    margin-bottom: auto;\n    flex-grow: 0;\n  }\n  .euiPageContent.euiPageContent--horizontalCenter {\n    width: auto;\n    max-width: 100%;\n    margin-left: auto;\n    margin-right: auto;\n    flex-grow: 0;\n  }\n  .euiPageContentBody--restrictWidth-default,\n  .euiPageContentBody--restrictWidth-custom {\n    margin-left: auto;\n    margin-right: auto;\n    width: 100%;\n  }\n  .euiPageContentBody--restrictWidth-default {\n    max-width: 1000px;\n  }\n  .euiPageContentBody--paddingSmall {\n    padding: 8px;\n  }\n  .euiPageContentBody--paddingMedium {\n    padding: 16px;\n  }\n  .euiPageContentBody--paddingLarge {\n    padding: 24px;\n  }\n  .euiPageContentHeader {\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n    align-items: center;\n  }\n  .euiPageContent[class*='paddingSmall'] .euiPageContentHeader {\n    margin-bottom: 8px;\n  }\n  .euiPageContent[class*='paddingMedium'] .euiPageContentHeader {\n    margin-bottom: 16px;\n  }\n  .euiPageContent[class*='paddingLarge'] .euiPageContentHeader {\n    margin-bottom: 24px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPageContentHeader--responsive {\n      flex-direction: column;\n      align-items: flex-start;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPageContentHeader--responsive {\n      flex-direction: column;\n      align-items: flex-start;\n    }\n  }\n  .euiPageContentHeaderSection + .euiPageContentHeaderSection {\n    margin-left: 32px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPageContent[class*='paddingSmall']\n      .euiPageContentHeader--responsive\n      .euiPageContentHeaderSection\n      + .euiPageContentHeaderSection {\n      margin-left: 0;\n      margin-top: 4px;\n    }\n    .euiPageContent[class*='paddingMedium']\n      .euiPageContentHeader--responsive\n      .euiPageContentHeaderSection\n      + .euiPageContentHeaderSection {\n      margin-left: 0;\n      margin-top: 8px;\n    }\n    .euiPageContent[class*='paddingLarge']\n      .euiPageContentHeader--responsive\n      .euiPageContentHeaderSection\n      + .euiPageContentHeaderSection {\n      margin-left: 0;\n      margin-top: 12px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPageContent[class*='paddingSmall']\n      .euiPageContentHeader--responsive\n      .euiPageContentHeaderSection\n      + .euiPageContentHeaderSection {\n      margin-left: 0;\n      margin-top: 4px;\n    }\n    .euiPageContent[class*='paddingMedium']\n      .euiPageContentHeader--responsive\n      .euiPageContentHeaderSection\n      + .euiPageContentHeaderSection {\n      margin-left: 0;\n      margin-top: 8px;\n    }\n    .euiPageContent[class*='paddingLarge']\n      .euiPageContentHeader--responsive\n      .euiPageContentHeaderSection\n      + .euiPageContentHeaderSection {\n      margin-left: 0;\n      margin-top: 12px;\n    }\n  }\n  .euiPageSideBar {\n    min-width: 240px;\n    flex: 0 1 0%;\n  }\n  .euiPageSideBar--paddingSmall {\n    padding: 8px;\n  }\n  .euiPageSideBar--paddingMedium {\n    padding: 16px;\n  }\n  .euiPageSideBar--paddingLarge {\n    padding: 24px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPageSideBar {\n      width: 100%;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPageSideBar {\n      width: 100%;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiPageSideBar--sticky {\n      scrollbar-width: thin;\n      overflow-y: auto;\n      flex-grow: 1;\n      position: sticky;\n      max-height: 100vh;\n      top: 0;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar {\n      width: 16px;\n      height: 16px;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar-thumb {\n      background-color: rgba(105, 112, 125, 0.5);\n      border: 6px solid rgba(0, 0, 0, 0);\n      background-clip: content-box;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar-corner,\n    .euiPageSideBar--sticky::-webkit-scrollbar-track {\n      background-color: rgba(0, 0, 0, 0);\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .euiPageSideBar--sticky {\n      scrollbar-width: thin;\n      overflow-y: auto;\n      flex-grow: 1;\n      position: sticky;\n      max-height: 100vh;\n      top: 0;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar {\n      width: 16px;\n      height: 16px;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar-thumb {\n      background-color: rgba(105, 112, 125, 0.5);\n      border: 6px solid rgba(0, 0, 0, 0);\n      background-clip: content-box;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar-corner,\n    .euiPageSideBar--sticky::-webkit-scrollbar-track {\n      background-color: rgba(0, 0, 0, 0);\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .euiPageSideBar--sticky {\n      scrollbar-width: thin;\n      overflow-y: auto;\n      flex-grow: 1;\n      position: sticky;\n      max-height: 100vh;\n      top: 0;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar {\n      width: 16px;\n      height: 16px;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar-thumb {\n      background-color: rgba(105, 112, 125, 0.5);\n      border: 6px solid rgba(0, 0, 0, 0);\n      background-clip: content-box;\n    }\n    .euiPageSideBar--sticky::-webkit-scrollbar-corner,\n    .euiPageSideBar--sticky::-webkit-scrollbar-track {\n      background-color: rgba(0, 0, 0, 0);\n    }\n  }\n  .euiPageHeader {\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    justify-content: space-between;\n    align-items: center;\n    flex-shrink: 0;\n  }\n  .euiPageHeader--restrictWidth-default,\n  .euiPageHeader--restrictWidth-custom {\n    margin-left: auto;\n    margin-right: auto;\n    width: 100%;\n  }\n  .euiPageHeader--restrictWidth-default {\n    max-width: 1000px;\n  }\n  .euiPageHeader--bottomBorder {\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiPageHeader--bottomBorder:not(.euiPageHeader--tabsAtBottom) {\n    padding-bottom: 24px;\n  }\n  .euiPageHeader--paddingSmall {\n    padding: 8px;\n  }\n  .euiPageHeader--paddingSmall.euiPageHeader--tabsAtBottom {\n    padding-bottom: 0;\n  }\n  .euiPageHeader--paddingSmall.euiPageHeader--tabsAtBottom.euiPageHeader--bottomBorder {\n    margin-bottom: 8px;\n  }\n  .euiPageHeader--paddingMedium {\n    padding: 16px;\n  }\n  .euiPageHeader--paddingMedium.euiPageHeader--tabsAtBottom {\n    padding-bottom: 0;\n  }\n  .euiPageHeader--paddingMedium.euiPageHeader--tabsAtBottom.euiPageHeader--bottomBorder {\n    margin-bottom: 16px;\n  }\n  .euiPageHeader--paddingLarge {\n    padding: 24px;\n  }\n  .euiPageHeader--paddingLarge.euiPageHeader--tabsAtBottom {\n    padding-bottom: 0;\n  }\n  .euiPageHeader--paddingLarge.euiPageHeader--tabsAtBottom.euiPageHeader--bottomBorder {\n    margin-bottom: 24px;\n  }\n  .euiPageHeader--top {\n    align-items: flex-start;\n  }\n  .euiPageHeader--bottom {\n    align-items: flex-end;\n  }\n  .euiPageHeader--stretch {\n    align-items: stretch;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPageHeader--responsive {\n      flex-direction: column;\n    }\n    .euiPageHeader--responsiveReverse {\n      flex-direction: column-reverse;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPageHeader--responsive {\n      flex-direction: column;\n    }\n    .euiPageHeader--responsiveReverse {\n      flex-direction: column-reverse;\n    }\n  }\n  .euiPageHeader .euiPageHeaderContent {\n    width: 100%;\n  }\n  .euiPageHeaderContent__titleIcon {\n    top: -4px;\n    position: relative;\n    margin-right: 16px;\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiPageHeaderContent__rightSideItems {\n      flex-direction: row-reverse;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .euiPageHeaderContent__rightSideItems {\n      flex-direction: row-reverse;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .euiPageHeaderContent__rightSideItems {\n      flex-direction: row-reverse;\n    }\n  }\n  .euiPageHeaderSection:not(:first-of-type) {\n    margin-left: 32px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiPageHeader--responsive .euiPageHeaderSection {\n      width: 100%;\n    }\n    .euiPageHeader--responsive .euiPageHeaderSection:not(:first-of-type) {\n      margin-left: 0;\n      margin-top: 16px;\n    }\n    .euiPageHeader--responsiveReverse .euiPageHeaderSection {\n      width: 100%;\n    }\n    .euiPageHeader--responsiveReverse\n      .euiPageHeaderSection:not(:first-of-type) {\n      margin-left: 0;\n    }\n    .euiPageHeader--responsiveReverse .euiPageHeaderSection:not(:last-of-type) {\n      margin-top: 16px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiPageHeader--responsive .euiPageHeaderSection {\n      width: 100%;\n    }\n    .euiPageHeader--responsive .euiPageHeaderSection:not(:first-of-type) {\n      margin-left: 0;\n      margin-top: 16px;\n    }\n    .euiPageHeader--responsiveReverse .euiPageHeaderSection {\n      width: 100%;\n    }\n    .euiPageHeader--responsiveReverse\n      .euiPageHeaderSection:not(:first-of-type) {\n      margin-left: 0;\n    }\n    .euiPageHeader--responsiveReverse .euiPageHeaderSection:not(:last-of-type) {\n      margin-top: 16px;\n    }\n  }\n  .euiPopover {\n    display: inline-block;\n    position: relative;\n    vertical-align: middle;\n    max-width: 100%;\n  }\n  .euiPopover__anchor {\n    display: inline-block;\n  }\n  .euiPopover--displayBlock {\n    display: block;\n  }\n  .euiPopover--displayBlock .euiPopover__anchor {\n    display: block;\n  }\n  .euiPopover__panel {\n    box-shadow:\n      0 12px 24px 0 rgba(65, 78, 101, 0.1),\n      0 6px 12px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n    border-color: #d0d3d9;\n    border-top-color: #d9dce0;\n    border-bottom-color: #aaafba;\n    position: absolute;\n    min-width: 112px;\n    max-width: calc(100vw - 32px);\n    backface-visibility: hidden;\n    pointer-events: none;\n    opacity: 0;\n    visibility: hidden;\n    transition:\n      opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms,\n      visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms;\n  }\n  .euiPopover__panel:not(.euiPopover__panel-isAttached) {\n    transform: translateY(0) translateX(0) translateZ(0);\n    transition:\n      opacity cubic-bezier(0.34, 1.61, 0.7, 1) 350ms,\n      visibility cubic-bezier(0.34, 1.61, 0.7, 1) 350ms,\n      transform cubic-bezier(0.34, 1.61, 0.7, 1) 450ms;\n  }\n  .euiPopover__panel.euiPopover__panel-isOpen {\n    opacity: 1;\n    visibility: visible;\n    pointer-events: auto;\n  }\n  .euiPopover__panel .euiPopover__panelArrow {\n    position: absolute;\n    width: 0;\n    height: 0;\n  }\n  .euiPopover__panel .euiPopover__panelArrow:before {\n    position: absolute;\n    content: '';\n    height: 0;\n    width: 0;\n  }\n  .euiPopover__panel .euiPopover__panelArrow:after {\n    position: absolute;\n    content: '';\n    height: 0;\n    width: 0;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--top:before {\n    bottom: -10px;\n    border-left: 12px solid rgba(0, 0, 0, 0);\n    border-right: 12px solid rgba(0, 0, 0, 0);\n    border-top: 12px solid #d3dae6;\n  }\n  .euiPopover__panel .euiPopover__panelArrow.euiPopover__panelArrow--top:after {\n    bottom: -9px;\n    border-left: 12px solid rgba(0, 0, 0, 0);\n    border-right: 12px solid rgba(0, 0, 0, 0);\n    border-top: 12px solid #fff;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--right:before {\n    left: -12px;\n    top: 50%;\n    border-top: 12px solid rgba(0, 0, 0, 0);\n    border-bottom: 12px solid rgba(0, 0, 0, 0);\n    border-right: 12px solid #d3dae6;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--right:after {\n    left: -11px;\n    top: 50%;\n    border-top: 12px solid rgba(0, 0, 0, 0);\n    border-bottom: 12px solid rgba(0, 0, 0, 0);\n    border-right: 12px solid #fff;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--bottom:before {\n    top: -12px;\n    border-left: 12px solid rgba(0, 0, 0, 0);\n    border-right: 12px solid rgba(0, 0, 0, 0);\n    border-bottom: 12px solid #d3dae6;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--bottom:after {\n    top: -11px;\n    border-left: 12px solid rgba(0, 0, 0, 0);\n    border-right: 12px solid rgba(0, 0, 0, 0);\n    border-bottom: 12px solid #fff;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--left:before {\n    right: -11px;\n    top: 50%;\n    border-top: 12px solid rgba(0, 0, 0, 0);\n    border-bottom: 12px solid rgba(0, 0, 0, 0);\n    border-left: 12px solid #d3dae6;\n  }\n  .euiPopover__panel\n    .euiPopover__panelArrow.euiPopover__panelArrow--left:after {\n    right: -10px;\n    top: 50%;\n    border-top: 12px solid rgba(0, 0, 0, 0);\n    border-bottom: 12px solid rgba(0, 0, 0, 0);\n    border-left: 12px solid #fff;\n  }\n  .euiPopover__panel.euiPopover__panel-noArrow .euiPopover__panelArrow {\n    display: none;\n  }\n  .euiPopover__panel.euiPopover__panel-isAttached.euiPopover__panel--bottom {\n    border-top-color: rgba(211, 218, 230, 0.8);\n    border-top-right-radius: 0;\n    border-top-left-radius: 0;\n  }\n  .euiPopover__panel.euiPopover__panel-isAttached.euiPopover__panel--top {\n    box-shadow:\n      0 0 12px -1px rgba(152, 162, 179, 0.2),\n      0 0 4px -1px rgba(152, 162, 179, 0.2),\n      0 0 2px 0 rgba(152, 162, 179, 0.2);\n    border-bottom-color: rgba(211, 218, 230, 0.8);\n    border-bottom-right-radius: 0;\n    border-bottom-left-radius: 0;\n  }\n  .euiPopover__panel.euiPopover__panel-isAttached.euiPopover__panel--top,\n  .euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--top {\n    transform: translateY(8px) translateZ(0);\n  }\n  .euiPopover__panel.euiPopover__panel-isAttached.euiPopover__panel--bottom,\n  .euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--bottom {\n    transform: translateY(-8px) translateZ(0);\n  }\n  .euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--left {\n    transform: translateX(8px) translateZ(0);\n  }\n  .euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--right {\n    transform: translateX(-8px) translateZ(0);\n  }\n  .euiPopoverTitle {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    padding: 12px;\n    text-transform: uppercase;\n    border-bottom: 1px solid #d3dae6;\n  }\n  .euiPopoverTitle--paddingNone {\n    padding: 0;\n  }\n  .euiPopoverTitle--paddingSmall {\n    padding: 8px;\n  }\n  .euiPopoverTitle--paddingMedium {\n    padding: 16px;\n  }\n  .euiPopoverTitle--paddingLarge {\n    padding: 24px;\n  }\n  .euiPopover__panel.euiPanel--paddingSmall .euiPopoverTitle {\n    margin: -8px -8px 8px;\n  }\n  .euiPopover__panel.euiPanel--paddingSmall\n    .euiPopoverTitle:not([class*='euiPopoverTitle--padding']) {\n    padding: 12px 8px;\n  }\n  .euiPopover__panel.euiPanel--paddingMedium .euiPopoverTitle {\n    margin: -16px -16px 16px;\n  }\n  .euiPopover__panel.euiPanel--paddingMedium\n    .euiPopoverTitle:not([class*='euiPopoverTitle--padding']) {\n    padding: 12px 16px;\n  }\n  .euiPopover__panel.euiPanel--paddingLarge .euiPopoverTitle {\n    margin: -24px -24px 24px;\n  }\n  .euiPopover__panel.euiPanel--paddingLarge\n    .euiPopoverTitle:not([class*='euiPopoverTitle--padding']) {\n    padding: 12px 24px;\n  }\n  .euiPopoverFooter {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    padding: 12px;\n    border-top: 1px solid #d3dae6;\n  }\n  .euiPopoverFooter--paddingNone {\n    padding: 0;\n  }\n  .euiPopoverFooter--paddingSmall {\n    padding: 8px;\n  }\n  .euiPopoverFooter--paddingMedium {\n    padding: 16px;\n  }\n  .euiPopoverFooter--paddingLarge {\n    padding: 24px;\n  }\n  .euiPopover__panel.euiPanel--paddingSmall .euiPopoverFooter {\n    margin: 8px -8px -8px;\n  }\n  .euiPopover__panel.euiPanel--paddingSmall\n    .euiPopoverFooter:not([class*='euiPopoverFooter--padding']) {\n    padding: 12px 8px;\n  }\n  .euiPopover__panel.euiPanel--paddingMedium .euiPopoverFooter {\n    margin: 16px -16px -16px;\n  }\n  .euiPopover__panel.euiPanel--paddingMedium\n    .euiPopoverFooter:not([class*='euiPopoverFooter--padding']) {\n    padding: 12px 16px;\n  }\n  .euiPopover__panel.euiPanel--paddingLarge .euiPopoverFooter {\n    margin: 24px -24px -24px;\n  }\n  .euiPopover__panel.euiPanel--paddingLarge\n    .euiPopoverFooter:not([class*='euiPopoverFooter--padding']) {\n    padding: 12px 24px;\n  }\n  .euiInputPopover {\n    max-width: 400px;\n  }\n  .euiInputPopover.euiInputPopover--fullWidth {\n    max-width: 100%;\n  }\n  .euiBody-hasPortalContent {\n    position: relative;\n  }\n  .euiProgress {\n    position: relative;\n    overflow: hidden;\n    background-color: #d3dae6;\n  }\n  .euiProgress--xs {\n    height: 2px;\n  }\n  .euiProgress--s {\n    height: 4px;\n  }\n  .euiProgress--m {\n    height: 8px;\n  }\n  .euiProgress--l {\n    height: 16px;\n  }\n  .euiProgress--native {\n    display: block;\n    width: 100%;\n    appearance: none;\n    border: none;\n  }\n  .euiProgress--native::-webkit-progress-bar {\n    background-color: #d3dae6;\n  }\n  .euiProgress--native::-webkit-progress-value {\n    transition: width 250ms linear;\n  }\n  .euiProgress--native::-moz-progress-bar {\n    transition: width 250ms linear;\n  }\n  .euiProgress--indeterminate:before {\n    position: absolute;\n    content: '';\n    width: 100%;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    transform: scaleX(0) translateX(0%);\n    animation: euiProgress 1s cubic-bezier(0.694, 0.0482, 0.335, 1) infinite;\n  }\n  .euiProgress--fixed {\n    position: fixed;\n    z-index: 1001;\n  }\n  .euiProgress--absolute {\n    position: absolute;\n  }\n  .euiProgress--fixed,\n  .euiProgress--absolute {\n    top: 0;\n    left: 0;\n    right: 0;\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiProgress--fixed.euiProgress--native::-webkit-progress-bar,\n  .euiProgress--absolute.euiProgress--native::-webkit-progress-bar {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiProgress--fixed.euiProgress--native::-moz-progress-bar,\n  .euiProgress--absolute.euiProgress--native::-moz-progress-bar {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiProgress--primary.euiProgress--native::-webkit-progress-value {\n    background-color: #006bb4;\n  }\n  .euiProgress--primary.euiProgress--native::-moz-progress-bar {\n    background-color: #006bb4;\n  }\n  .euiProgress--primary.euiProgress--indeterminate:before {\n    background-color: #006bb4;\n  }\n  .euiProgress__data--primary .euiProgress__valueText {\n    color: #006bb4;\n  }\n  .euiProgress--secondary.euiProgress--native::-webkit-progress-value {\n    background-color: #017d73;\n  }\n  .euiProgress--secondary.euiProgress--native::-moz-progress-bar {\n    background-color: #017d73;\n  }\n  .euiProgress--secondary.euiProgress--indeterminate:before {\n    background-color: #017d73;\n  }\n  .euiProgress__data--secondary .euiProgress__valueText {\n    color: #017d73;\n  }\n  .euiProgress--success.euiProgress--native::-webkit-progress-value {\n    background-color: #017d73;\n  }\n  .euiProgress--success.euiProgress--native::-moz-progress-bar {\n    background-color: #017d73;\n  }\n  .euiProgress--success.euiProgress--indeterminate:before {\n    background-color: #017d73;\n  }\n  .euiProgress__data--success .euiProgress__valueText {\n    color: #017d73;\n  }\n  .euiProgress--warning.euiProgress--native::-webkit-progress-value {\n    background-color: #f5a700;\n  }\n  .euiProgress--warning.euiProgress--native::-moz-progress-bar {\n    background-color: #f5a700;\n  }\n  .euiProgress--warning.euiProgress--indeterminate:before {\n    background-color: #f5a700;\n  }\n  .euiProgress__data--warning .euiProgress__valueText {\n    color: #9b6900;\n  }\n  .euiProgress--danger.euiProgress--native::-webkit-progress-value {\n    background-color: #bd271e;\n  }\n  .euiProgress--danger.euiProgress--native::-moz-progress-bar {\n    background-color: #bd271e;\n  }\n  .euiProgress--danger.euiProgress--indeterminate:before {\n    background-color: #bd271e;\n  }\n  .euiProgress__data--danger .euiProgress__valueText {\n    color: #bd271e;\n  }\n  .euiProgress--accent.euiProgress--native::-webkit-progress-value {\n    background-color: #dd0a73;\n  }\n  .euiProgress--accent.euiProgress--native::-moz-progress-bar {\n    background-color: #dd0a73;\n  }\n  .euiProgress--accent.euiProgress--indeterminate:before {\n    background-color: #dd0a73;\n  }\n  .euiProgress__data--accent .euiProgress__valueText {\n    color: #dd0a73;\n  }\n  .euiProgress--subdued.euiProgress--native::-webkit-progress-value {\n    background-color: #6a717d;\n  }\n  .euiProgress--subdued.euiProgress--native::-moz-progress-bar {\n    background-color: #6a717d;\n  }\n  .euiProgress--subdued.euiProgress--indeterminate:before {\n    background-color: #6a717d;\n  }\n  .euiProgress__data--subdued .euiProgress__valueText {\n    color: #6a717d;\n  }\n  .euiProgress--vis0.euiProgress--native::-webkit-progress-value {\n    background-color: #54b399;\n  }\n  .euiProgress--vis0.euiProgress--native::-moz-progress-bar {\n    background-color: #54b399;\n  }\n  .euiProgress--vis0.euiProgress--indeterminate:before {\n    background-color: #54b399;\n  }\n  .euiProgress__data--vis0 .euiProgress__valueText {\n    color: #3b7d6a;\n  }\n  .euiProgress--vis1.euiProgress--native::-webkit-progress-value {\n    background-color: #6092c0;\n  }\n  .euiProgress--vis1.euiProgress--native::-moz-progress-bar {\n    background-color: #6092c0;\n  }\n  .euiProgress--vis1.euiProgress--indeterminate:before {\n    background-color: #6092c0;\n  }\n  .euiProgress__data--vis1 .euiProgress__valueText {\n    color: #4e779c;\n  }\n  .euiProgress--vis2.euiProgress--native::-webkit-progress-value {\n    background-color: #d36086;\n  }\n  .euiProgress--vis2.euiProgress--native::-moz-progress-bar {\n    background-color: #d36086;\n  }\n  .euiProgress--vis2.euiProgress--indeterminate:before {\n    background-color: #d36086;\n  }\n  .euiProgress__data--vis2 .euiProgress__valueText {\n    color: #b55273;\n  }\n  .euiProgress--vis3.euiProgress--native::-webkit-progress-value {\n    background-color: #9170b8;\n  }\n  .euiProgress--vis3.euiProgress--native::-moz-progress-bar {\n    background-color: #9170b8;\n  }\n  .euiProgress--vis3.euiProgress--indeterminate:before {\n    background-color: #9170b8;\n  }\n  .euiProgress__data--vis3 .euiProgress__valueText {\n    color: #8365a6;\n  }\n  .euiProgress--vis4.euiProgress--native::-webkit-progress-value {\n    background-color: #ca8eae;\n  }\n  .euiProgress--vis4.euiProgress--native::-moz-progress-bar {\n    background-color: #ca8eae;\n  }\n  .euiProgress--vis4.euiProgress--indeterminate:before {\n    background-color: #ca8eae;\n  }\n  .euiProgress__data--vis4 .euiProgress__valueText {\n    color: #8d647a;\n  }\n  .euiProgress--vis5.euiProgress--native::-webkit-progress-value {\n    background-color: #d6bf57;\n  }\n  .euiProgress--vis5.euiProgress--native::-moz-progress-bar {\n    background-color: #d6bf57;\n  }\n  .euiProgress--vis5.euiProgress--indeterminate:before {\n    background-color: #d6bf57;\n  }\n  .euiProgress__data--vis5 .euiProgress__valueText {\n    color: #807234;\n  }\n  .euiProgress--vis6.euiProgress--native::-webkit-progress-value {\n    background-color: #b9a888;\n  }\n  .euiProgress--vis6.euiProgress--native::-moz-progress-bar {\n    background-color: #b9a888;\n  }\n  .euiProgress--vis6.euiProgress--indeterminate:before {\n    background-color: #b9a888;\n  }\n  .euiProgress__data--vis6 .euiProgress__valueText {\n    color: #7b705a;\n  }\n  .euiProgress--vis7.euiProgress--native::-webkit-progress-value {\n    background-color: #da8b45;\n  }\n  .euiProgress--vis7.euiProgress--native::-moz-progress-bar {\n    background-color: #da8b45;\n  }\n  .euiProgress--vis7.euiProgress--indeterminate:before {\n    background-color: #da8b45;\n  }\n  .euiProgress__data--vis7 .euiProgress__valueText {\n    color: #a16633;\n  }\n  .euiProgress--vis8.euiProgress--native::-webkit-progress-value {\n    background-color: #aa6556;\n  }\n  .euiProgress--vis8.euiProgress--native::-moz-progress-bar {\n    background-color: #aa6556;\n  }\n  .euiProgress--vis8.euiProgress--indeterminate:before {\n    background-color: #aa6556;\n  }\n  .euiProgress__data--vis8 .euiProgress__valueText {\n    color: #a26052;\n  }\n  .euiProgress--vis9.euiProgress--native::-webkit-progress-value {\n    background-color: #e7664c;\n  }\n  .euiProgress--vis9.euiProgress--native::-moz-progress-bar {\n    background-color: #e7664c;\n  }\n  .euiProgress--vis9.euiProgress--indeterminate:before {\n    background-color: #e7664c;\n  }\n  .euiProgress__data--vis9 .euiProgress__valueText {\n    color: #bc533e;\n  }\n  .euiProgress--customColor.euiProgress--native::-webkit-progress-value {\n    background-color: currentColor;\n  }\n  .euiProgress--customColor.euiProgress--native::-moz-progress-bar {\n    background-color: currentColor;\n  }\n  .euiProgress--customColor.euiProgress--indeterminate:before {\n    background-color: currentColor;\n  }\n  @keyframes euiProgress {\n    0% {\n      transform: scaleX(1) translateX(-100%);\n    }\n    100% {\n      transform: scaleX(1) translateX(100%);\n    }\n  }\n  .euiProgress__data {\n    display: flex;\n    justify-content: space-between;\n  }\n  .euiProgress__label,\n  .euiProgress__valueText {\n    color: #343741;\n    font-weight: 400;\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n  }\n  .euiProgress__label {\n    flex-grow: 1;\n  }\n  .euiProgress__label + .euiProgress__valueText {\n    padding-left: 4px;\n    flex-grow: 1;\n    text-align: right;\n    flex-shrink: 0;\n  }\n  .euiProgress__valueText {\n    font-feature-settings: 'tnum' 1;\n    margin-left: auto;\n  }\n  .euiProgress__data--l .euiProgress__label,\n  .euiProgress__data--l .euiProgress__valueText {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n  }\n  .euiTreeView__wrapper .euiTreeView {\n    margin: 0;\n    list-style-type: none;\n  }\n  .euiTreeView .euiTreeView {\n    padding-left: 24px;\n  }\n  .euiTreeView__node {\n    max-height: 32px;\n    line-height: 32px;\n  }\n  .euiTreeView__node--expanded {\n    max-height: 100vh;\n  }\n  .euiTreeView__nodeInner {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    padding-left: 8px;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    height: 32px;\n    border-radius: 4px;\n    width: 100%;\n    text-align-last: left;\n  }\n  .euiTreeView__nodeInner:focus {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiTreeView__nodeInner:hover,\n  .euiTreeView__nodeInner:active,\n  .euiTreeView__nodeInner:focus {\n    background-color: #e6f0f8;\n  }\n  .euiTreeView__nodeInner .euiTreeView__iconPlaceholder {\n    width: 32px;\n  }\n  .euiTreeView__nodeLabel {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n  }\n  .euiTreeView__iconWrapper {\n    margin-top: -2px;\n    margin-right: 8px;\n  }\n  .euiTreeView__iconWrapper .euiToken {\n    margin-top: 2px;\n  }\n  .euiTreeView--compressed .euiTreeView__node {\n    max-height: 24px;\n    line-height: 24px;\n  }\n  .euiTreeView--compressed .euiTreeView__node .euiTreeView__nodeInner {\n    height: 24px;\n  }\n  .euiTreeView--compressed .euiTreeView__node .euiTreeView__iconWrapper {\n    margin: 0 6px 0 0;\n  }\n  .euiTreeView--compressed .euiTreeView__node .euiTreeView__nodeLabel {\n    margin-top: -1px;\n  }\n  .euiTreeView--compressed .euiTreeView__node .euiTreeView__iconPlaceholder {\n    width: 24px;\n  }\n  .euiTreeView--compressed .euiTreeView__node--expanded {\n    max-height: 100vh;\n  }\n  .euiTreeView--withArrows .euiTreeView__expansionArrow {\n    margin-right: 4px;\n  }\n  .euiTreeView--withArrows.euiTreeView\n    .euiTreeView__nodeInner--withArrows\n    .euiTreeView__iconWrapper {\n    margin-left: 0;\n  }\n  .euiTreeView--withArrows.euiTreeView .euiTreeView__iconWrapper {\n    margin-left: 20px;\n  }\n  .euiTreeView--withArrows.euiTreeView--compressed\n    .euiTreeView__nodeInner--withArrows\n    .euiTreeView__iconWrapper {\n    margin-left: 0;\n  }\n  .euiTreeView--withArrows.euiTreeView--compressed .euiTreeView__iconWrapper {\n    margin-left: 16px;\n  }\n  .euiResizableButton {\n    position: relative;\n    flex-shrink: 0;\n    z-index: 1000;\n  }\n  .euiResizableButton:before,\n  .euiResizableButton:after {\n    content: '';\n    display: block;\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    background-color: #343741;\n    transition:\n      width 150ms ease,\n      height 150ms ease,\n      transform 150ms ease,\n      background-color 150ms ease;\n  }\n  .euiResizableButton.euiResizableButton--horizontal {\n    cursor: col-resize;\n    width: 16px;\n    margin-left: -8px;\n    margin-right: -8px;\n  }\n  .euiResizableButton.euiResizableButton--horizontal:before,\n  .euiResizableButton.euiResizableButton--horizontal:after {\n    width: 1px;\n    height: 12px;\n  }\n  .euiResizableButton.euiResizableButton--horizontal:before {\n    transform: translate(-2px, -50%);\n  }\n  .euiResizableButton.euiResizableButton--horizontal:after {\n    transform: translate(1px, -50%);\n  }\n  .euiResizableButton.euiResizableButton--vertical {\n    cursor: row-resize;\n    height: 16px;\n    margin-top: -8px;\n    margin-bottom: -8px;\n  }\n  .euiResizableButton.euiResizableButton--vertical:before,\n  .euiResizableButton.euiResizableButton--vertical:after {\n    width: 12px;\n    height: 1px;\n  }\n  .euiResizableButton.euiResizableButton--vertical:before {\n    transform: translate(-50%, -2px);\n  }\n  .euiResizableButton.euiResizableButton--vertical:after {\n    transform: translate(-50%, 1px);\n  }\n  .euiResizableButton:hover:not(:disabled):before,\n  .euiResizableButton:hover:not(:disabled):after {\n    background-color: #98a2b3;\n    transition-delay: 150ms;\n  }\n  .euiResizableButton:focus:not(:disabled) {\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiResizableButton:focus:not(:disabled):before,\n  .euiResizableButton:focus:not(:disabled):after {\n    background-color: #006bb4;\n    transition:\n      width 150ms ease,\n      height 150ms ease,\n      transform 150ms ease;\n    transition-delay: 75ms;\n  }\n  .euiResizableButton:hover:not(\n      :disabled\n    ).euiResizableButton--horizontal:before,\n  .euiResizableButton:hover:not(:disabled).euiResizableButton--horizontal:after,\n  .euiResizableButton:focus:not(\n      :disabled\n    ).euiResizableButton--horizontal:before,\n  .euiResizableButton:focus:not(\n      :disabled\n    ).euiResizableButton--horizontal:after {\n    height: 100%;\n  }\n  .euiResizableButton:hover:not(\n      :disabled\n    ).euiResizableButton--horizontal:before,\n  .euiResizableButton:focus:not(\n      :disabled\n    ).euiResizableButton--horizontal:before {\n    transform: translate(-1px, -50%);\n  }\n  .euiResizableButton:hover:not(:disabled).euiResizableButton--horizontal:after,\n  .euiResizableButton:focus:not(\n      :disabled\n    ).euiResizableButton--horizontal:after {\n    transform: translate(0, -50%);\n  }\n  .euiResizableButton:hover:not(:disabled).euiResizableButton--vertical:before,\n  .euiResizableButton:hover:not(:disabled).euiResizableButton--vertical:after,\n  .euiResizableButton:focus:not(:disabled).euiResizableButton--vertical:before,\n  .euiResizableButton:focus:not(:disabled).euiResizableButton--vertical:after {\n    width: 100%;\n  }\n  .euiResizableButton:hover:not(:disabled).euiResizableButton--vertical:before,\n  .euiResizableButton:focus:not(:disabled).euiResizableButton--vertical:before {\n    transform: translate(-50%, -1px);\n  }\n  .euiResizableButton:hover:not(:disabled).euiResizableButton--vertical:after,\n  .euiResizableButton:focus:not(:disabled).euiResizableButton--vertical:after {\n    transform: translate(-50%, 0);\n  }\n  .euiResizableButton:disabled {\n    display: none !important;\n  }\n  .euiResizableToggleButton {\n    box-shadow: 0 2px 2px -1px rgba(152, 162, 179, 0.3);\n    position: absolute;\n    z-index: 1001;\n    animation: none !important;\n    transition-property: background, box-shadow;\n  }\n  .euiResizableToggleButton:focus {\n    box-shadow:\n      0 4px 8px 0 rgba(152, 162, 179, 0.15),\n      0 2px 2px -1px rgba(152, 162, 179, 0.3);\n  }\n  .euiResizableToggleButton-isCollapsed {\n    box-shadow: none;\n    background: rgba(0, 0, 0, 0);\n    border-radius: 0;\n  }\n  .euiResizableToggleButton:not(:focus):not(:active):not(\n      .euiResizableToggleButton-isVisible\n    ):not(.euiResizableToggleButton-isCollapsed) {\n    position: absolute;\n    left: -10000px;\n    top: auto;\n    width: 1px;\n    height: 1px;\n    overflow: hidden;\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton--after {\n    right: 0;\n    top: 50%;\n    transform: translate(50%, -50%);\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton--after.euiResizableToggleButton--top {\n    top: 0;\n    transform: translate(50%, 16px);\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton--after.euiResizableToggleButton--bottom {\n    top: auto;\n    bottom: 0;\n    transform: translate(50%, -16px);\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton--before {\n    left: 0;\n    top: 50%;\n    transform: translate(-50%, -50%);\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton--before.euiResizableToggleButton--top {\n    top: 0;\n    transform: translate(-50%, 16px);\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton--before.euiResizableToggleButton--bottom {\n    top: auto;\n    bottom: 0;\n    transform: translate(-50%, -16px);\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton-isCollapsed {\n    top: 0 !important;\n    bottom: 0 !important;\n    transform: none !important;\n    height: 100%;\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton-isCollapsed.euiResizableToggleButton--top {\n    padding-top: 16px;\n    align-items: flex-start;\n  }\n  .euiResizableToggleButton--horizontal.euiResizableToggleButton.euiResizableToggleButton-isCollapsed.euiResizableToggleButton--bottom {\n    padding-bottom: 16px;\n    align-items: flex-end;\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton--after {\n    top: 100%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton--after.euiResizableToggleButton--left {\n    left: 0;\n    transform: translate(16px, -50%);\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton--after.euiResizableToggleButton--right {\n    left: auto;\n    right: 0;\n    transform: translate(-16px, -50%);\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton--before {\n    bottom: 100%;\n    left: 50%;\n    transform: translate(-50%, 50%);\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton--before.euiResizableToggleButton--left {\n    left: 0;\n    transform: translate(16px, 50%);\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton--before.euiResizableToggleButton--right {\n    left: auto;\n    right: 0;\n    transform: translate(-16px, 50%);\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton-isCollapsed {\n    top: 0 !important;\n    bottom: 0 !important;\n    left: 0 !important;\n    transform: none !important;\n    width: 100%;\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton-isCollapsed.euiResizableToggleButton--left {\n    padding-left: 16px;\n    justify-content: flex-start;\n  }\n  .euiResizableToggleButton--vertical.euiResizableToggleButton.euiResizableToggleButton-isCollapsed.euiResizableToggleButton--right {\n    padding-right: 16px;\n    justify-content: flex-end;\n  }\n  .euiResizableContainer {\n    display: flex;\n    width: 100%;\n  }\n  .euiResizableContainer--vertical {\n    flex-direction: column;\n  }\n  .euiResizablePanel {\n    position: relative;\n  }\n  .euiResizablePanel--paddingSmall {\n    padding: 8px;\n  }\n  .euiResizablePanel--paddingMedium {\n    padding: 16px;\n  }\n  .euiResizablePanel--paddingLarge {\n    padding: 24px;\n  }\n  .euiResizablePanel__content {\n    height: 100%;\n  }\n  .euiResizablePanel__content:not([class*='plain']) {\n    border-width: 0;\n  }\n  .euiResizablePanel__content--scrollable {\n    scrollbar-width: thin;\n    overflow-y: auto;\n  }\n  .euiResizablePanel__content--scrollable::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiResizablePanel__content--scrollable::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiResizablePanel__content--scrollable::-webkit-scrollbar-corner,\n  .euiResizablePanel__content--scrollable::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiResizablePanel-isCollapsed {\n    overflow: hidden;\n  }\n  .euiResizablePanel-isCollapsed .euiResizablePanel__content * {\n    display: none;\n  }\n  .euiResizableContainer--horizontal .euiResizablePanel-isCollapsed {\n    min-width: 0 !important;\n  }\n  .euiResizableContainer--horizontal\n    .euiResizablePanel--collapsible.euiResizablePanel-isCollapsed {\n    min-width: 24px !important;\n  }\n  .euiResizableContainer--vertical .euiResizablePanel-isCollapsed {\n    min-height: 0 !important;\n  }\n  .euiResizableContainer--vertical\n    .euiResizablePanel--collapsible.euiResizablePanel-isCollapsed {\n    min-height: 24px !important;\n  }\n  .euiSideNav__mobileToggle {\n    height: auto;\n    border-bottom: 1px solid #d3dae6;\n    width: 100%;\n    text-align: left;\n    border-radius: 0 !important;\n    font-size: 18px;\n    padding: 0 16px;\n  }\n  .euiSideNav__mobileToggle .euiSideNav__mobileToggleText {\n    padding: 16px 0;\n  }\n  .euiSideNav__mobileToggle .euiSideNav__mobileToggleContent {\n    justify-content: space-between;\n  }\n  .euiSideNav__heading {\n    margin-bottom: 24px;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiSideNav__contentMobile-xs {\n      overflow: hidden;\n      visibility: hidden;\n      opacity: 0;\n      max-height: 0;\n      padding: 0 24px;\n    }\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-xs {\n      visibility: visible;\n      opacity: 1;\n      padding: 24px;\n      max-height: 5000px;\n    }\n  }\n  @media only screen and (max-width: 574px) and (prefers-reduced-motion: no-preference) {\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-xs {\n      transition: all 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiSideNav__contentMobile-s {\n      overflow: hidden;\n      visibility: hidden;\n      opacity: 0;\n      max-height: 0;\n      padding: 0 24px;\n    }\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-s {\n      visibility: visible;\n      opacity: 1;\n      padding: 24px;\n      max-height: 5000px;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) and (prefers-reduced-motion: no-preference) {\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-s {\n      transition: all 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiSideNav__contentMobile-m {\n      overflow: hidden;\n      visibility: hidden;\n      opacity: 0;\n      max-height: 0;\n      padding: 0 24px;\n    }\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-m {\n      visibility: visible;\n      opacity: 1;\n      padding: 24px;\n      max-height: 5000px;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) and (prefers-reduced-motion: no-preference) {\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-m {\n      transition: all 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .euiSideNav__contentMobile-l {\n      overflow: hidden;\n      visibility: hidden;\n      opacity: 0;\n      max-height: 0;\n      padding: 0 24px;\n    }\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-l {\n      visibility: visible;\n      opacity: 1;\n      padding: 24px;\n      max-height: 5000px;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) and (prefers-reduced-motion: no-preference) {\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-l {\n      transition: all 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .euiSideNav__contentMobile-xl {\n      overflow: hidden;\n      visibility: hidden;\n      opacity: 0;\n      max-height: 0;\n      padding: 0 24px;\n    }\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-xl {\n      visibility: visible;\n      opacity: 1;\n      padding: 24px;\n      max-height: 5000px;\n    }\n  }\n  @media only screen and (min-width: 1200px) and (prefers-reduced-motion: no-preference) {\n    .euiSideNav-isOpenMobile .euiSideNav__contentMobile-xl {\n      transition: all 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n  }\n  .euiSideNavItemButton {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    text-align: left;\n    display: block;\n    width: 100%;\n    padding: 2px 0;\n    color: inherit;\n  }\n  .euiSideNavItemButton.euiSideNavItemButton--isClickable:not(:disabled):hover {\n    cursor: pointer;\n  }\n  .euiSideNavItemButton.euiSideNavItemButton--isClickable:not(:disabled):hover\n    .euiSideNavItemButton__label,\n  .euiSideNavItemButton.euiSideNavItemButton--isClickable:not(:disabled):focus\n    .euiSideNavItemButton__label {\n    text-decoration: underline;\n  }\n  .euiSideNavItemButton.euiSideNavItemButton-isSelected {\n    color: #006bb4;\n    font-weight: 700;\n  }\n  .euiSideNavItemButton.euiSideNavItemButton-isSelected\n    .euiSideNavItemButton__label {\n    text-decoration: underline;\n  }\n  .euiSideNavItemButton:disabled {\n    cursor: not-allowed;\n    text-decoration: none;\n    color: #a6a7aa;\n  }\n  .euiSideNavItemButton__content {\n    display: flex;\n    align-items: center;\n  }\n  .euiSideNavItemButton__icon {\n    margin-right: 8px;\n  }\n  .euiSideNavItemButton__labelContainer {\n    min-width: 0;\n  }\n  .euiSideNavItemButton__label {\n    flex-grow: 1;\n  }\n  .euiSideNavItemButton__label--truncated {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n  }\n  .euiSideNavItem--root.euiSideNavItem--rootIcon > .euiSideNavItem__items {\n    margin-left: 24px;\n  }\n  .euiSideNavItem--root > .euiSideNavItemButton {\n    margin-bottom: 8px;\n    padding: 0;\n    padding-left: 8px;\n    padding-right: 8px;\n    margin-left: -8px;\n    width: calc(100% + 16px);\n  }\n  .euiSideNavItem--root > .euiSideNavItemButton .euiSideNavItemButton__label {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    color: inherit;\n  }\n  .euiSideNavItem--root > .euiSideNavItem__items {\n    position: static;\n    margin-left: 0;\n  }\n  .euiSideNavItem--root > .euiSideNavItem__items:after {\n    display: none;\n  }\n  .euiSideNavItem--root + .euiSideNavItem--root {\n    margin-top: 32px;\n  }\n  .euiSideNavItem--trunk {\n    color: #1a1c21;\n  }\n  .euiSideNavItem--trunk > .euiSideNavItemButton {\n    padding-left: 8px;\n    padding-right: 8px;\n    margin-left: -8px;\n    width: calc(100% + 16px);\n  }\n  .euiSideNavItem--trunk > .euiSideNavItem__items {\n    margin-left: 8px;\n    width: 100%;\n  }\n  .euiSideNavItem--branch {\n    position: relative;\n    color: #656b77;\n  }\n  .euiSideNavItem--branch::after {\n    position: absolute;\n    content: '';\n    top: 0;\n    bottom: 0;\n    width: 1px;\n    background: #d3dae6;\n    left: 0;\n  }\n  .euiSideNavItem--branch:last-of-type::after {\n    height: 12px;\n  }\n  .euiSideNavItem--branch > .euiSideNavItemButton {\n    position: relative;\n    padding-left: 8px;\n    padding-right: 8px;\n  }\n  .euiSideNavItem--branch > .euiSideNavItemButton:after {\n    position: absolute;\n    content: '';\n    top: 12px;\n    left: 0;\n    width: 4px;\n    height: 1px;\n    background: #d3dae6;\n  }\n  .euiSideNavItem--branch > .euiSideNavItem__items {\n    margin-left: 16px;\n  }\n  .euiSideNavItem--emphasized {\n    background: rgba(211, 218, 230, 0.3);\n    color: #1a1c21;\n    box-shadow:\n      100px 0 0 0 rgba(211, 218, 230, 0.3),\n      -100px 0 0 0 rgba(211, 218, 230, 0.3);\n  }\n  .euiSideNavItem--emphasized > .euiSideNavItemButton {\n    font-weight: 700;\n  }\n  .euiSideNavItem--emphasized .euiSideNavItem--emphasized {\n    background: rgba(0, 0, 0, 0);\n    box-shadow: none;\n  }\n  .euiSpacer {\n    flex-shrink: 0;\n  }\n  .euiSpacer--xs {\n    height: 4px;\n  }\n  .euiSpacer--s {\n    height: 8px;\n  }\n  .euiSpacer--m {\n    height: 16px;\n  }\n  .euiSpacer--l {\n    height: 24px;\n  }\n  .euiSpacer--xl {\n    height: 32px;\n  }\n  .euiSpacer--xxl {\n    height: 40px;\n  }\n  .euiSearchBar__searchHolder {\n    min-width: 200px;\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiSearchBar__filtersHolder {\n      max-width: calc(100% - 16px);\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .euiSearchBar__filtersHolder {\n      max-width: calc(100% - 16px);\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .euiSearchBar__filtersHolder {\n      max-width: calc(100% - 16px);\n    }\n  }\n  .euiSelectable {\n    display: flex;\n    flex-direction: column;\n  }\n  .euiSelectable-fullHeight {\n    height: 100%;\n  }\n  .euiSelectableList:focus-within {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiSelectableList-fullHeight {\n    flex-grow: 1;\n  }\n  .euiSelectableList-bordered {\n    overflow: hidden;\n    border: 1px solid #d3dae6;\n    border-radius: 4px;\n  }\n  .euiSelectableList__list {\n    mask-image: linear-gradient(\n      to bottom,\n      rgba(255, 0, 0, 0.1) 0%,\n      red 7.5px,\n      red calc(100% - 7.5px),\n      rgba(255, 0, 0, 0.1) 100%\n    );\n    scrollbar-width: thin;\n  }\n  .euiSelectableList__list::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiSelectableList__list::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiSelectableList__list::-webkit-scrollbar-corner,\n  .euiSelectableList__list::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiSelectableList__groupLabel {\n    color: #1a1c21;\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid #eef2f7;\n    padding: 4px 12px;\n  }\n  .euiSelectableListItem {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    display: inline-flex;\n    width: 100%;\n    text-align: left;\n    color: #343741;\n    cursor: pointer;\n  }\n  .euiSelectableListItem:not(:last-of-type) {\n    border-bottom: 1px solid #eef2f7;\n  }\n  .euiSelectableListItem-isFocused:not([aria-disabled='true']),\n  .euiSelectableListItem:hover:not([aria-disabled='true']) {\n    color: #006bb4;\n    background-color: #e6f0f8;\n  }\n  .euiSelectableListItem-isFocused:not([aria-disabled='true'])\n    .euiSelectableListItem__text,\n  .euiSelectableListItem:hover:not([aria-disabled='true'])\n    .euiSelectableListItem__text {\n    text-decoration: underline;\n  }\n  .euiSelectableListItem[aria-disabled='true'] {\n    color: #98a2b3;\n    cursor: not-allowed;\n  }\n  .euiSelectableListItem__content {\n    padding: 4px 12px;\n    width: 100%;\n    display: flex;\n    align-items: center;\n  }\n  .euiSelectableListItem__icon,\n  .euiSelectableListItem__prepend {\n    margin-right: 12px;\n    flex-shrink: 0;\n  }\n  .euiSelectableListItem__append {\n    margin-left: 12px;\n    flex-shrink: 0;\n  }\n  .euiSelectableListItem__text {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    flex-grow: 1;\n  }\n  .euiSelectableMessage {\n    padding: 8px;\n    text-align: center;\n    word-wrap: break-word;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n  }\n  .euiSelectableMessage--bordered {\n    overflow: hidden;\n    border: 1px solid #d3dae6;\n    border-radius: 4px;\n  }\n  .euiHeader--dark .euiSelectableTemplateSitewide .euiFormControlLayout {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiHeader--dark .euiSelectableTemplateSitewide .euiFormControlLayout--group,\n  .euiHeader--dark .euiSelectableTemplateSitewide .euiFormControlLayout input {\n    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within) {\n    color: rgba(255, 255, 255, 0.7);\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    input {\n    color: inherit;\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    input::-webkit-input-placeholder {\n    color: rgba(255, 255, 255, 0.4);\n    opacity: 1;\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    input::-moz-placeholder {\n    color: rgba(255, 255, 255, 0.4);\n    opacity: 1;\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    input:-ms-input-placeholder {\n    color: rgba(255, 255, 255, 0.4);\n    opacity: 1;\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    input:-moz-placeholder {\n    color: rgba(255, 255, 255, 0.4);\n    opacity: 1;\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    input::placeholder {\n    color: rgba(255, 255, 255, 0.4);\n    opacity: 1;\n  }\n  .euiHeader--dark\n    .euiSelectableTemplateSitewide\n    .euiFormControlLayout:not(:focus-within)\n    .euiFormControlLayout__append {\n    background-color: rgba(0, 0, 0, 0);\n    color: inherit;\n  }\n  .euiSelectableTemplateSitewide__listItem .euiSelectableListItem__text {\n    text-decoration: none !important;\n  }\n  .euiSelectableTemplateSitewide__listItem[class*='-isFocused']:not(\n      [aria-disabled='true']\n    )\n    .euiSelectableTemplateSitewide__listItemTitle,\n  .euiSelectableTemplateSitewide__listItem:hover:not([aria-disabled='true'])\n    .euiSelectableTemplateSitewide__listItemTitle {\n    text-decoration: underline;\n  }\n  .euiSelectableTemplateSitewide__optionMetasList {\n    display: block;\n    margin-top: 4px;\n    font-size: 12px;\n    color: #6a717d;\n  }\n  .euiSelectableTemplateSitewide__optionMeta:not(:last-of-type)::after {\n    content: '•';\n    margin: 0 4px;\n    color: #6a717d;\n  }\n  .euiSelectableTemplateSitewide__optionMeta--application {\n    color: #4e779c;\n    font-weight: 500;\n  }\n  .euiSelectableTemplateSitewide__optionMeta--deployment {\n    color: #3b7d6a;\n    font-weight: 500;\n  }\n  .euiSelectableTemplateSitewide__optionMeta--article {\n    color: #8365a6;\n    font-weight: 500;\n  }\n  .euiSelectableTemplateSitewide__optionMeta--case {\n    color: #bc533e;\n    font-weight: 500;\n  }\n  .euiSelectableTemplateSitewide__optionMeta--platform {\n    color: #807234;\n    font-weight: 500;\n  }\n  .euiStat .euiStat__title {\n    color: #000;\n  }\n  .euiStat .euiStat__title--subdued {\n    color: #6a717d;\n  }\n  .euiStat .euiStat__title--primary {\n    color: #006bb4;\n  }\n  .euiStat .euiStat__title--secondary {\n    color: #017d73;\n  }\n  .euiStat .euiStat__title--success {\n    color: #017d73;\n  }\n  .euiStat .euiStat__title--danger {\n    color: #bd271e;\n  }\n  .euiStat .euiStat__title--accent {\n    color: #dd0a73;\n  }\n  .euiStat .euiStat__title-isLoading {\n    animation: euiStatPulse 1.5s infinite ease-in-out;\n  }\n  .euiStat.euiStat--leftAligned {\n    text-align: left;\n    align-items: flex-start;\n  }\n  .euiStat.euiStat--centerAligned {\n    text-align: center;\n    align-items: center;\n  }\n  .euiStat.euiStat--rightAligned {\n    text-align: right;\n    align-items: flex-end;\n  }\n  @keyframes euiStatPulse {\n    0% {\n      opacity: 1;\n    }\n    50% {\n      opacity: 0.25;\n    }\n    100% {\n      opacity: 1;\n    }\n  }\n  .euiStepNumber {\n    width: 32px;\n    height: 32px;\n    display: inline-block;\n    line-height: 32px;\n    border-radius: 32px;\n    text-align: center;\n    color: #fff;\n    background-color: #006bb4;\n    font-size: 16px;\n    font-weight: 500;\n  }\n  .euiStepNumber .euiStepNumber__icon {\n    vertical-align: middle;\n    position: relative;\n    top: -2px;\n  }\n  .euiStepNumber--small {\n    width: 24px;\n    height: 24px;\n    display: inline-block;\n    line-height: 24px;\n    border-radius: 24px;\n    text-align: center;\n    color: #fff;\n    background-color: #006bb4;\n    font-size: 12px;\n    font-weight: 500;\n  }\n  .euiStepNumber--small .euiStepNumber__icon {\n    top: -1px;\n  }\n  .euiStepNumber--complete .euiStepNumber__icon {\n    stroke: currentColor;\n    stroke-width: 0.5px;\n  }\n  @media screen and (prefers-reduced-motion: no-preference) {\n    .euiStepNumber--complete,\n    .euiStepNumber--warning,\n    .euiStepNumber--danger {\n      animation: euiGrow 150ms cubic-bezier(0.34, 1.61, 0.7, 1);\n    }\n  }\n  .euiStepNumber--loading {\n    background: rgba(0, 0, 0, 0);\n  }\n  .euiStepNumber.euiStepNumber-isHollow {\n    background-color: rgba(0, 0, 0, 0);\n    border: 2px solid #006bb4;\n  }\n  .euiStepNumber.euiStepNumber-isHollow .euiStepNumber__number {\n    display: none;\n  }\n  .euiStepNumber--warning {\n    color: #936400;\n    background-color: #fef6e6;\n  }\n  .euiStepNumber--danger {\n    color: #bd271e;\n    background-color: #f8e9e9;\n  }\n  .euiStepNumber--disabled {\n    color: #646a77;\n    background-color: #f0f1f2;\n  }\n  .euiStepNumber--incomplete {\n    color: #646a77;\n    background-color: #f0f1f2;\n  }\n  .euiStep:not(:last-of-type) {\n    background-image: linear-gradient(\n      to right,\n      transparent 0,\n      transparent 15px,\n      #d3dae6 15px,\n      #d3dae6 17px,\n      transparent 17px,\n      transparent 100%\n    );\n    background-repeat: no-repeat;\n    background-position: left 40px;\n  }\n  .euiStep--small:not(:last-of-type) {\n    background-position: left -4px top 32px;\n  }\n  .euiStep--small .euiStep__content {\n    padding-left: 28px;\n    margin-left: 12px;\n  }\n  .euiStep__titleWrapper {\n    display: flex;\n  }\n  .euiStep__circle {\n    flex-shrink: 0;\n    margin-right: 16px;\n    vertical-align: top;\n  }\n  .euiStep__title {\n    font-weight: 500;\n  }\n  .euiStep__content {\n    padding: 16px 16px 32px;\n    margin: 8px 0;\n    padding-left: 32px;\n    margin-left: 16px;\n  }\n  .euiSubSteps {\n    padding: 16px;\n    background-color: #f5f7fa;\n    margin-bottom: 16px;\n  }\n  .euiSubSteps > *:last-child {\n    margin-bottom: 0;\n  }\n  .euiText .euiSubSteps ol,\n  .euiSubSteps ol {\n    list-style-type: lower-alpha;\n  }\n  .euiStepsHorizontal {\n    display: flex;\n    align-items: stretch;\n    background: rgba(245, 247, 250, 0.5);\n  }\n  .euiStepHorizontal__item {\n    flex-grow: 1;\n    flex-basis: 0%;\n  }\n  .euiStepHorizontal__item:first-of-type > .euiStepHorizontal::before,\n  .euiStepHorizontal__item:last-of-type > .euiStepHorizontal::after {\n    display: none;\n  }\n  .euiStepHorizontal {\n    padding: 24px 16px 16px;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: flex-start;\n    cursor: pointer;\n    position: relative;\n    width: 100%;\n  }\n  .euiStepHorizontal:focus:not(.euiStepHorizontal-isDisabled)\n    .euiStepHorizontal__title,\n  .euiStepHorizontal:hover:not(.euiStepHorizontal-isDisabled)\n    .euiStepHorizontal__title {\n    text-decoration: underline;\n  }\n  .euiStepHorizontal:focus:not(.euiStepHorizontal-isDisabled) {\n    outline: none;\n  }\n  .euiStepHorizontal:focus:not(.euiStepHorizontal-isDisabled)\n    .euiStepHorizontal__number {\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimateLarge !important;\n  }\n  .euiStepHorizontal.euiStepHorizontal-isDisabled {\n    cursor: not-allowed;\n  }\n  .euiStepHorizontal::before,\n  .euiStepHorizontal::after {\n    content: '';\n    position: absolute;\n    width: calc(50% - 16px);\n    height: 1px;\n    top: 40px;\n    background-color: #d3dae6;\n    z-index: 0;\n  }\n  .euiStepHorizontal::before {\n    left: 0;\n  }\n  .euiStepHorizontal::after {\n    right: 0;\n  }\n  .euiStepHorizontal__number {\n    position: relative;\n    z-index: 1;\n    transition: all 150ms ease-in-out;\n  }\n  .euiStepHorizontal__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    margin-top: 8px;\n    font-weight: 400;\n    text-align: center;\n  }\n  .euiStepHorizontal-isDisabled .euiStepHorizontal__title {\n    color: #69707d;\n  }\n  .euiStepHorizontal-isComplete::before,\n  .euiStepHorizontal-isComplete::after {\n    height: 2px;\n    background-color: #006bb4;\n  }\n  .euiStepHorizontal-isSelected\n    .euiStepHorizontal__number:not([class*='danger']):not(\n      [class*='warning']\n    ):not([class*='loading']) {\n    box-shadow: 0 2px 2px -1px rgba(18, 104, 162, 0.3);\n  }\n  .euiStepHorizontal-isSelected::before {\n    height: 2px;\n    background-color: #006bb4;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiStepHorizontal {\n      padding-top: 16px;\n    }\n    .euiStepHorizontal::before,\n    .euiStepHorizontal::after {\n      top: 32px;\n    }\n    .euiStepHorizontal__title {\n      display: none;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiStepHorizontal {\n      padding-top: 16px;\n    }\n    .euiStepHorizontal::before,\n    .euiStepHorizontal::after {\n      top: 32px;\n    }\n    .euiStepHorizontal__title {\n      display: none;\n    }\n  }\n  .euiSuggestItem {\n    display: flex;\n    flex-grow: 1;\n    align-items: center;\n    font-size: 12px;\n    white-space: nowrap;\n  }\n  .euiSuggestItem.euiSuggestItem-isClickable {\n    width: 100%;\n    text-align: left;\n  }\n  .euiSuggestItem.euiSuggestItem-isClickable:hover,\n  .euiSuggestItem.euiSuggestItem-isClickable:focus {\n    cursor: pointer;\n    background-color: #f5f7fa;\n  }\n  .euiSuggestItem.euiSuggestItem-isClickable:hover .euiSuggestItem__type,\n  .euiSuggestItem.euiSuggestItem-isClickable:focus .euiSuggestItem__type {\n    color: #343741;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint0 {\n    background-color: #e0f1ed;\n    color: #357160;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint1 {\n    background-color: #e2ebf4;\n    color: #466b8d;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint2 {\n    background-color: #f7e2e9;\n    color: #a34a68;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint3 {\n    background-color: #ebe5f2;\n    color: #765b96;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint4 {\n    background-color: #f5ebf0;\n    color: #865f74;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint5 {\n    background-color: #f8f3e1;\n    color: #7a6c31;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint6 {\n    background-color: #f2efea;\n    color: #756a56;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint7 {\n    background-color: #f8eade;\n    color: #915c2e;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint8 {\n    background-color: #f0e3e1;\n    color: #92564a;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint9 {\n    background-color: #fbe3df;\n    color: #aa4b38;\n  }\n  .euiSuggestItem .euiSuggestItem__type--tint10 {\n    background-color: #e4e5e8;\n    color: #5f6571;\n  }\n  .euiSuggestItem .euiSuggestItem__label,\n  .euiSuggestItem .euiSuggestItem__type,\n  .euiSuggestItem .euiSuggestItem__description {\n    flex-grow: 0;\n    display: flex;\n    flex-direction: column;\n  }\n  .euiSuggestItem .euiSuggestItem__type {\n    position: relative;\n    flex-shrink: 0;\n    flex-basis: auto;\n    width: 32px;\n    height: 32px;\n    text-align: center;\n    overflow: hidden;\n    padding: 4px;\n    justify-content: center;\n    align-items: center;\n  }\n  .euiSuggestItem .euiSuggestItem__label {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    font-family: 'Roboto Mono', Consolas, Menlo, Courier, monospace;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 4px 8px;\n    color: #343741;\n    display: block;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width20 {\n    flex-basis: 20%;\n    min-width: 20%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width21 {\n    flex-basis: 21%;\n    min-width: 21%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width22 {\n    flex-basis: 22%;\n    min-width: 22%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width23 {\n    flex-basis: 23%;\n    min-width: 23%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width24 {\n    flex-basis: 24%;\n    min-width: 24%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width25 {\n    flex-basis: 25%;\n    min-width: 25%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width26 {\n    flex-basis: 26%;\n    min-width: 26%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width27 {\n    flex-basis: 27%;\n    min-width: 27%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width28 {\n    flex-basis: 28%;\n    min-width: 28%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width29 {\n    flex-basis: 29%;\n    min-width: 29%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width30 {\n    flex-basis: 30%;\n    min-width: 30%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width31 {\n    flex-basis: 31%;\n    min-width: 31%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width32 {\n    flex-basis: 32%;\n    min-width: 32%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width33 {\n    flex-basis: 33%;\n    min-width: 33%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width34 {\n    flex-basis: 34%;\n    min-width: 34%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width35 {\n    flex-basis: 35%;\n    min-width: 35%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width36 {\n    flex-basis: 36%;\n    min-width: 36%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width37 {\n    flex-basis: 37%;\n    min-width: 37%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width38 {\n    flex-basis: 38%;\n    min-width: 38%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width39 {\n    flex-basis: 39%;\n    min-width: 39%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width40 {\n    flex-basis: 40%;\n    min-width: 40%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width41 {\n    flex-basis: 41%;\n    min-width: 41%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width42 {\n    flex-basis: 42%;\n    min-width: 42%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width43 {\n    flex-basis: 43%;\n    min-width: 43%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width44 {\n    flex-basis: 44%;\n    min-width: 44%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width45 {\n    flex-basis: 45%;\n    min-width: 45%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width46 {\n    flex-basis: 46%;\n    min-width: 46%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width47 {\n    flex-basis: 47%;\n    min-width: 47%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width48 {\n    flex-basis: 48%;\n    min-width: 48%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width49 {\n    flex-basis: 49%;\n    min-width: 49%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width50 {\n    flex-basis: 50%;\n    min-width: 50%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width51 {\n    flex-basis: 51%;\n    min-width: 51%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width52 {\n    flex-basis: 52%;\n    min-width: 52%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width53 {\n    flex-basis: 53%;\n    min-width: 53%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width54 {\n    flex-basis: 54%;\n    min-width: 54%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width55 {\n    flex-basis: 55%;\n    min-width: 55%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width56 {\n    flex-basis: 56%;\n    min-width: 56%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width57 {\n    flex-basis: 57%;\n    min-width: 57%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width58 {\n    flex-basis: 58%;\n    min-width: 58%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width59 {\n    flex-basis: 59%;\n    min-width: 59%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width60 {\n    flex-basis: 60%;\n    min-width: 60%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width61 {\n    flex-basis: 61%;\n    min-width: 61%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width62 {\n    flex-basis: 62%;\n    min-width: 62%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width63 {\n    flex-basis: 63%;\n    min-width: 63%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width64 {\n    flex-basis: 64%;\n    min-width: 64%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width65 {\n    flex-basis: 65%;\n    min-width: 65%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width66 {\n    flex-basis: 66%;\n    min-width: 66%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width67 {\n    flex-basis: 67%;\n    min-width: 67%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width68 {\n    flex-basis: 68%;\n    min-width: 68%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width69 {\n    flex-basis: 69%;\n    min-width: 69%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width70 {\n    flex-basis: 70%;\n    min-width: 70%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width71 {\n    flex-basis: 71%;\n    min-width: 71%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width72 {\n    flex-basis: 72%;\n    min-width: 72%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width73 {\n    flex-basis: 73%;\n    min-width: 73%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width74 {\n    flex-basis: 74%;\n    min-width: 74%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width75 {\n    flex-basis: 75%;\n    min-width: 75%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width76 {\n    flex-basis: 76%;\n    min-width: 76%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width77 {\n    flex-basis: 77%;\n    min-width: 77%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width78 {\n    flex-basis: 78%;\n    min-width: 78%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width79 {\n    flex-basis: 79%;\n    min-width: 79%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width80 {\n    flex-basis: 80%;\n    min-width: 80%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width81 {\n    flex-basis: 81%;\n    min-width: 81%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width82 {\n    flex-basis: 82%;\n    min-width: 82%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width83 {\n    flex-basis: 83%;\n    min-width: 83%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width84 {\n    flex-basis: 84%;\n    min-width: 84%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width85 {\n    flex-basis: 85%;\n    min-width: 85%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width86 {\n    flex-basis: 86%;\n    min-width: 86%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width87 {\n    flex-basis: 87%;\n    min-width: 87%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width88 {\n    flex-basis: 88%;\n    min-width: 88%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width89 {\n    flex-basis: 89%;\n    min-width: 89%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__label--width90 {\n    flex-basis: 90%;\n    min-width: 90%;\n  }\n  .euiSuggestItem .euiSuggestItem__label.euiSuggestItem__labelDisplay--expand {\n    flex-basis: auto;\n    flex-shrink: 1;\n  }\n  .euiSuggestItem .euiSuggestItem__description {\n    color: #69707d;\n    flex-basis: auto;\n    padding-top: 2px;\n    display: block;\n  }\n  .euiSuggestItem\n    .euiSuggestItem__description.euiSuggestItem__description--wrap {\n    overflow-wrap: break-word !important;\n    word-wrap: break-word !important;\n    word-break: break-word;\n    white-space: normal;\n    line-height: 14px;\n  }\n  .euiSuggestItem\n    .euiSuggestItem__description.euiSuggestItem__description--truncate {\n    max-width: 100%;\n    overflow: hidden !important;\n    text-overflow: ellipsis !important;\n    white-space: nowrap !important;\n    word-wrap: normal !important;\n    line-height: 1.5;\n  }\n  .euiSuggestItem .euiSuggestItem__description:empty {\n    flex-grow: 0;\n    margin-left: 0;\n  }\n  .euiSuggestInput__statusIcon {\n    background-color: rgba(0, 0, 0, 0) !important;\n  }\n  .euiTable {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    font-feature-settings:\n      'calt' 1,\n      'kern' 1,\n      'liga' 1,\n      'tnum' 1;\n    width: 100%;\n    table-layout: fixed;\n    border: none;\n    border-collapse: collapse;\n    background-color: #fff;\n  }\n  .euiTable.euiTable--auto {\n    table-layout: auto;\n  }\n  .euiTableCaption {\n    position: relative;\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiTable--compressed .euiTableCellContent {\n      font-size: 12px;\n      font-size: 0.75rem;\n      line-height: 1.5;\n      padding: 4px;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .euiTable--compressed .euiTableCellContent {\n      font-size: 12px;\n      font-size: 0.75rem;\n      line-height: 1.5;\n      padding: 4px;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .euiTable--compressed .euiTableCellContent {\n      font-size: 12px;\n      font-size: 0.75rem;\n      line-height: 1.5;\n      padding: 4px;\n    }\n  }\n  .euiTableFooterCell,\n  .euiTableHeaderCell {\n    vertical-align: middle;\n    border-top: 1px solid #d3dae6;\n    border-bottom: 1px solid #d3dae6;\n    font-weight: inherit;\n    text-align: inherit;\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    font-weight: 500;\n    border-top: none;\n  }\n  .euiTableFooterCell .euiTableHeaderButton,\n  .euiTableHeaderCell .euiTableHeaderButton {\n    text-align: left;\n    font-weight: 500;\n  }\n  .euiTableFooterCell .euiTableCellContent__text,\n  .euiTableHeaderCell .euiTableCellContent__text {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5;\n    font-weight: 600;\n  }\n  .euiTableHeaderButton {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5;\n    color: inherit;\n    width: 100%;\n  }\n  .euiTableHeaderButton:hover .euiTableCellContent__text,\n  .euiTableHeaderButton:focus .euiTableCellContent__text {\n    text-decoration: underline;\n    color: #006bb4;\n  }\n  .euiTableHeaderButton:hover .euiTableSortIcon,\n  .euiTableHeaderButton:focus .euiTableSortIcon {\n    fill: #006bb4;\n  }\n  .euiTableSortIcon {\n    margin-left: 4px;\n    flex-shrink: 0;\n  }\n  .euiTableHeaderButton-isSorted .euiTableSortIcon {\n    fill: #1a1c21;\n  }\n  .euiTableHeaderCellCheckbox {\n    vertical-align: middle;\n    border-top: 1px solid #d3dae6;\n    border-bottom: 1px solid #d3dae6;\n    font-weight: inherit;\n    text-align: inherit;\n    width: 32px;\n    vertical-align: middle;\n    border-top: none;\n  }\n  .euiTableRow:hover {\n    background-color: #fafbfd;\n  }\n  .euiTableRow.euiTableRow-isClickable:hover {\n    background-color: rgba(0, 107, 180, 0.05);\n    cursor: pointer;\n  }\n  .euiTableRow.euiTableRow-isClickable:focus {\n    background-color: rgba(0, 107, 180, 0.1);\n  }\n  .euiTableRow.euiTableRow-isExpandedRow {\n    background-color: #fafbfd;\n  }\n  .euiTableRow.euiTableRow-isExpandedRow.euiTableRow-isSelectable\n    .euiTableCellContent {\n    padding-left: 40px;\n  }\n  .euiTableRow.euiTableRow-isSelected {\n    background-color: #eef5fa;\n  }\n  .euiTableRow.euiTableRow-isSelected\n    + .euiTableRow.euiTableRow-isExpandedRow\n    .euiTableRowCell {\n    background-color: #eef5fa;\n  }\n  .euiTableRow.euiTableRow-isSelected:hover,\n  .euiTableRow.euiTableRow-isSelected:hover\n    + .euiTableRow.euiTableRow-isExpandedRow\n    .euiTableRowCell {\n    background-color: #e6f0f8;\n  }\n  .euiTableRowCell {\n    vertical-align: middle;\n    border-top: 1px solid #d3dae6;\n    border-bottom: 1px solid #d3dae6;\n    font-weight: inherit;\n    text-align: inherit;\n    color: #343741;\n  }\n  .euiTableRowCell.euiTableRowCell--isMobileHeader {\n    display: none;\n  }\n  .euiTableRowCellCheckbox {\n    vertical-align: middle;\n    border-top: 1px solid #d3dae6;\n    border-bottom: 1px solid #d3dae6;\n    font-weight: inherit;\n    text-align: inherit;\n    width: 32px;\n    vertical-align: middle;\n  }\n  .euiTableFooterCell {\n    background-color: #f5f7fa;\n    border-bottom: none;\n  }\n  .euiTableCellContent {\n    overflow: hidden;\n    display: flex;\n    align-items: center;\n    padding: 8px;\n  }\n  .euiTableCellContent__text {\n    overflow-wrap: break-word !important;\n    word-wrap: break-word !important;\n    word-break: break-word;\n    min-width: 0;\n    text-overflow: ellipsis;\n  }\n  .euiTableCellContent--alignRight {\n    justify-content: flex-end;\n    text-align: right;\n  }\n  .euiTableCellContent--alignCenter {\n    justify-content: center;\n    text-align: center;\n  }\n  .euiTableHeaderCell,\n  .euiTableFooterCell,\n  .euiTableCellContent--truncateText {\n    white-space: nowrap;\n  }\n  .euiTableHeaderCell .euiTableCellContent__text,\n  .euiTableFooterCell .euiTableCellContent__text,\n  .euiTableCellContent--truncateText .euiTableCellContent__text {\n    overflow: hidden;\n  }\n  .euiTableCellContent--overflowingContent {\n    overflow: visible;\n    white-space: normal;\n    word-break: break-all;\n    word-break: break-word;\n  }\n  .euiTableCellContent--showOnHover > *:not(:first-child) {\n    margin-left: 8px;\n  }\n  .euiTableRow-hasActions\n    .euiTableCellContent--showOnHover\n    .euiTableCellContent__hoverItem {\n    flex-shrink: 0;\n    opacity: 0.7;\n    filter: grayscale(100%);\n    transition:\n      opacity 250ms cubic-bezier(0.694, 0.0482, 0.335, 1),\n      filter 250ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiTableRow-hasActions\n    .euiTableCellContent--showOnHover\n    .expandedItemActions__completelyHide,\n  .euiTableRow-hasActions\n    .euiTableCellContent--showOnHover\n    .expandedItemActions__completelyHide:disabled,\n  .euiTableRow-hasActions\n    .euiTableCellContent--showOnHover\n    .expandedItemActions__completelyHide:disabled:hover,\n  .euiTableRow-hasActions\n    .euiTableCellContent--showOnHover\n    .expandedItemActions__completelyHide:disabled:focus,\n  .euiTableRow:hover\n    .euiTableRow-hasActions\n    .euiTableCellContent--showOnHover\n    .expandedItemActions__completelyHide:disabled {\n    filter: grayscale(0%);\n    opacity: 0;\n  }\n  .euiTableRow-hasActions:hover\n    .euiTableCellContent--showOnHover\n    .euiTableCellContent__hoverItem:not(:disabled),\n  .euiTableRow-hasActions:hover\n    .euiTableCellContent--showOnHover\n    .euiTableCellContent__hoverItem:not(:disabled):hover,\n  .euiTableRow-hasActions:hover\n    .euiTableCellContent--showOnHover\n    .euiTableCellContent__hoverItem:not(:disabled):focus {\n    opacity: 1;\n    filter: grayscale(0%);\n  }\n  .euiTableRow-isExpandedRow .euiTableCellContent {\n    overflow: hidden;\n    animation: 250ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      growExpandedRow;\n  }\n  @keyframes growExpandedRow {\n    0% {\n      max-height: 0;\n    }\n    99% {\n      max-height: 100vh;\n    }\n    100% {\n      max-height: unset;\n    }\n  }\n  .euiTableRowCell__mobileHeader {\n    display: none;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiTableRowCell--hideForMobile {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiTableRowCell--hideForMobile {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 768px) and (max-width: 991px) {\n    .euiTableRowCell--hideForDesktop {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 992px) and (max-width: 1199px) {\n    .euiTableRowCell--hideForDesktop {\n      display: none !important;\n    }\n  }\n  @media only screen and (min-width: 1200px) {\n    .euiTableRowCell--hideForDesktop {\n      display: none !important;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .euiTable.euiTable--responsive thead {\n      display: none;\n    }\n    .euiTable.euiTable--responsive tfoot {\n      display: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell__mobileHeader {\n      max-width: 100%;\n      overflow: hidden !important;\n      text-overflow: ellipsis !important;\n      white-space: nowrap !important;\n      word-wrap: normal !important;\n      font-size: 11px;\n      font-size: 0.6875rem;\n      display: block;\n      color: #69707d;\n      padding: 8px;\n      padding-bottom: 0;\n      margin-bottom: -8px;\n      min-height: 24px;\n    }\n    .euiTableRowCell:only-child\n      .euiTable.euiTable--responsive\n      .euiTableRowCell__mobileHeader {\n      min-height: 0;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell--enlargeForMobile {\n      font-size: 16px;\n      font-size: 1rem;\n      line-height: 1.5;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell--isMobileFullWidth {\n      width: 100%;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRowCell--isMobileFullWidth\n      .euiTableCellContent--alignCenter {\n      justify-content: center;\n      text-align: center;\n    }\n    .euiTable.euiTable--responsive .euiTableRow {\n      background-color: #fff;\n      border: 1px solid #d3dae6;\n      border-radius: 4px;\n      flex-grow: 1;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--flexGrowZero {\n      flex-grow: 0;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--noBorder {\n      border: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--hasShadow {\n      box-shadow:\n        0 2px 2px -1px rgba(152, 162, 179, 0.3),\n        0 1px 5px -2px rgba(152, 162, 179, 0.3);\n      border: 1px solid #d3dae6;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--isClickable {\n      transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow--isClickable:enabled {\n      display: block;\n      width: 100%;\n      text-align: left;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--isClickable:hover,\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--isClickable:focus {\n      box-shadow:\n        0 4px 8px 0 rgba(152, 162, 179, 0.15),\n        0 2px 2px -1px rgba(152, 162, 179, 0.3);\n      transform: translateY(-2px);\n      cursor: pointer;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--borderRadiusNone {\n      border-radius: 0;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow--borderRadiusMedium {\n      border-radius: 4px;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--transparent {\n      background-color: rgba(0, 0, 0, 0);\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--plain {\n      background-color: #fff;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--subdued {\n      background-color: #fafbfd;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--accent {\n      background-color: #fce7f1;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--primary {\n      background-color: #e6f0f8;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--success {\n      background-color: #e6f2f1;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--warning {\n      background-color: #fef6e6;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--danger {\n      background-color: #f8e9e9;\n    }\n    .euiTable.euiTable--responsive .euiTableRow {\n      box-shadow:\n        0 2px 2px -1px rgba(152, 162, 179, 0.3),\n        0 1px 5px -2px rgba(152, 162, 179, 0.3);\n      background-color: #fff;\n      border-radius: 4px;\n      display: flex;\n      flex-wrap: wrap;\n      padding: 8px;\n      margin-bottom: 8px;\n    }\n    .euiTable.euiTable--responsive .euiTableRow:hover {\n      background-color: #fff;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isExpandable,\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-hasActions {\n      background-image: linear-gradient(\n        to right,\n        rgba(152, 162, 179, 0.1) 0,\n        rgba(152, 162, 179, 0.1) 1px,\n        transparent 1px,\n        transparent 100%\n      );\n      background-size: 40px 100%;\n      background-position-x: right;\n      background-repeat: no-repeat;\n      padding-right: 40px;\n      position: relative;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions {\n      min-width: 0;\n      width: 24px;\n      position: absolute;\n      top: 16px;\n      right: 8px;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander::before,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions::before {\n      display: none;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander\n      .euiTableCellContent,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions\n      .euiTableCellContent {\n      flex-direction: column;\n      padding: 0;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander\n      .euiTableCellContent\n      .euiLink,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions\n      .euiTableCellContent\n      .euiLink {\n      padding: 4px;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander {\n      top: auto;\n      bottom: 16px;\n      right: 0;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isSelectable {\n      padding-left: 36px;\n      position: relative;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isSelectable\n      .euiTableRowCellCheckbox {\n      position: absolute;\n      left: 4px;\n      top: 8px;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isSelected,\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isSelected:hover,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isSelected\n      + .euiTableRow.euiTableRow-isExpandedRow,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isSelected:hover\n      + .euiTableRow.euiTableRow-isExpandedRow\n      .euiTableRowCell {\n      background-color: #eef5fa;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isExpandedRow {\n      background-image: linear-gradient(\n        to right,\n        rgba(152, 162, 179, 0.1) 0,\n        rgba(152, 162, 179, 0.1) 1px,\n        transparent 1px,\n        transparent 100%\n      );\n      background-size: 40px 100%;\n      background-position-x: right;\n      background-repeat: no-repeat;\n      box-shadow:\n        0 2px 2px -1px rgba(152, 162, 179, 0.3),\n        0 1px 5px -2px rgba(152, 162, 179, 0.3);\n      margin-top: -16px;\n      position: relative;\n      z-index: 2;\n      border-top: none;\n      border-top-left-radius: 0;\n      border-top-right-radius: 0;\n      padding-left: 8px;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandedRow:hover {\n      background-color: #fff;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandedRow\n      .euiTableRowCell {\n      width: calc(100% - 40px);\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandedRow\n      .euiTableRowCell::before {\n      display: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell {\n      display: block;\n      min-width: 50%;\n      border: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCellCheckbox {\n      border: none;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow-hasActions\n      .euiTableCellContent--showOnHover\n      > * {\n      margin-left: 0;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow-hasActions\n      .euiTableCellContent--showOnHover\n      .expandedItemActions__completelyHide {\n      display: none;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow-hasActions\n      .euiTableCellContent--showOnHover\n      .euiTableCellContent__hoverItem {\n      opacity: 1;\n      filter: none;\n      margin-left: 0;\n      margin-bottom: 8px;\n    }\n    .euiTable.euiTable--responsive .euiTableCellContent--alignRight {\n      justify-content: flex-start;\n    }\n    .euiTable.euiTable--responsive .euiTableCellContent--alignCenter {\n      justify-content: flex-start;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiTable.euiTable--responsive thead {\n      display: none;\n    }\n    .euiTable.euiTable--responsive tfoot {\n      display: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell__mobileHeader {\n      max-width: 100%;\n      overflow: hidden !important;\n      text-overflow: ellipsis !important;\n      white-space: nowrap !important;\n      word-wrap: normal !important;\n      font-size: 11px;\n      font-size: 0.6875rem;\n      display: block;\n      color: #69707d;\n      padding: 8px;\n      padding-bottom: 0;\n      margin-bottom: -8px;\n      min-height: 24px;\n    }\n    .euiTableRowCell:only-child\n      .euiTable.euiTable--responsive\n      .euiTableRowCell__mobileHeader {\n      min-height: 0;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell--enlargeForMobile {\n      font-size: 16px;\n      font-size: 1rem;\n      line-height: 1.5;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell--isMobileFullWidth {\n      width: 100%;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRowCell--isMobileFullWidth\n      .euiTableCellContent--alignCenter {\n      justify-content: center;\n      text-align: center;\n    }\n    .euiTable.euiTable--responsive .euiTableRow {\n      background-color: #fff;\n      border: 1px solid #d3dae6;\n      border-radius: 4px;\n      flex-grow: 1;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--flexGrowZero {\n      flex-grow: 0;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--noBorder {\n      border: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--hasShadow {\n      box-shadow:\n        0 2px 2px -1px rgba(152, 162, 179, 0.3),\n        0 1px 5px -2px rgba(152, 162, 179, 0.3);\n      border: 1px solid #d3dae6;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--isClickable {\n      transition: all 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow--isClickable:enabled {\n      display: block;\n      width: 100%;\n      text-align: left;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--isClickable:hover,\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--isClickable:focus {\n      box-shadow:\n        0 4px 8px 0 rgba(152, 162, 179, 0.15),\n        0 2px 2px -1px rgba(152, 162, 179, 0.3);\n      transform: translateY(-2px);\n      cursor: pointer;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--borderRadiusNone {\n      border-radius: 0;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow--borderRadiusMedium {\n      border-radius: 4px;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--transparent {\n      background-color: rgba(0, 0, 0, 0);\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--plain {\n      background-color: #fff;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--subdued {\n      background-color: #fafbfd;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--accent {\n      background-color: #fce7f1;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--primary {\n      background-color: #e6f0f8;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--success {\n      background-color: #e6f2f1;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--warning {\n      background-color: #fef6e6;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow--danger {\n      background-color: #f8e9e9;\n    }\n    .euiTable.euiTable--responsive .euiTableRow {\n      box-shadow:\n        0 2px 2px -1px rgba(152, 162, 179, 0.3),\n        0 1px 5px -2px rgba(152, 162, 179, 0.3);\n      background-color: #fff;\n      border-radius: 4px;\n      display: flex;\n      flex-wrap: wrap;\n      padding: 8px;\n      margin-bottom: 8px;\n    }\n    .euiTable.euiTable--responsive .euiTableRow:hover {\n      background-color: #fff;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isExpandable,\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-hasActions {\n      background-image: linear-gradient(\n        to right,\n        rgba(152, 162, 179, 0.1) 0,\n        rgba(152, 162, 179, 0.1) 1px,\n        transparent 1px,\n        transparent 100%\n      );\n      background-size: 40px 100%;\n      background-position-x: right;\n      background-repeat: no-repeat;\n      padding-right: 40px;\n      position: relative;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions {\n      min-width: 0;\n      width: 24px;\n      position: absolute;\n      top: 16px;\n      right: 8px;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander::before,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions::before {\n      display: none;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander\n      .euiTableCellContent,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions\n      .euiTableCellContent {\n      flex-direction: column;\n      padding: 0;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander\n      .euiTableCellContent\n      .euiLink,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions\n      .euiTableRowCell--hasActions\n      .euiTableCellContent\n      .euiLink {\n      padding: 4px;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-hasActions.euiTableRow-isExpandable\n      .euiTableRowCell--isExpander {\n      top: auto;\n      bottom: 16px;\n      right: 0;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isSelectable {\n      padding-left: 36px;\n      position: relative;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isSelectable\n      .euiTableRowCellCheckbox {\n      position: absolute;\n      left: 4px;\n      top: 8px;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isSelected,\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isSelected:hover,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isSelected\n      + .euiTableRow.euiTableRow-isExpandedRow,\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isSelected:hover\n      + .euiTableRow.euiTableRow-isExpandedRow\n      .euiTableRowCell {\n      background-color: #eef5fa;\n    }\n    .euiTable.euiTable--responsive .euiTableRow.euiTableRow-isExpandedRow {\n      background-image: linear-gradient(\n        to right,\n        rgba(152, 162, 179, 0.1) 0,\n        rgba(152, 162, 179, 0.1) 1px,\n        transparent 1px,\n        transparent 100%\n      );\n      background-size: 40px 100%;\n      background-position-x: right;\n      background-repeat: no-repeat;\n      box-shadow:\n        0 2px 2px -1px rgba(152, 162, 179, 0.3),\n        0 1px 5px -2px rgba(152, 162, 179, 0.3);\n      margin-top: -16px;\n      position: relative;\n      z-index: 2;\n      border-top: none;\n      border-top-left-radius: 0;\n      border-top-right-radius: 0;\n      padding-left: 8px;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandedRow:hover {\n      background-color: #fff;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandedRow\n      .euiTableRowCell {\n      width: calc(100% - 40px);\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow.euiTableRow-isExpandedRow\n      .euiTableRowCell::before {\n      display: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCell {\n      display: block;\n      min-width: 50%;\n      border: none;\n    }\n    .euiTable.euiTable--responsive .euiTableRowCellCheckbox {\n      border: none;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow-hasActions\n      .euiTableCellContent--showOnHover\n      > * {\n      margin-left: 0;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow-hasActions\n      .euiTableCellContent--showOnHover\n      .expandedItemActions__completelyHide {\n      display: none;\n    }\n    .euiTable.euiTable--responsive\n      .euiTableRow-hasActions\n      .euiTableCellContent--showOnHover\n      .euiTableCellContent__hoverItem {\n      opacity: 1;\n      filter: none;\n      margin-left: 0;\n      margin-bottom: 8px;\n    }\n    .euiTable.euiTable--responsive .euiTableCellContent--alignRight {\n      justify-content: flex-start;\n    }\n    .euiTable.euiTable--responsive .euiTableCellContent--alignCenter {\n      justify-content: flex-start;\n    }\n  }\n  .euiTableHeaderMobile,\n  .euiTableHeaderCell--hideForDesktop {\n    display: none;\n  }\n  @media only screen and (max-width: 574px) {\n    .euiTableHeaderMobile {\n      display: flex;\n      justify-content: flex-end;\n      padding: 8px 0;\n    }\n    .euiTableSortMobile {\n      display: block;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiTableHeaderMobile {\n      display: flex;\n      justify-content: flex-end;\n      padding: 8px 0;\n    }\n    .euiTableSortMobile {\n      display: block;\n    }\n  }\n  .euiTextDiff del {\n    color: #bd271e;\n  }\n  .euiTextDiff ins {\n    color: #017d73;\n  }\n  .euiTitle + .euiTitle {\n    margin-top: 24px;\n  }\n  .euiTitle--uppercase {\n    text-transform: uppercase;\n  }\n  .euiTitle--xxxsmall {\n    color: #1a1c21;\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n  }\n  .euiTitle--xxsmall {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n  }\n  .euiTitle--xsmall {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n  }\n  .euiTitle--small {\n    color: #1a1c21;\n    font-size: 20px;\n    font-size: 1.25rem;\n    line-height: 2rem;\n    font-weight: 500;\n    letter-spacing: -0.025em;\n  }\n  .euiTitle--medium {\n    color: #1a1c21;\n    font-size: 28px;\n    font-size: 1.75rem;\n    line-height: 2.5rem;\n    font-weight: 300;\n    letter-spacing: -0.04em;\n  }\n  .euiTitle--large {\n    color: #1a1c21;\n    font-size: 36px;\n    font-size: 2.25rem;\n    line-height: 3rem;\n    font-weight: 300;\n    letter-spacing: -0.03em;\n  }\n  .euiGlobalToastList {\n    scrollbar-width: thin;\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n    position: fixed;\n    z-index: 9000;\n    bottom: 0;\n    width: 400px;\n    max-height: 100vh;\n    overflow-y: auto;\n    scrollbar-width: none;\n  }\n  .euiGlobalToastList::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n  .euiGlobalToastList::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n  .euiGlobalToastList::-webkit-scrollbar-corner,\n  .euiGlobalToastList::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n  .euiGlobalToastList::-webkit-scrollbar {\n    width: 0;\n    height: 0;\n  }\n  .euiGlobalToastList:not(:empty) {\n    padding: 16px;\n  }\n  .euiGlobalToastList--right:not(:empty) {\n    right: 0;\n    padding-left: 64px;\n  }\n  .euiGlobalToastList--left:not(:empty) {\n    left: 0;\n    padding-right: 64px;\n  }\n  .euiGlobalToastListItem {\n    margin-bottom: 16px;\n    animation: 250ms euiShowToast cubic-bezier(0.694, 0.0482, 0.335, 1);\n    opacity: 1;\n  }\n  .euiGlobalToastListItem:first-child {\n    margin-top: auto;\n  }\n  .euiGlobalToastListItem:last-child {\n    margin-bottom: 0;\n  }\n  .euiGlobalToastListItem.euiGlobalToastListItem-isDismissed {\n    transition: opacity 250ms;\n    opacity: 0;\n  }\n  @keyframes euiShowToast {\n    from {\n      transform: translateY(24px) scale(0.9);\n      opacity: 0;\n    }\n    to {\n      transform: translateY(0) scale(1);\n      opacity: 1;\n    }\n  }\n  @media only screen and (max-width: 574px) {\n    .euiGlobalToastList:not(:empty) {\n      left: 0;\n      padding-left: 16px;\n      padding-right: 16px;\n      width: 100%;\n    }\n  }\n  @media only screen and (min-width: 575px) and (max-width: 767px) {\n    .euiGlobalToastList:not(:empty) {\n      left: 0;\n      padding-left: 16px;\n      padding-right: 16px;\n      width: 100%;\n    }\n  }\n  .euiToast {\n    border: 1px solid #d3dae6;\n    box-shadow:\n      0 40px 64px 0 rgba(65, 78, 101, 0.1),\n      0 24px 32px 0 rgba(65, 78, 101, 0.1),\n      0 16px 16px 0 rgba(65, 78, 101, 0.1),\n      0 8px 8px 0 rgba(65, 78, 101, 0.1),\n      0 4px 4px 0 rgba(65, 78, 101, 0.1),\n      0 2px 2px 0 rgba(65, 78, 101, 0.1);\n    border-color: #c6cad1;\n    border-top-color: #e3e4e8;\n    border-bottom-color: #aaafba;\n    position: relative;\n    padding: 16px;\n    background-color: #fff;\n    width: 100%;\n  }\n  .euiToast:hover .euiToast__closeButton,\n  .euiToast:focus .euiToast__closeButton {\n    opacity: 1;\n  }\n  .euiToast__closeButton {\n    position: absolute;\n    top: 16px;\n    right: 16px;\n    line-height: 0;\n    appearance: none;\n    opacity: 0;\n    transition: opacity 150ms cubic-bezier(0.694, 0.0482, 0.335, 1);\n  }\n  .euiToast__closeButton svg {\n    fill: #8d8e90;\n  }\n  .euiToast__closeButton:hover svg {\n    fill: #1a1c21;\n  }\n  .euiToast__closeButton:focus {\n    background-color: #e6f0f8;\n    opacity: 1;\n  }\n  .euiToast__closeButton:focus svg {\n    fill: #006bb4;\n  }\n  .euiToast--primary {\n    border-top: 2px solid #006bb4;\n  }\n  .euiToast--success {\n    border-top: 2px solid #017d73;\n  }\n  .euiToast--warning {\n    border-top: 2px solid #c88800;\n  }\n  .euiToast--danger {\n    border-top: 2px solid #bd271e;\n  }\n  .euiToastHeader {\n    padding-right: 24px;\n    display: flex;\n    align-items: baseline;\n  }\n  .euiToastHeader > * + * {\n    margin-left: 8px;\n  }\n  .euiToastHeader__icon {\n    flex: 0 0 auto;\n    fill: #1a1c21;\n    transform: translateY(2px);\n  }\n  .euiToastHeader__title {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n    overflow-wrap: break-word !important;\n    word-wrap: break-word !important;\n    word-break: break-word;\n    font-weight: 300;\n  }\n  .euiToastHeader--withBody {\n    margin-bottom: 8px;\n  }\n  .euiToastBody {\n    word-wrap: break-word;\n  }\n  .euiToken {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n  }\n  .euiToken svg {\n    height: 100%;\n    margin: auto;\n  }\n  .euiToken--circle {\n    border-radius: 50%;\n  }\n  .euiToken--square {\n    border-radius: 3px;\n  }\n  .euiToken--xsmall {\n    width: 12px;\n    height: 12px;\n  }\n  .euiToken--xsmall.euiToken--rectangle {\n    padding: 0 4px;\n  }\n  .euiToken--small {\n    width: 16px;\n    height: 16px;\n  }\n  .euiToken--small.euiToken--rectangle {\n    padding: 0 4px;\n  }\n  .euiToken--medium {\n    width: 24px;\n    height: 24px;\n  }\n  .euiToken--medium.euiToken--rectangle {\n    padding: 0 8px;\n  }\n  .euiToken--large {\n    width: 32px;\n    height: 32px;\n  }\n  .euiToken--large.euiToken--rectangle {\n    padding: 0 8px;\n  }\n  .euiToken--rectangle {\n    box-sizing: content-box;\n  }\n  .euiToken--euiColorVis0 {\n    color: #54b399;\n  }\n  .euiToken--euiColorVis0.euiToken--light {\n    color: #387765;\n    background-color: #eef7f5;\n    box-shadow: inset 0 0 0 1px #cce8e0;\n  }\n  .euiToken--euiColorVis0.euiToken--dark {\n    background-color: #6dccb1;\n    color: #000;\n  }\n  .euiToken--euiColorVis1 {\n    color: #6092c0;\n  }\n  .euiToken--euiColorVis1.euiToken--light {\n    color: #4a7194;\n    background-color: #eff4f9;\n    box-shadow: inset 0 0 0 1px #cfdeec;\n  }\n  .euiToken--euiColorVis1.euiToken--dark {\n    background-color: #79aad9;\n    color: #000;\n  }\n  .euiToken--euiColorVis2 {\n    color: #d36086;\n  }\n  .euiToken--euiColorVis2.euiToken--light {\n    color: #ac4e6d;\n    background-color: #fbeff3;\n    box-shadow: inset 0 0 0 1px #f2cfdb;\n  }\n  .euiToken--euiColorVis2.euiToken--dark {\n    background-color: #ee789d;\n    color: #000;\n  }\n  .euiToken--euiColorVis3 {\n    color: #9170b8;\n  }\n  .euiToken--euiColorVis3.euiToken--light {\n    color: #7c609e;\n    background-color: #f4f1f8;\n    box-shadow: inset 0 0 0 1px #ded4ea;\n  }\n  .euiToken--euiColorVis3.euiToken--dark {\n    background-color: #a987d1;\n    color: #000;\n  }\n  .euiToken--euiColorVis4 {\n    color: #ca8eae;\n  }\n  .euiToken--euiColorVis4.euiToken--light {\n    color: #8d647a;\n    background-color: #faf4f7;\n    box-shadow: inset 0 0 0 1px #efdde7;\n  }\n  .euiToken--euiColorVis4.euiToken--dark {\n    background-color: #e4a6c7;\n    color: #000;\n  }\n  .euiToken--euiColorVis5 {\n    color: #d6bf57;\n  }\n  .euiToken--euiColorVis5.euiToken--light {\n    color: #807234;\n    background-color: #fbf9ee;\n    box-shadow: inset 0 0 0 1px #f3eccd;\n  }\n  .euiToken--euiColorVis5.euiToken--dark {\n    background-color: #f1d86f;\n    color: #000;\n  }\n  .euiToken--euiColorVis6 {\n    color: #b9a888;\n  }\n  .euiToken--euiColorVis6.euiToken--light {\n    color: #7b705a;\n    background-color: #f8f6f3;\n    box-shadow: inset 0 0 0 1px #eae5db;\n  }\n  .euiToken--euiColorVis6.euiToken--dark {\n    background-color: #d2c0a0;\n    color: #000;\n  }\n  .euiToken--euiColorVis7 {\n    color: #da8b45;\n  }\n  .euiToken--euiColorVis7.euiToken--light {\n    color: #996130;\n    background-color: #fbf3ec;\n    box-shadow: inset 0 0 0 1px #f4dcc7;\n  }\n  .euiToken--euiColorVis7.euiToken--dark {\n    background-color: #f5a35c;\n    color: #000;\n  }\n  .euiToken--euiColorVis8 {\n    color: #aa6556;\n  }\n  .euiToken--euiColorVis8.euiToken--light {\n    color: #9a5b4e;\n    background-color: #f7f0ee;\n    box-shadow: inset 0 0 0 1px #e6d1cc;\n  }\n  .euiToken--euiColorVis8.euiToken--dark {\n    background-color: #c47c6c;\n    color: #000;\n  }\n  .euiToken--euiColorVis9 {\n    color: #e7664c;\n  }\n  .euiToken--euiColorVis9.euiToken--light {\n    color: #b34f3b;\n    background-color: #fdf0ed;\n    box-shadow: inset 0 0 0 1px #f8d1c9;\n  }\n  .euiToken--euiColorVis9.euiToken--dark {\n    background-color: #ff7e62;\n    color: #000;\n  }\n  .euiToken--gray {\n    color: #69707d;\n  }\n  .euiToken--gray.euiToken--light {\n    color: #646a77;\n    background-color: #f0f1f2;\n    box-shadow: inset 0 0 0 1px #d2d4d8;\n  }\n  .euiToken--gray.euiToken--dark {\n    background-color: #69707d;\n    color: #fff;\n  }\n  .euiTour--minWidth-default {\n    min-width: 240px;\n  }\n  .euiTourHeader {\n    border-bottom: none;\n    margin-bottom: 8px !important;\n  }\n  .euiTourHeader .euiTourHeader__title {\n    margin-top: 0;\n  }\n  .euiTourHeader__subtitle {\n    color: #69707d;\n  }\n  .euiTourFooter {\n    background-color: #f5f7fa;\n    margin-top: 24px !important;\n  }\n  .euiTour .euiTour__beacon {\n    pointer-events: none;\n    position: absolute;\n    opacity: 0;\n    transition: opacity 0s 350ms;\n  }\n  .euiTour\n    .euiPopover__panelArrow.euiPopover__panelArrow--right\n    .euiTour__beacon {\n    opacity: 1;\n    top: 6px;\n    left: -24px;\n  }\n  .euiTour\n    .euiPopover__panelArrow.euiPopover__panelArrow--left\n    .euiTour__beacon {\n    opacity: 1;\n    top: 6px;\n    left: 12px;\n  }\n  .euiTour .euiPopover__panelArrow.euiPopover__panelArrow--top:after {\n    border-top-color: #f5f7fa;\n  }\n  .euiTour\n    .euiPopover__panelArrow.euiPopover__panelArrow--top\n    .euiTour__beacon {\n    opacity: 1;\n    top: 12px;\n    left: 6px;\n  }\n  .euiTour\n    .euiPopover__panelArrow.euiPopover__panelArrow--bottom\n    .euiTour__beacon {\n    opacity: 1;\n    top: -24px;\n    left: 6px;\n  }\n  .euiTourStepIndicator {\n    display: inline-block;\n  }\n  .euiText {\n    color: #343741;\n    font-weight: 400;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5;\n    color: inherit;\n    clear: both;\n    line-height: 1.5rem;\n  }\n  .euiText a:not([class]) {\n    color: #006bb4;\n  }\n  .euiText a:not([class]):hover,\n  .euiText a:not([class]):focus {\n    color: #004d81;\n    text-decoration: underline;\n  }\n  .euiText a:not([class]):focus {\n    background-color: #e6f0f8;\n    animation: 350ms cubic-bezier(0.694, 0.0482, 0.335, 1) 1 normal forwards\n      focusRingAnimate !important;\n  }\n  .euiText img {\n    display: block;\n    width: 100%;\n  }\n  .euiText ul {\n    list-style: disc;\n  }\n  .euiText ol {\n    list-style: decimal;\n  }\n  .euiText blockquote {\n    position: relative;\n    text-align: center;\n    margin-left: auto;\n    margin-right: auto;\n    font-family:\n      Georgia,\n      Times,\n      Times New Roman,\n      serif;\n    font-style: italic;\n    letter-spacing: normal;\n  }\n  .euiText blockquote p:last-child {\n    margin-bottom: 0;\n  }\n  .euiText blockquote:before,\n  .euiText blockquote:after {\n    position: absolute;\n    content: '';\n    height: 2px;\n    width: 50%;\n    right: 0;\n    transform: translateX(-50%);\n    background: #69707d;\n  }\n  .euiText blockquote:before {\n    top: 0;\n  }\n  .euiText blockquote:after {\n    bottom: 0;\n  }\n  .euiText h1 {\n    color: #1a1c21;\n    font-size: 36px;\n    font-size: 2.25rem;\n    line-height: 3rem;\n    font-weight: 300;\n    letter-spacing: -0.03em;\n  }\n  .euiText h2 {\n    color: #1a1c21;\n    font-size: 28px;\n    font-size: 1.75rem;\n    line-height: 2.5rem;\n    font-weight: 300;\n    letter-spacing: -0.04em;\n  }\n  .euiText h3 {\n    color: #1a1c21;\n    font-size: 20px;\n    font-size: 1.25rem;\n    line-height: 2rem;\n    font-weight: 500;\n    letter-spacing: -0.025em;\n  }\n  .euiText h4,\n  .euiText dt {\n    color: #1a1c21;\n    font-size: 16px;\n    font-size: 1rem;\n    line-height: 1.5rem;\n    font-weight: 600;\n    letter-spacing: -0.02em;\n  }\n  .euiText h5 {\n    color: #1a1c21;\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n  }\n  .euiText h6 {\n    color: #1a1c21;\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.5rem;\n    font-weight: 700;\n    text-transform: uppercase;\n  }\n  .euiText pre {\n    white-space: pre-wrap;\n    background: #f5f7fa;\n    color: #343741;\n  }\n  .euiText pre,\n  .euiText pre code {\n    display: block;\n  }\n  .euiText code {\n    font-family: 'Roboto Mono', Consolas, Menlo, Courier, monospace;\n    letter-spacing: normal;\n    display: inline-block;\n    font-weight: 400;\n  }\n  .euiText p,\n  .euiText ul,\n  .euiText ol,\n  .euiText dl,\n  .euiText blockquote,\n  .euiText img,\n  .euiText pre {\n    margin-bottom: 1.5rem;\n  }\n  .euiText ul,\n  .euiText ol {\n    margin-left: 1.5rem;\n  }\n  .euiText blockquote {\n    padding: 1.5rem;\n    font-size: 1.125rem;\n  }\n  .euiText h1,\n  .euiText h2,\n  .euiText h3,\n  .euiText h4,\n  .euiText h5,\n  .euiText h6 {\n    margin-bottom: 0.5rem;\n  }\n  .euiText dd + dt {\n    margin-top: 1rem;\n  }\n  .euiText * + h2,\n  .euiText * + h3,\n  .euiText * + h4,\n  .euiText * + h5,\n  .euiText * + h6 {\n    margin-top: 2rem;\n  }\n  .euiText h1 {\n    font-size: 2.25rem;\n    line-height: 3rem;\n  }\n  .euiText h2 {\n    font-size: 1.75rem;\n    line-height: 2.5rem;\n  }\n  .euiText h3 {\n    font-size: 1.25rem;\n    line-height: 2rem;\n  }\n  .euiText h4,\n  .euiText dt,\n  .euiText .eui-definitionListReverse dd {\n    font-size: 1rem;\n    line-height: 1.5rem;\n  }\n  .euiText .eui-definitionListReverse dt {\n    font-size: 0.75rem;\n    color: #343741;\n  }\n  .euiText h5 {\n    font-size: 0.875rem;\n    line-height: 1rem;\n  }\n  .euiText h6 {\n    font-size: 0.75rem;\n    line-height: 1rem;\n  }\n  .euiText small {\n    font-size: 0.875rem;\n  }\n  .euiText pre {\n    padding: 16px;\n  }\n  .euiText code {\n    font-size: 0.9rem;\n  }\n  .euiText.euiText--constrainedWidth {\n    max-width: 36em;\n    min-width: 75%;\n  }\n  .euiText > :last-child,\n  .euiText .euiTextColor > :last-child {\n    margin-bottom: 0 !important;\n  }\n  .euiText--small {\n    font-size: 14px;\n    font-size: 0.875rem;\n    line-height: 1.3125rem;\n  }\n  .euiText--small p,\n  .euiText--small ul,\n  .euiText--small ol,\n  .euiText--small dl,\n  .euiText--small blockquote,\n  .euiText--small img,\n  .euiText--small pre {\n    margin-bottom: 1.3125rem;\n  }\n  .euiText--small ul,\n  .euiText--small ol {\n    margin-left: 1.3125rem;\n  }\n  .euiText--small blockquote {\n    padding: 1.3125rem;\n    font-size: 0.984375rem;\n  }\n  .euiText--small h1,\n  .euiText--small h2,\n  .euiText--small h3,\n  .euiText--small h4,\n  .euiText--small h5,\n  .euiText--small h6 {\n    margin-bottom: 0.4375rem;\n  }\n  .euiText--small dd + dt {\n    margin-top: 0.875rem;\n  }\n  .euiText--small * + h2,\n  .euiText--small * + h3,\n  .euiText--small * + h4,\n  .euiText--small * + h5,\n  .euiText--small * + h6 {\n    margin-top: 1.75rem;\n  }\n  .euiText--small h1 {\n    font-size: 1.96875rem;\n    line-height: 2.625rem;\n  }\n  .euiText--small h2 {\n    font-size: 1.53125rem;\n    line-height: 2.1875rem;\n  }\n  .euiText--small h3 {\n    font-size: 1.09375rem;\n    line-height: 1.75rem;\n  }\n  .euiText--small h4,\n  .euiText--small dt,\n  .euiText--small .eui-definitionListReverse dd {\n    font-size: 0.875rem;\n    line-height: 1.3125rem;\n  }\n  .euiText--small .eui-definitionListReverse dt {\n    font-size: 0.65625rem;\n    color: #343741;\n  }\n  .euiText--small h5 {\n    font-size: 0.765625rem;\n    line-height: 0.875rem;\n  }\n  .euiText--small h6 {\n    font-size: 0.65625rem;\n    line-height: 0.875rem;\n  }\n  .euiText--small small {\n    font-size: 0.765625rem;\n  }\n  .euiText--small pre {\n    padding: 14px;\n  }\n  .euiText--small code {\n    font-size: 0.7875rem;\n  }\n  .euiText--extraSmall {\n    font-size: 12px;\n    font-size: 0.75rem;\n    line-height: 1.125rem;\n  }\n  .euiText--extraSmall p,\n  .euiText--extraSmall ul,\n  .euiText--extraSmall ol,\n  .euiText--extraSmall dl,\n  .euiText--extraSmall blockquote,\n  .euiText--extraSmall img,\n  .euiText--extraSmall pre {\n    margin-bottom: 1.125rem;\n  }\n  .euiText--extraSmall ul,\n  .euiText--extraSmall ol {\n    margin-left: 1.125rem;\n  }\n  .euiText--extraSmall blockquote {\n    padding: 1.125rem;\n    font-size: 0.84375rem;\n  }\n  .euiText--extraSmall h1,\n  .euiText--extraSmall h2,\n  .euiText--extraSmall h3,\n  .euiText--extraSmall h4,\n  .euiText--extraSmall h5,\n  .euiText--extraSmall h6 {\n    margin-bottom: 0.375rem;\n  }\n  .euiText--extraSmall dd + dt {\n    margin-top: 0.75rem;\n  }\n  .euiText--extraSmall * + h2,\n  .euiText--extraSmall * + h3,\n  .euiText--extraSmall * + h4,\n  .euiText--extraSmall * + h5,\n  .euiText--extraSmall * + h6 {\n    margin-top: 1.5rem;\n  }\n  .euiText--extraSmall h1 {\n    font-size: 1.6875rem;\n    line-height: 2.25rem;\n  }\n  .euiText--extraSmall h2 {\n    font-size: 1.3125rem;\n    line-height: 1.875rem;\n  }\n  .euiText--extraSmall h3 {\n    font-size: 0.9375rem;\n    line-height: 1.5rem;\n  }\n  .euiText--extraSmall h4,\n  .euiText--extraSmall dt,\n  .euiText--extraSmall .eui-definitionListReverse dd {\n    font-size: 0.75rem;\n    line-height: 1.125rem;\n  }\n  .euiText--extraSmall .eui-definitionListReverse dt {\n    font-size: 0.5625rem;\n    color: #343741;\n  }\n  .euiText--extraSmall h5 {\n    font-size: 0.65625rem;\n    line-height: 0.75rem;\n  }\n  .euiText--extraSmall h6 {\n    font-size: 0.5625rem;\n    line-height: 0.75rem;\n  }\n  .euiText--extraSmall small {\n    font-size: 0.65625rem;\n  }\n  .euiText--extraSmall pre {\n    padding: 12px;\n  }\n  .euiText--extraSmall code {\n    font-size: 0.675rem;\n  }\n  .euiTextColor--default {\n    color: #343741;\n  }\n  .euiTextColor--subdued {\n    color: #6a717d;\n  }\n  .euiTextColor--secondary {\n    color: #017d73;\n  }\n  .euiTextColor--success {\n    color: #017d73;\n  }\n  .euiTextColor--accent {\n    color: #dd0a73;\n  }\n  .euiTextColor--warning {\n    color: #9b6900;\n  }\n  .euiTextColor--danger {\n    color: #bd271e;\n  }\n  .euiTextColor--ghost {\n    color: #717171;\n    color: #fff !important;\n  }\n  .euiTextAlign--left {\n    text-align: left;\n  }\n  .euiTextAlign--right {\n    text-align: right;\n  }\n  .euiTextAlign--center {\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/globalStyles.ts",
    "content": "import { createGlobalStyle } from 'styled-components'\nimport { Theme } from 'uiSrc/components/base/theme/types'\n\n/**\n * Global styles for the application\n * These styles are applied to the entire application using styled-components\n */\nexport const GlobalStyles = createGlobalStyle<{ theme: Theme }>`\n  :root {\n    // todo: replace with theme colors at some point\n    /* Type colors for Redis data types */\n    --typeHashColor: #364cff;\n    --typeListColor: #008556;\n    --typeSetColor: #9c5c2b;\n    --typeZSetColor: #a00a6b;\n    --typeStringColor: #6a1dc3;\n    --typeReJSONColor: #3f4b5f;\n    --typeStreamColor: #6a741b;\n    --typeGraphColor: #14708d;\n    --typeTimeSeriesColor: #6e6e6e;\n\n    /* Group colors for Redis command groups */\n    --groupSortedSetColor: #a00a6b;\n    --groupBitmapColor: #3f4b5f;\n    --groupClusterColor: #6e6e6e;\n    --groupConnectionColor: #bf1046;\n    --groupGeoColor: #344e36;\n    --groupGenericColor: #4a2923;\n    --groupPubSubColor: #14365d;\n    --groupScriptingColor: #5d141c;\n    --groupTransactionsColor: #14708d;\n    --groupServerColor: #000000;\n    --groupHyperLolLogColor: #3f4b5f;\n\n    /* Default type color */\n    --defaultTypeColor: #aa4e4e;\n  }\n\n  .text-uppercase {\n    text-transform: uppercase;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/styles/main.scss",
    "content": "@import 'base/react_virtualized';\n@import 'base/base';\n@import 'base/helpers';\n@import 'base/typography';\n@import 'base/inputs';\n@import 'base/selects';\n@import 'base/functions';\n@import 'base/flex_groups';\n@import 'base/overrides';\n\n@import 'components/components';\n"
  },
  {
    "path": "redisinsight/ui/src/styles/main_plugin.scss",
    "content": "// Global styling\n@import \"./elastic\";\n\n@import \"base/react_virtualized\";\n@import \"base/base\";\n@import \"base/helpers\";\n@import \"base/typography\";\n@import \"base/inputs\";\n@import \"base/selects\";\n@import \"base/functions\";\n@import \"base/flex_groups\";\n@import \"base/overrides\";\n\n@import \"components/components\";\n\n// relative path for import fonts to plugins static\n@font-face {\n  font-family: \"SourceCodePro\";\n  src: url(\"./fonts/SourceCodePro-Regular.ttf\") format(\"truetype\");\n}\n@font-face {\n  font-family: \"Inconsolata\";\n  src: url(\"./fonts/Inconsolata-Regular.ttf\") format(\"truetype\");\n}\n\n@font-face {\n  font-family: \"Inconsolata\";\n  font-weight: bold;\n  src: url(\"./fonts/Inconsolata-Bold.ttf\") format(\"truetype\");\n}\n@font-face {\n  font-family: \"Graphik\";\n  font-weight: 300;\n  src: url(\"./fonts/Graphik-Light.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  font-weight: 300;\n  font-style: italic;\n  src: url(\"./fonts/Graphik-LightItalic.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  src: url(\"./fonts/Graphik-Regular.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  font-style: italic;\n  src: url(\"./fonts/Graphik-RegularItalic.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  font-weight: 500;\n  src: url(\"./fonts/Graphik-Medium.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  font-weight: 500;\n  font-style: italic;\n  src: url(\"./fonts/Graphik-MediumItalic.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  font-weight: 600;\n  src: url(\"./fonts/Graphik-Semibold.woff2\") format(\"woff2\");\n}\n\n@font-face {\n  font-family: \"Graphik\";\n  font-weight: 600;\n  font-style: italic;\n  src: url(\"./fonts/Graphik-SemiboldItalic.woff2\") format(\"woff2\");\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/mixins/_eui.scss",
    "content": "$euiBreakpoints: (\n  \"xs\": 0,\n  \"s\": 575px,\n  \"m\": 768px,\n  \"l\": 992px,\n  \"xl\": 1200px\n) !default;\n\n$euiBreakpointKeys: map-keys($euiBreakpoints);\n// Set scroll bar appearance on Chrome (and firefox).\n@mixin scrollBar() {\n  // Firefox's scrollbar coloring cascades, but the sizing does not,\n  // so it's being added to this mixin for allowing support wherever custom scrollbars are\n  // sass-lint:disable-block no-misspelled-properties\n  scrollbar-width: thin;\n\n  // sass-lint:disable-block no-vendor-prefixes\n  &::-webkit-scrollbar {\n    width: 16px;\n    height: 16px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background-color: var(--scrollBackgroundColor);\n    border: 6px solid transparent;\n    background-clip: content-box;\n  }\n\n  &::-webkit-scrollbar-corner,\n  &::-webkit-scrollbar-track {\n    background-color: transparent;\n  }\n\n  @content;\n}\n\n// This file makes use of double quotes to format errors with nested single quotes\n// The indentation linting freaks out on comments above else statements and is disabled.\n\n// sass-lint:disable quotes, no-warn, indentation\n\n// A sem-complicated mixin for breakpoints, that takes any number of\n// named breakpoints that exists in $euiBreakpoints.\n\n@mixin euiBreakpoint($sizes...) {\n  // Loop through each size parameter\n  @each $size in $sizes {\n    // Store the location of the size in the list to check against\n    $index: index($euiBreakpointKeys, $size);\n\n    // Check to make sure it exists in the allowed breakpoint names\n    @if ($index) {\n      // Set the min size to the value of the size\n      $minSize: map-get($euiBreakpoints, $size);\n\n      // If it is the last item, don't give it a max-width\n      @if ($index == length($euiBreakpointKeys)) {\n        @media only screen and (min-width: $minSize) {\n          @content;\n        }\n        // If it's not the last item, add a max-width\n      } @else {\n        // Set the max size to the value of the next size (-1px as to not overlap)\n        $maxSize: map-get($euiBreakpoints, nth($euiBreakpointKeys, $index + 1)) - 1px;\n\n        // If it's the the first item, don't set a min-width\n        @if ($index == 1) {\n          @media only screen and (max-width: $maxSize) {\n            @content;\n          }\n          // Otherwise it should have a min and max width\n        } @else {\n          @media only screen and (min-width: $minSize) and (max-width: $maxSize) {\n            @content;\n          }\n        }\n      }\n      // If it's not a known breakpoint, throw a warning\n    } @else {\n      @warn \"euiBreakpoint(): '#{$size}' is not a valid size in $euiBreakpoints. Accepted values are '#{$euiBreakpointKeys}'\";\n    }\n  }\n}\n\n@mixin transparent($color, $factor) {\n  $alpha: 1 - $factor;\n  color: rgba($color, $alpha);\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/mixins/_global.scss",
    "content": "@mixin insights-open($max-width: 1440px) {\n  :global(.insightsOpen) {\n    @media only screen and (max-width: $max-width) {\n      @content;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/mixins/index.ts",
    "content": "export {\n  scrollbarStyles,\n  breakpoint,\n  breakpoints,\n  insightsOpen,\n  truncateText,\n} from './styledComponents'\nexport type { BreakpointKey } from './styledComponents'\n"
  },
  {
    "path": "redisinsight/ui/src/styles/mixins/styledComponents.ts",
    "content": "import { css, CSSObject, FlattenSimpleInterpolation } from 'styled-components'\n\n/**\n * Breakpoint values matching EUI breakpoints\n * Equivalent to $euiBreakpoints in SCSS\n */\nexport const breakpoints = {\n  xs: 0,\n  s: 575,\n  m: 768,\n  l: 992,\n  xl: 1200,\n} as const\n\nexport type BreakpointKey = keyof typeof breakpoints\n\n/**\n * Media query helper for breakpoints\n * Equivalent to @include eui.euiBreakpoint() in SCSS\n *\n * @param sizes - One or more breakpoint keys\n * @returns A function that takes template literals and returns styled-components CSS\n *\n * @example\n * ```typescript\n * const Container = styled.div`\n *   padding: 10px;\n *\n *   // For xs and s screens only\n *   ${breakpoint('xs', 's')`\n *     padding: 5px;\n *   `}\n *\n *   // For m, l, and xl screens\n *   ${breakpoint('m', 'l', 'xl')`\n *     padding: 20px;\n *   `}\n * `\n * ```\n */\nexport const breakpoint = (...sizes: BreakpointKey[]) => {\n  return (\n    strings: TemplateStringsArray,\n    ...interpolations: Array<\n      string | number | FlattenSimpleInterpolation | CSSObject\n    >\n  ) => {\n    const content = css(strings, ...interpolations)\n    const breakpointKeys = Object.keys(breakpoints) as BreakpointKey[]\n\n    return sizes.map((size) => {\n      const index = breakpointKeys.indexOf(size)\n\n      if (index === -1) {\n        console.warn(\n          `breakpoint(): '${size}' is not a valid breakpoint. Valid breakpoints are: ${breakpointKeys.join(', ')}`,\n        )\n        return ''\n      }\n\n      const minSize = breakpoints[size]\n\n      // If it's the last breakpoint, don't set max-width\n      if (index === breakpointKeys.length - 1) {\n        return css`\n          @media only screen and (min-width: ${minSize}px) {\n            ${content}\n          }\n        `\n      }\n\n      // If it's the first breakpoint (xs), only set max-width\n      if (index === 0) {\n        const nextKey = breakpointKeys[index + 1]\n        const maxSize = breakpoints[nextKey] - 1\n        return css`\n          @media only screen and (max-width: ${maxSize}px) {\n            ${content}\n          }\n        `\n      }\n\n      // Otherwise, set both min and max width\n      const nextKey = breakpointKeys[index + 1]\n      const maxSize = breakpoints[nextKey] - 1\n      return css`\n        @media only screen and (min-width: ${minSize}px) and (max-width: ${maxSize}px) {\n          ${content}\n        }\n      `\n    })\n  }\n}\n\n/**\n * Insights panel open state responsive mixin\n * Equivalent to @include global.insights-open() in SCSS\n *\n * @param maxWidth - Maximum width for the media query (default: 1440px)\n * @returns A function that takes template literals and returns styled-components CSS\n *\n * @example\n * ```typescript\n * const ControlsIcon = styled(RiIcon)`\n *   margin-left: 3px;\n *\n *   ${insightsOpen(1440)`\n *     width: 18px !important;\n *     height: 18px !important;\n *   `}\n * `\n *\n * // With custom max-width\n * const Promo = styled.div`\n *   display: flex;\n *\n *   ${insightsOpen(1350)`\n *     display: none;\n *   `}\n * `\n * ```\n */\nexport const insightsOpen = (maxWidth: number = 1440) => {\n  return (\n    strings: TemplateStringsArray,\n    ...interpolations: Array<\n      string | number | FlattenSimpleInterpolation | CSSObject\n    >\n  ) => {\n    const content = css(strings, ...interpolations)\n    return css`\n      :global(.insightsOpen) {\n        @media only screen and (max-width: ${maxWidth}px) {\n          ${content}\n        }\n      }\n    `\n  }\n}\n\n/**\n * Scrollbar styling mixin for styled-components\n * Equivalent to @include eui.scrollBar() in SCSS\n *\n * @param width - The width of the scrollbar (default: 16px)\n * @returns CSS template with scrollbar styling\n *\n * @example\n * ```typescript\n * const Container = styled.div`\n *   ${scrollbarStyles()}\n *   height: 100%;\n * `\n *\n * // With custom width\n * const ThinContainer = styled.div`\n *   ${scrollbarStyles(12)}\n *   height: 100%;\n * `\n * ```\n */\nexport const scrollbarStyles = (width: number = 16) => css`\n  overflow-y: auto;\n  overflow-x: hidden;\n  scrollbar-width: thin;\n\n  &::-webkit-scrollbar {\n    width: ${width}px;\n    height: ${width}px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background-color: rgba(105, 112, 125, 0.5);\n    border: 6px solid rgba(0, 0, 0, 0);\n    background-clip: content-box;\n  }\n\n  &::-webkit-scrollbar-corner,\n  &::-webkit-scrollbar-track {\n    background-color: rgba(0, 0, 0, 0);\n  }\n`\n\n/**\n * Text truncation with ellipsis mixin\n * Equivalent to truncate pattern found in various components\n *\n * @returns CSS template with text truncation styling\n *\n * @example\n * ```typescript\n * const Label = styled.span`\n *   ${truncateText}\n *   max-width: 200px;\n * `\n *\n * // Can also be used inline\n * const Title = styled.h1`\n *   font-size: 24px;\n *   ${truncateText}\n * `\n * ```\n */\nexport const truncateText = css`\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  width: 100%;\n  & > div,\n  & > span,\n  & > p {\n    max-width: 100%;\n  }\n`\n"
  },
  {
    "path": "redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss",
    "content": "// elastic eui colors\n$euiColorGhost: #fff;\n$euiColorInk: #000;\n\n// Core\n$euiColorAccent: #f990c0;\n\n// Grays\n$euiColorDarkestShade: #d4dae5;\n$euiColorFullShade: #fff;\n\n// Backgrounds\n$euiColorHighlight: #2e2d25;\n\n// Variations from core\n$euiTextColor: #dfe5ef;\n$euiTitleColor: $euiTextColor;\n\n// Shadows\n$euiShadowColor: #000;\n$euiShadowColorLarge: #000;\n$euiColorDisabled: #434548;\n\n// Contrasty text variants\n$euiColorAccentText: #f990c0;\n$euiColorDisabledText: #4c4e51;\n\n//Charts\n$euiColorChartLines: #343741;\n$euiColorChartBand: #2a2b33;\n\n$euiColorPrimary: #8ba2ff;\n$euiColorPrimaryRGB: 139, 162, 255;\n$euiColorPrimaryText: #ffffff;\n$euiLinkColor: $euiColorPrimaryText;\n$euiColorSecondary: #465282;\n$euiColorSecondaryText: #ffffff;\n$euiColorSuccess: #8ba2ff;\n$euiColorSuccessText: #8ba2ff;\n$euiColorDanger: #ff6280;\n$euiColorDangerText: #ff6280;\n$euiColorWarning: #b12d26;\n$euiColorWarningText: #ce4841;\n$euiColorWarningLight: #d8ab52;\n$euiTextSubduedColor: #b5b6c0;\n$euiTextSubduedColorHover: #dfe5ef;\n$euiPageBackgroundColor: #121212;\n$euiTooltipBackgroundColor: #333d4f;\n$euiTooltipTextColor: #ffffff;\n$euiTooltipTextSecondColor: #e4e9f1;\n$euiTooltipTitleTextColor: #ffffff;\n$euiColorLightShade: #383838;\n$euiColorLightestShade: #2b2b2b;\n$euiColorEmptyShade: #202020;\n$euiBreadcrumbActive: #dfe5ef;\n$euiToastBackgroundColor: #333d4f;\n$euiToastSuccessBtnColor: #637084;\n$euiToastSuccessBorderColor: #7989a3;\n$euiToastDangerBtnColor: #ce4841;\n$euiToastDangerBorderColor: #e8524a;\n$euiToastDangerBgColor: #9e2f29;\n$euiColorMediumShade: #898a90;\n$euiColorGhost: #ffffff;\n\n$euiColorDarkShade: #aaa;\n$euiButtonIconPrimary: $euiColorPrimary;\n$euiButtonIconSecondary: $euiColorSecondary;\n\n$htmlColor: #dfe5ef;\n$navBackgroundColor: #0f1633;\n$externalLinkColor: #8ba2ff;\n$externalLinkTooltipColor: $externalLinkColor;\n$linkToastColor: $externalLinkColor;\n$defaultGreenColor: #608b4e;\n$insightsTriggerBgColor: #292f47;\n$scrollBackgroundColor: rgba(105, 112, 125, 0.5);\n\n$tableRowHoverColor: #070707;\n$tableRowSelectedColor: #212536;\n$tableLightestBorderColor: #707070;\n$tableLightBorderColor: #2b2b2b;\n$tableDarkestBorderColor: #2c2c2c;\n\n$browserTableRowEven: #171717;\n$browserViewTypePassive: #171717;\n$browserComponentActive: #002f47;\n$browserTreeNodeOpen: #2b2f40;\n\n$inputTextColor: #dfe5ef;\n$inputDisabledBackgroundColor: $euiColorEmptyShade;\n$inputPlaceholderColor: #898a90;\n$controlsBoxShadowColor: rgba(0, 0, 0, 0.45);\n$controlsBorderColor: #6b6b6b;\n$controlsLabelColor: #b5b6c0;\n$iconsDefaultColor: #b5b6c0;\n$iconsDefaultHoverColor: #dfe5ef;\n$separatorColor: #3d3d3d;\n$separatorColorLight: #555555;\n$separatorNavigationColor: #465282;\n$separatorDropdownColor: #8b90a3;\n\n$buttonSecondaryHoverColor: #364da8;\n$buttonSecondaryTextColor: #dfe5ef;\n$buttonSecondaryDisabledTextColor: #8b90a3;\n$buttonDangerHoverColor: #e8524a;\n$buttonIconPrimaryHover: #364da8;\n$buttonWarningColor: #9e2f29;\n$buttonWarningHoverColor: #b00c03;\n$buttonDangerToastColor: #e8524a;\n$buttonDangerToastHoverColor: #bf3932;\n$buttonGuideBgColor: #2b2b2b;\n$buttonSuccessColor: #5bc69b;\n$buttonDarkenTextColor: #ffffff;\n$buttonDarkenBgColor: #292f47;\n\n$comboBoxBadgeBgColor: #363636;\n$loadingContentColor: #313131;\n\n$hoverInListColor: #070707;\n$hoverInListColorLight: #465282;\n$hoverInListColorDarken: #292f47;\n$textColorShade: #b5b6c0;\n$cliOutputResponseColor: #ffffff;\n$cliOutputResponseFailColor: #e06c75;\n\n$badgeBackgroundColor: #707070;\n$commandGroupBadgeColor: #3f4b5f;\n$tooltipLightBgColor: #455064;\n\n$overlayPromoNYColor: #0000001a;\n\n$monacoBgColor: #111;\n$highlightDotColor: #2bbbb2;\n$monacoParamsColor: #ec85aa;\n\n$successBorderColor: #13a450;\n$warningBorderColor: #9d6901;\n$errorBorderColor: #ad0017;\n\n$overlayMaskBgColor: rgba(56, 56, 56, 0.66);\n\n// Types colors\n$typeHashColor: #364cff;\n$typeListColor: #008556;\n$typeSetColor: #9c5c2b;\n$typeZSetColor: #a00a6b;\n$typeStringColor: #6a1dc3;\n$typeReJSONColor: #3f4b5f;\n$typeStreamColor: #6a741b;\n$typeGraphColor: #14708d;\n$typeTimeSeriesColor: #6e6e6e;\n$groupSortedSetColor: #a00a6b;\n$groupBitmapColor: #3f4b5f;\n$groupClusterColor: #6e6e6e;\n$groupConnectionColor: #bf1046;\n$groupGeoColor: #344e36;\n$groupGenericColor: #4a2923;\n$groupPubSubColor: #14365d;\n$groupScriptingColor: #5d141c;\n$groupTransactionsColor: #14708d;\n$groupServerColor: #000000;\n$groupHyperLolLogColor: #3f4b5f;\n$defaultTypeColor: #aa4e4e;\n\n// JSON colors\n$jsonKeyNameColor: #c678dd;\n$jsonKeyNameArrayColor: #5da8e5;\n$jsonStringColor: #98c379;\n$jsonNumberColor: #5da8e5;\n$jsonBooleanColor: #d19a66;\n$jsonNullColor: #c6ccd8;\n$jsonNonStringPrimitiveColor: #6b9fff;\n\n// RediSearch colors\n$rsInputColor: #000;\n$rsInputWrapperColor: #202020;\n$rsSubmitBtn: #1ae26e;\n\n// Workbench\n$wbRunResultsBg: #000;\n$wbHoverIconColor: #ffffff;\n$wbActiveIconColor: #8ba2ff;\n$wbTextColor: #dfe5ef;\n\n// PubSub\n$pubSubClientsBadge: #008000;\n\n// Database analysis\n$badgeIconColor: #d8ab52;\n$recommendationBorderColor: #363636;\n\n// Recommendations\n$recommendationsBgColor: #3d3d3d;\n$recommendationBgColor: #2b2b2b;\n$recommendationLiveBorderColor: #b37a1e;\n$recommendationColor: #ffffff;\n$triggerIconActiveColor: #ffaf2b;\n$triggerIconTextColor: #3d3d3d;\n$recommendationsCountBgColor: #8ba2ff;\n\n// cloud sso\n$cloudSsoGoogle: #465282;\n$cloudSsoGithub: #393939;\n\n// RDI\n$rdiSecondaryBgColor: #171717;\n"
  },
  {
    "path": "redisinsight/ui/src/styles/themes/dark_theme/darkTheme.scss",
    "content": "@import \"theme_color\";\n\n:root {\n  // Core\n  --navBackgroundColor: #{$navBackgroundColor};\n  --tableRowHoverColor: #{$tableRowHoverColor};\n  --tableRowSelectedColor: #{$tableRowSelectedColor};\n  --euiColorPrimary: #{$euiColorPrimary};\n  --euiColorPrimaryRGB: #{$euiColorPrimaryRGB};\n  --euiColorSecondary: #{$euiColorSecondary};\n  --euiColorSuccess: #{$euiColorSuccess};\n  --euiColorAccent: #{$euiColorAccent};\n  --euiColorHighlight: #{$euiColorHighlight};\n  --euiColorDisabled: #{$euiColorDisabled};\n  --euiColorAccentText: #{$euiColorAccentText};\n  --euiColorDisabledText: #{$euiColorDisabledText};\n\n  // Status\n  --euiColorColorSuccess: #{$euiColorSuccess};\n  --euiColorColorWarning: #{$euiColorWarning};\n  --euiColorColorDanger: #{$euiColorDanger};\n\n  --euiColorWarningLight: #{$euiColorWarningLight};\n\n  // Grays\n  --euiColorEmptyShade: #{$euiColorEmptyShade};\n  --euiColorLightestShade: #{$euiColorLightestShade};\n  --euiColorLightShade: #{$euiColorLightShade};\n  --euiColorMediumShade: #{$euiColorMediumShade};\n  --euiColorDarkShade: #{$euiColorDarkShade};\n  --euiColorDarkestShade: #{$euiColorDarkestShade};\n  --euiColorFullShade: #{$euiColorFullShade};\n  --euiColorGhost: #{$euiColorGhost};\n\n  // Variations from core\n  --euiTextColor: #{$htmlColor};\n  --euiLinkColor: #{$euiLinkColor};\n  --euiShadowColor: #{$euiShadowColor};\n  --euiShadowColorLarge: #{$euiShadowColorLarge};\n  --euiPageBackgroundColor: #{$euiPageBackgroundColor};\n  --euiTextSubduedColor: #{$euiTextSubduedColor};\n  --euiTextSubduedColorHover: #{$euiTextSubduedColorHover};\n  --euiTitleColor: #{$euiTitleColor};\n  --euiBreadcrumbActive: #{$euiBreadcrumbActive};\n  --scrollBackgroundColor: #{$scrollBackgroundColor};\n\n  // Contrasty text variants\n  --euiColorPrimaryText: #{$euiColorPrimaryText};\n  --euiColorSecondaryText: #{$euiColorSecondaryText};\n  --euiColorSuccessText: #{$euiColorSuccessText};\n  --euiColorAccentText: #{$euiColorAccentText};\n  --euiColorWarningText: #{$euiColorWarningText};\n  --euiColorDangerText: #{$euiColorDangerText};\n\n  // Charts\n  --euiColorChartLines: #{$euiColorChartLines};\n  --euiColorChartBand: #{$euiColorChartBand};\n\n  // Tooltip\n  --euiTooltipBackgroundColor: #{$euiTooltipBackgroundColor};\n  --euiTooltipTextColor: #{$euiTooltipTextColor};\n  --euiTooltipTextSecondColor: #{$euiTooltipTextSecondColor};\n  --euiTooltipTitleTextColor: #{$euiTooltipTitleTextColor};\n  --euiToastBackgroundColor: #{$euiToastBackgroundColor};\n  --euiToastDangerBgColor: #{$euiToastDangerBgColor};\n  --euiToastSuccessBtnColor: #{$euiToastSuccessBtnColor};\n  --euiToastSuccessBorderColor: #{$euiToastSuccessBorderColor};\n  --euiToastDangerBtnColor: #{$euiToastDangerBtnColor};\n  --euiToastDangerBorderColor: #{$euiToastDangerBorderColor};\n  --euiToastLightColor: #{$euiColorDarkShade};\n\n  // Custom\n  --htmlColor: #{$htmlColor};\n  --textColorShade: #{$textColorShade};\n  --tableLightestBorderColor: #{$tableLightestBorderColor};\n  --tableLightBorderColor: #{$tableLightBorderColor};\n  --tableDarkestBorderColor: #{$tableDarkestBorderColor};\n  --browserTableRowEven: #{$browserTableRowEven};\n  --inputPlaceholderColor: #{$inputPlaceholderColor};\n  --inputDisabledBackgroundColor: #{$inputDisabledBackgroundColor};\n  --inputTextColor: #{$inputTextColor};\n  --controlsBoxShadowColor: #{$controlsBoxShadowColor};\n  --controlsBorderColor: #{$controlsBorderColor};\n  --controlsLabelColor: #{$controlsLabelColor};\n  --hoverInListColor: #{$hoverInListColor};\n  --hoverInListColorLight: #{$hoverInListColorLight};\n  --hoverInListColorDarken: #{$hoverInListColorDarken};\n  --externalLinkColor: #{$externalLinkColor};\n  --externalLinkTooltipColor: #{$externalLinkTooltipColor};\n  --linkToastColor: #{$linkToastColor};\n  --browserViewTypePassive: #{$browserViewTypePassive};\n  --browserComponentActive: #{$browserComponentActive};\n  --browserTreeNodeOpen: #{$browserTreeNodeOpen};\n  --defaultGreenColor: #{$defaultGreenColor};\n  --insightsTriggerBgColor: #{$insightsTriggerBgColor};\n\n  --iconsDefaultColor: #{$iconsDefaultColor};\n  --iconsDefaultHoverColor: #{$iconsDefaultHoverColor};\n\n  --separatorColor: #{$separatorColor};\n  --separatorColorLight: #{$separatorColorLight};\n  --separatorNavigationColor: #{$separatorNavigationColor};\n  --separatorDropdownColor: #{$separatorDropdownColor};\n\n  --buttonSecondaryHoverColor: #{$buttonSecondaryHoverColor};\n  --buttonSecondaryTextColor: #{$buttonSecondaryTextColor};\n  --buttonSecondaryDisabledTextColor: #{$buttonSecondaryDisabledTextColor};\n  --buttonDangerHoverColor: #{$buttonDangerHoverColor};\n  --buttonIconPrimaryHover: #{$buttonIconPrimaryHover};\n  --buttonWarningColor: #{$buttonWarningColor};\n  --buttonWarningHoverColor: #{$buttonWarningHoverColor};\n  --buttonDangerToastColor: #{$buttonDangerToastColor};\n  --buttonDangerToastHoverColor: #{$buttonDangerToastHoverColor};\n  --buttonGuideBgColor: #{$buttonGuideBgColor};\n  --buttonSuccessColor: #{$buttonSuccessColor};\n  --buttonDarkenTextColor: #{$buttonDarkenTextColor};\n  --buttonDarkenBgColor: #{$buttonDarkenBgColor};\n\n  --comboBoxBadgeBgColor: #{$comboBoxBadgeBgColor};\n  --loadingContentColor: #{$loadingContentColor};\n\n  --cliOutputResponseColor: #{$cliOutputResponseColor};\n  --cliOutputResponseFailColor: #{$cliOutputResponseFailColor};\n\n  --badgeBackgroundColor: #{$badgeBackgroundColor};\n  --commandGroupBadgeColor: #{$commandGroupBadgeColor};\n\n  --moduleBackgroundColor: #{$commandGroupBadgeColor};\n  --callOutBackgroundColor: #{$euiTooltipBackgroundColor};\n  --tooltipLightBgColor: #{$tooltipLightBgColor};\n\n  --overlayPromoNYColor: #{$overlayPromoNYColor};\n\n  --monacoBgColor: #{$monacoBgColor};\n  --highlightDotColor: #{$highlightDotColor};\n  --monacoParamsColor: #{$monacoParamsColor};\n\n  --successBorderColor: #{$successBorderColor};\n  --warningBorderColor: #{$warningBorderColor};\n  --errorBorderColor: #{$errorBorderColor};\n\n  --overlayMaskBgColor: #{$overlayMaskBgColor};\n\n  // KeyTypes\n  --typeHashColor: #{$typeHashColor};\n  --typeListColor: #{$typeListColor};\n  --typeSetColor: #{$typeSetColor};\n  --typeZSetColor: #{$typeZSetColor};\n  --typeStringColor: #{$typeStringColor};\n  --typeReJSONColor: #{$typeReJSONColor};\n  --typeStreamColor: #{$typeStreamColor};\n  --typeGraphColor: #{$typeGraphColor};\n  --typeTimeSeriesColor: #{$typeTimeSeriesColor};\n  --groupSortedSetColor: #{$groupSortedSetColor};\n  --groupBitmapColor: #{$groupBitmapColor};\n  --groupClusterColor: #{$groupClusterColor};\n  --groupConnectionColor: #{$groupConnectionColor};\n  --groupGeoColor: #{$groupGeoColor};\n  --groupGenericColor: #{$groupGenericColor};\n  --groupPubSubColor: #{$groupPubSubColor};\n  --groupScriptingColor: #{$groupScriptingColor};\n  --groupTransactionsColor: #{$groupTransactionsColor};\n  --groupServerColor: #{$groupServerColor};\n  --groupHyperLolLogColor: #{$groupHyperLolLogColor};\n  --defaultTypeColor: #{$defaultTypeColor};\n\n  // JSON colors\n  --jsonKeyNameColor: #{$jsonKeyNameColor};\n  --jsonKeyNameArrayColor: #{$jsonKeyNameArrayColor};\n  --jsonStringColor: #{$jsonStringColor};\n  --jsonNumberColor: #{$jsonNumberColor};\n  --jsonBooleanColor: #{$jsonBooleanColor};\n  --jsonNullColor: #{$jsonNullColor};\n  --jsonNonStringPrimitiveColor: #{$jsonNonStringPrimitiveColor};\n\n  // RediSearch colors;\n  --rsSubmitBtn: #{$rsSubmitBtn};\n  --rsInputColor: #{$rsInputColor};\n  --rsInputWrapperColor: #{$rsInputWrapperColor};\n\n  // Workbench\n  --wbRunResultsBg: #{$wbRunResultsBg};\n  --wbHoverIconColor: #{$wbHoverIconColor};\n  --wbActiveIconColor: #{$wbActiveIconColor};\n  --wbTextColor: #{$wbTextColor};\n\n  // Pub/Sub\n  --pubSubClientsBadge: #{$pubSubClientsBadge};\n\n  // Database analysis\n  --badgeIconColor: #{$badgeIconColor};\n  --recommendationBorderColor: #{$recommendationBorderColor};\n\n  // Recommendations\n  --recommendationsBgColor: #{$recommendationsBgColor};\n  --recommendationBgColor: #{$recommendationBgColor};\n  --recommendationLiveBorderColor: #{$recommendationLiveBorderColor};\n  --recommendationColor: #{$recommendationColor};\n  --triggerIconActiveColor: #{$triggerIconActiveColor};\n  --liveRecommendationVoteBgColor: #{$controlsBorderColor};\n  --triggerIconTextColor: #{$triggerIconTextColor};\n  --recommendationsCountBgColor: #{$recommendationsCountBgColor};\n\n  //cloud sso\n  --cloudSsoGoogle: #{$cloudSsoGoogle};\n  --cloudSsoGithub: #{$cloudSsoGithub};\n\n  // rdi\n  --rdiSecondaryBgColor: #{$rdiSecondaryBgColor};\n\n  // layout\n  --hrBackgroundColor: #{var(--euiColorDarkShade)};\n  --loadingContentLightestShade: #{var(--euiColorLightestShade)};\n}\n"
  },
  {
    "path": "redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss",
    "content": "$euiColorPrimary: #3953c3;\n$euiColorPrimaryRGB: 57,83,195;\n$euiColorPrimaryText: #ffffff;\n$euiColorSecondary: #243dac;\n$euiColorSecondaryText: #ffffff;\n$euiColorSuccess: #3953c3;\n$euiColorSuccessText: #8ba2ff;\n$euiColorDanger: #b92733;\n$euiColorDangerText: #ad0017;\n$euiColorWarning: #9d6901;\n$euiColorWarningText: #9d6901;\n$euiTextSubduedColor: #415681;\n$euiTextSubduedColorHover: #173369;\n$euiPageBackgroundColor: #ffffff;\n$euiTooltipBackgroundColor: #ffffff;\n$euiTooltipTextColor: #173369;\n$euiTooltipTextSecondColor: #395b88;\n$euiTooltipTitleTextColor: #395984;\n$euiColorLightShade: #cdd7e6;\n$euiColorLightestShade: #e4eaf2;\n$euiColorEmptyShade: #ffffff;\n$euiBreadcrumbActive: #395984;\n$euiToastBackgroundColor: #243dac;\n$euiToastSuccessBtnColor: #5165bd;\n$euiToastSuccessBorderColor: #5165bd;\n$euiToastDangerBtnColor: #c7535d;\n$euiToastDangerBorderColor: #c7535d;\n$euiToastDangerBgColor: #9e2f29;\n$euiColorMediumShade: #527298;\n$euiColorFullShade: #173369;\n$euiColorGhost: #ffffff;\n\n$htmlColor: #173369;\n$navBackgroundColor: #0f1633;\n$externalLinkColor: #3163d8;\n$externalLinkTooltipColor: #3163d8;\n$linkToastColor: #adbeff;\n$defaultGreenColor: #008000;\n$insightsTriggerBgColor: #D7E3FA;\n$scrollBackgroundColor: rgba(105, 112, 125, 0.5);\n\n$tableRowHoverColor: #fafbfd;\n$tableRowSelectedColor: #ebeffa;\n$tableLightestBorderColor: #c1cbd9;\n$tableLightBorderColor: #e4eaf2;\n$tableDarkestBorderColor: #e9edf3;\n\n$browserTableRowEven: #f6f7f9;\n$browserViewTypePassive: #f6f7f9;\n$browserComponentActive: #d7e3fa;\n$browserTreeNodeOpen: #d7e3fa;\n\n$inputTextColor: #173369;\n$inputPlaceholderColor: #395984;\n$inputDisabledBackgroundColor: #f6f8fd;\n$controlsBoxShadowColor: #c1cbd9;\n$controlsBorderColor: #8992b3;\n$controlsLabelColor: #385985;\n$controlsLabelHoverColor: #173369;\n$iconsDefaultColor: #728baf;\n$iconsDefaultHoverColor: #173369;\n$separatorColor: #cdd7e6;\n$separatorColorLight: #B2B9D1;\n$separatorNavigationColor: #465282;\n$separatorDropdownColor: #8b90a3;\n\n$buttonSecondaryHoverColor: #2848d7;\n$buttonSecondaryTextColor: #243dac;\n$buttonSecondaryDisabledTextColor: #dfe4fb;\n$buttonDangerHoverColor: #cb3844;\n$buttonIconPrimaryHover: #dfe4fb;\n$buttonWarningColor: #9e2f29;\n$buttonWarningHoverColor: #b00c03;\n$buttonDangerToastColor: #e8524a;\n$buttonDangerToastHoverColor: #bf3932;\n$buttonGuideBgColor: transparent;\n$buttonSuccessColor: #408B6D;\n$buttonDarkenTextColor: #3163D8;\n$buttonDarkenBgColor: #D7E3FA;\n\n$comboBoxBadgeBgColor: #edf0f5;\n$loadingContentColor: #eef1f6;\n\n$hoverInListColor: #e9edfa;\n$hoverInListColorLight: #d7e3fa;\n$textColorShade: #415681;\n$cliOutputResponseColor: #395b88;\n$cliOutputResponseFailColor: #ad0017;\n\n$badgeBackgroundColor: #e8efff;\n$commandGroupBadgeColor: #b8c5db;\n$callOutBackgroundColor: #e9edfa;\n$tooltipLightBgColor: #F5F8FF;\n\n$overlayPromoNYColor: #ffffff1a;\n\n$monacoBgColor: #f0f2f7;\n$highlightDotColor: #2BBBB2;\n$monacoParamsColor: #B63563;\n\n$successBorderColor: #5BC69B;\n$warningBorderColor: #FFAF2B;\n$errorBorderColor: #F74B57;\n\n$overlayMaskBgColor: rgba(255, 255, 255, 0.66);\n\n// Types colors\n$typeHashColor: #cdddf8;\n$typeListColor: #a5d4c3;\n$typeSetColor: #d4baa7;\n$typeZSetColor: #d9a0c6;\n$typeStringColor: #c7b0ea;\n$typeReJSONColor: #b8c5db;\n$typeStreamColor: #c7cea8;\n$typeGraphColor: #acccd7;\n$typeTimeSeriesColor: #c7c7c7;\n$groupSortedSetColor: #d9a0c6;\n$groupBitmapColor: #3f4b5f;\n$groupClusterColor: #6e6e6e;\n$groupConnectionColor: #ffb4d3;\n$groupGeoColor: #bbc4bc;\n$groupGenericColor: #f8f8aa;\n$groupPubSubColor: #bef0f2;\n$groupScriptingColor: #ffadad;\n$groupTransactionsColor: #14708d;\n$groupServerColor: #e8e8e8;\n$groupHyperLolLogColor: #3f4b5f;\n$defaultTypeColor: #E3AAAA;\n\n// JSON colors\n$jsonKeyNameColor: #a00595;\n$jsonKeyNameArrayColor: #00677e;\n$jsonStringColor: #006915;\n$jsonNumberColor: #00677e;\n$jsonBooleanColor: #9e6600;\n$jsonNullColor: #395984;\n$jsonNonStringPrimitiveColor: #6b9fff;\n\n// RediSearch colors\n$rsSubmitBtn: #13a450;\n$rsInputColor: #fff;\n$rsInputWrapperColor: #fff;\n\n// Workbench\n$wbRunResultsBg: #fff;\n$wbHoverIconColor: #415681;\n$wbActiveIconColor: #3163D8;\n$wbTextColor: #415681;\n\n// Pub/Sub\n$pubSubClientsBadge: #b5cea8;\n\n// Database analysis\n$badgeIconColor : #415681;\n$recommendationBorderColor: #3953c3;\n\n\n// Recommendations\n$recommendationsBgColor: #F6F7F9;\n$recommendationBgColor: #F6F7F9;\n$recommendationLiveBorderColor: #B37A1E;\n$recommendationColor: #415681;\n$triggerIconActiveColor: #FFAF2B;\n$triggerIconTextColor: #3D3D3D;\n$recommendationsCountBgColor: #243dac;\n\n// cloud sso\n$cloudSsoGoogle: #A2B8F2;\n$cloudSsoGithub: #393939;\n$euiColorDarkShade: #aaa;\n\n// RDI\n$rdiSecondaryBgColor: #EFEFEF;\n"
  },
  {
    "path": "redisinsight/ui/src/styles/themes/light_theme/lightTheme.scss",
    "content": "@import \"theme_color\";\n\n:root {\n  // Core\n  --navBackgroundColor: #{$navBackgroundColor};\n  --tableRowHoverColor: #{$tableRowHoverColor};\n  --tableRowSelectedColor: #{$tableRowSelectedColor};\n  --euiColorPrimary: #{$euiColorPrimary};\n  --euiColorPrimaryRGB: #{$euiColorPrimaryRGB};\n  --euiColorSecondary: #{$euiColorSecondary};\n  --euiColorSuccess: #{$euiColorSuccess};\n  --euiColorSecondaryText: #{$euiColorSecondaryText};\n\n  // Status\n  --euiColorColorSuccess: #{$euiColorSuccess};\n  --euiColorColorWarning: #{$euiColorWarning};\n  --euiColorColorDanger: #{$euiColorDanger};\n\n  --euiColorWarningLight: #{$euiColorWarning};\n\n  // Grays\n  --euiColorEmptyShade: #{$euiColorEmptyShade};\n  --euiColorLightestShade: #{$euiColorLightestShade};\n  --euiColorLightShade: #{$euiColorLightShade};\n  --euiColorMediumShade: #{$euiColorMediumShade};\n  --euiColorDarkShade: #{$euiColorDarkShade};\n  // --euiColorDarkestShade: #{$euiColorDarkestShade};\n  --euiColorFullShade: #{$euiColorFullShade};\n  --euiColorGhost: #{$euiColorSecondary};\n\n  // Variations from core\n  --euiTextColor: #{$htmlColor};\n  // --euiLinkColor: #{$euiLinkColor};\n  // --euiShadowColor: #{$euiShadowColor};\n  // --euiShadowColorLarge: #{$euiShadowColorLarge};\n  --euiPageBackgroundColor: #{$euiPageBackgroundColor};\n  --euiTextSubduedColor: #{$euiTextSubduedColor};\n  --euiTextSubduedColorHover: #{$euiTextSubduedColorHover};\n  // --euiTitleColor: #{$euiTitleColor};\n  --euiBreadcrumbActive: #{$euiBreadcrumbActive};\n  --scrollBackgroundColor: #{$scrollBackgroundColor};\n\n  // Contrasty text variants\n  --euiColorPrimaryText: #{$euiColorPrimaryText};\n  --euiColorWarningText: #{$euiColorWarningText};\n  --euiColorDangerText: #{$euiColorDangerText};\n  --euiColorSuccessText: #{$euiColorSuccessText};\n\n  // Charts\n  // --euiColorChartLines: #{$euiColorChartLines};\n  // --euiColorChartBand: #{$euiColorChartBand};\n\n  // Tooltip\n  --euiTooltipBackgroundColor: #{$euiTooltipBackgroundColor};\n  --euiTooltipTextColor: #{$euiTooltipTextColor};\n  --euiTooltipTextSecondColor: #{$euiTooltipTextSecondColor};\n  --euiTooltipTitleTextColor: #{$euiTooltipTitleTextColor};\n  --euiToastBackgroundColor: #{$euiToastBackgroundColor};\n  --euiToastDangerBgColor: #{$euiToastDangerBgColor};\n  --euiToastSuccessBtnColor: #{$euiToastSuccessBtnColor};\n  --euiToastSuccessBorderColor: #{$euiToastSuccessBorderColor};\n  --euiToastDangerBtnColor: #{$euiToastDangerBtnColor};\n  --euiToastDangerBorderColor: #{$euiToastDangerBorderColor};\n  --euiToastLightColor: #{$euiColorLightestShade};\n\n  // Custom\n  --htmlColor: #{$htmlColor};\n  --textColorShade: #{$textColorShade};\n  --tableLightestBorderColor: #{$tableLightestBorderColor};\n  --tableLightBorderColor: #{$tableLightBorderColor};\n  --tableDarkestBorderColor: #{$tableDarkestBorderColor};\n  --browserTableRowEven: #{$browserTableRowEven};\n  --inputPlaceholderColor: #{$inputPlaceholderColor};\n  --inputDisabledBackgroundColor: #{$inputDisabledBackgroundColor};\n  --inputTextColor: #{$inputTextColor};\n  --controlsBoxShadowColor: #{$controlsBoxShadowColor};\n  --controlsBorderColor: #{$controlsBorderColor};\n  --controlsLabelColor: #{$controlsLabelColor};\n  --controlsLabelHoverColor: #{$controlsLabelHoverColor};\n  --hoverInListColor: #{$hoverInListColor};\n  --hoverInListColorLight: #{$hoverInListColor};\n  --hoverInListColorDarken: #{$hoverInListColorLight};\n  --externalLinkColor: #{$externalLinkColor};\n  --externalLinkTooltipColor: #{$externalLinkTooltipColor};\n  --linkToastColor: #{$linkToastColor};\n  --browserViewTypePassive: #{$browserViewTypePassive};\n  --browserComponentActive: #{$browserComponentActive};\n  --browserTreeNodeOpen: #{$browserTreeNodeOpen};\n  --defaultGreenColor: #{$defaultGreenColor};\n  --insightsTriggerBgColor: #{$insightsTriggerBgColor};\n\n  --iconsDefaultColor: #{$iconsDefaultColor};\n  --iconsDefaultHoverColor: #{$iconsDefaultHoverColor};\n\n  --separatorColor: #{$separatorColor};\n  --separatorColorLight: #{$separatorColorLight};\n  --separatorNavigationColor: #{$separatorNavigationColor};\n  --separatorDropdownColor: #{$separatorDropdownColor};\n\n  --buttonSecondaryHoverColor: #{$buttonSecondaryHoverColor};\n  --buttonSecondaryTextColor: #{$buttonSecondaryTextColor};\n  --buttonSecondaryDisabledTextColor: #{$buttonSecondaryDisabledTextColor};\n  --buttonDangerHoverColor: #{$buttonDangerHoverColor};\n  --buttonIconPrimaryHover: #{$buttonIconPrimaryHover};\n  --buttonWarningColor: #{$buttonWarningColor};\n  --buttonWarningHoverColor: #{$buttonWarningHoverColor};\n  --buttonDangerToastColor: #{$buttonDangerToastColor};\n  --buttonDangerToastHoverColor: #{$buttonDangerToastHoverColor};\n  --buttonGuideBgColor: #{$buttonGuideBgColor};\n  --buttonSuccessColor: #{$buttonSuccessColor};\n  --buttonDarkenTextColor: #{$buttonDarkenTextColor};\n  --buttonDarkenBgColor: #{$buttonDarkenBgColor};\n\n  --comboBoxBadgeBgColor: #{$comboBoxBadgeBgColor};\n  --loadingContentColor: #{$loadingContentColor};\n\n  --cliOutputResponseColor: #{$cliOutputResponseColor};\n  --cliOutputResponseFailColor: #{$cliOutputResponseFailColor};\n\n  --badgeBackgroundColor: #{$badgeBackgroundColor};\n  --commandGroupBadgeColor: #{$commandGroupBadgeColor};\n\n  --moduleBackgroundColor: #{$typeHashColor};\n  --callOutBackgroundColor: #{$callOutBackgroundColor};\n  --tooltipLightBgColor: #{$tooltipLightBgColor};\n\n  --overlayPromoNYColor: #{$overlayPromoNYColor};\n\n  --monacoBgColor: #{$monacoBgColor};\n  --highlightDotColor: #{$highlightDotColor};\n  --monacoParamsColor: #{$monacoParamsColor};\n\n  --successBorderColor: #{$successBorderColor};\n  --warningBorderColor: #{$warningBorderColor};\n  --errorBorderColor: #{$errorBorderColor};\n\n  --overlayMaskBgColor: #{$overlayMaskBgColor};\n\n  // KeyTypes\n  --typeHashColor: #{$typeHashColor};\n  --typeListColor: #{$typeListColor};\n  --typeSetColor: #{$typeSetColor};\n  --typeZSetColor: #{$typeZSetColor};\n  --typeStringColor: #{$typeStringColor};\n  --typeReJSONColor: #{$typeReJSONColor};\n  --typeStreamColor: #{$typeStreamColor};\n  --typeGraphColor: #{$typeGraphColor};\n  --typeTimeSeriesColor: #{$typeTimeSeriesColor};\n  --groupSortedSetColor: #{$groupSortedSetColor};\n  --groupBitmapColor: #{$groupBitmapColor};\n  --groupClusterColor: #{$groupClusterColor};\n  --groupConnectionColor: #{$groupConnectionColor};\n  --groupGeoColor: #{$groupGeoColor};\n  --groupGenericColor: #{$groupGenericColor};\n  --groupPubSubColor: #{$groupPubSubColor};\n  --groupScriptingColor: #{$groupScriptingColor};\n  --groupTransactionsColor: #{$groupTransactionsColor};\n  --groupServerColor: #{$groupServerColor};\n  --groupHyperLolLogColor: #{$groupHyperLolLogColor};\n  --defaultTypeColor: #{$defaultTypeColor};\n\n  // JSON colors\n  --jsonKeyNameColor: #{$jsonKeyNameColor};\n  --jsonKeyNameArrayColor: #{$jsonKeyNameArrayColor};\n  --jsonStringColor: #{$jsonStringColor};\n  --jsonNumberColor: #{$jsonNumberColor};\n  --jsonBooleanColor: #{$jsonBooleanColor};\n  --jsonNullColor: #{$jsonNullColor};\n  --jsonNonStringPrimitiveColor: #{$jsonNonStringPrimitiveColor};\n\n  // RediSearch colors;\n  --rsSubmitBtn: #{$rsSubmitBtn};\n  --rsInputColor: #{$rsInputColor};\n  --rsInputWrapperColor: #{$rsInputWrapperColor};\n\n  // Workbench\n  --wbRunResultsBg: #{$wbRunResultsBg};\n  --wbHoverIconColor: #{$wbHoverIconColor};\n  --wbActiveIconColor: #{$wbActiveIconColor};\n  --wbTextColor: #{$wbTextColor};\n\n  // Pub/Sub\n  --pubSubClientsBadge: #{$pubSubClientsBadge};\n\n  // Database analysis\n  --badgeIconColor: #{$badgeIconColor};\n  --recommendationBorderColor: #{$recommendationBorderColor};\n\n  // Recommendations\n  --recommendationsBgColor: #{$recommendationsBgColor};\n  --recommendationColor: #{$recommendationColor};\n  --recommendationBgColor: #{$recommendationBgColor};\n  --recommendationLiveBorderColor: #{$recommendationLiveBorderColor};\n  --triggerIconActiveColor: #{$triggerIconActiveColor};\n  --liveRecommendationVoteBgColor: #{$separatorColor};\n  --triggerIconTextColor: #{$triggerIconTextColor};\n  --recommendationsCountBgColor: #{$recommendationsCountBgColor};\n\n  //cloud sso\n  --cloudSsoGoogle: #{$cloudSsoGoogle};\n  --cloudSsoGithub: #{$cloudSsoGithub};\n\n  // rdi\n  --rdiSecondaryBgColor: #{$rdiSecondaryBgColor};\n\n  // layout\n  --hrBackgroundColor: #{var(--euiColorLightShade)};\n  --loadingContentLightestShade: #{var(--euiColorLightestShade)};\n}\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/analytics.d.ts",
    "content": "declare global {\n  interface Window {\n    // Segment global analytics object, assigned in loadSegmentAnalytics().\n    analytics: SegmentAnalytics.AnalyticsJS\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/checkAnalytics.ts",
    "content": "import { get } from 'lodash'\nimport { store } from 'uiSrc/slices/store'\n\n// Check is user give access to collect his events\nexport const checkIsAnalyticsGranted = (): boolean =>\n  !!get(store.getState(), 'user.settings.config.agreements.analytics', false)\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/events.ts",
    "content": "/* eslint-disable max-len */\nexport enum TelemetryEvent {\n  APPLICATION_UPDATED = 'APPLICATION_UPDATED',\n  UPDATE_NOTIFICATION_DISPLAYED = 'UPDATE_NOTIFICATION_DISPLAYED',\n  UPDATE_NOTIFICATION_RESTART_CLICKED = 'UPDATE_NOTIFICATION_RESTART_CLICKED',\n  UPDATE_NOTIFICATION_CLOSED = 'UPDATE_NOTIFICATION_CLOSED',\n  CONSENT_MENU_VIEWED = 'CONSENT_MENU_VIEWED',\n\n  CONFIG_DATABASES_SINGLE_DATABASE_DELETE_CLICKED = 'CONFIG_DATABASES_SINGLE_DATABASE_DELETE_CLICKED',\n  CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED = 'CONFIG_DATABASES_MULTIPLE_DATABASES_DELETE_CLICKED',\n  CONFIG_DATABASES_DATABASE_EDIT_CLICKED = 'CONFIG_DATABASES_DATABASE_EDIT_CLICKED',\n  CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED = 'CONFIG_DATABASES_DATABASE_EDIT_CANCELLED_CLICKED',\n  CONFIG_DATABASES_DATABASE_LIST_SORTED = 'CONFIG_DATABASES_DATABASE_LIST_SORTED',\n  CONFIG_DATABASES_DATABASE_LIST_SEARCHED = 'CONFIG_DATABASES_DATABASE_LIST_SEARCHED',\n  CONFIG_DATABASES_DATABASE_MANAGE_TAGS_CLICKED = 'CONFIG_DATABASES_DATABASE_MANAGE_TAGS_CLICKED',\n  DATABASE_LIST_COLUMNS_CLICKED = 'DATABASE_LIST_COLUMNS_CLICKED',\n\n  CONFIG_DATABASES_HOST_PORT_COPIED = 'CONFIG_DATABASES_HOST_PORT_COPIED',\n  CONFIG_DATABASES_ADD_FORM_DISMISSED = 'CONFIG_DATABASES_ADD_FORM_DISMISSED',\n  CONFIG_DATABASES_OPEN_DATABASE = 'CONFIG_DATABASES_OPEN_DATABASE',\n  NAVIGATION_PANEL_OPENED = 'NAVIGATION_PANEL_OPENED',\n  CONFIG_DATABASES_CLICKED = 'CONFIG_DATABASES_CLICKED',\n  CONFIG_DATABASES_MANUALLY_SUBMITTED = 'CONFIG_DATABASES_MANUALLY_SUBMITTED',\n  CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_SUBMITTED = 'CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_SUBMITTED',\n  CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_CANCELLED = 'CONFIG_DATABASES_REDIS_SOFTWARE_AUTODISCOVERY_CANCELLED',\n  CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_SUBMITTED = 'CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_SUBMITTED',\n  CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED = 'CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_CANCELLED',\n  CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED = 'CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_SUBMITTED',\n  CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_CANCELLED = 'CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_CANCELLED',\n  CONFIG_DATABASES_GET_REDIS_CLOUD_ACCOUNT_CLICKED = 'CONFIG_DATABASES_GET_REDIS_CLOUD_ACCOUNT_CLICKED',\n  CREATE_FREE_CLOUD_DATABASE_CLICKED = 'CREATE_FREE_CLOUD_DATABASE_CLICKED',\n  CONFIG_DATABASES_DATABASE_CLONE_REQUESTED = 'CONFIG_DATABASES_DATABASE_CLONE_REQUESTED',\n  CONFIG_DATABASES_DATABASE_CLONE_CANCELLED = 'CONFIG_DATABASES_DATABASE_CLONE_CANCELLED',\n  CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED = 'CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED',\n  CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED = 'CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED',\n  CONFIG_DATABASES_REDIS_IMPORT_CANCELLED = 'CONFIG_DATABASES_REDIS_IMPORT_CANCELLED',\n  CONFIG_DATABASES_REDIS_IMPORT_CLICKED = 'CONFIG_DATABASES_REDIS_IMPORT_CLICKED',\n  CONFIG_DATABASES_REDIS_EXPORT_CLICKED = 'CONFIG_DATABASES_REDIS_EXPORT_CLICKED',\n  CONFIG_DATABASES_REDIS_EXPORT_SUCCEEDED = 'CONFIG_DATABASES_REDIS_EXPORT_SUCCEEDED',\n  CONFIG_DATABASES_REDIS_EXPORT_FAILED = 'CONFIG_DATABASES_REDIS_EXPORT_FAILED',\n  CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED = 'CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED',\n  CONFIG_DATABASES_TEST_CONNECTION_CLICKED = 'CONFIG_DATABASES_TEST_CONNECTION_CLICKED',\n\n  BUILD_FROM_SOURCE_CLICKED = 'BUILD_FROM_SOURCE_CLICKED',\n  BUILD_USING_DOCKER_CLICKED = 'BUILD_USING_DOCKER_CLICKED',\n  BUILD_USING_HOMEBREW_CLICKED = 'BUILD_USING_HOMEBREW_CLICKED',\n\n  INSTANCES_TAB_CHANGED = 'INSTANCES_TAB_CHANGED',\n\n  BROWSER_KEY_ADD_BUTTON_CLICKED = 'BROWSER_KEY_ADD_BUTTON_CLICKED',\n  BROWSER_KEY_BULK_ACTIONS_BUTTON_CLICKED = 'BROWSER_KEY_BULK_ACTIONS_BUTTON_CLICKED',\n  BROWSER_KEY_ADD_CANCELLED = 'BROWSER_KEY_ADD_CANCELLED',\n  BROWSER_KEY_DELETE_CLICKED = 'BROWSER_KEY_DELETE_CLICKED',\n  BROWSER_KEY_VALUE_REMOVE_CLICKED = 'BROWSER_KEY_VALUE_REMOVE_CLICKED',\n  BROWSER_KEY_ADD_VALUE_CLICKED = 'BROWSER_KEY_ADD_VALUE_CLICKED',\n  BROWSER_KEY_ADD_VALUE_CANCELLED = 'BROWSER_KEY_ADD_VALUE_CANCELLED',\n  BROWSER_KEY_COPIED = 'BROWSER_KEY_COPIED',\n  BROWSER_JSON_KEY_EXPANDED = 'BROWSER_JSON_KEY_EXPANDED',\n  BROWSER_JSON_KEY_COLLAPSED = 'BROWSER_JSON_KEY_COLLAPSED',\n  BROWSER_KEY_ADDED = 'BROWSER_KEY_ADDED',\n  BROWSER_KEY_VALUE_FILTERED = 'BROWSER_KEY_VALUE_FILTERED',\n  BROWSER_KEY_TTL_CHANGED = 'BROWSER_KEY_TTL_CHANGED',\n  BROWSER_KEY_VALUE_ADDED = 'BROWSER_KEY_VALUE_ADDED',\n  BROWSER_KEY_VALUE_REMOVED = 'BROWSER_KEY_VALUE_REMOVED',\n  BROWSER_KEY_VALUE_EDITED = 'BROWSER_KEY_VALUE_EDITED',\n  BROWSER_FIELD_TTL_EDITED = 'BROWSER_FIELD_TTL_EDITED',\n  BROWSER_JSON_PROPERTY_EDITED = 'BROWSER_JSON_PROPERTY_EDITED',\n  BROWSER_JSON_PROPERTY_DELETED = 'BROWSER_JSON_PROPERTY_DELETED',\n  BROWSER_JSON_PROPERTY_ADDED = 'BROWSER_JSON_PROPERTY_ADDED',\n  BROWSER_JSON_VALUE_IMPORT_CLICKED = 'BROWSER_JSON_VALUE_IMPORT_CLICKED',\n  BROWSER_KEYS_ADDITIONALLY_SCANNED = 'BROWSER_KEYS_ADDITIONALLY_SCANNED',\n  BROWSER_KEYS_SCANNED_WITH_FILTER_ENABLED = 'BROWSER_KEYS_SCANNED_WITH_FILTER_ENABLED',\n  BROWSER_KEY_LIST_AUTO_REFRESH_ENABLED = 'BROWSER_KEY_LIST_AUTO_REFRESH_ENABLED',\n  BROWSER_KEY_LIST_AUTO_REFRESH_DISABLED = 'BROWSER_KEY_LIST_AUTO_REFRESH_DISABLED',\n  BROWSER_KEY_DETAILS_AUTO_REFRESH_ENABLED = 'BROWSER_KEY_DETAILS_AUTO_REFRESH_ENABLED',\n  BROWSER_KEY_DETAILS_AUTO_REFRESH_DISABLED = 'BROWSER_KEY_DETAILS_AUTO_REFRESH_DISABLED',\n  BROWSER_KEY_VALUE_VIEWED = 'BROWSER_KEY_VALUE_VIEWED',\n  BROWSER_KEY_DETAILS_FULL_SCREEN_ENABLED = 'BROWSER_KEY_DETAILS_FULL_SCREEN_ENABLED',\n  BROWSER_KEY_DETAILS_FULL_SCREEN_DISABLED = 'BROWSER_KEY_DETAILS_FULL_SCREEN_DISABLED',\n  BROWSER_KEY_FIELD_VALUE_EXPANDED = 'BROWSER_KEY_FIELD_VALUE_EXPANDED',\n  BROWSER_KEY_FIELD_VALUE_COLLAPSED = 'BROWSER_KEY_FIELD_VALUE_COLLAPSED',\n  BROWSER_KEY_DETAILS_FORMATTER_CHANGED = 'BROWSER_KEY_DETAILS_FORMATTER_CHANGED',\n  BROWSER_WORKBENCH_LINK_CLICKED = 'BROWSER_WORKBENCH_LINK_CLICKED',\n  BROWSER_DATABASE_INDEX_CHANGED = 'BROWSER_DATABASE_INDEX_CHANGED',\n  BROWSER_FILTER_MODE_CHANGE_FAILED = 'BROWSER_FILTER_MODE_CHANGE_FAILED',\n  SHOW_BROWSER_COLUMN_CLICKED = 'SHOW_BROWSER_COLUMN_CLICKED',\n  LIST_VIEW_OPENED = 'LIST_VIEW_OPENED',\n\n  CLI_OPENED = 'CLI_OPENED',\n  CLI_CLOSED = 'CLI_CLOSED',\n  CLI_MINIMIZED = 'CLI_MINIMIZED',\n  CLI_COMMAND_SUBMITTED = 'CLI_COMMAND_SUBMITTED',\n  CLI_WORKBENCH_LINK_CLICKED = 'CLI_WORKBENCH_LINK_CLICKED',\n  COMMAND_HELPER_OPENED = 'COMMAND_HELPER_OPENED',\n  COMMAND_HELPER_CLOSED = 'COMMAND_HELPER_CLOSED',\n  COMMAND_HELPER_MINIMIZED = 'COMMAND_HELPER_MINIMIZED',\n  COMMAND_HELPER_INFO_DISPLAYED_FOR_CLI_INPUT = 'COMMAND_HELPER_INFO_DISPLAYED_FOR_CLI_INPUT',\n  COMMAND_HELPER_COMMAND_FILTERED = 'COMMAND_HELPER_COMMAND_FILTERED',\n  COMMAND_HELPER_COMMAND_OPENED = 'COMMAND_HELPER_COMMAND_OPENED',\n\n  SETTINGS_COLOR_THEME_CHANGED = 'SETTINGS_COLOR_THEME_CHANGED',\n  SETTINGS_NOTIFICATION_MESSAGES_ENABLED = 'SETTINGS_NOTIFICATION_MESSAGES_ENABLED',\n  SETTINGS_NOTIFICATION_MESSAGES_DISABLED = 'SETTINGS_NOTIFICATION_MESSAGES_DISABLED',\n  SETTINGS_DATE_TIME_FORMAT_CHANGED = 'SETTINGS_DATE_TIME_FORMAT_CHANGED',\n  SETTINGS_TIME_ZONE_CHANGED = 'SETTINGS_TIME_ZONE_CHANGED',\n  SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED = 'SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED',\n  SETTINGS_CLOUD_API_KEY_NAME_COPIED = 'SETTINGS_CLOUD_API_KEY_NAME_COPIED',\n  SETTINGS_CLOUD_API_KEY_REMOVE_CLICKED = 'SETTINGS_CLOUD_API_KEY_REMOVE_CLICKED',\n  SETTINGS_CLOUD_API_KEYS_REMOVE_CLICKED = 'SETTINGS_CLOUD_API_KEYS_REMOVE_CLICKED',\n  SETTINGS_CLOUD_API_KEYS_REMOVED = 'SETTINGS_CLOUD_API_KEYS_REMOVED',\n  SETTINGS_CLOUD_API_KEY_SORTED = 'SETTINGS_CLOUD_API_KEY_SORTED',\n\n  STRING_LOAD_ALL_CLICKED = 'STRING_LOAD_ALL_CLICKED',\n  STRING_DOWNLOAD_VALUE_CLICKED = 'STRING_DOWNLOAD_VALUE_CLICKED',\n\n  WORKBENCH_COMMAND_SUBMITTED = 'WORKBENCH_COMMAND_SUBMITTED',\n  WORKBENCH_COMMAND_COPIED = 'WORKBENCH_COMMAND_COPIED',\n  WORKBENCH_COMMAND_RUN_AGAIN = 'WORKBENCH_COMMAND_RUN_AGAIN',\n  WORKBENCH_COMMAND_PROFILE = 'WORKBENCH_COMMAND_PROFILE',\n  WORKBENCH_COMMAND_DELETE_COMMAND = 'WORKBENCH_COMMAND_DELETE_COMMAND',\n  WORKBENCH_RESULTS_IN_FULL_SCREEN = 'WORKBENCH_RESULTS_IN_FULL_SCREEN',\n  WORKBENCH_RESULTS_COLLAPSED = 'WORKBENCH_RESULTS_COLLAPSED',\n  WORKBENCH_RESULTS_EXPANDED = 'WORKBENCH_RESULTS_EXPANDED',\n  WORKBENCH_RESULT_VIEW_CHANGED = 'WORKBENCH_RESULT_VIEW_CHANGED',\n  WORKBENCH_NON_REDIS_EDITOR_OPENED = 'WORKBENCH_NON_REDIS_EDITOR_OPENED',\n  WORKBENCH_NON_REDIS_EDITOR_CANCELLED = 'WORKBENCH_NON_REDIS_EDITOR_CANCELLED',\n  WORKBENCH_NON_REDIS_EDITOR_SAVED = 'WORKBENCH_NON_REDIS_EDITOR_SAVED',\n  WORKBENCH_MODE_CHANGED = 'WORKBENCH_MODE_CHANGED',\n  WORKBENCH_CLEAR_RESULT_CLICKED = 'WORKBENCH_CLEAR_RESULT_CLICKED',\n  WORKBENCH_CLEAR_ALL_RESULTS_CLICKED = 'WORKBENCH_CLEAR_ALL_RESULTS_CLICKED',\n\n  PROFILER_OPENED = 'PROFILER_OPENED',\n  PROFILER_STARTED = 'PROFILER_STARTED',\n  PROFILER_STOPPED = 'PROFILER_STOPPED',\n  PROFILER_PAUSED = 'PROFILER_PAUSED',\n  PROFILER_RESUMED = 'PROFILER_RESUMED',\n  PROFILER_CLEARED = 'PROFILER_CLEARED',\n  PROFILER_CLOSED = 'PROFILER_CLOSED',\n  PROFILER_MINIMIZED = 'PROFILER_MINIMIZED',\n\n  TREE_VIEW_OPENED = 'TREE_VIEW_OPENED',\n  TREE_VIEW_KEY_ADD_BUTTON_CLICKED = 'TREE_VIEW_KEY_ADD_BUTTON_CLICKED',\n  TREE_VIEW_KEY_BULK_ACTIONS_BUTTON_CLICKED = 'TREE_VIEW_KEY_BULK_ACTIONS_BUTTON_CLICKED',\n  TREE_VIEW_KEY_ADD_CANCELLED = 'TREE_VIEW_KEY_ADD_CANCELLED',\n  TREE_VIEW_KEY_VALUE_FILTERED = 'TREE_VIEW_KEY_VALUE_FILTERED',\n  TREE_VIEW_KEY_TTL_CHANGED = 'TREE_VIEW_KEY_TTL_CHANGED',\n  TREE_VIEW_KEY_ADD_VALUE_CLICKED = 'TREE_VIEW_KEY_ADD_VALUE_CLICKED',\n  TREE_VIEW_KEY_ADD_VALUE_CANCELLED = 'TREE_VIEW_KEY_ADD_VALUE_CANCELLED',\n  TREE_VIEW_KEY_VALUE_ADDED = 'TREE_VIEW_KEY_VALUE_ADDED',\n  TREE_VIEW_KEY_VALUE_REMOVE_CLICKED = 'TREE_VIEW_KEY_VALUE_REMOVE_CLICKED',\n  TREE_VIEW_KEY_DELETE_CLICKED = 'TREE_VIEW_KEY_DELETE_CLICKED',\n  TREE_VIEW_FOLDER_DELETE_CLICKED = 'TREE_VIEW_FOLDER_DELETE_CLICKED',\n  TREE_VIEW_KEY_VALUE_REMOVED = 'TREE_VIEW_KEY_VALUE_REMOVED',\n  TREE_VIEW_KEY_VALUE_EDITED = 'TREE_VIEW_KEY_VALUE_EDITED',\n  TREE_VIEW_FIELD_TTL_EDITED = 'TREE_VIEW_FIELD_TTL_EDITED',\n  TREE_VIEW_KEY_COPIED = 'TREE_VIEW_KEY_COPIED',\n  TREE_VIEW_JSON_KEY_EXPANDED = 'TREE_VIEW_JSON_KEY_EXPANDED',\n  TREE_VIEW_JSON_KEY_COLLAPSED = 'TREE_VIEW_JSON_KEY_COLLAPSED',\n  TREE_VIEW_JSON_PROPERTY_EDITED = 'TREE_VIEW_JSON_PROPERTY_EDITED',\n  TREE_VIEW_JSON_PROPERTY_DELETED = 'TREE_VIEW_JSON_PROPERTY_DELETED',\n  TREE_VIEW_JSON_PROPERTY_ADDED = 'TREE_VIEW_JSON_PROPERTY_ADDED',\n  TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED = 'TREE_VIEW_KEYS_SCANNED_WITH_FILTER_ENABLED',\n  TREE_VIEW_KEYS_ADDITIONALLY_SCANNED = 'TREE_VIEW_KEYS_ADDITIONALLY_SCANNED',\n  TREE_VIEW_DELIMITER_CHANGED = 'TREE_VIEW_DELIMITER_CHANGED',\n  TREE_VIEW_KEYS_SORTED = 'TREE_VIEW_KEYS_SORTED',\n  TREE_VIEW_KEY_ADDED = 'TREE_VIEW_KEY_ADDED',\n  TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED = 'TREE_VIEW_KEY_LIST_AUTO_REFRESH_ENABLED',\n  TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED = 'TREE_VIEW_KEY_LIST_AUTO_REFRESH_DISABLED',\n  TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_ENABLED = 'TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_ENABLED',\n  TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_DISABLED = 'TREE_VIEW_KEY_DETAILS_AUTO_REFRESH_DISABLED',\n  TREE_VIEW_KEY_VALUE_VIEWED = 'TREE_VIEW_KEY_VALUE_VIEWED',\n  TREE_VIEW_KEY_DETAILS_FULL_SCREEN_ENABLED = 'TREE_VIEW_KEY_DETAILS_FULL_SCREEN_ENABLED',\n  TREE_VIEW_KEY_DETAILS_FULL_SCREEN_DISABLED = 'TREE_VIEW_KEY_DETAILS_FULL_SCREEN_DISABLED',\n  TREE_VIEW_KEY_FIELD_VALUE_EXPANDED = 'TREE_VIEW_KEY_FIELD_VALUE_EXPANDED',\n  TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED = 'TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED',\n  TREE_VIEW_KEY_DETAILS_FORMATTER_CHANGED = 'TREE_VIEW_KEY_DETAILS_FORMATTER_CHANGED',\n  TREE_VIEW_WORKBENCH_LINK_CLICKED = 'TREE_VIEW_WORKBENCH_LINK_CLICKED',\n  SHOW_HASH_TTL_CLICKED = 'SHOW_HASH_TTL_CLICKED',\n\n  SLOWLOG_LOADED = 'SLOWLOG_LOADED',\n  SLOWLOG_CLEARED = 'SLOWLOG_CLEARED',\n  SLOWLOG_SET_LOG_SLOWER_THAN = 'SLOWLOG_SET_LOG_SLOWER_THAN',\n  SLOWLOG_SET_MAX_LEN = 'SLOWLOG_SET_MAX_LEN',\n  SLOWLOG_SORTED = 'SLOWLOG_SORTED',\n  SLOWLOG_AUTO_REFRESH_ENABLED = 'SLOWLOG_AUTO_REFRESH_ENABLED',\n  SLOWLOG_AUTO_REFRESH_DISABLED = 'SLOWLOG_AUTO_REFRESH_DISABLED',\n\n  STREAM_DATA_FILTERED = 'STREAM_DATA_FILTERED',\n  STREAM_DATA_FILTER_RESET = 'STREAM_DATA_FILTER_RESET',\n  STREAM_CONSUMER_GROUPS_LOADED = 'STREAM_CONSUMER_GROUPS_LOADED',\n  STREAM_CONSUMER_GROUP_CREATED = 'STREAM_CONSUMER_GROUP_CREATED',\n  STREAM_CONSUMER_GROUP_DELETED = 'STREAM_CONSUMER_GROUP_DELETED',\n  STREAM_CONSUMER_GROUP_ID_SET = 'STREAM_CONSUMER_GROUP_ID_SET',\n  STREAM_CONSUMERS_LOADED = 'STREAM_CONSUMERS_LOADED',\n  STREAM_CONSUMER_MESSAGE_ACKNOWLEDGED = 'STREAM_CONSUMER_MESSAGE_ACKNOWLEDGED',\n  STREAM_CONSUMER_MESSAGE_CLAIMED = 'STREAM_CONSUMER_MESSAGE_CLAIMED',\n  STREAM_CONSUMER_MESSAGE_CLAIM_CANCELED = 'STREAM_CONSUMER_MESSAGE_CLAIM_CANCELED',\n  STREAM_CONSUMER_DELETED = 'STREAM_CONSUMER_DELETED',\n\n  PUBSUB_MESSAGES_CLEARED = 'PUBSUB_MESSAGES_CLEARED',\n  PUBSUB_AUTOSCROLL_PAUSED = 'PUBSUB_AUTOSCROLL_PAUSED',\n  PUBSUB_AUTOSCROLL_RESUMED = 'PUBSUB_AUTOSCROLL_RESUMED',\n\n  NOTIFICATIONS_HISTORY_OPENED = 'NOTIFICATIONS_HISTORY_OPENED',\n  NOTIFICATIONS_MESSAGE_CLOSED = 'NOTIFICATIONS_MESSAGE_CLOSED',\n\n  BULK_ACTIONS_OPENED = 'BULK_ACTIONS_OPENED',\n  BULK_ACTIONS_WARNING = 'BULK_ACTIONS_WARNING',\n  BULK_ACTIONS_CANCELLED = 'BULK_ACTIONS_CANCELLED',\n\n  DATABASE_ANALYSIS_STARTED = 'DATABASE_ANALYSIS_STARTED',\n  DATABASE_ANALYSIS_HISTORY_VIEWED = 'DATABASE_ANALYSIS_HISTORY_VIEWED',\n  DATABASE_ANALYSIS_EXTRAPOLATION_CHANGED = 'DATABASE_ANALYSIS_EXTRAPOLATION_CHANGED',\n  DATABASE_ANALYSIS_TIPS_CLICKED = 'DATABASE_ANALYSIS_TIPS_CLICKED',\n  DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED = 'DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED',\n  DATABASE_ANALYSIS_TIPS_EXPANDED = 'DATABASE_ANALYSIS_TIPS_EXPANDED',\n  DATABASE_ANALYSIS_TIPS_COLLAPSED = 'DATABASE_ANALYSIS_TIPS_COLLAPSED',\n  DATABASE_ANALYSIS_TIPS_VOTED = 'DATABASE_ANALYSIS_TIPS_VOTED',\n  DATABASE_TIPS_TUTORIAL_CLICKED = 'DATABASE_TIPS_TUTORIAL_CLICKED',\n  DATABASE_TIPS_KEY_COPIED = 'DATABASE_TIPS_KEY_COPIED',\n\n  USER_SURVEY_LINK_CLICKED = 'USER_SURVEY_LINK_CLICKED',\n  IMPORT_SAMPLES_CLICKED = 'IMPORT_SAMPLES_CLICKED',\n  SEARCH_MODE_CHANGED = 'SEARCH_MODE_CHANGED',\n  SEARCH_MODE_CHANGE_FAILED = 'SEARCH_MODE_CHANGE_FAILED',\n  SEARCH_INDEX_CHANGED = 'SEARCH_INDEX_CHANGED',\n  SEARCH_INDEX_ADD_BUTTON_CLICKED = 'SEARCH_INDEX_ADD_BUTTON_CLICKED',\n  SEARCH_INDEX_ADD_CANCELLED = 'SEARCH_INDEX_ADD_CANCELLED',\n  SEARCH_KEYS_SEARCHED = 'SEARCH_KEYS_SEARCHED',\n  SEARCH_INDEX_ADDED = 'SEARCH_INDEX_ADDED',\n  SEARCH_INDEX_DELETED = 'SEARCH_INDEX_DELETED',\n  ONBOARDING_TOUR_CLICKED = 'ONBOARDING_TOUR_CLICKED',\n  ONBOARDING_TOUR_ACTION_MADE = 'ONBOARDING_TOUR_ACTION_MADE',\n  ONBOARDING_TOUR_TRIGGERED = 'ONBOARDING_TOUR_TRIGGERED',\n  ONBOARDING_TOUR_FINISHED = 'ONBOARDING_TOUR_FINISHED',\n\n  RELEASE_NOTES_LINK_CLICKED = 'RELEASE_NOTES_LINK_CLICKED',\n\n  INSIGHTS_PANEL_OPENED = 'INSIGHTS_PANEL_OPENED',\n  INSIGHTS_PANEL_CLOSED = 'INSIGHTS_PANEL_CLOSED',\n  INSIGHTS_PANEL_TAB_CHANGED = 'INSIGHTS_PANEL_TAB_CHANGED',\n  INSIGHTS_PANEL_FULL_SCREEN_CLICKED = 'INSIGHTS_PANEL_FULL_SCREEN_CLICKED',\n  INSIGHTS_TIPS_TUTORIAL_CLICKED = 'INSIGHTS_TIPS_TUTORIAL_CLICKED',\n  INSIGHTS_TIPS_VOTED = 'INSIGHTS_TIPS_VOTED',\n  INSIGHTS_TIPS_SNOOZED = 'INSIGHTS_TIPS_SNOOZED',\n  INSIGHTS_TIPS_HIDE = 'INSIGHTS_TIPS_HIDE',\n  INSIGHTS_TIPS_SHOW_HIDDEN = 'INSIGHTS_TIPS_SHOW_HIDDEN',\n  INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED = 'INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED',\n  INSIGHTS_TIPS_KEY_COPIED = 'INSIGHTS_TIPS_KEY_COPIED',\n  INSIGHTS_TIPS_LINK_CLICKED = 'INSIGHTS_TIPS_LINK_CLICKED',\n\n  EXPLORE_PANEL_COMMAND_COPIED = 'EXPLORE_PANEL_COMMAND_COPIED',\n  EXPLORE_PANEL_COMMAND_RUN_CLICKED = 'EXPLORE_PANEL_COMMAND_RUN_CLICKED',\n  EXPLORE_PANEL_DATABASE_CHANGE_CLICKED = 'EXPLORE_PANEL_DATABASE_CHANGE_CLICKED',\n  EXPLORE_PANEL_IMPORT_CLICKED = 'EXPLORE_PANEL_IMPORT_CLICKED',\n  EXPLORE_PANEL_IMPORT_SUBMITTED = 'EXPLORE_PANEL_IMPORT_SUBMITTED',\n  EXPLORE_PANEL_TUTORIAL_DELETED = 'EXPLORE_PANEL_TUTORIAL_DELETED',\n  EXPLORE_PANEL_TUTORIAL_OPENED = 'EXPLORE_PANEL_TUTORIAL_OPENED',\n  EXPLORE_PANEL_LINK_CLICKED = 'EXPLORE_PANEL_LINK_CLICKED',\n  EXPLORE_PANEL_CREATE_TUTORIAL_LINK_CLICKED = 'EXPLORE_PANEL_CREATE_TUTORIAL_LINK_CLICKED',\n  EXPLORE_PANEL_DATA_UPLOAD_CLICKED = 'EXPLORE_PANEL_DATA_UPLOAD_CLICKED',\n  EXPLORE_PANEL_DATA_UPLOAD_SUBMITTED = 'EXPLORE_PANEL_DATA_UPLOAD_SUBMITTED',\n  EXPLORE_PANEL_DOWNLOAD_BULK_FILE_CLICKED = 'EXPLORE_PANEL_DOWNLOAD_BULK_FILE_CLICKED',\n\n  AI_CHAT_SESSION_RESTARTED = 'AI_CHAT_SESSION_RESTARTED',\n  AI_CHAT_BOT_MESSAGE_DISPLAYED = 'AI_CHAT_BOT_NEW_FEATURE_MESSAGE_DISPLAYED',\n  AI_CHAT_BOT_MESSAGE_CLICKED = 'AI_CHAT_BOT_NEW_FEATURE_MESSAGE_CLICKED',\n  AI_CHAT_OPENED = 'AI_CHAT_OPENED',\n  AI_CHAT_MESSAGE_SENT = 'AI_CHAT_MESSAGE_SENT',\n  AI_CHAT_BOT_COMMAND_RUN_CLICKED = 'AI_CHAT_BOT_COMMAND_RUN_CLICKED',\n  AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED = 'AI_CHAT_BOT_ERROR_MESSAGE_RECEIVED',\n  AI_CHAT_BOT_NO_INDEXES_MESSAGE_DISPLAYED = 'AI_CHAT_BOT_NO_INDEXES_MESSAGE_DISPLAYED',\n  AI_CHAT_BOT_TERMS_DISPLAYED = 'AI_CHAT_BOT_TERMS_DISPLAYED',\n  AI_CHAT_BOT_TERMS_ACCEPTED = 'AI_CHAT_BOT_TERMS_ACCEPTED',\n\n  CAPABILITY_POPOVER_DISPLAYED = 'CAPABILITY_POPOVER_DISPLAYED',\n\n  CLOUD_FREE_DATABASE_CLICKED = 'CLOUD_FREE_DATABASE_CLICKED',\n  CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED = 'CLOUD_SIGN_IN_SOCIAL_ACCOUNT_SELECTED',\n  CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED = 'CLOUD_SIGN_IN_SSO_OPTION_PROCEEDED',\n  CLOUD_SIGN_IN_SSO_OPTION_CANCELED = 'CLOUD_SIGN_IN_SSO_OPTION_CANCELED',\n  CLOUD_SIGN_IN_FORM_CLOSED = 'CLOUD_SIGN_IN_FORM_CLOSED',\n  CLOUD_SIGN_IN_CLICKED = 'CLOUD_SIGN_IN_CLICKED',\n  CLOUD_SIGN_IN_SUCCEEDED = 'CLOUD_SIGN_IN_SUCCEEDED',\n  CLOUD_SIGN_IN_FAILED = 'CLOUD_SIGN_IN_FAILED',\n  CLOUD_SIGN_IN_ACCOUNT_SELECTED = 'CLOUD_SIGN_IN_ACCOUNT_SELECTED',\n  CLOUD_SIGN_IN_ACCOUNT_FORM_CLOSED = 'CLOUD_SIGN_IN_ACCOUNT_FORM_CLOSED',\n  CLOUD_SIGN_IN_ACCOUNT_FAILED = 'CLOUD_SIGN_IN_ACCOUNT_FAILED',\n  CLOUD_SIGN_IN_PROVIDER_FORM_CLOSED = 'CLOUD_SIGN_IN_PROVIDER_FORM_CLOSED',\n  CLOUD_IMPORT_DATABASES_CLICKED = 'CLOUD_IMPORT_DATABASES_CLICKED',\n  CLOUD_IMPORT_DATABASES_SUBMITTED = 'CLOUD_IMPORT_DATABASES_SUBMITTED',\n  CLOUD_API_KEY_REMOVED = 'CLOUD_API_KEY_REMOVED',\n  CLOUD_LINK_CLICKED = 'CLOUD_LINK_CLICKED',\n  CLOUD_IMPORT_EXISTING_DATABASE = 'CLOUD_IMPORT_EXISTING_DATABASE',\n  CLOUD_IMPORT_DATABASE_FORBIDDEN = 'CLOUD_IMPORT_DATABASE_FORBIDDEN',\n  CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED = 'CLOUD_IMPORT_EXISTING_DATABASE_FORM_CLOSED',\n  CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION = 'CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION',\n  CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED = 'CLOUD_CREATE_DATABASE_IN_SUBSCRIPTION_FORM_CLOSED',\n  CLOUD_PROFILE_OPENED = 'CLOUD_PROFILE_OPENED',\n  CLOUD_ACCOUNT_SWITCHED = 'CLOUD_ACCOUNT_SWITCHED',\n  CLOUD_CONSOLE_CLICKED = 'CLOUD_CONSOLE_CLICKED',\n  CLOUD_SIGN_OUT_CLICKED = 'CLOUD_SIGN_OUT_CLICKED',\n  CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED = 'CLOUD_NOT_USED_DB_NOTIFICATION_VIEWED',\n\n  // Azure Auth events\n  AZURE_IMPORT_DATABASES_CLICKED = 'AZURE_IMPORT_DATABASES_CLICKED',\n  AZURE_SIGN_IN_CLICKED = 'AZURE_SIGN_IN_CLICKED',\n  AZURE_SWITCH_ACCOUNT_CLICKED = 'AZURE_SWITCH_ACCOUNT_CLICKED',\n  AZURE_SUBSCRIPTIONS_REFRESH_CLICKED = 'AZURE_SUBSCRIPTIONS_REFRESH_CLICKED',\n  AZURE_SUBSCRIPTION_SELECTED = 'AZURE_SUBSCRIPTION_SELECTED',\n  AZURE_DATABASES_REFRESH_CLICKED = 'AZURE_DATABASES_REFRESH_CLICKED',\n  AZURE_IMPORT_DATABASES_SUBMITTED = 'AZURE_IMPORT_DATABASES_SUBMITTED',\n  AZURE_IMPORT_DATABASES_CANCELLED = 'AZURE_IMPORT_DATABASES_CANCELLED',\n\n  RDI_INSTANCE_LIST_SORTED = 'RDI_INSTANCE_LIST_SORTED',\n  RDI_INSTANCE_SINGLE_DELETE_CLICKED = 'RDI_INSTANCE_SINGLE_DELETE_CLICKED',\n  RDI_INSTANCE_MULTIPLE_DELETE_CLICKED = 'RDI_INSTANCE_MULTIPLE_DELETE_CLICKED',\n  RDI_INSTANCE_LIST_SEARCHED = 'RDI_INSTANCE_LIST_SEARCHED',\n  RDI_INSTANCE_LIST_COLUMNS_CLICKED = 'RDI_INSTANCE_LIST_COLUMNS_CLICKED',\n  RDI_INSTANCE_URL_COPIED = 'RDI_INSTANCE_URL_COPIED',\n  RDI_INSTANCE_ADD_CLICKED = 'RDI_INSTANCE_ADD_CLICKED',\n  RDI_INSTANCE_ADD_CANCELLED = 'RDI_INSTANCE_ADD_CANCELLED',\n  RDI_INSTANCE_SUBMITTED = 'RDI_INSTANCE_SUBMITTED',\n  OPEN_RDI_CLICKED = 'OPEN_RDI_CLICKED',\n  RDI_ENDPOINT_ADDED = 'RDI_ENDPOINT_ADDED',\n  RDI_ENDPOINT_ADD_FAILED = 'RDI_ENDPOINT_ADD_FAILED',\n  RDI_PIPELINE_UPLOAD_FROM_SERVER_CLICKED = 'RDI_PIPELINE_UPLOAD_FROM_SERVER_CLICKED',\n  RDI_DEPLOY_CLICKED = 'RDI_DEPLOY_CLICKED',\n  RDI_PIPELINE_RESET_CLICKED = 'RDI_PIPELINE_RESET_CLICKED',\n  RDI_PIPELINE_RESET = 'RDI_PIPELINE_RESET',\n  RDI_PIPELINE_START_CLICKED = 'RDI_PIPELINE_START_CLICKED',\n  RDI_PIPELINE_STARTED = 'RDI_PIPELINE_STARTED',\n  RDI_PIPELINE_STOP_CLICKED = 'RDI_PIPELINE_STOP_CLICKED',\n  RDI_PIPELINE_STOPPED = 'RDI_PIPELINE_STOPPED',\n  RDI_TEST_JOB_OPENED = 'RDI_TEST_JOB_OPENED',\n  RDI_PIPELINE_DOWNLOAD_CLICKED = 'RDI_PIPELINE_DOWNLOAD_CLICKED',\n  RDI_PIPELINE_UPLOAD_FROM_FILE_CLICKED = 'RDI_PIPELINE_UPLOAD_FROM_FILE_CLICKED',\n  RDI_PIPELINE_UPLOAD_SUCCEEDED = 'RDI_PIPELINE_UPLOAD_SUCCEEDED',\n  RDI_PIPELINE_UPLOAD_FAILED = 'RDI_PIPELINE_UPLOAD_FAILED',\n  RDI_TEST_CONNECTIONS_CLICKED = 'RDI_TEST_CONNECTIONS_CLICKED',\n  RDI_TEST_JOB_RUN = 'RDI_TEST_JOB_RUN',\n  RDI_PIPELINE_JOB_CREATED = 'RDI_PIPELINE_JOB_CREATED',\n  RDI_PIPELINE_JOB_DELETED = 'RDI_PIPELINE_JOB_DELETED',\n  RDI_TEMPLATE_CLICKED = 'RDI_TEMPLATE_CLICKED',\n  RDI_STATISTICS_REFRESH_CLICKED = 'RDI_STATISTICS_REFRESH_CLICKED',\n  RDI_STATISTICS_AUTO_REFRESH_ENABLED = 'RDI_STATISTICS_AUTO_REFRESH_ENABLED',\n  RDI_STATISTICS_AUTO_REFRESH_DISABLED = 'RDI_STATISTICS_AUTO_REFRESH_DISABLED',\n  RDI_START_OPTION_SELECTED = 'RDI_START_OPTION_SELECTED',\n  RDI_DEDICATED_EDITOR_LANGUAGE_CHANGED = 'RDI_DEDICATED_EDITOR_LANGUAGE_CHANGED',\n  RDI_DEDICATED_EDITOR_OPENED = 'RDI_DEDICATED_EDITOR_OPENED',\n  RDI_DEDICATED_EDITOR_CANCELLED = 'RDI_DEDICATED_EDITOR_CANCELLED',\n  RDI_DEDICATED_EDITOR_SAVED = 'RDI_DEDICATED_EDITOR_SAVED',\n  RDI_UNSAVED_CHANGES_MESSAGE_DISPLAYED = 'RDI_UNSAVED_CHANGES_MESSAGE_DISPLAYED',\n\n  CONFIG_DATABASES_CERTIFICATE_REMOVED = 'CONFIG_DATABASES_CERTIFICATE_REMOVED',\n\n  OVERVIEW_AUTO_REFRESH_ENABLED = 'OVERVIEW_AUTO_REFRESH_ENABLED',\n  OVERVIEW_AUTO_REFRESH_DISABLED = 'OVERVIEW_AUTO_REFRESH_DISABLED',\n\n  // --- Vector Search v2: active events ---\n  VECTOR_SEARCH_ONBOARDING_VIEW_COMMAND_PREVIEW = 'VECTOR_SEARCH_ONBOARDING_VIEW_COMMAND_PREVIEW',\n  VECTOR_SEARCH_ONBOARDING_CREATE_INDEX_ERROR = 'VECTOR_SEARCH_ONBOARDING_CREATE_INDEX_ERROR',\n\n  // Search: make searchable (browser)\n  SEARCH_MAKE_SEARCHABLE_CLICKED = 'SEARCH_MAKE_SEARCHABLE_CLICKED',\n  SEARCH_MAKE_SEARCHABLE_CONFIRMED = 'SEARCH_MAKE_SEARCHABLE_CONFIRMED',\n  SEARCH_MAKE_SEARCHABLE_CANCELLED = 'SEARCH_MAKE_SEARCHABLE_CANCELLED',\n\n  // Search: view index (browser)\n  SEARCH_VIEW_INDEX_CLICKED = 'SEARCH_VIEW_INDEX_CLICKED',\n\n  // Search: onboarding & index creation\n  SEARCH_DEMO_ONBOARDING_TRIGGERED = 'SEARCH_DEMO_ONBOARDING_TRIGGERED',\n  SEARCH_DEMO_DATA_SELECTED = 'SEARCH_DEMO_DATA_SELECTED',\n  SEARCH_OWN_DATA_INDEX_TRIGGERED = 'SEARCH_OWN_DATA_INDEX_TRIGGERED',\n  SEARCH_INDEX_AUTO_SUGGESTION_VIEWED = 'SEARCH_INDEX_AUTO_SUGGESTION_VIEWED',\n  SEARCH_INDEX_CREATED = 'SEARCH_INDEX_CREATED',\n  SEARCH_CREATE_INDEX_ERROR = 'SEARCH_CREATE_INDEX_ERROR',\n  SEARCH_CREATE_INDEX_CANCELLED = 'SEARCH_CREATE_INDEX_CANCELLED',\n  SEARCH_CREATE_INDEX_TAB_CHANGED = 'SEARCH_CREATE_INDEX_TAB_CHANGED',\n  SEARCH_CREATE_INDEX_FIELD_EDITED = 'SEARCH_CREATE_INDEX_FIELD_EDITED',\n\n  // Search: create-index onboarding tour\n  SEARCH_CREATE_INDEX_ONBOARDING_STARTED = 'SEARCH_CREATE_INDEX_ONBOARDING_STARTED',\n  SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED = 'SEARCH_CREATE_INDEX_ONBOARDING_STEP_CLICKED',\n  SEARCH_CREATE_INDEX_ONBOARDING_COMPLETED = 'SEARCH_CREATE_INDEX_ONBOARDING_COMPLETED',\n\n  // Search: index list actions\n  SEARCH_INDEX_QUERY_CLICKED = 'SEARCH_INDEX_QUERY_CLICKED',\n  SEARCH_INDEX_BROWSE_DATASET_CLICKED = 'SEARCH_INDEX_BROWSE_DATASET_CLICKED',\n\n  // Search: query & results\n  SEARCH_COMMAND_SUBMITTED = 'SEARCH_COMMAND_SUBMITTED',\n  SEARCH_RESULT_VIEW_CHANGED = 'SEARCH_RESULT_VIEW_CHANGED',\n  SEARCH_COMMAND_COPIED = 'SEARCH_COMMAND_COPIED',\n  SEARCH_COMMAND_RUN_AGAIN = 'SEARCH_COMMAND_RUN_AGAIN',\n  SEARCH_RESULTS_IN_FULL_SCREEN = 'SEARCH_RESULTS_IN_FULL_SCREEN',\n  SEARCH_RESULTS_COLLAPSED = 'SEARCH_RESULTS_COLLAPSED',\n  SEARCH_RESULTS_EXPANDED = 'SEARCH_RESULTS_EXPANDED',\n  SEARCH_CLEAR_RESULT_CLICKED = 'SEARCH_CLEAR_RESULT_CLICKED',\n  SEARCH_CLEAR_ALL_RESULTS_CLICKED = 'SEARCH_CLEAR_ALL_RESULTS_CLICKED',\n  SEARCH_EDITOR_TAB_CHANGED = 'SEARCH_EDITOR_TAB_CHANGED',\n  SEARCH_INDEX_DETAILS_VIEWED = 'SEARCH_INDEX_DETAILS_VIEWED',\n\n  // Search: query library\n  SEARCH_QUERY_SAVED = 'SEARCH_QUERY_SAVED',\n  SEARCH_QUERY_LIBRARY_RUN = 'SEARCH_QUERY_LIBRARY_RUN',\n  SEARCH_QUERY_LIBRARY_LOADED = 'SEARCH_QUERY_LIBRARY_LOADED',\n  SEARCH_QUERY_DELETED = 'SEARCH_QUERY_DELETED',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/index.ts",
    "content": "import { TelemetryEvent } from './events'\nimport { TelemetryPageView } from './pageViews'\nimport type { ITelemetrySendEvent } from './interfaces'\n\nexport * from './telemetryUtils'\n\nexport { ITelemetrySendEvent, TelemetryEvent, TelemetryPageView }\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/interfaces.ts",
    "content": "import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport { TelemetryEvent } from './events'\n\nexport interface ITelemetryIdentify {\n  installationId: string\n  sessionId: number\n}\n\nexport type EventData = {\n  [key: string]: any\n  databaseId?: string\n  provider?: string\n}\n\nexport interface ITelemetrySendEvent {\n  event: TelemetryEvent\n  eventData?: EventData\n  nonTracking?: boolean\n  traits?: Object\n}\n\nexport interface ITelemetrySendPageView {\n  name: string\n  eventData?: EventData\n  nonTracking?: boolean\n}\n\nexport interface ITelemetryEvent {\n  event: TelemetryEvent\n  properties?: object\n}\n\nexport enum MatchType {\n  EXACT_VALUE_NAME = 'EXACT_VALUE_NAME',\n  PATTERN = 'PATTERN',\n}\n\nexport enum RedisModules {\n  RedisAI = 'ai',\n  RedisGraph = 'graph',\n  RedisGears = 'rg',\n  RedisBloom = 'bf',\n  RedisJSON = 'ReJSON',\n  RediSearch = 'search',\n  RedisTimeSeries = 'timeseries',\n}\n\nexport interface IModuleSummary {\n  loaded: boolean\n  version?: number\n  semanticVersion?: string\n}\n\nexport type RedisModulesKeyType = keyof typeof RedisModules\nexport interface IRedisModulesSummary\n  extends Record<keyof typeof RedisModules, IModuleSummary> {\n  customModules: AdditionalRedisModule[]\n}\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/pageViews.ts",
    "content": "export enum TelemetryPageView {\n  DATABASES_LIST_PAGE = 'Databases',\n  WELCOME_PAGE = 'Welcome',\n  SETTINGS_PAGE = 'Settings',\n  BROWSER_PAGE = 'Browser',\n  WORKBENCH_PAGE = 'Workbench',\n  SEARCH_AND_QUERY_PAGE = 'Search and Query',\n  SLOWLOG_PAGE = 'Slow Log',\n  CLUSTER_DETAILS_PAGE = 'Overview',\n  PUBSUB_PAGE = 'Pub/Sub',\n  DATABASE_ANALYSIS = 'Database Analysis',\n  RDI_INSTANCES_PAGE = 'RDI Instances',\n  RDI_CONFIG = 'RDI Configuration',\n  RDI_JOBS = 'RDI Jobs',\n  RDI_STATUS = 'RDI Status',\n  VECTOR_SEARCH_PAGE = 'Vector Search',\n}\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/telemetryUtils.ts",
    "content": "/**\n * Telemetry and analytics module.\n * This module abstracts the exact service/framework used for tracking usage.\n */\nimport isGlob from 'is-glob'\nimport { cloneDeep, get } from 'lodash'\nimport { Maybe, isRedisearchAvailable } from 'uiSrc/utils'\nimport { ApiEndpoints, KeyTypes } from 'uiSrc/constants'\nimport { KeyViewType } from 'uiSrc/slices/interfaces/keys'\nimport {\n  IModuleSummary,\n  ITelemetrySendEvent,\n  ITelemetrySendPageView,\n  RedisModulesKeyType,\n} from 'uiSrc/telemetry/interfaces'\nimport { apiService } from 'uiSrc/services'\nimport { store } from 'uiSrc/slices/store'\nimport { getInstanceInfo } from 'uiSrc/services/database/instancesService'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport { IRedisModulesSummary, MatchType, RedisModules } from './interfaces'\nimport { TelemetryEvent } from './events'\nimport { checkIsAnalyticsGranted } from './checkAnalytics'\n\nexport const getProviderData = (\n  dbId: string,\n): {\n  provider: Maybe<string>\n  serverName: Maybe<string>\n} => {\n  let provider\n  let serverName\n  const instance = get(\n    store.getState(),\n    'connections.instances.connectedInstance',\n  )\n  if (instance.id === dbId) {\n    provider = instance?.provider\n    const instanceOverview = get(\n      store.getState(),\n      'connections.instances.instanceOverview',\n    )\n    serverName = instanceOverview?.serverName || undefined\n  }\n  return { provider, serverName }\n}\n\nconst FREE_DB_IDENTIFIER_TELEMETRY_EVENTS = [\n  TelemetryEvent.INSIGHTS_PANEL_OPENED,\n  TelemetryEvent.INSIGHTS_PANEL_CLOSED,\n  TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED,\n  TelemetryEvent.EXPLORE_PANEL_COMMAND_COPIED,\n  TelemetryEvent.EXPLORE_PANEL_COMMAND_RUN_CLICKED,\n  TelemetryEvent.EXPLORE_PANEL_LINK_CLICKED,\n]\n\nconst getFreeDbFlag = (\n  event: TelemetryEvent,\n  freeDbEvents: TelemetryEvent[] = FREE_DB_IDENTIFIER_TELEMETRY_EVENTS,\n): { isFree?: boolean } => {\n  if (freeDbEvents.includes(event)) {\n    const state = get(\n      store.getState(),\n      'connections.instances.connectedInstance',\n    )\n    return state ? { isFree: state.isFreeDb } : {}\n  }\n\n  return {}\n}\n\nconst TELEMETRY_EMPTY_VALUE = 'none'\n\nconst sendEventTelemetry = async ({\n  event,\n  eventData = {},\n  traits = {},\n}: ITelemetrySendEvent) => {\n  let providerData\n  try {\n    const isAnalyticsGranted = checkIsAnalyticsGranted()\n    if (!isAnalyticsGranted) {\n      return\n    }\n\n    if (eventData.databaseId) {\n      providerData = getProviderData(eventData.databaseId)\n    }\n\n    const freeDbIdentifier = getFreeDbFlag(event)\n\n    await apiService.post(`${ApiEndpoints.ANALYTICS_SEND_EVENT}`, {\n      event,\n      eventData: { ...providerData, ...eventData, ...freeDbIdentifier },\n      traits,\n    })\n  } catch (e) {\n    // continue regardless of error\n  }\n}\n\nconst sendPageViewTelemetry = async ({\n  name,\n  eventData = {},\n}: ITelemetrySendPageView) => {\n  try {\n    let providerData\n    const isAnalyticsGranted = checkIsAnalyticsGranted()\n    if (!isAnalyticsGranted) {\n      return\n    }\n    if (eventData.databaseId) {\n      providerData = getProviderData(eventData.databaseId)\n    }\n    await apiService.post(`${ApiEndpoints.ANALYTICS_SEND_PAGE}`, {\n      event: name,\n      eventData: { ...providerData, ...eventData },\n    })\n  } catch (e) {\n    // continue regardless of error\n  }\n}\n\nconst getBasedOnViewTypeEvent = (\n  viewType: KeyViewType,\n  browserEvent: TelemetryEvent,\n  treeViewEvent: TelemetryEvent,\n): TelemetryEvent => {\n  switch (viewType) {\n    case KeyViewType.Browser:\n      return browserEvent\n    case KeyViewType.Tree:\n      return treeViewEvent\n    default:\n      return browserEvent\n  }\n}\n\nconst getJsonPathLevel = (path: string): number => {\n  try {\n    if (!path || path === '$') return 0\n\n    const stripped = path.startsWith('$.') ? path.slice(2) : path.slice(1)\n\n    const parts = stripped.split(/[.[\\]]/).filter(Boolean)\n\n    return parts.length\n  } catch (e) {\n    return 0\n  }\n}\n\nconst getAdditionalAddedEventData = (endpoint: ApiEndpoints, data: any) => {\n  switch (endpoint) {\n    case ApiEndpoints.HASH:\n      return {\n        keyType: KeyTypes.Hash,\n        length: data.fields?.length,\n        TTL: data.expire || -1,\n      }\n    case ApiEndpoints.SET:\n      return {\n        keyType: KeyTypes.Set,\n        length: data.members?.length,\n        TTL: data.expire || -1,\n      }\n    case ApiEndpoints.ZSET:\n      return {\n        keyType: KeyTypes.ZSet,\n        length: data.members?.length,\n        TTL: data.expire || -1,\n      }\n    case ApiEndpoints.STRING:\n      return {\n        keyType: KeyTypes.String,\n        length: data.value?.length,\n        TTL: data.expire || -1,\n      }\n    case ApiEndpoints.LIST:\n      return {\n        keyType: KeyTypes.List,\n        length: data.elements?.length,\n        TTL: data.expire || -1,\n      }\n    case ApiEndpoints.REJSON:\n      return {\n        keyType: KeyTypes.ReJSON,\n        TTL: -1,\n      }\n    case ApiEndpoints.STREAMS:\n      return {\n        keyType: KeyTypes.Stream,\n        length: 1,\n        TTL: data.expire || -1,\n      }\n    default:\n      return {}\n  }\n}\n\nconst getMatchType = (match: string): MatchType =>\n  !isGlob(match, { strict: false })\n    ? MatchType.EXACT_VALUE_NAME\n    : MatchType.PATTERN\n\nconst SUPPORTED_REDIS_MODULES = Object.freeze({\n  ai: RedisModules.RedisAI,\n  graph: RedisModules.RedisGraph,\n  rg: RedisModules.RedisGears,\n  bf: RedisModules.RedisBloom,\n  ReJSON: RedisModules.RedisJSON,\n  search: RedisModules.RediSearch,\n  timeseries: RedisModules.RedisTimeSeries,\n})\n\nconst DEFAULT_SUMMARY: IRedisModulesSummary = Object.freeze({\n  RediSearch: { loaded: false },\n  RedisAI: { loaded: false },\n  RedisGraph: { loaded: false },\n  RedisGears: { loaded: false },\n  RedisBloom: { loaded: false },\n  RedisJSON: { loaded: false },\n  RedisTimeSeries: { loaded: false },\n  customModules: [],\n})\n\nconst getEnumKeyBValue = (myEnum: any, enumValue: number | string): string => {\n  const keys = Object.keys(myEnum)\n  const index = keys.findIndex((x) => myEnum[x] === enumValue)\n  return index > -1 ? keys[index] : ''\n}\n\nconst getModuleSummaryToSent = (\n  module: AdditionalRedisModule,\n): IModuleSummary => ({\n  loaded: true,\n  version: module.version,\n  semanticVersion: module.semanticVersion,\n})\nconst getRedisInfoSummary = async (id: string) => {\n  let infoData: any = {}\n  try {\n    const info = await getInstanceInfo(id)\n    infoData = {\n      redis_version: info?.version,\n      uptime_in_days: info?.stats?.uptime_in_days,\n      used_memory: info?.usedMemory,\n      connected_clients: info?.connectedClients,\n      maxmemory_policy: info?.stats?.maxmemory_policy,\n      instantaneous_ops_per_sec: info?.stats?.instantaneous_ops_per_sec,\n      instantaneous_input_kbps: info?.stats?.instantaneous_input_kbps,\n      instantaneous_output_kbps: info?.stats?.instantaneous_output_kbps,\n      numberOfKeysRange: info?.stats?.numberOfKeysRange,\n      totalKeys: info?.totalKeys,\n    }\n  } catch (e) {\n    // continue regardless of error\n  }\n\n  return infoData\n}\nconst getRedisModulesSummary = (\n  modules: AdditionalRedisModule[] = [],\n): IRedisModulesSummary => {\n  const summary = cloneDeep(DEFAULT_SUMMARY)\n  try {\n    modules.forEach((module) => {\n      if (SUPPORTED_REDIS_MODULES[module.name]) {\n        const moduleName = getEnumKeyBValue(RedisModules, module.name)\n        summary[moduleName as RedisModulesKeyType] =\n          getModuleSummaryToSent(module)\n        return\n      }\n\n      if (isRedisearchAvailable([module])) {\n        const redisearchName = getEnumKeyBValue(\n          RedisModules,\n          RedisModules.RediSearch,\n        )\n        summary[redisearchName as RedisModulesKeyType] =\n          getModuleSummaryToSent(module)\n        return\n      }\n\n      summary.customModules.push(module)\n    })\n  } catch (e) {\n    // continue regardless of error\n  }\n  return summary\n}\n\nexport {\n  TELEMETRY_EMPTY_VALUE,\n  sendEventTelemetry,\n  sendPageViewTelemetry,\n  getBasedOnViewTypeEvent,\n  getJsonPathLevel,\n  getAdditionalAddedEventData,\n  getMatchType,\n  getRedisModulesSummary,\n  getFreeDbFlag,\n  getRedisInfoSummary,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/tests/telemetryUtils.spec.ts",
    "content": "import { RootState, store } from 'uiSrc/slices/store'\nimport { TelemetryEvent } from '../events'\nimport {\n  getRedisModulesSummary,\n  getFreeDbFlag,\n  getJsonPathLevel,\n} from '../telemetryUtils'\n\nconst DEFAULT_SUMMARY = Object.freeze({\n  RediSearch: { loaded: false },\n  RedisAI: { loaded: false },\n  RedisGraph: { loaded: false },\n  RedisGears: { loaded: false },\n  RedisBloom: { loaded: false },\n  RedisJSON: { loaded: false },\n  RedisTimeSeries: { loaded: false },\n  customModules: [],\n})\n\nconst getRedisModulesSummaryTests = [\n  {\n    input: [{ name: 'ai', version: 20000 }],\n    expected: {\n      ...DEFAULT_SUMMARY,\n      RedisAI: { loaded: true, version: 20000 },\n      customModules: [],\n    },\n  },\n  {\n    input: [{ name: 'search', version: 10000 }],\n    expected: {\n      ...DEFAULT_SUMMARY,\n      RediSearch: { loaded: true, version: 10000 },\n    },\n  },\n  {\n    input: [\n      { name: 'bf', version: 1000 },\n      { name: 'rediSQL', version: 1 },\n    ],\n    expected: {\n      ...DEFAULT_SUMMARY,\n      RedisBloom: { loaded: true, version: 1000 },\n      customModules: [{ name: 'rediSQL', version: 1 }],\n    },\n  },\n  {\n    input: [{ name: 'ReJSON' }],\n    expected: { ...DEFAULT_SUMMARY, RedisJSON: { loaded: true } },\n  },\n  {\n    input: [\n      { name: 'ai', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'graph', version: 20000, semanticVersion: '2.0.0' },\n      { name: 'rg', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'bf' },\n      { name: 'ReJSON', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'search', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'timeseries', version: 10000, semanticVersion: '1.0.0' },\n      { name: 'redisgears_2', version: 10000, semanticVersion: '1.0.0' },\n    ],\n    expected: {\n      RedisAI: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RedisGraph: { loaded: true, version: 20000, semanticVersion: '2.0.0' },\n      RedisGears: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RedisBloom: { loaded: true },\n      RedisJSON: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RediSearch: { loaded: true, version: 10000, semanticVersion: '1.0.0' },\n      RedisTimeSeries: {\n        loaded: true,\n        version: 10000,\n        semanticVersion: '1.0.0',\n      },\n      customModules: [\n        { name: 'redisgears_2', version: 10000, semanticVersion: '1.0.0' },\n      ],\n    },\n  },\n  { input: [], expected: DEFAULT_SUMMARY },\n  { input: {}, expected: DEFAULT_SUMMARY },\n  { input: undefined, expected: DEFAULT_SUMMARY },\n  { input: null, expected: DEFAULT_SUMMARY },\n  { input: 1, expected: DEFAULT_SUMMARY },\n]\n\ndescribe('getRedisModulesSummary', () => {\n  test.each(getRedisModulesSummaryTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getRedisModulesSummary(input)\n    expect(result).toEqual(expected)\n  })\n})\n\ndescribe('determineFreeDbFlag', () => {\n  describe.each`\n    isFreeDb\n    ${true}\n    ${false}\n  `('when isFreeDb=$isFreeDb', ({ isFreeDb }) => {\n    beforeEach(() => {\n      jest.spyOn(store, 'getState').mockImplementation(\n        () =>\n          ({\n            connections: {\n              instances: {\n                connectedInstance: {\n                  isFreeDb,\n                },\n              },\n            },\n          }) as RootState,\n      )\n    })\n\n    it(`returns { isFree: ${isFreeDb} } for an event in the freeDbEvents list`, () => {\n      const freeDbEvents = [\n        TelemetryEvent.INSIGHTS_PANEL_OPENED,\n        TelemetryEvent.INSIGHTS_PANEL_CLOSED,\n      ]\n      const event = TelemetryEvent.INSIGHTS_PANEL_OPENED\n\n      const result = getFreeDbFlag(event, freeDbEvents)\n\n      expect(result).toEqual({ isFree: isFreeDb })\n    })\n\n    it('returns {} for an event NOT in the freeDbEvents list', () => {\n      const freeDbEvents = [\n        TelemetryEvent.INSIGHTS_PANEL_OPENED,\n        TelemetryEvent.INSIGHTS_PANEL_CLOSED,\n      ]\n      const event = TelemetryEvent.AI_CHAT_BOT_COMMAND_RUN_CLICKED\n\n      const result = getFreeDbFlag(event, freeDbEvents)\n\n      expect(result).toEqual({})\n    })\n  })\n\n  it('returns {} if there is no connected instance', () => {\n    jest.spyOn(store, 'getState').mockImplementation(\n      () =>\n        ({\n          connections: {\n            instances: {},\n          },\n        }) as RootState,\n    )\n\n    const freeDbEvents = [\n      TelemetryEvent.INSIGHTS_PANEL_OPENED,\n      TelemetryEvent.INSIGHTS_PANEL_CLOSED,\n    ]\n    const event = TelemetryEvent.INSIGHTS_PANEL_OPENED\n\n    const result = getFreeDbFlag(event, freeDbEvents)\n\n    expect(result).toEqual({})\n  })\n})\n\ndescribe('getJsonPathLevel', () => {\n  it('returns 0 for empty or root path', () => {\n    expect(getJsonPathLevel('')).toBe(0)\n    expect(getJsonPathLevel('$')).toBe(0)\n  })\n\n  it('returns 1 for top-level properties', () => {\n    expect(getJsonPathLevel('$.foo')).toBe(1)\n    expect(getJsonPathLevel('$[0]')).toBe(1)\n  })\n\n  it('returns correct level for nested dot paths', () => {\n    expect(getJsonPathLevel('$.foo.bar')).toBe(2)\n    expect(getJsonPathLevel('$.foo.bar.baz')).toBe(3)\n  })\n\n  it('returns correct level for mixed dot and bracket paths', () => {\n    expect(getJsonPathLevel('$.foo[0].bar')).toBe(3)\n    expect(getJsonPathLevel('$[0].foo.bar')).toBe(3)\n    expect(getJsonPathLevel('$[0][1][2]')).toBe(3)\n  })\n\n  it('returns correct level for complex mixed paths', () => {\n    expect(getJsonPathLevel('$.foo[1].bar[2].baz')).toBe(5)\n    expect(getJsonPathLevel('$[0].foo[1].bar')).toBe(4)\n  })\n\n  it('handles malformed paths gracefully', () => {\n    expect(getJsonPathLevel('.foo.bar')).toBe(2) // missing $\n    expect(getJsonPathLevel('foo.bar')).toBe(2) // missing $\n    expect(getJsonPathLevel('$foo.bar')).toBe(2) // $ not followed by dot\n  })\n\n  it('returns 0 if an exception is thrown (e.g., non-string)', () => {\n    // @ts-expect-error testing runtime failure\n    expect(getJsonPathLevel(null)).toBe(0)\n    // @ts-expect-error testing runtime failure\n    expect(getJsonPathLevel(undefined)).toBe(0)\n    // @ts-expect-error testing runtime failure\n    expect(getJsonPathLevel({})).toBe(0)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/tests/usePageViewTelemetry.spec.ts",
    "content": "import { renderHook, act, cleanup } from '@testing-library/react-hooks'\nimport * as reactRedux from 'react-redux'\nimport { faker } from '@faker-js/faker'\nimport { cloneDeep } from 'lodash'\n\nimport { mockedStore } from 'uiSrc/utils/test-utils'\nimport { sendPageViewTelemetry } from 'uiSrc/telemetry'\nimport {\n  INSTANCE_ID_MOCK,\n  INSTANCES_MOCK,\n} from 'uiSrc/mocks/handlers/instances/instancesHandlers'\n\nimport { usePageViewTelemetry } from '../usePageViewTelemetry'\nimport { TelemetryPageView } from '../pageViews'\n\n// Mock the telemetry module, so we don't send actual telemetry data during tests\njest.mock('uiSrc/telemetry', () => ({\n  ...jest.requireActual('uiSrc/telemetry'),\n  sendPageViewTelemetry: jest.fn(),\n}))\n\ndescribe('usePageViewTelemetry', () => {\n  let store: typeof mockedStore\n  let mockUseSelector: jest.SpyInstance\n\n  const mockPage = faker.helpers.enumValue(TelemetryPageView)\n\n  beforeEach(() => {\n    jest.clearAllMocks()\n\n    cleanup()\n    store = cloneDeep(mockedStore)\n    store.clearActions()\n\n    mockUseSelector = jest.spyOn(reactRedux, 'useSelector')\n    mockUseSelector.mockReturnValue(INSTANCES_MOCK[0])\n  })\n\n  afterEach(() => {\n    jest.restoreAllMocks()\n  })\n\n  it('should send page view telemetry on mount if connected to instance', () => {\n    renderHook(() => usePageViewTelemetry({ page: mockPage }))\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: mockPage,\n      eventData: { databaseId: INSTANCE_ID_MOCK },\n    })\n  })\n\n  it('should not send page view telemetry if instanceId is not available', () => {\n    mockUseSelector.mockReturnValueOnce(null)\n\n    renderHook(() => usePageViewTelemetry({ page: mockPage }))\n\n    expect(sendPageViewTelemetry).not.toHaveBeenCalled()\n  })\n\n  it('should not send page view telemetry if already sent', () => {\n    const { rerender } = renderHook(() =>\n      usePageViewTelemetry({ page: mockPage }),\n    )\n\n    // Verify initial telemetry call\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n\n    // Simulate instance id change in the selector\n    mockUseSelector.mockReturnValue(INSTANCES_MOCK[1])\n    rerender()\n\n    // Should not send telemetry again\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n  })\n\n  it('should send page view telemetry with when called manually', () => {\n    const { result } = renderHook(() =>\n      usePageViewTelemetry({ page: mockPage }),\n    )\n\n    // Verify initial telemetry call\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: mockPage,\n      eventData: { databaseId: INSTANCE_ID_MOCK },\n    })\n\n    // Call the sendPageView method manually, with custom parameters\n    const customPage = faker.helpers.enumValue(TelemetryPageView)\n    const customInstanceId = 'custom-instance-1'\n\n    act(() => {\n      result.current.sendPageView(customPage, customInstanceId)\n    })\n\n    // Verify that the telemetry was sent with the custom parameters\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(2)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: customPage,\n      eventData: { databaseId: customInstanceId },\n    })\n  })\n\n  it('should include additional eventData when provided', () => {\n    const extra = { rqe_version: '2.8.0', number_of_indexes: 3 }\n\n    renderHook(() => usePageViewTelemetry({ page: mockPage, eventData: extra }))\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: mockPage,\n      eventData: {\n        databaseId: INSTANCE_ID_MOCK,\n        rqe_version: '2.8.0',\n        number_of_indexes: 3,\n      },\n    })\n  })\n\n  it('should not send page view telemetry when ready is false', () => {\n    const { rerender } = renderHook(\n      ({ ready }) => usePageViewTelemetry({ page: mockPage, ready }),\n      { initialProps: { ready: false } },\n    )\n\n    expect(sendPageViewTelemetry).not.toHaveBeenCalled()\n\n    rerender({ ready: true })\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n    expect(sendPageViewTelemetry).toHaveBeenCalledWith({\n      name: mockPage,\n      eventData: { databaseId: INSTANCE_ID_MOCK },\n    })\n  })\n\n  it('should default ready to true when not provided', () => {\n    renderHook(() => usePageViewTelemetry({ page: mockPage }))\n\n    expect(sendPageViewTelemetry).toHaveBeenCalledTimes(1)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/telemetry/usePageViewTelemetry.ts",
    "content": "import { useEffect, useState } from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry'\nimport { connectedInstanceSelector } from 'uiSrc/slices/instances/instances'\n\ninterface PageViewTelemetryProps {\n  page: TelemetryPageView\n  eventData?: Record<string, unknown>\n  ready?: boolean\n}\n\ninterface PageViewTelemetryHook {\n  sendPageView: (page: TelemetryPageView, instanceId: string) => void\n}\n\nexport const usePageViewTelemetry = ({\n  page,\n  eventData,\n  ready = true,\n}: PageViewTelemetryProps): PageViewTelemetryHook => {\n  const [isPageViewSent, setIsPageViewSent] = useState(false)\n  const { id: instanceId } = useSelector(connectedInstanceSelector)\n\n  useEffect(() => {\n    if (instanceId && ready && !isPageViewSent) {\n      sendPageViewTelemetry({\n        name: page,\n        eventData: {\n          databaseId: instanceId,\n          ...eventData,\n        },\n      })\n      setIsPageViewSent(true)\n    }\n  }, [instanceId, ready, isPageViewSent])\n\n  const sendPageView = (page: TelemetryPageView, instanceId: string) => {\n    sendPageViewTelemetry({\n      name: page,\n      eventData: {\n        databaseId: instanceId,\n      },\n    })\n    setIsPageViewSent(true)\n  }\n\n  return {\n    sendPageView,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/templates/autodiscovery-page-template/AutoDiscoveryPageTemplate.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport AutodiscoveryPageTemplate from './AutodiscoveryPageTemplate'\n\njest.mock('uiSrc/slices/panels/sidePanels', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/sidePanels'),\n  sidePanelsSelector: jest.fn().mockReturnValue({\n    openedPanel: 'insights',\n  }),\n}))\n\ndescribe('AutoDiscoveryPageTemplate', () => {\n  it('should render', () => {\n    expect(\n      render(<AutodiscoveryPageTemplate>1</AutodiscoveryPageTemplate>),\n    ).toBeTruthy()\n  })\n\n  it('should render children and side panel', () => {\n    render(\n      <AutodiscoveryPageTemplate>\n        <div data-testid=\"children\" />\n      </AutodiscoveryPageTemplate>,\n    )\n\n    expect(screen.getByTestId('children')).toBeInTheDocument()\n    expect(screen.getByTestId('side-panels-insights')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/templates/autodiscovery-page-template/AutodiscoveryPageTemplate.tsx",
    "content": "import React from 'react'\nimport { PageHeader } from 'uiSrc/components'\nimport ExplorePanelTemplate from 'uiSrc/templates/explore-panel/ExplorePanelTemplate'\n\nimport { Page, PageBody } from 'uiSrc/components/base/layout/page'\nimport styles from './styles.module.scss'\nimport { Spacer } from 'uiSrc/components/base/layout'\nimport { Col } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  children: React.ReactNode\n}\n\nconst AutodiscoveryPageTemplate = (props: Props) => {\n  const { children } = props\n  return (\n    <>\n      <PageHeader showInsights />\n      <Spacer size=\"s\" />\n      <ExplorePanelTemplate panelClassName={styles.explorePanel}>\n        <Page className={styles.page}>\n          <PageBody component=\"div\">\n            <Col>{children}</Col>\n          </PageBody>\n        </Page>\n      </ExplorePanelTemplate>\n    </>\n  )\n}\n\nexport default AutodiscoveryPageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/autodiscovery-page-template/index.ts",
    "content": "import AutodiscoveryPageTemplate from './AutodiscoveryPageTemplate'\n\nexport default AutodiscoveryPageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/autodiscovery-page-template/styles.module.scss",
    "content": ".page {\n  padding: 0 16px 16px !important;\n\n  :global {\n    .homePage {\n      display: flex;\n      flex-direction: column;\n      flex-grow: 1;\n    }\n\n    .databaseContainer {\n      flex-grow: 1;\n    }\n\n    .databaseList {\n      flex-grow: 1;\n      height: auto !important;\n    }\n\n    .footerAddDatabase {\n      flex-shrink: 0;\n    }\n  }\n}\n\n.explorePanel {\n  padding-bottom: 16px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/templates/explore-panel/ExplorePanelTemplate.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport ExplorePanelTemplate from './ExplorePanelTemplate'\n\njest.mock('uiSrc/slices/panels/sidePanels', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/sidePanels'),\n  sidePanelsSelector: jest.fn().mockReturnValue({\n    openedPanel: 'insights',\n  }),\n}))\n\ndescribe('ExplorePanelTemplate', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <ExplorePanelTemplate>\n          <div />\n        </ExplorePanelTemplate>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render children and insights panel', () => {\n    render(\n      <ExplorePanelTemplate>\n        <div data-testid=\"children\" />\n      </ExplorePanelTemplate>,\n    )\n\n    expect(screen.getByTestId('children')).toBeInTheDocument()\n    expect(screen.getByTestId('side-panels-insights')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/templates/explore-panel/ExplorePanelTemplate.tsx",
    "content": "import React from 'react'\nimport cx from 'classnames'\nimport { useSelector } from 'react-redux'\nimport { sidePanelsSelector } from 'uiSrc/slices/panels/sidePanels'\nimport SidePanels from 'uiSrc/components/side-panels'\n\nimport styles from './styles.module.scss'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\n\nexport interface Props {\n  children: React.ReactNode\n  panelClassName?: string\n}\n\nconst ExplorePanelTemplate = (props: Props) => {\n  const { children, panelClassName } = props\n  const { openedPanel } = useSelector(sidePanelsSelector)\n  return (\n    <Row full className={styles.mainWrapper}>\n      <Col className={cx(styles.mainPanel, { insightsOpen: !!openedPanel })}>\n        {children}\n      </Col>\n      <div\n        className={cx(styles.insigtsWrapper, {\n          [styles.insightsOpen]: !!openedPanel,\n        })}\n      >\n        <SidePanels panelClassName={panelClassName} />\n      </div>\n    </Row>\n  )\n}\n\nexport default React.memo(ExplorePanelTemplate)\n"
  },
  {
    "path": "redisinsight/ui/src/templates/explore-panel/index.ts",
    "content": "import ExplorePanelTemplate from './ExplorePanelTemplate'\n\nexport default ExplorePanelTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/explore-panel/styles.module.scss",
    "content": ".mainWrapper {\n  height: 100%;\n  width: 100%;\n  position: relative;\n}\n\n.mainPanel {\n  height: 100%;\n  width: 100%;\n\n  &:global(.insightsOpen) {\n    max-width: calc(100% - 460px);\n\n    @media only screen and (max-width: 1440px) {\n      max-width: calc(100% - 380px);\n    }\n  }\n}\n\n.insigtsWrapper {\n  width: 0;\n\n  &.insightsOpen {\n    width: 460px;\n    @media only screen and (max-width: 1440px) {\n      width: 380px;\n    }\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/templates/home-page-template/HomePageTemplate.spec.tsx",
    "content": "import React from 'react'\nimport { cloneDeep, set } from 'lodash'\nimport {\n  initialStateDefault,\n  mockStore,\n  render,\n  screen,\n} from 'uiSrc/utils/test-utils'\n\nimport { appInfoSelector } from 'uiSrc/slices/app/info'\nimport { BuildType } from 'uiSrc/constants/env'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport HomePageTemplate from './HomePageTemplate'\n\njest.mock('uiSrc/slices/app/info', () => ({\n  ...jest.requireActual('uiSrc/slices/app/info'),\n  appInfoSelector: jest.fn().mockReturnValue({\n    server: {},\n  }),\n}))\n\nconst mockAppInfoSelector = jest.requireActual('uiSrc/slices/app/info')\n\nconst ChildComponent = () => <div data-testid=\"child\" />\n\ndescribe('HomePageTemplate', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <HomePageTemplate>\n          <ChildComponent />\n        </HomePageTemplate>,\n      ),\n    ).toBeTruthy()\n  })\n\n  it('should render tabs by default', () => {\n    ;(appInfoSelector as jest.Mock).mockImplementation(() => ({\n      ...mockAppInfoSelector,\n      server: {\n        buildType: BuildType.DockerOnPremise,\n      },\n    }))\n\n    render(\n      <HomePageTemplate>\n        <ChildComponent />\n      </HomePageTemplate>,\n    )\n\n    expect(screen.getByTestId('child')).toBeInTheDocument()\n    expect(screen.getByTestId('home-tabs')).toBeInTheDocument()\n  })\n\n  it('should show feature dependent items when feature flag is on', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: true },\n    )\n\n    render(\n      <HomePageTemplate>\n        <ChildComponent />\n      </HomePageTemplate>,\n      {\n        store: mockStore(initialStoreState),\n      },\n    )\n    expect(screen.queryByTestId('home-page-sso-profile')).toBeInTheDocument()\n  })\n\n  it('should hide feature dependent items when feature flag is off', async () => {\n    const initialStoreState = set(\n      cloneDeep(initialStateDefault),\n      `app.features.featureFlags.features.${FeatureFlags.cloudSso}`,\n      { flag: false },\n    )\n\n    render(\n      <HomePageTemplate>\n        <ChildComponent />\n      </HomePageTemplate>,\n      {\n        store: mockStore(initialStoreState),\n      },\n    )\n    expect(\n      screen.queryByTestId('home-page-sso-profile'),\n    ).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/templates/home-page-template/HomePageTemplate.styles.ts",
    "content": "import styled from 'styled-components'\nimport { Row } from 'uiSrc/components/base/layout/flex'\nimport { type Theme } from 'uiSrc/components/base/theme/types'\n\nexport const PageDefaultHeader = styled(Row)`\n  height: ${({ theme }: { theme: Theme }) => theme.core.space.space800};\n  background-color: ${({ theme }: { theme: Theme }) =>\n    theme.semantic.color.background.neutral100};\n  border-bottom: 1px solid\n    ${({ theme }: { theme: Theme }) => theme.semantic.color.border.neutral500};\n  padding: 0 ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n  margin-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n`\n\nexport const PageWrapper = styled.div`\n  height: calc(\n    100% - ${({ theme }: { theme: Theme }) => theme.core.space.space800} -\n      ${({ theme }: { theme: Theme }) => theme.core.space.space200}\n  );\n  overflow: hidden;\n`\n\nexport const ExplorePanelWrapper = styled.div`\n  padding-bottom: ${({ theme }: { theme: Theme }) => theme.core.space.space200};\n  height: 100%;\n`\n"
  },
  {
    "path": "redisinsight/ui/src/templates/home-page-template/HomePageTemplate.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\n\nimport { ExplorePanelTemplate } from 'uiSrc/templates'\nimport HomeTabs from 'uiSrc/components/home-tabs'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { isAnyFeatureEnabled } from 'uiSrc/utils/features'\nimport { FeatureFlags } from 'uiSrc/constants'\nimport { FeatureFlagComponent, OAuthUserProfile } from 'uiSrc/components'\nimport { OAuthSocialSource } from 'uiSrc/slices/interfaces'\nimport { CopilotTrigger, InsightsTrigger } from 'uiSrc/components/triggers'\n\nimport { FlexGroup, FlexItem } from 'uiSrc/components/base/layout/flex'\nimport { ProgressBarLoader } from 'uiSrc/components/base/display'\nimport {\n  ExplorePanelWrapper,\n  PageDefaultHeader,\n  PageWrapper,\n} from './HomePageTemplate.styles'\nimport { instancesSelector as databaseInstancesSelector } from 'uiSrc/slices/instances/instances'\nimport { instancesSelector as rdiInstancesSelector } from 'uiSrc/slices/rdi/instances'\n\nexport interface Props {\n  children: React.ReactNode\n}\n\nconst HomePageTemplate = (props: Props) => {\n  const { children } = props\n\n  const {\n    [FeatureFlags.databaseChat]: databaseChatFeature,\n    [FeatureFlags.documentationChat]: documentationChatFeature,\n  } = useSelector(appFeatureFlagsFeaturesSelector)\n  const isAnyChatAvailable = isAnyFeatureEnabled([\n    databaseChatFeature,\n    documentationChatFeature,\n  ])\n\n  const { loading: instancesLoading } = useSelector(databaseInstancesSelector)\n  const { loading: rdiLoading } = useSelector(rdiInstancesSelector)\n\n  const loading = instancesLoading || rdiLoading\n\n  return (\n    <>\n      {loading && (\n        <ProgressBarLoader\n          color=\"primary\"\n          data-testid=\"progress-key-stream\"\n          absolute\n        />\n      )}\n      <PageDefaultHeader align=\"center\" justify=\"between\" gap=\"l\">\n        <HomeTabs />\n        <FlexGroup align=\"center\" justify=\"end\" gap=\"l\">\n          {isAnyChatAvailable && (\n            <FlexItem>\n              <CopilotTrigger />\n            </FlexItem>\n          )}\n          <FlexItem>\n            <InsightsTrigger source=\"home page\" />\n          </FlexItem>\n          <FeatureFlagComponent\n            name={[FeatureFlags.cloudSso, FeatureFlags.cloudAds]}\n          >\n            <FlexItem data-testid=\"home-page-sso-profile\">\n              <OAuthUserProfile source={OAuthSocialSource.UserProfile} />\n            </FlexItem>\n          </FeatureFlagComponent>\n        </FlexGroup>\n      </PageDefaultHeader>\n      <PageWrapper>\n        <ExplorePanelWrapper>\n          <ExplorePanelTemplate>{children}</ExplorePanelTemplate>\n        </ExplorePanelWrapper>\n      </PageWrapper>\n    </>\n  )\n}\n\nexport default React.memo(HomePageTemplate)\n"
  },
  {
    "path": "redisinsight/ui/src/templates/home-page-template/index.ts",
    "content": "import HomePageTemplate from './HomePageTemplate'\n\nexport default HomePageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/index.ts",
    "content": "import ExplorePanelTemplate from './explore-panel'\nimport InstancePageTemplate from './instance-page-template'\nimport AutodiscoveryPageTemplate from './autodiscovery-page-template'\nimport HomePageTemplate from './home-page-template'\nimport RdiInstancePageTemplate from './rdi-instance-page-template'\n\nexport {\n  ExplorePanelTemplate,\n  InstancePageTemplate,\n  AutodiscoveryPageTemplate,\n  HomePageTemplate,\n  RdiInstancePageTemplate,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/templates/instance-page-template/InstancePageTemplate.spec.tsx",
    "content": "import React from 'react'\n\nimport { localStorageService } from 'uiSrc/services'\nimport { render } from 'uiSrc/utils/test-utils'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport InstancePageTemplate, { getDefaultSizes } from './InstancePageTemplate'\n\nconst child = <div />\n\ndescribe('InstancePageTemplate', () => {\n  it('should render', () => {\n    expect(\n      render(<InstancePageTemplate>{child}</InstancePageTemplate>),\n    ).toBeTruthy()\n  })\n\n  it('should be called LocalStorage after Component Will Unmount', () => {\n    const defaultSizes = getDefaultSizes()\n    localStorageService.set = jest.fn()\n\n    const { unmount } = render(\n      <InstancePageTemplate>{child}</InstancePageTemplate>,\n    )\n\n    unmount()\n\n    expect(localStorageService.set).toBeCalledWith(\n      BrowserStorageItem.cliResizableContainer,\n      defaultSizes,\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/templates/instance-page-template/InstancePageTemplate.tsx",
    "content": "import React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { useSelector } from 'react-redux'\nimport styled from 'styled-components'\n\nimport InstanceHeader from 'uiSrc/components/instance-header'\nimport { ExplorePanelTemplate } from 'uiSrc/templates'\nimport BottomGroupComponents from 'uiSrc/components/bottom-group-components/BottomGroupComponents'\nimport { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings'\nimport { monitorSelector } from 'uiSrc/slices/cli/monitor'\n\nimport { localStorageService } from 'uiSrc/services'\nimport { BrowserStorageItem } from 'uiSrc/constants'\nimport {\n  ResizableContainer,\n  ResizablePanel,\n  ResizablePanelHandle,\n  Spacer,\n} from 'uiSrc/components/base/layout'\nimport { ImperativePanelGroupHandle } from 'uiSrc/components/base/layout/resize'\nimport { AppNavigation } from 'uiSrc/components'\nimport { AppNavigationActionsProvider } from 'uiSrc/contexts/AppNavigationActionsProvider'\nimport { Nullable } from 'uiSrc/utils'\nimport { useNavigation } from 'uiSrc/components/navigation-menu/hooks/useNavigation'\n\nexport const firstPanelId = 'main-component'\nexport const secondPanelId = 'cli'\n\nexport interface Props {\n  children: React.ReactNode\n}\n\nconst ButtonGroupResizablePanel = styled(ResizablePanel)`\n  flex-basis: 27px !important;\n`\n\nexport const getDefaultSizes = () => {\n  const storedSizes = localStorageService.get(\n    BrowserStorageItem.cliResizableContainer,\n  )\n\n  return storedSizes && Array.isArray(storedSizes) ? storedSizes : [60, 40]\n}\n\nconst roundUpSizes = (sizes: number[]) => [\n  Math.floor(sizes[0]),\n  Math.ceil(sizes[1]),\n]\n\nconst InstancePageTemplate = (props: Props) => {\n  const { children } = props\n  const [sizes, setSizes] = useState<number[]>(getDefaultSizes())\n\n  const { isShowCli, isShowHelper } = useSelector(cliSettingsSelector)\n  const { isShowMonitor } = useSelector(monitorSelector)\n  const { privateRoutes } = useNavigation()\n\n  const ref = useRef<ImperativePanelGroupHandle>(null)\n\n  useEffect(\n    () => () => {\n      setSizes((prevSizes: number[]) => {\n        const roundedSizes = roundUpSizes(prevSizes)\n        localStorageService.set(\n          BrowserStorageItem.cliResizableContainer,\n          roundedSizes,\n        )\n        return roundedSizes\n      })\n    },\n    [],\n  )\n\n  const isShowBottomGroup = isShowCli || isShowHelper || isShowMonitor\n\n  const onPanelWidthChange = useCallback(\n    (newSizes: any) => {\n      if (isShowBottomGroup) {\n        setSizes(roundUpSizes(newSizes))\n      }\n    },\n    [isShowBottomGroup],\n  )\n\n  useEffect(() => {\n    if (isShowBottomGroup) {\n      ref.current?.setLayout(roundUpSizes(sizes))\n    } else {\n      ref.current?.setLayout([100, 0])\n    }\n  }, [isShowBottomGroup])\n\n  const [actions, setActions] = useState<Nullable<React.ReactNode>>(null)\n\n  return (\n    <>\n      <InstanceHeader />\n      <AppNavigation\n        actions={actions}\n        onChange={() => setActions(null)}\n        routes={privateRoutes}\n      />\n      <Spacer size=\"m\" />\n      <ResizableContainer\n        ref={ref}\n        direction=\"vertical\"\n        onLayout={onPanelWidthChange}\n      >\n        <ResizablePanel\n          id={firstPanelId}\n          minSize={7}\n          defaultSize={isShowBottomGroup ? sizes[0] : 100}\n          data-testid={firstPanelId}\n        >\n          <AppNavigationActionsProvider\n            value={{\n              actions,\n              setActions,\n            }}\n          >\n            <ExplorePanelTemplate>{children}</ExplorePanelTemplate>\n          </AppNavigationActionsProvider>\n        </ResizablePanel>\n        <ResizablePanelHandle\n          direction=\"horizontal\"\n          id=\"resize-btn-browser-cli\"\n          data-testid=\"resize-btn-browser-cli\"\n          style={{ display: isShowBottomGroup ? 'inherit' : 'none' }}\n        />\n        {!isShowBottomGroup && <Spacer size=\"l\" />}\n        <ButtonGroupResizablePanel\n          id={secondPanelId}\n          defaultSize={isShowBottomGroup ? sizes[1] : 0}\n          minSize={isShowBottomGroup ? 20 : 0}\n          data-testid={secondPanelId}\n        >\n          <BottomGroupComponents />\n        </ButtonGroupResizablePanel>\n      </ResizableContainer>\n    </>\n  )\n}\n\nexport default InstancePageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/instance-page-template/index.ts",
    "content": "import InstancePageTemplate from './InstancePageTemplate'\n\nexport default InstancePageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/rdi-instance-page-template/RdiInstancePageTemplate.spec.tsx",
    "content": "import React from 'react'\nimport { render, screen } from 'uiSrc/utils/test-utils'\n\nimport RdiInstancePageTemplate from './RdiInstancePageTemplate'\n\njest.mock('uiSrc/slices/panels/sidePanels', () => ({\n  ...jest.requireActual('uiSrc/slices/panels/sidePanels'),\n  sidePanelsSelector: jest.fn().mockReturnValue({\n    openedPanel: 'insights',\n  }),\n}))\n\ndescribe('RdiInstancePageTemplate', () => {\n  it('should render', () => {\n    expect(\n      render(<RdiInstancePageTemplate>1</RdiInstancePageTemplate>),\n    ).toBeTruthy()\n  })\n\n  it('should render children and insights panel', () => {\n    render(\n      <RdiInstancePageTemplate>\n        <div data-testid=\"children\" />\n      </RdiInstancePageTemplate>,\n    )\n\n    expect(screen.getByTestId('children')).toBeInTheDocument()\n    expect(screen.getByTestId('side-panels-insights')).toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/templates/rdi-instance-page-template/RdiInstancePageTemplate.tsx",
    "content": "import React from 'react'\nimport { ExplorePanelTemplate } from 'uiSrc/templates'\nimport { RdiInstancePageContentContainer } from 'uiSrc/templates/rdi-instance-page-template/styles'\n\nexport interface Props {\n  children: React.ReactNode\n}\n\nconst RdiInstancePageTemplate = (props: Props) => {\n  const { children } = props\n\n  return (\n    <RdiInstancePageContentContainer>\n      <ExplorePanelTemplate>{children}</ExplorePanelTemplate>\n    </RdiInstancePageContentContainer>\n  )\n}\n\nexport default RdiInstancePageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/rdi-instance-page-template/index.ts",
    "content": "import RdiInstancePageTemplate from './RdiInstancePageTemplate'\n\nexport default RdiInstancePageTemplate\n"
  },
  {
    "path": "redisinsight/ui/src/templates/rdi-instance-page-template/styles.module.scss",
    "content": ".page {\n  height: 100%;\n  padding-bottom: 16px;\n}\n"
  },
  {
    "path": "redisinsight/ui/src/templates/rdi-instance-page-template/styles.ts",
    "content": "import styled from 'styled-components'\nimport { FlexItem } from 'uiSrc/components/base/layout/flex'\n\nexport const RdiInstancePageContentContainer = styled(FlexItem)`\n  height: calc(100% - 210px);\n`\n"
  },
  {
    "path": "redisinsight/ui/src/types/index.d.ts",
    "content": "import { Environment } from 'monaco-editor/esm/vs/editor/editor.api'\nimport { Buffer } from 'buffer'\n// eslint-disable-next-line import/order\nimport { Nullable } from 'uiSrc/utils'\nimport { KeyValueCompressor } from 'uiSrc/constants'\nimport {\n  RedisResponseBuffer,\n  RedisString,\n  UintArray,\n} from 'uiSrc/slices/interfaces'\nimport { Config } from 'uiSrc/config'\nimport { IPCHandler } from '../../../desktop/preload'\n\ndeclare global {\n  interface Window {\n    ri: RedisInsight\n    Buffer: typeof Buffer\n    app: WindowApp\n    windowId?: string\n    MonacoEnvironment: Environment\n    readonly __RI_PROXY_PATH__: string\n  }\n}\n\ndeclare global {\n  let riConfig: Config\n}\n\nexport interface RedisInsight {\n  bufferToUTF8: (reply: RedisResponseBuffer) => string\n  bufferToASCII: (reply: RedisResponseBuffer) => string\n  UintArrayToString: (reply: UintArray) => string\n  UTF8ToBuffer: (reply: string) => RedisResponseBuffer\n  ASCIIToBuffer: (reply: string) => RedisResponseBuffer\n  stringToBuffer: (reply: string) => RedisResponseBuffer\n  anyToBuffer: (reply: UintArray) => RedisResponseBuffer\n  bufferToString: (reply: RedisString) => string\n  hexToBuffer: (reply: string) => RedisResponseBuffer\n  bufferToHex: (reply: RedisResponseBuffer) => string\n  bufferToBinary: (reply: RedisResponseBuffer) => string\n  binaryToBuffer: (reply: string) => RedisResponseBuffer\n  getCompressor: (reply: RedisResponseBuffer) => Nullable<KeyValueCompressor>\n}\n\nexport interface WindowApp {\n  sendWindowId: any\n  cloudOauthCallback: any\n  azureOauthCallback: any\n  deepLinkAction: any\n  updateAvailable: any\n  ipc: IPCHandler\n  config: {\n    apiPort: string\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/api/chatbots.ts",
    "content": "import { CustomHeaders } from 'uiSrc/constants/api'\nimport { isStatusSuccessful } from 'uiSrc/utils'\nimport ApiStatusCode from '../../constants/apiStatusCode'\n\nconst TIMEOUT_FOR_MESSAGE_REQUEST = 30_000\n\nexport const getStreamedAnswer = async (\n  url: string,\n  message: string,\n  {\n    onMessage,\n    onFinish,\n    onError,\n  }: {\n    onMessage?: (message: string) => void\n    onFinish?: () => void\n    onError?: (error: unknown) => void\n  },\n) => {\n  try {\n    const controller = new AbortController()\n    const timeoutId = setTimeout(() => {\n      controller.abort()\n    }, TIMEOUT_FOR_MESSAGE_REQUEST)\n\n    const response = await fetch(url, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        Accept: 'text/event-stream',\n        [CustomHeaders.WindowId]: window.windowId || '',\n      },\n      body: JSON.stringify({ content: message }),\n      signal: controller.signal,\n    })\n\n    clearTimeout(timeoutId)\n\n    const reader = response\n      .body!.pipeThrough(new TextDecoderStream())\n      .getReader()\n    if (!isStatusSuccessful(response.status)) {\n      const { value } = await reader.read()\n\n      const errorResponse = value ? JSON.parse(value) : {}\n      const extendedResponseError = {\n        errorCode: errorResponse.errorCode ?? '',\n        details: errorResponse.details ?? {},\n      }\n      const error = Object.assign(response, extendedResponseError)\n      onError?.(error)\n      return\n    }\n\n    // eslint-disable-next-line no-constant-condition\n    while (true) {\n      // eslint-disable-next-line no-await-in-loop\n      const { value, done } = await reader!.read()\n      if (done) {\n        onFinish?.()\n        break\n      }\n      onMessage?.(value)\n    }\n  } catch (error: any) {\n    onError?.(\n      error?.name === 'AbortError'\n        ? { status: ApiStatusCode.Timeout, statusText: 'ERRTIMEOUT' }\n        : error,\n    )\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/api/index.ts",
    "content": "export * from './chatbots'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/apiResponse.ts",
    "content": "import { AxiosError } from 'axios'\nimport { first, isArray, get } from 'lodash'\nimport {\n  AddRedisDatabaseStatus,\n  EnhancedAxiosError,\n  ErrorOptions,\n  IBulkOperationResult,\n} from 'uiSrc/slices/interfaces'\nimport { parseCustomError } from 'uiSrc/utils'\n\nexport const DEFAULT_ERROR_MESSAGE = 'Something was wrong!'\n\nexport const getAxiosError = (error: EnhancedAxiosError): AxiosError => {\n  if (error?.response?.data.errorCode) {\n    return parseCustomError(error.response.data)\n  }\n  return error\n}\n\nexport const createAxiosError = (options: ErrorOptions): AxiosError =>\n  ({\n    response: {\n      data: options,\n    },\n  }) as AxiosError\n\nexport const getApiErrorCode = (error: AxiosError) => error?.response?.status\n\nexport function getApiErrorMessage(error: AxiosError): string {\n  // @ts-ignore\n  const errorMessage = error?.response?.data?.message\n  if (!error || !error.response) {\n    return DEFAULT_ERROR_MESSAGE\n  }\n  if (isArray(errorMessage)) {\n    return first(errorMessage)\n  }\n\n  return errorMessage\n}\n\nexport function getApiErrorName(error: AxiosError): string {\n  return get(error, 'response.data.name', 'Error') ?? ''\n}\n\nexport function getApiErrorsFromBulkOperation(\n  operations: IBulkOperationResult[],\n  ...errorNames: string[]\n): AxiosError[] {\n  let result: AxiosError<any>[] = []\n  try {\n    result = operations\n      .filter((item) => item.status === AddRedisDatabaseStatus.Fail)\n      .filter((item) =>\n        errorNames.length ? errorNames.includes(item?.error?.name) : true,\n      )\n      .map((item) => ({ response: { data: item.error } }) as AxiosError)\n  } catch (e) {\n    // continue regardless of error\n  }\n  return result\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/bigString.ts",
    "content": "import { isString } from 'lodash'\nimport { getConfig } from 'uiSrc/config'\nimport {\n  RedisResponseBuffer,\n  RedisResponseBufferType,\n  RedisString,\n} from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils/types'\n\nconst BIG_STRING_PREFIX = getConfig().app.truncatedStringPrefix\nconst BIG_STRING_PREFIX_BUFFER = Uint8Array.from(\n  Array.from(BIG_STRING_PREFIX).map((letter) => letter.charCodeAt(0)),\n)\n\nconst redisResponseBufferStartsWith = (\n  value: RedisResponseBuffer,\n  subBuffer: Uint8Array,\n) => {\n  if (subBuffer.length > value.length) {\n    return false\n  }\n\n  return subBuffer.every((v, i) => v === value[i])\n}\n\nexport const isTruncatedString = (value: Nullable<RedisString>) => {\n  if (!value) {\n    return false\n  }\n\n  try {\n    if (isString(value)) {\n      if (value[0] === '\"') {\n        return value.indexOf(BIG_STRING_PREFIX) === 1\n      }\n\n      return value.startsWith(BIG_STRING_PREFIX)\n    }\n\n    if (value.type === RedisResponseBufferType.Buffer && value.data) {\n      return redisResponseBufferStartsWith(value.data, BIG_STRING_PREFIX_BUFFER)\n    }\n  } catch (e) {\n    // ignore error\n  }\n\n  return false\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/calculateTextareaLines.ts",
    "content": "const APPROXIMATE_WIDTH_OF_SIGN = 7.05\n\nexport const calculateTextareaLines = (\n  text: string,\n  width: number = 1,\n  signWidth = APPROXIMATE_WIDTH_OF_SIGN,\n) =>\n  text\n    ?.split('\\n')\n    .reduce(\n      (prev, current) => Math.ceil((current.length * signWidth) / width) + prev,\n      0,\n    ) || 1\n"
  },
  {
    "path": "redisinsight/ui/src/utils/capability.ts",
    "content": "import { OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { store } from 'uiSrc/slices/store'\nimport { Nullable } from 'uiSrc/utils'\nimport { findMarkdownPath } from 'uiSrc/utils/workbench'\n\nconst getCapability = (\n  telemetryName: string = '',\n  name: string = '',\n  path: Nullable<string> = null,\n) => ({\n  telemetryName,\n  name,\n  path,\n})\n\nexport const getSourceTutorialByCapability = (moduleName = '') =>\n  `${moduleName}_tutorial`\n\nexport const getTutorialCapability = (source: any = '') => {\n  switch (source) {\n    // RediSearch\n    case OAuthSocialSource.RediSearch:\n    case OAuthSocialSource.BrowserSearch:\n    case getSourceTutorialByCapability(RedisDefaultModules.SearchLight):\n    case getSourceTutorialByCapability(RedisDefaultModules.Search):\n    case getSourceTutorialByCapability(RedisDefaultModules.FT):\n    case getSourceTutorialByCapability(RedisDefaultModules.FTL):\n      return getCapability(\n        'searchAndQuery',\n        'Redis Query Engine',\n        findMarkdownPath(store.getState()?.workbench?.tutorials?.items, {\n          id: 'sq-intro',\n        }),\n      )\n\n    // RedisJSON\n    case OAuthSocialSource.RedisJSON:\n    case getSourceTutorialByCapability(RedisDefaultModules.ReJSON):\n      return getCapability(\n        'JSON',\n        'JSON data structure',\n        findMarkdownPath(store.getState()?.workbench?.tutorials?.items, {\n          id: 'ds-json-intro',\n        }),\n      )\n\n    // TimeSeries\n    case OAuthSocialSource.RedisTimeSeries:\n    case getSourceTutorialByCapability(RedisDefaultModules.TimeSeries):\n      return getCapability(\n        'timeSeries',\n        'Time series data structure',\n        findMarkdownPath(store.getState()?.workbench?.tutorials?.items, {\n          id: 'ds-ts-intro',\n        }),\n      )\n\n    // Bloom\n    case OAuthSocialSource.RedisBloom:\n    case getSourceTutorialByCapability(RedisDefaultModules.Bloom):\n      return getCapability(\n        'probabilistic',\n        'Probabilistic data structures',\n        findMarkdownPath(store.getState()?.workbench?.tutorials?.items, {\n          id: 'ds-prob-intro',\n        }),\n      )\n\n    default:\n      return getCapability()\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/cliHelper.tsx",
    "content": "import React from 'react'\nimport { Dispatch, PayloadAction } from '@reduxjs/toolkit'\nimport parse from 'html-react-parser'\nimport { isUndefined } from 'lodash'\n\nimport { localStorageService } from 'uiSrc/services'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport {\n  resetOutput,\n  updateCliCommandHistory,\n} from 'uiSrc/slices/cli/cli-output'\nimport { BrowserStorageItem, ICommands, CommandGroup } from 'uiSrc/constants'\nimport { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants'\nimport { SelectCommand } from 'uiSrc/constants/cliOutput'\nimport { RedisDefaultModules, COMMAND_MODULES } from 'uiSrc/slices/interfaces'\n\nimport { getCommandsForExecution } from 'uiSrc/utils/monaco/monacoUtils'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport formatToText from './transformers/cliTextFormatter'\nimport { getDbIndex } from './longNames'\n\nexport enum CliPrefix {\n  Cli = 'cli',\n  QueryCard = 'query-card',\n}\n\ninterface IGroupModeCommand {\n  command: string\n  response: string\n  status: CommandExecutionStatus\n}\n\nconst cliParseTextResponseWithOffset = (\n  text: string = '',\n  command: string = '',\n  status: CommandExecutionStatus = CommandExecutionStatus.Success,\n) => [cliParseTextResponse(text, command, status), '\\n']\n\nconst replaceEmptyValue = (value: any) => {\n  if (isUndefined(value) || value === '' || value === false) {\n    return '(nil)'\n  }\n  return value\n}\n\nconst cliParseTextResponse = (\n  text: string | JSX.Element = '',\n  command: string = '',\n  status: CommandExecutionStatus = CommandExecutionStatus.Success,\n  prefix: CliPrefix = CliPrefix.Cli,\n  isParse: boolean = false,\n) => (\n  <span\n    key={Math.random()}\n    className={\n      status === CommandExecutionStatus.Success\n        ? `${prefix}-output-response-success`\n        : `${prefix}-output-response-fail`\n    }\n    data-testid={\n      status === CommandExecutionStatus.Success\n        ? `${prefix}-output-response-success`\n        : `${prefix}-output-response-fail`\n    }\n  >\n    {isParse ? parse(formatToText(text, command)) : formatToText(text, command)}\n  </span>\n)\n\nconst cliCommandOutput = (command: string, dbIndex = 0) => [\n  '\\n',\n  bashTextValue(dbIndex),\n  cliCommandWrapper(command),\n  '\\n',\n]\n\nconst bashTextValue = (dbIndex = 0) => `${getDbIndex(dbIndex)} > `.trimStart()\n\nconst cliCommandWrapper = (command: string) => (\n  <span\n    className=\"cli-command-wrapper\"\n    data-testid=\"cli-command-wrapper\"\n    key={Math.random()}\n  >\n    {command}\n  </span>\n)\n\nconst wbSummaryCommand = (command: string, db?: number) => (\n  <span className=\"cli-command-wrapper\" data-testid=\"wb-command\">\n    {`${getDbIndex(db)} > ${command} \\n`}\n  </span>\n)\n\nconst clearOutput = (dispatch: any) => {\n  dispatch(resetOutput())\n}\n\nconst cliParseCommandsGroupResult = (\n  result: IGroupModeCommand,\n  db?: number,\n) => {\n  const executionCommand = wbSummaryCommand(result.command, db)\n\n  let executionResult = []\n  if (result.status === CommandExecutionStatus.Success) {\n    executionResult = formatToText(\n      replaceEmptyValue(result.response),\n      result.command,\n    ).split('\\n')\n  } else {\n    executionResult = [\n      cliParseTextResponse(\n        result.response || '(nil)',\n        result.command,\n        result.status,\n      ),\n    ]\n  }\n\n  return [executionCommand, ...executionResult]\n}\n\nconst updateCliHistoryStorage = (\n  command: string = '',\n  dispatch: Dispatch<PayloadAction<string[]>>,\n) => {\n  if (!command) {\n    return\n  }\n  const maxCountCommandHistory = 20\n\n  const commandHistoryPrev =\n    localStorageService.get(BrowserStorageItem.cliInputHistory) ?? []\n\n  const commandHistory = [command?.trim()]\n    .concat(commandHistoryPrev)\n    .slice(0, maxCountCommandHistory)\n\n  localStorageService.set(\n    BrowserStorageItem.cliInputHistory,\n    commandHistory.slice(0, maxCountCommandHistory),\n  )\n\n  dispatch?.(updateCliCommandHistory?.(commandHistory))\n}\n\nconst checkUnsupportedCommand = (\n  unsupportedCommands: string[],\n  commandLine: string,\n) =>\n  unsupportedCommands?.find((command) =>\n    commandLine?.trim().toLowerCase().startsWith(command?.toLowerCase()),\n  )\n\nconst checkBlockingCommand = (\n  blockingCommands: string[],\n  commandLine: string,\n) =>\n  blockingCommands?.find((command) =>\n    commandLine?.trim().toLowerCase().startsWith(command),\n  )\n\nconst checkCommandModule = (command: string) => {\n  switch (true) {\n    case command.startsWith(ModuleCommandPrefix.RediSearch): {\n      return RedisDefaultModules.Search\n    }\n    case command.startsWith(ModuleCommandPrefix.JSON): {\n      return RedisDefaultModules.ReJSON\n    }\n    case command.startsWith(ModuleCommandPrefix.TimeSeries): {\n      return RedisDefaultModules.TimeSeries\n    }\n    case command.startsWith(ModuleCommandPrefix.BF):\n    case command.startsWith(ModuleCommandPrefix.CF):\n    case command.startsWith(ModuleCommandPrefix.CMS):\n    case command.startsWith(ModuleCommandPrefix.TDIGEST):\n    case command.startsWith(ModuleCommandPrefix.TOPK): {\n      return RedisDefaultModules.Bloom\n    }\n    default: {\n      return null\n    }\n  }\n}\n\nconst getUnsupportedModulesFromQuery = (\n  loadedModules: AdditionalRedisModule[],\n  query: string = '',\n): Set<RedisDefaultModules> => {\n  const result = new Set<RedisDefaultModules>()\n  getCommandsForExecution(query).forEach((command) => {\n    const module = checkUnsupportedModuleCommand(loadedModules, command)\n    if (module) {\n      result.add(module)\n    }\n  })\n  return result\n}\n\nconst checkUnsupportedModuleCommand = (\n  loadedModules: AdditionalRedisModule[],\n  commandLine: string,\n) => {\n  const command = commandLine?.trim().toUpperCase()\n\n  const commandModule = checkCommandModule(command)\n  if (!commandModule) {\n    return null\n  }\n  const isModuleLoaded = loadedModules?.some(({ name }) =>\n    COMMAND_MODULES[commandModule].some((module) => name === module),\n  )\n\n  if (isModuleLoaded) {\n    return null\n  }\n\n  return commandModule\n}\n\nconst getDbIndexFromSelectQuery = (query: string): number => {\n  const [command, ...args] = query.trim().split(' ')\n  if (command.toLowerCase() !== SelectCommand.toLowerCase()) {\n    throw new Error('Invalid command')\n  }\n  try {\n    return parseInt(args[0].replace(/['\"]/g, '').trim())\n  } catch (e) {\n    throw Error('Parsing error')\n  }\n}\n\nconst getCommandNameFromQuery = (\n  query: string,\n  commandsSpec: ICommands = {},\n  queryLimit: number = 50,\n): string | undefined => {\n  try {\n    const [command, firstArg] = query.slice(0, queryLimit).trim().split(/\\s+/)\n    if (commandsSpec[`${command} ${firstArg}`.toUpperCase()])\n      return `${command} ${firstArg}`\n    return command\n  } catch (error) {\n    return undefined\n  }\n}\n\nconst DEPRECATED_MODULE_PREFIXES = [ModuleCommandPrefix.Graph]\n\nconst DEPRECATED_MODULE_GROUPS = [CommandGroup.Graph]\n\nconst checkDeprecatedModuleCommand = (command: string) =>\n  DEPRECATED_MODULE_PREFIXES.some((prefix) =>\n    command.toUpperCase().startsWith(prefix),\n  )\n\nconst checkDeprecatedCommandGroup = (item: string) =>\n  DEPRECATED_MODULE_GROUPS.some((group) => group === item)\n\nconst removeDeprecatedModuleCommands = (commands: string[]) =>\n  commands.filter((command) => !checkDeprecatedModuleCommand(command))\n\nexport {\n  cliParseTextResponse,\n  cliParseTextResponseWithOffset,\n  cliParseCommandsGroupResult,\n  cliCommandOutput,\n  bashTextValue,\n  cliCommandWrapper,\n  clearOutput,\n  updateCliHistoryStorage,\n  checkCommandModule,\n  checkUnsupportedCommand,\n  checkBlockingCommand,\n  checkUnsupportedModuleCommand,\n  getDbIndexFromSelectQuery,\n  getCommandNameFromQuery,\n  wbSummaryCommand,\n  replaceEmptyValue,\n  removeDeprecatedModuleCommands,\n  checkDeprecatedModuleCommand,\n  checkDeprecatedCommandGroup,\n  getUnsupportedModulesFromQuery,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/cliOutputActions.ts",
    "content": "import { AxiosError } from 'axios'\nimport {\n  cliUnsupportedCommandsSelector,\n  updateCliClientAction,\n} from 'uiSrc/slices/cli/cli-settings'\nimport { CommandMonitor } from 'uiSrc/constants'\nimport { cliParseTextResponseWithOffset } from 'uiSrc/utils/cliHelper'\nimport {\n  cliTexts,\n  ConnectionSuccessOutputText,\n} from 'uiSrc/components/messages/cli-output/cliOutput'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { concatToOutput } from 'uiSrc/slices/cli/cli-output'\n\nimport { store } from 'uiSrc/slices/store'\nimport ApiErrors from 'uiSrc/constants/apiErrors'\nimport { getApiErrorMessage, getApiErrorName } from 'uiSrc/utils/apiResponse'\n\nexport function processUnsupportedCommand(\n  command: string = '',\n  unsupportedCommand: string = '',\n  onSuccessAction?: () => void,\n) {\n  const { getState, dispatch } = store\n\n  const state = getState()\n  // Due to requirements, the monitor command should not appear in the list of supported commands\n  // That is why we exclude it here\n  const unsupportedCommands = cliUnsupportedCommandsSelector(state, [\n    CommandMonitor.toLowerCase(),\n  ])\n\n  dispatch(\n    concatToOutput(\n      cliParseTextResponseWithOffset(\n        cliTexts.CLI_UNSUPPORTED_COMMANDS(\n          command.slice(0, unsupportedCommand.length),\n          unsupportedCommands.join(', '),\n        ),\n        command,\n        CommandExecutionStatus.Fail,\n      ),\n    ),\n  )\n\n  onSuccessAction?.()\n}\n\nexport function processUnrepeatableNumber(\n  command: string = '',\n  onSuccessAction?: () => void,\n) {\n  store.dispatch(\n    concatToOutput(\n      cliParseTextResponseWithOffset(\n        cliTexts.REPEAT_COUNT_INVALID,\n        command,\n        CommandExecutionStatus.Fail,\n      ),\n    ),\n  )\n\n  onSuccessAction?.()\n}\n\nexport function handleRecreateClient(command = ''): void {\n  const { getState, dispatch } = store\n\n  const state = getState()\n  const { cliClientUuid } = state.cli.settings\n\n  if (cliClientUuid) {\n    dispatch(\n      concatToOutput(\n        cliParseTextResponseWithOffset(\n          cliTexts.CONNECTION_CLOSED,\n          command,\n          CommandExecutionStatus.Fail,\n        ),\n      ),\n    )\n    dispatch(\n      updateCliClientAction(\n        cliClientUuid,\n        () => dispatch(concatToOutput(ConnectionSuccessOutputText)),\n        (message: string) =>\n          dispatch(\n            concatToOutput(\n              cliParseTextResponseWithOffset(\n                `${message}`,\n                command,\n                CommandExecutionStatus.Fail,\n              ),\n            ),\n          ),\n      ),\n    )\n  }\n}\n\nexport function cliCommandError(error: AxiosError, command: string) {\n  const { getState, dispatch } = store\n  const errorName = getApiErrorName(error)\n  const errorMessage = getApiErrorMessage(error)\n\n  const { cliClientUuid } = getState()?.cli?.settings\n\n  if (errorName === ApiErrors.ClientNotFound && cliClientUuid) {\n    handleRecreateClient(command)\n  } else {\n    dispatch(\n      concatToOutput(\n        cliParseTextResponseWithOffset(\n          errorMessage,\n          command,\n          CommandExecutionStatus.Fail,\n        ),\n      ),\n    )\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/colors.ts",
    "content": "export type RGBColor = [number, number, number]\n\nexport interface ColorScheme {\n  cHueStart: number\n  cHueRange: number\n  cSaturation: number\n  cLightness: number\n}\n\nconst HSLToRGB = (h: number, sI: number, lI: number): RGBColor => {\n  const s = sI / 100\n  const l = lI / 100\n  const k = (n: number) => (n + h / 30) % 12\n  const a = s * Math.min(l, 1 - l)\n  const f = (n: number) =>\n    l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))\n\n  return [255 * f(0), 255 * f(8), 255 * f(4)]\n}\n\nconst PBC = (r: number, g: number, b: number): number =>\n  Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) / 255\n\nconst correctBrightness = (rgb: RGBColor, cLightness: number) =>\n  1 / ((PBC(...rgb) * 100) / cLightness)\n\nconst applyBrightnessToRGB = (rgb: RGBColor, cLightness: number): RGBColor => {\n  const [r, g, b] = rgb\n  return [\n    Math.round(r * correctBrightness([r, g, b], cLightness)),\n    Math.round(g * correctBrightness([r, g, b], cLightness)),\n    Math.round(b * correctBrightness([r, g, b], cLightness)),\n  ] as RGBColor\n}\n\nconst getRGBColorByScheme = (\n  index: number,\n  shift: number,\n  colorScheme: ColorScheme,\n): RGBColor => {\n  const nc = index * shift + colorScheme.cHueStart\n  const rgb: RGBColor = HSLToRGB(\n    nc,\n    colorScheme.cSaturation,\n    colorScheme.cLightness,\n  )\n  return applyBrightnessToRGB(rgb, colorScheme.cLightness)\n}\n\nconst rgb = (rgb: RGBColor) => `rgb(${rgb.join(', ')})`\n\nexport {\n  HSLToRGB,\n  correctBrightness,\n  applyBrightnessToRGB,\n  getRGBColorByScheme,\n  rgb,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/commands.ts",
    "content": "import {\n  flatten,\n  isArray,\n  isEmpty,\n  isNumber,\n  reject,\n  toNumber,\n  isNaN,\n  isInteger,\n  isString,\n  forEach,\n} from 'lodash'\nimport {\n  CommandArgsType,\n  CommandProvider,\n  ICommand,\n  ICommandArg,\n  ICommandArgGenerated,\n} from 'uiSrc/constants'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\n\nenum ArgumentType {\n  INTEGER = 'integer',\n  DOUBLE = 'double',\n  STRING = 'string',\n  UNIX_TIME = 'unix-time',\n  PATTERN = 'pattern',\n  KEY = 'key',\n  ONEOF = 'oneof',\n  BLOCK = 'block',\n  PURE_TOKEN = 'pure-token',\n  COMMAND = 'command',\n  ENUM = 'enum', // temporary for backward compatibility\n}\n\nexport class Argument {\n  protected stack = []\n\n  protected name: string\n\n  protected type: ArgumentType\n\n  protected optional: boolean\n\n  protected multiple: boolean\n\n  protected multipleToken: boolean\n\n  protected token: string | null\n\n  protected display: string\n\n  protected arguments: Argument[]\n\n  protected enum: string[]\n\n  constructor(data: Record<string, any>) {\n    this.stack = []\n    this.name = data?.name\n    this.type = data?.type\n    this.optional = !!data?.optional\n    this.multiple = !!data?.multiple\n    this.multipleToken = !!data?.multiple_token\n    this.token = data?.token\n    this.display = data?.display_text || data?.command || this.name\n    this.enum = data?.enum\n    // todo: why we need this?\n    if (this.token === '') {\n      this.token = '\"\"'\n    }\n    this.arguments = (\n      (data?.arguments || data?.block || []) as Record<string, any>[]\n    ).map((childArg) => new Argument(childArg))\n  }\n\n  public syntax(opts: Record<string, any> = {}): string {\n    const showTypes = !!opts?.showTypes\n    const pureName = !!opts?.pureName\n    const onlyMandatory = !!opts?.onlyMandatory\n\n    if (onlyMandatory && this.optional) {\n      return ''\n    }\n\n    let args = ''\n\n    switch (this.type) {\n      case ArgumentType.BLOCK:\n        args += this.arguments.map((arg) => arg.syntax()).join(' ')\n        break\n      case ArgumentType.ONEOF:\n        args += this.arguments.map((arg) => arg.syntax()).join(' | ')\n        break\n      case ArgumentType.ENUM:\n        args += this.enum?.join(' | ')\n        break\n      case ArgumentType.PURE_TOKEN:\n        break\n      default:\n        args += this.display\n        if (showTypes) {\n          args += `:${this.type}`\n        }\n    }\n\n    let syntax = ''\n\n    if (this.token) {\n      syntax += this.token\n      if (this.type !== ArgumentType.PURE_TOKEN) {\n        syntax += ' '\n      }\n    }\n\n    let multipleSyntax = ''\n    if (this.multiple && !pureName) {\n      if (this.multipleToken) {\n        multipleSyntax += `${args} [${this.token} ${args} ...]`\n      } else {\n        multipleSyntax += `${args} [${args} ...]`\n      }\n    } else {\n      multipleSyntax += args\n    }\n\n    // if (this.type === ArgumentType.ONEOF && (!this.optional || this.token)) {\n    //   multipleSyntax = `<${multipleSyntax}>`\n    // }\n\n    syntax += multipleSyntax\n\n    if (this.optional) {\n      syntax = `[${syntax}]`\n    }\n\n    return syntax\n  }\n}\n\nexport const getComplexityShortNotation = (\n  complexity: string[] | string,\n): string => {\n  const value = isArray(complexity) ? complexity.join(' ') : complexity\n  return value.endsWith(')') && value.startsWith('O') ? value : ''\n}\n\nconst generateArgName = (\n  provider: string,\n  arg: ICommandArg,\n  pureName: boolean = false,\n  onlyMandatory: boolean = false,\n): string | string[] => {\n  try {\n    // todo: temporary workaround until all commands providers will be unified\n    if ([CommandProvider.Main].includes(<CommandProvider.Main>provider)) {\n      return new Argument(arg).syntax({\n        onlyMandatory,\n        pureName,\n      })\n    }\n\n    // We need this for backward compatibility now\n    const {\n      name: propName = '',\n      enum: enumArg,\n      command,\n      optional,\n      multiple,\n      type,\n      block,\n    } = arg\n\n    if (onlyMandatory && optional) return ''\n\n    const name = isArray(propName) ? propName?.join(' ') : propName\n    const enumName = enumArg && (!pureName || !name) ? enumArg?.join('|') : name\n    const commandName = command\n      ? command + (enumName ? ` ${enumName}` : '')\n      : enumName\n    const optionalName = optional ? `[${commandName}]` : commandName\n\n    const multipleNameTemp = [\n      ...commandName?.split?.(' '),\n      `[${commandName} ...]`,\n    ]\n    const multipleName = optional\n      ? `[${multipleNameTemp.join(' ')}]`\n      : multipleNameTemp\n\n    if (type === CommandArgsType.Block && isArray(block)) {\n      const blocks = flatten(\n        block?.map?.((block) =>\n          generateArgName(provider, block, pureName, onlyMandatory),\n        ),\n      )\n      return optional ? `[${blocks?.join?.(' ')}]` : blocks\n    }\n\n    return (\n      (multiple && !pureName && !onlyMandatory ? multipleName : optionalName) ??\n      ''\n    )\n  } catch (e) {\n    return ''\n  }\n}\n\nexport const generateArgs = (\n  provider = 'unknown',\n  args: ICommandArg[],\n): ICommandArgGenerated[] =>\n  flatten(\n    args.map((arg) => ({\n      ...arg,\n      generatedName: generateArgName(provider, arg, true),\n    })),\n  )\n\nexport const generateArgsNames = (\n  provider: string = 'unknown',\n  args: ICommandArg[],\n  pureName: boolean = false,\n  onlyMandatory: boolean = false,\n): string[] =>\n  reject(\n    flatten(\n      args.map((arg) =>\n        generateArgName(provider, arg, pureName, onlyMandatory),\n      ),\n    ),\n    isEmpty,\n  )\n\nexport const generateArgsForInsertText = (\n  argsNames: string[],\n  separator: string = ' ',\n): string =>\n  `${\n    !argsNames.length\n      ? ''\n      : argsNames\n          .join(' ')\n          .split(' ')\n          // eslint-disable-next-line sonarjs/no-nested-template-literals\n          .map(\n            (arg: string, i: number) =>\n              `\\${${i + 1}:${arg}}${argsNames.length !== i + 1 ? separator : ''}`,\n          )\n          .join('')\n  }`\n\nexport const getDocUrlForCommand = (commandName: string): string => {\n  const command = commandName.replace(/\\s+/g, '-').toLowerCase()\n  return getUtmExternalLink(\n    `https://redis.io/docs/latest/commands/${command}`,\n    {\n      campaign: 'redisinsight_command_helper',\n    },\n  )\n}\n\nexport const getCommandRepeat = (command = ''): [string, number] => {\n  const [countRepeatStr = '', ...restCommand] = command?.split?.(' ')\n  let countRepeat = toNumber(countRepeatStr)\n  let commandLine = restCommand.join(' ')\n  if (!isNumber(countRepeat) || isNaN(countRepeat) || !command) {\n    countRepeat = 1\n    commandLine = command\n  }\n\n  return [commandLine, countRepeat]\n}\n\nexport const isRepeatCountCorrect = (number: number): boolean =>\n  number >= 1 && isInteger(number)\n\ntype RedisArg = string | number | Array<RedisArg>\nexport const generateRedisCommand = (\n  command: string,\n  ...rest: Array<RedisArg>\n) => {\n  let commandToSend = command\n\n  forEach(rest, (arg: RedisArg) => {\n    if ((isString(arg) && arg) || isNumber(arg)) {\n      commandToSend += ` \"${arg}\"`\n    }\n    if (isArray(arg)) {\n      commandToSend += generateRedisCommand('', ...arg)\n    }\n  })\n\n  return commandToSend.replace(/\\s\\s+/g, ' ')\n}\n\nexport const arrayCommandToString = (command: string[] | null) => {\n  if (isArray(command)) {\n    return command\n      .map((arg) => (arg.includes(' ') ? `\"${arg}\"` : arg))\n      .join(' ')\n  }\n\n  return null\n}\n\nexport const getCommandMarkdown = (\n  command: ICommand,\n  docUrl: string = '',\n): string => {\n  const linkMore = !docUrl ? '' : ` [Read more](${docUrl})`\n  const lines: string[] = [command?.summary + linkMore]\n  if (command?.arguments?.length) {\n    // TODO: use i18n file for texts\n    lines.push('### Arguments:')\n    generateArgs(command?.provider, command.arguments).forEach(\n      (arg: ICommandArgGenerated): void => {\n        const { multiple, optional } = arg\n        const type: string = multiple\n          ? 'multiple'\n          : optional\n            ? 'optional'\n            : 'required'\n        const argDescription: string = `_${type}_ \\`${arg.generatedName}\\``\n        lines.push(argDescription)\n      },\n    )\n  }\n  return lines.join('\\n'.repeat(2))\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/common.ts",
    "content": "import { trim } from 'lodash'\nimport { IpcInvokeEvent } from 'uiSrc/electron/constants'\nimport { getConfig } from 'uiSrc/config'\n\nconst riConfig = getConfig()\nconst isDevelopment = riConfig.app.env === 'development'\nconst isWebApp = riConfig.app.type === 'web'\nconst { apiPort } = window.app?.config || { apiPort: riConfig.api.port }\nconst hostedApiBaseUrl = riConfig.api.hostedBaseUrl\n\nexport const getSocketApiUrl = (path = '') => {\n  let baseUrl = getBaseApiUrl()\n  try {\n    const url = new URL(baseUrl)\n    baseUrl = url.origin\n  } catch (e) {\n    console.error(e)\n  }\n  return `${baseUrl}/${trim(path, '/')}`\n}\n\nexport const getBaseApiUrl = () => {\n  if (hostedApiBaseUrl) {\n    return hostedApiBaseUrl\n  }\n\n  return !isDevelopment && isWebApp\n    ? window.location.origin\n    : `${riConfig.api.baseUrl}:${apiPort}`\n}\n\nexport const getProxyPath = () => {\n  if (window.__RI_PROXY_PATH__) {\n    return `/${window.__RI_PROXY_PATH__}/socket.io`\n  }\n\n  if (riConfig.api.hostedSocketProxyPath) {\n    return riConfig.api.hostedSocketProxyPath\n  }\n\n  return '/socket.io'\n}\n\ntype Node = number | string | JSX.Element\n\nexport const getNodeText = (node: Node | Node[]): string => {\n  if (['string', 'number'].includes(typeof node)) return node?.toString()\n  if (node instanceof Array) return node.map(getNodeText).join('')\n  if (typeof node === 'object' && node) return getNodeText(node.props.children)\n  return ''\n}\n\nexport const removeSymbolsFromStart = (str = '', symbol = ''): string => {\n  if (str.startsWith(symbol)) {\n    return str.slice(symbol.length)\n  }\n  return str\n}\n\nexport const openNewWindowDatabase = (location: string) => {\n  if (isWebApp) {\n    window.open(window.location.origin + location)\n    return\n  }\n\n  window.app?.ipc?.invoke(IpcInvokeEvent.windowOpen, { location })\n}\n\nexport const handleCopy = (text = '') => {\n  navigator?.clipboard?.writeText(text)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/comparisons/bigKeys.ts",
    "content": "import { KeyTypes } from 'uiSrc/constants'\n\nenum HighlightType {\n  Length = 'length',\n  Memory = 'memory',\n}\n\ninterface DefaultConfig {\n  [key: string]: number\n}\n\nconst defaultMemoryConfig: { [key: string]: number } = {\n  memory: 5_000_000,\n}\n\nconst defaultConfig: { [key: string]: number } = {\n  length: 5_000,\n  memory: 5_000_000,\n}\n\nconst bigKeysConfig: { [key: string]: DefaultConfig } = {\n  [KeyTypes.List]: defaultConfig,\n  [KeyTypes.ZSet]: defaultConfig,\n  [KeyTypes.Set]: defaultConfig,\n  [KeyTypes.Hash]: defaultConfig,\n}\n\nconst isBigKey = (\n  keyType: string,\n  type: HighlightType,\n  count: number,\n): boolean => {\n  if (!count) return false\n  if (bigKeysConfig[keyType]?.[type])\n    return count >= bigKeysConfig[keyType][type]\n  if (defaultMemoryConfig[type]) return count >= defaultMemoryConfig[type]\n\n  return false\n}\n\nexport { HighlightType, isBigKey }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/comparisons/compareConsents.ts",
    "content": "import { has } from 'lodash'\nimport { isVersionHigher } from 'uiSrc/utils/comparisons/compareVersions'\n\n// returns true if has different consents\nexport const isDifferentConsentsExists = (specs: any, applied: any) =>\n  !!compareConsents(specs, applied).length\n\nexport const compareConsents = (\n  specs: any = {},\n  applied: any = {},\n  isReturnAllNonRequired: boolean = false,\n): any[] => {\n  if (!specs) {\n    return []\n  }\n  return Object.keys(specs)\n    .filter(\n      (consent) =>\n        (isReturnAllNonRequired && !specs[consent]?.required) ||\n        applied === null ||\n        !has(applied, consent) ||\n        isVersionHigher(specs[consent]?.since, applied.version),\n    )\n    .map((consent) => ({ ...specs[consent], agreementName: consent }))\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/comparisons/compareVersions.ts",
    "content": "import semver from 'semver'\n\nexport const isVersionHigherOrEquals = (\n  sourceVersion: string = '',\n  comparableVersion: string = '',\n) => {\n  const sourceVersionArray = sourceVersion.split('.')\n  const comparableVersionArray = comparableVersion.split('.')\n\n  for (\n    let i = 0;\n    i <=\n    Math.max(sourceVersionArray.length - 1, comparableVersionArray.length - 1);\n    i++\n  ) {\n    const n1 = parseInt(sourceVersionArray[i] || '0')\n    const n2 = parseInt(comparableVersionArray[i] || '0')\n\n    if (n1 > n2) return true\n    if (n2 > n1) return false\n  }\n\n  return true\n}\n\nexport const isVersionHigher = (\n  sourceVersion: string = '',\n  comparableVersion: string = '',\n) => {\n  const sourceVersionArray = sourceVersion.split('.')\n  const comparableVersionArray = comparableVersion.split('.')\n\n  for (\n    let i = 0;\n    i <=\n    Math.max(sourceVersionArray.length - 1, comparableVersionArray.length - 1);\n    i++\n  ) {\n    const n1 = parseInt(sourceVersionArray[i] || '0')\n    const n2 = parseInt(comparableVersionArray[i] || '0')\n\n    if (n1 > n2) return true\n    if (n2 > n1) return false\n  }\n\n  return false\n}\n\nexport const isRedisVersionSupported = (\n  raw: string,\n  minVersion: string,\n): boolean => {\n  // Try a loose/full parse of the whole string first.\n  // This returns a normalized version string like \"7.2.0\" or null if not recognizable.\n  const vLoose = semver.valid(raw, { loose: true })\n  if (vLoose) return semver.satisfies(vLoose, `>=${minVersion}`)\n\n  // Fallback: try to coerce a version from arbitrary text,\n  // e.g. \"Redis 7.2.1\" -> SemVer { version: '7.2.1' }\n  const coerced = semver.coerce(raw)\n  if (!coerced) {\n    return false\n  }\n\n  return semver.gte(coerced, minVersion)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/comparisons/diff.ts",
    "content": "import { isEqual, reduce, isNull } from 'lodash'\n\ntype UnknownObject = {\n  [key: string]: any\n}\n\nexport const isObject = (data: any) =>\n  typeof data === 'object' && data !== null && !Array.isArray(data)\n\nexport const getDiffKeysOfObjectValues = (\n  obj1: UnknownObject = {},\n  obj2: UnknownObject = {},\n): string[] =>\n  reduce(\n    obj1,\n    (result: string[], value, key) =>\n      isEqual(value, obj2[key]) ? result : result.concat(key),\n    [],\n  )\n\nexport const getFormUpdates = (\n  obj1: UnknownObject = {},\n  obj2: UnknownObject = {},\n): UnknownObject => {\n  if (isNull(obj2)) {\n    return obj1\n  }\n\n  return reduce(\n    obj1,\n    (result: UnknownObject, value, key) => {\n      if (isObject(value)) {\n        const diff = getFormUpdates(value, obj2[key] || {})\n\n        if (Object.keys(diff).length) {\n          result[key] = diff\n        }\n      } else if (value !== obj2[key] && !(!value && !obj2[key])) {\n        result[key] = value ?? null\n      }\n\n      return result\n    },\n    {},\n  )\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/comparisons/index.ts",
    "content": "export * from './diff'\nexport * from './compareVersions'\nexport * from './compareConsents'\nexport * from './bigKeys'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/content.ts",
    "content": "import { merge } from 'lodash'\nimport { ContentCreateRedis } from 'uiSrc/slices/interfaces/content'\n\nexport const getContentByFeature = (\n  content: ContentCreateRedis,\n  featureFlags: any,\n) => {\n  if (!content?.features) return content\n\n  let featureContent = content\n\n  Object.keys(content.features).forEach((featureName: string) => {\n    if (featureFlags?.[featureName]?.flag) {\n      // @ts-ignore\n      featureContent = merge({}, content, content.features[featureName])\n    }\n  })\n\n  return featureContent\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/decompressors/decompressors.ts",
    "content": "import { forIn } from 'lodash'\nimport { decompress as decompressFzstd } from 'fzstd'\n// @ts-ignore\nimport { decompress as decompressLz4 } from 'lz4js'\nimport { decompress as decompressSnappy } from '@stablelib/snappy'\nimport { inflate, ungzip } from 'pako'\nimport {\n  COMPRESSOR_MAGIC_SYMBOLS,\n  ICompressorMagicSymbols,\n  KeyValueCompressor,\n} from 'uiSrc/constants'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\nimport {\n  anyToBuffer,\n  bufferToUint8Array,\n  isEqualBuffers,\n  Nullable,\n} from 'uiSrc/utils'\n\n// workaround for brotli-wasm\n// https://github.com/httptoolkit/brotli-wasm/issues/8#issuecomment-1746768478\nimport init, * as brotli from '../../../../../node_modules/brotli-dec-wasm/pkg/brotli_dec_wasm'\nimport brotliWasmUrl from '../../../../../node_modules/brotli-dec-wasm/pkg/brotli_dec_wasm_bg.wasm?url'\n\ninit(brotliWasmUrl).then(() => brotli)\n\nconst decompressingBuffer = (\n  reply: RedisResponseBuffer,\n  compressorInit: Nullable<KeyValueCompressor> = null,\n): {\n  value: RedisString\n  compressor: Nullable<KeyValueCompressor>\n  isCompressed: boolean\n} => {\n  const compressorByValue: Nullable<KeyValueCompressor> = getCompressor(reply)\n  const compressor =\n    compressorInit === compressorByValue ||\n    (!compressorByValue && compressorInit)\n      ? compressorInit\n      : null\n\n  try {\n    switch (compressor) {\n      case KeyValueCompressor.GZIP: {\n        const value = ungzip(Buffer.from(reply))\n\n        return {\n          compressor,\n          isCompressed: compressor === compressorByValue,\n          value: anyToBuffer(value),\n        }\n      }\n      case KeyValueCompressor.ZSTD: {\n        const value = decompressFzstd(Buffer.from(reply))\n\n        return {\n          compressor,\n          isCompressed: compressor === compressorByValue,\n          value: anyToBuffer(value),\n        }\n      }\n      case KeyValueCompressor.LZ4: {\n        const value = decompressLz4(Buffer.from(reply))\n        return {\n          compressor,\n          isCompressed: compressor === compressorByValue,\n          value: anyToBuffer(value),\n        }\n      }\n      case KeyValueCompressor.SNAPPY: {\n        const value = anyToBuffer(decompressSnappy(Buffer.from(reply)))\n\n        return {\n          value,\n          compressor,\n          // SNAPPY compressor don't have \"magic numbers\"\n          // for detect is value was compressed we should compare reply and decompressed value\n          isCompressed: !isEqualBuffers(value, reply),\n        }\n      }\n      case KeyValueCompressor.Brotli: {\n        const value = anyToBuffer(brotli.decompress(bufferToUint8Array(reply)))\n        return {\n          value,\n          compressor,\n          isCompressed: !isEqualBuffers(value, reply),\n        }\n      }\n      case KeyValueCompressor.PHPGZCompress: {\n        const decompressedValue = inflate(bufferToUint8Array(reply))\n        if (!decompressedValue)\n          return { value: reply, compressor: null, isCompressed: false }\n\n        const value = anyToBuffer(decompressedValue)\n        return {\n          value,\n          compressor,\n          isCompressed: !isEqualBuffers(value, reply),\n        }\n      }\n      default: {\n        return { value: reply, compressor: null, isCompressed: false }\n      }\n    }\n  } catch (error) {\n    return { value: reply, compressor, isCompressed: false }\n  }\n}\n\nconst getCompressor = (\n  reply: RedisResponseBuffer,\n): Nullable<KeyValueCompressor> => {\n  const replyStart = reply?.data?.slice?.(0, 10)?.join?.(',') ?? ''\n  let compressor: Nullable<KeyValueCompressor> = null\n\n  forIn<ICompressorMagicSymbols>(\n    COMPRESSOR_MAGIC_SYMBOLS,\n    (magicSymbols: string, compressorName: string) => {\n      if (\n        magicSymbols &&\n        replyStart.startsWith(magicSymbols) &&\n        replyStart.length > magicSymbols.length\n      ) {\n        compressor = compressorName as KeyValueCompressor\n        return false // break loop\n      }\n\n      return true\n    },\n  )\n\n  return compressor\n}\n\nexport { getCompressor, decompressingBuffer }\n\nwindow.ri = {\n  ...window.ri,\n  getCompressor,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/decompressors/index.ts",
    "content": "export * from './decompressors'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/downloadFile.ts",
    "content": "import { saveAs } from 'file-saver'\nimport { AxiosResponseHeaders } from 'axios'\n\nexport const DEFAULT_FILE_NAME = 'Redis-Insight'\n\nexport const downloadFile = (\n  data: string = '',\n  headers: AxiosResponseHeaders,\n) => {\n  const contentDisposition = headers?.['content-disposition'] || ''\n  const file = new Blob([data], { type: 'text/plain;charset=utf-8' })\n  const fileName = contentDisposition.split('\"')?.[1] || DEFAULT_FILE_NAME\n\n  saveAs(file, fileName)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/handleBrowsers.ts",
    "content": "export const isSafari = /^((?!chrome|android).)*safari/i.test(\n  navigator.userAgent,\n)\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/handlePlatforms.ts",
    "content": "export const isMacOs = () => navigator.platform.toUpperCase().startsWith('MAC')\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/index.ts",
    "content": "import setTitle from './setPageTitle'\n\nexport * from './scrollIntoView'\nexport * from './handlePlatforms'\nexport * from './handleBrowsers'\nexport * from './triggerDownloadFromUrl'\n\nexport { removePagePlaceholder } from './pagePlaceholder'\n\nexport { setTitle }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/pagePlaceholder.ts",
    "content": "export const PAGE_PLACEHOLDER_ID = 'page-placeholder'\n\nexport const removePagePlaceholder = () => {\n  const placeholderEl = document.getElementById(PAGE_PLACEHOLDER_ID)\n  placeholderEl?.remove()\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/scrollIntoView.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\n\nconst isScrollBehaviorSupported = (): boolean =>\n  'scrollBehavior' in globalThis.document.documentElement.style\n\nexport const scrollIntoView = (\n  el: Nullable<HTMLDivElement>,\n  opts?: ScrollIntoViewOptions,\n) => {\n  if (el && isScrollBehaviorSupported()) {\n    el?.scrollIntoView(opts)\n  } else {\n    el?.scrollIntoView(true)\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/setPageTitle.ts",
    "content": "const setTitle = (title: string) => {\n  document.title = title\n}\n\nexport default setTitle\n"
  },
  {
    "path": "redisinsight/ui/src/utils/dom/triggerDownloadFromUrl.ts",
    "content": "import { CustomHeaders } from 'uiSrc/constants/api'\n\n/**\n * Triggers a file download from a URL using fetch with proper headers\n * This is necessary for Electron app where window ID authentication is required\n * @param url The full URL to download from\n */\nexport const triggerDownloadFromUrl = async (url: string): Promise<void> => {\n  const headers: Record<string, string> = {}\n\n  // Add window ID header for Electron app authentication\n  if (window.windowId) {\n    headers[CustomHeaders.WindowId] = window.windowId\n  }\n\n  const response = await fetch(url, { headers })\n\n  if (!response.ok) {\n    throw new Error(`Download failed: ${response.statusText}`)\n  }\n\n  // Extract filename from Content-Disposition header\n  const contentDisposition = response.headers.get('content-disposition') || ''\n  const filenameMatch = contentDisposition.match(/filename=\"?([^\";\\n]+)\"?/)\n  const filename = filenameMatch?.[1] || 'download'\n\n  // Convert response to blob and trigger download\n  const blob = await response.blob()\n  const blobUrl = URL.createObjectURL(blob)\n\n  const link = document.createElement('a')\n  link.href = blobUrl\n  link.download = filename\n  link.style.display = 'none'\n  document.body.appendChild(link)\n  link.click()\n  document.body.removeChild(link)\n\n  // Clean up the blob URL\n  URL.revokeObjectURL(blobUrl)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/errors.tsx",
    "content": "import { AxiosError } from 'axios'\nimport { capitalize, isEmpty, isString, isArray, set, isNumber } from 'lodash'\nimport React from 'react'\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils'\nimport { CustomError } from 'uiSrc/slices/interfaces'\nimport {\n  EXTERNAL_LINKS,\n  UTM_CAMPAINGS,\n  UTM_MEDIUMS,\n} from 'uiSrc/constants/links'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\nimport { getUtmExternalLink } from './links'\n\nexport const getRdiValidationMessage = (\n  message: string = '',\n  loc?: Array<string | number>,\n): string => {\n  // first item is always \"body\"\n  if (!loc || !isArray(loc) || loc.length < 2) {\n    return message\n  }\n\n  const [, ...rest] = loc\n  const formattedLoc = rest.reduce<string[]>((acc, curr) => {\n    if (isNumber(curr)) {\n      acc[acc.length - 1] += `[${curr}]`\n    } else {\n      acc.push(curr)\n    }\n    return acc\n  }, [])\n\n  const field = formattedLoc.pop() as string\n  const path = formattedLoc.join('/')\n  const words = message.split(' ')\n\n  words[0] = path ? `${field} in ${path}` : field\n\n  return capitalize(words.join(' '))\n}\n\nexport const parseCustomError = (\n  err: CustomError | string = DEFAULT_ERROR_MESSAGE,\n): AxiosError => {\n  const error = {\n    response: {\n      status: 500,\n      data: {},\n    },\n  }\n\n  if (isString(err)) {\n    return set(error, 'response.data.message', err) as AxiosError\n  }\n\n  let title: string = 'Error'\n  let message: React.ReactElement | string = ''\n  const additionalInfo: Record<string, any> = {}\n\n  switch (err?.errorCode) {\n    case CustomErrorCodes.CloudOauthGithubEmailPermission:\n      title = 'Github Email Permission'\n      message = (\n        <>\n          Unable to get an email from the GitHub account. Make sure that it is\n          available.\n          <br />\n        </>\n      )\n      break\n    case CustomErrorCodes.CloudOauthMisconfiguration:\n      title = 'Misconfiguration'\n      message = (\n        <>\n          Authorization server encountered a misconfiguration error and was\n          unable to complete your request.\n          <Spacer size=\"xs\" />\n          Try again later.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n    case CustomErrorCodes.CloudOauthUnknownAuthorizationRequest:\n      title = 'Error'\n      message = (\n        <>\n          Unknown authorization request.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n    case CustomErrorCodes.CloudOauthUnexpectedError:\n      title = 'Error'\n      message = (\n        <>\n          An unexpected error occurred.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n    case CustomErrorCodes.CloudOauthSsoUnsupportedEmail:\n      title = 'Invalid email'\n      message = <>Invalid email.</>\n      break\n    case CustomErrorCodes.CloudApiBadRequest:\n      title = 'Bad request'\n      message = (\n        <>\n          Your request resulted in an error.\n          <Spacer size=\"xs\" />\n          Try again later.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n\n    case CustomErrorCodes.CloudApiForbidden:\n      title = 'Access denied'\n      message = <>You do not have permission to access Redis Cloud.</>\n      break\n\n    case CustomErrorCodes.CloudApiInternalServerError:\n      title = 'Server error'\n      message = (\n        <>\n          Try restarting Redis Insight.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n\n    case CustomErrorCodes.CloudApiNotFound:\n      title = 'Resource was not found'\n      message = (\n        <>\n          Resource requested could not be found.\n          <Spacer size=\"xs\" />\n          Try again later.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n\n    case CustomErrorCodes.CloudCapiUnauthorized:\n    case CustomErrorCodes.CloudApiUnauthorized:\n    case CustomErrorCodes.QueryAiUnauthorized:\n      title = 'Session expired'\n      message = (\n        <>\n          Sign in again to continue working with Redis Cloud.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      )\n      break\n\n    case CustomErrorCodes.CloudCapiKeyUnauthorized:\n      title = 'Invalid API key'\n      message = (\n        <>\n          Your Redis Cloud authorization failed.\n          <Spacer size=\"xs\" />\n          Remove the invalid API key from Redis Insight and try again.\n          <Spacer size=\"s\" />\n          Open the Settings page to manage Redis Cloud API keys.\n        </>\n      )\n      additionalInfo.resourceId = err.resourceId\n      additionalInfo.errorCode = err.errorCode\n      break\n\n    case CustomErrorCodes.CloudDatabaseAlreadyExistsFree:\n      title = 'Database already exists'\n      message = (\n        <>\n          You already have a free Redis Cloud database running.\n          <Spacer size=\"s\" />\n          Check out your\n          <a\n            href={getUtmExternalLink(EXTERNAL_LINKS.cloudConsole, {\n              campaign: UTM_CAMPAINGS.Main,\n              medium: UTM_MEDIUMS.Main,\n            })}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            Cloud console\n          </a>\n          for connection details.\n        </>\n      )\n      break\n\n    case CustomErrorCodes.RdiDeployPipelineFailure:\n      title = 'Pipeline not deployed'\n      message =\n        err?.message ||\n        'Unfortunately we’ve found some errors in your pipeline.'\n      additionalInfo.errorCode = err.errorCode\n      break\n\n    case CustomErrorCodes.RdiValidationError:\n      title = 'Validation error'\n      if (isString(err?.details)) {\n        message = err.details\n      } else {\n        const details = err?.details?.[0] || {}\n        message = getRdiValidationMessage(details.msg, details.loc)\n      }\n      if (!message && err?.message) {\n        message = err.message\n      }\n      break\n\n    case CustomErrorCodes.AzureEntraIdTokenExpired:\n      title = 'Azure session expired'\n      message =\n        err?.message ||\n        'Azure Entra ID token expired. Sign in to Azure again to continue.'\n      additionalInfo.errorCode = err.errorCode\n      break\n\n    default:\n      title = 'Error'\n      message = err?.message || DEFAULT_ERROR_MESSAGE\n      break\n  }\n\n  const parsedError: any = { title, message }\n\n  if (!isEmpty(additionalInfo)) {\n    parsedError.additionalInfo = additionalInfo\n  }\n\n  return set(error, 'response.data', parsedError) as AxiosError\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/events/handleDownloadButton.ts",
    "content": "/**\n * Triggers a client-side download of a text file containing the provided data.\n *\n * This function creates a temporary Blob from the given string data, generates\n * a download link programmatically, and simulates a click event to prompt\n * the browser to download the file with the specified filename.\n *\n * It automatically cleans up the created object URL after use to free memory.\n * Optionally, a callback (`onSuccess`) can be executed once the download\n * process has been initiated successfully.\n *\n * @param data - The string content to be saved in the downloaded file.\n * @param filename - The desired name (with extension) of the downloaded file.\n * @param onSuccess - Optional callback executed after the download trigger completes.\n */\nexport const handleDownloadButton = (\n  data: string,\n  filename: string,\n  onSuccess?: () => void,\n) => {\n  try {\n    const blob = new Blob([data], { type: 'text/plain' })\n    const url = URL.createObjectURL(blob)\n\n    const a = document.createElement('a')\n    a.href = url\n    a.download = filename\n    a.click()\n\n    URL.revokeObjectURL(url)\n\n    onSuccess?.()\n  } catch (e) {\n    // ignore error\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/events/handlePasteHostName.ts",
    "content": "const handlePasteHostName = (\n  onHostNamePaste: (text: string) => boolean,\n  e: React.ClipboardEvent & {\n    originalEvent?: {\n      clipboardData: DataTransfer | null\n    }\n  },\n) => {\n  const clipboardData = e.clipboardData || e.originalEvent?.clipboardData\n  /*\n   * If the details were autofilled, stop the default behaviour\n   * which would trigger a redundant onChange event. Autofill happens\n   * only when the pasted string is a connection string. If the pasted\n   * string is not a connection string, we let the default behaviour\n   * happen which is inserting the pasted string to the `host` input.\n   */\n  if (onHostNamePaste(clipboardData.getData('text'))) {\n    e.preventDefault()\n  }\n}\n\nexport default handlePasteHostName\n"
  },
  {
    "path": "redisinsight/ui/src/utils/events/index.ts",
    "content": "import handlePasteHostName from './handlePasteHostName'\nimport selectOnFocus from './selectOnFocus'\nexport * from './handleDownloadButton'\n\nexport { handlePasteHostName, selectOnFocus }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/events/selectOnFocus.ts",
    "content": "const selectOnFocus = (\n  e: React.FocusEvent,\n  callback?: (e: React.FocusEvent) => void,\n) => {\n  ;(e.target as HTMLInputElement)?.select()\n  callback?.(e)\n}\n\nexport default selectOnFocus\n"
  },
  {
    "path": "redisinsight/ui/src/utils/features.ts",
    "content": "import { some } from 'lodash'\nimport { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting'\nimport { FeatureFlagComponent } from 'uiSrc/slices/interfaces'\nimport { Maybe } from 'uiSrc/utils/types'\n\nexport const getPagesForFeatures = (features: string[] = []) => {\n  const result: { [key: string]: string[] } = {}\n  features.forEach((f) => {\n    if (f in BUILD_FEATURES) {\n      const pageName = BUILD_FEATURES[f].page\n      if (!pageName) return\n\n      if (result[pageName]) {\n        result[pageName] = [...result[pageName], f]\n      } else {\n        result[pageName] = [f]\n      }\n    }\n  })\n\n  return result\n}\n\nexport const getHighlightingFeatures = (\n  features: string[],\n): { [key: string]: boolean } =>\n  features.reduce((prev, next) => ({ ...prev, [next]: true }), {})\n\nexport const isAnyFeatureEnabled = (features: Maybe<FeatureFlagComponent>[]) =>\n  some(features, (feature) => feature?.flag)\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/bufferFormatters.ts",
    "content": "import { isString } from 'lodash'\nimport { ObjectInputStream } from 'java-object-serialization'\nimport { TextDecoder, TextEncoder } from 'text-encoding'\nimport { Buffer } from 'buffer'\nimport { KeyValueFormat } from 'uiSrc/constants'\nimport JavaDate from './java-date'\n// eslint-disable-next-line import/order\nimport {\n  RedisResponseBuffer,\n  RedisResponseBufferType,\n  RedisString,\n  UintArray,\n} from 'uiSrc/slices/interfaces'\nimport { Nullable } from '../types'\n\nObjectInputStream.RegisterObjectClass(\n  JavaDate,\n  JavaDate.ClassName,\n  JavaDate.SerialVersionUID,\n)\n\nconst decoder = new TextDecoder('utf-8')\nconst encoder = new TextEncoder()\n\nconst isEqualBuffers = (\n  a?: Nullable<RedisResponseBuffer>,\n  b?: Nullable<RedisResponseBuffer>,\n) => {\n  if (a?.data?.length !== b?.data?.length) return false\n  return a?.data?.join(',') === b?.data?.join(',')\n}\n\n// eslint-disable-next-line no-control-regex\nconst IS_NON_PRINTABLE_ASCII_CHARACTER = /[^ -~\\u0007\\b\\t\\n\\r]/\n\nconst decimalToHexString = (d: number, padding = 2) => {\n  const hex = Number(d).toString(16)\n  return '0'.repeat(padding).substring(0, padding - hex.length) + hex\n}\n\nconst bufferToHex = (reply: RedisResponseBuffer): string => {\n  let result = ''\n\n  reply.data.forEach((byte: number) => {\n    // eslint-disable-next-line\n    result += ('0' + (byte & 0xff).toString(16)).slice(-2)\n  })\n\n  return result\n}\n\nconst bufferToBinary = (reply: RedisResponseBuffer): string =>\n  Array.from(reply.data).reduce(\n    (str, byte) => str + byte.toString(2).padStart(8, '0'),\n    '',\n  )\n\nconst binaryToBuffer = (reply: string) => {\n  const data: number[] =\n    reply.match(/.{1,8}/g)?.map((v) => parseInt(v, 2)) || []\n  return anyToBuffer(data)\n}\n\nconst bufferToASCII = (reply: RedisResponseBuffer): string => {\n  let result = ''\n  reply.data.forEach((byte: number) => {\n    const char = decoder.decode(new Uint8Array([byte]))\n    if (IS_NON_PRINTABLE_ASCII_CHARACTER.test(char)) {\n      result += `\\\\x${decimalToHexString(byte)}`\n    } else {\n      switch (char) {\n        case '\\u0007': // Bell character\n          result += '\\\\a'\n          break\n        case '\\\\':\n          result += '\\\\\\\\'\n          break\n        case '\"':\n          result += '\\\\\"'\n          break\n        case '\\b':\n          result += '\\\\b'\n          break\n        case '\\t':\n          result += '\\\\t'\n          break\n        case '\\n':\n          result += '\\\\n'\n          break\n        case '\\r':\n          result += '\\\\r'\n          break\n        default:\n          result += char\n      }\n    }\n  })\n  return result\n}\n\nconst anyToBuffer = (reply: UintArray | ArrayBuffer): RedisResponseBuffer =>\n  ({ data: reply, type: RedisResponseBufferType.Buffer }) as RedisResponseBuffer\n\nconst ASCIIToBuffer = (strInit: string) => {\n  let result = ''\n  const str = strInit\n    .replace(/\\\\\"/g, '\"')\n    .replace(/\\\\\\\\/g, '\\\\')\n    .replace(/\\\\b/g, '\\b')\n    .replace(/\\\\t/g, '\\t')\n    .replace(/\\\\n/g, '\\n')\n    .replace(/\\\\r/g, '\\r')\n\n  for (let i = 0; i < str.length; ) {\n    if (str.substring(i, i + 2) === '\\\\x') {\n      result += str.substring(i + 2, i + 4)\n      i += 4\n    } else {\n      result += Buffer.from(str[i++]).toString('hex')\n    }\n  }\n\n  return anyToBuffer(Array.from(Buffer.from(result, 'hex')))\n}\n\nconst MIN_VECTOR_BYTES = 8 // at least 2 float32 values\n\n/**\n * Returns true when the buffer holds non-text (binary) data.\n * Uses a UTF-8 round-trip: encode → decode; if the bytes differ the\n * original payload is not valid UTF-8 text.\n */\nconst isBinaryData = (buf: RedisResponseBuffer): boolean => {\n  const utf8 = decoder.decode(new Uint8Array(buf.data))\n  const roundTrip = encoder.encode(utf8)\n  if (roundTrip.length !== buf.data.length) return true\n  for (let i = 0; i < roundTrip.length; i++) {\n    if (roundTrip[i] !== buf.data[i]) return true\n  }\n  return false\n}\n\n/**\n * Heuristic: returns true when the buffer is likely a binary-encoded\n * float32 vector (e.g. embeddings stored via Redis HSET as raw bytes).\n */\nconst isBinaryVector = (buf: RedisResponseBuffer): boolean => {\n  const len = buf.data.length\n  if (len < MIN_VECTOR_BYTES || len % 4 !== 0) return false\n  if (!isBinaryData(buf)) return false\n\n  const bytes = new Uint8Array(buf.data)\n  const view = new DataView(bytes.buffer)\n  for (let i = 0; i < view.byteLength; i += 4) {\n    const f = view.getFloat32(i, true)\n    if (!Number.isFinite(f)) return false\n  }\n  return true\n}\n\nconst bufferToFloat32Array = (data: Uint8Array) => {\n  const { buffer } = new Uint8Array(data)\n  const dataView = new DataView(buffer)\n  const vector = []\n\n  for (let i = 0; i < dataView.byteLength; i += 4) {\n    vector.push(dataView.getFloat32(i, true))\n  }\n  return new Float32Array(vector)\n}\n\nconst bufferToFloat64Array = (data: Uint8Array) => {\n  const { buffer } = new Uint8Array(data)\n  const dataView = new DataView(buffer)\n  const vector = []\n\n  for (let i = 0; i < dataView.byteLength; i += 8) {\n    vector.push(dataView.getFloat64(i, true))\n  }\n  return new Float64Array(vector)\n}\n\nconst bufferToUint8Array = (reply: RedisResponseBuffer): Uint8Array =>\n  new Uint8Array(reply.data)\nconst bufferToUTF8 = (reply: RedisResponseBuffer): string =>\n  decoder.decode(bufferToUint8Array(reply))\n\nconst UintArrayToString = (reply: UintArray): string =>\n  decoder.decode(new Uint8Array(reply))\n\nconst UTF8ToBuffer = (reply: string): RedisResponseBuffer =>\n  anyToBuffer(encoder.encode(reply))\n\n// common formatters\nconst stringToBuffer = (\n  data: string,\n  formatResult: KeyValueFormat = KeyValueFormat.Unicode,\n): RedisResponseBuffer => {\n  switch (formatResult) {\n    case KeyValueFormat.Unicode: {\n      return UTF8ToBuffer(data)\n    }\n    case KeyValueFormat.ASCII: {\n      return ASCIIToBuffer(data)\n    }\n    default: {\n      return UTF8ToBuffer(data)\n    }\n  }\n}\n\nconst hexToBuffer = (data: string): RedisResponseBuffer => {\n  let string = data\n  const result = []\n  while (string.length >= 2) {\n    result.push(parseInt(string.substring(0, 2), 16))\n    string = string.substring(2, string.length)\n  }\n  return {\n    type: RedisResponseBufferType.Buffer,\n    data: result,\n  } as RedisResponseBuffer\n}\n\nconst bufferToJava = (reply: RedisResponseBuffer) => {\n  const stream = new ObjectInputStream(bufferToUint8Array(reply))\n  const decoded = stream.readObject()\n\n  if (typeof decoded !== 'object') {\n    return decoded\n  }\n\n  if (decoded instanceof Date) {\n    return decoded\n  }\n\n  const { fields } = decoded\n  const fieldsArray = Array.from(fields, ([key, value]) => ({ [key]: value }))\n  return { ...decoded, fields: fieldsArray }\n}\n\nconst bufferToString = (\n  data: RedisString = '',\n  formatResult: KeyValueFormat = KeyValueFormat.Unicode,\n): string => {\n  if (!isString(data) && data?.type === RedisResponseBufferType.Buffer) {\n    switch (formatResult) {\n      case KeyValueFormat.Unicode: {\n        return bufferToUTF8(data)\n      }\n      case KeyValueFormat.ASCII: {\n        return bufferToASCII(data)\n      }\n\n      default: {\n        return bufferToUTF8(data)\n      }\n    }\n  }\n  return data?.toString()\n}\n\nexport {\n  bufferToUTF8,\n  bufferToASCII,\n  bufferToHex,\n  UTF8ToBuffer,\n  decimalToHexString,\n  ASCIIToBuffer,\n  isEqualBuffers,\n  stringToBuffer,\n  bufferToUint8Array,\n  bufferToString,\n  UintArrayToString,\n  hexToBuffer,\n  anyToBuffer,\n  bufferToBinary,\n  binaryToBuffer,\n  bufferToJava,\n  bufferToFloat32Array,\n  bufferToFloat64Array,\n  isBinaryData,\n  isBinaryVector,\n}\n\nwindow.ri = {\n  ...window.ri,\n  anyToBuffer,\n  bufferToUTF8,\n  bufferToASCII,\n  UTF8ToBuffer,\n  ASCIIToBuffer,\n  UintArrayToString,\n  stringToBuffer,\n  bufferToString,\n  bufferToHex,\n  hexToBuffer,\n  bufferToBinary,\n  binaryToBuffer,\n}\n\n// for BE libraries which work with Buffer\nwindow.Buffer = Buffer\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/index.ts",
    "content": "export * from './utils'\nexport * from './bufferFormatters'\nexport * from './valueFormatters'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/java-date.ts",
    "content": "import { ObjectInputStream, JavaSerializable } from 'java-object-serialization'\n\nexport default class JavaDate implements JavaSerializable {\n  // The class name in the serialized data\n  static readonly ClassName = 'java.util.Date'\n\n  // The serial version UID followed for 'java.util.Date'\n  static readonly SerialVersionUID = '7523967970034938905'\n\n  // The maximum value for a Java long\n  readonly JAVA_MAX_LONG = 9223372036854775807n // 2^63 - 1\n\n  // The maximum value for a two's complement long\n  readonly TWO_COMPLEMENT_MAX_LONG = 18446744073709551616n // 2^64\n\n  time: bigint = 0n\n\n  readObject(stream: ObjectInputStream): void {\n    this.time = stream.readLong()\n  }\n\n  readResolve() {\n    let timeValue: number\n\n    // Handle two's complement conversion for negative numbers\n    if (this.time > this.JAVA_MAX_LONG) {\n      // If the number is larger than MAX_LONG, it's a negative number in two's complement\n      timeValue = Number(this.time - this.TWO_COMPLEMENT_MAX_LONG)\n    } else {\n      timeValue = Number(this.time)\n    }\n\n    const date = new Date(timeValue)\n\n    // Validate the date\n    if (Number.isNaN(date.getTime())) {\n      throw new Error(\n        `Invalid date value: ${timeValue} (original: ${this.time})`,\n      )\n    }\n\n    return date\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/json.ts",
    "content": "import JSONBigInt from 'json-bigint'\n\nconst JSONParser = JSONBigInt({\n  useNativeBigInt: true,\n  protoAction: 'preserve',\n  constructorAction: 'preserve',\n})\n\nexport const reSerializeJSON = (val: string, space?: number) => {\n  try {\n    const json = JSONParser.parse(val)\n    return JSONParser.stringify(json, null, space)\n  } catch (e) {\n    return val\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/index.ts",
    "content": "import { rehypeLinks } from './rehypeLinks'\nimport { remarkImage } from './remarkImage'\nimport { remarkLink } from './remarkLink'\nimport { remarkCode } from './remarkCode'\nimport { remarkSanitize } from './remarkSanitize'\nimport { remarkRedisUpload } from './remarkRedisUpload'\n\nexport {\n  rehypeLinks,\n  remarkImage,\n  remarkLink,\n  remarkCode,\n  remarkRedisUpload,\n  remarkSanitize,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/rehypeLinks.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { createLocation, History } from 'history'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\n\ninterface IConfig {\n  history: History\n}\n\nexport const rehypeLinks =\n  (config?: IConfig): ((tree: Node) => void) =>\n  (tree: any) => {\n    visit(tree, 'link', (node) => {\n      if (\n        node.tagName === 'a' &&\n        node.properties &&\n        typeof node.properties.href === 'string'\n      ) {\n        const url: string = node.properties.href\n        if (IS_ABSOLUTE_PATH.test(url)) {\n          delete node.properties.title\n        }\n        if (url.startsWith('#') && config?.history) {\n          const { location: currentLocation } = config.history\n          const newLocation = createLocation(url, null, '', currentLocation)\n          newLocation.search = currentLocation.search\n          node.properties.href = config.history.createHref(newLocation)\n        }\n      }\n    })\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/remarkCode.ts",
    "content": "import { visit } from 'unist-util-visit'\n\nexport enum ButtonLang {\n  Redis = 'redis',\n}\n\nconst PARAMS_SEPARATOR = ':'\n\nexport const remarkCode =\n  (codeOptions?: Record<string, any>): ((tree: Node) => void) =>\n  (tree: any) => {\n    // Find code node in syntax tree\n    visit(tree, 'code', (codeNode) => {\n      const { value, meta, lang } = codeNode\n\n      if (!lang && !codeOptions?.allLangs) return\n\n      if (codeOptions?.allLangs) {\n        codeNode.type = 'html'\n        codeNode.value = `<Code label=\"${meta || ''}\" lang=\"${lang}\">{${JSON.stringify(value)}}</Code>`\n      }\n\n      const isRedisLang = lang?.startsWith(ButtonLang.Redis)\n      if (isRedisLang) {\n        const [, params] = lang?.split(PARAMS_SEPARATOR) || []\n\n        codeNode.type = 'html'\n        // Replace it with our custom component\n        // path - binding for JsxParser, it will be replaces as prop value\n        codeNode.value = `<Code label=\"${meta || ''}\" params=\"${params}\" path={path} lang=\"redis\">{${JSON.stringify(value)}}</Code>`\n      }\n    })\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/remarkImage.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { getFileUrlFromMd } from 'uiSrc/utils/pathUtil'\n\nexport const remarkImage =\n  (path: string): ((tree: Node) => void) =>\n  (tree: any) => {\n    // Find img node in syntax tree\n    visit(tree, 'image', (node) => {\n      node.url = getFileUrlFromMd(node.url, path)\n    })\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/remarkLink.ts",
    "content": "/* eslint-disable max-len */\nimport { visit } from 'unist-util-visit'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\n\nexport const remarkLink = (): ((tree: Node) => void) => (tree: any) => {\n  // Find link node in syntax tree\n  visit(tree, 'link', (node) => {\n    if (IS_ABSOLUTE_PATH.test(node.url)) {\n      // External link\n      const [text] = node.children || []\n      node.type = 'html'\n      node.value = `<Link external target=\"_blank\" href=\"${node.url}\" rel=\"nofollow noopener noreferrer\" variant=\"inline\" size=\"S\">${text?.value || EXTERNAL_LINKS.redisIo}</Link>`\n    }\n\n    if (node.title === 'Redis Cloud') {\n      const [text] = node.children || []\n      node.type = 'html'\n      node.value = `<CloudLink url=\"${node.url}\" text=\"${text?.value || 'Redis Cloud'}\" />`\n    }\n\n    if (node.url?.toLowerCase()?.startsWith('redisinsight')) {\n      const [text] = node.children || []\n      const url = node.url.replace('redisinsight:', '')\n      node.type = 'html'\n      node.value = `<RedisInsightLink url=\"${url}\" text=\"${text?.value || 'Redis Cloud'}\" size=\"S\" />`\n    }\n  })\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/remarkRedisUpload.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { getFileUrlFromMd } from 'uiSrc/utils/pathUtil'\n\nexport const remarkRedisUpload =\n  (path: string): ((tree: Node) => void) =>\n  (tree: any) => {\n    // Find code node in syntax tree\n    visit(tree, 'code', (node) => {\n      try {\n        const { lang, meta } = node\n\n        const value: string = `${lang} ${meta}`\n        const [, filePath, label] =\n          value.match(/^redis-upload:\\[(.*)] (.*)/i) || []\n\n        const { pathname } = new URL(getFileUrlFromMd(filePath, path))\n        const decodedPath = decodeURI(pathname)\n\n        if (path && label) {\n          node.type = 'html'\n          // Replace it with our custom component\n          node.value = `<RedisUploadButton label=\"${label}\" path=\"${decodedPath}\" />`\n        }\n      } catch (e) {\n        // ignore errors\n      }\n    })\n  }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/markdown/remarkSanitize.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport DOMPurify from 'dompurify'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\n\nconst isOpeningTag = (value: string) =>\n  value.startsWith('<') && !value.startsWith('</') && value.endsWith('>')\nconst removeClosingTag = (value: string) =>\n  value.replace(/<\\/[a-zA-Z][^>]*>/g, '')\nconst isContainsClosingTag = (value: string) => value.indexOf('</') > -1\n\nDOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {\n  if (node.tagName === 'A' && node.hasAttribute('href')) {\n    if (!IS_ABSOLUTE_PATH.test(node.getAttribute('href') || '')) {\n      node.removeAttribute('href')\n      return\n    }\n\n    node.setAttribute('target', '_blank')\n  }\n})\n\nexport const remarkSanitize = (): ((tree: Node) => void) => (tree: any) => {\n  visit(tree, 'html', (node) => {\n    const inputTag = node.value.toLowerCase()\n\n    // JUST BANNED\n    if (inputTag.indexOf('dangerouslysetinnerhtml') > -1) {\n      node.value = ''\n    }\n\n    if (isOpeningTag(inputTag)) {\n      const isTagContainsClosing = isContainsClosingTag(inputTag)\n      const sanitized = DOMPurify.sanitize(node.value)\n      node.value = isTagContainsClosing\n        ? sanitized\n        : removeClosingTag(sanitized)\n    }\n  })\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/msgpack/decoder.spec.ts",
    "content": "import { decodeMsgpackWithLz4 } from './decoder'\n\ndescribe('decoder', () => {\n  describe('decodeMsgpackWithLz4', () => {\n    describe('standard msgpack (no LZ4)', () => {\n      it('should decode simple object', () => {\n        // MessagePack for { \"name\": \"test\", \"value\": 123 }\n        const data = new Uint8Array([\n          0x82, // fixmap with 2 elements\n          0xa4,\n          0x6e,\n          0x61,\n          0x6d,\n          0x65, // \"name\" (fixstr)\n          0xa4,\n          0x74,\n          0x65,\n          0x73,\n          0x74, // \"test\" (fixstr)\n          0xa5,\n          0x76,\n          0x61,\n          0x6c,\n          0x75,\n          0x65, // \"value\" (fixstr)\n          0x7b, // 123 (positive fixint)\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual({ name: 'test', value: 123 })\n      })\n\n      it('should decode array', () => {\n        // MessagePack for [1, 2, 3]\n        const data = new Uint8Array([0x93, 0x01, 0x02, 0x03])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual([1, 2, 3])\n      })\n\n      it('should decode nested object', () => {\n        // MessagePack for { \"outer\": { \"inner\": 42 } }\n        const data = new Uint8Array([\n          0x81, // fixmap with 1 element\n          0xa5,\n          0x6f,\n          0x75,\n          0x74,\n          0x65,\n          0x72, // \"outer\"\n          0x81, // fixmap with 1 element\n          0xa5,\n          0x69,\n          0x6e,\n          0x6e,\n          0x65,\n          0x72, // \"inner\"\n          0x2a, // 42\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual({ outer: { inner: 42 } })\n      })\n\n      it('should decode deeply nested arrays', () => {\n        // MessagePack for [[1, 2], [3, 4]]\n        const data = new Uint8Array([\n          0x92, // fixarray with 2 elements\n          0x92,\n          0x01,\n          0x02, // [1, 2]\n          0x92,\n          0x03,\n          0x04, // [3, 4]\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual([\n          [1, 2],\n          [3, 4],\n        ])\n      })\n\n      it('should decode mixed types', () => {\n        // MessagePack for { \"arr\": [1, \"two\"], \"num\": 3 }\n        const data = new Uint8Array([\n          0x82, // fixmap with 2 elements\n          0xa3,\n          0x61,\n          0x72,\n          0x72, // \"arr\"\n          0x92,\n          0x01,\n          0xa3,\n          0x74,\n          0x77,\n          0x6f, // [1, \"two\"]\n          0xa3,\n          0x6e,\n          0x75,\n          0x6d, // \"num\"\n          0x03, // 3\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual({ arr: [1, 'two'], num: 3 })\n      })\n    })\n\n    describe('edge cases', () => {\n      it('should decode empty map', () => {\n        const data = new Uint8Array([0x80]) // fixmap with 0 elements\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual({})\n      })\n\n      it('should decode empty array', () => {\n        const data = new Uint8Array([0x90]) // fixarray with 0 elements\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual([])\n      })\n\n      it('should decode null', () => {\n        const data = new Uint8Array([0xc0]) // nil\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBeNull()\n      })\n\n      it('should decode boolean true', () => {\n        const data = new Uint8Array([0xc3]) // true\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe(true)\n      })\n\n      it('should decode boolean false', () => {\n        const data = new Uint8Array([0xc2]) // false\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe(false)\n      })\n\n      it('should decode string', () => {\n        // \"hello\"\n        const data = new Uint8Array([0xa5, 0x68, 0x65, 0x6c, 0x6c, 0x6f])\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe('hello')\n      })\n\n      it('should decode negative integer', () => {\n        // -1 (negative fixint)\n        const data = new Uint8Array([0xff])\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe(-1)\n      })\n\n      it('should decode float', () => {\n        // MessagePack float32 for 3.14 (approximately)\n        const data = new Uint8Array([0xca, 0x40, 0x48, 0xf5, 0xc3])\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBeCloseTo(3.14, 2)\n      })\n\n      it('should decode binary data', () => {\n        // MessagePack bin8 with 3 bytes\n        const data = new Uint8Array([0xc4, 0x03, 0x01, 0x02, 0x03])\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual(new Uint8Array([0x01, 0x02, 0x03]))\n      })\n    })\n\n    describe('LZ4 compressed data', () => {\n      it('should decode inline LZ4Block format (type 99)', () => {\n        // This is the inline format where compressed data is in the ext payload\n        // ext8 with type 99, 4-byte size + LZ4 compressed data\n        // Compressed content: msgpack for 42 (0x2a)\n        const data = new Uint8Array([\n          0xc7, // ext8\n          0x06, // payload length: 6 bytes (4 size + 2 compressed)\n          0x63, // type 99 (0x63)\n          0x00,\n          0x00,\n          0x00,\n          0x01, // uncompressed size: 1\n          0x10,\n          0x2a, // LZ4 compressed: literal 1 byte, value 0x2a (42)\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe(42)\n      })\n\n      it('should decode inline LZ4BlockArray format (type 98)', () => {\n        // ext8 with type 98, 4-byte size + LZ4 compressed data\n        // Compressed content: msgpack for \"hi\" (0xa2 0x68 0x69)\n        const data = new Uint8Array([\n          0xc7, // ext8\n          0x08, // payload length: 8 bytes (4 size + 4 compressed)\n          0x62, // type 98 (0x62)\n          0x00,\n          0x00,\n          0x00,\n          0x03, // uncompressed size: 3\n          0x30,\n          0xa2,\n          0x68,\n          0x69, // LZ4: literal 3 bytes, \"hi\" msgpack\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe('hi')\n      })\n\n      it('should decode array format LZ4BlockArray [ext(98, size), bin]', () => {\n        // Array format: [Ext(98, msgpack_size), binary_compressed_data]\n        // This is the format used by MessagePack-CSharp\n        // Compressed content: msgpack for 123 (0x7b)\n        const data = new Uint8Array([\n          0x92, // fixarray with 2 elements\n          0xd4, // fixext1 (1-byte payload)\n          0x62, // type 98\n          0x01, // size: 1 (msgpack positive fixint)\n          0xc4, // bin8\n          0x02, // binary length: 2\n          0x10,\n          0x7b, // LZ4 compressed: literal 1 byte, value 0x7b (123)\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe(123)\n      })\n\n      it('should decode array format with larger size (uint8)', () => {\n        // Size encoded as msgpack uint8 (0xcc)\n        // Compressed content: msgpack for \"test\" (0xa4 + \"test\")\n        const data = new Uint8Array([\n          0x92, // fixarray with 2 elements\n          0xd5, // fixext2 (2-byte payload)\n          0x62, // type 98\n          0xcc,\n          0x05, // size: 5 (msgpack uint8)\n          0xc4, // bin8\n          0x06, // binary length: 6\n          0x50,\n          0xa4,\n          0x74,\n          0x65,\n          0x73,\n          0x74, // LZ4: literal 5 bytes\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBe('test')\n      })\n\n      it('should decode nested object from LZ4', () => {\n        // Compressed content: msgpack for { \"a\": 1 }\n        // 81 a1 61 01 = fixmap(1), fixstr(1) \"a\", fixint 1\n        const data = new Uint8Array([\n          0x92, // fixarray with 2 elements\n          0xd4, // fixext1\n          0x62, // type 98\n          0x04, // size: 4\n          0xc4, // bin8\n          0x05, // binary length: 5\n          0x40,\n          0x81,\n          0xa1,\n          0x61,\n          0x01, // LZ4: literal 4 bytes\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toEqual({ a: 1 })\n      })\n    })\n\n    describe('timestamp extension', () => {\n      it('should decode standalone fixext4 timestamp as Date', () => {\n        // fixext4 with type -1 (timestamp), seconds = 1740397416\n        const data = new Uint8Array([\n          0xd6, // fixext4\n          0xff, // type -1 (timestamp)\n          0x67,\n          0xbc,\n          0x5b,\n          0x68, // seconds: 1740397416\n        ])\n\n        const result = decodeMsgpackWithLz4(data)\n        expect(result).toBeInstanceOf(Date)\n        expect((result as Date).toISOString()).toBe('2025-02-24T11:43:36.000Z')\n      })\n\n      it('should preserve timestamp inside an object', () => {\n        // { \"created\": <fixext4 timestamp> }\n        const data = new Uint8Array([\n          0x81, // fixmap with 1 element\n          0xa7,\n          0x63,\n          0x72,\n          0x65,\n          0x61,\n          0x74,\n          0x65,\n          0x64, // \"created\"\n          0xd6, // fixext4\n          0xff, // type -1 (timestamp)\n          0x67,\n          0xbc,\n          0x5b,\n          0x68, // seconds: 1740397416\n        ])\n\n        const result = decodeMsgpackWithLz4(data) as Record<string, unknown>\n        expect(result.created).toBeInstanceOf(Date)\n        expect((result.created as Date).toISOString()).toBe(\n          '2025-02-24T11:43:36.000Z',\n        )\n      })\n\n      it('should decode .NET DateTimeOffset pattern correctly', () => {\n        // Simulates MessagePack-CSharp Class1: [[timestamp, offset], 534, false]\n        // DateTimeOffset is serialized as [timestamp_ext(-1), offset_minutes]\n        const data = new Uint8Array([\n          0x93, // fixarray with 3 elements\n          0x92, // fixarray with 2 elements (DateTimeOffset)\n          0xd6, // fixext4\n          0xff, // type -1 (timestamp)\n          0x67,\n          0xbc,\n          0x5b,\n          0x68, // seconds: 1740397416\n          0x00, // offset: 0 (UTC)\n          0xcd,\n          0x02,\n          0x16, // uint16: 534\n          0xc2, // false\n        ])\n\n        const result = decodeMsgpackWithLz4(data) as unknown[]\n        const dateTimeOffset = result[0] as unknown[]\n\n        expect(dateTimeOffset[0]).toBeInstanceOf(Date)\n        expect((dateTimeOffset[0] as Date).toISOString()).toBe(\n          '2025-02-24T11:43:36.000Z',\n        )\n        expect(dateTimeOffset[1]).toBe(0)\n        expect(result[1]).toBe(534)\n        expect(result[2]).toBe(false)\n\n        // Verify JSON stringification produces ISO string, not {}\n        const json = JSON.stringify(result)\n        expect(json).toContain('2025-02-24T11:43:36.000Z')\n        expect(json).not.toContain('{}')\n      })\n    })\n\n    describe('error handling', () => {\n      it('should throw on invalid msgpack data', () => {\n        const invalidData = new Uint8Array([0xff, 0xff, 0xff])\n        expect(() => decodeMsgpackWithLz4(invalidData)).toThrow()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/msgpack/decoder.ts",
    "content": "/**\n * MessagePack decoder with LZ4 decompression support\n */\n\nimport { Unpackr } from 'msgpackr'\n\nimport { isLz4SizeMarker, decompressLz4 } from './lz4'\n// Import extensions module to ensure handlers are registered\nimport './extensions'\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    !(value instanceof Uint8Array) &&\n    !(value instanceof Date)\n  )\n}\n\n/**\n * Post-processes decoded data to find and decompress LZ4 patterns\n *\n * Looks for:\n * 1. [Lz4SizeMarker, Uint8Array] arrays (array format)\n * 2. Lz4SizeMarker with compressedData (inline format)\n */\nfunction postProcessLz4(value: unknown, decoder: Unpackr): unknown {\n  // Handle inline format markers with embedded compressed data\n  if (isLz4SizeMarker(value) && value.compressedData) {\n    const decompressed = decompressLz4(value.compressedData, value.size)\n    const decoded = decoder.unpack(decompressed)\n    return postProcessLz4(decoded, decoder)\n  }\n\n  // Handle array format: [Lz4SizeMarker, Uint8Array]\n  if (Array.isArray(value) && value.length === 2) {\n    const [first, second] = value\n    if (isLz4SizeMarker(first) && second instanceof Uint8Array) {\n      const decompressed = decompressLz4(second, first.size)\n      const decoded = decoder.unpack(decompressed)\n      return postProcessLz4(decoded, decoder)\n    }\n  }\n\n  // Recursively process arrays\n  if (Array.isArray(value)) {\n    return value.map((item) => postProcessLz4(item, decoder))\n  }\n\n  // Recursively process plain objects\n  if (isPlainObject(value)) {\n    const result: Record<string, unknown> = {}\n    for (const [key, val] of Object.entries(value)) {\n      result[key] = postProcessLz4(val, decoder)\n    }\n    return result\n  }\n\n  return value\n}\n\n// Singleton decoder instance\nconst decoder = new Unpackr({\n  useRecords: false,\n  mapsAsObjects: true,\n})\n\n/**\n * Decodes msgpack data with LZ4 decompression support\n *\n * This function must be used instead of directly using msgpackr's Unpackr\n * because the extension handlers only return markers that need to be\n * post-processed to actually decompress the LZ4 data.\n */\nexport function decodeMsgpackWithLz4(buffer: Uint8Array): unknown {\n  const decoded = decoder.unpack(buffer)\n  return postProcessLz4(decoded, decoder)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/msgpack/extensions.ts",
    "content": "/**\n * MessagePack extension handlers for .NET MessagePack-CSharp LZ4 compression\n *\n * The .NET MessagePack library uses extension types 98 and 99 for LZ4 compression:\n * - Type 98 (Lz4BlockArray): Array of LZ4 compressed blocks\n * - Type 99 (Lz4Block): Single LZ4 compressed block\n *\n * Supported LZ4BlockArray formats (type 98):\n *\n * Format 1 - Array format (most common):\n *   [Ext(98, msgpack_int(uncompressed_size)), bin(compressed_data)]\n *   - 2-element msgpack array\n *   - First element: ext type 98 with uncompressed size as msgpack integer\n *   - Second element: binary with LZ4 compressed data\n *\n * Format 2 - Inline format:\n *   Ext(98, [4-byte-big-endian-size, compressed_data...])\n *   - Single ext with size and data packed together\n *\n * @see https://github.com/MessagePack-CSharp/MessagePack-CSharp#lz4-compression\n */\n\nimport { addExtension } from 'msgpackr'\n\nimport { Lz4SizeMarker, LZ4_SIZE_MARKER, readMsgpackInt } from './lz4'\n\n// Extension type codes used by .NET MessagePack-CSharp\nconst LZ4_BLOCK_ARRAY_TYPE = 98\nconst LZ4_BLOCK_TYPE = 99\n\n// Maximum allowed uncompressed size (100MB) to prevent memory exhaustion\nconst MAX_UNCOMPRESSED_SIZE = 100_000_000\n\nconst isValidSize = (size: number): boolean =>\n  size > 0 && size < MAX_UNCOMPRESSED_SIZE\n\n/**\n * Extension handler for LZ4Block (type 99)\n * Format: 4-byte big-endian size + compressed data\n */\nfunction handleLz4BlockExt(data: Uint8Array): Lz4SizeMarker {\n  if (data.length >= 5) {\n    const view = new DataView(data.buffer, data.byteOffset, data.byteLength)\n    const size = view.getInt32(0, false)\n    if (isValidSize(size)) {\n      const compressedData = data.slice(4)\n      return { [LZ4_SIZE_MARKER]: true, size, compressedData }\n    }\n  }\n  return { [LZ4_SIZE_MARKER]: true, size: 0 }\n}\n\n/**\n * Extension handler for LZ4BlockArray (type 98)\n *\n * The ext payload can be:\n * 1. Just a msgpack-encoded size (array format) - compressed data is sibling element\n * 2. 4-byte size + compressed data (inline format)\n */\nfunction handleLz4BlockArrayExt(data: Uint8Array): Lz4SizeMarker {\n  // Try to read as msgpack integer first (array format)\n  const msgpackSize = readMsgpackInt(data)\n  if (msgpackSize !== null && isValidSize(msgpackSize)) {\n    // Looks like a reasonable size - this is likely the array format\n    // where compressed data will be the next element in the parent array\n    return { [LZ4_SIZE_MARKER]: true, size: msgpackSize }\n  }\n\n  // Try inline format (same as LZ4Block type 99)\n  const inlineResult = handleLz4BlockExt(data)\n  if (inlineResult.compressedData) {\n    return inlineResult\n  }\n\n  // Fallback: treat first byte as size, or 0 if empty\n  return { [LZ4_SIZE_MARKER]: true, size: data.length > 0 ? data[0] : 0 }\n}\n\n/**\n * Register extension handlers globally\n *\n * NOTE: This has side effects - it modifies the global msgpackr state.\n * After registration, ALL decode() calls will return Lz4SizeMarker objects\n * for LZ4 extension types. Code using decode() directly must handle these\n * markers or use decodeMsgpackWithLz4() instead.\n */\nexport function registerLz4Extensions(): void {\n  addExtension({\n    type: LZ4_BLOCK_ARRAY_TYPE,\n    unpack: handleLz4BlockArrayExt,\n  })\n\n  addExtension({\n    type: LZ4_BLOCK_TYPE,\n    unpack: handleLz4BlockExt,\n  })\n}\n\n// Register extensions when this module is imported\nregisterLz4Extensions()\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/msgpack/index.ts",
    "content": "/**\n * MessagePack utilities with .NET MessagePack-CSharp LZ4 compression support\n */\n\nexport { decodeMsgpackWithLz4 } from './decoder'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/msgpack/lz4.spec.ts",
    "content": "import {\n  LZ4_SIZE_MARKER,\n  isLz4SizeMarker,\n  readMsgpackInt,\n  decompressLz4,\n} from './lz4'\n\ndescribe('lz4', () => {\n  describe('isLz4SizeMarker', () => {\n    it('should return true for valid Lz4SizeMarker', () => {\n      const marker = { [LZ4_SIZE_MARKER]: true, size: 100 }\n      expect(isLz4SizeMarker(marker)).toBe(true)\n    })\n\n    it('should return true for marker with compressedData', () => {\n      const marker = {\n        [LZ4_SIZE_MARKER]: true,\n        size: 100,\n        compressedData: new Uint8Array([1, 2, 3]),\n      }\n      expect(isLz4SizeMarker(marker)).toBe(true)\n    })\n\n    it('should return false for null', () => {\n      expect(isLz4SizeMarker(null)).toBe(false)\n    })\n\n    it('should return false for undefined', () => {\n      expect(isLz4SizeMarker(undefined)).toBe(false)\n    })\n\n    it('should return false for primitive values', () => {\n      expect(isLz4SizeMarker(123)).toBe(false)\n      expect(isLz4SizeMarker('string')).toBe(false)\n      expect(isLz4SizeMarker(true)).toBe(false)\n    })\n\n    it('should return false for regular objects', () => {\n      expect(isLz4SizeMarker({ size: 100 })).toBe(false)\n      expect(isLz4SizeMarker({ foo: 'bar' })).toBe(false)\n    })\n\n    it('should return false for arrays', () => {\n      expect(isLz4SizeMarker([1, 2, 3])).toBe(false)\n    })\n  })\n\n  describe('readMsgpackInt', () => {\n    it('should return null for empty buffer', () => {\n      expect(readMsgpackInt(new Uint8Array([]))).toBeNull()\n    })\n\n    describe('positive fixint (0x00 - 0x7f)', () => {\n      it('should read 0', () => {\n        expect(readMsgpackInt(new Uint8Array([0x00]))).toBe(0)\n      })\n\n      it('should read 127', () => {\n        expect(readMsgpackInt(new Uint8Array([0x7f]))).toBe(127)\n      })\n\n      it('should read 42', () => {\n        expect(readMsgpackInt(new Uint8Array([0x2a]))).toBe(42)\n      })\n    })\n\n    describe('negative fixint (0xe0 - 0xff)', () => {\n      it('should read -1', () => {\n        expect(readMsgpackInt(new Uint8Array([0xff]))).toBe(-1)\n      })\n\n      it('should read -32', () => {\n        expect(readMsgpackInt(new Uint8Array([0xe0]))).toBe(-32)\n      })\n\n      it('should read -10', () => {\n        expect(readMsgpackInt(new Uint8Array([0xf6]))).toBe(-10)\n      })\n    })\n\n    describe('uint8 (0xcc)', () => {\n      it('should read 128', () => {\n        expect(readMsgpackInt(new Uint8Array([0xcc, 0x80]))).toBe(128)\n      })\n\n      it('should read 255', () => {\n        expect(readMsgpackInt(new Uint8Array([0xcc, 0xff]))).toBe(255)\n      })\n\n      it('should return null if buffer too short', () => {\n        expect(readMsgpackInt(new Uint8Array([0xcc]))).toBeNull()\n      })\n    })\n\n    describe('uint16 (0xcd)', () => {\n      it('should read 256', () => {\n        expect(readMsgpackInt(new Uint8Array([0xcd, 0x01, 0x00]))).toBe(256)\n      })\n\n      it('should read 65535', () => {\n        expect(readMsgpackInt(new Uint8Array([0xcd, 0xff, 0xff]))).toBe(65535)\n      })\n\n      it('should return null if buffer too short', () => {\n        expect(readMsgpackInt(new Uint8Array([0xcd, 0x01]))).toBeNull()\n      })\n    })\n\n    describe('uint32 (0xce)', () => {\n      it('should read 65536', () => {\n        expect(\n          readMsgpackInt(new Uint8Array([0xce, 0x00, 0x01, 0x00, 0x00])),\n        ).toBe(65536)\n      })\n\n      it('should read large number', () => {\n        expect(\n          readMsgpackInt(new Uint8Array([0xce, 0x00, 0x0f, 0x42, 0x40])),\n        ).toBe(1000000)\n      })\n\n      it('should return null if buffer too short', () => {\n        expect(\n          readMsgpackInt(new Uint8Array([0xce, 0x00, 0x01, 0x00])),\n        ).toBeNull()\n      })\n    })\n\n    describe('int8 (0xd0)', () => {\n      it('should read -128', () => {\n        expect(readMsgpackInt(new Uint8Array([0xd0, 0x80]))).toBe(-128)\n      })\n\n      it('should read -1', () => {\n        expect(readMsgpackInt(new Uint8Array([0xd0, 0xff]))).toBe(-1)\n      })\n\n      it('should read positive value', () => {\n        expect(readMsgpackInt(new Uint8Array([0xd0, 0x7f]))).toBe(127)\n      })\n    })\n\n    describe('int16 (0xd1)', () => {\n      it('should read -32768', () => {\n        expect(readMsgpackInt(new Uint8Array([0xd1, 0x80, 0x00]))).toBe(-32768)\n      })\n\n      it('should read -1', () => {\n        expect(readMsgpackInt(new Uint8Array([0xd1, 0xff, 0xff]))).toBe(-1)\n      })\n    })\n\n    describe('int32 (0xd2)', () => {\n      it('should read -1', () => {\n        expect(\n          readMsgpackInt(new Uint8Array([0xd2, 0xff, 0xff, 0xff, 0xff])),\n        ).toBe(-1)\n      })\n\n      it('should read negative number', () => {\n        expect(\n          readMsgpackInt(new Uint8Array([0xd2, 0xff, 0xf0, 0xbd, 0xc0])),\n        ).toBe(-1000000)\n      })\n    })\n\n    describe('unsupported formats', () => {\n      it('should return null for unsupported type', () => {\n        // 0xc0 is nil in msgpack, not an integer\n        expect(readMsgpackInt(new Uint8Array([0xc0]))).toBeNull()\n      })\n\n      it('should return null for string marker', () => {\n        // 0xa1 is fixstr\n        expect(readMsgpackInt(new Uint8Array([0xa1, 0x61]))).toBeNull()\n      })\n    })\n  })\n\n  describe('decompressLz4', () => {\n    it('should decompress simple LZ4 data', () => {\n      // LZ4 compressed \"hello\" (raw block format)\n      // This is a literal-only block: 0x50 = literal length 5, followed by \"hello\"\n      const compressed = new Uint8Array([0x50, 0x68, 0x65, 0x6c, 0x6c, 0x6f])\n      const result = decompressLz4(compressed, 5)\n\n      expect(result).toEqual(new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f])) // \"hello\"\n    })\n\n    it('should decompress longer literal data', () => {\n      // LZ4 compressed \"hello world\" (literal-only block)\n      // 0xB0 = literal length 11, followed by \"hello world\"\n      const text = 'hello world'\n      const encoder = new TextEncoder()\n      const textBytes = encoder.encode(text)\n      const compressed = new Uint8Array([0xb0, ...textBytes])\n      const result = decompressLz4(compressed, text.length)\n\n      expect(new TextDecoder().decode(result)).toBe('hello world')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/msgpack/lz4.ts",
    "content": "/**\n * LZ4 decompression utilities for MessagePack-CSharp format\n *\n * @see https://github.com/MessagePack-CSharp/MessagePack-CSharp#lz4-compression\n */\n\n// @ts-ignore - lz4js doesn't have type definitions\nimport { decompressBlock as decompressLz4Block, makeBuffer } from 'lz4js'\n\n// Marker symbol to identify LZ4 size markers in decoded output\nexport const LZ4_SIZE_MARKER = Symbol('lz4Size')\n\nexport interface Lz4SizeMarker {\n  [LZ4_SIZE_MARKER]: true\n  size: number\n  compressedData?: Uint8Array // For inline format where data is in the ext payload\n}\n\n/**\n * Checks if a value is an LZ4 size marker\n */\nexport function isLz4SizeMarker(value: unknown): value is Lz4SizeMarker {\n  return typeof value === 'object' && value !== null && LZ4_SIZE_MARKER in value\n}\n\n/**\n * Reads a msgpack integer from a buffer\n */\nexport function readMsgpackInt(buffer: Uint8Array): number | null {\n  if (buffer.length === 0) return null\n\n  const firstByte = buffer[0]\n\n  // Positive fixint (0x00 - 0x7f)\n  if (firstByte <= 0x7f) return firstByte\n\n  // Negative fixint (0xe0 - 0xff)\n  if (firstByte >= 0xe0) return firstByte - 256\n\n  // uint8 (0xcc)\n  if (firstByte === 0xcc && buffer.length >= 2) return buffer[1]\n\n  // uint16 (0xcd)\n  if (firstByte === 0xcd && buffer.length >= 3) {\n    return new DataView(buffer.buffer, buffer.byteOffset, 3).getUint16(1, false)\n  }\n\n  // uint32 (0xce)\n  if (firstByte === 0xce && buffer.length >= 5) {\n    return new DataView(buffer.buffer, buffer.byteOffset, 5).getUint32(1, false)\n  }\n\n  // int8 (0xd0)\n  if (firstByte === 0xd0 && buffer.length >= 2) {\n    return buffer[1] > 127 ? buffer[1] - 256 : buffer[1]\n  }\n\n  // int16 (0xd1)\n  if (firstByte === 0xd1 && buffer.length >= 3) {\n    return new DataView(buffer.buffer, buffer.byteOffset, 3).getInt16(1, false)\n  }\n\n  // int32 (0xd2)\n  if (firstByte === 0xd2 && buffer.length >= 5) {\n    return new DataView(buffer.buffer, buffer.byteOffset, 5).getInt32(1, false)\n  }\n\n  return null\n}\n\n/**\n * Decompresses raw LZ4 block data\n */\nexport function decompressLz4(\n  compressedData: Uint8Array,\n  uncompressedSize: number,\n): Uint8Array {\n  const dst = makeBuffer(uncompressedSize)\n  const actualSize = decompressLz4Block(\n    compressedData,\n    dst,\n    0,\n    compressedData.length,\n    0,\n  )\n  return new Uint8Array(dst.slice(0, actualSize))\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/utils.ts",
    "content": "export const bufferFormatRangeItems = (\n  items: any[],\n  startIndex: number,\n  lastIndex: number,\n  formatItem: (item: any) => any,\n): any[] => {\n  const newItems = []\n  if (lastIndex >= startIndex) {\n    for (let index = startIndex; index <= lastIndex; index++) {\n      if (!items[index]) return newItems\n      newItems.push(formatItem(items[index]))\n    }\n  }\n\n  return newItems\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/formatters/valueFormatters.tsx",
    "content": "import { encode } from 'msgpackr'\n// eslint-disable-next-line import/order\nimport { Buffer } from 'buffer'\nimport { isUndefined, get } from 'lodash'\nimport { serialize, unserialize } from 'php-serialize'\nimport { getData } from 'rawproto'\nimport { Parser } from 'pickleparser'\nimport JSONBigInt from 'json-bigint'\nimport { store } from 'uiSrc/slices/store'\n\nimport JSONViewer from 'uiSrc/components/json-viewer/JSONViewer'\nimport {\n  DATETIME_FORMATTER_DEFAULT,\n  KeyValueFormat,\n  TimezoneOption,\n} from 'uiSrc/constants'\nimport { decodeMsgpackWithLz4 } from './msgpack'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  anyToBuffer,\n  bufferToASCII,\n  bufferToBinary,\n  bufferToHex,\n  bufferToUTF8,\n  bufferToJava,\n  hexToBuffer,\n  stringToBuffer,\n  binaryToBuffer,\n  Maybe,\n  bufferToFloat64Array,\n  bufferToFloat32Array,\n  checkTimestamp,\n  convertTimestampToMilliseconds,\n  formatTimestamp,\n  UTF8ToBuffer,\n  isEqualBuffers,\n} from 'uiSrc/utils'\nimport { reSerializeJSON } from 'uiSrc/utils/formatters/json'\n\nexport interface FormattingProps {\n  expanded?: boolean\n  skipVector?: boolean\n  tooltip?: boolean\n}\n\nconst isTextViewFormatter = (format: KeyValueFormat) =>\n  [\n    KeyValueFormat.Unicode,\n    KeyValueFormat.ASCII,\n    KeyValueFormat.HEX,\n    KeyValueFormat.Binary,\n  ].includes(format)\nconst isJsonViewFormatter = (format: KeyValueFormat) =>\n  !isTextViewFormatter(format)\nconst isFormatEditable = (format: KeyValueFormat) =>\n  ![\n    KeyValueFormat.Protobuf,\n    KeyValueFormat.JAVA,\n    KeyValueFormat.Pickle,\n    KeyValueFormat.Vector32Bit,\n    KeyValueFormat.Vector64Bit,\n    KeyValueFormat.HEX,\n    KeyValueFormat.Binary,\n  ].includes(format)\n\nconst isFullStringLoaded = (\n  currentLength: Maybe<number>,\n  fullLength: Maybe<number>,\n) => currentLength === fullLength\n\nconst isNonUnicodeFormatter = (format: KeyValueFormat, isValid: boolean) => {\n  if (format === KeyValueFormat.Msgpack) {\n    return isValid\n  }\n  return [\n    KeyValueFormat.ASCII,\n    KeyValueFormat.HEX,\n    KeyValueFormat.Binary,\n  ].includes(format)\n}\n\nconst bufferToUnicode = (reply: RedisResponseBuffer): string =>\n  bufferToUTF8(reply)\n\nconst bufferToJSON = (\n  reply: RedisResponseBuffer,\n  props: FormattingProps,\n): { value: JSX.Element | string; isValid: boolean } =>\n  JSONViewer({ value: bufferToUTF8(reply), useNativeBigInt: false, ...props })\n\nconst formattingBuffer = (\n  reply: RedisResponseBuffer,\n  format: KeyValueFormat,\n  props?: FormattingProps,\n): { value: JSX.Element | string; isValid: boolean } => {\n  switch (format) {\n    case KeyValueFormat.ASCII:\n      return { value: bufferToASCII(reply), isValid: true }\n    case KeyValueFormat.HEX:\n      return { value: bufferToHex(reply), isValid: true }\n    case KeyValueFormat.Binary:\n      return { value: bufferToBinary(reply), isValid: true }\n    case KeyValueFormat.JSON:\n      return bufferToJSON(reply, props as FormattingProps)\n    case KeyValueFormat.Msgpack: {\n      try {\n        const data = Uint8Array.from(reply.data)\n        const decoded = decodeMsgpackWithLz4(data)\n        const value = JSONBigInt.stringify(decoded)\n        return JSONViewer({ value, ...props })\n      } catch (e) {\n        return { value: bufferToUTF8(reply), isValid: false }\n      }\n    }\n    case KeyValueFormat.PHP: {\n      try {\n        const decoded = unserialize(\n          bufferToUTF8(reply),\n          {},\n          { strict: false, encoding: 'utf8' },\n        )\n        const value = JSONBigInt.stringify(decoded)\n        return JSONViewer({ value, ...props })\n      } catch (e) {\n        return { value: bufferToUTF8(reply), isValid: false }\n      }\n    }\n    case KeyValueFormat.JAVA: {\n      try {\n        const decoded = bufferToJava(reply)\n        const value = JSONBigInt.stringify(decoded)\n        return JSONViewer({ value, ...props })\n      } catch (e) {\n        return { value: bufferToUTF8(reply), isValid: false }\n      }\n    }\n    case KeyValueFormat.Vector32Bit: {\n      const utfVariant = bufferToUTF8(reply)\n      try {\n        if (props?.skipVector) return { value: utfVariant, isValid: true }\n        const bufferFromUtf = UTF8ToBuffer(utfVariant)\n        if (isEqualBuffers(reply, bufferFromUtf)) {\n          return { value: utfVariant, isValid: true }\n        }\n        const vector = Array.from(\n          bufferToFloat32Array(reply.data as Uint8Array),\n        )\n        const value = JSONBigInt.stringify(vector)\n        return JSONViewer({ value, useNativeBigInt: false, ...props })\n      } catch (e) {\n        return { value: utfVariant, isValid: false }\n      }\n    }\n    case KeyValueFormat.Vector64Bit: {\n      const utfVariant = bufferToUTF8(reply)\n      try {\n        if (props?.skipVector) return { value: utfVariant, isValid: true }\n        const bufferFromUtf = UTF8ToBuffer(utfVariant)\n        if (isEqualBuffers(reply, bufferFromUtf)) {\n          return { value: utfVariant, isValid: true }\n        }\n        const vector = Array.from(\n          bufferToFloat64Array(reply.data as Uint8Array),\n        )\n        const value = JSONBigInt.stringify(vector)\n        return JSONViewer({ value, useNativeBigInt: false, ...props })\n      } catch (e) {\n        return { value: bufferToUTF8(reply), isValid: false }\n      }\n    }\n    case KeyValueFormat.Protobuf: {\n      try {\n        if (reply.data?.length === 0) {\n          throw new Error()\n        }\n\n        const decoded = getData(Buffer.from(reply.data))\n        const value = JSONBigInt.stringify(decoded)\n        return JSONViewer({ value, ...props })\n      } catch (e) {\n        return { value: bufferToUTF8(reply), isValid: false }\n      }\n    }\n    case KeyValueFormat.Pickle: {\n      try {\n        const parser = new Parser()\n        const decoded = parser.parse(new Uint8Array(reply.data))\n\n        if (isUndefined(decoded)) {\n          return {\n            value: bufferToUTF8(reply),\n            isValid: false,\n          }\n        }\n\n        const value = JSONBigInt.stringify(decoded)\n        return JSONViewer({ value, ...props })\n      } catch (e) {\n        return { value: bufferToUTF8(reply), isValid: false }\n      }\n    }\n    case KeyValueFormat.DateTime: {\n      const value = bufferToUnicode(reply)?.trim()\n      try {\n        if (checkTimestamp(value)) {\n          // formatting to DateTime only from timestamp(the number of milliseconds since January 1, 1970, UTC).\n          // if seconds - add milliseconds (since JS Date works only with milliseconds)\n          const timestamp = convertTimestampToMilliseconds(value)\n          const config = get(store.getState(), 'user.settings.config', null)\n          return {\n            value: formatTimestamp(\n              timestamp,\n              config?.dateFormat || DATETIME_FORMATTER_DEFAULT,\n              config?.timezone || TimezoneOption.Local,\n            ),\n            isValid: true,\n          }\n        }\n      } catch (e) {\n        // if error return default\n      }\n      return { value, isValid: false }\n    }\n    default:\n      return { value: bufferToUnicode(reply), isValid: true }\n  }\n}\n\nconst bufferToSerializedFormat = (\n  format: KeyValueFormat,\n  value: RedisResponseBuffer = stringToBuffer(''),\n  space?: number,\n): string => {\n  switch (format) {\n    case KeyValueFormat.ASCII:\n      return bufferToASCII(value)\n    case KeyValueFormat.HEX:\n      return bufferToHex(value)\n    case KeyValueFormat.Binary:\n      return bufferToBinary(value)\n    case KeyValueFormat.JSON:\n      return reSerializeJSON(bufferToUTF8(value), space)\n    case KeyValueFormat.Vector32Bit:\n      return bufferToFloat32Array(value.data as Uint8Array)\n    case KeyValueFormat.Vector64Bit:\n      return bufferToFloat64Array(value.data as Uint8Array)\n    case KeyValueFormat.Msgpack: {\n      try {\n        const decoded = decodeMsgpackWithLz4(Uint8Array.from(value.data))\n        const stringified = JSON.stringify(decoded)\n        return reSerializeJSON(stringified, space)\n      } catch (e) {\n        return bufferToUTF8(value)\n      }\n    }\n    case KeyValueFormat.PHP: {\n      try {\n        const decoded = unserialize(\n          bufferToUTF8(value),\n          {},\n          { strict: false, encoding: 'utf8' },\n        )\n        const stringified = JSON.stringify(decoded)\n        return reSerializeJSON(stringified, space)\n      } catch (e) {\n        return bufferToUTF8(value)\n      }\n    }\n    default:\n      return bufferToUTF8(value)\n  }\n}\n\nconst stringToSerializedBufferFormat = (\n  format: KeyValueFormat,\n  value: string,\n): RedisResponseBuffer => {\n  switch (format) {\n    case KeyValueFormat.HEX: {\n      if (\n        (value.match(/([0-9]|[a-f])/gim) || []).length === value.length &&\n        value.length % 2 === 0\n      ) {\n        return hexToBuffer(value)\n      }\n      return stringToBuffer(value)\n    }\n    case KeyValueFormat.Binary: {\n      const str = value.replace(/ /g, '')\n      if (str.length % 8 === 0 && /^[0-1]+$/g.test(str)) {\n        return binaryToBuffer(str)\n      }\n      return stringToBuffer(value)\n    }\n    case KeyValueFormat.JSON: {\n      return stringToBuffer(reSerializeJSON(value))\n    }\n    case KeyValueFormat.Msgpack: {\n      try {\n        const json = JSON.parse(value)\n        const encoded = encode(json)\n        return anyToBuffer(encoded)\n      } catch (e) {\n        return stringToBuffer(value, format)\n      }\n    }\n    case KeyValueFormat.PHP: {\n      try {\n        const json = JSON.parse(value)\n        const serialized = serialize(json)\n        return stringToBuffer(serialized)\n      } catch (e) {\n        return stringToBuffer(value, format)\n      }\n    }\n    default: {\n      return stringToBuffer(value, format)\n    }\n  }\n}\n\nexport {\n  formattingBuffer,\n  isTextViewFormatter,\n  isJsonViewFormatter,\n  isFormatEditable,\n  isFullStringLoaded,\n  bufferToSerializedFormat,\n  stringToSerializedBufferFormat,\n  isNonUnicodeFormatter,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/getLetterByIndex.ts",
    "content": "const getLetterByIndex = (index: number): string => {\n  const mod = index % 26\n  const pow = (index / 26) | 0\n  const out = String.fromCharCode(65 + mod)\n  return pow ? getLetterByIndex(pow - 1) + out : out\n}\n\nexport default getLetterByIndex\n"
  },
  {
    "path": "redisinsight/ui/src/utils/groupTypes.ts",
    "content": "import { GROUP_TYPES_DISPLAY, GroupTypesDisplay } from 'uiSrc/constants'\nimport { Nullable } from 'uiSrc/utils/types'\n\nexport const NO_TYPE_NAME = 'Unknown'\n\nexport const getGroupTypeDisplay = (type?: Nullable<string>) =>\n  type\n    ? type in GROUP_TYPES_DISPLAY\n      ? GROUP_TYPES_DISPLAY[type as GroupTypesDisplay]\n      : type?.replace(/_/g, ' ')\n    : NO_TYPE_NAME\n"
  },
  {
    "path": "redisinsight/ui/src/utils/index.ts",
    "content": "import type { Nullable, Maybe } from './types'\nimport getLetterByIndex from './getLetterByIndex'\nimport RouterWithSubRoutes from './routerWithSubRoutes'\n\nexport * from './common'\nexport * from './validations'\nexport * from './statuses'\nexport * from './instance'\nexport * from './apiResponse'\nexport * from './parseResponse'\nexport * from './parseRedisUrl'\nexport * from './comparisons'\nexport * from './longNames'\nexport * from './cliHelper'\nexport * from './commands'\nexport * from './workbench'\nexport * from './transformers'\nexport * from './monaco'\nexport * from './dom'\nexport * from './monitorUtils'\nexport * from './plugins'\nexport * from './redistack'\nexport * from './tree'\nexport * from './pubSubUtils'\nexport * from './formatters'\nexport * from './groupTypes'\nexport * from './modules'\nexport * from './events'\nexport * from './telemetry'\nexport * from './errors'\nexport * from './redisearch'\nexport * from './capability'\nexport * from './rdi'\nexport * from './bigString'\n\nexport { Maybe, Nullable, RouterWithSubRoutes, getLetterByIndex }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/getUrlInstance.ts",
    "content": "import { ApiEndpoints } from 'uiSrc/constants'\n\nconst getUrl = (...path: string[]) =>\n  `/${ApiEndpoints.DATABASES}/${path.join('/')}`\n\nexport default getUrl\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/index.ts",
    "content": "import getUrl from './getUrlInstance'\n\nexport * from './instanceModules'\nexport * from './instanceOptions'\nexport * from './instanceNavigation'\nexport * from './instanceProvider'\n\nexport { getUrl }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/instanceModules.spec.ts",
    "content": "import {\n  getModule,\n  ensureSemanticVersion,\n} from 'uiSrc/utils/instance/instanceModules'\nimport { AdditionalRedisModule } from 'uiSrc/slices/interfaces'\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\n\n// Test fixtures\nconst moduleTestCases = [\n  {\n    input: 'RedisJSON',\n    expected: {\n      name: 'RedisJSON',\n      abbreviation: 'RJ',\n      descriptionContains: 'JSON',\n    },\n    description: 'exact name matches',\n  },\n  {\n    input: 'redisjson',\n    expected: {\n      name: 'RedisJSON',\n      abbreviation: 'RJ',\n      descriptionContains: 'JSON',\n    },\n    description: 'name matches ignoring case',\n  },\n  {\n    input: 'RedisTimeSeries',\n    expected: {\n      name: 'RedisTimeSeries',\n      abbreviation: 'RT',\n      descriptionContains: 'Time-series',\n    },\n    description: 'name matches with camelCase',\n  },\n  {\n    input: 'RediSearch',\n    expected: {\n      name: 'RediSearch',\n      abbreviation: 'RS',\n      descriptionContains: 'Full-Text search',\n    },\n    description: 'name matches with different format',\n  },\n  {\n    input: 'redis_time_series',\n    expected: {\n      name: 'RedisTimeSeries',\n      abbreviation: 'RT',\n      descriptionContains: 'Time-series',\n    },\n    description: 'module names with mixed case and separators',\n  },\n]\n\nconst emptyResultTestCases = [\n  { input: 'NonExistentModule', description: 'module not found' },\n  { input: '', description: 'propName is empty' },\n  { input: undefined, description: 'propName is undefined' },\n]\n\ndescribe('instanceModules', () => {\n  describe('getModule', () => {\n    it.each(moduleTestCases)(\n      'should return module when $description',\n      ({ input, expected }) => {\n        const result = getModule(input)\n        expect(result.name).toBe(expected.name)\n        expect(result.abbreviation).toBe(expected.abbreviation)\n        expect(result.description).toContain(expected.descriptionContains)\n      },\n    )\n\n    it.each(emptyResultTestCases)(\n      'should return empty object when $description',\n      ({ input }) => {\n        const result = getModule(input)\n        expect(result).toEqual({})\n      },\n    )\n  })\n\n  describe('ensureSemanticVersion', () => {\n    const mockModule1: AdditionalRedisModule = {\n      name: 'RedisJSON',\n      version: 20400,\n      semanticVersion: '',\n    }\n\n    const mockModule2: AdditionalRedisModule = {\n      name: 'RediSearch',\n      version: 20604,\n      semanticVersion: '2.6.4',\n    }\n\n    const mockModule3: AdditionalRedisModule = {\n      name: 'RedisTimeSeries',\n      version: 10800,\n      semanticVersion: '',\n    }\n\n    it('should convert numeric version to semantic version when semanticVersion is empty', () => {\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule1],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules).toHaveLength(1)\n      expect(result.modules![0]).toEqual({\n        ...mockModule1,\n        semanticVersion: '2.4.0',\n      })\n    })\n\n    it('should preserve existing semantic version when already present', () => {\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule2],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules).toHaveLength(1)\n      expect(result.modules![0]).toEqual({\n        ...mockModule2,\n        semanticVersion: '2.6.4',\n      })\n    })\n\n    it('should handle multiple modules with mixed semantic version states', () => {\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule1, mockModule2, mockModule3],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules).toHaveLength(3)\n      expect(result.modules![0].semanticVersion).toBe('2.4.0')\n      expect(result.modules![1].semanticVersion).toBe('2.6.4')\n      expect(result.modules![2].semanticVersion).toBe('1.8.0')\n    })\n\n    it('should handle instance with no modules', () => {\n      const mockInstance = DBInstanceFactory.build({\n        modules: undefined as any,\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules).toBeUndefined()\n    })\n\n    it('should handle instance with empty modules array', () => {\n      const mockInstance = DBInstanceFactory.build({\n        modules: [],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules).toEqual([])\n    })\n\n    it('should handle modules with version 0', () => {\n      const moduleWithZeroVersion: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: 0,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [moduleWithZeroVersion],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules![0].semanticVersion).toBe('0')\n    })\n\n    it('should handle modules with undefined version', () => {\n      const moduleWithUndefinedVersion: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: undefined as any,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [moduleWithUndefinedVersion],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules![0].semanticVersion).toBe('')\n    })\n\n    it('should handle modules with null version', () => {\n      const moduleWithNullVersion: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: null as any,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [moduleWithNullVersion],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.modules![0].semanticVersion).toBe('')\n    })\n\n    it('should preserve other instance properties', () => {\n      const mockInstance = DBInstanceFactory.build({\n        id: 'test-id',\n        name: 'Test Instance',\n        host: 'localhost',\n        port: 6379,\n        modules: [mockModule1],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n\n      expect(result.id).toBe('test-id')\n      expect(result.name).toBe('Test Instance')\n      expect(result.host).toBe('localhost')\n      expect(result.port).toBe(6379)\n    })\n  })\n\n  describe('convert number to semantic version', () => {\n    it('should convert 6-digit version correctly', () => {\n      const mockModule: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: 123456,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n      expect(result.modules![0].semanticVersion).toBe('12.34.56')\n    })\n\n    it('should pad shorter versions with zeros', () => {\n      const mockModule: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: 123,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n      expect(result.modules![0].semanticVersion).toBe('0.1.23')\n    })\n\n    it('should handle single digit versions', () => {\n      const mockModule: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: 5,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n      expect(result.modules![0].semanticVersion).toBe('0.0.5')\n    })\n\n    it('should handle large version numbers', () => {\n      const mockModule: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: 9876543,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n      expect(result.modules![0].semanticVersion).toBe('987.65.43')\n    })\n\n    it('should return string representation for negative numbers', () => {\n      const mockModule: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: -123,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n      expect(result.modules![0].semanticVersion).toBe('-123')\n    })\n\n    it('should return string representation for non-numeric values', () => {\n      const mockModule: AdditionalRedisModule = {\n        name: 'TestModule',\n        version: 'invalid' as any,\n        semanticVersion: '',\n      }\n\n      const mockInstance = DBInstanceFactory.build({\n        modules: [mockModule],\n      })\n\n      const result = ensureSemanticVersion(mockInstance)\n      expect(result.modules![0].semanticVersion).toBe('invalid')\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/instanceModules.ts",
    "content": "import { find, isNumber } from 'lodash'\nimport modulesInit from 'uiSrc/constants/allRedisModules.json'\nimport {\n  DATABASE_LIST_MODULES_TEXT,\n  Instance,\n  AdditionalRedisModule,\n} from 'uiSrc/slices/interfaces'\n\nconst getGenericModuleName = (name = '') =>\n  (DATABASE_LIST_MODULES_TEXT[name] ?? name)\n    ?.toLowerCase?.()\n    .replaceAll?.(/[-_]/gi, '')\n\nexport const getModule = (propName = ''): any =>\n  find(\n    modulesInit,\n    ({ name }) => getGenericModuleName(name) === getGenericModuleName(propName),\n  ) ?? {}\n\nconst convertToSemanticVersion = (input?: number): string => {\n  if (input === undefined || input === null) {\n    return ''\n  }\n  const separator = '.'\n  try {\n    if (isNumber(input) && input > 0) {\n      // Pad input with optional zero symbols\n      const version = String(input).padStart(6, '0')\n      const patch = parseInt(version.slice(-2), 10)\n      const minor = parseInt(version.slice(-4, -2), 10)\n      const major = parseInt(version.slice(0, -4), 10)\n      return [major, minor, patch].join(separator)\n    }\n  } catch (e) {\n    console.error('Failed to generate semantic version.', e)\n  }\n  return `${input}`\n}\n\nconst ensureModuleSemanticVersion = (\n  module: AdditionalRedisModule,\n): AdditionalRedisModule => ({\n  ...module,\n  semanticVersion:\n    module.semanticVersion || convertToSemanticVersion(module.version),\n})\n\nexport const ensureSemanticVersion = (instance: Instance): Instance => {\n  if (!instance.modules || !Array.isArray(instance.modules)) {\n    return instance\n  }\n  return {\n    ...instance,\n    modules: instance.modules?.map(ensureModuleSemanticVersion),\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/instanceNavigation.ts",
    "content": "import { orderBy } from 'lodash'\nimport { PropertySort } from '@elastic/eui'\nimport { Instance, RdiInstance } from 'uiSrc/slices/interfaces'\nimport { getDbIndex } from '../longNames'\n\nexport const filterAndSort = (\n  arr: Array<Instance | RdiInstance>,\n  search: string,\n  sort: PropertySort,\n): Array<Instance | RdiInstance> => {\n  if (!arr?.length) return arr\n  const filtered = arr.filter((instance) => {\n    const label = `${instance.name} ${getDbIndex(instance.db)}`\n    return label.toLowerCase?.().includes(search.toLowerCase())\n  })\n\n  const sortingFunc = (ins) => {\n    if (sort.field === 'lastConnection') {\n      return ins.lastConnection ? -new Date(`${ins.lastConnection}`) : -Infinity\n    }\n    if (sort.field === 'host') {\n      return `${ins.host}:${ins.port}`\n    }\n    return sort.field\n  }\n\n  return orderBy(filtered, sortingFunc, sort.direction)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/instanceOptions.ts",
    "content": "import { find, identity, pickBy } from 'lodash'\nimport {\n  InstanceRedisCloud,\n  InstanceRedisCluster,\n} from 'uiSrc/slices/interfaces'\n\nexport const parseInstanceOptionsCluster = (\n  uid: number,\n  instances: InstanceRedisCluster[],\n) => {\n  const { options } =\n    find(instances, (instance: InstanceRedisCluster) => instance.uid === uid) ||\n    {}\n  return pickBy(options, identity)\n}\n\nexport const parseInstanceOptionsCloud = (\n  databaseId: number,\n  instances: InstanceRedisCloud[],\n) => {\n  const { options } =\n    find(\n      instances,\n      (instance: InstanceRedisCloud) => instance.databaseId === databaseId,\n    ) || {}\n  return pickBy(options, identity)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/instance/instanceProvider.ts",
    "content": "import { Instance } from 'uiSrc/slices/interfaces'\nimport { Nullable } from 'uiSrc/utils/types'\n\nconst AZURE_PROVIDER = 'azure'\n\nexport const isAzureDatabase = (\n  instance: Nullable<Partial<Instance>>,\n): boolean => {\n  if (!instance?.providerDetails) {\n    return false\n  }\n\n  return instance.providerDetails.provider === AZURE_PROVIDER\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/links.ts",
    "content": "import { UTM_MEDIUMS } from 'uiSrc/constants/links'\nimport { Instance } from 'uiSrc/slices/interfaces'\n\nexport interface UTMParams {\n  source?: string\n  medium?: string\n  campaign: string\n}\n\nexport const getUtmExternalLink = (baseUrl: string, params: UTMParams) => {\n  const { source = 'redisinsight', medium = UTM_MEDIUMS.App, campaign } = params\n  try {\n    const url = new URL(baseUrl)\n    url.searchParams.append('utm_source', source)\n    url.searchParams.append('utm_medium', medium)\n    url.searchParams.append('utm_campaign', campaign)\n    return url.toString()\n  } catch (e) {\n    return baseUrl\n  }\n}\n\nconst RI_PROTOCOL_SCHEMA = 'redisinsight://'\n\nexport const buildRedisInsightUrl = (instanceData: Instance) => {\n  if (!instanceData) {\n    return ''\n  }\n\n  const endpoint = `${instanceData.host}:${instanceData.port}`\n  const dbUrl = `redis://@${endpoint}`\n\n  const params: Record<string, string> = {\n    redisUrl: dbUrl,\n    cloudBdbId: instanceData.cloudDetails?.cloudId?.toString() || '',\n    databaseAlias: instanceData.name || '',\n  }\n\n  if (instanceData.tls) {\n    params.requiredTls = 'true'\n    params.requiredCaCert = 'true'\n  }\n\n  if (instanceData.tlsClientAuthRequired) {\n    params.requiredClientCert = 'true'\n  }\n\n  if (instanceData.cloudDetails) {\n    params.subscriptionType = instanceData.cloudDetails.subscriptionType || ''\n    params.planMemoryLimit =\n      instanceData.cloudDetails?.planMemoryLimit?.toString() || ''\n    params.memoryLimitMeasurementUnit =\n      instanceData.cloudDetails?.memoryLimitMeasurementUnit || ''\n    if (instanceData.cloudDetails.free) {\n      params.free = 'true'\n    }\n  }\n\n  return `${RI_PROTOCOL_SCHEMA}databases/connect${appendParams(params)}`\n}\n\nconst appendParams = (params: Record<string, string>) => {\n  const searchParams = new URLSearchParams(params)\n  return `?${searchParams.toString()}`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/longNames.tsx",
    "content": "import React from 'react'\nimport { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces'\nimport { replaceSpaces } from 'uiSrc/utils/transformers'\nimport { KeyValueFormat } from 'uiSrc/constants'\nimport { bufferToString, formattingBuffer } from './formatters'\n\nexport function formatLongName(\n  name = '',\n  maxNameLength = 500,\n  endPartLength = 50,\n  separator = '  ...  ',\n) {\n  // replace whitespace characters to no-break spaces - to prevent collapse spaces\n  const currentName = replaceSpaces(name)\n  if (currentName.length <= maxNameLength) {\n    return currentName\n  }\n  const startPart = currentName.substring(\n    0,\n    maxNameLength - endPartLength - separator.length,\n  )\n  const endPart = currentName.substring(currentName.length - endPartLength)\n  return `${startPart}${separator}${endPart}`\n}\n\nexport function createTooltipContent(\n  value: string | JSX.Element,\n  bufferValue: RedisResponseBuffer,\n  viewFormatProp: KeyValueFormat,\n  props = {},\n) {\n  if (React.isValidElement(value)) {\n    return formattingBuffer(bufferValue, viewFormatProp, {\n      tooltip: true,\n      ...props,\n    }).value\n  }\n\n  return formatLongName(value as string)\n}\n\nexport function formatNameShort(name = '') {\n  return formatLongName(name, 68, 15, '...')\n}\n\nexport function getDbIndex(db: number = 0) {\n  return db ? `[db${db}]` : ''\n}\n\nexport const truncateText = (text = '', maxLength = 0, separator = '...') =>\n  text.length >= maxLength ? text.slice(0, maxLength) + separator : text\n\nexport const createDeleteFieldHeader = (keyName: RedisString) =>\n  formatNameShort(bufferToString(keyName))\n\nexport const createDeleteFieldMessage = (field: RedisString) => (\n  <>\n    will be removed from <b>{formatNameShort(bufferToString(field))}</b>\n  </>\n)\n"
  },
  {
    "path": "redisinsight/ui/src/utils/modules.ts",
    "content": "import { find, some } from 'lodash'\n\nimport { getModule, truncateText } from 'uiSrc/utils'\nimport {\n  DATABASE_LIST_MODULES_TEXT,\n  Instance,\n  RedisDefaultModules,\n  REDISEARCH_MODULES,\n} from 'uiSrc/slices/interfaces'\nimport { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module'\nimport { DEFAULT_MODULES_INFO, ModuleInfo } from 'uiSrc/constants/modules'\nimport { AllIconsType } from 'uiSrc/components/base/icons'\n\nexport interface IDatabaseModule {\n  abbreviation: string\n  moduleName: string\n  icon?: any\n  content?: any\n  [key: string]: any\n}\n\nconst PREDEFINED_MODULE_NAMES_ORDER: (keyof typeof DATABASE_LIST_MODULES_TEXT)[] =\n  [\n    RedisDefaultModules.Search,\n    RedisDefaultModules.SearchLight,\n    RedisDefaultModules.ReJSON,\n    RedisDefaultModules.Graph,\n    RedisDefaultModules.TimeSeries,\n    RedisDefaultModules.Bloom,\n    RedisDefaultModules.Gears,\n    RedisDefaultModules.AI,\n    RedisDefaultModules.VectorSet,\n  ]\n\nconst PREDEFINED_MODULES_ORDER = PREDEFINED_MODULE_NAMES_ORDER.map(\n  (module) => DATABASE_LIST_MODULES_TEXT[module],\n) as readonly string[]\n\nconst getModuleOrder = (name: string) =>\n  (PREDEFINED_MODULES_ORDER as readonly string[]).indexOf(name)\n\nexport const sortModules = (modules: IDatabaseModule[] = []) =>\n  modules.sort((a, b) => {\n    if (!a.moduleName && !a.abbreviation) return 1\n    if (!b.moduleName && !b.abbreviation) return -1\n    if (getModuleOrder(a.moduleName) === -1) return 1\n    if (getModuleOrder(b.moduleName) === -1) return -1\n    return getModuleOrder(a.moduleName) - getModuleOrder(b.moduleName)\n  })\n\nexport const sortModulesByName = (modules: AdditionalRedisModule[] = []) =>\n  [...modules].sort((a, b) => {\n    const aIndex = getModuleOrder(a.name)\n    const bIndex = getModuleOrder(b.name)\n    if (aIndex === -1) return 1\n    if (bIndex === -1) return -1\n    return aIndex - bIndex\n  })\n\nexport const isRedisearchModule = (moduleName: string) =>\n  REDISEARCH_MODULES.some((value) => moduleName === value)\nexport const isRedisearchAvailable = (\n  modules: AdditionalRedisModule[],\n): boolean => modules?.some(({ name }) => isRedisearchModule(name))\n\nexport const getRedisearchVersion = (\n  modules: AdditionalRedisModule[] = [],\n): string | undefined => {\n  const mod = modules.find((m) => isRedisearchModule(m.name))\n  return mod?.semanticVersion ?? mod?.version?.toString()\n}\n\nexport const isContainJSONModule = (\n  modules: AdditionalRedisModule[],\n): boolean =>\n  modules?.some(\n    (m: AdditionalRedisModule) => m.name === RedisDefaultModules.ReJSON,\n  )\n\nexport const getDbWithModuleLoaded = (\n  databases: Instance[],\n  moduleName: string,\n) =>\n  find(databases, ({ modules }) => {\n    if (isRedisearchModule(moduleName)) return isRedisearchAvailable(modules)\n\n    return some(modules, ({ name }) => name === moduleName)\n  })\n\nexport const transformModule = (\n  additionalModule: AdditionalRedisModule,\n): IDatabaseModule => {\n  const {\n    name: propName,\n    semanticVersion = '',\n    version = '',\n  } = additionalModule\n\n  const isValidModuleKey = Object.values(RedisDefaultModules).includes(\n    propName as RedisDefaultModules,\n  )\n\n  const module: ModuleInfo | undefined = isValidModuleKey\n    ? DEFAULT_MODULES_INFO[propName as RedisDefaultModules]\n    : undefined\n  const moduleName = module?.text || propName\n\n  const { abbreviation = '', name = moduleName } = getModule(moduleName)\n\n  const moduleAlias = truncateText(name, 50)\n  let icon: AllIconsType | undefined = module?.icon\n  const versionText =\n    semanticVersion || version ? ` v. ${semanticVersion || version}` : ''\n  const content = `${moduleAlias}${versionText}`\n\n  if (!icon && !abbreviation) {\n    icon = 'UnknownModuleIcon'\n  }\n\n  return {\n    moduleName,\n    icon,\n    abbreviation,\n    content,\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/completionProvider.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport const getCompletionProvider = (\n  keywords: string[] = [],\n  functions: monacoEditor.languages.CompletionItem[] = [],\n): monacoEditor.languages.CompletionItemProvider => ({\n  provideCompletionItems: (\n    model: monacoEditor.editor.IModel,\n    position: monacoEditor.Position,\n  ): monacoEditor.languages.CompletionList => {\n    const word = model.getWordUntilPosition(position)\n    const range = {\n      startLineNumber: position.lineNumber,\n      endLineNumber: position.lineNumber,\n      endColumn: word.endColumn,\n      startColumn: word.startColumn,\n    }\n\n    // display suggestions only for words that don't belong to a folding area\n    if (!model.getValueInRange(range).startsWith(' ')) {\n      const keywordsSuggestions: monacoEditor.languages.CompletionItem[] =\n        keywords.map((item: string) => ({\n          label: item,\n          kind: monacoEditor.languages.CompletionItemKind.Keyword,\n          insertText: item,\n          range,\n          sortText: `a${item}`,\n        }))\n\n      const functionsSuggestions: monacoEditor.languages.CompletionItem[] =\n        functions.map((item) => ({\n          ...item,\n          insertText: `${item.insertText ?? item.label}`,\n          kind: monacoEditor.languages.CompletionItemKind.Function,\n          range,\n          sortText: `b${item.label}`,\n          insertTextRules:\n            monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n        }))\n\n      return {\n        suggestions: [...keywordsSuggestions, ...functionsSuggestions],\n      }\n    }\n    return { suggestions: [] }\n  },\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/index.ts",
    "content": "export * from './monacoUtils'\nexport * from './monacoInterfaces'\nexport * from './monacoRedisCompletionProvider'\nexport * from './monacoRedisMonarchTokensProvider'\nexport * from './monacoRedisSignatureHelpProvider'\nexport * from './monacoActions'\nexport * from './monacoDecorations'\nexport * from './regex'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoActions.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport enum MonacoAction {\n  Submit = 'submit',\n  ChangeGroupMode = 'change-group-mode',\n}\n\nexport const getMonacoAction = (\n  actionId: MonacoAction,\n  action: (\n    editor: monacoEditor.editor.IStandaloneCodeEditor,\n    ...args: any[]\n  ) => void | Promise<void>,\n  monaco: typeof monacoEditor,\n): monacoEditor.editor.IActionDescriptor => {\n  if (actionId === MonacoAction.Submit) {\n    return {\n      id: 'submit',\n      label: 'Run Commands',\n      keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],\n      run: action,\n    }\n  }\n\n  return { id: '', label: '', run: () => {} }\n}\n\nexport const actionTriggerParameterHints = (\n  editor: monacoEditor.editor.IStandaloneCodeEditor,\n) => editor.trigger('', 'editor.action.triggerParameterHints', '')\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoDecorations.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nexport interface ILightWeightDecoration {\n  id: string\n  range: monacoEditor.IRange\n}\n\ninterface IModelDeltaDecoration\n  extends monacoEditor.editor.IModelDeltaDecoration {}\n\nexport const toModelDeltaDecoration = (\n  dec: ILightWeightDecoration,\n): IModelDeltaDecoration => ({\n  range: dec.range,\n  options: {\n    className: dec.id,\n    isWholeLine: false,\n    glyphMarginClassName: 'monaco-glyph-run-command',\n    // glyphMarginHoverMessage: { value: 'Run command' }\n  },\n})\n\nexport const decoration = (\n  monaco: typeof monacoEditor,\n  id: string,\n  startLineNumber: number,\n  startColumn: number,\n  endLineNumber: number,\n  endColum: number,\n): ILightWeightDecoration => ({\n  id,\n  range: new monaco.Range(\n    startLineNumber,\n    startColumn,\n    endLineNumber,\n    endColum,\n  ),\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoInterfaces.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { IRedisCommand } from 'uiSrc/constants'\n\nexport interface IMonacoCommand {\n  name: string\n  info?: IRedisCommand\n  position?: monacoEditor.Position\n}\n\nexport interface IMonacoQuery {\n  name: string\n  fullQuery: string\n  commandQuery: string\n  args: [string[], string[]]\n  cursor: {\n    isCursorInQuotes: boolean\n    prevCursorChar: string\n    nextCursorChar: string\n    argLeftOffset: number\n    argRightOffset: number\n  }\n  allArgs: string[]\n  info?: IRedisCommand\n  commandPosition: any\n  position?: monacoEditor.Position\n  commandCursorPosition: number\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoRedisCompletionProvider.ts",
    "content": "import { isNaN } from 'lodash'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\nimport { ICommand, ICommands } from 'uiSrc/constants'\nimport {\n  generateArgsNames,\n  generateArgsForInsertText,\n  getCommandMarkdown,\n  getDocUrlForCommand,\n} from 'uiSrc/utils/commands'\n\ntype DependencyProposals = {\n  [key: string]: monacoEditor.languages.CompletionItem\n}\n\nexport const createDependencyProposals = (\n  commandsSpec: ICommands,\n): DependencyProposals => {\n  const result: DependencyProposals = {}\n  const commandsArr = Object.keys(commandsSpec).sort()\n  commandsArr.forEach((command: string) => {\n    const commandInfo: ICommand = commandsSpec[command]\n    const range = {\n      startLineNumber: 0,\n      endLineNumber: 0,\n      startColumn: 0,\n      endColumn: 0,\n    }\n    const commandArgs = commandInfo?.arguments || []\n    const detail: string = `${command} ${generateArgsNames(commandInfo?.provider, commandArgs).join(' ')}`\n    const argsNames = generateArgsNames(\n      commandInfo?.provider,\n      commandArgs,\n      false,\n      true,\n    )\n    const insertText = `${command} ${generateArgsForInsertText(argsNames)}`\n\n    result[command] = {\n      label: command,\n      kind: monacoEditor.languages.CompletionItemKind.Function,\n      detail,\n      insertText,\n      documentation: {\n        value: getCommandMarkdown(\n          commandsSpec[command],\n          getDocUrlForCommand(command),\n        ),\n      },\n      insertTextRules:\n        monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n      range,\n    }\n  })\n\n  return result\n}\n\nexport const getRedisCompletionProvider = (\n  commandsSpec: ICommands,\n): monacoEditor.languages.CompletionItemProvider => {\n  // generate completion item for each redis command\n  const dependencyProposals = createDependencyProposals(commandsSpec)\n  const commandsArr = Object.keys(commandsSpec).sort()\n\n  return {\n    provideCompletionItems: (\n      model: monacoEditor.editor.IModel,\n      position: monacoEditor.Position,\n    ): monacoEditor.languages.CompletionList => {\n      const word = model.getWordUntilPosition(position)\n      const line = model.getLineContent(position.lineNumber)\n      const indexOfSpace = line.indexOf(' ')\n\n      const range = {\n        startLineNumber: position.lineNumber,\n        endLineNumber: position.lineNumber,\n        endColumn: word.endColumn,\n        startColumn:\n          word.startColumn > indexOfSpace &&\n          !isNaN(+line.slice(0, indexOfSpace))\n            ? indexOfSpace + 2\n            : 1,\n      }\n\n      // display suggestions only for words that don't belong to a folding area\n      if (!model.getValueInRange(range).startsWith(' ')) {\n        return {\n          suggestions: commandsArr.map((command: string) => ({\n            ...dependencyProposals[command],\n            range,\n          })),\n        }\n      }\n      return { suggestions: [] }\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { remove } from 'lodash'\nimport { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants'\nimport { IRedisCommand } from 'uiSrc/constants'\nimport { sanitizeToken } from 'uiSrc/utils'\n\nconst STRING_DOUBLE = 'string.double'\n\nexport const getRedisMonarchTokensProvider = (\n  commands: IRedisCommand[],\n): monacoEditor.languages.IMonarchLanguage => {\n  const commandRedisCommands = [...commands]\n  const searchCommands = remove(commandRedisCommands, ({ token }) =>\n    token?.startsWith(ModuleCommandPrefix.RediSearch),\n  )\n  const COMMON_COMMANDS_REGEX = `^\\\\s*(\\\\d+\\\\s+)?(${commandRedisCommands.map(({ token }) => sanitizeToken(token)).join('|')})\\\\b`\n  const SEARCH_COMMANDS_REGEX = `^\\\\s*(\\\\d+\\\\s+)?(${searchCommands.map(({ token }) => sanitizeToken(token)).join('|')})\\\\b`\n\n  return {\n    defaultToken: '',\n    tokenPostfix: '.redis',\n    ignoreCase: true,\n    includeLF: true,\n    brackets: [\n      { open: '[', close: ']', token: 'delimiter.square' },\n      { open: '(', close: ')', token: 'delimiter.parenthesis' },\n    ],\n    keywords: [],\n    operators: [],\n    tokenizer: {\n      root: [\n        { include: '@startOfLine' },\n        { include: '@whitespace' },\n        { include: '@strings' },\n        { include: '@keyword' },\n        [/[;,.]/, 'delimiter'],\n        [/[()]/, '@brackets'],\n        [\n          /[\\w@#$]+/,\n          {\n            cases: {\n              '@keywords': 'keyword',\n              '@operators': 'operator',\n              '@default': 'identifier',\n            },\n          },\n        ],\n        [/[<>=!%&+\\-*/|~^]/, 'operator'],\n        { include: '@numbers' },\n      ],\n      keyword: [\n        [COMMON_COMMANDS_REGEX, { token: 'keyword' }],\n        [\n          SEARCH_COMMANDS_REGEX,\n          {\n            token: '@rematch',\n            nextEmbedded: 'redisearch',\n            next: '@endRedisearch',\n          },\n        ],\n      ],\n      whitespace: [\n        [/\\s+/, 'white'],\n        [/\\/\\/.*/, 'comment'],\n      ],\n      numbers: [\n        [/0[xX][0-9a-fA-F]*/, 'number'],\n        [/[$][+-]*\\d*(\\.\\d*)?/, 'number'],\n        [/((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][-+]?\\d+)?/, 'number'],\n      ],\n      strings: [\n        [/'/, { token: 'string', next: '@string' }],\n        [/\"/, { token: STRING_DOUBLE, next: '@stringDouble' }],\n      ],\n      string: [\n        [/\\\\./, 'string'],\n        [/'/, { token: 'string', next: '@pop' }],\n        [/[^\\\\']+/, 'string'],\n      ],\n      stringDouble: [\n        [/\\\\./, STRING_DOUBLE],\n        [/\"/, { token: STRING_DOUBLE, next: '@pop' }],\n        [/[^\\\\\"]+/, STRING_DOUBLE],\n      ],\n      // TODO: can be tokens or functions the same - need to think how to avoid wrong ending\n      endRedisearch: [\n        [\n          `^\\\\s*${COMMON_COMMANDS_REGEX}`,\n          {\n            token: '@rematch',\n            next: '@root',\n            nextEmbedded: '@pop',\n            log: 'end',\n          },\n        ],\n      ],\n      startOfLine: [[/\\n/, { next: '@root', token: '@pop' }]],\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoRedisSignatureHelpProvider.ts",
    "content": "import { MutableRefObject } from 'react'\nimport { monaco as monacoEditor } from 'react-monaco-editor'\nimport { isNull } from 'lodash'\nimport { ICommands } from 'uiSrc/constants'\nimport { findCommandEarlier } from 'uiSrc/utils'\nimport { generateArgsNames } from 'uiSrc/utils/commands'\n\nexport const getRedisSignatureHelpProvider = (\n  commandsSpec: ICommands,\n  commandsArray: string[],\n  isBlockedRef?: MutableRefObject<boolean>,\n): monacoEditor.languages.SignatureHelpProvider =>\n  // generate signature help provider\n  ({\n    // signatureHelpTriggerCharacters: [' '],\n    // signatureHelpRetriggerCharacters: [' '],\n    provideSignatureHelp: (\n      model: monacoEditor.editor.IModel,\n      position: monacoEditor.Position,\n    ) => {\n      const command = findCommandEarlier(\n        model,\n        position,\n        commandsSpec,\n        commandsArray,\n      )\n\n      if (isNull(command) || isBlockedRef?.current) {\n        return null\n      }\n\n      const commandArgs = command.info?.arguments ?? []\n      const label: string = `${command?.name} ${generateArgsNames(command.info?.provider, commandArgs).join(' ')}`\n\n      return {\n        dispose: () => {},\n        value: {\n          activeParameter: 0,\n          activeSignature: 0,\n          signatures: [\n            {\n              label,\n              parameters: [{ label: `${command?.name}` }],\n            },\n          ],\n        },\n      }\n    },\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monacoUtils.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport {\n  first,\n  isEmpty,\n  isNaN,\n  isUndefined,\n  reject,\n  toNumber,\n  without,\n} from 'lodash'\nimport { decode } from 'html-entities'\nimport { ICommand, ICommands } from 'uiSrc/constants'\nimport {\n  generateArgsForInsertText,\n  generateArgsNames,\n  getCommandMarkdown,\n  IMonacoCommand,\n  IMonacoQuery,\n} from 'uiSrc/utils'\nimport { TJMESPathFunctions } from 'uiSrc/slices/interfaces'\nimport { Nullable } from '../types'\nimport { getCommandRepeat, isRepeatCountCorrect } from '../commands'\n\nconst COMMENT_SYMBOLS = '//'\nconst BLANK_LINE_REGEX = /^\\s*\\n/gm\nconst QUOTES = [\"'\", '\"', '`']\nconst COMMENT_LINE_REGEX = /^\\s+\\/\\/.*/\n\nconst removeCommentsFromLine = (\n  text: string = '',\n  prefix: string = '',\n): string => {\n  const [command, ...rest] = text.split(COMMENT_SYMBOLS)\n  const isOddQuotes = QUOTES.some(\n    (quote: string) => ((prefix + command).split(quote).length - 1) % 2 !== 0,\n  )\n\n  if (isOddQuotes && command && rest.length) {\n    return removeCommentsFromLine(\n      rest.join(COMMENT_SYMBOLS),\n      prefix + command + COMMENT_SYMBOLS,\n    )\n  }\n\n  return prefix + text.replace(/\\/\\/.*/, '')\n}\n\nexport const splitMonacoValuePerLines = (command = '') => {\n  const linesResult: string[] = []\n  const lines = getMonacoLines(command)\n  // remove execute params\n  if (isParamsLine(first(lines))) {\n    lines.splice(0, 1, removeParams(first(lines)))\n  }\n\n  lines.forEach((line) => {\n    const [commandLine, countRepeat] = getCommandRepeat(line || '')\n\n    if (!isRepeatCountCorrect(countRepeat)) {\n      linesResult.push(line)\n      return\n    }\n    linesResult.push(...Array(countRepeat).fill(commandLine))\n  })\n\n  return linesResult\n}\n\nexport const getMultiCommands = (commands: string[] = []) =>\n  reject(commands, isEmpty).join('\\n') ?? ''\n\nexport const removeMonacoComments = (text: string = '') =>\n  text\n    .split('\\n')\n    .filter((line: string) => !COMMENT_LINE_REGEX.test(line))\n    .map((line: string) => removeCommentsFromLine(line))\n    .join('\\n')\n    .trim()\n\nexport const getCommandsForExecution = (query = '') =>\n  without(\n    splitMonacoValuePerLines(query).map((command) =>\n      removeMonacoComments(decode(command).trim()),\n    ),\n    '',\n  )\n\nexport const multilineCommandToOneLine = (text: string = '') =>\n  text\n    .split(/(\\r\\n|\\n|\\r)+\\s+/gm)\n    .filter((line: string) => !(BLANK_LINE_REGEX.test(line) || isEmpty(line)))\n    .join(' ')\n\nexport const findCommandEarlier = (\n  model: monacoEditor.editor.ITextModel,\n  position: monacoEditor.Position,\n  commandsSpec: ICommands = {},\n  commandsArray: string[] = [],\n): Nullable<IMonacoCommand> => {\n  const { lineNumber } = position\n  let commandName = ''\n  const notCommandRegEx = /^\\s|\\/\\//\n\n  // find command in the previous lines if current line is argument\n  // eslint-disable-next-line for-direction\n  for (\n    let previousLineNumber = lineNumber;\n    previousLineNumber > 0;\n    previousLineNumber--\n  ) {\n    commandName = model.getLineContent(previousLineNumber)?.toUpperCase() ?? ''\n\n    if (!notCommandRegEx.test(commandName)) {\n      break\n    }\n  }\n\n  const matchedCommand = commandsArray.find((command) =>\n    commandName?.trim().startsWith(command),\n  )\n\n  if (isUndefined(matchedCommand)) {\n    return null\n  }\n\n  return {\n    position,\n    name: matchedCommand,\n    info: commandsSpec[matchedCommand],\n  }\n}\n\nexport const isCompositeArgument = (\n  arg: string,\n  prevArg?: string,\n  args: string[] = [],\n) => args.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' '))\n\nexport const splitQueryByArgs = (\n  query: string,\n  position: number = 0,\n  compositeArgs: string[] = [],\n) => {\n  const args: [string[], string[]] = [[], []]\n  let arg = ''\n  let inQuotes = false\n  let escapeNextChar = false\n  let quoteChar = ''\n  let isCursorInQuotes = false\n  let lastArg = ''\n  let argLeftOffset = 0\n  let argRightOffset = 0\n\n  const pushToProperTuple = (isAfterOffset: boolean, arg: string) => {\n    lastArg = arg\n    isAfterOffset ? args[1].push(arg) : args[0].push(arg)\n  }\n\n  const updateLastArgument = (isAfterOffset: boolean, arg: string) => {\n    const argsBySide = args[isAfterOffset ? 1 : 0]\n    argsBySide[argsBySide.length - 1] =\n      `${argsBySide[argsBySide.length - 1]} ${arg}`\n  }\n\n  const updateArgOffsets = (left: number, right: number) => {\n    argLeftOffset = left\n    argRightOffset = right\n  }\n\n  for (let i = 0; i < query.length; i++) {\n    const char = query[i]\n    const isAfterOffset = i >= position + (inQuotes ? -1 : 0)\n\n    if (escapeNextChar) {\n      arg += char\n      escapeNextChar = !quoteChar\n    } else if (char === '\\\\') {\n      escapeNextChar = true\n    } else if (inQuotes) {\n      if (char === quoteChar) {\n        inQuotes = false\n        const argWithChar = arg + char\n\n        if (isAfterOffset && !argLeftOffset) {\n          updateArgOffsets(i - arg.length, i + 1)\n        }\n\n        if (isCompositeArgument(argWithChar, lastArg, compositeArgs)) {\n          updateLastArgument(isAfterOffset, argWithChar)\n        } else {\n          pushToProperTuple(isAfterOffset, argWithChar)\n        }\n\n        arg = ''\n      } else {\n        arg += char\n      }\n    } else if (char === '\"' || char === \"'\") {\n      inQuotes = true\n      quoteChar = char\n      arg += char\n    } else if (char === ' ' || char === '\\n') {\n      if (arg.length > 0) {\n        if (isAfterOffset && !argLeftOffset) {\n          updateArgOffsets(i - arg.length, i)\n        }\n\n        if (isCompositeArgument(arg, lastArg, compositeArgs)) {\n          updateLastArgument(isAfterOffset, arg)\n        } else {\n          pushToProperTuple(isAfterOffset, arg)\n        }\n\n        arg = ''\n      }\n    } else {\n      arg += char\n    }\n\n    if (i === position - 1) isCursorInQuotes = inQuotes\n  }\n\n  if (arg.length > 0) {\n    if (!argLeftOffset)\n      updateArgOffsets(query.length - arg.length, query.length)\n    pushToProperTuple(true, arg)\n  }\n\n  const cursor = {\n    isCursorInQuotes,\n    prevCursorChar: query[position - 1]?.trim() || '',\n    nextCursorChar: query[position]?.trim() || '',\n    argLeftOffset,\n    argRightOffset,\n  }\n\n  return { args, cursor }\n}\n\nexport const findCompleteQuery = (\n  model: monacoEditor.editor.ITextModel,\n  position: monacoEditor.Position,\n  commandsSpec: ICommands = {},\n  commandsArray: string[] = [],\n  compositeArgs: string[] = [],\n): Nullable<IMonacoQuery> => {\n  const { lineNumber } = position\n  let commandName = ''\n  let fullQuery = ''\n  const notCommandRegEx = /^\\s|\\/\\//\n  const commandPosition = {\n    startLine: 0,\n    endLine: 0,\n  }\n\n  // find command and args in the previous lines if current line is argument\n  // eslint-disable-next-line for-direction\n  for (\n    let previousLineNumber = lineNumber;\n    previousLineNumber > 0;\n    previousLineNumber--\n  ) {\n    commandName = model.getLineContent(previousLineNumber) ?? ''\n    const lineBeforePosition =\n      previousLineNumber === lineNumber\n        ? commandName.slice(0, position.column - 1)\n        : commandName\n    fullQuery = lineBeforePosition + fullQuery\n    commandPosition.startLine = previousLineNumber\n\n    if (!notCommandRegEx.test(commandName)) {\n      break\n    }\n\n    fullQuery = `\\n${fullQuery}`\n  }\n\n  const commandCursorPosition = fullQuery.length\n  // find args in the next lines\n  const linesCount = model.getLineCount()\n  for (\n    let nextLineNumber = lineNumber;\n    nextLineNumber <= linesCount;\n    nextLineNumber++\n  ) {\n    const lineContent = model.getLineContent(nextLineNumber) ?? ''\n\n    if (nextLineNumber !== lineNumber && !notCommandRegEx.test(lineContent)) {\n      break\n    }\n\n    commandPosition.endLine = nextLineNumber\n    const lineAfterPosition =\n      nextLineNumber === lineNumber\n        ? lineContent.slice(\n            position.column - 1,\n            model.getLineLength(lineNumber),\n          )\n        : lineContent\n\n    if (nextLineNumber !== lineNumber) {\n      fullQuery += '\\n'\n    }\n\n    fullQuery += lineAfterPosition\n  }\n\n  const { args, cursor } = splitQueryByArgs(\n    fullQuery,\n    commandCursorPosition,\n    compositeArgs,\n  )\n\n  const [beforeCursorArgs] = args\n  const commandNameFromQuery = isNaN(toNumber(beforeCursorArgs[0]))\n    ? beforeCursorArgs[0]\n    : beforeCursorArgs[1]\n  const matchedCommand = commandsArray.find(\n    (command) => commandNameFromQuery?.toUpperCase() === command.toUpperCase(),\n  )\n\n  const cursorContext = {\n    position,\n    fullQuery,\n    commandQuery: fullQuery.replace(/^\\d+\\s+/, ''),\n    args,\n    allArgs: args.flat(),\n    cursor,\n  }\n\n  if (isUndefined(matchedCommand)) {\n    return cursorContext as IMonacoQuery\n  }\n\n  return {\n    ...cursorContext,\n    commandPosition,\n    commandCursorPosition,\n    name: matchedCommand,\n    info: commandsSpec[matchedCommand],\n  } as IMonacoQuery\n}\n\nexport const findArgIndexByCursor = (\n  args: string[] = [],\n  fullQuery: string,\n  cursorPosition: number,\n): Nullable<number> => {\n  let argIndex = null\n  for (let i = 0; i < args.length; i++) {\n    const part = args[i]\n    const searchIndex = fullQuery?.indexOf(part) || 0\n    if (\n      searchIndex < cursorPosition &&\n      searchIndex + part.length > cursorPosition\n    ) {\n      argIndex = i\n      break\n    }\n  }\n  return argIndex\n}\n\nexport const createSyntaxWidget = (text: string, shortcutText: string) => {\n  const widget = document.createElement('div')\n  const title = document.createElement('span')\n  title.classList.add('monaco-widget__title')\n  title.innerHTML = text\n\n  const shortcut = document.createElement('span')\n  shortcut.classList.add('monaco-widget__shortcut')\n  widget.setAttribute('data-testid', 'monaco-widget')\n  shortcut.innerHTML = shortcutText\n\n  widget.append(title, shortcut)\n  widget.classList.add('monaco-widget')\n\n  return widget\n}\n\nexport const isParamsLine = (commandInit: string = '') => {\n  const command = commandInit.trim()\n  return command.startsWith('[') && command.indexOf(']') !== -1\n}\n\nconst removeParams = (commandInit: string = '') => {\n  const command = commandInit.trim()\n  const paramsLastIndex = command.indexOf(']')\n  return command.substring(paramsLastIndex + 1).trim()\n}\n\nexport const getMonacoLines = (command: string = '') =>\n  command.split(/\\n(?=[^\\s])/g)\n\nexport const getCommandsFromQuery = (\n  query: string,\n  commandsArray: string[] = [],\n) => {\n  const commands = getCommandsForExecution(query)\n  const [commandLine, ...rest] = commands.map((command = '') => {\n    const matchedCommand = commandsArray?.find((commandName) =>\n      command.toUpperCase().startsWith(commandName),\n    )\n    return matchedCommand ?? command.split(' ')?.[0]\n  })\n\n  const multiCommands = getMultiCommands(rest).replaceAll('\\n', ';')\n  const listOfCommands = [commandLine, multiCommands].filter(Boolean)\n  return listOfCommands.length ? listOfCommands.join(';') : null\n}\n\n/**\n * Force a cursor position update by moving to (1,1) and back.\n * This is a stateless utility — it only needs the editor instance.\n */\nexport const triggerUpdateCursorPosition = (\n  editor: monacoEditor.editor.IStandaloneCodeEditor,\n) => {\n  const position = editor.getPosition()\n  editor.trigger('mouse', '_moveTo', {\n    position: { lineNumber: 1, column: 1 },\n  })\n  editor.trigger('mouse', '_moveTo', { position })\n  editor.focus()\n}\n\nexport const parseJMESPathFunctions = (functions: TJMESPathFunctions) =>\n  Object.entries(functions).map(([label, func]) => {\n    const { arguments: args } = func\n    const range = {\n      startLineNumber: 0,\n      endLineNumber: 0,\n      startColumn: 0,\n      endColumn: 0,\n    }\n    const detail = `${label}(${generateArgsNames('', args).join(', ')})`\n    const argsNames = generateArgsNames('', args, false, true)\n    const insertText = `${label}(${generateArgsForInsertText(argsNames, ', ')})`\n\n    return {\n      label,\n      detail,\n      range,\n      documentation: {\n        value: getCommandMarkdown(func as ICommand),\n      },\n      insertText,\n      kind: monacoEditor.languages.CompletionItemKind.Function,\n      insertTextRules:\n        monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n    }\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monarchTokens/cypherTokens.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport {\n  FUNCTIONS,\n  KEYWORDS,\n  OPERATORS,\n} from 'uiSrc/constants/monaco/cypher/monacoCypher'\n\nconst STRING_DOUBLE = 'string.double'\nconst functions = FUNCTIONS.map((f) => f.label)\n\nexport const getCypherMonarchTokensProvider =\n  (): monacoEditor.languages.IMonarchLanguage => ({\n    defaultToken: '',\n    tokenPostfix: '.cypher',\n    ignoreCase: true,\n    brackets: [\n      { open: '[', close: ']', token: 'delimiter.square' },\n      { open: '(', close: ')', token: 'delimiter.parenthesis' },\n      { open: '{', close: '}', token: 'delimiter.curly' },\n    ],\n    keywords: KEYWORDS,\n    operators: OPERATORS,\n    functions,\n    strings: [],\n    tokenizer: {\n      root: [\n        { include: '@whitespace' },\n        { include: '@numbers' },\n        { include: '@keyword' },\n        { include: '@function' },\n        [/[;,.]/, 'delimiter'],\n        // eslint-disable-next-line\n        [/[{}()\\[\\]]/, '@brackets'],\n        [\n          /[\\w@#$]+/,\n          {\n            cases: {\n              '@keywords': 'keyword',\n              '@functions': 'function',\n              '@default': 'identifier',\n            },\n          },\n        ],\n        { include: '@strings' },\n      ],\n      keyword: [[`\\\\b(${KEYWORDS.join('|')})\\\\b`, 'keyword']],\n      function: [[`\\\\b(${functions.join('|')})(?=\\\\s*\\\\()`, 'function']],\n      strings: [\n        [/'/, { token: 'string', next: '@string' }],\n        [/\"/, { token: STRING_DOUBLE, next: '@stringDouble' }],\n      ],\n      string: [\n        [/[^']+/, 'string'],\n        [/''/, 'string'],\n        [/'/, { token: 'string', next: '@pop' }],\n      ],\n      stringDouble: [\n        [/[^\"]+/, STRING_DOUBLE],\n        [/\"\"/, STRING_DOUBLE],\n        [/\"/, { token: STRING_DOUBLE, next: '@pop' }],\n      ],\n      whitespace: [\n        [/\\s+/, 'white'],\n        [/\\/\\/.*$/, 'comment'],\n      ],\n      numbers: [\n        [/0[xX][0-9a-fA-F]*/, 'number'],\n        [/[$][+-]*\\d*(\\.\\d*)?/, 'number'],\n        [/((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][-+]?\\d+)?/, 'number'],\n      ],\n    },\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monarchTokens/jmespathTokens.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nconst STRING_DOUBLE = 'string.double'\n\nexport const getJmespathMonarchTokensProvider = (\n  functions: string[],\n): monacoEditor.languages.IMonarchLanguage => ({\n  defaultToken: '',\n  tokenPostfix: '.jmespath',\n  ignoreCase: true,\n  brackets: [\n    { open: '[', close: ']', token: 'delimiter.square' },\n    { open: '(', close: ')', token: 'delimiter.parenthesis' },\n  ],\n  functions,\n  operators: [\n    // NOT SUPPORTED\n  ],\n  builtinFunctions: [\n    // NOT SUPPORTED\n  ],\n  builtinVariables: [\n    // NOT SUPPORTED\n  ],\n  pseudoColumns: [\n    // NOT SUPPORTED\n  ],\n  tokenizer: {\n    root: [\n      { include: '@whitespace' },\n      { include: '@pseudoColumns' },\n      { include: '@numbers' },\n      { include: '@strings' },\n      { include: '@scopes' },\n      { include: '@keyword' },\n      [/[;,.]/, 'delimiter'],\n      [/[()]/, '@brackets'],\n      [\n        /[\\w@#$]+/,\n        {\n          cases: {\n            '@functions': 'keyword',\n            '@operators': 'operator',\n            '@builtinVariables': 'predefined',\n            '@builtinFunctions': 'predefined',\n            '@default': 'identifier',\n          },\n        },\n      ],\n      [/[<>=!%&+\\-*/|~^]/, 'operator'],\n    ],\n    keyword: [[`\\\\b(${functions.join('|')})(?=\\\\s*\\\\()`, 'keyword']],\n    whitespace: [\n      [/\\s+/, 'white'],\n      [/\\/\\/.*$/, 'comment'],\n    ],\n    pseudoColumns: [\n      [\n        /[$][A-Za-z_][\\w@#$]*/,\n        {\n          cases: {\n            '@pseudoColumns': 'predefined',\n            '@default': 'identifier',\n          },\n        },\n      ],\n    ],\n    numbers: [\n      [/0[xX][0-9a-fA-F]*/, 'number'],\n      [/[$][+-]*\\d*(\\.\\d*)?/, 'number'],\n      [/((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][-+]?\\d+)?/, 'number'],\n    ],\n    strings: [\n      [/'/, { token: 'string', next: '@string' }],\n      [/\"/, { token: STRING_DOUBLE, next: '@stringDouble' }],\n    ],\n    string: [\n      [/[^']+/, 'string'],\n      [/''/, 'string'],\n      [/'/, { token: 'string', next: '@pop' }],\n    ],\n    stringDouble: [\n      [/[^\"]+/, STRING_DOUBLE],\n      [/\"\"/, STRING_DOUBLE],\n      [/\"/, { token: STRING_DOUBLE, next: '@pop' }],\n    ],\n    scopes: [\n      // NOT SUPPORTED\n    ],\n  },\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\nimport { remove } from 'lodash'\nimport { IRedisCommandTree } from 'uiSrc/constants'\nimport {\n  generateKeywords,\n  generateTokens,\n  generateTokensWithFunctions,\n  getBlockTokens,\n  isIndexAfterKeyword,\n  isQueryAfterIndex,\n} from 'uiSrc/utils/monaco/redisearch/utils'\nimport { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates'\n\nconst STRING_DOUBLE = 'string.double'\n\nexport const getRediSearchSubRedisMonarchTokensProvider = (\n  commands: IRedisCommandTree[],\n): monacoEditor.languages.IMonarchLanguage => {\n  const withoutIndexSuggestions = [...commands]\n  const withNextIndexSuggestions = remove(\n    withoutIndexSuggestions,\n    isIndexAfterKeyword,\n  )\n  const withNextQueryIndexSuggestions = remove(\n    [...withNextIndexSuggestions],\n    isQueryAfterIndex,\n  )\n\n  const generateTokensForCommands = () => {\n    let commandTokens: any = {}\n\n    commands.forEach((command) => {\n      const isIndexAfterCommand = isIndexAfterKeyword(command)\n      const argTokens = generateTokens(command)\n      const tokenName = command.token?.replace(/(\\.| )/g, '_')\n      const blockTokens = getBlockTokens(tokenName, argTokens?.pureTokens)\n\n      if (blockTokens.length) {\n        commandTokens[`argument.block.${tokenName}`] = blockTokens\n      }\n\n      if (isIndexAfterCommand) {\n        commandTokens = {\n          ...commandTokens,\n          ...generateTokensWithFunctions(\n            tokenName,\n            argTokens?.tokensWithQueryAfter,\n          ),\n        }\n      }\n    })\n\n    return commandTokens\n  }\n\n  const tokens = generateTokensForCommands()\n\n  const includeTokens = () => {\n    const tokensToInclude = Object.keys(tokens).filter((name) =>\n      name.startsWith('argument.block'),\n    )\n    return tokensToInclude.map((include) => ({ include: `@${include}` }))\n  }\n\n  return {\n    defaultToken: '',\n    tokenPostfix: '.redisearch',\n    includeLF: true,\n    ignoreCase: true,\n    brackets: [\n      { open: '[', close: ']', token: 'delimiter.square' },\n      { open: '(', close: ')', token: 'delimiter.parenthesis' },\n    ],\n    keywords: [],\n    tokenizer: {\n      root: [\n        { include: '@startOfLine' },\n        { include: '@keywords' },\n        ...includeTokens(),\n        { include: '@fields' },\n        { include: '@whitespace' },\n        { include: '@numbers' },\n        { include: '@strings' },\n        [/[;,.]/, 'delimiter'],\n        [/[()]/, '@brackets'],\n        [/[<>=!%&+\\-*/|~^]/, 'operator'],\n        [/[\\w@#$.]+/, 'identifier'],\n      ],\n      keywords: [\n        [\n          `^\\\\s*(\\\\d\\\\s)?(${generateKeywords(withNextQueryIndexSuggestions).join('|')})\\\\b`,\n          { token: 'keyword', next: '@index.query' },\n        ],\n        [\n          `^\\\\s*(\\\\d\\\\s)?(${generateKeywords(withNextIndexSuggestions).join('|')})\\\\b`,\n          { token: 'keyword', next: '@index' },\n        ],\n        [\n          `^\\\\s*(\\\\d\\\\s)?(${generateKeywords(withoutIndexSuggestions).join('|')})\\\\b`,\n          { token: 'keyword', next: '@root' },\n        ],\n      ],\n      ...tokens,\n      ...generateQuery(),\n      index: [\n        [/\"([^\"\\\\]|\\\\.)*\"/, { token: 'index', next: '@root' }],\n        [/'([^'\\\\]|\\\\.)*'/, { token: 'index', next: '@root' }],\n        [/[\\w:]+/, { token: 'index', next: '@root' }],\n        { include: 'root' }, // Fallback to the root state if nothing matches\n      ],\n      'index.query': [\n        [/\"([^\"\\\\]|\\\\.)*\"/, { token: 'index', next: '@query' }],\n        [/'([^'\\\\]|\\\\.)*'/, { token: 'index', next: '@query' }],\n        [/[\\w:]+/, { token: 'index', next: '@query' }],\n        { include: 'root' }, // Fallback to the root state if nothing matches\n      ],\n      fields: [[/@\\w+/, { token: 'field' }]],\n      whitespace: [\n        [/\\s+/, 'white'],\n        [/\\/\\/.*/, 'comment'],\n      ],\n      numbers: [\n        [/0[xX][0-9a-fA-F]*/, 'number'],\n        [/[$][+-]*\\d*(\\.\\d*)?/, 'number'],\n        [/((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][-+]?\\d+)?/, 'number'],\n      ],\n      strings: [\n        [/'/, { token: 'string', next: '@string' }],\n        [/\"/, { token: STRING_DOUBLE, next: '@stringDouble' }],\n      ],\n      string: [\n        [/\\\\./, 'string'],\n        [/'/, { token: 'string', next: '@pop' }],\n        [/[^\\\\']+/, 'string'],\n      ],\n      stringDouble: [\n        [/\\\\./, STRING_DOUBLE],\n        [/\"/, { token: STRING_DOUBLE, next: '@pop' }],\n        [/[^\\\\\"]+/, STRING_DOUBLE],\n      ],\n      startOfLine: [[/\\s?\\n/, { next: '@root', token: '@pop' }]],\n    },\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts",
    "content": "import { languages } from 'monaco-editor'\nimport { curryRight } from 'lodash'\nimport { Maybe, sanitizeToken } from 'uiSrc/utils'\nimport { IRedisCommand } from 'uiSrc/constants'\n\nconst appendToken = (token: string, name: Maybe<string>) =>\n  name ? `${token}.${name}` : token\nexport const generateQuery = (\n  argToken?: IRedisCommand,\n  args?: IRedisCommand[],\n): { [name: string]: languages.IMonarchLanguageRule[] } => {\n  const curriedAppendToken = curryRight(appendToken)\n  const appendTokenName = curriedAppendToken(argToken?.token)\n\n  const getFunctionsTokens = (\n    tokenName: string,\n  ): languages.IMonarchLanguageRule =>\n    args?.length\n      ? [\n          `(${args?.map(({ token }) => sanitizeToken(token)).join('|')})\\\\b`,\n          { token: 'function', next: appendTokenName(tokenName) },\n        ]\n      : [/_/, '']\n\n  return {\n    [appendTokenName('query')]: [\n      [\n        /\"/,\n        {\n          token: appendTokenName('query'),\n          next: appendTokenName('@query.inside.double'),\n        },\n      ],\n      [\n        /'/,\n        {\n          token: appendTokenName('query'),\n          next: appendTokenName('@query.inside.single'),\n        },\n      ],\n      [/[a-zA-Z_]\\w*/, { token: appendTokenName('query'), next: '@root' }],\n      { include: 'root' }, // Fallback to the root state if nothing matches\n    ],\n    [appendTokenName('query.inside.double')]: [\n      [/@/, { token: 'field', next: appendTokenName('@field.inside.double') }],\n      [\n        /\\\\\"/,\n        { token: 'query', next: appendTokenName('@query.inside.double') },\n      ],\n      [/==|!=|<=|>=|<|>/, { token: 'query.operator' }],\n      [/&&|\\|\\|/, { token: 'query.operator' }],\n      [/[()]/, 'delimiter.parenthesis'],\n      getFunctionsTokens('@function.inside.double'),\n      [/\"/, { token: appendTokenName('query'), next: '@root' }],\n      [\n        /./,\n        {\n          token: appendTokenName('query'),\n          next: appendTokenName('@query.inside.double'),\n        },\n      ],\n      { include: '@query' }, // Fallback to the root state if nothing matches\n    ],\n    [appendTokenName('query.inside.single')]: [\n      [/@/, { token: 'field', next: appendTokenName('@field.inside.single') }],\n      [\n        /\\\\'/,\n        {\n          token: appendTokenName('query'),\n          next: appendTokenName('query.inside.single'),\n        },\n      ],\n      [/==|!=|<=|>=|<|>/, { token: 'query.operator' }],\n      [/&&|\\|\\|/, { token: 'query.operator' }],\n      [/[()]/, 'delimiter.parenthesis'],\n      getFunctionsTokens('@function.inside.single'),\n      [/'/, { token: appendTokenName('query'), next: '@root' }],\n      [\n        /./,\n        {\n          token: appendTokenName('query'),\n          next: appendTokenName('@query.inside.single'),\n        },\n      ],\n      { include: appendTokenName('@query') }, // Fallback to the root state if nothing matches\n    ],\n    [appendTokenName('field.inside.double')]: [\n      [\n        /\\w+/,\n        { token: 'field', next: appendTokenName('@query.inside.double') },\n      ],\n      [\n        /\\s+/,\n        { token: '@rematch', next: appendTokenName('@query.inside.double') },\n      ],\n      [/\"/, { token: appendTokenName('query'), next: '@root' }],\n      { include: appendTokenName('@query') }, // Fallback to the root state if nothing matches\n    ],\n    [appendTokenName('field.inside.single')]: [\n      [\n        /\\w+/,\n        { token: 'field', next: appendTokenName('@query.inside.single') },\n      ],\n      [\n        /\\s+/,\n        { token: '@rematch', next: appendTokenName('@query.inside.single') },\n      ],\n      [/'/, { token: appendTokenName('query'), next: '@root' }],\n\n      { include: appendTokenName('@query') },\n    ],\n    [appendTokenName('function.inside.double')]: [\n      [/\\s+/, 'white'], // Handle whitespace\n      [\n        /\\(/,\n        {\n          token: 'delimiter.parenthesis',\n          next: appendTokenName('@function.args.double'),\n        },\n      ],\n      { include: appendTokenName('@query') },\n    ],\n    [appendTokenName('function.args.double')]: [\n      [\n        /\\)/,\n        {\n          token: 'delimiter.parenthesis',\n          next: appendTokenName('@query.inside.double'),\n        },\n      ],\n      [/,/, 'delimiter.comma'], // Match commas between arguments\n      getFunctionsTokens('@function.inside.double'),\n      [/[a-zA-Z_]\\w*/, { token: 'parameter' }], // Highlight parameters\n      [/\\s+/, 'white'], // Handle whitespace\n      [/@\\w+/, { token: 'field' }],\n\n      // // Handle strings with escaped quotes\n      [/\\\\\"/, 'parameter'], // Match escaped double quote\n      [/\\\\'/, 'parameter'], // Match escaped single quote\n      [/'/, 'parameter'], // Match escaped single quote\n\n      { include: appendTokenName('@query') }, // Fallback to root state\n    ],\n    [appendTokenName('function.inside.single')]: [\n      [/\\s+/, 'white'], // Handle whitespace\n      [\n        /\\(/,\n        {\n          token: 'delimiter.parenthesis',\n          next: appendTokenName('@function.args.single'),\n        },\n      ],\n      { include: appendTokenName('@query') },\n    ],\n    [appendTokenName('function.args.single')]: [\n      [\n        /\\)/,\n        {\n          token: 'delimiter.parenthesis',\n          next: appendTokenName('@query.inside.single'),\n        },\n      ],\n      [/,/, 'delimiter.comma'], // Match commas between arguments\n      getFunctionsTokens('@function.inside.single'),\n      [/[a-zA-Z_]\\w*/, { token: 'parameter' }], // Highlight parameters\n      [/\\s+/, 'white'], // Handle whitespace\n      [/@\\w+/, { token: 'field' }],\n\n      [/\"/, 'parameter'], // Match escaped double quote\n      // // Handle strings with escaped quotes\n      [/\\\\\"/, 'parameter'], // Match escaped double quote\n      [/\\\\'/, 'parameter'], // Match escaped single quote\n\n      { include: appendTokenName('@query') }, // Fallback to root state\n    ],\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/monarchTokens/sqliteFunctionsTokens.ts",
    "content": "import { monaco as monacoEditor } from 'react-monaco-editor'\n\nconst STRING_DOUBLE = 'string.double'\n\nexport const getSqliteFunctionsMonarchTokensProvider = (\n  functions: string[],\n): monacoEditor.languages.IMonarchLanguage => ({\n  defaultToken: '',\n  tokenPostfix: '.jmespath',\n  ignoreCase: true,\n  brackets: [\n    { open: '[', close: ']', token: 'delimiter.square' },\n    { open: '(', close: ')', token: 'delimiter.parenthesis' },\n  ],\n  functions,\n  operators: [\n    // NOT SUPPORTED\n  ],\n  builtinFunctions: [\n    // NOT SUPPORTED\n  ],\n  builtinVariables: [\n    // NOT SUPPORTED\n  ],\n  pseudoColumns: [\n    // NOT SUPPORTED\n  ],\n  tokenizer: {\n    root: [\n      { include: '@whitespace' },\n      { include: '@pseudoColumns' },\n      { include: '@numbers' },\n      { include: '@strings' },\n      { include: '@scopes' },\n      { include: '@keyword' },\n      [/[;,.]/, 'delimiter'],\n      [/[()]/, '@brackets'],\n      [\n        /[\\w@#$]+/,\n        {\n          cases: {\n            '@functions': 'keyword',\n            '@operators': 'operator',\n            '@builtinVariables': 'predefined',\n            '@builtinFunctions': 'predefined',\n            '@default': 'identifier',\n          },\n        },\n      ],\n      [/[<>=!%&+\\-*/|~^]/, 'operator'],\n    ],\n    keyword: [[`\\\\b(${functions.join('|')})(?=\\\\s*\\\\()`, 'keyword']],\n    whitespace: [\n      [/\\s+/, 'white'],\n      [/\\/\\/.*$/, 'comment'],\n    ],\n    pseudoColumns: [\n      [\n        /[$][A-Za-z_][\\w@#$]*/,\n        {\n          cases: {\n            '@pseudoColumns': 'predefined',\n            '@default': 'identifier',\n          },\n        },\n      ],\n    ],\n    numbers: [\n      [/0[xX][0-9a-fA-F]*/, 'number'],\n      [/[$][+-]*\\d*(\\.\\d*)?/, 'number'],\n      [/((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][-+]?\\d+)?/, 'number'],\n    ],\n    strings: [\n      [/'/, { token: 'string', next: '@string' }],\n      [/\"/, { token: STRING_DOUBLE, next: '@stringDouble' }],\n    ],\n    string: [\n      [/[^']+/, 'string'],\n      [/''/, 'string'],\n      [/'/, { token: 'string', next: '@pop' }],\n    ],\n    stringDouble: [\n      [/[^\"]+/, STRING_DOUBLE],\n      [/\"\"/, STRING_DOUBLE],\n      [/\"/, { token: STRING_DOUBLE, next: '@pop' }],\n    ],\n    scopes: [\n      // NOT SUPPORTED\n    ],\n  },\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/redisearch/utils.ts",
    "content": "import { isNumber, remove } from 'lodash'\nimport { languages } from 'monaco-editor'\nimport { Maybe, Nullable, sanitizeToken } from 'uiSrc/utils'\nimport { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates'\nimport { ICommandTokenType, IRedisCommand } from 'uiSrc/constants'\nimport { DefinedArgumentName } from 'uiSrc/pages/workbench/constants'\n\nexport const generateKeywords = (commands: IRedisCommand[]) =>\n  commands.map(({ name }) => name)\nexport const generateTokens = (\n  command?: IRedisCommand,\n): Nullable<{\n  pureTokens: Array<Array<IRedisCommand>>\n  tokensWithQueryAfter: Array<\n    Array<{ token: IRedisCommand; arguments: IRedisCommand[] }>\n  >\n}> => {\n  if (!command) return null\n  const pureTokens: Array<Array<IRedisCommand>> = []\n  const tokensWithQueryAfter: Array<\n    Array<{ token: IRedisCommand; arguments: IRedisCommand[] }>\n  > = []\n\n  function processArguments(args: IRedisCommand[], level = 0) {\n    if (!pureTokens[level]) pureTokens[level] = []\n    if (!tokensWithQueryAfter[level]) tokensWithQueryAfter[level] = []\n\n    args.forEach((arg) => {\n      if (arg.token) pureTokens[level].push(arg)\n\n      if (arg.type === ICommandTokenType.Block && arg.arguments) {\n        const blockToken = arg.arguments[0]\n        const nextArgs = arg.arguments\n        const isArgHasOwnSyntax =\n          arg.arguments[0].expression && !!arg.arguments[0].arguments?.length\n\n        if (blockToken?.token) {\n          if (isArgHasOwnSyntax) {\n            tokensWithQueryAfter[level].push({\n              token: blockToken,\n              arguments: arg.arguments[0].arguments as IRedisCommand[],\n            })\n          } else {\n            pureTokens[level].push(blockToken)\n          }\n        }\n\n        processArguments(\n          blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs,\n          level + 1,\n        )\n      }\n\n      if (arg.type === ICommandTokenType.OneOf && arg.arguments) {\n        arg.arguments.forEach((choice) => {\n          if (choice?.token) pureTokens[level].push(choice)\n        })\n      }\n    })\n  }\n\n  if (command.arguments) {\n    processArguments(command.arguments, 0)\n  }\n\n  return { pureTokens, tokensWithQueryAfter }\n}\n\nexport const isIndexAfterKeyword = (command?: IRedisCommand) => {\n  if (!command) return false\n\n  const index = command.arguments?.findIndex(\n    ({ name }) => name === DefinedArgumentName.index,\n  )\n  return isNumber(index) && index === 0\n}\n\nexport const isQueryAfterIndex = (command?: IRedisCommand) => {\n  if (!command) return false\n\n  const index = command.arguments?.findIndex(\n    ({ name }) => name === DefinedArgumentName.index,\n  )\n  return isNumber(index) && index > -1\n    ? command.arguments?.[index + 1]?.name === DefinedArgumentName.query\n    : false\n}\n\nexport const appendTokenWithQuery = (\n  args: Array<{ token: IRedisCommand; arguments: IRedisCommand[] }>,\n  level: number,\n): languages.IMonarchLanguageRule[] =>\n  args.map(({ token }) => [\n    `(${token.token})\\\\b`,\n    { token: `argument.block.${level}`, next: `@query.${token.token}` },\n  ])\n\nexport const appendQueryWithNextFunctions = (\n  tokens: Array<{ token: IRedisCommand; arguments: IRedisCommand[] }>,\n): {\n  [name: string]: languages.IMonarchLanguageRule[]\n} => {\n  let result: { [name: string]: languages.IMonarchLanguageRule[] } = {}\n\n  tokens.forEach(({ token, arguments: args }) => {\n    result = {\n      ...result,\n      ...generateQuery(token, args),\n    }\n  })\n\n  return result\n}\n\nexport const generateTokensWithFunctions = (\n  name: string = '',\n  tokens?: Array<Array<{ token: IRedisCommand; arguments: IRedisCommand[] }>>,\n): {\n  [name: string]: languages.IMonarchLanguageRule[]\n} => {\n  if (!tokens) return {}\n\n  const actualTokens = tokens.filter((tokens) => tokens.length)\n\n  if (!actualTokens.length) return {}\n\n  return {\n    [`argument.block.${name}.withFunctions`]: [\n      ...actualTokens\n        .map((tokens, lvl) => appendTokenWithQuery(tokens, lvl))\n        .flat(),\n    ],\n    ...appendQueryWithNextFunctions(actualTokens.flat()),\n  }\n}\n\nexport const getBlockTokens = (\n  name: string = '',\n  pureTokens: Maybe<Array<IRedisCommand>[]>,\n): languages.IMonarchLanguageRule[] => {\n  if (!pureTokens) return []\n\n  const getLeveledToken = (\n    tokens: IRedisCommand[],\n    lvl: number,\n  ): languages.IMonarchLanguageRule[] => {\n    const result: languages.IMonarchLanguageRule[] = []\n    const restTokens = [...tokens]\n    const tokensWithNextExpression = remove(\n      restTokens,\n      ({ expression }) => expression,\n    )\n\n    if (tokensWithNextExpression.length) {\n      result.push([\n        `(${tokensWithNextExpression.map(({ token }) => sanitizeToken(token)).join('|')})\\\\b`,\n        {\n          token: `argument.block.${lvl}.${name}`,\n          next: '@query',\n        },\n      ])\n    }\n\n    if (restTokens.length) {\n      result.push([\n        `(${restTokens.map(({ token }) => sanitizeToken(token)).join('|')})\\\\b`,\n        { token: `argument.block.${lvl}.${name}`, next: '@root' },\n      ])\n    }\n\n    return result\n  }\n\n  return pureTokens.map((tokens, lvl) => getLeveledToken(tokens, lvl)).flat()\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monaco/regex.ts",
    "content": "// Escape special regex characters in tokens\nexport const sanitizeToken = (token: string = '') =>\n  token.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') || ''\n"
  },
  {
    "path": "redisinsight/ui/src/utils/monitorUtils.ts",
    "content": "import { format } from 'date-fns'\nimport { isFinite } from 'lodash'\n\nexport const getFormatTime = (time: string) =>\n  typeof time === 'string' && isFinite(+time)\n    ? format(new Date(+time * 1_000), 'HH:mm:ss.SSS')\n    : 'Invalid time'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/numbers.ts",
    "content": "import { toNumber, isNull } from 'lodash'\nimport { Nullable } from 'uiSrc/utils'\n\nexport const isNaNConvertedString = (value: string): boolean =>\n  Number.isNaN(toNumber(value))\n\nexport const numberWithSpaces = (number: number = 0) =>\n  number.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ' ')\n\nexport const nullableNumberWithSpaces = (number: Nullable<number> = 0) => {\n  if (isNull(number)) {\n    return '-'\n  }\n  return numberWithSpaces(number)\n}\n\nexport const getPercentage = (\n  value = 0,\n  sum = 1,\n  round = false,\n  decimals = 2,\n) => {\n  const percent = parseFloat(((value / sum) * 100).toFixed(decimals))\n  return round ? Math.round(percent) : percent\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/oauth/cloudSsoUtm.tsx",
    "content": "import { CloudSsoUtmCampaign, OAuthSocialSource } from 'uiSrc/slices/interfaces'\n\n// Map oauth social source to utm campaign parameter\nexport const getCloudSsoUtmCampaign = (\n  source?: string | null,\n): CloudSsoUtmCampaign => {\n  switch (source) {\n    case OAuthSocialSource.ListOfDatabases:\n    case OAuthSocialSource.DatabaseConnectionList:\n      return CloudSsoUtmCampaign.ListOfDatabases\n    case OAuthSocialSource.BrowserSearch:\n      return CloudSsoUtmCampaign.BrowserSearch\n    case OAuthSocialSource.RediSearch:\n    case OAuthSocialSource.RedisJSON:\n    case OAuthSocialSource.RedisTimeSeries:\n    case OAuthSocialSource.RedisGraph:\n    case OAuthSocialSource.RedisBloom:\n      return CloudSsoUtmCampaign.Workbench\n    case OAuthSocialSource.BrowserContentMenu:\n      return CloudSsoUtmCampaign.BrowserOverview\n    case OAuthSocialSource.BrowserFiltering:\n      return CloudSsoUtmCampaign.BrowserFilter\n    case OAuthSocialSource.WelcomeScreen:\n      return CloudSsoUtmCampaign.WelcomeScreen\n    case OAuthSocialSource.Tutorials:\n      return CloudSsoUtmCampaign.Tutorial\n    case OAuthSocialSource.Autodiscovery:\n    case OAuthSocialSource.DiscoveryForm:\n      return CloudSsoUtmCampaign.AutoDiscovery\n    case OAuthSocialSource.AiChat:\n      return CloudSsoUtmCampaign.Copilot\n    case OAuthSocialSource.UserProfile:\n      return CloudSsoUtmCampaign.UserProfile\n    case OAuthSocialSource.SettingsPage:\n      return CloudSsoUtmCampaign.Settings\n    case OAuthSocialSource.NavigationMenu:\n      return CloudSsoUtmCampaign.NavigationMenu\n    case OAuthSocialSource.AddDbForm:\n      return CloudSsoUtmCampaign.AddDbForm\n    default:\n      return CloudSsoUtmCampaign.Unknown\n  }\n}\n\n// Create search query utm parameters\nexport const getCloudSsoUtmParams = (source?: string | null): URLSearchParams =>\n  new URLSearchParams([\n    ['source', 'redisinsight'],\n    ['medium', 'sso'], // todo: distinguish between electron and web?\n    ['campaign', getCloudSsoUtmCampaign(source)],\n  ])\n"
  },
  {
    "path": "redisinsight/ui/src/utils/onboarding.tsx",
    "content": "import React from 'react'\nimport { PopoverAnchorPosition } from '@elastic/eui'\nimport { OnboardingTour } from 'uiSrc/components'\nimport { OnboardingTourOptions } from 'uiSrc/components/onboarding-tour'\nimport { Props as OnboardingTourProps } from 'uiSrc/components/onboarding-tour/OnboardingTourWrapper'\nimport { Maybe } from 'uiSrc/utils/types'\n\ninterface Props extends Omit<OnboardingTourProps, 'children' | 'options'> {\n  options: Maybe<OnboardingTourOptions>\n  anchorPosition?: PopoverAnchorPosition\n}\n\nconst renderOnboardingTourWithChild = (\n  children: React.ReactElement,\n  props: Props,\n  isActive = true,\n  key: string,\n) =>\n  props.options && isActive ? (\n    <OnboardingTour\n      {...props}\n      options={props.options as OnboardingTourOptions}\n      key={key}\n    >\n      {children}\n    </OnboardingTour>\n  ) : (\n    children\n  )\n\nexport { renderOnboardingTourWithChild }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/parseRedisUrl.ts",
    "content": "import { Maybe, Nullable } from 'uiSrc/utils/types'\n\n/*\n  [redis[s]://]             - Optional Protocol (redis or rediss)\n  [username][:password]@    - Optional username and password\n  host                      - Hostname or IP address\n  [:port]                   - Optional port\n  [/db-number]              - Optional database number\n*/\n\ninterface ParsedRedisUrl {\n  protocol: string\n  username: string\n  password: string\n  host: string\n  port: Maybe<number>\n  hostname: Maybe<string>\n  dbNumber: Maybe<number>\n}\n\nconst parseRedisUrl = (urlString: string = ''): Nullable<ParsedRedisUrl> => {\n  const pureUrlPattern = /^([^:]+):(\\d+)$/\n  const pureMatch = urlString.match(pureUrlPattern)\n\n  if (pureMatch) {\n    const [, host, port] = pureMatch\n    return {\n      protocol: 'redis',\n      username: '',\n      password: '',\n      host,\n      port: port ? parseInt(port, 10) : undefined,\n      hostname: port ? `${host}:${port}` : host,\n      dbNumber: undefined,\n    }\n  }\n\n  // eslint-disable-next-line no-useless-escape\n  const redisUrlPattern =\n    /^(redis[s]?):\\/\\/(?:(.+)?@)?(?:.*@)?([^:\\/]+)(?::(\\d+))?(?:\\/(\\d+))?$/\n  const match = urlString.match(redisUrlPattern)\n\n  if (!match) {\n    return null\n  }\n\n  const [, protocol, userInfo, host, port, dbNumber] = match\n  const [, username, password] = userInfo?.match(/^(.*?)(?::(.*))?$/) || []\n\n  return {\n    protocol: protocol || 'redis',\n    username: username || '',\n    password: password || '',\n    host,\n    port: port ? parseInt(port, 10) : undefined,\n    hostname: port ? `${host}:${port}` : host,\n    dbNumber: dbNumber ? parseInt(dbNumber, 10) : undefined,\n  }\n}\n\nexport { parseRedisUrl }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/parseResponse.ts",
    "content": "import { find, map, sortBy, omit, forEach, isNull } from 'lodash'\nimport { ModifiedSentinelMaster } from 'uiSrc/slices/interfaces'\nimport { initialStateSentinelStatus } from 'uiSrc/slices/instances/sentinel'\n\nimport { AddSentinelMasterResponse } from 'apiSrc/modules/instances/dto/redis-sentinel.dto'\nimport { SentinelMaster } from 'apiSrc/modules/redis-sentinel/models/sentinel'\n\nconst DEFAULT_NODE_ID = 'standalone'\n\nexport const parseMastersSentinel = (\n  masters: SentinelMaster[],\n): ModifiedSentinelMaster[] =>\n  map(sortBy(masters, 'name'), (master, i) => ({\n    ...initialStateSentinelStatus,\n    ...master,\n    id: `${i + 1}`,\n    alias: '',\n    username: '',\n    password: '',\n  }))\n\nexport const parseAddedMastersSentinel = (\n  masters: ModifiedSentinelMaster[],\n  statuses: AddSentinelMasterResponse[],\n): ModifiedSentinelMaster[] =>\n  sortBy(masters, 'message').map((master) => ({\n    ...master,\n    ...find(statuses, (status) => master.name === status.name),\n    loading: false,\n  }))\n\nexport const parseKeysListResponse = (prevShards = {}, data = []) => {\n  const shards = { ...prevShards }\n\n  const result = {\n    nextCursor: '0',\n    total: 0,\n    scanned: 0,\n    keys: [],\n    shardsMeta: {},\n  }\n\n  data.forEach((node) => {\n    const id = node.host ? `${node.host}:${node.port}` : DEFAULT_NODE_ID\n    const shard = (() => {\n      if (!shards[id]) {\n        shards[id] = omit(node, 'keys')\n      } else {\n        shards[id] = {\n          ...omit(node, 'keys'),\n          scanned: shards[id].scanned + node.scanned,\n        }\n      }\n      return shards[id]\n    })()\n\n    // summarize shard values\n    if (\n      (shard.scanned > shard.total || shard.cursor === 0) &&\n      !isNull(shard.total)\n    ) {\n      shard.scanned = shard.total\n    }\n\n    // result.keys.push(...node.keys)\n    result.keys = result.keys.concat(node.keys)\n  })\n\n  // summarize result numbers\n  const nextCursor = []\n  forEach(shards, (shard, id) => {\n    if (shard.total === null) {\n      result.total = shard.total\n    } else {\n      // we don't know how many keys we lost in total = null shard\n      result.total = isNull(result.total) ? null : result.total + shard.total\n    }\n    result.scanned += shard.scanned\n\n    // ignore already scanned shards on get more call\n    if (shard.cursor === 0) {\n      return\n    }\n\n    if (id === DEFAULT_NODE_ID) {\n      nextCursor.push(shard.cursor)\n    } else {\n      nextCursor.push(`${id}@${shard.cursor}`)\n    }\n  })\n\n  result.nextCursor = nextCursor.join('||') || '0'\n  result.shardsMeta = shards\n\n  return result\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/pathUtil.ts",
    "content": "import { getOriginUrl } from 'uiSrc/services/resourcesService'\nimport { IS_ABSOLUTE_PATH } from 'uiSrc/constants/regex'\n\nenum TutorialsPaths {\n  CustomTutorials = 'custom-tutorials',\n  Guide = 'guides',\n  Tutorials = 'tutorials',\n}\n\nexport const getRootStaticPath = (mdPath: string) => {\n  const paths = mdPath?.split('/') || []\n  const tutorialFolder = paths[1]\n\n  if (tutorialFolder === TutorialsPaths.CustomTutorials)\n    return paths.slice(0, 3).join('/')\n  if (\n    tutorialFolder === TutorialsPaths.Guide ||\n    tutorialFolder === TutorialsPaths.Tutorials\n  ) {\n    return paths.slice(0, 2).join('/')\n  }\n\n  return mdPath\n}\n\nconst processAbsolutePath = (nodeUrl: string, mdPath: string) => {\n  const tutorialRootPath = getRootStaticPath(mdPath)\n  return new URL(tutorialRootPath + nodeUrl, getOriginUrl()).toString()\n}\n\nexport const getFileUrlFromMd = (nodeUrl: string, mdPath: string): string => {\n  // process external link\n  if (IS_ABSOLUTE_PATH.test(nodeUrl)) return nodeUrl\n\n  if (nodeUrl.startsWith('/') || nodeUrl.startsWith('\\\\')) {\n    return processAbsolutePath(nodeUrl, mdPath)\n  }\n\n  // process relative path\n  const pathUrl = new URL(mdPath, getOriginUrl())\n  return new URL(nodeUrl, pathUrl).toString()\n}\n\nexport const getFileNameFromPath = (path: string): string =>\n  path.split('/').pop() || ''\n"
  },
  {
    "path": "redisinsight/ui/src/utils/plugins.ts",
    "content": "import { IPluginVisualization } from 'uiSrc/slices/interfaces'\nimport { getBaseApiUrl } from 'uiSrc/utils/common'\n\nexport const getVisualizationsByCommand = (\n  query: string = '',\n  visualizations: IPluginVisualization[],\n) =>\n  visualizations.filter((visualization: IPluginVisualization) =>\n    visualization.matchCommands.some(\n      (matchCommand) =>\n        query?.startsWith(matchCommand) ||\n        new RegExp(`^${matchCommand}`, 'i').test(query),\n    ),\n  )\n\nexport const urlForAsset = (basePluginUrl: string, path: string) => {\n  const baseApiUrl = getBaseApiUrl()\n  return `${baseApiUrl}${basePluginUrl}${path}`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/polyfills.ts",
    "content": ""
  },
  {
    "path": "redisinsight/ui/src/utils/pubSubUtils.ts",
    "content": "import { format } from 'date-fns'\n\nexport const getFormatDateTime = (time: number = 0) =>\n  format(new Date(+time), 'HH:mm:ss dd MMM yyyy')\n"
  },
  {
    "path": "redisinsight/ui/src/utils/rdi/getUrlRdiInstance.ts",
    "content": "import { ApiEndpoints } from 'uiSrc/constants'\n\nconst getRdiUrl = (...path: string[]) =>\n  `/${ApiEndpoints.RDI_INSTANCES}/${path.join('/')}`\n\nexport default getRdiUrl\n"
  },
  {
    "path": "redisinsight/ui/src/utils/rdi/index.ts",
    "content": "import getRdiUrl from './getUrlRdiInstance'\nimport isEqualPipelineFile from './pipeline'\n\nexport { getRdiUrl, isEqualPipelineFile }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/rdi/pipeline.ts",
    "content": "import { isEqual } from 'lodash'\nimport { load } from 'js-yaml'\n\nconst isEqualPipelineFile = (cur: string, prev: string = '') => {\n  try {\n    return isEqual(load(cur), load(prev))\n  } catch (e) {\n    return false\n  }\n}\n\nexport default isEqualPipelineFile\n"
  },
  {
    "path": "redisinsight/ui/src/utils/recommendation/helper.ts",
    "content": "import { isString, sortBy } from 'lodash'\nimport { IRecommendationsStatic } from 'uiSrc/slices/interfaces/recommendations'\n\nconst replaceVariables = (\n  value: any[] | any,\n  parameter?: string[],\n  params?: any,\n) =>\n  parameter && isString(value)\n    ? value.replace(/\\$\\{\\d}/g, (matched) => {\n        const parameterIndex: string = matched.substring(\n          matched.indexOf('{') + 1,\n          matched.lastIndexOf('}'),\n        )\n        return params[parameter[+parameterIndex]]\n      })\n    : value\n\nconst sortRecommendations = (\n  recommendations: any[],\n  recommendationsContent: IRecommendationsStatic,\n) =>\n  sortBy(recommendations, [\n    ({ name }) => name !== 'searchJSON',\n    ({ name }) => name !== 'searchIndexes',\n    ({ name }) => recommendationsContent[name]?.redisStack,\n    ({ name }) => name,\n  ])\n\nexport { sortRecommendations, replaceVariables }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/recommendation/index.ts",
    "content": "export * from './helper'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/redisearch.ts",
    "content": "import { FIELD_TYPE_OPTIONS } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nexport const getFieldTypeOptions = () =>\n  FIELD_TYPE_OPTIONS.map(({ value, text }) => ({\n    value,\n    inputDisplay: text,\n    label: text,\n  }))\n"
  },
  {
    "path": "redisinsight/ui/src/utils/redistack.ts",
    "content": "import { isArray, map, concat, remove, find } from 'lodash'\nimport { Instance, RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { isVersionHigherOrEquals, Nullable } from 'uiSrc/utils'\n\nconst REDISTACK_VERSION = '6.2.5'\nconst NON_REDISTACK_VERSION = '7.9'\n\nconst REDISTACK_REQUIRE_MODULES: Array<string | Array<string>> = [\n  RedisDefaultModules.ReJSON,\n  RedisDefaultModules.Bloom,\n  [RedisDefaultModules.Search, RedisDefaultModules.SearchLight],\n  RedisDefaultModules.TimeSeries,\n]\n\nconst REDISTACK_OPTIONAL_MODULES: Array<string | Array<string>> = [\n  RedisDefaultModules.Graph,\n  [RedisDefaultModules.RedisGears, RedisDefaultModules.RedisGears2],\n]\n\nconst MIN_MODULES_LENGTH = REDISTACK_REQUIRE_MODULES.length\nconst MAX_MODULES_LENGTH =\n  REDISTACK_REQUIRE_MODULES.length + REDISTACK_OPTIONAL_MODULES.length\n\nconst checkRediStackModules = (modules: any[]) => {\n  if (!modules?.length) return false\n  const moduleNames = map(modules, 'name').sort()\n\n  if (modules.length === MIN_MODULES_LENGTH) {\n    return moduleNames.every((m, index) =>\n      isArray(REDISTACK_REQUIRE_MODULES[index])\n        ? (REDISTACK_REQUIRE_MODULES[index] as Array<string>).some(\n            (rm) => rm === m,\n          )\n        : REDISTACK_REQUIRE_MODULES[index] === m,\n    )\n  }\n\n  if (\n    modules.length > MIN_MODULES_LENGTH &&\n    modules.length <= MAX_MODULES_LENGTH\n  ) {\n    let isCustomModule = false\n    const rediStackModules = concat(\n      REDISTACK_REQUIRE_MODULES,\n      REDISTACK_OPTIONAL_MODULES,\n    ).sort()\n\n    const diff = rediStackModules.reduce(\n      (acc: Array<string>, current: string | Array<string>) => {\n        const moduleName = isArray(current)\n          ? (current as Array<string>).find((item) =>\n              find(moduleNames, (m) => item === m),\n            )\n          : find(moduleNames, (name) => name === current)\n\n        if (moduleName) {\n          remove(acc, (name) => moduleName === name)\n\n          return acc\n        }\n        isCustomModule = true\n\n        return acc\n      },\n      moduleNames,\n    )\n\n    return isCustomModule && !diff.length\n  }\n\n  return false\n}\n\nconst isRediStack = (modules: any[], version?: Nullable<string>): boolean => {\n  if (!version) {\n    return checkRediStackModules(modules)\n  }\n\n  if (isVersionHigherOrEquals(version, NON_REDISTACK_VERSION)) {\n    return false\n  }\n\n  if (isVersionHigherOrEquals(version, REDISTACK_VERSION)) {\n    return checkRediStackModules(modules)\n  }\n\n  return false\n}\n\nconst checkRediStack = (instances: Instance[]): Instance[] =>\n  instances.map((instance) => ({\n    ...instance,\n    isRediStack: isRediStack(instance.modules, instance.version),\n  }))\n\nexport { checkRediStack, isRediStack }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/rejson.spec.ts",
    "content": "import { checkExistingPath } from './rejson'\n\ndescribe('checkExistingPath', () => {\n  it('returns true for empty path (a.k.a. the whole object)', () => {\n    const obj = { foo: 123 }\n    expect(checkExistingPath(`$`, obj)).toBe(true)\n  })\n\n  it('detects root-level existing key', () => {\n    const obj = { foo: 123 }\n    expect(checkExistingPath(`$['foo']`, obj)).toBe(true)\n  })\n\n  it('detects root-level missing key', () => {\n    const obj = { foo: 123 }\n    expect(checkExistingPath(`$['bar']`, obj)).toBe(false)\n  })\n\n  it('detects nested existing key', () => {\n    const obj = { array: { nested: 42 } }\n    expect(checkExistingPath(`$['array']['nested']`, obj)).toBe(true)\n  })\n\n  it('detects nested missing key', () => {\n    const obj = { array: { nested: 42 } }\n    expect(checkExistingPath(`$['array']['newNested']`, obj)).toBe(false)\n  })\n\n  it('returns false if parent is missing', () => {\n    const obj = {}\n    expect(checkExistingPath(`$['nonExistent']['child']`, obj)).toBe(false)\n  })\n\n  it('handles numeric index paths', () => {\n    const obj = { arr: [{ val: 1 }] }\n    expect(checkExistingPath(`$['arr'][0]['val']`, obj)).toBe(true)\n    expect(checkExistingPath(`$['arr'][1]['val']`, obj)).toBe(false)\n  })\n\n  it('handles non-object parents gracefully', () => {\n    const obj = { a: 123 }\n    expect(checkExistingPath(`$['a']['b']`, obj)).toBe(false) // number can't have a child\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/rejson.ts",
    "content": "import { get } from 'lodash'\nimport { IJSONData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/interfaces'\nimport parseRedisJsonPath from './transformers/parseRedisJsonPath'\n\n/**\n * Checks whether a given Redis JSONPath would override existing data in the target object.\n *\n * This function inspects the parent object of the target key and determines if the key already exists.\n *\n * Example:\n * - If path is \"$['foo']['bar']\" it will check if object.foo.bar already exists.\n * - Returns false if key does not exist or parent is missing.\n *\n * @param path - Redis JSONPath string (e.g., \"$['foo'][0]['bar']\")\n * @param object - JSON-like object to check against\n * @returns true if the key already exists and would be overwritten, false otherwise\n */\nexport const checkExistingPath = (path: string, object: IJSONData): boolean => {\n  const parsedPath = parseRedisJsonPath(path)\n\n  if (!parsedPath.length) {\n    // Path is root \"$\". We don't want to override the whole object.\n    return true\n  }\n\n  const isRootKey = parsedPath.length === 1\n  const parent = isRootKey ? object : get(object, parsedPath.slice(0, -1))\n  const key = parsedPath[parsedPath.length - 1]\n\n  if (typeof parent !== 'object' || parent === null) return false\n\n  return Object.prototype.hasOwnProperty.call(parent, key)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/routerWithSubRoutes.tsx",
    "content": "import React from 'react'\nimport { Redirect, Route } from 'react-router-dom'\nimport { useSelector } from 'react-redux'\nimport { userSettingsSelector } from 'uiSrc/slices/user/user-settings'\nimport { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'\nimport { IRoute, FeatureFlags, Pages } from 'uiSrc/constants'\n\nconst PrivateRoute = (route: IRoute) => {\n  const { path, exact, routes, featureFlag, redirect } = route\n  const { [featureFlag as FeatureFlags]: feature } = useSelector(\n    appFeatureFlagsFeaturesSelector,\n  )\n  const { isShowConceptsPopup: haveToAcceptAgreements } =\n    useSelector(userSettingsSelector)\n\n  return (\n    <Route\n      path={path}\n      exact={exact}\n      render={(props) => {\n        if (redirect) {\n          return (\n            <Redirect\n              to={{\n                search: props.location.search,\n                pathname: redirect(props?.match?.params),\n              }}\n            />\n          )\n        }\n\n        if (haveToAcceptAgreements) {\n          return <Redirect to=\"/\" />\n        }\n\n        return feature?.flag === false ? (\n          <Redirect to={Pages.notFound} />\n        ) : (\n          // pass the sub-routes down to keep nesting\n          // @ts-ignore\n          <route.component {...props} routes={routes} />\n        )\n      }}\n    />\n  )\n}\n\nconst RouteWithSubRoutes = (route: IRoute) => {\n  const { isAvailableWithoutAgreements, featureFlag, path, exact, routes } =\n    route\n\n  return !isAvailableWithoutAgreements || featureFlag ? (\n    PrivateRoute(route)\n  ) : (\n    <Route\n      path={path}\n      exact={exact}\n      render={(props) => (\n        // pass the sub-routes down to keep nesting\n        // @ts-ignore\n        <route.component {...props} routes={routes} />\n      )}\n    />\n  )\n}\n\nexport default RouteWithSubRoutes\n"
  },
  {
    "path": "redisinsight/ui/src/utils/routing.ts",
    "content": "import { IRoute } from 'uiSrc/constants'\nimport { Maybe, Nullable } from 'uiSrc/utils'\nimport DEFAULT_ROUTES from 'uiSrc/components/main-router/constants/defaultRoutes'\n\nconst CURRENT_PAGE_URL_SYNTAX = '/_'\n\nexport const findRouteByPathname = (\n  routes: IRoute[],\n  pathname: string,\n): Maybe<IRoute> => {\n  let findRoute\n\n  // eslint-disable-next-line no-restricted-syntax\n  for (const route of routes) {\n    if (new RegExp(`${pathname}$`).test(route.path)) {\n      return route\n    }\n\n    if (route.routes) {\n      findRoute = findRouteByPathname(route.routes, pathname)\n      if (findRoute) {\n        return findRoute\n      }\n    }\n  }\n\n  return findRoute\n}\n\n// undefined - route was not found\n// null - route found but private\nexport const getRedirectionPage = (\n  pageInput: string,\n  databaseId?: string,\n  currentPathname?: string,\n): Nullable<Maybe<string>> => {\n  let page = pageInput.replace(/^\\//, '')\n  try {\n    const { pathname, searchParams } = new URL(page, window.location.origin)\n\n    if (currentPathname && pathname === CURRENT_PAGE_URL_SYNTAX) {\n      return `${currentPathname}?${searchParams.toString()}`\n    }\n\n    if (searchParams.has('guidePath') || searchParams.has('tutorialId')) {\n      page += '&insights=open'\n    }\n\n    const foundRoute = findRouteByPathname(DEFAULT_ROUTES, pathname)\n    if (!foundRoute) return undefined\n\n    if (foundRoute.path.includes(':instanceId')) {\n      if (databaseId) {\n        return `/${databaseId}/${page}`\n      }\n      return null\n    }\n\n    return `/${page}`\n  } catch (_e) {\n    return undefined\n  }\n}\n\nexport const getPageName = (databaseId: string, path: string) =>\n  path?.replace(`/${databaseId}`, '')\n"
  },
  {
    "path": "redisinsight/ui/src/utils/statuses.ts",
    "content": "export const isStatusInformation = (status: number) =>\n  status >= 100 && status < 200\n\nexport const isStatusSuccessful = (status: number) =>\n  status >= 200 && status < 300\n\nexport const isStatusRedirection = (status: number) =>\n  status >= 300 && status < 400\n\nexport const isStatusClientError = (status: number) =>\n  status >= 400 && status < 500\n\nexport const isStatusServerError = (status: number) =>\n  status >= 500 && status < 600\n\nexport const isStatusNotFoundError = (status: number) => status === 404\n"
  },
  {
    "path": "redisinsight/ui/src/utils/streamUtils.ts",
    "content": "import { format } from 'date-fns'\nimport { orderBy } from 'lodash'\nimport { SortOrder } from 'uiSrc/constants'\nimport {\n  SCAN_STREAM_START_DEFAULT,\n  SCAN_STREAM_END_DEFAULT,\n} from 'uiSrc/constants/api'\nimport { RedisResponseBuffer } from 'uiSrc/slices/interfaces'\nimport {\n  ClaimPendingEntryDto,\n  ConsumerDto,\n  ConsumerGroupDto,\n  PendingEntryDto,\n} from 'apiSrc/modules/browser/stream/dto'\nimport { isEqualBuffers } from './formatters'\n\nexport enum ClaimTimeOptions {\n  RELATIVE = 'idle',\n  ABSOLUTE = 'time',\n}\n\ninterface IForm {\n  consumerName: string\n  minIdleTime: string\n  timeCount: string\n  timeOption: ClaimTimeOptions\n  retryCount: string\n  force: boolean\n}\n\nexport const getFormatTime = (time: string = '') =>\n  format(new Date(+time), 'HH:mm:ss.SSS d MMM yyyy')\n\nexport const getTimestampFromId = (id: string = ''): number =>\n  parseInt(id.split('-')[0], 10)\n\nexport const getStreamRangeStart = (start: string, firstEntryId: string) => {\n  if (\n    start === '' ||\n    !firstEntryId ||\n    start === getTimestampFromId(firstEntryId).toString()\n  ) {\n    return SCAN_STREAM_START_DEFAULT\n  }\n  return start\n}\n\nexport const getStreamRangeEnd = (end: string, endEtryId: string) => {\n  if (\n    end === '' ||\n    !endEtryId ||\n    end === getTimestampFromId(endEtryId).toString()\n  ) {\n    return SCAN_STREAM_END_DEFAULT\n  }\n  return end\n}\n\nexport const getNextId = (id: string, sortOrder: SortOrder): string => {\n  const splittedId = id.split('-')\n  // if we don't have prefix\n  if (splittedId.length === 1) {\n    return `${id}-1`\n  }\n  if (sortOrder === SortOrder.DESC) {\n    return splittedId[1] === '0'\n      ? `${parseInt(splittedId[0], 10) - 1}`\n      : `${splittedId[0]}-${+splittedId[1] - 1}`\n  }\n  return `${splittedId[0]}-${+splittedId[1] + 1}`\n}\n\nexport const getDefaultConsumer = (consumers: ConsumerDto[]): ConsumerDto => {\n  const sortedConsumers = orderBy(\n    consumers,\n    ['pending', 'name'],\n    ['asc', 'asc'],\n  )\n  return sortedConsumers[0]\n}\n\nexport const prepareDataForClaimRequest = (\n  values: IForm,\n  entries: string[],\n  isOptionalAvailable: boolean,\n): Partial<ClaimPendingEntryDto> => {\n  const {\n    consumerName,\n    minIdleTime,\n    timeCount,\n    timeOption,\n    retryCount,\n    force,\n  } = values\n  if (isOptionalAvailable) {\n    return {\n      consumerName,\n      minIdleTime: minIdleTime ? parseInt(minIdleTime, 10) : 0,\n      [timeOption]: timeCount ? parseInt(timeCount, 10) : 0,\n      retryCount: retryCount ? parseInt(retryCount, 10) : 0,\n      force,\n      entries,\n    }\n  }\n  return {\n    consumerName,\n    minIdleTime: minIdleTime ? parseInt(minIdleTime, 10) : 0,\n    entries,\n  }\n}\n\nexport const updateConsumerGroups = (\n  groups: ConsumerGroupDto[],\n  groupName: RedisResponseBuffer,\n  consumers: ConsumerDto[],\n) =>\n  groups?.map((group: ConsumerGroupDto) => {\n    if (isEqualBuffers(group.name, groupName)) {\n      group.consumers = consumers?.length\n      group.pending = consumers?.reduce((a, { pending }) => a + pending, 0)\n    }\n    return group\n  })\n\nexport const updateConsumers = (\n  consumers: ConsumerDto[],\n  consumerName: RedisResponseBuffer,\n  messages: PendingEntryDto[],\n) =>\n  consumers?.map((consumer: ConsumerDto) => {\n    if (isEqualBuffers(consumer.name, consumerName)) {\n      consumer.pending = messages?.length\n    }\n    return consumer\n  })\n"
  },
  {
    "path": "redisinsight/ui/src/utils/telemetry.ts",
    "content": "import { isNil } from 'lodash'\nimport { numberWithSpaces } from 'uiSrc/utils/numbers'\nimport { Maybe } from 'uiSrc/utils'\n\nexport const BULK_THRESHOLD_BREAKPOINTS = [5000, 10000, 50000, 100000, 1000000]\n\nexport const getRangeForNumber = (\n  value: Maybe<number>,\n  breakpoints: number[] = BULK_THRESHOLD_BREAKPOINTS,\n): Maybe<string> => {\n  if (isNil(value)) {\n    return undefined\n  }\n  const index = breakpoints.findIndex((threshold: number) => value <= threshold)\n  if (index === 0) {\n    return `0 - ${numberWithSpaces(breakpoints[0])}`\n  }\n  if (index === -1) {\n    const lastItem = breakpoints[breakpoints.length - 1]\n    return `${numberWithSpaces(lastItem + 1)} +`\n  }\n  return `${numberWithSpaces(\n    breakpoints[index - 1] + 1,\n  )} - ${numberWithSpaces(breakpoints[index])}`\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/test-store.ts",
    "content": "import type { ReduxStore } from 'uiSrc/slices/store'\n\n// Re-export all types and exports from the real store to avoid circular dependencies during tests\n\nexport type { RootState, AppDispatch, ReduxStore } from 'uiSrc/slices/store'\n\n// Lazy reference to avoid circular dependencies\n// The store will be set by the store module itself after it's created\nlet storeRef: ReduxStore | null = null\n\n// This function will be called by the store modules to set the reference\nexport const setStoreRef = (store: ReduxStore) => {\n  storeRef = store\n}\n\nconst getState: ReduxStore['getState'] = () => {\n  if (!storeRef) {\n    throw new Error(\n      'Store not initialized. Make sure store-dynamic is imported after store creation.',\n    )\n  }\n  return storeRef.getState()\n}\n\nexport const dispatch: ReduxStore['dispatch'] = (action: any) => {\n  if (!storeRef) {\n    throw new Error(\n      'Store not initialized. Make sure store-dynamic is imported after store creation.',\n    )\n  }\n  return storeRef.dispatch(action)\n}\n\nconst subscribe: ReduxStore['subscribe'] = (listener: () => void) => {\n  if (!storeRef) {\n    throw new Error(\n      'Store not initialized. Make sure store-dynamic is imported after store creation.',\n    )\n  }\n  return storeRef.subscribe(listener)\n}\n\n// Export store object that matches the real store interface\nexport const store = {\n  getState,\n  dispatch,\n  subscribe,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/test-utils.tsx",
    "content": "// test-utils.js\nimport React from 'react'\nimport { cloneDeep, first, map } from 'lodash'\nimport { Provider } from 'react-redux'\nimport thunk from 'redux-thunk'\nimport { BrowserRouter } from 'react-router-dom'\nimport configureMockStore from 'redux-mock-store'\nimport {\n  render as rtlRender,\n  renderHook as rtlRenderHook,\n  waitFor,\n  screen,\n  within,\n} from '@testing-library/react'\n\nimport { ThemeProvider } from 'styled-components'\nimport { theme } from '@redis-ui/styles'\nimport userEvent from '@testing-library/user-event'\nimport type { RootState, ReduxStore } from 'uiSrc/slices/store'\nimport { initialState as initialStateInstances } from 'uiSrc/slices/instances/instances'\nimport { initialState as initialStateTags } from 'uiSrc/slices/instances/tags'\nimport { initialState as initialStateCaCerts } from 'uiSrc/slices/instances/caCerts'\nimport { initialState as initialStateClientCerts } from 'uiSrc/slices/instances/clientCerts'\nimport { initialState as initialStateCluster } from 'uiSrc/slices/instances/cluster'\nimport { initialState as initialStateCloud } from 'uiSrc/slices/instances/cloud'\nimport { initialState as initialStateSentinel } from 'uiSrc/slices/instances/sentinel'\nimport { initialState as initialStateKeys } from 'uiSrc/slices/browser/keys'\nimport { initialState as initialStateString } from 'uiSrc/slices/browser/string'\nimport { initialState as initialStateZSet } from 'uiSrc/slices/browser/zset'\nimport { initialState as initialStateSet } from 'uiSrc/slices/browser/set'\nimport { initialState as initialStateHash } from 'uiSrc/slices/browser/hash'\nimport { initialState as initialStateList } from 'uiSrc/slices/browser/list'\nimport { initialState as initialStateRejson } from 'uiSrc/slices/browser/rejson'\nimport { initialState as initialStateStream } from 'uiSrc/slices/browser/stream'\nimport { initialState as initialStateBulkActions } from 'uiSrc/slices/browser/bulkActions'\nimport { initialState as initialStateNotifications } from 'uiSrc/slices/app/notifications'\nimport { initialState as initialStateAppInfo } from 'uiSrc/slices/app/info'\nimport { initialState as initialStateAppContext } from 'uiSrc/slices/app/context'\nimport { initialState as initialStateAppRedisCommands } from 'uiSrc/slices/app/redis-commands'\nimport { initialState as initialStateAppPluginsReducer } from 'uiSrc/slices/app/plugins'\nimport { initialState as initialStateAppSocketConnectionReducer } from 'uiSrc/slices/app/socket-connection'\nimport { initialState as initialStateAppFeaturesReducer } from 'uiSrc/slices/app/features'\nimport { initialState as initialStateAppUrlHandlingReducer } from 'uiSrc/slices/app/url-handling'\nimport { initialState as initialStateAppCsrfReducer } from 'uiSrc/slices/app/csrf'\nimport { initialState as initialStateCliSettings } from 'uiSrc/slices/cli/cli-settings'\nimport { initialState as initialStateCliOutput } from 'uiSrc/slices/cli/cli-output'\nimport { initialState as initialStateMonitor } from 'uiSrc/slices/cli/monitor'\nimport { initialState as initialStateUserSettings } from 'uiSrc/slices/user/user-settings'\nimport { initialState as initialStateUserProfile } from 'uiSrc/slices/user/cloud-user-profile'\nimport { initialState as initialStateWBResults } from 'uiSrc/slices/workbench/wb-results'\nimport { initialState as initialStateWBETutorials } from 'uiSrc/slices/workbench/wb-tutorials'\nimport { initialState as initialStateWBECustomTutorials } from 'uiSrc/slices/workbench/wb-custom-tutorials'\nimport { initialState as initialStateSearchAndQuery } from 'uiSrc/slices/search/searchAndQuery'\nimport { initialState as initialStateCreateRedisButtons } from 'uiSrc/slices/content/create-redis-buttons'\nimport { initialState as initialStateGuideLinks } from 'uiSrc/slices/content/guide-links'\nimport { initialState as initialStateSlowLog } from 'uiSrc/slices/analytics/slowlog'\nimport { initialState as initialClusterDetails } from 'uiSrc/slices/analytics/clusterDetails'\nimport { initialState as initialStateAnalyticsSettings } from 'uiSrc/slices/analytics/settings'\nimport { initialState as initialStateDbAnalysis } from 'uiSrc/slices/analytics/dbAnalysis'\nimport { initialState as initialStatePubSub } from 'uiSrc/slices/pubsub/pubsub'\nimport { initialState as initialStateRedisearch } from 'uiSrc/slices/browser/redisearch'\nimport { initialState as initialStateRecommendations } from 'uiSrc/slices/recommendations/recommendations'\nimport { initialState as initialStateOAuth } from 'uiSrc/slices/oauth/cloud'\nimport { initialState as initialStateAzureAuth } from 'uiSrc/slices/oauth/azure'\nimport { initialState as initialStateSidePanels } from 'uiSrc/slices/panels/sidePanels'\nimport { initialState as initialStateRdiPipeline } from 'uiSrc/slices/rdi/pipeline'\nimport { initialState as initialStateRdi } from 'uiSrc/slices/rdi/instances'\nimport { initialState as initialStateRdiDryRunJob } from 'uiSrc/slices/rdi/dryRun'\nimport { initialState as initialStateRdiStatistics } from 'uiSrc/slices/rdi/statistics'\nimport { initialState as initialStateRdiTestConnections } from 'uiSrc/slices/rdi/testConnections'\nimport { initialState as initialStateAiAssistant } from 'uiSrc/slices/panels/aiAssistant'\nimport { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'\nimport { apiService } from 'uiSrc/services'\nimport { initialState as initialStateAppConnectivity } from 'uiSrc/slices/app/connectivity'\nimport { initialState as initialStateAppDbSettings } from 'uiSrc/slices/app/db-settings'\nimport { initialState as initialStateAppInit } from 'uiSrc/slices/app/init'\nimport * as appFeaturesSlice from 'uiSrc/slices/app/features'\nimport { setStoreRef } from './test-store'\n\ninterface Options {\n  initialState?: RootState\n  store?: ReduxStore\n  withRouter?: boolean\n  [property: string]: any\n}\n\n// root state\nconst initialStateDefault: RootState = {\n  app: {\n    info: cloneDeep(initialStateAppInfo),\n    notifications: cloneDeep(initialStateNotifications),\n    context: cloneDeep(initialStateAppContext),\n    redisCommands: cloneDeep(initialStateAppRedisCommands),\n    plugins: cloneDeep(initialStateAppPluginsReducer),\n    socketConnection: cloneDeep(initialStateAppSocketConnectionReducer),\n    features: cloneDeep(initialStateAppFeaturesReducer),\n    urlHandling: cloneDeep(initialStateAppUrlHandlingReducer),\n    csrf: cloneDeep(initialStateAppCsrfReducer),\n    init: cloneDeep(initialStateAppInit),\n    connectivity: cloneDeep(initialStateAppConnectivity),\n    dbSettings: cloneDeep(initialStateAppDbSettings),\n  },\n  connections: {\n    instances: cloneDeep(initialStateInstances),\n    caCerts: cloneDeep(initialStateCaCerts),\n    clientCerts: cloneDeep(initialStateClientCerts),\n    cluster: cloneDeep(initialStateCluster),\n    cloud: cloneDeep(initialStateCloud),\n    sentinel: cloneDeep(initialStateSentinel),\n    tags: cloneDeep(initialStateTags),\n  },\n  browser: {\n    keys: cloneDeep(initialStateKeys),\n    string: cloneDeep(initialStateString),\n    zset: cloneDeep(initialStateZSet),\n    set: cloneDeep(initialStateSet),\n    hash: cloneDeep(initialStateHash),\n    list: cloneDeep(initialStateList),\n    rejson: cloneDeep(initialStateRejson),\n    stream: cloneDeep(initialStateStream),\n    bulkActions: cloneDeep(initialStateBulkActions),\n    redisearch: cloneDeep(initialStateRedisearch),\n  },\n  cli: {\n    settings: cloneDeep(initialStateCliSettings),\n    output: cloneDeep(initialStateCliOutput),\n    monitor: cloneDeep(initialStateMonitor),\n  },\n  user: {\n    settings: cloneDeep(initialStateUserSettings),\n    cloudProfile: cloneDeep(initialStateUserProfile),\n  },\n  workbench: {\n    results: cloneDeep(initialStateWBResults),\n    tutorials: cloneDeep(initialStateWBETutorials),\n    customTutorials: cloneDeep(initialStateWBECustomTutorials),\n  },\n  search: {\n    query: cloneDeep(initialStateSearchAndQuery),\n  },\n  content: {\n    createRedisButtons: cloneDeep(initialStateCreateRedisButtons),\n    guideLinks: cloneDeep(initialStateGuideLinks),\n  },\n  analytics: {\n    settings: cloneDeep(initialStateAnalyticsSettings),\n    slowlog: cloneDeep(initialStateSlowLog),\n    clusterDetails: cloneDeep(initialClusterDetails),\n    databaseAnalysis: cloneDeep(initialStateDbAnalysis),\n  },\n  recommendations: cloneDeep(initialStateRecommendations),\n  pubsub: cloneDeep(initialStatePubSub),\n  oauth: {\n    cloud: cloneDeep(initialStateOAuth),\n    azure: cloneDeep(initialStateAzureAuth),\n  },\n  panels: {\n    sidePanels: cloneDeep(initialStateSidePanels),\n    aiAssistant: cloneDeep(initialStateAiAssistant),\n  },\n  rdi: {\n    pipeline: cloneDeep(initialStateRdiPipeline),\n    instances: cloneDeep(initialStateRdi),\n    dryRun: cloneDeep(initialStateRdiDryRunJob),\n    statistics: cloneDeep(initialStateRdiStatistics),\n    testConnections: cloneDeep(initialStateRdiTestConnections),\n  },\n}\n\n// mocked store\nexport const mockStore = configureMockStore<RootState>([thunk])\nexport const mockedStore = mockStore(initialStateDefault)\nexport const mockedStoreFn = () => mockStore(initialStateDefault)\nexport const createMockedStore = () => {\n  const store = mockStore(initialStateDefault)\n  setStoreRef(store)\n  return store\n}\n\n// Set the mock store reference for the dynamic store wrapper\n// This ensures that store-dynamic works correctly in tests\nsetStoreRef(mockedStore)\n\n// insert root state to the render Component\nconst render = (\n  ui: JSX.Element,\n  {\n    initialState: _initialState,\n    store = mockedStore,\n    withRouter,\n    ...renderOptions\n  }: Options = initialStateDefault,\n) => {\n  if (store !== mockedStore) {\n    setStoreRef(store)\n  }\n\n  const Wrapper = ({ children }: { children: JSX.Element }) => (\n    <ThemeProvider theme={theme}>\n      <Provider store={store}>{children}</Provider>\n    </ThemeProvider>\n  )\n\n  const wrapper = !withRouter ? Wrapper : BrowserRouter\n\n  return rtlRender(ui, { wrapper, ...renderOptions })\n}\n\nconst renderHook = <T,>(\n  hook: (initialProps: unknown) => T,\n  {\n    initialState: _initialState,\n    store = mockedStore,\n    withRouter,\n    ...renderOptions\n  }: Options = initialStateDefault,\n) => {\n  if (store !== mockedStore) {\n    setStoreRef(store)\n  }\n\n  const Wrapper = ({ children }: { children: JSX.Element }) => (\n    <Provider store={store}>{children}</Provider>\n  )\n\n  const wrapper = !withRouter ? Wrapper : BrowserRouter\n\n  return rtlRenderHook(hook, { wrapper, ...renderOptions })\n}\n\n// for render components WithRouter\nconst renderWithRouter = (ui: JSX.Element, { route = '/' } = {}) => {\n  window.history.pushState({}, 'Test page', route)\n\n  return render(ui, { wrapper: BrowserRouter })\n}\n\nconst clearStoreActions = (actions: any[]) => {\n  const newActions = map(actions, (action) => {\n    const newAction = { ...action }\n    if (newAction?.payload) {\n      const payload = {\n        ...first<any>(newAction.payload),\n        key: '',\n      }\n      newAction.payload = [payload]\n    }\n    return newAction\n  })\n  return JSON.stringify(newActions)\n}\n\n/**\n * Ensure the RiTooltip being tested is open and visible before continuing\n */\nconst waitForRiTooltipVisible = async (timeout = 500) => {\n  await waitFor(\n    () => {\n      const tooltip = document.querySelector(\n        '[data-radix-popper-content-wrapper]',\n      )\n      expect(tooltip).toBeInTheDocument()\n    },\n    { timeout }, // Account for long delay on tooltips\n  )\n}\n\nconst waitForRiTooltipHidden = async () => {\n  await waitFor(() => {\n    const tooltip = document.querySelector(\n      '[data-radix-popper-content-wrapper]',\n    )\n    expect(tooltip).toBeNull()\n  })\n}\n\nconst waitForRiPopoverVisible = async (timeout = 500) => {\n  await waitFor(\n    () => {\n      const tooltip = document.querySelector(\n        'div[data-radix-popper-content-wrapper]',\n      ) as HTMLElement | null\n      expect(tooltip).toBeInTheDocument()\n\n      if (tooltip) {\n        // Note: during unit tests, the popover is not interactive by default so we need to enable pointer events\n        tooltip.style.pointerEvents = 'all'\n      }\n    },\n    { timeout }, // Account for long delay on popover\n  )\n}\n\nexport const waitForRedisUiSelectVisible = async (timeout = 500) => {\n  await waitFor(\n    () => {\n      const element = document.querySelector(\n        '[data-radix-popper-content-wrapper]',\n      )\n      expect(element).toBeInTheDocument()\n    },\n    { timeout }, // Account for long delay on popover\n  )\n}\n\nexport const waitForStack = async (timeout = 0) => {\n  await waitFor(() => {}, { timeout })\n}\n\nexport const toggleAccordion = async (testId: string) => {\n  const accordion = screen.getByTestId(testId)\n  expect(accordion).toBeInTheDocument()\n  // Find the collapse button by aria-label (RiAccordion renders ActionButton before CollapseButton)\n  const btn = within(accordion).getByLabelText(\n    /Expand Section|Collapse Section/,\n  )\n  await userEvent.click(btn)\n}\n\n// mock useHistory\njest.mock('react-router-dom', () => ({\n  ...jest.requireActual('react-router-dom'),\n  useHistory: () => ({\n    push: jest.fn,\n  }),\n  useLocation: () => ({\n    pathname: 'pathname',\n    search: '',\n  }),\n  useParams: () => ({\n    instanceId: 'instanceId',\n    rdiInstanceId: 'rdiInstanceId',\n    jobName: 'jobName',\n  }),\n}))\n\n// // mock useDispatch\n// jest.mock('react-redux', () => ({\n//   ...jest.requireActual('react-redux'),\n//   useDispatch: () => ({\n//     dispatch: jest.fn,\n//   }),\n// }))\n\n// mock <AutoSizer />\njest.mock(\n  'react-virtualized-auto-sizer',\n  () =>\n    ({ children }: { children: any }) =>\n      children({ height: 600, width: 600 }),\n)\n\nexport const MOCKED_HIGHLIGHTING_FEATURES = [\n  'importDatabases',\n  'anotherFeature',\n]\njest.mock('uiSrc/constants/featuresHighlighting', () => ({\n  BUILD_FEATURES: {\n    importDatabases: {\n      type: 'tooltip',\n      title: 'Import Database Connections',\n      content: 'Import your database connections from other Redis UIs',\n      page: 'browser',\n    },\n    anotherFeature: {\n      type: 'tooltip',\n      title: 'Import Database Connections',\n      content: 'Import your database connections from other Redis UIs',\n      page: 'browser',\n    },\n  },\n}))\n\njest.mock('uiSrc/constants/recommendations', () => ({\n  ...jest.requireActual('uiSrc/constants/recommendations'),\n  ANIMATION_INSIGHT_PANEL_MS: jest.fn().mockReturnValue(0),\n}))\n\n// mock to not import routes\njest.mock('uiSrc/utils/routing', () => ({\n  ...jest.requireActual('uiSrc/utils/routing'),\n  getRedirectionPage: jest.fn(),\n}))\n\nexport const localStorageMock = {\n  getItem: jest.fn(),\n  setItem: jest.fn(),\n  removeItem: jest.fn(),\n  clear: jest.fn(),\n}\nObject.defineProperty(window, 'localStorage', { value: localStorageMock })\n\nexport const sessionStorageMock = {\n  getItem: jest.fn(),\n  setItem: jest.fn(),\n  removeItem: jest.fn(),\n  clear: jest.fn(),\n}\nObject.defineProperty(window, 'sessionStorage', { value: sessionStorageMock })\n\nconst scrollIntoViewMock = jest.fn()\nwindow.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock\n\nconst matchMediaMock = (_: any) => ({\n  matches: false,\n  addEventListener: jest.fn(),\n  removeEventListener: jest.fn(),\n})\n\nObject.defineProperty(window, 'matchMedia', {\n  writable: true,\n  value: jest.fn().mockImplementation((query) => matchMediaMock(query)),\n})\n\nexport const getMswResourceURL = (path: string = '') =>\n  RESOURCES_BASE_URL.concat(path)\n\nexport const getMswURL = (path: string = '') =>\n  apiService.defaults.baseURL?.concat(\n    path.startsWith('/') ? path.slice(1) : path,\n  ) ?? ''\n\nexport const mockWindowLocation = (initialHref = '') => {\n  const setHrefMock = jest.fn()\n  let href = initialHref\n  Object.defineProperty(window, 'location', {\n    value: {\n      set href(url) {\n        setHrefMock(url)\n        href = url\n      },\n      get href() {\n        return href\n      },\n    },\n    writable: true,\n  })\n\n  return setHrefMock\n}\n\nexport const mockFeatureFlags = (\n  overrides?: Partial<\n    typeof initialStateAppFeaturesReducer.featureFlags.features\n  >,\n) => {\n  const initialFlags = initialStateAppFeaturesReducer.featureFlags.features\n\n  return jest\n    .spyOn(appFeaturesSlice, 'appFeatureFlagsFeaturesSelector')\n    .mockReturnValue({\n      ...initialFlags,\n      ...(overrides || {}),\n    })\n}\n\n/**\n * Helper function to check if expected actions are contained within actual store actions\n * @param actualActions - The actual actions dispatched to the store\n * @param expectedActions - The expected actions that should be present\n */\nconst expectActionsToContain = (\n  actualActions: any[],\n  expectedActions: any[],\n) => {\n  expect(actualActions).toEqual(expect.arrayContaining(expectedActions))\n}\n\n/**\n * Helper function to check if actions are not contained within actual store actions\n * @param actualActions - The actual actions dispatched to the store\n * @param expectedActions - The expected actions that should not be presented\n */\nconst expectActionsToNotContain = (\n  actualActions: any[],\n  expectedActions: any[],\n) => {\n  expect(actualActions).not.toEqual(expect.arrayContaining(expectedActions))\n}\n\n// re-export everything\nexport * from '@testing-library/react'\n// override render method\nexport {\n  userEvent,\n  initialStateDefault,\n  render,\n  renderHook,\n  renderWithRouter,\n  clearStoreActions,\n  waitForRiTooltipVisible,\n  waitForRiTooltipHidden,\n  waitForRiPopoverVisible,\n  expectActionsToContain,\n  expectActionsToNotContain,\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/apiResponse.spec.ts",
    "content": "import { AxiosError } from 'axios'\nimport {\n  DEFAULT_ERROR_MESSAGE,\n  getApiErrorCode,\n  getApiErrorMessage,\n  getAxiosError,\n  parseCustomError,\n} from 'uiSrc/utils'\nimport { EnhancedAxiosError } from 'uiSrc/slices/interfaces'\n\nconst error = { response: { data: { message: 'error' } } } as AxiosError\nconst errors = {\n  response: { data: { message: ['error1', 'error2'] } },\n} as AxiosError\n\nconst customError1: EnhancedAxiosError = {\n  response: { data: { message: 'error' }, status: 500 },\n}\nconst customError2: EnhancedAxiosError = {\n  response: { data: { message: 'error', errorCode: 11_002 }, status: 402 },\n}\nconst customError3: EnhancedAxiosError = {\n  response: {\n    data: { message: 'error', error: 'UnexpectedError' },\n    status: 503,\n  },\n}\n\ndescribe('getAxiosError', () => {\n  it('should return proper error', () => {\n    expect(getAxiosError(customError1)).toEqual(customError1)\n    expect(getAxiosError(customError2)).toEqual(\n      parseCustomError(customError2.response?.data),\n    )\n    expect(getAxiosError(customError3)).toEqual(customError3)\n  })\n})\n\ndescribe('getApiErrorMessage', () => {\n  it('should return proper message', () => {\n    expect(getApiErrorMessage(error)).toEqual('error')\n    expect(getApiErrorMessage(null)).toEqual(DEFAULT_ERROR_MESSAGE)\n    expect(getApiErrorMessage(errors)).toEqual('error1')\n  })\n})\n\ndescribe('getAxiosError', () => {\n  it('should return proper error code', () => {\n    expect(getApiErrorCode(customError1)).toEqual(500)\n    expect(getApiErrorCode(customError2)).toEqual(402)\n    expect(getApiErrorCode(customError3)).toEqual(503)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/bigString.spec.ts",
    "content": "import * as bigStringUtil from 'uiSrc/utils/bigString'\nimport { getConfig } from 'uiSrc/config'\nimport { stringToBuffer } from 'uiSrc/utils'\n\nconst BIG_STRING_PREFIX = getConfig().app.truncatedStringPrefix\n\ndescribe('bigStringUtil', () => {\n  describe('isTruncatedString', () => {\n    it.each([\n      { input: 'some string', output: false },\n      { input: JSON.stringify('some string'), output: false },\n      { input: stringToBuffer('some string'), output: false },\n      { input: `${BIG_STRING_PREFIX} some string`, output: true },\n      {\n        input: JSON.stringify(`${BIG_STRING_PREFIX} some string`),\n        output: true,\n      },\n      {\n        input: stringToBuffer(`${BIG_STRING_PREFIX} some string`),\n        output: true,\n      },\n      { input: null, output: false },\n      { input: '', output: false },\n      { input: stringToBuffer(''), output: false },\n    ])('%j', async ({ input, output }) => {\n      expect(bigStringUtil.isTruncatedString(input)).toEqual(output)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/capability.spec.ts",
    "content": "import { OAuthSocialSource, RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport {\n  getSourceTutorialByCapability,\n  getTutorialCapability,\n} from '../capability'\n\nconst getSourceTutorialByCapabilityTests: any[] = [\n  ['123', '123_tutorial'],\n  ['ueouoeu', 'ueouoeu_tutorial'],\n  ['2.puipy4', '2.puipy4_tutorial'],\n]\n\ndescribe('getSourceTutorialByCapability', () => {\n  it.each(getSourceTutorialByCapabilityTests)(\n    'for input: %s (input), should be output: %s',\n    (input, expected) => {\n      const result = getSourceTutorialByCapability(input)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\nconst emptyCapability = { name: '', telemetryName: '', path: null }\nconst searchCapability = {\n  name: 'Redis Query Engine',\n  telemetryName: 'searchAndQuery',\n  path: null,\n}\nconst jsonCapability = {\n  name: 'JSON data structure',\n  telemetryName: 'JSON',\n  path: null,\n}\nconst tsCapability = {\n  name: 'Time series data structure',\n  telemetryName: 'timeSeries',\n  path: null,\n}\nconst bloomCapability = {\n  name: 'Probabilistic data structures',\n  telemetryName: 'probabilistic',\n  path: null,\n}\n\nconst getTutorialCapabilityTests: any[] = [\n  [OAuthSocialSource.RediSearch, searchCapability],\n  [OAuthSocialSource.BrowserSearch, searchCapability],\n  [\n    getSourceTutorialByCapability(RedisDefaultModules.SearchLight),\n    searchCapability,\n  ],\n  [getSourceTutorialByCapability(RedisDefaultModules.Search), searchCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.FT), searchCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.FTL), searchCapability],\n\n  [OAuthSocialSource.RedisJSON, jsonCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.ReJSON), jsonCapability],\n\n  [OAuthSocialSource.RedisTimeSeries, tsCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.TimeSeries), tsCapability],\n\n  [OAuthSocialSource.RedisBloom, bloomCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.Bloom), bloomCapability],\n\n  // empty capabilities\n  [OAuthSocialSource.Autodiscovery, emptyCapability],\n  [OAuthSocialSource.ListOfDatabases, emptyCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.Graph), emptyCapability],\n  [getSourceTutorialByCapability(RedisDefaultModules.AI), emptyCapability],\n]\n\ndescribe('getTutorialCapability', () => {\n  it.each(getTutorialCapabilityTests)(\n    'for input: %s (source), should be output: %s',\n    (source, expected) => {\n      const result = getTutorialCapability(source)\n\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/cliHelper.spec.ts",
    "content": "import {\n  getDbIndexFromSelectQuery,\n  getCommandNameFromQuery,\n  cliParseCommandsGroupResult,\n  CliPrefix,\n  wbSummaryCommand,\n  checkUnsupportedModuleCommand,\n  checkCommandModule,\n  checkUnsupportedCommand,\n  checkBlockingCommand,\n  replaceEmptyValue,\n  removeDeprecatedModuleCommands,\n  checkDeprecatedModuleCommand,\n  checkDeprecatedCommandGroup,\n  getUnsupportedModulesFromQuery,\n} from 'uiSrc/utils'\nimport { MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\n\nconst getDbIndexFromSelectQueryTests = [\n  { input: 'select 0', expected: 0 },\n  { input: 'select 1', expected: 1 },\n  { input: 'SELECT 10', expected: 10 },\n  { input: 'SeLeCt 10', expected: 10 },\n  { input: 'select \"1\"', expected: 1 },\n  { input: '   select \"1\"   ', expected: 1 },\n  { input: \"select '1'\", expected: 1 },\n  { input: \"select '1'\", expected: 1 },\n  { input: 'info', expected: new Error('Invalid command') },\n  { input: 'select \"1 1231\"', expected: new Error('Parsing error') },\n  { input: 'select abc', expected: new Error('Parsing error') },\n  { input: 'select ', expected: new Error('Parsing error') },\n]\n\nconst replaceEmptyValueTests = [\n  { input: '', expected: '(nil)' },\n  { input: undefined, expected: '(nil)' },\n  { input: false, expected: '(nil)' },\n  { input: 'string', expected: 'string' },\n  { input: 0, expected: 0 },\n  { input: 1, expected: 1 },\n  { input: [], expected: [] },\n  { input: {}, expected: {} },\n]\n\ndescribe('replaceEmptyValue', () => {\n  test.each(replaceEmptyValueTests)('%j', ({ input, expected }) => {\n    expect(replaceEmptyValue(input)).toEqual(expected)\n  })\n})\n\ndescribe('getDbIndexFromSelectQuery', () => {\n  test.each(getDbIndexFromSelectQueryTests)('%j', ({ input, expected }) => {\n    if (expected instanceof Error) {\n      try {\n        getDbIndexFromSelectQuery(input)\n      } catch (e) {\n        expect(e.message).toEqual(expected.message)\n      }\n    } else {\n      expect(getDbIndexFromSelectQuery(input)).toEqual(expected)\n    }\n  })\n})\n\nconst getCommandNameFromQueryTests = [\n  { input: ['set foo bar', MOCK_COMMANDS_SPEC], expected: 'set' },\n  { input: ['  SET       foo bar', MOCK_COMMANDS_SPEC], expected: 'SET' },\n  { input: ['client kill 1', MOCK_COMMANDS_SPEC], expected: 'client kill' },\n  { input: ['client kill 1', {}], expected: 'client' },\n  {\n    input: ['custom.command foo bar', MOCK_COMMANDS_SPEC],\n    expected: 'custom.command',\n  },\n  { input: ['FT._LIST', MOCK_COMMANDS_SPEC], expected: 'FT._LIST' },\n  {\n    input: [\n      `${' '.repeat(20)} CLIENT ${' '.repeat(100)} KILL`,\n      MOCK_COMMANDS_SPEC,\n    ],\n    expected: 'CLIENT',\n  },\n  {\n    input: [\n      `${' '.repeat(20)} CLIENT ${' '.repeat(100)} KILL`,\n      MOCK_COMMANDS_SPEC,\n      500,\n    ],\n    expected: 'CLIENT KILL',\n  },\n  { input: [1], expected: undefined },\n]\n\nconst checkUnsupportedModuleCommandTests = [\n  { input: [[], 'FT.foo bar'], expected: RedisDefaultModules.Search },\n  {\n    input: [[{ name: RedisDefaultModules.Search }], 'foo bar'],\n    expected: null,\n  },\n  {\n    input: [[{ name: RedisDefaultModules.Search }], 'ft.foo bar'],\n    expected: null,\n  },\n  {\n    input: [[{ name: RedisDefaultModules.SearchLight }], 'ft.foo bar'],\n    expected: null,\n  },\n  {\n    input: [[{ name: RedisDefaultModules.FT }], ' FT.foo bar'],\n    expected: null,\n  },\n  {\n    input: [[{ name: RedisDefaultModules.FTL }], '  ft.foo bar'],\n    expected: null,\n  },\n]\n\nconst checkCommandModuleTests = [\n  { input: 'FT.foo bar', expected: RedisDefaultModules.Search },\n  { input: 'JSON.foo bar', expected: RedisDefaultModules.ReJSON },\n  { input: 'TS.foo bar', expected: RedisDefaultModules.TimeSeries },\n  { input: 'BF.foo bar', expected: RedisDefaultModules.Bloom },\n  { input: 'CF.foo bar', expected: RedisDefaultModules.Bloom },\n  { input: 'CMS.foo bar', expected: RedisDefaultModules.Bloom },\n  { input: 'TDIGEST.foo bar', expected: RedisDefaultModules.Bloom },\n  { input: 'TOPK.foo bar', expected: RedisDefaultModules.Bloom },\n  { input: 'FOO.foo bar', expected: null },\n]\n\nconst checkUnsupportedCommandTests = [\n  { input: [['FT'], 'FT.foo bar'], expected: 'FT' },\n  { input: [['FT'], ' ft.foo bar  '], expected: 'FT' },\n  { input: [['FOO', 'BAR'], 'FT.foo bar'], expected: undefined },\n]\n\nconst checkBlockingCommandTests = [\n  { input: [['ft'], 'FT.foo bar'], expected: 'ft' },\n  { input: [['ft'], ' ft.foo bar  '], expected: 'ft' },\n  { input: [['foo', 'bar'], 'FT.foo bar'], expected: undefined },\n]\n\nconst checkDeprecatedModuleCommandTests = [\n  { input: 'FT.foo bar', expected: false },\n  { input: 'GRAPH foo bar', expected: false },\n  { input: 'GRAPH.foo bar', expected: true },\n  { input: 'graph.foo bar', expected: true },\n  { input: 'FOO bar', expected: false },\n]\n\nconst removeDeprecatedModuleCommandsTests = [\n  { input: ['FT.foo'], expected: ['FT.foo'] },\n  { input: ['GRAPH.foo', 'FT.foo'], expected: ['FT.foo'] },\n  {\n    input: ['FOO', 'GRAPH.FOO', 'CF.FOO', 'GRAPH.BAR'],\n    expected: ['FOO', 'CF.FOO'],\n  },\n]\n\nconst checkDeprecatedCommandGroupTests = [\n  { input: 'cluster', expected: false },\n  { input: 'connection', expected: false },\n  { input: 'geo', expected: false },\n  { input: 'bitmap', expected: false },\n  { input: 'generic', expected: false },\n  { input: 'pubsub', expected: false },\n  { input: 'scripting', expected: false },\n  { input: 'transactions', expected: false },\n  { input: 'server', expected: false },\n  { input: 'sorted-set', expected: false },\n  { input: 'hyperloglog', expected: false },\n  { input: 'hash', expected: false },\n  { input: 'set', expected: false },\n  { input: 'stream', expected: false },\n  { input: 'list', expected: false },\n  { input: 'string', expected: false },\n  { input: 'search', expected: false },\n  { input: 'json', expected: false },\n  { input: 'timeseries', expected: false },\n  { input: 'graph', expected: true },\n  { input: 'ai', expected: false },\n  { input: 'tdigest', expected: false },\n  { input: 'cms', expected: false },\n  { input: 'topk', expected: false },\n  { input: 'bf', expected: false },\n  { input: 'cf', expected: false },\n]\n\nconst getUnsupportedModulesFromQueryTests: Array<{\n  input: [Array<any>, string]\n  expected: Set<string>\n}> = [\n  { input: [[], 'ft.info'], expected: new Set(['search']) },\n  { input: [[], 'bf.info'], expected: new Set(['bf']) },\n  { input: [[], 'bf.info \\nJSON.GET'], expected: new Set(['ReJSON', 'bf']) },\n  { input: [[{ name: 'search' }], 'ft.info'], expected: new Set([]) },\n]\n\ndescribe('getCommandNameFromQuery', () => {\n  test.each(getCommandNameFromQueryTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    expect(getCommandNameFromQuery(...input)).toEqual(expected)\n  })\n})\n\ndescribe('cliParseCommandsGroupResult success status', () => {\n  const mockResult = {\n    command: 'command',\n    response: 'response',\n    status: CommandExecutionStatus.Success,\n  }\n  const parsedResult = cliParseCommandsGroupResult(mockResult)\n  render(parsedResult)\n\n  expect(screen.queryByTestId('wb-command')).toBeInTheDocument()\n  expect(screen.getByText('> command')).toBeInTheDocument()\n  expect(\n    screen.queryByTestId(`${CliPrefix.Cli}-output-response-fail`),\n  ).not.toBeInTheDocument()\n  expect(parsedResult[1]).toEqual('\"response\"')\n})\n\ndescribe('cliParseCommandsGroupResult error status', () => {\n  const mockResult = {\n    command: 'command',\n    response: 'response',\n    status: CommandExecutionStatus.Fail,\n  }\n  render(cliParseCommandsGroupResult(mockResult))\n\n  expect(\n    screen.queryByTestId(`${CliPrefix.Cli}-output-response-fail`),\n  ).toBeInTheDocument()\n})\n\nconst wbSummaryCommandTests: any[] = [\n  ['SET', 0, '> SET'],\n  ['iueigc h pb32 ueo', 0, '> iueigc h pb32 ueo'],\n  ['SET', 1, '[db1] > SET'],\n  ['INFO', 10, '[db10] > INFO'],\n  ['aoeuaoeu', 10, '[db10] > aoeuaoeu'],\n]\n\ndescribe('wbSummaryCommand', () => {\n  it.each(wbSummaryCommandTests)(\n    'for input: %s (command), should be output: %s',\n    (command, db, expected) => {\n      const { container } = render(wbSummaryCommand(command, db))\n      expect(container).toHaveTextContent(expected)\n    },\n  )\n})\n\ndescribe('checkUnsupportedModuleCommand', () => {\n  test.each(checkUnsupportedModuleCommandTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    expect(checkUnsupportedModuleCommand(...input)).toEqual(expected)\n  })\n})\n\ndescribe('checkCommandModule', () => {\n  test.each(checkCommandModuleTests)('%j', ({ input, expected }) => {\n    expect(checkCommandModule(input)).toEqual(expected)\n  })\n})\n\ndescribe('checkUnsupportedCommand', () => {\n  test.each(checkUnsupportedCommandTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    expect(checkUnsupportedCommand(...input)).toEqual(expected)\n  })\n})\n\ndescribe('checkBlockingCommand', () => {\n  test.each(checkBlockingCommandTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    expect(checkBlockingCommand(...input)).toEqual(expected)\n  })\n})\n\ndescribe('checkDeprecatedModuleCommand', () => {\n  test.each(checkDeprecatedModuleCommandTests)('%j', ({ input, expected }) => {\n    expect(checkDeprecatedModuleCommand(input)).toEqual(expected)\n  })\n})\n\ndescribe('removeDeprecatedModuleCommands', () => {\n  test.each(removeDeprecatedModuleCommandsTests)(\n    '%j',\n    ({ input, expected }) => {\n      expect(removeDeprecatedModuleCommands(input)).toEqual(expected)\n    },\n  )\n})\n\ndescribe('checkDeprecatedCommandGroup', () => {\n  test.each(checkDeprecatedCommandGroupTests)('%j', ({ input, expected }) => {\n    expect(checkDeprecatedCommandGroup(input)).toEqual(expected)\n  })\n})\n\ndescribe('getUnsupportedModulesFromQuery', () => {\n  test.each(getUnsupportedModulesFromQueryTests)(\n    '%j',\n    ({ input, expected }) => {\n      expect(getUnsupportedModulesFromQuery(...input)).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/cliOutputActions.spec.ts",
    "content": "import { cloneDeep } from 'lodash'\nimport { AxiosError } from 'axios'\nimport { cleanup, clearStoreActions, mockedStore } from 'uiSrc/utils/test-utils'\nimport {\n  cliCommandError,\n  processUnrepeatableNumber,\n  processUnsupportedCommand,\n} from 'uiSrc/utils/cliOutputActions'\nimport { concatToOutput } from 'uiSrc/slices/cli/cli-output'\nimport { cliParseTextResponseWithOffset } from 'uiSrc/utils'\nimport { cliTexts } from 'uiSrc/components/messages/cli-output/cliOutput'\nimport { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'\nimport ApiErrors from 'uiSrc/constants/apiErrors'\nimport { store } from 'uiSrc/slices/store'\nimport { processCliClient } from 'uiSrc/slices/cli/cli-settings'\n\nlet storeActions: typeof mockedStore\nbeforeEach(() => {\n  cleanup()\n  storeActions = cloneDeep(mockedStore)\n  storeActions.clearActions()\n})\n\nconst unsupportedCommands: string[] = ['sync', 'subscription']\n\njest.mock('uiSrc/slices/store', () => ({\n  ...jest.requireActual('uiSrc/slices/store'),\n  store: mockedStore,\n}))\n\njest.mock('uiSrc/slices/cli/cli-settings', () => ({\n  ...jest.requireActual('uiSrc/slices/cli/cli-settings'),\n  cliUnsupportedCommandsSelector: jest\n    .fn()\n    .mockReturnValue(unsupportedCommands),\n}))\n\ndescribe('processUnsupportedCommand', () => {\n  it('should call proper actions', () => {\n    const command = 'sync'\n    processUnsupportedCommand(command, command)\n\n    const expectedActions = [\n      concatToOutput(\n        cliParseTextResponseWithOffset(\n          cliTexts.CLI_UNSUPPORTED_COMMANDS(\n            command,\n            unsupportedCommands.join(', '),\n          ),\n          command,\n          CommandExecutionStatus.Fail,\n        ),\n      ),\n    ]\n    expect(clearStoreActions(storeActions.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n})\n\ndescribe('processUnrepeatableNumber', () => {\n  it('should call proper actions', () => {\n    const command = 'sync'\n    processUnrepeatableNumber(command)\n\n    const expectedActions = [\n      concatToOutput(\n        cliParseTextResponseWithOffset(\n          cliTexts.REPEAT_COUNT_INVALID,\n          command,\n          CommandExecutionStatus.Fail,\n        ),\n      ),\n    ]\n    expect(clearStoreActions(storeActions.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n})\n\ndescribe('cliCommandError', () => {\n  it('should call proper actions without client uid', () => {\n    const command = 'sync'\n    const errorMessage = 'error'\n    const error = {\n      response: {\n        status: 500,\n        data: { message: errorMessage },\n      },\n    }\n    cliCommandError(error as AxiosError, command)\n\n    const expectedActions = [\n      concatToOutput(\n        cliParseTextResponseWithOffset(\n          errorMessage,\n          command,\n          CommandExecutionStatus.Fail,\n        ),\n      ),\n    ]\n    expect(clearStoreActions(storeActions.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n\n  it('should call proper actions with client uid', () => {\n    const command = 'sync'\n    const errorMessage = 'error'\n    const error = {\n      response: {\n        status: 500,\n        data: { message: errorMessage, name: ApiErrors.ClientNotFound },\n      },\n    }\n\n    store.getState = jest.fn().mockReturnValue({\n      ...mockedStore.getState(),\n      cli: { settings: { cliClientUuid: '123' } },\n    })\n\n    cliCommandError(error as AxiosError, command)\n\n    const expectedActions = [\n      concatToOutput(\n        cliParseTextResponseWithOffset(\n          cliTexts.CONNECTION_CLOSED,\n          command,\n          CommandExecutionStatus.Fail,\n        ),\n      ),\n      processCliClient(),\n    ]\n    expect(clearStoreActions(storeActions.getActions())).toEqual(\n      clearStoreActions(expectedActions),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/colors.spec.ts",
    "content": "import { ColorScheme, getRGBColorByScheme, rgb } from 'uiSrc/utils/colors'\n\nconst colorScheme: ColorScheme = {\n  cHueStart: 180,\n  cHueRange: 140,\n  cSaturation: 55,\n  cLightness: 45,\n}\n\nconst RGBColorsTests: any[] = [\n  // colors for length 3\n  [0, 0, [39, 135, 135]],\n  [1, 140 / 3, [66, 101, 226]],\n  [2, 140 / 3, [143, 60, 208]],\n\n  // other colors\n  [1, 140 / 3, [66, 101, 226]],\n  [2, 140 / 4, [101, 72, 248]],\n  [3, 140 / 5, [129, 65, 224]],\n  [4, 140 / 6, [143, 60, 208]],\n  [5, 140 / 7, [151, 57, 197]],\n]\n\ndescribe('getRGBColorByScheme', () => {\n  it.each(RGBColorsTests)(\n    'for input: %s (index), %s (shift), should be output: %s',\n    (index, shift, expected) => {\n      const result = getRGBColorByScheme(index, shift, colorScheme)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n\ndescribe('rgb', () => {\n  it('should return proper rgb string color', () => {\n    expect(rgb([0, 0, 0])).toEqual('rgb(0, 0, 0)')\n    expect(rgb([100, 30, 10])).toEqual('rgb(100, 30, 10)')\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/commands.spec.ts",
    "content": "import {\n  ICommandArgGenerated,\n  ICommands,\n  MOCK_COMMANDS_SPEC,\n} from 'uiSrc/constants'\nimport { getUtmExternalLink } from 'uiSrc/utils/links'\nimport {\n  generateArgs,\n  generateArgsNames,\n  getComplexityShortNotation,\n  getDocUrlForCommand,\n  generateRedisCommand,\n} from '../commands'\nimport { cleanup } from '../test-utils'\n\nconst ALL_REDIS_COMMANDS: ICommands = MOCK_COMMANDS_SPEC\n\ninterface IMockedCommands {\n  matchedCommand: string\n  argStr?: string\n  argsNamesWithEnumsMock?: string[]\n  argsNamesMock?: (string | string[])[]\n  complexityShortMock?: string\n}\n\nbeforeEach(() => {\n  cleanup()\n})\n\nconst mockedCommands: IMockedCommands[] = [\n  {\n    matchedCommand: 'xgroup',\n    argStr: 'XGROUP',\n    argsNamesWithEnumsMock: [],\n    argsNamesMock: [],\n    complexityShortMock: 'O(1)',\n  },\n  {\n    matchedCommand: 'hset',\n    argStr: 'HSET key field value [field value ...]',\n    argsNamesWithEnumsMock: ['key', 'field value [field value ...]'],\n    argsNamesMock: ['key', 'field value'],\n    complexityShortMock: 'O(1)',\n  },\n  {\n    matchedCommand: 'acl setuser',\n    argStr: 'ACL SETUSER username [rule [rule ...]]',\n    argsNamesWithEnumsMock: ['username', '[rule [rule ...]]'],\n    argsNamesMock: ['username', '[rule]'],\n    complexityShortMock: 'O(N)',\n  },\n  {\n    matchedCommand: 'bitfield',\n    argStr:\n      'BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]',\n    argsNamesWithEnumsMock: [\n      'key',\n      '[GET encoding offset | [OVERFLOW WRAP | SAT | FAIL] SET encoding offset value | INCRBY encoding offset increment [GET encoding offset | [OVERFLOW WRAP | SAT | FAIL] SET encoding offset value | INCRBY encoding offset increment ...]]',\n    ],\n    argsNamesMock: [\n      'key',\n      '[GET encoding offset | [OVERFLOW WRAP | SAT | FAIL] SET encoding offset value | INCRBY encoding offset increment]',\n    ],\n    complexityShortMock: 'O(1)',\n  },\n  {\n    matchedCommand: 'client kill',\n    argStr:\n      'CLIENT KILL [ip:port] [ID client-id] [TYPE normal|master|slave|pubsub] [USER username] [ADDR ip:port] [LADDR ip:port] [SKIPME yes/no]',\n    argsNamesWithEnumsMock: [\n      'ip:port | [ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] [[ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] ...]',\n    ],\n    argsNamesMock: [\n      'ip:port | [ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] [[ID client-id] | [TYPE NORMAL | MASTER | SLAVE | REPLICA | PUBSUB] | [USER username] | [ADDR ip:port] | [LADDR ip:port] | [SKIPME YES | NO] ...]',\n    ],\n    complexityShortMock: 'O(N)',\n  },\n  {\n    matchedCommand: 'geoadd',\n    argStr:\n      'GEOADD key [NX|XX] [CH] longitude latitude member [longitude latitude member ...]',\n    argsNamesWithEnumsMock: [\n      'key',\n      '[NX | XX]',\n      '[CH]',\n      'longitude latitude member [longitude latitude member ...]',\n    ],\n    argsNamesMock: ['key', '[NX | XX]', '[CH]', 'longitude latitude member'],\n    complexityShortMock: 'O(log(N))',\n  },\n  {\n    matchedCommand: 'zadd',\n    argStr:\n      'ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]',\n    argsNamesWithEnumsMock: [\n      'key',\n      '[NX | XX]',\n      '[GT | LT]',\n      '[CH]',\n      '[INCR]',\n      'score member [score member ...]',\n    ],\n    argsNamesMock: [\n      'key',\n      '[NX | XX]',\n      '[GT | LT]',\n      '[CH]',\n      '[INCR]',\n      'score member',\n    ],\n    complexityShortMock: 'O(log(N))',\n  },\n]\n\ndescribe('getComplexityShortNotation', () => {\n  it('Complexity short should return text according mocked data', () => {\n    mockedCommands.forEach(({ matchedCommand = '', complexityShortMock }) => {\n      const complexity =\n        ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.complexity ?? ''\n      const complexityShort = getComplexityShortNotation(complexity)\n\n      if (complexityShort) {\n        expect(complexityShort).toEqual(complexityShortMock)\n      } else {\n        expect(complexityShort).toEqual('')\n      }\n    })\n  })\n  it('handle case when complexity is array of strings', () => {\n    const result = getComplexityShortNotation([\n      'O(1) for each field/value pair added',\n      'O(N) to add N field/value pairs when the command is called with multiple field/value pairs.',\n    ])\n\n    expect(result).toEqual('')\n  })\n})\n\ndescribe('generateArgs', () => {\n  it('generateArgs short should return argument with GeneratedName (with Enums names)', () => {\n    mockedCommands.forEach(({ matchedCommand = '', argsNamesMock = [] }) => {\n      const argsInit =\n        ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.arguments ?? []\n\n      const argsMocked: ICommandArgGenerated[] = argsInit.map((arg, i) => ({\n        ...arg,\n        generatedName: argsNamesMock[i] ?? '',\n      }))\n\n      const args = generateArgs(\n        ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.provider,\n        argsInit,\n      )\n\n      expect(args).toEqual(argsMocked)\n    })\n  })\n})\n\ndescribe('generateArgName', () => {\n  it('Arguments names should return text according mocked data (with Enums values)', () => {\n    mockedCommands.forEach(\n      ({ matchedCommand = '', argsNamesWithEnumsMock }) => {\n        const args =\n          ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.arguments ?? []\n\n        const generatedArgNames = generateArgsNames(\n          ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.provider,\n          args,\n        )\n        expect(generatedArgNames).toEqual(argsNamesWithEnumsMock)\n      },\n    )\n  })\n  it('Arguments names should return text according mocked data (with Enums names)', () => {\n    mockedCommands.forEach(({ matchedCommand = '', argsNamesMock }) => {\n      const args =\n        ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.arguments ?? []\n\n      const generatedArgNames = generateArgsNames(\n        ALL_REDIS_COMMANDS[matchedCommand?.toUpperCase()]?.provider,\n        args,\n        true,\n      )\n      expect(generatedArgNames).toEqual(argsNamesMock)\n    })\n  })\n})\n\nconst getDocUrlForCommandTests: any[] = [\n  ['SET', 'https://redis.io/docs/latest/commands/set'],\n  ['ACL SETUSER', 'https://redis.io/docs/latest/commands/acl-setuser'],\n  ['JSON.GET', 'https://redis.io/docs/latest/commands/json.get'],\n  ['FT.CREATE', 'https://redis.io/docs/latest/commands/ft.create'],\n  ['FT.ALTER', 'https://redis.io/docs/latest/commands/ft.alter'],\n  ['TS.ADD', 'https://redis.io/docs/latest/commands/ts.add'],\n  ['TS.CREATE', 'https://redis.io/docs/latest/commands/ts.create'],\n  ['GRAPH.EXPLAIN', 'https://redis.io/docs/latest/commands/graph.explain'],\n  ['GRAPH.QUERY', 'https://redis.io/docs/latest/commands/graph.query'],\n  ['BF.INFO', 'https://redis.io/docs/latest/commands/bf.info'],\n  ['CMS.INITBYDIM', 'https://redis.io/docs/latest/commands/cms.initbydim'],\n  ['CF.INSERT', 'https://redis.io/docs/latest/commands/cf.insert'],\n  ['RG.CONFIGSET', 'https://redis.io/docs/latest/commands/rg.configset'],\n  ['TOPK.INFO', 'https://redis.io/docs/latest/commands/topk.info'],\n  [\n    'NON.EXIST COMMAND',\n    'https://redis.io/docs/latest/commands/non.exist-command',\n  ],\n]\n\ndescribe('getDocUrlForCommand', () => {\n  it.each(getDocUrlForCommandTests)(\n    'for input: %s (command), should be output: %s',\n    (command, expected) => {\n      const result = getDocUrlForCommand(command)\n      expect(result).toBe(\n        getUtmExternalLink(expected, {\n          campaign: 'redisinsight_command_helper',\n        }),\n      )\n    },\n  )\n})\n\nconst generateRedisCommandTests = [\n  {\n    input: ['info'],\n    output: 'info',\n  },\n  {\n    input: ['set', ['a', 'b']],\n    output: 'set \"a\" \"b\"',\n  },\n  {\n    input: ['set', 'a', 'b'],\n    output: 'set \"a\" \"b\"',\n  },\n  {\n    input: ['command', ['a', 'b'], ['b', 'b'], 0, 'a', 'a b c'],\n    output: 'command \"a\" \"b\" \"b\" \"b\" \"0\" \"a\" \"a b c\"',\n  },\n]\n\ndescribe('generateRedisCommand', () => {\n  it.each(generateRedisCommandTests)(\n    'for input: %s (input), should be output: %s',\n    ({ input, output }) => {\n      const result = generateRedisCommand(...input)\n      expect(result).toBe(output)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/common.spec.tsx",
    "content": "import React from 'react'\nimport { getNodeText, removeSymbolsFromStart } from '../common'\n\nconst getOutputForGetNodeTextTests: any[] = [\n  [5, '5'],\n  ['5', '5'],\n  ['test test', 'test test'],\n  [123_4324_432, '1234324432'],\n  [<div>123</div>, '123'],\n  [\n    <div>\n      <span>222</span>\n    </div>,\n    '222',\n  ],\n  [\n    <div>\n      <span>111</span>\n      <span>222</span>\n    </div>,\n    '111222',\n  ],\n]\n\ndescribe('getNodeText', () => {\n  it.each(getOutputForGetNodeTextTests)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = getNodeText(reply)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\nconst getOutputForRemoveSymbolsFromStartTests: any[] = [\n  ['', '1', ''],\n  ['5', '5', ''],\n  ['5', '1', '5'],\n  ['test test', 'test', ' test'],\n  ['/guides/tutorial', '/', 'guides/tutorial'],\n  ['123123/guides/tutorial', '123123', '/guides/tutorial'],\n]\n\ndescribe('removeSymbolsFromStart', () => {\n  it.each(getOutputForRemoveSymbolsFromStartTests)(\n    'for input: %s (string), %s (symbols), should be output: %s',\n    (string, symbols, expected) => {\n      const result = removeSymbolsFromStart(string, symbols)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/comparisons/bigKeys.spec.ts",
    "content": "import { KeyTypes } from 'uiSrc/constants'\nimport { HighlightType, isBigKey } from 'uiSrc/utils'\n\nconst isBigKeyTests: any[] = [\n  [KeyTypes.Hash, HighlightType.Memory, 100, false],\n  [KeyTypes.Hash, HighlightType.Memory, 5_000_000, true],\n  [KeyTypes.Hash, HighlightType.Length, 50_000_000, true],\n  [KeyTypes.String, HighlightType.Memory, 50_000_000, true],\n  [KeyTypes.String, HighlightType.Length, 50_000_000, false],\n  [KeyTypes.Stream, HighlightType.Memory, 50_000_000, true],\n  [KeyTypes.Stream, HighlightType.Length, 50_000_000, false],\n  [KeyTypes.Stream, HighlightType.Memory, 199, false],\n  ['newType', HighlightType.Memory, 98391283123123, true],\n  ['newType', HighlightType.Length, 98391283123123, false],\n]\n\ndescribe('isBigKey', () => {\n  it.each(isBigKeyTests)(\n    'for input: %s (keyType), %s (type), %s (count) should be output: %s',\n    (keyType, type, count, expected) => {\n      const result = isBigKey(keyType, type, count)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/comparisons/compareConsents.spec.ts",
    "content": "import { compareConsents, isDifferentConsentsExists } from 'uiSrc/utils'\n\nconst spec = {\n  agreements: {\n    eula: {\n      defaultValue: false,\n      required: true,\n      editable: false,\n      since: '1.0.2',\n      title: 'EULA: Redis Insight License Terms',\n      label: 'Label',\n    },\n  },\n}\n\ndescribe('compareConsents', () => {\n  it('compareConsents should return array of difference of consents', () => {\n    const agreements1 = {\n      eula: true,\n      version: '1.0.2',\n    }\n\n    const agreements2 = {\n      eula: true,\n      eulaNew: false,\n      version: '1.0.2',\n    }\n\n    const agreements3 = {\n      eula: false,\n      version: '1.0.0',\n    }\n\n    expect(compareConsents(spec.agreements, agreements1)).toHaveLength(0)\n    expect(compareConsents(spec.agreements, agreements2)).toHaveLength(0)\n    expect(compareConsents(spec.agreements, agreements3)).toHaveLength(1)\n  })\n})\n\ndescribe('isDifferentConsentsExists', () => {\n  it('isDifferentConsentsExists should return true if some difference in consents', () => {\n    const agreements1 = {\n      eula: true,\n      version: '1.0.2',\n    }\n\n    const agreements2 = {\n      eula: true,\n      eulaNew: false,\n      version: '1.0.2',\n    }\n\n    const agreements3 = {\n      eula: false,\n      version: '1.0.0',\n    }\n\n    expect(isDifferentConsentsExists(spec.agreements, agreements1)).toBeFalsy()\n    expect(isDifferentConsentsExists(spec.agreements, agreements2)).toBeFalsy()\n    expect(isDifferentConsentsExists(spec.agreements, agreements3)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/comparisons/compareVersions.spec.ts",
    "content": "import {\n  isVersionHigherOrEquals,\n  isVersionHigher,\n  isRedisVersionSupported,\n} from 'uiSrc/utils'\n\ndescribe('isVersionHigherOrEqual', () => {\n  it('isVersionHigherOrEqual should return true if the first version provided is higher or equal', () => {\n    const version1 = '6.2'\n    const version2 = '6.2.9'\n    const version3 = '6.2.1'\n    const version4 = '6.1.9'\n    const version5 = '1.2'\n    const version6 = '10.2'\n    const version7 = '6.2.10'\n    const version8 = '10'\n    const version9 = '10.0.0'\n\n    expect(isVersionHigherOrEquals(version1, version1)).toBeTruthy()\n    expect(isVersionHigherOrEquals(version2, '6.2')).toBeTruthy()\n    expect(isVersionHigherOrEquals(version3, '6.2.4')).toBeFalsy()\n    expect(isVersionHigherOrEquals(version4, '6.2')).toBeFalsy()\n    expect(isVersionHigherOrEquals(version5, '6.2')).toBeFalsy()\n    expect(isVersionHigherOrEquals(version6, '6.2')).toBeTruthy()\n    expect(isVersionHigherOrEquals(version7, '6.2')).toBeTruthy()\n    expect(isVersionHigherOrEquals(version8, '6.2')).toBeTruthy()\n    expect(isVersionHigherOrEquals(version9, '6.2')).toBeTruthy()\n  })\n})\n\ndescribe('isVersionHigher', () => {\n  it('isVersionHigher should return true if the first version provided is higher', () => {\n    const version1 = '6.2'\n    const version2 = '6.2.9'\n    const version3 = '6.2.1'\n    const version4 = '6.1.9'\n    const version5 = '1.2'\n    const version6 = '10.2'\n    const version7 = '6.2.10'\n    const version8 = '10'\n    const version9 = '10.0.0'\n\n    expect(isVersionHigher(version1, version1)).toBeFalsy()\n    expect(isVersionHigher(version2, '6.2')).toBeTruthy()\n    expect(isVersionHigher(version3, '6.2.4')).toBeFalsy()\n    expect(isVersionHigher(version4, '6.2')).toBeFalsy()\n    expect(isVersionHigher(version5, '6.2')).toBeFalsy()\n    expect(isVersionHigher(version6, '6.2')).toBeTruthy()\n    expect(isVersionHigher(version7, '6.2')).toBeTruthy()\n    expect(isVersionHigher(version8, '6.2')).toBeTruthy()\n    expect(isVersionHigher(version9, '6.2')).toBeTruthy()\n  })\n})\n\ndescribe('useRedisInstanceCompatibility', () => {\n  const MOCK_MIN_SUPPORTED_REDIS_VERSION = '7.2.0'\n\n  test.each([\n    [undefined, false],\n    [null, false],\n    ['', false],\n    ['7.1.9', false],\n    ['7.2', true],\n    ['v7.2', true],\n    ['7.2.0', true],\n    ['7.2.0+build.5', true],\n    ['7.2.0-rc.1', false], // prerelease should NOT satisfy >=7.2.0\n    ['Redis 7.2-rc1 (abc)', true], // coerced to 7.2.0 -> true (note: prerelease info lost)\n    ['Redis 6 something', false],\n    ['nonsense', false],\n  ])('isRedisVersionSupported(%p) === %p', (input, expected) => {\n    expect(\n      isRedisVersionSupported(input as any, MOCK_MIN_SUPPORTED_REDIS_VERSION),\n    ).toBe(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/comparisons/diff.spec.ts",
    "content": "import { getDiffKeysOfObjectValues, getFormUpdates } from 'uiSrc/utils'\n\nconst getDiffKeysOfObjectValuesTests: any[] = [\n  [{}, {}, []],\n  [{ key1: '1' }, { key1: '2' }, ['key1']],\n  [{ key1: '1' }, { key2: 2 }, ['key1']],\n  [{}, { key2: '1' }, []],\n  [{ key1: 1 }, {}, ['key1']],\n]\n\ndescribe('getDiffKeysOfObjectValues', () => {\n  it.each(getDiffKeysOfObjectValuesTests)(\n    'for input: %s, %s should be output: %s',\n    (obj1, obj2, expected) => {\n      const result = getDiffKeysOfObjectValues(obj1, obj2)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n\nconst getFormUpdatesTests: any[] = [\n  [{ name: 'name' }, { name: 'name' }, {}],\n  [{ name: '' }, { name: 'name' }, { name: '' }],\n  [{ name: 'name' }, { name: '' }, { name: 'name' }],\n  [{ name: 'name', port: 123 }, { name: '', port: 123 }, { name: 'name' }],\n  [\n    { name: 'name', port: 123 },\n    { name: '', port: 1 },\n    { name: 'name', port: 123 },\n  ],\n  [\n    { name: 'name', sshOptions: { password: 123 } },\n    { name: '', sshOptions: { password: 123 } },\n    { name: 'name' },\n  ],\n  [\n    { name: 'name', sshOptions: { password: 123, name: 'custom' } },\n    { name: '', sshOptions: { password: 1, name: 'custom' } },\n    { name: 'name', sshOptions: { password: 123 } },\n  ],\n  [\n    {\n      name: 'name',\n      sshOptions: { password: 123, name: 'custom' },\n      nested: { plain: '1', obj2: { plain1: 2, plain2: '2' } },\n    },\n    {\n      name: 'name1',\n      sshOptions: { password: 123, name: 'custom' },\n      nested: { plain: '2', obj2: { plain1: 2, plain2: '3' } },\n    },\n    { name: 'name', nested: { plain: '1', obj2: { plain2: '2' } } },\n  ],\n  [{ arr: [1, 2] }, { arr: [2] }, { arr: [1, 2] }],\n]\n\ndescribe('getFormUpdates', () => {\n  it.each(getFormUpdatesTests)(\n    'for input: %s (obj), %s (previous), should be output: %s',\n    (obj, previous, expected) => {\n      const result = getFormUpdates(obj, previous)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/decompressors/constants.ts",
    "content": "export const DECOMPRESSED_VALUE_1 = [49]\nexport const DECOMPRESSED_VALUE_2 = [50]\nexport const DECOMPRESSED_VALUE_STR_1 = '1'\nexport const DECOMPRESSED_VALUE_STR_2 = '2'\n\nexport const GZIP_COMPRESSED_VALUE_1 = [\n  31, 139, 8, 0, 223, 246, 236, 99, 0, 3, 1, 1, 0, 254, 255, 49, 183, 239, 220,\n  131, 1, 0, 0, 0,\n]\nexport const GZIP_COMPRESSED_VALUE_2 = [\n  31, 139, 8, 0, 180, 246, 236, 99, 0, 3, 1, 1, 0, 254, 255, 50, 13, 190, 213,\n  26, 1, 0, 0, 0,\n]\n\nexport const ZSTD_COMPRESSED_VALUE_1 = [40, 181, 47, 253, 32, 1, 9, 0, 0, 49]\nexport const ZSTD_COMPRESSED_VALUE_2 = [40, 181, 47, 253, 32, 1, 9, 0, 0, 50]\n\nexport const LZ4_COMPRESSED_VALUE_1 = [\n  4, 34, 77, 24, 64, 112, 223, 1, 0, 0, 128, 49, 0, 0, 0, 0,\n]\nexport const LZ4_COMPRESSED_VALUE_2 = [\n  4, 34, 77, 24, 64, 112, 223, 1, 0, 0, 128, 50, 0, 0, 0, 0,\n]\n\nexport const SNAPPY_COMPRESSED_VALUE_1 = [1, 0, 49]\nexport const SNAPPY_COMPRESSED_VALUE_2 = [1, 0, 50]\n\nexport const BROTLI_COMPRESSED_VALUE_1 = [11, 0, 128, 49, 3]\nexport const BROTLI_COMPRESSED_VALUE_2 = [11, 0, 128, 50, 3]\n\nexport const PHPGZCOMPRESS_COMPRESSED_VALUE_1 = [\n  120, 156, 51, 4, 0, 0, 50, 0, 50,\n]\nexport const PHPGZCOMPRESS_COMPRESSED_VALUE_2 = [\n  120, 156, 51, 2, 0, 0, 51, 0, 51,\n]\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/decompressors/decompressors.spec.ts",
    "content": "import { toNumber } from 'lodash'\nimport { COMPRESSOR_MAGIC_SYMBOLS, KeyValueCompressor } from 'uiSrc/constants'\nimport { UintArray } from 'uiSrc/slices/interfaces'\nimport { decompressingBuffer, getCompressor } from 'uiSrc/utils/decompressors'\nimport { anyToBuffer } from 'uiSrc/utils/formatters'\nimport {\n  GZIP_COMPRESSED_VALUE_1,\n  GZIP_COMPRESSED_VALUE_2,\n  DECOMPRESSED_VALUE_1,\n  DECOMPRESSED_VALUE_2,\n  DECOMPRESSED_VALUE_STR_1,\n  DECOMPRESSED_VALUE_STR_2,\n  ZSTD_COMPRESSED_VALUE_1,\n  ZSTD_COMPRESSED_VALUE_2,\n  LZ4_COMPRESSED_VALUE_1,\n  LZ4_COMPRESSED_VALUE_2,\n  SNAPPY_COMPRESSED_VALUE_2,\n  SNAPPY_COMPRESSED_VALUE_1,\n  PHPGZCOMPRESS_COMPRESSED_VALUE_1,\n  PHPGZCOMPRESS_COMPRESSED_VALUE_2,\n} from './constants'\n\nconst defaultValues = [\n  {\n    input: [49],\n    compressor: null,\n    output: [49],\n    outputStr: '1',\n    isCompressed: false,\n  },\n  {\n    input: [49, 50],\n    compressor: null,\n    output: [49, 50],\n    outputStr: '12',\n    isCompressed: false,\n  },\n  {\n    input: COMPRESSOR_MAGIC_SYMBOLS[KeyValueCompressor.GZIP]\n      .split(',')\n      .map((symbol) => toNumber(symbol)),\n    compressor: null,\n    output: [31, 139],\n    outputStr: '\\\\x1f\\\\x8b',\n    isCompressed: false,\n  },\n  {\n    input: COMPRESSOR_MAGIC_SYMBOLS[KeyValueCompressor.ZSTD]\n      .split(',')\n      .map((symbol) => toNumber(symbol)),\n    compressor: null,\n    output: [40, 181, 47, 253],\n    outputStr: '(\\\\xb5/\\\\xfd',\n    isCompressed: false,\n  },\n  {\n    input: GZIP_COMPRESSED_VALUE_1,\n    compressor: KeyValueCompressor.GZIP,\n    output: DECOMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    isCompressed: true,\n  },\n  {\n    input: GZIP_COMPRESSED_VALUE_2,\n    compressor: KeyValueCompressor.GZIP,\n    output: DECOMPRESSED_VALUE_2,\n    outputStr: DECOMPRESSED_VALUE_STR_2,\n    isCompressed: true,\n  },\n  {\n    input: ZSTD_COMPRESSED_VALUE_1,\n    compressor: KeyValueCompressor.ZSTD,\n    output: DECOMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    isCompressed: true,\n  },\n  {\n    input: ZSTD_COMPRESSED_VALUE_2,\n    compressor: KeyValueCompressor.ZSTD,\n    output: DECOMPRESSED_VALUE_2,\n    outputStr: DECOMPRESSED_VALUE_STR_2,\n    isCompressed: true,\n  },\n  {\n    input: LZ4_COMPRESSED_VALUE_1,\n    compressor: KeyValueCompressor.LZ4,\n    output: DECOMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    isCompressed: true,\n  },\n  {\n    input: LZ4_COMPRESSED_VALUE_2,\n    compressor: KeyValueCompressor.LZ4,\n    output: DECOMPRESSED_VALUE_2,\n    outputStr: DECOMPRESSED_VALUE_STR_2,\n    isCompressed: true,\n  },\n  {\n    input: SNAPPY_COMPRESSED_VALUE_1,\n    compressor: KeyValueCompressor.SNAPPY,\n    compressorInit: KeyValueCompressor.SNAPPY,\n    output: DECOMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    isCompressed: true,\n  },\n  {\n    input: SNAPPY_COMPRESSED_VALUE_2,\n    compressor: KeyValueCompressor.SNAPPY,\n    compressorInit: KeyValueCompressor.SNAPPY,\n    output: DECOMPRESSED_VALUE_2,\n    outputStr: DECOMPRESSED_VALUE_STR_2,\n    isCompressed: true,\n  },\n  {\n    input: GZIP_COMPRESSED_VALUE_1,\n    compressor: null,\n    output: GZIP_COMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    compressorInit: KeyValueCompressor.LZ4,\n    compressorByValue: KeyValueCompressor.GZIP,\n    isCompressed: false,\n  },\n  {\n    input: ZSTD_COMPRESSED_VALUE_1,\n    compressor: null,\n    compressorInit: KeyValueCompressor.LZ4,\n    compressorByValue: KeyValueCompressor.ZSTD,\n    output: ZSTD_COMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    isCompressed: false,\n  },\n  // TODO: Skipped: Requires significant time to fix WASM issues for Jest. Story to fix tests #RI-6565\n  // {\n  //   input: BROTLI_COMPRESSED_VALUE_1,\n  //   compressor: KeyValueCompressor.Brotli,\n  //   compressorInit: KeyValueCompressor.Brotli,\n  //   output: DECOMPRESSED_VALUE_1,\n  //   outputStr: DECOMPRESSED_VALUE_STR_1,\n  //   isCompressed: true,\n  // },\n  // {\n  //   input: BROTLI_COMPRESSED_VALUE_2,\n  //   compressor: KeyValueCompressor.Brotli,\n  //   compressorInit: KeyValueCompressor.Brotli,\n  //   output: DECOMPRESSED_VALUE_2,\n  //   outputStr: DECOMPRESSED_VALUE_STR_2,\n  //   isCompressed: true,\n  // },\n  {\n    input: PHPGZCOMPRESS_COMPRESSED_VALUE_1,\n    compressor: KeyValueCompressor.PHPGZCompress,\n    compressorInit: KeyValueCompressor.PHPGZCompress,\n    output: DECOMPRESSED_VALUE_1,\n    outputStr: DECOMPRESSED_VALUE_STR_1,\n    isCompressed: true,\n  },\n  {\n    input: PHPGZCOMPRESS_COMPRESSED_VALUE_2,\n    compressor: KeyValueCompressor.PHPGZCompress,\n    compressorInit: KeyValueCompressor.PHPGZCompress,\n    output: DECOMPRESSED_VALUE_2,\n    outputStr: DECOMPRESSED_VALUE_STR_2,\n    isCompressed: true,\n  },\n].map((value) => ({\n  ...value,\n  input: anyToBuffer(value.input),\n}))\n\ndescribe('getCompressor', () => {\n  test.each(defaultValues)(\n    '%j',\n    ({ input, compressor, compressorByValue = null }) => {\n      let expected = compressorByValue || compressor\n\n      // SNAPPY doesn't have magic symbols\n      if (\n        compressor === KeyValueCompressor.SNAPPY ||\n        compressor === KeyValueCompressor.Brotli ||\n        compressor === KeyValueCompressor.PHPGZCompress\n      ) {\n        expected = null\n      }\n\n      const result = getCompressor(input)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n\ndescribe('decompressingBuffer', () => {\n  test.each(defaultValues)(\n    '%j',\n    ({ input, compressor, output, compressorInit = null, isCompressed }) => {\n      const result = decompressingBuffer(input, compressorInit || compressor)\n      let value: UintArray = output\n\n      if (compressor) {\n        value = new Uint8Array(output)\n      }\n\n      expect(result).toEqual({\n        value: anyToBuffer(value),\n        compressor,\n        isCompressed,\n      })\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/decompressors/index.ts",
    "content": "export * from './constants'\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/dom/downloadFile.spec.ts",
    "content": "import { saveAs } from 'file-saver'\nimport { DEFAULT_FILE_NAME, downloadFile } from 'uiSrc/utils/dom/downloadFile'\n\njest.mock('file-saver', () => ({\n  ...jest.requireActual('file-saver'),\n  saveAs: jest.fn(),\n}))\n\nconst getDownloadFileTests: any[] = [\n  ['5123123123', { 'content-disposition': '123\"123\"123' }, '123'],\n  [\n    'test\\ntest123',\n    { 'content-disposition': '123\"filename.txt\"123' },\n    'filename.txt',\n  ],\n  [\n    '5123 uoeu aoue ao123123',\n    { 'content-disposition': '123\"1uaoeutaoeu\"123' },\n    '1uaoeutaoeu',\n  ],\n  [null, { 'content-disposition': '123\"123\"123' }, '123'],\n  ['5123 3', {}, DEFAULT_FILE_NAME],\n]\n\ndescribe('downloadFile', () => {\n  it.each(getDownloadFileTests)(\n    'saveAs should be called with: %s (data), %s (headers), ',\n    (data: string, headers, fileName: string) => {\n      const saveAsMock = jest.fn()\n      ;(saveAs as jest.Mock).mockImplementation(() => saveAsMock)\n\n      downloadFile(data, headers)\n      expect(saveAs).toBeCalledWith(\n        new Blob([data], { type: 'text/plain;charset=utf-8' }),\n        fileName,\n      )\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/dom/handlePlatform.spec.ts",
    "content": "import { isMacOs } from 'uiSrc/utils'\n\ndescribe('isMacOs', () => {\n  const originalPlatform = navigator.platform\n  afterAll(() => {\n    Object.defineProperty(navigator, 'platform', {\n      value: originalPlatform,\n      writable: false,\n    })\n  })\n  it('should return false for Linux', () => {\n    Object.defineProperty(navigator, 'platform', {\n      value: 'Linux',\n      writable: true,\n    })\n    expect(isMacOs()).toBeFalsy()\n  })\n  it('should return true for MacIntel', () => {\n    Object.defineProperty(navigator, 'platform', {\n      value: 'MacIntel',\n      writable: true,\n    })\n    expect(isMacOs()).toBeTruthy()\n  })\n  it('should return true for MacPPC', () => {\n    Object.defineProperty(navigator, 'platform', {\n      value: 'MacPPC',\n      writable: true,\n    })\n    expect(isMacOs()).toBeTruthy()\n  })\n  it('should return false for Windows', () => {\n    Object.defineProperty(navigator, 'platform', {\n      value: 'Windows',\n      writable: true,\n    })\n    expect(isMacOs()).toBeFalsy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/dom/scrollIntoView.spec.ts",
    "content": "import { scrollIntoView } from 'uiSrc/utils'\n\ndescribe('scrollIntoView', () => {\n  it('should called with options for all browser except Safari', () => {\n    const mockScrollIntoView = jest.fn()\n    const opts: ScrollIntoViewOptions = {\n      behavior: 'smooth',\n      inline: 'end',\n      block: 'nearest',\n    }\n    const newDiv = document.createElement('div')\n    newDiv.scrollIntoView = mockScrollIntoView\n    scrollIntoView(newDiv, opts)\n    expect(mockScrollIntoView).toBeCalledWith(opts)\n  })\n\n  it('should called with \"true\" instead of options for Safari', () => {\n    const mockScrollIntoView = jest.fn()\n    const opts: ScrollIntoViewOptions = {\n      behavior: 'smooth',\n      inline: 'end',\n      block: 'nearest',\n    }\n\n    Object.defineProperty(global.document.documentElement, 'style', {\n      value: {},\n    })\n    const newDiv = document.createElement('div')\n    newDiv.scrollIntoView = mockScrollIntoView\n    scrollIntoView(newDiv, opts)\n    expect(mockScrollIntoView).toBeCalledWith(true)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/errors.spec.tsx",
    "content": "import { set, cloneDeep } from 'lodash'\nimport React from 'react'\nimport { AxiosError } from 'axios'\nimport { parseCustomError, getRdiValidationMessage, Maybe } from 'uiSrc/utils'\nimport { CustomError } from 'uiSrc/slices/interfaces'\nimport { CustomErrorCodes } from 'uiSrc/constants'\nimport { EXTERNAL_LINKS } from 'uiSrc/constants/links'\nimport { Spacer } from 'uiSrc/components/base/layout/spacer'\n\nconst responseData = { response: { data: {}, status: 500 } }\n\nconst parseCustomErrorTests = [\n  [\n    undefined,\n    set(cloneDeep(responseData), 'response.data', {\n      message: 'Something was wrong!',\n    }),\n  ],\n  ['', set(cloneDeep(responseData), 'response.data', { message: '' })],\n  ['test', set(cloneDeep(responseData), 'response.data', { message: 'test' })],\n  [\n    { errorCode: 11_003 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Bad request',\n      message: (\n        <>\n          Your request resulted in an error.\n          <Spacer size=\"xs\" />\n          Try again later.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: 11_002 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Access denied',\n      message: <>You do not have permission to access Redis Cloud.</>,\n    }),\n  ],\n  [\n    { errorCode: 11_000 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Server error',\n      message: (\n        <>\n          Try restarting Redis Insight.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: 11_004 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Resource was not found',\n      message: (\n        <>\n          Resource requested could not be found.\n          <Spacer size=\"xs\" />\n          Try again later.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: 11_001 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Session expired',\n      message: (\n        <>\n          Sign in again to continue working with Redis Cloud.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: 11_021 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Session expired',\n      message: (\n        <>\n          Sign in again to continue working with Redis Cloud.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: CustomErrorCodes.CloudOauthGithubEmailPermission },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Github Email Permission',\n      message: (\n        <>\n          Unable to get an email from the GitHub account. Make sure that it is\n          available.\n          <br />\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: CustomErrorCodes.CloudOauthMisconfiguration },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Misconfiguration',\n      message: (\n        <>\n          Authorization server encountered a misconfiguration error and was\n          unable to complete your request.\n          <Spacer size=\"xs\" />\n          Try again later.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: CustomErrorCodes.CloudOauthUnknownAuthorizationRequest },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Error',\n      message: (\n        <>\n          Unknown authorization request.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: CustomErrorCodes.CloudOauthUnexpectedError },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Error',\n      message: (\n        <>\n          An unexpected error occurred.\n          <Spacer size=\"s\" />\n          If the issue persists,{' '}\n          <a\n            href={EXTERNAL_LINKS.githubIssues}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            report the issue.\n          </a>\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: CustomErrorCodes.CloudOauthSsoUnsupportedEmail },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Invalid email',\n      message: <>Invalid email.</>,\n    }),\n  ],\n  [\n    { errorCode: 111_001 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Error',\n      message: 'Something was wrong!',\n    }),\n  ],\n  [\n    { errorCode: 11_108 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Database already exists',\n      message: (\n        <>\n          You already have a free Redis Cloud database running.\n          <Spacer size=\"s\" />\n          Check out your\n          <a\n            href=\"https://cloud.redis.io/?utm_source=redisinsight&utm_medium=main&utm_campaign=main#/databases/\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          >\n            Cloud console\n          </a>\n          for connection details.\n        </>\n      ),\n    }),\n  ],\n  [\n    { errorCode: 11_022 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Invalid API key',\n      message: (\n        <>\n          Your Redis Cloud authorization failed.\n          <Spacer size=\"xs\" />\n          Remove the invalid API key from Redis Insight and try again.\n          <Spacer size=\"s\" />\n          Open the Settings page to manage Redis Cloud API keys.\n        </>\n      ),\n      additionalInfo: {\n        errorCode: 11022,\n        resourceId: undefined,\n      },\n    }),\n  ],\n  [\n    { errorCode: 11_401 },\n    set(cloneDeep(responseData), 'response.data', {\n      title: 'Pipeline not deployed',\n      message: 'Unfortunately we’ve found some errors in your pipeline.',\n      additionalInfo: {\n        errorCode: 11_401,\n      },\n    }),\n  ],\n]\n\ndescribe('parseCustomError', () => {\n  test.each(parseCustomErrorTests as [string | CustomError, AxiosError][])(\n    '%j',\n    (input, expected) => {\n      const result = parseCustomError(input)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n\nconst getRdiValidationMessageTests: Array<\n  [[Maybe<string>, Array<string | number>], string]\n> = [\n  [[undefined, []], ''],\n  [['Custom message', []], 'Custom message'],\n  [['Input is required', ['field']], 'Input is required'],\n  [['Input required', ['body', 'targets']], 'Targets required'],\n  [\n    [\n      \"Input should be 'postgresql', 'mysql', 'oracle', 'cassandra', 'sqlserver' or 'redis'\",\n      ['body', 'targets', 'type'],\n    ],\n    \"Type in targets should be 'postgresql', 'mysql', 'oracle', 'cassandra', 'sqlserver' or 'redis'\",\n  ],\n  [\n    [\n      'Input should be a valid integer, unable to parse string as an integer',\n      ['body', 'targets', 'my-redis', 'connection'],\n    ],\n    'Connection in targets/my-redis should be a valid integer, unable to parse string as an integer',\n  ],\n  [\n    [\n      'Input should be a valid integer, unable to parse string as an integer',\n      ['body', 'targets', 'my-redis', 0],\n    ],\n    'My-redis[0] in targets should be a valid integer, unable to parse string as an integer',\n  ],\n  [['Input required', ['body', 'targets', 0]], 'Targets[0] required'],\n  [\n    [\n      'Input should be a valid integer, unable to parse string as an integer',\n      ['body', 'targets', 'my-redis', 2, 'db', 'password'],\n    ],\n    'Password in targets/my-redis[2]/db should be a valid integer, unable to parse string as an integer',\n  ],\n  [\n    [\n      'Input should be a valid integer, unable to parse string as an integer',\n      ['body', 'targets', 'my-redis', 2, 'password'],\n    ],\n    'Password in targets/my-redis[2] should be a valid integer, unable to parse string as an integer',\n  ],\n  [\n    [\n      'Input should be a valid integer, unable to parse string as an integer',\n      ['body', 'targets', 'my-redis', 2, 'password', 0],\n    ],\n    'Password[0] in targets/my-redis[2] should be a valid integer, unable to parse string as an integer',\n  ],\n]\n\ndescribe('getRdiValidationMessage', () => {\n  test.each(getRdiValidationMessageTests)('%j', (input, expected) => {\n    const result = getRdiValidationMessage(...input)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/events/handleDownloadButton.spec.ts",
    "content": "import { handleDownloadButton } from 'uiSrc/utils/events/handleDownloadButton'\n\ndescribe('handleDownloadButton', () => {\n  let createObjectURLMock: jest.SpyInstance\n  let revokeObjectURLMock: jest.SpyInstance\n  let createElementMock: jest.SpyInstance\n  let mockAnchor: HTMLAnchorElement\n\n  beforeEach(() => {\n    // Mock URL.createObjectURL and URL.revokeObjectURL\n    createObjectURLMock = jest\n      .spyOn(URL, 'createObjectURL')\n      .mockReturnValue('mock-url')\n    revokeObjectURLMock = jest\n      .spyOn(URL, 'revokeObjectURL')\n      .mockImplementation(() => {})\n\n    // Mock document.createElement\n    mockAnchor = {\n      href: '',\n      download: '',\n      click: jest.fn(),\n    } as unknown as HTMLAnchorElement\n\n    createElementMock = jest\n      .spyOn(document, 'createElement')\n      .mockReturnValue(mockAnchor)\n  })\n\n  afterEach(() => {\n    jest.restoreAllMocks()\n  })\n\n  it('should create a blob with the provided data', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(createObjectURLMock).toHaveBeenCalledWith(\n      expect.objectContaining({\n        size: data.length,\n        type: 'text/plain',\n      }),\n    )\n  })\n\n  it('should create an anchor element', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(createElementMock).toHaveBeenCalledWith('a')\n  })\n\n  it('should set the href attribute to the blob URL', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(mockAnchor.href).toBe('mock-url')\n  })\n\n  it('should set the download attribute to the filename', () => {\n    const data = 'test data'\n    const filename = 'test-file.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(mockAnchor.download).toBe(filename)\n  })\n\n  it('should trigger click on the anchor element', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(mockAnchor.click).toHaveBeenCalled()\n  })\n\n  it('should revoke the object URL after use', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(revokeObjectURLMock).toHaveBeenCalledWith('mock-url')\n  })\n\n  it('should call onSuccess callback when provided', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n    const onSuccess = jest.fn()\n\n    handleDownloadButton(data, filename, onSuccess)\n\n    expect(onSuccess).toHaveBeenCalled()\n  })\n\n  it('should not call onSuccess callback when not provided', () => {\n    const data = 'test data'\n    const filename = 'test.txt'\n\n    expect(() => handleDownloadButton(data, filename)).not.toThrow()\n  })\n\n  it('should handle errors gracefully without crashing', () => {\n    createObjectURLMock.mockImplementation(() => {\n      throw new Error('Test error')\n    })\n\n    const data = 'test data'\n    const filename = 'test.txt'\n    const onSuccess = jest.fn()\n\n    expect(() => handleDownloadButton(data, filename, onSuccess)).not.toThrow()\n  })\n\n  it('should not call onSuccess callback if an error occurs', () => {\n    createObjectURLMock.mockImplementation(() => {\n      throw new Error('Test error')\n    })\n\n    const data = 'test data'\n    const filename = 'test.txt'\n    const onSuccess = jest.fn()\n\n    handleDownloadButton(data, filename, onSuccess)\n\n    expect(onSuccess).not.toHaveBeenCalled()\n  })\n\n  it('should handle empty data', () => {\n    const data = ''\n    const filename = 'empty.txt'\n\n    expect(() => handleDownloadButton(data, filename)).not.toThrow()\n    expect(mockAnchor.click).toHaveBeenCalled()\n  })\n\n  it('should handle special characters in filename', () => {\n    const data = 'test data'\n    const filename = 'test-file (1) [copy].txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(mockAnchor.download).toBe(filename)\n  })\n\n  it('should handle large data strings', () => {\n    const data = 'a'.repeat(10000)\n    const filename = 'large.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(createObjectURLMock).toHaveBeenCalled()\n    expect(mockAnchor.click).toHaveBeenCalled()\n    expect(revokeObjectURLMock).toHaveBeenCalled()\n  })\n\n  it('should handle multiline data', () => {\n    const data = 'line1\\nline2\\nline3'\n    const filename = 'multiline.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(createObjectURLMock).toHaveBeenCalledWith(\n      expect.objectContaining({\n        type: 'text/plain',\n      }),\n    )\n    expect(mockAnchor.click).toHaveBeenCalled()\n  })\n\n  it('should handle data with special characters', () => {\n    const data = 'Special chars: ñ, é, ü, 中文, 🎉'\n    const filename = 'special.txt'\n\n    handleDownloadButton(data, filename)\n\n    expect(mockAnchor.click).toHaveBeenCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts",
    "content": "import { RedisResponseBufferType } from 'uiSrc/slices/interfaces'\nimport JavaDate from 'uiSrc/utils/formatters/java-date'\nimport {\n  bufferToString,\n  anyToBuffer,\n  stringToBuffer,\n  bufferToUTF8,\n  bufferToASCII,\n  UTF8ToBuffer,\n  isEqualBuffers,\n  hexToBuffer,\n  bufferToHex,\n  bufferToBinary,\n  binaryToBuffer,\n  bufferToJava,\n  bufferToUint8Array,\n  isBinaryData,\n  isBinaryVector,\n} from 'uiSrc/utils'\n\ntry {\n  // Register JavaDate class for deserialization\n  ObjectInputStream.RegisterObjectClass(\n    JavaDate,\n    JavaDate.ClassName,\n    JavaDate.SerialVersionUID,\n  )\n  // eslint-disable-next-line no-empty\n} catch (e) {}\n\nconst defaultValues = [\n  {\n    unicode: 'test',\n    ascii: 'test',\n    hex: '74657374',\n    uint8Array: [116, 101, 115, 116],\n    binary: '01110100011001010111001101110100',\n  },\n  {\n    unicode: 'test test',\n    ascii: 'test test',\n    hex: '746573742074657374',\n    uint8Array: [116, 101, 115, 116, 32, 116, 101, 115, 116],\n    binary:\n      '011101000110010101110011011101000010000001110100011001010111001101110100',\n  },\n  {\n    unicode: '嘿',\n    ascii: '\\\\xe5\\\\x98\\\\xbf',\n    hex: 'e598bf',\n    uint8Array: [229, 152, 191],\n    binary: '111001011001100010111111',\n  },\n  {\n    unicode: '\\xea12 \\x12 p5',\n    ascii: '\\\\xc3\\\\xaa12 \\\\x12 p5',\n    hex: 'c3aa31322012207035',\n    uint8Array: [195, 170, 49, 50, 32, 18, 32, 112, 53],\n    binary:\n      '110000111010101000110001001100100010000000010010001000000111000000110101',\n  },\n  {\n    unicode: 'hi \\n hi \\t',\n    ascii: 'hi \\\\n hi \\\\t',\n    hex: '6869200a2068692009',\n    uint8Array: [104, 105, 32, 10, 32, 104, 105, 32, 9],\n    binary:\n      '011010000110100100100000000010100010000001101000011010010010000000001001',\n  },\n  {\n    unicode: \"!@#54ueo'6&*(){\",\n    ascii: \"!@#54ueo'6&*(){\",\n    hex: '214023353475656f2736262a28297b',\n    uint8Array: [\n      33, 64, 35, 53, 52, 117, 101, 111, 39, 54, 38, 42, 40, 41, 123,\n    ],\n    binary:\n      '001000010100000000100011001101010011010001110101011001010110111100100111001101100010011000101010001010000010100101111011',\n  },\n  {\n    unicode: 'привет',\n    ascii: '\\\\xd0\\\\xbf\\\\xd1\\\\x80\\\\xd0\\\\xb8\\\\xd0\\\\xb2\\\\xd0\\\\xb5\\\\xd1\\\\x82',\n    hex: 'd0bfd180d0b8d0b2d0b5d182',\n    uint8Array: [208, 191, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130],\n    binary:\n      '110100001011111111010001100000001101000010111000110100001011001011010000101101011101000110000010',\n  },\n]\n\nconst getStringToBufferTests = defaultValues.map(({ unicode, uint8Array }) => ({\n  input: unicode,\n  expected: { data: uint8Array, type: RedisResponseBufferType.Buffer },\n}))\n\ndescribe('stringToBuffer', () => {\n  test.each(getStringToBufferTests)('%j', ({ input, expected }) => {\n    const result = stringToBuffer(input)\n    result.data = Array.from(result.data)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getAnyToBufferTests = defaultValues.map(({ uint8Array }) => ({\n  input: uint8Array,\n  expected: { data: uint8Array, type: RedisResponseBufferType.Buffer },\n}))\n\ndescribe('anyToBuffer', () => {\n  test.each(getAnyToBufferTests)('%j', ({ input, expected }) => {\n    const result = anyToBuffer(input)\n    result.data = Array.from(result.data)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getHexToBufferTests = defaultValues.map(({ hex, uint8Array }) => ({\n  input: hex,\n  expected: { data: uint8Array, type: RedisResponseBufferType.Buffer },\n}))\n\ndescribe('hexToBuffer', () => {\n  test.each(getHexToBufferTests)('%j', ({ input, expected }) => {\n    const result = hexToBuffer(input)\n    result.data = Array.from(result.data)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getBufferToStringTests = defaultValues.map(({ unicode, uint8Array }) => ({\n  input: anyToBuffer(uint8Array),\n  expected: unicode,\n}))\n\ndescribe('bufferToString', () => {\n  test.each(getBufferToStringTests)('%j', ({ input, expected }) => {\n    expect(bufferToString(input)).toEqual(expected)\n  })\n})\n\ndescribe('bufferToUTF8', () => {\n  test.each(getBufferToStringTests)('%j', ({ input, expected }) => {\n    expect(bufferToUTF8(input)).toEqual(expected)\n  })\n})\n\nconst getBufferToASCIITests = defaultValues.map(({ ascii, uint8Array }) => ({\n  input: anyToBuffer(uint8Array),\n  expected: ascii,\n}))\n\ndescribe('bufferToASCII', () => {\n  test.each(getBufferToASCIITests)('%j', ({ input, expected }) => {\n    expect(bufferToASCII(input)).toEqual(expected)\n  })\n})\n\nconst getBufferToHexTests = defaultValues.map(({ hex, uint8Array }) => ({\n  input: anyToBuffer(uint8Array),\n  expected: hex,\n}))\n\ndescribe('bufferToASCII', () => {\n  test.each(getBufferToHexTests)('%j', ({ input, expected }) => {\n    expect(bufferToHex(input)).toEqual(expected)\n  })\n})\n\ndescribe('UTF8ToBuffer', () => {\n  test.each(getStringToBufferTests)('%j', ({ input, expected }) => {\n    const result = UTF8ToBuffer(input)\n    result.data = Array.from(result.data)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getBuffersTest = [\n  {\n    input1: anyToBuffer([116, 101, 115, 116]),\n    input2: anyToBuffer([116, 101, 115, 116]),\n    expected: true,\n  },\n  {\n    input1: anyToBuffer([16, 101, 115, 116]),\n    input2: anyToBuffer([116, 101, 116]),\n    expected: false,\n  },\n  {\n    input1: anyToBuffer([16, 101, 5435, 116]),\n    input2: anyToBuffer([116, 101, 543]),\n    expected: false,\n  },\n  {\n    input1: { data: [16, 101, 35, 116] },\n    input2: anyToBuffer([16, 101, 35, 116]),\n    expected: true,\n  },\n  {\n    input1: { data: [16, 101, 35, 116] },\n    input2: { data: [16, 101, 35, 116] },\n    expected: true,\n  },\n  {\n    input1: anyToBuffer([116, 101, 115, 116]),\n    input2: anyToBuffer([116, 101, 115]),\n    expected: false,\n  },\n]\n\ndescribe('isEqualBuffers', () => {\n  test.each(getBuffersTest)('%j', ({ input1, input2, expected }) => {\n    // @ts-ignore\n    expect(isEqualBuffers(input1, input2)).toEqual(expected)\n  })\n})\n\nconst getBufferToBinaryTests = defaultValues.map(({ binary, uint8Array }) => ({\n  input: anyToBuffer(uint8Array),\n  expected: binary,\n}))\n\ndescribe('bufferToBinary', () => {\n  test.each(getBufferToBinaryTests)('%j', ({ input, expected }) => {\n    expect(bufferToBinary(input)).toEqual(expected)\n  })\n})\n\ndescribe('binaryToBuffer', () => {\n  test.each(getBufferToBinaryTests)('%j', ({ input, expected }) => {\n    expect(binaryToBuffer(expected)).toEqual(input)\n  })\n})\n\nconst javaValues = [\n  {\n    uint8Array: [\n      172, 237, 0, 5, 115, 114, 0, 8, 69, 109, 112, 108, 111, 121, 101, 101, 2,\n      94, 116, 52, 103, 198, 18, 60, 2, 0, 3, 73, 0, 6, 110, 117, 109, 98, 101,\n      114, 76, 0, 7, 97, 100, 100, 114, 101, 115, 115, 116, 0, 18, 76, 106, 97,\n      118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76,\n      0, 4, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 0, 0, 0, 101, 116,\n      0, 25, 80, 104, 111, 107, 107, 97, 32, 75, 117, 97, 110, 44, 32, 65, 109,\n      98, 101, 104, 116, 97, 32, 80, 101, 101, 114, 116, 0, 9, 82, 101, 121, 97,\n      110, 32, 65, 108, 105,\n    ],\n    value: {\n      annotations: [],\n      className: 'Employee',\n      fields: [\n        { number: 101 },\n        { address: 'Phokka Kuan, Ambehta Peer' },\n        { name: 'Reyan Ali' },\n      ],\n      serialVersionUid: 170701604314812988n,\n    },\n  },\n  {\n    uint8Array: [\n      172, 237, 0, 5, 115, 114, 0, 32, 115, 101, 114, 105, 97, 108, 105, 122,\n      97, 116, 105, 111, 110, 68, 101, 109, 111, 46, 65, 110, 110, 111, 116, 97,\n      116, 105, 111, 110, 84, 101, 115, 116, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1,\n      73, 0, 6, 110, 117, 109, 98, 101, 114, 120, 112, 0, 0, 0, 90,\n    ],\n    value: {\n      annotations: [],\n      className: 'serializationDemo.AnnotationTest',\n      fields: [{ number: 90 }],\n      serialVersionUid: 2n,\n    },\n  },\n  {\n    uint8Array: [\n      172, 237, 0, 5, 115, 114, 0, 14, 106, 97, 118, 97, 46, 117, 116, 105, 108,\n      46, 68, 97, 116, 101, 104, 106, 129, 1, 75, 89, 116, 25, 3, 0, 0, 120,\n      112, 119, 8, 0, 0, 1, 146, 226, 121, 165, 136, 120,\n    ],\n    value: new Date(Number(1730376476040n)),\n  },\n]\n\nconst getBufferToJavaTests = javaValues.map(({ uint8Array, value }) => ({\n  input: anyToBuffer(uint8Array),\n  expected: value,\n}))\n\ndescribe('bufferToJava', () => {\n  test.each(getBufferToJavaTests)('%o', ({ input, expected }) => {\n    expect(bufferToJava(input)).toEqual(expected)\n  })\n})\n\ndescribe('bufferToUint8Array', () => {\n  test.each(javaValues)('%o', ({ uint8Array }) => {\n    expect(bufferToUint8Array(anyToBuffer(uint8Array))).toEqual(\n      new Uint8Array(uint8Array),\n    )\n  })\n})\n\ndescribe('isBinaryData', () => {\n  it('should return false for plain ASCII text', () => {\n    expect(isBinaryData(anyToBuffer([116, 101, 115, 116]))).toBe(false)\n  })\n\n  it('should return false for valid UTF-8 multibyte text', () => {\n    // \"привет\" in UTF-8\n    expect(\n      isBinaryData(\n        anyToBuffer([\n          208, 191, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130,\n        ]),\n      ),\n    ).toBe(false)\n  })\n\n  it('should return true for binary data that is not valid UTF-8 round-trip', () => {\n    // float32 representation of 1.0 (little-endian: 0x00 0x00 0x80 0x3F)\n    expect(isBinaryData(anyToBuffer([0x00, 0x00, 0x80, 0x3f]))).toBe(true)\n  })\n})\n\ndescribe('isBinaryVector', () => {\n  it('should return true for a valid float32 binary vector', () => {\n    // Two float32 values: 1.0 and 2.0 in little-endian\n    const float1 = [0x00, 0x00, 0x80, 0x3f] // 1.0\n    const float2 = [0x00, 0x00, 0x00, 0x40] // 2.0\n    expect(isBinaryVector(anyToBuffer([...float1, ...float2]))).toBe(true)\n  })\n\n  it('should return false for plain text', () => {\n    // \"test\" is 4 bytes, divisible by 4, but valid UTF-8\n    expect(isBinaryVector(anyToBuffer([116, 101, 115, 116]))).toBe(false)\n  })\n\n  it('should return false for buffer with length not divisible by 4', () => {\n    expect(isBinaryVector(anyToBuffer([0x00, 0x00, 0x80]))).toBe(false)\n  })\n\n  it('should return false for buffer shorter than MIN_VECTOR_BYTES', () => {\n    // 4 bytes = only 1 float32 (need at least 2)\n    expect(isBinaryVector(anyToBuffer([0x00, 0x00, 0x80, 0x3f]))).toBe(false)\n  })\n\n  it('should return false when float32 values contain NaN', () => {\n    // NaN in float32: 0x7FC00000 little-endian = [0x00, 0x00, 0xC0, 0x7F]\n    const nan = [0x00, 0x00, 0xc0, 0x7f]\n    const valid = [0x00, 0x00, 0x80, 0x3f]\n    expect(isBinaryVector(anyToBuffer([...nan, ...valid]))).toBe(false)\n  })\n\n  it('should return false for empty buffer', () => {\n    expect(isBinaryVector(anyToBuffer([]))).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/markdown/remarkImage.spec.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'\nimport { remarkImage } from 'uiSrc/utils/formatters/markdown'\n\njest.mock('unist-util-visit')\nconst TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id'\nconst testCases = [\n  {\n    url: '../../../_images/relative.png',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`,\n  },\n  {\n    url: '/_images/relative.png',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`,\n  },\n  {\n    url: 'https://somesite.test/image.png',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: 'https://somesite.test/image.png',\n  },\n]\ndescribe('remarkImage', () => {\n  testCases.forEach((tc) => {\n    it(`should return ${tc.result} for url:${tc.url}, path: ${tc.path} `, () => {\n      const node = {\n        url: tc.url,\n      }\n\n      // mock implementation\n      ;(visit as jest.Mock).mockImplementation(\n        (_tree: any, _name: string, callback: (node: any) => void) => {\n          callback(node)\n        },\n      )\n\n      const remark = remarkImage(tc.path)\n      remark({} as Node)\n      expect(node).toEqual({\n        ...node,\n        url: tc.result,\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/markdown/remarkLink.spec.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { remarkLink } from 'uiSrc/utils/formatters/markdown'\n\njest.mock('unist-util-visit')\n\ndescribe('remarkLink', () => {\n  it('should not modify codeNode if title is not Redis Cloud', () => {\n    const codeNode = {\n      type: 'link',\n      url: 'https://mysite.com',\n      children: [\n        {\n          type: 'text',\n          value: 'Redis Stack Server',\n        },\n      ],\n    }\n    // mock implementation\n    ;(visit as jest.Mock).mockImplementation(\n      (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n        callback(codeNode)\n      },\n    )\n\n    const remark = remarkLink()\n    remark({} as Node)\n    expect(codeNode).toEqual({\n      ...codeNode,\n    })\n  })\n\n  it('should properly modify codeNode with title Redis Cloud', () => {\n    const codeNode = {\n      title: 'Redis Cloud',\n      type: 'link',\n      url: 'https://mysite.com',\n      children: [\n        {\n          type: 'text',\n          value: 'Setup Redis Cloud',\n        },\n      ],\n    }\n    // mock implementation\n    ;(visit as jest.Mock).mockImplementation(\n      (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n        callback(codeNode)\n      },\n    )\n\n    const remark = remarkLink()\n    remark({} as Node)\n    expect(codeNode).toEqual({\n      ...codeNode,\n      type: 'html',\n      value: '<CloudLink url=\"https://mysite.com\" text=\"Setup Redis Cloud\" />',\n    })\n  })\n\n  it('should properly modify codeNode with internal app link', () => {\n    const codeNode = {\n      type: 'link',\n      url: 'redisinsight:workbench',\n      children: [\n        {\n          type: 'text',\n          value: 'Workbench',\n        },\n      ],\n    }\n    // mock implementation\n    ;(visit as jest.Mock).mockImplementation(\n      (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n        callback(codeNode)\n      },\n    )\n\n    const remark = remarkLink()\n    remark({} as Node)\n    expect(codeNode).toEqual({\n      ...codeNode,\n      type: 'html',\n      value: '<RedisInsightLink url=\"workbench\" text=\"Workbench\" size=\"S\" />',\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/markdown/remarkRedisCode.spec.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { remarkCode } from 'uiSrc/utils/formatters/markdown'\n\njest.mock('unist-util-visit')\n\nconst getValue = (\n  meta: string,\n  lang: string,\n  params?: string,\n  value?: string,\n) =>\n  `<Code label=\"${meta}\" params=\"${params}\" path={path} lang=\"${lang}\">{${JSON.stringify(value)}}</Code>`\n\ndescribe('remarkRedisCode', () => {\n  it('should not modify codeNode if lang not redis', () => {\n    const codeNode = {\n      lang: 'html',\n      value: '1',\n      meta: '2',\n    }\n    // mock implementation\n    ;(visit as jest.Mock).mockImplementation(\n      (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n        callback(codeNode)\n      },\n    )\n\n    const remark = remarkCode()\n    remark({} as Node)\n    expect(codeNode).toEqual({\n      ...codeNode,\n    })\n  })\n\n  it('should properly modify codeNode with lang redis', () => {\n    const codeNode = {\n      lang: 'redis',\n      value: '1',\n      meta: '2',\n    }\n    // mock implementation\n    ;(visit as jest.Mock).mockImplementation(\n      (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n        callback(codeNode)\n      },\n    )\n\n    const remark = remarkCode()\n    remark({} as Node)\n    expect(codeNode).toEqual({\n      ...codeNode,\n      type: 'html',\n      value: getValue(codeNode.meta, 'redis', undefined, '1'),\n    })\n  })\n\n  it('should properly modify codeNode with any lang', () => {\n    const codeNode = {\n      lang: 'java',\n      value: '1',\n      meta: '2',\n    }\n    // mock implementation\n    ;(visit as jest.Mock).mockImplementation(\n      (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n        callback(codeNode)\n      },\n    )\n\n    const remark = remarkCode({ allLangs: true })\n    remark({} as Node)\n    expect(codeNode).toEqual({\n      ...codeNode,\n      type: 'html',\n      value: `<Code label=\"2\" lang=\"java\">{${JSON.stringify('1')}}</Code>`,\n    })\n  })\n\n  describe('should properly modify codeNode with lang redis', () => {\n    it('with auto execute param redis:[auto=true;results=group]', () => {\n      const params = '[results=group]'\n      const codeNode = {\n        lang: `redis:${params}`,\n        value: '1',\n        meta: '2',\n      }\n      // mock implementation\n      ;(visit as jest.Mock).mockImplementation(\n        (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n          callback(codeNode)\n        },\n      )\n\n      const remark = remarkCode()\n      remark({} as Node)\n      expect(codeNode).toEqual({\n        ...codeNode,\n        type: 'html',\n        value: getValue(codeNode.meta, 'redis', params, '1'),\n      })\n    })\n    it('without auto execute param redis:[results=group;pipeline=2]', () => {\n      const params = '[results=group;pipeline=2]'\n      const codeNode = {\n        lang: `redis:${params}`,\n        value: '1',\n        meta: '2',\n      }\n      // mock implementation\n      ;(visit as jest.Mock).mockImplementation(\n        (_tree: any, _name: string, callback: (codeNode: any) => void) => {\n          callback(codeNode)\n        },\n      )\n\n      const remark = remarkCode()\n      remark({} as Node)\n      expect(codeNode).toEqual({\n        ...codeNode,\n        type: 'html',\n        value: getValue(codeNode.meta, 'redis', params, '1'),\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/markdown/remarkRedisUpload.spec.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { remarkRedisUpload } from 'uiSrc/utils/formatters/markdown'\n\njest.mock('unist-util-visit')\n\nconst getValue = (label: string, path: string) =>\n  `<RedisUploadButton label=\"${label}\" path=\"${path}\" />`\n\nconst TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id'\n\nconst testCases = [\n  {\n    lang: 'redis-upload:[../../../_data/strings.txt]',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    meta: 'Upload data',\n    resultPath: `/${TUTORIAL_PATH}/_data/strings.txt`,\n  },\n  {\n    lang: 'redis-upload:[/_data/s t rings.txt]',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    meta: 'Upload data',\n    resultPath: `/${TUTORIAL_PATH}/_data/s t rings.txt`,\n  },\n  {\n    lang: 'redis-upload:[https://somesite.test/image.png]',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    meta: 'Upload data',\n    resultPath: '/image.png',\n  },\n]\n\ndescribe('remarkRedisUpload', () => {\n  testCases.forEach((tc) => {\n    it(`should return ${tc.resultPath} + ${tc.meta} for ${tc.lang} ${tc.meta}`, () => {\n      const node = {\n        type: 'code',\n        lang: tc.lang,\n        meta: tc.meta,\n      }\n\n      // mock implementation\n      ;(visit as jest.Mock).mockImplementation(\n        (_tree: any, _name: string, callback: (node: any) => void) => {\n          callback(node)\n        },\n      )\n\n      const remark = remarkRedisUpload(tc.path)\n      remark({} as Node)\n      expect(node).toEqual({\n        ...node,\n        type: 'html',\n        value: getValue(tc.meta, tc.resultPath),\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/markdown/remarkSanitize.spec.ts",
    "content": "import { visit } from 'unist-util-visit'\nimport { remarkSanitize } from 'uiSrc/utils/formatters/markdown'\n\njest.mock('unist-util-visit')\n\nconst testCases = [\n  { input: '', output: '' },\n  {\n    input: '<a href=\"https://localhost\">',\n    output: '<a href=\"https://localhost\" target=\"_blank\">',\n  },\n  { input: '<a href=\"/settings\">', output: '<a>' },\n  { input: '<a href=\"javascript:alert(1)\">', output: '<a>' },\n  { input: '<img onload=\"alert(1)\">', output: '<img>' },\n  { input: '<img src=\"javascript:alert(1)\">', output: '<img>' },\n  { input: '<img src=\"img.png\">', output: '<img src=\"img.png\">' },\n  {\n    input:\n      '<div dangerouslySetInnerHTML={{\"__html\": \"<img src=x onerror=alert(\\'this.still.works\\')>\"}} />',\n    output: '',\n  },\n  { input: '<script>', output: '' },\n  { input: '<script>alert(1)</script>', output: '' },\n]\n\ndescribe('remarkSanitize', () => {\n  testCases.forEach((tc) => {\n    it('should return proper sanitized value', () => {\n      const node = {\n        value: tc.input,\n      }\n\n      // mock implementation\n      ;(visit as jest.Mock).mockImplementation(\n        (_tree: any, _name: string, callback: (node: any) => void) => {\n          callback(node)\n        },\n      )\n\n      const remark = remarkSanitize()\n      remark({} as Node)\n      expect(node).toEqual({\n        ...node,\n        value: tc.output,\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts",
    "content": "import { format } from 'date-fns'\nimport { encode } from 'msgpackr'\nimport { serialize } from 'php-serialize'\nimport { DATETIME_FORMATTER_DEFAULT, KeyValueFormat } from 'uiSrc/constants'\nimport {\n  anyToBuffer,\n  bufferToSerializedFormat,\n  formattingBuffer,\n  stringToBuffer,\n  stringToSerializedBufferFormat,\n} from 'uiSrc/utils'\n\ndescribe('bufferToSerializedFormat', () => {\n  describe(KeyValueFormat.JSON, () => {\n    describe('should properly serialize', () => {\n      const testValues = [{}, '\"\"', 1, true, { a: { b: [1, 2, '3'] } }].map(\n        (v) => JSON.stringify(v),\n      )\n\n      test.each(testValues)('test %j', (val) => {\n        expect(\n          bufferToSerializedFormat(KeyValueFormat.JSON, stringToBuffer(val)),\n        ).toEqual(val)\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = ['1-', '[1, 2,]', '{ zx1***.[']\n\n      test.each(testValues)('test json values', (val) => {\n        expect(\n          bufferToSerializedFormat(KeyValueFormat.JSON, stringToBuffer(val)),\n        ).toEqual(val)\n      })\n    })\n  })\n\n  describe(KeyValueFormat.Vector32Bit, () => {\n    describe('should properly serialize', () => {\n      const testValues = [\n        new Float32Array([1.0, 2.0]),\n        new Float32Array([12.12, 13.41]),\n        new Float32Array([0.34, 0.63, -0.54, -0.69, 0.98, 0.61]),\n      ].map((v) => ({\n        input: anyToBuffer(v.buffer),\n        expected: JSON.stringify(v),\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(\n          JSON.stringify(\n            bufferToSerializedFormat(KeyValueFormat.Vector32Bit, input),\n          ),\n        ).toEqual(expected)\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = [new Float32Array(['test'])].map((v) => ({\n        input: anyToBuffer(v.buffer),\n        expected: JSON.stringify(v),\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(\n          JSON.stringify(\n            bufferToSerializedFormat(KeyValueFormat.Vector32Bit, input),\n          ),\n        ).toEqual(expected)\n      })\n    })\n  })\n\n  describe(KeyValueFormat.Vector64Bit, () => {\n    describe('should properly serialize', () => {\n      const testValues = [\n        new Float64Array([1.0, 2.0]),\n        new Float64Array([12.12, 13.41]),\n      ].map((v) => ({\n        input: anyToBuffer(v.buffer),\n        expected: JSON.stringify(v),\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(\n          JSON.stringify(\n            bufferToSerializedFormat(KeyValueFormat.Vector64Bit, input),\n          ),\n        ).toEqual(expected)\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = [new Float64Array(['test'])].map((v) => ({\n        input: anyToBuffer(v.buffer),\n        expected: JSON.stringify(v),\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(\n          JSON.stringify(\n            bufferToSerializedFormat(KeyValueFormat.Vector64Bit, input),\n          ),\n        ).toEqual(expected)\n      })\n    })\n  })\n\n  describe(KeyValueFormat.Msgpack, () => {\n    describe('should properly serialize', () => {\n      const testValues = [{}, '\"\"', 6677, true, { a: { b: [1, 2, '3'] } }].map(\n        (v) => ({\n          input: anyToBuffer(encode(v)),\n          expected: JSON.stringify(v),\n        }),\n      )\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(bufferToSerializedFormat(KeyValueFormat.Msgpack, input)).toEqual(\n          expected,\n        )\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = ['1-', '[1, 2,]', '{ zx1***.[']\n\n      test.each(testValues)('test json values', (val) => {\n        expect(\n          bufferToSerializedFormat(KeyValueFormat.Msgpack, stringToBuffer(val)),\n        ).toEqual(val)\n      })\n    })\n  })\n\n  describe(KeyValueFormat.PHP, () => {\n    describe('should properly serialize', () => {\n      const testValues = [\n        [1],\n        '\"\"',\n        '反序列化',\n        6677,\n        true,\n        { a: { b: [1, 2, '3'] } },\n      ].map((v) => ({\n        input: stringToBuffer(serialize(v)),\n        expected: JSON.stringify(v),\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(bufferToSerializedFormat(KeyValueFormat.PHP, input)).toEqual(\n          expected,\n        )\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = ['1-', '[1, 2,]', '{ zx1***.[']\n\n      test.each(testValues)('test json values', (val) => {\n        expect(\n          bufferToSerializedFormat(KeyValueFormat.PHP, stringToBuffer(val)),\n        ).toEqual(val)\n      })\n    })\n  })\n})\n\ndescribe('stringToSerializedBufferFormat', () => {\n  describe(KeyValueFormat.JSON, () => {\n    describe('should properly unserialize', () => {\n      const testValues = [{}, '\"\"', 1, true, { a: { b: [1, 2, '3'] } }].map(\n        (v) => JSON.stringify(v),\n      )\n\n      test.each(testValues)('test %j', (val) => {\n        expect(\n          stringToSerializedBufferFormat(KeyValueFormat.JSON, val),\n        ).toEqual(stringToBuffer(val))\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = ['1-', '[1, 2,]', '{ zx1***.[']\n\n      test.each(testValues)('test json values', (val) => {\n        expect(\n          stringToSerializedBufferFormat(KeyValueFormat.JSON, val),\n        ).toEqual(stringToBuffer(val))\n      })\n    })\n  })\n\n  describe(KeyValueFormat.Msgpack, () => {\n    describe('should properly unserialize', () => {\n      const testValues = [{}, '\"\"', 6677, true, { a: { b: [1, 2, '3'] } }].map(\n        (v) => ({\n          input: JSON.stringify(v),\n          expected: anyToBuffer(encode(v)),\n        }),\n      )\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(\n          stringToSerializedBufferFormat(KeyValueFormat.Msgpack, input),\n        ).toEqual(expected)\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = ['1-', '[1, 2,]', '{ zx1***.[']\n\n      test.each(testValues)('test json values', (val) => {\n        expect(\n          stringToSerializedBufferFormat(KeyValueFormat.Msgpack, val),\n        ).toEqual(stringToBuffer(val))\n      })\n    })\n  })\n\n  describe(KeyValueFormat.PHP, () => {\n    describe('should properly unserialize', () => {\n      const testValues = [\n        [1],\n        '\"\"',\n        '反序列化',\n        6677,\n        true,\n        { a: { b: [1, 2, '3'] } },\n      ].map((v) => ({\n        input: JSON.stringify(v),\n        expected: stringToBuffer(serialize(v)),\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(\n          stringToSerializedBufferFormat(KeyValueFormat.PHP, input),\n        ).toEqual(expected)\n      })\n    })\n\n    describe('should properly return value with invalid values', () => {\n      const testValues = ['1-', '[1, 2,]', '{ zx1***.[']\n\n      test.each(testValues)('test json values', (val) => {\n        expect(stringToSerializedBufferFormat(KeyValueFormat.PHP, val)).toEqual(\n          stringToBuffer(val),\n        )\n      })\n    })\n  })\n})\n\ndescribe('formattingBuffer', () => {\n  describe(KeyValueFormat.Vector32Bit, () => {\n    describe('should properly serialize', () => {\n      const floatTestValues = [\n        new Float32Array([1.0, 2.0]),\n        new Float32Array([12.12, 13.41]),\n        new Float32Array([0.34, 0.63, -0.54, -0.69, 0.98, 0.61]),\n      ].map((v) => ({\n        input: anyToBuffer(v.buffer),\n        expected: { value: JSON.stringify(v), isValid: true },\n      }))\n      const stringTestValues = ['[1,2.0,3]', 'hello', 'привет'].map((v) => ({\n        input: stringToBuffer(v),\n        expected: { value: v, isValid: true },\n      }))\n      test.each(floatTestValues)('test %j', ({ input, expected }) => {\n        expect(\n          formattingBuffer(input, KeyValueFormat.Vector32Bit).isValid,\n        ).toEqual(expected.isValid)\n      })\n      test.each(stringTestValues)('test %j', ({ input, expected }) => {\n        expect(formattingBuffer(input, KeyValueFormat.Vector32Bit)).toEqual(\n          expected,\n        )\n      })\n    })\n  })\n\n  describe(KeyValueFormat.Vector64Bit, () => {\n    describe('should properly serialize', () => {\n      const floatTestValues = [\n        new Float64Array([1.0, 2.0]),\n        new Float64Array([12.12, 13.41]),\n        new Float64Array([0.34, 0.63, -0.54, -0.69, 0.98, 0.61]),\n      ].map((v) => ({\n        input: anyToBuffer(v.buffer),\n        expected: { value: JSON.stringify(v), isValid: true },\n      }))\n      const stringTestValues = ['[1,2.0,3]', 'hello', 'привет'].map((v) => ({\n        input: stringToBuffer(v),\n        expected: { value: v, isValid: true },\n      }))\n      test.each(floatTestValues)('test %j', ({ input, expected }) => {\n        expect(\n          formattingBuffer(input, KeyValueFormat.Vector64Bit).isValid,\n        ).toEqual(expected.isValid)\n      })\n      test.each(stringTestValues)('test %j', ({ input, expected }) => {\n        expect(formattingBuffer(input, KeyValueFormat.Vector64Bit)).toEqual(\n          expected,\n        )\n      })\n    })\n  })\n\n  describe(KeyValueFormat.DateTime, () => {\n    describe('should properly format timestamp number', () => {\n      // Since we formatting with local timezome, we cannot hardcode the expected string result\n      const expected = new Date(1722593319805)\n      const testValues = [\n        new Uint8Array([49, 55, 50, 50, 53, 57, 51, 51, 49, 57, 56, 48, 53]),\n      ].map((v) => ({\n        input: anyToBuffer(v),\n        expected: {\n          value: format(expected, DATETIME_FORMATTER_DEFAULT),\n          isValid: true,\n        },\n      }))\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(formattingBuffer(input, KeyValueFormat.DateTime)).toEqual(\n          expected,\n        )\n      })\n    })\n\n    describe('should left iso strings and other strings as they are', () => {\n      const testValues = [\n        {\n          input: anyToBuffer(\n            new Uint8Array([65, 110, 121, 32, 83, 116, 114, 105, 110, 103]),\n          ),\n          expected: { value: 'Any String', isValid: false },\n        },\n        {\n          input: anyToBuffer(\n            new Uint8Array([\n              50, 48, 50, 52, 45, 48, 56, 45, 48, 50, 84, 48, 48, 58, 48, 48,\n              58, 48, 48, 46, 48, 48, 48, 90,\n            ]),\n          ),\n          expected: { value: '2024-08-02T00:00:00.000Z', isValid: false },\n        },\n      ]\n\n      test.each(testValues)('test %j', ({ input, expected }) => {\n        expect(formattingBuffer(input, KeyValueFormat.DateTime)).toEqual(\n          expected,\n        )\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/getLetterByIndex.spec.ts",
    "content": "import { getLetterByIndex } from 'uiSrc/utils'\n\nconst getLetterByIndexTests: any[] = [\n  [0, 'A'],\n  [5, 'F'],\n  [25, 'Z'],\n  [26, 'AA'],\n  [52, 'BA'],\n  [522, 'TC'],\n  [1024, 'AMK'],\n]\n\ndescribe('getLetterByIndex', () => {\n  it.each(getLetterByIndexTests)(\n    'for input: %s (index), should be output: %s',\n    (index, expected) => {\n      expect(getLetterByIndex(index)).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/groupTypes.spec.ts",
    "content": "import { CommandGroup, GROUP_TYPES_DISPLAY, KeyTypes } from 'uiSrc/constants'\nimport { getGroupTypeDisplay, NO_TYPE_NAME } from 'uiSrc/utils'\n\nconst getGroupTypeDisplayTests: any[] = [\n  [KeyTypes.Hash, GROUP_TYPES_DISPLAY[KeyTypes.Hash]],\n  [KeyTypes.ReJSON, GROUP_TYPES_DISPLAY[KeyTypes.ReJSON]],\n  [KeyTypes.String, GROUP_TYPES_DISPLAY[KeyTypes.String]],\n  [KeyTypes.ZSet, GROUP_TYPES_DISPLAY[KeyTypes.ZSet]],\n  [CommandGroup.Connection, GROUP_TYPES_DISPLAY[CommandGroup.Connection]],\n  [CommandGroup.HyperLogLog, GROUP_TYPES_DISPLAY[CommandGroup.HyperLogLog]],\n  [CommandGroup.CuckooFilter, GROUP_TYPES_DISPLAY[CommandGroup.CuckooFilter]],\n  [CommandGroup.PubSub, GROUP_TYPES_DISPLAY[CommandGroup.PubSub]],\n  ['Some_group', 'Some group'],\n  ['group', 'group'],\n  [null, NO_TYPE_NAME],\n  ['', NO_TYPE_NAME],\n  [undefined, NO_TYPE_NAME],\n]\n\ndescribe('getGroupTypeDisplay', () => {\n  it.each(getGroupTypeDisplayTests)(\n    'for input: %s (type), should be output: %s',\n    (type, expected) => {\n      const result = getGroupTypeDisplay(type)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/highlighting.spec.ts",
    "content": "import {\n  getHighlightingFeatures,\n  getPagesForFeatures,\n} from 'uiSrc/utils/features'\nimport { MOCKED_HIGHLIGHTING_FEATURES } from 'uiSrc/utils/test-utils'\n\ndescribe('getPagesForFeatures', () => {\n  it('should return proper pages for features', () => {\n    expect(getPagesForFeatures()).toEqual({})\n    expect(getPagesForFeatures([])).toEqual({})\n    expect(getPagesForFeatures(['a'])).toEqual({})\n    expect(getPagesForFeatures(['importDatabases'])).toEqual({\n      browser: ['importDatabases'],\n    })\n    expect(getPagesForFeatures(MOCKED_HIGHLIGHTING_FEATURES)).toEqual({\n      browser: MOCKED_HIGHLIGHTING_FEATURES,\n    })\n  })\n})\n\ndescribe('getPagesForFeatures', () => {\n  it('should return proper pages for features', () => {\n    expect(getHighlightingFeatures([])).toEqual({})\n    expect(getHighlightingFeatures(['feature1'])).toEqual({ feature1: true })\n    expect(getHighlightingFeatures(['f1', 'f2'])).toEqual({\n      f1: true,\n      f2: true,\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/instance/instanceNavigation.spec.ts",
    "content": "import { filterAndSort } from 'uiSrc/utils'\nimport { Instance, RdiInstance } from 'uiSrc/slices/interfaces'\n\nconst instances: Array<Instance | RdiInstance> = [\n  {\n    name: 'Instance12',\n    db: 1,\n    lastConnection: '2023-10-01',\n    host: 'host4',\n    port: 5432,\n  },\n  {\n    name: 'Instance2',\n    db: 2,\n    lastConnection: '2023-10-02',\n    host: 'host1',\n    port: 5432,\n  },\n  {\n    name: 'InstanceThree',\n    db: 3,\n    lastConnection: '2023-10-03',\n    host: 'host2',\n    port: 5433,\n  },\n]\n\ndescribe('filterAndSort', () => {\n  it('should return an empty array if input array is empty', () => {\n    expect(\n      filterAndSort([], 'test', { field: 'name', direction: 'asc' }),\n    ).toEqual([])\n  })\n\n  it('should filter instances by name', () => {\n    const result = filterAndSort(instances, 'instance2', {\n      field: 'name',\n      direction: 'asc',\n    })\n    expect(result).toEqual([instances[1]])\n  })\n\n  it('should filter instances by db index', () => {\n    const result = filterAndSort(instances, '3', {\n      field: 'name',\n      direction: 'asc',\n    })\n    expect(result).toEqual([instances[2]])\n  })\n\n  it('should sort instances by lastConnection in ascending order', () => {\n    const result = filterAndSort(instances, '', {\n      field: 'lastConnection',\n      direction: 'asc',\n    })\n    expect(result).toEqual([instances[2], instances[1], instances[0]])\n  })\n\n  it('should sort instances by lastConnection in descending order', () => {\n    const result = filterAndSort(instances, '', {\n      field: 'lastConnection',\n      direction: 'desc',\n    })\n    expect(result).toEqual([instances[0], instances[1], instances[2]])\n  })\n\n  it('should sort instances by host in ascending order', () => {\n    const result = filterAndSort(instances, '', {\n      field: 'host',\n      direction: 'asc',\n    })\n    expect(result).toEqual([instances[1], instances[2], instances[0]])\n  })\n\n  it('should sort instances by host in descending order', () => {\n    const result = filterAndSort(instances, '', {\n      field: 'host',\n      direction: 'desc',\n    })\n    expect(result).toEqual([instances[0], instances[2], instances[1]])\n  })\n\n  it('should handle mixed filtering and sorting', () => {\n    const result = filterAndSort(instances, '2', {\n      field: 'lastConnection',\n      direction: 'asc',\n    })\n    expect(result).toEqual([instances[1], instances[0]])\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/instance/instanceOptions.spec.ts",
    "content": "import { mock } from 'ts-mockito'\nimport {\n  parseInstanceOptionsCloud,\n  parseInstanceOptionsCluster,\n} from 'uiSrc/utils'\nimport {\n  InstanceRedisCloud,\n  InstanceRedisCluster,\n} from 'uiSrc/slices/interfaces'\n\nconst instancesRedisClusterMock = [\n  {\n    ...mock<InstanceRedisCluster>(),\n    uid: 1,\n    options: {\n      id: 1,\n    },\n  },\n  {\n    ...mock<InstanceRedisCluster>(),\n    uid: 2,\n  },\n  {\n    ...mock<InstanceRedisCluster>(),\n    uid: 3,\n  },\n]\n\nconst instancesRedisCloudMock = [\n  {\n    ...mock<InstanceRedisCloud>(),\n    databaseId: 1,\n    options: {\n      id: 1,\n    },\n  },\n  {\n    ...mock<InstanceRedisCloud>(),\n    databaseId: 2,\n  },\n  {\n    ...mock<InstanceRedisCloud>(),\n    databaseId: 3,\n  },\n]\n\ndescribe('parseInstanceOptionsCluster', () => {\n  it('should parse', () => {\n    expect(parseInstanceOptionsCluster(1, instancesRedisClusterMock)).toEqual({\n      id: 1,\n    })\n  })\n})\n\ndescribe('parseInstanceOptionsCloud', () => {\n  it('should parse', () => {\n    expect(parseInstanceOptionsCloud(1, instancesRedisCloudMock)).toEqual({\n      id: 1,\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/instance/instanceProvider.spec.ts",
    "content": "import { isAzureDatabase } from 'uiSrc/utils'\nimport { DBInstanceFactory } from 'uiSrc/mocks/factories/database/DBInstance.factory'\n\n// \"as any\" is used for providerDetails because Instance extends Partial<DatabaseInstanceResponse>\n// from the API, which types providerDetails with CloudProvider and AzureAuthType enums.\n// These enums can't be imported directly in UI tests due to API module path resolution issues.\ndescribe('isAzureDatabase', () => {\n  it('should return true when providerDetails.provider is \"azure\"', () => {\n    const instance = DBInstanceFactory.build({\n      providerDetails: {\n        provider: 'azure',\n        authType: 'entraId',\n      } as any,\n    })\n\n    expect(isAzureDatabase(instance)).toBe(true)\n  })\n\n  it('should return true when providerDetails.provider is \"azure\" with access-key auth', () => {\n    const instance = DBInstanceFactory.build({\n      providerDetails: {\n        provider: 'azure',\n        authType: 'accessKey',\n      } as any,\n    })\n\n    expect(isAzureDatabase(instance)).toBe(true)\n  })\n\n  it('should return false when providerDetails is undefined', () => {\n    const instance = DBInstanceFactory.build({\n      providerDetails: undefined,\n    })\n\n    expect(isAzureDatabase(instance)).toBe(false)\n  })\n\n  it('should return false when instance is null', () => {\n    expect(isAzureDatabase(null)).toBe(false)\n  })\n\n  it('should return false when instance is undefined', () => {\n    expect(isAzureDatabase(undefined as any)).toBe(false)\n  })\n\n  it('should return false when providerDetails has different provider', () => {\n    const instance = DBInstanceFactory.build({\n      providerDetails: {\n        provider: 'aws',\n        authType: 'some-auth',\n      } as any,\n    })\n\n    expect(isAzureDatabase(instance)).toBe(false)\n  })\n\n  it('should return false for empty object instance', () => {\n    expect(isAzureDatabase({})).toBe(false)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/links.spec.ts",
    "content": "import {\n  getUtmExternalLink,\n  buildRedisInsightUrl,\n  UTMParams,\n} from 'uiSrc/utils/links'\nimport { Instance } from 'uiSrc/slices/interfaces'\n\nconst addUtmToLinkTests: Array<{\n  input: [string, UTMParams]\n  expected: string\n}> = [\n  {\n    input: ['http://www.google.com', { campaign: 'name' }],\n    expected:\n      'http://www.google.com/?utm_source=redisinsight&utm_medium=app&utm_campaign=name',\n  },\n  {\n    input: ['http://www.google.com', { campaign: 'name', medium: 'main' }],\n    expected:\n      'http://www.google.com/?utm_source=redisinsight&utm_medium=main&utm_campaign=name',\n  },\n  {\n    input: [\n      'http://www.google.com',\n      { campaign: 'name', medium: 'main', source: 'source' },\n    ],\n    expected:\n      'http://www.google.com/?utm_source=source&utm_medium=main&utm_campaign=name',\n  },\n]\n\ndescribe('getUtmExternalLink', () => {\n  test.each(addUtmToLinkTests)('%j', ({ input, expected }) => {\n    const result = getUtmExternalLink(...input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst buildRedisInsightUrlTests: Array<{\n  input: Instance\n  expected: string\n}> = [\n  {\n    input: {\n      id: '0',\n      host: 'aws-instance.amazonaws.com',\n      port: 6379,\n      name: 'free aws instance',\n      tls: false,\n      tlsClientAuthRequired: false,\n      modules: [],\n      version: '1.0.0',\n      cloudDetails: {\n        subscriptionId: 1,\n        cloudId: 1,\n        subscriptionType: 'fixed',\n        planMemoryLimit: 1024,\n        memoryLimitMeasurementUnit: 'MB',\n        free: true,\n      },\n    },\n    expected:\n      'redisinsight://databases/connect?redisUrl=redis%3A%2F%2F%40aws-instance.amazonaws.com%3A6379&cloudBdbId=1&databaseAlias=free+aws+instance&subscriptionType=fixed&planMemoryLimit=1024&memoryLimitMeasurementUnit=MB&free=true',\n  },\n  {\n    input: {\n      id: '1',\n      host: '127.0.0.1',\n      port: 6380,\n      name: 'cert localhost instance',\n      tls: true,\n      tlsClientAuthRequired: true,\n      modules: [],\n      version: '1.0.0',\n    },\n    expected:\n      'redisinsight://databases/connect?redisUrl=redis%3A%2F%2F%40127.0.0.1%3A6380&cloudBdbId=&databaseAlias=cert+localhost+instance&requiredTls=true&requiredCaCert=true&requiredClientCert=true',\n  },\n  {\n    input: {\n      id: '2',\n      host: 'gcp-instance.example.com',\n      port: 6379,\n      name: 'mixed cert gcp instance',\n      tls: true,\n      tlsClientAuthRequired: false,\n      modules: [],\n      version: '1.0.0',\n      cloudDetails: {\n        subscriptionId: 2,\n        cloudId: 2,\n        subscriptionType: 'fixed',\n        planMemoryLimit: 2048,\n        memoryLimitMeasurementUnit: 'MB',\n      },\n    },\n    expected:\n      'redisinsight://databases/connect?redisUrl=redis%3A%2F%2F%40gcp-instance.example.com%3A6379&cloudBdbId=2&databaseAlias=mixed+cert+gcp+instance&requiredTls=true&requiredCaCert=true&subscriptionType=fixed&planMemoryLimit=2048&memoryLimitMeasurementUnit=MB',\n  },\n]\n\ndescribe('buildRedisInsightUrl', () => {\n  test.each(buildRedisInsightUrlTests)('%j', ({ input, expected }) => {\n    const result = buildRedisInsightUrl(input)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/longNames.spec.ts",
    "content": "import { formatLongName, formatNameShort, getDbIndex } from 'uiSrc/utils'\n\nconst longName =\n  'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut varius massa. Vestibulum non nulla turpis. ' +\n  'Morbi non viverra risus. Curabitur aliquet lorem at interdum ultrices. Praesent accumsan leo sit amet purus vestibulum, non placerat sem vestibulum. ' +\n  'Cras mattis tempus vulputate. Nam in libero.'\n\ndescribe('formatLongName', () => {\n  it('should format long names', () => {\n    expect(formatLongName(longName, 50, 10, '...')).toEqual(\n      'Lorem ipsum dolor sit amet, consectet...in libero.',\n    )\n    expect(formatLongName(longName, 10, 5, '...')).toEqual('Lo...bero.')\n    expect(formatLongName(longName, 30, 1, '  ')).toEqual(\n      'Lorem ipsum dolor sit amet,  .',\n    )\n  })\n})\n\ndescribe('getDbIndex', () => {\n  it('should format long names', () => {\n    expect(getDbIndex(0)).toEqual('')\n    expect(getDbIndex(1)).toEqual('[db1]')\n    expect(getDbIndex(10)).toEqual('[db10]')\n  })\n})\n\ndescribe('formatNameShort', () => {\n  it('should format long values', () => {\n    expect(formatNameShort(longName)).toEqual(\n      'Lorem ipsum dolor sit amet, consectetur adipiscing... Nam in libero.',\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/modules.spec.ts",
    "content": "import { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport {\n  getDbWithModuleLoaded,\n  getRedisearchVersion,\n  IDatabaseModule,\n  isContainJSONModule,\n  isRedisearchAvailable,\n  sortModules,\n} from 'uiSrc/utils/modules'\n\nconst modules1: IDatabaseModule[] = [\n  { moduleName: 'JSON', abbreviation: 'RS' },\n  { moduleName: 'My1Module', abbreviation: 'MD' },\n  { moduleName: 'Redis Query Engine', abbreviation: 'RS' },\n]\nconst modules2: IDatabaseModule[] = [\n  { moduleName: '', abbreviation: '' },\n  { moduleName: '', abbreviation: '' },\n  { moduleName: 'Probabilistic', abbreviation: 'RS' },\n  { moduleName: '', abbreviation: '' },\n  { moduleName: '', abbreviation: '' },\n  { moduleName: 'MycvModule', abbreviation: 'MC' },\n  { moduleName: 'My1Module', abbreviation: 'MD' },\n  { moduleName: 'JSON', abbreviation: 'RS' },\n  { moduleName: 'My2Modul2e', abbreviation: 'MX' },\n  { moduleName: 'Redis Query Engine', abbreviation: 'RS' },\n]\n\nconst result1: IDatabaseModule[] = [\n  { moduleName: 'Redis Query Engine', abbreviation: 'RS' },\n  { moduleName: 'JSON', abbreviation: 'RS' },\n  { moduleName: 'My1Module', abbreviation: 'MD' },\n]\n\nconst result2: IDatabaseModule[] = [\n  { moduleName: 'Redis Query Engine', abbreviation: 'RS' },\n  { moduleName: 'JSON', abbreviation: 'RS' },\n  { moduleName: 'Probabilistic', abbreviation: 'RS' },\n  { moduleName: 'MycvModule', abbreviation: 'MC' },\n  { moduleName: 'My1Module', abbreviation: 'MD' },\n  { moduleName: 'My2Modul2e', abbreviation: 'MX' },\n  { moduleName: '', abbreviation: '' },\n  { moduleName: '', abbreviation: '' },\n  { moduleName: '', abbreviation: '' },\n  { moduleName: '', abbreviation: '' },\n]\n\ndescribe('sortModules', () => {\n  it('should proper sort modules list', () => {\n    expect(sortModules(modules1)).toEqual(result1)\n    expect(sortModules(modules2)).toEqual(result2)\n  })\n})\n\nconst nameToModule = (name: string) => ({ name })\n\nconst getOutputForRedisearchAvailable: any[] = [\n  [['1', 'json'].map(nameToModule), false],\n  [['1', 'uoeuoeu ueaooe'].map(nameToModule), false],\n  [['1', 'json', RedisDefaultModules.Search].map(nameToModule), true],\n  [['1', 'json', RedisDefaultModules.SearchLight].map(nameToModule), true],\n  [['1', 'json', RedisDefaultModules.FT].map(nameToModule), true],\n  [['1', 'json', RedisDefaultModules.FTL].map(nameToModule), true],\n]\n\ndescribe('isRedisearchAvailable', () => {\n  it.each(getOutputForRedisearchAvailable)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = isRedisearchAvailable(reply)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\nconst getOutputForReJSONAvailable: any[] = [\n  [['1', 'json'].map(nameToModule), false],\n  [['1', 'json', RedisDefaultModules.ReJSON].map(nameToModule), true],\n  [['1', 'json', RedisDefaultModules.SearchLight].map(nameToModule), false],\n  [\n    [\n      '1',\n      'json',\n      RedisDefaultModules.SearchLight,\n      RedisDefaultModules.ReJSON,\n    ].map(nameToModule),\n    true,\n  ],\n]\n\ndescribe('isContainJSONModule', () => {\n  it.each(getOutputForReJSONAvailable)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = isContainJSONModule(reply)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\nconst getDbWithModuleLoadedTests: Array<{\n  input: [any, string]\n  expected: any\n}> = [\n  {\n    input: [\n      [\n        { id: '1', modules: [{ name: 'module1' }] },\n        { id: '2', modules: [{ name: 'module1' }] },\n      ],\n      'module1',\n    ],\n    expected: { id: '1', modules: [{ name: 'module1' }] },\n  },\n  {\n    input: [\n      [\n        { id: '1', modules: [{ name: 'module2' }] },\n        { id: '2', modules: [{ name: 'module3' }] },\n      ],\n      'module1',\n    ],\n    expected: undefined,\n  },\n  {\n    input: [\n      [\n        { id: '1', modules: [{ name: 'redisgears' }] },\n        { id: '2', modules: [{ name: 'redisgears_2' }] },\n      ],\n      'redisgears',\n    ],\n    expected: { id: '1', modules: [{ name: 'redisgears' }] },\n  },\n]\n\ndescribe('getRedisearchVersion', () => {\n  it('should return undefined when modules is empty', () => {\n    const result = getRedisearchVersion([])\n    expect(result).toBeUndefined()\n  })\n\n  it('should return undefined when modules is undefined', () => {\n    const result = getRedisearchVersion()\n    expect(result).toBeUndefined()\n  })\n\n  it('should return semanticVersion when available', () => {\n    const result = getRedisearchVersion([\n      {\n        name: RedisDefaultModules.Search,\n        version: 20800,\n        semanticVersion: '2.8.0',\n      },\n    ])\n    expect(result).toBe('2.8.0')\n  })\n\n  it('should fall back to version number as string', () => {\n    const result = getRedisearchVersion([\n      { name: RedisDefaultModules.Search, version: 20800 },\n    ])\n    expect(result).toBe('20800')\n  })\n\n  it('should find FT module name', () => {\n    const result = getRedisearchVersion([\n      { name: 'ReJSON', version: 20400 },\n      {\n        name: RedisDefaultModules.FT,\n        version: 20800,\n        semanticVersion: '2.8.0',\n      },\n    ])\n    expect(result).toBe('2.8.0')\n  })\n\n  it('should return undefined when no RQE module present', () => {\n    const result = getRedisearchVersion([\n      { name: 'ReJSON', version: 20400 },\n      { name: 'timeseries', version: 10800 },\n    ])\n    expect(result).toBeUndefined()\n  })\n})\n\ndescribe('getDbWithModuleLoaded', () => {\n  it.each(getDbWithModuleLoadedTests)(\n    'for input: %s (reply), should be output: %s',\n    ({ input, expected }) => {\n      const result = getDbWithModuleLoaded(...input)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/monaco/cyber/completionProvider.spec.ts",
    "content": "import { getCompletionProvider } from 'uiSrc/utils/monaco/completionProvider'\n\ndescribe('getCypherCompletionProvider', () => {\n  it('should call getWordUntilPosition and getValueInRange', () => {\n    const provider = getCompletionProvider()\n\n    const positionMock = {}\n    const contextMock = {}\n    const tokenMock = {}\n\n    const modelMock = {\n      getWordUntilPosition: jest.fn().mockImplementation(() => ({})),\n      getValueInRange: jest.fn().mockImplementation(() => ''),\n    }\n\n    provider.provideCompletionItems(\n      modelMock,\n      positionMock,\n      contextMock,\n      tokenMock,\n    )\n\n    expect(modelMock.getWordUntilPosition).toBeCalled()\n    expect(modelMock.getValueInRange).toBeCalled()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts",
    "content": "import { getCypherMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/cypherTokens'\nimport { getJmespathMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/jmespathTokens'\nimport { getSqliteFunctionsMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/sqliteFunctionsTokens'\nimport { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensSubRedis'\nimport { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands'\n\ndescribe('getCypherMonarchTokensProvider', () => {\n  it('should be truthy', () => {\n    expect(getCypherMonarchTokensProvider()).toBeTruthy()\n  })\n})\n\ndescribe('getJmespathMonarchTokensProvider', () => {\n  it('should be truthy', () => {\n    expect(getJmespathMonarchTokensProvider([])).toBeTruthy()\n  })\n})\n\ndescribe('getSqliteFunctionsMonarchTokensProvider', () => {\n  it('should be truthy', () => {\n    expect(getSqliteFunctionsMonarchTokensProvider([])).toBeTruthy()\n  })\n})\n\ndescribe('getRediSearchMonarchTokensProvider', () => {\n  it('should be truthy', () => {\n    expect(getRediSearchSubRedisMonarchTokensProvider([])).toBeTruthy()\n  })\n\n  it('should be truthy with command', () => {\n    const commands = Object.keys(MOCKED_REDIS_COMMANDS).map((key) => ({\n      ...MOCKED_REDIS_COMMANDS[key],\n      name: key,\n    }))\n    expect(getRediSearchSubRedisMonarchTokensProvider(commands)).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/monaco/monacoRedisCompletionProvider.spec.ts",
    "content": "import { MOCK_COMMANDS_SPEC } from 'uiSrc/constants'\nimport {\n  createDependencyProposals,\n  getCommandMarkdown,\n  getDocUrlForCommand,\n} from 'uiSrc/utils'\n\nconst spec = { GET: MOCK_COMMANDS_SPEC.GET }\n\ndescribe('createDependencyProposals', () => {\n  it('should prepare completion', () => {\n    const result = createDependencyProposals(spec)\n    expect(result).toEqual({\n      GET: {\n        label: 'GET',\n        kind: 1,\n        detail: 'GET key',\n        // eslint-disable-next-line no-template-curly-in-string\n        insertText: 'GET ${1:key}',\n        documentation: {\n          value: getCommandMarkdown(\n            MOCK_COMMANDS_SPEC.GET,\n            getDocUrlForCommand('GET'),\n          ),\n        },\n        insertTextRules: 4,\n        range: {\n          endColumn: 0,\n          endLineNumber: 0,\n          startColumn: 0,\n          startLineNumber: 0,\n        },\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts",
    "content": "import {\n  multilineCommandToOneLine,\n  removeMonacoComments,\n  splitMonacoValuePerLines,\n  findArgIndexByCursor,\n  isParamsLine,\n  getMonacoLines,\n  getCommandsFromQuery,\n  splitQueryByArgs,\n} from 'uiSrc/utils'\n\ndescribe('removeMonacoComments', () => {\n  const cases = [\n    // Multiline command with comments\n    [\n      'set\\r\\n  foo // key name\\r\\n  // comment line\\r\\n  bar // key value',\n      'set\\r\\n  foo \\r\\n  bar',\n    ],\n    // Multiline command with comments slashes in the double quotes\n    [\n      'set\\r\\n  foo \"// key name\"\\r\\n  // comment line\\r\\n // key value',\n      'set\\r\\n  foo \"// key name\"',\n    ],\n    // Multiline command with comments slashes in the single quotes\n    [\n      \"set\\r\\n  foo '// key name'\\r\\n  // comment line\\r\\n // key value\",\n      \"set\\r\\n  foo '// key name'\",\n    ],\n    // Multiline command with comments slashes in the apostrophes\n    [\n      'set\\r\\n  foo `// key name`\\r\\n  // comment line\\r\\n // key value',\n      'set\\r\\n  foo `// key name`',\n    ],\n    // Multiline command with comments\n    [\n      'set\\n  foo // key name\\n  // comment line\\n  bar // key value',\n      'set\\n  foo \\n  bar',\n    ],\n    // Multiline command with comments and single-line command\n    ['// comment line\\nset\\n foo\\n bar\\nget foo', 'set\\n foo\\n bar\\nget foo'],\n  ]\n  test.each(cases)(\n    'given %p as argument, returns %p',\n    (arg: string, expectedResult) => {\n      const result = removeMonacoComments(arg)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\ndescribe('multilineCommandToOneLine', () => {\n  const cases = [\n    // Multiline command and indent with single space\n    ['set\\r\\n foo\\r\\n bar', 'set foo bar'],\n    // Multiline command and indent with multiple spaces\n    ['set\\n    foo\\n    bar', 'set foo bar'],\n    // Multiline command with quotes\n    [\n      \"\\\"hset test2\\n 'http://' 'http://123'\\n 'test//test' 'test//test'\\\"\",\n      \"\\\"hset test2 'http://' 'http://123' 'test//test' 'test//test'\\\"\",\n    ],\n  ]\n  test.each(cases)(\n    'given %p as argument, returns %p',\n    (arg: string, expectedResult) => {\n      const result = multilineCommandToOneLine(arg)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\ndescribe('splitMonacoValuePerLines', () => {\n  const cases = [\n    // Multi commands\n    ['get test\\nget test2\\nget bar', ['get test', 'get test2', 'get bar']],\n    // Multi commands a lot of lines\n    [\n      'get test\\nget test2\\nget bar\\nget bar\\nget bar\\nget bar\\nget bar\\nget bar',\n      [\n        'get test',\n        'get test2',\n        'get bar',\n        'get bar',\n        'get bar',\n        'get bar',\n        'get bar',\n        'get bar',\n      ],\n    ],\n    // Multi commands with repeating\n    [\n      'get test\\n3 get test2\\nget bar',\n      ['get test', 'get test2', 'get test2', 'get test2', 'get bar'],\n    ],\n    // Multi commands with repeating syntax error\n    ['get test\\n3get test2\\nget bar', ['get test', '3get test2', 'get bar']],\n    // Multi commands with parameters and repeating syntax error\n    [\n      '[results=group;mode=raw]info\\nget test\\n3get test2\\nget bar',\n      ['info', 'get test', '3get test2', 'get bar'],\n    ],\n  ]\n  test.each(cases)(\n    'given %p as argument, returns %p',\n    (arg: string, expectedResult) => {\n      const result = splitMonacoValuePerLines(arg)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\ndescribe('findArgIndexByCursor', () => {\n  const cases = [\n    [['get', 'foo', 'bar'], 'get foo bar', 10, 2],\n    [['get', 'foo', 'bar'], 'get foo bar', 5, 1],\n    [['get', 'foo', 'bar'], 'get foo \\n      bar', 17, 2],\n    [['get', 'foo', 'bar'], 'get foo \\n\\n\\n      bar', 19, 2],\n    [['get', 'foo', 'bar'], 'get foo \\n\\n\\n      bar', 25, null],\n  ]\n  test.each(cases)(\n    'given %p as args, %p as fullQuery, %p as cursor position, returns %p',\n    (\n      args: string[],\n      fullQuery: string,\n      cursorPosition: number,\n      expectedResult,\n    ) => {\n      const result = findArgIndexByCursor(args, fullQuery, cursorPosition)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\ndescribe('isParamsLine', () => {\n  const cases = [\n    ['[1]', true],\n    ['[1', false],\n    ['[groups=raw]', true],\n    ['[groups=raw]', true],\n    ['1]', false],\n    ['1[groups=raw]', false],\n    ['[groups==]aw', true],\n  ]\n  test.each(cases)(\n    'given %p as argument, returns %p',\n    (arg: string, expectedResult) => {\n      const result = isParamsLine(arg)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\ndescribe('getMonacoLines', () => {\n  const cases = [\n    ['1', ['1']],\n    ['[1', ['[1']],\n    ['1\\n2', ['1', '2']],\n    [\n      '[groups=raw] \\neget test\\nget test2',\n      ['[groups=raw] ', 'eget test', 'get test2'],\n    ],\n  ]\n  test.each(cases)(\n    'given %p as argument, returns %p',\n    (arg: string, expectedResult) => {\n      const result = getMonacoLines(arg)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\ndescribe('getCommandsFromQuery', () => {\n  const commandsArray = ['FT.INFO', 'INFO']\n  const cases = [\n    ['ft.info a b \\ninfo\\nany command', 'FT.INFO;INFO;any'],\n    ['a b \\nset a b', 'a;set'],\n    ['\\n\\ninfo \\n\\nany command', 'INFO;any'],\n    ['info', 'INFO'],\n  ]\n  test.each(cases)(\n    'given %p as argument, returns %p',\n    (query: string, expectedResult) => {\n      const result = getCommandsFromQuery(query, commandsArray)\n      expect(result).toEqual(expectedResult)\n    },\n  )\n})\n\nconst splitQueryByArgsTests: Array<{\n  input: [string, number?]\n  result: any\n}> = [\n  {\n    input: ['FT.SEARCH \"idx:bicycle\" \"\" WITHSORTKEYS'],\n    result: {\n      args: [[], ['FT.SEARCH', '\"idx:bicycle\"', '\"\"', 'WITHSORTKEYS']],\n      cursor: {\n        argLeftOffset: 10,\n        argRightOffset: 23,\n        isCursorInQuotes: false,\n        nextCursorChar: 'F',\n        prevCursorChar: '',\n      },\n    },\n  },\n  {\n    input: ['FT.SEARCH \"idx:bicycle\" \"\" WITHSORTKEYS', 17],\n    result: {\n      args: [['FT.SEARCH'], ['\"idx:bicycle\"', '\"\"', 'WITHSORTKEYS']],\n      cursor: {\n        argLeftOffset: 10,\n        argRightOffset: 23,\n        isCursorInQuotes: true,\n        nextCursorChar: 'c',\n        prevCursorChar: 'i',\n      },\n    },\n  },\n  {\n    input: ['FT.SEARCH \"idx:bicycle\" \"\" WITHSORTKEYS', 39],\n    result: {\n      args: [['FT.SEARCH', '\"idx:bicycle\"', '\"\"'], ['WITHSORTKEYS']],\n      cursor: {\n        argLeftOffset: 27,\n        argRightOffset: 39,\n        isCursorInQuotes: false,\n        nextCursorChar: '',\n        prevCursorChar: 'S',\n      },\n    },\n  },\n  {\n    input: ['FT.SEARCH \"idx:bicycle\" \"\" WITHSORTKEYS ', 40],\n    result: {\n      args: [['FT.SEARCH', '\"idx:bicycle\"', '\"\"', 'WITHSORTKEYS'], []],\n      cursor: {\n        argLeftOffset: 0,\n        argRightOffset: 0,\n        isCursorInQuotes: false,\n        nextCursorChar: '',\n        prevCursorChar: '',\n      },\n    },\n  },\n  {\n    input: ['FT.SEARCH \"idx:bicycle \\\\\" \\\\\"\" \"\" WITHSORTKEYS ', 46],\n    result: {\n      args: [['FT.SEARCH', '\"idx:bicycle \" \"\"', '\"\"', 'WITHSORTKEYS'], []],\n      cursor: {\n        argLeftOffset: 0,\n        argRightOffset: 0,\n        isCursorInQuotes: false,\n        nextCursorChar: '',\n        prevCursorChar: '',\n      },\n    },\n  },\n]\n\ndescribe('splitQueryByArgs', () => {\n  it.each(splitQueryByArgsTests)(\n    'should return for %input proper result',\n    ({ input, result }) => {\n      const testResult = splitQueryByArgs(...input)\n      expect(testResult).toEqual(result)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/monitorUtils.spec.ts",
    "content": "import { getFormatTime } from '../monitorUtils'\n\nconst getOutputForFormatTime: any[] = [\n  [undefined, 'Invalid time'],\n  [null, 'Invalid time'],\n  [{}, 'Invalid time'],\n  ['oeuoeu', 'Invalid time'],\n  [1, 'Invalid time'],\n  [11641450853, 'Invalid time'],\n  ['1641450853.668074[0', 'Invalid time'],\n  ['1641450853.668074', ':34:13.668'],\n  ['1641450854.612083', ':34:14.612'],\n  ['1641450856.616102', ':34:16.616'],\n  ['1641450858.616121', ':34:18.616'],\n]\n\ndescribe('formatToText', () => {\n  it.each(getOutputForFormatTime)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = getFormatTime(reply)\n      expect(result).toContain(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/nodes.json",
    "content": "[\n  {\n    \"name\": \"hash\",\n    \"children\": [\n      {\n        \"name\": \"keys:keys\",\n        \"children\": [],\n        \"keys\": {\n          \"hash:3\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 51]\n            },\n            \"nameString\": \"hash:3\"\n          },\n          \"hash:153\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 49, 53, 51]\n            },\n            \"nameString\": \"hash:153\"\n          },\n          \"hash:2\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 50]\n            },\n            \"nameString\": \"hash:2\"\n          },\n          \"hash:21\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 50, 49]\n            },\n            \"nameString\": \"hash:21\"\n          },\n          \"hash:151\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 49, 53, 49]\n            },\n            \"nameString\": \"hash:151\"\n          },\n          \"hash:11\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 49, 49]\n            },\n            \"nameString\": \"hash:11\"\n          },\n          \"hash:15\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 49, 53]\n            },\n            \"nameString\": \"hash:15\"\n          },\n          \"hash:152\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 49, 53, 50]\n            },\n            \"nameString\": \"hash:152\"\n          },\n          \"hash:5\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 58, 53]\n            },\n            \"nameString\": \"hash:5\"\n          }\n        },\n        \"keyCount\": 9,\n        \"fullName\": \"hash:keys:keys:\",\n        \"keyApproximate\": 50,\n        \"id\": \"0.g9y9ox4nau\"\n      },\n      {\n        \"name\": \"string\",\n        \"children\": [\n          {\n            \"name\": \"keys:keys\",\n            \"children\": [],\n            \"keys\": {\n              \"hash:string:2\": {\n                \"name\": {\n                  \"type\": \"Buffer\",\n                  \"data\": [\n                    104, 97, 115, 104, 58, 115, 116, 114, 105, 110, 103, 58, 50\n                  ]\n                },\n                \"nameString\": \"hash:string:2\"\n              },\n              \"hash:string:1\": {\n                \"name\": {\n                  \"type\": \"Buffer\",\n                  \"data\": [\n                    104, 97, 115, 104, 58, 115, 116, 114, 105, 110, 103, 58, 49\n                  ]\n                },\n                \"nameString\": \"hash:string:1\"\n              }\n            },\n            \"keyCount\": 2,\n            \"fullName\": \"hash:string:keys:keys:\",\n            \"keyApproximate\": 11.11111111111111,\n            \"id\": \"0.baqi0l0e9j6\"\n          },\n          {\n            \"name\": \"1\",\n            \"children\": [\n              {\n                \"name\": \"1\",\n                \"children\": [\n                  {\n                    \"name\": \"1\",\n                    \"children\": [\n                      {\n                        \"name\": \"keys:keys\",\n                        \"children\": [],\n                        \"keys\": {\n                          \"hash:string:1:1:1:3\": {\n                            \"name\": {\n                              \"type\": \"Buffer\",\n                              \"data\": [\n                                104, 97, 115, 104, 58, 115, 116, 114, 105, 110,\n                                103, 58, 49, 58, 49, 58, 49, 58, 51\n                              ]\n                            },\n                            \"nameString\": \"hash:string:1:1:1:3\"\n                          },\n                          \"hash:string:1:1:1:5\": {\n                            \"name\": {\n                              \"type\": \"Buffer\",\n                              \"data\": [\n                                104, 97, 115, 104, 58, 115, 116, 114, 105, 110,\n                                103, 58, 49, 58, 49, 58, 49, 58, 53\n                              ]\n                            },\n                            \"nameString\": \"hash:string:1:1:1:5\"\n                          },\n                          \"hash:string:1:1:1:1\": {\n                            \"name\": {\n                              \"type\": \"Buffer\",\n                              \"data\": [\n                                104, 97, 115, 104, 58, 115, 116, 114, 105, 110,\n                                103, 58, 49, 58, 49, 58, 49, 58, 49\n                              ]\n                            },\n                            \"nameString\": \"hash:string:1:1:1:1\"\n                          },\n                          \"hash:string:1:1:1:2\": {\n                            \"name\": {\n                              \"type\": \"Buffer\",\n                              \"data\": [\n                                104, 97, 115, 104, 58, 115, 116, 114, 105, 110,\n                                103, 58, 49, 58, 49, 58, 49, 58, 50\n                              ]\n                            },\n                            \"nameString\": \"hash:string:1:1:1:2\"\n                          },\n                          \"hash:string:1:1:1:4\": {\n                            \"name\": {\n                              \"type\": \"Buffer\",\n                              \"data\": [\n                                104, 97, 115, 104, 58, 115, 116, 114, 105, 110,\n                                103, 58, 49, 58, 49, 58, 49, 58, 52\n                              ]\n                            },\n                            \"nameString\": \"hash:string:1:1:1:4\"\n                          }\n                        },\n                        \"keyCount\": 5,\n                        \"fullName\": \"hash:string:1:1:1:keys:keys:\",\n                        \"keyApproximate\": 27.77777777777778,\n                        \"id\": \"0.6uk30kg4cll\"\n                      }\n                    ],\n                    \"keyCount\": 5,\n                    \"fullName\": \"hash:string:1:1:1:\",\n                    \"keyApproximate\": 27.77777777777778,\n                    \"id\": \"0.mdmi066idef\"\n                  }\n                ],\n                \"keyCount\": 5,\n                \"fullName\": \"hash:string:1:1:\",\n                \"keyApproximate\": 27.77777777777778,\n                \"id\": \"0.i4phiogdhog\"\n              }\n            ],\n            \"keyCount\": 5,\n            \"fullName\": \"hash:string:1:\",\n            \"keyApproximate\": 27.77777777777778,\n            \"id\": \"0.fll9zclury\"\n          }\n        ],\n        \"keyCount\": 7,\n        \"fullName\": \"hash:string:\",\n        \"keyApproximate\": 38.88888888888889,\n        \"id\": \"0.0nrbpnjpg77i\"\n      }\n    ],\n    \"keyCount\": 16,\n    \"fullName\": \"hash:\",\n    \"keyApproximate\": 88.88888888888889,\n    \"id\": \"0.75h78cd80yk\"\n  },\n  {\n    \"name\": \"hash2\",\n    \"children\": [\n      {\n        \"name\": \"keys:keys\",\n        \"children\": [],\n        \"keys\": {\n          \"hash2:2\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 50, 58, 50]\n            },\n            \"nameString\": \"hash2:2\"\n          },\n          \"hash2:1\": {\n            \"name\": {\n              \"type\": \"Buffer\",\n              \"data\": [104, 97, 115, 104, 50, 58, 49]\n            },\n            \"nameString\": \"hash2:1\"\n          }\n        },\n        \"keyCount\": 2,\n        \"fullName\": \"hash2:keys:keys:\",\n        \"keyApproximate\": 11.11111111111111,\n        \"id\": \"0.g1rrlj3k0d\"\n      }\n    ],\n    \"keyCount\": 2,\n    \"fullName\": \"hash2:\",\n    \"keyApproximate\": 11.11111111111111,\n    \"id\": \"0.tge74eh7psa\"\n  }\n]\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/oauth/cloudSsoUtm.spec.tsx",
    "content": "import { getCloudSsoUtmParams } from 'uiSrc/utils/oauth/cloudSsoUtm'\nimport { CloudSsoUtmCampaign, OAuthSocialSource } from 'uiSrc/slices/interfaces'\n\nconst getCloudSsoUtmCampaignTestCases = [\n  [OAuthSocialSource.ListOfDatabases, CloudSsoUtmCampaign.ListOfDatabases],\n  [\n    OAuthSocialSource.DatabaseConnectionList,\n    CloudSsoUtmCampaign.ListOfDatabases,\n  ],\n  [OAuthSocialSource.BrowserSearch, CloudSsoUtmCampaign.BrowserSearch],\n  [OAuthSocialSource.RediSearch, CloudSsoUtmCampaign.Workbench],\n  [OAuthSocialSource.RedisJSON, CloudSsoUtmCampaign.Workbench],\n  [OAuthSocialSource.RedisTimeSeries, CloudSsoUtmCampaign.Workbench],\n  [OAuthSocialSource.RedisGraph, CloudSsoUtmCampaign.Workbench],\n  [OAuthSocialSource.RedisBloom, CloudSsoUtmCampaign.Workbench],\n  [OAuthSocialSource.BrowserContentMenu, CloudSsoUtmCampaign.BrowserOverview],\n  [OAuthSocialSource.BrowserFiltering, CloudSsoUtmCampaign.BrowserFilter],\n  [OAuthSocialSource.WelcomeScreen, CloudSsoUtmCampaign.WelcomeScreen],\n  [OAuthSocialSource.Tutorials, CloudSsoUtmCampaign.Tutorial],\n  [OAuthSocialSource.Autodiscovery, CloudSsoUtmCampaign.AutoDiscovery],\n  [OAuthSocialSource.NavigationMenu, CloudSsoUtmCampaign.NavigationMenu],\n  [OAuthSocialSource.AddDbForm, CloudSsoUtmCampaign.AddDbForm],\n  [null, CloudSsoUtmCampaign.Unknown],\n  [undefined, CloudSsoUtmCampaign.Unknown],\n]\n\ndescribe('getCloudSsoUtmCampaign', () => {\n  test.each(getCloudSsoUtmCampaignTestCases)('%j', (input, expected) => {\n    expect(getCloudSsoUtmParams(input)).toEqual(\n      new URLSearchParams([\n        ['source', 'redisinsight'],\n        ['medium', 'sso'],\n        ['campaign', expected],\n      ]),\n    )\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/onboarding.spec.tsx",
    "content": "import React from 'react'\nimport { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding'\nimport { render, screen } from 'uiSrc/utils/test-utils'\nimport { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features'\n\njest.mock('uiSrc/slices/app/features', () => ({\n  ...jest.requireActual('uiSrc/slices/app/features'),\n  appFeatureOnboardingSelector: jest.fn().mockReturnValue({\n    currentStep: 1,\n    isActive: true,\n    totalSteps: 10,\n  }),\n}))\n\ndescribe('renderOnboardingTourWithChild', () => {\n  it('should render child into tour', () => {\n    render(\n      <div>\n        {renderOnboardingTourWithChild(\n          <span data-testid=\"span\" />,\n          {\n            options: ONBOARDING_FEATURES.BROWSER_PAGE,\n            anchorPosition: 'downLeft',\n          },\n          true,\n        )}\n      </div>,\n    )\n\n    expect(screen.getByTestId('span')).toBeInTheDocument()\n    expect(screen.getByTestId('onboarding-tour')).toBeInTheDocument()\n  })\n\n  it('should render child without tour', () => {\n    render(\n      <div>\n        {renderOnboardingTourWithChild(\n          <span data-testid=\"span\" />,\n          {\n            options: ONBOARDING_FEATURES.BROWSER_PAGE,\n            anchorPosition: 'downLeft',\n          },\n          false,\n        )}\n      </div>,\n    )\n\n    expect(screen.getByTestId('span')).toBeInTheDocument()\n    expect(screen.queryByTestId('onboarding-tour')).not.toBeInTheDocument()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/parseRedisUrl.spec.ts",
    "content": "import { parseRedisUrl } from 'uiSrc/utils/parseRedisUrl'\n\nconst defaultRedisParams = {\n  protocol: 'redis',\n  username: '',\n  password: '',\n  port: undefined,\n  dbNumber: undefined,\n}\n\nconst parseRedisUrlTests: Array<[string, any]> = [\n  ['http://user:pass@localhost:6380', null],\n  ['localhost', null],\n  [\n    'localhost:6379',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6379,\n      hostname: 'localhost:6379',\n    },\n  ],\n  [\n    'redis://localhost',\n    { ...defaultRedisParams, host: 'localhost', hostname: 'localhost' },\n  ],\n  [\n    'redis://:@localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'redis://user:pass/@localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n      username: 'user',\n      password: 'pass/',\n    },\n  ],\n  [\n    'redis://user:pa@ss@localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n      username: 'user',\n      password: 'pa@ss',\n    },\n  ],\n  [\n    'redis://us@er:pa@ss@localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n      username: 'us@er',\n      password: 'pa@ss',\n    },\n  ],\n  [\n    'redis://us@er:pa@:ss@localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n      username: 'us@er',\n      password: 'pa@:ss',\n    },\n  ],\n  [\n    'redis://localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'redis://@localhost:6380',\n    {\n      ...defaultRedisParams,\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'redis://user@localhost:6380',\n    {\n      ...defaultRedisParams,\n      username: 'user',\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'redis://:pass@localhost:6380',\n    {\n      ...defaultRedisParams,\n      password: 'pass',\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'redis://user:pass@localhost:6380',\n    {\n      ...defaultRedisParams,\n      username: 'user',\n      password: 'pass',\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'rediss://user:pa%712ss@localhost:6380',\n    {\n      ...defaultRedisParams,\n      protocol: 'rediss',\n      username: 'user',\n      password: 'pa%712ss',\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n    },\n  ],\n  [\n    'rediss://d&@&21^$:pa%@7:12:ss@local-host-123.net.com:6380',\n    {\n      ...defaultRedisParams,\n      protocol: 'rediss',\n      username: 'd&@&21^$',\n      password: 'pa%@7:12:ss',\n      host: 'local-host-123.net.com',\n      port: 6380,\n      hostname: 'local-host-123.net.com:6380',\n    },\n  ],\n  [\n    'rediss://user:pa%712ss@localhost:6380/2',\n    {\n      protocol: 'rediss',\n      username: 'user',\n      password: 'pa%712ss',\n      host: 'localhost',\n      port: 6380,\n      hostname: 'localhost:6380',\n      dbNumber: 2,\n    },\n  ],\n]\n\ndescribe('parseRedisUrl', () => {\n  it.each(parseRedisUrlTests)(\n    'for input: %s (index), %s (shift), should be output: %s',\n    (url, expected) => {\n      const result = parseRedisUrl(url)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/parseResponse.spec.ts",
    "content": "import { omit } from 'lodash'\nimport { parseKeysListResponse } from 'uiSrc/utils'\n\nconst nextCursor = '127.0.0.1:7000@100||127.0.0.1:7002@9'\n\ndescribe('parseKeysListResponse', () => {\n  it('should handle empty array in response', () => {\n    const currentState = {\n      total: 0,\n      scanned: 0,\n      nextCursor: '0',\n      keys: [],\n      shardsMeta: {},\n    }\n    const scanResponse = []\n    expect(\n      parseKeysListResponse(currentState.shardsMeta, scanResponse),\n    ).toEqual(currentState)\n  })\n  it('should summarize data with initial state (standalone)', () => {\n    const currentState = {\n      nextCursor: '0',\n      total: 0,\n      scanned: 0,\n      shardsMeta: {},\n    }\n    const scanResponse = [\n      {\n        cursor: 100,\n        total: 200,\n        scanned: 150,\n        keys: ['keywithdetails'],\n      },\n    ]\n    const result = parseKeysListResponse(currentState.shardsMeta, scanResponse)\n    expect(result).toEqual({\n      ...omit(scanResponse[0], 'cursor'),\n      nextCursor: `${scanResponse[0].cursor}`,\n      shardsMeta: {\n        standalone: omit(scanResponse[0], 'keys'),\n      },\n    })\n  })\n  it('should summarize data with existing one (standalone)', () => {\n    const currentState = {\n      nextCursor: '100',\n      total: 200,\n      scanned: 150,\n      shardsMeta: {\n        standalone: {\n          cursor: 100,\n          total: 200,\n          scanned: 150,\n        },\n      },\n    }\n    const scanResponse = [\n      {\n        cursor: 0,\n        total: 201,\n        scanned: 150,\n        keys: ['keywithdetails'],\n      },\n    ]\n    const result = parseKeysListResponse(currentState.shardsMeta, scanResponse)\n    expect(result).toEqual({\n      ...omit(scanResponse[0], 'cursor'),\n      scanned: scanResponse[0].total,\n      nextCursor: `${scanResponse[0].cursor}`,\n      shardsMeta: {\n        standalone: {\n          ...omit(scanResponse[0], 'keys'),\n          scanned: scanResponse[0].total,\n        },\n      },\n    })\n  })\n  it('should summarize data  with initial state (cluster)', () => {\n    const currentState = {\n      nextCursor,\n      total: 200 + 50 + 400,\n      scanned: 150 + 50 + 150,\n      shardsMeta: {},\n    }\n    const scanResponse = [\n      {\n        cursor: 100,\n        total: 200,\n        scanned: 150,\n        keys: ['shard1_key'],\n        host: '127.0.0.1',\n        port: 7000,\n      },\n      {\n        cursor: 0,\n        total: 50,\n        scanned: 150,\n        keys: [],\n        host: '127.0.0.1',\n        port: 7001,\n      },\n      {\n        cursor: 9,\n        total: 400,\n        scanned: 150,\n        keys: ['shard3_key'],\n        host: '127.0.0.1',\n        port: 7002,\n      },\n    ]\n    const result = parseKeysListResponse(currentState.shardsMeta, scanResponse)\n    expect(result).toEqual({\n      nextCursor,\n      total: 200 + 50 + 400,\n      scanned: 150 + 50 + 150,\n      keys: ['shard1_key', 'shard3_key'],\n      shardsMeta: {\n        '127.0.0.1:7000': {\n          cursor: 100,\n          total: 200,\n          scanned: 150,\n          host: '127.0.0.1',\n          port: 7000,\n        },\n        '127.0.0.1:7001': {\n          cursor: 0,\n          total: 50,\n          scanned: 50,\n          host: '127.0.0.1',\n          port: 7001,\n        },\n        '127.0.0.1:7002': {\n          cursor: 9,\n          total: 400,\n          scanned: 150,\n          host: '127.0.0.1',\n          port: 7002,\n        },\n      },\n    })\n  })\n  it('should summarize data  with initial one (cluster)', () => {\n    const currentState = {\n      nextCursor: '127.0.0.1:7000@100||127.0.0.1:7002@9',\n      total: 200 + 50 + 400,\n      scanned: 150 + 50 + 150,\n      keys: ['shard1_key', 'shard3_key'],\n      shardsMeta: {\n        '127.0.0.1:7000': {\n          cursor: 100,\n          total: 200,\n          scanned: 150,\n          host: '127.0.0.1',\n          port: 7000,\n        },\n        '127.0.0.1:7001': {\n          cursor: 0,\n          total: 50,\n          scanned: 50,\n          host: '127.0.0.1',\n          port: 7001,\n        },\n        '127.0.0.1:7002': {\n          cursor: 9,\n          total: 400,\n          scanned: 150,\n          host: '127.0.0.1',\n          port: 7002,\n        },\n      },\n    }\n    const scanResponse = [\n      {\n        cursor: 0,\n        total: 201,\n        scanned: 150,\n        keys: ['new_shard1_key'],\n        host: '127.0.0.1',\n        port: 7000,\n      },\n      {\n        cursor: 18,\n        total: 400,\n        scanned: 150,\n        keys: ['new_shard3_key'],\n        host: '127.0.0.1',\n        port: 7002,\n      },\n    ]\n    const result = parseKeysListResponse(currentState.shardsMeta, scanResponse)\n    expect(result).toEqual({\n      nextCursor: '127.0.0.1:7002@18',\n      total: 201 + 50 + 400,\n      scanned: 201 + 50 + 300,\n      keys: ['new_shard1_key', 'new_shard3_key'],\n      shardsMeta: {\n        '127.0.0.1:7000': {\n          cursor: 0,\n          total: 201,\n          scanned: 201,\n          host: '127.0.0.1',\n          port: 7000,\n        },\n        '127.0.0.1:7001': {\n          cursor: 0,\n          total: 50,\n          scanned: 50,\n          host: '127.0.0.1',\n          port: 7001,\n        },\n        '127.0.0.1:7002': {\n          cursor: 18,\n          total: 400,\n          scanned: 300,\n          host: '127.0.0.1',\n          port: 7002,\n        },\n      },\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/pathUtil.spec.ts",
    "content": "import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService'\nimport { getFileUrlFromMd } from '../pathUtil'\n\njest.mock('unist-util-visit')\nconst TUTORIAL_PATH = 'static/custom-tutorials/tutorial-id'\nconst GUIDES_PATH = 'static/guides'\nconst testCases = [\n  {\n    url: '../../../_images/relative.png',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`,\n  },\n  {\n    url: '/_images/relative.png',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: `${RESOURCES_BASE_URL}${TUTORIAL_PATH}/_images/relative.png`,\n  },\n  {\n    url: '/_images/relative.png',\n    path: `${GUIDES_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: `${RESOURCES_BASE_URL}${GUIDES_PATH}/_images/relative.png`,\n  },\n  {\n    url: '/_images/relative.png',\n    path: '/unknown-path/lvl1/lvl2/lvl3/intro.md',\n    result: `${RESOURCES_BASE_URL}unknown-path/lvl1/lvl2/lvl3/intro.md/_images/relative.png`,\n  },\n  {\n    url: 'https://somesite.test/image.png',\n    path: `${TUTORIAL_PATH}/lvl1/lvl2/lvl3/intro.md`,\n    result: 'https://somesite.test/image.png',\n  },\n]\ndescribe('getFileUrlFromMd', () => {\n  testCases.forEach((tc) => {\n    it(`should return ${tc.result} for url:${tc.url}, path: ${tc.path} `, () => {\n      const url = getFileUrlFromMd(tc.url, tc.path)\n      expect(url).toEqual(tc.result)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/plugins.spec.ts",
    "content": "import { getVisualizationsByCommand } from 'uiSrc/utils'\nimport { IPluginVisualization } from 'uiSrc/slices/interfaces'\n\ndescribe('getVisualizationsByCommand', () => {\n  const getVisualizationsByCommandTests: [string, number][] = [\n    ['ft.search sa', 2],\n    ['ft.get zxc', 2],\n    ['command ft. zxc zxcz ft', 0],\n    ['command ft', 0],\n    ['any command', 0],\n    ['get key', 1],\n  ]\n\n  const visualizations = [\n    { matchCommands: ['ft.search', 'ft.get'] },\n    { matchCommands: ['ft._list'] },\n    { matchCommands: ['ft.*'] },\n    { matchCommands: ['get'] },\n  ] as IPluginVisualization[]\n\n  test.each(getVisualizationsByCommandTests)(\n    'for %j, should be %i',\n    (input, expected) => {\n      // @ts-ignore\n      const result = getVisualizationsByCommand(input, visualizations)\n      expect(result).toHaveLength(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/rdi/getUrlRdiInstance.spec.ts",
    "content": "import { getRdiUrl } from 'uiSrc/utils'\n\nconst getRdiUrlTests: [string[], string][] = [\n  [[], '/rdi/'],\n  [[''], '/rdi/'],\n  [['sub_path'], '/rdi/sub_path'],\n  [['path1', 'path2'], '/rdi/path1/path2'],\n  [['path1', 'path2', 'path3'], '/rdi/path1/path2/path3'],\n]\n\ndescribe('getRdiUrl', () => {\n  it.each(getRdiUrlTests)(\n    'for input: %s (input), should be output: %s',\n    (input, expected) => {\n      const result = getRdiUrl(...input)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/rdi/pipeline.spec.ts",
    "content": "import { isEqualPipelineFile } from 'uiSrc/utils'\n\nconst isEqualPipelineFileTests: [[string, string], boolean][] = [\n  [['', ''], true],\n  [[':', ':'], false],\n  [\n    [\n      `\n  source:\n    server_name: localhost\n    schema: public\n    table: employee\n    row_format: full\n  `,\n      `\n  source:\n    server_name: localhost\n    schema: public\n    table: employee\n    row_format: full\n  `,\n    ],\n    true,\n  ],\n  [\n    [\n      `\n  source:\n    server_name: localhost\n      schema: public\n    table: employee\n    row_format: full\n  `,\n      `\n  source:\n    server_name: localhost\n    schema: public\n    table: employee\n    row_format: full\n  `,\n    ],\n    false,\n  ],\n  [\n    [\n      `\n  source:\n    server_name: localhost\n    schema: public\n    table: employee\n    row_format: full\n  `,\n      `\n  source:\n    server_name: localhost\n    table: employee\n    schema: public\n    row_format: full\n  `,\n    ],\n    true,\n  ],\n  [\n    [\n      `\n  source:\n    server_name: localhost\n    schema: public\n    # some\n    table: employee\n    row_format: full\n    #comment\n  `,\n      `\n  source:\n    server_name: localhost\n    table: employee\n    #foo bar\n    schema: public\n    row_format: full\n  `,\n    ],\n    true,\n  ],\n]\n\ndescribe('isEqualPipelineFile', () => {\n  it.each(isEqualPipelineFileTests)(\n    'for input: %s (input), should be output: %s',\n    (input, expected) => {\n      const result = isEqualPipelineFile(...input)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/recommendation/helper.spec.ts",
    "content": "import { MOCK_RECOMMENDATIONS } from 'uiSrc/constants/mocks/mock-recommendations'\nimport { sortRecommendations, replaceVariables } from '../../recommendation'\n\nconst sortRecommendationsTests = [\n  {\n    input: [],\n    expected: [],\n  },\n  {\n    input: [\n      { name: 'luaScript' },\n      { name: 'bigSets' },\n      { name: 'searchIndexes' },\n    ],\n    expected: [\n      { name: 'searchIndexes' },\n      { name: 'bigSets' },\n      { name: 'luaScript' },\n    ],\n  },\n  {\n    input: [{ name: 'luaScript' }, { name: 'bigSets' }, { name: 'searchJSON' }],\n    expected: [\n      { name: 'searchJSON' },\n      { name: 'bigSets' },\n      { name: 'luaScript' },\n    ],\n  },\n  {\n    input: [\n      { name: 'luaScript' },\n      { name: 'bigSets' },\n      { name: 'searchIndexes' },\n      { name: 'searchJSON' },\n      { name: 'useSmallerKeys' },\n      { name: 'RTS' },\n    ],\n    expected: [\n      { name: 'searchJSON' },\n      { name: 'searchIndexes' },\n      { name: 'RTS' },\n      { name: 'bigSets' },\n      { name: 'luaScript' },\n      { name: 'useSmallerKeys' },\n    ],\n  },\n]\n\nconst replaceVariablesTests = [\n  { input: ['value'], expected: 'value' },\n  // eslint-disable-next-line no-template-curly-in-string\n  {\n    input: ['some ${0} text ${1}', ['foo', 'bar'], { foo: '7', bar: 'bar' }],\n    expected: 'some 7 text bar',\n  },\n  { input: ['value'], expected: 'value' },\n  { input: ['value'], expected: 'value' },\n]\n\ndescribe('sortRecommendations', () => {\n  test.each(sortRecommendationsTests)('%j', ({ input, expected }) => {\n    const result = sortRecommendations(input, MOCK_RECOMMENDATIONS)\n    expect(result).toEqual(expected)\n  })\n})\n\ndescribe('replaceVariables', () => {\n  test.each(replaceVariablesTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = replaceVariables(...input)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/redisearch.spec.ts",
    "content": "import { getFieldTypeOptions } from 'uiSrc/utils'\nimport { RedisDefaultModules } from 'uiSrc/slices/interfaces'\nimport { FIELD_TYPE_OPTIONS } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'\n\nconst nameAndVersionToModule = ([name, semanticVersion, version]: any[]) => ({\n  name,\n  semanticVersion,\n  version,\n})\n\nconst ALL_OPTIONS = FIELD_TYPE_OPTIONS.map(({ value, text }) => ({\n  value,\n  inputDisplay: text,\n  label: text,\n}))\n\nconst getFieldTypeOptionsTests: any[] = [\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Search, '2.8.4'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Search, '2.8.3'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.3'],\n      [RedisDefaultModules.SearchLight, '2.8.4'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.SearchLight, '2.8.3'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.3'],\n      [RedisDefaultModules.FT, '2.8.4'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FT, '2.8.3'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.3'],\n      [RedisDefaultModules.FTL, '2.8.4'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FTL, '2.8.3'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Gears, '2.8.4'],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Search, undefined, 20804],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Search, undefined, 20803],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.SearchLight, undefined, 20804],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.SearchLight, undefined, 20803],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FT, undefined, 20804],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FT, undefined, 20803],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FTL, undefined, 20804],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FTL, undefined, 20803],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Gears, undefined, 20804],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.Gears, undefined, 20803],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FTL, '2.8.3', 20803],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n  [\n    [\n      ['1', '2.8.4'],\n      [RedisDefaultModules.FTL, '2.8.4', 20804],\n    ].map(nameAndVersionToModule),\n    ALL_OPTIONS,\n  ],\n]\n\ndescribe('getFieldTypeOptions', () => {\n  it.each(getFieldTypeOptionsTests)(\n    'for input: %s (type), should be output: %s',\n    (_, expected) => {\n      const result = getFieldTypeOptions()\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/redistack.spec.ts",
    "content": "/* eslint-disable max-len */\nimport { isRediStack } from 'uiSrc/utils'\n\nconst unmapWithName = (arr: any[]) => arr.map((item) => ({ name: item }))\n\nconst isRediStackTests = [\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight']),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [unmapWithName(['bf', 'timeseries', 'ReJSON', 'search']), '6.2.5'],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'searchlight']),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight', 'graph']),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'graph']),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'redisgears']),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears',\n      ]),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'redisgears_2']),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears_2',\n      ]),\n      '6.2.5',\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'search',\n        'redisgears_2',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'redisgears_2',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'search',\n        'graph',\n        'redisgears_2',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'redisgears',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'search',\n        'graph',\n        'redisgears',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'search',\n        'graph',\n        'redisgears',\n        'redisgears_2',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight', 'custom']),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'custom',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'redisgears',\n        'custom',\n      ]),\n      '6.2.5',\n    ],\n    expected: false,\n  },\n\n  {\n    input: [unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight']), null],\n    expected: true,\n  },\n  {\n    input: [unmapWithName(['bf', 'timeseries', 'ReJSON', 'search']), null],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'searchlight']),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight', 'graph']),\n      null,\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'graph']),\n      null,\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'redisgears']),\n      null,\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears',\n      ]),\n      null,\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'search', 'redisgears_2']),\n      null,\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears_2',\n      ]),\n      null,\n    ],\n    expected: true,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'search',\n        'redisgears_2',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'redisgears_2',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'search',\n        'graph',\n        'redisgears_2',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'redisgears',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'search',\n        'graph',\n        'redisgears',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'search',\n        'graph',\n        'redisgears',\n        'redisgears_2',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight', 'custom']),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'custom',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'graph',\n        'redisgears',\n        'custom',\n      ]),\n      null,\n    ],\n    expected: false,\n  },\n\n  {\n    input: [\n      unmapWithName(['bf', 'timeseries', 'ReJSON', 'searchlight']),\n      '6.2.4',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears',\n      ]),\n      '7.9',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears',\n      ]),\n      '8.0.0',\n    ],\n    expected: false,\n  },\n  {\n    input: [\n      unmapWithName([\n        'bf',\n        'timeseries',\n        'ReJSON',\n        'searchlight',\n        'redisgears',\n      ]),\n      '8.0.0-rc',\n    ],\n    expected: false,\n  },\n]\n\ndescribe('isRediStack', () => {\n  test.each(isRediStackTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = isRediStack(...input)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/routerWithSubRoutes.spec.tsx",
    "content": "import React from 'react'\nimport Router from 'uiSrc/Router'\nimport { render } from 'uiSrc/utils/test-utils'\nimport RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes'\n\nimport { Pages } from 'uiSrc/constants'\nimport { SettingsPage } from 'uiSrc/pages'\n\ndescribe('RouteWithSubRoutes', () => {\n  it('should render', () => {\n    expect(\n      render(\n        <Router>\n          <RouteWithSubRoutes\n            key={1}\n            path={Pages.settings}\n            component={SettingsPage}\n          />\n        </Router>,\n      ),\n    ).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/routing.spec.ts",
    "content": "import { getPageName, getRedirectionPage } from 'uiSrc/utils/routing'\n\njest.mock('uiSrc/utils/routing', () => ({\n  ...jest.requireActual('uiSrc/utils/routing'),\n}))\n\nObject.defineProperty(window, 'location', {\n  value: {\n    origin: 'http://localhost',\n  },\n  writable: true,\n})\n\nconst databaseId = '1'\nconst getRedirectionPageTests = [\n  { input: ['settings'], expected: '/settings' },\n  { input: ['workbench', databaseId], expected: '/1/workbench' },\n  { input: ['/workbench', databaseId], expected: '/1/workbench' },\n  { input: ['browser', databaseId], expected: '/1/browser' },\n  { input: ['/browser', databaseId], expected: '/1/browser' },\n  {\n    input: ['/analytics/slowlog', databaseId],\n    expected: '/1/analytics/slowlog',\n  },\n  { input: ['/analytics/slowlog'], expected: null },\n  { input: ['/analytics', databaseId], expected: '/1/analytics' },\n  { input: ['/analytics/page', databaseId], expected: undefined },\n  { input: ['/analytics'], expected: null },\n  { input: ['some-page'], expected: undefined },\n  {\n    input: ['/workbench?guidePath=introduction.md', databaseId],\n    expected: '/1/workbench?guidePath=introduction.md&insights=open',\n  },\n  { input: ['/_?tutorialId=tutorial'], expected: undefined },\n  {\n    input: ['/_?tutorialId=tutorial', databaseId, `/${databaseId}/workbench`],\n    expected: '/1/workbench?tutorialId=tutorial',\n  },\n]\n\ndescribe('getRedirectionPage', () => {\n  test.each(getRedirectionPageTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getRedirectionPage(...input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst getPageNameTests = [\n  { input: ['instanceId', '/instanceId/page1'], expected: '/page1' },\n  {\n    input: ['instanceId', '/instanceId/page1/page2'],\n    expected: '/page1/page2',\n  },\n  { input: ['instanceId', '/page1'], expected: '/page1' },\n]\n\ndescribe('getPageName', () => {\n  test.each(getPageNameTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = getPageName(...input)\n    expect(result).toEqual(expected)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/streamUtils.spec.ts",
    "content": "import { ConsumerDto } from 'apiSrc/modules/browser/stream/dto'\nimport { getDefaultConsumer } from '../streamUtils'\n\nconst consumers1: ConsumerDto[] = [\n  { name: 'name_2', pending: 1, idle: 123 },\n  { name: 'name_1', pending: 1, idle: 123 },\n  { name: 'name_3', pending: 3, idle: 123 },\n]\n\nconst consumers2: ConsumerDto[] = [{ name: 'name_2', pending: 1, idle: 123 }]\n\nconst consumers3: ConsumerDto[] = []\n\ndescribe('getDefaultConsumer', () => {\n  it('should get consumer with lowest pending messages count and ', () => {\n    expect(getDefaultConsumer(consumers1)).toEqual({\n      name: 'name_1',\n      pending: 1,\n      idle: 123,\n    })\n    expect(getDefaultConsumer(consumers2)).toEqual({\n      name: 'name_2',\n      pending: 1,\n      idle: 123,\n    })\n    expect(getDefaultConsumer(consumers3)).toEqual(undefined)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/telemetry.spec.ts",
    "content": "import { getRangeForNumber, BULK_THRESHOLD_BREAKPOINTS } from 'uiSrc/utils'\n\nconst testCases = [\n  {\n    number: undefined,\n    result: undefined,\n  },\n  {\n    number: 0,\n    result: '0 - 5 000',\n  },\n  {\n    number: 10,\n    result: '0 - 5 000',\n  },\n  {\n    number: 5_000,\n    result: '0 - 5 000',\n  },\n  {\n    number: 5_001,\n    result: '5 001 - 10 000',\n  },\n  {\n    number: 7_050,\n    result: '5 001 - 10 000',\n  },\n  {\n    number: 10_000,\n    result: '5 001 - 10 000',\n  },\n  {\n    number: 10_001,\n    result: '10 001 - 50 000',\n  },\n  {\n    number: 50_000,\n    result: '10 001 - 50 000',\n  },\n  {\n    number: 50_001,\n    result: '50 001 - 100 000',\n  },\n  {\n    number: 100_000,\n    result: '50 001 - 100 000',\n  },\n  {\n    number: 1_000_000,\n    result: '100 001 - 1 000 000',\n  },\n  {\n    number: 1_000_001,\n    result: '1 000 001 +',\n  },\n]\ndescribe('getRangeForNumber', () => {\n  testCases.forEach((tc) => {\n    it(`should return ${tc.result} for number:${tc.number}`, () => {\n      const range = getRangeForNumber(tc.number, BULK_THRESHOLD_BREAKPOINTS)\n      expect(range).toEqual(tc.result)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/browser.spec.ts",
    "content": "import { comboBoxToArray } from 'uiSrc/utils'\n\nconst getOutputForFormatToTextTests: any[] = [\n  [[], []],\n  [\n    [{ label: '123' }, { label: 'test' }],\n    ['123', 'test'],\n  ],\n  [[{ label1: '123' }], []],\n  [\n    [{ label: '123' }, { label: 'test' }],\n    ['123', 'test'],\n  ],\n]\n\ndescribe('formatToText', () => {\n  it.each(getOutputForFormatToTextTests)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = comboBoxToArray(reply)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/chatbot.spec.ts",
    "content": "import {\n  generateHumanMessage,\n  generateAiMessage,\n} from 'uiSrc/utils/transformers/chatbot'\nimport { AiChatMessageType } from 'uiSrc/slices/interfaces/aiAssistant'\n\ndescribe('generateHumanMessage', () => {\n  it('should properly return human message object', () => {\n    expect(generateHumanMessage('hello')).toEqual({\n      id: expect.any(String),\n      type: AiChatMessageType.HumanMessage,\n      content: 'hello',\n      context: {},\n    })\n  })\n})\n\ndescribe('generateAiMessage', () => {\n  it('should properly return human message object', () => {\n    expect(generateAiMessage('hello')).toEqual({\n      id: expect.any(String),\n      type: AiChatMessageType.AIMessage,\n      content: 'hello',\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/cliTextFormatter.spec.ts",
    "content": "import { bulkReplyCommands } from 'uiSrc/constants'\nimport { formatToText } from 'uiSrc/utils'\n\nconst getOutputForFormatToTextTests: any[] = [\n  [5, 'GET', '(integer) 5'],\n  ['5', 'GET', '\"5\"'],\n  [null, 'GET', '(nil)'],\n  [[], 'GET', '(empty list or set)'],\n  [['1', '2', '3'], 'GET', '1) \"1\"\\n2) \"2\"\\n3) \"3\"'],\n  ['test\\r\\ntest', bulkReplyCommands[0], 'test\\r\\ntest'],\n  ['test2\\r\\ntest2\\r\\ntest2', bulkReplyCommands[1], 'test2\\r\\ntest2\\r\\ntest2'],\n]\n\ndescribe('formatToText', () => {\n  it.each(getOutputForFormatToTextTests)(\n    'for input: %s (reply), %s (command), should be output: %s',\n    (reply, command, expected) => {\n      const result = formatToText(reply, command)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/extrapolation.spec.ts",
    "content": "import { extrapolate, formatExtrapolation, Maybe } from 'uiSrc/utils'\n\nconst extrapolationTests: Array<\n  [\n    number,\n    {\n      apply: boolean\n      extrapolation?: number | undefined\n      showPrefix?: boolean | undefined\n    },\n    Maybe<(val: number) => string | number>,\n    string | number,\n  ]\n> = [\n  [500, { apply: true, extrapolation: 2 }, undefined, '~1000'],\n  [270, { apply: true, extrapolation: 2.33 }, Math.round, '~629'],\n  [100.125, { apply: false, extrapolation: 2.33 }, Math.round, 100],\n  [\n    100.125,\n    { apply: true, extrapolation: 2.5, showPrefix: false },\n    Math.round,\n    250,\n  ],\n]\n\ndescribe('extrapolation', () => {\n  it.each(extrapolationTests)(\n    'for input: %s (value), %s (options), %s (callback) should be output: %s',\n    (value, options, callback, expected) => {\n      const result = extrapolate(value, options, callback)\n      expect(result).toBe(expected)\n    },\n  )\n})\n\ndescribe('formatExtrapolation', () => {\n  it('should properly return value', () => {\n    expect(formatExtrapolation(112)).toBe('~112')\n    expect(formatExtrapolation(112, false)).toBe(112)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/formatBytes.spec.ts",
    "content": "import { formatBytes, toBytes } from 'uiSrc/utils'\n\nconst formatBytesTests: any[] = [\n  [256, 3, '256 B'],\n  [256, undefined, '256 B'],\n  [0, undefined, '0 B'],\n  [1024, undefined, '1 KB'],\n  [1200, undefined, '1.172 KB'],\n  [14758, 0, '14 KB'],\n  [14758, 3, '14.412 KB'],\n  [1048576, undefined, '1 MB'],\n  [1572864, undefined, '1.5 MB'],\n  [1572864, 0, '2 MB'],\n  [1572864, -1, '2 MB'],\n  [1347545989, 0, '1 GB'],\n  [1347545989, 3, '1.255 GB'],\n  [36538736640, 0, '34 GB'],\n  [1099511627776, undefined, '1 TB'],\n  [1649267441664, undefined, '1.5 TB'],\n  [1649267441664, 0, '2 TB'],\n  [1379887092858, 3, '1.255 TB'],\n  [1379887092858, undefined, '1.255 TB'],\n  [1379887092858, 1, '1.3 TB'],\n  [1125899906842624, undefined, '1 PB'],\n  [1125899906842624, 3, '1 PB'],\n  [1413004383087493, 3, '1.255 PB'],\n  [1413004383087493, 1, '1.3 PB'],\n  [1152921504606847000, undefined, '1 EB'],\n  [1152921504606847000, 3, '1 EB'],\n  [1446916488281592800, 3, '1.255 EB'],\n  [1446916488281592800, 1, '1.3 EB'],\n  [1.1805916207174113e21, 0, '1 ZB'],\n  [1.2089258196146292e24, 0, '1 YB'],\n  [1.2379400392853803e27, 0, '1024 YB'],\n  ['1', 0, '1 B'],\n  ['string', 0, '-'],\n  [-100, 0, '-'],\n]\n\ndescribe('formatBytes', () => {\n  it.each(formatBytesTests)(\n    'for input: %s (bytes), %s (decimals), should be output: %s',\n    (bytes, decimals, expected) => {\n      const result = formatBytes(bytes, decimals)\n      expect(result).toBe(expected)\n    },\n  )\n  it('should return proper array with splitResults', () => {\n    expect(formatBytes(1572864, 0, true)).toEqual([2, 'MB'])\n    expect(formatBytes(1347545989, 3, true)).toEqual([1.255, 'GB'])\n    expect(formatBytes(0, 3, true)).toEqual([0, 'B'])\n  })\n\n  it('should properly set the baseK', () => {\n    expect(formatBytes(1347545989, 3, true)).toEqual([1.255, 'GB']) // default uses 1024\n    expect(formatBytes(1347545989, 3, true, 1000)).toEqual([1.348, 'GB'])\n  })\n})\n\nconst toBytesTests: any[] = [\n  [256, '256 B'],\n  [256, '256 B'],\n  [0, '0 B'],\n  [1024, '1 KB'],\n  [1048576, '1 MB'],\n  [1572864, '1.5 MB'],\n  [1099511627776, '1 TB'],\n  [1649267441664, '1.5 TB'],\n  [1379887092858, '1.255 TB'],\n  [1125899906842624, '1 PB'],\n  [1125899906842624, '1 PB'],\n  [1413004383087493, '1.255 PB'],\n  [1152921504606847000, '1 EB'],\n  [1152921504606847000, '1 EB'],\n  [1446916488281592800, '1.255 EB'],\n  [1.1805916207174113e21, '1 ZB'],\n  [1.2089258196146292e24, '1 YB'],\n  [1.2379400392853803e27, '1024 YB'],\n]\n\ndescribe('toBytes', () => {\n  it.each(toBytesTests)(\n    'should be output: %s, for value: $s',\n    (expected, formatted) => {\n      const [bytes, type] = formatted.split(' ')\n\n      const result = toBytes(+bytes, type)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/formatDate.spec.ts",
    "content": "import { format } from 'date-fns'\nimport { DATETIME_FORMATTER_DEFAULT, TimezoneOption } from 'uiSrc/constants'\nimport {\n  checkDateTimeFormat,\n  formatTimestamp,\n  secondsToMinutes,\n} from 'uiSrc/utils'\n\nconst defaultValue = new Date('2023-10-05T12:34:56Z')\nconst defaultFormat = DATETIME_FORMATTER_DEFAULT\nconst defaultTimezone = 'UTC'\n\nconst secondsToMinutesTests: any[] = [\n  [0, '0 seconds'],\n  [1, '1 second'],\n  [25, '25 seconds'],\n  [60, '1 minute'],\n  [65, '1 minute'],\n  [120, '2 minutes'],\n  [1300, '21 minutes'],\n]\n\nconst checkDateTimeFormatTests: any[] = [\n  ['yyyy-MM-dd', true],\n  ['invalid-format', false],\n]\n\nconst formatTimestampTests: any[] = [\n  {\n    // valid date, format, local timezone\n    timezone: 'local',\n    expect: format(defaultValue, defaultFormat),\n  },\n  {\n    // invalid date, valid format, local timezone\n    value: 'invalid date',\n    timezone: 'local',\n    expect: 'invalid date',\n  },\n  {\n    // valid date, valid format, time utc\n    format: 'yyyy-MM-dd HH:mm:ss',\n    expect: '2023-10-05 12:34:56',\n  },\n  // invalid date, valid format,time utc\n  {\n    value: 'invalid date',\n    expect: 'invalid date',\n  },\n  {\n    // valid date, INvalid format, time utc\n    format: 'invalid format',\n    expect: defaultValue.toString(),\n  },\n  {\n    // valid date, valid format, invalid timezone\n    timezone: 'invalid timezone',\n    expect: defaultValue.toString(),\n  },\n  // timezone check\n  {\n    // in UTC+ format not working\n    timezone: 'UTC+14:00',\n    expect: defaultValue.toString(),\n  },\n]\n\ndescribe('secondsToMinutes', () => {\n  it.each(secondsToMinutesTests)(\n    'should be output: %s, for value: $s',\n    (input, output) => {\n      const result = secondsToMinutes(input)\n      expect(result).toBe(output)\n    },\n  )\n})\n\ndescribe('checkDateTimeFormat', () => {\n  it.each(checkDateTimeFormatTests)(\n    'should be output: %s, for value: $s',\n    (input, output) => {\n      const result = checkDateTimeFormat(input)\n      const resultUTC = checkDateTimeFormat(input, TimezoneOption.UTC)\n      expect(result.valid).toBe(output)\n      expect(resultUTC.valid).toBe(output)\n    },\n  )\n})\n\ndescribe('formatTimestamp', () => {\n  it.each(formatTimestampTests)(\n    'should be output: %s, for value: $s',\n    (testcase) => {\n      const value = testcase.value || defaultValue\n      const format = testcase.format || defaultFormat\n      const timezone = testcase.timezone || defaultTimezone\n      const result = formatTimestamp(value, format, timezone)\n      expect(result).toBe(testcase.expect)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/getTruncatedName.spec.ts",
    "content": "import { getTruncatedName } from 'uiSrc/utils'\n\nconst getTruncatedNameTests: any[] = [\n  ['Bill Russell', 'BR'],\n  ['Bill Russell Van Der', 'BR'],\n  ['Bill', 'B'],\n  ['', ''],\n  [null, ''],\n  [undefined, ''],\n]\n\ndescribe('getTruncatedName', () => {\n  it.each(getTruncatedNameTests)(\n    'should be output: %s, for value: $s',\n    (input, output) => {\n      expect(getTruncatedName(input)).toBe(output)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/removeEmpty.spec.ts",
    "content": "import { removeEmpty } from 'uiSrc/utils'\n\ndescribe('removeEmpty', () => {\n  it('should remove empty fields from object', () => {\n    expect(removeEmpty({ a: '', b: { c: '', d: { g: '' } }, e: 1 })).toEqual({\n      b: { d: {} },\n      e: 1,\n    })\n    expect(removeEmpty({ a: '' })).toEqual({})\n    expect(removeEmpty({ a: '', b: { c: '' }, e: 1 })).toEqual({ b: {}, e: 1 })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/replaceSpaces.spec.ts",
    "content": "import { replaceSpaces } from 'uiSrc/utils'\n\nconst getReplaceSpacesTests: any[] = [\n  ['10', '10'],\n  [' trala la ', ' trala la '],\n  [\n    'tr    la    lo lu',\n    'tr\\u00a0\\u00a0\\u00a0\\u00a0la\\u00a0\\u00a0\\u00a0\\u00a0lo lu',\n  ],\n  ['tralalala', 'tralalala'],\n  [\n    '       1233 123123  tral lalal ',\n    '\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0\\u00a0 1233 123123\\u00a0\\u00a0tral lalal ',\n  ],\n  [11, '11'],\n]\n\ndescribe('replaceSpaces', () => {\n  it.each(getReplaceSpacesTests)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = replaceSpaces(reply)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/transformQueryParams.spec.ts",
    "content": "import { transformQueryParamsObject } from 'uiSrc/utils'\n\nconst transformQueryParamsObjectTests: any[] = [\n  [\n    { v1: 'true', v2: '123', v3: 'qwe', v4: '1q' },\n    { v1: true, v2: 123, v3: 'qwe', v4: '1q' },\n  ],\n  [\n    { v1: 'true', v2: '123', v3: 'qwe', v4: '1q', v5: 'false' },\n    { v1: true, v2: 123, v3: 'qwe', v4: '1q', v5: false },\n  ],\n  [{ v1: 'd' }, { v1: 'd' }],\n  [{}, {}],\n]\n\ndescribe('transformQueryParamsObject', () => {\n  it.each(transformQueryParamsObjectTests)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = transformQueryParamsObject(reply)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts",
    "content": "import {\n  pipelineToYaml,\n  pipelineToJson,\n  transformConnectionResults,\n} from 'uiSrc/utils'\n\nconst pipelineToJsonTests: any[] = [\n  [\n    {\n      config:\n        'connections:\\n  # Redis data DB connection details\\n  # This section is for configuring the Redis database to which Redis Data Integration will connect to\\n  target:\\n    # Target type - Redis is the only supported type (default: redis)\\n    type: redis\\n    # Host of the Redis database to which Redis Data Integration will write the processed data\\n    host: redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com\\n    # Port for the Redis database to which Redis Data Integration will write the processed data\\n    port: 18262\\n    # User of the Redis database to which Redis Data Integration will write the processed data\\n    user: default',\n      jobs: [\n        {\n          name: 'job1',\n          value:\n            \"source:\\n  row_format: full\\n  server_name: chinook\\n  schema: dbo\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      language: jmespath\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\noutput:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\",\n        },\n      ],\n    },\n    {\n      config: {\n        connections: {\n          target: {\n            host: 'redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com',\n            port: 18262,\n            type: 'redis',\n            user: 'default',\n          },\n        },\n      },\n      jobs: {\n        job1: {\n          output: [\n            {\n              uses: 'redis.write',\n              with: {\n                data_type: 'json',\n                mapping: ['EmployeeId', 'FirstName', 'LastName'],\n              },\n            },\n          ],\n          source: {\n            row_format: 'full',\n            schema: 'dbo',\n            server_name: 'chinook',\n            table: 'Employee',\n          },\n          transform: [\n            {\n              uses: 'filter',\n              with: {\n                expression: \"in(after.LastName,['Smith']) && opcode == 'c'\",\n                language: 'jmespath',\n              },\n            },\n          ],\n        },\n      },\n    },\n    0,\n    {},\n  ],\n  [\n    {\n      config:\n        'connections:incorrect\\n  # Redis data DB connection details\\n  # This section is for configuring the Redis database to which Redis Data Integration will connect to\\n  target:\\n    # Target type - Redis is the only supported type (default: redis)\\n    type: redis\\n    # Host of the Redis database to which Redis Data Integration will write the processed data\\n    host: redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com\\n    # Port for the Redis database to which Redis Data Integration will write the processed data\\n    port: 18262\\n    # User of the Redis database to which Redis Data Integration will write the processed data\\n    user: default',\n      jobs: [\n        {\n          name: 'job1',\n          value:\n            \"source:\\n  row_format: full\\n  server_name: chinook\\n  schema: dbo\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      language: jmespath\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\noutput:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\",\n        },\n      ],\n    },\n    undefined,\n    1,\n    [\n      {\n        filename: 'config',\n        msg: 'end of the stream or a document separator is expected',\n      },\n    ],\n  ],\n  [\n    {\n      config:\n        'connections:\\n  # Redis data DB connection details\\n  # This section is for configuring the Redis database to which Redis Data Integration will connect to\\n  target:\\n    # Target type - Redis is the only supported type (default: redis)\\n    type: redis\\n    # Host of the Redis database to which Redis Data Integration will write the processed data\\n    host: redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com\\n    # Port for the Redis database to which Redis Data Integration will write the processed data\\n    port: 18262\\n    # User of the Redis database to which Redis Data Integration will write the processed data\\n    user: default',\n      jobs: [\n        {\n          name: 'job1',\n          value:\n            \"source:incorrect\\n  row_format: full\\n  server_name: chinook\\n  schema: dbo\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      language: jmespath\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\noutput:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\",\n        },\n      ],\n    },\n    undefined,\n    1,\n    [\n      {\n        filename: 'job1',\n        msg: 'end of the stream or a document separator is expected',\n      },\n    ],\n  ],\n  [\n    {\n      config:\n        'connections:incorrect\\n  # Redis data DB connection details\\n  # This section is for configuring the Redis database to which Redis Data Integration will connect to\\n  target:\\n    # Target type - Redis is the only supported type (default: redis)\\n    type: redis\\n    # Host of the Redis database to which Redis Data Integration will write the processed data\\n    host: redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com\\n    # Port for the Redis database to which Redis Data Integration will write the processed data\\n    port: 18262\\n    # User of the Redis database to which Redis Data Integration will write the processed data\\n    user: default',\n      jobs: [\n        {\n          name: 'job1',\n          value:\n            \"source:incorrect\\n  row_format: full\\n  server_name: chinook\\n  schema: dbo\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      language: jmespath\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\noutput:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\",\n        },\n        {\n          name: 'job2',\n          value:\n            \"source:\\n  row_format: full\\n  server_name: chinook\\n  schema: dbo\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      language: jmespath\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\noutput:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\",\n        },\n        {\n          name: 'job3',\n          value:\n            \"source:incorrect\\n  row_format: full\\n  server_name: chinook\\n  schema: dbo\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      language: jmespath\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\noutput:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\",\n        },\n      ],\n    },\n    undefined,\n    1,\n    [\n      {\n        filename: 'config',\n        msg: 'end of the stream or a document separator is expected',\n      },\n      {\n        filename: 'job1',\n        msg: 'end of the stream or a document separator is expected',\n      },\n      {\n        filename: 'job3',\n        msg: 'end of the stream or a document separator is expected',\n      },\n    ],\n  ],\n]\n\ndescribe('pipelineToJson', () => {\n  it.each(pipelineToJsonTests)(\n    'for input: %s (input), should be output: %s',\n    (input, expected, callback, errors) => {\n      const mockOnError = jest.fn()\n      const result = pipelineToJson(input, mockOnError)\n      expect(result).toEqual(expected)\n      expect(mockOnError).toBeCalledTimes(callback)\n      if (callback) {\n        expect(mockOnError).toBeCalledWith(errors)\n      }\n    },\n  )\n})\n\nconst pipelineToYamlTests: any[] = [\n  [\n    {\n      config: {\n        connections: {\n          target: {\n            host: 'redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com',\n            port: 18262,\n            type: 'redis',\n            user: 'default',\n          },\n        },\n      },\n      jobs: {\n        job1: {\n          output: [\n            {\n              uses: 'redis.write',\n              with: {\n                data_type: 'json',\n                mapping: ['EmployeeId', 'FirstName', 'LastName'],\n              },\n            },\n          ],\n          source: {\n            row_format: 'full',\n            schema: 'dbo',\n            server_name: 'chinook',\n            table: 'Employee',\n          },\n          transform: [\n            {\n              uses: 'filter',\n              with: {\n                expression: \"in(after.LastName,['Smith']) && opcode == 'c'\",\n                language: 'jmespath',\n              },\n            },\n          ],\n        },\n      },\n    },\n    {\n      config:\n        'connections:\\n  target:\\n    host: redis-18262.c232.us-east-1-2.ec2.cloud.redislabs.com\\n    port: 18262\\n    type: redis\\n    user: default\\n',\n      jobs: [\n        {\n          name: 'job1',\n          value:\n            \"output:\\n  - uses: redis.write\\n    with:\\n      data_type: json\\n      mapping:\\n        - EmployeeId\\n        - FirstName\\n        - LastName\\nsource:\\n  row_format: full\\n  schema: dbo\\n  server_name: chinook\\n  table: Employee\\ntransform:\\n  - uses: filter\\n    with:\\n      expression: in(after.LastName,['Smith']) && opcode == 'c'\\n      language: jmespath\\n\",\n        },\n      ],\n    },\n  ],\n]\n\ndescribe('pipelineToYaml', () => {\n  it.each(pipelineToYamlTests)(\n    'for input: %s (input), should be output: %s',\n    (input, expected) => {\n      const result = pipelineToYaml(input)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n\nconst transformConnectionResultsTests: any[] = [\n  [\n    null,\n    { target: { success: [], fail: [] }, source: { success: [], fail: [] } },\n  ],\n  [\n    {\n      targets: {\n        target1: {\n          status: 'success',\n          error: {\n            code: 'INVALID_CREDENTIALS',\n            message:\n              'Failed to establish connection to the PostgreSQL database. Invalid credentials provided',\n          },\n        },\n        target2: {\n          status: 'failed',\n          error: {\n            code: 'INVALID_CREDENTIALS',\n            message:\n              'Failed to establish connection to the PostgreSQL database. Invalid credentials provided',\n          },\n        },\n        target3: {\n          status: 'wrong status',\n        },\n        target4: {\n          status: 'wrong status',\n          error: {\n            code: 'INVALID_CREDENTIALS',\n            message:\n              'Failed to establish connection to the PostgreSQL database. Invalid credentials provided',\n          },\n        },\n        target5: {\n          status: 'success',\n        },\n        target6: {\n          unknownProperty: 'foo bar',\n        },\n        target7: {\n          status: 'failed',\n        },\n      },\n    },\n    {\n      target: {\n        success: [{ target: 'target1' }, { target: 'target5' }],\n        fail: [\n          {\n            target: 'target2',\n            error:\n              'Failed to establish connection to the PostgreSQL database. Invalid credentials provided',\n          },\n          { target: 'target7', error: 'Error' },\n        ],\n      },\n      source: {\n        success: [],\n        fail: [],\n      },\n    },\n  ],\n  [\n    {\n      targets: {\n        target1: { status: 'success' },\n      },\n      sources: {\n        source1: {\n          connected: true,\n          error: 'Success',\n        },\n      },\n    },\n    {\n      target: {\n        success: [{ target: 'target1' }],\n        fail: [],\n      },\n      source: {\n        success: [{ target: 'source1' }],\n        fail: [],\n      },\n    },\n  ],\n  [\n    {\n      targets: {\n        target1: { status: 'success' },\n      },\n      sources: {\n        source1: {\n          connected: false,\n          error: 'Database unreachable',\n        },\n      },\n    },\n    {\n      target: {\n        success: [{ target: 'target1' }],\n        fail: [],\n      },\n      source: {\n        success: [],\n        fail: [{ target: 'source1', error: 'Database unreachable' }],\n      },\n    },\n  ],\n  [\n    {\n      targets: {\n        target1: { status: 'success' },\n      },\n      sources: {\n        source1: {\n          connected: false,\n          error: 'Database unreachable',\n        },\n        source2: {\n          connected: true,\n          error: '',\n        },\n      },\n    },\n    {\n      target: {\n        success: [{ target: 'target1' }],\n        fail: [],\n      },\n      source: {\n        success: [{ target: 'source2' }],\n        fail: [{ target: 'source1', error: 'Database unreachable' }],\n      },\n    },\n  ],\n]\n\ndescribe('transformConnectionResults', () => {\n  it.each(transformConnectionResultsTests)(\n    'for input: %s (input), should be output: %s',\n    (input, expected) => {\n      const result = transformConnectionResults(input)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/truncateNumber.spec.ts",
    "content": "import { truncateNumberToRange } from 'uiSrc/utils'\n\ndescribe('truncateNumberToRange', () => {\n  it('truncateNumberToRange should return value between 0 and 999', () => {\n    const number1 = 10\n    const number2 = 100\n    const number3 = 256\n    const number4 = 612\n    const number5 = 999\n\n    const expectedResponse1 = '10'\n    const expectedResponse2 = '100'\n    const expectedResponse3 = '256'\n    const expectedResponse4 = '612'\n    const expectedResponse5 = '999'\n\n    expect(truncateNumberToRange(number1)).toEqual(expectedResponse1)\n    expect(truncateNumberToRange(number2)).toEqual(expectedResponse2)\n    expect(truncateNumberToRange(number3)).toEqual(expectedResponse3)\n    expect(truncateNumberToRange(number4)).toEqual(expectedResponse4)\n    expect(truncateNumberToRange(number5)).toEqual(expectedResponse5)\n  })\n\n  it('truncateNumberToRange should return value between 1 K and 99 K', () => {\n    const number1 = 10_000\n    const number2 = 100_000\n    const number3 = 256_000\n    const number4 = 612_000\n    const number5 = 999_000\n\n    const expectedResponse1 = '10 K'\n    const expectedResponse2 = '100 K'\n    const expectedResponse3 = '256 K'\n    const expectedResponse4 = '612 K'\n    const expectedResponse5 = '999 K'\n\n    expect(truncateNumberToRange(number1)).toEqual(expectedResponse1)\n    expect(truncateNumberToRange(number2)).toEqual(expectedResponse2)\n    expect(truncateNumberToRange(number3)).toEqual(expectedResponse3)\n    expect(truncateNumberToRange(number4)).toEqual(expectedResponse4)\n    expect(truncateNumberToRange(number5)).toEqual(expectedResponse5)\n  })\n\n  it('truncateNumberToRange should return value between 1 M and 999 M', () => {\n    const number1 = 10_000_000\n    const number2 = 100_000_000\n    const number3 = 256_000_000\n    const number4 = 612_000_000\n    const number5 = 999_000_000\n\n    const expectedResponse1 = '10 M'\n    const expectedResponse2 = '100 M'\n    const expectedResponse3 = '256 M'\n    const expectedResponse4 = '612 M'\n    const expectedResponse5 = '999 M'\n\n    expect(truncateNumberToRange(number1)).toEqual(expectedResponse1)\n    expect(truncateNumberToRange(number2)).toEqual(expectedResponse2)\n    expect(truncateNumberToRange(number3)).toEqual(expectedResponse3)\n    expect(truncateNumberToRange(number4)).toEqual(expectedResponse4)\n    expect(truncateNumberToRange(number5)).toEqual(expectedResponse5)\n  })\n\n  it('truncateNumberToRange should return value between 1 B and 2 B', () => {\n    const number1 = 1_000_000_001\n    const number2 = 1_500_001_200\n    const number3 = 2_120_042_300\n\n    const expectedResponse1 = '1 B'\n    const expectedResponse2 = '1 B'\n    const expectedResponse3 = '2 B'\n\n    expect(truncateNumberToRange(number1)).toEqual(expectedResponse1)\n    expect(truncateNumberToRange(number2)).toEqual(expectedResponse2)\n    expect(truncateNumberToRange(number3)).toEqual(expectedResponse3)\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/transformers/truncateTTL.spec.ts",
    "content": "import {\n  truncateNumberToDuration,\n  truncateNumberToFirstUnit,\n  truncateTTLToRange,\n  truncateTTLToSeconds,\n} from '../../transformers/truncateTTL'\n\ndescribe('Truncate TTL util tests', () => {\n  describe('truncateTTLToRange', () => {\n    it('truncateTTLToRange should return \"No limit\"', () => {\n      const ttl = -1\n      const expectedResponse = 'No limit'\n\n      expect(truncateTTLToRange(ttl)).toEqual(expectedResponse)\n    })\n\n    it('truncateTTLToRange should return value between 0 and 999', () => {\n      const ttl1 = 10\n      const ttl2 = 100\n      const ttl3 = 256\n      const ttl4 = 612\n      const ttl5 = 999\n\n      const expectedResponse1 = '10'\n      const expectedResponse2 = '100'\n      const expectedResponse3 = '256'\n      const expectedResponse4 = '612'\n      const expectedResponse5 = '999'\n\n      expect(truncateTTLToRange(ttl1)).toEqual(expectedResponse1)\n      expect(truncateTTLToRange(ttl2)).toEqual(expectedResponse2)\n      expect(truncateTTLToRange(ttl3)).toEqual(expectedResponse3)\n      expect(truncateTTLToRange(ttl4)).toEqual(expectedResponse4)\n      expect(truncateTTLToRange(ttl5)).toEqual(expectedResponse5)\n    })\n\n    it('truncateTTLToRange should return value between 1 K and 99 K', () => {\n      const ttl1 = 10_000\n      const ttl2 = 100_000\n      const ttl3 = 256_000\n      const ttl4 = 612_000\n      const ttl5 = 999_000\n\n      const expectedResponse1 = '10 K'\n      const expectedResponse2 = '100 K'\n      const expectedResponse3 = '256 K'\n      const expectedResponse4 = '612 K'\n      const expectedResponse5 = '999 K'\n\n      expect(truncateTTLToRange(ttl1)).toEqual(expectedResponse1)\n      expect(truncateTTLToRange(ttl2)).toEqual(expectedResponse2)\n      expect(truncateTTLToRange(ttl3)).toEqual(expectedResponse3)\n      expect(truncateTTLToRange(ttl4)).toEqual(expectedResponse4)\n      expect(truncateTTLToRange(ttl5)).toEqual(expectedResponse5)\n    })\n\n    it('truncateTTLToRange should return value between 1 M and 999 M', () => {\n      const ttl1 = 10_000_000\n      const ttl2 = 100_000_000\n      const ttl3 = 256_000_000\n      const ttl4 = 612_000_000\n      const ttl5 = 999_000_000\n\n      const expectedResponse1 = '10 M'\n      const expectedResponse2 = '100 M'\n      const expectedResponse3 = '256 M'\n      const expectedResponse4 = '612 M'\n      const expectedResponse5 = '999 M'\n\n      expect(truncateTTLToRange(ttl1)).toEqual(expectedResponse1)\n      expect(truncateTTLToRange(ttl2)).toEqual(expectedResponse2)\n      expect(truncateTTLToRange(ttl3)).toEqual(expectedResponse3)\n      expect(truncateTTLToRange(ttl4)).toEqual(expectedResponse4)\n      expect(truncateTTLToRange(ttl5)).toEqual(expectedResponse5)\n    })\n\n    it('truncateTTLToRange should return value between 1 B and 2 B', () => {\n      const ttl1 = 1_000_000_001\n      const ttl2 = 1_500_001_200\n      const ttl3 = 2_120_042_300\n\n      const expectedResponse1 = '1 B'\n      const expectedResponse2 = '1 B'\n      const expectedResponse3 = '2 B'\n\n      expect(truncateTTLToRange(ttl1)).toEqual(expectedResponse1)\n      expect(truncateTTLToRange(ttl2)).toEqual(expectedResponse2)\n      expect(truncateTTLToRange(ttl3)).toEqual(expectedResponse3)\n    })\n  })\n\n  describe('truncateNumberToDuration', () => {\n    it('truncateNumberToDuration should return appropriate value', () => {\n      const ttl1 = 100\n      const ttl2 = 1_534\n      const ttl3 = 54_334\n      const ttl4 = 4_325_634\n      const ttl5 = 112_012_330\n      const ttl6 = 2_120_042_300\n\n      const expectedResponse1 = '1 min, 40 s'\n      const expectedResponse2 = '25 min, 34 s'\n      const expectedResponse3 = '15 h, 5 min, 34 s'\n      const expectedResponse4 = '1 mo, 19 d, 1 h, 33 min, 54 s'\n      const expectedResponse5 = '3 yr, 6 mo, 19 d, 10 h, 32 min, 10 s'\n      const expectedResponse6 = '67 yr, 2 mo, 6 d, 12 h, 38 min, 20 s'\n\n      expect(truncateNumberToDuration(ttl1)).toEqual(expectedResponse1)\n      expect(truncateNumberToDuration(ttl2)).toEqual(expectedResponse2)\n      expect(truncateNumberToDuration(ttl3)).toEqual(expectedResponse3)\n      expect(truncateNumberToDuration(ttl4)).toEqual(expectedResponse4)\n      expect(truncateNumberToDuration(ttl5)).toEqual(expectedResponse5)\n      expect(truncateNumberToDuration(ttl6)).toEqual(expectedResponse6)\n    })\n  })\n\n  describe('truncateTTLToSeconds', () => {\n    it('truncateTTLToSeconds should return appropriate value', () => {\n      const ttl1 = 100\n      const ttl2 = 10_000\n      const ttl3 = 1_231_231\n      const ttl4 = 122_331_231\n\n      const expectedResponse1 = '100'\n      const expectedResponse2 = '10 000'\n      const expectedResponse3 = '1 231 231'\n      const expectedResponse4 = '122 331 231'\n\n      expect(truncateTTLToSeconds(ttl1)).toEqual(expectedResponse1)\n      expect(truncateTTLToSeconds(ttl2)).toEqual(expectedResponse2)\n      expect(truncateTTLToSeconds(ttl3)).toEqual(expectedResponse3)\n      expect(truncateTTLToSeconds(ttl4)).toEqual(expectedResponse4)\n    })\n  })\n\n  describe('truncateNumberToFirstUnit', () => {\n    it('truncateNumberToFirstUnit should return appropriate value', () => {\n      const number1 = 100\n      const number2 = 1_534\n      const number3 = 54_334\n      const number4 = 4_325_634\n      const number5 = 112_012_330\n      const number6 = 2_120_042_300\n\n      const expectedResponse1 = '1 min' // '1 min, 40 s'\n      const expectedResponse2 = '25 min' // '25 min, 34 s'\n      const expectedResponse3 = '15 h' // '15 h, 5 min, 34 s'\n      const expectedResponse4 = '1 mo' // '1 mo, 19 d, 1 h, 33 min, 54 s'\n      const expectedResponse5 = '3 yr' // '3 yr, 6 mo, 19 d, 10 h, 32 min, 10 s'\n      const expectedResponse6 = '67 yr' // '67 yr, 2 mo, 6 d, 12 h, 38 min, 20 s'\n\n      expect(truncateNumberToFirstUnit(number1)).toEqual(expectedResponse1)\n      expect(truncateNumberToFirstUnit(number2)).toEqual(expectedResponse2)\n      expect(truncateNumberToFirstUnit(number3)).toEqual(expectedResponse3)\n      expect(truncateNumberToFirstUnit(number4)).toEqual(expectedResponse4)\n      expect(truncateNumberToFirstUnit(number5)).toEqual(expectedResponse5)\n      expect(truncateNumberToFirstUnit(number6)).toEqual(expectedResponse6)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/tree.spec.ts",
    "content": "import { getTreeLeafField } from 'uiSrc/utils'\n\nconst getTreeLeafFieldTests: any[] = [\n  [':', 'keys:keys'],\n  [';', 'keys;keys'],\n  ['123', 'keys123keys'],\n  ['   ', 'keys   keys'],\n  ['_', 'keys_keys'],\n  ['abc', 'keysabckeys'],\n  ['$$$', 'keys$$$keys'],\n  ['-', 'keys-keys'],\n]\n\ndescribe('getTreeLeafField', () => {\n  it.each(getTreeLeafFieldTests)(\n    'for input: %s (reply), should be output: %s',\n    (reply, expected) => {\n      const result = getTreeLeafField(reply)\n      expect(result).toBe(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/validations.spec.ts",
    "content": "import {\n  MAX_TTL_NUMBER,\n  validateEmail,\n  validateField,\n  validateTTLNumber,\n  validateCountNumber,\n  validateScoreNumber,\n  validateTTLNumberForAddKey,\n  validateCertName,\n  validateRefreshRateNumber,\n  MAX_REFRESH_RATE,\n  errorValidateRefreshRateNumber,\n  errorValidateNegativeInteger,\n  validateConsumerGroupId,\n  validateNumber,\n  checkTimestamp,\n  checkConvertToDate,\n} from 'uiSrc/utils'\n\nconst text1 = '123 123 123'\nconst text2 = 'lorem lorem12312 lorem'\nconst text3 = 'мама мыла раму'\nconst text4 = 'euihao crhrc.hrch !@#^&*($@#$'\nconst text5 = 'test@test.com'\nconst text6 = '-2323'\nconst text7 = '348234'\nconst text8 = '34823443924234234'\nconst text9 = '-3482.344392424'\nconst text10 = '348.344392421312321312312316786724'\nconst text11 = '3.3.1'\nconst text12 = '-3-2'\nconst text13 = '5'\n\nconst checkTimestampTests = [\n  { input: '1234567891', expected: true },\n  { input: '1234567891234', expected: true },\n  { input: '1234567891234567', expected: true },\n  { input: '1234567891234567891', expected: true },\n  { input: '1234567891.2', expected: true },\n  // it should be valid timestamp (for date < 1970)\n  { input: '-123456789', expected: true },\n  { input: '', expected: false },\n  { input: '-', expected: false },\n  { input: '0', expected: false },\n  { input: '1', expected: false },\n  { input: '123', expected: false },\n  { input: '12345678911', expected: false },\n  { input: '12345678912345', expected: false },\n  { input: '12345678912345678', expected: false },\n  { input: '1234567891.2.2', expected: false },\n  { input: '1234567891asd', expected: false },\n  { input: 'inf', expected: false },\n  { input: '-inf', expected: false },\n  { input: '1234567891:12', expected: false },\n  { input: '1234567891a12', expected: false },\n]\n\nconst checkConvertToDateTests = [\n  ...checkTimestampTests,\n  { input: '2024-08-02T00:00:00.000Z', expected: true },\n  { input: '10-10-2020', expected: true },\n  { input: '10/10/2020', expected: true },\n  { input: '10/10/2020invalid', expected: false },\n  { input: 'invalid', expected: false },\n]\n\ndescribe('Validations utils', () => {\n  describe('validateField', () => {\n    it('validateField should return text without empty spaces', () => {\n      const expectedResponse1 = '123123123'\n      const expectedResponse2 = 'loremlorem12312lorem'\n      const expectedResponse3 = 'мамамылараму'\n      const expectedResponse4 = 'euihaocrhrc.hrch!@#^&*($@#$'\n\n      expect(validateField(text1)).toEqual(expectedResponse1)\n      expect(validateField(text2)).toEqual(expectedResponse2)\n      expect(validateField(text3)).toEqual(expectedResponse3)\n      expect(validateField(text4)).toEqual(expectedResponse4)\n    })\n  })\n\n  describe('validateCountNumber', () => {\n    it('validateCountNumber should return only positive numbers', () => {\n      const expectedResponse1 = '123123123'\n      const expectedResponse2 = '12312'\n      const expectedResponse4 = ''\n      const expectedResponse5 = ''\n      const expectedResponse6 = '2323'\n      const expectedResponse7 = '348234'\n\n      expect(validateCountNumber(text1)).toEqual(expectedResponse1)\n      expect(validateCountNumber(text2)).toEqual(expectedResponse2)\n      expect(validateCountNumber(text4)).toEqual(expectedResponse4)\n      expect(validateCountNumber(text5)).toEqual(expectedResponse5)\n      expect(validateCountNumber(text6)).toEqual(expectedResponse6)\n      expect(validateCountNumber(text7)).toEqual(expectedResponse7)\n    })\n  })\n\n  describe('validateTTLNumber', () => {\n    it('validateTTLNumber should return only numbers between 0 and MAX_TTL_NUMBER', () => {\n      const expectedResponse1 = '123123123'\n      const expectedResponse2 = '12312'\n      const expectedResponse4 = ''\n      const expectedResponse5 = ''\n      const expectedResponse6 = '2323'\n      const expectedResponse7 = '348234'\n      const expectedResponse8 = `${MAX_TTL_NUMBER}`\n\n      expect(validateTTLNumber(text1)).toEqual(expectedResponse1)\n      expect(validateTTLNumber(text2)).toEqual(expectedResponse2)\n      expect(validateTTLNumber(text4)).toEqual(expectedResponse4)\n      expect(validateTTLNumber(text5)).toEqual(expectedResponse5)\n      expect(validateTTLNumber(text6)).toEqual(expectedResponse6)\n      expect(validateTTLNumber(text7)).toEqual(expectedResponse7)\n      expect(validateTTLNumber(text8)).toEqual(expectedResponse8)\n    })\n  })\n\n  describe('validateTTLNumberForAddKey', () => {\n    it('validateTTLNumberForAddKey should return only numbers between 1 and MAX_TTL_NUMBER', () => {\n      expect(validateTTLNumberForAddKey('0')).toEqual('')\n      expect(validateTTLNumberForAddKey('0123')).toEqual('123')\n      expect(validateTTLNumberForAddKey('300')).toEqual('300')\n    })\n  })\n\n  describe('validateScoreNumber', () => {\n    it('validateScoreNumber should return numbers with 15 decimal places max, negative values are allowed', () => {\n      const expectedResponse1 = '123123123'\n      const expectedResponse2 = '12312'\n      const expectedResponse6 = '-2323'\n      const expectedResponse7 = '348234'\n      const expectedResponse9 = '-3482.344392424'\n      const expectedResponse10 = '348.344392421312321'\n      const expectedResponse11 = '3.31'\n      const expectedResponse12 = '-32'\n\n      expect(validateScoreNumber(text1)).toEqual(expectedResponse1)\n      expect(validateScoreNumber(text2)).toEqual(expectedResponse2)\n      expect(validateScoreNumber(text6)).toEqual(expectedResponse6)\n      expect(validateScoreNumber(text7)).toEqual(expectedResponse7)\n      expect(validateScoreNumber(text9)).toEqual(expectedResponse9)\n      expect(validateScoreNumber(text10)).toEqual(expectedResponse10)\n      expect(validateScoreNumber(text11)).toEqual(expectedResponse11)\n      expect(validateScoreNumber(text12)).toEqual(expectedResponse12)\n    })\n  })\n\n  describe('validateEmail', () => {\n    it('validateEmail should return \"true\" only for email format text', () => {\n      expect(validateEmail(text1)).toBeFalsy()\n      expect(validateEmail(text2)).toBeFalsy()\n      expect(validateEmail(text4)).toBeFalsy()\n      expect(validateEmail(text5)).toBeTruthy()\n      expect(validateEmail(text6)).toBeFalsy()\n      expect(validateEmail(text7)).toBeFalsy()\n      expect(validateEmail(text8)).toBeFalsy()\n    })\n  })\n\n  describe('validateCertName', () => {\n    it.each([\n      ['my-new_cert', 'my-new_cert'],\n      ['my-1new1_cert', 'my-1new1_cert'],\n      ['my-1!@#$%^&*-new1_cert', 'my-1!@#$%^&*-new1_cert'],\n      ['my-[new]_(cert)', 'my-[new]_(cert)'],\n      ['my [new] {cert}', 'my [new] cert'],\n      ['MY-0123456789_cert', 'MY-0123456789_cert'],\n      ['my-ффффффф[new]_фффф{cert}', 'my-[new]_cert'],\n    ])('for input: %s (input), should be output: %s', (input, expected) => {\n      const result = validateCertName(input)\n      expect(result).toBe(expected)\n    })\n  })\n\n  describe('validateRefreshRateNumber', () => {\n    it.each([\n      [text1, `${MAX_REFRESH_RATE}`],\n      [text2, `${MAX_REFRESH_RATE}`],\n      [text3, ''],\n      [text4, '.'],\n      [text5, '.'],\n      [text6, `${MAX_REFRESH_RATE}`],\n      [text7, `${MAX_REFRESH_RATE}`],\n      [text8, `${MAX_REFRESH_RATE}`],\n      [text9, `${MAX_REFRESH_RATE}`],\n      [text10, '348.3'],\n      [text12, '32'],\n      [text13, '5'],\n    ])('for input: %s (input), should be output: %s', (input, expected) => {\n      const result = validateRefreshRateNumber(input)\n      expect(result).toBe(expected)\n    })\n  })\n\n  describe('errorValidateRefreshRateNumber', () => {\n    it.each([\n      [validateRefreshRateNumber(text1), false],\n      [validateRefreshRateNumber(text2), false],\n      [validateRefreshRateNumber(text3), true],\n      [validateRefreshRateNumber(text4), true],\n      [validateRefreshRateNumber(text5), true],\n      [validateRefreshRateNumber(text6), false],\n      [validateRefreshRateNumber(text7), false],\n      [validateRefreshRateNumber(text8), false],\n      [validateRefreshRateNumber(text9), false],\n      [validateRefreshRateNumber(text10), false],\n      [validateRefreshRateNumber(text12), false],\n      [validateRefreshRateNumber(text13), false],\n    ])('for input: %s (input), should be output: %s', (input, expected) => {\n      const result = errorValidateRefreshRateNumber(input)\n      expect(result).toBe(expected)\n    })\n  })\n\n  describe('errorValidateNegativeInteger', () => {\n    it.each([\n      [validateRefreshRateNumber(text1), true],\n      [validateRefreshRateNumber(text2), true],\n      [validateRefreshRateNumber(text3), true],\n      [validateRefreshRateNumber(text4), true],\n      [validateRefreshRateNumber(text5), true],\n      [validateRefreshRateNumber(text6), true],\n      [validateRefreshRateNumber(text7), true],\n      [validateRefreshRateNumber(text8), true],\n      [validateRefreshRateNumber(text9), true],\n      [validateRefreshRateNumber(text10), true],\n      [validateRefreshRateNumber(text12), false],\n      [validateRefreshRateNumber(text13), false],\n    ])('for input: %s (input), should be output: %s', (input, expected) => {\n      const result = errorValidateNegativeInteger(input)\n      expect(result).toBe(expected)\n    })\n  })\n\n  describe('validateConsumerGroupId', () => {\n    it.each([\n      ['123', '123'],\n      ['123-1', '123-1'],\n      ['$', '$'],\n      ['11.zx-1', '11-1'],\n    ])('for input: %s (input), should be output: %s', (input, expected) => {\n      const result = validateConsumerGroupId(input)\n      expect(result).toBe(expected)\n    })\n  })\n\n  describe('validateNumber', () => {\n    it.each([\n      ['123', '123'],\n      ['123-1', '1231'],\n      ['$', ''],\n      ['11.zx-1', '111'],\n      ['1ueooeu1', '11'],\n      ['euiejk', ''],\n      ['0', '0'],\n      ['31231231231', '31231231231'],\n    ])('for input: %s (input), should be output: %s', (input, expected) => {\n      const result = validateNumber(input)\n      expect(result).toBe(expected)\n    })\n  })\n\n  describe('checkTimestamp', () => {\n    test.each(checkTimestampTests)('%j', ({ input, expected }) => {\n      expect(checkTimestamp(input)).toEqual(expected)\n    })\n  })\n\n  describe('checkConvertToDate', () => {\n    test.each(checkConvertToDateTests)('%j', ({ input, expected }) => {\n      expect(checkConvertToDate(input)).toEqual(expected)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/workbench.spec.ts",
    "content": "import {\n  ExecuteQueryParams,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport {\n  getExecuteParams,\n  getParsedParamsInQuery,\n  parseParams,\n  findMarkdownPath,\n} from 'uiSrc/utils'\nimport { CodeButtonParams, MOCK_TUTORIALS_ITEMS } from 'uiSrc/constants'\n\nconst paramsState: ExecuteQueryParams = {\n  activeRunQueryMode: RunQueryMode.ASCII,\n  resultsMode: ResultsMode.GroupMode,\n  batchSize: 10,\n}\n\ndescribe('getExecuteParams', () => {\n  it('should properly return params', () => {\n    const btnParams1: CodeButtonParams = { pipeline: '5', mode: 'raw' }\n    const btnParams2: CodeButtonParams = { pipeline: '1.5', mode: 'ascii' }\n    const btnParams3: CodeButtonParams = { pipeline: 'abc' }\n    const btnParams4: CodeButtonParams = { results: 'single', mode: 'raw' }\n    const btnParams5: CodeButtonParams = {\n      results: 'single',\n      mode: 'raw',\n      pipeline: '4',\n    }\n    const btnParams6: CodeButtonParams = {\n      results: 'single',\n      mode: 'raw',\n      pipeline: '-4',\n    }\n\n    const expect1 = {\n      activeRunQueryMode: RunQueryMode.Raw,\n      resultsMode: ResultsMode.GroupMode,\n      batchSize: 5,\n    }\n    const expect2 = {\n      activeRunQueryMode: RunQueryMode.ASCII,\n      resultsMode: ResultsMode.GroupMode,\n      batchSize: 10,\n    }\n    const expect3 = {\n      activeRunQueryMode: RunQueryMode.ASCII,\n      resultsMode: ResultsMode.GroupMode,\n      batchSize: 10,\n    }\n    const expect4 = {\n      activeRunQueryMode: RunQueryMode.Raw,\n      resultsMode: ResultsMode.Default,\n      batchSize: 10,\n    }\n    const expect5 = {\n      activeRunQueryMode: RunQueryMode.Raw,\n      resultsMode: ResultsMode.Default,\n      batchSize: 4,\n    }\n    const expect6 = {\n      activeRunQueryMode: RunQueryMode.Raw,\n      resultsMode: ResultsMode.Default,\n      batchSize: 10,\n    }\n\n    expect(getExecuteParams(btnParams1, paramsState)).toEqual(expect1)\n    expect(getExecuteParams(btnParams2, paramsState)).toEqual(expect2)\n    expect(getExecuteParams(btnParams3, paramsState)).toEqual(expect3)\n    expect(getExecuteParams(btnParams4, paramsState)).toEqual(expect4)\n    expect(getExecuteParams(btnParams5, paramsState)).toEqual(expect5)\n    expect(getExecuteParams(btnParams6, paramsState)).toEqual(expect6)\n  })\n})\n\ndescribe('getParsedParamsInQuery', () => {\n  it.each([\n    ['123', {}],\n    ['get test\\nget test2', {}],\n    ['get test\\nget test2\\nget test3', {}],\n    ['[]\\nget test\\nget test2\\nget test3', undefined],\n    ['get test\\n[mode=raw]\\nget test2\\nget test3', {}],\n    ['[mode=raw]\\nget test\\nget test2\\nget test3', { mode: 'raw' }],\n    ['[mode=raw;mode=ascii]\\nget test\\nget test2\\nget test3', { mode: 'raw' }],\n    [\n      '[mode=raw;results=ascii]info\\nget test\\nget test2\\nget test3',\n      { mode: 'raw', results: 'ascii' },\n    ],\n    [\n      '[mode=raw;results=group;pipeline=10]\\nget test\\nget test2\\nget test3',\n      { mode: 'raw', results: 'group', pipeline: '10' },\n    ],\n  ])('for input: %s (input), should be output: %s', (input, expected) => {\n    const result = getParsedParamsInQuery(input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst findMarkdownPathTests = [\n  // mdPath\n  {\n    input: { mdPath: '/static/workbench/quick-guides/document/learn-more.md' },\n    expected: '0/0',\n  },\n  { input: { mdPath: 'quick-guides/working-with-hash.html' }, expected: '0/2' },\n  { input: { mdPath: 'quick-guides/working-with-json.html' }, expected: '0/1' },\n  {\n    input: { mdPath: 'quick-guides/document-capabilities.html' },\n    expected: '1',\n  },\n  { input: { mdPath: '/redis_stack/working_with_json.md' }, expected: '4' },\n\n  // id\n  { input: { id: 'document-capabilities' }, expected: '0/0' },\n  { input: { id: 'working-with-hash' }, expected: '0/2' },\n  { input: { id: 'working-with-json' }, expected: '0/1' },\n  { input: { id: 'second-internal-page' }, expected: '2' },\n  { input: { id: 'working_with_json' }, expected: '4' },\n]\n\ndescribe('findMarkdownPath', () => {\n  test.each(findMarkdownPathTests)('%j', ({ input, expected }) => {\n    // @ts-ignore\n    const result = findMarkdownPath(MOCK_TUTORIALS_ITEMS, input)\n    expect(result).toEqual(expected)\n  })\n})\n\nconst parseParamsTests: any[] = [\n  ['[]', undefined],\n  ['[execute=auto]', { execute: 'auto' }],\n  ['[execute=auto;]', { execute: 'auto' }],\n  ['[execute=auto;mode=group]', { execute: 'auto', mode: 'group' }],\n  ['[execute=auto;mode=group;]', { execute: 'auto', mode: 'group' }],\n  ['[execute=auto;  mode=group;   ]', { execute: 'auto', mode: 'group' }],\n  ['[mode=raw;mode=ascii;mode=group;]', { mode: 'raw' }], // first parameters should be applied\n  ['[mode=raw]\\n', { mode: 'raw' }],\n  ['[mode=raw]\\r', { mode: 'raw' }],\n]\n\ndescribe('parseParams', () => {\n  it.each(parseParamsTests)(\n    'for input: %s (params), should be output: %s',\n    (params, expected) => {\n      const result = parseParams(params)\n      expect(result).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tests/сontent.spec.ts",
    "content": "import { getContentByFeature } from 'uiSrc/utils/content'\n\nconst getContentByFeatureTests: any[] = [\n  [\n    {\n      title: 'Title1',\n      description: 'desc1',\n    },\n    {\n      feature: {\n        flag: true,\n      },\n    },\n    {\n      title: 'Title1',\n      description: 'desc1',\n    },\n  ],\n  [\n    {\n      title: 'Title1',\n      description: 'desc1',\n      features: {\n        cloud: {\n          title: 'Title2',\n          description: 'desc2',\n          another: 'another',\n        },\n      },\n    },\n    {\n      cloud: {\n        flag: false,\n      },\n    },\n    {\n      title: 'Title1',\n      description: 'desc1',\n      features: {\n        cloud: {\n          title: 'Title2',\n          description: 'desc2',\n          another: 'another',\n        },\n      },\n    },\n  ],\n  [\n    {\n      title: 'Title1',\n      description: 'desc1',\n      features: {\n        cloud: {\n          title: 'Title2',\n          description: 'desc2',\n          another: 'another',\n        },\n      },\n    },\n    {\n      cloud: {\n        flag: true,\n      },\n    },\n    {\n      title: 'Title2',\n      description: 'desc2',\n      another: 'another',\n      features: {\n        cloud: {\n          title: 'Title2',\n          description: 'desc2',\n          another: 'another',\n        },\n      },\n    },\n  ],\n]\n\ndescribe('getContentByFeature', () => {\n  it.each(getContentByFeatureTests)(\n    'for input: %s (content), %s (flags), should be output: %s',\n    (content, flags, expected) => {\n      expect(getContentByFeature(content, flags)).toEqual(expected)\n    },\n  )\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/browser.ts",
    "content": "import { EuiComboBoxOptionOption } from '@elastic/eui'\n\nexport const comboBoxToArray = (items: EuiComboBoxOptionOption[]) =>\n  [...items].map(({ label }) => label)\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/chatbot.ts",
    "content": "import { v4 as uuidv4 } from 'uuid'\nimport {\n  AiChatMessage,\n  AiChatMessageType,\n} from 'uiSrc/slices/interfaces/aiAssistant'\n\nexport const generateHumanMessage = (message: string): AiChatMessage => ({\n  id: `ai_${uuidv4()}`,\n  type: AiChatMessageType.HumanMessage,\n  content: message,\n  context: {},\n})\n\nexport const generateAiMessage = (message = ''): AiChatMessage => ({\n  id: `ai_${uuidv4()}`,\n  type: AiChatMessageType.AIMessage,\n  content: message,\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/cliTextFormatter.ts",
    "content": "import { flattenDeep, isArray, isInteger, isNull, isObject } from 'lodash'\nimport { bulkReplyCommands } from 'uiSrc/constants'\n\nconst formatToText = (reply: any, command: string = ''): string => {\n  let result\n  if (isNull(reply)) {\n    result = '(nil)'\n  } else if (isInteger(reply)) {\n    result = `(integer) ${reply}`\n  } else if (isArray(reply)) {\n    result = formatRedisArrayReply(reply)\n  } else if (isObject(reply)) {\n    result = formatRedisArrayReply(flattenDeep(Object.entries(reply)))\n  } else if (isFormattedCommand(command)) {\n    result = reply\n  } else {\n    result = `\"${reply}\"`\n  }\n\n  return result\n}\n\nconst isFormattedCommand = (commandLine: string = '') =>\n  !!bulkReplyCommands?.find((command) =>\n    commandLine?.trim().toUpperCase().startsWith(command),\n  )\n\nconst formatRedisArrayReply = (reply: any | any[], level = 0): string => {\n  let result: string\n  if (isArray(reply)) {\n    if (!reply.length) {\n      result = '(empty list or set)'\n    } else {\n      result = reply\n        .map((item, index) => {\n          const leftMargin = index > 0 ? '   '.repeat(level) : ''\n          const lineIndex = `${leftMargin}${index + 1})`\n          const value = formatRedisArrayReply(item, level + 1)\n          return `${lineIndex} ${value}`\n        })\n        .join('\\n')\n    }\n  } else {\n    result = `\"${reply}\"`\n  }\n  return result\n}\n\nexport default formatToText\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/extrapolation.ts",
    "content": "export const formatExtrapolation = (\n  value: number | string,\n  showPrefix: boolean = true,\n): string | number => (showPrefix ? `~${value}` : value)\n\nexport const extrapolate = (\n  value: number,\n  options: { apply: boolean; extrapolation?: number; showPrefix?: boolean },\n  fn: (val: number) => string | number = (val) => val,\n): number | string => {\n  const { apply, extrapolation = 1, showPrefix = true } = options\n  if (!apply) {\n    return fn(value)\n  }\n\n  const extrapolated = extrapolation * value\n  const appliedFn = fn(extrapolated)\n\n  return formatExtrapolation(appliedFn, showPrefix)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/formatBytes.ts",
    "content": "const SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\n\nexport const formatBytes = (\n  input: number,\n  decimals: number = 3,\n  splitResult: boolean = false,\n  baseK = 1024,\n): string | [number, string] => {\n  try {\n    const bytes = parseFloat(String(input))\n    const k = baseK\n    const dm = decimals < 0 ? 0 : decimals\n    if (Number.isNaN(bytes) || bytes < 0) return '-'\n    if (bytes === 0) return splitResult ? [0, SIZES[0]] : `0 ${SIZES[0]}`\n\n    const i = Math.floor(Math.log(bytes) / Math.log(k))\n    const sizeIndex = Math.min(i, SIZES.length - 1)\n\n    const value = parseFloat((bytes / k ** sizeIndex).toFixed(dm))\n    const size = SIZES[sizeIndex]\n\n    return splitResult ? [value, size] : `${value} ${size}`\n  } catch (e) {\n    return '-'\n  }\n}\n\nexport const toBytes = (size: number, type: string): number => {\n  const key = SIZES.indexOf(type.toUpperCase())\n\n  return Math.floor(size * 1024 ** key)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/formatDate.ts",
    "content": "import {\n  addMilliseconds,\n  format as formatDateFns,\n  formatDistanceToNow,\n  isValid,\n  isDate,\n} from 'date-fns'\nimport { format as formatDateTZ, toZonedTime } from 'date-fns-tz'\nimport { DATETIME_FORMATTER_DEFAULT, TimezoneOption } from 'uiSrc/constants'\nimport { truncateNumberToFirstUnit } from './truncateTTL'\nimport { IS_NUMBER_REGEX, checkTimestamp } from '../validations'\nimport { Nullable } from '../types'\n\nexport const lastConnectionFormat = (date?: Date) =>\n  date ? `${formatDistanceToNow(new Date(date), { addSuffix: true })}` : 'Never'\n\nexport const millisecondsFormat = (\n  milliseconds: number,\n  formatMask: string = 'HH:mm:ss.SSS',\n) => {\n  const d = new Date(0)\n  return formatDateFns(\n    addMilliseconds(\n      new Date(milliseconds + d.getTimezoneOffset() * 1000 * 60),\n      0,\n    ),\n    formatMask,\n  )\n}\n\nexport const truncateMilliseconds = (milliseconds: number): string => {\n  if (milliseconds < 1000) {\n    return `${milliseconds} msec`\n  }\n\n  return truncateNumberToFirstUnit(milliseconds / 1000)\n}\n\nexport const secondsToMinutes = (time: number) => {\n  if (time < 60) {\n    return `${time} second${time === 1 ? '' : 's'}`\n  }\n  const minutes = Math.floor(time / 60)\n  return `${minutes} minute${minutes === 1 ? '' : 's'}`\n}\n\nexport const checkDateTimeFormat = (\n  format: string,\n  timeZone: TimezoneOption = TimezoneOption.Local,\n): { valid: boolean; error: Nullable<string> } => {\n  try {\n    const dateString = formatDateInternal(new Date(), format, timeZone)\n    return { valid: !!dateString, error: null }\n  } catch (e: any) {\n    return { valid: false, error: e.message }\n  }\n}\n\nexport const convertTimestampToMilliseconds = (value: string): number => {\n  // seconds, microseconds, nanoseconds to milliseconds\n  switch (parseInt(value, 10).toString().length) {\n    case 10:\n      return +value * 1000\n    case 16:\n      return +value / 1000\n    case 19:\n      return +value / 1000000\n    default:\n      return +value\n  }\n}\n\nconst formatDateInternal = (\n  date: Date,\n  format: string,\n  timeZone: TimezoneOption,\n) => {\n  if (timeZone === TimezoneOption.Local || !timeZone) {\n    return formatDateFns(date, format)\n  }\n  const zonedDate = toZonedTime(date, timeZone)\n  return formatDateTZ(zonedDate, format, { timeZone })\n}\n\nconst formatStringTimestamp = (\n  value: string,\n  format: string,\n  timezone: TimezoneOption,\n): string => {\n  // check for string to be a valid value for datetime formatting\n  if (!IS_NUMBER_REGEX.test(value) && isValid(new Date(value))) {\n    return formatDateInternal(new Date(value), format, timezone)\n  }\n  if (checkTimestamp(value)) {\n    const timestamp = convertTimestampToMilliseconds(value)\n    return formatDateInternal(new Date(timestamp), format, timezone)\n  }\n  return value\n}\n\nexport const formatTimestamp = (\n  value: string | Date | number,\n  format: string = DATETIME_FORMATTER_DEFAULT,\n  timezone: TimezoneOption = TimezoneOption.Local,\n): string => {\n  try {\n    if (isDate(value)) {\n      return formatDateInternal(value, format, timezone)\n    }\n    return formatStringTimestamp(value.toString(), format, timezone)\n  } catch (e) {\n    return value.toString()\n  }\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/getTruncatedName.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\n\nexport const getTruncatedName = (fullName?: Nullable<string>) => {\n  if (!fullName) return ''\n\n  if (!/\\s/g.test(fullName)) {\n    return fullName.charAt(0).toUpperCase()\n  }\n\n  return fullName\n    .split(' ')\n    .splice(0, 2)\n    .map((i) => i.charAt(0))\n    .join('')\n    .toUpperCase()\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/index.ts",
    "content": "import replaceSpaces from './replaceSpaces'\nimport removeEmpty from './removeEmpty'\nimport formatToText from './cliTextFormatter'\nimport toRedisCodeBlock from './toRedisCodeBlock'\n\nexport * from './truncateTTL'\nexport * from './truncateNumber'\nexport * from './formatBytes'\nexport * from './formatDate'\nexport * from './extrapolation'\nexport * from './transformQueryParams'\nexport * from './getTruncatedName'\nexport * from './transformRdiPipeline'\nexport * from './browser'\n\nexport { replaceSpaces, removeEmpty, formatToText, toRedisCodeBlock }\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/parseRedisJsonPath.spec.ts",
    "content": "import { wrapPath } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/utils'\nimport parseRedisJsonPath from './parseRedisJsonPath'\n\ndescribe('parseRedisJsonPath', () => {\n  it('parses empty root', () => {\n    expect(parseRedisJsonPath(`$`)).toEqual([])\n  })\n\n  it('parses simple string keys', () => {\n    expect(parseRedisJsonPath(`$[\"foo\"]`)).toEqual(['foo'])\n  })\n\n  it('parses multiple string keys and numbers', () => {\n    expect(parseRedisJsonPath(`$[\"foo\"][0][\"bar\"]`)).toEqual(['foo', 0, 'bar'])\n    expect(parseRedisJsonPath(`$[\"array\"][\"nested\"]`)).toEqual([\n      'array',\n      'nested',\n    ])\n  })\n\n  it('parses keys with spaces and special characters', () => {\n    expect(parseRedisJsonPath(`$[\"some key with spaces\"]`)).toEqual([\n      'some key with spaces',\n    ])\n    expect(parseRedisJsonPath(`$[\"foo[bar]\"]`)).toEqual(['foo[bar]'])\n  })\n\n  it('parses keys with escaped quotes', () => {\n    expect(parseRedisJsonPath(`$[\"redis\\\\\" is cool name\"]`)).toEqual([\n      'redis\" is cool name',\n    ])\n    expect(parseRedisJsonPath(`$[\"She said: 'hello'\"]`)).toEqual([\n      `She said: 'hello'`,\n    ])\n  })\n\n  it('parses unicode characters', () => {\n    expect(parseRedisJsonPath(`$[\"ключ\"]`)).toEqual(['ключ'])\n  })\n\n  it('parses numeric keys properly', () => {\n    expect(parseRedisJsonPath(`$[0][1][2]`)).toEqual([0, 1, 2])\n  })\n\n  it('throws on invalid segments', () => {\n    expect(() => parseRedisJsonPath(`$[invalid]`)).toThrow()\n    expect(() => parseRedisJsonPath(`$[0][\"foo\"`)).toThrow()\n    expect(() => parseRedisJsonPath(`foo[bar]`)).toThrow()\n  })\n\n  it('works without $ root', () => {\n    expect(parseRedisJsonPath(`[\"foo\"][0][\"bar\"]`)).toEqual(['foo', 0, 'bar'])\n  })\n\n  it('parses keys with escaped backslashes and quotes correctly', () => {\n    expect(parseRedisJsonPath(`$[\"foo\\\\\\\\bar\"]`)).toEqual(['foo\\\\bar'])\n    expect(parseRedisJsonPath(`$[\"She's cool\"]`)).toEqual([`She's cool`])\n    expect(parseRedisJsonPath(`$[\"He said: \\\\\"hi\\\\\"\"]`)).toEqual([\n      `He said: \"hi\"`,\n    ])\n    expect(parseRedisJsonPath(`$[\"a\\\\\\\\'b\"]`)).toEqual([`a\\\\'b`])\n    expect(parseRedisJsonPath(`$[\"a\\\\\\\\b\\\\\\\\c\"]`)).toEqual(['a\\\\b\\\\c'])\n  })\n\n  it('parses empty string keys', () => {\n    expect(parseRedisJsonPath(`$[\"\"]`)).toEqual([''])\n  })\n})\n\ndescribe('wrapPath + parseRedisJsonPath roundtrip', () => {\n  const testCases: string[] = [\n    'foo',\n    'foo bar',\n    'foo[bar]',\n    \"She's cool\",\n    '',\n    'ключ',\n\n    // Next cases are commented out because\n    // wrapPath() currently does not handle them correctly\n\n    // 'a\"b',\n    // 'She said: \"hello\"',\n    // 'a\\\\b\\\\c',\n    // 'a\\\\\"b\\\\\\'c',\n  ]\n\n  testCases.forEach((originalKey) => {\n    it(`wraps and parses back \"${originalKey}\"`, () => {\n      const wrapped = wrapPath(JSON.stringify(originalKey), '$')\n      expect(wrapped).not.toBeNull()\n\n      const result = parseRedisJsonPath(wrapped!)\n      const lastSegment = result[result.length - 1]\n\n      expect(lastSegment).toBe(originalKey)\n    })\n  })\n})\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/parseRedisJsonPath.ts",
    "content": "// Matches [number] or ['string'] / [\"string\"] segments (with support for escaped characters inside strings)\nconst REGEX = /\\[(?:([\"'])((?:\\\\.|(?!\\1).)*)\\1|(\\d+))\\]/g\n\n/**\n * Parses a Redis JSONPath string into lodash.get compatible path chunks.\n * Supports both numeric indices and JSON string keys (single or double quoted).\n *\n * Example: $['foo'][0][\"bar\"] => ['foo', 0, 'bar']\n */\n\nconst parseRedisJsonPath = (path: string): (string | number)[] => {\n  if (typeof path !== 'string') throw new TypeError('Path must be a string')\n\n  const matches = Array.from(path.matchAll(REGEX))\n\n  const chunks: (string | number)[] = []\n  let lastIndex = 0\n\n  if (path.startsWith('$')) {\n    lastIndex = 1\n  }\n\n  matches.forEach((match) => {\n    if (match.index !== lastIndex) {\n      throw new SyntaxError(\n        `Invalid segment at position ${lastIndex}: \"${path.slice(lastIndex)}\"`,\n      )\n    }\n\n    const [, quote, strContent, numContent] = match\n\n    // Assumming the path will be created from wrapPath()\n    // no need to handle the JSON encodings\n    if (quote) {\n      const jsonStr = `\"${strContent}\"`\n      chunks.push(JSON.parse(jsonStr))\n    } else if (numContent) {\n      chunks.push(Number(numContent))\n    }\n\n    lastIndex = match.index! + match[0].length\n  })\n\n  if (lastIndex !== path.length) {\n    throw new SyntaxError(\n      `Unexpected trailing content starting at position ${lastIndex}: \"${path.slice(lastIndex)}\"`,\n    )\n  }\n\n  return chunks\n}\n\nexport default parseRedisJsonPath\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/redisCommands.ts",
    "content": "import { ICommand, ICommands, ICommandTokenType } from 'uiSrc/constants'\n\nexport const mergeRedisCommandsSpecs = (\n  initialSpec: ICommands,\n  updatedSpec: ICommands,\n): ICommand[] =>\n  Object.keys(initialSpec).map((name) => ({\n    name,\n    token: name,\n    type: ICommandTokenType.Block,\n    ...(name in updatedSpec ? updatedSpec[name] : initialSpec[name] || {}),\n  }))\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/removeEmpty.ts",
    "content": "export default function removeEmpty(obj: any) {\n  Object.keys(obj).forEach((key) => {\n    // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n    ;(obj[key] && typeof obj[key] === 'object' && removeEmpty(obj[key])) ||\n      ((obj[key] === '' || obj[key] === null) && delete obj[key])\n  })\n  return obj\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/replaceSpaces.ts",
    "content": "export default function replaceSpaces(text: string | number = '') {\n  if (text === ' ') {\n    return '\\u00a0'\n  }\n  return text?.toString().replace(/\\s\\s/g, '\\u00a0\\u00a0') ?? ''\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/toRedisCodeBlock.ts",
    "content": "import { Nullable } from 'uiSrc/utils'\n\nconst toRedisCodeBlock = (query: Nullable<string>) => {\n  if (!query) return null\n\n  return `\\`\\`\\`redis\\n${query}\\n\\`\\`\\``\n}\n\nexport default toRedisCodeBlock\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/transformQueryParams.ts",
    "content": "import { toNumber, transform, isNaN } from 'lodash'\n\nexport const transformQueryParamsObject = (\n  properties: Record<string, any> = {},\n) =>\n  transform(\n    properties,\n    (result: Record<string, any>, value, key) => {\n      if (value === 'true' || value === 'false') {\n        result[key] = value === 'true'\n        return\n      }\n      if (!isNaN(toNumber(value))) {\n        result[key] = toNumber(value)\n        return\n      }\n\n      result[key] = value\n    },\n    {},\n  )\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts",
    "content": "import yaml, { YAMLException } from 'js-yaml'\nimport { isEmpty } from 'lodash'\nimport {\n  IConnectionResult,\n  IPipeline,\n  IPipelineJSON,\n  IYamlFormatError,\n  TestConnectionStatus,\n  TransformResult,\n} from 'uiSrc/slices/interfaces'\n\nexport const yamlToJson = (value: string, onError: (e: string) => void) => {\n  try {\n    return yaml.load(value) || {}\n  } catch (e) {\n    if (e instanceof YAMLException) {\n      onError(e.reason)\n    }\n    return undefined\n  }\n}\n\nexport const pipelineToYaml = (pipeline: IPipelineJSON) => ({\n  config: isEmpty(pipeline?.config) ? '' : yaml.dump(pipeline.config),\n  jobs: pipeline?.jobs\n    ? Object.entries(pipeline.jobs)?.map(([key, value]) => ({\n        name: key,\n        value: yaml.dump(value),\n      }))\n    : [],\n})\n\nexport const pipelineToJson = (\n  { config, jobs }: IPipeline,\n  onError: (errors: IYamlFormatError[]) => void,\n) => {\n  const result: IPipelineJSON = {\n    config: {},\n    jobs: [],\n  }\n  const errors: IYamlFormatError[] = []\n\n  result.config =\n    yamlToJson(config, (msg) => errors.push({ filename: 'config', msg })) || {}\n\n  result.jobs = jobs.reduce<{ [key: string]: unknown }>((acc, job) => {\n    acc[job.name] =\n      yamlToJson(job.value, (msg) =>\n        errors.push({ filename: job.name, msg }),\n      ) || {}\n    return acc\n  }, {})\n\n  if (errors.length) {\n    onError(errors)\n    return undefined\n  }\n\n  return result\n}\n\nexport const transformConnectionResults = (\n  results: IConnectionResult,\n): TransformResult => {\n  const result: TransformResult = {\n    target: { success: [], fail: [] },\n    source: { success: [], fail: [] },\n  }\n\n  if (!results?.targets) {\n    return result\n  }\n\n  try {\n    Object.entries(results.targets).forEach(([target, details]) => {\n      if (details.status === TestConnectionStatus.Success) {\n        result.target.success.push({ target })\n      } else if (details.status === TestConnectionStatus.Fail) {\n        const errorMessage = details.error?.message || 'Error'\n        result.target.fail.push({ target, error: errorMessage })\n      }\n    })\n  } catch (error) {\n    // ignore\n  }\n\n  if (!results?.sources) {\n    return result\n  }\n\n  Object.entries(results.sources).forEach(([source, details]) => {\n    if (details.connected) {\n      result.source.success.push({ target: source })\n    } else {\n      const errorMessage = details.error || 'Error'\n      result.source.fail.push({ target: source, error: errorMessage })\n    }\n  })\n\n  return result\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/truncateNumber.ts",
    "content": "// truncate Number to Range:\n// 500 => 500\n// 1500 => 1 K\n// 2500000 => 2 M\n// 2500000000 => 2 B\nexport const truncateNumberToRange = (number: number) => {\n  const thousand = 1_000\n  const million = 1_000_000\n  const billion = 1_000_000_000\n\n  if (number >= billion) {\n    return `${Math.floor(number / billion)} B`\n  }\n\n  if (number >= million) {\n    return `${Math.floor(number / million)} M`\n  }\n\n  if (number >= thousand) {\n    return `${Math.floor(number / thousand)} K`\n  }\n\n  return number.toString()\n}\n\nexport const truncatePercentage = (value = 0, afterDotCount = 0) =>\n  Number.isInteger(value) ? value : value.toFixed(afterDotCount)\n"
  },
  {
    "path": "redisinsight/ui/src/utils/transformers/truncateTTL.ts",
    "content": "import { formatDuration, intervalToDuration } from 'date-fns'\nimport { isNumber } from 'lodash'\n\nimport { MAX_TTL_NUMBER } from '../validations'\n\nconst TRUNCATE_DELIMITER = ', '\n// Replace default strings of duration to cutted\n// 94 years, 9 month, 3 minutes => 94 yr, 9mo, 3min\nexport const cutDurationText = (text = '') =>\n  text\n    .replace(/years?/, 'yr')\n    .replace(/months?/, 'mo')\n    .replace(/days?/, 'd')\n    .replace(/hours?/, 'h')\n    .replace(/minutes?/, 'min')\n    .replace(/seconds?/, 's')\n\n// truncate TTL to Range:\n// 500 => 500\n// 1500 => 1 K\n// 2500000 => 2 M\n// 2500000000 => 2 B\nexport const truncateTTLToRange = (ttl: number) => {\n  if (!isNumber(ttl)) {\n    return '-'\n  }\n  if (ttl === -1) {\n    return 'No limit'\n  }\n\n  if (ttl >= 0 && ttl < 1_000) {\n    return `${ttl}`\n  }\n\n  const thousand = 1_000\n  const million = 1_000_000\n  const billion = 1_000_000_000\n\n  if (ttl >= thousand && ttl < million) {\n    return `${Math.floor(ttl / thousand)} K`\n  }\n\n  if (ttl >= million && ttl < billion) {\n    return `${Math.floor(ttl / million)} M`\n  }\n\n  if (ttl >= billion && ttl <= MAX_TTL_NUMBER) {\n    return `${Math.floor(ttl / billion)} B`\n  }\n\n  return `${ttl}`\n}\n\n// truncate TTL to Seconds with spaces:\n// 500 => 8min, 20s\n// 1500 => 25 min\n// 2500000 => 28d, 22h, 26min\nexport const truncateNumberToDuration = (number: number): string => {\n  try {\n    const duration = intervalToDuration({\n      start: 0,\n      end: number * 1_000,\n    })\n\n    const formattedDuration = formatDuration(duration, {\n      delimiter: TRUNCATE_DELIMITER,\n    })\n\n    return cutDurationText(formattedDuration)\n  } catch (e) {\n    return ''\n  }\n}\n\n// truncate TTL to Seconds with spaces:\n// 500 => 500\n// 1500 => 1 500\n// 2500000 => 2 500 000\nexport const truncateTTLToSeconds = (ttl: number) =>\n  ttl?.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ' ') ?? ''\n\nexport const truncateNumberToFirstUnit = (number: number): string =>\n  truncateNumberToDuration(number).split(TRUNCATE_DELIMITER)[0]\n"
  },
  {
    "path": "redisinsight/ui/src/utils/tree.ts",
    "content": "export const getTreeLeafField = (delimiter = '') => `keys${delimiter}keys`\n"
  },
  {
    "path": "redisinsight/ui/src/utils/types.ts",
    "content": "/**\n * The value MUST exist, but can be undefined\n */\nexport type Maybe<T> = T | undefined\n\n/**\n * The value can have NULL value\n */\nexport type Nullable<T> = T | null\n"
  },
  {
    "path": "redisinsight/ui/src/utils/validations.ts",
    "content": "import { floor } from 'lodash'\nimport { isValid } from 'date-fns'\n\nexport const MAX_TTL_NUMBER = 2_147_483_647\nexport const MAX_PORT_NUMBER = 65_535\nexport const MAX_TIMEOUT_NUMBER = 1_000_000\nexport const MAX_SCORE_DECIMAL_LENGTH = 15\nexport const MAX_REFRESH_RATE = 999.9\nexport const MIN_REFRESH_RATE = 1.0\n\nexport const entryIdRegex = /^(\\*)$|^(([0-9]+)(-)((\\*)$|([0-9]+$)))/\nexport const consumerGroupIdRegex = /^(\\$)$|^0$|^(([0-9]+)(-)([0-9]+$))/\n\nexport const validateField = (text: string) => text.replace(/\\s/g, '')\n\nexport const validateEntryId = (initValue: string) =>\n  initValue.replace(/[^0-9-*]+/gi, '')\nexport const validateConsumerGroupId = (initValue: string) =>\n  initValue.replace(/[^0-9-$]+/gi, '')\n\nexport const validateCountNumber = (initValue: string) => {\n  const value = initValue.replace(/[^0-9]+/gi, '')\n\n  if (+value <= 0) {\n    return ''\n  }\n\n  return value\n}\n\nexport const validateTTLNumber = (initValue: string) => {\n  const value = +initValue.replace(/[^0-9]+/gi, '')\n\n  if (value > MAX_TTL_NUMBER) {\n    return MAX_TTL_NUMBER.toString()\n  }\n\n  if (value < 0 || (value === 0 && initValue !== '0')) {\n    return ''\n  }\n\n  return value.toString()\n}\n\nexport const validateTTLNumberForAddKey = (iniValue: string) =>\n  validateTTLNumber(iniValue).replace(/^(0)?/, '')\n\nexport const validateListIndex = (initValue: string) =>\n  initValue.replace(/[^0-9]+/gi, '')\n\nexport const validateScoreNumber = (initValue: string) => {\n  let value = initValue\n    .replace(/[^-0-9.]+/gi, '')\n    .replace(/^(-?\\d*\\.?)|(-?\\d*)\\.?/g, '$1$2')\n    .replace(/(?!^)-/g, '')\n\n  if (\n    value.includes('.') &&\n    value.split('.')[1].length > MAX_SCORE_DECIMAL_LENGTH\n  ) {\n    const numberOfExceed = value.split('.')[1].length - MAX_SCORE_DECIMAL_LENGTH\n    value = value.slice(0, -numberOfExceed)\n  }\n  return value.toString()\n}\n\nexport const validateEmail = (email: string) => {\n  const re =\n    /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/\n  return re.test(String(email).toLowerCase())\n}\n\nexport const validateNumber = (\n  initValue: string,\n  minNumber: number = 0,\n  maxNumber: number = Infinity,\n) => {\n  const positiveNumbers = /[^0-9]+/gi\n  const negativeNumbers = /[^0-9-]+/gi\n  const value = initValue\n    ? initValue.replace(minNumber < 0 ? negativeNumbers : positiveNumbers, '')\n    : ''\n\n  if (+value > maxNumber) {\n    return maxNumber.toString()\n  }\n\n  if (+value < minNumber) {\n    return ''\n  }\n\n  return value.toString()\n}\n\nexport const validateRefreshRateNumber = (initValue: string) => {\n  let value = initValue.replace(/[^0-9.]/gi, '')\n\n  if (countDecimals(+value) > 0) {\n    value = `${floor(+value, 1)}`\n  }\n\n  if (+value > MAX_REFRESH_RATE) {\n    return MAX_REFRESH_RATE.toString()\n  }\n\n  if (+value < 0) {\n    return ''\n  }\n\n  return value.toString()\n}\n\nexport const errorValidateRefreshRateNumber = (value: string) => {\n  const decimalsRegexp = /^\\d+(\\.\\d{1})?$/\n  return !decimalsRegexp.test(value)\n}\n\nexport const errorValidateNegativeInteger = (value: string) => {\n  const negativeIntegerRegexp = /^-?\\d+$/\n  return !negativeIntegerRegexp.test(value)\n}\n\nexport const validateCertName = (initValue: string) =>\n  initValue.replace(/[^ a-zA-Z0-9!@#$%^&*\\-_()[\\]]+/gi, '').toString()\n\nexport const isRequiredStringsValid = (...params: string[]) =>\n  params.every((p = '') => p.length > 0)\n\nconst countDecimals = (value: number) => {\n  if (Math.floor(value) === value) return 0\n  return value.toString().split('.')?.[1]?.length || 0\n}\n\nconst getApproximateNumber = (number: number): string =>\n  number < 1 ? '<1' : `${Math.round(number)}`\n\nexport const getApproximatePercentage = (\n  total?: number,\n  part: number = 0,\n): string => {\n  const percent = (total ? part / total : 1) * 100\n  return `${getApproximateNumber(percent)}%`\n}\n\nexport const IS_NUMBER_REGEX = /^-?\\d*(\\.\\d+)?$/\nexport const IS_TIMESTAMP = /^(\\d{10}|\\d{13}|\\d{16}|\\d{19})$/\nexport const IS_NEGATIVE_TIMESTAMP = /^-(\\d{9}|\\d{12}|\\d{15}|\\d{18})$/\nexport const IS_INTEGER_NUMBER_REGEX = /^\\d+$/\n\nconst detailedTimestampCheck = (value: string) => {\n  try {\n    // test integer to be of 10, 13, 16 or 19 digits\n    const integerPart = parseInt(value, 10).toString()\n\n    if (\n      IS_TIMESTAMP.test(integerPart) ||\n      IS_NEGATIVE_TIMESTAMP.test(integerPart)\n    ) {\n      if (integerPart.length === value.length) {\n        return true\n      }\n      // check part after dot separator (checking floating numbers)\n      const subPart = value.replace(integerPart, '')\n      return IS_INTEGER_NUMBER_REGEX.test(subPart.substring(1, subPart.length))\n    }\n    return false\n  } catch (err) {\n    // ignore errors\n    return false\n  }\n}\n\n// checks stringified number to may be a timestamp\nexport const checkTimestamp = (value: string): boolean =>\n  IS_NUMBER_REGEX.test(value) && detailedTimestampCheck(value)\n\n// checks any string to may be converted to date\nexport const checkConvertToDate = (value: string): boolean => {\n  // if string is not number-like, try to convert it to date\n  if (!IS_NUMBER_REGEX.test(value)) {\n    return isValid(new Date(value))\n  }\n\n  return checkTimestamp(value)\n}\n"
  },
  {
    "path": "redisinsight/ui/src/utils/workbench.ts",
    "content": "import { first, identity, isInteger, pickBy } from 'lodash'\nimport {\n  CodeButtonResults,\n  CodeButtonRunQueryMode,\n  CodeButtonParams,\n} from 'uiSrc/constants'\nimport { WBQueryType } from 'uiSrc/pages/workbench/constants'\nimport {\n  EnablementAreaComponent,\n  ExecuteQueryParams,\n  IEnablementAreaItem,\n  IPluginVisualization,\n  ResultsMode,\n  RunQueryMode,\n} from 'uiSrc/slices/interfaces'\nimport { getVisualizationsByCommand } from 'uiSrc/utils/plugins'\nimport { store } from 'uiSrc/slices/store'\nimport { getMonacoLines, isParamsLine } from './monaco'\nimport { Maybe, Nullable } from './types'\n\nconst getWBQueryType = (\n  query: string = '',\n  views: IPluginVisualization[] = [],\n) => {\n  const defaultPluginView = getVisualizationsByCommand(query, views).find(\n    (view) => view.default,\n  )\n\n  return defaultPluginView ? WBQueryType.Plugin : WBQueryType.Text\n}\n\nconst getExecuteParams = (\n  params: CodeButtonParams = {},\n  state: ExecuteQueryParams,\n): ExecuteQueryParams => {\n  const {\n    batchSize: batchSizeState,\n    resultsMode: resultsModeState,\n    activeRunQueryMode: activeRunQueryModeState,\n  } = state\n  const { results, mode, pipeline } = params\n\n  const batchSize =\n    pipeline && isInteger(+pipeline) && +pipeline >= 0\n      ? +pipeline\n      : batchSizeState\n  const resultsMode =\n    results && results in CodeButtonResults\n      ? CodeButtonResults[results]\n      : resultsModeState\n  const activeRunQueryMode =\n    mode && mode in CodeButtonRunQueryMode\n      ? CodeButtonRunQueryMode[mode]\n      : activeRunQueryModeState\n\n  return { batchSize, resultsMode, activeRunQueryMode }\n}\n\nexport const parseParams = (params?: string): Maybe<CodeButtonParams> => {\n  if (params?.trim().match(/(^\\[).+(]$)/g)) {\n    return pickBy(\n      params\n        ?.trim()\n        ?.replaceAll(' ', '')\n        ?.replace(/^\\[|]$/g, '')\n        ?.split(';')\n        .reduce((prev: {}, next: string) => {\n          const [key, value] = next.split('=')\n          return {\n            [key]: value,\n            ...prev,\n          }\n        }, {}),\n      identity,\n    )\n  }\n  return undefined\n}\n\nexport const getParsedParamsInQuery = (query: string) => {\n  let parsedParams: Maybe<CodeButtonParams> = {}\n  const lines = getMonacoLines(query)\n\n  if (isParamsLine(first(lines))) {\n    const paramsLine = lines.shift() || ''\n    const params = paramsLine?.substring?.(paramsLine.indexOf(']') + 1, 0) ?? ''\n\n    parsedParams = parseParams(params)\n  }\n\n  return parsedParams\n}\n\nexport const findMarkdownPath = (\n  manifest: IEnablementAreaItem[],\n  { mdPath = '', id = '' }: { mdPath?: string; id?: string },\n): Nullable<string> => {\n  if (!manifest) return null\n\n  const stack: {\n    data: IEnablementAreaItem[]\n    mdPath: string\n    id: string\n    path: number[]\n  }[] = [{ data: manifest, mdPath, id, path: [] }]\n\n  while (stack.length > 0) {\n    const { data, mdPath, id, path } = stack.pop()!\n\n    for (let i = 0; i < data.length; i++) {\n      const obj = data[i]\n      const currentPath = [...path, i]\n      const isCurrentObject =\n        (id && obj.id === id) || (mdPath && obj.args?.path?.includes(mdPath))\n\n      if (\n        obj.type === EnablementAreaComponent.InternalLink &&\n        isCurrentObject\n      ) {\n        return currentPath.join('/')\n      }\n\n      if (obj.type === EnablementAreaComponent.Group && obj.children) {\n        stack.push({ data: obj.children, mdPath, id, path: currentPath })\n      }\n    }\n  }\n\n  return null\n}\n\nexport const findTutorialPath = (options: { mdPath?: string; id?: string }) =>\n  findMarkdownPath(store.getState().workbench.tutorials?.items, options)\n\nconst isGroupMode = (mode?: ResultsMode) => mode === ResultsMode.GroupMode\nconst isRawMode = (mode?: RunQueryMode) => mode === RunQueryMode.Raw\nconst isSilentMode = (mode?: ResultsMode) => mode === ResultsMode.Silent\nconst isGroupResults = (mode?: ResultsMode) =>\n  mode === ResultsMode.GroupMode || mode === ResultsMode.Silent\nconst isSilentModeWithoutError = (mode?: ResultsMode, fail?: number) =>\n  isSilentMode(mode) && fail === 0\n\nexport {\n  getWBQueryType,\n  getExecuteParams,\n  isGroupMode,\n  isRawMode,\n  isGroupResults,\n  isSilentMode,\n  isSilentModeWithoutError,\n}\n"
  },
  {
    "path": "redisinsight/ui/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\"\n}\n"
  },
  {
    "path": "redisinsight/ui/vite-env.d.ts",
    "content": "/// <reference types=\"vite-plugin-svgr/client\" />\n\ninterface ImportMetaEnv {\n  readonly RI_BASE_API_URL: string\n  readonly RI_APP_PREFIX: string\n  readonly RI_APP_PORT: number\n  readonly RI_SCAN_TREE_COUNT: number\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv\n}\n"
  },
  {
    "path": "redisinsight/ui/vite.config.mjs",
    "content": "import 'dotenv/config';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport svgr from 'vite-plugin-svgr';\nimport fixReactVirtualized from 'esbuild-plugin-react-virtualized';\nimport { reactClickToComponent } from 'vite-plugin-react-click-to-component';\nimport { ViteEjsPlugin } from 'vite-plugin-ejs';\nimport istanbul from 'vite-plugin-istanbul';\n// import { compression } from 'vite-plugin-compression2'\nimport { fileURLToPath, URL } from 'url';\nimport path from 'path';\nimport { defaultConfig } from './src/config/default';\n\nconst isElectron = defaultConfig.app.type === 'ELECTRON';\n// set path to index.tsx in the index.html\nprocess.env.RI_INDEX_NAME = isElectron ? 'indexElectron.tsx' : 'index.tsx';\nconst outDir = isElectron ? '../dist/renderer' : './dist';\n\nlet base;\nif (defaultConfig.api.hostedBase) {\n  base = defaultConfig.api.hostedBase;\n} else {\n  base =\n    defaultConfig.app.env === 'development'\n      ? '/'\n      : isElectron\n        ? ''\n        : '/__RIPROXYPATH__';\n}\n\n/**\n * @type {import('vite').UserConfig}\n */\nexport default defineConfig({\n  base,\n  plugins: [\n    react(),\n    svgr({ include: ['**/*.svg?react'] }),\n    reactClickToComponent(),\n    ViteEjsPlugin(),\n    // Inject app info to window global object via custom plugin\n    {\n      name: 'app-info',\n      transformIndexHtml(html) {\n        const script = `<script>window.appInfo = ${JSON.stringify({\n          version: defaultConfig.app.version,\n          sha: defaultConfig.app.sha,\n        })};</script>`;\n\n        return html.replace(/<head>/, `<head>\\n  ${script}`);\n      },\n    },\n    // Add istanbul plugin for coverage collection when COLLECT_COVERAGE is true\n    ...(process.env.COLLECT_COVERAGE === 'true'\n      ? [\n          istanbul({\n            include: 'src/**/*',\n            exclude: [\n              'node_modules',\n              'test/',\n              '**/*.spec.ts',\n              '**/*.spec.tsx',\n              '**/*.test.ts',\n              '**/*.test.tsx',\n            ],\n            extension: ['.js', '.ts', '.tsx'],\n            requireEnv: false,\n          }),\n        ]\n      : []),\n    // !isElectron && compression({\n    //   include: [/\\.(js)$/, /\\.(css)$/],\n    //   deleteOriginalAssets: true\n    // }),\n  ],\n  resolve: {\n    alias: {\n      lodash: 'lodash-es',\n      '@elastic/eui$': '@elastic/eui/optimize/lib',\n      '@redislabsdev/redis-ui-components': '@redis-ui/components',\n      '@redislabsdev/redis-ui-styles': '@redis-ui/styles',\n      '@redislabsdev/redis-ui-icons': '@redis-ui/icons',\n      '@redislabsdev/redis-ui-table': '@redis-ui/table',\n      uiSrc: fileURLToPath(new URL('./src', import.meta.url)),\n      apiSrc: fileURLToPath(new URL('../api/src', import.meta.url)),\n    },\n  },\n  server: {\n    port: 8080,\n    fs: {\n      allow: ['..', '../../node_modules/monaco-editor', 'static', 'defaults'],\n    },\n  },\n  envPrefix: 'RI_',\n  optimizeDeps: {\n    include: ['monaco-editor', 'monaco-yaml/yaml.worker'],\n    exclude: [\n      'react-json-tree',\n      'redisinsight-plugin-sdk',\n      'plotly.js-dist-min',\n      '@antv/x6',\n      '@antv/x6-react-shape',\n      '@antv/hierarchy',\n      'class-transformer',\n      'keytar',\n      '@nestjs/common',\n      '@nestjs/core',\n      '@nestjs/event-emitter',\n      '@nestjs/platform-express',\n      '@nestjs/platform-socket.io',\n      '@nestjs/serve-static',\n      '@nestjs/swagger',\n      '@nestjs/typeorm',\n      '@nestjs/websockets',\n      'nestjs-form-data',\n    ],\n    esbuildOptions: {\n      // fix for https://github.com/bvaughn/react-virtualized/issues/1722\n      plugins: [fixReactVirtualized],\n    },\n  },\n  build: {\n    commonjsOptions: {\n      exclude: ['./packages'],\n    },\n    outDir,\n    target: 'es2020',\n    minify: 'esbuild',\n    rollupOptions: {\n      output: {\n        manualChunks(id) {\n          if (id.includes('node_modules')) {\n            return id\n              .toString()\n              .split('node_modules/')[1]\n              .split('/')[0]\n              .toString();\n          }\n\n          if (id.includes('ui/src/assets')) {\n            return 'assets';\n          }\n          return 'index';\n        },\n      },\n    },\n    define: {\n      this: 'window',\n    },\n  },\n  css: {\n    preprocessorOptions: {\n      scss: {\n        // add @layer app for css ordering. Styles without layer have the highest priority\n        // https://github.com/vitejs/vite/issues/3924\n        additionalData: (source, filename) => {\n          if (path.extname(filename) === '.scss') {\n            const skipFiles = ['/main.scss', '/App.scss'];\n            if (skipFiles.every((file) => !filename.endsWith(file))) {\n              return `\n                @use \"uiSrc/styles/mixins/_eui.scss\";\n                @use \"uiSrc/styles/mixins/_global.scss\";\n                @layer app { ${source} }\n              `;\n            }\n          }\n          return source;\n        },\n      },\n    },\n  },\n  define: {\n    global: 'globalThis',\n    'process.env': {},\n    riConfig: defaultConfig,\n  },\n  // hack: apply proxy path to monaco webworker\n  experimental: {\n    renderBuiltUrl() {\n      return { relative: true };\n    },\n  },\n});\n"
  },
  {
    "path": "resources/app/redisinsight.sh",
    "content": "#!/bin/sh\n\nopen /Applications/RedisInsight-preview\n"
  },
  {
    "path": "resources/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n  </dict>\n</plist>"
  },
  {
    "path": "resources/entitlements.mas.inherit.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.app-sandbox</key>\n    <true/>\n    <key>com.apple.security.inherit</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "resources/entitlements.mas.loginhelper.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>com.apple.security.app-sandbox</key>\n        <true/>\n        <key>com.apple.security.inherit</key>\n        <true/>\n    </dict>\n</plist>\n"
  },
  {
    "path": "resources/entitlements.mas.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.app-sandbox</key>\n    <true/>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.files.bookmarks.app-scope</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.network.server</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n    <key>com.apple.security.application-groups</key>\n    <array>\n    <string>UUK47G4BAZ.com.redis.RedisInsight</string>\n    </array>\n    <key>com.apple.application-identifier</key>\n    <string>UUK47G4BAZ.com.redis.RedisInsight</string>\n    <key>com.apple.developer.team-identifier</key>\n    <string>UUK47G4BAZ</string>\n  </dict>\n</plist>\n"
  },
  {
    "path": "resources/resources.d.ts",
    "content": "declare module '*.svg' {\n  const content: any\n  export default content\n}\n\ndeclare module '*.png' {\n  const content: any\n  export default content\n}\n\ndeclare module '*.jpg' {\n  const content: any\n  export default content\n}\n"
  },
  {
    "path": "scripts/.eslintrc",
    "content": "{\n  \"rules\": {\n    \"no-console\": \"off\",\n    \"global-require\": \"off\",\n    \"import/no-dynamic-require\": \"off\",\n    \"import/no-extraneous-dependencies\": \"off\",\n    \"import/prefer-default-export\": \"off\"\n  }\n}\n"
  },
  {
    "path": "scripts/DeleteDistWeb.js",
    "content": "const path = require('path');\nconst rimraf = require('rimraf');\n\nmodule.exports = function deleteDistWeb() {\n  rimraf.sync(path.join(__dirname, '../redisinsight/ui/dist/*'));\n};\n"
  },
  {
    "path": "scripts/DeleteSourceMaps.js",
    "content": "import { join } from 'path';\nimport rimraf from 'rimraf';\n\nexport default function deleteSourceMaps() {\n  rimraf.sync(join(__dirname, '../redisinsight/ui/dist/*.js.map'));\n  rimraf.sync(join(__dirname, '../redisinsight/ui/*.js.map'));\n}\n"
  },
  {
    "path": "scripts/build-statics.cmd",
    "content": "@echo off\n\n:: =============== Plugins ===============\nset PLUGINS_DIR=\".\\redisinsight\\api\\static\\plugins\"\nset PLUGINS_VENDOR_DIR=\".\\redisinsight\\api\\static\\resources\\plugins\"\n\n:: Default plugins assets\ncall sass \".\\redisinsight\\ui\\src\\styles\\main_plugin.scss\" \".\\vendor\\global_styles.css\" --style=compressed --no-source-map\ncall sass \".\\redisinsight\\ui\\src\\styles\\themes\\dark_theme\\darkTheme.scss\" \".\\vendor\\dark_theme.css\" --style=compressed --no-source-map\ncall sass \".\\redisinsight\\ui\\src\\styles\\themes\\light_theme\\lightTheme.scss\" \".\\vendor\\light_theme.css\" --style=compressed --no-source-map\nxcopy \".\\redisinsight\\ui\\src\\assets\\fonts\\graphik\" \".\\vendor\\fonts\\\" /s /e /y\nxcopy \".\\redisinsight\\ui\\src\\assets\\fonts\\inconsolata\" \".\\vendor\\fonts\\\" /s /e /y\nif not exist %PLUGINS_VENDOR_DIR% mkdir %PLUGINS_VENDOR_DIR%\nxcopy \".\\vendor\\.\" \"%PLUGINS_VENDOR_DIR%\" /s /e /y\n\n:: Install developing tools for plugins\nset PACKAGES_DIR=\".\\redisinsight\\ui\\src\\packages\"\ncall yarn --cwd \"%PACKAGES_DIR%\"\n\n:: Install plugins dependencies\nset REDISEARCH_DIR=\".\\redisinsight\\ui\\src\\packages\\redisearch\"\ncall yarn --cwd \"%REDISEARCH_DIR%\"\n\nset REDISGRAPH_DIR=\".\\redisinsight\\ui\\src\\packages\\redisgraph\"\ncall yarn --cwd \"%REDISGRAPH_DIR%\"\n\nset REDISTIMESERSIES_DIR=\".\\redisinsight\\ui\\src\\packages\\redistimeseries-app\"\ncall yarn --cwd \"%REDISTIMESERSIES_DIR%\"\n\nset RI_EXPLIAIN_DIR=\".\\redisinsight\\ui\\src\\packages\\ri-explain\"\ncall yarn --cwd \"%RI_EXPLIAIN_DIR%\"\n\nset CLIENTS_LIST_DIR=\".\\redisinsight\\ui\\src\\packages\\clients-list\"\ncall yarn --cwd \"%CLIENTS_LIST_DIR%\"\n\n::  Build all plugins and common libraries\ncall yarn --cwd \"%PACKAGES_DIR%\" build\n\n:: Copy common libraries to plugins\nset COMMON_DIR=\".\\redisinsight\\ui\\src\\packages\\common\"\nif not exist \"%PLUGINS_DIR%\\common\" mkdir \"%PLUGINS_DIR%\\common\"\nxcopy /E /Y \"%COMMON_DIR%\\index*.js\" \"%PLUGINS_DIR%\\common\"\ncopy \"%COMMON_DIR%\\package.json\" \"%PLUGINS_DIR%\\common\\\"\n\n\n:: Copy redisearch plugin\nif not exist \"%PLUGINS_DIR%\\redisearch\" mkdir \"%PLUGINS_DIR%\\redisearch\"\nif not exist \"%PLUGINS_DIR%\\redisearch\\dist\" mkdir \"%PLUGINS_DIR%\\redisearch\\dist\"\nxcopy \"%REDISEARCH_DIR%\\dist\" \"%PLUGINS_DIR%\\redisearch\\dist\\\" /s /e /y\ncopy \"%REDISEARCH_DIR%\\package.json\" \"%PLUGINS_DIR%\\redisearch\\\"\n\n:: Copy redisgraph plugin\nif not exist \"%PLUGINS_DIR%\\redisgraph\" mkdir \"%PLUGINS_DIR%\\redisgraph\"\nif not exist \"%PLUGINS_DIR%\\redisgraph\\dist\" mkdir \"%PLUGINS_DIR%\\redisgraph\\dist\"\nxcopy \"%REDISGRAPH_DIR%\\dist\" \"%PLUGINS_DIR%\\redisgraph\\dist\\\" /s /e /y\ncopy \"%REDISGRAPH_DIR%\\package.json\" \"%PLUGINS_DIR%\\redisgraph\\\"\n\n:: Copy redistimeseries plugin\nif not exist \"%PLUGINS_DIR%\\redistimeseries-app\" mkdir \"%PLUGINS_DIR%\\redistimeseries-app\"\nif not exist \"%PLUGINS_DIR%\\redistimeseries-app\\dist\" mkdir \"%PLUGINS_DIR%\\redistimeseries-app\\dist\"\nxcopy \"%REDISTIMESERSIES_DIR%\\dist\" \"%PLUGINS_DIR%\\redistimeseries-app\\dist\\\" /s /e /y\ncopy \"%REDISTIMESERSIES_DIR%\\package.json\" \"%PLUGINS_DIR%\\redistimeseries-app\\\"\n\n:: Copy ri-explain plugin\nif not exist \"%PLUGINS_DIR%\\ri-explain\" mkdir \"%PLUGINS_DIR%\\ri-explain\"\nif not exist \"%PLUGINS_DIR%\\ri-explain\\dist\" mkdir \"%PLUGINS_DIR%\\ri-explain\\dist\"\nxcopy \"%RI_EXPLIAIN_DIR%\\dist\" \"%PLUGINS_DIR%\\ri-explain\\dist\\\" /s /e /y\ncopy \"%RI_EXPLIAIN_DIR%\\package.json\" \"%PLUGINS_DIR%\\ri-explain\\\"\n\n:: Copy clients-list and json plugin\nif not exist \"%PLUGINS_DIR%\\clients-list\" mkdir \"%PLUGINS_DIR%\\clients-list\"\nif not exist \"%PLUGINS_DIR%\\clients-list\\dist\" mkdir \"%PLUGINS_DIR%\\clients-list\\dist\"\nxcopy \"%CLIENTS_LIST_DIR%\\dist\" \"%PLUGINS_DIR%\\clients-list\\dist\\\" /s /e /y\ncopy \"%CLIENTS_LIST_DIR%\\package.json\" \"%PLUGINS_DIR%\\clients-list\\\"\n"
  },
  {
    "path": "scripts/build-statics.sh",
    "content": "#!/bin/bash\nset -e\n\npluginsOnlyInstall=\"${pluginsOnlyInstall:-0}\"\n\n# =============== Plugins ===============\nPLUGINS_DIR=\"./redisinsight/api/static/plugins\"\nPLUGINS_VENDOR_DIR=\"./redisinsight/api/static/resources/plugins\"\n\n# Default plugins assets\nsass \"./redisinsight/ui/src/styles/main_plugin.scss\" \"./vendor/global_styles.css\" --style=compressed --no-source-map;\nsass \"./redisinsight/ui/src/styles/themes/dark_theme/darkTheme.scss\" \"./vendor/dark_theme.css\" --style=compressed --no-source-map;\nsass \"./redisinsight/ui/src/styles/themes/light_theme/lightTheme.scss\" \"./vendor/light_theme.css\" --style=compressed --no-source-map;\ncp -R \"./redisinsight/ui/src/assets/fonts/graphik/\" \"./vendor/fonts\"\ncp -R \"./redisinsight/ui/src/assets/fonts/inconsolata/\" \"./vendor/fonts\"\nmkdir -p \"${PLUGINS_VENDOR_DIR}\"\ncp -R \"./vendor/.\" \"${PLUGINS_VENDOR_DIR}\"\n\n\n# Install developing tools for plugins\nPACKAGES_DIR=\"./redisinsight/ui/src/packages\"\nyarn --cwd \"${PACKAGES_DIR}\"\n\n# Install plugins dependencies\nREDISEARCH_DIR=\"./redisinsight/ui/src/packages/redisearch\"\nyarn --cwd \"${REDISEARCH_DIR}\"\n\nREDISGRAPH_DIR=\"./redisinsight/ui/src/packages/redisgraph\"\nyarn --cwd \"${REDISGRAPH_DIR}\"\n\nREDISTIMESERIES_DIR=\"./redisinsight/ui/src/packages/redistimeseries-app\"\nyarn --cwd \"${REDISTIMESERIES_DIR}\"\n\nRI_EXPLIAIN_DIR=\"./redisinsight/ui/src/packages/ri-explain\"\nyarn --cwd \"${RI_EXPLIAIN_DIR}\"\n\nCLIENTS_LIST_DIR=\"./redisinsight/ui/src/packages/clients-list\"\nyarn --cwd \"${CLIENTS_LIST_DIR}\"\n\n# Build all plugins and common libraries\nNODE_OPTIONS=--max_old_space_size=4096 yarn --cwd \"${PACKAGES_DIR}\" build\n\n# Copy common libraries to plugins\nCOMMON_DIR=\"./redisinsight/ui/src/packages/common\"\nif [ $pluginsOnlyInstall != 1 ]; then\n  mkdir -p \"${PLUGINS_DIR}/common\"\n  cp -R \"${COMMON_DIR}/index\"*.js \"${COMMON_DIR}/package.json\" \"${PLUGINS_DIR}/common\"\nfi\n\n# Copy redisearch plugin\nif [ $pluginsOnlyInstall != 1 ]; then\n  mkdir -p \"${PLUGINS_DIR}/redisearch\"\n  cp -R \"${REDISEARCH_DIR}/dist\" \"${REDISEARCH_DIR}/package.json\" \"${PLUGINS_DIR}/redisearch\"\nfi\n\n\n# Copy redisgraph plugin\nif [ $pluginsOnlyInstall != 1 ]; then\n  mkdir -p \"${PLUGINS_DIR}/redisgraph\"\n  cp -R \"${REDISGRAPH_DIR}/dist\" \"${REDISGRAPH_DIR}/package.json\" \"${PLUGINS_DIR}/redisgraph\"\nfi\n\n# Copy timeseries plugin\nif [ $pluginsOnlyInstall != 1 ]; then\n  mkdir -p \"${PLUGINS_DIR}/redistimeseries-app\"\n  cp -R \"${REDISTIMESERIES_DIR}/dist\" \"${REDISTIMESERIES_DIR}/package.json\" \"${PLUGINS_DIR}/redistimeseries-app\"\nfi\n\n# Copy ri-explain plugin\nif [ $pluginsOnlyInstall != 1 ]; then\n  mkdir -p \"${PLUGINS_DIR}/ri-explain\"\n  cp -R \"${RI_EXPLIAIN_DIR}/dist\" \"${RI_EXPLIAIN_DIR}/package.json\" \"${PLUGINS_DIR}/ri-explain\"\nfi\n\n# Copy clients-list and json plugins\nif [ $pluginsOnlyInstall != 1 ]; then\n  mkdir -p \"${PLUGINS_DIR}/clients-list\"\n  cp -R \"${CLIENTS_LIST_DIR}/dist\" \"${CLIENTS_LIST_DIR}/package.json\" \"${PLUGINS_DIR}/clients-list\"\nfi\n"
  },
  {
    "path": "scripts/check-port-in-use.js",
    "content": "import chalk from 'chalk';\nimport detectPort from 'detect-port';\n\nconst port = process.env.PORT || '1212';\n\ndetectPort(port, (err, availablePort) => {\n  if (port !== String(availablePort)) {\n    throw new Error(\n      chalk.whiteBright.bgRed.bold(\n        `Port \"${port}\" on \"localhost\" is already in use. Please use another port. ex: PORT=4343 npm start`,\n      ),\n    );\n  } else {\n    process.exit(0);\n  }\n});\n"
  },
  {
    "path": "scripts/deb-after-install.sh",
    "content": "#!/bin/bash\nset -e\n\nOLD_INSTALL_PATH=\"/opt/Redis Insight\"\nNEW_INSTALL_PATH=\"/opt/redisinsight\"\nDESKTOP_FILE=\"/usr/share/applications/redisinsight.desktop\"\n\n\necho \"Checking for running RedisInsight instances...\"\nRUNNING_PIDS=$(pgrep -f \"$NEW_INSTALL_PATH/redisinsight\" || pgrep -f \"$OLD_INSTALL_PATH/redisinsight\" || true)\n\nOUR_PID=$$\nfor PID in $RUNNING_PIDS; do\n    if ! ps -o pid= --ppid $OUR_PID | grep -q $PID; then\n        echo \"Found running RedisInsight instance (PID: $PID), attempting to terminate...\"\n        kill $PID 2>/dev/null || true\n    fi\ndone\n\n# Brief pause to let processes terminate\nsleep 1\n\nif [ -f \"$DESKTOP_FILE\" ]; then\n    echo \"Updating desktop file for launcher compatibility...\"\n\n    # First replace the old path with the new path throughout the file\n    sed -i \"s|$OLD_INSTALL_PATH|$NEW_INSTALL_PATH|g\" \"$DESKTOP_FILE\" || true\n\n    # Then ensure the Exec line is properly formatted without quotes\n    sed -i \"s|^Exec=.*|Exec=$NEW_INSTALL_PATH/redisinsight %U|g\" \"$DESKTOP_FILE\" || true\n\n    # Update desktop database to refresh the icon\n    update-desktop-database 2>/dev/null || true\nfi\n\n# Handle update case: redisinsight exists, Redis Insight exists too\n# This means that we are in an update scenario\nif [ -d \"$NEW_INSTALL_PATH\" ] && [ -d \"$OLD_INSTALL_PATH\" ]; then\n    echo \"Both old and new paths exist - handling update scenario\"\n\n    cp -rf \"$OLD_INSTALL_PATH\"/* \"$NEW_INSTALL_PATH\"/ || true\n\n    rm -rf \"$OLD_INSTALL_PATH\" || true\n\n    # Ensure binary link and permissions\n    ln -sf \"$NEW_INSTALL_PATH/redisinsight\" \"/usr/bin/redisinsight\" || true\n    if [ -f \"$NEW_INSTALL_PATH/redisinsight\" ]; then\n        chmod +x \"$NEW_INSTALL_PATH/redisinsight\" || true\n    fi\n    if [ -f \"$NEW_INSTALL_PATH/chrome-sandbox\" ]; then\n        chown root:root \"$NEW_INSTALL_PATH/chrome-sandbox\" || true\n        chmod 4755 \"$NEW_INSTALL_PATH/chrome-sandbox\" || true\n    fi\n\n    echo \"Update handled successfully\"\n    exit 0\nfi\n\n# Handle simple auto-update case: only redisinsight exists\nif [ -d \"$NEW_INSTALL_PATH\" ] && [ ! -d \"$OLD_INSTALL_PATH\" ]; then\n    echo \"New path exists but old doesn't - likely clean install or auto-update\"\n\n    # Ensure binary link and permissions\n    ln -sf \"$NEW_INSTALL_PATH/redisinsight\" \"/usr/bin/redisinsight\" || true\n    if [ -f \"$NEW_INSTALL_PATH/redisinsight\" ]; then\n        chmod +x \"$NEW_INSTALL_PATH/redisinsight\" || true\n    fi\n    if [ -f \"$NEW_INSTALL_PATH/chrome-sandbox\" ]; then\n        chown root:root \"$NEW_INSTALL_PATH/chrome-sandbox\" || true\n        chmod 4755 \"$NEW_INSTALL_PATH/chrome-sandbox\" || true\n    fi\n\n    echo \"Installation/update completed successfully\"\n    exit 0\nfi\n\n# Handle migration case: only Redis Insight exists.\n#This is to ensure that if somebody updates from a very old version to a newer one, we'll still migrate it as expected\nif [ ! -d \"$NEW_INSTALL_PATH\" ] && [ -d \"$OLD_INSTALL_PATH\" ]; then\n    echo \"Old path found but new doesn't exist - migrating to new path\"\n\n    # Simply move the directory\n    mv \"$OLD_INSTALL_PATH\" \"$NEW_INSTALL_PATH\" || true\n\n    # Ensure binary link and permissions\n    ln -sf \"$NEW_INSTALL_PATH/redisinsight\" \"/usr/bin/redisinsight\" || true\n    if [ -f \"$NEW_INSTALL_PATH/redisinsight\" ]; then\n        chmod +x \"$NEW_INSTALL_PATH/redisinsight\" || true\n    fi\n    if [ -f \"$NEW_INSTALL_PATH/chrome-sandbox\" ]; then\n        chown root:root \"$NEW_INSTALL_PATH/chrome-sandbox\" || true\n        chmod 4755 \"$NEW_INSTALL_PATH/chrome-sandbox\" || true\n    fi\n\n    echo \"Migration completed successfully\"\n    exit 0\nfi\n\n# Neither directory exists - unexpected state\necho \"Neither old nor new path exists. This is an unexpected state.\"\necho \"Creating new installation directory as a fallback\"\nmkdir -p \"$NEW_INSTALL_PATH\" || true\n\n# Always set up the binary link\nln -sf \"$NEW_INSTALL_PATH/redisinsight\" \"/usr/bin/redisinsight\" || true\n\necho \"Post-installation completed with warnings\"\n"
  },
  {
    "path": "scripts/deb-before-remove.sh",
    "content": "#!/bin/bash\nset -e\n\nOLD_INSTALL_PATH=\"/opt/Redis Insight\"\nNEW_INSTALL_PATH=\"/opt/redisinsight\"\nSYMLINK_PATH=\"/usr/bin/redisinsight\"\n\n# Function to kill running RedisInsight instances\nkill_running_instances() {\n    echo \"Checking for running RedisInsight instances...\"\n    RUNNING_PIDS=$(pgrep -f \"$NEW_INSTALL_PATH/redisinsight\" || pgrep -f \"$OLD_INSTALL_PATH/redisinsight\" || true)\n\n    for PID in $RUNNING_PIDS; do\n        echo \"Found running RedisInsight instance (PID: $PID), terminating...\"\n        kill $PID 2>/dev/null || true\n    done\n\n    sleep 2\n\n    REMAINING_PIDS=$(pgrep -f \"$NEW_INSTALL_PATH/redisinsight\" || pgrep -f \"$OLD_INSTALL_PATH/redisinsight\" || true)\n    for PID in $REMAINING_PIDS; do\n        echo \"Force killing remaining RedisInsight instance (PID: $PID)...\"\n        kill -9 $PID 2>/dev/null || true\n    done\n    echo \"All running RedisInsight instances terminated.\"\n}\n\n# Always kill running instances regardless of action\nkill_running_instances\n\ncase \"$1\" in\n    upgrade)\n        echo \"Upgrade detected - skipping directory removal\"\n        # During upgrade, dpkg handles file replacement\n        # We only need to ensure processes are stopped\n        exit 0\n        ;;\n    remove|purge)\n        echo \"Removal detected - performing full cleanup\"\n\n        if [ -L \"$SYMLINK_PATH\" ]; then\n            echo \"Removing symlink: $SYMLINK_PATH\"\n            rm -f \"$SYMLINK_PATH\" || true\n        fi\n\n        if [ -d \"$NEW_INSTALL_PATH\" ]; then\n            echo \"Removing directory: $NEW_INSTALL_PATH\"\n            rm -rf \"$NEW_INSTALL_PATH\" || true\n        fi\n\n        if [ -d \"$OLD_INSTALL_PATH\" ]; then\n            echo \"Removing old directory: $OLD_INSTALL_PATH\"\n            rm -rf \"$OLD_INSTALL_PATH\" || true\n        fi\n\n        if command -v update-desktop-database >/dev/null 2>&1; then\n            echo \"Updating desktop database...\"\n            update-desktop-database 2>/dev/null || true\n        fi\n\n        echo \"RedisInsight cleanup completed successfully\"\n        ;;\n    *)\n        echo \"Unknown action: $1 - performing safe cleanup (processes only)\"\n        exit 0\n        ;;\nesac\n"
  },
  {
    "path": "scripts/fetch-jira-tickets.js",
    "content": "#!/usr/bin/env node\n/**\n * Fetch JIRA tickets using JQL query and return detailed ticket information.\n * This script uses JIRA REST API to fetch tickets with all necessary details.\n */\n\nconst fs = require('fs');\nconst https = require('https');\nconst { URL } = require('url');\n\nfunction parseJiraFilterUrl(urlString) {\n  /** Extract JQL query from JIRA filter URL. */\n  const url = new URL(urlString);\n  const jql = url.searchParams.get('jql');\n  if (!jql) {\n    throw new Error('No JQL parameter found in URL');\n  }\n  // URLSearchParams.get() already returns URL-decoded values per URL Standard\n  // No need for additional decodeURIComponent() call\n  return jql;\n}\n\nfunction getJiraCredentials() {\n  /** Get JIRA credentials from .env.mcp file. */\n  const envFile = '.env.mcp';\n  if (!fs.existsSync(envFile)) {\n    throw new Error(\n      `Credentials file ${envFile} not found. Please set up JIRA credentials.`,\n    );\n  }\n\n  const credentials = {};\n  const content = fs.readFileSync(envFile, 'utf-8');\n  const lines = content.split('\\n');\n\n  for (const line of lines) {\n    const trimmed = line.trim();\n    if (trimmed && trimmed.includes('=') && !trimmed.startsWith('#')) {\n      const [key, ...valueParts] = trimmed.split('=');\n      const value = valueParts.join('=').trim();\n      credentials[key.trim()] = value;\n    }\n  }\n\n  const required = ['JIRA_URL', 'JIRA_USERNAME', 'JIRA_API_TOKEN'];\n  const missing = required.filter((key) => !credentials[key]);\n  if (missing.length > 0) {\n    throw new Error(`Missing required credentials: ${missing.join(', ')}`);\n  }\n\n  return credentials;\n}\n\nfunction extractTextFromAdf(adfContent) {\n  /** Extract plain text from Atlassian Document Format (ADF). */\n  if (typeof adfContent === 'string') {\n    return adfContent;\n  }\n  if (typeof adfContent !== 'object' || adfContent === null) {\n    return String(adfContent);\n  }\n\n  const textParts = [];\n\n  function traverse(node) {\n    if (Array.isArray(node)) {\n      for (const item of node) {\n        traverse(item);\n      }\n    } else if (typeof node === 'object' && node !== null) {\n      if (node.type === 'text' && node.text) {\n        textParts.push(node.text);\n      }\n      if (node.content) {\n        traverse(node.content);\n      }\n    }\n  }\n\n  traverse(adfContent);\n  return textParts.join(' ').trim();\n}\n\nfunction fetchJiraTickets(jqlQuery, credentials) {\n  /** Fetch tickets from JIRA using JQL query. */\n  return new Promise((resolve, reject) => {\n    const baseUrl = credentials.JIRA_URL.replace(/\\/$/, '');\n    const username = credentials.JIRA_USERNAME;\n    const apiToken = credentials.JIRA_API_TOKEN;\n\n    // Create authentication header\n    const authString = `${username}:${apiToken}`;\n    const authBase64 = Buffer.from(authString).toString('base64');\n\n    // Fields to fetch (comprehensive list similar to CSV export)\n    const fields = [\n      'summary',\n      'issuetype',\n      'status',\n      'priority',\n      'labels',\n      'description',\n      'created',\n      'updated',\n      'resolved',\n      'assignee',\n      'reporter',\n      'creator',\n      'fixVersions',\n      'components',\n      'customfield_10020', // Sprint field\n    ];\n\n    const allTickets = [];\n    let startAt = 0;\n\n    function fetchPage() {\n      const params = new URLSearchParams({\n        jql: jqlQuery,\n        fields: fields.join(','),\n        maxResults: '1000',\n        expand: 'changelog',\n        startAt: String(startAt),\n      });\n\n      const searchUrl = `${baseUrl}/rest/api/3/search/jql?${params.toString()}`;\n      const url = new URL(searchUrl);\n\n      const options = {\n        hostname: url.hostname,\n        port: url.port || 443,\n        path: url.pathname + url.search,\n        method: 'GET',\n        headers: {\n          Authorization: `Basic ${authBase64}`,\n          Accept: 'application/json',\n          'Content-Type': 'application/json',\n        },\n        // TLS certificate verification is enabled by default (rejectUnauthorized defaults to true)\n        // This ensures secure connections and prevents man-in-the-middle attacks\n      };\n\n      const req = https.request(options, (res) => {\n        let data = '';\n\n        res.on('data', (chunk) => {\n          data += chunk;\n        });\n\n        res.on('end', () => {\n          try {\n            if (res.statusCode !== 200) {\n              reject(new Error(`JIRA API error: ${res.statusCode} - ${data}`));\n              return;\n            }\n\n            const responseData = JSON.parse(data);\n            const issues = responseData.issues || [];\n\n            if (issues.length === 0) {\n              resolve(allTickets);\n              return;\n            }\n\n            for (const issue of issues) {\n              const ticket = {\n                'Issue key': issue.key,\n                Summary: issue.fields?.summary || '',\n                'Issue Type': issue.fields?.issuetype?.name || '',\n                Status: issue.fields?.status?.name || '',\n                Priority: issue.fields?.priority?.name || '',\n                Labels: issue.fields?.labels || [],\n                Description: extractTextFromAdf(\n                  issue.fields?.description || '',\n                ),\n                Created: issue.fields?.created || '',\n                Updated: issue.fields?.updated || '',\n                Resolved: issue.fields?.resolved || '',\n                Assignee: issue.fields?.assignee?.displayName || '',\n                Reporter: issue.fields?.reporter?.displayName || '',\n              };\n              allTickets.push(ticket);\n            }\n\n            startAt += issues.length;\n            if (startAt >= (responseData.total || 0)) {\n              resolve(allTickets);\n            } else {\n              // Fetch next page\n              fetchPage();\n            }\n          } catch (error) {\n            reject(\n              new Error(`Failed to parse JSON response: ${error.message}`),\n            );\n          }\n        });\n\n        // Add error handler for response stream to prevent hanging promises\n        res.on('error', (error) => {\n          reject(new Error(`Error reading response: ${error.message}`));\n        });\n      });\n\n      req.on('error', (error) => {\n        reject(new Error(`Error fetching from JIRA: ${error.message}`));\n      });\n\n      req.setTimeout(30000, () => {\n        req.destroy();\n        reject(new Error('Request timeout'));\n      });\n\n      req.end();\n    }\n\n    fetchPage();\n  });\n}\n\nasync function main() {\n  if (process.argv.length < 3) {\n    console.error('Usage: node fetch-jira-tickets.js <jira-filter-url>');\n    process.exit(1);\n  }\n\n  const url = process.argv[2];\n\n  try {\n    // Parse JQL from URL\n    const jql = parseJiraFilterUrl(url);\n    console.error(`Extracted JQL: ${jql}\\n`);\n\n    // Get credentials\n    const credentials = getJiraCredentials();\n\n    // Fetch tickets\n    const tickets = await fetchJiraTickets(jql, credentials);\n\n    // Output as JSON\n    console.log(JSON.stringify(tickets, null, 2));\n  } catch (error) {\n    console.error(`Error: ${error.message}`);\n    process.exit(1);\n  }\n}\n\nif (require.main === module) {\n  main();\n}\n\nmodule.exports = {\n  parseJiraFilterUrl,\n  getJiraCredentials,\n  fetchJiraTickets,\n  extractTextFromAdf,\n};\n"
  },
  {
    "path": "scripts/prebuild.js",
    "content": "import rimraf from 'rimraf';\nimport fs from 'fs';\nimport webpackPaths from '../configs/webpack.paths';\n\nconst foldersToRemove = [webpackPaths.distPath, webpackPaths.buildPath];\n\n// remove dist folders\nfoldersToRemove.forEach((folder) => {\n  if (fs.existsSync(folder)) rimraf.sync(folder);\n});\n"
  },
  {
    "path": "scripts/update-version.js",
    "content": "/**\n * Script to update the version number in all necessary files\n *\n * Usage: node scripts/update-version.js <new-version>\n * Example: node scripts/update-version.js 2.65.0\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nif (process.argv.length < 3) {\n  console.error('Please provide a version number as an argument.');\n  console.error('Usage: node update-version.js <new-version>');\n  process.exit(1);\n}\n\nconst newVersion = process.argv[2];\nconst semverRegex = /^\\d+\\.\\d+\\.\\d+$/;\n\nif (!semverRegex.test(newVersion)) {\n  console.error(\n    'Invalid version format. Please use semantic versioning (e.g., 2.65.0).',\n  );\n  process.exit(1);\n}\n\nconst filesToUpdate = [\n  {\n    path: path.join(__dirname, '../redisinsight/package.json'),\n    regex: /\"version\":\\s*\"([^\"]+)\"/,\n    replacement: (match, p1) => match.replace(p1, newVersion),\n  },\n  {\n    path: path.join(__dirname, '../redisinsight/api/package.json'),\n    regex: /\"version\":\\s*\"([^\"]+)\"/,\n    replacement: (match, p1) => match.replace(p1, newVersion),\n  },\n  {\n    path: path.join(__dirname, '../redisinsight/api/config/default.ts'),\n    regex: /appVersion:\\s*process\\.env\\.RI_APP_VERSION\\s*\\|\\|\\s*'([^']+)'/,\n    replacement: (match, p1) => match.replace(p1, newVersion),\n  },\n  {\n    path: path.join(__dirname, '../redisinsight/api/config/swagger.ts'),\n    regex: /version:\\s*'([^']+)'/,\n    replacement: (match, p1) => match.replace(p1, newVersion),\n  },\n  {\n    path: path.join(\n      __dirname,\n      '../redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts',\n    ),\n    regex: /app\\.getVersion\\(\\)\\s*\\|\\|\\s*'([^']+)'/,\n    replacement: (match, p1) => match.replace(p1, newVersion),\n  },\n  {\n    path: path.join(__dirname, '../.github/build/release-docker.sh'),\n    regex: /-v\\s*-\\s*Semver\\s*\\(([^)]+)\\)/,\n    replacement: (match, p1) => match.replace(p1, newVersion),\n  },\n];\n\nlet updatedFiles = 0;\nlet skippedFiles = 0;\nlet errorFiles = 0;\n\nfilesToUpdate.forEach((file) => {\n  try {\n    if (!fs.existsSync(file.path)) {\n      console.warn(`File not found: ${file.path}`);\n      skippedFiles++;\n      return;\n    }\n\n    let content = fs.readFileSync(file.path, 'utf8');\n    const originalContent = content;\n\n    content = content.replace(file.regex, file.replacement);\n\n    if (content === originalContent) {\n      console.warn(`No version pattern found in: ${file.path}`);\n      skippedFiles++;\n      return;\n    }\n\n    fs.writeFileSync(file.path, content);\n    console.log(`Updated version in: ${file.path}`);\n    updatedFiles++;\n  } catch (error) {\n    console.error(`Error updating file ${file.path}:`, error.message);\n    errorFiles++;\n  }\n});\n\nconsole.log('\\n----------------------------------------');\nconsole.log(`Version update summary for ${newVersion}:`);\nconsole.log(`  Files updated: ${updatedFiles}`);\nconsole.log(`  Files skipped: ${skippedFiles}`);\nconsole.log(`  Files with errors: ${errorFiles}`);\nconsole.log('----------------------------------------\\n');\n\nif (updatedFiles === filesToUpdate.length) {\n  console.log('Version updated successfully!');\n} else if (updatedFiles > 0) {\n  console.log('Version updated successfully in SOME files!');\n} else {\n  console.log('No files were updated.');\n  process.exit(1);\n}\n"
  },
  {
    "path": "stories/Playground.mdx",
    "content": "import * as PlaygroundStories from './playground/Playground.stories';\nimport { Meta, Canvas } from '@storybook/addon-docs/blocks';\n\n<Meta title=\"Playground\" of={PlaygroundStories} />\n\n## Playground\n\n### Theme\n\n<Canvas of={ PlaygroundStories.ThemeStory }/>\n\n### Icons\n\n<Canvas of={ PlaygroundStories.GalleryStory }/>\n\n### Colors\n\n<Canvas of={PlaygroundStories.ColorsStory} />\n"
  },
  {
    "path": "stories/Start.mdx",
    "content": "import { Meta } from \"@storybook/addon-docs/blocks\";\n\n<Meta title=\"Getting Started\" />\n\n## Start\n\n### Docs\nRedis Insight is a visual tool that provides capabilities to design, develop, and optimize your Redis application.\n\nQuery, analyse and interact with your Redis data. [Download it here](https://redis.io/insight/#insight-form)!\n\n### Storybook\n\n#### [Play function](https://storybook.js.org/docs/writing-stories/play-function)\nBesides development in isolation, Storybook also provides a way to test interactions and user flows.\n\n#### [Interaction testing](https://storybook.js.org/docs/writing-tests/interaction-testing)\nEvery story you write can be render tested. A render test is a simple version of an interaction test that only tests the ability of a component to render successfully in a given state.\n\n#### [Accessibility testing](https://storybook.js.org/docs/writing-tests/accessibility-testing)\nStorybook provides a way to test accessibility of our components.\n"
  },
  {
    "path": "stories/playground/Colors.tsx",
    "content": "import React, { useState } from 'react'\nimport styled from 'styled-components'\nimport { useTheme } from '@redis-ui/styles'\nimport { Col, Grid, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { type Theme as ThemeType } from 'uiSrc/components/base/theme/types'\nimport { ColorText } from 'uiSrc/components/base/text'\nimport SearchInput from 'uiSrc/components/base/inputs/SearchInput'\n\nconst StyledColorContainer = styled(Col).attrs({\n  gap: 'l',\n  justify: 'start',\n})`\n  max-height: 600px;\n  height: 600px;\n  overflow-y: auto;\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nconst StyledColorItem = styled(Col).attrs({\n  gap: 's',\n  justify: 'center',\n  align: 'center',\n})`\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral200};\n  opacity: 0.8;\n  padding: 5px;\n  min-width: 100;\n  border: 1px solid\n    ${({ theme }: { theme: ThemeType }) =>\n      theme.semantic.color.border.neutral500};\n`\n\nconst ColorSquare = styled.div<{\n  $color: any\n}>`\n  width: 40px;\n  height: 40px;\n  border: 1px solid;\n  background-color: ${({ $color }) => $color};\n`\nconst ColorItem = ({\n  color,\n  colorName,\n}: {\n  color: string\n  colorName: string\n}) => (\n  <StyledColorItem>\n    <Text variant=\"semiBold\" component=\"span\" color=\"primary\">\n      {colorName}\n    </Text>\n    <ColorSquare $color={color} />\n    <Text variant=\"semiBold\" component=\"span\" color=\"primary\">\n      {color}\n    </Text>\n  </StyledColorItem>\n)\nconst ColorSectionTitle = ({ title }: { title: string }) => (\n  <Title\n    size=\"S\"\n    color=\"secondary\"\n    style={{ textAlign: 'center', marginTop: 10 }}\n  >\n    {title}\n  </Title>\n)\n\nconst ColorSection = ({\n  title,\n  colors,\n}: {\n  title: string\n  colors: [string, string][]\n}) => (\n  <>\n    <ColorSectionTitle title={title} />\n    <Grid\n      columns={4}\n      gap=\"m\"\n      centered\n      responsive\n      style={{\n        flexGrow: 1,\n        padding: 10,\n      }}\n    >\n      {colors.map(([colorName, color]) => (\n        <ColorItem key={colorName} color={color} colorName={colorName} />\n      ))}\n    </Grid>\n  </>\n)\n\nexport const Colors = () => {\n  const theme = useTheme()\n  const { semantic } = theme\n  const { color: semanticColors } = semantic\n  const [search, setSearch] = useState('')\n  // Create regex pattern: each character from search with .* in between\n  // Escape special regex characters\n  const escapedSearch = search.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n  const pattern = escapedSearch.split('').join('.*')\n  const regex = new RegExp(pattern, 'i')\n  let semanticCount = 0\n\n  const filteredSemanticColors = Object.keys(semanticColors).reduce(\n    (acc, colorSection) => {\n      const tempColors =\n        semanticColors[colorSection as keyof typeof semanticColors]\n      Object.entries(tempColors).forEach(([colorName, color]) => {\n        if (!search || regex.test(colorName)) {\n          semanticCount++\n          if (acc[colorSection] === undefined) {\n            acc[colorSection] = {}\n          }\n          acc[colorSection][colorName] = color\n        }\n      })\n      return acc\n    },\n    {} as Record<string, Record<string, string>>,\n  )\n\n  return (\n    <StyledColorContainer>\n      <Row gap=\"l\" align=\"center\" grow={false}>\n        <SearchInput\n          allowReset\n          placeholder=\"Search colors\"\n          onChange={(value) => setSearch(value)}\n          value={search}\n          variant=\"underline\"\n        />\n        {search !== '' ? (\n          <Text size=\"s\">\n            <ColorText size=\"XL\" color=\"accent\" variant=\"italic\">\n              {search}\n            </ColorText>\n            :&nbsp;&nbsp;found {semanticCount} colors\n          </Text>\n        ) : (\n          <Text>{semanticCount} colors</Text>\n        )}\n      </Row>\n\n      <ColorSectionTitle title=\"Semantic colors\" />\n      {Object.entries(filteredSemanticColors).map(([colorSection, colors]) => (\n        <ColorSection\n          title={`Semantic: ${colorSection}`}\n          colors={Object.entries(colors)}\n          key={`semantic-${colorSection}`}\n        />\n      ))}\n    </StyledColorContainer>\n  )\n}\n"
  },
  {
    "path": "stories/playground/Gallery.tsx",
    "content": "import React, { useState } from 'react'\nimport styled from 'styled-components'\nimport { Col, FlexItem, Grid, Row } from 'uiSrc/components/base/layout/flex'\nimport { AllIconsType, RiIcon } from 'uiSrc/components/base/icons/RiIcon'\nimport * as Icons from 'uiSrc/components/base/icons/iconRegistry'\nimport { ColorText, Text } from 'uiSrc/components/base/text'\nimport { type Theme as ThemeType } from 'uiSrc/components/base/theme/types'\nimport { SearchInput } from 'uiSrc/components/base/inputs'\n\nconst skip = [\n  'IconProps',\n  'Icon',\n  'IconSizeType',\n  'IconColorType',\n  'ColorIconProps',\n  'MonochromeIconProps',\n  'IconType',\n]\n\nconst StyledContainer = styled(Grid).attrs({\n  columns: 3,\n  gap: 'm',\n  centered: true,\n  responsive: true,\n})`\n  height: 600px;\n  width: 100%;\n  overflow-y: scroll;\n  flex-shrink: 0;\n  flex-grow: 0;\n  gap: 1rem;\n`\n\nconst StyledIcon = styled(FlexItem)`\n  height: 70px;\n  padding: 5px;\n  align-items: center;\n  justify-content: center;\n  gap: 1rem;\n  svg {\n    display: block;\n  }\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral300};\n  border: 1px solid\n    ${({ theme }: { theme: ThemeType }) =>\n      theme.semantic.color.border.neutral500};\n`\n\nexport const Gallery = () => {\n  const [search, setSearch] = useState('')\n  const filteredIcons = Object.keys(Icons).filter((icon) => {\n    if (skip.includes(icon)) {\n      return false\n    }\n    if (!search) {\n      return true\n    }\n    // Create regex pattern: each character from search with .* in between\n    // Escape special regex characters\n    const escapedSearch = search.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n    const pattern = escapedSearch.split('').join('.*')\n    const regex = new RegExp(pattern, 'i')\n    return regex.test(icon)\n  })\n  return (\n    <Col gap=\"l\" align=\"start\" justify=\"start\">\n      <Row gap=\"l\" align=\"center\">\n        <SearchInput\n          allowReset\n          placeholder=\"Search icons\"\n          onChange={(value) => setSearch(value)}\n          value={search}\n          variant=\"underline\"\n        />\n        {search !== '' ? (\n          <Text size=\"s\">\n            <ColorText size=\"XL\" color=\"accent\" variant=\"italic\">\n              {search}\n            </ColorText>\n            :&nbsp;&nbsp;found {filteredIcons.length} icons\n          </Text>\n        ) : (\n          <Text>{filteredIcons.length} icons</Text>\n        )}\n      </Row>\n      <StyledContainer>\n        {filteredIcons.map((icon) => {\n          return (\n            <StyledIcon key={icon}>\n              <RiIcon\n                type={icon as AllIconsType}\n                size=\"XL\"\n                color=\"informative400\"\n              />\n              <Text color=\"primary\" size=\"S\" component=\"span\">\n                {icon}\n              </Text>\n            </StyledIcon>\n          )\n        })}\n      </StyledContainer>\n    </Col>\n  )\n}\n"
  },
  {
    "path": "stories/playground/Playground.stories.tsx",
    "content": "import React from 'react'\nimport { Meta, StoryObj } from '@storybook/react-vite'\nimport { PlaygroundPage } from './PlaygroundPage'\nimport { Theme } from './Theme'\nimport { Colors } from './Colors'\nimport { Gallery } from './Gallery'\n\nconst meta: Meta<typeof PlaygroundPage> = {\n  title: 'Playground',\n  component: PlaygroundPage,\n  tags: ['skip-test', '!autodocs', '!dev'],\n}\nexport default meta\n\nexport const ThemeStory: StoryObj = {\n  render: () => <Theme />,\n}\n\nexport const ColorsStory: StoryObj = {\n  render: () => <Colors />,\n}\n\nexport const GalleryStory: StoryObj = {\n  render: () => <Gallery />,\n}\n"
  },
  {
    "path": "stories/playground/PlaygroundPage.tsx",
    "content": "import React, { useState } from 'react'\nimport styled, { ThemeProvider } from 'styled-components'\nimport { themesDefault, themesRebrand } from '@redis-ui/styles'\nimport { Col, Row } from 'uiSrc/components/base/layout/flex'\nimport { Text } from 'uiSrc/components/base/text'\nimport { Title } from 'uiSrc/components/base/text/Title'\nimport { type Theme as ThemeType } from 'uiSrc/components/base/theme/types'\nimport { Colors } from './Colors'\nimport { Gallery } from './Gallery'\nimport { Theme } from './Theme'\n\nexport const Container = styled(Row).attrs({ gap: 'm' })`\n  padding: 2rem;\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral100};\n  max-width: 100%;\n`\nexport const MainContent = styled(Col).attrs({ gap: 'xl', align: 'center' })`\n  flex-grow: 1;\n`\nconst NavContainer = styled(Col).attrs({ gap: 'm', grow: false })`\n  height: 100%;\n  min-width: 200px;\n  background-color: rgb(\n    from\n      ${({ theme }: { theme: ThemeType }) =>\n        theme.semantic.color.background.neutral100}\n      r g b / 0.75\n  );\n`\n\nconst NavContent = styled.ul`\n  position: sticky;\n  top: 10px;\n  z-index: 100;\n  list-style: none;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n`\n\nexport const PlaygroundPage = () => {\n  const [uiTheme, setUiTheme] = useState(themesDefault.light)\n\n  return (\n    <ThemeProvider theme={uiTheme}>\n      <Container>\n        <NavContainer>\n          <Title color=\"primary\" size=\"L\">\n            Playground\n          </Title>\n          <NavContent>\n            <li>\n              <label>\n                <Text variant=\"semiBold\" color=\"primary\" component=\"span\">\n                  Theme:\n                </Text>\n                &nbsp;\n                <select\n                  style={{\n                    height: 30,\n                    width: 100,\n                    fontWeight: 'bold',\n                  }}\n                  onChange={(event) => {\n                    let theme = uiTheme\n                    switch (event.target.value) {\n                      case 'd2':\n                        theme = themesRebrand.dark\n                        break\n                      case 'l2':\n                        theme = themesRebrand.light\n                        break\n                      case 'd':\n                        theme = themesDefault.dark\n                        break\n                      case 'l':\n                      default:\n                        theme = themesDefault.light\n                        break\n                    }\n                    setUiTheme(theme)\n                  }}\n                >\n                  <option value=\"l\">Light</option>\n                  <option value=\"d\">Dark</option>\n                  <option value=\"o\">Old theme</option>\n                </select>\n              </label>\n            </li>\n            <li>\n              <Text variant=\"semiBold\" color=\"primary\" component=\"span\">\n                <a href=\"#theme\">Theme</a>\n              </Text>\n            </li>\n            <li>\n              <Text variant=\"semiBold\" color=\"primary\" component=\"span\">\n                <a href=\"#icons\">Icons</a>\n              </Text>\n            </li>\n            <li>\n              <Text variant=\"semiBold\" color=\"primary\" component=\"span\">\n                <a href=\"#colors\">Colors</a>\n              </Text>\n            </li>\n          </NavContent>\n          <div\n            style={{\n              height: 10000,\n            }}\n          />\n        </NavContainer>\n        <MainContent>\n          <Title\n            color=\"primary\"\n            id=\"theme\"\n            size=\"XL\"\n            style={{ textAlign: 'center' }}\n          >\n            Theme\n          </Title>\n          <Theme />\n          <Title\n            color=\"primary\"\n            id=\"icons\"\n            size=\"XL\"\n            style={{ textAlign: 'center' }}\n          >\n            Icons\n          </Title>\n          <Gallery />\n          <Title\n            color=\"primary\"\n            id=\"colors\"\n            size=\"XL\"\n            style={{\n              textAlign: 'center',\n              marginTop: 100,\n            }}\n          >\n            Colors\n          </Title>\n          <Colors />\n        </MainContent>\n      </Container>\n    </ThemeProvider>\n  )\n}\n"
  },
  {
    "path": "stories/playground/Theme.tsx",
    "content": "import React, { useMemo, useState } from 'react'\nimport styled, { css } from 'styled-components'\nimport { useTheme } from '@redis-ui/styles'\nimport ReactMonacoEditor from 'react-monaco-editor'\nimport { Col, Grid, Row } from 'uiSrc/components/base/layout/flex'\nimport Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'\nimport { ColorText, Text, Title } from 'uiSrc/components/base/text'\nimport { RiTooltip } from 'uiSrc/components'\nimport MonacoEnvironmentInitializer from 'uiSrc/components/MonacoEnvironmentInitializer/MonacoEnvironmentInitializer'\nimport { type Theme as ThemeType } from 'uiSrc/components/base/theme/types'\n\nenum ThemeTabs {\n  raw = 'raw',\n  formatted = 'formatted',\n}\n\n/**\n * Converts a CSS value to pixels if it's not already in pixels\n * @param value CSS value (e.g., '1rem', '10px', '50%')\n * @returns The original value and the calculated pixel value if applicable\n */\nconst convertToPixels = (value: string) => {\n  // If it's already in pixels, return as is\n  if (value.endsWith('px')) {\n    return { original: value, pixels: value }\n  }\n\n  // Handle rem values\n  if (value.endsWith('rem')) {\n    const remValue = parseFloat(value)\n    // Get the root font size (default to 16px if not set)\n    const rootFontSize =\n      parseFloat(getComputedStyle(document.documentElement).fontSize) || 16\n    const pixelValue = remValue * rootFontSize\n    return { original: value, pixels: `${pixelValue.toFixed(2)}px` }\n  }\n\n  // Handle em values (would need the element's font size)\n  // This is more complex as it depends on the parent element\n\n  // For other units, return null for pixels\n  return { original: value, pixels: null }\n}\n\n// Styled components\nconst FontFacesContainer = styled(Col).attrs({\n  gap: 'l',\n  align: 'stretch',\n})`\n  padding: 10px;\n  opacity: 0.8;\n  min-width: 200px;\n  flex-grow: 1;\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nconst StyledFontItem = styled.div`\n  flex-grow: 1;\n  padding: 10px;\n  border: 1px solid\n    ${({ theme }: { theme: ThemeType }) =>\n      theme.semantic.color.border.neutral500};\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nconst ThemeContainer = styled(Grid).attrs({\n  columns: 4,\n  gap: 'm',\n  centered: true,\n  responsive: true,\n})`\n  padding: 10px;\n  flex-grow: 1;\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nconst StyledItemContainer = styled(Col).attrs({\n  gap: 'l',\n  align: 'start',\n})`\n  opacity: 0.8;\n  padding: 30px;\n  min-width: 200px;\n  border: 1px solid\n    ${({ theme }: { theme: ThemeType }) =>\n      theme.semantic.color.border.neutral500};\n  background-color: ${({ theme }: { theme: ThemeType }) =>\n    theme.semantic.color.background.neutral100};\n`\n\nconst StyledShadowItem = styled.div<{\n  $value: string\n  children?: React.ReactNode\n}>`\n  width: 100%;\n  height: 35px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: transparent;\n  border: 1px solid\n    ${({ theme }: { theme: ThemeType }) =>\n      theme.semantic.color.border.neutral500};\n  box-shadow: ${({ $value }) => css`\n    ${$value}\n  `};\n`\n\nconst StyledSpaceItem = styled.div`\n  width: 50px;\n  height: 15px;\n  background-color: transparent;\n  border: 1px solid\n    ${({ theme }: { theme: ThemeType }) =>\n      theme.semantic.color.border.neutral800};\n`\n\n// Helper components (defined before Theme component that uses them)\nconst FontItem = ({\n  name,\n  value,\n  fontFaces,\n}: {\n  name: string\n  value: string\n  fontFaces: Record<string, string>\n}) => {\n  const { pixels } = convertToPixels(value)\n\n  return (\n    <StyledFontItem>\n      <dl>\n        <dt style={{ marginBottom: 5 }}>\n          <ColorText variant=\"semiBold\" color=\"accent\">\n            {name}\n          </ColorText>\n        </dt>\n        <dd>\n          <Text variant=\"semiBold\" color=\"primary\">\n            {value} {pixels && `(${pixels})`}\n          </Text>\n          {Object.values(fontFaces).map((fontFace) => (\n            <Text\n              color=\"secondary\"\n              key={`${name}-${fontFace}`}\n              style={{\n                fontFamily: fontFace,\n                fontSize: value,\n              }}\n            >\n              Sample text 0124 ,.;:\n            </Text>\n          ))}\n        </dd>\n      </dl>\n    </StyledFontItem>\n  )\n}\n\nconst Fonts = ({\n  fonts,\n}: {\n  fonts: {\n    fontFamily: Record<string, string>\n    fontSize: Record<string, string>\n  }\n}) => (\n  <FontFacesContainer>\n    <Title size=\"S\" variant=\"semiBold\" color=\"primary\">\n      Font faces\n    </Title>\n    {Object.entries(fonts.fontFamily).map(([name, value]) => (\n      <StyledFontItem key={name}>\n        <dl>\n          <dt style={{ marginBottom: 5 }}>\n            <ColorText variant=\"semiBold\" color=\"accent\">\n              {name}\n            </ColorText>\n          </dt>\n          <dd>\n            <Text\n              size=\"L\"\n              variant=\"semiBold\"\n              style={{\n                fontFamily: `${value}`,\n              }}\n            >\n              {value}\n            </Text>\n          </dd>\n        </dl>\n      </StyledFontItem>\n    ))}\n    <Title size=\"S\" variant=\"semiBold\" color=\"primary\">\n      Font sizes\n    </Title>\n\n    {Object.entries(fonts.fontSize).map(([name, value]) => (\n      <FontItem\n        key={name}\n        name={name}\n        value={value}\n        fontFaces={fonts.fontFamily}\n      />\n    ))}\n  </FontFacesContainer>\n)\n\nconst ShadowItem = ({ name, value }: { name: string; value: string }) => (\n  <StyledItemContainer>\n    <StyledShadowItem $value={value}>\n      <RiTooltip title={value}>\n        <Text color=\"accent\">{name}</Text>\n      </RiTooltip>\n    </StyledShadowItem>\n  </StyledItemContainer>\n)\n\nconst Shadows = ({ shadows }: { shadows: Record<string, string> }) => (\n  <ThemeContainer>\n    {Object.entries(shadows).map(([name, value]) => (\n      <ShadowItem key={`shadow-${name}`} name={name} value={value} />\n    ))}\n  </ThemeContainer>\n)\n\nconst SpaceItem = ({ name, value }: { name: string; value: string }) => {\n  const { pixels } = convertToPixels(value)\n\n  return (\n    <StyledItemContainer>\n      <dl>\n        <dt style={{ marginBottom: 5 }}>\n          <ColorText variant=\"semiBold\" color=\"accent\">\n            {name}\n          </ColorText>\n        </dt>\n        <dd>\n          <Text variant=\"semiBold\" color=\"primary\">\n            {value} {pixels && `(${pixels})`}\n          </Text>\n        </dd>\n      </dl>\n\n      <Row style={{ gap: value }}>\n        <StyledSpaceItem />\n        <StyledSpaceItem />\n      </Row>\n    </StyledItemContainer>\n  )\n}\n\nconst Spaces = ({ spaces }: { spaces: Record<string, string> }) => (\n  <ThemeContainer>\n    {Object.entries(spaces).map(([name, value]) => (\n      <SpaceItem key={name} name={name} value={value} />\n    ))}\n  </ThemeContainer>\n)\n\nexport const Theme = () => {\n  const theme = useTheme()\n  const monacoOptions = {\n    readOnly: true,\n    automaticLayout: true,\n    minimap: {\n      enabled: false,\n    },\n  }\n  const [viewTab, setViewTab] = useState(ThemeTabs.raw)\n  const tabs: TabInfo[] = useMemo(() => {\n    const visibleTabs: TabInfo[] = [\n      {\n        value: ThemeTabs.raw,\n        content: (\n          <ReactMonacoEditor\n            language=\"json\"\n            value={JSON.stringify(theme, null, 2)}\n            options={monacoOptions}\n            theme={theme.name === 'dark' ? 'vs-dark' : 'vs'}\n            height={500}\n            width={800}\n          />\n        ),\n        label: (\n          <Text color=\"secondary\" component=\"div\">\n            Raw\n          </Text>\n        ),\n      },\n      {\n        value: ThemeTabs.formatted,\n        content: (\n          <Col\n            style={{ padding: 20, maxHeight: 500, overflowY: 'scroll' }}\n            gap=\"l\"\n          >\n            <Title size=\"M\">\n              Name:&nbsp;\n              <ColorText variant=\"semiBold\" color=\"accent\">\n                {theme.name}\n              </ColorText>\n            </Title>\n            <Title size=\"M\">Core</Title>\n            <Title size=\"S\">Spaces</Title>\n            <Spaces spaces={theme.core.space} />\n            <Title size=\"S\">Shadows</Title>\n            <Shadows shadows={theme.core.shadow} />\n            <Title size=\"S\">Fonts</Title>\n            <Fonts fonts={theme.core.font} />\n          </Col>\n        ),\n        label: (\n          <Text color=\"secondary\" component=\"div\">\n            Formatted\n          </Text>\n        ),\n      },\n    ]\n\n    return visibleTabs\n  }, [viewTab, theme.name])\n  const handleTabChange = (id: string) => {\n    if (viewTab === id) return\n    setViewTab(id as ThemeTabs)\n  }\n  return (\n    <Col align=\"center\" style={{ maxWidth: 1000, minWidth: 600 }}>\n      <MonacoEnvironmentInitializer />\n      <Tabs tabs={tabs} value={viewTab} onChange={handleTabChange} />\n    </Col>\n  )\n}\n"
  },
  {
    "path": "tests/e2e/.desktop.env",
    "content": "COMMON_URL=https://localhost:5530\nAPI_URL=https://localhost:5530/api\nOSS_SENTINEL_PASSWORD=password\nRI_APP_FOLDER_NAME=.redis-insight-stage\n\nOSS_STANDALONE_HOST=localhost\nOSS_STANDALONE_PORT=8100\n\nOSS_STANDALONE_V5_HOST=localhost\nOSS_STANDALONE_V5_PORT=8101\n\nOSS_STANDALONE_V7_HOST=localhost\nOSS_STANDALONE_V7_PORT=8108\n\nOSS_STANDALONE_V8_HOST=localhost\nOSS_STANDALONE_V8_PORT=8109\n\nOSS_STANDALONE_REDISEARCH_HOST=localhost\nOSS_STANDALONE_REDISEARCH_PORT=8102\n\nOSS_STANDALONE_BIG_HOST=localhost\nOSS_STANDALONE_BIG_PORT=8103\n\nOSS_STANDALONE_TLS_HOST=localhost\nOSS_STANDALONE_TLS_PORT=8104\n\nOSS_STANDALONE_EMPTY_HOST=localhost\nOSS_STANDALONE_EMPTY_PORT=8105\n\nOSS_STANDALONE_REDISGEARS_HOST=localhost\nOSS_STANDALONE_REDISGEARS_PORT=8106\n\nOSS_STANDALONE_NOPERM_HOST=localhost\nOSS_STANDALONE_NOPERM_PORT=8100\n\nOSS_CLUSTER_REDISGEARS_2_HOST=localhost\nOSS_CLUSTER_REDISGEARS_2_PORT=8107\n\nOSS_CLUSTER_HOST=localhost\nOSS_CLUSTER_PORT=8200\n\nOSS_SENTINEL_HOST=localhost\nOSS_SENTINEL_PORT=28100\n\nRE_CLUSTER_HOST=localhost\nRE_CLUSTER_PORT=19443\n\nRI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/e2e/notifications.json\nRI_NOTIFICATION_SYNC_INTERVAL=30000\n\nRI_FEATURES_CONFIG_URL=http://localhost:5551/remote/features-config.json\nRI_FEATURES_CONFIG_SYNC_INTERVAL=50000\nREMOTE_FOLDER_PATH=/home/runner/work/RedisInsight/RedisInsight/tests/e2e/remote\n"
  },
  {
    "path": "tests/e2e/.dockerignore",
    "content": "report\nnode_modules\n"
  },
  {
    "path": "tests/e2e/.eslintignore",
    "content": "#Each of the following lines is a glob pattern indicating which paths should be omitted from linting...\n#1st path here\n#2nd path here\n#3rd path here"
  },
  {
    "path": "tests/e2e/.eslintrc",
    "content": "{\n  \"root\": true,\n  \"env\": {\n    \"browser\": true,\n    \"es2017\": true\n  },\n  \"extends\": [\"plugin:@typescript-eslint/recommended\", \"plugin:import/errors\"],\n  \"globals\": {\n    \"Atomics\": \"readonly\",\n    \"SharedArrayBuffer\": \"readonly\"\n  },\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"@typescript-eslint\"],\n  \"overrides\": [\n    {\n      \"files\": [\"**/*.ts\", \"**/**/*.ts\", \"**/**/**/*.ts\", \"**/**/**/**/*.ts\", \"**/**/**/**/**/*.ts\"],\n      \"rules\": {\n        //this will conflict with our standards regarding horizontal and vertical aligning in page models class parameters.\n        //we must decide if this rules is relevant or not, we can't do both...\n        //******************************************** STANDARDS ********************************************\n        //Forcing usage of ;\n        \"semi\": \"off\",\n        \"@typescript-eslint/semi\": [\"error\"],\n        //All clauses must have {}\n        \"curly\": [\"error\", \"all\"],\n        //Forces usage of string[] over Array<string>\n        \"@typescript-eslint/array-type\": [\"error\", {\n          \"array\": true\n        }],\n        //Makes sure that the identation is 4 each all the time.\n        \"indent\": [\"error\", 4, { \"SwitchCase\": 1 }],\n        //Forcing a function to have a return type\n        \"@typescript-eslint/explicit-function-return-type\": \"error\",\n        //Use ' instead of \"\n        \"quotes\": [\"error\", \"single\"],\n        //Use let only when reusing a variable. Else always use const.\n        \"prefer-const\": \"error\",\n        //Use grace accent (`) for string concetration rather than +\n        \"prefer-template\": \"error\",\n        //Forcing usage of primitive types over boxed types. i.e. Use number instead of Number\n        \"@typescript-eslint/ban-types\": [\n          \"error\",\n          {\n            \"types\": {\n              \"object\": false\n            }\n          }\n        ],\n        //Require === and !== (eqeqeq) unless Comparing two literal values, Evaluating the value of typeof, Comparing against null\n        \"eqeqeq\": [\"error\", \"smart\"],\n\n        // ******************** NOT README STANDARDS ********************\n        //Limits the length of a row to 400\n        \"max-len\": [\"error\", 400],\n\n        //Forcing that all of the comments are with spaces.\n        \"spaced-comment\": \"off\",\n        //Forcing not to have random spaces.\n        \"no-trailing-spaces\": [\n          \"error\",\n          {\n            \"ignoreComments\": false\n          }\n        ],\n        //Forcing to use json.['dot'] vs json.dot\n        \"dot-notation\": \"error\",\n\n        //preventing from us to keep unused variables. relevant for imports as well.\n        \"no-unused-vars\": \"off\",\n        \"@typescript-eslint/no-unused-vars\": \"error\",\n        //prevents from having multiple empty lines, our standard should be max of 1 empty line.\n        \"no-multiple-empty-lines\": [\n          \"error\",\n          {\n            \"max\": 1\n          }\n        ],\n        //Require or disallow trailing commas\n        \"comma-dangle\": [\"error\", \"never\"],\n        //Forcing if, else if, else, finally, catch to have opening curly brackets on the same line\n        \"brace-style\": [\"error\", \"stroustrup\"],\n        //using { key: value } or { 'key': value } consistently only!\n        \"quote-props\": [\"error\", \"consistent\"],\n        //cannot use constructor () only constructor()\n        \"space-before-function-paren\": [\"error\", \"never\"],\n        //cannot use else return\n        \"no-else-return\": [\n          \"error\",\n          {\n            \"allowElseIf\": true\n          }\n        ],\n        //spaces between ops, bad: a+ b, good: a + b\n        \"space-infix-ops\": \"error\",\n        //Disabling the requirement of using the Radix parameter when using parseInt function\n        \"radix\": \"off\",\n        //Forcing command spacing, comma (,) should not have a space before but should have one after.\n        \"comma-spacing\": [\n          \"error\",\n          {\n            \"before\": false,\n            \"after\": true\n          }\n        ],\n        //Ensures an imported module can be resolved to a module on the local filesystem\n        \"import/no-unresolved\": \"off\",\n        //Disallow or enforce spaces inside of parentheses\n        \"space-in-parens\": [\"error\", \"never\"],\n        //Require Default Case in Switch Statements\n        \"default-case\": \"error\",\n        //Disallow unnecessary semicolons\n        \"no-extra-semi\": \"error\",\n        //Disallow empty functions\n        \"no-empty-function\": \"error\",\n        //Disallow spacing between function identifiers and their applications\n        \"no-spaced-func\": \"error\",\n        //Eequire or disallow spacing between function identifiers and their invocations\n        \"func-call-spacing\": [\"error\", \"never\"],\n        //Enforce newline before and after dot\n        \"dot-location\": [\"off\"],\n        //Require space before/after arrow function's arrow. (a) => {}\n        \"arrow-spacing\": [\n          \"error\",\n          {\n            \"before\": true,\n            \"after\": true\n          }\n        ],\n        //Enforce a convention in module import order\n        \"import/order\": \"error\",\n        \"object-curly-spacing\": [\"error\", \"always\"],\n        //Disallow duplicate imports\n        \"import/no-duplicates\": \"error\",\n        /* Below are extended rules regarding the recommended typescript rule package...*/\n        //Require consistent spacing around type annotations\n        \"@typescript-eslint/type-annotation-spacing\": \"error\", //RELEVANT: makes sure you have spaces around type declaration.\n        //Require a specific member delimiter style for interfaces and type literals.\n        \"@typescript-eslint/member-delimiter-style\": [\n          \"error\",\n          {\n            \"multiline\": {\n              \"delimiter\": \"comma\",\n              \"requireLast\": false\n            },\n            \"singleline\": {\n              \"delimiter\": \"comma\",\n              \"requireLast\": false\n            }\n          }\n        ], //interface styling for each row, we use comma\n        //Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean\n        \"@typescript-eslint/no-inferrable-types\": \"error\",\n\n        //Enforce consistent spacing between keys and values in object literal properties\n        //Colons never have a space before and only after. key: item instead of key : item\n        \"key-spacing\": [\n          \"error\",\n          {\n            \"beforeColon\": false,\n            \"afterColon\": true\n          }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/e2e/.gitignore",
    "content": "plugins\nreport\nresults\nremote\n.redisinsight\n.redisinsight-v2\n.redisinsight-app\n.redis-insight\nrihomedir\nrdi\nrte/rdi\nchrome_logs.txt"
  },
  {
    "path": "tests/e2e/.testcafe-electron-rc.js",
    "content": "module.exports = {\n  mainWindowUrl: process.env.COMMON_URL + '#/',\n  electronPath: process.env.ELECTRON_PATH,\n  appArgs: [\"--no-sandbox\"]\n}\n"
  },
  {
    "path": "tests/e2e/README.md",
    "content": "# Redis Insight Testcafe e2e tests\n\n## 📚 Redis Test Environments\n\nFor a complete guide to all available Redis environments (standalone, clusters, sentinel, etc.), see **[REDIS_ENVIRONMENTS.md](./REDIS_ENVIRONMENTS.md)**\n\nThis includes:\n- Connection details for 15+ Redis environments\n- VPN setup for cluster access\n- SSH tunneling configuration\n\n---\n\n### Before run tests run next commands\n\n## start application:\n\n```bash\nyarn start:app\n```\n\n## run docker with last redis version\n\n```bash\ndocker run --name redis-last-version -p 7777:6379 -d redislabs/redismod\n```\n\n### Run tests locally from the tests/e2e folder\n\n- to run tests in Chrome browser run\n\n```bash\nyarn test:chrome\n```\n\n### Local test development\n\n- There is no need to run whole test suite when you are developing only one (2, 3 :) ) test.\n  To mark which tests should be run use `test.only(...)` syntax.\n\n```javascript\ntest.only\n    ...\n})\n```\n\n- You can use `test.skip(...)` syntax to skip only one test.\n\n```javascript\ntest.skip\n    ...\n})\n```\n"
  },
  {
    "path": "tests/e2e/REDIS_ENVIRONMENTS.md",
    "content": "# Redis Test Environments - Connection Guide\n\nThis document lists all available Redis environments for testing RedisInsight.\n\n## 🚀 Quick Start\n\n### 1. Start All Redis Services\n```bash\ncd tests/e2e\ndocker-compose -f rte.docker-compose.yml up -d\n```\n\n### 2. Start VPN (Required for Clusters and Sentinel)\n```bash\ndocker-compose -f vpn.docker-compose.yml up -d\n```\n\n### 3. Connect to VPN\n- **Profile**: `tests/e2e/rte/openvpn/test.ovpn`\n- **Import** into your OpenVPN client and Connect\n- **Verify**: `ping 172.31.100.221` should work\n\n### 4. Import Pre-configured Databases (Optional)\nFor quick setup, import all databases at once:\n- **File**: `tests/e2e/rte/RedisInsight_Connections.json`\n- **In RedisInsight**: Datasbes → + Connect existing database → Import → Select the JSON file\n- **Includes**: 12+ pre-configured connections (standalone, clusters, sentinel, SSH tunnel example)\n\n### 5. Ready to Test!\nAll Redis environments are now accessible. See below for connection details.\n\n---\n\n## 📊 Available Environments Summary\n\n- **8 Standalone instances** (different versions and configurations) - No VPN needed\n- **3 Cluster configurations** (plain, RediSearch, RedisGears) - VPN required\n- **1 Sentinel setup** - VPN required\n- **1 Redis Enterprise** - No VPN needed (but needs 12GB+ RAM)\n- **1 SSH server** - For testing SSH tunneling\n\n| Database Type | URL | User/Password | VPN Required | Notes |\n|--------------|-----|----------|--------------|-------|\n| OSS Standalone | localhost:8100 | None | No | Redis with modules (Search, Graph, TimeSeries, JSON, Bloom) |\n| OSS Standalone v5 | localhost:8101 | None | No | Redis 5.0.14 |\n| OSS Standalone v7 | localhost:8108 | None | No | Redis 7.4-rc2 |\n| OSS Standalone v8 | localhost:8109 | None | No | Redis 8.0-M02 |\n| OSS Standalone Empty | localhost:8105 | None | No | Empty database with all modules |\n| OSS Standalone Big | localhost:8103 | None | No | Large dataset (~2.6GB) for performance testing |\n| OSS Standalone TLS | localhost:8104 | None | No | TLS/SSL enabled |\n| OSS Standalone RedisGears | localhost:8106 | None | No | RedisGears 2.0 module |\n| OSS Cluster v7 | localhost:8200 or 172.31.100.211:6379 | None | Yes | 3 master nodes, cluster mode |\n| OSS Cluster RediSearch | localhost:8221 or 172.31.100.221:6379 | None | Yes | 3 masters with RediSearch & JSON |\n| OSS Cluster RedisGears 2.0 | 172.31.100.191:6379 | None | **Yes** | 6 nodes (3 masters + 3 replicas), VPN only |\n| OSS Sentinel | localhost:28100 | password | Yes | Sentinel with 2 primary nodes |\n| Redis Enterprise API | https://localhost:19443 | demo@redislabs.com / 123456 | No | Web UI access |\n| Redis Enterprise DB | localhost:12000 | None | No | Requires 12GB+ RAM, may fail on low memory |\n| SSH Server | localhost:2222 | u / pass | No | For SSH tunneling. Connect to 172.31.100.109:6379 (OSS Standalone) |\n\n\n"
  },
  {
    "path": "tests/e2e/common-actions/browser-actions.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { BrowserPage } from '../pageObjects';\n\nconst browserPage = new BrowserPage();\n\nexport class BrowserActions {\n    /**\n     * Check that all rendered keys on page has info displayed\n     */\n    async verifyAllRenderedKeysHasText(): Promise<void> {\n        const keyListItems = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow);\n        // Take 1st, middle and last one rendered items for test\n        const keysForTest = [keyListItems.nth(0), keyListItems.nth(Math.round(await keyListItems.count / 2)), keyListItems.nth(await keyListItems.count - 2)];\n\n        // Verify that keys info in all columns is not empty\n        for (const key of keysForTest) {\n            const keyColumnsSelectors = [\n                browserPage.cssSelectorKey,\n                browserPage.cssKeyBadge,\n                browserPage.cssKeyTtl,\n                browserPage.cssKeySize\n            ];\n\n            for (const columnSelector of keyColumnsSelectors) {\n                const keyRenderedName = await key.find(keyColumnsSelectors[0]).innerText;\n                const listRenderedKeyInfo = await key.find(columnSelector).innerText;\n\n                await t.expect(listRenderedKeyInfo).notEql('', `\"${keyRenderedName}\" Key has empty data`);\n            }\n        }\n    }\n\n    /**\n     * Verify tooltip contains text\n     * @param expectedText Expected link that is compared with actual\n     * @param contains Should this tooltip contains or not contains text\n     */\n    async verifyTooltipContainsText(expectedText: string, contains: boolean): Promise<void> {\n        contains\n            ? await t.expect(browserPage.tooltip.innerText).contains(expectedText, `\"${expectedText}\" Text is incorrect in tooltip`)\n            : await t.expect(browserPage.tooltip.innerText).notContains(expectedText, `Tooltip still contains text \"${expectedText}\"`);\n    }\n\n    /**\n     * Verify dialog contains text\n     * @param expectedText Expected link that is compared with actual\n     * @param contains Should this tooltip contains or not contains text\n     */\n    async verifyDialogContainsText(expectedText: string, contains: boolean): Promise<void> {\n        contains\n            ? await t.expect(browserPage.dialog.textContent).contains(expectedText, `\"${expectedText}\" Text is incorrect in tooltip`)\n            : await t.expect(browserPage.dialog.textContent).notContains(expectedText, `Dialog still contains text \"${expectedText}\"`);\n    }\n\n    /**\n     * Verify that the new key is displayed at the top of the list of keys and opened and pre-selected in List view\n     * @param keyName Key name\n     */\n    async verifyKeyDisplayedTopAndOpened(keyName: string): Promise<void> {\n        await t.expect(Selector('[aria-rowindex=\"1\"]').withText(keyName).visible).ok(`element with ${keyName} is not visible in the top of list`);\n        await t.expect(browserPage.keyNameFormDetails.withText(keyName).visible).ok(`element with ${keyName} is not opened`);\n    }\n\n    /**\n     * Verify that the new key is not displayed at the top of the list of keys and opened and pre-selected in List view\n     * @param keyName Key name\n     */\n    async verifyKeyIsNotDisplayedTop(keyName: string): Promise<void> {\n        await t.expect(Selector('[aria-rowindex=\"1\"]').withText(keyName).exists).notOk(`element with ${keyName} is not visible in the top of list`);\n    }\n\n    /**\n     * Verify that not patterned keys not displayed with delimiter\n     * @param delimiter string with delimiter value\n     */\n    async verifyNotPatternedKeysNotDisplayed(delimiter: string): Promise<void> {\n        const notPatternedKeys = Selector('[data-testid^=\"badge\"]').parent('[data-testid^=\"node-item_\"]');\n        const notPatternedKeysNumber = await notPatternedKeys.count;\n\n        for (let i = 0; i < notPatternedKeysNumber; i++) {\n            await t.expect(notPatternedKeys.nth(i).withText(delimiter).exists).notOk('Not contained delimiter keys');\n        }\n    }\n\n    /**\n     * Get node name by folders\n     * @param startFolder start folder\n     * @param folderName name of folder\n     * @param delimiter string with delimiter value\n     */\n    getNodeName(startFolder: string, folderName: string, delimiter?: string): string {\n        return delimiter ? `${startFolder}${delimiter}${folderName}` : `${startFolder}${folderName}`;\n    }\n\n    /**\n     * Get node selector by name\n     * @param name node name\n     */\n    getNodeSelector(name: string): Selector {\n        return Selector(`[data-testid^=\"node-item_${name}\"]`);\n    }\n\n    /**\n     * Check tree view structure\n     * @param folders name of folders for tree view build\n     * @param delimiter string with delimiter value\n     */\n    async checkTreeViewFoldersStructure(folders: string[][], delimiters: string[]): Promise<void> {\n        await this.verifyNotPatternedKeysNotDisplayed(delimiters[0]);\n\n        for (let i = 0; i < folders.length; i++) {\n            const delimiter = delimiters.length > 1 ? '-' : delimiters[0];\n            let prevNodeName = '';\n            let prevDelimiter = '';\n\n            // Expand subfolders\n            for (let j = 0; j < folders[i].length; j++) {\n                const nodeName = this.getNodeName(prevNodeName, folders[i][j], prevDelimiter);\n                const node = this.getNodeSelector(nodeName);\n                const fullTestIdSelector = await node.getAttribute('data-testid');\n\n                if (!fullTestIdSelector?.includes('expanded')) {\n                    await t.click(node);\n                }\n\n                prevNodeName = nodeName;\n                prevDelimiter = delimiter;\n            }\n\n            // Verify that the last folder level contains required keys\n            const foundKeyName = `${folders[i].join(delimiter)}`;\n            const firstFolderName = this.getNodeName('', folders[i][0]);\n            const firstFolder = this.getNodeSelector(firstFolderName);\n            await t\n                .expect(Selector(`[data-testid*=\"node-item_${foundKeyName}\"]`).find('[data-testid^=\"key-\"]').exists).ok('Specific key not found')\n                .click(firstFolder);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/common-actions/common-elements-actions.ts",
    "content": "import { t } from 'testcafe';\n\nexport class CommonElementsActions {\n\n    /**\n     * Select Checkbox\n     * @param checkbox Selector of the checkbox to check\n     * @param value value of the checkbox\n     */\n    static async checkCheckbox(checkbox: Selector, value: boolean): Promise<void> {\n\n        if (await checkbox.checked !== value) {\n            await t.click(checkbox);\n        }\n    }\n\n}\n"
  },
  {
    "path": "tests/e2e/common-actions/databases-actions.ts",
    "content": "import * as fs from 'fs';\nimport { Selector, t } from 'testcafe';\nimport { MyRedisDatabasePage } from '../pageObjects';\nimport { DatabaseAPIRequests } from '../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nexport class DatabasesActions {\n    /**\n     * Verify that databases are displayed\n     * @param databases The list of databases to verify\n     */\n    async verifyDatabasesDisplayed(databases: string[]): Promise<void> {\n        for (const db of databases) {\n            const databaseName = myRedisDatabasePage.dbNameList.withText(db);\n            await t.expect(databaseName.exists).ok(`\"${db}\" database doesn't exist`);\n        }\n    }\n\n    /**\n     * Import database using file\n     * @param fileParameters The arguments of imported file\n     */\n    async importDatabase(fileParameters: ImportDatabaseParameters): Promise<void> {\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton)\n            .click(myRedisDatabasePage.importDatabasesBtn)\n            .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [fileParameters.path])\n            .click(myRedisDatabasePage.submitChangesButton)\n            .expect(myRedisDatabasePage.successResultsAccordion.exists).ok(`Databases from ${fileParameters.type} not imported`);\n    }\n\n    /**\n     * Parse json for importing databases\n     * @param path The path to json file\n     */\n    parseDbJsonByPath(path: string): any[] {\n        return JSON.parse(fs.readFileSync(path, 'utf-8'));\n    }\n\n    /**\n     * Select databases checkboxes by their names\n     * @param databases The list of databases to select\n     */\n    async selectDatabasesByNames(databases: string[]): Promise<void> {\n        for (const db of databases) {\n            const databaseId = await databaseAPIRequests.getDatabaseIdByName(db);\n            const databaseCheckbox = Selector(`[data-test-subj=checkboxSelectRow-${databaseId}]`);\n            await t.click(databaseCheckbox);\n        }\n    }\n\n    /**\n     * Find files by they name starts from directory\n     * @param dir The path directory of file\n     * @param fileStarts The file name should start from\n     */\n    async findFilesByFileStarts(dir: string, fileStarts: string): Promise<string[]> {\n        const matchedFiles: string[] = [];\n        const files = fs.readdirSync(dir);\n\n        if (fs.existsSync(dir)) {\n\n            for (const file of files) {\n                if (file.startsWith(fileStarts)) {\n                    matchedFiles.push(file);\n                }\n            }\n        }\n        return matchedFiles;\n    }\n\n    /**\n     * Get files count by name starts from directory\n     * @param dir The path directory of file\n     * @param fileStarts The file name should start from\n     */\n    async getFileCount(dir: string, fileStarts: string): Promise<number> {\n        if (fs.existsSync(dir)) {\n            const matchedFiles: string[] = [];\n            const files = fs.readdirSync(dir);\n            for (const file of files) {\n                if (file.startsWith(fileStarts)) {\n                    matchedFiles.push(file);\n                }\n            }\n            return matchedFiles.length;\n        }\n        return 0;\n    }\n}\n\n/**\n * Import database parameters\n * @param path The path to file\n * @param type The type of application\n * @param dbNames The names of databases\n * @param userName The username of db\n * @param password The password of db\n * @param connectionType The connection type of db\n * @param fileName The file name\n * @param parsedJson The parsed json content\n */\nexport type ImportDatabaseParameters = {\n    path: string,\n    type?: string,\n    dbNames?: string[],\n    userName?: string,\n    password?: string,\n    connectionType?: string,\n    fileName?: string,\n    parsedJson?: any\n};\n"
  },
  {
    "path": "tests/e2e/common-actions/recommendations-actions.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class RecommendationsActions {\n    /**\n     * Get recommendation container by name\n     * @param recommendationName Name of recommendation\n     */\n    async getRecommendationSelectorByName(recommendationName: string): Promise<Selector> {\n        return Selector(`[data-testid=${recommendationName}-recommendation]`);\n    }\n\n    /**\n     * Get vote selector by recommendation name\n     * @param recommendationName Name of recommendation\n     * @param option Option can be \"useful/not-useful\"\n     */\n    async getVoteSelectorByName(recommendationName: string, option: string): Promise<Selector> {\n        const recomSelector = await this.getRecommendationSelectorByName(recommendationName);\n\n        return recomSelector.find(`[data-testid='${option}-vote-btn']`);\n    }\n\n    /**\n     * Vote for recommendation by name and option\n     * @param recommendationName Name of recommendation\n     * @param option Option can be \"useful/not-useful\"\n     */\n    async voteForRecommendation(recommendationName: string, option: string): Promise<void> {\n        const voteSelector = await this.getVoteSelectorByName(recommendationName, option);\n\n        await t.click(voteSelector);\n    }\n\n    /**\n     * Verify that vote is selected by recommendation name and option\n     * @param recommendationName Name of recommendation\n     * @param option Option can be \"useful/not-useful\"\n     */\n    async verifyVoteIsSelected(recommendationName: string, option: string): Promise<void> {\n        const voteSelector = await this.getVoteSelectorByName(recommendationName, option);\n\n        await t.expect(voteSelector.getAttribute('class')).contains('selected', `${option} vote button for ${recommendationName} recommendation is not selected`);\n    }\n\n    /**\n     * Verify that vote popup is displayed by recommendation name and option\n     * @param recommendationName Name of recommendation\n     * @param option Option can be \"useful/not-useful\"\n     */\n    async verifyVotePopUpIsDisplayed(recommendationName: string, option: string): Promise<void> {\n        const popoverSelector = Selector(`[data-testid='${recommendationName}-${option}-popover']`);\n        await t.expect(popoverSelector.visible).ok(`popover is displayed for ${recommendationName}`);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/common-actions/workbench-actions.ts",
    "content": "import { t, Selector } from 'testcafe';\nimport { WorkbenchPage } from '../pageObjects';\n\nconst workbenchPage = new WorkbenchPage();\n\nexport class WorkbenchActions {\n    /**\n        Verify after client list command columns are visible\n        @param columns List of columns to verify\n    */\n    async verifyClientListColumnsAreVisible(columns: string[]): Promise<void> {\n        await t.switchToIframe(workbenchPage.iframe);\n        for (const column of columns) {\n            const columnSelector = Selector(`[data-test-subj^=tableHeaderCell_${column}_]`);\n            await t.expect(columnSelector.visible).ok(`${column} column is not visible`);\n        }\n        await t.switchToMainWindow();\n    }\n    /**\n        Verify after `client list` command table rows are expected count\n    */\n    async verifyClientListTableViewRowCount(): Promise<void>{\n        await t.click(workbenchPage.selectViewType)\n            .click(workbenchPage.viewTypeOptionsText);\n        // get number of rows from text view\n        const numberOfRowsInTextView = await Selector('[data-testid^=row-]').count - 1;\n        // select client list table view\n        await t.click(workbenchPage.selectViewType)\n            .click(workbenchPage.viewTypeOptionClientList);\n        await t.switchToIframe(workbenchPage.iframe);\n        const paginationSelector = Selector('a[data-test-subj=pagination-button-next]');\n        const tableRow =  Selector('tbody tr');\n        let rowCount = await tableRow.count;\n        if(await paginationSelector.visible) {\n            await t.click(paginationSelector);\n            rowCount += await tableRow.count;\n        }\n        await t.expect(rowCount).eql(numberOfRowsInTextView);\n    }\n    /**\n        Verify error message after `client list` command if there is no permission to run\n     */\n    async verifyClientListErrorMessage(): Promise<void>{\n        await t.switchToIframe(workbenchPage.iframe);\n        await t.expect(Selector('div').withText('NOPERM this user has no permissions to run the \\'client\\' command or its subcommand').visible)\n            .ok('NOPERM error message is not displayed');\n        await t.switchToMainWindow();\n    }\n}\n"
  },
  {
    "path": "tests/e2e/desktop.runner.ci.ts",
    "content": "import testcafe from 'testcafe';\n\n(async(): Promise<void> => {\n    await testcafe('localhost')\n        .then(t => {\n            return t\n                .createRunner()\n                .compilerOptions({\n                    'typescript': {\n                        configPath: 'tsconfig.testcafe.json',\n                        experimentalDecorators: true\n                    } })\n                .src((process.env.TEST_FILES || 'tests/electron/**/*.e2e.ts').split('\\n'))\n                .browsers(['electron'])\n                .screenshots({\n                    path: './report/screenshots/',\n                    takeOnFails: true,\n                    pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png'\n                })\n                .reporter([\n                    'spec',\n                    {\n                        name: 'xunit',\n                        output: './results/results.xml'\n                    },\n                    {\n                        name: 'json',\n                        output: './results/e2e.results.json'\n                    },\n                    {\n                        name: 'html',\n                        output: './report/index.html'\n                    }\n                ])\n                .run({\n                    skipJsErrors: true,\n                    browserInitTimeout: 120000,\n                    selectorTimeout: 10000,\n                    assertionTimeout: 10000,\n                    speed: 1,\n                    quarantineMode: { successThreshold: 1, attemptLimit: 3 },\n                    pageRequestTimeout: 20000,\n                    disableMultipleWindows: true\n                });\n        })\n        .then((failedCount) => {\n            process.exit(failedCount);\n        })\n        .catch((e) => {\n            console.error(e);\n            process.exit(1);\n        });\n})();\n"
  },
  {
    "path": "tests/e2e/desktop.runner.ts",
    "content": "import testcafe from 'testcafe';\n\n(async(): Promise<void> => {\n    await testcafe('localhost')\n        .then(t => {\n            return t\n                .createRunner()\n                .compilerOptions({\n                    'typescript': {\n                        configPath: 'tsconfig.testcafe.json',\n                        experimentalDecorators: true\n                    } })\n                .src((process.env.TEST_FILES || 'tests/electron/**/*.e2e.ts').split('\\n'))\n                .browsers(['electron'])\n                .screenshots({\n                    path: './report/screenshots/',\n                    takeOnFails: true,\n                    pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png'\n                })\n                .reporter([\n                    'spec',\n                    {\n                        name: 'xunit',\n                        output: './results/results.xml'\n                    },\n                    {\n                        name: 'json',\n                        output: './results/e2e.results.json'\n                    },\n                    {\n                        name: 'html',\n                        output: './report/index.html'\n                    }\n                ])\n                .run({\n                    skipJsErrors: true,\n                    browserInitTimeout: 60000,\n                    selectorTimeout: 5000,\n                    assertionTimeout: 5000,\n                    speed: 1,\n                    pageRequestTimeout: 8000,\n                    disableMultipleWindows: true\n                });\n        })\n        .then((failedCount) => {\n            process.exit(failedCount);\n        })\n        .catch((e) => {\n            console.error(e);\n            process.exit(1);\n        });\n})();\n"
  },
  {
    "path": "tests/e2e/desktop.runner.win.ts",
    "content": "import testcafe from 'testcafe';\n\n(async(): Promise<void> => {\n    await testcafe('localhost')\n        .then(t => {\n            return t\n                .createRunner()\n                .compilerOptions({\n                    'typescript': {\n                        configPath: 'tsconfig.testcafe.json',\n                        experimentalDecorators: true\n                    } })\n                .src((process.env.TEST_FILES || 'tests/electron/**/*.e2e.ts').split('\\n'))\n                .browsers(['electron'])\n                .screenshots({\n                    path: 'report/screenshots/',\n                    takeOnFails: true,\n                    pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png'\n                })\n                .reporter([\n                    'spec',\n                    {\n                        name: 'xunit',\n                        output: './results/results.xml'\n                    },\n                    {\n                        name: 'json',\n                        output: './results/e2e.results.json'\n                    },\n                    {\n                        name: 'html',\n                        output: './report/index.html'\n                    }\n                ])\n                .run({\n                    skipJsErrors: true,\n                    browserInitTimeout: 60000,\n                    selectorTimeout: 5000,\n                    assertionTimeout: 5000,\n                    speed: 1,\n                    quarantineMode: { successThreshold: 1, attemptLimit: 3 },\n                    disableMultipleWindows: true\n                });\n        })\n        .then((failedCount) => {\n            process.exit(failedCount);\n        })\n        .catch((e) => {\n            console.error(e);\n            process.exit(1);\n        });\n})();\n"
  },
  {
    "path": "tests/e2e/docker.web.docker-compose.yml",
    "content": "version: \"3.4\"\n\nservices:\n  e2e:\n    profiles:\n      - e2e\n    shm_size: 2gb\n    build:\n      context: .\n      dockerfile: e2e.Dockerfile\n    tty: true\n    volumes:\n      - ${E2E_VOLUME_PATH:-.}/results:/usr/src/app/results\n      - ${E2E_VOLUME_PATH:-.}/report:/usr/src/app/report\n      - ./plugins:/usr/src/app/plugins\n      - rihomedir:/root/.redis-insight\n      - tmp:/tmp\n      - ./remote:/root/remote\n      # - ./rdi:/root/rdi\n    env_file:\n      - ./.env\n    entrypoint: [\n      './upload-custom-plugins.sh',\n    ]\n    environment:\n      TEST_FILES: $TEST_FILES\n      E2E_CLOUD_DATABASE_HOST: $E2E_CLOUD_DATABASE_HOST\n      E2E_CLOUD_DATABASE_PORT: $E2E_CLOUD_DATABASE_PORT\n      E2E_CLOUD_DATABASE_PASSWORD: $E2E_CLOUD_DATABASE_PASSWORD\n      E2E_CLOUD_DATABASE_USERNAME: $E2E_CLOUD_DATABASE_USERNAME\n      E2E_CLOUD_DATABASE_NAME: $E2E_CLOUD_DATABASE_NAME\n      E2E_CLOUD_API_ACCESS_KEY: $E2E_CLOUD_API_ACCESS_KEY\n      E2E_CLOUD_API_SECRET_KEY: $E2E_CLOUD_API_SECRET_KEY\n      REMOTE_FOLDER_PATH: \"/root/remote\"\n    command: [\n      './wait-for-it.sh', \"${E2E_SLOWEST_SERVICE:-oss-standalone:6379}\", '-s', '-t', '240',\n      '--',\n      'npm', 'run', 'test:chrome:ci'\n    ]\n\n  # Built image\n  app:\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n    logging:\n      driver: none\n    image: redisinsight:amd64\n    env_file:\n      - ./.env\n    environment:\n      RI_ENCRYPTION_KEY: $E2E_RI_ENCRYPTION_KEY\n      RI_SERVER_TLS_CERT: $RI_SERVER_TLS_CERT\n      RI_SERVER_TLS_KEY: $RI_SERVER_TLS_KEY\n      RI_STDOUT_LOGGER: 'false'\n    volumes:\n      - rihomedir:/data\n      - tmp:/tmp\n      - ./test-data:/test-data\n    ports:\n      - 5540:5540\n\nvolumes:\n  tmp:\n  rihomedir:\n"
  },
  {
    "path": "tests/e2e/e2e.Dockerfile",
    "content": "FROM testcafe/testcafe\n\nUSER root\n\nWORKDIR /usr/src/app\n\nRUN apk add --no-cache bash curl\n\nCOPY package.json yarn.lock ./\n\nRUN npx yarn\n\nCOPY . .\n\nRUN chmod +x wait-for-it.sh\nRUN chmod +x upload-custom-plugins.sh\n\nENTRYPOINT [\"npx\", \"yarn\", \"test:chrome:ci\"]\n"
  },
  {
    "path": "tests/e2e/helpers/api/api-common.ts",
    "content": "import request from 'supertest';\nimport { Common } from '../common';\n\nconst endpoint = Common.getEndpoint();\nconst jsonType = 'application/json';\n\n/**\n * Send GET request using API\n * @param resourcePath URI path segment\n */\nexport async function sendGetRequest(resourcePath: string): Promise<any> {\n    const windowId = Common.getWindowId();\n    let requestEndpoint: any;\n\n    requestEndpoint = request(endpoint)\n        .get(resourcePath)\n        .set('Accept', jsonType);\n    if (await windowId) {\n        requestEndpoint.set('X-Window-Id', await windowId);\n    }\n\n    return requestEndpoint;\n}\n\n/**\n * Send POST request using API\n * @param resourcePath URI path segment\n * @param body Request body\n */\nexport async function sendPostRequest(\n    resourcePath: string,\n    body?: Record<string, unknown>\n): Promise<any> {\n    const windowId = Common.getWindowId();\n    let requestEndpoint: any;\n\n    requestEndpoint = request(endpoint)\n        .post(resourcePath)\n        .send(body)\n        .set('Accept', jsonType);\n\n    if (await windowId) {\n        requestEndpoint.set('X-Window-Id', await windowId);\n    }\n    return requestEndpoint;\n}\n\n/**\n * Send DELETE request using API\n * @param resourcePath URI path segment\n * @param body Request body\n */\nexport async function sendDeleteRequest(\n    resourcePath: string,\n    body?: Record<string, unknown>\n): Promise<any> {\n    const windowId = Common.getWindowId();\n    let requestEndpoint: any;\n\n    requestEndpoint = request(endpoint)\n        .delete(resourcePath)\n        .send(body)\n        .set('Accept', jsonType);\n\n    if (await windowId) {\n        requestEndpoint.set('X-Window-Id', await windowId);\n    }\n\n    return requestEndpoint;\n}\n"
  },
  {
    "path": "tests/e2e/helpers/api/api-database.ts",
    "content": "import { t } from 'testcafe';\nimport { Chance } from 'chance';\nimport { asyncFilter, doAsyncStuff } from '../async-helper';\nimport {\n    AddNewDatabaseParameters,\n    OSSClusterParameters,\n    databaseParameters,\n    SentinelParameters,\n    ClusterNodes\n} from '../../pageObjects/dialogs/add-redis-database-dialog';\nimport { ResourcePath } from '../constants';\nimport {\n    sendGetRequest,\n    sendPostRequest,\n    sendDeleteRequest\n} from './api-common';\n\nconst chance = new Chance();\n\nexport class DatabaseAPIRequests {\n    /**\n     * Add a new Standalone database through api using host and port\n     * @param databaseParameters The database parameters\n     */\n    async addNewStandaloneDatabaseApi(\n        databaseParameters: AddNewDatabaseParameters, isCloud = false\n    ): Promise<void> {\n        const uniqueId = chance.string({ length: 10 });\n        const uniqueIdNumber = chance.integer({ min: 1, max: 1000 });\n        const requestBody: {\n            name?: string,\n            host: string,\n            port: number,\n            username?: string,\n            password?: string,\n            tls?: boolean,\n            verifyServerCert?: boolean,\n            caCert?: {\n                name: string,\n                certificate?: string\n            },\n            clientCert?: {\n                name: string,\n                certificate?: string,\n                key?: string\n            },\n            cloudDetails?: {\n                cloudId: number,\n                subscriptionType: string,\n                planMemoryLimit: number,\n                memoryLimitMeasurementUnit: string,\n                free: boolean\n            }\n        } = {\n            name: databaseParameters.databaseName,\n            host: databaseParameters.host,\n            port: Number(databaseParameters.port),\n            username: databaseParameters.databaseUsername,\n            password: databaseParameters.databasePassword\n        };\n\n        if (databaseParameters.caCert) {\n            requestBody.tls = true;\n            requestBody.verifyServerCert = false;\n            requestBody.caCert = {\n                name: `ca}-${uniqueId}`,\n                certificate: databaseParameters.caCert.certificate\n            };\n            requestBody.clientCert = {\n                name: `client}-${uniqueId}`,\n                certificate: databaseParameters.clientCert!.certificate,\n                key: databaseParameters.clientCert!.key\n            };\n        }\n\n        if(isCloud) {\n            requestBody.cloudDetails = {\n                cloudId: uniqueIdNumber,\n                subscriptionType: 'fixed',\n                planMemoryLimit: 30,\n                memoryLimitMeasurementUnit: 'mb',\n                free: true\n            };\n        }\n        const response = await sendPostRequest(\n            ResourcePath.Databases,\n            requestBody\n        );\n        await t\n            .expect(await response.body.name)\n            .eql(\n                databaseParameters.databaseName,\n                `Database Name is not equal to ${databaseParameters.databaseName} in response`\n            );\n        await t.expect(await response.status).eql(201);\n    }\n\n    /**\n     * Add a new Standalone databases through api using host and port\n     * @param databasesParameters The databases parameters array\n     */\n    async addNewStandaloneDatabasesApi(\n        databasesParameters: AddNewDatabaseParameters[]\n    ): Promise<void> {\n        if (databasesParameters.length) {\n            databasesParameters.forEach(async(parameter) => {\n                await this.addNewStandaloneDatabaseApi(parameter);\n            });\n        }\n    }\n\n    /**\n     * Add a new database from OSS Cluster through api using host and port\n     * @param databaseParameters The database parameters\n     */\n    async addNewOSSClusterDatabaseApi(\n        databaseParameters: OSSClusterParameters\n    ): Promise<void> {\n        const requestBody = {\n            name: databaseParameters.ossClusterDatabaseName,\n            host: databaseParameters.ossClusterHost,\n            port: Number(databaseParameters.ossClusterPort)\n        };\n        const response = await sendPostRequest(\n            ResourcePath.Databases,\n            requestBody\n        );\n        await t\n            .expect(await response.body.name)\n            .eql(\n                databaseParameters.ossClusterDatabaseName,\n                `Database Name is not equal to ${databaseParameters.ossClusterDatabaseName} in response`\n            );\n        await t.expect(await response.status).eql(201);\n    }\n\n    /**\n     * Add a Sentinel database via autodiscover through api\n     * @param databaseParameters The database parameters\n     * @param primaryGroupsNumber Number of added primary groups\n     */\n    async discoverSentinelDatabaseApi(\n        databaseParameters: SentinelParameters,\n        primaryGroupsNumber?: number\n    ): Promise<void> {\n        let masters = databaseParameters.masters;\n        if (primaryGroupsNumber) {\n            masters = databaseParameters.masters!.slice(0, primaryGroupsNumber);\n        }\n        const requestBody = {\n            host: databaseParameters.sentinelHost,\n            port: Number(databaseParameters.sentinelPort),\n            password: databaseParameters.sentinelPassword,\n            masters: masters\n        };\n        const resourcePath =\n            ResourcePath.RedisSentinel + ResourcePath.Databases;\n        const response = await sendPostRequest(resourcePath, requestBody);\n\n        await t.expect(await response.status).eql(201);\n    }\n\n    /**\n     * Get all databases through api\n     */\n    async getAllDatabases(): Promise<string[]> {\n        const response = await sendGetRequest(ResourcePath.Databases);\n\n        await t.expect(await response.status).eql(200);\n        return await response.body;\n    }\n\n    /**\n     * Get database through api using database name\n     * @param databaseName The database name\n     */\n    async getDatabaseIdByName(databaseName?: string): Promise<string> {\n        if (!databaseName) {\n            throw new Error('Error: Missing databaseName');\n        }\n        let databaseId;\n        const allDataBases = await this.getAllDatabases();\n        const response = await asyncFilter(\n            allDataBases,\n            async(item: databaseParameters) => {\n                await doAsyncStuff();\n                return item.name === databaseName;\n            }\n        );\n\n        if (response.length !== 0) {\n            databaseId = await response[0].id;\n        }\n        return databaseId;\n    }\n\n    /**\n     * Get database through api using database connection type\n     * @param connectionType The database connection type\n     */\n    async getDatabaseByConnectionType(\n        connectionType?: string\n    ): Promise<string> {\n        if (!connectionType) {\n            throw new Error('Error: Missing connectionType');\n        }\n        const allDataBases = await this.getAllDatabases();\n        let response: databaseParameters[] = [];\n        response = await asyncFilter(\n            allDataBases,\n            async(item: databaseParameters) => {\n                await doAsyncStuff();\n                return item.connectionType === connectionType;\n            }\n        );\n        return response?.[0]?.id;\n    }\n\n    /**\n     * Delete all databases through api\n     */\n    async deleteAllDatabasesApi(): Promise<void> {\n        const allDatabases = await this.getAllDatabases();\n        console.log(`common db count is \"${allDatabases}\"`);\n        if (allDatabases.length > 0) {\n            const databaseIds: string[] = [];\n            for (let i = 0; i < allDatabases.length; i++) {\n                const dbData = JSON.parse(JSON.stringify(allDatabases[i]));\n                databaseIds.push(dbData.id);\n            }\n            if (databaseIds.length > 0) {\n                const requestBody = { ids: databaseIds };\n                const response = await sendDeleteRequest(\n                    ResourcePath.Databases,\n                    requestBody\n                );\n                await t.expect(await response.status).eql(200);\n            }\n            await this.deleteAllDatabasesByConnectionTypeApi('SENTINEL');\n        }\n    }\n\n    /**\n     * Delete Standalone database through api\n     * @param databaseParameters The database parameters\n     */\n    async deleteStandaloneDatabaseApi(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await this.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n        if (databaseId) {\n            const requestBody = { ids: [`${databaseId}`] };\n            const response = await sendDeleteRequest(\n                ResourcePath.Databases,\n                requestBody\n            );\n            await t.expect(await response.status).eql(200);\n        }\n        else {\n            throw new Error('Error: Missing databaseId');\n        }\n    }\n\n    /**\n     * Delete Standalone databases using their names through api\n     * @param databaseNames Databases names\n     */\n    async deleteStandaloneDatabasesByNamesApi(\n        databaseNames: string[]\n    ): Promise<void> {\n        databaseNames.forEach(async(databaseName) => {\n            const databaseId = await this.getDatabaseIdByName(databaseName);\n            if (databaseId) {\n                const requestBody = { ids: [`${databaseId}`] };\n                const response = await sendDeleteRequest(\n                    ResourcePath.Databases,\n                    requestBody\n                );\n                await t.expect(await response.status).eql(200);\n            }\n            else {\n                throw new Error('Error: Missing databaseId');\n            }\n        });\n    }\n\n    /**\n     * Delete database from OSS Cluster through api\n     * @param databaseParameters The database parameters\n     */\n    async deleteOSSClusterDatabaseApi(\n        databaseParameters: OSSClusterParameters\n    ): Promise<void> {\n        const databaseId = await this.getDatabaseIdByName(\n            databaseParameters.ossClusterDatabaseName\n        );\n        const requestBody = { ids: [`${databaseId}`] };\n        const response = await sendDeleteRequest(\n            ResourcePath.Databases,\n            requestBody\n        );\n        await t.expect(await response.status).eql(200);\n    }\n\n    /**\n     * Delete all primary groups from Sentinel through api\n     * @param databaseParameters The database parameters\n     */\n    async deleteAllSentinelDatabasesApi(\n        databaseParameters: SentinelParameters\n    ): Promise<void> {\n        for (let i = 0; i < databaseParameters.name!.length; i++) {\n            const databaseId = await this.getDatabaseIdByName(\n                databaseParameters.name![i]\n            );\n            const requestBody = { ids: [`${databaseId}`] };\n            const response = await sendDeleteRequest(\n                ResourcePath.Databases,\n                requestBody\n            );\n            await t.expect(await response.status).eql(200);\n        }\n    }\n\n    /**\n     * Delete all databases by connection type\n     */\n    async deleteAllDatabasesByConnectionTypeApi(\n        connectionType: string\n    ): Promise<void> {\n        const databaseIds = await this.getDatabaseByConnectionType(\n            connectionType\n        );\n        if(databaseIds?.length > 0) {\n            const requestBody = { ids: [`${databaseIds}`] };\n            const response = await sendDeleteRequest(\n                ResourcePath.Databases,\n                requestBody\n            );\n            await t.expect(await response.status).eql(200);\n        }\n    }\n\n    /**\n     * Delete Standalone databases through api\n     * @param databasesParameters The databases parameters as array\n     */\n    async deleteStandaloneDatabasesApi(\n        databasesParameters: AddNewDatabaseParameters[]\n    ): Promise<void> {\n        if (databasesParameters.length) {\n            databasesParameters.forEach(async(parameter) => {\n                await this.deleteStandaloneDatabaseApi(parameter);\n            });\n        }\n    }\n\n    /**\n     * Get OSS Cluster nodes\n     * @param databaseParameters The database parameters\n     */\n    async getClusterNodesApi(\n        databaseParameters: OSSClusterParameters\n    ): Promise<string[]> {\n        const databaseId = await this.getDatabaseIdByName(\n            databaseParameters.ossClusterDatabaseName\n        );\n        const resourcePath =\n            `${ResourcePath.Databases  }/${databaseId}${  ResourcePath.ClusterDetails}`;\n        const response = await sendGetRequest(resourcePath);\n\n        await t.expect(await response.status).eql(200);\n\n        const nodes = await response.body.nodes;\n        const nodeNames = await nodes.map(\n            (node: ClusterNodes) => `${node.host}:${node.port}`\n        );\n        return nodeNames;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/api/api-info.ts",
    "content": "import { t } from 'testcafe';\nimport { ResourcePath } from '../constants';\nimport { sendPostRequest } from './api-common';\n\n/**\n * Synchronize features\n */\nexport async function syncFeaturesApi(): Promise<void> {\n    const response = await sendPostRequest(ResourcePath.SyncFeatures);\n\n    await t.expect(await response.status).eql(200);\n}\n"
  },
  {
    "path": "tests/e2e/helpers/api/api-keys.ts",
    "content": "import { t } from 'testcafe';\nimport { AddNewDatabaseParameters } from '../../pageObjects/dialogs/add-redis-database-dialog';\nimport {\n    HashKeyParameters,\n    StringKeyParameters,\n    ListKeyParameters,\n    SetKeyParameters,\n    SortedSetKeyParameters,\n    StreamKeyParameters,\n    JsonKeyParameters,\n} from '../../pageObjects/browser-page'\nimport { sendDeleteRequest, sendPostRequest } from './api-common';\nimport { DatabaseAPIRequests } from './api-database';\n\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst bufferPathMask = '/databases/databaseId/keys?encoding=buffer';\nexport class APIKeyRequests {\n\n    /**\n     * Add Hash key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addHashKeyApi(\n        keyParameters: HashKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: Buffer.from(keyParameters.keyName, 'utf-8'),\n            expire: keyParameters.ttl,\n            fields: keyParameters.fields\n                .map((fields) => ({ ...fields,\n                    field: Buffer.from(fields.field, 'utf-8'),\n                    value: Buffer.from(fields.value, 'utf-8') }))\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/hash?encoding=buffer`,\n            requestBody\n        );\n\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new Hash key request failed');\n    }\n\n    /**\n     * Add Stream key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addStreamKeyApi(\n        keyParameters: StreamKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: Buffer.from(keyParameters.keyName, 'utf-8'),\n            expire: keyParameters.ttl,\n            entries: keyParameters.entries\n                .map((member) =>\n                    ({\n                        ...member,\n                        fields: member.fields.map(({ name, value }) => ({\n                            name: Buffer.from(name, 'utf-8'),\n                            value: Buffer.from(value, 'utf-8')\n                        }))\n                    }))\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/streams?encoding=buffer`,\n            requestBody\n        );\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new Stream key request failed');\n    }\n\n    /**\n     * Add Set key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addSetKeyApi(\n        keyParameters: SetKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: Buffer.from(keyParameters.keyName, 'utf-8'),\n            expire: keyParameters.ttl,\n            members: keyParameters.members\n                .map((member) => (Buffer.from(member, 'utf-8')))\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/set?encoding=buffer`,\n            requestBody\n        );\n\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new Set key request failed');\n    }\n\n    /**\n     * Add Sorted Set key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addSortedSetKeyApi(\n        keyParameters: SortedSetKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: Buffer.from(keyParameters.keyName, 'utf-8'),\n            expire: keyParameters.ttl,\n            members: keyParameters.members\n                .map((member) => ({ ...member, name: Buffer.from(member.name, 'utf-8') }))\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/zSet?encoding=buffer`,\n            requestBody\n        );\n\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new Sorted Set key request failed');\n    }\n\n    /**\n     * Add List key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addListKeyApi(\n        keyParameters: ListKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: Buffer.from(keyParameters.keyName, 'utf-8'),\n            expire: keyParameters.ttl,\n            elements: keyParameters.elements\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/list?encoding=buffer`,\n            requestBody\n        );\n\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new List key request failed');\n    }\n\n    /**\n     * Add String key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addStringKeyApi(\n        keyParameters: StringKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: Buffer.from(keyParameters.keyName, 'utf-8'),\n            expire: keyParameters.ttl,\n            value: Buffer.from(keyParameters.value, 'utf-8')\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/string?encoding=buffer`,\n            requestBody\n        );\n\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new string key request failed');\n    }\n\n    /**\n     * Add Json key\n     * @param keyParameters The key parameters\n     * @param databaseParameters The database parameters\n     */\n    async addJsonKeyApi(\n        keyParameters: JsonKeyParameters,\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        // ensure key doesn't exist\n        await this.deleteKeyApi(keyParameters.keyName, databaseId);\n\n        const requestBody = {\n            keyName: keyParameters.keyName,\n            expire: keyParameters.ttl,\n            data: JSON.stringify(keyParameters.data)\n        };\n        const response = await sendPostRequest(\n            `/databases/${databaseId}/rejson-rl`,\n            requestBody\n        );\n\n        await t\n            .expect(response.status)\n            .eql(201, 'The creation of new json key request failed');\n    }\n\n    /**\n     * Search Key by name\n     * @param keyName The key name\n     * @param databaseName The database name\n     */\n    async searchKeyByNameApi(\n        keyName: string,\n        databaseName: string\n    ): Promise<string[]> {\n        const requestBody = {\n            cursor: '0',\n            match: keyName\n        };\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseName\n        );\n        const response = await sendPostRequest(\n            bufferPathMask.replace('databaseId', databaseId),\n            requestBody\n        );\n        await t.expect(response.status).eql(200, 'Getting key request failed');\n        return await response.body[0].keys;\n    }\n\n    /**\n     * Delete Key by name if it exists\n     * @param keyName The key name\n     * @param databaseName The database name\n     */\n    async deleteKeyByNameApi(\n        keyName: string,\n        databaseName: string\n    ): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseName\n        );\n        const isKeyExist = await this.searchKeyByNameApi(keyName, databaseName);\n        if (isKeyExist.length > 0) {\n            const requestBody = { keyNames: [Buffer.from(keyName, 'utf-8')] };\n            const response = await sendDeleteRequest(\n                bufferPathMask.replace('databaseId', databaseId),\n                requestBody\n            );\n            await t\n                .expect(response.status)\n                .eql(200, 'The deletion of the key request failed');\n        }\n    }\n\n    /**\n     * Delete Key by name\n     * @param keyName The key name\n     * @param databaseId The database id\n     */\n    async deleteKeyApi(\n        keyName: string,\n        databaseId: string,\n    ): Promise<void> {\n        try {\n            await sendDeleteRequest(\n                `/databases/${databaseId}/keys`,\n                {\n                    keyNames: [keyName],\n                },\n            )\n        } catch (e) {\n            // ignore errors\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/api/api-rdi.ts",
    "content": "import { t } from 'testcafe';\nimport { ResourcePath } from '../constants';\nimport { sendDeleteRequest, sendGetRequest, sendPostRequest } from './api-common';\n\nexport class RdiApiRequests {\n\n    /**\n     * Delete all rdi through api\n     */\n    async deleteAllRdiApi(): Promise<void> {\n        const allRdi = await this.getAllRdi();\n        console.log(`common db count is \"${allRdi}\"`);\n        if (allRdi.length > 0) {\n            const rdiIds: string[] = [];\n            for (let i = 0; i < allRdi.length; i++) {\n                const dbData = JSON.parse(JSON.stringify(allRdi[i]));\n                rdiIds.push(dbData.id);\n            }\n            if (rdiIds.length > 0) {\n                const requestBody = { ids: rdiIds };\n                const response = await sendDeleteRequest(\n                    ResourcePath.Rdi,\n                    requestBody\n                );\n                await t.expect(await response.status).eql(200);\n            }\n        }\n    }\n\n    /**\n     * Get all rdi instances through api\n     */\n    async getAllRdi(): Promise<string[]> {\n        const response = await sendGetRequest(ResourcePath.Rdi);\n\n        await t.expect(await response.status).eql(200);\n        return await response.body;\n    }\n\n    /**\n     * Add a new rdi through api using host and port\n     * @param rdiParameters The rdi parameters\n     */\n    async addNewRdiApi(\n        rdiParameters: AddNewRdiParameters\n    ): Promise<void> {\n\n        const response = await sendPostRequest(\n            ResourcePath.Rdi,\n            rdiParameters\n        );\n        await t\n            .expect(await response.body.name)\n            .eql(\n                rdiParameters.name,\n                `rdi Name is not equal to ${rdiParameters.name} in response`\n            );\n        await t.expect(await response.status).eql(201);\n    }\n}\n/**\n * Add new database parameters\n * @param username The username for rdi\n * @param name rdi name(alias)\n * @param password The password for rdi\n * @param url The url of the rdi\n */\nexport type AddNewRdiParameters = {\n    username: string,\n    name: string,\n    password: string,\n    url: string\n};\n"
  },
  {
    "path": "tests/e2e/helpers/async-helper.ts",
    "content": "/**\n * Helper function to work with arr.filter() method with async functions\n * @param array The array\n * @param callback The callback function need to be processed\n */\nasync function asyncFilter(array: string[], callback: (item: any) => Promise<boolean>): Promise<any[]> {\n    const fail = Symbol();\n    return (await Promise.all(array.map(async item => (await callback(item)) ? item : fail))).filter(i => i !== fail);\n}\n\n/**\n * Helper function to work with arr.find() method with async functions\n * @param array The array\n * @param asyncCallback The callback function need to be processed\n */\nasync function asyncFind(array: string[], asyncCallback: (item: any) => Promise<boolean>): Promise<string> {\n    const index = (await Promise.all(array.map(asyncCallback))).findIndex(result => result);\n    return array[index];\n}\n\n/**\n * Helper function for waiting until promise be resolved\n */\nfunction doAsyncStuff(): Promise<void> {\n    return Promise.resolve();\n}\n\nexport { asyncFilter, asyncFind, doAsyncStuff };"
  },
  {
    "path": "tests/e2e/helpers/common.ts",
    "content": "import * as path from 'path';\nimport * as fs from 'fs';\nimport * as fsp from 'fs/promises';\n\nimport { ClientFunction, RequestMock, t } from 'testcafe';\nimport { Chance } from 'chance';\nimport { apiUrl } from './conf';\nconst archiver = require('archiver');\n\nconst chance = new Chance();\n\ndeclare global {\n    interface Window {\n        windowId?: string\n    }\n}\n\nconst settingsApiUrl = `${apiUrl}/settings`;\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]\nconst mockedSettingsResponse = {\n    \"theme\": null,\n    \"dateFormat\": null,\n    \"timezone\": null,\n    \"scanThreshold\": 10000,\n    \"batchSize\": 5,\n    \"agreements\": null\n};\n\nexport class Common {\n    static mockSettingsResponse(): RequestMock {\n        return RequestMock()\n            .onRequestTo(settingsApiUrl)\n            .respond(mockedSettingsResponse, 200, {\n                'Access-Control-Allow-Origin': '*',\n                'Access-Control-Allow-Credentials': 'true',\n                'Access-Control-Allow-Headers': 'x-window-id'\n            });\n    }\n\n    static async waitForElementNotVisible(elm: Selector): Promise<void> {\n        try {\n            await t.expect(elm.exists).notOk({ timeout: 15000 }); // Increased from 10000 to 15000\n        } catch (error) {\n            // Element still exists, try to wait for it to become invisible instead\n            try {\n                await t.expect(elm.visible).notOk({ timeout: 15000 });\n            } catch {\n                // Log warning but don't fail the test - element might be legitimately persistent\n                console.warn('Element still visible after timeout, but continuing test execution');\n            }\n        }\n    }\n\n    /**\n     * Create array of keys\n     * @param length The amount of array elements\n     */\n    static createArrayWithKeys(length: number): string[] {\n        return Array.from({ length }, (_, i) => `key${i}`);\n    }\n\n    /**\n    * Create array of keys and values\n    * @param length The amount of array elements\n    */\n    static async createArrayWithKeyValue(length: number): Promise<string[]> {\n        const arr: string[] = [];\n        for (let i = 1; i <= length * 2; i++) {\n            arr[i] = `${chance.word({ length: 10 })}-key${i}`;\n            arr[i + 1] = `${chance.word({ length: 10 })}-value${i}`;\n            i++;\n        }\n        return arr;\n    }\n\n    /**\n    * Create array of keys and values\n    * @param length The amount of array elements\n    */\n    static async createArrayWithKeyValueAndDelimiter(length: number): Promise<string[]> {\n        const keyNameArray: string[] = [];\n        for (let i = 1; i <= length; i++) {\n            const key = `\"key${i}:test${i}\"`;\n            const value = `\"value${this.generateSentence(i * 2)}\"`;\n            keyNameArray.push(key, value);\n        }\n        return keyNameArray;\n    }\n\n    /**\n    * Create array of keys and values\n    * @param length The amount of array elements\n    */\n    static async createArrayWithKeyAndDelimiter(length: number): Promise<string[]> {\n        const keyNameArray: string[] = [];\n        for (let i = 1; i <= length; i++) {\n            const key = `\"key${i}:test${i}\"`;\n            keyNameArray.push(key);\n        }\n        return keyNameArray;\n    }\n\n    /**\n    * Create array of keys and values for using in OSS Cluster\n    * @param length The amount of array elements\n    */\n    static async createArrayWithKeyValueForOSSCluster(length: number): Promise<string[]> {\n        const arr: string[] = [];\n        for (let i = 1; i <= length * 2; i++) {\n            arr[i] = `{user1}:${chance.word({ length: 10 })}-key${i}`;\n            arr[i + 1] = `${chance.word({ length: 10 })}-value${i}`;\n            i++;\n        }\n        return arr;\n    }\n\n    /**\n    * Create array of keys and values with edittable counter value\n    * @param length The amount of array elements\n    * @param keyName The name of the key\n    */\n    static async createArrayWithKeyValueAndKeyname(length: number, keyName: string): Promise<string[]> {\n        const keyNameArray: string[] = [];\n        for (let i = 1; i <= length; i++) {\n            const key = `${keyName}${i}`;\n            const value = `value${i}`;\n            keyNameArray.push(key, value);\n        }\n        return keyNameArray;\n    }\n\n    /**\n     * Create array of pairs [key, value]\n     * @param length The amount of array elements\n     */\n    static createArrayPairsWithKeyValue(length: number): [string, number][] {\n        return Array.from({ length }, (_, i) => [`key${i}`, i]);\n    }\n\n    /**\n    * Create array of numbers\n    * @param length The amount of array elements\n    */\n    static async createArray(length: number): Promise<string[]> {\n        const arr: string[] = [];\n        for (let i = 1; i <= length; i++) {\n            arr[i] = `${i}`;\n        }\n        return arr;\n    }\n\n    /**\n    * Get background colour of element\n    * @param element The selector of the element\n    */\n    static async getBackgroundColour(element: Selector): Promise<string> {\n        return element.getStyleProperty('background-color');\n    }\n\n    /**\n    * Generate word by number of symbols\n    * @param number The number of symbols\n    */\n    static generateWord(number: number): string {\n        return chance.word({ length: number });\n    }\n\n    /**\n    * Generate sentence by number of words\n    * @param number The number of words\n    */\n    static generateSentence(number: number): string {\n        return chance.sentence({ words: number });\n    }\n\n    /**\n    * Return api endpoint with disabled certificate validation\n    */\n    static getEndpoint(): string {\n        return apiUrl;\n    }\n\n    /**\n    * Return windowId\n    */\n    static getWindowId(): Promise<string> {\n        return t.eval(() => window.windowId);\n    }\n\n    /**\n     * Check opened URL\n     * @param expectedUrl Expected link that is compared with actual\n     */\n    static async checkURL(expectedUrl: string): Promise<void> {\n        const getPageUrl = await this.getPageUrl();\n        await t.expect(getPageUrl).eql(expectedUrl, 'Opened URL is not correct');\n    }\n\n    /**\n     * Check opened URL contains text\n     * @param expectedText Expected link that is compared with actual\n     */\n    static async checkURLContainsText(expectedText: string): Promise<void> {\n        const getPageUrl = await this.getPageUrl();\n        await t.expect(getPageUrl).contains(expectedText, `Opened URL not contains text ${expectedText}`);\n    }\n\n    /**\n     * Replace spaces and line breaks\n     * @param text text to be replaced\n     */\n    static async removeEmptySpacesAndBreak(text: string): Promise<string> {\n        return text\n            .replace(/ /g, '')\n            .replace(/\\n/g, '');\n    }\n\n    /**\n     * Get current page url\n     */\n    static async getPageUrl(): Promise<string> {\n        return (ClientFunction(() => window.location.href))();\n    }\n\n    /**\n     * generate url base on params to create DB\n     * @param params params for creating DB\n     */\n    static generateUrlTParams(params: Record<string, any>): string {\n        return new URLSearchParams(params).toString();\n    }\n\n    /**\n     * Get json property value by property name and path\n     * @param expectedText Expected link that is compared with actual\n     */\n    static async getJsonPropertyValue(property: string, path: string): Promise<string | number> {\n        const parsedJson = JSON.parse(fs.readFileSync(path, 'utf-8'));\n        return parsedJson[property];\n    }\n\n    /**\n     * Create Zip archive from folder\n     * @param folderPath Path to folder to archive\n     * @param zipName Zip archive name\n     */\n    static async createZipFromFolder(folderPath: string, zipName: string): Promise<void> {\n        const sourceDir = path.join(__dirname, folderPath);\n        const zipFilePath = path.join(__dirname, zipName);\n        const output = fs.createWriteStream(zipFilePath);\n        const archive = archiver('zip', { zlib: { level: 9 } });\n\n        // Add the contents of the directory to the zip archive\n        archive.directory(sourceDir, false);\n        // Finalize the archive and write it to disk\n        await archive.finalize();\n        archive.pipe(output);\n    }\n\n    /**\n      * Delete file from folder\n      * @param filePath Path to file\n     */\n    static async deleteFileFromFolder(filePath: string): Promise<void> {\n        fs.unlinkSync(path.join(__dirname, filePath));\n    }\n\n    /**\n      * Delete file from folder if exists\n      * @param filePath Path to file\n     */\n    static async deleteFileFromFolderIfExists(filePath: string): Promise<void> {\n        if (fs.existsSync(filePath)) {\n            fs.unlinkSync(filePath);\n        }\n    }\n\n    /**\n     * Delete folder\n     * @param filePath Path to file\n     */\n    static async deleteFolderIfExists(filePath: string): Promise<void> {\n        try {\n            await fsp.rm(filePath, { recursive: true, force: true });\n            console.log(`Directory Deleted: ${filePath}`);\n        } catch (error) {\n            console.error(`Failed to delete directory: ${filePath}`, error);\n        }\n    }\n\n    /**\n      * Read file from folder\n      * @param filePath Path to file\n     */\n    static async readFileFromFolder(filePath: string): Promise<string> {\n        return fs.readFileSync(filePath, 'utf8');\n    }\n\n    /**\n      * Get current machine platform\n     */\n    static getPlatform(): { isMac: boolean, isLinux: boolean } {\n        return {\n            isMac: process.platform === 'darwin',\n            isLinux: process.platform === 'linux'\n        };\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/conf.ts",
    "content": "import * as os from 'os';\nimport * as fs from 'fs';\nimport { join as joinPath } from 'path';\nimport * as path from 'path';\nimport { Chance } from 'chance';\nconst chance = new Chance();\n\n// Urls for using in the tests\nexport const commonUrl = process.env.COMMON_URL || 'https://localhost:5540';\nexport const apiUrl = process.env.API_URL || 'https://localhost:5540/api';\nexport const googleUser = process.env.GOOGLE_USER || '';\nexport const googleUserPassword = process.env.GOOGLE_USER_PASSWORD || '';\nexport const samlUser = process.env.E2E_SSO_EMAIL || '';\nexport const samlUserPassword = process.env.E2E_SSO_PASSWORD || '';\n\nexport const workingDirectory = process.env.RI_APP_FOLDER_ABSOLUTE_PATH\n    || (joinPath(os.homedir(), process.env.RI_APP_FOLDER_NAME || '.redis-insight'));\nexport const fileDownloadPath = joinPath(os.homedir(), 'Downloads');\nexport const uniqueId = chance.word({ length: 10 });\n\nexport const ossStandaloneConfig = {\n    host: process.env.OSS_STANDALONE_HOST || 'oss-standalone',\n    port: process.env.OSS_STANDALONE_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'test_standalone'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_PASSWORD\n};\n\nexport const ossStandaloneConfigEmpty = {\n    host: process.env.OSS_STANDALONE_EMPTY_HOST || 'oss-standalone-empty',\n    port: process.env.OSS_STANDALONE_EMPTY_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_EMPTY_DATABASE_NAME || 'test_standalone_empty'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_EMPTY_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_EMPTY_PASSWORD\n};\n\nexport const ossStandaloneV5Config = {\n    host: process.env.OSS_STANDALONE_V5_HOST || 'oss-standalone-v5',\n    port: process.env.OSS_STANDALONE_V5_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_V5_DATABASE_NAME || 'test_standalone-v5'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_V5_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_V5_PASSWORD\n};\n\nexport const ossStandaloneV7Config = {\n    host: process.env.OSS_STANDALONE_V7_HOST || 'oss-standalone-v7',\n    port: process.env.OSS_STANDALONE_V7_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_V7_DATABASE_NAME || 'test_standalone-v7'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_V7_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_V7_PASSWORD\n};\n\nexport const ossClusterConfig = {\n    ossClusterHost: process.env.OSS_CLUSTER_HOST || 'master-plain-7-1',\n    ossClusterPort: process.env.OSS_CLUSTER_PORT || '6379',\n    ossClusterDatabaseName: `${process.env.OSS_CLUSTER_DATABASE_NAME || 'test_cluster'}-${uniqueId}`\n};\n\nexport const ossSentinelConfig = {\n    sentinelHost: process.env.OSS_SENTINEL_HOST || 'oss-sentinel',\n    sentinelPort: process.env.OSS_SENTINEL_PORT || '26379',\n    sentinelPassword: process.env.OSS_SENTINEL_PASSWORD || 'password',\n    masters: [{\n        alias: `primary-group-1}-${uniqueId}`,\n        db: '0',\n        name: 'primary-group-1',\n        password: 'defaultpass'\n    },\n    {\n        alias: `primary-group-2}-${uniqueId}`,\n        db: '0',\n        name: 'primary-group-2',\n        password: 'defaultpass'\n    }],\n    name: ['primary-group-1', 'primary-group-2']\n};\n\nexport const redisEnterpriseClusterConfig = {\n    host: process.env.RE_CLUSTER_HOST || 'redis-enterprise',\n    port: process.env.RE_CLUSTER_PORT || '9443',\n    databaseName: process.env.RE_CLUSTER_DATABASE_NAME || 'test-re-standalone',\n    databaseUsername: process.env.RE_CLUSTER_ADMIN_USER || 'demo@redislabs.com',\n    databasePassword: process.env.RE_CLUSTER_ADMIN_PASSWORD || '123456'\n};\n\nexport const invalidOssStandaloneConfig = {\n    host: 'oss-standalone-invalid',\n    port: '1010',\n    databaseName: `${process.env.OSS_STANDALONE_INVALID_DATABASE_NAME || 'test_standalone-invalid'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_INVALID_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_INVALID_PASSWORD\n};\n\nexport const ossStandaloneBigConfig = {\n    host: process.env.OSS_STANDALONE_BIG_HOST || 'oss-standalone-big',\n    port: process.env.OSS_STANDALONE_BIG_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_BIG_DATABASE_NAME || 'test_standalone_big'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_BIG_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_BIG_PASSWORD\n};\n\nexport const cloudDatabaseConfig = {\n    host: process.env.E2E_CLOUD_DATABASE_HOST || '',\n    port: process.env.E2E_CLOUD_DATABASE_PORT || '',\n    databaseName: `${process.env.E2E_CLOUD_DATABASE_NAME || 'cloud-database'}-${uniqueId}`,\n    databaseUsername: process.env.E2E_CLOUD_DATABASE_USERNAME,\n    databasePassword: process.env.E2E_CLOUD_DATABASE_PASSWORD,\n    accessKey: process.env.E2E_CLOUD_API_ACCESS_KEY || '',\n    secretKey: process.env.E2E_CLOUD_API_SECRET_KEY || ''\n};\n\nexport const ossStandaloneNoPermissionsConfig = {\n    host: process.env.OSS_STANDALONE_NOPERM_HOST || 'oss-standalone',\n    port: process.env.OSS_STANDALONE_NOPERM_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_NOPERM_DATABASE_NAME || 'oss-standalone-no-permissions'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_NOPERM_USERNAME || 'noperm',\n    databasePassword: process.env.OSS_STANDALONE_NOPERM_PASSWORD\n};\n\nexport const ossStandaloneForSSHConfig = {\n    host: process.env.OSS_STANDALONE_SSH_HOST || '172.33.100.111',\n    port: process.env.OSS_STANDALONE_SSH_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_SSH_DATABASE_NAME || 'oss-standalone-for-ssh'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_SSH_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_SSH_PASSWORD\n};\n\nexport const ossClusterForSSHConfig = {\n    host: process.env.OSS_CLUSTER_SSH_HOST || '172.31.100.211',\n    port: process.env.OSS_CLUSTER_SSH_PORT || '6379',\n    databaseName: `${process.env.OSS_CLUSTER_SSH_DATABASE_NAME || 'oss-cluster-for-ssh'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_CLUSTER_SSH_USERNAME,\n    databasePassword: process.env.OSS_CLUSTER_SSH_PASSWORD\n};\n\nexport const ossStandaloneTlsConfig = {\n    host: process.env.OSS_STANDALONE_TLS_HOST || 'oss-standalone-tls',\n    port: process.env.OSS_STANDALONE_TLS_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_TLS_DATABASE_NAME || 'test_standalone_tls'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_TLS_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_TLS_PASSWORD,\n    caCert: {\n        name: `ca}-${uniqueId}`,\n        certificate: process.env.E2E_CA_CRT || fs.readFileSync(path.resolve(__dirname, '../rte/oss-standalone-tls/certs/redisCA.crt'), 'utf-8')\n    },\n    clientCert: {\n        name: `client}-${uniqueId}`,\n        certificate: process.env.E2E_CLIENT_CRT || fs.readFileSync(path.resolve(__dirname, '../rte/oss-standalone-tls/certs/redis.crt'), 'utf-8'),\n        key: process.env.E2E_CLIENT_KEY || fs.readFileSync(path.resolve(__dirname, '../rte/oss-standalone-tls/certs/redis.key'), 'utf-8')\n    }\n};\n\n// todo: investigate if we need this. EOL\nexport const ossStandaloneRedisGears = {\n    host: process.env.OSS_STANDALONE_REDISGEARS_HOST || 'oss-standalone-redisgears-2-0',\n    port: process.env.OSS_STANDALONE_REDISGEARS_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_REDISGEARS_DATABASE_NAME || 'test_standalone_redisgears'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_REDISGEARS_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_REDISGEARS_PASSWORD\n};\n\nexport const ossClusterRedisGears = {\n    ossClusterHost: process.env.OSS_CLUSTER_REDISGEARS_2_HOST || 'gears-cluster-2-0-node-1',\n    ossClusterPort: process.env.OSS_CLUSTER_REDISGEARS_2_PORT || '6379',\n    ossClusterDatabaseName: `${process.env.OSS_CLUSTER_REDISGEARS_2_NAME || 'test_cluster-gears-2.0'}-${uniqueId}`\n};\n"
  },
  {
    "path": "tests/e2e/helpers/constants.ts",
    "content": "export enum KeyTypesTexts {\n    Hash = 'Hash',\n    List = 'List',\n    Set = 'Set',\n    ZSet = 'Sorted Set',\n    String = 'String',\n    ReJSON = 'JSON',\n    Stream = 'Stream',\n    Graph = 'Graph',\n    TimeSeries = 'Time Series',\n}\nexport const keyLength = 50;\n\nexport const COMMANDS_TO_CREATE_KEY = Object.freeze({\n    [KeyTypesTexts.Hash]: (key: string, value: string | number = 'value', field: string | number = 'field') => `HSET ${key} '${field}' '${value}'`,\n    [KeyTypesTexts.List]: (key: string, element: string | number = 'element') => `LPUSH ${key} '${element}'`,\n    [KeyTypesTexts.Set]: (key: string, member = 'member') => `SADD ${key} '${member}'`,\n    [KeyTypesTexts.ZSet]: (key: string, member = 'member', score = 1) => `ZADD ${key} ${score} '${member}'`,\n    [KeyTypesTexts.String]: (key: string, value = 'val') => `SET ${key} '${value}'`,\n    [KeyTypesTexts.ReJSON]: (key: string, json = '\"val\"') => `JSON.SET ${key} . '${json}'`,\n    [KeyTypesTexts.Stream]: (key: string, value: string | number = 'value', field: string | number = 'field') => `XADD ${key} * '${field}' '${value}'`,\n    [KeyTypesTexts.Graph]: (key: string) => `GRAPH.QUERY ${key} \"CREATE ()\"`,\n    [KeyTypesTexts.TimeSeries]: (key: string) => `TS.CREATE ${key}`\n});\n\nexport enum rte {\n    none = 'none',\n    standalone = 'standalone',\n    sentinel = 'sentinel',\n    ossCluster = 'oss-cluster',\n    reCluster = 're-cluster',\n    reCloud = 're-cloud'\n}\n\nexport enum env {\n    web = 'web',\n    desktop = 'desktop'\n}\n\nexport enum RecommendationIds {\n    redisVersion = 'redisVersion',\n    searchVisualization = 'searchVisualization',\n    setPassword = 'setPassword',\n    optimizeTimeSeries = 'RTS',\n    luaScript = 'luaScript',\n    useSmallerKeys = 'useSmallerKeys',\n    avoidLogicalDatabases = 'avoidLogicalDatabases',\n    searchJson = 'searchJSON',\n    rdi = 'tryRDI'\n}\n\nexport enum LibrariesSections {\n    Functions = 'Functions',\n    KeyspaceTriggers = 'Keyspace',\n    ClusterFunctions = 'Cluster',\n    StreamFunctions= 'Stream',\n}\n\nexport enum FunctionsSections {\n    General = 'General',\n    Flag = 'Flag',\n}\n\nexport enum MonacoEditorInputs {\n    //add library fields\n    Code = 'code-value',\n    Configuration = 'configuration-value',\n    // added library fields\n    Library = 'library-code',\n    LibraryConfiguration = 'library-configuration',\n}\n\nexport enum ResourcePath {\n    Databases = '/databases',\n    RedisSentinel = '/redis-sentinel',\n    ClusterDetails = '/cluster-details',\n    SyncFeatures = '/features/sync',\n    Rdi = '/rdi'\n}\n\nexport enum ExploreTabs {\n    Tutorials  = 'Tutorials',\n    Tips = 'Tips',\n}\n\nexport enum Compatibility {\n    SearchAndQuery  = 'search',\n    Json = 'json',\n    TimeSeries = 'time-series'\n}\n\nexport enum ChatBotTabs {\n    General  = 'General',\n    Database = 'Database',\n}\n\nexport enum RedisOverviewPage {\n    DataBase  = 'Redis Databases',\n    Rdi = 'My RDI instances',\n}\n\nexport enum TextConnectionSection {\n    Success  = 'success',\n    Failed = 'failed',\n}\n\nexport enum RdiTemplatePipelineType {\n    Ingest  = 'ingest',\n    WriteBehind = 'write-behind',\n}\n\nexport enum RdiTemplateDatabaseType  {\n    SqlServer  = 'sql',\n    Oracle = 'oracle',\n    MySql = 'mysql',\n}\n\nexport enum RdiPopoverOptions  {\n    Server  = 'server',\n    File = 'file',\n    Pipeline = 'empty',\n}\n\nexport enum TlsCertificates  {\n    CA  = 'ca',\n    Client = 'client',\n}\n\nexport enum AddElementInList  {\n    Head ,\n    Tail,\n}\n\n"
  },
  {
    "path": "tests/e2e/helpers/database-scripts.ts",
    "content": "import * as sqlite3 from 'sqlite3';\nimport {workingDirectory} from '../helpers/conf';\nimport {promisify} from \"util\";\nimport {createTimeout} from \"./utils\";\n\nconst dbPath = `${workingDirectory}/redisinsight.db`;\n\nexport class DatabaseScripts {\n    /**\n     * Update table column value into local DB for a specific row\n     * @param dbTableParameters The sqlite database table parameters\n     */\n    static async updateColumnValueInDBTable(dbTableParameters: DbTableParameters): Promise<void> {\n        const db = new sqlite3.Database(dbPath);\n        try {\n            const runAsync = (query: string, p: (string | number | undefined)[]) => promisify(db.run.bind(db)); // convert db.run to a Promise-based function\n            const query = `UPDATE ${dbTableParameters.tableName}\n                           SET ${dbTableParameters.columnName} = ?\n                           WHERE ${dbTableParameters.conditionWhereColumnName} = ?`;\n            await runAsync(query, [dbTableParameters.rowValue, dbTableParameters.conditionWhereColumnValue]);\n        } catch (err) {\n            console.log(`Error during changing ${dbTableParameters.columnName} column value: ${err}`)\n            throw new Error(\n                `Error during changing ${dbTableParameters.columnName} column value: ${err}`,\n            );\n        } finally {\n            console.log(\"Close DB\")\n            db.close();\n        }\n\n    }\n\n    /**\n     * Get Column value from table in local Database\n     * @param dbTableParameters The sqlite database table parameters\n     */\n    static async getColumnValueFromTableInDB(dbTableParameters: DbTableParameters): Promise<any> {\n        // Open the database in read/write mode and fail early if it cannot be opened.\n        const db = await new Promise<sqlite3.Database>((resolve, reject) => {\n            const database = new sqlite3.Database(\n                dbPath,\n                sqlite3.OPEN_READWRITE,\n                (err: Error | null) => {\n                    if (err) {\n                        reject(new Error(`Error opening DB at path ${dbPath}: ${err.message}`));\n                    } else {\n                        resolve(database);\n                    }\n                }\n            );\n        });\n\n        const query = `SELECT ${dbTableParameters.columnName}\n                       FROM ${dbTableParameters.tableName}\n                       WHERE ${dbTableParameters.conditionWhereColumnName} = ?`;\n        try {\n            const getAsync = (query: string, p: (string | number | undefined)[]) => promisify(db.get.bind(db));\n            const row = await Promise.race([\n                getAsync(query, [dbTableParameters.conditionWhereColumnValue]),\n                createTimeout('Query timed out after 10 seconds',10000)\n            ]);\n            if (!row) {\n                throw new Error(`No row found for column ${dbTableParameters.columnName}`);\n            }\n            return row[dbTableParameters.columnName!];\n        } catch (err: any) {\n            throw new Error(`Error during getting ${dbTableParameters.columnName} column value: ${err.message}`);\n        } finally {\n            db.close();\n        }\n    }\n\n    /**\n     * Delete all rows from table in local DB\n     * @param dbTableParameters The sqlite database table parameters\n     */\n    static async deleteRowsFromTableInDB(dbTableParameters: DbTableParameters): Promise<void> {\n        const db = await new Promise<sqlite3.Database>((resolve, reject) => {\n            const database = new sqlite3.Database(\n                dbPath,\n                sqlite3.OPEN_READWRITE,\n                (err: Error | null) => {\n                    if (err) {\n                        console.log(`Error during deleteRowsFromTableInDB: ${err}`);\n                        reject(new Error(`Error opening DB at path ${dbPath}: ${err.message}`));\n                    } else {\n                        resolve(database);\n                    }\n                }\n            );\n        });\n\n        const query = `DELETE\n                       FROM ${dbTableParameters.tableName}`;\n\n        try {\n            const runAsync = promisify(db.run.bind(db));\n            await Promise.race([\n                runAsync(query),\n                createTimeout('DELETE operation timed out after 10 seconds', 10000)\n            ]);\n        } catch (err: any) {\n            throw new Error(`Error during ${dbTableParameters.tableName} table rows deletion: ${err.message}`);\n        } finally {\n            db.close();\n        }\n    }\n\n}\n\n/**\n * Add new database parameters\n * @param tableName The name of table in DB\n * @param columnName The name of column in table\n * @param rowValue Value to update in table\n * @param conditionWhereColumnName The name of the column to search\n * @param conditionWhereColumnValue The value to match in the column\n */\nexport type DbTableParameters = {\n    tableName: string,\n    columnName?: string,\n    rowValue?: string | number,\n    conditionWhereColumnName?: string,\n    conditionWhereColumnValue?: string\n};\n"
  },
  {
    "path": "tests/e2e/helpers/database.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport {\n    AddNewDatabaseParameters,\n    SentinelParameters,\n    OSSClusterParameters\n} from '../pageObjects/dialogs/add-redis-database-dialog';\nimport { DiscoverMasterGroupsPage } from '../pageObjects/sentinel/discovered-sentinel-master-groups-page';\nimport {\n    MyRedisDatabasePage,\n    BrowserPage,\n    AutoDiscoverREDatabases\n} from '../pageObjects';\nimport { UserAgreementDialog } from '../pageObjects/dialogs';\nimport { DatabaseAPIRequests } from './api/api-database';\nimport { RedisOverviewPage } from './constants';\nimport { RdiInstancesListPage } from '../pageObjects/rdi-instances-list-page';\nimport { updateControlNumber } from './insights';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst discoverMasterGroupsPage = new DiscoverMasterGroupsPage();\nconst autoDiscoverREDatabases = new AutoDiscoverREDatabases();\nconst browserPage = new BrowserPage();\nconst userAgreementDialog = new UserAgreementDialog();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst rdiInstancesListPage = new RdiInstancesListPage();\n\nexport class DatabaseHelper {\n    /**\n     * Add a new database manually using host and port\n     * @param databaseParameters The database parameters\n     */\n    async addNewStandaloneDatabase(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        // Fill the add database form\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDataBase(\n            databaseParameters\n        );\n        // Click for saving\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton)\n            // Wait for database to be exist\n            .expect(\n                myRedisDatabasePage.dbNameList.withExactText(\n                    databaseParameters.databaseName ?? ''\n                ).exists\n            )\n            .ok('The database not displayed', { timeout: 10000 })\n            // Close message\n            .click(myRedisDatabasePage.Toast.toastCloseButton);\n    }\n\n    /**\n     * Add a new database via autodiscover using Sentinel option\n     * @param databaseParameters The Sentinel parameters: host, port and sentinel password\n     */\n    async discoverSentinelDatabase(\n        databaseParameters: SentinelParameters\n    ): Promise<void> {\n        // Fill sentinel parameters to auto-discover Master Groups\n        await myRedisDatabasePage.AddRedisDatabaseDialog.discoverSentinelDatabases(\n            databaseParameters\n        );\n        // Click for autodiscover\n        await t\n            .click(\n                myRedisDatabasePage.AddRedisDatabaseDialog\n                    .addRedisDatabaseButton\n            )\n            .expect(discoverMasterGroupsPage.addPrimaryGroupButton.exists)\n            .ok('User is not on the second step of Sentinel flow', {\n                timeout: 10000\n            });\n        // Select Master Groups and Add to Redis Insight\n        await discoverMasterGroupsPage.addMasterGroups();\n        await t.click(autoDiscoverREDatabases.viewDatabasesButton);\n    }\n\n    /**\n     * Add a new database from RE Cluster via auto-discover flow\n     * @param databaseParameters The database parameters\n     */\n    async addNewRedisSoftwareDatabase(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        // Fill the add database form\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addAutodiscoverRedisSoftwareDatabase(\n            databaseParameters\n        );\n        // Click on submit button\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton)\n            // Wait for database to be exist in the list of Autodiscover databases and select it\n            .expect(\n                autoDiscoverREDatabases.databaseName.withExactText(\n                    databaseParameters.databaseName ?? ''\n                ).exists\n            )\n            .ok('The database not displayed', { timeout: 10000 })\n            .typeText(\n                autoDiscoverREDatabases.search,\n                databaseParameters.databaseName ?? ''\n            )\n            .click(autoDiscoverREDatabases.databaseCheckbox)\n            // Click Add selected databases button\n            .click(autoDiscoverREDatabases.addSelectedDatabases)\n            .click(autoDiscoverREDatabases.viewDatabasesButton);\n    }\n\n    /**\n     * Add a new database from OSS Cluster via auto-discover flow\n     * @param databaseParameters The database parameters\n     */\n    async addOSSClusterDatabase(\n        databaseParameters: OSSClusterParameters\n    ): Promise<void> {\n        // Enter required parameters for OSS Cluster\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addOssClusterDatabase(\n            databaseParameters\n        );\n        // Click for saving\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton)\n            // Check for info message that DB was added\n            .expect(myRedisDatabasePage.Toast.toastHeader.exists)\n            .ok('Info message not exists', { timeout: 10000 })\n            // Wait for database to be exist\n            .expect(\n                myRedisDatabasePage.dbNameList.withExactText(\n                    databaseParameters.ossClusterDatabaseName\n                ).exists\n            )\n            .ok('The database not displayed', { timeout: 10000 });\n    }\n\n    /**\n     * Add a new database from Redis Cloud via auto-discover flow\n     * @param cloudAPIAccessKey The Cloud API Access Key\n     * @param cloudAPISecretKey The Cloud API Secret Key\n     */\n    async autodiscoverRedisCloudDatabase(\n        cloudAPIAccessKey: string,\n        cloudAPISecretKey: string\n    ): Promise<string> {\n        // Fill the add database form and Submit\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addAutodiscoverRedisCloudDatabase(\n            cloudAPIAccessKey,\n            cloudAPISecretKey\n        );\n        await t.click(\n            myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton\n        );\n        await t\n            .expect(\n                autoDiscoverREDatabases.title.withExactText(\n                    'Redis Cloud Subscriptions'\n                ).exists\n            )\n            .ok('Subscriptions list not displayed', { timeout: 120000 });\n        // Select subscriptions\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.selectAllCheckbox);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.showDatabasesButton);\n        // Select databases for adding\n        const databaseName = await autoDiscoverREDatabases.getDatabaseName();\n        await t.click(autoDiscoverREDatabases.databaseCheckbox);\n        await t.click(autoDiscoverREDatabases.addSelectedDatabases);\n        // Wait for database to be exist in the redis databases list\n        await t\n            .expect(\n                autoDiscoverREDatabases.title.withExactText(\n                    'Redis Enterprise Databases Added'\n                ).exists\n            )\n            .ok('Added databases list not displayed', { timeout: 20000 });\n        await t.click(autoDiscoverREDatabases.viewDatabasesButton);\n        // uncomment when fixed db will be added to cloud subscription\n        // await t.expect(myRedisDatabasePage.dbNameList.withExactText(databaseName).exists).ok('The database not displayed', { timeout: 10000 });\n        return databaseName;\n    }\n\n    /**\n     * Accept License terms and add database\n     * @param databaseParameters The database parameters\n     * @param databaseName The database name\n     */\n    async acceptLicenseTermsAndAddDatabase(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        await this.acceptLicenseTerms();\n        await this.addNewStandaloneDatabase(databaseParameters);\n        // Connect to DB\n        await myRedisDatabasePage.clickOnDBByName(\n            databaseParameters.databaseName!\n        );\n    }\n\n    /**\n     * Accept License terms and add database using api\n     * @param databaseParameters The database parameters\n     * @param databaseName The database name\n     */\n    async acceptLicenseTermsAndAddDatabaseApi(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        await this.acceptLicenseTerms();\n\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName\n        );\n\n        if (!databaseId) {\n            await databaseAPIRequests.addNewStandaloneDatabaseApi(\n                databaseParameters\n            );\n\n            // Reload Page to see the new added database through api\n            await myRedisDatabasePage.reloadPage();\n        }\n\n        // Connect to DB\n        await myRedisDatabasePage.clickOnDBByName(\n            databaseParameters.databaseName!\n        );\n    }\n\n    /**\n     * Accept License terms and add OSS cluster database\n     * @param databaseParameters The database parameters\n     * @param databaseName The database name\n     */\n    async acceptLicenseTermsAndAddOSSClusterDatabase(\n        databaseParameters: OSSClusterParameters\n    ): Promise<void> {\n        await this.acceptLicenseTerms();\n\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.ossClusterDatabaseName\n        );\n\n        if (!databaseId) {\n            await this.addOSSClusterDatabase(databaseParameters);\n        }\n\n        // Connect to DB\n        await myRedisDatabasePage.clickOnDBByName(\n            databaseParameters.ossClusterDatabaseName!\n        );\n    }\n\n    /**\n     * Accept License terms and add Sentinel database using api\n     * @param databaseParameters The database parameters\n     */\n    async acceptLicenseTermsAndAddSentinelDatabaseApi(\n        databaseParameters: SentinelParameters\n    ): Promise<void> {\n        await this.acceptLicenseTerms();\n        await databaseAPIRequests.discoverSentinelDatabaseApi(\n            databaseParameters\n        );\n        // Reload Page to see the database added through api\n        await myRedisDatabasePage.reloadPage();\n        // Connect to DB\n        await myRedisDatabasePage.clickOnDBByName(\n            databaseParameters.masters![1].alias ?? ''\n        );\n    }\n\n    /**\n     * Accept License terms and add RE Cluster database\n     * @param databaseParameters The database parameters\n     */\n    async acceptLicenseTermsAndAddRedisSoftwareDatabase(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        await this.acceptLicenseTerms();\n        await this.addNewRedisSoftwareDatabase(databaseParameters);\n        // Connect to DB\n        await myRedisDatabasePage.clickOnDBByName(\n            databaseParameters.databaseName ?? ''\n        );\n    }\n\n    /**\n     * Accept License terms and add RE Cloud database\n     * @param databaseParameters The database parameters\n     */\n    async acceptLicenseTermsAndAddRedisCloudDatabase(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const searchTimeout = 60 * 1000; // 60 sec to wait database appearing\n        const dbSelector = myRedisDatabasePage.dbNameList.withExactText(\n            databaseParameters.databaseName ?? ''\n        );\n        const startTime = Date.now();\n\n        await this.acceptLicenseTerms();\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDataBase(\n            databaseParameters\n        );\n        // Click for saving\n        await t.click(\n            myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton\n        );\n        await t.wait(3000);\n        // Reload page until db appears\n        do {\n            await myRedisDatabasePage.reloadPage();\n        } while (\n            !(await dbSelector.exists) &&\n            Date.now() - startTime < searchTimeout\n        );\n        await t\n            .expect(\n                myRedisDatabasePage.dbNameList.withExactText(\n                    databaseParameters.databaseName ?? ''\n                ).exists\n            )\n            .ok('The database not displayed', { timeout: 5000 });\n        await myRedisDatabasePage.clickOnDBByName(\n            databaseParameters.databaseName ?? ''\n        );\n        await t\n            .expect(browserPage.keysSummary.exists)\n            .ok('Key list not loaded', { timeout: 15000 });\n    }\n\n    /**\n     * Add RE Cloud database\n     * @param databaseParameters The database parameters\n     */\n    async addRedisCloudDatabase(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        const searchTimeout = 60 * 1000; // 60 sec to wait database appearing\n        const dbSelector = myRedisDatabasePage.dbNameList.withExactText(\n            databaseParameters.databaseName ?? ''\n        );\n        const startTime = Date.now();\n\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDataBase(\n            databaseParameters\n        );\n        // Click for saving\n        await t.click(\n            myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton\n        );\n        await t.wait(3000);\n        // Reload page until db appears\n        do {\n            await myRedisDatabasePage.reloadPage();\n        } while (\n            !(await dbSelector.exists) &&\n            Date.now() - startTime < searchTimeout\n        );\n        await t\n            .expect(\n                myRedisDatabasePage.dbNameList.withExactText(\n                    databaseParameters.databaseName ?? ''\n                ).exists\n            )\n            .ok('The database not displayed', { timeout: 5000 });\n    }\n\n    // Accept License terms\n    async acceptLicenseTerms(): Promise<void> {\n        await t.maximizeWindow();\n        await userAgreementDialog.acceptLicenseTerms();\n        // Required since that file is used both in electron and web tests\n        if(process.env.RI_SOCKETS_CORS){\n            await updateControlNumber(48.2);\n        }\n\n        // Open default databases list tab if RDI opened\n        if (await rdiInstancesListPage.addRdiInstanceButton.exists) {\n            await myRedisDatabasePage.setActivePage(RedisOverviewPage.DataBase);\n        }\n        // TODO delete after releasing chatbot\n        if (await myRedisDatabasePage.AddRedisDatabaseDialog.aiChatMessage.exists) {\n            await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.aiCloseMessage)\n        }\n    }\n\n    // Accept License terms and connect to the RedisStack database\n    async acceptLicenseAndConnectToRedisStack(): Promise<void> {\n        await this.acceptLicenseTerms();\n        //Connect to DB\n        await t\n            .click(myRedisDatabasePage.NavigationPanel.myRedisDBButton)\n            .click(\n                myRedisDatabasePage.AddRedisDatabaseDialog.connectToRedisStackButton\n            );\n    }\n\n    /**\n     * Delete database\n     * @param databaseName The database name\n     */\n    async deleteDatabase(databaseName: string): Promise<void> {\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        if (\n            await myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton.exists\n        ) {\n            await this.deleteDatabaseByNameApi(databaseName);\n        }\n    }\n\n    /**\n     * Delete database with custom name\n     * @param databaseName The database name\n     */\n    async deleteCustomDatabase(databaseName: string): Promise<void> {\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        if (\n            await myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton.exists\n        ) {\n            await myRedisDatabasePage.deleteDatabaseByName(databaseName);\n        }\n    }\n\n    /**\n     * Accept License terms and add database or connect to the Redis stask database\n     * @param databaseParameters The database parameters\n     * @param databaseName The database name\n     */\n    async acceptTermsAddDatabaseOrConnectToRedisStack(\n        databaseParameters: AddNewDatabaseParameters\n    ): Promise<void> {\n        if (\n            await myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton.exists\n        ) {\n            await this.acceptLicenseTermsAndAddDatabase(databaseParameters);\n        }\n        else {\n            await this.acceptLicenseAndConnectToRedisStack();\n        }\n    }\n\n    /**\n     * Click on the edit database button by name\n     * @param databaseName The name of the database\n     */\n    async clickOnEditDatabaseByName(databaseName: string): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseName\n        );\n        const databaseEditBtn = Selector(\n            `[data-testid=edit-instance-${databaseId}]`\n        );\n\n        await t\n            .expect(databaseEditBtn.exists)\n            .ok(`\"${databaseName}\" database not displayed`);\n        await t.click(databaseEditBtn);\n    }\n\n    /**\n     * Delete database button by name\n     * @param databaseName The name of the database\n     */\n    async deleteDatabaseByNameApi(databaseName: string): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(\n            databaseName\n        );\n        const databaseDeleteBtn = Selector(\n            `[data-testid=delete-instance-${databaseId}-icon]`\n        );\n\n        await t\n            .expect(databaseDeleteBtn.exists)\n            .ok(`\"${databaseName}\" database not displayed`);\n        await t.click(databaseDeleteBtn);\n        await t.click(myRedisDatabasePage.confirmDeleteButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/decompressors/base-decompressors-populator.ts",
    "content": "import { createClient } from 'redis';\n\nexport abstract class BaseDatabasePopulator {\n    private client: ReturnType<typeof createClient>;\n\n    protected abstract createCompressedKeys(): Promise<void>;\n\n    constructor(private host: string, private port: string) {\n        const dbConf = { port: Number.parseInt(port), host, username: 'default' };\n        this.client = createClient(dbConf);\n\n        this.client.on('error', (error: string) => {\n            throw new Error(`Redis connection error: ${error}`);\n        });\n    }\n\n    /**\n     * Populate db with compressed keys\n     */\n    public async populateDB(): Promise<void> {\n        this.client.on('connect', async () => {\n            console.log('Connected to Redis');\n            try {\n                await this.createCompressedKeys();\n            } catch (error) {\n                console.error('Error during key creation:', error);\n            } finally {\n                await this.client.quit();\n            }\n        });\n    }\n\n    /**\n     * create a hash\n     * @param prefix prefix of the key name\n     * @param values values of the hash\n     */\n    protected async createHash(\n        prefix: string,\n        values: Buffer[]\n    ): Promise<void> {\n        let fields: string[] = [];\n\n        const randomNumber = Array.from({ length: 5 }).map(() => Math.random());\n\n        values.forEach((value) => {\n            const field = `${value.toString()}:${randomNumber.toString()}`;\n            const fieldValue = `${value.toString()}:${randomNumber.toString()}`;\n            fields.push(field, fieldValue);\n        });\n\n        try {\n            await this.client.hset(`${prefix}:hash`, ...fields);\n            console.log(`Hash created with prefix: ${prefix}`);\n        } catch (error) {\n            console.error(`Error creating hash with prefix ${prefix}:`, error);\n            throw error;\n        }\n    }\n\n    /**\n     * create a string\n     * @param prefix prefix of the key name\n     * @param value values of the string\n     */\n    protected async createString(prefix: string, value: Buffer): Promise<void> {\n        this.client.set(`${prefix}:string`, value, (error: Error | null) => {\n            if (error) {\n                console.error(`Error saving key ${prefix}:`, error);\n                throw error;\n            }\n            console.log(`Key ${prefix} successfully saved.`);\n        });\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/decompressors/brotli-database-populator.ts",
    "content": "import * as proto from 'protobufjs';\nimport { pack as msgpackrPack } from 'msgpackr';\nimport * as brotli from 'brotli-unicode';\nimport * as fflate from 'fflate';\nimport * as fs from 'fs';\nimport { BaseDatabasePopulator } from './base-decompressors-populator';\n\nconst COMPRESSED_PREFIX = 'Comp';\nconst BROTLI_PREFIX = 'BROTLI';\n\nexport class BrotliDatabasePopulator extends BaseDatabasePopulator {\n\n    /**\n     * Create keys with all types of Bolti compression\n     */\n        protected async createCompressedKeys(): Promise<void> {\n        await this.createBrotliUnicodeKeys();\n        await this.createBrotliASCIIKeys();\n        await this.createBrotliVectorKeys();\n        await this.createBrotliJSONKeys();\n        await this.createBrotliPHPUnserializedJSONKeys();\n        await this.createBrotliJavaSerializedObjectKeys();\n        await this.createBrotliMsgpackKeys();\n        await this.createBrotliProtobufKeys();\n        await this.createBrotliPickleKeys();\n    }\n\n    private async createBrotliUnicodeKeys() {\n        const encoder = new TextEncoder();\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:Unicode`;\n        const rawValue = '漢字';\n        const buf = encoder.encode(rawValue);\n        const value = Buffer.from(await brotli.compress(buf));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createBrotliASCIIKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:ASCII`;\n        const rawValue = '\\xac\\xed\\x00\\x05t\\x0a4102';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(await brotli.compress(buf));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createBrotliVectorKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:Vector`;\n        const rawValue = JSON.parse(fs.readFileSync('./test-data/decompressors/vector.json', 'utf8'));\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(await brotli.compress(buf));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createBrotliJSONKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:JSON`;\n        const rawValue = '{\"test\":\"test\"}';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(await brotli.compress(buf));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createBrotliPHPUnserializedJSONKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:PHP`;\n        const rawValue = 'a:2:{i:0;s:12:\"Sample array\";i:1;a:2:{i:0;s:5:\"Apple\";i:1;s:6:\"Orange\";}}';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(await brotli.compress(buf));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createBrotliPickleKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:Pickle`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/pickleFile1.pickle');\n        const value = Buffer.from(await brotli.compress(rawValue));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    };\n\n    private async createBrotliJavaSerializedObjectKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:Java`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/test_serialised_obj.ser');\n        const rawValue2 = fs.readFileSync('./test-data/decompressors/test_annotated_obj.ser');\n\n        const value = Buffer.from(await brotli.compress(rawValue));\n        const value2 = Buffer.from(await brotli.compress(rawValue2));\n\n        await this.createString(prefix, value);\n        await this.createString(prefix, value2);\n        await this.createHash(prefix, [value,value2]);\n    }\n\n    private async createBrotliMsgpackKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:Msgpack`;\n        const rawValue = msgpackrPack({\n            hello: 'World',\n            array: [1, 2],\n            obj: {test: 'test'},\n            boolean: false,\n        });\n        const value = Buffer.from(await brotli.compress(rawValue));\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private createBrotliProtobufKeys(): Promise<void> {\n        return new Promise((resolve, reject) => {\n            const prefix = `${COMPRESSED_PREFIX}:${BROTLI_PREFIX}:Proto`;\n            proto.load('./test-data/decompressors/awesome.proto', async (err, root) => {\n                if (err || !root) {\n                    console.error('Error loading protobuf:', err);\n                    return reject(err);\n                }\n\n                try {\n                    const Book = root.lookupType('com.book.BookStore');\n                    const payload = {name: 'Test name', books: {0: 'book 1', 1: 'book 2'}};\n                    const message = Book.create(payload);\n                    const rawValue = Book.encode(message).finish();\n\n                    const value = Buffer.from(await brotli.compress(rawValue));\n                    await this.createString(prefix, value);\n                    await this.createHash(prefix, [value]);\n                    resolve();\n                } catch (error) {\n                    reject(error);\n                }\n            });\n        });\n    }\n}\n\n"
  },
  {
    "path": "tests/e2e/helpers/decompressors/gzip-database-populator.ts",
    "content": "import { pack as msgpackrPack } from 'msgpackr';\nimport * as fs from 'fs';\nimport * as fflate from 'fflate';\nimport * as proto from 'protobufjs';\nimport { BaseDatabasePopulator } from './base-decompressors-populator';\n\n\nconst COMPRESSED_PREFIX = 'Comp';\nconst GZIP_PREFIX = 'GZIP';\n\nexport class GzipDatabasePopulator extends BaseDatabasePopulator {\n\n    /**\n     * Create keys with all types of Gzip compression\n     */\n    protected async createCompressedKeys(): Promise<void> {\n        await this.createGZIPUnicodeKeys();\n        await this.createGZIPASCIIKeys();\n        await this.createGZIPJSONKeys();\n        await this.createGZIPPHPUnserializedJSONKeys();\n        await this.createGZIPMsgpackKeys();\n        await this.createGZIPProtobufKeys();\n        await this.createGZIPPickleKeys();\n        await this.createGZIPJavaSerializedObjectKeys();\n        await this.createGZIPVectorKeys();\n    }\n\n    private async createGZIPUnicodeKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:Unicode`;\n        const rawValue = '漢字';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.compressSync(buf));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createGZIPASCIIKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:ASCII`;\n        const rawValue = '\\xac\\xed\\x00\\x05t\\x0a4102';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.compressSync(buf));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createGZIPJSONKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:JSON`;\n        const rawValue = '{\"test\":\"test\"}';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.compressSync(buf));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createGZIPPHPUnserializedJSONKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:PHP`;\n        const rawValue =\n            'a:2:{i:0;s:12:\"Sample array\";i:1;a:2:{i:0;s:5:\"Apple\";i:1;s:6:\"Orange\";}}';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.compressSync(buf));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createGZIPJavaSerializedObjectKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:Java`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/test_serialised_obj.ser');\n        const rawValue2 = fs.readFileSync('./test-data/decompressors/test_annotated_obj.ser');\n\n        const value = Buffer.from(fflate.compressSync(rawValue));\n        const value2 = Buffer.from(fflate.compressSync(rawValue2));\n\n        await this.createString(prefix, value);\n        await this.createString(prefix, value2);\n        await this.createHash(prefix, [value,value2]);\n    }\n\n    private async createGZIPMsgpackKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:Msgpack`;\n        const rawValue = msgpackrPack({\n            hello: 'World',\n            array: [1, 2],\n            obj: {test: 'test'},\n            boolean: false,\n        });\n\n        const value = Buffer.from(fflate.compressSync(rawValue));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private async createGZIPVectorKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:Vector`;\n        const rawValue = JSON.parse(fs.readFileSync('./test-data/decompressors/vector.json', 'utf8'));\n        const value = Buffer.from(fflate.compressSync(rawValue));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n\n    private createGZIPProtobufKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:Proto`;\n\n        return new Promise((resolve, reject) => {\n            proto.load('./test-data/decompressors/awesome.proto', async (err, root) => {\n                if (err || !root) {\n                    console.error('Error loading protobuf:', err);\n                    return reject(err);\n                }\n\n                const Book = root.lookupType('com.book.BookStore');\n                const payloadBookStore = {\n                    name: 'Test name',\n                    books: {0: 'book 1', 1: 'book 2'},\n                };\n                const message = Book.create(payloadBookStore);\n                const rawValue = Book.encode(message).finish();\n                const value = Buffer.from(fflate.compressSync(rawValue));\n\n                await this.createString(prefix, value);\n                await this.createHash(prefix, [value]);\n\n                resolve();\n            });\n        });\n    }\n\n    private async createGZIPPickleKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${GZIP_PREFIX}:Pickle`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/pickleFile1.pickle');\n        const value = Buffer.from(fflate.compressSync(rawValue));\n\n        await this.createString(prefix, value);\n        await this.createHash(prefix, [value]);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/decompressors/lz4-database-populator.ts",
    "content": "import { pack as msgpackrPack } from 'msgpackr';\nimport * as fflate from 'fflate';\nimport * as fs from 'fs';\nimport * as lz4js from 'lz4js';\nimport * as proto from 'protobufjs';\nimport { BaseDatabasePopulator } from './base-decompressors-populator';\n\nconst COMPRESSED_PREFIX = 'Comp';\nconst LZ4_PREFIX = 'LZ4';\n\nexport class LZ4DatabasePopulator extends BaseDatabasePopulator{\n\n    /**\n     * Create keys with all types of LZ4 compression\n     */\n    protected async createCompressedKeys(): Promise<void> {\n        await this.createLZ4UnicodeKeys();\n        await this.createLZ4ASCIIKeys();\n        await this.createLZ4JSONKeys();\n        await this.createLZ4PHPUnserializedJSONKeys();\n        await this.createLZ4MsgpackKeys();\n        await this.createLZ4ProtobufKeys();\n        await this.createLZ4PickleKeys();\n        await this.createLZ4JavaSerializedObjectKeys();\n        await this.createLZ4VectorKeys();\n    }\n\n    private async createLZ4UnicodeKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:Unicode`;\n        const rawValue = '漢字';\n        const buf = new TextEncoder().encode(rawValue);\n        const value = Buffer.from(lz4js.compress(buf));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n\n    private async createLZ4ASCIIKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:ASCII`;\n        const rawValue = '\\xac\\xed\\x00\\x05t\\x0a4102';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(lz4js.compress(buf));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n\n    private async createLZ4JSONKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:JSON`;\n        const rawValue = '{\"test\":\"test\"}';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(lz4js.compress(buf));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n\n    private async createLZ4PHPUnserializedJSONKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:PHP`;\n        const rawValue = 'a:2:{i:0;s:12:\"Sample array\";i:1;a:2:{i:0;s:5:\"Apple\";i:1;s:6:\"Orange\";}}';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(lz4js.compress(buf));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n\n    private async createLZ4JavaSerializedObjectKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:Java`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/test_serialised_obj.ser');\n        const rawValue2 = fs.readFileSync('./test-data/decompressors/test_annotated_obj.ser');\n        const value = Buffer.from(lz4js.compress(rawValue));\n        const value2 = Buffer.from(lz4js.compress(rawValue2));\n        await this.createHash(prefix, [value,value2]);\n        await this.createString(prefix, value);\n    }\n\n    private async createLZ4MsgpackKeys(): Promise<void> {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:Msgpack`;\n        const rawValue = msgpackrPack({\n            hello: 'World',\n            array: [1, 2],\n            obj: { test: 'test' },\n            boolean: false,\n        });\n        const value = Buffer.from(lz4js.compress(rawValue));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n\n    private createLZ4ProtobufKeys(): Promise<void> {\n        return new Promise((resolve, reject) => {\n            const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:Proto`;\n            proto.load('./test-data/decompressors/awesome.proto', async (err, root) => {\n                if (err || !root) {\n                    console.error('Error loading protobuf:', err);\n                    return reject(err);\n                }\n                try {\n                    const Book = root.lookupType('com.book.BookStore');\n                    const payload = { name: 'Test name', books: { 0: 'book 1', 1: 'book 2' } };\n                    const message = Book.create(payload);\n                    const rawValue = Book.encode(message).finish();\n                    const value = Buffer.from(lz4js.compress(rawValue));\n                    await this.createHash(prefix, [value]);\n                    await this.createString(prefix, value);\n                    resolve();\n                } catch (error) {\n                    reject(error);\n                }\n            });\n        });\n    }\n\n    private async createLZ4PickleKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:Pickle`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/pickleFile1.pickle');\n        const value = Buffer.from(lz4js.compress(rawValue));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n\n    private async createLZ4VectorKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${LZ4_PREFIX}:Vector`;\n        const rawValue = JSON.parse(fs.readFileSync('./test-data/decompressors/vector.json', 'utf8'));\n        const value = Buffer.from(lz4js.compress(rawValue));\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/decompressors/php-gzcompress-database-populator.ts",
    "content": "import { pack as msgpackrPack } from 'msgpackr';\nimport * as fflate from 'fflate';\nimport * as fs from 'fs';\nimport * as proto from 'protobufjs';\nimport { BaseDatabasePopulator } from './base-decompressors-populator';\n\nconst COMPRESSED_PREFIX = 'Comp';\nconst GZLIB_PREFIX = 'GZLIB';\n\nexport class PhpGzcompressDatabasePopulator extends BaseDatabasePopulator {\n\n    /**\n     * Create keys with all types of LZ4 compression\n     */\n\n    protected async createCompressedKeys(): Promise<void> {\n        await this.createGZCompressUnicodeKeys();\n        await this.createGZCompressASCIIKeys();\n        await this.createGZCompressJSONKeys();\n        await this.createGZCompressPHPUnserializedJSONKeys();\n        await this.createGZCompressMsgpackKeys();\n        await this.createGZCompressProtobufKeys();\n        await this.createGZCompressPickleKeys();\n        await this.createGZCompressJavaSerializedObjectKeys();\n        await this.createGZCompressVectorKeys();\n    };\n\n    private async createGZCompressUnicodeKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:Unicode`;\n        const rawValue = '漢字';\n\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.zlibSync(buf));\n\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    };\n\n    private async createGZCompressASCIIKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:ASCII`;\n        const rawValue = '\\xac\\xed\\x00\\x05t\\x0a4102';\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.zlibSync(buf));\n\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    };\n\n    private async createGZCompressJSONKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:JSON`;\n        const rawValue = '{\"test\":\"test\"}';\n\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.zlibSync(buf));\n\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    };\n\n    private async createGZCompressPHPUnserializedJSONKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:PHP`;\n        const rawValue = 'a:2:{i:0;s:12:\"Sample array\";i:1;a:2:{i:0;s:5:\"Apple\";i:1;s:6:\"Orange\";}}';\n\n        const buf = fflate.strToU8(rawValue);\n        const value = Buffer.from(fflate.zlibSync(buf));\n\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    };\n\n    private async createGZCompressJavaSerializedObjectKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:Java`;\n        const rawValue = fs.readFileSync('./test-data/decompressors/test_serialised_obj.ser');\n        const rawValue2 = fs.readFileSync('./test-data/decompressors/test_annotated_obj.ser');\n\n        const value = Buffer.from(fflate.zlibSync(rawValue));\n        const value2 = Buffer.from(fflate.zlibSync(rawValue2));\n\n        await this.createHash(prefix, [value,value2]);\n        await this.createString(prefix, value);\n    };\n\n    private async createGZCompressMsgpackKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:Msgpack`;\n        const rawValue = msgpackrPack({\n            hello: 'World',\n            array: [1, 2],\n            obj: {test: 'test'},\n            boolean: false,\n        });\n\n        const value = Buffer.from(fflate.zlibSync(rawValue));\n\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    };\n\n    private async createGZCompressVectorKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:Vector`;\n        const rawValue = JSON.parse(fs.readFileSync('./test-data/decompressors/vector.json', 'utf8'));\n\n        const value = Buffer.from(fflate.zlibSync(rawValue));\n\n        await this.createHash(prefix, [value]);\n        await this.createString(prefix, value);\n    };\n\n    private createGZCompressProtobufKeys() : Promise<void> {\n        return new Promise((resolve, reject) => {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:Proto`;\n\n            proto.load('./test-data/decompressors/awesome.proto', async (err, root) => {\n                if (err || !root) {\n                    console.error('Error loading protobuf:', err);\n                    return reject(err);\n                }\n                try {\n                    const Book = root.lookupType('com.book.BookStore')\n                    const payloadBookStore = {\n                        name: 'Test name',\n                        books: { 0: 'book 1', 1: 'book 2' },\n                    };\n                    const message = Book.create(payloadBookStore); // or use .fromObject if conversion is necessary\n\n                    // Encode a message to an Uint8Array (browser) or Buffer (node)\n                    const rawValue = Book.encode(message).finish();\n\n                    const value = Buffer.from(fflate.zlibSync(rawValue));\n                    await this.createHash(prefix, [value]);\n                    await this.createString(prefix, value);\n                    resolve();\n                } catch (error) {\n                    reject(error);\n                }\n            });\n        });\n    };\n\n    private async createGZCompressPickleKeys() {\n        const prefix = `${COMPRESSED_PREFIX}:${GZLIB_PREFIX}:Pickle`;\n\n        const rawValue = fs.readFileSync('./test-data/decompressors/test_serialised_obj.ser');\n        const rawValue2 = fs.readFileSync('./test-data/decompressors/test_annotated_obj.ser');\n\n        const value = Buffer.from(fflate.zlibSync(rawValue));\n        const value2 = Buffer.from(fflate.zlibSync(rawValue2));\n        await this.createHash(prefix, [value,value2]);\n        await this.createString(prefix, value);\n    };\n}\n\n"
  },
  {
    "path": "tests/e2e/helpers/helpers.ts",
    "content": "import { filterArguments } from 'cli-argument-parser';\n\nexport const cliArguments = filterArguments('--', '=') as Arguments;\n\n/**\n * The available/used CLI arguments\n * @param databaseHostname The hostname of the database\n * @param databasePort The port of the database\n * @param databaseName The name of the database\n * @param databaseUsername The username of the database\n * @param databasePassword The password of the database\n * @param sentinelHost The hostname of sentinel\n * @param sentinelPort The port of sentinel\n * @param sentinelPassword The password of sentinel\n * @param ossClusterHost The OSS Cluster host\n * @param ossClusterPort The OSS Cluster port\n * @param ossClusterDatabaseName The OSS Cluster database name\n */\n\nexport type Arguments = {\n  databaseHostname?: string,\n  databasePort?: string,\n  databaseName?: string,\n  databaseUsername?: string,\n  databasePassword?: string,\n  sentinelHost?: string,\n  sentinelPort?: string,\n  sentinelPassword?: string,\n  ossClusterHost?: string,\n  ossClusterPort?: string,\n  ossClusterDatabaseName?: string,\n  [key: string]: any\n};\n"
  },
  {
    "path": "tests/e2e/helpers/index.ts",
    "content": "import { DatabaseScripts, DbTableParameters } from './database-scripts';\nimport { Common } from './common';\nimport { DatabaseHelper } from './database';\nimport { SsoAuthorization } from './sso-authorization';\nimport { Telemetry } from './telemetry';\n\nexport {\n    DatabaseScripts,\n    DbTableParameters,\n    Common,\n    DatabaseHelper,\n    SsoAuthorization,\n    Telemetry\n};\n"
  },
  {
    "path": "tests/e2e/helpers/insights.ts",
    "content": "import * as path from 'path';\nimport * as fs from 'fs-extra';\nimport { syncFeaturesApi } from './api/api-info';\nimport { DatabaseScripts, DbTableParameters } from './database-scripts';\nimport { t } from 'testcafe';\n\nconst dbTableParams: DbTableParameters = {\n    tableName: 'features_config',\n    columnName: 'controlNumber',\n    conditionWhereColumnName: 'id',\n    conditionWhereColumnValue: '1'\n};\n\n/**\n * Update features-config file for static server\n * @param filePath Path to feature config json\n */\nexport async function modifyFeaturesConfigJson(filePath: string): Promise<void> {\n    const configFileName = 'features-config.json';\n    const remoteConfigPath = process.env.REMOTE_FOLDER_PATH || './remote';\n    const targetFilePath = path.join(remoteConfigPath, configFileName);\n\n    return new Promise((resolve, reject) => {\n        try {\n            fs.ensureFileSync(targetFilePath);\n            fs.writeFileSync(targetFilePath, fs.readFileSync(filePath));\n            resolve();\n        }\n        catch (err: any) {\n            reject(new Error(`Error updating remote config file: ${err.message}`));\n        }\n    });\n}\n\n/**\n * Update Control Number of current user and sync\n * @param controlNumber Control number to update\n */\nexport async function updateControlNumber(controlNumber: number): Promise<void> {\n    await syncFeaturesApi();\n    await DatabaseScripts.updateColumnValueInDBTable({ ...dbTableParams, rowValue: controlNumber });\n    await syncFeaturesApi();\n    await t.eval(() => location.reload());\n}\n\n/**\n * Refresh test data for features sync\n */\nexport async function refreshFeaturesTestData(): Promise<void> {\n    const defaultConfigPath = path.join('.', 'test-data', 'features-configs', 'insights-default-remote.json');\n\n    await modifyFeaturesConfigJson(defaultConfigPath);\n    await DatabaseScripts.deleteRowsFromTableInDB(dbTableParams);\n    await syncFeaturesApi();\n}\n"
  },
  {
    "path": "tests/e2e/helpers/keys.ts",
    "content": "import { createClient } from 'redis';\nimport { t } from 'testcafe';\nimport { Chance } from 'chance';\nimport { random } from 'lodash';\nimport { BrowserPage } from '../pageObjects';\nimport { KeyData, AddKeyArguments } from '../pageObjects/browser-page';\nimport { COMMANDS_TO_CREATE_KEY, KeyTypesTexts } from './constants';\nimport { Common } from './common';\nimport { populateBigKeys, populateDb } from './scripts/generate-big-data';\n\nconst browserPage = new BrowserPage();\n\nexport function getRandomKeyName(keyNameLength: number): string {\n    const chance = new Chance();\n    let result = '';\n    for (let i = 0; i < keyNameLength; i++) {\n        result += chance.character();\n    }\n    return result;\n}\n\n/**\n * Create random paragraph with amount of sentences\n * @param sentences The amount of sentences in paragraph\n */\nexport function getRandomParagraph(sentences: number): string {\n    const chance = new Chance();\n    return chance.paragraph({ sentences: sentences });\n}\n\nexport const keyTypes = [\n    { textType: KeyTypesTexts.Hash, keyName: 'hash', data: 'value' },\n    { textType: KeyTypesTexts.List, keyName: 'list_', data: 'value' },\n    { textType: KeyTypesTexts.Set, keyName: 'set', data: 'member' },\n    { textType: KeyTypesTexts.ZSet, keyName: 'zset', data: 'member' },\n    { textType: KeyTypesTexts.String, keyName: 'string', data: 'value' },\n    { textType: KeyTypesTexts.ReJSON, keyName: 'json', data: 'data' },\n    { textType: KeyTypesTexts.Stream, keyName: 'stream', data: 'field' },\n    { textType: KeyTypesTexts.Graph, keyName: 'graph' },\n    { textType: KeyTypesTexts.TimeSeries, keyName: 'timeSeries' }\n];\n\nexport const formattersKeyTypes = [\n    { textType: KeyTypesTexts.Hash, keyName: 'hash', data: 'value' },\n    { textType: KeyTypesTexts.List, keyName: 'list_', data: 'value' },\n    { textType: KeyTypesTexts.Set, keyName: 'set', data: 'member' },\n    { textType: KeyTypesTexts.ZSet, keyName: 'zset', data: 'member' },\n    { textType: KeyTypesTexts.String, keyName: 'string', data: 'value' },\n    { textType: KeyTypesTexts.Stream, keyName: 'stream', data: 'field' }\n];\n\n/**\n * Adding keys of each type through the cli\n * @param keyData The key data\n * @param keyValue The key value\n * @param keyField The key field value\n */\nexport async function addKeysViaCli(keyData: KeyData, keyValue?: string, keyField?: string): Promise<void> {\n    await t.click(browserPage.Cli.cliExpandButton);\n    for (const { textType, keyName } of keyData) {\n        if (textType in COMMANDS_TO_CREATE_KEY) {\n            textType === 'Hash' || textType === 'Stream'\n                ? await t.typeText(browserPage.Cli.cliCommandInput, COMMANDS_TO_CREATE_KEY[textType](keyName, keyValue, keyField), { paste: true })\n                : await t.typeText(browserPage.Cli.cliCommandInput, COMMANDS_TO_CREATE_KEY[textType](keyName, keyValue), { paste: true });\n            await t.pressKey('enter');\n        }\n    }\n    await t\n        .click(browserPage.Cli.cliCollapseButton)\n        .click(browserPage.refreshKeysButton);\n}\n\n/**\n * Delete keys of each type through the cli\n * @param keyData The key data\n */\nexport async function deleteKeysViaCli(keyData: KeyData): Promise<void> {\n    const keys: string[] = [];\n    for (const { keyName } of keyData) {\n        keys.push(keyName);\n    }\n    await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n}\n\n/**\n * Populate database with hash keys\n * @param host The host of database\n * @param port The port of database\n * @param keyArguments The arguments of key\n */\nexport async function populateDBWithHashes(host: string, port: string, keyArguments: AddKeyArguments): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({ url });\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error, check DB connection', error);\n        throw error;\n    });\n\n    try {\n        await client.connect();\n\n        if (keyArguments.keysCount) {\n            for (let i = 0; i < keyArguments.keysCount; i++) {\n                const keyName = `${keyArguments.keyNameStartWith}${Common.generateWord(20)}`;\n                await client.hSet(keyName, 'field1', 'Hello');\n            }\n        }\n\n    } catch (error) {\n        console.error('Error during Redis operations:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\n/**\n * Populate hash key with fields\n * @param host The host of database\n * @param port The port of database\n * @param keyArguments The arguments of key and its fields\n */\nexport async function populateHashWithFields(host: string, port: string, keyArguments: AddKeyArguments): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({url});\n    const fields: Record<string, string> = {};\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error, check DB connection', error);\n        throw error;\n    });\n\n    try {\n        await client.connect();\n\n        if (keyArguments.fieldsCount) {\n            for (let i = 0; i < keyArguments.fieldsCount; i++) {\n                const field = `${keyArguments.fieldStartWith}${Common.generateWord(10)}`;\n                const fieldValue = `${keyArguments.fieldValueStartWith}${Common.generateWord(10)}`;\n                fields[field] = fieldValue;\n            }\n        }\n\n        if (keyArguments.keyName) {\n            await client.hSet(keyArguments.keyName, fields);\n        } else {\n            throw new Error('keyName is required to populate the hash.');\n        }\n\n    } catch (error) {\n        console.error('Error setting hash fields:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\n/**\n * Populate list key with elements\n * @param host The host of database\n * @param port The port of database\n * @param keyArguments The arguments of key and its members\n */\nexport async function populateListWithElements(host: string, port: string, keyArguments: AddKeyArguments): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({ url });\n    const elements: string[] = [];\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error', error);\n        throw error;\n    });\n\n    try {\n        await client.connect();\n\n        if (keyArguments.elementsCount) {\n            for (let i = 0; i < keyArguments.elementsCount; i++) {\n                const element = `${keyArguments.elementStartWith}${Common.generateWord(10)}`;\n                elements.push(element);\n            }\n        }\n\n        if (keyArguments.keyName) {\n            await client.lPush(keyArguments.keyName, elements);\n        } else {\n            throw new Error('keyName is required to populate the list.');\n        }\n\n    } catch (error) {\n        console.error('Error pushing elements to list:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\n/**\n * Populate set key with members\n * @param host The host of database\n * @param port The port of database\n * @param keyArguments The arguments of key and its members\n */\nexport async function populateSetWithMembers(host: string, port: string, keyArguments: AddKeyArguments): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({ url });\n    const members: string[] = [];\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error', error);\n        throw error;\n    });\n\n    try {\n        await client.connect();\n\n        if (keyArguments.membersCount) {\n            for (let i = 0; i < keyArguments.membersCount; i++) {\n                const member = `${keyArguments.memberStartWith}${Common.generateWord(10)}`;\n                members.push(member);\n            }\n        }\n\n        if (keyArguments.keyName) {\n            await client.sAdd(keyArguments.keyName, members);\n        } else {\n            throw new Error('keyName is required to populate the set.');\n        }\n\n    } catch (error) {\n        console.error('Error adding members to set:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\n/**\n * Populate Zset key with members\n * @param host The host of database\n * @param port The port of database\n * @param keyArguments The arguments of key and its members\n */\nexport async function populateZSetWithMembers(host: string, port: string, keyArguments: AddKeyArguments): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({ url });\n    const minScoreValue = -10;\n    const maxScoreValue = 10;\n    const members: { score: number; value: string }[] = [];\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error', error);\n    });\n\n    try {\n        await client.connect();\n\n        if (keyArguments.membersCount) {\n            for (let i = 0; i < keyArguments.membersCount; i++) {\n                const memberName = `${keyArguments.memberStartWith}${Common.generateWord(10)}`;\n                const scoreValue = random(minScoreValue, maxScoreValue);\n                members.push({ score: scoreValue, value: memberName });\n            }\n        }\n\n        if (keyArguments.keyName) {\n            await client.zAdd(keyArguments.keyName, members);\n        } else {\n            throw new Error('keyName is required to populate the sorted set.');\n        }\n\n    } catch (error) {\n        console.error('Error adding members to sorted set:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\n/**\n * Delete all keys from database\n * @param host The host of database\n * @param port The port of database\n */\nexport async function deleteAllKeysFromDB(host: string, port: string): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({\n        url,\n        socket: {\n            connectTimeout: 10000\n        }\n    });\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error', error);\n        throw error;\n    });\n\n    try {\n        await client.connect();\n\n        await client.flushAll();\n\n    } catch (error) {\n        console.error('Error flushing database:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\nexport async function populateBigData(host: string, port: string): Promise<void> {\n    const url = `redis://default@${host}:${port}`;\n    const client = createClient({\n        url,\n        socket: {\n            connectTimeout: 10000\n        }\n    });\n\n    client.on('error', (error: Error) => {\n        console.error('Redis Client Error', error);\n    });\n\n    try {\n        await populateDb(client, {\n            mainKeysLimit: 50_000, // 50_000 main keys, default 500_000\n            secondaryKeysLimit: 12_500, // 12_500 secondary keys, default 125_000\n        });\n        await populateBigKeys(client);\n    } catch (error) {\n        console.error('Error populating database:', error);\n    } finally {\n        await client.disconnect();\n    }\n}\n\n/**\n* Verifying if the Keys are in the List of keys\n* @param keyNames The names of the keys\n* @param isDisplayed True if keys should be displayed\n*/\nexport async function verifyKeysDisplayingInTheList(keyNames: string[], isDisplayed: boolean): Promise<void> {\n    for (const keyName of keyNames) {\n        isDisplayed\n            ? await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok(`The key ${keyName} not found`)\n            : await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`);\n    }\n}\n\n/**\n* Verify search/filter value\n* @param value The value in search/filter input\n*/\nexport async function verifySearchFilterValue(value: string): Promise<void> {\n    await t.expect(browserPage.filterByPatterSearchInput.withAttribute('value', value).exists).ok(`Filter per key name ${value} is not applied/correct`);\n}\n"
  },
  {
    "path": "tests/e2e/helpers/notifications.ts",
    "content": "import { workingDirectory } from '../helpers/conf';\nimport { NotificationParameters } from '../pageObjects/components/navigation/notification-panel';\n\nconst dbPath = `${workingDirectory}/redisinsight.db`;\n\nconst sqlite3 = require('sqlite3').verbose();\n\n/**\n * Delete all the notifications from local DB\n */\nexport function deleteAllNotificationsFromDB(): void {\n    const db = new sqlite3.Database(dbPath);\n    db.run('DELETE from notification', function(err: { message: string }) {\n        if (err) {\n            return console.log(`error during notification deletion: ${err.message}`);\n        }\n    });\n    db.close();\n}\n\n/**\n * Insert specified notification to local  DB\n * @param notifications Array with notification data\n */\nexport function insertNotificationInDB(notifications: NotificationParameters[]): void {\n    const db = new sqlite3.Database(dbPath);\n    let query = 'insert into notification (\"type\", \"timestamp\", \"title\", \"body\", \"read\") values';\n    for (let i = 0; i < notifications.length; i++) {\n        const messageWithQuotes = `${notifications[i].type}, ${notifications[i].timestamp},\n        ${notifications[i].title}, ${notifications[i].body}, ${notifications[i].isRead}`;\n        if (i === notifications.length - 1) {\n            query = `${query} (${messageWithQuotes})`;\n        }\n        else {\n            query = `${query} (${messageWithQuotes}),`;\n        }\n    }\n    db.run(query, function(err: { message: string }) {\n        if (err) {\n            return console.log(`error during notification creation: ${err.message}`);\n        }\n    });\n    db.close();\n}\n"
  },
  {
    "path": "tests/e2e/helpers/pub-sub.ts",
    "content": "import { t } from 'testcafe';\nimport { PubSubPage } from '../pageObjects';\n\nconst pubSubPage = new PubSubPage();\n\n/**\n * Verify message is/not displayed in pubsub\n * @param message The message text\n * @param displayed Boolean - displayed or not\n */\nexport async function verifyMessageDisplayingInPubSub(message: string, displayed: boolean): Promise<void> {\n    const messageByText = pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText(message);\n    displayed\n        ? await t.expect(messageByText.exists).ok(`\"${message}\" Message is not displayed`, { timeout: 5000 })\n        : await t.expect(messageByText.exists).notOk(`\"${message}\" Message is still displayed`);\n}\n"
  },
  {
    "path": "tests/e2e/helpers/scripts/browser-scripts.ts",
    "content": "import { exec } from 'child_process';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport CDP from 'chrome-remote-interface';\nimport { promisify } from 'util';\nimport { Common } from '../common';\n\ninterface Target {\n    type: string;\n    url: string;\n}\nconst execPromise = promisify(exec);\n\n/**\n * Close Chrome browser instance\n */\nexport async function closeChrome(): Promise<void> {\n    console.log('Closing Chrome...');\n    try {\n        const { stdout, stderr } = await execPromise(`pkill chrome`);\n        console.log('Chrome closed successfully. stdout:', stdout);\n        if (stderr) {\n            console.error('stderr:', stderr);\n        }\n    } catch (error) {\n        console.error('Error closing Chrome:', error);\n    }\n}\n\n/**\n * Open a new Chrome browser instance\n */\nexport async function openChromeWindow(): Promise<void> {\n    const { isMac, isLinux } = Common.getPlatform();\n\n    if (isMac) {\n        await execPromise(`open -na \"Google Chrome\" --args --new-window`);\n        console.log('Chrome opened on Mac');\n    } else if (isLinux) {\n        console.log('Opening Chrome on Linux...');\n        try {\n            exec(`google-chrome --remote-debugging-port=9223 --disable-gpu --disable-search-engine-choice-screen --disable-dev-shm-usage --disable-software-rasterizer --enable-logging --disable-extensions --no-default-browser-check --disable-default-apps --disable-domain-reliability --disable-web-security --no-sandbox --remote-allow-origins=* --disable-popup-blocking about:blank &`, (error, stdout, stderr) => {\n                if (error) {\n                    console.error(`Error launching Chrome: ${error}`);\n                } else {\n                    console.log(\"Chrome started successfully in the background.\");\n                }\n            });\n        } catch (error) {\n            console.error(\"Error occurred in execSync:\", error);\n            return;\n        }\n\n        // Check if Chrome is running after opening it\n        const isChromeRunning = await waitForChromeProcess();\n        if (isChromeRunning) {\n            console.log('Chrome is running.');\n        } else {\n            console.error('Chrome process not found after attempting to launch.');\n        }\n    }\n}\n\n/**\n * Waiting for chrome process start\n * @param maxWaitTime Max waiting time\n * @param interval Interval between check\n */\nasync function waitForChromeProcess(maxWaitTime = 10000, interval = 1000): Promise<boolean> {\n    const start = Date.now();\n    while (Date.now() - start < maxWaitTime) {\n        try {\n            const { stdout } = await execPromise(`pgrep \"chrome\"`);\n            if (stdout.trim()) {\n                return true;\n            }\n        } catch (error) {\n            // Ignore errors, Chrome may not be running yet\n        }\n        await new Promise(resolve => setTimeout(resolve, interval));\n    }\n    return false;\n}\n\n/**\n * Retrieve opened tab in Google Chrome using Chrome DevTools Protocol\n * @param urlSubstring Optional substring to match in the URL\n * @returns Promise<string> Resolves to the URL of the opened tab\n */\nexport async function getOpenedChromeTab(urlSubstring?: string): Promise<string> {\n    const { isMac, isLinux } = Common.getPlatform();\n    const maxRetries = 30;\n    const retryDelay = 400;\n    const chromeDebuggingPort = 9223;\n\n    if (isMac) {\n        const scriptPath = path.join(__dirname, 'get_chrome_tab_url.applescript');\n        return new Promise((resolve, reject) => {\n            exec(`osascript ${scriptPath}`, (error, stdout) => {\n                if (error) {\n                    console.error('Error retrieving tabs and windows on macOS:', error);\n                    reject(error);\n                    return;\n                }\n                resolve(stdout.trim());\n            });\n        });\n    } else if (isLinux) {\n        for (let attempts = 0; attempts < maxRetries; attempts++) {\n            console.log(`Attempting to connect to Chrome DevTools (Attempt: ${attempts + 1}/${maxRetries})...`);\n\n            try {\n                const targets = await new Promise<Target[]>((resolve, reject) => {\n                    CDP.List({ port: chromeDebuggingPort }, (err, targets) => {\n                        if (err) {\n                            console.error('Error connecting to Chrome with CDP:', err);\n                            reject(err);\n                        } else {\n                            resolve(targets);\n                        }\n                    });\n                });\n\n                const pageTargets = targets.filter(target => target.type === 'page');\n                console.log(`Found ${pageTargets.length} open tabs in Chrome`);\n                console.log(`Found ${targets[0].url} url open tabs in Chrome`);\n\n                // Check for a new tab matching criteria\n                const newTab = pageTargets.find(target =>\n                    (urlSubstring && target.url.includes(urlSubstring)) ||\n                    target.url.includes('authorize?')\n                );\n\n                if (newTab) {\n                    console.log('Correct tab found:', newTab.url);\n                    return newTab.url;\n                } else {\n                    console.log('No matching tab found, retrying...');\n                }\n            } catch (err) {\n                console.error('Error during Chrome connection attempt:', err);\n            }\n\n            // Wait before the next attempt\n            await new Promise(resolve => setTimeout(resolve, retryDelay));\n        }\n\n        throw new Error('No new tab matching criteria was found within the maximum attempts.');\n    } else {\n        throw new Error('Unsupported operating system: ' + process.platform);\n    }\n}\n\n/**\n * Save opened chrome tab URL to file\n * @param logsFilePath The path to the file with logged URL\n * @param timeout The timeout for monitoring Chrome tabs\n */\nexport async function saveOpenedChromeTabUrl(logsFilePath: string, timeout = 100): Promise<void> {\n    await new Promise(resolve => setTimeout(resolve, timeout));\n    try {\n        const url = await getOpenedChromeTab();\n        fs.writeFileSync(logsFilePath, url, 'utf8');\n    } catch (err) {\n        console.error('Error saving logs:', err);\n    }\n}\n\n/**\n * Close Chrome browser instance\n */\nexport async function openChromeOnCi(): Promise<void> {\n    await openChromeWindow();\n    await new Promise(resolve => setTimeout(resolve, 1000));\n    await closeChrome();\n    await new Promise(resolve => setTimeout(resolve, 1000));\n    await openChromeWindow();\n    await new Promise(resolve => setTimeout(resolve, 1000));\n}\n"
  },
  {
    "path": "tests/e2e/helpers/scripts/close_chrome_tab.applescript",
    "content": "tell application \"Google Chrome\"\n\tset windowList to every tab of every window whose URL starts with \"https://www.example.com\"\n\trepeat with tabList in windowList\n\t\tset tabList to tabList as any\n\t\trepeat with tabItr in tabList\n\t\t\tset tabItr to tabItr as any\n\t\t\tdelete tabItr\n\t\tend repeat\n\tend repeat\nend tell"
  },
  {
    "path": "tests/e2e/helpers/scripts/generate-big-data.ts",
    "content": "import { isNull, isNumber } from 'lodash';\nimport RedisClient from '@redis/client/dist/lib/client';\n\n\nconst iterationsPrimary = 500_000;\nconst iterationsSecondary = 125_000;\nconst batchSizeDefault = 10_000;\n\ntype CommandType = [cmd: string, ...args: (string | number)[]];\ntype CommandsType = CommandType[];\n\nfunction prepareCommandArgs(args: CommandType) {\n  const strArgs = args.map((arg) => (isNumber(arg) ? arg.toString() : arg)) as string[];\n  if (!strArgs || !strArgs.length) {\n    return [];\n  }\n  const cmdArg = strArgs.shift() || '';\n  return [...cmdArg.split(' '), ...strArgs];\n}\n\nfunction prepareCommandOptions(options: {\n  replyEncoding?: string;\n}): any {\n  let replyEncoding: string | null = null;\n\n  if (options?.replyEncoding === 'utf8') {\n    replyEncoding = 'utf8';\n  }\n\n  return {\n    returnBuffers: isNull(replyEncoding),\n  };\n}\n\nasync function sendCommand(client: RedisClient<any, any, any>, command: CommandType, options ?: any) {\n  let commandArgs = prepareCommandArgs(command);\n  return client.sendCommand(commandArgs, prepareCommandOptions(options));\n}\n\nasync function sendPipeline(\n  client: RedisClient<any, any, any>,\n  commands: CommandsType,\n  options?: any,\n) {\n  return Promise.all(\n    commands.map(\n      (cmd) => sendCommand(client, cmd, options)\n        .then((res: any) => [null, res])\n        .catch((e: any) => [e, null]),\n    ),\n  );\n}\n\nfunction* generateBigData(baseKey: string, separator: string, limit: number, batchSize = batchSizeDefault) {\n  const keyTypes = [\n    'string', 'json', 'hash', 'list', 'set', 'zset',\n  ];\n  let sent = 0;\n  while (sent < limit) {\n    const commands: CommandsType = [];\n    for (let i = 0; i < batchSize && sent < limit; i++) {\n      sent += 1;\n      for (const keyType of keyTypes) {\n        const keyName = `${baseKey}${separator}${sent}${separator}${keyType}`;\n        let command: CommandType;\n        switch (keyType) {\n          case 'json':\n            command = ['json.set', keyName, '$', JSON.stringify({ id: sent })];\n            break;\n          case 'hash':\n            command = ['hset', keyName, 'k0', sent];\n            break;\n          case 'list':\n            command = ['lpush', keyName, sent];\n            break;\n          case 'set':\n            command = ['sadd', keyName, sent];\n            break;\n          case 'zset':\n            command = ['zadd', keyName, 0, sent];\n            break;\n          case 'string':\n          default:\n            command = ['set', keyName, `${sent}`];\n            break;\n        }\n        commands.push(command);\n      }\n    }\n    yield commands;\n  }\n}\n\nconst SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\nconst toBytes = (size: number, type: string): number => {\n  const key = SIZES.indexOf(type.toUpperCase());\n\n  return Math.floor(size * 1024 ** key);\n};\n\nfunction generateRepeatedString(char: string = 'a', size: number | string = '10KB'): string {\n  let bytes: number = 0;\n  if (typeof size === 'string') {\n    const unit = size.slice(-2).toUpperCase();\n    const value = parseInt(size.slice(0, -2), 10);\n    if (!SIZES.includes(unit)) {\n      console.warn(`Invalid unit ${unit}, expected one of ${SIZES}`);\n      bytes = value;\n    } else {\n      bytes = toBytes(value, unit);\n    }\n  } else if (typeof size === 'number') {\n    bytes = size;\n  }\n\n  return char.repeat(bytes);\n}\n\nasync function seedBigKeys(\n  client: RedisClient<any, any, any>,\n  keyName: string,\n  command = 'set',\n  generateCommand: (i: number) => (string | number) | (string | number)[],\n  limit = 1_000_001, batchSize = 100,\n) {\n  const bigCommands: CommandsType = [];\n  let batchCommands: (string | number)[] = [];\n  for (let i = 1; i < limit; i++) {\n    const items = generateCommand(i);\n    if (Array.isArray(items)) {\n      batchCommands.push(...items);\n    } else {\n      batchCommands.push(items);\n    }\n    if (i % batchSize === 0) {\n      bigCommands.push([command, keyName, ...batchCommands]);\n      batchCommands = [];\n    }\n  }\n  await sendPipeline(client, bigCommands);\n}\n\n/**\n * Populate big keys in Redis Database\n *\n * Generates a range of keys and values to demonstrate large data sets.\n *\n * @param client - The Redis client to use.\n * @param withBigStrings - If `true`, generates big string keys as well.\n * @returns A promise that resolves when the keys are populated.\n */\nexport const populateBigKeys = async (client: RedisClient<any, any, any>, withBigStrings = false) => {\n  const bigStrings = [\n    {\n      char: 'a',\n      size: '1MB',\n      content: '1MB key',\n    },\n    {\n      char: 'b',\n      size: '2MB',\n      content: '2MB key',\n    },\n    {\n      char: 'c',\n      size: '3MB',\n      content: '3MB key',\n    },\n    {\n      char: 'd',\n      size: '4MB',\n      content: '4MB key',\n    },\n    {\n      char: 'e',\n      size: '5MB',\n      content: '5MB key',\n    },\n  ];\n  try {\n    console.log('Starting big keys...');\n    await client.connect();\n    if (withBigStrings) {\n      console.log('Generating big string keys...');\n      const bigKeyStringCommands: CommandsType = [];\n      for (const {\n        char,\n        size,\n        content\n      } of bigStrings) {\n        const key = generateRepeatedString(char, size);\n        bigKeyStringCommands.push(['set', key, content]);\n      }\n      await sendPipeline(client, bigKeyStringCommands);\n    }\n    // big string 5M\n    console.log('Generating 5 MB string key...');\n    const bigStringKey = 'big string 5MB';\n    await sendCommand(client, ['set', bigStringKey, generateRepeatedString('e', '5MB')]);\n\n    // big hash 1M\n    console.log('Generating 1_000_000 fields hash key...');\n    await seedBigKeys(client, 'big hash 1M', 'hset', (i) => [`key${i}`, i], 1_000_001, 100);\n    // big list 1M\n    console.log('Generating 1_000_000 items list key...');\n    await seedBigKeys(client, 'big list 1M', 'lpush', (i) => i, 1_000_001, 100);\n    // big set 1M\n    console.log('Generating 1_000_000 items set key...');\n    await seedBigKeys(client, 'big set 1M', 'sadd', (i) => i, 1_000_001, 100);\n    // big zset 1M\n    console.log('Generating 1_000_000 items zset key...');\n    await seedBigKeys(client, 'big zset 1M', 'zadd', (i) => [i, i], 1_000_001, 100);\n    console.log('Done');\n  } catch (e) {\n    console.error(e);\n  } finally {\n    await client.disconnect();\n  }\n};\n\ntype PopulateDbOptionsType = {\n  mainKeysLimit?: number;\n  secondaryKeysLimit?: number;\n  separatorPrimary?: string;\n  separatorSecondary?: string;\n  baseKeys?: string[];\n  secondaryKeys?: string[];\n}\n\n/**\n * Populate Redis database with data.\n *\n * Populates Redis database with data in format of:\n * - Primary key: `device:eu-east-1:1`, `device:eu-east-1:2`, ... `device:eu-east-1:1000`\n * - Secondary key: `device_eu-east-1_1`, `device_eu-east-1_2`, ... `device_eu-east-1_1000`\n *\n * @param client - Redis client object\n * @param {PopulateDbOptionsType} options - Options object\n */\nexport const populateDb = async (\n  client: RedisClient<any, any, any>,\n  {\n    mainKeysLimit = iterationsPrimary,\n    secondaryKeysLimit = iterationsSecondary,\n    baseKeys = [\n      'device', 'mobile', 'user',\n    ],\n    secondaryKeys = [\n      'eu-east-1', 'eu-west-1', 'us-east-1', 'us-west-1',\n    ],\n    separatorPrimary = ':',\n    separatorSecondary = '_',\n  }: PopulateDbOptionsType,\n): Promise<void> => {\n\n  try {\n    console.log('Starting...');\n    client.on('error', err => console.log('Redis Client Error', err));\n    let executions = 0;\n    await client.connect();\n    for (let bk of baseKeys) {\n      const generator = generateBigData(bk, separatorPrimary, mainKeysLimit);\n      for (const commands of generator) {\n        // process the commands\n        await sendPipeline(client, commands);\n        console.log(`${bk}: ${++executions}`);\n      }\n\n      for (let sk of secondaryKeys) {\n        const generator = generateBigData(`${bk}${separatorSecondary}${sk}`, separatorSecondary, secondaryKeysLimit);\n        for (const commands of generator) {\n          // process the commands\n          await sendPipeline(client, commands);\n          console.log(`${bk}${separatorSecondary}${sk}: ${++executions}`);\n        }\n      }\n    }\n\n    console.log('Done');\n  } catch (e) {\n    console.error(e);\n  } finally {\n    await client.disconnect();\n  }\n};\n// const host = '127.0.0.1';\n// const port = '6666';\n// const port = '8103';\n// const url = `redis://default@${host}:${port}`;\n\n// const client: RedisClient<any, any, any> = createClient({url});\n//\n// const remoteClient = createClient({\n//   username: 'default',\n//   password: 'Lg7qA8JPsOcBE8Em7e9fSRcHHHvsNpP7',\n//   socket: {\n//     host: 'redis-13690.crce8.us-east-1-mz.ec2.qa-cloud.redislabs.com',\n//     port: 13690\n//   }\n// });\n// populateBigKeys(client, true).then(() => {\n//   console.log('Populating DB...');\n//   return populateDb(client, iterationsPrimary, iterationsSecondary);\n// });\n"
  },
  {
    "path": "tests/e2e/helpers/scripts/get_chrome_tab_url.applescript",
    "content": "tell application \"System Events\"\ntell application \"Google Chrome\" to return URL of active tab of front window\nend tell"
  },
  {
    "path": "tests/e2e/helpers/sso-authorization.ts",
    "content": "import { connect } from 'puppeteer-real-browser'\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { exec } from 'child_process';\nimport { samlUser, samlUserPassword } from './conf';\nimport { MyRedisDatabasePage, SsoAuthorizationPage } from '../pageObjects';\nimport { Common } from './common';\nimport { closeChrome, openChromeOnCi, saveOpenedChromeTabUrl } from './scripts/browser-scripts';\nimport { t } from 'testcafe';\nimport { AiChatBotPanel } from '../pageObjects/components/chatbot/ai-chatbot-panel';\n\nexport class SsoAuthorization {\n    /**\n     * Process SSO authorization using Puppeteer\n     * @param urlToUse The url to process authorization\n     * @param authorizationType The type of SSO authorization\n     */\n    static async processSSOPuppeteer(urlToUse: string, authorizationType: 'Google' | 'Github' | 'SAML'): Promise<void> {\n        const ssoAuthorizationPage = new SsoAuthorizationPage();\n        const { browser, page } = await connect({\n            headless: false,\n            args: [],\n            customConfig: {},\n            turnstile: true,\n            connectOption: {},\n            disableXvfb: true,\n            ignoreAllFlags: false,\n        })\n\n        try {\n            await ssoAuthorizationPage.signInUsingSso(authorizationType, page, urlToUse, samlUser, samlUserPassword);\n\n            const currentUrl = page.url();\n            const parts = currentUrl.split('?');\n            const modifiedUrl = parts.length > 1 ? parts[1] : currentUrl;\n\n            const protocol = 'redisinsight://';\n            const callbackUrl = 'cloud/oauth/callback';\n            const redirectUrl = `${protocol}${callbackUrl}?${modifiedUrl}`;\n\n            await this.openRedisInsightWithDeeplink(redirectUrl);\n        } catch (error) {\n            console.error('Error during SSO:', error);\n            // Take a screenshot if there's an error\n            fs.mkdirSync('./report/screenshots/', { recursive: true });\n            const screenshot = await page.screenshot();\n            fs.writeFileSync(`./report/screenshots/puppeteer_screenshot_${Common.generateWord(5)}.png`, screenshot, 'base64');\n            throw error;\n        } finally {\n            await browser.close();\n        }\n    }\n\n    /**\n     * Helper function for waiting for timeout\n     */\n    static async waitForTimeout(ms: number) {\n        return new Promise(resolve => setTimeout(resolve, ms));\n    }\n\n    /**\n     * Open Redis Insight electron app using deeplink\n     * @param redirectUrl The redirect url for deeplink\n     */\n    static async openRedisInsightWithDeeplink(redirectUrl: string) {\n        if (process.platform === 'linux') {\n            console.log('redirectUrl: ', redirectUrl);\n            exec(`xdg-open \"${redirectUrl}\"`, (error, stdout, stderr) => {\n                if (error) {\n                    console.error('Error opening Redis Insight on Linux:', error);\n                    return;\n                }\n                console.log('Redis Insight opened successfully:', stdout);\n            });\n        } else {\n            const open = (await import('open')).default;\n            await open(redirectUrl, { app: { name: 'Redis Insight' } });\n        }\n    }\n\n    /**\n     * Sign in using SAML SSO\n     * @param urlToUse The url to process authorization\n     */\n    static async signInThroughSamlSso(urlToUse: string): Promise<void> {\n        const myRedisDatabasePage = new MyRedisDatabasePage();\n        const aiChatBotPanel = new AiChatBotPanel();\n        const logsWithUrlFilePath = path.join('test-data', 'chrome_logs.txt');\n\n        await openChromeOnCi();\n        await t.click(myRedisDatabasePage.NavigationHeader.copilotButton);\n        await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement);\n        await t.click(aiChatBotPanel.RedisCloudSigninPanel.ssoOauthButton);\n        await t.typeText(aiChatBotPanel.RedisCloudSigninPanel.ssoEmailInput, samlUser, { replace: true, paste: true });\n    \n        await t.wait(2000);\n        await t.click(aiChatBotPanel.RedisCloudSigninPanel.submitBtn);\n        await saveOpenedChromeTabUrl(logsWithUrlFilePath);\n    \n        await t.wait(2000);\n        urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath);\n        await t.expect(urlToUse).contains('authorize?');\n        await closeChrome();\n        await t.wait(2000);\n        await this.processSSOPuppeteer(urlToUse, 'SAML');\n        await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 });\n        await myRedisDatabasePage.reloadPage();\n        await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed');\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/telemetry.ts",
    "content": "import { RequestLogger, t } from 'testcafe';\n\nexport class Telemetry {\n    /**\n     * Create new logger\n     */\n    createLogger(): RequestLogger {\n        const options = { logRequestBody: true, logRequestHeaders: true, stringifyRequestBody: true };\n        const logger = RequestLogger(/.*/, options);\n        return logger;\n    }\n\n    /**\n     * Wait for telemetry event request sent by its name\n     * @param eventName The telemetry event name\n     * @param logger The logger object\n     */\n    async waitForEventRequestSentByName(eventName: string, logger: any): Promise<LoggedRequest> {\n        const request = logger.requests.find(request => {\n            const requestBody = request.request.body.toString();\n            if (!requestBody) {\n                return false;\n            } // make sure the request body is not empty\n            try {\n                const requestBodyJson = JSON.parse(requestBody);\n                return requestBodyJson.event === eventName;\n            }\n            catch (error) {\n                console.error(`Failed to parse JSON in request body: ${error}`);\n                return false;\n            }\n        });\n\n        await t.expect(request).ok(`${eventName} Event not found or does not have expected body`);\n        return request!;\n    }\n\n    /**\n     * Verify that event has properties\n     * @param eventName The telemetry event name\n     * @param properties The telemetry event properties\n     * @param logger The logger object\n     */\n    async verifyEventHasProperties(eventName: string, properties: string[], logger: any): Promise<void> {\n        // Extract the request body as JSON\n        const request = await this.waitForEventRequestSentByName(eventName, logger);\n        const requestBody = JSON.parse(request.request.body.toString());\n\n        // Verify that event has all properties\n        for (const property of properties) {\n            t.expect(requestBody).hasOwnProperty(property);\n        }\n    }\n\n    /**\n     * Verify that event has property with value\n     * @param eventName The telemetry event name\n     * @param property The telemetry event property\n     * @param value The property value\n     * @param logger The logger object\n     */\n    async verifyEventPropertyValue(eventName: string, property: string, value: string, logger: any): Promise<void> {\n        // Extract the request body as JSON\n        const request = await this.waitForEventRequestSentByName(eventName, logger);\n        const requestBody = JSON.parse(request.request.body.toString());\n\n        // Verify that event has correct property value\n        await t.expect(String(requestBody.eventData[property])).eql(value);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/helpers/utils.ts",
    "content": "import { ClientFunction } from 'testcafe';\n\nexport const goBackHistory = ClientFunction(() => window.history.back());\n\nexport const createTimeout = (errorMessage: string, timeout: number): Promise<any> =>\n    new Promise((_, reject) => {\n        setTimeout(() => {\n            reject(new Error(errorMessage));\n        }, timeout);\n    });\n"
  },
  {
    "path": "tests/e2e/local.web.docker-compose.yml",
    "content": "version: \"3.4\"\n\nservices:\n  e2e:\n    profiles:\n      - e2e\n    build:\n      context: .\n      dockerfile: e2e.Dockerfile\n    tty: true\n    volumes:\n      - ./results:/usr/src/app/results\n      - ./report:/usr/src/app/report\n      - ./plugins:/usr/src/app/plugins\n      - rihomedir:/root/.redis-insight\n      - tmp:/tmp\n      - ./remote:/root/remote\n      # - ./rdi:/root/rdi\n    env_file:\n      - ./.env\n    entrypoint: [\n        './upload-custom-plugins.sh',\n    ]\n    environment:\n      TEST_FILES: $TEST_FILES\n      E2E_CLOUD_DATABASE_HOST: $E2E_CLOUD_DATABASE_HOST\n      E2E_CLOUD_DATABASE_PORT: $E2E_CLOUD_DATABASE_PORT\n      E2E_CLOUD_DATABASE_PASSWORD: $E2E_CLOUD_DATABASE_PASSWORD\n      E2E_CLOUD_DATABASE_USERNAME: $E2E_CLOUD_DATABASE_USERNAME\n      E2E_CLOUD_DATABASE_NAME: $E2E_CLOUD_DATABASE_NAME\n      E2E_CLOUD_API_ACCESS_KEY: $E2E_CLOUD_API_ACCESS_KEY\n      E2E_CLOUD_API_SECRET_KEY: $E2E_CLOUD_API_SECRET_KEY\n      REMOTE_FOLDER_PATH: \"/root/remote\"\n    command: [\n        './wait-for-it.sh', 'redis-enterprise:12000', '-s', '-t', '120',\n        '--',\n        'npm', 'run', 'test:chrome:ci'\n    ]\n\n  # Built image\n  app:\n    logging:\n      driver: none\n    image: redisinsight:amd64\n    env_file:\n      - ./.env\n    environment:\n      RI_ENCRYPTION_KEY: $RI_ENCRYPTION_KEY\n      RI_SERVER_TLS_CERT: $RI_SERVER_TLS_CERT\n      RI_SERVER_TLS_KEY: $RI_SERVER_TLS_KEY\n      BUILD_TYPE: DOCKER_ON_PREMISE\n    volumes:\n      - ./rihomedir:/data\n      - tmp:/tmp\n      - ./test-data:/test-data\n    ports:\n      - 5540:5540\n\nvolumes:\n  tmp:\n  rihomedir:\n"
  },
  {
    "path": "tests/e2e/package.json",
    "content": "{\n  \"name\": \"redisinsight\",\n  \"version\": \"1.0.0\",\n  \"description\": \"End-to-end tests\",\n  \"private\": true,\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test:live\": \"testcafe --live chrome \",\n    \"start:api\": \"cross-env yarn --cwd ../../redisinsight/api start:prod\",\n    \"build:api\": \"yarn --cwd ../../redisinsight/api build:prod\",\n    \"build:statics\": \"cross-env E2E=true yarn --cwd ../../ build:statics\",\n    \"build:statics:win\": \"cross-env E2E=true yarn --cwd ../../ build:statics:win\",\n    \"build:ui\": \"yarn --cwd ../../ build:ui\",\n    \"redis:last\": \"docker run --name redis-last-version -p 7777:6379 -d redislabs/redismod\",\n    \"start:app\": \"cross-env yarn start:api\",\n    \"test:chrome\": \"ts-node ./web.runner.ts\",\n    \"test:chrome:ci\": \"ts-node ./web.runner.ci.ts\",\n    \"test\": \"yarn test:chrome\",\n    \"lint\": \"eslint . --ext .ts,.js,.tsx,.jsx\",\n    \"test:desktop:ci\": \"ts-node ./desktop.runner.ci.ts\",\n    \"test:desktop:ci:win\": \"ts-node ./desktop.runner.win.ts\",\n    \"test:desktop\": \"ts-node ./desktop.runner.ts\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"dependencies\": {\n    \"axios\": \"^1.13.5\",\n    \"brotli-unicode\": \"^1.0.2\",\n    \"cli-argument-parser\": \"0.7.4\",\n    \"fflate\": \"^0.8.2\",\n    \"js-yaml\": \"^4.1.1\",\n    \"lz4js\": \"^0.2.0\",\n    \"msgpackr\": \"^1.11.0\",\n    \"protobufjs\": \"^7.4.0\",\n    \"puppeteer\": \"^23.7.0\",\n    \"puppeteer-extra\": \"^3.3.6\",\n    \"puppeteer-real-browser\": \"^1.3.17\"\n  },\n  \"resolutions\": {\n    \"@types/lodash\": \"4.14.192\",\n    \"@types/node\": \"20.3.1\",\n    \"word-wrap\": \"1.2.4\",\n    \"**/semver\": \"^7.5.2\",\n    \"testcafe-browser-provider-electron/**/ip\": \"^2.0.1\"\n  },\n  \"devDependencies\": {\n    \"@types/archiver\": \"^6.0.2\",\n    \"@types/chance\": \"1.1.6\",\n    \"@types/chrome-remote-interface\": \"^0.31.14\",\n    \"@types/edit-json-file\": \"1.7.3\",\n    \"@types/fs-extra\": \"11.0.4\",\n    \"@types/sqlite3\": \"^3.1.11\",\n    \"@types/supertest\": \"^6.0.2\",\n    \"@typescript-eslint/eslint-plugin\": \"8.9.0\",\n    \"@typescript-eslint/parser\": \"8.9.0\",\n    \"archiver\": \"^7.0.1\",\n    \"chance\": \"1.1.12\",\n    \"chromedriver\": \"^135.0.2\",\n    \"cli-argument-parser\": \"0.7.4\",\n    \"cross-env\": \"^7.0.3\",\n    \"date-fns\": \"^4.1.0\",\n    \"dotenv-cli\": \"^7.4.2\",\n    \"edit-json-file\": \"1.8.0\",\n    \"eslint\": \"9.12.0\",\n    \"eslint-plugin-import\": \"2.31.0\",\n    \"fs-extra\": \"^11.2.0\",\n    \"open\": \"^10.1.0\",\n    \"redis\": \"4.7.0\",\n    \"sqlite3\": \"^5.1.7\",\n    \"supertest\": \"^7.0.0\",\n    \"testcafe\": \"3.7.0\",\n    \"testcafe-browser-provider-electron\": \"0.0.21\",\n    \"testcafe-reporter-html\": \"1.4.6\",\n    \"testcafe-reporter-json\": \"2.2.0\",\n    \"testcafe-reporter-spec\": \"2.2.0\",\n    \"ts-node\": \"10.9.2\",\n    \"typescript\": \"5.6.3\"\n  }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/auto-discover-redis-enterprise-databases.ts",
    "content": "import { Selector } from 'testcafe';\nimport { BasePage } from './base-page';\n\nexport class AutoDiscoverREDatabases extends BasePage {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    addSelectedDatabases = Selector('[data-testid=btn-add-databases]');\n    databaseCheckbox = Selector('[data-test-subj^=checkboxSelectRow]');\n    search = Selector('[data-testid=search]');\n    viewDatabasesButton = Selector('[data-testid=btn-view-databases]');\n    //TEXT INPUTS (also referred to as 'Text fields')\n    title = Selector('[data-testid=title]');\n    databaseName = Selector('[data-testid^=db_name_]', { timeout: 15000 });\n\n    // Get databases name\n    async getDatabaseName(): Promise<string> {\n        return this.databaseName.textContent;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/base-overview-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { RedisOverviewPage } from '../helpers/constants';\nimport { Toast } from './components/common/toast';\nimport { ShortcutsPanel } from './components/shortcuts-panel';\nimport { EditorButton } from './components/common/editorButton';\nimport { NavigationHeader } from './components/navigation/navigation-header';\nimport { Modal } from './components/common/modal';\n\nexport class BaseOverviewPage {\n    ShortcutsPanel = new ShortcutsPanel();\n    Toast = new Toast();\n    EditorButton = new EditorButton();\n    NavigationHeader = new NavigationHeader();\n    Modal = new Modal();\n\n    notification = Selector('[data-testid^=-notification]');\n    deleteRowButton = Selector('[data-testid^=delete-instance-]');\n    editRowButton = Selector('[data-testid^=edit-instance-]');\n    confirmDeleteButton = Selector('[data-testid^=delete-instance-]').withExactText('Remove');\n    confirmDeleteAllDbButton = Selector('[data-testid=delete-selected-dbs]');\n\n    instanceRow = Selector('[class*=euiTableRow-isSelectable]');\n\n    selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]');\n    deleteButtonInPopover = Selector('#deletePopover button');\n\n    databasePageLink = Selector('[data-testid=home-tab-databases]');\n    rdiPageLink = Selector('[data-testid=home-tab-rdi-instances]');\n    exploreRedisBtn = Selector('[data-testid=explore-redis-btn]');\n\n    /**\n     * Reload page\n     */\n    async reloadPage(): Promise<void> {\n        await t.eval(() => location.reload());\n    }\n\n    async setActivePage(type: RedisOverviewPage): Promise<void> {\n\n        if(type === RedisOverviewPage.Rdi) {\n            await t.click(this.rdiPageLink);\n        }\n        else {\n            await t.click(this.databasePageLink);\n        }\n    }\n\n    /**\n     * Delete instances\n     */\n    async deleteAllInstance(): Promise<void> {\n        const rows = this.instanceRow;\n        const count = await rows.count;\n        if (count > 1) {\n            await t\n                .click(this.selectAllCheckbox)\n                .click(this.deleteButtonInPopover)\n                .click(this.confirmDeleteAllDbButton);\n        }\n        else if (count === 1) {\n            await t\n                .click(this.deleteRowButton)\n                .click(this.confirmDeleteButton);\n        }\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n    }\n\n    /**\n     * Get all databases from List of DBs page\n     * @param actualList Actual list\n     * @param sortedList Expected list\n     */\n    async compareInstances(actualList: string[], sortedList: string[]): Promise<void> {\n        for (let k = 0; k < actualList.length; k++) {\n            await t.expect(actualList[k].trim()).eql(sortedList[k].trim());\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/base-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { NavigationPanel } from './components/navigation-panel';\nimport { Toast } from './components/common/toast';\nimport { ShortcutsPanel } from './components/shortcuts-panel';\nimport { EditorButton } from './components/common/editorButton';\nimport { Modal } from './components/common/modal';\nimport {NavigationTabs} from './components/navigation-tabs'\n\nexport class BasePage {\n    notification = Selector('[data-testid^=-notification]');\n\n    NavigationTabs = new NavigationTabs();\n    NavigationPanel = new NavigationPanel();\n    ShortcutsPanel = new ShortcutsPanel();\n    Toast = new Toast();\n    EditorButton = new EditorButton();\n    Modal = new Modal();\n\n    /**\n     * Reload page\n     */\n    async reloadPage(): Promise<void> {\n        await t.eval(() => location.reload());\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/browser-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { Common } from '../helpers/common';\nimport { InstancePage } from './instance-page';\nimport { BulkActions, TreeView } from './components/browser';\nimport { AddElementInList } from '../helpers/constants';\nimport { createEnhancedSelector, EnhancedSelector } from './enhanced-selector';\n\nexport class BrowserPage extends InstancePage {\n    BulkActions = new BulkActions();\n    TreeView = new TreeView();\n\n    //CSS Selectors\n    cssSelectorGrid = '[aria-label=\"grid\"]';\n    cssSelectorRows = '[aria-label=\"row\"]';\n    cssSelectorKey = '[data-testid^=key-]';\n    cssFilteringLabel = '[data-testid=multi-search]';\n    cssJsonValue = '[data-testid=value-as-json]';\n    cssRowInVirtualizedTable = '[role=gridcell]';\n    cssVirtualTableRow = '[aria-label=row]';\n    cssKeyBadge = '[data-testid^=badge-]';\n    cssKeyTtl = '[data-testid^=ttl-]';\n    cssKeySize = '[data-testid^=size-]';\n    cssRemoveSuggestionItem = '[data-testid^=remove-suggestion-item-]';\n\n    //BUTTONS\n    applyButton = Selector('[data-testid=apply-btn]');\n    deleteKeyButton = Selector('[data-testid=delete-key-btn]');\n    submitDeleteKeyButton = Selector('[data-testid=submit-delete-key]');\n    confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]');\n    editKeyTTLButton = Selector('[data-testid=edit-ttl-btn]');\n    refreshKeysButton = Selector('[data-testid=keys-refresh-btn]');\n    refreshKeyButton = Selector('[data-testid=key-refresh-btn]');\n    editKeyNameButton = Selector('[data-testid=edit-key-btn]');\n    editKeyValueButton = Selector('[data-testid=edit-key-value-btn]', { timeout: 500 });\n    closeKeyButton = Selector('[data-testid=close-key-btn]');\n    plusAddKeyButton = Selector('[data-testid=btn-add-key]');\n    addKeyValueItemsButton = Selector('[data-testid=add-key-value-items-btn]');\n    saveHashFieldButton = Selector('[data-testid=save-fields-btn]');\n    saveMemberButton = Selector('[data-testid=save-members-btn]');\n    searchButtonInKeyDetails = Selector('[data-testid=search-button]');\n    addKeyButton = Selector('button').withExactText('Add Key');\n    keyTypeDropDown = Selector('[data-testid=select-key-type]');\n    confirmRemoveHashFieldButton = Selector('[role=dialog] [data-testid^=remove-hash-button]');\n    removeSetMemberButton = Selector('[data-testid^=set-remove-btn]');\n    removeHashFieldButton = Selector('[data-testid^=remove-hash-button]');\n    removeZsetMemberButton = Selector('[data-testid^=zset-remove-button]');\n    confirmRemoveSetMemberButton = Selector('[role=dialog] [data-testid^=set-remove-btn-]');\n    confirmRemoveZSetMemberButton = Selector('[role=dialog] [data-testid^=zset-remove-button-]');\n    saveElementButton = Selector('[data-testid=save-elements-btn]');\n    removeElementFromListIconButton = Selector('[data-testid=remove-key-value-items-btn]');\n    removeElementFromListButton = Selector('[data-testid=remove-elements-btn]');\n    confirmRemoveListElementButton = Selector('[data-testid=remove-submit]');\n    removeElementFromListSelect = Selector('[data-testid=destination-select]');\n    destinationSelect = Selector('[data-testid=destination-select]');\n    addJsonObjectButton = Selector('[data-testid=add-object-btn]');\n    addJsonFieldButton = Selector('[data-testid=add-field-btn]');\n    expandJsonObject = Selector('[data-testid=expand-object]');\n    scoreButton = Selector('[data-testid=score-button]');\n    sortingButton = Selector('[data-testid=header-sorting-button]');\n    editJsonObjectButton = Selector('[data-testid=edit-json-field]');\n    applyEditButton = Selector('[data-testid=apply-edit-btn]');\n    cancelEditButton = Selector('[data-testid=cancel-edit-btn]');\n    scanMoreButton = Selector('[data-testid=scan-more]');\n    resizeBtnKeyList = Selector('[data-test-subj=resize-btn-keyList-keyDetails]');\n    treeViewButton = Selector('[data-testid=view-type-list-btn]');\n    browserViewButton = Selector('[data-testid=view-type-browser-btn]');\n    searchButton = Selector('[data-testid=search-btn]');\n    clearFilterButton = Selector('[data-testid=reset-filter-btn]');\n    fullScreenModeButton = Selector('[data-testid=toggle-full-screen]');\n    closeRightPanel = Selector('[data-testid=close-right-panel-btn]');\n    addNewStreamEntry = Selector('[data-testid=add-key-value-items-btn]');\n    removeEntryButton = Selector('[data-testid^=remove-entry-button-]');\n    confirmRemoveEntryButton = Selector('[data-testid^=remove-entry-button-]').withExactText('Remove');\n    clearStreamEntryInputs = Selector('[data-testid=remove-item]');\n    saveGroupsButton = Selector('[data-testid=save-groups-btn]');\n    acknowledgeButton = Selector('[data-testid=acknowledge-btn]');\n    confirmAcknowledgeButton = Selector('[data-testid=acknowledge-submit]');\n    claimPendingMessageButton = Selector('[data-testid=claim-pending-message]');\n    submitButton = Selector('[data-testid=btn-submit]');\n    consumerDestinationSelect = Selector('[data-testid=destination-select]');\n    removeConsumerButton = Selector('[data-testid^=remove-consumer-button]');\n    removeConsumerGroupButton = Selector('[data-testid^=remove-groups-button]');\n    optionalParametersSwitcher = Selector('[data-testid=optional-parameters-switcher]');\n    forceClaimCheckbox = Selector('[data-testid=force-claim-checkbox]').sibling();\n    editStreamLastIdButton = Selector('[data-testid^=stream-group_edit-btn]');\n    saveButton = Selector('[data-testid=save-btn]');\n    bulkActionsButton = Selector('[data-testid=btn-bulk-actions]');\n    editHashButton = Selector('[data-testid^=hash_edit-btn-]');\n    editHashFieldTtlButton = Selector('[data-testid^=hash-ttl_edit-btn-]', { timeout: 500 });\n    editZsetButton = Selector('[data-testid^=zset_edit-btn-]');\n    editListButton = Selector('[data-testid^=list_edit-btn-]');\n    cancelStreamGroupBtn = Selector('[data-testid=cancel-stream-groups-btn]');\n    patternModeBtn = Selector('[data-testid=search-mode-pattern-btn]');\n    redisearchModeBtn = Selector('[data-testid=search-mode-redisearch-btn]');\n    showFilterHistoryBtn = Selector('[data-testid=show-suggestions-btn]');\n    clearFilterHistoryBtn = Selector('[data-testid=clear-history-btn]');\n    loadSampleDataBtn = Selector('[data-testid=load-sample-data-btn]');\n    executeBulkKeyLoadBtn = Selector('[data-testid=load-sample-data-btn-confirm]');\n    backToBrowserBtn = Selector('[data-testid=back-right-panel-btn]');\n    loadAllBtn = Selector('[data-testid=load-all-value-btn]');\n    downloadAllValueBtn = Selector('[data-testid=download-all-value-btn]');\n    openTutorialsBtn = Selector('[data-testid=explore-msg-btn]')\n    keyItem = Selector('[data-testid*=\"node-item\"][data-testid*=\"keys:\"]');\n    columnsBtn = Selector('[data-testid=btn-columns-actions]')\n    //CONTAINERS\n    streamGroupsContainer = Selector('[data-testid=stream-groups-container]');\n    streamConsumersContainer = Selector('[data-testid=stream-consumers-container]');\n    breadcrumbsContainer = Selector('[data-testid=breadcrumbs-container]');\n    virtualTableContainer = Selector('[data-testid=virtual-table-container]');\n    streamEntriesContainer = Selector('[data-testid=stream-entries-container]');\n    streamMessagesContainer = Selector('[data-testid=stream-messages-container]');\n    loader = Selector('[data-testid=type-loading]');\n    newIndexPanel = Selector('[data-testid=create-index-panel]');\n    //LINKS\n    internalLinkToWorkbench = Selector('[data-testid=internal-workbench-link]');\n    userSurveyLink = Selector('[data-testid=user-survey-link]');\n    redisearchFreeLink = Selector('[data-testid=get-started-link]');\n    guideLinksBtn = Selector('[data-testid^=guide-button-]');\n    //OPTION ELEMENTS\n    stringOption = Selector('[data-test-subj=string]').parent('[role=option]');\n    jsonOption = Selector('[data-test-subj=ReJSON-RL]').parent('[role=option]');\n    setOption = Selector('[data-test-subj=set]').parent('[role=option]');\n    zsetOption = Selector('[data-test-subj=zset]').parent('[role=option]');\n    listOption = Selector('[data-test-subj=list]').parent('[role=option]');\n    hashOption = Selector('[data-test-subj=hash]').parent('[role=option]');\n    streamOption = Selector('[data-test-subj=stream]').parent('[role=option]');\n    pushToTailSelection = Selector('[role=option] span').withExactText('Push to tail').parent('[role=option]');\n    pushToHeadSelection = Selector('[role=option] span').withExactText('Push to head').parent('[role=option]');\n    removeFromHeadSelection = Selector('span').withExactText('Remove from head').parent('[role=option]');\n    filterOptionType = Selector('[data-test-subj^=filter-option-type-]');\n    filterByKeyTypeDropDown = Selector('[data-testid=select-filter-key-type]', { timeout: 500 });\n    filterAllKeyType = Selector('[role=option] span').withExactText('All Key Types').parent('[role=option]');\n    consumerOption = Selector('[data-testid=consumer-option]');\n    claimTimeOptionSelect = Selector('[data-testid=time-option-select]');\n    relativeTimeOption = Selector('#idle');\n    timestampOption = Selector('#time');\n    formatSwitcher = Selector('[data-testid=select-format-key-value]', { timeout: 2000 });\n    formatSwitcherIcon = Selector('[data-testid^=key-value-formatter-option-selected]');\n    refreshIndexButton = Selector('[data-testid=refresh-indexes-btn]');\n    selectIndexDdn = Selector('[data-testid=select-index-placeholder],[data-testid=select-search-mode]', { timeout: 1000 });\n    createIndexBtn = Selector('[data-testid=create-index-btn]');\n    cancelIndexCreationBtn = Selector('[data-testid=create-index-cancel-btn]');\n    confirmIndexCreationBtn = Selector('[data-testid=create-index-btn]');\n    resizeTrigger = Selector('[data-testid^=resize-trigger-]');\n    filterHistoryOption = Selector('[data-testid^=suggestion-item-]');\n    filterHistoryItemText = Selector('[data-testid=suggestion-item-text]');\n    //TABS\n    streamTabGroups = Selector('[data-testid=stream-details] [role=tablist] p').withExactText('Consumer Groups').parent('[role=tab]');\n    streamTabConsumers = Selector('[data-testid=stream-details] [role=tab][aria-controls*=-content-Consumers]');\n    streamTabs = Selector('[data-testid=stream-tabs]');\n    //TEXT INPUTS (also referred to as 'Text fields')\n    addKeyNameInput = Selector('[data-testid=key]');\n    keyNameInput = Selector('[data-testid=edit-key-input]');\n    keyTTLInput = Selector('[data-testid=ttl]');\n    editKeyTTLInput = Selector('[data-testid=edit-ttl-input]');\n    ttlText = Selector('[data-testid=key-ttl-text] span');\n    hashFieldValueInput = Selector('[data-testid=field-value]');\n    hashFieldNameInput = Selector('[data-testid=field-name]');\n    hashFieldValueEditor = Selector('[data-testid^=hash_value-editor]');\n    hashTtlFieldInput = Selector('[data-testid=hash-ttl]');\n    listKeyElementEditorInput = Selector('[data-testid^=list_value-editor-]');\n    stringKeyValueInput = Selector('[data-testid=string-value]');\n    jsonKeyValueInput = Selector('[data-mode-id=json]');\n    jsonUploadInput = Selector('[data-testid=upload-input-file]');\n    setMemberInput = Selector('[data-testid=member-name]');\n    zsetMemberScoreInput = Selector('[data-testid=member-score]');\n    filterByPatterSearchInput = Selector('[data-testid=search-key]');\n    hashFieldInput = Selector('[data-testid=hash-field]');\n    hashValueInput = Selector('[data-testid=hash-value]');\n    searchInput = Selector('[data-testid=search]');\n    jsonKeyInput = Selector('[data-testid=json-key]');\n    jsonValueInput = Selector('[data-testid=json-value]');\n    countInput = Selector('[data-testid=count-input]');\n    streamEntryId = Selector('[data-testid=entryId]');\n    streamField = Selector('[data-testid=field-name]');\n    streamValue = Selector('[data-testid=field-value]');\n    addAdditionalElement = Selector('[data-testid=add-item]');\n    streamFieldsValues = Selector('[data-testid^=stream-entry-field-]');\n    streamEntryIDDateValue = Selector('[data-testid^=stream-entry-][data-testid$=date]');\n    groupNameInput = Selector('[data-testid=group-name-field]');\n    consumerIdInput = Selector('[data-testid=id-field]');\n    streamMinIdleTimeInput = Selector('[data-testid=min-idle-time]');\n    claimIdleTimeInput = Selector('[data-testid=time-count]');\n    claimRetryCountInput = Selector('[data-testid=retry-count]');\n    lastIdInput = Selector('[data-testid=last-id-field]');\n    inlineItemEditor = Selector('[data-testid=inline-item-editor]');\n    indexNameInput = Selector('[data-testid=index-name]');\n    prefixFieldInput = Selector('[data-test-subj=comboBoxInput]');\n    indexIdentifierInput = Selector('[data-testid^=identifier-]');\n    //TEXT ELEMENTS\n    keySizeDetails = Selector('[data-testid=key-size-text]');\n    keyLengthDetails = Selector('[data-testid=key-length-text]');\n    keyNameInTheList = Selector(this.cssSelectorKey);\n    hashFieldsList = Selector('[data-testid^=hash-field-] span');\n    hashValuesList = Selector('[data-testid^=hash_content-value-] p');\n    hashField = Selector('[data-testid^=hash-field-]').nth(0);\n    hashFieldValue = Selector('[data-testid^=hash_content-value-]');\n    setMembersList = Selector('[data-testid^=set-member-value-]');\n    zsetMembersList = Selector('[data-testid^=zset-member-value-]');\n    zsetScoresList = Selector('[data-testid^=zset_content-value-]');\n    listElementsList = Selector('[data-testid^=list_content-value-]');\n    jsonKeyValue = createEnhancedSelector('[data-testid=json-data]');\n    jsonError = Selector('[data-testid=edit-json-error]');\n    tooltip = Selector('[role=tooltip]', { timeout: 500 });\n    dialog = Selector('[role=dialog]', { timeout: 500 });\n    noResultsFound = Selector('[data-test-subj=no-result-found]');\n    noResultsFoundOnly = Selector('[data-testid=no-result-found-only]');\n    searchAdvices = Selector('[data-test-subj=search-advices]');\n    keysNumberOfResults = Selector('[data-testid=keys-number-of-results]');\n    scannedValue = Selector('[data-testid=keys-number-of-scanned]');\n    totalKeysNumber = Selector('[data-testid=keys-total]');\n    keyDetailsBadge = Selector('[data-testid=key-details-header] [data-testid^=badge-]');\n    modulesTypeDetails = Selector('[data-testid=modules-type-details]');\n    filteringLabel = Selector('[data-testid^=badge-]');\n    keysSummary = Selector('[data-testid=keys-summary]');\n    multiSearchArea = Selector(this.cssFilteringLabel);\n    keyDetailsHeader = Selector('[data-testid=key-details-header]');\n    keysContainer = Selector('[id=keys]');\n    keyListTable = Selector('[data-testid=keyList-table]');\n    keyListMessage = Selector('[data-testid=no-result-found-msg]');\n    keyDetailsTable = Selector('[data-testid=key-details]');\n    keyNameFormDetails = Selector('p[data-testid=edit-key-input]');\n    keyDetailsTTL = Selector('[data-testid=key-ttl-text]');\n    progressLine = Selector('div.euiProgress');\n    progressKeyList = Selector('[data-testid=progress-key-list]');\n    jsonScalarValue = Selector('[data-testid=json-scalar-value]');\n    noKeysToDisplayText = Selector('[data-testid=no-result-found-msg]');\n    streamEntryDate = Selector('[data-testid*=-date][data-testid*=stream-entry]');\n    streamEntryIdValue = Selector('.streamItemId[data-testid*=stream-entry]');\n    streamFields = Selector('[data-testid=stream-entries-container] .truncateText');\n    streamVirtualContainer = Selector('[data-testid=virtual-grid-container] div div').nth(0);\n    streamEntryFields = Selector('[data-testid^=stream-entry-field]');\n    confirmationMessagePopover = Selector('[role=dialog][data-state=open]');\n    streamGroupId = Selector('.streamItemId[data-testid^=stream-group-id]');\n    streamGroupName = Selector('[data-testid^=stream-group-name]');\n    streamMessage = Selector('[data-testid*=-date][data-testid^=stream-message]');\n    streamConsumerName = Selector('[data-testid^=stream-consumer-]');\n    consumerGroup = Selector('[data-testid^=stream-group-]');\n    entryIdInfoIcon = Selector('[data-testid=entry-id-info-icon]');\n    entryIdError = Selector('[data-testid=id-error]');\n    pendingCount = Selector('[data-testid=pending-count]');\n    streamRangeBar = Selector('[data-testid=mock-fill-range]');\n    rangeLeftTimestamp = Selector('[data-testid=range-left-timestamp]');\n    rangeRightTimestamp = Selector('[data-testid=range-right-timestamp]');\n    jsonValue = Selector('[data-testid=value-as-json]');\n    stringValueAsJson = Selector(this.cssJsonValue);\n    // POPUPS\n    changeValueWarning = Selector('[data-testid=confirm-popover]');\n    // TABLE\n    keyListItem = Selector('[role=rowgroup] [role=row]');\n    // Dialog\n    noReadySearchDialogTitle = Selector('[data-testid=welcome-page-title]');\n    //checkbox\n    showTtlCheckbox =  Selector('[data-testid=test-check-ttl]~label');\n    showTtlColumnCheckbox =  Selector('[data-testid=show-ttl]~label');\n    showSizeColumnCheckbox =  Selector('[data-testid=show-key-size]~label');\n\n    //Get Hash key field ttl value\n    //for Redis databases 7.4 and higher\n    getHashTtlFieldInput = (fieldName: string): EnhancedSelector => (createEnhancedSelector(`[data-testid=hash-ttl_content-value-${fieldName}]`));\n    getListElementInput = (count: number): Selector => (Selector(`[data-testid*=element-${count}]`));\n    getKeySize = (keyName: string): Selector => (Selector(`[data-testid=size-${keyName}]`));\n    getKeyTTl = (keyName: string): Selector => (Selector(`[data-testid=ttl-${keyName}]`));\n\n\n    /**\n     * Common part for Add any new key\n     * @param keyName The name of the key\n     * @param TTL The Time to live value of the key\n     */\n    async commonAddNewKey(keyName: string, TTL?: string): Promise<void> {\n        await Common.waitForElementNotVisible(this.progressLine);\n        await Common.waitForElementNotVisible(this.loader);\n        await t\n            .click(this.plusAddKeyButton)\n            .click(this.addKeyNameInput)\n            .typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        if (TTL !== undefined) {\n            await t\n                .click(this.keyTTLInput)\n                .typeText(this.keyTTLInput, TTL, { replace: true, paste: true });\n        }\n        await t.click(this.keyTypeDropDown);\n    }\n\n    /**\n     * Adding a new String key\n     * @param keyName The name of the key\n     * @param TTL The Time to live value of the key\n     * @param value The key value\n     */\n    async addStringKey(keyName: string, value = ' ', TTL?: string): Promise<void> {\n        await t.click(this.plusAddKeyButton);\n        await t.click(this.keyTypeDropDown);\n        await t.click(this.stringOption);\n        await t.click(this.addKeyNameInput);\n        await t.typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        if (TTL !== undefined) {\n            await t.click(this.keyTTLInput)\n                .typeText(this.keyTTLInput, TTL, { replace: true, paste: true });\n        }\n        await t.click(this.stringKeyValueInput);\n        await t.typeText(this.stringKeyValueInput, value);\n        await t.click(this.addKeyButton);\n    }\n\n    /**\n     *Adding a new Json key\n     * @param keyName The name of the key\n     * @param value The key value\n     * @param TTL The Time to live value of the key (optional parameter)\n     */\n    async addJsonKey(keyName: string, value: string, TTL?: string): Promise<void> {\n        await t.click(this.plusAddKeyButton);\n        await t.click(this.keyTypeDropDown);\n        await t.click(this.jsonOption);\n        await t.click(this.addKeyNameInput);\n        await t.typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        await t.click(this.jsonKeyValueInput);\n        await t.typeText(this.jsonKeyValueInput, value, { replace: true, paste: true });\n        if (TTL !== undefined) {\n            await t.click(this.keyTTLInput);\n            await t.typeText(this.keyTTLInput, TTL);\n        }\n        await t.click(this.addKeyButton);\n    }\n\n    /**\n     * Adding a new Set key\n     * @param keyName The name of the key\n     * @param TTL The Time to live value of the key\n     * @param members The key members\n     */\n    async addSetKey(keyName: string, TTL = ' ', members = ' '): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await Common.waitForElementNotVisible(this.progressLine);\n        await Common.waitForElementNotVisible(this.loader);\n        await t.click(this.plusAddKeyButton);\n        await t.click(this.keyTypeDropDown);\n        await t.click(this.setOption);\n        await t.click(this.addKeyNameInput);\n        await t.typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        await t.click(this.keyTTLInput);\n        await t.typeText(this.keyTTLInput, TTL);\n        await t.typeText(this.setMemberInput, members, { replace: true, paste: true });\n        await t.click(this.addKeyButton);\n    }\n\n    /**\n     * Adding a new ZSet key\n     * @param keyName The name of the key\n     * @param scores The score of the key member\n     * @param TTL The Time to live value of the key\n     * @param members The key members\n     */\n    async addZSetKey(keyName: string, scores = ' ', TTL = ' ', members = ' '): Promise<void> {\n        await Common.waitForElementNotVisible(this.progressLine);\n        await Common.waitForElementNotVisible(this.loader);\n        await t.click(this.plusAddKeyButton);\n        await t.click(this.keyTypeDropDown);\n        await t.click(this.zsetOption);\n        await t.click(this.addKeyNameInput);\n        await t.typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        await t.click(this.keyTTLInput);\n        await t.typeText(this.keyTTLInput, TTL, { replace: true, paste: true });\n        await t.typeText(this.setMemberInput, members, { replace: true, paste: true });\n        await t.typeText(this.zsetMemberScoreInput, scores, { replace: true, paste: true });\n        await t.click(this.addKeyButton);\n    }\n\n    /**\n     * Adding a new List key\n     * @param keyName The name of the key\n     * @param TTL The Time to live value of the key\n     * @param element The key element\n     */\n    async addListKey(keyName: string, TTL = ' ', element: string[] = [' '], position: AddElementInList = AddElementInList.Tail): Promise<void> {\n        await Common.waitForElementNotVisible(this.progressLine);\n        await Common.waitForElementNotVisible(this.loader);\n        await t.click(this.plusAddKeyButton);\n        await t.click(this.keyTypeDropDown);\n        await t.click(this.listOption);\n        await t.click(this.addKeyNameInput);\n        await t.typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        await t.click(this.keyTTLInput);\n        await t.typeText(this.keyTTLInput, TTL, { replace: true, paste: true });\n\n        if(position === AddElementInList.Head){\n            await t.click(this.destinationSelect);\n            await t.click(this.pushToHeadSelection);\n            await t.expect(this.pushToHeadSelection.exists).notOk();\n        }\n\n        for(let i = 0; i < element.length; i++ ) {\n            await t.click(this.getListElementInput(i));\n            await t.typeText(this.getListElementInput(i), element[i], { replace: true, paste: true });\n            // If there's more than one element and it's not the last element, add a new row\n            if (element.length > 1 && i < element.length - 1) {\n                await t.click(this.addAdditionalElement);\n            }\n        }\n        await t.click(this.addKeyButton);\n    }\n\n    /**\n     * Adding a new Hash key\n     * @param keyName The name of the key\n     * @param TTL The Time to live value of the key\n     * @param field The field name of the key\n     * @param value The value of the key\n     * @param fieldTtl The ttl of the field for Redis databases 7.4 and higher*/\n    async addHashKey(keyName: string, TTL = ' ', field = ' ', value = ' ', fieldTtl = ''): Promise<void> {\n        if (await this.Toast.toastCloseButton.visible) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await Common.waitForElementNotVisible(this.progressLine);\n        await Common.waitForElementNotVisible(this.loader);\n        await t.click(this.plusAddKeyButton);\n        await t.click(this.keyTypeDropDown);\n        await t.click(this.hashOption);\n        await t.click(this.addKeyNameInput);\n        await t.typeText(this.addKeyNameInput, keyName, { replace: true, paste: true });\n        await t.click(this.keyTTLInput);\n        await t.typeText(this.keyTTLInput, TTL, { replace: true, paste: true });\n        await t.typeText(this.hashFieldNameInput, field, { replace: true, paste: true });\n        await t.typeText(this.hashFieldValueInput, value, { replace: true, paste: true });\n        if(fieldTtl !== ''){\n            await t.typeText(this.hashTtlFieldInput, fieldTtl, { replace: true, paste: true });\n        }\n        await t.click(this.addKeyButton);\n    }\n\n    /**\n     * Adding a new Stream key\n     * @param keyName The name of the key\n     * @param field The field name of the key\n     * @param value The value of the key\n     * @param TTL The Time to live value of the key\n     */\n    async addStreamKey(keyName: string, field: string, value: string, TTL?: string): Promise<void> {\n        await this.commonAddNewKey(keyName, TTL);\n        await t.click(this.streamOption);\n        // Verify that user can see Entity ID filled by * by default on add Stream key form\n        await t.expect(this.streamEntryId.withAttribute('value', '*').exists).ok('Preselected Stream Entity ID field not displayed');\n        await t.typeText(this.streamField, field, { replace: true, paste: true });\n        await t.typeText(this.streamValue, value, { replace: true, paste: true });\n        await t.expect(this.addKeyButton.withAttribute('disabled').exists).notOk('Add Key button not clickable');\n        await t.click(this.addKeyButton);\n        await t.click(this.Toast.toastCloseButton);\n    }\n\n    /**\n     * Adding a new Entry to a Stream key\n     * @param field The field name of the key\n     * @param value The value of the key\n     * @param entryId The identification of specific entry of the Stream Key\n     */\n    async addEntryToStream(field: string, value: string, entryId?: string): Promise<void> {\n        await t\n            .click(this.addNewStreamEntry)\n            // Specify field, value and add new entry\n            .typeText(this.streamField, field, { replace: true, paste: true })\n            .typeText(this.streamValue, value, { replace: true, paste: true });\n        if (entryId !== undefined) {\n            await t.typeText(this.streamEntryId, entryId, { replace: true, paste: true });\n        }\n        await t\n            .click(this.saveElementButton)\n            // Validate that new entry is added\n            .expect(this.streamEntriesContainer.textContent).contains(field, 'Field parameter not correct')\n            .expect(this.streamEntriesContainer.textContent).contains(value, 'Value parameter not correct');\n    }\n\n    /**\n     * Adding a new Entry to a Stream key\n     * @param fields The field name of the key\n     * @param values The value of the key\n     * @param entryId The identification of specific entry of the Stream Key\n     */\n    async fulfillSeveralStreamFields(fields: string[], values: string[], entryId?: string): Promise<void> {\n        for (let i = 0; i < fields.length; i++) {\n            await t.typeText(this.streamField.nth(-1), fields[i], { replace: true, paste: true })\n                .typeText(this.streamValue.nth(-1), values[i], { replace: true, paste: true });\n            if (i < fields.length - 1) {\n                await t.click(this.addAdditionalElement);\n            }\n        }\n        if (entryId !== undefined) {\n            await t.typeText(this.streamEntryId, entryId, { replace: true, paste: true });\n        }\n    }\n\n    /**\n     * Select keys filter group type\n     * @param groupName The group name\n     */\n    async selectFilterGroupType(groupName: string): Promise<void> {\n        await t\n            .click(this.filterByKeyTypeDropDown)\n            .click(this.filterOptionType.withExactText(groupName));\n    }\n\n    /**\n     * Select all key type filter group type\n     */\n    async setAllKeyType(): Promise<void> {\n        await t\n            .click(this.filterByKeyTypeDropDown)\n            .click(this.filterAllKeyType);\n    }\n\n    /**\n     * Searching by Key name in the list\n     * @param keyName The name of the key\n     */\n    async navigateToKey(keyName: string): Promise<void> {\n        // todo: check if we outside of database\n        await this.searchByKeyName(keyName);\n        await this.openKeyDetailsByKeyName(keyName);\n    }\n\n    /**\n     * Searching by Key name in the list\n     * @param keyName The name of the key\n     */\n    async searchByKeyName(keyName: string): Promise<void> {\n        await t.click(this.filterByPatterSearchInput);\n        await t.typeText(this.filterByPatterSearchInput, keyName, { replace: true, paste: true });\n        await t.pressKey('enter');\n    }\n\n    /**\n     * Get selector by key name\n     * @param keyName The name of the key\n     */\n    getKeySelectorByName(keyName: string): Selector {\n        return Selector(`[data-testid=\"key-${keyName}\"]`);\n    }\n\n    /**\n     * Verifying if the Key is in the List of keys\n     * @param keyName The name of the key\n     */\n    async isKeyIsDisplayedInTheList(keyName: string): Promise<boolean> {\n        const keyNameInTheList = this.getKeySelectorByName(keyName);\n        await Common.waitForElementNotVisible(this.loader);\n        return keyNameInTheList.exists;\n    }\n\n    //Delete key from details\n    async deleteKey(): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await t.click(this.keyNameInTheList);\n        await t.click(this.deleteKeyButton);\n        await t.click(this.confirmDeleteKeyButton);\n    }\n\n    /**\n     * Delete key by Name from details\n     * @param keyName The name of the key\n     */\n    async deleteKeyByName(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName);\n        await t.hover(this.keyNameInTheList);\n        await t.click(this.keyNameInTheList);\n        await t.click(this.deleteKeyButton);\n        await t.click(this.confirmDeleteKeyButton);\n    }\n\n    /**\n     * Delete keys by their Names\n     * @param keyNames The names of the key array\n     */\n    async deleteKeysByNames(keyNames: string[]): Promise<void> {\n        for (const name of keyNames) {\n            await this.deleteKeyByName(name);\n        }\n    }\n\n    /**\n     * Delete Key By name after Hovering\n     * @param keyName The name of the key\n     */\n    async deleteKeyByNameFromList(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName);\n        await t.hover(this.keyNameInTheList);\n        await t.click(Selector(`[data-testid=\"delete-key-btn-${keyName}\"]`));\n        await t.click(this.submitDeleteKeyButton);\n    }\n\n    /**\n     * Edit key name from details\n     * @param keyName The name of the key\n     */\n    async editKeyName(keyName: string): Promise<void> {\n        await t\n            .click(this.editKeyNameButton)\n            .typeText(this.keyNameInput, keyName, { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn);\n    }\n\n    /**\n     * Edit String key value from details\n     * @param value The value of the key\n     */\n    async editStringKeyValue(value: string): Promise<void> {\n        await t\n            .click(this.stringKeyValueInput)\n            .typeText(this.stringKeyValueInput, value, { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn);\n    }\n\n    //Get String key value from details\n    async getStringKeyValue(): Promise<string> {\n        return this.stringKeyValueInput.textContent;\n    }\n\n    //Get Zset key score from details\n    async getZsetKeyScore(): Promise<string> {\n        return this.zsetScoresList.textContent;\n    }\n\n    /**\n     * Add field to hash key\n     * @param keyFieldValue The value of the hash field\n     * @param keyValue The hash value\n     * @param fieldTtl The hash field ttl value for Redis databases 7.4 and higher\n     */\n    async addFieldToHash(keyFieldValue: string, keyValue: string, fieldTtl = ''): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await t.click(this.addKeyValueItemsButton);\n        await t.typeText(this.hashFieldInput, keyFieldValue, { replace: true, paste: true });\n        await t.typeText(this.hashValueInput, keyValue, { replace: true, paste: true });\n        if(fieldTtl !== ''){\n            await t.typeText(this.hashTtlFieldInput, fieldTtl, { replace: true, paste: true });\n        }\n        await t.click(this.saveHashFieldButton);\n    }\n\n    /**\n     * Edit Hash key the first value from details\n     * @param value The  new value of the key\n     */\n    async editHashKeyValue(value: string): Promise<void> {\n        await t\n            .hover(this.hashFieldValue)\n            .click(this.editHashButton)\n            .typeText(this.hashFieldValueEditor, value, { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn);\n    }\n\n    /**\n     * Edit Hash field ttl value\n     * @param fieldName The field name\n     * @param fieldTtl The hash field ttl value for Redis databases 7.4 and higher\n     */\n    async editHashFieldTtlValue(fieldName: string, fieldTtl: string): Promise<void> {\n        await t\n            .hover(this.getHashTtlFieldInput(fieldName))\n            .click(this.editHashFieldTtlButton)\n            .typeText(this.inlineItemEditor, fieldTtl, { replace: true, paste: true })\n            .click(this.applyButton);\n    }\n\n    //Get Hash key value from details\n    async getHashKeyValue(): Promise<string> {\n        return this.hashFieldValue.textContent;\n    }\n\n    /**\n     * Edit List key value from details\n     * @param value The value of the key\n     */\n    async editListKeyValue(value: string): Promise<void> {\n        await t\n            .hover(this.listElementsList)\n            .click(this.editListButton)\n            .typeText(this.listKeyElementEditorInput, value, { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn);\n    }\n\n    //Get List key value from details\n    async getListKeyValue(): Promise<string> {\n        return this.listElementsList.textContent;\n    }\n\n    //Get JSON key value from details\n    async getJsonKeyValue(): Promise<string> {\n        return this.jsonKeyValue.textContentWithoutButtons;\n    }\n\n    /**\n     * Search by the value in the key details\n     * @param value The value of the search parameter\n     */\n    async searchByTheValueInKeyDetails(value: string): Promise<void> {\n        await t\n            .click(this.searchButtonInKeyDetails)\n            .typeText(this.searchInput, value, { replace: true, paste: true })\n            .pressKey('enter');\n    }\n\n    /**\n     * Search by the value in the key details\n     * @param value The value of the search parameter\n     */\n    async secondarySearchByTheValueInKeyDetails(value: string): Promise<void> {\n        await t\n            .typeText(this.searchInput, value, { replace: true, paste: true })\n            .pressKey('enter');\n    }\n\n    /**\n     * Search by the value in the set key details\n     * @param value The value of the search parameter\n     */\n    async searchByTheValueInSetKey(value: string): Promise<void> {\n        await t\n            .click(this.searchInput)\n            .typeText(this.searchInput, value, { replace: true, paste: true })\n            .pressKey('enter');\n    }\n\n    /**\n     * Add member to the Set key\n     * @param keyMember The value of the set member\n     */\n    async addMemberToSet(keyMember: string): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await t\n            .click(this.addKeyValueItemsButton)\n            .typeText(this.setMemberInput, keyMember, { replace: true, paste: true })\n            .click(this.saveMemberButton);\n    }\n\n    /**\n     * Add member to the ZSet key\n     * @param keyMember The value of the Zset member\n     * @param score The value of the score\n     */\n    async addMemberToZSet(keyMember: string, score: string): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await t\n            .click(this.addKeyValueItemsButton)\n            .typeText(this.setMemberInput, keyMember, { replace: true, paste: true })\n            .typeText(this.zsetMemberScoreInput, score, { replace: true, paste: true })\n            .click(this.saveMemberButton);\n    }\n\n    /**\n     * Open key details with search\n     * @param keyName The name of the key\n     */\n    async openKeyDetails(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName);\n        await t.click(this.keyNameInTheList);\n    }\n\n    /**\n     * Open key details of the key by name\n     * @param keyName The name of the key\n     */\n    async openKeyDetailsByKeyName(keyName: string): Promise<void> {\n        const keyNameInTheList = Selector(`[data-testid=\"key-${keyName}\"]`);\n        await t.click(keyNameInTheList);\n    }\n\n    /**\n     * Add element to the List key\n     * @param element The value of the list element\n     */\n    async addElementToList(element: string[], position: AddElementInList = AddElementInList.Tail ): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        await t\n            .click(this.addKeyValueItemsButton)\n        if(position === AddElementInList.Head){\n            await t.click(this.destinationSelect);\n            await t.click(this.pushToHeadSelection);\n            await t.expect(this.pushToHeadSelection.exists).notOk();\n        }\n        for (let i = 0; i < element.length; i ++){\n            await t.typeText(this.getListElementInput(i), element[i], { replace: true, paste: true });\n            // If there's more than one element and it's not the last element, add a new row\n            if (element.length > 1 && i < element.length - 1) {\n                await t.click(this.addAdditionalElement);\n            }\n        }\n        await t.click(this.saveElementButton);\n    }\n\n    //Remove List element from head for Redis databases less then v. 6.2.\n    async removeListElementFromHeadOld(): Promise<void> {\n        await t.click(this.removeElementFromListIconButton);\n        await t.expect(this.countInput.withAttribute('disabled').exists).ok('Input field not disabled');\n        //Select Remove from head selection\n        await t.click(this.removeElementFromListSelect);\n        await t.click(this.removeFromHeadSelection);\n        //Confirm removing\n        await t.click(this.removeElementFromListButton);\n        await t.click(this.confirmRemoveListElementButton);\n    }\n\n    /**\n     * Remove List element from tail\n     * @param count The count if elements for removing\n     */\n    async removeListElementFromTail(count: string): Promise<void> {\n        await t.click(this.removeElementFromListIconButton);\n        await t.typeText(this.countInput, count, { replace: true, paste: true });\n        await t.click(this.removeElementFromListButton);\n        await t.click(this.confirmRemoveListElementButton);\n    }\n\n    /**\n     * Remove List element from head\n     * @param count The count if elements for removing\n     */\n    async removeListElementFromHead(count: string): Promise<void> {\n        await t.click(this.removeElementFromListIconButton);\n        //Enter count of the removing elements\n        await t.typeText(this.countInput, count, { replace: true, paste: true });\n        //Select Remove from head selection\n        await t.click(this.removeElementFromListSelect);\n        await t.click(this.removeFromHeadSelection);\n        //Confirm removing\n        await t.click(this.removeElementFromListButton);\n        await t.click(this.confirmRemoveListElementButton);\n    }\n\n    /**\n     * Add json key with value on the same level of the structure\n     * @param jsonKey The json key name\n     * @param jsonKeyValue The value of the json key\n     */\n    async addJsonKeyOnTheSameLevel(jsonKey: string, jsonKeyValue: string): Promise<void> {\n        await t.click(this.addJsonObjectButton);\n        await t.typeText(this.jsonKeyInput, jsonKey, { paste: true });\n        await t.typeText(this.jsonValueInput, jsonKeyValue, { paste: true });\n        await t.click(this.EditorButton.applyBtn);\n    }\n\n    /**\n     * Add json key with value inside the Json structure\n     * @param jsonKey The json key name\n     * @param jsonKeyValue The value of the json key\n     */\n    async addJsonKeyInsideStructure(jsonKey: string, jsonKeyValue: string): Promise<void> {\n        await t.click(this.expandJsonObject);\n        await t.click(this.addJsonFieldButton);\n        await t.typeText(this.jsonKeyInput, jsonKey, { paste: true });\n        await t.typeText(this.jsonValueInput, jsonKeyValue, { paste: true });\n        await t.click(this.EditorButton.applyBtn);\n    }\n\n    /**\n     * Add json value inside the Json structure\n     * @param jsonKeyValue The value of the json key\n     */\n    async addJsonValueInsideStructure(jsonKeyValue: string): Promise<void> {\n        await t.click(this.expandJsonObject);\n        await t.click(this.addJsonFieldButton);\n        await t.typeText(this.jsonValueInput, jsonKeyValue, { paste: true });\n        await t.click(this.applyButton);\n    }\n\n    /**\n     * Add json structure\n     * @param jsonStructure The structure of the json key\n     */\n    async addJsonStructure(jsonStructure: string): Promise<void> {\n        if (await this.expandJsonObject.exists) {\n            await t.click(this.expandJsonObject);\n        }\n        await t.click(this.editJsonObjectButton);\n        await t.typeText(this.jsonValueInput, jsonStructure, { paste: true });\n        await t.click(this.applyEditButton);\n    }\n\n    //Delete entry from Stream key\n    async deleteStreamEntry(): Promise<void> {\n        await t.click(this.removeEntryButton)\n            .click(this.confirmRemoveEntryButton);\n    }\n\n    /**\n     * Get key length from opened key details\n     */\n    async getKeyLength(): Promise<string> {\n        const rawValue = await this.keyLengthDetails.textContent;\n        return rawValue.split(' ')[rawValue.split(' ').length - 1];\n    }\n\n    /**\n     * Create new consumer group in Stream key\n     * @groupName The name of the Consumer Group\n     * @id The ID of the Consumer Group\n     */\n    async createConsumerGroup(groupName: string, id?: string): Promise<void> {\n        await t\n            .click(this.addKeyValueItemsButton)\n            .typeText(this.groupNameInput, groupName, { replace: true, paste: true });\n        if (id !== undefined) {\n            await t.typeText(this.consumerIdInput, id, { replace: true, paste: true });\n        }\n        await t.click(this.saveGroupsButton);\n    }\n\n    /**\n     * Open pendings view in Stream key\n     * @keyName The name of the Stream Key\n     */\n    async openStreamPendingsView(keyName: string): Promise<void> {\n        await this.openKeyDetails(keyName);\n        await t.click(this.streamTabGroups);\n        await t.click(this.consumerGroup);\n        await t.click(this.streamConsumerName);\n    }\n\n    /**\n     * Open formatter and select option\n     * @param formatter The name of format\n     */\n    async selectFormatter(formatter: string): Promise<void> {\n        const option = Selector(`[data-test-subj=\"format-option-${formatter}\"]`);\n        await t\n            .click(this.formatSwitcher)\n            .click(option);\n    }\n\n    /**\n     * Verify that keys can be scanned more and results increased\n    */\n    async verifyScannningMore(): Promise<void> {\n        for (let i = 10; i < 100; i += 10) {\n            // Remember results value\n            const rememberedScanResults = Number((await this.keysNumberOfResults.textContent).replace(/\\s/g, ''));\n            await t.expect(this.progressKeyList.exists).notOk('Progress Bar is still displayed', { timeout: 30000 });\n            const scannedValueText = this.scannedValue.textContent;\n            const regExp = new RegExp(`${i} ` + '...');\n            await t\n                .expect(scannedValueText).match(regExp, `The database is not automatically scanned by ${i} 000 keys`)\n                .click(this.scanMoreButton);\n            const scannedResults = Number((await this.keysNumberOfResults.textContent).replace(/\\s/g, ''));\n            await t.expect(scannedResults).gt(rememberedScanResults);\n        }\n    }\n\n    /**\n     * Open Select Index droprown and select option\n     * @param index The name of format\n    */\n    async selectIndexByName(index: string): Promise<void> {\n        const option = Selector(`[data-test-subj=\"mode-option-type-${index}\"]`);\n        const placeholder = Selector('[data-testid=\"select-index-placeholder\"]');\n        const dropdown = Selector('[data-testid=\"select-search-mode\"]');\n\n        // Click placeholder if it exists, otherwise click dropdown\n        const triggerElement = await placeholder.exists ? placeholder : dropdown;\n\n        await t\n            .click(triggerElement)\n            .click(option);\n    }\n\n    /**\n    * Verify that database has no keys\n    */\n    async verifyNoKeysInDatabase(): Promise<void> {\n        await t.expect(this.keyListMessage.exists).ok('Database not empty')\n            .expect(this.keysSummary.exists).notOk('Total value is displayed for empty database');\n    }\n\n    /**\n    * Clear filter on Browser page\n    */\n    async clearFilter(): Promise<void> {\n        await t.click(this.clearFilterButton);\n    }\n\n    /**\n     * Open Guide link by name\n     * @param guide The guide name\n     */\n    async clickGuideLinksByName(guide: string): Promise<void> {\n        const linkGuide = Selector('[data-testid^=\"guide-button-\"]').withExactText(guide);\n        await t.click(linkGuide);\n    }\n}\n\n/**\n * Add new keys parameters\n * @param keyName The name of the key\n * @param TTL The ttl of the key\n * @param value The value of the key\n * @param members The members of the key\n * @param scores The scores of the key member\n * @param field The field of the key\n */\nexport type AddNewKeyParameters = {\n    keyName: string,\n    value?: string,\n    TTL?: string,\n    members?: string,\n    scores?: string,\n    field?: string,\n    fields?: [{\n        field?: string,\n        valuse?: string\n    }]\n};\n\nexport type BaseKeyParameters = {\n    keyName: string,\n    ttl?: number\n};\n\n/**\n * Hash key parameters\n * @param keyName The name of the key\n * @param fields The Array with fields\n * @param field The field of the field\n * @param value The value of the field\n\n */\nexport type HashKeyParameters = BaseKeyParameters & {\n    fields: {\n        field: string,\n        value: string\n    }[]\n};\n\n/**\n * Stream key parameters\n * @param keyName The name of the key\n * @param entries The Array with entries\n * @param id The id of entry\n * @param fields The Array with fields\n */\nexport type StreamKeyParameters = BaseKeyParameters & {\n    entries: {\n        id: string,\n        fields: {\n            name: string,\n            value: string\n        }[]\n    }[]\n};\n\n/**\n * Set key parameters\n * @param keyName The name of the key\n * @param members The Array with members\n */\nexport type SetKeyParameters = BaseKeyParameters & {\n    members: string[]\n};\n\n/**\n * Sorted Set key parameters\n * @param keyName The name of the key\n * @param members The Array with members\n * @param name The name of the member\n * @param id The id of the member\n */\nexport type SortedSetKeyParameters = BaseKeyParameters & {\n    members: {\n        name: string,\n        score: number\n    }[]\n};\n\n/**\n * List key parameters\n * @param keyName The name of the key\n * @param element The element in list\n */\nexport type ListKeyParameters = BaseKeyParameters & {\n    elements: string[]\n};\n\n/**\n * String key parameters\n * @param keyName The name of the key\n * @param value The value in the string\n */\nexport type StringKeyParameters = BaseKeyParameters & {\n    value: string\n};\n\n/**\n * Json key parameters\n * @param keyName The name of the key\n * @param value The value in the json\n */\nexport type JsonKeyParameters = BaseKeyParameters & {\n    data: any\n};\n\n/**\n * The key arguments for multiple keys/fields adding\n * @param keysCount The number of keys to add\n * @param fieldsCount The number of fields in key to add\n * @param elementsCount The number of elements in key to add\n * @param membersCount The number of members in key to add\n * @param keyName The full key name\n * @param keyNameStartWith The name of key should start with\n * @param fieldStartWitht The name of field should start with\n * @param fieldValueStartWith The name of field value should start with\n * @param elementStartWith The name of element should start with\n * @param memberStartWith The name of member should start with\n */\n\nexport type AddKeyArguments = {\n    keysCount?: number,\n    fieldsCount?: number,\n    elementsCount?: number,\n    membersCount?: number,\n    keyName?: string,\n    keyNameStartWith?: string,\n    fieldStartWith?: string,\n    fieldValueStartWith?: string,\n    elementStartWith?: string,\n    memberStartWith?: string\n};\n\n/**\n * Keys Data parameters\n * @param textType The type of the key\n * @param keyName The name of the key\n */\nexport type KeyData = {\n    textType: string,\n    keyName: string\n}[];\n"
  },
  {
    "path": "tests/e2e/pageObjects/cluster-details-page.ts",
    "content": "import { Selector } from 'testcafe';\nimport { InstancePage } from './instance-page';\n\nexport class ClusterDetailsPage extends InstancePage {\n    //CSS Selectors\n    cssTableRow = 'tbody tr';\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    overviewTab = Selector('[data-testid=analytics-tabs] [role=tab] p').withExactText('Overview').parent('[role=tab]');\n    // COMPONENTS\n    clusterDetailsUptime = Selector('[data-testid=cluster-details-uptime]');\n    //TABLE COMPONENTS\n    tableHeaderCell = Selector('[data-testid=primary-nodes-table] thead th');\n    primaryNodesTable = Selector('[data-testid=primary-nodes-table]');\n    tableRow = Selector('tbody tr');\n\n    /**\n     * Get Primary nodes count in table\n     */\n    async getPrimaryNodesCount(): Promise<number> {\n        return await this.primaryNodesTable.find(this.cssTableRow).count;\n    }\n\n    /**\n     * Get total value from all rows in column\n     * @param column The column name\n     */\n    async getTotalValueByColumnName(column: string): Promise<number> {\n        let totalNumber = 0;\n        let columnInSelector = '';\n        switch (column) {\n            case 'Commands/s':\n                columnInSelector = 'opsPerSecond';\n                break;\n            case 'Clients':\n                columnInSelector = 'connectedClients';\n                break;\n            case 'Total Keys':\n                columnInSelector = 'totalKeys';\n                break;\n            case 'Network Input':\n                columnInSelector = 'networkInKbps';\n                break;\n            case 'Network Output':\n                columnInSelector = 'networkOutKbps';\n                break;\n            case 'Total Memory':\n                columnInSelector = 'usedMemory';\n                break;\n            default: columnInSelector = '';\n        }\n        const rowSelector = Selector(`[data-testid^=${columnInSelector}-value]`);\n        for (let i = 0; i < await rowSelector.count; i++) {\n            totalNumber += Number(await rowSelector.nth(i).textContent);\n        }\n        return totalNumber;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/bottom-panel/cli.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { Common } from '../../../helpers/common';\n\nexport class Cli {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    cliExpandButton = Selector('[data-testid=expand-cli]');\n    cliCollapseButton = Selector('[data-testid=close-cli]');\n    minimizeCliButton = Selector('[data-testid=hide-cli]');\n\n    cliBadge = Selector('[data-testid=expand-cli] div');\n\n    cliResizeButton = Selector('[data-test-subj=resize-btn-browser-cli]');\n    workbenchLink = Selector('[data-test-subj=cli-workbench-page-btn]');\n\n    //TEXT INPUTS (also referred to as 'Text fields')\n    cliCommandInput = Selector('[data-testid=cli-command]');\n    cliArea = Selector('[data-testid=cli');\n\n    cliOutputResponseSuccess = Selector('[data-testid=cli-output-response-success]');\n    cliOutputResponseFail = Selector('[data-testid=cli-output-response-fail]');\n\n    cliCommandAutocomplete = Selector('[data-testid=cli-command-autocomplete]');\n    cliCommandExecuted = Selector('[data-testid=cli-command-wrapper]');\n    cliReadMoreJSONCommandDocumentation = Selector('[id=jsonset]');\n    cliReadMoreRediSearchCommandDocumentation = Selector('[id=ftexplain]');\n\n    cliEndpoint = Selector('[data-testid^=cli-endpoint]');\n    cliDbIndex = Selector('[data-testid=cli-db-index]');\n    cliWarningMessage = Selector('[class*=euiTextColor--danger]');\n    cliLinkToPubSub = Selector('[data-test-subj=pubsub-page-btn]');\n    // Panel\n    cliPanel = Selector('[data-testid=cli]');\n\n    /**\n     * Add keys from CLI\n     * @param keyCommand The command from cli to add key\n     * @param amount The amount of the keys\n     * @param keyName The name of the keys. The default value is keyName\n     */\n    async addKeysFromCli(keyCommand: string, amount: number, keyName = 'keyName'): Promise<void> {\n        const keyValueArray = await Common.createArrayWithKeyValueAndKeyname(amount, keyName);\n\n        // Open CLI\n        await t.click(this.cliExpandButton);\n        // Add keys\n        await t.typeText(this.cliCommandInput, `${keyCommand} ${keyValueArray.join(' ')}`, { replace: true, paste: true });\n        await t.pressKey('enter');\n        await t.click(this.cliCollapseButton);\n    }\n\n    /**\n     * Add keys from CLI with delimiter\n     * @param keyCommand The command from cli to add key\n     * @param amount The amount of the keys\n     */\n    async addKeysFromCliWithDelimiter(keyCommand: string, amount: number): Promise<void> {\n        //Open CLI\n        await t.click(this.cliExpandButton);\n        //Add keys\n        const keyValueArray = await Common.createArrayWithKeyValueAndDelimiter(amount);\n        await t.typeText(this.cliCommandInput, `${keyCommand} ${keyValueArray.join(' ')}`, { replace: true, paste: true });\n        await t.pressKey('enter');\n        await t.click(this.cliCollapseButton);\n    }\n\n    /**\n     * Delete keys from CLI with delimiter\n     * @param amount The amount of the keys\n     */\n    async deleteKeysFromCliWithDelimiter(amount: number): Promise<void> {\n        //Open CLI\n        await t.click(this.cliExpandButton);\n        //Add keys\n        const keyValueArray = await Common.createArrayWithKeyAndDelimiter(amount);\n        await t.typeText(this.cliCommandInput, `DEL ${keyValueArray.join(' ')}`, { replace: true, paste: true });\n        await t.pressKey('enter');\n        await t.click(this.cliCollapseButton);\n    }\n\n    /**\n     * Send command in Cli\n     * @param command The command to send\n     */\n    async sendCommandInCli(command: string): Promise<void> {\n        // Open CLI\n        await t.click(this.cliExpandButton);\n        await t.typeText(this.cliCommandInput, command, { replace: true, paste: true });\n        await t.pressKey('enter');\n        await t.click(this.cliCollapseButton);\n    }\n\n    /**\n     * Send command in Cli\n     * @param commands The commands to send\n     */\n    async sendCommandsInCli(commands: string[]): Promise<void> {\n        await t.click(this.cliExpandButton);\n        for (const command of commands) {\n            await t.typeText(this.cliCommandInput, command, { replace: true, paste: true });\n            await t.pressKey('enter');\n        }\n        await t.click(this.cliCollapseButton);\n    }\n\n    /**\n     * Get command result execution\n     * @param command The command for send in CLI\n     */\n    async getSuccessCommandResultFromCli(command: string): Promise<string> {\n        // Open CLI\n        await t.click(this.cliExpandButton);\n        // Add keys\n        await t.typeText(this.cliCommandInput, command, { replace: true, paste: true });\n        await t.pressKey('enter');\n        const commandResult = await this.cliOutputResponseSuccess.innerText;\n        await t.click(this.cliCollapseButton);\n        return commandResult;\n    }\n\n    /**\n     * Send command in Cli and wait for total keys after 5 seconds\n     * @param command The command to send\n     */\n    async sendCliCommandAndWaitForTotalKeys(command: string): Promise<void> {\n        await this.sendCommandInCli(command);\n        // Wait 5 seconds and return total keys\n        await t.wait(5000);\n    }\n\n    /**\n     *  Create random index name with CLI and return\n     */\n\n    async createIndexwithCLI(prefix: string): Promise<string> {\n        const word = Common.generateWord(10);\n        const index = `idx:${word}`;\n        const commands = [\n            `FT.CREATE ${index} ON HASH PREFIX 1 ${prefix} SCHEMA \"name\" TEXT`\n        ];\n        await this.sendCommandsInCli(commands);\n        return index;\n    }\n\n    /**\n     * Add cached scripts\n     * @param numberOfScripts The number of cached scripts to add\n     */\n    async addCachedScripts(numberOfScripts: number): Promise<void> {\n        const scripts: string[] = [];\n\n        for (let i = 0; i < numberOfScripts; i++) {\n            scripts.push(`EVAL \"return '${Common.generateWord(3)}'\" 0`);\n        }\n\n        await this.sendCommandsInCli(scripts);\n    }\n\n    /**\n     * Get warning message text by command\n     * @param command command name\n     */\n    async getWarningMessageText(command: string): Promise<string> {\n\n        const executedCommand = await this.cliCommandExecuted.withExactText(command);\n        return await executedCommand.nextSibling(0).textContent;\n    }\n\n    /**\n     * Get executed text by index\n     * @param index index of the command in the CLI, by default is the last one\n     */\n    async getExecutedCommandTextByIndex(index = -1): Promise<string> {\n        return await this.cliCommandExecuted.nth(index).textContent;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/bottom-panel/command-helper.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { Common } from '../../../helpers/common';\n\nexport class CommandHelper {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    expandCommandHelperButton = Selector('[data-testid=expand-command-helper]');\n    closeCommandHelperButton = Selector('[data-testid=close-command-helper]');\n    minimizeCommandHelperButton = Selector('[data-testid=hide-command-helper]');\n    commandHelperBadge = Selector('[data-testid=expand-command-helper] span');\n    commandHelperArea = Selector('[data-testid=command-helper]');\n    cliHelperSearch = Selector('[data-testid=cli-helper-search]');\n    readMoreButton = Selector('[data-testid=read-more]');\n    returnToList = Selector('[data-testid=cli-helper-back-to-list-btn]');\n    filterGroupTypeButton = Selector('[data-testid=select-filter-group-type]');\n    filterOptionGroupType = Selector('[data-test-subj^=filter-option-group-type-]');\n    clearAllGroupFilters = Selector('[data-testid=cli-helper] button[title=\"Clear All\"]');\n\n    //TEXT ELEMENTS\n    cliHelper = Selector('[data-testid=cli-helper]');\n    cliHelperText = Selector('[data-testid=cli-helper-default]');\n    cliHelperOutputTitles = Selector('[data-testid^=cli-helper-output-title-]');\n    cliHelperTitle = Selector('[data-testid=cli-helper-title]');\n    cliHelperTitleArgs = Selector('[data-testid=cli-helper-title-args]');\n    cliHelperSummary = Selector('[data-testid=cli-helper-summary]');\n    cliHelperArguments = Selector('[data-testid=cli-helper-arguments]');\n    cliHelperComplexity = Selector('[data-testid=cli-helper-complexity]');\n\n    /**\n     * Select filter group type\n     * @param groupName The group name\n     */\n    async selectFilterGroupType(groupName: string): Promise<void> {\n        await t.click(this.filterGroupTypeButton);\n        await t.click(this.filterOptionGroupType.withExactText(groupName));\n    }\n\n    /**\n     * Check URL of command opened from command helper\n     * @param command The command for which to open Read more link\n     * @param url Command URL for external resource\n     */\n    async checkURLCommand(command: string, url: string): Promise<void> {\n        await t.click(this.cliHelperOutputTitles.withExactText(command));\n        await t.click(this.readMoreButton);\n        await t.expect(await Common.getPageUrl()).eql(url, 'The opened page not correct');\n    }\n\n    /**\n     * Check list of commands searched\n     * @param searchedCommand Searched command in Command Helper\n     * @param listToCompare The list with commands to compare with opened in Command Helper\n     */\n    async checkSearchedCommandInCommandHelper(searchedCommand: string, listToCompare: string[]): Promise<void> {\n        await t.typeText(this.cliHelperSearch, searchedCommand, { speed: 0.5 });\n        //Verify results in the output\n        const commandsCount = await this.cliHelperOutputTitles.count;\n        for (let i = 0; i < commandsCount; i++) {\n            await t.expect(this.cliHelperOutputTitles.nth(i).textContent).eql(listToCompare[i], 'Results in the output contains searched value');\n        }\n    }\n\n    /**\n     * Check commands list\n     * @param listToCompare The list with commands to compare with opened in Command Helper\n     */\n    async checkCommandsInCommandHelper(listToCompare: string[]): Promise<void> {\n        //Verify results in the output\n        const commandsCount = await this.cliHelperOutputTitles.count;\n        for (let i = 0; i < commandsCount; i++) {\n            await t.expect(this.cliHelperOutputTitles.nth(i).textContent).eql(listToCompare[i], 'Results in the output not contain searched value');\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/bottom-panel/index.ts",
    "content": "import { Profiler } from './profiler';\nimport { Cli } from './cli';\nimport { CommandHelper } from './command-helper';\nimport { SurveyLink } from './survey-link'\n\nexport {\n    Cli,\n    CommandHelper,\n    Profiler,\n    SurveyLink\n};\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/bottom-panel/profiler.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class Profiler {\n    //BUTTONS\n    expandMonitor = Selector('[data-testid=expand-monitor]');\n    runMonitorToggle = Selector('[data-testid=toggle-run-monitor]');\n    startMonitorButton = Selector('[data-testid=start-monitor]');\n    clearMonitorButton = Selector('[data-testid=clear-monitor]');\n    hideMonitor = Selector('[data-testid=hide-monitor]');\n    closeMonitor = Selector('[data-testid=close-monitor]');\n    resetProfilerButton = Selector('[data-testid=reset-profiler-btn]');\n    saveLogContainer = Selector('[data-testid=save-log-container]');\n    saveLogSwitchButton = Selector('[data-testid=save-log-switch]');\n    downloadLogButton = Selector('[data-testid=download-log-btn]');\n    //TEXT ELEMENTS\n    monitorIsStoppedText = Selector('[data-testid=monitor-stopped]');\n    monitorIsStartedText = Selector('[data-testid=monitor-started]');\n    monitorArea = Selector('[data-testid=monitor]');\n    monitorWarningMessage = Selector('[data-testid=monitor-warning-message]');\n    monitorCommandLinePart = Selector('[data-testid=monitor] span');\n    monitorCommandLineTimestamp = Selector('[data-testid=monitor] span').withText(/[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}/);\n    monitorNoPermissionsMessage = Selector('[data-testid=monitor-error-message]');\n    saveLogToolTip = Selector('[data-testid=save-log-tooltip]');\n    monitorNotStartedElement = Selector('[data-testid=monitor-not-started]');\n    profilerRunningTime = Selector('[data-testid=profiler-running-time]');\n    downloadLogPanel = Selector('[data-testid=download-log-panel]');\n\n    /**\n     * Check specific command in Monitor\n     * @param command A command which should be displayed in monitor\n     * @param parameters An arguments which should be displayed in monitor\n     * @param expected specify is the command is present or not\n     * @param timeout timeout\n     */\n    async checkCommandInMonitorResults(command: string, parameters?: string[], expected: boolean = true, timeout: number = 6000): Promise<void> {\n        const commandArray = command.split(' ');\n        for (const value of commandArray) {\n            if(expected){\n                await t.expect(this.monitorCommandLinePart.withText(value).exists).ok({ timeout: timeout });\n            }\n            else {\n                await t.expect(this.monitorCommandLinePart.withText(value).exists).notOk({ timeout: 1000 });\n            }\n        }\n        if (!!parameters) {\n            for (const argument of parameters) {\n                await t.expect(this.monitorCommandLinePart.withText(argument).exists).ok({ timeout: timeout });\n            }\n        }\n    }\n    /**\n     * Start monitor function and verify info\n     */\n    async startMonitorAndVerifyStart(): Promise<void> {\n        await t\n            .click(this.expandMonitor)\n            .click(this.startMonitorButton);\n        //Check for \"info\" command that is sent automatically every 5 seconds from BE side\n        await this.checkCommandInMonitorResults('info');\n    }\n\n    /**\n     * Start monitor function and verify info\n     */\n    async startMonitor(): Promise<void> {\n        if (!(await this.startMonitorButton.exists)){\n            await t\n                .click(this.expandMonitor)\n        }\n        await t\n            .click(this.startMonitorButton);\n    }\n\n    /**\n     * Start monitor with Save log function\n     */\n    async startMonitorWithSaveLog(): Promise<void> {\n        await t\n            .click(this.expandMonitor)\n            .click(this.saveLogSwitchButton)\n            .click(this.startMonitorButton);\n        //Check for \"info\" command that is sent automatically every 5 seconds from BE side\n        await this.checkCommandInMonitorResults('info');\n    }\n    /**\n     * Stop monitor function\n     */\n    async stopMonitor(): Promise<void> {\n        await t\n            .click(this.runMonitorToggle)\n            .expect(this.resetProfilerButton.exists).ok('Reset profiler button not appeared');\n    }\n\n    //Reset profiler\n    async resetProfiler(): Promise<void> {\n        await t\n            .click(this.runMonitorToggle)\n            .click(this.resetProfilerButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/bottom-panel/survey-link.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class SurveyLink {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    userSurveyLink = Selector('[data-testid=user-survey-link]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/browser/bulk-actions.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class BulkActions {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    bulkDeleteTooltipIcon = Selector('[data-testid=bulk-delete-tooltip]');\n    actionButton = Selector('[data-testid=bulk-action-warning-btn]');\n    bulkApplyButton = Selector('[data-testid=bulk-action-apply-btn]', { timeout: 500 });\n    bulkStopButton = Selector('[data-testid=bulk-action-stop-btn]');\n    bulkStartAgainButton = Selector('[data-testid=bulk-action-start-again-btn]');\n    bulkCancelButton = Selector('[data-testid=bulk-action-cancel-btn]');\n    bulkClosePanelButton = Selector('[data-testid=bulk-close-panel]');\n    bulkUpdateTab = Selector('[data-testid=bulk-action-tab-upload]');\n    bulkActionStartNewButton = Selector('[data-testid=bulk-action-start-new-btn]');\n    removeFileBtn = Selector('[aria-label=\"Clear selected files\"]');\n    bulkActionsOpenButton = Selector('[data-testid=btn-bulk-actions]');\n    //TEXT\n    infoFilter = Selector('[data-testid=bulk-actions-info-filter]');\n    infoSearch = Selector('[data-testid=bulk-actions-info-search]');\n    bulkActionsPlaceholder = Selector('[data-testid=bulk-actions-placeholder]');\n    bulkDeleteSummary = Selector('[data-testid=bulk-delete-summary]');\n    bulkActionWarningTooltip = Selector('[data-testid=bulk-action-tooltip]');\n    bulkStatusInProgress = Selector('[data-testid=bulk-status-progress]');\n    bulkStatusStopped = Selector('[data-testid=bulk-status-stopped]');\n    bulkStatusCompleted = Selector('[data-testid=bulk-status-completed]');\n    bulkDeleteCompletedSummary = Selector('[data-testid=bulk-delete-completed-summary]');\n    bulkUploadCompletedSummary = Selector('[data-testid=bulk-upload-completed-summary]');\n    //CONTAINERS\n    bulkActionsContainer = Selector('[data-testid=bulk-actions-content]');\n    bulkActionsSummary = Selector('[data-testid=bulk-actions-info]');\n    progressLine = Selector('[data-testid=progress-line]');\n    bulkUploadContainer = Selector('[data-testid=bulk-upload-container]');\n    // IMPORT\n    bulkUploadInput = Selector('[data-testid=bulk-upload-file-input]');\n\n    /**\n     * Open Bulk Actions and confirm deletion\n     */\n    async startBulkDelete(): Promise<void> {\n        await t\n            .click(this.actionButton)\n            .click(this.bulkApplyButton);\n    }\n\n    /**\n     * Bulk Upload of file\n     * @param path Path to file to upload\n     */\n    async uploadFileInBulk(path: string): Promise<void> {\n        await t\n            .setFilesToUpload(this.bulkUploadInput, [path])\n            .click(this.actionButton)\n            .click(this.bulkApplyButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/browser/index.ts",
    "content": "import { BulkActions } from './bulk-actions';\nimport { TreeView } from './tree-view';\n\nexport {\n    BulkActions,\n    TreeView\n};\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/browser/tree-view.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { Common } from '../../../helpers/common';\nimport { FiltersDialog } from '../../dialogs';\n\nexport class TreeView {\n    FiltersDialog = new FiltersDialog();\n\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    treeViewSettingsBtn = Selector('[data-testid=tree-view-settings-btn]');\n    sortingProgressBar = Selector('[data-testid=progress-key-tree]');\n    // TEXT ELEMENTS\n    treeViewKeysNumber = Selector('[data-testid^=count_]');\n    treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div');\n\n    /**\n     * Get folder selector by folder name\n     * @param folderName The name of the folder\n     */\n    getFolderSelectorByName(folderName: string): Selector {\n        return Selector(`[data-testid^=\"node-item_${folderName}\"]`);\n    }\n\n    /**\n     * Get folder counter selector by folder name\n     * @param folderName The name of the folder\n     */\n    getFolderCountSelectorByName(folderName: string): Selector {\n        return Selector(`[data-testid^=\"count_${folderName}\"]`);\n    }\n\n    /**\n    * Verifying if the Keys are in the List of keys\n    * @param keyNames The names of the keys\n    * @param isDisplayed True if keys should be displayed\n    */\n    async verifyFolderDisplayingInTheList(folderName: string, isDisplayed: boolean): Promise<void> {\n        isDisplayed\n            ? await t.expect(this.getFolderSelectorByName(folderName).exists).ok(`The folder ${folderName} not found`)\n            : await t.expect(this.getFolderSelectorByName(folderName).exists).notOk(`The folder ${folderName} found`);\n    }\n\n    /**\n     * Change delimiter value\n     * @param delimiter string with delimiter value\n     */\n    async changeDelimiterInTreeView(delimiter: string): Promise<void> {\n        // Open delimiter popup\n        await t.click(this.treeViewSettingsBtn);\n        await this.FiltersDialog.clearDelimiterCombobox();\n        // Apply new value to the field\n        await this.FiltersDialog.addDelimiterItem(delimiter);\n        // Click on save button\n        await t.click(this.FiltersDialog.treeViewDelimiterValueSave);\n    }\n\n    /**\n     * Change ordering value\n     * @param order ASC/DESC ordering for tree view\n     */\n    async changeOrderingInTreeView(order: string): Promise<void> {\n        // Open settings popup\n        await t.click(this.treeViewSettingsBtn);\n        await t.click(this.FiltersDialog.sortingBtn);\n        order === 'ASC'\n            ? await t.click(this.FiltersDialog.sortingASCoption)\n            : await t.click(this.FiltersDialog.sortingDESCoption);\n\n        // Click on save button\n        await t.click(this.FiltersDialog.treeViewDelimiterValueSave);\n        await Common.waitForElementNotVisible(this.sortingProgressBar);\n    }\n\n    /**\n    * Get text from tree element by number\n    * @param number The number of tree folder\n    */\n    async getTextFromNthTreeElement(number: number): Promise<string> {\n        return (await Selector('[role=\"treeitem\"]').nth(number).find('[data-testid^=folder-]').textContent).replace(/\\s/g, '');\n    }\n\n    /**\n    * Open tree folder with multiple level\n    * @param names folder names with sequence of subfolder\n    */\n    async openTreeFolders(names: string[]): Promise<void> {\n        let base = `node-item_${names[0]}`;\n        await this.clickElementIfNotExpanded(base);\n        if (names.length > 1) {\n            for (let i = 1; i < names.length; i++) {\n                base = `${base}:${names[i]}:`;\n                await this.clickElementIfNotExpanded(base);\n            }\n        }\n    }\n\n    /**\n    * Get all keys from tree view list with order\n    */\n    async getAllItemsArray(): Promise<string[]> {\n        const textArray: string[] = [];\n        const treeViewItemElements = Selector('[role=\"treeitem\"]');\n        const itemCount = await treeViewItemElements.count;\n\n        for (let i = 0; i < itemCount; i++) {\n            const treeItem = treeViewItemElements.nth(i);\n            const keyItem = treeItem.find('[data-testid^=\"key-\"]');\n            if (await keyItem.exists) {\n                textArray.push(await keyItem.textContent);\n            }\n        }\n\n        return textArray;\n    }\n\n    /**\n     * click on the folder element if it is not expanded\n     * @param base the base element\n     */\n    private async clickElementIfNotExpanded(base: string): Promise<void> {\n        const baseSelector = Selector(`[data-testid^=\"${base}\"]`);\n        const  elementSelector = await baseSelector.getAttribute('data-testid');\n        if (!elementSelector?.includes('expanded')) {\n            await t.click(Selector(`[data-testid^=\"${base}\"]`));\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/chatbot/ai-chatbot-panel.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { ChatBotTabs } from '../../../helpers/constants';\nimport { DatabaseChatBotTab } from './database-chatbot-tab';\nimport { GeneralChatBotTab } from './general-chatbot-tab';\nimport { RedisCloudSigninPanel } from '../redis-cloud-sign-in-panel';\n\nexport class AiChatBotPanel {\n    RedisCloudSigninPanel = new RedisCloudSigninPanel();\n\n    // CONTAINERS\n    sidePanel = Selector('[data-testid=redis-copilot]');\n    copilotButton = Selector('[data-testid=]');\n    closeButton = Selector('[data-testid=]');\n    activeTab = Selector('[class*=euiTab-isSelected]');\n\n    generalTab = Selector('[data-testid=ai-general-chat_tab]');\n    databaseTab = Selector('[data-testid=eai-database-chat_tab]');\n\n    databasePopover = Selector('[data-testid=]');\n\n    /**\n     * Open/Close  Panel\n     * @param state State of panel\n     */\n    async togglePanel(state: boolean): Promise<void> {\n        const isPanelExists = await this.sidePanel.exists;\n\n        if (state !== isPanelExists) {\n            await t.click(this.copilotButton);\n        }\n    }\n\n    /**\n     * get active tab\n     */\n    async getActiveTabName(): Promise<string> {\n        return this.activeTab.textContent;\n    }\n    /**\n     * Click on Panel tab\n     * @param type of the tab\n     */\n    async setActiveTab(type: ChatBotTabs.Database): Promise<DatabaseChatBotTab>\n    async setActiveTab(type: ChatBotTabs.General): Promise<GeneralChatBotTab>\n    async setActiveTab(type: ChatBotTabs): Promise<DatabaseChatBotTab | GeneralChatBotTab> {\n        const activeTabName  = await this.getActiveTabName();\n        if(type === ChatBotTabs.General) {\n            if(type !== activeTabName) {\n                await t.click(this.generalTab);\n            }\n            return new GeneralChatBotTab();\n        }\n\n        if(type !== activeTabName) {\n            await t.click(this.databaseTab);\n        }\n        return new DatabaseChatBotTab();\n\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/chatbot/chatbot-base-tab.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class ChatBotBaseTab {\n    searchField = Selector('[data-testid=ai-message-textarea]');\n    submitButton = Selector('[data-testid=ai-submit-message-btn]');\n    answer = Selector('[data-testid^=ai-message-AIMessage_]');\n    question = Selector('[data-testid^=ai-message-HumanMessage_]');\n    runAnswerButton =  Selector('[data-testid=run-btn-]');\n    copyAnswerButton =  Selector('[data-testid=copy-btn-]');\n\n    /**\n     * Run question\n     * @param query query to run\n     */\n    async runQuery(query: string): Promise<void> {\n        await t.typeText(this.searchField, query);\n        await t.click(this.submitButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/chatbot/database-chatbot-tab.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { ChatBotBaseTab } from './chatbot-base-tab';\n\nexport class DatabaseChatBotTab extends ChatBotBaseTab{\n    clearSessionButton = Selector('[data-testid=]');\n    databaseName = Selector('[data-testid=]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/chatbot/general-chatbot-tab.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { ChatBotBaseTab } from './chatbot-base-tab';\n\nexport class GeneralChatBotTab extends ChatBotBaseTab {\n    suggestion = Selector('[data-testid*=ai-chat-suggestion_]');\n    restartSessionButton = Selector('[data-testid=ai-general-restart-session-btn]');\n\n    /**\n     * Select question suggestion\n     * @param index of suggestion\n     */\n    async runSuggestion(index: number): Promise<void> {\n        await t.click(this.suggestion.nth(index));\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/common/editorButton.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class EditorButton {\n    cancelBtn = Selector('[data-testid=cancel-btn]');\n    applyBtn = Selector('[data-testid=apply-btn]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/common/modal.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class Modal {\n    closeModalButton = Selector('[class*=euiModal__closeIcon]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/common/toast.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class Toast {\n    toastHeader = Selector('[data-testid=redisui-toast-message]', { timeout: 30000 });\n    toastBody = Selector('[data-testid=redisui-toast-description]');\n    toastSuccess = Selector('[data-testid=redisui-toast]');\n    toastError = Selector('[data-testid=toast-error]', { timeout: 30000 });\n    toastCloseButton = Selector('[data-testid=redisui-toast-action-button]');\n    toastSubmitBtn = Selector('[data-testid=redisui-toast-action-button]');\n    // todo: investigate. there is no cancel button in toast\n    toastCancelBtn = Selector('[data-testid=redisui-toast-action-button]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/explore-tab.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class ExploreTab {\n    dataStructureAccordionTutorialButton = Selector('[data-testid=ri-accordion-header-ds]');\n    internalLinkWorkingWithHashes = Selector('[data-testid=internal-link-ds-hashes]');\n    redisStackTutorialsButton = Selector('[data-testid=accordion-button-redis_stack]');\n    timeSeriesLink = Selector('[data-testid=accordion-button-ds-ts]');\n    internalTimeSeriesLink = Selector('[data-testid=internal-link-ds-ts-ret-compact]');\n    scrolledEnablementArea = Selector('[data-testid=enablement-area__page]');\n    closeEnablementPage = Selector('[data-testid=enablement-area__page-close]');\n    tutorialLinkField = Selector('[data-testid=tutorial-link-field]');\n    tutorialLatestDeleteIcon = Selector('[data-testid^=delete-tutorial-icon-]').nth(0);\n    tutorialDeleteButton = Selector('button[data-testid^=delete-tutorial-]');\n    preselectArea = Selector('[data-testid=enablementArea]');\n    enablementAreaPagination = Selector('[data-testid=enablement-area__toggle-pagination-menu-btn]');\n    paginationPopoverButtons = Selector('[data-testid=enablement-area__pagination-menu] [role=menuitem]');\n    enablementAreaPaginationPopover = Selector('[data-testid=enablement-area__pagination-menu]');\n    nextPageButton = Selector('[data-testid=enablement-area__next-page-btn]');\n    prevPageButton = Selector('[data-testid=enablement-area__prev-page-btn]');\n    guidesGraphAccordion = Selector('[data-testid=accordion-button-graph]');\n    guidesIntroductionGraphLink = Selector('[data-testid=internal-link-introduction]');\n    enablementAreaEmptyContent = Selector('[data-testid=enablement-area__empty-prompt]');\n    tutorialsWorkingWithGraphLink = Selector('[data-testid=internal-link-working_with_graphs]');\n    codeBlock = Selector('[data-testid=code-button-block-content]');\n    codeBlockLabel = Selector('[data-testid=code-button-block-label]');\n    cloudFreeLinkTooltip = Selector('[data-testid=tutorials-get-started-link]');\n    openDatabasePopover = Selector('[data-testid=database-not-opened-popover]');\n    tutorialPopoverConfirmRunButton = Selector('[data-testid=tutorial-popover-apply-run]');\n    //Custom tutorials\n    customTutorials = Selector('[data-testid=ri-accordion-header-custom-tutorials]');\n    tutorialOpenUploadButton = Selector('[data-testid=open-upload-tutorial-btn]');\n    tutorialSubmitButton = Selector('[data-testid=submit-upload-tutorial-btn]');\n    tutorialImport = Selector('[data-testid=import-tutorial]');\n    tutorialAccordionButton = Selector('[data-testid^=ri-accordion-header-]');\n    uploadDataBulkBtn = Selector('[data-testid=upload-data-bulk-btn]');\n    uploadDataBulkApplyBtn = Selector('[data-testid=upload-data-bulk-apply-btn]');\n    downloadFileBtn = Selector('[data-testid=download-redis-upload-file]');\n    tutorialLink = Selector('[data-testid=redisinsight-link]');\n\n    //CSS\n    cssTutorialDeleteIcon = '[data-testid^=delete-tutorial-icon-]';\n\n    runMask = '[data-testid=\"run-btn-$name\"]';\n    copyMask = '[data-testid=\"copy-btn-$name\"]';\n\n    /**\n     * Run code\n     * @param block Name of the block\n     */\n    async copyBlockCode(block: string): Promise<void> {\n        const copyButton = Selector(this.copyMask.replace(/\\$name/g, block));\n        await t.scrollIntoView(copyButton);\n        await t.click(copyButton);\n    }\n\n    /**\n     * Run code\n     * @param block Name of the block\n     */\n    async runBlockCode(block: string): Promise<void> {\n        const runButton = Selector(this.runMask.replace(/\\$name/g, block));\n        await t.scrollIntoView(runButton);\n        await t.click(runButton);\n        if (await this.tutorialPopoverConfirmRunButton.exists) {\n            await t.click(this.tutorialPopoverConfirmRunButton);\n        }\n    }\n\n    /**\n     * get code\n     * @param block Name of the block\n     */\n    async getBlockCode(block: string): Promise<string> {\n        return await this.codeBlockLabel.withExactText(block).parent().parent().nextSibling().textContent;\n    }\n\n    /**\n     * get run selector\n     * @param block Name of the block\n     */\n    getRunSelector(block: string): Selector {\n        return Selector(this.runMask.replace(/\\$name/g, block));\n    }\n\n    /**\n     * Get selector with tutorial name\n     * @param tutorialName name of the uploaded tutorial\n     */\n    getAccordionButtonWithName(tutorialName: string): Selector {\n        return Selector(`[data-testid=ri-accordion-header-${tutorialName}]`);\n    }\n\n    /**\n     * Get internal tutorial link with .md name\n     * @param internalLink name of the .md file\n     */\n    getInternalLinkWithManifest(internalLink: string): Selector {\n        return Selector(`[data-testid=\"internal-link-${internalLink}.md\"]`);\n    }\n\n    /**\n     * Find image in tutorial by alt text\n     * @param alt Image alt text\n     */\n    getTutorialImageByAlt(alt: string): Selector {\n        return Selector('img').withAttribute('alt', alt);\n    }\n\n    /**\n     * Wait until image rendered\n     * @param selector Image selector\n     */\n    async waitUntilImageRendered(selector: Selector): Promise<void> {\n        const searchTimeout = 5 * 1000; // 5 sec maximum wait\n        const startTime = Date.now();\n        let imageHeight;\n\n        do {\n            imageHeight = await selector.getStyleProperty('height');\n        }\n        while ((imageHeight == '0px') && Date.now() - startTime < searchTimeout);\n    }\n\n    /**\n     * Get internal tutorial link without .md name\n     * @param internalLink name of the label\n     */\n    getInternalLinkWithoutManifest(internalLink: string): Selector {\n        return Selector(`[data-testid=\"internal-link-${internalLink}\"]`);\n    }\n\n    /**\n     * Delete tutorial by name\n     * @param name A tutorial name\n     */\n    async deleteTutorialByName(name: string): Promise<void> {\n        const deleteTutorialBtn = this.tutorialAccordionButton.withText(name).find(this.cssTutorialDeleteIcon);\n        if (await this.closeEnablementPage.exists) {\n            await t.click(this.closeEnablementPage);\n        }\n        await this.toggleMyTutorialPanel();\n        await t.click(deleteTutorialBtn);\n        await t.click(this.tutorialDeleteButton);\n    }\n\n    /**\n     * Find tutorial selector by name\n     * @param name A tutorial name\n     */\n    getTutorialByName(name: string): Selector {\n        return Selector('div').withText(name);\n    }\n\n    /**\n     * Expand/Collapse My tutorial Panel\n     * @param state State of panel\n     */\n    async toggleMyTutorialPanel(state: boolean = true): Promise<void> {\n        const currentState = await this.customTutorials.getAttribute('aria-expanded') === 'true';\n        if (currentState !== state) {\n            await t.click(this.customTutorials);\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/insights-panel.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { ExploreTabs } from '../../helpers/constants';\nimport { RecommendationsTab } from './recommendations-tab';\nimport { ExploreTab } from './explore-tab';\n\nexport class InsightsPanel {\n    // CONTAINERS\n    sidePanel = Selector('[data-testid=side-panels-insights]');\n    closeButton = Selector('[data-testid=close-insights-btn]');\n    activeTab = this.sidePanel.find('[data-testid=insights-tabs] [data-state=active]');\n\n    recommendationsTab = this.sidePanel.find('[role=tablist] span').withText(/^Tips/).parent('[role=tab]');\n    exploreTab = this.sidePanel.find('[role=tablist] span').withExactText('Tutorials').parent('[role=tab]');\n    copilotTab = Selector('[data-testid=ai-assistant-tab]');\n\n    existsCompatibilityPopover = Selector('[data-testid=explore-capability-popover]');\n\n    activeTabMask = '[data-testid=insights-tabs] [data-state=active]';\n\n    /**\n     * get active tab\n     */\n    async getActiveTabName(): Promise<string> {\n        return (this.sidePanel.find(this.activeTabMask)).textContent;\n    }\n\n    /**\n     * Click on Panel tab\n     * @param type of the tab\n     */\n    async setActiveTab(type: ExploreTabs.Tutorials): Promise<ExploreTab>\n    async setActiveTab(type: ExploreTabs.Tips): Promise<RecommendationsTab>\n    async setActiveTab(type: ExploreTabs): Promise<ExploreTab | RecommendationsTab> {\n        const activeTabName  = await this.getActiveTabName();\n        if(type === ExploreTabs.Tutorials) {\n            if(type !== activeTabName) {\n                await t.click(this.exploreTab);\n            }\n            return new ExploreTab();\n        }\n\n        if(type !== activeTabName) {\n            await t.click(this.recommendationsTab);\n        }\n        return new RecommendationsTab();\n\n    }\n\n    /**\n     * Get Insights panel selector\n     */\n    getInsightsPanel(): Selector {\n        return Selector('[class=euiButton__text]').withExactText(ExploreTabs.Tips);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/monaco-editor.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class MonacoEditor {\n    //MONACO ELEMENTS\n    monacoCommandDetails = Selector('div.suggest-details-container');\n    monacoSuggestion = Selector('span.monaco-icon-name-container');\n    monacoContextMenu = Selector('div.shadow-root-host').shadowRoot();\n    monacoShortcutInput = Selector('input.input');\n    monacoSuggestionOption = Selector('div.monaco-list-row');\n    monacoHintWithArguments = Selector('[widgetid=\"editor.widget.parameterHintsWidget\"]');\n    monacoCommandIndicator = Selector('div.monaco-glyph-run-command');\n    monacoWidget = Selector('[data-testid=monaco-widget]');\n    monacoSuggestWidget = Selector('.suggest-widget');\n    nonRedisEditorResizeBottom = Selector('.t_resize-bottom');\n    nonRedisEditorResizeTop = Selector('.t_resize-top');\n\n    /**\n     * Send commands in monacoEditor\n     * @param input The input locator\n     * @param command command\n     * @param clean if  field should be cleaned\n     */\n    async sendTextToMonaco(input: Selector, command: string, clean = true): Promise<void> {\n\n        await t.click(input);\n        if (clean) {\n            await t\n                // remove text since replace doesn't work here\n                .pressKey('ctrl+a')\n                .pressKey('delete');\n        }\n        await t.typeText(input, command);\n    }\n\n    /**\n     * Send lines in monacoEditor without additional space that typeText can add\n     * @param input The input locator\n     * @param lines lines\n     * @param depth level of depth of the object\n     */\n    async insertTextByLines(input: Selector, lines: string[], depth: number): Promise<void> {\n        for (let i = 0; i < lines.length; i++) {\n            const line = lines[i];\n\n            for (let j = 0; j < depth; j++) {\n                await t.pressKey('shift+tab');\n            }\n\n            if (line) {\n                await t.typeText(input, line, { paste: true });\n            }\n            await t.pressKey('esc');\n            await t.pressKey('enter');\n        }\n    }\n\n    /**\n     * Get text from monacoEditor\n     */\n    async getTextFromMonaco(): Promise<string> {\n        const textAreaMonaco = Selector('[class^=view-lines ]');\n        return (await textAreaMonaco.textContent).replace(/\\s+/g, ' ');\n    }\n\n    /**\n    * Get suggestions as ordered array from monaco from the beginning\n    * @param suggestions number of elements to get\n    */\n    async getSuggestionsArrayFromMonaco(suggestions: number): Promise<string[]> {\n        const textArray: string[] = [];\n        const suggestionElements = this.monacoSuggestion;\n\n        for (let i = 0; i < suggestions; i++) {\n            const suggestionItem = suggestionElements.nth(i);\n            if (await suggestionItem.exists) {\n                textArray.push(await suggestionItem.textContent);\n            }\n        }\n\n        return textArray;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation/base-navigation-panel.ts",
    "content": "import { Selector } from 'testcafe';\n\nimport NotificationPanel from '././notification-panel';\nimport { HelpCenter } from './help-center';\n\nexport class BaseNavigationPanel {\n    NotificationPanel = new NotificationPanel();\n    HelpCenter  = new HelpCenter();\n\n    myRedisDBButton = Selector('[data-testid=redis-logo-link]', { timeout: 1000 });\n    notificationCenterButton = Selector('[data-testid=notification-menu-button]');\n\n    settingsButton = Selector('[data-testid=settings-page-btn]');\n    helpCenterButton = Selector('[data-testid=help-menu-button]');\n    githubButton = Selector('[data-testid=github-repo-icon]');\n    cloudButton = Selector('[data-testid=cloud-db-icon]');\n\n    buttonsLocator = Selector('[aria-label=\"Main navigation\"] button');\n\n    /**\n     * get buttons count\n     */\n    async getButtonsCount(): Promise<number> {\n        return  await this.buttonsLocator.count;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation/help-center.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class HelpCenter {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    helpCenterSubmitBugButton = Selector('[data-testid=submit-bug-btn]');\n    helpCenterShortcutButton = Selector('[data-testid=shortcuts-btn]');\n    helpCenterReleaseNotesButton = Selector('[data-testid=release-notes-btn]');\n    //PANELS\n    helpCenterPanel = Selector('[data-testid=help-center]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation/navigation-header.ts",
    "content": "import { t, Selector } from 'testcafe';\nimport { InsightsPanel } from '../insights-panel';\n\nexport class NavigationHeader {\n    insightsTriggerButton = Selector('[data-testid=insights-trigger]');\n    cloudSignInButton = Selector('[data-testid=cloud-sign-in-btn]');\n    copilotButton = Selector('[data-testid=copilot-trigger]');\n    dbName = Selector('[data-testid=nav-instance-popover-btn]');\n    dbNameExactText = Selector('[data-testid=nav-instance-popover-btn] p');\n    homeLinkNavigation = Selector('[class*=homePageLink]');\n    dbListInstance = Selector('[data-testid^=instance-item-]');\n    rdiNavigationTab = Selector('[role=tab][id*=\"Redis Data Integration\"]');\n    dbListInput = Selector('[data-testid=instances-nav-popover-search]');\n\n    /**\n     * Open/Close  Panel\n     * @param state State of panel\n     */\n    async togglePanel(state: boolean): Promise<void> {\n        const isPanelExists = await (new InsightsPanel()).sidePanel.exists;\n\n        if (state !== isPanelExists) {\n            await t.click(this.insightsTriggerButton);\n        }\n    }\n\n    /**\n     * Get all databases from List of DBs page\n     */\n    async getAllDatabases(): Promise<string[]> {\n        const databases: string[] = [];\n        const n = await this.dbListInstance.count;\n\n        for(let k = 0; k < n; k++) {\n            const name = await this.dbListInstance.nth(k).textContent;\n            databases.push(name);\n        }\n        return databases;\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation/notification-panel.ts",
    "content": "import { Selector } from 'testcafe';\n\nclass NotificationPanel {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    // CSS Selectors\n    cssNotificationList = '[data-testid=notifications-list]';\n    cssNotificationTitle = '[data-testid=notification-title]';\n    cssNotificationBody = '[data-testid=notification-body]';\n    cssNotificationDate = '[data-testid=notification-date]';\n    //BUTTONS\n    notificationCenterButton = Selector('[data-testid=notification-menu-button]');\n    closeNotificationPopup = Selector('[data-testid=close-notification-btn]');\n    //PANEL\n    notificationCenterPanel = Selector('[data-testid=notification-center]');\n    notificationPopup = Selector('[data-testid=notification-popover]');\n    //TEXT ELEMENTS\n    emptyNotificationMessage = Selector('[data-testid=no-notifications-text]');\n    unreadNotification = Selector('[data-testid^=notification-item-unread]');\n    notificationTitle = Selector(this.cssNotificationTitle);\n    notificationBody = Selector(this.cssNotificationBody);\n    notificationDate = Selector(this.cssNotificationDate);\n    notificationList = Selector(this.cssNotificationList);\n    notificationCategory = Selector('[data-testid=notification-category]');\n    //ICONS\n    notificationBadge = Selector('[data-testid=total-unread-badge]', { timeout: 10000 });\n\n    /**\n     * Get number of unread messages from notification bell\n     */\n    async getUnreadNotificationNumber(): Promise<number> {\n        return parseInt(await this.notificationBadge.textContent);\n    }\n\n    /**\n     * Get number of unread messages from notification bell\n     */\n    async convertEpochDateToMessageDate(notification: NotificationParameters): Promise<string> {\n        const epochTimeConversion = new Date(notification.timestamp * 1000).toDateString();\n        const converted = epochTimeConversion.split(' ');\n        return [converted[2], converted[1], converted[3]].join(' ');\n    }\n}\n/**\n * Notification parameters\n * @param notificationType Type of notification\n * @param notificationDate Date of notification\n * @param notificationTitle Title of notification\n * @param notificationBody Text of notification\n * @param isNotificationRead Identification is message read\n */\nexport type NotificationParameters = {\n    title: string,\n    timestamp: number,\n    body: string,\n    type?: string,\n    isRead?: boolean,\n    category?: string,\n    colorCategory?: string,\n    rbgColor?: string\n};\n\nexport default NotificationPanel;\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation/rdi-navigation-panel.ts",
    "content": "import { Selector } from 'testcafe';\nimport { BaseNavigationPanel } from './base-navigation-panel';\n\nexport class RdiNavigationPanel extends BaseNavigationPanel{\n    statusPageButton = Selector('[data-testid=pipeline-status-page-btn]');\n    managementPageButton = Selector('[data-testid=pipeline-management-page-btn]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation-panel.ts",
    "content": "import { Selector } from 'testcafe';\n\nimport { BaseNavigationPanel } from './navigation/base-navigation-panel';\n\nexport class NavigationPanel extends BaseNavigationPanel{\n    settingsButton = Selector('[data-testid=settings-page-btn]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/navigation-tabs.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class NavigationTabs {\n    browserButton = Selector('[role=tablist] p').withExactText('Browse').parent('[type=button]');\n    workbenchButton = Selector('[role=tablist] p').withExactText('Workbench').parent('[type=button]');\n    analysisButton = Selector('[role=tablist] p').withExactText('Analyze').parent('[type=button]');\n    pubSubButton = Selector('[role=tablist] p').withExactText('Pub/Sub').parent('[type=button]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/overview-panel.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { EditorButton } from './common/editorButton';\n\nexport class OverviewPanel {\n    EditorButton = new EditorButton();\n    // TEXT ELEMENTS\n    overviewTotalKeys = Selector('[data-test-subj=overview-total-keys]');\n    overviewTotalMemory = Selector('[data-test-subj=overview-total-memory]');\n    overviewCpu = Selector('[data-test-subj=overview-cpu]');\n    overviewConnectedClients = Selector('[data-test-subj=overview-connected-clients]');\n    overviewCommandsSec = Selector('[data-test-subj=overview-commands-sec]');\n    overviewSpinner = Selector('[class*=euiLoadingSpinner--medium]');\n    // BUTTONS\n    myRedisDBLink = Selector('[data-testid=my-redis-db-btn]', { timeout: 1000 });\n    changeIndexBtn = Selector('[data-testid=change-index-btn]');\n    databaseInfoIcon = Selector('[data-testid=db-info-icon]');\n    autoRefreshArrow = Selector('[data-testid=auto-refresh-overview-auto-refresh-config-btn]');\n    autoRefreshCheckbox = Selector('[data-testid=auto-refresh-overview-auto-refresh-switch]');\n    // PANEL\n    databaseInfoToolTip = Selector('[data-testid=db-info-tooltip]', { timeout: 2000 });\n    // INPUTS\n    changeIndexInput = Selector('[data-testid=change-index-input]');\n    autoRefreshRateInput = Selector('[data-testid=auto-refresh-overview-refresh-rate]');\n    inlineItemEditor = Selector('[data-testid=inline-item-editor]');\n\n    /**\n     * Change database index\n     * @param dbIndex The index of logical database\n     */\n    async changeDbIndex(dbIndex: number): Promise<void> {\n        await t.click(this.changeIndexBtn)\n            .typeText(this.changeIndexInput, dbIndex.toString(), { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn)\n            .expect(this.changeIndexBtn.textContent).contains(dbIndex.toString());\n    }\n\n    /**\n     * Verify that definite database index selected\n     * @param dbIndex The index of logical database\n     */\n    async verifyDbIndexSelected(dbIndex: number): Promise<void> {\n        await t.expect(this.changeIndexBtn.textContent).contains(dbIndex.toString());\n    }\n\n    /**\n     * wait for cpu is displayed\n     */\n    async waitForCpuIsCalculated(): Promise<void> {\n        await t.expect(this.overviewSpinner.visible).notOk('cpu is not calculated, spinner is still displayed');\n    }\n\n     /**\n     * set auto refresh rate\n     * @param rate rate value\n     */\n    async setAutoRefreshValue(rate: string): Promise<void> {\n        if(!(await this.autoRefreshRateInput.exists)){\n            await t.click(this.autoRefreshArrow)\n        }\n        await t.click(this.autoRefreshRateInput);\n        await t.typeText(this.inlineItemEditor, rate, { replace: true });\n        await t.click(this.EditorButton.applyBtn);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/recommendations-tab.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { RecommendationIds } from '../../helpers/constants';\n\nexport class RecommendationsTab {\n    // CSS Selectors\n    cssKeyName = '[data-testid=recommendation-key-name]';\n    // BUTTONS\n    insightsBtn = Selector('[data-testid=recommendations-trigger]');\n    showHiddenCheckBox = Selector('[data-testid=checkbox-show-hidden]');\n    showHiddenButton = Selector('[data-testid=checkbox-show-hidden ] ~ label');\n    analyzeDatabaseButton = Selector('[data-testid=insights-db-analysis-link]');\n    analyzeTooltipButton = Selector('[data-testid=approve-insights-db-analysis-btn]');\n    // CONTAINERS\n    insightsPanel = Selector('[data-testid=insights-panel]');\n    noRecommendationsScreen = Selector('[data-testid=no-recommendations-screen]');\n    redisVersionRecommendation = Selector('[data-testid=redisVersion-recommendation]');\n    optimizeTimeSeriesRecommendation = Selector('[data-testid=RTS-recommendation]');\n    //LINKS\n    analyzeDatabaseLink = Selector('[data-testid=footer-db-analysis-link]');\n\n    /**\n     * Get Insights panel recommendation selector by name\n     * @param recommendationName name of the recommendation\n     */\n    getRecommendationByName(recommendationName: RecommendationIds): Selector {\n        return Selector(`[data-testid=${recommendationName}-accordion]`);\n    }\n\n    /**\n     * Check/uncheck recommendation\n     * @param state State of panel\n     */\n    async toggleShowHiddenRecommendations(state: boolean): Promise<void> {\n        if(await this.showHiddenCheckBox.exists) {\n            if ((await this.showHiddenCheckBox.checked) !== state) {\n                await t.click(this.showHiddenButton);\n            }\n        }\n    }\n\n    /**\n     * Expand/Collapse Recommendation\n     * @param recommendationName Name of recommendation\n     * @param state State of recommendation\n     */\n    async toggleRecommendation(recommendationName: RecommendationIds, state: boolean): Promise<void> {\n        const recommendationSelector = Selector(`[data-test-subj=${recommendationName}-button]`);\n        const isRecommendationExpanded = await this.getRecommendationByName(recommendationName).withAttribute('class', /-isOpen/).exists;\n\n        if (state !== isRecommendationExpanded) {\n            await t.click(recommendationSelector);\n        }\n    }\n\n    /**\n     * Hide Recommendation\n     * @param recommendationName Name of recommendation\n     */\n    async hideRecommendation(recommendationName: RecommendationIds): Promise<void> {\n        const recommendationHideBtn = Selector(`[data-testid=toggle-hide-${recommendationName}-btn]`);\n        await t.click(recommendationHideBtn);\n    }\n\n    /**\n     * Snooze Recommendation\n     * @param recommendationName Name of recommendation\n     */\n    async snoozeRecommendation(recommendationName: RecommendationIds): Promise<void> {\n        const recommendationSnoozeBtn = Selector(`[data-testid=${recommendationName}-delete-btn]`);\n        await t.click(recommendationSnoozeBtn);\n    }\n    /**\n     * click tutorial button in the recommendation\n     * @param recommendationName Name of recommendation\n     */\n    async clickOnTutorialLink(recommendationName: RecommendationIds): Promise<void> {\n        const tutorialBtn = Selector(`[data-testid=${recommendationName}-to-tutorial-btn]`);\n        await t.click(tutorialBtn);\n    }\n\n    /**\n     * click on navigate button in the recommendation\n     * @param recommendationName Name of recommendation\n     */\n    async clickOnNavigationButton(recommendationName: RecommendationIds): Promise<void> {\n        const button = Selector(`[data-testid^=internal-link-${recommendationName}]`);\n        await t.click(button);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/redis-cloud-sign-in-panel.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class RedisCloudSigninPanel {\n    ssoOauthButton = Selector('[data-testid=sso-oauth]');\n    ssoEmailInput = Selector('[data-testid=sso-email]');\n    submitBtn = Selector('[data-testid=btn-submit]');\n    oauthAgreement = Selector('[for=ouath-agreement]');\n    googleOauth = Selector('[data-testid=google-oauth]');\n    githubOauth = Selector('[data-testid=github-oauth]');\n    ssoOauth = Selector('[data-testid=sso-oauth]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/shortcuts-panel.ts",
    "content": "import { Selector } from 'testcafe';\n\nexport class ShortcutsPanel {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    shortcutsCloseButton = Selector('[role=dialog][title=Shortcuts] button[title=Close]');\n    //TEXT ELEMENTS\n    shortcutsTitle = Selector('[role=dialog][title=Shortcuts] [data-role=\"drawer-heading\"]');\n    shortcutsDesktopApplicationSection = Selector('[data-test-subj=\"shortcuts-section-Desktop application\"]');\n    shortcutsCLISection = Selector('[data-test-subj=shortcuts-section-CLI]');\n    shortcutsWorkbenchSection = Selector('[data-test-subj=shortcuts-section-Workbench]');\n    //PANELS\n    shortcutsPanel = Selector('[data-test-subj=shortcuts-flyout]');\n\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/top-panel/database-overview.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class DatabaseOverview {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    cloudSignInButton = Selector('[data-testid=cloud-sign-in-btn]');\n    databasesBackButton = Selector('[data-testid=my-redis-db-btn]');\n    adminConsoleBackButton = Selector('[data-testid=admin-console-breadcrumb-btn]');\n    copilotTriggerButton = Selector('[data-testid=copilot-trigger]');\n    navInstancesPopoverButton = Selector('[data-testid=nav-instance-popover-btn]');\n    userProfileButton = Selector('[data-testid=user-profile-btn]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/components/top-panel/index.ts",
    "content": "import { DatabaseOverview } from './database-overview';\n\nexport {\n    DatabaseOverview,\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/add-rdi-instance-dialog.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class AddRdiInstanceDialog {\n    //INPUTS\n    rdiAliasInput = Selector('[data-testid=connection-form-name-input]');\n    urlInput = Selector('[data-testid=connection-form-url-input]');\n    usernameInput = Selector('[data-testid=connection-form-username-input]');\n    passwordInput = Selector('[data-testid=connection-form-password-input]');\n\n    //BUTTONS\n    addInstanceButton = Selector('[data-testid=connection-form-add-button]');\n    cancelInstanceBtn = Selector('[data-testid=connection-form-cancel-button]');\n\n    connectToRdiForm = Selector('[data-testid=connection-form]');\n    // ICONS\n    urlInputInfoIcon = Selector('[data-testid=connection-form-url-input]').parent('div').parent('div').find('svg');\n    usernameInputInfoIcon = Selector('[data-testid=connection-form-username-input]').parent('div').parent('div').find('svg');\n    passwordInputInfoIcon = Selector('[data-testid=connection-form-password-input]').parent('div').parent('div').find('svg');\n}\n\n/**\n * String key parameters\n * @param alias The name of the rdi\n * @param url The url for rdi\n * @param version The version for rdi\n * @param lastConnection The last Connection to the rdi instance\n * @param username The username for rdi\n * @param password The password for rdi\n */\nexport type RdiInstance = {\n    alias: string,\n    url: string,\n    version?: string,\n    lastConnection?: string,\n    username?: string,\n    password?: string\n};\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/add-redis-database-dialog.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { TlsCertificates } from '../../helpers/constants';\nimport { RedisCloudSigninPanel } from '../components/redis-cloud-sign-in-panel';\n\nexport class AddRedisDatabaseDialog {\n    RedisCloudSigninPanel = new RedisCloudSigninPanel();\n\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    // BUTTONS\n    addDatabaseButton = Selector('[data-testid^=add-redis-database]');\n    addRedisDatabaseButton = Selector('[data-testid=btn-submit]');\n    addRedisDatabaseButtonHover = Selector('[data-testid=btn-submit]').parent();\n    customSettingsButton = Selector('[data-testid=btn-connection-settings]');\n    addAutoDiscoverDatabase = Selector('[data-testid=add-database_tab_software]');\n    addCloudDatabaseButton = Selector('[data-testid=create-free-db-btn]');\n    redisSotfwareButton = Selector('[data-testid=option-btn-software]');\n    redisSentinelButton = Selector('[data-testid=option-btn-sentinel]');\n    showDatabasesButton = Selector('[data-testid=btn-show-databases]');\n    databaseName = Selector('.euiTableCellContent.column_name');\n    selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]');\n    databaseIndexCheckbox = Selector('[data-testid=showDb]', { timeout: 500 });\n    connectToRedisStackButton = Selector('[aria-label=\"Connect to database\"]');\n    cloneDatabaseButton = Selector('[data-testid=clone-db-btn]');\n    cancelButton = Selector('[data-testid=btn-cancel]');\n    testConnectionBtn = Selector('[data-testid=btn-test-connection]');\n    testConnectionBtnHover = Selector('[data-testid=btn-test-connection]').parent();\n    backButton = Selector('[data-testid=back-btn]');\n    generalTab = Selector('[data-testid=manual-form-tabs] [role=tab][id*=-general]');\n    securityTab = Selector('[data-testid=manual-form-tabs] [role=tab][id*=-security]');\n    decompressionTab = Selector('[data-testid=manual-form-tab-decompression]');\n\n    // TEXT INPUTS (also referred to as 'Text fields')\n    disabledDatabaseInfo = Selector('[class=euiListGroupItem__label]');\n    hostInput = Selector('[data-testid=host]');\n    portInput = Selector('[data-testid=port]');\n    databaseAliasInput = Selector('[data-testid=name]');\n    passwordInput = Selector('[data-testid=password]');\n    usernameInput = Selector('[data-testid=username]');\n    connectionUrlInput = Selector('[data-testid=connection-url]');\n    accessKeyInput = Selector('[data-testid=access-key]');\n    secretKeyInput = Selector('[data-testid=secret-key]');\n    databaseIndexInput = Selector('[data-testid=db]');\n    databaseIndexMessage = Selector('[data-testid=db-index-message]');\n    primaryGroupNameInput = Selector('[data-testid=primary-group]');\n    masterGroupPassword = Selector('[data-testid=sentinel-master-password]');\n    connectionType = Selector('[data-testid=connection-type]');\n    sentinelForm = Selector('[data-testid=form]');\n    sshHostInput = Selector('[data-testid=sshHost]');\n    sshPortInput = Selector('[data-testid=sshPort]');\n    sshUsernameInput = Selector('[data-testid=sshUsername]');\n    sshPasswordInput = Selector('[data-testid=sshPassword]');\n    sshPrivateKeyInput = Selector('[data-testid=sshPrivateKey]');\n    sshPassphraseInput = Selector('[data-testid=sshPassphrase]');\n    timeoutInput = Selector('[data-testid=timeout]');\n    // DROPDOWNS\n    caCertField = Selector('[data-testid=select-ca-cert]', { timeout: 500 });\n    clientCertField = Selector('[data-testid=select-cert]', { timeout: 500 });\n    selectCompressor = Selector('[data-testid=select-compressor]', { timeout: 1000 });\n    certificateDropdownList = Selector('div.euiSuperSelect__listbox div');\n    // CHECKBOXES\n    useSSHCheckbox = Selector('[data-testid=use-ssh] ~ label', { timeout: 500 });\n    dataCompressorCheckbox = Selector('[data-testid=showCompressor] ~ label');\n    requiresTlsClientCheckbox = Selector('[data-testid=tls-required-checkbox]  ~ label');\n    useCloudAccount = Selector('#cloud-account').parent();\n    useCloudKeys = Selector('#cloud-api-keys').parent();\n    // RADIO BUTTONS\n    sshPasswordRadioBtn = Selector('[for=\"password\"]', { timeout: 500 });\n    sshPrivateKeyRadioBtn = Selector('[for=\"privateKey\"]', { timeout: 500 });\n    cloudOptionsRadioBtn =  Selector('[data-testid=cloud-options]');\n    // LABELS\n    dataCompressorLabel = Selector('[data-testid=showCompressor] ~ label', { timeout: 1000 });\n    aiChatMessage = Selector('[data-testid=ai-chat-message-btn]');\n    aiCloseMessage = Selector('[aria-label=\"Closes this modal window\"]');\n\n    trashIconMsk = (certificate: TlsCertificates) => `[data-testid^=delete-${certificate}-cert]`\n\n    getDeleteCertificate = (certificate: TlsCertificates) => Selector(this.trashIconMsk(certificate));\n\n    /**\n     * Adding a new redis database\n     * @param parameters the parameters of the database\n     */\n    async addRedisDataBase(parameters: AddNewDatabaseParameters): Promise<void> {\n\n        await this.addDatabaseButton.with({ visibilityCheck: true, timeout: 10000 })();\n        await t\n            .click(this.addDatabaseButton)\n            .click(this.customSettingsButton);\n\n        await t\n            .typeText(this.hostInput, parameters.host, { replace: true, paste: true })\n            .typeText(this.portInput, parameters.port, { replace: true, paste: true })\n            .typeText(this.databaseAliasInput, parameters.databaseName!, { replace: true, paste: true });\n        if (!!parameters.databaseUsername) {\n            await t.typeText(this.usernameInput, parameters.databaseUsername, { replace: true, paste: true });\n        }\n        if (!!parameters.databasePassword) {\n            await t.typeText(this.passwordInput, parameters.databasePassword, { replace: true, paste: true });\n        }\n    }\n\n    /**\n     * Adding a new redis database with index\n     * @param parameters the parameters of the database\n     * @param index the logical index of database\n     */\n    async addLogicalRedisDatabase(parameters: AddNewDatabaseParameters, index: string): Promise<void> {\n        await t\n            .click(this.addDatabaseButton)\n            .click(this.customSettingsButton);\n\n        await t\n            .typeText(this.hostInput, parameters.host, { replace: true, paste: true })\n            .typeText(this.portInput, parameters.port, { replace: true, paste: true })\n            .typeText(this.databaseAliasInput, parameters.databaseName!, { replace: true, paste: true });\n        if (!!parameters.databaseUsername) {\n            await t.typeText(this.usernameInput, parameters.databaseUsername, { replace: true, paste: true });\n        }\n        if (!!parameters.databasePassword) {\n            await t.typeText(this.passwordInput, parameters.databasePassword, { replace: true, paste: true });\n        }\n        // Enter logical index\n        await t.click(this.databaseIndexCheckbox);\n        await t.typeText(this.databaseIndexInput, index, { replace: true, paste: true });\n        // Click for saving\n        await t.click(this.addRedisDatabaseButton);\n    }\n\n    /**\n     * Adding a new standalone database with SSH\n     * @param databaseParameters the parameters of the database\n     * @param sshParameters the parameters of ssh\n     */\n    async addStandaloneSSHDatabase(databaseParameters: AddNewDatabaseParameters, sshParameters: SSHParameters): Promise<void> {\n\n        await t\n            .click(this.addDatabaseButton)\n            .click(this.customSettingsButton);\n\n        await t\n            .typeText(this.hostInput, databaseParameters.host, { replace: true, paste: true })\n            .typeText(this.portInput, databaseParameters.port, { replace: true, paste: true })\n            .typeText(this.databaseAliasInput, databaseParameters.databaseName!, { replace: true, paste: true });\n        if (!!databaseParameters.databaseUsername) {\n            await t.typeText(this.usernameInput, databaseParameters.databaseUsername, { replace: true, paste: true });\n        }\n        if (!!databaseParameters.databasePassword) {\n            await t.typeText(this.passwordInput, databaseParameters.databasePassword, { replace: true, paste: true });\n        }\n        // Select SSH Tunnel checkbox\n        await t.click(this.securityTab);\n        await t.click(this.useSSHCheckbox);\n        // Enter SSH fields\n        await t\n            .typeText(this.sshHostInput, sshParameters.sshHost, { replace: true, paste: true })\n            .typeText(this.sshPortInput, sshParameters.sshPort, { replace: true, paste: true })\n            .typeText(this.sshUsernameInput, sshParameters.sshUsername, { replace: true, paste: true });\n        if (!!sshParameters.sshPassword) {\n            await t.typeText(this.sshPasswordInput, sshParameters.sshPassword, { replace: true, paste: true });\n        }\n        if (!!sshParameters.sshPrivateKey) {\n            await t\n                .click(this.sshPrivateKeyRadioBtn)\n                .typeText(this.sshPrivateKeyInput, sshParameters.sshPrivateKey, { replace: true, paste: true });\n        }\n        if (!!sshParameters.sshPassphrase) {\n            await t\n                .click(this.sshPrivateKeyRadioBtn)\n                .typeText(this.sshPrivateKeyInput, sshParameters.sshPrivateKey!, { replace: true, paste: true })\n                .typeText(this.sshPassphraseInput, sshParameters.sshPassphrase, { replace: true, paste: true });\n        }\n        // Click for saving\n        await t.click(this.addRedisDatabaseButton);\n    }\n\n    /**\n     * Auto-discover Master Groups from Sentinel\n     * @param parameters - Parameters of Sentinel: host, port and Sentinel password\n     */\n    async discoverSentinelDatabases(parameters: SentinelParameters): Promise<void> {\n\n        await t\n            .click(this.addDatabaseButton)\n\n        await t.click(this.redisSentinelButton);\n        if (!!parameters.sentinelHost) {\n            await t.typeText(this.hostInput, parameters.sentinelHost, { replace: true, paste: true });\n        }\n        if (!!parameters.sentinelPort) {\n            await t.typeText(this.portInput, parameters.sentinelPort, { replace: true, paste: true });\n        }\n        if (!!parameters.sentinelPassword) {\n            await t.typeText(this.passwordInput, parameters.sentinelPassword, { replace: true, paste: true });\n        }\n    }\n\n    /**\n     * Adding a new database from RE Cluster via auto-discover flow\n     * @param parameters the parameters of the database\n     */\n    async addAutodiscoverRedisSoftwareDatabase(parameters: AddNewDatabaseParameters): Promise<void> {\n\n        await t\n            .click(this.addDatabaseButton)\n\n        await t.click(this.redisSotfwareButton);\n        await t\n            .typeText(this.hostInput, parameters.host, { replace: true, paste: true })\n            .typeText(this.portInput, parameters.port, { replace: true, paste: true })\n            .typeText(this.usernameInput, parameters.databaseUsername!, { replace: true, paste: true })\n            .typeText(this.passwordInput, parameters.databasePassword!, { replace: true, paste: true });\n    }\n\n    /**\n     * Adding a new database from RE Cloud via auto-discover flow\n     * @param parameters the parameters of the database\n     */\n    async addAutodiscoverRedisCloudDatabase(cloudAPIAccessKey: string, cloudAPISecretKey: string): Promise<void> {\n\n        await t\n            .click(this.addDatabaseButton)\n            .click(this.addCloudDatabaseButton);\n\n        await t\n            .typeText(this.accessKeyInput, cloudAPIAccessKey, { replace: true, paste: true })\n            .typeText(this.secretKeyInput, cloudAPISecretKey, { replace: true, paste: true });\n    }\n\n    /**\n     * Auto-discover Master Groups from Sentinel\n     * @param parameters - Parameters of Sentinel: host, port and Sentinel password\n     */\n    async addOssClusterDatabase(parameters: OSSClusterParameters): Promise<void> {\n\n        await t\n            .click(this.addDatabaseButton)\n            .click(this.customSettingsButton);\n\n        if (!!parameters.ossClusterHost) {\n            await t.typeText(this.hostInput, parameters.ossClusterHost, { replace: true, paste: true });\n        }\n        if (!!parameters.ossClusterPort) {\n            await t.typeText(this.portInput, parameters.ossClusterPort, { replace: true, paste: true });\n        }\n        if (!!parameters.ossClusterDatabaseName) {\n            await t.typeText(this.databaseAliasInput, parameters.ossClusterDatabaseName, { replace: true, paste: true });\n        }\n    }\n\n    /**\n     * set copressor value in dropdown\n     * @param compressor - compressor value\n     */\n    async setCompressorValue(compressor: string): Promise<void> {\n        if(!await this.selectCompressor.exists) {\n            await t.click(this.dataCompressorLabel);\n        }\n\n        await t.click(this.selectCompressor);\n        await t.click(Selector(`[id=\"${compressor}\"]`));\n    }\n\n    /**\n     * Remove certificate\n     * @param certificate - certificate\n     * @param name - name of the certificate\n     */\n    async removeCertificateButton(certificate: TlsCertificates, name: string): Promise<void> {\n        await t.click(this.securityTab);\n        const row =  Selector('button')\n            .find('div')\n            .withText(name);\n        const removeButton = this.trashIconMsk(certificate);\n        const removeButtonFooter = Selector('[class^=_popoverFooter]');\n\n        if (certificate === TlsCertificates.CA) {\n            await t.click(this.caCertField);\n        } else {\n            await t.click(this.clientCertField);\n        }\n\n        await t.click(row.find(removeButton));\n\n        await t.click(removeButtonFooter.find(removeButton));\n    }\n}\n\n/**\n * Add new database parameters\n * @param host The hostname of the database\n * @param port The port of the database\n * @param databaseName The name of the database\n * @param databaseUsername The username of the database\n * @param databasePassword The password of the database\n */\nexport type AddNewDatabaseParameters = {\n    host: string,\n    port: string,\n    databaseName?: string,\n    databaseUsername?: string,\n    databasePassword?: string,\n    caCert?: {\n        name?: string,\n        certificate?: string\n    },\n    clientCert?: {\n        name?: string,\n        certificate?: string,\n        key?: string\n    }\n};\n\n/**\n * Sentinel database parameters\n * @param sentinelHost The host of sentinel\n * @param sentinelPort The port of sentinel\n * @param sentinelPassword The password of sentinel\n */\nexport type SentinelParameters = {\n    sentinelHost: string,\n    sentinelPort: string,\n    masters?: {\n        alias?: string,\n        db?: string,\n        name?: string,\n        password?: string\n    }[],\n    sentinelPassword?: string,\n    name?: string[]\n};\n\n/**\n * OSS Cluster database parameters\n * @param ossClusterHost The host of OSS Cluster\n * @param ossClusterPort The port of OSS Cluster\n * @param ossClusterDatabaseName Database name for OSS Cluster\n */\n\nexport type OSSClusterParameters = {\n    ossClusterHost: string,\n    ossClusterPort: string,\n    ossClusterDatabaseName: string\n};\n\n/**\n * Already existing database parameters\n * @param id The id of the database\n * @param host The host of the database\n * @param port The port of the database\n * @param name The name of the database\n * @param connectionType The connection type of the database\n * @param lastConnection The last connection time of the database\n */\nexport type databaseParameters = {\n    id: string,\n    host?: string,\n    port?: string,\n    name?: string,\n    connectionType?: string,\n    lastConnection?: string\n};\n\n/**\n * Nodes in OSS Cluster parameters\n * @param host The host of the node\n * @param port The port of the node\n */\nexport type ClusterNodes = {\n    host: string,\n    port: string\n};\n\n/**\n * SSH parameters\n * @param sshHost The hostname of ssh\n * @param sshPort The port of ssh\n * @param sshUsername The username of ssh\n * @param sshPassword The password of ssh\n * @param sshPrivateKey The private key of ssh\n * @param sshPassphrase The passphrase of ssh\n */\nexport type SSHParameters = {\n    sshHost: string,\n    sshPort: string,\n    sshUsername: string,\n    sshPassword?: string,\n    sshPrivateKey?: string,\n    sshPassphrase?: string\n};\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/authorization-dialog.ts",
    "content": "import { Selector } from 'testcafe';\nimport { RedisCloudSigninPanel } from '../components/redis-cloud-sign-in-panel';\n\nexport class AuthorizationDialog {\n    RedisCloudSigninPanel = new RedisCloudSigninPanel();\n\n    //COMPONENTS\n    authDialog = Selector('[data-testid=social-oauth-dialog]');\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/filters-dialog.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class FiltersDialog {\n    // INPUTS\n    delimiterCombobox = Selector('[data-testid=delimiter-combobox]');\n    delimiterComboboxInput = Selector('[data-testid=delimiter-combobox] input[data-test-subj=autoTagInput]');\n    // BUTTONS\n    treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]');\n    treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]');\n    sortingBtn = Selector('[data-testid=tree-view-sorting-select]');\n    sortingASCoption = Selector('[data-testid=tree-view-sorting-item-ASC]').parent('[role=option]');\n    sortingDESCoption = Selector('[data-testid=tree-view-sorting-item-DESC]').parent('[role=option]');\n\n    /**\n     * Get Delimiter badge selector by title\n     * @param delimiterTitle title of the delimiter item\n     */\n    getDelimiterBadgeByTitle(delimiterTitle: string): Selector {\n        return this.delimiterCombobox.find(`span[title='${delimiterTitle}']`);\n    }\n\n    /**\n     * Get Delimiter close button selector by title\n     * @param delimiterTitle title of the delimiter item\n     */\n    getDelimiterCloseBtnByTitle(delimiterTitle: string): Selector {\n        return this.getDelimiterBadgeByTitle(delimiterTitle).find('button');\n    }\n\n    /**\n     * Add new delimiter\n     * @param delimiterName name of the delimiter item\n     */\n     async addDelimiterItem(delimiterName: string): Promise<void> {\n        await t.click(this.delimiterComboboxInput);\n        await t.typeText(this.delimiterComboboxInput, delimiterName, { paste: true }).pressKey('enter')\n    }\n\n    /**\n     * Delete existing delimiter\n     * @param delimiterName name of the delimiter item\n     */\n    async removeDelimiterItem(delimiterName: string): Promise<void> {\n        await t.click(this.getDelimiterCloseBtnByTitle(delimiterName));\n    }\n\n    /**\n     * Remove all existing delimiters in combobox\n     */\n    async clearDelimiterCombobox(): Promise<void> {\n        const delimiters = this.delimiterCombobox.find('button');\n        const count = await delimiters.count;\n        for (let i = 0; i < count; i++) {\n            await t.click(delimiters.nth(i));\n        }\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/index.ts",
    "content": "import { AddRedisDatabaseDialog } from './add-redis-database-dialog';\nimport { AuthorizationDialog } from './authorization-dialog';\nimport { OnboardingCardsDialog } from './onboarding-cards-dialog';\nimport { FiltersDialog } from './filters-dialog';\nimport { UserAgreementDialog } from './user-agreement-dialog';\n\n\nexport {\n    AddRedisDatabaseDialog,\n    AuthorizationDialog,\n    OnboardingCardsDialog,\n    FiltersDialog,\n    UserAgreementDialog\n};\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/onboarding-cards-dialog.ts",
    "content": "import { Selector, t } from 'testcafe';\n\nexport class OnboardingCardsDialog {\n    backButton = Selector('[data-testid=back-btn]');\n    nextButton = Selector('[data-testid=next-btn]');\n    showMeAroundButton = Selector('[data-testid=start-tour-btn]');\n    skipTourButton = Selector('[data-testid=skip-tour-btn]');\n    stepTitle = Selector('[data-testid=step-title]');\n    wbOnbardingCommand = Selector('[data-testid=wb-onboarding-command]');\n    copyCodeButton = Selector('[data-testid=copy-code-btn]');\n    resetOnboardingBtn = Selector('[data-testid=reset-onboarding-btn]');\n\n    /**\n     * Verify onboarding step visible based on title\n     * @param stepName title of the step\n     */\n    async verifyStepVisible(stepName: string): Promise<void> {\n        await t.expect(this.stepTitle.withText(stepName).exists).ok(`${stepName} step is not visible`);\n    }\n    /**\n     Click next step\n     */\n    async clickNextStep(): Promise<void> {\n        await t.click(this.nextButton);\n    }\n    /**\n     Click next step until the last step\n     */\n    async clickNextUntilLastStep(): Promise<void> {\n        do {\n            await this.clickNextStep();\n        }\n        while (await this.skipTourButton.visible);\n    }\n    /**\n     Start onboarding process\n     */\n    async startOnboarding(): Promise<void> {\n        await t.click(this.showMeAroundButton);\n    }\n    /**\n     Complete onboarding process\n     */\n    async completeOnboarding(): Promise<void> {\n        await t.expect(this.showMeAroundButton.exists).notOk('Show me around button still visible');\n        await t.expect(this.stepTitle.exists).notOk('Onboarding tooltip still visible');\n    }\n    /**\n     Click back step\n     */\n    async clickBackStep(): Promise<void> {\n        await t.click(this.backButton);\n    }\n    /**\n     Click skip tour step\n     */\n    async clickSkipTour(): Promise<void> {\n        await t.click(this.skipTourButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/dialogs/user-agreement-dialog.ts",
    "content": "import { t, Selector } from 'testcafe';\n\nexport class UserAgreementDialog {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //COMPONENTS\n    userAgreementsPopup = Selector('[data-testid=consents-settings-popup]');\n    //BUTTONS\n    submitButton = Selector('[data-testid=btn-submit]');\n    switchOptionEula = Selector('[data-testid=switch-option-eula]');\n    switchOptionEncryption = Selector('[data-testid=switch-option-encryption]');\n    pluginSectionWithText = Selector('[data-testid=plugin-section]');\n    recommendedSwitcher = Selector('[data-testid=switch-option-recommended]');\n\n    //Accept Redis Insight License Terms\n    async acceptLicenseTerms(): Promise<void> {\n        if (await this.switchOptionEula.exists) {\n            await t\n                .click(this.recommendedSwitcher)\n                .click(this.switchOptionEula)\n                .click(this.submitButton)\n                .expect(this.userAgreementsPopup.exists).notOk('The user agreements popup is not shown', { timeout: 2000 });\n        }\n    }\n\n    /**\n     * Get state of Recommended switcher\n     */\n    async getRecommendedSwitcherValue(): Promise<string | null> {\n        return await this.recommendedSwitcher.getAttribute('aria-checked');\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/enhanced-selector.ts",
    "content": "import { Selector } from 'testcafe'\n\nexport interface EnhancedSelector extends Selector {\n    textContentWithoutButtons: Promise<string>;\n}\n\nexport const createEnhancedSelector = (selector: string): EnhancedSelector => {\n    return Selector(selector).addCustomDOMProperties({\n        textContentWithoutButtons: el => {\n            const clone = el.cloneNode(true) as HTMLElement;\n            clone.querySelectorAll('button').forEach(btn => btn.remove());\n            clone.querySelectorAll('svg').forEach(btn => btn.remove());\n            return clone.textContent?.trim() ?? '';\n        }\n    }) as unknown as EnhancedSelector\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/index.ts",
    "content": "import { AutoDiscoverREDatabases } from './auto-discover-redis-enterprise-databases';\nimport { BrowserPage } from './browser-page';\nimport { MyRedisDatabasePage } from './my-redis-databases-page';\nimport { SettingsPage } from './settings-page';\nimport { WorkbenchPage } from './workbench-page';\nimport { MemoryEfficiencyPage } from './memory-efficiency-page';\nimport { ClusterDetailsPage } from './cluster-details-page';\nimport { PubSubPage } from './pub-sub-page';\nimport { SlowLogPage } from './slow-log-page';\nimport { SsoAuthorizationPage } from './sso-authorization-page';\nimport { BasePage } from './base-page';\nimport { InstancePage } from './instance-page';\n\nexport {\n    AutoDiscoverREDatabases,\n    BrowserPage,\n    MyRedisDatabasePage,\n    SettingsPage,\n    WorkbenchPage,\n    MemoryEfficiencyPage,\n    ClusterDetailsPage,\n    PubSubPage,\n    SlowLogPage,\n    SsoAuthorizationPage,\n    BasePage,\n    InstancePage,\n};\n"
  },
  {
    "path": "tests/e2e/pageObjects/instance-page.ts",
    "content": "import { BasePage } from './base-page';\nimport { Profiler, Cli, CommandHelper, SurveyLink } from './components/bottom-panel';\nimport { OverviewPanel } from './components/overview-panel';\nimport { InsightsPanel } from './components/insights-panel';\nimport { MonacoEditor } from './components/monaco-editor';\nimport { NavigationHeader } from './components/navigation/navigation-header';\nimport { DatabaseOverview } from './components/top-panel';\nexport class InstancePage extends BasePage {\n    Profiler = new Profiler();\n    Cli = new Cli();\n    CommandHelper = new CommandHelper();\n    DatabaseOverview = new DatabaseOverview();\n    SurveyLink = new SurveyLink();\n    OverviewPanel = new OverviewPanel();\n    InsightsPanel = new InsightsPanel();\n    MonacoEditor = new MonacoEditor();\n    NavigationHeader = new NavigationHeader();\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/memory-efficiency-page.ts",
    "content": "import { Selector } from 'testcafe';\nimport { RecommendationIds } from '../helpers/constants';\nimport { InstancePage } from './instance-page';\n\nexport class MemoryEfficiencyPage extends InstancePage {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    // CSS Selectors\n    cssReadMoreLink = '[data-testid=read-more-link]';\n    cssKeyName = '[data-testid=recommendation-key-name]';\n    // BUTTONS\n    databaseAnalysisTab = Selector('[data-testid=analytics-tabs] [role=tab] p').withText('Database Analysis').parent('[role=tab]');\n    newReportBtn = Selector('[data-testid=start-database-analysis-btn]');\n    sortByKeyPattern = Selector('[data-testid=tableHeaderSortButton]');\n    showNoExpiryToggle = Selector('[data-testid=show-no-expiry-switch]');\n    reportItem = Selector('[role=listbox] [data-test-subj^=items-report-]').parent('[role=option]');\n    selectedReport = Selector('[data-testid=select-report]');\n    sortByLength = Selector('[data-testid=btn-change-table-keys]');\n    recommendationsTab = Selector('[data-testid=database-analysis-tabs] [role=tab] p').withText(/^Tips/).parent('[role=tab]');\n\n    veryUsefulVoteBtn = Selector('[data-testid=very-useful-vote-btn]').nth(0);\n    usefulVoteBtn = Selector('[data-testid=useful-vote-btn]').nth(0);\n    notUsefulVoteBtn = Selector('[data-testid=not-useful-vote-btn]').nth(0);\n    recommendationsFeedbackBtn = Selector('[data-testid=recommendation-feedback-btn]');\n    // ICONS\n    reportTooltipIcon = Selector('[data-testid=db-new-reports-icon]');\n    // TEXT ELEMENTS\n    noReportsText = Selector('[data-testid=empty-analysis-no-reports]');\n    noKeysText = Selector('[data-testid=empty-analysis-no-keys]');\n    scannedPercentageInReport = Selector('[data-testid=analysis-progress]');\n    scannedKeysInReport = Selector('[data-testid=bulk-delete-summary]');\n    topKeysTitle = Selector('[data-testid=top-keys-title]');\n    topKeysKeyName = Selector('[data-testid=top-keys-table-name]');\n    topNamespacesEmptyContainer = Selector('[data-testid=top-namespaces-empty]');\n    topNamespacesEmptyMessage = Selector('[data-testid=top-namespaces-message]');\n    noRecommendationsMessage =  Selector('[data-testid=empty-recommendations-message]');\n    codeChangesLabel = Selector('[data-testid=code_changes]');\n    configurationChangesLabel = Selector('[data-testid=configuration_changes]');\n    topKeysKeySizeCell = Selector('[data-testid^=nsp-usedMemory-value]');\n    topKeysLengthCell = Selector('[data-testid^=length-value]');\n    // TABLE\n    namespaceTable = Selector('[data-testid=nsp-table-memory]');\n    nameSpaceTableRows = this.namespaceTable.find('tbody tr');\n    nspTableExpandArrowBtn = this.nameSpaceTableRows.find('td:nth-child(5) button');\n    expandedRow = Selector('[data-testid^=expanded-]');\n    expandedItem = this.expandedRow.find('button');\n    tableKeyPatternHeader = this.namespaceTable.find('th:nth-child(1)');\n    tableMemoryHeader = this.namespaceTable.find('th:nth-child(3)');\n    tableKeysHeader = this.namespaceTable.find('th:nth-child(4)');\n    // GRAPH ELEMENTS\n    donutTotalKeys = Selector('[data-testid=donut-title-keys]');\n    firstPoint = Selector('[data-testid*=bar-3600]');\n    thirdPoint = Selector('[data-testid*=bar-43200]');\n    fourthPoint = Selector('[data-testid*=bar-86400]');\n    noExpiryPoint = Selector('[data-testid*=bar-0-]:not(rect[data-testid=bar-0-0])');\n    // LINKS\n    treeViewLink = Selector('[data-testid=tree-view-page-link]');\n    readMoreLink = Selector('[data-testid=read-more-link]');\n    workbenchLink = Selector('[data-test-subj=workbench-page-btn]');\n    // CONTAINERS\n    analysisPage = Selector('[data-testid=database-analysis-page]');\n\n    /**\n     * Get recommendation selector by name\n     * @param recommendationName Name of the recommendation\n     */\n    getRecommendationByName(recommendationName: RecommendationIds): Selector {\n        return Selector(`[data-testid=${recommendationName}-recommendation]`);\n    }\n\n    /**\n     * Get recommendation label by recommendation name\n     * @param recommendationName Name of the recommendation\n     * @param label Label of changes\n     */\n    getRecommendationLabelByName(recommendationName: RecommendationIds, label: string): Selector {\n        return this.getRecommendationByName(recommendationName).find(`[data-testid=${label}_changes]`);\n    }\n\n    /**\n     * Get recommendation expand/collapse button by recommendation name\n     * @param recommendationName Name of the recommendation\n     */\n    getRecommendationButtonByName(recommendationName: RecommendationIds): Selector {\n        return Selector(`[data-testid=ri-accordion-header-${recommendationName}]`);\n    }\n    /**\n     * Get recommendation Tutorial button by recommendation name\n     * @param recommendationName Name of the recommendation\n     */\n    getToTutorialBtnByRecomName(recommendationName: RecommendationIds): Selector {\n        return Selector(`[data-testid=${recommendationName}-to-tutorial-btn]`);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/my-redis-databases-page.ts",
    "content": "import { t, Selector } from 'testcafe';\nimport { DatabaseAPIRequests } from '../helpers/api/api-database';\nimport { InsightsPanel } from './components/insights-panel';\nimport { BaseOverviewPage } from './base-overview-page';\nimport { NavigationPanel } from './components/navigation-panel';\nimport { NavigationHeader } from './components/navigation/navigation-header';\nimport { AuthorizationDialog } from './dialogs/authorization-dialog';\nimport { AddRedisDatabaseDialog } from './dialogs';\n\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nexport class MyRedisDatabasePage extends BaseOverviewPage {\n\n    NavigationPanel = new NavigationPanel();\n    AddRedisDatabaseDialog = new AddRedisDatabaseDialog();\n    InsightsPanel = new InsightsPanel();\n    NavigationHeader = new NavigationHeader();\n    AuthorizationDialog = new AuthorizationDialog();\n\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    // CSS Selectors\n    cssNumberOfDbs = '[data-testid=number-of-dbs]';\n    cssRedisStackIcon = '[data-testid=redis-stack-icon]';\n    //BUTTONS\n    deleteDatabaseButton = Selector('[data-testid^=delete-instance-]');\n    confirmDeleteButton = Selector('[data-testid^=delete-instance-]').withExactText('Remove');\n    deleteButtonInPopover = Selector('#deletePopover button');\n    confirmDeleteAllDbButton = Selector('[data-testid=delete-selected-dbs]');\n    editDatabaseButton = Selector('[data-testid^=edit-instance]');\n    popoverHeader = Selector('#formModalHeader');\n    submitChangesButton = Selector('[data-testid=btn-submit]');\n    promoButton = Selector('[data-testid=promo-btn]');\n    sortByDatabaseAlias = Selector('table th').withText('Database Alias');\n    sortByHostAndPort = Selector('table th').withText('Host:Port');\n    sortByConnectionType = Selector('table th').withText('Connection Type');\n    importDatabasesBtn = Selector('[data-testid=option-btn-import]');\n    retryImportBtn = Selector('[data-testid=btn-retry]');\n    removeImportedFileBtn = Selector('[aria-label=\"Clear selected files\"]');\n    exportBtn = Selector('[data-testid=export-btn]');\n    exportSelectedDbsBtn = Selector('[data-testid=export-selected-dbs]');\n    userProfileBtn = Selector('[data-testid=user-profile-btn]');\n    closeImportBtn = Selector('[data-testid=btn-close]');\n    //CHECKBOXES\n    selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]');\n    exportPasswordsCheckbox = Selector('[data-testid=export-passwords]~div', { timeout: 500 });\n    starFreeDbCheckbox = Selector('[data-test-subj=checkboxSelectRow-create-free-cloud-db]');\n    //ICONS\n    moduleColumn = Selector('[data-test-subj=tableHeaderCell_modules_3]');\n    moduleSearchIcon = Selector(\"[data-testid^='Redis Query Engine']\");\n    moduleGraphIcon = Selector('[data-testid^=Graph]');\n    moduleJSONIcon = Selector('[data-testid^=JSON]');\n    moduleTimeseriesIcon = Selector(\"[data-testid^='Time Series']\");\n    moduleBloomIcon = Selector('[data-testid^=Probabilistic]');\n    moduleAIIcon = Selector('[data-testid^=AI]');\n    moduleGearsIcon = Selector('[data-testid^=Gears]');\n    redisStackIcon = Selector('[data-testid=redis-stack-icon]');\n    tooltipRedisStackLogo = Selector('[data-testid=tooltip-redis-stack-icon]');\n    iconNotUsedDatabase = Selector('[data-testid^=database-status-tryDatabase-]');\n    iconDeletedDatabase = Selector('[data-testid^=database-status-checkIfDeleted-]');\n    //TEXT INPUTS (also referred to as 'Text fields')\n    searchInput = Selector('[data-testid=search-database-list]');\n    importDatabaseInput = Selector('[data-testid=import-file-modal-filepicker]');\n    //TEXT ELEMENTS\n    moduleTooltip = Selector('[data-radix-popper-content-wrapper]');\n    moduleQuantifier = Selector('[data-testid=_module]');\n    dbNameList = Selector('[data-testid^=instance-name]', { timeout: 3000 });\n    tableRowContent = Selector('[data-test-subj=database-alias-column]');\n    hostPort = Selector('[data-testid=host-port]');\n    failedImportMessage = Selector('[data-testid=result-failed]');\n    importResult = Selector('[data-testid^=table-result-]');\n    userProfileAccountInfo = Selector('[data-testid^=profile-account-]');\n    portCloudDb = Selector('[class*=column_host]');\n    // DIALOG\n    successResultsAccordion = Selector('[data-testid^=success-results-]');\n    partialResultsAccordion = Selector('[data-testid^=partial-results-]');\n    failedResultsAccordion = Selector('[data-testid^=failed-results-]');\n    notificationUnusedDbMessage = Selector('[class^=_warningTooltipContent]');\n    // CONTAINERS\n    databaseContainer = Selector('.databaseContainer');\n    connectionTypeTitle  = Selector('[data-test-subj=tableHeaderCell_connectionType_2]');\n    addDatabaseImport = Selector('[data-testid=add-db_import]');\n\n    async navigateToDatabase(dbName: string): Promise<void> {\n        await t.click(this.NavigationPanel.myRedisDBButton);\n        await this.clickOnDBByName(dbName);\n    }\n\n    /**\n     * Click on the database by name\n     * @param dbName The name of the database to be opened\n     */\n    async clickOnDBByName(dbName: string): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        const db = this.dbNameList.withExactText(dbName.trim());\n        await t.expect(db.exists).ok(`\"${dbName}\" database doesn't exist`, { timeout: 10000 });\n        await t.click(db);\n    }\n\n    //Delete all the databases from the list\n    async deleteAllDatabases(): Promise<void> {\n        await t.click(this.NavigationPanel.myRedisDBButton);\n        const dbNames = this.tableRowContent;\n        const count = await dbNames.count;\n        if (count > 1) {\n            await t\n                .click(this.selectAllCheckbox)\n                .click(this.deleteButtonInPopover)\n                .click(this.confirmDeleteAllDbButton);\n        }\n        else if (count === 1) {\n            await t\n                .click(this.deleteDatabaseButton)\n                .click(this.confirmDeleteButton);\n        }\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n    }\n\n    /**\n     * Delete DB by name\n     * @param dbName The name of the database to be deleted\n     */\n    async deleteDatabaseByName(dbName: string): Promise<void> {\n        const dbNames = this.tableRowContent;\n        const count = await dbNames.count;\n        for (let i = 0; i < count; i++) {\n            if ((await dbNames.nth(i).innerText || '').includes(dbName)) {\n                await t\n                    .click(this.deleteRowButton.nth(i-1))\n                    .click(this.confirmDeleteButton);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Click on the edit database button by name\n     * @param databaseName The name of the database to be edited\n     */\n    async clickOnEditDBByName(databaseName: string): Promise<void> {\n        const dbNames = this.dbNameList;\n        const count = dbNames.count;\n        for (let i = 0; i < await count; i++) {\n            if ((await dbNames.nth(i).innerText || '').includes(databaseName)) {\n                await t.click(this.editDatabaseButton.nth(i));\n                break;\n            }\n        }\n    }\n\n    /**\n     * Check module inside of tooltip\n     * @param moduleNameList Array with modules list\n     */\n    async checkModulesInTooltip(moduleNameList: string[]): Promise<void> {\n        for (const item of moduleNameList) {\n            await t.expect(this.moduleTooltip.find('span').withText(`${item} v.`).exists).ok(item);\n        }\n    }\n\n    /**\n     * Check module icons on the page\n     * @param moduleList Array with modules list\n     */\n    async checkModulesOnPage(moduleList: Selector[]): Promise<void> {\n        for (const item of moduleList) {\n            await t.expect(item.exists).ok(`${item} icon`);\n        }\n    }\n\n    /**\n     * Get all databases from List of DBs page\n     */\n    async getAllDatabases(): Promise<string[]> {\n\n        const databases: string[] = [];\n        await t.expect(this.dbNameList.exists).ok()\n        const n = await this.dbNameList.count;\n        for(let k = 0; k < n; k++) {\n            const name = await this.dbNameList.nth(k).textContent;\n            databases.push(name);\n        }\n        return databases;\n    }\n\n    /**\n     * Get all databases from List of DBs page\n     * @param actualList Actual databases list\n     * @param sortedList Expected list\n     */\n    async compareDatabases(actualList: string[], sortedList: string[]): Promise<void> {\n        for (let k = 0; k < actualList.length; k++) {\n            await t.expect(actualList[k].trim()).eql(sortedList[k].trim());\n        }\n    }\n\n    /**\n     * Verify database status is visible\n     * @param databaseName The name of the database\n    */\n    async verifyDatabaseStatusIsVisible(databaseName: string): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseName);\n        const databaseNewPoint = Selector(`[data-testid=database-status-new-${databaseId}]`);\n\n        await t.expect(databaseNewPoint.exists).ok(`Database status is not visible for ${databaseName}`);\n    }\n\n    /**\n    * Verify database status is not visible\n    * @param databaseName The name of the database\n    */\n    async verifyDatabaseStatusIsNotVisible(databaseName: string): Promise<void> {\n        const databaseId = await databaseAPIRequests.getDatabaseIdByName(databaseName);\n        const databaseEditBtn = Selector(`[data-testid=database-status-new-${databaseId}]`);\n\n        await t.expect(databaseEditBtn.exists).notOk(`Database status is still visible for ${databaseName}`);\n    }\n\n    /**\n    * Filter array with database objects by result field and return names\n     * @param listOfDb Actual databases list\n     * @param result The expected import result\n    */\n    getDatabaseNamesFromListByResult(listOfDb: DatabasesForImport, result: string): string[] {\n        return listOfDb.filter(element => element.result === result).map(item => item.name!);\n    }\n}\n\n/**\n * Database for import parameters\n * @param host Host of connection\n * @param port Port of connection\n * @param name The name of connection\n * @param result The expected result of connection import\n * @param username The username of connection\n * @param auth Password of connection\n * @param cluster Is the connection has cluster\n * @param indName The name of coonection with index\n * @param db The index of connection\n * @param ssh_port The ssh port of connection\n * @param timeout_connect The connect timeout of connection\n * @param timeout_execute The execute timeout of connection\n * @param other_field The test field\n * @param ssl Is the connection have ssl\n * @param ssl_ca_cert_path The CA certificate of connection by path\n * @param ssl_local_cert_path The Client certificate of connection by path\n * @param ssl_private_key_path The Client key of connection by path\n */\nexport type DatabasesForImport = {\n    host?: string,\n    port?: number | string,\n    name?: string,\n    result?: string,\n    username?: string,\n    auth?: string,\n    cluster?: boolean | string,\n    indName?: string,\n    db?: number,\n    ssh_port?: number,\n    timeout_connect?: number,\n    timeout_execute?: number,\n    other_field?: string,\n    ssl?: boolean,\n    ssl_ca_cert_path?: string,\n    ssl_local_cert_path?: string,\n    ssl_private_key_path?: string\n}[];\n"
  },
  {
    "path": "tests/e2e/pageObjects/pub-sub-page.ts",
    "content": "import { t, Selector } from 'testcafe';\nimport { InstancePage } from './instance-page';\n\nexport class PubSubPage extends InstancePage {\n    //CSS Selectors\n    cssSelectorMessage = '[data-testid=\"messages-list\"] tr';\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //COMPONENTS\n    initialPage = Selector('[data-testid=pub-sub-page] [data-testid=\"empty-messages-list\"]')\n    subscribeStatus = Selector('[data-testid=pub-sub-status]');\n    messages = Selector('[data-testid=\"messages-list\"] tr');\n    messagesTable = Selector('[data-testid=\"messages-list\"] table')\n    messagesTableBottomNav = Selector('[data-testid=\"messages-list\"] nav[data-role=pagination]')\n    messagesTableFirstPageBtn = Selector('[data-testid=\"messages-list\"] nav[data-role=pagination] button[title=\"First page\"]')\n    messagesTableLastPageBtn = Selector('[data-testid=\"messages-list\"] nav[data-role=pagination] button[title=\"Last page\"]')\n    totalMessagesCount = Selector('[data-testid=pub-sub-messages-count]');\n    pubSubPageContainer = Selector('[data-testid=pub-sub-page]');\n    publishResult = Selector('[data-testid=publish-result]');\n    clearButtonTooltip = Selector('[data-radix-popper-content-wrapper]');\n    ossClusterEmptyMessage = Selector('[data-testid=empty-messages-list-cluster]');\n    //BUTTONS\n    subscribeButton = Selector('[data-testid=subscribe-btn]').withText('Subscribe');\n    unsubscribeButton = Selector('[data-testid=subscribe-btn]');\n    publishButton = Selector('[data-testid=publish-message-submit]');\n    clearPubSubButton = Selector('[data-testid=clear-pubsub-btn]');\n    scrollDownButton = Selector('[data-testid=messages-list-anchor-btn]');\n    //INPUTS\n    channelNameInput = Selector('[data-testid=field-channel-name]');\n    messageInput = Selector('[data-testid=field-message]');\n    channelsSubscribeInput = Selector('[data-testid=channels-input]');\n\n    patternsCount = Selector('[data-testid=patterns-count]');\n    messageCount = Selector('[data-testid=pub-sub-messages-count]');\n\n    /**\n     * Publish message in pubsub\n     * @param channel The name of channel\n     * @param message The message\n     */\n    async publishMessage(channel: string, message: string): Promise<void> {\n        await t.click(this.channelNameInput);\n        await t.typeText(this.channelNameInput, channel, { replace: true, paste: true });\n        await t.click(this.messageInput);\n        await t.typeText(this.messageInput, message, { replace: true, paste: true });\n        await t.click(this.publishButton);\n    }\n\n    /**\n     * Subscribe to channel and publish message in pubsub\n     * @param channel The name of channel\n     * @param message The message\n     */\n    async subsribeToChannelAndPublishMessage(channel: string, message: string): Promise<void> {\n        await t.click(this.subscribeButton);\n        // Wait for pubsub loading\n        await t.wait(1000);\n        await this.publishMessage(channel, message);\n        await t.expect((this.messages.withText('message')).exists).ok('Message is not displayed');\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/rdi-instance-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { RdiPopoverOptions, RdiTemplateDatabaseType, RdiTemplatePipelineType } from '../helpers/constants';\nimport { BaseOverviewPage } from './base-overview-page';\nimport { RdiNavigationPanel } from './components/navigation/rdi-navigation-panel';\nimport { TestConnectionPanel } from './components/rdi/test-connection-panel';\nimport { RdiHeader } from './components/rdi/rdi-header';\nimport { PipelineManagementPanel } from './components/rdi/pipeline-management-panel';\nimport { MonacoEditor } from './components/monaco-editor';\n\nexport class RdiInstancePage extends BaseOverviewPage {\n\n    NavigationPanel = new RdiNavigationPanel();\n    TestConnectionPanel = new TestConnectionPanel();\n    PipelineManagementPanel = new PipelineManagementPanel();\n    RdiHeader = new RdiHeader();\n    MonacoEditor = new MonacoEditor();\n\n    dryRunButton = Selector('[data-testid=rdi-job-dry-run]');\n    dryRunSubmitBtn = Selector('[data-testid=dry-run-btn]');\n    closeDryRunPanelBtn = Selector('[data-testid=close-dry-run-btn]');\n    dryRunPanel = Selector('[data-testid=dry-run-panel]');\n    transformationsTab = Selector('[data-testid=transformations-tab]');\n    transformationInput = Selector('[data-testid=wrapper-input-value]');\n    transformationResults = Selector('[data-testid=wrapper-transformations-output]');\n    commandsOutput = Selector('[data-testid=commands-output]');\n    outputTab = Selector('[data-testid=output-tab]');\n    uploadPipelineBtn = Selector('[data-testid=submit-btn]');\n    okUploadPipelineBtn = Selector('[data-testid=ok-btn]');\n    closeImportModelBtn = Selector('[data-testid=import-file-modal] button');\n\n    loadingIndicator = Selector('[class*=rdi__loading]');\n\n    configurationInput = Selector('[data-testid=wrapper-rdi-monaco-config]');\n    configurationLink = Selector('[data-testid=rdi-pipeline-config-link]');\n\n    jobsInput = Selector('[data-testid=wrapper-rdi-monaco-job]');\n    draggableArea = Selector('[data-testid=draggable-area]');\n    dedicatedLanguageSelect = Selector('[data-testid=dedicated-editor-language-select]');\n    languageDropdown = Selector('[class*=_selectLanguage]');\n    jmesPathOption = Selector('[id=jmespath]');\n    sqlEditorButton = Selector('[data-testid=open-dedicated-editor-btn]');\n\n    errorDeployNotification = Selector('[data-test-subj=toast-error-deploy]');\n    failedUploadingPipelineNotification = Selector('[data-testid=result-failed]');\n    closeNotification =  Selector('[class*=euiModal__closeIcon]');\n    noPipelineText = Selector('[data-testid=no-pipeline]');\n\n    // Test Connection\n    textConnectionBtn = Selector('[data-testid=rdi-test-connection-btn]');\n\n    //template\n    templateButton = Selector('[data-testid^=template-trigger-]');\n    templateApplyButton = Selector('[data-testid=template-apply-btn]');\n    templateCancelButton = Selector('[data-testid=template-cancel-btn]');\n    databaseDropdown =  Selector('[data-testid=db-type-select]');\n\n    //dialog\n    selectOptionDialog = Selector('[data-testid=rdi-pipeline-source-dialog]', { timeout: 1000 });\n    closeConfirmNavigateDialog = Selector('[data-testid=oauth-select-account-dialog] button', { timeout: 1000 });\n    proceedNavigateDialog = Selector('[data-testid=confirm-leave-page]', { timeout: 1000 });\n    downloadNavigateDialog = Selector('[data-testid=popup-download-pipeline-btn]', { timeout: 1000 });\n\n    tooltip = Selector('[role=tooltip]', { timeout: 500 });\n    /**\n     * Send a data in Transformation Input\n     * @param text The text to send\n     * @param speed The speed in seconds. Default is 1\n     * @param paste\n     */\n    async sendTransformationInput(command: string, speed = 1, paste = true): Promise<void> {\n        await t\n            .click(this.transformationInput)\n            .typeText(this.transformationInput, command, { replace: true, speed, paste })\n            .click(this.dryRunSubmitBtn);\n    }\n\n    /**\n     * Select value from template dropdowns\n     * @param database value of database dropdown\n     */\n    async setTemplateDropdownValue(database?: RdiTemplateDatabaseType): Promise<void> {\n        if(database != null) {\n            await t.click(this.databaseDropdown);\n            await t.click(Selector(`[id='${database}']`));\n        }\n        await t.click(this.templateApplyButton);\n    }\n\n    /**\n     * Select option from 'Select an option to start with your pipeline' popover\n     * @param option option to select\n     */\n    async selectStartPipelineOption(option: RdiPopoverOptions): Promise<void> {\n        const selector =  Selector(`[data-testid='${option}-source-pipeline-dialog']`);\n\n        await t.click(selector);\n    }\n\n    /**\n     * Verify tooltip contains text\n     * @param expectedText Expected link that is compared with actual\n     */\n    async verifyTooltipContainsText(expectedText: string): Promise<void> {\n        await t.expect(this.tooltip.nth(-1).textContent).contains(expectedText, `\"${expectedText}\" Text is incorrect in tooltip`);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/rdi-instances-list-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { BaseOverviewPage } from './base-overview-page';\nimport { RdiNavigationPanel } from './components/navigation/rdi-navigation-panel';\nimport { AddRdiInstanceDialog, RdiInstance } from './dialogs/add-rdi-instance-dialog';\n\nexport class RdiInstancesListPage extends BaseOverviewPage {\n    NavigationPanel = new RdiNavigationPanel();\n    AddRdiInstanceDialog = new AddRdiInstanceDialog();\n\n    addRdiInstanceButton = Selector('[data-testid=rdi-instance]');\n    addRdiFromEmptyListBtn = Selector('[data-testid=empty-rdi-instance-button]');\n\n    quickstartBtn = Selector('[data-testid=empty-rdi-quickstart-button]');\n\n    rdiInstanceRow = Selector('[class*=euiTableRow-isSelectable]');\n    emptyRdiList = Selector('[data-testid=empty-rdi-instance-list]', { timeout: 1000 });\n    rdiNameList = Selector('[class*=column_name] div', { timeout: 3000 });\n\n    searchInput = Selector('[data-testid=search-rdi-instance-list]');\n\n    sortBy = Selector('[data-test-subj=tableHeaderSortButton] span');\n\n    cssRdiAlias = '[data-test-subj=rdi-alias-column]';\n    cssUrl = '[data-testid=url]';\n    cssRdiVersion = '[data-test-subj=rdi-instance-version-column]';\n    cssLastConnection = '[data-test-subj=rdi-instance-last-connection-column]';\n\n    /**\n     * add Rdi instance\n     * @param instanceValue rdi instance data\n     */\n    async addRdi(instanceValue: RdiInstance): Promise<void> {\n        await t.click(this.addRdiInstanceButton);\n        await t\n            .typeText(this.AddRdiInstanceDialog.rdiAliasInput, instanceValue.alias)\n            .typeText(this.AddRdiInstanceDialog.urlInput, instanceValue.url)\n            .typeText(this.AddRdiInstanceDialog.usernameInput, instanceValue.username as string)\n            .typeText(this.AddRdiInstanceDialog.passwordInput, instanceValue.password as string);\n        await t.click(this.AddRdiInstanceDialog.addInstanceButton);\n    }\n\n    /**\n     * get Rdi instance by index\n     * @param index index of rdi\n     */\n    async getRdiInstanceValuesByIndex(index: number): Promise<RdiInstance> {\n        const alias: string = await this.rdiInstanceRow.nth(index).find(this.cssRdiAlias).innerText;\n        const currentLastConnection: string =  await this.rdiInstanceRow.nth(0).find(this.cssLastConnection).innerText;\n        const currentVersion: string =  await this.rdiInstanceRow.nth(0).find(this.cssRdiVersion).innerText;\n        const currentUrl: string =  await this.rdiInstanceRow.nth(0).find(this.cssUrl).innerText;\n\n        const rdiInstance: RdiInstance = {\n            alias: alias,\n            url: currentUrl,\n            version: currentVersion,\n            lastConnection: currentLastConnection\n        };\n\n        return rdiInstance;\n    }\n\n    /**\n     * Delete Rdi by name\n     * @param dbName The name of the rdi to be deleted\n     */\n    async deleteRdiByName(dbName: string): Promise<void> {\n        const dbNames = this.rdiInstanceRow;\n        const count = await dbNames.count;\n\n        for (let i = 0; i < count; i++) {\n            if ((await dbNames.nth(i).innerText || '').includes(dbName)) {\n                await t\n                    .click(this.deleteRowButton.nth(i))\n                    .click(this.confirmDeleteButton);\n                break;\n            }\n        }\n    }\n\n    /**\n     * Edit Rdi by name\n     * @param dbName The name of the rdi to be deleted\n     */\n    async clickEditRdiByName(dbName: string): Promise<void> {\n        const rdiNames = this.rdiInstanceRow;\n        const count = await rdiNames.count;\n\n        for (let i = 0; i < count; i++) {\n            if ((await rdiNames.nth(i).innerText || '').includes(dbName)) {\n                await t\n                    .click(this.editRowButton.nth(i));\n                break;\n            }\n        }\n    }\n\n    /**\n     * click Rdi by name\n     * @param dbName The name of the rdi\n     */\n    async clickRdiByName(rdiName: string): Promise<void> {\n        if (await this.Toast.toastCloseButton.exists) {\n            await t.click(this.Toast.toastCloseButton);\n        }\n        const rdi = this.rdiNameList.withExactText(rdiName.trim());\n        await t.expect(rdi.exists).ok(`\"${rdi}\" rdi doesn't exist`, { timeout: 10000 });\n        await t.click(rdi);\n    }\n\n    /**\n     * Sort rdi list by column\n     * @param columnName The name of column\n     */\n    async sortByColumn(columnName: string): Promise<void> {\n        await t.click(this.sortBy.withText(columnName));\n    }\n    /**\n     * Get all Rdi alias\n     */\n    async getAllRdiNames(): Promise<string[]> {\n        const rdis: string[] = [];\n        const n = await this.rdiInstanceRow.count;\n\n        for(let k = 0; k < n; k++) {\n            const name = await this.rdiInstanceRow.nth(k).find(this.cssRdiAlias).innerText;\n            rdis.push(name);\n        }\n        return rdis;\n    }\n}\n\n"
  },
  {
    "path": "tests/e2e/pageObjects/rdi-status-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { BaseOverviewPage } from './base-overview-page';\nimport { RdiNavigationPanel } from './components/navigation/rdi-navigation-panel';\n\nexport class RdiStatusPage extends BaseOverviewPage {\n\n    NavigationPanel = new RdiNavigationPanel();\n\n    targetConnectionTable = Selector('[data-testid=target-connections-table]');\n    processingPerformanceInformationContainer = Selector('[aria-controls=processing-performance-info]');\n    processingPerformanceInformationTable = Selector('[id=processing-performance-info]');\n    dataStreamsOverviewTable = Selector('[data-testid=data-streams-table]');\n    clientsTable = Selector('[data-testid=clients-table]');\n\n    refreshStreamsButton = Selector('[data-testid=data-streams-refresh-btn]');\n    processingPerformanceRefreshMessage = Selector('[data-testid=processing-performance-info-refresh-message]');\n    clientRefreshMessage = Selector('[data-testid=clients-refresh-message]');\n    dataStreamsRefreshMessage = Selector('[data-testid=data-streams-refresh-message]');\n\n    tooltip = Selector('[role=tooltip]', { timeout: 500 });\n\n    /**\n     * Get row data\n     * @param tableSelector selector of the table\n     * @param index number of the row\n     */\n    async getTableRowData(tableSelector: Selector, index: number): Promise<string[]> {\n        const rowSelector = tableSelector.find('tr').nth(index);\n        const text = await rowSelector.innerText;\n        return text.split(/\\s+/);\n    }\n\n    /**\n     * Get row data\n     * @param tableSelector selector of the table\n     * @param index number of the row\n     * @param columnIndex number of the column\n     */\n    async hoverValueInTable(tableSelector: Selector, rowIndex: number, columnIndex: number): Promise<void> {\n        const itemSelector = tableSelector.find('tr').nth(rowIndex).find('td').nth(columnIndex);\n        await t.hover(itemSelector);\n    }\n\n    /**\n     * Get row data\n     * @param tableSelector selector of the table\n     * @param index number of the row\n     * @param columnIndex number of the column\n     */\n    async getValueInTable(tableSelector: Selector, rowIndex: number, columnIndex: number): Promise<string> {\n        const itemSelector = tableSelector.find('tr').nth(rowIndex).find('td').nth(columnIndex).find('span');\n        const text =  await itemSelector.innerText;\n        return text.replace(/\\.{3}/g, '');\n    }\n}\n\n"
  },
  {
    "path": "tests/e2e/pageObjects/sentinel/adding-master-groups-result-page.ts",
    "content": "import { t, Selector } from 'testcafe';\n\nexport class AddingdMasterGroupsResultPage {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //COLUMNS\n    addMasterGroupsResultColumn = Selector('[data-test-subj=tableHeaderCell_message_0]');\n    viewDatabasesButton = Selector('span').withText('View Databases');\n\n    async checkResultStatus(): Promise<void> {\n        await t.click(this.viewDatabasesButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/sentinel/discovered-sentinel-master-groups-page.ts",
    "content": "import { t, Selector } from 'testcafe';\n\nexport class DiscoverMasterGroupsPage {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    // todo: add data-testid to the checkbox and to the table on UI\n    selectAllCheckbox = Selector('[thead th:nth-child(1) button]');\n    addPrimaryGroupButton = Selector('[data-testid=btn-add-primary-group]');\n    masterGroupsTitle = Selector('//h1[text()=\"Auto-Discover Redis Sentinel Primary Groups\"]');\n\n    /**\n * Add all Master Groups from Sentinel\n */\n    async addMasterGroups(): Promise<void> {\n        await t\n            .click(this.selectAllCheckbox)\n            .expect(this.selectAllCheckbox.checked).ok();\n        await t\n            .click(this.addPrimaryGroupButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/settings-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { BasePage } from './base-page';\n\nexport class SettingsPage extends BasePage {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTONS\n    accordionAppearance = Selector('[data-test-subj=accordion-appearance]');\n    accordionPrivacySettings = Selector('[data-test-subj=accordion-privacy-settings]');\n    accordionAdvancedSettings = Selector('[data-test-subj=accordion-advanced-settings]');\n    accordionWorkbenchSettings = Selector('[data-testid=accordion-workbench-settings]');\n    switchAnalyticsOption = Selector('[data-testid=switch-option-analytics]');\n    switchEulaOption = Selector('[data-testid=switch-option-eula]');\n    submitConsentsPopupButton = Selector('[data-testid=consents-settings-popup] [data-testid=btn-submit]');\n    switchNotificationsOption = Selector('[data-testid=switch-option-notifications]');\n    switchEditorCleanupOption = Selector('[data-testid=switch-workbench-cleanup]');\n    //TEXT INPUTS (also referred to as 'Text fields')\n    keysToScanValue = Selector('[data-testid=keys-to-scan-value]');\n    keysToScanInput = Selector('[data-testid=keys-to-scan-input]');\n    commandsInPipelineValue = Selector('[data-testid=pipeline-bunch-value]');\n    commandsInPipelineInput = Selector('[data-testid=pipeline-bunch-input]');\n    pipelineLink = Selector('[data-testid=pipelining-link]');\n\n    //Date and Time Format\n    selectFormatDropdown = Selector('[data-test-subj=select-datetime]');\n    selectTimezoneDropdown = Selector('[data-test-subj=select-timezone]');\n    dataPreview = Selector('[data-testid=data-preview]');\n    customRadioButton = Selector('[data-testid=format-timestamp-form-radio-group] button[value=custom]');\n    commonRadioButton = Selector('[data-testid=format-timestamp-form-radio-group] button[value=common]');\n    customTextField =  Selector('[data-testid=custom-datetime-input]');\n    saveCustomFormatButton = Selector('[data-testid=datetime-custom-btn]');\n\n    getDateTimeOption = (option: string): Selector =>\n        Selector(`[data-test-subj^=date-option-${option}]`);\n    getZoneOption = (option: string): Selector =>\n        Selector(`[data-test-subj=zone-option-${option}]`);\n\n    /**\n     * Change Keys to Scan value\n     * @param value Value for scan\n     */\n    async changeKeysToScanValue(value: string): Promise<void> {\n        await t\n            .hover(this.keysToScanValue)\n            .click(this.keysToScanInput)\n            .typeText(this.keysToScanInput, value, { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn);\n    }\n\n    /**\n    * Change Commands In Pipeline value\n    * @param value Value for pipeline\n    */\n    async changeCommandsInPipeline(value: string): Promise<void> {\n        await t.hover(this.commandsInPipelineValue)\n            .click(this.commandsInPipelineInput)\n            .typeText(this.commandsInPipelineInput, value, { replace: true, paste: true })\n            .click(this.EditorButton.applyBtn);\n    }\n\n    /**\n     * Get state of Analytics switcher\n     */\n    async getAnalyticsSwitcherValue(): Promise<boolean> {\n        return await this.switchAnalyticsOption.getAttribute('aria-checked') === 'true';\n    }\n\n    /**\n     * Get state of Notifications switcher\n     */\n    async getNotificationsSwitcherValue(): Promise<boolean> {\n        return await this.switchNotificationsOption.getAttribute('aria-checked') === 'true';\n    }\n\n    /**\n     * Get state of Eula switcher\n     */\n    async getEulaSwitcherValue(): Promise<boolean> {\n        return await this.switchEulaOption.getAttribute('aria-checked') === 'true';\n    }\n\n    /**\n     * Get state of Editor Cleanup switcher\n     */\n    async getEditorCleanupSwitcherValue(): Promise<boolean> {\n        return await this.switchEditorCleanupOption.getAttribute('aria-checked')  === 'true';\n    }\n\n    /**\n    * Enable Editor Cleanup switcher\n    * @param state Enabled(true) or disabled(false)\n    */\n    async changeEditorCleanupSwitcher(state: boolean): Promise<void> {\n        const currentState = await this.getEditorCleanupSwitcherValue();\n        if (currentState !== state) {\n            await t.click(this.switchEditorCleanupOption);\n        }\n    }\n\n    /**\n     * Turn on/off notifications in Settings\n     */\n    async changeNotificationsSwitcher(toValue: boolean): Promise<void> {\n        await t.click(this.NavigationPanel.settingsButton);\n        await t.click(this.accordionAppearance);\n        if (toValue !== await this.getNotificationsSwitcherValue()) {\n            await t.click(this.switchNotificationsOption);\n        }\n    }\n\n    /**\n     * Turn on/off Analytics in Settings\n     */\n    async changeAnalyticsSwitcher(toValue: boolean): Promise<void> {\n        await t.click(this.accordionPrivacySettings);\n        if (toValue !== await this.getAnalyticsSwitcherValue()) {\n            await t.click(this.switchAnalyticsOption);\n        }\n    }\n\n    /**\n     * Select data time option in Settings\n     */\n    async selectDataFormatDropdown(value: string): Promise<void>{\n        await t.click(this.selectFormatDropdown);\n        await t.click(this.getDateTimeOption(value));\n    }\n\n    /**\n     * Select timezone option in Settings\n     */\n    async selectTimeZoneDropdown(value: string): Promise<void>{\n        await t.click(this.selectTimezoneDropdown);\n        await t.click(this.getZoneOption(value));\n    }\n    /**\n     * Enter text in custom field Select timezone option in Settings\n     */\n    async enterTextInCustom(command: string): Promise<void>{\n        await t.typeText(this.customTextField, command, { replace: true });\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/slow-log-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { InstancePage } from './instance-page';\n\nexport class SlowLogPage extends InstancePage {\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //CSS Selectors\n    cssSelectorDurationValue = '[data-testid=duration-value]';\n    //BUTTONS\n    slowLogSortByTimestamp = Selector('[data-testid=header-sorting-button]');\n    slowLogNumberOfCommandsDropdown = Selector('[data-testid=count-select]');\n    slowLogConfigureButton = Selector('[data-testid=configure-btn]');\n    slowLogConfigureUnitButton = Selector('[data-test-subj=select-default-unit]');\n    slowLogConfigureMilliSecondsUnit = Selector('[data-test-subj=unit-milli-second]');\n    slowLogConfigureMicroSecondsUnit = Selector('[data-test-subj=unit-micro-second]');\n    slowLogSaveConfigureButton = Selector('[data-testid=slowlog-config-save-btn]');\n    slowLogCancelConfigureButton = Selector('[data-testid=slowlog-config-cancel-btn]');\n    slowLogDefaultConfigureButton = Selector('[data-testid=slowlog-config-default-btn]');\n    slowLogRefreshButton = Selector('[data-testid=slowlog-refresh-btn]');\n    slowLogClearButton = Selector('[data-testid=clear-btn]');\n    slowLogConfirmClearButton = Selector('[data-testid=reset-confirm-btn]');\n    slowLogTab = Selector('[data-testid=analytics-tabs] [role=tab] p').withText('Slow Log').parent('[role=tab]');\n    //INPUTS\n    slowLogSlowerThanConfig = Selector('[data-testid=slower-than-input]');\n    slowLogMaxLengthConfig = Selector('[data-testid=max-len-input]');\n    //TEXT ELEMENTS\n    slowLogTimestampValue = Selector('[data-testid=timestamp-value]');\n    slowLogDurationValue = Selector(this.cssSelectorDurationValue);\n    slowLogCommandValue = Selector('[data-testid=command-value]');\n    slowLogEmptyResult = Selector('[data-testid=empty-slow-log]');\n    slowLogCommandStatistics = Selector('[data-testid=entries-from-timestamp]');\n    configInfo = Selector('[data-testid=config-info]');\n    // Table\n    slowLogTable = Selector('[data-testid=slowlog-table]');\n\n    /**\n     * Set value for slowlog-log-slower-than parameter\n     * @param slowerThan Value for slowlog-log-slower-than property\n     * @param unit Value for unit property\n     */\n    async changeSlowerThanParameter(slowerThan: number, unit?: Selector): Promise<void> {\n        await t\n            .click(this.slowLogConfigureButton)\n            .typeText(this.slowLogSlowerThanConfig, slowerThan.toString(), { replace: true, paste: true });\n        if (unit !== undefined) {\n            await t\n                .click(this.slowLogConfigureUnitButton)\n                .click(unit);\n        }\n        await t.click(this.slowLogSaveConfigureButton);\n    }\n\n    /**\n     * Set value for slowlog-max-len parameter\n     * @param maxLength Value for slowlog-max-len property\n     */\n    async changeMaxLengthParameter(maxLength: number): Promise<void> {\n        await t\n            .click(this.slowLogConfigureButton)\n            .typeText(this.slowLogMaxLengthConfig, maxLength.toString(), { replace: true, paste: true })\n            .click(this.slowLogSaveConfigureButton);\n    }\n\n    /**\n     * Change Display Up To parameter in Slow Log\n     * @param option option for commands to display\n     */\n    async changeDisplayUpToParameter(option: string): Promise<void> {\n        await t.click(this.slowLogNumberOfCommandsDropdown);\n        await t.click(Selector('[role=listbox] [role=option] span').withText(`${option}`));\n    }\n\n    /**\n     * Reset max-length and slowlog-log-slower-than to default\n     */\n    async resetToDefaultConfig(): Promise<void> {\n        await t\n            .click(this.slowLogConfigureButton)\n            .click(this.slowLogDefaultConfigureButton)\n            .click(this.slowLogSaveConfigureButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/sso-authorization-page.ts",
    "content": "import { PageWithCursor } from \"puppeteer-real-browser\";\nimport { SsoAuthorization } from \"../helpers\";\n\nexport class SsoAuthorizationPage {\n    // BUTTONS\n    submitFormButton = 'input[type=\"submit\"]';\n    tryAnotherWayButton = `//*[text()='Try another way']`\n    googleNextButton = '#identifierNext';\n    googleSubmitPasswordButton = '#passwordNext';\n    // INPUTS\n    oktaUserNameInput = 'input[autocomplete=\"username\"]';\n    oktaPasswordInput = 'input[autocomplete=\"current-password\"]';\n    googleEmailInput = 'input[type=\"email\"]';\n    goooglePasswordInput = 'input[type=\"password\"]';\n    githubUserNameInput = '#login_field';\n    githubPasswordInput = '#password';\n\n    /**\n     * Sign in using SSO\n     * @param authorizationType The authorization page type 'Google' || 'Github' || 'SAML'\n     * @param page Puppeteer page instance\n     * @param urlToUse The url to process authorization\n     * @param username The username to okta account\n     * @param password The password to okta account\n     */\n    async signInUsingSso(authorizationType: 'Google' | 'Github' | 'SAML', page: PageWithCursor, urlToUse: string, username: string, password: string): Promise<void> {\n        await page.goto(urlToUse);\n        await SsoAuthorization.waitForTimeout(2000);\n\n\n        switch (authorizationType) {\n            case 'SAML':\n                await this.submitOktaForm(page, username, password);\n                break;\n            case 'Google':\n                await this.submitGoogleForm(page, username, password);\n                break;\n            case 'Github':\n                await this.submitGithubForm(page, username, password);\n                break;\n            default:\n                throw new Error(`Unsupported authorization type: ${authorizationType}`);\n        }\n\n        await SsoAuthorization.waitForTimeout(2000);\n        // Wait for the authorization to complete\n        await page.waitForFunction(() => window.location.href.includes('#success'), { timeout: 11000 });\n    }\n\n    /**\n     * Submit login OKTA form\n     * @param urlToUse The url to process authorization\n     * @param username The username to okta account\n     * @param password The password to okta account\n     */\n    async submitOktaForm(page: PageWithCursor, username: string, password: string): Promise<void> {\n        await page.waitForSelector(this.oktaUserNameInput, { visible: true });\n        await page.type(this.oktaUserNameInput, username, { delay: Math.random() * 100 + 50 });\n        await page.type(this.oktaPasswordInput, password, { delay: Math.random() * 100 + 50 });\n        await page.click(this.submitFormButton);\n    }\n\n    /**\n     * Submit login Google form\n     * @param page Puppeteer page instance\n     * @param username The username to okta account\n     * @param password The password to okta account\n     */\n    async submitGoogleForm(page: PageWithCursor, username: string, password: string): Promise<void> {\n        await page.waitForSelector(this.googleEmailInput, { visible: true });\n        await page.type(this.googleEmailInput, username, { delay: Math.random() * 100 + 50 });\n        await Promise.all([\n            page.click(this.googleNextButton),\n            page.waitForNavigation({ waitUntil: 'networkidle0' })\n        ]);\n        await page.waitForSelector(this.goooglePasswordInput, { visible: true });\n        await SsoAuthorization.waitForTimeout(500);\n\n        await page.type(this.goooglePasswordInput, password, { delay: Math.random() * 100 + 50 });\n        await Promise.all([\n            page.click(this.googleSubmitPasswordButton),\n            page.waitForNavigation({ waitUntil: 'networkidle0' })\n        ]);\n        await SsoAuthorization.waitForTimeout(500);\n\n        // Check for \"Try another way\" button\n        const tryAnotherWayButtons = await page.$$(this.tryAnotherWayButton);\n        if (tryAnotherWayButtons.length > 0) {\n            const buttonVisible = await tryAnotherWayButtons[0].isIntersectingViewport();\n            if (buttonVisible) {\n                await tryAnotherWayButtons[0].click();\n            } else {\n                console.log(\"'Try another way' button not found or not visible.\");\n            }\n        }\n    }\n\n    /**\n     * Submit login GitHub form\n     * @param urlToUse The url to process authorization\n     * @param username The username to okta account\n     * @param password The password to okta account\n     */\n    async submitGithubForm(page: PageWithCursor, username: string, password: string): Promise<void> {\n        await page.waitForSelector(this.githubUserNameInput, { visible: true });\n        await page.type(this.githubUserNameInput, username, { delay: Math.random() * 100 + 50 });\n        await page.type(this.githubPasswordInput, password, { delay: Math.random() * 100 + 50 });\n        await page.click(this.submitFormButton);\n    }\n}\n"
  },
  {
    "path": "tests/e2e/pageObjects/workbench-page.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { InstancePage } from './instance-page';\nimport { Common } from '../helpers/common';\n\nexport class WorkbenchPage extends InstancePage {\n    //CSS selectors\n    cssSelectorPaginationButtonPrevious = '[data-test-subj=pagination-button-previous]';\n    cssSelectorPaginationButtonNext = '[data-test-subj=pagination-button-next]';\n    cssMonacoCommandPaletteLine = '[aria-label=\"Command Palette\"]';\n    cssWorkbenchCommandInHistory = '[data-testid=wb-command]';\n    queryGraphContainer = '[data-testid=query-graph-container]';\n    cssQueryCardCommand = '[data-testid=query-card-command]';\n    cssRowInVirtualizedTable = '[data-testid^=row-]';\n    cssClientListViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__clients-list]';\n    cssQueryCardContainer = '[data-testid^=\"query-card-container-\"]';\n    cssQueryTextResult = '[data-testid=query-cli-result]';\n    cssReRunCommandButton = '[data-testid=re-run-command]';\n    cssDeleteCommandButton = '[data-testid=delete-command]';\n    cssJsonViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__json-view]';\n    cssQueryTableResult = '[data-testid^=query-table-result-]';\n    cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]';\n    cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]';\n    //-------------------------------------------------------------------------------------------\n    //DECLARATION OF SELECTORS\n    //*Declare all elements/components of the relevant page.\n    //*Target any element/component via data-id, if possible!\n    //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.).\n    //-------------------------------------------------------------------------------------------\n    //BUTTON\n    submitCommandButton = Selector('[data-testid=btn-submit]');\n    queryInput = Selector('[data-testid=query-input-container]');\n    queryInputForText = Selector('[data-testid=query-input-container] .view-lines');\n    resizeButtonForScriptingAndResults = Selector('[data-test-subj=resize-btn-scripting-area-and-results]');\n    paginationButtonPrevious = Selector(this.cssSelectorPaginationButtonPrevious);\n    paginationButtonNext = Selector(this.cssSelectorPaginationButtonNext);\n    preselectButtons = Selector('[data-testid^=preselect-]');\n    preselectManual = Selector('[data-testid=preselect-Manual]');\n    queryCardNoModuleButton = Selector('[data-testid=query-card-no-module-button] a');\n    groupMode = Selector('[data-testid=btn-change-group-mode]');\n    runButtonToolTip = Selector('[data-testid=run-query-tooltip]');\n    loadedCommand = Selector('[class=euiLoadingContent__singleLine]');\n    runButtonSpinner = Selector('[data-testid=loading-spinner]');\n    commandExecutionDateAndTime = Selector('[data-testid=command-execution-date-time]');\n    executionCommandTime = Selector('[data-testid=command-execution-time-value]');\n    executionCommandIcon = Selector('[data-testid=command-execution-time-icon]');\n    executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 1500 });\n    queryResult = Selector('[data-testid=query-common-result]');\n    queryInputScriptArea = Selector('[data-testid=query-input-container] .view-line');\n    parametersAnchor = Selector('[data-testid=parameters-anchor]');\n    clearResultsBtn = Selector('[data-testid=clear-history-btn]');\n\n    // OVERLAY/LOADING ELEMENTS\n    // Selector for the problematic overlay that obstructs workbench interactions in CI\n    overlayContainer = Selector('.RI-flex-group.RI-flex-row').filter((node) => {\n        const style = node.getAttribute('style');\n        return !!(style && style.includes('height: 100%'));\n    });\n\n    //ICONS\n    noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]');\n    groupModeIcon = Selector('[data-testid=group-mode-tooltip]');\n    silentModeIcon = Selector('[data-testid=silent-mode-tooltip]');\n    rawModeIcon = Selector('[data-testid=raw-mode-tooltip]');\n\n    //TEXT ELEMENTS\n    responseInfo = Selector('[class=\"responseInfo\"]');\n    parsedRedisReply = Selector('[class=\"parsedRedisReply\"]');\n    mainEditorArea = Selector('[data-testid=main-input-container-area]');\n    queryColumns = Selector('[data-testid*=query-column-]');\n    noCommandHistorySection = Selector('[data-testid=wb_no-results]');\n    noCommandHistoryTitle = Selector('[data-testid=wb_no-results__title]');\n    noCommandHistoryText = Selector('[data-testid=wb_no-results__summary]');\n    scrolledEnablementArea = Selector('[data-testid=enablement-area__page]');\n    commandExecutionResult = Selector('[data-testid=welcome-page-title]');\n    commandExecutionResultFailed = Selector('[data-testid=cli-output-response-fail]');\n    chartViewTypeOptionSelected = Selector('[data-testid=view-type-selected-Plugin-redistimeseries__redistimeseries-chart]');\n    scriptsLines = Selector('[data-testid=query-input-container] .view-lines');\n    queryJsonResult = Selector('[data-testid=json-view]');\n    jsonStringViewTypeOption = Selector('[data-test-subj=view-type-option-Plugin-client-list__json-string-view]');\n\n    graphViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin-graph]');\n    typeSelectedClientsList = Selector('[data-testid=view-type-selected-Plugin-client-list__clients-list]');\n    viewTypeOptionClientList = Selector('[data-test-subj=view-type-option-Plugin-client-list__clients-list]');\n    viewTypeOptionsText = Selector('[data-test-subj=view-type-option-Text-default__Text]');\n\n    // History containers\n    queryCardCommand = Selector('[data-testid=query-card-command]');\n    fullScreenButton = Selector('[data-testid=toggle-full-screen]');\n    rawModeBtn = Selector('[data-testid=\"btn-change-mode\"]');\n    queryCardContainer = Selector('[data-testid^=query-card-container]');\n    reRunCommandButton = Selector('[data-testid=re-run-command]');\n    copyBtn = Selector('[data-testid^=copy-btn-]');\n    copyCommand = Selector('[data-testid=copy-command]');\n\n    //OPTIONS\n    selectViewType = Selector('[data-testid=select-view-type]');\n    queryTableResult = Selector('[data-testid^=query-table-result-]');\n    textViewTypeOption = Selector('[data-test-subj^=view-type-option-Text]');\n    tableViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin]');\n\n    iframe = Selector('[data-testid=pluginIframe]');\n\n    queryTextResult = Selector(this.cssQueryTextResult);\n\n    getTutorialLinkLocator = (tutorialName: string): Selector =>\n        Selector(`[data-testid=query-tutorials-link_${tutorialName}]`, { timeout: 2000 } );\n\n\n    // Select view option in Workbench results\n    async selectViewTypeGraph(): Promise<void> {\n        await t\n            .click(this.selectViewType)\n            .click(this.graphViewTypeOption);\n    }\n\n    /**\n     * Send multiple commands in Workbench\n     * @param commands The commands\n     */\n    async sendMultipleCommandsInWorkbench(commands: string[]): Promise<void> {\n        for (const command of commands) {\n            await t\n                .typeText(this.queryInput, command, { replace: false, speed: 1, paste: true })\n                .pressKey('esc')\n                .pressKey('enter');\n        }\n        await t.click(this.submitCommandButton);\n    }\n\n    /**\n     * Send commands array in Workbench page\n     * @param commands The array of commands to send\n     */\n    async sendCommandsArrayInWorkbench(commands: string[]): Promise<void> {\n        for (const command of commands) {\n            await this.sendCommandInWorkbench(command);\n        }\n    }\n\n    // Select Json view option in Workbench results\n    async selectViewTypeJson(): Promise<void> {\n        await t\n            .click(this.selectViewType)\n            .click(this.jsonStringViewTypeOption);\n    }\n    /**\n     * Get card container by command\n     * @param command The command\n     */\n    async getCardContainerByCommand(command: string): Promise<Selector> {\n        return this.queryCardCommand.withExactText(command).parent(this.cssQueryCardContainer);\n    }\n\n    /**\n     * Send a command in Workbench with retry mechanism for CI overlay issues\n     * @param command The command\n     * @param speed The speed in seconds. Default is 1\n     * @param paste Whether to paste the command. Default is true\n     */\n    async sendCommandInWorkbench(command: string, speed = 1, paste = true): Promise<void> {\n        const maxRetries = 5;\n        let lastError: Error | null = null;\n\n        for (let i = 0; i < maxRetries; i++) {\n            try {\n                // Wait for any loading states to complete before attempting interaction\n                await Common.waitForElementNotVisible(this.runButtonSpinner);\n                await Common.waitForElementNotVisible(this.loadedCommand);\n\n                // Wait for the problematic overlay to disappear (CI-specific issue)\n                await Common.waitForElementNotVisible(this.overlayContainer);\n\n                // Enhanced wait for database readiness and stability\n                await t.wait(2000); // Increased from 500ms to 2000ms\n\n                // Verify UI elements are ready before interaction\n                await t.expect(this.queryInput.exists).ok('Query input not found', { timeout: 10000 });\n                await t.expect(this.submitCommandButton.exists).ok('Submit button not found', { timeout: 10000 });\n\n                // Perform the actual workbench interaction\n                await t\n                    .click(this.queryInput)\n                    .wait(200) // Small pause after click\n                    .typeText(this.queryInput, command, { replace: true, speed, paste })\n                    .wait(200) // Small pause after typing\n                    .click(this.submitCommandButton);\n\n                // Wait for command to be processed\n                await t.wait(1000);\n\n                return; // Success, exit the retry loop\n            } catch (error) {\n                lastError = error as Error;\n                console.warn(`Workbench command attempt ${i + 1}/${maxRetries} failed for command \"${command}\":`, error);\n                console.warn('Error details:', lastError.message, lastError.stack);\n\n                if (i === maxRetries - 1) {\n                    // Final attempt failed, throw the error\n                    throw new Error(`Failed to send command \"${command}\" after ${maxRetries} attempts. Last error: ${lastError.message}`);\n                }\n\n                // Wait before retrying to allow any animations/transitions to complete\n                await t.wait(2000);\n            }\n        }\n    }\n\n    /**\n     * Check the last command and result in workbench\n     * @param command The command to check\n     * @param result The result to check\n     * @param childNum Indicator which command result need to check\n     */\n    async checkWorkbenchCommandResult(command: string, result: string, childNum = 0): Promise<void> {\n        // Compare the command with executed command\n        const actualCommand = await this.queryCardContainer.nth(childNum).find(this.cssQueryCardCommand).textContent;\n        await t.expect(actualCommand).contains(command, 'Actual command is not equal to executed');\n        // Compare the command result with executed command\n        const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent;\n        await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed');\n    }\n\n    // Select Text view option in Workbench results\n    async selectViewTypeText(): Promise<void> {\n        await t\n            .click(this.selectViewType)\n            .click(this.textViewTypeOption);\n    }\n\n    // Select Table view option in Workbench results\n    async selectViewTypeTable(): Promise<void> {\n        await t\n            .click(this.selectViewType)\n            .doubleClick(this.tableViewTypeOption);\n    }\n\n    /**\n     * Select query using autosuggest\n     * @param query Value of query\n     */\n    async selectFieldUsingAutosuggest(value: string): Promise<void> {\n        await t.wait(200);\n        await t.typeText(this.queryInput, '@', { replace: false });\n        await t.expect(this.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed');\n        await t.typeText(this.queryInput, value, { replace: false });\n        // Select query option into autosuggest and go out of quotes\n        await t.pressKey('tab');\n        await t.pressKey('tab');\n        await t.pressKey('right');\n        await t.pressKey('space');\n    }\n}\n"
  },
  {
    "path": "tests/e2e/rte/RedisInsight_Connections.json",
    "content": "[\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"85f3d430-6da3-4599-9df4-72f68da5a82e\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8100,\n    \"name\": \"OSS Standalone (latest with modules)\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:26:01.041Z\",\n    \"modules\": [\n      {\n        \"name\": \"timeseries\",\n        \"version\": 10616,\n        \"semanticVersion\": \"1.6.16\"\n      },\n      {\n        \"name\": \"graph\",\n        \"version\": 20815,\n        \"semanticVersion\": \"2.8.15\"\n      },\n      {\n        \"name\": \"search\",\n        \"version\": 999999,\n        \"semanticVersion\": \"99.99.99\"\n      },\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 20011,\n        \"semanticVersion\": \"2.0.11\"\n      },\n      {\n        \"name\": \"bf\",\n        \"version\": 20209,\n        \"semanticVersion\": \"2.2.9\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"792b278c-739f-44a2-ac81-8ecf8b831c32\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8108,\n    \"name\": \"OSS Standalone v7\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:25:49.580Z\",\n    \"modules\": [],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"e9962be7-69ce-43bd-b078-483a2a99c99f\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8105,\n    \"name\": \"OSS Standalone Empty\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:25:57.571Z\",\n    \"modules\": [\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 20011,\n        \"semanticVersion\": \"2.0.11\"\n      },\n      {\n        \"name\": \"graph\",\n        \"version\": 20815,\n        \"semanticVersion\": \"2.8.15\"\n      },\n      {\n        \"name\": \"search\",\n        \"version\": 999999,\n        \"semanticVersion\": \"99.99.99\"\n      },\n      {\n        \"name\": \"timeseries\",\n        \"version\": 10616,\n        \"semanticVersion\": \"1.6.16\"\n      },\n      {\n        \"name\": \"bf\",\n        \"version\": 20209,\n        \"semanticVersion\": \"2.2.9\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"16cd9391-a573-4257-ae1c-043d290597e0\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8103,\n    \"name\": \"OSS Standalone Big\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:25:59.442Z\",\n    \"modules\": [\n      {\n        \"name\": \"search\",\n        \"version\": 999999,\n        \"semanticVersion\": \"99.99.99\"\n      },\n      {\n        \"name\": \"rg\",\n        \"version\": 10204,\n        \"semanticVersion\": \"1.2.4\"\n      },\n      {\n        \"name\": \"bf\",\n        \"version\": 20209,\n        \"semanticVersion\": \"2.2.9\"\n      },\n      {\n        \"name\": \"ai\",\n        \"version\": 10205,\n        \"semanticVersion\": \"1.2.5\"\n      },\n      {\n        \"name\": \"graph\",\n        \"version\": 20815,\n        \"semanticVersion\": \"2.8.15\"\n      },\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 20011,\n        \"semanticVersion\": \"2.0.11\"\n      },\n      {\n        \"name\": \"timeseries\",\n        \"version\": 10616,\n        \"semanticVersion\": \"1.6.16\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"f9a04117-7ad8-482e-af6c-4a8539b8ec7e\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8106,\n    \"name\": \"OSS Standalone RedisGears 2.0\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:25:55.153Z\",\n    \"modules\": [\n      {\n        \"name\": \"redisgears_2\",\n        \"version\": 999999\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"51354de3-ee6f-4f12-92d4-81039f0dfa64\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8200,\n    \"name\": \"OSS Cluster v7\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"CLUSTER\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:45:45.846Z\",\n    \"modules\": [\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 10008,\n        \"semanticVersion\": \"1.0.8\"\n      },\n      {\n        \"name\": \"search\",\n        \"version\": 20013,\n        \"semanticVersion\": \"2.0.13\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"163ff5d1-cc81-45dc-b6d2-577452027216\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8101,\n    \"name\": \"OSS Standalone v5\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:25:51.133Z\",\n    \"modules\": [],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"862a26c2-43d7-4708-a3f4-ae63c1aa1c65\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8109,\n    \"name\": \"OSS Standalone v8\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:25:47.850Z\",\n    \"modules\": [\n      {\n        \"name\": \"search\",\n        \"version\": 79901,\n        \"semanticVersion\": \"7.99.1\"\n      },\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 79901,\n        \"semanticVersion\": \"7.99.1\"\n      },\n      {\n        \"name\": \"timeseries\",\n        \"version\": 79901,\n        \"semanticVersion\": \"7.99.1\"\n      },\n      {\n        \"name\": \"bf\",\n        \"version\": 79901,\n        \"semanticVersion\": \"7.99.1\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"d9873d56-5b75-4c6c-afb5-8322d93eef3d\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 8221,\n    \"name\": \"OSS Cluster v7 with RediSearch\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"CLUSTER\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:46:16.309Z\",\n    \"modules\": [\n      {\n        \"name\": \"search\",\n        \"version\": 999999,\n        \"semanticVersion\": \"99.99.99\"\n      },\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 10008,\n        \"semanticVersion\": \"1.0.8\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"459d7404-b0c1-4217-a7e5-3e1942e34612\",\n    \"host\": \"172.31.100.191\",\n    \"port\": 6379,\n    \"name\": \"OSS Cluster RedisGears 2.0\",\n    \"db\": 0,\n    \"username\": \"default\",\n    \"password\": null,\n    \"connectionType\": \"CLUSTER\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:49:30.187Z\",\n    \"modules\": [\n      {\n        \"name\": \"redisgears_2\",\n        \"version\": 999999\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": false,\n    \"sshOptions\": null,\n    \"forceStandalone\": false,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"8781d79a-784c-40e3-bfd0-7cb5140c01da\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 28100,\n    \"name\": \"Sentinel - primary-group-2\",\n    \"db\": 0,\n    \"username\": null,\n    \"password\": \"password\",\n    \"connectionType\": \"SENTINEL\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:51:18.340Z\",\n    \"sentinelMaster\": {\n      \"name\": \"primary-group-2\",\n      \"username\": null,\n      \"password\": null\n    },\n    \"modules\": [],\n    \"tls\": null,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": null,\n    \"sshOptions\": null,\n    \"forceStandalone\": null,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"c8fcde68-a462-4005-96ef-a6b824903108\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 28100,\n    \"name\": \"Sentinel - primary-group-1\",\n    \"db\": 0,\n    \"username\": null,\n    \"password\": \"password\",\n    \"connectionType\": \"SENTINEL\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T12:51:12.609Z\",\n    \"sentinelMaster\": {\n      \"name\": \"primary-group-1\",\n      \"username\": null,\n      \"password\": null\n    },\n    \"modules\": [],\n    \"tls\": null,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": null,\n    \"sshOptions\": null,\n    \"forceStandalone\": null,\n    \"tags\": []\n  },\n  {\n    \"compressor\": \"NONE\",\n    \"id\": \"6486d1ac-2a80-48d2-8cd9-0747552f8b7a\",\n    \"host\": \"172.31.100.109\",\n    \"port\": 6379,\n    \"name\": \"SSH Server\",\n    \"db\": 0,\n    \"username\": null,\n    \"password\": null,\n    \"connectionType\": \"STANDALONE\",\n    \"nameFromProvider\": null,\n    \"provider\": \"REDIS_COMMUNITY_EDITION\",\n    \"lastConnection\": \"2025-11-19T13:21:59.492Z\",\n    \"modules\": [\n      {\n        \"name\": \"search\",\n        \"version\": 999999,\n        \"semanticVersion\": \"99.99.99\"\n      },\n      {\n        \"name\": \"ReJSON\",\n        \"version\": 20011,\n        \"semanticVersion\": \"2.0.11\"\n      },\n      {\n        \"name\": \"timeseries\",\n        \"version\": 10616,\n        \"semanticVersion\": \"1.6.16\"\n      },\n      {\n        \"name\": \"graph\",\n        \"version\": 20815,\n        \"semanticVersion\": \"2.8.15\"\n      },\n      {\n        \"name\": \"bf\",\n        \"version\": 20209,\n        \"semanticVersion\": \"2.2.9\"\n      }\n    ],\n    \"tls\": false,\n    \"tlsServername\": null,\n    \"verifyServerCert\": null,\n    \"caCert\": null,\n    \"clientCert\": null,\n    \"ssh\": true,\n    \"sshOptions\": {\n      \"id\": \"71207769-d1d0-48df-8c2e-161137a43648\",\n      \"host\": \"localhost\",\n      \"port\": 2222,\n      \"username\": \"u\",\n      \"password\": \"pass\",\n      \"privateKey\": null,\n      \"passphrase\": null\n    },\n    \"forceStandalone\": false,\n    \"tags\": []\n  }\n]"
  },
  {
    "path": "tests/e2e/rte/openvpn/docker-compose.yml",
    "content": "version: \"3.4\"\n\nservices:\n  openvpn:\n    cap_add:\n      - NET_ADMIN\n    image: kylemanna/openvpn\n    container_name: openvpn\n    ports:\n      - \"1194:1194/udp\"\n    restart: always\n    volumes:\n      - ./openvpn-data/conf:/etc/openvpn\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/crl.pem",
    "content": "-----BEGIN X509 CRL-----\nMIIB8TCB2gIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0Fw0yNTA5\nMDMxNjM0MjhaFw0zNTA5MDExNjM0MjhaMEcwIQIQE50liYbSTPfyAA9DZep83hcN\nMjUwOTAzMTYzNDIyWjAiAhEAgFWASsrgEJAw+3lH8xFHqRcNMjUwOTAzMTYzNDEx\nWqBOMEwwSgYDVR0jBEMwQYAUPwGq0LHvvgK4J9XB8b16BvFvj3OhE6QRMA8xDTAL\nBgNVBAMMBHRlc3SCFHKh49rkvHCvUmEIiZqCEf+31ND6MA0GCSqGSIb3DQEBCwUA\nA4IBAQCLLeHHjnBvTKOpkWoxfhagp3/mHeWWFc+ea4DfowkhK6RW09Qt9jzR8prF\nIy5+auYLlnhh9uWGc07FoId7wL9tiIKBmSMOtw/ySkYweuD11T2xoaCojwisfrVF\nQChjnQ10ZoDMw/ZfTQPns7qlYghdZyUIkIB/3IdqUIEpoGFvqf4Sf1Lx/1/WMD3E\nJN7/zqPXMJ0/+ihxIEsWWkliG5/3RjPy6XDVQfkpfd2gX8EM9pIKAwqvBbmFPKQe\nn5lgWRSYaUGO4/RbgbcOz3XrgRP8+aguO6uXQkzPJPA8ODWVdvWBo/AAul2gsfW3\n6WRaBfGuawIXka6K++C6NcW41fOR\n-----END X509 CRL-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/openvpn.conf",
    "content": "server 192.168.255.0 255.255.255.0\nverb 3\nkey /etc/openvpn/pki/private/localhost.key\nca /etc/openvpn/pki/ca.crt\ncert /etc/openvpn/pki/issued/localhost.crt\ndh /etc/openvpn/pki/dh.pem\ntls-auth /etc/openvpn/pki/ta.key\nkey-direction 0\nkeepalive 10 60\npersist-key\npersist-tun\n\nproto udp\n# Rely on Docker to do port mapping, internally always 1194\nport 1194\ndev tun0\nstatus /tmp/openvpn-status.log\n\nuser nobody\ngroup nogroup\ncomp-lzo no\n\n### Push Configurations Below\npush \"dhcp-option DNS 192.168.13.6\"\npush \"comp-lzo no\"\npush \"dhcp-option DOMAIN localhost\"\npush \"route 192.168.13.0 255.255.255.0\"\npush \"route 172.30.0.0 255.255.0.0\"\npush \"route 172.31.0.0 255.255.0.0\"\npush \"route 172.33.0.0 255.255.0.0\"\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/openvpn.conf.1636357834.bak",
    "content": "server 192.168.255.0 255.255.255.0\nverb 3\nkey /etc/openvpn/pki/private/localhost.key\nca /etc/openvpn/pki/ca.crt\ncert /etc/openvpn/pki/issued/localhost.crt\ndh /etc/openvpn/pki/dh.pem\ntls-auth /etc/openvpn/pki/ta.key\nkey-direction 0\nkeepalive 10 60\npersist-key\npersist-tun\n\nproto udp\n# Rely on Docker to do port mapping, internally always 1194\nport 1194\ndev tun0\nstatus /tmp/openvpn-status.log\n\nuser nobody\ngroup nogroup\ncomp-lzo no\n\n### Route Configurations Below\nroute 192.168.254.0 255.255.255.0\n\n### Push Configurations Below\npush \"block-outside-dns\"\npush \"dhcp-option DNS 8.8.8.8\"\npush \"dhcp-option DNS 8.8.4.4\"\npush \"comp-lzo no\"\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/ovpn_env.sh",
    "content": "declare -x OVPN_AUTH=\ndeclare -x OVPN_CIPHER=\ndeclare -x OVPN_CLIENT_TO_CLIENT=\ndeclare -x OVPN_CN=localhost\ndeclare -x OVPN_COMP_LZO=0\ndeclare -x OVPN_DEFROUTE=0\ndeclare -x OVPN_DEVICE=tun\ndeclare -x OVPN_DEVICEN=0\ndeclare -x OVPN_DISABLE_PUSH_BLOCK_DNS=1\ndeclare -x OVPN_DNS=1\ndeclare -x OVPN_DNS_SERVERS=([0]=\"192.168.13.6\")\ndeclare -x OVPN_ENV=/etc/openvpn/ovpn_env.sh\ndeclare -x OVPN_EXTRA_CLIENT_CONFIG=()\ndeclare -x OVPN_EXTRA_SERVER_CONFIG=()\ndeclare -x OVPN_FRAGMENT=\ndeclare -x OVPN_KEEPALIVE='10 60'\ndeclare -x OVPN_MTU=\ndeclare -x OVPN_NAT=1\ndeclare -x OVPN_PORT=1194\ndeclare -x OVPN_PROTO=udp\ndeclare -x OVPN_PUSH=([0]=\"dhcp-option DOMAIN localhost\" [1]=\"route 192.168.13.0 255.255.255.0\" [2]=\"route 172.17.0.0 255.255.0.0\")\ndeclare -x OVPN_ROUTES=()\ndeclare -x OVPN_SERVER=192.168.255.0/24\ndeclare -x OVPN_SERVER_URL=udp://localhost\ndeclare -x OVPN_TLS_CIPHER=\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDNjCCAh6gAwIBAgIUcqHj2uS8cK9SYQiJmoIR/7fU0PowDQYJKoZIhvcNAQEL\nBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0yMTExMDgwNzQ5MTBaFw0zMTExMDYwNzQ5\nMTBaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQCibt8kh9lqTC0O631rPHN0kMQ4kMQ/eZ59mKhAJZ3rBchIBrQne2yTw2z+\nX1ESa3VTkW2jyJ5r7iuo+Xyc8246tfBwO3u0DJ2DeZZOYPzMg48nJNxs3ur3iXAT\nr6Aiwp0gtMNC2XcW7y5OPl8l+BhSt2PsWcdEdmLJgvRPJ2x+Ea8wivuw6FO6byK7\nMxw7/CbNMw8Eey9eSz9kWDrgetS0kOgfqtt1ZnKDZkbLy8jFl0xW488VUrefUR1g\nlOje8QySjDvzT8sUR0lASyS+/J6j/3gLlSS42e4SxMz00jEus+ye56cO16Pc+vKI\nXsev8cRPiSDTZTvc7Eaq/OcKVl11AgMBAAGjgYkwgYYwHQYDVR0OBBYEFD8BqtCx\n774CuCfVwfG9egbxb49zMEoGA1UdIwRDMEGAFD8BqtCx774CuCfVwfG9egbxb49z\noROkETAPMQ0wCwYDVQQDDAR0ZXN0ghRyoePa5Lxwr1JhCImaghH/t9TQ+jAMBgNV\nHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAXUrDhVAX\nTkbhKRBuhUGQb03RyACQKBFM/SwhrmwpQMXo7BUuqWJ27U5/TRHrfKJxDgppmwIs\nqmtrT07tA7e/OyFSZtZ9p/4H+5xM9FCsmu6YMQ3ZloHHGWmibrDNK70frVgRAEAS\nFyAsEgpKZCr6OJNd7v2dbvO4AniZVVvccU17cJAx177YC3fNIuRtpHkm93D3qI+1\n4SED7rktVfXUKs6RMFmqIum5WRzgiJBAtk2GVQMrAAu/xmUPS/aqzstNte4KQ+UY\n2qI9v1wYM8j+BT5nsBT02K+zOsYdkG39n7QEfcecPAjOkKsaFbSf/WZcsb6oCVgl\nd/Nz24kfh76SqQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/certs_by_serial/139D258986D24CF7F2000F4365EA7CDE.pem",
    "content": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            13:9d:25:89:86:d2:4c:f7:f2:00:0f:43:65:ea:7c:de\n        Signature Algorithm: sha256WithRSAEncryption\n        Issuer: CN=test\n        Validity\n            Not Before: Nov  8 07:49:15 2021 GMT\n            Not After : Feb 11 07:49:15 2024 GMT\n        Subject: CN=localhost\n        Subject Public Key Info:\n            Public Key Algorithm: rsaEncryption\n                RSA Public-Key: (2048 bit)\n                Modulus:\n                    00:b8:d5:e2:66:d3:aa:e8:f7:2f:d8:76:b6:c6:67:\n                    5c:09:77:df:0b:1b:59:ca:f9:a9:fe:cc:50:91:91:\n                    a4:2a:96:55:54:8c:a9:17:25:23:8d:93:76:05:5c:\n                    9e:86:68:82:22:42:52:f6:7d:72:5f:85:5c:7c:61:\n                    d2:b2:a3:a2:5b:40:05:6f:eb:be:63:75:86:29:e7:\n                    97:e4:d7:20:1e:b4:c4:79:76:f7:cf:1d:70:ba:b0:\n                    10:ef:4e:9c:dc:15:4f:ee:b9:a7:b9:3f:f1:97:dd:\n                    77:0b:0e:3b:0b:c2:bd:b3:87:07:a4:95:2c:78:6b:\n                    7c:ac:7a:e4:02:c1:a0:3e:f5:ef:3a:51:f4:b3:4a:\n                    48:58:d0:16:10:8d:64:ba:a0:16:88:f0:62:55:fe:\n                    36:7b:9d:45:9f:f8:6d:e9:2a:1c:35:57:67:8e:2f:\n                    55:2f:27:87:dd:ce:df:a4:f3:9b:b5:80:7b:4a:f6:\n                    28:74:52:2d:cf:d9:ae:34:7f:6c:1d:89:f2:fc:00:\n                    aa:1c:fa:a0:30:22:14:19:76:65:9c:31:60:39:5d:\n                    0d:0a:15:80:b2:26:44:69:73:a2:0d:11:c0:b5:21:\n                    6f:52:cd:4a:2f:87:23:48:28:fc:8c:db:83:83:56:\n                    7a:a5:63:61:4c:6c:bb:3b:80:9f:ba:ad:66:63:b0:\n                    63:57\n                Exponent: 65537 (0x10001)\n        X509v3 extensions:\n            X509v3 Basic Constraints: \n                CA:FALSE\n            X509v3 Subject Key Identifier: \n                DD:B7:0D:86:6B:E4:2F:30:5F:6C:C1:A8:A8:23:66:06:36:C4:30:BC\n            X509v3 Authority Key Identifier: \n                keyid:3F:01:AA:D0:B1:EF:BE:02:B8:27:D5:C1:F1:BD:7A:06:F1:6F:8F:73\n                DirName:/CN=test\n                serial:72:A1:E3:DA:E4:BC:70:AF:52:61:08:89:9A:82:11:FF:B7:D4:D0:FA\n\n            X509v3 Extended Key Usage: \n                TLS Web Server Authentication\n            X509v3 Key Usage: \n                Digital Signature, Key Encipherment\n            X509v3 Subject Alternative Name: \n                DNS:localhost\n    Signature Algorithm: sha256WithRSAEncryption\n         09:97:10:6a:b8:62:e7:e8:a5:a0:45:33:d2:85:0d:ca:61:73:\n         8c:85:27:1d:3d:68:8b:65:55:4b:51:d9:86:a8:89:92:52:6b:\n         98:4c:4b:75:05:ed:6e:e0:63:96:ce:44:b1:47:2a:71:32:32:\n         86:f2:e3:5d:68:bd:82:1c:66:23:7a:ff:9e:e4:c3:a2:cd:79:\n         2c:a0:63:9e:f5:cc:e4:71:60:0d:a5:69:5e:b5:c1:cb:4e:94:\n         18:c5:f9:cd:89:c3:7a:33:4d:5b:6c:ec:9d:0c:0b:fe:72:72:\n         07:b6:6d:ba:2b:10:e6:6e:0b:94:b6:3e:67:1a:c1:fe:73:e0:\n         dd:be:4c:1d:29:2b:01:fe:3e:ec:c6:d0:c8:de:04:77:ff:6a:\n         7e:81:8f:86:1b:42:70:38:d1:47:cd:b9:11:33:9c:b2:7d:fa:\n         b4:5e:a2:a4:cd:0c:ed:3e:b1:28:f6:3d:6f:df:ea:34:83:b2:\n         fc:c1:31:28:75:02:fb:64:20:06:89:a9:31:ff:7c:0a:bc:c0:\n         aa:11:45:a4:e0:f4:98:cc:f7:77:21:de:41:34:32:97:3b:d7:\n         88:58:47:7b:fb:c1:d2:9a:dc:5f:02:3f:4c:d9:99:71:f4:7b:\n         c8:31:c6:31:55:93:0e:42:28:b7:cb:43:e3:21:ce:84:de:0c:\n         a5:e1:7b:32\n-----BEGIN CERTIFICATE-----\nMIIDXzCCAkegAwIBAgIQE50liYbSTPfyAA9DZep83jANBgkqhkiG9w0BAQsFADAP\nMQ0wCwYDVQQDDAR0ZXN0MB4XDTIxMTEwODA3NDkxNVoXDTI0MDIxMTA3NDkxNVow\nFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAuNXiZtOq6Pcv2Ha2xmdcCXffCxtZyvmp/sxQkZGkKpZVVIypFyUjjZN2\nBVyehmiCIkJS9n1yX4VcfGHSsqOiW0AFb+u+Y3WGKeeX5NcgHrTEeXb3zx1wurAQ\n706c3BVP7rmnuT/xl913Cw47C8K9s4cHpJUseGt8rHrkAsGgPvXvOlH0s0pIWNAW\nEI1kuqAWiPBiVf42e51Fn/ht6SocNVdnji9VLyeH3c7fpPObtYB7SvYodFItz9mu\nNH9sHYny/ACqHPqgMCIUGXZlnDFgOV0NChWAsiZEaXOiDRHAtSFvUs1KL4cjSCj8\njNuDg1Z6pWNhTGy7O4Cfuq1mY7BjVwIDAQABo4GxMIGuMAkGA1UdEwQCMAAwHQYD\nVR0OBBYEFN23DYZr5C8wX2zBqKgjZgY2xDC8MEoGA1UdIwRDMEGAFD8BqtCx774C\nuCfVwfG9egbxb49zoROkETAPMQ0wCwYDVQQDDAR0ZXN0ghRyoePa5Lxwr1JhCIma\nghH/t9TQ+jATBgNVHSUEDDAKBggrBgEFBQcDATALBgNVHQ8EBAMCBaAwFAYDVR0R\nBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQAJlxBquGLn6KWgRTPS\nhQ3KYXOMhScdPWiLZVVLUdmGqImSUmuYTEt1Be1u4GOWzkSxRypxMjKG8uNdaL2C\nHGYjev+e5MOizXksoGOe9czkcWANpWletcHLTpQYxfnNicN6M01bbOydDAv+cnIH\ntm26KxDmbguUtj5nGsH+c+DdvkwdKSsB/j7sxtDI3gR3/2p+gY+GG0JwONFHzbkR\nM5yyffq0XqKkzQztPrEo9j1v3+o0g7L8wTEodQL7ZCAGiakx/3wKvMCqEUWk4PSY\nzPd3Id5BNDKXO9eIWEd7+8HSmtxfAj9M2Zlx9HvIMcYxVZMOQii3y0PjIc6E3gyl\n4Xsy\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/certs_by_serial/8055804ACAE0109030FB7947F31147A9.pem",
    "content": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            80:55:80:4a:ca:e0:10:90:30:fb:79:47:f3:11:47:a9\n        Signature Algorithm: sha256WithRSAEncryption\n        Issuer: CN=test\n        Validity\n            Not Before: Nov  8 07:51:56 2021 GMT\n            Not After : Feb 11 07:51:56 2024 GMT\n        Subject: CN=test\n        Subject Public Key Info:\n            Public Key Algorithm: rsaEncryption\n                RSA Public-Key: (2048 bit)\n                Modulus:\n                    00:cd:ba:3a:57:9b:0f:9b:dd:5a:c3:8c:ac:f0:24:\n                    2e:20:8e:3b:33:6d:86:b2:cb:81:00:83:4a:4f:16:\n                    40:cd:b3:e5:1d:c5:7f:98:e7:4b:a0:f5:6f:f3:5b:\n                    1b:a2:6b:16:4d:5c:5b:fe:46:c3:58:8e:0e:13:f9:\n                    ec:69:68:37:f6:7d:e0:7e:8b:95:0f:71:ba:89:b1:\n                    5d:0e:ca:7c:9b:9e:07:57:c2:4b:e3:42:96:ef:5e:\n                    43:ea:fe:11:f2:38:3a:b4:0c:e3:e2:4c:28:e2:07:\n                    bb:9a:56:63:98:88:91:15:f5:27:4d:a5:d1:88:0c:\n                    49:48:24:8f:71:8d:7d:0e:48:1b:d9:95:a4:7b:f2:\n                    b7:f6:68:95:0c:14:2f:19:8d:ac:c5:cd:95:ac:42:\n                    93:ab:6e:60:33:40:90:f6:80:4e:a8:4b:f0:0f:d4:\n                    d6:c0:5d:f2:8f:dd:c0:41:2b:78:96:12:60:37:e7:\n                    c5:cc:ba:7a:36:de:0a:f0:e5:c9:90:51:3d:66:a6:\n                    d1:b9:d2:b4:d3:ad:cb:72:f9:46:45:33:65:4a:e3:\n                    e9:95:ee:23:37:92:b0:6b:a8:95:02:06:04:6b:7e:\n                    44:a9:4e:3c:fd:93:5b:32:4c:c3:40:24:9e:52:14:\n                    d1:ac:aa:c5:88:4b:88:75:51:1c:96:26:c2:d7:75:\n                    c6:7b\n                Exponent: 65537 (0x10001)\n        X509v3 extensions:\n            X509v3 Basic Constraints: \n                CA:FALSE\n            X509v3 Subject Key Identifier: \n                8E:9E:25:9F:4F:53:6D:1D:D6:2C:08:03:2C:66:C3:6F:90:16:53:96\n            X509v3 Authority Key Identifier: \n                keyid:3F:01:AA:D0:B1:EF:BE:02:B8:27:D5:C1:F1:BD:7A:06:F1:6F:8F:73\n                DirName:/CN=test\n                serial:72:A1:E3:DA:E4:BC:70:AF:52:61:08:89:9A:82:11:FF:B7:D4:D0:FA\n\n            X509v3 Extended Key Usage: \n                TLS Web Client Authentication\n            X509v3 Key Usage: \n                Digital Signature\n    Signature Algorithm: sha256WithRSAEncryption\n         00:67:d2:93:e6:69:cb:fd:f6:9f:df:f6:59:20:2a:f6:0f:03:\n         ba:b3:da:65:de:62:23:36:a2:8d:4f:27:22:0e:3d:01:80:d9:\n         59:cd:c5:f0:1a:9b:c5:e8:f4:6f:e2:c7:29:fc:37:21:2f:6f:\n         9d:b8:8c:f6:6e:37:c9:b1:4a:0d:9d:e5:cd:0a:4b:01:0a:98:\n         8f:46:e9:24:97:9c:ef:75:dd:a4:f7:33:7d:df:09:f3:4c:b6:\n         3c:38:a7:2e:26:1d:68:f9:87:9a:ae:6c:60:d9:de:32:f1:69:\n         66:97:cb:20:81:0d:b5:01:74:b5:73:8c:85:2b:5a:73:ea:cd:\n         e5:25:13:44:3a:24:0a:0a:72:4d:42:cc:0b:5a:c9:96:05:20:\n         37:fb:1b:95:18:8d:66:ff:10:f8:3a:d8:03:6c:6c:37:6e:de:\n         51:59:08:7e:d1:33:11:08:74:ed:fc:3f:4d:19:00:82:88:9f:\n         95:66:a6:e9:f1:73:55:e1:7a:3f:ae:a6:e1:b7:51:df:92:28:\n         19:42:1d:a5:a7:ed:b9:e4:00:ea:a7:55:e3:55:12:45:5f:f9:\n         e1:a5:1f:13:f1:ee:1a:31:e5:ae:9d:2e:ef:dd:d8:56:b2:7c:\n         f6:ba:08:41:db:13:16:31:0e:5d:41:b2:6d:98:01:e4:43:a2:\n         d1:34:9e:91\n-----BEGIN CERTIFICATE-----\nMIIDRTCCAi2gAwIBAgIRAIBVgErK4BCQMPt5R/MRR6kwDQYJKoZIhvcNAQELBQAw\nDzENMAsGA1UEAwwEdGVzdDAeFw0yMTExMDgwNzUxNTZaFw0yNDAyMTEwNzUxNTZa\nMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\nAQDNujpXmw+b3VrDjKzwJC4gjjszbYayy4EAg0pPFkDNs+UdxX+Y50ug9W/zWxui\naxZNXFv+RsNYjg4T+expaDf2feB+i5UPcbqJsV0OynybngdXwkvjQpbvXkPq/hHy\nODq0DOPiTCjiB7uaVmOYiJEV9SdNpdGIDElIJI9xjX0OSBvZlaR78rf2aJUMFC8Z\njazFzZWsQpOrbmAzQJD2gE6oS/AP1NbAXfKP3cBBK3iWEmA358XMuno23grw5cmQ\nUT1mptG50rTTrcty+UZFM2VK4+mV7iM3krBrqJUCBgRrfkSpTjz9k1syTMNAJJ5S\nFNGsqsWIS4h1URyWJsLXdcZ7AgMBAAGjgZswgZgwCQYDVR0TBAIwADAdBgNVHQ4E\nFgQUjp4ln09TbR3WLAgDLGbDb5AWU5YwSgYDVR0jBEMwQYAUPwGq0LHvvgK4J9XB\n8b16BvFvj3OhE6QRMA8xDTALBgNVBAMMBHRlc3SCFHKh49rkvHCvUmEIiZqCEf+3\n1ND6MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1UdDwQEAwIHgDANBgkqhkiG9w0B\nAQsFAAOCAQEAAGfSk+Zpy/32n9/2WSAq9g8DurPaZd5iIzaijU8nIg49AYDZWc3F\n8Bqbxej0b+LHKfw3IS9vnbiM9m43ybFKDZ3lzQpLAQqYj0bpJJec73XdpPczfd8J\n80y2PDinLiYdaPmHmq5sYNneMvFpZpfLIIENtQF0tXOMhStac+rN5SUTRDokCgpy\nTULMC1rJlgUgN/sblRiNZv8Q+DrYA2xsN27eUVkIftEzEQh07fw/TRkAgoiflWam\n6fFzVeF6P66m4bdR35IoGUIdpaftueQA6qdV41USRV/54aUfE/HuGjHlrp0u793Y\nVrJ89roIQdsTFjEOXUGybZgB5EOi0TSekQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/crl.pem",
    "content": "-----BEGIN X509 CRL-----\nMIIB8TCB2gIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0ZXN0Fw0yNTA5\nMDMxNjM0MjhaFw0zNTA5MDExNjM0MjhaMEcwIQIQE50liYbSTPfyAA9DZep83hcN\nMjUwOTAzMTYzNDIyWjAiAhEAgFWASsrgEJAw+3lH8xFHqRcNMjUwOTAzMTYzNDEx\nWqBOMEwwSgYDVR0jBEMwQYAUPwGq0LHvvgK4J9XB8b16BvFvj3OhE6QRMA8xDTAL\nBgNVBAMMBHRlc3SCFHKh49rkvHCvUmEIiZqCEf+31ND6MA0GCSqGSIb3DQEBCwUA\nA4IBAQCLLeHHjnBvTKOpkWoxfhagp3/mHeWWFc+ea4DfowkhK6RW09Qt9jzR8prF\nIy5+auYLlnhh9uWGc07FoId7wL9tiIKBmSMOtw/ySkYweuD11T2xoaCojwisfrVF\nQChjnQ10ZoDMw/ZfTQPns7qlYghdZyUIkIB/3IdqUIEpoGFvqf4Sf1Lx/1/WMD3E\nJN7/zqPXMJ0/+ihxIEsWWkliG5/3RjPy6XDVQfkpfd2gX8EM9pIKAwqvBbmFPKQe\nn5lgWRSYaUGO4/RbgbcOz3XrgRP8+aguO6uXQkzPJPA8ODWVdvWBo/AAul2gsfW3\n6WRaBfGuawIXka6K++C6NcW41fOR\n-----END X509 CRL-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/dh.pem",
    "content": "-----BEGIN DH PARAMETERS-----\nMIIBCAKCAQEAlG3e5COo1wC8aPTfb2ilsHyfPSj8WHMDMFfZYAiJsLAW6sNaA33L\n9AfgsYDgb5CoHhil47Yrons97nvdC6lwVuG61Q7S77VX0MV6b12Gu+D8VJElnoNB\nyQP/z6Frfg7OKCDelIfkvqYYPqQD33S7XR2a+7vO2E/vnc7vcfHozUUPKHqFtxyt\nMNYuIs74l+2HHBHEO9fKWHc4IfHEkROQbehy0y6//qiKz/WqWAkQPX6eqgf26V23\nTyOT4UBvNv7nqOEpV4WS+zg+qH5c/kkcwSD/8jJMMi1cEWvz+9w+Kh7ponzQqOU0\nLOUysoYDcsZfuE/1SftOs44jUb4UCsKC4wIBAg==\n-----END DH PARAMETERS-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/index.txt",
    "content": "R\t240211074915Z\t250903163422Z\t139D258986D24CF7F2000F4365EA7CDE\tunknown\t/CN=localhost\nR\t240211075156Z\t250903163411Z\t8055804ACAE0109030FB7947F31147A9\tunknown\t/CN=test\nV\t271207163443Z\t\tD9F53C541F0085D0D704948CD0A688DA\tunknown\t/CN=localhost\nV\t271207163451Z\t\t34AAB5929E45F5A821E772DBBB0F2568\tunknown\t/CN=test\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/index.txt.attr",
    "content": "unique_subject = no\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/index.txt.attr.old",
    "content": "unique_subject = no\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/index.txt.old",
    "content": "R\t240211074915Z\t250903163422Z\t139D258986D24CF7F2000F4365EA7CDE\tunknown\t/CN=localhost\nR\t240211075156Z\t250903163411Z\t8055804ACAE0109030FB7947F31147A9\tunknown\t/CN=test\nV\t271207163443Z\t\tD9F53C541F0085D0D704948CD0A688DA\tunknown\t/CN=localhost\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/issued/localhost.crt",
    "content": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            d9:f5:3c:54:1f:00:85:d0:d7:04:94:8c:d0:a6:88:da\n        Signature Algorithm: sha256WithRSAEncryption\n        Issuer: CN=test\n        Validity\n            Not Before: Sep  3 16:34:43 2025 GMT\n            Not After : Dec  7 16:34:43 2027 GMT\n        Subject: CN=localhost\n        Subject Public Key Info:\n            Public Key Algorithm: rsaEncryption\n                RSA Public-Key: (2048 bit)\n                Modulus:\n                    00:a6:15:cb:fb:01:2c:07:de:73:6f:8f:e4:79:3b:\n                    3b:8a:e4:90:f7:4f:f4:5b:b4:f0:89:1e:90:5c:67:\n                    1c:9f:8c:22:7c:3a:87:8e:f6:8f:72:cf:32:72:48:\n                    47:86:57:39:cb:af:12:1a:32:76:34:13:2b:45:0c:\n                    bc:fa:b1:7a:f9:8f:52:17:7c:b3:3f:4c:9e:86:ac:\n                    97:99:61:91:28:70:60:74:57:f0:f3:bd:aa:e5:b6:\n                    c5:7e:9c:61:ac:48:84:67:f7:8c:ca:b8:1e:5f:90:\n                    2c:e7:85:33:d8:57:c5:68:3d:2a:15:08:e0:94:6a:\n                    9c:47:ab:eb:c3:5e:5d:70:a2:0b:69:8a:38:57:04:\n                    14:c8:cb:44:8f:56:37:1b:f9:39:47:bb:eb:e2:fe:\n                    f0:96:77:e0:d5:83:c7:f3:9d:cc:cf:0a:03:fe:fe:\n                    c3:3e:53:bb:c4:b1:6e:82:27:60:ab:1c:6e:79:41:\n                    b1:b2:5c:46:1c:f0:68:8a:0f:36:fd:eb:d7:16:98:\n                    9a:4e:8e:62:9a:d9:90:22:5a:ab:9e:3a:22:05:63:\n                    8c:d5:59:35:27:39:58:c3:cf:c7:34:0c:6e:32:34:\n                    48:69:a5:ad:f5:58:d3:41:f1:59:a2:83:ec:ff:f5:\n                    13:ee:f6:15:6e:62:49:76:a8:fd:7d:9d:6d:fe:f8:\n                    7a:09\n                Exponent: 65537 (0x10001)\n        X509v3 extensions:\n            X509v3 Basic Constraints: \n                CA:FALSE\n            X509v3 Subject Key Identifier: \n                0C:AC:8B:B4:4C:75:DF:6E:A8:FD:08:35:40:9B:30:23:5E:3A:2E:4D\n            X509v3 Authority Key Identifier: \n                keyid:3F:01:AA:D0:B1:EF:BE:02:B8:27:D5:C1:F1:BD:7A:06:F1:6F:8F:73\n                DirName:/CN=test\n                serial:72:A1:E3:DA:E4:BC:70:AF:52:61:08:89:9A:82:11:FF:B7:D4:D0:FA\n\n            X509v3 Extended Key Usage: \n                TLS Web Server Authentication\n            X509v3 Key Usage: \n                Digital Signature, Key Encipherment\n            X509v3 Subject Alternative Name: \n                DNS:localhost\n    Signature Algorithm: sha256WithRSAEncryption\n         0b:ae:62:ed:32:35:77:64:00:44:01:30:ed:eb:e1:21:42:9c:\n         05:ef:af:1d:f3:0f:58:58:e2:0c:14:1a:04:06:79:e8:f6:da:\n         bc:60:90:fd:08:90:0a:bd:a3:95:b2:66:b9:3d:9b:0a:14:87:\n         b0:c1:16:c6:7c:4a:4f:ef:d5:be:bd:85:bc:5a:cf:e4:a0:dd:\n         3e:f0:c6:1e:2f:1a:01:c8:91:0e:16:59:aa:44:11:fe:76:69:\n         ba:31:ba:56:7e:03:ba:9d:cb:59:fa:98:b3:77:a2:f8:f1:d1:\n         4f:ba:72:0a:90:00:b2:a9:28:11:8e:ee:9a:be:29:6b:64:18:\n         65:8d:8d:23:84:61:68:ed:33:57:04:06:69:48:c0:84:51:fd:\n         bf:c5:b3:cd:3f:33:d7:94:6c:71:10:bf:7e:05:35:c9:31:93:\n         bb:c0:d7:4e:58:eb:93:a9:bc:13:06:d1:60:44:01:12:62:7f:\n         40:65:21:f3:ce:6d:67:54:3e:46:b9:b0:bc:ce:3b:a8:eb:d3:\n         2e:46:57:45:95:b0:83:f5:b0:02:8a:ff:16:bd:bd:f4:c6:6b:\n         d4:a3:a8:57:96:a2:08:13:ff:49:54:b6:8c:c4:24:7b:86:ec:\n         3c:0c:cf:5d:dd:a8:f1:b3:0e:ed:40:bd:93:26:98:e2:22:38:\n         27:eb:c8:cd\n-----BEGIN CERTIFICATE-----\nMIIDYDCCAkigAwIBAgIRANn1PFQfAIXQ1wSUjNCmiNowDQYJKoZIhvcNAQELBQAw\nDzENMAsGA1UEAwwEdGVzdDAeFw0yNTA5MDMxNjM0NDNaFw0yNzEyMDcxNjM0NDNa\nMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAKYVy/sBLAfec2+P5Hk7O4rkkPdP9Fu08IkekFxnHJ+MInw6h472j3LP\nMnJIR4ZXOcuvEhoydjQTK0UMvPqxevmPUhd8sz9Mnoasl5lhkShwYHRX8PO9quW2\nxX6cYaxIhGf3jMq4Hl+QLOeFM9hXxWg9KhUI4JRqnEer68NeXXCiC2mKOFcEFMjL\nRI9WNxv5OUe76+L+8JZ34NWDx/OdzM8KA/7+wz5Tu8SxboInYKscbnlBsbJcRhzw\naIoPNv3r1xaYmk6OYprZkCJaq546IgVjjNVZNSc5WMPPxzQMbjI0SGmlrfVY00Hx\nWaKD7P/1E+72FW5iSXao/X2dbf74egkCAwEAAaOBsTCBrjAJBgNVHRMEAjAAMB0G\nA1UdDgQWBBQMrIu0THXfbqj9CDVAmzAjXjouTTBKBgNVHSMEQzBBgBQ/AarQse++\nArgn1cHxvXoG8W+Pc6ETpBEwDzENMAsGA1UEAwwEdGVzdIIUcqHj2uS8cK9SYQiJ\nmoIR/7fU0PowEwYDVR0lBAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMBQGA1Ud\nEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAC65i7TI1d2QARAEw\n7evhIUKcBe+vHfMPWFjiDBQaBAZ56PbavGCQ/QiQCr2jlbJmuT2bChSHsMEWxnxK\nT+/Vvr2FvFrP5KDdPvDGHi8aAciRDhZZqkQR/nZpujG6Vn4Dup3LWfqYs3ei+PHR\nT7pyCpAAsqkoEY7umr4pa2QYZY2NI4RhaO0zVwQGaUjAhFH9v8WzzT8z15RscRC/\nfgU1yTGTu8DXTljrk6m8EwbRYEQBEmJ/QGUh885tZ1Q+RrmwvM47qOvTLkZXRZWw\ng/WwAor/Fr299MZr1KOoV5aiCBP/SVS2jMQke4bsPAzPXd2o8bMO7UC9kyaY4iI4\nJ+vIzQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/issued/test.crt",
    "content": "Certificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number:\n            34:aa:b5:92:9e:45:f5:a8:21:e7:72:db:bb:0f:25:68\n        Signature Algorithm: sha256WithRSAEncryption\n        Issuer: CN=test\n        Validity\n            Not Before: Sep  3 16:34:51 2025 GMT\n            Not After : Dec  7 16:34:51 2027 GMT\n        Subject: CN=test\n        Subject Public Key Info:\n            Public Key Algorithm: rsaEncryption\n                RSA Public-Key: (2048 bit)\n                Modulus:\n                    00:bb:03:97:b2:e4:57:a3:5c:52:25:a2:e5:3e:41:\n                    75:0f:95:c0:c3:94:f2:08:6c:0d:31:b9:b5:2c:04:\n                    13:5c:42:c1:f7:79:dd:4a:e1:8b:f6:89:5f:ff:eb:\n                    0c:7b:46:4d:8f:2b:65:d9:81:a3:f5:f2:d4:46:cb:\n                    c0:84:0d:55:ac:dc:15:51:ae:00:60:8b:a1:87:50:\n                    df:b8:1a:34:7f:3b:67:ba:02:2c:8f:9f:39:06:68:\n                    9c:f3:35:93:5a:f2:16:a1:21:df:87:63:5e:40:c6:\n                    ea:05:bf:f3:fe:21:10:4c:78:6e:11:60:2f:b8:a4:\n                    08:69:1a:82:cb:64:7f:3f:ea:af:b0:85:c5:fc:3a:\n                    8f:64:5c:39:61:12:5f:73:4a:9a:af:be:75:21:f4:\n                    9d:be:9c:59:12:72:19:59:96:5b:92:24:95:64:2d:\n                    de:b5:8a:a7:79:91:60:8d:57:63:7d:5d:8b:5f:83:\n                    38:54:8b:ad:49:11:48:1d:dd:d5:cb:a3:07:f7:a4:\n                    b7:b6:00:ae:40:cf:37:87:22:57:33:a0:57:3c:d7:\n                    6f:0d:98:e7:99:4e:d4:ff:44:5d:a3:4e:70:6e:3f:\n                    21:d1:35:a3:d8:96:51:0a:9c:66:ba:c2:61:17:c3:\n                    3f:f6:89:da:36:bc:88:4d:51:c4:1e:88:22:52:b8:\n                    52:3b\n                Exponent: 65537 (0x10001)\n        X509v3 extensions:\n            X509v3 Basic Constraints: \n                CA:FALSE\n            X509v3 Subject Key Identifier: \n                CE:19:19:52:AF:6C:46:95:D9:EE:D6:6E:5F:C3:43:64:28:0B:AF:72\n            X509v3 Authority Key Identifier: \n                keyid:3F:01:AA:D0:B1:EF:BE:02:B8:27:D5:C1:F1:BD:7A:06:F1:6F:8F:73\n                DirName:/CN=test\n                serial:72:A1:E3:DA:E4:BC:70:AF:52:61:08:89:9A:82:11:FF:B7:D4:D0:FA\n\n            X509v3 Extended Key Usage: \n                TLS Web Client Authentication\n            X509v3 Key Usage: \n                Digital Signature\n    Signature Algorithm: sha256WithRSAEncryption\n         5b:43:e2:c9:cc:5a:28:5a:15:35:08:0e:9b:ab:a5:bf:db:06:\n         ab:a4:61:20:7d:0f:9b:65:3a:12:4e:2e:b4:49:8d:3e:73:f9:\n         94:0e:67:aa:b8:72:88:dc:10:13:fb:5c:eb:0e:d5:39:9d:93:\n         39:01:8b:4e:87:27:17:95:57:62:3a:f4:31:04:9c:6c:4c:ec:\n         e2:49:a5:5d:ec:98:7b:d4:eb:9f:d9:63:22:49:40:df:bb:78:\n         ee:fe:24:3e:f1:ae:9c:73:e1:28:42:c4:24:df:5a:46:01:64:\n         ba:76:42:c3:c6:19:c1:7c:e8:0d:a6:4b:5e:70:a4:99:30:ea:\n         de:b4:4b:38:90:29:3b:0d:8e:5d:ef:34:87:e1:75:63:b7:77:\n         2c:e1:ea:33:d8:24:9d:a5:f3:33:e1:07:c6:48:46:bd:78:aa:\n         1e:cb:c7:e6:ed:94:1b:e6:dc:e2:89:64:4e:19:5d:83:2d:47:\n         b8:a5:d6:3b:18:5e:e2:6e:41:f2:c4:ce:b9:89:89:b0:36:dc:\n         c3:ce:7e:a3:d9:c6:da:c8:d3:39:a5:e7:58:9a:29:57:7e:cf:\n         27:f1:55:e8:94:ca:7f:c9:67:17:f2:df:5a:d8:b2:7c:df:be:\n         41:d9:94:e2:f4:bf:65:79:bc:39:bb:73:eb:5c:82:d6:03:f0:\n         cf:a5:e5:20\n-----BEGIN CERTIFICATE-----\nMIIDRDCCAiygAwIBAgIQNKq1kp5F9agh53Lbuw8laDANBgkqhkiG9w0BAQsFADAP\nMQ0wCwYDVQQDDAR0ZXN0MB4XDTI1MDkwMzE2MzQ1MVoXDTI3MTIwNzE2MzQ1MVow\nDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nALsDl7LkV6NcUiWi5T5BdQ+VwMOU8ghsDTG5tSwEE1xCwfd53Urhi/aJX//rDHtG\nTY8rZdmBo/Xy1EbLwIQNVazcFVGuAGCLoYdQ37gaNH87Z7oCLI+fOQZonPM1k1ry\nFqEh34djXkDG6gW/8/4hEEx4bhFgL7ikCGkagstkfz/qr7CFxfw6j2RcOWESX3NK\nmq++dSH0nb6cWRJyGVmWW5IklWQt3rWKp3mRYI1XY31di1+DOFSLrUkRSB3d1cuj\nB/ekt7YArkDPN4ciVzOgVzzXbw2Y55lO1P9EXaNOcG4/IdE1o9iWUQqcZrrCYRfD\nP/aJ2ja8iE1RxB6IIlK4UjsCAwEAAaOBmzCBmDAJBgNVHRMEAjAAMB0GA1UdDgQW\nBBTOGRlSr2xGldnu1m5fw0NkKAuvcjBKBgNVHSMEQzBBgBQ/AarQse++Argn1cHx\nvXoG8W+Pc6ETpBEwDzENMAsGA1UEAwwEdGVzdIIUcqHj2uS8cK9SYQiJmoIR/7fU\n0PowEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQDAgeAMA0GCSqGSIb3DQEB\nCwUAA4IBAQBbQ+LJzFooWhU1CA6bq6W/2warpGEgfQ+bZToSTi60SY0+c/mUDmeq\nuHKI3BAT+1zrDtU5nZM5AYtOhycXlVdiOvQxBJxsTOziSaVd7Jh71Ouf2WMiSUDf\nu3ju/iQ+8a6cc+EoQsQk31pGAWS6dkLDxhnBfOgNpktecKSZMOretEs4kCk7DY5d\n7zSH4XVjt3cs4eoz2CSdpfMz4QfGSEa9eKoey8fm7ZQb5tziiWROGV2DLUe4pdY7\nGF7ibkHyxM65iYmwNtzDzn6j2cbayNM5pedYmilXfs8n8VXolMp/yWcX8t9a2LJ8\n375B2ZTi9L9lebw5u3PrXILWA/DPpeUg\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/openssl-easyrsa.cnf",
    "content": "# For use with Easy-RSA 3.0+ and OpenSSL or LibreSSL\n\n####################################################################\n[ ca ]\ndefault_ca\t= CA_default\t\t# The default ca section\n\n####################################################################\n[ CA_default ]\n\ndir\t\t= $ENV::EASYRSA_PKI\t# Where everything is kept\ncerts\t\t= $dir\t\t\t# Where the issued certs are kept\ncrl_dir\t\t= $dir\t\t\t# Where the issued crl are kept\ndatabase\t= $dir/index.txt\t# database index file.\nnew_certs_dir\t= $dir/certs_by_serial\t# default place for new certs.\n\ncertificate\t= $dir/ca.crt\t \t# The CA certificate\nserial\t\t= $dir/serial \t\t# The current serial number\ncrl\t\t= $dir/crl.pem \t\t# The current CRL\nprivate_key\t= $dir/private/ca.key\t# The private key\nRANDFILE\t= $dir/.rand\t\t# private random number file\n\nx509_extensions\t= basic_exts\t\t# The extensions to add to the cert\n\n# This allows a V2 CRL. Ancient browsers don't like it, but anything Easy-RSA\n# is designed for will. In return, we get the Issuer attached to CRLs.\ncrl_extensions\t= crl_ext\n\ndefault_days\t= $ENV::EASYRSA_CERT_EXPIRE\t# how long to certify for\ndefault_crl_days= $ENV::EASYRSA_CRL_DAYS\t# how long before next CRL\ndefault_md\t= $ENV::EASYRSA_DIGEST\t\t# use public key default MD\npreserve\t= no\t\t\t# keep passed DN ordering\n\n# This allows to renew certificates which have not been revoked\nunique_subject\t= no\n\n# A few different ways of specifying how similar the request should look\n# For type CA, the listed attributes must be the same, and the optional\n# and supplied fields are just that :-)\npolicy\t\t= policy_anything\n\n# For the 'anything' policy, which defines allowed DN fields\n[ policy_anything ]\ncountryName\t\t= optional\nstateOrProvinceName\t= optional\nlocalityName\t\t= optional\norganizationName\t= optional\norganizationalUnitName\t= optional\ncommonName\t\t= supplied\nname\t\t\t= optional\nemailAddress\t\t= optional\n\n####################################################################\n# Easy-RSA request handling\n# We key off $DN_MODE to determine how to format the DN\n[ req ]\ndefault_bits\t\t= $ENV::EASYRSA_KEY_SIZE\ndefault_keyfile \t= privkey.pem\ndefault_md\t\t= $ENV::EASYRSA_DIGEST\ndistinguished_name\t= $ENV::EASYRSA_DN\nx509_extensions\t\t= easyrsa_ca\t# The extensions to add to the self signed cert\n\n# A placeholder to handle the $EXTRA_EXTS feature:\n#%EXTRA_EXTS%\t# Do NOT remove or change this line as $EXTRA_EXTS support requires it\n\n####################################################################\n# Easy-RSA DN (Subject) handling\n\n# Easy-RSA DN for cn_only support:\n[ cn_only ]\ncommonName\t\t= Common Name (eg: your user, host, or server name)\ncommonName_max\t\t= 64\ncommonName_default\t= $ENV::EASYRSA_REQ_CN\n\n# Easy-RSA DN for org support:\n[ org ]\ncountryName\t\t\t= Country Name (2 letter code)\ncountryName_default\t\t= $ENV::EASYRSA_REQ_COUNTRY\ncountryName_min\t\t\t= 2\ncountryName_max\t\t\t= 2\n\nstateOrProvinceName\t\t= State or Province Name (full name)\nstateOrProvinceName_default\t= $ENV::EASYRSA_REQ_PROVINCE\n\nlocalityName\t\t\t= Locality Name (eg, city)\nlocalityName_default\t\t= $ENV::EASYRSA_REQ_CITY\n\n0.organizationName\t\t= Organization Name (eg, company)\n0.organizationName_default\t= $ENV::EASYRSA_REQ_ORG\n\norganizationalUnitName\t\t= Organizational Unit Name (eg, section)\norganizationalUnitName_default\t= $ENV::EASYRSA_REQ_OU\n\ncommonName\t\t\t= Common Name (eg: your user, host, or server name)\ncommonName_max\t\t\t= 64\ncommonName_default\t\t= $ENV::EASYRSA_REQ_CN\n\nemailAddress\t\t\t= Email Address\nemailAddress_default\t\t= $ENV::EASYRSA_REQ_EMAIL\nemailAddress_max\t\t= 64\n\n####################################################################\n# Easy-RSA cert extension handling\n\n# This section is effectively unused as the main script sets extensions\n# dynamically. This core section is left to support the odd usecase where\n# a user calls openssl directly.\n[ basic_exts ]\nbasicConstraints\t= CA:FALSE\nsubjectKeyIdentifier\t= hash\nauthorityKeyIdentifier\t= keyid,issuer:always\n\n# The Easy-RSA CA extensions\n[ easyrsa_ca ]\n\n# PKIX recommendations:\n\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always,issuer:always\n\n# This could be marked critical, but it's nice to support reading by any\n# broken clients who attempt to do so.\nbasicConstraints = CA:true\n\n# Limit key usage to CA tasks. If you really want to use the generated pair as\n# a self-signed cert, comment this out.\nkeyUsage = cRLSign, keyCertSign\n\n# nsCertType omitted by default. Let's try to let the deprecated stuff die.\n# nsCertType = sslCA\n\n# CRL extensions.\n[ crl_ext ]\n\n# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.\n\n# issuerAltName=issuer:copy\nauthorityKeyIdentifier=keyid:always,issuer:always\n\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/private/ca.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,9953B10D522B006610527D745ED2B3FF\n\nxOtU092mS9pMlkXv5EZVQ3mAAnOPh/qb/0zmHagFTys1jHKKBGnXUWr8cYGD5Dyk\nNVDoEn3RjAKhNV0Hvs18m/MUDqsuqj1fNw55LWszQ0/OioOA3+CiZlRd3qUmOFIZ\n2oG+5l/k1XoH2S/zho4L53vxAMcTr62R+BXO++G4C1nMFwIchjpYInCoVZZblhP/\nIue1lnYKkFhV609F5digBdM2mNIBJi5RST5T9rm0/EkxYNcfJgq9WMI4fN6ISXFm\nKgxAT9+22v90KEGTb7ehNoczOZ7YS3kYC4zMoniIZ78E5Fv0d83strIa0S6rkKL+\nhGZ6KmVrtJpisdUIQ6Vv6aD5UCJfxdC/mC64F+gs0ElrYFC4t+Rb1uA2YyNgfuue\nK0uAKcXPA4mpVyHd95TP5Yf332DiifnfO7/HqnULC+YQ+4Eo0zYJEfqbSbdI1hJD\ngcneKrtRMIFL0ypzD9d8mGZce5hOHeJXR+xSWweMlQADYT/lmILHwUkShnSxa9em\nS8ZgQNw56yIv1Ug3aBbtxxa/qYHWv11ZZeXA5vvkvXwt4lxmsFxYg8PySpoOqbE4\nXoJ4u5VeNS3EZZ2fHuDraTAkisfJoXRq+nYnNxK5Y+v03/y3/Ywj7UOSvH5b04Mu\nlMiauOstDwlMZWsH+BqN70LWbddffZ3z2t6RdQbgxWHucMA4WB9EN18ynM3IEIM/\nHfiNVM5JwyZfsXULAq1bwCbkvbuuZDzjF0itBi7nZA6H+bdETP9neHUtHHhqW+zP\nlQmQt4qq0JMkDNXQUt69p213iy8lTOnWzMhfLoTcqcJxFjUjujb3DUSOfcmFx8a8\n5sYaXbiSY8PbjWeX8S/dFx94Zwyy1pWj/rWiGVjeUjvxDzdfFyKf8JSSaXltCVvw\nHoKaQFycuB0cuWBo+UxHRI+sUDj0rzdNSR1MxdCJbnWmOJBOoQCZWPcodt9ttSux\nOxSNTU90bUMKRyAI9FevWXR7ZGx1o+VXPG0lGAD638eHIfBfc6RLqiYAyrDLOp8y\n/yRvTCJAFIvSfqPOjXjDX9OI6KhNpsQRpLeEJrTG8DPDLvjTqb+yCimryIe0GpM2\nH8OGu9bQkUzbotQ/c1wPXWRGUgk0wJG5vwNE58y9QpkxZEkToXcE9DZ/SdyXd7YU\nfA1V67Li0AfnBXAS921GHWvwH8di+OEpquy91ftv6Dwq429h2TFAqlPi3QpfGhgP\nQQlmv5y5Wfi9SVqOTa/lz31NyJ2CVZpMdC+8vGZg1YTisss/ARYZtPBlcozYp9Op\nubovrEVugPzmzbTB8FxRuw+6GdCO0502B1k/32bvuV7XUMXltNJR094i0d/VqGeZ\nWxdGMVSYn+32ai9YZeAVvfAisBjAVmG49iNpCAkL4a5s2ONbJ+H7AhhQadDmBoxA\nJA4JNeV19LgSWUxfNE+8IMpoDewa8RQnr68VALZDIZ8TkJvMet2fC2xy86cSNTsJ\nMNbIzVZ8d+ZazE1Ki6jaToX9i4a/fBhftG4Ssi9TlmkpPli+/JgHA6WOY58+ZiZ/\npFht4WjYeayos9hRDwGrXqVJ/pQph5swTZIbvOkIkCtNRiDkHW4VLq8gVI8eNcxg\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/private/localhost.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmFcv7ASwH3nNv\nj+R5OzuK5JD3T/RbtPCJHpBcZxyfjCJ8OoeO9o9yzzJySEeGVznLrxIaMnY0EytF\nDLz6sXr5j1IXfLM/TJ6GrJeZYZEocGB0V/DzvarltsV+nGGsSIRn94zKuB5fkCzn\nhTPYV8VoPSoVCOCUapxHq+vDXl1wogtpijhXBBTIy0SPVjcb+TlHu+vi/vCWd+DV\ng8fznczPCgP+/sM+U7vEsW6CJ2CrHG55QbGyXEYc8GiKDzb969cWmJpOjmKa2ZAi\nWqueOiIFY4zVWTUnOVjDz8c0DG4yNEhppa31WNNB8Vmig+z/9RPu9hVuYkl2qP19\nnW3++HoJAgMBAAECggEALsd246Tp3PGH/AVAafEIDU/hkPcBMY9eLJDiQlR8mGel\nEu59XgQOVV0MrWm0U59f1QHjjGNoLbS0fEdhNw8kPwRiL+Fcr1iKUg5syLPVadl1\nIW/fmxdx+i+nosizLg5i1NHB4xtG2s3GCBPZjXbzUC/iGXidoNACYqGGz0lV8jIN\n8ZRLz4f9TJ+/kO+O0fhUFg3QHXp3kvx7ki9I/0ZPLAzX8NekJqDWP0qYM2cWb/7Y\n/ktbt+3Jzx0xd/cMIv46FHnzWn5xZHYg/CNDThugg/QgA7/gAYw1nePoZEqWKgjA\nrNlAkpeieQ35X6f8eScolJabGwFvXOaWU/lcIcvRAQKBgQDQC68VIbn6gH5gOvA8\nVP+Nmn+F5xZX7/13thmHfvO1r4MngFGvSS2k6ZlUU4PENbyYmcEDt2p6/K/B1rdo\n46rBHe3346qLEZ+8aqqn/N0lpY+XZ2wWfFpcAYj3yVdVVwsvY9y7RqtXljHUGYao\nNZv9Hozyn+LVyUTVL7SuD2Qk4QKBgQDMXh2+LZFDwTvRKu36bFeS7guQlZB19W5g\n/ImetnBV3A5UZJvlF4sDOuL3Bn/cNg90DliHqEtt//dqwiXAgsNE5JIPyvXfZp3X\nSruL9AjkXBowifvhHOU9YpN/4LcksPWpyc3rw5o7jAK5CMT14+KZh2doGXMzZ9ra\n+rAtqW7SKQKBgQC57DcjY2iY/Yvy47kdsbhQC+o6+DF3lPmnqg1WkZWqoJdNDdfy\nFiu8XSaxvZDcIEnS9lUPxTJbhsZrLD/sdMl8nAq2pbcbYTynXYv1ZH0dz79rRvnJ\nfogiAR0uk7iAg7FzQINaueUV+Ru+uLEmUgJ9Sngbu9czxxLEkkWd6BNPAQKBgFvE\nVy6yX2xBNI7z6/Bq/dfoNCE0R63wCyRZwaIW4dJsAbM7ihUQwUcuBgROUtObIs1G\nM9KWa2h1a6/whesvzGb/C+czh7xM48tsr1JkvilsggYRr5yh0P4DaaAeU56SJ32a\nNHENYBAlCoPzwB15uqKd/nzsEKKgm9GAh+O5FR4hAoGAFn0RWrkDrF1J/OqlM5no\nQkKnIzSXFiB3ZIGPbhxomqKTou4qd/AUmO34/Cxlku5PP575nWyOHv1rACC8MYRq\n30O1OSSW/+3gJBohmf24eTFP1ynjnSq8uYUEBZfwANEk/Ap8xCa3UuJmJ2iGpo6L\nda5ZkU53EzkWgjXme3Ls7Pc=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/private/test.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7A5ey5FejXFIl\nouU+QXUPlcDDlPIIbA0xubUsBBNcQsH3ed1K4Yv2iV//6wx7Rk2PK2XZgaP18tRG\ny8CEDVWs3BVRrgBgi6GHUN+4GjR/O2e6AiyPnzkGaJzzNZNa8hahId+HY15AxuoF\nv/P+IRBMeG4RYC+4pAhpGoLLZH8/6q+whcX8Oo9kXDlhEl9zSpqvvnUh9J2+nFkS\nchlZlluSJJVkLd61iqd5kWCNV2N9XYtfgzhUi61JEUgd3dXLowf3pLe2AK5AzzeH\nIlczoFc8128NmOeZTtT/RF2jTnBuPyHRNaPYllEKnGa6wmEXwz/2ido2vIhNUcQe\niCJSuFI7AgMBAAECggEANYnfwf0RAdmKQvARhPMkWiPbHaLkb3jPhi7OKR25lS4f\nbYPb0HYlpZzKeO+HkTsdNSGNdOliUSUFlIb8RFG091nCWLHhtcIDqkOC6KfH46zU\nEzJQI4XHw4frds0dtGmeBN83qQDPmOfZU3ASn+xDSEEp8ZuBzfBX4A8Q5r2MmHVQ\nKnGR9bOsFOMJNRDCjFhgvyW2u2th1OPkW4s3f4bPFaQroRuw9G1LtJTkTFyMe3T1\nUtI2d13kmlam1eMizMA4qT5270zx2xPIvQcNRM+kViA8JlUkfS+JzhzFsZ0YxRFc\nti7EgE4nxv30WGhDitSABwZ4wcZGoTEDhwIVenXiMQKBgQDf2uxms6M+iw+nbNLl\nyT0GGCbii28UeeW0Wm/6n8uL37hrniwVvTSC0knsU9JRqgNdfActImfVS3Ya7p4G\nHPA2wwuPG5G4O//9V2+FhkA1t98gtDGPB0npxUsMTD0dcd0I2HOQAhePY4YMKixi\naJkyYG/wPn5b9kcE+n8NWOm5JQKBgQDV3lzP3IfmfsP8hFzX/Orvf7wrSGsaanxI\nN1F2ekk73X66005bkOgxdCiF+Q0YRAl5dJZqMf+onQozQ0Cs1e0rZ1BERxgHdM8s\n262MVlZ7cMSk0isGj9XwzFhlXXhH9XMvABQncbUIlsWkL6V38UkRtqaCyS6j370L\nYvFAd6Vv3wKBgBvgJioP2pcyN+vQaF7G9XtpzIXPeTCikVEpJeKevXkYjd2Q38qB\nOiXETAJK2djjg/HDPR2IuEdeU0G9Tx5RwjP/i9PxSe8YQaNpnPCSrDJESFvJNJas\nAK9EqzjH0aB2CmFMmu9m40ouyYWARvCmN9WHHsla7t9Cxss+6k1eMhSlAoGABx7e\nLDWFeNEjqVam2LIdCFhSZYaFul1tQeJFnhI4OfwH6iR66WWLtYnOh5dhLjulrRuK\nnoKHpo+D9Wz5zEdbHCTWcU+Ep0rmUvIFdzSwsG9yvKIauJ81Wk0TbFyOUqcDbL8t\n7JpGksCiV/MWKUYpTqOsK5KTMPWUD7r9mU3ifjUCgYEAxMA0ydm630LJYmBUiNG8\nuyicDxxNXnCACV+ylS4xEH6hrHnR9pDmxXK7RV8B9kHYniyg6qRHs1mu5xW0ekvS\nHrwV6h7VdhAtW95tUjigRAg952z47lMs7y0Nzn85eWP8EEh0wH2z+yCvJY/cP94j\nclwVk8wIS8Bzce1f6lsmV4s=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/reqs/localhost.req",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAphXL+wEsB95zb4/keTs7iuSQ90/0W7TwiR6QXGcc\nn4wifDqHjvaPcs8yckhHhlc5y68SGjJ2NBMrRQy8+rF6+Y9SF3yzP0yehqyXmWGR\nKHBgdFfw872q5bbFfpxhrEiEZ/eMyrgeX5As54Uz2FfFaD0qFQjglGqcR6vrw15d\ncKILaYo4VwQUyMtEj1Y3G/k5R7vr4v7wlnfg1YPH853MzwoD/v7DPlO7xLFugidg\nqxxueUGxslxGHPBoig82/evXFpiaTo5imtmQIlqrnjoiBWOM1Vk1JzlYw8/HNAxu\nMjRIaaWt9VjTQfFZooPs//UT7vYVbmJJdqj9fZ1t/vh6CQIDAQABoAAwDQYJKoZI\nhvcNAQELBQADggEBAE2omeTAH8e2r8WU1gRkIjPb3cS+NWujtn133/M0wshgm3wz\nf91u6qYLAERkPyus/6sizy+rHlSqFIELyyPNizx1QaiV+/0B9KSP7HdK/5lMQcxS\ngxiAh13nQg2/pIdgreSpVrvuorBGnYxrsWQ1a5LI7dUSEwEBj2gRNj0MeJGhiacA\n+NRGCrU5FvvWFgPNUwaZGKomsYopqCSUar3iI7yF/1wl8ZPjVQeBentKqJVPqxWk\nS55d0YlY/Kkut5wCOyDMFPwDaMUquW3+os4FIRNMyAR4y1FzTICJVlK/8cR9v/Fq\nQlAtiDuEgmV37cnmJB9Q8+yjVH61DAOCT3ka8PU=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/reqs/test.req",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICVDCCATwCAQAwDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALsDl7LkV6NcUiWi5T5BdQ+VwMOU8ghsDTG5tSwEE1xCwfd5\n3Urhi/aJX//rDHtGTY8rZdmBo/Xy1EbLwIQNVazcFVGuAGCLoYdQ37gaNH87Z7oC\nLI+fOQZonPM1k1ryFqEh34djXkDG6gW/8/4hEEx4bhFgL7ikCGkagstkfz/qr7CF\nxfw6j2RcOWESX3NKmq++dSH0nb6cWRJyGVmWW5IklWQt3rWKp3mRYI1XY31di1+D\nOFSLrUkRSB3d1cujB/ekt7YArkDPN4ciVzOgVzzXbw2Y55lO1P9EXaNOcG4/IdE1\no9iWUQqcZrrCYRfDP/aJ2ja8iE1RxB6IIlK4UjsCAwEAAaAAMA0GCSqGSIb3DQEB\nCwUAA4IBAQByw6bvSYbDfUDxNhgmK3VeTK361zggwP6Fyt+u709cLZeSPvlFjtKo\nbj2pFjVGmxM0033oT7QFrw1vEdss9ZA6rLTCeQwGIij78SeytNtBOJr8B/0E7QF2\nOmkbTvhjU1xgAS7lVaXKNNa1hx1NeSMBu7URRx2VtkWZXEWpNM1WKC0Wk3tAlE1N\nsF2K887NjPQvD33mDpVuOiDxLcwQh4Mh7a1tn+JWOQOlnxprjmYLgWYUjXWeb0QC\nw5t7gRPb88vKZJQR9qezFTnMJodqaYvHLlNYcdXol4golbxxKricyxlhl7fUrZ2o\nqeG1lo11sE3D4j4TEZmFqvPeOvmlMc/l\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/safessl-easyrsa.cnf",
    "content": "# For use with Easy-RSA 3.0+ and OpenSSL or LibreSSL\n\n####################################################################\n[ ca ]\ndefault_ca\t= CA_default\t\t# The default ca section\n\n####################################################################\n[ CA_default ]\n\ndir\t\t= /etc/openvpn/pki\t# Where everything is kept\ncerts\t\t= /etc/openvpn/pki\t\t\t# Where the issued certs are kept\ncrl_dir\t\t= /etc/openvpn/pki\t\t\t# Where the issued crl are kept\ndatabase\t= /etc/openvpn/pki/index.txt\t# database index file.\nnew_certs_dir\t= /etc/openvpn/pki/certs_by_serial\t# default place for new certs.\n\ncertificate\t= /etc/openvpn/pki/ca.crt\t \t# The CA certificate\nserial\t\t= /etc/openvpn/pki/serial \t\t# The current serial number\ncrl\t\t= /etc/openvpn/pki/crl.pem \t\t# The current CRL\nprivate_key\t= /etc/openvpn/pki/private/ca.key\t# The private key\nRANDFILE\t= /etc/openvpn/pki/.rand\t\t# private random number file\n\nx509_extensions\t= basic_exts\t\t# The extensions to add to the cert\n\n# This allows a V2 CRL. Ancient browsers don't like it, but anything Easy-RSA\n# is designed for will. In return, we get the Issuer attached to CRLs.\ncrl_extensions\t= crl_ext\n\ndefault_days\t= 825\t# how long to certify for\ndefault_crl_days= 3650\t# how long before next CRL\ndefault_md\t= sha256\t\t# use public key default MD\npreserve\t= no\t\t\t# keep passed DN ordering\n\n# This allows to renew certificates which have not been revoked\nunique_subject\t= no\n\n# A few different ways of specifying how similar the request should look\n# For type CA, the listed attributes must be the same, and the optional\n# and supplied fields are just that :-)\npolicy\t\t= policy_anything\n\n# For the 'anything' policy, which defines allowed DN fields\n[ policy_anything ]\ncountryName\t\t= optional\nstateOrProvinceName\t= optional\nlocalityName\t\t= optional\norganizationName\t= optional\norganizationalUnitName\t= optional\ncommonName\t\t= supplied\nname\t\t\t= optional\nemailAddress\t\t= optional\n\n####################################################################\n# Easy-RSA request handling\n# We key off $DN_MODE to determine how to format the DN\n[ req ]\ndefault_bits\t\t= 2048\ndefault_keyfile \t= privkey.pem\ndefault_md\t\t= sha256\ndistinguished_name\t= cn_only\nx509_extensions\t\t= easyrsa_ca\t# The extensions to add to the self signed cert\n\n# A placeholder to handle the $EXTRA_EXTS feature:\n#%EXTRA_EXTS%\t# Do NOT remove or change this line as $EXTRA_EXTS support requires it\n\n####################################################################\n# Easy-RSA DN (Subject) handling\n\n# Easy-RSA DN for cn_only support:\n[ cn_only ]\ncommonName\t\t= Common Name (eg: your user, host, or server name)\ncommonName_max\t\t= 64\ncommonName_default\t= ChangeMe\n\n# Easy-RSA DN for org support:\n[ org ]\ncountryName\t\t\t= Country Name (2 letter code)\ncountryName_default\t\t= US\ncountryName_min\t\t\t= 2\ncountryName_max\t\t\t= 2\n\nstateOrProvinceName\t\t= State or Province Name (full name)\nstateOrProvinceName_default\t= California\n\nlocalityName\t\t\t= Locality Name (eg, city)\nlocalityName_default\t\t= San Francisco\n\n0.organizationName\t\t= Organization Name (eg, company)\n0.organizationName_default\t= Copyleft Certificate Co\n\norganizationalUnitName\t\t= Organizational Unit Name (eg, section)\norganizationalUnitName_default\t= My Organizational Unit\n\ncommonName\t\t\t= Common Name (eg: your user, host, or server name)\ncommonName_max\t\t\t= 64\ncommonName_default\t\t= ChangeMe\n\nemailAddress\t\t\t= Email Address\nemailAddress_default\t\t= me@example.net\nemailAddress_max\t\t= 64\n\n####################################################################\n# Easy-RSA cert extension handling\n\n# This section is effectively unused as the main script sets extensions\n# dynamically. This core section is left to support the odd usecase where\n# a user calls openssl directly.\n[ basic_exts ]\nbasicConstraints\t= CA:FALSE\nsubjectKeyIdentifier\t= hash\nauthorityKeyIdentifier\t= keyid,issuer:always\n\n# The Easy-RSA CA extensions\n[ easyrsa_ca ]\n\n# PKIX recommendations:\n\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always,issuer:always\n\n# This could be marked critical, but it's nice to support reading by any\n# broken clients who attempt to do so.\nbasicConstraints = CA:true\n\n# Limit key usage to CA tasks. If you really want to use the generated pair as\n# a self-signed cert, comment this out.\nkeyUsage = cRLSign, keyCertSign\n\n# nsCertType omitted by default. Let's try to let the deprecated stuff die.\n# nsCertType = sslCA\n\n# CRL extensions.\n[ crl_ext ]\n\n# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.\n\n# issuerAltName=issuer:copy\nauthorityKeyIdentifier=keyid:always,issuer:always\n\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/serial",
    "content": "34AAB5929E45F5A821E772DBBB0F2569\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/serial.old",
    "content": "34aab5929e45f5a821e772dbbb0f2568\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/openvpn-data/conf/pki/ta.key",
    "content": "#\n# 2048 bit OpenVPN static key\n#\n-----BEGIN OpenVPN Static key V1-----\n456226c4d5a6895c48dad7fd5d36ee57\n3eb280683fbbfe1699d63e9fd4e5ec5b\n70500489c3ec36e0c30d6b18f9c48b6b\naede839a99d492fd26beb51317c08eb2\nebb320a0b980da0b13a88e37559594f5\n03b21fa6d72548f7be5fdb41ad1de315\n82373a95c5c503c1101236f43a59ec68\nddc9a83d4b4a4437f2db9e16bcbd433a\n5211d060bc8376f1efe99bbf2413e543\n4e4473d5028c95f33ad5df3637505c31\nbb7661b03e7d882c3ec1c5ca5f9c2277\n09e2e4323392efb0dff0abadbe6d6887\n27bddf4a2f7f795fe7c227813f76cac2\n9e919074c638ad36e5001a187d113c4b\n3faab93dde06734c15a198ad686a315a\n3e1f91612528d4f6c4281916625e54b1\n-----END OpenVPN Static key V1-----\n"
  },
  {
    "path": "tests/e2e/rte/openvpn/test.ovpn",
    "content": "\nclient\nnobind\ndev tun\nremote-cert-tls server\n\nremote localhost 1194 udp\n\n<key>\n-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7A5ey5FejXFIl\nouU+QXUPlcDDlPIIbA0xubUsBBNcQsH3ed1K4Yv2iV//6wx7Rk2PK2XZgaP18tRG\ny8CEDVWs3BVRrgBgi6GHUN+4GjR/O2e6AiyPnzkGaJzzNZNa8hahId+HY15AxuoF\nv/P+IRBMeG4RYC+4pAhpGoLLZH8/6q+whcX8Oo9kXDlhEl9zSpqvvnUh9J2+nFkS\nchlZlluSJJVkLd61iqd5kWCNV2N9XYtfgzhUi61JEUgd3dXLowf3pLe2AK5AzzeH\nIlczoFc8128NmOeZTtT/RF2jTnBuPyHRNaPYllEKnGa6wmEXwz/2ido2vIhNUcQe\niCJSuFI7AgMBAAECggEANYnfwf0RAdmKQvARhPMkWiPbHaLkb3jPhi7OKR25lS4f\nbYPb0HYlpZzKeO+HkTsdNSGNdOliUSUFlIb8RFG091nCWLHhtcIDqkOC6KfH46zU\nEzJQI4XHw4frds0dtGmeBN83qQDPmOfZU3ASn+xDSEEp8ZuBzfBX4A8Q5r2MmHVQ\nKnGR9bOsFOMJNRDCjFhgvyW2u2th1OPkW4s3f4bPFaQroRuw9G1LtJTkTFyMe3T1\nUtI2d13kmlam1eMizMA4qT5270zx2xPIvQcNRM+kViA8JlUkfS+JzhzFsZ0YxRFc\nti7EgE4nxv30WGhDitSABwZ4wcZGoTEDhwIVenXiMQKBgQDf2uxms6M+iw+nbNLl\nyT0GGCbii28UeeW0Wm/6n8uL37hrniwVvTSC0knsU9JRqgNdfActImfVS3Ya7p4G\nHPA2wwuPG5G4O//9V2+FhkA1t98gtDGPB0npxUsMTD0dcd0I2HOQAhePY4YMKixi\naJkyYG/wPn5b9kcE+n8NWOm5JQKBgQDV3lzP3IfmfsP8hFzX/Orvf7wrSGsaanxI\nN1F2ekk73X66005bkOgxdCiF+Q0YRAl5dJZqMf+onQozQ0Cs1e0rZ1BERxgHdM8s\n262MVlZ7cMSk0isGj9XwzFhlXXhH9XMvABQncbUIlsWkL6V38UkRtqaCyS6j370L\nYvFAd6Vv3wKBgBvgJioP2pcyN+vQaF7G9XtpzIXPeTCikVEpJeKevXkYjd2Q38qB\nOiXETAJK2djjg/HDPR2IuEdeU0G9Tx5RwjP/i9PxSe8YQaNpnPCSrDJESFvJNJas\nAK9EqzjH0aB2CmFMmu9m40ouyYWARvCmN9WHHsla7t9Cxss+6k1eMhSlAoGABx7e\nLDWFeNEjqVam2LIdCFhSZYaFul1tQeJFnhI4OfwH6iR66WWLtYnOh5dhLjulrRuK\nnoKHpo+D9Wz5zEdbHCTWcU+Ep0rmUvIFdzSwsG9yvKIauJ81Wk0TbFyOUqcDbL8t\n7JpGksCiV/MWKUYpTqOsK5KTMPWUD7r9mU3ifjUCgYEAxMA0ydm630LJYmBUiNG8\nuyicDxxNXnCACV+ylS4xEH6hrHnR9pDmxXK7RV8B9kHYniyg6qRHs1mu5xW0ekvS\nHrwV6h7VdhAtW95tUjigRAg952z47lMs7y0Nzn85eWP8EEh0wH2z+yCvJY/cP94j\nclwVk8wIS8Bzce1f6lsmV4s=\n-----END PRIVATE KEY-----\n</key>\n<cert>\n-----BEGIN CERTIFICATE-----\nMIIDRDCCAiygAwIBAgIQNKq1kp5F9agh53Lbuw8laDANBgkqhkiG9w0BAQsFADAP\nMQ0wCwYDVQQDDAR0ZXN0MB4XDTI1MDkwMzE2MzQ1MVoXDTI3MTIwNzE2MzQ1MVow\nDzENMAsGA1UEAwwEdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nALsDl7LkV6NcUiWi5T5BdQ+VwMOU8ghsDTG5tSwEE1xCwfd53Urhi/aJX//rDHtG\nTY8rZdmBo/Xy1EbLwIQNVazcFVGuAGCLoYdQ37gaNH87Z7oCLI+fOQZonPM1k1ry\nFqEh34djXkDG6gW/8/4hEEx4bhFgL7ikCGkagstkfz/qr7CFxfw6j2RcOWESX3NK\nmq++dSH0nb6cWRJyGVmWW5IklWQt3rWKp3mRYI1XY31di1+DOFSLrUkRSB3d1cuj\nB/ekt7YArkDPN4ciVzOgVzzXbw2Y55lO1P9EXaNOcG4/IdE1o9iWUQqcZrrCYRfD\nP/aJ2ja8iE1RxB6IIlK4UjsCAwEAAaOBmzCBmDAJBgNVHRMEAjAAMB0GA1UdDgQW\nBBTOGRlSr2xGldnu1m5fw0NkKAuvcjBKBgNVHSMEQzBBgBQ/AarQse++Argn1cHx\nvXoG8W+Pc6ETpBEwDzENMAsGA1UEAwwEdGVzdIIUcqHj2uS8cK9SYQiJmoIR/7fU\n0PowEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQDAgeAMA0GCSqGSIb3DQEB\nCwUAA4IBAQBbQ+LJzFooWhU1CA6bq6W/2warpGEgfQ+bZToSTi60SY0+c/mUDmeq\nuHKI3BAT+1zrDtU5nZM5AYtOhycXlVdiOvQxBJxsTOziSaVd7Jh71Ouf2WMiSUDf\nu3ju/iQ+8a6cc+EoQsQk31pGAWS6dkLDxhnBfOgNpktecKSZMOretEs4kCk7DY5d\n7zSH4XVjt3cs4eoz2CSdpfMz4QfGSEa9eKoey8fm7ZQb5tziiWROGV2DLUe4pdY7\nGF7ibkHyxM65iYmwNtzDzn6j2cbayNM5pedYmilXfs8n8VXolMp/yWcX8t9a2LJ8\n375B2ZTi9L9lebw5u3PrXILWA/DPpeUg\n-----END CERTIFICATE-----\n</cert>\n<ca>\n-----BEGIN CERTIFICATE-----\nMIIDNjCCAh6gAwIBAgIUcqHj2uS8cK9SYQiJmoIR/7fU0PowDQYJKoZIhvcNAQEL\nBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0yMTExMDgwNzQ5MTBaFw0zMTExMDYwNzQ5\nMTBaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQCibt8kh9lqTC0O631rPHN0kMQ4kMQ/eZ59mKhAJZ3rBchIBrQne2yTw2z+\nX1ESa3VTkW2jyJ5r7iuo+Xyc8246tfBwO3u0DJ2DeZZOYPzMg48nJNxs3ur3iXAT\nr6Aiwp0gtMNC2XcW7y5OPl8l+BhSt2PsWcdEdmLJgvRPJ2x+Ea8wivuw6FO6byK7\nMxw7/CbNMw8Eey9eSz9kWDrgetS0kOgfqtt1ZnKDZkbLy8jFl0xW488VUrefUR1g\nlOje8QySjDvzT8sUR0lASyS+/J6j/3gLlSS42e4SxMz00jEus+ye56cO16Pc+vKI\nXsev8cRPiSDTZTvc7Eaq/OcKVl11AgMBAAGjgYkwgYYwHQYDVR0OBBYEFD8BqtCx\n774CuCfVwfG9egbxb49zMEoGA1UdIwRDMEGAFD8BqtCx774CuCfVwfG9egbxb49z\noROkETAPMQ0wCwYDVQQDDAR0ZXN0ghRyoePa5Lxwr1JhCImaghH/t9TQ+jAMBgNV\nHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAXUrDhVAX\nTkbhKRBuhUGQb03RyACQKBFM/SwhrmwpQMXo7BUuqWJ27U5/TRHrfKJxDgppmwIs\nqmtrT07tA7e/OyFSZtZ9p/4H+5xM9FCsmu6YMQ3ZloHHGWmibrDNK70frVgRAEAS\nFyAsEgpKZCr6OJNd7v2dbvO4AniZVVvccU17cJAx177YC3fNIuRtpHkm93D3qI+1\n4SED7rktVfXUKs6RMFmqIum5WRzgiJBAtk2GVQMrAAu/xmUPS/aqzstNte4KQ+UY\n2qI9v1wYM8j+BT5nsBT02K+zOsYdkG39n7QEfcecPAjOkKsaFbSf/WZcsb6oCVgl\nd/Nz24kfh76SqQ==\n-----END CERTIFICATE-----\n</ca>\nkey-direction 1\n<tls-auth>\n#\n# 2048 bit OpenVPN static key\n#\n-----BEGIN OpenVPN Static key V1-----\n456226c4d5a6895c48dad7fd5d36ee57\n3eb280683fbbfe1699d63e9fd4e5ec5b\n70500489c3ec36e0c30d6b18f9c48b6b\naede839a99d492fd26beb51317c08eb2\nebb320a0b980da0b13a88e37559594f5\n03b21fa6d72548f7be5fdb41ad1de315\n82373a95c5c503c1101236f43a59ec68\nddc9a83d4b4a4437f2db9e16bcbd433a\n5211d060bc8376f1efe99bbf2413e543\n4e4473d5028c95f33ad5df3637505c31\nbb7661b03e7d882c3ec1c5ca5f9c2277\n09e2e4323392efb0dff0abadbe6d6887\n27bddf4a2f7f795fe7c227813f76cac2\n9e919074c638ad36e5001a187d113c4b\n3faab93dde06734c15a198ad686a315a\n3e1f91612528d4f6c4281916625e54b1\n-----END OpenVPN Static key V1-----\n</tls-auth>\n\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7/Dockerfile",
    "content": "FROM --platform=linux/amd64 redislabs/rejson:1.0.8 AS rejson\n\nFROM --platform=linux/amd64 redislabs/redisearch:2.0.13 AS redisearch\n\nFROM --platform=linux/amd64 redis:7.0.0\n\nCOPY redis.conf /etc/redis/\nCOPY --from=rejson /usr/lib/redis/modules/rejson.so /etc/redis/modules/\nCOPY --from=redisearch /usr/lib/redis/modules/redisearch.so /etc/redis/modules/\n\nCMD [ \"redis-server\", \"/etc/redis/redis.conf\" ]\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7/cluster-create.sh",
    "content": "#!/bin/sh\n\necho 'Try to sleep for a while...'\nsleep 25\necho 'Creating cluster...'\necho \"yes\" | redis-cli \\\n  --cluster create \\\n  172.31.100.211:6379 \\\n  172.31.100.212:6379 \\\n  172.31.100.213:6379 \\\n  --cluster-replicas 0 \\\n  && redis-server\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7/creator.Dockerfile",
    "content": "FROM --platform=linux/amd64 redis:7.0.0\n\nUSER root\n\nCOPY cluster-create.sh ./\n\nRUN chmod a+x cluster-create.sh\n\nCMD [\"/bin/sh\", \"./cluster-create.sh\"]\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Note that option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\nloadmodule /etc/redis/modules/rejson.so\nloadmodule /etc/redis/modules/redisearch.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all available network interfaces on the host machine.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only on the\n# IPv4 loopback interface address (this means Redis will only be able to\n# accept client connections from the same host that it is running on).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT OUT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# bind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\n# protected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need a high backlog in order\n# to avoid slow clients connection issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Force network equipment in the middle to consider the connection to be\n#    alive.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# TLS/SSL #####################################\n\n# By default, TLS/SSL is disabled. To enable it, the \"tls-port\" configuration\n# directive can be used to define TLS-listening ports. To enable TLS on the\n# default port, use:\n#\n# port 0\n# tls-port 6379\n\n# Configure a X.509 certificate and private key to use for authenticating the\n# server to connected clients, masters or cluster peers.  These files should be\n# PEM formatted.\n#\n# tls-cert-file /etc/redis/redis.crt\n# tls-key-file /etc/redis/redis.key\n\n# Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange:\n#\n# tls-dh-params-file redis.dh\n\n# Configure a CA certificate(s) bundle or directory to authenticate TLS/SSL\n# clients and peers.  Redis requires an explicit configuration of at least one\n# of these, and will not implicitly use the system wide configuration.\n#\n# tls-ca-cert-file /etc/redis/ca.crt\n# tls-ca-cert-dir /etc/ssl/certs\n\n# By default, clients (including replica servers) on a TLS port are required\n# to authenticate using valid client side certificates.\n#\n# If \"no\" is specified, client certificates are not required and not accepted.\n# If \"optional\" is specified, client certificates are accepted and must be\n# valid if provided, but are not required.\n#\n# tls-auth-clients yes\n# tls-auth-clients optional\n\n# By default, a Redis replica does not attempt to establish a TLS connection\n# with its master.\n#\n# Use the following directive to enable TLS on replication links.\n#\n# tls-replication yes\n\n# By default, the Redis Cluster bus uses a plain TCP connection. To enable\n# TLS for the bus protocol, use the following directive:\n#\n# tls-cluster yes\n\n# Explicitly specify TLS versions to support. Allowed values are case insensitive\n# and include \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\" (OpenSSL >= 1.1.1) or\n# any combination. To enable only TLSv1.2 and TLSv1.3, use:\n#\n# tls-protocols \"TLSv1.2 TLSv1.3\"\n\n# Configure allowed ciphers.  See the ciphers(1ssl) manpage for more information\n# about the syntax of this string.\n#\n# Note: this configuration applies only to <= TLSv1.2.\n#\n# tls-ciphers DEFAULT:!MEDIUM\n\n# Configure allowed TLSv1.3 ciphersuites.  See the ciphers(1ssl) manpage for more\n# information about the syntax of this string, and specifically for TLSv1.3\n# ciphersuites.\n#\n# tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256\n\n# When choosing a cipher, use the server's preference instead of the client\n# preference. By default, the server follows the client's preference.\n#\n# tls-prefer-server-ciphers yes\n\n# By default, TLS session caching is enabled to allow faster and less expensive\n# reconnections by clients that support it. Use the following directive to disable\n# caching.\n#\n# tls-session-caching no\n\n# Change the default number of TLS sessions cached. A zero value sets the cache\n# to unlimited size. The default size is 20480.\n#\n# tls-session-cache-size 5000\n\n# Change the default timeout of cached TLS sessions. The default timeout is 300\n# seconds.\n#\n# tls-session-cache-timeout 60\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#                        requires \"expect stop\" in your upstart job config\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile /var/run/redis_6379.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\nlogfile \"\"\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behavior will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# By default compression is enabled as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# Remove RDB files used by replication in instances without persistence\n# enabled. By default this option is disabled, however there are environments\n# where for regulations or other security concerns, RDB files persisted on\n# disk by masters in order to feed replicas, or stored on disk by replicas\n# in order to load them for the initial synchronization, should be deleted\n# ASAP. Note that this option ONLY WORKS in instances that have both AOF\n# and RDB persistence disabled, otherwise is completely ignored.\n#\n# An alternative (and sometimes better) way to obtain the same effect is\n# to use diskless replication on both master and replicas instances. However\n# in the case of replicas, diskless is not always an option.\n# in the case of replicas, diskless is not always an option.\nrdb-del-sync-files no\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir ./\n\n################################# REPLICATION #################################\n\n# Master-Replica replication. Use replicaof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n#   +------------------+      +---------------+\n#   |      Master      | ---> |    Replica    |\n#   | (receive writes) |      |  (exact copy) |\n#   +------------------+      +---------------+\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of replicas.\n# 2) Redis replicas are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition replicas automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# replicaof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the replica to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the replica request.\n#\n# masterauth defaultpass\n#\n# However this is not enough if you are using Redis ACLs (for Redis version\n# 6 or greater), and the default user is not capable of running the PSYNC\n# command and/or other commands needed for replication. In this case it's\n# better to configure a special user to use with replication, and specify the\n# masteruser configuration as such:\n#\n# masteruser <username>\n#\n# When masteruser is specified, the replica will authenticate against its\n# master using the new AUTH form: AUTH <username> <password>.\n\n# When a replica loses its connection with the master, or when the replication\n# is still in progress, the replica can act in two different ways:\n#\n# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) If replica-serve-stale-data is set to 'no' the replica will reply with\n#    an error \"SYNC with master in progress\" to all commands except:\n#    INFO, REPLICAOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG, SUBSCRIBE,\n#    UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB, COMMAND, POST,\n#    HOST and LATENCY.\n#\nreplica-serve-stale-data yes\n\n# You can configure a replica instance to accept writes or not. Writing against\n# a replica instance may be useful to store some ephemeral data (because data\n# written on a replica will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default replicas are read-only.\n#\n# Note: read only replicas are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only replica exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only replicas using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nreplica-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# New replicas and reconnecting replicas that are not able to continue the\n# replication process just receiving differences, need to do what is called a\n# \"full synchronization\". An RDB file is transmitted from the master to the\n# replicas.\n#\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the replicas incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to replica sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more replicas\n# can be queued and served with the RDB file as soon as the current child\n# producing the RDB file finishes its work. With diskless replication instead\n# once the transfer starts, new replicas arriving will be queued and a new\n# transfer will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple\n# replicas will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the replicas.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new replicas arriving, that will be queued for the next RDB transfer, so the\n# server waits a delay in order to let more replicas arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# -----------------------------------------------------------------------------\n# WARNING: RDB diskless load is experimental. Since in this setup the replica\n# does not immediately store an RDB on disk, it may cause data loss during\n# failovers. RDB diskless load + Redis modules not handling I/O reads may also\n# cause Redis to abort in case of I/O errors during the initial synchronization\n# stage with the master. Use only if your do what you are doing.\n# -----------------------------------------------------------------------------\n#\n# Replica can load the RDB it reads from the replication link directly from the\n# socket, or store the RDB to a file and read that file after it was completely\n# received from the master.\n#\n# In many cases the disk is slower than the network, and storing and loading\n# the RDB file may increase replication time (and even increase the master's\n# Copy on Write memory and salve buffers).\n# However, parsing the RDB file directly from the socket may mean that we have\n# to flush the contents of the current database before the full rdb was\n# received. For this reason we have the following options:\n#\n# \"disabled\"    - Don't use diskless load (store the rdb file to the disk first)\n# \"on-empty-db\" - Use diskless load only when it is completely safe.\n# \"swapdb\"      - Keep a copy of the current db contents in RAM while parsing\n#                 the data directly from the socket. note that this requires\n#                 sufficient memory, if you don't have it, you risk an OOM kill.\nrepl-diskless-load disabled\n\n# Replicas send PINGs to server in a predefined interval. It's possible to\n# change this interval with the repl_ping_replica_period option. The default\n# value is 10 seconds.\n#\n# repl-ping-replica-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of replica.\n# 2) Master timeout from the point of view of replicas (data, pings).\n# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-replica-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the replica. The default\n# value is 60 seconds.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the replica socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to replicas. But this can add a delay for\n# the data to appear on the replica side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the replica side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and replicas are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# replica data when replicas are disconnected for some time, so that when a\n# replica wants to reconnect again, often a full resync is not needed, but a\n# partial resync is enough, just passing the portion of data the replica\n# missed while disconnected.\n#\n# The bigger the replication backlog, the longer the replica can endure the\n# disconnect and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated if there is at least one replica connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no connected replicas for some time, the backlog will be\n# freed. The following option configures the amount of seconds that need to\n# elapse, starting from the time the last replica disconnected, for the backlog\n# buffer to be freed.\n#\n# Note that replicas never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with other replicas: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The replica priority is an integer number published by Redis in the INFO\n# output. It is used by Redis Sentinel in order to select a replica to promote\n# into a master if the master is no longer working correctly.\n#\n# A replica with a low priority number is considered better for promotion, so\n# for instance if there are three replicas with priority 10, 100, 25 Sentinel\n# will pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the replica as not able to perform the\n# role of master, so a replica with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nreplica-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N replicas connected, having a lag less or equal than M seconds.\n#\n# The N replicas need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the replica, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough replicas\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 replicas with a lag <= 10 seconds use:\n#\n# min-replicas-to-write 3\n# min-replicas-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-replicas-to-write is set to 0 (feature disabled) and\n# min-replicas-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# replicas in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover replica instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP address and port normally reported by a replica is\n# obtained in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the replica to connect with the master.\n#\n#   Port: The port is communicated by the replica during the replication\n#   handshake, and is normally the port that the replica is using to\n#   listen for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the replica may actually be reachable via different IP and port\n# pairs. The following two options can be used by a replica in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# replica-announce-ip 5.5.5.5\n# replica-announce-port 1234\n\n############################### KEYS TRACKING #################################\n\n# Redis implements server assisted support for client side caching of values.\n# This is implemented using an invalidation table that remembers, using\n# 16 millions of slots, what clients may have certain subsets of keys. In turn\n# this is used in order to send invalidation messages to clients. Please\n# check this page to understand more about the feature:\n#\n#   https://redis.io/topics/client-side-caching\n#\n# When tracking is enabled for a client, all the read only queries are assumed\n# to be cached: this will force Redis to store information in the invalidation\n# table. When keys are modified, such information is flushed away, and\n# invalidation messages are sent to the clients. However if the workload is\n# heavily dominated by reads, Redis could use more and more memory in order\n# to track the keys fetched by many clients.\n#\n# For this reason it is possible to configure a maximum fill value for the\n# invalidation table. By default it is set to 1M of keys, and once this limit\n# is reached, Redis will start to evict keys in the invalidation table\n# even if they were not modified, just to reclaim memory: this will in turn\n# force the clients to invalidate the cached values. Basically the table\n# maximum size is a trade off between the memory you want to spend server\n# side to track information about who cached what, and the ability of clients\n# to retain cached objects in memory.\n#\n# If you set the value to 0, it means there are no limits, and Redis will\n# retain as many keys as needed in the invalidation table.\n# In the \"stats\" INFO section, you can find information about the number of\n# keys in the invalidation table at every given moment.\n#\n# Note: when key tracking is used in broadcasting mode, no memory is used\n# in the server side so this setting is useless.\n#\n# tracking-table-max-keys 1000000\n\n################################## SECURITY ###################################\n\n# Warning: since Redis is pretty fast, an outside user can try up to\n# 1 million passwords per second against a modern box. This means that you\n# should use very strong passwords, otherwise they will be very easy to break.\n# Note that because the password is really a shared secret between the client\n# and the server, and should not be memorized by any human, the password\n# can be easily a long string from /dev/urandom or whatever, so by using a\n# long and unguessable password no brute force attack will be possible.\n\n# Redis ACL users are defined in the following format:\n#\n#   user <username> ... acl rules ...\n#\n# For example:\n#\n#   user worker +@list +@connection ~jobs:* on >ffa9203c493aa99\n#\n# The special username \"default\" is used for new connections. If this user\n# has the \"nopass\" rule, then new connections will be immediately authenticated\n# as the \"default\" user without the need of any password provided via the\n# AUTH command. Otherwise if the \"default\" user is not flagged with \"nopass\"\n# the connections will start in not authenticated state, and will require\n# AUTH (or the HELLO command AUTH option) in order to be authenticated and\n# start to work.\n#\n# The ACL rules that describe what a user can do are the following:\n#\n#  on           Enable the user: it is possible to authenticate as this user.\n#  off          Disable the user: it's no longer possible to authenticate\n#               with this user, however the already authenticated connections\n#               will still work.\n#  +<command>   Allow the execution of that command\n#  -<command>   Disallow the execution of that command\n#  +@<category> Allow the execution of all the commands in such category\n#               with valid categories are like @admin, @set, @sortedset, ...\n#               and so forth, see the full list in the server.c file where\n#               the Redis command table is described and defined.\n#               The special category @all means all the commands, but currently\n#               present in the server, and that will be loaded in the future\n#               via modules.\n#  +<command>|subcommand    Allow a specific subcommand of an otherwise\n#                           disabled command. Note that this form is not\n#                           allowed as negative like -DEBUG|SEGFAULT, but\n#                           only additive starting with \"+\".\n#  allcommands  Alias for +@all. Note that it implies the ability to execute\n#               all the future commands loaded via the modules system.\n#  nocommands   Alias for -@all.\n#  ~<pattern>   Add a pattern of keys that can be mentioned as part of\n#               commands. For instance ~* allows all the keys. The pattern\n#               is a glob-style pattern like the one of KEYS.\n#               It is possible to specify multiple patterns.\n#  allkeys      Alias for ~*\n#  resetkeys    Flush the list of allowed keys patterns.\n#  ><password>  Add this password to the list of valid password for the user.\n#               For example >mypass will add \"mypass\" to the list.\n#               This directive clears the \"nopass\" flag (see later).\n#  <<password>  Remove this password from the list of valid passwords.\n#  nopass       All the set passwords of the user are removed, and the user\n#               is flagged as requiring no password: it means that every\n#               password will work against this user. If this directive is\n#               used for the default user, every new connection will be\n#               immediately authenticated with the default user without\n#               any explicit AUTH command required. Note that the \"resetpass\"\n#               directive will clear this condition.\n#  resetpass    Flush the list of allowed passwords. Moreover removes the\n#               \"nopass\" status. After \"resetpass\" the user has no associated\n#               passwords and there is no way to authenticate without adding\n#               some password (or setting it as \"nopass\" later).\n#  reset        Performs the following actions: resetpass, resetkeys, off,\n#               -@all. The user returns to the same state it has immediately\n#               after its creation.\n#\n# ACL rules can be specified in any order: for instance you can start with\n# passwords, then flags, or key patterns. However note that the additive\n# and subtractive rules will CHANGE MEANING depending on the ordering.\n# For instance see the following example:\n#\n#   user alice on +@all -DEBUG ~* >somepassword\n#\n# This will allow \"alice\" to use all the commands with the exception of the\n# DEBUG command, since +@all added all the commands to the set of the commands\n# alice can use, and later DEBUG was removed. However if we invert the order\n# of two ACL rules the result will be different:\n#\n#   user alice on -DEBUG +@all ~* >somepassword\n#\n# Now DEBUG was removed when alice had yet no commands in the set of allowed\n# commands, later all the commands are added, so the user will be able to\n# execute everything.\n#\n# Basically ACL rules are processed left-to-right.\n#\n# For more information about ACL configuration please refer to\n# the Redis web site at https://redis.io/topics/acl\n\n# ACL LOG\n#\n# The ACL Log tracks failed commands and authentication events associated\n# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked\n# by ACLs. The ACL Log is stored in memory. You can reclaim memory with\n# ACL LOG RESET. Define the maximum entry length of the ACL Log below.\nacllog-max-len 128\n\n# Using an external ACL file\n#\n# Instead of configuring users here in this file, it is possible to use\n# a stand-alone file just listing users. The two methods cannot be mixed:\n# if you configure users here and at the same time you activate the external\n# ACL file, the server will refuse to start.\n#\n# The format of the external ACL user file is exactly the same as the\n# format that is used inside redis.conf to describe users.\n#\n# aclfile /etc/redis/users.acl\n\n# aclfile /etc/redis/users.acl\n\n# IMPORTANT NOTE: starting with Redis 6 \"requirepass\" is just a compatibility\n# layer on top of the new ACL system. The option effect will be just setting\n# the password for the default user. Clients will still authenticate using\n# AUTH <password> as usually, or more explicitly with AUTH default <password>\n# if they follow the new protocol: both will work.\n#\n# requirepass somepass\n\n# Command renaming (DEPRECATED).\n#\n# ------------------------------------------------------------------------\n# WARNING: avoid using this option if possible. Instead use ACLs to remove\n# commands from the default user, and put them only in some admin user you\n# create for administrative purposes.\n# ------------------------------------------------------------------------\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to replicas may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# IMPORTANT: When Redis Cluster is used, the max number of connections is also\n# shared with the cluster bus: every node in the cluster will use two\n# connections, one incoming and another outgoing. It is important to size the\n# limit accordingly in case of very large clusters.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have replicas attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the replicas are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of replicas is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have replicas attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for replica\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select one from the following behaviors:\n#\n# volatile-lru -> Evict using approximated LRU, only keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key having an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. By default Redis will check five keys and pick the one that was\n# used least recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n# Starting from Redis 5, by default a replica will ignore its maxmemory setting\n# (unless it is promoted to master after a failover or manually). It means\n# that the eviction of keys will be just handled by the master, sending the\n# DEL commands to the replica as keys evict in the master side.\n#\n# This behavior ensures that masters and replicas stay consistent, and is usually\n# what you want, however if your replica is writable, or you want the replica\n# to have a different memory setting, and you are sure all the writes performed\n# to the replica are idempotent, then you may change this default (but be sure\n# to understand what you are doing).\n#\n# Note that since the replica by default does not evict, it may end using more\n# memory than the one set via maxmemory (there are certain buffers that may\n# be larger on the replica, or data structures may sometimes take more memory\n# and so forth). So make sure you monitor your replicas and make sure they\n# have enough memory to never hit a real out-of-memory condition before the\n# master hits the configured maxmemory setting.\n#\n# replica-ignore-maxmemory yes\n\n# Redis reclaims expired keys in two ways: upon access when those keys are\n# found to be expired, and also in background, in what is called the\n# \"active expire key\". The key space is slowly and interactively scanned\n# looking for expired keys to reclaim, so that it is possible to free memory\n# of keys that are expired and will never be accessed again in a short time.\n#\n# The default effort of the expire cycle will try to avoid having more than\n# ten percent of expired keys still in memory, and will try to avoid consuming\n# more than 25% of total memory and to add latency to the system. However\n# it is possible to increase the expire \"effort\" that is normally set to\n# \"1\", to a greater value, up to the value \"10\". At its maximum value the\n# system will use more CPU, longer cycles (and technically may introduce\n# more latency), and will tolerate less already expired keys still present\n# in the system. It's a tradeoff between memory, CPU and latency.\n#\n# active-expire-effort 1\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a replica performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives.\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nreplica-lazy-flush no\n\n# It is also possible, for the case when to replace the user code DEL calls\n# with UNLINK calls is not easy, to modify the default behavior of the DEL\n# command to act exactly like UNLINK, using the following configuration\n# directive:\n\nlazyfree-lazy-user-del no\n\n################################ THREADED I/O #################################\n\n# Redis is mostly single threaded, however there are certain threaded\n# operations such as UNLINK, slow I/O accesses and other things that are\n# performed on side threads.\n#\n# Now it is also possible to handle Redis clients socket reads and writes\n# in different I/O threads. Since especially writing is so slow, normally\n# Redis users use pipelining in order to speed up the Redis performances per\n# core, and spawn multiple instances in order to scale more. Using I/O\n# threads it is possible to easily speedup two times Redis without resorting\n# to pipelining nor sharding of the instance.\n#\n# By default threading is disabled, we suggest enabling it only in machines\n# that have at least 4 or more cores, leaving at least one spare core.\n# Using more than 8 threads is unlikely to help much. We also recommend using\n# threaded I/O only if you actually have performance problems, with Redis\n# instances being able to use a quite big percentage of CPU time, otherwise\n# there is no point in using this feature.\n#\n# So for instance if you have a four cores boxes, try to use 2 or 3 I/O\n# threads, if you have a 8 cores, try to use 6 threads. In order to\n# enable I/O threads use the following configuration directive:\n#\n# io-threads 4\n#\n# Setting io-threads to 1 will just use the main thread as usual.\n# When I/O threads are enabled, we only use threads for writes, that is\n# to thread the write(2) syscall and transfer the client buffers to the\n# socket. However it is also possible to enable threading of reads and\n# protocol parsing using the following configuration directive, by setting\n# it to yes:\n#\n# io-threads-do-reads no\n#\n# Usually threading reads doesn't help much.\n#\n# NOTE 1: This configuration directive cannot be changed at runtime via\n# CONFIG SET. Aso this feature currently does not work when SSL is\n# enabled.\n#\n# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make\n# sure you also run the benchmark itself in threaded mode, using the\n# --threads option to match the number of Redis threads, otherwise you'll not\n# be able to notice the improvements.\n\n############################ KERNEL OOM CONTROL ##############################\n\n# On Linux, it is possible to hint the kernel OOM killer on what processes\n# should be killed first when out of memory.\n#\n# Enabling this feature makes Redis actively control the oom_score_adj value\n# for all its processes, depending on their role. The default scores will\n# attempt to have background child processes killed before all others, and\n# replicas killed before masters.\n#\n# Redis supports three options:\n#\n# no:       Don't make changes to oom-score-adj (default).\n# yes:      Alias to \"relative\" see below.\n# absolute: Values in oom-score-adj-values are written as is to the kernel.\n# relative: Values are used relative to the initial value of oom_score_adj when\n#           the server starts and are then clamped to a range of -1000 to 1000.\n#           Because typically the initial value is 0, they will often match the\n#           absolute values.\noom-score-adj no\n\n# When oom-score-adj is used, this directive controls the specific values used\n# for master, replica and background child processes. Values range -2000 to\n# 2000 (higher means more likely to be killed).\n#\n# Unprivileged processes (not root, and without CAP_SYS_RESOURCE capabilities)\n# can freely increase their value, but not decrease it below its initial\n# settings. This means that setting oom-score-adj to \"relative\" and setting the\n# oom-score-adj-values to positive values will always succeed.\noom-score-adj-values 0 200 800\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading, Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, then continues loading the AOF\n# tail.\naof-use-rdb-preamble yes\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet call any write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\ncluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file /etc/data/nodes.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are a multiple of the node timeout.\n#\ncluster-node-timeout 15000\n\n# A replica of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a replica to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple replicas able to failover, they exchange messages\n#    in order to try to give an advantage to the replica with the best\n#    replication offset (more data from the master processed).\n#    Replicas will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single replica computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the replica will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a replica will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * cluster-replica-validity-factor) + repl-ping-replica-period\n#\n# So for example if node-timeout is 30 seconds, and the cluster-replica-validity-factor\n# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the\n# replica will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large cluster-replica-validity-factor may allow replicas with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a replica at all.\n#\n# For maximum availability, it is possible to set the cluster-replica-validity-factor\n# to a value of 0, which means, that replicas will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-replica-validity-factor 10\n\n# Cluster replicas are able to migrate to orphaned masters, that are masters\n# that are left without working replicas. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working replicas.\n#\n# Replicas migrate to orphaned masters only if there are still at least a\n# given number of other working replicas for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a replica\n# will migrate only if there is at least 1 other working replica for its master\n# and so forth. It usually reflects the number of replicas you want for every\n# master in your cluster.\n#\n# Default is 1 (replicas migrate only if their masters remain with at least\n# one replica). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least a hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents replicas from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-replica-no-failover no\n\n# This option, when set to yes, allows nodes to serve read traffic while the\n# the cluster is in a down state, as long as it believes it owns the slots.\n#\n# This is useful for two cases.  The first case is for when an application\n# doesn't require consistency of data during node failures or network partitions.\n# One example of this is a cache, where as long as the node has the data it\n# should be able to serve it.\n#\n# The second use case is for configurations that don't meet the recommended\n# three shards but want to enable cluster mode and scale later. A\n# master outage in a 1 or 2 shard configuration causes a read/write outage to the\n# entire cluster without this option set, with it set there is only a write outage.\n# Without a quorum of masters, slot ownership will not change automatically.\n#\n# cluster-allow-reads-when-down no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instructs the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usual.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  t     Stream commands\n#  m     Key-miss events (Note: It is not included in the 'A' class)\n#  A     Alias for g$lshzxet, so that the \"AKE\" string means all the events\n#        (Except key-miss events which are excluded from 'A' due to their\n#         unique nature).\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### GOPHER SERVER #################################\n\n# Redis contains an implementation of the Gopher protocol, as specified in\n# the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt).\n#\n# The Gopher protocol was very popular in the late '90s. It is an alternative\n# to the web, and the implementation both server and client side is so simple\n# that the Redis server has just 100 lines of code in order to implement this\n# support.\n#\n# What do you do with Gopher nowadays? Well Gopher never *really* died, and\n# lately there is a movement in order for the Gopher more hierarchical content\n# composed of just plain text documents to be resurrected. Some want a simpler\n# internet, others believe that the mainstream internet became too much\n# controlled, and it's cool to create an alternative space for people that\n# want a bit of fresh air.\n#\n# Anyway for the 10nth birthday of the Redis, we gave it the Gopher protocol\n# as a gift.\n#\n# --- HOW IT WORKS? ---\n#\n# The Redis Gopher support uses the inline protocol of Redis, and specifically\n# two kind of inline requests that were anyway illegal: an empty request\n# or any request that starts with \"/\" (there are no Redis commands starting\n# with such a slash). Normal RESP2/RESP3 requests are completely out of the\n# path of the Gopher protocol implementation and are served as usual as well.\n#\n# If you open a connection to Redis when Gopher is enabled and send it\n# a string like \"/foo\", if there is a key named \"/foo\" it is served via the\n# Gopher protocol.\n#\n# In order to create a real Gopher \"hole\" (the name of a Gopher site in Gopher\n# talking), you likely need a script like the following:\n#\n#   https://github.com/antirez/gopher2redis\n#\n# --- SECURITY WARNING ---\n#\n# If you plan to put Redis on the internet in a publicly accessible address\n# to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance.\n# Once a password is set:\n#\n#   1. The Gopher server (when enabled, not by default) will still serve\n#      content via Gopher.\n#   2. However other commands cannot be called before the client will\n#      authenticate.\n#\n# So use the 'requirepass' option to protect your instance.\n#\n# Note that Gopher is not currently supported when 'io-threads-do-reads'\n# is enabled.\n#\n# To enable Gopher support, uncomment the following line and set the option\n# from no (the default) to yes.\n#\n# gopher-enabled no\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Streams macro node max size / items. The stream data structure is a radix\n# tree of big nodes that encode multiple items inside. Using this configuration\n# it is possible to configure how big a single node can be in bytes, and the\n# maximum number of items it may contain before switching to a new node when\n# appending new stream entries. If any of the following settings are set to\n# zero, the limit is ignored, so for instance it is possible to set just a\n# max entires limit by setting max-bytes to 0 and max-entries to the desired\n# value.\nstream-node-max-bytes 4096\nstream-node-max-entries 100\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# replica  -> replica clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and replica clients, since\n# subscribers and replicas receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit replica 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here, but must be 1mb or greater\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# Normally it is useful to have an HZ value which is proportional to the\n# number of clients connected. This is useful in order, for instance, to\n# avoid too many clients are processed for each background task invocation\n# in order to avoid latency spikes.\n#\n# Since the default HZ value by default is conservatively set to 10, Redis\n# offers, and enables by default, the ability to use an adaptive HZ value\n# which will temporarily raise when there are many connected clients.\n#\n# When dynamic HZ is enabled, the actual configured HZ will be used\n# as a baseline, but multiples of the configured HZ value will be actually\n# used as needed once more clients are connected. In this way an idle\n# instance will use very little CPU time while a busy instance will be\n# more responsive.\ndynamic-hz yes\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# When redis saves RDB file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\nrdb-save-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in a \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag no\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage, to be used when the lower\n# threshold is reached\n# active-defrag-cycle-min 1\n\n# Maximal effort for defrag in CPU percentage, to be used when the upper\n# threshold is reached\n# active-defrag-cycle-max 25\n\n# Maximum number of set/hash/zset/list fields that will be processed from\n# the main dictionary scan\n# active-defrag-max-scan-fields 1000\n\n# Jemalloc background thread for purging will be enabled by default\njemalloc-bg-thread yes\n\n# It is possible to pin different threads and processes of Redis to specific\n# CPUs in your system, in order to maximize the performances of the server.\n# This is useful both in order to pin different Redis threads in different\n# CPUs, but also in order to make sure that multiple Redis instances running\n# in the same host will be pinned to different CPUs.\n#\n# Normally you can do this using the \"taskset\" command, however it is also\n# possible to this via Redis configuration directly, both in Linux and FreeBSD.\n#\n# You can pin the server/IO threads, bio threads, aof rewrite child process, and\n# the bgsave child process. The syntax to specify the cpu list is the same as\n# the taskset command:\n#\n# Set redis server/io threads to cpu affinity 0,2,4,6:\n# server_cpulist 0-7:2\n#\n# Set bio threads to cpu affinity 1,3:\n# bio_cpulist 1,3\n#\n# Set aof rewrite child process to cpu affinity 8,9,10,11:\n# aof_rewrite_cpulist 8-11\n#\n# Set bgsave child process to cpu affinity 1,10,11\n# bgsave_cpulist 1,10-11\n\n# In some cases redis will emit warnings and even refuse to start if it detects\n# that the system is in bad state, it is possible to suppress these warnings\n# by setting the following config which takes a space delimited list of warnings\n# to suppress\n#\n# ignore-warnings ARM64-COW-BUG\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7-rs/Dockerfile",
    "content": "FROM --platform=linux/amd64 redislabs/rejson:1.0.8 AS rejson\n\nFROM --platform=linux/amd64 redis:7.0.8\n\nCOPY redis.conf /etc/redis/\nCOPY --from=rejson /usr/lib/redis/modules/rejson.so /etc/redis/modules/\n\nCMD ls -la\nRUN ls -la /etc/redis/modules\n\nADD https://s3.amazonaws.com/redisinsight.test/public/rte/modules/redisearch-coord/redisearch-coord.so.tar.gz .\n\nRUN ls -la\nRUN ls -la /etc/redis/modules\nRUN tar -xvzf redisearch-coord.so.tar.gz && rm redisearch-coord.so.tar.gz && cp redisearch-coord.so /etc/redis/modules\nRUN ls -la\nRUN ls -la /etc/redis/modules\n\nCMD [ \"redis-server\", \"/etc/redis/redis.conf\" ]\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7-rs/cluster-rs-create.sh",
    "content": "#!/bin/sh\n\necho 'Try to sleep for a while...'\nsleep 25\necho 'Creating cluster...'\necho \"yes\" | redis-cli \\\n  --cluster create \\\n  172.31.100.221:6379 \\\n  172.31.100.222:6379 \\\n  172.31.100.223:6379 \\\n  --cluster-replicas 0 \\\n  && redis-server\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7-rs/creator.Dockerfile",
    "content": "FROM --platform=linux/amd64 redis:7.0.6\n\nUSER root\n\nCOPY cluster-rs-create.sh ./\n\nRUN chmod a+x cluster-rs-create.sh\n\nCMD [\"/bin/sh\", \"./cluster-rs-create.sh\"]\n"
  },
  {
    "path": "tests/e2e/rte/oss-cluster-7-rs/redis.conf",
    "content": "# Redis configuration file example.\n#\n# Note that in order to read the configuration file, Redis must be\n# started with the file path as first argument:\n#\n# ./redis-server /path/to/redis.conf\n\n# Note on units: when memory size is needed, it is possible to specify\n# it in the usual form of 1k 5GB 4M and so forth:\n#\n# 1k => 1000 bytes\n# 1kb => 1024 bytes\n# 1m => 1000000 bytes\n# 1mb => 1024*1024 bytes\n# 1g => 1000000000 bytes\n# 1gb => 1024*1024*1024 bytes\n#\n# units are case insensitive so 1GB 1Gb 1gB are all the same.\n\n################################## INCLUDES ###################################\n\n# Include one or more other config files here.  This is useful if you\n# have a standard template that goes to all Redis servers but also need\n# to customize a few per-server settings.  Include files can include\n# other files, so use this wisely.\n#\n# Note that option \"include\" won't be rewritten by command \"CONFIG REWRITE\"\n# from admin or Redis Sentinel. Since Redis always uses the last processed\n# line as value of a configuration directive, you'd better put includes\n# at the beginning of this file to avoid overwriting config change at runtime.\n#\n# If instead you are interested in using includes to override configuration\n# options, it is better to use include as the last line.\n#\n# include /path/to/local.conf\n# include /path/to/other.conf\n\n################################## MODULES #####################################\n\n# Load modules at startup. If the server is not able to load modules\n# it will abort. It is possible to use multiple loadmodule directives.\n#\nloadmodule /etc/redis/modules/rejson.so\nloadmodule /etc/redis/modules/redisearch-coord.so\n# loadmodule /path/to/other_module.so\n\n################################## NETWORK #####################################\n\n# By default, if no \"bind\" configuration directive is specified, Redis listens\n# for connections from all available network interfaces on the host machine.\n# It is possible to listen to just one or multiple selected interfaces using\n# the \"bind\" configuration directive, followed by one or more IP addresses.\n#\n# Examples:\n#\n# bind 192.168.1.100 10.0.0.1\n# bind 127.0.0.1 ::1\n#\n# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the\n# internet, binding to all the interfaces is dangerous and will expose the\n# instance to everybody on the internet. So by default we uncomment the\n# following bind directive, that will force Redis to listen only on the\n# IPv4 loopback interface address (this means Redis will only be able to\n# accept client connections from the same host that it is running on).\n#\n# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES\n# JUST COMMENT OUT THE FOLLOWING LINE.\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# bind 127.0.0.1\n\n# Protected mode is a layer of security protection, in order to avoid that\n# Redis instances left open on the internet are accessed and exploited.\n#\n# When protected mode is on and if:\n#\n# 1) The server is not binding explicitly to a set of addresses using the\n#    \"bind\" directive.\n# 2) No password is configured.\n#\n# The server only accepts connections from clients connecting from the\n# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain\n# sockets.\n#\n# By default protected mode is enabled. You should disable it only if\n# you are sure you want clients from other hosts to connect to Redis\n# even if no authentication is configured, nor a specific set of interfaces\n# are explicitly listed using the \"bind\" directive.\n# protected-mode yes\n\n# Accept connections on the specified port, default is 6379 (IANA #815344).\n# If port 0 is specified Redis will not listen on a TCP socket.\nport 6379\n\n# TCP listen() backlog.\n#\n# In high requests-per-second environments you need a high backlog in order\n# to avoid slow clients connection issues. Note that the Linux kernel\n# will silently truncate it to the value of /proc/sys/net/core/somaxconn so\n# make sure to raise both the value of somaxconn and tcp_max_syn_backlog\n# in order to get the desired effect.\ntcp-backlog 511\n\n# Unix socket.\n#\n# Specify the path for the Unix socket that will be used to listen for\n# incoming connections. There is no default, so Redis will not listen\n# on a unix socket when not specified.\n#\n# unixsocket /tmp/redis.sock\n# unixsocketperm 700\n\n# Close the connection after a client is idle for N seconds (0 to disable)\ntimeout 0\n\n# TCP keepalive.\n#\n# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence\n# of communication. This is useful for two reasons:\n#\n# 1) Detect dead peers.\n# 2) Force network equipment in the middle to consider the connection to be\n#    alive.\n#\n# On Linux, the specified value (in seconds) is the period used to send ACKs.\n# Note that to close the connection the double of the time is needed.\n# On other kernels the period depends on the kernel configuration.\n#\n# A reasonable value for this option is 300 seconds, which is the new\n# Redis default starting with Redis 3.2.1.\ntcp-keepalive 300\n\n################################# TLS/SSL #####################################\n\n# By default, TLS/SSL is disabled. To enable it, the \"tls-port\" configuration\n# directive can be used to define TLS-listening ports. To enable TLS on the\n# default port, use:\n#\n# port 0\n# tls-port 6379\n\n# Configure a X.509 certificate and private key to use for authenticating the\n# server to connected clients, masters or cluster peers.  These files should be\n# PEM formatted.\n#\n# tls-cert-file /etc/redis/redis.crt\n# tls-key-file /etc/redis/redis.key\n\n# Configure a DH parameters file to enable Diffie-Hellman (DH) key exchange:\n#\n# tls-dh-params-file redis.dh\n\n# Configure a CA certificate(s) bundle or directory to authenticate TLS/SSL\n# clients and peers.  Redis requires an explicit configuration of at least one\n# of these, and will not implicitly use the system wide configuration.\n#\n# tls-ca-cert-file /etc/redis/ca.crt\n# tls-ca-cert-dir /etc/ssl/certs\n\n# By default, clients (including replica servers) on a TLS port are required\n# to authenticate using valid client side certificates.\n#\n# If \"no\" is specified, client certificates are not required and not accepted.\n# If \"optional\" is specified, client certificates are accepted and must be\n# valid if provided, but are not required.\n#\n# tls-auth-clients yes\n# tls-auth-clients optional\n\n# By default, a Redis replica does not attempt to establish a TLS connection\n# with its master.\n#\n# Use the following directive to enable TLS on replication links.\n#\n# tls-replication yes\n\n# By default, the Redis Cluster bus uses a plain TCP connection. To enable\n# TLS for the bus protocol, use the following directive:\n#\n# tls-cluster yes\n\n# Explicitly specify TLS versions to support. Allowed values are case insensitive\n# and include \"TLSv1\", \"TLSv1.1\", \"TLSv1.2\", \"TLSv1.3\" (OpenSSL >= 1.1.1) or\n# any combination. To enable only TLSv1.2 and TLSv1.3, use:\n#\n# tls-protocols \"TLSv1.2 TLSv1.3\"\n\n# Configure allowed ciphers.  See the ciphers(1ssl) manpage for more information\n# about the syntax of this string.\n#\n# Note: this configuration applies only to <= TLSv1.2.\n#\n# tls-ciphers DEFAULT:!MEDIUM\n\n# Configure allowed TLSv1.3 ciphersuites.  See the ciphers(1ssl) manpage for more\n# information about the syntax of this string, and specifically for TLSv1.3\n# ciphersuites.\n#\n# tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256\n\n# When choosing a cipher, use the server's preference instead of the client\n# preference. By default, the server follows the client's preference.\n#\n# tls-prefer-server-ciphers yes\n\n# By default, TLS session caching is enabled to allow faster and less expensive\n# reconnections by clients that support it. Use the following directive to disable\n# caching.\n#\n# tls-session-caching no\n\n# Change the default number of TLS sessions cached. A zero value sets the cache\n# to unlimited size. The default size is 20480.\n#\n# tls-session-cache-size 5000\n\n# Change the default timeout of cached TLS sessions. The default timeout is 300\n# seconds.\n#\n# tls-session-cache-timeout 60\n\n################################# GENERAL #####################################\n\n# By default Redis does not run as a daemon. Use 'yes' if you need it.\n# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.\ndaemonize no\n\n# If you run Redis from upstart or systemd, Redis can interact with your\n# supervision tree. Options:\n#   supervised no      - no supervision interaction\n#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode\n#                        requires \"expect stop\" in your upstart job config\n#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET\n#   supervised auto    - detect upstart or systemd method based on\n#                        UPSTART_JOB or NOTIFY_SOCKET environment variables\n# Note: these supervision methods only signal \"process is ready.\"\n#       They do not enable continuous pings back to your supervisor.\nsupervised no\n\n# If a pid file is specified, Redis writes it where specified at startup\n# and removes it at exit.\n#\n# When the server runs non daemonized, no pid file is created if none is\n# specified in the configuration. When the server is daemonized, the pid file\n# is used even if not specified, defaulting to \"/var/run/redis.pid\".\n#\n# Creating a pid file is best effort: if Redis is not able to create it\n# nothing bad happens, the server will start and run normally.\npidfile /var/run/redis_6379.pid\n\n# Specify the server verbosity level.\n# This can be one of:\n# debug (a lot of information, useful for development/testing)\n# verbose (many rarely useful info, but not a mess like the debug level)\n# notice (moderately verbose, what you want in production probably)\n# warning (only very important / critical messages are logged)\nloglevel notice\n\n# Specify the log file name. Also the empty string can be used to force\n# Redis to log on the standard output. Note that if you use standard\n# output for logging but daemonize, logs will be sent to /dev/null\nlogfile \"\"\n\n# To enable logging to the system logger, just set 'syslog-enabled' to yes,\n# and optionally update the other syslog parameters to suit your needs.\n# syslog-enabled no\n\n# Specify the syslog identity.\n# syslog-ident redis\n\n# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.\n# syslog-facility local0\n\n# Set the number of databases. The default database is DB 0, you can select\n# a different one on a per-connection basis using SELECT <dbid> where\n# dbid is a number between 0 and 'databases'-1\ndatabases 16\n\n# By default Redis shows an ASCII art logo only when started to log to the\n# standard output and if the standard output is a TTY. Basically this means\n# that normally a logo is displayed only in interactive sessions.\n#\n# However it is possible to force the pre-4.0 behavior and always show a\n# ASCII art logo in startup logs by setting the following option to yes.\nalways-show-logo yes\n\n################################ SNAPSHOTTING  ################################\n#\n# Save the DB on disk:\n#\n#   save <seconds> <changes>\n#\n#   Will save the DB if both the given number of seconds and the given\n#   number of write operations against the DB occurred.\n#\n#   In the example below the behavior will be to save:\n#   after 900 sec (15 min) if at least 1 key changed\n#   after 300 sec (5 min) if at least 10 keys changed\n#   after 60 sec if at least 10000 keys changed\n#\n#   Note: you can disable saving completely by commenting out all \"save\" lines.\n#\n#   It is also possible to remove all the previously configured save\n#   points by adding a save directive with a single empty string argument\n#   like in the following example:\n#\n#   save \"\"\n\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# By default Redis will stop accepting writes if RDB snapshots are enabled\n# (at least one save point) and the latest background save failed.\n# This will make the user aware (in a hard way) that data is not persisting\n# on disk properly, otherwise chances are that no one will notice and some\n# disaster will happen.\n#\n# If the background saving process will start working again Redis will\n# automatically allow writes again.\n#\n# However if you have setup your proper monitoring of the Redis server\n# and persistence, you may want to disable this feature so that Redis will\n# continue to work as usual even if there are problems with disk,\n# permissions, and so forth.\nstop-writes-on-bgsave-error yes\n\n# Compress string objects using LZF when dump .rdb databases?\n# By default compression is enabled as it's almost always a win.\n# If you want to save some CPU in the saving child set it to 'no' but\n# the dataset will likely be bigger if you have compressible values or keys.\nrdbcompression yes\n\n# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.\n# This makes the format more resistant to corruption but there is a performance\n# hit to pay (around 10%) when saving and loading RDB files, so you can disable it\n# for maximum performances.\n#\n# RDB files created with checksum disabled have a checksum of zero that will\n# tell the loading code to skip the check.\nrdbchecksum yes\n\n# The filename where to dump the DB\ndbfilename dump.rdb\n\n# Remove RDB files used by replication in instances without persistence\n# enabled. By default this option is disabled, however there are environments\n# where for regulations or other security concerns, RDB files persisted on\n# disk by masters in order to feed replicas, or stored on disk by replicas\n# in order to load them for the initial synchronization, should be deleted\n# ASAP. Note that this option ONLY WORKS in instances that have both AOF\n# and RDB persistence disabled, otherwise is completely ignored.\n#\n# An alternative (and sometimes better) way to obtain the same effect is\n# to use diskless replication on both master and replicas instances. However\n# in the case of replicas, diskless is not always an option.\n# in the case of replicas, diskless is not always an option.\nrdb-del-sync-files no\n\n# The working directory.\n#\n# The DB will be written inside this directory, with the filename specified\n# above using the 'dbfilename' configuration directive.\n#\n# The Append Only File will also be created inside this directory.\n#\n# Note that you must specify a directory here, not a file name.\ndir ./\n\n################################# REPLICATION #################################\n\n# Master-Replica replication. Use replicaof to make a Redis instance a copy of\n# another Redis server. A few things to understand ASAP about Redis replication.\n#\n#   +------------------+      +---------------+\n#   |      Master      | ---> |    Replica    |\n#   | (receive writes) |      |  (exact copy) |\n#   +------------------+      +---------------+\n#\n# 1) Redis replication is asynchronous, but you can configure a master to\n#    stop accepting writes if it appears to be not connected with at least\n#    a given number of replicas.\n# 2) Redis replicas are able to perform a partial resynchronization with the\n#    master if the replication link is lost for a relatively small amount of\n#    time. You may want to configure the replication backlog size (see the next\n#    sections of this file) with a sensible value depending on your needs.\n# 3) Replication is automatic and does not need user intervention. After a\n#    network partition replicas automatically try to reconnect to masters\n#    and resynchronize with them.\n#\n# replicaof <masterip> <masterport>\n\n# If the master is password protected (using the \"requirepass\" configuration\n# directive below) it is possible to tell the replica to authenticate before\n# starting the replication synchronization process, otherwise the master will\n# refuse the replica request.\n#\n# masterauth defaultpass\n#\n# However this is not enough if you are using Redis ACLs (for Redis version\n# 6 or greater), and the default user is not capable of running the PSYNC\n# command and/or other commands needed for replication. In this case it's\n# better to configure a special user to use with replication, and specify the\n# masteruser configuration as such:\n#\n# masteruser <username>\n#\n# When masteruser is specified, the replica will authenticate against its\n# master using the new AUTH form: AUTH <username> <password>.\n\n# When a replica loses its connection with the master, or when the replication\n# is still in progress, the replica can act in two different ways:\n#\n# 1) if replica-serve-stale-data is set to 'yes' (the default) the replica will\n#    still reply to client requests, possibly with out of date data, or the\n#    data set may just be empty if this is the first synchronization.\n#\n# 2) If replica-serve-stale-data is set to 'no' the replica will reply with\n#    an error \"SYNC with master in progress\" to all commands except:\n#    INFO, REPLICAOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, CONFIG, SUBSCRIBE,\n#    UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB, COMMAND, POST,\n#    HOST and LATENCY.\n#\nreplica-serve-stale-data yes\n\n# You can configure a replica instance to accept writes or not. Writing against\n# a replica instance may be useful to store some ephemeral data (because data\n# written on a replica will be easily deleted after resync with the master) but\n# may also cause problems if clients are writing to it because of a\n# misconfiguration.\n#\n# Since Redis 2.6 by default replicas are read-only.\n#\n# Note: read only replicas are not designed to be exposed to untrusted clients\n# on the internet. It's just a protection layer against misuse of the instance.\n# Still a read only replica exports by default all the administrative commands\n# such as CONFIG, DEBUG, and so forth. To a limited extent you can improve\n# security of read only replicas using 'rename-command' to shadow all the\n# administrative / dangerous commands.\nreplica-read-only yes\n\n# Replication SYNC strategy: disk or socket.\n#\n# New replicas and reconnecting replicas that are not able to continue the\n# replication process just receiving differences, need to do what is called a\n# \"full synchronization\". An RDB file is transmitted from the master to the\n# replicas.\n#\n# The transmission can happen in two different ways:\n#\n# 1) Disk-backed: The Redis master creates a new process that writes the RDB\n#                 file on disk. Later the file is transferred by the parent\n#                 process to the replicas incrementally.\n# 2) Diskless: The Redis master creates a new process that directly writes the\n#              RDB file to replica sockets, without touching the disk at all.\n#\n# With disk-backed replication, while the RDB file is generated, more replicas\n# can be queued and served with the RDB file as soon as the current child\n# producing the RDB file finishes its work. With diskless replication instead\n# once the transfer starts, new replicas arriving will be queued and a new\n# transfer will start when the current one terminates.\n#\n# When diskless replication is used, the master waits a configurable amount of\n# time (in seconds) before starting the transfer in the hope that multiple\n# replicas will arrive and the transfer can be parallelized.\n#\n# With slow disks and fast (large bandwidth) networks, diskless replication\n# works better.\nrepl-diskless-sync no\n\n# When diskless replication is enabled, it is possible to configure the delay\n# the server waits in order to spawn the child that transfers the RDB via socket\n# to the replicas.\n#\n# This is important since once the transfer starts, it is not possible to serve\n# new replicas arriving, that will be queued for the next RDB transfer, so the\n# server waits a delay in order to let more replicas arrive.\n#\n# The delay is specified in seconds, and by default is 5 seconds. To disable\n# it entirely just set it to 0 seconds and the transfer will start ASAP.\nrepl-diskless-sync-delay 5\n\n# -----------------------------------------------------------------------------\n# WARNING: RDB diskless load is experimental. Since in this setup the replica\n# does not immediately store an RDB on disk, it may cause data loss during\n# failovers. RDB diskless load + Redis modules not handling I/O reads may also\n# cause Redis to abort in case of I/O errors during the initial synchronization\n# stage with the master. Use only if your do what you are doing.\n# -----------------------------------------------------------------------------\n#\n# Replica can load the RDB it reads from the replication link directly from the\n# socket, or store the RDB to a file and read that file after it was completely\n# received from the master.\n#\n# In many cases the disk is slower than the network, and storing and loading\n# the RDB file may increase replication time (and even increase the master's\n# Copy on Write memory and salve buffers).\n# However, parsing the RDB file directly from the socket may mean that we have\n# to flush the contents of the current database before the full rdb was\n# received. For this reason we have the following options:\n#\n# \"disabled\"    - Don't use diskless load (store the rdb file to the disk first)\n# \"on-empty-db\" - Use diskless load only when it is completely safe.\n# \"swapdb\"      - Keep a copy of the current db contents in RAM while parsing\n#                 the data directly from the socket. note that this requires\n#                 sufficient memory, if you don't have it, you risk an OOM kill.\nrepl-diskless-load disabled\n\n# Replicas send PINGs to server in a predefined interval. It's possible to\n# change this interval with the repl_ping_replica_period option. The default\n# value is 10 seconds.\n#\n# repl-ping-replica-period 10\n\n# The following option sets the replication timeout for:\n#\n# 1) Bulk transfer I/O during SYNC, from the point of view of replica.\n# 2) Master timeout from the point of view of replicas (data, pings).\n# 3) Replica timeout from the point of view of masters (REPLCONF ACK pings).\n#\n# It is important to make sure that this value is greater than the value\n# specified for repl-ping-replica-period otherwise a timeout will be detected\n# every time there is low traffic between the master and the replica. The default\n# value is 60 seconds.\n#\n# repl-timeout 60\n\n# Disable TCP_NODELAY on the replica socket after SYNC?\n#\n# If you select \"yes\" Redis will use a smaller number of TCP packets and\n# less bandwidth to send data to replicas. But this can add a delay for\n# the data to appear on the replica side, up to 40 milliseconds with\n# Linux kernels using a default configuration.\n#\n# If you select \"no\" the delay for data to appear on the replica side will\n# be reduced but more bandwidth will be used for replication.\n#\n# By default we optimize for low latency, but in very high traffic conditions\n# or when the master and replicas are many hops away, turning this to \"yes\" may\n# be a good idea.\nrepl-disable-tcp-nodelay no\n\n# Set the replication backlog size. The backlog is a buffer that accumulates\n# replica data when replicas are disconnected for some time, so that when a\n# replica wants to reconnect again, often a full resync is not needed, but a\n# partial resync is enough, just passing the portion of data the replica\n# missed while disconnected.\n#\n# The bigger the replication backlog, the longer the replica can endure the\n# disconnect and later be able to perform a partial resynchronization.\n#\n# The backlog is only allocated if there is at least one replica connected.\n#\n# repl-backlog-size 1mb\n\n# After a master has no connected replicas for some time, the backlog will be\n# freed. The following option configures the amount of seconds that need to\n# elapse, starting from the time the last replica disconnected, for the backlog\n# buffer to be freed.\n#\n# Note that replicas never free the backlog for timeout, since they may be\n# promoted to masters later, and should be able to correctly \"partially\n# resynchronize\" with other replicas: hence they should always accumulate backlog.\n#\n# A value of 0 means to never release the backlog.\n#\n# repl-backlog-ttl 3600\n\n# The replica priority is an integer number published by Redis in the INFO\n# output. It is used by Redis Sentinel in order to select a replica to promote\n# into a master if the master is no longer working correctly.\n#\n# A replica with a low priority number is considered better for promotion, so\n# for instance if there are three replicas with priority 10, 100, 25 Sentinel\n# will pick the one with priority 10, that is the lowest.\n#\n# However a special priority of 0 marks the replica as not able to perform the\n# role of master, so a replica with priority of 0 will never be selected by\n# Redis Sentinel for promotion.\n#\n# By default the priority is 100.\nreplica-priority 100\n\n# It is possible for a master to stop accepting writes if there are less than\n# N replicas connected, having a lag less or equal than M seconds.\n#\n# The N replicas need to be in \"online\" state.\n#\n# The lag in seconds, that must be <= the specified value, is calculated from\n# the last ping received from the replica, that is usually sent every second.\n#\n# This option does not GUARANTEE that N replicas will accept the write, but\n# will limit the window of exposure for lost writes in case not enough replicas\n# are available, to the specified number of seconds.\n#\n# For example to require at least 3 replicas with a lag <= 10 seconds use:\n#\n# min-replicas-to-write 3\n# min-replicas-max-lag 10\n#\n# Setting one or the other to 0 disables the feature.\n#\n# By default min-replicas-to-write is set to 0 (feature disabled) and\n# min-replicas-max-lag is set to 10.\n\n# A Redis master is able to list the address and port of the attached\n# replicas in different ways. For example the \"INFO replication\" section\n# offers this information, which is used, among other tools, by\n# Redis Sentinel in order to discover replica instances.\n# Another place where this info is available is in the output of the\n# \"ROLE\" command of a master.\n#\n# The listed IP address and port normally reported by a replica is\n# obtained in the following way:\n#\n#   IP: The address is auto detected by checking the peer address\n#   of the socket used by the replica to connect with the master.\n#\n#   Port: The port is communicated by the replica during the replication\n#   handshake, and is normally the port that the replica is using to\n#   listen for connections.\n#\n# However when port forwarding or Network Address Translation (NAT) is\n# used, the replica may actually be reachable via different IP and port\n# pairs. The following two options can be used by a replica in order to\n# report to its master a specific set of IP and port, so that both INFO\n# and ROLE will report those values.\n#\n# There is no need to use both the options if you need to override just\n# the port or the IP address.\n#\n# replica-announce-ip 5.5.5.5\n# replica-announce-port 1234\n\n############################### KEYS TRACKING #################################\n\n# Redis implements server assisted support for client side caching of values.\n# This is implemented using an invalidation table that remembers, using\n# 16 millions of slots, what clients may have certain subsets of keys. In turn\n# this is used in order to send invalidation messages to clients. Please\n# check this page to understand more about the feature:\n#\n#   https://redis.io/topics/client-side-caching\n#\n# When tracking is enabled for a client, all the read only queries are assumed\n# to be cached: this will force Redis to store information in the invalidation\n# table. When keys are modified, such information is flushed away, and\n# invalidation messages are sent to the clients. However if the workload is\n# heavily dominated by reads, Redis could use more and more memory in order\n# to track the keys fetched by many clients.\n#\n# For this reason it is possible to configure a maximum fill value for the\n# invalidation table. By default it is set to 1M of keys, and once this limit\n# is reached, Redis will start to evict keys in the invalidation table\n# even if they were not modified, just to reclaim memory: this will in turn\n# force the clients to invalidate the cached values. Basically the table\n# maximum size is a trade off between the memory you want to spend server\n# side to track information about who cached what, and the ability of clients\n# to retain cached objects in memory.\n#\n# If you set the value to 0, it means there are no limits, and Redis will\n# retain as many keys as needed in the invalidation table.\n# In the \"stats\" INFO section, you can find information about the number of\n# keys in the invalidation table at every given moment.\n#\n# Note: when key tracking is used in broadcasting mode, no memory is used\n# in the server side so this setting is useless.\n#\n# tracking-table-max-keys 1000000\n\n################################## SECURITY ###################################\n\n# Warning: since Redis is pretty fast, an outside user can try up to\n# 1 million passwords per second against a modern box. This means that you\n# should use very strong passwords, otherwise they will be very easy to break.\n# Note that because the password is really a shared secret between the client\n# and the server, and should not be memorized by any human, the password\n# can be easily a long string from /dev/urandom or whatever, so by using a\n# long and unguessable password no brute force attack will be possible.\n\n# Redis ACL users are defined in the following format:\n#\n#   user <username> ... acl rules ...\n#\n# For example:\n#\n#   user worker +@list +@connection ~jobs:* on >ffa9203c493aa99\n#\n# The special username \"default\" is used for new connections. If this user\n# has the \"nopass\" rule, then new connections will be immediately authenticated\n# as the \"default\" user without the need of any password provided via the\n# AUTH command. Otherwise if the \"default\" user is not flagged with \"nopass\"\n# the connections will start in not authenticated state, and will require\n# AUTH (or the HELLO command AUTH option) in order to be authenticated and\n# start to work.\n#\n# The ACL rules that describe what a user can do are the following:\n#\n#  on           Enable the user: it is possible to authenticate as this user.\n#  off          Disable the user: it's no longer possible to authenticate\n#               with this user, however the already authenticated connections\n#               will still work.\n#  +<command>   Allow the execution of that command\n#  -<command>   Disallow the execution of that command\n#  +@<category> Allow the execution of all the commands in such category\n#               with valid categories are like @admin, @set, @sortedset, ...\n#               and so forth, see the full list in the server.c file where\n#               the Redis command table is described and defined.\n#               The special category @all means all the commands, but currently\n#               present in the server, and that will be loaded in the future\n#               via modules.\n#  +<command>|subcommand    Allow a specific subcommand of an otherwise\n#                           disabled command. Note that this form is not\n#                           allowed as negative like -DEBUG|SEGFAULT, but\n#                           only additive starting with \"+\".\n#  allcommands  Alias for +@all. Note that it implies the ability to execute\n#               all the future commands loaded via the modules system.\n#  nocommands   Alias for -@all.\n#  ~<pattern>   Add a pattern of keys that can be mentioned as part of\n#               commands. For instance ~* allows all the keys. The pattern\n#               is a glob-style pattern like the one of KEYS.\n#               It is possible to specify multiple patterns.\n#  allkeys      Alias for ~*\n#  resetkeys    Flush the list of allowed keys patterns.\n#  ><password>  Add this password to the list of valid password for the user.\n#               For example >mypass will add \"mypass\" to the list.\n#               This directive clears the \"nopass\" flag (see later).\n#  <<password>  Remove this password from the list of valid passwords.\n#  nopass       All the set passwords of the user are removed, and the user\n#               is flagged as requiring no password: it means that every\n#               password will work against this user. If this directive is\n#               used for the default user, every new connection will be\n#               immediately authenticated with the default user without\n#               any explicit AUTH command required. Note that the \"resetpass\"\n#               directive will clear this condition.\n#  resetpass    Flush the list of allowed passwords. Moreover removes the\n#               \"nopass\" status. After \"resetpass\" the user has no associated\n#               passwords and there is no way to authenticate without adding\n#               some password (or setting it as \"nopass\" later).\n#  reset        Performs the following actions: resetpass, resetkeys, off,\n#               -@all. The user returns to the same state it has immediately\n#               after its creation.\n#\n# ACL rules can be specified in any order: for instance you can start with\n# passwords, then flags, or key patterns. However note that the additive\n# and subtractive rules will CHANGE MEANING depending on the ordering.\n# For instance see the following example:\n#\n#   user alice on +@all -DEBUG ~* >somepassword\n#\n# This will allow \"alice\" to use all the commands with the exception of the\n# DEBUG command, since +@all added all the commands to the set of the commands\n# alice can use, and later DEBUG was removed. However if we invert the order\n# of two ACL rules the result will be different:\n#\n#   user alice on -DEBUG +@all ~* >somepassword\n#\n# Now DEBUG was removed when alice had yet no commands in the set of allowed\n# commands, later all the commands are added, so the user will be able to\n# execute everything.\n#\n# Basically ACL rules are processed left-to-right.\n#\n# For more information about ACL configuration please refer to\n# the Redis web site at https://redis.io/topics/acl\n\n# ACL LOG\n#\n# The ACL Log tracks failed commands and authentication events associated\n# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked\n# by ACLs. The ACL Log is stored in memory. You can reclaim memory with\n# ACL LOG RESET. Define the maximum entry length of the ACL Log below.\nacllog-max-len 128\n\n# Using an external ACL file\n#\n# Instead of configuring users here in this file, it is possible to use\n# a stand-alone file just listing users. The two methods cannot be mixed:\n# if you configure users here and at the same time you activate the external\n# ACL file, the server will refuse to start.\n#\n# The format of the external ACL user file is exactly the same as the\n# format that is used inside redis.conf to describe users.\n#\n# aclfile /etc/redis/users.acl\n\n# aclfile /etc/redis/users.acl\n\n# IMPORTANT NOTE: starting with Redis 6 \"requirepass\" is just a compatibility\n# layer on top of the new ACL system. The option effect will be just setting\n# the password for the default user. Clients will still authenticate using\n# AUTH <password> as usually, or more explicitly with AUTH default <password>\n# if they follow the new protocol: both will work.\n#\n# requirepass somepass\n\n# Command renaming (DEPRECATED).\n#\n# ------------------------------------------------------------------------\n# WARNING: avoid using this option if possible. Instead use ACLs to remove\n# commands from the default user, and put them only in some admin user you\n# create for administrative purposes.\n# ------------------------------------------------------------------------\n#\n# It is possible to change the name of dangerous commands in a shared\n# environment. For instance the CONFIG command may be renamed into something\n# hard to guess so that it will still be available for internal-use tools\n# but not available for general clients.\n#\n# Example:\n#\n# rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52\n#\n# It is also possible to completely kill a command by renaming it into\n# an empty string:\n#\n# rename-command CONFIG \"\"\n#\n# Please note that changing the name of commands that are logged into the\n# AOF file or transmitted to replicas may cause problems.\n\n################################### CLIENTS ####################################\n\n# Set the max number of connected clients at the same time. By default\n# this limit is set to 10000 clients, however if the Redis server is not\n# able to configure the process file limit to allow for the specified limit\n# the max number of allowed clients is set to the current file limit\n# minus 32 (as Redis reserves a few file descriptors for internal uses).\n#\n# Once the limit is reached Redis will close all the new connections sending\n# an error 'max number of clients reached'.\n#\n# IMPORTANT: When Redis Cluster is used, the max number of connections is also\n# shared with the cluster bus: every node in the cluster will use two\n# connections, one incoming and another outgoing. It is important to size the\n# limit accordingly in case of very large clusters.\n#\n# maxclients 10000\n\n############################## MEMORY MANAGEMENT ################################\n\n# Set a memory usage limit to the specified amount of bytes.\n# When the memory limit is reached Redis will try to remove keys\n# according to the eviction policy selected (see maxmemory-policy).\n#\n# If Redis can't remove keys according to the policy, or if the policy is\n# set to 'noeviction', Redis will start to reply with errors to commands\n# that would use more memory, like SET, LPUSH, and so on, and will continue\n# to reply to read-only commands like GET.\n#\n# This option is usually useful when using Redis as an LRU or LFU cache, or to\n# set a hard memory limit for an instance (using the 'noeviction' policy).\n#\n# WARNING: If you have replicas attached to an instance with maxmemory on,\n# the size of the output buffers needed to feed the replicas are subtracted\n# from the used memory count, so that network problems / resyncs will\n# not trigger a loop where keys are evicted, and in turn the output\n# buffer of replicas is full with DELs of keys evicted triggering the deletion\n# of more keys, and so forth until the database is completely emptied.\n#\n# In short... if you have replicas attached it is suggested that you set a lower\n# limit for maxmemory so that there is some free RAM on the system for replica\n# output buffers (but this is not needed if the policy is 'noeviction').\n#\n# maxmemory <bytes>\n\n# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory\n# is reached. You can select one from the following behaviors:\n#\n# volatile-lru -> Evict using approximated LRU, only keys with an expire set.\n# allkeys-lru -> Evict any key using approximated LRU.\n# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.\n# allkeys-lfu -> Evict any key using approximated LFU.\n# volatile-random -> Remove a random key having an expire set.\n# allkeys-random -> Remove a random key, any key.\n# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)\n# noeviction -> Don't evict anything, just return an error on write operations.\n#\n# LRU means Least Recently Used\n# LFU means Least Frequently Used\n#\n# Both LRU, LFU and volatile-ttl are implemented using approximated\n# randomized algorithms.\n#\n# Note: with any of the above policies, Redis will return an error on write\n#       operations, when there are no suitable keys for eviction.\n#\n#       At the date of writing these commands are: set setnx setex append\n#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd\n#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby\n#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby\n#       getset mset msetnx exec sort\n#\n# The default is:\n#\n# maxmemory-policy noeviction\n\n# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated\n# algorithms (in order to save memory), so you can tune it for speed or\n# accuracy. By default Redis will check five keys and pick the one that was\n# used least recently, you can change the sample size using the following\n# configuration directive.\n#\n# The default of 5 produces good enough results. 10 Approximates very closely\n# true LRU but costs more CPU. 3 is faster but not very accurate.\n#\n# maxmemory-samples 5\n\n# Starting from Redis 5, by default a replica will ignore its maxmemory setting\n# (unless it is promoted to master after a failover or manually). It means\n# that the eviction of keys will be just handled by the master, sending the\n# DEL commands to the replica as keys evict in the master side.\n#\n# This behavior ensures that masters and replicas stay consistent, and is usually\n# what you want, however if your replica is writable, or you want the replica\n# to have a different memory setting, and you are sure all the writes performed\n# to the replica are idempotent, then you may change this default (but be sure\n# to understand what you are doing).\n#\n# Note that since the replica by default does not evict, it may end using more\n# memory than the one set via maxmemory (there are certain buffers that may\n# be larger on the replica, or data structures may sometimes take more memory\n# and so forth). So make sure you monitor your replicas and make sure they\n# have enough memory to never hit a real out-of-memory condition before the\n# master hits the configured maxmemory setting.\n#\n# replica-ignore-maxmemory yes\n\n# Redis reclaims expired keys in two ways: upon access when those keys are\n# found to be expired, and also in background, in what is called the\n# \"active expire key\". The key space is slowly and interactively scanned\n# looking for expired keys to reclaim, so that it is possible to free memory\n# of keys that are expired and will never be accessed again in a short time.\n#\n# The default effort of the expire cycle will try to avoid having more than\n# ten percent of expired keys still in memory, and will try to avoid consuming\n# more than 25% of total memory and to add latency to the system. However\n# it is possible to increase the expire \"effort\" that is normally set to\n# \"1\", to a greater value, up to the value \"10\". At its maximum value the\n# system will use more CPU, longer cycles (and technically may introduce\n# more latency), and will tolerate less already expired keys still present\n# in the system. It's a tradeoff between memory, CPU and latency.\n#\n# active-expire-effort 1\n\n############################# LAZY FREEING ####################################\n\n# Redis has two primitives to delete keys. One is called DEL and is a blocking\n# deletion of the object. It means that the server stops processing new commands\n# in order to reclaim all the memory associated with an object in a synchronous\n# way. If the key deleted is associated with a small object, the time needed\n# in order to execute the DEL command is very small and comparable to most other\n# O(1) or O(log_N) commands in Redis. However if the key is associated with an\n# aggregated value containing millions of elements, the server can block for\n# a long time (even seconds) in order to complete the operation.\n#\n# For the above reasons Redis also offers non blocking deletion primitives\n# such as UNLINK (non blocking DEL) and the ASYNC option of FLUSHALL and\n# FLUSHDB commands, in order to reclaim memory in background. Those commands\n# are executed in constant time. Another thread will incrementally free the\n# object in the background as fast as possible.\n#\n# DEL, UNLINK and ASYNC option of FLUSHALL and FLUSHDB are user-controlled.\n# It's up to the design of the application to understand when it is a good\n# idea to use one or the other. However the Redis server sometimes has to\n# delete keys or flush the whole database as a side effect of other operations.\n# Specifically Redis deletes objects independently of a user call in the\n# following scenarios:\n#\n# 1) On eviction, because of the maxmemory and maxmemory policy configurations,\n#    in order to make room for new data, without going over the specified\n#    memory limit.\n# 2) Because of expire: when a key with an associated time to live (see the\n#    EXPIRE command) must be deleted from memory.\n# 3) Because of a side effect of a command that stores data on a key that may\n#    already exist. For example the RENAME command may delete the old key\n#    content when it is replaced with another one. Similarly SUNIONSTORE\n#    or SORT with STORE option may delete existing keys. The SET command\n#    itself removes any old content of the specified key in order to replace\n#    it with the specified string.\n# 4) During replication, when a replica performs a full resynchronization with\n#    its master, the content of the whole database is removed in order to\n#    load the RDB file just transferred.\n#\n# In all the above cases the default is to delete objects in a blocking way,\n# like if DEL was called. However you can configure each case specifically\n# in order to instead release memory in a non-blocking way like if UNLINK\n# was called, using the following configuration directives.\n\nlazyfree-lazy-eviction no\nlazyfree-lazy-expire no\nlazyfree-lazy-server-del no\nreplica-lazy-flush no\n\n# It is also possible, for the case when to replace the user code DEL calls\n# with UNLINK calls is not easy, to modify the default behavior of the DEL\n# command to act exactly like UNLINK, using the following configuration\n# directive:\n\nlazyfree-lazy-user-del no\n\n################################ THREADED I/O #################################\n\n# Redis is mostly single threaded, however there are certain threaded\n# operations such as UNLINK, slow I/O accesses and other things that are\n# performed on side threads.\n#\n# Now it is also possible to handle Redis clients socket reads and writes\n# in different I/O threads. Since especially writing is so slow, normally\n# Redis users use pipelining in order to speed up the Redis performances per\n# core, and spawn multiple instances in order to scale more. Using I/O\n# threads it is possible to easily speedup two times Redis without resorting\n# to pipelining nor sharding of the instance.\n#\n# By default threading is disabled, we suggest enabling it only in machines\n# that have at least 4 or more cores, leaving at least one spare core.\n# Using more than 8 threads is unlikely to help much. We also recommend using\n# threaded I/O only if you actually have performance problems, with Redis\n# instances being able to use a quite big percentage of CPU time, otherwise\n# there is no point in using this feature.\n#\n# So for instance if you have a four cores boxes, try to use 2 or 3 I/O\n# threads, if you have a 8 cores, try to use 6 threads. In order to\n# enable I/O threads use the following configuration directive:\n#\n# io-threads 4\n#\n# Setting io-threads to 1 will just use the main thread as usual.\n# When I/O threads are enabled, we only use threads for writes, that is\n# to thread the write(2) syscall and transfer the client buffers to the\n# socket. However it is also possible to enable threading of reads and\n# protocol parsing using the following configuration directive, by setting\n# it to yes:\n#\n# io-threads-do-reads no\n#\n# Usually threading reads doesn't help much.\n#\n# NOTE 1: This configuration directive cannot be changed at runtime via\n# CONFIG SET. Aso this feature currently does not work when SSL is\n# enabled.\n#\n# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make\n# sure you also run the benchmark itself in threaded mode, using the\n# --threads option to match the number of Redis threads, otherwise you'll not\n# be able to notice the improvements.\n\n############################ KERNEL OOM CONTROL ##############################\n\n# On Linux, it is possible to hint the kernel OOM killer on what processes\n# should be killed first when out of memory.\n#\n# Enabling this feature makes Redis actively control the oom_score_adj value\n# for all its processes, depending on their role. The default scores will\n# attempt to have background child processes killed before all others, and\n# replicas killed before masters.\n#\n# Redis supports three options:\n#\n# no:       Don't make changes to oom-score-adj (default).\n# yes:      Alias to \"relative\" see below.\n# absolute: Values in oom-score-adj-values are written as is to the kernel.\n# relative: Values are used relative to the initial value of oom_score_adj when\n#           the server starts and are then clamped to a range of -1000 to 1000.\n#           Because typically the initial value is 0, they will often match the\n#           absolute values.\noom-score-adj no\n\n# When oom-score-adj is used, this directive controls the specific values used\n# for master, replica and background child processes. Values range -2000 to\n# 2000 (higher means more likely to be killed).\n#\n# Unprivileged processes (not root, and without CAP_SYS_RESOURCE capabilities)\n# can freely increase their value, but not decrease it below its initial\n# settings. This means that setting oom-score-adj to \"relative\" and setting the\n# oom-score-adj-values to positive values will always succeed.\noom-score-adj-values 0 200 800\n\n############################## APPEND ONLY MODE ###############################\n\n# By default Redis asynchronously dumps the dataset on disk. This mode is\n# good enough in many applications, but an issue with the Redis process or\n# a power outage may result into a few minutes of writes lost (depending on\n# the configured save points).\n#\n# The Append Only File is an alternative persistence mode that provides\n# much better durability. For instance using the default data fsync policy\n# (see later in the config file) Redis can lose just one second of writes in a\n# dramatic event like a server power outage, or a single write if something\n# wrong with the Redis process itself happens, but the operating system is\n# still running correctly.\n#\n# AOF and RDB persistence can be enabled at the same time without problems.\n# If the AOF is enabled on startup Redis will load the AOF, that is the file\n# with the better durability guarantees.\n#\n# Please check http://redis.io/topics/persistence for more information.\n\nappendonly no\n\n# The name of the append only file (default: \"appendonly.aof\")\n\nappendfilename \"appendonly.aof\"\n\n# The fsync() call tells the Operating System to actually write data on disk\n# instead of waiting for more data in the output buffer. Some OS will really flush\n# data on disk, some other OS will just try to do it ASAP.\n#\n# Redis supports three different modes:\n#\n# no: don't fsync, just let the OS flush the data when it wants. Faster.\n# always: fsync after every write to the append only log. Slow, Safest.\n# everysec: fsync only one time every second. Compromise.\n#\n# The default is \"everysec\", as that's usually the right compromise between\n# speed and data safety. It's up to you to understand if you can relax this to\n# \"no\" that will let the operating system flush the output buffer when\n# it wants, for better performances (but if you can live with the idea of\n# some data loss consider the default persistence mode that's snapshotting),\n# or on the contrary, use \"always\" that's very slow but a bit safer than\n# everysec.\n#\n# More details please check the following article:\n# http://antirez.com/post/redis-persistence-demystified.html\n#\n# If unsure, use \"everysec\".\n\n# appendfsync always\nappendfsync everysec\n# appendfsync no\n\n# When the AOF fsync policy is set to always or everysec, and a background\n# saving process (a background save or AOF log background rewriting) is\n# performing a lot of I/O against the disk, in some Linux configurations\n# Redis may block too long on the fsync() call. Note that there is no fix for\n# this currently, as even performing fsync in a different thread will block\n# our synchronous write(2) call.\n#\n# In order to mitigate this problem it's possible to use the following option\n# that will prevent fsync() from being called in the main process while a\n# BGSAVE or BGREWRITEAOF is in progress.\n#\n# This means that while another child is saving, the durability of Redis is\n# the same as \"appendfsync none\". In practical terms, this means that it is\n# possible to lose up to 30 seconds of log in the worst scenario (with the\n# default Linux settings).\n#\n# If you have latency problems turn this to \"yes\". Otherwise leave it as\n# \"no\" that is the safest pick from the point of view of durability.\n\nno-appendfsync-on-rewrite no\n\n# Automatic rewrite of the append only file.\n# Redis is able to automatically rewrite the log file implicitly calling\n# BGREWRITEAOF when the AOF log size grows by the specified percentage.\n#\n# This is how it works: Redis remembers the size of the AOF file after the\n# latest rewrite (if no rewrite has happened since the restart, the size of\n# the AOF at startup is used).\n#\n# This base size is compared to the current size. If the current size is\n# bigger than the specified percentage, the rewrite is triggered. Also\n# you need to specify a minimal size for the AOF file to be rewritten, this\n# is useful to avoid rewriting the AOF file even if the percentage increase\n# is reached but it is still pretty small.\n#\n# Specify a percentage of zero in order to disable the automatic AOF\n# rewrite feature.\n\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb\n\n# An AOF file may be found to be truncated at the end during the Redis\n# startup process, when the AOF data gets loaded back into memory.\n# This may happen when the system where Redis is running\n# crashes, especially when an ext4 filesystem is mounted without the\n# data=ordered option (however this can't happen when Redis itself\n# crashes or aborts but the operating system still works correctly).\n#\n# Redis can either exit with an error when this happens, or load as much\n# data as possible (the default now) and start if the AOF file is found\n# to be truncated at the end. The following option controls this behavior.\n#\n# If aof-load-truncated is set to yes, a truncated AOF file is loaded and\n# the Redis server starts emitting a log to inform the user of the event.\n# Otherwise if the option is set to no, the server aborts with an error\n# and refuses to start. When the option is set to no, the user requires\n# to fix the AOF file using the \"redis-check-aof\" utility before to restart\n# the server.\n#\n# Note that if the AOF file will be found to be corrupted in the middle\n# the server will still exit with an error. This option only applies when\n# Redis will try to read more data from the AOF file but not enough bytes\n# will be found.\naof-load-truncated yes\n\n# When rewriting the AOF file, Redis is able to use an RDB preamble in the\n# AOF file for faster rewrites and recoveries. When this option is turned\n# on the rewritten AOF file is composed of two different stanzas:\n#\n#   [RDB file][AOF tail]\n#\n# When loading, Redis recognizes that the AOF file starts with the \"REDIS\"\n# string and loads the prefixed RDB file, then continues loading the AOF\n# tail.\naof-use-rdb-preamble yes\n\n################################ LUA SCRIPTING  ###############################\n\n# Max execution time of a Lua script in milliseconds.\n#\n# If the maximum execution time is reached Redis will log that a script is\n# still in execution after the maximum allowed time and will start to\n# reply to queries with an error.\n#\n# When a long running script exceeds the maximum execution time only the\n# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be\n# used to stop a script that did not yet call any write commands. The second\n# is the only way to shut down the server in the case a write command was\n# already issued by the script but the user doesn't want to wait for the natural\n# termination of the script.\n#\n# Set it to 0 or a negative value for unlimited execution without warnings.\nlua-time-limit 5000\n\n################################ REDIS CLUSTER  ###############################\n\n# Normal Redis instances can't be part of a Redis Cluster; only nodes that are\n# started as cluster nodes can. In order to start a Redis instance as a\n# cluster node enable the cluster support uncommenting the following:\n#\ncluster-enabled yes\n\n# Every cluster node has a cluster configuration file. This file is not\n# intended to be edited by hand. It is created and updated by Redis nodes.\n# Every Redis Cluster node requires a different cluster configuration file.\n# Make sure that instances running in the same system do not have\n# overlapping cluster configuration file names.\n#\n# cluster-config-file /etc/data/nodes.conf\n\n# Cluster node timeout is the amount of milliseconds a node must be unreachable\n# for it to be considered in failure state.\n# Most other internal time limits are a multiple of the node timeout.\n#\ncluster-node-timeout 15000\n\n# A replica of a failing master will avoid to start a failover if its data\n# looks too old.\n#\n# There is no simple way for a replica to actually have an exact measure of\n# its \"data age\", so the following two checks are performed:\n#\n# 1) If there are multiple replicas able to failover, they exchange messages\n#    in order to try to give an advantage to the replica with the best\n#    replication offset (more data from the master processed).\n#    Replicas will try to get their rank by offset, and apply to the start\n#    of the failover a delay proportional to their rank.\n#\n# 2) Every single replica computes the time of the last interaction with\n#    its master. This can be the last ping or command received (if the master\n#    is still in the \"connected\" state), or the time that elapsed since the\n#    disconnection with the master (if the replication link is currently down).\n#    If the last interaction is too old, the replica will not try to failover\n#    at all.\n#\n# The point \"2\" can be tuned by user. Specifically a replica will not perform\n# the failover if, since the last interaction with the master, the time\n# elapsed is greater than:\n#\n#   (node-timeout * cluster-replica-validity-factor) + repl-ping-replica-period\n#\n# So for example if node-timeout is 30 seconds, and the cluster-replica-validity-factor\n# is 10, and assuming a default repl-ping-replica-period of 10 seconds, the\n# replica will not try to failover if it was not able to talk with the master\n# for longer than 310 seconds.\n#\n# A large cluster-replica-validity-factor may allow replicas with too old data to failover\n# a master, while a too small value may prevent the cluster from being able to\n# elect a replica at all.\n#\n# For maximum availability, it is possible to set the cluster-replica-validity-factor\n# to a value of 0, which means, that replicas will always try to failover the\n# master regardless of the last time they interacted with the master.\n# (However they'll always try to apply a delay proportional to their\n# offset rank).\n#\n# Zero is the only value able to guarantee that when all the partitions heal\n# the cluster will always be able to continue.\n#\n# cluster-replica-validity-factor 10\n\n# Cluster replicas are able to migrate to orphaned masters, that are masters\n# that are left without working replicas. This improves the cluster ability\n# to resist to failures as otherwise an orphaned master can't be failed over\n# in case of failure if it has no working replicas.\n#\n# Replicas migrate to orphaned masters only if there are still at least a\n# given number of other working replicas for their old master. This number\n# is the \"migration barrier\". A migration barrier of 1 means that a replica\n# will migrate only if there is at least 1 other working replica for its master\n# and so forth. It usually reflects the number of replicas you want for every\n# master in your cluster.\n#\n# Default is 1 (replicas migrate only if their masters remain with at least\n# one replica). To disable migration just set it to a very large value.\n# A value of 0 can be set but is useful only for debugging and dangerous\n# in production.\n#\n# cluster-migration-barrier 1\n\n# By default Redis Cluster nodes stop accepting queries if they detect there\n# is at least a hash slot uncovered (no available node is serving it).\n# This way if the cluster is partially down (for example a range of hash slots\n# are no longer covered) all the cluster becomes, eventually, unavailable.\n# It automatically returns available as soon as all the slots are covered again.\n#\n# However sometimes you want the subset of the cluster which is working,\n# to continue to accept queries for the part of the key space that is still\n# covered. In order to do so, just set the cluster-require-full-coverage\n# option to no.\n#\n# cluster-require-full-coverage yes\n\n# This option, when set to yes, prevents replicas from trying to failover its\n# master during master failures. However the master can still perform a\n# manual failover, if forced to do so.\n#\n# This is useful in different scenarios, especially in the case of multiple\n# data center operations, where we want one side to never be promoted if not\n# in the case of a total DC failure.\n#\n# cluster-replica-no-failover no\n\n# This option, when set to yes, allows nodes to serve read traffic while the\n# the cluster is in a down state, as long as it believes it owns the slots.\n#\n# This is useful for two cases.  The first case is for when an application\n# doesn't require consistency of data during node failures or network partitions.\n# One example of this is a cache, where as long as the node has the data it\n# should be able to serve it.\n#\n# The second use case is for configurations that don't meet the recommended\n# three shards but want to enable cluster mode and scale later. A\n# master outage in a 1 or 2 shard configuration causes a read/write outage to the\n# entire cluster without this option set, with it set there is only a write outage.\n# Without a quorum of masters, slot ownership will not change automatically.\n#\n# cluster-allow-reads-when-down no\n\n# In order to setup your cluster make sure to read the documentation\n# available at http://redis.io web site.\n\n########################## CLUSTER DOCKER/NAT support  ########################\n\n# In certain deployments, Redis Cluster nodes address discovery fails, because\n# addresses are NAT-ted or because ports are forwarded (the typical case is\n# Docker and other containers).\n#\n# In order to make Redis Cluster working in such environments, a static\n# configuration where each node knows its public address is needed. The\n# following two options are used for this scope, and are:\n#\n# * cluster-announce-ip\n# * cluster-announce-port\n# * cluster-announce-bus-port\n#\n# Each instructs the node about its address, client port, and cluster message\n# bus port. The information is then published in the header of the bus packets\n# so that other nodes will be able to correctly map the address of the node\n# publishing the information.\n#\n# If the above options are not used, the normal Redis Cluster auto-detection\n# will be used instead.\n#\n# Note that when remapped, the bus port may not be at the fixed offset of\n# clients port + 10000, so you can specify any port and bus-port depending\n# on how they get remapped. If the bus-port is not set, a fixed offset of\n# 10000 will be used as usual.\n#\n# Example:\n#\n# cluster-announce-ip 10.1.1.5\n# cluster-announce-port 6379\n# cluster-announce-bus-port 6380\n\n################################## SLOW LOG ###################################\n\n# The Redis Slow Log is a system to log queries that exceeded a specified\n# execution time. The execution time does not include the I/O operations\n# like talking with the client, sending the reply and so forth,\n# but just the time needed to actually execute the command (this is the only\n# stage of command execution where the thread is blocked and can not serve\n# other requests in the meantime).\n#\n# You can configure the slow log with two parameters: one tells Redis\n# what is the execution time, in microseconds, to exceed in order for the\n# command to get logged, and the other parameter is the length of the\n# slow log. When a new command is logged the oldest one is removed from the\n# queue of logged commands.\n\n# The following time is expressed in microseconds, so 1000000 is equivalent\n# to one second. Note that a negative number disables the slow log, while\n# a value of zero forces the logging of every command.\nslowlog-log-slower-than 10000\n\n# There is no limit to this length. Just be aware that it will consume memory.\n# You can reclaim memory used by the slow log with SLOWLOG RESET.\nslowlog-max-len 128\n\n################################ LATENCY MONITOR ##############################\n\n# The Redis latency monitoring subsystem samples different operations\n# at runtime in order to collect data related to possible sources of\n# latency of a Redis instance.\n#\n# Via the LATENCY command this information is available to the user that can\n# print graphs and obtain reports.\n#\n# The system only logs operations that were performed in a time equal or\n# greater than the amount of milliseconds specified via the\n# latency-monitor-threshold configuration directive. When its value is set\n# to zero, the latency monitor is turned off.\n#\n# By default latency monitoring is disabled since it is mostly not needed\n# if you don't have latency issues, and collecting data has a performance\n# impact, that while very small, can be measured under big load. Latency\n# monitoring can easily be enabled at runtime using the command\n# \"CONFIG SET latency-monitor-threshold <milliseconds>\" if needed.\nlatency-monitor-threshold 0\n\n############################# EVENT NOTIFICATION ##############################\n\n# Redis can notify Pub/Sub clients about events happening in the key space.\n# This feature is documented at http://redis.io/topics/notifications\n#\n# For instance if keyspace events notification is enabled, and a client\n# performs a DEL operation on key \"foo\" stored in the Database 0, two\n# messages will be published via Pub/Sub:\n#\n# PUBLISH __keyspace@0__:foo del\n# PUBLISH __keyevent@0__:del foo\n#\n# It is possible to select the events that Redis will notify among a set\n# of classes. Every class is identified by a single character:\n#\n#  K     Keyspace events, published with __keyspace@<db>__ prefix.\n#  E     Keyevent events, published with __keyevent@<db>__ prefix.\n#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...\n#  $     String commands\n#  l     List commands\n#  s     Set commands\n#  h     Hash commands\n#  z     Sorted set commands\n#  x     Expired events (events generated every time a key expires)\n#  e     Evicted events (events generated when a key is evicted for maxmemory)\n#  t     Stream commands\n#  m     Key-miss events (Note: It is not included in the 'A' class)\n#  A     Alias for g$lshzxet, so that the \"AKE\" string means all the events\n#        (Except key-miss events which are excluded from 'A' due to their\n#         unique nature).\n#\n#  The \"notify-keyspace-events\" takes as argument a string that is composed\n#  of zero or multiple characters. The empty string means that notifications\n#  are disabled.\n#\n#  Example: to enable list and generic events, from the point of view of the\n#           event name, use:\n#\n#  notify-keyspace-events Elg\n#\n#  Example 2: to get the stream of the expired keys subscribing to channel\n#             name __keyevent@0__:expired use:\n#\n#  notify-keyspace-events Ex\n#\n#  By default all notifications are disabled because most users don't need\n#  this feature and the feature has some overhead. Note that if you don't\n#  specify at least one of K or E, no events will be delivered.\nnotify-keyspace-events \"\"\n\n############################### GOPHER SERVER #################################\n\n# Redis contains an implementation of the Gopher protocol, as specified in\n# the RFC 1436 (https://www.ietf.org/rfc/rfc1436.txt).\n#\n# The Gopher protocol was very popular in the late '90s. It is an alternative\n# to the web, and the implementation both server and client side is so simple\n# that the Redis server has just 100 lines of code in order to implement this\n# support.\n#\n# What do you do with Gopher nowadays? Well Gopher never *really* died, and\n# lately there is a movement in order for the Gopher more hierarchical content\n# composed of just plain text documents to be resurrected. Some want a simpler\n# internet, others believe that the mainstream internet became too much\n# controlled, and it's cool to create an alternative space for people that\n# want a bit of fresh air.\n#\n# Anyway for the 10nth birthday of the Redis, we gave it the Gopher protocol\n# as a gift.\n#\n# --- HOW IT WORKS? ---\n#\n# The Redis Gopher support uses the inline protocol of Redis, and specifically\n# two kind of inline requests that were anyway illegal: an empty request\n# or any request that starts with \"/\" (there are no Redis commands starting\n# with such a slash). Normal RESP2/RESP3 requests are completely out of the\n# path of the Gopher protocol implementation and are served as usual as well.\n#\n# If you open a connection to Redis when Gopher is enabled and send it\n# a string like \"/foo\", if there is a key named \"/foo\" it is served via the\n# Gopher protocol.\n#\n# In order to create a real Gopher \"hole\" (the name of a Gopher site in Gopher\n# talking), you likely need a script like the following:\n#\n#   https://github.com/antirez/gopher2redis\n#\n# --- SECURITY WARNING ---\n#\n# If you plan to put Redis on the internet in a publicly accessible address\n# to server Gopher pages MAKE SURE TO SET A PASSWORD to the instance.\n# Once a password is set:\n#\n#   1. The Gopher server (when enabled, not by default) will still serve\n#      content via Gopher.\n#   2. However other commands cannot be called before the client will\n#      authenticate.\n#\n# So use the 'requirepass' option to protect your instance.\n#\n# Note that Gopher is not currently supported when 'io-threads-do-reads'\n# is enabled.\n#\n# To enable Gopher support, uncomment the following line and set the option\n# from no (the default) to yes.\n#\n# gopher-enabled no\n\n############################### ADVANCED CONFIG ###############################\n\n# Hashes are encoded using a memory efficient data structure when they have a\n# small number of entries, and the biggest entry does not exceed a given\n# threshold. These thresholds can be configured using the following directives.\nhash-max-ziplist-entries 512\nhash-max-ziplist-value 64\n\n# Lists are also encoded in a special way to save a lot of space.\n# The number of entries allowed per internal list node can be specified\n# as a fixed maximum size or a maximum number of elements.\n# For a fixed maximum size, use -5 through -1, meaning:\n# -5: max size: 64 Kb  <-- not recommended for normal workloads\n# -4: max size: 32 Kb  <-- not recommended\n# -3: max size: 16 Kb  <-- probably not recommended\n# -2: max size: 8 Kb   <-- good\n# -1: max size: 4 Kb   <-- good\n# Positive numbers mean store up to _exactly_ that number of elements\n# per list node.\n# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),\n# but if your use case is unique, adjust the settings as necessary.\nlist-max-ziplist-size -2\n\n# Lists may also be compressed.\n# Compress depth is the number of quicklist ziplist nodes from *each* side of\n# the list to *exclude* from compression.  The head and tail of the list\n# are always uncompressed for fast push/pop operations.  Settings are:\n# 0: disable all list compression\n# 1: depth 1 means \"don't start compressing until after 1 node into the list,\n#    going from either the head or tail\"\n#    So: [head]->node->node->...->node->[tail]\n#    [head], [tail] will always be uncompressed; inner nodes will compress.\n# 2: [head]->[next]->node->node->...->node->[prev]->[tail]\n#    2 here means: don't compress head or head->next or tail->prev or tail,\n#    but compress all nodes between them.\n# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]\n# etc.\nlist-compress-depth 0\n\n# Sets have a special encoding in just one case: when a set is composed\n# of just strings that happen to be integers in radix 10 in the range\n# of 64 bit signed integers.\n# The following configuration setting sets the limit in the size of the\n# set in order to use this special memory saving encoding.\nset-max-intset-entries 512\n\n# Similarly to hashes and lists, sorted sets are also specially encoded in\n# order to save a lot of space. This encoding is only used when the length and\n# elements of a sorted set are below the following limits:\nzset-max-ziplist-entries 128\nzset-max-ziplist-value 64\n\n# HyperLogLog sparse representation bytes limit. The limit includes the\n# 16 bytes header. When an HyperLogLog using the sparse representation crosses\n# this limit, it is converted into the dense representation.\n#\n# A value greater than 16000 is totally useless, since at that point the\n# dense representation is more memory efficient.\n#\n# The suggested value is ~ 3000 in order to have the benefits of\n# the space efficient encoding without slowing down too much PFADD,\n# which is O(N) with the sparse encoding. The value can be raised to\n# ~ 10000 when CPU is not a concern, but space is, and the data set is\n# composed of many HyperLogLogs with cardinality in the 0 - 15000 range.\nhll-sparse-max-bytes 3000\n\n# Streams macro node max size / items. The stream data structure is a radix\n# tree of big nodes that encode multiple items inside. Using this configuration\n# it is possible to configure how big a single node can be in bytes, and the\n# maximum number of items it may contain before switching to a new node when\n# appending new stream entries. If any of the following settings are set to\n# zero, the limit is ignored, so for instance it is possible to set just a\n# max entires limit by setting max-bytes to 0 and max-entries to the desired\n# value.\nstream-node-max-bytes 4096\nstream-node-max-entries 100\n\n# Active rehashing uses 1 millisecond every 100 milliseconds of CPU time in\n# order to help rehashing the main Redis hash table (the one mapping top-level\n# keys to values). The hash table implementation Redis uses (see dict.c)\n# performs a lazy rehashing: the more operation you run into a hash table\n# that is rehashing, the more rehashing \"steps\" are performed, so if the\n# server is idle the rehashing is never complete and some more memory is used\n# by the hash table.\n#\n# The default is to use this millisecond 10 times every second in order to\n# actively rehash the main dictionaries, freeing memory when possible.\n#\n# If unsure:\n# use \"activerehashing no\" if you have hard latency requirements and it is\n# not a good thing in your environment that Redis can reply from time to time\n# to queries with 2 milliseconds delay.\n#\n# use \"activerehashing yes\" if you don't have such hard requirements but\n# want to free memory asap when possible.\nactiverehashing yes\n\n# The client output buffer limits can be used to force disconnection of clients\n# that are not reading data from the server fast enough for some reason (a\n# common reason is that a Pub/Sub client can't consume messages as fast as the\n# publisher can produce them).\n#\n# The limit can be set differently for the three different classes of clients:\n#\n# normal -> normal clients including MONITOR clients\n# replica  -> replica clients\n# pubsub -> clients subscribed to at least one pubsub channel or pattern\n#\n# The syntax of every client-output-buffer-limit directive is the following:\n#\n# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>\n#\n# A client is immediately disconnected once the hard limit is reached, or if\n# the soft limit is reached and remains reached for the specified number of\n# seconds (continuously).\n# So for instance if the hard limit is 32 megabytes and the soft limit is\n# 16 megabytes / 10 seconds, the client will get disconnected immediately\n# if the size of the output buffers reach 32 megabytes, but will also get\n# disconnected if the client reaches 16 megabytes and continuously overcomes\n# the limit for 10 seconds.\n#\n# By default normal clients are not limited because they don't receive data\n# without asking (in a push way), but just after a request, so only\n# asynchronous clients may create a scenario where data is requested faster\n# than it can read.\n#\n# Instead there is a default limit for pubsub and replica clients, since\n# subscribers and replicas receive data in a push fashion.\n#\n# Both the hard or the soft limit can be disabled by setting them to zero.\nclient-output-buffer-limit normal 0 0 0\nclient-output-buffer-limit replica 256mb 64mb 60\nclient-output-buffer-limit pubsub 32mb 8mb 60\n\n# Client query buffers accumulate new commands. They are limited to a fixed\n# amount by default in order to avoid that a protocol desynchronization (for\n# instance due to a bug in the client) will lead to unbound memory usage in\n# the query buffer. However you can configure it here if you have very special\n# needs, such us huge multi/exec requests or alike.\n#\n# client-query-buffer-limit 1gb\n\n# In the Redis protocol, bulk requests, that are, elements representing single\n# strings, are normally limited to 512 mb. However you can change this limit\n# here, but must be 1mb or greater\n#\n# proto-max-bulk-len 512mb\n\n# Redis calls an internal function to perform many background tasks, like\n# closing connections of clients in timeout, purging expired keys that are\n# never requested, and so forth.\n#\n# Not all tasks are performed with the same frequency, but Redis checks for\n# tasks to perform according to the specified \"hz\" value.\n#\n# By default \"hz\" is set to 10. Raising the value will use more CPU when\n# Redis is idle, but at the same time will make Redis more responsive when\n# there are many keys expiring at the same time, and timeouts may be\n# handled with more precision.\n#\n# The range is between 1 and 500, however a value over 100 is usually not\n# a good idea. Most users should use the default of 10 and raise this up to\n# 100 only in environments where very low latency is required.\nhz 10\n\n# Normally it is useful to have an HZ value which is proportional to the\n# number of clients connected. This is useful in order, for instance, to\n# avoid too many clients are processed for each background task invocation\n# in order to avoid latency spikes.\n#\n# Since the default HZ value by default is conservatively set to 10, Redis\n# offers, and enables by default, the ability to use an adaptive HZ value\n# which will temporarily raise when there are many connected clients.\n#\n# When dynamic HZ is enabled, the actual configured HZ will be used\n# as a baseline, but multiples of the configured HZ value will be actually\n# used as needed once more clients are connected. In this way an idle\n# instance will use very little CPU time while a busy instance will be\n# more responsive.\ndynamic-hz yes\n\n# When a child rewrites the AOF file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\naof-rewrite-incremental-fsync yes\n\n# When redis saves RDB file, if the following option is enabled\n# the file will be fsync-ed every 32 MB of data generated. This is useful\n# in order to commit the file to the disk more incrementally and avoid\n# big latency spikes.\nrdb-save-incremental-fsync yes\n\n# Redis LFU eviction (see maxmemory setting) can be tuned. However it is a good\n# idea to start with the default settings and only change them after investigating\n# how to improve the performances and how the keys LFU change over time, which\n# is possible to inspect via the OBJECT FREQ command.\n#\n# There are two tunable parameters in the Redis LFU implementation: the\n# counter logarithm factor and the counter decay time. It is important to\n# understand what the two parameters mean before changing them.\n#\n# The LFU counter is just 8 bits per key, it's maximum value is 255, so Redis\n# uses a probabilistic increment with logarithmic behavior. Given the value\n# of the old counter, when a key is accessed, the counter is incremented in\n# this way:\n#\n# 1. A random number R between 0 and 1 is extracted.\n# 2. A probability P is calculated as 1/(old_value*lfu_log_factor+1).\n# 3. The counter is incremented only if R < P.\n#\n# The default lfu-log-factor is 10. This is a table of how the frequency\n# counter changes with a different number of accesses with different\n# logarithmic factors:\n#\n# +--------+------------+------------+------------+------------+------------+\n# | factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |\n# +--------+------------+------------+------------+------------+------------+\n# | 0      | 104        | 255        | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 1      | 18         | 49         | 255        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 10     | 10         | 18         | 142        | 255        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n# | 100    | 8          | 11         | 49         | 143        | 255        |\n# +--------+------------+------------+------------+------------+------------+\n#\n# NOTE: The above table was obtained by running the following commands:\n#\n#   redis-benchmark -n 1000000 incr foo\n#   redis-cli object freq foo\n#\n# NOTE 2: The counter initial value is 5 in order to give new objects a chance\n# to accumulate hits.\n#\n# The counter decay time is the time, in minutes, that must elapse in order\n# for the key counter to be divided by two (or decremented if it has a value\n# less <= 10).\n#\n# The default value for the lfu-decay-time is 1. A special value of 0 means to\n# decay the counter every time it happens to be scanned.\n#\n# lfu-log-factor 10\n# lfu-decay-time 1\n\n########################### ACTIVE DEFRAGMENTATION #######################\n#\n# What is active defragmentation?\n# -------------------------------\n#\n# Active (online) defragmentation allows a Redis server to compact the\n# spaces left between small allocations and deallocations of data in memory,\n# thus allowing to reclaim back memory.\n#\n# Fragmentation is a natural process that happens with every allocator (but\n# less so with Jemalloc, fortunately) and certain workloads. Normally a server\n# restart is needed in order to lower the fragmentation, or at least to flush\n# away all the data and create it again. However thanks to this feature\n# implemented by Oran Agra for Redis 4.0 this process can happen at runtime\n# in a \"hot\" way, while the server is running.\n#\n# Basically when the fragmentation is over a certain level (see the\n# configuration options below) Redis will start to create new copies of the\n# values in contiguous memory regions by exploiting certain specific Jemalloc\n# features (in order to understand if an allocation is causing fragmentation\n# and to allocate it in a better place), and at the same time, will release the\n# old copies of the data. This process, repeated incrementally for all the keys\n# will cause the fragmentation to drop back to normal values.\n#\n# Important things to understand:\n#\n# 1. This feature is disabled by default, and only works if you compiled Redis\n#    to use the copy of Jemalloc we ship with the source code of Redis.\n#    This is the default with Linux builds.\n#\n# 2. You never need to enable this feature if you don't have fragmentation\n#    issues.\n#\n# 3. Once you experience fragmentation, you can enable this feature when\n#    needed with the command \"CONFIG SET activedefrag yes\".\n#\n# The configuration parameters are able to fine tune the behavior of the\n# defragmentation process. If you are not sure about what they mean it is\n# a good idea to leave the defaults untouched.\n\n# Enabled active defragmentation\n# activedefrag no\n\n# Minimum amount of fragmentation waste to start active defrag\n# active-defrag-ignore-bytes 100mb\n\n# Minimum percentage of fragmentation to start active defrag\n# active-defrag-threshold-lower 10\n\n# Maximum percentage of fragmentation at which we use maximum effort\n# active-defrag-threshold-upper 100\n\n# Minimal effort for defrag in CPU percentage, to be used when the lower\n# threshold is reached\n# active-defrag-cycle-min 1\n\n# Maximal effort for defrag in CPU percentage, to be used when the upper\n# threshold is reached\n# active-defrag-cycle-max 25\n\n# Maximum number of set/hash/zset/list fields that will be processed from\n# the main dictionary scan\n# active-defrag-max-scan-fields 1000\n\n# Jemalloc background thread for purging will be enabled by default\njemalloc-bg-thread yes\n\n# It is possible to pin different threads and processes of Redis to specific\n# CPUs in your system, in order to maximize the performances of the server.\n# This is useful both in order to pin different Redis threads in different\n# CPUs, but also in order to make sure that multiple Redis instances running\n# in the same host will be pinned to different CPUs.\n#\n# Normally you can do this using the \"taskset\" command, however it is also\n# possible to this via Redis configuration directly, both in Linux and FreeBSD.\n#\n# You can pin the server/IO threads, bio threads, aof rewrite child process, and\n# the bgsave child process. The syntax to specify the cpu list is the same as\n# the taskset command:\n#\n# Set redis server/io threads to cpu affinity 0,2,4,6:\n# server_cpulist 0-7:2\n#\n# Set bio threads to cpu affinity 1,3:\n# bio_cpulist 1,3\n#\n# Set aof rewrite child process to cpu affinity 8,9,10,11:\n# aof_rewrite_cpulist 8-11\n#\n# Set bgsave child process to cpu affinity 1,10,11\n# bgsave_cpulist 1,10-11\n\n# In some cases redis will emit warnings and even refuse to start if it detects\n# that the system is in bad state, it is possible to suppress these warnings\n# by setting the following config which takes a space delimited list of warnings\n# to suppress\n#\n# ignore-warnings ARM64-COW-BUG\n"
  },
  {
    "path": "tests/e2e/rte/oss-sentinel/Dockerfile",
    "content": "FROM redis:5\n\n\nADD sentinel.conf /etc/redis/sentinel.conf\nRUN chown redis:redis /etc/redis/sentinel.conf\nENV SENTINEL_QUORUM 2\nENV SENTINEL_DOWN_AFTER 5000\nENV SENTINEL_FAILOVER 10000\nENV SENTINEL_PORT 26000\nENV AUTH_PASS password\nENV REQUIREPASS=\"\"\nADD entrypoint.sh /usr/local/bin/\nRUN chmod +x /usr/local/bin/entrypoint.sh\nENTRYPOINT [\"entrypoint.sh\"]"
  },
  {
    "path": "tests/e2e/rte/oss-sentinel/entrypoint.sh",
    "content": "#!/bin/sh\n\nsed -i \"s/\\$SENTINEL_PORT/$SENTINEL_PORT/g\" /etc/redis/sentinel.conf\nsed -i \"s/\\$SENTINEL_QUORUM/$SENTINEL_QUORUM/g\" /etc/redis/sentinel.conf\nsed -i \"s/\\$SENTINEL_DOWN_AFTER/$SENTINEL_DOWN_AFTER/g\" /etc/redis/sentinel.conf\nsed -i \"s/\\$SENTINEL_FAILOVER/$SENTINEL_FAILOVER/g\" /etc/redis/sentinel.conf\nsed -i \"s/\\$AUTH_PASS/$AUTH_PASS/g\" /etc/redis/sentinel.conf\nsed -i \"s/\\$REQUIREPASS/$REQUIREPASS/g\" /etc/redis/sentinel.conf\n\nexec docker-entrypoint.sh redis-server /etc/redis/sentinel.conf --sentinel\n"
  },
  {
    "path": "tests/e2e/rte/oss-sentinel/sentinel.conf",
    "content": "port 26379\ndir /tmp\nsentinel monitor primary-group-1 oss-sentinel-primary-1 6379 $SENTINEL_QUORUM\nsentinel down-after-milliseconds primary-group-1 $SENTINEL_DOWN_AFTER\nsentinel parallel-syncs primary-group-1 1\nsentinel failover-timeout primary-group-1 $SENTINEL_FAILOVER\nsentinel auth-pass primary-group-1 password\n\nsentinel monitor primary-group-2 oss-sentinel-primary-2 6379 $SENTINEL_QUORUM\nsentinel down-after-milliseconds primary-group-2 $SENTINEL_DOWN_AFTER\nsentinel parallel-syncs primary-group-2 1\nsentinel failover-timeout primary-group-2 $SENTINEL_FAILOVER\nsentinel auth-pass primary-group-2 password\n\nrequirepass \"password\"\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-big/Dockerfile",
    "content": "FROM redislabs/redismod\n\nARG TEST_DB_DUMP\nADD $TEST_DB_DUMP /data/\n\nADD entrypoint.sh .\nRUN chmod +x entrypoint.sh\n\nENTRYPOINT [\"sh\", \"entrypoint.sh\", \"redis-server\"]\nCMD [\"--loadmodule\", \"/usr/lib/redis/modules/redisai.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisearch.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisgraph.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redistimeseries.so\", \"--loadmodule\", \"/usr/lib/redis/modules/rejson.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisbloom.so\", \"--loadmodule\", \"/usr/lib/redis/modules/redisgears.so\", \"Plugin\", \"/var/opt/redislabs/modules/rg/plugin/gears_python.so\"]\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-big/entrypoint.sh",
    "content": "#!/bin/sh\n\nif [ -e dump.tar.gz ]\nthen\n  echo 'Extracting .rdb file...'\n  tar -zxvf dump.tar.gz\nfi\n\nexec \"$@\"\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-tls/Dockerfile",
    "content": "FROM bitnamilegacy/redis:6.0.8\n\nENV ALLOW_EMPTY_PASSWORD yes\n\n# TLS options\nENV REDIS_TLS_ENABLED yes\nENV REDIS_TLS_PORT 6379\nENV REDIS_TLS_CERT_FILE /opt/bitnami/redis/certs/redis.crt\nENV REDIS_TLS_KEY_FILE /opt/bitnami/redis/certs/redis.key\nENV REDIS_TLS_CA_FILE /opt/bitnami/redis/certs/redisCA.crt\nENV REDIS_TLS_AUTH_CLIENTS yes\n\nCOPY --chown=1001 ./certs /opt/bitnami/redis/certs/\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-tls/certs/redis.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhEwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI3WhcNMzExMDI2MTMyNzI3WjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAM4osMW/hBlde+/E20wP9X+zJ0AMD6OtfhqQ5brC\ngbVs9mPccZ/8R0fj83YtZfnoEodMZ/7yUTeCCPMdprAezMU1KBf9EpZmTdWhpO3e\nkESHcQdsKkqGtyjYF7dDahTmKt4a4aHlPH0DJLltB5HlbVabkzlo+3S8QaNwH5lY\nyJTQIqiqVzs9oRLT76nZuJjsym0dNXE42rO3KCniI6kvJDmUzBD8Wc94iDExfy7q\nqHyV7b2DCp1w7XP4yrQAFQ6kiVqNcfTTAO4MHNP54V2nZLPdOsUD5BsYY8hu0HDc\n/PisZ9ZMcw7LMfpUd3dfA9zefQXPQsWJK20ZCNmdIFtwvIFUpYu/FEF3FWO83zeI\nXkVZiuCOnoFvp8JIDvXgzXUBWzvYmxLqVSZAuabqU5pKRswDPLGlZTkHbuP2DiXD\nLD5AsTnICpzYkUeERSZKf2qc/nTUk04W/7FUT/75ItVzZvu90mPJlmArB0j4zdAG\nKwKo8v/cF1hA1YznhibxcUAA/Q/O3Y6CPQ7C3NeaGKcycgUxWoEY3Leno40ukijd\nR0MvsaY7V0/up37fkPtH9rcCkZOGVT5Q4Ww9gVO3yseXVkxbJyzHV1tuwg6yY9wO\nLOU2Bbmazkjlkb8a5OyQol2zZNJ0L3lvRWTildtGUTsBkeqI6HAedTTHJkhjHh/P\nP0+TAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nAKn+aH60zdd4ItMOhgd4BIok/u6S4NF0oe4kDwC/EU5C0hbuT6gLAn7ArHMeadue\nOuSnhqIPfxNR9WCes9OU1O00WeCuWCE4XfNMMFN5iDFfLxO4Oy5Adt0T74hWbcy6\nh28TdcjrkJEr7HR59G5tQ8TW5gVB4a0WXDw0ob9DSxbFKZU1uZm9L/+MgB/SNCHL\nGZSKt75Z/M10b9BTC3OG9swsoWvXEjR2ICiwzk+LxVf5K38faDyBrNJVglrpEUZz\ngP60kL73qK0y1/i35UuP0yIJIy48XnDsSByN7eBVsNTGMW3CFLKWA4RVfnEHNUff\nvsLHXZFYsUIPnPc5jksFwb/wKAe9JbCrgQPhBYaIYkRGiYt64C48r3boIIVoz9+1\n9Nq0Ik06fCzlI9APq2nzEiVeB7mDyZ692neu32QM6zRkYor+W8uI21YnRJWlOx7+\nx2GIh2EZnEYNvbpbvk/fV5AqkYOu9auRAkcKfME7dJ3Gwndl0YBOjE2DMTv6vIjS\ndVuGXQCvlzkRAnPMh5MR5/bSUKVvBryXs9ecAMgoVXBVB+4tGWct5ziL+8qyNtgA\nWJ2EWj3xtLlMwwQmLjRsCrZjL4liLJG8Yn8Ehfq1rRJREH2O8uYKCO1fdhuI0Y5S\niBPfqJi6QBHj7i01K9OpNUB7l+xAFLA3cBsegcm2GPoL\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-tls/certs/redis.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDOKLDFv4QZXXvv\nxNtMD/V/sydADA+jrX4akOW6woG1bPZj3HGf/EdH4/N2LWX56BKHTGf+8lE3ggjz\nHaawHszFNSgX/RKWZk3VoaTt3pBEh3EHbCpKhrco2Be3Q2oU5ireGuGh5Tx9AyS5\nbQeR5W1Wm5M5aPt0vEGjcB+ZWMiU0CKoqlc7PaES0++p2biY7MptHTVxONqztygp\n4iOpLyQ5lMwQ/FnPeIgxMX8u6qh8le29gwqdcO1z+Mq0ABUOpIlajXH00wDuDBzT\n+eFdp2Sz3TrFA+QbGGPIbtBw3Pz4rGfWTHMOyzH6VHd3XwPc3n0Fz0LFiSttGQjZ\nnSBbcLyBVKWLvxRBdxVjvN83iF5FWYrgjp6Bb6fCSA714M11AVs72JsS6lUmQLmm\n6lOaSkbMAzyxpWU5B27j9g4lwyw+QLE5yAqc2JFHhEUmSn9qnP501JNOFv+xVE/+\n+SLVc2b7vdJjyZZgKwdI+M3QBisCqPL/3BdYQNWM54Ym8XFAAP0Pzt2Ogj0OwtzX\nmhinMnIFMVqBGNy3p6ONLpIo3UdDL7GmO1dP7qd+35D7R/a3ApGThlU+UOFsPYFT\nt8rHl1ZMWycsx1dbbsIOsmPcDizlNgW5ms5I5ZG/GuTskKJds2TSdC95b0Vk4pXb\nRlE7AZHqiOhwHnU0xyZIYx4fzz9PkwIDAQABAoICAHyc/+0oDHNAnK+bsGrTorNj\n2S/Pmox3TChGuXYgKEM/79cA4vWvim6cDQe7/U4Hx1tdBeeHFSyWP06k96Kxm1kA\n/pExedDLWfTt1kGqLE4gCGRSL2YI9CGOLRerei3TysmiOgygAeYWxlYG33KC2Ypm\nU6F6IbS4LnzaQ19v2R6KiMim3j+CyyAUV2O1pO1bBCjcZPdhRGEpLu/SL3gOdLkR\nhiAmSSstUjVaE+SKFvnnrmLFGN996SoWkoAnJJNLRXMk2GMCQCejzrEa8+ymSCqo\naOO5rGHsZjQ7N2dhTNALdmCEqW+hxz3nXKcdGbqiCbQ/Sb8ZYNR7M2xGm85p4Ka9\n0UK4cOM1VJPwz8hotSKAUmXnpbu73CsZi5HyzwZkk4FpcaYCYrCbGVmm1cIKEKI7\n8SN/oqgFdj4Ha9cemnu+RecQZouK+wPWtcILd2kstJl52TV952fVOrnXQDo6XCXB\nfbs9IYN1hB6N79xv4L7Jj53hSRMeSNf70Ejkh1FXPOvmvFT12wy5JQdBBR5nnb4a\nGEsMpGVe1k3bxjK7K263tLSH0UZ8dMgdSx1E4D1hT1K/gEwTSMOJ0E1R0M6SJmF2\n6TnZ0MbJWx6PbICmyZrd2agfTQrq6CgY1fWLGbQrtnwXtsUR7PiHarydXfs3V8g1\nxHnK1bItOBBMOMcWV93hAoIBAQD2xMceUfBUN0TeWrJ4izD3lUYxmWqjTg7kPcjJ\n0VN8v3txGAcyPmbuz5AEzvdFjTispoZNB9QOrmsUVWTQDE5otnei9kHWzqJWYHg4\nUSuUuAh8OJGCiepo8zHT3qHDNhKGtOAp5LC8TaOznUFr35zCBCOsvQfRUKrv5IOc\nvCFjO07Xly8+M3qK7/UswRQ6480VlE2t1p+VNaORHdTDg2tes3/9owuiNmR/sPT8\nnIoe01LS7qmZoiB1vracaLcBf1Iwd7RvKg7mgFJzmowZUYxyX2YGK5qZ1h74In2X\n55+qQnNW0RwPijopTv711pMhMKWl8i3ilcCfoeBXz8zCwFfbAoIBAQDV3wHAO7ic\nMYP/Bm5jgIwlix1eOWY/yB+VqdYn2GmC49vTEIlIVlFRq0vZE06iUxs87BIV08zO\n4w/iKXd7ktkuhphiEuU2yXA3LQPHpbSOW43RONbf4glFU/DlP/P6fiybbWj6+f7L\n7Zbvtz5AW03Y4ZpagJTqOgVdJ0MdLnh9vZj6okGGV1fidKtG6hr7Z/oLhnl9aAZK\n4vrvBZ//qz99vEVByiV5kRaJDulu+diBy4n6iBjzjHA5a9e7lY3sUBw3DMgb7kYs\nJJPkCPdSxCYq4Ef3z/Eao0tyUuCzyznfCMGJA1gBdTpwDNDCTaXqkjR5nvsdE5k0\nIVQgFPtcOPCpAoIBABujNlni+3OzLPdqWQq/LCDOiyoK8LKRj4FomhBgbWVPXNfx\nxPyPmJ+uh4bCV1dm1a4giHIgKlPqnPuOBNh4SF/Z79REmGMiiXP7IfvMu4DQi8K9\n4y4nnCVc93uvN5bRe4mywFhw0IqGd4sqVaVrSfdA124FTdbXng14HnVzbJncjpv+\nxr/ErDjbXy5AAbAGy3VbQsfxfbYMZ+Fc4fNzyJa2q+MQW8EzLlZOz2Frdty09lXB\nfSVDzzbgwTsLT1PPmrjq7z50C28teA6ShJZhV8WHgbm3MH2CSb2ov0BAJNXA04Ip\nsWbcKF9wBYYrHhddh2/qi9EQzJ4UVzf+ggRd3nkCggEAWcjyWjp4KRJcgJ65jwoz\nS7uYS6s7MsGYCOOw5S9kNC/mZDhH+ddK8kdAY1RIqbrL74qHmSQ+kgge7epMn9Mp\nW+/jXyDhm1t7wZ4jPRhisXTcF56ODpU9IR65PfTYPyvjHCkVbm+vOPt4ZxB9kNUD\n3G3xt9bNLXvILrBB66lLqjYDWAzwBy751Tb3hKDZTPv8rAP7Uttt8NhTUi8BWXsR\n/34fcRwlGWEAne9lrlIzQ2IofcXO+8fUgTa17ak+WJvVDINQKvGgAf4lHBFrixKP\nl2ZqsC1a4bz1+nuym6hQlkJ9xUBjHNGTA+FNbpTcd5qDbx9/+lf09D6dq45DbBb3\naQKCAQBrnFYocTm/fIeKo1n1kyF2ULkd6k984ztH8UyluXleSS1ShFFoo/x3vz35\nfsZNUggRnSe7/OBGZYquF/1roVULI1hKh4tbEmW4SWNeTFvwXKdRe6T7NnWSZtS/\nKtamA3lT2wtoEVOvfMo8M0hoFuRWdT2M0i+LKZQdRsq18XPLqdHt1kkSNcnPDERm\n4gLQ8zXTf2fHrtZmyM8fc0GuTVwprPFeJkLtSPehkeXSTgb6rpyelX9NBUILwRgP\nnw0+cbjFDFKaLnIrMFoVAAn/8DcnbbSt1TZhgNsMxY+GHWPBYW8SUi5nBmQQtmA7\nn3ju44acIPvJ9sWuZruVlWZGFaHm\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-tls/certs/redisCA.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-tls/certs/user.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhIwDQYJKoZIhvcNAQEL\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTQz\nNTAzWhcNMzExMDI2MTQzNTAzWjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBAKOod8jpFXqjtNvl0FgIkg0fSZbzvh7jbI7TEUVQ\nmyeZxjmB3fZh5f6dxM7TZ048CUOeUeq3lemDqay+Moku0rL4PsFNe8z1C1zHuhf9\n4Qw/f7rMBIZ73L4Y/7cPWfjZbeme06+D7HMBZGTWGHZCWrqZQOwA3hKBjC3VY/a5\nz6oP78+w18WDpnavGwXwgCd1yTOwz3tVJUOcJdjGv3iwrHABcGVfxUEKTabP+p6V\nHA/+w4AlCloS57GQCh0RWCXMyfekv6MGBaqQa6GtOK5ScLJ1YSlJ6PRoK2N+shbw\nL/kQGlilgYBVGOQgNKd94+PwJgOCy72S7p9yF3ZTBB4/51Bwl7IV74Om/GmqzJMx\nxY9/PPaxKlOkP+dW41/IrcDULdh0jAfe9rKdFf9/9NWA37S68pKFpzRuRrpLqIwm\nBPtHvtLnTbhgmS/O1Rwmxqs8r+VA6D8+/drAor/KAcCwgRiYLvhvl4ABoqj4toEK\njCXAR/jeoLAb8HDBzkot4hhJPjMhQMYX9/HfdK4YX359EkHdsO/+R6+ImXb68DS5\nzh0028ktMM+KEhWSffSmU3imZOrH1/TQfSxfzuTHvyd0HXAHvzx+w1VWNK4fqU8O\ntDbMt1GAaatrfrqwP4qTjzLEqtlJLIjg4qgzpYCRUvgVdxyeii9o7IeYT8I6Penf\nQpAJAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\nABb+A9C1AqstP5d2HXS/pIef1BNYAV8A/D2+nUpEXiautmjRZBRNwzBX2ffEZWV7\nEMkvqrRwz2ZgOJ4BRzJiDIYoF8dOhd0lc/0PqoR0xVzjOFDqd0ZcPHAjaY3UoBE7\njQSQ6ccc1tY5peNLAWCvRO6V9yhdV/SKGhveXGl/24MK9juwArnAitekCWZJQifT\nCFOJX5UvifrT8s0v0AqkycaNpkMvl0BAl4DRDJ3+EwZmzfOdATawyXBVXHt1Gz+N\niskPJAJsIjEdFYTjDUzwRN3bHFbTRXt2v1U18YIvMjvxq8MlITEC2lEW+3Xu90d3\naE/N9mLNJCgmZ2CGywWoaJlUXix2LTo5kT5coVVx0HK0tg5EcBua05qM3xO9Rgxv\nHkCnm/jMeN4oQ5o7h+q7UQja8mg1bjCzlt+RxqoA1snjglra/h5I8TTEhvSfxEy7\nh5Wiwne/TH/e8fN1IYRDvv602MNSZnAEPyG3Hc5xQOSGNpoKOZG7tpU+mRYIvlPe\nJgA5WNZ83y25JqSxF3kQuk7vrLByzEByqV3j+jIAQiHu/qIXwXUpkoV3L6A18yx/\nTbpQasr/bRFZKe83WlNl2ASAVyubal8ocmA0ua24/RV0I0VOCEXiIkl+pZ6e5Qn4\nL6Tryy5NxaEpUAZ9yv3P75PfNVQ3+vGYi3BLuhZUf/Dd\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/rte/oss-standalone-tls/certs/user.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCjqHfI6RV6o7Tb\n5dBYCJINH0mW874e42yO0xFFUJsnmcY5gd32YeX+ncTO02dOPAlDnlHqt5Xpg6ms\nvjKJLtKy+D7BTXvM9Qtcx7oX/eEMP3+6zASGe9y+GP+3D1n42W3pntOvg+xzAWRk\n1hh2Qlq6mUDsAN4SgYwt1WP2uc+qD+/PsNfFg6Z2rxsF8IAndckzsM97VSVDnCXY\nxr94sKxwAXBlX8VBCk2mz/qelRwP/sOAJQpaEuexkAodEVglzMn3pL+jBgWqkGuh\nrTiuUnCydWEpSej0aCtjfrIW8C/5EBpYpYGAVRjkIDSnfePj8CYDgsu9ku6fchd2\nUwQeP+dQcJeyFe+DpvxpqsyTMcWPfzz2sSpTpD/nVuNfyK3A1C3YdIwH3vaynRX/\nf/TVgN+0uvKShac0bka6S6iMJgT7R77S5024YJkvztUcJsarPK/lQOg/Pv3awKK/\nygHAsIEYmC74b5eAAaKo+LaBCowlwEf43qCwG/Bwwc5KLeIYST4zIUDGF/fx33Su\nGF9+fRJB3bDv/keviJl2+vA0uc4dNNvJLTDPihIVkn30plN4pmTqx9f00H0sX87k\nx78ndB1wB788fsNVVjSuH6lPDrQ2zLdRgGmra366sD+Kk48yxKrZSSyI4OKoM6WA\nkVL4FXccnoovaOyHmE/COj3p30KQCQIDAQABAoICADAiwPiq9dJYjD2RXrJF8w9B\nAJgRoP3cznVDx3SnvLrtE8yeUfbB3LADH3vl2iC8r8zfqCBtVv6T5zgTyTFoQDi7\no1mfvKYP/QORCz87QRIlKyB6GWqky8xt9eiV71SuPxHT0Vdyaf15j1nJTvCZm63+\nnYXMy4SN7fkdJoXPKTFP9q0TyqMhkbie0Efy8P6qOj+l5aDU7lzwdIFKE88fx9g5\n1CE9BfuXWDeUPJagLNzXhhEO0/iiTtt/Djp2e4LCtTTNlEAS6V+9kqq/FEjRnqwe\nsjE+t/ILIZfmD+OHSdTr05P3OhvQ671Na69H69uDKuslcV+U8/KZ0CTRTgjHqvUZ\neLNC8BZfAk8IZx637/rSlqPmxyS/j35vdslebTbWV2KM7jXPqSb9YokdoJ6M0NZX\nIYiMK2reVzjy2YvX1Nhp4Xn68il10XVS4P9tFxyNWdTclCbuSlTfgc27ercQMMgY\nfe7/8+A/QhV8tdly8W3HwTmvkmmWRSTMziI+zQzZmYYlAWb33rQYfMoHs4tEf2u2\nRf0Oso56X73sc3ncnOFm+s5iwTeUH6EgF3ephJX4nR3canmtpy40nbXUJ+tAuaAj\nuo56KNlPxIHKf96o2LGXGTrgbH39f0MebWOq/7YjtCg6sUbwuyyG3afLTHHuss13\n5bTJ5gD3rsiGUWjfY3oBAoIBAQDRR/BnDw501Hky32/Vhlt7Ef1iplru+Fh0yQj7\n2DQ+U+L1Ir4Q67ESH8qDnjkyLP1a8BDNOIEEGp5dBb+OHb/rwdb+RZ7OCIzFCQ/d\nWR7m0ucuPBQwytQb7iXa9w0umZwoeTXEGP8aGe+bSBIHv8/em26rkSx0A1rxr2/O\n1ho8xxgBmOxL3NSCnv56JUu/W0vFq/7OfWQ19SOvFahp3TeqR1gkHe76teWv11Pj\n+RdiIIdCOifWChZPEdgMZD4rl1cs9QQb+n+WkRt/mZgtTIRQIe+we+vIha7TW46X\n6A1DjSxV4WUSXvv10heYYpZkKzpNG9YOhRB3bvyDkRy11XZ5AoIBAQDIMUETtoa9\nEFNY+uieZwJCTWrrB1njLLRZS3eCAKsVegHD0txLG8H5VMkyZQErRe52zR9QXWU/\nU80tIO5BTbP3ME2AbjJvMwuiEe1lBKlVnn2JSGjbtzUMa1QBvDRmBEZkr8OneMN6\np2tX3L3Vw8Xm/97rjkAgo3gQkqyDf6VZ4xvH2Wo405yMywcoifMZXo/PN9fI5V8S\nfi3XjHrHzaY4cucbdaezVb4Zd0xwl+c6Ifw6+VtmRyfCEHk8yvSkoKWqdxtD0p3a\n3e8txYoI/YZltAICZ2vjZPv05Ts/VwWVzaxUArYiUH+k6J+6yCavKWesmeac0vLG\nyN07gpRPPsIRAoIBADIp+UDqxf9REsAT+L2I2BK27DKiR3eyhZlwuruLRnKOLv+t\nVTu/ExGSFzvXSERzrkMG+jAG1D4El2MaxqCtFtzO+Na4H2mpePydwHTBMPwJH6rg\nccKES7VqLx6+SyWZYmn9K9sWVseN4fYpn1DGNHBad3ueb7ZbO4hlEfrVLTLWUjXH\nzxQcGcA5liv3FqIGozH9mTUrr0KTwPrtyRGfGgGx2jnGBwuHYEf26D/j7Cv0Ohew\n0u2mO1S2pT/LI2/VderrzBFcyQpxO9MpIOXyymBe0hJOkeTdzlsRPivBTrSbeT4Y\nqd5ucByrQEahkwTtq6rh+jw+vwSx0MtElEotoZkCggEAB8ujNRlOdd5E4JokpMZu\nGBbbqvtGTMpY24FMzgsonlV57B4x5drW2taqXwP/36eBea7TIVYBs02YF8HIhVJ5\nR47h9bZU0G+0bEM2c1CTJ3pceRQQwT2JG0qyor6pa6+O7izJ+aOCOSx7yZgW7FQL\nSMt96r5HUP4MltifTx+RWMa3NjkJId1boz/kr3dvt/UutGsARBpqcVXogxQ9U7p2\nVoxi43bZaOpV1LgIifngTysznzhGjt0Gd1Ac6HkevapjyReKQEHbU8KApc+jaGY2\n7Y7s5RsR4HD2PrsOa5D/7q1roHnajcuErO9CCQvyNa/vEZGMoV61hXgc5UxYah2P\ngQKCAQEAkzISMmGPyQT7t6F/P2dFmrotAUU8gsEaWhrlkS0AuREXv1p14I1OnQhS\neWU7I9qSG4NfslRi5WUnowyawQKYibShtJ9/tOWMTaEELVTDtPAIu2y9kcquiG2j\no34vfpByz0w1vhmd/hwcPAvBFV+oaGN6lPz9Pv9MlNBLJoMhCPdr3aBJJuThT1Ka\nJQ/RT0XfU7XXSC74x7JwoKB4bobVHdON09yielC6w9wq9anqD18nrz/4wBwWDhDE\nKPxeXVpnIZfhukmWxkBY8NLAOFEenS3f6D4wzuOD25mPRSJQTngh7w9XkZYzDnOo\niwa43+YOKJx4Qh4SeXLBc/Udm1eMTA==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte/redis-enterprise/Dockerfile",
    "content": "FROM redislabs/redis:8.0.2-17\n\n## Set the env var to instruct RE to create a cluster on startup\nENV BOOTSTRAP_ACTION create_cluster\nENV BOOTSTRAP_CLUSTER_FQDN cluster.local\n\nCOPY entrypoint.sh db.json ./\n\nENTRYPOINT [ \"bash\", \"./entrypoint.sh\" ]\n"
  },
  {
    "path": "tests/e2e/rte/redis-enterprise/db.json",
    "content": "{\n  \"name\": \"test-re-standalone\",\n  \"type\": \"redis\",\n  \"memory_size\": 268435456,\n  \"port\": 12000\n}\n"
  },
  {
    "path": "tests/e2e/rte/redis-enterprise/entrypoint.sh",
    "content": "#! /bin/bash\n\nTEST_RE_USER=${TEST_RE_USER:-\"demo@redislabs.com\"}\nTEST_RE_PASS=${TEST_RE_PASS:-\"123456\"}\n\nset -e\n\n# enable job control\nset -m\n\n/opt/start.sh &\n\n# This command queries the REST API and outputs the status code\nCURL_CMD=\"curl --silent --fail --output /dev/null -i -w %{http_code} -u $TEST_RE_USER:$TEST_RE_PASS -k https://localhost:9443/v1/nodes\"\n\n# Wait to get 2 consecutive 200 responses from the REST API\nwhile true\ndo\n    echo yay $CURL_CMD\n    CURL_CMD_OUTPUT=$($CURL_CMD || true)\n    if [ $CURL_CMD_OUTPUT == \"200\" ]\n    then\n        echo \"Got 200 response, trying again in 5 seconds to verify...\"\n        sleep 5\n        if [ $($CURL_CMD || true) == \"200\" ]\n        then\n            echo \"Got 200 response after 5 seconds again, proceeding...\"\n            break\n        fi\n    else\n        echo \"Did not get 200 response, got $CURL_CMD_OUTPUT, trying again in 10 seconds...\"\n        sleep 10\n    fi\ndone\n\necho \"Creating databases...\"\n\ncurl -k -u \"$TEST_RE_USER:$TEST_RE_PASS\" --request POST --url \"https://localhost:9443/v1/bdbs\" --header 'content-type: application/json' --data-binary \"@db.json\"\n\n# now we bring the primary process back into the foreground\n# and leave it there\nfg\n"
  },
  {
    "path": "tests/e2e/rte/ssh/keys/pub/test.pub",
    "content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPXS0xkxY7o+MUNBJJnf6fKh6AFFpzB0YIfifHSSseXw\n"
  },
  {
    "path": "tests/e2e/rte/ssh/keys/pub/testp.pub",
    "content": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEs/ewkUXl0+uDr7hxSM2vURqdRNFHm7+x05azzW/Yzu\n"
  },
  {
    "path": "tests/e2e/rte/ssh/keys/test",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8AAAAKBv1saEb9bG\nhAAAAAtzc2gtZWQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8A\nAAAEDyew1DnmWamAr0OrUM87FauJfFfea+pi8ctpKNnurNi/XS0xkxY7o+MUNBJJnf6fKh\n6AFFpzB0YIfifHSSseXwAAAAG3pvem9Aem96by1IUC1Qcm9Cb29rLTQ1MC1HNwEC\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte/ssh/keys/testp",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBPcEHCGN\nDrMHhpQnPwc0XwAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEs/ewkUXl0+uDr7\nhxSM2vURqdRNFHm7+x05azzW/YzuAAAAoEhNzctHXM6YBV0z4zzvdniQ5cLwsv8TfMZp2G\nWUhZU05yugvKlRu1pml5q3XGSP5wYCF4vvi4BE563PMDKZWAqFFGtiTotEn+XuD/eP+P8H\nxdf91tV5kE+1yvVwxUNMcijHY0uYopnG2NN3bdjOH/4YmW0WLyDu10EoMZKVnrP0qBbOrR\nxKIy5lqa39SrAnUnGSoTEJsEWGLiIS2rBhkVc=\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/rte.critical-path.docker-compose.yml",
    "content": "x-logging:\n  logging: &logging\n    driver: none\n\nservices:\n  oss-standalone:\n    logging: *logging\n    image: redis:8.2.1-alpine\n    ports:\n      - \"8100:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.109\n      ssh:\n        ipv4_address: 172.33.100.109\n\n  oss-standalone-v5:\n    logging: *logging\n    image: redis:5\n    ports:\n      - \"8101:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.111\n      ssh:\n        ipv4_address: 172.33.100.111\n\n  # todo: Investigate if we can have lower size of data (2.6GB currently)\n  oss-standalone-big:\n    logging: *logging\n    build:\n      context: ./rte/oss-standalone-big\n      dockerfile: Dockerfile\n      args:\n        TEST_DB_DUMP: $TEST_BIG_DB_DUMP\n    ports:\n      - \"8103:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.116\n      ssh:\n        ipv4_address: 172.33.100.116\n\n  # todo: investigate the Redis version and if we need it for critical-path tests\n  oss-standalone-tls:\n    logging: *logging\n    build:\n      context: ./rte/oss-standalone-tls\n      dockerfile: Dockerfile\n    ports:\n      - \"8104:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.117\n      ssh:\n        ipv4_address: 172.33.100.117\n\n  # todo: investigate the Redis version and Graph module tests based on it\n  oss-standalone-empty:\n    logging: *logging\n    image: redislabs/redismod\n    command: [\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisearch.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisgraph.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redistimeseries.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/rejson.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisbloom.so\"\n    ]\n    ports:\n      - \"8105:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.110\n      ssh:\n        ipv4_address: 172.33.100.110\n\n  # todo: investigate if we need this. EOL\n  oss-standalone-redisgears-2-0:\n    logging: *logging\n    image: redislabs/redisgears:edge\n    ports:\n      - \"8106:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.115\n      ssh:\n        ipv4_address: 172.33.100.115\n\n  # todo: investigate base Redis version and if we need this in critical-path tests\n  # ========== oss cluster (v7) ==========\n  cluster-plain-creator-7:\n    logging: *logging\n    build:\n      context: ./rte/oss-cluster-7\n      dockerfile: creator.Dockerfile\n    depends_on:\n      - master-plain-7-1\n      - master-plain-7-2\n      - master-plain-7-3\n  master-plain-7-1:\n    logging: *logging\n    build: &cluster-plain-7-build ./rte/oss-cluster-7\n    ports:\n      - \"8200:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.211\n      ssh:\n        ipv4_address: 172.33.100.211\n  master-plain-7-2:\n    logging: *logging\n    build: *cluster-plain-7-build\n    networks:\n      default:\n        ipv4_address: 172.31.100.212\n  master-plain-7-3:\n    logging: *logging\n    build: *cluster-plain-7-build\n    networks:\n      default:\n        ipv4_address: 172.31.100.213\n  # ========== END oss cluster (v7) ==========\n\n  # todo: investigate base Redis version and if we need this in critical-path tests\n  # ========== oss sentinel ==========\n  oss-sentinel:\n    logging: *logging\n    build: ./rte/oss-sentinel\n    depends_on:\n      - oss-sentinel-primary-1\n      - oss-sentinel-primary-2\n    ports:\n      - \"28100:26379\"\n\n  oss-sentinel-primary-1:\n    logging: *logging\n    image: redis:5\n\n  oss-sentinel-primary-2:\n    logging: *logging\n    image: redis:5\n  # ========== END oss sentinel ==========\n"
  },
  {
    "path": "tests/e2e/rte.docker-compose.yml",
    "content": "version: \"3.4\"\n\nx-logging:\n  logging: &logging\n    driver: none\n\nservices:\n  static-server:\n    logging: *logging\n    build:\n      context: .\n      dockerfile: static-server.Dockerfile\n    volumes:\n      - ./remote:/app/remote\n    ports:\n      - 5551:5551\n    networks:\n      default:\n        ipv4_address: 172.31.100.131\n      ssh:\n        ipv4_address: 172.33.100.131\n  # RDI mocked\n  # rdi:\n  #   logging: *logging\n  #   build:\n  #     context: rte/rdi\n  #     dockerfile: Dockerfile\n  #   volumes:\n  #     - ./rdi:/data\n  #   ports:\n  #     - 4000:4000\n  # ssh\n  ssh:\n    logging: *logging\n    image: lscr.io/linuxserver/openssh-server:9.7_p1-r4-ls172\n    environment:\n      - PASSWORD_ACCESS=true\n      - USER_PASSWORD=pass\n      - USER_NAME=u\n      - DOCKER_MODS=linuxserver/mods:openssh-server-ssh-tunnel\n      - PUBLIC_KEY_DIR=/keys/pub\n    volumes:\n      - ./rte/ssh/keys:/keys\n    ports:\n      - 2222:2222\n    networks:\n      default:\n        ipv4_address: 172.31.100.245\n      ssh:\n        ipv4_address: 172.33.100.245\n\n  # oss standalone\n  oss-standalone:\n    logging: *logging\n    image: redislabs/redismod\n    command: [\n        \"--loadmodule\", \"/usr/lib/redis/modules/redisearch.so\",\n        \"--loadmodule\", \"/usr/lib/redis/modules/redisgraph.so\",\n        \"--loadmodule\", \"/usr/lib/redis/modules/redistimeseries.so\",\n        \"--loadmodule\", \"/usr/lib/redis/modules/rejson.so\",\n        \"--loadmodule\", \"/usr/lib/redis/modules/redisbloom.so\"\n    ]\n    ports:\n      - 8100:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.109\n      ssh:\n        ipv4_address: 172.33.100.109\n\n  oss-standalone-empty:\n    logging: *logging\n    image: redislabs/redismod\n    command: [\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisearch.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisgraph.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redistimeseries.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/rejson.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisbloom.so\"\n    ]\n    ports:\n      - 8105:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.110\n      ssh:\n        ipv4_address: 172.33.100.110\n\n  # oss standalone v5\n  oss-standalone-v5:\n    logging: *logging\n    image: redis:5\n    ports:\n      - 8101:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.111\n      ssh:\n        ipv4_address: 172.33.100.111\n\n  # oss standalone v7\n  oss-standalone-v7:\n    logging: *logging\n    image: redis:7.4-rc2\n    ports:\n      - 8108:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.112\n      ssh:\n        ipv4_address: 172.33.100.112\n\n  # oss standalone v8\n  oss-standalone-v8:\n    logging: *logging\n    image: redis:8.0-M02\n    ports:\n      - 8109:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.113\n      ssh:\n        ipv4_address: 172.33.100.113\n\n  oss-standalone-redisgears-2-0:\n    logging: *logging\n    image: redislabs/redisgears:edge\n    ports:\n      - 8106:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.115\n      ssh:\n        ipv4_address: 172.33.100.115\n\n  oss-standalone-big:\n    logging: *logging\n    build:\n      context: ./rte/oss-standalone-big\n      dockerfile: Dockerfile\n      args:\n        TEST_DB_DUMP: $TEST_BIG_DB_DUMP\n    ports:\n      - 8103:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.116\n      ssh:\n        ipv4_address: 172.33.100.116\n\n  # oss standalone tls\n  oss-standalone-tls:\n    logging: *logging\n    build:\n      context: ./rte/oss-standalone-tls\n      dockerfile: Dockerfile\n    ports:\n      - 8104:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.117\n      ssh:\n        ipv4_address: 172.33.100.117\n\n  # oss sentinel\n  oss-sentinel:\n    logging: *logging\n    build: ./rte/oss-sentinel\n    depends_on:\n      - oss-sentinel-primary-1\n      - oss-sentinel-primary-2\n    ports:\n      - 28100:26379\n\n  oss-sentinel-primary-1:\n    logging: *logging\n    image: redis:5\n\n  oss-sentinel-primary-2:\n    logging: *logging\n    image: redis:5\n\n  # oss cluster (v7)\n  cluster-plain-creator-7:\n    logging: *logging\n    build:\n      context: ./rte/oss-cluster-7\n      dockerfile: creator.Dockerfile\n    depends_on:\n      - master-plain-7-1\n      - master-plain-7-2\n      - master-plain-7-3\n  master-plain-7-1:\n    logging: *logging\n    build: &cluster-plain-7-build ./rte/oss-cluster-7\n    ports:\n      - 8200:6379\n      - 18200:16379\n    command: redis-server /etc/redis/redis.conf --cluster-announce-ip host.docker.internal --cluster-announce-port 8200 --cluster-announce-bus-port 18200\n    networks:\n      default:\n        ipv4_address: 172.31.100.211\n      ssh:\n        ipv4_address: 172.33.100.211\n  master-plain-7-2:\n    logging: *logging\n    build: *cluster-plain-7-build\n    ports:\n      - 8201:6379\n      - 18201:16379\n    command: redis-server /etc/redis/redis.conf --cluster-announce-ip host.docker.internal --cluster-announce-port 8201 --cluster-announce-bus-port 18201\n    networks:\n      default:\n        ipv4_address: 172.31.100.212\n  master-plain-7-3:\n    logging: *logging\n    build: *cluster-plain-7-build\n    ports:\n      - 8202:6379\n      - 18202:16379\n    command: redis-server /etc/redis/redis.conf --cluster-announce-ip host.docker.internal --cluster-announce-port 8202 --cluster-announce-bus-port 18202\n    networks:\n      default:\n        ipv4_address: 172.31.100.213\n\n  # oss cluster (v7) with rediserch > 2.2\n  cluster-rs-creator-7:\n    logging: *logging\n    build:\n      context: &cluster-rs-7-build ./rte/oss-cluster-7-rs\n      dockerfile: creator.Dockerfile\n    depends_on:\n      - master-rs-7-1\n      - master-rs-7-2\n      - master-rs-7-3\n  master-rs-7-1:\n    logging: *logging\n    build: *cluster-rs-7-build\n    ports:\n      - 8221:6379\n    networks:\n      default:\n        ipv4_address: 172.31.100.221\n  master-rs-7-2:\n    logging: *logging\n    build: *cluster-rs-7-build\n    networks:\n      default:\n        ipv4_address: 172.31.100.222\n  master-rs-7-3:\n    logging: *logging\n    build: *cluster-rs-7-build\n    networks:\n      default:\n        ipv4_address: 172.31.100.223\n\n  # oss cluster with redisgears 2\n  gears-cluster-2-0-creator:\n    logging: *logging\n    image: redis:latest\n    entrypoint: ['/bin/sh', '-c', 'redis-cli --cluster create 172.31.100.191:6379 172.31.100.192:6379 172.31.100.193:6379 172.31.100.194:6379 172.31.100.195:6379 172.31.100.196:6379 --cluster-replicas 1 --cluster-yes && tail -f /dev/null']\n    depends_on:\n      - gears-cluster-2-0-node-1\n      - gears-cluster-2-0-node-2\n      - gears-cluster-2-0-node-3\n      - gears-cluster-2-0-node-4\n      - gears-cluster-2-0-node-5\n      - gears-cluster-2-0-node-6\n  gears-cluster-2-0-node-1:\n    logging: *logging\n    image: &gears-cluster-img redislabs/redisgears:edge\n    command: &gears-cluster-cmd redis-server --protected-mode no --loadmodule /build/target/release/libredisgears.so v8-plugin-path /build/target/release/libredisgears_v8_plugin.so --cluster-enabled yes\n    networks:\n      default:\n        ipv4_address: 172.31.100.191\n  gears-cluster-2-0-node-2:\n    logging: *logging\n    image: *gears-cluster-img\n    command: *gears-cluster-cmd\n    networks:\n      default:\n        ipv4_address: 172.31.100.192\n  gears-cluster-2-0-node-3:\n    logging: *logging\n    image: *gears-cluster-img\n    command: *gears-cluster-cmd\n    networks:\n      default:\n        ipv4_address: 172.31.100.193\n  gears-cluster-2-0-node-4:\n    logging: *logging\n    image: *gears-cluster-img\n    command: *gears-cluster-cmd\n    networks:\n      default:\n        ipv4_address: 172.31.100.194\n  gears-cluster-2-0-node-5:\n    logging: *logging\n    image: *gears-cluster-img\n    command: *gears-cluster-cmd\n    networks:\n      default:\n        ipv4_address: 172.31.100.195\n  gears-cluster-2-0-node-6:\n    logging: *logging\n    image: *gears-cluster-img\n    command: *gears-cluster-cmd\n    networks:\n      default:\n        ipv4_address: 172.31.100.196\n\n  # redis enterprise\n  redis-enterprise:\n    logging: *logging\n    platform: linux/amd64\n    build: ./rte/redis-enterprise\n    cap_add:\n      - sys_resource\n    ports:\n      - 19443:9443\n      - 12000:12000\nnetworks:\n  default:\n    name: \"e2e-private-network\"\n    ipam:\n      driver: default\n      config:\n        - subnet: 172.31.100.0/24\n          gateway: 172.31.100.1\n  ssh:\n    name: \"e2e-ssh-network\"\n    ipam:\n      driver: default\n      config:\n        - subnet: 172.33.100.0/24\n          gateway: 172.33.100.1\n"
  },
  {
    "path": "tests/e2e/rte.networks.docker-compose.yml",
    "content": "networks:\n  default:\n    ipam:\n      driver: default\n      config:\n        - subnet: 172.31.100.0/24\n          gateway: 172.31.100.1\n  ssh:\n    ipam:\n      driver: default\n      config:\n        - subnet: 172.33.100.0/24\n          gateway: 172.33.100.1\n"
  },
  {
    "path": "tests/e2e/rte.regression.docker-compose.yml",
    "content": "x-logging:\n  logging: &logging\n    driver: none\n\nservices:\n  oss-standalone:\n    logging: *logging\n    image: redis:8.2.1-alpine\n    ports:\n      - \"8100:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.109\n      ssh:\n        ipv4_address: 172.33.100.109\n\n  # todo: Investigate if we can have lower size of data (2.6GB currently)\n  oss-standalone-big:\n    logging: *logging\n    build:\n      context: ./rte/oss-standalone-big\n      dockerfile: Dockerfile\n      args:\n        TEST_DB_DUMP: $TEST_BIG_DB_DUMP\n    ports:\n      - \"8103:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.116\n      ssh:\n        ipv4_address: 172.33.100.116\n\n  # todo: review Redis version\n  # oss standalone tls\n  oss-standalone-tls:\n    logging: *logging\n    build:\n      context: ./rte/oss-standalone-tls\n      dockerfile: Dockerfile\n    ports:\n      - \"8104:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.117\n      ssh:\n        ipv4_address: 172.33.100.117\n\n  # todo: investigate the Redis version and Graph module tests based on it\n  oss-standalone-empty:\n    logging: *logging\n    image: redislabs/redismod\n    command: [\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisearch.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisgraph.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redistimeseries.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/rejson.so\",\n      \"--loadmodule\", \"/usr/lib/redis/modules/redisbloom.so\"\n    ]\n    ports:\n      - \"8105:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.110\n      ssh:\n        ipv4_address: 172.33.100.110\n\n  # todo: investigate if we need this. EOL\n  oss-standalone-redisgears-2-0:\n    logging: *logging\n    image: redislabs/redisgears:edge\n    ports:\n      - \"8106:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.115\n      ssh:\n        ipv4_address: 172.33.100.115\n\n  oss-standalone-v7:\n    logging: *logging\n    image: redis:7.4-rc2\n    ports:\n      - \"8108:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.112\n      ssh:\n        ipv4_address: 172.33.100.112\n\n  # todo: investigate base Redis version and if we need this in critical-path tests\n  # ========== oss cluster (v7) ==========\n  cluster-plain-creator-7:\n    logging: *logging\n    build:\n      context: ./rte/oss-cluster-7\n      dockerfile: creator.Dockerfile\n    depends_on:\n      - master-plain-7-1\n      - master-plain-7-2\n      - master-plain-7-3\n  master-plain-7-1:\n    logging: *logging\n    build: &cluster-plain-7-build ./rte/oss-cluster-7\n    ports:\n      - \"8200:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.211\n      ssh:\n        ipv4_address: 172.33.100.211\n  master-plain-7-2:\n    logging: *logging\n    build: *cluster-plain-7-build\n    networks:\n      default:\n        ipv4_address: 172.31.100.212\n  master-plain-7-3:\n    logging: *logging\n    build: *cluster-plain-7-build\n    networks:\n      default:\n        ipv4_address: 172.31.100.213\n  # ========== END oss cluster (v7) ==========\n\n  # todo: investigate base Redis version and if we need this in critical-path tests\n  # ========== oss sentinel ==========\n  oss-sentinel:\n    logging: *logging\n    build: ./rte/oss-sentinel\n    depends_on:\n      - oss-sentinel-primary-1\n      - oss-sentinel-primary-2\n    ports:\n      - \"28100:26379\"\n\n  oss-sentinel-primary-1:\n    logging: *logging\n    image: redis:5\n\n  oss-sentinel-primary-2:\n    logging: *logging\n    image: redis:5\n  # ========== END oss sentinel ==========\n"
  },
  {
    "path": "tests/e2e/rte.smoke.docker-compose.yml",
    "content": "x-logging:\n  logging: &logging\n    driver: none\n\nservices:\n  oss-standalone:\n    logging: *logging\n    image: redis:8.2.1-alpine\n    ports:\n      - \"8100:6379\"\n    networks:\n      default:\n        ipv4_address: 172.31.100.109\n      ssh:\n        ipv4_address: 172.33.100.109\n"
  },
  {
    "path": "tests/e2e/static-server.Dockerfile",
    "content": "FROM node:20.14-alpine\n\nWORKDIR /app\n\nCOPY package.json .\nRUN yarn add express fs-extra\nCOPY . .\n\nCMD [\"node\", \"static.ts\"]\n"
  },
  {
    "path": "tests/e2e/static.ts",
    "content": "const express = require('express');\n\nconst app = express();\napp.use('/remote', express.static('./remote'));\n\napp.listen(5551);\n"
  },
  {
    "path": "tests/e2e/test-data/big-json/big-json.json",
    "content": "{\n  \"_id\": \"666c56b9f4f88278244b6dc8\",\n  \"index\": 0,\n  \"guid\": \"1743c04d-b95a-4ab9-9e8c-51b0e9b7e009\",\n  \"isActive\": true,\n  \"balance\": \"$2,469.16\",\n  \"picture\": \"http://placehold.it/32x32\",\n  \"age\": 32,\n  \"eyeColor\": \"green\",\n  \"name\": \"Camille Sims\",\n  \"gender\": \"female\",\n  \"company\": \"ZBOO\",\n  \"email\": \"camillesims@zboo.com\",\n  \"phone\": \"+1 (942) 411-2857\",\n  \"address\": \"817 Graham Avenue, Ezel, South Carolina, 5845\",\n  \"about\": \"Ut mollit elit eu veniam non do ut in aliquip. Lorem culpa laborum non aliqua non eiusmod amet cupidatat sunt officia elit elit. Ut officia elit adipisicing labore ex deserunt. Amet sit excepteur quis ut adipisicing occaecat proident occaecat in minim.\\r\\n\",\n  \"registered\": \"2017-08-30T02:25:04 -02:00\",\n  \"latitude\": 7.648678,\n  \"longitude\": 109.700128,\n  \"tags\": [\n    \"sit\",\n    \"irure\",\n    \"nisi\",\n    \"ullamco\",\n    \"sint\",\n    \"consequat\",\n    \"do\",\n    \"ad\",\n    \"elit\",\n    \"deserunt\",\n    \"ipsum\",\n    \"nisi\",\n    \"pariatur\",\n    \"sit\",\n    \"non\",\n    \"ad\",\n    \"do\",\n    \"nulla\",\n    \"ad\",\n    \"irure\",\n    \"ipsum\",\n    \"incididunt\",\n    \"Lorem\",\n    \"enim\",\n    \"proident\",\n    \"veniam\",\n    \"in\",\n    \"reprehenderit\",\n    \"irure\",\n    \"pariatur\",\n    \"cupidatat\",\n    \"ipsum\",\n    \"sit\",\n    \"nisi\",\n    \"irure\",\n    \"ipsum\",\n    \"cillum\",\n    \"adipisicing\",\n    \"reprehenderit\",\n    \"occaecat\",\n    \"irure\",\n    \"irure\",\n    \"amet\",\n    \"reprehenderit\",\n    \"voluptate\",\n    \"veniam\",\n    \"cillum\",\n    \"incididunt\",\n    \"magna\",\n    \"laboris\",\n    \"nisi\",\n    \"duis\",\n    \"irure\",\n    \"do\",\n    \"amet\",\n    \"aliquip\",\n    \"commodo\",\n    \"nisi\",\n    \"pariatur\",\n    \"sunt\",\n    \"dolor\",\n    \"ut\",\n    \"ea\",\n    \"voluptate\",\n    \"laboris\",\n    \"pariatur\",\n    \"qui\",\n    \"duis\",\n    \"commodo\",\n    \"deserunt\"\n  ],\n  \"friends\": [\n    {\n      \"id\": 0,\n      \"name\": \"Erna Lynch\"\n    },\n    {\n      \"id\": 1,\n      \"name\": \"Vicky Vega\"\n    },\n    {\n      \"id\": 2,\n      \"name\": \"Haley Meyer\"\n    },\n    {\n      \"id\": 3,\n      \"name\": \"Darlene Knight\"\n    },\n    {\n      \"id\": 4,\n      \"name\": \"Odonnell Cain\"\n    },\n    {\n      \"id\": 5,\n      \"name\": \"Hopper Sellers\"\n    },\n    {\n      \"id\": 6,\n      \"name\": \"Knapp Barry\"\n    },\n    {\n      \"id\": 7,\n      \"name\": \"Alvarado Clements\"\n    },\n    {\n      \"id\": 8,\n      \"name\": \"Wolfe Taylor\"\n    },\n    {\n      \"id\": 9,\n      \"name\": \"Janell Chambers\"\n    },\n    {\n      \"id\": 10,\n      \"name\": \"Merle Wood\"\n    },\n    {\n      \"id\": 11,\n      \"name\": \"Ellen Savage\"\n    },\n    {\n      \"id\": 12,\n      \"name\": \"Elliott Humphrey\"\n    },\n    {\n      \"id\": 13,\n      \"name\": \"Ayers Chase\"\n    },\n    {\n      \"id\": 14,\n      \"name\": \"Robbie Kirkland\"\n    },\n    {\n      \"id\": 15,\n      \"name\": \"Whitney Perkins\"\n    },\n    {\n      \"id\": 16,\n      \"name\": \"Marisa Lloyd\"\n    },\n    {\n      \"id\": 17,\n      \"name\": \"Helene Case\"\n    },\n    {\n      \"id\": 18,\n      \"name\": \"Berry Tyler\"\n    },\n    {\n      \"id\": 19,\n      \"name\": \"Frieda Cannon\"\n    },\n    {\n      \"id\": 20,\n      \"name\": \"Sonya Leon\"\n    },\n    {\n      \"id\": 21,\n      \"name\": \"Wilda Glass\"\n    },\n    {\n      \"id\": 22,\n      \"name\": \"Sims Kaufman\"\n    },\n    {\n      \"id\": 23,\n      \"name\": \"Ballard Preston\"\n    },\n    {\n      \"id\": 24,\n      \"name\": \"Carroll Clay\"\n    },\n    {\n      \"id\": 25,\n      \"name\": \"Medina Curry\"\n    },\n    {\n      \"id\": 26,\n      \"name\": \"Beatriz Herrera\"\n    },\n    {\n      \"id\": 27,\n      \"name\": \"Marianne Vargas\"\n    },\n    {\n      \"id\": 28,\n      \"name\": \"Galloway Stanton\"\n    },\n    {\n      \"id\": 29,\n      \"name\": \"Rhea Marshall\"\n    }\n  ],\n  \"greeting\": \"Hello, Camille Sims! You have 10 unread messages.\",\n  \"favoriteFruit\": \"strawberry\"\n}"
  },
  {
    "path": "tests/e2e/test-data/big-json/json-BigInt.json",
    "content": "{\n  \"result\": [ \n    {\n      \"message\": 1234567899876543211111111,\n      \"phoneNumber\": \"(870) 933-5398 x28029\",\n      \"phoneVariation\": \"+90 363 988 10 51\",\n      \"status\": \"disabled\",\n      \"name\": {\n        \"first\": \"Hudson\",\n        \"middle\": \"Marlowe\",\n        \"last\": \"Romaguera\"\n      },\n      \"username\": \"Hudson-Romaguera\",\n      \"password\": \"15dmjQlqS2ZBhVq\",\n      \"emails\": [\n        \"Van11@gmail.com\",\n        \"Elissa.VonRueden82@example.com\"\n      ],\n      \"location\": {\n        \"street\": \"9109 2nd Avenue\",\n        \"city\": \"Howeview\",\n        \"state\": \"North Dakota\",\n        \"country\": \"Saint Helena\",\n        \"zip\": \"62862-7589\",\n        \"coordinates\": {\n          \"latitude\": \"81.1514\",\n          \"longitude\": \"-134.2206\"\n        }\n      },\n      \"website\": \"https://skinny-barn.info\",\n      \"domain\": \"responsible-notebook.net\",\n      \"job\": {\n        \"title\": \"Internal Solutions Officer\",\n        \"descriptor\": \"Principal\",\n        \"area\": \"Branding\",\n        \"type\": \"Developer\",\n        \"company\": \"Shields Group\"\n      },\n      \"creditCard\": {\n        \"number\": \"3409-218740-43272\",\n        \"cvv\": \"909\",\n        \"issuer\": \"visa\"\n      },\n      \"uuid\": \"91d6042d-8453-440f-9bf1-7412fee085ee\",\n      \"objectId\": 123456789987654321\n    }\n  ]\n}"
  },
  {
    "path": "tests/e2e/test-data/bulk-upload/bigKeysData.rtf",
    "content": "SET Key0 Value0\nSET Key1 Value1\nSET Key2 Value2\nSET Key3 Value3\nSET Key4 Value4\nSET Key5 Value5\nSET Key6 Value6\nSET Key7 Value7\nSET Key8 Value8\nSET Key9 Value9\nSET Key10 Value10\nSET Key11 Value11\nSET Key12 Value12\nSET Key13 Value13\nSET Key14 Value14\nSET Key15 Value15\nSET Key16 Value16\nSET Key17 Value17\nSET Key18 Value18\nSET Key19 Value19\nSET Key20 Value20\nSET Key21 Value21\nSET Key22 Value22\nSET Key23 Value23\nSET Key24 Value24\nSET Key25 Value25\nSET Key26 Value26\nSET Key27 Value27\nSET Key28 Value28\nSET Key29 Value29\nSET Key30 Value30\nSET Key31 Value31\nSET Key32 Value32\nSET Key33 Value33\nSET Key34 Value34\nSET Key35 Value35\nSET Key36 Value36\nSET Key37 Value37\nSET Key38 Value38\nSET Key39 Value39\nSET Key40 Value40\nSET Key41 Value41\nSET Key42 Value42\nSET Key43 Value43\nSET Key44 Value44\nSET Key45 Value45\nSET Key46 Value46\nSET Key47 Value47\nSET Key48 Value48\nSET Key49 Value49\nSET Key50 Value50\nSET Key51 Value51\nSET Key52 Value52\nSET Key53 Value53\nSET Key54 Value54\nSET Key55 Value55\nSET Key56 Value56\nSET Key57 Value57\nSET Key58 Value58\nSET Key59 Value59\nSET Key60 Value60\nSET Key61 Value61\nSET Key62 Value62\nSET Key63 Value63\nSET Key64 Value64\nSET Key65 Value65\nSET Key66 Value66\nSET Key67 Value67\nSET Key68 Value68\nSET Key69 Value69\nSET Key70 Value70\nSET Key71 Value71\nSET Key72 Value72\nSET Key73 Value73\nSET Key74 Value74\nSET Key75 Value75\nSET Key76 Value76\nSET Key77 Value77\nSET Key78 Value78\nSET Key79 Value79\nSET Key80 Value80\nSET Key81 Value81\nSET Key82 Value82\nSET Key83 Value83\nSET Key84 Value84\nSET Key85 Value85\nSET Key86 Value86\nSET Key87 Value87\nSET Key88 Value88\nSET Key89 Value89\nSET Key90 Value90\nSET Key91 Value91\nSET Key92 Value92\nSET Key93 Value93\nSET Key94 Value94\nSET Key95 Value95\nSET Key96 Value96\nSET Key97 Value97\nSET Key98 Value98\nSET Key99 Value99\nSET Key100 Value100\nSET Key101 Value101\nSET Key102 Value102\nSET Key103 Value103\nSET Key104 Value104\nSET Key105 Value105\nSET Key106 Value106\nSET Key107 Value107\nSET Key108 Value108\nSET Key109 Value109\nSET Key110 Value110\nSET Key111 Value111\nSET Key112 Value112\nSET Key113 Value113\nSET Key114 Value114\nSET Key115 Value115\nSET Key116 Value116\nSET Key117 Value117\nSET Key118 Value118\nSET Key119 Value119\nSET Key120 Value120\nSET Key121 Value121\nSET Key122 Value122\nSET Key123 Value123\nSET Key124 Value124\nSET Key125 Value125\nSET Key126 Value126\nSET Key127 Value127\nSET Key128 Value128\nSET Key129 Value129\nSET Key130 Value130\nSET Key131 Value131\nSET Key132 Value132\nSET Key133 Value133\nSET Key134 Value134\nSET Key135 Value135\nSET Key136 Value136\nSET Key137 Value137\nSET Key138 Value138\nSET Key139 Value139\nSET Key140 Value140\nSET Key141 Value141\nSET Key142 Value142\nSET Key143 Value143\nSET Key144 Value144\nSET Key145 Value145\nSET Key146 Value146\nSET Key147 Value147\nSET Key148 Value148\nSET Key149 Value149\nSET Key150 Value150\nSET Key151 Value151\nSET Key152 Value152\nSET Key153 Value153\nSET Key154 Value154\nSET Key155 Value155\nSET Key156 Value156\nSET Key157 Value157\nSET Key158 Value158\nSET Key159 Value159\nSET Key160 Value160\nSET Key161 Value161\nSET Key162 Value162\nSET Key163 Value163\nSET Key164 Value164\nSET Key165 Value165\nSET Key166 Value166\nSET Key167 Value167\nSET Key168 Value168\nSET Key169 Value169\nSET Key170 Value170\nSET Key171 Value171\nSET Key172 Value172\nSET Key173 Value173\nSET Key174 Value174\nSET Key175 Value175\nSET Key176 Value176\nSET Key177 Value177\nSET Key178 Value178\nSET Key179 Value179\nSET Key180 Value180\nSET Key181 Value181\nSET Key182 Value182\nSET Key183 Value183\nSET Key184 Value184\nSET Key185 Value185\nSET Key186 Value186\nSET Key187 Value187\nSET Key188 Value188\nSET Key189 Value189\nSET Key190 Value190\nSET Key191 Value191\nSET Key192 Value192\nSET Key193 Value193\nSET Key194 Value194\nSET Key195 Value195\nSET Key196 Value196\nSET Key197 Value197\nSET Key198 Value198\nSET Key199 Value199\nSET Key200 Value200\nSET Key201 Value201\nSET Key202 Value202\nSET Key203 Value203\nSET Key204 Value204\nSET Key205 Value205\nSET Key206 Value206\nSET Key207 Value207\nSET Key208 Value208\nSET Key209 Value209\nSET Key210 Value210\nSET Key211 Value211\nSET Key212 Value212\nSET Key213 Value213\nSET Key214 Value214\nSET Key215 Value215\nSET Key216 Value216\nSET Key217 Value217\nSET Key218 Value218\nSET Key219 Value219\nSET Key220 Value220\nSET Key221 Value221\nSET Key222 Value222\nSET Key223 Value223\nSET Key224 Value224\nSET Key225 Value225\nSET Key226 Value226\nSET Key227 Value227\nSET Key228 Value228\nSET Key229 Value229\nSET Key230 Value230\nSET Key231 Value231\nSET Key232 Value232\nSET Key233 Value233\nSET Key234 Value234\nSET Key235 Value235\nSET Key236 Value236\nSET Key237 Value237\nSET Key238 Value238\nSET Key239 Value239\nSET Key240 Value240\nSET Key241 Value241\nSET Key242 Value242\nSET Key243 Value243\nSET Key244 Value244\nSET Key245 Value245\nSET Key246 Value246\nSET Key247 Value247\nSET Key248 Value248\nSET Key249 Value249\nSET Key250 Value250\nSET Key251 Value251\nSET Key252 Value252\nSET Key253 Value253\nSET Key254 Value254\nSET Key255 Value255\nSET Key256 Value256\nSET Key257 Value257\nSET Key258 Value258\nSET Key259 Value259\nSET Key260 Value260\nSET Key261 Value261\nSET Key262 Value262\nSET Key263 Value263\nSET Key264 Value264\nSET Key265 Value265\nSET Key266 Value266\nSET Key267 Value267\nSET Key268 Value268\nSET Key269 Value269\nSET Key270 Value270\nSET Key271 Value271\nSET Key272 Value272\nSET Key273 Value273\nSET Key274 Value274\nSET Key275 Value275\nSET Key276 Value276\nSET Key277 Value277\nSET Key278 Value278\nSET Key279 Value279\nSET Key280 Value280\nSET Key281 Value281\nSET Key282 Value282\nSET Key283 Value283\nSET Key284 Value284\nSET Key285 Value285\nSET Key286 Value286\nSET Key287 Value287\nSET Key288 Value288\nSET Key289 Value289\nSET Key290 Value290\nSET Key291 Value291\nSET Key292 Value292\nSET Key293 Value293\nSET Key294 Value294\nSET Key295 Value295\nSET Key296 Value296\nSET Key297 Value297\nSET Key298 Value298\nSET Key299 Value299\nSET Key300 Value300\nSET Key301 Value301\nSET Key302 Value302\nSET Key303 Value303\nSET Key304 Value304\nSET Key305 Value305\nSET Key306 Value306\nSET Key307 Value307\nSET Key308 Value308\nSET Key309 Value309\nSET Key310 Value310\nSET Key311 Value311\nSET Key312 Value312\nSET Key313 Value313\nSET Key314 Value314\nSET Key315 Value315\nSET Key316 Value316\nSET Key317 Value317\nSET Key318 Value318\nSET Key319 Value319\nSET Key320 Value320\nSET Key321 Value321\nSET Key322 Value322\nSET Key323 Value323\nSET Key324 Value324\nSET Key325 Value325\nSET Key326 Value326\nSET Key327 Value327\nSET Key328 Value328\nSET Key329 Value329\nSET Key330 Value330\nSET Key331 Value331\nSET Key332 Value332\nSET Key333 Value333\nSET Key334 Value334\nSET Key335 Value335\nSET Key336 Value336\nSET Key337 Value337\nSET Key338 Value338\nSET Key339 Value339\nSET Key340 Value340\nSET Key341 Value341\nSET Key342 Value342\nSET Key343 Value343\nSET Key344 Value344\nSET Key345 Value345\nSET Key346 Value346\nSET Key347 Value347\nSET Key348 Value348\nSET Key349 Value349\nSET Key350 Value350\nSET Key351 Value351\nSET Key352 Value352\nSET Key353 Value353\nSET Key354 Value354\nSET Key355 Value355\nSET Key356 Value356\nSET Key357 Value357\nSET Key358 Value358\nSET Key359 Value359\nSET Key360 Value360\nSET Key361 Value361\nSET Key362 Value362\nSET Key363 Value363\nSET Key364 Value364\nSET Key365 Value365\nSET Key366 Value366\nSET Key367 Value367\nSET Key368 Value368\nSET Key369 Value369\nSET Key370 Value370\nSET Key371 Value371\nSET Key372 Value372\nSET Key373 Value373\nSET Key374 Value374\nSET Key375 Value375\nSET Key376 Value376\nSET Key377 Value377\nSET Key378 Value378\nSET Key379 Value379\nSET Key380 Value380\nSET Key381 Value381\nSET Key382 Value382\nSET Key383 Value383\nSET Key384 Value384\nSET Key385 Value385\nSET Key386 Value386\nSET Key387 Value387\nSET Key388 Value388\nSET Key389 Value389\nSET Key390 Value390\nSET Key391 Value391\nSET Key392 Value392\nSET Key393 Value393\nSET Key394 Value394\nSET Key395 Value395\nSET Key396 Value396\nSET Key397 Value397\nSET Key398 Value398\nSET Key399 Value399\nSET Key400 Value400\nSET Key401 Value401\nSET Key402 Value402\nSET Key403 Value403\nSET Key404 Value404\nSET Key405 Value405\nSET Key406 Value406\nSET Key407 Value407\nSET Key408 Value408\nSET Key409 Value409\nSET Key410 Value410\nSET Key411 Value411\nSET Key412 Value412\nSET Key413 Value413\nSET Key414 Value414\nSET Key415 Value415\nSET Key416 Value416\nSET Key417 Value417\nSET Key418 Value418\nSET Key419 Value419\nSET Key420 Value420\nSET Key421 Value421\nSET Key422 Value422\nSET Key423 Value423\nSET Key424 Value424\nSET Key425 Value425\nSET Key426 Value426\nSET Key427 Value427\nSET Key428 Value428\nSET Key429 Value429\nSET Key430 Value430\nSET Key431 Value431\nSET Key432 Value432\nSET Key433 Value433\nSET Key434 Value434\nSET Key435 Value435\nSET Key436 Value436\nSET Key437 Value437\nSET Key438 Value438\nSET Key439 Value439\nSET Key440 Value440\nSET Key441 Value441\nSET Key442 Value442\nSET Key443 Value443\nSET Key444 Value444\nSET Key445 Value445\nSET Key446 Value446\nSET Key447 Value447\nSET Key448 Value448\nSET Key449 Value449\nSET Key450 Value450\nSET Key451 Value451\nSET Key452 Value452\nSET Key453 Value453\nSET Key454 Value454\nSET Key455 Value455\nSET Key456 Value456\nSET Key457 Value457\nSET Key458 Value458\nSET Key459 Value459\nSET Key460 Value460\nSET Key461 Value461\nSET Key462 Value462\nSET Key463 Value463\nSET Key464 Value464\nSET Key465 Value465\nSET Key466 Value466\nSET Key467 Value467\nSET Key468 Value468\nSET Key469 Value469\nSET Key470 Value470\nSET Key471 Value471\nSET Key472 Value472\nSET Key473 Value473\nSET Key474 Value474\nSET Key475 Value475\nSET Key476 Value476\nSET Key477 Value477\nSET Key478 Value478\nSET Key479 Value479\nSET Key480 Value480\nSET Key481 Value481\nSET Key482 Value482\nSET Key483 Value483\nSET Key484 Value484\nSET Key485 Value485\nSET Key486 Value486\nSET Key487 Value487\nSET Key488 Value488\nSET Key489 Value489\nSET Key490 Value490\nSET Key491 Value491\nSET Key492 Value492\nSET Key493 Value493\nSET Key494 Value494\nSET Key495 Value495\nSET Key496 Value496\nSET Key497 Value497\nSET Key498 Value498\nSET Key499 Value499\nSET Key500 Value500\nSET Key501 Value501\nSET Key502 Value502\nSET Key503 Value503\nSET Key504 Value504\nSET Key505 Value505\nSET Key506 Value506\nSET Key507 Value507\nSET Key508 Value508\nSET Key509 Value509\nSET Key510 Value510\nSET Key511 Value511\nSET Key512 Value512\nSET Key513 Value513\nSET Key514 Value514\nSET Key515 Value515\nSET Key516 Value516\nSET Key517 Value517\nSET Key518 Value518\nSET Key519 Value519\nSET Key520 Value520\nSET Key521 Value521\nSET Key522 Value522\nSET Key523 Value523\nSET Key524 Value524\nSET Key525 Value525\nSET Key526 Value526\nSET Key527 Value527\nSET Key528 Value528\nSET Key529 Value529\nSET Key530 Value530\nSET Key531 Value531\nSET Key532 Value532\nSET Key533 Value533\nSET Key534 Value534\nSET Key535 Value535\nSET Key536 Value536\nSET Key537 Value537\nSET Key538 Value538\nSET Key539 Value539\nSET Key540 Value540\nSET Key541 Value541\nSET Key542 Value542\nSET Key543 Value543\nSET Key544 Value544\nSET Key545 Value545\nSET Key546 Value546\nSET Key547 Value547\nSET Key548 Value548\nSET Key549 Value549\nSET Key550 Value550\nSET Key551 Value551\nSET Key552 Value552\nSET Key553 Value553\nSET Key554 Value554\nSET Key555 Value555\nSET Key556 Value556\nSET Key557 Value557\nSET Key558 Value558\nSET Key559 Value559\nSET Key560 Value560\nSET Key561 Value561\nSET Key562 Value562\nSET Key563 Value563\nSET Key564 Value564\nSET Key565 Value565\nSET Key566 Value566\nSET Key567 Value567\nSET Key568 Value568\nSET Key569 Value569\nSET Key570 Value570\nSET Key571 Value571\nSET Key572 Value572\nSET Key573 Value573\nSET Key574 Value574\nSET Key575 Value575\nSET Key576 Value576\nSET Key577 Value577\nSET Key578 Value578\nSET Key579 Value579\nSET Key580 Value580\nSET Key581 Value581\nSET Key582 Value582\nSET Key583 Value583\nSET Key584 Value584\nSET Key585 Value585\nSET Key586 Value586\nSET Key587 Value587\nSET Key588 Value588\nSET Key589 Value589\nSET Key590 Value590\nSET Key591 Value591\nSET Key592 Value592\nSET Key593 Value593\nSET Key594 Value594\nSET Key595 Value595\nSET Key596 Value596\nSET Key597 Value597\nSET Key598 Value598\nSET Key599 Value599\nSET Key600 Value600\nSET Key601 Value601\nSET Key602 Value602\nSET Key603 Value603\nSET Key604 Value604\nSET Key605 Value605\nSET Key606 Value606\nSET Key607 Value607\nSET Key608 Value608\nSET Key609 Value609\nSET Key610 Value610\nSET Key611 Value611\nSET Key612 Value612\nSET Key613 Value613\nSET Key614 Value614\nSET Key615 Value615\nSET Key616 Value616\nSET Key617 Value617\nSET Key618 Value618\nSET Key619 Value619\nSET Key620 Value620\nSET Key621 Value621\nSET Key622 Value622\nSET Key623 Value623\nSET Key624 Value624\nSET Key625 Value625\nSET Key626 Value626\nSET Key627 Value627\nSET Key628 Value628\nSET Key629 Value629\nSET Key630 Value630\nSET Key631 Value631\nSET Key632 Value632\nSET Key633 Value633\nSET Key634 Value634\nSET Key635 Value635\nSET Key636 Value636\nSET Key637 Value637\nSET Key638 Value638\nSET Key639 Value639\nSET Key640 Value640\nSET Key641 Value641\nSET Key642 Value642\nSET Key643 Value643\nSET Key644 Value644\nSET Key645 Value645\nSET Key646 Value646\nSET Key647 Value647\nSET Key648 Value648\nSET Key649 Value649\nSET Key650 Value650\nSET Key651 Value651\nSET Key652 Value652\nSET Key653 Value653\nSET Key654 Value654\nSET Key655 Value655\nSET Key656 Value656\nSET Key657 Value657\nSET Key658 Value658\nSET Key659 Value659\nSET Key660 Value660\nSET Key661 Value661\nSET Key662 Value662\nSET Key663 Value663\nSET Key664 Value664\nSET Key665 Value665\nSET Key666 Value666\nSET Key667 Value667\nSET Key668 Value668\nSET Key669 Value669\nSET Key670 Value670\nSET Key671 Value671\nSET Key672 Value672\nSET Key673 Value673\nSET Key674 Value674\nSET Key675 Value675\nSET Key676 Value676\nSET Key677 Value677\nSET Key678 Value678\nSET Key679 Value679\nSET Key680 Value680\nSET Key681 Value681\nSET Key682 Value682\nSET Key683 Value683\nSET Key684 Value684\nSET Key685 Value685\nSET Key686 Value686\nSET Key687 Value687\nSET Key688 Value688\nSET Key689 Value689\nSET Key690 Value690\nSET Key691 Value691\nSET Key692 Value692\nSET Key693 Value693\nSET Key694 Value694\nSET Key695 Value695\nSET Key696 Value696\nSET Key697 Value697\nSET Key698 Value698\nSET Key699 Value699\nSET Key700 Value700\nSET Key701 Value701\nSET Key702 Value702\nSET Key703 Value703\nSET Key704 Value704\nSET Key705 Value705\nSET Key706 Value706\nSET Key707 Value707\nSET Key708 Value708\nSET Key709 Value709\nSET Key710 Value710\nSET Key711 Value711\nSET Key712 Value712\nSET Key713 Value713\nSET Key714 Value714\nSET Key715 Value715\nSET Key716 Value716\nSET Key717 Value717\nSET Key718 Value718\nSET Key719 Value719\nSET Key720 Value720\nSET Key721 Value721\nSET Key722 Value722\nSET Key723 Value723\nSET Key724 Value724\nSET Key725 Value725\nSET Key726 Value726\nSET Key727 Value727\nSET Key728 Value728\nSET Key729 Value729\nSET Key730 Value730\nSET Key731 Value731\nSET Key732 Value732\nSET Key733 Value733\nSET Key734 Value734\nSET Key735 Value735\nSET Key736 Value736\nSET Key737 Value737\nSET Key738 Value738\nSET Key739 Value739\nSET Key740 Value740\nSET Key741 Value741\nSET Key742 Value742\nSET Key743 Value743\nSET Key744 Value744\nSET Key745 Value745\nSET Key746 Value746\nSET Key747 Value747\nSET Key748 Value748\nSET Key749 Value749\nSET Key750 Value750\nSET Key751 Value751\nSET Key752 Value752\nSET Key753 Value753\nSET Key754 Value754\nSET Key755 Value755\nSET Key756 Value756\nSET Key757 Value757\nSET Key758 Value758\nSET Key759 Value759\nSET Key760 Value760\nSET Key761 Value761\nSET Key762 Value762\nSET Key763 Value763\nSET Key764 Value764\nSET Key765 Value765\nSET Key766 Value766\nSET Key767 Value767\nSET Key768 Value768\nSET Key769 Value769\nSET Key770 Value770\nSET Key771 Value771\nSET Key772 Value772\nSET Key773 Value773\nSET Key774 Value774\nSET Key775 Value775\nSET Key776 Value776\nSET Key777 Value777\nSET Key778 Value778\nSET Key779 Value779\nSET Key780 Value780\nSET Key781 Value781\nSET Key782 Value782\nSET Key783 Value783\nSET Key784 Value784\nSET Key785 Value785\nSET Key786 Value786\nSET Key787 Value787\nSET Key788 Value788\nSET Key789 Value789\nSET Key790 Value790\nSET Key791 Value791\nSET Key792 Value792\nSET Key793 Value793\nSET Key794 Value794\nSET Key795 Value795\nSET Key796 Value796\nSET Key797 Value797\nSET Key798 Value798\nSET Key799 Value799\nSET Key800 Value800\nSET Key801 Value801\nSET Key802 Value802\nSET Key803 Value803\nSET Key804 Value804\nSET Key805 Value805\nSET Key806 Value806\nSET Key807 Value807\nSET Key808 Value808\nSET Key809 Value809\nSET Key810 Value810\nSET Key811 Value811\nSET Key812 Value812\nSET Key813 Value813\nSET Key814 Value814\nSET Key815 Value815\nSET Key816 Value816\nSET Key817 Value817\nSET Key818 Value818\nSET Key819 Value819\nSET Key820 Value820\nSET Key821 Value821\nSET Key822 Value822\nSET Key823 Value823\nSET Key824 Value824\nSET Key825 Value825\nSET Key826 Value826\nSET Key827 Value827\nSET Key828 Value828\nSET Key829 Value829\nSET Key830 Value830\nSET Key831 Value831\nSET Key832 Value832\nSET Key833 Value833\nSET Key834 Value834\nSET Key835 Value835\nSET Key836 Value836\nSET Key837 Value837\nSET Key838 Value838\nSET Key839 Value839\nSET Key840 Value840\nSET Key841 Value841\nSET Key842 Value842\nSET Key843 Value843\nSET Key844 Value844\nSET Key845 Value845\nSET Key846 Value846\nSET Key847 Value847\nSET Key848 Value848\nSET Key849 Value849\nSET Key850 Value850\nSET Key851 Value851\nSET Key852 Value852\nSET Key853 Value853\nSET Key854 Value854\nSET Key855 Value855\nSET Key856 Value856\nSET Key857 Value857\nSET Key858 Value858\nSET Key859 Value859\nSET Key860 Value860\nSET Key861 Value861\nSET Key862 Value862\nSET Key863 Value863\nSET Key864 Value864\nSET Key865 Value865\nSET Key866 Value866\nSET Key867 Value867\nSET Key868 Value868\nSET Key869 Value869\nSET Key870 Value870\nSET Key871 Value871\nSET Key872 Value872\nSET Key873 Value873\nSET Key874 Value874\nSET Key875 Value875\nSET Key876 Value876\nSET Key877 Value877\nSET Key878 Value878\nSET Key879 Value879\nSET Key880 Value880\nSET Key881 Value881\nSET Key882 Value882\nSET Key883 Value883\nSET Key884 Value884\nSET Key885 Value885\nSET Key886 Value886\nSET Key887 Value887\nSET Key888 Value888\nSET Key889 Value889\nSET Key890 Value890\nSET Key891 Value891\nSET Key892 Value892\nSET Key893 Value893\nSET Key894 Value894\nSET Key895 Value895\nSET Key896 Value896\nSET Key897 Value897\nSET Key898 Value898\nSET Key899 Value899\nSET Key900 Value900\nSET Key901 Value901\nSET Key902 Value902\nSET Key903 Value903\nSET Key904 Value904\nSET Key905 Value905\nSET Key906 Value906\nSET Key907 Value907\nSET Key908 Value908\nSET Key909 Value909\nSET Key910 Value910\nSET Key911 Value911\nSET Key912 Value912\nSET Key913 Value913\nSET Key914 Value914\nSET Key915 Value915\nSET Key916 Value916\nSET Key917 Value917\nSET Key918 Value918\nSET Key919 Value919\nSET Key920 Value920\nSET Key921 Value921\nSET Key922 Value922\nSET Key923 Value923\nSET Key924 Value924\nSET Key925 Value925\nSET Key926 Value926\nSET Key927 Value927\nSET Key928 Value928\nSET Key929 Value929\nSET Key930 Value930\nSET Key931 Value931\nSET Key932 Value932\nSET Key933 Value933\nSET Key934 Value934\nSET Key935 Value935\nSET Key936 Value936\nSET Key937 Value937\nSET Key938 Value938\nSET Key939 Value939\nSET Key940 Value940\nSET Key941 Value941\nSET Key942 Value942\nSET Key943 Value943\nSET Key944 Value944\nSET Key945 Value945\nSET Key946 Value946\nSET Key947 Value947\nSET Key948 Value948\nSET Key949 Value949\nSET Key950 Value950\nSET Key951 Value951\nSET Key952 Value952\nSET Key953 Value953\nSET Key954 Value954\nSET Key955 Value955\nSET Key956 Value956\nSET Key957 Value957\nSET Key958 Value958\nSET Key959 Value959\nSET Key960 Value960\nSET Key961 Value961\nSET Key962 Value962\nSET Key963 Value963\nSET Key964 Value964\nSET Key965 Value965\nSET Key966 Value966\nSET Key967 Value967\nSET Key968 Value968\nSET Key969 Value969\nSET Key970 Value970\nSET Key971 Value971\nSET Key972 Value972\nSET Key973 Value973\nSET Key974 Value974\nSET Key975 Value975\nSET Key976 Value976\nSET Key977 Value977\nSET Key978 Value978\nSET Key979 Value979\nSET Key980 Value980\nSET Key981 Value981\nSET Key982 Value982\nSET Key983 Value983\nSET Key984 Value984\nSET Key985 Value985\nSET Key986 Value986\nSET Key987 Value987\nSET Key988 Value988\nSET Key989 Value989\nSET Key990 Value990\nSET Key991 Value991\nSET Key992 Value992\nSET Key993 Value993\nSET Key994 Value994\nSET Key995 Value995\nSET Key996 Value996\nSET Key997 Value997\nSET Key998 Value998\nSET Key999 Value999\nSET Key1000 Value1000\nSET Key1001 Value1001\nSET Key1002 Value1002\nSET Key1003 Value1003\nSET Key1004 Value1004\nSET Key1005 Value1005\nSET Key1006 Value1006\nSET Key1007 Value1007\nSET Key1008 Value1008\nSET Key1009 Value1009\nSET Key1010 Value1010\nSET Key1011 Value1011\nSET Key1012 Value1012\nSET Key1013 Value1013\nSET Key1014 Value1014\nSET Key1015 Value1015\nSET Key1016 Value1016\nSET Key1017 Value1017\nSET Key1018 Value1018\nSET Key1019 Value1019\nSET Key1020 Value1020\nSET Key1021 Value1021\nSET Key1022 Value1022\nSET Key1023 Value1023\nSET Key1024 Value1024\nSET Key1025 Value1025\nSET Key1026 Value1026\nSET Key1027 Value1027\nSET Key1028 Value1028\nSET Key1029 Value1029\nSET Key1030 Value1030\nSET Key1031 Value1031\nSET Key1032 Value1032\nSET Key1033 Value1033\nSET Key1034 Value1034\nSET Key1035 Value1035\nSET Key1036 Value1036\nSET Key1037 Value1037\nSET Key1038 Value1038\nSET Key1039 Value1039\nSET Key1040 Value1040\nSET Key1041 Value1041\nSET Key1042 Value1042\nSET Key1043 Value1043\nSET Key1044 Value1044\nSET Key1045 Value1045\nSET Key1046 Value1046\nSET Key1047 Value1047\nSET Key1048 Value1048\nSET Key1049 Value1049\nSET Key1050 Value1050\nSET Key1051 Value1051\nSET Key1052 Value1052\nSET Key1053 Value1053\nSET Key1054 Value1054\nSET Key1055 Value1055\nSET Key1056 Value1056\nSET Key1057 Value1057\nSET Key1058 Value1058\nSET Key1059 Value1059\nSET Key1060 Value1060\nSET Key1061 Value1061\nSET Key1062 Value1062\nSET Key1063 Value1063\nSET Key1064 Value1064\nSET Key1065 Value1065\nSET Key1066 Value1066\nSET Key1067 Value1067\nSET Key1068 Value1068\nSET Key1069 Value1069\nSET Key1070 Value1070\nSET Key1071 Value1071\nSET Key1072 Value1072\nSET Key1073 Value1073\nSET Key1074 Value1074\nSET Key1075 Value1075\nSET Key1076 Value1076\nSET Key1077 Value1077\nSET Key1078 Value1078\nSET Key1079 Value1079\nSET Key1080 Value1080\nSET Key1081 Value1081\nSET Key1082 Value1082\nSET Key1083 Value1083\nSET Key1084 Value1084\nSET Key1085 Value1085\nSET Key1086 Value1086\nSET Key1087 Value1087\nSET Key1088 Value1088\nSET Key1089 Value1089\nSET Key1090 Value1090\nSET Key1091 Value1091\nSET Key1092 Value1092\nSET Key1093 Value1093\nSET Key1094 Value1094\nSET Key1095 Value1095\nSET Key1096 Value1096\nSET Key1097 Value1097\nSET Key1098 Value1098\nSET Key1099 Value1099\nSET Key1100 Value1100\nSET Key1101 Value1101\nSET Key1102 Value1102\nSET Key1103 Value1103\nSET Key1104 Value1104\nSET Key1105 Value1105\nSET Key1106 Value1106\nSET Key1107 Value1107\nSET Key1108 Value1108\nSET Key1109 Value1109\nSET Key1110 Value1110\nSET Key1111 Value1111\nSET Key1112 Value1112\nSET Key1113 Value1113\nSET Key1114 Value1114\nSET Key1115 Value1115\nSET Key1116 Value1116\nSET Key1117 Value1117\nSET Key1118 Value1118\nSET Key1119 Value1119\nSET Key1120 Value1120\nSET Key1121 Value1121\nSET Key1122 Value1122\nSET Key1123 Value1123\nSET Key1124 Value1124\nSET Key1125 Value1125\nSET Key1126 Value1126\nSET Key1127 Value1127\nSET Key1128 Value1128\nSET Key1129 Value1129\nSET Key1130 Value1130\nSET Key1131 Value1131\nSET Key1132 Value1132\nSET Key1133 Value1133\nSET Key1134 Value1134\nSET Key1135 Value1135\nSET Key1136 Value1136\nSET Key1137 Value1137\nSET Key1138 Value1138\nSET Key1139 Value1139\nSET Key1140 Value1140\nSET Key1141 Value1141\nSET Key1142 Value1142\nSET Key1143 Value1143\nSET Key1144 Value1144\nSET Key1145 Value1145\nSET Key1146 Value1146\nSET Key1147 Value1147\nSET Key1148 Value1148\nSET Key1149 Value1149\nSET Key1150 Value1150\nSET Key1151 Value1151\nSET Key1152 Value1152\nSET Key1153 Value1153\nSET Key1154 Value1154\nSET Key1155 Value1155\nSET Key1156 Value1156\nSET Key1157 Value1157\nSET Key1158 Value1158\nSET Key1159 Value1159\nSET Key1160 Value1160\nSET Key1161 Value1161\nSET Key1162 Value1162\nSET Key1163 Value1163\nSET Key1164 Value1164\nSET Key1165 Value1165\nSET Key1166 Value1166\nSET Key1167 Value1167\nSET Key1168 Value1168\nSET Key1169 Value1169\nSET Key1170 Value1170\nSET Key1171 Value1171\nSET Key1172 Value1172\nSET Key1173 Value1173\nSET Key1174 Value1174\nSET Key1175 Value1175\nSET Key1176 Value1176\nSET Key1177 Value1177\nSET Key1178 Value1178\nSET Key1179 Value1179\nSET Key1180 Value1180\nSET Key1181 Value1181\nSET Key1182 Value1182\nSET Key1183 Value1183\nSET Key1184 Value1184\nSET Key1185 Value1185\nSET Key1186 Value1186\nSET Key1187 Value1187\nSET Key1188 Value1188\nSET Key1189 Value1189\nSET Key1190 Value1190\nSET Key1191 Value1191\nSET Key1192 Value1192\nSET Key1193 Value1193\nSET Key1194 Value1194\nSET Key1195 Value1195\nSET Key1196 Value1196\nSET Key1197 Value1197\nSET Key1198 Value1198\nSET Key1199 Value1199\nSET Key1200 Value1200\nSET Key1201 Value1201\nSET Key1202 Value1202\nSET Key1203 Value1203\nSET Key1204 Value1204\nSET Key1205 Value1205\nSET Key1206 Value1206\nSET Key1207 Value1207\nSET Key1208 Value1208\nSET Key1209 Value1209\nSET Key1210 Value1210\nSET Key1211 Value1211\nSET Key1212 Value1212\nSET Key1213 Value1213\nSET Key1214 Value1214\nSET Key1215 Value1215\nSET Key1216 Value1216\nSET Key1217 Value1217\nSET Key1218 Value1218\nSET Key1219 Value1219\nSET Key1220 Value1220\nSET Key1221 Value1221\nSET Key1222 Value1222\nSET Key1223 Value1223\nSET Key1224 Value1224\nSET Key1225 Value1225\nSET Key1226 Value1226\nSET Key1227 Value1227\nSET Key1228 Value1228\nSET Key1229 Value1229\nSET Key1230 Value1230\nSET Key1231 Value1231\nSET Key1232 Value1232\nSET Key1233 Value1233\nSET Key1234 Value1234\nSET Key1235 Value1235\nSET Key1236 Value1236\nSET Key1237 Value1237\nSET Key1238 Value1238\nSET Key1239 Value1239\nSET Key1240 Value1240\nSET Key1241 Value1241\nSET Key1242 Value1242\nSET Key1243 Value1243\nSET Key1244 Value1244\nSET Key1245 Value1245\nSET Key1246 Value1246\nSET Key1247 Value1247\nSET Key1248 Value1248\nSET Key1249 Value1249\nSET Key1250 Value1250\nSET Key1251 Value1251\nSET Key1252 Value1252\nSET Key1253 Value1253\nSET Key1254 Value1254\nSET Key1255 Value1255\nSET Key1256 Value1256\nSET Key1257 Value1257\nSET Key1258 Value1258\nSET Key1259 Value1259\nSET Key1260 Value1260\nSET Key1261 Value1261\nSET Key1262 Value1262\nSET Key1263 Value1263\nSET Key1264 Value1264\nSET Key1265 Value1265\nSET Key1266 Value1266\nSET Key1267 Value1267\nSET Key1268 Value1268\nSET Key1269 Value1269\nSET Key1270 Value1270\nSET Key1271 Value1271\nSET Key1272 Value1272\nSET Key1273 Value1273\nSET Key1274 Value1274\nSET Key1275 Value1275\nSET Key1276 Value1276\nSET Key1277 Value1277\nSET Key1278 Value1278\nSET Key1279 Value1279\nSET Key1280 Value1280\nSET Key1281 Value1281\nSET Key1282 Value1282\nSET Key1283 Value1283\nSET Key1284 Value1284\nSET Key1285 Value1285\nSET Key1286 Value1286\nSET Key1287 Value1287\nSET Key1288 Value1288\nSET Key1289 Value1289\nSET Key1290 Value1290\nSET Key1291 Value1291\nSET Key1292 Value1292\nSET Key1293 Value1293\nSET Key1294 Value1294\nSET Key1295 Value1295\nSET Key1296 Value1296\nSET Key1297 Value1297\nSET Key1298 Value1298\nSET Key1299 Value1299\nSET Key1300 Value1300\nSET Key1301 Value1301\nSET Key1302 Value1302\nSET Key1303 Value1303\nSET Key1304 Value1304\nSET Key1305 Value1305\nSET Key1306 Value1306\nSET Key1307 Value1307\nSET Key1308 Value1308\nSET Key1309 Value1309\nSET Key1310 Value1310\nSET Key1311 Value1311\nSET Key1312 Value1312\nSET Key1313 Value1313\nSET Key1314 Value1314\nSET Key1315 Value1315\nSET Key1316 Value1316\nSET Key1317 Value1317\nSET Key1318 Value1318\nSET Key1319 Value1319\nSET Key1320 Value1320\nSET Key1321 Value1321\nSET Key1322 Value1322\nSET Key1323 Value1323\nSET Key1324 Value1324\nSET Key1325 Value1325\nSET Key1326 Value1326\nSET Key1327 Value1327\nSET Key1328 Value1328\nSET Key1329 Value1329\nSET Key1330 Value1330\nSET Key1331 Value1331\nSET Key1332 Value1332\nSET Key1333 Value1333\nSET Key1334 Value1334\nSET Key1335 Value1335\nSET Key1336 Value1336\nSET Key1337 Value1337\nSET Key1338 Value1338\nSET Key1339 Value1339\nSET Key1340 Value1340\nSET Key1341 Value1341\nSET Key1342 Value1342\nSET Key1343 Value1343\nSET Key1344 Value1344\nSET Key1345 Value1345\nSET Key1346 Value1346\nSET Key1347 Value1347\nSET Key1348 Value1348\nSET Key1349 Value1349\nSET Key1350 Value1350\nSET Key1351 Value1351\nSET Key1352 Value1352\nSET Key1353 Value1353\nSET Key1354 Value1354\nSET Key1355 Value1355\nSET Key1356 Value1356\nSET Key1357 Value1357\nSET Key1358 Value1358\nSET Key1359 Value1359\nSET Key1360 Value1360\nSET Key1361 Value1361\nSET Key1362 Value1362\nSET Key1363 Value1363\nSET Key1364 Value1364\nSET Key1365 Value1365\nSET Key1366 Value1366\nSET Key1367 Value1367\nSET Key1368 Value1368\nSET Key1369 Value1369\nSET Key1370 Value1370\nSET Key1371 Value1371\nSET Key1372 Value1372\nSET Key1373 Value1373\nSET Key1374 Value1374\nSET Key1375 Value1375\nSET Key1376 Value1376\nSET Key1377 Value1377\nSET Key1378 Value1378\nSET Key1379 Value1379\nSET Key1380 Value1380\nSET Key1381 Value1381\nSET Key1382 Value1382\nSET Key1383 Value1383\nSET Key1384 Value1384\nSET Key1385 Value1385\nSET Key1386 Value1386\nSET Key1387 Value1387\nSET Key1388 Value1388\nSET Key1389 Value1389\nSET Key1390 Value1390\nSET Key1391 Value1391\nSET Key1392 Value1392\nSET Key1393 Value1393\nSET Key1394 Value1394\nSET Key1395 Value1395\nSET Key1396 Value1396\nSET Key1397 Value1397\nSET Key1398 Value1398\nSET Key1399 Value1399\nSET Key1400 Value1400\nSET Key1401 Value1401\nSET Key1402 Value1402\nSET Key1403 Value1403\nSET Key1404 Value1404\nSET Key1405 Value1405\nSET Key1406 Value1406\nSET Key1407 Value1407\nSET Key1408 Value1408\nSET Key1409 Value1409\nSET Key1410 Value1410\nSET Key1411 Value1411\nSET Key1412 Value1412\nSET Key1413 Value1413\nSET Key1414 Value1414\nSET Key1415 Value1415\nSET Key1416 Value1416\nSET Key1417 Value1417\nSET Key1418 Value1418\nSET Key1419 Value1419\nSET Key1420 Value1420\nSET Key1421 Value1421\nSET Key1422 Value1422\nSET Key1423 Value1423\nSET Key1424 Value1424\nSET Key1425 Value1425\nSET Key1426 Value1426\nSET Key1427 Value1427\nSET Key1428 Value1428\nSET Key1429 Value1429\nSET Key1430 Value1430\nSET Key1431 Value1431\nSET Key1432 Value1432\nSET Key1433 Value1433\nSET Key1434 Value1434\nSET Key1435 Value1435\nSET Key1436 Value1436\nSET Key1437 Value1437\nSET Key1438 Value1438\nSET Key1439 Value1439\nSET Key1440 Value1440\nSET Key1441 Value1441\nSET Key1442 Value1442\nSET Key1443 Value1443\nSET Key1444 Value1444\nSET Key1445 Value1445\nSET Key1446 Value1446\nSET Key1447 Value1447\nSET Key1448 Value1448\nSET Key1449 Value1449\nSET Key1450 Value1450\nSET Key1451 Value1451\nSET Key1452 Value1452\nSET Key1453 Value1453\nSET Key1454 Value1454\nSET Key1455 Value1455\nSET Key1456 Value1456\nSET Key1457 Value1457\nSET Key1458 Value1458\nSET Key1459 Value1459\nSET Key1460 Value1460\nSET Key1461 Value1461\nSET Key1462 Value1462\nSET Key1463 Value1463\nSET Key1464 Value1464\nSET Key1465 Value1465\nSET Key1466 Value1466\nSET Key1467 Value1467\nSET Key1468 Value1468\nSET Key1469 Value1469\nSET Key1470 Value1470\nSET Key1471 Value1471\nSET Key1472 Value1472\nSET Key1473 Value1473\nSET Key1474 Value1474\nSET Key1475 Value1475\nSET Key1476 Value1476\nSET Key1477 Value1477\nSET Key1478 Value1478\nSET Key1479 Value1479\nSET Key1480 Value1480\nSET Key1481 Value1481\nSET Key1482 Value1482\nSET Key1483 Value1483\nSET Key1484 Value1484\nSET Key1485 Value1485\nSET Key1486 Value1486\nSET Key1487 Value1487\nSET Key1488 Value1488\nSET Key1489 Value1489\nSET Key1490 Value1490\nSET Key1491 Value1491\nSET Key1492 Value1492\nSET Key1493 Value1493\nSET Key1494 Value1494\nSET Key1495 Value1495\nSET Key1496 Value1496\nSET Key1497 Value1497\nSET Key1498 Value1498\nSET Key1499 Value1499\nSET Key1500 Value1500\nSET Key1501 Value1501\nSET Key1502 Value1502\nSET Key1503 Value1503\nSET Key1504 Value1504\nSET Key1505 Value1505\nSET Key1506 Value1506\nSET Key1507 Value1507\nSET Key1508 Value1508\nSET Key1509 Value1509\nSET Key1510 Value1510\nSET Key1511 Value1511\nSET Key1512 Value1512\nSET Key1513 Value1513\nSET Key1514 Value1514\nSET Key1515 Value1515\nSET Key1516 Value1516\nSET Key1517 Value1517\nSET Key1518 Value1518\nSET Key1519 Value1519\nSET Key1520 Value1520\nSET Key1521 Value1521\nSET Key1522 Value1522\nSET Key1523 Value1523\nSET Key1524 Value1524\nSET Key1525 Value1525\nSET Key1526 Value1526\nSET Key1527 Value1527\nSET Key1528 Value1528\nSET Key1529 Value1529\nSET Key1530 Value1530\nSET Key1531 Value1531\nSET Key1532 Value1532\nSET Key1533 Value1533\nSET Key1534 Value1534\nSET Key1535 Value1535\nSET Key1536 Value1536\nSET Key1537 Value1537\nSET Key1538 Value1538\nSET Key1539 Value1539\nSET Key1540 Value1540\nSET Key1541 Value1541\nSET Key1542 Value1542\nSET Key1543 Value1543\nSET Key1544 Value1544\nSET Key1545 Value1545\nSET Key1546 Value1546\nSET Key1547 Value1547\nSET Key1548 Value1548\nSET Key1549 Value1549\nSET Key1550 Value1550\nSET Key1551 Value1551\nSET Key1552 Value1552\nSET Key1553 Value1553\nSET Key1554 Value1554\nSET Key1555 Value1555\nSET Key1556 Value1556\nSET Key1557 Value1557\nSET Key1558 Value1558\nSET Key1559 Value1559\nSET Key1560 Value1560\nSET Key1561 Value1561\nSET Key1562 Value1562\nSET Key1563 Value1563\nSET Key1564 Value1564\nSET Key1565 Value1565\nSET Key1566 Value1566\nSET Key1567 Value1567\nSET Key1568 Value1568\nSET Key1569 Value1569\nSET Key1570 Value1570\nSET Key1571 Value1571\nSET Key1572 Value1572\nSET Key1573 Value1573\nSET Key1574 Value1574\nSET Key1575 Value1575\nSET Key1576 Value1576\nSET Key1577 Value1577\nSET Key1578 Value1578\nSET Key1579 Value1579\nSET Key1580 Value1580\nSET Key1581 Value1581\nSET Key1582 Value1582\nSET Key1583 Value1583\nSET Key1584 Value1584\nSET Key1585 Value1585\nSET Key1586 Value1586\nSET Key1587 Value1587\nSET Key1588 Value1588\nSET Key1589 Value1589\nSET Key1590 Value1590\nSET Key1591 Value1591\nSET Key1592 Value1592\nSET Key1593 Value1593\nSET Key1594 Value1594\nSET Key1595 Value1595\nSET Key1596 Value1596\nSET Key1597 Value1597\nSET Key1598 Value1598\nSET Key1599 Value1599\nSET Key1600 Value1600\nSET Key1601 Value1601\nSET Key1602 Value1602\nSET Key1603 Value1603\nSET Key1604 Value1604\nSET Key1605 Value1605\nSET Key1606 Value1606\nSET Key1607 Value1607\nSET Key1608 Value1608\nSET Key1609 Value1609\nSET Key1610 Value1610\nSET Key1611 Value1611\nSET Key1612 Value1612\nSET Key1613 Value1613\nSET Key1614 Value1614\nSET Key1615 Value1615\nSET Key1616 Value1616\nSET Key1617 Value1617\nSET Key1618 Value1618\nSET Key1619 Value1619\nSET Key1620 Value1620\nSET Key1621 Value1621\nSET Key1622 Value1622\nSET Key1623 Value1623\nSET Key1624 Value1624\nSET Key1625 Value1625\nSET Key1626 Value1626\nSET Key1627 Value1627\nSET Key1628 Value1628\nSET Key1629 Value1629\nSET Key1630 Value1630\nSET Key1631 Value1631\nSET Key1632 Value1632\nSET Key1633 Value1633\nSET Key1634 Value1634\nSET Key1635 Value1635\nSET Key1636 Value1636\nSET Key1637 Value1637\nSET Key1638 Value1638\nSET Key1639 Value1639\nSET Key1640 Value1640\nSET Key1641 Value1641\nSET Key1642 Value1642\nSET Key1643 Value1643\nSET Key1644 Value1644\nSET Key1645 Value1645\nSET Key1646 Value1646\nSET Key1647 Value1647\nSET Key1648 Value1648\nSET Key1649 Value1649\nSET Key1650 Value1650\nSET Key1651 Value1651\nSET Key1652 Value1652\nSET Key1653 Value1653\nSET Key1654 Value1654\nSET Key1655 Value1655\nSET Key1656 Value1656\nSET Key1657 Value1657\nSET Key1658 Value1658\nSET Key1659 Value1659\nSET Key1660 Value1660\nSET Key1661 Value1661\nSET Key1662 Value1662\nSET Key1663 Value1663\nSET Key1664 Value1664\nSET Key1665 Value1665\nSET Key1666 Value1666\nSET Key1667 Value1667\nSET Key1668 Value1668\nSET Key1669 Value1669\nSET Key1670 Value1670\nSET Key1671 Value1671\nSET Key1672 Value1672\nSET Key1673 Value1673\nSET Key1674 Value1674\nSET Key1675 Value1675\nSET Key1676 Value1676\nSET Key1677 Value1677\nSET Key1678 Value1678\nSET Key1679 Value1679\nSET Key1680 Value1680\nSET Key1681 Value1681\nSET Key1682 Value1682\nSET Key1683 Value1683\nSET Key1684 Value1684\nSET Key1685 Value1685\nSET Key1686 Value1686\nSET Key1687 Value1687\nSET Key1688 Value1688\nSET Key1689 Value1689\nSET Key1690 Value1690\nSET Key1691 Value1691\nSET Key1692 Value1692\nSET Key1693 Value1693\nSET Key1694 Value1694\nSET Key1695 Value1695\nSET Key1696 Value1696\nSET Key1697 Value1697\nSET Key1698 Value1698\nSET Key1699 Value1699\nSET Key1700 Value1700\nSET Key1701 Value1701\nSET Key1702 Value1702\nSET Key1703 Value1703\nSET Key1704 Value1704\nSET Key1705 Value1705\nSET Key1706 Value1706\nSET Key1707 Value1707\nSET Key1708 Value1708\nSET Key1709 Value1709\nSET Key1710 Value1710\nSET Key1711 Value1711\nSET Key1712 Value1712\nSET Key1713 Value1713\nSET Key1714 Value1714\nSET Key1715 Value1715\nSET Key1716 Value1716\nSET Key1717 Value1717\nSET Key1718 Value1718\nSET Key1719 Value1719\nSET Key1720 Value1720\nSET Key1721 Value1721\nSET Key1722 Value1722\nSET Key1723 Value1723\nSET Key1724 Value1724\nSET Key1725 Value1725\nSET Key1726 Value1726\nSET Key1727 Value1727\nSET Key1728 Value1728\nSET Key1729 Value1729\nSET Key1730 Value1730\nSET Key1731 Value1731\nSET Key1732 Value1732\nSET Key1733 Value1733\nSET Key1734 Value1734\nSET Key1735 Value1735\nSET Key1736 Value1736\nSET Key1737 Value1737\nSET Key1738 Value1738\nSET Key1739 Value1739\nSET Key1740 Value1740\nSET Key1741 Value1741\nSET Key1742 Value1742\nSET Key1743 Value1743\nSET Key1744 Value1744\nSET Key1745 Value1745\nSET Key1746 Value1746\nSET Key1747 Value1747\nSET Key1748 Value1748\nSET Key1749 Value1749\nSET Key1750 Value1750\nSET Key1751 Value1751\nSET Key1752 Value1752\nSET Key1753 Value1753\nSET Key1754 Value1754\nSET Key1755 Value1755\nSET Key1756 Value1756\nSET Key1757 Value1757\nSET Key1758 Value1758\nSET Key1759 Value1759\nSET Key1760 Value1760\nSET Key1761 Value1761\nSET Key1762 Value1762\nSET Key1763 Value1763\nSET Key1764 Value1764\nSET Key1765 Value1765\nSET Key1766 Value1766\nSET Key1767 Value1767\nSET Key1768 Value1768\nSET Key1769 Value1769\nSET Key1770 Value1770\nSET Key1771 Value1771\nSET Key1772 Value1772\nSET Key1773 Value1773\nSET Key1774 Value1774\nSET Key1775 Value1775\nSET Key1776 Value1776\nSET Key1777 Value1777\nSET Key1778 Value1778\nSET Key1779 Value1779\nSET Key1780 Value1780\nSET Key1781 Value1781\nSET Key1782 Value1782\nSET Key1783 Value1783\nSET Key1784 Value1784\nSET Key1785 Value1785\nSET Key1786 Value1786\nSET Key1787 Value1787\nSET Key1788 Value1788\nSET Key1789 Value1789\nSET Key1790 Value1790\nSET Key1791 Value1791\nSET Key1792 Value1792\nSET Key1793 Value1793\nSET Key1794 Value1794\nSET Key1795 Value1795\nSET Key1796 Value1796\nSET Key1797 Value1797\nSET Key1798 Value1798\nSET Key1799 Value1799\nSET Key1800 Value1800\nSET Key1801 Value1801\nSET Key1802 Value1802\nSET Key1803 Value1803\nSET Key1804 Value1804\nSET Key1805 Value1805\nSET Key1806 Value1806\nSET Key1807 Value1807\nSET Key1808 Value1808\nSET Key1809 Value1809\nSET Key1810 Value1810\nSET Key1811 Value1811\nSET Key1812 Value1812\nSET Key1813 Value1813\nSET Key1814 Value1814\nSET Key1815 Value1815\nSET Key1816 Value1816\nSET Key1817 Value1817\nSET Key1818 Value1818\nSET Key1819 Value1819\nSET Key1820 Value1820\nSET Key1821 Value1821\nSET Key1822 Value1822\nSET Key1823 Value1823\nSET Key1824 Value1824\nSET Key1825 Value1825\nSET Key1826 Value1826\nSET Key1827 Value1827\nSET Key1828 Value1828\nSET Key1829 Value1829\nSET Key1830 Value1830\nSET Key1831 Value1831\nSET Key1832 Value1832\nSET Key1833 Value1833\nSET Key1834 Value1834\nSET Key1835 Value1835\nSET Key1836 Value1836\nSET Key1837 Value1837\nSET Key1838 Value1838\nSET Key1839 Value1839\nSET Key1840 Value1840\nSET Key1841 Value1841\nSET Key1842 Value1842\nSET Key1843 Value1843\nSET Key1844 Value1844\nSET Key1845 Value1845\nSET Key1846 Value1846\nSET Key1847 Value1847\nSET Key1848 Value1848\nSET Key1849 Value1849\nSET Key1850 Value1850\nSET Key1851 Value1851\nSET Key1852 Value1852\nSET Key1853 Value1853\nSET Key1854 Value1854\nSET Key1855 Value1855\nSET Key1856 Value1856\nSET Key1857 Value1857\nSET Key1858 Value1858\nSET Key1859 Value1859\nSET Key1860 Value1860\nSET Key1861 Value1861\nSET Key1862 Value1862\nSET Key1863 Value1863\nSET Key1864 Value1864\nSET Key1865 Value1865\nSET Key1866 Value1866\nSET Key1867 Value1867\nSET Key1868 Value1868\nSET Key1869 Value1869\nSET Key1870 Value1870\nSET Key1871 Value1871\nSET Key1872 Value1872\nSET Key1873 Value1873\nSET Key1874 Value1874\nSET Key1875 Value1875\nSET Key1876 Value1876\nSET Key1877 Value1877\nSET Key1878 Value1878\nSET Key1879 Value1879\nSET Key1880 Value1880\nSET Key1881 Value1881\nSET Key1882 Value1882\nSET Key1883 Value1883\nSET Key1884 Value1884\nSET Key1885 Value1885\nSET Key1886 Value1886\nSET Key1887 Value1887\nSET Key1888 Value1888\nSET Key1889 Value1889\nSET Key1890 Value1890\nSET Key1891 Value1891\nSET Key1892 Value1892\nSET Key1893 Value1893\nSET Key1894 Value1894\nSET Key1895 Value1895\nSET Key1896 Value1896\nSET Key1897 Value1897\nSET Key1898 Value1898\nSET Key1899 Value1899\nSET Key1900 Value1900\nSET Key1901 Value1901\nSET Key1902 Value1902\nSET Key1903 Value1903\nSET Key1904 Value1904\nSET Key1905 Value1905\nSET Key1906 Value1906\nSET Key1907 Value1907\nSET Key1908 Value1908\nSET Key1909 Value1909\nSET Key1910 Value1910\nSET Key1911 Value1911\nSET Key1912 Value1912\nSET Key1913 Value1913\nSET Key1914 Value1914\nSET Key1915 Value1915\nSET Key1916 Value1916\nSET Key1917 Value1917\nSET Key1918 Value1918\nSET Key1919 Value1919\nSET Key1920 Value1920\nSET Key1921 Value1921\nSET Key1922 Value1922\nSET Key1923 Value1923\nSET Key1924 Value1924\nSET Key1925 Value1925\nSET Key1926 Value1926\nSET Key1927 Value1927\nSET Key1928 Value1928\nSET Key1929 Value1929\nSET Key1930 Value1930\nSET Key1931 Value1931\nSET Key1932 Value1932\nSET Key1933 Value1933\nSET Key1934 Value1934\nSET Key1935 Value1935\nSET Key1936 Value1936\nSET Key1937 Value1937\nSET Key1938 Value1938\nSET Key1939 Value1939\nSET Key1940 Value1940\nSET Key1941 Value1941\nSET Key1942 Value1942\nSET Key1943 Value1943\nSET Key1944 Value1944\nSET Key1945 Value1945\nSET Key1946 Value1946\nSET Key1947 Value1947\nSET Key1948 Value1948\nSET Key1949 Value1949\nSET Key1950 Value1950\nSET Key1951 Value1951\nSET Key1952 Value1952\nSET Key1953 Value1953\nSET Key1954 Value1954\nSET Key1955 Value1955\nSET Key1956 Value1956\nSET Key1957 Value1957\nSET Key1958 Value1958\nSET Key1959 Value1959\nSET Key1960 Value1960\nSET Key1961 Value1961\nSET Key1962 Value1962\nSET Key1963 Value1963\nSET Key1964 Value1964\nSET Key1965 Value1965\nSET Key1966 Value1966\nSET Key1967 Value1967\nSET Key1968 Value1968\nSET Key1969 Value1969\nSET Key1970 Value1970\nSET Key1971 Value1971\nSET Key1972 Value1972\nSET Key1973 Value1973\nSET Key1974 Value1974\nSET Key1975 Value1975\nSET Key1976 Value1976\nSET Key1977 Value1977\nSET Key1978 Value1978\nSET Key1979 Value1979\nSET Key1980 Value1980\nSET Key1981 Value1981\nSET Key1982 Value1982\nSET Key1983 Value1983\nSET Key1984 Value1984\nSET Key1985 Value1985\nSET Key1986 Value1986\nSET Key1987 Value1987\nSET Key1988 Value1988\nSET Key1989 Value1989\nSET Key1990 Value1990\nSET Key1991 Value1991\nSET Key1992 Value1992\nSET Key1993 Value1993\nSET Key1994 Value1994\nSET Key1995 Value1995\nSET Key1996 Value1996\nSET Key1997 Value1997\nSET Key1998 Value1998\nSET Key1999 Value1999\nSET Key2000 Value2000\nSET Key2001 Value2001\nSET Key2002 Value2002\nSET Key2003 Value2003\nSET Key2004 Value2004\nSET Key2005 Value2005\nSET Key2006 Value2006\nSET Key2007 Value2007\nSET Key2008 Value2008\nSET Key2009 Value2009\nSET Key2010 Value2010\nSET Key2011 Value2011\nSET Key2012 Value2012\nSET Key2013 Value2013\nSET Key2014 Value2014\nSET Key2015 Value2015\nSET Key2016 Value2016\nSET Key2017 Value2017\nSET Key2018 Value2018\nSET Key2019 Value2019\nSET Key2020 Value2020\nSET Key2021 Value2021\nSET Key2022 Value2022\nSET Key2023 Value2023\nSET Key2024 Value2024\nSET Key2025 Value2025\nSET Key2026 Value2026\nSET Key2027 Value2027\nSET Key2028 Value2028\nSET Key2029 Value2029\nSET Key2030 Value2030\nSET Key2031 Value2031\nSET Key2032 Value2032\nSET Key2033 Value2033\nSET Key2034 Value2034\nSET Key2035 Value2035\nSET Key2036 Value2036\nSET Key2037 Value2037\nSET Key2038 Value2038\nSET Key2039 Value2039\nSET Key2040 Value2040\nSET Key2041 Value2041\nSET Key2042 Value2042\nSET Key2043 Value2043\nSET Key2044 Value2044\nSET Key2045 Value2045\nSET Key2046 Value2046\nSET Key2047 Value2047\nSET Key2048 Value2048\nSET Key2049 Value2049\nSET Key2050 Value2050\nSET Key2051 Value2051\nSET Key2052 Value2052\nSET Key2053 Value2053\nSET Key2054 Value2054\nSET Key2055 Value2055\nSET Key2056 Value2056\nSET Key2057 Value2057\nSET Key2058 Value2058\nSET Key2059 Value2059\nSET Key2060 Value2060\nSET Key2061 Value2061\nSET Key2062 Value2062\nSET Key2063 Value2063\nSET Key2064 Value2064\nSET Key2065 Value2065\nSET Key2066 Value2066\nSET Key2067 Value2067\nSET Key2068 Value2068\nSET Key2069 Value2069\nSET Key2070 Value2070\nSET Key2071 Value2071\nSET Key2072 Value2072\nSET Key2073 Value2073\nSET Key2074 Value2074\nSET Key2075 Value2075\nSET Key2076 Value2076\nSET Key2077 Value2077\nSET Key2078 Value2078\nSET Key2079 Value2079\nSET Key2080 Value2080\nSET Key2081 Value2081\nSET Key2082 Value2082\nSET Key2083 Value2083\nSET Key2084 Value2084\nSET Key2085 Value2085\nSET Key2086 Value2086\nSET Key2087 Value2087\nSET Key2088 Value2088\nSET Key2089 Value2089\nSET Key2090 Value2090\nSET Key2091 Value2091\nSET Key2092 Value2092\nSET Key2093 Value2093\nSET Key2094 Value2094\nSET Key2095 Value2095\nSET Key2096 Value2096\nSET Key2097 Value2097\nSET Key2098 Value2098\nSET Key2099 Value2099\nSET Key2100 Value2100\nSET Key2101 Value2101\nSET Key2102 Value2102\nSET Key2103 Value2103\nSET Key2104 Value2104\nSET Key2105 Value2105\nSET Key2106 Value2106\nSET Key2107 Value2107\nSET Key2108 Value2108\nSET Key2109 Value2109\nSET Key2110 Value2110\nSET Key2111 Value2111\nSET Key2112 Value2112\nSET Key2113 Value2113\nSET Key2114 Value2114\nSET Key2115 Value2115\nSET Key2116 Value2116\nSET Key2117 Value2117\nSET Key2118 Value2118\nSET Key2119 Value2119\nSET Key2120 Value2120\nSET Key2121 Value2121\nSET Key2122 Value2122\nSET Key2123 Value2123\nSET Key2124 Value2124\nSET Key2125 Value2125\nSET Key2126 Value2126\nSET Key2127 Value2127\nSET Key2128 Value2128\nSET Key2129 Value2129\nSET Key2130 Value2130\nSET Key2131 Value2131\nSET Key2132 Value2132\nSET Key2133 Value2133\nSET Key2134 Value2134\nSET Key2135 Value2135\nSET Key2136 Value2136\nSET Key2137 Value2137\nSET Key2138 Value2138\nSET Key2139 Value2139\nSET Key2140 Value2140\nSET Key2141 Value2141\nSET Key2142 Value2142\nSET Key2143 Value2143\nSET Key2144 Value2144\nSET Key2145 Value2145\nSET Key2146 Value2146\nSET Key2147 Value2147\nSET Key2148 Value2148\nSET Key2149 Value2149\nSET Key2150 Value2150\nSET Key2151 Value2151\nSET Key2152 Value2152\nSET Key2153 Value2153\nSET Key2154 Value2154\nSET Key2155 Value2155\nSET Key2156 Value2156\nSET Key2157 Value2157\nSET Key2158 Value2158\nSET Key2159 Value2159\nSET Key2160 Value2160\nSET Key2161 Value2161\nSET Key2162 Value2162\nSET Key2163 Value2163\nSET Key2164 Value2164\nSET Key2165 Value2165\nSET Key2166 Value2166\nSET Key2167 Value2167\nSET Key2168 Value2168\nSET Key2169 Value2169\nSET Key2170 Value2170\nSET Key2171 Value2171\nSET Key2172 Value2172\nSET Key2173 Value2173\nSET Key2174 Value2174\nSET Key2175 Value2175\nSET Key2176 Value2176\nSET Key2177 Value2177\nSET Key2178 Value2178\nSET Key2179 Value2179\nSET Key2180 Value2180\nSET Key2181 Value2181\nSET Key2182 Value2182\nSET Key2183 Value2183\nSET Key2184 Value2184\nSET Key2185 Value2185\nSET Key2186 Value2186\nSET Key2187 Value2187\nSET Key2188 Value2188\nSET Key2189 Value2189\nSET Key2190 Value2190\nSET Key2191 Value2191\nSET Key2192 Value2192\nSET Key2193 Value2193\nSET Key2194 Value2194\nSET Key2195 Value2195\nSET Key2196 Value2196\nSET Key2197 Value2197\nSET Key2198 Value2198\nSET Key2199 Value2199\nSET Key2200 Value2200\nSET Key2201 Value2201\nSET Key2202 Value2202\nSET Key2203 Value2203\nSET Key2204 Value2204\nSET Key2205 Value2205\nSET Key2206 Value2206\nSET Key2207 Value2207\nSET Key2208 Value2208\nSET Key2209 Value2209\nSET Key2210 Value2210\nSET Key2211 Value2211\nSET Key2212 Value2212\nSET Key2213 Value2213\nSET Key2214 Value2214\nSET Key2215 Value2215\nSET Key2216 Value2216\nSET Key2217 Value2217\nSET Key2218 Value2218\nSET Key2219 Value2219\nSET Key2220 Value2220\nSET Key2221 Value2221\nSET Key2222 Value2222\nSET Key2223 Value2223\nSET Key2224 Value2224\nSET Key2225 Value2225\nSET Key2226 Value2226\nSET Key2227 Value2227\nSET Key2228 Value2228\nSET Key2229 Value2229\nSET Key2230 Value2230\nSET Key2231 Value2231\nSET Key2232 Value2232\nSET Key2233 Value2233\nSET Key2234 Value2234\nSET Key2235 Value2235\nSET Key2236 Value2236\nSET Key2237 Value2237\nSET Key2238 Value2238\nSET Key2239 Value2239\nSET Key2240 Value2240\nSET Key2241 Value2241\nSET Key2242 Value2242\nSET Key2243 Value2243\nSET Key2244 Value2244\nSET Key2245 Value2245\nSET Key2246 Value2246\nSET Key2247 Value2247\nSET Key2248 Value2248\nSET Key2249 Value2249\nSET Key2250 Value2250\nSET Key2251 Value2251\nSET Key2252 Value2252\nSET Key2253 Value2253\nSET Key2254 Value2254\nSET Key2255 Value2255\nSET Key2256 Value2256\nSET Key2257 Value2257\nSET Key2258 Value2258\nSET Key2259 Value2259\nSET Key2260 Value2260\nSET Key2261 Value2261\nSET Key2262 Value2262\nSET Key2263 Value2263\nSET Key2264 Value2264\nSET Key2265 Value2265\nSET Key2266 Value2266\nSET Key2267 Value2267\nSET Key2268 Value2268\nSET Key2269 Value2269\nSET Key2270 Value2270\nSET Key2271 Value2271\nSET Key2272 Value2272\nSET Key2273 Value2273\nSET Key2274 Value2274\nSET Key2275 Value2275\nSET Key2276 Value2276\nSET Key2277 Value2277\nSET Key2278 Value2278\nSET Key2279 Value2279\nSET Key2280 Value2280\nSET Key2281 Value2281\nSET Key2282 Value2282\nSET Key2283 Value2283\nSET Key2284 Value2284\nSET Key2285 Value2285\nSET Key2286 Value2286\nSET Key2287 Value2287\nSET Key2288 Value2288\nSET Key2289 Value2289\nSET Key2290 Value2290\nSET Key2291 Value2291\nSET Key2292 Value2292\nSET Key2293 Value2293\nSET Key2294 Value2294\nSET Key2295 Value2295\nSET Key2296 Value2296\nSET Key2297 Value2297\nSET Key2298 Value2298\nSET Key2299 Value2299\nSET Key2300 Value2300\nSET Key2301 Value2301\nSET Key2302 Value2302\nSET Key2303 Value2303\nSET Key2304 Value2304\nSET Key2305 Value2305\nSET Key2306 Value2306\nSET Key2307 Value2307\nSET Key2308 Value2308\nSET Key2309 Value2309\nSET Key2310 Value2310\nSET Key2311 Value2311\nSET Key2312 Value2312\nSET Key2313 Value2313\nSET Key2314 Value2314\nSET Key2315 Value2315\nSET Key2316 Value2316\nSET Key2317 Value2317\nSET Key2318 Value2318\nSET Key2319 Value2319\nSET Key2320 Value2320\nSET Key2321 Value2321\nSET Key2322 Value2322\nSET Key2323 Value2323\nSET Key2324 Value2324\nSET Key2325 Value2325\nSET Key2326 Value2326\nSET Key2327 Value2327\nSET Key2328 Value2328\nSET Key2329 Value2329\nSET Key2330 Value2330\nSET Key2331 Value2331\nSET Key2332 Value2332\nSET Key2333 Value2333\nSET Key2334 Value2334\nSET Key2335 Value2335\nSET Key2336 Value2336\nSET Key2337 Value2337\nSET Key2338 Value2338\nSET Key2339 Value2339\nSET Key2340 Value2340\nSET Key2341 Value2341\nSET Key2342 Value2342\nSET Key2343 Value2343\nSET Key2344 Value2344\nSET Key2345 Value2345\nSET Key2346 Value2346\nSET Key2347 Value2347\nSET Key2348 Value2348\nSET Key2349 Value2349\nSET Key2350 Value2350\nSET Key2351 Value2351\nSET Key2352 Value2352\nSET Key2353 Value2353\nSET Key2354 Value2354\nSET Key2355 Value2355\nSET Key2356 Value2356\nSET Key2357 Value2357\nSET Key2358 Value2358\nSET Key2359 Value2359\nSET Key2360 Value2360\nSET Key2361 Value2361\nSET Key2362 Value2362\nSET Key2363 Value2363\nSET Key2364 Value2364\nSET Key2365 Value2365\nSET Key2366 Value2366\nSET Key2367 Value2367\nSET Key2368 Value2368\nSET Key2369 Value2369\nSET Key2370 Value2370\nSET Key2371 Value2371\nSET Key2372 Value2372\nSET Key2373 Value2373\nSET Key2374 Value2374\nSET Key2375 Value2375\nSET Key2376 Value2376\nSET Key2377 Value2377\nSET Key2378 Value2378\nSET Key2379 Value2379\nSET Key2380 Value2380\nSET Key2381 Value2381\nSET Key2382 Value2382\nSET Key2383 Value2383\nSET Key2384 Value2384\nSET Key2385 Value2385\nSET Key2386 Value2386\nSET Key2387 Value2387\nSET Key2388 Value2388\nSET Key2389 Value2389\nSET Key2390 Value2390\nSET Key2391 Value2391\nSET Key2392 Value2392\nSET Key2393 Value2393\nSET Key2394 Value2394\nSET Key2395 Value2395\nSET Key2396 Value2396\nSET Key2397 Value2397\nSET Key2398 Value2398\nSET Key2399 Value2399\nSET Key2400 Value2400\nSET Key2401 Value2401\nSET Key2402 Value2402\nSET Key2403 Value2403\nSET Key2404 Value2404\nSET Key2405 Value2405\nSET Key2406 Value2406\nSET Key2407 Value2407\nSET Key2408 Value2408\nSET Key2409 Value2409\nSET Key2410 Value2410\nSET Key2411 Value2411\nSET Key2412 Value2412\nSET Key2413 Value2413\nSET Key2414 Value2414\nSET Key2415 Value2415\nSET Key2416 Value2416\nSET Key2417 Value2417\nSET Key2418 Value2418\nSET Key2419 Value2419\nSET Key2420 Value2420\nSET Key2421 Value2421\nSET Key2422 Value2422\nSET Key2423 Value2423\nSET Key2424 Value2424\nSET Key2425 Value2425\nSET Key2426 Value2426\nSET Key2427 Value2427\nSET Key2428 Value2428\nSET Key2429 Value2429\nSET Key2430 Value2430\nSET Key2431 Value2431\nSET Key2432 Value2432\nSET Key2433 Value2433\nSET Key2434 Value2434\nSET Key2435 Value2435\nSET Key2436 Value2436\nSET Key2437 Value2437\nSET Key2438 Value2438\nSET Key2439 Value2439\nSET Key2440 Value2440\nSET Key2441 Value2441\nSET Key2442 Value2442\nSET Key2443 Value2443\nSET Key2444 Value2444\nSET Key2445 Value2445\nSET Key2446 Value2446\nSET Key2447 Value2447\nSET Key2448 Value2448\nSET Key2449 Value2449\nSET Key2450 Value2450\nSET Key2451 Value2451\nSET Key2452 Value2452\nSET Key2453 Value2453\nSET Key2454 Value2454\nSET Key2455 Value2455\nSET Key2456 Value2456\nSET Key2457 Value2457\nSET Key2458 Value2458\nSET Key2459 Value2459\nSET Key2460 Value2460\nSET Key2461 Value2461\nSET Key2462 Value2462\nSET Key2463 Value2463\nSET Key2464 Value2464\nSET Key2465 Value2465\nSET Key2466 Value2466\nSET Key2467 Value2467\nSET Key2468 Value2468\nSET Key2469 Value2469\nSET Key2470 Value2470\nSET Key2471 Value2471\nSET Key2472 Value2472\nSET Key2473 Value2473\nSET Key2474 Value2474\nSET Key2475 Value2475\nSET Key2476 Value2476\nSET Key2477 Value2477\nSET Key2478 Value2478\nSET Key2479 Value2479\nSET Key2480 Value2480\nSET Key2481 Value2481\nSET Key2482 Value2482\nSET Key2483 Value2483\nSET Key2484 Value2484\nSET Key2485 Value2485\nSET Key2486 Value2486\nSET Key2487 Value2487\nSET Key2488 Value2488\nSET Key2489 Value2489\nSET Key2490 Value2490\nSET Key2491 Value2491\nSET Key2492 Value2492\nSET Key2493 Value2493\nSET Key2494 Value2494\nSET Key2495 Value2495\nSET Key2496 Value2496\nSET Key2497 Value2497\nSET Key2498 Value2498\nSET Key2499 Value2499\nSET Key2500 Value2500\nSET Key2501 Value2501\nSET Key2502 Value2502\nSET Key2503 Value2503\nSET Key2504 Value2504\nSET Key2505 Value2505\nSET Key2506 Value2506\nSET Key2507 Value2507\nSET Key2508 Value2508\nSET Key2509 Value2509\nSET Key2510 Value2510\nSET Key2511 Value2511\nSET Key2512 Value2512\nSET Key2513 Value2513\nSET Key2514 Value2514\nSET Key2515 Value2515\nSET Key2516 Value2516\nSET Key2517 Value2517\nSET Key2518 Value2518\nSET Key2519 Value2519\nSET Key2520 Value2520\nSET Key2521 Value2521\nSET Key2522 Value2522\nSET Key2523 Value2523\nSET Key2524 Value2524\nSET Key2525 Value2525\nSET Key2526 Value2526\nSET Key2527 Value2527\nSET Key2528 Value2528\nSET Key2529 Value2529\nSET Key2530 Value2530\nSET Key2531 Value2531\nSET Key2532 Value2532\nSET Key2533 Value2533\nSET Key2534 Value2534\nSET Key2535 Value2535\nSET Key2536 Value2536\nSET Key2537 Value2537\nSET Key2538 Value2538\nSET Key2539 Value2539\nSET Key2540 Value2540\nSET Key2541 Value2541\nSET Key2542 Value2542\nSET Key2543 Value2543\nSET Key2544 Value2544\nSET Key2545 Value2545\nSET Key2546 Value2546\nSET Key2547 Value2547\nSET Key2548 Value2548\nSET Key2549 Value2549\nSET Key2550 Value2550\nSET Key2551 Value2551\nSET Key2552 Value2552\nSET Key2553 Value2553\nSET Key2554 Value2554\nSET Key2555 Value2555\nSET Key2556 Value2556\nSET Key2557 Value2557\nSET Key2558 Value2558\nSET Key2559 Value2559\nSET Key2560 Value2560\nSET Key2561 Value2561\nSET Key2562 Value2562\nSET Key2563 Value2563\nSET Key2564 Value2564\nSET Key2565 Value2565\nSET Key2566 Value2566\nSET Key2567 Value2567\nSET Key2568 Value2568\nSET Key2569 Value2569\nSET Key2570 Value2570\nSET Key2571 Value2571\nSET Key2572 Value2572\nSET Key2573 Value2573\nSET Key2574 Value2574\nSET Key2575 Value2575\nSET Key2576 Value2576\nSET Key2577 Value2577\nSET Key2578 Value2578\nSET Key2579 Value2579\nSET Key2580 Value2580\nSET Key2581 Value2581\nSET Key2582 Value2582\nSET Key2583 Value2583\nSET Key2584 Value2584\nSET Key2585 Value2585\nSET Key2586 Value2586\nSET Key2587 Value2587\nSET Key2588 Value2588\nSET Key2589 Value2589\nSET Key2590 Value2590\nSET Key2591 Value2591\nSET Key2592 Value2592\nSET Key2593 Value2593\nSET Key2594 Value2594\nSET Key2595 Value2595\nSET Key2596 Value2596\nSET Key2597 Value2597\nSET Key2598 Value2598\nSET Key2599 Value2599\nSET Key2600 Value2600\nSET Key2601 Value2601\nSET Key2602 Value2602\nSET Key2603 Value2603\nSET Key2604 Value2604\nSET Key2605 Value2605\nSET Key2606 Value2606\nSET Key2607 Value2607\nSET Key2608 Value2608\nSET Key2609 Value2609\nSET Key2610 Value2610\nSET Key2611 Value2611\nSET Key2612 Value2612\nSET Key2613 Value2613\nSET Key2614 Value2614\nSET Key2615 Value2615\nSET Key2616 Value2616\nSET Key2617 Value2617\nSET Key2618 Value2618\nSET Key2619 Value2619\nSET Key2620 Value2620\nSET Key2621 Value2621\nSET Key2622 Value2622\nSET Key2623 Value2623\nSET Key2624 Value2624\nSET Key2625 Value2625\nSET Key2626 Value2626\nSET Key2627 Value2627\nSET Key2628 Value2628\nSET Key2629 Value2629\nSET Key2630 Value2630\nSET Key2631 Value2631\nSET Key2632 Value2632\nSET Key2633 Value2633\nSET Key2634 Value2634\nSET Key2635 Value2635\nSET Key2636 Value2636\nSET Key2637 Value2637\nSET Key2638 Value2638\nSET Key2639 Value2639\nSET Key2640 Value2640\nSET Key2641 Value2641\nSET Key2642 Value2642\nSET Key2643 Value2643\nSET Key2644 Value2644\nSET Key2645 Value2645\nSET Key2646 Value2646\nSET Key2647 Value2647\nSET Key2648 Value2648\nSET Key2649 Value2649\nSET Key2650 Value2650\nSET Key2651 Value2651\nSET Key2652 Value2652\nSET Key2653 Value2653\nSET Key2654 Value2654\nSET Key2655 Value2655\nSET Key2656 Value2656\nSET Key2657 Value2657\nSET Key2658 Value2658\nSET Key2659 Value2659\nSET Key2660 Value2660\nSET Key2661 Value2661\nSET Key2662 Value2662\nSET Key2663 Value2663\nSET Key2664 Value2664\nSET Key2665 Value2665\nSET Key2666 Value2666\nSET Key2667 Value2667\nSET Key2668 Value2668\nSET Key2669 Value2669\nSET Key2670 Value2670\nSET Key2671 Value2671\nSET Key2672 Value2672\nSET Key2673 Value2673\nSET Key2674 Value2674\nSET Key2675 Value2675\nSET Key2676 Value2676\nSET Key2677 Value2677\nSET Key2678 Value2678\nSET Key2679 Value2679\nSET Key2680 Value2680\nSET Key2681 Value2681\nSET Key2682 Value2682\nSET Key2683 Value2683\nSET Key2684 Value2684\nSET Key2685 Value2685\nSET Key2686 Value2686\nSET Key2687 Value2687\nSET Key2688 Value2688\nSET Key2689 Value2689\nSET Key2690 Value2690\nSET Key2691 Value2691\nSET Key2692 Value2692\nSET Key2693 Value2693\nSET Key2694 Value2694\nSET Key2695 Value2695\nSET Key2696 Value2696\nSET Key2697 Value2697\nSET Key2698 Value2698\nSET Key2699 Value2699\nSET Key2700 Value2700\nSET Key2701 Value2701\nSET Key2702 Value2702\nSET Key2703 Value2703\nSET Key2704 Value2704\nSET Key2705 Value2705\nSET Key2706 Value2706\nSET Key2707 Value2707\nSET Key2708 Value2708\nSET Key2709 Value2709\nSET Key2710 Value2710\nSET Key2711 Value2711\nSET Key2712 Value2712\nSET Key2713 Value2713\nSET Key2714 Value2714\nSET Key2715 Value2715\nSET Key2716 Value2716\nSET Key2717 Value2717\nSET Key2718 Value2718\nSET Key2719 Value2719\nSET Key2720 Value2720\nSET Key2721 Value2721\nSET Key2722 Value2722\nSET Key2723 Value2723\nSET Key2724 Value2724\nSET Key2725 Value2725\nSET Key2726 Value2726\nSET Key2727 Value2727\nSET Key2728 Value2728\nSET Key2729 Value2729\nSET Key2730 Value2730\nSET Key2731 Value2731\nSET Key2732 Value2732\nSET Key2733 Value2733\nSET Key2734 Value2734\nSET Key2735 Value2735\nSET Key2736 Value2736\nSET Key2737 Value2737\nSET Key2738 Value2738\nSET Key2739 Value2739\nSET Key2740 Value2740\nSET Key2741 Value2741\nSET Key2742 Value2742\nSET Key2743 Value2743\nSET Key2744 Value2744\nSET Key2745 Value2745\nSET Key2746 Value2746\nSET Key2747 Value2747\nSET Key2748 Value2748\nSET Key2749 Value2749\nSET Key2750 Value2750\nSET Key2751 Value2751\nSET Key2752 Value2752\nSET Key2753 Value2753\nSET Key2754 Value2754\nSET Key2755 Value2755\nSET Key2756 Value2756\nSET Key2757 Value2757\nSET Key2758 Value2758\nSET Key2759 Value2759\nSET Key2760 Value2760\nSET Key2761 Value2761\nSET Key2762 Value2762\nSET Key2763 Value2763\nSET Key2764 Value2764\nSET Key2765 Value2765\nSET Key2766 Value2766\nSET Key2767 Value2767\nSET Key2768 Value2768\nSET Key2769 Value2769\nSET Key2770 Value2770\nSET Key2771 Value2771\nSET Key2772 Value2772\nSET Key2773 Value2773\nSET Key2774 Value2774\nSET Key2775 Value2775\nSET Key2776 Value2776\nSET Key2777 Value2777\nSET Key2778 Value2778\nSET Key2779 Value2779\nSET Key2780 Value2780\nSET Key2781 Value2781\nSET Key2782 Value2782\nSET Key2783 Value2783\nSET Key2784 Value2784\nSET Key2785 Value2785\nSET Key2786 Value2786\nSET Key2787 Value2787\nSET Key2788 Value2788\nSET Key2789 Value2789\nSET Key2790 Value2790\nSET Key2791 Value2791\nSET Key2792 Value2792\nSET Key2793 Value2793\nSET Key2794 Value2794\nSET Key2795 Value2795\nSET Key2796 Value2796\nSET Key2797 Value2797\nSET Key2798 Value2798\nSET Key2799 Value2799\nSET Key2800 Value2800\nSET Key2801 Value2801\nSET Key2802 Value2802\nSET Key2803 Value2803\nSET Key2804 Value2804\nSET Key2805 Value2805\nSET Key2806 Value2806\nSET Key2807 Value2807\nSET Key2808 Value2808\nSET Key2809 Value2809\nSET Key2810 Value2810\nSET Key2811 Value2811\nSET Key2812 Value2812\nSET Key2813 Value2813\nSET Key2814 Value2814\nSET Key2815 Value2815\nSET Key2816 Value2816\nSET Key2817 Value2817\nSET Key2818 Value2818\nSET Key2819 Value2819\nSET Key2820 Value2820\nSET Key2821 Value2821\nSET Key2822 Value2822\nSET Key2823 Value2823\nSET Key2824 Value2824\nSET Key2825 Value2825\nSET Key2826 Value2826\nSET Key2827 Value2827\nSET Key2828 Value2828\nSET Key2829 Value2829\nSET Key2830 Value2830\nSET Key2831 Value2831\nSET Key2832 Value2832\nSET Key2833 Value2833\nSET Key2834 Value2834\nSET Key2835 Value2835\nSET Key2836 Value2836\nSET Key2837 Value2837\nSET Key2838 Value2838\nSET Key2839 Value2839\nSET Key2840 Value2840\nSET Key2841 Value2841\nSET Key2842 Value2842\nSET Key2843 Value2843\nSET Key2844 Value2844\nSET Key2845 Value2845\nSET Key2846 Value2846\nSET Key2847 Value2847\nSET Key2848 Value2848\nSET Key2849 Value2849\nSET Key2850 Value2850\nSET Key2851 Value2851\nSET Key2852 Value2852\nSET Key2853 Value2853\nSET Key2854 Value2854\nSET Key2855 Value2855\nSET Key2856 Value2856\nSET Key2857 Value2857\nSET Key2858 Value2858\nSET Key2859 Value2859\nSET Key2860 Value2860\nSET Key2861 Value2861\nSET Key2862 Value2862\nSET Key2863 Value2863\nSET Key2864 Value2864\nSET Key2865 Value2865\nSET Key2866 Value2866\nSET Key2867 Value2867\nSET Key2868 Value2868\nSET Key2869 Value2869\nSET Key2870 Value2870\nSET Key2871 Value2871\nSET Key2872 Value2872\nSET Key2873 Value2873\nSET Key2874 Value2874\nSET Key2875 Value2875\nSET Key2876 Value2876\nSET Key2877 Value2877\nSET Key2878 Value2878\nSET Key2879 Value2879\nSET Key2880 Value2880\nSET Key2881 Value2881\nSET Key2882 Value2882\nSET Key2883 Value2883\nSET Key2884 Value2884\nSET Key2885 Value2885\nSET Key2886 Value2886\nSET Key2887 Value2887\nSET Key2888 Value2888\nSET Key2889 Value2889\nSET Key2890 Value2890\nSET Key2891 Value2891\nSET Key2892 Value2892\nSET Key2893 Value2893\nSET Key2894 Value2894\nSET Key2895 Value2895\nSET Key2896 Value2896\nSET Key2897 Value2897\nSET Key2898 Value2898\nSET Key2899 Value2899\nSET Key2900 Value2900\nSET Key2901 Value2901\nSET Key2902 Value2902\nSET Key2903 Value2903\nSET Key2904 Value2904\nSET Key2905 Value2905\nSET Key2906 Value2906\nSET Key2907 Value2907\nSET Key2908 Value2908\nSET Key2909 Value2909\nSET Key2910 Value2910\nSET Key2911 Value2911\nSET Key2912 Value2912\nSET Key2913 Value2913\nSET Key2914 Value2914\nSET Key2915 Value2915\nSET Key2916 Value2916\nSET Key2917 Value2917\nSET Key2918 Value2918\nSET Key2919 Value2919\nSET Key2920 Value2920\nSET Key2921 Value2921\nSET Key2922 Value2922\nSET Key2923 Value2923\nSET Key2924 Value2924\nSET Key2925 Value2925\nSET Key2926 Value2926\nSET Key2927 Value2927\nSET Key2928 Value2928\nSET Key2929 Value2929\nSET Key2930 Value2930\nSET Key2931 Value2931\nSET Key2932 Value2932\nSET Key2933 Value2933\nSET Key2934 Value2934\nSET Key2935 Value2935\nSET Key2936 Value2936\nSET Key2937 Value2937\nSET Key2938 Value2938\nSET Key2939 Value2939\nSET Key2940 Value2940\nSET Key2941 Value2941\nSET Key2942 Value2942\nSET Key2943 Value2943\nSET Key2944 Value2944\nSET Key2945 Value2945\nSET Key2946 Value2946\nSET Key2947 Value2947\nSET Key2948 Value2948\nSET Key2949 Value2949\nSET Key2950 Value2950\nSET Key2951 Value2951\nSET Key2952 Value2952\nSET Key2953 Value2953\nSET Key2954 Value2954\nSET Key2955 Value2955\nSET Key2956 Value2956\nSET Key2957 Value2957\nSET Key2958 Value2958\nSET Key2959 Value2959\nSET Key2960 Value2960\nSET Key2961 Value2961\nSET Key2962 Value2962\nSET Key2963 Value2963\nSET Key2964 Value2964\nSET Key2965 Value2965\nSET Key2966 Value2966\nSET Key2967 Value2967\nSET Key2968 Value2968\nSET Key2969 Value2969\nSET Key2970 Value2970\nSET Key2971 Value2971\nSET Key2972 Value2972\nSET Key2973 Value2973\nSET Key2974 Value2974\nSET Key2975 Value2975\nSET Key2976 Value2976\nSET Key2977 Value2977\nSET Key2978 Value2978\nSET Key2979 Value2979\nSET Key2980 Value2980\nSET Key2981 Value2981\nSET Key2982 Value2982\nSET Key2983 Value2983\nSET Key2984 Value2984\nSET Key2985 Value2985\nSET Key2986 Value2986\nSET Key2987 Value2987\nSET Key2988 Value2988\nSET Key2989 Value2989\nSET Key2990 Value2990\nSET Key2991 Value2991\nSET Key2992 Value2992\nSET Key2993 Value2993\nSET Key2994 Value2994\nSET Key2995 Value2995\nSET Key2996 Value2996\nSET Key2997 Value2997\nSET Key2998 Value2998\nSET Key2999 Value2999\nSET Key3000 Value3000\nSET Key3001 Value3001\nSET Key3002 Value3002\nSET Key3003 Value3003\nSET Key3004 Value3004\nSET Key3005 Value3005\nSET Key3006 Value3006\nSET Key3007 Value3007\nSET Key3008 Value3008\nSET Key3009 Value3009\nSET Key3010 Value3010\nSET Key3011 Value3011\nSET Key3012 Value3012\nSET Key3013 Value3013\nSET Key3014 Value3014\nSET Key3015 Value3015\nSET Key3016 Value3016\nSET Key3017 Value3017\nSET Key3018 Value3018\nSET Key3019 Value3019\nSET Key3020 Value3020\nSET Key3021 Value3021\nSET Key3022 Value3022\nSET Key3023 Value3023\nSET Key3024 Value3024\nSET Key3025 Value3025\nSET Key3026 Value3026\nSET Key3027 Value3027\nSET Key3028 Value3028\nSET Key3029 Value3029\nSET Key3030 Value3030\nSET Key3031 Value3031\nSET Key3032 Value3032\nSET Key3033 Value3033\nSET Key3034 Value3034\nSET Key3035 Value3035\nSET Key3036 Value3036\nSET Key3037 Value3037\nSET Key3038 Value3038\nSET Key3039 Value3039\nSET Key3040 Value3040\nSET Key3041 Value3041\nSET Key3042 Value3042\nSET Key3043 Value3043\nSET Key3044 Value3044\nSET Key3045 Value3045\nSET Key3046 Value3046\nSET Key3047 Value3047\nSET Key3048 Value3048\nSET Key3049 Value3049\nSET Key3050 Value3050\nSET Key3051 Value3051\nSET Key3052 Value3052\nSET Key3053 Value3053\nSET Key3054 Value3054\nSET Key3055 Value3055\nSET Key3056 Value3056\nSET Key3057 Value3057\nSET Key3058 Value3058\nSET Key3059 Value3059\nSET Key3060 Value3060\nSET Key3061 Value3061\nSET Key3062 Value3062\nSET Key3063 Value3063\nSET Key3064 Value3064\nSET Key3065 Value3065\nSET Key3066 Value3066\nSET Key3067 Value3067\nSET Key3068 Value3068\nSET Key3069 Value3069\nSET Key3070 Value3070\nSET Key3071 Value3071\nSET Key3072 Value3072\nSET Key3073 Value3073\nSET Key3074 Value3074\nSET Key3075 Value3075\nSET Key3076 Value3076\nSET Key3077 Value3077\nSET Key3078 Value3078\nSET Key3079 Value3079\nSET Key3080 Value3080\nSET Key3081 Value3081\nSET Key3082 Value3082\nSET Key3083 Value3083\nSET Key3084 Value3084\nSET Key3085 Value3085\nSET Key3086 Value3086\nSET Key3087 Value3087\nSET Key3088 Value3088\nSET Key3089 Value3089\nSET Key3090 Value3090\nSET Key3091 Value3091\nSET Key3092 Value3092\nSET Key3093 Value3093\nSET Key3094 Value3094\nSET Key3095 Value3095\nSET Key3096 Value3096\nSET Key3097 Value3097\nSET Key3098 Value3098\nSET Key3099 Value3099\nSET Key3100 Value3100\nSET Key3101 Value3101\nSET Key3102 Value3102\nSET Key3103 Value3103\nSET Key3104 Value3104\nSET Key3105 Value3105\nSET Key3106 Value3106\nSET Key3107 Value3107\nSET Key3108 Value3108\nSET Key3109 Value3109\nSET Key3110 Value3110\nSET Key3111 Value3111\nSET Key3112 Value3112\nSET Key3113 Value3113\nSET Key3114 Value3114\nSET Key3115 Value3115\nSET Key3116 Value3116\nSET Key3117 Value3117\nSET Key3118 Value3118\nSET Key3119 Value3119\nSET Key3120 Value3120\nSET Key3121 Value3121\nSET Key3122 Value3122\nSET Key3123 Value3123\nSET Key3124 Value3124\nSET Key3125 Value3125\nSET Key3126 Value3126\nSET Key3127 Value3127\nSET Key3128 Value3128\nSET Key3129 Value3129\nSET Key3130 Value3130\nSET Key3131 Value3131\nSET Key3132 Value3132\nSET Key3133 Value3133\nSET Key3134 Value3134\nSET Key3135 Value3135\nSET Key3136 Value3136\nSET Key3137 Value3137\nSET Key3138 Value3138\nSET Key3139 Value3139\nSET Key3140 Value3140\nSET Key3141 Value3141\nSET Key3142 Value3142\nSET Key3143 Value3143\nSET Key3144 Value3144\nSET Key3145 Value3145\nSET Key3146 Value3146\nSET Key3147 Value3147\nSET Key3148 Value3148\nSET Key3149 Value3149\nSET Key3150 Value3150\nSET Key3151 Value3151\nSET Key3152 Value3152\nSET Key3153 Value3153\nSET Key3154 Value3154\nSET Key3155 Value3155\nSET Key3156 Value3156\nSET Key3157 Value3157\nSET Key3158 Value3158\nSET Key3159 Value3159\nSET Key3160 Value3160\nSET Key3161 Value3161\nSET Key3162 Value3162\nSET Key3163 Value3163\nSET Key3164 Value3164\nSET Key3165 Value3165\nSET Key3166 Value3166\nSET Key3167 Value3167\nSET Key3168 Value3168\nSET Key3169 Value3169\nSET Key3170 Value3170\nSET Key3171 Value3171\nSET Key3172 Value3172\nSET Key3173 Value3173\nSET Key3174 Value3174\nSET Key3175 Value3175\nSET Key3176 Value3176\nSET Key3177 Value3177\nSET Key3178 Value3178\nSET Key3179 Value3179\nSET Key3180 Value3180\nSET Key3181 Value3181\nSET Key3182 Value3182\nSET Key3183 Value3183\nSET Key3184 Value3184\nSET Key3185 Value3185\nSET Key3186 Value3186\nSET Key3187 Value3187\nSET Key3188 Value3188\nSET Key3189 Value3189\nSET Key3190 Value3190\nSET Key3191 Value3191\nSET Key3192 Value3192\nSET Key3193 Value3193\nSET Key3194 Value3194\nSET Key3195 Value3195\nSET Key3196 Value3196\nSET Key3197 Value3197\nSET Key3198 Value3198\nSET Key3199 Value3199\nSET Key3200 Value3200\nSET Key3201 Value3201\nSET Key3202 Value3202\nSET Key3203 Value3203\nSET Key3204 Value3204\nSET Key3205 Value3205\nSET Key3206 Value3206\nSET Key3207 Value3207\nSET Key3208 Value3208\nSET Key3209 Value3209\nSET Key3210 Value3210\nSET Key3211 Value3211\nSET Key3212 Value3212\nSET Key3213 Value3213\nSET Key3214 Value3214\nSET Key3215 Value3215\nSET Key3216 Value3216\nSET Key3217 Value3217\nSET Key3218 Value3218\nSET Key3219 Value3219\nSET Key3220 Value3220\nSET Key3221 Value3221\nSET Key3222 Value3222\nSET Key3223 Value3223\nSET Key3224 Value3224\nSET Key3225 Value3225\nSET Key3226 Value3226\nSET Key3227 Value3227\nSET Key3228 Value3228\nSET Key3229 Value3229\nSET Key3230 Value3230\nSET Key3231 Value3231\nSET Key3232 Value3232\nSET Key3233 Value3233\nSET Key3234 Value3234\nSET Key3235 Value3235\nSET Key3236 Value3236\nSET Key3237 Value3237\nSET Key3238 Value3238\nSET Key3239 Value3239\nSET Key3240 Value3240\nSET Key3241 Value3241\nSET Key3242 Value3242\nSET Key3243 Value3243\nSET Key3244 Value3244\nSET Key3245 Value3245\nSET Key3246 Value3246\nSET Key3247 Value3247\nSET Key3248 Value3248\nSET Key3249 Value3249\nSET Key3250 Value3250\nSET Key3251 Value3251\nSET Key3252 Value3252\nSET Key3253 Value3253\nSET Key3254 Value3254\nSET Key3255 Value3255\nSET Key3256 Value3256\nSET Key3257 Value3257\nSET Key3258 Value3258\nSET Key3259 Value3259\nSET Key3260 Value3260\nSET Key3261 Value3261\nSET Key3262 Value3262\nSET Key3263 Value3263\nSET Key3264 Value3264\nSET Key3265 Value3265\nSET Key3266 Value3266\nSET Key3267 Value3267\nSET Key3268 Value3268\nSET Key3269 Value3269\nSET Key3270 Value3270\nSET Key3271 Value3271\nSET Key3272 Value3272\nSET Key3273 Value3273\nSET Key3274 Value3274\nSET Key3275 Value3275\nSET Key3276 Value3276\nSET Key3277 Value3277\nSET Key3278 Value3278\nSET Key3279 Value3279\nSET Key3280 Value3280\nSET Key3281 Value3281\nSET Key3282 Value3282\nSET Key3283 Value3283\nSET Key3284 Value3284\nSET Key3285 Value3285\nSET Key3286 Value3286\nSET Key3287 Value3287\nSET Key3288 Value3288\nSET Key3289 Value3289\nSET Key3290 Value3290\nSET Key3291 Value3291\nSET Key3292 Value3292\nSET Key3293 Value3293\nSET Key3294 Value3294\nSET Key3295 Value3295\nSET Key3296 Value3296\nSET Key3297 Value3297\nSET Key3298 Value3298\nSET Key3299 Value3299\nSET Key3300 Value3300\nSET Key3301 Value3301\nSET Key3302 Value3302\nSET Key3303 Value3303\nSET Key3304 Value3304\nSET Key3305 Value3305\nSET Key3306 Value3306\nSET Key3307 Value3307\nSET Key3308 Value3308\nSET Key3309 Value3309\nSET Key3310 Value3310\nSET Key3311 Value3311\nSET Key3312 Value3312\nSET Key3313 Value3313\nSET Key3314 Value3314\nSET Key3315 Value3315\nSET Key3316 Value3316\nSET Key3317 Value3317\nSET Key3318 Value3318\nSET Key3319 Value3319\nSET Key3320 Value3320\nSET Key3321 Value3321\nSET Key3322 Value3322\nSET Key3323 Value3323\nSET Key3324 Value3324\nSET Key3325 Value3325\nSET Key3326 Value3326\nSET Key3327 Value3327\nSET Key3328 Value3328\nSET Key3329 Value3329\nSET Key3330 Value3330\nSET Key3331 Value3331\nSET Key3332 Value3332\nSET Key3333 Value3333\nSET Key3334 Value3334\nSET Key3335 Value3335\nSET Key3336 Value3336\nSET Key3337 Value3337\nSET Key3338 Value3338\nSET Key3339 Value3339\nSET Key3340 Value3340\nSET Key3341 Value3341\nSET Key3342 Value3342\nSET Key3343 Value3343\nSET Key3344 Value3344\nSET Key3345 Value3345\nSET Key3346 Value3346\nSET Key3347 Value3347\nSET Key3348 Value3348\nSET Key3349 Value3349\nSET Key3350 Value3350\nSET Key3351 Value3351\nSET Key3352 Value3352\nSET Key3353 Value3353\nSET Key3354 Value3354\nSET Key3355 Value3355\nSET Key3356 Value3356\nSET Key3357 Value3357\nSET Key3358 Value3358\nSET Key3359 Value3359\nSET Key3360 Value3360\nSET Key3361 Value3361\nSET Key3362 Value3362\nSET Key3363 Value3363\nSET Key3364 Value3364\nSET Key3365 Value3365\nSET Key3366 Value3366\nSET Key3367 Value3367\nSET Key3368 Value3368\nSET Key3369 Value3369\nSET Key3370 Value3370\nSET Key3371 Value3371\nSET Key3372 Value3372\nSET Key3373 Value3373\nSET Key3374 Value3374\nSET Key3375 Value3375\nSET Key3376 Value3376\nSET Key3377 Value3377\nSET Key3378 Value3378\nSET Key3379 Value3379\nSET Key3380 Value3380\nSET Key3381 Value3381\nSET Key3382 Value3382\nSET Key3383 Value3383\nSET Key3384 Value3384\nSET Key3385 Value3385\nSET Key3386 Value3386\nSET Key3387 Value3387\nSET Key3388 Value3388\nSET Key3389 Value3389\nSET Key3390 Value3390\nSET Key3391 Value3391\nSET Key3392 Value3392\nSET Key3393 Value3393\nSET Key3394 Value3394\nSET Key3395 Value3395\nSET Key3396 Value3396\nSET Key3397 Value3397\nSET Key3398 Value3398\nSET Key3399 Value3399\nSET Key3400 Value3400\nSET Key3401 Value3401\nSET Key3402 Value3402\nSET Key3403 Value3403\nSET Key3404 Value3404\nSET Key3405 Value3405\nSET Key3406 Value3406\nSET Key3407 Value3407\nSET Key3408 Value3408\nSET Key3409 Value3409\nSET Key3410 Value3410\nSET Key3411 Value3411\nSET Key3412 Value3412\nSET Key3413 Value3413\nSET Key3414 Value3414\nSET Key3415 Value3415\nSET Key3416 Value3416\nSET Key3417 Value3417\nSET Key3418 Value3418\nSET Key3419 Value3419\nSET Key3420 Value3420\nSET Key3421 Value3421\nSET Key3422 Value3422\nSET Key3423 Value3423\nSET Key3424 Value3424\nSET Key3425 Value3425\nSET Key3426 Value3426\nSET Key3427 Value3427\nSET Key3428 Value3428\nSET Key3429 Value3429\nSET Key3430 Value3430\nSET Key3431 Value3431\nSET Key3432 Value3432\nSET Key3433 Value3433\nSET Key3434 Value3434\nSET Key3435 Value3435\nSET Key3436 Value3436\nSET Key3437 Value3437\nSET Key3438 Value3438\nSET Key3439 Value3439\nSET Key3440 Value3440\nSET Key3441 Value3441\nSET Key3442 Value3442\nSET Key3443 Value3443\nSET Key3444 Value3444\nSET Key3445 Value3445\nSET Key3446 Value3446\nSET Key3447 Value3447\nSET Key3448 Value3448\nSET Key3449 Value3449\nSET Key3450 Value3450\nSET Key3451 Value3451\nSET Key3452 Value3452\nSET Key3453 Value3453\nSET Key3454 Value3454\nSET Key3455 Value3455\nSET Key3456 Value3456\nSET Key3457 Value3457\nSET Key3458 Value3458\nSET Key3459 Value3459\nSET Key3460 Value3460\nSET Key3461 Value3461\nSET Key3462 Value3462\nSET Key3463 Value3463\nSET Key3464 Value3464\nSET Key3465 Value3465\nSET Key3466 Value3466\nSET Key3467 Value3467\nSET Key3468 Value3468\nSET Key3469 Value3469\nSET Key3470 Value3470\nSET Key3471 Value3471\nSET Key3472 Value3472\nSET Key3473 Value3473\nSET Key3474 Value3474\nSET Key3475 Value3475\nSET Key3476 Value3476\nSET Key3477 Value3477\nSET Key3478 Value3478\nSET Key3479 Value3479\nSET Key3480 Value3480\nSET Key3481 Value3481\nSET Key3482 Value3482\nSET Key3483 Value3483\nSET Key3484 Value3484\nSET Key3485 Value3485\nSET Key3486 Value3486\nSET Key3487 Value3487\nSET Key3488 Value3488\nSET Key3489 Value3489\nSET Key3490 Value3490\nSET Key3491 Value3491\nSET Key3492 Value3492\nSET Key3493 Value3493\nSET Key3494 Value3494\nSET Key3495 Value3495\nSET Key3496 Value3496\nSET Key3497 Value3497\nSET Key3498 Value3498\nSET Key3499 Value3499\nSET Key3500 Value3500\nSET Key3501 Value3501\nSET Key3502 Value3502\nSET Key3503 Value3503\nSET Key3504 Value3504\nSET Key3505 Value3505\nSET Key3506 Value3506\nSET Key3507 Value3507\nSET Key3508 Value3508\nSET Key3509 Value3509\nSET Key3510 Value3510\nSET Key3511 Value3511\nSET Key3512 Value3512\nSET Key3513 Value3513\nSET Key3514 Value3514\nSET Key3515 Value3515\nSET Key3516 Value3516\nSET Key3517 Value3517\nSET Key3518 Value3518\nSET Key3519 Value3519\nSET Key3520 Value3520\nSET Key3521 Value3521\nSET Key3522 Value3522\nSET Key3523 Value3523\nSET Key3524 Value3524\nSET Key3525 Value3525\nSET Key3526 Value3526\nSET Key3527 Value3527\nSET Key3528 Value3528\nSET Key3529 Value3529\nSET Key3530 Value3530\nSET Key3531 Value3531\nSET Key3532 Value3532\nSET Key3533 Value3533\nSET Key3534 Value3534\nSET Key3535 Value3535\nSET Key3536 Value3536\nSET Key3537 Value3537\nSET Key3538 Value3538\nSET Key3539 Value3539\nSET Key3540 Value3540\nSET Key3541 Value3541\nSET Key3542 Value3542\nSET Key3543 Value3543\nSET Key3544 Value3544\nSET Key3545 Value3545\nSET Key3546 Value3546\nSET Key3547 Value3547\nSET Key3548 Value3548\nSET Key3549 Value3549\nSET Key3550 Value3550\nSET Key3551 Value3551\nSET Key3552 Value3552\nSET Key3553 Value3553\nSET Key3554 Value3554\nSET Key3555 Value3555\nSET Key3556 Value3556\nSET Key3557 Value3557\nSET Key3558 Value3558\nSET Key3559 Value3559\nSET Key3560 Value3560\nSET Key3561 Value3561\nSET Key3562 Value3562\nSET Key3563 Value3563\nSET Key3564 Value3564\nSET Key3565 Value3565\nSET Key3566 Value3566\nSET Key3567 Value3567\nSET Key3568 Value3568\nSET Key3569 Value3569\nSET Key3570 Value3570\nSET Key3571 Value3571\nSET Key3572 Value3572\nSET Key3573 Value3573\nSET Key3574 Value3574\nSET Key3575 Value3575\nSET Key3576 Value3576\nSET Key3577 Value3577\nSET Key3578 Value3578\nSET Key3579 Value3579\nSET Key3580 Value3580\nSET Key3581 Value3581\nSET Key3582 Value3582\nSET Key3583 Value3583\nSET Key3584 Value3584\nSET Key3585 Value3585\nSET Key3586 Value3586\nSET Key3587 Value3587\nSET Key3588 Value3588\nSET Key3589 Value3589\nSET Key3590 Value3590\nSET Key3591 Value3591\nSET Key3592 Value3592\nSET Key3593 Value3593\nSET Key3594 Value3594\nSET Key3595 Value3595\nSET Key3596 Value3596\nSET Key3597 Value3597\nSET Key3598 Value3598\nSET Key3599 Value3599\nSET Key3600 Value3600\nSET Key3601 Value3601\nSET Key3602 Value3602\nSET Key3603 Value3603\nSET Key3604 Value3604\nSET Key3605 Value3605\nSET Key3606 Value3606\nSET Key3607 Value3607\nSET Key3608 Value3608\nSET Key3609 Value3609\nSET Key3610 Value3610\nSET Key3611 Value3611\nSET Key3612 Value3612\nSET Key3613 Value3613\nSET Key3614 Value3614\nSET Key3615 Value3615\nSET Key3616 Value3616\nSET Key3617 Value3617\nSET Key3618 Value3618\nSET Key3619 Value3619\nSET Key3620 Value3620\nSET Key3621 Value3621\nSET Key3622 Value3622\nSET Key3623 Value3623\nSET Key3624 Value3624\nSET Key3625 Value3625\nSET Key3626 Value3626\nSET Key3627 Value3627\nSET Key3628 Value3628\nSET Key3629 Value3629\nSET Key3630 Value3630\nSET Key3631 Value3631\nSET Key3632 Value3632\nSET Key3633 Value3633\nSET Key3634 Value3634\nSET Key3635 Value3635\nSET Key3636 Value3636\nSET Key3637 Value3637\nSET Key3638 Value3638\nSET Key3639 Value3639\nSET Key3640 Value3640\nSET Key3641 Value3641\nSET Key3642 Value3642\nSET Key3643 Value3643\nSET Key3644 Value3644\nSET Key3645 Value3645\nSET Key3646 Value3646\nSET Key3647 Value3647\nSET Key3648 Value3648\nSET Key3649 Value3649\nSET Key3650 Value3650\nSET Key3651 Value3651\nSET Key3652 Value3652\nSET Key3653 Value3653\nSET Key3654 Value3654\nSET Key3655 Value3655\nSET Key3656 Value3656\nSET Key3657 Value3657\nSET Key3658 Value3658\nSET Key3659 Value3659\nSET Key3660 Value3660\nSET Key3661 Value3661\nSET Key3662 Value3662\nSET Key3663 Value3663\nSET Key3664 Value3664\nSET Key3665 Value3665\nSET Key3666 Value3666\nSET Key3667 Value3667\nSET Key3668 Value3668\nSET Key3669 Value3669\nSET Key3670 Value3670\nSET Key3671 Value3671\nSET Key3672 Value3672\nSET Key3673 Value3673\nSET Key3674 Value3674\nSET Key3675 Value3675\nSET Key3676 Value3676\nSET Key3677 Value3677\nSET Key3678 Value3678\nSET Key3679 Value3679\nSET Key3680 Value3680\nSET Key3681 Value3681\nSET Key3682 Value3682\nSET Key3683 Value3683\nSET Key3684 Value3684\nSET Key3685 Value3685\nSET Key3686 Value3686\nSET Key3687 Value3687\nSET Key3688 Value3688\nSET Key3689 Value3689\nSET Key3690 Value3690\nSET Key3691 Value3691\nSET Key3692 Value3692\nSET Key3693 Value3693\nSET Key3694 Value3694\nSET Key3695 Value3695\nSET Key3696 Value3696\nSET Key3697 Value3697\nSET Key3698 Value3698\nSET Key3699 Value3699\nSET Key3700 Value3700\nSET Key3701 Value3701\nSET Key3702 Value3702\nSET Key3703 Value3703\nSET Key3704 Value3704\nSET Key3705 Value3705\nSET Key3706 Value3706\nSET Key3707 Value3707\nSET Key3708 Value3708\nSET Key3709 Value3709\nSET Key3710 Value3710\nSET Key3711 Value3711\nSET Key3712 Value3712\nSET Key3713 Value3713\nSET Key3714 Value3714\nSET Key3715 Value3715\nSET Key3716 Value3716\nSET Key3717 Value3717\nSET Key3718 Value3718\nSET Key3719 Value3719\nSET Key3720 Value3720\nSET Key3721 Value3721\nSET Key3722 Value3722\nSET Key3723 Value3723\nSET Key3724 Value3724\nSET Key3725 Value3725\nSET Key3726 Value3726\nSET Key3727 Value3727\nSET Key3728 Value3728\nSET Key3729 Value3729\nSET Key3730 Value3730\nSET Key3731 Value3731\nSET Key3732 Value3732\nSET Key3733 Value3733\nSET Key3734 Value3734\nSET Key3735 Value3735\nSET Key3736 Value3736\nSET Key3737 Value3737\nSET Key3738 Value3738\nSET Key3739 Value3739\nSET Key3740 Value3740\nSET Key3741 Value3741\nSET Key3742 Value3742\nSET Key3743 Value3743\nSET Key3744 Value3744\nSET Key3745 Value3745\nSET Key3746 Value3746\nSET Key3747 Value3747\nSET Key3748 Value3748\nSET Key3749 Value3749\nSET Key3750 Value3750\nSET Key3751 Value3751\nSET Key3752 Value3752\nSET Key3753 Value3753\nSET Key3754 Value3754\nSET Key3755 Value3755\nSET Key3756 Value3756\nSET Key3757 Value3757\nSET Key3758 Value3758\nSET Key3759 Value3759\nSET Key3760 Value3760\nSET Key3761 Value3761\nSET Key3762 Value3762\nSET Key3763 Value3763\nSET Key3764 Value3764\nSET Key3765 Value3765\nSET Key3766 Value3766\nSET Key3767 Value3767\nSET Key3768 Value3768\nSET Key3769 Value3769\nSET Key3770 Value3770\nSET Key3771 Value3771\nSET Key3772 Value3772\nSET Key3773 Value3773\nSET Key3774 Value3774\nSET Key3775 Value3775\nSET Key3776 Value3776\nSET Key3777 Value3777\nSET Key3778 Value3778\nSET Key3779 Value3779\nSET Key3780 Value3780\nSET Key3781 Value3781\nSET Key3782 Value3782\nSET Key3783 Value3783\nSET Key3784 Value3784\nSET Key3785 Value3785\nSET Key3786 Value3786\nSET Key3787 Value3787\nSET Key3788 Value3788\nSET Key3789 Value3789\nSET Key3790 Value3790\nSET Key3791 Value3791\nSET Key3792 Value3792\nSET Key3793 Value3793\nSET Key3794 Value3794\nSET Key3795 Value3795\nSET Key3796 Value3796\nSET Key3797 Value3797\nSET Key3798 Value3798\nSET Key3799 Value3799\nSET Key3800 Value3800\nSET Key3801 Value3801\nSET Key3802 Value3802\nSET Key3803 Value3803\nSET Key3804 Value3804\nSET Key3805 Value3805\nSET Key3806 Value3806\nSET Key3807 Value3807\nSET Key3808 Value3808\nSET Key3809 Value3809\nSET Key3810 Value3810\nSET Key3811 Value3811\nSET Key3812 Value3812\nSET Key3813 Value3813\nSET Key3814 Value3814\nSET Key3815 Value3815\nSET Key3816 Value3816\nSET Key3817 Value3817\nSET Key3818 Value3818\nSET Key3819 Value3819\nSET Key3820 Value3820\nSET Key3821 Value3821\nSET Key3822 Value3822\nSET Key3823 Value3823\nSET Key3824 Value3824\nSET Key3825 Value3825\nSET Key3826 Value3826\nSET Key3827 Value3827\nSET Key3828 Value3828\nSET Key3829 Value3829\nSET Key3830 Value3830\nSET Key3831 Value3831\nSET Key3832 Value3832\nSET Key3833 Value3833\nSET Key3834 Value3834\nSET Key3835 Value3835\nSET Key3836 Value3836\nSET Key3837 Value3837\nSET Key3838 Value3838\nSET Key3839 Value3839\nSET Key3840 Value3840\nSET Key3841 Value3841\nSET Key3842 Value3842\nSET Key3843 Value3843\nSET Key3844 Value3844\nSET Key3845 Value3845\nSET Key3846 Value3846\nSET Key3847 Value3847\nSET Key3848 Value3848\nSET Key3849 Value3849\nSET Key3850 Value3850\nSET Key3851 Value3851\nSET Key3852 Value3852\nSET Key3853 Value3853\nSET Key3854 Value3854\nSET Key3855 Value3855\nSET Key3856 Value3856\nSET Key3857 Value3857\nSET Key3858 Value3858\nSET Key3859 Value3859\nSET Key3860 Value3860\nSET Key3861 Value3861\nSET Key3862 Value3862\nSET Key3863 Value3863\nSET Key3864 Value3864\nSET Key3865 Value3865\nSET Key3866 Value3866\nSET Key3867 Value3867\nSET Key3868 Value3868\nSET Key3869 Value3869\nSET Key3870 Value3870\nSET Key3871 Value3871\nSET Key3872 Value3872\nSET Key3873 Value3873\nSET Key3874 Value3874\nSET Key3875 Value3875\nSET Key3876 Value3876\nSET Key3877 Value3877\nSET Key3878 Value3878\nSET Key3879 Value3879\nSET Key3880 Value3880\nSET Key3881 Value3881\nSET Key3882 Value3882\nSET Key3883 Value3883\nSET Key3884 Value3884\nSET Key3885 Value3885\nSET Key3886 Value3886\nSET Key3887 Value3887\nSET Key3888 Value3888\nSET Key3889 Value3889\nSET Key3890 Value3890\nSET Key3891 Value3891\nSET Key3892 Value3892\nSET Key3893 Value3893\nSET Key3894 Value3894\nSET Key3895 Value3895\nSET Key3896 Value3896\nSET Key3897 Value3897\nSET Key3898 Value3898\nSET Key3899 Value3899\nSET Key3900 Value3900\nSET Key3901 Value3901\nSET Key3902 Value3902\nSET Key3903 Value3903\nSET Key3904 Value3904\nSET Key3905 Value3905\nSET Key3906 Value3906\nSET Key3907 Value3907\nSET Key3908 Value3908\nSET Key3909 Value3909\nSET Key3910 Value3910\nSET Key3911 Value3911\nSET Key3912 Value3912\nSET Key3913 Value3913\nSET Key3914 Value3914\nSET Key3915 Value3915\nSET Key3916 Value3916\nSET Key3917 Value3917\nSET Key3918 Value3918\nSET Key3919 Value3919\nSET Key3920 Value3920\nSET Key3921 Value3921\nSET Key3922 Value3922\nSET Key3923 Value3923\nSET Key3924 Value3924\nSET Key3925 Value3925\nSET Key3926 Value3926\nSET Key3927 Value3927\nSET Key3928 Value3928\nSET Key3929 Value3929\nSET Key3930 Value3930\nSET Key3931 Value3931\nSET Key3932 Value3932\nSET Key3933 Value3933\nSET Key3934 Value3934\nSET Key3935 Value3935\nSET Key3936 Value3936\nSET Key3937 Value3937\nSET Key3938 Value3938\nSET Key3939 Value3939\nSET Key3940 Value3940\nSET Key3941 Value3941\nSET Key3942 Value3942\nSET Key3943 Value3943\nSET Key3944 Value3944\nSET Key3945 Value3945\nSET Key3946 Value3946\nSET Key3947 Value3947\nSET Key3948 Value3948\nSET Key3949 Value3949\nSET Key3950 Value3950\nSET Key3951 Value3951\nSET Key3952 Value3952\nSET Key3953 Value3953\nSET Key3954 Value3954\nSET Key3955 Value3955\nSET Key3956 Value3956\nSET Key3957 Value3957\nSET Key3958 Value3958\nSET Key3959 Value3959\nSET Key3960 Value3960\nSET Key3961 Value3961\nSET Key3962 Value3962\nSET Key3963 Value3963\nSET Key3964 Value3964\nSET Key3965 Value3965\nSET Key3966 Value3966\nSET Key3967 Value3967\nSET Key3968 Value3968\nSET Key3969 Value3969\nSET Key3970 Value3970\nSET Key3971 Value3971\nSET Key3972 Value3972\nSET Key3973 Value3973\nSET Key3974 Value3974\nSET Key3975 Value3975\nSET Key3976 Value3976\nSET Key3977 Value3977\nSET Key3978 Value3978\nSET Key3979 Value3979\nSET Key3980 Value3980\nSET Key3981 Value3981\nSET Key3982 Value3982\nSET Key3983 Value3983\nSET Key3984 Value3984\nSET Key3985 Value3985\nSET Key3986 Value3986\nSET Key3987 Value3987\nSET Key3988 Value3988\nSET Key3989 Value3989\nSET Key3990 Value3990\nSET Key3991 Value3991\nSET Key3992 Value3992\nSET Key3993 Value3993\nSET Key3994 Value3994\nSET Key3995 Value3995\nSET Key3996 Value3996\nSET Key3997 Value3997\nSET Key3998 Value3998\nSET Key3999 Value3999\nSET Key4000 Value4000\nSET Key4001 Value4001\nSET Key4002 Value4002\nSET Key4003 Value4003\nSET Key4004 Value4004\nSET Key4005 Value4005\nSET Key4006 Value4006\nSET Key4007 Value4007\nSET Key4008 Value4008\nSET Key4009 Value4009\nSET Key4010 Value4010\nSET Key4011 Value4011\nSET Key4012 Value4012\nSET Key4013 Value4013\nSET Key4014 Value4014\nSET Key4015 Value4015\nSET Key4016 Value4016\nSET Key4017 Value4017\nSET Key4018 Value4018\nSET Key4019 Value4019\nSET Key4020 Value4020\nSET Key4021 Value4021\nSET Key4022 Value4022\nSET Key4023 Value4023\nSET Key4024 Value4024\nSET Key4025 Value4025\nSET Key4026 Value4026\nSET Key4027 Value4027\nSET Key4028 Value4028\nSET Key4029 Value4029\nSET Key4030 Value4030\nSET Key4031 Value4031\nSET Key4032 Value4032\nSET Key4033 Value4033\nSET Key4034 Value4034\nSET Key4035 Value4035\nSET Key4036 Value4036\nSET Key4037 Value4037\nSET Key4038 Value4038\nSET Key4039 Value4039\nSET Key4040 Value4040\nSET Key4041 Value4041\nSET Key4042 Value4042\nSET Key4043 Value4043\nSET Key4044 Value4044\nSET Key4045 Value4045\nSET Key4046 Value4046\nSET Key4047 Value4047\nSET Key4048 Value4048\nSET Key4049 Value4049\nSET Key4050 Value4050\nSET Key4051 Value4051\nSET Key4052 Value4052\nSET Key4053 Value4053\nSET Key4054 Value4054\nSET Key4055 Value4055\nSET Key4056 Value4056\nSET Key4057 Value4057\nSET Key4058 Value4058\nSET Key4059 Value4059\nSET Key4060 Value4060\nSET Key4061 Value4061\nSET Key4062 Value4062\nSET Key4063 Value4063\nSET Key4064 Value4064\nSET Key4065 Value4065\nSET Key4066 Value4066\nSET Key4067 Value4067\nSET Key4068 Value4068\nSET Key4069 Value4069\nSET Key4070 Value4070\nSET Key4071 Value4071\nSET Key4072 Value4072\nSET Key4073 Value4073\nSET Key4074 Value4074\nSET Key4075 Value4075\nSET Key4076 Value4076\nSET Key4077 Value4077\nSET Key4078 Value4078\nSET Key4079 Value4079\nSET Key4080 Value4080\nSET Key4081 Value4081\nSET Key4082 Value4082\nSET Key4083 Value4083\nSET Key4084 Value4084\nSET Key4085 Value4085\nSET Key4086 Value4086\nSET Key4087 Value4087\nSET Key4088 Value4088\nSET Key4089 Value4089\nSET Key4090 Value4090\nSET Key4091 Value4091\nSET Key4092 Value4092\nSET Key4093 Value4093\nSET Key4094 Value4094\nSET Key4095 Value4095\nSET Key4096 Value4096\nSET Key4097 Value4097\nSET Key4098 Value4098\nSET Key4099 Value4099\nSET Key4100 Value4100\nSET Key4101 Value4101\nSET Key4102 Value4102\nSET Key4103 Value4103\nSET Key4104 Value4104\nSET Key4105 Value4105\nSET Key4106 Value4106\nSET Key4107 Value4107\nSET Key4108 Value4108\nSET Key4109 Value4109\nSET Key4110 Value4110\nSET Key4111 Value4111\nSET Key4112 Value4112\nSET Key4113 Value4113\nSET Key4114 Value4114\nSET Key4115 Value4115\nSET Key4116 Value4116\nSET Key4117 Value4117\nSET Key4118 Value4118\nSET Key4119 Value4119\nSET Key4120 Value4120\nSET Key4121 Value4121\nSET Key4122 Value4122\nSET Key4123 Value4123\nSET Key4124 Value4124\nSET Key4125 Value4125\nSET Key4126 Value4126\nSET Key4127 Value4127\nSET Key4128 Value4128\nSET Key4129 Value4129\nSET Key4130 Value4130\nSET Key4131 Value4131\nSET Key4132 Value4132\nSET Key4133 Value4133\nSET Key4134 Value4134\nSET Key4135 Value4135\nSET Key4136 Value4136\nSET Key4137 Value4137\nSET Key4138 Value4138\nSET Key4139 Value4139\nSET Key4140 Value4140\nSET Key4141 Value4141\nSET Key4142 Value4142\nSET Key4143 Value4143\nSET Key4144 Value4144\nSET Key4145 Value4145\nSET Key4146 Value4146\nSET Key4147 Value4147\nSET Key4148 Value4148\nSET Key4149 Value4149\nSET Key4150 Value4150\nSET Key4151 Value4151\nSET Key4152 Value4152\nSET Key4153 Value4153\nSET Key4154 Value4154\nSET Key4155 Value4155\nSET Key4156 Value4156\nSET Key4157 Value4157\nSET Key4158 Value4158\nSET Key4159 Value4159\nSET Key4160 Value4160\nSET Key4161 Value4161\nSET Key4162 Value4162\nSET Key4163 Value4163\nSET Key4164 Value4164\nSET Key4165 Value4165\nSET Key4166 Value4166\nSET Key4167 Value4167\nSET Key4168 Value4168\nSET Key4169 Value4169\nSET Key4170 Value4170\nSET Key4171 Value4171\nSET Key4172 Value4172\nSET Key4173 Value4173\nSET Key4174 Value4174\nSET Key4175 Value4175\nSET Key4176 Value4176\nSET Key4177 Value4177\nSET Key4178 Value4178\nSET Key4179 Value4179\nSET Key4180 Value4180\nSET Key4181 Value4181\nSET Key4182 Value4182\nSET Key4183 Value4183\nSET Key4184 Value4184\nSET Key4185 Value4185\nSET Key4186 Value4186\nSET Key4187 Value4187\nSET Key4188 Value4188\nSET Key4189 Value4189\nSET Key4190 Value4190\nSET Key4191 Value4191\nSET Key4192 Value4192\nSET Key4193 Value4193\nSET Key4194 Value4194\nSET Key4195 Value4195\nSET Key4196 Value4196\nSET Key4197 Value4197\nSET Key4198 Value4198\nSET Key4199 Value4199\nSET Key4200 Value4200\nSET Key4201 Value4201\nSET Key4202 Value4202\nSET Key4203 Value4203\nSET Key4204 Value4204\nSET Key4205 Value4205\nSET Key4206 Value4206\nSET Key4207 Value4207\nSET Key4208 Value4208\nSET Key4209 Value4209\nSET Key4210 Value4210\nSET Key4211 Value4211\nSET Key4212 Value4212\nSET Key4213 Value4213\nSET Key4214 Value4214\nSET Key4215 Value4215\nSET Key4216 Value4216\nSET Key4217 Value4217\nSET Key4218 Value4218\nSET Key4219 Value4219\nSET Key4220 Value4220\nSET Key4221 Value4221\nSET Key4222 Value4222\nSET Key4223 Value4223\nSET Key4224 Value4224\nSET Key4225 Value4225\nSET Key4226 Value4226\nSET Key4227 Value4227\nSET Key4228 Value4228\nSET Key4229 Value4229\nSET Key4230 Value4230\nSET Key4231 Value4231\nSET Key4232 Value4232\nSET Key4233 Value4233\nSET Key4234 Value4234\nSET Key4235 Value4235\nSET Key4236 Value4236\nSET Key4237 Value4237\nSET Key4238 Value4238\nSET Key4239 Value4239\nSET Key4240 Value4240\nSET Key4241 Value4241\nSET Key4242 Value4242\nSET Key4243 Value4243\nSET Key4244 Value4244\nSET Key4245 Value4245\nSET Key4246 Value4246\nSET Key4247 Value4247\nSET Key4248 Value4248\nSET Key4249 Value4249\nSET Key4250 Value4250\nSET Key4251 Value4251\nSET Key4252 Value4252\nSET Key4253 Value4253\nSET Key4254 Value4254\nSET Key4255 Value4255\nSET Key4256 Value4256\nSET Key4257 Value4257\nSET Key4258 Value4258\nSET Key4259 Value4259\nSET Key4260 Value4260\nSET Key4261 Value4261\nSET Key4262 Value4262\nSET Key4263 Value4263\nSET Key4264 Value4264\nSET Key4265 Value4265\nSET Key4266 Value4266\nSET Key4267 Value4267\nSET Key4268 Value4268\nSET Key4269 Value4269\nSET Key4270 Value4270\nSET Key4271 Value4271\nSET Key4272 Value4272\nSET Key4273 Value4273\nSET Key4274 Value4274\nSET Key4275 Value4275\nSET Key4276 Value4276\nSET Key4277 Value4277\nSET Key4278 Value4278\nSET Key4279 Value4279\nSET Key4280 Value4280\nSET Key4281 Value4281\nSET Key4282 Value4282\nSET Key4283 Value4283\nSET Key4284 Value4284\nSET Key4285 Value4285\nSET Key4286 Value4286\nSET Key4287 Value4287\nSET Key4288 Value4288\nSET Key4289 Value4289\nSET Key4290 Value4290\nSET Key4291 Value4291\nSET Key4292 Value4292\nSET Key4293 Value4293\nSET Key4294 Value4294\nSET Key4295 Value4295\nSET Key4296 Value4296\nSET Key4297 Value4297\nSET Key4298 Value4298\nSET Key4299 Value4299\nSET Key4300 Value4300\nSET Key4301 Value4301\nSET Key4302 Value4302\nSET Key4303 Value4303\nSET Key4304 Value4304\nSET Key4305 Value4305\nSET Key4306 Value4306\nSET Key4307 Value4307\nSET Key4308 Value4308\nSET Key4309 Value4309\nSET Key4310 Value4310\nSET Key4311 Value4311\nSET Key4312 Value4312\nSET Key4313 Value4313\nSET Key4314 Value4314\nSET Key4315 Value4315\nSET Key4316 Value4316\nSET Key4317 Value4317\nSET Key4318 Value4318\nSET Key4319 Value4319\nSET Key4320 Value4320\nSET Key4321 Value4321\nSET Key4322 Value4322\nSET Key4323 Value4323\nSET Key4324 Value4324\nSET Key4325 Value4325\nSET Key4326 Value4326\nSET Key4327 Value4327\nSET Key4328 Value4328\nSET Key4329 Value4329\nSET Key4330 Value4330\nSET Key4331 Value4331\nSET Key4332 Value4332\nSET Key4333 Value4333\nSET Key4334 Value4334\nSET Key4335 Value4335\nSET Key4336 Value4336\nSET Key4337 Value4337\nSET Key4338 Value4338\nSET Key4339 Value4339\nSET Key4340 Value4340\nSET Key4341 Value4341\nSET Key4342 Value4342\nSET Key4343 Value4343\nSET Key4344 Value4344\nSET Key4345 Value4345\nSET Key4346 Value4346\nSET Key4347 Value4347\nSET Key4348 Value4348\nSET Key4349 Value4349\nSET Key4350 Value4350\nSET Key4351 Value4351\nSET Key4352 Value4352\nSET Key4353 Value4353\nSET Key4354 Value4354\nSET Key4355 Value4355\nSET Key4356 Value4356\nSET Key4357 Value4357\nSET Key4358 Value4358\nSET Key4359 Value4359\nSET Key4360 Value4360\nSET Key4361 Value4361\nSET Key4362 Value4362\nSET Key4363 Value4363\nSET Key4364 Value4364\nSET Key4365 Value4365\nSET Key4366 Value4366\nSET Key4367 Value4367\nSET Key4368 Value4368\nSET Key4369 Value4369\nSET Key4370 Value4370\nSET Key4371 Value4371\nSET Key4372 Value4372\nSET Key4373 Value4373\nSET Key4374 Value4374\nSET Key4375 Value4375\nSET Key4376 Value4376\nSET Key4377 Value4377\nSET Key4378 Value4378\nSET Key4379 Value4379\nSET Key4380 Value4380\nSET Key4381 Value4381\nSET Key4382 Value4382\nSET Key4383 Value4383\nSET Key4384 Value4384\nSET Key4385 Value4385\nSET Key4386 Value4386\nSET Key4387 Value4387\nSET Key4388 Value4388\nSET Key4389 Value4389\nSET Key4390 Value4390\nSET Key4391 Value4391\nSET Key4392 Value4392\nSET Key4393 Value4393\nSET Key4394 Value4394\nSET Key4395 Value4395\nSET Key4396 Value4396\nSET Key4397 Value4397\nSET Key4398 Value4398\nSET Key4399 Value4399\nSET Key4400 Value4400\nSET Key4401 Value4401\nSET Key4402 Value4402\nSET Key4403 Value4403\nSET Key4404 Value4404\nSET Key4405 Value4405\nSET Key4406 Value4406\nSET Key4407 Value4407\nSET Key4408 Value4408\nSET Key4409 Value4409\nSET Key4410 Value4410\nSET Key4411 Value4411\nSET Key4412 Value4412\nSET Key4413 Value4413\nSET Key4414 Value4414\nSET Key4415 Value4415\nSET Key4416 Value4416\nSET Key4417 Value4417\nSET Key4418 Value4418\nSET Key4419 Value4419\nSET Key4420 Value4420\nSET Key4421 Value4421\nSET Key4422 Value4422\nSET Key4423 Value4423\nSET Key4424 Value4424\nSET Key4425 Value4425\nSET Key4426 Value4426\nSET Key4427 Value4427\nSET Key4428 Value4428\nSET Key4429 Value4429\nSET Key4430 Value4430\nSET Key4431 Value4431\nSET Key4432 Value4432\nSET Key4433 Value4433\nSET Key4434 Value4434\nSET Key4435 Value4435\nSET Key4436 Value4436\nSET Key4437 Value4437\nSET Key4438 Value4438\nSET Key4439 Value4439\nSET Key4440 Value4440\nSET Key4441 Value4441\nSET Key4442 Value4442\nSET Key4443 Value4443\nSET Key4444 Value4444\nSET Key4445 Value4445\nSET Key4446 Value4446\nSET Key4447 Value4447\nSET Key4448 Value4448\nSET Key4449 Value4449\nSET Key4450 Value4450\nSET Key4451 Value4451\nSET Key4452 Value4452\nSET Key4453 Value4453\nSET Key4454 Value4454\nSET Key4455 Value4455\nSET Key4456 Value4456\nSET Key4457 Value4457\nSET Key4458 Value4458\nSET Key4459 Value4459\nSET Key4460 Value4460\nSET Key4461 Value4461\nSET Key4462 Value4462\nSET Key4463 Value4463\nSET Key4464 Value4464\nSET Key4465 Value4465\nSET Key4466 Value4466\nSET Key4467 Value4467\nSET Key4468 Value4468\nSET Key4469 Value4469\nSET Key4470 Value4470\nSET Key4471 Value4471\nSET Key4472 Value4472\nSET Key4473 Value4473\nSET Key4474 Value4474\nSET Key4475 Value4475\nSET Key4476 Value4476\nSET Key4477 Value4477\nSET Key4478 Value4478\nSET Key4479 Value4479\nSET Key4480 Value4480\nSET Key4481 Value4481\nSET Key4482 Value4482\nSET Key4483 Value4483\nSET Key4484 Value4484\nSET Key4485 Value4485\nSET Key4486 Value4486\nSET Key4487 Value4487\nSET Key4488 Value4488\nSET Key4489 Value4489\nSET Key4490 Value4490\nSET Key4491 Value4491\nSET Key4492 Value4492\nSET Key4493 Value4493\nSET Key4494 Value4494\nSET Key4495 Value4495\nSET Key4496 Value4496\nSET Key4497 Value4497\nSET Key4498 Value4498\nSET Key4499 Value4499\nSET Key4500 Value4500\nSET Key4501 Value4501\nSET Key4502 Value4502\nSET Key4503 Value4503\nSET Key4504 Value4504\nSET Key4505 Value4505\nSET Key4506 Value4506\nSET Key4507 Value4507\nSET Key4508 Value4508\nSET Key4509 Value4509\nSET Key4510 Value4510\nSET Key4511 Value4511\nSET Key4512 Value4512\nSET Key4513 Value4513\nSET Key4514 Value4514\nSET Key4515 Value4515\nSET Key4516 Value4516\nSET Key4517 Value4517\nSET Key4518 Value4518\nSET Key4519 Value4519\nSET Key4520 Value4520\nSET Key4521 Value4521\nSET Key4522 Value4522\nSET Key4523 Value4523\nSET Key4524 Value4524\nSET Key4525 Value4525\nSET Key4526 Value4526\nSET Key4527 Value4527\nSET Key4528 Value4528\nSET Key4529 Value4529\nSET Key4530 Value4530\nSET Key4531 Value4531\nSET Key4532 Value4532\nSET Key4533 Value4533\nSET Key4534 Value4534\nSET Key4535 Value4535\nSET Key4536 Value4536\nSET Key4537 Value4537\nSET Key4538 Value4538\nSET Key4539 Value4539\nSET Key4540 Value4540\nSET Key4541 Value4541\nSET Key4542 Value4542\nSET Key4543 Value4543\nSET Key4544 Value4544\nSET Key4545 Value4545\nSET Key4546 Value4546\nSET Key4547 Value4547\nSET Key4548 Value4548\nSET Key4549 Value4549\nSET Key4550 Value4550\nSET Key4551 Value4551\nSET Key4552 Value4552\nSET Key4553 Value4553\nSET Key4554 Value4554\nSET Key4555 Value4555\nSET Key4556 Value4556\nSET Key4557 Value4557\nSET Key4558 Value4558\nSET Key4559 Value4559\nSET Key4560 Value4560\nSET Key4561 Value4561\nSET Key4562 Value4562\nSET Key4563 Value4563\nSET Key4564 Value4564\nSET Key4565 Value4565\nSET Key4566 Value4566\nSET Key4567 Value4567\nSET Key4568 Value4568\nSET Key4569 Value4569\nSET Key4570 Value4570\nSET Key4571 Value4571\nSET Key4572 Value4572\nSET Key4573 Value4573\nSET Key4574 Value4574\nSET Key4575 Value4575\nSET Key4576 Value4576\nSET Key4577 Value4577\nSET Key4578 Value4578\nSET Key4579 Value4579\nSET Key4580 Value4580\nSET Key4581 Value4581\nSET Key4582 Value4582\nSET Key4583 Value4583\nSET Key4584 Value4584\nSET Key4585 Value4585\nSET Key4586 Value4586\nSET Key4587 Value4587\nSET Key4588 Value4588\nSET Key4589 Value4589\nSET Key4590 Value4590\nSET Key4591 Value4591\nSET Key4592 Value4592\nSET Key4593 Value4593\nSET Key4594 Value4594\nSET Key4595 Value4595\nSET Key4596 Value4596\nSET Key4597 Value4597\nSET Key4598 Value4598\nSET Key4599 Value4599\nSET Key4600 Value4600\nSET Key4601 Value4601\nSET Key4602 Value4602\nSET Key4603 Value4603\nSET Key4604 Value4604\nSET Key4605 Value4605\nSET Key4606 Value4606\nSET Key4607 Value4607\nSET Key4608 Value4608\nSET Key4609 Value4609\nSET Key4610 Value4610\nSET Key4611 Value4611\nSET Key4612 Value4612\nSET Key4613 Value4613\nSET Key4614 Value4614\nSET Key4615 Value4615\nSET Key4616 Value4616\nSET Key4617 Value4617\nSET Key4618 Value4618\nSET Key4619 Value4619\nSET Key4620 Value4620\nSET Key4621 Value4621\nSET Key4622 Value4622\nSET Key4623 Value4623\nSET Key4624 Value4624\nSET Key4625 Value4625\nSET Key4626 Value4626\nSET Key4627 Value4627\nSET Key4628 Value4628\nSET Key4629 Value4629\nSET Key4630 Value4630\nSET Key4631 Value4631\nSET Key4632 Value4632\nSET Key4633 Value4633\nSET Key4634 Value4634\nSET Key4635 Value4635\nSET Key4636 Value4636\nSET Key4637 Value4637\nSET Key4638 Value4638\nSET Key4639 Value4639\nSET Key4640 Value4640\nSET Key4641 Value4641\nSET Key4642 Value4642\nSET Key4643 Value4643\nSET Key4644 Value4644\nSET Key4645 Value4645\nSET Key4646 Value4646\nSET Key4647 Value4647\nSET Key4648 Value4648\nSET Key4649 Value4649\nSET Key4650 Value4650\nSET Key4651 Value4651\nSET Key4652 Value4652\nSET Key4653 Value4653\nSET Key4654 Value4654\nSET Key4655 Value4655\nSET Key4656 Value4656\nSET Key4657 Value4657\nSET Key4658 Value4658\nSET Key4659 Value4659\nSET Key4660 Value4660\nSET Key4661 Value4661\nSET Key4662 Value4662\nSET Key4663 Value4663\nSET Key4664 Value4664\nSET Key4665 Value4665\nSET Key4666 Value4666\nSET Key4667 Value4667\nSET Key4668 Value4668\nSET Key4669 Value4669\nSET Key4670 Value4670\nSET Key4671 Value4671\nSET Key4672 Value4672\nSET Key4673 Value4673\nSET Key4674 Value4674\nSET Key4675 Value4675\nSET Key4676 Value4676\nSET Key4677 Value4677\nSET Key4678 Value4678\nSET Key4679 Value4679\nSET Key4680 Value4680\nSET Key4681 Value4681\nSET Key4682 Value4682\nSET Key4683 Value4683\nSET Key4684 Value4684\nSET Key4685 Value4685\nSET Key4686 Value4686\nSET Key4687 Value4687\nSET Key4688 Value4688\nSET Key4689 Value4689\nSET Key4690 Value4690\nSET Key4691 Value4691\nSET Key4692 Value4692\nSET Key4693 Value4693\nSET Key4694 Value4694\nSET Key4695 Value4695\nSET Key4696 Value4696\nSET Key4697 Value4697\nSET Key4698 Value4698\nSET Key4699 Value4699\nSET Key4700 Value4700\nSET Key4701 Value4701\nSET Key4702 Value4702\nSET Key4703 Value4703\nSET Key4704 Value4704\nSET Key4705 Value4705\nSET Key4706 Value4706\nSET Key4707 Value4707\nSET Key4708 Value4708\nSET Key4709 Value4709\nSET Key4710 Value4710\nSET Key4711 Value4711\nSET Key4712 Value4712\nSET Key4713 Value4713\nSET Key4714 Value4714\nSET Key4715 Value4715\nSET Key4716 Value4716\nSET Key4717 Value4717\nSET Key4718 Value4718\nSET Key4719 Value4719\nSET Key4720 Value4720\nSET Key4721 Value4721\nSET Key4722 Value4722\nSET Key4723 Value4723\nSET Key4724 Value4724\nSET Key4725 Value4725\nSET Key4726 Value4726\nSET Key4727 Value4727\nSET Key4728 Value4728\nSET Key4729 Value4729\nSET Key4730 Value4730\nSET Key4731 Value4731\nSET Key4732 Value4732\nSET Key4733 Value4733\nSET Key4734 Value4734\nSET Key4735 Value4735\nSET Key4736 Value4736\nSET Key4737 Value4737\nSET Key4738 Value4738\nSET Key4739 Value4739\nSET Key4740 Value4740\nSET Key4741 Value4741\nSET Key4742 Value4742\nSET Key4743 Value4743\nSET Key4744 Value4744\nSET Key4745 Value4745\nSET Key4746 Value4746\nSET Key4747 Value4747\nSET Key4748 Value4748\nSET Key4749 Value4749\nSET Key4750 Value4750\nSET Key4751 Value4751\nSET Key4752 Value4752\nSET Key4753 Value4753\nSET Key4754 Value4754\nSET Key4755 Value4755\nSET Key4756 Value4756\nSET Key4757 Value4757\nSET Key4758 Value4758\nSET Key4759 Value4759\nSET Key4760 Value4760\nSET Key4761 Value4761\nSET Key4762 Value4762\nSET Key4763 Value4763\nSET Key4764 Value4764\nSET Key4765 Value4765\nSET Key4766 Value4766\nSET Key4767 Value4767\nSET Key4768 Value4768\nSET Key4769 Value4769\nSET Key4770 Value4770\nSET Key4771 Value4771\nSET Key4772 Value4772\nSET Key4773 Value4773\nSET Key4774 Value4774\nSET Key4775 Value4775\nSET Key4776 Value4776\nSET Key4777 Value4777\nSET Key4778 Value4778\nSET Key4779 Value4779\nSET Key4780 Value4780\nSET Key4781 Value4781\nSET Key4782 Value4782\nSET Key4783 Value4783\nSET Key4784 Value4784\nSET Key4785 Value4785\nSET Key4786 Value4786\nSET Key4787 Value4787\nSET Key4788 Value4788\nSET Key4789 Value4789\nSET Key4790 Value4790\nSET Key4791 Value4791\nSET Key4792 Value4792\nSET Key4793 Value4793\nSET Key4794 Value4794\nSET Key4795 Value4795\nSET Key4796 Value4796\nSET Key4797 Value4797\nSET Key4798 Value4798\nSET Key4799 Value4799\nSET Key4800 Value4800\nSET Key4801 Value4801\nSET Key4802 Value4802\nSET Key4803 Value4803\nSET Key4804 Value4804\nSET Key4805 Value4805\nSET Key4806 Value4806\nSET Key4807 Value4807\nSET Key4808 Value4808\nSET Key4809 Value4809\nSET Key4810 Value4810\nSET Key4811 Value4811\nSET Key4812 Value4812\nSET Key4813 Value4813\nSET Key4814 Value4814\nSET Key4815 Value4815\nSET Key4816 Value4816\nSET Key4817 Value4817\nSET Key4818 Value4818\nSET Key4819 Value4819\nSET Key4820 Value4820\nSET Key4821 Value4821\nSET Key4822 Value4822\nSET Key4823 Value4823\nSET Key4824 Value4824\nSET Key4825 Value4825\nSET Key4826 Value4826\nSET Key4827 Value4827\nSET Key4828 Value4828\nSET Key4829 Value4829\nSET Key4830 Value4830\nSET Key4831 Value4831\nSET Key4832 Value4832\nSET Key4833 Value4833\nSET Key4834 Value4834\nSET Key4835 Value4835\nSET Key4836 Value4836\nSET Key4837 Value4837\nSET Key4838 Value4838\nSET Key4839 Value4839\nSET Key4840 Value4840\nSET Key4841 Value4841\nSET Key4842 Value4842\nSET Key4843 Value4843\nSET Key4844 Value4844\nSET Key4845 Value4845\nSET Key4846 Value4846\nSET Key4847 Value4847\nSET Key4848 Value4848\nSET Key4849 Value4849\nSET Key4850 Value4850\nSET Key4851 Value4851\nSET Key4852 Value4852\nSET Key4853 Value4853\nSET Key4854 Value4854\nSET Key4855 Value4855\nSET Key4856 Value4856\nSET Key4857 Value4857\nSET Key4858 Value4858\nSET Key4859 Value4859\nSET Key4860 Value4860\nSET Key4861 Value4861\nSET Key4862 Value4862\nSET Key4863 Value4863\nSET Key4864 Value4864\nSET Key4865 Value4865\nSET Key4866 Value4866\nSET Key4867 Value4867\nSET Key4868 Value4868\nSET Key4869 Value4869\nSET Key4870 Value4870\nSET Key4871 Value4871\nSET Key4872 Value4872\nSET Key4873 Value4873\nSET Key4874 Value4874\nSET Key4875 Value4875\nSET Key4876 Value4876\nSET Key4877 Value4877\nSET Key4878 Value4878\nSET Key4879 Value4879\nSET Key4880 Value4880\nSET Key4881 Value4881\nSET Key4882 Value4882\nSET Key4883 Value4883\nSET Key4884 Value4884\nSET Key4885 Value4885\nSET Key4886 Value4886\nSET Key4887 Value4887\nSET Key4888 Value4888\nSET Key4889 Value4889\nSET Key4890 Value4890\nSET Key4891 Value4891\nSET Key4892 Value4892\nSET Key4893 Value4893\nSET Key4894 Value4894\nSET Key4895 Value4895\nSET Key4896 Value4896\nSET Key4897 Value4897\nSET Key4898 Value4898\nSET Key4899 Value4899\nSET Key4900 Value4900\nSET Key4901 Value4901\nSET Key4902 Value4902\nSET Key4903 Value4903\nSET Key4904 Value4904\nSET Key4905 Value4905\nSET Key4906 Value4906\nSET Key4907 Value4907\nSET Key4908 Value4908\nSET Key4909 Value4909\nSET Key4910 Value4910\nSET Key4911 Value4911\nSET Key4912 Value4912\nSET Key4913 Value4913\nSET Key4914 Value4914\nSET Key4915 Value4915\nSET Key4916 Value4916\nSET Key4917 Value4917\nSET Key4918 Value4918\nSET Key4919 Value4919\nSET Key4920 Value4920\nSET Key4921 Value4921\nSET Key4922 Value4922\nSET Key4923 Value4923\nSET Key4924 Value4924\nSET Key4925 Value4925\nSET Key4926 Value4926\nSET Key4927 Value4927\nSET Key4928 Value4928\nSET Key4929 Value4929\nSET Key4930 Value4930\nSET Key4931 Value4931\nSET Key4932 Value4932\nSET Key4933 Value4933\nSET Key4934 Value4934\nSET Key4935 Value4935\nSET Key4936 Value4936\nSET Key4937 Value4937\nSET Key4938 Value4938\nSET Key4939 Value4939\nSET Key4940 Value4940\nSET Key4941 Value4941\nSET Key4942 Value4942\nSET Key4943 Value4943\nSET Key4944 Value4944\nSET Key4945 Value4945\nSET Key4946 Value4946\nSET Key4947 Value4947\nSET Key4948 Value4948\nSET Key4949 Value4949\nSET Key4950 Value4950\nSET Key4951 Value4951\nSET Key4952 Value4952\nSET Key4953 Value4953\nSET Key4954 Value4954\nSET Key4955 Value4955\nSET Key4956 Value4956\nSET Key4957 Value4957\nSET Key4958 Value4958\nSET Key4959 Value4959\nSET Key4960 Value4960\nSET Key4961 Value4961\nSET Key4962 Value4962\nSET Key4963 Value4963\nSET Key4964 Value4964\nSET Key4965 Value4965\nSET Key4966 Value4966\nSET Key4967 Value4967\nSET Key4968 Value4968\nSET Key4969 Value4969\nSET Key4970 Value4970\nSET Key4971 Value4971\nSET Key4972 Value4972\nSET Key4973 Value4973\nSET Key4974 Value4974\nSET Key4975 Value4975\nSET Key4976 Value4976\nSET Key4977 Value4977\nSET Key4978 Value4978\nSET Key4979 Value4979\nSET Key4980 Value4980\nSET Key4981 Value4981\nSET Key4982 Value4982\nSET Key4983 Value4983\nSET Key4984 Value4984\nSET Key4985 Value4985\nSET Key4986 Value4986\nSET Key4987 Value4987\nSET Key4988 Value4988\nSET Key4989 Value4989\nSET Key4990 Value4990\nSET Key4991 Value4991\nSET Key4992 Value4992\nSET Key4993 Value4993\nSET Key4994 Value4994\nSET Key4995 Value4995\nSET Key4996 Value4996\nSET Key4997 Value4997\nSET Key4998 Value4998\nSET Key4999 Value4999\nSET Key5000 Value5000\nSET Key5001 Value5001\nSET Key5002 Value5002\nSET Key5003 Value5003\nSET Key5004 Value5004\nSET Key5005 Value5005\nSET Key5006 Value5006\nSET Key5007 Value5007\nSET Key5008 Value5008\nSET Key5009 Value5009\nSET Key5010 Value5010\nSET Key5011 Value5011\nSET Key5012 Value5012\nSET Key5013 Value5013\nSET Key5014 Value5014\nSET Key5015 Value5015\nSET Key5016 Value5016\nSET Key5017 Value5017\nSET Key5018 Value5018\nSET Key5019 Value5019\nSET Key5020 Value5020\nSET Key5021 Value5021\nSET Key5022 Value5022\nSET Key5023 Value5023\nSET Key5024 Value5024\nSET Key5025 Value5025\nSET Key5026 Value5026\nSET Key5027 Value5027\nSET Key5028 Value5028\nSET Key5029 Value5029\nSET Key5030 Value5030\nSET Key5031 Value5031\nSET Key5032 Value5032\nSET Key5033 Value5033\nSET Key5034 Value5034\nSET Key5035 Value5035\nSET Key5036 Value5036\nSET Key5037 Value5037\nSET Key5038 Value5038\nSET Key5039 Value5039\nSET Key5040 Value5040\nSET Key5041 Value5041\nSET Key5042 Value5042\nSET Key5043 Value5043\nSET Key5044 Value5044\nSET Key5045 Value5045\nSET Key5046 Value5046\nSET Key5047 Value5047\nSET Key5048 Value5048\nSET Key5049 Value5049\nSET Key5050 Value5050\nSET Key5051 Value5051\nSET Key5052 Value5052\nSET Key5053 Value5053\nSET Key5054 Value5054\nSET Key5055 Value5055\nSET Key5056 Value5056\nSET Key5057 Value5057\nSET Key5058 Value5058\nSET Key5059 Value5059\nSET Key5060 Value5060\nSET Key5061 Value5061\nSET Key5062 Value5062\nSET Key5063 Value5063\nSET Key5064 Value5064\nSET Key5065 Value5065\nSET Key5066 Value5066\nSET Key5067 Value5067\nSET Key5068 Value5068\nSET Key5069 Value5069\nSET Key5070 Value5070\nSET Key5071 Value5071\nSET Key5072 Value5072\nSET Key5073 Value5073\nSET Key5074 Value5074\nSET Key5075 Value5075\nSET Key5076 Value5076\nSET Key5077 Value5077\nSET Key5078 Value5078\nSET Key5079 Value5079\nSET Key5080 Value5080\nSET Key5081 Value5081\nSET Key5082 Value5082\nSET Key5083 Value5083\nSET Key5084 Value5084\nSET Key5085 Value5085\nSET Key5086 Value5086\nSET Key5087 Value5087\nSET Key5088 Value5088\nSET Key5089 Value5089\nSET Key5090 Value5090\nSET Key5091 Value5091\nSET Key5092 Value5092\nSET Key5093 Value5093\nSET Key5094 Value5094\nSET Key5095 Value5095\nSET Key5096 Value5096\nSET Key5097 Value5097\nSET Key5098 Value5098\nSET Key5099 Value5099\nSET Key5100 Value5100\nSET Key5101 Value5101\nSET Key5102 Value5102\nSET Key5103 Value5103\nSET Key5104 Value5104\nSET Key5105 Value5105\nSET Key5106 Value5106\nSET Key5107 Value5107\nSET Key5108 Value5108\nSET Key5109 Value5109\nSET Key5110 Value5110\nSET Key5111 Value5111\nSET Key5112 Value5112\nSET Key5113 Value5113\nSET Key5114 Value5114\nSET Key5115 Value5115\nSET Key5116 Value5116\nSET Key5117 Value5117\nSET Key5118 Value5118\nSET Key5119 Value5119\nSET Key5120 Value5120\nSET Key5121 Value5121\nSET Key5122 Value5122\nSET Key5123 Value5123\nSET Key5124 Value5124\nSET Key5125 Value5125\nSET Key5126 Value5126\nSET Key5127 Value5127\nSET Key5128 Value5128\nSET Key5129 Value5129\nSET Key5130 Value5130\nSET Key5131 Value5131\nSET Key5132 Value5132\nSET Key5133 Value5133\nSET Key5134 Value5134\nSET Key5135 Value5135\nSET Key5136 Value5136\nSET Key5137 Value5137\nSET Key5138 Value5138\nSET Key5139 Value5139\nSET Key5140 Value5140\nSET Key5141 Value5141\nSET Key5142 Value5142\nSET Key5143 Value5143\nSET Key5144 Value5144\nSET Key5145 Value5145\nSET Key5146 Value5146\nSET Key5147 Value5147\nSET Key5148 Value5148\nSET Key5149 Value5149\nSET Key5150 Value5150\nSET Key5151 Value5151\nSET Key5152 Value5152\nSET Key5153 Value5153\nSET Key5154 Value5154\nSET Key5155 Value5155\nSET Key5156 Value5156\nSET Key5157 Value5157\nSET Key5158 Value5158\nSET Key5159 Value5159\nSET Key5160 Value5160\nSET Key5161 Value5161\nSET Key5162 Value5162\nSET Key5163 Value5163\nSET Key5164 Value5164\nSET Key5165 Value5165\nSET Key5166 Value5166\nSET Key5167 Value5167\nSET Key5168 Value5168\nSET Key5169 Value5169\nSET Key5170 Value5170\nSET Key5171 Value5171\nSET Key5172 Value5172\nSET Key5173 Value5173\nSET Key5174 Value5174\nSET Key5175 Value5175\nSET Key5176 Value5176\nSET Key5177 Value5177\nSET Key5178 Value5178\nSET Key5179 Value5179\nSET Key5180 Value5180\nSET Key5181 Value5181\nSET Key5182 Value5182\nSET Key5183 Value5183\nSET Key5184 Value5184\nSET Key5185 Value5185\nSET Key5186 Value5186\nSET Key5187 Value5187\nSET Key5188 Value5188\nSET Key5189 Value5189\nSET Key5190 Value5190\nSET Key5191 Value5191\nSET Key5192 Value5192\nSET Key5193 Value5193\nSET Key5194 Value5194\nSET Key5195 Value5195\nSET Key5196 Value5196\nSET Key5197 Value5197\nSET Key5198 Value5198\nSET Key5199 Value5199\nSET Key5200 Value5200\nSET Key5201 Value5201\nSET Key5202 Value5202\nSET Key5203 Value5203\nSET Key5204 Value5204\nSET Key5205 Value5205\nSET Key5206 Value5206\nSET Key5207 Value5207\nSET Key5208 Value5208\nSET Key5209 Value5209\nSET Key5210 Value5210\nSET Key5211 Value5211\nSET Key5212 Value5212\nSET Key5213 Value5213\nSET Key5214 Value5214\nSET Key5215 Value5215\nSET Key5216 Value5216\nSET Key5217 Value5217\nSET Key5218 Value5218\nSET Key5219 Value5219\nSET Key5220 Value5220\nSET Key5221 Value5221\nSET Key5222 Value5222\nSET Key5223 Value5223\nSET Key5224 Value5224\nSET Key5225 Value5225\nSET Key5226 Value5226\nSET Key5227 Value5227\nSET Key5228 Value5228\nSET Key5229 Value5229\nSET Key5230 Value5230\nSET Key5231 Value5231\nSET Key5232 Value5232\nSET Key5233 Value5233\nSET Key5234 Value5234\nSET Key5235 Value5235\nSET Key5236 Value5236\nSET Key5237 Value5237\nSET Key5238 Value5238\nSET Key5239 Value5239\nSET Key5240 Value5240\nSET Key5241 Value5241\nSET Key5242 Value5242\nSET Key5243 Value5243\nSET Key5244 Value5244\nSET Key5245 Value5245\nSET Key5246 Value5246\nSET Key5247 Value5247\nSET Key5248 Value5248\nSET Key5249 Value5249\nSET Key5250 Value5250\nSET Key5251 Value5251\nSET Key5252 Value5252\nSET Key5253 Value5253\nSET Key5254 Value5254\nSET Key5255 Value5255\nSET Key5256 Value5256\nSET Key5257 Value5257\nSET Key5258 Value5258\nSET Key5259 Value5259\nSET Key5260 Value5260\nSET Key5261 Value5261\nSET Key5262 Value5262\nSET Key5263 Value5263\nSET Key5264 Value5264\nSET Key5265 Value5265\nSET Key5266 Value5266\nSET Key5267 Value5267\nSET Key5268 Value5268\nSET Key5269 Value5269\nSET Key5270 Value5270\nSET Key5271 Value5271\nSET Key5272 Value5272\nSET Key5273 Value5273\nSET Key5274 Value5274\nSET Key5275 Value5275\nSET Key5276 Value5276\nSET Key5277 Value5277\nSET Key5278 Value5278\nSET Key5279 Value5279\nSET Key5280 Value5280\nSET Key5281 Value5281\nSET Key5282 Value5282\nSET Key5283 Value5283\nSET Key5284 Value5284\nSET Key5285 Value5285\nSET Key5286 Value5286\nSET Key5287 Value5287\nSET Key5288 Value5288\nSET Key5289 Value5289\nSET Key5290 Value5290\nSET Key5291 Value5291\nSET Key5292 Value5292\nSET Key5293 Value5293\nSET Key5294 Value5294\nSET Key5295 Value5295\nSET Key5296 Value5296\nSET Key5297 Value5297\nSET Key5298 Value5298\nSET Key5299 Value5299\nSET Key5300 Value5300\nSET Key5301 Value5301\nSET Key5302 Value5302\nSET Key5303 Value5303\nSET Key5304 Value5304\nSET Key5305 Value5305\nSET Key5306 Value5306\nSET Key5307 Value5307\nSET Key5308 Value5308\nSET Key5309 Value5309\nSET Key5310 Value5310\nSET Key5311 Value5311\nSET Key5312 Value5312\nSET Key5313 Value5313\nSET Key5314 Value5314\nSET Key5315 Value5315\nSET Key5316 Value5316\nSET Key5317 Value5317\nSET Key5318 Value5318\nSET Key5319 Value5319\nSET Key5320 Value5320\nSET Key5321 Value5321\nSET Key5322 Value5322\nSET Key5323 Value5323\nSET Key5324 Value5324\nSET Key5325 Value5325\nSET Key5326 Value5326\nSET Key5327 Value5327\nSET Key5328 Value5328\nSET Key5329 Value5329\nSET Key5330 Value5330\nSET Key5331 Value5331\nSET Key5332 Value5332\nSET Key5333 Value5333\nSET Key5334 Value5334\nSET Key5335 Value5335\nSET Key5336 Value5336\nSET Key5337 Value5337\nSET Key5338 Value5338\nSET Key5339 Value5339\nSET Key5340 Value5340\nSET Key5341 Value5341\nSET Key5342 Value5342\nSET Key5343 Value5343\nSET Key5344 Value5344\nSET Key5345 Value5345\nSET Key5346 Value5346\nSET Key5347 Value5347\nSET Key5348 Value5348\nSET Key5349 Value5349\nSET Key5350 Value5350\nSET Key5351 Value5351\nSET Key5352 Value5352\nSET Key5353 Value5353\nSET Key5354 Value5354\nSET Key5355 Value5355\nSET Key5356 Value5356\nSET Key5357 Value5357\nSET Key5358 Value5358\nSET Key5359 Value5359\nSET Key5360 Value5360\nSET Key5361 Value5361\nSET Key5362 Value5362\nSET Key5363 Value5363\nSET Key5364 Value5364\nSET Key5365 Value5365\nSET Key5366 Value5366\nSET Key5367 Value5367\nSET Key5368 Value5368\nSET Key5369 Value5369\nSET Key5370 Value5370\nSET Key5371 Value5371\nSET Key5372 Value5372\nSET Key5373 Value5373\nSET Key5374 Value5374\nSET Key5375 Value5375\nSET Key5376 Value5376\nSET Key5377 Value5377\nSET Key5378 Value5378\nSET Key5379 Value5379\nSET Key5380 Value5380\nSET Key5381 Value5381\nSET Key5382 Value5382\nSET Key5383 Value5383\nSET Key5384 Value5384\nSET Key5385 Value5385\nSET Key5386 Value5386\nSET Key5387 Value5387\nSET Key5388 Value5388\nSET Key5389 Value5389\nSET Key5390 Value5390\nSET Key5391 Value5391\nSET Key5392 Value5392\nSET Key5393 Value5393\nSET Key5394 Value5394\nSET Key5395 Value5395\nSET Key5396 Value5396\nSET Key5397 Value5397\nSET Key5398 Value5398\nSET Key5399 Value5399\nSET Key5400 Value5400\nSET Key5401 Value5401\nSET Key5402 Value5402\nSET Key5403 Value5403\nSET Key5404 Value5404\nSET Key5405 Value5405\nSET Key5406 Value5406\nSET Key5407 Value5407\nSET Key5408 Value5408\nSET Key5409 Value5409\nSET Key5410 Value5410\nSET Key5411 Value5411\nSET Key5412 Value5412\nSET Key5413 Value5413\nSET Key5414 Value5414\nSET Key5415 Value5415\nSET Key5416 Value5416\nSET Key5417 Value5417\nSET Key5418 Value5418\nSET Key5419 Value5419\nSET Key5420 Value5420\nSET Key5421 Value5421\nSET Key5422 Value5422\nSET Key5423 Value5423\nSET Key5424 Value5424\nSET Key5425 Value5425\nSET Key5426 Value5426\nSET Key5427 Value5427\nSET Key5428 Value5428\nSET Key5429 Value5429\nSET Key5430 Value5430\nSET Key5431 Value5431\nSET Key5432 Value5432\nSET Key5433 Value5433\nSET Key5434 Value5434\nSET Key5435 Value5435\nSET Key5436 Value5436\nSET Key5437 Value5437\nSET Key5438 Value5438\nSET Key5439 Value5439\nSET Key5440 Value5440\nSET Key5441 Value5441\nSET Key5442 Value5442\nSET Key5443 Value5443\nSET Key5444 Value5444\nSET Key5445 Value5445\nSET Key5446 Value5446\nSET Key5447 Value5447\nSET Key5448 Value5448\nSET Key5449 Value5449\nSET Key5450 Value5450\nSET Key5451 Value5451\nSET Key5452 Value5452\nSET Key5453 Value5453\nSET Key5454 Value5454\nSET Key5455 Value5455\nSET Key5456 Value5456\nSET Key5457 Value5457\nSET Key5458 Value5458\nSET Key5459 Value5459\nSET Key5460 Value5460\nSET Key5461 Value5461\nSET Key5462 Value5462\nSET Key5463 Value5463\nSET Key5464 Value5464\nSET Key5465 Value5465\nSET Key5466 Value5466\nSET Key5467 Value5467\nSET Key5468 Value5468\nSET Key5469 Value5469\nSET Key5470 Value5470\nSET Key5471 Value5471\nSET Key5472 Value5472\nSET Key5473 Value5473\nSET Key5474 Value5474\nSET Key5475 Value5475\nSET Key5476 Value5476\nSET Key5477 Value5477\nSET Key5478 Value5478\nSET Key5479 Value5479\nSET Key5480 Value5480\nSET Key5481 Value5481\nSET Key5482 Value5482\nSET Key5483 Value5483\nSET Key5484 Value5484\nSET Key5485 Value5485\nSET Key5486 Value5486\nSET Key5487 Value5487\nSET Key5488 Value5488\nSET Key5489 Value5489\nSET Key5490 Value5490\nSET Key5491 Value5491\nSET Key5492 Value5492\nSET Key5493 Value5493\nSET Key5494 Value5494\nSET Key5495 Value5495\nSET Key5496 Value5496\nSET Key5497 Value5497\nSET Key5498 Value5498\nSET Key5499 Value5499\nSET Key5500 Value5500\nSET Key5501 Value5501\nSET Key5502 Value5502\nSET Key5503 Value5503\nSET Key5504 Value5504\nSET Key5505 Value5505\nSET Key5506 Value5506\nSET Key5507 Value5507\nSET Key5508 Value5508\nSET Key5509 Value5509\nSET Key5510 Value5510\nSET Key5511 Value5511\nSET Key5512 Value5512\nSET Key5513 Value5513\nSET Key5514 Value5514\nSET Key5515 Value5515\nSET Key5516 Value5516\nSET Key5517 Value5517\nSET Key5518 Value5518\nSET Key5519 Value5519\nSET Key5520 Value5520\nSET Key5521 Value5521\nSET Key5522 Value5522\nSET Key5523 Value5523\nSET Key5524 Value5524\nSET Key5525 Value5525\nSET Key5526 Value5526\nSET Key5527 Value5527\nSET Key5528 Value5528\nSET Key5529 Value5529\nSET Key5530 Value5530\nSET Key5531 Value5531\nSET Key5532 Value5532\nSET Key5533 Value5533\nSET Key5534 Value5534\nSET Key5535 Value5535\nSET Key5536 Value5536\nSET Key5537 Value5537\nSET Key5538 Value5538\nSET Key5539 Value5539\nSET Key5540 Value5540\nSET Key5541 Value5541\nSET Key5542 Value5542\nSET Key5543 Value5543\nSET Key5544 Value5544\nSET Key5545 Value5545\nSET Key5546 Value5546\nSET Key5547 Value5547\nSET Key5548 Value5548\nSET Key5549 Value5549\nSET Key5550 Value5550\nSET Key5551 Value5551\nSET Key5552 Value5552\nSET Key5553 Value5553\nSET Key5554 Value5554\nSET Key5555 Value5555\nSET Key5556 Value5556\nSET Key5557 Value5557\nSET Key5558 Value5558\nSET Key5559 Value5559\nSET Key5560 Value5560\nSET Key5561 Value5561\nSET Key5562 Value5562\nSET Key5563 Value5563\nSET Key5564 Value5564\nSET Key5565 Value5565\nSET Key5566 Value5566\nSET Key5567 Value5567\nSET Key5568 Value5568\nSET Key5569 Value5569\nSET Key5570 Value5570\nSET Key5571 Value5571\nSET Key5572 Value5572\nSET Key5573 Value5573\nSET Key5574 Value5574\nSET Key5575 Value5575\nSET Key5576 Value5576\nSET Key5577 Value5577\nSET Key5578 Value5578\nSET Key5579 Value5579\nSET Key5580 Value5580\nSET Key5581 Value5581\nSET Key5582 Value5582\nSET Key5583 Value5583\nSET Key5584 Value5584\nSET Key5585 Value5585\nSET Key5586 Value5586\nSET Key5587 Value5587\nSET Key5588 Value5588\nSET Key5589 Value5589\nSET Key5590 Value5590\nSET Key5591 Value5591\nSET Key5592 Value5592\nSET Key5593 Value5593\nSET Key5594 Value5594\nSET Key5595 Value5595\nSET Key5596 Value5596\nSET Key5597 Value5597\nSET Key5598 Value5598\nSET Key5599 Value5599\nSET Key5600 Value5600\nSET Key5601 Value5601\nSET Key5602 Value5602\nSET Key5603 Value5603\nSET Key5604 Value5604\nSET Key5605 Value5605\nSET Key5606 Value5606\nSET Key5607 Value5607\nSET Key5608 Value5608\nSET Key5609 Value5609\nSET Key5610 Value5610\nSET Key5611 Value5611\nSET Key5612 Value5612\nSET Key5613 Value5613\nSET Key5614 Value5614\nSET Key5615 Value5615\nSET Key5616 Value5616\nSET Key5617 Value5617\nSET Key5618 Value5618\nSET Key5619 Value5619\nSET Key5620 Value5620\nSET Key5621 Value5621\nSET Key5622 Value5622\nSET Key5623 Value5623\nSET Key5624 Value5624\nSET Key5625 Value5625\nSET Key5626 Value5626\nSET Key5627 Value5627\nSET Key5628 Value5628\nSET Key5629 Value5629\nSET Key5630 Value5630\nSET Key5631 Value5631\nSET Key5632 Value5632\nSET Key5633 Value5633\nSET Key5634 Value5634\nSET Key5635 Value5635\nSET Key5636 Value5636\nSET Key5637 Value5637\nSET Key5638 Value5638\nSET Key5639 Value5639\nSET Key5640 Value5640\nSET Key5641 Value5641\nSET Key5642 Value5642\nSET Key5643 Value5643\nSET Key5644 Value5644\nSET Key5645 Value5645\nSET Key5646 Value5646\nSET Key5647 Value5647\nSET Key5648 Value5648\nSET Key5649 Value5649\nSET Key5650 Value5650\nSET Key5651 Value5651\nSET Key5652 Value5652\nSET Key5653 Value5653\nSET Key5654 Value5654\nSET Key5655 Value5655\nSET Key5656 Value5656\nSET Key5657 Value5657\nSET Key5658 Value5658\nSET Key5659 Value5659\nSET Key5660 Value5660\nSET Key5661 Value5661\nSET Key5662 Value5662\nSET Key5663 Value5663\nSET Key5664 Value5664\nSET Key5665 Value5665\nSET Key5666 Value5666\nSET Key5667 Value5667\nSET Key5668 Value5668\nSET Key5669 Value5669\nSET Key5670 Value5670\nSET Key5671 Value5671\nSET Key5672 Value5672\nSET Key5673 Value5673\nSET Key5674 Value5674\nSET Key5675 Value5675\nSET Key5676 Value5676\nSET Key5677 Value5677\nSET Key5678 Value5678\nSET Key5679 Value5679\nSET Key5680 Value5680\nSET Key5681 Value5681\nSET Key5682 Value5682\nSET Key5683 Value5683\nSET Key5684 Value5684\nSET Key5685 Value5685\nSET Key5686 Value5686\nSET Key5687 Value5687\nSET Key5688 Value5688\nSET Key5689 Value5689\nSET Key5690 Value5690\nSET Key5691 Value5691\nSET Key5692 Value5692\nSET Key5693 Value5693\nSET Key5694 Value5694\nSET Key5695 Value5695\nSET Key5696 Value5696\nSET Key5697 Value5697\nSET Key5698 Value5698\nSET Key5699 Value5699\nSET Key5700 Value5700\nSET Key5701 Value5701\nSET Key5702 Value5702\nSET Key5703 Value5703\nSET Key5704 Value5704\nSET Key5705 Value5705\nSET Key5706 Value5706\nSET Key5707 Value5707\nSET Key5708 Value5708\nSET Key5709 Value5709\nSET Key5710 Value5710\nSET Key5711 Value5711\nSET Key5712 Value5712\nSET Key5713 Value5713\nSET Key5714 Value5714\nSET Key5715 Value5715\nSET Key5716 Value5716\nSET Key5717 Value5717\nSET Key5718 Value5718\nSET Key5719 Value5719\nSET Key5720 Value5720\nSET Key5721 Value5721\nSET Key5722 Value5722\nSET Key5723 Value5723\nSET Key5724 Value5724\nSET Key5725 Value5725\nSET Key5726 Value5726\nSET Key5727 Value5727\nSET Key5728 Value5728\nSET Key5729 Value5729\nSET Key5730 Value5730\nSET Key5731 Value5731\nSET Key5732 Value5732\nSET Key5733 Value5733\nSET Key5734 Value5734\nSET Key5735 Value5735\nSET Key5736 Value5736\nSET Key5737 Value5737\nSET Key5738 Value5738\nSET Key5739 Value5739\nSET Key5740 Value5740\nSET Key5741 Value5741\nSET Key5742 Value5742\nSET Key5743 Value5743\nSET Key5744 Value5744\nSET Key5745 Value5745\nSET Key5746 Value5746\nSET Key5747 Value5747\nSET Key5748 Value5748\nSET Key5749 Value5749\nSET Key5750 Value5750\nSET Key5751 Value5751\nSET Key5752 Value5752\nSET Key5753 Value5753\nSET Key5754 Value5754\nSET Key5755 Value5755\nSET Key5756 Value5756\nSET Key5757 Value5757\nSET Key5758 Value5758\nSET Key5759 Value5759\nSET Key5760 Value5760\nSET Key5761 Value5761\nSET Key5762 Value5762\nSET Key5763 Value5763\nSET Key5764 Value5764\nSET Key5765 Value5765\nSET Key5766 Value5766\nSET Key5767 Value5767\nSET Key5768 Value5768\nSET Key5769 Value5769\nSET Key5770 Value5770\nSET Key5771 Value5771\nSET Key5772 Value5772\nSET Key5773 Value5773\nSET Key5774 Value5774\nSET Key5775 Value5775\nSET Key5776 Value5776\nSET Key5777 Value5777\nSET Key5778 Value5778\nSET Key5779 Value5779\nSET Key5780 Value5780\nSET Key5781 Value5781\nSET Key5782 Value5782\nSET Key5783 Value5783\nSET Key5784 Value5784\nSET Key5785 Value5785\nSET Key5786 Value5786\nSET Key5787 Value5787\nSET Key5788 Value5788\nSET Key5789 Value5789\nSET Key5790 Value5790\nSET Key5791 Value5791\nSET Key5792 Value5792\nSET Key5793 Value5793\nSET Key5794 Value5794\nSET Key5795 Value5795\nSET Key5796 Value5796\nSET Key5797 Value5797\nSET Key5798 Value5798\nSET Key5799 Value5799\nSET Key5800 Value5800\nSET Key5801 Value5801\nSET Key5802 Value5802\nSET Key5803 Value5803\nSET Key5804 Value5804\nSET Key5805 Value5805\nSET Key5806 Value5806\nSET Key5807 Value5807\nSET Key5808 Value5808\nSET Key5809 Value5809\nSET Key5810 Value5810\nSET Key5811 Value5811\nSET Key5812 Value5812\nSET Key5813 Value5813\nSET Key5814 Value5814\nSET Key5815 Value5815\nSET Key5816 Value5816\nSET Key5817 Value5817\nSET Key5818 Value5818\nSET Key5819 Value5819\nSET Key5820 Value5820\nSET Key5821 Value5821\nSET Key5822 Value5822\nSET Key5823 Value5823\nSET Key5824 Value5824\nSET Key5825 Value5825\nSET Key5826 Value5826\nSET Key5827 Value5827\nSET Key5828 Value5828\nSET Key5829 Value5829\nSET Key5830 Value5830\nSET Key5831 Value5831\nSET Key5832 Value5832\nSET Key5833 Value5833\nSET Key5834 Value5834\nSET Key5835 Value5835\nSET Key5836 Value5836\nSET Key5837 Value5837\nSET Key5838 Value5838\nSET Key5839 Value5839\nSET Key5840 Value5840\nSET Key5841 Value5841\nSET Key5842 Value5842\nSET Key5843 Value5843\nSET Key5844 Value5844\nSET Key5845 Value5845\nSET Key5846 Value5846\nSET Key5847 Value5847\nSET Key5848 Value5848\nSET Key5849 Value5849\nSET Key5850 Value5850\nSET Key5851 Value5851\nSET Key5852 Value5852\nSET Key5853 Value5853\nSET Key5854 Value5854\nSET Key5855 Value5855\nSET Key5856 Value5856\nSET Key5857 Value5857\nSET Key5858 Value5858\nSET Key5859 Value5859\nSET Key5860 Value5860\nSET Key5861 Value5861\nSET Key5862 Value5862\nSET Key5863 Value5863\nSET Key5864 Value5864\nSET Key5865 Value5865\nSET Key5866 Value5866\nSET Key5867 Value5867\nSET Key5868 Value5868\nSET Key5869 Value5869\nSET Key5870 Value5870\nSET Key5871 Value5871\nSET Key5872 Value5872\nSET Key5873 Value5873\nSET Key5874 Value5874\nSET Key5875 Value5875\nSET Key5876 Value5876\nSET Key5877 Value5877\nSET Key5878 Value5878\nSET Key5879 Value5879\nSET Key5880 Value5880\nSET Key5881 Value5881\nSET Key5882 Value5882\nSET Key5883 Value5883\nSET Key5884 Value5884\nSET Key5885 Value5885\nSET Key5886 Value5886\nSET Key5887 Value5887\nSET Key5888 Value5888\nSET Key5889 Value5889\nSET Key5890 Value5890\nSET Key5891 Value5891\nSET Key5892 Value5892\nSET Key5893 Value5893\nSET Key5894 Value5894\nSET Key5895 Value5895\nSET Key5896 Value5896\nSET Key5897 Value5897\nSET Key5898 Value5898\nSET Key5899 Value5899\nSET Key5900 Value5900\nSET Key5901 Value5901\nSET Key5902 Value5902\nSET Key5903 Value5903\nSET Key5904 Value5904\nSET Key5905 Value5905\nSET Key5906 Value5906\nSET Key5907 Value5907\nSET Key5908 Value5908\nSET Key5909 Value5909\nSET Key5910 Value5910\nSET Key5911 Value5911\nSET Key5912 Value5912\nSET Key5913 Value5913\nSET Key5914 Value5914\nSET Key5915 Value5915\nSET Key5916 Value5916\nSET Key5917 Value5917\nSET Key5918 Value5918\nSET Key5919 Value5919\nSET Key5920 Value5920\nSET Key5921 Value5921\nSET Key5922 Value5922\nSET Key5923 Value5923\nSET Key5924 Value5924\nSET Key5925 Value5925\nSET Key5926 Value5926\nSET Key5927 Value5927\nSET Key5928 Value5928\nSET Key5929 Value5929\nSET Key5930 Value5930\nSET Key5931 Value5931\nSET Key5932 Value5932\nSET Key5933 Value5933\nSET Key5934 Value5934\nSET Key5935 Value5935\nSET Key5936 Value5936\nSET Key5937 Value5937\nSET Key5938 Value5938\nSET Key5939 Value5939\nSET Key5940 Value5940\nSET Key5941 Value5941\nSET Key5942 Value5942\nSET Key5943 Value5943\nSET Key5944 Value5944\nSET Key5945 Value5945\nSET Key5946 Value5946\nSET Key5947 Value5947\nSET Key5948 Value5948\nSET Key5949 Value5949\nSET Key5950 Value5950\nSET Key5951 Value5951\nSET Key5952 Value5952\nSET Key5953 Value5953\nSET Key5954 Value5954\nSET Key5955 Value5955\nSET Key5956 Value5956\nSET Key5957 Value5957\nSET Key5958 Value5958\nSET Key5959 Value5959\nSET Key5960 Value5960\nSET Key5961 Value5961\nSET Key5962 Value5962\nSET Key5963 Value5963\nSET Key5964 Value5964\nSET Key5965 Value5965\nSET Key5966 Value5966\nSET Key5967 Value5967\nSET Key5968 Value5968\nSET Key5969 Value5969\nSET Key5970 Value5970\nSET Key5971 Value5971\nSET Key5972 Value5972\nSET Key5973 Value5973\nSET Key5974 Value5974\nSET Key5975 Value5975\nSET Key5976 Value5976\nSET Key5977 Value5977\nSET Key5978 Value5978\nSET Key5979 Value5979\nSET Key5980 Value5980\nSET Key5981 Value5981\nSET Key5982 Value5982\nSET Key5983 Value5983\nSET Key5984 Value5984\nSET Key5985 Value5985\nSET Key5986 Value5986\nSET Key5987 Value5987\nSET Key5988 Value5988\nSET Key5989 Value5989\nSET Key5990 Value5990\nSET Key5991 Value5991\nSET Key5992 Value5992\nSET Key5993 Value5993\nSET Key5994 Value5994\nSET Key5995 Value5995\nSET Key5996 Value5996\nSET Key5997 Value5997\nSET Key5998 Value5998\nSET Key5999 Value5999\nSET Key6000 Value6000\nSET Key6001 Value6001\nSET Key6002 Value6002\nSET Key6003 Value6003\nSET Key6004 Value6004\nSET Key6005 Value6005\nSET Key6006 Value6006\nSET Key6007 Value6007\nSET Key6008 Value6008\nSET Key6009 Value6009\nSET Key6010 Value6010\nSET Key6011 Value6011\nSET Key6012 Value6012\nSET Key6013 Value6013\nSET Key6014 Value6014\nSET Key6015 Value6015\nSET Key6016 Value6016\nSET Key6017 Value6017\nSET Key6018 Value6018\nSET Key6019 Value6019\nSET Key6020 Value6020\nSET Key6021 Value6021\nSET Key6022 Value6022\nSET Key6023 Value6023\nSET Key6024 Value6024\nSET Key6025 Value6025\nSET Key6026 Value6026\nSET Key6027 Value6027\nSET Key6028 Value6028\nSET Key6029 Value6029\nSET Key6030 Value6030\nSET Key6031 Value6031\nSET Key6032 Value6032\nSET Key6033 Value6033\nSET Key6034 Value6034\nSET Key6035 Value6035\nSET Key6036 Value6036\nSET Key6037 Value6037\nSET Key6038 Value6038\nSET Key6039 Value6039\nSET Key6040 Value6040\nSET Key6041 Value6041\nSET Key6042 Value6042\nSET Key6043 Value6043\nSET Key6044 Value6044\nSET Key6045 Value6045\nSET Key6046 Value6046\nSET Key6047 Value6047\nSET Key6048 Value6048\nSET Key6049 Value6049\nSET Key6050 Value6050\nSET Key6051 Value6051\nSET Key6052 Value6052\nSET Key6053 Value6053\nSET Key6054 Value6054\nSET Key6055 Value6055\nSET Key6056 Value6056\nSET Key6057 Value6057\nSET Key6058 Value6058\nSET Key6059 Value6059\nSET Key6060 Value6060\nSET Key6061 Value6061\nSET Key6062 Value6062\nSET Key6063 Value6063\nSET Key6064 Value6064\nSET Key6065 Value6065\nSET Key6066 Value6066\nSET Key6067 Value6067\nSET Key6068 Value6068\nSET Key6069 Value6069\nSET Key6070 Value6070\nSET Key6071 Value6071\nSET Key6072 Value6072\nSET Key6073 Value6073\nSET Key6074 Value6074\nSET Key6075 Value6075\nSET Key6076 Value6076\nSET Key6077 Value6077\nSET Key6078 Value6078\nSET Key6079 Value6079\nSET Key6080 Value6080\nSET Key6081 Value6081\nSET Key6082 Value6082\nSET Key6083 Value6083\nSET Key6084 Value6084\nSET Key6085 Value6085\nSET Key6086 Value6086\nSET Key6087 Value6087\nSET Key6088 Value6088\nSET Key6089 Value6089\nSET Key6090 Value6090\nSET Key6091 Value6091\nSET Key6092 Value6092\nSET Key6093 Value6093\nSET Key6094 Value6094\nSET Key6095 Value6095\nSET Key6096 Value6096\nSET Key6097 Value6097\nSET Key6098 Value6098\nSET Key6099 Value6099\nSET Key6100 Value6100\nSET Key6101 Value6101\nSET Key6102 Value6102\nSET Key6103 Value6103\nSET Key6104 Value6104\nSET Key6105 Value6105\nSET Key6106 Value6106\nSET Key6107 Value6107\nSET Key6108 Value6108\nSET Key6109 Value6109\nSET Key6110 Value6110\nSET Key6111 Value6111\nSET Key6112 Value6112\nSET Key6113 Value6113\nSET Key6114 Value6114\nSET Key6115 Value6115\nSET Key6116 Value6116\nSET Key6117 Value6117\nSET Key6118 Value6118\nSET Key6119 Value6119\nSET Key6120 Value6120\nSET Key6121 Value6121\nSET Key6122 Value6122\nSET Key6123 Value6123\nSET Key6124 Value6124\nSET Key6125 Value6125\nSET Key6126 Value6126\nSET Key6127 Value6127\nSET Key6128 Value6128\nSET Key6129 Value6129\nSET Key6130 Value6130\nSET Key6131 Value6131\nSET Key6132 Value6132\nSET Key6133 Value6133\nSET Key6134 Value6134\nSET Key6135 Value6135\nSET Key6136 Value6136\nSET Key6137 Value6137\nSET Key6138 Value6138\nSET Key6139 Value6139\nSET Key6140 Value6140\nSET Key6141 Value6141\nSET Key6142 Value6142\nSET Key6143 Value6143\nSET Key6144 Value6144\nSET Key6145 Value6145\nSET Key6146 Value6146\nSET Key6147 Value6147\nSET Key6148 Value6148\nSET Key6149 Value6149\nSET Key6150 Value6150\nSET Key6151 Value6151\nSET Key6152 Value6152\nSET Key6153 Value6153\nSET Key6154 Value6154\nSET Key6155 Value6155\nSET Key6156 Value6156\nSET Key6157 Value6157\nSET Key6158 Value6158\nSET Key6159 Value6159\nSET Key6160 Value6160\nSET Key6161 Value6161\nSET Key6162 Value6162\nSET Key6163 Value6163\nSET Key6164 Value6164\nSET Key6165 Value6165\nSET Key6166 Value6166\nSET Key6167 Value6167\nSET Key6168 Value6168\nSET Key6169 Value6169\nSET Key6170 Value6170\nSET Key6171 Value6171\nSET Key6172 Value6172\nSET Key6173 Value6173\nSET Key6174 Value6174\nSET Key6175 Value6175\nSET Key6176 Value6176\nSET Key6177 Value6177\nSET Key6178 Value6178\nSET Key6179 Value6179\nSET Key6180 Value6180\nSET Key6181 Value6181\nSET Key6182 Value6182\nSET Key6183 Value6183\nSET Key6184 Value6184\nSET Key6185 Value6185\nSET Key6186 Value6186\nSET Key6187 Value6187\nSET Key6188 Value6188\nSET Key6189 Value6189\nSET Key6190 Value6190\nSET Key6191 Value6191\nSET Key6192 Value6192\nSET Key6193 Value6193\nSET Key6194 Value6194\nSET Key6195 Value6195\nSET Key6196 Value6196\nSET Key6197 Value6197\nSET Key6198 Value6198\nSET Key6199 Value6199\nSET Key6200 Value6200\nSET Key6201 Value6201\nSET Key6202 Value6202\nSET Key6203 Value6203\nSET Key6204 Value6204\nSET Key6205 Value6205\nSET Key6206 Value6206\nSET Key6207 Value6207\nSET Key6208 Value6208\nSET Key6209 Value6209\nSET Key6210 Value6210\nSET Key6211 Value6211\nSET Key6212 Value6212\nSET Key6213 Value6213\nSET Key6214 Value6214\nSET Key6215 Value6215\nSET Key6216 Value6216\nSET Key6217 Value6217\nSET Key6218 Value6218\nSET Key6219 Value6219\nSET Key6220 Value6220\nSET Key6221 Value6221\nSET Key6222 Value6222\nSET Key6223 Value6223\nSET Key6224 Value6224\nSET Key6225 Value6225\nSET Key6226 Value6226\nSET Key6227 Value6227\nSET Key6228 Value6228\nSET Key6229 Value6229\nSET Key6230 Value6230\nSET Key6231 Value6231\nSET Key6232 Value6232\nSET Key6233 Value6233\nSET Key6234 Value6234\nSET Key6235 Value6235\nSET Key6236 Value6236\nSET Key6237 Value6237\nSET Key6238 Value6238\nSET Key6239 Value6239\nSET Key6240 Value6240\nSET Key6241 Value6241\nSET Key6242 Value6242\nSET Key6243 Value6243\nSET Key6244 Value6244\nSET Key6245 Value6245\nSET Key6246 Value6246\nSET Key6247 Value6247\nSET Key6248 Value6248\nSET Key6249 Value6249\nSET Key6250 Value6250\nSET Key6251 Value6251\nSET Key6252 Value6252\nSET Key6253 Value6253\nSET Key6254 Value6254\nSET Key6255 Value6255\nSET Key6256 Value6256\nSET Key6257 Value6257\nSET Key6258 Value6258\nSET Key6259 Value6259\nSET Key6260 Value6260\nSET Key6261 Value6261\nSET Key6262 Value6262\nSET Key6263 Value6263\nSET Key6264 Value6264\nSET Key6265 Value6265\nSET Key6266 Value6266\nSET Key6267 Value6267\nSET Key6268 Value6268\nSET Key6269 Value6269\nSET Key6270 Value6270\nSET Key6271 Value6271\nSET Key6272 Value6272\nSET Key6273 Value6273\nSET Key6274 Value6274\nSET Key6275 Value6275\nSET Key6276 Value6276\nSET Key6277 Value6277\nSET Key6278 Value6278\nSET Key6279 Value6279\nSET Key6280 Value6280\nSET Key6281 Value6281\nSET Key6282 Value6282\nSET Key6283 Value6283\nSET Key6284 Value6284\nSET Key6285 Value6285\nSET Key6286 Value6286\nSET Key6287 Value6287\nSET Key6288 Value6288\nSET Key6289 Value6289\nSET Key6290 Value6290\nSET Key6291 Value6291\nSET Key6292 Value6292\nSET Key6293 Value6293\nSET Key6294 Value6294\nSET Key6295 Value6295\nSET Key6296 Value6296\nSET Key6297 Value6297\nSET Key6298 Value6298\nSET Key6299 Value6299\nSET Key6300 Value6300\nSET Key6301 Value6301\nSET Key6302 Value6302\nSET Key6303 Value6303\nSET Key6304 Value6304\nSET Key6305 Value6305\nSET Key6306 Value6306\nSET Key6307 Value6307\nSET Key6308 Value6308\nSET Key6309 Value6309\nSET Key6310 Value6310\nSET Key6311 Value6311\nSET Key6312 Value6312\nSET Key6313 Value6313\nSET Key6314 Value6314\nSET Key6315 Value6315\nSET Key6316 Value6316\nSET Key6317 Value6317\nSET Key6318 Value6318\nSET Key6319 Value6319\nSET Key6320 Value6320\nSET Key6321 Value6321\nSET Key6322 Value6322\nSET Key6323 Value6323\nSET Key6324 Value6324\nSET Key6325 Value6325\nSET Key6326 Value6326\nSET Key6327 Value6327\nSET Key6328 Value6328\nSET Key6329 Value6329\nSET Key6330 Value6330\nSET Key6331 Value6331\nSET Key6332 Value6332\nSET Key6333 Value6333\nSET Key6334 Value6334\nSET Key6335 Value6335\nSET Key6336 Value6336\nSET Key6337 Value6337\nSET Key6338 Value6338\nSET Key6339 Value6339\nSET Key6340 Value6340\nSET Key6341 Value6341\nSET Key6342 Value6342\nSET Key6343 Value6343\nSET Key6344 Value6344\nSET Key6345 Value6345\nSET Key6346 Value6346\nSET Key6347 Value6347\nSET Key6348 Value6348\nSET Key6349 Value6349\nSET Key6350 Value6350\nSET Key6351 Value6351\nSET Key6352 Value6352\nSET Key6353 Value6353\nSET Key6354 Value6354\nSET Key6355 Value6355\nSET Key6356 Value6356\nSET Key6357 Value6357\nSET Key6358 Value6358\nSET Key6359 Value6359\nSET Key6360 Value6360\nSET Key6361 Value6361\nSET Key6362 Value6362\nSET Key6363 Value6363\nSET Key6364 Value6364\nSET Key6365 Value6365\nSET Key6366 Value6366\nSET Key6367 Value6367\nSET Key6368 Value6368\nSET Key6369 Value6369\nSET Key6370 Value6370\nSET Key6371 Value6371\nSET Key6372 Value6372\nSET Key6373 Value6373\nSET Key6374 Value6374\nSET Key6375 Value6375\nSET Key6376 Value6376\nSET Key6377 Value6377\nSET Key6378 Value6378\nSET Key6379 Value6379\nSET Key6380 Value6380\nSET Key6381 Value6381\nSET Key6382 Value6382\nSET Key6383 Value6383\nSET Key6384 Value6384\nSET Key6385 Value6385\nSET Key6386 Value6386\nSET Key6387 Value6387\nSET Key6388 Value6388\nSET Key6389 Value6389\nSET Key6390 Value6390\nSET Key6391 Value6391\nSET Key6392 Value6392\nSET Key6393 Value6393\nSET Key6394 Value6394\nSET Key6395 Value6395\nSET Key6396 Value6396\nSET Key6397 Value6397\nSET Key6398 Value6398\nSET Key6399 Value6399\nSET Key6400 Value6400\nSET Key6401 Value6401\nSET Key6402 Value6402\nSET Key6403 Value6403\nSET Key6404 Value6404\nSET Key6405 Value6405\nSET Key6406 Value6406\nSET Key6407 Value6407\nSET Key6408 Value6408\nSET Key6409 Value6409\nSET Key6410 Value6410\nSET Key6411 Value6411\nSET Key6412 Value6412\nSET Key6413 Value6413\nSET Key6414 Value6414\nSET Key6415 Value6415\nSET Key6416 Value6416\nSET Key6417 Value6417\nSET Key6418 Value6418\nSET Key6419 Value6419\nSET Key6420 Value6420\nSET Key6421 Value6421\nSET Key6422 Value6422\nSET Key6423 Value6423\nSET Key6424 Value6424\nSET Key6425 Value6425\nSET Key6426 Value6426\nSET Key6427 Value6427\nSET Key6428 Value6428\nSET Key6429 Value6429\nSET Key6430 Value6430\nSET Key6431 Value6431\nSET Key6432 Value6432\nSET Key6433 Value6433\nSET Key6434 Value6434\nSET Key6435 Value6435\nSET Key6436 Value6436\nSET Key6437 Value6437\nSET Key6438 Value6438\nSET Key6439 Value6439\nSET Key6440 Value6440\nSET Key6441 Value6441\nSET Key6442 Value6442\nSET Key6443 Value6443\nSET Key6444 Value6444\nSET Key6445 Value6445\nSET Key6446 Value6446\nSET Key6447 Value6447\nSET Key6448 Value6448\nSET Key6449 Value6449\nSET Key6450 Value6450\nSET Key6451 Value6451\nSET Key6452 Value6452\nSET Key6453 Value6453\nSET Key6454 Value6454\nSET Key6455 Value6455\nSET Key6456 Value6456\nSET Key6457 Value6457\nSET Key6458 Value6458\nSET Key6459 Value6459\nSET Key6460 Value6460\nSET Key6461 Value6461\nSET Key6462 Value6462\nSET Key6463 Value6463\nSET Key6464 Value6464\nSET Key6465 Value6465\nSET Key6466 Value6466\nSET Key6467 Value6467\nSET Key6468 Value6468\nSET Key6469 Value6469\nSET Key6470 Value6470\nSET Key6471 Value6471\nSET Key6472 Value6472\nSET Key6473 Value6473\nSET Key6474 Value6474\nSET Key6475 Value6475\nSET Key6476 Value6476\nSET Key6477 Value6477\nSET Key6478 Value6478\nSET Key6479 Value6479\nSET Key6480 Value6480\nSET Key6481 Value6481\nSET Key6482 Value6482\nSET Key6483 Value6483\nSET Key6484 Value6484\nSET Key6485 Value6485\nSET Key6486 Value6486\nSET Key6487 Value6487\nSET Key6488 Value6488\nSET Key6489 Value6489\nSET Key6490 Value6490\nSET Key6491 Value6491\nSET Key6492 Value6492\nSET Key6493 Value6493\nSET Key6494 Value6494\nSET Key6495 Value6495\nSET Key6496 Value6496\nSET Key6497 Value6497\nSET Key6498 Value6498\nSET Key6499 Value6499\nSET Key6500 Value6500\nSET Key6501 Value6501\nSET Key6502 Value6502\nSET Key6503 Value6503\nSET Key6504 Value6504\nSET Key6505 Value6505\nSET Key6506 Value6506\nSET Key6507 Value6507\nSET Key6508 Value6508\nSET Key6509 Value6509\nSET Key6510 Value6510\nSET Key6511 Value6511\nSET Key6512 Value6512\nSET Key6513 Value6513\nSET Key6514 Value6514\nSET Key6515 Value6515\nSET Key6516 Value6516\nSET Key6517 Value6517\nSET Key6518 Value6518\nSET Key6519 Value6519\nSET Key6520 Value6520\nSET Key6521 Value6521\nSET Key6522 Value6522\nSET Key6523 Value6523\nSET Key6524 Value6524\nSET Key6525 Value6525\nSET Key6526 Value6526\nSET Key6527 Value6527\nSET Key6528 Value6528\nSET Key6529 Value6529\nSET Key6530 Value6530\nSET Key6531 Value6531\nSET Key6532 Value6532\nSET Key6533 Value6533\nSET Key6534 Value6534\nSET Key6535 Value6535\nSET Key6536 Value6536\nSET Key6537 Value6537\nSET Key6538 Value6538\nSET Key6539 Value6539\nSET Key6540 Value6540\nSET Key6541 Value6541\nSET Key6542 Value6542\nSET Key6543 Value6543\nSET Key6544 Value6544\nSET Key6545 Value6545\nSET Key6546 Value6546\nSET Key6547 Value6547\nSET Key6548 Value6548\nSET Key6549 Value6549\nSET Key6550 Value6550\nSET Key6551 Value6551\nSET Key6552 Value6552\nSET Key6553 Value6553\nSET Key6554 Value6554\nSET Key6555 Value6555\nSET Key6556 Value6556\nSET Key6557 Value6557\nSET Key6558 Value6558\nSET Key6559 Value6559\nSET Key6560 Value6560\nSET Key6561 Value6561\nSET Key6562 Value6562\nSET Key6563 Value6563\nSET Key6564 Value6564\nSET Key6565 Value6565\nSET Key6566 Value6566\nSET Key6567 Value6567\nSET Key6568 Value6568\nSET Key6569 Value6569\nSET Key6570 Value6570\nSET Key6571 Value6571\nSET Key6572 Value6572\nSET Key6573 Value6573\nSET Key6574 Value6574\nSET Key6575 Value6575\nSET Key6576 Value6576\nSET Key6577 Value6577\nSET Key6578 Value6578\nSET Key6579 Value6579\nSET Key6580 Value6580\nSET Key6581 Value6581\nSET Key6582 Value6582\nSET Key6583 Value6583\nSET Key6584 Value6584\nSET Key6585 Value6585\nSET Key6586 Value6586\nSET Key6587 Value6587\nSET Key6588 Value6588\nSET Key6589 Value6589\nSET Key6590 Value6590\nSET Key6591 Value6591\nSET Key6592 Value6592\nSET Key6593 Value6593\nSET Key6594 Value6594\nSET Key6595 Value6595\nSET Key6596 Value6596\nSET Key6597 Value6597\nSET Key6598 Value6598\nSET Key6599 Value6599\nSET Key6600 Value6600\nSET Key6601 Value6601\nSET Key6602 Value6602\nSET Key6603 Value6603\nSET Key6604 Value6604\nSET Key6605 Value6605\nSET Key6606 Value6606\nSET Key6607 Value6607\nSET Key6608 Value6608\nSET Key6609 Value6609\nSET Key6610 Value6610\nSET Key6611 Value6611\nSET Key6612 Value6612\nSET Key6613 Value6613\nSET Key6614 Value6614\nSET Key6615 Value6615\nSET Key6616 Value6616\nSET Key6617 Value6617\nSET Key6618 Value6618\nSET Key6619 Value6619\nSET Key6620 Value6620\nSET Key6621 Value6621\nSET Key6622 Value6622\nSET Key6623 Value6623\nSET Key6624 Value6624\nSET Key6625 Value6625\nSET Key6626 Value6626\nSET Key6627 Value6627\nSET Key6628 Value6628\nSET Key6629 Value6629\nSET Key6630 Value6630\nSET Key6631 Value6631\nSET Key6632 Value6632\nSET Key6633 Value6633\nSET Key6634 Value6634\nSET Key6635 Value6635\nSET Key6636 Value6636\nSET Key6637 Value6637\nSET Key6638 Value6638\nSET Key6639 Value6639\nSET Key6640 Value6640\nSET Key6641 Value6641\nSET Key6642 Value6642\nSET Key6643 Value6643\nSET Key6644 Value6644\nSET Key6645 Value6645\nSET Key6646 Value6646\nSET Key6647 Value6647\nSET Key6648 Value6648\nSET Key6649 Value6649\nSET Key6650 Value6650\nSET Key6651 Value6651\nSET Key6652 Value6652\nSET Key6653 Value6653\nSET Key6654 Value6654\nSET Key6655 Value6655\nSET Key6656 Value6656\nSET Key6657 Value6657\nSET Key6658 Value6658\nSET Key6659 Value6659\nSET Key6660 Value6660\nSET Key6661 Value6661\nSET Key6662 Value6662\nSET Key6663 Value6663\nSET Key6664 Value6664\nSET Key6665 Value6665\nSET Key6666 Value6666\nSET Key6667 Value6667\nSET Key6668 Value6668\nSET Key6669 Value6669\nSET Key6670 Value6670\nSET Key6671 Value6671\nSET Key6672 Value6672\nSET Key6673 Value6673\nSET Key6674 Value6674\nSET Key6675 Value6675\nSET Key6676 Value6676\nSET Key6677 Value6677\nSET Key6678 Value6678\nSET Key6679 Value6679\nSET Key6680 Value6680\nSET Key6681 Value6681\nSET Key6682 Value6682\nSET Key6683 Value6683\nSET Key6684 Value6684\nSET Key6685 Value6685\nSET Key6686 Value6686\nSET Key6687 Value6687\nSET Key6688 Value6688\nSET Key6689 Value6689\nSET Key6690 Value6690\nSET Key6691 Value6691\nSET Key6692 Value6692\nSET Key6693 Value6693\nSET Key6694 Value6694\nSET Key6695 Value6695\nSET Key6696 Value6696\nSET Key6697 Value6697\nSET Key6698 Value6698\nSET Key6699 Value6699\nSET Key6700 Value6700\nSET Key6701 Value6701\nSET Key6702 Value6702\nSET Key6703 Value6703\nSET Key6704 Value6704\nSET Key6705 Value6705\nSET Key6706 Value6706\nSET Key6707 Value6707\nSET Key6708 Value6708\nSET Key6709 Value6709\nSET Key6710 Value6710\nSET Key6711 Value6711\nSET Key6712 Value6712\nSET Key6713 Value6713\nSET Key6714 Value6714\nSET Key6715 Value6715\nSET Key6716 Value6716\nSET Key6717 Value6717\nSET Key6718 Value6718\nSET Key6719 Value6719\nSET Key6720 Value6720\nSET Key6721 Value6721\nSET Key6722 Value6722\nSET Key6723 Value6723\nSET Key6724 Value6724\nSET Key6725 Value6725\nSET Key6726 Value6726\nSET Key6727 Value6727\nSET Key6728 Value6728\nSET Key6729 Value6729\nSET Key6730 Value6730\nSET Key6731 Value6731\nSET Key6732 Value6732\nSET Key6733 Value6733\nSET Key6734 Value6734\nSET Key6735 Value6735\nSET Key6736 Value6736\nSET Key6737 Value6737\nSET Key6738 Value6738\nSET Key6739 Value6739\nSET Key6740 Value6740\nSET Key6741 Value6741\nSET Key6742 Value6742\nSET Key6743 Value6743\nSET Key6744 Value6744\nSET Key6745 Value6745\nSET Key6746 Value6746\nSET Key6747 Value6747\nSET Key6748 Value6748\nSET Key6749 Value6749\nSET Key6750 Value6750\nSET Key6751 Value6751\nSET Key6752 Value6752\nSET Key6753 Value6753\nSET Key6754 Value6754\nSET Key6755 Value6755\nSET Key6756 Value6756\nSET Key6757 Value6757\nSET Key6758 Value6758\nSET Key6759 Value6759\nSET Key6760 Value6760\nSET Key6761 Value6761\nSET Key6762 Value6762\nSET Key6763 Value6763\nSET Key6764 Value6764\nSET Key6765 Value6765\nSET Key6766 Value6766\nSET Key6767 Value6767\nSET Key6768 Value6768\nSET Key6769 Value6769\nSET Key6770 Value6770\nSET Key6771 Value6771\nSET Key6772 Value6772\nSET Key6773 Value6773\nSET Key6774 Value6774\nSET Key6775 Value6775\nSET Key6776 Value6776\nSET Key6777 Value6777\nSET Key6778 Value6778\nSET Key6779 Value6779\nSET Key6780 Value6780\nSET Key6781 Value6781\nSET Key6782 Value6782\nSET Key6783 Value6783\nSET Key6784 Value6784\nSET Key6785 Value6785\nSET Key6786 Value6786\nSET Key6787 Value6787\nSET Key6788 Value6788\nSET Key6789 Value6789\nSET Key6790 Value6790\nSET Key6791 Value6791\nSET Key6792 Value6792\nSET Key6793 Value6793\nSET Key6794 Value6794\nSET Key6795 Value6795\nSET Key6796 Value6796\nSET Key6797 Value6797\nSET Key6798 Value6798\nSET Key6799 Value6799\nSET Key6800 Value6800\nSET Key6801 Value6801\nSET Key6802 Value6802\nSET Key6803 Value6803\nSET Key6804 Value6804\nSET Key6805 Value6805\nSET Key6806 Value6806\nSET Key6807 Value6807\nSET Key6808 Value6808\nSET Key6809 Value6809\nSET Key6810 Value6810\nSET Key6811 Value6811\nSET Key6812 Value6812\nSET Key6813 Value6813\nSET Key6814 Value6814\nSET Key6815 Value6815\nSET Key6816 Value6816\nSET Key6817 Value6817\nSET Key6818 Value6818\nSET Key6819 Value6819\nSET Key6820 Value6820\nSET Key6821 Value6821\nSET Key6822 Value6822\nSET Key6823 Value6823\nSET Key6824 Value6824\nSET Key6825 Value6825\nSET Key6826 Value6826\nSET Key6827 Value6827\nSET Key6828 Value6828\nSET Key6829 Value6829\nSET Key6830 Value6830\nSET Key6831 Value6831\nSET Key6832 Value6832\nSET Key6833 Value6833\nSET Key6834 Value6834\nSET Key6835 Value6835\nSET Key6836 Value6836\nSET Key6837 Value6837\nSET Key6838 Value6838\nSET Key6839 Value6839\nSET Key6840 Value6840\nSET Key6841 Value6841\nSET Key6842 Value6842\nSET Key6843 Value6843\nSET Key6844 Value6844\nSET Key6845 Value6845\nSET Key6846 Value6846\nSET Key6847 Value6847\nSET Key6848 Value6848\nSET Key6849 Value6849\nSET Key6850 Value6850\nSET Key6851 Value6851\nSET Key6852 Value6852\nSET Key6853 Value6853\nSET Key6854 Value6854\nSET Key6855 Value6855\nSET Key6856 Value6856\nSET Key6857 Value6857\nSET Key6858 Value6858\nSET Key6859 Value6859\nSET Key6860 Value6860\nSET Key6861 Value6861\nSET Key6862 Value6862\nSET Key6863 Value6863\nSET Key6864 Value6864\nSET Key6865 Value6865\nSET Key6866 Value6866\nSET Key6867 Value6867\nSET Key6868 Value6868\nSET Key6869 Value6869\nSET Key6870 Value6870\nSET Key6871 Value6871\nSET Key6872 Value6872\nSET Key6873 Value6873\nSET Key6874 Value6874\nSET Key6875 Value6875\nSET Key6876 Value6876\nSET Key6877 Value6877\nSET Key6878 Value6878\nSET Key6879 Value6879\nSET Key6880 Value6880\nSET Key6881 Value6881\nSET Key6882 Value6882\nSET Key6883 Value6883\nSET Key6884 Value6884\nSET Key6885 Value6885\nSET Key6886 Value6886\nSET Key6887 Value6887\nSET Key6888 Value6888\nSET Key6889 Value6889\nSET Key6890 Value6890\nSET Key6891 Value6891\nSET Key6892 Value6892\nSET Key6893 Value6893\nSET Key6894 Value6894\nSET Key6895 Value6895\nSET Key6896 Value6896\nSET Key6897 Value6897\nSET Key6898 Value6898\nSET Key6899 Value6899\nSET Key6900 Value6900\nSET Key6901 Value6901\nSET Key6902 Value6902\nSET Key6903 Value6903\nSET Key6904 Value6904\nSET Key6905 Value6905\nSET Key6906 Value6906\nSET Key6907 Value6907\nSET Key6908 Value6908\nSET Key6909 Value6909\nSET Key6910 Value6910\nSET Key6911 Value6911\nSET Key6912 Value6912\nSET Key6913 Value6913\nSET Key6914 Value6914\nSET Key6915 Value6915\nSET Key6916 Value6916\nSET Key6917 Value6917\nSET Key6918 Value6918\nSET Key6919 Value6919\nSET Key6920 Value6920\nSET Key6921 Value6921\nSET Key6922 Value6922\nSET Key6923 Value6923\nSET Key6924 Value6924\nSET Key6925 Value6925\nSET Key6926 Value6926\nSET Key6927 Value6927\nSET Key6928 Value6928\nSET Key6929 Value6929\nSET Key6930 Value6930\nSET Key6931 Value6931\nSET Key6932 Value6932\nSET Key6933 Value6933\nSET Key6934 Value6934\nSET Key6935 Value6935\nSET Key6936 Value6936\nSET Key6937 Value6937\nSET Key6938 Value6938\nSET Key6939 Value6939\nSET Key6940 Value6940\nSET Key6941 Value6941\nSET Key6942 Value6942\nSET Key6943 Value6943\nSET Key6944 Value6944\nSET Key6945 Value6945\nSET Key6946 Value6946\nSET Key6947 Value6947\nSET Key6948 Value6948\nSET Key6949 Value6949\nSET Key6950 Value6950\nSET Key6951 Value6951\nSET Key6952 Value6952\nSET Key6953 Value6953\nSET Key6954 Value6954\nSET Key6955 Value6955\nSET Key6956 Value6956\nSET Key6957 Value6957\nSET Key6958 Value6958\nSET Key6959 Value6959\nSET Key6960 Value6960\nSET Key6961 Value6961\nSET Key6962 Value6962\nSET Key6963 Value6963\nSET Key6964 Value6964\nSET Key6965 Value6965\nSET Key6966 Value6966\nSET Key6967 Value6967\nSET Key6968 Value6968\nSET Key6969 Value6969\nSET Key6970 Value6970\nSET Key6971 Value6971\nSET Key6972 Value6972\nSET Key6973 Value6973\nSET Key6974 Value6974\nSET Key6975 Value6975\nSET Key6976 Value6976\nSET Key6977 Value6977\nSET Key6978 Value6978\nSET Key6979 Value6979\nSET Key6980 Value6980\nSET Key6981 Value6981\nSET Key6982 Value6982\nSET Key6983 Value6983\nSET Key6984 Value6984\nSET Key6985 Value6985\nSET Key6986 Value6986\nSET Key6987 Value6987\nSET Key6988 Value6988\nSET Key6989 Value6989\nSET Key6990 Value6990\nSET Key6991 Value6991\nSET Key6992 Value6992\nSET Key6993 Value6993\nSET Key6994 Value6994\nSET Key6995 Value6995\nSET Key6996 Value6996\nSET Key6997 Value6997\nSET Key6998 Value6998\nSET Key6999 Value6999\nSET Key7000 Value7000\nSET Key7001 Value7001\nSET Key7002 Value7002\nSET Key7003 Value7003\nSET Key7004 Value7004\nSET Key7005 Value7005\nSET Key7006 Value7006\nSET Key7007 Value7007\nSET Key7008 Value7008\nSET Key7009 Value7009\nSET Key7010 Value7010\nSET Key7011 Value7011\nSET Key7012 Value7012\nSET Key7013 Value7013\nSET Key7014 Value7014\nSET Key7015 Value7015\nSET Key7016 Value7016\nSET Key7017 Value7017\nSET Key7018 Value7018\nSET Key7019 Value7019\nSET Key7020 Value7020\nSET Key7021 Value7021\nSET Key7022 Value7022\nSET Key7023 Value7023\nSET Key7024 Value7024\nSET Key7025 Value7025\nSET Key7026 Value7026\nSET Key7027 Value7027\nSET Key7028 Value7028\nSET Key7029 Value7029\nSET Key7030 Value7030\nSET Key7031 Value7031\nSET Key7032 Value7032\nSET Key7033 Value7033\nSET Key7034 Value7034\nSET Key7035 Value7035\nSET Key7036 Value7036\nSET Key7037 Value7037\nSET Key7038 Value7038\nSET Key7039 Value7039\nSET Key7040 Value7040\nSET Key7041 Value7041\nSET Key7042 Value7042\nSET Key7043 Value7043\nSET Key7044 Value7044\nSET Key7045 Value7045\nSET Key7046 Value7046\nSET Key7047 Value7047\nSET Key7048 Value7048\nSET Key7049 Value7049\nSET Key7050 Value7050\nSET Key7051 Value7051\nSET Key7052 Value7052\nSET Key7053 Value7053\nSET Key7054 Value7054\nSET Key7055 Value7055\nSET Key7056 Value7056\nSET Key7057 Value7057\nSET Key7058 Value7058\nSET Key7059 Value7059\nSET Key7060 Value7060\nSET Key7061 Value7061\nSET Key7062 Value7062\nSET Key7063 Value7063\nSET Key7064 Value7064\nSET Key7065 Value7065\nSET Key7066 Value7066\nSET Key7067 Value7067\nSET Key7068 Value7068\nSET Key7069 Value7069\nSET Key7070 Value7070\nSET Key7071 Value7071\nSET Key7072 Value7072\nSET Key7073 Value7073\nSET Key7074 Value7074\nSET Key7075 Value7075\nSET Key7076 Value7076\nSET Key7077 Value7077\nSET Key7078 Value7078\nSET Key7079 Value7079\nSET Key7080 Value7080\nSET Key7081 Value7081\nSET Key7082 Value7082\nSET Key7083 Value7083\nSET Key7084 Value7084\nSET Key7085 Value7085\nSET Key7086 Value7086\nSET Key7087 Value7087\nSET Key7088 Value7088\nSET Key7089 Value7089\nSET Key7090 Value7090\nSET Key7091 Value7091\nSET Key7092 Value7092\nSET Key7093 Value7093\nSET Key7094 Value7094\nSET Key7095 Value7095\nSET Key7096 Value7096\nSET Key7097 Value7097\nSET Key7098 Value7098\nSET Key7099 Value7099\nSET Key7100 Value7100\nSET Key7101 Value7101\nSET Key7102 Value7102\nSET Key7103 Value7103\nSET Key7104 Value7104\nSET Key7105 Value7105\nSET Key7106 Value7106\nSET Key7107 Value7107\nSET Key7108 Value7108\nSET Key7109 Value7109\nSET Key7110 Value7110\nSET Key7111 Value7111\nSET Key7112 Value7112\nSET Key7113 Value7113\nSET Key7114 Value7114\nSET Key7115 Value7115\nSET Key7116 Value7116\nSET Key7117 Value7117\nSET Key7118 Value7118\nSET Key7119 Value7119\nSET Key7120 Value7120\nSET Key7121 Value7121\nSET Key7122 Value7122\nSET Key7123 Value7123\nSET Key7124 Value7124\nSET Key7125 Value7125\nSET Key7126 Value7126\nSET Key7127 Value7127\nSET Key7128 Value7128\nSET Key7129 Value7129\nSET Key7130 Value7130\nSET Key7131 Value7131\nSET Key7132 Value7132\nSET Key7133 Value7133\nSET Key7134 Value7134\nSET Key7135 Value7135\nSET Key7136 Value7136\nSET Key7137 Value7137\nSET Key7138 Value7138\nSET Key7139 Value7139\nSET Key7140 Value7140\nSET Key7141 Value7141\nSET Key7142 Value7142\nSET Key7143 Value7143\nSET Key7144 Value7144\nSET Key7145 Value7145\nSET Key7146 Value7146\nSET Key7147 Value7147\nSET Key7148 Value7148\nSET Key7149 Value7149\nSET Key7150 Value7150\nSET Key7151 Value7151\nSET Key7152 Value7152\nSET Key7153 Value7153\nSET Key7154 Value7154\nSET Key7155 Value7155\nSET Key7156 Value7156\nSET Key7157 Value7157\nSET Key7158 Value7158\nSET Key7159 Value7159\nSET Key7160 Value7160\nSET Key7161 Value7161\nSET Key7162 Value7162\nSET Key7163 Value7163\nSET Key7164 Value7164\nSET Key7165 Value7165\nSET Key7166 Value7166\nSET Key7167 Value7167\nSET Key7168 Value7168\nSET Key7169 Value7169\nSET Key7170 Value7170\nSET Key7171 Value7171\nSET Key7172 Value7172\nSET Key7173 Value7173\nSET Key7174 Value7174\nSET Key7175 Value7175\nSET Key7176 Value7176\nSET Key7177 Value7177\nSET Key7178 Value7178\nSET Key7179 Value7179\nSET Key7180 Value7180\nSET Key7181 Value7181\nSET Key7182 Value7182\nSET Key7183 Value7183\nSET Key7184 Value7184\nSET Key7185 Value7185\nSET Key7186 Value7186\nSET Key7187 Value7187\nSET Key7188 Value7188\nSET Key7189 Value7189\nSET Key7190 Value7190\nSET Key7191 Value7191\nSET Key7192 Value7192\nSET Key7193 Value7193\nSET Key7194 Value7194\nSET Key7195 Value7195\nSET Key7196 Value7196\nSET Key7197 Value7197\nSET Key7198 Value7198\nSET Key7199 Value7199\nSET Key7200 Value7200\nSET Key7201 Value7201\nSET Key7202 Value7202\nSET Key7203 Value7203\nSET Key7204 Value7204\nSET Key7205 Value7205\nSET Key7206 Value7206\nSET Key7207 Value7207\nSET Key7208 Value7208\nSET Key7209 Value7209\nSET Key7210 Value7210\nSET Key7211 Value7211\nSET Key7212 Value7212\nSET Key7213 Value7213\nSET Key7214 Value7214\nSET Key7215 Value7215\nSET Key7216 Value7216\nSET Key7217 Value7217\nSET Key7218 Value7218\nSET Key7219 Value7219\nSET Key7220 Value7220\nSET Key7221 Value7221\nSET Key7222 Value7222\nSET Key7223 Value7223\nSET Key7224 Value7224\nSET Key7225 Value7225\nSET Key7226 Value7226\nSET Key7227 Value7227\nSET Key7228 Value7228\nSET Key7229 Value7229\nSET Key7230 Value7230\nSET Key7231 Value7231\nSET Key7232 Value7232\nSET Key7233 Value7233\nSET Key7234 Value7234\nSET Key7235 Value7235\nSET Key7236 Value7236\nSET Key7237 Value7237\nSET Key7238 Value7238\nSET Key7239 Value7239\nSET Key7240 Value7240\nSET Key7241 Value7241\nSET Key7242 Value7242\nSET Key7243 Value7243\nSET Key7244 Value7244\nSET Key7245 Value7245\nSET Key7246 Value7246\nSET Key7247 Value7247\nSET Key7248 Value7248\nSET Key7249 Value7249\nSET Key7250 Value7250\nSET Key7251 Value7251\nSET Key7252 Value7252\nSET Key7253 Value7253\nSET Key7254 Value7254\nSET Key7255 Value7255\nSET Key7256 Value7256\nSET Key7257 Value7257\nSET Key7258 Value7258\nSET Key7259 Value7259\nSET Key7260 Value7260\nSET Key7261 Value7261\nSET Key7262 Value7262\nSET Key7263 Value7263\nSET Key7264 Value7264\nSET Key7265 Value7265\nSET Key7266 Value7266\nSET Key7267 Value7267\nSET Key7268 Value7268\nSET Key7269 Value7269\nSET Key7270 Value7270\nSET Key7271 Value7271\nSET Key7272 Value7272\nSET Key7273 Value7273\nSET Key7274 Value7274\nSET Key7275 Value7275\nSET Key7276 Value7276\nSET Key7277 Value7277\nSET Key7278 Value7278\nSET Key7279 Value7279\nSET Key7280 Value7280\nSET Key7281 Value7281\nSET Key7282 Value7282\nSET Key7283 Value7283\nSET Key7284 Value7284\nSET Key7285 Value7285\nSET Key7286 Value7286\nSET Key7287 Value7287\nSET Key7288 Value7288\nSET Key7289 Value7289\nSET Key7290 Value7290\nSET Key7291 Value7291\nSET Key7292 Value7292\nSET Key7293 Value7293\nSET Key7294 Value7294\nSET Key7295 Value7295\nSET Key7296 Value7296\nSET Key7297 Value7297\nSET Key7298 Value7298\nSET Key7299 Value7299\nSET Key7300 Value7300\nSET Key7301 Value7301\nSET Key7302 Value7302\nSET Key7303 Value7303\nSET Key7304 Value7304\nSET Key7305 Value7305\nSET Key7306 Value7306\nSET Key7307 Value7307\nSET Key7308 Value7308\nSET Key7309 Value7309\nSET Key7310 Value7310\nSET Key7311 Value7311\nSET Key7312 Value7312\nSET Key7313 Value7313\nSET Key7314 Value7314\nSET Key7315 Value7315\nSET Key7316 Value7316\nSET Key7317 Value7317\nSET Key7318 Value7318\nSET Key7319 Value7319\nSET Key7320 Value7320\nSET Key7321 Value7321\nSET Key7322 Value7322\nSET Key7323 Value7323\nSET Key7324 Value7324\nSET Key7325 Value7325\nSET Key7326 Value7326\nSET Key7327 Value7327\nSET Key7328 Value7328\nSET Key7329 Value7329\nSET Key7330 Value7330\nSET Key7331 Value7331\nSET Key7332 Value7332\nSET Key7333 Value7333\nSET Key7334 Value7334\nSET Key7335 Value7335\nSET Key7336 Value7336\nSET Key7337 Value7337\nSET Key7338 Value7338\nSET Key7339 Value7339\nSET Key7340 Value7340\nSET Key7341 Value7341\nSET Key7342 Value7342\nSET Key7343 Value7343\nSET Key7344 Value7344\nSET Key7345 Value7345\nSET Key7346 Value7346\nSET Key7347 Value7347\nSET Key7348 Value7348\nSET Key7349 Value7349\nSET Key7350 Value7350\nSET Key7351 Value7351\nSET Key7352 Value7352\nSET Key7353 Value7353\nSET Key7354 Value7354\nSET Key7355 Value7355\nSET Key7356 Value7356\nSET Key7357 Value7357\nSET Key7358 Value7358\nSET Key7359 Value7359\nSET Key7360 Value7360\nSET Key7361 Value7361\nSET Key7362 Value7362\nSET Key7363 Value7363\nSET Key7364 Value7364\nSET Key7365 Value7365\nSET Key7366 Value7366\nSET Key7367 Value7367\nSET Key7368 Value7368\nSET Key7369 Value7369\nSET Key7370 Value7370\nSET Key7371 Value7371\nSET Key7372 Value7372\nSET Key7373 Value7373\nSET Key7374 Value7374\nSET Key7375 Value7375\nSET Key7376 Value7376\nSET Key7377 Value7377\nSET Key7378 Value7378\nSET Key7379 Value7379\nSET Key7380 Value7380\nSET Key7381 Value7381\nSET Key7382 Value7382\nSET Key7383 Value7383\nSET Key7384 Value7384\nSET Key7385 Value7385\nSET Key7386 Value7386\nSET Key7387 Value7387\nSET Key7388 Value7388\nSET Key7389 Value7389\nSET Key7390 Value7390\nSET Key7391 Value7391\nSET Key7392 Value7392\nSET Key7393 Value7393\nSET Key7394 Value7394\nSET Key7395 Value7395\nSET Key7396 Value7396\nSET Key7397 Value7397\nSET Key7398 Value7398\nSET Key7399 Value7399\nSET Key7400 Value7400\nSET Key7401 Value7401\nSET Key7402 Value7402\nSET Key7403 Value7403\nSET Key7404 Value7404\nSET Key7405 Value7405\nSET Key7406 Value7406\nSET Key7407 Value7407\nSET Key7408 Value7408\nSET Key7409 Value7409\nSET Key7410 Value7410\nSET Key7411 Value7411\nSET Key7412 Value7412\nSET Key7413 Value7413\nSET Key7414 Value7414\nSET Key7415 Value7415\nSET Key7416 Value7416\nSET Key7417 Value7417\nSET Key7418 Value7418\nSET Key7419 Value7419\nSET Key7420 Value7420\nSET Key7421 Value7421\nSET Key7422 Value7422\nSET Key7423 Value7423\nSET Key7424 Value7424\nSET Key7425 Value7425\nSET Key7426 Value7426\nSET Key7427 Value7427\nSET Key7428 Value7428\nSET Key7429 Value7429\nSET Key7430 Value7430\nSET Key7431 Value7431\nSET Key7432 Value7432\nSET Key7433 Value7433\nSET Key7434 Value7434\nSET Key7435 Value7435\nSET Key7436 Value7436\nSET Key7437 Value7437\nSET Key7438 Value7438\nSET Key7439 Value7439\nSET Key7440 Value7440\nSET Key7441 Value7441\nSET Key7442 Value7442\nSET Key7443 Value7443\nSET Key7444 Value7444\nSET Key7445 Value7445\nSET Key7446 Value7446\nSET Key7447 Value7447\nSET Key7448 Value7448\nSET Key7449 Value7449\nSET Key7450 Value7450\nSET Key7451 Value7451\nSET Key7452 Value7452\nSET Key7453 Value7453\nSET Key7454 Value7454\nSET Key7455 Value7455\nSET Key7456 Value7456\nSET Key7457 Value7457\nSET Key7458 Value7458\nSET Key7459 Value7459\nSET Key7460 Value7460\nSET Key7461 Value7461\nSET Key7462 Value7462\nSET Key7463 Value7463\nSET Key7464 Value7464\nSET Key7465 Value7465\nSET Key7466 Value7466\nSET Key7467 Value7467\nSET Key7468 Value7468\nSET Key7469 Value7469\nSET Key7470 Value7470\nSET Key7471 Value7471\nSET Key7472 Value7472\nSET Key7473 Value7473\nSET Key7474 Value7474\nSET Key7475 Value7475\nSET Key7476 Value7476\nSET Key7477 Value7477\nSET Key7478 Value7478\nSET Key7479 Value7479\nSET Key7480 Value7480\nSET Key7481 Value7481\nSET Key7482 Value7482\nSET Key7483 Value7483\nSET Key7484 Value7484\nSET Key7485 Value7485\nSET Key7486 Value7486\nSET Key7487 Value7487\nSET Key7488 Value7488\nSET Key7489 Value7489\nSET Key7490 Value7490\nSET Key7491 Value7491\nSET Key7492 Value7492\nSET Key7493 Value7493\nSET Key7494 Value7494\nSET Key7495 Value7495\nSET Key7496 Value7496\nSET Key7497 Value7497\nSET Key7498 Value7498\nSET Key7499 Value7499\nSET Key7500 Value7500\nSET Key7501 Value7501\nSET Key7502 Value7502\nSET Key7503 Value7503\nSET Key7504 Value7504\nSET Key7505 Value7505\nSET Key7506 Value7506\nSET Key7507 Value7507\nSET Key7508 Value7508\nSET Key7509 Value7509\nSET Key7510 Value7510\nSET Key7511 Value7511\nSET Key7512 Value7512\nSET Key7513 Value7513\nSET Key7514 Value7514\nSET Key7515 Value7515\nSET Key7516 Value7516\nSET Key7517 Value7517\nSET Key7518 Value7518\nSET Key7519 Value7519\nSET Key7520 Value7520\nSET Key7521 Value7521\nSET Key7522 Value7522\nSET Key7523 Value7523\nSET Key7524 Value7524\nSET Key7525 Value7525\nSET Key7526 Value7526\nSET Key7527 Value7527\nSET Key7528 Value7528\nSET Key7529 Value7529\nSET Key7530 Value7530\nSET Key7531 Value7531\nSET Key7532 Value7532\nSET Key7533 Value7533\nSET Key7534 Value7534\nSET Key7535 Value7535\nSET Key7536 Value7536\nSET Key7537 Value7537\nSET Key7538 Value7538\nSET Key7539 Value7539\nSET Key7540 Value7540\nSET Key7541 Value7541\nSET Key7542 Value7542\nSET Key7543 Value7543\nSET Key7544 Value7544\nSET Key7545 Value7545\nSET Key7546 Value7546\nSET Key7547 Value7547\nSET Key7548 Value7548\nSET Key7549 Value7549\nSET Key7550 Value7550\nSET Key7551 Value7551\nSET Key7552 Value7552\nSET Key7553 Value7553\nSET Key7554 Value7554\nSET Key7555 Value7555\nSET Key7556 Value7556\nSET Key7557 Value7557\nSET Key7558 Value7558\nSET Key7559 Value7559\nSET Key7560 Value7560\nSET Key7561 Value7561\nSET Key7562 Value7562\nSET Key7563 Value7563\nSET Key7564 Value7564\nSET Key7565 Value7565\nSET Key7566 Value7566\nSET Key7567 Value7567\nSET Key7568 Value7568\nSET Key7569 Value7569\nSET Key7570 Value7570\nSET Key7571 Value7571\nSET Key7572 Value7572\nSET Key7573 Value7573\nSET Key7574 Value7574\nSET Key7575 Value7575\nSET Key7576 Value7576\nSET Key7577 Value7577\nSET Key7578 Value7578\nSET Key7579 Value7579\nSET Key7580 Value7580\nSET Key7581 Value7581\nSET Key7582 Value7582\nSET Key7583 Value7583\nSET Key7584 Value7584\nSET Key7585 Value7585\nSET Key7586 Value7586\nSET Key7587 Value7587\nSET Key7588 Value7588\nSET Key7589 Value7589\nSET Key7590 Value7590\nSET Key7591 Value7591\nSET Key7592 Value7592\nSET Key7593 Value7593\nSET Key7594 Value7594\nSET Key7595 Value7595\nSET Key7596 Value7596\nSET Key7597 Value7597\nSET Key7598 Value7598\nSET Key7599 Value7599\nSET Key7600 Value7600\nSET Key7601 Value7601\nSET Key7602 Value7602\nSET Key7603 Value7603\nSET Key7604 Value7604\nSET Key7605 Value7605\nSET Key7606 Value7606\nSET Key7607 Value7607\nSET Key7608 Value7608\nSET Key7609 Value7609\nSET Key7610 Value7610\nSET Key7611 Value7611\nSET Key7612 Value7612\nSET Key7613 Value7613\nSET Key7614 Value7614\nSET Key7615 Value7615\nSET Key7616 Value7616\nSET Key7617 Value7617\nSET Key7618 Value7618\nSET Key7619 Value7619\nSET Key7620 Value7620\nSET Key7621 Value7621\nSET Key7622 Value7622\nSET Key7623 Value7623\nSET Key7624 Value7624\nSET Key7625 Value7625\nSET Key7626 Value7626\nSET Key7627 Value7627\nSET Key7628 Value7628\nSET Key7629 Value7629\nSET Key7630 Value7630\nSET Key7631 Value7631\nSET Key7632 Value7632\nSET Key7633 Value7633\nSET Key7634 Value7634\nSET Key7635 Value7635\nSET Key7636 Value7636\nSET Key7637 Value7637\nSET Key7638 Value7638\nSET Key7639 Value7639\nSET Key7640 Value7640\nSET Key7641 Value7641\nSET Key7642 Value7642\nSET Key7643 Value7643\nSET Key7644 Value7644\nSET Key7645 Value7645\nSET Key7646 Value7646\nSET Key7647 Value7647\nSET Key7648 Value7648\nSET Key7649 Value7649\nSET Key7650 Value7650\nSET Key7651 Value7651\nSET Key7652 Value7652\nSET Key7653 Value7653\nSET Key7654 Value7654\nSET Key7655 Value7655\nSET Key7656 Value7656\nSET Key7657 Value7657\nSET Key7658 Value7658\nSET Key7659 Value7659\nSET Key7660 Value7660\nSET Key7661 Value7661\nSET Key7662 Value7662\nSET Key7663 Value7663\nSET Key7664 Value7664\nSET Key7665 Value7665\nSET Key7666 Value7666\nSET Key7667 Value7667\nSET Key7668 Value7668\nSET Key7669 Value7669\nSET Key7670 Value7670\nSET Key7671 Value7671\nSET Key7672 Value7672\nSET Key7673 Value7673\nSET Key7674 Value7674\nSET Key7675 Value7675\nSET Key7676 Value7676\nSET Key7677 Value7677\nSET Key7678 Value7678\nSET Key7679 Value7679\nSET Key7680 Value7680\nSET Key7681 Value7681\nSET Key7682 Value7682\nSET Key7683 Value7683\nSET Key7684 Value7684\nSET Key7685 Value7685\nSET Key7686 Value7686\nSET Key7687 Value7687\nSET Key7688 Value7688\nSET Key7689 Value7689\nSET Key7690 Value7690\nSET Key7691 Value7691\nSET Key7692 Value7692\nSET Key7693 Value7693\nSET Key7694 Value7694\nSET Key7695 Value7695\nSET Key7696 Value7696\nSET Key7697 Value7697\nSET Key7698 Value7698\nSET Key7699 Value7699\nSET Key7700 Value7700\nSET Key7701 Value7701\nSET Key7702 Value7702\nSET Key7703 Value7703\nSET Key7704 Value7704\nSET Key7705 Value7705\nSET Key7706 Value7706\nSET Key7707 Value7707\nSET Key7708 Value7708\nSET Key7709 Value7709\nSET Key7710 Value7710\nSET Key7711 Value7711\nSET Key7712 Value7712\nSET Key7713 Value7713\nSET Key7714 Value7714\nSET Key7715 Value7715\nSET Key7716 Value7716\nSET Key7717 Value7717\nSET Key7718 Value7718\nSET Key7719 Value7719\nSET Key7720 Value7720\nSET Key7721 Value7721\nSET Key7722 Value7722\nSET Key7723 Value7723\nSET Key7724 Value7724\nSET Key7725 Value7725\nSET Key7726 Value7726\nSET Key7727 Value7727\nSET Key7728 Value7728\nSET Key7729 Value7729\nSET Key7730 Value7730\nSET Key7731 Value7731\nSET Key7732 Value7732\nSET Key7733 Value7733\nSET Key7734 Value7734\nSET Key7735 Value7735\nSET Key7736 Value7736\nSET Key7737 Value7737\nSET Key7738 Value7738\nSET Key7739 Value7739\nSET Key7740 Value7740\nSET Key7741 Value7741\nSET Key7742 Value7742\nSET Key7743 Value7743\nSET Key7744 Value7744\nSET Key7745 Value7745\nSET Key7746 Value7746\nSET Key7747 Value7747\nSET Key7748 Value7748\nSET Key7749 Value7749\nSET Key7750 Value7750\nSET Key7751 Value7751\nSET Key7752 Value7752\nSET Key7753 Value7753\nSET Key7754 Value7754\nSET Key7755 Value7755\nSET Key7756 Value7756\nSET Key7757 Value7757\nSET Key7758 Value7758\nSET Key7759 Value7759\nSET Key7760 Value7760\nSET Key7761 Value7761\nSET Key7762 Value7762\nSET Key7763 Value7763\nSET Key7764 Value7764\nSET Key7765 Value7765\nSET Key7766 Value7766\nSET Key7767 Value7767\nSET Key7768 Value7768\nSET Key7769 Value7769\nSET Key7770 Value7770\nSET Key7771 Value7771\nSET Key7772 Value7772\nSET Key7773 Value7773\nSET Key7774 Value7774\nSET Key7775 Value7775\nSET Key7776 Value7776\nSET Key7777 Value7777\nSET Key7778 Value7778\nSET Key7779 Value7779\nSET Key7780 Value7780\nSET Key7781 Value7781\nSET Key7782 Value7782\nSET Key7783 Value7783\nSET Key7784 Value7784\nSET Key7785 Value7785\nSET Key7786 Value7786\nSET Key7787 Value7787\nSET Key7788 Value7788\nSET Key7789 Value7789\nSET Key7790 Value7790\nSET Key7791 Value7791\nSET Key7792 Value7792\nSET Key7793 Value7793\nSET Key7794 Value7794\nSET Key7795 Value7795\nSET Key7796 Value7796\nSET Key7797 Value7797\nSET Key7798 Value7798\nSET Key7799 Value7799\nSET Key7800 Value7800\nSET Key7801 Value7801\nSET Key7802 Value7802\nSET Key7803 Value7803\nSET Key7804 Value7804\nSET Key7805 Value7805\nSET Key7806 Value7806\nSET Key7807 Value7807\nSET Key7808 Value7808\nSET Key7809 Value7809\nSET Key7810 Value7810\nSET Key7811 Value7811\nSET Key7812 Value7812\nSET Key7813 Value7813\nSET Key7814 Value7814\nSET Key7815 Value7815\nSET Key7816 Value7816\nSET Key7817 Value7817\nSET Key7818 Value7818\nSET Key7819 Value7819\nSET Key7820 Value7820\nSET Key7821 Value7821\nSET Key7822 Value7822\nSET Key7823 Value7823\nSET Key7824 Value7824\nSET Key7825 Value7825\nSET Key7826 Value7826\nSET Key7827 Value7827\nSET Key7828 Value7828\nSET Key7829 Value7829\nSET Key7830 Value7830\nSET Key7831 Value7831\nSET Key7832 Value7832\nSET Key7833 Value7833\nSET Key7834 Value7834\nSET Key7835 Value7835\nSET Key7836 Value7836\nSET Key7837 Value7837\nSET Key7838 Value7838\nSET Key7839 Value7839\nSET Key7840 Value7840\nSET Key7841 Value7841\nSET Key7842 Value7842\nSET Key7843 Value7843\nSET Key7844 Value7844\nSET Key7845 Value7845\nSET Key7846 Value7846\nSET Key7847 Value7847\nSET Key7848 Value7848\nSET Key7849 Value7849\nSET Key7850 Value7850\nSET Key7851 Value7851\nSET Key7852 Value7852\nSET Key7853 Value7853\nSET Key7854 Value7854\nSET Key7855 Value7855\nSET Key7856 Value7856\nSET Key7857 Value7857\nSET Key7858 Value7858\nSET Key7859 Value7859\nSET Key7860 Value7860\nSET Key7861 Value7861\nSET Key7862 Value7862\nSET Key7863 Value7863\nSET Key7864 Value7864\nSET Key7865 Value7865\nSET Key7866 Value7866\nSET Key7867 Value7867\nSET Key7868 Value7868\nSET Key7869 Value7869\nSET Key7870 Value7870\nSET Key7871 Value7871\nSET Key7872 Value7872\nSET Key7873 Value7873\nSET Key7874 Value7874\nSET Key7875 Value7875\nSET Key7876 Value7876\nSET Key7877 Value7877\nSET Key7878 Value7878\nSET Key7879 Value7879\nSET Key7880 Value7880\nSET Key7881 Value7881\nSET Key7882 Value7882\nSET Key7883 Value7883\nSET Key7884 Value7884\nSET Key7885 Value7885\nSET Key7886 Value7886\nSET Key7887 Value7887\nSET Key7888 Value7888\nSET Key7889 Value7889\nSET Key7890 Value7890\nSET Key7891 Value7891\nSET Key7892 Value7892\nSET Key7893 Value7893\nSET Key7894 Value7894\nSET Key7895 Value7895\nSET Key7896 Value7896\nSET Key7897 Value7897\nSET Key7898 Value7898\nSET Key7899 Value7899\nSET Key7900 Value7900\nSET Key7901 Value7901\nSET Key7902 Value7902\nSET Key7903 Value7903\nSET Key7904 Value7904\nSET Key7905 Value7905\nSET Key7906 Value7906\nSET Key7907 Value7907\nSET Key7908 Value7908\nSET Key7909 Value7909\nSET Key7910 Value7910\nSET Key7911 Value7911\nSET Key7912 Value7912\nSET Key7913 Value7913\nSET Key7914 Value7914\nSET Key7915 Value7915\nSET Key7916 Value7916\nSET Key7917 Value7917\nSET Key7918 Value7918\nSET Key7919 Value7919\nSET Key7920 Value7920\nSET Key7921 Value7921\nSET Key7922 Value7922\nSET Key7923 Value7923\nSET Key7924 Value7924\nSET Key7925 Value7925\nSET Key7926 Value7926\nSET Key7927 Value7927\nSET Key7928 Value7928\nSET Key7929 Value7929\nSET Key7930 Value7930\nSET Key7931 Value7931\nSET Key7932 Value7932\nSET Key7933 Value7933\nSET Key7934 Value7934\nSET Key7935 Value7935\nSET Key7936 Value7936\nSET Key7937 Value7937\nSET Key7938 Value7938\nSET Key7939 Value7939\nSET Key7940 Value7940\nSET Key7941 Value7941\nSET Key7942 Value7942\nSET Key7943 Value7943\nSET Key7944 Value7944\nSET Key7945 Value7945\nSET Key7946 Value7946\nSET Key7947 Value7947\nSET Key7948 Value7948\nSET Key7949 Value7949\nSET Key7950 Value7950\nSET Key7951 Value7951\nSET Key7952 Value7952\nSET Key7953 Value7953\nSET Key7954 Value7954\nSET Key7955 Value7955\nSET Key7956 Value7956\nSET Key7957 Value7957\nSET Key7958 Value7958\nSET Key7959 Value7959\nSET Key7960 Value7960\nSET Key7961 Value7961\nSET Key7962 Value7962\nSET Key7963 Value7963\nSET Key7964 Value7964\nSET Key7965 Value7965\nSET Key7966 Value7966\nSET Key7967 Value7967\nSET Key7968 Value7968\nSET Key7969 Value7969\nSET Key7970 Value7970\nSET Key7971 Value7971\nSET Key7972 Value7972\nSET Key7973 Value7973\nSET Key7974 Value7974\nSET Key7975 Value7975\nSET Key7976 Value7976\nSET Key7977 Value7977\nSET Key7978 Value7978\nSET Key7979 Value7979\nSET Key7980 Value7980\nSET Key7981 Value7981\nSET Key7982 Value7982\nSET Key7983 Value7983\nSET Key7984 Value7984\nSET Key7985 Value7985\nSET Key7986 Value7986\nSET Key7987 Value7987\nSET Key7988 Value7988\nSET Key7989 Value7989\nSET Key7990 Value7990\nSET Key7991 Value7991\nSET Key7992 Value7992\nSET Key7993 Value7993\nSET Key7994 Value7994\nSET Key7995 Value7995\nSET Key7996 Value7996\nSET Key7997 Value7997\nSET Key7998 Value7998\nSET Key7999 Value7999\nSET Key8000 Value8000\nSET Key8001 Value8001\nSET Key8002 Value8002\nSET Key8003 Value8003\nSET Key8004 Value8004\nSET Key8005 Value8005\nSET Key8006 Value8006\nSET Key8007 Value8007\nSET Key8008 Value8008\nSET Key8009 Value8009\nSET Key8010 Value8010\nSET Key8011 Value8011\nSET Key8012 Value8012\nSET Key8013 Value8013\nSET Key8014 Value8014\nSET Key8015 Value8015\nSET Key8016 Value8016\nSET Key8017 Value8017\nSET Key8018 Value8018\nSET Key8019 Value8019\nSET Key8020 Value8020\nSET Key8021 Value8021\nSET Key8022 Value8022\nSET Key8023 Value8023\nSET Key8024 Value8024\nSET Key8025 Value8025\nSET Key8026 Value8026\nSET Key8027 Value8027\nSET Key8028 Value8028\nSET Key8029 Value8029\nSET Key8030 Value8030\nSET Key8031 Value8031\nSET Key8032 Value8032\nSET Key8033 Value8033\nSET Key8034 Value8034\nSET Key8035 Value8035\nSET Key8036 Value8036\nSET Key8037 Value8037\nSET Key8038 Value8038\nSET Key8039 Value8039\nSET Key8040 Value8040\nSET Key8041 Value8041\nSET Key8042 Value8042\nSET Key8043 Value8043\nSET Key8044 Value8044\nSET Key8045 Value8045\nSET Key8046 Value8046\nSET Key8047 Value8047\nSET Key8048 Value8048\nSET Key8049 Value8049\nSET Key8050 Value8050\nSET Key8051 Value8051\nSET Key8052 Value8052\nSET Key8053 Value8053\nSET Key8054 Value8054\nSET Key8055 Value8055\nSET Key8056 Value8056\nSET Key8057 Value8057\nSET Key8058 Value8058\nSET Key8059 Value8059\nSET Key8060 Value8060\nSET Key8061 Value8061\nSET Key8062 Value8062\nSET Key8063 Value8063\nSET Key8064 Value8064\nSET Key8065 Value8065\nSET Key8066 Value8066\nSET Key8067 Value8067\nSET Key8068 Value8068\nSET Key8069 Value8069\nSET Key8070 Value8070\nSET Key8071 Value8071\nSET Key8072 Value8072\nSET Key8073 Value8073\nSET Key8074 Value8074\nSET Key8075 Value8075\nSET Key8076 Value8076\nSET Key8077 Value8077\nSET Key8078 Value8078\nSET Key8079 Value8079\nSET Key8080 Value8080\nSET Key8081 Value8081\nSET Key8082 Value8082\nSET Key8083 Value8083\nSET Key8084 Value8084\nSET Key8085 Value8085\nSET Key8086 Value8086\nSET Key8087 Value8087\nSET Key8088 Value8088\nSET Key8089 Value8089\nSET Key8090 Value8090\nSET Key8091 Value8091\nSET Key8092 Value8092\nSET Key8093 Value8093\nSET Key8094 Value8094\nSET Key8095 Value8095\nSET Key8096 Value8096\nSET Key8097 Value8097\nSET Key8098 Value8098\nSET Key8099 Value8099\nSET Key8100 Value8100\nSET Key8101 Value8101\nSET Key8102 Value8102\nSET Key8103 Value8103\nSET Key8104 Value8104\nSET Key8105 Value8105\nSET Key8106 Value8106\nSET Key8107 Value8107\nSET Key8108 Value8108\nSET Key8109 Value8109\nSET Key8110 Value8110\nSET Key8111 Value8111\nSET Key8112 Value8112\nSET Key8113 Value8113\nSET Key8114 Value8114\nSET Key8115 Value8115\nSET Key8116 Value8116\nSET Key8117 Value8117\nSET Key8118 Value8118\nSET Key8119 Value8119\nSET Key8120 Value8120\nSET Key8121 Value8121\nSET Key8122 Value8122\nSET Key8123 Value8123\nSET Key8124 Value8124\nSET Key8125 Value8125\nSET Key8126 Value8126\nSET Key8127 Value8127\nSET Key8128 Value8128\nSET Key8129 Value8129\nSET Key8130 Value8130\nSET Key8131 Value8131\nSET Key8132 Value8132\nSET Key8133 Value8133\nSET Key8134 Value8134\nSET Key8135 Value8135\nSET Key8136 Value8136\nSET Key8137 Value8137\nSET Key8138 Value8138\nSET Key8139 Value8139\nSET Key8140 Value8140\nSET Key8141 Value8141\nSET Key8142 Value8142\nSET Key8143 Value8143\nSET Key8144 Value8144\nSET Key8145 Value8145\nSET Key8146 Value8146\nSET Key8147 Value8147\nSET Key8148 Value8148\nSET Key8149 Value8149\nSET Key8150 Value8150\nSET Key8151 Value8151\nSET Key8152 Value8152\nSET Key8153 Value8153\nSET Key8154 Value8154\nSET Key8155 Value8155\nSET Key8156 Value8156\nSET Key8157 Value8157\nSET Key8158 Value8158\nSET Key8159 Value8159\nSET Key8160 Value8160\nSET Key8161 Value8161\nSET Key8162 Value8162\nSET Key8163 Value8163\nSET Key8164 Value8164\nSET Key8165 Value8165\nSET Key8166 Value8166\nSET Key8167 Value8167\nSET Key8168 Value8168\nSET Key8169 Value8169\nSET Key8170 Value8170\nSET Key8171 Value8171\nSET Key8172 Value8172\nSET Key8173 Value8173\nSET Key8174 Value8174\nSET Key8175 Value8175\nSET Key8176 Value8176\nSET Key8177 Value8177\nSET Key8178 Value8178\nSET Key8179 Value8179\nSET Key8180 Value8180\nSET Key8181 Value8181\nSET Key8182 Value8182\nSET Key8183 Value8183\nSET Key8184 Value8184\nSET Key8185 Value8185\nSET Key8186 Value8186\nSET Key8187 Value8187\nSET Key8188 Value8188\nSET Key8189 Value8189\nSET Key8190 Value8190\nSET Key8191 Value8191\nSET Key8192 Value8192\nSET Key8193 Value8193\nSET Key8194 Value8194\nSET Key8195 Value8195\nSET Key8196 Value8196\nSET Key8197 Value8197\nSET Key8198 Value8198\nSET Key8199 Value8199\nSET Key8200 Value8200\nSET Key8201 Value8201\nSET Key8202 Value8202\nSET Key8203 Value8203\nSET Key8204 Value8204\nSET Key8205 Value8205\nSET Key8206 Value8206\nSET Key8207 Value8207\nSET Key8208 Value8208\nSET Key8209 Value8209\nSET Key8210 Value8210\nSET Key8211 Value8211\nSET Key8212 Value8212\nSET Key8213 Value8213\nSET Key8214 Value8214\nSET Key8215 Value8215\nSET Key8216 Value8216\nSET Key8217 Value8217\nSET Key8218 Value8218\nSET Key8219 Value8219\nSET Key8220 Value8220\nSET Key8221 Value8221\nSET Key8222 Value8222\nSET Key8223 Value8223\nSET Key8224 Value8224\nSET Key8225 Value8225\nSET Key8226 Value8226\nSET Key8227 Value8227\nSET Key8228 Value8228\nSET Key8229 Value8229\nSET Key8230 Value8230\nSET Key8231 Value8231\nSET Key8232 Value8232\nSET Key8233 Value8233\nSET Key8234 Value8234\nSET Key8235 Value8235\nSET Key8236 Value8236\nSET Key8237 Value8237\nSET Key8238 Value8238\nSET Key8239 Value8239\nSET Key8240 Value8240\nSET Key8241 Value8241\nSET Key8242 Value8242\nSET Key8243 Value8243\nSET Key8244 Value8244\nSET Key8245 Value8245\nSET Key8246 Value8246\nSET Key8247 Value8247\nSET Key8248 Value8248\nSET Key8249 Value8249\nSET Key8250 Value8250\nSET Key8251 Value8251\nSET Key8252 Value8252\nSET Key8253 Value8253\nSET Key8254 Value8254\nSET Key8255 Value8255\nSET Key8256 Value8256\nSET Key8257 Value8257\nSET Key8258 Value8258\nSET Key8259 Value8259\nSET Key8260 Value8260\nSET Key8261 Value8261\nSET Key8262 Value8262\nSET Key8263 Value8263\nSET Key8264 Value8264\nSET Key8265 Value8265\nSET Key8266 Value8266\nSET Key8267 Value8267\nSET Key8268 Value8268\nSET Key8269 Value8269\nSET Key8270 Value8270\nSET Key8271 Value8271\nSET Key8272 Value8272\nSET Key8273 Value8273\nSET Key8274 Value8274\nSET Key8275 Value8275\nSET Key8276 Value8276\nSET Key8277 Value8277\nSET Key8278 Value8278\nSET Key8279 Value8279\nSET Key8280 Value8280\nSET Key8281 Value8281\nSET Key8282 Value8282\nSET Key8283 Value8283\nSET Key8284 Value8284\nSET Key8285 Value8285\nSET Key8286 Value8286\nSET Key8287 Value8287\nSET Key8288 Value8288\nSET Key8289 Value8289\nSET Key8290 Value8290\nSET Key8291 Value8291\nSET Key8292 Value8292\nSET Key8293 Value8293\nSET Key8294 Value8294\nSET Key8295 Value8295\nSET Key8296 Value8296\nSET Key8297 Value8297\nSET Key8298 Value8298\nSET Key8299 Value8299\nSET Key8300 Value8300\nSET Key8301 Value8301\nSET Key8302 Value8302\nSET Key8303 Value8303\nSET Key8304 Value8304\nSET Key8305 Value8305\nSET Key8306 Value8306\nSET Key8307 Value8307\nSET Key8308 Value8308\nSET Key8309 Value8309\nSET Key8310 Value8310\nSET Key8311 Value8311\nSET Key8312 Value8312\nSET Key8313 Value8313\nSET Key8314 Value8314\nSET Key8315 Value8315\nSET Key8316 Value8316\nSET Key8317 Value8317\nSET Key8318 Value8318\nSET Key8319 Value8319\nSET Key8320 Value8320\nSET Key8321 Value8321\nSET Key8322 Value8322\nSET Key8323 Value8323\nSET Key8324 Value8324\nSET Key8325 Value8325\nSET Key8326 Value8326\nSET Key8327 Value8327\nSET Key8328 Value8328\nSET Key8329 Value8329\nSET Key8330 Value8330\nSET Key8331 Value8331\nSET Key8332 Value8332\nSET Key8333 Value8333\nSET Key8334 Value8334\nSET Key8335 Value8335\nSET Key8336 Value8336\nSET Key8337 Value8337\nSET Key8338 Value8338\nSET Key8339 Value8339\nSET Key8340 Value8340\nSET Key8341 Value8341\nSET Key8342 Value8342\nSET Key8343 Value8343\nSET Key8344 Value8344\nSET Key8345 Value8345\nSET Key8346 Value8346\nSET Key8347 Value8347\nSET Key8348 Value8348\nSET Key8349 Value8349\nSET Key8350 Value8350\nSET Key8351 Value8351\nSET Key8352 Value8352\nSET Key8353 Value8353\nSET Key8354 Value8354\nSET Key8355 Value8355\nSET Key8356 Value8356\nSET Key8357 Value8357\nSET Key8358 Value8358\nSET Key8359 Value8359\nSET Key8360 Value8360\nSET Key8361 Value8361\nSET Key8362 Value8362\nSET Key8363 Value8363\nSET Key8364 Value8364\nSET Key8365 Value8365\nSET Key8366 Value8366\nSET Key8367 Value8367\nSET Key8368 Value8368\nSET Key8369 Value8369\nSET Key8370 Value8370\nSET Key8371 Value8371\nSET Key8372 Value8372\nSET Key8373 Value8373\nSET Key8374 Value8374\nSET Key8375 Value8375\nSET Key8376 Value8376\nSET Key8377 Value8377\nSET Key8378 Value8378\nSET Key8379 Value8379\nSET Key8380 Value8380\nSET Key8381 Value8381\nSET Key8382 Value8382\nSET Key8383 Value8383\nSET Key8384 Value8384\nSET Key8385 Value8385\nSET Key8386 Value8386\nSET Key8387 Value8387\nSET Key8388 Value8388\nSET Key8389 Value8389\nSET Key8390 Value8390\nSET Key8391 Value8391\nSET Key8392 Value8392\nSET Key8393 Value8393\nSET Key8394 Value8394\nSET Key8395 Value8395\nSET Key8396 Value8396\nSET Key8397 Value8397\nSET Key8398 Value8398\nSET Key8399 Value8399\nSET Key8400 Value8400\nSET Key8401 Value8401\nSET Key8402 Value8402\nSET Key8403 Value8403\nSET Key8404 Value8404\nSET Key8405 Value8405\nSET Key8406 Value8406\nSET Key8407 Value8407\nSET Key8408 Value8408\nSET Key8409 Value8409\nSET Key8410 Value8410\nSET Key8411 Value8411\nSET Key8412 Value8412\nSET Key8413 Value8413\nSET Key8414 Value8414\nSET Key8415 Value8415\nSET Key8416 Value8416\nSET Key8417 Value8417\nSET Key8418 Value8418\nSET Key8419 Value8419\nSET Key8420 Value8420\nSET Key8421 Value8421\nSET Key8422 Value8422\nSET Key8423 Value8423\nSET Key8424 Value8424\nSET Key8425 Value8425\nSET Key8426 Value8426\nSET Key8427 Value8427\nSET Key8428 Value8428\nSET Key8429 Value8429\nSET Key8430 Value8430\nSET Key8431 Value8431\nSET Key8432 Value8432\nSET Key8433 Value8433\nSET Key8434 Value8434\nSET Key8435 Value8435\nSET Key8436 Value8436\nSET Key8437 Value8437\nSET Key8438 Value8438\nSET Key8439 Value8439\nSET Key8440 Value8440\nSET Key8441 Value8441\nSET Key8442 Value8442\nSET Key8443 Value8443\nSET Key8444 Value8444\nSET Key8445 Value8445\nSET Key8446 Value8446\nSET Key8447 Value8447\nSET Key8448 Value8448\nSET Key8449 Value8449\nSET Key8450 Value8450\nSET Key8451 Value8451\nSET Key8452 Value8452\nSET Key8453 Value8453\nSET Key8454 Value8454\nSET Key8455 Value8455\nSET Key8456 Value8456\nSET Key8457 Value8457\nSET Key8458 Value8458\nSET Key8459 Value8459\nSET Key8460 Value8460\nSET Key8461 Value8461\nSET Key8462 Value8462\nSET Key8463 Value8463\nSET Key8464 Value8464\nSET Key8465 Value8465\nSET Key8466 Value8466\nSET Key8467 Value8467\nSET Key8468 Value8468\nSET Key8469 Value8469\nSET Key8470 Value8470\nSET Key8471 Value8471\nSET Key8472 Value8472\nSET Key8473 Value8473\nSET Key8474 Value8474\nSET Key8475 Value8475\nSET Key8476 Value8476\nSET Key8477 Value8477\nSET Key8478 Value8478\nSET Key8479 Value8479\nSET Key8480 Value8480\nSET Key8481 Value8481\nSET Key8482 Value8482\nSET Key8483 Value8483\nSET Key8484 Value8484\nSET Key8485 Value8485\nSET Key8486 Value8486\nSET Key8487 Value8487\nSET Key8488 Value8488\nSET Key8489 Value8489\nSET Key8490 Value8490\nSET Key8491 Value8491\nSET Key8492 Value8492\nSET Key8493 Value8493\nSET Key8494 Value8494\nSET Key8495 Value8495\nSET Key8496 Value8496\nSET Key8497 Value8497\nSET Key8498 Value8498\nSET Key8499 Value8499\nSET Key8500 Value8500\nSET Key8501 Value8501\nSET Key8502 Value8502\nSET Key8503 Value8503\nSET Key8504 Value8504\nSET Key8505 Value8505\nSET Key8506 Value8506\nSET Key8507 Value8507\nSET Key8508 Value8508\nSET Key8509 Value8509\nSET Key8510 Value8510\nSET Key8511 Value8511\nSET Key8512 Value8512\nSET Key8513 Value8513\nSET Key8514 Value8514\nSET Key8515 Value8515\nSET Key8516 Value8516\nSET Key8517 Value8517\nSET Key8518 Value8518\nSET Key8519 Value8519\nSET Key8520 Value8520\nSET Key8521 Value8521\nSET Key8522 Value8522\nSET Key8523 Value8523\nSET Key8524 Value8524\nSET Key8525 Value8525\nSET Key8526 Value8526\nSET Key8527 Value8527\nSET Key8528 Value8528\nSET Key8529 Value8529\nSET Key8530 Value8530\nSET Key8531 Value8531\nSET Key8532 Value8532\nSET Key8533 Value8533\nSET Key8534 Value8534\nSET Key8535 Value8535\nSET Key8536 Value8536\nSET Key8537 Value8537\nSET Key8538 Value8538\nSET Key8539 Value8539\nSET Key8540 Value8540\nSET Key8541 Value8541\nSET Key8542 Value8542\nSET Key8543 Value8543\nSET Key8544 Value8544\nSET Key8545 Value8545\nSET Key8546 Value8546\nSET Key8547 Value8547\nSET Key8548 Value8548\nSET Key8549 Value8549\nSET Key8550 Value8550\nSET Key8551 Value8551\nSET Key8552 Value8552\nSET Key8553 Value8553\nSET Key8554 Value8554\nSET Key8555 Value8555\nSET Key8556 Value8556\nSET Key8557 Value8557\nSET Key8558 Value8558\nSET Key8559 Value8559\nSET Key8560 Value8560\nSET Key8561 Value8561\nSET Key8562 Value8562\nSET Key8563 Value8563\nSET Key8564 Value8564\nSET Key8565 Value8565\nSET Key8566 Value8566\nSET Key8567 Value8567\nSET Key8568 Value8568\nSET Key8569 Value8569\nSET Key8570 Value8570\nSET Key8571 Value8571\nSET Key8572 Value8572\nSET Key8573 Value8573\nSET Key8574 Value8574\nSET Key8575 Value8575\nSET Key8576 Value8576\nSET Key8577 Value8577\nSET Key8578 Value8578\nSET Key8579 Value8579\nSET Key8580 Value8580\nSET Key8581 Value8581\nSET Key8582 Value8582\nSET Key8583 Value8583\nSET Key8584 Value8584\nSET Key8585 Value8585\nSET Key8586 Value8586\nSET Key8587 Value8587\nSET Key8588 Value8588\nSET Key8589 Value8589\nSET Key8590 Value8590\nSET Key8591 Value8591\nSET Key8592 Value8592\nSET Key8593 Value8593\nSET Key8594 Value8594\nSET Key8595 Value8595\nSET Key8596 Value8596\nSET Key8597 Value8597\nSET Key8598 Value8598\nSET Key8599 Value8599\nSET Key8600 Value8600\nSET Key8601 Value8601\nSET Key8602 Value8602\nSET Key8603 Value8603\nSET Key8604 Value8604\nSET Key8605 Value8605\nSET Key8606 Value8606\nSET Key8607 Value8607\nSET Key8608 Value8608\nSET Key8609 Value8609\nSET Key8610 Value8610\nSET Key8611 Value8611\nSET Key8612 Value8612\nSET Key8613 Value8613\nSET Key8614 Value8614\nSET Key8615 Value8615\nSET Key8616 Value8616\nSET Key8617 Value8617\nSET Key8618 Value8618\nSET Key8619 Value8619\nSET Key8620 Value8620\nSET Key8621 Value8621\nSET Key8622 Value8622\nSET Key8623 Value8623\nSET Key8624 Value8624\nSET Key8625 Value8625\nSET Key8626 Value8626\nSET Key8627 Value8627\nSET Key8628 Value8628\nSET Key8629 Value8629\nSET Key8630 Value8630\nSET Key8631 Value8631\nSET Key8632 Value8632\nSET Key8633 Value8633\nSET Key8634 Value8634\nSET Key8635 Value8635\nSET Key8636 Value8636\nSET Key8637 Value8637\nSET Key8638 Value8638\nSET Key8639 Value8639\nSET Key8640 Value8640\nSET Key8641 Value8641\nSET Key8642 Value8642\nSET Key8643 Value8643\nSET Key8644 Value8644\nSET Key8645 Value8645\nSET Key8646 Value8646\nSET Key8647 Value8647\nSET Key8648 Value8648\nSET Key8649 Value8649\nSET Key8650 Value8650\nSET Key8651 Value8651\nSET Key8652 Value8652\nSET Key8653 Value8653\nSET Key8654 Value8654\nSET Key8655 Value8655\nSET Key8656 Value8656\nSET Key8657 Value8657\nSET Key8658 Value8658\nSET Key8659 Value8659\nSET Key8660 Value8660\nSET Key8661 Value8661\nSET Key8662 Value8662\nSET Key8663 Value8663\nSET Key8664 Value8664\nSET Key8665 Value8665\nSET Key8666 Value8666\nSET Key8667 Value8667\nSET Key8668 Value8668\nSET Key8669 Value8669\nSET Key8670 Value8670\nSET Key8671 Value8671\nSET Key8672 Value8672\nSET Key8673 Value8673\nSET Key8674 Value8674\nSET Key8675 Value8675\nSET Key8676 Value8676\nSET Key8677 Value8677\nSET Key8678 Value8678\nSET Key8679 Value8679\nSET Key8680 Value8680\nSET Key8681 Value8681\nSET Key8682 Value8682\nSET Key8683 Value8683\nSET Key8684 Value8684\nSET Key8685 Value8685\nSET Key8686 Value8686\nSET Key8687 Value8687\nSET Key8688 Value8688\nSET Key8689 Value8689\nSET Key8690 Value8690\nSET Key8691 Value8691\nSET Key8692 Value8692\nSET Key8693 Value8693\nSET Key8694 Value8694\nSET Key8695 Value8695\nSET Key8696 Value8696\nSET Key8697 Value8697\nSET Key8698 Value8698\nSET Key8699 Value8699\nSET Key8700 Value8700\nSET Key8701 Value8701\nSET Key8702 Value8702\nSET Key8703 Value8703\nSET Key8704 Value8704\nSET Key8705 Value8705\nSET Key8706 Value8706\nSET Key8707 Value8707\nSET Key8708 Value8708\nSET Key8709 Value8709\nSET Key8710 Value8710\nSET Key8711 Value8711\nSET Key8712 Value8712\nSET Key8713 Value8713\nSET Key8714 Value8714\nSET Key8715 Value8715\nSET Key8716 Value8716\nSET Key8717 Value8717\nSET Key8718 Value8718\nSET Key8719 Value8719\nSET Key8720 Value8720\nSET Key8721 Value8721\nSET Key8722 Value8722\nSET Key8723 Value8723\nSET Key8724 Value8724\nSET Key8725 Value8725\nSET Key8726 Value8726\nSET Key8727 Value8727\nSET Key8728 Value8728\nSET Key8729 Value8729\nSET Key8730 Value8730\nSET Key8731 Value8731\nSET Key8732 Value8732\nSET Key8733 Value8733\nSET Key8734 Value8734\nSET Key8735 Value8735\nSET Key8736 Value8736\nSET Key8737 Value8737\nSET Key8738 Value8738\nSET Key8739 Value8739\nSET Key8740 Value8740\nSET Key8741 Value8741\nSET Key8742 Value8742\nSET Key8743 Value8743\nSET Key8744 Value8744\nSET Key8745 Value8745\nSET Key8746 Value8746\nSET Key8747 Value8747\nSET Key8748 Value8748\nSET Key8749 Value8749\nSET Key8750 Value8750\nSET Key8751 Value8751\nSET Key8752 Value8752\nSET Key8753 Value8753\nSET Key8754 Value8754\nSET Key8755 Value8755\nSET Key8756 Value8756\nSET Key8757 Value8757\nSET Key8758 Value8758\nSET Key8759 Value8759\nSET Key8760 Value8760\nSET Key8761 Value8761\nSET Key8762 Value8762\nSET Key8763 Value8763\nSET Key8764 Value8764\nSET Key8765 Value8765\nSET Key8766 Value8766\nSET Key8767 Value8767\nSET Key8768 Value8768\nSET Key8769 Value8769\nSET Key8770 Value8770\nSET Key8771 Value8771\nSET Key8772 Value8772\nSET Key8773 Value8773\nSET Key8774 Value8774\nSET Key8775 Value8775\nSET Key8776 Value8776\nSET Key8777 Value8777\nSET Key8778 Value8778\nSET Key8779 Value8779\nSET Key8780 Value8780\nSET Key8781 Value8781\nSET Key8782 Value8782\nSET Key8783 Value8783\nSET Key8784 Value8784\nSET Key8785 Value8785\nSET Key8786 Value8786\nSET Key8787 Value8787\nSET Key8788 Value8788\nSET Key8789 Value8789\nSET Key8790 Value8790\nSET Key8791 Value8791\nSET Key8792 Value8792\nSET Key8793 Value8793\nSET Key8794 Value8794\nSET Key8795 Value8795\nSET Key8796 Value8796\nSET Key8797 Value8797\nSET Key8798 Value8798\nSET Key8799 Value8799\nSET Key8800 Value8800\nSET Key8801 Value8801\nSET Key8802 Value8802\nSET Key8803 Value8803\nSET Key8804 Value8804\nSET Key8805 Value8805\nSET Key8806 Value8806\nSET Key8807 Value8807\nSET Key8808 Value8808\nSET Key8809 Value8809\nSET Key8810 Value8810\nSET Key8811 Value8811\nSET Key8812 Value8812\nSET Key8813 Value8813\nSET Key8814 Value8814\nSET Key8815 Value8815\nSET Key8816 Value8816\nSET Key8817 Value8817\nSET Key8818 Value8818\nSET Key8819 Value8819\nSET Key8820 Value8820\nSET Key8821 Value8821\nSET Key8822 Value8822\nSET Key8823 Value8823\nSET Key8824 Value8824\nSET Key8825 Value8825\nSET Key8826 Value8826\nSET Key8827 Value8827\nSET Key8828 Value8828\nSET Key8829 Value8829\nSET Key8830 Value8830\nSET Key8831 Value8831\nSET Key8832 Value8832\nSET Key8833 Value8833\nSET Key8834 Value8834\nSET Key8835 Value8835\nSET Key8836 Value8836\nSET Key8837 Value8837\nSET Key8838 Value8838\nSET Key8839 Value8839\nSET Key8840 Value8840\nSET Key8841 Value8841\nSET Key8842 Value8842\nSET Key8843 Value8843\nSET Key8844 Value8844\nSET Key8845 Value8845\nSET Key8846 Value8846\nSET Key8847 Value8847\nSET Key8848 Value8848\nSET Key8849 Value8849\nSET Key8850 Value8850\nSET Key8851 Value8851\nSET Key8852 Value8852\nSET Key8853 Value8853\nSET Key8854 Value8854\nSET Key8855 Value8855\nSET Key8856 Value8856\nSET Key8857 Value8857\nSET Key8858 Value8858\nSET Key8859 Value8859\nSET Key8860 Value8860\nSET Key8861 Value8861\nSET Key8862 Value8862\nSET Key8863 Value8863\nSET Key8864 Value8864\nSET Key8865 Value8865\nSET Key8866 Value8866\nSET Key8867 Value8867\nSET Key8868 Value8868\nSET Key8869 Value8869\nSET Key8870 Value8870\nSET Key8871 Value8871\nSET Key8872 Value8872\nSET Key8873 Value8873\nSET Key8874 Value8874\nSET Key8875 Value8875\nSET Key8876 Value8876\nSET Key8877 Value8877\nSET Key8878 Value8878\nSET Key8879 Value8879\nSET Key8880 Value8880\nSET Key8881 Value8881\nSET Key8882 Value8882\nSET Key8883 Value8883\nSET Key8884 Value8884\nSET Key8885 Value8885\nSET Key8886 Value8886\nSET Key8887 Value8887\nSET Key8888 Value8888\nSET Key8889 Value8889\nSET Key8890 Value8890\nSET Key8891 Value8891\nSET Key8892 Value8892\nSET Key8893 Value8893\nSET Key8894 Value8894\nSET Key8895 Value8895\nSET Key8896 Value8896\nSET Key8897 Value8897\nSET Key8898 Value8898\nSET Key8899 Value8899\nSET Key8900 Value8900\nSET Key8901 Value8901\nSET Key8902 Value8902\nSET Key8903 Value8903\nSET Key8904 Value8904\nSET Key8905 Value8905\nSET Key8906 Value8906\nSET Key8907 Value8907\nSET Key8908 Value8908\nSET Key8909 Value8909\nSET Key8910 Value8910\nSET Key8911 Value8911\nSET Key8912 Value8912\nSET Key8913 Value8913\nSET Key8914 Value8914\nSET Key8915 Value8915\nSET Key8916 Value8916\nSET Key8917 Value8917\nSET Key8918 Value8918\nSET Key8919 Value8919\nSET Key8920 Value8920\nSET Key8921 Value8921\nSET Key8922 Value8922\nSET Key8923 Value8923\nSET Key8924 Value8924\nSET Key8925 Value8925\nSET Key8926 Value8926\nSET Key8927 Value8927\nSET Key8928 Value8928\nSET Key8929 Value8929\nSET Key8930 Value8930\nSET Key8931 Value8931\nSET Key8932 Value8932\nSET Key8933 Value8933\nSET Key8934 Value8934\nSET Key8935 Value8935\nSET Key8936 Value8936\nSET Key8937 Value8937\nSET Key8938 Value8938\nSET Key8939 Value8939\nSET Key8940 Value8940\nSET Key8941 Value8941\nSET Key8942 Value8942\nSET Key8943 Value8943\nSET Key8944 Value8944\nSET Key8945 Value8945\nSET Key8946 Value8946\nSET Key8947 Value8947\nSET Key8948 Value8948\nSET Key8949 Value8949\nSET Key8950 Value8950\nSET Key8951 Value8951\nSET Key8952 Value8952\nSET Key8953 Value8953\nSET Key8954 Value8954\nSET Key8955 Value8955\nSET Key8956 Value8956\nSET Key8957 Value8957\nSET Key8958 Value8958\nSET Key8959 Value8959\nSET Key8960 Value8960\nSET Key8961 Value8961\nSET Key8962 Value8962\nSET Key8963 Value8963\nSET Key8964 Value8964\nSET Key8965 Value8965\nSET Key8966 Value8966\nSET Key8967 Value8967\nSET Key8968 Value8968\nSET Key8969 Value8969\nSET Key8970 Value8970\nSET Key8971 Value8971\nSET Key8972 Value8972\nSET Key8973 Value8973\nSET Key8974 Value8974\nSET Key8975 Value8975\nSET Key8976 Value8976\nSET Key8977 Value8977\nSET Key8978 Value8978\nSET Key8979 Value8979\nSET Key8980 Value8980\nSET Key8981 Value8981\nSET Key8982 Value8982\nSET Key8983 Value8983\nSET Key8984 Value8984\nSET Key8985 Value8985\nSET Key8986 Value8986\nSET Key8987 Value8987\nSET Key8988 Value8988\nSET Key8989 Value8989\nSET Key8990 Value8990\nSET Key8991 Value8991\nSET Key8992 Value8992\nSET Key8993 Value8993\nSET Key8994 Value8994\nSET Key8995 Value8995\nSET Key8996 Value8996\nSET Key8997 Value8997\nSET Key8998 Value8998\nSET Key8999 Value8999\nSET Key9000 Value9000\nSET Key9001 Value9001\nSET Key9002 Value9002\nSET Key9003 Value9003\nSET Key9004 Value9004\nSET Key9005 Value9005\nSET Key9006 Value9006\nSET Key9007 Value9007\nSET Key9008 Value9008\nSET Key9009 Value9009\nSET Key9010 Value9010\nSET Key9011 Value9011\nSET Key9012 Value9012\nSET Key9013 Value9013\nSET Key9014 Value9014\nSET Key9015 Value9015\nSET Key9016 Value9016\nSET Key9017 Value9017\nSET Key9018 Value9018\nSET Key9019 Value9019\nSET Key9020 Value9020\nSET Key9021 Value9021\nSET Key9022 Value9022\nSET Key9023 Value9023\nSET Key9024 Value9024\nSET Key9025 Value9025\nSET Key9026 Value9026\nSET Key9027 Value9027\nSET Key9028 Value9028\nSET Key9029 Value9029\nSET Key9030 Value9030\nSET Key9031 Value9031\nSET Key9032 Value9032\nSET Key9033 Value9033\nSET Key9034 Value9034\nSET Key9035 Value9035\nSET Key9036 Value9036\nSET Key9037 Value9037\nSET Key9038 Value9038\nSET Key9039 Value9039\nSET Key9040 Value9040\nSET Key9041 Value9041\nSET Key9042 Value9042\nSET Key9043 Value9043\nSET Key9044 Value9044\nSET Key9045 Value9045\nSET Key9046 Value9046\nSET Key9047 Value9047\nSET Key9048 Value9048\nSET Key9049 Value9049\nSET Key9050 Value9050\nSET Key9051 Value9051\nSET Key9052 Value9052\nSET Key9053 Value9053\nSET Key9054 Value9054\nSET Key9055 Value9055\nSET Key9056 Value9056\nSET Key9057 Value9057\nSET Key9058 Value9058\nSET Key9059 Value9059\nSET Key9060 Value9060\nSET Key9061 Value9061\nSET Key9062 Value9062\nSET Key9063 Value9063\nSET Key9064 Value9064\nSET Key9065 Value9065\nSET Key9066 Value9066\nSET Key9067 Value9067\nSET Key9068 Value9068\nSET Key9069 Value9069\nSET Key9070 Value9070\nSET Key9071 Value9071\nSET Key9072 Value9072\nSET Key9073 Value9073\nSET Key9074 Value9074\nSET Key9075 Value9075\nSET Key9076 Value9076\nSET Key9077 Value9077\nSET Key9078 Value9078\nSET Key9079 Value9079\nSET Key9080 Value9080\nSET Key9081 Value9081\nSET Key9082 Value9082\nSET Key9083 Value9083\nSET Key9084 Value9084\nSET Key9085 Value9085\nSET Key9086 Value9086\nSET Key9087 Value9087\nSET Key9088 Value9088\nSET Key9089 Value9089\nSET Key9090 Value9090\nSET Key9091 Value9091\nSET Key9092 Value9092\nSET Key9093 Value9093\nSET Key9094 Value9094\nSET Key9095 Value9095\nSET Key9096 Value9096\nSET Key9097 Value9097\nSET Key9098 Value9098\nSET Key9099 Value9099\nSET Key9100 Value9100\nSET Key9101 Value9101\nSET Key9102 Value9102\nSET Key9103 Value9103\nSET Key9104 Value9104\nSET Key9105 Value9105\nSET Key9106 Value9106\nSET Key9107 Value9107\nSET Key9108 Value9108\nSET Key9109 Value9109\nSET Key9110 Value9110\nSET Key9111 Value9111\nSET Key9112 Value9112\nSET Key9113 Value9113\nSET Key9114 Value9114\nSET Key9115 Value9115\nSET Key9116 Value9116\nSET Key9117 Value9117\nSET Key9118 Value9118\nSET Key9119 Value9119\nSET Key9120 Value9120\nSET Key9121 Value9121\nSET Key9122 Value9122\nSET Key9123 Value9123\nSET Key9124 Value9124\nSET Key9125 Value9125\nSET Key9126 Value9126\nSET Key9127 Value9127\nSET Key9128 Value9128\nSET Key9129 Value9129\nSET Key9130 Value9130\nSET Key9131 Value9131\nSET Key9132 Value9132\nSET Key9133 Value9133\nSET Key9134 Value9134\nSET Key9135 Value9135\nSET Key9136 Value9136\nSET Key9137 Value9137\nSET Key9138 Value9138\nSET Key9139 Value9139\nSET Key9140 Value9140\nSET Key9141 Value9141\nSET Key9142 Value9142\nSET Key9143 Value9143\nSET Key9144 Value9144\nSET Key9145 Value9145\nSET Key9146 Value9146\nSET Key9147 Value9147\nSET Key9148 Value9148\nSET Key9149 Value9149\nSET Key9150 Value9150\nSET Key9151 Value9151\nSET Key9152 Value9152\nSET Key9153 Value9153\nSET Key9154 Value9154\nSET Key9155 Value9155\nSET Key9156 Value9156\nSET Key9157 Value9157\nSET Key9158 Value9158\nSET Key9159 Value9159\nSET Key9160 Value9160\nSET Key9161 Value9161\nSET Key9162 Value9162\nSET Key9163 Value9163\nSET Key9164 Value9164\nSET Key9165 Value9165\nSET Key9166 Value9166\nSET Key9167 Value9167\nSET Key9168 Value9168\nSET Key9169 Value9169\nSET Key9170 Value9170\nSET Key9171 Value9171\nSET Key9172 Value9172\nSET Key9173 Value9173\nSET Key9174 Value9174\nSET Key9175 Value9175\nSET Key9176 Value9176\nSET Key9177 Value9177\nSET Key9178 Value9178\nSET Key9179 Value9179\nSET Key9180 Value9180\nSET Key9181 Value9181\nSET Key9182 Value9182\nSET Key9183 Value9183\nSET Key9184 Value9184\nSET Key9185 Value9185\nSET Key9186 Value9186\nSET Key9187 Value9187\nSET Key9188 Value9188\nSET Key9189 Value9189\nSET Key9190 Value9190\nSET Key9191 Value9191\nSET Key9192 Value9192\nSET Key9193 Value9193\nSET Key9194 Value9194\nSET Key9195 Value9195\nSET Key9196 Value9196\nSET Key9197 Value9197\nSET Key9198 Value9198\nSET Key9199 Value9199\nSET Key9200 Value9200\nSET Key9201 Value9201\nSET Key9202 Value9202\nSET Key9203 Value9203\nSET Key9204 Value9204\nSET Key9205 Value9205\nSET Key9206 Value9206\nSET Key9207 Value9207\nSET Key9208 Value9208\nSET Key9209 Value9209\nSET Key9210 Value9210\nSET Key9211 Value9211\nSET Key9212 Value9212\nSET Key9213 Value9213\nSET Key9214 Value9214\nSET Key9215 Value9215\nSET Key9216 Value9216\nSET Key9217 Value9217\nSET Key9218 Value9218\nSET Key9219 Value9219\nSET Key9220 Value9220\nSET Key9221 Value9221\nSET Key9222 Value9222\nSET Key9223 Value9223\nSET Key9224 Value9224\nSET Key9225 Value9225\nSET Key9226 Value9226\nSET Key9227 Value9227\nSET Key9228 Value9228\nSET Key9229 Value9229\nSET Key9230 Value9230\nSET Key9231 Value9231\nSET Key9232 Value9232\nSET Key9233 Value9233\nSET Key9234 Value9234\nSET Key9235 Value9235\nSET Key9236 Value9236\nSET Key9237 Value9237\nSET Key9238 Value9238\nSET Key9239 Value9239\nSET Key9240 Value9240\nSET Key9241 Value9241\nSET Key9242 Value9242\nSET Key9243 Value9243\nSET Key9244 Value9244\nSET Key9245 Value9245\nSET Key9246 Value9246\nSET Key9247 Value9247\nSET Key9248 Value9248\nSET Key9249 Value9249\nSET Key9250 Value9250\nSET Key9251 Value9251\nSET Key9252 Value9252\nSET Key9253 Value9253\nSET Key9254 Value9254\nSET Key9255 Value9255\nSET Key9256 Value9256\nSET Key9257 Value9257\nSET Key9258 Value9258\nSET Key9259 Value9259\nSET Key9260 Value9260\nSET Key9261 Value9261\nSET Key9262 Value9262\nSET Key9263 Value9263\nSET Key9264 Value9264\nSET Key9265 Value9265\nSET Key9266 Value9266\nSET Key9267 Value9267\nSET Key9268 Value9268\nSET Key9269 Value9269\nSET Key9270 Value9270\nSET Key9271 Value9271\nSET Key9272 Value9272\nSET Key9273 Value9273\nSET Key9274 Value9274\nSET Key9275 Value9275\nSET Key9276 Value9276\nSET Key9277 Value9277\nSET Key9278 Value9278\nSET Key9279 Value9279\nSET Key9280 Value9280\nSET Key9281 Value9281\nSET Key9282 Value9282\nSET Key9283 Value9283\nSET Key9284 Value9284\nSET Key9285 Value9285\nSET Key9286 Value9286\nSET Key9287 Value9287\nSET Key9288 Value9288\nSET Key9289 Value9289\nSET Key9290 Value9290\nSET Key9291 Value9291\nSET Key9292 Value9292\nSET Key9293 Value9293\nSET Key9294 Value9294\nSET Key9295 Value9295\nSET Key9296 Value9296\nSET Key9297 Value9297\nSET Key9298 Value9298\nSET Key9299 Value9299\nSET Key9300 Value9300\nSET Key9301 Value9301\nSET Key9302 Value9302\nSET Key9303 Value9303\nSET Key9304 Value9304\nSET Key9305 Value9305\nSET Key9306 Value9306\nSET Key9307 Value9307\nSET Key9308 Value9308\nSET Key9309 Value9309\nSET Key9310 Value9310\nSET Key9311 Value9311\nSET Key9312 Value9312\nSET Key9313 Value9313\nSET Key9314 Value9314\nSET Key9315 Value9315\nSET Key9316 Value9316\nSET Key9317 Value9317\nSET Key9318 Value9318\nSET Key9319 Value9319\nSET Key9320 Value9320\nSET Key9321 Value9321\nSET Key9322 Value9322\nSET Key9323 Value9323\nSET Key9324 Value9324\nSET Key9325 Value9325\nSET Key9326 Value9326\nSET Key9327 Value9327\nSET Key9328 Value9328\nSET Key9329 Value9329\nSET Key9330 Value9330\nSET Key9331 Value9331\nSET Key9332 Value9332\nSET Key9333 Value9333\nSET Key9334 Value9334\nSET Key9335 Value9335\nSET Key9336 Value9336\nSET Key9337 Value9337\nSET Key9338 Value9338\nSET Key9339 Value9339\nSET Key9340 Value9340\nSET Key9341 Value9341\nSET Key9342 Value9342\nSET Key9343 Value9343\nSET Key9344 Value9344\nSET Key9345 Value9345\nSET Key9346 Value9346\nSET Key9347 Value9347\nSET Key9348 Value9348\nSET Key9349 Value9349\nSET Key9350 Value9350\nSET Key9351 Value9351\nSET Key9352 Value9352\nSET Key9353 Value9353\nSET Key9354 Value9354\nSET Key9355 Value9355\nSET Key9356 Value9356\nSET Key9357 Value9357\nSET Key9358 Value9358\nSET Key9359 Value9359\nSET Key9360 Value9360\nSET Key9361 Value9361\nSET Key9362 Value9362\nSET Key9363 Value9363\nSET Key9364 Value9364\nSET Key9365 Value9365\nSET Key9366 Value9366\nSET Key9367 Value9367\nSET Key9368 Value9368\nSET Key9369 Value9369\nSET Key9370 Value9370\nSET Key9371 Value9371\nSET Key9372 Value9372\nSET Key9373 Value9373\nSET Key9374 Value9374\nSET Key9375 Value9375\nSET Key9376 Value9376\nSET Key9377 Value9377\nSET Key9378 Value9378\nSET Key9379 Value9379\nSET Key9380 Value9380\nSET Key9381 Value9381\nSET Key9382 Value9382\nSET Key9383 Value9383\nSET Key9384 Value9384\nSET Key9385 Value9385\nSET Key9386 Value9386\nSET Key9387 Value9387\nSET Key9388 Value9388\nSET Key9389 Value9389\nSET Key9390 Value9390\nSET Key9391 Value9391\nSET Key9392 Value9392\nSET Key9393 Value9393\nSET Key9394 Value9394\nSET Key9395 Value9395\nSET Key9396 Value9396\nSET Key9397 Value9397\nSET Key9398 Value9398\nSET Key9399 Value9399\nSET Key9400 Value9400\nSET Key9401 Value9401\nSET Key9402 Value9402\nSET Key9403 Value9403\nSET Key9404 Value9404\nSET Key9405 Value9405\nSET Key9406 Value9406\nSET Key9407 Value9407\nSET Key9408 Value9408\nSET Key9409 Value9409\nSET Key9410 Value9410\nSET Key9411 Value9411\nSET Key9412 Value9412\nSET Key9413 Value9413\nSET Key9414 Value9414\nSET Key9415 Value9415\nSET Key9416 Value9416\nSET Key9417 Value9417\nSET Key9418 Value9418\nSET Key9419 Value9419\nSET Key9420 Value9420\nSET Key9421 Value9421\nSET Key9422 Value9422\nSET Key9423 Value9423\nSET Key9424 Value9424\nSET Key9425 Value9425\nSET Key9426 Value9426\nSET Key9427 Value9427\nSET Key9428 Value9428\nSET Key9429 Value9429\nSET Key9430 Value9430\nSET Key9431 Value9431\nSET Key9432 Value9432\nSET Key9433 Value9433\nSET Key9434 Value9434\nSET Key9435 Value9435\nSET Key9436 Value9436\nSET Key9437 Value9437\nSET Key9438 Value9438\nSET Key9439 Value9439\nSET Key9440 Value9440\nSET Key9441 Value9441\nSET Key9442 Value9442\nSET Key9443 Value9443\nSET Key9444 Value9444\nSET Key9445 Value9445\nSET Key9446 Value9446\nSET Key9447 Value9447\nSET Key9448 Value9448\nSET Key9449 Value9449\nSET Key9450 Value9450\nSET Key9451 Value9451\nSET Key9452 Value9452\nSET Key9453 Value9453\nSET Key9454 Value9454\nSET Key9455 Value9455\nSET Key9456 Value9456\nSET Key9457 Value9457\nSET Key9458 Value9458\nSET Key9459 Value9459\nSET Key9460 Value9460\nSET Key9461 Value9461\nSET Key9462 Value9462\nSET Key9463 Value9463\nSET Key9464 Value9464\nSET Key9465 Value9465\nSET Key9466 Value9466\nSET Key9467 Value9467\nSET Key9468 Value9468\nSET Key9469 Value9469\nSET Key9470 Value9470\nSET Key9471 Value9471\nSET Key9472 Value9472\nSET Key9473 Value9473\nSET Key9474 Value9474\nSET Key9475 Value9475\nSET Key9476 Value9476\nSET Key9477 Value9477\nSET Key9478 Value9478\nSET Key9479 Value9479\nSET Key9480 Value9480\nSET Key9481 Value9481\nSET Key9482 Value9482\nSET Key9483 Value9483\nSET Key9484 Value9484\nSET Key9485 Value9485\nSET Key9486 Value9486\nSET Key9487 Value9487\nSET Key9488 Value9488\nSET Key9489 Value9489\nSET Key9490 Value9490\nSET Key9491 Value9491\nSET Key9492 Value9492\nSET Key9493 Value9493\nSET Key9494 Value9494\nSET Key9495 Value9495\nSET Key9496 Value9496\nSET Key9497 Value9497\nSET Key9498 Value9498\nSET Key9499 Value9499\nSET Key9500 Value9500\nSET Key9501 Value9501\nSET Key9502 Value9502\nSET Key9503 Value9503\nSET Key9504 Value9504\nSET Key9505 Value9505\nSET Key9506 Value9506\nSET Key9507 Value9507\nSET Key9508 Value9508\nSET Key9509 Value9509\nSET Key9510 Value9510\nSET Key9511 Value9511\nSET Key9512 Value9512\nSET Key9513 Value9513\nSET Key9514 Value9514\nSET Key9515 Value9515\nSET Key9516 Value9516\nSET Key9517 Value9517\nSET Key9518 Value9518\nSET Key9519 Value9519\nSET Key9520 Value9520\nSET Key9521 Value9521\nSET Key9522 Value9522\nSET Key9523 Value9523\nSET Key9524 Value9524\nSET Key9525 Value9525\nSET Key9526 Value9526\nSET Key9527 Value9527\nSET Key9528 Value9528\nSET Key9529 Value9529\nSET Key9530 Value9530\nSET Key9531 Value9531\nSET Key9532 Value9532\nSET Key9533 Value9533\nSET Key9534 Value9534\nSET Key9535 Value9535\nSET Key9536 Value9536\nSET Key9537 Value9537\nSET Key9538 Value9538\nSET Key9539 Value9539\nSET Key9540 Value9540\nSET Key9541 Value9541\nSET Key9542 Value9542\nSET Key9543 Value9543\nSET Key9544 Value9544\nSET Key9545 Value9545\nSET Key9546 Value9546\nSET Key9547 Value9547\nSET Key9548 Value9548\nSET Key9549 Value9549\nSET Key9550 Value9550\nSET Key9551 Value9551\nSET Key9552 Value9552\nSET Key9553 Value9553\nSET Key9554 Value9554\nSET Key9555 Value9555\nSET Key9556 Value9556\nSET Key9557 Value9557\nSET Key9558 Value9558\nSET Key9559 Value9559\nSET Key9560 Value9560\nSET Key9561 Value9561\nSET Key9562 Value9562\nSET Key9563 Value9563\nSET Key9564 Value9564\nSET Key9565 Value9565\nSET Key9566 Value9566\nSET Key9567 Value9567\nSET Key9568 Value9568\nSET Key9569 Value9569\nSET Key9570 Value9570\nSET Key9571 Value9571\nSET Key9572 Value9572\nSET Key9573 Value9573\nSET Key9574 Value9574\nSET Key9575 Value9575\nSET Key9576 Value9576\nSET Key9577 Value9577\nSET Key9578 Value9578\nSET Key9579 Value9579\nSET Key9580 Value9580\nSET Key9581 Value9581\nSET Key9582 Value9582\nSET Key9583 Value9583\nSET Key9584 Value9584\nSET Key9585 Value9585\nSET Key9586 Value9586\nSET Key9587 Value9587\nSET Key9588 Value9588\nSET Key9589 Value9589\nSET Key9590 Value9590\nSET Key9591 Value9591\nSET Key9592 Value9592\nSET Key9593 Value9593\nSET Key9594 Value9594\nSET Key9595 Value9595\nSET Key9596 Value9596\nSET Key9597 Value9597\nSET Key9598 Value9598\nSET Key9599 Value9599\nSET Key9600 Value9600\nSET Key9601 Value9601\nSET Key9602 Value9602\nSET Key9603 Value9603\nSET Key9604 Value9604\nSET Key9605 Value9605\nSET Key9606 Value9606\nSET Key9607 Value9607\nSET Key9608 Value9608\nSET Key9609 Value9609\nSET Key9610 Value9610\nSET Key9611 Value9611\nSET Key9612 Value9612\nSET Key9613 Value9613\nSET Key9614 Value9614\nSET Key9615 Value9615\nSET Key9616 Value9616\nSET Key9617 Value9617\nSET Key9618 Value9618\nSET Key9619 Value9619\nSET Key9620 Value9620\nSET Key9621 Value9621\nSET Key9622 Value9622\nSET Key9623 Value9623\nSET Key9624 Value9624\nSET Key9625 Value9625\nSET Key9626 Value9626\nSET Key9627 Value9627\nSET Key9628 Value9628\nSET Key9629 Value9629\nSET Key9630 Value9630\nSET Key9631 Value9631\nSET Key9632 Value9632\nSET Key9633 Value9633\nSET Key9634 Value9634\nSET Key9635 Value9635\nSET Key9636 Value9636\nSET Key9637 Value9637\nSET Key9638 Value9638\nSET Key9639 Value9639\nSET Key9640 Value9640\nSET Key9641 Value9641\nSET Key9642 Value9642\nSET Key9643 Value9643\nSET Key9644 Value9644\nSET Key9645 Value9645\nSET Key9646 Value9646\nSET Key9647 Value9647\nSET Key9648 Value9648\nSET Key9649 Value9649\nSET Key9650 Value9650\nSET Key9651 Value9651\nSET Key9652 Value9652\nSET Key9653 Value9653\nSET Key9654 Value9654\nSET Key9655 Value9655\nSET Key9656 Value9656\nSET Key9657 Value9657\nSET Key9658 Value9658\nSET Key9659 Value9659\nSET Key9660 Value9660\nSET Key9661 Value9661\nSET Key9662 Value9662\nSET Key9663 Value9663\nSET Key9664 Value9664\nSET Key9665 Value9665\nSET Key9666 Value9666\nSET Key9667 Value9667\nSET Key9668 Value9668\nSET Key9669 Value9669\nSET Key9670 Value9670\nSET Key9671 Value9671\nSET Key9672 Value9672\nSET Key9673 Value9673\nSET Key9674 Value9674\nSET Key9675 Value9675\nSET Key9676 Value9676\nSET Key9677 Value9677\nSET Key9678 Value9678\nSET Key9679 Value9679\nSET Key9680 Value9680\nSET Key9681 Value9681\nSET Key9682 Value9682\nSET Key9683 Value9683\nSET Key9684 Value9684\nSET Key9685 Value9685\nSET Key9686 Value9686\nSET Key9687 Value9687\nSET Key9688 Value9688\nSET Key9689 Value9689\nSET Key9690 Value9690\nSET Key9691 Value9691\nSET Key9692 Value9692\nSET Key9693 Value9693\nSET Key9694 Value9694\nSET Key9695 Value9695\nSET Key9696 Value9696\nSET Key9697 Value9697\nSET Key9698 Value9698\nSET Key9699 Value9699\nSET Key9700 Value9700\nSET Key9701 Value9701\nSET Key9702 Value9702\nSET Key9703 Value9703\nSET Key9704 Value9704\nSET Key9705 Value9705\nSET Key9706 Value9706\nSET Key9707 Value9707\nSET Key9708 Value9708\nSET Key9709 Value9709\nSET Key9710 Value9710\nSET Key9711 Value9711\nSET Key9712 Value9712\nSET Key9713 Value9713\nSET Key9714 Value9714\nSET Key9715 Value9715\nSET Key9716 Value9716\nSET Key9717 Value9717\nSET Key9718 Value9718\nSET Key9719 Value9719\nSET Key9720 Value9720\nSET Key9721 Value9721\nSET Key9722 Value9722\nSET Key9723 Value9723\nSET Key9724 Value9724\nSET Key9725 Value9725\nSET Key9726 Value9726\nSET Key9727 Value9727\nSET Key9728 Value9728\nSET Key9729 Value9729\nSET Key9730 Value9730\nSET Key9731 Value9731\nSET Key9732 Value9732\nSET Key9733 Value9733\nSET Key9734 Value9734\nSET Key9735 Value9735\nSET Key9736 Value9736\nSET Key9737 Value9737\nSET Key9738 Value9738\nSET Key9739 Value9739\nSET Key9740 Value9740\nSET Key9741 Value9741\nSET Key9742 Value9742\nSET Key9743 Value9743\nSET Key9744 Value9744\nSET Key9745 Value9745\nSET Key9746 Value9746\nSET Key9747 Value9747\nSET Key9748 Value9748\nSET Key9749 Value9749\nSET Key9750 Value9750\nSET Key9751 Value9751\nSET Key9752 Value9752\nSET Key9753 Value9753\nSET Key9754 Value9754\nSET Key9755 Value9755\nSET Key9756 Value9756\nSET Key9757 Value9757\nSET Key9758 Value9758\nSET Key9759 Value9759\nSET Key9760 Value9760\nSET Key9761 Value9761\nSET Key9762 Value9762\nSET Key9763 Value9763\nSET Key9764 Value9764\nSET Key9765 Value9765\nSET Key9766 Value9766\nSET Key9767 Value9767\nSET Key9768 Value9768\nSET Key9769 Value9769\nSET Key9770 Value9770\nSET Key9771 Value9771\nSET Key9772 Value9772\nSET Key9773 Value9773\nSET Key9774 Value9774\nSET Key9775 Value9775\nSET Key9776 Value9776\nSET Key9777 Value9777\nSET Key9778 Value9778\nSET Key9779 Value9779\nSET Key9780 Value9780\nSET Key9781 Value9781\nSET Key9782 Value9782\nSET Key9783 Value9783\nSET Key9784 Value9784\nSET Key9785 Value9785\nSET Key9786 Value9786\nSET Key9787 Value9787\nSET Key9788 Value9788\nSET Key9789 Value9789\nSET Key9790 Value9790\nSET Key9791 Value9791\nSET Key9792 Value9792\nSET Key9793 Value9793\nSET Key9794 Value9794\nSET Key9795 Value9795\nSET Key9796 Value9796\nSET Key9797 Value9797\nSET Key9798 Value9798\nSET Key9799 Value9799\nSET Key9800 Value9800\nSET Key9801 Value9801\nSET Key9802 Value9802\nSET Key9803 Value9803\nSET Key9804 Value9804\nSET Key9805 Value9805\nSET Key9806 Value9806\nSET Key9807 Value9807\nSET Key9808 Value9808\nSET Key9809 Value9809\nSET Key9810 Value9810\nSET Key9811 Value9811\nSET Key9812 Value9812\nSET Key9813 Value9813\nSET Key9814 Value9814\nSET Key9815 Value9815\nSET Key9816 Value9816\nSET Key9817 Value9817\nSET Key9818 Value9818\nSET Key9819 Value9819\nSET Key9820 Value9820\nSET Key9821 Value9821\nSET Key9822 Value9822\nSET Key9823 Value9823\nSET Key9824 Value9824\nSET Key9825 Value9825\nSET Key9826 Value9826\nSET Key9827 Value9827\nSET Key9828 Value9828\nSET Key9829 Value9829\nSET Key9830 Value9830\nSET Key9831 Value9831\nSET Key9832 Value9832\nSET Key9833 Value9833\nSET Key9834 Value9834\nSET Key9835 Value9835\nSET Key9836 Value9836\nSET Key9837 Value9837\nSET Key9838 Value9838\nSET Key9839 Value9839\nSET Key9840 Value9840\nSET Key9841 Value9841\nSET Key9842 Value9842\nSET Key9843 Value9843\nSET Key9844 Value9844\nSET Key9845 Value9845\nSET Key9846 Value9846\nSET Key9847 Value9847\nSET Key9848 Value9848\nSET Key9849 Value9849\nSET Key9850 Value9850\nSET Key9851 Value9851\nSET Key9852 Value9852\nSET Key9853 Value9853\nSET Key9854 Value9854\nSET Key9855 Value9855\nSET Key9856 Value9856\nSET Key9857 Value9857\nSET Key9858 Value9858\nSET Key9859 Value9859\nSET Key9860 Value9860\nSET Key9861 Value9861\nSET Key9862 Value9862\nSET Key9863 Value9863\nSET Key9864 Value9864\nSET Key9865 Value9865\nSET Key9866 Value9866\nSET Key9867 Value9867\nSET Key9868 Value9868\nSET Key9869 Value9869\nSET Key9870 Value9870\nSET Key9871 Value9871\nSET Key9872 Value9872\nSET Key9873 Value9873\nSET Key9874 Value9874\nSET Key9875 Value9875\nSET Key9876 Value9876\nSET Key9877 Value9877\nSET Key9878 Value9878\nSET Key9879 Value9879\nSET Key9880 Value9880\nSET Key9881 Value9881\nSET Key9882 Value9882\nSET Key9883 Value9883\nSET Key9884 Value9884\nSET Key9885 Value9885\nSET Key9886 Value9886\nSET Key9887 Value9887\nSET Key9888 Value9888\nSET Key9889 Value9889\nSET Key9890 Value9890\nSET Key9891 Value9891\nSET Key9892 Value9892\nSET Key9893 Value9893\nSET Key9894 Value9894\nSET Key9895 Value9895\nSET Key9896 Value9896\nSET Key9897 Value9897\nSET Key9898 Value9898\nSET Key9899 Value9899\nSET Key9900 Value9900\nSET Key9901 Value9901\nSET Key9902 Value9902\nSET Key9903 Value9903\nSET Key9904 Value9904\nSET Key9905 Value9905\nSET Key9906 Value9906\nSET Key9907 Value9907\nSET Key9908 Value9908\nSET Key9909 Value9909\nSET Key9910 Value9910\nSET Key9911 Value9911\nSET Key9912 Value9912\nSET Key9913 Value9913\nSET Key9914 Value9914\nSET Key9915 Value9915\nSET Key9916 Value9916\nSET Key9917 Value9917\nSET Key9918 Value9918\nSET Key9919 Value9919\nSET Key9920 Value9920\nSET Key9921 Value9921\nSET Key9922 Value9922\nSET Key9923 Value9923\nSET Key9924 Value9924\nSET Key9925 Value9925\nSET Key9926 Value9926\nSET Key9927 Value9927\nSET Key9928 Value9928\nSET Key9929 Value9929\nSET Key9930 Value9930\nSET Key9931 Value9931\nSET Key9932 Value9932\nSET Key9933 Value9933\nSET Key9934 Value9934\nSET Key9935 Value9935\nSET Key9936 Value9936\nSET Key9937 Value9937\nSET Key9938 Value9938\nSET Key9939 Value9939\nSET Key9940 Value9940\nSET Key9941 Value9941\nSET Key9942 Value9942\nSET Key9943 Value9943\nSET Key9944 Value9944\nSET Key9945 Value9945\nSET Key9946 Value9946\nSET Key9947 Value9947\nSET Key9948 Value9948\nSET Key9949 Value9949\nSET Key9950 Value9950\nSET Key9951 Value9951\nSET Key9952 Value9952\nSET Key9953 Value9953\nSET Key9954 Value9954\nSET Key9955 Value9955\nSET Key9956 Value9956\nSET Key9957 Value9957\nSET Key9958 Value9958\nSET Key9959 Value9959\nSET Key9960 Value9960\nSET Key9961 Value9961\nSET Key9962 Value9962\nSET Key9963 Value9963\nSET Key9964 Value9964\nSET Key9965 Value9965\nSET Key9966 Value9966\nSET Key9967 Value9967\nSET Key9968 Value9968\nSET Key9969 Value9969\nSET Key9970 Value9970\nSET Key9971 Value9971\nSET Key9972 Value9972\nSET Key9973 Value9973\nSET Key9974 Value9974\nSET Key9975 Value9975\nSET Key9976 Value9976\nSET Key9977 Value9977\nSET Key9978 Value9978\nSET Key9979 Value9979\nSET Key9980 Value9980\nSET Key9981 Value9981\nSET Key9982 Value9982\nSET Key9983 Value9983\nSET Key9984 Value9984\nSET Key9985 Value9985\nSET Key9986 Value9986\nSET Key9987 Value9987\nSET Key9988 Value9988\nSET Key9989 Value9989\nSET Key9990 Value9990\nSET Key9991 Value9991\nSET Key9992 Value9992\nSET Key9993 Value9993\nSET Key9994 Value9994\nSET Key9995 Value9995\nSET Key9996 Value9996\nSET Key9997 Value9997\nSET Key9998 Value9998\nSET Key9999 Value9999"
  },
  {
    "path": "tests/e2e/test-data/bulk-upload/bulkUplAllKeyTypes.txt",
    "content": "HSET hashkey1 'field' 'value'\nLPUSH listkey1 'element'\nSADD setkey1 'member'\nZADD zsetkey1 1 'member'\nSET stringkey1 'value'\nJSON.SET jsonkey1 . '1'\nXADD streamkey1 * 'field' 'value'\nGRAPH.QUERY graphkey1 \"CREATE ()\"\nTS.CREATE tskey1"
  },
  {
    "path": "tests/e2e/test-data/certs/ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedCACertificate\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/certsByPath/caPath.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedCACertificatePath\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/certsByPath/caSameBody.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedCACertificatePath\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/certsByPath/clientPath.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedClientCrtPath\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/certsByPath/clientPath.key",
    "content": "-----BEGIN PRIVATE KEY-----\nmockedPrivateKeyPath\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/certsByPath/clientSameBody.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedClientCrtPath\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/certsByPath/clientSameBody.key",
    "content": "-----BEGIN PRIVATE KEY-----\nmockedPrivateKeyPath\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/client.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedClientCrt\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/client.key",
    "content": "-----BEGIN PRIVATE KEY-----\nmockedPrivateKey\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/sameNameCerts/caPath.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedCACertificatePath1\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/sameNameCerts/clientPath.crt",
    "content": "-----BEGIN CERTIFICATE-----\nmockedClientCrtPath1\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/e2e/test-data/certs/sameNameCerts/clientPath.key",
    "content": "-----BEGIN PRIVATE KEY-----\nmockedPrivateKeyPath1\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/test-data/decompressors/awesome.proto",
    "content": "syntax = \"proto3\";\n\npackage com.book;\n\nmessage Book {\n    int64 isbn = 1;\n    string title = 2;\n    string author = 3;\n}\n\n\nmessage GetBookViaAuthor {\n    string author = 1;\n}\n\nservice BookService {\n    rpc GetBook (GetBookRequest) returns (Book) {}\n    rpc GetBooksViaAuthor (GetBookViaAuthor) returns (stream Book) {}\n    rpc GetGreatestBook (stream GetBookRequest) returns (Book) {}\n    rpc GetBooks (stream GetBookRequest) returns (stream Book) {}\n}\n\nmessage BookStore {\n    string name = 1;\n    map<int64, string> books = 2;\n}\n"
  },
  {
    "path": "tests/e2e/test-data/decompressors/pickleFile1.pickle",
    "content": "(lp0\nS'Привет 漢字'\np1\naI23123\naS'\\xae\\xae'\np2\na."
  },
  {
    "path": "tests/e2e/test-data/decompressors/vector.json",
    "content": "[\n  8,\n  61,\n  76,\n  188,\n  93,\n  150,\n  56,\n  188,\n  206,\n  238,\n  71,\n  60,\n  76,\n  184,\n  215,\n  188,\n  60,\n  42,\n  181,\n  187,\n  187,\n  82,\n  208,\n  60,\n  208,\n  184,\n  35,\n  60,\n  247,\n  94,\n  235,\n  188,\n  2,\n  247,\n  66,\n  188,\n  175,\n  91,\n  237,\n  188,\n  171,\n  101,\n  162,\n  60,\n  2,\n  247,\n  194,\n  58,\n  172,\n  169,\n  27,\n  188,\n  124,\n  56,\n  96,\n  187,\n  242,\n  104,\n  32,\n  187,\n  97,\n  152,\n  72,\n  188,\n  20,\n  201,\n  222,\n  60,\n  249,\n  40,\n  199,\n  188,\n  180,\n  185,\n  128,\n  60,\n  108,\n  48,\n  160,\n  188,\n  42,\n  249,\n  141,\n  60,\n  124,\n  41,\n  147,\n  59,\n  211,\n  91,\n  40,\n  188,\n  9,\n  22,\n  117,\n  60,\n  125,\n  163,\n  176,\n  60,\n  9,\n  156,\n  215,\n  60,\n  97,\n  152,\n  200,\n  60,\n  145,\n  24,\n  81,\n  188,\n  131,\n  84,\n  10,\n  60,\n  26,\n  244,\n  213,\n  187,\n  70,\n  141,\n  96,\n  59,\n  101,\n  154,\n  88,\n  60,\n  28,\n  41,\n  130,\n  187,\n  170,\n  128,\n  52,\n  188,\n  39,\n  86,\n  9,\n  189,\n  129,\n  16,\n  17,\n  189,\n  193,\n  30,\n  188,\n  187,\n  41,\n  154,\n  2,\n  189,\n  143,\n  197,\n  10,\n  60,\n  143,\n  224,\n  28,\n  188,\n  177,\n  10,\n  183,\n  60,\n  101,\n  154,\n  88,\n  60,\n  1,\n  152,\n  55,\n  186,\n  223,\n  204,\n  40,\n  188,\n  213,\n  25,\n  63,\n  187,\n  109,\n  21,\n  142,\n  60,\n  203,\n  209,\n  37,\n  60,\n  168,\n  167,\n  139,\n  188,\n  212,\n  64,\n  150,\n  188,\n  247,\n  228,\n  205,\n  60,\n  211,\n  225,\n  138,\n  60,\n  152,\n  67,\n  200,\n  59,\n  139,\n  222,\n  140,\n  188,\n  57,\n  13,\n  19,\n  189,\n  138,\n  115,\n  188,\n  59,\n  68,\n  180,\n  55,\n  188,\n  168,\n  167,\n  139,\n  60,\n  236,\n  61,\n  169,\n  59,\n  13,\n  158,\n  231,\n  59,\n  231,\n  208,\n  200,\n  187,\n  56,\n  55,\n  242,\n  59,\n  113,\n  157,\n  0,\n  187,\n  125,\n  163,\n  176,\n  188,\n  255,\n  95,\n  131,\n  60,\n  247,\n  79,\n  158,\n  59,\n  106,\n  7,\n  185,\n  58,\n  150,\n  121,\n  236,\n  187,\n  16,\n  184,\n  129,\n  187,\n  169,\n  48,\n  118,\n  185,\n  7,\n  88,\n  222,\n  60,\n  19,\n  228,\n  112,\n  60,\n  124,\n  148,\n  60,\n  136,\n  59,\n  8,\n  60,\n  3,\n  220,\n  48,\n  60,\n  122,\n  244,\n  230,\n  58,\n  194,\n  3,\n  170,\n  60,\n  122,\n  122,\n  201,\n  60,\n  77,\n  35,\n  40,\n  59,\n  25,\n  161,\n  143,\n  60,\n  3,\n  235,\n  253,\n  188,\n  201,\n  19,\n  143,\n  60,\n  30,\n  124,\n  200,\n  187,\n  9,\n  7,\n  40,\n  60,\n  221,\n  29,\n  223,\n  188,\n  148,\n  65,\n  184,\n  60,\n  171,\n  211,\n  250,\n  188,\n  157,\n  164,\n  227,\n  59,\n  59,\n  96,\n  89,\n  60,\n  242,\n  131,\n  178,\n  60,\n  240,\n  63,\n  57,\n  188,\n  123,\n  217,\n  84,\n  58,\n  60,\n  176,\n  23,\n  188,\n  172,\n  169,\n  155,\n  60,\n  241,\n  9,\n  149,\n  187,\n  126,\n  124,\n  217,\n  59,\n  66,\n  124,\n  131,\n  188,\n  108,\n  48,\n  160,\n  186,\n  149,\n  148,\n  254,\n  59,\n  170,\n  235,\n  4,\n  189,\n  182,\n  134,\n  228,\n  59,\n  226,\n  111,\n  173,\n  60,\n  96,\n  57,\n  189,\n  59,\n  11,\n  224,\n  80,\n  188,\n  195,\n  71,\n  35,\n  186,\n  19,\n  91,\n  6,\n  189,\n  158,\n  110,\n  63,\n  59,\n  118,\n  254,\n  155,\n  188,\n  131,\n  221,\n  244,\n  187,\n  197,\n  166,\n  174,\n  60,\n  58,\n  135,\n  176,\n  60,\n  103,\n  100,\n  52,\n  189,\n  218,\n  244,\n  247,\n  188,\n  56,\n  55,\n  114,\n  59,\n  212,\n  186,\n  179,\n  187,\n  36,\n  179,\n  132,\n  188,\n  201,\n  19,\n  59,\n  245,\n  17,\n  145,\n  42,\n  189,\n  107,\n  224,\n  225,\n  187,\n  136,\n  59,\n  136,\n  184,\n  207,\n  211,\n  181,\n  188,\n  63,\n  98,\n  105,\n  186,\n  75,\n  223,\n  174,\n  187,\n  213,\n  159,\n  161,\n  188,\n  235,\n  222,\n  157,\n  60,\n  165,\n  31,\n  25,\n  60,\n  254,\n  122,\n  21,\n  61,\n  177,\n  37,\n  73,\n  188,\n  15,\n  104,\n  67,\n  59,\n  233,\n  20,\n  66,\n  189,\n  3,\n  86,\n  78,\n  59,\n  166,\n  126,\n  164,\n  60,\n  147,\n  92,\n  74,\n  188,\n  195,\n  71,\n  163,\n  59,\n  138,\n  8,\n  236,\n  60,\n  166,\n  248,\n  193,\n  187,\n  96,\n  191,\n  159,\n  188,\n  71,\n  221,\n  30,\n  188,\n  58,\n  135,\n  48,\n  190,\n  216,\n  188,\n  195,\n  60,\n  184,\n  175,\n  203,\n  59,\n  78,\n  237,\n  131,\n  188,\n  240,\n  63,\n  57,\n  60,\n  33,\n  138,\n  157,\n  59,\n  171,\n  89,\n  93,\n  60,\n  207,\n  211,\n  181,\n  60,\n  202,\n  7,\n  202,\n  188,\n  185,\n  41,\n  105,\n  188,\n  240,\n  63,\n  185,\n  186,\n  206,\n  9,\n  218,\n  59,\n  75,\n  211,\n  233,\n  188,\n  166,\n  141,\n  241,\n  187,\n  134,\n  247,\n  142,\n  60,\n  48,\n  51,\n  33,\n  188,\n  4,\n  208,\n  107,\n  188,\n  104,\n  46,\n  144,\n  186,\n  29,\n  151,\n  90,\n  60,\n  91,\n  216,\n  33,\n  188,\n  209,\n  145,\n  76,\n  188,\n  221,\n  29,\n  223,\n  60,\n  187,\n  235,\n  195,\n  139,\n  187,\n  162,\n  124,\n  20,\n  189,\n  169,\n  33,\n  41,\n  60,\n  66,\n  124,\n  131,\n  188,\n  241,\n  158,\n  68,\n  188,\n  169,\n  21,\n  100,\n  188,\n  137,\n  35,\n  126,\n  187,\n  151,\n  228,\n  188,\n  60,\n  211,\n  91,\n  40,\n  187,\n  115,\n  213,\n  52,\n  187,\n  71,\n  87,\n  188,\n  59,\n  235,\n  237,\n  234,\n  188,\n  43,\n  210,\n  54,\n  60,\n  96,\n  57,\n  61,\n  60,\n  143,\n  90,\n  186,\n  188,\n  231,\n  86,\n  171,\n  188,\n  202,\n  7,\n  74,\n  60,\n  131,\n  84,\n  138,\n  59,\n  96,\n  191,\n  31,\n  187,\n  204,\n  197,\n  96,\n  60,\n  125,\n  178,\n  125,\n  187,\n  189,\n  162,\n  14,\n  61,\n  122,\n  244,\n  230,\n  59,\n  169,\n  21,\n  228,\n  59,\n  27,\n  217,\n  195,\n  188,\n  122,\n  244,\n  230,\n  188,\n  40,\n  62,\n  255,\n  187,\n  70,\n  99,\n  1,\n  60,\n  81,\n  10,\n  166,\n  188,\n  197,\n  32,\n  76,\n  189,\n  24,\n  66,\n  132,\n  187,\n  66,\n  124,\n  3,\n  188,\n  1,\n  30,\n  26,\n  188,\n  34,\n  233,\n  40,\n  188,\n  217,\n  27,\n  207,\n  60,\n  64,\n  44,\n  69,\n  188,\n  226,\n  245,\n  143,\n  61,\n  7,\n  73,\n  17,\n  61,\n  36,\n  72,\n  52,\n  187,\n  95,\n  96,\n  20,\n  59,\n  251,\n  81,\n  174,\n  59,\n  74,\n  116,\n  222,\n  59,\n  97,\n  18,\n  230,\n  59,\n  228,\n  179,\n  166,\n  188,\n  102,\n  5,\n  169,\n  188,\n  64,\n  44,\n  197,\n  59,\n  60,\n  42,\n  181,\n  60,\n  180,\n  51,\n  158,\n  188,\n  247,\n  228,\n  205,\n  60,\n  231,\n  220,\n  141,\n  188,\n  192,\n  191,\n  176,\n  188,\n  158,\n  110,\n  191,\n  188,\n  118,\n  227,\n  9,\n  188,\n  161,\n  44,\n  214,\n  60,\n  239,\n  90,\n  75,\n  188,\n  31,\n  85,\n  113,\n  188,\n  73,\n  6,\n  6,\n  60,\n  143,\n  197,\n  138,\n  59,\n  122,\n  15,\n  121,\n  60,\n  25,\n  161,\n  15,\n  188,\n  80,\n  198,\n  44,\n  188,\n  199,\n  73,\n  179,\n  187,\n  250,\n  120,\n  133,\n  186,\n  132,\n  179,\n  21,\n  188,\n  24,\n  176,\n  220,\n  187,\n  106,\n  236,\n  38,\n  189,\n  29,\n  136,\n  141,\n  60,\n  149,\n  148,\n  126,\n  188,\n  91,\n  216,\n  161,\n  60,\n  78,\n  130,\n  179,\n  188,\n  35,\n  221,\n  227,\n  60,\n  171,\n  101,\n  34,\n  187,\n  85,\n  12,\n  182,\n  59,\n  103,\n  73,\n  34,\n  59,\n  199,\n  100,\n  69,\n  60,\n  17,\n  23,\n  141,\n  60,\n  171,\n  101,\n  34,\n  189,\n  3,\n  86,\n  206,\n  187,\n  20,\n  186,\n  17,\n  189,\n  112,\n  65,\n  253,\n  188,\n  246,\n  240,\n  18,\n  61,\n  246,\n  121,\n  253,\n  60,\n  225,\n  165,\n  209,\n  188,\n  49,\n  24,\n  192,\n  188,\n  170,\n  250,\n  81,\n  59\n]"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-analytics-filter-off.json",
    "content": "{\n  \"version\": 9,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"perc\": [\n        [\n          44,\n          50\n        ]\n      ],\n      \"flag\": true,\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": false,\n          \"cond\": \"eq\"\n        },\n        {\n          \"or\": [\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"DOCKER_ON_PREMISE\",\n              \"cond\": \"eq\"\n            },\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"ELECTRON\",\n              \"cond\": \"eq\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-build-type-filter.json",
    "content": "{\n  \"version\": 15,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"perc\": [\n        [\n          44,\n          50\n        ]\n      ],\n      \"flag\": true,\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": false,\n          \"cond\": \"eq\"\n        },\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"REDIS_STACK\",\n          \"cond\": \"eq\"\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-default-remote.json",
    "content": "{\n  \"version\": 1.9,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"flag\": true,\n      \"perc\": [\n        [\n          0,\n          20\n        ]\n      ],\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": true,\n          \"cond\": \"eq\"\n        },\n        {\n          \"or\": [\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"DOCKER_ON_PREMISE\",\n              \"cond\": \"eq\"\n            },\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"ELECTRON\",\n              \"cond\": \"eq\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-docker-build.json",
    "content": "{\n  \"version\": 11,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"perc\": [\n        [\n          44,\n          50\n        ]\n      ],\n      \"flag\": true,\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"DOCKER_ON_PREMISE\",\n          \"cond\": \"eq\"\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-electron-build.json",
    "content": "{\n  \"version\": 20,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"perc\": [\n        [\n          44,\n          50\n        ]\n      ],\n      \"flag\": true,\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-flag-off.json",
    "content": "{\n  \"version\": 17,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"perc\": [\n        [\n          44,\n          50\n        ]\n      ],\n      \"flag\": false,\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": false,\n          \"cond\": \"eq\"\n        },\n        {\n          \"or\": [\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"DOCKER_ON_PREMISE\",\n              \"cond\": \"eq\"\n            },\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"ELECTRON\",\n              \"cond\": \"eq\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-invalid.json",
    "content": "{\n  \"version\": 5,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"flag\": true,\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": true,\n          \"cond\": \"eq\"\n        },\n        {\n          \"or\": [\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"DOCKER_ON_PREMISE\",\n              \"cond\": \"eq\"\n            },\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"ELECTRON\",\n              \"cond\": \"eq\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/insights-valid.json",
    "content": "{\n  \"version\": 8,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"perc\": [\n        [\n          44,\n          50\n        ]\n      ],\n      \"flag\": true,\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": true,\n          \"cond\": \"eq\"\n        },\n        {\n          \"or\": [\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"DOCKER_ON_PREMISE\",\n              \"cond\": \"eq\"\n            },\n            {\n              \"name\": \"config.server.buildType\",\n              \"value\": \"ELECTRON\",\n              \"cond\": \"eq\"\n            }\n          ]\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/features-configs/sso-docker-build.json",
    "content": "{\n  \"version\": 11,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"flag\": true,\n      \"perc\": [[44,50]],\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": true,\n          \"cond\": \"eq\"\n        },\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"DOCKER_ON_PREMISE\",\n          \"cond\": \"eq\"\n        }\n      ]\n    },\n    \"cloudSso\": {\n      \"flag\": true,\n      \"perc\": [[44,50]],\n      \"filters\": [\n        {\n          \"name\": \"agreements.analytics\",\n          \"value\": true,\n          \"cond\": \"eq\"\n        },\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"DOCKER_ON_PREMISE\",\n          \"cond\": \"eq\"\n        }\n      ],\n      \"data\": {\n        \"selectPlan\": {\n          \"components\": {\n            \"triggersAndFunctions\": [\n              {\n                \"provider\": \"AWS\",\n                \"regions\": [\"ap-southeast-1\"]\n              },\n              {\n                \"provider\": \"GCP\",\n                \"regions\": [\"asia-northeast1\"]\n              }\n            ]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/e2e/test-data/features-configs/sso-electron-build.json",
    "content": "{\n  \"version\": 20,\n  \"features\": {\n    \"insightsRecommendations\": {\n      \"flag\": true,\n      \"perc\": [[44,50]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ]\n    },\n    \"cloudSso\": {\n      \"flag\": true,\n      \"perc\": [[44,50]],\n      \"filters\": [\n        {\n          \"name\": \"config.server.buildType\",\n          \"value\": \"ELECTRON\",\n          \"cond\": \"eq\"\n        }\n      ],\n      \"data\": {\n        \"selectPlan\": {\n          \"components\": {\n            \"triggersAndFunctions\": [\n              {\n                \"provider\": \"AWS\",\n                \"regions\": [\"ap-southeast-1\"]\n              },\n              {\n                \"provider\": \"GCP\",\n                \"regions\": [\"asia-northeast1\"]\n              }\n            ]\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/ASCII.ts",
    "content": "export const ASCIIFormatter = {\n    format: 'ASCII',\n    fromText: '山女子水 рус ascii',\n    fromTextEdit: '山女子水 рус ascii 山女子',\n    formattedText: '\\\\xe5\\\\xb1\\\\xb1\\\\xe5\\\\xa5\\\\xb3\\\\xe5\\\\xad\\\\x90\\\\xe6\\\\xb0\\\\xb4 \\\\xd1\\\\x80\\\\xd1\\\\x83\\\\xd1\\\\x81 ascii',\n    formattedTextEdit: '\\\\xe5\\\\xb1\\\\xb1\\\\xe5\\\\xa5\\\\xb3\\\\xe5\\\\xad\\\\x90\\\\xe6\\\\xb0\\\\xb4 \\\\xd1\\\\x80\\\\xd1\\\\x83\\\\xd1\\\\x81 ascii \\\\xe5\\\\xb1\\\\xb1\\\\xe5\\\\xa5\\\\xb3\\\\xe5\\\\xad\\\\x90'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Binary.ts",
    "content": "export const BinaryFormatter = {\n    format: 'Binary',\n    fromText: '水 рус bin',\n    fromTextEdit: '水山 рус bin 子',\n    formattedText: '1110011010110000101101000010000011010001100000001101000110000011110100011000000100100000011000100110100101101110',\n    formattedTextEdit: '111001101011000010110100111001011011000110110001001000001101000110000000110100011000001111010001100000010010000001100010011010010110111000100000111001011010110110010000'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/DataTime.ts",
    "content": "export const DataTimeFormatter = {\n    format: 'Timestamp to DateTime',\n    fromText: '1633072800',\n    fromTextEdit: '-179064000000',\n    formattedText: '07:20:00 1 Oct 2021',\n    formattedTextEdit: '12:00:00 29 Apr 1964'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/HEX.ts",
    "content": "export const HEXFormatter = {\n    format: 'HEX',\n    fromText: '山女子水 рус hex',\n    fromTextEdit: '山女子水 рус hex 山女子',\n    formattedText: 'e5b1b1e5a5b3e5ad90e6b0b420d180d183d18120686578',\n    formattedTextEdit: 'e5b1b1e5a5b3e5ad90e6b0b420d180d183d1812068657820e5b1b1e5a5b3e5ad90'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/JSON.ts",
    "content": "export const JSONFormatter = {\n    format: 'JSON',\n    fromText: '{ \"field\": \"value\" }',\n    fromTextEdit: '{ \"field\": \"value123\" }',\n    fromBigInt: '{ \"field\": 248480010225057793 }'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Java.ts",
    "content": "export const JavaFormatter = {\n    format: 'Java serialized',\n    fromHexText: 'aced000573720008456d706c6f796565025e743467c6123c0200034900066e756d6265724c0007616464726573737400124c6a6176612f6c616e672f537472696e673b4c00046e616d6571007e000178700000006574001950686f6b6b61204b75616e2c20416d62656874612050656572740009526579616e20416c69',\n    formattedText: '{ \"fields\": [ { \"number\": 101 }, { \"address\": \"Phokka Kuan, Ambehta Peer\" }, { \"name\": \"Reyan Ali\" } ], \"annotations\": [], \"className\": \"Employee\", \"serialVersionUid\": 170701604314812988 }'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Msgpack.ts",
    "content": "export const MsgpackFormatter = {\n    format: 'Msgpack',\n    fromHexText: 'DF00000001A56669656C64A576616C7565',\n    fromText: '{ \"field\": \"value\" }',\n    fromTextEdit: '{ \"field\": \"value123\" }',\n    formattedText: '{ \"field\": \"value\" }'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/PHP.ts",
    "content": "export const PHPFormatter = {\n    format: 'PHP serialized',\n    fromText: 'a:2:{i:0;s:12:\"Sample array\";i:1;a:2:{i:0;s:5:\"Apple\";i:1;s:6:\"Orange\";}}',\n    fromTextEdit: '[ \"Sample array\", [ \"Apple\", \"Orange15\" ] ]',\n    formattedText: '[ \"Sample array\", [ \"Apple\", \"Orange\" ] ]'\n};\n\n/**\n * PHP data for convertion including different php serialized data types\n */\nexport const phpData = [{\n    dataType: 'Object',\n    //commented due to bug: RI-4906 it's not a priority. can be removed if users won't submitted bugs for a short period of time\n    //from: 'a:6:{i:1;s:30:\"PHP code tester Sandbox Online\";s:5:\"emoji\";s:24:\"😀 😃 😄 😁 😆\";i:2;i:5;i:5;i:89009;s:13:\"Random number\";i:341;s:11:\"PHP Version\";s:5:\"8.1.9\";}',\n    //converted: '{ \"1\": \"PHP code tester Sandbox Online\", \"2\": 5, \"5\": 89009, \"emoji\": \"😀 😃 😄 😁 😆\", \"Random number\": 341, \"PHP Version\": \"8.1.9\" }'\n    from: 'a:5:{s:1:\"1\";s:30:\"PHP code tester Sandbox Online\";s:1:\"2\";i:5;s:1:\"5\";i:89009;s:13:\"Random number\";i:341;s:11:\"PHP Version\";s:5:\"8.1.9\";}',\n    converted: '{ \"1\": \"PHP code tester Sandbox Online\", \"2\": 5, \"5\": 89009, \"Random number\": 341, \"PHP Version\": \"8.1.9\" }'\n},\n{\n    dataType: 'Number',\n    from: 'i:34567234;',\n    converted: '34567234'\n},\n{\n    dataType: 'String',\n    from: 's:72:\"Dumbledore took Harry in his arms and turned toward the Dursleys\\' house.\";',\n    converted: '\"Dumbledore took Harry in his arms and turned toward the Dursleys\\' house.\"'\n}];\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Pickle.ts",
    "content": "export const PickleFormatter = {\n    format: 'Pickle',\n    fromHexText: '286470300a5327617272270a70310a286c70320a49310a6149320a617353276f626a270a70330a286470340a532761270a70350a532762270a70360a7373532748656c6c6f270a70370a5327776f726c64270a70380a732e',\n    formattedText: '{ \"arr\": [ 1, 2 ], \"obj\": { \"a\": \"b\" }, \"Hello\": \"world\" }'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Protobuf.ts",
    "content": "export const ProtobufFormatter = {\n    format: 'Protobuf',\n    fromHexText: '08d90f10d802',\n    formattedText: '[ { \"1\": 2009 }, { \"2\": 344 } ]'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Vector32Bit.ts",
    "content": "export const Vector32BitFormatter = {\n    format: 'Vector 32-bit',\n    fromHexText: '0000803f0000004000004040',\n    formattedText: '[ 1, 2, 3 ]'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/Vector64Bit.ts",
    "content": "export const Vector64BitFormatter = {\n    format: 'Vector 64-bit',\n    fromHexText: '000000000000f03f00000000000000400000000000000840',\n    formattedText: '[ 1, 2, 3 ]'\n};\n"
  },
  {
    "path": "tests/e2e/test-data/formatters/index.ts",
    "content": "export * from './ASCII';\nexport * from './Binary';\nexport * from './HEX';\nexport * from './Java';\nexport * from './JSON';\nexport * from './Msgpack';\nexport * from './PHP';\nexport * from './Pickle';\nexport * from './Protobuf';\nexport * from './Vector32Bit';\nexport * from './Vector64Bit';\n"
  },
  {
    "path": "tests/e2e/test-data/formatters-data.ts",
    "content": "import {\n    ASCIIFormatter,\n    BinaryFormatter,\n    HEXFormatter,\n    JavaFormatter,\n    JSONFormatter,\n    MsgpackFormatter,\n    PHPFormatter,\n    PickleFormatter,\n    ProtobufFormatter,\n    Vector32BitFormatter,\n    Vector64BitFormatter\n} from './formatters';\nimport { DataTimeFormatter } from './formatters/DataTime';\n\ninterface IFormatter {\n    format: string,\n    fromText?: string,\n    fromTextEdit?: string,\n    fromBigInt?: string,\n    formattedText?: string,\n    fromHexText?: string,\n    formattedTextEdit?: string\n}\n\n/**\n * Formatters objects with test data for format convertion\n */\nexport const formatters: IFormatter[] = [\n    JSONFormatter,\n    MsgpackFormatter,\n    ProtobufFormatter,\n    PHPFormatter,\n    JavaFormatter,\n    ASCIIFormatter,\n    HEXFormatter,\n    BinaryFormatter,\n    PickleFormatter,\n    Vector32BitFormatter,\n    Vector64BitFormatter,\n    DataTimeFormatter\n];\n\nexport const binaryFormattersSet: IFormatter[] = [\n    ASCIIFormatter,\n    // HEXFormatter,\n    // BinaryFormatter\n    // HEX and Binary are failing in the tests\n];\n\nexport const formattersHighlightedSet: IFormatter[] = [JSONFormatter, PHPFormatter];\nexport const formattersForEditSet: IFormatter[] = [\n    JSONFormatter,\n    MsgpackFormatter,\n    PHPFormatter\n];\nexport const formattersWithTooltipSet: IFormatter[] = [\n    JSONFormatter,\n    MsgpackFormatter,\n    ProtobufFormatter,\n    PHPFormatter,\n    JavaFormatter,\n    PickleFormatter\n];\nexport const notEditableFormattersSet: IFormatter[] = [\n    ProtobufFormatter,\n    JavaFormatter,\n    PickleFormatter,\n    Vector32BitFormatter,\n    Vector64BitFormatter\n];\n"
  },
  {
    "path": "tests/e2e/test-data/import-databases/ardm-valid.ano",
    "content": "W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9LHsiaG9zdCI6Im9zcy1zZW50aW5lbCIsInBvcnQiOiIyNjM3OSIsIm5hbWUiOiJhcmRtU2VudGluZWwiLCJhdXRoIjoicGFzc3dvcmQiLCJzZW50aW5lbE9wdGlvbnMiOnsibWFzdGVyTmFtZSI6InByaW1hcnktZ3JvdXAtMSIsIm5vZGVQYXNzd29yZCI6ImRlZmF1bHRwYXNzIn19XQ=="
  },
  {
    "path": "tests/e2e/test-data/import-databases/racompFullSSH.json",
    "content": "[\n\t{\n\t\t\"srvRecords\": [\n\t\t\t{\n\t\t\t\t\"priority\": null,\n\t\t\t\t\"weight\": null,\n\t\t\t\t\"port\": null,\n\t\t\t\t\"name\": null\n\t\t\t}\n\t\t],\n\t\t\"useSRVRecords\": false,\n\t\t\"natMaps\": [\n\t\t\t{\n\t\t\t\t\"privateHost\": null,\n\t\t\t\t\"privatePort\": null,\n\t\t\t\t\"publicHost\": null,\n\t\t\t\t\"publicPort\": null\n\t\t\t}\n\t\t],\n\t\t\"enableNatMaps\": false,\n\t\t\"clusterOptions\": {\n\t\t\t\"slotsRefreshInterval\": 5000,\n\t\t\t\"slotsRefreshTimeout\": 1000,\n\t\t\t\"retryDelayOnTryAgain\": 100,\n\t\t\t\"retryDelayOnClusterDown\": 100,\n\t\t\t\"retryDelayOnFailover\": 100,\n\t\t\t\"retryDelayOnMoved\": 0,\n\t\t\t\"maxRedirections\": 16,\n\t\t\t\"dnsLookup\": false,\n\t\t\t\"scaleReads\": \"master\",\n\t\t\t\"startupNodes\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"enableStartupNodes\": false,\n\t\t\"sentinelOptions\": {\n\t\t\t\"tls\": {\n\t\t\t\t\"key\": null,\n\t\t\t\t\"keyBookmark\": null,\n\t\t\t\t\"ca\": null,\n\t\t\t\t\"caBookmark\": null,\n\t\t\t\t\"cert\": null,\n\t\t\t\t\"certBookmark\": null\n\t\t\t},\n\t\t\t\"preferredSlaves\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null,\n\t\t\t\t\t\"priority\": null\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"role\": null,\n\t\t\t\"sentinelPassword\": null,\n\t\t\t\"name\": null\n\t\t},\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": null,\n\t\t\"sshKeyFile\": null,\n\t\t\"sshPassword\": \"pass\",\n\t\t\"sshUser\": \"u\",\n\t\t\"sshPort\": 2222,\n\t\t\"sshHost\": \"172.31.100.245\",\n\t\t\"ssh\": true,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": null,\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 6379,\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"keyPrefix\": null,\n\t\t\"name\": \"racompSSHpass\",\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"586f8b9e-a9c3-4c9f-8ff8-3954611473e6\",\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": \"test\",\n\t\t\"sshKeyFileBookmark\": \"Ym9vaywDAAAAAAQQMAAAAIBWP/gr7z3b88NnoG4DgHrVdcb9xooy0jrMPBXgQNZ5KAIAAAQAAAADAwAAAAgAKAUAAAABAQAAVXNlcnMAAAAPAAAAAQEAAHZsYWRpc2xhdmRhcmdlbAAIAAAAAQEAAFByb2plY3RzCAAAAAEBAABzc2gtZW52cwMAAAABAQAAc3NoAAQAAAABAQAAa2V5cwUAAAABAQAAdGVzdHAAAAAcAAAAAQYAABAAAAAgAAAAOAAAAEgAAABYAAAAZAAAAHAAAAAIAAAABAMAAAFdAAAAAAAACAAAAAQDAAB2gQAAAAAAAAgAAAAEAwAAQpMRAAAAAAAIAAAABAMAAC783QAAAAAACAAAAAQDAAAz/N0AAAAAAAgAAAAEAwAANPzdAAAAAAAIAAAABAMAADn83QAAAAAAHAAAAAEGAACkAAAAtAAAAMQAAADUAAAA5AAAAPQAAAAEAQAACAAAAAAEAABBxLVvdQAAABgAAAABAgAAAQAAAAAAAAAPAAAAAAAAAAAAAAAAAAAACAAAAAQDAAAFAAAAAAAAAAQAAAADAwAA9QEAAAgAAAABCQAAZmlsZTovLy8MAAAAAQEAAE1hY2ludG9zaCBIRAgAAAAEAwAAAACHETkAAAAIAAAAAAQAAEHEUm5rAAAAJAAAAAEBAABDMjExQUM1Qy00MTlGLTQ0NTQtOEZEOS0xNDEwMDFBMDhFOTQYAAAAAQIAAIEAAAABAAAA7xMAAAEAAAAAAAAAAAAAAAEAAAABAQAALwAAAAAAAAABBQAAzAAAAP7///8BAAAAAAAAABAAAAAEEAAAgAAAAAAAAAAFEAAAFAEAAAAAAAAQEAAASAEAAAAAAABAEAAAOAEAAAAAAAACIAAAFAIAAAAAAAAFIAAAhAEAAAAAAAAQIAAAlAEAAAAAAAARIAAAyAEAAAAAAAASIAAAqAEAAAAAAAATIAAAuAEAAAAAAAAgIAAA9AEAAAAAAAAwIAAAIAIAAAAAAAABwAAAaAEAAAAAAAARwAAAIAAAAAAAAAASwAAAeAEAAAAAAAAQ0AAABAAAAAAAAAA=\",\n\t\t\"sshKeyFile\": \"/test-data/ssh/sshPrivateKeyPasscode\",\n\t\t\"sshPassword\": null,\n\t\t\"sshUser\": \"u\",\n\t\t\"sshPort\": 2222,\n\t\t\"sshHost\": \"172.31.100.245\",\n\t\t\"ssh\": true,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": null,\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 6379,\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"keyPrefix\": null,\n\t\t\"name\": \"racompSSHPassphrase\",\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"a120b622-d03a-445c-9414-bb9e015f01eb\",\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": \"Ym9vaygDAAAAAAQQMAAAAFCmPsFThwh4jmVDMVb0pSIjuOx1dHbxRiMdvR0o1X/gJAIAAAQAAAADAwAAAAgAKAUAAAABAQAAVXNlcnMAAAAPAAAAAQEAAHZsYWRpc2xhdmRhcmdlbAAIAAAAAQEAAFByb2plY3RzCAAAAAEBAABzc2gtZW52cwMAAAABAQAAc3NoAAQAAAABAQAAa2V5cwQAAAABAQAAdGVzdBwAAAABBgAAEAAAACAAAAA4AAAASAAAAFgAAABkAAAAcAAAAAgAAAAEAwAAAV0AAAAAAAAIAAAABAMAAHaBAAAAAAAACAAAAAQDAABCkxEAAAAAAAgAAAAEAwAALvzdAAAAAAAIAAAABAMAADP83QAAAAAACAAAAAQDAAA0/N0AAAAAAAgAAAAEAwAANfzdAAAAAAAcAAAAAQYAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAAABAAAIAAAAAAQAAEHEtW9qAAAAGAAAAAECAAABAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAIAAAABAMAAAUAAAAAAAAABAAAAAMDAAD1AQAACAAAAAEJAABmaWxlOi8vLwwAAAABAQAATWFjaW50b3NoIEhECAAAAAQDAAAAAIcROQAAAAgAAAAABAAAQcRSbmsAAAAkAAAAAQEAAEMyMTFBQzVDLTQxOUYtNDQ1NC04RkQ5LTE0MTAwMUEwOEU5NBgAAAABAgAAgQAAAAEAAADvEwAAAQAAAAAAAAAAAAAAAQAAAAEBAAAvAAAAAAAAAAEFAADMAAAA/v///wEAAAAAAAAAEAAAAAQQAAB8AAAAAAAAAAUQAAAQAQAAAAAAABAQAABEAQAAAAAAAEAQAAA0AQAAAAAAAAIgAAAQAgAAAAAAAAUgAACAAQAAAAAAABAgAACQAQAAAAAAABEgAADEAQAAAAAAABIgAACkAQAAAAAAABMgAAC0AQAAAAAAACAgAADwAQAAAAAAADAgAAAcAgAAAAAAAAHAAABkAQAAAAAAABHAAAAgAAAAAAAAABLAAAB0AQAAAAAAABDQAAAEAAAAAAAAAA==\",\n\t\t\"sshKeyFile\": \"/test-data/ssh/sshPrivateKey\",\n\t\t\"sshPassword\": null,\n\t\t\"sshUser\": \"u\",\n\t\t\"sshPort\": 2222,\n\t\t\"sshHost\": \"172.31.100.245\",\n\t\t\"ssh\": true,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": null,\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 6379,\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"keyPrefix\": null,\n\t\t\"name\": \"racompSSHPrivateKey\",\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"1cafc9cc-bfe3-4b60-9a29-adf033cbd909\",\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": \"Ym9vaygDAAAAAAQQMAAAAFCmPsFThwh4jmVDMVb0pSIjuOx1dHbxRiMdvR0o1X/gJAIAAAQAAAADAwAAAAgAKAUAAAABAQAAVXNlcnMAAAAPAAAAAQEAAHZsYWRpc2xhdmRhcmdlbAAIAAAAAQEAAFByb2plY3RzCAAAAAEBAABzc2gtZW52cwMAAAABAQAAc3NoAAQAAAABAQAAa2V5cwQAAAABAQAAdGVzdBwAAAABBgAAEAAAACAAAAA4AAAASAAAAFgAAABkAAAAcAAAAAgAAAAEAwAAAV0AAAAAAAAIAAAABAMAAHaBAAAAAAAACAAAAAQDAABCkxEAAAAAAAgAAAAEAwAALvzdAAAAAAAIAAAABAMAADP83QAAAAAACAAAAAQDAAA0/N0AAAAAAAgAAAAEAwAANfzdAAAAAAAcAAAAAQYAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAAABAAAIAAAAAAQAAEHEtW9qAAAAGAAAAAECAAABAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAIAAAABAMAAAUAAAAAAAAABAAAAAMDAAD1AQAACAAAAAEJAABmaWxlOi8vLwwAAAABAQAATWFjaW50b3NoIEhECAAAAAQDAAAAAIcROQAAAAgAAAAABAAAQcRSbmsAAAAkAAAAAQEAAEMyMTFBQzVDLTQxOUYtNDQ1NC04RkQ5LTE0MTAwMUEwOEU5NBgAAAABAgAAgQAAAAEAAADvEwAAAQAAAAAAAAAAAAAAAQAAAAEBAAAvAAAAAAAAAAEFAADMAAAA/v///wEAAAAAAAAAEAAAAAQQAAB8AAAAAAAAAAUQAAAQAQAAAAAAABAQAABEAQAAAAAAAEAQAAA0AQAAAAAAAAIgAAAQAgAAAAAAAAUgAACAAQAAAAAAABAgAACQAQAAAAAAABEgAADEAQAAAAAAABIgAACkAQAAAAAAABMgAAC0AQAAAAAAACAgAADwAQAAAAAAADAgAAAcAgAAAAAAAAHAAABkAQAAAAAAABHAAAAgAAAAAAAAABLAAAB0AQAAAAAAABDQAAAEAAAAAAAAAA==\",\n\t\t\"sshKeyFile\": null,\n\t\t\"sshPassword\": null,\n\t\t\"sshUser\": \"u\",\n\t\t\"sshPort\": 2222,\n\t\t\"sshHost\": \"172.31.100.245\",\n\t\t\"ssh\": true,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": null,\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 6379,\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"keyPrefix\": null,\n\t\t\"name\": \"racompSSHonlyRequired\",\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"1cafc9cc-bfe3-4b60-9a29-adf033cbd909\",\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"auth\": \"\",\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"name\": \"rdmSSHPrivateKey\",\n\t\t\"port\": 6379,\n\t\t\"ssh_host\": \"172.31.100.245\",\n\t\t\"ssh_password\": \"\",\n\t\t\"ssh_port\": 2222,\n\t\t\"ssh_private_key_path\": \"/test-data/ssh/sshPrivateKeyPasscode\",\n\t\t\"ssh_user\": \"u\",\n\t\t\"timeout_connect\": 60000,\n\t\t\"timeout_execute\": 60000,\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"auth\": \"\",\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"name\": \"rdmSSHAgentPath\",\n\t\t\"port\": 6379,\n\t\t\"ssh_host\": \"172.31.100.245\",\n\t\t\"ssh_password\": \"test\",\n\t\t\"ssh_port\": 2222,\n\t\t\"ssh_private_key_path\": \"\",\n\t\t\"ssh_agent_path\": \"/test-data/ssh/sshPrivateKeyPasscode\",\n\t\t\"ssh_user\": \"u\",\n\t\t\"timeout_connect\": 60000,\n\t\t\"timeout_execute\": 60000,\n\t\t\"result\": \"partial\"\n\t},\n\t{\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": \"Ym9vaygDAAAAAAQQMAAAAFCmPsFThwh4jmVDMVb0pSIjuOx1dHbxRiMdvR0o1X/gJAIAAAQAAAADAwAAAAgAKAUAAAABAQAAVXNlcnMAAAAPAAAAAQEAAHZsYWRpc2xhdmRhcmdlbAAIAAAAAQEAAFByb2plY3RzCAAAAAEBAABzc2gtZW52cwMAAAABAQAAc3NoAAQAAAABAQAAa2V5cwQAAAABAQAAdGVzdBwAAAABBgAAEAAAACAAAAA4AAAASAAAAFgAAABkAAAAcAAAAAgAAAAEAwAAAV0AAAAAAAAIAAAABAMAAHaBAAAAAAAACAAAAAQDAABCkxEAAAAAAAgAAAAEAwAALvzdAAAAAAAIAAAABAMAADP83QAAAAAACAAAAAQDAAA0/N0AAAAAAAgAAAAEAwAANfzdAAAAAAAcAAAAAQYAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAAABAAAIAAAAAAQAAEHEtW9qAAAAGAAAAAECAAABAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAIAAAABAMAAAUAAAAAAAAABAAAAAMDAAD1AQAACAAAAAEJAABmaWxlOi8vLwwAAAABAQAATWFjaW50b3NoIEhECAAAAAQDAAAAAIcROQAAAAgAAAAABAAAQcRSbmsAAAAkAAAAAQEAAEMyMTFBQzVDLTQxOUYtNDQ1NC04RkQ5LTE0MTAwMUEwOEU5NBgAAAABAgAAgQAAAAEAAADvEwAAAQAAAAAAAAAAAAAAAQAAAAEBAAAvAAAAAAAAAAEFAADMAAAA/v///wEAAAAAAAAAEAAAAAQQAAB8AAAAAAAAAAUQAAAQAQAAAAAAABAQAABEAQAAAAAAAEAQAAA0AQAAAAAAAAIgAAAQAgAAAAAAAAUgAACAAQAAAAAAABAgAACQAQAAAAAAABEgAADEAQAAAAAAABIgAACkAQAAAAAAABMgAAC0AQAAAAAAACAgAADwAQAAAAAAADAgAAAcAgAAAAAAAAHAAABkAQAAAAAAABHAAAAgAAAAAAAAABLAAAB0AQAAAAAAABDQAAAEAAAAAAAAAA==\",\n\t\t\"sshPassword\": \"pass\",\n\t\t\"sshUser\": \"u\",\n\t\t\"sshHost\": \"172.31.100.245\",\n\t\t\"ssh\": true,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": null,\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 6379,\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"keyPrefix\": null,\n\t\t\"name\": \"racompSSHnoPort\",\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"1cafc9cc-bfe3-4b60-9a29-adf033cbd909\",\n\t\t\"result\": \"partial\"\n\t},\n\t{\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": \"Ym9vaygDAAAAAAQQMAAAAFCmPsFThwh4jmVDMVb0pSIjuOx1dHbxRiMdvR0o1X/gJAIAAAQAAAADAwAAAAgAKAUAAAABAQAAVXNlcnMAAAAPAAAAAQEAAHZsYWRpc2xhdmRhcmdlbAAIAAAAAQEAAFByb2plY3RzCAAAAAEBAABzc2gtZW52cwMAAAABAQAAc3NoAAQAAAABAQAAa2V5cwQAAAABAQAAdGVzdBwAAAABBgAAEAAAACAAAAA4AAAASAAAAFgAAABkAAAAcAAAAAgAAAAEAwAAAV0AAAAAAAAIAAAABAMAAHaBAAAAAAAACAAAAAQDAABCkxEAAAAAAAgAAAAEAwAALvzdAAAAAAAIAAAABAMAADP83QAAAAAACAAAAAQDAAA0/N0AAAAAAAgAAAAEAwAANfzdAAAAAAAcAAAAAQYAAKAAAACwAAAAwAAAANAAAADgAAAA8AAAAAABAAAIAAAAAAQAAEHEtW9qAAAAGAAAAAECAAABAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAIAAAABAMAAAUAAAAAAAAABAAAAAMDAAD1AQAACAAAAAEJAABmaWxlOi8vLwwAAAABAQAATWFjaW50b3NoIEhECAAAAAQDAAAAAIcROQAAAAgAAAAABAAAQcRSbmsAAAAkAAAAAQEAAEMyMTFBQzVDLTQxOUYtNDQ1NC04RkQ5LTE0MTAwMUEwOEU5NBgAAAABAgAAgQAAAAEAAADvEwAAAQAAAAAAAAAAAAAAAQAAAAEBAAAvAAAAAAAAAAEFAADMAAAA/v///wEAAAAAAAAAEAAAAAQQAAB8AAAAAAAAAAUQAAAQAQAAAAAAABAQAABEAQAAAAAAAEAQAAA0AQAAAAAAAAIgAAAQAgAAAAAAAAUgAACAAQAAAAAAABAgAACQAQAAAAAAABEgAADEAQAAAAAAABIgAACkAQAAAAAAABMgAAC0AQAAAAAAACAgAADwAQAAAAAAADAgAAAcAgAAAAAAAAHAAABkAQAAAAAAABHAAAAgAAAAAAAAABLAAAB0AQAAAAAAABDQAAAEAAAAAAAAAA==\",\n\t\t\"sshPassword\": \"pass\",\n\t\t\"sshPort\": 2222,\n\t\t\"sshHost\": \"172.31.100.245\",\n\t\t\"ssh\": true,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": null,\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 6379,\n\t\t\"host\": \"172.33.100.10\",\n\t\t\"keyPrefix\": null,\n\t\t\"name\": \"racompSSHnoUser\",\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"1cafc9cc-bfe3-4b60-9a29-adf033cbd909\",\n\t\t\"result\": \"partial\"\n\t}\n]\n"
  },
  {
    "path": "tests/e2e/test-data/import-databases/racompass-invalid.json",
    "content": "[\n\t{\n\t\t\"srvRecords\": [\n\t\t\t{\n\t\t\t\t\"priority\": null,\n\t\t\t\t\"weight\": null,\n\t\t\t\t\"port\": null,\n\t\t\t\t\"name\": null\n\t\t\t}\n\t\t],\n\t\t\"useSRVRecords\": false,\n\t\t\"natMaps\": [\n\t\t\t{\n\t\t\t\t\"privateHost\": null,\n\t\t\t\t\"privatePort\": null,\n\t\t\t\t\"publicHost\": null,\n\t\t\t\t\"publicPort\": null\n\t\t\t}\n\t\t],\n\t\t\"enableNatMaps\": false,\n\t\t\"clusterOptions\": {\n\t\t\t\"slotsRefreshInterval\": 5000,\n\t\t\t\"slotsRefreshTimeout\": 1000,\n\t\t\t\"retryDelayOnTryAgain\": 100,\n\t\t\t\"retryDelayOnClusterDown\": 100,\n\t\t\t\"retryDelayOnFailover\": 100,\n\t\t\t\"retryDelayOnMoved\": 0,\n\t\t\t\"maxRedirections\": 16,\n\t\t\t\"dnsLookup\": false,\n\t\t\t\"scaleReads\": \"master\",\n\t\t\t\"startupNodes\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"enableStartupNodes\": false,\n\t\t\"sentinelOptions\": {\n\t\t\t\"tls\": {\n\t\t\t\t\"key\": null,\n\t\t\t\t\"keyBookmark\": null,\n\t\t\t\t\"ca\": null,\n\t\t\t\t\"caBookmark\": null,\n\t\t\t\t\"cert\": null,\n\t\t\t\t\"certBookmark\": null\n\t\t\t},\n\t\t\t\"preferredSlaves\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null,\n\t\t\t\t\t\"priority\": null\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"role\": null,\n\t\t\t\"sentinelPassword\": null,\n\t\t\t\"name\": null\n\t\t},\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": null,\n\t\t\"sshKeyFile\": null,\n\t\t\"sshUser\": null,\n\t\t\"sshPort\": null,\n\t\t\"sshHost\": null,\n\t\t\"ssh\": false,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": \"\",\n\t\t\"color\": \"#4B5563\",\n\t\t\"host\": \"localhost\",\n\t\t\"keyPrefix\": null,\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"f99a5d6d-daf4-489c-885b-6f8e411adbc9\"\n\t},\n\t{\n\t\t\"srvRecords\": [\n\t\t\t{\n\t\t\t\t\"priority\": null,\n\t\t\t\t\"weight\": null,\n\t\t\t\t\"port\": null,\n\t\t\t\t\"name\": null\n\t\t\t}\n\t\t],\n\t\t\"useSRVRecords\": false,\n\t\t\"natMaps\": [\n\t\t\t{\n\t\t\t\t\"privateHost\": null,\n\t\t\t\t\"privatePort\": null,\n\t\t\t\t\"publicHost\": null,\n\t\t\t\t\"publicPort\": null\n\t\t\t}\n\t\t],\n\t\t\"enableNatMaps\": false,\n\t\t\"clusterOptions\": {\n\t\t\t\"slotsRefreshInterval\": 5000,\n\t\t\t\"slotsRefreshTimeout\": 1000,\n\t\t\t\"retryDelayOnTryAgain\": 100,\n\t\t\t\"retryDelayOnClusterDown\": 100,\n\t\t\t\"retryDelayOnFailover\": 100,\n\t\t\t\"retryDelayOnMoved\": 0,\n\t\t\t\"maxRedirections\": 16,\n\t\t\t\"dnsLookup\": false,\n\t\t\t\"scaleReads\": \"master\",\n\t\t\t\"startupNodes\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"enableStartupNodes\": false,\n\t\t\"sentinelOptions\": {\n\t\t\t\"tls\": {\n\t\t\t\t\"key\": null,\n\t\t\t\t\"keyBookmark\": null,\n\t\t\t\t\"ca\": null,\n\t\t\t\t\"caBookmark\": null,\n\t\t\t\t\"cert\": null,\n\t\t\t\t\"certBookmark\": null\n\t\t\t},\n\t\t\t\"preferredSlaves\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null,\n\t\t\t\t\t\"priority\": null\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"role\": null,\n\t\t\t\"sentinelPassword\": null,\n\t\t\t\"name\": null\n\t\t},\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": null,\n\t\t\"sshKeyFile\": null,\n\t\t\"sshUser\": null,\n\t\t\"sshPort\": null,\n\t\t\"sshHost\": null,\n\t\t\"ssh\": false,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 1,\n\t\t\"password\": \"vfsd\",\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 1111,\n\t\t\"keyPrefix\": null,\n\t\t\"type\": \"standalone\",\n\t\t\"connectionName\": \"vd\",\n\t\t\"id\": \"f99a5d6d-daf4-489c-885b-6f8e411adbc9\",\n\t\t\"cluster\": true,\n\t\t\"name\": \"vd long host\"\n]"
  },
  {
    "path": "tests/e2e/test-data/import-databases/racompass-valid.json",
    "content": "[\n\t{\n\t\t\"srvRecords\": [\n\t\t\t{\n\t\t\t\t\"priority\": null,\n\t\t\t\t\"weight\": null,\n\t\t\t\t\"port\": null,\n\t\t\t\t\"name\": null\n\t\t\t}\n\t\t],\n\t\t\"useSRVRecords\": false,\n\t\t\"natMaps\": [\n\t\t\t{\n\t\t\t\t\"privateHost\": null,\n\t\t\t\t\"privatePort\": null,\n\t\t\t\t\"publicHost\": null,\n\t\t\t\t\"publicPort\": null\n\t\t\t}\n\t\t],\n\t\t\"enableNatMaps\": false,\n\t\t\"clusterOptions\": {\n\t\t\t\"slotsRefreshInterval\": 5000,\n\t\t\t\"slotsRefreshTimeout\": 1000,\n\t\t\t\"retryDelayOnTryAgain\": 100,\n\t\t\t\"retryDelayOnClusterDown\": 100,\n\t\t\t\"retryDelayOnFailover\": 100,\n\t\t\t\"retryDelayOnMoved\": 0,\n\t\t\t\"maxRedirections\": 16,\n\t\t\t\"dnsLookup\": false,\n\t\t\t\"scaleReads\": \"master\",\n\t\t\t\"startupNodes\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"enableStartupNodes\": false,\n\t\t\"sentinelOptions\": {\n\t\t\t\"tls\": {\n\t\t\t\t\"key\": null,\n\t\t\t\t\"keyBookmark\": null,\n\t\t\t\t\"ca\": null,\n\t\t\t\t\"caBookmark\": null,\n\t\t\t\t\"cert\": null,\n\t\t\t\t\"certBookmark\": null\n\t\t\t},\n\t\t\t\"preferredSlaves\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null,\n\t\t\t\t\t\"priority\": null\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"role\": null,\n\t\t\t\"sentinelPassword\": null,\n\t\t\t\"name\": null\n\t\t},\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": null,\n\t\t\"sshKeyFile\": null,\n\t\t\"sshUser\": null,\n\t\t\"sshPort\": null,\n\t\t\"sshHost\": null,\n\t\t\"ssh\": false,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 1,\n\t\t\"password\": \"\",\n\t\t\"color\": \"#4B5563\",\n\t\t\"port\": 8100,\n\t\t\"host\": \"racompassDbWithIndex\",\n\t\t\"keyPrefix\": null,\n\t\t\"type\": \"standalone\",\n\t\t\"id\": \"f99a5d6d-daf4-489c-885b-6f8e411adbc9\"\n\t},\n\t{\n\t\t\"srvRecords\": [\n\t\t\t{\n\t\t\t\t\"priority\": null,\n\t\t\t\t\"weight\": null,\n\t\t\t\t\"port\": null,\n\t\t\t\t\"name\": null\n\t\t\t}\n\t\t],\n\t\t\"useSRVRecords\": false,\n\t\t\"natMaps\": [\n\t\t\t{\n\t\t\t\t\"privateHost\": null,\n\t\t\t\t\"privatePort\": null,\n\t\t\t\t\"publicHost\": null,\n\t\t\t\t\"publicPort\": null\n\t\t\t}\n\t\t],\n\t\t\"enableNatMaps\": false,\n\t\t\"clusterOptions\": {\n\t\t\t\"slotsRefreshInterval\": 5000,\n\t\t\t\"slotsRefreshTimeout\": 1000,\n\t\t\t\"retryDelayOnTryAgain\": 100,\n\t\t\t\"retryDelayOnClusterDown\": 100,\n\t\t\t\"retryDelayOnFailover\": 100,\n\t\t\t\"retryDelayOnMoved\": 0,\n\t\t\t\"maxRedirections\": 16,\n\t\t\t\"dnsLookup\": false,\n\t\t\t\"scaleReads\": \"master\",\n\t\t\t\"startupNodes\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"enableStartupNodes\": false,\n\t\t\"sentinelOptions\": {\n\t\t\t\"tls\": {\n\t\t\t\t\"key\": null,\n\t\t\t\t\"keyBookmark\": null,\n\t\t\t\t\"ca\": null,\n\t\t\t\t\"caBookmark\": null,\n\t\t\t\t\"cert\": null,\n\t\t\t\t\"certBookmark\": null\n\t\t\t},\n\t\t\t\"preferredSlaves\": [\n\t\t\t\t{\n\t\t\t\t\t\"host\": null,\n\t\t\t\t\t\"port\": null,\n\t\t\t\t\t\"priority\": null\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"role\": null,\n\t\t\t\"sentinelPassword\": null,\n\t\t\t\"name\": null\n\t\t},\n\t\t\"enablePreferredSlaves\": false,\n\t\t\"sshKeyPassphrase\": null,\n\t\t\"sshKeyFileBookmark\": null,\n\t\t\"sshKeyFile\": null,\n\t\t\"sshUser\": null,\n\t\t\"sshPort\": null,\n\t\t\"sshHost\": null,\n\t\t\"ssh\": false,\n\t\t\"caCertBookmark\": null,\n\t\t\"caCert\": null,\n\t\t\"certificateBookmark\": null,\n\t\t\"certificate\": null,\n\t\t\"keyFileBookmark\": null,\n\t\t\"keyFile\": null,\n\t\t\"ssl\": false,\n\t\t\"default\": false,\n\t\t\"star\": false,\n\t\t\"totalDb\": 16,\n\t\t\"db\": 0,\n\t\t\"password\": \"vfsd\",\n\t\t\"color\": \"#4B5563\",\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 1111,\n\t\t\"keyPrefix\": null,\n\t\t\"id\": \"f99a5d6d-daf4-489c-885b-6f8e411adbc9\",\n\t\t\"type\": \"cluster\",\n\t\t\"name\": \"racompassCluster\"\n\t}\n]"
  },
  {
    "path": "tests/e2e/test-data/import-databases/rdm-certificates.json",
    "content": "[\n\t{\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 8102,\n\t\t\"name\": \"theSameBody1\",\n\t\t\"caCert\": {\n\t\t\t\"name\": \"testCaName\",\n\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----mockedCACertificate1-----END CERTIFICATE-----\"\n\t\t},\n\t\t\"clientCert\": {\n\t\t\t\"name\": \"testClientCertName\",\n\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----mockedClientCertificate1-----END CERTIFICATE-----\",\n\t\t\t\"key\": \"-----BEGIN PRIVATE KEY-----mockedClientKey1-----END PRIVATE KEY-----\"\n\t\t},\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 8101,\n\t\t\"name\": \"theSameBody2\",\n\t\t\"caCert\": {\n\t\t\t\"name\": \"testCaName2\",\n\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----mockedCACertificate1-----END CERTIFICATE-----\"\n\t\t},\n\t\t\"clientCert\": {\n\t\t\t\"name\": \"testClientCertName2\",\n\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----mockedClientCertificate1-----END CERTIFICATE-----\",\n\t\t\t\"key\": \"-----BEGIN PRIVATE KEY-----mockedClientKey1-----END PRIVATE KEY-----\"\n\t\t},\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 8103,\n\t\t\"name\": \"theSameName\",\n\t\t\"caCert\": {\n\t\t\t\"name\": \"testCaName\",\n\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----mockedCACertificate2-----END CERTIFICATE-----\"\n\t\t},\n\t\t\"clientCert\": {\n\t\t\t\"name\": \"testClientCertName\",\n\t\t\t\"certificate\": \"-----BEGIN CERTIFICATE-----mockedClientCertificate2-----END CERTIFICATE-----\",\n\t\t\t\"key\": \"-----BEGIN PRIVATE KEY-----mockedClientKey2-----END PRIVATE KEY-----\"\n\t\t},\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 8102,\n\t\t\"name\": \"theSameBody1Path\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/certsByPath/caPath.crt\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/certsByPath/clientPath.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/certsByPath/clientPath.key\",\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 8101,\n\t\t\"name\": \"theSameBody2Path\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/certsByPath/caSameBody.crt\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/certsByPath/clientSameBody.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/certsByPath/clientSameBody.key\",\n\t\t\"result\": \"success\"\n\t},\n\t{\n\t\t\"host\": \"localhost\",\n\t\t\"port\": 8103,\n\t\t\"name\": \"theSameNamePath\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/sameNameCerts/caPath.crt\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/sameNameCerts/clientPath.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/sameNameCerts/clientPath.key\",\n\t\t\"result\": \"success\"\n\t}\n]\n"
  },
  {
    "path": "tests/e2e/test-data/import-databases/rdm-full.json",
    "content": "[\n    {\n        \"host\": \"localhost\",\n        \"port\": 8100,\n\t\t\"name\": \"rdmHost+Port+Name\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"localhost\",\n\t\t\"port\": 8101,\n\t\t\"name\": \"rdmHost+Port+Name+Username+Password\",\n        \"username\": \"rdmUsername\",\n        \"auth\": \"rdmAuth\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"172.30.100.151\",\n        \"port\": 6379,\n\t\t\"name\": \"rdmHost+Port+Name+ClusterTrue\",\n\t\t\"cluster\": true,\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"172.30.100.151\",\n        \"port\": 6379,\n\t\t\"name\": \"rdmHost+Port+Name+ClusterFalse\",\n\t\t\"cluster\": false,\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmHost+Port+Name+Index\",\n\t\t\"db\": 2,\n        \"result\": \"success\",\n        \"indName\": \"rdmHost+Port+Name+Index [2]\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmHost+Port+Name+otherFields\",\n        \"ssh_port\": 22,\n        \"timeout_connect\": 60000,\n        \"timeout_execute\": 60000,\n\t\t\"other_field\": \"inv\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmHost+Port+Name+CaCert\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/ca.crt\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmHost+Port+Name+clientCert+privateKey\",\n\t\t\"ssl\": true,\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/client.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/client.key\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmHost+Port+Name+CaCert+clientCert+privateKey\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/ca.crt\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/client.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/client.key\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmHost+Port+Name+CaCert+clientCert+privateKey(notbypath)\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/ca.crt\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/client.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/client.key\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"172.30.100.103\",\n        \"port\": 6379,\n\t\t\"name\": \"rdmHost+Port+Name+username+pass+CaCert+clientCert+privateKey\",\n\t\t\"ssl\": true,\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/ca.crt\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/client.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/client.key\",\n\t\t\"username\": \"admin\",\n        \"auth\": \"pass\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"redis-13237.c263.us-east-1-2.ec2.cloud.redislabs.com\",\n        \"port\": 13237,\n\t\t\"name\": \"rdmHost+Port+Name CloudDb\",\n        \"auth\": \"fRVdFcJftYnVoJmTCuiAPW6yu9qimXA1\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"3.85.88.37\",\n        \"port\": 14547,\n\t\t\"name\": \"rdmHost+Port+Name Cluster+CloudDb\",\n        \"auth\": \"Admin123!\",\n        \"result\": \"success\"\n    },\n\t{\n        \"host\": \"172.30.100.201\",\n        \"port\": 6379,\n\t\t\"name\": \"rdmHost+Port+Name Sentinel\",\n        \"auth\": \"defaultpass\",\n        \"result\": \"success\"\n    },\n    {\n        \"id\": \"192b55ac-3ade-4787-8dcf-0dc2bc0d20f3\",\n        \"name\": \"v1Host+Port+Name Sentinel\",\n        \"host\": \"172.30.100.200\",\n        \"port\": 26379,\n        \"username\": null,\n        \"password\": \"defaultpass\",\n        \"tls\": false,\n        \"verifyServerCert\": false,\n        \"connectionType\": \"SENTINEL\",\n        \"sentinelMaster\": {\n            \"name\": \"sent-5-primary1\",\n            \"username\": \"\",\n            \"password\": \"defaultpass\"\n        },\n        \"result\": \"success\"\n    },\n    {\n        \"id\": \"7f3413d7-7dda-4944-aa46-9b46e338f068\",\n        \"name\": \"v1SentinelWithCerts\",\n        \"host\": \"172.30.100.190\",\n        \"port\": 26379,\n        \"username\": null,\n        \"password\": \"defaultpass\",\n        \"tls\": true,\n        \"verifyServerCert\": true,\n        \"connectionType\": \"SENTINEL\",\n        \"sentinelMaster\": {\n            \"name\": \"tls-client-primary1\",\n            \"username\": \"\",\n            \"password\": \"defaultpass\"\n        },\n        \"clientCert\": {\n            \"name\": \"client\",\n            \"certificate\": \"-----BEGIN CERTIFICATE-----\\nMIIEzTCCArWgAwIBAgIUALiX/81ndGTG8UCPzu8r4Ev2IhIwDQYJKoZIhvcNAQEL\\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTQz\\nNTAzWhcNMzExMDI2MTQzNTAzWjANMQswCQYDVQQGEwJBVTCCAiIwDQYJKoZIhvcN\\nAQEBBQADggIPADCCAgoCggIBAKOod8jpFXqjtNvl0FgIkg0fSZbzvh7jbI7TEUVQ\\nmyeZxjmB3fZh5f6dxM7TZ048CUOeUeq3lemDqay+Moku0rL4PsFNe8z1C1zHuhf9\\n4Qw/f7rMBIZ73L4Y/7cPWfjZbeme06+D7HMBZGTWGHZCWrqZQOwA3hKBjC3VY/a5\\nz6oP78+w18WDpnavGwXwgCd1yTOwz3tVJUOcJdjGv3iwrHABcGVfxUEKTabP+p6V\\nHA/+w4AlCloS57GQCh0RWCXMyfekv6MGBaqQa6GtOK5ScLJ1YSlJ6PRoK2N+shbw\\nL/kQGlilgYBVGOQgNKd94+PwJgOCy72S7p9yF3ZTBB4/51Bwl7IV74Om/GmqzJMx\\nxY9/PPaxKlOkP+dW41/IrcDULdh0jAfe9rKdFf9/9NWA37S68pKFpzRuRrpLqIwm\\nBPtHvtLnTbhgmS/O1Rwmxqs8r+VA6D8+/drAor/KAcCwgRiYLvhvl4ABoqj4toEK\\njCXAR/jeoLAb8HDBzkot4hhJPjMhQMYX9/HfdK4YX359EkHdsO/+R6+ImXb68DS5\\nzh0028ktMM+KEhWSffSmU3imZOrH1/TQfSxfzuTHvyd0HXAHvzx+w1VWNK4fqU8O\\ntDbMt1GAaatrfrqwP4qTjzLEqtlJLIjg4qgzpYCRUvgVdxyeii9o7IeYT8I6Penf\\nQpAJAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIB\\nABb+A9C1AqstP5d2HXS/pIef1BNYAV8A/D2+nUpEXiautmjRZBRNwzBX2ffEZWV7\\nEMkvqrRwz2ZgOJ4BRzJiDIYoF8dOhd0lc/0PqoR0xVzjOFDqd0ZcPHAjaY3UoBE7\\njQSQ6ccc1tY5peNLAWCvRO6V9yhdV/SKGhveXGl/24MK9juwArnAitekCWZJQifT\\nCFOJX5UvifrT8s0v0AqkycaNpkMvl0BAl4DRDJ3+EwZmzfOdATawyXBVXHt1Gz+N\\niskPJAJsIjEdFYTjDUzwRN3bHFbTRXt2v1U18YIvMjvxq8MlITEC2lEW+3Xu90d3\\naE/N9mLNJCgmZ2CGywWoaJlUXix2LTo5kT5coVVx0HK0tg5EcBua05qM3xO9Rgxv\\nHkCnm/jMeN4oQ5o7h+q7UQja8mg1bjCzlt+RxqoA1snjglra/h5I8TTEhvSfxEy7\\nh5Wiwne/TH/e8fN1IYRDvv602MNSZnAEPyG3Hc5xQOSGNpoKOZG7tpU+mRYIvlPe\\nJgA5WNZ83y25JqSxF3kQuk7vrLByzEByqV3j+jIAQiHu/qIXwXUpkoV3L6A18yx/\\nTbpQasr/bRFZKe83WlNl2ASAVyubal8ocmA0ua24/RV0I0VOCEXiIkl+pZ6e5Qn4\\nL6Tryy5NxaEpUAZ9yv3P75PfNVQ3+vGYi3BLuhZUf/Dd\\n-----END CERTIFICATE-----\",\n            \"key\": \"-----BEGIN PRIVATE KEY-----\\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCjqHfI6RV6o7Tb\\n5dBYCJINH0mW874e42yO0xFFUJsnmcY5gd32YeX+ncTO02dOPAlDnlHqt5Xpg6ms\\nvjKJLtKy+D7BTXvM9Qtcx7oX/eEMP3+6zASGe9y+GP+3D1n42W3pntOvg+xzAWRk\\n1hh2Qlq6mUDsAN4SgYwt1WP2uc+qD+/PsNfFg6Z2rxsF8IAndckzsM97VSVDnCXY\\nxr94sKxwAXBlX8VBCk2mz/qelRwP/sOAJQpaEuexkAodEVglzMn3pL+jBgWqkGuh\\nrTiuUnCydWEpSej0aCtjfrIW8C/5EBpYpYGAVRjkIDSnfePj8CYDgsu9ku6fchd2\\nUwQeP+dQcJeyFe+DpvxpqsyTMcWPfzz2sSpTpD/nVuNfyK3A1C3YdIwH3vaynRX/\\nf/TVgN+0uvKShac0bka6S6iMJgT7R77S5024YJkvztUcJsarPK/lQOg/Pv3awKK/\\nygHAsIEYmC74b5eAAaKo+LaBCowlwEf43qCwG/Bwwc5KLeIYST4zIUDGF/fx33Su\\nGF9+fRJB3bDv/keviJl2+vA0uc4dNNvJLTDPihIVkn30plN4pmTqx9f00H0sX87k\\nx78ndB1wB788fsNVVjSuH6lPDrQ2zLdRgGmra366sD+Kk48yxKrZSSyI4OKoM6WA\\nkVL4FXccnoovaOyHmE/COj3p30KQCQIDAQABAoICADAiwPiq9dJYjD2RXrJF8w9B\\nAJgRoP3cznVDx3SnvLrtE8yeUfbB3LADH3vl2iC8r8zfqCBtVv6T5zgTyTFoQDi7\\no1mfvKYP/QORCz87QRIlKyB6GWqky8xt9eiV71SuPxHT0Vdyaf15j1nJTvCZm63+\\nnYXMy4SN7fkdJoXPKTFP9q0TyqMhkbie0Efy8P6qOj+l5aDU7lzwdIFKE88fx9g5\\n1CE9BfuXWDeUPJagLNzXhhEO0/iiTtt/Djp2e4LCtTTNlEAS6V+9kqq/FEjRnqwe\\nsjE+t/ILIZfmD+OHSdTr05P3OhvQ671Na69H69uDKuslcV+U8/KZ0CTRTgjHqvUZ\\neLNC8BZfAk8IZx637/rSlqPmxyS/j35vdslebTbWV2KM7jXPqSb9YokdoJ6M0NZX\\nIYiMK2reVzjy2YvX1Nhp4Xn68il10XVS4P9tFxyNWdTclCbuSlTfgc27ercQMMgY\\nfe7/8+A/QhV8tdly8W3HwTmvkmmWRSTMziI+zQzZmYYlAWb33rQYfMoHs4tEf2u2\\nRf0Oso56X73sc3ncnOFm+s5iwTeUH6EgF3ephJX4nR3canmtpy40nbXUJ+tAuaAj\\nuo56KNlPxIHKf96o2LGXGTrgbH39f0MebWOq/7YjtCg6sUbwuyyG3afLTHHuss13\\n5bTJ5gD3rsiGUWjfY3oBAoIBAQDRR/BnDw501Hky32/Vhlt7Ef1iplru+Fh0yQj7\\n2DQ+U+L1Ir4Q67ESH8qDnjkyLP1a8BDNOIEEGp5dBb+OHb/rwdb+RZ7OCIzFCQ/d\\nWR7m0ucuPBQwytQb7iXa9w0umZwoeTXEGP8aGe+bSBIHv8/em26rkSx0A1rxr2/O\\n1ho8xxgBmOxL3NSCnv56JUu/W0vFq/7OfWQ19SOvFahp3TeqR1gkHe76teWv11Pj\\n+RdiIIdCOifWChZPEdgMZD4rl1cs9QQb+n+WkRt/mZgtTIRQIe+we+vIha7TW46X\\n6A1DjSxV4WUSXvv10heYYpZkKzpNG9YOhRB3bvyDkRy11XZ5AoIBAQDIMUETtoa9\\nEFNY+uieZwJCTWrrB1njLLRZS3eCAKsVegHD0txLG8H5VMkyZQErRe52zR9QXWU/\\nU80tIO5BTbP3ME2AbjJvMwuiEe1lBKlVnn2JSGjbtzUMa1QBvDRmBEZkr8OneMN6\\np2tX3L3Vw8Xm/97rjkAgo3gQkqyDf6VZ4xvH2Wo405yMywcoifMZXo/PN9fI5V8S\\nfi3XjHrHzaY4cucbdaezVb4Zd0xwl+c6Ifw6+VtmRyfCEHk8yvSkoKWqdxtD0p3a\\n3e8txYoI/YZltAICZ2vjZPv05Ts/VwWVzaxUArYiUH+k6J+6yCavKWesmeac0vLG\\nyN07gpRPPsIRAoIBADIp+UDqxf9REsAT+L2I2BK27DKiR3eyhZlwuruLRnKOLv+t\\nVTu/ExGSFzvXSERzrkMG+jAG1D4El2MaxqCtFtzO+Na4H2mpePydwHTBMPwJH6rg\\nccKES7VqLx6+SyWZYmn9K9sWVseN4fYpn1DGNHBad3ueb7ZbO4hlEfrVLTLWUjXH\\nzxQcGcA5liv3FqIGozH9mTUrr0KTwPrtyRGfGgGx2jnGBwuHYEf26D/j7Cv0Ohew\\n0u2mO1S2pT/LI2/VderrzBFcyQpxO9MpIOXyymBe0hJOkeTdzlsRPivBTrSbeT4Y\\nqd5ucByrQEahkwTtq6rh+jw+vwSx0MtElEotoZkCggEAB8ujNRlOdd5E4JokpMZu\\nGBbbqvtGTMpY24FMzgsonlV57B4x5drW2taqXwP/36eBea7TIVYBs02YF8HIhVJ5\\nR47h9bZU0G+0bEM2c1CTJ3pceRQQwT2JG0qyor6pa6+O7izJ+aOCOSx7yZgW7FQL\\nSMt96r5HUP4MltifTx+RWMa3NjkJId1boz/kr3dvt/UutGsARBpqcVXogxQ9U7p2\\nVoxi43bZaOpV1LgIifngTysznzhGjt0Gd1Ac6HkevapjyReKQEHbU8KApc+jaGY2\\n7Y7s5RsR4HD2PrsOa5D/7q1roHnajcuErO9CCQvyNa/vEZGMoV61hXgc5UxYah2P\\ngQKCAQEAkzISMmGPyQT7t6F/P2dFmrotAUU8gsEaWhrlkS0AuREXv1p14I1OnQhS\\neWU7I9qSG4NfslRi5WUnowyawQKYibShtJ9/tOWMTaEELVTDtPAIu2y9kcquiG2j\\no34vfpByz0w1vhmd/hwcPAvBFV+oaGN6lPz9Pv9MlNBLJoMhCPdr3aBJJuThT1Ka\\nJQ/RT0XfU7XXSC74x7JwoKB4bobVHdON09yielC6w9wq9anqD18nrz/4wBwWDhDE\\nKPxeXVpnIZfhukmWxkBY8NLAOFEenS3f6D4wzuOD25mPRSJQTngh7w9XkZYzDnOo\\niwa43+YOKJx4Qh4SeXLBc/Udm1eMTA==\\n-----END PRIVATE KEY-----\"\n        },\n        \"caCert\": {\n            \"name\": \"ca\",\n            \"certificate\": \"-----BEGIN CERTIFICATE-----\\nMIIFHzCCAwegAwIBAgIUKeAfHPO6uJBW+s8fY2cWKOc+DfgwDQYJKoZIhvcNAQEL\\nBQAwHzELMAkGA1UEBhMCQVUxEDAOBgNVBAMMB2V4YW1wbGUwHhcNMjExMDI4MTMy\\nNzI2WhcNMzExMDI2MTMyNzI2WjAfMQswCQYDVQQGEwJBVTEQMA4GA1UEAwwHZXhh\\nbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANF9A4aeHru2fX1j\\nU+Bz9D2supYsMG64f+wXNrFPTMxPS/rdjNcqAWeVCknY7d8EO0uBf64Gm4ufQAPV\\nboINIdgoso9tGfl5LMaaiYq0aD5CK0wmU38pPbKA2Vr9bkrNIYLUFU6oPI7RJ5fL\\nPl/vbvHyaXQKcDd5xxusAu3Ytrylq3WaLNWwhT//WRor4SU2qt9s06PiOgCABY+D\\nolMXI72gDaehRhnbOVXc6GadlHCsE5GHYJ3WcLLY0rGEdlwphcEG5TRVHGBiHOg/\\nJ0vsiuhwTLyRqQq5L6eFm33d4aRI9JLY8LlU5ywGiVoNl+fFdQr3ovWw7eObQSbg\\nBuOJhQBTpEmiPgiOC3kAUUrgT/uGS1x9RX+Wj0sY6zs+qOkfhFAcScXQBeZSLNT9\\nRYAjZQOTtTQYVwH8NcF2MlwI3tb3qk2+2Xby4YfTHxp42B8IHkedwfFzrwfUDnNM\\nCm3GSVtDGv/j4/7fp0oZZROpd5+h1wRhR/HO08rkpwuobo6xGRrrxqbdlsid3OB4\\nKk92Wl8reccxyr2a/7OlrWk284xpQI/nlU6a8bByJp2eTPYuNJFfJkrqua94YOJy\\nK4d4tLNbQ4X/5g12unGEHg8/HVNHJjCKiU2Gwxhxm50EqmgdgaboDmf+GuVF0tL1\\nkGPbbjSrlt2pS+Tdza9EJfmAzVppAgMBAAGjUzBRMB0GA1UdDgQWBBQWqqhnKa+s\\n5ToC0TYKlIdfytda/jAfBgNVHSMEGDAWgBQWqqhnKa+s5ToC0TYKlIdfytda/jAP\\nBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQATnfNftaIalbJmNHrq\\n7KIxyAAEimiIvci5i1f2X8ZB4XY1TWcf0eQ/+oJW9E2Kj83yGaAqKg1FFP8ugquG\\npxzMyljTlQi3cWp58ByaYg2XMEyRY5FtbcdbHHJgdM2PSHsegWN/vfJw3H3J2673\\nJ6Kc/69IwJcE+aYDbLa/cnnRBn9AKHdtNoRmPH+s162oo8I86LQuG942Hx2CHCjt\\nttkLwVBtsBKc5hRaPR+Psx68jS41iHJGHUwHNcPl7llBAQe8kKNg4GHJWT5vh7rd\\nrw4jAGCsoaE5Ol1HyRDprpdcC4o+eQbhMrjMcFzYduuCx1F96BlSXA2mQe+9lD08\\nLzdS35ILmSqCTbtOcdHCmnjWp9fhl5mIrJ+I3G33QPHaHJXBfWGNidxjkibwdVxK\\neNAOv4lEMCoVQ0occzNyUluJQUFJyvXtXWFSErRH6b78Gsc/AvPijPbSNuT8hRK9\\nGC3yRYltDFXcr4+2lxJyoQoR6Y8399oaJm4U17fOIwlM+iI8dT8x+qsT8brw+5kk\\noKu4jz8jfkZrUF8V8hIfAUc08IvMAmDwvIMeAjXFmDBEECxXBGRw2hTcY/53Nfdt\\nPRWzaL/YtKOy/9UfJQKs3Ihqte59/v3EJO9x9vTLmcpoCgh9piNVgD6OS38cOiEa\\nsnS90+qMig9Gx3aJ+UvktWcp3Q==\\n-----END CERTIFICATE-----\"\n        },\n        \"result\": \"success\"\n    },\n\t{\n        \"port\": 8100,\n\t\t\"name\": \"rdmnoHost\",\n        \"result\": \"failed\"\n    },\n\t{\n        \"host\": \"localhost\",\n\t\t\"name\": \"rdmnoPort\",\n        \"result\": \"failed\"\n    },\n    {\n        \"name\": \"rdmnoHost+noPort\",\n        \"result\": \"failed\"\n    },\n\n\t{\n        \"host\": \"rdmLargeName\",\n\t\t\"port\": \"8101\",\n\t\t\"name\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur metus in libero pretium congue. Curabitur eget eleifend nibh, cursus tincidunt lorem. Vivamus magna erat, vestibulum at sem et, bibendum volutpat velit. Cras dapibus lorem quam, at efficitur mi sollicitudin id. Vivamus dapibus nec elit ut tincidunt. Sed porta tempus lorem id iaculis. Vestibulum ut arcu vitae massa dapibus egestas. Suspendisse ante tortor, tristique vel malesuada id, finibus et libero. Nulla suscipit libero.1\",\n        \"result\": \"failed\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 65536,\n\t\t\"name\": \"rdmLargePort\",\n        \"result\": \"failed\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmIndexNotNumber\",\n\t\t\"db\": \"dsad\",\n        \"result\": \"failed\"\n    },\n  {\n    \"compressor\": \"Invalid\",\n    \"host\": \"localhost\",\n    \"port\": 8102,\n    \"name\": \"rdmCompressorInvalid\",\n    \"result\": \"partial\"\n  },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8102,\n\t\t\"name\": \"rdmCaCertInvalidBody\",\n\t\t\"ssl_ca_cert_path\": \"invalid body\",\n        \"result\": \"partial\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmOnlyClientCert\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/client.crt\",\n        \"result\": \"partial\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmOnlyPrivateKey\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/client.key\",\n        \"result\": \"partial\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmInvalidClientCert\",\n\t\t\"ssl_local_cert_path\": \"invalid client cert\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/client.key\",\n        \"result\": \"partial\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmInvalidPrivateKey\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/client.crt\",\n\t\t\"ssl_private_key_path\": \"invalid private key\",\n        \"result\": \"partial\"\n    },\n\t    {\n        \"auth\": \"\",\n        \"db_scan_limit\": 21,\n        \"host\": \"127.0.0.1\",\n        \"keys_pattern\": \"*1\",\n        \"lua_keys_loading\": true,\n        \"name\": \"rdmInvalidAllCertificates\",\n        \"namespace_separator\": \":3\",\n        \"port\": 8100,\n        \"ssh_port\": 22,\n        \"ssl\": false,\n        \"ssl_ca_cert_path\": \"fdsafsadfsad\",\n        \"ssl_local_cert_path\": \"fsdfds\",\n        \"ssl_private_key_path\": \"fdsafasdf\",\n        \"timeout_connect\": 61000,\n        \"timeout_execute\": 61000,\n        \"result\": \"partial\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmCaCertInvalidPath\",\n\t\t\"ssl_ca_cert_path\": \"/test-data/certs/caInvalid.crt\",\n        \"result\": \"partial\"\n    },\n\t{\n        \"host\": \"localhost\",\n        \"port\": 8103,\n\t\t\"name\": \"rdmClientCert+PrivateKeyInvalidPathes\",\n\t\t\"ssl_local_cert_path\": \"/test-data/certs/clientInvalid.crt\",\n\t\t\"ssl_private_key_path\": \"/test-data/certs/clientInvalid.key\",\n        \"result\": \"partial\"\n    }\n]\n"
  },
  {
    "path": "tests/e2e/test-data/ssh/sshPrivateKey",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8AAAAKBv1saEb9bG\nhAAAAAtzc2gtZWQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8A\nAAAEDyew1DnmWamAr0OrUM87FauJfFfea+pi8ctpKNnurNi/XS0xkxY7o+MUNBJJnf6fKh\n6AFFpzB0YIfifHSSseXwAAAAG3pvem9Aem96by1IUC1Qcm9Cb29rLTQ1MC1HNwEC\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/test-data/ssh/sshPrivateKeyPasscode",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBPcEHCGN\nDrMHhpQnPwc0XwAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEs/ewkUXl0+uDr7\nhxSM2vURqdRNFHm7+x05azzW/YzuAAAAoEhNzctHXM6YBV0z4zzvdniQ5cLwsv8TfMZp2G\nWUhZU05yugvKlRu1pml5q3XGSP5wYCF4vvi4BE563PMDKZWAqFFGtiTotEn+XuD/eP+P8H\nxdf91tV5kE+1yvVwxUNMcijHY0uYopnG2NN3bdjOH/4YmW0WLyDu10EoMZKVnrP0qBbOrR\nxKIy5lqa39SrAnUnGSoTEJsEWGLiIS2rBhkVc=\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/e2e/test-data/sshPrivateKeys.ts",
    "content": "export const sshPrivateKey = `\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8AAAAKBv1saEb9bG\nhAAAAAtzc2gtZWQyNTUxOQAAACD10tMZMWO6PjFDQSSZ3+nyoegBRacwdGCH4nx0krHl8A\nAAAEDyew1DnmWamAr0OrUM87FauJfFfea+pi8ctpKNnurNi/XS0xkxY7o+MUNBJJnf6fKh\n6AFFpzB0YIfifHSSseXwAAAAG3pvem9Aem96by1IUC1Qcm9Cb29rLTQ1MC1HNwEC\n-----END OPENSSH PRIVATE KEY-----`;\n\nexport const sshPrivateKeyWithPasscode = `\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBPcEHCGN\nDrMHhpQnPwc0XwAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEs/ewkUXl0+uDr7\nhxSM2vURqdRNFHm7+x05azzW/YzuAAAAoEhNzctHXM6YBV0z4zzvdniQ5cLwsv8TfMZp2G\nWUhZU05yugvKlRu1pml5q3XGSP5wYCF4vvi4BE563PMDKZWAqFFGtiTotEn+XuD/eP+P8H\nxdf91tV5kE+1yvVwxUNMcijHY0uYopnG2NN3bdjOH/4YmW0WLyDu10EoMZKVnrP0qBbOrR\nxKIy5lqa39SrAnUnGSoTEJsEWGLiIS2rBhkVc=\n-----END OPENSSH PRIVATE KEY-----`;\n"
  },
  {
    "path": "tests/e2e/test-data/triggers-and-functions/invoke_function.txt",
    "content": "#!js api_version=1.0 name=lib\n\nredis.registerFunction('function', function(client, word1, word2, w3rd1){\n\n    return '${word1 ${word2} ${word3}';\n});\n"
  },
  {
    "path": "tests/e2e/test-data/triggers-and-functions/library.txt",
    "content": "#!js api_version=1.0 name=lib\nvar last_update_field_name = '__last_update__'\n\nif (redis.config.last_update_field_name !== undefined) {\n    if (typeof redis.config.last_update_field_name != 'string') {\n        throw \"last_update_field_name must be a string\";\n    }\n    last_update_field_name = redis.config.last_update_field_name\n}\n\nredis.registerFunction(\"function\", function(client, key, field, val){\n    // get the current time in ms\n    var curr_time = client.call(\"time\")[0];\n    return client.call('hset', key, field, val, last_update_field_name, curr_time);\n});\n"
  },
  {
    "path": "tests/e2e/test-data/upload-json/sample.json",
    "content": "{\n  \"product\": \"Live JSON generator\",\n  \"version\": 3.1,\n  \"releaseDate\": \"2014-06-25T00:00:00.000Z\",\n  \"demo\": true,\n  \"person\": {\n    \"id\": 12345,\n    \"name\": \"John Doe\",\n    \"phones\": {\n      \"home\": \"800-123-4567\",\n      \"mobile\": \"877-123-1234\"\n    },\n    \"email\": [\n      \"jd@example.com\",\n      \"jd@example.org\"\n    ],\n    \"dateOfBirth\": \"1980-01-02T00:00:00.000Z\",\n    \"registered\": true,\n    \"emergencyContacts\": [\n      {\n        \"name\": \"Jane Doe\",\n        \"phone\": \"888-555-1212\",\n        \"relationship\": \"spouse\"\n      },\n      {\n        \"name\": \"Justin Doe\",\n        \"phone\": \"877-123-1212\",\n        \"relationship\": \"parent\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplAllKeyTypes.txt",
    "content": "HSET hashkey1 'field' 'value'\nLPUSH listkey1 'element'\nSADD setkey1 'member'\nZADD zsetkey1 1 'member'\nSET stringkey1 'value'\nJSON.SET jsonkey1 . '1'\nXADD streamkey1 * 'field' 'value'\nGRAPH.QUERY graphkey1 \"CREATE ()\"\nTS.CREATE tskey1"
  },
  {
    "path": "tests/e2e/test-data/upload-tutorials/customTutorials/_upload/bulkUplString.txt",
    "content": "SET stringkey1test 'value'"
  },
  {
    "path": "tests/e2e/test-data/upload-tutorials/customTutorials/folder-1/probably-1.md",
    "content": "In very broad terms probabilistic data structures (PDS) allow us to get to a \"close enough\" result in a much shorter time and by using significantly less memory.\n\nRelative path button:\n\n```redis-upload:[../_upload/bulkUplAllKeyTypes.txt] Upload relative\n```\n\nRelative path long name button:\n\n```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname longname\n```\n\nAbsolute path button:\n\n```redis-upload:[/_upload/bulkUplString.txt] Upload absolute\n```\n\nExternal:\n\n![Redis Insight screen external](https://github.com/RedisInsight/RedisInsight/blob/main/.github/redisinsight_browser.png?raw=true)\n\nInvalid absolute path button:\n\n```redis-upload:[/_upload/bulkUplAllKeyTypes] Invalid absolute\n```\n\nInvalid relative path button:\n\n```redis-upload:[../../_upload/bulkUplAllKeyTypes.txt] Invalid relative\n```\n\nRedis Stack supports 4 of the most famous PDS:\n- Bloom filters\n- Cuckoo filters\n- Count-Min Sketch\n- Top-K\n\nIn the rest of this tutorial we'll introduce how you can use a Bloom filter to save many heavy calls to the relational database, or a lot of memory, compared to using sets or hashes.\nA Bloom filter is a probabilistic data structure that enables you to check if an element is present in a set using a very small memory space of a fixed size. **It can guarantee the absence of an element from a set, but it can only give an estimation about its presence**. So when it responds that an element is not present in a set (a negative answer), you can be sure that indeed is the case. However, one out of every N positive answers will be wrong.\nEven though it looks unusual at a first glance, this kind of uncertainty still has its place in computer science. There are many cases out there where a negative answer will prevent very costly operations;\n\nHow can a Bloom filter be useful to our bike shop? For starters, we could keep a Bloom filter that stores all usernames of people who've already registered with our service. That way, when someone is creating a new account we can very quickly check if that username is free. If the answer is yes, we'd still have to go and check the main database for the precise result, but if the answer is no, we can skip that call and continue with the registration. \n\nAnother, perhaps more interesting example is for showing better and more relevant ads to users. We could keep a bloom filter per user with all the products they've bought from the shop, and when we get a list of products from our suggestion engine we could check it against this filter.\n\n\n```redis Add all bought product ids in the Bloom filter\nBF.MADD user:778:bought_products  4545667 9026875 3178945 4848754 1242449\n```\n\nJust before we try to show an ad to a user, we can first check if that product id is already in their \"bought products\" Bloom filter. If the answer is yes - we might choose to check the main database, or we might skip to the next recommendation from our list. But if the answer is no, then we know for sure that our user hasn't bought that product:\n\n```redis Has a user bought this product?\nBF.EXISTS  user:778:bought_products 1234567  // No, the user has not bought this product\nBF.EXISTS  user:778:bought_products 3178945  // The user might have bought this product\n```\n"
  },
  {
    "path": "tests/e2e/test-data/upload-tutorials/customTutorials/folder-2/vector-2.md",
    "content": "In very broad terms probabilistic data structures (PDS) allow us to get to a \"close enough\" result in a much shorter time and by using significantly less memory.\n\n[linkTheSamePage](redisinsight:_?tutorialId=ds-json-create)\n\n[link2AnalyticsPageWithTutorial](redisinsight:analytics/database-analysis?tutorialId=ds-json-intro)\n\n[link3InvalidPage](redisinsight:invalidPage?tutorialId=ds-json-intro)\n\n[link4InvalidTutorial](redisinsight:invalidPage?tutorialId=invalid-tutorial)\n\n[link5JustAnalyticsPage](redisinsight:analytics/database-analysis)\n\n[link6JustTheSamePage](redisinsight:_)\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/a-first-start-form/autodiscovery.e2e.ts",
    "content": "import { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, workingDirectory } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport {fixture, test} from 'testcafe'\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\n\nconst standalonePorts = [8100, 8101, 8102, 8103, 12000];\nconst otherPorts = [28100, 8200];\n\n// TODO: Other tests are messing up the state of this test for now seams like deleting the folder  will do as workaround\nconst cleanBeforeClientStarts = async () => {await Common.deleteFolderIfExists(workingDirectory)}\ncleanBeforeClientStarts().then(async () => {\n\n    fixture `Autodiscovery`\n        .meta({ type: 'critical_path', rte: rte.none })\n        .page(commonUrl)\n        .beforeEach(async(t) => {\n\n            await databaseHelper.acceptLicenseTerms();\n        });\n\n    test\n        .after(async() => {\n            // Delete all auto-discovered databases\n            await databaseAPIRequests.deleteAllDatabasesApi();\n        })('Verify that when users open application for the first time, they can see all auto-discovered Standalone DBs', async t => {\n            // Check that standalone DBs have been added into the application\n            const n = await myRedisDatabasePage.dbNameList.count;\n            for(let k = 0; k < n; k++) {\n                const name = await myRedisDatabasePage.dbNameList.nth(k).textContent;\n                console.log(`AUTODISCOVERY ${k}: ${name}`);\n            }\n            // Verify that user can see all the databases automatically discovered with 127.0.0.1 host instead of localhost\n            for(let i = 0; i < standalonePorts.length; i++) {\n                await t.expect(myRedisDatabasePage.dbNameList.withExactText(`127.0.0.1:${standalonePorts[i]}`).exists).eql(true, `Standalone DBs is not found for ${standalonePorts[i]}`);\n            }\n            // Check that Sentinel and OSS cluster have not been added into the application\n            for(let j = 0; j < otherPorts.length; j++) {\n                await t.expect(myRedisDatabasePage.dbNameList.withExactText(`127.0.0.1:${otherPorts[j]}`).exists).notOk('Sentinel and OSS DBs');\n            }\n        });\n\n})\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/a-first-start-form/user-agreements-form.e2e.ts",
    "content": "import { commonUrl } from '../../../../helpers/conf';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { Common } from '../../../../helpers/common';\nimport { rte } from '../../../../helpers/constants';\nimport { UserAgreementDialog } from '../../../../pageObjects/dialogs';\n\nconst userAgreementDialog = new UserAgreementDialog();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\n\nfixture `Agreements Verification`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .page(commonUrl)\n    .requestHooks(Common.mockSettingsResponse())\ntest('Verify that user should accept User Agreements to continue working with the application', async t => {\n        await t.expect(userAgreementDialog.userAgreementsPopup.exists).ok('User Agreements Popup is shown');\n        // Verify that I still has agreements popup & cannot add a database\n        await t.expect(userAgreementDialog.submitButton.hasAttribute('disabled')).ok('Submit button not disabled by default');\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton.exists).notOk('User can\\'t add a database');\n    });\ntest('Verify that the encryption enabled by default and specific message', async t => {   \n    await t.expect(userAgreementDialog.pluginSectionWithText.exists).ok('Plugin text is not displayed');\n    // Verify that text that is displayed in window is 'While adding new visualization plugins, use files only from trusted authors to avoid automatic execution of malicious code.'\n    // unskip the verification when encription will be fixed for test builds\n    // // Verify that encryption enabled by default\n    // await t.expect(userAgreementDialog.switchOptionEncryption.withAttribute('aria-checked', 'true').exists).ok('Encryption enabled by default');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/analysis-tools/recommendations.e2e.ts",
    "content": "import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage } from '../../../../pageObjects';\nimport { RecommendationIds, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneV5Config\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { RecommendationsActions } from '../../../../common-actions/recommendations-actions';\n\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst recommendationsActions = new RecommendationsActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst luaScriptRecommendation = RecommendationIds.luaScript;\nconst useSmallerKeysRecommendation = RecommendationIds.useSmallerKeys;\nconst redisVersionRecommendation = RecommendationIds.redisVersion;\n\nfixture `Database Analysis Recommendations`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        // Add cached scripts and generate new report\n        await memoryEfficiencyPage.Cli.addCachedScripts(11);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Go to Recommendations tab\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('SCRIPT FLUSH');\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig);\n    })('Recommendations displaying', async t => {\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10\n        await t.expect(await memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).exists)\n            .ok('Avoid dynamic lua script recommendation not displayed');\n        // Verify that user can see type of recommendation badge\n        await t.expect(memoryEfficiencyPage.getRecommendationLabelByName(luaScriptRecommendation, 'code').exists)\n            .ok('Avoid dynamic lua script recommendation not have Code Changes label');\n        await t.expect(memoryEfficiencyPage.getRecommendationLabelByName(luaScriptRecommendation, 'configuration').exists)\n            .notOk('Avoid dynamic lua script recommendation have Configuration Changes label');\n\n        // Verify that user can see Use smaller keys recommendation when database has 1M+ keys\n        await t.expect(await memoryEfficiencyPage.getRecommendationByName(useSmallerKeysRecommendation).exists).ok('Use smaller keys recommendation not displayed');\n\n        // Verify that user can see all the recommendations expanded by default\n        await t.expect(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation).getAttribute('aria-expanded'))\n            .eql('true', 'Avoid dynamic lua script recommendation not expanded');\n        await t.expect(memoryEfficiencyPage.getRecommendationButtonByName(useSmallerKeysRecommendation).getAttribute('aria-expanded'))\n            .eql('true', 'Use smaller keys recommendation not expanded');\n\n        // Verify that user can expand/collapse recommendation\n        const expandedTextContaiterSize = await memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight;\n        await t.click(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation));\n        await t.expect(memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight)\n            .lt(expandedTextContaiterSize, 'Lua script recommendation not collapsed');\n        await t.click(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation));\n        await t.expect(memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight)\n            .eql(expandedTextContaiterSize, 'Lua script recommendation not expanded');\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n        // Go to Analysis Tools page and create new report and open recommendations\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n    }).after(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config);\n    })('Verify that user can upvote recommendations', async t => {\n        const notUsefulVoteOption = 'not useful';\n        const usefulVoteOption = 'useful';\n        await recommendationsActions.voteForRecommendation(redisVersionRecommendation, notUsefulVoteOption);\n        // Verify that user can rate recommendations with one of 2 existing types at the same time\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecommendation, notUsefulVoteOption);\n\n        // Verify that user can see the popup with link when he votes for “Not useful”\n        await recommendationsActions.verifyVotePopUpIsDisplayed(redisVersionRecommendation, notUsefulVoteOption);\n\n        // Verify that user can see previous votes when reload the page\n        await memoryEfficiencyPage.reloadPage();\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecommendation, notUsefulVoteOption);\n\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await recommendationsActions.voteForRecommendation(redisVersionRecommendation, usefulVoteOption);\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecommendation, usefulVoteOption);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/browser/bulk-delete.e2e.ts",
    "content": "import { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { deleteAllKeysFromDB } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyNames = [Common.generateWord(20), Common.generateWord(20)];\nconst dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port };\n\nfixture `Bulk Delete`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.addHashKey(keyNames[0], '100000', Common.generateWord(20), Common.generateWord(20));\n        await browserPage.addSetKey(keyNames[1], '100000', Common.generateWord(20));\n        if (await browserPage.Toast.toastCloseButton.exists) {\n            await t.click(browserPage.Toast.toastCloseButton);\n        }\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await deleteAllKeysFromDB(dbParameters.host, dbParameters.port);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that when bulk deletion is completed, status Action completed is displayed', async t => {\n    // Filter by Hash keys\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.expect(browserPage.BulkActions.bulkStatusCompleted.exists).ok('Bulk deletion not completed', { timeout: 15000 });\n    await t.expect(browserPage.BulkActions.bulkStatusCompleted.textContent).eql('Action completed', 'Action completed text is not visible');\n    // Verify that when bulk deletion is completed, button Delete changes to Start New\n    await t.expect(browserPage.BulkActions.bulkStartAgainButton.exists).ok('\"Start New\" button not displayed');\n    // Verify that user can click on Start New and existed filter will be applied\n    await t.click(browserPage.BulkActions.bulkStartAgainButton);\n    await t.expect(browserPage.BulkActions.bulkDeleteSummary.innerText).contains('Scanned 100% (2/2) and found 1 keys', 'Bulk delete summary is not correct');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/browser/bulk-upload.e2e.ts",
    "content": "import * as path from 'path';\nimport { t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port };\nconst filesToUpload = ['bulkUplAllKeyTypes.txt', 'bigKeysData.rtf'];\nconst filePathes = {\n    allKeysFile: path.join('..', '..', '..', '..', 'test-data', 'bulk-upload', filesToUpload[0]),\n    bigDataFile: path.join('..', '..', '..', '..', 'test-data', 'bulk-upload', filesToUpload[1])\n};\nconst keyNames = ['hashkey1', 'listkey1', 'setkey1', 'zsetkey1', 'stringkey1', 'jsonkey1', 'streamkey1', 'graphkey1', 'tskey1'];\nconst verifyCompletedResultText = async(resultsText: string[]): Promise<void> => {\n    for (const result of resultsText) {\n        await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).contains(result, 'Bulk upload completed summary not correct');\n    }\n    await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).notContains('0:00:00.000', 'Bulk upload Time taken not correct');\n};\n\nfixture `Bulk Upload`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await deleteAllKeysFromDB(dbParameters.host, dbParameters.port);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify bulk upload of different text docs formats', async t => {\n    // Verify bulk upload for electron app version\n    const allKeysResults = ['9Commands Processed', '9Success', '0Errors'];\n    const bigKeysResults = ['10 000Commands Processed', '10 000Success', '0Errors'];\n    const defaultText = 'Select or drag and drop a file';\n\n    // Open bulk actions\n    await t.click(browserPage.bulkActionsButton);\n    // Open bulk upload tab\n    await t.click(browserPage.BulkActions.bulkUpdateTab);\n    // Verify that Upload button disabled by default\n    await t.expect(browserPage.BulkActions.actionButton.hasAttribute('disabled')).ok('Upload button enabled without added file');\n\n    // Verify that keys of all types can be uploaded\n    await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile);\n    await verifyCompletedResultText(allKeysResults);\n    await browserPage.searchByKeyName('*key1');\n    await verifyKeysDisplayingInTheList(keyNames, true);\n\n    // Verify that Upload button disabled after starting new upload\n    await t.click(browserPage.BulkActions.bulkActionStartNewButton);\n    await t.expect(browserPage.BulkActions.actionButton.hasAttribute('disabled')).ok('Upload button enabled without added file');\n\n    // Verify that user can remove uploaded file\n    await t.setFilesToUpload(browserPage.BulkActions.bulkUploadInput, [filePathes.bigDataFile]);\n    await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(filesToUpload[1], 'Filename not displayed in upload input');\n    await t.click(browserPage.BulkActions.removeFileBtn);\n    await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(defaultText, 'File not removed from upload input');\n\n    // Verify that user can upload 10000 keys\n    await browserPage.BulkActions.uploadFileInBulk(filePathes.bigDataFile);\n    await verifyCompletedResultText(bigKeysResults);\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/database/add-ssh-db.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { BrowserPage, ClusterDetailsPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossClusterForSSHConfig, ossStandaloneForSSHConfig } from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { sshPrivateKey, sshPrivateKeyWithPasscode } from '../../../../test-data/sshPrivateKeys';\nimport { Common } from '../../../../helpers/common';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst clusterPage = new ClusterDetailsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserActions = new BrowserActions();\n\nconst sshParams = {\n    sshHost: '172.31.100.245',\n    sshPort: '2222',\n    sshUsername: 'u'\n};\nconst newClonedDatabaseAlias = 'Cloned ssh database';\nconst sshDbPass = {\n    ...ossStandaloneForSSHConfig,\n    databaseName: `SSH_${Common.generateWord(5)}`\n};\nconst sshDbPrivateKey = {\n    ...ossStandaloneForSSHConfig,\n    databaseName: `SSH_${Common.generateWord(5)}`\n};\nconst sshDbPasscode = {\n    ...ossStandaloneForSSHConfig,\n    databaseName: `SSH_${Common.generateWord(5)}`\n};\nconst sshDbClusterPass = {\n    ...ossClusterForSSHConfig,\n    databaseName: `SSH_Cluster_${Common.generateWord(5)}`\n};\n\nfixture `Adding database with SSH`\n    .meta({ type: 'critical_path'})\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .after(async() => {\n        // Delete databases\n        await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi([sshDbPass.databaseName, sshDbPrivateKey.databaseName, sshDbPasscode.databaseName, newClonedDatabaseAlias, sshDbClusterPass.databaseName]);\n    });\ntest.skip\n    .meta({ rte: rte.standalone })('Adding database with SSH', async t => {\n    const tooltipText = [\n        'Enter a value for required fields (3):',\n        'SSH Host',\n        'SSH Username',\n        'SSH Private Key'\n    ];\n    const hiddenPass = '••••••••••••';\n    const sshWithPass = {\n        ...sshParams,\n        sshPassword: 'pass'\n    };\n    const sshWithPrivateKey = {\n        ...sshParams,\n        sshPrivateKey: sshPrivateKey\n    };\n    const sshWithPassphrase = {\n        ...sshParams,\n        sshPrivateKey: sshPrivateKeyWithPasscode,\n        sshPassphrase: 'test'\n    };\n    // Verify that if user have not entered any required value he can see that this field should be specified when hover over the button to add a database\n    await t\n        .click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton)\n        .click(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton)\n        .click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab)\n        .click(myRedisDatabasePage.AddRedisDatabaseDialog.useSSHCheckbox)\n        .click(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyRadioBtn)\n        .hover(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButtonHover);\n    for (const text of tooltipText) {\n        await browserActions.verifyTooltipContainsText(text, true);\n    }\n    // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes\n    await t.hover(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtnHover);\n    for (const text of tooltipText) {\n        await browserActions.verifyTooltipContainsText(text, true);\n    }\n\n    // Verify that user can add SSH tunnel with Password for Standalone database\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbPass, sshWithPass);\n    await myRedisDatabasePage.clickOnDBByName(sshDbPass.databaseName);\n    await Common.checkURLContainsText('browser');\n\n    // Verify that user can add SSH tunnel with Private Key\n    await t.click(browserPage.OverviewPanel.myRedisDBLink);\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbPrivateKey, sshWithPrivateKey);\n    await myRedisDatabasePage.clickOnDBByName(sshDbPrivateKey.databaseName);\n    await Common.checkURLContainsText('browser');\n\n    // Verify that user can edit SSH parameters for existing database connections\n    await t.click(browserPage.OverviewPanel.myRedisDBLink);\n    await myRedisDatabasePage.clickOnEditDBByName(sshDbPrivateKey.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t\n        .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyInput, sshWithPassphrase.sshPrivateKey, { replace: true, paste: true })\n        .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.sshPassphraseInput, sshWithPassphrase.sshPassphrase, { replace: true, paste: true });\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton.exists).notOk('Edit database panel still displayed');\n    await databaseHelper.clickOnEditDatabaseByName(sshDbPrivateKey.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    // Verify that password, passphrase and private key are hidden for SSH option\n    await t\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyInput.textContent).eql(hiddenPass, 'Edited Private key not saved')\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPassphraseInput.value).eql(hiddenPass, 'Edited Passphrase not saved');\n\n    // Verify that user can clone database with SSH tunnel\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.generalTab);\n    // Edit Database alias before cloning\n    await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput, newClonedDatabaseAlias, { replace: true });\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(newClonedDatabaseAlias).exists).ok('DB with SSH was not cloned');\n\n    // Verify that user can add SSH tunnel with Passcode\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbPasscode, sshWithPassphrase);\n    await myRedisDatabasePage.clickOnDBByName(sshDbPasscode.databaseName);\n    await Common.checkURLContainsText('browser');\n});\ntest.skip\n    .meta({ rte: rte.ossCluster, skipComment: \"Unstable and will be affected by RI-5995\" })('Verify that  OSS Cluster database with SSH can be added and work correctly', async t => {\n    const sshWithPass = {\n        ...sshParams,\n        sshPassword: 'pass'\n    };\n    // Verify that user can add SSH tunnel with Password for OSS Cluster database\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbClusterPass, sshWithPass);\n    // TODO: should be deleted after https://redislabs.atlassian.net/browse/RI-5995\n    await t.wait(6000);\n    await myRedisDatabasePage.clickOnDBByName(sshDbClusterPass.databaseName);\n    if(! await browserPage.plusAddKeyButton.exists){\n        await myRedisDatabasePage.clickOnDBByName(sshDbClusterPass.databaseName);\n    }\n    //verify that db is added and profiler works\n    await t.click(browserPage.Profiler.expandMonitor);\n    await t.click(browserPage.Profiler.startMonitorButton);\n    await t.expect(browserPage.Profiler.monitorIsStartedText.innerText).eql('Profiler is started.');\n\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(clusterPage.overviewTab);\n    await t.expect(await clusterPage.getPrimaryNodesCount()).eql(Number('3'), 'Primary nodes in table are not corrected');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/database/clone-databases.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Clone databases`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Delete databases\n        const dbNumber = await myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count;\n        for (let i = 0; i < dbNumber; i++) {\n            await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n        }\n    });\ntest.skip('Verify that user can clone Standalone db', async t => {\n    await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n\n    // Verify that user can test Standalone connection on edit and see the success message\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n    await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Standalone connection is not successful');\n\n    // Verify that user can cancel the Clone by clicking the \"Cancel\" or the \"x\" button\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    await t.expect(myRedisDatabasePage.popoverHeader.withText('Clone ').exists).notOk('Clone panel is still displayed', { timeout: 2000 });\n    await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n    // Verify that user see the \"Add Database Manually\" form pre-populated with all the connection data when cloning DB\n    await t\n    // Verify that name in the header has the prefix \"Clone\"\n        .expect(myRedisDatabasePage.popoverHeader.withText('Clone ').exists).ok('Clone panel is not displayed')\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.hostInput.getAttribute('value')).eql(ossStandaloneConfig.host, 'Wrong host value')\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.portInput.getAttribute('value')).eql(ossStandaloneConfig.port, 'Wrong port value')\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput.getAttribute('value')).eql(ossStandaloneConfig.databaseName, 'Wrong host value')\n    // Verify that timeout input is displayed for clone db window\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.timeoutInput.value).eql('30', 'Timeout is not defaulted to 30 on clone window');\n    // Verify that user can confirm the creation of the database by clicking \"Clone Database\"\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count).eql(2, 'DB was not cloned');\n\n    // Verify new connection badge for cloned database\n    await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossStandaloneConfig.databaseName);\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts",
    "content": "import * as fs from 'fs';\nimport editJsonFile from 'edit-json-file';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, workingDirectory } from '../../../../helpers/conf';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nif (fs.existsSync(workingDirectory)) {\n\n    // Tutorials content\n    const tutorialsTimestampPath = `${workingDirectory}/tutorials/build.json`;\n    // const tutorialsTimeSeriesFilePath = `${workingDirectory}/tutorials/redis_stack/redis_for_time_series.md`;\n\n    // Remove md files from local folder. When desktop tests are started, files will be updated from remote repository\n    // Need to uncomment when desktop tests are started\n    // fs.unlinkSync(guidesGraphIntroductionFilePath);\n    // fs.unlinkSync(tutorialsTimeSeriesFilePath);\n\n    // Update timestamp for build files\n    const tutorialsTimestampFile = editJsonFile(tutorialsTimestampPath);\n\n    const tutorialNewTimestamp = tutorialsTimestampFile.get('timestamp') - 10;\n\n    tutorialsTimestampFile.set('timestamp', tutorialNewTimestamp);\n    tutorialsTimestampFile.save();\n\n    fixture `Auto-update in Enablement Area`\n        .meta({ type: 'critical_path', rte: rte.standalone, skipComment: \"Skipped because it is not running in CI\" })\n        .page(commonUrl)\n        .beforeEach(async() => {\n            await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        })\n        .afterEach(async() => {\n            await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n        });\n    test.skip('Verify that user can see updated info in Enablement Area', async t => {\n        // Create new file due to cache-ability\n        const tutorialsTimestampFileNew = editJsonFile(tutorialsTimestampPath);\n\n        // Open Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n\n        // Check Enablement area and validate that removed file is existed in Guides\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tab = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.click(tab.guidesGraphAccordion); // TODO: - FAILS to find selector\n        await t.click(tab.guidesIntroductionGraphLink.nth(1));\n        await t.expect(tab.enablementAreaEmptyContent.visible).notOk('Guides folder is not updated');\n        await t.click(tab.closeEnablementPage);\n\n        // Check Enablement area and validate that removed file is existed in Tutorials\n        await t.click(tab.redisStackTutorialsButton);\n        await t.click(tab.timeSeriesLink);\n        await t.expect(tab.enablementAreaEmptyContent.visible).notOk('Tutorials folder is not updated');\n\n        // Check that timestamp is new\n        const actualTutorialTimestamp = await tutorialsTimestampFileNew.get('timestamp');\n        await t.expect(actualTutorialTimestamp).notEql(tutorialNewTimestamp, 'Tutorials timestamp is not updated');\n    });\n}\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/files-auto-update/promo-button-autoupdate.e2e.ts",
    "content": "import * as fs from 'fs';\nimport { Chance } from 'chance';\nimport editJsonFile from 'edit-json-file';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {commonUrl, ossStandaloneConfig, workingDirectory} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst chance = new Chance();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nif (fs.existsSync(workingDirectory)) {\n    const timestampPromoButtonPath = `${workingDirectory}/content/build.json`;\n    const contentPromoButtonPath = `${workingDirectory}/content/create-redis.json`;\n    const timestampPromoButtonFile = editJsonFile(timestampPromoButtonPath);\n    const contentPromoButtonFile = editJsonFile(contentPromoButtonPath);\n    const timestampBeforeUpdate = timestampPromoButtonFile.get('timestamp');\n    const newTimestamp = timestampBeforeUpdate - 1;\n    const newPromoButtonText = chance.word({ length: 10 });\n\n    //Edit json file values\n    contentPromoButtonFile.set('cloud.title', newPromoButtonText);\n    contentPromoButtonFile.set('cloud.description', newPromoButtonText);\n    contentPromoButtonFile.save();\n    timestampPromoButtonFile.set('timestamp', newTimestamp);\n    timestampPromoButtonFile.save();\n\n    fixture `Auto-update in Promo Button`\n        .meta({ type: 'critical_path', skipComment: \"Skipped because it is not run in the CI\" })\n        .page(commonUrl)\n        .beforeEach(async() => {\n            await databaseHelper.acceptLicenseTerms();\n        });\n    test.skip('Verify that user has the ability to update \"Create free database\" button without changing the app', async t => {\n        // Create new file paths due to cache-ability\n        const timestampPathNew = editJsonFile(timestampPromoButtonPath);\n        const contentPathNew = editJsonFile(contentPromoButtonPath);\n        // Check the promo button after the opening of app\n        await t.expect(myRedisDatabasePage.promoButton.textContent).notContains(newPromoButtonText, 'Promo button text is not updated'); // TODO: - check what is with this promo button\n        // Get the values from build.json and create-redis.json files\n        const actualTimestamp = await timestampPathNew.get('timestamp');\n        const actualPromoButtonTitle = await contentPathNew.get('cloud.title');\n        const actualPromoButtonDescription = await contentPathNew.get('cloud.description');\n        // Check the json files are automatically updated\n        await t.expect(actualPromoButtonTitle).notEql(newPromoButtonText, 'The cloud title in the create-redis.json file is automatically updated');\n        await t.expect(actualPromoButtonDescription).notEql(newPromoButtonText, 'The cloud description in the create-redis.json file is automatically updated');\n        await t.expect(actualTimestamp).notEql(newTimestamp, 'The timestamp in the build.json file is automatically updated');\n    });\n}\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    WorkbenchPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst keyName = `${Common.generateWord(20)}-key`;\nconst keyValue = `${Common.generateWord(10)}-value`;\n\nfixture `Monitor`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can work with Monitor', async t => {\n    const command = 'set';\n    //Verify that user can open Monitor\n    await t.click(browserPage.Profiler.expandMonitor);\n    //Check that monitor is opened\n    await t.expect(browserPage.Profiler.monitorArea.exists).ok('Profiler area');\n    await t.expect(browserPage.Profiler.startMonitorButton.exists).ok('Start profiler button');\n    //Verify that user can see message inside Monitor \"Running Monitor will decrease throughput, avoid running it in production databases.\" when opens it for the first time\n    await t.expect(browserPage.Profiler.monitorWarningMessage.exists).ok('Profiler warning message');\n    await t.expect(browserPage.Profiler.monitorWarningMessage.withText('Running Profiler will decrease throughput, avoid running it in production databases.').exists).ok('Profiler warning message is not correct');\n    //Verify that user can run Monitor by clicking \"Run\" command in the message inside Monitor\n    await t.click(browserPage.Profiler.startMonitorButton);\n    await t.expect(browserPage.Profiler.monitorIsStartedText.innerText).eql('Profiler is started.');\n    //Verify that user can see run commands in monitor\n    await browserPage.Cli.getSuccessCommandResultFromCli(`${command} ${keyName} ${keyValue}`);\n    await browserPage.Profiler.checkCommandInMonitorResults(command, [keyName, keyValue]);\n});\ntest.skip('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => {\n    //Define commands in different clients\n    const cli_command = 'command';\n    const workbench_command = 'hello';\n    const common_command = 'info';\n    const browser_command = 'hset';\n    //Start Monitor\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    //Send command in CLI\n    await browserPage.Cli.getSuccessCommandResultFromCli(cli_command);\n    //Check that command from CLI is displayed in monitor\n    await browserPage.Profiler.checkCommandInMonitorResults(cli_command);\n    //Refresh the page to send command from Browser client\n    await t.click(browserPage.refreshKeysButton);\n    //Check the command from browser client\n    await browserPage.addHashKey(keyName);\n    await browserPage.Profiler.checkCommandInMonitorResults(browser_command);\n    //Open Workbench page to create new client\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    //Send command in Workbench\n    await workbenchPage.sendCommandInWorkbench(workbench_command);\n    //Check that command from Workbench is displayed in monitor\n    await workbenchPage.Profiler.checkCommandInMonitorResults(workbench_command);\n    //Check the command from common client\n    await workbenchPage.Profiler.checkCommandInMonitorResults(common_command);\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage, PubSubPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst pubSubPage = new PubSubPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Subscribe/Unsubscribe from a channel`\n    .meta({ rte: rte.standalone, type: 'critical_path' })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        //Go to PubSub page\n        await t.click(pubSubPage.NavigationTabs.pubSubButton);\n    })\n    .afterEach(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest('Verify that when user subscribe to the pubsub channel he can see all the messages being published to my database from the moment of my subscription', async t => {\n    // Verify that the Channel field placeholder is 'Enter Channel Name'\n    await t.expect(pubSubPage.channelNameInput.getAttribute('placeholder')).eql('Enter Channel Name', 'No placeholder in Channel field');\n    // Verify that the Message field placeholder is 'Enter Message'\n    await t.expect(pubSubPage.messageInput.getAttribute('placeholder')).eql('Enter Message', 'No placeholder in Message field');\n    // Verify that user is unsubscribed from the pubsub channel when he go to the pubsub window after launching application for the first time\n    await t.expect(pubSubPage.subscribeStatus.textContent).eql('You are not subscribed', 'User is not unsubscribed');\n    await t.expect(pubSubPage.subscribeButton.textContent).eql('Subscribe', 'Subscribe button is not displayed');\n\n    // Subscribe to channel\n    await t.click(pubSubPage.subscribeButton);\n    await t.expect(pubSubPage.subscribeStatus.textContent).eql('You are  subscribed', 'User is not subscribed', { timeout: 10000 });\n    // Verify that user can publish a message to a channel\n    await pubSubPage.publishMessage('test', 'published message');\n    await verifyMessageDisplayingInPubSub('published message', true);\n    await t.click(pubSubPage.unsubscribeButton);\n    //Verify that when user unsubscribe from a pubsub channel he can see no new data being published to the channel from the moment he unsubscribe\n    await t.expect(pubSubPage.subscribeStatus.textContent).eql('You are not subscribed', 'User is not unsubscribed', { timeout: 10000 });\n    //Verify that user can publish a message regardless of my subscription state.\n    await pubSubPage.publishMessage('test', 'message in unsubscribed status');\n    //Verify that message is not displayed\n    await verifyMessageDisplayingInPubSub('message in unsubscribed status', false);\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/slow-log/slow-log.e2e.ts",
    "content": "import { SlowLogPage, MyRedisDatabasePage, BrowserPage, ClusterDetailsPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst slowLogPage = new SlowLogPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst overviewPage = new ClusterDetailsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst slowerThanParameter = 1;\nlet maxCommandLength = 50;\nconst command = `slowlog get ${maxCommandLength}`;\n\nfixture `Slow Log`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        await t.click(slowLogPage.NavigationTabs.analysisButton);\n        await t.click(slowLogPage.slowLogTab);\n    })\n    .afterEach(async() => {\n        await slowLogPage.resetToDefaultConfig();\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig);\n    });\ntest('Verify that user can open new Slow Log page using new icon on left app panel', async t => {\n    // Verify that user see \"Slow Log\" page by default for non OSS Cluster\n    await t.expect(overviewPage.overviewTab.withAttribute('aria-selected', 'true').exists).notOk('The Overview tab is displayed for non OSS Cluster db');\n    // Verify that user can configure slowlog-max-len for Slow Log and see whole set of commands according to the setting\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.Cli.sendCommandInCli(command);\n    await t.click(slowLogPage.slowLogRefreshButton);\n    const duration = await slowLogPage.slowLogCommandValue.withExactText(command).parent(3).find(slowLogPage.cssSelectorDurationValue).textContent;\n    await t.expect(parseInt(duration)).gte(slowerThanParameter, 'Displayed command time execution is more than specified');\n    // Verify that user can see 3 columns with timestamp, duration and command in Slow Log\n    await t.expect(slowLogPage.slowLogTimestampValue.exists).ok('Timestamp column');\n    await t.expect(slowLogPage.slowLogDurationValue.exists).ok('Duration column');\n    await t.expect(slowLogPage.slowLogCommandValue.exists).ok('Command column');\n});\ntest('Verify that user can see \"No Slow Logs found\" message when slowlog-max-len=0', async t => {\n    // Set slowlog-max-len=0\n    maxCommandLength = 0;\n    await slowLogPage.changeMaxLengthParameter(maxCommandLength);\n    await t.click(slowLogPage.slowLogRefreshButton);\n    // Check that no records are displayed in SlowLog table\n    await t.expect(slowLogPage.slowLogEmptyResult.exists).ok('Empty results not found');\n    // Verify that user can see not more that number of commands that was specified in configuration\n    maxCommandLength = 30;\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.changeMaxLengthParameter(maxCommandLength);\n    // Go to Browser page to scan keys and turn back\n    await t.click(slowLogPage.NavigationTabs.browserButton);\n    await t.click(browserPage.refreshKeysButton);\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(slowLogPage.slowLogTab);\n    // Compare number of logged commands with maxLength\n    await t.expect(slowLogPage.slowLogCommandStatistics.withText(`${maxCommandLength} entries`).exists).ok(`Number of displayed commands is less than selected ${maxCommandLength}`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/tree-view/tree-view-improvements.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keyNames: string[];\nlet keyName1: string;\nlet keyName2: string;\nlet keyNameSingle: string;\nlet index: string;\n\nfixture `Tree view navigations improvement tests`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Tree view preselected folder', async t => {\n        keyName1 = Common.generateWord(10);\n        keyName2 = Common.generateWord(10);\n        keyNameSingle = Common.generateWord(10);\n        keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle];\n\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `HSET ${keyNames[2]} field value`,\n            `SADD ${keyNames[3]} value`,\n            `SADD ${keyNames[4]} value`\n        ];\n\n        // Create 5 keys\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.treeViewButton);\n        // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.TreeView.openTreeFolders([await browserPage.TreeView.getTextFromNthTreeElement(1)]);\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Set);\n        // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.setAllKeyType();\n        // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n        await verifyKeysDisplayingInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`], false);\n\n        // switch between browser view and tree view\n        await t.click(browserPage.browserViewButton)\n            .click(browserPage.treeViewButton);\n        await browserPage.deleteKeyByName(keyNames[4]);\n        await t.click(browserPage.clearFilterButton);\n        // get first folder name\n        const firstTreeItemText = await browserPage.TreeView.getTextFromNthTreeElement(0);\n        // All folders with namespaces are collapsed when there is no folder without any patterns\n        await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false);\n\n        const commands1 = [\n            `SADD ${keyNames[4]} value`\n        ];\n\n        await browserPage.Cli.sendCommandsInCli(commands1);\n        await t.click(browserPage.refreshKeysButton);\n        // Folders are collapsed after refresh\n        await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false);\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n        // Only folders according to key type filter are displayed\n        await verifyKeysDisplayingInTheList([keyNameSingle], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(firstTreeItemText, true);\n\n        await browserPage.searchByKeyName(`${keyName1}*`);\n        // Only folders according to filter by key names are displayed\n        await verifyKeysDisplayingInTheList([keyNameSingle], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n\n        await t.click(browserPage.clearFilterButton);\n        // All folders are displayed and collapsed after cleared filter\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n        await verifyKeysDisplayingInTheList([keyName1], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Stream);\n        // Verify that No results found message is displayed in case of invalid filtering\n        await t.expect(browserPage.noResultsFoundOnly.textContent).contains('No results found.', 'Key is not found message not displayed');\n\n        await browserPage.setAllKeyType(); // clear stream from filter\n        // Verify that no results found message not displayed after clearing filter\n        await t.expect(browserPage.noResultsFoundOnly.exists).notOk('Key is not found message still displayed');\n        // All folders are displayed and collapsed after cleared filter\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n        await verifyKeysDisplayingInTheList([keyName1], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify tree view navigation for index based search', async t => {\n        keyName1 = Common.generateWord(10); // used to create index name\n        keyName2 = Common.generateWord(10); // used to create index name\n        const subFolder1 = Common.generateWord(10); // used to create index name\n        keyNames = [`${keyName1}:${subFolder1}:1`, `${keyName1}:${subFolder1}:2`, `${keyName2}:1:1`, `${keyName2}:1:2`];\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `HSET ${keyNames[2]} field value`,\n            `HSET ${keyNames[3]} field value`\n        ];\n        await browserPage.Cli.sendCommandsInCli(commands);\n\n        // generate index based on keyName\n        const folders = [keyName1, subFolder1];\n        index = await browserPage.Cli.createIndexwithCLI(folders.join(':'));\n        await t.click(browserPage.redisearchModeBtn); // click redisearch button\n        await browserPage.selectIndexByName(index);\n        await t.click(browserPage.treeViewButton);\n        await browserPage.TreeView.openTreeFolders(folders);\n        await t.click(browserPage.refreshKeysButton);\n        // Refreshed Tree view preselected folder for index based search\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n    });\n\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await t.click(browserPage.patternModeBtn);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Search capability Refreshed Tree view preselected folder', async t => {\n        keyName1 = Common.generateWord(10);\n        keyName2 = Common.generateWord(10);\n        keyNameSingle = Common.generateWord(10);\n        keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle];\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `RPUSH ${keyNames[2]} field`,\n            `RPUSH ${keyNames[3]} field`,\n            `SADD ${keyNames[4]} value`\n        ];\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.treeViewButton);\n        // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash\n        await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n        // Only related to key types filter folders are displayed\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false);\n        await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], false);\n\n        await browserPage.setAllKeyType();\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]);\n        await t.click(browserPage.refreshKeysButton); // refresh keys\n        // Only related to filter folders are displayed when key does not exist after keys refresh\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await verifyKeysDisplayingInTheList([keyNames[4]], true);\n        await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false);\n\n        await browserPage.searchByKeyName('*');\n        await t.click(browserPage.refreshKeysButton);\n        // Search capability Refreshed Tree view preselected folder\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await verifyKeysDisplayingInTheList([keyNames[4]], true);\n        await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nlet indexName = Common.generateWord(5);\n\nfixture `Index Schema at Workbench`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async t => {\n        // Drop index, documents and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest.skip('Verify that user can open results in Text and Table views for FT.INFO for Hash in Workbench', async t => {\n    indexName = Common.generateWord(5);\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`,\n        'HMSET product:1 name \"Apple Juice\"'\n    ];\n    const searchCommand = `FT.INFO ${indexName}`;\n\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n    // Send search command\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    // Check that result is displayed in Table view\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The result is displayed in Table view');\n    // Select Text view type\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    // Check that result is displayed in Text view\n    await t.expect(workbenchPage.queryTextResult.exists).ok('The result is displayed in Text view');\n});\ntest.skip('Verify that user can open results in Text and Table views for FT.INFO for JSON in Workbench', async t => {\n    indexName = Common.generateWord(5);\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON JSON SCHEMA $.user.name AS name TEXT $.user.tag AS country TAG`,\n        'JSON.SET myDoc1 $ \\'{\"user\":{\"name\":\"John Smith\",\"tag\":\"foo,bar\",\"hp\":1000, \"dmg\":150}}\\''\n    ];\n    const searchCommand = `FT.INFO ${indexName}`;\n\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n    // Send search command\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    // Check that result is displayed in Table view\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The result is displayed in Table view');\n    // Select Text view type\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    // Check that result is displayed in Text view\n    await t.expect(workbenchPage.queryTextResult.exists).ok('The result is displayed in Text view');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nlet indexName = Common.generateWord(5);\n\nfixture `JSON verifications at Workbench`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async t => {\n        // Drop index, documents and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\n// TODO unskip after resolving https://redislabs.atlassian.net/browse/RI-6670\ntest.skip('Verify that user can see result in Table and Text view for JSON data types for FT.AGGREGATE command in Workbench', async t => {\n    indexName = Common.generateWord(5);\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON JSON SCHEMA $.user.name AS name TEXT $.user.tag AS country TAG`,\n        'JSON.SET myDoc1 $ \\'{\"user\":{\"name\":\"John Smith\",\"tag\":\"foo,bar\",\"hp\":1000, \"dmg\":150}}\\'',\n        'JSON.SET myDoc2 $ \\'{\"user\":{\"name\":\"John Smith\",\"tag\":\"foo,bar\",\"hp\":500, \"dmg\":300}}\\''\n    ];\n    const searchCommand = `FT.AGGREGATE ${indexName} \"*\" LOAD 6 $.user.hp AS hp $.user.dmg AS dmg APPLY \"@hp-@dmg\" AS points`;\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n    // Send search command\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    // Check that result is displayed in Table view\n    await t.switchToIframe(workbenchPage.iframe);\n    const resultTableExists = await workbenchPage.queryTableResult.exists\n    // TODO:  - Result is displayed but the table with values is not,  however seams that manually this is working, commenting this check but requires more investigation\n    //await t.expect(workbenchPage.queryTableResult.exists).ok('The result is displayed in Table view');\n    // Select Text view type\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    // Check that result is displayed in Text view\n    await t.expect(workbenchPage.queryTextResult.exists).ok('The result is displayed in Text view');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/browser/keys-all-databases.e2e.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    redisEnterpriseClusterConfig\n} from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst verifyKeysAdded = async(): Promise<void> => {\n    keyName = Common.generateWord(10);\n    // Add Hash key\n    await browserPage.addHashKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not correct');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const keyNameInTheList = Selector(`[data-testid=\"key-${keyName}\"]`);\n    await Common.waitForElementNotVisible(browserPage.loader);\n    await t.expect(keyNameInTheList.exists).ok(`${keyName} key is not added`);\n};\n\nfixture `Work with keys in all types of databases`\n    .meta({ type: 'regression' })\n    .page(commonUrl);\ntest\n    .meta({ rte: rte.reCluster })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisSoftwareDatabase(redisEnterpriseClusterConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, redisEnterpriseClusterConfig.databaseName);\n        await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName);\n    })\n    .skip('Verify that user can add Key in RE Cluster DB', async() => {\n        await verifyKeysAdded();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/cli/cli-re-cluster.e2e.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    redisEnterpriseClusterConfig\n} from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst verifyCommandsInCli = async(): Promise<void> => {\n    keyName = Common.generateWord(10);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Add key from CLI\n    await t.typeText(browserPage.Cli.cliCommandInput, `SADD ${keyName} \"chinese\" \"japanese\" \"german\"`, { replace: true, paste: true });\n    await t.pressKey('enter');\n    // Check that the key is added\n    await browserPage.searchByKeyName(keyName);\n    const keyNameInTheList = Selector(`[data-testid=\"key-${keyName}\"]`);\n    await Common.waitForElementNotVisible(browserPage.loader);\n    await t.expect(keyNameInTheList.exists).ok(`${keyName} key is not added`);\n};\n\nfixture `Work with CLI in RE Cluster`\n    .meta({ type: 'regression' })\n    .page(commonUrl);\ntest.skip\n    .meta({ rte: rte.reCluster })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisSoftwareDatabase(redisEnterpriseClusterConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, redisEnterpriseClusterConfig.databaseName);\n        await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName);\n    })('Verify that user can add data via CLI in RE Cluster DB', async() => {\n        // Verify that database index switcher not displayed for RE Cluster\n        await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).notOk('Change Db index control displayed for RE Cluster DB');\n\n        await verifyCommandsInCli();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/database/cloud-sso.e2e.ts",
    "content": "import * as path from 'path';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, samlUser } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { modifyFeaturesConfigJson, updateControlNumber } from '../../../../helpers/insights';\nimport { closeChrome, openChromeOnCi, openChromeWindow, saveOpenedChromeTabUrl } from '../../../../helpers/scripts/browser-scripts';\nimport { Common, SsoAuthorization } from '../../../../helpers';\nimport { AiChatBotPanel } from '../../../../pageObjects/components/chatbot/ai-chatbot-panel';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst aiChatBotPanel = new AiChatBotPanel();\n\nlet urlToUse = '';\nconst pathes = {\n    defaultRemote: path.join('.', 'test-data', 'features-configs', 'insights-default-remote.json'),\n    electronConfig: path.join('.', 'test-data', 'features-configs', 'sso-electron-build.json')\n};\nconst logsWithUrlFilePath = path.join('test-data', 'chrome_logs.txt');\n// TODO unskip after fixing testcafe issue with new electron RI-6365\nfixture.skip `Cloud SSO`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseHelper.acceptLicenseTerms();\n        // Update remote config .json to config with buildType filter including current app build\n        await modifyFeaturesConfigJson(pathes.electronConfig);\n        await updateControlNumber(48.2);\n    })\n    .afterEach(async() => {\n        await Common.deleteFileFromFolderIfExists(logsWithUrlFilePath);\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest('Verify that user can see SSO feature if it is enabled in feature config', async t => {\n    await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).ok('Cloud Sign in button not displayed when SSO feature enabled');\n\n    // TODO fix once Sign in modal will be available to testcafe https://redislabs.atlassian.net/browse/RI-6048\n    // Open Cloud Sign in dialog\n    // await t.click(myRedisDatabasePage.promoButton);\n    // Verify that Cloud Sign in dialog has authorization buttons for Electron app\n    // await t.expect(myRedisDatabasePage.AuthorizationDialog.authDialog.exists).ok('Cloud Sigh in modal not opened');\n    // await t.expect(myRedisDatabasePage.AuthorizationDialog.googleAuth.exists).ok('Google auth button not displayed in Sigh in modal');\n    // await t.expect(myRedisDatabasePage.AuthorizationDialog.gitHubAuth.exists).ok('Github auth button not displayed in Sigh in modal');\n    // await t.expect(myRedisDatabasePage.AuthorizationDialog.ssoAuth.exists).ok('SSO auth button not displayed in Sigh in modal');\n    // await t.click(myRedisDatabasePage.Modal.closeModalButton);\n\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addCloudDatabaseButton);\n    // Verify that RE Cloud auto-discovery options Use Cloud Account and Use Cloud API Keys are displayed on Welcome screen\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.useCloudAccount.exists).ok('Use Cloud Account accordion not displayed when SSO feature enabled');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.useCloudKeys.exists).ok('Use Cloud Keys accordion not displayed when SSO feature enabled');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.useCloudAccount);\n    // Verify that Auth buttons are displayed for auto-discovery panel on Electron app\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.RedisCloudSigninPanel.googleOauth.exists).ok('Google auth button not displayed when SSO feature enabled');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.RedisCloudSigninPanel.githubOauth.exists).ok('Github auth button not displayed when SSO feature enabled');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.RedisCloudSigninPanel.ssoOauth.exists).ok('SSO auth button not displayed when SSO feature enabled');\n});\n// skip until adding tests for SSO feature\ntest.skip('Verify that user can sign in using SSO SAML auth', async t => {\n    // Open Chrome with a sample URL and save it to logs file\n    await openChromeOnCi();\n    await t.click(myRedisDatabasePage.NavigationHeader.copilotButton);\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement);\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.ssoOauthButton);\n    await t.typeText(aiChatBotPanel.RedisCloudSigninPanel.ssoEmailInput, samlUser, { replace: true, paste: true });\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.submitBtn);\n    await saveOpenedChromeTabUrl(logsWithUrlFilePath);\n\n    await t.wait(2000);\n    urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath);\n    await t.expect(urlToUse).contains('authorize?');\n    await closeChrome();\n    await SsoAuthorization.processSSOPuppeteer(urlToUse, 'SAML');\n    await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 });\n    await myRedisDatabasePage.reloadPage();\n    await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed');\n    await t.click(myRedisDatabasePage.userProfileBtn);\n    await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('Geor', 'User not signed in');\n});\n// Can be run only locally for google auth\ntest.skip('Verify that user can sign in using SSO Google auth', async t => {\n    await t.expect(myRedisDatabasePage.promoButton.exists).ok('Import Cloud database button not displayed when SSO feature enabled');\n    // Open Chrome with a sample URL and save it to logs file\n    await openChromeWindow();\n    await t.wait(2000);\n    await t.click(myRedisDatabasePage.NavigationHeader.copilotButton);\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement);\n\n    await t.wait(2000);\n    await saveOpenedChromeTabUrl(logsWithUrlFilePath);\n    // Click the button to trigger the Google authorization page\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.googleOauth);\n\n    await t.wait(2000);\n    urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath);\n    await t.expect(urlToUse).contains('authorize?');\n    await closeChrome();\n    await SsoAuthorization.processSSOPuppeteer(urlToUse, 'Google');\n    await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 });\n    await myRedisDatabasePage.reloadPage();\n    await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed');\n    await t.click(myRedisDatabasePage.userProfileBtn);\n    await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('Geor', 'User not signed in');\n});\n// Can be run only locally for github auth\ntest.skip('Verify that user can sign in using SSO Github auth', async t => {\n    // Open Chrome with a sample URL and save it to logs file\n    await openChromeWindow();\n    await t.wait(1000);\n    await t.click(myRedisDatabasePage.NavigationHeader.copilotButton);\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement);\n\n    await t.wait(2000);\n    await saveOpenedChromeTabUrl(logsWithUrlFilePath);\n    // Click the button to trigger the Github authorization page\n    await t.click(aiChatBotPanel.RedisCloudSigninPanel.githubOauth);\n\n    await t.wait(2000);\n    urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath);\n    await t.expect(urlToUse).contains('authorize?');\n    await closeChrome();\n    await SsoAuthorization.processSSOPuppeteer(urlToUse, 'Github');\n    await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 });\n    await myRedisDatabasePage.reloadPage();\n    await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed');\n    await t.click(myRedisDatabasePage.userProfileBtn);\n    await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('Geor', 'User not signed in');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/database/edit-db.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst database = Object.assign({}, ossStandaloneConfig);\nconst previousDatabaseName = Common.generateWord(20);\ndatabase.databaseName = previousDatabaseName;\nconst keyName = Common.generateWord(10);\n\nfixture `List of Databases`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n        await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.portInput, ossStandaloneConfig.port, { replace: true, paste: true });\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest.skip('Verify that context for previous database not saved after editing port/username/password/certificates/SSH', async t => {\n    const command = 'HSET';\n\n    // Create context modificaions and navigate to db list\n    await browserPage.addStringKey(keyName);\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n    await t.pressKey('enter');\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Edit port of added database\n    await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n    await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.portInput, ossStandaloneBigConfig.port, { replace: true, paste: true });\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    // Verify that keys from the database with new port are displayed\n    await t.expect(browserPage.keysSummary.find('b').withText('18 00').exists).ok('DB with new port not opened');\n    // Verify that context not saved\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).notOk('The key details is still selected');\n    await t.expect(browserPage.Cli.cliCommandExecuted.withExactText(command).exists).notOk(`Executed command '${command}' in CLI is still displayed`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/insights/feature-flag.e2e.ts",
    "content": "import * as path from 'path';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneV5Config } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { DatabaseScripts } from '../../../../helpers/database-scripts';\nimport { modifyFeaturesConfigJson, updateControlNumber } from '../../../../helpers/insights';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst pathes = {\n    defaultRemote: path.join('.', 'test-data', 'features-configs', 'insights-default-remote.json'),\n    dockerConfig: path.join('.', 'test-data', 'features-configs', 'insights-docker-build.json'),\n    electronConfig: path.join('.', 'test-data', 'features-configs', 'insights-electron-build.json')\n};\n\nfixture `Feature flag`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n    })\n    .afterEach(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config);\n        // Update remote config .json to default\n        await modifyFeaturesConfigJson(pathes.defaultRemote);\n        // Clear features config table\n        await DatabaseScripts.deleteRowsFromTableInDB({ tableName: 'features_config' });\n    });\n// the test is skipped due to story https://redislabs.atlassian.net/browse/RI-5089\ntest.skip('Verify that Insights panel can be displayed for Electron app according to filters', async t => {\n    // Update remote config .json to config with buildType filter excluding current app build\n    await modifyFeaturesConfigJson(pathes.dockerConfig);\n    await updateControlNumber(48.2);\n    await t.expect(browserPage.NavigationHeader.insightsTriggerButton.exists).notOk('Insights panel displayed when filter excludes this buildType');\n\n    // Update remote config .json to config with buildType filter including current app build\n    await modifyFeaturesConfigJson(pathes.electronConfig);\n    await updateControlNumber(48.2);\n    await t.expect(browserPage.NavigationHeader.insightsTriggerButton.exists).ok('Insights panel not displayed when filter includes this buildType');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/monitor/monitor.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    WorkbenchPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneNoPermissionsConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { WorkbenchActions } from '../../../../common-actions/workbench-actions';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst workbenchActions = new WorkbenchActions();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Monitor`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\n\ntest.skip\n    .before(async t => {\n        await  databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.Cli.sendCommandInCli('acl setuser noperm nopass on +@all ~* -monitor -client');\n        // Check command result in CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.expect(browserPage.Cli.cliOutputResponseSuccess.textContent).eql('\"OK\"', 'Command from autocomplete was not found & executed');\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig);\n        await browserPage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneNoPermissionsConfig.databaseName);\n    })\n    .after(async t => {\n        // Delete created user\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneNoPermissionsConfig.databaseName);\n        await browserPage.Cli.sendCommandInCli('acl DELUSER noperm');\n        // Delete database\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('STANDALONE');\n    })('Verify that if user doesn\\'t have permissions to run monitor, user can see error message', async t => {\n        const command = 'CLIENT LIST';\n        // Expand the Profiler\n        await t.click(browserPage.Profiler.expandMonitor);\n        // Click on run monitor button\n        await t.click(browserPage.Profiler.startMonitorButton);\n        // Check that error message is displayed\n        await t.expect(browserPage.Profiler.monitorNoPermissionsMessage.visible).ok('Error message not found');\n        // Check the error message text\n        await t.expect(browserPage.Profiler.monitorNoPermissionsMessage.innerText).eql('The Profiler cannot be started. This user has no permissions to run the \\'monitor\\' command', 'No Permissions message not found');\n        // Verify that if user doesn't have permissions to run monitor, run monitor button is not available\n        await t.expect(browserPage.Profiler.runMonitorToggle.withAttribute('disabled').exists).ok('No permissions run icon not found');\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await workbenchPage.sendCommandInWorkbench(command);\n        // Verify that user have the following error when there is no permission to run the CLIENT LIST: \"NOPERM this user has no permissions to run the 'CLIENT LIST' command or its subcommand\"\n        await workbenchActions.verifyClientListErrorMessage();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/shortcuts/shortcuts.e2e.ts",
    "content": "// import { ClientFunction } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl } from '../../../../helpers/conf';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n// const getPageUrl = ClientFunction(() => window.location.href);\n\nfixture `Shortcuts`\n    .meta({ type: 'regression', rte: rte.none })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest('Verify that user can see a summary of Shortcuts by clicking \"Keyboard Shortcuts\" button in Help Center for desktop', async t => {\n    // Click on help center icon and verify panel\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.exists).ok('Help Center panel is not opened');\n    // Click on Shortcuts option and verify panel\n    await t.click(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterShortcutButton);\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).ok('Shortcuts panel is not opened');\n    // Validate Title and sections of Shortcuts\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).ok('Shortcuts panel is not opened');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsTitle.exists).ok('shortcutsTitle is not opened');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsDesktopApplicationSection.exists).ok('shortcutsDesktopApplicationSection is not opened');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsCLISection.exists).ok('shortcutsCLISection is not displayed');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsWorkbenchSection.exists).ok('shortcutsWorkbenchSection is not displayed');\n    // Verify that user can close the Shortcuts\n    await t.click(myRedisDatabasePage.ShortcutsPanel.shortcutsCloseButton);\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).notOk('Shortcuts panel is not displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst keyNameGraph = 'bikes_graph';\n\nfixture `Redis Stack command in Workbench`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async() => {\n        // Drop key and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`GRAPH.DELETE ${keyNameGraph}`);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\n//skipped due the inaccessibility of the iframe\ntest.skip('Verify that user can switches between Graph and Text for GRAPH command and see results corresponding to their views', async t => {\n    // Send Graph command\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.redisStackTutorialsButton);\n    await t.click(tutorials.tutorialsWorkingWithGraphLink);\n    await tutorials.runBlockCode('Create a bike node');\n    await t.click(workbenchPage.submitCommandButton);\n    // Switch to Text view and check result\n    await workbenchPage.selectViewTypeText();\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).ok('The text view is not switched for GRAPH command');\n    // Switch to Graph view and check result\n    await workbenchPage.selectViewTypeGraph();\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.queryGraphContainer).exists).ok('The Graph view is not switched for GRAPH command');\n});\n//skipped due to Graph no longer displayed in tutorials\ntest.skip('Verify that user can see \"No data to visualize\" message for Graph command', async t => {\n    // Send Graph command\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.redisStackTutorialsButton);\n    await tutorials.runBlockCode('Show all sales per region');\n    await t.click(workbenchPage.submitCommandButton);\n    // Check result\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.responseInfo.textContent).eql('No data to visualize. Raw information is presented below.', 'The info message is not displayed for Graph');\n\n    // Get result text content\n    const graphModeText = await workbenchPage.parsedRedisReply.textContent;\n    // Switch to Text view and check result\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    await t.expect(workbenchPage.queryTextResult.exists).ok('The result in text view is not displayed');\n    // Verify that when there is nothing to visualize in RedisGraph, user can see just text result\n    await t.expect(workbenchPage.queryTextResult.textContent).notEql(graphModeText, 'Text of command in Graph mode is the same as in Text mode');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, redisEnterpriseClusterConfig } from '../../../../helpers/conf';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst browserPage = new BrowserPage();\n\nconst commandForSend1 = 'info';\nconst commandForSend2 = 'FT._LIST';\nconst verifyCommandsInWorkbench = async(): Promise<void> => {\n    const multipleCommands = [\n        'info',\n        'command',\n        'FT.SEARCH idx *'\n    ];\n\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandForSend1);\n    await workbenchPage.sendCommandInWorkbench(commandForSend2);\n    // Check that all the previous run commands are saved and displayed\n    await workbenchPage.reloadPage();\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend1).exists).ok('The previous run commands are saved');\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend2).exists).ok('The previous run commands are saved');\n    // Send multiple commands in one query\n    await workbenchPage.sendCommandInWorkbench(multipleCommands.join('\\n'), 0.75);\n    // Check that the results for all commands are displayed\n    for (const command of multipleCommands) {\n        await t.expect(workbenchPage.queryCardCommand.withExactText(command).exists).ok(`The command ${command} from multiple query is displayed`);\n    }\n};\n\nfixture `Work with Workbench in RE Cluster`\n    .meta({ type: 'regression' })\n    .page(commonUrl);\ntest.skip\n    .meta({ rte: rte.reCluster })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisSoftwareDatabase(redisEnterpriseClusterConfig);\n    })\n    .after(async() => {\n        // Delete database\n        await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName);\n    })('Verify that user can run commands in Workbench in RE Cluster DB', async() => {\n        await verifyCommandsInWorkbench();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/smoke/browser/add-keys.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `Add keys`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can add Hash Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add Hash key\n    await browserPage.addHashKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The Hash key is not added');\n});\ntest('Verify that user can add Set Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add Set key\n    await browserPage.addSetKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The Set key is not added');\n});\ntest('Verify that user can add List Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add List key\n    await browserPage.addListKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The List key is not added');\n});\ntest('Verify that user can add String Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add String key\n    await browserPage.addStringKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The String key is not added');\n});\ntest('Verify that user can add ZSet Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add ZSet key\n    await browserPage.addZSetKey(keyName, '111');\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The ZSet key is not added');\n});\ntest('Verify that user can add JSON Key', async t => {\n    keyName = Common.generateWord(10);\n    const keyTTL = '2147476121';\n    const value = '{\"name\":\"xyz\"}';\n\n    // Add JSON key\n    await browserPage.addJsonKey(keyName, value, keyTTL);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The JSON key is not added');\n});\ntest('Verify that user can add Stream key', async t => {\n    keyName = Common.generateWord(20);\n    const keyField = Common.generateWord(20);\n    const keyValue = Common.generateWord(20);\n\n    // Add New Stream Key\n    await browserPage.addStreamKey(keyName, keyField, keyValue);\n    // Verify that user can see Stream details opened after key creation\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Stream Key Name not visible');\n    // Verify that user can see newly added Stream key in key list clicking on keys refresh button\n    await t.click(browserPage.refreshKeysButton);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('Stream is not added');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/smoke/browser/list-of-keys-verifications.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nlet keyNames: string[] = [];\nconst keyTTL = '2147476121';\n\nfixture `List of keys verifications`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .after(async() => {\n        // Clear and delete database\n        for(const name of keyNames) {\n            await apiKeyRequests.deleteKeyByNameApi(name, ossStandaloneConfig.databaseName);\n        }\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that user can scroll List of Keys in DB', async t => {\n        keyNames = [\n            `key-${Common.generateWord(10)}`,\n            `key-${Common.generateWord(10)}`,\n            `key-${Common.generateWord(10)}`,\n            `key-${Common.generateWord(10)}`\n        ];\n\n        await browserPage.addStringKey(keyNames[0]);\n        await browserPage.addHashKey(keyNames[1]);\n        await browserPage.addListKey(keyNames[2]);\n        await browserPage.addStringKey(keyNames[3]);\n        await t.click(browserPage.refreshKeysButton);\n        // Verify that user can see List of Keys in DB\n        await t.expect(browserPage.keyNameInTheList.exists).ok('The list of keys is not displayed');\n\n        // Scroll to the key element\n        await t.hover(browserPage.keyNameInTheList);\n        await t.expect(browserPage.keyNameInTheList.exists).ok('The list of keys is not displayed');\n    });\ntest('Verify that user can refresh Keys', async t => {\n    keyName = Common.generateWord(10);\n    const newKeyName = 'KeyNameAfterEdit!testKey';\n\n    // Add hash key\n    await browserPage.addHashKey(keyName, keyTTL);\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification is not displayed');\n    await t.click(browserPage.closeKeyButton);\n    // Search for the added key\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).eql(true, 'The key is not in the list');\n    // Edit the key name in details\n    await t.click(browserPage.keyNameInTheList);\n    await browserPage.editKeyName(newKeyName);\n    // Refresh Keys\n    await t.click(browserPage.refreshKeysButton);\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsNotDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsNotDisplayedInTheList).eql(false, 'The key is still in the list');\n});\ntest('Verify that user can open key details', async t => {\n    keyName = Common.generateWord(10);\n    const keyValue = 'StringValue!';\n\n    // Add String key\n    await browserPage.addStringKey(keyName, keyTTL, keyValue);\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification is not displayed');\n    await t.click(browserPage.closeKeyButton);\n    // Search for the added key\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The key is not in the list');\n    // Open the key details\n    await t.click(browserPage.keyNameInTheList);\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyName, 'The Key details is not opened');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/smoke/cli/cli.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `CLI`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can add data via CLI', async t => {\n    keyName = Common.generateWord(10);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Verify that user can expand CLI\n    await t.expect(browserPage.Cli.cliArea.exists).ok('CLI area is not displayed');\n    await t.expect(browserPage.Cli.cliCommandInput.exists).ok('CLI input is not displayed');\n\n    // Add key from CLI\n    await t.typeText(browserPage.Cli.cliCommandInput, `SADD ${keyName} \"chinese\" \"japanese\" \"german\"`, { replace: true, paste: true });\n    await t.pressKey('enter');\n    // Check that the key is added\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The key is not added');\n});\n"
  },
  {
    "path": "tests/e2e/tests/electron/smoke/database/autodiscover-db.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    redisEnterpriseClusterConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Add database`\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest.skip\n    .meta({ rte: rte.reCluster })\n    .after(async() => {\n        await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName);\n    })('Verify that user can add database from RE Cluster via auto-discover flow', async() => {\n        await databaseHelper.addNewRedisSoftwareDatabase(redisEnterpriseClusterConfig);\n        // Verify that user can see an indicator of databases that are added using autodiscovery and not opened yet\n        // Verify new connection badge for RE cluster\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(redisEnterpriseClusterConfig.databaseName);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/electron/smoke/database/edit-db.e2e.ts",
    "content": "import { ClientFunction } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    redisEnterpriseClusterConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Edit Databases`\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\n// Returns the URL of the current web page\nconst getPageUrl = ClientFunction(() => window.location.href);\ntest.skip\n    .meta({ rte: rte.reCluster })\n    .after(async() => {\n        // Delete database\n        await databaseHelper.deleteDatabase(redisEnterpriseClusterConfig.databaseName);\n    })('Verify that user can connect to the RE cluster database', async t => {\n        await databaseHelper.addNewRedisSoftwareDatabase(redisEnterpriseClusterConfig);\n        await myRedisDatabasePage.clickOnDBByName(redisEnterpriseClusterConfig.databaseName);\n        await t.expect(getPageUrl()).contains('browser', 'The edit view is not opened');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/a-first-start-form/user-agreements-form.e2e.ts",
    "content": "import { commonUrl } from '../../../../helpers/conf';\nimport { SettingsPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { Common } from '../../../../helpers/common';\nimport { rte } from '../../../../helpers/constants';\nimport { UserAgreementDialog } from '../../../../pageObjects/dialogs';\n\nconst userAgreementDialog = new UserAgreementDialog();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst settingsPage = new SettingsPage();\n\nfixture `Agreements Verification`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .page(commonUrl)\n    .requestHooks(Common.mockSettingsResponse())\n    .beforeEach(async t => {\n        await t.maximizeWindow();\n    });\ntest('Verify that user should accept User Agreements to continue working with the application', async t => {\n    await t.expect(userAgreementDialog.userAgreementsPopup.exists).ok('User Agreements Popup is shown');\n    // Verify that I still has agreements popup & cannot add a database\n    await t.expect(userAgreementDialog.submitButton.hasAttribute('disabled')).ok('Submit button not disabled by default');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton.exists).notOk('User can\\'t add a database');\n});\ntest('Verify that the encryption enabled by default', async t => {\n    // Verify that encryption enabled by default\n    await t.expect(userAgreementDialog.switchOptionEncryption.withAttribute('aria-checked', 'true').exists).ok('Encryption enabled by default');\n});\ntest('Verify that when user checks \"Use recommended settings\" option on EULA screen, all options (except Licence Terms) are checked', async t => {\n    // Verify options unchecked before enabling Use recommended settings\n    await t.expect(await settingsPage.getAnalyticsSwitcherValue()).notOk('Enable Analytics switcher is checked');\n    await t.expect(await settingsPage.getNotificationsSwitcherValue()).notOk('Enable Notifications switcher is checked');\n    // Check Use recommended settings switcher\n    await t.click(userAgreementDialog.recommendedSwitcher);\n    // Verify options checked after enabling Use recommended settings\n    await t.expect(await settingsPage.getAnalyticsSwitcherValue()).ok('Enable Analytics switcher is unchecked');\n    await t.expect(await settingsPage.getNotificationsSwitcherValue()).ok('Enable Notifications switcher is unchecked');\n    await t.expect(await settingsPage.getEulaSwitcherValue()).notOk('EULA switcher is checked');\n    // Uncheck Use recommended settings switcher\n    await t.click(userAgreementDialog.recommendedSwitcher);\n    // Verify that when user unchecks \"Use recommended settings\" option on EULA screen, previous state of checkboxes for the options is applied\n    await t.expect(await settingsPage.getAnalyticsSwitcherValue()).notOk('Enable Analytics switcher is checked');\n    await t.expect(await settingsPage.getNotificationsSwitcherValue()).notOk('Enable Notifications switcher is checked');\n    await t.expect(await settingsPage.getEulaSwitcherValue()).notOk('EULA switcher is checked');\n});\ntest('Verify that if \"Use recommended settings\" is selected, and user unchecks any of the option, \"Use recommended settings\" is unchecked', async t => {\n    // Check Use recommended settings switcher\n    await t.click(userAgreementDialog.recommendedSwitcher);\n    // Verify Use recommended settings switcher unchecked after unchecking analytics switcher\n    await t.click(settingsPage.switchAnalyticsOption);\n    await t.expect(await userAgreementDialog.getRecommendedSwitcherValue()).eql('false', 'Use recommended settings switcher is still checked');\n    // Check Use recommended settings switcher\n    await t.click(userAgreementDialog.recommendedSwitcher);\n    // Verify Use recommended settings switcher unchecked after unchecking notifications switcher\n    await t.click(settingsPage.switchNotificationsOption);\n    await t.expect(await userAgreementDialog.getRecommendedSwitcherValue()).eql('false', 'Use recommended settings switcher is still checked');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts",
    "content": "import { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { deleteAllKeysFromDB, populateDBWithHashes } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyNames = [Common.generateWord(20), Common.generateWord(20)];\nconst dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port };\nconst keyToAddParameters = { keysCount: 10000, keyNameStartWith: 'hashKey' };\nconst keyToAddParameters2 = { keysCount: 500000, keyNameStartWith: 'hashKey' };\n\nfixture.skip(`Bulk Delete`)\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.addHashKey(keyNames[0], '100000', Common.generateWord(20), Common.generateWord(20));\n        await browserPage.addSetKey(keyNames[1], '100000', Common.generateWord(20));\n        if (await browserPage.Toast.toastCloseButton.exists) {\n            await t.click(browserPage.Toast.toastCloseButton);\n        }\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await deleteAllKeysFromDB(dbParameters.host, dbParameters.port);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\n\ntest.skip('Verify that user can access the bulk actions screen in the Browser', async t => {\n    // Filter by Hash keys\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    // Open bulk actions\n    await t.click(browserPage.bulkActionsButton);\n    await t.expect(browserPage.BulkActions.bulkActionsContainer.exists).ok('Bulk actions screen not opened');\n    // Verify that user can see pattern summary of the keys selected: key type, pattern\n    await t.expect(browserPage.BulkActions.infoFilter.innerText).contains('Key type:\\nHASH', 'Key type is not correct');\n    await t.expect(browserPage.BulkActions.infoSearch.innerText).contains('Pattern: *', 'Key pattern is not correct');\n    // Verify that user can hover over info icon in Bulk Delete preview and see info about accuracy of the calculation\n    const tooltipText = 'Expected amount is estimated based on the number of keys scanned and the scan percentage. The final number may be different.';\n    await t.hover(browserPage.BulkActions.bulkDeleteTooltipIcon);\n    await t.expect(browserPage.tooltip.innerText).eql(tooltipText, 'Tooltip is not displayed or text is invalid');\n    // Verify that user can see warning message clicking on Delete button for Bulk Deletion\n    const warningTooltipTitle = 'Are you sure you want to perform this action?';\n    const warningTooltipMessage = 'All keys with HASH key type and selected pattern will be deleted.';\n    await t.click(browserPage.BulkActions.actionButton);\n    await t.expect(browserPage.BulkActions.bulkActionWarningTooltip.textContent).contains(warningTooltipTitle, 'Warning Tooltip title is not displayed or text is invalid');\n    await t.expect(browserPage.BulkActions.bulkActionWarningTooltip.textContent).contains(warningTooltipMessage, 'Warning Tooltip message is not displayed or text is invalid');\n    await t.expect(browserPage.BulkActions.bulkApplyButton.exists).ok('Confirm deletion button not displayed');\n\n});\ntest.skip('Verify that user can see summary of scanned level', async t => {\n    const expectedAmount = new RegExp('Expected amount: ~(9|10) \\\\d{3} keys');\n    const scannedKeys = new RegExp('Scanned (5|10)% \\\\((500|1 000)/10 \\\\d{3}\\\\) and found \\\\d{3,5} keys');\n    const messageTitle = 'No pattern or key type set';\n    const messageText = 'To perform a bulk action, set the pattern or select the key type';\n\n    // Add 10000 Hash keys\n    await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters);\n    // Open bulk actions\n    await t.click(browserPage.bulkActionsButton);\n    // Verify that user can see no pattern selected message when no key type and pattern applied for Bulk Delete\n    await t.expect(browserPage.BulkActions.bulkActionsPlaceholder.textContent).contains(messageTitle, 'No pattern title not displayed');\n    await t.expect(browserPage.BulkActions.bulkActionsPlaceholder.textContent).contains(messageText, 'No pattern message not displayed');\n    // Filter by Hash keys\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    // Verify that prediction of # of keys matching the filter in the entire database displayed\n    await t.expect(browserPage.BulkActions.bulkActionsSummary.textContent).match(expectedAmount, 'Bulk actions summary is not correct');\n    // Verify that % of total keys scanned, # of keys scanned / total # of keys in the database, # of keys matching the filter displayed\n    await t.expect(browserPage.BulkActions.bulkDeleteSummary.innerText).match(scannedKeys, 'Bulk delete summary is not correct');\n\n});\ntest.skip('Verify that user can see blue progress line during the process of bulk deletion', async t => {\n    // Add 500000 Hash keys\n    await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2);\n    // Filter and search by Hash keys added\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    await browserPage.searchByKeyName('hashKey*');\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.expect(browserPage.BulkActions.progressLine.exists).ok('Blue progress line not displayed', { timeout: 5000 });\n    await t.expect(browserPage.BulkActions.bulkStatusInProgress.exists).ok('Progress value not displayed', { timeout: 5000 });\n});\ntest.skip\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Add 1000000 Hash keys\n        await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2);\n        await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2);\n        // Filter and search by Hash keys added\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n        await browserPage.searchByKeyName('hashKey*');\n    })('Verify that bulk deletion is still run when user goes to any other page in the application inside of this DB', async t => {\n        await t.click(browserPage.bulkActionsButton);\n        await browserPage.BulkActions.startBulkDelete();\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        // Go to Browser Page\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await t.expect(browserPage.BulkActions.bulkStatusInProgress.exists).ok('Progress value not displayed', { timeout: 5000 });\n    });\ntest.skip\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Add 500000 keys\n        await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters2);\n        // Filter and search by Hash keys added\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n        await browserPage.searchByKeyName('hashKey*');\n    })('Verify that when user stops bulk deletion, operation is stopped', async t => {\n        await t.click(browserPage.bulkActionsButton);\n        await browserPage.BulkActions.startBulkDelete();\n        await t.click(browserPage.BulkActions.bulkStopButton);\n        const stoppedProgress = parseInt((await browserPage.BulkActions.bulkStatusStopped.innerText).replace(/[^\\d]/g, ''));\n        await t.expect(browserPage.BulkActions.bulkStatusStopped.exists).ok('Progress value not displayed');\n        // Verify that when user stop bulk deletion, he can see the percentage at which the operation was stopped\n        await t.expect(stoppedProgress).gt(1, 'Progress value not displayed');\n        await t.expect(stoppedProgress).lt(100, 'Progress value not correct');\n    });\ntest.skip('Verify that when bulk deletion is completed, status Action completed is displayed', async t => {\n    // Filter by Hash keys\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.expect(browserPage.BulkActions.bulkStatusCompleted.exists).ok('Bulk deletion not completed', { timeout: 15000 });\n    await t.expect(browserPage.BulkActions.bulkStatusCompleted.textContent).eql('Action completed', 'Action completed text is not visible');\n    // Verify that when bulk deletion is completed, button Delete changes to Start New\n    await t.expect(browserPage.BulkActions.bulkStartAgainButton.exists).ok('\"Start New\" button not displayed');\n    // Verify that user can click on Start New and existed filter will be applied\n    await t.click(browserPage.BulkActions.bulkStartAgainButton);\n    await t.expect(browserPage.BulkActions.bulkDeleteSummary.innerText).contains('Scanned 100% (2/2) and found 1 keys', 'Bulk delete summary is not correct');\n});\ntest.skip\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.addSetKey(keyNames[1], '100000', Common.generateWord(20));\n        if (await browserPage.Toast.toastCloseButton.exists) {\n            await t.click(browserPage.Toast.toastCloseButton);\n        }\n        // Add 10000 Hash keys\n        await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters);\n        // Filter by Hash keys\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    })('Verify that after finishing bulk deletion user can see # of processed keys, # of deleted keys, # of errors, execution time', async t => {\n        await t.click(browserPage.bulkActionsButton);\n        await browserPage.BulkActions.startBulkDelete();\n        await t.expect(browserPage.BulkActions.bulkDeleteCompletedSummary.textContent).contains('10 000Keys Processed', 'Bulk delete completed summary not correct');\n        await t.expect(browserPage.BulkActions.bulkDeleteCompletedSummary.textContent).contains('10 000Success', 'Bulk delete completed summary not correct');\n        await t.expect(browserPage.BulkActions.bulkDeleteCompletedSummary.textContent).contains('0Errors', 'Bulk delete completed summary not correct');\n        await t.expect(browserPage.BulkActions.bulkDeleteCompletedSummary.textContent).notContains('0:00:00.00', 'Bulk delete completed summary not correct');\n    });\ntest('Verify that after bulk deletion is completed, user can start new bulk delete', async t => {\n    // Filter by Hash keys\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.click(browserPage.BulkActions.bulkStartAgainButton);\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Stream);\n    await t.expect(browserPage.BulkActions.infoFilter.innerText).contains('Key type:\\nSTREAM', 'Key type is not correct');\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.expect(browserPage.BulkActions.bulkStatusCompleted.textContent).eql('Action completed', 'Action completed text is not visible');\n});\ntest('Verify that when user clicks on Close button when bulk delete is completed, panel is closed, no context is saved', async t => {\n    // Filter by Hash keys\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.click(browserPage.BulkActions.bulkCancelButton);\n    await t.click(browserPage.bulkActionsButton);\n    // Verify context not saved\n    await t.expect(browserPage.BulkActions.bulkDeleteCompletedSummary.exists).notOk('Bulk delete completed summary still displayed');\n    await t.expect(browserPage.BulkActions.bulkDeleteSummary.textContent).contains('Scanned 100% (2/2) and found 1 keys', 'Bulk delete summary is not correct');\n    // Verify that when user clicks on cross icon when bulk delete is completed, panel is closed, no context is saved\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.click(browserPage.BulkActions.bulkClosePanelButton);\n    await t.click(browserPage.bulkActionsButton);\n    // Verify context not saved\n    await t.expect(browserPage.BulkActions.bulkDeleteCompletedSummary.exists).notOk('Bulk delete completed summary still displayed');\n    await t.expect(browserPage.BulkActions.bulkDeleteSummary.textContent).contains('Scanned 100% (2/2) and found 1 keys', 'Bulk delete summary is not correct');\n});\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.addHashKey(keyNames[0], '100000', Common.generateWord(20), Common.generateWord(20));\n    })('Verify that user can see the list of keys when click on “Back” button from the bulk actions', async t => {\n        await t.click(browserPage.bulkActionsButton);\n        await t.expect(browserPage.backToBrowserBtn.exists).notOk('\"< Browser\" button displayed for normal screen resolution');\n        // Minimize the window to check icon\n        await t.resizeWindow(1200, 900);\n        await t.expect(browserPage.keyDetailsTable.visible).ok('Bulk actions not opened', { timeout: 1000 });\n        // Verify that user can see the “Back” button when work with the bulk actions on small resolutions\n        await t.expect(browserPage.backToBrowserBtn.exists).ok('\"< Browser\" button not displayed for small screen resolution');\n        await t.click(browserPage.backToBrowserBtn);\n        // Verify that key details closed\n        await t.expect(browserPage.keyDetailsTable.visible).notOk('Bulk actions not closed by clicking on \"< Browser\" button', { timeout: 1000 });\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/bulk-upload.e2e.ts",
    "content": "import * as path from 'path';\nimport { t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port };\nconst filesToUpload = ['bulkUplAllKeyTypes.txt', 'bigKeysData.rtf'];\nconst filePathes = {\n    allKeysFile: path.join('..', '..', '..', '..', 'test-data', 'bulk-upload', filesToUpload[0]),\n    bigDataFile: path.join('..', '..', '..', '..', 'test-data', 'bulk-upload', filesToUpload[1])\n};\nconst keyNames = ['hashkey1', 'listkey1', 'setkey1', 'zsetkey1', 'stringkey1', 'jsonkey1', 'streamkey1', 'graphkey1', 'tskey1'];\nconst verifyCompletedResultText = async(resultsText: string[]): Promise<void> => {\n    for (const result of resultsText) {\n        await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).contains(result, 'Bulk upload completed summary not correct');\n    }\n    await t.expect(browserPage.BulkActions.bulkUploadCompletedSummary.textContent).notContains('0:00:00.000', 'Bulk upload Time taken not correct');\n};\n\nfixture.skip(`Bulk Upload`)\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await deleteAllKeysFromDB(dbParameters.host, dbParameters.port);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify bulk upload of different text docs formats', async t => {\n    // Verify bulk upload for docker app version\n    const allKeysResults = ['9Commands Processed', '9Success', '0Errors'];\n    const bigKeysResults = ['10 000Commands Processed', '10 000Success', '0Errors'];\n    const defaultText = 'Select or drag and drop a file';\n\n    // Open bulk actions\n    await t.click(browserPage.bulkActionsButton);\n    // Open bulk upload tab\n    await t.click(browserPage.BulkActions.bulkUpdateTab);\n    // Verify that Upload button disabled by default\n    await t.expect(browserPage.BulkActions.actionButton.hasAttribute('disabled')).ok('Upload button enabled without added file');\n\n    // Verify that keys of all types can be uploaded\n    await browserPage.BulkActions.uploadFileInBulk(filePathes.allKeysFile);\n    await verifyCompletedResultText(allKeysResults);\n    await browserPage.searchByKeyName('*key1');\n    await verifyKeysDisplayingInTheList(keyNames, true);\n\n    // Verify that Upload button disabled after starting new upload\n    await t.click(browserPage.BulkActions.bulkActionStartNewButton);\n    await t.expect(browserPage.BulkActions.actionButton.hasAttribute('disabled')).ok('Upload button enabled without added file');\n\n    // Verify that user can remove uploaded file\n    await t.setFilesToUpload(browserPage.BulkActions.bulkUploadInput, [filePathes.bigDataFile]);\n\n    await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(filesToUpload[1], 'Filename not displayed in upload input');\n    await t.click(browserPage.BulkActions.removeFileBtn);\n    await t.expect(browserPage.BulkActions.bulkUploadContainer.textContent).contains(defaultText, 'File not removed from upload input');\n\n    // Verify that user can upload 10000 keys\n    await browserPage.BulkActions.uploadFileInBulk(filePathes.bigDataFile);\n    await verifyCompletedResultText(bigKeysResults);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/consumer-group.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(20);\nlet consumerGroupName = Common.generateWord(20);\nconst keyField = Common.generateWord(20);\nconst keyValue = Common.generateWord(20);\nconst entryIds = [\n    '0',\n    '$',\n    '1654594146318-0'\n];\n\nfixture `Consumer group`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        if (await browserPage.closeKeyButton.exists, { timeout: 500 }) {\n            await t.click(browserPage.closeKeyButton);\n        }\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        // await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can create a new Consumer Group in the current Stream', async t => {\n    const toolTip = [\n        'Enter Valid ID, 0 or $',\n        '\\nSpecify the ID of the last delivered entry in the stream from the new group\\'s perspective.',\n        '\\nOtherwise,',\n        '$',\n        'represents the ID of the last entry in the stream,',\n        '0',\n        'fetches the entire stream from the beginning.'\n    ];\n    keyName = Common.generateWord(20);\n    consumerGroupName = `qwerty123456${Common.generateWord(20)}!@#$%^&*()_+=`;\n\n    // Add New Stream Key\n    await apiKeyRequests.addStreamKeyApi({\n        keyName,\n        entries: [{\n            id: '*',\n            fields: [{\n                name: keyField,\n                value: keyValue,\n            }],\n        }],\n    }, ossStandaloneConfig);\n\n    await browserPage.navigateToKey(keyName);\n    await t.click(browserPage.fullScreenModeButton);\n    // Open Stream consumer groups and add group\n    await t.click(browserPage.streamTabGroups);\n    await browserPage.createConsumerGroup(consumerGroupName);\n    await t.expect(browserPage.streamGroupsContainer.textContent).contains(consumerGroupName, 'The new Consumer Group is not added');\n    // Verify the tooltip under 'i' element\n    await t.click(browserPage.addKeyValueItemsButton);\n    await t.hover(browserPage.entryIdInfoIcon);\n    for (const text of toolTip) {\n        await t.expect(browserPage.tooltip.innerText).contains(text, 'The toolTip message not displayed');\n    }\n});\ntest('Verify that user can input the 0, $ and Valid Entry ID in the ID field', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n\n    // Add New Stream Key\n    await apiKeyRequests.addStreamKeyApi({\n        keyName,\n        entries: [{\n            id: '*',\n            fields: [{\n                name: keyField,\n                value: keyValue,\n            }],\n        }],\n    }, ossStandaloneConfig);\n\n    await browserPage.navigateToKey(keyName);\n    await t.click(browserPage.fullScreenModeButton);\n    // Open Stream consumer groups and add group with different IDs\n    await t.click(browserPage.streamTabGroups);\n    for (const entryId of entryIds) {\n        await browserPage.createConsumerGroup(`${consumerGroupName}${entryId}`, entryId);\n        await t.expect(browserPage.streamGroupsContainer.textContent).contains(`${consumerGroupName}${entryId}`, 'The new Consumer Group is not added');\n    }\n});\ntest('Verify that user can see the Consumer group columns (Group Name, Consumers, Pending, Last Delivered ID)', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const groupColumns = [\n        'Group Name',\n        'Consumers',\n        'Pending',\n        'Last Delivered ID'\n    ];\n    const message = 'Your Consumer Group has no Consumers available.';\n\n    // Add New Stream Key\n    await apiKeyRequests.addStreamKeyApi({\n        keyName,\n        entries: [{\n            id: '*',\n            fields: [{\n                name: keyField,\n                value: keyValue,\n            }],\n        }],\n    }, ossStandaloneConfig);\n\n    await browserPage.navigateToKey(keyName);\n    await t.click(browserPage.fullScreenModeButton);\n    // Open Stream consumer groups and add group with different IDs\n    await t.click(browserPage.streamTabGroups);\n    await browserPage.createConsumerGroup(consumerGroupName);\n    for (let i = 0; i < groupColumns.length; i++) {\n        await t.expect(browserPage.scoreButton.nth(i).textContent).eql(groupColumns[i], `The ${i} Consumer group column name not correct`);\n    }\n    // Verify that user can see the message when there are no Consumers in the Consumer Group\n    await t.click(browserPage.consumerGroup);\n    await t.expect(browserPage.streamConsumersContainer.textContent).contains(message, 'The message for empty Consumer Group not displayed');\n});\ntest('Verify that user can see the Consumer information columns (Consumer Name, Pendings, Idle Time,ms)', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`\n    ];\n    const consumerColumns = [\n        'Consumer Name',\n        'Pending',\n        'Idle Time, msec'\n    ];\n    // Add New Stream Key with groups and consumers\n    for(const command of cliCommands){\n        await browserPage.Cli.sendCommandInCli(command);\n    }\n    // Open Stream consumer info view\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.streamTabGroups);\n    await t.click(browserPage.consumerGroup);\n    for (let i = 0; i < consumerColumns.length; i++) {\n        await t.expect(browserPage.scoreButton.nth(i).textContent).eql(consumerColumns[i], `The ${i} Consumers info column name not correct`);\n    }\n    // Verify that user can navigate to Consumer Groups screen using the link in the breadcrumbs\n    await t.expect(browserPage.streamTabs.exists).ok('Stream navigation tabs visibility');\n    await t.click(browserPage.streamTabGroups);\n    await t.expect(browserPage.streamTabGroups.withAttribute('aria-selected', 'true').exists).ok('The Consumer Groups screen is not opened');\n});\ntest('Verify that user can delete the Consumer from the Consumer Group', async t => {\n    keyName = Common.generateWord(20);\n    const consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName} Bob COUNT 1 STREAMS ${keyName} >`\n    ];\n    // Add New Stream Key with groups and consumers\n    for (const command of cliCommands) {\n        await browserPage.Cli.sendCommandInCli(command);\n    }\n    // Open Stream consumer info view\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.streamTabGroups);\n    await t.click(browserPage.consumerGroup);\n    // Delete consumer and check results\n    const consumerCountBefore = await browserPage.streamConsumerName.count;\n    await t.click(browserPage.removeConsumerButton);\n    await t.expect(browserPage.confirmationMessagePopover.textContent).contains(`will be removed from Consumer Group ${consumerGroupName}`, 'The confirmation message not displayed');\n    await t.click(browserPage.removeConsumerButton.nth(2));\n    await t.expect(browserPage.streamConsumerName.count).eql(consumerCountBefore - 1, 'The Consumers number after deletion not correct');\n});\ntest('Verify that user can delete a Consumer Group', async t => {\n    keyName = Common.generateWord(20);\n    const consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`\n    ];\n    // Add New Stream Key with groups and consumers\n    for (const command of cliCommands) {\n        await browserPage.Cli.sendCommandInCli(command);\n    }\n    // Open Stream consumer info view\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.streamTabGroups);\n    // Verify that user can change the ID set for the Consumer Group when click on the Pencil button\n    for (const id of entryIds) {\n        const idBefore = await browserPage.streamGroupId.textContent;\n        await t.hover(browserPage.streamGroupId);\n        await t.click(browserPage.editStreamLastIdButton);\n        await t.typeText(browserPage.lastIdInput, id, { replace: true, paste: true });\n        await t.click(browserPage.saveButton);\n        await t.expect(browserPage.streamGroupId.textContent).notEql(idBefore, 'The last delivered ID is modified and the table is not reloaded');\n    }\n    // Delete consumer group and check results\n    await t.click(browserPage.removeConsumerGroupButton);\n    await t.expect(browserPage.confirmationMessagePopover.textContent).contains(`${consumerGroupName}and all its consumers will be removed from ${keyName}`, 'The confirmation message not displayed');\n    await t.click(browserPage.removeConsumerGroupButton.nth(1));\n    await t.expect(browserPage.streamGroupsContainer.textContent).contains('Your Key has no Consumer Groups available.', 'The Consumer Group is not removed from the table');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/context.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { verifySearchFilterValue } from '../../../../helpers/keys';\nimport { t } from 'testcafe'\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst speed = 0.4;\nlet keyName = Common.generateWord(10);\nlet keys: string[];\n\nfixture `Browser Context`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n// Update after resolving https://redislabs.atlassian.net/browse/RI-3299\ntest.skip('Verify that user can see saved CLI size on Browser page when he returns back to Browser page', async t => {\n    const offsetY = 200;\n\n    await t.click(browserPage.Cli.cliExpandButton);\n    const cliAreaHeight = await browserPage.Cli.cliArea.clientHeight;\n    const cliAreaHeightEnd = cliAreaHeight + 150;\n    const cliResizeButton = browserPage.Cli.cliResizeButton;\n    await t.hover(cliResizeButton);\n    // move resize 200px up\n    await t.drag(cliResizeButton, 0, -offsetY, { speed: 0.01 });\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    await t.expect(await browserPage.Cli.cliArea.clientHeight).gt(cliAreaHeightEnd, 'Saved context for resizable cli is incorrect');\n});\ntest.skip('Verify that user can see saved Key details and Keys tables size on Browser page when he returns back to Browser page', async t => {\n    const offsetX = 200;\n    const keyListWidth = await browserPage.keyListTable.clientWidth;\n    const cliResizeButton = await browserPage.resizeBtnKeyList;\n\n    // move resize 200px right\n    await t.drag(cliResizeButton, offsetX, 0, { speed });\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    await t.expect(await browserPage.keyListTable.clientWidth).gt(keyListWidth, 'Saved browser resizable context is proper');\n});\ntest('Verify that user can see saved filter per key type applied when he returns back to Browser page', async t => {\n    keyName = Common.generateWord(10);\n    // Filter per key type String and open Settings\n    await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Return back to Browser and check filter applied\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneConfig.databaseName);\n    await t.expect(browserPage.filterByKeyTypeDropDown.innerText).eql(KeyTypesTexts.String, 'Filter per key type is still applied');\n    // Clear filter\n    await browserPage.setAllKeyType();\n    // Filter per key name and open Settings\n    await browserPage.searchByKeyName(keyName);\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Return back to Browser and check filter applied\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneConfig.databaseName);\n    // Verify that user can see saved input entered into the filter per Key name when he returns back to Browser page\n    await verifySearchFilterValue(keyName);\n});\ntest('Verify that user can see saved executed commands in CLI on Browser page when he returns back to Browser page', async t => {\n    const commands = [\n        'SET key',\n        'client getname'\n    ];\n\n    // Execute command in CLI and open Settings page\n    await t.click(browserPage.Cli.cliExpandButton);\n    for(const command of commands) {\n        await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n        await t.pressKey('enter');\n    }\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Return back to Browser and check executed command in CLI\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneConfig.databaseName);\n    for(const command of commands) {\n        await t.expect(browserPage.Cli.cliCommandExecuted.withExactText(command).exists).ok(`Executed command '${command}' in CLI is saved`);\n    }\n});\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })('Verify that user can see key details selected when he returns back to Browser page', async t => {\n        // Scroll keys elements\n        const scrollY = 1000;\n        await t.scroll(browserPage.cssSelectorGrid, 0, scrollY);\n\n        const virtualizedTableKeyIndex = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).nth(10);\n        const targetKeyIndex = await virtualizedTableKeyIndex.getAttribute('aria-rowindex');\n        const targetKey = browserPage.virtualTableContainer.find(`[aria-rowindex=\"${targetKeyIndex}\"`);\n        const targetKeyName = await targetKey.find(browserPage.cssSelectorKey).innerText;\n\n        // Open key details\n        await t.click(targetKey);\n        // Verify that key selected\n        await t.expect(targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list');\n\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        // Return back to Browser and check key details selected\n        await myRedisDatabasePage.navigateToDatabase(ossStandaloneBigConfig.databaseName);\n        // Check Keys details saved\n        await t.expect(browserPage.keyNameFormDetails.innerText).eql(targetKeyName, 'Key details is not saved as context');\n        // Check Key selected in Key List\n        await t.expect(targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list');\n    });\ntest\n    .after(async() => {\n        // Clear and delete database\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n    })('Verify that user can see list of keys viewed on Browser page when he returns back to Browser page', async t => {\n        const numberOfItems = 5000;\n        const scrollY = 3200;\n        // Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        // Create new keys\n        keys = await Common.createArrayWithKeyValue(numberOfItems);\n        await t.typeText(browserPage.Cli.cliCommandInput, `MSET ${keys.join(' ')}`, { replace: true, paste: true });\n        await t.pressKey('enter');\n        await t.click(browserPage.Cli.cliCollapseButton);\n        await t.click(browserPage.refreshKeysButton);\n\n        const keyList = browserPage.keyListTable;\n        const keyListSGrid = keyList.find(browserPage.cssSelectorGrid);\n\n        // Scroll key list\n        await t.scroll(keyListSGrid, 0, scrollY);\n        // Find any key from list that is visible\n        const renderedRows = keyList.find(browserPage.cssSelectorRows);\n        const renderedRowsCount = await renderedRows.count;\n        const randomKey = renderedRows.nth(Math.floor((Math.random() * renderedRowsCount)));\n        const randomKeyName = await randomKey.find(browserPage.cssSelectorKey).textContent;\n\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n\n        // Check that previous found key is still visible\n        const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(randomKeyName);\n        await t.expect(isKeyIsDisplayedInTheList).ok('Scrolled position and saved key list is proper');\n    });\n\n\nfixture `Browser Context`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/filtering-history.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig\n} from '../../../../helpers/conf';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Key name filters history`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    });\ntest('Recent filters history', async t => {\n    const keysForSearch = ['device', 'mobile'];\n\n    await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n    // Verify that user can not see filters per only key type in the history of results\n    await t.expect(browserPage.showFilterHistoryBtn.exists).notOk('Filter history button displayed for key type search');\n    // Search by valid key\n    await browserPage.searchByKeyName(`${keysForSearch[0]}*`);\n    await browserPage.clearFilter();\n\n    // Verify that user can see the history query is automatically run once selected\n    await t.click(browserPage.showFilterHistoryBtn);\n    await t.click(browserPage.filterHistoryOption.nth(0));\n    for (let i = 0; i < 5; i++) {\n        // Verify that keys are filtered\n        await t.expect(browserPage.keyNameInTheList.nth(i).textContent).contains(keysForSearch[0], 'Keys not filtered by key name')\n            .expect(browserPage.filteringLabel.nth(i).textContent).contains(KeyTypesTexts.String, 'Keys not filtered by key type');\n    }\n\n    // Verify that user do not see duplicate history requests\n    await browserPage.clearFilter();\n    await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n    await browserPage.searchByKeyName(`${keysForSearch[0]}*`);\n    await t.click(browserPage.showFilterHistoryBtn);\n    await t.expect(browserPage.filterHistoryItemText.withText(keysForSearch[0]).count).eql(1, 'Filter history requests can be duplicated in list');\n\n    // Refresh the page\n    await browserPage.reloadPage();\n    // Verify that user can see the list of filters even when reloading page\n    await t.click(browserPage.showFilterHistoryBtn);\n    await t.expect(browserPage.filterHistoryItemText.withText(keysForSearch[0]).exists).ok('Filter history requests not saved after reloading page');\n\n    // Open Tree view to check also there\n    await t.click(browserPage.showFilterHistoryBtn);\n    await t.click(browserPage.treeViewButton);\n    // Search by 2nd key name\n    await browserPage.searchByKeyName(`${keysForSearch[1]}*`);\n    await t.click(browserPage.showFilterHistoryBtn);\n    // Verify that user can remove saved filter from list by clicking on \"X\"\n    await t.hover(browserPage.filterHistoryItemText.withText(keysForSearch[1]));\n    await t.click(browserPage.filterHistoryOption.withText(keysForSearch[1]).find(browserPage.cssRemoveSuggestionItem));\n    await t.expect(browserPage.filterHistoryItemText.withText(keysForSearch[1]).exists).notOk('Filter history request not deleted');\n    // Verify that user can clear the history of requests\n    await t.click(browserPage.clearFilterHistoryBtn);\n    await t.expect(browserPage.showFilterHistoryBtn.exists).notOk('Filter history button displayed for key type search');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/filtering.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneConfigEmpty\n} from '../../../../helpers/conf';\nimport { keyLength, KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../../helpers/keys';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keysData = keyTypes.map(object => ({ ...object }));\nkeysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${Common.generateWord(keyLength)}`);\n\nfixture `Filtering per key name in Browser page`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n    });\ntest\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfigEmpty.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfigEmpty);\n    })('Verify that user can search a key with selected data type is filters', async t => {\n        keyName = Common.generateWord(10);\n        // Add new key\n        await browserPage.addStringKey(keyName);\n        // Search by key with full name & specified type\n        await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n        await browserPage.searchByKeyName(keyName);\n        // Verify that key was found\n        const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n        await t.expect(isKeyIsDisplayedInTheList).ok('The key was not found');\n\n        // Verify that key not found when selecting other key type\n        await browserPage.selectFilterGroupType(KeyTypesTexts.List);\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk('The key was found by invalid filter');\n\n        // Verify that user can see filtering per key name starts when he press Enter or clicks the control to filter per key name\n        // Clear filter\n        await t.click(browserPage.clearFilterButton);\n        // Check the filtering starts by press Enter\n        await t.typeText(browserPage.filterByPatterSearchInput, 'InvalidText', { replace: true, paste: true });\n        await t.pressKey('enter');\n        await t.expect(browserPage.searchAdvices.exists).ok('The filtering is not set');\n        // Check the filtering starts by clicks the control\n        await browserPage.reloadPage();\n        await t.typeText(browserPage.filterByPatterSearchInput, 'InvalidText', { replace: true, paste: true });\n        await t.click(browserPage.searchButton);\n        await t.expect(browserPage.searchAdvices.exists).ok('The filtering is not set');\n    });\ntest\n    .after(async() => {\n        // Clear keys and database\n        await deleteKeysViaCli(keysData);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfigEmpty);\n    })('Verify that user can filter keys per data type in Browser page', async t => {\n        keyName = Common.generateWord(10);\n        // Create new keys\n        await addKeysViaCli(keysData);\n        for (const { textType, keyName } of keysData) {\n            await browserPage.selectFilterGroupType(textType);\n            await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok(`The key of type ${textType} was found`);\n            textType !== KeyTypesTexts.Graph\n            ? await t.expect(browserPage.filteringLabel.textContent).contains(textType, 'Keys not filtered by key type')\n            : await t.expect(browserPage.filteringLabel.textContent).contains('graphdata', 'Keys not filtered by key type')\n            const regExp = new RegExp('[1-9]');\n            await t.expect(browserPage.keysNumberOfResults.textContent).match(regExp, 'Number of found keys');\n        }\n        // Check for tree view\n        await t.click(browserPage.treeViewButton);\n        for (const { textType, keyName } of keysData) {\n            await browserPage.selectFilterGroupType(textType);\n            await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok(`The key of type ${textType} was found`);\n            textType !== KeyTypesTexts.Graph\n            ? await t.expect(browserPage.filteringLabel.textContent).contains(textType, 'Keys not filtered by key type')\n            : await t.expect(browserPage.filteringLabel.textContent).contains('graphdata', 'Keys not filtered by key type')\n            const regExp = new RegExp('[1-9]');\n            await t.expect(browserPage.keysNumberOfResults.textContent).match(regExp, 'Number of found keys');\n        }\n    });\ntest\n    .before(async(t) => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })('Verify that user see the key type label when filtering per key types and when removes label the filter is removed on Browser page', async t => { //Check filtering labels\n        for (const { textType } of keyTypes) {\n            await browserPage.selectFilterGroupType(textType);\n            // Check key type label\n            await t.expect((await browserPage.filterByKeyTypeDropDown.innerText).toUpperCase).eql(textType.toUpperCase, `The label of type ${textType} is displayed`);\n            if (['Stream', 'Graph', 'Time Series'].includes(textType)) {\n                await t.expect(browserPage.keysNumberOfResults.textContent).eql('0', 'Number of found keys');\n            }\n            else {\n                const regExp = new RegExp('5..');\n                await t.expect(browserPage.keysNumberOfResults.textContent).match(regExp, 'Number of found keys');\n            }\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/formatters.e2e.ts",
    "content": "import { Selector } from 'testcafe';\nimport { keyLength, KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { addKeysViaCli, deleteKeysViaCli, formattersKeyTypes } from '../../../../helpers/keys';\nimport { Common, DatabaseHelper } from '../../../../helpers';\nimport { BrowserPage, SettingsPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport {\n    binaryFormattersSet,\n    formattersForEditSet,\n    formattersHighlightedSet,\n    formattersWithTooltipSet,\n    notEditableFormattersSet,\n    formatters\n} from '../../../../test-data/formatters-data';\nimport { phpData } from '../../../../test-data/formatters';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst workbenchPage = new WorkbenchPage();\nconst settingsPage = new SettingsPage();\n\nconst keysData = formattersKeyTypes.map(item =>\n    ({ ...item, keyName: `${item.keyName}` + '-' + `${Common.generateWord(keyLength)}` }));\nconst defaultFormatter = 'Unicode';\n\nfixture `Formatters`\n    .meta({\n        type: 'critical_path',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Create new keys\n        await addKeysViaCli(keysData);\n    })\n    .afterEach(async() => {\n        // Clear keys and database\n        await deleteKeysViaCli(keysData);\n    });\nformattersHighlightedSet.forEach(formatter => {\n    test\n        .before(async() => {\n            await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n            // Create new keys\n            await addKeysViaCli(keysData, formatter.fromText, formatter.fromText);\n        })(`Verify that user can see highlighted key details in ${formatter.format} format`, async t => {\n            // Verify for JSON and PHP serialized\n            // Verify for Hash, List, Set, ZSet, String, Stream keys\n            for (const key of keysData) {\n                const valueSelector = Selector(`[data-testid^=${key.keyName.split('-')[0]}][data-testid*=${key.data}]`);\n                await browserPage.navigateToKey(key.keyName);\n                // Verify that value not formatted with default formatter\n                await browserPage.selectFormatter(defaultFormatter);\n                await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).notOk(`${key.textType} Value is formatted to ${formatter.format}`);\n                await browserPage.selectFormatter(formatter.format);\n                // Verify that value is formatted and highlighted\n                await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).ok(`${key.textType} Value is not formatted to ${formatter.format}`);\n                // Verify that Hash field is formatted and highlighted for JSON and PHP serialized\n                if (key.textType === 'Hash') {\n                    await t.expect(browserPage.hashField.find(browserPage.cssJsonValue).exists).ok(`Hash field is not formatted to ${formatter.format}`);\n                }\n                // Verify that Stream field is formatted and highlighted for JSON and PHP serialized\n                if (key.textType === 'Stream') {\n                    await t.expect(Selector(browserPage.cssJsonValue).count).eql(2, `Hash field is not formatted to ${formatter.format}`);\n                }\n            }\n        });\n});\nformattersForEditSet.forEach(formatter => {\n    test(`Verify that user can edit the values in the key regardless if they are valid in ${formatter.format} format or not`, async t => {\n        // Verify for JSON, Msgpack, PHP serialized formatters\n        const invalidText = 'invalid text';\n        // Open key details and select formatter\n        await browserPage.navigateToKey(keysData[0].keyName);\n        await browserPage.selectFormatter(formatter.format);\n        await browserPage.editHashKeyValue(invalidText);\n        await t.click(browserPage.saveButton);\n        // Verify that invalid value can be saved\n        await t.expect(browserPage.hashFieldValue.textContent).contains(invalidText, `Invalid ${formatter.format} value is not saved`);\n        // Add valid value which can be converted\n        await browserPage.editHashKeyValue(formatter.fromText ?? '');\n        // Verify that valid value can be saved on edit\n        formatter.format === 'PHP serialized'\n            ? await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedText ?? '', `Valid ${formatter.format} value is not saved`)\n            : await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromText ?? '', `Valid ${formatter.format} value is not saved`);\n        await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to ${formatter.format}`);\n        await browserPage.editHashKeyValue(formatter.fromTextEdit ?? '');\n        // Verify that valid value can be edited to another valid value\n        await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromTextEdit ?? '', `Valid ${formatter.format} value is not saved`);\n        await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to ${formatter.format}`);\n        if(formatter.format === 'JSON'){\n            // bigInt can be displayed for JSON format\n            await browserPage.editHashKeyValue(formatter.fromBigInt ?? '');\n            await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromBigInt ?? '', `Valid ${formatter.format} value is not saved`);\n        }\n    });\n});\nformattersWithTooltipSet.forEach(formatter => {\n    test(`  ${formatter.format}`, async t => {\n        // Verify for JSON, Msgpack, Protobuf, PHP serialized, Java serialized object, Pickle, Vector 32-bit, Vector 64-bit formatters\n        const failedMessage = `Failed to convert to ${formatter.format}`;\n        for (let i = 0; i < keysData.length; i++) {\n            const valueSelector = Selector(`[data-testid^=${keysData[i].keyName.split('-')[0]}][data-testid*=${keysData[i].data}]`);\n            let innerValueSelector = Selector('');\n            if(keysData[i].keyName.split('-')[0] !== 'string'){\n                innerValueSelector  = valueSelector.find('span');\n            }\n            else{\n                innerValueSelector = valueSelector;\n            }\n            // Open key details and select formatter\n            await browserPage.navigateToKey(keysData[i].keyName);\n            await browserPage.selectFormatter(formatter.format);\n            // Verify that not valid value is not formatted\n            await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).notOk(`${keysData[i].textType} Value is formatted to ${formatter.format}`);\n            await t.hover(innerValueSelector);\n            // Verify that tooltip with convertion failed message displayed\n            await t.expect(browserPage.tooltip.textContent).contains(failedMessage, `\"${failedMessage}\" is not displayed in tooltip`);\n        }\n    });\n});\nbinaryFormattersSet.forEach(formatter => {\n    test\n        .before(async() => {\n            await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n            // Create new keys\n            await addKeysViaCli(keysData, formatter.fromText);\n        })(`Verify that user can see key details converted to ${formatter.format} format`, async t => {\n            // Verify for ASCII, HEX, Binary formatters\n            // Verify for Hash, List, Set, ZSet, String, Stream keys\n            for (let i = 0; i < keysData.length; i++) {\n                const valueSelector = Selector(`[data-testid^=${keysData[i].keyName.split('-')[0]}][data-testid*=${keysData[i].data}]`);\n                await browserPage.navigateToKey(keysData[i].keyName);\n                // Verify that value not formatted with default formatter\n                await browserPage.selectFormatter(defaultFormatter);\n                await t.expect(valueSelector.innerText).contains(formatter.fromText ?? '', `Value is formatted as ${formatter.format} in Unicode`);\n                await browserPage.selectFormatter(formatter.format);\n                // Verify that value is formatted\n                await t.expect(valueSelector.innerText).contains(formatter.formattedText ?? '', `Value is not formatted to ${formatter.format}`);\n                // Verify that Hash field is formatted to ASCII/HEX/Binary\n                if (keysData[i].keyName === 'hash') {\n                    await t.expect(browserPage.hashField.innerText).contains(formatter.formattedText ?? '', `Hash field is not formatted to ${formatter.format}`);\n                }\n            }\n        });\n    test(`Verify that user can edit value for Hash field in ${formatter.format} and convert them to another format`, async t => {\n        // Verify for ASCII, HEX, Binary formatters\n        // Open key details and select formatter\n        await browserPage.navigateToKey(keysData[0].keyName);\n        await browserPage.selectFormatter(formatter.format);\n        // Add value in selected format\n        await browserPage.editHashKeyValue(formatter.formattedText ?? '');\n        // Verify that value saved in selected format\n        await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedText ?? '', `${formatter.format} value is not saved`);\n        await browserPage.selectFormatter('Unicode');\n        // Verify that value converted to Unicode\n        await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromText ?? '', `${formatter.format} value is not converted to Unicode`);\n        await browserPage.selectFormatter(formatter.format);\n        await browserPage.editHashKeyValue(formatter.formattedTextEdit ?? '');\n        // Verify that valid converted value can be edited to another\n        await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedTextEdit ?? '', `${formatter.format} value is not saved`);\n        await browserPage.selectFormatter('Unicode');\n        // Verify that value converted to Unicode\n        await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromTextEdit ?? '', `${formatter.format} value is not converted to Unicode`);\n    });\n});\ntest('Verify that user can format different data types of PHP serialized', async t => {\n    // Open Hash key details\n    await browserPage.navigateToKey(keysData[0].keyName);\n    for (const type of phpData) {\n        //Add fields to the hash key\n        await browserPage.selectFormatter('Unicode');\n        await browserPage.addFieldToHash(type.dataType, type.from);\n        //Search the added field\n        await browserPage.searchByTheValueInKeyDetails(type.dataType);\n        await browserPage.selectFormatter('PHP serialized');\n        // Verify that PHP serialized value is formatted and highlighted\n        await t.expect(browserPage.hashFieldValue.innerText).contains(type.converted, `Value is not saved as PHP ${type.dataType}`);\n        await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to PHP ${type.dataType}`);\n    }\n});\nnotEditableFormattersSet.forEach(formatter => {\n    test(`Verify that user see edit icon disabled for all keys when ${formatter.format} selected`, async t => {\n        // Verify for Protobuf, Java serialized, Pickle, Vector 32-bit, Vector 64-bit\n        // Verify for Hash, List, ZSet, String keys\n        const editableValueKeyTypes = [\n            KeyTypesTexts.Hash,\n            KeyTypesTexts.List,\n            KeyTypesTexts.String\n        ];\n        for (const key of keysData) {\n            if (editableValueKeyTypes.includes(key.textType)) {\n                const editBtn = (key.textType === 'String')\n                    ? browserPage.editKeyValueButton\n                    : Selector(`[data-testid*=${key.keyName.split('-')[0]}][data-testid*=edit-]`, { timeout: 500 });\n                const valueSelector = Selector(`[data-testid^=${key.keyName.split('-')[0]}][data-testid*=${key.data}]`);\n                await browserPage.navigateToKey(key.keyName);\n                await browserPage.selectFormatter(formatter.format);\n                // Verify that edit button disabled\n                await t.hover(valueSelector);\n                await t.expect(editBtn.hasAttribute('disabled')).ok(`Key ${key.textType} is enabled for ${formatter.format} formatter`);\n                // Hover on disabled button\n                await t.hover(editBtn);\n                // Verify tooltip content\n                await t.expect(browserPage.tooltip.textContent).contains('Cannot edit the value in this format', 'Tooltip has wrong text');\n            }\n            if (key.textType === 'Sorted Set') {\n                const editBtn = Selector(`[data-testid*=${key.keyName.split('-')[0]}][data-testid*=edit-]`, { timeout: 500 });\n                const valueSelector = Selector('[data-testid*=zset_content-value]');\n                await browserPage.navigateToKey(key.keyName);\n                await browserPage.selectFormatter(formatter.format);\n                // Verify that edit button enabled for ZSet\n                await t.hover(valueSelector);\n                await t.expect(editBtn.hasAttribute('disabled')).notOk(`Key ${key.textType} is disabled for ${formatter.format} formatter`);\n            }\n        }\n    });\n});\ntest('Verify that user can format timestamp value', async t => {\n    const formatterName = 'Timestamp to DateTime';\n    await browserPage.navigateToKey(keysData[0].keyName);\n    //Add fields to the hash key\n    await browserPage.selectFormatter('Unicode');\n    const formatter = formatters.find(f => f.format === formatterName);\n    if (!formatter) {\n        throw new Error('Formatter  not found');\n    }\n    // add key in sec\n    const hashSec = {\n        field: 'fromTextSec',\n        value: formatter.fromText!\n    };\n    // add key in msec\n    const hashMsec = {\n        field: 'fromTextMsec',\n        value: `${formatter.fromText!}000`\n    };\n    // add key with minus\n    const hashMinusSec = {\n        field: 'fromTextEdit',\n        value: formatter.fromTextEdit!\n    };\n    //Search the added field\n    await browserPage.addFieldToHash(\n        hashSec.field, hashSec.value\n    );\n    await browserPage.addFieldToHash(\n        hashMsec.field, hashMsec.value\n    );\n    await browserPage.addFieldToHash(\n        hashMinusSec.field, hashMinusSec.value\n    );\n\n    await browserPage.searchByTheValueInKeyDetails(hashSec.field);\n    await browserPage.selectFormatter('DateTime');\n    await t.expect(await browserPage.getHashKeyValue()).eql(formatter.formattedText!, `Value is not formatted as DateTime ${formatter.fromText}`);\n\n    await browserPage.searchByTheValueInKeyDetails(hashMsec.field);\n    await t.expect(await browserPage.getHashKeyValue()).eql(formatter.formattedText!, `Value is not formatted as DateTime ${formatter.fromTextEdit}`);\n\n    await browserPage.searchByTheValueInKeyDetails(hashMinusSec.field);\n    await t.expect(await browserPage.getHashKeyValue()).eql(formatter.formattedTextEdit!, `Value is not formatted as DateTime ${formatter.fromTextEdit}`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/hash-field.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = 2147476121;\nconst keyFieldValue = 'hashField11111';\nconst keyValue = 'hashValue11111!';\n\nfixture `Hash Key fields verification`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can search by full field name in Hash', async t => {\n    keyName = Common.generateWord(10);\n\n    await apiKeyRequests.addHashKeyApi({\n        keyName,\n        ttl: keyTTL,\n        fields: [{\n            field: keyFieldValue,\n            value: keyValue\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Search by full field name\n    await browserPage.searchByTheValueInKeyDetails(keyFieldValue);\n    // Check the search result\n    const result = browserPage.hashFieldsList.nth(0).textContent;\n    await t.expect(result).contains(keyFieldValue, 'The hash field not found by full field name');\n    // Verify that user can search by part field name in Hash with pattern * in Hash\n    await browserPage.secondarySearchByTheValueInKeyDetails('hashField*');\n    // Check the search result\n    await t.expect(result).eql(keyFieldValue, 'The hash field');\n    // Search by part field name and the * in the beggining\n    await browserPage.secondarySearchByTheValueInKeyDetails('*11111');\n    // Check the search result\n    await t.expect(result).eql(keyFieldValue, 'The hash field');\n    // Search by part field name and the * in the middle\n    await browserPage.secondarySearchByTheValueInKeyDetails('hash*11111');\n    // Check the search result\n    await t.expect(result).eql(keyFieldValue, 'The hash field not found by pattern');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/json-key.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst value = '{\"name\":\"xyz\"}';\n\nfixture `JSON Key verification`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can not add invalid JSON structure inside of created JSON', async t => {\n    keyName = Common.generateWord(10);\n    // Add Json key with json object\n    await browserPage.addJsonKey(keyName, value, keyTTL);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification');\n    await t.click(browserPage.Toast.toastCloseButton);\n    // Add key with value on the same level\n    await browserPage.addJsonKeyOnTheSameLevel('\"key1\"', '{}');\n    // Add invalid JSON structure\n    await browserPage.addJsonStructure('{\"name\": \"Joe\", \"age\": null, }');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.jsonError.textContent).eql('Value should have JSON format.', 'The json object error not displayed');\n    // Add another invalid JSON structure\n    await t.click(browserPage.refreshKeyButton);\n    await browserPage.addJsonStructure('{\"name\": \"Joe\", \"age\": null]');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.jsonError.textContent).eql('Value should have JSON format.', 'The json object error not displayed');\n    // Add another invalid JSON structure\n    await t.click(browserPage.refreshKeyButton);\n    await browserPage.addJsonStructure('{\"name\": \"Joe\", \"age\": null, }');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.jsonError.textContent).eql('Value should have JSON format.', 'The json object error not displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/key-details.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName: string;\n\nfixture `Key Details`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        keyName = Common.generateWord(10);\n        await browserPage.addStringKey(keyName);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest\n.skip('Verify that user can see the list of keys when click on “Back” button', async t => {\n    await t.expect(browserPage.backToBrowserBtn.exists).notOk('\"< Browser\" button displayed for normal screen resolution');\n    // Minimize the window to check icon\n    await t.resizeWindow(1200, 900);\n    await t.expect(browserPage.keyDetailsTable.visible).ok('Key details not opened', { timeout: 1000 });\n    // Verify that user can see the “Back” button when work with the values of keys on small resolutions\n    await t.expect(browserPage.backToBrowserBtn.exists).ok('\"< Browser\" button not displayed for small screen resolution');\n    await t.click(browserPage.backToBrowserBtn);\n    // Verify that key details closed\n    await t.expect(browserPage.keyDetailsTable.visible).notOk('Key details not closed by clicking on \"< Browser\" button', { timeout: 1000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/keylist-actions.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keyName: string;\n\nfixture `Actions with Key List on Browser page`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        keyName = Common.generateWord(10);\n    });\ntest('Verify that user can delete key in List mode', async t => {\n    // Add new key\n    await browserPage.addStringKey(keyName);\n    await browserPage.deleteKeyByNameFromList(keyName);\n    await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk('The Key wasn\\'t deleted');\n});\ntest('Verify that user can delete key in Tree view', async t => {\n    // Add new key\n    await browserPage.addStringKey(keyName);\n    await t.click(browserPage.treeViewButton);\n    await browserPage.deleteKeyByNameFromList(keyName);\n    await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk('The Key wasn\\'t deleted');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/large-data.e2e.ts",
    "content": "import * as fs from 'fs';\nimport { join as joinPath } from 'path';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { Common } from '../../../../helpers/common';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, fileDownloadPath, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nimport { StringKeyParameters } from '../../../../pageObjects/browser-page';\nimport { DatabasesActions } from '../../../../common-actions/databases-actions';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst databasesActions = new DatabasesActions();\n\nlet keyName = Common.generateWord(10);\nlet bigKeyName = Common.generateWord(10);\nlet foundStringDownloadedFiles = 0;\nconst downloadedFile = 'string_value';\n\nfixture `Cases with large data`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can see relevant information about key size', async t => {\n    keyName = Common.generateWord(10);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Create new key with a lot of members\n    const length = 500;\n    const arr = await Common.createArrayWithKeyValue(length);\n    await t.typeText(browserPage.Cli.cliCommandInput, `HSET ${keyName} ${arr.join(' ')}`, { paste: true });\n    await t.pressKey('enter');\n    await t.click(browserPage.Cli.cliCollapseButton);\n    await browserPage.openKeyDetails(keyName);\n    // Remember the values of the key size and length\n    const keySizeText = await browserPage.keySizeDetails.textContent;\n    const keyLength = await browserPage.keyLengthDetails.textContent;\n    const sizeArray = keySizeText.split(' ');\n    const keySize = sizeArray[sizeArray.length - 2];\n    // Verify that user can see relevant information about key length\n    await t.expect(keyLength).eql(`Length: ${length}`, 'Key length not correct');\n    // Verify that user can see relevant information about key size\n    await t.expect(keySizeText).contains('KB', 'Key measure not correct');\n    await t.expect(+keySize).gt(10, 'Key size value not correct');\n});\ntest\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(bigKeyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n        // Delete downloaded file\n        const foundDownloadedFiles = await databasesActions.findFilesByFileStarts(fileDownloadPath, downloadedFile);\n        fs.unlinkSync(joinPath(fileDownloadPath, foundDownloadedFiles[0]));\n    })('Verify that user can download String key value as txt file when it has > 5000 characters', async t => {\n        const disabledEditTooltip = 'Load the entire value to edit it';\n        const disabledFormattersTooltip = 'Load the entire value to select a format';\n        keyName = Common.generateWord(10);\n        bigKeyName = Common.generateWord(10);\n        // Create string key with 5000 characters\n        const length = 5000;\n        const keyValue = Common.generateWord(length);\n        const stringKeyParameters: StringKeyParameters = {\n            keyName: keyName,\n            value: keyValue\n        };\n        const bigStringKeyParameters: StringKeyParameters = {\n            keyName: bigKeyName,\n            value: keyValue + 1\n        };\n\n        await apiKeyRequests.addStringKeyApi(stringKeyParameters, ossStandaloneConfig);\n        await apiKeyRequests.addStringKeyApi(bigStringKeyParameters, ossStandaloneConfig);\n        await browserPage.reloadPage();\n        await browserPage.openKeyDetails(keyName);\n        await t.expect(browserPage.loadAllBtn.exists).notOk('Load All button displayed for 5000 length String key');\n        await t.expect(browserPage.downloadAllValueBtn.exists).notOk('Download All button displayed for 5000 length String key');\n\n        await browserPage.openKeyDetails(bigKeyName);\n        await t.expect(browserPage.editKeyValueButton.hasAttribute('disabled')).ok('Edit button not disabled for String > 5000');\n        await t.expect(browserPage.formatSwitcher.hasAttribute('disabled')).ok('Formatters control not disabled for String > 5000');\n\n        // Verify that user can see \"Load the entire value to edit it.\" tooltip when hovering on disabled edit button before loading all\n        await t.hover(browserPage.editKeyValueButton.parent());\n        await t.expect(browserPage.tooltip.textContent).eql(disabledEditTooltip, 'Edit button tooltip contains invalid message');\n\n        // Verify that user can see \"Load the entire value to select a format.\" tooltip when hovering on disabled formatters button before loading all\n        await t.hover(browserPage.formatSwitcher.child('span'));\n        await t.expect(browserPage.tooltip.textContent).eql(disabledFormattersTooltip, 'Formatter button tooltip contains invalid message');\n\n        // Verify that user can see String key value with only 5000 characters uploaded if length is more than 5000\n        // Verify that 3 dots after truncated big strings displayed\n        await t.expect((await browserPage.stringKeyValueInput.textContent).length).eql(stringKeyParameters.value.length + 3, 'String key > 5000 value is fully loaded by default');\n\n        await t.click(browserPage.loadAllBtn);\n        // Verify that user can see \"Load all\" button for String Key with more than 5000 characters and see full value by clicking on it\n        await t.expect((await browserPage.stringKeyValueInput.textContent).length).eql(bigStringKeyParameters.value.length, 'String key > 5000 value is not fully loaded after clicking Load All');\n        await t.expect(browserPage.editKeyValueButton.hasAttribute('disabled')).notOk('Edit button disabled for String > 5000 which is fully loaded');\n        await t.expect(browserPage.formatSwitcher.hasAttribute('disabled')).notOk('Formatters control disabled for String > 5000 which is fully loaded');\n\n        // Verify that user can see not fully loaded String key with > 5000 characters after clicking on Refresh button\n        await t.click(browserPage.refreshKeyButton);\n        await t.expect(browserPage.loadAllBtn.exists).ok('Load All button not displayed for 5000 length String key after Refresh');\n\n        // Verify that user can download String key value as txt file when it has > 5000 characters\n        await t.click(browserPage.downloadAllValueBtn);\n        // Verify that user can see default file name is “string_value” when downloading String key value\n        foundStringDownloadedFiles = await databasesActions.getFileCount(fileDownloadPath, downloadedFile);\n        await t.expect(foundStringDownloadedFiles).gt(0, 'String value file not saved');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/list-key.e2e.ts",
    "content": "import { toNumber } from 'lodash';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig,\n    ossStandaloneV5Config\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = 2147476121;\nconst elements = ['1111listElement11111', '2222listElement22222', '33333listElement33333'];\n\nfixture `List Key verification`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can search List element by index', async t => {\n    keyName = Common.generateWord(10);\n\n    await apiKeyRequests.addListKeyApi({\n        keyName,\n        ttl: keyTTL,\n        elements,\n    }, ossStandaloneConfig);\n\n    await browserPage.navigateToKey(keyName);\n\n    // Search List element by index\n    await browserPage.searchByTheValueInKeyDetails('1');\n    // Check the search result\n    const result = await browserPage.listElementsList.nth(0).textContent;\n    await t.expect(result).eql(elements[1], 'The list elemnt with searched index not found');\n});\ntest\n    .before(async() => {\n        // add oss standalone v5\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n    })\n    .after(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneV5Config.databaseName);\n    })('Verify that user can remove only one element for List for Redis v. <6.2', async t => {\n        keyName = Common.generateWord(10);\n        // Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        // Create new key\n        await t.typeText(browserPage.Cli.cliCommandInput, `LPUSH ${keyName} 1 2 3 4 5`, { paste: true });\n        await t.pressKey('enter');\n        await t.click(browserPage.Cli.cliCollapseButton);\n        // Remove element from the key\n        await browserPage.navigateToKey(keyName);\n        const lengthBeforeRemove = (await browserPage.keyLengthDetails.textContent).split(': ')[1];\n        await browserPage.removeListElementFromHeadOld();\n        // Check that only one element is removed\n        const lengthAfterRemove = (await browserPage.keyLengthDetails.textContent).split(': ')[1];\n        const removedElements = toNumber(lengthBeforeRemove) - toNumber(lengthAfterRemove);\n        await t.expect(removedElements).eql(1, 'only one element is removed');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/scan-keys.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    BrowserPage,\n    SettingsPage\n} from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keys: string[] = [];\n\nconst explicitErrorHandler = (): void => {\n    window.addEventListener('error', e => {\n        if(e.message === 'ResizeObserver loop limit exceeded') {\n            e.stopImmediatePropagation();\n        }\n    });\n};\n\nfixture `Browser - Specify Keys to Scan`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .clientScripts({ content: `(${explicitErrorHandler.toString()})()` })\n    .beforeEach(async(t) => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .afterEach(async t => {\n        //Clear and delete database\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionAdvancedSettings);\n        await settingsPage.changeKeysToScanValue('10000');\n    });\ntest('Verify that the user can see this number of keys applied to new filter requests and to \"scan more\" functionality in Browser page', async t => {\n    const searchPattern = 'key[12]*';\n    // Go to Settings page\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Specify keys to scan\n    await t.click(settingsPage.accordionAdvancedSettings);\n    await settingsPage.changeKeysToScanValue('1000');\n    // Go to Browser Page\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Connect to DB\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Create new keys\n    keys = await Common.createArrayWithKeyValue(2500);\n    await t.typeText(browserPage.Cli.cliCommandInput, `MSET ${keys.join(' ')}`, { paste: true });\n    await t.pressKey('enter');\n    await t.click(browserPage.Cli.cliCollapseButton);\n    // Search keys\n    await browserPage.searchByKeyName(searchPattern);\n    const keysNumberOfScanned = await browserPage.scannedValue.textContent;\n    // Verify that number of scanned is 1000\n    await t.expect(keysNumberOfScanned).contains('1 000', 'Number of scanned is not 1000');\n    // Scan more\n    await t.click(browserPage.scanMoreButton);\n    const keysNumberOfScannedScanMore = await browserPage.scannedValue.textContent;\n    // Verify that number of results is 2000\n    await t.expect(keysNumberOfScannedScanMore).contains('2 000', 'Number of scanned is not 2000');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { Telemetry } from '../../../../helpers/telemetry';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneConfig,\n    ossStandaloneV5Config\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nimport { goBackHistory } from '../../../../helpers/utils';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst telemetry = new Telemetry();\n\nconst telemetryEvents = ['SEARCH_MODE_CHANGED', 'SEARCH_INDEX_ADD_BUTTON_CLICKED', 'SEARCH_INDEX_ADDED'];\nconst logger = telemetry.createLogger();\nconst expectedPropertiesMode = [\n    'current',\n    'databaseId',\n    'previous',\n    'provider',\n    'view'\n];\n\nconst expectedPropertiesCreateIndex = [\n    'databaseId',\n    'provider',\n    'view'\n];\n\nconst expectedPropertiesAddedIndex = [\n    'countOfFieldNames',\n    'countOfPrefixes',\n    'dataType',\n    'databaseId',\n    'provider',\n    'view'\n];\n\nconst patternModeTooltipText = 'Filter by Key Name or Pattern';\nconst redisearchModeTooltipText = 'Search by Values of Keys';\nconst notSelectedIndexText = 'Select an index and enter a query to search per values of keys.';\nconst searchPerValue = '(@name:\"Hall School\") | (@students:[500, 1000])';\nlet keyName = Common.generateWord(10);\nlet keyNames: string[];\nlet indexName = Common.generateWord(5);\n\nconst keyNameSimpleDb = Common.generateWord(10);\nconst keyNameBigDb = Common.generateWord(10);\n\nconst indexNameSimpleDb = `idx:${keyNameSimpleDb}`; // index in the standalone database\nconst indexNameBigDb = `idx:${keyNameBigDb}`; // index in the big standalone database\n\nconst simpleDbName = ossStandaloneConfig.databaseName;\nconst bigDbName = ossStandaloneBigConfig.databaseName;\nasync function verifyContext(): Promise<void> {\n    await t\n        .expect(browserPage.selectIndexDdn.withText(indexName).exists).ok('Index selection not saved')\n        .expect(browserPage.filterByPatterSearchInput.value).eql(searchPerValue, 'Search per Value not saved in input')\n        .expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Key details not opened');\n}\n\n// todo: rework tests. seems flaky. requires database to be empty to verify keys existence.\nfixture `Search capabilities in Browser`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        keyName = Common.generateWord(10);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName}`]);\n    })('RediSearch capabilities in Browser view to search per Hashes or JSONs', async t => {\n        indexName = `idx:${keyName}`;\n        keyNames = [`${keyName}:1`, `${keyName}:2`, `${keyName}:3`];\n        const commands = [\n            `HSET ${keyName} \"\" \"\"`,\n            `HSET ${keyNames[0]} \"name\" \"Hall School\" \"description\" \" Spanning 10 states\" \"class\" \"independent\" \"type\" \"traditional\" \"address_city\" \"London\" \"address_street\" \"Manor Street\" \"students\" 342 \"location\" \"51.445417, -0.258352\"`,\n            `HSET ${keyNames[1]} \"name\" \"Garden School\" \"description\" \"Garden School is a new outdoor\" \"class\" \"state\" \"type\" \"forest; montessori;\" \"address_city\" \"London\" \"address_street\" \"Gordon Street\" \"students\" 1452 \"location\" \"51.402926, -0.321523\"`,\n            `HSET ${keyNames[2]} \"name\" \"Gillford School\" \"description\" \"Gillford School is a centre\" \"class\" \"private\" \"type\" \"democratic; waldorf\" \"address_city\" \"Goudhurst\" \"address_street\" \"Goudhurst\" \"students\" 721 \"location\" \"51.112685, 0.451076\"`,\n            `FT.CREATE ${indexName} ON HASH PREFIX 1 \"${keyName}:\" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR \";\" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`\n        ];\n\n        // Create 3 keys and index\n        await browserPage.Cli.sendCommandsInCli(commands);\n        // Verify that user see the tooltips for the controls to switch the modes\n        await t.hover(browserPage.patternModeBtn);\n        await t.expect(browserPage.tooltip.textContent).contains(patternModeTooltipText, 'Invalid text in pattern mode tooltip');\n        await t.hover(browserPage.redisearchModeBtn);\n        await t.expect(browserPage.tooltip.textContent).contains(redisearchModeTooltipText, 'Invalid text in redisearch mode tooltip');\n\n        // Verify that user see the \"Select an index\" message when he switches to Search\n        await t.click(browserPage.redisearchModeBtn);\n        await t.expect(browserPage.keyListTable.textContent).contains(notSelectedIndexText, 'Select an index message not displayed');\n\n        // Verify that user can search by index in Browser view\n        await browserPage.selectIndexByName(indexName);\n        await verifyKeysDisplayingInTheList(keyNames, true);\n        await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Key without index displayed after search');\n        // Verify that user can search by index plus key value\n        await browserPage.searchByKeyName('Hall School');\n        await t.expect(browserPage.getKeySelectorByName(keyNames[0]).exists).ok(`The key ${keyNames[0]} not found`);\n        await t.expect(browserPage.getKeySelectorByName(keyNames[1]).exists).notOk(`Invalid key ${keyNames[1]} is displayed after search`);\n        // Verify that user can search by index plus multiple key values\n        await browserPage.searchByKeyName(searchPerValue);\n        await t.expect(browserPage.getKeySelectorByName(keyNames[0]).exists).ok(`The first valid key ${keyNames[0]} not found`);\n        await t.expect(browserPage.getKeySelectorByName(keyNames[2]).exists).ok(`The second valid key ${keyNames[2]} not found`);\n        await t.expect(browserPage.getKeySelectorByName(keyNames[1]).exists).notOk(`Invalid key ${keyNames[1]} is displayed after search`);\n\n        // Verify that user can use filter history for RediSearch query\n        await t.click(browserPage.showFilterHistoryBtn);\n        await t.click(browserPage.filterHistoryOption.withText('Hall School'));\n        await t.expect(browserPage.getKeySelectorByName(keyNames[0]).exists).ok(`The key ${keyNames[0]} not found`);\n        await t.expect(browserPage.getKeySelectorByName(keyNames[1]).exists).notOk(`Invalid key ${keyNames[1]} is displayed after search`);\n\n        // Verify that user can clear the search\n        await t.click(browserPage.clearFilterButton);\n        await t.expect(browserPage.getKeySelectorByName(keyNames[1]).exists).ok(`The key ${keyNames[1]} not found`);\n        await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Search not cleared');\n\n        // Verify that user can search by index in Tree view\n        await t.click(browserPage.treeViewButton);\n        // Change delimiter\n        await browserPage.TreeView.changeDelimiterInTreeView('-');\n        await browserPage.selectIndexByName(indexName);\n        await verifyKeysDisplayingInTheList(keyNames, true);\n        await t.expect(browserPage.getKeySelectorByName(keyName).exists).notOk('Key without index displayed after search');\n\n        // Verify that user see the database scanned when he switches to Pattern search mode\n        await t.click(browserPage.patternModeBtn);\n        await t.click(browserPage.browserViewButton);\n        await verifyKeysDisplayingInTheList(keyNames, true);\n        await t.expect(browserPage.getKeySelectorByName(keyName).exists).ok('Database not scanned after returning to Pattern search mode');\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`);\n    })('Search by index keys scanned for JSON', async t => {\n        keyName = Common.generateWord(10);\n        indexName = `idx:${keyName}`;\n        await t.click(browserPage.redisearchModeBtn);\n        const command = `FT.CREATE ${indexName} ON JSON PREFIX 1 \"device:\" SCHEMA id numeric`;\n\n        // Create index for JSON keys\n        await browserPage.Cli.sendCommandInCli(command);\n        await t.click(browserPage.refreshIndexButton);\n        // Verify that user can can get 500 keys (limit 0 500) in Browser view\n        await browserPage.selectIndexByName(indexName);\n        // Verify that all keys are displayed according to selected index\n        for (let i = 0; i < 15; i++) {\n            await t.expect(browserPage.keyListItem.textContent).contains('device:', 'Keys out of index displayed');\n        }\n        // Verify that user can can get 10 000 keys in Tree view\n        await t.click(browserPage.treeViewButton);\n        const keysNumberOfResults = browserPage.keysNumberOfResults.textContent;\n        await t.expect(keysNumberOfResults).contains('10 000', 'Number of results is not 10 000');\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n    })('No Redis Query Engine module message', async t => {\n        const noRedisearchMessage = 'Redis Query Engine is not available for this database';\n        const externalPageLinkFirst = 'https://redis.io/try-free';\n\n        await t.click(browserPage.redisearchModeBtn);\n        // Verify that user can see message in the dialog when he doesn't have Redis Query Engine module\n        await t.expect(browserPage.noReadySearchDialogTitle.textContent).contains(noRedisearchMessage, 'Invalid text in no Redis Query Engine popover');\n\n        // Verify that user can navigate by link to create a Redis db\n        await t.click(browserPage.redisearchFreeLink);\n        await Common.checkURLContainsText(externalPageLinkFirst);\n        await Common.checkURLContainsText('utm_source=redisinsight');\n        await Common.checkURLContainsText('utm_medium=app');\n        await Common.checkURLContainsText('utm_campaign=redisinsight_browser_search');\n    });\ntest.requestHooks(logger)\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`);\n    })('Index creation', async t => {\n        const createIndexLink = 'https://redis.io/docs/latest/commands/ft.create/';\n\n        // Verify that telemetry event 'SEARCH_MODE_CHANGED' sent\n        await t.click(browserPage.redisearchModeBtn);\n        await telemetry.verifyEventHasProperties(telemetryEvents[0], expectedPropertiesMode, logger);\n\n        // Verify that user can cancel index creation\n        await t.click(browserPage.selectIndexDdn);\n        await t.click(browserPage.createIndexBtn);\n\n        // Verify that telemetry event 'SEARCH_INDEX_ADD_BUTTON_CLICKED' sent\n        await telemetry.verifyEventHasProperties(telemetryEvents[1], expectedPropertiesCreateIndex, logger);\n\n        await t.expect(browserPage.newIndexPanel.exists).ok('New Index panel is not displayed');\n        await t.click(browserPage.cancelIndexCreationBtn);\n        await t.expect(browserPage.newIndexPanel.exists).notOk('New Index panel is displayed');\n\n        // Verify that user can create an index with all mandatory parameters\n        await t.click(browserPage.redisearchModeBtn);\n        await t.click(browserPage.selectIndexDdn);\n        await t.click(browserPage.createIndexBtn);\n        await t.expect(browserPage.newIndexPanel.exists).ok('New Index panel is not displayed');\n\n        // Verify that user can see a link to create a profound index and navigate\n        await t.click(browserPage.newIndexPanel.find('a'));\n        await Common.checkURL(createIndexLink);\n        await goBackHistory();\n\n        // Verify that user can create an index with multiple prefixes\n        await t.click(browserPage.selectIndexDdn);\n        await t.click(browserPage.createIndexBtn);\n        await t.click(browserPage.indexNameInput);\n        await t.typeText(browserPage.indexNameInput, indexName);\n        await t.click(browserPage.prefixFieldInput);\n        await t.typeText(browserPage.prefixFieldInput, 'device:');\n        await t.pressKey('enter');\n        await t.typeText(browserPage.prefixFieldInput, 'mobile_');\n        await t.pressKey('enter');\n        await t.typeText(browserPage.prefixFieldInput, 'user_');\n        await t.pressKey('enter');\n        await t.expect(browserPage.prefixFieldInput.find('button').count).eql(3, '3 prefixes are not displayed');\n\n        // Verify that user can create an index with multiple fields (up to 20)\n        await t.click(browserPage.indexIdentifierInput);\n        await t.typeText(browserPage.indexIdentifierInput, 'k0');\n        await t.click(browserPage.confirmIndexCreationBtn);\n\n        await telemetry.verifyEventHasProperties(telemetryEvents[2], expectedPropertiesAddedIndex, logger);\n        await telemetry.verifyEventPropertyValue(telemetryEvents[2], 'countOfPrefixes', '3', logger);\n\n        await t.expect(browserPage.newIndexPanel.exists).notOk('New Index panel is displayed');\n        await t.click(browserPage.selectIndexDdn);\n        await browserPage.selectIndexByName(indexName);\n    }).skip.meta({skipComment: \"Unstable CI execution, after hook error, needs investigation \"});\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        // await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexName}`);\n    })('Context for RediSearch capability', async t => {\n        keyName = Common.generateWord(10);\n        indexName = `idx:${keyName}`;\n        const commands = [\n            `HSET ${keyName} \"name\" \"Hall School\" \"description\" \" Spanning 10 states\" \"class\" \"independent\" \"type\" \"traditional\" \"address_city\" \"London\" \"address_street\" \"Manor Street\" \"students\" 342 \"location\" \"51.445417, -0.258352\"`,\n            `FT.CREATE ${indexName} ON HASH PREFIX 1 \"${keyName}\" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR \";\" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`\n        ];\n\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.redisearchModeBtn);\n        await browserPage.selectIndexByName(indexName);\n        await browserPage.searchByKeyName(searchPerValue);\n        // Select key\n        await t.click(browserPage.getKeySelectorByName(keyName));\n\n        // Verify that Redisearch context (inputs, key selected, scroll, key details) saved after switching between pages\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await verifyContext();\n\n        // Verify that Redisearch context saved when switching between browser/tree view\n        await t.click(browserPage.treeViewButton);\n        await verifyContext();\n        await t.click(browserPage.browserViewButton);\n        await verifyContext();\n\n        // Verify that Search control opened after reloading page\n        await browserPage.reloadPage();\n        await t.expect(browserPage.keyListTable.textContent).contains(notSelectedIndexText, 'Search by Values of Keys section not opened');\n    });\n\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        //clear database\n        await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexNameBigDb}`);\n        await t.click(browserPage.OverviewPanel.myRedisDBLink); // go back to database selection page\n        await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database\n        await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${indexNameSimpleDb}`);\n        await t.click(browserPage.patternModeBtn);\n        await t.click(browserPage.browserViewButton);\n        await browserPage.deleteKeysByNames(keyNames);\n\n        //delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig);\n    })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => {\n        // Link to ticket: https://redislabs.atlassian.net/browse/RI-3863\n\n        // key names to validate in the standalone database\n        keyNames = [`${keyNameSimpleDb}:1`, `${keyNameSimpleDb}:2`, `${keyNameSimpleDb}:3`, `${keyNameSimpleDb}:4`, `${keyNameSimpleDb}:5`];\n\n        /*\n            create index as name ${indexNameBigDb}\n            in the big standalone database\n            with the help of CLI\n        */\n        const commandsForBigStandalone = [\n            `FT.CREATE ${indexNameBigDb} ON hash PREFIX 1 mobile SCHEMA k0 text`\n        ];\n\n        await browserPage.Cli.sendCommandsInCli(commandsForBigStandalone);\n\n        await t.click(browserPage.OverviewPanel.myRedisDBLink); // go back to database selection page\n        await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database\n\n        const commandsForStandalone = [\n            `HSET ${keyNames[0]} \"name\" \"Hall School\" \"description\" \" Spanning 10 states\" \"class\" \"independent\" \"type\" \"traditional\" \"address_city\" \"London\" \"address_street\" \"Manor Street\" \"students\" 342 \"location\" \"51.445417, -0.258352\"`,\n            `HSET ${keyNames[1]} \"name\" \"Garden School\" \"description\" \"Garden School is a new outdoor\" \"class\" \"state\" \"type\" \"forest; montessori;\" \"address_city\" \"London\" \"address_street\" \"Gordon Street\" \"students\" 1452 \"location\" \"51.402926, -0.321523\"`,\n            `HSET ${keyNames[2]} \"name\" \"Gillford School\" \"description\" \"Gillford School is a centre\" \"class\" \"private\" \"type\" \"democratic; waldorf\" \"address_city\" \"Goudhurst\" \"address_street\" \"Goudhurst\" \"students\" 721 \"location\" \"51.112685, 0.451076\"`,\n            `HSET ${keyNames[3]} \"name\" \"Box School\" \"description\" \"Top School is a new outdoor\" \"class\" \"state\" \"type\" \"forest; montessori;\" \"address_city\" \"London\" \"address_street\" \"Gordon Street\" \"students\" 1452 \"location\" \"51.402926, -0.321523\"`,\n            `HSET ${keyNames[4]} \"name\" \"Bill School\" \"description\" \"Billing School is a centre\" \"class\" \"private\" \"type\" \"democratic; waldorf\" \"address_city\" \"Goudhurst\" \"address_street\" \"Goudhurst\" \"students\" 721 \"location\" \"51.112685, 0.451076\"`,\n            `FT.CREATE ${indexNameSimpleDb} ON HASH PREFIX 1 \"${keyNameSimpleDb}:\" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR \";\" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`\n        ];\n        // Create 5 keys and index\n        await browserPage.Cli.sendCommandsInCli(commandsForStandalone);\n\n        await t.click(browserPage.treeViewButton); // switch to tree view\n        await t.click(browserPage.redisearchModeBtn); // click redisearch button\n        await browserPage.selectIndexByName(indexNameSimpleDb); // select pre-created index in the standalone database\n        await browserPage.TreeView.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily\n\n        await verifyKeysDisplayingInTheList(keyNames, true); // verify created keys are visible\n\n        await t.click(browserPage.OverviewPanel.myRedisDBLink); // go back to database selection page\n        await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName\n\n        await verifyKeysDisplayingInTheList(keyNames, false); // Verify that standandalone database keys are NOT visible\n\n        await t.expect(Selector('span').withText('Select Index').exists).ok('Index is still selected');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/set-key.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = 2147476121;\nconst keyMember = '1111setMember11111';\n\nfixture `Set Key fields verification`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can search by full member name in Set', async t => {\n    keyName = Common.generateWord(10);\n\n    await apiKeyRequests.addSetKeyApi({\n        keyName,\n        ttl: keyTTL,\n        members: ['1111', keyMember],\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName)\n\n    await browserPage.searchByTheValueInSetKey(keyMember);\n    // Verify search by full member name\n    let result = await browserPage.setMembersList.nth(0).textContent;\n    await t.expect(result).eql(keyMember, 'The set member not found');\n\n    // Verify that user can search by part member name with pattern * in Set\n    await browserPage.searchByTheValueInSetKey('1111set*');\n    // Verify search by part member name in the end\n    await t.expect(result).eql(keyMember, 'The set member by part member name in the end not found');\n\n    await browserPage.searchByTheValueInSetKey('*Member11111');\n    // Verify search by part member name in the beggining\n    result = await browserPage.setMembersList.nth(0).textContent;\n    await t.expect(result).eql(keyMember, 'The set member by part member name in the beggining not found');\n\n    await browserPage.searchByTheValueInSetKey('1111*11111');\n    // Verify search by part member name in the middle\n    result = await browserPage.setMembersList.nth(0).textContent;\n    await t.expect(result).eql(keyMember, 'The set member by part member name in the middle not found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/stream-key-entry-deletion.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(20);\nconst fields = [\n    'Pressure',\n    'Humidity',\n    'Temperature'\n];\nconst values = [\n    '234',\n    '78',\n    '27'\n];\n\nfixture `Stream key entry deletion`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that the Stream information is refreshed and the deleted entry is removed when user confirm the deletion of an entry', async t => {\n    keyName = Common.generateWord(20);\n    const fieldForDeletion = fields[2];\n\n    const cliCommands: string[] = [];\n    // Add new Stream key with 3 fields\n    for(let i = 0; i < fields.length; i++){\n        cliCommands.push(`XADD ${keyName} * ${fields[i]} ${values[i]}`);\n    }\n\n    await browserPage.Cli.sendCommandsInCli(cliCommands);\n\n    // Open key details and remember the Stream information\n    await browserPage.openKeyDetails(keyName);\n    await t.expect(browserPage.streamFields.nth(1).textContent).eql(fieldForDeletion, 'The first field entry name not found');\n    const entriesCountBefore = (await browserPage.keyLengthDetails.textContent).split(': ')[1];\n    // Delete entry from the Stream\n    await browserPage.deleteStreamEntry();\n    // Check results\n    const entriesCountAfter = (await browserPage.keyLengthDetails.textContent).split(': ')[1];\n    await t.expect(Number(entriesCountBefore) - 1).eql(Number(entriesCountAfter), 'The Entries length is not refreshed');\n    const fieldsLengthAfter = await browserPage.streamFields.count;\n    for(let i = fieldsLengthAfter - 1; i <= 0; i--){\n        const fieldName = await browserPage.streamFields.nth(i).textContent;\n        await t.expect(fieldName).notEql(fieldForDeletion, 'The deleted entry is not removed from the Stream');\n    }\n});\ntest('Verify that when user delete the last Entry from the Stream the Stream key is not deleted', async t => {\n    keyName = Common.generateWord(20);\n    const emptyStreamMessage = 'There are no Entries in the Stream.';\n    // Add new Stream key with 1 field\n    await browserPage.Cli.sendCommandInCli(`XADD ${keyName} * ${fields[0]} ${values[0]}`);\n    // Open key details and delete entry from the Stream\n    await browserPage.openKeyDetails(keyName);\n    await browserPage.deleteStreamEntry();\n    // Check results\n    await t.expect(browserPage.streamEntriesContainer.textContent).contains(emptyStreamMessage, 'The message after deletion of the last Entry from the Stream not found');\n    await browserPage.searchByKeyName(keyName);\n    await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The Stream key is deleted');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/stream-key.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { Selector } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst chance = new Chance();\n\nlet keyName = Common.generateWord(20);\nconst keyField = Common.generateWord(20);\nconst keyValue = Common.generateWord(20);\n\nfixture `Stream Key`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        //Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can create Stream key via Add New Key form', async t => {\n    keyName = Common.generateWord(20);\n    // Add New Stream Key\n    await browserPage.addStreamKey(keyName, keyField, keyValue);\n    // Verify that user can see Stream details opened after key creation\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Stream Key Name not visible');\n    // Verify that user can see newly added Stream key in key list clicking on keys refresh button\n    await t.click(browserPage.refreshKeysButton);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('Stream is not added');\n});\ntest('Verify that user can add several fields and values during Stream key creation', async t => {\n    const keyName = Common.generateWord(20);\n    // Create an array with different data types for Stream fields\n    const streamData = { 'string': Common.generateWord(20), 'array': `[${Common.generateWord(20)}, ${chance.integer()}]`, 'integer': `${chance.integer()}`, 'json': '{\\'test\\': \\'test\\'}', 'null': 'null', 'boolean': 'true' };\n    const scrollSelector = Selector('.eui-yScroll').nth(-1);\n\n    // Open Add New Stream Key form\n    await browserPage.commonAddNewKey(keyName);\n    await t.click(browserPage.streamOption);\n    // Verify that user can see Entity ID filled by * by default on add Stream key form\n    await t.expect(browserPage.streamEntryId.withAttribute('value', '*').exists).ok('Preselected Stream Entity ID field not correct');\n    // Verify that user can specify valid custom value for Entry ID\n    await t.typeText(browserPage.streamEntryId, '0-1', { replace: true, paste: true });\n    // Filled fields and value by different data types\n    for (let i = 0; i < Object.keys(streamData).length; i++) {\n        await t.typeText(browserPage.streamField.nth(-1), Object.keys(streamData)[i], { replace: true, paste: true });\n        await t.typeText(browserPage.streamValue.nth(-1), Object.values(streamData)[i], { replace: true, paste: true });\n        await t.scroll(scrollSelector, 'bottom');\n        await t.expect(browserPage.streamField.count).eql(i + 1, 'Number of added fields not correct');\n        if (i < Object.keys(streamData).length - 1) {\n            await t.click(browserPage.addAdditionalElement);\n        }\n    }\n    await t.expect(browserPage.addKeyButton.withAttribute('disabled').exists).notOk('Clickable Add Key button');\n    await t.click(browserPage.addKeyButton);\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Stream Key Name');\n});\ntest('Verify that user can add new Stream Entry for Stream data type key which has an Entry ID, Field and Value', async t => {\n    keyName = Common.generateWord(20);\n    const newField = Common.generateWord(20);\n\n    // Add New Stream Key and check columns and rows\n    await apiKeyRequests.addStreamKeyApi({\n        keyName,\n        entries: [{\n            id: '*',\n            fields: [{\n                name: keyField,\n                value: keyValue\n            }],\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n    await t.expect(browserPage.streamEntryIDDateValue.count).eql(1, 'One Entry ID not displayed');\n    await t.expect(browserPage.streamFields.count).eql(4, 'One field in table not displayed');\n    await t.expect(browserPage.streamEntryFields.count).eql(1, 'One value in table not displayed');\n    // Create new field and value and check that new column is added\n    await browserPage.addEntryToStream(newField, Common.generateWord(20));\n    await t.expect(browserPage.streamEntryIDDateValue.count).eql(2, 'Two Entries ID not displayed');\n    await t.expect(browserPage.streamFields.count).eql(7, 'Two fields in table not displayed');\n    await t.expect(browserPage.streamEntryFields.count).eql(4, 'Four values in table not displayed');\n    // Create value to existed filed and check that new column was not added\n    await browserPage.addEntryToStream(newField, Common.generateWord(20));\n    await t.expect(browserPage.streamEntryIDDateValue.count).eql(3, 'Three Entries ID not displayed');\n    await t.expect(browserPage.streamFields.count).eql(8, 'Still two fields in table not displayed');\n    await t.expect(browserPage.streamEntryFields.count).eql(6, 'Six values in table not displayed');\n}).skip.meta({skipComment: \"CI execution unstable, verification failure, needs investigation\"});\ntest('Verify that during new entry adding to existing Stream, user can clear the value and the row itself', async t => {\n    keyName = Common.generateWord(20);\n    // Generate data for stream\n    const fields = [keyField, Common.generateWord(20)];\n    const values = [keyValue, Common.generateWord(20)];\n\n    // Add New Stream Key\n    await apiKeyRequests.addStreamKeyApi({\n        keyName,\n        entries: [{\n            id: '*',\n            fields: [{\n                name: keyField,\n                value: keyValue\n            }],\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    await t.click(browserPage.addNewStreamEntry);\n    await browserPage.fulfillSeveralStreamFields(fields, values);\n    // Check number of rows\n    const fieldsNumberBeforeDeletion = await browserPage.streamField.count;\n    // Click on delete field for the last entity\n    await t.click(browserPage.clearStreamEntryInputs.nth(-1));\n    const fieldsNumberAfterDeletion = await browserPage.streamField.count;\n    await t.expect(fieldsNumberAfterDeletion).lt(fieldsNumberBeforeDeletion, 'Number of fields after deletion not correct');\n    // Validate that the last field and value were fulfilled\n    await t.expect(browserPage.streamField.withAttribute('value', keyField).exists).ok('Input for field not filled');\n    await t.expect(browserPage.streamValue.withAttribute('value', keyValue).exists).ok('Input for value not filled');\n    // Click on clear button\n    await t.hover(browserPage.streamValue);\n    await t.click(browserPage.clearStreamEntryInputs);\n    // Validate that data was cleared\n    await t.expect(browserPage.streamField.withAttribute('value', keyField).exists).notOk('Input for field not cleared');\n    await t.expect(browserPage.streamValue.withAttribute('value', keyValue).exists).notOk('Input for value not cleared');\n    // Validate that the form is still displayed\n    await t.expect(browserPage.streamField.count).eql(fieldsNumberAfterDeletion, 'Number of fields after deletion not correct');\n});\ntest('Verify that user can add several fields and values to the existing Stream Key', async t => {\n    keyName = Common.generateWord(20);\n    // Generate field value data\n    const entryQuantity = 5;\n    const fields: string[] = [];\n    const values: string[] = [];\n\n    for (let i = 0; i < entryQuantity; i++) {\n        const randomGeneratorValue = chance.integer({ min: 1, max: 50 });\n        fields.push(chance.word({ length: randomGeneratorValue }));\n        values.push(chance.word({ length: randomGeneratorValue }));\n    }\n\n    // Add New Stream Key\n    await apiKeyRequests.addStreamKeyApi({\n        keyName,\n        entries: [{\n            id: '*',\n            fields: [{\n                name: keyField,\n                value: keyValue\n            }],\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    await t.click(browserPage.addNewStreamEntry);\n    // Filled Stream by new several Fields\n    await browserPage.fulfillSeveralStreamFields(fields, values);\n    await t.click(browserPage.saveElementButton);\n    // Check that all data is saved in Stream\n    await t.click(browserPage.fullScreenModeButton);\n    for (let i = 0; i < fields.length; i++) {\n        await t.expect(browserPage.streamFieldsValues.find('span').withExactText(values[i]).exists).ok('Added Value not displayed');\n        await t.expect(browserPage.streamEntriesContainer.find('div').withExactText(fields[i]).exists).ok('Added Field not displayed');\n    }\n    // Check Stream length\n    const streamLength = await browserPage.getKeyLength();\n    await t.expect(streamLength).eql('2', 'Stream length after adding new entry not correct');\n    await t.click(browserPage.fullScreenModeButton);\n});\ntest('Verify that user can see the Stream range filter', async t => {\n    keyName = Common.generateWord(20);\n    // Add new Stream key with 1 field\n    await browserPage.Cli.sendCommandInCli(`XADD ${keyName} * fields values`);\n    // Open key details and check filter\n    await browserPage.openKeyDetails(keyName);\n    await t.expect(browserPage.rangeLeftTimestamp.visible).ok('The stream range start timestamp not visible');\n    await t.expect(browserPage.rangeRightTimestamp.visible).ok('The stream range end timestamp not visible');\n    await t.expect(browserPage.streamRangeBar.visible).ok('The stream range bar not visible');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/stream-pending-messages.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(20);\nlet consumerGroupName = Common.generateWord(20);\n\n// todo: enable after RI-7447 will be fixed\nfixture.skip `Acknowledge and Claim of Pending messages`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        if (await browserPage.closeKeyButton.exists){\n            await t.click(browserPage.closeKeyButton);\n        }\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can acknowledge any message in the list of pending messages', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommands);\n\n    // Open Stream pending view\n    await browserPage.openStreamPendingsView(keyName);\n    // Acknowledge message and check result\n    await t.click(browserPage.acknowledgeButton);\n    await t.expect(browserPage.confirmationMessagePopover.textContent).contains('will be acknowledged and removed from the pending messages list', 'The confirmation message');\n    await t.click(browserPage.confirmAcknowledgeButton);\n    await t.expect(browserPage.streamMessagesContainer.textContent).contains('Your Consumer has no pending messages.', 'The messages is acknowledged from the table');\n});\ntest('Verify that user can claim any message in the list of pending messages', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName} Bob COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommands);\n\n    // Open Stream pendings view\n    await browserPage.openStreamPendingsView(keyName);\n    // Claim message and check result\n    await t.click(browserPage.claimPendingMessageButton);\n    await t.expect(browserPage.pendingCount.textContent).eql('pending: 1', 'The number of pending messages for selected consumer');\n    await t.click(browserPage.submitButton);\n    await t.expect(browserPage.streamMessagesContainer.textContent).contains('Your Consumer has no pending messages.', 'The messages is claimed and removed from the table');\n    await t.click(browserPage.streamTabConsumers);\n    await t.click(browserPage.streamConsumerName.nth(1));\n    await t.expect(browserPage.streamMessage.count).eql(2, 'The claimed messages is in the selected Consumer');\n});\ntest('Verify that claim with optional parameters, the message removed from this Consumer and appeared in the selected Consumer', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName} Bob COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommands);\n\n    // Open Stream pendings view\n    await browserPage.openStreamPendingsView(keyName);\n    // Claim message with optional parameters and check result\n    await t.click(browserPage.claimPendingMessageButton);\n    await t.expect(browserPage.optionalParametersSwitcher.withAttribute('aria-checked', 'false').exists).ok('By default toggle for optional parameters is off');\n    await t.click(browserPage.optionalParametersSwitcher);\n    await t.typeText(browserPage.claimIdleTimeInput, '100', { replace: true, paste: true });\n    await t.click(browserPage.forceClaimCheckbox);\n    await t.click(browserPage.submitButton);\n    await t.expect(browserPage.streamMessagesContainer.textContent).contains('Your Consumer has no pending messages.', 'The messages is claimed and removed from the table');\n    await t.click(browserPage.streamTabConsumers);\n    await t.click(browserPage.streamConsumerName.nth(1));\n    await t.expect(browserPage.streamMessage.count).eql(2, 'The claimed messages is in the selected Consumer');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/browser/zset-key.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { Common } from '../../../../helpers/common';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = 2147476121;\nconst keyMember = '1111ZsetMember11111';\n\nfixture `ZSet Key fields verification`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can search by member in Zset', async t => {\n    keyName = Common.generateWord(10);\n    await apiKeyRequests.addSortedSetKeyApi({\n        keyName,\n        ttl: keyTTL,\n        members: [{\n            name: '12345qwerty',\n            score: 0,\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Add member to the ZSet key\n    await browserPage.addMemberToZSet(keyMember, '3');\n    // Search by member name\n    await browserPage.searchByTheValueInKeyDetails(keyMember);\n    // Check the search result\n    const result = await browserPage.zsetMembersList.nth(0).textContent;\n    await t.expect(result).eql(keyMember, 'The Zset member');\n});\ntest('Verify that user can sort Zset members by score by DESC and ASC', async t => {\n    keyName = Common.generateWord(10);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    //Create new key with a lot of members\n    const arr = await Common.createArray(100);\n    await t.typeText(browserPage.Cli.cliCommandInput, `ZADD ${keyName} ${arr.join(' ')}`, { paste: true });\n    await t.pressKey('enter');\n    await t.click(browserPage.Cli.cliCollapseButton);\n    // Open key details\n    await browserPage.openKeyDetails(keyName);\n    // Sort Zset members by score by DESC and verify result\n    await t.click(browserPage.scoreButton);\n    let result = await browserPage.zsetScoresList.textContent;\n    await t.expect(result).eql(arr[100 - 1], 'The Zset sort by desc');\n    // Sort Zset members by score by ASC and verify result\n    await t.click(browserPage.scoreButton);\n    result = await browserPage.zsetScoresList.textContent;\n    await t.expect(result).eql(arr[1], 'The Zset sort by desc');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/cli/cli-command-helper.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { BrowserPage } from '../../../../pageObjects';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst defaultHelperText = 'Enter any command in CLI or use search to see detailed information.';\nconst COMMAND_APPEND = 'APPEND';\nconst COMMAND_GROUP_SET = 'Set';\nconst COMMAND_GROUP_TIMESERIES = 'TimeSeries';\nconst COMMAND_GROUP_GRAPH = 'Graph';\n\nfixture `CLI Command helper`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify Command Helper search and filter', async t => {\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Verify default text\n    await t.expect(browserPage.CommandHelper.cliHelperText.textContent).eql(defaultHelperText, 'Default text for CLI Helper is not shown');\n    // Search any command\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, 'SET', { replace: true, paste: true });\n    await t.expect(browserPage.CommandHelper.cliHelperOutputTitles.count).gt(0, 'List of commands were not found');\n    // Clear search input\n    // todo: add proper attr to html\n    const clearButton = browserPage.CommandHelper.cliHelper.find('button[title=Reset]');\n    await t.click(clearButton);\n    // Verify that when user clears the input in the Search of CLI Helper (via x icon), he can see the default screen with proper the text\n    await t.expect(browserPage.CommandHelper.cliHelperText.textContent).eql(defaultHelperText, 'Default text for CLI Helper is not shown');\n\n    // Verify that user can unselect the command filtered to remove filters\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_SET);\n    await t.expect(browserPage.CommandHelper.cliHelperOutputTitles.count).gt(0, 'List of commands were not found');\n    // Unselect previous command from list\n    await t.click(browserPage.CommandHelper.clearAllGroupFilters);\n    await t.expect(browserPage.CommandHelper.cliHelperOutputTitles.count).eql(0, 'List of commands were not cleared');\n    await t.expect(browserPage.CommandHelper.cliHelperText.textContent).eql(defaultHelperText, 'Default text for CLI Helper is not shown');\n\n    // Verify that user can see relevant search results in Command Helper per every entered symbol\n    await t.click(browserPage.CommandHelper.cliHelperSearch);\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, 's');\n    // Verify that we found commands\n    await t.expect(browserPage.CommandHelper.cliHelperOutputTitles.count).gt(0, 'List of commands were not found');\n    const countCommandsOfOneLetterSearch = await browserPage.CommandHelper.cliHelperOutputTitles.count;\n    // Continue typing\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, 'a');\n    const countCommandsOfTwoLettersSearch = await browserPage.CommandHelper.cliHelperOutputTitles.count;\n    // Verify that first list has more count than the second\n    await t.expect(countCommandsOfOneLetterSearch).gt(countCommandsOfTwoLettersSearch, 'Count of commands with 1 letter not more than 2');\n\n    // Verify that when user has used search and apply filters, search results include only commands from the filter group applied\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_SET);\n    await t.expect(browserPage.CommandHelper.cliHelperOutputTitles.withText('SAVE').exists).notOk('Commands found from another group');\n    await t.expect(browserPage.CommandHelper.cliHelperOutputTitles.withText('SADD').exists).ok('Proper command was not found');\n\n    // Verify that Command helper cleared when user runs the command in CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Enter command into CLI\n    await t.typeText(browserPage.Cli.cliCommandInput, COMMAND_APPEND, { speed: 0.5, replace: true, paste: true });\n    await t.expect(browserPage.CommandHelper.filterGroupTypeButton.textContent).notContains(COMMAND_GROUP_SET, 'Filter was not cleared');\n    await t.expect(browserPage.CommandHelper.cliHelperSearch.value).eql('', 'Search was not cleared');\n\n    // Verify that when user enters command in CLI, Helper displays additional info about the command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql('APPEND key value', 'Command name and syntax not displayed');\n    await t.expect(browserPage.CommandHelper.cliHelperTitle.innerText).contains('STRING', 'Command Group badge not displayed');\n    await t.expect(browserPage.CommandHelper.cliHelperSummary.innerText).contains('Appends a string to the value of a key. Creates the key if it doesn\\'t exist.', 'Command summary not displayed');\n});\ntest('Verify that user can type TS. in Command helper and see commands from RedisTimeSeries commands.json', async t => {\n    const commandForSearch = 'TS.';\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select group from list and remember commands\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_TIMESERIES);\n    const commandsFilterCount = await browserPage.CommandHelper.cliHelperOutputTitles.count;\n    const timeSeriesCommands: string[] = [];\n    for(let i = 0; i < commandsFilterCount; i++) {\n        timeSeriesCommands.push(await browserPage.CommandHelper.cliHelperOutputTitles.nth(i).textContent);\n    }\n    // Unselect group from list\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_TIMESERIES);\n    // Search per part of command and check all opened commands\n    await browserPage.CommandHelper.checkSearchedCommandInCommandHelper(commandForSearch, timeSeriesCommands);\n    // Check the first command documentation url\n    await browserPage.CommandHelper.checkURLCommand(timeSeriesCommands[0], `https://redis.io/docs/latest/commands/${timeSeriesCommands[0].toLowerCase()}?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper`);\n});\n// outdated after https://redislabs.atlassian.net/browse/RI-4608\ntest.skip('Verify that user can type GRAPH. in Command helper and see auto-suggestions from RedisGraph commands.json', async t => {\n    const commandForSearch = 'GRAPH.';\n    // const externalPageLink = 'https://redis.io/commands/graph.config-get/';\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select group from list and remember commands\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_GRAPH);\n    const commandsFilterCount = await browserPage.CommandHelper.cliHelperOutputTitles.count;\n    const graphCommands: string[] = [];\n    for(let i = 0; i < commandsFilterCount; i++) {\n        graphCommands.push(await browserPage.CommandHelper.cliHelperOutputTitles.nth(i).textContent);\n    }\n    // Unselect group from list\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_GRAPH);\n    // Search per part of command and check all opened commands\n    await browserPage.CommandHelper.checkSearchedCommandInCommandHelper(commandForSearch, graphCommands);\n    // update after resolving testcafe Native Automation mode limitations\n    // // Check the first command documentation url\n    // await browserPage.CommandHelper.checkURLCommand(graphCommands[0], externalPageLink);\n    // await t.switchToParentWindow();\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/cli/cli-critical.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { Common } from '../../../../helpers/common';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    ossClusterConfig,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst chance = new Chance();\n\nconst pairsToSet = Common.createArrayPairsWithKeyValue(4);\nconst MAX_AUTOCOMPLETE_EXECUTIONS = 100;\nlet keyName = Common.generateWord(10);\nlet value = chance.natural({ length: 5 });\n\nfixture `CLI critical`\n    .meta({ type: 'critical_path' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .meta({ rte: rte.standalone })('Verify that Redis returns error if command is not correct when user works with CLI', async t => {\n        //Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n\n        await t.typeText(browserPage.Cli.cliCommandInput, 'SET key', { replace: true, paste: true });\n        await t.pressKey('enter');\n        // Check error\n        const errWrongArgs = browserPage.Cli.cliOutputResponseFail.withText('ERR wrong number of arguments for \\'set\\' command');\n        await t.expect(errWrongArgs.exists).ok('Error with wrong number of arguments was not shown');\n\n        await t.typeText(browserPage.Cli.cliCommandInput, 'lorem', { replace: true, paste: true });\n        await t.pressKey('enter');\n        // Check error\n        const errWrongCmnd = browserPage.Cli.cliOutputResponseFail.withText('ERR unknown command');\n        await t.expect(errWrongCmnd.exists).ok('Error unknown command was not shown');\n    });\ntest\n    .meta({ rte: rte.standalone })('Verify that user can scroll commands using \"Tab\" in CLI & execute it', async t => {\n        const commandToAutoComplete = 'INFO';\n        const commandStartsWith = 'I';\n        // Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.typeText(browserPage.Cli.cliCommandInput, commandStartsWith, { replace: true, paste: true });\n        // Press tab while we won't find 'INFO' command\n        // Avoid endless cycle\n        let operationsCount = 0;\n        while (await browserPage.Cli.cliCommandInput.textContent !== commandToAutoComplete && operationsCount < MAX_AUTOCOMPLETE_EXECUTIONS) {\n            await t.pressKey('tab');\n            ++operationsCount;\n        }\n        await t.pressKey('enter');\n        // Check that command was executed and user got success result\n        await t.expect(browserPage.Cli.cliOutputResponseSuccess.exists).ok('Command from autocomplete was not found & executed');\n    });\ntest\n    .skip('Verify that when user enters in CLI RediSearch/JSON commands (FT.CREATE, FT.DROPINDEX/JSON.GET, JSON.DEL), he can see hints with arguments', async t => {\n        const commandHints = [\n            'index [data_type] [prefix] [filter] [default_lang] [lang_attribute] [default_score] [score_attribute] [payload_attribute] [maxtextfields] [seconds] [nooffsets] [nohl] [nofields] [nofreqs] [stopwords] [skipinitialscan] schema field [field ...]',\n            'index [delete docs]',\n            'key [indent] [newline] [space] [path [path ...]]',\n            'key [path]'\n        ];\n        const commands = [\n            'FT.CREATE',\n            'FT.DROPINDEX',\n            'JSON.GET',\n            'JSON.DEL'\n        ];\n        const commandHint = 'key [META] [BLOB]';\n        const command = 'ai.modelget';\n\n        // Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n        // Verify that user can type AI command in CLI and see agruments in hints from RedisAI commands.json\n        await t.expect(browserPage.Cli.cliCommandAutocomplete.textContent).eql(commandHint, `The hints with arguments for command ${command} not shown`);\n\n        // Enter commands and check hints with arguments\n        for(const command of commands) {\n            await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n            await t.expect(browserPage.Cli.cliCommandAutocomplete.textContent).eql(commandHints[commands.indexOf(command)], `The hints with arguments for command ${command} not shown`);\n        }\n    })\n    .meta({ rte: rte.standalone, skipComments: \"CI execution unstable, selector failure, needs investigation\" });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/cluster-details/cluster-details.e2e.ts",
    "content": "import { Selector } from 'testcafe';\nimport { BrowserPage, MyRedisDatabasePage, ClusterDetailsPage, WorkbenchPage } from '../../../../pageObjects';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossClusterConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst clusterDetailsPage = new ClusterDetailsPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst headerColumns = {\n    'Type': 'OSS Cluster',\n    'Version': '7.0.0',\n    'User': 'default'\n};\ntype HeaderColumn = keyof typeof headerColumns;\n\nconst keyName = Common.generateWord(10);\nconst commandToAddKey = `set ${keyName} test`;\n\nfixture `Overview`\n    .meta({ type: 'critical_path', rte: rte.ossCluster })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    });\ntest('Overview tab header for OSS Cluster', async t => {\n    const uptime = /[1-9][0-9]\\s|[0-9]\\smin|[1-9][0-9]\\smin|[0-9]\\sh/;\n\n    // Verify that user see \"Overview\" tab by default for OSS Cluster\n    await t.expect(clusterDetailsPage.overviewTab.withAttribute('aria-selected', 'true').exists).ok('The Overview tab not opened');\n    // Verify that user see \"Overview\" header with OSS Cluster info\n    for (const key in headerColumns) {\n        const columnSelector = Selector(`[data-testid=cluster-details-item-${key}]`);\n        await t.expect(columnSelector.textContent).contains(`${headerColumns[key as HeaderColumn]}`, `Cluster detail ${key} is incorrect`);\n    }\n    // Verify that Uptime is displayed as time in seconds or minutes from start\n    await t.expect(clusterDetailsPage.clusterDetailsUptime.textContent).match(uptime, 'Uptime value is not correct');\n});\n// todo: enable after RI-7449 fix\ntest.skip\n    .after(async() => {\n    //Clear database and delete\n        await browserPage.Cli.sendCommandInCli(`DEL ${keyName}`);\n        await browserPage.Cli.sendCommandInCli('FT.DROPINDEX idx:schools DD');\n        await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig);\n    })('Primary node statistics table displaying', async t => {\n    // Remember initial table values\n        const initialValues: number[] = [];\n        const nodes = (await databaseAPIRequests.getClusterNodesApi(ossClusterConfig)).sort();\n        const columns = ['Commands/s', 'Clients', 'Total Keys', 'Network Input', 'Network Output', 'Total Memory'];\n\n        for (const column in columns) {\n            initialValues.push(await clusterDetailsPage.getTotalValueByColumnName(column));\n        }\n        const nodesNumberInHeader = parseInt((await clusterDetailsPage.tableHeaderCell.nth(0).textContent).match(/\\d+/)![0]);\n\n        // Add key from CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.typeText(browserPage.Cli.cliCommandInput, commandToAddKey);\n        await t.pressKey('enter');\n        await t.click(browserPage.Cli.cliCollapseButton);\n        // Verify nodes in header column equal to rows\n        await t.expect(await clusterDetailsPage.getPrimaryNodesCount()).eql(nodesNumberInHeader, 'Primary nodes in table are not displayed');\n\n        // todo: change verification of nodes since order is not guaranteed\n        // Verify that all nodes from BE response are displayed in table\n        for (const node of nodes) {\n            await t.expect(clusterDetailsPage.tableRow.nth(nodes.indexOf(node)).textContent).contains(node, `Node ${node} is not displayed in table`);\n        }\n        //Run Create hash index command to load network and memory\n        await clusterDetailsPage.NavigationHeader.togglePanel(true);\n        const tutorials = await clusterDetailsPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n\n        await t.click(tutorials.dataStructureAccordionTutorialButton);\n        await t.click(tutorials.internalLinkWorkingWithHashes);\n        await tutorials.runBlockCode('Create a hash');\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        // Verify that values in table are dynamic\n        for (const column in columns) {\n            await t.expect(await clusterDetailsPage.getTotalValueByColumnName(column)).notEql(initialValues[columns.indexOf(column)], `${column} not dynamic`);\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/clone-databases.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossClusterConfig, ossSentinelConfig, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst newOssDatabaseAlias = 'cloned oss cluster';\n\nfixture `Clone databases`\n    .meta({ type: 'critical_path' })\n    .page(commonUrl);\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .after(async() => {\n        // Delete databases\n        const dbNumber = await myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count;\n        for (let i = 0; i < dbNumber; i++) {\n            await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n        }\n    })\n    .meta({ rte: rte.standalone })\n    .skip('Verify that user can clone Standalone db', async t => {\n        await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n\n        // Verify that user can test Standalone connection on edit and see the success message\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n        await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Standalone connection is not successful');\n\n        // Verify that user can cancel the Clone by clicking the “Cancel” or the “x” button\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n        await t.expect(myRedisDatabasePage.popoverHeader.withText('Clone ').exists).notOk('Clone panel is still displayed', { timeout: 2000 });\n        await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n        // Verify that user see the “Add Database Manually” form pre-populated with all the connection data when cloning DB\n        await t\n            // Verify that name in the header has the prefix “Clone”\n            .expect(myRedisDatabasePage.popoverHeader.withText('Clone ').exists).ok('Clone panel is not displayed')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.hostInput.getAttribute('value')).eql(ossStandaloneConfig.host, 'Wrong host value')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.portInput.getAttribute('value')).eql(ossStandaloneConfig.port, 'Wrong port value')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput.getAttribute('value')).eql(ossStandaloneConfig.databaseName, 'Wrong host value')\n            // Verify that timeout input is displayed for clone db window\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.timeoutInput.value).eql('30', 'Timeout is not defaulted to 30 on clone window');\n        // Verify that user can confirm the creation of the database by clicking “Clone Database”\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count).eql(2, 'DB was not cloned');\n\n        // Verify new connection badge for cloned database\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossStandaloneConfig.databaseName);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .after(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig);\n        await myRedisDatabasePage.deleteDatabaseByName(newOssDatabaseAlias);\n    })\n    .meta({ rte: rte.ossCluster })\n    .skip('Verify that user can clone OSS Cluster', async t => {\n        await databaseHelper.clickOnEditDatabaseByName(ossClusterConfig.ossClusterDatabaseName);\n\n        // Verify that user can test OSS Cluster connection on edit and see the success message\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n        await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'OSS Cluster connection is not successful');\n\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n        await t\n            .expect(myRedisDatabasePage.popoverHeader.withText('Clone ').exists).ok('Clone panel is not displayed')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.portInput.getAttribute('value')).eql(ossClusterConfig.ossClusterPort, 'Wrong port value')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput.getAttribute('value')).eql(ossClusterConfig.ossClusterDatabaseName, 'Wrong host value');\n        // Edit Database alias before cloning\n        await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput, newOssDatabaseAlias, { replace: true });\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await t.expect(myRedisDatabasePage.dbNameList.withExactText(newOssDatabaseAlias).exists).ok('DB was not closed');\n        await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossClusterConfig.ossClusterDatabaseName).exists).ok('Original DB is not displayed');\n\n        // New connections indicator\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossClusterConfig.ossClusterDatabaseName);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        // Add Sentinel databases\n        await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .after(async() => {\n        // Delete all primary groups\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL');\n        await myRedisDatabasePage.reloadPage();\n    })\n    .meta({ rte: rte.sentinel })\n    .skip('Verify that user can clone Sentinel', async t => {\n        const hiddenPassword = '••••••••••••';\n\n        await databaseHelper.clickOnEditDatabaseByName(ossSentinelConfig.masters[1].alias);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n\n        // Verify that user can test Sentinel connection on edit and see the success message\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n        await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Sentinel connection is not successful');\n\n        // Verify that for Sentinel Host and Port fields are replaced with editable Primary Group Name field\n        await t\n            .expect(myRedisDatabasePage.popoverHeader.withText('Clone ').exists).ok('Clone panel is not displayed')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput.getAttribute('value')).eql(ossSentinelConfig.masters[1].alias, 'Invalid primary group alias value')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.primaryGroupNameInput.getAttribute('value')).eql(ossSentinelConfig.masters[1].name, 'Invalid primary group name value');\n        // Validate Databases section\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.masterGroupPassword.getAttribute('value')).eql(hiddenPassword, 'Invalid sentinel database password');\n        // Validate Sentinel section\n        await t\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.portInput.getAttribute('value')).eql(ossSentinelConfig.sentinelPort, 'Invalid sentinel port')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.passwordInput.getAttribute('value')).eql(hiddenPassword, 'Invalid sentinel password');\n        // Clone Sentinel Primary Group\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossSentinelConfig.masters[1].alias).count).gt(1, 'Primary Group was not cloned');\n\n        // Verify new connection badge for Sentinel db\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossSentinelConfig.masters[1].alias);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/connecting-to-the-db.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    invalidOssStandaloneConfig,\n    ossClusterForSSHConfig,\n    ossStandaloneForSSHConfig,\n    ossStandaloneRedisGears,\n} from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { sshPrivateKey, sshPrivateKeyWithPasscode } from '../../../../test-data/sshPrivateKeys';\nimport { Common } from '../../../../helpers/common';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\nimport { goBackHistory } from '../../../../helpers/utils';\nimport { AddRedisDatabaseDialog } from '../../../../pageObjects/dialogs';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserActions = new BrowserActions();\nconst addDbDialog = new AddRedisDatabaseDialog();\n\nconst { host, port, databaseName, databaseUsername = '', databasePassword = '' } = ossStandaloneRedisGears;\nconst username = 'alice&&';\nconst password = 'p1pp0@&';\n\nconst sshParams = {\n    sshHost: '172.31.100.245',\n    sshPort: '2222',\n    sshUsername: 'u'\n};\nconst newClonedDatabaseAlias = 'Cloned ssh database';\nconst sshDbPass = {\n    ...ossStandaloneForSSHConfig,\n    databaseName: `SSH_${Common.generateWord(5)}`\n};\nconst sshDbPrivateKey = {\n    ...ossStandaloneForSSHConfig,\n    databaseName: `SSH_${Common.generateWord(5)}`\n};\nconst sshDbPasscode = {\n    ...ossStandaloneForSSHConfig,\n    databaseName: `SSH_${Common.generateWord(5)}`\n};\nconst sshDbClusterPass = {\n    ...ossClusterForSSHConfig,\n    databaseName: `SSH_Cluster_${Common.generateWord(5)}`\n};\n\nfixture `Connecting to the databases verifications`\n    .meta({ type: 'critical_path' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest\n    .meta({ rte: rte.none })('Verify that user can see error message if he can not connect to added Database', async t => {\n        const errorMessage = `Could not connect to ${invalidOssStandaloneConfig.host}:${invalidOssStandaloneConfig.port}, please check the connection details.`;\n\n        // Fill the add database form\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDataBase(invalidOssStandaloneConfig);\n\n        // Verify that when user request to test database connection is not successfull, can see standart connection error\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n        await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Error', 'Invalid connection has no error on test');\n\n        // Click for saving\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        // Verify that the database is not in the list\n        await t.expect(myRedisDatabasePage.Toast.toastError.textContent).contains('Error', 'Error message not displayed', { timeout: 10000 });\n        await t.expect(myRedisDatabasePage.Toast.toastError.textContent).contains(errorMessage, 'Error message not displayed', { timeout: 10000 });\n    });\ntest\n    .meta({ rte: rte.none })('Fields to add database prepopulation', async t => {\n        const defaultHost = '127.0.0.1';\n        const defaultPort = '6379';\n        const defaultSentinelPort = '26379';\n\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton)\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton);\n\n        // Verify that the Host, Port, Database Alias values pre-populated by default for the manual flow\n        await t\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.hostInput.value).eql(defaultHost, 'Default host not prepopulated')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.portInput.value).eql(defaultPort, 'Default port not prepopulated')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput.value).eql(`${defaultHost}:${defaultPort}`, 'Default db alias not prepopulated');\n        // Verify that the Host, Port, Database Alias values pre-populated by default for Sentinel\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.backButton)\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.redisSentinelButton);\n        await t\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.hostInput.value).eql(defaultHost, 'Default sentinel host not prepopulated')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.portInput.value).eql(defaultSentinelPort, 'Default sentinel port not prepopulated');\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .after(async() => {\n        // Delete databases\n        await databaseAPIRequests.deleteStandaloneDatabasesByNamesApi([sshDbPass.databaseName, sshDbPrivateKey.databaseName, sshDbPasscode.databaseName, newClonedDatabaseAlias]);\n    })\n    .skip('Adding database with SSH', async t => {\n        const hiddenPass = '••••••••••••';\n        const tooltipText = [\n            'Enter a value for required fields (3):',\n            'SSH Host',\n            'SSH Username',\n            'SSH Private Key'\n        ];\n        const sshWithPass = {\n            ...sshParams,\n            sshPassword: 'pass'\n        };\n        const sshWithPrivateKey = {\n            ...sshParams,\n            sshPrivateKey: sshPrivateKey\n        };\n        const sshWithPassphrase = {\n            ...sshParams,\n            sshPrivateKey: sshPrivateKeyWithPasscode,\n            sshPassphrase: 'test'\n        };\n        // Verify that if user have not entered any required value he can see that this field should be specified when hover over the button to add a database\n\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton)\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton)\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.useSSHCheckbox)\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyRadioBtn)\n            .hover(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        for (const text of tooltipText) {\n            await browserActions.verifyTooltipContainsText(text, true);\n        }\n        // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes\n        await t.hover(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n        for (const text of tooltipText) {\n            await browserActions.verifyTooltipContainsText(text, true);\n        }\n\n        // Verify that user can add SSH tunnel with Password for Standalone database\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbPass, sshWithPass);\n        await myRedisDatabasePage.clickOnDBByName(sshDbPass.databaseName);\n        await Common.checkURLContainsText('browser');\n\n        // Verify that user can add SSH tunnel with Private Key\n        await t.click(browserPage.OverviewPanel.myRedisDBLink);\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbPrivateKey, sshWithPrivateKey);\n        await myRedisDatabasePage.clickOnDBByName(sshDbPrivateKey.databaseName);\n        await Common.checkURLContainsText('browser');\n\n        // Verify that user can edit SSH parameters for existing database connections\n        await t.click(browserPage.OverviewPanel.myRedisDBLink);\n        await myRedisDatabasePage.clickOnEditDBByName(sshDbPrivateKey.databaseName);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n        await t\n            .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyInput, sshWithPassphrase.sshPrivateKey, { replace: true, paste: true })\n            .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.sshPassphraseInput, sshWithPassphrase.sshPassphrase, { replace: true, paste: true });\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton.exists).notOk('Edit database panel still displayed');\n        await databaseHelper.clickOnEditDatabaseByName(sshDbPrivateKey.databaseName);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n        // Verify that password, passphrase and private key are hidden for SSH option\n        await t\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyInput.textContent).eql(hiddenPass, 'Edited Private key not saved')\n            .expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPassphraseInput.value).eql(hiddenPass, 'Edited Passphrase not saved');\n\n        // Verify that user can clone database with SSH tunnel\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n        await databaseHelper.clickOnEditDatabaseByName(sshDbPrivateKey.databaseName);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cloneDatabaseButton);\n        // Edit Database alias before cloning\n        await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput, newClonedDatabaseAlias, { replace: true });\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await t.expect(myRedisDatabasePage.dbNameList.withExactText(newClonedDatabaseAlias).exists).ok('DB with SSH was not cloned');\n\n        // Verify that user can add SSH tunnel with Passcode\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbPasscode, sshWithPassphrase);\n        await myRedisDatabasePage.clickOnDBByName(sshDbPasscode.databaseName);\n        await Common.checkURLContainsText('browser');\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .after(async() => {\n        // Delete databases\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(sshDbClusterPass);\n    })\n    .skip('Adding OSS Cluster database with SSH', async t => {\n        const sshWithPass = {\n            ...sshParams,\n            sshPassword: 'pass'\n        };\n        // Verify that user can add SSH tunnel with Password for OSS Cluster database\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addStandaloneSSHDatabase(sshDbClusterPass, sshWithPass);\n        // TODO should be deleted after https://redislabs.atlassian.net/browse/RI-5995\n        await t.wait(6000)\n        await myRedisDatabasePage.clickOnDBByName(sshDbClusterPass.databaseName);\n        if(! await browserPage.plusAddKeyButton.exists){\n            await myRedisDatabasePage.clickOnDBByName(sshDbClusterPass.databaseName);\n        }\n        await Common.checkURLContainsText('browser');\n    });\n\ntest\n    .meta({ rte: rte.none })\n    .before(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .skip('Verify that create free cloud db is displayed always', async t => {\n        const externalPageLinkList = 'https://redis.io/try-free?utm_source=redisinsight&utm_medium=app&utm_campaign=list_of_databases';\n        const externalPageLinkNavigation = 'https://redis.io/try-free?utm_source=redisinsight&utm_medium=app&utm_campaign=navigation_menu';\n\n        await t.expect(myRedisDatabasePage.dbNameList.exists).notOk('some db is added');\n        await t.expect(myRedisDatabasePage.tableRowContent.textContent).contains('Free Redis Cloud DB', `create free db row is not displayed`);\n        await t.expect(myRedisDatabasePage.starFreeDbCheckbox.exists).ok('star checkbox is not displayed next to free db link');\n        await t.expect(myRedisDatabasePage.portCloudDb.textContent).contains('Set up in a few clicks', `create free db row is not displayed`);\n\n        // skipped until https://redislabs.atlassian.net/browse/RI-6556\n        // await t.click(myRedisDatabasePage.tableRowContent);\n        // await Common.checkURL(externalPageLinkList);\n        // await goBackHistory();\n\n        await t.click(myRedisDatabasePage.NavigationPanel.cloudButton);\n        await Common.checkURL(externalPageLinkNavigation);\n        await goBackHistory();\n    });\ntest\n    .meta({ rte: rte.none })\n    .before(async t  => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisGears);\n        await browserPage.Cli.sendCommandInCli(`acl DELUSER ${username}`);\n        await browserPage.Cli.sendCommandInCli(`ACL SETUSER ${username} on >${password} +@all ~*`);\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n    })\n    .after(async t => {\n        // Delete all existing connections\n        await t.click(addDbDialog.cancelButton);\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(databaseName);\n        await browserPage.Cli.sendCommandInCli(`acl DELUSER ${username}`);\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    ('Verify that inserted URL is parsed', async t => {\n        const codedUrl = `redis://${username}:${password}@${host}:${port}`;\n        await t\n            .click(addDbDialog.addDatabaseButton);\n\n        // Verify that 'redis://default@127.0.0.1:6379' default value prepopulated for connection URL field and the same for placeholder\n        await t.expect(addDbDialog.connectionUrlInput.textContent).eql(`redis://default@127.0.0.1:6379`, 'Connection URL not prepopulated');\n\n        await t.typeText(addDbDialog.connectionUrlInput, codedUrl, { replace: true, paste: true });\n        await t.click(addDbDialog.customSettingsButton);\n        await t.expect(addDbDialog.databaseAliasInput.getAttribute('value')).eql(`${host}:${port}`, 'name is incorrected');\n        await t.expect(addDbDialog.hostInput.getAttribute('value')).eql(`${host}`, 'host is incorrected');\n        await t.expect(addDbDialog.portInput.getAttribute('value')).eql(`${port}`, 'port is incorrected');\n        await t.expect(addDbDialog.usernameInput.getAttribute('value')).eql(`${username}`, 'username is incorrected');\n        await t.expect(addDbDialog.passwordInput.getAttribute('value')).eql(`${password}`, 'username is incorrected');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/encryption.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossSentinelConfig\n} from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { DatabaseScripts, DbTableParameters } from '../../../../helpers/database-scripts';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst dbTableParams: DbTableParameters = {\n    tableName: 'database_instance',\n    columnName: 'password',\n    rowValue: 'invalid',\n    conditionWhereColumnName: 'name',\n    conditionWhereColumnValue: ossSentinelConfig.masters[1].alias\n};\n\nfixture `Encryption`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .afterEach(async() => {\n        await databaseHelper.deleteDatabase(ossSentinelConfig.masters[1].alias);\n    });\ntest('Verify that data encrypted using KEY', async t => {\n    const decryptionError = 'Unable to decrypt data';\n    // Connect to DB\n    await myRedisDatabasePage.clickOnDBByName(ossSentinelConfig.masters[1].alias);\n    // Return back to db list page\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n\n    await DatabaseScripts.updateColumnValueInDBTable(dbTableParams);\n    // Verify that Encription by KEY applied for connection if RI_ENCRYPTION_KEY variable exists\n    await t\n        .expect(await DatabaseScripts.getColumnValueFromTableInDB({ ...dbTableParams, columnName: 'encryption' }))\n        .eql('KEY', 'Encryption is not applied by RI_ENCRYPTION_KEY');\n    await databaseHelper.clickOnEditDatabaseByName(ossSentinelConfig.masters[1].alias);\n    await t.expect(myRedisDatabasePage.Toast.toastError.textContent).contains(decryptionError, 'Invalid encrypted field is decrypted');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n})\n    .skip\n    .meta({skipComment: \"Unstable in CI, Missing selector\"});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/export-databases.e2e.ts",
    "content": "import * as fs from 'fs';\nimport { join as joinPath } from 'path';\nimport { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    cloudDatabaseConfig,\n    commonUrl,\n    fileDownloadPath,\n    ossClusterConfig,\n    ossSentinelConfig,\n    ossStandaloneConfig,\n    ossStandaloneTlsConfig\n} from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { DatabasesActions } from '../../../../common-actions/databases-actions';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databasesActions = new DatabasesActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet foundExportedFiles: string[];\n\nfixture `Export databases`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .page(commonUrl);\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneTlsConfig);\n        await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig);\n        await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .after(async() => {\n        // Delete exported file\n        fs.unlinkSync(joinPath(fileDownloadPath, foundExportedFiles[0]));\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    .skip('Exporting Standalone, OSS Cluster, and Sentinel connection types', async t => {\n        const databaseNames = [\n            ossStandaloneConfig.databaseName,\n            ossStandaloneTlsConfig.databaseName,\n            ossClusterConfig.ossClusterDatabaseName,\n            ossSentinelConfig.masters[1].alias\n        ];\n\n        const compressor = 'Brotli';\n\n        await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.decompressionTab);\n        await myRedisDatabasePage.AddRedisDatabaseDialog.setCompressorValue(compressor);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n\n        // Select databases checkboxes\n        await databasesActions.selectDatabasesByNames(databaseNames);\n        // Export connections with passwords\n        await t\n            .click(myRedisDatabasePage.exportBtn)\n            .click(myRedisDatabasePage.exportSelectedDbsBtn)\n            .wait(2000);\n\n        // Verify that user can see “RedisInsight_connections_{timestamp}” as the default file name\n        foundExportedFiles = await databasesActions.findFilesByFileStarts(fileDownloadPath, 'RedisInsight_connections_');\n        // Verify that user can export database with passwords and client certificates with “Export database passwords and client certificates” control selected\n        await t.expect(foundExportedFiles.length).gt(0, 'The Exported file not saved');\n\n        // Delete databases\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await myRedisDatabasePage.reloadPage();\n\n        const exportedData = {\n            path: joinPath(fileDownloadPath, foundExportedFiles[0]),\n            successNumber: databaseNames.length,\n            dbImportedNames: databaseNames\n        };\n\n        await databasesActions.importDatabase(exportedData);\n        await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n            .contains(`${exportedData.successNumber}`, 'Not correct successfully imported number');\n        await t.click(myRedisDatabasePage.closeImportBtn);\n        // Verify that user can import exported file with all datatypes and certificates\n        await databasesActions.verifyDatabasesDisplayed(exportedData.dbImportedNames);\n\n        const modulesDbRedisStackIcon = myRedisDatabasePage.dbNameList.child('span').withExactText(ossStandaloneConfig.databaseName).parent('tr')\n            .find(myRedisDatabasePage.cssRedisStackIcon);\n        // Verify that db has redis stack icon\n        await t.expect(modulesDbRedisStackIcon.exists).ok('module icon is displayed');\n\n        await databaseHelper.clickOnEditDatabaseByName(databaseNames[1]);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).contains('ca', 'CA certificate import incorrect');\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).contains('client', 'Client certificate import incorrect');\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n        await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.decompressionTab);\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.selectCompressor.textContent).eql(compressor, 'Compressor import incorrect');\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneTlsConfig);\n        await databaseHelper.addRedisCloudDatabase(cloudDatabaseConfig);\n        await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig);\n        await myRedisDatabasePage.reloadPage();\n    })\n    .after(async() => {\n        // Delete databases\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneTlsConfig);\n        await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName);\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL');\n        // Delete exported file\n        fs.unlinkSync(joinPath(fileDownloadPath, foundExportedFiles[0]));\n    })('Export databases without passwords', async t => {\n        const databaseNames = [ossStandaloneTlsConfig.databaseName, cloudDatabaseConfig.databaseName, ossSentinelConfig.masters[1].alias];\n\n        // Select databases checkboxes\n        await databasesActions.selectDatabasesByNames(databaseNames);\n        // Export connections without passwords\n        await t\n            .click(myRedisDatabasePage.exportBtn)\n            .click(myRedisDatabasePage.exportPasswordsCheckbox)\n            .click(myRedisDatabasePage.exportSelectedDbsBtn)\n            .wait(2000);\n\n        foundExportedFiles = await databasesActions.findFilesByFileStarts(fileDownloadPath, 'RedisInsight_connections_');\n        const parsedExportedJson = databasesActions.parseDbJsonByPath(joinPath(fileDownloadPath, foundExportedFiles[0]));\n        // Verify that user can export databases without database passwords and client key when “Export passwords” control not selected\n        for (const db of parsedExportedJson) {\n            await t.expect(db.hasOwnProperty('password')).eql(false, 'Databases exported with passwords');\n            // Verify for standalone with TLS\n            if (db.tls === true) {\n                await t.expect(db.clientCert.hasOwnProperty('key')).eql(false, 'Databases exported with client key');\n            }\n            // Verify for sentinel\n            if ('sentinelMaster' in db) {\n                await t.expect(db.sentinelMaster.hasOwnProperty('password')).eql(false, 'Sentinel primary group exported with passwords');\n            }\n        }\n    }).skip.meta({skipComment: \"Unstable CI execution,  Error in test.before hook \"});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/import-databases.e2e.ts",
    "content": "import * as path from 'path';\nimport { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { DatabasesActions } from '../../../../common-actions/databases-actions';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databasesActions = new DatabasesActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst fileNames = {\n    racompassValidJson: 'racompass-valid.json',\n    racompassInvalidJson: 'racompass-invalid.json',\n    rdmFullJson: 'rdm-full.json',\n    rdmCertsJson: 'rdm-certificates.json',\n    ardmValidAno: 'ardm-valid.ano',\n    racompFullSSHJson: 'racompFullSSH.json'\n};\nconst filePathes = {\n    ardmValidPath: path.join('..', '..', '..', '..', 'test-data', 'import-databases', fileNames.ardmValidAno),\n    racompassInvalidJsonPath: path.join('..', '..', '..', '..', 'test-data', 'import-databases', fileNames.racompassInvalidJson),\n    rdmPath: path.join('..', '..', '..', '..', 'test-data', 'import-databases', fileNames.rdmFullJson),\n    rdmCertsPath: path.join('..', '..', '..', '..', 'test-data', 'import-databases', fileNames.rdmCertsJson),\n    racompassValidJson: path.join('..', '..', '..', '..', 'test-data', 'import-databases', fileNames.racompassValidJson),\n    racompassSshPath: path.join('..', '..', '..', '..', 'test-data', 'import-databases', fileNames.racompFullSSHJson)\n};\n\nconst rdmListOfDB = databasesActions.parseDbJsonByPath(path.join('test-data', 'import-databases', fileNames.rdmFullJson));\nconst rdmListOfCertsDB = databasesActions.parseDbJsonByPath(path.join('test-data', 'import-databases', fileNames.rdmCertsJson));\nconst racompListOfSSHDB = databasesActions.parseDbJsonByPath(path.join('test-data', 'import-databases', fileNames.racompFullSSHJson));\nconst rdmResults = {\n    successNames: myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmListOfDB, 'success'),\n    partialNames: myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmListOfDB, 'partial'),\n    failedNames: myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmListOfDB, 'failed')\n};\nconst racompassSshResults = {\n    successNames: myRedisDatabasePage.getDatabaseNamesFromListByResult(racompListOfSSHDB, 'success'),\n    partialNames: myRedisDatabasePage.getDatabaseNamesFromListByResult(racompListOfSSHDB, 'partial'),\n    failedNames: myRedisDatabasePage.getDatabaseNamesFromListByResult(racompListOfSSHDB, 'failed')\n\n};\n\nconst rdmData = {\n    type: 'rdm',\n    path: filePathes.rdmPath,\n    sshPath: filePathes.rdmCertsPath,\n    connectionType: 'Cluster',\n    successNumber: rdmResults.successNames.length,\n    partialNumber: rdmResults.partialNames.length,\n    failedNumber: rdmResults.failedNames.length,\n    dbImportedNames: [...rdmResults.successNames, ...rdmResults.partialNames]\n};\nconst racompSSHData = {\n    type: 'racompass',\n    path: filePathes.racompassSshPath,\n    successNumber: racompassSshResults.successNames.length,\n    partialNumber: racompassSshResults.partialNames .length,\n    failedNumber: racompassSshResults.failedNames.length,\n    importedSSHdbNames: [ ...racompassSshResults.successNames, ...racompassSshResults.partialNames ]\n};\nconst dbData = [\n    {\n        type: 'racompass',\n        path: filePathes.racompassValidJson,\n        dbNames: ['racompassCluster', 'racompassDbWithIndex:8100 [db1]']\n    },\n    {\n        type: 'ardm',\n        path: filePathes.ardmValidPath,\n        dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername', 'ardmSentinel']\n    }\n];\nconst findImportedRdmDbNameInList = async(dbName: string): Promise<string> => rdmData.dbImportedNames.find(item => item === dbName)!;\nconst hiddenPassword = '••••••••••••';\n\nfixture `Import databases`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        // Delete all existing connections\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseHelper.acceptLicenseTerms();\n        await myRedisDatabasePage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Delete all existing connections\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\n// todo: enable after RI-7450 fix\ntest.skip.before(async() => {\n    await databaseAPIRequests.deleteAllDatabasesApi();\n    await databaseHelper.acceptLicenseTerms();\n    await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n    await myRedisDatabasePage.reloadPage();\n})('Connection import modal window', async t => {\n    const defaultText = 'Select or drag and drop a file';\n    const parseFailedMsg = 'Failed to add database connections';\n    const parseFailedMsg2 = `Unable to parse ${fileNames.racompassInvalidJson}`;\n\n    // *** - outdated - Verify that user can see the “Import Database Connections” tooltip\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton);\n    await t.expect(myRedisDatabasePage.importDatabasesBtn.visible).ok('The import databases button not displayed');\n\n    // Verify that Import dialogue is not closed when clicking any area outside the box\n    await t.click(myRedisDatabasePage.importDatabasesBtn);\n    await t.expect(myRedisDatabasePage.addDatabaseImport.exists).ok('Import Database Connections dialog not opened');\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await t.expect(myRedisDatabasePage.addDatabaseImport.exists).ok('Import Database Connections dialog not displayed');\n\n    // Verify that user see the message when parse error appears\n    await t\n        .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [filePathes.racompassInvalidJsonPath])\n        .click(myRedisDatabasePage.submitChangesButton)\n        .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed')\n        .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg)\n        .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg2);\n\n    // Verify that user can remove file from import input\n    await t.click(myRedisDatabasePage.retryImportBtn);\n    await t.setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [rdmData.path]);\n\n    await t.expect(myRedisDatabasePage.addDatabaseImport.textContent).contains(fileNames.rdmFullJson, 'Filename not displayed in import input');\n    // Click on remove button\n    await t.click(myRedisDatabasePage.removeImportedFileBtn);\n    await t.expect(myRedisDatabasePage.addDatabaseImport.textContent).contains(defaultText, 'File not removed from import input');\n});\ntest\n    .skip('Connection import from JSON', async t => {\n    // Verify that user can import database with mandatory/optional fields\n    await databasesActions.importDatabase(rdmData);\n\n    // Fully imported table\n    await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n        .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number');\n    // Partially imported table\n    await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n        .contains(`${rdmData.partialNumber}`, 'Not correct partially imported number');\n    // Failed to import table\n    await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n        .contains(`${rdmData.failedNumber}`, 'Not correct import failed number');\n\n    // Verify that list of databases is reloaded when database added\n    await t.click(myRedisDatabasePage.closeImportBtn);\n    await databasesActions.verifyDatabasesDisplayed(rdmData.dbImportedNames);\n\n    await databaseHelper.clickOnEditDatabaseByName(rdmData.dbImportedNames[1]);\n    // Verify username imported\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.usernameInput.value).eql(rdmListOfDB[1].username, 'Username import incorrect');\n    // Verify password imported\n    // Verify that user can see 12 hidden characters regardless of the actual database password when it is set\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.passwordInput.value).eql(hiddenPassword, 'Password import incorrect');\n\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    // Verify cluster connection type imported\n    await databaseHelper.clickOnEditDatabaseByName(rdmData.dbImportedNames[2]);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.connectionType.textContent).eql(rdmData.connectionType, 'Connection type import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    /*\n     Verify that user can import database with CA certificate\n     Verify that user can import database with certificates by an absolute folder path(CA certificate, Client certificate, Client private key)\n     Verify that user can see the certificate name as the certificate file name\n    */\n    await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert'));\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('ca', 'CA certificate import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.exists).notOk('Client certificate was imported');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that user can import database with Client certificate, Client private key\n    await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+clientCert+privateKey'));\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql('client', 'Client certificate import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that user can import database with all certificates\n    await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert+clientCert+privateKey'));\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('ca', 'CA certificate import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql('client', 'Client certificate import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that certificate not imported when any certificate field has not been parsed\n    await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmCaCertInvalidBody'));\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.exists).notOk('Client certificate was imported');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    await databaseHelper.clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmInvalidClientCert'));\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.exists).notOk('Client certificate was imported');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that user can import files from Racompass, ARDM, RDM\n    for (const db of dbData) {\n        await databasesActions.importDatabase(db);\n        await t.click(myRedisDatabasePage.closeImportBtn);\n        await databasesActions.verifyDatabasesDisplayed(db.dbNames);\n    }\n\n    // Verify that user can import Sentinel database connections by corresponding fields in JSON\n    await databaseHelper.clickOnEditDatabaseByName(dbData[1].dbNames[2]);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sentinelForm.textContent).contains('Sentinel', 'Sentinel connection type import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    await myRedisDatabasePage.clickOnDBByName(dbData[1].dbNames[2]);\n    await Common.checkURLContainsText('browser');\n});\ntest\n    .skip('Certificates import with/without path', async t => {\n    await databasesActions.importDatabase({ path: rdmData.sshPath });\n    await t.click(myRedisDatabasePage.closeImportBtn);\n\n    // Verify that when user imports a certificate and the same certificate body already exists, the existing certificate (with its name) is applied\n    await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[0].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql(rdmListOfCertsDB[0].caCert.name, 'CA certificate import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql(rdmListOfCertsDB[0].clientCert.name, 'Client certificate import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[1].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql(rdmListOfCertsDB[0].caCert.name, 'CA certificate name with the same body is incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql(rdmListOfCertsDB[0].clientCert.name, 'Client certificate name with the same body is incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that when user imports a certificate and the same certificate name exists but with a different body, the certificate imported with \"({incremental_number})_certificate_name\" name\n    await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[2].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql(`1_${rdmListOfCertsDB[0].caCert.name}`, 'CA certificate name with the same body is incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql(`1_${rdmListOfCertsDB[0].clientCert.name}`, 'Client certificate name with the same body is incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that when user imports a certificate by path and the same certificate body already exists, the existing certificate (with its name) is applied\n    await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[3].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('caPath', 'CA certificate import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql('clientPath', 'Client certificate import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[4].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('caPath', 'CA certificate import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql('clientPath', 'Client certificate import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    // Verify that when user imports a certificate by path and the same certificate name exists but with a different body, the certificate imported with \"({incremental_number})certificate_name\" name\n    await databaseHelper.clickOnEditDatabaseByName(rdmListOfCertsDB[5].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).eql('1_caPath', 'CA certificate import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).eql('1_clientPath', 'Client certificate import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n});\ntest\n    .skip('Import SSH parameters', async t => {\n    const sshAgentsResult = 'SSH Agents are not supported';\n\n    await databasesActions.importDatabase(racompSSHData);\n    // Fully imported table with SSH\n    await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n        .contains(`${racompSSHData.successNumber}`, 'Not correct successfully SSH imported number');\n    // Partially imported table with SSH\n    await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n        .contains(`${racompSSHData.partialNumber}`, 'Not correct partially SSH imported number');\n    // Failed to import table with SSH\n    await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent)\n        .contains(`${racompSSHData.failedNumber}`, 'Not correct SSH import failed number');\n    // Expand partial results\n    await t.click(myRedisDatabasePage.partialResultsAccordion);\n    // Verify that database is partially imported with corresponding message when the ssh_agent_path specified in imported JSON\n    await t.expect(myRedisDatabasePage.importResult.withText(sshAgentsResult).exists).ok('SSH agents not supported message not displayed in result');\n\n    await t.click(myRedisDatabasePage.closeImportBtn);\n    await databaseHelper.clickOnEditDatabaseByName(racompListOfSSHDB[0].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    // Verify that user can import the SSH parameters with Password\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshHostInput.value).eql(racompListOfSSHDB[0].sshHost, 'SSH host import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPortInput.value).eql((racompListOfSSHDB[0].sshPort).toString(), 'SSH port import incorrect');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshUsernameInput.value).eql(racompListOfSSHDB[0].sshUser, 'SSH username import incorrect');\n    // Verify that password, passphrase and private key are hidden for SSH option\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPasswordInput.value).eql(hiddenPassword, 'SSH password import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n\n    await databaseHelper.clickOnEditDatabaseByName(racompListOfSSHDB[1].name);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    // Verify that user can import the SSH Private Key both by its value specified in the file and by the file path\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPrivateKeyInput.textContent).contains(hiddenPassword, 'SSH Private key import incorrect');\n    // Verify that user can import the SSH parameters with Passcode\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.sshPassphraseInput.value).eql(hiddenPassword, 'SSH Passphrase import incorrect');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/logical-databases.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Logical databases`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .afterEach(async() => {\n        //Delete database\n        await databaseHelper.deleteDatabase(ossStandaloneConfig.databaseName);\n    });\ntest.skip('Verify that user can add DB with logical index via host and port from Add DB manually form', async t => {\n    const index = '10';\n\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDataBase(ossStandaloneConfig);\n\n    // Verify that user can test database connection and see success message\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n    await t.expect(myRedisDatabasePage.Toast.toastHeader.textContent).contains('Connection is successful', 'Standalone connection is not successful');\n\n    // Enter logical index\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.databaseIndexCheckbox);\n    await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseIndexInput, index, { replace: true, paste: true });\n    // *** - outdated - Verify that user when users select DB index they can see info message how to work with DB index in add DB screen\n    // Verify that logical db message not displayed in add database form\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseIndexMessage.exists).notOk('Index message is still displayed')\n        .expect(myRedisDatabasePage.AddRedisDatabaseDialog.databaseIndexCheckbox.parent().withExactText('Select Logical Database').exists).ok('Checkbox text not displayed');\n    // Click for saving\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n    // Verify that the database is in the list\n    await t.expect(myRedisDatabasePage.dbNameList.withText(ossStandaloneConfig.databaseName).exists).ok('Database not exist', { timeout: 10000 });\n    // Verify that if user adds DB with logical DB > 0, DB name contains postfix \"space+[{database index}]\"\n    // Verify that user can see the db{index} instead of {index} in database alias\n    await t.expect(myRedisDatabasePage.dbNameList.textContent).eql(`${ossStandaloneConfig.databaseName} [db${index}]`, 'The postfix is not added to the database name', { timeout: 10000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database/modules.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst chance = new Chance();\n\nconst moduleNameList = ['Redis Query Engine', 'JSON', 'Graph', 'Time Series', 'Probabilistic', 'Gears', 'AI'];\nconst moduleList = [myRedisDatabasePage.moduleSearchIcon, myRedisDatabasePage.moduleJSONIcon, myRedisDatabasePage.moduleGraphIcon, myRedisDatabasePage.moduleTimeseriesIcon, myRedisDatabasePage.moduleBloomIcon, myRedisDatabasePage.moduleGearsIcon, myRedisDatabasePage.moduleAIIcon];\nconst uniqueId = chance.string({ length: 10 });\nlet database = {\n    ...ossStandaloneConfig,\n    databaseName: `test_standalone-redisearch-${uniqueId}`\n};\n\nfixture `Database modules`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        database = {\n            ...ossStandaloneConfig,\n            databaseName: `test_standalone-redisearch-${uniqueId}`\n        };\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(database);\n        // Reload Page\n        await browserPage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(database);\n    });\ntest.skip('Verify that user can see DB modules on DB list page for Standalone DB', async t => {\n    // Check module column on DB list page\n    await t.expect(myRedisDatabasePage.moduleColumn.exists).ok('Module column not found');\n    // Verify that user can see the following sorting order: Search, JSON, Graph, TimeSeries, Bloom, Gears, AI for modules\n    const databaseLine = myRedisDatabasePage.dbNameList.withExactText(database.databaseName).parent('tr');\n    await t.expect(databaseLine.visible).ok('Database not found in db list');\n    const moduleIcons = databaseLine.find('[data-testid*=_module]');\n    const numberOfIcons = await moduleIcons.count;\n    for (let i = 0; i < numberOfIcons; i++) {\n        const moduleName = await moduleIcons.nth(i).getAttribute('data-testid');\n        const expectedName = await moduleList[i].getAttribute('data-testid');\n        await t.expect(moduleName).eql(expectedName, `${moduleName} icon not found`);\n    }\n    // Minimize the window to check quantifier\n    await t.resizeWindow(1000, 700);\n    // Verify that user can see +N icon (where N>1) on DB list page when modules icons don't fit the Module column width\n    await t.expect(myRedisDatabasePage.moduleQuantifier.textContent).eql('+3');\n    await t.expect(myRedisDatabasePage.moduleQuantifier.exists).ok('Quantifier icon not found');\n    // Verify that user can hover over the module icons and see tooltip with all modules name\n    await t.hover(myRedisDatabasePage.moduleQuantifier);\n    await t.expect(myRedisDatabasePage.moduleTooltip.visible).ok('Module tooltip not found');\n    // Verify that user can hover over the module icons and see tooltip with version.\n    await myRedisDatabasePage.checkModulesInTooltip(moduleNameList);\n});\ntest.skip('Verify that user can see full module list in the Edit mode', async t => {\n    // Verify that module column is displayed\n    await t.expect(myRedisDatabasePage.connectionTypeTitle.visible).ok('connection type column not found');\n    // Open Edit mode\n    await t.click(myRedisDatabasePage.editDatabaseButton);\n    // **** Deprecated **** Verify that module column is not displayed\n    // await myRedisDatabasePage.NavigationHeader.togglePanel(true);\n    // await t.expect(myRedisDatabasePage.connectionTypeTitle.visible).notOk('connection type column not found');\n    // Verify modules in Edit mode\n    await myRedisDatabasePage.checkModulesOnPage(moduleList);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { Common } from '../../../../helpers/common';\nimport {\n    MyRedisDatabasePage,\n    BrowserPage,\n    WorkbenchPage,\n    MemoryEfficiencyPage\n} from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { verifyKeysDisplayingInTheList, verifySearchFilterValue } from '../../../../helpers/keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyName = Common.generateWord(10);\nconst indexName = `idx:${keyName}`;\nconst keyNames = [`${keyName}:1`, `${keyName}:2`];\nconst commands = [\n    `HSET ${keyNames[0]} \"name\" \"Hall School\" \"description\" \" Spanning 10 states\" \"class\" \"independent\" \"type\" \"traditional\" \"address_city\" \"London\" \"address_street\" \"Manor Street\" \"students\" 342 \"location\" \"51.445417, -0.258352\"`,\n    `HSET ${keyNames[1]} \"name\" \"Garden School\" \"description\" \"Garden School is a new outdoor\" \"class\" \"state\" \"type\" \"forest; montessori;\" \"address_city\" \"London\" \"address_street\" \"Gordon Street\" \"students\" 1452 \"location\" \"51.402926, -0.321523\"`,\n    `FT.CREATE ${indexName} ON HASH PREFIX 1 \"${keyName}:\" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR \";\" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`\n];\nconst keyNameForSearchInLogicalDb = 'keyForSearch';\nconst logicalDbKey = `${keyName}:3`;\n\nfixture `Allow to change database index`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Create 3 keys and index\n        await browserPage.Cli.sendCommandsInCli(commands);\n    })\n    .afterEach(async() => {\n        // Delete keys in logical database\n        await browserPage.OverviewPanel.changeDbIndex(1);\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNameForSearchInLogicalDb}`, `DEL ${logicalDbKey}`]);\n        // Delete and clear database\n        await browserPage.OverviewPanel.changeDbIndex(0);\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `DEL ${keyName}`, `FT.DROPINDEX ${indexName}`]);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest.skip('Switching between indexed databases', async t => {\n    const command = `HSET ${logicalDbKey} \"name\" \"Gillford School\" \"description\" \"Gillford School is a centre\" \"class\" \"private\" \"type\" \"democratic; waldorf\" \"address_city\" \"Goudhurst\" \"address_street\" \"Goudhurst\" \"students\" 721 \"location\" \"51.112685, 0.451076\"`;\n\n    // Change index to logical db\n    // Verify that database index switcher displayed for Standalone db\n    await browserPage.OverviewPanel.changeDbIndex(1);\n    // Verify that the same client connections are used after changing index\n    // issue https://redislabs.atlassian.net/browse/RI-5079\n    // const logicalDbConnectedClients = await browserPage.overviewConnectedClients.textContent;\n    // await t.expect(rememberedConnectedClients).eql(logicalDbConnectedClients);\n\n    // Verify that data changed for indexed db on Browser view\n    await browserPage.verifyNoKeysInDatabase();\n\n    // Verify that logical db not changed after reloading page\n    await browserPage.reloadPage();\n    await browserPage.OverviewPanel.verifyDbIndexSelected(1);\n    await browserPage.verifyNoKeysInDatabase();\n\n    // Add key to logical (index=1) database\n    await browserPage.addHashKey(keyNameForSearchInLogicalDb);\n    // Verify that data changed for indexed db on Tree view\n    await t.click(browserPage.treeViewButton);\n    await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], true);\n    await verifyKeysDisplayingInTheList(keyNames, false);\n\n    // Filter by Hash keys and search by key name\n    await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n    await browserPage.searchByKeyName(keyNameForSearchInLogicalDb);\n    // Return to default database\n    await browserPage.OverviewPanel.changeDbIndex(0);\n\n    // Verify that search/filter saved after switching index in Browser\n    await verifySearchFilterValue(keyNameForSearchInLogicalDb);\n    await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], false);\n    await t.click(browserPage.browserViewButton);\n    // Change index to logical db\n    await browserPage.OverviewPanel.changeDbIndex(1);\n    await verifySearchFilterValue(keyNameForSearchInLogicalDb);\n    await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb], true);\n\n    // Return to default database and open search capability\n    await browserPage.OverviewPanel.changeDbIndex(0);\n    await t.click(browserPage.redisearchModeBtn);\n    await browserPage.selectIndexByName(indexName);\n    await verifyKeysDisplayingInTheList(keyNames, true);\n    // Change index to logical db\n    await browserPage.OverviewPanel.changeDbIndex(1);\n    // Search by value and return to default database\n    await browserPage.searchByKeyName('Hall School');\n    await browserPage.OverviewPanel.changeDbIndex(0);\n    // Verify that data changed for indexed db on Search capability page\n    await verifyKeysDisplayingInTheList([keyNames[0]], true);\n    // Change index to logical db\n    await browserPage.OverviewPanel.changeDbIndex(1);\n    // Verify that search/filter saved after switching index in Search capability\n    await verifySearchFilterValue('Hall School');\n\n    // Open Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench(command);\n    // Verify that user can see the database index before the command name executed in Workbench\n    await workbenchPage.checkWorkbenchCommandResult(`[db1] ${command}`, '8');\n\n    // Open Browser page\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Clear filter\n    await t.click(browserPage.clearFilterButton);\n    // Verify that data changed for indexed db on Workbench page (on Search capability page)\n    await verifyKeysDisplayingInTheList([logicalDbKey], true);\n    await t.click(browserPage.patternModeBtn);\n    // Clear filter\n    await t.click(browserPage.clearFilterButton);\n    // Verify that data changed for indexed db on Workbench page\n    await verifyKeysDisplayingInTheList([keyNameForSearchInLogicalDb, logicalDbKey], true);\n    await browserPage.OverviewPanel.changeDbIndex(0);\n    await verifyKeysDisplayingInTheList([logicalDbKey], false);\n\n    // Go to Analysis Tools page and create new report\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(memoryEfficiencyPage.newReportBtn);\n\n    // Verify that data changed for indexed db on Database analysis page\n    await t.expect(memoryEfficiencyPage.topKeysKeyName.withExactText(keyNames[0]).exists).ok('Keys from current db index not displayed in report');\n    await t.expect(memoryEfficiencyPage.topKeysKeyName.withExactText(logicalDbKey).exists).notOk('Keys from other db index displayed in report');\n    await t.expect(memoryEfficiencyPage.selectedReport.textContent).notContains('[db', 'Index displayed for 0 index in report name');\n    // Change index to logical db\n    await browserPage.OverviewPanel.changeDbIndex(1);\n    await t.click(memoryEfficiencyPage.newReportBtn);\n    await t.expect(memoryEfficiencyPage.selectedReport.textContent).contains('[db1]', 'Index not displayed in report name');\n    await t.expect(memoryEfficiencyPage.topKeysKeyName.withExactText(logicalDbKey).exists).ok('Keys from current db index not displayed in report');\n    await t.expect(memoryEfficiencyPage.topKeysKeyName.withExactText(keyNames[0]).exists).notOk('Keys from other db index displayed in report');\n\n    // Verify that user can see the database index before the report date in Database Analysis\n    await t.click(memoryEfficiencyPage.selectedReport);\n    await t.expect(memoryEfficiencyPage.reportItem.withText('[db1]').count).eql(1, 'Index not displayed in report name');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { Common } from '../../../../helpers/common';\nimport {\n    MyRedisDatabasePage,\n    BrowserPage,\n    WorkbenchPage\n} from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig,\n    ossStandaloneV5Config,\n    ossStandaloneBigConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst chance = new Chance();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst fiveSecondsTimeout = 5000;\nlet keyName = chance.string({ length: 10 });\nlet keys: string[];\nlet keys1: string[];\nlet keys2: string[];\n\n// todo: rethink. might be flaky since requires empty database to calculate overview\nfixture `Database overview`\n    .meta({ type: 'critical_path' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .meta({ rte: rte.standalone })\n    ('Verify that user can see the list of Modules updated each time when he connects to the database', async t => {\n        const firstDatabaseModules: string[] = [];\n        const secondDatabaseModules: string[] = [];\n        //Remember modules\n        await t.hover(browserPage.OverviewPanel.databaseInfoIcon);\n        await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.visible).ok('Tooltip is not opened');\n        const moduleIcons = await browserPage.OverviewPanel.databaseInfoToolTip.find('[data-testid^=module_]');\n        let countOfModules = await moduleIcons.count;\n        for(let i = 0; i < countOfModules; i++) {\n            firstDatabaseModules.push(await moduleIcons.nth(i).textContent);\n        }\n\n        // Verify that user can be redirected to db list page by clicking on \"Databases\" link in the top left corner\n        await t.click(browserPage.OverviewPanel.myRedisDBLink);\n        //Add database with different modules\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config);\n        await browserPage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n        await t.hover(browserPage.OverviewPanel.databaseInfoIcon);\n        await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.visible).ok('Tooltip is not opened');\n        countOfModules = await moduleIcons.count;\n        for(let i = 0; i < countOfModules; i++) {\n            secondDatabaseModules.push(await moduleIcons.nth(i).textContent);\n        }\n        //Verify the list of modules\n        await t.expect(firstDatabaseModules).notEql(secondDatabaseModules, 'The list of Modules updated');\n    });\ntest\n    .meta({ rte: rte.standalone })('Verify that when user adds or deletes a new key, info in DB header is updated in 5 seconds', async t => {\n        keyName = chance.string({ length: 10 });\n        //Remember the total keys number\n        const totalKeysBeforeAdd = await browserPage.OverviewPanel.overviewTotalKeys.innerText;\n        //Add new key\n        await browserPage.addHashKey(keyName);\n        //Wait 5 seconds\n        await t.wait(fiveSecondsTimeout);\n        //Verify that the info on DB header is updated after adds\n        const totalKeysAftreAdd = await browserPage.OverviewPanel.overviewTotalKeys.innerText;\n        await t.expect(totalKeysAftreAdd).eql((Number(totalKeysBeforeAdd) + 1).toString(), 'Info in DB header after ADD');\n        //Delete key\n        await browserPage.deleteKeyByName(keyName);\n        //Wait 5 seconds\n        await t.wait(fiveSecondsTimeout);\n        //Verify that the info on DB header is updated after deletion\n        const totalKeysAftreDelete = await browserPage.OverviewPanel.overviewTotalKeys.innerText;\n        await t.expect(totalKeysAftreDelete).eql((Number(totalKeysAftreAdd) - 1).toString(), 'Info in DB header after DELETE');\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .after(async t => {\n        //Clear and delete database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys1.join(' ')}`);\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys2.join(' ')}`);\n    })('Verify that user can see total number of keys rounded in format 100, 1K, 1M, 1B in DB header in Browser page', async t => {\n        const initialKeys = parseInt(await browserPage.OverviewPanel.overviewTotalKeys.innerText, 10) || 0;\n        //Add 100 keys\n        keys1 = await Common.createArrayWithKeyValue(100);\n        await browserPage.Cli.sendCliCommandAndWaitForTotalKeys(`MSET ${keys1.join(' ')}`);\n        let totalKeys = await browserPage.OverviewPanel.overviewTotalKeys.innerText;\n        //Verify that the info on DB header is updated after adds\n        await t.expect(totalKeys).eql(`${initialKeys + 100}`, 'Info in DB header after ADD 100 keys');\n        //Add 1000 keys\n        keys2 = await Common.createArrayWithKeyValue(1000);\n        await browserPage.Cli.sendCliCommandAndWaitForTotalKeys(`MSET ${keys2.join(' ')}`);\n        totalKeys = await browserPage.OverviewPanel.overviewTotalKeys.innerText;\n        //Verify that the info on DB header is updated after adds\n        await t.expect(totalKeys).eql('1 K', 'Info in DB header after ADD 1000 keys');\n        //Add database with more than 1M keys\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneBigConfig);\n        await browserPage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneBigConfig.databaseName);\n        //Wait 5 seconds\n        await t.wait(fiveSecondsTimeout);\n        //Verify that the info on DB header is rounded\n        totalKeys = await browserPage.OverviewPanel.overviewTotalKeys.innerText;\n        await t.expect(totalKeys).eql('18 M', 'Info in DB header is 18 M keys');\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .after(async() => {\n        //Clear and delete database\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n    })('Verify that user can see total memory rounded in format B, KB, MB, GB, TB in DB header in Browser page', async t => {\n        //Add new keys\n        keys = await Common.createArrayWithKeyValue(100);\n        await browserPage.Cli.sendCommandInCli(`MSET ${keys.join(' ')}`);\n        //Verify total memory\n        await t.wait(fiveSecondsTimeout);\n        await t.expect(browserPage.OverviewPanel.overviewTotalMemory.textContent).contains('MB', 'Total memory value is MB');\n    });\n// todo: rethink. flaky test. cpu, cmd/s are not guaranteed.\ntest.skip\n    .meta({ rte: rte.standalone })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    })\n    .after(async t => {\n        //Delete database and index\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await workbenchPage.sendCommandInWorkbench('FT.DROPINDEX idx:schools DD');\n    })('Verify that user can see additional information in Overview: Connected Clients, Commands/Sec, CPU (%) using Standalone DB connection type', async t => {\n        const commandsSecBeforeEdit = await browserPage.OverviewPanel.overviewCommandsSec.textContent;\n        await browserPage.OverviewPanel.waitForCpuIsCalculated();\n        //Verify that additional information in Overview: Connected Clients, Commands/Sec, CPU (%) is displayed\n        await t.expect(browserPage.OverviewPanel.overviewConnectedClients.exists).ok('Connected Clients is dispalyed in the Overview');\n        await t.expect(browserPage.OverviewPanel.overviewCommandsSec.exists).ok('Commands/Sec is dispalyed in the Overview');\n        await t.expect(browserPage.OverviewPanel.overviewCpu.exists).ok('CPU (%) is dispalyed in the Overview');\n        //Run Create hash index command\n        await browserPage.NavigationHeader.togglePanel(true);\n        const tutorials = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.click(tutorials.dataStructureAccordionTutorialButton);\n        await t.click(tutorials.internalLinkWorkingWithHashes);\n        const cpuBeforeEdit = (await browserPage.OverviewPanel.overviewCpu.textContent).split(' ')[0];\n        await tutorials.runBlockCode('Create a hash');\n        //Verify that CPU and commands per second parameters are changed\n        const commandsSecAfterEdit = await browserPage.OverviewPanel.overviewCommandsSec.textContent;\n        await browserPage.OverviewPanel.waitForCpuIsCalculated();\n        const cpuAfterEdit = (await browserPage.OverviewPanel.overviewCpu.textContent).split(' ')[0];\n        await t.expect(Number(cpuAfterEdit)).gt(Number(cpuBeforeEdit), 'CPU parameter is changed');\n        await t.expect(commandsSecAfterEdit).notEql(commandsSecBeforeEdit, 'Commands per second parameter is changed');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {commonUrl, ossStandaloneConfig, ossStandaloneV5Config} from '../../../../helpers/conf'\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { deleteAllKeysFromDB, verifySearchFilterValue } from '../../../../helpers/keys';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst chance = new Chance();\n\nconst hashKeyName = 'test:Hash1';\nconst hashValue = 'hashValue11111!';\nconst streamKeyName = 'test:Stream1';\nconst streamKeyNameDelimiter = 'test-Stream1';\nconst keySpaces = ['test:*', 'key1:*', 'key2:*', 'key5:*', 'key5:5', 'test-*', 'key4:*'];\nconst keysTTL = [3500, 86300, 2147476121];\nconst numberOfGeneratedKeys = 6;\nconst keyNamesReport = chance.unique(chance.word, numberOfGeneratedKeys);\n\nfixture(`Memory Efficiency`)\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('No reports/keys message and report tooltip', async t => {\n        const noReportsMessage = 'No Reports foundClick \"Analyze\" to generate the first report.';\n        const noKeysMessage = 'No keys to displayUse Workbench Guides and Tutorials to quickly load the data.';\n        const tooltipText = 'Analyze up to 10 000 keys to get an overview of your data and tips';\n\n        // Verify that user can see the “No reports found” message when report wasn't generated\n        await t.expect(memoryEfficiencyPage.noReportsText.textContent).eql(noReportsMessage, 'No reports message not displayed or text is invalid');\n        // Verify that user can see the “No keys to display” message when there are no keys in database\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await t.expect(memoryEfficiencyPage.noKeysText.textContent).eql(noKeysMessage, 'No keys message not displayed or text is invalid');\n        // Verify that user can open workbench page from No keys to display message\n        await t.click(memoryEfficiencyPage.workbenchLink);\n        await t.expect(workbenchPage.queryInput.visible).ok('Workbench page is not opened');\n        // Turn back to Memory Efficiency page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        // Verify that user can see a tooltip when hovering over the icon on the right of the “New analysis” button\n        await t.hover(memoryEfficiencyPage.reportTooltipIcon);\n        await t.expect(browserPage.tooltip.textContent).contains(tooltipText, 'Report tooltip is not displayed or text is invalid');\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await apiKeyRequests.addHashKeyApi({\n            keyName: hashKeyName,\n            fields: [{\n                field: `${keysTTL[2]}`,\n                value: hashValue,\n            }]\n        }, ossStandaloneConfig);\n        await apiKeyRequests.addStreamKeyApi({\n            keyName: streamKeyName,\n            ttl: keysTTL[2],\n            entries: [{\n                id: '*',\n                fields: [{\n                    name: 'field',\n                    value: 'value',\n                }],\n            }]\n        }, ossStandaloneConfig);\n        await apiKeyRequests.addStreamKeyApi({\n            keyName: streamKeyNameDelimiter,\n            ttl: keysTTL[2],\n            entries: [{\n                id: '*',\n                fields: [{\n                    name: 'field',\n                    value: 'value',\n                }],\n            }]\n        }, ossStandaloneConfig);\n\n        await browserPage.Cli.addKeysFromCliWithDelimiter('MSET', 15);\n        await t.click(browserPage.treeViewButton);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async t => {\n        await browserPage.Cli.deleteKeysFromCliWithDelimiter(15);\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await t.click(browserPage.browserViewButton);\n        await apiKeyRequests.deleteKeyByNameApi(hashKeyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(streamKeyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(streamKeyNameDelimiter, ossStandaloneConfig.databaseName);\n    })('Keyspaces displaying in Summary per keyspaces table', async t => {\n        const noNamespacesMessage = 'No namespaces to displayConfigure the delimiter in Tree View to customize the namespaces displayed.';\n\n        // Create new report\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Verify that up to 15 keyspaces based on the delimiter set in the Tree view are displayed on memory efficiency page\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.count).eql(15, 'Namespaces table has more/less than 15 keyspaces');\n\n        // Verify that sorting by Total Memory from big to small applied by default\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[0], 'Biggest memory keyspace is not at top');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(14).textContent).contains(keySpaces[2], 'Smallest memory keyspace is not at down');\n\n        await t.click(memoryEfficiencyPage.nspTableExpandArrowBtn);\n        // Verify that Key Pattern with >1 keys can be expanded\n        await t.expect(memoryEfficiencyPage.expandedRow.count).eql(2, 'Expandable row has no items');\n        // Verify that user can quickly set the filters per keyspaces in the Browser/Tree View from the list of keyspaces\n        await t.click(memoryEfficiencyPage.expandedItem);\n        // Verify filter by data type applied\n        await t.expect(browserPage.filteringLabel.textContent).eql('Stream', 'Key type lable is not displayed in search input');\n        // Verify keyname in search input prepopulated\n        await verifySearchFilterValue(keySpaces[0]);\n        // Verify key is displayed\n        await t.click(browserPage.browserViewButton);\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(streamKeyName)).ok('Key is not found');\n\n        // Clear filter\n        await t\n            .click(browserPage.treeViewButton)\n            .click(browserPage.clearFilterButton);\n        // Change delimiter\n        await browserPage.TreeView.changeDelimiterInTreeView('-');\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        // Create new report\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Verify that delimiter can be changed in Tree View and applied\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.count).eql(1, 'New delimiter not applied');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[5], 'Keyspace not displayed');\n\n        // No namespaces message with link\n        await t.click(browserPage.NavigationTabs.browserButton);\n        // Change delimiter to delimiter with no keys\n        await browserPage.TreeView.changeDelimiterInTreeView('+');\n        // Go to Analysis Tools page and create report\n        await t\n            .click(browserPage.NavigationTabs.analysisButton)\n            .click(memoryEfficiencyPage.newReportBtn);\n        // Verify that user can see the message when he do not have any namespaces selected in delimiter\n        await t.expect(memoryEfficiencyPage.topNamespacesEmptyContainer.exists).ok('No namespaces section not displayed');\n        await t.expect(memoryEfficiencyPage.topNamespacesEmptyMessage.textContent).contains(noNamespacesMessage, 'No namespaces message not displayed/correct');\n        // Verify that user can redirect to Tree view by clicking on button\n        await t.click(memoryEfficiencyPage.treeViewLink);\n        await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).ok('Tree view not opened');\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await deleteAllKeysFromDB(ossStandaloneConfig.host, ossStandaloneConfig.port);\n        await apiKeyRequests.addHashKeyApi({\n            keyName: keySpaces[4],\n            fields: [{\n                field: `${keysTTL[2]}`,\n                value: hashValue,\n            }]\n        }, ossStandaloneConfig);\n        await browserPage.Cli.addKeysFromCliWithDelimiter('MSET', 5);\n        await t.click(browserPage.treeViewButton);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async t => {\n        await browserPage.Cli.deleteKeysFromCliWithDelimiter(5);\n        await apiKeyRequests.deleteKeyByNameApi(keySpaces[4], ossStandaloneConfig.databaseName);\n    })('Namespaces sorting', async t => {\n        // Create new report\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Verify that user can sort by Key Pattern column ASC\n        await t.click(memoryEfficiencyPage.tableKeyPatternHeader);\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[1], 'Sorting by Key Pattern ASC not working');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(4).textContent).contains(keySpaces[3], 'Sorting by Key Pattern ASC not working');\n        // Verify that user can sort by Key Pattern column DESC\n        await t.click(memoryEfficiencyPage.tableKeyPatternHeader);\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[3], 'Sorting by Key Pattern DESC not working');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(4).textContent).contains(keySpaces[1], 'Sorting by Key Pattern DESC not working');\n\n        // Verify that user can sort by Total Memory column DESC\n        await t.click(memoryEfficiencyPage.tableMemoryHeader);\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[3], 'Sorting by Total Memory DESC not working');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(4).textContent).contains(keySpaces[1], 'Sorting by Total Memory DESC not working');\n        // Verify that user can sort by Total Memory column ASC\n        await t.click(memoryEfficiencyPage.tableMemoryHeader);\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[1], 'Sorting by Total Memory ASC not working');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(4).textContent).contains(keySpaces[3], 'Sorting by Total Memory ASC not working');\n\n        // Verify that user can sort by Total Keys column DESC\n        await t.click(memoryEfficiencyPage.tableKeysHeader);\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[3], 'Sorting by Total Keys DESC not working');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(4).textContent).contains(keySpaces[1], 'Sorting by Total Keys DESC not working');\n        // Verify that user can sort by Total Keys column ASC\n        await t.click(memoryEfficiencyPage.tableKeysHeader);\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[6], 'Sorting by Total Keys ASC not working');\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(4).textContent).contains(keySpaces[3], 'Sorting by Total Keys ASC not working');\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await apiKeyRequests.addHashKeyApi({\n            keyName: hashKeyName,\n            fields: [{\n                field: `${keysTTL[2]}`,\n                value: hashValue,\n            }]\n        }, ossStandaloneConfig);\n        await t.click(browserPage.treeViewButton);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async t => {\n        await apiKeyRequests.deleteKeyByNameApi(hashKeyName, ossStandaloneConfig.databaseName);\n    })('Memory efficiency context saved', async t => {\n        // Create new report\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Reload page\n        await memoryEfficiencyPage.reloadPage();\n        // Verify that context saved after reloading page\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[0], 'Summary per keyspaces context not saved');\n        //Go to PubSub page\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        // Verify that context saved after switching between pages\n        await t.expect(memoryEfficiencyPage.nameSpaceTableRows.nth(0).textContent).contains(keySpaces[0], 'Summary per keyspaces context not saved');\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await apiKeyRequests.addHashKeyApi({\n            keyName: hashKeyName,\n            fields: [{\n                field: `${keysTTL[0]}`,\n                value: hashValue,\n            }]\n        }, ossStandaloneConfig);\n        await apiKeyRequests.addStreamKeyApi({\n            keyName: streamKeyName,\n            ttl: keysTTL[1],\n            entries: [{\n                id: '*',\n                fields: [{\n                    name: 'field',\n                    value: 'value',\n                }],\n            }]\n        }, ossStandaloneConfig);\n        await apiKeyRequests.addStreamKeyApi({\n            keyName: streamKeyNameDelimiter,\n            entries: [{\n                id: '*',\n                fields: [{\n                    name: 'field',\n                    value: 'value',\n                }],\n            }]\n        }, ossStandaloneConfig);\n\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async t => {\n        await apiKeyRequests.deleteKeyByNameApi(hashKeyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(streamKeyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(streamKeyNameDelimiter, ossStandaloneConfig.databaseName);\n    })('Summary per expiration time', async t => {\n        const yAxis = 218;\n        // Create new report\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Points are displayed in graph according to their TTL\n        const firstPointLocation = +((await memoryEfficiencyPage.firstPoint.getAttribute('y'))!.slice(0, 2));\n        const thirdPointLocation = await memoryEfficiencyPage.thirdPoint.getAttribute('y');\n        const fourthPointLocation = +((await memoryEfficiencyPage.fourthPoint.getAttribute('y'))!.slice(0, 2));\n        const noExpiryDefaultPointLocation = memoryEfficiencyPage.noExpiryPoint;\n\n        await t.expect(firstPointLocation).lt(yAxis, 'Point in <1 hr breakdown doesn\\'t contain key');\n        await t.expect(fourthPointLocation).lt(yAxis, 'Point in 12-25 Hrs breakdown doesn\\'t contain key');\n        await t.expect(thirdPointLocation).eql(`${yAxis}`, 'Point in 4-12 Hrs breakdown contains key');\n        await t.expect(noExpiryDefaultPointLocation.visible).notOk('No expiry breakdown displayed when toggle is off', { timeout: 1000 });\n        // No Expiry toggle shows No expiry breakdown\n        await t.click(memoryEfficiencyPage.showNoExpiryToggle);\n        const noExpiryPointLocation = +((await memoryEfficiencyPage.noExpiryPoint.getAttribute('y'))!.slice(0, 2));\n        await t.expect(noExpiryPointLocation).lt(yAxis, 'Point in No expiry breakdown doesn\\'t contain key');\n    });\n// todo: rethink. flaky test. requires accurate number of keys which is not correct. also redis scan might return more keys.\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli(`del ${keyNamesReport.join(' ')}`);\n    })('Analysis history', async t => {\n        const numberOfKeys: string[] = [];\n        const dbSize = (await browserPage.Cli.getSuccessCommandResultFromCli('dbsize')).split(' ');\n        const existedNumberOfKeys = parseInt(dbSize[dbSize.length - 1]);\n        for (let i = 0; i < 6; i++) {\n            await browserPage.Cli.sendCommandInCli(`set ${keyNamesReport[i]} ${chance.word()}`);\n            await t.hover(memoryEfficiencyPage.newReportBtn);\n            await t.click(memoryEfficiencyPage.newReportBtn).setTestSpeed(.9);\n            const compareValue = parseInt(await memoryEfficiencyPage.donutTotalKeys.sibling(1).textContent);\n            await t.expect(compareValue).eql((existedNumberOfKeys + i + 1), 'New report is not displayed', { timeout: 2000 });\n            numberOfKeys.push(await memoryEfficiencyPage.donutTotalKeys.sibling(1).textContent);\n        }\n        await t.click(memoryEfficiencyPage.selectedReport);\n        // Verify that user can see up to the 5 most recent previous results per database in the history\n        await t.expect(memoryEfficiencyPage.reportItem.count).eql(5, 'Number of saved reports is not correct');\n        // Verify that user can switch between reports and see all data updated in each report\n        await t.click(memoryEfficiencyPage.reportItem.nth(0));\n        for (let i = 0; i < 5; i++) {\n            await t.click(memoryEfficiencyPage.selectedReport);\n            await t.hover(memoryEfficiencyPage.reportItem.nth(i));\n            await t.click(memoryEfficiencyPage.reportItem.nth(i)).setTestSpeed(.9);\n            await t.expect(memoryEfficiencyPage.reportItem.exists).notOk('Report is not switched');\n            await t.expect(memoryEfficiencyPage.scannedKeysInReport.textContent).contains(`(${numberOfKeys[5 - i]}/${numberOfKeys[5 - i]} keys)`);\n            const actualNumber = await memoryEfficiencyPage.donutTotalKeys.sibling(1).textContent;\n            await t.expect(actualNumber).eql(numberOfKeys[5 - i], 'Report content (total keys) is not correct', { timeout: 2000 });\n        }\n        // Verify that specific report is saved as context\n        await t.click(memoryEfficiencyPage.selectedReport);\n        await t.click(memoryEfficiencyPage.reportItem.nth(3));\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await t.expect(memoryEfficiencyPage.donutTotalKeys.sibling(1).textContent).eql(numberOfKeys[2], 'Context is not saved');\n        // Verify that user can see top keys table saved as context\n        await t.expect(memoryEfficiencyPage.topKeysKeyName.count).eql(parseInt(numberOfKeys[2]), 'Top Keys table is not saved as context');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts",
    "content": "import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { ExploreTabs, RecommendationIds, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    ossSentinelConfig,\n    ossStandaloneBigConfig,\n    ossStandaloneConfig,\n    ossStandaloneV5Config\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { RecommendationsActions } from '../../../../common-actions/recommendations-actions';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nimport { Telemetry } from '../../../../helpers';\n\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst recommendationsActions = new RecommendationsActions();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst telemetry = new Telemetry();\n\nconst logger = telemetry.createLogger();\n\nconst telemetryEvent = 'DATABASE_ANALYSIS_TIPS_COLLAPSED';\nconst expectedProperties = [\n    'databaseId',\n    'provider',\n    'recommendation'\n];\n\n// const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/';\nlet keyName = `recomKey-${Common.generateWord(10)}`;\nconst stringKeyName = `smallStringKey-${Common.generateWord(5)}`;\nconst index = '1';\nconst luaScriptRecommendation = RecommendationIds.luaScript;\nconst useSmallerKeysRecommendation = RecommendationIds.useSmallerKeys;\nconst avoidLogicalDbRecommendation = RecommendationIds.avoidLogicalDatabases;\nconst redisVersionRecommendation = RecommendationIds.redisVersion;\nconst searchJsonRecommendation = RecommendationIds.searchJson;\n\nfixture(`Memory Efficiency Recommendations`)\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest\n    .requestHooks(logger)\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        // Add cached scripts and generate new report\n        await memoryEfficiencyPage.Cli.addCachedScripts(11);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Go to Recommendations tab\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('SCRIPT FLUSH');\n    })('Recommendations displaying', async t => {\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10\n        await t.expect(await memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).exists)\n            .ok('Avoid dynamic lua script recommendation not displayed');\n        // Verify that user can see type of recommendation badge\n        await t.expect(memoryEfficiencyPage.getRecommendationLabelByName(luaScriptRecommendation, 'code').exists)\n            .ok('Avoid dynamic lua script recommendation not have Code Changes label');\n        await t.expect(memoryEfficiencyPage.getRecommendationLabelByName(luaScriptRecommendation, 'configuration').exists)\n            .notOk('Avoid dynamic lua script recommendation have Configuration Changes label');\n\n        // Verify that user can see Use smaller keys recommendation when database has 1M+ keys\n        await t.expect(await memoryEfficiencyPage.getRecommendationByName(useSmallerKeysRecommendation).exists).ok('Use smaller keys recommendation not displayed');\n\n        // Verify that user can see all the recommendations expanded by default\n        await t.expect(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation).getAttribute('aria-expanded'))\n            .eql('true', 'Avoid dynamic lua script recommendation not expanded');\n        await t.expect(memoryEfficiencyPage.getRecommendationButtonByName(useSmallerKeysRecommendation).getAttribute('aria-expanded'))\n            .eql('true', 'Use smaller keys recommendation not expanded');\n\n        // Verify that user can expand/collapse recommendation\n        const expandedTextContaiterSize = await memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight;\n        await t.click(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation));\n\n        //Verify telemetry event\n        await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n\n        await t.expect(memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight)\n            .lt(expandedTextContaiterSize, 'Lua script recommendation not collapsed');\n        await t.click(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation));\n        await t.expect(memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight)\n            .eql(expandedTextContaiterSize, 'Lua script recommendation not expanded');\n    });\n// skipped due to inability to receive no recommendations for now\ntest.skip('No recommendations message', async t => {\n    keyName = `recomKey-${Common.generateWord(10)}`;\n    const noRecommendationsMessage = 'No Tips at the moment,keep up the good work!';\n    const command = `HSET ${keyName} field value`;\n\n    // Create Hash key and create report\n    await browserPage.Cli.sendCommandInCli(command);\n    // Go to Analysis Tools page\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(memoryEfficiencyPage.newReportBtn);\n    // Go to Recommendations tab\n    await t.click(memoryEfficiencyPage.recommendationsTab);\n    // No recommendations message\n    await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed');\n});\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        keyName = `recomKey-${Common.generateWord(10)}`;\n        await apiKeyRequests.addStringKeyApi({\n            keyName: stringKeyName,\n            value: 'field',\n            ttl: 2147476121,\n        }, ossStandaloneConfig);\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addLogicalRedisDatabase(ossStandaloneConfig, index);\n        await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName} [db${index}]`);\n        await browserPage.Cli.sendCommandInCli(`SET ${keyName} 1`);\n    })\n    .after(async t => {\n        // Clear and delete database\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await browserPage.deleteKeyByName(keyName);\n        await databaseHelper.deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await browserPage.deleteKeyByName(stringKeyName);\n    })('Avoid using logical databases recommendation', async t => {\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Go to Recommendations tab\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n        // Verify that user can see Avoid using logical databases recommendation when the database supports logical databases and there are keys in more than 1 logical database\n        await t.expect(await memoryEfficiencyPage.getRecommendationByName(avoidLogicalDbRecommendation).exists)\n            .ok('Avoid using logical databases recommendation not displayed');\n        await t.expect(memoryEfficiencyPage.getRecommendationLabelByName(avoidLogicalDbRecommendation, 'code').exists)\n            .ok('Avoid using logical databases recommendation not have Code Changes label');\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n        // Go to Analysis Tools page and create new report and open recommendations\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n    })('Verify that user can upvote recommendations', async t => {\n        const notUsefulVoteOption = 'not useful';\n        const usefulVoteOption = 'useful';\n        await recommendationsActions.voteForRecommendation(redisVersionRecommendation, notUsefulVoteOption);\n        // Verify that user can rate recommendations with one of 2 existing types at the same time\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecommendation, notUsefulVoteOption);\n\n        // Verify that user can see the popup with link when he votes for “Not useful”\n        await recommendationsActions.verifyVotePopUpIsDisplayed(redisVersionRecommendation, notUsefulVoteOption);\n\n        // Verify that user can see previous votes when reload the page\n        await memoryEfficiencyPage.reloadPage();\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecommendation, notUsefulVoteOption);\n\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await recommendationsActions.voteForRecommendation(redisVersionRecommendation, usefulVoteOption);\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecommendation, usefulVoteOption);\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        keyName = `recomKey-${Common.generateWord(10)}`;\n        const jsonValue = '{\"name\":\"xyz\"}';\n        await browserPage.addJsonKey(keyName, jsonValue);\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName);\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The JSON key is not added');\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Go to Recommendations tab\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that user can see the Tutorial opened when clicking on \"Tutorial\" for recommendations', async t => {\n        const recommendation = memoryEfficiencyPage.getRecommendationByName(searchJsonRecommendation);\n        for (let i = 0; i < 5; i++) {\n            if (!(await recommendation.exists)) {\n                await t.click(memoryEfficiencyPage.newReportBtn);\n            }\n        }\n        // Verify that Optimize the use of time series recommendation displayed\n        await t.expect(recommendation.exists).ok('Query and search JSON documents recommendation not displayed');\n        // Verify that tutorial opened\n        await t.click(memoryEfficiencyPage.getToTutorialBtnByRecomName(searchJsonRecommendation));\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorial = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.expect(tutorial.preselectArea.visible).ok('Workbench Enablement area not opened');\n        // Verify that REDIS FOR TIME SERIES tutorial expanded\n        await t.expect(tutorial.getTutorialByName('INTRODUCTION').visible).ok('INTRODUCTION tutorial is not expanded');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/memory-efficiency/top-keys-table.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { Selector } from 'testcafe';\nimport { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { deleteAllKeysFromDB, populateDBWithHashes, populateHashWithFields } from '../../../../helpers/keys';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst chance = new Chance();\n\nconst keyToAddParameters = { keysCount: 13, keyNameStartWith: 'hashKey' };\nconst keyName = `TestHashKey-${ Common.generateWord(10) }`;\nconst keyToAddParameters2 = { fieldsCount: 80000, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' };\nconst members = [...Array(100).keys()].toString().replace(/,/g, ' '); // The smallest key\nconst keyNamesMemory = ['string', 'list', 'bloom', 'set'];\nconst keyNamesLength = ['string', 'set', 'list'];\nconst mbloomCommand = 'bf.add bloom 1';\nconst listCommand = `rpush list ${members.slice(0, -3)}`;\nconst stringCommand = `set string \"${chance.paragraph({ sentences: 100 })}\"`; // The biggest key\nconst setCommand = `sadd set ${members}`; // Middle key\n\nfixture(`Memory Efficiency Top Keys Table`).skip\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Create keys\n        await populateDBWithHashes(ossStandaloneConfig.host, ossStandaloneConfig.port, keyToAddParameters);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await deleteAllKeysFromDB(ossStandaloneConfig.host, ossStandaloneConfig.port);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Top Keys displaying in Summary of big keys', async t => {\n        // Verify that user can see “-” as length for all unsupported data types\n        await browserPage.Cli.sendCommandInCli(mbloomCommand);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await t.expect(Selector('[data-testid=length-empty-bloom]').textContent).eql('-', 'Length is defined for unknown types');\n        // Verify that user cannot see quantifier if keys are less than 15\n        await t.expect(memoryEfficiencyPage.topKeysTitle.textContent).eql('TOP KEYS', 'Title is not correct');\n        // Verify that top 15 keys are displayed per memory\n        await browserPage.Cli.sendCommandInCli(listCommand);\n        await browserPage.Cli.sendCommandInCli(stringCommand);\n        await browserPage.Cli.sendCommandInCli(setCommand);\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        await t.expect(memoryEfficiencyPage.topKeysTitle.textContent).eql('TOP 15 KEYS', 'Title is not correct');\n        await t.expect(memoryEfficiencyPage.topKeysKeyName.count).eql(15, 'Number of lines is not 15');\n        // Verify that sorting by Key Size DESC applied by default in by Memory tab\n        for (let i = 0; i < 4; i++) {\n            await t.expect(memoryEfficiencyPage.topKeysKeyName.nth(i).textContent).eql(keyNamesMemory[i], 'Key name by Memory order is not correct');\n        }\n        // Verify that top 15 keys are displayed per length\n        await t.click(memoryEfficiencyPage.sortByLength);\n        await t.expect(memoryEfficiencyPage.topKeysKeyName.count).eql(15, 'Number of lines is not 15');\n        // Verify that sorting by Length DESC applied by default in by Length tab\n        for (let i = 0; i < 3; i++) {\n            await t.expect(memoryEfficiencyPage.topKeysKeyName.nth(i).textContent).eql(keyNamesLength[i], 'Key name by Length order is not correct');\n        }\n        // Verify that user can click on a key name and see key details in Browser\n        await t.click(memoryEfficiencyPage.topKeysKeyName.nth(1).find('button'));\n        await t.expect(browserPage.keyNameFormDetails.find('b').textContent).eql(keyNamesLength[1]);\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Create keys\n        await populateHashWithFields(ossStandaloneConfig.host, ossStandaloneConfig.port, keyToAddParameters2);\n        // Go to Analysis Tools page\n        await t.click(browserPage.NavigationTabs.analysisButton);\n    })\n    .after(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Big highlighted key tooltip', async t => {\n        const tooltipText = 'Consider splitting it into multiple keys';\n\n        await t.click(memoryEfficiencyPage.newReportBtn);\n        // Tooltip with text \"Consider splitting it into multiple keys\" is displayed for highlighted keys\n        await t.hover(memoryEfficiencyPage.topKeysKeySizeCell);\n        await t.expect(browserPage.tooltip.textContent).contains(tooltipText, `\"${tooltipText}\" is not displayed in Key size tooltip`);\n        await t.hover(memoryEfficiencyPage.topKeysLengthCell);\n        await t.expect(browserPage.tooltip.textContent).contains(tooltipText, `\"${tooltipText}\" is not displayed in Length tooltip`);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    WorkbenchPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst keyName = `${Common.generateWord(20)}-key`;\nconst keyValue = `${Common.generateWord(10)}-value`;\n\nfixture `Monitor`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can work with Monitor', async t => {\n    const command = 'set';\n    //Verify that user can open Monitor\n    await t.click(browserPage.Profiler.expandMonitor);\n    //Check that monitor is opened\n    await t.expect(browserPage.Profiler.monitorArea.exists).ok('Profiler area');\n    await t.expect(browserPage.Profiler.startMonitorButton.exists).ok('Start profiler button');\n    //Verify that user can see message inside Monitor \"Running Monitor will decrease throughput, avoid running it in production databases.\" when opens it for the first time\n    await t.expect(browserPage.Profiler.monitorWarningMessage.exists).ok('Profiler warning message');\n    await t.expect(browserPage.Profiler.monitorWarningMessage.withText('Running Profiler will decrease throughput, avoid running it in production databases.').exists).ok('Profiler warning message is not correct');\n    //Verify that user can run Monitor by clicking \"Run\" command in the message inside Monitor\n    await t.click(browserPage.Profiler.startMonitorButton);\n    await t.expect(browserPage.Profiler.monitorIsStartedText.innerText).eql('Profiler is started.');\n    //Verify that user can see run commands in monitor\n    await browserPage.Cli.getSuccessCommandResultFromCli(`${command} ${keyName} ${keyValue}`);\n    await browserPage.Profiler.checkCommandInMonitorResults(command, [keyName, keyValue]);\n});\ntest.skip('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => {\n    //Define commands in different clients\n    const cli_command = 'command';\n    const workbench_command = 'hello';\n    const common_command = 'info';\n    const browser_command = 'hset';\n    //Start Monitor\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    //Send command in CLI\n    await browserPage.Cli.getSuccessCommandResultFromCli(cli_command);\n    //Check that command from CLI is displayed in monitor\n    await browserPage.Profiler.checkCommandInMonitorResults(cli_command);\n    //Refresh the page to send command from Browser client\n    await t.click(browserPage.refreshKeysButton);\n    //Check the command from browser client\n    await browserPage.addHashKey(keyName);\n    await browserPage.Profiler.checkCommandInMonitorResults(browser_command);\n    //Open Workbench page to create new client\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    //Send command in Workbench\n    await workbenchPage.sendCommandInWorkbench(workbench_command);\n    //Check that command from Workbench is displayed in monitor\n    await workbenchPage.Profiler.checkCommandInMonitorResults(workbench_command);\n    //Check the command from common client\n    await workbenchPage.Profiler.checkCommandInMonitorResults(common_command);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/monitor/save-commands.e2e.ts",
    "content": "import * as fs from 'fs';\nimport * as os from 'os';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    fileDownloadPath,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { DatabasesActions } from '../../../../common-actions/databases-actions';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst databasesActions = new DatabasesActions();\n\nconst tempDir = os.tmpdir();\nconst fileStarts = 'test_standalone';\n\nfixture `Save commands`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can see a tooltip and toggle that allows to save Profiler log or not in the Profiler', async t => {\n    const toolTip = [\n        'Allows you to download the generated log file after pausing the Profiler',\n        'Profiler log is saved to a file on your local machine with no size limitation. The temporary log file will be automatically rewritten when the Profiler is reset.'\n    ];\n\n    await t.click(browserPage.Profiler.expandMonitor);\n    // Check the toggle and Tooltip for Save log\n    await t.expect(browserPage.Profiler.saveLogSwitchButton.exists).ok('The toggle that allows to save Profiler log is not displayed');\n    await t.hover(browserPage.Profiler.saveLogSwitchButton);\n    for (const message of toolTip) {\n        await t.expect(browserPage.Profiler.saveLogToolTip.innerText).contains(message, 'The toolTip for save log in Profiler is not displayed');\n    }\n    // Check toggle state\n    await t.expect(browserPage.Profiler.saveLogSwitchButton.getAttribute('aria-checked')).eql('false', 'The toggle state is not OFF when Profiler opened');\n});\ntest('Verify that user can see that toggle is not displayed when Profiler is started', async t => {\n    // Start Monitor without save logs\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    // Check the toggle\n    await t.expect(browserPage.Profiler.saveLogSwitchButton.exists).notOk('The toggle is displayed when Profiler is started');\n    // Restart Monitor with Save logs\n    await browserPage.Profiler.stopMonitor();\n    await t.click(browserPage.Profiler.resetProfilerButton);\n    await t.click(browserPage.Profiler.saveLogSwitchButton);\n    await t.click(browserPage.Profiler.startMonitorButton);\n    // Check the toggle\n    await t.expect(browserPage.Profiler.saveLogSwitchButton.exists).notOk('The toggle is displayed when Profiler is started');\n});\ntest('Verify that when user switch toggle to ON and started the Profiler, temporary Log file Created and recording', async t => {\n    const cli_command = 'command';\n    // Remember the number of files in Temp\n    const numberOfTempFiles = fs.readdirSync(tempDir).length;\n\n    // Start Monitor with Save logs\n    await browserPage.Profiler.startMonitorWithSaveLog();\n    // Send command in CLI\n    await browserPage.Cli.getSuccessCommandResultFromCli(cli_command);\n    await browserPage.Profiler.checkCommandInMonitorResults(cli_command);\n    // Verify that temporary Log file Created\n    await t.expect(numberOfTempFiles).lt(fs.readdirSync(tempDir).length, 'The temporary Log file is not created');\n});\ntest('Verify that when user switch toggle to OFF and started the Profiler, temporary Log file is not Created and recording', async t => {\n    // Remember the number of files in Temp\n    const numberOfTempFiles = fs.readdirSync(tempDir).length;\n\n    // Start Monitor without Save logs\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    // Verify that temporary Log file is not created\n    await t.expect(numberOfTempFiles).gte(fs.readdirSync(tempDir).length, 'The temporary Log file is created');\n});\ntest('Verify the Profiler Button panel when toggle was switched to ON and user pauses/resumes the Profiler', async t => {\n    // Start Monitor with Save logs\n    await browserPage.Profiler.startMonitorWithSaveLog();\n    // Pause the Profiler\n    await t.click(browserPage.Profiler.runMonitorToggle);\n    // Check the panel\n    await t.expect(browserPage.Profiler.downloadLogPanel.exists).ok('The download log panel not appeared');\n    await t.expect(browserPage.Profiler.resetProfilerButton.exists).ok('The Reset Profiler button not visible');\n    await t.expect(browserPage.Profiler.downloadLogButton.exists).ok('The Download button not visible');\n});\ntest('Verify that when user see the toggle is OFF - Profiler logs are not being saved', async t => {\n    // Remember the number of files in Temp\n    const numberOfDownloadFiles = await databasesActions.getFileCount(fileDownloadPath, fileStarts);\n\n    // Start Monitor without Save logs\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    await t.wait(3000);\n    // Check the download files\n    await t.expect(await databasesActions.getFileCount(fileDownloadPath, fileStarts)).eql(numberOfDownloadFiles, 'The Profiler logs are saved');\n});\ntest('Verify that when user see the toggle is ON - Profiler logs are being saved', async t => {\n    // Remember the number of files in Temp\n    const numberOfDownloadFiles = await databasesActions.getFileCount(fileDownloadPath, fileStarts);\n\n    // Start Monitor with Save logs\n    await browserPage.Profiler.startMonitorWithSaveLog();\n    // Download logs and check result\n    await browserPage.Profiler.stopMonitor();\n    await t.click(browserPage.Profiler.downloadLogButton);\n    await t.expect(await databasesActions.getFileCount(fileDownloadPath, fileStarts)).gt(numberOfDownloadFiles, 'The Profiler logs not saved', { timeout: 5000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/notifications/notification-center.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { deleteAllNotificationsFromDB } from '../../../../helpers/notifications';\nimport { commonUrl } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage, SettingsPage } from '../../../../pageObjects';\nimport { NotificationParameters } from '../../../../pageObjects/components/navigation/notification-panel';\n\nconst description = require('./notifications.json');\nconst jsonNotifications: NotificationParameters[] = description.notifications;\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst NotificationPanel = myRedisDatabasePage.NavigationPanel.NotificationPanel;\n\n// Sort all notifications in json file\nconst sortedNotifications = jsonNotifications.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);\n\n// todo: rework. flaky tests.\nfixture.skip `Notifications`\n    .meta({ rte: rte.none, type: 'critical_path' })\n    .page(commonUrl)\n    .beforeEach(async(t) => {\n        await t.setTestSpeed(.9);\n        await databaseHelper.acceptLicenseTerms();\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await settingsPage.changeNotificationsSwitcher(true);\n        await deleteAllNotificationsFromDB();\n        await myRedisDatabasePage.reloadPage();\n    });\ntest('Verify that when manager publishes new notification, it appears in the app', async t => {\n    // Get number of notifications in the badge\n    const newMessagesBeforeClosing = await NotificationPanel.getUnreadNotificationNumber();\n    // Verify that user can see new notification in left popup\n    // Verify that user can change notification toggle to on and new popup message will be displayed\n    await t.expect(NotificationPanel.notificationPopup.visible)\n        .ok('Notification popup is displayed', { timeout: 4000 });\n    // Verify that new notification contains title, body and date\n    await t.expect(NotificationPanel.notificationTitle.visible).ok('Title in popup is displayed');\n    await t.expect(NotificationPanel.notificationBody.visible).ok('Body in popup is displayed');\n    await t.expect(NotificationPanel.notificationDate.visible).ok('Date in popup is displayed');\n    // Verify that user can see notification with category badge and category color in a single notification\n    await t.expect(NotificationPanel.notificationCategory.visible).ok('Category is not displayed in popup');\n\n    if (sortedNotifications[0].category !== undefined) {\n        await t.expect(NotificationPanel.notificationCategory.innerText)\n            .eql(sortedNotifications[0].category ?? '', 'Text for category is not correct');\n        await t.expect(NotificationPanel.notificationCategory\n            .withExactText(sortedNotifications[0].category ?? '').withAttribute('style', `background-color: rgb${sortedNotifications[0].rbgColor};`).exists).ok('Category color');\n    }\n    // Verify that user can click on close button and received notification will be closed\n    await t.click(NotificationPanel.closeNotificationPopup);\n    await t.expect(NotificationPanel.notificationPopup.visible).notOk('Notification popup is not displayed');\n    // Verify that when user closes new notification popup, number of unread messages decreased in badge\n    if (await NotificationPanel.notificationBadge.exists) {\n        const newMessagesAfterClosing = await NotificationPanel.getUnreadNotificationNumber();\n        await t.expect(newMessagesBeforeClosing).eql(newMessagesAfterClosing + 1, 'Reduced number of unread messages');\n    }\n});\ntest('Verify that user can see message \"No notifications to display.\" when no messages received', async t => {\n    await t.click(NotificationPanel.notificationCenterButton);\n    await t.expect(NotificationPanel.notificationCenterPanel.child().withExactText('Notification Center').visible).ok('Panel is opened');\n    await t.expect(NotificationPanel.emptyNotificationMessage.child().withExactText('No notifications to display.').visible).ok('Empty message is displayed');\n    // Verify that user can close notification center by clicking on any other area of the application\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await t.expect(NotificationPanel.notificationCenterPanel.visible).notOk('Panel is not displayed');\n    // Wait for new notification\n    await t.expect(NotificationPanel.notificationPopup.exists).ok('New notifications appear', { timeout: 35000 });\n    // Verify that user can open notification center by clicking on new received message popup\n    await t.click(NotificationPanel.notificationPopup);\n    await t.expect(NotificationPanel.notificationCenterPanel.visible).ok('Notification center is opened');\n});\ntest('Verify that user can open notification center by clicking on icon and see all received messages', async t => {\n    // Wait for new notifications\n    await t.expect(NotificationPanel.notificationBadge.exists).ok('New notifications appear', { timeout: 35000 });\n    // Verify that user can see number of new notifications on notification center icon\n    await t.expect(await NotificationPanel.getUnreadNotificationNumber())\n        .eql(jsonNotifications.length, 'Number of unread messages in badge');\n    // Verify that badge with number of unread notifications disappears when user opens notification center\n    await t.click(NotificationPanel.notificationCenterButton);\n    await t.expect(NotificationPanel.notificationBadge.exists).notOk('Badge disappears');\n    // Verify that user can see unread messages by blue icon highlighting in notification center\n    await t.expect(NotificationPanel.unreadNotification.count).eql(jsonNotifications.length, 'Highlighted messages');\n    // Verify that user can see title, body, and received date of the message in notification center\n    for (let i = 0; i < jsonNotifications.length; i++) {\n        // Check date of the message\n        await t.expect(NotificationPanel.notificationTitle.withExactText(jsonNotifications[i].title).exists).ok('Displayed title');\n        await t.expect(NotificationPanel.notificationBody.withExactText(jsonNotifications[i].body).exists).ok('Displayed body');\n        await t.expect(NotificationPanel.notificationDate.withExactText(await NotificationPanel.convertEpochDateToMessageDate(jsonNotifications[i])).exists).ok('Displayed date');\n        // Verify that user can see notification with category badge and category color in the notification center\n        if (jsonNotifications[i].category !== undefined) {\n            await t.expect(NotificationPanel.notificationCategory.withExactText(jsonNotifications[i].category ?? '').exists).ok(`${jsonNotifications[i].category} category name not displayed`);\n            await t.expect(NotificationPanel.notificationCategory.withExactText(jsonNotifications[i].category ?? '').withAttribute('style', `background-color: rgb${jsonNotifications[i].rbgColor}; color: rgb(0, 0, 0);`).exists).ok('Category color');\n        }\n    }\n    // Verify that as soon as user closes notification center, unread messages become read\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); // Close notification center\n    await t.expect(NotificationPanel.notificationBadge.exists).notOk('No unread messages badge');\n    // Verify that next time when users open notification center, highlighting is not displayed for those messages that were unread previous center opening\n    await t.click(NotificationPanel.notificationCenterButton); // Open notification center again\n    await t.expect(NotificationPanel.unreadNotification.exists).notOk('No unread notifications');\n});\ntest('Verify that all messages in notification center are sorted by timestamp from newest to oldest', async t => {\n    // Wait for new notifications\n    await t.expect(NotificationPanel.notificationBadge.exists).ok('New notifications appear', { timeout: 35000 });\n    await t.click(NotificationPanel.notificationCenterButton);\n    for (let i = 0; i < sortedNotifications.length; i++) {\n        // Get data one by one from notification center\n        const title = await NotificationPanel.notificationList.child(i).find(NotificationPanel.cssNotificationTitle).textContent;\n        const body = await NotificationPanel.notificationList.child(i).find(NotificationPanel.cssNotificationBody).textContent;\n        const date = await NotificationPanel.notificationList.child(i).find(NotificationPanel.cssNotificationDate).textContent;\n        // Compare with what contained in sorted set\n        await t.expect(title).eql(sortedNotifications[i].title, 'Title corresponds to sorted notification');\n        await t.expect(body).eql(sortedNotifications[i].body, 'Body corresponds to sorted notification');\n        await t.expect(date).eql(await NotificationPanel.convertEpochDateToMessageDate(sortedNotifications[i]), 'Date corresponds to sorted notification');\n    }\n});\ntest\n    .before(async t => {\n        await t.setTestSpeed(.9);\n        await databaseHelper.acceptLicenseTerms();\n        await settingsPage.changeNotificationsSwitcher(false);\n        await deleteAllNotificationsFromDB();\n        await myRedisDatabasePage.reloadPage();\n        await t.expect(NotificationPanel.notificationBadge.exists).notOk('No badge');\n    })('Verify that new popup message is not displayed when notifications are turned off', async t => {\n        // Verify that user can see notification badge increased when new messages is sent and notifications are turned off\n        await t.expect(NotificationPanel.notificationBadge.exists).ok('New notifications appear', { timeout: 35000 });\n        await t.expect(NotificationPanel.notificationPopup.exists).notOk('Popup is not displayed');\n        // Verify that new messages is displayed only in notification center if notifications are turned off\n        await t.click(NotificationPanel.notificationCenterButton);\n        await t.expect(NotificationPanel.unreadNotification.count).eql(jsonNotifications.length, 'Unread notifications number');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/notifications/notifications.json",
    "content": "{\n  \"notifications\": [\n    {\n      \"title\": \"Lorem ipsum dolor sit amet\",\n      \"body\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque id augue ligula. Nulla facilisi. In hac habitasse platea dictumst. Maecenas gravida interdum velit ut consectetur. Vestibulum urna tellus, bibendum vitae massa ut, aliquet fringilla quam. Quisque commodo, neque eu condimentum dictum, turpis purus efficitur quam, in faucibus augue augue sed felis. Donec et faucibus nisl, sit amet tincidunt neque. In a enim nec odio condimentum sodales. Integer sed ligula eget neque venenatis commodo ut quis mauris. Phasellus urna augue, auctor ac nisl vel, fermentum pellentesque libero. Suspendisse tellus mauris, ultricies sed rhoncus vitae, suscipit quis odio. Nullam pellentesque enim eros, et lacinia risus placerat in. Aliquam et lorem ante. Proin tempor volutpat augue, in tincidunt magna viverra id.\",\n      \"timestamp\": 1659438264,\n\t    \"category\": \"Habitasse platea dictumst\",\n      \"categoryColor\": \"#a00a6b\",\n      \"rbgColor\": \"(160, 10, 107)\"\n    },\n    {\n      \"title\": \"Sed a elit quis sem fringilla imperdiet cras malesuada justo sed urna fringilla lobortis\",\n      \"body\": \"Aenean rhoncus massa ac vehicula scelerisque. Ut in lorem id eros commodo ultrices. Nunc consequat non augue vitae pharetra. Vivamus ut urna vel lacus accumsan pharetra. Aliquam rhoncus nibh ut elementum laoreet. Cras ut risus tortor. Aenean lacinia blandit felis, et tempor tellus vulputate ut. Maecenas in nibh a eros posuere cursus in ac ligula. Suspendisse iaculis congue risus, eget tristique quam porta id. In consequat, ligula in interdum laoreet, lacus leo tempus turpis, vitae mollis tellus lectus a felis. Sed accumsan ornare lorem, vel facilisis purus commodo eget. Vestibulum mattis blandit orci et molestie. Ut molestie ante eget eros gravida dictum. Maecenas eleifend eget dui sed imperdiet. In dapibus lectus et venenatis malesuada. Vivamus ut nunc neque.\",\n      \"timestamp\": 1658935023,\n\t    \"category\": \"Maecenas nec ultrices\",\n      \"rbgColor\": \"(102, 102, 102)\"\n    },\n    {\n      \"title\": \"In vel ultricies justo sed at mi id nisl lacinia vehicula\",\n      \"body\": \"In vel ultricies justo. Sed at mi id nisl lacinia vehicula ut eget orci. In hac habitasse platea dictumst. Ut vel elementum justo. Nulla finibus convallis felis. Aenean dictum interdum lorem, non placerat risus egestas vel. Nullam sit amet dui a mauris eleifend pharetra mattis vitae nisl. Sed semper justo id arcu suscipit, et pretium felis ornare. Nulla in auctor eros, vel pellentesque dui. Phasellus ut laoreet ipsum, ac aliquet erat\",\n      \"timestamp\": 1359438264\n    },\n    {\n      \"title\": \"In a enim nec odio condimentum sodales. Integer sed ligula eget neque venenatis commodo ut quis mauris\",\n      \"body\": \"In vel ultricies justo. Sed at mi id nisl lacinia vehicula ut eget orci. In hac habitasse platea dictumst. Ut vel elementum justo. Nulla finibus convallis felis. Aenean dictum interdum lorem, non placerat risus egestas vel. Nullam sit amet dui a mauris eleifend pharetra mattis vitae nisl. Sed semper justo id arcu suscipit, et pretium felis ornare. Nulla in auctor eros, vel pellentesque dui. Phasellus ut laoreet ipsum, ac aliquet erat\",\n      \"timestamp\": 1557538664,\n\t    \"category\": \"Condimentum\",\n      \"categoryColor\": \"#008556\",\n      \"rbgColor\": \"(0, 133, 86)\"\n    },\n    {\n      \"title\": \"In vel ultricies justo sed at mi id nisl lacinia vehicula\",\n      \"body\": \"In vel ultricies justo. Sed at mi id nisl lacinia vehicula ut eget orci. In hac habitasse platea dictumst. Ut vel elementum justo. Nulla finibus convallis felis. Aenean dictum interdum lorem, non placerat risus egestas vel. Nullam sit amet dui a mauris eleifend pharetra mattis vitae nisl. Sed semper justo id arcu suscipit, et pretium felis ornare. Nulla in auctor eros, vel pellentesque dui. Phasellus ut laoreet ipsum, ac aliquet erat\",\n      \"timestamp\": 1669449262,\n\t    \"category\": \"Pellentesque\",\n      \"categoryColor\": \"#364cff\",\n      \"rbgColor\": \"(54, 76, 255)\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, PubSubPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Telemetry } from '../../../../helpers';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst pubSubPage = new PubSubPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\nconst telemetry = new Telemetry();\n\nconst logger = telemetry.createLogger();\n\nconst telemetryEvent = 'PUBSUB_MESSAGES_CLEARED';\nconst expectedProperties = [\n    'databaseId',\n    'messages',\n    'provider'\n];\n\nfixture `Subscribe/Unsubscribe from a channel`\n    .meta({ rte: rte.standalone, type: 'critical_path' })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        //Go to PubSub page\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n    });\ntest('Verify that when user subscribe to the pubsub channel he can see all the messages being published to my database from the moment of my subscription', async t => {\n    // Verify that the Channel field placeholder is 'Enter Channel Name'\n    await t.expect(pubSubPage.channelNameInput.getAttribute('placeholder')).eql('Enter Channel Name', 'No placeholder in Channel field');\n    // Verify that the Message field placeholder is 'Enter Message'\n    await t.expect(pubSubPage.messageInput.getAttribute('placeholder')).eql('Enter Message', 'No placeholder in Message field');\n    // Verify that user is unsubscribed from the pubsub channel when he go to the pubsub window after launching application for the first time\n    await t.expect(pubSubPage.initialPage.textContent).contains('You are not subscribed', 'User is not unsubscribed');\n    await t.expect(pubSubPage.subscribeButton.exists).eql(true, 'Subscribe button is not displayed');\n\n    // Subscribe to channel\n    await t.click(pubSubPage.subscribeButton);\n    await t.expect(pubSubPage.subscribeStatus.textContent).contains('Subscribed', 'User is not subscribed', { timeout: 10000 });\n    // Verify that user can publish a message to a channel\n    await pubSubPage.publishMessage('test', 'published message');\n    await verifyMessageDisplayingInPubSub('published message', true);\n    await t.click(pubSubPage.unsubscribeButton);\n    //Verify that when user unsubscribe from a pubsub channel he can see no new data being published to the channel from the moment he unsubscribe\n    await t.expect(pubSubPage.subscribeStatus.textContent).contains('Unsubscribed', 'User is not unsubscribed', { timeout: 10000 });\n    //Verify that user can publish a message regardless of my subscription state.\n    await pubSubPage.publishMessage('test', 'message in unsubscribed status');\n    //Verify that message is not displayed\n    await verifyMessageDisplayingInPubSub('message in unsubscribed status', false);\n});\ntest('Verify that the focus gets always shifted to a newest message (auto-scroll)', async t => {\n    await pubSubPage.subsribeToChannelAndPublishMessage('test', 'first message');\n    // Verify that when user click Publish and the publication is successful, he can see a response: badge with the number <# of clients received>\n    await t.expect(pubSubPage.publishResult.exists).ok('Publish results is not displayed');\n    await t.expect(pubSubPage.publishResult.textContent).contains('Published (1)', 'Publish result is not displayed', { timeout: 10000 });\n\n    // Go to Redis Databases Page\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneConfig.databaseName);\n    // Go back to PubSub page\n    await t.click(browserPage.NavigationTabs.pubSubButton);\n    // Verify that my subscription state is preserved when user navigate through the app while connected to current database and in current app session\n    await t.expect(pubSubPage.subscribeStatus.textContent).contains('Subscribed', 'User is not subscribed', { timeout: 10000 });\n\n    // Publish 100 messages\n    await pubSubPage.Cli.sendCommandInCli('100 publish channel test100Message');\n    // Verify that the first message is not visible in view port\n    await verifyMessageDisplayingInPubSub('first message', false);\n    await verifyMessageDisplayingInPubSub('test100Message', true);\n});\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n        await t.click(pubSubPage.NavigationPanel.myRedisDBButton);\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to PubSub page\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n    })('Verify that user subscription state is changed to unsubscribed, all the messages are cleared and total message counter is reset when user connect to another database', async t => {\n        await t.click(pubSubPage.subscribeButton);\n        // Publish 10 messages\n        await pubSubPage.Cli.sendCommandInCli('10 publish channel message');\n        await verifyMessageDisplayingInPubSub('message', true);\n        // Verify that user can see total number of messages received\n        await t.expect(pubSubPage.totalMessagesCount.textContent).contains('10', 'Total counter value is incorrect');\n        // Connect to second database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n        // Verify no subscription, messages and total messages\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n        await t.expect(pubSubPage.initialPage.textContent).contains('You are not subscribed', 'User is not unsubscribed');\n        await verifyMessageDisplayingInPubSub('message', false);\n        await t.expect(pubSubPage.totalMessagesCount.exists).notOk('Total counter is still displayed');\n    });\ntest.skip('Verify that user can see a internal link to pubsub window under word “Pub/Sub” when he tries to run PSUBSCRIBE or SUBSCRIBE commands in CLI or Workbench', async t => {\n    const commandFirst = 'PSUBSCRIBE';\n    const commandSecond = 'SUBSCRIBE';\n\n    // Go to Browser Page\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Verify that user can see a custom message when he tries to run PSUBSCRIBE command in CLI or Workbench: “Use Pub/Sub to see the messages published to all channels in your database”\n    await pubSubPage.Cli.sendCommandInCli(commandFirst);\n    await t.click(pubSubPage.Cli.cliExpandButton);\n    await t.expect(await pubSubPage.Cli.getWarningMessageText(commandFirst)).eql('Use Pub/Sub to see the messages published to all channels in your database.', 'Message is not displayed', { timeout: 10000 });\n\n    // Verify internal link to pubsub page in CLI\n    await t.expect(pubSubPage.Cli.cliLinkToPubSub.exists).ok('Link to pubsub page is not displayed');\n    await t.click(pubSubPage.Cli.cliLinkToPubSub);\n    await t.expect(pubSubPage.pubSubPageContainer.exists).ok('Pubsub page is opened');\n\n    // Verify that user can see a custom message when he tries to run SUBSCRIBE command in CLI: “Use Pub/Sub tool to subscribe to channels.”\n    await t.click(pubSubPage.Cli.cliCollapseButton);\n    await pubSubPage.Cli.sendCommandInCli(commandSecond);\n    await t.click(pubSubPage.Cli.cliExpandButton);\n    await t.expect(await pubSubPage.Cli.getWarningMessageText(commandSecond)).eql('Use Pub/Sub tool to subscribe to channels.', 'Message is not displayed', { timeout: 10000 });\n\n    // Verify internal link to pubsub page in CLI\n    await t.expect(pubSubPage.Cli.cliLinkToPubSub.exists).ok('Link to pubsub page is not displayed');\n    await t.click(pubSubPage.Cli.cliLinkToPubSub);\n    await t.expect(pubSubPage.pubSubPageContainer.exists).ok('Pubsub page is opened');\n\n    // Verify that user can see a custom message when he tries to run SUBSCRIBE command in Workbench: “Use Pub/Sub tool to subscribe to channels.”\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench(commandSecond);\n    await t.expect(await workbenchPage.queryResult.textContent).eql('Use Pub/Sub tool to subscribe to channels.', 'Message is not displayed', { timeout: 10000 });\n\n});\ntest('Verify that the Message field input is preserved until user Publish a message', async t => {\n    // Fill in Channel and Message inputs\n    await t.click(pubSubPage.subscribeButton);\n    await t.click(pubSubPage.channelNameInput);\n    await t.typeText(pubSubPage.channelNameInput, 'testChannel', { replace: true });\n    await t.click(pubSubPage.messageInput);\n    await t.typeText(pubSubPage.messageInput, 'message', { replace: true });\n    await t.expect(pubSubPage.messageInput.value).eql('message', 'Message input is empty', { timeout: 10000 });\n    // Go to Browser Page\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneConfig.databaseName);\n    // Go to PubSub page\n    await t.click(browserPage.NavigationTabs.pubSubButton);\n    // Verify that message is preserved until publishing\n    await t.expect(pubSubPage.messageInput.value).eql('message', 'Message input is empty', { timeout: 10000 });\n    await t.click(pubSubPage.publishButton);\n    await t.expect(pubSubPage.messageInput.value).eql('', 'Message input is empty', { timeout: 10000 });\n    // Verify that the Channel field input is preserved until user modify it (publishing a message does not clear the field)\n    await t.expect(pubSubPage.channelNameInput.value).eql('testChannel', 'Channel input is empty', { timeout: 10000 });\n});\n// todo: fix after \"clear\" button will be added\ntest.skip.requestHooks(logger)('Verify that user can clear all the messages from the pubsub window', async t => {\n    await pubSubPage.subsribeToChannelAndPublishMessage('testChannel', 'message');\n    await pubSubPage.publishMessage('testChannel2', 'second m');\n    // Verify the tooltip text 'Clear Messages' appears on hover the clear button\n    await t.hover(pubSubPage.clearPubSubButton);\n    await t.expect(pubSubPage.clearButtonTooltip.textContent).contains('Clear Messages', 'Clear Messages tooltip not displayed');\n    await t.click(pubSubPage.clearPubSubButton);\n\n    //Verify telemetry event\n    await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n\n    // Verify that the clear of the messages does not affect the subscription state\n    await t.expect(pubSubPage.subscribeStatus.textContent).eql('You are  subscribed', 'User is not subscribed', { timeout: 10000 });\n    // Verify that the Messages counter is reset after clear messages\n    await t.expect(pubSubPage.totalMessagesCount.textContent).contains('0', 'Total counter value is incorrect');\n    // Verify messages are cleared\n    await verifyMessageDisplayingInPubSub('message', false);\n    await verifyMessageDisplayingInPubSub('second m', false);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/settings/settings.e2e.ts",
    "content": "import { MyRedisDatabasePage, SettingsPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl } from '../../../../helpers/conf';\nimport { Common, Telemetry } from '../../../../helpers';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst telemetry = new Telemetry();\n\nconst logger = telemetry.createLogger();\n\nconst telemetryEvent = 'SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED';\n\nconst expectedProperties = [\n    'currentValue',\n    'newValue'\n];\n\nconst explicitErrorHandler = (): void => {\n    window.addEventListener('error', e => {\n        if(e.message === 'ResizeObserver loop limit exceeded') {\n            e.stopImmediatePropagation();\n        }\n    });\n};\n\nfixture `Settings`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .page(commonUrl)\n    .clientScripts({ content: `(${explicitErrorHandler.toString()})()` })\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest\n    .after(async() => {\n        await settingsPage.changeKeysToScanValue('10000');\n    })('Verify that user can customize a number of keys to scan in filters per key name or key type', async t => {\n    // Go to Settings page\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        // Change keys to Scan\n        await t.click(settingsPage.accordionAdvancedSettings);\n        await settingsPage.changeKeysToScanValue('1500');\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n        // Check that value was set\n        await t.click(settingsPage.accordionAdvancedSettings);\n        await t.expect(settingsPage.keysToScanValue.textContent).eql('1500', 'Keys to Scan has proper value');\n    });\ntest('Verify that user can turn on/off Analytics in Settings in the application', async t => {\n    // Go to Settings page\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    await t.click(settingsPage.accordionPrivacySettings);\n\n    const currentValue = await settingsPage.getAnalyticsSwitcherValue();\n    // We sort the values so as not to be tied to the current setting\n    const equalValues = [true, false].sort((_, b) => b === currentValue ? -1 : 0);\n\n    for (const value of equalValues) {\n        await t.click(settingsPage.switchAnalyticsOption);\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n        await t.click(settingsPage.accordionPrivacySettings);\n        await t.expect(await settingsPage.getAnalyticsSwitcherValue()).eql(value, 'Analytics was switched properly');\n        // Verify that telemetry is turned off\n        if(value === false){\n            await t.click(settingsPage.accordionWorkbenchSettings);\n            //turn on and turn off option\n            await t.click(settingsPage.switchEditorCleanupOption);\n            await t.click(settingsPage.switchEditorCleanupOption);\n\n            try {\n                await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n                await t.expect(true).eql(false, 'telemetry is sent when analytics is disabled');\n            } catch (error) {\n                await t.expect(true).eql(true, 'telemetry is not sent when analytics is disabled');\n            }\n        }\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/slow-log/slow-log.e2e.ts",
    "content": "import { SlowLogPage, MyRedisDatabasePage, BrowserPage, ClusterDetailsPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Telemetry } from '../../../../helpers';\n\nconst slowLogPage = new SlowLogPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst overviewPage = new ClusterDetailsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst telemetry = new Telemetry();\n\nconst logger = telemetry.createLogger();\n\nconst telemetryEvents = ['SLOWLOG_CLEARED','SLOWLOG_LOADED'];\nconst clearExpectedProperties = [\n    'databaseId',\n    'provider'\n];\n\nconst loadExpectedProperties = [\n    'databaseId',\n    'numberOfCommands',\n    'provider'\n];\n\nconst slowerThanParameter = 1;\nlet maxCommandLength = 50;\nlet command = `slowlog get ${maxCommandLength}`;\n\nfixture `Slow Log`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        await t.click(browserPage.NavigationTabs.analysisButton);\n        await t.click(slowLogPage.slowLogTab);\n    })\n    .afterEach(async() => {\n        await slowLogPage.resetToDefaultConfig();\n    });\ntest('Verify that user can open new Slow Log page using new icon on left app panel', async t => {\n    // Verify that user see \"Slow Log\" page by default for non OSS Cluster\n    await t.expect(overviewPage.overviewTab.withAttribute('aria-selected', 'true').exists).notOk('The Overview tab is displayed for non OSS Cluster db');\n    // Verify that user can configure slowlog-max-len for Slow Log and see whole set of commands according to the setting\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.Cli.sendCommandInCli(command);\n    await t.click(slowLogPage.slowLogRefreshButton);\n    const duration = await slowLogPage.slowLogCommandValue.withExactText(command).parent(3).find(slowLogPage.cssSelectorDurationValue).textContent;\n    await t.expect(parseInt(duration)).gte(slowerThanParameter, 'Displayed command time execution is more than specified');\n    // Verify that user can see 3 columns with timestamp, duration and command in Slow Log\n    await t.expect(slowLogPage.slowLogTimestampValue.exists).ok('Timestamp column');\n    await t.expect(slowLogPage.slowLogDurationValue.exists).ok('Duration column');\n    await t.expect(slowLogPage.slowLogCommandValue.exists).ok('Command column');\n});\ntest('Verify that user can see \"No Slow Logs found\" message when slowlog-max-len=0', async t => {\n    // Set slowlog-max-len=0\n    maxCommandLength = 0;\n    await slowLogPage.changeMaxLengthParameter(maxCommandLength);\n    await t.click(slowLogPage.slowLogRefreshButton);\n    // Check that no records are displayed in SlowLog table\n    await t.expect(slowLogPage.slowLogEmptyResult.exists).ok('Empty results not found');\n    // Verify that user can see not more that number of commands that was specified in configuration\n    maxCommandLength = 30;\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.changeMaxLengthParameter(maxCommandLength);\n    // Go to Browser page to scan keys and turn back\n    await t.click(browserPage.NavigationTabs.browserButton);\n    await t.click(browserPage.refreshKeysButton);\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(slowLogPage.slowLogTab);\n    // Compare number of logged commands with maxLength\n    await t.expect(slowLogPage.slowLogCommandStatistics.withText(`${maxCommandLength} entries`).exists).ok(`Number of displayed commands is less than selected ${maxCommandLength}`);\n});\ntest('Verify that users can specify number of commands that they want to display (10, 25, 50, 100, Max) in Slow Log', async t => {\n    maxCommandLength = 128;\n    const numberOfCommandsArray = ['10', '25', '50', '100', 'Max available'];\n    // Change slower-than parameter\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.changeMaxLengthParameter(maxCommandLength);\n    // Go to Browser page to scan keys and turn back\n    await t.click(browserPage.NavigationTabs.browserButton);\n    await t.click(browserPage.refreshKeysButton);\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(slowLogPage.slowLogTab);\n    for (let i = 0; i < numberOfCommandsArray.length; i++) {\n        await slowLogPage.changeDisplayUpToParameter(numberOfCommandsArray[i]);\n        if (i === numberOfCommandsArray.length - 1) {\n            await t.expect(slowLogPage.slowLogCommandStatistics.withText(`${maxCommandLength} entries`).exists).ok('Number of displayed commands is not equal to 128');\n        }\n        else {\n            await t.expect(slowLogPage.slowLogCommandStatistics.withText(`${numberOfCommandsArray[i]} entries`).exists).ok(`Number of displayed commands is not equal to ${numberOfCommandsArray[i]}`);\n        }\n    }\n});\ntest('Verify that user can set slowlog-log-slower-than value in milliseconds and command duration will be re-calculated to ms', async t => {\n    // Set slower than parameter to log commands\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    // Send command in microseconds\n    command = 'scan 0 MATCH * COUNT 5000';\n    await slowLogPage.Cli.sendCommandInCli(command);\n    await t.click(slowLogPage.slowLogRefreshButton);\n    // Get duration of this command in microseconds\n    let microsecondsDuration = await slowLogPage.slowLogCommandValue.withExactText(command).parent(3).find(slowLogPage.cssSelectorDurationValue).textContent;\n    // Change microseconds to  milliseconds in configuration\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMilliSecondsUnit);\n    await t.expect(slowLogPage.slowLogTable.find('span').withExactText('Duration, msec').exists).ok('Micro-seconds were converted to milli-seconds');\n    let millisecondsDuration = await slowLogPage.slowLogCommandValue.withExactText(command).parent(3).find(slowLogPage.cssSelectorDurationValue).textContent;\n    await t.expect(parseFloat(millisecondsDuration)).eql(parseFloat(microsecondsDuration.replace(' ', '')) / 1000);\n    // Verify that user can set slowlog-log-slower-than value in microseconds and command duration will be re-calculated to microseconds\n    command = 'scan 0 MATCH * COUNT 50000';\n    await slowLogPage.Cli.sendCommandInCli(command);\n    await t.click(slowLogPage.slowLogRefreshButton);\n    // Get duration of this command in milliseconds\n    millisecondsDuration = await slowLogPage.slowLogCommandValue.withExactText(command).parent(3).find(slowLogPage.cssSelectorDurationValue).textContent;\n    // Change milliseconds to microseconds in configuration\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await t.expect(slowLogPage.slowLogTable.find('span').withExactText('Duration, µs').exists).ok('Micro-seconds were converted to milli-seconds');\n    microsecondsDuration = await slowLogPage.slowLogCommandValue.withExactText(command).parent(3).find(slowLogPage.cssSelectorDurationValue).textContent;\n    await t.expect(parseFloat(microsecondsDuration.replace(' ', '')) / 1000).eql(parseFloat(millisecondsDuration));\n    await t.expect(parseFloat(microsecondsDuration.replace(' ', ''))).eql(parseFloat(millisecondsDuration) * 1000);\n});\ntest.requestHooks(logger)\n('Verify that user can reset settings to default on Slow Log page', async t => {\n    // Set slowlog-max-len=0\n    command = 'info';\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.Cli.sendCommandInCli('info');\n    await t.click(slowLogPage.slowLogRefreshButton);\n    await t.expect(slowLogPage.slowLogCommandValue.withExactText(command).exists).ok('Logged command not found');\n    await t.click(slowLogPage.slowLogClearButton);\n    await t.click(slowLogPage.slowLogConfirmClearButton);\n\n    //Verify telemetry event\n    await telemetry.verifyEventHasProperties(telemetryEvents[0], clearExpectedProperties, logger);\n\n    // Verify that user can clear Slow Log\n    await t.expect(slowLogPage.slowLogEmptyResult.exists).ok('Slow log is not cleared');\n\n    // Set slower than parameter and max length\n    await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit);\n    await slowLogPage.changeMaxLengthParameter(maxCommandLength);\n\n    //Verify telemetry event\n    await telemetry.verifyEventHasProperties(telemetryEvents[1], loadExpectedProperties, logger);\n\n    // Reset settings to default\n    await slowLogPage.resetToDefaultConfig();\n    // Compare configuration after re-setting\n    const configText = await slowLogPage.configInfo.textContent;\n    await t.expect(configText.replace(/\\u00a0/g, ' ')).contains('Execution time: 10 msec, Max length: 128', 'Not reset configuration');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts",
    "content": "import { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../../helpers/conf'\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nimport { HashKeyParameters } from '../../../../pageObjects/browser-page';\n\nconst browserPage = new BrowserPage();\nconst browserActions = new BrowserActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyNames: string[];\n\nfixture `Delimiter tests`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async () => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    });\ntest('Verify that user can see that input is not saved when the Cancel button is clicked', async t => {\n    // Switch to tree view\n    await t.click(browserPage.treeViewButton);\n    await t.click(browserPage.TreeView.treeViewSettingsBtn);\n    // Check the default delimiter value\n    await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'Default delimiter not applied');\n    // Apply new value to the field\n    await browserPage.TreeView.FiltersDialog.removeDelimiterItem(':');\n    await browserPage.TreeView.FiltersDialog.addDelimiterItem('test');\n    // Click on Cancel button\n    await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueCancel);\n    // Check the previous delimiter value\n    await t.click(browserPage.TreeView.treeViewSettingsBtn);\n    await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'Previous delimiter not applied');\n    await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle('test').exists).eql(false, 'Previous delimiter not applied');\n    await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueCancel);\n\n    // Change delimiter\n    await browserPage.TreeView.changeDelimiterInTreeView('-');\n    // Verify that when user changes the delimiter and clicks on Save button delimiter is applied\n    await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], ['-']);\n});\ntest\n    .before(async () => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        keyNames = [\n            `device:common-dev`,\n            `device-common:dev`,\n            `device:common:dev`,\n            `device-common-dev`,\n            `device:common-stage`,\n            `device:common1-stage`,\n            `mobile:common-dev`,\n            `mobile:common-stage`\n        ];\n        for (const keyName of keyNames) {\n            let hashKeyParameters: HashKeyParameters = {\n                keyName: keyName,\n                fields: [\n                    {\n                        field: 'field',\n                        value: 'value',\n                    },\n                ],\n            }\n            await apiKeyRequests.addHashKeyApi(\n                hashKeyParameters,\n                ossStandaloneConfig,\n            )\n        }\n        await browserPage.reloadPage();\n    })\n    .after(async () => {\n        for (const keyName of keyNames) {\n            await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        }\n    })('Verify that user can set multiple delimiters in the tree view', async t => {\n        // Switch to tree view\n        await t.click(browserPage.treeViewButton);\n        // Verify folders ordering with default delimiter\n        await browserActions.checkTreeViewFoldersStructure([['device', 'common'], ['device-common'], ['mobile']], [':']);\n        await t.click(browserPage.TreeView.treeViewSettingsBtn);\n        // Apply new value to the field\n        await browserPage.TreeView.FiltersDialog.addDelimiterItem('-');\n        await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueSave);\n        // Verify that when user changes the delimiter and clicks on Save button delimiter is applied\n        await browserActions.checkTreeViewFoldersStructure([['device', 'common'], ['device', 'common1'], ['mobile', 'common']], [':', '-']);\n\n        await t.setTestSpeed(.3); // change default test speed to check tooltip\n        // Verify that namespace names tooltip contains valid names and delimiter\n        await t.click(browserActions.getNodeSelector('device'));\n        await t.hover(browserActions.getNodeSelector('device-common'));\n        await browserActions.verifyTooltipContainsText('device-common-*\\n:\\n-\\n5 key(s)', true);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/tree-view/tree-view-improvements.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keyNames: string[];\nlet keyName1: string;\nlet keyName2: string;\nlet keyNameSingle: string;\nlet index: string;\n\nfixture `Tree view navigations improvement tests`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Tree view preselected folder', async t => {\n        keyName1 = Common.generateWord(10);\n        keyName2 = Common.generateWord(10);\n        keyNameSingle = Common.generateWord(10);\n        keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle];\n\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `HSET ${keyNames[2]} field value`,\n            `SADD ${keyNames[3]} value`,\n            `SADD ${keyNames[4]} value`\n        ];\n\n        // Create 5 keys\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.treeViewButton);\n        // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.TreeView.openTreeFolders([await browserPage.TreeView.getTextFromNthTreeElement(1)]);\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Set);\n        // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.setAllKeyType();\n        // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n        await verifyKeysDisplayingInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`], false);\n\n        // switch between browser view and tree view\n        await t.click(browserPage.browserViewButton)\n            .click(browserPage.treeViewButton);\n        await browserPage.deleteKeyByName(keyNames[4]);\n        await t.click(browserPage.clearFilterButton);\n        // get first folder name\n        const firstTreeItemText = await browserPage.TreeView.getTextFromNthTreeElement(0);\n        // All folders with namespaces are collapsed when there is no folder without any patterns\n        await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false);\n\n        const commands1 = [\n            `SADD ${keyNames[4]} value`\n        ];\n\n        await browserPage.Cli.sendCommandsInCli(commands1);\n        await t.click(browserPage.refreshKeysButton);\n        // Folders are collapsed after refresh\n        await verifyKeysDisplayingInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`], false);\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n        // Only folders according to key type filter are displayed\n        await verifyKeysDisplayingInTheList([keyNameSingle], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(firstTreeItemText, true);\n\n        await browserPage.searchByKeyName(`${keyName1}*`);\n        // Only folders according to filter by key names are displayed\n        await verifyKeysDisplayingInTheList([keyNameSingle], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n\n        await t.click(browserPage.clearFilterButton);\n        // All folders are displayed and collapsed after cleared filter\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n        await verifyKeysDisplayingInTheList([keyName1], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Stream);\n        // Verify that No results found message is displayed in case of invalid filtering\n        await t.expect(browserPage.noResultsFoundOnly.textContent).contains('No results found.', 'Key is not found message not displayed');\n\n        await browserPage.setAllKeyType(); // clear stream from filter\n        // Verify that no results found message not displayed after clearing filter\n        await t.expect(browserPage.noResultsFoundOnly.exists).notOk('Key is not found message still displayed');\n        // All folders are displayed and collapsed after cleared filter\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n        await verifyKeysDisplayingInTheList([keyName1], false);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli(`FT.DROPINDEX ${index}`);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Verify tree view navigation for index based search', async t => {\n        keyName1 = Common.generateWord(10); // used to create index name\n        keyName2 = Common.generateWord(10); // used to create index name\n        const subFolder1 = Common.generateWord(10); // used to create index name\n        keyNames = [`${keyName1}:${subFolder1}:1`, `${keyName1}:${subFolder1}:2`, `${keyName2}:1:1`, `${keyName2}:1:2`];\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `HSET ${keyNames[2]} field value`,\n            `HSET ${keyNames[3]} field value`\n        ];\n        await browserPage.Cli.sendCommandsInCli(commands);\n\n        // generate index based on keyName\n        const folders = [keyName1, subFolder1];\n        index = await browserPage.Cli.createIndexwithCLI(folders.join(':'));\n        await t.click(browserPage.redisearchModeBtn); // click redisearch button\n        await browserPage.selectIndexByName(index);\n        await t.click(browserPage.treeViewButton);\n        await browserPage.TreeView.openTreeFolders(folders);\n        await t.click(browserPage.refreshKeysButton);\n        // Refreshed Tree view preselected folder for index based search\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await t.click(browserPage.patternModeBtn);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Search capability Refreshed Tree view preselected folder', async t => {\n        keyName1 = Common.generateWord(10);\n        keyName2 = Common.generateWord(10);\n        keyNameSingle = Common.generateWord(10);\n        keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle];\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `RPUSH ${keyNames[2]} field`,\n            `RPUSH ${keyNames[3]} field`,\n            `SADD ${keyNames[4]} value`\n        ];\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.treeViewButton);\n        // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns\n        await verifyKeysDisplayingInTheList([keyNameSingle], true);\n\n        await browserPage.TreeView.openTreeFolders([keyName1]); // Type: hash\n        await browserPage.TreeView.openTreeFolders([keyName2]); // Type: list\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Hash);\n        // Only related to key types filter folders are displayed\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, false);\n        await verifyKeysDisplayingInTheList([keyNames[0], keyNames[1]], false);\n\n        await browserPage.setAllKeyType();\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames[0]}`]);\n        await t.click(browserPage.refreshKeysButton); // refresh keys\n        // Only related to filter folders are displayed when key does not exist after keys refresh\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await verifyKeysDisplayingInTheList([keyNames[4]], true);\n        await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false);\n\n        await browserPage.searchByKeyName('*');\n        await t.click(browserPage.refreshKeysButton);\n        // Search capability Refreshed Tree view preselected folder\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName1, true);\n        await browserPage.TreeView.verifyFolderDisplayingInTheList(keyName2, true);\n        await verifyKeysDisplayingInTheList([keyNames[4]], true);\n        await verifyKeysDisplayingInTheList([keyNames[0], keyNames[2], keyNames[3]], false);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/tree-view/tree-view.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {BrowserPage, MyRedisDatabasePage, SettingsPage} from '../../../../pageObjects'\nimport { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf';\nimport { rte, KeyTypesTexts } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { verifySearchFilterValue } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\n\nfixture `Tree view verifications`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    });\ntest('Verify that user can see that \"Tree view\" mode is enabled state is saved when refreshes the page', async t => {\n    await t.click(browserPage.NavigationPanel.settingsButton);\n    await t.click(settingsPage.accordionAdvancedSettings);\n    await settingsPage.changeKeysToScanValue('10000');\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneBigConfig.databaseName);\n    // Verify that when user opens the application he can see that Tree View is disabled by default(Browser is selected by default)\n    await t.expect(browserPage.browserViewButton.getStyleProperty('background-color')).eql('rgb(41, 47, 71)', 'The Browser is not selected by default');\n    await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).notOk('The tree view is displayed', { timeout: 5000 });\n\n    await t.click(browserPage.treeViewButton);\n    await browserPage.reloadPage();\n    // Verify that \"Tree view\" mode enabled state is saved\n    await t.expect(browserPage.TreeView.treeViewSettingsBtn.exists).ok('The tree view is not displayed');\n\n    // Verify that user can scan DB by 10K in tree view\n    await browserPage.verifyScannningMore();\n}).skip.meta({skipComment: \"Unstable CI execution,  AssertionError, needs investigation\"});\n// outdated Verify that when user enables filtering by key name he can see only folder with appropriate keys are displayed and the number of keys and percentage is recalculated\ntest('Verify that when user switched from Tree View to Browser and goes back state of filer by key name/key type is saved', async t => {\n    const keyName = 'user*';\n    await t.click(browserPage.treeViewButton);\n    await browserPage.searchByKeyName(keyName);\n    await t.click(browserPage.browserViewButton);\n    await t.click(browserPage.treeViewButton);\n    // Verify that state of filer by key name is saved\n    await verifySearchFilterValue(keyName);\n    await t.click(browserPage.treeViewButton);\n    // Set filter by key type\n    await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n    await t.click(browserPage.browserViewButton);\n    await t.click(browserPage.treeViewButton);\n    // Verify that state of filer by key type is saved\n    await t.expect(browserPage.filterByKeyTypeDropDown.innerText).eql(KeyTypesTexts.String, 'Filter per key type is not applied');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/url-handling/url-handling.e2e.ts",
    "content": "import { commonUrl, ossStandaloneRedisGears } from '../../../../helpers/conf';\nimport { ClientFunction } from 'testcafe';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { OnboardingCardsDialog } from '../../../../pageObjects/dialogs';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\n\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst databaseHelper = new DatabaseHelper();\nconst onboardingCardsDialog = new OnboardingCardsDialog();\n\nconst { host, port, databaseName, databaseUsername = '', databasePassword = '' } = ossStandaloneRedisGears;\nconst username = 'alice&&';\nconst password = 'p1pp0@&';\n\nfunction generateLink(params: Record<string, any>, connectType: string, url: string ): string {\n    const params1 = Common.generateUrlTParams(params);\n    const from = encodeURIComponent(`${connectType}?${params1}`);\n    return (new URL(`?from=${from}`, url)).toString();\n}\n\nconst redisConnect = 'redisinsight://databases/connect';\nconst redisOpen = 'redisinsight://open';\n\nfixture `Add DB from SM`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .afterEach(async() => {\n        // Delete all existing connections\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest\n    .page(commonUrl)\n    .skip('Add DB using url via manual flow', async t => {\n        const connectUrlParams = {\n            redisUrl: `redis://${databaseUsername}:${databasePassword}@${host}:${port}`,\n            databaseAlias: databaseName,\n            redirect: 'workbench'\n        };\n        await t.navigateTo(generateLink(connectUrlParams, redisConnect,commonUrl));\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.disabledDatabaseInfo.nth(0).getAttribute('title')).contains(host, 'Wrong host value');\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.disabledDatabaseInfo.nth(1).getAttribute('title')).contains(port, 'Wrong port value');\n        await t.wait(5_000);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        // wait for db is added\n        await t.wait(10_000);\n        await t.expect(workbenchPage.submitCommandButton.exists).ok('Redirection to Workbench is not correct');\n    });\n\n//Verify that RedisInsight can work with the encoded redis URLs passed from Cloud via deep linking.\ntest\n    .before(async()  => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisGears);\n        await browserPage.Cli.sendCommandInCli(`acl DELUSER ${username}`);\n        await browserPage.Cli.sendCommandInCli(`ACL SETUSER ${username} on >${password} +@all ~*`);\n    })\n    .after(async t => {\n        // Delete all existing connections\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(databaseName);\n        await browserPage.Cli.sendCommandInCli(`acl DELUSER ${username}`);\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    .page(commonUrl)('Add DB using url automatically', async t => {\n        const codedUrl = `redis://${username}:${password}@${host}:${port}`;\n        const connectUrlParams = {\n            redisUrl: codedUrl,\n            databaseAlias: databaseName,\n            redirect: 'workbench?tutorialId=ds-json-intro',\n            cloudBdbId: '1232',\n            subscriptionType: 'fixed',\n            planMemoryLimit: '30',\n            memoryLimitMeasurementUnit: 'mb',\n            free: 'true'\n        };\n\n        const connectUrlParams2 = {\n            redirect: '/_',\n            onboarding: 'true',\n            copilot: 'false'\n        };\n\n        await t.navigateTo(generateLink(connectUrlParams, redisConnect,commonUrl));\n        await t.wait(10_000);\n        await t.expect(workbenchPage.submitCommandButton.exists).ok('Redirection to Workbench is not correct');\n        const tab = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.expect(tab.preselectArea.textContent).contains('INTRODUCTION', 'the tutorial page is incorrect');\n        await t.expect(tab.preselectArea.textContent).contains('JSON', 'the tutorial is incorrect');\n\n        const getPageUrl = ClientFunction(() => window.location.href);\n        const url = await getPageUrl();\n\n        await t.navigateTo(generateLink(connectUrlParams2, redisOpen, url));\n        await t.wait(10_000);\n        await t.expect(workbenchPage.submitCommandButton.exists).ok('Redirection to the same page is not correct');\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await t.expect(onboardingCardsDialog.showMeAroundButton.exists).ok('onboarding is nor reset');\n        await t.click(onboardingCardsDialog.skipTourButton);\n\n        //Verify that the same db is not added\n        await t.navigateTo(generateLink(connectUrlParams, redisConnect,commonUrl));\n        await t.wait(10_000);\n        await t.click(workbenchPage.NavigationPanel.myRedisDBButton);\n        await t.expect(browserPage.notification.exists).notOk({ timeout: 10000 });\n        await t.expect(myRedisDatabasePage.dbNameList.child('span').withExactText(databaseName).count).eql(2, 'the same db is added twice');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nfixture `Autocomplete for entered commands`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest('Verify that when user have selected a command (via “Enter” from the list of auto-suggested commands), user can see the required arguments inserted to the Editor', async t => {\n    const commandArguments = [\n        'key',\n        'index'\n    ];\n\n    // Start type characters and select command\n    await t.typeText(workbenchPage.queryInput, 'LI', { replace: true });\n    // Verify that the list with auto-suggestions is displayed\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).ok('Auto-suggestions are displayed');\n    // Select command and check result\n    await t.pressKey('enter');\n    const script = await workbenchPage.queryInputScriptArea.textContent;\n    // Verify that user can select a command from the list with auto-suggestions when type in any character in the Editor\n    await t.expect(script.replace(/\\s/g, ' ')).eql('LINDEX ', 'Result of sent command not exists');\n\n    // Check the required arguments suggested\n    for (const argument of commandArguments) {\n        await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(argument, `The required argument ${argument} is not suggested`);\n    }\n});\ntest('Verify that user can change any required argument inserted', async t => {\n    const command = 'HMGE';\n    const commandArguments = [\n        'key',\n        'field'\n    ];\n    const commandArgumentsForChange = [\n        'firstArgument',\n        'secondArgument'\n    ];\n\n    // Select HMGET command via Enter\n    await t.typeText(workbenchPage.queryInput, command, { replace: true });\n    await t.pressKey('enter');\n    // Change required arguments\n    const scriptBeforeEdit = await workbenchPage.queryInputScriptArea.textContent;\n    await t.typeText(workbenchPage.queryInput, commandArgumentsForChange[0]);\n    await t.pressKey('tab');\n    await t.typeText(workbenchPage.queryInput, commandArgumentsForChange[1]);\n    const scriptAfterEdit = await workbenchPage.queryInputScriptArea.textContent;\n    // Verify the command after changes\n    await t.expect(scriptBeforeEdit).notEql(scriptAfterEdit, 'The required arguments are not editable');\n    await t.expect(scriptAfterEdit).notContains(commandArguments[0], `The argument ${commandArguments[0]} is not changed`);\n});\ntest('Verify that the list of optional arguments will not be inserted with autocomplete', async t => {\n    const command = 'ZPOPMA';\n    const commandRequiredArgument = 'key';\n    const commandOptionalArgument = '[count]';\n\n    // Select ZPOPMAX command via Enter\n    await t.typeText(workbenchPage.queryInput, command, { replace: true });\n    await t.pressKey('enter');\n    // Verify the command arguments inserted\n    const script = await workbenchPage.queryInputScriptArea.textContent;\n    await t.expect(script.replace(/\\s/g, ' ')).eql('ZPOPMAX ', 'Result of sent command not exists');\n\n    // Check the required and optional arguments suggested\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(commandRequiredArgument, `The required argument is not suggested`);\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(commandOptionalArgument, `The optional argument is not suggested in blocks`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst commandForSend1 = 'info';\nconst commandForSend2 = 'FT._LIST';\nlet indexName = Common.generateWord(5);\n\nfixture `Command results at Workbench`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest.skip('Verify that user can see re-run icon near the already executed command and re-execute the command by clicking on the icon in Workbench page', async t => {\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandForSend1);\n    await workbenchPage.sendCommandInWorkbench(commandForSend2);\n    const containerOfCommand = await workbenchPage.getCardContainerByCommand(commandForSend1);\n    const containerOfCommand2 = await workbenchPage.getCardContainerByCommand(commandForSend2);\n    // Verify that re-run icon is displayed\n    await t.expect(await workbenchPage.reRunCommandButton.visible).ok('Re-run icon is not displayed');\n    // Re-run the last command in results\n    await t.click(containerOfCommand.find(workbenchPage.cssReRunCommandButton));\n    // Verify that command is re-executed\n    await t.expect(workbenchPage.queryCardCommand.textContent).eql(commandForSend1, 'The command is not re-executed');\n\n    // Verify that user can see expanded result after command re-run at the top of results table in Workbench\n    await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible)\n        .ok('Re-executed command is not expanded');\n    await t.expect(workbenchPage.queryCardCommand.nth(0).textContent).eql(commandForSend1, 'The re-executed command is not at the top of results table');\n\n    // Delete the command from results\n    await t.click(containerOfCommand2.find(workbenchPage.cssDeleteCommandButton));\n    // Verify that user can delete command with result from table with results in Workbench\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend2).exists).notOk(`Command ${commandForSend2} is not deleted from table with results`);\n});\ntest.skip('Verify that user can see the results found in the table view by default for FT.INFO, FT.SEARCH and FT.AGGREGATE', async t => {\n    const commands = [\n        'FT.INFO',\n        'FT.SEARCH',\n        'FT.AGGREGATE'\n    ];\n        // Send commands and check table view is default\n    for(const command of commands) {\n        await workbenchPage.sendCommandInWorkbench(command);\n        await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssTableViewTypeOption).exists).ok(`The table view is not selected by default for command ${command}`);\n    }\n});\ntest\n    .after(async() => {\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    })\n    .skip('Verify that user can switch between views and see results according to the view rules in Workbench in results', async t => {\n        indexName = Common.generateWord(5);\n        const commands = [\n            'hset doc:10 title \"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\" url \"redis.io\" author \"Test\" rate \"undefined\" review \"0\" comment \"Test comment\"',\n            `FT.CREATE ${indexName} ON HASH PREFIX 1 doc: SCHEMA title TEXT WEIGHT 5.0 body TEXT url TEXT author TEXT rate TEXT review TEXT comment TEXT`,\n            `FT.SEARCH ${indexName} * limit 0 10000`\n        ];\n        // Send commands and check table view is default for Search command\n        for (const command of commands) {\n            await workbenchPage.sendCommandInWorkbench(command);\n        }\n        await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssTableViewTypeOption).exists)\n            .ok('The table view is not selected by default for command FT.SEARCH');\n        await t.switchToIframe(workbenchPage.iframe);\n        await t.expect(await workbenchPage.queryTableResult.visible).ok('The table result is not displayed for command FT.SEARCH');\n        // Select Text view and check result\n        await t.switchToMainWindow();\n        await workbenchPage.selectViewTypeText();\n        await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The result is not displayed in Text view');\n    });\n\ntest\n    .skip('Verify that user can switches between Table and Text for Client List and see results corresponding to their views', async t => {\n    const command = 'CLIENT LIST';\n    // Send command and check table view is default\n    await workbenchPage.sendCommandInWorkbench(command);\n    await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssClientListViewTypeOption).exists)\n        .ok('The table view is not selected by default for command CLIENT LIST');\n    await t.switchToIframe(workbenchPage.iframe);\n\n    await t.expect(await workbenchPage.queryTableResult.visible)\n        .ok('The search results are not displayed in Client List Table view by default');\n    // Select Text view from dropdown and check search results\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible)\n        .ok('The result is not displayed in Text view');\n});\n\ntest\n    .after(async() => {\n        // remove all keys\n        workbenchPage.sendCommandInWorkbench('flushdb');\n    })\n    .skip('Verify that user can switches between JSON view and Text view and see proper result', async t => {\n        const jsonObj = { a: 2 };\n        const json = JSON.stringify(jsonObj);\n        const sendCommandsJsonGet = [\n            `JSON.SET doc1 $ '${json}'`,\n            'JSON.GET doc1'\n        ];\n\n        const sendCommandsJsonMGet = [\n            `JSON.SET doc2 $ '${json}'`,\n            'JSON.MGET doc2 $'\n        ];\n\n        const sendCommandsStringGet = [\n            `SET doc3 '${json}'`,\n            'GET doc3'\n        ];\n\n        // Send command and check json view is default for json.get\n        await workbenchPage.sendCommandInWorkbench(sendCommandsJsonGet.join('\\n'));\n        await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssJsonViewTypeOption).exists)\n            .ok('The json view is not selected by default for command JSON.GET');\n\n        await t.switchToIframe(workbenchPage.iframe);\n\n        let jsonTextActual = await Common.removeEmptySpacesAndBreak(await workbenchPage.queryJsonResult.textContent);\n        let jsonTextExpected = await Common.removeEmptySpacesAndBreak(json);\n        await t.expect(jsonTextActual).eql(jsonTextExpected);\n\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(sendCommandsJsonMGet.join('\\n'));\n        await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssJsonViewTypeOption).exists)\n            .ok('The json view is not selected by default for command JSON.MGET');\n\n        await t.switchToIframe(workbenchPage.iframe);\n        const expectedJsonMGet = JSON.stringify([[jsonObj]]);\n        jsonTextActual = await Common.removeEmptySpacesAndBreak(await workbenchPage.queryJsonResult.textContent);\n        jsonTextExpected = await Common.removeEmptySpacesAndBreak(expectedJsonMGet);\n        await t.expect(jsonTextActual).eql(jsonTextExpected);\n\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(sendCommandsStringGet.join('\\n'));\n        await workbenchPage.selectViewTypeJson();\n\n        await t.switchToIframe(workbenchPage.iframe);\n        jsonTextActual = await Common.removeEmptySpacesAndBreak(await workbenchPage.queryJsonResult.textContent);\n        jsonTextExpected = await Common.removeEmptySpacesAndBreak(json);\n        await t.expect(jsonTextActual).eql(jsonTextExpected);\n    });\n\ntest\n    .after(async() => {\n        //Drop database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })\n    .skip('Verify that user can populate commands in Editor from history by clicking keyboard “up” button', async t => {\n        const commands = [\n            'FT.INFO',\n            'RANDOMKEY',\n            'set'\n        ];\n        // Send commands\n        for(const command of commands) {\n            await workbenchPage.sendCommandInWorkbench(command);\n        }\n        // Clear input\n        await t\n            .click(workbenchPage.queryInput)\n            .pressKey('ctrl+a')\n            .pressKey('delete')\n            .pressKey('esc');\n        // Verify the quick access to command history by up button\n        for (const command of commands.reverse()) {\n            await t.pressKey('up');\n            const script = await workbenchPage.scriptsLines.textContent;\n            await t.expect(script.replace(/\\s/g, ' ')).contains(command, 'Result of Manual command is not displayed');\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/context.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst speed = 0.4;\nlet indexName = Common.generateWord(5);\n\n// todo: investigate. seems like should fail but passes\nfixture `Workbench Context`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async() => {\n        // Drop index, documents and database\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    });\ntest('Verify that user can see saved input in Editor when navigates away to any other page', async t => {\n    indexName = Common.generateWord(5);\n    const command = `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`;\n    // Enter the command in the Workbench editor and navigate to Browser\n    await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed });\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Return back to Workbench and check input in editor\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect((await workbenchPage.queryInputScriptArea.textContent).replace(/\\s/g, ' ')).eql(command, 'Input in Editor is saved');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nfixture `Cypher syntax at Workbench`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest\n    .skip('Verify that user can see popover Editor when clicks on “Use Cypher Syntax” popover in the Editor or “Shift+Space”', async t => {\n    const command = 'GRAPH.QUERY graph';\n\n    // Type command and put the cursor inside\n    await t.typeText(workbenchPage.queryInput, `${command} \"query\"`, { replace: true });\n    await t.pressKey('left');\n    // Open popover editor by clicks on “Use Cypher Syntax”\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    await t.expect(await workbenchPage.queryInput.nth(1).visible).ok('The user can not see opened popover Editor');\n    // Close popover editor and re-open by shortcut\n    await t.pressKey('esc');\n    await t.expect(await workbenchPage.queryInput.nth(1).visible).notOk('The popover Editor is not closed');\n    await t.pressKey('shift+space');\n    await t.expect(await workbenchPage.queryInput.nth(1).visible).ok('The user can not see opened popover Editor');\n});\ntest\n    .skip('Verify that popover Editor is populated with the script that was detected between the quotes or it is blank if quotes were empty', async t => {\n    const command = 'GRAPH.QUERY graph';\n    const script = 'query';\n\n    // Type command with empty script and open popover\n    await t.typeText(workbenchPage.queryInput, `${command} \"\"`, { replace: true });\n    await t.pressKey('left');\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    // Verify that the Editor is blank\n    await t.expect(workbenchPage.scriptsLines.nth(1).textContent).eql('', 'The user can not see blank Editor');\n    // Close popover editor and re-open with added script\n    await t.pressKey('esc');\n    await t.typeText(workbenchPage.queryInput, `${command} \"${script}`, { replace: true });\n    await t.pressKey('left');\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    // Verify that the Editor is populated with the script\n    await t.expect(workbenchPage.scriptsLines.nth(1).textContent).eql(script, 'The user can not see editor populated with the script that was detected between the quotes');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Telemetry } from '../../../../helpers/telemetry';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst chance = new Chance();\nconst telemetry = new Telemetry();\n\nlet indexName = chance.word({ length: 5 });\nlet keyName = chance.word({ length: 5 });\nconst logger = telemetry.createLogger();\nconst tutorialTelemetryEvent = 'EXPLORE_PANEL_TUTORIAL_OPENED';\nconst workbenchTelemetryEvents = ['WORKBENCH_COMMAND_SUBMITTED','WORKBENCH_MODE_CHANGED']\nconst telemetryPath = 'static/tutorials/ds/hashes.md';\nconst tutorialExpectedProperties = [\n    'databaseId',\n    'path'\n];\nconst workbenchExpectedProperties = [\n    'command',\n    'databaseId',\n    'multiple',\n    'pipeline',\n    'provider',\n    'rawMode',\n    'results'\n];\nconst rawModeExpectedProperties = [\n    'changedFromMode',\n    'changedToMode',\n    'databaseId',\n    'provider',\n];\n\nfixture `Default scripts area at Workbench`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async t => {\n        // Drop index, documents and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    });\ntest\n    .skip\n    .requestHooks(logger)('Verify that user can run automatically  \"FT._LIST\" and \"FT.INFO {index}\" scripts in Workbench and see the results', async t => {\n        indexName = 'idx:schools';\n        keyName = chance.word({ length: 5 });\n        const commandsForSend = [\n            `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`,\n            `HMSET product:1 name \"${keyName}\"`,\n            `HMSET product:2 name \"${keyName}\"`\n        ];\n        const addedScript = 'FT._LIST \\n' +\n\n            `FT.INFO \"${indexName}\"`;\n        // Send commands\n        await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n        await telemetry.verifyEventHasProperties(workbenchTelemetryEvents[0], workbenchExpectedProperties, logger);\n        // Run automatically added \"FT._LIST\" and \"FT.INFO {index}\" scripts\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.click(tutorials.dataStructureAccordionTutorialButton);\n        await t.click(tutorials.internalLinkWorkingWithHashes);\n\n        // Verify that telemetry event 'WORKBENCH_ENABLEMENT_AREA_GUIDE_OPENED' sent and has all expected properties\n        await telemetry.verifyEventHasProperties(tutorialTelemetryEvent, tutorialExpectedProperties, logger);\n        await telemetry.verifyEventPropertyValue(tutorialTelemetryEvent, 'path', telemetryPath, logger);\n\n        await workbenchPage.sendCommandInWorkbench(addedScript);\n\n        // Check the FT._LIST result\n        await t.expect(workbenchPage.queryTextResult.textContent).contains(indexName, 'The result of the FT._LIST command not found');\n        // Verify telemetry event\n        await t.click(workbenchPage.rawModeBtn);\n        await telemetry.verifyEventHasProperties(workbenchTelemetryEvents[1], rawModeExpectedProperties, logger);\n        // Check the FT.INFO result\n        await t.switchToIframe(workbenchPage.iframe);\n        await t.expect(workbenchPage.queryColumns.textContent).contains('name', 'The result of the FT.INFO command not found');\n    });\ntest.skip('Verify that user can edit and run automatically added \"Search\" script in Workbench and see the results', async t => {\n    indexName = chance.word({ length: 5 });\n    keyName = chance.word({ length: 5 });\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`,\n        `HMSET product:1 name \"${keyName}\"`,\n        `HMSET product:2 name \"${keyName}\"`\n    ];\n    const searchCommand = `FT.SEARCH ${indexName} \"${keyName}\"`;\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n    // Run automatically added FT.SEARCH script with edits\n\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    // Check the FT.SEARCH result\n    await t.switchToIframe(workbenchPage.iframe);\n    const key = workbenchPage.queryTableResult.withText('product:1');\n    const name = workbenchPage.queryTableResult.withText(keyName);\n    await t.expect(key.exists).ok('The added key is not in the Search result');\n    await t.expect(name.exists).ok('The added key name field is not in the Search result');\n});\ntest.skip('Verify that user can edit and run automatically added \"Aggregate\" script in Workbench and see the results', async t => {\n    indexName = chance.word({ length: 5 });\n    const aggregationResultField = 'max_price';\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE`,\n        'HMSET product:1 price 20',\n        'HMSET product:2 price 100'\n    ];\n    const searchCommand = `FT.Aggregate ${indexName} * GROUPBY 0 REDUCE MAX 1 @price AS ${aggregationResultField}`;\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'), 0.5);\n    // Run automatically added FT.Aggregate script with edits\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    // Check the FT.Aggregate result\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.textContent).contains(aggregationResultField, 'The aggregation field name is not in the Search result');\n    await t.expect(workbenchPage.queryTableResult.textContent).contains('100', 'The aggregation max value is in not the Search result');\n});\n// Outdated after https://redislabs.atlassian.net/browse/RI-4279\ntest.skip('Verify that when the “Manual” option clicked, user can see the Editor is automatically prepopulated with the information', async t => {\n    const information = [\n        '// Workbench is the advanced Redis command-line interface that allows to send commands to Redis, read and visualize the replies sent by the server.',\n        '// Enter multiple commands at different rows to run them at once.',\n        '// Start a new line with an indent (Tab) to specify arguments for any Redis command in multiple line mode.'\n    ];\n        // Click on the Manual option\n    await t.click(workbenchPage.preselectManual);\n    // Resize the scripting area\n    const offsetY = 200;\n    await t.drag(workbenchPage.resizeButtonForScriptingAndResults, 0, offsetY, { speed: 0.4 });\n    // Check the result\n    const script = await workbenchPage.scriptsLines.textContent;\n    for(const info of information) {\n        await t.expect(script.replace(/\\s/g, ' ')).contains(info, 'Result of Manual command is not displayed');\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/no-indexes-suggestions.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossClusterConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst workbenchPage = new WorkbenchPage();\n\nfixture `Search and Query Raw mode`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\n\ntest\n    .before(async () => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Verify suggestions when there are no indexes', async t => {\n\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n\n        await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true });\n        await t.pressKey('tab');\n\n        await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('No indexes to display').exists).ok('info text is not displayed');\n\n        await t.pressKey('ctrl+space');\n        await t.expect(await workbenchPage.MonacoEditor.monacoCommandDetails.find('a').exists).ok('no link in the details')\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nlet indexName = Common.generateWord(5);\nlet keyName = Common.generateWord(5);\n\nfixture `Scripting area at Workbench`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        //Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async t => {\n        await t.switchToMainWindow();\n        //Drop index, documents and database\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    });\n// Update after resolving https://redislabs.atlassian.net/browse/RI-3299\ntest\n    .skip('Verify that user can resize scripting area in Workbench', async t => {\n    const commandForSend = 'info';\n    const offsetY = 130;\n\n    await workbenchPage.sendCommandInWorkbench(commandForSend);\n    // Verify that user can run any script from CLI in Workbench and see the results\n    await t.expect(workbenchPage.queryCardContainer.exists).ok('Query card was added');\n    const sentCommandText = workbenchPage.queryCardCommand.withExactText(commandForSend);\n    await t.expect(sentCommandText.exists).ok('Result of sent command exists');\n\n    const inputHeightStart = await workbenchPage.queryInput.clientHeight;\n\n    await t.hover(workbenchPage.resizeButtonForScriptingAndResults);\n    await t.drag(workbenchPage.resizeButtonForScriptingAndResults, 0, offsetY, { speed: 0.1 });\n    // Verify that user can resize scripting area\n    const inputHeightEnd = inputHeightStart + 15;\n    await t.expect(await workbenchPage.queryInput.clientHeight).gt(inputHeightEnd, 'Scripting area after resize has incorrect size');\n});\ntest\n    .skip('Verify that user when he have more than 10 results can request to view more results in Workbench', async t => {\n    indexName = Common.generateWord(5);\n    keyName = Common.generateWord(5);\n    const commandsForSendInCli = [\n        `HMSET product:1 name \"${keyName}\"`,\n        `HMSET product:2 name \"${keyName}\"`,\n        `HMSET product:3 name \"${keyName}\"`,\n        `HMSET product:4 name \"${keyName}\"`,\n        `HMSET product:5 name \"${keyName}\"`,\n        `HMSET product:6 name \"${keyName}\"`,\n        `HMSET product:7 name \"${keyName}\"`,\n        `HMSET product:8 name \"${keyName}\"`,\n        `HMSET product:9 name \"${keyName}\"`,\n        `HMSET product:10 name \"${keyName}\"`,\n        `HMSET product:11 name \"${keyName}\"`,\n        `HMSET product:12 name \"${keyName}\"`\n    ];\n    const commandToCreateSchema = `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`;\n    const searchCommand = `FT.SEARCH ${indexName} * LIMIT 0 20`;\n    //Open CLI\n    await t.click(workbenchPage.Cli.cliExpandButton);\n    //Create new keys for search\n    for(const command of commandsForSendInCli) {\n        await t.typeText(workbenchPage.Cli.cliCommandInput, command, { replace: true });\n        await t.pressKey('enter');\n    }\n    await t.click(workbenchPage.Cli.cliCollapseButton);\n    //Send commands\n    await workbenchPage.sendCommandInWorkbench(commandToCreateSchema);\n    //Send search command\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    //Verify that we have pagination buttons\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.paginationButtonPrevious.exists).ok('Pagination previous button exists');\n    await t.expect(workbenchPage.paginationButtonNext.exists).ok('Pagination next button exists');\n});\ntest\n    .skip('Verify that user can see result in Table and Text views for Hash data types for FT.SEARCH command in Workbench', async t => {\n    indexName = Common.generateWord(5);\n    keyName = Common.generateWord(5);\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`,\n        `HMSET product:1 name \"${keyName}\"`,\n        `HMSET product:2 name \"${keyName}\"`\n    ];\n    const searchCommand = `FT.SEARCH ${indexName} * LIMIT 0 20`;\n    //Send commands\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n    //Send search command\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    //Check that result is displayed in Table view\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The result is displayed in Table view');\n    //Select Text view type\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    //Check that result is displayed in Text view\n    await t.expect(workbenchPage.queryTextResult.exists).ok('The result is displayed in Text view');\n});\ntest\n    .skip('Verify that user can run one command in multiple lines in Workbench page', async t => {\n    indexName = Common.generateWord(5);\n    const multipleLinesCommand = [\n        `FT.CREATE ${indexName}`,\n        'ON HASH PREFIX 1 product:',\n        'SCHEMA price NUMERIC SORTABLE'\n    ];\n    // Send command in multiple lines\n    await workbenchPage.sendCommandInWorkbench(multipleLinesCommand.join('\\n\\t'), 0.5);\n    // Check the result\n    const resultCommand = await workbenchPage.queryCardCommand.nth(0).textContent;\n    for(const commandPart of multipleLinesCommand) {\n        await t.expect(resultCommand).contains(commandPart, 'The multiple lines command is in the result');\n    }\n});\ntest\n    .skip('Verify that user can use one indent to indicate command in several lines in Workbench page', async t => {\n    indexName = Common.generateWord(5);\n    const multipleLinesCommand = [\n        `FT.CREATE ${indexName}`,\n        'ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE'\n    ];\n    // Send command in multiple lines\n    await t.typeText(workbenchPage.queryInput, multipleLinesCommand[0]);\n    await t.pressKey('enter esc tab');\n    await t.typeText(workbenchPage.queryInput, multipleLinesCommand[1]);\n    await t.click(workbenchPage.submitCommandButton);\n    // Check the result\n    const resultCommand = await workbenchPage.queryCardCommand.nth(0).textContent;\n    for(const commandPart of multipleLinesCommand) {\n        await t.expect(resultCommand).contains(commandPart, 'The multiple lines command is in the result');\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/critical-path/workbench/search-and-query-autocomplete.e2e.ts",
    "content": "import { Common, DatabaseHelper } from '../../../../helpers';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst keyName = Common.generateWord(10);\nlet keyNames: string[];\nlet indexName1: string;\nlet indexName2: string;\nlet indexName3: string;\n\nfixture `Autocomplete for entered commands in search and query`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        indexName1 = `idx1:${keyName}`;\n        indexName2 = `idx2:${keyName}`;\n        indexName3 = `idx3:${keyName}`;\n        keyNames = [`${keyName}:1`, `${keyName}:2`, `${keyName}:3`];\n        const commands = [\n            `HSET ${keyNames[0]} \"name\" \"Hall School\" \"description\" \" Spanning 10 states\" \"class\" \"independent\" \"type\" \"traditional\" \"address_city\" \"London\" \"address_street\" \"Manor Street\" \"students\" 342 \"location\" \"51.445417, -0.258352\"`,\n            `HSET ${keyNames[1]} \"name\" \"Garden School\" \"description\" \"Garden School is a new outdoor\" \"class\" \"state\" \"type\" \"forest; montessori;\" \"address_city\" \"London\" \"address_street\" \"Gordon Street\" \"students\" 1452 \"location\" \"51.402926, -0.321523\"`,\n            `HSET ${keyNames[2]} \"name\" \"Gillford School\" \"description\" \"Gillford School is a centre\" \"class\" \"private\" \"type\" \"democratic; waldorf\" \"address_city\" \"Goudhurst\" \"address_street\" \"Goudhurst\" \"students\" 721 \"location\" \"51.112685, 0.451076\"`,\n            `FT.CREATE ${indexName1} ON HASH PREFIX 1 \"${keyName}:\" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR \";\" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`,\n            `FT.CREATE ${indexName2} ON HASH PREFIX 1 \"${keyName}:\" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR \";\" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`\n        ];\n\n        // Create 3 keys and index\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await browserPage.Cli.sendCommandsInCli([\n            `DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName1}`,\n            `DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName2}`,\n        ]);\n    });\ntest\n    .skip('Verify that tutorials can be opened from Workbench', async t => {\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.click(workbenchPage.getTutorialLinkLocator('sq-intro'));\n    await t.expect(workbenchPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened');\n    const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.expect(tab.preselectArea.textContent).contains('INTRODUCTION', 'the tutorial page is incorrect');\n});\ntest('Verify that user can use show more to see command fully in 2nd tooltip', async t => {\n    const commandDetails = [\n        'index query [VERBATIM] [LOAD count field [field ...]]',\n        'Run a search query on an index and perform aggregate transformations on the results',\n        'Arguments:',\n        'required index',\n        'required query',\n        'optional [verbatim]'\n    ];\n    await t.typeText(workbenchPage.queryInput, 'FT.A', { replace: true });\n    // Verify that user can use show more to see command fully in 2nd tooltip\n    await t.pressKey('ctrl+space');\n    await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).ok('The \"read more\" about the command is not opened');\n    for(const detail of commandDetails) {\n        await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.textContent).contains(detail, `The ${detail} command detail is not displayed`);\n    }\n    // Verify that user can close show more tooltip by 'x' or 'Show less'\n    await t.pressKey('ctrl+space');\n    await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).notOk('The \"read more\" about the command is not closed');\n});\ntest('Verify full commands suggestions with index and query for FT.AGGREGATE', async t => {\n    const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE ';\n    const indexFields = [\n        'address',\n        'city',\n        'class',\n        'description',\n        'location',\n        'name',\n        'students',\n        'type'\n    ];\n    const ftSortedCommands = ['FT.SEARCH', 'FT.AGGREGATE', 'FT.CREATE', 'FT.EXPLAIN', 'FT.PROFILE'];\n\n    // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE\n    await t.typeText(workbenchPage.queryInput, 'FT', { replace: true });\n    // Verify custom sorting for FT. commands\n    await t.expect(await workbenchPage.MonacoEditor.getSuggestionsArrayFromMonaco(5)).eql(ftSortedCommands, 'Wrong order of FT commands');\n    // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT._LIST').exists).ok('FT._LIST auto-suggestions are not displayed');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed');\n\n    // Select command and check result\n    await t.typeText(workbenchPage.queryInput, '.AG', { replace: false });\n    await t.pressKey('enter');\n    let script = await workbenchPage.queryInputScriptArea.textContent;\n    await t.expect(script.replace(/\\s/g, ' ')).contains('FT.AGGREGATE ', 'Result of sent command exists');\n\n    // Verify that user can see the list of all the indexes in database when put a space after only FT.SEARCH and FT.AGGREGATE commands\n    await t.expect(script.replace(/\\s/g, ' ')).contains(`'${indexName1}' 'query to search' `, 'Index not suggested into input');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(indexName1).exists).ok('Index not auto-suggested');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(indexName2).exists).ok('All indexes not auto-suggested');\n\n    await t.pressKey('tab');\n    await t.wait(200);\n    await t.typeText(workbenchPage.queryInput, '@', { replace: false });\n    script = await workbenchPage.queryInputScriptArea.textContent;\n    // Verify that user can see the list of fields from the index selected when type in “@”\n    await t.expect(script.replace(/\\s/g, ' ')).contains('address', 'Index not suggested into input');\n    for(const field of indexFields) {\n        await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(field).exists).ok(`${field} Index field not auto-suggested`);\n    }\n    // Verify that user can use autosuggestions by typing fields from index after \"@\"\n    await t.typeText(workbenchPage.queryInput, 'ci', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('city').exists).ok('Index field not auto-suggested after starting typing');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.count).eql(1, 'Wrong index fields suggested after typing first letter');\n\n    // Go out of index field\n    await t.pressKey('tab');\n    await t.pressKey('tab');\n    await t.pressKey('right');\n    await t.pressKey('space');\n    // Verify contextual suggestions after typing letters for commands\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('FT.AGGREGATE arguments not suggested');\n    await t.typeText(workbenchPage.queryInput, 'g', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('GROUPBY', 'Argument not suggested after typing first letters');\n\n    await t.pressKey('tab');\n    // Verify that user can see widget about entered argument\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(groupByArgInfo, 'Widget with info about entered argument not displayed');\n\n    await t.typeText(workbenchPage.queryInput, '1 \"London\"', { replace: false });\n    await t.pressKey('space');\n    // Verify correct order of suggested arguments like LOAD, GROUPBY, SORTBY\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments');\n    await t.pressKey('tab');\n    await t.typeText(workbenchPage.queryInput, 'SUM 1 @students', { replace: false });\n    await t.pressKey('space');\n\n    // Verify expression and function suggestions like AS for APPLY/GROUPBY\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('AS', 'Incorrect order of suggested arguments');\n    await t.pressKey('tab');\n    await t.typeText(workbenchPage.queryInput, 'stud', { replace: false });\n\n    await t.pressKey('space');\n    // Verify multiple argument option suggestions\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments');\n    // Verify complex command sequences like nargs and properties are suggested accurately for GROUPBY\n    const expectedText = `FT.AGGREGATE '${indexName1}' '@city:{tag} ' GROUPBY 1 \"London\" REDUCE SUM 1 @students AS stud REDUCE`.trim().replace(/\\s+/g, ' ');\n    await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments');\n});\ntest('Verify full commands suggestions with index and query for FT.SEARCH', async t => {\n    await t.typeText(workbenchPage.queryInput, 'FT.SEA', { replace: true });\n    // Select command and check result\n    await t.pressKey('enter');\n    const script = await workbenchPage.queryInputScriptArea.textContent;\n    await t.expect(script.replace(/\\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists');\n\n    await t.pressKey('tab')\n    // Select '@city' field\n    await workbenchPage.selectFieldUsingAutosuggest('city')\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested');\n    await t.typeText(workbenchPage.queryInput, 'n', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters');\n    await t.pressKey('tab');\n    // Verify that FT.SEARCH and FT.AGGREGATE non-multiple arguments are suggested only once\n    await t.pressKey('space');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('NOCONTENT').exists).notOk('Non-multiple arguments are suggested not only once');\n\n    // Verify that suggestions correct to closest valid commands or options for invalid typing like WRONGCOMMAND\n    await t.typeText(workbenchPage.queryInput, 'WRONGCOMMAND', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('WITHSORTKEYS').exists).ok('Closest suggestions not displayed');\n\n    await t.pressKey('space');\n    await t.pressKey('backspace');\n    await t.pressKey('backspace');\n    // Verify that 'No suggestions' tooltip is displayed when returning to invalid typing like WRONGCOMMAND\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested');\n});\ntest('Verify full commands suggestions with index and query for FT.PROFILE(SEARCH)', async t => {\n    await t.typeText(workbenchPage.queryInput, 'FT.PR', { replace: true });\n    // Select command and check result\n    await t.pressKey('enter');\n    const script = await workbenchPage.queryInputScriptArea.textContent;\n    await t.expect(script.replace(/\\s/g, ' ')).contains('FT.PROFILE ', 'Result of sent command exists');\n\n    await t.pressKey('tab');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('AGGREGATE').exists).ok('FT.PROFILE aggregate argument not suggested');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('SEARCH').exists).ok('FT.PROFILE search argument not suggested');\n\n    // Select SEARCH command\n    await t.typeText(workbenchPage.queryInput, 'SEA', { replace: false });\n    await t.pressKey('enter');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE SEARCH arguments not suggested');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE SEARCH arguments not suggested');\n\n    // Select QUERY\n    await t.typeText(workbenchPage.queryInput, 'QUE', { replace: false });\n    await t.pressKey('enter');\n    await workbenchPage.selectFieldUsingAutosuggest('city');\n    // Verify that there are no more suggestions\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested');\n    const expectedText = `FT.PROFILE '${indexName1}' SEARCH QUERY '@city:{tag} '`.trim().replace(/\\s+/g, ' ');\n    // Verify command entered correctly\n    await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments');\n});\ntest('Verify full commands suggestions with index and query for FT.PROFILE(AGGREGATE)', async t => {\n    await t.typeText(workbenchPage.queryInput, 'FT.PR', { replace: true });\n    // Select command and check result\n    await t.pressKey('enter');\n    await t.pressKey('tab');\n    // Select AGGREGATE command\n    await t.typeText(workbenchPage.queryInput, 'AGG', { replace: false });\n    await t.pressKey('enter');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE AGGREGATE arguments not suggested');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE AGGREGATE arguments not suggested');\n\n    // Select QUERY\n    await t.typeText(workbenchPage.queryInput, 'QUE', { replace: false });\n    await t.pressKey('enter');\n    await workbenchPage.selectFieldUsingAutosuggest('city');\n    // Verify that there are no more suggestions\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested');\n    const expectedText = `FT.PROFILE '${indexName1}' AGGREGATE QUERY '@city:{tag} '`.trim().replace(/\\s+/g, ' ');\n    // Verify command entered correctly\n    await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments');\n});\ntest('Verify full commands suggestions with index and query for FT.EXPLAIN', async t => {\n    await t.typeText(workbenchPage.queryInput, 'FT.EX', { replace: true });\n    // Select command and check result\n    await t.pressKey('enter');\n    await t.pressKey('tab');\n    await workbenchPage.selectFieldUsingAutosuggest('city');\n\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.EXPLAIN arguments not suggested');\n    // Add DIALECT\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, 'dialectTest', { replace: false });\n    // Verify that there are no more suggestions\n    await t.pressKey('space');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested');\n    const expectedText = `FT.EXPLAIN '${indexName1}' '@city:{tag} ' DIALECT dialectTest`.trim().replace(/\\s+/g, ' ');\n    // Verify command entered correctly\n    await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments');\n});\ntest('Verify commands suggestions for APPLY and FILTER', async t => {\n    await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true });\n    await t.pressKey('enter');\n\n    await t.typeText(workbenchPage.queryInput, '*');\n    await t.pressKey('right');\n    await t.pressKey('space');\n    // Verify APPLY command\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('Apply is not suggested');\n    await t.pressKey('enter');\n\n    await t.typeText(workbenchPage.queryInput, 'g');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).ok('commands is not suggested');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, '@', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed');\n    await t.typeText(workbenchPage.queryInput, 'location', { replace: false });\n    await t.typeText(workbenchPage.queryInput, ', \"40.7128,-74.0060\"');\n    for (let i = 0; i < 3; i++) {\n        await t.pressKey('right');\n    }\n    await t.pressKey('space');\n    await t.typeText(workbenchPage.queryInput, 'a');\n    await t.pressKey('tab');\n    await t.typeText(workbenchPage.queryInput, 'apply_key', { replace: false });\n\n    await t.pressKey('space');\n    // Verify Filter command\n    await t.typeText(workbenchPage.queryInput, 'F');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('FILTER').exists).ok('FILTER is not suggested');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, 'apply_key < 5000', { replace: false });\n    await t.pressKey('right');\n    await t.pressKey('space');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('GROUPBY').exists).ok('query can not be prolong');\n});\ntest('Verify REDUCE commands', async t => {\n    await t.typeText(workbenchPage.queryInput, `FT.AGGREGATE ${indexName1} \"*\" GROUPBY 1 @location`, { replace: true });\n    await t.pressKey('space');\n    // select Reduce\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('REDUCE is not suggested');\n    await t.typeText(workbenchPage.queryInput, 'R');\n    await t.pressKey('enter');\n\n    // set value of reduce\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed');\n    // Select COUNT\n    await t.typeText(workbenchPage.queryInput, 'CO');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, '0');\n\n    // verify that count of nargs is correct\n    await t.pressKey('space');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, 'item_count ');\n\n    // add additional reduce\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('Apply is not suggested');\n    await t.typeText(workbenchPage.queryInput, 'R');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, 'SUM');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, '1 ');\n\n    await t.typeText(workbenchPage.queryInput, '@', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed');\n    await t.typeText(workbenchPage.queryInput, 'students ', { replace: false });\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested');\n    await t.pressKey('enter');\n    await t.typeText(workbenchPage.queryInput, 'total_students');\n});\n// todo: rewrite. seems flaky. passes when run as a single test when failing when run with other tests\ntest.skip('Verify suggestions for fields', async t => {\n    await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true });\n    await t.typeText(workbenchPage.queryInput, 'idx1');\n    await t.pressKey('enter');\n    await t.wait(200);\n\n    await t.typeText(workbenchPage.queryInput, '@');\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed');\n\n    // verify suggestions for geo\n    await t.typeText(workbenchPage.queryInput, 'l');\n    await t.pressKey('tab');\n    await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE '${indexName1}' '@location:[lon lat radius unit] '`);\n\n    // verify for numeric\n    await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true });\n    await t.typeText(workbenchPage.queryInput, 'idx1');\n    await t.pressKey('enter');\n    await t.wait(200);\n\n    await t.typeText(workbenchPage.queryInput, '@');\n    await t.typeText(workbenchPage.queryInput, 's');\n    await t.pressKey('tab');\n    await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE '${indexName1}' '@students:[range] '`);\n});\n// Unskip after fixing https://redislabs.atlassian.net/browse/RI-6212\ntest.skip\n    .after(async() => {\n    // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName1}`]);\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName2}`]);\n        await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName3}`]);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify commands suggestions for CREATE', async t => {\n        await t.typeText(workbenchPage.queryInput, 'FT.CREATE ', { replace: true });\n        // Verify that indexes are not suggested for FT.CREATE\n        await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Existing index suggested');\n\n        // Enter index name\n        await t.typeText(workbenchPage.queryInput, indexName3);\n        await t.pressKey('space');\n\n        // Select FILTER keyword\n        await t.typeText(workbenchPage.queryInput, 'FI');\n        await t.pressKey('tab');\n        await t.typeText(workbenchPage.queryInput, 'filterNew', { replace: false });\n        await t.pressKey('space');\n\n        // Select SCHEMA keyword\n        await t.typeText(workbenchPage.queryInput, 'SCH');\n        await t.pressKey('tab');\n        await t.typeText(workbenchPage.queryInput, 'field_name', { replace: false });\n        await t.pressKey('space');\n\n        // Select TEXT keyword\n        await t.typeText(workbenchPage.queryInput, 'te', { replace: false });\n        await t.pressKey('tab');\n\n        // Select SORTABLE\n        await t.typeText(workbenchPage.queryInput, 'so', { replace: false });\n        await t.pressKey('tab');\n\n        // Enter second field to SCHEMA\n        await t.typeText(workbenchPage.queryInput, 'field2_num', { replace: false });\n        await t.pressKey('space');\n        await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('NUMERIC').exists).ok('query can not be prolong');\n\n        // Select NUMERIC keyword\n        await t.typeText(workbenchPage.queryInput, 'so', { replace: false });\n        await t.pressKey('tab');\n\n        await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.CREATE ${indexName3} FILTER filterNew SCHEMA field_name TEXT SORTABLE field2_num NUMERIC`);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/add-keys.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\nimport path from 'path';\n\nconst browserPage = new BrowserPage();\nconst browserActions = new BrowserActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst jsonFilePath = path.join('..', '..', '..', '..', 'test-data', 'big-json', 'json-BigInt.json');\n\nconst jsonKeys = [['JSON-string', '\"test\"'], ['JSON-number', '782364'], ['JSON-boolean', 'true'], ['JSON-null', 'null'], ['JSON-array', '[1, 2, 3]']];\nlet keyNames: string[];\nlet indexName: string;\n\nfixture `Add keys`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        let commandString = 'DEL';\n        for (const key of jsonKeys) {\n            commandString = commandString.concat(` ${key[0]}`);\n        }\n        await browserPage.Cli.sendCommandInCli(commandString);\n    });\ntest('Verify that user can create different types(string, number, null, array, boolean) of JSON', async t => {\n    for (let i = 0; i < jsonKeys.length; i++) {\n        const keySelector = browserPage.getKeySelectorByName(jsonKeys[i][0]);\n        await browserPage.addJsonKey(jsonKeys[i][0], jsonKeys[i][1]);\n        await t.hover(browserPage.Toast.toastCloseButton);\n        await t.click(browserPage.Toast.toastCloseButton);\n        await t.click(browserPage.refreshKeysButton);\n        await t.expect(keySelector.exists).ok(`${jsonKeys[i][0]} key not displayed`);\n        // Add additional check for array elements\n        if (jsonKeys[i][0].includes('array')) {\n            for (const j of JSON.parse(jsonKeys[i][1])) {\n                await t.expect(browserPage.jsonScalarValue.withText(j.toString()).exists).ok('JSON value not correct');\n            }\n        }\n        else {\n            await t.expect(browserPage.jsonKeyValue.withText(jsonKeys[i][1]).exists).ok('JSON value not correct');\n        }\n    }\n});\n// https://redislabs.atlassian.net/browse/RI-3995\ntest\n    .before(async(t) => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .after(async() => {\n        let commandString = 'DEL';\n        for (const key of keyNames) {\n            commandString = commandString.concat(` ${key}`);\n        }\n        const commands = [`FT.DROPINDEX ${indexName}`, commandString];\n        await browserPage.Cli.sendCommandsInCli(commands);\n    })('Verify that the new key is displayed at the top of the list', async t => {\n        const keyName = Common.generateWord(12);\n        const keyName1 = Common.generateWord(12);\n        const keyName2 = Common.generateWord(36);\n        const keyName3 = Common.generateWord(10);\n        const keyName4 = `${Common.generateWord(10)}-test`;\n        const keyName5 = `hash-${Common.generateWord(12)}`;\n        keyNames = [keyName, keyName1, keyName2, keyName3, keyName4, keyName5];\n        indexName = `idx:${keyName5}`;\n        const command = `FT.CREATE ${indexName} ON HASH PREFIX 1 hash- SCHEMA name TEXT`;\n        await browserPage.Cli.sendCommandInCli(command);\n\n        await browserPage.addStringKey(keyName);\n        await browserActions.verifyKeyDisplayedTopAndOpened(keyName);\n        // Verify displaying added multiple keys\n        await browserPage.addSetKey(keyName1);\n        await browserActions.verifyKeyDisplayedTopAndOpened(keyName1);\n\n        await browserPage.addHashKey(keyName2);\n        await browserActions.verifyKeyDisplayedTopAndOpened(keyName2);\n        // Verify that user can see the key removed from the top when refresh List view\n        await t.click(browserPage.refreshKeysButton);\n        await browserActions.verifyKeyIsNotDisplayedTop(keyName1);\n        // Verify that the new key is not displayed at the top when filter per key name applied\n        await browserPage.searchByKeyName('*test');\n        await browserPage.addHashKey(keyName4);\n        await browserActions.verifyKeyIsNotDisplayedTop(keyName4);\n\n        await t.click(browserPage.clearFilterButton);\n        await t.click(browserPage.treeViewButton);\n        await browserPage.addHashKey(keyName3);\n        // Verify that user can see Tree view recalculated when new key is added in Tree view\n        await browserActions.verifyKeyIsNotDisplayedTop(keyName3);\n        await t.expect(browserPage.keyNameFormDetails.withExactText(keyName3).exists).ok(`Key ${keyName3} details not opened`);\n\n        await t.click(browserPage.redisearchModeBtn);\n        await browserPage.selectIndexByName(indexName);\n        await browserPage.addHashKey(keyName5, '100000', 'name', 'value');\n        // Verify that the new key is not displayed at the top for the Search capability\n        await browserActions.verifyKeyIsNotDisplayedTop(keyName3);\n    });\ntest('Verify that user can add json with BigInt', async t => {\n    const keyName = Common.generateWord(12);\n\n    // Add Json key with json object\n    await t.click(browserPage.plusAddKeyButton);\n    await t.click(browserPage.keyTypeDropDown);\n    await t.click(browserPage.jsonOption);\n    await t.click(browserPage.addKeyNameInput);\n    await t.typeText(browserPage.addKeyNameInput, keyName, { replace: true, paste: true });\n    await t.setFilesToUpload(browserPage.jsonUploadInput, [jsonFilePath]);\n    await t.click(browserPage.addKeyButton);\n\n    await t.click(browserPage.editJsonObjectButton);\n    await t.expect(await browserPage.jsonValueInput.textContent).contains('message', 'edit value is empty');\n    await t.click(browserPage.cancelEditButton);\n\n    await t.click(browserPage.expandJsonObject);\n    await t.click(browserPage.expandJsonObject);\n    await t.expect(await browserPage.jsonKeyValue.textContent).contains('1.2345678998765432e+24', 'BigInt is not displayed');\n    await t.expect(await browserPage.jsonKeyValue.textContent).contains('123456789987654321', 'BigInt is not displayed');\n\n    await browserPage.addJsonKeyOnTheSameLevel('\"key2\"', '7777777777888889455');\n    await t.expect(await browserPage.jsonKeyValue.textContent).contains('7777777777888889455', 'BigInt is not displayed');\n\n    await t.click(browserPage.editJsonObjectButton.nth(3));\n    await t.typeText(browserPage.jsonValueInput, '121212121111112121212111', { paste: true, replace: true });\n    await t.click(browserPage.applyEditButton);\n    await t.expect(await browserPage.jsonKeyValue.textContent).contains('1.2121212111111212e+23', 'BigInt is not displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/consumer-group.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keyName = Common.generateWord(20);\nlet consumerGroupName = Common.generateWord(20);\nconst keyField = Common.generateWord(20);\nconst keyValue = Common.generateWord(20);\n\nfixture `Consumer group`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        if (await browserPage.closeKeyButton.visible){\n            await t.click(browserPage.closeKeyButton);\n        }\n    });\ntest('Verify that when user enter invalid Group Name the error message appears', async t => {\n    const message = 'Your Key has no Consumer Groups available.';\n    const error = 'BUSYGROUP Consumer Group name already exists';\n    const errorFormat = 'ID format is not correct';\n    const invalidEntryIds = [\n        'qwerty12344545',\n        '16545941463181654594146318'\n    ];\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n\n    // Add New Stream Key\n    await browserPage.addStreamKey(keyName, keyField, keyValue);\n    // Open Stream consumer groups and check message\n    await t.click(browserPage.streamTabGroups);\n    // Verify that user can see the message when there are no Consumer Groups\n    await t.expect(browserPage.streamGroupsContainer.textContent).contains(message, 'No Consumer Groups message not displayed');\n\n    // Open Stream consumer groups and enter invalid EntryIds\n    await t.click(browserPage.addKeyValueItemsButton);\n    // Verify that when user enter invalid format ID the error message appears\n    for (const entryId of invalidEntryIds) {\n        await t.typeText(browserPage.consumerIdInput, entryId, { replace: true, paste: true });\n        await t.click(browserPage.saveGroupsButton);\n        await t.expect(browserPage.entryIdError.textContent).eql(errorFormat, 'The invalid Id error message not displayed');\n    }\n\n    await t.click(browserPage.cancelStreamGroupBtn);\n    await browserPage.createConsumerGroup(consumerGroupName);\n    // Verify the error message\n    await t.click(browserPage.streamTabGroups);\n    await browserPage.createConsumerGroup(consumerGroupName);\n    await t.expect(browserPage.Toast.toastError.textContent).contains(error, 'The error message that the Group name already exists not displayed');\n});\ntest('Verify that user can sort Consumer Group column: A>Z / Z>A(A>Z is default table sorting)', async t => {\n    keyName = Common.generateWord(20);\n    const consumerGroupNames = [\n        'agroup',\n        'bgroup',\n        'zgroup'\n    ];\n\n    // Add New Stream Key\n    await browserPage.addStreamKey(keyName, keyField, keyValue);\n    // Open Stream consumer groups and add few groups\n    await t.click(browserPage.streamTabGroups);\n    for(const group of consumerGroupNames){\n        await browserPage.createConsumerGroup(group);\n    }\n    // Verify default sorting\n    const groupsCount = await browserPage.streamGroupName.count;\n    for(let i = 0; i < groupsCount; i++){\n        await t.expect(browserPage.streamGroupName.nth(i).textContent).contains(consumerGroupNames[i], 'The Consumer Groups default sorting not working');\n    }\n    // Verify the Z>A sorting\n    await t.click(browserPage.scoreButton.nth(0));\n    for(let i = 0; i < groupsCount; i++){\n        await t.expect(browserPage.streamGroupName.nth(i).textContent).contains(consumerGroupNames[groupsCount - 1 - i], 'The Consumer Groups Z>A sorting not working');\n    }\n});\ntest('Verify that A>Z is default table sorting in Consumer column', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const consumerNames = [\n        'Alice',\n        'Zalice'\n    ];\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} ${consumerNames[0]} COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName} ${consumerNames[1]} COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with groups and consumers\n    for(const command of cliCommands){\n        await browserPage.Cli.sendCommandInCli(command);\n    }\n    // Open Stream consumer info view\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.streamTabGroups);\n    await t.click(browserPage.consumerGroup);\n    // Verify default sorting\n    const consumerCount = await browserPage.streamConsumerName.count;\n    for(let i = 0; i < consumerCount; i++){\n        await t.expect(browserPage.streamConsumerName.nth(i).textContent).contains(consumerNames[i], 'The Consumers default sorting not working');\n    }\n});\ntest('Verify that user can see error message if enter invalid last delivered ID', async t => {\n    keyName = Common.generateWord(20);\n    const consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`\n    ];\n    const invalidEntryIds = [\n        '!@#%^&*()_',\n        '12345678901242532366121324'\n    ];\n    const errorMessage = 'ID format is not correct';\n\n    // Add New Stream Key with groups and consumers\n    for(const command of cliCommands){\n        await browserPage.Cli.sendCommandInCli(command);\n    }\n    // Open Stream consumer info view\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.streamTabGroups);\n    // Change the ID set for the Consumer Group\n    await t.hover(browserPage.streamGroupId);\n    await t.click(browserPage.editStreamLastIdButton);\n    for(const id of invalidEntryIds){\n        const idBefore = await browserPage.streamGroupId.textContent;\n        await t.typeText(browserPage.lastIdInput, id, { replace: true, paste: true });\n        await t.click(browserPage.saveButton);\n        await t.expect(browserPage.streamGroupId.textContent).eql(idBefore, 'The last delivered ID is not modified');\n        await t.expect(browserPage.entryIdError.textContent).eql(errorMessage, 'The error message not displayed');\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/context.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { verifySearchFilterValue } from '../../../../helpers/keys';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `Browser Context`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that if user has saved context on Browser page and go to Settings page, Browser and Workbench icons are displayed and user is able to open Browser with saved context', async t => {\n    keyName = Common.generateWord(10);\n    const command = 'HSET';\n    // Create context modificaions and navigate to Settings\n    await apiKeyRequests.addStringKeyApi({\n        keyName,\n        value: 'v',\n    }, ossStandaloneConfig);\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n    await t.pressKey('enter');\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Open Browser page and verify context\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneConfig.databaseName);\n    await verifySearchFilterValue(keyName);\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('The key details is not selected');\n    await t.expect(browserPage.Cli.cliCommandExecuted.withExactText(command).exists).ok(`Executed command '${command}' in CLI is not saved`);\n    await t.click(browserPage.Cli.cliCollapseButton);\n});\ntest('Verify that when user reload the window with saved context(on any page), context is not saved when he returns back to Browser page', async t => {\n    keyName = Common.generateWord(10);\n    // Create context modificaions and navigate to Workbench\n    await apiKeyRequests.addStringKeyApi({\n        keyName,\n        value: 'v',\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Open Browser page and verify context\n    await t.click(browserPage.NavigationTabs.browserButton);\n    await verifySearchFilterValue(keyName);\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('The key details is not selected');\n    // Navigate to Workbench and reload the window\n    await t.click(browserPage.NavigationTabs.pubSubButton);\n    await myRedisDatabasePage.reloadPage();\n    // Return back to Browser and check context is not saved\n    await t.click(browserPage.NavigationTabs.browserButton);\n    await t.expect(browserPage.filterByPatterSearchInput.withAttribute('value', keyName).exists).notOk('Filter per key name is applied');\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).notOk('The key details is selected');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/filtering-iteratively.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossClusterConfig, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keys: string[];\n\nfixture `Filtering iteratively in Browser page`\n    .meta({ type: 'regression' })\n    .page(commonUrl)\n    .beforeEach(async(t) => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n    });\ntest\n    .meta({ rte: rte.standalone })('Verify that user can see search results per 500 keys if number of results is 500', async t => {\n        // Create new keys\n        keys = await Common.createArrayWithKeyValue(500);\n        await browserPage.Cli.sendCommandInCli(`MSET ${keys.join(' ')}`);\n        // Search all keys\n        await browserPage.searchByKeyName('*');\n        const keysNumberOfResults = await browserPage.keysNumberOfResults.textContent;\n        // Verify that number of results is 500\n        await t.expect(keysNumberOfResults).match(/50[0-9]/, 'Number of results is not 500');\n    });\ntest\n    .meta({ rte: rte.standalone })('Verify that user can search iteratively via Scan more for search pattern and selected data type', async t => {\n        // Create new keys\n        keys = await Common.createArrayWithKeyValue(1000);\n        await browserPage.Cli.sendCommandInCli(`MSET ${keys.join(' ')}`);\n        // Search all string keys\n        await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n        await browserPage.searchByKeyName('*');\n        // Verify that scan more button is shown\n        await t.expect(browserPage.scanMoreButton.exists).ok('Scan more is not shown');\n        await t.click(browserPage.scanMoreButton);\n        // Verify that number of results is 1000\n        const keysNumberOfResults = await browserPage.keysNumberOfResults.textContent;\n        await t.expect(keysNumberOfResults).match(/1 00[0-9]/, 'Number of results is not 1 000');\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .before(async(t) => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n    })('Verify that user can search via Scan more for search pattern and selected data type in OSS Cluster DB', async t => {\n        // Create new keys\n        keys = await Common.createArrayWithKeyValueForOSSCluster(1000);\n        await browserPage.Cli.sendCommandInCli(`MSET ${keys.join(' ')}`);\n        // Search all string keys\n        await browserPage.selectFilterGroupType(KeyTypesTexts.String);\n        await browserPage.searchByKeyName('*');\n        // Verify that scan more button is shown\n        await t.expect(browserPage.scanMoreButton.exists).ok('Scan more is not shown');\n        await t.click(browserPage.scanMoreButton);\n        const regExp = new RegExp('1 0' + '.');\n        // Verify that number of results is 1000\n        const scannedValueText = await browserPage.scannedValue.textContent;\n        await t.expect(scannedValueText).match(regExp, 'Number of results is not 1 000');\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .before(async(t) => {\n        // Add Big standalone DB\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })('Verify that user use Scan More in DB with 10-50 millions of keys (when search by pattern/)', async t => {\n        // Search all string keys\n        await browserPage.searchByKeyName('*');\n        // Verify that scan more button is shown\n        await t.expect(browserPage.scanMoreButton.exists).ok('Scan more is not shown');\n        await t.click(browserPage.scanMoreButton);\n        const regExp = new RegExp('1 0' + '.');\n        // Verify that number of results is 1000\n        const scannedValueText = await browserPage.scannedValue.textContent;\n        await t.expect(scannedValueText).match(regExp, 'Number of results is not 1 000');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/filtering.e2e.ts",
    "content": "import { Selector } from 'testcafe';\nimport { KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneBigConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { keyTypes } from '../../../../helpers/keys';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(20);\nlet keyName2 = Common.generateWord(20);\nconst COMMAND_GROUP_SET = 'Set';\n\nfixture `Filtering per key name in Browser page`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that when user searches not existed key, he can see the standard screen when there are no keys found', async t => {\n    keyName = `KeyForSearch*${Common.generateWord(10)}?[]789`;\n    const searchedKeyName = 'key00000qwertyuiop[asdfghjkl';\n    const searchedValue = 'KeyForSear*';\n\n    // Add new key\n    await apiKeyRequests.addStringKeyApi({\n        keyName,\n        value: 'v',\n    }, ossStandaloneConfig);\n\n    // Search not existed key\n    await browserPage.searchByKeyName(searchedKeyName);\n    // Verify the standard screen when there are no keys found\n    const noResultsFound = await browserPage.noResultsFound.textContent;\n    const searchAdvices = await browserPage.searchAdvices.textContent;\n    await t.expect(noResultsFound).eql('No results found.', 'The no results text not displayed');\n    await t.expect(searchAdvices).eql('Check the spelling.Check upper and lower cases.Use an asterisk (*) in your request for more generic results.', 'The advices text not displayed');\n\n    // Filter per pattern with *\n    await browserPage.searchByKeyName(searchedValue);\n    // Verify that user can filter per pattern with * (matches keys with any number of characters instead of *)\n    await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');\n});\ntest('Verify that user can filter per pattern with ? (matches keys with any character (only one) instead of ?)', async t => {\n    const randomValue = Common.generateWord(10);\n    const searchedValue = `?eyForSearch\\\\*\\\\?\\\\[]789${randomValue}`;\n    keyName = `KeyForSearch*?[]789${randomValue}`;\n\n    // Add new key\n    await apiKeyRequests.addStringKeyApi({\n        keyName,\n        value: 'v',\n    }, ossStandaloneConfig);\n    // Filter per pattern with ?\n    await browserPage.searchByKeyName(searchedValue);\n    // Verify that key was found\n    await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');\n});\ntest\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(keyName2, ossStandaloneConfig.databaseName);\n    })('Verify that user can filter per pattern with [xy] (matches one symbol: either x or y))', async t => {\n        keyName = `KeyForSearch${Common.generateWord(10)}`;\n        keyName2 = `KeyForFearch${Common.generateWord(10)}`;\n        const searchedValue1 = 'KeyFor[SF]*';\n        const searchedValue2 = 'KeyFor[^F]*';\n        const searchedValue3 = 'KeyFor[A-G]*';\n\n        // Add keys\n        await apiKeyRequests.addStringKeyApi({\n            keyName,\n            value: 'v',\n        }, ossStandaloneConfig);\n        await apiKeyRequests.addHashKeyApi({\n            keyName: keyName2,\n            fields: [{\n                field: 'f',\n                value: 'v',\n            }],\n        }, ossStandaloneConfig);\n        // Filter per pattern with [XY]\n        await browserPage.searchByKeyName(searchedValue1);\n        // Verify that key was found with filter per pattern with [xy]\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName2)).ok('The key was not found');\n\n        await browserPage.searchByKeyName(searchedValue2);\n        // Verify that user can filter per pattern with [^x] (matches one symbol except x)\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName2)).notOk('The wrong key found');\n\n        await browserPage.searchByKeyName(searchedValue3);\n        // Verify that user can filter per pattern with [a-z] (matches any symbol in range from A till Z)\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName2)).ok('The key was not found');\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk('The wrong key found');\n    });\ntest('Verify that when user clicks on “clear” control with no filter per key name applied all characters and filter per key type are removed, “clear” control is disappeared', async t => {\n    keyName = `KeyForSearch${Common.generateWord(10)}`;\n\n    // Set filter by key type and filter per key name\n    await browserPage.searchByKeyName(keyName);\n    await browserPage.selectFilterGroupType(COMMAND_GROUP_SET);\n    // Verify that when user clicks on “clear” control and filter per key name is applied all characters and filter per key type are removed, “clear” control is disappeared\n    await t.click(browserPage.clearFilterButton);\n    await t.expect(browserPage.multiSearchArea.find(browserPage.cssFilteringLabel).visible).notOk('The filter per key type is not removed');\n    await t.expect(browserPage.filterByPatterSearchInput.getAttribute('value')).eql('', 'All characters from filter input are not removed');\n    await t.expect(browserPage.clearFilterButton.visible).notOk('The clear control is not disappeared');\n    await apiKeyRequests.addStringKeyApi({\n        keyName,\n        value: 'v',\n    }, ossStandaloneConfig);\n    // Search for not existed key name\n    await browserPage.searchByKeyName(keyName2);\n    await t.expect(browserPage.keysContainer.textContent).contains('No results found.', 'Key is not found message not displayed');\n    // Verify that when user clicks on “clear” control and filter per key name is applied filter is reset and rescan initiated\n    await t.click(browserPage.clearFilterButton);\n    await t.expect(browserPage.filterByPatterSearchInput.getAttribute('value')).eql('', 'The filtering is not reset');\n    await t.expect(browserPage.noResultsFound.exists).notOk('No results found message is not hidden');\n\n    // Set filter by key type and type characters\n    await t.typeText(browserPage.filterByPatterSearchInput, keyName);\n    await browserPage.selectFilterGroupType(COMMAND_GROUP_SET);\n    // Verify the clear control with no filter per key name\n    await t.click(browserPage.clearFilterButton);\n    await t.expect(browserPage.multiSearchArea.find(browserPage.cssFilteringLabel).visible).notOk('The filter per key type is not removed');\n    await t.expect(browserPage.filterByPatterSearchInput.getAttribute('value')).eql('', 'All characters from filter input are not removed');\n    await t.expect(browserPage.clearFilterButton.visible).notOk('The clear control is not disappeared');\n});\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    })\n    .after(async() => {\n        // Delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneBigConfig.databaseName);\n    })('Verify that user can filter per exact key without using any patterns in DB with 10 millions of keys', async t => {\n        // Create new key\n        keyName = `KeyForSearch-${Common.generateWord(10)}`;\n        await apiKeyRequests.addSetKeyApi({\n            keyName,\n            members: ['m'],\n        }, ossStandaloneBigConfig);\n        // Search by key name\n        await browserPage.searchByKeyName(keyName);\n        // Verify that required key is displayed\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not found');\n        // Check searched key in tree view\n        await t.click(browserPage.treeViewButton);\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not found');\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    })('Verify that user can filter per key name using patterns in DB with 10-50 millions of keys', async t => {\n        keyName = 'device*';\n        await browserPage.selectFilterGroupType(KeyTypesTexts.Set);\n        await browserPage.searchByKeyName(keyName);\n        for (let i = 0; i < 10; i++) {\n            // Verify that keys are filtered\n            await t.expect(browserPage.keyNameInTheList.nth(i).textContent).contains('device', 'Keys filtered incorrectly by key name')\n                .expect(browserPage.keyNameInTheList.nth(i).textContent).contains('set', 'Keys filtered incorrectly by key type');\n        }\n        await t.click(browserPage.treeViewButton);\n        // Verify that user can use the \"Scan More\" button to search per another 10000 keys\n        await browserPage.verifyScannningMore();\n\n        // Verify that user can filter per key type in DB with 10-50 millions of keys\n        await t.click(browserPage.browserViewButton);\n        await t.click(browserPage.clearFilterButton);\n        for (let i = 0; i < keyTypes.length - 2; i++) {\n            await browserPage.selectFilterGroupType(keyTypes[i].textType);\n            const filteredTypeKeys = keyTypes[i].keyName === 'json'\n                ? Selector('[data-testid^=badge-ReJSON]')\n                : Selector(`[data-testid^=badge-${keyTypes[i].keyName}]`);\n            // Verify that all results have the same type as in filter\n            await t.expect(browserPage.filteringLabel.count).eql(await filteredTypeKeys.count, `The keys of type ${keyTypes[i].textType} not filtered correctly`);\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/format-switcher.e2e.ts",
    "content": "import { keyLength, rte } from '../../../../helpers/constants';\nimport { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../../helpers/keys';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfigEmpty } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keysData = keyTypes.map(object => ({ ...object }));\nkeysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${Common.generateWord(keyLength)}`);\nconst databasesForAdding = [\n    { host: ossStandaloneConfigEmpty.host, port: ossStandaloneConfigEmpty.port, databaseName: 'testDB1' },\n    { host: ossStandaloneConfigEmpty.host, port: ossStandaloneConfigEmpty.port, databaseName: 'testDB2' }\n];\n\nfixture `Format switcher functionality`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        // Create new keys\n        await addKeysViaCli(keysData);\n    })\n    .afterEach(async() => {\n        // Clear keys and database\n        await deleteKeysViaCli(keysData);\n    });\ntest\n    .before(async() => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding);\n        // Reload Page\n        await browserPage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName);\n        // Create new keys\n        await addKeysViaCli(keysData);\n    })\n    .after(async() => {\n        // Clear keys and database\n        await deleteKeysViaCli(keysData);\n        await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding);\n    })('Formatters saved selection', async t => {\n        // Open key details and select JSON formatter\n        await browserPage.openKeyDetails(keysData[0].keyName);\n        await browserPage.selectFormatter('JSON');\n        // Reopen key details\n        await t.click(browserPage.closeKeyButton);\n        await browserPage.navigateToKey(keysData[0].keyName);\n        // Verify that formatters selection is saved when user switches between keys\n        await t.expect(browserPage.formatSwitcher.withExactText('JSON').visible).ok('Formatter value is not saved');\n        // Verify that formatters selection is saved when user reloads the page\n        await browserPage.reloadPage();\n        await browserPage.navigateToKey(keysData[1].keyName);\n        await t.expect(browserPage.formatSwitcher.withExactText('JSON').visible).ok('Formatter value is not saved');\n        // Go to another database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName);\n        await browserPage.navigateToKey(keysData[2].keyName);\n        // Verify that formatters selection is saved when user switches between databases\n        await t.expect(browserPage.formatSwitcher.withExactText('JSON').visible).ok('Formatter value is not saved');\n    });\ntest('Verify that user can see switcher icon for narrow screen and tooltip by hovering', async t => {\n    // Create array with JSON, GRAPH, TS keys\n    const keysWithoutSwitcher = [keysData[5], keysData[7], keysData[8]];\n\n    for (let i = 0; i < keysWithoutSwitcher.length; i++) {\n        await browserPage.navigateToKey(keysWithoutSwitcher[i].keyName);\n        // Verify that user don`t see format switcher for JSON, GRAPH, TS keys\n        await t.expect(browserPage.formatSwitcher.exists).notOk(`Formatter is displayed for ${keysWithoutSwitcher[i].textType} type`, { timeout: 1000 });\n    }\n\n    await browserPage.navigateToKey(keysData[0].keyName);\n    await browserPage.selectFormatter('JSON');\n    // Verify icon is not displayed with high screen resolution\n    await t.expect(browserPage.formatSwitcherIcon.exists).notOk('Format switcher Icon is displayed with high screen resolution');\n    // Minimize the window to check icon\n    await t.resizeWindow(1500, 900);\n    // Verify icon is displayed with low screen resolution\n    await t.expect(browserPage.formatSwitcherIcon.exists).ok('Format switcher Icon is not displayed with low screen resolution');\n    await t.setTestSpeed(0.7);\n    await t.hover(browserPage.formatSwitcher.find('[data-testid^=key-value-formatter-option-selected]'));\n    // Verify tooltip is displayed on hover with low screen resolution\n    await t.expect(browserPage.tooltip.textContent).contains('JSON', 'Selected formatter is not displayed in tooltip');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/formatter-warning.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst jsonInvalidStructure = '\"{\\\"test\\\": 123\"';\nconst title = 'Value will be saved as Unicode';\nconst reason = 'as it is not valid in the selected format.';\nlet keyName = Common.generateWord(10);\n\nfixture `Warning for invalid formatter value`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear keys and database\n        await browserPage.Cli.sendCommandInCli(`del ${keyName}`);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\n// todo: enable after fix RI-7461\ntest.skip('Verify that user can see warning message when editing value', async t => {\n    // Open key details\n    await browserPage.addStringKey(keyName, '{\"test\": 123}');\n    await browserPage.selectFormatter('JSON');\n    await browserPage.editStringKeyValue(jsonInvalidStructure);\n    await t\n        // Verify that user sees warning message when value in selected format is not correct\n        .expect(browserPage.changeValueWarning.visible).ok('Warning is not displayed')\n        // Verify that tooltip has text \"Value will be saved as Unicode as it is not valid in the selected format.\"\n        .expect(browserPage.changeValueWarning.find('h4').withExactText(title).visible).ok('Title is not correct')\n        .expect(browserPage.changeValueWarning.find('p').withExactText(reason).visible).ok('Reason is not correct');\n    await t.click(browserPage.saveButton);\n    // Verify that when user click on save button, value is saved in Unicode format\n    await t.expect(browserPage.stringValueAsJson.exists).notOk('Value is not converted to Unicode');\n    // Verify that user doesn't see warning message if saving value is correct in selected format\n    await browserPage.editStringKeyValue('{\"test\": 123}');\n    await t\n        .expect(browserPage.changeValueWarning.visible).notOk('Warning is not displayed')\n        .expect(browserPage.stringValueAsJson.exists).ok('Value is not converted to JSON object');\n});\ntest('Verify that user can remove invalid format value warning the message by clicking on ESC button', async t => {\n    keyName = Common.generateWord(10);\n    const keyValue = 'a:1:{s:8:\"glossary\";a:2:{s:5:\"title\";s:7:\"example\";s:8:\"GlossDiv\";a:2:{s:5:\"title\";s:1:\"S\";s:9:\"GlossList\";a:1:{s:10:\"GlossEntry\";a:3:{s:2:\"ID\";s:4:\"SGML\";s:8:\"GlossDef\";a:2:{s:4:\"para\";s:8:\"language\";s:12:\"GlossSeeAlso\";a:1:{i:0;s:3:\"XML\";}}s:8:\"GlossSee\";s:6:\"markup\";}}}}}';\n    await browserPage.addHashKey(keyName, '5000', 'PHP Serialized', keyValue);\n    await browserPage.selectFormatter('PHP serialized');\n    await browserPage.editHashKeyValue(jsonInvalidStructure);\n    await t.expect(browserPage.changeValueWarning.visible).ok('Warning is not displayed');\n    await t.pressKey('esc');\n    await t.expect(browserPage.changeValueWarning.visible).notOk('Warning is still displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/formatters.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { Common, DatabaseHelper } from '../../../../helpers';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nconst apiKeyRequests = new APIKeyRequests();\n\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyName = `TestHashKey-${ Common.generateWord(10) }`;\n\nfixture `Formatters`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n\n    })\n    .afterEach(async() => {\n        // Clear keys and database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\n\ntest('Verify that UTF8 in PHP serialized', async t => {\n    const phpValueChinese = '测试';\n    const phpValueCRussian = 'Привет мир!';\n    const setValue =`SET ${keyName} \"a:3:{s:4:\\\\\"name\\\\\";s:6:\\\\\"${phpValueChinese}\\\\\";s:3:\\\\\"age\\\\\";i:30;s:7:\\\\\"message\\\\\";s:20:\\\\\"${phpValueCRussian}\\\\\";}\"\\n`;\n\n    await browserPage.Cli.sendCommandInCli(setValue);\n    await t.click(browserPage.refreshKeysButton);\n\n    await browserPage.openKeyDetailsByKeyName(keyName);\n    await browserPage.selectFormatter('PHP serialized');\n    await t.expect(await browserPage.getStringKeyValue()).contains(phpValueChinese, 'data is not serialized in php');\n    await t.expect(await browserPage.getStringKeyValue()).contains(phpValueCRussian, 'data is not serialized in php');\n});\n\ntest\n    .skip('Verify that dataTime is displayed in Java serialized', async t => {\n    const hexValue ='ACED00057372000E6A6176612E7574696C2E44617465686A81014B59741903000078707708000000BEACD0567278';\n    const javaTimeValue = '\"1995-12-14T12:12:01.010Z\"'\n\n    await browserPage.addHashKey(keyName);\n    // Add valid value in HEX format for convertion\n    await browserPage.selectFormatter('HEX');\n    await browserPage.editHashKeyValue(hexValue);\n    await browserPage.selectFormatter('Java serialized');\n    await t.expect(browserPage.hashFieldValue.innerText).eql(javaTimeValue, 'data is not serialized in java');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/full-screen.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneConfigEmpty } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst keyName = Common.generateWord(20);\nconst keyValue = Common.generateWord(20);\n\nfixture `Full Screen`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async(t) => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await browserPage.reloadPage();\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n    })\n    .afterEach(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    });\ntest\n    .after(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    })('Verify that user can switch to full screen from key details in Browser', async t => {\n        await apiKeyRequests.addStringKeyApi({\n            keyName,\n            value: keyValue,\n        }, ossStandaloneConfig);\n        await browserPage.navigateToKey(keyName);\n\n        // Save tables size before switching to full screen mode\n        const widthBeforeFullScreen = await browserPage.keyDetailsHeader.clientWidth;\n        // Switch to full screen mode\n        await t.click(browserPage.fullScreenModeButton);\n        // Compare size of details table after switching\n        const widthAfterFullScreen = await browserPage.keyDetailsHeader.clientWidth;\n        await t.expect(widthAfterFullScreen).gt(widthBeforeFullScreen, 'Width after switching to full screen not greater then before');\n        await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Key Details Table not displayed');\n        await t.expect(browserPage.stringKeyValueInput.withExactText(keyValue).exists).ok('Key Value in Details not displayed');\n        // Verify that user can exit full screen in key details and two tables with keys and key details are displayed\n        await t.click(browserPage.fullScreenModeButton);\n        const widthAfterExitFullScreen = await browserPage.keyDetailsHeader.clientWidth;\n        await t.expect(widthAfterExitFullScreen).lt(widthAfterFullScreen, 'Width after switching from full screen not less then before');\n    });\ntest('Verify that when no keys are selected user can click on \"Close\" control for right table and see key list in full screen', async t => {\n    // Verify that user sees two panels(key list and empty details panel) opening Browser page for the first time\n    await t.expect(browserPage.noKeysToDisplayText.visible).ok('No keys selected panel not displayed');\n    // Save key table size before switching to full screen\n    const widthKeysBeforeFullScreen = await browserPage.keyListTable.clientWidth;\n    // Close right panel with key details\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).notOk('Key Details Table not displayed');\n    await t.click(browserPage.closeRightPanel);\n    // Check that table is in full screen\n    const widthTableAfterFullScreen = await browserPage.keyListTable.clientWidth;\n    await t.expect(widthTableAfterFullScreen).gt(widthKeysBeforeFullScreen, 'Width after switching to full screen not greater then before');\n});\ntest('Verify that when user closes key details in full screen mode the list of keys displayed in full screen', async t => {\n    await apiKeyRequests.addSetKeyApi({\n        keyName,\n        members: [keyValue],\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Save keys table size before switching to full screen\n    const widthKeysBeforeFullScreen = await browserPage.keyListTable.clientWidth;\n    // Open full mode for key details\n    await t.click(browserPage.fullScreenModeButton);\n    // Close key details\n    await t.click(browserPage.closeKeyButton);\n    // Check that key list is opened in full screen\n    const widthTableAfterFullScreen = await browserPage.keyListTable.clientWidth;\n    await t.expect(widthTableAfterFullScreen).gt(widthKeysBeforeFullScreen, 'Width after switching to full screen not greater then before');\n    // Verify that when user selects the key while key list is in full screen, key details is opened on the right side panel\n    const widthKeysBeforeExitFullScreen = await browserPage.keyListTable.clientWidth;\n    await browserPage.openKeyDetails(keyName);\n    const widthKeysAfterExitFullScreen = await browserPage.keyListTable.clientWidth;\n    await t.expect(widthKeysAfterExitFullScreen).lt(widthKeysBeforeExitFullScreen, 'Width after switching from full screen not less then before');\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Key details not opened');\n});\ntest('Verify that when users close key details not in full mode, they can see full key list screen', async t => {\n    await apiKeyRequests.addHashKeyApi({\n        keyName,\n        ttl: 58965422,\n        fields: [{\n            field: 'field',\n            value: 'value',\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Save key list table size before switching to full screen\n    const widthKeysBeforeFullScreen = await browserPage.keyListTable.clientWidth;\n    // Close key details\n    await t.click(browserPage.closeKeyButton);\n    // Check that key list is opened in full screen\n    const widthTableAfterFullScreen = await browserPage.keyListTable.clientWidth;\n    await t.expect(widthTableAfterFullScreen).gt(widthKeysBeforeFullScreen, 'Width after switching to full screen not greater then before');\n    // Verify that user can not see key details\n    await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).visible).notOk('Key details not opened');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/handle-dbsize-permissions.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneNoPermissionsConfig\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst createUserCommand = 'acl setuser noperm nopass on +@all ~* -dbsize';\nconst keyName = Common.generateWord(20);\nconst createKeyCommand = `set ${keyName} ${Common.generateWord(20)}`;\nconst noPermDatabase = {\n    ...ossStandaloneBigConfig,\n    databaseName: ossStandaloneNoPermissionsConfig.databaseName,\n}\n\nfixture `Handle user permissions`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        await browserPage.Cli.sendCommandInCli(createUserCommand);\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(noPermDatabase);\n        await browserPage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(noPermDatabase);\n    });\n\ntest('Verify that user without dbsize permissions can connect to DB', async t => {\n    // Connect to DB\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneNoPermissionsConfig.databaseName);\n    // Check that user can see total number of key is overview\n    await t.expect(browserPage.OverviewPanel.overviewTotalKeys.find('div').withExactText('18 M').exists).ok('Total keys are not displayed');\n    // Check that user can see total number of keys in browser\n    await t.expect(browserPage.keysSummary.withText('18 00').exists).ok('Total number is not displayed');\n    // Check that user can search per key\n    await browserPage.Cli.sendCommandInCli(createKeyCommand);\n    await browserPage.searchByKeyName(keyName);\n    await t.expect(browserPage.keysNumberOfResults.textContent).eql('1', 'Found keys number not correct');\n    await t.expect(browserPage.scannedValue.textContent).contains('18 000', 'Number of scanned not correct');\n    await t.expect(browserPage.totalKeysNumber.textContent).contains('18 000', 'Number of total keys not correct');\n    // Check bulk delete\n    await browserPage.Cli.sendCommandInCli(createKeyCommand);\n    await browserPage.searchByKeyName(keyName);\n    await t.click(browserPage.bulkActionsButton);\n    await browserPage.BulkActions.startBulkDelete();\n    await t.expect(browserPage.BulkActions.bulkStatusCompleted.visible).ok('Bulk deletion is not completed', { timeout: 60000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/hash-field.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { populateHashWithFields } from '../../../../helpers/keys';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port };\nconst keyName = `TestHashKey-${Common.generateWord(10)}`;\nconst fieldForSearch = `SearchField-${Common.generateWord(5)}`;\nconst keyToAddParameters = { fieldsCount: 500000, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' };\n\nfixture(`Hash Key fields verification`)\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await apiKeyRequests.addHashKeyApi({\n            keyName,\n            ttl: 2147476121,\n            fields: [{\n                field: 'field',\n                value: 'value',\n            }]\n        }, ossStandaloneConfig);\n        await browserPage.navigateToKey(keyName);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can search per exact field name in Hash in DB with 1 million of fields', async t => {\n    // Add 1000000 fields to the hash key\n    await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters);\n    await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters);\n    // Add custom field to the hash key\n    await browserPage.openKeyDetails(keyName);\n    await browserPage.addFieldToHash(fieldForSearch, 'testHashValue');\n    // Search by full field name\n    await browserPage.searchByTheValueInKeyDetails(fieldForSearch);\n    // Check the search result\n    const result = await browserPage.hashFieldsList.nth(0).textContent;\n    await t.expect(result).eql(fieldForSearch, 'Hash field not found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/key-messages.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfigEmpty } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst dataTypes: string[] = [\n    'RedisTimeSeries',\n    'RedisGraph'\n];\n\nfixture `Key messages`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n    })\ntest('Verify that user can see updated message in Browser for TimeSeries and Graph data types', async t => {\n    for(let i = 0; i < dataTypes.length; i++) {\n        keyName = Common.generateWord(10);\n        const commands: string[] = [\n            `TS.CREATE ${keyName}`,\n            `GRAPH.QUERY ${keyName} \"CREATE ()\"`\n        ];\n        const messages: string[] = [\n            `This is a ${dataTypes[i]} key`,\n            'Use Redis commands in the ',\n            'Workbench',\n            ' tool to view the value.'\n        ];\n\n        // Add key and verify message in Browser\n        await browserPage.Cli.sendCommandInCli(commands[i]);\n        await browserPage.searchByKeyName(keyName);\n        await t.click(browserPage.keyNameInTheList);\n        for(const message of messages) {\n            await t.expect(browserPage.modulesTypeDetails.textContent).contains(message, `The message for ${dataTypes[i]} key is not displayed`);\n        }\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfigEmpty.databaseName);\n    }\n});\ntest('Verify that user can see link to Workbench under word “Workbench” in the RedisTimeSeries and Graph key details', async t => {\n    for(let i = 0; i < dataTypes.length; i++) {\n        keyName = Common.generateWord(10);\n        const commands: string[] = [\n            `TS.CREATE ${keyName}`,\n            `GRAPH.QUERY ${keyName} \"CREATE ()\"`\n        ];\n\n        // Add key and verify Workbench link\n        await browserPage.Cli.sendCommandInCli(commands[i]);\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await browserPage.searchByKeyName(keyName);\n        await t.click(browserPage.keyNameInTheList);\n        await t.click(browserPage.internalLinkToWorkbench);\n        await t.expect(workbenchPage.queryInput.visible).ok(`The message for ${dataTypes[i]} key is not displayed`);\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfigEmpty.databaseName);\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    cloudDatabaseConfig,\n    commonUrl,\n    ossClusterConfig,\n    ossSentinelConfig,\n    ossStandaloneBigConfig\n} from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserActions = new BrowserActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst verifyKeysAdded = async(): Promise<void> => {\n    keyName = Common.generateWord(10);\n    // Add Hash key\n    await browserPage.addStringKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not correct');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const keyNameInTheList = Selector(`[data-testid=\"key-${keyName}\"]`);\n    await Common.waitForElementNotVisible(browserPage.loader);\n    await t.expect(keyNameInTheList.exists).ok(`${keyName} key is not added`);\n};\n\nfixture `Work with keys in all types of databases`\n    .meta({ type: 'regression' })\n    .page(commonUrl);\ntest\n    .meta({ rte: rte.reCloud })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisCloudDatabase(cloudDatabaseConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, cloudDatabaseConfig.databaseName);\n    })\n    .skip('Verify that user can add Key in RE Cloud DB', async() => {\n        await verifyKeysAdded();\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossClusterConfig.ossClusterDatabaseName);\n    })('Verify that user can add Key in OSS Cluster DB', async() => {\n        await verifyKeysAdded();\n    });\ntest\n    .meta({ rte: rte.sentinel })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await browserPage.deleteKeyByName(keyName);\n    })('Verify that user can add Key in Sentinel Primary Group', async() => {\n        await verifyKeysAdded();\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    })('Verify that user can scroll key virtualized table and see keys info displayed', async() => {\n        // Force switch to list view\n        await t.click(browserPage.browserViewButton);\n        const listItems = browserPage.keysContainer.find(browserPage.cssVirtualTableRow);\n        const maxNumberOfScrolls = 15;\n        let numberOfScrolls = 0;\n\n        // Scroll down the virtualized list 15 times\n        while (numberOfScrolls < maxNumberOfScrolls) {\n            const currentLastRenderedItemIndex = await listItems.count - 1;\n            const currentLastRenderedItemText = await listItems.nth(currentLastRenderedItemIndex).find(browserPage.cssSelectorKey).innerText;\n            const currentLastRenderedItem = listItems.withText(currentLastRenderedItemText);\n\n            await t.scrollIntoView(currentLastRenderedItem);\n            numberOfScrolls++;\n            // Verify that last rendered item name is not empty\n            await t.expect(currentLastRenderedItemText).notEql('', `\"${currentLastRenderedItemText}\" Key name is empty`);\n        }\n\n        // Verify that keys info in row not empty\n        await browserActions.verifyAllRenderedKeysHasText();\n\n        await t.click(browserPage.refreshKeysButton);\n        // Verify that keys info in row not empty after refreshing page\n        await browserActions.verifyAllRenderedKeysHasText();\n\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        // Go to Browser Page\n        await t.click(browserPage.NavigationTabs.browserButton);\n        // Verify that keys info in row not empty after switching between pages\n        await browserActions.verifyAllRenderedKeysHasText();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/large-key-details-values.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst field = Common.generateWord(20);\nconst value = Common.generateSentence(400);\nconst value1 = Common.generateWord(20);\nconst keyName = Common.generateWord(20);\nconst keyTTL = 2147476121;\n\nfixture `Expand/Collapse large values in key details`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        if (await browserPage.closeKeyButton.visible) {\n            await t.click(browserPage.closeKeyButton);\n        }\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can click on a row to expand it if any of its cells contains a value which is truncated.', async t => {\n    const entryFieldLong = browserPage.streamEntryFields.nth(1).parent(1);\n    const entryFieldSmall = browserPage.streamEntryFields.nth(0).parent(1);\n    // Create stream key\n    await browserPage.Cli.sendCommandsInCli([\n        `XADD ${keyName} * '${field}' '${value}'`,\n        `XADD ${keyName} * '${field}' '${value1}'`\n    ]);\n    // Open key details\n    await browserPage.openKeyDetails(keyName);\n    // Remember height of the cells\n    const startLongCellHeight = await entryFieldLong.clientHeight;\n    const startSmallCellHeight = await entryFieldSmall.clientHeight;\n    await t.click(entryFieldSmall);\n    // Verify that field with small text is not expanded\n    await t.expect(entryFieldSmall.clientHeight).lt(startSmallCellHeight + 5, 'Row is expanded', { timeout: 5000 });\n    // Verify that user can expand/collapse for stream data type\n    await t.click(entryFieldLong);\n    await t.expect(entryFieldLong.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 });\n    // Verify that user can collapse the row by clicking anywhere on the expanded row\n    await t.click(entryFieldLong);\n    await t.expect(entryFieldLong.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 });\n});\ntest('Verify that user can expand/collapse for hash data type', async t => {\n    const fieldValueCell = browserPage.hashFieldValue.parent(2);\n    // Create hash key\n    await apiKeyRequests.addHashKeyApi({\n        keyName,\n        ttl: keyTTL,\n        fields: [{\n            field,\n            value,\n        }]\n    }, ossStandaloneConfig)\n    await browserPage.navigateToKey(keyName);\n    // Remember height of the cell with long value\n    const startCellHeight = await fieldValueCell.clientHeight;\n    // Verify that user can expand a row of hash data type\n    await t.click(fieldValueCell);\n    await t.expect(fieldValueCell.clientHeight).gt(startCellHeight + 130, 'Row is not expanded', { timeout: 5000 });\n    // Verify that user can collapse a row of hash data type\n    await t.click(fieldValueCell);\n    await t.expect(fieldValueCell.clientHeight).eql(startCellHeight, 'Row is not collapsed', { timeout: 5000 });\n});\ntest('Verify that user can expand/collapse for set data type', async t => {\n    const memberValueCell = browserPage.setMembersList.parent(2);\n    // Create set key\n    await apiKeyRequests.addSetKeyApi({\n        keyName,\n        ttl: keyTTL,\n        members: [value]\n    }, ossStandaloneConfig)\n    await browserPage.navigateToKey(keyName);\n    // Remember height of the cell with long value\n    const startLongCellHeight = await memberValueCell.clientHeight;\n    // Verify that user can expand a row of set data type\n    await t.click(memberValueCell);\n    await t.expect(memberValueCell.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 });\n    // Verify that user can collapse a row of set data type\n    await t.click(memberValueCell);\n    await t.expect(memberValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 });\n});\ntest('Verify that user can expand/collapse for sorted set data type', async t => {\n    const memberValueCell = browserPage.zsetMembersList.parent(1);\n    // Create zset key\n    await apiKeyRequests.addSortedSetKeyApi({\n        keyName,\n        ttl: keyTTL,\n        members: [{\n            name: value,\n            score: 0,\n        }]\n    }, ossStandaloneConfig)\n    await browserPage.navigateToKey(keyName);\n    // Remember height of the cell with long value\n    const startLongCellHeight = await memberValueCell.clientHeight;\n    // Verify that user can expand a row of sorted set data type\n    await t.click(memberValueCell);\n    await t.expect(memberValueCell.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 });\n    // Verify that user can collapse a row of sorted set data type\n    await t.click(memberValueCell);\n    await t.expect(memberValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 });\n});\ntest('Verify that user can expand/collapse for list data type', async t => {\n    const elementValueCell = browserPage.listElementsList.parent(2);\n    // Create list key\n    await apiKeyRequests.addListKeyApi({\n        keyName,\n        ttl: keyTTL,\n        elements: [value]\n    }, ossStandaloneConfig)\n    await browserPage.navigateToKey(keyName);\n    // Remember height of the cell with long value\n    const startLongCellHeight = await elementValueCell.clientHeight;\n    // Verify that user can expand a row of list data type\n    await t.click(elementValueCell);\n    await t.expect(elementValueCell.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 });\n    // Verify that user can collapse a row of list data type\n    await t.click(elementValueCell);\n    await t.expect(elementValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 });\n});\ntest('Verify that user can work in full mode with expanded/collapsed value', async t => {\n    const elementValueCell = browserPage.listElementsList.parent(2);\n    // Create list key\n    await apiKeyRequests.addListKeyApi({\n        keyName,\n        ttl: keyTTL,\n        elements: [value]\n    }, ossStandaloneConfig)\n    await browserPage.navigateToKey(keyName);\n    // Open full mode for key details\n    await t.click(browserPage.fullScreenModeButton);\n    // Remember height of the cell with long value\n    const startLongCellHeight = await elementValueCell.clientHeight;\n    // Verify that user can expand a row in full mode\n    await t.click(elementValueCell);\n    await t.expect(elementValueCell.clientHeight).gt(startLongCellHeight + 60, 'Row is not expanded', { timeout: 5000 });\n    // Verify that user can collapse a row in full mode\n    await t.click(elementValueCell);\n    await t.expect(elementValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/last-refresh.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `Last refresh`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can see my timer updated when I refresh the list of Keys of the list of values', async t => {\n    keyName = Common.generateWord(10);\n    // Hover on the refresh icon\n    await t.hover(browserPage.refreshKeysButton);\n    // Verify that user can see the date and time of the last update of my Keys in the tooltip\n    await t.expect(browserPage.tooltip.innerText).contains('Last Refresh\\nnow', 'tooltip text not correct');\n\n    // Add key\n    await browserPage.addStringKey(keyName);\n    await browserPage.openKeyDetails(keyName);\n    // Wait for 1 min\n    await t.wait(60000);\n    // Hover on the refresh icon\n    await t.hover(browserPage.refreshKeyButton);\n    await t.expect(browserPage.tooltip.innerText).contains('Last Refresh\\n1 min', 'tooltip text not correct');\n    // Click on Refresh and check last refresh\n    await t.click(browserPage.refreshKeyButton);\n    await t.hover(browserPage.keyDetailsHeader);\n    await t.hover(browserPage.refreshKeyButton);\n    // Verify that user can see the date and time of the last update of my Key values in the tooltip\n    // Verify that user can see my last refresh updated each time I hover over the Refresh icon\n    await t.expect(browserPage.tooltip.innerText).contains('Last Refresh\\nnow', 'tooltip text not correct');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/list-key.e2e.ts",
    "content": "import { AddElementInList, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfigEmpty,\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { populateListWithElements } from '../../../../helpers/keys';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nimport { Telemetry } from '../../../../helpers/telemetry';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\nconst telemetry = new Telemetry();\n\nconst dbParameters = { host: ossStandaloneConfigEmpty.host, port: ossStandaloneConfigEmpty.port };\nconst keyName = `TestListKey-${ Common.generateWord(10) }`;\nconst elementForSearch = `SearchField-${ Common.generateWord(5) }`;\nconst keyToAddParameters = { elementsCount: 500000, keyName, elementStartWith: 'listElement' };\n\nconst telemetryEvent = 'LIST_VIEW_OPENED';\nconst logger = telemetry.createLogger();\n\nconst expectedProperties = [\n    'databaseId',\n    'provider'\n];\n\nfixture `List Key verification`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        await apiKeyRequests.addListKeyApi({\n            keyName,\n            ttl: 2147476121,\n            elements: ['testElement'],\n        }, ossStandaloneConfigEmpty);\n        await browserPage.navigateToKey(keyName);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfigEmpty.databaseName);\n    });\ntest.requestHooks(logger)\n  ('Verify that user can search per exact element index in List key in DB with 1 million of fields', async t => {\n        // Add 1000000 elements to the list key\n        await populateListWithElements(dbParameters.host, dbParameters.port, keyToAddParameters);\n        await populateListWithElements(dbParameters.host, dbParameters.port, keyToAddParameters);\n\n        // Verify that telemetry event 'TREE_VIEW_KEY_VALUE_VIEWED' sent\n        await t.click(browserPage.browserViewButton);\n        await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n\n        // Add custom element to the list key\n        await browserPage.openKeyDetails(keyName);\n        await browserPage.addElementToList([elementForSearch]);\n        // Search by element index\n        await browserPage.searchByTheValueInKeyDetails('1000001');\n        // Check the search result\n        const result = await browserPage.listElementsList.nth(0).textContent;\n        await t.expect(result).eql(elementForSearch, 'List element not found');\n    });\n\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })\n    ('Verify that user can add a multiple fields', async t => {\n\n        const tailKeyName  = 'tailListKey';\n        const headKeyName = 'headKeyName';\n\n        const elementsValue = [\n            'element1',\n            'element2',\n            'element3',\n            'element4'\n        ]\n\n        await browserPage.addListKey(tailKeyName, '2147476121', [elementsValue[0],elementsValue[1], elementsValue[2]]);\n        await browserPage.openKeyDetails(tailKeyName);\n\n        await t.expect(browserPage.listElementsList.nth(0).textContent).eql(elementsValue[0], 'the first element is not corrected for add in tail');\n        let count = await browserPage.listElementsList.count;\n        await t.expect(browserPage.listElementsList.nth(count - 1).textContent).eql(elementsValue[2], 'the last element is not corrected for add in tail');\n\n        await browserPage.addListKey(headKeyName, '2147476121', [elementsValue[0],elementsValue[1], elementsValue[2]], AddElementInList.Head);\n        await browserPage.openKeyDetails(headKeyName);\n\n        await t.expect(browserPage.listElementsList.nth(0).textContent).eql(elementsValue[2], 'the first element is not corrected for add in tail');\n        count = await browserPage.listElementsList.count;\n        await t.expect(browserPage.listElementsList.nth(count - 1).textContent).eql(elementsValue[0], 'the last element is not corrected for add in tail');\n\n    });\ntest('Verify that user can edit a multiple fields', async t => {\n        const elementsValue = [\n            'element1',\n            'element2',\n            'element3',\n            'element4'\n        ]\n        await browserPage.openKeyDetails(keyName);\n\n        await browserPage.addElementToList([elementsValue[0], elementsValue[1]]);\n        await t.expect(browserPage.listElementsList.nth(0).textContent).eql('testElement', 'the first element is not corrected for add in tail');\n        let count = await browserPage.listElementsList.count;\n        await t.expect(browserPage.listElementsList.nth(count - 1).textContent).eql(elementsValue[1], 'the last element is not corrected for add in tail');\n\n        await browserPage.addElementToList([elementsValue[2], elementsValue[3]], AddElementInList.Head);\n        await t.expect(browserPage.listElementsList.nth(0).textContent).eql(elementsValue[3], 'the first element is not corrected for add in head');\n        count = await browserPage.listElementsList.count;\n        await t.expect(browserPage.listElementsList.nth(count - 1).textContent).eql(elementsValue[1], 'the last element is not corrected for add in head');\n    });\ntest('Verify that user can hide fields', async t => {\n    await t.expect(browserPage.getKeySize(keyName).exists).ok('size is not displayed')\n    await t.expect(browserPage.getKeyTTl(keyName).exists).ok('ttl is not displayed')\n\n    await t.click(browserPage.columnsBtn);\n    await t.click(browserPage.showTtlColumnCheckbox);\n    await t.click(browserPage.columnsBtn);\n    await t.expect(browserPage.getKeySize(keyName).exists).ok('size is not displayed')\n    await t.expect(browserPage.getKeyTTl(keyName).exists).notOk('ttl is displayed')\n\n    await t.click(browserPage.columnsBtn);\n    await t.click(browserPage.showSizeColumnCheckbox);\n    await t.click(browserPage.columnsBtn);\n    await t.expect(browserPage.getKeySize(keyName).exists).notOk('size is not displayed')\n    await t.expect(browserPage.getKeyTTl(keyName).exists).notOk('ttl is displayed')\n\n    await t.click(browserPage.columnsBtn);\n    await t.click(browserPage.showSizeColumnCheckbox);\n    await t.click(browserPage.showTtlColumnCheckbox);\n    await t.click(browserPage.columnsBtn);\n    await t.expect(browserPage.getKeySize(keyName).exists).ok('size is not displayed')\n    await t.expect(browserPage.getKeyTTl(keyName).exists).ok('ttl is displayed')\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/onboarding.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl, ossStandaloneConfigEmpty\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { Common } from '../../../../helpers/common';\nimport {\n    MemoryEfficiencyPage,\n    SlowLogPage,\n    WorkbenchPage,\n    PubSubPage,\n    MyRedisDatabasePage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { Telemetry } from '../../../../helpers/telemetry';\nimport { OnboardingCardsDialog } from '../../../../pageObjects/dialogs';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst onboardingCardsDialog = new OnboardingCardsDialog();\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst workBenchPage = new WorkbenchPage();\nconst slowLogPage = new SlowLogPage();\nconst pubSubPage = new PubSubPage();\nconst telemetry = new Telemetry();\nconst databaseHelper = new DatabaseHelper();\n\nconst logger = telemetry.createLogger();\nconst indexName = Common.generateWord(10);\nconst telemetryEvent = 'ONBOARDING_TOUR_FINISHED';\nconst expectedProperties = [\n    'databaseId'\n];\n\nfixture `Onboarding new user tests`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n    })\n    .afterEach(async() => {\n        await browserPage.Cli.sendCommandInCli(`DEL ${indexName}`);\n    });\n// https://redislabs.atlassian.net/browse/RI-4070, https://redislabs.atlassian.net/browse/RI-4067\n// https://redislabs.atlassian.net/browse/RI-4278\ntest\n    .skip('Verify onboarding new user steps', async t => {\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened');\n    // Verify that user can reset onboarding\n    await t.click(onboardingCardsDialog.resetOnboardingBtn);\n    await t.expect(onboardingCardsDialog.showMeAroundButton.visible).ok('onboarding starting is not visible');\n    await onboardingCardsDialog.startOnboarding();\n    // verify browser step is visible\n    await onboardingCardsDialog.verifyStepVisible('Browser');\n    // move to next step\n    await onboardingCardsDialog.clickNextStep();\n    // verify tree view step is visible\n    await onboardingCardsDialog.verifyStepVisible('Tree view');\n    await onboardingCardsDialog.clickNextStep();\n    await onboardingCardsDialog.verifyStepVisible('Filter and search');\n    await onboardingCardsDialog.clickNextStep();\n    // verify cli is opened\n    await t.expect(browserPage.Cli.cliPanel.visible).ok('cli is not expanded');\n    await onboardingCardsDialog.verifyStepVisible('CLI');\n    await onboardingCardsDialog.clickNextStep();\n    // verify command helper area is opened\n    await t.expect(browserPage.CommandHelper.commandHelperArea.visible).ok('command helper is not expanded');\n    await onboardingCardsDialog.verifyStepVisible('Command Helper');\n    await onboardingCardsDialog.clickNextStep();\n    // verify profiler is opened\n    await t.expect(browserPage.Profiler.monitorArea.visible).ok('profiler is not expanded');\n    await onboardingCardsDialog.verifyStepVisible('Profiler');\n    await onboardingCardsDialog.clickNextStep();\n    // Verify that client list command visible when there is not any index created\n    await t.expect(onboardingCardsDialog.wbOnbardingCommand.withText('CLIENT LIST').visible).ok('CLIENT LIST command is not visible');\n    await t.expect(onboardingCardsDialog.copyCodeButton.visible).ok('copy code button is not visible');\n    // verify workbench page is opened\n    await t.expect(workBenchPage.mainEditorArea.visible).ok('workbench is not opened');\n    await onboardingCardsDialog.verifyStepVisible('Try Workbench!');\n    // create index in order to see in FT.INFO {index} in onboarding step\n    await browserPage.Cli.sendCommandInCli(`FT.CREATE ${indexName} ON HASH PREFIX 1 test SCHEMA \"name\" TEXT`);\n    // click back step button\n    await onboardingCardsDialog.clickBackStep();\n    // create index in order to see in FT.INFO {index} in onboarding step\n    await workBenchPage.Cli.sendCommandInCli(`FT.CREATE ${indexName} ON HASH PREFIX 1 test SCHEMA \"name\" TEXT`);\n    // verify one step before is opened\n    await t.expect(browserPage.Profiler.monitorArea.visible).ok('profiler is not expanded');\n    await onboardingCardsDialog.verifyStepVisible('Profiler');\n    await onboardingCardsDialog.clickNextStep();\n    // verify workbench page is opened\n    await t.expect(onboardingCardsDialog.wbOnbardingCommand.withText(`FT.INFO ${indexName}`).visible).ok(`FT.INFO ${indexName} command is not visible`);\n    await t.expect(onboardingCardsDialog.copyCodeButton.visible).ok('copy code button is not visible');\n    await t.expect(workBenchPage.mainEditorArea.visible).ok('workbench is not opened');\n    await onboardingCardsDialog.verifyStepVisible('Try Workbench!');\n    await onboardingCardsDialog.clickNextStep();\n    await onboardingCardsDialog.verifyStepVisible('Explore and learn more');\n    await onboardingCardsDialog.clickNextStep();\n    await onboardingCardsDialog.verifyStepVisible('Upload your tutorials');\n    await onboardingCardsDialog.clickNextStep();\n    // verify analysis tools page is opened\n    await t.expect(memoryEfficiencyPage.noReportsText.visible).ok('analysis tools is not opened');\n    await onboardingCardsDialog.verifyStepVisible('Database Analysis');\n    await onboardingCardsDialog.clickNextStep();\n    // verify slow log is opened\n    await t.expect(slowLogPage.slowLogConfigureButton.visible).ok('slow log is not opened');\n    await onboardingCardsDialog.verifyStepVisible('Slow Log');\n    await onboardingCardsDialog.clickNextStep();\n    // verify pub/sub page is opened\n    await t.expect(pubSubPage.subscribeButton.visible).ok('pub/sub page is not opened');\n    await onboardingCardsDialog.verifyStepVisible('Pub/Sub');\n    await onboardingCardsDialog.clickNextStep();\n    // verify last step of onboarding process is visible\n    await onboardingCardsDialog.verifyStepVisible('Great job!');\n    await onboardingCardsDialog.clickNextStep();\n    // verify onboarding step completed successfully\n    await onboardingCardsDialog.completeOnboarding();\n    await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened');\n});\n// https://redislabs.atlassian.net/browse/RI-4067, https://redislabs.atlassian.net/browse/RI-4278\ntest\n    .skip('Verify onboard new user skip tour', async(t) => {\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened');\n    // Verify that user can reset onboarding\n    await t.click(onboardingCardsDialog.resetOnboardingBtn);\n    await t.expect(onboardingCardsDialog.showMeAroundButton.visible).ok('onboarding starting is not visible');\n    // start onboarding process\n    await onboardingCardsDialog.startOnboarding();\n    // verify browser step is visible\n    await onboardingCardsDialog.verifyStepVisible('Browser');\n    // move to next step\n    await onboardingCardsDialog.clickNextStep();\n    // verify tree view step is visible\n    await onboardingCardsDialog.verifyStepVisible('Tree view');\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened');\n    await t.click(onboardingCardsDialog.resetOnboardingBtn);\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Verify that when user reset onboarding, user can see the onboarding triggered when user open the Browser page.\n    await t.expect(onboardingCardsDialog.showMeAroundButton.visible).ok('onboarding starting is not visible');\n    // click skip tour\n    await onboardingCardsDialog.clickSkipTour();\n    // verify onboarding step completed successfully\n    await onboardingCardsDialog.completeOnboarding();\n    await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened');\n    await myRedisDatabasePage.reloadPage();\n    // verify onboarding step still not visible after refresh page\n    await onboardingCardsDialog.completeOnboarding();\n    await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened');\n});\n// https://redislabs.atlassian.net/browse/RI-4305\ntest.requestHooks(logger)\n    .skip('Verify that the final onboarding step is closed when user opens another page', async(t) => {\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.click(onboardingCardsDialog.resetOnboardingBtn);\n    await onboardingCardsDialog.startOnboarding();\n    await onboardingCardsDialog.clickNextUntilLastStep();\n    // Verify last step of onboarding process is visible\n    await onboardingCardsDialog.verifyStepVisible('Great job!');\n    // Go to Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n\n    // Verify that “ONBOARDING_TOUR_FINISHED” event is sent when user opens another page (or close the app)\n    await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n\n    // Go to PubSub page\n    await t.click(browserPage.NavigationTabs.pubSubButton);\n    // Verify onboarding completed successfully\n    await t.expect(onboardingCardsDialog.showMeAroundButton.exists).notOk('Show me around button still visible');\n    await t.expect(onboardingCardsDialog.stepTitle.exists).notOk('Onboarding tooltip still visible');\n    // Go to Browser Page\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Verify onboarding completed successfully\n    await onboardingCardsDialog.completeOnboarding();\n    await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyName = Common.generateWord(10);\nconst longFieldName = Common.generateSentence(20);\nconst keys = [\n    {   type: 'Hash',\n        name: `${keyName}:1`,\n        offsetX: 30,\n        fieldWidthStart: 0,\n        fieldWidthEnd: 0\n    },\n    {\n        type: 'List',\n        name: `${keyName}:2`,\n        offsetX: 20,\n        fieldWidthStart: 0,\n        fieldWidthEnd: 0\n    },\n    {\n        type: 'Zset',\n        name: `${keyName}:3`,\n        offsetX: 10,\n        fieldWidthStart: 0,\n        fieldWidthEnd: 0\n    }\n];\nconst keyNames: string[] = [];\nkeys.forEach(key => keyNames.push(key.name));\nconst databaseName1 = `testDB1_${Common.generateWord(10)}`;\nconst databaseName2 = `testDB2_${Common.generateWord(10)}`;\nconst databasesForAdding = [\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: databaseName1 },\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: databaseName2 }\n];\n\nfixture `Resize columns in Key details`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(databasesForAdding[0]);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(databasesForAdding[1]);\n        await browserPage.addHashKey(keys[0].name, '2147476121', longFieldName, longFieldName);\n        await browserPage.addListKey(keys[1].name, '2147476121', ['element']);\n        await browserPage.addZSetKey(keys[2].name, '1', '2147476121', 'member');\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await browserPage.OverviewPanel.changeDbIndex(0);\n        await browserPage.deleteKeysByNames(keyNames);\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest\n    .skip('Resize of columns in Hash, List, Zset Key details', async t => {\n    const field = browserPage.keyDetailsTable.find(browserPage.cssRowInVirtualizedTable);\n    const tableHeaderResizeTrigger = browserPage.resizeTrigger;\n\n    for(const key of keys) {\n        await browserPage.openKeyDetails(key.name);\n        // Remember initial column width\n        key.fieldWidthStart = await field.clientWidth;\n        await t.hover(tableHeaderResizeTrigger);\n        await t.drag(tableHeaderResizeTrigger, -key.offsetX, 0, { speed: 0.1 });\n        // Remember last column width\n        key.fieldWidthEnd = await field.clientWidth;\n        // Verify that user can resize columns for Hash, List, Zset Keys\n        await t.expect(key.fieldWidthEnd).within(key.fieldWidthStart - key.offsetX - 5, key.fieldWidthStart - key.offsetX + 5, `Field is not resized for ${key.type} key`);\n    }\n\n    // Verify that resize saved when switching between pages\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.click(browserPage.NavigationTabs.browserButton);\n    await browserPage.openKeyDetails(keys[0].name);\n    await t.expect(field.clientWidth).within(keys[0].fieldWidthEnd - 5, keys[0].fieldWidthEnd + 5, 'Resize context not saved for key when switching between pages');\n\n    // Apply filter to save it in filter history\n    await browserPage.searchByKeyName(`${keys[0].name}*`);\n\n    // Verify that resize saved when switching between databases\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Go to 2nd database\n    await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName);\n    // Verify that resize saved for specific data type\n    for (const key of keys) {\n        await browserPage.openKeyDetails(key.name);\n        await t.expect(field.clientWidth).within(key.fieldWidthEnd - 5, key.fieldWidthEnd + 5, `Resize context not saved for ${key.type} key when switching between databases`);\n    }\n\n    // Change db index for 2nd database\n    await browserPage.OverviewPanel.changeDbIndex(1);\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Go back to 1st database\n    await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName);\n    // Verify that user can see the list of filters even when switching between databases\n    await t.click(browserPage.showFilterHistoryBtn);\n    await t.expect(browserPage.filterHistoryOption.withText(keys[0].name).exists).ok('Filter history requests not saved after switching between db');\n\n    // Verify that logical db not changed after switching between databases\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName);\n    await browserPage.OverviewPanel.verifyDbIndexSelected(1);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/scan-keys.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage, SettingsPage } from '../../../../pageObjects';\nimport { commonUrl } from '../../../../helpers/conf';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\n\nconst explicitErrorHandler = (): void => {\n    window.addEventListener('error', e => {\n        if(e.message === 'ResizeObserver loop limit exceeded') {\n            e.stopImmediatePropagation();\n        }\n    });\n};\n\nfixture `Browser - Specify Keys to Scan`\n    .meta({ type: 'regression', rte: rte.none })\n    .page(commonUrl)\n    .clientScripts({ content: `(${explicitErrorHandler.toString()})()` })\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .afterEach(async() => {\n        await settingsPage.changeKeysToScanValue('10000');\n    });\ntest('Verify that the user not enter the value less than 500 - the system automatically applies min value if user enters less than min', async t => {\n    // Go to Settings page\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Specify keys to scan less than 500\n    await t.click(settingsPage.accordionAdvancedSettings);\n    await settingsPage.changeKeysToScanValue('100');\n    // Verify the applied scan value\n    await t.expect(await settingsPage.keysToScanValue.textContent).eql('500', 'The system automatically not applies min value 500');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/set-key.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { populateSetWithMembers } from '../../../../helpers/keys';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port };\nconst keyName = `TestSetKey-${ Common.generateWord(10) }`;\nconst memberForSearch = `SearchField-${ Common.generateWord(5) }`;\nconst keyToAddParameters = { membersCount: 500000, keyName, memberStartWith: 'setMember' };\n\nfixture `Set Key verification`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await apiKeyRequests.addSetKeyApi({\n            keyName,\n            ttl: 2147476121,\n            members: ['testMember']\n        }, ossStandaloneConfig);\n        await browserPage.navigateToKey(keyName);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can search per exact member name in Set key in DB with 1 million of members', async t => {\n    // Add 1000000 members to the set key\n    await populateSetWithMembers(dbParameters.host, dbParameters.port, keyToAddParameters);\n    await populateSetWithMembers(dbParameters.host, dbParameters.port, keyToAddParameters);\n    // Add custom member to the set key\n    await browserPage.openKeyDetails(keyName);\n    await browserPage.addMemberToSet(memberForSearch);\n    // Search by full member name\n    await browserPage.searchByTheValueInSetKey(memberForSearch);\n    // Check the search result\n    const result = await browserPage.setMembersList.nth(0).textContent;\n    await t.expect(result).eql(memberForSearch, 'Set member not found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/stream-key.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst value = Common.generateWord(5);\nlet field = Common.generateWord(5);\nlet keyName = Common.generateWord(20);\n\nfixture `Stream key`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can see a Stream in a table format', async t => {\n    const streamFields = [\n        'Entry ID',\n        field\n    ];\n    keyName = Common.generateWord(20);\n    const command = `XADD ${keyName} * '${field}' '${value}'`;\n\n    // Add new Stream key with 5 EntryIds\n    for(let i = 0; i < 5; i++){\n        await browserPage.Cli.sendCommandInCli(command);\n    }\n    // Open key details and check Steam format\n    await browserPage.openKeyDetails(keyName);\n    await t.expect(browserPage.streamEntriesContainer.visible).ok('The Stream is not displayed in a table format');\n    for(const field of streamFields){\n        await t.expect(browserPage.streamEntriesContainer.textContent).contains(field, 'The Stream fields are not displayed in the table');\n    }\n    await t.expect(browserPage.streamFieldsValues.textContent).contains(value, 'The Stream field value is not displayed in the table');\n});\ntest('Verify that user can sort ASC/DESC by Entry ID', async t => {\n    keyName = Common.generateWord(20);\n    const command = `XADD ${keyName} * '${field}' '${value}'`;\n\n    // Add new Stream key with 5 EntryIds\n    for(let i = 0; i < 5; i++){\n        await browserPage.Cli.sendCommandInCli(command);\n        await t.wait(1000);\n    }\n    // Open key details and check Entry ID ASC sorting\n    await browserPage.openKeyDetails(keyName);\n    const entryCount = await browserPage.streamEntryDate.count;\n    for(let i = 0; i < entryCount - 1; i++){\n        const entryDateFirstAsc = Date.parse(await browserPage.streamEntryDate.nth(i).textContent);\n        const entryDateSecondAsc = Date.parse(await browserPage.streamEntryDate.nth(i + 1).textContent);\n        await t.expect(entryDateFirstAsc).gt(entryDateSecondAsc, 'By default the table is not sorted by Entry ID');\n    }\n    // Check the DESC sorting\n    await t.click(browserPage.sortingButton);\n    for(let i = 0; i < entryCount - 1; i++){\n        const entryDateFirstDesc = Date.parse(await browserPage.streamEntryDate.nth(i).textContent);\n        const entryDateSecondDesc = Date.parse(await browserPage.streamEntryDate.nth(i + 1).textContent);\n        await t.expect(entryDateFirstDesc).lt(entryDateSecondDesc, 'The Stream fields are not sorted DESC by Entry ID');\n    }\n});\ntest('Verify that user can see all the columns are displayed by default for Stream', async t => {\n    keyName = Common.generateWord(20);\n    const fields = [\n        'Pressure',\n        'Humidity',\n        'Temperature'\n    ];\n    const values = [\n        '234',\n        '78',\n        '27'\n    ];\n\n    // Add new Stream key with 3 fields\n    for(let i = 0; i < fields.length; i++){\n        await browserPage.Cli.sendCommandInCli(`XADD ${keyName} * ${fields[i]} ${values[i]}`);\n    }\n    // Open key details and check fields\n    await browserPage.openKeyDetails(keyName);\n    await t.click(browserPage.fullScreenModeButton);\n    for(let i = fields.length - 1; i <= 0; i--){\n        const fieldName = await browserPage.streamFields.nth(i).textContent;\n        await t.expect(fieldName).eql(fields[i], 'All the columns are not displayed by default for Stream');\n    }\n    await t.click(browserPage.fullScreenModeButton);\n});\ntest('Verify that the multi-line cell value tooltip is available on hover as per standard key details behavior', async t => {\n    keyName = Common.generateWord(20);\n    const fields = [\n        'Pressure',\n        'Humidity'\n    ];\n    const entryValue = Common.generateSentence(5);\n\n    // Add new Stream key with multi-line cell value\n    for(let i = 0; i < fields.length; i++){\n        await browserPage.Cli.sendCommandInCli(`XADD ${keyName} * '${fields[i]}' '${entryValue}'`);\n    }\n    // Open key details and check tooltip\n    await browserPage.openKeyDetails(keyName);\n    await t.hover(browserPage.streamEntryFields);\n    await t.expect(browserPage.tooltip.textContent).contains(entryValue, 'The multi-line cell value tooltip is not available');\n});\ntest('Verify that user can see a confirmation message when request to delete an entry in the Stream', async t => {\n    keyName = Common.generateWord(20);\n    field = 'fieldForRemoving';\n    const confirmationMessage = `will be removed from ${keyName}`;\n\n    // Add new Stream key with 1 field\n    await browserPage.Cli.sendCommandInCli(`XADD ${keyName} * ${field} ${value}`);\n    // Open key details and click on delete entry\n    await browserPage.openKeyDetails(keyName);\n    const entryId = await browserPage.streamEntryIdValue.textContent;\n    await t.click(browserPage.removeEntryButton);\n    // Check the confirmation message\n    await t.expect(browserPage.confirmationMessagePopover.textContent).contains(confirmationMessage, `The confirmation message ${keyName} not displayed`);\n    await t.expect(browserPage.confirmationMessagePopover.textContent).contains(entryId, 'The confirmation message for removing Entry not displayed');\n});\ntest('Verify that the Entry ID field, Delete button are always displayed while scrolling for Stream data', async t => {\n    keyName = Common.generateWord(20);\n    const fields = Common.createArrayWithKeys(9);\n    const values = Common.createArrayWithKeys(9);\n\n    const commands: string[] = [];\n    // Add new Stream key with 3 fields\n    for (let i = 0; i < fields.length; i++) {\n        commands.push(`XADD ${keyName} * ${fields[i]} ${values[i]}`);\n    }\n    await browserPage.Cli.sendCommandsInCli(commands);\n    // Open key details\n    await browserPage.openKeyDetails(keyName);\n    // Scroll right\n    await t.pressKey('shift').scroll(browserPage.streamVirtualContainer, 'right');\n    // Verify that Entry ID field and Delete button are always displayed\n    await t.expect(browserPage.streamFieldsValues.withText(fields[2]).visible).ok(`The Stream field ${fields[2]} is not visible`)\n        .expect(browserPage.removeEntryButton.visible).ok('Delete icon is not visible')\n        .expect(browserPage.streamEntryDate.visible).ok('Entry ID column is not visible');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/stream-pending-messages.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(20);\nlet consumerGroupName = Common.generateWord(20);\n\nfixture `Pending messages`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        if (await browserPage.closeKeyButton.visible){\n            await t.click(browserPage.closeKeyButton);\n        }\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can\\'t select currently selected Consumer to Claim message in the drop-down', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const consumerNames = [\n        'Alice',\n        'Bob'\n    ];\n    const cliCommandsForStream = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} ${consumerNames[0]} COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName}  ${consumerNames[1]} COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommandsForStream);\n    // Open Stream pending view\n    await browserPage.openStreamPendingsView(keyName);\n    // Click on Claim message and check result\n    await t.click(browserPage.claimPendingMessageButton);\n    await t.click(browserPage.consumerDestinationSelect);\n    await t.expect(browserPage.consumerOption.textContent).notContains(consumerNames[0], 'The currently selected Consumer is in the drop-down');\n});\n// todo: enable after RI-7447 will be fixed\ntest.skip('Verify that the message is claimed only if its idle time is greater than the Min Idle Time', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName} Bob COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommands);\n    // Open Stream pendings view\n    await browserPage.openStreamPendingsView(keyName);\n    const streamMessageBefore = await browserPage.streamMessage.count;\n    // Claim message and check result when Min Idle Time is greater than the idle time\n    await t.click(browserPage.claimPendingMessageButton);\n    await t.typeText(browserPage.streamMinIdleTimeInput, '100000000', { replace: true, paste: true });\n    await t.click(browserPage.submitButton);\n    await t.expect(browserPage.Toast.toastHeader.textContent).contains('No messages claimed', 'The message is not claimed notification');\n    await t.expect(browserPage.streamMessage.count).eql(streamMessageBefore, 'The number of pendings in the table not correct');\n});\n// todo: enable after RI-7447 will be fixed\ntest.skip('Verify that when user toggle optional parameters on, he can see optional fields', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const cliCommands = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XADD ${keyName} * message orange`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`,\n        `XREADGROUP GROUP ${consumerGroupName} Bob COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommands);\n    // Open Stream pendings view\n    await browserPage.openStreamPendingsView(keyName);\n    // Click Claim message with optional parameters and check fields\n    await t.click(browserPage.claimPendingMessageButton);\n    await t.click(browserPage.optionalParametersSwitcher);\n    await t.expect(browserPage.claimIdleTimeInput.visible).ok('The Idle Time field is not displayed in optional parameters');\n    await t.expect(browserPage.claimRetryCountInput.visible).ok('The Retry Count field is not displayed in optional parameters');\n    await t.expect(browserPage.claimTimeOptionSelect.visible).ok('The Idle Time Format is not displayed in optional parameters');\n    await t.expect(browserPage.forceClaimCheckbox.visible).ok('The Force Claim is not displayed in optional parameters');\n    await t.click(browserPage.claimTimeOptionSelect);\n    await t.expect(browserPage.relativeTimeOption.textContent).eql('Relative Time', 'The first option in the time format select list not displayed');\n    await t.expect(browserPage.timestampOption.textContent).eql('Timestamp', 'The second option in the time format select list not displayed');\n});\ntest('Verify that user see the column names in the Pending messages table and navigate by tabs', async t => {\n    keyName = Common.generateWord(20);\n    consumerGroupName = Common.generateWord(20);\n    const columns = [\n        'Entry ID',\n        'Last Message Delivered',\n        'Times Message Delivered'\n    ];\n    const cliCommandsForStream = [\n        `XGROUP CREATE ${keyName} ${consumerGroupName} $ MKSTREAM`,\n        `XADD ${keyName} * message apple`,\n        `XREADGROUP GROUP ${consumerGroupName} Alice COUNT 1 STREAMS ${keyName} >`\n    ];\n\n    // Add New Stream Key with pending message\n    await browserPage.Cli.sendCommandsInCli(cliCommandsForStream);\n    // Open Stream pendings view and check columns\n    await browserPage.openStreamPendingsView(keyName);\n    // Click Claim message with optional parameters and check fields\n    for(const column of columns){\n        await t.expect(browserPage.streamMessagesContainer.textContent).contains(column, `The column name ${column} not correct`);\n    }\n    // Check navigation\n    await t.click(browserPage.streamTabConsumers);\n    await t.expect(browserPage.scoreButton.textContent).eql('Consumer Name', 'The Conusmer view is not opened');\n    await t.click(browserPage.streamTabGroups);\n    await t.expect(browserPage.scoreButton.textContent).eql('Group Name', 'The Consumer Groups view is not opened');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/survey-link.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { goBackHistory } from '../../../../helpers/utils';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst externalPageLink = 'https://www.surveymonkey.com/r/redisinsight';\n\nfixture `User Survey`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can use survey link', async t => {\n    // Verify that user can see survey link on any page inside of DB\n    // Browser page\n    await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed');\n\n    await t.click(browserPage.userSurveyLink);\n    // Verify that when users click on RI survey, they are redirected to https://www.surveymonkey.com/r/redisinsight\n    await Common.checkURL(externalPageLink);\n    await goBackHistory();\n    // Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed');\n    // Slow Log page\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed');\n    // PubSub page\n    await t.click(browserPage.NavigationTabs.pubSubButton);\n    await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed');\n    // Verify that user cannot see survey link for list of databases page\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await t.expect(browserPage.userSurveyLink.exists).notOk('Survey Link is visible');\n    // Verify that user cannot see survey link for welcome page\n    await databaseAPIRequests.deleteAllDatabasesApi();\n    await browserPage.reloadPage();\n    await t.expect(browserPage.userSurveyLink.exists).notOk('Survey Link is visible');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/ttl-format.e2e.ts",
    "content": "import { Selector } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { deleteKeysViaCli, keyTypes } from '../../../../helpers/keys';\nimport { rte, COMMANDS_TO_CREATE_KEY, keyLength } from '../../../../helpers/constants';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyName = Common.generateWord(20);\nconst keysData = keyTypes.map(object => ({ ...object })).slice(0, 6);\nfor (const key of keysData) {\n    key.keyName = `${key.keyName}` + '-' + `${Common.generateWord(keyLength)}`;\n}\n// Arrays with TTL in seconds, min, hours, days, months, years and their values in Browser Page\nconst ttlForSet = [59, 800, 20000, 2000000, 31000000, 2147483647];\nconst ttlValues = ['s', '13 min', '5 h', '23 d', '11 mo', '68 yr'];\n\nfixture `TTL values in Keys Table`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await deleteKeysViaCli(keysData);\n    });\ntest('Verify that user can see TTL in the list of keys rounded down to the nearest unit', async t => {\n    // Create new keys with TTL\n    await t.click(browserPage.Cli.cliExpandButton);\n    for (let i = 0; i < keysData.length; i++) {\n        await t.typeText(browserPage.Cli.cliCommandInput, COMMANDS_TO_CREATE_KEY[keysData[i].textType](keysData[i].keyName), { replace: true, paste: true })\n            .pressKey('enter')\n            .typeText(browserPage.Cli.cliCommandInput, `EXPIRE ${keysData[i].keyName} ${ttlForSet[i]}`, { replace: true, paste: true })\n            .pressKey('enter');\n    }\n    await t.click(browserPage.Cli.cliCollapseButton);\n    // Refresh Keys in Browser\n    await t.click(browserPage.refreshKeysButton);\n    // Check that Keys has correct TTL value in keys table\n    for (let i = 0; i < keysData.length; i++) {\n        const ttlValueElement = Selector(`[data-testid=\"ttl-${keysData[i].keyName}\"]`);\n        await t.expect(ttlValueElement.textContent).contains(ttlValues[i], `TTL value in keys table is not ${ttlValues[i]}`);\n    }\n});\ntest\n    .after(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that Key is deleted if TTL finishes', async t => {\n        // Create new key with TTL\n        const TTL = 15;\n        let ttlToCompare = TTL;\n        await browserPage.addStringKey(keyName, 'test', TTL.toString());\n        await t.click(browserPage.refreshKeysButton);\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Key not added');\n        // Specify selector with TTL\n        const ttlValueElement = Selector(`[data-testid=\"ttl-${keyName}\"]`);\n        // Check that TTL reduces every page refresh\n        while (await browserPage.isKeyIsDisplayedInTheList(keyName)) {\n            const actualTTL = Number((await ttlValueElement.innerText).slice(0, -2));\n            await t.expect(actualTTL).lte(ttlToCompare, 'Wrong TTL displayed');\n            await t.click(browserPage.refreshKeysButton);\n            ttlToCompare = actualTTL;\n        }\n        // Check that key with finished TTL is deleted\n        await t.click(browserPage.refreshKeysButton);\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk('Key is still displayed');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/browser/upload-json-key.e2e.ts",
    "content": "import * as path from 'path';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst filePath = path.join('..', '..', '..', '..', 'test-data', 'upload-json', 'sample.json');\nconst jsonValues = ['Live JSON generator', '3.1', '\"2014-06-25T00:00:00.000Z\"', 'true'];\nconst keyName = Common.generateWord(10);\n\nfixture `Upload json file`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await browserPage.Cli.sendCommandInCli(`DEL ${keyName}`);\n    });\n// https://redislabs.atlassian.net/browse/RI-4061\ntest('Verify that user can insert a JSON from .json file on the form to add a JSON key', async t => {\n    await t.click(browserPage.plusAddKeyButton);\n    await t.click(browserPage.keyTypeDropDown);\n    await t.click(browserPage.jsonOption);\n    await t.click(browserPage.addKeyNameInput);\n    await t.typeText(browserPage.addKeyNameInput, keyName, { replace: true, paste: true });\n    await t.setFilesToUpload(browserPage.jsonUploadInput, [filePath]);\n    await t.click(browserPage.addKeyButton);\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The key added notification not found');\n    // Verify that user can see the JSON value populated from the file when the insert is successful.\n    for (const el of jsonValues) {\n        await t.expect(browserPage.jsonScalarValue.withText(el).exists).ok(`${el} is not visible, JSON value not correct`);\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/cli/cli-all-db-types.e2e.ts",
    "content": "import { Selector, t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport {\n    cloudDatabaseConfig,\n    commonUrl, ossClusterConfig,\n    ossSentinelConfig\n} from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst verifyCommandsInCli = async(): Promise<void> => {\n    keyName = Common.generateWord(10);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Add key from CLI\n    await t.typeText(browserPage.Cli.cliCommandInput, `SADD ${keyName} \"chinese\" \"japanese\" \"german\"`, { replace: true, paste: true });\n    await t.pressKey('enter');\n    // Check that the key is added\n    await browserPage.searchByKeyName(keyName);\n    const keyNameInTheList = Selector(`[data-testid=\"key-${keyName}\"]`);\n    await Common.waitForElementNotVisible(browserPage.loader);\n    await t.expect(keyNameInTheList.exists).ok(`${keyName} key is not added`);\n};\n\nfixture `Work with CLI in all types of databases`\n    .meta({ type: 'regression' })\n    .page(commonUrl);\ntest('Verify that user can add data via CLI in RE Cloud DB', async() => {\n        // Verify that database index switcher not displayed for RE Cloud\n        await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).notOk('Change Db index control displayed for RE Cloud DB');\n\n        await verifyCommandsInCli();\n    }).skip\n    .meta({ rte: rte.reCloud, skipComment: \"Unstable in CI, assertion error, needs investigation\" })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisCloudDatabase(cloudDatabaseConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, cloudDatabaseConfig.databaseName);\n        await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName);\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossClusterConfig.ossClusterDatabaseName);\n        await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig);\n    })('Verify that user can add data via CLI in OSS Cluster DB', async() => {\n        // Verify that database index switcher not displayed for RE Cloud\n        await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).notOk('Change Db index control displayed for OSS Cluster DB');\n\n        await verifyCommandsInCli();\n    });\ntest\n    .meta({ rte: rte.sentinel })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig);\n    })\n    .after(async() => {\n        // Clear and delete database\n        await browserPage.deleteKeyByName(keyName);\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL');\n    })('Verify that user can add data via CLI in Sentinel Primary Group', async() => {\n        // Verify that database index switcher displayed for Sentinel\n        await t.expect(browserPage.OverviewPanel.changeIndexBtn.exists).ok('Change Db index control not displayed for Sentinel DB');\n\n        await verifyCommandsInCli();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/cli/cli-command-helper.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { Common } from '../../../../helpers/common';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { goBackHistory } from '../../../../helpers/utils';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet filteringGroup = '';\nlet filteringGroups: string[] = [];\nlet commandToCheck = '';\nlet commandsToCheck: string[] = [];\nlet commandArgumentsToCheck = '';\nlet commandsArgumentsToCheck: string[] = [];\nlet externalPageLink = '';\nlet externalPageLinks: string[] = [];\n\n// todo: uncomment when RI-7447 will be fixed\nfixture.skip`CLI Command helper`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async () => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can open/close CLI separately from Command Helper', async t => {\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Verify that CLI is opened separately\n    await t.expect(browserPage.CommandHelper.commandHelperArea.visible).notOk('Command Helper is not closed');\n    await t.expect(browserPage.Cli.cliCollapseButton.visible).ok('CLI is not opended');\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Verify that user can close CLI separately\n    await t.click(browserPage.Cli.cliCollapseButton);\n    await t.expect(browserPage.CommandHelper.commandHelperArea.visible).ok('Command Helper is not displayed');\n    await t.expect(browserPage.Cli.cliCollapseButton.visible).notOk('CLI is not closed');\n\n    // Verify that user can open/close Command Helper separately from CLI\n    await t.expect(browserPage.CommandHelper.commandHelperArea.visible).ok('Command Helper is not opened');\n    await t.expect(browserPage.Cli.cliCollapseButton.visible).notOk('CLI is not closed');\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Verify that Command Helper is closed separately\n    await t.click(browserPage.CommandHelper.closeCommandHelperButton);\n    await t.expect(browserPage.CommandHelper.commandHelperArea.visible).notOk('Command Helper is not closed');\n    await t.expect(browserPage.Cli.cliCollapseButton.visible).ok('CLI is not opended');\n});\ntest('Verify that user can see that Command Helper is minimized when he clicks the \"minimize\" button', async t => {\n    const helperColourBefore = await Common.getBackgroundColour(browserPage.CommandHelper.commandHelperBadge);\n    // Open Command Helper and minimize\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    await t.click(browserPage.CommandHelper.minimizeCommandHelperButton);\n    // Verify Command helper is minimized\n    const helperColourAfter = await Common.getBackgroundColour(browserPage.CommandHelper.commandHelperBadge);\n    await t.expect(helperColourAfter).notEql(helperColourBefore, 'Command helper badge colour is not changed');\n    await t.expect(browserPage.Cli.minimizeCliButton.visible).eql(false, 'Command helper is not mimized');\n});\ntest('Verify that user can see that Command Helper displays the previous information when he re-opens it', async t => {\n    filteringGroup = 'Search';\n    commandToCheck = 'FT.EXPLAIN';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from the list\n    await browserPage.CommandHelper.selectFilterGroupType(filteringGroup);\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck));\n    // Minimize and re-open Command Helper\n    await t.click(browserPage.CommandHelper.minimizeCommandHelperButton);\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Verify Command helper information\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).contains(commandToCheck, 'Command Helper information not persists after reopening');\n});\ntest('Verify that user can see in Command helper and click on new group \"JSON\", can choose it and see list of commands in the group', async t => {\n    filteringGroup = 'JSON';\n    commandToCheck = 'JSON.SET';\n    commandArgumentsToCheck = 'JSON.SET key path value [condition]';\n    externalPageLink = 'https://redis.io/docs/latest/commands/json.set/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from the list\n    await browserPage.CommandHelper.selectFilterGroupType(filteringGroup);\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck));\n    // Verify results of opened command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct');\n\n    // Click on Read More link for selected command\n    await t.click(browserPage.CommandHelper.readMoreButton);\n    // Check new opened window page with the correct URL\n    await Common.checkURL(externalPageLink);\n});\ntest('Verify that user can see in Command helper and click on new group \"Search\", can choose it and see list of commands in the group', async t => {\n    filteringGroup = 'Search';\n    commandToCheck = 'FT.EXPLAIN';\n    commandArgumentsToCheck = 'FT.EXPLAIN index query [dialect]';\n    externalPageLink = 'https://redis.io/docs/latest/commands/ft.explain/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from the list\n    await browserPage.CommandHelper.selectFilterGroupType(filteringGroup);\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck));\n    // Verify results of opened command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct');\n\n    // Click on Read More link for selected command\n    await t.click(browserPage.CommandHelper.readMoreButton);\n    // Check new opened window page with the correct URL\n    await Common.checkURL(externalPageLink);\n});\ntest('Verify that user can see HyperLogLog title in Command Helper for this command group', async t => {\n    filteringGroup = 'HyperLogLog';\n    commandToCheck = 'PFCOUNT';\n    commandArgumentsToCheck = 'PFCOUNT key [key ...]';\n    externalPageLink = 'https://redis.io/docs/latest/commands/pfcount/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from the list\n    await browserPage.CommandHelper.selectFilterGroupType(filteringGroup);\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck));\n    // Verify results of opened command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct');\n\n    // Click on Read More link for selected command\n    await t.click(browserPage.CommandHelper.readMoreButton);\n    // Check new opened window page with the correct URL\n    await Common.checkURL(externalPageLink);\n});\ntest('Verify that user can work with Gears group in Command Helper (RedisGears module)', async t => {\n    filteringGroup = 'Gears';\n    commandToCheck = 'RG.GETEXECUTION';\n    commandArgumentsToCheck = 'RG.GETEXECUTION id [SHARD|CLUSTER]';\n    // externalPageLink = 'https://redis.io/commands/rg.getexecution';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Verify that user can see Gears group in Command Helper (RedisGears module)\n    await browserPage.CommandHelper.selectFilterGroupType(filteringGroup);\n    // Select one command from the Gears list\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck));\n    // Verify results of opened command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandArgumentsToCheck, 'Selected command title not correct');\n    // Verify that user can use Read More link for Gears group in Command Helper (RedisGears module)\n    // Currently these links are deleted from redis.io\n    // await t.click(browserPage.CommandHelper.readMoreButton);\n    // Check new opened window page with the correct URL\n    // await Common.checkURL(externalPageLink);\n});\ntest('Verify that user can work with Bloom groups in Command Helper (RedisBloom module)', async t => {\n    filteringGroups = ['Bloom Filter', 'CMS', 'TDigest', 'TopK', 'Cuckoo Filter'];\n    commandsToCheck = [\n        'BF.MEXISTS',\n        'CMS.QUERY',\n        'TDIGEST.RESET',\n        'TOPK.LIST',\n        'CF.ADD'\n    ];\n    commandsArgumentsToCheck = [\n        'BF.MEXISTS key item [item ...]',\n        'CMS.QUERY key item [item ...]',\n        'TDIGEST.RESET key',\n        'TOPK.LIST key [withcount]',\n        'CF.ADD key item'\n    ];\n    externalPageLinks = [\n        'https://redis.io/docs/latest/commands/bf.mexists/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper',\n        'https://redis.io/docs/latest/commands/cms.query/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper',\n        'https://redis.io/docs/latest/commands/tdigest.reset/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper',\n        'https://redis.io/docs/latest/commands/topk.list/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper',\n        'https://redis.io/docs/latest/commands/cf.add/?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_command_helper'\n    ];\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    let i = 0;\n    while (i < filteringGroup.length) {\n        // Verify that user can see Bloom, Cuckoo, CMS, TDigest, TopK groups in Command Helper (RedisBloom module)\n        await browserPage.CommandHelper.selectFilterGroupType(filteringGroups[i]);\n        // Click on the command\n        await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandsToCheck[i]));\n        // Verify results of opened command\n        await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql(commandsArgumentsToCheck[i], 'Selected command title not correct');\n\n        // Verify that user can use Read More link for Bloom, Cuckoo, CMS, TDigest, TopK groups in Command Helper (RedisBloom module).\n        await t.click(browserPage.CommandHelper.readMoreButton);\n        // Check new opened window page with the correct URL\n        await Common.checkURL(externalPageLinks[i]);\n        // Close the window with external link to switch to the application window\n        await goBackHistory();\n        await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n        i++;\n    }\n});\ntest('Verify that user can go back to list of commands for group in Command Helper', async t => {\n    filteringGroup = 'Search';\n    commandToCheck = 'FT.EXPLAIN';\n    const commandForSearch = 'EXPLAIN';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from the list\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, commandForSearch);\n    await browserPage.CommandHelper.selectFilterGroupType(filteringGroup);\n    // Remember found commands\n    const commandsFilterCount = await browserPage.CommandHelper.cliHelperOutputTitles.count;\n    const filteredCommands: string[] = [];\n    for (let i = 0; i < commandsFilterCount; i++) {\n        filteredCommands.push(await browserPage.CommandHelper.cliHelperOutputTitles.nth(i).textContent);\n    }\n    // Select command\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandToCheck));\n    // Click return button\n    await t.click(browserPage.CommandHelper.returnToList);\n    // Check that user returned to list with filter and search applied\n    await browserPage.CommandHelper.checkCommandsInCommandHelper(filteredCommands);\n    await t.expect(browserPage.CommandHelper.returnToList.exists).notOk('Return to list button still displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/cli/cli-logical-db.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet index = '0';\nlet databaseEndpoint = `${ossStandaloneConfig.host}:${ossStandaloneConfig.port}`;\nconst cliMessage = [\n    'Pinging Redis server on ',\n    databaseEndpoint,\n    'Connected.',\n    'Ready to execute commands.'\n];\n\nfixture `CLI logical database`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseHelper.deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`);\n    });\ntest\n    .after(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that working with logical DBs, user can not see 0 DB index in CLI', async t => {\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addLogicalRedisDatabase(ossStandaloneConfig, index);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        // Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        // Verify that user can not see 0 DB index in CLI\n        for (const text of cliMessage) {\n            await t.expect(browserPage.Cli.cliArea.textContent).contains(text, 'No DB index is not displayed in the CLI message');\n        }\n        await t.expect(browserPage.Cli.cliDbIndex.visible).eql(false, 'No DB index before the > character in CLI is not displayed');\n    });\ntest('Verify that working with logical DBs, user can see N DB index in CLI', async t => {\n    index = '1';\n\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addLogicalRedisDatabase(ossStandaloneConfig, index);\n    await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName  } [db${index}]`);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Verify that user can see DB index in CLI\n    // Verify that user can see the db{index} instead of {index} in CLI input and endpoint\n    for (const text of cliMessage) {\n        await t.expect(browserPage.Cli.cliArea.textContent).contains(text, 'DB index is not displayed in the CLI message');\n    }\n    await t.expect(browserPage.Cli.cliDbIndex.textContent).eql(`[db${index}] `, 'DB index before the > character in CLI is not displayed');\n});\ntest('Verify that user can see DB index in the endpoint in CLI header is automatically changed when switched to another logical DB', async t => {\n    index = '2';\n    const indexAfter = '3';\n\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addLogicalRedisDatabase(ossStandaloneConfig, index);\n    await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName  } [db${index}]`);\n\n    // Open CLI and verify that user can see DB index in CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.expect(browserPage.Cli.cliDbIndex.textContent).eql(`[db${index}] `, 'DB index before the > character in CLI is not displayed');\n    // Re-creates client in CLI\n    await t.click(browserPage.Cli.cliCollapseButton);\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Verify that when user re-creates client in CLI the new client is connected to the DB index selected for the DB by default\n    await t.expect(browserPage.Cli.cliDbIndex.textContent).eql(`[db${index}] `, 'The new client is not connected to the DB index selected for the DB by default');\n\n    // Switch to another logical database and check endpoint\n    await t.typeText(browserPage.Cli.cliCommandInput, `Select ${indexAfter}`, { paste: true });\n    await t.pressKey('enter');\n    await t.expect(browserPage.Cli.cliDbIndex.textContent).eql(`[db${indexAfter}] `, `Db index is not automatically changed to the new ${indexAfter}`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Promote workbench in CLI`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .skip('Verify that user can see saved workbench context after redirection from CLI to workbench', async t => {\n    // Open Workbench\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    const command = 'INFO';\n    await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: 1, paste: true });\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Verify that users can see workbench promotion message when they open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.expect(browserPage.Cli.workbenchLink.parent().textContent).eql('Try Workbench, our advanced CLI. Check out our Quick Guides to learn more about Redis capabilities.', 'Wrong promotion message');\n    // Verify that user is redirected to Workbench page clicking on workbench link in CLI\n    await t.click(browserPage.Cli.workbenchLink);\n    await t.expect(workbenchPage.queryInput.exists).ok('Workbench page is not opened');\n    // Verify that CLI panel is minimized after redirection to workbench from CLI\n    await t.expect(workbenchPage.Cli.cliPanel.visible).notOk('Closed CLI');\n\n    // Check editor\n    await t.expect(workbenchPage.mainEditorArea.find('span').withExactText(command).visible).ok('Command is not saved in editor');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/cli/cli.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { Common } from '../../../../helpers/common';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(20);\nconst keyTTL = '2147476121';\nconst jsonValue = '{\"name\":\"xyz\"}';\nconst cliCommands = ['get test', 'acl help', 'client list'];\n\nfixture `CLI`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can see CLI is minimized when he clicks the \"minimize\" button', async t => {\n    const cliColourBefore = await Common.getBackgroundColour(browserPage.Cli.cliBadge);\n\n    // Open CLI and minimize\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.click(browserPage.Cli.minimizeCliButton);\n    // Verify cli is minimized\n    const cliColourAfter = await Common.getBackgroundColour(browserPage.Cli.cliBadge);\n    await t.expect(cliColourAfter).notEql(cliColourBefore, 'CLI badge colour is not changed');\n    await t.expect(browserPage.Cli.minimizeCliButton.visible).eql(false, 'CLI is not mimized');\n});\ntest('Verify that user can see results history when he re-opens CLI after minimizing', async t => {\n    const command = 'SET key';\n\n    // Open CLI and run commands\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n    await t.pressKey('enter');\n    // Minimize and re-open cli\n    await t.click(browserPage.Cli.minimizeCliButton);\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Verify cli results history\n    await t.expect(browserPage.Cli.cliCommandExecuted.textContent).eql(command, 'CLI results history not persists after reopening');\n});\ntest\n    .after(async() => {\n        // Clear database and delete\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    })('Verify that user can repeat commands by entering a number of repeats before the Redis command in CLI', async t => {\n        keyName = Common.generateWord(20);\n        const command = `SET ${keyName} a`;\n        const repeats = 10;\n\n        // Open CLI and run command with repeats\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.typeText(browserPage.Cli.cliCommandInput, `${repeats} ${command}`, { replace: true, paste: true });\n        await t.pressKey('enter');\n        // Verify result\n        await t.expect(browserPage.Cli.cliOutputResponseSuccess.count).eql(repeats, `CLI not contains ${repeats} results`);\n    });\ntest\n    .after(async() => {\n        // Clear database and delete\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    })('Verify that user can run command json.get and see JSON object with escaped quotes (\\\" instead of \")', async t => {\n        keyName = Common.generateWord(20);\n        const jsonValueCli = '\"{\\\\\"name\\\\\":\\\\\"xyz\\\\\"}\"';\n\n        // Add Json key with json object\n        await browserPage.addJsonKey(keyName, jsonValue, keyTTL);\n        const command = `JSON.GET ${keyName}`;\n        // Open CLI and run command\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n        await t.pressKey('enter');\n        // Verify result\n        await t.expect(browserPage.Cli.cliOutputResponseSuccess.innerText).eql(jsonValueCli, 'The user can not see JSON object with escaped quotes');\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        for (const command of cliCommands) {\n            await browserPage.Cli.sendCommandInCli(command);\n        }\n    })('Verify that user can use \"Up\" and \"Down\" keys to view previous commands in CLI in the application', async t => {\n        await t.click(browserPage.Cli.cliExpandButton);\n        await t.expect(browserPage.Cli.cliCommandInput.innerText).eql('');\n        for (let i = cliCommands.length - 1; i >= 0; i--) {\n            await t.pressKey('up');\n            await t.expect(browserPage.Cli.cliCommandInput.innerText).eql(cliCommands[i]);\n        }\n        for (let i = 0; i < cliCommands.length; i++) {\n            await t.expect(browserPage.Cli.cliCommandInput.innerText).eql(cliCommands[i]);\n            await t.pressKey('down');\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/add-sentinel-db.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossSentinelConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\n// todo: enable after RI-7463 fix\nfixture.skip `Add DBs from Sentinel`\n    .page(commonUrl)\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .afterEach(async() => {\n        //Delete database\n        await myRedisDatabasePage.deleteDatabaseByName(ossSentinelConfig.masters[0].alias);\n        await myRedisDatabasePage.deleteDatabaseByName(ossSentinelConfig.masters[1].alias);\n    });\ntest('Verify that user can add Sentinel DB', async t => {\n    await databaseHelper.discoverSentinelDatabase(ossSentinelConfig);\n    await t.expect(myRedisDatabasePage.hostPort.textContent).eql(`${ossSentinelConfig.sentinelHost}:${ossSentinelConfig.sentinelPort}`, 'The sentinel database is not in the list');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/add-standalone-db.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { Chance } from 'chance';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    ossStandaloneConfig,\n    ossClusterConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { Telemetry } from '../../../../helpers/telemetry';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst telemetry = new Telemetry();\nconst chance = new Chance();\nconst databaseHelper = new DatabaseHelper();\n\nconst logger = telemetry.createLogger();\nconst telemetryEvents = ['CONFIG_DATABASES_OPEN_DATABASE','CONFIG_DATABASES_CLICKED'];\nconst expectedProperties = [\n    'databaseId',\n    'RediSearch',\n    'RedisAI',\n    'RedisGraph',\n    'RedisGears',\n    'RedisBloom',\n    'RedisJSON',\n    'RedisTimeSeries',\n    'customModules'\n];\nconst clickButtonExpectedProperties = [\n    'source'\n];\nlet databaseName = `test_standalone-${chance.string({ length: 10 })}`;\n\nfixture `Add database`\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .requestHooks(logger)\n    .after(async() => {\n        await databaseHelper.deleteDatabase(databaseName);\n    })\n    .skip('Verify that user can add Standalone Database', async() => {\n        const connectionTimeout = '20';\n        databaseName = `test_standalone-${chance.string({ length: 10 })}`;\n\n        // Fill the add database form\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton.with({ visibilityCheck: true, timeout: 10000 })();\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton);\n        // TODO : event not found in the request needs further investigation test is failing on this check same fot he other event bellow\n        // Verify that telemetry event 'CONFIG_DATABASES_CLICKED' sent and has all expected properties\n        // await telemetry.verifyEventHasProperties(telemetryEvents[1], clickButtonExpectedProperties, logger);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton);\n        await t\n            .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.hostInput, ossStandaloneConfig.host, { replace: true, paste: true })\n            .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.portInput, ossStandaloneConfig.port, { replace: true, paste: true })\n            .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput, databaseName, { replace: true, paste: true })\n            // Verify that user can customize the connection timeout for the manual flow\n            .typeText(myRedisDatabasePage.AddRedisDatabaseDialog.timeoutInput, connectionTimeout, { replace: true, paste: true });\n        await t\n            .click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton)\n            // Wait for database to be exist\n            .expect(myRedisDatabasePage.dbNameList.withExactText(databaseName).exists).ok('The database not displayed', { timeout: 10000 })\n            // Close message\n            .click(myRedisDatabasePage.Toast.toastCloseButton);\n\n        // Verify that user can see an indicator of databases that are added manually and not opened yet\n        await t.expect(myRedisDatabasePage.starFreeDbCheckbox.exists).ok('free db link is not displayed when db is added')\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(databaseName);\n        await myRedisDatabasePage.clickOnDBByName(databaseName);\n\n        // Verify that telemetry event 'CONFIG_DATABASES_OPEN_DATABASE' sent and has all expected properties\n       // await telemetry.verifyEventHasProperties(telemetryEvents[0], expectedProperties, logger);\n\n        await t.click(browserPage.OverviewPanel.myRedisDBLink);\n        // Verify that user can't see an indicator of databases that were opened\n        await myRedisDatabasePage.verifyDatabaseStatusIsNotVisible(databaseName);\n\n        // Verify that connection timeout value saved\n        await myRedisDatabasePage.clickOnEditDBByName(databaseName);\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.timeoutInput.value).eql(connectionTimeout, 'Connection timeout is not customized');\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.cancelButton);\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .after(async() => {\n        await databaseHelper.deleteDatabase(ossClusterConfig.ossClusterDatabaseName);\n    })\n    .skip('Verify that user can add OSS Cluster DB', async() => {\n        await databaseHelper.addOSSClusterDatabase(ossClusterConfig);\n        // Verify new connection badge for OSS cluster\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossClusterConfig.ossClusterDatabaseName);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/autodiscover-db.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    cloudDatabaseConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { AutoDiscoverREDatabases, MyRedisDatabasePage } from '../../../../pageObjects';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst autoDiscoverREDatabases = new AutoDiscoverREDatabases();\nconst databaseHelper = new DatabaseHelper();\n\nfixture(`Add database`)\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest.skip\n    .meta({ rte: rte.reCloud, skipComment: \"Skipped since environment has changed needs rework\" })\n    .after(async() => {\n        await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName);\n    })('Verify that user can add database from RE Cloud', async() => {\n        await databaseHelper.addRedisCloudDatabase(cloudDatabaseConfig);\n        // Verify new connection badge for RE cloud\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(cloudDatabaseConfig.databaseName);\n        // Verify redis stack icon for RE Cloud with all 5 modules\n        await t.expect(myRedisDatabasePage.redisStackIcon.visible).ok('Redis Stack icon not found for RE Cloud db with all 5 modules');\n    });\n// unskip after closing https://redislabs.atlassian.net/browse/RI-5768\ntest.skip\n    .meta({ rte: rte.reCloud })('Verify that user can add a subscription via auto-discover flow', async t => {\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addAutodiscoverRedisCloudDatabase(\n            cloudDatabaseConfig.accessKey,\n            cloudDatabaseConfig.secretKey\n        );\n        await t.click(\n            myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n        await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Cloud Subscriptions').exists)\n            .ok('Subscriptions list not displayed', { timeout: 120000 });\n        // Select subscriptions\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.selectAllCheckbox);\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.showDatabasesButton);\n        await t.expect(autoDiscoverREDatabases.title.withExactText('Redis Cloud Databases').exists)\n            .ok('database page is not displayed', { timeout: 120000 });\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/cloud-sso.e2e.ts",
    "content": "import * as path from 'path';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { modifyFeaturesConfigJson, refreshFeaturesTestData, updateControlNumber } from '../../../../helpers/insights';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst pathes = {\n    dockerConfig: path.join('.', 'test-data', 'features-configs', 'sso-docker-build.json')\n};\n\nfixture `Cloud SSO`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await refreshFeaturesTestData();\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await refreshFeaturesTestData();\n    });\ntest('Verify that user can not see the promo Cloud databases for docker build', async t => {\n    //TODO should be updated when AI or sth other will be added\n\n    // Deprecated after https://redislabs.atlassian.net/browse/RI-5649, can be updated to test force changing config\n    // Update remote config .json to config with buildType filter excluding current app build\n    // await modifyFeaturesConfigJson(pathes.dockerConfig);\n    // await updateControlNumber(48.2);\n    // await t.expect(myRedisDatabasePage.promoButton.textContent).notOk('Import Cloud database button displayed for docker build');\n\n    // Verify that when SSO flag disabled - Use Cloud API Keys displayed not as dropdown\n    await t.click(\n        myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton);\n    await t.click(\n        myRedisDatabasePage.AddRedisDatabaseDialog.addCloudDatabaseButton);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.useCloudAccount.exists).notOk('Use Cloud Account accordion displayed for docker build');\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.useCloudKeys.exists).notOk('Use Cloud Keys accordion displayed for docker build');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/connecting-to-the-db.e2e.ts",
    "content": "import { ClientFunction } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossClusterConfig,\n    ossSentinelConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\nconst getPageUrl = ClientFunction(() => window.location.href);\n\nfixture `Connecting to the databases verifications`\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\n// TODO : More investigation needed around the connection\ntest.skip\n    .meta({ rte: rte.sentinel, skipComment: \"Skipped because of failure to connect to local sentinel DB\"})\n    .after(async() => {\n        // Delete database\n        await myRedisDatabasePage.deleteDatabaseByName(ossSentinelConfig.masters[0].name);\n        await myRedisDatabasePage.deleteDatabaseByName(ossSentinelConfig.masters[1].name);\n    })('Verify that user can connect to Sentinel DB', async t => {\n        // Add OSS Sentinel DB\n        await databaseHelper.discoverSentinelDatabase(ossSentinelConfig);\n\n        // Get groups & their count\n        const sentinelGroups = myRedisDatabasePage.dbNameList.withText('primary-group');\n        const sentinelGroupsCount = await sentinelGroups.count;\n\n        // Verify new connection badge for Sentinel db\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossSentinelConfig.masters[0].name);\n        await myRedisDatabasePage.verifyDatabaseStatusIsVisible(ossSentinelConfig.masters[1].name);\n\n        // Verify all groups for connection\n        for (let i = 0; i < sentinelGroupsCount; i++) {\n            const groupSelector = sentinelGroups.nth(i);\n            // Connect to DB\n            await myRedisDatabasePage.clickOnDBByName(await groupSelector.textContent);\n            // Check that browser page was opened\n            await t.expect(getPageUrl()).contains('browser', 'Browser page not opened');\n            // Go to databases list\n            await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        }\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .after(async() => {\n        await databaseHelper.deleteDatabase(ossClusterConfig.ossClusterDatabaseName);\n    })\n    .skip('Verify that user can connect to OSS Cluster DB', async t => {\n        // Add OSS Cluster DB\n        await databaseHelper.addOSSClusterDatabase(ossClusterConfig);\n        await myRedisDatabasePage.clickOnDBByName(ossClusterConfig.ossClusterDatabaseName);\n        // Check that browser page was opened\n        await t.expect(getPageUrl()).contains('browser', 'Browser page not opened');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/database-list-search.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneV5Config, ossSentinelConfig, ossClusterConfig } from '../../../../helpers/conf';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst databasesForSearch = [\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testSearch' },\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testSecondSearch' },\n    { host: ossStandaloneV5Config.host, port: ossStandaloneV5Config.port, databaseName: 'testV5' },\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'lastConnection' }\n];\nconst databasesForAdding = [\n    ossStandaloneConfig,\n    databasesForSearch[0],\n    databasesForSearch[1],\n    databasesForSearch[2]\n];\n\nfixture `Database list search`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding);\n        await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig);\n        await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig, 1);\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Clear and delete databases\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest\n    .skip('Verify DB list search', async t => {\n    const searchedDBHostInvalid = 'invalid';\n    const searchedDBName = 'Search';\n    const searchedDBHost = ossStandaloneConfig.host;\n    const searchedDBPort = ossSentinelConfig.sentinelPort;\n    const searchedDBConType = 'OSS Cluster';\n    const searchedDBFirst = 'less than a minute ago';\n    const searchedDBSecond = '1 minute ago';\n    const searchTimeout = 60 * 1000; // 60 sec to wait for changing Last Connection time\n    const dbSelector = myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[2].databaseName);\n    const startTime = Date.now();\n    const noModulesDbRedisStackIcon = myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[2].databaseName).parent('tr').find(myRedisDatabasePage.cssRedisStackIcon);\n\n    // Verify that db without modules has no redis stack icon\n    await t.expect(noModulesDbRedisStackIcon.exists).notOk('The database with other alias is found');\n\n    // Search for DB by Invalid search\n    await t.typeText(myRedisDatabasePage.searchInput, searchedDBHostInvalid, { replace: true, paste: true });\n    // Verify that free cloud db is displayed always\n    await t.expect(myRedisDatabasePage.tableRowContent.textContent).contains('Free Redis Cloud DB', `create free db row is not displayed`);\n\n    // Search for DB by name\n    await t.typeText(myRedisDatabasePage.searchInput, searchedDBName, { replace: true, paste: true });\n    // Verify that user can search DB by database name on the List of databases\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[0].databaseName).exists).ok('The database with alias not found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[1].databaseName).exists).ok('The database with alias not found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[2].databaseName).exists).notOk('The database with other alias is found', { timeout: 10000 });\n\n    // Search for DB by host\n    await t.typeText(myRedisDatabasePage.searchInput, searchedDBHost, { replace: true, paste: true });\n    // Verify that user can search DB by host on the List of databases\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[0].databaseName).exists).ok('The database with host not found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[1].databaseName).exists).ok('The database with host not found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossSentinelConfig.masters[0].alias).exists).notOk('The database with other host is found', { timeout: 10000 });\n\n    // Search for DB by port\n    await t.typeText(myRedisDatabasePage.searchInput, searchedDBPort, { replace: true, paste: true });\n    // Verify that user can search DB by port on the List of databases\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[0].databaseName).exists).notOk('The database with port is found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[1].databaseName).exists).notOk('The database with port is found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossSentinelConfig.masters[0].alias).exists).ok('The database with other port is not found', { timeout: 10000 });\n\n    // Search for DB by connection type\n    await t.typeText(myRedisDatabasePage.searchInput, searchedDBConType, { replace: true, paste: true });\n    // Verify that user can search DB by Connection Type on the List of databases\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossClusterConfig.ossClusterDatabaseName).exists).ok('The database with connection type not found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[0].databaseName).exists).notOk('The database with other connection type found', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[1].databaseName).exists).notOk('The database with other connection type found', { timeout: 10000 });\n\n    // Search for DB by Last Connection\n    await t.typeText(myRedisDatabasePage.searchInput, searchedDBFirst, { replace: true, paste: true });\n    // Verify that database added < 1min ago found on the list search by Last Connection\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[1].databaseName).exists).ok('The database with Last Connection not found', { timeout: 10000 });\n    // Verify that database added > 1min ago found on the list search by Last Connection\n    do {\n        await myRedisDatabasePage.reloadPage();\n        await t.typeText(myRedisDatabasePage.searchInput, searchedDBSecond, { replace: true, paste: true });\n    }\n    while (!(await dbSelector.exists) && Date.now() - startTime < searchTimeout);\n    // Verify that user can search DB by Last Connection on the List of databases\n    await t.expect(dbSelector.exists).ok('The database with Last Connection not found', { timeout: 10000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/database-sorting.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport {\n    commonUrl,\n    ossStandaloneConfig,\n    ossSentinelConfig,\n    ossClusterConfig\n} from '../../../../helpers/conf';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst databases = [\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: ossStandaloneConfig.databaseName },\n    { host: ossClusterConfig.ossClusterHost, port: ossClusterConfig.ossClusterPort, databaseName: ossClusterConfig.ossClusterDatabaseName },\n    { host: ossSentinelConfig.sentinelHost, port: ossSentinelConfig.sentinelPort, databaseName: ossSentinelConfig.masters[0].alias }\n];\nlet actualDatabaseList: string[] = [];\nconst oldDBName = ossStandaloneConfig.databaseName;\nconst newDBName = '! Edited Standalone DB name';\nconst sortList = async(): Promise<string[]> => {\n    const sortedByName = databases.sort((a, b) => a.databaseName > b.databaseName ? 1 : -1);\n    const sortedDatabaseNames: string[] = [];\n    for (let i = 0; i < sortedByName.length; i++) {\n        sortedDatabaseNames.push(sortedByName[i].databaseName);\n    }\n    return sortedDatabaseNames;\n};\n\nfixture `Remember database sorting`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        // Delete all existing databases\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig);\n        await databaseAPIRequests.discoverSentinelDatabaseApi(ossSentinelConfig, 1);\n        // Reload Page\n        await browserPage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Clear and delete databases\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('STANDALONE');\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('CLUSTER');\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL');\n    });\ntest('Verify that sorting on the list of databases saved when database opened', async t => {\n    // Sort by Connection Type\n    const sortedByConnectionType = [ossClusterConfig.ossClusterDatabaseName, ossSentinelConfig.masters[0].alias, ossStandaloneConfig.databaseName];\n    await t.click(myRedisDatabasePage.sortByConnectionType);\n    actualDatabaseList = await myRedisDatabasePage.getAllDatabases();\n    await myRedisDatabasePage.compareInstances(actualDatabaseList, sortedByConnectionType);\n    // Connect to DB and check sorting\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    await t.expect(browserPage.refreshKeysButton.visible).ok('Browser page is not opened');\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    actualDatabaseList = await myRedisDatabasePage.getAllDatabases();\n    await myRedisDatabasePage.compareInstances(actualDatabaseList, sortedByConnectionType);\n    // Sort by Host and Port\n    await t.click(myRedisDatabasePage.sortByHostAndPort);\n    actualDatabaseList = await myRedisDatabasePage.getAllDatabases();\n    const sortedDatabaseHost = [ossClusterConfig.ossClusterDatabaseName, ossSentinelConfig.masters[0].alias, ossStandaloneConfig.databaseName];\n    await myRedisDatabasePage.compareInstances(actualDatabaseList, sortedDatabaseHost);\n    // Verify that sorting on the list of databases saved when databases list refreshed\n    await myRedisDatabasePage.reloadPage();\n    actualDatabaseList = await myRedisDatabasePage.getAllDatabases();\n    await myRedisDatabasePage.compareInstances(actualDatabaseList, sortedDatabaseHost);\n});\ntest\n    .skip('Verify that user has the same sorting if db name is changed', async t => {\n    // Sort by Database name\n    await t.click(myRedisDatabasePage.sortByDatabaseAlias);\n    actualDatabaseList = await myRedisDatabasePage.getAllDatabases();\n    await myRedisDatabasePage.compareInstances(actualDatabaseList, await sortList());\n    // Change DB name inside of sorted list\n    await databaseHelper.clickOnEditDatabaseByName(ossStandaloneConfig.databaseName);\n    await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput, newDBName, { replace: true, paste: true });\n    await t.click(myRedisDatabasePage.submitChangesButton);\n    // Change DB is control list\n    const index = databases.findIndex((item) => {\n        return item.databaseName === oldDBName;\n    });\n    databases[index].databaseName = newDBName;\n    // Compare sorting with expected list\n    actualDatabaseList = await myRedisDatabasePage.getAllDatabases();\n    await myRedisDatabasePage.compareInstances(actualDatabaseList, await sortList());\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/edit-db.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst database = Object.assign({}, ossStandaloneConfig);\nconst previousDatabaseName = Common.generateWord(20);\nconst newDatabaseName = Common.generateWord(20);\ndatabase.databaseName = previousDatabaseName;\n\nfixture `List of Databases`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(database);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest\n    .skip('Verify that user can edit DB alias of Standalone DB', async t => {\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Edit alias of added database\n    await databaseHelper.clickOnEditDatabaseByName(database.databaseName);\n\n    // Verify that timeout input is displayed for edit db window with default value when it wasn't specified\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.timeoutInput.value).eql('30', 'Timeout is not defaulted to 30');\n\n    await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseAliasInput, newDatabaseName, { replace: true, paste: true });\n    await t.click(myRedisDatabasePage.submitChangesButton);\n    // Verify that database has new alias\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(newDatabaseName).exists).ok('The database with new alias is in not the list', { timeout: 10000 });\n    await t.expect(myRedisDatabasePage.dbNameList.withExactText(previousDatabaseName).exists).notOk('The database with previous alias is still in the list', { timeout: 10000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/github.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nfixture `Github functionality`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can work with Github link in the application', async t => {\n    // Verify that user can see the icon for GitHub reference at the bottom of the left side bar in the List of DBs\n    await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button not found');\n    //Verify that user can see the icon for GitHub reference at the bottom of the left side bar on the Browser page\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button not found');\n    // Verify that user can see the icon for GitHub reference at the bottom of the left side bar on the Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button');\n    // Verify that when user clicks on Github icon he redirects to the URL: https://github.com/RedisInsight/RedisInsight\n    await t.click(myRedisDatabasePage.NavigationPanel.githubButton);\n\n    await Common.checkURLContainsText('https://github.com/redis/RedisInsight');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/logical-databases.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Logical databases`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that if user enters any index of the logical database that does not exist in the database, he can see Redis error \"ERR DB index is out of range\" and cannot proceed', async t => {\n    const index = '0';\n\n    // Add database with logical index\n    await myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDataBase(ossStandaloneConfig);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.databaseIndexCheckbox);\n    await t.typeText(myRedisDatabasePage.AddRedisDatabaseDialog.databaseIndexInput, index, { paste: true });\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n    // Open database and run command with non-existing index\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.typeText(browserPage.Cli.cliCommandInput, 'Select 19', { paste: true });\n    await t.pressKey('enter');\n    // Verify the error\n    await t.expect(browserPage.Cli.cliOutputResponseFail.textContent).eql('\"ERR DB index is out of range\"', 'Error is not dispalyed in CLI');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/navigation.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig, ossStandaloneV7Config\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n//import { AddNewRdiParameters } from '../../../../helpers/api/api-rdi';\n//import { RdiApiRequests } from '../../../../helpers/api/api-rdi';\n\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\n\n//const rdiApiRequests = new RdiApiRequests();\n\n// const rdiInstance: AddNewRdiParameters = {\n//     name: 'testInstance',\n//     url: 'https://11.111.111.111',\n//     username: 'username',\n//     password: '111'\n// };\n\nfixture `Database Navigation`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(\n            ossStandaloneConfig\n        );\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(\n            ossStandaloneV7Config\n        );\n        await myRedisDatabasePage.reloadPage();\n        // TODO: uncomment when RDI e2e starts running\n        //await rdiApiRequests.addNewRdiApi(rdiInstance);\n\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest('Verify that user can navigate to instances using navigation widget', async t => {\n    const dbListPageNames = await myRedisDatabasePage.getAllDatabases()\n    await myRedisDatabasePage.clickOnDBByName(\n        ossStandaloneConfig.databaseName\n    );\n    await t.click(browserPage.NavigationHeader.dbName)\n    let dbWidgetNames = await browserPage.NavigationHeader.getAllDatabases();\n    // todo: check if it is a bug default sorting in db list vs db popover are not the same\n    await t.expect(dbListPageNames.sort()).eql(dbWidgetNames.sort(), 'DB Lists have the same names');\n    await t.click(browserPage.NavigationHeader.dbListInstance.withText(ossStandaloneV7Config.databaseName));\n    await t.expect(browserPage.NavigationHeader.dbNameExactText.textContent).eql(ossStandaloneV7Config.databaseName, 'user can not be navigated');\n    await t.click(browserPage.NavigationHeader.dbName)\n    await t.click(browserPage.NavigationHeader.homeLinkNavigation);\n    await t.expect(myRedisDatabasePage.hostPort.exists).ok('Db list page is not opened');\n    await myRedisDatabasePage.clickOnDBByName(\n        ossStandaloneConfig.databaseName\n    )\n    await t.click(browserPage.NavigationHeader.dbName)\n    await t.typeText(browserPage.NavigationHeader.dbListInput, ossStandaloneV7Config.databaseName);\n    dbWidgetNames = await browserPage.NavigationHeader.getAllDatabases();\n    await t.expect(dbWidgetNames.length).eql(1, 'DB List is not searched');\n    await t.click(browserPage.NavigationHeader.rdiNavigationTab);\n\n    // TODO: uncomment when RDI e2e starts running\n    //await t.expect(dbListPageNames.length).eql(1, 'RDI List is not searched');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/notification.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { DatabaseScripts, DbTableParameters } from '../../../../helpers';\nimport { format, subDays } from 'date-fns';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\n\nconst currentDate = new Date();\nconst fiveDaysAgo = subDays(currentDate, 5);\nconst rowValue5 = format(fiveDaysAgo, 'yyyy-MM-dd HH:mm:ss');\n\nconst seventeenDaysAgo = subDays(currentDate, 17);\nconst rowValue16 = format(seventeenDaysAgo, 'yyyy-MM-dd HH:mm:ss');\n\nconst dbTableParams5days: DbTableParameters = {\n    tableName: 'database_instance',\n    columnName: 'lastConnection',\n    rowValue: rowValue5,\n    conditionWhereColumnName: 'name',\n    conditionWhereColumnValue: ossStandaloneConfig.databaseName\n};\n\nconst dbTableParams16days: DbTableParameters = {\n    tableName: 'database_instance',\n    columnName: 'lastConnection',\n    rowValue: rowValue16,\n    conditionWhereColumnName: 'name',\n    conditionWhereColumnValue: ossStandaloneConfig.databaseName\n};\n\nfixture `DB expire notifications`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl);\n\ntest.before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig, true);\n        await browserPage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n    })\n    .after(async() => {\n      //  await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    .skip('Verify that notifications are displayed if the db will be expired soon', async t => {\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await workbenchPage.sendCommandInWorkbench('CMS.INITBYDIM');\n\n        await DatabaseScripts.updateColumnValueInDBTable(dbTableParams5days);\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n        await t.hover(myRedisDatabasePage.iconNotUsedDatabase);\n        await t.expect(myRedisDatabasePage.notificationUnusedDbMessage.textContent).contains('Probabilistic data structures', 'there is no info about module');\n        await t.expect(myRedisDatabasePage.notificationUnusedDbMessage.textContent).contains('free Redis Cloud databases will be deleted after 15 days of inactivity.', 'there is no expected info');\n\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n        await DatabaseScripts.updateColumnValueInDBTable(dbTableParams16days);\n        await myRedisDatabasePage.reloadPage();\n\n        await t.hover(myRedisDatabasePage.iconDeletedDatabase);\n        await t.expect(myRedisDatabasePage.notificationUnusedDbMessage.textContent).contains('Build your app with Redis Cloud', 'there is no common');\n        await t.expect(myRedisDatabasePage.notificationUnusedDbMessage.textContent).contains('Free Redis Cloud DBs auto-delete after 15 days', 'there is no expected info');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database/redisstack.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig,\n    ossStandaloneConfigEmpty,\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst moduleNameList = ['Redis Query Engine', 'Graph', 'Probabilistic', 'JSON', 'Time Series'];\n\nfixture `Redis Stack`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfigEmpty);\n        // Reload Page\n        await browserPage.reloadPage();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    });\ntest('Verify that user can see module list Redis Stack icon hovering (without Redis Stack text)', async t => {\n    // Verify that user can see Redis Stack icon when Redis Stack DB is added in the application\n    await t.expect(myRedisDatabasePage.redisStackIcon.visible).ok('Redis Stack icon not found');\n    // Hover over redis stack icon\n    await t.hover(myRedisDatabasePage.redisStackIcon);\n    await t.expect(myRedisDatabasePage.moduleTooltip.visible).ok('Tooltip with modules not found');\n    // Verify that user can see the Redis Stack logo is placed in the module list tooltip in the list of DBs\n    await t.expect(myRedisDatabasePage.tooltipRedisStackLogo.visible).ok('Redis Stack logo not found');\n    // Check all Redis Stack modules inside\n    await myRedisDatabasePage.checkModulesInTooltip(moduleNameList);\n});\n// Deprecated since RI-6268, TODO remove after entire feature\ntest.skip('Verify that user can see Redis Stack icon in Edit mode near the DB name', async t => {\n    // Open Edit mode\n    await t.click(myRedisDatabasePage.editDatabaseButton);\n    // Check redis stack icon near the db name\n    await t.expect(myRedisDatabasePage.redisStackIcon.visible).ok('Redis Stack icon not found');\n    // Verify that user can see the Redis Stack logo is placed in the DB edit form when hover over the RedisStack logo\n    await t.hover(myRedisDatabasePage.redisStackIcon);\n    await t.expect(myRedisDatabasePage.tooltipRedisStackLogo.visible).ok('Redis Stack logo not found');\n    const databaseName = myRedisDatabasePage.redisStackIcon.parent().nextSibling();\n    await t.expect(databaseName.withAttribute('data-testid', 'edit-alias-btn').exists).ok('Edit button not found');\n});\ntest.before(async() => {\n    // Add new databases using API\n    await databaseHelper.acceptLicenseTerms();\n    await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n    // Reload Page\n    await browserPage.reloadPage();\n})\n.skip('Verify that Redis Stack is not displayed for stack >8', async t => {\n    // Verify that user can not see Redis Stack icon when Redis Stack DB > 8 is added in the application\n    await t.expect(myRedisDatabasePage.redisStackIcon.visible).notOk('Redis Stack icon found');\n    await t.click(myRedisDatabasePage.editDatabaseButton);\n    // Check redis stack icon near the db name\n    await t.expect(myRedisDatabasePage.redisStackIcon.visible).notOk('Redis Stack icon found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    WorkbenchPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Database info tooltips`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can see DB name, endpoint, connection type, Redis version, user name in tooltip when hover over the (i) icon', async t => {\n    const version = /[0-9].[0-9].[0-9]/;\n    const logicalDbText = 'Select logical databases to work with in Browser, Workbench, and Database Analysis.';\n\n    await t.hover(browserPage.OverviewPanel.databaseInfoIcon);\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).contains(ossStandaloneConfig.databaseName, 'User can see database name in tooltip');\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).contains(`${ossStandaloneConfig.host}:${ossStandaloneConfig.port}`, 'User can see endpoint in tooltip');\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).contains('Standalone', 'User can not see connection type in tooltip');\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).match(version, 'User can not see Redis version in tooltip');\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).contains('Default', 'User can not see user name in tooltip');\n    // Verify that user can see the tooltip by hovering on index control switcher\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).contains('Logical databases', 'Logical Databases text not displayed in tooltip');\n    await t.expect(browserPage.OverviewPanel.databaseInfoToolTip.textContent).contains(logicalDbText, 'Logical Databases text not displayed in tooltip');\n\n    // Verify that user can see an (i) icon next to the database name on Browser and Workbench pages\n    await t.expect(browserPage.OverviewPanel.databaseInfoIcon.visible).ok('User can not see (i) icon on Browser page', { timeout: 10000 });\n    // Move to the Workbench page and check icon\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect(workbenchPage.OverviewPanel.overviewTotalMemory.visible).ok('User can not see (i) icon on Workbench page', { timeout: 10000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database-overview/database-overview-keys.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    WorkbenchPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { cloudDatabaseConfig, commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst browserActions = new BrowserActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keys: string[];\nconst keyName = Common.generateWord(10);\nconst keysAmount = 5;\nconst index = '1';\n\nfixture `Database overview`\n    .meta({ type: 'regression' })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        // Create databases and keys\n        await databaseHelper.acceptLicenseTermsAndAddDatabase(ossStandaloneConfig);\n        await browserPage.addStringKey(keyName);\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.AddRedisDatabaseDialog.addLogicalRedisDatabase(ossStandaloneConfig, index);\n        await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName} [db${index}]`);\n        keys = await Common.createArrayWithKeyValue(keysAmount);\n        await browserPage.Cli.sendCommandInCli(`MSET ${keys.join(' ')}`);\n    })\n    .afterEach(async t => {\n        // Clear and delete databases\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName} [db${index}]`);\n        await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n        await databaseHelper.deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [db${index}]`);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .skip('Verify that user can see total and current logical database number of keys (if there are any keys in other logical DBs)', async t => {\n        // Wait for Total Keys number refreshed\n        await t.expect(browserPage.OverviewPanel.overviewTotalKeys.withText(`${keysAmount + 1}`).exists).ok('Total keys are not changed', { timeout: 10000 });\n        await t.hover(workbenchPage.OverviewPanel.overviewTotalKeys);\n        // Verify that user can see total number of keys and number of keys in current logical database\n        await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed');\n        await browserActions.verifyTooltipContainsText(`${keysAmount + 1}\\nTotal Keys\\ndb1:\\n${keysAmount}\\nKeys`, true);\n\n        // Open Database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await t.hover(workbenchPage.OverviewPanel.overviewTotalKeys);\n        // Verify that user can see total number of keys and not it current logical database (if there are no any keys in other logical DBs)\n        await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed');\n        await browserActions.verifyTooltipContainsText(`${keysAmount + 1}\\nTotal Keys`, true);\n        await browserActions.verifyTooltipContainsText('db1', false);\n    });\ntest('Verify that when users hover over keys icon in Overview for Cloud DB, they see only total number of keys in tooltip', async t => {\n        await t.hover(workbenchPage.OverviewPanel.overviewTotalKeys);\n        // Verify that user can see only total number of keys\n        await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed');\n        await browserActions.verifyTooltipContainsText('Total Keys', true);\n        await browserActions.verifyTooltipContainsText('db1', false);\n    })\n    .skip\n    .meta({ rte: rte.reCloud, skipComment: \"Unstable CI execution, assertion failure, needs investigation\" })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisCloudDatabase(cloudDatabaseConfig);\n    })\n    .after(async() => {\n        // Delete database\n        await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    WorkbenchPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { cloudDatabaseConfig, commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet keys: string[];\n\nfixture `Database overview`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest.after(async() => {\n    // Delete database\n    await browserPage.Cli.sendCommandInCli(`DEL ${keys.join(' ')}`);\n    await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n\n})('Verify that user can connect to DB and see breadcrumbs at the top of the application', async t => {\n    // Create new keys\n    keys = await Common.createArrayWithKeyValue(10);\n    await browserPage.Cli.sendCommandInCli(`MSET ${keys.join(' ')}`);\n\n    // Verify that user can see breadcrumbs in Browser and Workbench views\n    await t.expect(browserPage.breadcrumbsContainer.visible).ok('User can not see breadcrumbs in Browser page', { timeout: 10000 });\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect(browserPage.breadcrumbsContainer.visible).ok('User can not see breadcrumbs in Workbench page', { timeout: 10000 });\n\n    // Verify that user can see total memory and total number of keys updated in DB header in Workbench page\n    await t.expect(workbenchPage.OverviewPanel.overviewTotalKeys.exists).ok('User can not see total keys');\n    await t.expect(workbenchPage.OverviewPanel.overviewTotalMemory.exists).ok('User can not see total memory');\n});\ntest('Verify that user can set overview refresh', async t => {\n    const common_command = 'info';\n\n    await t.click(browserPage.OverviewPanel.autoRefreshArrow);\n    // todo: review UI component. in fact eql to \"5 sEdit\" due to <title> inside this span. span -> div -> svg -> title\n    await t.expect(browserPage.OverviewPanel.autoRefreshRateInput.textContent).contains('5 s', 'default value is incorrect');\n    await t.click(browserPage.OverviewPanel.autoRefreshCheckbox);\n    //Start Monitor\n    await browserPage.Profiler.startMonitor();\n    //Wait for 6 sec\n    await t.wait(6000);\n    await browserPage.Profiler.checkCommandInMonitorResults(common_command, undefined, false);\n\n    await browserPage.Profiler.stopMonitor();\n    await browserPage.OverviewPanel.setAutoRefreshValue('10');\n    await t.click(browserPage.OverviewPanel.autoRefreshCheckbox);\n    //Start Monitor\n    await t.click( browserPage.Profiler.resetProfilerButton);\n    await browserPage.Profiler.startMonitor();\n    // verify that the info is not displayed after default value\n    await t.wait(5000);\n    await workbenchPage.Profiler.checkCommandInMonitorResults(common_command, undefined, false);\n    // verify that the info is displayed after set value\n    await workbenchPage.Profiler.checkCommandInMonitorResults(common_command);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database-overview/database-tls-certificates.e2e.ts",
    "content": "import { rte, TlsCertificates } from '../../../../helpers/constants';\nimport { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneTlsConfig } from '../../../../helpers/conf';\nimport { DatabaseHelper } from '../../../../helpers';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `tls certificates`\n    .meta({ type: 'regression', rte: rte.none })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await myRedisDatabasePage.reloadPage();\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneTlsConfig);\n    });\ntest\n    .skip('Verify that user can remove added certificates', async t => {\n    await t.click(browserPage.NavigationPanel.myRedisDBButton);\n    await myRedisDatabasePage.clickOnEditDBByName(ossStandaloneTlsConfig.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await myRedisDatabasePage.AddRedisDatabaseDialog.removeCertificateButton(TlsCertificates.CA, 'ca');\n    // remove if other Certificates were added\n    if( await myRedisDatabasePage.AddRedisDatabaseDialog.getDeleteCertificate(TlsCertificates.CA).exists){\n        await myRedisDatabasePage.AddRedisDatabaseDialog.removeCertificateButton(TlsCertificates.CA, 'ca');\n    }\n    await myRedisDatabasePage.reloadPage();\n    // wait for dbs are displayed\n    await t.expect(myRedisDatabasePage.dbNameList.count).gt(0);\n    await myRedisDatabasePage.clickOnEditDBByName(ossStandaloneTlsConfig.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    //verify that ca certificate is deleted\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).contains('No CA Certificate', 'CA certificate was not deleted');\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.certificateDropdownList.exists).notOk('CA certificate was not deleted');\n\n    //verify that client certificate is deleted\n    await myRedisDatabasePage.AddRedisDatabaseDialog.removeCertificateButton(TlsCertificates.Client, 'client');\n    await myRedisDatabasePage.reloadPage();\n\n    // wait for dbs are displayed\n    await t.expect(myRedisDatabasePage.dbNameList.count).gt(0);\n    await myRedisDatabasePage.clickOnEditDBByName(ossStandaloneTlsConfig.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.requiresTlsClientCheckbox);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).contains('Add new certificate', 'Client certificate was not deleted');\n    await myRedisDatabasePage.reloadPage();\n\n    await myRedisDatabasePage.clickOnEditDBByName(ossStandaloneTlsConfig.databaseName);\n    await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n    await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.requiresTlsClientCheckbox.checked).notOk('the certificate was not removed');\n    await myRedisDatabasePage.reloadPage();\n\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName!);\n\n    await t.click(browserPage.NavigationPanel.myRedisDBButton);\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneTlsConfig.databaseName);\n    await t.expect(browserPage.Toast.toastError.textContent).contains('CA or Client certificate', 'user can connect to db without certificates');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/database-overview/overview.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, cloudDatabaseConfig } from '../../../../helpers/conf';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Overview`\n    .meta({ type: 'regression', rte: rte.reCloud })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisCloudDatabase(cloudDatabaseConfig);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName);\n    });\ntest\n    .skip('Verify that user can see not available metrics from Overview in tooltip with the text \"<Metric_name> is/are not available\"', async t => {\n    // Verify that CPU parameter is not displayed in Overview\n    await t.expect(browserPage.OverviewPanel.overviewCpu.exists).notOk('Not available CPU is displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/insights/feature-flag.e2e.ts",
    "content": "import * as path from 'path';\nimport { BrowserPage, MyRedisDatabasePage, SettingsPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { ExploreTabs, rte, RecommendationIds } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { DatabaseScripts, DbTableParameters } from '../../../../helpers/database-scripts';\nimport { modifyFeaturesConfigJson, refreshFeaturesTestData, updateControlNumber } from '../../../../helpers/insights';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst featuresConfigTable = 'features_config';\nconst redisVersionRecom = RecommendationIds.redisVersion;\nconst pathes = {\n    invalidConfig: path.join('.', 'test-data', 'features-configs', 'insights-invalid.json'),\n    validConfig: path.join('.', 'test-data', 'features-configs', 'insights-valid.json'),\n    analyticsConfig: path.join('.', 'test-data', 'features-configs', 'insights-analytics-filter-off.json'),\n    buildTypeConfig: path.join('.', 'test-data', 'features-configs', 'insights-build-type-filter.json'),\n    flagOffConfig: path.join('.', 'test-data', 'features-configs', 'insights-flag-off.json')\n};\nconst dbTableParams: DbTableParameters = {\n    tableName: featuresConfigTable,\n    columnName: 'data',\n    conditionWhereColumnName: 'id',\n    conditionWhereColumnValue: '1'\n};\n\n// the tests are skipped due to story https://redislabs.atlassian.net/browse/RI-5089\nfixture.skip `Feature flag`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n        await refreshFeaturesTestData();\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config);\n        await refreshFeaturesTestData();\n    });\ntest('Verify that default config applied when remote config version is lower', async t => {\n    await updateControlNumber(19.2);\n\n    const featureVersion = await JSON.parse(await DatabaseScripts.getColumnValueFromTableInDB(dbTableParams)).version;\n\n    await t.expect(featureVersion).eql(2.3403, 'Config with lowest version applied');\n    await browserPage.NavigationHeader.togglePanel(true);\n    await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).ok('Insights panel displayed when disabled in default config');\n});\ntest('Verify that invaid remote config not applied even if its version is higher than in the default config', async t => {\n    // Update remote config .json to invalid\n    await modifyFeaturesConfigJson(pathes.invalidConfig);\n    await updateControlNumber(19.2);\n\n    const featureVersion = await JSON.parse(await DatabaseScripts.getColumnValueFromTableInDB(dbTableParams)).version;\n\n    await t.expect(featureVersion).eql(2.3403, 'Config highest version not applied');\n    await browserPage.NavigationHeader.togglePanel(true);\n    await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).ok('Insights panel displayed when disabled in default config');\n});\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config);\n        await refreshFeaturesTestData();\n    })\n    .after(async t => {\n        // Turn on telemetry\n        await t.click(browserPage.NavigationPanel.settingsButton);\n        await settingsPage.changeAnalyticsSwitcher(true);\n        // Delete databases connections\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config);\n        // Update remote config .json to default\n        await refreshFeaturesTestData();\n    })('Verify that valid remote config applied with version higher than in the default config', async t => {\n        // Update remote config .json to valid\n        await modifyFeaturesConfigJson(pathes.validConfig);\n        await updateControlNumber(48.2);\n        let featureVersion = await JSON.parse(await DatabaseScripts.getColumnValueFromTableInDB(dbTableParams)).version;\n        let versionFromConfig = await Common.getJsonPropertyValue('version', pathes.validConfig);\n\n        await t.expect(featureVersion).eql(versionFromConfig, 'Config with invalid data applied');\n        // Verify that Insights panel displayed if user's controlNumber is in range from config file\n        await browserPage.NavigationHeader.togglePanel(true);\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).ok('Insights panel not displayed when enabled from remote config');\n\n        // Verify that recommendations displayed for all databases if option enabled\n        await t.click(browserPage.OverviewPanel.myRedisDBLink);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n        await browserPage.NavigationHeader.togglePanel(true);\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).ok('Insights panel not displayed for the other db connection');\n        await browserPage.NavigationHeader.togglePanel(true);\n        const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        await t.expect(tab.getRecommendationByName(redisVersionRecom).exists).ok('Redis Version recommendation not displayed');\n\n        await browserPage.NavigationHeader.togglePanel(false);\n        // Verify that Insights panel can be displayed for Telemetry enabled/disabled according to filters\n        await t.click(browserPage.NavigationPanel.settingsButton);\n        await settingsPage.changeAnalyticsSwitcher(false);\n        await myRedisDatabasePage.navigateToDatabase(ossStandaloneV5Config.databaseName);\n        await browserPage.NavigationHeader.togglePanel(true);\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).notOk('Insights panel displayed without analytics when its filter is on');\n\n        // Update remote config .json to config without analytics filter\n        await modifyFeaturesConfigJson(pathes.analyticsConfig);\n        await updateControlNumber(48.2);\n        await browserPage.NavigationHeader.togglePanel(true);\n        // Verify that Insights panel can be displayed for WebStack app according to filters\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).ok('Insights panel not displayed without analytics when its filter is off');\n\n        // Verify that Insights panel not displayed if user's controlNumber is out of range from config file\n        await updateControlNumber(30.1);\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).notOk('Insights panel displayed for user with control number out of the config');\n\n        // Update remote config .json to config with buildType filter excluding current app build\n        await modifyFeaturesConfigJson(pathes.buildTypeConfig);\n        await updateControlNumber(48.2);\n        await browserPage.NavigationHeader.togglePanel(true);\n        // Verify that buildType filter applied\n        featureVersion = await JSON.parse(await DatabaseScripts.getColumnValueFromTableInDB(dbTableParams)).version;\n        versionFromConfig = await Common.getJsonPropertyValue('version', pathes.buildTypeConfig);\n        await t.expect(featureVersion).eql(versionFromConfig, 'Config highest version not applied');\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).notOk('Insights panel displayed when filter excludes this buildType');\n\n        // Update remote config .json to config with insights feature disabled\n        await modifyFeaturesConfigJson(pathes.flagOffConfig);\n        await updateControlNumber(48.2);\n        await browserPage.NavigationHeader.togglePanel(true);\n        // Verify that Insights panel not displayed if the remote config file has it disabled\n        featureVersion = await JSON.parse(await DatabaseScripts.getColumnValueFromTableInDB(dbTableParams)).version;\n        versionFromConfig = await Common.getJsonPropertyValue('version', pathes.flagOffConfig);\n        await t.expect(featureVersion).eql(versionFromConfig, 'Config highest version not applied');\n        await t.expect(browserPage.InsightsPanel.getInsightsPanel().exists).notOk('Insights panel displayed when filter excludes this buildType');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts",
    "content": "import * as path from 'path';\nimport * as fs from 'fs';\nimport { join as joinPath } from 'path';\nimport { t } from 'testcafe';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneConfigEmpty, fileDownloadPath } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { deleteAllKeysFromDB, verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\nimport { DatabasesActions } from '../../../../common-actions/databases-actions';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst databasesActions = new DatabasesActions();\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\n\nconst zipFolderName = 'customTutorials';\nconst folderPath = path.join('..', 'test-data', 'upload-tutorials', zipFolderName);\nconst folder1 = 'folder-1';\nconst folder2 = 'folder-2';\nconst internalLinkName2 = 'vector-2';\nlet tutorialName = `${zipFolderName}${Common.generateWord(5)}`;\nlet zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`);\nlet internalLinkName1 = 'probably-1';\nlet foundExportedFiles: string[];\nconst verifyCompletedResultText = async(resultsText: string[]): Promise<void> => {\n    for (const result of resultsText) {\n        await t.expect(workbenchPage.Toast.toastBody.textContent).contains(result, 'Bulk upload completed summary not correct');\n    }\n    await t.expect(workbenchPage.Toast.toastBody.textContent).notContains('0:00:00.000', 'Bulk upload Time taken not correct');\n    await t.click(workbenchPage.Toast.toastSubmitBtn);\n};\n\nfixture `Upload custom tutorials`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\n/* https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4198,\nhttps://redislabs.atlassian.net/browse/RI-4302, https://redislabs.atlassian.net/browse/RI-4318\n*/\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n\n        tutorialName = `${zipFolderName}${Common.generateWord(5)}`;\n        zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`);\n        // Create zip file for uploading\n        await Common.createZipFromFolder(folderPath, zipFilePath);\n    })\n    .after(async() => {\n        // Delete zip file\n        await Common.deleteFileFromFolder(zipFilePath);\n    })('Verify that user can upload tutorial with local zip file without manifest.json', async t => {\n        // Verify that user can upload custom tutorials on docker version\n        internalLinkName1 = 'probably-1';\n        const imageExternalPath = 'Redis Insight screen external';\n\n        // Verify that user can see the “MY TUTORIALS” section in the Enablement area.\n        await browserPage.NavigationHeader.togglePanel(true);\n        const tutorials = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n\n        await t.expect(tutorials.customTutorials.visible).ok('custom tutorials sections is not visible');\n        // Verify that user can see \"My Tutorials\" tab is collapsed by default in tutorials\n        await t.expect(tutorials.customTutorials.getAttribute('aria-expanded')).eql('false', 'My tutorials not closed by default');\n\n        // Expand My tutorials\n        await tutorials.toggleMyTutorialPanel();\n        await t.click(tutorials.tutorialOpenUploadButton);\n        await t.expect(tutorials.tutorialSubmitButton.hasAttribute('disabled')).ok('submit button is not disabled');\n\n        // Verify that User can request to add a new custom Tutorial by uploading a .zip archive from a local folder\n        await t.setFilesToUpload(tutorials.tutorialImport, [zipFilePath]);\n        await t.click(tutorials.tutorialSubmitButton);\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`);\n\n        // Verify that when user upload a .zip archive without a .json manifest, all markdown files are inserted at the same hierarchy level\n        await t.click(tutorials.tutorialAccordionButton.withText(tutorialName));\n        await t.expect(tutorials.getAccordionButtonWithName(folder1).visible).ok(`${folder1} is not visible`);\n        await t.expect(tutorials.getAccordionButtonWithName(folder2).visible).ok(`${folder2} is not visible`);\n        await t.click(tutorials.getAccordionButtonWithName(folder1));\n        await t.expect(tutorials.getInternalLinkWithManifest(internalLinkName1).visible)\n            .ok(`${internalLinkName1} is not visible`);\n        await t.click(tutorials.getAccordionButtonWithName(folder2));\n        await t.expect(tutorials.getInternalLinkWithManifest(internalLinkName2).visible)\n            .ok(`${internalLinkName2} is not visible`);\n        await t.expect(tutorials.scrolledEnablementArea.exists).notOk('enablement area is visible before clicked');\n        await t.click(tutorials.getInternalLinkWithManifest(internalLinkName1));\n        await t.expect(tutorials.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked');\n\n        // Verify that user can see image in custom tutorials by providing absolute external path in md file\n        const imageExternal = tutorials.getTutorialImageByAlt(imageExternalPath);\n        await tutorials.waitUntilImageRendered(imageExternal);\n        const imageExternalHeight = await imageExternal.getStyleProperty('height');\n        await t.expect(parseInt(imageExternalHeight.replace(/[^\\d]/g, ''))).gte(150);\n\n        /* Uncomment after fix https://redislabs.atlassian.net/browse/RI-4486\n        also need to add in probably-1.md file:\n        Relative:\n        ![RedisInsight screen relative](../_images/image.png) */\n        // Verify that user can see image in custom tutorials by providing relative path in md file\n        // const imageRelative = await workbenchPage.getTutorialImageByAlt(imageRelativePath);\n        // await workbenchPage.waitUntilImageRendered(imageRelative);\n        // const imageRelativeHeight = await imageRelative.getStyleProperty('height');\n        // await t.expect(parseInt(imageRelativeHeight.replace(/[^\\d]/g, ''))).gte(150);\n\n        // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench\n        await t.click(tutorials.closeEnablementPage);\n        await t.click(tutorials.tutorialLatestDeleteIcon);\n        await t.expect(tutorials.tutorialDeleteButton.visible).ok('Delete popup is not visible');\n        await t.click(tutorials.tutorialDeleteButton);\n        await t.expect(tutorials.tutorialDeleteButton.exists).notOk('Delete popup is still visible');\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).exists)\n            .notOk(`${tutorialName} tutorial is not uploaded`);\n    });\n// https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4213, https://redislabs.atlassian.net/browse/RI-4302\ntest\n    .after(async() => {\n        tutorialName = 'Tutorials with manifest';\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        if(await tutorials.tutorialAccordionButton.withText(tutorialName).exists) {\n            await tutorials.deleteTutorialByName(tutorialName);\n        }\n    })('Verify that user can upload tutorial with URL with manifest.json', async t => {\n        const labelFromManifest = 'Working with JSON label';\n        const link = 'https://github.com/RedisInsight/RedisInsight/raw/9155d0241f6937c213893a29fe24c2f560cd48f3/tests/e2e/test-data/upload-tutorials/TutorialsWithManifest.zip';\n        internalLinkName1 = 'manifest-id';\n        tutorialName = 'Tutorials with manifest';\n        const summary = 'Summary for JSON';\n\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await tutorials.toggleMyTutorialPanel();\n        await t.click(tutorials.tutorialOpenUploadButton);\n        // Verify that user can upload tutorials using a URL\n        await t.typeText(tutorials.tutorialLinkField, link);\n        await t.click(tutorials.tutorialSubmitButton);\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).with({ timeout: 20000 }).visible)\n            .ok(`${tutorialName} tutorial is not uploaded`);\n        await t.click(tutorials.tutorialAccordionButton.withText(tutorialName));\n        // Verify that User can see the same structure in the tutorial uploaded as described in the .json manifest\n        await t.expect(tutorials.getInternalLinkWithoutManifest(internalLinkName1).visible)\n            .ok(`${internalLinkName1} folder specified in manifest is not visible`);\n        await t.expect(tutorials.getInternalLinkWithoutManifest(internalLinkName1).textContent)\n            .contains(labelFromManifest, `${labelFromManifest} tutorial specified in manifest is not visible`);\n        await t.expect(tutorials.getInternalLinkWithoutManifest(internalLinkName1).textContent)\n            .contains(summary, `${summary} tutorial specified in manifest is not visible`);\n        await t.click(tutorials.getInternalLinkWithoutManifest(internalLinkName1));\n        await t.expect(tutorials.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked');\n        await t.click(tutorials.closeEnablementPage);\n        await t.click(tutorials.tutorialLatestDeleteIcon);\n        await t.expect(tutorials.tutorialDeleteButton.visible).ok('Delete popup is not visible');\n        await t.click(tutorials.tutorialDeleteButton);\n        await t.expect(tutorials.tutorialDeleteButton.exists).notOk('Delete popup is still visible');\n        // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).exists)\n            .notOk(`${tutorialName} tutorial is not uploaded`);\n    });\n// https://redislabs.atlassian.net/browse/RI-4352\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n\n        tutorialName = `${zipFolderName}${Common.generateWord(5)}`;\n        zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`);\n        // Create zip file for uploading\n        await Common.createZipFromFolder(folderPath, zipFilePath);\n    })\n    .after(async() => {\n        // Delete exported file\n        fs.unlinkSync(joinPath(fileDownloadPath, foundExportedFiles[0]));\n        await Common.deleteFileFromFolder(zipFilePath);\n        await deleteAllKeysFromDB(ossStandaloneConfigEmpty.host, ossStandaloneConfigEmpty.port);\n        // Clear and delete database\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await tutorials.deleteTutorialByName(tutorialName);\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).exists)\n            .notOk(`${tutorialName} tutorial is not deleted`);\n    })('Verify that user can bulk upload data from custom tutorial', async t => {\n        const allKeysResults = ['9Commands Processed', '9Success', '0Errors'];\n        const absolutePathResults = ['1Commands Processed', '1Success', '0Errors'];\n        const invalidPathes = ['Invalid relative', 'Invalid absolute'];\n        const keyNames = ['hashkey1', 'listkey1', 'setkey1', 'zsetkey1', 'stringkey1', 'jsonkey1', 'streamkey1', 'graphkey1', 'tskey1', 'stringkey1test'];\n        internalLinkName1 = 'probably-1';\n        // todo: this tests doesn't wotk locally because of different \"fileStart\" value\n        const fileStarts = 'Upload';\n\n        // Upload custom tutorial\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await tutorials.toggleMyTutorialPanel();\n        await t\n            .click(tutorials.tutorialOpenUploadButton)\n            .setFilesToUpload(tutorials.tutorialImport, [zipFilePath])\n            .click(tutorials.tutorialSubmitButton);\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`);\n        // Open tutorial\n        await t\n            .click(tutorials.tutorialAccordionButton.withText(tutorialName))\n            .click(tutorials.getAccordionButtonWithName(folder1))\n            .click(tutorials.getInternalLinkWithManifest(internalLinkName1));\n        await t.expect(tutorials.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked');\n\n        // Verify that user can bulk upload data by relative path\n\n        // Remember the number of files in Temp\n        const numberOfDownloadFiles = await databasesActions.getFileCount(fileDownloadPath, fileStarts);\n        await t.click(tutorials.uploadDataBulkBtn.withText('Upload relative'));\n        await t.click(tutorials.downloadFileBtn);\n        foundExportedFiles = await databasesActions.findFilesByFileStarts(fileDownloadPath, fileStarts);\n        await t.expect(await databasesActions.getFileCount(fileDownloadPath, fileStarts)).gt(numberOfDownloadFiles, 'The tutorials files not saved', { timeout: 5000 });\n\n        await t.click(tutorials.uploadDataBulkApplyBtn);\n        // Verify that user can see the summary when the command execution is completed\n        await verifyCompletedResultText(allKeysResults);\n\n        // Verify that user can bulk upload data by absolute path\n        await t.click(tutorials.uploadDataBulkBtn.withText('Upload absolute'));\n        await t.click(tutorials.uploadDataBulkApplyBtn);\n        await verifyCompletedResultText(absolutePathResults);\n\n        // Verify that user can't upload file by invalid relative path\n        // Verify that user can't upload file by invalid absolute path\n        for (const path of invalidPathes) {\n            await t.click(tutorials.uploadDataBulkBtn.withText(path));\n            await t.click(tutorials.uploadDataBulkApplyBtn);\n            // Verify that user can see standard error messages when any error occurs while finding the file or parsing it\n            await t.expect(workbenchPage.Toast.toastError.textContent).contains('Data file was not found', 'Bulk upload not failed');\n            await t.click(workbenchPage.Toast.toastCancelBtn);\n        }\n\n        // Open Browser page\n        await t.click(browserPage.NavigationTabs.browserButton);\n        // Verify that keys of all types can be uploaded\n        await browserPage.searchByKeyName('*key1*');\n        await verifyKeysDisplayingInTheList(keyNames, true);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(\n            ossStandaloneConfigEmpty\n        );\n        await myRedisDatabasePage.reloadPage();\n        tutorialName = `${zipFolderName}${Common.generateWord(5)}`;\n        zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`);\n        // Create zip file for uploading\n        await Common.createZipFromFolder(folderPath, zipFilePath);\n    })\n    .after(async() => {\n        await Common.deleteFileFromFolder(zipFilePath);\n        // Clear and delete database\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await tutorials.deleteTutorialByName(tutorialName);\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).exists)\n            .notOk(`${tutorialName} tutorial is not deleted`);\n    })('Verify that user can open tutorial from links in other tutorials', async t => {\n        // Upload custom tutorial\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await tutorials.toggleMyTutorialPanel();\n        await t\n            .click(tutorials.tutorialOpenUploadButton)\n            .setFilesToUpload(tutorials.tutorialImport, [zipFilePath])\n            .click(tutorials.tutorialSubmitButton);\n        await t.expect(tutorials.tutorialAccordionButton.withText(tutorialName).visible).ok(`${tutorialName} tutorial is not uploaded`);\n        // Open tutorial\n        await t\n            .click(tutorials.tutorialAccordionButton.withText(tutorialName))\n            .click(tutorials.getAccordionButtonWithName(folder2))\n            .click(tutorials.getInternalLinkWithManifest(internalLinkName2));\n        await t.expect(tutorials.scrolledEnablementArea.visible).ok('Enablement area is not visible after clicked');\n\n        // Verify that user do not see the standard popover when open a tutorial page via the link\n        await t.click(tutorials.tutorialLink.withText('linkTheSamePage'));\n        await t.expect(tutorials.getTutorialByName('CREATE DOCUMENTS').exists).ok('Tutorial not opened by link');\n\n        // Verify that user can see a standard popover to open a database when clicking a link where page is inside of the database which is not opened\n        await t.click(tutorials.closeEnablementPage);\n        await t.click(tutorials.getInternalLinkWithManifest(internalLinkName2));\n        await t.click(tutorials.tutorialLink.withText('link2AnalyticsPageWithTutorial'));\n        await t.expect(tutorials.openDatabasePopover.exists).ok('Open a database popover is not displayed');\n\n        // Verify that user not redirected anywhere and do not see an error when clicking on the broken link\n        await t.click(tutorials.tutorialLink.withText('link3InvalidPage'));\n        await t.expect(tutorials.getTutorialByName('VECTOR 2').exists).ok('Tutorial page is changed');\n        // await t.expect(tutorials.openDatabasePopover.exists).notOk('Open a database popover is still displayed');\n        await t.click(tutorials.tutorialLink.withText('link4InvalidTutorial'));\n        await t.expect(tutorials.getTutorialByName('VECTOR 2').exists).ok('Tutorial page is changed');\n        // await t.expect(tutorials.openDatabasePopover.exists).notOk('Open a database popover is still displayed');\n\n        // Open existing database\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfigEmpty.databaseName);\n\n        // Verify that user can use '[link](redisinsight:_?tutorialId={tutorialId})' syntax to cross-reference tutorials\n        await t.click(tutorials.tutorialLink.withText('link2AnalyticsPageWithTutorial'));\n        await t.expect(tutorials.getTutorialByName('INTRODUCTION').exists).ok('Tutorial not opened by link');\n        await t.expect(memoryEfficiencyPage.analysisPage.visible).ok('Analysis page is not opened by link');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts",
    "content": "import * as path from 'path';\nimport { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { ExploreTabs, RecommendationIds, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneV5Config, ossStandaloneV7Config } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { Telemetry } from '../../../../helpers/telemetry';\nimport { RecommendationsActions } from '../../../../common-actions/recommendations-actions';\nimport { modifyFeaturesConfigJson, refreshFeaturesTestData, updateControlNumber } from '../../../../helpers/insights';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst telemetry = new Telemetry();\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst recommendationsActions = new RecommendationsActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst databasesForAdding = [\n    { host: ossStandaloneV5Config.host, port: ossStandaloneV5Config.port, databaseName: ossStandaloneV5Config.databaseName },\n    { host: ossStandaloneV7Config.host, port: ossStandaloneV7Config.port, databaseName: ossStandaloneV7Config.databaseName }\n];\nconst tenSecondsTimeout = 10000;\nconst keyName = `recomKey-${Common.generateWord(10)}`;\nconst logger = telemetry.createLogger();\nconst telemetryEvent = 'INSIGHTS_TIPS_VOTED';\nconst expectedProperties = [\n    'buildType',\n    'databaseId',\n    'name',\n    'provider',\n    'vote'\n];\nconst featuresConfig = path.join('.', 'test-data', 'features-configs', 'insights-valid.json');\nconst redisVersionRecom = RecommendationIds.redisVersion;\nconst redisTimeSeriesRecom = RecommendationIds.optimizeTimeSeries;\nconst searchVisualizationRecom = RecommendationIds.searchVisualization;\nconst setPasswordRecom = RecommendationIds.setPassword;\n\nfixture `Live Recommendations`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await refreshFeaturesTestData();\n        await modifyFeaturesConfigJson(featuresConfig);\n        await updateControlNumber(47.2);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    })\n    .afterEach(async() => {\n        await refreshFeaturesTestData();\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .before(async() => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await refreshFeaturesTestData();\n        await modifyFeaturesConfigJson(featuresConfig);\n        await updateControlNumber(47.2);\n        await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding);\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName);\n\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await myRedisDatabasePage.reloadPage();\n    })\n    .after(async() => {\n        // Clear and delete database\n        await browserPage.NavigationHeader.togglePanel(false);\n        await refreshFeaturesTestData();\n        await browserPage.OverviewPanel.changeDbIndex(0);\n        await apiKeyRequests.deleteKeyByNameApi(keyName, databasesForAdding[1].databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding);\n    })\n    .skip('Verify Insights panel Recommendations displaying', async t => {\n        await browserPage.NavigationHeader.togglePanel(true);\n        // Verify that \"Welcome to recommendations\" panel displayed when there are no recommendations\n        let tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        await t\n            .expect(tab.noRecommendationsScreen.exists).ok('No tips panel not displayed')\n            .expect(tab.noRecommendationsScreen.textContent).contains('Welcome toTips!', 'Welcome to recommendations text not displayed');\n\n        await browserPage.NavigationHeader.togglePanel(false);\n        // Go to 2nd database\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName);\n        await browserPage.NavigationHeader.togglePanel(true);\n        // Verify that live recommendations displayed for each database separately\n        // Verify that user can see the live recommendation \"Update Redis database\" when Redis database is less than 6.0 highlighted as RedisStack\n        tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        await t\n            .expect(await tab.getRecommendationByName(redisVersionRecom).visible).ok('Redis Version recommendation not displayed')\n            .expect(await tab.getRecommendationByName(redisTimeSeriesRecom).visible).notOk('Optimize Time Series recommendation displayed');\n        await browserPage.NavigationHeader.togglePanel(false);\n\n        // Create Sorted Set with TimeSeries value\n        await browserPage.addZSetKey(keyName, '151153320500121', '231231251', '1511533205001:21');\n        // Verify that the list of recommendations updated every 10 seconds\n        await t.wait(tenSecondsTimeout);\n        await browserPage.NavigationHeader.togglePanel(true);\n        tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        // Verify that user can see the live recommendation \"Optimize the use of time series\"\n        await t.expect(await tab.getRecommendationByName(redisTimeSeriesRecom).visible).ok('Optimize Time Series recommendation not displayed');\n        await tab.clickOnTutorialLink(redisTimeSeriesRecom);\n        const tabTutorial = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.expect(tabTutorial.preselectArea.textContent).contains('INTRODUCTION', 'the tutorial page is incorrect');\n        await t.expect(tabTutorial.preselectArea.textContent).contains('Time Series', 'the tutorial is incorrect');\n    });\ntest\n    .requestHooks(logger)\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await refreshFeaturesTestData();\n        await modifyFeaturesConfigJson(featuresConfig);\n        await updateControlNumber(47.2);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config);\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n    }).after(async() => {\n        await refreshFeaturesTestData();\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config);\n    })\n    .skip('Verify that user can upvote recommendations', async t => {\n        const notUsefulVoteOption = 'not useful';\n        const usefulVoteOption = 'useful';\n        await browserPage.NavigationHeader.togglePanel(true);\n        await t.expect(await browserPage.InsightsPanel.getActiveTabName()).contains(ExploreTabs.Tips);\n        await recommendationsActions.voteForRecommendation(redisVersionRecom, notUsefulVoteOption);\n        // Verify that user can rate recommendations with one of 2 existing types at the same time\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecom, notUsefulVoteOption);\n\n        // Verify that user can see the popup with link when he votes for “Not useful”\n        await recommendationsActions.verifyVotePopUpIsDisplayed(redisVersionRecom, notUsefulVoteOption);\n\n        // Verify that the INSIGHTS_RECOMMENDATIONS_VOTED event sent with Database ID, Recommendation_name, Vote type parameters when user voted for recommendation\n        await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n        await telemetry.verifyEventPropertyValue(telemetryEvent, 'name', 'redisVersion', logger);\n        await telemetry.verifyEventPropertyValue(telemetryEvent, 'vote', notUsefulVoteOption, logger);\n\n        // Verify that user can see previous votes when reload the page\n        await browserPage.reloadPage();\n        await browserPage.NavigationHeader.togglePanel(true);\n        const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        await tab.toggleRecommendation(redisVersionRecom, true);\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecom, notUsefulVoteOption);\n\n        // Verify that user can change previous votes\n        await recommendationsActions.voteForRecommendation(redisVersionRecom, usefulVoteOption);\n        // Verify that user can rate recommendations with one of 2 existing types at the same time\n        await recommendationsActions.verifyVoteIsSelected(redisVersionRecom, usefulVoteOption);\n    });\ntest\n    .skip('Verify that user can hide recommendations and checkbox value is saved', async t => {\n    const commandToGetRecommendation = 'FT.INFO';\n    await browserPage.Cli.sendCommandInCli(commandToGetRecommendation);\n\n    await browserPage.NavigationHeader.togglePanel(true);\n    let tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    await t.click(browserPage.InsightsPanel.closeButton);\n    await browserPage.NavigationHeader.togglePanel(true);\n    await t.expect(await browserPage.InsightsPanel.getActiveTabName()).eql(ExploreTabs.Tips);\n    await tab.toggleShowHiddenRecommendations(false);\n    await tab.hideRecommendation(searchVisualizationRecom);\n    await t.expect(await tab.getRecommendationByName(searchVisualizationRecom).exists)\n        .notOk('recommendation is displayed when show hide recommendation is unchecked');\n\n    // check recommendation state is saved after reload\n    await browserPage.reloadPage();\n    await browserPage.NavigationHeader.togglePanel(true);\n    tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    await t.expect(await tab.getRecommendationByName(searchVisualizationRecom).exists)\n        .notOk('recommendation is displayed when show hide recommendation is unchecked');\n\n    // check value saved to show hidden recommendations\n    await tab.toggleShowHiddenRecommendations(true);\n    await t.expect(await tab.getRecommendationByName(searchVisualizationRecom).visible)\n        .ok('recommendation is not displayed when show hide recommendation is checked');\n    await browserPage.reloadPage();\n    await browserPage.NavigationHeader.togglePanel(true);\n    tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    await t.expect(await tab.getRecommendationByName(searchVisualizationRecom).visible)\n        .ok('recommendation is not displayed when show hide recommendation is checked');\n});\ntest\n    .skip('Verify that user can snooze recommendation', async t => {\n    const commandToGetRecommendation = 'FT.INFO';\n    await browserPage.Cli.sendCommandInCli(commandToGetRecommendation);\n\n    await browserPage.NavigationHeader.togglePanel(true);\n    let tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    await tab.snoozeRecommendation(searchVisualizationRecom);\n\n    await browserPage.reloadPage();\n    await browserPage.NavigationHeader.togglePanel(true);\n    tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    await t.expect(await tab.getRecommendationByName(searchVisualizationRecom).visible)\n        .notOk('recommendation is displayed when after snoozing');\n    await browserPage.NavigationHeader.togglePanel(false);\n    await browserPage.Cli.sendCommandInCli(commandToGetRecommendation);\n    await browserPage.NavigationHeader.togglePanel(true);\n    tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    await t.expect(await tab.getRecommendationByName(searchVisualizationRecom).visible).ok('recommendation is not displayed again');\n});\ntest\n    .skip('Verify that recommendations from database analysis are displayed in Insight panel above live recommendations', async t => {\n    await browserPage.NavigationHeader.togglePanel(true);\n    let tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    const redisVersionRecommendationSelector = tab.getRecommendationByName(redisVersionRecom);\n    // Verify that live recommendation displayed in Insights panel\n    await t.expect(await tab.getRecommendationByName(redisVersionRecom).visible).ok(`${redisVersionRecom} recommendation not displayed`);\n    // Verify that recommendation from db analysis not displayed in Insights panel\n    await t.expect(await tab.getRecommendationByName(setPasswordRecom).visible).notOk(`${setPasswordRecom} recommendation displayed`);\n    await browserPage.NavigationHeader.togglePanel(false);\n    // Go to Analysis Tools page\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(memoryEfficiencyPage.newReportBtn);\n    await browserPage.NavigationHeader.togglePanel(true);\n    tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n    // Verify that recommendations are synchronized\n    await t.expect(await tab.getRecommendationByName(setPasswordRecom).visible).ok('Recommendations are not synchronized');\n    // Verify that duplicates are not displayed\n    await t.expect(redisVersionRecommendationSelector.count).eql(1, `${redisVersionRecom} recommendation duplicated`);\n});\n//https://redislabs.atlassian.net/browse/RI-4413\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await refreshFeaturesTestData();\n        await modifyFeaturesConfigJson(featuresConfig);\n        await updateControlNumber(47.2);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV7Config);\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV7Config.databaseName);\n    }).after(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV7Config);\n        await refreshFeaturesTestData();\n    })('Verify that if user clicks on the Analyze button and link, the pop up with analyze button is displayed and new report is generated', async t => {\n        await browserPage.NavigationHeader.togglePanel(true);\n        let tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        await t.click(tab.analyzeDatabaseButton);\n        await t.click(tab.analyzeTooltipButton);\n        //Verify that user is navigated to DB Analysis page via Analyze button and new report is generated\n        await t.click(memoryEfficiencyPage.selectedReport);\n        await t.expect(memoryEfficiencyPage.reportItem.visible).ok('Database analysis page not opened');\n        await t.click(browserPage.NavigationTabs.browserButton);\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        await t.click(tab.analyzeDatabaseLink);\n        await t.click(tab.analyzeTooltipButton);\n        //Verify that user is navigated to DB Analysis page via Analyze link and new report is generated\n        await t.click(memoryEfficiencyPage.selectedReport);\n        await t.expect(memoryEfficiencyPage.reportItem.count).eql(2, 'report was not generated');\n    });\n//https://redislabs.atlassian.net/browse/RI-4493\ntest\n    .after(async() => {\n        await refreshFeaturesTestData();\n        await browserPage.deleteKeyByName(keyName);\n        await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding);\n    })\n    .skip('Verify that key name is displayed for Insights and DA recommendations', async t => {\n        const cliCommand = `JSON.SET ${keyName} $ '{ \"model\": \"Hyperion\", \"brand\": \"Velorim\"}'`;\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await browserPage.Cli.sendCommandInCli(cliCommand);\n        await t.click(browserPage.refreshKeysButton);\n        await browserPage.NavigationHeader.togglePanel(true);\n        const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips);\n        let keyNameFromRecommendation = await tab.getRecommendationByName(RecommendationIds.searchJson)\n            .find(tab.cssKeyName)\n            .innerText;\n        await t.expect(keyNameFromRecommendation).eql(keyName);\n        await t.click(tab.analyzeDatabaseLink);\n        await t.click(tab.analyzeTooltipButton);\n        await t.click(memoryEfficiencyPage.recommendationsTab);\n        keyNameFromRecommendation = await tab.getRecommendationByName(RecommendationIds.searchJson)\n            .find(tab.cssKeyName)\n            .innerText;\n        await t.expect(keyNameFromRecommendation).eql(keyName);\n        await t.click(memoryEfficiencyPage.NavigationTabs.browserButton);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts",
    "content": "import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { Compatibility, ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    commonUrl,\n    ossStandaloneConfig,\n    ossStandaloneV5Config\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\n\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Open insights panel`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.deleteAllDatabasesApi();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneV5Config);\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig, true);\n        await browserPage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n    })\n    .after(async() => {\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    .skip('Verify that insights panel is opened in cloud db if users db does not have some module', async t => {\n        await t.click(browserPage.redisearchModeBtn);\n        await t.click(browserPage.Modal.closeModalButton);\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await t.expect(browserPage.InsightsPanel.sidePanel.exists).ok('Insights panel is not opened');\n        await t.expect(await browserPage.InsightsPanel.existsCompatibilityPopover.textContent).contains('Redis Query Engine', 'popover is not displayed');\n        const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.expect(tab.preselectArea.textContent).contains('How To Query Your Data', 'the tutorial is incorrect');\n\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await workbenchPage.sendCommandInWorkbench('TS.');\n\n        await t.click(browserPage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await t.expect(browserPage.InsightsPanel.sidePanel.exists).ok('Insights panel is not opened');\n        await t.expect(await browserPage.InsightsPanel.existsCompatibilityPopover.textContent).contains('Time series data structure', 'popover is not displayed');\n        await t.expect(tab.preselectArea.textContent).contains('Time Series', 'the tutorial is incorrect');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/monitor/monitor.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport {\n    MyRedisDatabasePage,\n    SettingsPage,\n    BrowserPage\n} from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneConfig,\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst settingsPage = new SettingsPage();\nconst browserPage = new BrowserPage();\nconst chance = new Chance();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Monitor`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify Monitor refresh/stop', async t => {\n    // Run monitor\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    // Close Monitor\n    await t.click(browserPage.Profiler.closeMonitor);\n    // Verify that monitor is not displayed\n    await t.expect(browserPage.Profiler.monitorArea.visible).notOk('Profiler area not found');\n    // Verify that user open monitor again\n    await t.click(browserPage.Profiler.expandMonitor);\n    // Verify that when user closes the Monitor by clicking on \"Close Monitor\" button Monitor stopped\n    await t.expect(browserPage.Profiler.startMonitorButton.visible).ok('Start profiler button not found');\n\n    // Run monitor\n    await t.click(browserPage.Profiler.startMonitorButton);\n    await browserPage.Profiler.checkCommandInMonitorResults('info');\n    // Click on Stop Monitor button\n    await t.click(browserPage.Profiler.runMonitorToggle);\n    // Check that Monitor is stopped\n    await t.expect(browserPage.Profiler.resetProfilerButton.visible).ok('Reset profiler button not appeared');\n    // Get the last log line\n    const lastTimestamp = await browserPage.Profiler.monitorCommandLineTimestamp.nth(-1).textContent;\n    // Click on refresh keys to get new logs\n    await t.click(browserPage.refreshKeysButton);\n    // Verify that Monitor is stopped when user clicks on \"Stop\" button\n    await t.expect(browserPage.Profiler.monitorCommandLineTimestamp.nth(-1).textContent).eql(lastTimestamp, 'The last line of monitor logs not correct');\n\n    // Run monitor\n    await t.click(browserPage.Profiler.resetProfilerButton);\n    await t.click(browserPage.Profiler.startMonitorButton);\n    await browserPage.Profiler.checkCommandInMonitorResults('info');\n    // Refresh the page\n    await browserPage.reloadPage();\n    // Check that monitor is closed\n    await t.expect(browserPage.Profiler.monitorArea.exists).notOk('Monitor area not found');\n    // Verify that when user refreshes the page the list of results in Monitor is not saved\n    await t.click(browserPage.Profiler.expandMonitor);\n    await t.expect(browserPage.Profiler.monitorWarningMessage.exists).ok('Warning message in monitor not found');\n\n    // Run monitor\n    await t.click(browserPage.Profiler.startMonitorButton);\n\n    await browserPage.Profiler.checkCommandInMonitorResults('info', undefined, true, 10000);\n    // Click on refresh keys to get new logs\n    await t.click(browserPage.refreshKeysButton);\n    // Get last timestamp\n    const lastTimestampSelector = browserPage.Profiler.monitorCommandLineTimestamp.nth(-1);\n    // Stop Monitor\n    await browserPage.Profiler.stopMonitor();\n    // Click on Clear button\n    await t.click(browserPage.Profiler.clearMonitorButton);\n    // Verify that when user clicks on \"Clear\" button in Monitor, all commands history is removed\n    await t.expect(lastTimestampSelector.exists).notOk('Cleared last line not found');\n});\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionAdvancedSettings);\n        await settingsPage.changeKeysToScanValue('20000000');\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneBigConfig.databaseName);\n    })\n    .after(async t => {\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionAdvancedSettings);\n        await settingsPage.changeKeysToScanValue('10000');\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig);\n    })('Verify that user can see monitor results in high DB load', async t => {\n        // Run monitor\n        await browserPage.Profiler.startMonitorAndVerifyStart();\n        // Search by not existed key pattern\n        await browserPage.searchByKeyName(`${chance.string({ length: 10 })}*`);\n        // Check that the last child is updated\n        for (let i = 0; i <= 10; i++) {\n            const previousTimestamp = await browserPage.Profiler.monitorCommandLineTimestamp.nth(-1).textContent;\n            await t.wait(5500);\n            const nextTimestamp = await browserPage.Profiler.monitorCommandLineTimestamp.nth(-1).textContent;\n            await t.expect(previousTimestamp).notEql(nextTimestamp, 'Monitor results not correct');\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/monitor/save-commands.e2e.ts",
    "content": "import * as fs from 'fs';\nimport * as os from 'os';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst tempDir = os.tmpdir();\n\nfixture `Save commands`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that when clicks on “Reset Profiler” button he brought back to Profiler home screen', async t => {\n    // Start Monitor without Save logs\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    // Remember the number of files in Temp\n    const numberOfTempFiles = fs.readdirSync(tempDir).length;\n    // Reset profiler\n    await browserPage.Profiler.resetProfiler();\n    //Check the screen\n    await t.expect(browserPage.Profiler.monitorNotStartedElement.visible).ok('The Profiler home screen not appeared');\n    await t.click(browserPage.Profiler.closeMonitor);\n    // Start Monitor with Save logs\n    await browserPage.Profiler.startMonitorWithSaveLog();\n    // Reset profiler\n    await browserPage.Profiler.resetProfiler();\n    // Check the screen\n    await t.expect(browserPage.Profiler.monitorNotStartedElement.visible).ok('The Profiler home screen not appeared');\n    await t.expect(browserPage.Profiler.monitorIsStartedText.visible).notOk('The current Profiler session is not closed');\n    // temporary Log file is deleted\n    await t.expect(numberOfTempFiles).eql(fs.readdirSync(tempDir).length, 'The temporary Log file is not deleted');\n});\ntest('Verify that when user clears the Profiler he doesn\\'t brought back to Profiler home screen', async t => {\n    // Start Monitor\n    await browserPage.Profiler.startMonitorAndVerifyStart();\n    // Clear monitor and check the view\n    await t.click(browserPage.Profiler.clearMonitorButton);\n    await t.expect(browserPage.Profiler.monitorNotStartedElement.visible).notOk('Profiler home screen is still opened after Clear');\n    await t.click(browserPage.Profiler.closeMonitor);\n    // Start Monitor with Save logs\n    await browserPage.Profiler.startMonitorWithSaveLog();\n    // Clear monitor and check the view\n    await t.click(browserPage.Profiler.clearMonitorButton);\n    await t.expect(browserPage.Profiler.monitorNotStartedElement.visible).notOk('Profiler home screen is still opened after Clear');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/pub-sub/debug-mode.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, PubSubPage } from '../../../../pageObjects'\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst pubSubPage = new PubSubPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture.skip `PubSub debug mode`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to PubSub page and subscribe to channel\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n        await t.click(pubSubPage.subscribeButton);\n        // Publish different messages\n        await pubSubPage.Cli.sendCommandInCli('10 publish channel first');\n        await pubSubPage.Cli.sendCommandInCli('10 publish channel second');\n        await pubSubPage.Cli.sendCommandInCli('10 publish channel third');\n    })\n    .afterEach(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\n\ntest('Example of new test', async t => {\n    // Shouldn't see message with text first on 1st table page\n    await t.expect(pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText('first').visible).notOk('Oldest message is shown on the first table page');\n    // Navigate to last table page\n    await t.click(pubSubPage.messagesTableLastPageBtn);\n    // Should have the oldest messages on the last table page\n    await t.expect(pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText('first').visible).ok('Oldest message should be shown on the last table page');\n    // Navigate to first table page\n    await t.click(pubSubPage.messagesTableFirstPageBtn);\n\n    // ...\n})\ntest('Verify that when user navigating away and back to pubsub window the debug mode state will be reset to default auto-scroll', async t => {\n    // Scroll to the first messages\n    await t.scrollIntoView(pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText('first'));\n    // Verify that new messages keep arriving in the background and user can always see up to date count of total messages received.\n    await pubSubPage.publishMessage('test', 'message sent in the background');\n    await t.expect(pubSubPage.totalMessagesCount.textContent).contains('31', 'Total counter value is incorrect');\n    // Verify that when user scroll away from the newest message the auto-scroll is stopped\n    await pubSubPage.Cli.sendCommandInCli('30 publish channel additionalMessages');\n    await pubSubPage.publishMessage('test', 'new message with no scroll');\n    await verifyMessageDisplayingInPubSub('new message with no scroll', false);\n    // Go to Browser Page\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Go to PubSub page\n    await t.click(browserPage.NavigationTabs.pubSubButton);\n    // Verify that the debug mode state is reset to default auto-scroll\n    await verifyMessageDisplayingInPubSub('new message with no scroll', true);\n});\ntest('Verify that when user scroll all the way to the newest available message (down), auto-scroll resumes automatically.', async t => {\n    // Scroll to the first messages\n    await t.scrollIntoView(pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText('first'));\n    await pubSubPage.publishMessage('test', 'message to scroll');\n    await t.scrollIntoView(pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText('message to scroll'));\n    await pubSubPage.Cli.sendCommandInCli('20 publish channel fourth');\n    // Verify auto-scroll resumes automatically\n    await verifyMessageDisplayingInPubSub('fourth', true);\n});\ntest('Verify that user can get to the newest message in one click', async t => {\n    // Scroll to the first messages\n    await t.scrollIntoView(pubSubPage.pubSubPageContainer.find(pubSubPage.cssSelectorMessage).withText('first'));\n    await pubSubPage.publishMessage('test', 'new message');\n    await t.click(pubSubPage.scrollDownButton);\n    // Verify the user scrolled to the newest message\n    await verifyMessageDisplayingInPubSub('new message', true);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, PubSubPage } from '../../../../pageObjects'\nimport { commonUrl, ossStandaloneConfig, ossClusterConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst pubSubPage = new PubSubPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `PubSub OSS Cluster 7 tests`\n    .meta({ type: 'regression' })\n    .page(commonUrl)\n\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewOSSClusterDatabaseApi(ossClusterConfig);\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossClusterConfig.ossClusterDatabaseName);\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n    })\n    .afterEach(async() => {\n        await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig);\n    });\ntest\n\n    .meta({ rte: rte.ossCluster })('Verify that SPUBLISH message is displayed for OSS Cluster 7 database', async t => {\n        await t.expect(pubSubPage.ossClusterEmptyMessage.exists).ok('SPUBLISH message not displayed');\n        // Verify that user can see published messages for OSS Cluster 7\n        await t.click(pubSubPage.subscribeButton);\n        // Publish different messages\n        await pubSubPage.Cli.sendCommandInCli('50 publish channel oss_cluster_message');\n        await verifyMessageDisplayingInPubSub('oss_cluster_message', true);\n        // Verify that SPUBLISHED messages are not displayed for OSS Cluster 7\n        await pubSubPage.Cli.sendCommandInCli('10 spublish channel oss_cluster_message_spublish');\n        await verifyMessageDisplayingInPubSub('oss_cluster_message_spublish', false);\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabaseApi(ossStandaloneConfig);\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await t.click(browserPage.NavigationTabs.pubSubButton);\n    })\n    .after(async() => {\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })\n    .meta({ rte: rte.standalone })('Verify that SPUBLISH message is not displayed for other databases expect OSS Cluster 7', async t => {\n        await t.expect(pubSubPage.ossClusterEmptyMessage.exists).notOk('No SPUBLISH message still displayed');\n        // Verify that user can't see published messages for Standalone DB\n        await t.click(pubSubPage.subscribeButton);\n        await pubSubPage.Cli.sendCommandInCli('10 spublish channel oss_cluster_message_spublish');\n        await verifyMessageDisplayingInPubSub('oss_cluster_message_spublish', false);\n    });\n\ntest.meta({ rte: rte.ossCluster })('Verify that PSUBSCRIBE works, that user can specify channel name to subscribe', async t => {\n    const channelsName = 'first second third';\n    const namesList = channelsName.split(' ');\n\n    await t.expect(pubSubPage.channelsSubscribeInput.value).eql('*', 'the default value is not set');\n    await t.typeText(pubSubPage.channelsSubscribeInput, channelsName, { replace: true });\n    await t.click(pubSubPage.subscribeButton);\n    await t.expect(pubSubPage.channelsSubscribeInput.hasAttribute('disabled')).ok('the field is not disabled after subscribe');\n    await pubSubPage.publishMessage(namesList[0], 'published message');\n    await verifyMessageDisplayingInPubSub('published message', true);\n    await pubSubPage.publishMessage(namesList[1], 'second message');\n    await verifyMessageDisplayingInPubSub('second message', true);\n    await pubSubPage.publishMessage('not exist', 'not exist message');\n    await verifyMessageDisplayingInPubSub('not exist message', false);\n\n    await t.expect(pubSubPage.patternsCount.textContent).contains(namesList.length.toString(), 'patterns count is not calculated correctly');\n    await t.expect(pubSubPage.messageCount.textContent).contains('2', 'message count is not calculated correctly');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/settings/settings.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport {\n    BrowserPage,\n    MemoryEfficiencyPage,\n    MyRedisDatabasePage,\n    SettingsPage,\n    WorkbenchPage\n} from '../../../../pageObjects'\nimport {\n    commonUrl, ossClusterConfig,\n} from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common, DatabaseHelper, Telemetry } from '../../../../helpers';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst workbenchPage = new WorkbenchPage();\nconst settingsPage = new SettingsPage();\nconst memoryEfficiencyPage = new MemoryEfficiencyPage();\nconst databaseHelper = new DatabaseHelper();\nconst telemetry = new Telemetry();\n\nconst logger = telemetry.createLogger();\n\nlet keyName = Common.generateWord(20);\n\nconst telemetryEvents = ['SETTINGS_DATE_TIME_FORMAT_CHANGED','DATABASE_ANALYSIS_STARTED'];\nconst settingsExpectedProperties = [\n    'currentFormat'\n];\nconst databaseAnalysisExpectedProperties = [\n    'databaseId',\n    'provider'\n];\n\nfixture `DataTime format setting`\n    .meta({\n        type: 'regression',\n        rte: rte.standalone\n    })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n    })\n    .afterEach(async t => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n        await t.click(workbenchPage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionAppearance);\n        await t.click(settingsPage.commonRadioButton);\n        await settingsPage.selectDataFormatDropdown('HH\\\\:mm\\\\:ss');\n        await settingsPage.selectTimeZoneDropdown('local');\n    });\ntest\n    .requestHooks(logger)\n    .skip('Verify that user can select date time format', async t => {\n    const defaultDateRegExp = /^([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d \\d{1,2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4}$/;\n    const selectedDateReqExp = /^(0[1-9]|[12]\\d|3[01])\\.(0[1-9]|1[0-2])\\.\\d{4} ([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d$/;\n    keyName = `DateTimeTestKey-${Common.generateWord(5)}`;\n    const hashField = '1724674140';\n\n    const selectorForOption = 'dd\\\\.MM\\\\.yyyy';\n    const selectedOption = 'dd.MM.yyyy HH:mm:ss';\n    const zoneSelectOption = 'UTC';\n\n    await browserPage.addHashKey(keyName, '100000', hashField, hashField);\n    await browserPage.openKeyDetails(keyName);\n    await browserPage.selectFormatter('DateTime');\n    await t.expect(defaultDateRegExp.test(await browserPage.getHashKeyValue())).ok('date is not in default format HH:mm:ss d MMM yyyy');\n\n    await t.click(workbenchPage.NavigationPanel.settingsButton);\n    await t.click(settingsPage.accordionAppearance);\n    await settingsPage.selectDataFormatDropdown(selectorForOption);\n    //Verify telemetry event\n    await telemetry.verifyEventHasProperties(telemetryEvents[0], settingsExpectedProperties, logger);\n\n    await t.expect(settingsPage.selectFormatDropdown.textContent).eql(selectedOption, 'option is not selected');\n    await t.expect(selectedDateReqExp.test(await settingsPage.dataPreview.textContent)).ok(`preview is not valid for ${selectedOption}`);\n\n    await t.click(workbenchPage.NavigationPanel.myRedisDBButton);\n    await t.click(browserPage.NavigationTabs.browserButton);\n    await browserPage.openKeyDetails(keyName);\n    await t.expect(selectedDateReqExp.test(await browserPage.getHashKeyValue())).ok(`date is not in selected format ${selectedOption}`);\n\n    await t.click(workbenchPage.NavigationPanel.settingsButton);\n    await t.click(settingsPage.accordionAppearance);\n    await settingsPage.selectTimeZoneDropdown(zoneSelectOption);\n    await t.expect(settingsPage.selectTimezoneDropdown.textContent).eql(zoneSelectOption, 'option is not selected');\n\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench('info');\n    const dateTime = await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent;\n    await t.expect(selectedDateReqExp.test(dateTime)).ok('date is not in default format HH:mm:ss.SSS d MMM yyyy');\n});\n\ntest .requestHooks(logger)\n('Verify that user can set custom date time format', async t => {\n    const enteredFormat = 'MMM dd yyyy/ HH.mm.ss';\n    const enteredDateReqExp = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (0[1-9]|[12]\\d|3[01]) \\d{4}\\/ ([01]\\d|2[0-3])\\.[0-5]\\d\\.[0-5]\\d$/;\n\n    await t.click(workbenchPage.NavigationPanel.settingsButton);\n    await t.click(settingsPage.accordionAppearance);\n    await t.click(settingsPage.customRadioButton);\n    await settingsPage.enterTextInCustom(enteredFormat);\n    await t.expect(enteredDateReqExp.test(await settingsPage.dataPreview.textContent)).ok(`preview is not valid for ${enteredFormat}`);\n    await t.click(settingsPage.saveCustomFormatButton);\n\n    await myRedisDatabasePage.navigateToDatabase(ossClusterConfig.ossClusterDatabaseName);\n    await t.click(browserPage.NavigationTabs.analysisButton);\n    await t.click(memoryEfficiencyPage.databaseAnalysisTab);\n    await t.click(memoryEfficiencyPage.newReportBtn);\n\n    //Verify telemetry event\n    await telemetry.verifyEventHasProperties(telemetryEvents[1], databaseAnalysisExpectedProperties, logger);\n\n    await t.expect(enteredDateReqExp.test((await memoryEfficiencyPage.selectedReport.textContent).trim())).ok(`custom format is not working ${enteredFormat}`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/shortcuts/shortcuts.e2e.ts",
    "content": "// import { ClientFunction } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Shortcuts`\n    .meta({ type: 'regression', rte: rte.none })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest('Verify that user can see a summary of Shortcuts by clicking \"Keyboard Shortcuts\" button in Help Center', async t => {\n    // const link = 'https://github.com/RedisInsight/RedisInsight/releases';\n\n    // Click on help center icon and verify panel\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.exists).ok('Help Center panel is not opened');\n    // Click on Shortcuts option and verify panel\n    await t.click(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterShortcutButton);\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).ok('Shortcuts panel is not opened');\n    // Validate Title and sections of Shortcuts\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).ok('Shortcuts panel is not opened');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsTitle.exists).ok('shortcutsTitle is not opened');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsCLISection.exists).ok('shortcutsCLISection is not displayed');\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsWorkbenchSection.exists).ok('shortcutsWorkbenchSection is not displayed');\n    // Verify that user can close the Shortcuts\n    await t.click(myRedisDatabasePage.ShortcutsPanel.shortcutsCloseButton);\n    await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.exists).notOk('Shortcuts panel is not displayed');\n\n    // Click on the Release Notes in Help Center\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.click(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterReleaseNotesButton);\n    // Verify redirected link opening Release Notes in Help Center\n    await Common.checkURL('https://github.com/redis/RedisInsight/releases');\n});\ntest('Verify that user can see description of the “up” shortcut in the Help Center > Keyboard Shortcuts > Workbench table', async t => {\n    const description = [\n        'Quick-access to command history',\n        'Up Arrow'\n    ];\n    const description2 = [\n        'Use Non-Redis Editor',\n        // 'Shift+Space' // todo: investigate what should be shown. currently \"⇧\" when should be \"Shift+\"?\n    ];\n\n    // Open Shortcuts\n    await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton);\n    await t.click(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterShortcutButton);\n\n    // Verify that user can see the description of the “Shift+Space” keyboard shortcut in the Keyboard Shortcuts\n    for (const element of description2) {\n        await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.textContent).contains(element, 'The user can not see description of the “Shift+Space” shortcut');\n    }\n\n    // Verify that user can see description of the “up” shortcut\n    for (const element of description) {\n        await t.expect(myRedisDatabasePage.ShortcutsPanel.shortcutsPanel.textContent).contains(element, 'The user can not see description of the “up” shortcut');\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { Telemetry } from '../../../../helpers/telemetry';\nimport {\n    commonUrl,\n    ossStandaloneBigConfig,\n    ossStandaloneConfig,\n    ossStandaloneConfigEmpty,\n    ossStandaloneRedisGears\n} from '../../../../helpers/conf';\nimport { ExploreTabs, KeyTypesTexts, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { verifyKeysDisplayingInTheList } from '../../../../helpers/keys';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst telemetry = new Telemetry();\n\nlet keyNames: string[] = [];\nconst telemetryEvent = 'TREE_VIEW_KEY_VALUE_VIEWED';\nconst logger = telemetry.createLogger();\n\nconst expectedProperties = [\n    'databaseId',\n    'keyType',\n    'provider'\n];\n\nfixture `Tree view verifications`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n    });\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Verify that user has load sample data button when there are no keys in the database', async t => {\n        const message = 'Let\\'sstartworkingLoadsampledata+Addkeymanually';\n        const actualMessage = await browserPage.keyListMessage.innerText;\n        const cleanMessage = actualMessage.replace(/[\\s\\n]+/g, '');\n        const capabilities = [{\n            name: 'Search and Query',\n            tutorial: 'How To Query Your Data'\n        },\n        {\n            name: 'JSON',\n            tutorial: 'JSON'\n        },\n        {\n            name: 'Time Series',\n            tutorial: 'Time Series'\n        },\n        {\n            name: 'Probabilistic',\n            tutorial: 'Probabilistic'\n        }];\n\n        await t.click(browserPage.refreshKeysButton);\n        await t.expect(browserPage.guideLinksBtn.count).gte(4);\n        for (const capability of capabilities) {\n            await browserPage.clickGuideLinksByName(capability.name);\n            await t.expect(browserPage.InsightsPanel.sidePanel.exists).ok('Insights panel not opened');\n            const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n            await t.expect(tutorials.closeEnablementPage.textContent)\n                .contains(capability.tutorial, `${capability.tutorial} tutorial not opened from No Keys page`);\n            await t.click(browserPage.InsightsPanel.closeButton);\n        }\n\n        // Verify the message\n        await t.click(browserPage.treeViewButton);\n        await t.expect(cleanMessage).contains(message, 'The message is not displayed');\n        // Verify that user can see the same tutorial opened as on the list of databases when clicking on capabilities\n        await t.click(browserPage.loadSampleDataBtn);\n        await t.click(browserPage.executeBulkKeyLoadBtn);\n        await t.expect(browserPage.Toast.toastBody.textContent).contains('0Errors', 'the info message after upload does not appear');\n        const totalKeysValue = Number(await browserPage.totalKeysNumber.textContent);\n        // the number should be updated after real prod data will be loaded\n        await t.expect(totalKeysValue).gte(100, 'the info message after upload does not appear');\n    });\n\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisGears);\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })\n    .after(async() => {\n        // Delete database\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Verify that user can load sample data if database does not have modules', async t => {\n        // Verify the message\n        await t.click(browserPage.refreshKeysButton);\n        await t.click(browserPage.loadSampleDataBtn);\n        await t.click(browserPage.executeBulkKeyLoadBtn);\n        //verify that there is no errors if some modules are not added\n        await t.expect(browserPage.Toast.toastBody.textContent).contains('0Errors', 'the info message after upload does not appear');\n        const totalKeysValue = Number(await browserPage.totalKeysNumber.textContent);\n        // the number should be updated after real prod data will bw load\n        await t.expect(totalKeysValue).gte(10, 'the info message after upload does not appear');\n    });\n\ntest.requestHooks(logger)('Verify that user can see the total number of keys, the number of keys scanned, the “Scan more” control displayed at the top of Tree view and Browser view', async t => {\n    await browserPage.selectFilterGroupType(KeyTypesTexts.ReJSON);\n    // Verify the controls on the Browser view\n    await t.expect(browserPage.totalKeysNumber.visible).ok('The total number of keys is not displayed on the Browser view');\n    await t.expect(browserPage.scannedValue.visible).ok('The number of keys scanned is not displayed on the Browser view');\n    await t.expect(browserPage.scanMoreButton.visible).ok('The scan more button is not displayed on the Browser view');\n    //Verify the controls on the Tree view\n    await t.click(browserPage.treeViewButton);\n    await t.expect(browserPage.totalKeysNumber.visible).ok('The total number of keys is not displayed on the Tree view');\n    await t.expect(browserPage.scannedValue.visible).ok('The number of keys scanned is not displayed on the Tree view');\n    await t.expect(browserPage.scanMoreButton.visible).ok('The scan more button is not displayed on the Tree view');\n\n    // Verify that telemetry event 'TREE_VIEW_KEY_VALUE_VIEWED' sent\n    await t.click(browserPage.keyItem);\n    await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n});\ntest('Verify that when user deletes the key he can see the key is removed from the folder, the number of keys is reduced, the percentage is recalculated', async t => {\n    const mainFolder = browserPage.TreeView.getFolderSelectorByName('device');\n    // Open the first key in the tree view and remove\n    await t.click(browserPage.treeViewButton);\n    // Verify the default separator\n    await t.click(browserPage.TreeView.treeViewSettingsBtn);\n    await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'The “:” (colon) not used as a default separator for namespaces');\n    // Verify that user can see that “:” (colon) used as a default separator for namespaces and see the number of keys found per each namespace\n    await t.expect(browserPage.TreeView.treeViewKeysNumber.visible).ok('The user can not see the number of keys');\n\n    await t.expect(mainFolder.visible).ok('The key folder is not displayed');\n    await t.click(mainFolder);\n    const numberOfKeys = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent;\n    const targetFolderName = await mainFolder.nth(1).find('[data-testid^=folder-]').textContent;\n    const targetFolderSelector = browserPage.TreeView.getFolderSelectorByName(`device:${targetFolderName}`);\n    await t.click(targetFolderSelector);\n    await browserPage.deleteKey();\n    // Verify the results\n    await t.expect(targetFolderSelector.exists).notOk('The previous folder is not closed after removing key folder');\n    await t.click(browserPage.TreeView.treeViewDeviceFolder);\n    await t.expect(mainFolder.nth(1).textContent).notEql(targetFolderName, 'The key folder is not removed from the tree view');\n    const actualCount = await browserPage.TreeView.getFolderCountSelectorByName('device').textContent;\n    await t.expect(+actualCount).lt(+numberOfKeys, 'The number of keys is not recalculated');\n});\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Verify that if there are keys without namespaces, they are displayed in the root directory after all folders by default in the Tree view', async t => {\n        keyNames = [\n            `atest:a-${Common.generateWord(10)}`,\n            `atest:z-${Common.generateWord(10)}`,\n            `ztest:a-${Common.generateWord(10)}`,\n            `ztest:z-${Common.generateWord(10)}`,\n            `atest-${Common.generateWord(10)}`,\n            `ztest-${Common.generateWord(10)}`\n        ];\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `HSET ${keyNames[2]} field value`,\n            `SADD ${keyNames[3]} value`,\n            `SADD ${keyNames[4]} value`,\n            `HSET ${keyNames[5]} field value`\n        ];\n        const expectedSortedByASC = [\n            keyNames[0].split(':')[1],\n            keyNames[1].split(':')[1],\n            keyNames[2].split(':')[1],\n            keyNames[3].split(':')[1],\n            keyNames[4],\n            keyNames[5]\n        ];\n        const expectedSortedByDESC = [\n            keyNames[3].split(':')[1],\n            keyNames[2].split(':')[1],\n            keyNames[1].split(':')[1],\n            keyNames[0].split(':')[1],\n            keyNames[5],\n            keyNames[4]\n        ];\n\n        // Create 5 keys\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.treeViewButton);\n\n        await t.setTestSpeed(.9)\n        // Verify that if there are keys without namespaces, they are displayed in the root directory after all folders by default in the Tree view\n        await browserPage.TreeView.openTreeFolders([`${keyNames[0]}`.split(':')[0]]);\n        await browserPage.TreeView.openTreeFolders([`${keyNames[2]}`.split(':')[0]]);\n        let actualItemsArray = await browserPage.TreeView.getAllItemsArray();\n        // Verify that user can see all folders and keys sorted by name ASC by default\n        await t.expect(actualItemsArray).eql(expectedSortedByASC);\n\n        // Verify that user can change the sorting ASC-DESC\n        await browserPage.TreeView.changeOrderingInTreeView('DESC');\n        await browserPage.TreeView.openTreeFolders([`${keyNames[2]}`.split(':')[0]]);\n        await browserPage.TreeView.openTreeFolders([`${keyNames[0]}`.split(':')[0]]);\n        actualItemsArray = await browserPage.TreeView.getAllItemsArray();\n        await t.expect(actualItemsArray).eql(expectedSortedByDESC);\n    });\n\n// https://redislabs.atlassian.net/browse/RI-5131\ntest\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .after(async() => {\n        await browserPage.Cli.sendCommandInCli('flushdb');\n    })('Verify that if filtering results has only 1 folder, the folder will be expanded', async t => {\n        const name = Common.generateWord(10);\n        const additionalCharacter = Common.generateWord(1);\n        const keyName1 = Common.generateWord(3);\n        const keyName2 = Common.generateWord(3);\n        keyNames = [`${name}${additionalCharacter}:${keyName1}`, `${name}${additionalCharacter}:${keyName2}`, name];\n\n        const commands = [\n            'flushdb',\n            `HSET ${keyNames[0]} field value`,\n            `HSET ${keyNames[1]} field value`,\n            `HSET ${keyNames[2]} field value`\n        ];\n        await browserPage.Cli.sendCommandsInCli(commands);\n        await t.click(browserPage.treeViewButton);\n\n        // Verify if there is only folder, a user can see keys inside\n        await browserPage.searchByKeyName(`${name}${additionalCharacter}*`);\n        await verifyKeysDisplayingInTheList([keyName1, keyName2], true);\n\n        // Verify if there are folder and key, a user can't see keys inside the folder\n        await browserPage.searchByKeyName(`${name}*`);\n        await verifyKeysDisplayingInTheList([keyName1, keyName2], false);\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/triggers-and-functions/libraries.e2e.ts",
    "content": ""
  },
  {
    "path": "tests/e2e/tests/web/regression/url-handling/url-handling.e2e.ts",
    "content": "import { commonUrl, ossStandaloneRedisGears } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserActions } from '../../../../common-actions/browser-actions';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserActions = new BrowserActions();\n\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst databaseHelper = new DatabaseHelper();\n\nlet { host, port, databaseName, databaseUsername = '', databasePassword = '' } = ossStandaloneRedisGears;\n\nfunction generateLink(params: Record<string, any>): string {\n    const params1 = Common.generateUrlTParams(params);\n    const from = encodeURIComponent(`${redisConnect}?${params1}`);\n    return (new URL(`?from=${from}`, commonUrl)).toString();\n}\n\nconst redisConnect = 'redisinsight://databases/connect';\n\nfixture `Add DB from SM`\n    .meta({ type: 'critical_path', rte: rte.none })\n    .afterEach(async() => {\n        // Delete all existing connections\n        await databaseAPIRequests.deleteAllDatabasesApi();\n    })\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\ntest\n    .page(commonUrl)('Tls dropdown', async t => {\n        const connectUrlParams = {\n            redisUrl: `redis://${databaseUsername}:${databasePassword}@${host}:${port}`,\n            databaseAlias: databaseName,\n            redirect: 'workbench',\n            requiredCaCert: 'true',\n            requiredClientCert: 'true'\n        };\n\n        const tooltipText = [\n            'CA Certificate Name',\n            'CA certificate',\n            'Client Certificate Name',\n            'Client Certificate',\n            'Private Key'\n        ];\n\n        await t.navigateTo(generateLink(connectUrlParams));\n        await t.click(myRedisDatabasePage.AddRedisDatabaseDialog.securityTab);\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.caCertField.textContent).contains('Add new CA certificate', 'add CA certificate is not shown');\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.clientCertField.textContent).contains('Add new certificate', 'add client certificate is not shown');\n        await t.hover(myRedisDatabasePage.AddRedisDatabaseDialog.addRedisDatabaseButton);\n\n        for (const text of tooltipText) {\n            await browserActions.verifyTooltipContainsText(text, true);\n        }\n\n        // Verify that user can see the Test Connection button enabled/disabled with the same rules as the button to add/apply the changes\n        await t.hover(myRedisDatabasePage.AddRedisDatabaseDialog.testConnectionBtn);\n        for (const text of tooltipText) {\n            await browserActions.verifyTooltipContainsText(text, true);\n        }\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nfixture`Autocomplete for entered commands`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest('Verify that user can open the \"read more\" about the command by clicking on the \">\" icon or \"ctrl+space\"', async t => {\n    const command = 'HSE';\n    const commandDetails = [\n        'HSET key field value [field value ...]',\n        'Creates or modifies the value of a field in a hash.',\n        'Read more',\n        'Arguments:',\n        'required key',\n        'multiple field value'\n    ];\n\n    // Type command\n    await t.typeText(workbenchPage.queryInput, command, { replace: true });\n    // Open the read more by clicking on the \"ctrl+space\" and check\n    await t.pressKey('ctrl+space');\n    await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).ok('The \"read more\" about the command is not opened');\n    for (const detail of commandDetails) {\n        await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.textContent).contains(detail, `The ${detail} command detail is not displayed`);\n    }\n    // Close the command details\n    await t.pressKey('ctrl+space');\n    await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).notOk('The \"read more\" about the command is not closed');\n});\ntest('Verify that user can see static list of arguments is displayed when he enters the command in Editor in Workbench', async t => {\n    const command2 = 'TS.DELETERULE ';\n\n    await t.typeText(workbenchPage.queryInput, command2, { replace: true });\n    // Check that hint with arguments are displayed\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.visible).ok('Hints with arguments are not displayed');\n    // Remove hints with arguments\n    await t.pressKey('esc');\n    // Verify that user can close the static list of arguments by pressing “ESC”\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.visible).notOk('Hints with arguments are still displayed');\n});\ntest('Verify that user can see the static list of arguments when he uses “Ctrl+Shift+Space” combination for already entered command for Windows', async t => {\n    const command = 'JSON.ARRAPPEN';\n    await t.typeText(workbenchPage.queryInput, command, { replace: true });\n    // Verify that the list with auto-suggestions is displayed\n    await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).ok('Auto-suggestions are not displayed');\n    // Select the command from suggestion list\n    await t.pressKey('enter');\n    // Check that the command is displayed in Editing area after selecting\n    const script = await workbenchPage.queryInputScriptArea.textContent;\n    await t.expect(script.replace(/\\s/g, ' ')).eql('JSON.ARRAPPEND ', 'Result of sent command not exists');\n    // Check that hint with arguments are displayed\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains('JSON.ARRAPPEND key [path] value', `The required argument is not suggested`);\n    // Remove hints with arguments\n    await t.pressKey('esc');\n    // Check no hints are displayed\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.visible).notOk('Hints with arguments are still displayed');\n    // Check that using shortcut “Ctrl+Shift+Space” hints are displayed\n    await t.pressKey('ctrl+shift+space');\n    await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.visible).ok('Hints with arguments are not displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, BrowserPage } from '../../../../pageObjects';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nfixture `Workbench Auto-Execute button`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\n// Test is skipped until Enablement area will be updated with auto-execute buttons\ntest.skip('Verify that when user clicks on auto-execute button, command is run', async t => {\n    const command = 'INFO';\n    // Verify that clicking on auto-executed button, command is not inserted to Editor\n    await t.typeText(workbenchPage.queryInput, command, { replace: true, paste: true });\n    // Verify that admin can use redis-auto format in .md file for Guides for auto-executed button\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.dataStructureAccordionTutorialButton);\n    await t.click(tutorials.internalLinkWorkingWithHashes);\n    await tutorials.runBlockCode('Create');\n    await t.expect(workbenchPage.queryInput.textContent).eql(command, 'Editor is changed');\n    // Verify that admin can use redis-auto format in .md file for Tutorials for auto-executed button\n    await t.click(tutorials.redisStackTutorialsButton);\n    await t.click(tutorials.timeSeriesLink);\n    // Verify that when user runs auto-executed commands, selected group mode setting is considered\n    await t.click(workbenchPage.groupMode);\n    // Verify that when user runs auto-executed commands, selected raw mode setting is considered\n    await t.click(workbenchPage.rawModeBtn);\n    await tutorials.runBlockCode('Load more data points');\n    // Verify that user can see auto-executed command result in command history\n    const regExp = new RegExp('66[1-9] Command(s) - \\d+ success, \\d+ error(s)');\n    await t.expect(workbenchPage.queryCardCommand.textContent).match(regExp, 'Not valid summary for group mode');\n    await t.expect(workbenchPage.commandExecutionDateAndTime.find('span').withExactText('-r')).ok('Not valid summary for raw mode');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/command-results.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfigEmpty } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { WorkbenchActions } from '../../../../common-actions/workbench-actions';\n\nconst workbenchPage = new WorkbenchPage();\nconst workBenchActions = new WorkbenchActions();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst indexName = Common.generateWord(5);\nconst commandsForIndex = [\n    `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE`,\n    'HMSET product:1 price 20',\n    'HMSET product:2 price 100'\n];\n\nfixture `Command results at Workbench`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        // Add index and data\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex);\n    })\n    .afterEach(async t => {\n        // Drop index and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    });\ntest\n    .skip('Verify that user can switches between Table and Text for FT.INFO and see results corresponding to their views', async t => {\n    const infoCommand = `FT.INFO ${indexName}`;\n\n    // Send FT.INFO and switch to Text view\n    await workbenchPage.sendCommandInWorkbench(infoCommand);\n    await workbenchPage.selectViewTypeText();\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).ok('The text view is not switched for command FT.INFO');\n    // Switch to Table view and check result\n    await workbenchPage.selectViewTypeTable();\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The table view is not switched for command FT.INFO');\n});\ntest\n    .skip('Verify that user can switches between Table and Text for FT.SEARCH and see results corresponding to their views', async t => {\n    const searchCommand = `FT.SEARCH ${indexName} *`;\n\n    // Send FT.SEARCH and switch to Text view\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    await workbenchPage.selectViewTypeText();\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.SEARCH');\n    // Switch to Table view and check result\n    await workbenchPage.selectViewTypeTable();\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The table view is not switched for command FT.SEARCH');\n});\ntest\n    .skip('Verify that user can switches between Table and Text for FT.AGGREGATE and see results corresponding to their views', async t => {\n    const aggregateCommand = `FT.Aggregate ${indexName} * GROUPBY 0 REDUCE MAX 1 @price AS max_price`;\n\n    // Send FT.AGGREGATE and switch to Text view\n    await workbenchPage.sendCommandInWorkbench(aggregateCommand);\n    await workbenchPage.selectViewTypeText();\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.AGGREGATE');\n    // Switch to Table view and check result\n    await workbenchPage.selectViewTypeTable();\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The table view is not switched for command FT.AGGREGATE');\n});\ntest\n    .skip('Verify that user can switches between views and see results according to this view in full mode in Workbench', async t => {\n    const command = 'CLIENT LIST';\n\n    // Send command and check table view is default in full mode\n    await workbenchPage.sendCommandInWorkbench(command);\n    await t.click(workbenchPage.fullScreenButton);\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryTableResult.exists).ok('The search results are displayed in Table view by default');\n    // Select Text view from dropdown\n    await t.switchToMainWindow();\n    await workbenchPage.selectViewTypeText();\n    // Verify that search results are displayed in Text view\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).ok('The result is displayed in Text view');\n});\ntest\n    .skip('Big output in workbench is visible in virtualized table', async t => {\n    // Send commands\n    const command = 'graph.query t \"UNWIND range(1,1000) AS x return x\"';\n    const bottomText = 'Query internal execution time';\n    let numberOfScrolls = 0;\n\n    // Send command in workbench with Text view type\n    await workbenchPage.sendCommandInWorkbench(command);\n    await workbenchPage.selectViewTypeText();\n\n    const containerOfCommand = await workbenchPage.getCardContainerByCommand(command);\n    const listItems = containerOfCommand.find(workbenchPage.cssRowInVirtualizedTable);\n    const lastExpectedItem = listItems.withText(bottomText);\n\n    // Scroll down the virtualized list until the last row\n    while (!await lastExpectedItem.exists && numberOfScrolls < 100) {\n        const currentLastRenderedItemIndex = await listItems.count - 1;\n        const currentLastRenderedItemText = await listItems.nth(currentLastRenderedItemIndex).textContent;\n        const currentLastRenderedItem = listItems.withText(currentLastRenderedItemText);\n\n        await t.scrollIntoView(currentLastRenderedItem);\n        numberOfScrolls++;\n    }\n\n    // Verify that all commands scrolled\n    await t.expect(lastExpectedItem.visible).ok('Final execution time message not displayed');\n});\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .after(async t => {\n        await t.switchToMainWindow();\n    })\n    .skip('Verify that user can see the client List visualization available for all users', async t => {\n        const command = 'CLIENT LIST';\n        // Send command in workbench to view client list\n        await workbenchPage.sendCommandInWorkbench(command);\n        await t.expect(workbenchPage.typeSelectedClientsList.exists).ok('client list view button is not visible');\n        await workBenchActions.verifyClientListColumnsAreVisible(['id', 'addr', 'name', 'user']);\n        // verify table view row count match with text view after client list command\n        await workBenchActions.verifyClientListTableViewRowCount();\n    });\ntest\n    .skip('Verify that user can clear all results at once.', async t => {\n    await t.click(workbenchPage.clearResultsBtn);\n    await t.expect(workbenchPage.queryTextResult.exists).notOk('Clear all button does not remove commands');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/context.e2e.ts",
    "content": "import { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst speed = 0.4;\n\nfixture `Workbench Context`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest('Verify that user can see saved CLI state when navigates away to any other page', async t => {\n    // Expand CLI and navigate to Browser\n    await t.click(workbenchPage.Cli.cliExpandButton);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect(workbenchPage.Cli.cliCollapseButton.exists).ok('CLI is not expanded');\n});\n// Update after resolving https://redislabs.atlassian.net/browse/RI-3299\ntest\n    .skip('Verify that user can see saved CLI size when navigates away to any other page', async t => {\n    const offsetY = 200;\n\n    await t.click(workbenchPage.Cli.cliExpandButton);\n    const cliAreaHeight = await workbenchPage.Cli.cliArea.clientHeight;\n    const cliAreaHeightEnd = cliAreaHeight + 150;\n    const cliResizeButton = workbenchPage.Cli.cliResizeButton;\n    await t.hover(cliResizeButton);\n    // Resize CLI 50px up and navigate to the Redis Databases page\n    await t.drag(cliResizeButton, 0, -offsetY, { speed: 0.01 });\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Navigate back to the database Workbench and check CLI size\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    await t.expect(await workbenchPage.Cli.cliArea.clientHeight > cliAreaHeightEnd).ok('Saved context for resizable cli is incorrect');\n});\ntest('Verify that user can see all the information removed when reloads the page', async t => {\n    const command = 'FT._LIST';\n    // Create context modificaions and navigate to Browser\n    await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed });\n    await t.click(workbenchPage.Cli.cliExpandButton);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await t.expect(workbenchPage.Cli.cliCollapseButton.exists).ok('CLI is not expanded');\n    await t.expect(workbenchPage.queryInputScriptArea.textContent).eql(command, 'Input in Editor is not saved');\n    // Reload the window and chek context\n    await workbenchPage.reloadPage();\n    await t.expect(workbenchPage.Cli.cliCollapseButton.exists).notOk('CLI is not collapsed');\n    await t.expect(workbenchPage.queryInputScriptArea.textContent).eql('', 'Input in Editor is not removed');\n});\ntest('Verify that user can see saved state of the Enablement area when navigates back to the Workbench from other page', async t => {\n    // Collapse the Enablement area and open Settings\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await workbenchPage.NavigationHeader.togglePanel(false);\n    await t.expect(tutorials.preselectArea.exists).notOk('the panel is not closed');\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    await t.click(workbenchPage.NavigationTabs.browserButton);\n    await t.expect(tutorials.preselectArea.exists).ok('the panel is opened');\n    await t.click(browserPage.InsightsPanel.closeButton);\n    await t.expect(tutorials.preselectArea.exists).notOk('the panel is not closed');\n\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/cypher.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst command = 'GRAPH.QUERY graph';\n\nfixture `Cypher syntax at Workbench`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest('Verify that user can see popover “Use Cypher Syntax” when cursor is inside the query argument double/single quotes in the GRAPH command', async t => {\n    // Type command and put the cursor inside\n    await t.typeText(workbenchPage.queryInput, `${command} \"query\"`, { replace: true });\n    await t.pressKey('left');\n    // Check that user can see popover\n    await t.expect(await workbenchPage.MonacoEditor.monacoWidget.textContent).contains('Use Cypher Editor', 'The user can not see popover Use Cypher Syntax');\n    await t.expect(await workbenchPage.MonacoEditor.monacoWidget.textContent).contains('Shift+Space', 'The user can not see shortcut for Cypher Syntax');\n    // Verify the popover with single quotes\n    await t.typeText(workbenchPage.queryInput, `${command} ''`, { replace: true });\n    await t.pressKey('left');\n    await t.expect(await workbenchPage.MonacoEditor.monacoWidget.textContent).contains('Use Cypher Editor', 'The user can not see popover Use Cypher Syntax');\n});\ntest\n    .skip('Verify that when user clicks on the “X” control or use shortcut “ESC” popover Editor is closed and changes are not saved', async t => {\n    const cypherCommand = `${command} \"query\"`;\n    // Type command and open the popover editor\n    await t.typeText(workbenchPage.queryInput, cypherCommand, { replace: true });\n    await t.pressKey('left');\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    // Do some changes in the Editor and close by “X” control\n    await t.typeText(workbenchPage.queryInput.nth(1), 'test', { replace: true });\n    await t.click(workbenchPage.EditorButton.cancelBtn);\n    // Verify that editor is closed and changes are not saved\n    let commandAfter = await workbenchPage.scriptsLines.textContent;\n    await t.expect(workbenchPage.queryInput.nth(1).exists).notOk('The popover Editor is not closed');\n    await t.expect(commandAfter.replace(/\\s/g, ' ')).eql(cypherCommand, 'The changes are still saved from the Editor');\n    // Re-open the Editor and do some changes and close by shortcut “ESC”\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    await t.typeText(workbenchPage.queryInput.nth(1), 'test', { replace: true });\n    await t.pressKey('esc');\n    // Verify that editor is closed and changes are not saved\n    commandAfter = await workbenchPage.scriptsLines.textContent;\n    await t.expect(commandAfter.replace(/\\s/g, ' ')).eql(cypherCommand, 'The changes are still saved from the Editor');\n});\ntest\n    .skip('Verify that when user use shortcut “CTRL+ENTER” or clicks on the “V” control popover Editor is closed and changes are saved', async t => {\n    let script = 'query';\n    // Type command and open the popover editor\n    await t.typeText(workbenchPage.queryInput, `${command} \"${script}`, { replace: true });\n    await t.pressKey('left');\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    // Do some changes in the Editor and click on the “V” control\n    script = 'test';\n    await t.pressKey('ctrl+a');\n    await t.typeText(workbenchPage.queryInput.nth(1), script, { replace: true });\n    await t.click(workbenchPage.EditorButton.applyBtn);\n    // Verify that editor is closed and changes are saved\n    let commandAfter = await workbenchPage.scriptsLines.textContent;\n    await t.expect(workbenchPage.queryInput.nth(1).exists).notOk('The popover Editor is not closed');\n    await t.expect(commandAfter.replace(/\\s/g, ' ')).eql(`${command} \"${script}\"`, 'The changes are not saved from the Editor');\n    // Re-open the Editor and do some changes and use keyboard shortcut “CTRL+ENTER”\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    script = 'test2';\n    await t.pressKey('ctrl+a');\n    await t.typeText(workbenchPage.queryInput.nth(1), 'test2', { paste: true, replace: true });\n    await t.pressKey('ctrl+enter');\n    // Verify that editor is closed and changes are not saved\n    commandAfter = await workbenchPage.scriptsLines.textContent;\n    await t.expect(commandAfter.replace(/\\s/g, ' ')).eql(`${command} \"${script}\"`, 'The changes are still saved from the Editor');\n});\ntest\n    .skip('Verify that user can see the opacity of main Editor is 80%, Run button is disabled when the non-Redis editor is opened', async t => {\n    // Type command and open Cypher editor\n    await t.typeText(workbenchPage.queryInput, `${command} \"query\"`, { replace: true });\n    await t.pressKey('left');\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    // Check the main Editor and Run button\n    await t.expect(workbenchPage.mainEditorArea.getStyleProperty('opacity')).eql('0.8', 'The opacity of main Editor is incorrect');\n    await t.click(workbenchPage.submitCommandButton);\n    await t.expect(workbenchPage.noCommandHistorySection.visible).ok('The Run button in main Editor is not disabled');\n    await t.hover(workbenchPage.submitCommandButton);\n    await t.expect(workbenchPage.runButtonToolTip.visible).notOk('The Run button in main Editor still react on hover');\n});\ntest\n    .skip('Verify that user can resize non-Redis editor only by the top and bottom borders', async t => {\n    const offsetY = 50;\n    await t.drag(workbenchPage.resizeButtonForScriptingAndResults, 0, offsetY * 10, { speed: 0.4 });\n    // Type command and open Cypher editor\n    await t.typeText(workbenchPage.queryInput, `${command} \"query\"`, { replace: true });\n    await t.pressKey('left');\n    await t.click(workbenchPage.MonacoEditor.monacoWidget);\n    // Check that user can resize editor by top border\n    let editorHeight = await workbenchPage.queryInput.nth(1).clientHeight;\n    await t.drag(workbenchPage.MonacoEditor.nonRedisEditorResizeTop, 0, -offsetY, { speed: 0.4 });\n    await t.expect(workbenchPage.queryInput.nth(1).clientHeight).eql(editorHeight + offsetY, 'The non-Redis editor is not resized by the top border');\n    // Check that user can resize editor by bottom border\n    editorHeight = await workbenchPage.queryInput.nth(1).clientHeight;\n    await t.drag(workbenchPage.MonacoEditor.nonRedisEditorResizeBottom, 0, -offsetY, { speed: 0.4 });\n    await t.expect(workbenchPage.queryInput.nth(1).clientHeight).eql(editorHeight - offsetY, 'The non-Redis editor is not resized by the bottom border');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts",
    "content": "import { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nfixture `Default scripts area at Workbench`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest('Verify that user can see the [Manual] option in the Enablement area', async t => {\n    const optionsForCheck = [\n        'Manual',\n        'List the Indices',\n        'Index info',\n        'Search',\n        'Aggregate'\n    ];\n\n    // Remember the options displayed in the area\n    const countOfOptions = await workbenchPage.preselectButtons.count;\n    const displayedOptions: string[] = [];\n    for(let i = 0; i < countOfOptions; i++) {\n        displayedOptions.push(await workbenchPage.preselectButtons.nth(i).textContent);\n    }\n    // Verify the options in the area\n    for(let i = 0; i < countOfOptions; i++) {\n        await t.expect(displayedOptions[i]).eql(optionsForCheck[i], `Option ${optionsForCheck} is not in the Enablement area`);\n    }\n});\ntest('Verify that user can see saved article in Enablement area when he leaves Workbench page and goes back again', async t => {\n    const tooltipText = 'Open Workbench in the left menu to see the command results.';\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.dataStructureAccordionTutorialButton);\n    await t.expect(tutorials.internalLinkWorkingWithHashes.visible).ok('The working with hashes link is not visible', { timeout: 5000 });\n    // Open Working with Hashes section\n    await t.click(tutorials.internalLinkWorkingWithHashes);\n    let selector = tutorials.getRunSelector('Create a hash');\n\n    // https://redislabs.atlassian.net/browse/RI-5340\n    // Verify that user can see “Open Workbench in the left menu to see the command results.” tooltip when hovering over Run button\n    await t.hover(selector);\n    await t.expect(browserPage.tooltip.withText(tooltipText).exists).ok('Tooltip is not displayed or text is invalid');\n\n    // Check the button from Hash page is visible\n    await tutorials.runBlockCode('Create a hash');\n    selector = tutorials.getRunSelector('Create a hash');\n    await t.expect(selector.visible).ok('The end of the page is not visible');\n\n    // Verify that user can see the “success” icon during 5 s after a command has been run and button can't be clicked at that time\n    await t.expect(selector.withAttribute('disabled').exists).ok('Run button is not disabled', { timeout: 5000 });\n    await t.wait(5000);\n    await t.expect(selector.withAttribute('disabled').exists).notOk('Run button is still disabled');\n\n    // Go to Browser page\n    await t.click(browserPage.NavigationTabs.browserButton);\n    // Go back to Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Verify that the same article is opened in Enablement area\n    selector = tutorials.getRunSelector('Create a hash');\n    await t.expect(selector.visible).ok('The end of the page is not visible');\n    // Go to list of DBs page\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    // Go back to active DB again\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    // Check that user is on Workbench page and \"Working with Hashes\" page is displayed\n    selector = tutorials.getRunSelector('Create a hash');\n    await t.expect(selector.visible).ok('The end of the page is not visible');\n});\n// todo: investigate. seems flaky\ntest.skip('Verify that user can see saved scroll position in Enablement area when he leaves Workbench page and goes back again', async t => {\n    // Open Working with Hashes section\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.dataStructureAccordionTutorialButton);\n    await t.click(tutorials.internalLinkWorkingWithHashes);\n    // Evaluate the last button in Enablement Area\n    const buttonsQuantity = await workbenchPage.copyBtn.count;\n    const lastButton = workbenchPage.copyBtn.nth(buttonsQuantity - 1);\n    // Scroll to the very bottom of the page\n    await t.scrollIntoView(lastButton);\n    await workbenchPage.NavigationHeader.togglePanel(false);\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    // Check the scroll position\n    const scrollPosition = await workbenchPage.scrolledEnablementArea.scrollTop;\n    // Check that scroll position is saved\n    await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'The scroll position status is incorrect');\n    // Go to list of DBs page\n    await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n    await t.wait(1000)\n    // Go back to active DB again\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    // Check that scroll position is saved\n    await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'Scroll position is not correct');\n});\ntest('Verify that user can see the siblings menu by clicking on page counter element between Back and Next buttons', async t => {\n    const popoverButtons = [\n        'Strings',\n        'Hashes',\n        'Lists',\n        'Sets',\n        'Sorted sets'\n    ];\n\n    // Open Working with Hashes section and click on the on page counter\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.dataStructureAccordionTutorialButton);\n    await t.expect(tutorials.internalLinkWorkingWithHashes.visible).ok('The working with hachs link is not visible', { timeout: 5000 });\n    await t.click(tutorials.internalLinkWorkingWithHashes);\n    // Verify that user can see the quick navigation section to navigate between siblings under the scrolling content\n    await t.expect(tutorials.enablementAreaPagination.visible).ok('The quick navigation section is not displayed');\n\n    await t.click(tutorials.enablementAreaPagination);\n    // Verify the siblings menu\n    await t.expect(tutorials.enablementAreaPaginationPopover.visible).ok('The siblings menu is not displayed');\n    const countOfButtons = await tutorials.paginationPopoverButtons.count;\n    for (let i = 0; i < countOfButtons; i++) {\n        const popoverButton = tutorials.paginationPopoverButtons.nth(i);\n        await t.expect(popoverButton.textContent).eql(popoverButtons[i], `The siblings menu button ${popoverButtons[i]} is not displayed`);\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, MyRedisDatabasePage, SettingsPage, BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Telemetry } from '../../../../helpers';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\nconst telemetry = new Telemetry();\n\nconst logger = telemetry.createLogger();\n\nconst commandToSend = 'info server';\nconst databasesForAdding = [\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB1' },\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' }\n];\n\nconst telemetryEvent = 'SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED';\nconst expectedProperties = [\n    'currentValue',\n    'newValue'\n];\n\nfixture `Workbench Editor Cleanup`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .skip('Disabled Editor Cleanup toggle behavior', async t => {\n    // Go to Settings page\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    await t.click(settingsPage.accordionWorkbenchSettings);\n    // Disable Editor Cleanup\n    await t.click(settingsPage.switchEditorCleanupOption);\n    // Verify that user can see text \"Clear the Editor after running commands\" for Editor Cleanup In Settings\n    await t.expect(settingsPage.switchEditorCleanupOption.sibling(0).withExactText('Clear the Editor after running commands').visible).ok('Cleanup text is not correct');\n    // Go to Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandToSend);\n    await workbenchPage.sendCommandInWorkbench(commandToSend);\n    // Verify that Editor input is not affected after user running command\n    await t.expect((await workbenchPage.queryInputScriptArea.textContent).replace(/\\s/g, ' ')).eql(commandToSend, 'Input in Editor is saved');\n});\ntest('Enabled Editor Cleanup toggle behavior', async t => {\n    // Go to Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandToSend);\n    await workbenchPage.sendCommandInWorkbench(commandToSend);\n    // Verify that Editor input is cleared after running command\n    await t.pressKey('esc');\n    await t.expect(await workbenchPage.queryInputScriptArea.textContent).eql('', 'Input in Editor is saved');\n});\ntest\n    .before(async() => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding);\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName);\n    })\n    .requestHooks(logger)\n    .after(async() => {\n        // Clear and delete database\n        await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding);\n    })('Editor Cleanup settings', async t => {\n        // Go to Settings page\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionWorkbenchSettings);\n        // Disable Editor Cleanup\n        await settingsPage.changeEditorCleanupSwitcher(false);\n        //Verify telemetry event\n        await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n        await myRedisDatabasePage.reloadPage();\n        await t.click(settingsPage.accordionWorkbenchSettings);\n        // Verify that Editor Cleanup setting is saved when refreshing the page\n        await t.expect(await settingsPage.getEditorCleanupSwitcherValue()).notOk('Editor Cleanup switcher changed');\n        // Go to another database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName);\n        // Go to Settings page\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionWorkbenchSettings);\n        // Verify that Editor Cleanup setting is saved when switching between databases\n        await t.expect(await settingsPage.getEditorCleanupSwitcherValue()).notOk('Editor Cleanup switcher changed');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts",
    "content": "import { Selector } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Empty command history in Workbench`\n    .meta({ type: 'regression' })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .skip('Verify that user can see placeholder text in Workbench history if no commands have not been run yet', async t => {\n        const commandToSend = 'info server';\n\n        // Verify that all the elements from empty command history placeholder are displayed\n        await t.expect(workbenchPage.noCommandHistorySection.visible).ok('No command history section is not visible');\n        await t.expect(workbenchPage.noCommandHistoryIcon.visible).ok('No command history icon is not visible');\n        await t.expect(workbenchPage.noCommandHistoryTitle.visible).ok('No command history title is not visible');\n        await t.expect(workbenchPage.noCommandHistoryText.visible).ok('No command history text is not visible');\n        // Run a command\n        await workbenchPage.sendCommandInWorkbench(commandToSend);\n        // Verify that empty command history placeholder is not displayed\n        await t.expect(workbenchPage.noCommandHistorySection.visible).notOk('No command history section is still visible');\n        // Delete the command result\n        await t.click(Selector(workbenchPage.cssDeleteCommandButton));\n        // Verify that empty command history placeholder is displayed\n        await t.expect(workbenchPage.noCommandHistorySection.visible).ok('No command history section is not visible');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts",
    "content": "import { Selector } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst counter = 7;\nconst command = 'RANDOMKEY';\nconst commands = ['set key test', 'get key', 'del key'];\nconst commandsResult = ['OK', 'test', '1'];\nconst commandsNumber = commands.length;\nconst commandsString = commands.join('\\n');\n\nfixture `Workbench Group Mode`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest\n    .skip('Verify that user can run the commands from the Editor in the group mode', async t => {\n    await t.click(workbenchPage.groupMode);\n    // Verify that user can run a command with quantifier and see results in group(10 info)\n    await workbenchPage.sendCommandInWorkbench(`${counter} ${command}`);\n    // Verify that user can see number of total commands in group, success commands, number of failed commands in header summary in Workbench\n    await t.expect(workbenchPage.queryCardCommand.textContent).contains(`${counter} Command(s) - ${counter} success, 0 error(s)`, 'Not valid summary');\n    // Verify that if users execute commands in group mode, they see summary of the commands execution\n    await t.expect(workbenchPage.executionCommandTime.exists).ok('Execution time is not displayed');\n    await t.expect(workbenchPage.executionCommandIcon.exists).ok('Execution time icon is not displayed');\n    // Verify that user can see full list of commands with results run in group\n    await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandInHistory).withText(`> ${command}`).count).eql(counter, 'Number of commands is not correct');\n    // Verify that if the only one command is executed in group, the result will be displayed as for group mode\n    await workbenchPage.sendCommandInWorkbench(`${command}`);\n    await t.expect(workbenchPage.queryCardCommand.textContent).eql('1 Command(s) - 1 success, 0 error(s)', 'Not valid summary for 1 command');\n    // Turn off group mode\n    await t.click(workbenchPage.groupMode);\n    await workbenchPage.sendCommandInWorkbench(commandsString);\n    await t.expect(workbenchPage.queryCardCommand.textContent).notEql(`${commandsNumber} Command(s) - ${commandsNumber} success, 0 error(s)`, 'Commands are sent in groups');\n    for (let i = 0; i++; i < commandsNumber) {\n        await workbenchPage.checkWorkbenchCommandResult(command[i], commandsResult[i], i);\n    }\n});\n// Skip due to testcafe doesn't work with clipboard buffer. Need to add client function to check this test\ntest.skip('Verify that when user clicks on copy icon for group result, all commands are copied', async t => {\n    await t.click(workbenchPage.groupMode);\n    await workbenchPage.sendCommandInWorkbench(`${commandsString}`); // 3 commands are sent in group mode\n    // Copy commands from group result\n    await t.click(workbenchPage.copyCommand);\n    await t.rightClick(workbenchPage.queryInputScriptArea);\n    await t.click(Selector('span').withAttribute('aria-label', 'Paste'));\n    await t.pressKey('ctrl+enter');\n    await t.expect(workbenchPage.queryCardCommand.textContent).eql(`${commandsNumber} Command(s) - ${commandsNumber} success, 0 error(s)`, 'Not valid summary');\n});\ntest\n    .skip('Verify that user can see group results in full mode', async t => {\n    await t.click(workbenchPage.groupMode);\n    await workbenchPage.sendCommandInWorkbench(`${commandsString}`); // 3 commands are sent in group mode\n    // Open full mode\n    await t.click(workbenchPage.fullScreenButton);\n    await t.expect(workbenchPage.queryCardCommand.textContent).eql(`${commandsNumber} Command(s) - ${commandsNumber} success, 0 error(s)`, 'Not valid summary');\n    await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandInHistory).withText('> ').count).eql(commandsNumber, 'Number of commands is not correct');\n    await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandInHistory).count).eql(commandsNumber, 'Number of command result is not correct');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts",
    "content": "import { getRandomParagraph } from '../../../../helpers/keys';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst oneMinuteTimeout = 60000;\nlet keyName = Common.generateWord(10);\nconst command = `set ${keyName} test`;\n\nfixture `History of results at Workbench`\n    .meta({ type: 'regression' })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await workbenchPage.Cli.sendCommandInCli(`DEL ${keyName}`);\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .skip('Verify that user can see original date and time of command execution in Workbench history after the page update', async t => {\n        keyName = Common.generateWord(5);\n        // Send command and remember the time\n        await workbenchPage.sendCommandInWorkbench(command);\n        const dateTime = await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent;\n        // Wait fo 1 minute, refresh page and check results\n        await t.wait(oneMinuteTimeout);\n        await workbenchPage.reloadPage();\n        await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent).eql(dateTime, 'The original date and time of command execution is not saved after the page update');\n    });\n//skipped due the long time execution and hangs of test\ntest.skip\n    .meta({ rte: rte.standalone })\n    .after(async() => {\n        // overwrite aftereach\n    })('Verify that if command result is more than 1 MB and user refreshes the page, the message \"Results have been deleted since they exceed 1 MB. Re-run the command to see new results.\" is displayed', async t => {\n        const commandToSend = 'set key';\n        const commandToGet = 'get key';\n        const commandText = getRandomParagraph(10).repeat(100);\n\n        // Send command with value that exceed 1MB\n        await workbenchPage.sendCommandInWorkbench(`${commandToSend} \"${commandText}\"`);\n        await workbenchPage.sendCommandInWorkbench(commandToGet);\n        // Refresh the page and check result\n        await workbenchPage.reloadPage();\n        await t.click(workbenchPage.queryCardContainer.withText(commandToGet));\n        await t.expect(workbenchPage.queryTextResult.textContent).eql('\"Results have been deleted since they exceed 1 MB. Re-run the command to see new results.\"', 'The message is not displayed');\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .skip('Verify that the first command in workbench history is deleted when user executes 31 command (new the following result replaces the first result)', async t => {\n        keyName = Common.generateWord(10);\n        const numberOfCommands = 30;\n        const firstCommand = 'FT._LIST';\n\n        //Send command the first command\n        await workbenchPage.sendCommandInWorkbench(firstCommand);\n        await t.expect(workbenchPage.queryCardContainer.nth(0).textContent).contains(firstCommand, 'The first executed command is not in the workbench history');\n        // Send 30 commands and check the results\n        await workbenchPage.sendCommandInWorkbench(`${numberOfCommands} ${command}`);\n        await t.expect(workbenchPage.queryCardCommand.find('span').withExactText(`${firstCommand}`).exists).notOk('The first command is still in the history result');\n        await t.expect(workbenchPage.queryCardCommand.count).eql(30, { timeout: 5000 });\n    });\ntest\n    .meta({ rte: rte.none })\n    .skip('Verify that user can see cursor is at the first character when Editor is empty', async t => {\n        const commands = [\n            'FT.INFO',\n            'RANDOMKEY'\n        ];\n        const commandForCheck = 'SET';\n\n        // Send commands\n        for(const command of commands) {\n            await workbenchPage.sendCommandInWorkbench(command);\n        }\n        // Verify the quick access to history works when cursor is at the first character\n        await t.typeText(workbenchPage.queryInput, commandForCheck);\n        await t.pressKey('enter');\n        await t.pressKey('up');\n        const script = await workbenchPage.scriptsLines.textContent;\n        await t.expect(script.replace(/\\s/g, ' ')).contains(commandForCheck, 'The command is not changed');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig, ossStandaloneConfigEmpty } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst keyName = Common.generateWord(10);\nconst indexName = Common.generateWord(5);\nconst keyValue = '\\\\xe5\\\\xb1\\\\xb1\\\\xe5\\\\xa5\\\\xb3\\\\xe9\\\\xa6\\\\xac / \\\\xe9\\\\xa9\\\\xac\\\\xe7\\\\x9b\\\\xae abc 123';\nconst unicodeValue = '山女馬 / 马目 abc 123';\nconst commandsForSend = [\n    `set ${keyName} \"${keyValue}\"`,\n    `get ${keyName}`\n];\nconst databasesForAdding = [\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB1' },\n    { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' }\n];\n\nfixture `Workbench Raw mode`\n    .meta({ type: 'critical_path', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async t => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest\n    .skip('Use raw mode for Workbech result', async t => {\n    // Send commands\n    await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend);\n    // Display result in Ascii when raw mode is off\n    await t.expect(workbenchPage.queryTextResult.textContent).contains(`\"${keyValue}\"`, 'The result is not correct');\n    // Verify that user can't see Raw marker in Workbench command history\n    await t.expect(workbenchPage.parametersAnchor.exists).notOk('Raw mode icon displayed');\n    //Send command in raw mode\n    await t.click(workbenchPage.rawModeBtn);\n    await workbenchPage.sendCommandInWorkbench(commandsForSend[1]);\n    // Verify that user can see command result execution in raw mode\n    await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `\"${unicodeValue}\"`);\n    // Verify that user can see R marker in command history\n    await t.hover(workbenchPage.parametersAnchor);\n    await t.expect(workbenchPage.rawModeIcon.exists).ok('Raw mode icon not displayed');\n});\ntest\n    .before(async t => {\n        // Add new databases using API\n        await databaseHelper.acceptLicenseTerms();\n        await databaseAPIRequests.addNewStandaloneDatabasesApi(databasesForAdding);\n        // Reload Page\n        await myRedisDatabasePage.reloadPage();\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .skip('Save Raw mode state', async t => {\n        // Send command in raw mode\n        await t.click(workbenchPage.rawModeBtn);\n        await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend);\n        // Verify that user can see saved Raw mode state after page refresh\n        await workbenchPage.reloadPage();\n        await workbenchPage.sendCommandInWorkbench(commandsForSend[1]);\n        await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `\"${unicodeValue}\"`);\n        // Go to another database\n        await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton);\n        await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        // Verify that user can see saved Raw mode state after re-connection to another DB\n        await workbenchPage.sendCommandInWorkbench(commandsForSend[1]);\n        await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `\"${unicodeValue}\"`);\n        // Verify that currently selected mode is applied when User re-run the command from history\n        await t.click(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssReRunCommandButton));\n        await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `\"${unicodeValue}\"`);\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfigEmpty);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .after(async t => {\n        // Drop index, documents and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfigEmpty);\n    })\n    .skip('Display Raw mode for plugins', async t => {\n        const commandsForSend = [\n            `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`,\n            `HMSET product:1 name \"${unicodeValue}\"`,\n            `FT.SEARCH ${indexName} \"${unicodeValue}\"`\n        ];\n        // Send command in raw mode\n        await t.click(workbenchPage.rawModeBtn);\n        await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend);\n        // Check the FT.SEARCH result\n        await t.switchToIframe(workbenchPage.iframe);\n        const name = workbenchPage.queryTableResult.withText(unicodeValue);\n        await t.expect(name.exists).ok('The added key name field is not converted to Unicode');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst keyNameGraph = 'bikes_graph';\n\nfixture `Redis Stack command in Workbench`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async() => {\n        // Drop key and database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`GRAPH.DELETE ${keyNameGraph}`);\n    });\ntest\n    .skip('Verify that user can switches between Chart and Text for TimeSeries command and see results corresponding to their views', async t => {\n    // Send TimeSeries command\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n    await t.click(tutorials.dataStructureAccordionTutorialButton);\n    await t.click(tutorials.timeSeriesLink);\n    await t.click(tutorials.internalTimeSeriesLink);\n    await tutorials.runBlockCode('Get all samples');\n    await t.click(workbenchPage.submitCommandButton);\n    // Check result is in chart view\n    await t.expect(workbenchPage.chartViewTypeOptionSelected.exists).ok('The chart view option is not selected by default');\n    // Switch to Text view and check result\n    await workbenchPage.selectViewTypeText();\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).ok('The result in text view is not displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts",
    "content": "import { ClientFunction } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneV5Config } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst commandForSend = 'FT._LIST';\nconst getPageUrl = ClientFunction(() => window.location.href);\n\nfixture `Redisearch module not available`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\n// Skipped as outdated after implementing RI-4230\ntest.skip('Verify that user can see the \"Create your free Redis Cloud database with RediSearch on Redis Cloud\" button and click on it in Workbench when module in not loaded', async t => {\n    const link = 'https://redis.io/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_redisearch';\n\n    // Send command with 'FT.'\n    await workbenchPage.sendCommandInWorkbench(commandForSend);\n    // Verify the button in the results\n    await t.expect(await workbenchPage.queryCardNoModuleButton.visible).ok('The \"Create your free Redis Cloud database with RediSearch on Redis Cloud\" button is not visible');\n    // Click on the button in the results\n    await t.click(workbenchPage.queryCardNoModuleButton);\n    await t.expect(getPageUrl()).contains(link, 'The Try Redis Enterprise page is not opened');\n    await t.switchToParentWindow();\n});\n// https://redislabs.atlassian.net/browse/RI-4230\ntest\n    .skip('Verify that user can see options on what can be done to work with capabilities in Workbench for docker', async t => {\n    const commandJSON = 'JSON.ARRAPPEND key value';\n    const commandFT = 'FT.LIST';\n\n    await workbenchPage.NavigationHeader.togglePanel(true);\n    await workbenchPage.sendCommandInWorkbench(commandJSON);\n    // Verify change screens when capability not available - 'JSON'\n    await t.expect(await workbenchPage.commandExecutionResult.withText('JSON data structure is not available').visible)\n        .ok('Missing JSON title is not visible');\n    await workbenchPage.sendCommandInWorkbench(commandFT);\n    // Verify change screens when capability not available - 'Search'\n    await t.expect(await workbenchPage.commandExecutionResult.withText('Redis Query Engine is not available').visible)\n        .ok('Missing Search title is not visible');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts",
    "content": "import { ExploreTabs, rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage, WorkbenchPage, SettingsPage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst browserPage = new BrowserPage();\n\nconst indexName = Common.generateWord(5);\nlet keyName = Common.generateWord(5);\n\nfixture `Scripting area at Workbench`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    });\ntest\n    .skip('Verify that user can run multiple commands written in multiple lines in Workbench page', async t => {\n    const commandsForSend = [\n        'info',\n        `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`,\n        'HMSET product:1 price 20',\n        'FT._LIST'\n    ];\n\n    // Go to Settings page\n    await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n    // Specify Commands in pipeline\n    await t.click(settingsPage.accordionWorkbenchSettings);\n    await settingsPage.changeCommandsInPipeline('1');\n    // Go to Workbench page\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Send commands in multiple lines\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'), 0.5);\n    // Check the result\n    for (let i = 1; i < commandsForSend.length + 1; i++) {\n        const resultCommand = workbenchPage.queryCardCommand.nth(i - 1).textContent;\n        await t.expect(resultCommand).eql(commandsForSend[commandsForSend.length - i], `The command ${commandsForSend[commandsForSend.length - i]} is in not the result`);\n    }\n});\ntest\n    .after(async() => {\n        // Clear and delete database\n        await workbenchPage.Cli.sendCommandInCli(`DEL ${keyName}`);\n    })\n    .skip('Verify that user can use double slashes (//) wrapped in double quotes and these slashes will not comment out any characters', async t => {\n        keyName = Common.generateWord(10);\n        const commandsForSend = [\n            `HMSET ${keyName} price 20`,\n            'FT._LIST'\n        ];\n\n        // Go to Settings page\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        // Specify Commands in pipeline\n        await t.click(settingsPage.accordionWorkbenchSettings);\n        await settingsPage.changeCommandsInPipeline('1');\n        // Go to Workbench page\n        await t.click(settingsPage.NavigationTabs.workbenchButton);\n        // Send commands in multiple lines with double slashes (//) wrapped in double quotes\n        await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n\"//\"'), 0.5);\n        // Check that all commands are executed\n        for (let i = 1; i < commandsForSend.length + 1; i++) {\n            const resultCommand = workbenchPage.queryCardCommand.nth(i - 1).textContent;\n            await t.expect(resultCommand).contains(commandsForSend[commandsForSend.length - i], `The command ${commandsForSend[commandsForSend.length - i]} is not in the result`);\n        }\n    });\ntest\n    .after(async() => {\n        // Clear and delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that user can see an indication (green triangle) of commands from the left side of the line numbers', async t => {\n        // Open Working with Hashes page\n        await workbenchPage.NavigationHeader.togglePanel(true);\n        const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials);\n        await t.click(tutorials.dataStructureAccordionTutorialButton);\n        await t.expect(tutorials.internalLinkWorkingWithHashes.visible).ok('The working with hashes link is not visible', { timeout: 5000 });\n        await t.click(tutorials.internalLinkWorkingWithHashes);\n        // Put Create Hash commands into Editing area\n        const codeText  = await tutorials.getBlockCode('Create a hash');\n        const regex = new RegExp('HSET', 'g');\n        const monacoCommandIndicatorCount = codeText.match(regex)!.length;\n        await tutorials.runBlockCode('Create a hash');\n        //Get number of commands in scripting area\n        const numberOfCommands = await workbenchPage.executedCommandTitle.withText('HSET').count;\n        //Compare number of indicator displayed and expected value\n        await t.expect(monacoCommandIndicatorCount).eql(numberOfCommands, 'Number of command indicator is incorrect');\n    });\ntest\n    .after(async() => {\n        // Clear and delete database\n        await workbenchPage.Cli.sendCommandInCli(`DEL ${keyName}`);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })\n    .skip('Verify that user can find (using right click) \"Run Commands\" custom shortcut option in monaco menu and run a command', async t => {\n        keyName = Common.generateWord(10);\n        const command = `HSET ${keyName} field value`;\n\n        // Put a command in Editing Area\n        await t.typeText(workbenchPage.queryInput, command);\n        // Right click to get context menu\n        await t.rightClick(workbenchPage.queryInput);\n        // Select Command Palette option\n        await t.click(workbenchPage.MonacoEditor.monacoContextMenu.find(workbenchPage.cssMonacoCommandPaletteLine));\n        // Print \"Run Commands\" shortcut\n        await t.typeText(workbenchPage.MonacoEditor.monacoShortcutInput, 'Run Commands');\n        // Select \"Run Commands\" from menu\n        await t.click(workbenchPage.MonacoEditor.monacoSuggestionOption);\n        // Check the result with sent command\n        await t.expect(workbenchPage.queryCardCommand.withExactText(command).exists).ok('The result of sent command is not displayed');\n    });\ntest\n    .skip('Verify that user can repeat commands by entering a number of repeats before the Redis command and see separate results per each command in Workbench', async t => {\n    const command = 'FT._LIST';\n    const command2 = 'select 13';\n    const repeats = 5;\n    const result = '\"select is not supported by the Workbench.';\n\n    // Run command in Workbench with repeats\n    await workbenchPage.sendCommandInWorkbench(`${repeats} ${command}`);\n    // Verify result\n    for (let i = 0; i < repeats; i++) {\n        await t.expect(workbenchPage.queryCardContainer.nth(i).textContent).contains(command, 'Workbench not contains separate results');\n    }\n\n    // Run Select command in Workbench\n    await workbenchPage.sendCommandInWorkbench(command2);\n    // Verify that user can not run \"Select\" command in Workbench\n    await t.expect(workbenchPage.commandExecutionResultFailed.textContent).contains(result, 'The select command unsupported message is incorrect');\n\n    // Type command and use Ctrl + Enter\n    await t.typeText(workbenchPage.queryInput, command, { replace: true, paste: true });\n    await t.pressKey('ctrl+enter');\n    // Verify that user can use Ctrl + Enter to run the query in Workbench\n    await t.expect(workbenchPage.queryCardCommand.withExactText(command).exists).ok('The user can not use Ctrl + Enter to run the query');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts",
    "content": "import { t } from 'testcafe';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects';\nimport { cloudDatabaseConfig, commonUrl, ossClusterConfig, ossSentinelConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst commandForSend1 = 'info';\nconst commandForSend2 = 'FT._LIST';\nconst verifyCommandsInWorkbench = async(): Promise<void> => {\n    const multipleCommands = [\n        'info',\n        'command',\n        'FT.SEARCH idx *'\n    ];\n\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandForSend1);\n    await workbenchPage.sendCommandInWorkbench(commandForSend2);\n    // Check that all the previous run commands are saved and displayed\n    await workbenchPage.reloadPage();\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend1).exists).ok('The previous run commands are saved');\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend2).exists).ok('The previous run commands are saved');\n    // Send multiple commands in one query\n    await workbenchPage.sendCommandInWorkbench(multipleCommands.join('\\n'), 0.75);\n    // Check that the results for all commands are displayed\n    for (const command of multipleCommands) {\n        await t.expect(workbenchPage.queryCardCommand.withExactText(command).exists).ok(`The command ${command} from multiple query is displayed`);\n    }\n};\n\nfixture `Work with Workbench in all types of databases`\n    .meta({ type: 'regression' })\n    .page(commonUrl);\ntest\n    .meta({ rte: rte.reCloud })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddRedisCloudDatabase(cloudDatabaseConfig);\n    })\n    .after(async() => {\n        // Delete database\n        await databaseHelper.deleteDatabase(cloudDatabaseConfig.databaseName);\n    })\n    .skip('Verify that user can run commands in Workbench in RE Cloud DB', async() => {\n        await verifyCommandsInWorkbench();\n    });\ntest\n    .meta({ rte: rte.ossCluster })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig);\n    })\n    .after(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig);\n    })\n    .skip('Verify that user can run commands in Workbench in OSS Cluster DB', async() => {\n        await verifyCommandsInWorkbench();\n    });\ntest\n    .meta({ rte: rte.sentinel })\n    .before(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddSentinelDatabaseApi(ossSentinelConfig);\n    })\n    .after(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteAllDatabasesByConnectionTypeApi('SENTINEL');\n    })\n    .skip('Verify that user can run commands in Workbench in Sentinel Primary Group', async() => {\n        await verifyCommandsInWorkbench();\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { rte } from '../../../../helpers/constants';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst counter = 7;\nconst unicodeValue = '山女馬 / 马目 abc 123';\nconst keyName = Common.generateWord(10);\nconst keyValue = '\\\\xe5\\\\xb1\\\\xb1\\\\xe5\\\\xa5\\\\xb3\\\\xe9\\\\xa6\\\\xac / \\\\xe9\\\\xa9\\\\xac\\\\xe7\\\\x9b\\\\xae abc 123';\nconst parameters = [\n    '[results=group]',\n    '[mode=raw]',\n    '[mode=raw;results=group;pipeline=3]',\n    '[mode=ascii;results=single]',\n    '[mode=ascii;mode=raw;results=single]',\n    '[mode=raw;results=silent;pipeline=3]',\n    '[mode=ascii;results=silent;pipeline=1]'\n];\nconst commands = [\n    `${counter} INFO`,\n    `${counter} get ${keyName}`,\n    `get ${keyName}`,\n    'invalidCommand'\n];\n\nfixture `Workbench modes to non-auto guides`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest\n    .before(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n        await workbenchPage.sendCommandInWorkbench(`set ${keyName} \"${keyValue}\"`);\n    })\n    .after(async t => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    })\n    .skip('Workbench modes from editor', async t => {\n        const groupCommandResultName = `${counter} Command(s) - ${counter} success, 0 error(s)`;\n        const containerOfCommand = await workbenchPage.getCardContainerByCommand(groupCommandResultName);\n\n        // Verify that results parameter applied from the first raw in the Workbench Editor\n        await workbenchPage.sendMultipleCommandsInWorkbench([parameters[0], commands[0]]);\n        await t.expect(workbenchPage.queryCardCommand.textContent).eql(groupCommandResultName, 'Group mode not applied');\n        await t.hover(workbenchPage.parametersAnchor);\n        // Verify that group mode icon is displayed\n        await t.expect(workbenchPage.groupModeIcon.exists).ok('Group mode icon not displayed');\n\n        // Verify that mode parameter applied from the first raw in the Workbench Editor\n        await workbenchPage.sendMultipleCommandsInWorkbench([parameters[1], commands[2]]);\n        await workbenchPage.checkWorkbenchCommandResult(commands[2], `\"${unicodeValue}\"`);\n        await t.hover(workbenchPage.parametersAnchor);\n        // Verify that raw mode icon is displayed\n        await t.expect(workbenchPage.rawModeIcon.exists).ok('Raw mode icon not displayed');\n\n        // Verify that multiple parameters applied from the first raw in the Workbench Editor\n        await workbenchPage.sendMultipleCommandsInWorkbench([parameters[2], commands[1]]);\n        await t.expect(workbenchPage.queryCardCommand.textContent).eql(groupCommandResultName, 'Group mode not applied');\n        const actualCommandResult = await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).textContent;\n        await t.expect(actualCommandResult).contains(`\"${unicodeValue}\"`, 'Actual command result is not equal to executed');\n\n        await t.hover(workbenchPage.parametersAnchor);\n        // Verify that raw and group mode icons are displayed\n        await t.expect(workbenchPage.groupModeIcon.exists).ok('Group mode icon not displayed');\n        await t.expect(workbenchPage.rawModeIcon.exists).ok('Raw mode icon not displayed');\n\n        // Add text with parameters in Workbench editor input\n        await t.typeText(workbenchPage.queryInput, parameters[4], { replace: true });\n        // Re-run the last command in results\n        await t.click(containerOfCommand.find(workbenchPage.cssReRunCommandButton));\n        // Verify that on re-run any command from history the same parameters specified regardless of Workbench editor input\n        await t.expect(actualCommandResult).contains(`\"${unicodeValue}\"`, 'The command is not re-executed');\n\n        // Clear value in input\n        await t.click(workbenchPage.submitCommandButton);\n        // Turn on raw and group modes\n        await t.click(workbenchPage.rawModeBtn);\n        await t.click(workbenchPage.groupMode);\n        await workbenchPage.sendMultipleCommandsInWorkbench([parameters[3], commands[1]]);\n        // Verify that Workbench Editor parameters have more priority than manually clicked modes\n        await t.expect(workbenchPage.queryTextResult.textContent).contains(`\"${keyValue}\"`, 'The mode is not applied from editor parameters');\n        await t.expect(workbenchPage.queryCardCommand.textContent).eql(`get ${keyName}`, 'The result is not applied from editor parameters');\n\n        // Turn off raw and group modes\n        await t.click(workbenchPage.rawModeBtn);\n        await t.click(workbenchPage.groupMode);\n        // Verify that if user specifies the same parameters he can see the first one is applied\n        await workbenchPage.sendMultipleCommandsInWorkbench([parameters[4], commands[1]]);\n        await t.expect(workbenchPage.queryTextResult.textContent).contains(`\"${keyValue}\"`, 'The first duplicated parameter not applied');\n    });\ntest\n    .skip('Workbench Silent mode', async t => {\n    const silentCommandSuccessResultName = `${counter} Command(s) - ${counter} success`;\n    const silentCommandErrorsResultName = `${counter + 1} Command(s) - ${counter} success, 1 error(s)`;\n    const errorResult = `\"ERR unknown command \\'${commands[3]}\\', with args beginning with: \"`;\n\n    await workbenchPage.sendMultipleCommandsInWorkbench([parameters[5], commands[0]]);\n    // Verify that user can see the success command output with header: {number} Command(s) - {number} success\n    await t.expect(workbenchPage.queryCardCommand.textContent).eql(silentCommandSuccessResultName, 'Silent mode not applied');\n    // Verify that user can see the command output is grouped into one window when run any guide or tutorial with the [results=silent]\n    await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).notOk('The result is displayed in silent mode');\n\n    await t.hover(workbenchPage.parametersAnchor);\n    // Verify that silent mode icon displayed\n    await t.expect(workbenchPage.silentModeIcon.exists).ok('Silent mode icon not displayed');\n\n    await workbenchPage.sendMultipleCommandsInWorkbench([parameters[6], commands[3], commands[0]]);\n    // Verify that user can expand the results to see the list of commands with errors and the list of errors per a command\n    await t.click(workbenchPage.queryCardContainer.nth(0));\n    await t.expect(workbenchPage.queryTextResult.nth(0).textContent).contains(commands[3], 'Silent mode result does not contain error');\n    await t.expect(workbenchPage.commandExecutionResultFailed.textContent).contains(errorResult, 'Error message not displayed');\n    await t.expect(workbenchPage.queryTextResult.nth(0).textContent).notContains('INFO', 'Silent mode result contains not only errors');\n    // Verify that user can see the errors command output with header: {number} Command(s) - {number} success, {number} error(s)\n    await t.expect(workbenchPage.queryCardCommand.textContent).eql(silentCommandErrorsResultName, 'Silent mode with errors header text is invalid');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, MyRedisDatabasePage, SettingsPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { Common } from '../../../../helpers/common';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst workbenchPage = new WorkbenchPage();\nconst browserPage = new BrowserPage();\nconst settingsPage = new SettingsPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst externalPageLink = 'https://redis.io/docs/latest/develop/using-commands/pipelining/';\nconst pipelineValues = ['-5', '5', '4', '20'];\nconst commandForSend = '100 scan 0 match * count 5000';\n\nfixture `Workbench Pipeline`\n    .meta({ type: 'regression', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig);\n        // Go to Settings page - Pipeline mode\n        await t.click(myRedisDatabasePage.NavigationPanel.settingsButton);\n        await t.click(settingsPage.accordionWorkbenchSettings);\n    });\ntest('Verify that user can see the text in settings for pipeline with link', async t => {\n    const pipelineText = 'Sets the size of a command batch for the pipeline mode in Workbench. 0 or 1 pipelines every command.';\n\n    // Verify that user can enter only numbers >0 in \"Commands in pipeline\" input\n    await t.hover(settingsPage.commandsInPipelineValue);\n    await t.click(settingsPage.commandsInPipelineInput);\n    await t.typeText(settingsPage.commandsInPipelineInput, '-25', { replace: true });\n    await t.click(settingsPage.EditorButton.applyBtn);\n    // Verify that negative number converted to positive\n    await t.hover(settingsPage.commandsInPipelineValue);\n    await t.click(settingsPage.commandsInPipelineInput);\n    await t.expect(settingsPage.commandsInPipelineInput.value).eql('25', 'Value is incorrect');\n\n    // Verify text in setting for pipeline\n    await t.expect(settingsPage.accordionWorkbenchSettings.textContent).contains(pipelineText, 'Text is incorrect');\n\n    await t.click(settingsPage.pipelineLink);\n    // Check new opened window page with the correct URL\n    await Common.checkURL(externalPageLink);\n});\ntest.skip('Verify that only chosen in pipeline number of commands is loading at the same time in Workbench', async t => {\n    await settingsPage.changeCommandsInPipeline(pipelineValues[1]);\n    // Go to Workbench page\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneBigConfig.databaseName);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench(commandForSend, 0.01);\n    // Verify that only selected pipeline number of commands are loaded at the same time\n    await t.expect(workbenchPage.loadedCommand.count).eql(Number(pipelineValues[1]), 'The number of sending commands is incorrect');\n});\ntest.skip('Verify that user can see spinner over Run button and grey preloader for each command', async t => {\n    await settingsPage.changeCommandsInPipeline(pipelineValues[3]);\n    // Go to Workbench page\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneBigConfig.databaseName);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench(commandForSend, 0.01);\n    // Verify that user can`t start new commands from the Workbench while command(s) is executing\n    await t.expect(workbenchPage.submitCommandButton.withAttribute('disabled').exists).ok('Run button is not disabled', { timeout: 5000 });\n    // Verify that user can see spinner over the disabled and shrunk Run button\n    await t.expect(workbenchPage.runButtonSpinner.exists).ok('Loading spinner is not displayed for Run button', { timeout: 5000 });\n    await t.expect(workbenchPage.queryCardContainer.find(workbenchPage.cssDeleteCommandButton).withAttribute('disabled').count).eql(Number(pipelineValues[3]), 'The number of commands is incorrect');\n});\ntest\n    .skip('Verify that user can interact with the Editor while command(s) in progress', async t => {\n    const valueInEditor = '100';\n\n    await settingsPage.changeCommandsInPipeline(pipelineValues[2]);\n    // Go to Workbench page\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneBigConfig.databaseName);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench(commandForSend);\n    await t.typeText(workbenchPage.queryInput, commandForSend, { replace: true });\n    // await t.pressKey('enter');\n    // Verify that user can interact with the Editor\n    await t.expect(workbenchPage.queryInputScriptArea.textContent).contains(valueInEditor, { timeout: 5000 });\n});\ntest\n    .skip('Verify that command results are added to history in order most recent - on top', async t => {\n    const multipleCommands = [\n        'INFO',\n        'FT._LIST',\n        'FT.INFO',\n        'RANDOMKEY',\n        'CLIENT LIST'\n    ];\n    const reverseCommands = multipleCommands.slice().reverse();\n\n    await settingsPage.changeCommandsInPipeline(pipelineValues[2]);\n    // Go to Workbench page\n    await myRedisDatabasePage.navigateToDatabase(ossStandaloneBigConfig.databaseName);\n    await t.click(browserPage.NavigationTabs.workbenchButton);\n    await workbenchPage.sendCommandInWorkbench(multipleCommands.join('\\n'));\n    // Check that the results for all commands are displayed in workbench history in reverse order (most recent - on top)\n    for (let i = 0; i < multipleCommands.length; i++) {\n        await t.expect(workbenchPage.queryCardCommand.nth(i).textContent).contains(reverseCommands[i], 'Wrong order of commands');\n    }\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/add-keys.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `Add keys`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can add Hash Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add Hash key\n    await browserPage.addHashKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The Hash key is not added');\n});\ntest('Verify that user can add Set Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add Set key\n    await browserPage.addSetKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The Set key is not added');\n});\ntest('Verify that user can add List Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add List key\n    await browserPage.addListKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The List key is not added');\n});\ntest('Verify that user can add String Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add String key\n    await browserPage.addStringKey(keyName);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The String key is not added');\n});\ntest('Verify that user can add ZSet Key', async t => {\n    keyName = Common.generateWord(10);\n    // Add ZSet key\n    await browserPage.addZSetKey(keyName, '111');\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The ZSet key is not added');\n});\ntest('Verify that user can add JSON Key', async t => {\n    keyName = Common.generateWord(10);\n    const keyTTL = '2147476121';\n    const value = '{\"name\":\"xyz\"}';\n\n    // Add JSON key\n    await browserPage.addJsonKey(keyName, value, keyTTL);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not displayed');\n    // Check that new key is displayed in the list\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The JSON key is not added');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/edit-key-name.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { Telemetry } from '../../../../helpers/telemetry';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst telemetry = new Telemetry();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyNameBefore = Common.generateWord(10);\nlet keyNameAfter = Common.generateWord(10);\nconst keyTTL = 2147476121;\nconst logger = telemetry.createLogger();\nconst telemetryEvent = 'BROWSER_KEY_VALUE_VIEWED';\nconst expectedProperties = [\n    'databaseId',\n    'keyType',\n    'length'\n];\n\nfixture `Edit Key names verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyNameAfter, ossStandaloneConfig.databaseName);\n    });\ntest\n    .requestHooks(logger)('Verify that user can edit String Key name', async t => {\n        keyNameBefore = Common.generateWord(10);\n        keyNameAfter = Common.generateWord(10);\n\n        await apiKeyRequests.addStringKeyApi({\n            keyName: keyNameBefore,\n            value: 'v',\n            ttl: keyTTL,\n        }, ossStandaloneConfig);\n        await browserPage.navigateToKey(keyNameBefore);\n\n        let keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n        await t.expect(keyNameFromDetails).contains(keyNameBefore, 'The String Key Name not correct before editing');\n\n        // TODO : This can be a separate test however this is failing at present making the test unstable\n        // Verify that telemetry event 'BROWSER_KEY_VALUE_VIEWED' sent and has all expected properties\n        // await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger);\n        // await telemetry.verifyEventPropertyValue(telemetryEvent, 'keyType', 'string', logger);\n\n        await browserPage.editKeyName(keyNameAfter);\n        keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n        await t.expect(keyNameFromDetails).contains(keyNameAfter, 'The String Key Name not correct after editing');\n    });\ntest('Verify that user can edit Set Key name', async t => {\n    keyNameBefore = Common.generateWord(10);\n    keyNameAfter = Common.generateWord(10);\n\n    await apiKeyRequests.addSetKeyApi({\n        keyName: keyNameBefore,\n        members: ['m'],\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyNameBefore);\n\n    let keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameBefore, 'The Set Key Name not correct before editing');\n    await browserPage.editKeyName(keyNameAfter);\n    keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameAfter, 'The Set Key Name not correct after editing');\n});\ntest('Verify that user can edit Zset Key name', async t => {\n    keyNameBefore = Common.generateWord(10);\n    keyNameAfter = Common.generateWord(10);\n\n    await apiKeyRequests.addSortedSetKeyApi({\n        keyName: keyNameBefore,\n        members: [{\n            name: 'n',\n            score: 0,\n        }],\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyNameBefore);\n\n    let keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameBefore, 'The Zset Key Name not correct before editing');\n    await browserPage.editKeyName(keyNameAfter);\n    keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameAfter, 'The Zset Key Name not correct after editing');\n});\ntest('Verify that user can edit Hash Key name', async t => {\n    keyNameBefore = Common.generateWord(10);\n    keyNameAfter = Common.generateWord(10);\n\n    await apiKeyRequests.addHashKeyApi({\n        keyName: keyNameBefore,\n        ttl: keyTTL,\n        fields: [{\n            field: 'f',\n            value:'v',\n        }]\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyNameBefore);\n\n    let keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameBefore, 'The Hash Key Name not correct before editing');\n    await browserPage.editKeyName(keyNameAfter);\n    keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameAfter, 'The Hash Key Name not correct after editing');\n});\ntest('Verify that user can edit List Key name', async t => {\n    keyNameBefore = Common.generateWord(10);\n    keyNameAfter = Common.generateWord(10);\n\n    await apiKeyRequests.addListKeyApi({\n        keyName: keyNameBefore,\n        elements: ['e'],\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyNameBefore);\n\n    let keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameBefore, 'The List Key Name not correct before editing');\n    await browserPage.editKeyName(keyNameAfter);\n    keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameAfter, 'The List Key Name not correct after editing');\n});\ntest('Verify that user can edit JSON Key name', async t => {\n    keyNameBefore = Common.generateWord(10);\n    keyNameAfter = Common.generateWord(10);\n    const keyValue = '{\"name\":\"xyz\"}';\n\n    await apiKeyRequests.addJsonKeyApi({\n        keyName: keyNameBefore,\n        data: keyValue,\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyNameBefore);\n\n    let keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameBefore, 'The JSON Key Name not correct before editing');\n    await browserPage.editKeyName(keyNameAfter);\n    keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyNameAfter, 'The JSON Key Name not correct after editing');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/edit-key-value.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nconst keyTTL = 2147476121;\nconst keyValueBefore = 'ValueBeforeEdit!';\nconst keyValueAfter = 'ValueAfterEdit!';\nlet keyName = Common.generateWord(10);\n\nfixture `Edit Key values verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can edit String value', async t => {\n    keyName = Common.generateWord(10);\n\n    // Add string key\n    await apiKeyRequests.addStringKeyApi({\n        keyName,\n        value: keyValueBefore,\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Check the key value before edit\n    let keyValue = await browserPage.getStringKeyValue();\n    await t.expect(keyValue).contains(keyValueBefore, 'The String value is incorrect');\n    // Edit String key value\n    await t.click(browserPage.stringKeyValueInput);\n    await t.typeText(browserPage.stringKeyValueInput, keyValueAfter, { replace: true, paste: true });\n    // Verify that refresh is disabled for String key when editing value\n    await t.expect(browserPage.refreshKeyButton.hasAttribute('disabled')).ok('Refresh button not disabled');\n\n    await t.click(browserPage.EditorButton.applyBtn);\n    // Check the key value after edit\n    keyValue = await browserPage.getStringKeyValue();\n    await t.expect(keyValue).contains(keyValueAfter, 'Edited String value is incorrect');\n});\ntest('Verify that user can edit Zset Key member', async t => {\n    keyName = Common.generateWord(10);\n    const scoreBefore = 5;\n    const scoreAfter = 10;\n\n    // Add zset key\n    await apiKeyRequests.addSortedSetKeyApi({\n        keyName,\n        members: [{\n            name: keyValueBefore,\n            score: scoreBefore,\n        }],\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Check the key score before edit\n    let zsetScore = await browserPage.getZsetKeyScore();\n    await t.expect(zsetScore).eql(`${scoreBefore}`, 'Zset Score is incorrect');\n    // Edit Zset key score\n    await t.hover(browserPage.zsetScoresList);\n    await t.click(browserPage.editZsetButton);\n    await t.typeText(browserPage.inlineItemEditor, `${scoreAfter}`, { replace: true, paste: true });\n    // Verify that refresh is disabled for Zset key when editing member\n    await t.expect(browserPage.refreshKeyButton.hasAttribute('disabled')).ok('Refresh button not disabled');\n\n    await t.click(browserPage.EditorButton.applyBtn);\n    // Check Zset key score after edit\n    zsetScore = await browserPage.getZsetKeyScore();\n    await t.expect(zsetScore).contains(`${scoreAfter}`, 'Zset Score is not edited');\n});\ntest('Verify that user can edit Hash Key field', async t => {\n    const fieldName = 'test';\n    keyName = Common.generateWord(10);\n\n    // Add Hash key\n    await apiKeyRequests.addHashKeyApi({\n        keyName,\n        fields: [{\n            field: 'f',\n            value: keyValueBefore,\n        }],\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Check the key value before edit\n    let keyValue = await browserPage.getHashKeyValue();\n    await t.expect(keyValue).eql(keyValueBefore, 'The Hash value is incorrect');\n    // Edit Hash key value\n    await t.hover(browserPage.hashFieldValue);\n    await t.click(browserPage.editHashButton);\n    await t.typeText(browserPage.hashFieldValueEditor, keyValueAfter, { replace: true, paste: true });\n    // Verify that refresh is disabled for Hash key when editing field\n    await t.expect(browserPage.refreshKeyButton.hasAttribute('disabled')).ok('Refresh button not disabled');\n\n    await t.click(browserPage.EditorButton.applyBtn);\n    // Check Hash key value after edit\n    keyValue = await browserPage.getHashKeyValue();\n    await t.expect(keyValue).contains(keyValueAfter, 'Edited Hash value is incorrect');\n});\ntest('Verify that user can edit List Key element', async t => {\n    keyName = Common.generateWord(10);\n\n    // Add List key\n    await apiKeyRequests.addListKeyApi({\n        keyName,\n        elements: [keyValueBefore],\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n\n    await browserPage.navigateToKey(keyName);\n    // Check the key value before edit\n    let keyValue = await browserPage.getListKeyValue();\n    await t.expect(keyValue).eql(keyValueBefore, 'The List value is incorrect');\n    // Edit List key value\n    await t.hover(browserPage.listElementsList);\n    await t.click(browserPage.editListButton);\n    await t.typeText(browserPage.listKeyElementEditorInput, keyValueAfter, { replace: true, paste: true });\n    // Verify that refresh is disabled for List key when editing element\n    await t.expect(browserPage.refreshKeyButton.hasAttribute('disabled')).ok('Refresh button not disabled');\n\n    await t.click(browserPage.EditorButton.applyBtn);\n    // Check List key value after edit\n    keyValue = await browserPage.getListKeyValue();\n    await t.expect(keyValue).contains(keyValueAfter, 'Edited List value is incorrect');\n});\ntest('Verify that user can edit JSON Key value', async t => {\n    const jsonValueBefore = { name : 'xyz'};\n    const jsonEditedValue = '\"xyz test\"';\n    const jsonValueAfter = '{name:\"xyz test\"}';\n    keyName = Common.generateWord(10);\n\n    // Add JSON key with json object\n    await apiKeyRequests.addJsonKeyApi({\n        keyName,\n        data: jsonValueBefore,\n        ttl: keyTTL,\n    }, ossStandaloneConfig);\n    await browserPage.navigateToKey(keyName);\n\n    // Check the key value before edit\n    await t.expect(await browserPage.getJsonKeyValue()).eql('{name:\"xyz\"}', 'The JSON value is incorrect');\n    // Edit JSON key value\n    await t.click(browserPage.jsonScalarValue);\n    await t.typeText(browserPage.inlineItemEditor, jsonEditedValue, { replace: true, paste: true });\n    // Verify that refresh is not disabled for JSON key when editing value\n    await t.expect(browserPage.refreshKeyButton.hasAttribute('disabled')).notOk('Refresh button disabled for JSON');\n\n    await t.click(browserPage.EditorButton.applyBtn);\n    // Check JSON key value after edit\n    await t.expect(await browserPage.getJsonKeyValue()).contains(jsonValueAfter, 'Edited JSON value is incorrect');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/filtering.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = `KeyForSearch*?[]789${Common.generateWord(10)}`;\nlet keyName2 = Common.generateWord(10);\nlet randomValue = Common.generateWord(10);\nconst valueWithEscapedSymbols = 'KeyFor[A-G]*(';\nconst searchedKeyName = 'KeyForSearch\\\\*\\\\?\\\\[]789';\nconst searchedValueWithEscapedSymbols = 'KeyFor\\\\[A-G\\\\]\\*\\(';\n\nfixture `Filtering per key name in Browser page`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await browserPage.deleteKeyByName(`${searchedKeyName}${randomValue}`);\n    });\ntest('Verify that user can search per full key name', async t => {\n    randomValue = Common.generateWord(10);\n    keyName = `KeyForSearch*?[]789${randomValue}`;\n    // Add new key\n    await apiKeyRequests.addStringKeyApi({ keyName, value: 'a' }, ossStandaloneConfig)\n    // Search by key with full name\n    await browserPage.searchByKeyName(`${searchedKeyName}${randomValue}`);\n    // Verify that key was found\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The key was not found');\n});\ntest('Verify that user can filter per exact key without using any patterns', async t => {\n    randomValue = Common.generateWord(10);\n    keyName = `KeyForSearch*?[]789${randomValue}`;\n\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Create new key for search\n    await t.typeText(browserPage.Cli.cliCommandInput, `APPEND ${keyName} 1`, { replace: true, paste: true });\n    await t.pressKey('enter');\n    await t.click(browserPage.Cli.cliCollapseButton);\n    // Filter per exact key without using any patterns\n    await browserPage.searchByKeyName(`${searchedKeyName}${randomValue}`);\n    // Verify that key was found\n    await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');\n});\ntest\n    .after(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(keyName2, ossStandaloneConfig.databaseName);\n        await apiKeyRequests.deleteKeyByNameApi(valueWithEscapedSymbols, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that user can filter per combined pattern with ?, *, [xy], [^x], [a-z] and escaped special symbols', async t => {\n        keyName = `KeyForSearch${Common.generateWord(10)}`;\n        keyName2 = `KeyForSomething${Common.generateWord(10)}`;\n\n        // Add keys\n        await apiKeyRequests.addStringKeyApi({ keyName, value: 'a' }, ossStandaloneConfig)\n        await apiKeyRequests.addHashKeyApi({ keyName: keyName2, fields: [{ field: 'f', value: 'v' }] }, ossStandaloneConfig)\n        await apiKeyRequests.addHashKeyApi({ keyName: valueWithEscapedSymbols, fields: [{ field: 'f', value: 'v' }] }, ossStandaloneConfig)\n\n        // Filter per pattern with ?, *, [xy], [^x], [a-z]\n        const searchedValue = 'Key?[A-z]rS[^o][ae]*';\n        await browserPage.searchByKeyName(searchedValue);\n        // Verify that key was found\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('The key was not found');\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName2)).notOk('The key is found');\n        // Filter with escaped special symbols\n        await browserPage.searchByKeyName(searchedValueWithEscapedSymbols);\n        // Verify that key was found\n        await t.expect(await browserPage.isKeyIsDisplayedInTheList(valueWithEscapedSymbols)).ok('The key was not found');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/hash-field.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst keyFieldValue = 'hashField11111';\nconst keyValue = 'hashValue11111!';\n\nfixture `Hash Key fields verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can add field to Hash', async t => {\n    keyName = Common.generateWord(10);\n    await browserPage.addHashKey(keyName, keyTTL);\n    // Add field to the hash key\n    await browserPage.addFieldToHash(keyFieldValue, keyValue);\n    // Search the added field\n    await browserPage.searchByTheValueInKeyDetails(keyFieldValue);\n    // Check the added field\n    await t.expect(browserPage.hashValuesList.withExactText(keyValue).exists).ok('The value is not displayed', { timeout: 10000 });\n    await t.expect(browserPage.hashFieldsList.withExactText(keyFieldValue).exists).ok('The field is not displayed', { timeout: 10000 });\n\n    // Verify that user can remove field from Hash\n    await t.click(browserPage.removeHashFieldButton);\n    await t.click(browserPage.confirmRemoveHashFieldButton);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Field has been removed', 'The notification is not displayed');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/json-key.e2e.ts",
    "content": "import * as path from 'path';\nimport { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst value = '{\"name\":\"xyz\"}';\nconst jsonObjectValue = '{name:\"xyz\"}';\nconst jsonFilePath = path.join('..', '..', '..', '..', 'test-data', 'big-json', 'big-json.json');\n\nfixture `JSON Key verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can add key with value to any level of JSON structure', async t => {\n    keyName = Common.generateWord(10);\n    // Add Json key with json object\n    await browserPage.addJsonKey(keyName, value, keyTTL);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not found');\n    // Verify that user can create JSON object\n    await t.expect(browserPage.addJsonObjectButton.exists).ok('The add Json object button not found', { timeout: 10000 });\n    await t.expect(browserPage.jsonKeyValue.textContentWithoutButtons).eql(jsonObjectValue, 'The json object value not found');\n\n    // Add key with value on the same level\n    await browserPage.addJsonKeyOnTheSameLevel('\"key1\"', '\"value1\"');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.addJsonObjectButton.exists).ok('The add Json object button not found', { timeout: 10000 });\n    await t.expect(browserPage.jsonKeyValue.textContentWithoutButtons).eql('{name:\"xyz\"key1:\"value1\"}', 'The json object value not found');\n    // Add key with value inside the json\n    await browserPage.addJsonKeyOnTheSameLevel('\"key2\"', '{}');\n    await browserPage.addJsonKeyInsideStructure('\"key2222\"', '12345');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.jsonKeyValue.textContentWithoutButtons).eql('{name:\"xyz\"key1:\"value1\"key2:{key2222:12345}}', 'The json object value not found');\n});\ntest('Verify that user can add key with value to any level of JSON structure for big JSON object', async t => {\n    keyName = Common.generateWord(10);\n    // Add Json key with json object\n    await t.click(browserPage.plusAddKeyButton);\n    await t.click(browserPage.keyTypeDropDown);\n    await t.click(browserPage.jsonOption);\n    await t.click(browserPage.addKeyNameInput);\n    await t.typeText(browserPage.addKeyNameInput, keyName, { replace: true, paste: true });\n    await t.setFilesToUpload(browserPage.jsonUploadInput, [jsonFilePath]);\n    await t.click(browserPage.addKeyButton);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification not found');\n    // Add key with value on the same level\n    await browserPage.addJsonKeyOnTheSameLevel('\"key1\"', '\"value1\"');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.addJsonObjectButton.exists).ok('The add Json object button not found', { timeout: 10000 });\n    await t.expect(browserPage.jsonKeyValue.textContentWithoutButtons).contains('\"key1:\"value1\"}', 'The json object value not found');\n    // Add value inside the json array\n    await browserPage.addJsonValueInsideStructure('12345');\n    // Check the added key contains json object with added key\n    await t.expect(browserPage.jsonKeyValue.textContentWithoutButtons).contains('\"70:12345]', 'The json object value not found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/list-key.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst element = '1111listElement11111';\nconst element2 = '2222listElement22222';\nconst element3 = '33333listElement33333';\n\nfixture `List Key verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can select remove List element position: from tail', async t => {\n    keyName = Common.generateWord(10);\n    await browserPage.addListKey(keyName, keyTTL);\n    // Add few elements to the List key\n    await browserPage.addElementToList([element]);\n    // Verify that user can add element to List\n    await t.expect(browserPage.listElementsList.withExactText(element).exists).ok('The list element not added', { timeout: 10000 });\n\n    await browserPage.addElementToList([element2,element3]);\n    // Remove element from the key\n    await browserPage.removeListElementFromTail('1');\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Elements have been removed', 'The notification not found');\n    // Check the removed element is not in the list\n    await t.expect(browserPage.listElementsList.withExactText(element3).exists).notOk('The list element not removed', { timeout: 10000 });\n});\ntest('Verify that user can select remove List element position: from head', async t => {\n    keyName = Common.generateWord(10);\n    await browserPage.addListKey(keyName, keyTTL, [element]);\n    // Add few elements to the List key\n    await browserPage.addElementToList([element2,element3]);\n    // Remove element from the key\n    await browserPage.removeListElementFromHead('1');\n    // Check the notification message\n    const notofication = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notofication).contains('Elements have been removed', 'The notification not found');\n    // Check the removed element is not in the list\n    await t.expect(browserPage.listElementsList.withExactText(element).exists).notOk('The list element not removed', { timeout: 10000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/list-of-keys-verifications.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nlet keyNames: string[] = [];\nconst keyTTL = '2147476121';\n\nfixture `List of keys verifications`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest\n    .after(async() => {\n        // Clear and delete database\n        for(const name of keyNames) {\n            await apiKeyRequests.deleteKeyByNameApi(name, ossStandaloneConfig.databaseName);\n        }\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that user can scroll List of Keys in DB', async t => {\n        keyNames = [\n            `key-${Common.generateWord(10)}`,\n            `key-${Common.generateWord(10)}`,\n            `key-${Common.generateWord(10)}`,\n            `key-${Common.generateWord(10)}`\n        ];\n\n        await browserPage.addStringKey(keyNames[0]);\n        await browserPage.addHashKey(keyNames[1]);\n        await browserPage.addListKey(keyNames[2]);\n        await browserPage.addStringKey(keyNames[3]);\n        await t.click(browserPage.refreshKeysButton);\n        // Verify that user can see List of Keys in DB\n        await t.expect(browserPage.keyNameInTheList.exists).ok('The list of keys is not displayed');\n\n        // Scroll to the key element\n        await t.hover(browserPage.keyNameInTheList);\n        await t.expect(browserPage.keyNameInTheList.exists).ok('The list of keys is not displayed');\n    });\ntest('Verify that user can open key details', async t => {\n    keyName = Common.generateWord(10);\n    const keyValue = 'StringValue!';\n\n    // Add String key\n    await browserPage.addStringKey(keyName, keyTTL, keyValue);\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification is not displayed');\n    await t.click(browserPage.closeKeyButton);\n    // Search for the added key\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).ok('The key is not in the list');\n    // Open the key details\n    await t.click(browserPage.keyNameInTheList);\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    await t.expect(keyNameFromDetails).contains(keyName, 'The Key details is not opened');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/set-key.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst keyMember = '1111setMember11111';\n\nfixture `Set Key fields verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can remove member from Set', async t => {\n    keyName = Common.generateWord(10);\n    await browserPage.addSetKey(keyName, keyTTL);\n    // Add member to the Set key\n    await browserPage.addMemberToSet(keyMember);\n    // Verify that user can add member to Set\n    await t.expect(browserPage.setMembersList.withExactText(keyMember).exists).ok('The set member not found', { timeout: 10000 });\n\n    // Remove member from the key\n    await t.click(browserPage.removeSetMemberButton);\n    await t.click(browserPage.confirmRemoveSetMemberButton);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Member has been removed', 'The notification not found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/set-ttl-for-key.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `Set TTL for Key`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can specify TTL for Key', async t => {\n    keyName = Common.generateWord(10);\n    const ttlValue = '2147476121';\n\n    // Create new key without TTL\n    await browserPage.addStringKey(keyName);\n    // Open Key details\n    await browserPage.openKeyDetails(keyName);\n    // Click on TTL button to edit TTL\n    await t.click(browserPage.editKeyTTLButton);\n    // Set TTL value\n    await t.typeText(browserPage.editKeyTTLInput, ttlValue, { replace: true, paste: true });\n    // Save the TTL value\n    await t.click(browserPage.EditorButton.applyBtn);\n    // Refresh the page in several seconds\n    await t.wait(3000);\n    await t.click(browserPage.refreshKeyButton);\n    // Verify that TTL was updated\n    const newTtlValue = await browserPage.ttlText.innerText;\n    await t.expect(Number(ttlValue)).gt(Number(newTtlValue), 'ttlValue is not greater than newTTLValue');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/verify-key-details.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst expectedTTL = /214747612*/;\n\nfixture `Key details verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can see Hash Key details', async t => {\n    keyName = Common.generateWord(10);\n\n    await browserPage.addHashKey(keyName, keyTTL);\n    const keyDetails = await browserPage.keyDetailsHeader.textContent;\n    const keyBadge = await browserPage.keyDetailsBadge.textContent;\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    const keyTTLValue = await browserPage.keyDetailsTTL.textContent;\n\n    await t.expect(keyNameFromDetails).contains(keyName, 'The Hash Key Name is incorrect');\n    await t.expect(keyDetails).contains('Hash', 'The Hash Key Type is incorrect');\n    await t.expect(keyDetails).contains('TTL', 'The Hash TTL is incorrect');\n    await t.expect(keyTTLValue).match(expectedTTL, 'The Hash Key TTL is incorrect');\n    await t.expect(keyBadge).contains('Hash', 'The Hash Key Badge is incorrect');\n});\ntest('Verify that user can see List Key details', async t => {\n    keyName = Common.generateWord(10);\n\n    await browserPage.addListKey(keyName, keyTTL);\n    const keyDetails = await browserPage.keyDetailsHeader.textContent;\n    const keyBadge = await browserPage.keyDetailsBadge.textContent;\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    const keyTTLValue = await browserPage.keyDetailsTTL.textContent;\n\n    await t.expect(keyNameFromDetails).contains(keyName, 'The List Key Name is incorrect');\n    await t.expect(keyDetails).contains('List', 'The List Key Type is incorrect');\n    await t.expect(keyDetails).contains('TTL', 'The List TTL is incorrect');\n    await t.expect(keyTTLValue).match(expectedTTL, 'The List Key TTL is incorrect');\n    await t.expect(keyBadge).contains('List', 'The List Key Badge is incorrect');\n});\ntest('Verify that user can see Set Key details', async t => {\n    keyName = Common.generateWord(10);\n\n    await browserPage.addSetKey(keyName, keyTTL);\n    const keyDetails = await browserPage.keyDetailsHeader.textContent;\n    const keyBadge = await browserPage.keyDetailsBadge.textContent;\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    const keyTTLValue = await browserPage.keyDetailsTTL.textContent;\n\n    await t.expect(keyNameFromDetails).contains(keyName, 'The Set Key Name is incorrect');\n    await t.expect(keyDetails).contains('Set', 'The Set Key Type is incorrect');\n    await t.expect(keyDetails).contains('TTL', 'The Set TTL is incorrect');\n    await t.expect(keyTTLValue).match(expectedTTL, 'The Set Key TTL is incorrect');\n    await t.expect(keyBadge).contains('Set', 'The Set Key Badge is incorrect');\n});\ntest('Verify that user can see String Key details', async t => {\n    keyName = Common.generateWord(10);\n    const value = 'keyValue12334353434;';\n\n    await browserPage.addStringKey(keyName, value, keyTTL);\n    const keyDetails = await browserPage.keyDetailsHeader.textContent;\n    const keyBadge = await browserPage.keyDetailsBadge.textContent;\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    const keyTTLValue = await browserPage.keyDetailsTTL.textContent;\n\n    await t.expect(keyNameFromDetails).contains(keyName, 'The String Key Name is incorrect');\n    await t.expect(keyDetails).contains('String', 'The String Key Type is incorrect');\n    await t.expect(keyDetails).contains('TTL', 'The StringTTL is incorrect');\n    await t.expect(keyTTLValue).match(expectedTTL, 'The String Key TTL is incorrect');\n    await t.expect(keyBadge).contains('String', 'The String Key Badge is incorrect');\n});\ntest('Verify that user can see ZSet Key details', async t => {\n    keyName = Common.generateWord(10);\n\n    await browserPage.addZSetKey(keyName, '1', keyTTL);\n    const keyDetails = await browserPage.keyDetailsHeader.textContent;\n    const keyBadge = await browserPage.keyDetailsBadge.textContent;\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    const keyTTLValue = await browserPage.keyDetailsTTL.textContent;\n\n    await t.expect(keyNameFromDetails).contains(keyName, 'The ZSet Key Name is incorrect');\n    await t.expect(keyDetails).contains('Sorted Set', 'The ZSet Key Type is incorrect');\n    await t.expect(keyDetails).contains('TTL', 'The ZSet TTL is incorrect');\n    await t.expect(keyTTLValue).match(expectedTTL, 'The ZSet Key TTL is incorrect');\n    await t.expect(keyBadge).contains('Sorted Set', 'The ZSet Key Badge is incorrect');\n});\ntest('Verify that user can see JSON Key details', async t => {\n    keyName = Common.generateWord(10);\n\n    const jsonValue = '{\"employee\":{ \"name\":\"John\", \"age\":30, \"city\":\"New York\" }}';\n\n    await browserPage.addJsonKey(keyName, jsonValue, keyTTL);\n    const keyDetails = await browserPage.keyDetailsHeader.textContent;\n    const keyBadge = await browserPage.keyDetailsBadge.textContent;\n    const keyNameFromDetails = await browserPage.keyNameFormDetails.textContent;\n    const keyTTLValue = await browserPage.keyDetailsTTL.textContent;\n\n    await t.expect(keyNameFromDetails).contains(keyName, 'The JSON Key Name is incorrect');\n    await t.expect(keyDetails).contains('JSON', 'The JSON Key Type is incorrect');\n    await t.expect(keyDetails).contains('TTL', 'The JSON TTL is incorrect');\n    await t.expect(keyTTLValue).match(expectedTTL, 'The JSON Key TTL is incorrect');\n    await t.expect(keyBadge).contains('JSON', 'The JSON Key Badge is incorrect');\n});\ntest('Verify that user can set ttl for Hash fields', async t => {\n    keyName = Common.generateWord(10);\n    const keyName2 = Common.generateWord(10);\n    const field1 = 'Field1WithTtl';\n    const field2 = 'Field2WithTtl';\n    await browserPage.addHashKey(keyName, ' ',  field1,  'value',  keyTTL);\n\n    //verify that user can create key with ttl for the has field\n    let ttlFieldValue = await browserPage.getHashTtlFieldInput(field1).textContent;\n    await t.expect(ttlFieldValue).match(expectedTTL, 'the field ttl is not set');\n\n    // verify that ttl can have empty value\n    await browserPage.editHashFieldTtlValue(field1, ' ');\n    ttlFieldValue = await browserPage.getHashTtlFieldInput(field1).textContentWithoutButtons;\n    await t.expect(ttlFieldValue).eql('No Limit', 'the field ttl can not be removed');\n\n    //verify that ttl field value can be set\n    await browserPage.addFieldToHash(field2, 'value', keyTTL);\n    ttlFieldValue = await browserPage.getHashTtlFieldInput(field2).textContent;\n    await t.expect(ttlFieldValue).match(expectedTTL, 'the field ttl is not set');\n\n    //verify that ttl column can be hidden\n    await t.click(browserPage.showTtlCheckbox);\n    await t.expect(await browserPage.getHashTtlFieldInput(field2).exists).notOk('the ttl column is not hidden');\n    await t.click(browserPage.showTtlCheckbox);\n\n    //verify that field is removed after ttl field is expired\n    await browserPage.editHashFieldTtlValue(field1, '1');\n    await t.wait(1000);\n    await t.click(browserPage.refreshKeyButton);\n    const result = browserPage.hashFieldsList.count;\n    await t.expect(result).eql(1, 'the field was not removed');\n\n    //verify that the key is removed if key has 1 field and ttl field is expired\n    await browserPage.addHashKey(keyName2, ' ',  field1);\n    await browserPage.editHashFieldTtlValue(field1, '1');\n    await t.wait(1000);\n    await t.click(browserPage.refreshKeysButton);\n\n    await t.expect(browserPage.getKeySelectorByName(keyName2).exists).notOk('key is not removed when the field ttl is expired');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/verify-keys-refresh.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `Keys refresh functionality`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest.skip('Verify that user can refresh Keys', async t => {\n    keyName = Common.generateWord(10);\n    const newKeyName = 'KeyNameAfterEdit!testKey';\n\n    // Add hash key\n    await browserPage.addHashKey(keyName);\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Key has been added', 'The notification is not displayed');\n    await t.click(browserPage.closeKeyButton);\n    // Search for the added key\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsDisplayedInTheList).eql(true, 'The key is not in the list');\n    // Edit the key name in details\n    await t.click(browserPage.keyNameInTheList);\n    await browserPage.editKeyName(newKeyName);\n    // Refresh Keys\n    await t.click(browserPage.refreshKeysButton);\n    await browserPage.searchByKeyName(keyName);\n    const isKeyIsNotDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n    await t.expect(isKeyIsNotDisplayedInTheList).eql(false, 'The key is still in the list');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/browser/zset-key.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\nconst keyTTL = '2147476121';\nconst keyMember = '1111ZsetMember11111';\nconst score = '0';\n\nfixture `ZSet Key fields verification`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Clear and delete database\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n    });\ntest('Verify that user can remove member from ZSet', async t => {\n    keyName = Common.generateWord(10);\n    await browserPage.addZSetKey(keyName, '6', keyTTL);\n    // Add member to the ZSet key\n    await browserPage.addMemberToZSet(keyMember, score);\n    // Verify that user can add members to Zset\n    await t.expect(browserPage.zsetMembersList.withExactText(keyMember).exists).ok('The Zset member not found', { timeout: 10000 });\n    await t.expect(browserPage.zsetScoresList.withExactText(score).exists).ok('The Zset score not found', { timeout: 10000 });\n\n    // Remove member from the key\n    await t.click(browserPage.removeZsetMemberButton);\n    await t.click(browserPage.confirmRemoveZSetMemberButton);\n    // Check the notification message\n    const notification = browserPage.Toast.toastHeader.textContent;\n    await t.expect(notification).contains('Member has been removed', 'The notification not found');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/cli/cli-command-helper.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { BrowserPage } from '../../../../pageObjects';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nconst COMMAND_APPEND = 'APPEND';\nconst COMMAND_GROUP_SET = 'Set';\n\nfixture `CLI Command helper`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest('Verify that user can search per command in Command Helper and see relevant results', async t => {\n    const commandForSearch = 'ADD';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Search per command\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, commandForSearch, { replace: true, paste: true });\n    // Verify results in the output\n    const count = await browserPage.CommandHelper.cliHelperOutputTitles.count;\n    for(let i = 0; i < count; i++){\n        await t.expect(await browserPage.CommandHelper.cliHelperOutputTitles.textContent).contains(commandForSearch, 'Results in the output not contains searched value');\n    }\n\n    // Close Command helper\n    await t.click(browserPage.CommandHelper.closeCommandHelperButton);\n    // Verify that user can close Command helper\n    await t.expect(browserPage.CommandHelper.cliHelperText.exists).notOk('Command helper');\n});\ntest('Verify that user can select one of the commands from the list of commands described in the Groups table', async t => {\n    const commandForCheck = 'SADD';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from list\n    await browserPage.CommandHelper.selectFilterGroupType(COMMAND_GROUP_SET);\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandForCheck));\n    // Verify results\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql('SADD key member [member ...]', 'Selected command information not correct');\n});\ntest('Verify that user can click on any of searched commands in Command Helper and see details of the command', async t => {\n    const commandForSearch = 'Ap';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from list of searched commands\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, commandForSearch, { replace: true, paste: true });\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(COMMAND_APPEND));\n    // Verify details of the command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.textContent).eql('APPEND key value', 'Command name and syntax not correct');\n    await t.expect(browserPage.CommandHelper.cliHelperTitle.innerText).contains('STRING', 'Command Group badge not correct');\n    await t.expect(browserPage.CommandHelper.cliHelperSummary.innerText).contains('Appends a string to the value of a key. Creates the key if it doesn\\'t exist.', 'Command summary not correct');\n});\ntest('Verify that when user enters command, he can see Command Name, Complexity, Arguments, Summary, Group, Read more', async t => {\n    const commandForSearch = 'pop';\n    const commandForCheck = 'LPOP';\n\n    // Open Command Helper\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Select one command from list of searched commands\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, commandForSearch, { replace: true, paste: true });\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(commandForCheck));\n    // Verify details of the command\n    await t.expect(browserPage.CommandHelper.cliHelperTitleArgs.innerText).eql('LPOP key [count]', 'Command Name not correct');\n    await t.expect(browserPage.CommandHelper.cliHelperComplexity.innerText).eql('Complexity:\\n\\nO(N) where N is the number of elements returned', 'Complexity not correct');\n    await t.expect(browserPage.CommandHelper.cliHelperArguments.innerText).eql('Arguments:\\n\\nRequired\\n\\nkey\\n\\nOptional\\n\\n[count]', 'Arguments not correct');\n    await t.expect(browserPage.CommandHelper.cliHelperSummary.innerText).contains('Returns the first elements in a list after removing it. Deletes the list if the last element was popped.', 'Command Summary not correct');\n    await t.expect(browserPage.CommandHelper.cliHelperTitle.innerText).contains('LIST', 'Command Group not correct');\n    await t.expect(browserPage.CommandHelper.readMoreButton.exists).ok('Read more button not displayed');\n});\ntest('Verify that user can see that command is autocompleted in CLI with required arguments', async t => {\n    const command = 'HDEL';\n\n    // Open CLI and Helper\n    await t.click(browserPage.Cli.cliExpandButton);\n    await t.click(browserPage.CommandHelper.expandCommandHelperButton);\n    // Search for the command and remember arguments\n    await t.typeText(browserPage.CommandHelper.cliHelperSearch, command, { replace: true, paste: true });\n    await t.click(browserPage.CommandHelper.cliHelperOutputTitles.withExactText(command));\n    const commandArgsFromCliHelper = await browserPage.CommandHelper.cliHelperTitleArgs.innerText;\n    // Enter the command in CLI\n    await t.typeText(browserPage.Cli.cliCommandInput, command, { replace: true, paste: true });\n    // Verify autocompleted arguments\n    const commandAutocomplete = await browserPage.Cli.cliCommandAutocomplete.innerText;\n    await t.expect(commandArgsFromCliHelper).contains(commandAutocomplete, 'Command autocomplete arguments not correct');\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/cli/cli.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\nimport { APIKeyRequests } from '../../../../helpers/api/api-keys';\nimport { goBackHistory } from '../../../../helpers/utils';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst browserPage = new BrowserPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\nconst apiKeyRequests = new APIKeyRequests();\n\nlet keyName = Common.generateWord(10);\n\nfixture `CLI`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n    })\n    .afterEach(async() => {\n        // Delete database\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    });\ntest\n    .after(async() => {\n        await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName);\n        await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig);\n    })('Verify that user can add data via CLI', async t => {\n        keyName = Common.generateWord(10);\n        // Open CLI\n        await t.click(browserPage.Cli.cliExpandButton);\n        // Verify that user can expand CLI\n        await t.expect(browserPage.Cli.cliArea.exists).ok('CLI area is not displayed');\n        await t.expect(browserPage.Cli.cliCommandInput.exists).ok('CLI input is not displayed');\n\n        // Add key from CLI\n        await t.typeText(browserPage.Cli.cliCommandInput, `SADD ${keyName} \"chinese\" \"japanese\" \"german\"`, { replace: true, paste: true });\n        await t.pressKey('enter');\n        // Check that the key is added\n        await browserPage.searchByKeyName(keyName);\n        const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName);\n        await t.expect(isKeyIsDisplayedInTheList).ok('The key is not added');\n    });\ntest.skip('Verify that user can use blocking command', async t => {\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Check that CLI is opened\n    await t.expect(browserPage.Cli.cliArea.visible).ok('CLI area is not displayed');\n    // Type blocking command\n    await t.typeText(browserPage.Cli.cliCommandInput, 'blpop newKey 10000', { replace: true, paste: true });\n    await t.pressKey('enter');\n    // Verify that user input is blocked\n    await t.expect(browserPage.Cli.cliCommandInput.exists).notOk('Cli input is still shown');\n\n    // Collaple CLI\n    await t.click(browserPage.Cli.cliCollapseButton);\n    // Verify that user can collapse CLI\n    await t.expect(browserPage.Cli.cliArea.visible).notOk('CLI area should still displayed');\n});\n// update after resolving testcafe Native Automation mode limitations\ntest.skip('Verify that user can use unblocking command', async t => {\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Get clientId\n    await t.typeText(browserPage.Cli.cliCommandInput, 'client id');\n    await t.pressKey('enter');\n    const clientId = (await browserPage.Cli.cliOutputResponseSuccess.textContent).replace(/^\\D+/g, '');\n    // Type blocking command\n    await t.typeText(browserPage.Cli.cliCommandInput, 'blpop newKey 10000', { replace: true, paste: true });\n    await t.pressKey('enter');\n    // Verify that user input is blocked\n    await t.expect(browserPage.Cli.cliCommandInput.exists).notOk('Cli input is still shown');\n    // Create new window to unblock the client\n    //await openRedisHomePage();\n    await t.click(browserPage.NavigationPanel.myRedisDBButton);\n    await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n    // Open CLI\n    await t.click(browserPage.Cli.cliExpandButton);\n    // Unblock client\n    await t.typeText(browserPage.Cli.cliCommandInput, `client unblock ${clientId}`, { replace: true, paste: true });\n    await t.pressKey('enter');\n    await goBackHistory();\n    await t.expect(browserPage.Cli.cliCommandInput.exists).ok('Cli input is not shown, the client still blocked', { timeout: 10000 });\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/database/delete-the-db.e2e.ts",
    "content": "import { Chance } from 'chance';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { rte } from '../../../../helpers/constants';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\n\nconst chance = new Chance();\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst databaseHelper = new DatabaseHelper();\n\nconst uniqueId = chance.string({ length: 10 });\nlet database = {\n    ...ossStandaloneConfig,\n    databaseName: `test_standalone-${uniqueId}`\n};\n\nfixture `Delete database`\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n        database = {\n            ...ossStandaloneConfig,\n            databaseName: `test_standalone-${uniqueId}`\n        };\n    });\ntest\n    .meta({ rte: rte.standalone })\n    .skip('Verify that user can delete databases', async t => {\n        await databaseHelper.addNewStandaloneDatabase(database);\n        await myRedisDatabasePage.deleteDatabaseByName(database.databaseName);\n        await t.expect(myRedisDatabasePage.dbNameList.withExactText(database.databaseName).exists).notOk('The database not deleted', { timeout: 10000 });\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/database/edit-db.e2e.ts",
    "content": "import { ClientFunction } from 'testcafe';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { MyRedisDatabasePage } from '../../../../pageObjects';\nimport {\n    commonUrl,\n    ossStandaloneConfig\n} from '../../../../helpers/conf';\nimport { rte } from '../../../../helpers/constants';\nimport { UserAgreementDialog } from '../../../../pageObjects/dialogs';\n\nconst myRedisDatabasePage = new MyRedisDatabasePage();\nconst userAgreementDialog = new UserAgreementDialog();\nconst databaseHelper = new DatabaseHelper();\n\nfixture `Edit Databases`\n    .meta({ type: 'smoke' })\n    .page(commonUrl)\n    .beforeEach(async() => {\n        await databaseHelper.acceptLicenseTerms();\n    });\n// Returns the URL of the current web page\nconst getPageUrl = ClientFunction(() => window.location.href);\ntest\n    .meta({ rte: rte.standalone })\n    .after(async() => {\n        // Delete database\n        await databaseHelper.deleteDatabase(ossStandaloneConfig.databaseName);\n    })\n    .skip('Verify that user open edit view of database', async t => {\n        await userAgreementDialog.acceptLicenseTerms();\n        await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.addDatabaseButton.exists).ok('The add redis database view not found', { timeout: 10000 });\n        await databaseHelper.addNewStandaloneDatabase(ossStandaloneConfig);\n        await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName);\n        await t.expect(getPageUrl()).contains('browser', 'Browser page not opened');\n    });\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts",
    "content": "import { rte } from '../../../../helpers/constants';\nimport { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport { Common } from '../../../../helpers/common';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nlet indexName = Common.generateWord(10);\n\nfixture `JSON verifications at Workbench`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    })\n    .afterEach(async t => {\n        // Clear and delete database\n        await t.switchToMainWindow();\n        await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`);\n    });\ntest\n    .skip('Verify that user can execute redisearch command for JSON data type in Workbench', async t => {\n    indexName = Common.generateWord(10);\n    const commandsForSend = [\n        `FT.CREATE ${indexName} ON JSON SCHEMA $.title AS title TEXT`,\n        'JSON.SET myDoc $ \\'{\"title\": \"foo\", \"content\": \"bar\"}\\''\n    ];\n    const searchCommand = `FT.SEARCH ${indexName} \"@title:foo\"`;\n\n    // Send commands for add JSON document and create index\n    await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\\n'));\n    // Verify that the commandsForSend are executed\n    for(const command of commandsForSend) {\n        await t.expect((await workbenchPage.getCardContainerByCommand(command)).textContent).contains('OK', `The ${command} command is not executed`);\n    }\n    // Send search command to find JSON document\n    await workbenchPage.sendCommandInWorkbench(searchCommand);\n    // Verify that the search command is executed\n    await t.switchToIframe(workbenchPage.iframe);\n    await t.expect(workbenchPage.queryColumns.nth(1).textContent).contains('{\\\\\"title\\\\\":\\\\\"foo\\\\\",\\\\\"content\\\\\":\\\\\"bar\\\\\"}', `The ${searchCommand} command is not executed`);\n});\n"
  },
  {
    "path": "tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts",
    "content": "import { DatabaseHelper } from '../../../../helpers/database';\nimport { BrowserPage, WorkbenchPage } from '../../../../pageObjects';\nimport { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf';\nimport {  rte } from '../../../../helpers/constants';\nimport { DatabaseAPIRequests } from '../../../../helpers/api/api-database';\n\nconst browserPage = new BrowserPage();\nconst workbenchPage = new WorkbenchPage();\nconst databaseHelper = new DatabaseHelper();\nconst databaseAPIRequests = new DatabaseAPIRequests();\n\nfixture `Scripting area at Workbench`\n    .meta({ type: 'smoke', rte: rte.standalone })\n    .page(commonUrl)\n    .beforeEach(async t => {\n        await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig);\n        // Go to Workbench page\n        await t.click(browserPage.NavigationTabs.workbenchButton);\n    });\ntest.skip('Verify that user can comment out any characters in scripting area and all these characters in this raw number are not send in the request', async t => {\n    const command1 = 'info';\n    const command2 = 'command';\n    const commandForSend = [\n        '// some comment before command',\n        '\\n',\n        command1,\n        '\\n',\n        '// some comment between commands with slashes // ** //',\n        '\\n',\n        `${command2} // comment in the row with command`,\n        '\\n',\n        '// some comment after command'\n    ];\n        // Send command\n    await workbenchPage.sendCommandInWorkbench(commandForSend.join(''));\n    // Check that 2 results are shown\n    await t.expect(workbenchPage.queryCardContainer.count).eql(2);\n    // Check that we have results with sent commands\n    const sentCommandText1 = workbenchPage.queryCardCommand.withExactText(command1);\n    await t.expect(sentCommandText1.exists).ok('Result of sent command not exists');\n    const sentCommandText2 = workbenchPage.queryCardCommand.withExactText(command2);\n    await t.expect(sentCommandText2.exists).ok('Result of sent command not exists');\n});\ntest.skip('Verify that user can run multiple commands in one query in Workbench', async t => {\n    const commandForSend1 = 'info';\n    const commandForSend2 = 'FT._LIST';\n    const multipleCommands = [\n        'info',\n        'command',\n        'FT.SEARCH idx *'\n    ];\n        // Send commands\n    await workbenchPage.sendCommandInWorkbench(commandForSend1);\n    await t.expect(workbenchPage.executionCommandTime.exists).ok('Execution command time is not displayed for single command');\n    await t.expect(workbenchPage.executionCommandTime.exists).ok('Execution time icon is not displayed for single command');\n    await workbenchPage.sendCommandInWorkbench(commandForSend2);\n    // Check that all the previous run commands are saved and displayed\n    await workbenchPage.reloadPage();\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend1).exists).ok('The previous run commands are not saved');\n    await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend2).exists).ok('The previous run commands are not saved');\n    // Send multiple commands in one query\n    await workbenchPage.sendCommandInWorkbench(multipleCommands.join('\\n'), 0.75);\n    // Check that the results for all commands are displayed\n    for(const command of multipleCommands) {\n        await t.expect(workbenchPage.queryCardCommand.withExactText(command).exists).ok(`The command ${command} from multiple query is not displayed`);\n    }\n    // Reload page and validate that time executions are displayed for collapsed commands\n    await workbenchPage.reloadPage();\n    const countCommandsInHistory = await workbenchPage.queryCardCommand.count;\n    const countExecutionTime = await workbenchPage.executionCommandTime.count;\n    const countExecutionIcon = await workbenchPage.executionCommandIcon.count;\n    await t.expect(countExecutionTime).eql(countCommandsInHistory, 'Not correct number of execution time');\n    await t.expect(countExecutionIcon).eql(countCommandsInHistory, 'Not correct number of execution time');\n});\n"
  },
  {
    "path": "tests/e2e/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"CommonJS\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\"]\n  },\n  \"exclude\": [\n    \"./node_modules\",\n    \"**/node_modules\"\n  ]\n}\n"
  },
  {
    "path": "tests/e2e/tsconfig.testcafe.json",
    "content": "{\n    \"extends\": \"./tsconfig.json\",\n    \"compilerOptions\": {\n        \"types\": []\n  }\n}\n"
  },
  {
    "path": "tests/e2e/upload-custom-plugins.sh",
    "content": "#!/usr/bin/env bash\n\ncurl --request GET -sL \\\n     --url 'https://s3.amazonaws.com/redisinsight.test/public/custom_plugins/plugins.zip'\\\n     --output './plugins.zip'\n\necho \"Custom plugins archive was downloaded\"\n\nmkdir -p .redis-insight\nunzip -o plugins.zip -d ./.redis-insight/plugins\n\necho \"Custom plugins were unarchived\"\n\nexec \"$@\"\n"
  },
  {
    "path": "tests/e2e/vpn.docker-compose.yml",
    "content": "version: \"3.4\"\n\nservices:\n  # openvpn server to reach private network from the host\n  openvpn:\n    extends:\n      file: ./rte/openvpn/docker-compose.yml\n      service: openvpn\n    networks:\n      default:\n        ipv4_address: 172.31.100.247\n"
  },
  {
    "path": "tests/e2e/wait-for-it.sh",
    "content": "#!/usr/bin/env bash\n# Use this script to test if a given TCP host/port are available\n\nWAITFORIT_cmdname=${0##*/}\n\nechoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo \"$@\" 1>&2; fi }\n\nusage()\n{\n    cat << USAGE >&2\nUsage:\n    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]\n    -h HOST | --host=HOST       Host or IP under test\n    -p PORT | --port=PORT       TCP port under test\n                                Alternatively, you specify the host and port as host:port\n    -s | --strict               Only execute subcommand if the test succeeds\n    -q | --quiet                Don't output any status messages\n    -t TIMEOUT | --timeout=TIMEOUT\n                                Timeout in seconds, zero for no timeout\n    -- COMMAND ARGS             Execute command with args after the test finishes\nUSAGE\n    exit 1\n}\n\nwait_for()\n{\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    else\n        echoerr \"$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout\"\n    fi\n    WAITFORIT_start_ts=$(date +%s)\n    while :\n    do\n        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then\n            nc -z $WAITFORIT_HOST $WAITFORIT_PORT\n            WAITFORIT_result=$?\n        else\n            (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1\n            WAITFORIT_result=$?\n        fi\n        if [[ $WAITFORIT_result -eq 0 ]]; then\n            WAITFORIT_end_ts=$(date +%s)\n            echoerr \"$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds\"\n            break\n        fi\n        sleep 60\n    done\n    return $WAITFORIT_result\n}\n\nwait_for_wrapper()\n{\n    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692\n    if [[ $WAITFORIT_QUIET -eq 1 ]]; then\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    else\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    fi\n    WAITFORIT_PID=$!\n    trap \"kill -INT -$WAITFORIT_PID\" INT\n    wait $WAITFORIT_PID\n    WAITFORIT_RESULT=$?\n    if [[ $WAITFORIT_RESULT -ne 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    fi\n    return $WAITFORIT_RESULT\n}\n\n# process arguments\nwhile [[ $# -gt 0 ]]\ndo\n    case \"$1\" in\n        *:* )\n        WAITFORIT_hostport=(${1//:/ })\n        WAITFORIT_HOST=${WAITFORIT_hostport[0]}\n        WAITFORIT_PORT=${WAITFORIT_hostport[1]}\n        shift 1\n        ;;\n        --child)\n        WAITFORIT_CHILD=1\n        shift 1\n        ;;\n        -q | --quiet)\n        WAITFORIT_QUIET=1\n        shift 1\n        ;;\n        -s | --strict)\n        WAITFORIT_STRICT=1\n        shift 1\n        ;;\n        -h)\n        WAITFORIT_HOST=\"$2\"\n        if [[ $WAITFORIT_HOST == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --host=*)\n        WAITFORIT_HOST=\"${1#*=}\"\n        shift 1\n        ;;\n        -p)\n        WAITFORIT_PORT=\"$2\"\n        if [[ $WAITFORIT_PORT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --port=*)\n        WAITFORIT_PORT=\"${1#*=}\"\n        shift 1\n        ;;\n        -t)\n        WAITFORIT_TIMEOUT=\"$2\"\n        if [[ $WAITFORIT_TIMEOUT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --timeout=*)\n        WAITFORIT_TIMEOUT=\"${1#*=}\"\n        shift 1\n        ;;\n        --)\n        shift\n        WAITFORIT_CLI=(\"$@\")\n        break\n        ;;\n        --help)\n        usage\n        ;;\n        *)\n        echoerr \"Unknown argument: $1\"\n        usage\n        ;;\n    esac\ndone\n\nif [[ \"$WAITFORIT_HOST\" == \"\" || \"$WAITFORIT_PORT\" == \"\" ]]; then\n    echoerr \"Error: you need to provide a host and port to test.\"\n    usage\nfi\n\nWAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}\nWAITFORIT_STRICT=${WAITFORIT_STRICT:-0}\nWAITFORIT_CHILD=${WAITFORIT_CHILD:-0}\nWAITFORIT_QUIET=${WAITFORIT_QUIET:-0}\n\n# Check to see if timeout is from busybox?\nWAITFORIT_TIMEOUT_PATH=$(type -p timeout)\nWAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)\n\nWAITFORIT_BUSYTIMEFLAG=\"\"\nif [[ $WAITFORIT_TIMEOUT_PATH =~ \"busybox\" ]]; then\n    WAITFORIT_ISBUSY=1\n    # Check if busybox timeout uses -t flag\n    # (recent Alpine versions don't support -t anymore)\n    if timeout &>/dev/stdout | grep -q -e '-t '; then\n        WAITFORIT_BUSYTIMEFLAG=\"-t\"\n    fi\nelse\n    WAITFORIT_ISBUSY=0\nfi\n\nif [[ $WAITFORIT_CHILD -gt 0 ]]; then\n    wait_for\n    WAITFORIT_RESULT=$?\n    exit $WAITFORIT_RESULT\nelse\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        wait_for_wrapper\n        WAITFORIT_RESULT=$?\n    else\n        wait_for\n        WAITFORIT_RESULT=$?\n    fi\nfi\n\nif [[ $WAITFORIT_CLI != \"\" ]]; then\n    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then\n        echoerr \"$WAITFORIT_cmdname: strict mode, refusing to execute subprocess\"\n        exit $WAITFORIT_RESULT\n    fi\n    exec \"${WAITFORIT_CLI[@]}\"\nelse\n    exit $WAITFORIT_RESULT\nfi\n"
  },
  {
    "path": "tests/e2e/wait-for-redis.sh",
    "content": "#!/bin/sh\n\nHOST=${1}\nPORT=${2}\nTIMEOUT=${TIMEOUT:-120}\n\necho \"Waiting for redis on $HOST:$PORT... \"\nwhile [ $TIMEOUT -gt 0 ]; do\n    INFO=$(echo -e \"*1\\r\\n\\$4\\r\\INFO\\r\\n\" | nc $HOST $PORT | head -1)\n    TIMEOUT=$((TIMEOUT - 1))\n\n    if [ \"$INFO\" ]; then\n      echo \"Redis is available on $HOST:$PORT\"\n      exit 0;\n    fi\n\n    sleep 30\n    echo \"Waiting... (left: $TIMEOUT)\"\ndone\n\necho \"Unable to establish connection to $HOST:$PORT\"\nexit 1;\n"
  },
  {
    "path": "tests/e2e/web.runner.ci.ts",
    "content": "import testcafe from 'testcafe';\n\n(async(): Promise<void> => {\n    await testcafe()\n        .then(t => {\n            return t\n                .createRunner()\n                .compilerOptions({\n                    'typescript': {\n                        configPath: 'tsconfig.testcafe.json',\n                        experimentalDecorators: true\n                    } })\n                .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\\n'))\n                .browsers(['chromium:headless --disable-search-engine-choice-screen --ignore-certificate-errors --disable-dev-shm-usage --no-sandbox --disable-gpu --retry-test-pages'])\n                .screenshots({\n                    path: 'report/screenshots/',\n                    takeOnFails: true,\n                    pathPattern: '${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png'\n                })\n                .reporter([\n                    'spec',\n                    {\n                        name: 'xunit',\n                        output: './results/results.xml'\n                    },\n                    {\n                        name: 'json',\n                        output: './results/e2e.results.json'\n                    },\n                    {\n                        name: 'html',\n                        output: './report/index.html'\n                    }\n                ])\n                .run({\n                    skipJsErrors: true,\n                    browserInitTimeout: 240000,\n                    selectorTimeout: 5000,\n                    assertionTimeout: 5000,\n                    speed: 1,\n                    quarantineMode: { successThreshold: 1, attemptLimit: 3 },\n                    pageRequestTimeout: 8000,\n                    disableMultipleWindows: true\n                });\n        })\n        .then((failedCount) => {\n            process.exit(failedCount);\n        })\n        .catch((e) => {\n            console.error(e);\n            process.exit(1);\n        });\n})();\n"
  },
  {
    "path": "tests/e2e/web.runner.ts",
    "content": "import testcafe from 'testcafe';\n\n(async(): Promise<void> => {\n    await testcafe()\n        .then(t => {\n            return t\n                .createRunner()\n                .compilerOptions({\n                    'typescript': {\n                        configPath: 'tsconfig.testcafe.json',\n                        experimentalDecorators: true\n                    } })\n                .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\\n'))\n                .browsers([`${process.env.TEST_BROWSER || 'chromium'} --disable-search-engine-choice-screen --ignore-certificate-errors --disable-dev-shm-usage --no-sandbox`])\n                .screenshots({\n                    path: 'report/screenshots/',\n                    takeOnFails: true,\n                    pathPattern: '${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png'\n                })\n                .reporter([\n                    'spec',\n                    {\n                        name: 'xunit',\n                        output: './results/results.xml'\n                    },\n                    {\n                        name: 'json',\n                        output: './results/e2e.results.json'\n                    },\n                    {\n                        name: 'html',\n                        output: './report/index.html'\n                    }\n                ])\n                .run({\n                    skipJsErrors: true,\n                    browserInitTimeout: 120000,\n                    selectorTimeout: 15000,\n                    assertionTimeout: 15000,\n                    speed: 1,\n                    quarantineMode: { successThreshold: 1, attemptLimit: 3 },\n                    pageRequestTimeout: 20000,\n                    disableMultipleWindows: true,\n                    pageLoadTimeout: 30000\n                });\n        })\n        .then((failedCount) => {\n            process.exit(failedCount);\n        })\n        .catch((e) => {\n            console.error(e);\n            process.exit(1);\n        });\n})();\n"
  },
  {
    "path": "tests/e2e-playwright/.eslintrc.json",
    "content": "{\n  \"root\": true,\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaVersion\": 2022,\n    \"sourceType\": \"module\",\n    \"project\": \"./tsconfig.json\"\n  },\n  \"plugins\": [\"@typescript-eslint\"],\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\"\n  ],\n  \"rules\": {\n    \"@typescript-eslint/no-explicit-any\": \"warn\",\n    \"@typescript-eslint/no-unused-vars\": [\"error\", { \"argsIgnorePattern\": \"^_\" }]\n  }\n}\n\n"
  },
  {
    "path": "tests/e2e-playwright/.gitignore",
    "content": "node_modules/\ntest-results/\nplaywright-report/\nplaywright/.cache/\n.env\n*.log\n.eslintcache\n\n"
  },
  {
    "path": "tests/e2e-playwright/.husky/pre-commit",
    "content": "#!/usr/bin/env sh\n\n# Get the directory where this hook is located\nHOOK_DIR=\"$(dirname \"$0\")\"\nE2E_DIR=\"$(dirname \"$HOOK_DIR\")\"\n\n# Check if staged files are in tests/e2e-playwright directory\nSTAGED_E2E_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep \"^tests/e2e-playwright/\" | grep \"\\.ts$\" || true)\n\nif [ -n \"$STAGED_E2E_FILES\" ]; then\n  echo \"🔍 Running E2E tests pre-commit checks...\"\n\n  # Change to e2e-playwright directory\n  cd \"$E2E_DIR\"\n\n  # Run TypeScript check\n  echo \"📝 Running TypeScript check...\"\n  npm run type-check\n  if [ $? -ne 0 ]; then\n    echo \"❌ TypeScript check failed. Please fix the errors before committing.\"\n    exit 1\n  fi\n  echo \"✅ TypeScript check passed\"\n\n  # Run lint-staged for linting\n  echo \"🧹 Running ESLint on staged files...\"\n  npx lint-staged\n  if [ $? -ne 0 ]; then\n    echo \"❌ ESLint check failed. Please fix the errors before committing.\"\n    exit 1\n  fi\n  echo \"✅ ESLint check passed\"\n\n  echo \"✅ All E2E pre-commit checks passed!\"\nfi\n\n"
  },
  {
    "path": "tests/e2e-playwright/.prettierrc",
    "content": "{\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 120\n}\n\n"
  },
  {
    "path": "tests/e2e-playwright/README.md",
    "content": "# RedisInsight E2E Tests v2\n\nStandalone Playwright E2E test suite for RedisInsight.\n\n## Documentation\n\n| Document | Purpose |\n|----------|---------|\n| [`TEST_PLAN.md`](./TEST_PLAN.md) | Test coverage status and priorities |\n| [`.ai/rules/e2e-testing.md`](../../.ai/rules/e2e-testing.md) | Standards and patterns for writing tests |\n\n### AI Commands\n\nUse these commands with Augment AI to generate and fix tests:\n\n| Command | Description |\n|---------|-------------|\n| `@e2e-generate <url> [focus]` | Explore UI with Playwright MCP and generate tests |\n| `@e2e-fix <test-pattern>` | Run tests and fix failures |\n\nExample:\n```\n@e2e-generate http://localhost:8080/browser \"add key\"\n@e2e-fix \"Analytics > Slow Log\"\n```\n\n## Prerequisites\n\n### Common Setup (All Projects)\n\nStart Redis Test Environment (RTE) using Docker Compose:\n```bash\ncd tests/e2e\ndocker-compose -f rte.docker-compose.yml up -d\n```\n\n### Project-Specific Setup\n\n| Project | Setup Command | Run Tests |\n|---------|---------------|-----------|\n| **Chromium** | `yarn dev:api` + `yarn dev:ui` (two terminals) | `npm run test:chromium` |\n| **Electron** | `yarn package:prod` | `npm run test:electron` |\n\n> **Note:** Setup commands run from the repository root. Test commands run from `tests/e2e-playwright/`.\n\n## Installation\n\n```bash\ncd tests/e2e-playwright\nnpm install\nnpx playwright install chromium\n```\n\n## Configuration\n\nCopy `example.env` to `.env` and update values for your environment:\n```bash\ncp example.env .env\n```\n\nEnvironment variables:\n- `RI_CLIENT_URL` - RedisInsight UI URL (default: `http://localhost:8080`)\n- `RI_API_URL` - RedisInsight API URL (default: `http://localhost:5540`)\n- `OSS_STANDALONE_*` - Standalone Redis connection details\n- `OSS_CLUSTER_*` - Cluster Redis connection details\n- `OSS_SENTINEL_*` - Sentinel Redis connection details\n\n## Running Tests\n\n### Commands\n\n| Command | Description |\n|---------|-------------|\n| `npm test` | Run all tests (all projects) |\n| `npm run test:chromium` | Run Chromium browser tests |\n| `npm run test:chromium:headed` | Run with visible browser window |\n| `npm run test:chromium:debug` | Run with Playwright Inspector (pause & step through) |\n| `npm run test:chromium:ui` | Open interactive UI dashboard |\n| `npm run test:electron` | Run Electron desktop tests |\n| `npm run test:electron:headed` | Run with visible Electron window |\n| `npm run test:electron:debug` | Run with Playwright Inspector |\n| `npm run test:report` | View HTML test report |\n| `npm run test:codegen` | Record actions and generate test code |\n\n### Chromium Browser Tests\n\n```bash\nnpm test                      # Run all tests\nnpm run test:chromium         # Run Chromium project only\nnpm run test:chromium:headed  # Watch tests run\nnpm run test:chromium:debug   # Pause and step through tests\nnpm run test:chromium:ui      # Interactive test runner\n```\n\n### Electron Desktop Tests\n\n```bash\nnpm run test:electron         # Run all Electron tests\nnpm run test:electron:headed  # Watch the app\nnpm run test:electron:debug   # Debug with Inspector\n```\n\n#### Custom Executable Path\n\nIf you need to override the default path (e.g., for a custom build location):\n\n```bash\nELECTRON_EXECUTABLE_PATH=\"/path/to/your/app\" npm run test:electron\n```\n\nDefault paths by platform:\n| Platform | Default Path |\n|----------|--------------|\n| macOS arm64 | `release/mac-arm64/Redis Insight.app/Contents/MacOS/Redis Insight` |\n| macOS x64 | `release/mac-x64/Redis Insight.app/Contents/MacOS/Redis Insight` |\n| Linux | `release/linux-unpacked/redisinsight` |\n| Windows | `release/win-unpacked/Redis Insight.exe` |\n\n#### Electron Test Considerations\n\n- **Single worker**: Electron tests run with 1 worker (sequential execution)\n- **Longer timeouts**: Electron tests have 120s timeout (vs 60s for browser)\n- **UI-based navigation**: All navigation uses UI clicks (works for both browser and Electron)\n- **Same test files**: Browser and Electron tests use the same test files\n\n#### Navigation Methods\n\nAll navigation is UI-based for consistency across platforms.\n\n**BasePage** provides fundamental navigation:\n```typescript\nawait this.gotoHome();              // Click Redis logo → databases list\nawait this.gotoDatabase(dbId);      // Click database → Browser page (default)\n```\n\n**Each page has its own `goto()` method** that handles navigation + waiting:\n```typescript\n// Navigate to specific pages\nawait settingsPage.goto();           // Settings page\nawait browserPage.goto(dbId);        // Browser page for database\nawait workbenchPage.goto(dbId);      // Workbench page for database\nawait analyticsPage.goto(dbId);      // Analytics page for database\nawait pubSubPage.goto(dbId);         // Pub/Sub page for database\n```\n\n**InstancePage** provides tab switching within a connected database:\n```typescript\n// Switch tabs (when already connected to a database)\nawait browserPage.navigationTabs.gotoBrowser();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.navigationTabs.gotoAnalyze();\nawait browserPage.navigationTabs.gotoPubSub();\n```\n\n## Multi-Environment Support\n\nThe framework supports multiple environments via the `ENV` variable:\n\n```bash\n# Local (default) - uses .env\nnpm test\n\n# CI - uses .env.ci\nENV=ci npm test\n\n# Staging - uses .env.staging\nENV=staging npm test\n```\n\nCreate environment-specific `.env.{name}` files for different environments.\n\n## Folder Structure\n\n```\ntests/e2e-playwright/\n├── config/            # Environment configuration\n├── fixtures/          # Test fixtures (page objects, API helpers)\n├── helpers/           # Utility functions\n├── pages/             # Page Object Models (component-based)\n│   └── databases/\n│       ├── DatabasesPage.ts\n│       └── components/\n│           ├── AddDatabaseDialog.ts\n│           └── DatabaseList.ts\n├── test-data/         # Test data factories\n├── tests/             # Test files organized by project\n│   ├── main/          # Main parallel tests (default)\n│   │   ├── browser/\n│   │   ├── workbench/\n│   │   └── databases/\n│   ├── auto-update/   # Auto-update tests (serial, special setup)\n│   └── electron/      # Electron-specific tests\n├── types/             # TypeScript type definitions\n├── setup/             # Global setup/teardown per project\n│   ├── browser.setup.ts\n│   ├── browser.teardown.ts\n│   ├── electron.setup.ts\n│   └── electron.teardown.ts\n└── playwright.config.ts\n```\n\n### Page Object Structure\n\nPage objects are organized into component-based POMs for better maintainability:\n\n```\nBasePage (abstract)\n  ├── DatabasesPage           # Databases list page\n  ├── SettingsPage            # Settings page\n  └── InstancePage (abstract) # Base for all database instance pages\n        ├── instanceHeader    # Database name, stats, breadcrumb\n        ├── navigationTabs    # Browse, Workbench, Analyze, Pub/Sub\n        ├── bottomPanel       # CLI, Command Helper, Profiler\n        └── BrowserPage       # Browser-specific (extends InstancePage)\n              └── WorkbenchPage (future)\n              └── AnalyzePage (future)\n              └── PubSubPage (future)\n```\n\n- **BasePage** - Common navigation methods for all pages\n- **InstancePage** - Base class for pages within a connected database (provides shared header, tabs, bottom panel)\n- **Component POMs** (`AddDatabaseDialog`, `KeyList`) - Reusable UI components\n\n```typescript\n// Access component POMs through the page\nawait databasesPage.addDatabaseDialog.fillForm(config);\nawait browserPage.keyList.selectKey(keyName);\n\n// InstancePage provides common components\nawait browserPage.instanceHeader.getDatabaseName();\nawait browserPage.navigationTabs.gotoWorkbench();\nawait browserPage.bottomPanel.openCli();\n```\n\n### Test Structure\n\nTests are organized into **projects** (folders) based on execution requirements, then by feature:\n\n```\ntests/\n├── main/                   # Default parallel tests\n│   ├── browser/\n│   │   ├── add-key/\n│   │   └── key-details/\n│   ├── databases/\n│   │   ├── add-database/\n│   │   └── edit-database/\n│   └── workbench/\n├── auto-update/            # Serial tests with special setup\n│   └── update-flow.spec.ts\n└── electron/               # Electron-only features\n    └── deep-links.spec.ts\n```\n\n### Playwright Projects\n\nTests are organized into **projects** that can have different configurations:\n\n| Project | Folder | Parallelism | Use Case |\n|---------|--------|-------------|----------|\n| `chromium` | `tests/main/` | Parallel | Standard tests in Chromium browser |\n| `electron` | `tests/main/` | Serial | Same tests in Electron desktop app |\n| `auto-update` | `tests/auto-update/` | Serial | Tests requiring special setup or causing flakiness |\n\nRun specific projects:\n```bash\nnpx playwright test --project=chromium       # Chromium browser tests\nnpx playwright test --project=electron       # Electron desktop tests\nnpx playwright test --project=auto-update    # Auto-update tests\nnpx playwright test                           # All projects\n```\n\n**When to create a new project:**\n- Tests require different parallelism settings (serial vs parallel)\n- Tests need different global setup/teardown\n- Tests would cause flakiness when run with other tests\n- Tests require special environment configuration\n\n## Writing Tests\n\nTests are organized by feature area. Each test file should:\n- Use descriptive test names\n- Follow AAA pattern (Arrange, Act, Assert)\n- Use Page Object Models for UI interactions\n- Use faker for test data generation\n- Use test data factories from `test-data/`\n- Clean up created data in `afterEach` via API (faster and more reliable)\n\nExample:\n```typescript\nimport { test, expect } from '../../../fixtures/base';\nimport { getStandaloneConfig } from '../../../test-data/databases';\n\ntest.describe('Add Database > Standalone', () => {\n  test.afterEach(async ({ apiHelper }) => {\n    // Clean up all test databases via API (fast)\n    await apiHelper.deleteTestDatabases();\n  });\n\n  test('should add standalone database', async ({ databasesPage }) => {\n    const config = getStandaloneConfig();\n\n    await databasesPage.goto();\n    await databasesPage.addDatabase(config);\n\n    await expect(databasesPage.databaseList.getRow(config.name)).toBeVisible();\n  });\n});\n```\n\n## API Helper\n\nUse the `apiHelper` fixture for test setup/teardown via API (faster than UI):\n\n```typescript\ntest('should work with pre-created database', async ({ databasesPage, apiHelper }) => {\n  // Create database via API (fast)\n  const db = await apiHelper.createDatabase(getStandaloneConfig());\n\n  // Test UI behavior\n  await databasesPage.goto();\n  await expect(databasesPage.getDatabaseRow(db.name)).toBeVisible();\n\n  // Cleanup via API\n  await apiHelper.deleteDatabase(db.id);\n});\n```\n\n"
  },
  {
    "path": "tests/e2e-playwright/TEST_PLAN.md",
    "content": "# RedisInsight E2E Test Plan\n\nThis document outlines the comprehensive E2E testing strategy for RedisInsight features.\n\n> **📋 Rules**: Before implementing tests, read [`.ai/rules/e2e-testing.md`](../../.ai/rules/e2e-testing.md) for coding standards, patterns, and best practices.\n\n## Overview\n\nThe test plan is organized by feature area. Tests are grouped for parallel execution:\n- **main** - Default group for all tests that can run in parallel\n- Additional groups can be added for tests requiring special conditions (app reinstall, auto-update, etc.)\n\n## Test Status Legend\n\n- ✅ Implemented\n- 🔲 Not implemented\n- ⏳ In progress\n- ⏸️ Skipped\n\n---\n\n## 0. Navigation & Global UI\n\n### 0.1 Main Navigation\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Navigate to Settings page |\n| ✅ | main | Navigate to home via Redis logo |\n| ✅ | main | Show GitHub repo link |\n| ✅ | main | Show Redis Cloud link |\n\n### 0.2 Help Menu\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should open Help Center and display all menu options |\n| ✅ | main | should have Release Notes link pointing to GitHub releases |\n| ✅ | main | should have Provide Feedback link pointing to GitHub issues |\n\n### 0.3 Notification Center\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should open Notification Center and display notifications |\n| ✅ | main | should close Notification Center |\n| ✅ | main | should display notification links that are clickable |\n| ✅ | main | should show unread badge when there are unread notifications |\n\n### 0.4 Copilot Panel\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Open Copilot panel |\n| 🔲 | main | Close Copilot panel |\n| 🔲 | main | Open full screen mode |\n| 🔲 | main | View sign-in options (Google, GitHub, SSO) |\n| 🔲 | main | Accept terms checkbox |\n\n### 0.5 Insights Panel ✅\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Open Insights panel |\n| ✅ | main | Close Insights panel |\n| ✅ | main | Switch to Tutorials tab |\n| ✅ | main | Switch to Tips tab |\n| ✅ | main | Expand/collapse tutorial folders |\n| ✅ | main | View Tutorials section |\n| ✅ | main | Run through a tutorial with pagination |\n| ✅ | main | Run a tutorial command |\n| 🔲 | main | Upload custom tutorial |\n| 🔲 | main | Delete custom tutorial |\n| 🔲 | main | View Tips when no recommendations |\n| 🔲 | main | Vote on a tip (like/dislike) |\n| 🔲 | main | Show/hide hidden tips |\n| 🔲 | main | Navigate to database analysis from Tips |\n\n---\n\n## 1. Database Management\n\n### 1.1 Add Database\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should add standalone database |\n| ✅ | main | should add database with no auth |\n| ✅ | main | should add database with username only |\n| ✅ | main | should add database with username and password |\n| ✅ | main | should add cluster database |\n| ✅ | main | should add database with TLS/SSL |\n| ✅ | main | should validate required fields |\n| ✅ | main | should test connection before saving |\n| ✅ | main | should cancel add database |\n| ✅ | main | should add database via Connection URL |\n| ✅ | main | should open Connection settings from URL form |\n| ✅ | main | should configure timeout setting |\n| ✅ | main | should select logical database |\n| ✅ | main | should display logical database index in database list |\n| ✅ | main | should display logical database index in database header |\n| ✅ | main | should display logical database index in edit form |\n| ✅ | main | should enable force standalone connection |\n| ✅ | main | should enable automatic data decompression |\n| ✅ | main | should configure key name format |\n\n### 1.1.1 Connection Security\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Add database using SSH tunneling |\n| ✅ | main | Connect using SNI configuration |\n| ✅ | main | Connect with TLS using CA, client, and private key certificates |\n\n### 1.1.2 Add Database (Advanced)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | should add database with SSH tunnel |\n| 🔲 | main | should add database via Redis Sentinel option |\n| 🔲 | main | should add database via Redis Software option |\n| 🔲 | main | should auto-discover databases from Redis Software |\n| 🔲 | main | should auto-discover Redis Cloud databases after signing in |\n| 🔲 | main | should add databases using Cloud API keys |\n\n### 1.2 Database List\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Filter databases by search query |\n| ✅ | main | Filter with partial match |\n| ✅ | main | Case-insensitive search |\n| ✅ | main | Filter by host:port |\n| ✅ | main | Clear search |\n| ✅ | main | No results message |\n| ✅ | main | Show columns button |\n| ✅ | main | Hide/show columns |\n| ✅ | main | Select single database |\n| ✅ | main | Select multiple databases |\n| ✅ | main | Select all databases |\n| ✅ | main | Delete multiple databases |\n| ✅ | main | Edit database connection |\n| ✅ | main | Clone database connection |\n| ✅ | main | Connect to database |\n| 🔲 | main | Database connection status indicator |\n| ✅ | main | Search by database name |\n| ✅ | main | Search by host |\n| ✅ | main | Search by port |\n| 🔲 | main | Search by connection type (OSS Cluster, Sentinel) |\n| 🔲 | main | Search by last connection time |\n| ✅ | main | Verify Redis Stack icon displayed for databases with modules |\n\n### 1.3 Clone Database\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Clone standalone database with pre-populated form |\n| ✅ | main | Clone database with same name |\n| ✅ | main | Clone database with new name |\n| ✅ | main | Cancel clone operation |\n| ✅ | main | Go back to edit dialog from clone dialog |\n| ✅ | main | Clone OSS Cluster database |\n| 🔲 | main | Clone Sentinel database |\n| ✅ | main | Verify \"New Connection\" badge on cloned database |\n| ✅ | main | Verify cloned database appears in list after creation |\n\n### 1.4 Pagination (when > 15 databases)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Show pagination when > 15 databases |\n| ✅ | main | Navigate to next page |\n| ✅ | main | Navigate to previous page |\n| ✅ | main | Navigate to first/last page |\n| ✅ | main | Change items per page (10, 25, 50, 100) |\n| ✅ | main | Select page from dropdown |\n| ✅ | main | Show correct row count \"Showing X out of Y rows\" |\n| ✅ | main | Pagination buttons disabled state (first/previous on page 1) |\n\n### 1.5 Import/Export\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Open import dialog |\n| ✅ | main | Import single database |\n| ✅ | main | Import multiple databases |\n| ✅ | main | Show success count after import |\n| ✅ | main | Cancel import dialog |\n| ✅ | main | Export databases |\n| ✅ | main | Import with errors (partial success) |\n| ✅ | main | Import invalid file format |\n| ✅ | main | Confirm database tags are exported/imported correctly |\n| 🔲 | main | Confirm import summary distinguishes Fully/Partially Imported and Failed |\n\n### 1.6 Database Tags\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should open tags dialog for a database |\n| ✅ | main | should add descriptive tags to a database |\n| ✅ | main | should remove tags from a database |\n| ✅ | main | should cancel adding a tag without saving |\n| ✅ | main | should persist tags after saving and reopening |\n| 🔲 | main | Import tags automatically from Redis Cloud databases |\n\n### 1.7 Certificate and Encryption Handling\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Store credentials encrypted in local keychain when encryption enabled |\n| ✅ | main | Display warning when encryption disabled and credentials stored as plaintext |\n\n### 1.8 Decompression\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Confirm setting a decompression type works |\n\n---\n\n## 2. Browser Page\n\n### 2.1 Key List View\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View key list |\n| 🔲 | main | Search/filter keys by pattern |\n| 🔲 | main | Filter by key type |\n| 🔲 | main | Filter keys by exact name |\n| 🔲 | main | Clear search filter |\n| 🔲 | main | Click on key to view details |\n| 🔲 | main | Refresh key list |\n| 🔲 | main | Show no results message for non-matching pattern |\n| 🔲 | main | Delete key |\n| 🔲 | main | Delete multiple keys (bulk) |\n| 🔲 | main | Search by Values of Keys |\n| 🔲 | main | Configure columns visibility |\n| 🔲 | main | Configure auto-refresh |\n| 🔲 | main | View database stats (CPU, Keys, Memory, Clients) |\n\n### 2.2 Key Tree View\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Switch to tree view |\n| 🔲 | main | Expand/collapse tree nodes |\n| 🔲 | main | Configure delimiter |\n| 🔲 | main | Sort tree nodes |\n| 🔲 | main | View folder percentage and count |\n| 🔲 | main | Scan more keys (covered by \"should show scan more button when searching\" test) |\n| 🔲 | main | Open tree view settings |\n| 🔲 | main | Tree view mode state persists after page refresh |\n| 🔲 | main | Filter state preserved when switching between Browser and Tree view |\n| 🔲 | main | Key type filter state preserved when switching views |\n| 🔲 | main | Configure multiple delimiters in tree view |\n| 🔲 | main | Cancel delimiter change reverts to previous value |\n| 🔲 | main | Verify namespace tooltip shows key pattern and delimiter |\n| 🔲 | main | Scan DB by 10K keys in tree view |\n\n### 2.3 Add Keys\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Add String key |\n| ✅ | main | Add Hash key |\n| ✅ | main | Add List key |\n| ✅ | main | Add Set key |\n| ✅ | main | Add Sorted Set (ZSet) key |\n| ✅ | main | Add Stream key |\n| ✅ | main | Add JSON key |\n| ✅ | main | Add key with TTL |\n| ✅ | main | Validate key name (required) |\n| ✅ | main | Cancel add key dialog |\n\n### 2.4 Key Details - String\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View string value |\n| 🔲 | main | Edit string value |\n| 🔲 | main | View/edit TTL |\n| 🔲 | main | Copy key name (covered by \"should show copy key name button on hover\" test) |\n| 🔲 | main | Change value format (text/binary/hex) |\n| 🔲 | main | Rename key and confirm new name propagates across Browser |\n| 🔲 | main | Confirm TTL countdown updates in real time |\n\n### 2.5 Key Details - Hash\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View hash fields |\n| 🔲 | main | Add hash field |\n| 🔲 | main | Edit hash field |\n| 🔲 | main | Delete hash field |\n| 🔲 | main | Search hash fields |\n| 🔲 | main | Pagination (N/A - hash fields use virtual scrolling, not pagination) |\n\n### 2.6 Key Details - List\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View list elements |\n| 🔲 | main | Add element (LPUSH/RPUSH) |\n| 🔲 | main | Edit list element |\n| 🔲 | main | Remove element |\n| 🔲 | main | Search by index |\n\n### 2.7 Key Details - Set\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View set members |\n| 🔲 | main | Add member |\n| 🔲 | main | Remove member |\n| 🔲 | main | Search members |\n\n### 2.8 Key Details - Sorted Set\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View sorted set members |\n| 🔲 | main | Add member with score |\n| 🔲 | main | Edit member score |\n| 🔲 | main | Remove member |\n| 🔲 | main | Search members |\n| 🔲 | main | Sort by score/member |\n\n### 2.9 Key Details - Stream\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View stream entries |\n| 🔲 | main | Add stream entry |\n| 🔲 | main | Remove stream entry |\n| 🔲 | main | View consumer groups (covered by \"should show no consumer groups message\" test) |\n| 🔲 | main | Add consumer group |\n| 🔲 | main | View consumers (N/A - requires active consumers which need external client) |\n\n### 2.9.1 Stream Consumer Groups\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Create consumer group with Entry ID \"0\" (from beginning) |\n| 🔲 | main | Create consumer group with Entry ID \"$\" (new messages only) |\n| 🔲 | main | Create consumer group with custom Entry ID |\n| 🔲 | main | View consumer group columns (Group Name, Consumers, Pending, Last Delivered ID) - covered by \"should open Consumer Groups tab\" test |\n| 🔲 | main | View consumer information columns (Consumer Name, Pending, Idle Time) |\n| 🔲 | main | Delete consumer from consumer group |\n| 🔲 | main | Delete consumer group |\n| 🔲 | main | Edit Last Delivered ID for consumer group |\n| 🔲 | main | Cancel creating consumer group |\n\n### 2.9.2 Stream Pending Messages\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View pending messages for consumer |\n| 🔲 | main | Acknowledge pending message |\n| 🔲 | main | Claim pending message |\n| 🔲 | main | Claim pending message with idle time parameter |\n| 🔲 | main | Force claim pending message |\n\n### 2.10 Key Details - JSON\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View JSON value |\n| 🔲 | main | Edit JSON value |\n| 🔲 | main | Add JSON path (covered by \"should add JSON field\" test) |\n| 🔲 | main | Delete JSON path (covered by \"should remove JSON field\" test) |\n| 🔲 | main | Expand/collapse JSON tree (N/A - JSON tree view not available in current UI) |\n\n### 2.11 Bulk Actions\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Open Bulk Actions panel |\n| 🔲 | main | Show Delete Keys tab by default |\n| 🔲 | main | Switch to Upload Data tab |\n| 🔲 | main | Close Bulk Actions panel |\n| 🔲 | main | Show message when no pattern set |\n| 🔲 | main | Filter by pattern for deletion |\n| 🔲 | main | Show file upload area |\n| 🔲 | main | Bulk delete keys |\n| 🔲 | main | Bulk delete with pattern |\n| 🔲 | main | Bulk upload data |\n| 🔲 | main | View bulk action progress (expected key count before deletion) |\n| 🔲 | main | Confirm summary screen displays processed, deleted, failed counts |\n| 🔲 | main | Confirm deletion failures surfaced in summary log |\n| 🔲 | main | Confirm performance when deleting thousands of keys |\n| 🔲 | main | Confirm performance when bulk uploading large datasets (>10K keys) |\n\n### 2.12 Value Formatters\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Display format dropdown |\n| 🔲 | main | Switch to ASCII format |\n| 🔲 | main | Switch to HEX format |\n| 🔲 | main | Switch to Binary format |\n| 🔲 | main | Switch to JSON format |\n| 🔲 | main | Show all format options in dropdown |\n| 🔲 | main | View value in Msgpack format |\n| 🔲 | main | View value in Protobuf format |\n| 🔲 | main | View value in Java serialized format |\n| 🔲 | main | View value in PHP serialized format |\n| 🔲 | main | View value in Pickle format |\n| 🔲 | main | View value in DateTime/timestamp format |\n| 🔲 | main | Confirm conversion between formats is smooth |\n| 🔲 | main | Confirm non-editable formats disable inline editing |\n| 🔲 | main | Confirm tooltip explains conversion errors |\n| 🔲 | main | Confirm switching formats for large keys (>10MB) doesn't freeze UI |\n| 🔲 | main | Edit value in JSON format and save |\n| 🔲 | main | Edit value in PHP serialized format and save |\n| 🔲 | main | Verify bigInt values display correctly |\n\n### 2.13 Search Keys (Search Index)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Create a new search index from index creation form |\n| 🔲 | main | Select existing index and search by indexed fields |\n| 🔲 | main | Perform search by full key name with exact match |\n| 🔲 | main | Create index with FT.CREATE command with multiple prefixes |\n| 🔲 | main | Switch between RediSearch mode and pattern mode |\n| 🔲 | main | View tooltip explaining RediSearch mode |\n| 🔲 | main | Search by index in Browser view |\n| 🔲 | main | Search by index in Tree view |\n| 🔲 | main | View filter history for RediSearch queries |\n| 🔲 | main | Verify context persistence for RediSearch across navigation |\n| 🔲 | main | Display \"No Redis Query Engine\" message when module not available |\n| 🔲 | main | Delete search index with FT.DROPINDEX |\n\n### 2.14 Key Filtering Patterns\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Filter keys with asterisk (*) wildcard |\n| ✅ | main | Filter keys with question mark (?) single character wildcard |\n| ✅ | main | Filter keys with [xy] character class (matches x or y) |\n| ✅ | main | Filter keys with [^x] negated character class |\n| ✅ | main | Filter keys with [a-z] character range |\n| ✅ | main | Escape special characters in filter pattern |\n| ✅ | main | Clear filter and search again |\n| 🔲 | main | Filter exact key name in large database (10M+ keys) |\n| 🔲 | main | Filter by pattern in large database (10M+ keys) |\n| 🔲 | main | Filter by key type in large database |\n\n### 2.15 Browser Context\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Browser context preserved when switching tabs |\n| 🔲 | main | Selected key details preserved when switching tabs |\n| 🔲 | main | Context cleared when page is reloaded |\n| 🔲 | main | CLI command history preserved in context |\n| 🔲 | main | Context cleared when navigating to different database |\n\n---\n\n## 3. Workbench\n\n### 3.1 Command Execution\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Execute single Redis command |\n| 🔲 | main | Execute multiple commands |\n| 🔲 | main | View command result |\n| 🔲 | main | Command autocomplete |\n| 🔲 | main | Command syntax highlighting |\n| 🔲 | main | Handle command error |\n| 🔲 | main | Clear editor |\n| 🔲 | main | History navigation |\n| 🔲 | main | Toggle Raw mode |\n| 🔲 | main | Toggle Group results |\n| 🔲 | main | Confirm command history persists after page refresh or session restart |\n| 🔲 | main | Re-run a previous command from history |\n| 🔲 | main | Run commands with quantifier (e.g., \"10 RANDOMKEY\") |\n| 🔲 | main | View group summary (X Command(s) - Y success, Z error(s)) |\n| 🔲 | main | View full list of commands with results in group mode |\n| 🔲 | main | Copy all commands from group result |\n| 🔲 | main | View group results in full screen mode |\n| 🔲 | main | Original datetime preserved in history after page refresh |\n| 🔲 | main | Display message when result exceeds 1MB after refresh |\n| 🔲 | main | History limited to 30 commands (oldest replaced by newest) |\n| 🔲 | main | Quick-access to command history with Up Arrow |\n| 🔲 | main | Use Non-Redis Editor with Shift+Space |\n\n### 3.1.1 Workbench Context\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Editor content preserved when switching tabs |\n| 🔲 | main | Command results preserved when switching tabs |\n| 🔲 | main | Context cleared when page is reloaded |\n| 🔲 | main | Insights panel state preserved when navigating |\n\n### 3.2 Results View\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View text result |\n| 🔲 | main | View table result |\n| 🔲 | main | View JSON result |\n| 🔲 | main | Copy result |\n| 🔲 | main | Expand/collapse results |\n| 🔲 | main | Clear results |\n| 🔲 | main | Re-run command |\n| 🔲 | main | Delete command result |\n\n### 3.2.1 Plugin and Visualization Support\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Confirm plugins for Search, TimeSeries load correctly |\n| 🔲 | main | Run FT.SEARCH command and confirm visualized table output |\n| 🔲 | main | Run TS.RANGE command and confirm chart visualization |\n| 🔲 | main | Confirm plugins display module-specific icons and metadata |\n| 🔲 | main | Switch between views (Table ↔ Text) and confirm format updates instantly |\n| 🔲 | main | Confirm TimeSeries visualization displays correct axes, values, and units |\n\n### 3.3 Tutorials\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Open Intro to search tutorial |\n| 🔲 | main | Open Basic use cases tutorial |\n| 🔲 | main | Open Intro to vector search tutorial |\n| 🔲 | main | Click Explore button |\n| 🔲 | main | Close insights panel |\n\n### 3.4 Profiler (Bottom Panel)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Start profiler |\n| 🔲 | main | Stop profiler |\n| 🔲 | main | Toggle Save Log |\n| 🔲 | main | View profiler warning |\n| 🔲 | main | Hide/close profiler panel |\n| 🔲 | main | Reset profiler |\n| 🔲 | main | Open profiler panel |\n\n### 3.5 Command Helper (Bottom Panel)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Open Command Helper panel |\n| ✅ | main | Search for a command |\n| ✅ | main | Filter commands by category |\n| ✅ | main | View command details |\n| ✅ | main | Hide/close Command Helper panel |\n\n---\n\n## 4. CLI\n\n### 4.1 CLI Panel\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Open CLI panel |\n| ✅ | main | Execute command |\n| ✅ | main | View command output |\n| ✅ | main | Close CLI panel |\n| ✅ | main | Hide CLI panel |\n| ✅ | main | Handle command errors |\n| ✅ | main | Execute multiple commands in sequence |\n| ✅ | main | Command history (up/down arrows) |\n| ✅ | main | Tab completion |\n\n### 4.2 Command Helper Integration\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Type command in CLI; confirm Command Helper updates dynamically |\n| 🔲 | main | Filter helper results by command category (Keys, Strings, JSON, Search) |\n| 🔲 | main | Open \"Read more\" link and confirm redirection to Redis.io documentation |\n| 🔲 | main | Confirm helper displays module-specific commands (FT., JSON., TS.*) |\n\n---\n\n## 5. Pub/Sub\n\n### 5.1 Subscribe\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Subscribe to channel |\n| 🔲 | main | Subscribe with pattern |\n| 🔲 | main | Receive messages |\n| 🔲 | main | Unsubscribe |\n| 🔲 | main | Multiple subscriptions | Feature not available - single pattern subscription only |\n| 🔲 | main | Clear messages | <!-- Feature not implemented in UI yet -->\n| 🔲 | main | Confirm newest messages appear at top of message table |\n| 🔲 | main | Confirm connection/subscription persist while navigating in same DB context |\n| 🔲 | main | Confirm performance under high throughput (≥5,000 messages/minute) |\n\n### 5.2 Publish\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Publish message to channel (form fill) |\n| 🔲 | main | Publish with different formats | Feature not available - plain text only |\n| 🔲 | main | Confirm published message appears instantly in message feed | _Covered by \"should receive published message\" test_ |\n| 🔲 | main | Confirm publish button shows status report with affected clients count |\n\n### 5.3 Message Table View\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View message table with subscribed messages |\n| 🔲 | main | Navigate message table pages |\n| 🔲 | main | Sort message table by columns |\n| 🔲 | main | Confirm table configuration persists across navigation |\n| 🔲 | main | Confirm message table with multiple messages |\n| 🔲 | main | Confirm status bar shows proper subscription status |\n| 🔲 | main | Confirm message count displays in status bar |\n\n### 5.4 Cluster Mode (Pub/Sub)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Confirm info message about SPUBLISH on welcome screen |\n| 🔲 | main | Confirm status report doesn't show affected clients in cluster mode |\n| 🔲 | main | SPUBLISH messages visibility | _Note: Use SSUBSCRIBE in Workbench_ |\n\n---\n\n## 6. Analytics\n\n### 6.1 Slow Log\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View slow log entries |\n| 🔲 | main | Refresh slow log |\n| 🔲 | main | Clear slow log button visible |\n| 🔲 | main | Configure slow log button visible |\n| 🔲 | main | Sort entries |\n| 🔲 | main | Filter entries | _Skipped: No filter UI available in current version_ |\n| 🔲 | main | Confirm slowlog-max-len and slowlog-log-slower-than configuration values display |\n| 🔲 | main | View command timestamp, duration, and execution details |\n| 🔲 | main | Change duration units between milliseconds and microseconds | _Skipped: No UI to change display units - duration always shown in msec_ |\n| 🔲 | main | Adjust slowlog-log-slower-than threshold and confirm results update |\n| 🔲 | main | Confirm empty state message displays correctly |\n| 🔲 | main | Confirm performance with thousands of slowlog entries |\n\n### 6.2 Database Analysis\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Run database analysis |\n| ✅ | main | View analysis results |\n| ✅ | main | View top keys table |\n| ✅ | main | View top namespaces |\n| ✅ | main | View TTL distribution |\n| ✅ | main | View recommendations (Tips tab) |\n| ✅ | main | History of analyses |\n| ✅ | main | Confirm charts for data types, namespaces, expirations render |\n| ✅ | main | Confirm extrapolation toggle adjusts charted values | Uses pre-seeded big database (port 8103) for partial scan |\n| ✅ | main | Confirm analysis distinguishes between scanned and estimated data |\n| ✅ | main | Confirm responsiveness on large datasets |\n| ✅ | main | Sort namespaces by key pattern |\n| ✅ | main | Sort namespaces by memory |\n| ✅ | main | Sort namespaces by number of keys |\n| ✅ | main | Filter namespace to Browser view |\n| 🔲 | main | Display \"No namespaces\" message with Tree View link |\n| ✅ | main | Toggle \"No Expiry\" in TTL distribution graph |\n| ✅ | main | View analysis history (up to 5 reports) |\n| ✅ | main | View voting section for recommendations |\n| 🔲 | main | Vote recommendation as useful | Voting buttons disabled - requires telemetry enabled |\n| 🔲 | main | Vote recommendation as not useful | Voting buttons disabled - requires telemetry enabled |\n| ✅ | main | Expand/collapse recommendation details |\n| ✅ | main | View recommendation labels (code changes, configuration changes) |\n| ✅ | main | Open tutorial from recommendation |\n\n### 6.2.1 Profiler\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Start profiler |\n| 🔲 | main | Stop profiler |\n| 🔲 | main | Toggle Save Log |\n| 🔲 | main | View profiler warning |\n| 🔲 | main | Observe live command feed without delay |\n| 🔲 | main | Toggle \"Save Logs\" and confirm local temp log file creation |\n| 🔲 | main | Test profiler behavior under heavy load (thousands of commands/minute) |\n\n### 6.3 Cluster Details\n> ⚠️ Requires properly configured OSS Cluster infrastructure (multiple nodes)\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View cluster nodes |\n| 🔲 | main | View node details |\n| 🔲 | main | View slot distribution |\n| 🔲 | main | Refresh cluster info |\n| 🔲 | main | View Overview tab by default for OSS Cluster |\n| 🔲 | main | View cluster header info (Type, Version, User) |\n| 🔲 | main | View cluster uptime |\n| 🔲 | main | View primary node statistics table |\n| 🔲 | main | View columns (Commands/s, Clients, Total Keys, Network Input/Output, Total Memory) |\n| 🔲 | main | Verify dynamic values update in statistics table |\n\n---\n\n## 7. Settings\n\n### 7.1 General Settings\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | View settings page |\n| ✅ | main | Show theme dropdown |\n| ✅ | main | Toggle show notifications |\n| ✅ | main | Show date/time format options |\n| ✅ | main | Change date/time format (custom) |\n| ✅ | main | Show time zone dropdown |\n\n### 7.2 Privacy Settings\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View privacy settings |\n| 🔲 | main | Show usage data switch |\n| 🔲 | main | Show privacy policy link |\n\n### 7.3 Workbench Settings\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Show editor cleanup switch |\n| ✅ | main | Show pipeline commands setting |\n| 🔲 | main | Configure command timeout (N/A - per-database setting, not in settings page) |\n\n### 7.4 Redis Cloud Settings\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | View Redis Cloud settings |\n| ✅ | main | Configure cloud account |\n\n### 7.5 Advanced Settings\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | Show keys to scan setting |\n| ✅ | main | Show advanced settings warning |\n\n---\n\n## 8. Vector Search\n\n### 8.1 Navigation and RQE Availability\n\n> **Spec:** `tests/main/vector-search/navigation/navigation.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should show welcome screen when no indexes exist |\n| ✅ | main | should show list screen when indexes exist |\n| ✅ | main | should show RQE not available screen for Redis without search module |\n\n### 8.2 Select Key Onboarding\n\n> **Spec:** `tests/main/vector-search/create-index/onboarding.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should show select key onboarding and dismiss on \"Got it\" |\n| ✅ | main | should not show select key onboarding on subsequent visit |\n\n### 8.3 Create Index - Onboarding\n\n> **Spec:** `tests/main/vector-search/create-index/onboarding.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should complete onboarding flow through all steps |\n| ✅ | main | should skip onboarding |\n| ✅ | main | should not show onboarding after completion |\n\n### 8.4 Create Index - Sample Data\n\n> **Spec:** `tests/main/vector-search/create-index/sample-data.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should close the sample data modal and return to list page |\n| ✅ | main | should cancel index creation from \"See index definition\" and return to list page |\n| ✅ | main | (E-Commerce Discovery) should create index via \"Start querying\" and verify query library is seeded |\n| ✅ | main | (E-Commerce Discovery) should create index via \"See index definition\" and verify toast |\n| ✅ | main | (Content Recommendations) should create index via \"Start querying\" and verify query library is seeded |\n| ✅ | main | (Content Recommendations) should create index via \"See index definition\" and verify toast |\n\n### 8.5 Create Index - Existing Data\n\n> **Spec:** `tests/main/vector-search/create-index/existing-data.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should create index with default settings and navigate to query page |\n| ✅ | main | should edit key prefix and create index |\n| ✅ | main | should add field and create index |\n| ✅ | main | should deselect a field row and exclude it from the index |\n| ✅ | main | should change index name and create index |\n| ✅ | main | should show duplicate index name validation and disable create button |\n| ✅ | main | should create index from JSON key and navigate to query page |\n\n### 8.6 List Indexes\n\n> **Spec:** `tests/main/vector-search/list-indexes/list-indexes.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should display indexes table with index name and create index button |\n| ✅ | main | should navigate to query page when Query button is clicked |\n| ✅ | main | should navigate to browser page when Browse dataset action is clicked |\n| ✅ | main | should open index details side panel via View index action |\n| ✅ | main | should delete index with confirmation |\n\n### 8.7 Create Index from List Page\n\n> **Spec:** `tests/main/vector-search/list-indexes/create-index.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should open sample data modal and complete \"Start querying\" flow |\n| ✅ | main | should open sample data modal and navigate to \"See index definition\" |\n| ✅ | main | should create index from existing data via list page menu |\n| ✅ | main | should disable \"Use existing data\" when no hash or JSON keys exist |\n\n### 8.8 Query Page\n\n> **Spec:** `tests/main/vector-search/query/query-editor.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should run query and view results |\n| ✅ | main | should expand and collapse query result card |\n| ✅ | main | should re-run query from result card |\n| ✅ | main | should delete individual result card |\n| ✅ | main | should clear all results |\n| ✅ | main | should disable explain and profile buttons when editor is empty |\n| ✅ | main | should disable explain and profile buttons for non-FT query |\n| ✅ | main | should disable save button when editor is empty |\n| ✅ | main | should execute explain query action |\n| ✅ | main | should execute profile query action |\n\n### 8.9 Query Page Onboarding\n\n> **Spec:** `tests/main/vector-search/query/query-onboarding.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should show query onboarding and dismiss on \"Got it\" |\n| ✅ | main | should not show query onboarding on subsequent visit |\n\n### 8.10 Save Query\n\n> **Spec:** `tests/main/vector-search/query/save-query.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should save query and verify it appears in query library |\n| ✅ | main | should navigate to query library via success toast action |\n| ✅ | main | should cancel save query modal |\n\n### 8.11 Query Library\n\n> **Spec:** `tests/main/vector-search/query/query-library.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should search and filter saved queries in the library |\n| ✅ | main | should expand and collapse a query library item |\n| ✅ | main | should run query from library |\n| ✅ | main | should load query into editor from library |\n| ✅ | main | should delete query from library and show notification |\n\n### 8.12 Browser Page Integration\n\n> **Spec:** `tests/main/vector-search/browser-integration/browser-integration.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| ✅ | main | should show \"View index\" button for key indexed by a single index |\n| ✅ | main | should show \"View index\" dropdown for key indexed by multiple indexes |\n| ✅ | main | should show \"Make searchable\" button for non-indexed key and create index |\n| ✅ | main | should show \"Index\" button on folder node and create index |\n| ✅ | main | should show RQE not available when navigating to Search tab on Redis without search module |\n\n---\n\n## 9. Redis Cloud Integration\n\n> ⚠️ Requires Redis Cloud account credentials.\n\n### 9.1 Auto-Discovery\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Connect to Redis Cloud account |\n| 🔲 | main | View subscriptions |\n| 🔲 | main | View databases |\n| 🔲 | main | Add cloud database to list |\n\n---\n\n## 10. Sentinel\n\n> ⚠️ Sentinel tests require external dependencies (requires Sentinel infrastructure).\n> These tests should be run in environments with Sentinel setup available.\n\n### 10.1 Sentinel Discovery\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Connect to Sentinel |\n| 🔲 | main | Discover databases |\n| 🔲 | main | Add discovered database |\n\n---\n\n## 11. RDI - Redis Data Integration\n\n> ⚠️ RDI require external dependencies (requires RDI backend services).\n> These tests should be run in environments with RDI infrastructure available.\n\n### 11.1 RDI Instance Management\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Add RDI instance |\n| 🔲 | main | Connect to RDI instance |\n| 🔲 | main | View RDI instance list |\n| 🔲 | main | Edit RDI instance |\n| 🔲 | main | Delete RDI instance |\n| 🔲 | main | Test RDI connection |\n| 🔲 | main | Error message displayed for invalid/non-existent RDI instance |\n\n### 11.2 RDI Pipeline\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View pipeline status |\n| 🔲 | main | Start pipeline |\n| 🔲 | main | Stop pipeline |\n| 🔲 | main | Reset pipeline |\n| 🔲 | main | View pipeline statistics |\n| 🔲 | main | Popover displayed for Reset button |\n| 🔲 | main | Popover displayed for Stop button |\n| 🔲 | main | Deploy successfully deploys configuration with success notification |\n| 🔲 | main | Pipeline state: Not running / Streaming |\n| 🔲 | main | Show loading indicators when waiting for action |\n\n### 11.3 RDI Jobs\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View jobs list |\n| 🔲 | main | Deploy job |\n| 🔲 | main | Edit job configuration |\n| 🔲 | main | Delete job |\n| 🔲 | main | Dry run job |\n| 🔲 | main | Add job via side menu |\n| 🔲 | main | Delete job via side menu |\n| 🔲 | main | Job shows unsaved changes indicator (blue) |\n| 🔲 | main | Job shows error indicator (red icon with hover details) |\n\n### 11.4 RDI Configuration\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View configuration |\n| 🔲 | main | Edit configuration |\n| 🔲 | main | Deploy configuration |\n| 🔲 | main | Download template |\n| 🔲 | main | Configuration shows unsaved changes indicator |\n| 🔲 | main | Configuration shows error indicator with hover details |\n| 🔲 | main | Insert template button opens menu |\n| 🔲 | main | Apply template only works on empty editor |\n\n### 11.5 RDI Control Menu\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Download deployed pipeline action |\n| 🔲 | main | Import pipeline from ZIP file |\n| 🔲 | main | Upload from file allows only ZIP files |\n| 🔲 | main | Save to file (ZIP) successfully |\n\n### 11.6 RDI Analytics\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Auto-refresh opens configuration panel |\n| 🔲 | main | Auto-refresh can be disabled |\n| 🔲 | main | Display data based on pipeline metrics |\n| 🔲 | main | Test connection opens panel with results |\n| 🔲 | main | Test connection displays all targets and sources |\n\n---\n\n## 12. Miscellaneous\n\n### 12.1 Notifications\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Confirm unread notifications display with distinct highlight/badge |\n| 🔲 | main | Confirm notification badge count updates when new messages arrive |\n| 🔲 | main | Confirm each notification displays title, description, and timestamp |\n\n### 12.2 Telemetry & Analytics\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Trigger key events and confirm telemetry records correctly |\n| 🔲 | main | Confirm telemetry payloads contain Database ID, Timestamp, Event Type |\n| 🔲 | main | Confirm telemetry events appear in analytics console/local logs |\n| 🔲 | main | Disable telemetry in Settings and confirm no new events logged |\n\n### 12.3 EULA & Privacy Settings\n\n> **Special Test Requirements:**\n> - EULA tests require fresh app state (no agreements stored)\n> - EULA popup blocks all UI interactions until accepted\n> - **Must run in isolation** - before other tests or in separate test run\n> - UI shows popup when: `config.agreements = null` OR consent key missing OR `spec.since > applied.version`\n> - **Reset via API:** `DELETE /api/settings/agreements` - resets agreements to null, triggering EULA popup on next page load\n> - **Auto-accept via env:** `RI_ACCEPT_TERMS_AND_CONDITIONS=true` - bypasses EULA popup entirely\n> - **Test file:** `tests/settings/eula/eula.spec.ts`\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | First launch shows EULA & Privacy Agreement dialog |\n| 🔲 | main | Submit button disabled until EULA checkbox checked |\n| 🔲 | main | \"Use recommended settings\" toggle auto-selects telemetry and encryption |\n| 🔲 | main | Encryption checkbox enabled by default |\n| 🔲 | main | Analytics checkbox respects \"Use recommended settings\" toggle |\n| 🔲 | main | Notifications checkbox available |\n| 🔲 | main | EULA link opens Redis license page |\n| 🔲 | main | Privacy policy link works |\n| 🔲 | main | Accepting EULA stores agreement version in database |\n| 🔲 | main | Version bump shows EULA popup again |\n| 🔲 | main | Decline analytics confirms telemetry events not sent |\n\n### 12.4 Onboarding Tour\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Onboarding starts after EULA acceptance (first database connection) |\n| 🔲 | main | Reset onboarding from Help Menu |\n| 🔲 | main | Onboarding step: Browser |\n| 🔲 | main | Onboarding step: Tree view |\n| 🔲 | main | Onboarding step: Filter and search |\n| 🔲 | main | Onboarding step: CLI (panel opens) |\n| 🔲 | main | Onboarding step: Command Helper (panel opens) |\n| 🔲 | main | Onboarding step: Profiler (panel opens) |\n| 🔲 | main | Onboarding step: Try Workbench (shows CLIENT LIST or FT.INFO) |\n| 🔲 | main | Onboarding step: Explore and learn more |\n| 🔲 | main | Onboarding step: Upload your tutorials |\n| 🔲 | main | Onboarding step: Database Analysis |\n| 🔲 | main | Onboarding step: Slow Log |\n| 🔲 | main | Onboarding step: Pub/Sub |\n| 🔲 | main | Onboarding step: Great job! (final step) |\n| 🔲 | main | Skip tour button completes onboarding |\n| 🔲 | main | Back button navigates to previous step |\n| 🔲 | main | Next button advances to next step |\n| 🔲 | main | Onboarding state persists after page refresh |\n| 🔲 | main | Final step closes when navigating to another page |\n\n### 12.5 Redis Cloud Conversion Funnel\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | User signs up with Google/GitHub → account, subscription, DB created → redirected to RI |\n| 🔲 | main | Existing Redis Cloud user without DB → free DB created → connection prompt |\n| 🔲 | main | All CTAs to Redis Cloud complete successfully (including tutorials) |\n| 🔲 | main | All CTAs pass UTM parameters correctly to Redis Cloud |\n| 🔲 | main | Telemetry events for conversion funnel are successful |\n\n### 12.6 App Settings\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Open Settings and update general preferences (theme, notifications) |\n| 🔲 | main | Confirm edits apply immediately across UI |\n\n### 12.7 Deep Linking (URL Handling)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Add database via redisinsight://databases/connect?redisUrl=... |\n| 🔲 | main | Auto-connect to database with redirect to workbench |\n| 🔲 | main | Open specific tutorial via tutorial parameter |\n| 🔲 | main | Cloud parameters (cloudBdbId, subscriptionType, planMemoryLimit, memoryLimitMeasurementUnit) |\n| 🔲 | main | Onboarding parameter opens onboarding flow |\n| 🔲 | main | Copilot parameter opens AI assistant |\n| 🔲 | main | Invalid URL shows error message |\n| 🔲 | main | URL with missing required parameters shows validation error |\n\n### 12.8 Keyboard Shortcuts\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Open keyboard shortcuts panel from Help Center |\n| 🔲 | main | View Desktop application shortcuts section |\n| 🔲 | main | View CLI shortcuts section |\n| 🔲 | main | View Workbench shortcuts section |\n| 🔲 | main | Close shortcuts panel |\n| 🔲 | main | Display desktop shortcuts (Open new window, Reload page) |\n| 🔲 | main | Display CLI shortcuts (Autocomplete, Clear screen, etc.) |\n| 🔲 | main | Display Workbench shortcuts (Run Commands, etc.) |\n| 🔲 | main | Up arrow navigates command history in CLI |\n| 🔲 | main | Shift+Space opens Non-Redis Editor |\n\n### 12.9 Live Recommendations\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View live recommendations in Insights panel |\n| 🔲 | main | Recommendations are database-specific (shown after analysis) |\n| 🔲 | main | View recommendation voting options |\n| 🔲 | main | Vote recommendation as not useful | Voting buttons disabled - requires telemetry enabled |\n| 🔲 | main | Hide recommendation | Hide/snooze only in Database Analysis Tips tab |\n| 🔲 | main | Snooze recommendation | Hide/snooze only in Database Analysis Tips tab |\n| 🔲 | main | Expand/collapse recommendation details |\n| 🔲 | main | View recommendation labels (code changes, configuration changes) |\n| 🔲 | main | Open tutorial from recommendation |\n| 🔲 | main | Recommendations sync with Database Analysis recommendations |\n\n### 12.10 Custom Tutorials (Upload/Import)\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | View \"My Tutorials\" section in Enablement area |\n| 🔲 | main | Upload custom tutorial from local ZIP file |\n| 🔲 | main | Upload custom tutorial via URL |\n| 🔲 | main | Upload tutorial with manifest.json (structured hierarchy) |\n| 🔲 | main | Upload tutorial without manifest.json (flat hierarchy) |\n| 🔲 | main | Delete custom tutorial |\n| 🔲 | main | View images in custom tutorials (external path) |\n| 🔲 | main | Bulk upload data from custom tutorial (relative path) |\n| 🔲 | main | Bulk upload data from custom tutorial (absolute path) |\n| 🔲 | main | Verify bulk upload summary (processed, success, errors) |\n| 🔲 | main | Open tutorial from links in other tutorials |\n| 🔲 | main | Cross-reference tutorials using redisinsight:// syntax |\n| 🔲 | main | Download tutorial data file |\n| 🔲 | main | Error message for invalid file path during bulk upload |\n\n### 12.11 Redis Stack Detection\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Display Redis Stack icon for databases with modules |\n| 🔲 | main | Show module list tooltip on Redis Stack icon hover |\n| 🔲 | main | Display Redis Stack logo in tooltip |\n| 🔲 | main | Verify all Redis Stack modules listed (Query Engine, Graph, Probabilistic, JSON, Time Series) |\n\n### 12.12 Feature Flags / Remote Config\n> ⚠️ Internal testing feature - remote config management\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Apply default config when remote config version is lower |\n| 🔲 | main | Invalid remote config not applied even with higher version |\n| 🔲 | main | Valid remote config applied with higher version |\n| 🔲 | main | Feature flags respect analytics filter |\n| 🔲 | main | Feature flags respect buildType filter |\n| 🔲 | main | Feature flags respect controlNumber range |\n\n### 12.13 Database List Sorting\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Sort databases by name (ascending/descending) |\n| 🔲 | main | Sort databases by host |\n| 🔲 | main | Sort databases by port |\n| 🔲 | main | Sort databases by last connection time |\n| 🔲 | main | Maintain sort order after refresh |\n\n### 12.14 Browser UI Enhancements\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Resize columns in key list |\n| 🔲 | main | Full screen mode for key details |\n| 🔲 | main | Last refresh timestamp display |\n| 🔲 | main | Handle DBSIZE permissions (show/hide key count) |\n| 🔲 | main | Large key details values handling |\n| 🔲 | main | Upload JSON key from file |\n| 🔲 | main | Iterative filtering (filter within filtered results) |\n\n### 12.15 Workbench Pipeline Mode\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Configure pipeline batch size in settings |\n| 🔲 | main | Verify pipeline text with external link in settings |\n| 🔲 | main | Only positive numbers allowed in pipeline input |\n| 🔲 | main | Pipeline limits concurrent command execution |\n| 🔲 | main | Spinner displayed over Run button during pipeline execution |\n| 🔲 | main | Editor remains interactive during pipeline execution |\n| 🔲 | main | Command results ordered most recent on top |\n\n### 12.16 Cypher / Graph Syntax (FalkorDB)\n> ⚠️ Requires FalkorDB/Graph module\n\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Open Cypher popover editor with \"Use Cypher Syntax\" |\n| 🔲 | main | Open Cypher popover editor with Shift+Space shortcut |\n| 🔲 | main | Popover populated with script detected between quotes |\n| 🔲 | main | Blank popover when quotes are empty |\n\n### 12.17 Survey Link\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Display survey link in browser |\n| 🔲 | main | Survey link opens correct external URL |\n\n### 12.18 GitHub Integration\n| Status | Group | Test Case |\n|--------|-------|-----------|\n| 🔲 | main | Display GitHub repository link |\n| 🔲 | main | GitHub link opens correct repository URL |\n\n"
  },
  {
    "path": "tests/e2e-playwright/config/app.ts",
    "content": "import { getEnv } from './env';\n\n/**\n * Application configuration\n */\nexport const appConfig = {\n  clientUrl: getEnv('RI_CLIENT_URL', 'http://localhost:8080'),\n  apiUrl: getEnv('RI_API_URL', 'http://localhost:5540'),\n  electronApiUrl: getEnv('RI_ELECTRON_API_URL', 'http://localhost:5530'),\n  electronExecutablePath: getEnv('ELECTRON_EXECUTABLE_PATH', ''),\n};\n"
  },
  {
    "path": "tests/e2e-playwright/config/databases/cluster.ts",
    "content": "import { RedisConnectionConfig } from 'e2eSrc/types';\nimport { getEnv, getEnvNumber } from '../env';\n\n/**\n * Cluster Redis configuration\n */\nexport const clusterConfig: RedisConnectionConfig = {\n  host: getEnv('OSS_CLUSTER_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_CLUSTER_PORT', 8200),\n};\n"
  },
  {
    "path": "tests/e2e-playwright/config/databases/index.ts",
    "content": "/**\n * Database configurations - organized by connection type\n * Add new database configs in separate files and re-export here\n */\n\nimport {\n  standaloneConfig,\n  standaloneV5Config,\n  standaloneV7Config,\n  standaloneV8Config,\n  standaloneEmptyConfig,\n  standaloneBigConfig,\n} from './standalone';\n\nimport { clusterConfig } from './cluster';\n\nimport { sentinelConfig } from './sentinel';\n\nimport { sshRedisConfig, sshTunnelConfig } from './ssh';\n\nimport { tlsRedisConfig, tlsCaCert, tlsClientCert, createUniqueTlsCerts } from './tls';\n\nexport const redisConfig = {\n  standalone: standaloneConfig,\n  standaloneV5: standaloneV5Config,\n  standaloneV7: standaloneV7Config,\n  standaloneV8: standaloneV8Config,\n  standaloneEmpty: standaloneEmptyConfig,\n  standaloneBig: standaloneBigConfig,\n  cluster: clusterConfig,\n  sentinel: sentinelConfig,\n  sshRedis: sshRedisConfig,\n  sshTunnel: sshTunnelConfig,\n  tlsRedis: tlsRedisConfig,\n  tlsCaCert,\n  tlsClientCert,\n  createUniqueTlsCerts,\n};\n"
  },
  {
    "path": "tests/e2e-playwright/config/databases/sentinel.ts",
    "content": "import { SentinelConfig } from 'e2eSrc/types';\nimport { getEnv, getEnvNumber } from '../env';\n\n/**\n * Sentinel Redis configuration\n */\nexport const sentinelConfig: SentinelConfig = {\n  host: getEnv('OSS_SENTINEL_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_SENTINEL_PORT', 28100),\n  password: getEnv('OSS_SENTINEL_PASSWORD', 'password'),\n  masterName: getEnv('OSS_SENTINEL_MASTER_NAME', 'primary-group-1'),\n};\n"
  },
  {
    "path": "tests/e2e-playwright/config/databases/ssh.ts",
    "content": "import { RedisConnectionConfig, SshTunnelConfig } from 'e2eSrc/types';\nimport { getEnvNumber, getEnvOptional } from '../env';\n\n/**\n * Redis configuration for SSH connection\n */\nexport const sshRedisConfig: Partial<RedisConnectionConfig> = {\n  host: getEnvOptional('REDIS_SSH_HOST'),\n  port: getEnvOptional('REDIS_SSH_PORT') ? getEnvNumber('REDIS_SSH_PORT') : undefined,\n};\n\n/**\n * SSH tunnel configuration\n */\nexport const sshTunnelConfig: SshTunnelConfig = {\n  host: getEnvOptional('SSH_HOST'),\n  port: getEnvOptional('SSH_PORT') ? getEnvNumber('SSH_PORT') : undefined,\n  username: getEnvOptional('SSH_USERNAME'),\n  password: getEnvOptional('SSH_PASSWORD'),\n};\n"
  },
  {
    "path": "tests/e2e-playwright/config/databases/standalone.ts",
    "content": "import { RedisConnectionConfig } from 'e2eSrc/types';\nimport { getEnv, getEnvNumber } from '../env';\n\n/**\n * Standalone Redis configurations\n */\nexport const standaloneConfig: RedisConnectionConfig = {\n  host: getEnv('OSS_STANDALONE_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_STANDALONE_PORT', 8100),\n};\n\nexport const standaloneV5Config: RedisConnectionConfig = {\n  host: getEnv('OSS_STANDALONE_V5_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_STANDALONE_V5_PORT', 8101),\n};\n\nexport const standaloneV7Config: RedisConnectionConfig = {\n  host: getEnv('OSS_STANDALONE_V7_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_STANDALONE_V7_PORT', 8108),\n};\n\nexport const standaloneV8Config: RedisConnectionConfig = {\n  host: getEnv('OSS_STANDALONE_V8_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_STANDALONE_V8_PORT', 8109),\n};\n\nexport const standaloneEmptyConfig: RedisConnectionConfig = {\n  host: getEnv('OSS_STANDALONE_EMPTY_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_STANDALONE_EMPTY_PORT', 8105),\n};\n\nexport const standaloneBigConfig: RedisConnectionConfig = {\n  host: getEnv('OSS_STANDALONE_BIG_HOST', '127.0.0.1'),\n  port: getEnvNumber('OSS_STANDALONE_BIG_PORT', 8103),\n};\n"
  },
  {
    "path": "tests/e2e-playwright/config/databases/tls.ts",
    "content": "import * as fs from 'fs';\nimport * as path from 'path';\nimport { TlsCertConfig, TlsClientCertConfig } from 'e2eSrc/types';\nimport { getEnvNumber, getEnvOptional } from '../env';\n\n// Path to certificates in the RTE folder\nconst CERTS_PATH = path.resolve(__dirname, '../../../e2e/rte/oss-standalone-tls/certs');\n\nfunction readCertFile(filename: string): string {\n  return fs.readFileSync(path.join(CERTS_PATH, filename), 'utf-8');\n}\n\nfunction generateUniqueSuffix(): string {\n  return `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;\n}\n\n// Generate unique suffix once per module load for backward compatibility\nconst uniqueSuffix = generateUniqueSuffix();\n\n/**\n * TLS Redis connection configuration\n */\nexport const tlsRedisConfig = {\n  host: getEnvOptional('OSS_STANDALONE_TLS_HOST') || '127.0.0.1',\n  port: getEnvNumber('OSS_STANDALONE_TLS_PORT', 8104),\n};\n\n/**\n * CA Certificate for TLS connection\n */\nexport const tlsCaCert: TlsCertConfig = {\n  name: getEnvOptional('TLS_CA_CERT_NAME') || `test-ca-${uniqueSuffix}`,\n  certificate: getEnvOptional('TLS_CA_CERT') || readCertFile('redisCA.crt'),\n};\n\n/**\n * Client Certificate for TLS connection (mutual TLS)\n */\nexport const tlsClientCert: TlsClientCertConfig = {\n  name: getEnvOptional('TLS_CLIENT_CERT_NAME') || `test-client-${uniqueSuffix}`,\n  certificate: getEnvOptional('TLS_CLIENT_CERT') || readCertFile('user.crt'),\n  key: getEnvOptional('TLS_CLIENT_KEY') || readCertFile('user.key'),\n};\n\n/**\n * Create fresh TLS cert configs with a unique suffix per call,\n * so multiple tests in the same module don't collide on cert names.\n */\nexport function createUniqueTlsCerts(): { caCert: TlsCertConfig; clientCert: TlsClientCertConfig } {\n  const suffix = generateUniqueSuffix();\n  const caBase = getEnvOptional('TLS_CA_CERT_NAME') || 'test-ca';\n  const clientBase = getEnvOptional('TLS_CLIENT_CERT_NAME') || 'test-client';\n  return {\n    caCert: {\n      ...tlsCaCert,\n      name: `${caBase}-${suffix}`,\n    },\n    clientCert: {\n      ...tlsClientCert,\n      name: `${clientBase}-${suffix}`,\n    },\n  };\n}\n"
  },
  {
    "path": "tests/e2e-playwright/config/env.ts",
    "content": "import * as dotenv from 'dotenv';\nimport * as path from 'path';\n\n/**\n * Environment configuration loader\n *\n * Supports multiple environments via ENV variable:\n *   ENV=staging npm test   → loads .env.staging\n *   ENV=ci npm test        → loads .env.ci\n *   npm test               → loads .env (default/local)\n *\n * Priority: process.env > .env.{ENV} > .env\n */\n\nconst env = process.env.ENV || 'local';\nconst envFile = env === 'local' ? '.env' : `.env.${env}`;\nconst envPath = path.resolve(__dirname, '..', envFile);\n\n// Load environment-specific file first\ndotenv.config({ path: envPath });\n\n// Load default .env as fallback (won't override existing values)\ndotenv.config({ path: path.resolve(__dirname, '../.env') });\n\n// Export current environment name\nexport const currentEnv = env;\n\n/**\n * Get environment variable with optional default value\n */\nexport function getEnv(key: string, defaultValue?: string): string {\n  const value = process.env[key] ?? defaultValue;\n  if (value === undefined) {\n    throw new Error(`Missing required environment variable: ${key}`);\n  }\n  return value;\n}\n\n/**\n * Get environment variable as number\n */\nexport function getEnvNumber(key: string, defaultValue?: number): number {\n  const value = process.env[key];\n  if (value === undefined) {\n    if (defaultValue === undefined) {\n      throw new Error(`Missing required environment variable: ${key}`);\n    }\n    return defaultValue;\n  }\n  const num = parseInt(value, 10);\n  if (isNaN(num)) {\n    throw new Error(`Environment variable ${key} must be a number, got: ${value}`);\n  }\n  return num;\n}\n\n/**\n * Get optional environment variable\n */\nexport function getEnvOptional(key: string): string | undefined {\n  return process.env[key];\n}\n"
  },
  {
    "path": "tests/e2e-playwright/config/index.ts",
    "content": "/**\n * Central configuration exports\n *\n * Structure:\n * - config/app.ts        - Application URLs\n * - config/env.ts        - Environment variable helpers\n * - config/databases/    - Database configs by type (standalone, cluster, sentinel, ssh)\n */\n\n// App configuration\nexport { appConfig } from './app';\n\n// Database configurations\nexport * from './databases';\n\n// Environment helpers (for custom configs)\nexport { getEnv, getEnvNumber, getEnvOptional, currentEnv } from './env';\n"
  },
  {
    "path": "tests/e2e-playwright/example.env",
    "content": "# RedisInsight E2E Test Configuration\n# Copy this file to .env and update values for your environment\n\n# Application URLs\nRI_CLIENT_URL=http://localhost:8080\nRI_API_URL=http://localhost:5540\n\n# Electron Desktop App Testing\n# Platform-specific paths:\n# - macOS arm64: ../../release/mac-arm64/Redis Insight.app/Contents/MacOS/Redis Insight\n# - macOS x64:   ../../release/mac-x64/Redis Insight.app/Contents/MacOS/Redis Insight\n# - Linux:       ../../release/linux-unpacked/redisinsight\n# - Windows:     ../../release/win-unpacked/Redis Insight.exe\nELECTRON_EXECUTABLE_PATH=../../release/mac-arm64/Redis Insight.app/Contents/MacOS/Redis Insight\n\n# Redis Standalone instances (from docker-compose)\nOSS_STANDALONE_HOST=127.0.0.1\nOSS_STANDALONE_PORT=8100\n\nOSS_STANDALONE_V7_HOST=127.0.0.1\nOSS_STANDALONE_V7_PORT=8108\n\nOSS_STANDALONE_V5_HOST=127.0.0.1\nOSS_STANDALONE_V5_PORT=8101\n\nOSS_STANDALONE_EMPTY_HOST=127.0.0.1\nOSS_STANDALONE_EMPTY_PORT=8105\n\n# Redis TLS instance\nOSS_STANDALONE_TLS_HOST=127.0.0.1\nOSS_STANDALONE_TLS_PORT=8104\n\n# Redis Cluster instances\nOSS_CLUSTER_HOST=127.0.0.1\nOSS_CLUSTER_PORT=8200\n\n# Redis Sentinel instances\nOSS_SENTINEL_HOST=127.0.0.1\nOSS_SENTINEL_PORT=28100\nOSS_SENTINEL_PASSWORD=password\nOSS_SENTINEL_MASTER_NAME=primary-group-1\n\n# SSH tunnel configuration (optional)\n# REDIS_SSH_HOST=172.31.100.109\n# REDIS_SSH_PORT=6379\n# SSH_HOST=localhost\n# SSH_PORT=2222\n# SSH_USERNAME=u\n# SSH_PASSWORD=pass\n\n"
  },
  {
    "path": "tests/e2e-playwright/fixtures/base.ts",
    "content": "import { test as base, ElectronApplication, _electron as electron } from '@playwright/test';\nimport {\n  BrowserPage,\n  CliPanel,\n  CommandHelperPanel,\n  DatabasesPage,\n  WorkbenchPage,\n  AnalyticsPage,\n  SettingsPage,\n  PubSubPage,\n  EulaPage,\n  SidebarPanel,\n  InsightsPanel,\n  VectorSearchPage,\n} from 'e2eSrc/pages';\nimport { ApiHelper, retry } from 'e2eSrc/helpers';\n\n/**\n * Extended ElectronApplication with windowId for API authentication\n */\ninterface ElectronAppWithWindowId extends ElectronApplication {\n  windowId?: string;\n}\n\n/**\n * Test-scoped fixtures\n */\ntype Fixtures = {\n  apiHelper: ApiHelper;\n  /**\n   * Browser page fixture\n   * Use browserPage.goto(databaseId) to navigate\n   */\n  browserPage: BrowserPage;\n  cliPanel: CliPanel;\n  commandHelperPanel: CommandHelperPanel;\n  databasesPage: DatabasesPage;\n  workbenchPage: WorkbenchPage;\n  analyticsPage: AnalyticsPage;\n  settingsPage: SettingsPage;\n  pubSubPage: PubSubPage;\n  eulaPage: EulaPage;\n  sidebarPanel: SidebarPanel;\n  insightsPanel: InsightsPanel;\n  vectorSearchPage: VectorSearchPage;\n};\n\n/**\n * Worker-scoped fixtures and options (shared across all tests in a worker)\n */\ntype WorkerFixtures = {\n  /** Path to Electron executable - when set, tests run in Electron mode */\n  electronExecutablePath: string | undefined;\n  apiUrl: string;\n  electronApp: ElectronAppWithWindowId | undefined;\n};\n\n/**\n * Base test with custom options and common fixtures\n */\nconst baseTest = base.extend<Fixtures, WorkerFixtures>({\n  // Custom options - can be set per-project in playwright.config.ts\n  // Worker-scoped so they're available to worker-scoped fixtures\n  electronExecutablePath: [undefined, { option: true, scope: 'worker' }],\n  apiUrl: ['', { option: true, scope: 'worker' }],\n\n  // Electron app - worker-scoped, shared across all tests in a worker\n  // Only launched when electronExecutablePath is set\n  electronApp: [\n    async ({ electronExecutablePath, apiUrl }, use) => {\n      if (!electronExecutablePath) {\n        // Browser mode - no Electron app needed\n        await use(undefined);\n        return;\n      }\n\n      console.log(`Launching Electron app: ${electronExecutablePath}`);\n\n      const electronApp = await electron.launch({\n        executablePath: electronExecutablePath,\n        args: ['--no-sandbox'],\n        timeout: 60000,\n      });\n\n      try {\n        // Log Electron console messages for debugging\n        electronApp.on('console', (msg) => {\n          console.log(`[Electron] ${msg.type()}: ${msg.text()}`);\n        });\n\n        // Wait for the main window (not the splash screen)\n        let mainWindow = await electronApp.firstWindow();\n\n        // If we got the splash screen, wait for the main window\n        if (mainWindow.url().includes('splash')) {\n          console.log('Waiting for main window (splash detected)...');\n          mainWindow = await electronApp.waitForEvent('window', {\n            timeout: 5000,\n          });\n        }\n\n        // Wait for the page to fully load\n        await mainWindow.waitForLoadState('load');\n\n        // Additional wait for React to render and IPC to complete\n        // The windowId is set in indexElectron.tsx after the IPC message is received\n        await new Promise((resolve) => setTimeout(resolve, 2000));\n\n        // Extract windowId from the Electron app's window object\n        // The windowId is set via IPC and may take a moment to be available\n        let windowId: string | undefined;\n        const getWindowId = async () => {\n          if (mainWindow.isClosed()) {\n            throw new Error('Window was closed unexpectedly');\n          }\n          windowId = await mainWindow.evaluate(() => (window as Window & { windowId?: string }).windowId);\n          if (!windowId) {\n            throw new Error('windowId not yet available');\n          }\n        };\n        await retry(getWindowId, {\n          maxAttempts: 5,\n          errorMessage: 'windowId not available - Electron app may not have initialized correctly',\n        });\n        console.log(`Got Electron windowId: ${windowId}`);\n\n        // Wait for API to be available with windowId for authentication\n        const apiHelper = new ApiHelper({ apiUrl, windowId });\n        const checkApi = async () => {\n          console.log(`Checking API at ${apiUrl} with windowId...`);\n          await apiHelper.getDatabases();\n          console.log('Electron API is ready');\n        };\n        await retry(checkApi, {\n          maxAttempts: 5,\n          errorMessage: 'Electron API did not become available',\n        });\n        await apiHelper.dispose();\n\n        // Create extended electronApp with windowId\n        const electronAppWithWindowId: ElectronAppWithWindowId = Object.assign(electronApp, {\n          windowId: windowId,\n        });\n\n        await use(electronAppWithWindowId);\n      } finally {\n        console.log('Closing Electron app...');\n        await electronApp.close();\n      }\n    },\n    { scope: 'worker' },\n  ],\n\n  // Page - from Electron app or browser depending on mode\n  page: async ({ electronApp, page, baseURL }, use) => {\n    if (!electronApp) {\n      // Browser mode - navigate to app if on blank page\n      if (page.url() === 'about:blank' && baseURL) {\n        await page.goto(baseURL);\n        await page.waitForLoadState('domcontentloaded');\n      }\n      // Skip onboarding by setting localStorage (faster than waiting for UI)\n      // Setting to null marks onboarding as completed/skipped\n      await page.evaluate(() => {\n        localStorage.setItem('onboardingStep', 'null');\n      });\n      await use(page);\n      return;\n    }\n\n    // Electron mode - get page from Electron app\n    const electronPage = await electronApp.firstWindow();\n    // Skip onboarding by setting localStorage before reload\n    // This ensures the app reads the value when it initializes\n    await electronPage.evaluate(() => {\n      localStorage.setItem('onboardingStep', 'null');\n    });\n    // Reload to pick up any data created in beforeAll (e.g., databases via API)\n    // and to apply the localStorage setting\n    await electronPage.reload();\n    await electronPage.waitForLoadState('domcontentloaded');\n\n    await use(electronPage);\n  },\n\n  apiHelper: async ({ apiUrl, electronApp }, use) => {\n    // Get windowId from electronApp if available (for Electron API authentication)\n    const windowId = electronApp?.windowId;\n\n    const helper = new ApiHelper({ apiUrl, windowId });\n    await helper.ensureEulaAccepted();\n    await use(helper);\n    await helper.dispose();\n  },\n\n  browserPage: async ({ page }, use) => {\n    await use(new BrowserPage(page));\n  },\n\n  cliPanel: async ({ page }, use) => {\n    await use(new CliPanel(page));\n  },\n\n  commandHelperPanel: async ({ page }, use) => {\n    await use(new CommandHelperPanel(page));\n  },\n\n  databasesPage: async ({ page }, use) => {\n    await use(new DatabasesPage(page));\n  },\n\n  workbenchPage: async ({ page }, use) => {\n    await use(new WorkbenchPage(page));\n  },\n\n  analyticsPage: async ({ page }, use) => {\n    await use(new AnalyticsPage(page));\n  },\n\n  settingsPage: async ({ page }, use) => {\n    await use(new SettingsPage(page));\n  },\n\n  pubSubPage: async ({ page }, use) => {\n    await use(new PubSubPage(page));\n  },\n\n  eulaPage: async ({ page }, use) => {\n    await use(new EulaPage(page));\n  },\n\n  sidebarPanel: async ({ page }, use) => {\n    await use(new SidebarPanel(page));\n  },\n\n  insightsPanel: async ({ page }, use) => {\n    await use(new InsightsPanel(page));\n  },\n\n  vectorSearchPage: async ({ page }, use) => {\n    await use(new VectorSearchPage(page));\n  },\n});\n\nexport const test = baseTest;\nexport { expect } from '@playwright/test';\n"
  },
  {
    "path": "tests/e2e-playwright/helpers/api.ts",
    "content": "import { request, APIRequestContext } from '@playwright/test';\nimport { AddDatabaseConfig, DatabaseInstance, IndexSchemaField } from 'e2eSrc/types';\nimport { TEST_DB_PREFIX } from 'e2eSrc/test-data/databases';\n\n/**\n * API Helper for database operations\n * Used for test setup/teardown to avoid slow UI interactions\n */\nexport class ApiHelper {\n  private context: APIRequestContext | null = null;\n  private readonly apiUrl: string;\n  private readonly windowId?: string;\n\n  constructor(options: { apiUrl: string; windowId?: string }) {\n    this.apiUrl = options.apiUrl;\n    this.windowId = options.windowId;\n  }\n\n  private async getContext(): Promise<APIRequestContext> {\n    if (!this.context) {\n      this.context = await request.newContext({\n        baseURL: this.apiUrl,\n        // Ignore HTTPS certificate errors for self-signed certificates (used in Electron tests)\n        ignoreHTTPSErrors: true,\n        // Include X-Window-Id header for Electron app authentication\n        extraHTTPHeaders: this.windowId ? { 'X-Window-Id': this.windowId } : undefined,\n      });\n    }\n    return this.context;\n  }\n\n  /**\n   * Create a database via API\n   */\n  async createDatabase(config: AddDatabaseConfig): Promise<DatabaseInstance> {\n    const ctx = await this.getContext();\n    const response = await ctx.post('/api/databases', {\n      data: {\n        name: config.name,\n        host: config.host,\n        port: config.port,\n        username: config.username || null,\n        password: config.password || null,\n        db: config.db ?? 0,\n      },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create database: ${response.status()} - ${body}`);\n    }\n\n    return response.json();\n  }\n\n  /**\n   * Delete a database by ID\n   */\n  async deleteDatabase(id: string): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.delete(`/api/databases/${id}`);\n\n    if (!response.ok() && response.status() !== 404) {\n      const body = await response.text();\n      throw new Error(`Failed to delete database: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Update a database via API (PATCH)\n   */\n  async updateDatabase(id: string, data: Record<string, unknown>): Promise<DatabaseInstance> {\n    const ctx = await this.getContext();\n    const response = await ctx.patch(`/api/databases/${id}`, { data });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to update database: ${response.status()} - ${body}`);\n    }\n\n    return response.json();\n  }\n\n  /**\n   * Get all databases\n   */\n  async getDatabases(): Promise<DatabaseInstance[]> {\n    const ctx = await this.getContext();\n    const response = await ctx.get('/api/databases');\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to get databases: ${response.status()} - ${body}`);\n    }\n\n    return response.json();\n  }\n\n  /**\n   * Get a database by ID\n   */\n  async getDatabase(id: string): Promise<DatabaseInstance | null> {\n    const ctx = await this.getContext();\n    const response = await ctx.get(`/api/databases/${id}`);\n\n    if (response.status() === 404) {\n      return null;\n    }\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to get database: ${response.status()} - ${body}`);\n    }\n\n    return response.json();\n  }\n\n  /**\n   * Delete databases matching a name pattern\n   * Useful for cleanup of test databases\n   */\n  async deleteDatabasesByPattern(pattern: RegExp): Promise<number> {\n    const databases = await this.getDatabases();\n    const matching = databases.filter((db) => pattern.test(db.name));\n\n    for (const db of matching) {\n      await this.deleteDatabase(db.id);\n    }\n\n    return matching.length;\n  }\n\n  /**\n   * Delete all test databases (names starting with TEST_DB_PREFIX)\n   */\n  async deleteTestDatabases(): Promise<number> {\n    return this.deleteDatabasesByPattern(new RegExp(`^${TEST_DB_PREFIX}`));\n  }\n\n  /**\n   * Create a String key via API\n   * Value can be a string or a Buffer-like object { type: 'Buffer', data: number[] } for binary data\n   */\n  async createStringKey(\n    databaseId: string,\n    keyName: string,\n    value: string | { type: 'Buffer'; data: number[] },\n  ): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/string`, {\n      data: { keyName, value },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create string key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Create a Hash key via API\n   */\n  async createHashKey(databaseId: string, keyName: string, fields: { field: string; value: string }[]): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/hash`, {\n      data: { keyName, fields },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create hash key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Create a List key via API\n   * Uses the POST /list endpoint with elements array\n   */\n  async createListKey(databaseId: string, keyName: string, elements: string[]): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/list`, {\n      data: { keyName, elements, destination: 'TAIL' },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create list key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Create a Set key via API\n   */\n  async createSetKey(databaseId: string, keyName: string, members: string[]): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/set`, {\n      data: { keyName, members },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create set key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Create a Sorted Set (ZSet) key via API\n   * Uses the POST /zSet endpoint with members array containing {name, score}\n   */\n  async createZSetKey(\n    databaseId: string,\n    keyName: string,\n    members: { member: string; score: string }[],\n  ): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/zSet`, {\n      data: { keyName, members: members.map((m) => ({ name: m.member, score: parseFloat(m.score) })) },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create zset key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Create a Stream key via API\n   * Uses the POST /streams endpoint with entries array containing {id, fields}\n   * Fields should be {name, value} pairs\n   */\n  async createStreamKey(\n    databaseId: string,\n    keyName: string,\n    fields: { field: string; value: string }[],\n    entryId: string = '*',\n  ): Promise<void> {\n    const ctx = await this.getContext();\n    // Convert field/value to name/value format expected by API\n    const formattedFields = fields.map((f) => ({ name: f.field, value: f.value }));\n    const response = await ctx.post(`/api/databases/${databaseId}/streams`, {\n      data: { keyName, entries: [{ id: entryId, fields: formattedFields }] },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create stream key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Create a JSON key via API\n   */\n  async createJsonKey(databaseId: string, keyName: string, value: string): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/rejson-rl`, {\n      data: { keyName, data: value },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create json key: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Delete keys matching a pattern in a database\n   * Uses SCAN + DEL to avoid blocking\n   */\n  async deleteKeysByPattern(databaseId: string, pattern: string): Promise<number> {\n    const ctx = await this.getContext();\n\n    // First, scan for keys matching the pattern\n    const scanResponse = await ctx.post(`/api/databases/${databaseId}/keys`, {\n      data: {\n        cursor: '0',\n        count: 10000,\n        match: pattern,\n      },\n    });\n\n    if (!scanResponse.ok()) {\n      // If scan fails, it might be because there are no keys - that's OK\n      return 0;\n    }\n\n    const scanResult = await scanResponse.json();\n    const keys = scanResult.keys || [];\n\n    if (keys.length === 0) {\n      return 0;\n    }\n\n    // Delete the keys\n    const keyNames = keys.map((k: { name: string }) => k.name);\n    const deleteResponse = await ctx.delete(`/api/databases/${databaseId}/keys`, {\n      data: { keys: keyNames },\n    });\n\n    if (!deleteResponse.ok()) {\n      // Ignore delete errors - keys might already be gone\n      return 0;\n    }\n\n    return keyNames.length;\n  }\n\n  /**\n   * Get current app settings\n   */\n  async getSettings(): Promise<{\n    agreements: {\n      eula: boolean;\n      analytics: boolean;\n      encryption: boolean;\n      notifications: boolean;\n      version: string;\n    } | null;\n  }> {\n    const ctx = await this.getContext();\n    const response = await ctx.get('/api/settings');\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to get settings: ${response.status()} - ${body}`);\n    }\n\n    return response.json();\n  }\n\n  /**\n   * Reset agreements to trigger EULA popup\n   */\n  async resetAgreements(): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.delete('/api/settings/agreements');\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to reset agreements: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Accept EULA and set agreements via API\n   */\n  async acceptEula(options?: { analytics?: boolean; encryption?: boolean; notifications?: boolean }): Promise<void> {\n    const ctx = await this.getContext();\n    const response = await ctx.patch('/api/settings', {\n      data: {\n        agreements: {\n          eula: true,\n          analytics: options?.analytics ?? false,\n          encryption: options?.encryption ?? true,\n          notifications: options?.notifications ?? false,\n        },\n      },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to accept EULA: ${response.status()} - ${body}`);\n    }\n  }\n\n  /**\n   * Ensure EULA is accepted (check first, accept if needed)\n   */\n  async ensureEulaAccepted(): Promise<void> {\n    const settings = await this.getSettings();\n    if (!settings.agreements || !settings.agreements.eula) {\n      await this.acceptEula();\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // Vector Search — Index management\n  // ---------------------------------------------------------------------------\n\n  /**\n   * Create a RediSearch index via the CLI endpoint.\n   *\n   * Builds an `FT.CREATE` command from the provided arguments and sends it\n   * through the same send-command helper used for ad-hoc Redis commands.\n   */\n  async createIndex(\n    databaseId: string,\n    indexName: string,\n    prefix: string,\n    schema: IndexSchemaField[],\n    keyType: 'hash' | 'json' = 'hash',\n  ): Promise<void> {\n    const schemaArgs = schema.flatMap((f) => [`\"${f.name}\"`, f.type.toUpperCase()]);\n    const command = [\n      'FT.CREATE',\n      `\"${indexName}\"`,\n      'ON',\n      keyType.toUpperCase(),\n      'PREFIX',\n      '1',\n      `\"${prefix}\"`,\n      'SCHEMA',\n      ...schemaArgs,\n    ].join(' ');\n\n    await this.sendCommand(databaseId, command);\n  }\n\n  /**\n   * Delete a single RediSearch index.\n   * Silently ignores 404 (index already gone).\n   */\n  async deleteIndex(databaseId: string, indexName: string): Promise<void> {\n    await this.sendCommand(databaseId, `FT.DROPINDEX ${indexName}`).catch(() => {});\n  }\n\n  /**\n   * Delete RediSearch indexes in the database.\n   * When called without a filter, deletes all indexes.\n   * Pass a filter predicate to delete only matching indexes.\n   */\n  async deleteAllIndexes(databaseId: string, filter?: (name: string) => boolean): Promise<void> {\n    const indexes = await this.getIndexes(databaseId);\n    const toDelete = filter ? indexes.filter(filter) : indexes;\n\n    for (const name of toDelete) {\n      await this.deleteIndex(databaseId, name);\n    }\n  }\n\n  /**\n   * List all RediSearch indexes in the database.\n   */\n  async getIndexes(databaseId: string): Promise<string[]> {\n    const response = await this.sendCommand(databaseId, 'FT._LIST');\n\n    if (Array.isArray(response)) {\n      return response.map(String).filter(Boolean);\n    }\n\n    return [];\n  }\n\n  // ---------------------------------------------------------------------------\n  // Vector Search — Saved queries\n  // ---------------------------------------------------------------------------\n\n  /**\n   * Create a saved query via API.\n   * Returns the created query object (including `id`).\n   */\n  async createSavedQuery(\n    databaseId: string,\n    indexName: string,\n    name: string,\n    query: string,\n  ): Promise<{ id: string; name: string; query: string }> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/query-library`, {\n      data: { indexName, name, query },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Failed to create saved query: ${response.status()} - ${body}`);\n    }\n\n    return response.json();\n  }\n\n  /**\n   * Delete all saved queries for a given index.\n   */\n  async deleteAllSavedQueries(databaseId: string, indexName: string): Promise<void> {\n    const ctx = await this.getContext();\n    const listResponse = await ctx.get(`/api/databases/${databaseId}/query-library?indexName=${indexName}`);\n\n    if (!listResponse.ok()) return;\n\n    const queries: { id: string }[] = await listResponse.json();\n    for (const q of queries) {\n      await ctx.delete(`/api/databases/${databaseId}/query-library/${q.id}`);\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // Vector Search — Command history\n  // ---------------------------------------------------------------------------\n\n  /**\n   * Delete all search-type command execution history entries for a database.\n   */\n  async deleteCommandExecutions(databaseId: string): Promise<void> {\n    const ctx = await this.getContext();\n    const listResponse = await ctx.get(`/api/databases/${databaseId}/workbench/command-executions?type=SEARCH`);\n\n    if (!listResponse.ok()) return;\n\n    const executions: { id: string }[] = await listResponse.json();\n    if (executions.length === 0) return;\n\n    for (const e of executions) {\n      await ctx.delete(`/api/databases/${databaseId}/workbench/command-executions/${e.id}`);\n    }\n  }\n\n  // ---------------------------------------------------------------------------\n  // General — Send arbitrary Redis command\n  // ---------------------------------------------------------------------------\n\n  /**\n   * Send a Redis command via the Workbench command-executions endpoint.\n   * Returns the `response` field from the first result entry.\n   */\n  async sendCommand(databaseId: string, command: string): Promise<unknown> {\n    const ctx = await this.getContext();\n    const response = await ctx.post(`/api/databases/${databaseId}/workbench/command-executions`, {\n      data: { commands: [command] },\n    });\n\n    if (!response.ok()) {\n      const body = await response.text();\n      throw new Error(`Command \"${command}\" failed: ${response.status()} - ${body}`);\n    }\n\n    const executions = await response.json();\n    return executions?.[0]?.result?.[0]?.response;\n  }\n\n  /**\n   * Cleanup resources\n   */\n  async dispose(): Promise<void> {\n    if (this.context) {\n      await this.context.dispose();\n      this.context = null;\n    }\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/helpers/index.ts",
    "content": "export * from './api';\nexport * from './retry';\n"
  },
  {
    "path": "tests/e2e-playwright/helpers/retry.ts",
    "content": "/**\n * Retry options\n */\nexport interface RetryOptions {\n  maxAttempts?: number;\n  delayMs?: number;\n  errorMessage?: string;\n}\n\n/**\n * Retry a function until it succeeds or max attempts reached\n * @param fn - Async function to retry\n * @param options - Retry options\n * @returns Result of the function\n * @throws Error if all attempts fail\n */\nexport async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {\n  const { maxAttempts = 5, delayMs = 1000, errorMessage } = options;\n\n  let lastError: Error | undefined;\n\n  for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n    try {\n      return await fn();\n    } catch (error) {\n      lastError = error instanceof Error ? error : new Error(String(error));\n      if (attempt < maxAttempts) {\n        await new Promise((resolve) => setTimeout(resolve, delayMs));\n      }\n    }\n  }\n\n  const message = errorMessage || `Failed after ${maxAttempts} attempts`;\n  throw new Error(`${message}: ${lastError?.message}`);\n}\n"
  },
  {
    "path": "tests/e2e-playwright/package.json",
    "content": "{\n  \"name\": \"e2e-playwright\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Standalone Playwright E2E tests for RedisInsight\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"test\": \"playwright test\",\n    \"test:chromium\": \"playwright test --project=chromium\",\n    \"test:chromium:headed\": \"playwright test --project=chromium --headed\",\n    \"test:chromium:debug\": \"playwright test --project=chromium --debug\",\n    \"test:chromium:ui\": \"playwright test --project=chromium --ui\",\n    \"test:electron\": \"playwright test --project=electron\",\n    \"test:electron:headed\": \"playwright test --project=electron --headed\",\n    \"test:electron:debug\": \"playwright test --project=electron --debug\",\n    \"test:report\": \"playwright show-report\",\n    \"test:codegen\": \"playwright codegen http://localhost:8080\",\n    \"lint\": \"eslint . --ext .ts\",\n    \"lint:fix\": \"eslint . --ext .ts --fix\",\n    \"format\": \"prettier --write \\\"**/*.ts\\\"\",\n    \"format:check\": \"prettier --check \\\"**/*.ts\\\"\",\n    \"type-check\": \"tsc --noEmit\",\n    \"prepare\": \"cd ../.. && husky tests/e2e-playwright/.husky\"\n  },\n  \"lint-staged\": {\n    \"*.ts\": [\n      \"eslint --cache --fix\",\n      \"prettier --write\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@faker-js/faker\": \"^9.3.0\",\n    \"@playwright/test\": \"^1.49.1\",\n    \"@types/node\": \"^22.10.2\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.18.1\",\n    \"@typescript-eslint/parser\": \"^8.18.1\",\n    \"eslint\": \"^8.57.1\",\n    \"fishery\": \"^2.4.0\",\n    \"husky\": \"^9.1.7\",\n    \"lint-staged\": \"^15.2.10\",\n    \"prettier\": \"^3.4.2\",\n    \"typescript\": \"^5.7.2\"\n  },\n  \"dependencies\": {\n    \"dotenv\": \"^16.4.7\"\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/BasePage.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Base Page Object class with common functionality\n * All page objects should extend this class\n *\n * Navigation is UI-based for cross-platform compatibility (browser + Electron)\n */\nexport abstract class BasePage {\n  readonly page: Page;\n\n  // Common UI elements\n  readonly loadingSpinner: Locator;\n  readonly toastSuccess: Locator;\n  readonly toastError: Locator;\n  readonly toastContainer: Locator;\n\n  // Navigation elements\n  readonly redisLogo: Locator;\n  readonly homeTabs: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Navigation locators\n    this.redisLogo = page.getByTestId('redis-logo-link');\n    this.homeTabs = page.getByTestId('home-tabs');\n\n    // Common locators\n    this.loadingSpinner = page.locator('[data-testid=\"loading-spinner\"], .loading-spinner');\n    this.toastSuccess = page.locator('.toast-success, [data-testid=\"toast-success\"]');\n    this.toastError = page.locator('.toast-error, [data-testid=\"toast-error\"]');\n    this.toastContainer = page.locator('.Toastify, [data-testid=\"toast-container\"]');\n  }\n\n  // ===========================================\n  // Navigation Methods (UI-based, works on all platforms)\n  // ===========================================\n\n  /**\n   * Navigate to the home page (databases list)\n   * Click the Redis logo - works on both browser and Electron\n   */\n  async gotoHome(): Promise<void> {\n    await this.redisLogo.click();\n    await this.homeTabs.waitFor({ state: 'visible', timeout: 10000 });\n  }\n\n  /**\n   * Navigate to a specific database instance (opens Browser page by default)\n   * Waits for database to be connected (navigation tabs visible)\n   * @param databaseId - ID of the database to connect to\n   */\n  async gotoDatabase(databaseId: string): Promise<void> {\n    await this.gotoHome();\n\n    const dbRow = this.page.getByTestId(`instance-name-${databaseId}`);\n\n    await this.paginateToRow(dbRow);\n\n    // The database may have been created via API after the home page\n    // fetched its list. Clicking the logo while already on \"/\" doesn't\n    // trigger a data re-fetch, so the list can be stale. Reload once\n    // to get a fresh list and retry pagination.\n    if (!(await dbRow.isVisible())) {\n      await this.page.reload();\n      await this.paginateToRow(dbRow);\n    }\n\n    await dbRow.click();\n    await this.page.getByRole('tab', { name: 'Browse' }).waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Wait for the database list to load, then paginate through all pages\n   * starting from page 1 until {@link row} is visible or no more pages remain.\n   */\n  private async paginateToRow(row: Locator): Promise<void> {\n    const firstPage = this.page.getByRole('button', { name: 'first page' });\n    const nextPage = this.page.getByRole('button', { name: 'next page' });\n\n    await this.page\n      .getByTestId(/^instance-name-/)\n      .first()\n      .waitFor({ state: 'visible' });\n\n    // Always start from page 1 so we scan every page.\n    if ((await firstPage.isVisible()) && (await firstPage.isEnabled())) {\n      await firstPage.click();\n      await this.page\n        .getByTestId(/^instance-name-/)\n        .first()\n        .waitFor({ state: 'visible' });\n    }\n\n    while (!(await row.isVisible())) {\n      if (!(await nextPage.isVisible()) || !(await nextPage.isEnabled())) break;\n      await nextPage.click();\n      await this.page\n        .getByTestId(/^instance-name-/)\n        .first()\n        .waitFor({ state: 'visible' });\n    }\n  }\n\n  /**\n   * Navigate to this page's URL\n   * Must be implemented by each page\n   * @param options - Optional navigation options (e.g., databaseId)\n   */\n  abstract goto(...args: unknown[]): Promise<void>;\n\n  /**\n   * Wait for page to be fully loaded\n   * Override in child classes for page-specific loading indicators\n   */\n  async waitForLoad(): Promise<void> {\n    await this.page.waitForLoadState('domcontentloaded');\n    await this.waitForSpinnerToDisappear();\n  }\n\n  /**\n   * Wait for loading spinner to disappear\n   */\n  async waitForSpinnerToDisappear(timeout = 30000): Promise<void> {\n    try {\n      await this.loadingSpinner.waitFor({ state: 'hidden', timeout });\n    } catch {\n      // Spinner might not appear at all, which is fine\n    }\n  }\n\n  /**\n   * Wait for a success toast to appear\n   */\n  async waitForSuccessToast(timeout = 10000): Promise<void> {\n    await expect(this.toastSuccess).toBeVisible({ timeout });\n  }\n\n  /**\n   * Wait for an error toast to appear\n   */\n  async waitForErrorToast(timeout = 10000): Promise<void> {\n    await expect(this.toastError).toBeVisible({ timeout });\n  }\n\n  /**\n   * Dismiss all toasts\n   */\n  async dismissToasts(): Promise<void> {\n    const closeButtons = this.toastContainer.locator('button[aria-label=\"close\"], .Toastify__close-button');\n    const count = await closeButtons.count();\n    for (let i = 0; i < count; i++) {\n      await closeButtons\n        .nth(i)\n        .click()\n        .catch(() => {});\n    }\n  }\n\n  /**\n   * Take a screenshot for debugging\n   */\n  async screenshot(name: string): Promise<void> {\n    await this.page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });\n  }\n\n  /**\n   * Get current page URL\n   */\n  getUrl(): string {\n    return this.page.url();\n  }\n\n  /**\n   * Check if element is visible\n   */\n  async isVisible(locator: Locator, timeout = 5000): Promise<boolean> {\n    try {\n      await expect(locator).toBeVisible({ timeout });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Wait for URL to contain specific path\n   */\n  async waitForUrl(urlPattern: string | RegExp, timeout = 10000): Promise<void> {\n    await this.page.waitForURL(urlPattern, { timeout });\n  }\n\n  /**\n   * Reload the page and wait for load\n   */\n  async reload(): Promise<void> {\n    await this.page.reload();\n    await this.waitForLoad();\n  }\n\n  /**\n   * Press keyboard shortcut\n   */\n  async pressKey(key: string): Promise<void> {\n    await this.page.keyboard.press(key);\n  }\n\n  /**\n   * Fill input with clear first\n   */\n  async fillInput(locator: Locator, value: string): Promise<void> {\n    await locator.clear();\n    await locator.fill(value);\n  }\n\n  /**\n   * Click and wait for navigation\n   */\n  async clickAndWaitForNavigation(locator: Locator): Promise<void> {\n    await Promise.all([this.page.waitForURL(/.*/, { waitUntil: 'domcontentloaded' }), locator.click()]);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/InstancePage.ts",
    "content": "import { Page } from '@playwright/test';\nimport { BasePage } from './BasePage';\nimport { InstanceHeader, NavigationTabs, BottomPanel } from './components';\n\n/**\n * Base class for all database instance pages (Browser, Workbench, Analyze, Pub/Sub)\n *\n * Provides common components:\n * - Instance header (database name, stats, breadcrumb)\n * - Navigation tabs (Browse, Workbench, Analyze, Pub/Sub)\n * - Bottom panel (CLI, Command Helper, Profiler)\n *\n * Specific pages (BrowserPage, WorkbenchPage, etc.) should extend this class.\n */\nexport abstract class InstancePage extends BasePage {\n  // Common components for all instance pages\n  readonly instanceHeader: InstanceHeader;\n  readonly navigationTabs: NavigationTabs;\n  readonly bottomPanel: BottomPanel;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Initialize common components\n    this.instanceHeader = new InstanceHeader(page);\n    this.navigationTabs = new NavigationTabs(page);\n    this.bottomPanel = new BottomPanel(page);\n  }\n\n  /**\n   * Navigate to a database and this specific page\n   * @param databaseId - The ID of the database to navigate to\n   */\n  abstract goto(databaseId: string): Promise<void>;\n\n  /**\n   * Navigate to Browser tab (staying on same database)\n   */\n  async navigateToBrowser(): Promise<void> {\n    await this.navigationTabs.gotoBrowser();\n    await this.waitForLoad();\n  }\n\n  /**\n   * Navigate to Search tab (staying on same database)\n   */\n  async navigateToSearch(): Promise<void> {\n    await this.navigationTabs.gotoSearch();\n    await this.waitForLoad();\n  }\n\n  /**\n   * Navigate to Workbench tab (staying on same database)\n   */\n  async navigateToWorkbench(): Promise<void> {\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n\n  /**\n   * Navigate to Analyze tab (staying on same database)\n   */\n  async navigateToAnalyze(): Promise<void> {\n    await this.navigationTabs.gotoAnalyze();\n    await this.waitForLoad();\n  }\n\n  /**\n   * Navigate to Pub/Sub tab (staying on same database)\n   */\n  async navigateToPubSub(): Promise<void> {\n    await this.navigationTabs.gotoPubSub();\n    await this.waitForLoad();\n  }\n\n  /**\n   * Open CLI panel\n   */\n  async openCli(): Promise<void> {\n    await this.bottomPanel.openCli();\n  }\n\n  /**\n   * Open Command Helper panel\n   */\n  async openCommandHelper(): Promise<void> {\n    await this.bottomPanel.openCommandHelper();\n  }\n\n  /**\n   * Open Profiler panel\n   */\n  async openProfiler(): Promise<void> {\n    await this.bottomPanel.openProfiler();\n  }\n\n  /**\n   * Get the database name from header\n   */\n  async getDatabaseName(): Promise<string | null> {\n    return this.instanceHeader.getDatabaseName();\n  }\n\n  /**\n   * Get the logical database index (e.g., \"db0\", \"db1\")\n   */\n  async getLogicalDatabaseIndex(): Promise<string | null> {\n    return this.instanceHeader.getLogicalDatabaseIndex();\n  }\n\n  /**\n   * Check if logical database button is visible\n   */\n  async isLogicalDatabaseButtonVisible(): Promise<boolean> {\n    return this.instanceHeader.isLogicalDatabaseButtonVisible();\n  }\n\n  /**\n   * Navigate back to databases list\n   */\n  async goToDatabases(): Promise<void> {\n    await this.instanceHeader.goToDatabases();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/analytics/AnalyticsPage.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\n/**\n * Analytics Page Object Model\n * Contains Slow Log and Database Analysis sub-pages\n *\n * Extends InstancePage to get access to:\n * - instanceHeader (database name, stats, breadcrumb)\n * - navigationTabs (Browse, Workbench, Analyze, Pub/Sub)\n * - bottomPanel (CLI, Command Helper, Profiler)\n */\nexport class AnalyticsPage extends InstancePage {\n  // Sub-page tabs\n  readonly databaseAnalysisTab: Locator;\n  readonly slowLogTab: Locator;\n\n  // Slow Log elements\n  readonly slowLogTable: Locator;\n  readonly slowLogRows: Locator;\n  readonly configureButton: Locator;\n  readonly clearSlowLogButton: Locator;\n  readonly refreshButton: Locator;\n  readonly displayUpToDropdown: Locator;\n  readonly executionTimeText: Locator;\n  readonly slowLogEmptyState: Locator;\n  readonly slowLogEmptyStateMessage: Locator;\n\n  // Database Analysis page and header elements\n  readonly databaseAnalysisPage: Locator;\n  readonly analysisHeader: Locator;\n  readonly newReportButton: Locator;\n  readonly analysisProgress: Locator;\n  readonly noReportsMessage: Locator;\n  readonly reportHistorySelect: Locator;\n  readonly scannedKeysText: Locator;\n\n  // Database Analysis sub-tabs\n  readonly dataSummaryTab: Locator;\n  readonly tipsTab: Locator;\n\n  // Summary per data (donut charts)\n  readonly summaryPerData: Locator;\n  readonly summaryPerDataCharts: Locator;\n  readonly memoryChartTitle: Locator;\n  readonly keysChartTitle: Locator;\n  readonly totalMemoryValue: Locator;\n  readonly totalKeysValue: Locator;\n  readonly extrapolateSwitch: Locator;\n\n  // TTL / Expiration\n  readonly ttlDistributionChart: Locator;\n  readonly showNoExpirySwitch: Locator;\n\n  // Top Namespaces\n  readonly topNamespacesContainer: Locator;\n  readonly topNamespacesEmpty: Locator;\n  readonly topNamespacesMessage: Locator;\n  readonly treeViewPageLink: Locator;\n  readonly nspTableMemory: Locator;\n  readonly nspTableKeys: Locator;\n\n  // Top Keys\n  readonly topKeysTitle: Locator;\n  readonly topKeysTableMemory: Locator;\n  readonly topKeysTableLength: Locator;\n\n  // Tips/Recommendations elements\n  readonly badgesLegend: Locator;\n  readonly recommendationAccordions: Locator;\n  readonly emptyRecommendationsMessage: Locator;\n  readonly tutorialButton: Locator;\n  readonly votingSection: Locator;\n  readonly usefulVoteButton: Locator;\n  readonly notUsefulVoteButton: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Sub-page tabs (rendered by @redis-ui/components Tabs with role=\"tab\")\n    this.databaseAnalysisTab = page.getByRole('tab', { name: 'Database Analysis' });\n    this.slowLogTab = page.getByRole('tab', { name: 'Slow Log' });\n\n    // Slow Log elements\n    this.slowLogTable = page.getByTestId('slowlog-table');\n    this.slowLogRows = this.slowLogTable.getByRole('row').filter({ hasNot: page.locator('[role=\"columnheader\"]') });\n    this.configureButton = page.getByRole('button', { name: 'Configure' });\n    this.clearSlowLogButton = page.getByRole('button', { name: 'Clear Slow Log' });\n    this.refreshButton = page.getByTestId('refresh-slowlog-btn').or(page.locator('[data-testid*=\"refresh\"]').first());\n    this.displayUpToDropdown = page.getByRole('combobox').filter({ hasText: /^\\d+$/ });\n    this.executionTimeText = page.getByText(/Execution time:/);\n    this.slowLogEmptyState = page.getByText('No Slow Logs found');\n    this.slowLogEmptyStateMessage = page.getByText(/Either no commands exceeding/);\n\n    // Database Analysis page and header\n    this.databaseAnalysisPage = page.getByTestId('database-analysis-page');\n    this.analysisHeader = page.getByTestId('db-analysis-header');\n    this.newReportButton = page.getByTestId('start-database-analysis-btn');\n    this.analysisProgress = page.getByTestId('analysis-progress');\n    this.noReportsMessage = page.getByTestId('empty-analysis-no-reports');\n    this.reportHistorySelect = page.getByTestId('select-report');\n    this.scannedKeysText = page.getByText(/Scanned \\d+%/);\n\n    // Database Analysis sub-tabs\n    this.dataSummaryTab = page.getByRole('tab', { name: 'Data Summary' });\n    this.tipsTab = page.getByRole('tab', { name: /Tips/ });\n\n    // Summary per data (donut charts)\n    this.summaryPerData = page.getByTestId('summary-per-data');\n    this.summaryPerDataCharts = page.getByTestId('summary-per-data-charts');\n    this.memoryChartTitle = page.getByTestId('donut-title-memory');\n    this.keysChartTitle = page.getByTestId('donut-title-keys');\n    this.totalMemoryValue = page.getByTestId('total-memory-value');\n    this.totalKeysValue = page.getByTestId('total-keys-value');\n    this.extrapolateSwitch = page.getByTestId('extrapolate-results').first();\n\n    // TTL / Expiration\n    this.ttlDistributionChart = page.getByTestId('analysis-ttl');\n    this.showNoExpirySwitch = page.getByTestId('show-no-expiry-switch');\n\n    // Top Namespaces\n    this.topNamespacesContainer = page.getByTestId('top-namespaces');\n    this.topNamespacesEmpty = page.getByTestId('top-namespaces-empty');\n    this.topNamespacesMessage = page.getByTestId('top-namespaces-message');\n    this.treeViewPageLink = page.getByTestId('tree-view-page-link');\n    this.nspTableMemory = page.getByTestId('nsp-table-memory');\n    this.nspTableKeys = page.getByTestId('nsp-table-keys');\n\n    // Top Keys\n    this.topKeysTitle = page.getByTestId('top-keys-title');\n    this.topKeysTableMemory = page.getByTestId('top-keys-table-memory');\n    this.topKeysTableLength = page.getByTestId('top-keys-table-length');\n\n    // Tips/Recommendations elements\n    this.badgesLegend = page.getByTestId('badges-legend');\n    this.recommendationAccordions = page.locator('[data-testid$=\"-accordion\"]');\n    this.emptyRecommendationsMessage = page.getByTestId('empty-recommendations-message');\n    this.tutorialButton = page.locator('[data-testid$=\"-to-tutorial-btn\"]');\n    this.votingSection = page.getByTestId('recommendation-voting');\n    this.usefulVoteButton = page.getByTestId('useful-vote-btn');\n    this.notUsefulVoteButton = page.getByTestId('not useful-vote-btn');\n  }\n\n  /**\n   * Navigate to Analytics page - defaults to Slow Log\n   */\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoSlowLog(databaseId);\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.slowLogTab.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Navigate to Slow Log page via UI\n   */\n  async gotoSlowLog(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoAnalyze();\n    await this.slowLogTab.click();\n    await this.slowLogTab.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Navigate to Database Analysis page via UI\n   */\n  async gotoDatabaseAnalysis(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoAnalyze();\n    await this.databaseAnalysisTab.click();\n    await this.databaseAnalysisTab.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Switch to Slow Log sub-tab\n   */\n  async clickSlowLogTab(): Promise<void> {\n    await this.slowLogTab.click();\n  }\n\n  /**\n   * Switch to Database Analysis sub-tab\n   */\n  async clickDatabaseAnalysisTab(): Promise<void> {\n    await this.databaseAnalysisTab.click();\n  }\n\n  /**\n   * Get slow log entries count\n   */\n  async getSlowLogEntriesCount(): Promise<number> {\n    await this.slowLogTable.waitFor({ state: 'visible' });\n    return await this.slowLogRows.count();\n  }\n\n  /**\n   * Check if slow log has entries\n   */\n  async hasSlowLogEntries(): Promise<boolean> {\n    const count = await this.getSlowLogEntriesCount();\n    return count > 0;\n  }\n\n  /**\n   * Click New Report button to generate analysis\n   */\n  async clickNewReport(): Promise<void> {\n    await this.newReportButton.click();\n  }\n\n  /**\n   * Wait for analysis report to be generated\n   * @param timeout - max wait time in ms (default 30s, use longer for large datasets)\n   */\n  async waitForReportGenerated(timeout = 30000): Promise<void> {\n    await this.analysisProgress.waitFor({ state: 'visible', timeout });\n  }\n\n  /**\n   * Check if analysis report is visible\n   */\n  async isReportVisible(): Promise<boolean> {\n    try {\n      await this.analysisProgress.waitFor({ state: 'visible', timeout: 5000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Ensure a report has been generated (check first, generate if needed)\n   * Use in beforeEach or at the start of tests that require a report\n   * @param timeout - max wait time in ms (default 30s, use longer for large datasets)\n   */\n  async ensureReportGenerated(timeout = 30000): Promise<void> {\n    const hasReport = await this.isReportVisible();\n    if (!hasReport) {\n      await this.clickNewReport();\n      await this.waitForReportGenerated(timeout);\n    }\n  }\n\n  /**\n   * Get tips count from tab label\n   */\n  async getTipsCount(): Promise<number> {\n    const tabText = await this.tipsTab.textContent();\n    const match = tabText?.match(/Tips\\s*\\((\\d+)\\)/);\n    return match ? parseInt(match[1], 10) : 0;\n  }\n\n  /**\n   * Refresh slow log\n   */\n  async refreshSlowLog(): Promise<void> {\n    await this.refreshButton.click();\n    await this.page.getByText(/Last refresh:.*now/).waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Get last refresh time text\n   */\n  async getLastRefreshText(): Promise<string> {\n    const lastRefreshElement = this.page\n      .locator('[class*=\"last-refresh\"]')\n      .or(this.page.getByText(/Last refresh:/).locator('..'));\n    return (await lastRefreshElement.textContent()) || '';\n  }\n\n  /**\n   * Check if TTL distribution chart is visible\n   */\n  async isTtlDistributionVisible(): Promise<boolean> {\n    try {\n      await this.ttlDistributionChart.waitFor({ state: 'visible', timeout: 5000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Check if report history select is visible\n   */\n  async isReportHistoryVisible(): Promise<boolean> {\n    try {\n      await this.reportHistorySelect.waitFor({ state: 'visible', timeout: 5000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Get report history options count\n   */\n  async getReportHistoryCount(): Promise<number> {\n    await this.reportHistorySelect.click();\n    const options = this.page.getByRole('option');\n    const count = await options.count();\n    await this.page.keyboard.press('Escape');\n    return count;\n  }\n\n  /**\n   * Toggle show no expiry switch\n   */\n  async toggleShowNoExpiry(): Promise<void> {\n    await this.showNoExpirySwitch.click();\n  }\n\n  /**\n   * Clear slow log entries\n   */\n  async clearSlowLog(): Promise<void> {\n    await this.clearSlowLogButton.click();\n    await this.page.getByText('Clear slow log').waitFor({ state: 'visible' });\n    await this.page.getByTestId('reset-confirm-btn').click();\n    await this.page.getByText('Clear slow log').waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Check if slow log empty state is visible\n   */\n  async isSlowLogEmpty(): Promise<boolean> {\n    try {\n      await this.slowLogEmptyState.waitFor({ state: 'visible', timeout: 3000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Open slow log configuration dialog\n   */\n  async openSlowLogConfig(): Promise<void> {\n    await this.configureButton.click();\n    await this.page.getByTestId('slowlog-config-save-btn').waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Set slowlog-log-slower-than threshold value\n   */\n  async setSlowLogThreshold(value: string, unit: 'msec' | 'µs' = 'msec'): Promise<void> {\n    const thresholdInput = this.page.getByRole('textbox', { name: 'slowlog-log-slower-than' });\n    await thresholdInput.clear();\n    await thresholdInput.fill(value);\n\n    const unitCombobox = this.page.getByRole('combobox').filter({ hasText: /msec|µs/ });\n    const currentUnit = await unitCombobox.textContent();\n    if (currentUnit && !currentUnit.includes(unit)) {\n      await unitCombobox.click();\n      await this.page.getByRole('option', { name: unit }).click();\n    }\n  }\n\n  /**\n   * Save slow log configuration\n   */\n  async saveSlowLogConfig(): Promise<void> {\n    await this.page.getByTestId('slowlog-config-save-btn').click();\n    await this.page.getByTestId('slowlog-config-save-btn').waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Cancel slow log configuration\n   */\n  async cancelSlowLogConfig(): Promise<void> {\n    await this.page.getByTestId('slowlog-config-cancel-btn').click();\n    await this.page.getByTestId('slowlog-config-cancel-btn').waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Get current execution time threshold from the header\n   */\n  async getExecutionTimeThreshold(): Promise<string> {\n    const text = await this.executionTimeText.textContent();\n    const match = text?.match(/Execution time:\\s*([\\d.]+)\\s*msec/);\n    return match ? match[1] : '';\n  }\n\n  // ===== Top Namespaces Methods =====\n\n  /**\n   * Switch top namespaces table to view by Memory\n   */\n  async switchTopNamespacesView(view: 'memory' | 'keys'): Promise<void> {\n    const testId = view === 'memory' ? 'btn-change-table-memory' : 'btn-change-table-keys';\n    await this.topNamespacesContainer.getByTestId(testId).click();\n  }\n\n  /**\n   * Get the visible top namespaces table (memory or keys view)\n   */\n  async getVisibleNamespacesTable(): Promise<Locator> {\n    if (await this.nspTableMemory.isVisible()) {\n      return this.nspTableMemory;\n    }\n    return this.nspTableKeys;\n  }\n\n  // ===== Top Keys Methods =====\n\n  /**\n   * Switch top keys table to view by Memory or Length\n   */\n  async switchTopKeysView(view: 'memory' | 'length'): Promise<void> {\n    const testId = view === 'memory' ? 'btn-change-table-memory' : 'btn-change-table-keys';\n    // Scope to the top keys section to avoid conflict with namespace buttons\n    await this.topKeysTitle.locator('..').getByTestId(testId).click();\n  }\n\n  /**\n   * Get the visible top keys table (memory or length view)\n   */\n  async getVisibleTopKeysTable(): Promise<Locator> {\n    if (await this.topKeysTableMemory.isVisible()) {\n      return this.topKeysTableMemory;\n    }\n    return this.topKeysTableLength;\n  }\n\n  // ===== Tips/Recommendations Methods =====\n\n  /**\n   * Click on Tips tab\n   */\n  async clickTipsTab(): Promise<void> {\n    await this.tipsTab.click();\n    await this.tipsTab.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Get count of recommendation accordions\n   */\n  async getRecommendationCount(): Promise<number> {\n    return this.recommendationAccordions.count();\n  }\n\n  /**\n   * Check if badges legend is visible (contains recommendation type labels)\n   */\n  async isBadgesLegendVisible(): Promise<boolean> {\n    return this.badgesLegend.isVisible();\n  }\n\n  /**\n   * Expand or collapse a recommendation by index\n   */\n  async toggleRecommendation(index: number): Promise<void> {\n    const accordion = this.recommendationAccordions.nth(index);\n    const button = accordion.locator('button[aria-expanded]');\n    await button.click();\n  }\n\n  /**\n   * Check if a recommendation is expanded\n   */\n  async isRecommendationExpanded(index: number): Promise<boolean> {\n    const accordion = this.recommendationAccordions.nth(index);\n    const button = accordion.locator('button[aria-expanded]');\n    const expanded = await button.getAttribute('aria-expanded');\n    return expanded === 'true';\n  }\n\n  /**\n   * Check if tutorial button is visible for any recommendation\n   */\n  async hasTutorialButton(): Promise<boolean> {\n    const count = await this.tutorialButton.count();\n    return count > 0;\n  }\n\n  /**\n   * Click tutorial button for first recommendation that has one\n   */\n  async clickTutorialButton(): Promise<void> {\n    await this.tutorialButton.first().click();\n  }\n\n  /**\n   * Check if voting section is visible for any recommendation\n   */\n  async isVotingSectionVisible(): Promise<boolean> {\n    const count = await this.votingSection.count();\n    return count > 0;\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/analytics/index.ts",
    "content": "export { AnalyticsPage } from './AnalyticsPage';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/BrowserPage.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\nimport { AddKeyDialog, BulkActionsPanel, KeyDetails, KeyList, MakeSearchableModal } from './components';\n\n/**\n * Browser Page Object\n * Main page for browsing Redis keys\n *\n * Extends InstancePage which provides:\n * - instanceHeader: Database name, stats, breadcrumb\n * - navigationTabs: Browse, Workbench, Analyze, Pub/Sub tabs\n * - bottomPanel: CLI, Command Helper, Profiler buttons\n */\nexport class BrowserPage extends InstancePage {\n  // Browser-specific components\n  readonly addKeyDialog: AddKeyDialog;\n  readonly bulkActionsPanel: BulkActionsPanel;\n  readonly keyDetails: KeyDetails;\n  readonly keyList: KeyList;\n  readonly makeSearchableModal: MakeSearchableModal;\n\n  // Browser-specific action buttons\n  readonly addKeyButton: Locator;\n  readonly bulkActionsButton: Locator;\n\n  // Key details panel\n  readonly keyDetailsPanel: Locator;\n  readonly noKeySelectedMessage: Locator;\n\n  // View index / Make searchable (key details header)\n  readonly viewIndexButton: Locator;\n  readonly viewIndexMenuTrigger: Locator;\n  readonly viewIndexCountBadge: Locator;\n  readonly makeSearchableButton: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Initialize browser-specific components\n    this.addKeyDialog = new AddKeyDialog(page);\n    this.bulkActionsPanel = new BulkActionsPanel(page);\n    this.keyDetails = new KeyDetails(page);\n    this.keyList = new KeyList(page);\n    this.makeSearchableModal = new MakeSearchableModal(page);\n\n    // Browser-specific action buttons\n    this.addKeyButton = page.getByText('Add key', { exact: true });\n    this.bulkActionsButton = page.getByRole('button', { name: /bulk actions/i });\n\n    // Key details panel\n    this.keyDetailsPanel = page.getByTestId('key-details-header');\n    this.noKeySelectedMessage = page.getByText(/Select the key from the list/);\n\n    // View index / Make searchable\n    this.viewIndexButton = page.getByRole('button', { name: 'View index', exact: true });\n    this.viewIndexMenuTrigger = page.getByTestId('view-index-data-menu-trigger');\n    this.viewIndexCountBadge = page.getByTestId('view-index-data-count-badge');\n    this.makeSearchableButton = page.getByRole('button', { name: 'Make searchable' });\n  }\n\n  /**\n   * Navigate to Browser page for a specific database\n   * @param databaseId - The ID of the database to navigate to\n   */\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.waitForLoad();\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.page.waitForLoadState('domcontentloaded');\n    await this.keyList.waitForKeysLoaded();\n  }\n\n  async openAddKeyDialog(): Promise<void> {\n    await this.addKeyButton.click();\n    await expect(this.addKeyDialog.title).toBeVisible();\n  }\n\n  async closeAddKeyDialog(): Promise<void> {\n    if (await this.addKeyDialog.isVisible()) {\n      await this.addKeyDialog.clickCancel();\n    }\n  }\n\n  async expectKeyInList(keyName: string): Promise<void> {\n    const exists = await this.keyList.keyExists(keyName);\n    expect(exists).toBe(true);\n  }\n\n  async expectKeyNotInList(keyName: string): Promise<void> {\n    const exists = await this.keyList.keyExists(keyName);\n    expect(exists).toBe(false);\n  }\n\n  /**\n   * Get a \"View index\" menu item by index name (inside the dropdown)\n   */\n  getViewIndexMenuItem(indexName: string): Locator {\n    return this.page.getByRole('menuitem', { name: indexName, exact: true });\n  }\n\n  /**\n   * Get the \"Index\" button that appears on a folder node when hovered\n   */\n  getIndexFolderButton(folderName: string): Locator {\n    return this.page.getByTestId(`index-folder-btn-${folderName}`);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/components/AddKeyDialog.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\nimport { KeyType } from '../../../types';\n\n/**\n * Add Key Dialog component\n */\nexport class AddKeyDialog {\n  readonly page: Page;\n\n  // Dialog container\n  readonly container: Locator;\n  readonly title: Locator;\n\n  // Form fields\n  readonly keyTypeSelect: Locator;\n  readonly keyTypeDropdown: Locator;\n  readonly keyNameInput: Locator;\n  readonly ttlInput: Locator;\n\n  // Hash-specific fields (Field, Value, TTL per field)\n  readonly hashFieldInput: Locator;\n  readonly hashValueInput: Locator;\n  readonly hashTtlInput: Locator;\n\n  // String-specific fields (single Value input)\n  readonly stringValueInput: Locator;\n\n  // List-specific fields (Element input)\n  readonly listElementInput: Locator;\n\n  // Set-specific fields (Member input)\n  readonly setMemberInput: Locator;\n\n  // ZSet-specific fields (Member + Score)\n  readonly zsetMemberInput: Locator;\n  readonly zsetScoreInput: Locator;\n\n  // Stream-specific fields (Entry ID, Field, Value)\n  readonly streamEntryIdInput: Locator;\n  readonly streamFieldInput: Locator;\n  readonly streamValueInput: Locator;\n\n  // JSON-specific fields (Monaco editor)\n  readonly jsonValueInput: Locator;\n\n  // Action buttons\n  readonly addItemButton: Locator;\n  readonly removeItemButton: Locator;\n  readonly cancelButton: Locator;\n  readonly addKeyButton: Locator;\n  readonly backButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Dialog container (the add key panel)\n    this.container = page.getByText('New Key').locator('..');\n    this.title = page.getByText('New Key');\n\n    // Form fields - common\n    this.keyTypeSelect = page.getByTestId('select-key-type');\n    this.keyTypeDropdown = page.locator('[role=\"listbox\"]');\n    this.keyNameInput = page.getByPlaceholder('Enter Key Name');\n    this.ttlInput = page.getByPlaceholder('No limit');\n\n    // Hash fields - has Field, Value, TTL per field\n    this.hashFieldInput = page.getByPlaceholder('Enter Field').first();\n    this.hashValueInput = page.getByPlaceholder('Enter Value').first();\n    this.hashTtlInput = page.getByPlaceholder('Enter TTL');\n\n    // String fields - single Value textbox (use testid for specificity)\n    this.stringValueInput = page.getByTestId('string-value');\n\n    // List fields - Element input\n    this.listElementInput = page.getByPlaceholder('Enter Element');\n\n    // Set fields - Member input\n    this.setMemberInput = page.getByPlaceholder('Enter Member');\n\n    // ZSet fields - Member + Score\n    this.zsetMemberInput = page.getByPlaceholder('Enter Member');\n    this.zsetScoreInput = page.getByPlaceholder('Enter Score');\n\n    // Stream fields - Entry ID, Field, Value\n    this.streamEntryIdInput = page.getByPlaceholder('Enter Entry ID');\n    this.streamFieldInput = page.getByPlaceholder('Enter Field').first();\n    this.streamValueInput = page.getByPlaceholder('Enter Value').first();\n\n    // JSON fields - Monaco editor\n    this.jsonValueInput = page.locator('[data-testid=\"json-value\"] textarea, .monaco-editor textarea').first();\n\n    // Action buttons - use testid for specificity\n    this.addItemButton = page.getByRole('button', { name: /add new item/i });\n    this.removeItemButton = page.getByRole('button', { name: /remove item/i });\n    this.cancelButton = page.getByRole('button', { name: 'Cancel' });\n    // Add Key button has different testids per key type, use submit button type\n    this.addKeyButton = page.locator('button[type=\"submit\"][data-testid*=\"add-key\"]');\n    this.backButton = page.getByRole('button', { name: 'Back' });\n  }\n\n  async isVisible(): Promise<boolean> {\n    try {\n      await expect(this.title).toBeVisible({ timeout: 3000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async selectKeyType(type: KeyType): Promise<void> {\n    await this.keyTypeSelect.click();\n    await this.page.getByRole('option', { name: type, exact: true }).click();\n  }\n\n  async fillKeyName(name: string): Promise<void> {\n    await this.keyNameInput.fill(name);\n  }\n\n  async fillTtl(ttl: string): Promise<void> {\n    await this.ttlInput.fill(ttl);\n  }\n\n  async clickAddKey(): Promise<void> {\n    await expect(this.addKeyButton).toBeEnabled();\n    await this.addKeyButton.click();\n  }\n\n  async clickCancel(): Promise<void> {\n    await this.cancelButton.click();\n  }\n\n  // Type-specific fill methods\n  async fillStringValue(value: string): Promise<void> {\n    await this.stringValueInput.fill(value);\n  }\n\n  async fillHashField(field: string, value: string): Promise<void> {\n    await this.hashFieldInput.fill(field);\n    await this.hashValueInput.fill(value);\n  }\n\n  async fillListElement(element: string): Promise<void> {\n    await this.listElementInput.fill(element);\n  }\n\n  async fillSetMember(member: string): Promise<void> {\n    await this.setMemberInput.fill(member);\n  }\n\n  async fillZSetMember(member: string, score: string): Promise<void> {\n    await this.zsetMemberInput.fill(member);\n    await this.zsetScoreInput.fill(score);\n  }\n\n  async fillStreamField(field: string, value: string): Promise<void> {\n    await this.streamFieldInput.fill(field);\n    await this.streamValueInput.fill(value);\n  }\n\n  async fillJsonValue(value: string): Promise<void> {\n    await this.jsonValueInput.fill(value);\n  }\n\n  async clickBack(): Promise<void> {\n    await this.backButton.click();\n  }\n\n  async expectAddKeyEnabled(): Promise<void> {\n    await expect(this.addKeyButton).toBeEnabled();\n  }\n\n  async expectAddKeyDisabled(): Promise<void> {\n    await expect(this.addKeyButton).toBeDisabled();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/components/BulkActionsPanel.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Bulk Actions Panel component - for bulk delete and upload operations\n */\nexport class BulkActionsPanel {\n  readonly page: Page;\n\n  // Panel elements\n  readonly openButton: Locator;\n  readonly closeButton: Locator;\n  readonly content: Locator;\n\n  // Tabs\n  readonly deleteKeysTab: Locator;\n  readonly uploadDataTab: Locator;\n\n  // Delete Keys elements\n  readonly deleteButton: Locator;\n  readonly confirmDeleteButton: Locator;\n  readonly cancelButton: Locator;\n  readonly deleteSummary: Locator;\n  readonly deleteCompletedSummary: Locator;\n  readonly deleteInfo: Locator;\n  readonly downloadReportCheckbox: Locator;\n  readonly statusCompleted: Locator;\n\n  // Upload Data elements\n  readonly fileInput: Locator;\n  readonly uploadContainer: Locator;\n  readonly uploadButton: Locator;\n  readonly uploadStatusCompleted: Locator;\n  readonly uploadSummary: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Panel elements\n    this.openButton = page.getByTestId('btn-bulk-actions');\n    this.closeButton = page.getByTestId('bulk-close-panel');\n    this.content = page.getByTestId('bulk-actions-content');\n\n    // Tabs\n    this.deleteKeysTab = page.getByRole('tab', { name: 'Delete Keys' });\n    this.uploadDataTab = page.getByRole('tab', { name: 'Upload Data' });\n\n    // Delete Keys elements\n    this.deleteButton = page.getByTestId('bulk-action-warning-btn');\n    this.confirmDeleteButton = page.getByTestId('bulk-action-apply-btn');\n    this.cancelButton = page.getByTestId('bulk-action-cancel-btn');\n    this.deleteSummary = page.getByTestId('bulk-delete-summary');\n    this.deleteCompletedSummary = page.getByTestId('bulk-delete-completed-summary');\n    this.deleteInfo = page.getByTestId('bulk-actions-info');\n    this.downloadReportCheckbox = page.getByRole('checkbox', { name: 'Download report' });\n    this.statusCompleted = page.getByTestId('bulk-status-completed');\n\n    // Upload Data elements\n    this.fileInput = page.getByTestId('bulk-upload-file-input');\n    this.uploadContainer = page.getByTestId('bulk-upload-container');\n    this.uploadButton = page.getByTestId('bulk-action-warning-btn');\n    this.uploadStatusCompleted = page.getByTestId('bulk-status-completed');\n    this.uploadSummary = page.getByTestId('bulk-upload-completed-summary');\n  }\n\n  async open(): Promise<void> {\n    await this.openButton.click();\n    await this.content.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  async close(): Promise<void> {\n    await this.closeButton.click();\n    await this.content.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async isOpen(): Promise<boolean> {\n    try {\n      await this.content.waitFor({ state: 'visible', timeout: 2000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async selectDeleteKeysTab(): Promise<void> {\n    await this.deleteKeysTab.click();\n    await expect(this.deleteKeysTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  async selectUploadDataTab(): Promise<void> {\n    await this.uploadDataTab.click();\n    await expect(this.uploadDataTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  async getDeleteSummary(): Promise<string> {\n    return await this.deleteSummary.innerText();\n  }\n\n  async clickDelete(): Promise<void> {\n    await this.deleteButton.click();\n  }\n\n  async confirmDelete(): Promise<void> {\n    await this.confirmDeleteButton.waitFor({ state: 'visible', timeout: 5000 });\n    await this.confirmDeleteButton.click();\n  }\n\n  async clickCancel(): Promise<void> {\n    await this.cancelButton.click();\n  }\n\n  async waitForDeleteComplete(): Promise<void> {\n    // Wait for the status completed message to appear\n    await this.statusCompleted.waitFor({ state: 'visible', timeout: 30000 });\n  }\n\n  async performBulkDelete(): Promise<void> {\n    await this.clickDelete();\n    await this.confirmDelete();\n    await this.waitForDeleteComplete();\n  }\n\n  async getExpectedKeyCount(): Promise<number> {\n    // The expected key count is shown in the bulk delete summary area\n    // Format: \"Expected amount: X keys\" or \"Expected amount: ~X keys\"\n    const expectedText = await this.page.getByText(/Expected amount:/i).textContent();\n    if (!expectedText) return 0;\n    const match = expectedText.match(/Expected amount:\\s*~?(\\d[\\d\\s]*)\\s*keys/i);\n    if (!match) return 0;\n    // Remove spaces from number (e.g., \"1 000\" -> \"1000\")\n    return parseInt(match[1].replace(/\\s/g, ''), 10);\n  }\n\n  async uploadFile(filePath: string): Promise<void> {\n    await this.selectUploadDataTab();\n    await this.fileInput.setInputFiles(filePath);\n  }\n\n  async clickUpload(): Promise<void> {\n    await this.uploadButton.click();\n  }\n\n  async confirmUpload(): Promise<void> {\n    // Same confirmation button as delete\n    await this.confirmDeleteButton.waitFor({ state: 'visible', timeout: 5000 });\n    await this.confirmDeleteButton.click();\n  }\n\n  async waitForUploadComplete(): Promise<void> {\n    // Wait for the \"Action completed\" message to appear\n    await this.page.getByText('Action completed').waitFor({ state: 'visible', timeout: 30000 });\n  }\n\n  async performBulkUpload(filePath: string): Promise<void> {\n    // Select upload tab and upload file\n    await this.uploadFile(filePath);\n    await this.clickUpload();\n    await this.confirmUpload();\n    await this.waitForUploadComplete();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/components/KeyDetails.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Key Details component - displays details for a selected key\n * Used for viewing/editing String, Hash, List, Set, ZSet, Stream, JSON keys\n */\nexport class KeyDetails {\n  readonly page: Page;\n\n  // Container - the right panel showing key details\n  readonly container: Locator;\n\n  // Header elements\n  readonly keyType: Locator;\n  readonly keyName: Locator;\n  readonly keyInfo: Locator;\n  readonly ttlValue: Locator;\n  readonly ttlEditInput: Locator;\n  readonly ttlApplyButton: Locator;\n  readonly ttlCancelButton: Locator;\n\n  // Actions\n  readonly deleteKeyButton: Locator;\n  readonly autoRefreshButton: Locator;\n  readonly backButton: Locator;\n  readonly closeKeyButton: Locator;\n  readonly copyKeyNameButton: Locator;\n\n  // Format dropdown\n  readonly formatDropdown: Locator;\n\n  // String-specific\n  readonly stringValue: Locator;\n  readonly editValueButton: Locator;\n  readonly stringEditTextbox: Locator;\n  readonly applyEditButton: Locator;\n  readonly cancelEditButton: Locator;\n\n  // Hash-specific\n  readonly addFieldsButton: Locator;\n  readonly hashFieldsGrid: Locator;\n\n  // List-specific\n  readonly addElementButton: Locator;\n  readonly removeElementButton: Locator;\n  readonly listGrid: Locator;\n\n  // Set-specific\n  readonly addMembersButton: Locator;\n  readonly setGrid: Locator;\n\n  // ZSet-specific (Sorted Set)\n  readonly zsetGrid: Locator;\n  readonly scoreSortButton: Locator;\n\n  // Stream-specific\n  readonly newEntryButton: Locator;\n  readonly streamDataTab: Locator;\n  readonly consumerGroupsTab: Locator;\n  readonly streamEntries: Locator;\n  readonly newGroupButton: Locator;\n  readonly consumerGroupsGrid: Locator;\n  readonly noConsumerGroupsMessage: Locator;\n\n  // JSON-specific\n  readonly jsonContent: Locator;\n  readonly addJsonFieldButton: Locator;\n  readonly changeEditorTypeButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Container - the key details panel (right side)\n    this.container = page.getByTestId('key-details-header').locator('..').locator('..');\n\n    // Header - key info\n    // Key type badge: data-testid=\"badge-string_\" or \"badge-hash_\" etc.\n    this.keyType = page.getByTestId('key-details-header').locator('p').first();\n    // Key name is in the second paragraph in the header\n    this.keyName = page.getByTestId('key-details-header').locator('p').nth(1);\n    // Key info (size, length, ttl)\n    this.keyInfo = page.getByTestId('key-size-text');\n    // TTL value - displayed as \"TTL:No limit\" or \"TTL:60\" etc.\n    this.ttlValue = page.getByTestId('key-ttl-text');\n    // TTL edit controls\n    this.ttlEditInput = page.getByRole('textbox', { name: /no limit/i });\n    this.ttlApplyButton = page.getByRole('button', { name: 'Apply' });\n    this.ttlCancelButton = page.getByRole('button', { name: 'Cancel editing' });\n\n    // Actions - Back button closes the panel (when key list is collapsed)\n    // Close key button closes the panel (when key list is visible)\n    this.deleteKeyButton = page.getByTestId('delete-key-btn');\n    this.autoRefreshButton = page.getByTestId('key-auto-refresh-config-btn');\n    this.backButton = page.getByTestId('back-right-panel-btn');\n    this.closeKeyButton = page.getByTestId('close-key-btn');\n    this.copyKeyNameButton = page.getByRole('button', { name: 'Copy Key Name' });\n\n    // Format dropdown\n    this.formatDropdown = page.getByTestId('select-format-key-value');\n\n    // String-specific\n    this.stringValue = page.getByTestId('string-value');\n    this.editValueButton = page.getByTestId('edit-key-value-btn');\n    this.stringEditTextbox = page.getByPlaceholder('Enter Value');\n    this.applyEditButton = page.getByTestId('apply-btn');\n    this.cancelEditButton = page.getByTestId('cancel-btn');\n\n    // Hash-specific\n    this.addFieldsButton = page.getByRole('button', { name: 'Add Fields' });\n    this.hashFieldsGrid = page.getByTestId('hash-details');\n\n    // List-specific\n    this.addElementButton = page.getByRole('button', { name: 'Add Elements' });\n    this.removeElementButton = page.getByRole('button', { name: 'Remove Elements' });\n    this.listGrid = page.getByTestId('list-details');\n\n    // Set-specific\n    this.addMembersButton = page.getByRole('button', { name: 'Add Members' });\n    this.setGrid = page.getByTestId('set-details');\n\n    // ZSet-specific (Sorted Set)\n    this.zsetGrid = page.getByTestId('zset-details');\n    this.scoreSortButton = page.getByRole('button', { name: /Score/ });\n\n    // Stream-specific\n    this.newEntryButton = page.getByRole('button', { name: 'New Entry' });\n    this.streamDataTab = page.getByRole('tab', { name: 'Stream Data' });\n    this.consumerGroupsTab = page.getByRole('tab', { name: 'Consumer Groups' });\n    this.streamEntries = page.locator('[data-testid=\"stream-entries-container\"]');\n    this.newGroupButton = page.getByRole('button', { name: 'New Group' });\n    this.consumerGroupsGrid = page.locator('grid').filter({ hasText: /Group Name/ });\n    this.noConsumerGroupsMessage = page.getByText('Your Key has no Consumer Groups available.');\n\n    // JSON-specific\n    this.jsonContent = page.getByTestId('json-details');\n    this.addJsonFieldButton = page.getByRole('button', { name: 'Add field' });\n    this.changeEditorTypeButton = page.getByRole('button', { name: 'Change editor type' });\n  }\n\n  async isVisible(): Promise<boolean> {\n    try {\n      // Check if key details panel is visible by looking for key name\n      await this.page.getByTestId('key-details-header').waitFor({ state: 'visible', timeout: 5000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async waitForKeyDetails(): Promise<void> {\n    await this.page.getByTestId('key-details-header').waitFor({ state: 'visible' });\n  }\n\n  async getKeyType(): Promise<string> {\n    return await this.keyType.innerText();\n  }\n\n  async getKeyName(): Promise<string> {\n    return await this.keyName.innerText();\n  }\n\n  async renameKey(newKeyName: string): Promise<void> {\n    // Click on the key name to enter edit mode\n    await this.keyName.click();\n    // Wait for the edit input to appear\n    const keyNameInput = this.page.getByRole('textbox', { name: 'Enter Key Name' });\n    await keyNameInput.waitFor({ state: 'visible' });\n    // Clear and fill the new key name\n    await keyNameInput.clear();\n    await keyNameInput.fill(newKeyName);\n    // Click Apply button\n    const applyButton = this.page.getByRole('button', { name: 'Apply' });\n    await applyButton.click();\n    // Wait for the edit mode to close\n    await keyNameInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async close(): Promise<void> {\n    // Try to click the back button first (when key list is collapsed)\n    // If not visible, click the close key button (when key list is visible)\n    const backButtonVisible = await this.backButton.isVisible();\n    if (backButtonVisible) {\n      await this.backButton.click();\n    } else {\n      await this.closeKeyButton.click();\n    }\n  }\n\n  async deleteKey(): Promise<void> {\n    await this.deleteKeyButton.click();\n    // Wait for confirmation dialog and confirm\n    await this.page.getByRole('dialog').waitFor({ state: 'visible', timeout: 5000 });\n    await this.page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();\n    // Wait for the key details to close (key was deleted)\n    await this.page.getByTestId('key-details-header').waitFor({ state: 'hidden', timeout: 10000 });\n  }\n\n  async copyKeyName(): Promise<void> {\n    // Hover over the key name to reveal the copy button\n    await this.keyName.hover();\n    // Wait for the copy button to appear and click it\n    await this.copyKeyNameButton.waitFor({ state: 'visible', timeout: 5000 });\n    await this.copyKeyNameButton.click();\n  }\n\n  async isCopyKeyNameButtonVisible(): Promise<boolean> {\n    // Hover over the key name to reveal the copy button\n    await this.keyName.hover();\n    try {\n      await this.copyKeyNameButton.waitFor({ state: 'visible', timeout: 3000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async getTtlValue(): Promise<string> {\n    await this.ttlValue.waitFor({ state: 'visible' });\n    return await this.ttlValue.innerText();\n  }\n\n  async editTtl(ttlSeconds: string): Promise<void> {\n    // Click on TTL to open edit mode\n    await this.ttlValue.click();\n    // Wait for the edit input to appear - use the textbox with \"No limit\" placeholder\n    const ttlInput = this.page.getByRole('textbox', { name: /no limit/i });\n    await ttlInput.waitFor({ state: 'visible' });\n    // Clear and fill the new TTL value\n    await ttlInput.clear();\n    await ttlInput.fill(ttlSeconds);\n    // Click Apply button\n    await this.ttlApplyButton.click();\n    // Wait for the edit mode to close\n    await ttlInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getValueFormat(): Promise<string> {\n    await this.formatDropdown.waitFor({ state: 'visible', timeout: 10000 });\n    // Click on format dropdown to open it and see the selected value\n    await this.formatDropdown.click();\n    // Find the selected option (it will have aria-selected=\"true\")\n    const selectedOption = this.page.locator('[role=\"option\"][aria-selected=\"true\"]');\n    await selectedOption.waitFor({ state: 'visible' });\n    const format = await selectedOption.innerText();\n    // Close the dropdown by pressing Escape\n    await this.page.keyboard.press('Escape');\n    return format;\n  }\n\n  async changeValueFormat(format: string): Promise<void> {\n    // Click on format dropdown to open it\n    await this.formatDropdown.click();\n    // Wait for dropdown options to appear and click the desired format\n    const option = this.page.getByRole('option', { name: format });\n    await option.waitFor({ state: 'visible' });\n    await option.click();\n    // Wait for dropdown to close\n    await option.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  // String methods\n  async getStringValue(): Promise<string> {\n    await this.stringValue.waitFor({ state: 'visible' });\n    return await this.stringValue.innerText();\n  }\n\n  async clickEditValue(): Promise<void> {\n    await this.editValueButton.click();\n  }\n\n  async editStringValue(newValue: string): Promise<void> {\n    // Click edit button to enter edit mode\n    await this.editValueButton.click();\n    // Wait for textbox to appear\n    await this.stringEditTextbox.waitFor({ state: 'visible' });\n    // Clear and fill new value\n    await this.stringEditTextbox.fill(newValue);\n    // Click apply\n    await this.applyEditButton.click();\n    // Wait for edit mode to close (textbox disappears)\n    await this.stringEditTextbox.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async cancelStringEdit(): Promise<void> {\n    await this.cancelEditButton.click();\n    await this.stringEditTextbox.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  // Hash methods\n  async getHashFieldCount(): Promise<number> {\n    const rows = this.hashFieldsGrid.locator('[role=\"row\"]');\n    // Subtract 1 for header row\n    return (await rows.count()) - 1;\n  }\n\n  async clickAddFields(): Promise<void> {\n    await this.addFieldsButton.click();\n  }\n\n  async addHashField(fieldName: string, fieldValue: string): Promise<void> {\n    // Click Add Fields button\n    await this.page.getByRole('button', { name: 'Add Fields' }).click();\n    // Fill in field name and value\n    await this.page.getByPlaceholder('Enter Field').fill(fieldName);\n    await this.page.getByPlaceholder('Enter Value').fill(fieldValue);\n    // Click Save\n    await this.page.getByRole('button', { name: 'Save' }).click();\n    // Wait for the form to close\n    await this.page.getByPlaceholder('Enter Field').waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async editHashField(fieldName: string, newValue: string): Promise<void> {\n    // Click on the row to show edit button\n    const row = this.hashFieldsGrid.locator('[role=\"row\"]').filter({ hasText: fieldName });\n    await row.click();\n    // Click edit button\n    await this.page.getByTestId(`hash_edit-btn-${fieldName}`).click();\n    // Fill new value\n    await this.page.getByPlaceholder('Enter Value').fill(newValue);\n    // Click apply\n    await this.page.getByTestId('apply-btn').click();\n    // Wait for edit mode to close\n    await this.page.getByPlaceholder('Enter Value').waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async deleteHashField(fieldName: string): Promise<void> {\n    // Find the row with the field name\n    const row = this.hashFieldsGrid.locator('[role=\"row\"]').filter({ hasText: fieldName });\n    // Click the remove field button in that row\n    const removeButton = row.getByRole('button', { name: 'Remove field' });\n    await removeButton.click();\n    // Confirm deletion in the dialog - use testid for the confirmation button\n    await this.page.getByTestId(`remove-hash-button-${fieldName}`).click();\n    // Wait for the field to be removed\n    await row.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getHashFieldValue(fieldName: string): Promise<string> {\n    const row = this.hashFieldsGrid.locator('[role=\"row\"]').filter({ hasText: fieldName });\n    // Value is in the second gridcell\n    const valueCell = row.locator('[role=\"gridcell\"]').nth(1);\n    return await valueCell.innerText();\n  }\n\n  async hashFieldExists(fieldName: string): Promise<boolean> {\n    // Look for the field in gridcells (data cells), not in header rows\n    const fieldCell = this.hashFieldsGrid.locator('[role=\"gridcell\"]').filter({ hasText: fieldName });\n    return (await fieldCell.count()) > 0;\n  }\n\n  async searchHashFields(searchTerm: string): Promise<void> {\n    // Click the search button to open search input\n    await this.page.getByTestId('search-button').click();\n    // Fill the search input\n    const searchInput = this.page.getByTestId('search');\n    await searchInput.fill(searchTerm);\n    await searchInput.press('Enter');\n  }\n\n  async clearHashFieldSearch(): Promise<void> {\n    // Click the reset button inside the hash details grid to clear search\n    // The reset button is inside the search input container in the grid header\n    const resetButton = this.hashFieldsGrid.locator('button[title=\"Reset\"]');\n    if (await resetButton.isVisible()) {\n      await resetButton.click();\n    }\n  }\n\n  async isNoResultsMessageVisible(): Promise<boolean> {\n    const noResults = this.page.getByText('No results found.');\n    return await noResults.isVisible();\n  }\n\n  // List methods\n  async getListElementCount(): Promise<number> {\n    // Wait for the grid to be visible\n    await this.listGrid.waitFor({ state: 'visible' });\n    // Get rows from the data rowgroup (not header)\n    const rows = this.listGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    return await rows.count();\n  }\n\n  async getListElements(): Promise<string[]> {\n    await this.listGrid.waitFor({ state: 'visible' });\n    const rows = this.listGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    const count = await rows.count();\n    const elements: string[] = [];\n    for (let i = 0; i < count; i++) {\n      const element = await rows.nth(i).locator('[role=\"gridcell\"]').nth(1).innerText();\n      elements.push(element);\n    }\n    return elements;\n  }\n\n  async clickAddElements(): Promise<void> {\n    await this.addElementButton.click();\n  }\n\n  async clickRemoveElements(): Promise<void> {\n    await this.removeElementButton.click();\n  }\n\n  async addListElement(element: string, position: 'head' | 'tail' = 'tail'): Promise<void> {\n    await this.addElementButton.click();\n    // Select position if not default\n    if (position === 'head') {\n      await this.page.getByRole('combobox').filter({ hasText: 'Push to' }).click();\n      await this.page.getByRole('option', { name: 'Push to head' }).click();\n    }\n    await this.page.getByPlaceholder('Enter Element').fill(element);\n    await this.page.getByRole('button', { name: 'Save' }).click();\n    // Wait for the form to close\n    await this.page.getByPlaceholder('Enter Element').waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async editListElement(index: number, newValue: string): Promise<void> {\n    // Click on the row to show edit button\n    const row = this.listGrid.locator('[role=\"row\"]').filter({ hasText: new RegExp(`^${index}`) });\n    await row.click();\n    // Click edit button using testid pattern: list_edit-btn-{index}\n    await this.page.getByTestId(`list_edit-btn-${index}`).click();\n    // Clear and fill new value using testid pattern: list_value-editor-{index}\n    const textbox = this.page.getByTestId(`list_value-editor-${index}`);\n    await textbox.clear();\n    await textbox.fill(newValue);\n    // Apply changes using testid: apply-btn\n    await this.page.getByTestId('apply-btn').click();\n    // Wait for edit mode to close\n    await textbox.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async removeListElements(count: number, position: 'head' | 'tail' = 'tail'): Promise<void> {\n    await this.removeElementButton.click();\n    // Select position if not default\n    if (position === 'head') {\n      await this.page.getByRole('combobox').filter({ hasText: 'Remove from' }).click();\n      await this.page.getByRole('option', { name: 'Remove from head' }).click();\n    }\n    const countInput = this.page.getByPlaceholder('Enter Count*');\n    await countInput.fill(count.toString());\n    // Wait for the Remove button to be enabled and click it\n    const removeBtn = this.page.getByTestId('remove-elements-btn');\n    await removeBtn.waitFor({ state: 'visible' });\n    await expect(removeBtn).toBeEnabled({ timeout: 5000 });\n    await removeBtn.click();\n    // Confirm in the dialog\n    const confirmBtn = this.page.getByTestId('remove-submit');\n    await confirmBtn.waitFor({ state: 'visible' });\n    await confirmBtn.click();\n    // Wait for the form to close\n    await countInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getListElementByIndex(index: number): Promise<string> {\n    const row = this.listGrid.locator('[role=\"row\"]').filter({ hasText: new RegExp(`^${index}`) });\n    const valueCell = row.locator('[role=\"gridcell\"]').nth(1);\n    return await valueCell.innerText();\n  }\n\n  async listElementExists(elementValue: string): Promise<boolean> {\n    // Look for the element in gridcells (data cells), not in header rows\n    const elementCell = this.listGrid.locator('[role=\"gridcell\"]').filter({ hasText: elementValue });\n    return (await elementCell.count()) > 0;\n  }\n\n  async searchListByIndex(index: string): Promise<void> {\n    // Click the search button to open search input\n    const searchButton = this.listGrid.getByRole('button', { name: 'Search index' });\n    await searchButton.click();\n    // Fill the search input\n    const searchInput = this.listGrid.getByPlaceholder('Search');\n    await searchInput.fill(index);\n    await searchInput.press('Enter');\n  }\n\n  // Set methods\n  async getSetMemberCount(): Promise<number> {\n    await this.setGrid.waitFor({ state: 'visible' });\n    const rows = this.setGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    return await rows.count();\n  }\n\n  async getSetMembers(): Promise<string[]> {\n    await this.setGrid.waitFor({ state: 'visible' });\n    const rows = this.setGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    const count = await rows.count();\n    const members: string[] = [];\n    for (let i = 0; i < count; i++) {\n      const member = await rows.nth(i).locator('[role=\"gridcell\"]').first().innerText();\n      members.push(member);\n    }\n    return members;\n  }\n\n  async clickAddMembers(): Promise<void> {\n    await this.addMembersButton.click();\n  }\n\n  async addSetMember(member: string): Promise<void> {\n    await this.addMembersButton.click();\n    const memberInput = this.page.getByTestId('member-name');\n    await memberInput.fill(member);\n    await this.page.getByTestId('save-members-btn').click();\n    // Wait for the form to close\n    await memberInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async removeSetMember(member: string): Promise<void> {\n    const removeBtn = this.page.getByTestId(`set-remove-btn-${member}-icon`);\n    await removeBtn.click();\n    // Confirm in the dialog - the confirm button has the same testid without -icon\n    const confirmBtn = this.page.getByTestId(`set-remove-btn-${member}`);\n    await confirmBtn.waitFor({ state: 'visible' });\n    await confirmBtn.click();\n    // Wait for the dialog to close\n    await confirmBtn.waitFor({ state: 'hidden', timeout: 5000 });\n    // Wait for the row to be removed from the grid\n    await this.page.getByTestId(`set-remove-btn-${member}-icon`).waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async setMemberExists(memberName: string): Promise<boolean> {\n    // Look for the member in gridcells (data cells), not in header rows\n    const memberCell = this.setGrid.locator('[role=\"gridcell\"]').filter({ hasText: memberName });\n    return (await memberCell.count()) > 0;\n  }\n\n  async searchSetMembers(searchTerm: string): Promise<void> {\n    // For Set, the search input is always visible in the header\n    const searchInput = this.setGrid.getByPlaceholder('Search');\n    await searchInput.fill(searchTerm);\n    await searchInput.press('Enter');\n  }\n\n  // ZSet (Sorted Set) methods\n  async getZSetMemberCount(): Promise<number> {\n    await this.zsetGrid.waitFor({ state: 'visible' });\n    const rows = this.zsetGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    return await rows.count();\n  }\n\n  async getZSetMembers(): Promise<Array<{ member: string; score: string }>> {\n    await this.zsetGrid.waitFor({ state: 'visible' });\n    const rows = this.zsetGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    const count = await rows.count();\n    const members: Array<{ member: string; score: string }> = [];\n    for (let i = 0; i < count; i++) {\n      const member = await rows.nth(i).locator('[role=\"gridcell\"]').nth(0).innerText();\n      const score = await rows.nth(i).locator('[role=\"gridcell\"]').nth(1).innerText();\n      members.push({ member, score });\n    }\n    return members;\n  }\n\n  async clickSortByScore(): Promise<void> {\n    await this.scoreSortButton.click();\n  }\n\n  async addZSetMember(member: string, score: string): Promise<void> {\n    await this.addMembersButton.click();\n    const memberInput = this.page.getByTestId('member-name');\n    const scoreInput = this.page.getByTestId('member-score');\n    await memberInput.fill(member);\n    await scoreInput.fill(score);\n    await this.page.getByTestId('save-members-btn').click();\n    // Wait for the form to close\n    await memberInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async removeZSetMember(member: string): Promise<void> {\n    const removeBtn = this.page.getByTestId(`zset-remove-button-${member}-icon`);\n    await removeBtn.click();\n    // Confirm in the dialog - the confirm button has the same testid without -icon\n    const confirmBtn = this.page.getByTestId(`zset-remove-button-${member}`);\n    await confirmBtn.waitFor({ state: 'visible' });\n    await confirmBtn.click();\n    // Wait for the dialog to close\n    await confirmBtn.waitFor({ state: 'hidden', timeout: 5000 });\n    // Wait for the row to be removed from the grid\n    await this.page.getByTestId(`zset-remove-button-${member}-icon`).waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async editZSetMemberScore(rowIndex: number, newScore: string): Promise<void> {\n    // Click on the score cell to show edit button\n    const rows = this.zsetGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    const row = rows.nth(rowIndex);\n    const scoreCell = row.locator('[role=\"gridcell\"]').nth(1);\n    await scoreCell.click();\n    // Wait for edit button to appear and click it\n    const editButton = this.page.getByRole('button', { name: 'Edit field' });\n    await editButton.waitFor({ state: 'visible', timeout: 5000 });\n    await editButton.click();\n    // Fill new score in the textbox\n    const scoreInput = this.page.getByPlaceholder('Enter Score');\n    await scoreInput.waitFor({ state: 'visible', timeout: 5000 });\n    await scoreInput.clear();\n    await scoreInput.fill(newScore);\n    // Apply changes\n    await this.page.getByTestId('apply-btn').click();\n    // Wait for edit mode to close\n    await scoreInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getZSetMemberScore(rowIndex: number): Promise<string> {\n    const rows = this.zsetGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    const row = rows.nth(rowIndex);\n    const scoreCell = row.locator('[role=\"gridcell\"]').nth(1);\n    return await scoreCell.innerText();\n  }\n\n  async zsetMemberExists(memberName: string): Promise<boolean> {\n    // Look for the member in gridcells (data cells), not in header rows\n    const memberCell = this.zsetGrid.locator('[role=\"gridcell\"]').filter({ hasText: memberName });\n    return (await memberCell.count()) > 0;\n  }\n\n  async searchZSetMembers(searchTerm: string): Promise<void> {\n    // Click the search button to open search input\n    const searchButton = this.zsetGrid.getByRole('button', { name: 'Search name' });\n    await searchButton.click();\n    // Fill the search input\n    const searchInput = this.zsetGrid.getByPlaceholder('Search');\n    await searchInput.fill(searchTerm);\n    await searchInput.press('Enter');\n  }\n\n  async toggleZSetSortOrder(): Promise<void> {\n    // Click the sort button in the Score column header\n    const sortButton = this.page.getByTestId('header-sorting-button');\n    await sortButton.click();\n  }\n\n  async getZSetSortOrder(): Promise<'asc' | 'desc'> {\n    // Check if the sort button shows Arrow Up (ascending) or Arrow Down (descending)\n    const arrowUp = this.zsetGrid.getByRole('button', { name: 'Arrow Up' });\n    if (await arrowUp.isVisible()) {\n      return 'asc';\n    }\n    return 'desc';\n  }\n\n  async getZSetScores(): Promise<string[]> {\n    await this.zsetGrid.waitFor({ state: 'visible' });\n    const rows = this.zsetGrid.locator('[role=\"row\"]').filter({ hasNot: this.page.locator('[role=\"columnheader\"]') });\n    const count = await rows.count();\n    const scores: string[] = [];\n    for (let i = 0; i < count; i++) {\n      const score = await rows.nth(i).locator('[role=\"gridcell\"]').nth(1).innerText();\n      scores.push(score);\n    }\n    return scores;\n  }\n\n  // Stream methods\n  async getStreamEntryCount(): Promise<number> {\n    // Stream entries are displayed differently - look for entry ID elements\n    const entries = this.page.locator('[role=\"button\"]').filter({ hasText: /Entry ID/ });\n    return await entries.count();\n  }\n\n  async clickNewEntry(): Promise<void> {\n    await this.newEntryButton.click();\n  }\n\n  async clickStreamDataTab(): Promise<void> {\n    await this.streamDataTab.click();\n  }\n\n  async clickConsumerGroupsTab(): Promise<void> {\n    await this.consumerGroupsTab.click();\n  }\n\n  async isStreamDataTabSelected(): Promise<boolean> {\n    const selected = await this.streamDataTab.getAttribute('aria-selected');\n    return selected === 'true';\n  }\n\n  async isConsumerGroupsTabSelected(): Promise<boolean> {\n    const selected = await this.consumerGroupsTab.getAttribute('aria-selected');\n    return selected === 'true';\n  }\n\n  async addStreamEntry(fieldName: string, fieldValue: string): Promise<string> {\n    await this.newEntryButton.click();\n    // Entry ID is auto-generated with '*', just fill field and value\n    const fieldInput = this.page.getByTestId('field-name');\n    const valueInput = this.page.getByTestId('field-value');\n    await fieldInput.fill(fieldName);\n    await valueInput.fill(fieldValue);\n    await this.page.getByTestId('save-elements-btn').click();\n    // Wait for the form to close\n    await fieldInput.waitFor({ state: 'hidden', timeout: 5000 });\n    // Return the entry ID (we can't know it in advance since it's auto-generated)\n    return '*';\n  }\n\n  async removeStreamEntry(entryId: string): Promise<void> {\n    const removeBtn = this.page.getByTestId(`remove-entry-button-${entryId}-icon`);\n    await removeBtn.click();\n    // Confirm in the dialog\n    const confirmBtn = this.page.getByTestId(`remove-entry-button-${entryId}`);\n    await confirmBtn.waitFor({ state: 'visible' });\n    await confirmBtn.click();\n    // Wait for the dialog to close\n    await confirmBtn.waitFor({ state: 'hidden', timeout: 5000 });\n    // Wait for the entry to be removed\n    await this.page.getByTestId(`remove-entry-button-${entryId}-icon`).waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getStreamEntryIds(): Promise<string[]> {\n    // Get all entry IDs from the stream\n    const entries = this.page.locator('[data-testid^=\"stream-entry-\"][data-testid$=\"-date\"]');\n    const count = await entries.count();\n    const ids: string[] = [];\n    for (let i = 0; i < count; i++) {\n      const testid = await entries.nth(i).getAttribute('data-testid');\n      if (testid) {\n        // Extract entry ID from testid like \"stream-entry-1747742800051-0-date\"\n        const match = testid.match(/stream-entry-(.+)-date/);\n        if (match) {\n          ids.push(match[1]);\n        }\n      }\n    }\n    return ids;\n  }\n\n  // Consumer Group methods\n  async clickNewGroup(): Promise<void> {\n    await this.newGroupButton.click();\n  }\n\n  async addConsumerGroup(groupName: string, id: string = '$'): Promise<void> {\n    await this.consumerGroupsTab.click();\n    await this.newGroupButton.click();\n    // Fill in the group name\n    const groupNameInput = this.page.getByPlaceholder('Enter Group Name*');\n    await groupNameInput.waitFor({ state: 'visible' });\n    await groupNameInput.fill(groupName);\n    // Fill in the ID (default is $)\n    const idInput = this.page.getByPlaceholder('ID*');\n    await idInput.clear();\n    await idInput.fill(id);\n    // Click Save\n    await this.page.getByRole('button', { name: 'Save' }).click();\n    // Wait for the form to close\n    await groupNameInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getConsumerGroupCount(): Promise<number> {\n    // Count rows in the consumer groups grid (excluding header)\n    const rows = this.page.locator('[role=\"row\"]').filter({ hasText: /^\\d+$/ });\n    return await rows.count();\n  }\n\n  async isConsumerGroupVisible(groupName: string): Promise<boolean> {\n    // Look for the group name in a gridcell\n    const groupCell = this.page.getByRole('gridcell', { name: groupName });\n    try {\n      await groupCell.waitFor({ state: 'visible', timeout: 5000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async isNoConsumerGroupsMessageVisible(): Promise<boolean> {\n    try {\n      await this.noConsumerGroupsMessage.waitFor({ state: 'visible', timeout: 3000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async clickConsumerGroup(groupName: string): Promise<void> {\n    const groupRow = this.page.getByRole('row', { name: new RegExp(groupName) });\n    await groupRow.click();\n  }\n\n  async getConsumerGroupNames(): Promise<string[]> {\n    // Get all group names from the consumer groups grid\n    const groupCells = this.page.locator('[role=\"gridcell\"]').filter({ hasText: /^[a-zA-Z0-9_-]+$/ });\n    const count = await groupCells.count();\n    const names: string[] = [];\n    for (let i = 0; i < count; i++) {\n      const text = await groupCells.nth(i).innerText();\n      if (text && !text.match(/^\\d+$/)) {\n        names.push(text);\n      }\n    }\n    return names;\n  }\n\n  // JSON methods\n  async isJsonContentVisible(): Promise<boolean> {\n    // JSON content is displayed in the json-details container\n    try {\n      await this.jsonContent.waitFor({ state: 'visible', timeout: 5000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  async clickAddJsonField(): Promise<void> {\n    await this.addJsonFieldButton.click();\n  }\n\n  async clickChangeEditorType(): Promise<void> {\n    await this.changeEditorTypeButton.click();\n  }\n\n  async getJsonEditButtons(): Promise<number> {\n    const editButtons = this.page.getByRole('button', { name: 'Edit field' });\n    return await editButtons.count();\n  }\n\n  async getJsonRemoveButtons(): Promise<number> {\n    const removeButtons = this.page.getByRole('button', { name: 'Remove field' });\n    return await removeButtons.count();\n  }\n\n  async addJsonField(key: string, value: string): Promise<void> {\n    // Get initial count before adding\n    const initialCount = await this.page.getByTestId('json-scalar-value').count();\n\n    await this.addJsonFieldButton.click();\n    const keyInput = this.page.getByTestId('json-key');\n    const valueInput = this.page.getByTestId('json-value');\n    await keyInput.waitFor({ state: 'visible' });\n    await keyInput.fill(key);\n    await valueInput.fill(value);\n    await this.page.getByTestId('apply-btn').click();\n\n    // Wait for the new field to appear (count should increase)\n    await this.page.waitForFunction(\n      (expectedCount) => {\n        const elements = document.querySelectorAll('[data-testid=\"json-scalar-value\"]');\n        return elements.length > expectedCount;\n      },\n      initialCount,\n      { timeout: 5000 },\n    );\n  }\n\n  async removeJsonField(): Promise<void> {\n    // Click the first remove button\n    const removeBtn = this.page.getByRole('button', { name: 'Remove field' }).first();\n    await removeBtn.click();\n    // Confirm in the dialog - the button is labeled \"Remove\"\n    const dialog = this.page.getByRole('dialog');\n    await dialog.waitFor({ state: 'visible' });\n    const confirmBtn = dialog.getByRole('button', { name: 'Remove' });\n    await confirmBtn.click();\n    // Wait for the dialog to close\n    await dialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getJsonFieldCount(): Promise<number> {\n    // Count the number of JSON scalar values\n    const fields = this.page.getByTestId('json-scalar-value');\n    return await fields.count();\n  }\n\n  async editJsonValue(fieldIndex: number, newValue: string): Promise<void> {\n    // Click on the JSON scalar value to enter edit mode\n    const scalarValues = this.page.getByTestId('json-scalar-value');\n    const targetValue = scalarValues.nth(fieldIndex);\n    await targetValue.click();\n    // Fill new value in the textbox\n    const valueInput = this.page.getByPlaceholder('Enter JSON value');\n    await valueInput.waitFor({ state: 'visible', timeout: 5000 });\n    await valueInput.clear();\n    await valueInput.fill(newValue);\n    // Apply changes\n    await this.page.getByTestId('apply-btn').click();\n    // Wait for edit mode to close\n    await valueInput.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getJsonValue(fieldIndex: number): Promise<string> {\n    const scalarValues = this.page.getByTestId('json-scalar-value');\n    const targetValue = scalarValues.nth(fieldIndex);\n    return await targetValue.innerText();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/components/KeyList.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\nimport { KeyType } from '../../../types';\n\n/**\n * Key List component (left panel in Browser)\n */\nexport class KeyList {\n  readonly page: Page;\n\n  // Filter controls\n  readonly filterByNameButton: Locator;\n  readonly searchByValuesButton: Locator;\n  readonly keyTypeFilter: Locator;\n  readonly searchInput: Locator;\n  readonly searchButton: Locator;\n  readonly resetFilterButton: Locator;\n\n  // View controls\n  readonly listViewButton: Locator;\n  readonly treeViewButton: Locator;\n  readonly columnsButton: Locator;\n  readonly refreshButton: Locator;\n  readonly treeSettingsButton: Locator;\n\n  // Results info\n  readonly resultsCount: Locator;\n  readonly totalCount: Locator;\n  readonly scannedCount: Locator;\n  readonly lastRefresh: Locator;\n  readonly scanMoreButton: Locator;\n\n  // Key list container\n  readonly keyListContainer: Locator;\n  readonly container: Locator;\n  readonly noKeysMessage: Locator;\n\n  // Index selector (Redisearch mode)\n  readonly indexSelector: Locator;\n\n  // Key type filter dropdown\n  readonly keyTypeFilterDropdown: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Filter controls - use testid for specificity\n    this.filterByNameButton = page.getByRole('button', { name: /filter by key name/i });\n    this.searchByValuesButton = page.getByRole('button', { name: /search by values/i });\n    this.keyTypeFilter = page.getByTestId('select-filter-key-type');\n    this.keyTypeFilterDropdown = page.locator('[role=\"listbox\"]');\n    this.searchInput = page.getByPlaceholder('Filter by Key Name or Pattern');\n    this.searchButton = page.getByTestId('search-btn');\n    this.resetFilterButton = page.getByTestId('reset-filter-btn');\n\n    // Index selector (Redisearch mode)\n    this.indexSelector = page.getByTestId('select-search-mode');\n\n    // View controls\n    this.listViewButton = page.getByTestId('view-type-browser-btn');\n    this.treeViewButton = page.getByTestId('view-type-list-btn');\n    this.columnsButton = page.getByRole('button', { name: 'columns' });\n    this.refreshButton = page.getByRole('button', { name: /refresh/i }).first();\n    this.treeSettingsButton = page.getByTestId('tree-view-settings-btn');\n\n    // Results info\n    this.resultsCount = page.getByText(/Results:/);\n    this.totalCount = page.getByText(/Total:/);\n    this.scannedCount = page.getByText(/Scanned/);\n    this.lastRefresh = page.getByText(/Last refresh:/);\n    this.scanMoreButton = page.getByTestId('scan-more');\n\n    // Key list container — matches new panel, legacy list view, or legacy tree view\n    this.keyListContainer = page.locator(\n      '[data-testid=\"keys-browser-panel\"], [data-testid=\"keyList-table\"], [data-testid=\"virtual-tree\"]',\n    );\n    this.container = page.locator(\n      '[data-testid=\"keys-browser-panel\"], [data-testid=\"keyList-table\"], [data-testid=\"virtual-tree\"]',\n    );\n    this.noKeysMessage = page.getByText(/no keys/i);\n  }\n\n  /**\n   * Wait for keys to load\n   * Handles List view (Total:), Tree view (Results:), and empty database\n   */\n  async waitForKeysLoaded(timeout = 30000): Promise<void> {\n    // Wait for either:\n    // - \"Total:\" (List view with keys)\n    // - \"Results:\" (Tree view or filtered results)\n    // - \"Let's start working\" (empty database)\n    await expect(this.page.getByText(/Total:|Results:|Let's start working/).first()).toBeVisible({ timeout });\n  }\n\n  /**\n   * Search for keys by pattern\n   */\n  async searchKeys(pattern: string): Promise<void> {\n    await this.searchInput.fill(pattern);\n    await this.searchButton.click();\n    // Wait for \"Results:\" to appear (indicates filter is applied)\n    await this.resultsCount.waitFor({ state: 'visible', timeout: 10000 });\n  }\n\n  /**\n   * Clear search by clicking the reset filter button\n   */\n  async clearSearch(): Promise<void> {\n    await this.resetFilterButton.click();\n    // Wait for reset button to disappear (indicates filter is cleared)\n    await this.resetFilterButton.waitFor({ state: 'hidden', timeout: 10000 });\n  }\n\n  /**\n   * Filter by key type\n   */\n  async filterByType(type: KeyType | 'All Key Types'): Promise<void> {\n    await this.keyTypeFilter.click();\n    // Wait for dropdown to appear\n    await this.keyTypeFilterDropdown.waitFor({ state: 'visible' });\n    // Use exact match for type to avoid \"Set\" matching \"Sorted Set\"\n    await this.page.getByRole('option', { name: type, exact: true }).click();\n  }\n\n  /**\n   * Click scan more button to load more keys\n   */\n  async scanMore(): Promise<void> {\n    await this.scanMoreButton.click();\n  }\n\n  /**\n   * Check if scan more button is visible\n   */\n  async isScanMoreVisible(): Promise<boolean> {\n    try {\n      await this.scanMoreButton.waitFor({ state: 'visible', timeout: 3000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Get scanned count text (e.g., \"Scanned 1 000 / 3 274\")\n   */\n  async getScannedCountText(): Promise<string> {\n    await this.scannedCount.waitFor({ state: 'visible' });\n    return await this.scannedCount.innerText();\n  }\n\n  /**\n   * Switch to list view\n   */\n  async switchToListView(): Promise<void> {\n    await this.listViewButton.click();\n  }\n\n  /**\n   * Switch to tree view\n   */\n  async switchToTreeView(): Promise<void> {\n    await this.treeViewButton.click();\n  }\n\n  /**\n   * Click on a key by name\n   */\n  async clickKey(keyName: string): Promise<void> {\n    // Try grid row first (list view), then treeitem (tree view)\n    const gridRow = this.page.getByRole('row', { name: new RegExp(keyName) });\n    const treeItem = this.page.getByRole('treeitem', { name: new RegExp(keyName) });\n\n    if (await gridRow.isVisible()) {\n      await gridRow.click();\n    } else {\n      await treeItem.click();\n    }\n  }\n\n  /**\n   * Select a key in tree view by expanding its folder and clicking the leaf.\n   * @param keyName full key name (e.g. `prefix:key1`)\n   * @param delimiter tree delimiter (default `:`)\n   */\n  async selectKeyInTree(keyName: string, delimiter = ':'): Promise<void> {\n    const delimiterIndex = keyName.lastIndexOf(delimiter);\n    const leafName = keyName.substring(delimiterIndex + delimiter.length) || keyName;\n    const leafNode = this.container.getByRole('treeitem').filter({ hasText: leafName });\n\n    if (delimiterIndex !== -1) {\n      const folder = keyName.substring(0, delimiterIndex);\n      const collapsed = this.page.getByTestId(`node-item_${folder}`);\n      const expanded = this.page.getByTestId(`node-item_${folder}--expanded`);\n\n      await expanded.or(collapsed).waitFor({ state: 'visible' });\n\n      // VirtualTree may auto-expand single-child folders while keeping\n      // the collapsed testid. Only click to expand when the leaf is not\n      // already visible.\n      const leafAlreadyVisible = await leafNode.isVisible().catch(() => false);\n\n      if (!leafAlreadyVisible && (await collapsed.isVisible())) {\n        await collapsed.click();\n      }\n    }\n\n    await leafNode.click();\n  }\n\n  /**\n   * Check if key exists in list\n   * Handles both List view (grid) and Tree view (treeitem)\n   */\n  async keyExists(keyName: string, timeout = 5000): Promise<boolean> {\n    try {\n      // Escape special regex characters in key name\n      const escapedKeyName = keyName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n      // Try grid cell (list view) - look for exact key name in gridcell\n      const gridCell = this.page.getByRole('gridcell', { name: keyName });\n\n      // Try treeitem (tree view) - key name appears in the treeitem accessible name\n      const treeItem = this.page.getByRole('treeitem', { name: new RegExp(escapedKeyName) });\n\n      const keyElement = gridCell.or(treeItem);\n      await keyElement.waitFor({ state: 'visible', timeout });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Get key row locator by name\n   * Returns a locator that can be used for assertions (visible/not visible)\n   */\n  getKeyRow(keyName: string): Locator {\n    // Escape special regex characters in key name\n    const escapedKeyName = keyName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n    // Return locator for grid cell (list view) or treeitem (tree view)\n    // Use or() to combine both selectors\n    return this.page\n      .getByRole('gridcell', { name: keyName })\n      .or(this.page.getByRole('treeitem', { name: new RegExp(escapedKeyName) }));\n  }\n\n  /**\n   * Get results count text\n   */\n  async getResultsCountText(): Promise<string | null> {\n    return this.resultsCount.textContent();\n  }\n\n  /**\n   * Get total count text\n   */\n  async getTotalCountText(): Promise<string | null> {\n    return this.totalCount.textContent();\n  }\n\n  /**\n   * Get key count as number\n   */\n  async getKeyCount(): Promise<number> {\n    const text = await this.getResultsCountText();\n    if (!text) return 0;\n    // Extract number from \"Results: X.\" or \"Total: X\"\n    const match = text.match(/(\\d+)/);\n    return match ? parseInt(match[1], 10) : 0;\n  }\n\n  /**\n   * Refresh the key list\n   */\n  async refresh(): Promise<void> {\n    await this.refreshButton.click();\n  }\n\n  /**\n   * Check if no keys message is visible\n   */\n  async isNoKeysMessageVisible(): Promise<boolean> {\n    try {\n      // Check for various \"no keys\" indicators\n      // Use first() to handle multiple matches\n      const noKeysText = this.page.getByText(/no keys|no results found|0 keys/i).first();\n      const totalZero = this.page.getByText(/Total:\\s*0/).first();\n      const resultsZero = this.page.getByText(/Results:\\s*0/).first();\n\n      const noKeysVisible = await noKeysText.isVisible().catch(() => false);\n      const totalZeroVisible = await totalZero.isVisible().catch(() => false);\n      const resultsZeroVisible = await resultsZero.isVisible().catch(() => false);\n\n      return noKeysVisible || totalZeroVisible || resultsZeroVisible;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Check if tree view is active\n   */\n  async isTreeViewActive(): Promise<boolean> {\n    const isActive = await this.treeViewButton.getAttribute('class');\n    return isActive?.includes('active') || false;\n  }\n\n  /**\n   * Check if list view is active\n   */\n  async isListViewActive(): Promise<boolean> {\n    const isActive = await this.listViewButton.getAttribute('class');\n    return isActive?.includes('active') || false;\n  }\n\n  /**\n   * Open tree view settings dialog\n   */\n  async openTreeViewSettings(): Promise<void> {\n    await this.treeSettingsButton.click();\n    // Wait for dialog to appear\n    await this.page.getByRole('dialog').waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Hover over a folder node in the tree view (expanded or collapsed)\n   */\n  async hoverFolderNode(folderName: string): Promise<void> {\n    const collapsed = this.page.getByTestId(`node-item_${folderName}`);\n    const expanded = this.page.getByTestId(`node-item_${folderName}--expanded`);\n    const folderNode = expanded.or(collapsed);\n    await folderNode.hover();\n  }\n\n  /**\n   * Get folder by name in tree view\n   */\n  getFolderByName(folderName: string): Locator {\n    return this.page.getByRole('treeitem', { name: new RegExp(`Folder ${folderName}`) });\n  }\n\n  /**\n   * Expand folder in tree view\n   */\n  async expandFolder(folderName: string): Promise<void> {\n    const folder = this.getFolderByName(folderName);\n    await folder.click();\n    // Wait for chevron to change to down\n    await this.page\n      .getByRole('treeitem', { name: new RegExp(`Chevron Down.*${folderName}`) })\n      .waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Collapse folder in tree view\n   */\n  async collapseFolder(folderName: string): Promise<void> {\n    const folder = this.getFolderByName(folderName);\n    await folder.click();\n    // Wait for chevron to change to right\n    await this.page\n      .getByRole('treeitem', { name: new RegExp(`Chevron Right.*${folderName}`) })\n      .waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Check if folder is expanded\n   */\n  async isFolderExpanded(folderName: string): Promise<boolean> {\n    const expandedFolder = this.page.getByRole('treeitem', { name: new RegExp(`Chevron Down.*${folderName}`) });\n    return expandedFolder.isVisible();\n  }\n\n  /**\n   * Get folder percentage text\n   */\n  async getFolderPercentage(folderName: string): Promise<string | null> {\n    const folder = this.getFolderByName(folderName);\n    const percentageElement = folder\n      .locator('div')\n      .filter({ hasText: /\\d+%|<1%/ })\n      .first();\n    return percentageElement.textContent();\n  }\n\n  /**\n   * Get folder count\n   */\n  async getFolderCount(folderName: string): Promise<string | null> {\n    const folder = this.getFolderByName(folderName);\n    // The count is the last number in the folder row\n    const countElement = folder.locator('div').last();\n    return countElement.textContent();\n  }\n\n  /**\n   * Get current delimiter from tree view settings\n   */\n  async getCurrentDelimiter(): Promise<string> {\n    const delimiterChip = this.page.getByRole('dialog').locator('[class*=\"chip\"]').first();\n    const text = await delimiterChip.textContent();\n    return text?.replace('Remove', '').trim() || '';\n  }\n\n  /**\n   * Add delimiter in tree view settings\n   */\n  async addDelimiter(delimiter: string): Promise<void> {\n    const delimiterInput = this.page.getByRole('textbox', { name: 'Delimiter' });\n    await delimiterInput.fill(delimiter);\n    await delimiterInput.press('Enter');\n  }\n\n  /**\n   * Remove delimiter in tree view settings\n   */\n  async removeDelimiter(delimiter: string): Promise<void> {\n    const delimiterChip = this.page.getByRole('dialog').locator(`[class*=\"chip\"]`).filter({ hasText: delimiter });\n    await delimiterChip.getByRole('button', { name: 'Remove' }).click();\n  }\n\n  /**\n   * Apply tree view settings\n   */\n  async applyTreeViewSettings(): Promise<void> {\n    await this.page.getByRole('button', { name: 'Apply' }).click();\n    // Wait for dialog to close\n    await this.page.getByRole('dialog').waitFor({ state: 'hidden' });\n  }\n\n  /**\n   * Cancel tree view settings\n   */\n  async cancelTreeViewSettings(): Promise<void> {\n    await this.page.getByRole('button', { name: 'Cancel' }).click();\n    // Wait for dialog to close\n    await this.page.getByRole('dialog').waitFor({ state: 'hidden' });\n  }\n\n  /**\n   * Change sort by option in tree view settings\n   */\n  async changeSortBy(option: string): Promise<void> {\n    const sortByDropdown = this.page.getByRole('combobox', { name: 'Sort by' });\n    await sortByDropdown.click();\n    await this.page.getByRole('option', { name: option }).click();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/components/MakeSearchableModal.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * \"Make this data searchable\" modal\n *\n * Shown when the user clicks \"Make searchable\" on a key or\n * \"Index\" on a folder node in the browser tree.\n */\nexport class MakeSearchableModal {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly heading: Locator;\n  readonly continueButton: Locator;\n  readonly cancelButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByRole('dialog');\n    this.heading = this.container.getByText('Make this data searchable');\n    this.continueButton = this.container.getByRole('button', { name: 'Continue' });\n    this.cancelButton = this.container.getByRole('button', { name: 'Cancel' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/components/index.ts",
    "content": "export { AddKeyDialog } from './AddKeyDialog';\nexport { BulkActionsPanel } from './BulkActionsPanel';\nexport { KeyDetails } from './KeyDetails';\nexport { KeyList } from './KeyList';\nexport { MakeSearchableModal } from './MakeSearchableModal';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/browser/index.ts",
    "content": "export { BrowserPage } from './BrowserPage';\nexport { AddKeyDialog, KeyList } from './components';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/cli/CliPanel.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * CLI Panel component\n * Handles the CLI panel at the bottom of the page\n */\nexport class CliPanel {\n  readonly page: Page;\n  readonly container: Locator;\n  readonly expandButton: Locator;\n  readonly hideButton: Locator;\n  readonly closeButton: Locator;\n  readonly commandInput: Locator;\n  readonly successOutput: Locator;\n  readonly errorOutput: Locator;\n  readonly commandWrapper: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.container = page.getByTestId('cli').first();\n    this.expandButton = page.getByTestId('expand-cli');\n    this.hideButton = page.getByTestId('hide-cli');\n    this.closeButton = page.getByTestId('close-cli');\n    this.commandInput = page.getByTestId('cli-command');\n    this.successOutput = page.getByTestId('cli-output-response-success');\n    this.errorOutput = page.getByTestId('cli-output-response-fail');\n    this.commandWrapper = page.getByTestId('cli-command-wrapper');\n  }\n\n  /**\n   * Open the CLI panel\n   */\n  async open(): Promise<void> {\n    const isVisible = await this.hideButton.isVisible();\n    if (!isVisible) {\n      await this.expandButton.click();\n      await this.hideButton.waitFor({ state: 'visible' });\n    }\n  }\n\n  /**\n   * Close the CLI panel\n   */\n  async close(): Promise<void> {\n    const isVisible = await this.closeButton.isVisible();\n    if (isVisible) {\n      await this.closeButton.click();\n    }\n  }\n\n  /**\n   * Hide the CLI panel (minimize)\n   */\n  async hide(): Promise<void> {\n    const isVisible = await this.hideButton.isVisible();\n    if (isVisible) {\n      await this.hideButton.click();\n    }\n  }\n\n  /**\n   * Check if CLI panel is open\n   */\n  async isOpen(): Promise<boolean> {\n    return this.hideButton.isVisible();\n  }\n\n  /**\n   * Execute a command in the CLI\n   */\n  async executeCommand(command: string): Promise<void> {\n    await this.commandInput.focus();\n    await this.page.keyboard.type(command);\n    await this.page.keyboard.press('Enter');\n  }\n\n  /**\n   * Execute a command and wait for any response (success or error) to appear.\n   * Callers should assert on `successOutput` / `errorOutput` after this returns.\n   */\n  async executeCommandAndWait(command: string): Promise<void> {\n    const successBefore = await this.successOutput.count();\n    const errorBefore = await this.errorOutput.count();\n    await this.executeCommand(command);\n    await expect(this.successOutput.nth(successBefore).or(this.errorOutput.nth(errorBefore))).toBeVisible({\n      timeout: 10_000,\n    });\n  }\n\n  /**\n   * Type a command in the CLI without executing it\n   * This triggers the Command Helper integration\n   */\n  async typeCommand(command: string): Promise<void> {\n    await this.commandInput.focus();\n    // Type the command character by character to trigger updates\n    await this.page.keyboard.type(command, { delay: 50 });\n  }\n\n  /**\n   * Clear the current command input\n   */\n  async clearInput(): Promise<void> {\n    await this.commandInput.focus();\n    // Select all and delete\n    await this.page.keyboard.press('Control+a');\n    await this.page.keyboard.press('Backspace');\n  }\n\n  /**\n   * Get the current text in the command input (ContentEditable)\n   */\n  async getInputText(): Promise<string> {\n    return this.commandInput.innerText();\n  }\n\n  /**\n   * Get the CLI output text\n   */\n  async getOutput(): Promise<string> {\n    return this.container.innerText();\n  }\n\n  /**\n   * Get the text of the last error response\n   */\n  async getLastErrorResponse(): Promise<string> {\n    const count = await this.errorOutput.count();\n    if (count === 0) return '';\n    return this.errorOutput.nth(count - 1).innerText();\n  }\n\n  /**\n   * Check if output contains specific text\n   */\n  async outputContains(text: string): Promise<boolean> {\n    const output = await this.getOutput();\n    return output.includes(text);\n  }\n\n  /**\n   * Wait for specific text in output\n   */\n  async waitForOutput(text: string, timeout = 5000): Promise<void> {\n    await this.page.waitForFunction(\n      ({ selector, expectedText }) => {\n        const element = document.querySelector(selector);\n        return element?.textContent?.includes(expectedText);\n      },\n      { selector: '[data-testid=\"cli\"]', expectedText: text },\n      { timeout },\n    );\n  }\n\n  /**\n   * Press ArrowUp to navigate command history (older)\n   */\n  async pressArrowUp(): Promise<void> {\n    await this.commandInput.focus();\n    await this.page.keyboard.press('ArrowUp');\n  }\n\n  /**\n   * Press ArrowDown to navigate command history (newer)\n   */\n  async pressArrowDown(): Promise<void> {\n    await this.commandInput.focus();\n    await this.page.keyboard.press('ArrowDown');\n  }\n\n  /**\n   * Press Tab to trigger command completion\n   */\n  async pressTab(): Promise<void> {\n    await this.commandInput.focus();\n    await this.page.keyboard.press('Tab');\n  }\n\n  /**\n   * Clear the CLI output\n   */\n  async clear(): Promise<void> {\n    await this.executeCommand('CLEAR');\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/cli/index.ts",
    "content": "export { CliPanel } from './CliPanel';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/command-helper/CommandHelperPanel.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Command Helper Panel component\n * Handles the Command Helper panel at the bottom of the page\n */\nexport class CommandHelperPanel {\n  readonly page: Page;\n  readonly container: Locator;\n  readonly innerContainer: Locator;\n  readonly expandButton: Locator;\n  readonly hideButton: Locator;\n  readonly closeButton: Locator;\n  readonly searchInput: Locator;\n  readonly filterDropdown: Locator;\n  readonly defaultText: Locator;\n  readonly commandTitle: Locator;\n  readonly commandSummary: Locator;\n  readonly searchResultTitles: Locator;\n  readonly backToListButton: Locator;\n  readonly readMoreLink: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.container = page.getByTestId('command-helper');\n    this.innerContainer = page.getByTestId('cli-helper');\n    this.expandButton = page.getByTestId('expand-command-helper');\n    this.hideButton = page.getByTestId('hide-command-helper');\n    this.closeButton = page.getByTestId('close-command-helper');\n    this.searchInput = page.getByTestId('cli-helper-search');\n    this.filterDropdown = page.getByTestId('select-filter-group-type');\n    this.defaultText = page.getByTestId('cli-helper-default');\n    this.commandTitle = page.getByTestId('cli-helper-title');\n    this.commandSummary = page.getByTestId('cli-helper-summary');\n    this.searchResultTitles = page.getByTestId(/cli-helper-output-title/);\n    this.backToListButton = page.getByTestId('cli-helper-back-to-list-btn');\n    this.readMoreLink = page.getByTestId('read-more');\n  }\n\n  /**\n   * Open the Command Helper panel\n   */\n  async open(): Promise<void> {\n    const isVisible = await this.hideButton.isVisible();\n    if (!isVisible) {\n      await this.expandButton.click();\n      await this.hideButton.waitFor({ state: 'visible' });\n    }\n  }\n\n  /**\n   * Close the Command Helper panel (removes it completely)\n   */\n  async close(): Promise<void> {\n    const isVisible = await this.closeButton.isVisible();\n    if (isVisible) {\n      await this.closeButton.click();\n    }\n  }\n\n  /**\n   * Hide the Command Helper panel (minimize)\n   */\n  async hide(): Promise<void> {\n    const isVisible = await this.hideButton.isVisible();\n    if (isVisible) {\n      await this.hideButton.click();\n    }\n  }\n\n  /**\n   * Check if Command Helper panel is open\n   */\n  async isOpen(): Promise<boolean> {\n    return this.hideButton.isVisible();\n  }\n\n  /**\n   * Search for a command\n   */\n  async search(query: string): Promise<void> {\n    await this.searchInput.fill(query);\n  }\n\n  /**\n   * Clear the search input\n   */\n  async clearSearch(): Promise<void> {\n    await this.searchInput.clear();\n  }\n\n  /**\n   * Filter commands by category/group type\n   * @param groupType - The internal group type value (e.g., 'string', 'hash', 'set')\n   */\n  async filterByCategory(groupType: string): Promise<void> {\n    await this.filterDropdown.click();\n    // Use data-test-subj attribute for robust selection (from CHSearchFilter.tsx)\n    // The dropdown options have data-test-subj=\"filter-option-group-type-{value}\"\n    await this.page.locator(`[data-test-subj=\"filter-option-group-type-${groupType}\"]`).click();\n  }\n\n  /**\n   * Select a command from search results\n   */\n  async selectCommand(commandName: string): Promise<void> {\n    await this.page.getByTestId(`cli-helper-output-title-${commandName}`).click();\n  }\n\n  /**\n   * Go back to search results list from command details\n   */\n  async backToList(): Promise<void> {\n    await this.backToListButton.click();\n  }\n\n  /**\n   * Get search result count\n   */\n  async getSearchResultCount(): Promise<number> {\n    return this.searchResultTitles.count();\n  }\n\n  /**\n   * Get the command title text\n   */\n  async getCommandTitle(): Promise<string> {\n    return this.commandTitle.innerText();\n  }\n\n  /**\n   * Get the command summary text\n   */\n  async getCommandSummary(): Promise<string> {\n    return this.commandSummary.innerText();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/command-helper/index.ts",
    "content": "export { CommandHelperPanel } from './CommandHelperPanel';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/components/BottomPanel.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Bottom Panel component\n * Common bottom panel shown on all database instance pages\n * Contains CLI, Command Helper, and Profiler buttons\n */\nexport class BottomPanel {\n  readonly page: Page;\n\n  // Panel toggle buttons\n  readonly cliButton: Locator;\n  readonly commandHelperButton: Locator;\n  readonly profilerButton: Locator;\n\n  // Feedback link\n  readonly feedbackLink: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Bottom panel buttons - using text content with parent element\n    this.cliButton = page.getByText('CLI').locator('..');\n    this.commandHelperButton = page.getByText('Command Helper').locator('..');\n    this.profilerButton = page.getByText('Profiler').locator('..');\n\n    // Feedback link\n    this.feedbackLink = page.getByRole('link', { name: 'Let us know what you think' });\n  }\n\n  /**\n   * Open CLI panel\n   */\n  async openCli(): Promise<void> {\n    await this.cliButton.click();\n  }\n\n  /**\n   * Open Command Helper panel\n   */\n  async openCommandHelper(): Promise<void> {\n    await this.commandHelperButton.click();\n  }\n\n  /**\n   * Open Profiler panel\n   */\n  async openProfiler(): Promise<void> {\n    await this.profilerButton.click();\n  }\n\n  /**\n   * Check if CLI button is visible\n   */\n  async isCliButtonVisible(): Promise<boolean> {\n    return this.cliButton.isVisible();\n  }\n\n  /**\n   * Check if Command Helper button is visible\n   */\n  async isCommandHelperButtonVisible(): Promise<boolean> {\n    return this.commandHelperButton.isVisible();\n  }\n\n  /**\n   * Check if Profiler button is visible\n   */\n  async isProfilerButtonVisible(): Promise<boolean> {\n    return this.profilerButton.isVisible();\n  }\n\n  /**\n   * Click on feedback link\n   */\n  async openFeedback(): Promise<void> {\n    await this.feedbackLink.click();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/components/InstanceHeader.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Instance Header component\n * Common header shown on all database instance pages (Browser, Workbench, Analyze, Pub/Sub)\n * Contains database name, connection info, and stats (CPU, memory, keys, etc.)\n */\nexport class InstanceHeader {\n  readonly page: Page;\n\n  // Breadcrumb navigation\n  readonly databasesButton: Locator;\n  readonly databaseNameDropdown: Locator;\n  readonly databaseName: Locator;\n  readonly logicalDatabaseButton: Locator;\n  readonly databaseInfoButton: Locator;\n\n  // Database stats\n  readonly cpuUsage: Locator;\n  readonly commandsPerSec: Locator;\n  readonly totalMemory: Locator;\n  readonly totalKeys: Locator;\n  readonly connectedClients: Locator;\n\n  // Header actions\n  readonly refreshButton: Locator;\n  readonly autoRefreshButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Breadcrumb navigation\n    this.databasesButton = page.getByRole('button', { name: 'Redis Databases' });\n    this.databaseNameDropdown = page.locator('[data-testid=\"db-name-dropdown\"]');\n    this.databaseName = page.locator('[data-testid=\"db-name\"], [data-testid=\"database-name\"]');\n    this.logicalDatabaseButton = page.getByRole('button', { name: /^db\\d+$/i });\n    this.databaseInfoButton = page.getByRole('img', { name: 'Info' });\n\n    // Database stats - using approximate locators based on UI structure\n    this.cpuUsage = page.locator('[data-testid=\"cpu-usage\"]');\n    this.commandsPerSec = page.locator('[data-testid=\"commands-per-sec\"]');\n    this.totalMemory = page.locator('[data-testid=\"total-memory\"]');\n    this.totalKeys = page.locator('[data-testid=\"total-keys\"]');\n    this.connectedClients = page.locator('[data-testid=\"connected-clients\"]');\n\n    // Header actions\n    this.refreshButton = page.getByRole('button', { name: /refresh/i });\n    this.autoRefreshButton = page.getByRole('button', { name: 'Auto-refresh config popover' });\n  }\n\n  /**\n   * Navigate back to databases list\n   */\n  async goToDatabases(): Promise<void> {\n    await this.databasesButton.click();\n  }\n\n  /**\n   * Get the database name displayed in header\n   */\n  async getDatabaseName(): Promise<string | null> {\n    try {\n      return await this.databaseName.textContent();\n    } catch {\n      return null;\n    }\n  }\n\n  /**\n   * Get the logical database index (e.g., \"db0\", \"db1\")\n   */\n  async getLogicalDatabaseIndex(): Promise<string | null> {\n    try {\n      if (await this.logicalDatabaseButton.isVisible()) {\n        const text = await this.logicalDatabaseButton.textContent();\n        return text?.trim() || null;\n      }\n      return null;\n    } catch {\n      return null;\n    }\n  }\n\n  /**\n   * Check if logical database button is visible\n   */\n  async isLogicalDatabaseButtonVisible(): Promise<boolean> {\n    return this.logicalDatabaseButton.isVisible();\n  }\n\n  /**\n   * Open database info panel\n   */\n  async openDatabaseInfo(): Promise<void> {\n    await this.databaseInfoButton.click();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/components/NavigationTabs.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Navigation Tabs component\n * Common tabs shown on all database instance pages (Browse, Search, Workbench, Analyze, Pub/Sub)\n */\nexport class NavigationTabs {\n  readonly page: Page;\n\n  // Tab elements\n  readonly browseTab: Locator;\n  readonly searchTab: Locator;\n  readonly workbenchTab: Locator;\n  readonly analyzeTab: Locator;\n  readonly pubSubTab: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Navigation tabs\n    this.browseTab = page.getByRole('tab', { name: 'Browse' });\n    this.searchTab = page.getByRole('tab', { name: 'Search' });\n    this.workbenchTab = page.getByRole('tab', { name: 'Workbench' });\n    this.analyzeTab = page.getByRole('tab', { name: 'Analyze' });\n    this.pubSubTab = page.getByRole('tab', { name: 'Pub/Sub' });\n  }\n\n  /**\n   * Navigate to Browser tab\n   */\n  async gotoBrowser(): Promise<void> {\n    await this.browseTab.click();\n    await expect(this.browseTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  /**\n   * Navigate to Search tab\n   */\n  async gotoSearch(): Promise<void> {\n    await this.searchTab.click();\n    await expect(this.searchTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  /**\n   * Navigate to Workbench tab\n   */\n  async gotoWorkbench(): Promise<void> {\n    await this.workbenchTab.click();\n    await expect(this.workbenchTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  /**\n   * Navigate to Analyze tab\n   */\n  async gotoAnalyze(): Promise<void> {\n    await this.analyzeTab.click();\n    await expect(this.analyzeTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  /**\n   * Navigate to Pub/Sub tab\n   */\n  async gotoPubSub(): Promise<void> {\n    await this.pubSubTab.click();\n    await expect(this.pubSubTab).toHaveAttribute('aria-selected', 'true');\n  }\n\n  /**\n   * Get the currently selected tab name\n   */\n  async getSelectedTab(): Promise<string | null> {\n    const tabs = [this.browseTab, this.searchTab, this.workbenchTab, this.analyzeTab, this.pubSubTab];\n    for (const tab of tabs) {\n      const isSelected = await tab.getAttribute('aria-selected');\n      if (isSelected === 'true') {\n        return await tab.textContent();\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Check if a specific tab is selected\n   */\n  async isTabSelected(tabName: 'Browse' | 'Search' | 'Workbench' | 'Analyze' | 'Pub/Sub'): Promise<boolean> {\n    const tabMap = {\n      Browse: this.browseTab,\n      Search: this.searchTab,\n      Workbench: this.workbenchTab,\n      Analyze: this.analyzeTab,\n      'Pub/Sub': this.pubSubTab,\n    };\n    const tab = tabMap[tabName];\n    const isSelected = await tab.getAttribute('aria-selected');\n    return isSelected === 'true';\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/components/index.ts",
    "content": "export { InstanceHeader } from './InstanceHeader';\nexport { NavigationTabs } from './NavigationTabs';\nexport { BottomPanel } from './BottomPanel';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/DatabasesPage.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { BasePage } from '../BasePage';\nimport { AddDatabaseDialog } from './components/AddDatabaseDialog';\nimport { CloneDatabaseDialog } from './components/CloneDatabaseDialog';\nimport { DatabaseList } from './components/DatabaseList';\nimport { ImportDatabaseDialog } from './components/ImportDatabaseDialog';\nimport { TagsDialog } from './components/TagsDialog';\nimport { AddDatabaseConfig } from '../../types';\n\n/**\n * Page Object for the Databases List page\n * Composes smaller component POMs for better maintainability\n */\nexport class DatabasesPage extends BasePage {\n  // Component POMs\n  readonly addDatabaseDialog: AddDatabaseDialog;\n  readonly cloneDatabaseDialog: CloneDatabaseDialog;\n  readonly databaseList: DatabaseList;\n  readonly importDatabaseDialog: ImportDatabaseDialog;\n  readonly tagsDialog: TagsDialog;\n\n  // Page-level elements\n  readonly connectDatabaseButton: Locator;\n  readonly createCloudDatabaseButton: Locator;\n  readonly importFromFileButton: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Initialize component POMs\n    this.addDatabaseDialog = new AddDatabaseDialog(page);\n    this.cloneDatabaseDialog = new CloneDatabaseDialog(page);\n    this.databaseList = new DatabaseList(page);\n    this.importDatabaseDialog = new ImportDatabaseDialog(page);\n    this.tagsDialog = new TagsDialog(page);\n\n    // Page-level elements\n    this.connectDatabaseButton = page.getByTestId('add-redis-database-short');\n    this.createCloudDatabaseButton = page.getByRole('button', { name: /create free cloud database/i });\n    this.importFromFileButton = page.getByTestId('option-btn-import');\n  }\n\n  /**\n   * Navigate to the databases page (home)\n   */\n  async goto(): Promise<void> {\n    await this.gotoHome();\n  }\n\n  /**\n   * Open the Add Database dialog\n   */\n  async openAddDatabaseDialog(): Promise<void> {\n    await this.connectDatabaseButton.click();\n  }\n\n  /**\n   * Add a database - convenience method that combines dialog open + form fill\n   */\n  async addDatabase(config: AddDatabaseConfig): Promise<void> {\n    await this.openAddDatabaseDialog();\n    await this.addDatabaseDialog.addDatabase(config);\n  }\n\n  /**\n   * Open the Import from file dialog\n   */\n  async openImportDialog(): Promise<void> {\n    await this.openAddDatabaseDialog();\n    await this.importFromFileButton.click();\n  }\n\n  /**\n   * Import databases from file - full flow\n   */\n  async importDatabasesFromFile(filePath: string): Promise<{ success: number; failed: number }> {\n    await this.openImportDialog();\n    return this.importDatabaseDialog.importFile(filePath);\n  }\n\n  // Delegate common operations to components for backward compatibility\n\n  /**\n   * Get a database row by name\n   * @deprecated Use databaseList.getRow() instead\n   */\n  getDatabaseRow(name: string): Locator {\n    return this.databaseList.getRow(name);\n  }\n\n  /**\n   * Delete a database by name\n   * @deprecated Use databaseList.delete() instead\n   */\n  async deleteDatabase(name: string): Promise<void> {\n    await this.databaseList.delete(name);\n  }\n\n  /**\n   * Check if a database exists\n   * @deprecated Use databaseList.exists() instead\n   */\n  async databaseExists(name: string): Promise<boolean> {\n    return this.databaseList.exists(name);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/components/AddDatabaseDialog.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { AddDatabaseConfig, TlsConfig } from '../../../types';\n\n/**\n * Component Page Object for the Add Database Dialog\n * Handles all interactions with the add database form\n */\nexport class AddDatabaseDialog {\n  readonly page: Page;\n\n  // Dialog controls\n  readonly connectionUrlInput: Locator;\n  readonly connectionSettingsButton: Locator;\n  readonly addDatabaseButton: Locator;\n\n  // Connection settings form fields\n  readonly databaseAliasInput: Locator;\n  readonly hostInput: Locator;\n  readonly portInput: Locator;\n  readonly usernameInput: Locator;\n  readonly passwordInput: Locator;\n  readonly addRedisDatabaseButton: Locator;\n  readonly cancelButton: Locator;\n  readonly closeButton: Locator;\n  readonly testConnectionButton: Locator;\n  readonly dialog: Locator;\n\n  // Additional settings\n  readonly timeoutInput: Locator;\n  readonly selectLogicalDatabaseCheckbox: Locator;\n  readonly databaseIndexInput: Locator;\n  readonly forceStandaloneCheckbox: Locator;\n\n  // Tabs\n  readonly generalTab: Locator;\n  readonly securityTab: Locator;\n  readonly decompressionTab: Locator;\n\n  // Security tab\n  readonly useTlsCheckbox: Locator;\n\n  // TLS settings\n  readonly verifyTlsCertCheckbox: Locator;\n  readonly caCertDropdown: Locator;\n  readonly caCertNameInput: Locator;\n  readonly caCertTextarea: Locator;\n  readonly requiresClientAuthCheckbox: Locator;\n  readonly clientCertNameInput: Locator;\n  readonly clientCertTextarea: Locator;\n  readonly clientPrivateKeyTextarea: Locator;\n\n  // SNI settings\n  readonly sniCheckbox: Locator;\n  readonly sniServernameInput: Locator;\n\n  // SSH settings\n  readonly useSshCheckbox: Locator;\n  readonly sshHostInput: Locator;\n  readonly sshPortInput: Locator;\n  readonly sshUsernameInput: Locator;\n  readonly sshPasswordInput: Locator;\n\n  // Decompression & Formatters tab\n  readonly enableDecompressionCheckbox: Locator;\n  readonly compressorDropdown: Locator;\n  readonly keyNameFormatDropdown: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Dialog controls\n    this.dialog = page.getByRole('dialog', { name: /add database|connection settings/i });\n    this.connectionUrlInput = page.getByPlaceholder(/redis:\\/\\//i);\n    this.connectionSettingsButton = page.getByTestId('btn-connection-settings');\n    this.addDatabaseButton = page.getByRole('button', {\n      name: 'Add database',\n      exact: true,\n    });\n    this.closeButton = page.getByRole('button', { name: 'close' });\n    this.cancelButton = page.getByRole('button', { name: 'Cancel' });\n    this.testConnectionButton = page.getByRole('button', { name: 'Test Connection' });\n\n    // Connection settings form\n    this.databaseAliasInput = page.getByPlaceholder('Enter Database Alias');\n    this.hostInput = page.getByPlaceholder('Enter Hostname / IP address / Connection URL');\n    this.portInput = page.getByRole('spinbutton', { name: /port/i });\n    this.usernameInput = page.getByPlaceholder('Enter Username');\n    this.passwordInput = page.getByPlaceholder('Enter Password');\n    this.addRedisDatabaseButton = page.getByTestId('btn-submit');\n\n    // Additional settings\n    this.timeoutInput = page.getByRole('spinbutton', { name: /timeout/i });\n    this.selectLogicalDatabaseCheckbox = page.getByTestId('showDb');\n    this.databaseIndexInput = page.getByRole('spinbutton', { name: /database index/i });\n    this.forceStandaloneCheckbox = page.getByTestId('forceStandalone');\n\n    // Tabs\n    this.generalTab = page.getByRole('tab', { name: 'General' });\n    this.securityTab = page.getByRole('tab', { name: 'Security' });\n    this.decompressionTab = page.getByRole('tab', { name: 'Decompression & Formatters' });\n\n    // Security tab\n    this.useTlsCheckbox = page.getByTestId('tls');\n\n    // TLS settings\n    this.verifyTlsCertCheckbox = page.getByRole('checkbox', { name: /verify tls certificate/i });\n    this.caCertDropdown = page.getByTestId('select-ca-cert');\n    this.caCertNameInput = page.getByTestId('qa-ca-cert');\n    this.caCertTextarea = page.getByTestId('new-ca-cert');\n    this.requiresClientAuthCheckbox = page.getByTestId('tls-required-checkbox');\n    // Note: testid has typo in source code (tsl instead of tls)\n    this.clientCertNameInput = page.getByTestId('new-tsl-cert-pair-name');\n    this.clientCertTextarea = page.getByTestId('new-tls-client-cert');\n    this.clientPrivateKeyTextarea = page.getByTestId('new-tls-client-cert-key');\n\n    // SNI settings\n    this.sniCheckbox = page.getByTestId('sni');\n    this.sniServernameInput = page.getByTestId('sni-servername');\n\n    // SSH settings\n    this.useSshCheckbox = page.getByTestId('use-ssh');\n    this.sshHostInput = page.getByTestId('sshHost');\n    this.sshPortInput = page.getByTestId('sshPort');\n    this.sshUsernameInput = page.getByTestId('sshUsername');\n    this.sshPasswordInput = page.getByTestId('sshPassword');\n\n    // Decompression & Formatters tab\n    this.enableDecompressionCheckbox = page.getByRole('checkbox', { name: /enable automatic data decompression/i });\n    this.compressorDropdown = page.getByTestId('select-compressor');\n    this.keyNameFormatDropdown = page.getByRole('combobox', { name: /key name format/i });\n  }\n\n  async openConnectionSettings(): Promise<void> {\n    await this.connectionSettingsButton.click();\n  }\n\n  async fillForm(config: AddDatabaseConfig): Promise<void> {\n    await this.databaseAliasInput.fill(config.name);\n    await this.hostInput.fill(config.host);\n    await this.portInput.fill(config.port.toString());\n\n    if (config.username) {\n      await this.usernameInput.fill(config.username);\n    }\n\n    if (config.password) {\n      await this.passwordInput.fill(config.password);\n    }\n  }\n\n  async submit(): Promise<void> {\n    await this.addRedisDatabaseButton.click();\n  }\n\n  async addDatabase(config: AddDatabaseConfig): Promise<void> {\n    await this.openConnectionSettings();\n    await this.fillForm(config);\n    await this.submit();\n  }\n\n  async addDatabaseByUrl(url: string): Promise<void> {\n    await this.connectionUrlInput.fill(url);\n    await this.addDatabaseButton.click();\n  }\n\n  async cancel(): Promise<void> {\n    await this.cancelButton.click();\n  }\n\n  async close(): Promise<void> {\n    await this.closeButton.click();\n  }\n\n  async isVisible(): Promise<boolean> {\n    return this.dialog.isVisible();\n  }\n\n  async testConnection(): Promise<void> {\n    await this.testConnectionButton.click();\n  }\n\n  /**\n   * Wait for dialog to close\n   */\n  async waitForHidden(): Promise<void> {\n    await this.dialog.waitFor({ state: 'hidden' });\n  }\n\n  /**\n   * Enable logical database selection with specified index\n   */\n  async setLogicalDatabase(index: number): Promise<void> {\n    const isChecked = await this.selectLogicalDatabaseCheckbox.isChecked();\n    if (!isChecked) {\n      await this.selectLogicalDatabaseCheckbox.click();\n    }\n    await this.databaseIndexInput.fill(index.toString());\n  }\n\n  /**\n   * Configure timeout setting\n   */\n  async setTimeout(seconds: number): Promise<void> {\n    await this.timeoutInput.fill(seconds.toString());\n  }\n\n  /**\n   * Enable force standalone connection\n   */\n  async setForceStandalone(enabled: boolean): Promise<void> {\n    const isChecked = await this.forceStandaloneCheckbox.isChecked();\n    if (enabled !== isChecked) {\n      await this.forceStandaloneCheckbox.click();\n    }\n  }\n\n  /**\n   * Go to decompression tab and enable decompression with a specific format\n   */\n  async enableDecompression(format: string = 'GZIP'): Promise<void> {\n    await this.decompressionTab.click();\n    const isChecked = await this.enableDecompressionCheckbox.isChecked();\n    if (!isChecked) {\n      await this.enableDecompressionCheckbox.click();\n    }\n    await this.selectCompressor(format);\n  }\n\n  /**\n   * Select a compressor format from the dropdown\n   */\n  async selectCompressor(format: string): Promise<void> {\n    await this.compressorDropdown.click();\n    await this.page.getByRole('option', { name: format }).click();\n  }\n\n  /**\n   * Configure TLS settings on the Security tab\n   * @param tlsConfig TLS configuration options\n   */\n  async configureTls(tlsConfig: TlsConfig): Promise<void> {\n    // Go to Security tab\n    await this.securityTab.click();\n\n    // Enable TLS\n    const isTlsChecked = await this.useTlsCheckbox.isChecked();\n    if (tlsConfig.enabled && !isTlsChecked) {\n      await this.useTlsCheckbox.click();\n    }\n\n    if (!tlsConfig.enabled) {\n      return;\n    }\n\n    // Configure verify server cert if specified\n    if (tlsConfig.verifyServerCert !== undefined) {\n      const isVerifyChecked = await this.verifyTlsCertCheckbox.isChecked();\n      if (tlsConfig.verifyServerCert !== isVerifyChecked) {\n        await this.verifyTlsCertCheckbox.click();\n      }\n    }\n\n    // Configure CA Certificate if provided\n    if (tlsConfig.caCert) {\n      await this.caCertDropdown.click();\n      await this.page.getByRole('option', { name: 'Add new CA certificate' }).click();\n      await this.caCertNameInput.fill(tlsConfig.caCert.name);\n      await this.caCertTextarea.fill(tlsConfig.caCert.certificate);\n    }\n\n    // Configure Client Certificate if provided (mutual TLS)\n    if (tlsConfig.clientCert) {\n      // First check the \"Requires TLS Client Authentication\" checkbox\n      const isClientAuthChecked = await this.requiresClientAuthCheckbox.isChecked();\n      if (!isClientAuthChecked) {\n        await this.requiresClientAuthCheckbox.click();\n      }\n\n      // Client cert dropdown should already show \"Add new certificate\"\n      await this.clientCertNameInput.fill(tlsConfig.clientCert.name);\n      await this.clientCertTextarea.fill(tlsConfig.clientCert.certificate);\n      await this.clientPrivateKeyTextarea.fill(tlsConfig.clientCert.key);\n    }\n  }\n\n  /**\n   * Configure SSH tunnel on the Security tab\n   */\n  async configureSsh(sshConfig: { host: string; port: number; username: string; password?: string }): Promise<void> {\n    await this.securityTab.click();\n\n    const isSshChecked = await this.useSshCheckbox.isChecked();\n    if (!isSshChecked) {\n      await this.useSshCheckbox.click();\n    }\n\n    await this.sshHostInput.fill(sshConfig.host);\n    await this.sshPortInput.fill(sshConfig.port.toString());\n    await this.sshUsernameInput.fill(sshConfig.username);\n\n    if (sshConfig.password) {\n      await this.sshPasswordInput.fill(sshConfig.password);\n    }\n  }\n\n  /**\n   * Enable SNI on the Security tab (TLS must be enabled first)\n   */\n  async enableSni(servername: string): Promise<void> {\n    await this.securityTab.click();\n\n    const isSniChecked = await this.sniCheckbox.isChecked();\n    if (!isSniChecked) {\n      await this.sniCheckbox.click();\n    }\n    await this.sniServernameInput.fill(servername);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/components/CloneDatabaseDialog.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Component Page Object for the Clone Database Dialog\n * Handles all interactions with the clone database form\n */\nexport class CloneDatabaseDialog {\n  readonly page: Page;\n\n  // Dialog controls\n  readonly dialog: Locator;\n  readonly closeButton: Locator;\n  readonly backButton: Locator;\n  readonly cancelButton: Locator;\n  readonly cloneDatabaseButton: Locator;\n  readonly testConnectionButton: Locator;\n\n  // Connection settings form fields\n  readonly databaseAliasInput: Locator;\n  readonly hostInput: Locator;\n  readonly portInput: Locator;\n  readonly usernameInput: Locator;\n  readonly passwordInput: Locator;\n\n  // Additional settings\n  readonly timeoutInput: Locator;\n  readonly selectLogicalDatabaseCheckbox: Locator;\n  readonly databaseIndexInput: Locator;\n  readonly forceStandaloneCheckbox: Locator;\n\n  // Tabs\n  readonly generalTab: Locator;\n  readonly securityTab: Locator;\n  readonly decompressionTab: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Dialog controls\n    this.dialog = page.getByRole('dialog', { name: /clone database/i });\n    this.closeButton = page.getByRole('button', { name: 'close' });\n    this.backButton = page.getByRole('button', { name: 'back' });\n    this.cancelButton = page.getByRole('button', { name: 'Cancel' });\n    this.cloneDatabaseButton = page.getByRole('button', { name: 'Clone Database' });\n    this.testConnectionButton = page.getByRole('button', { name: 'Test Connection' });\n\n    // Connection settings form\n    this.databaseAliasInput = page.getByPlaceholder('Enter Database Alias');\n    this.hostInput = page.getByPlaceholder('Enter Hostname / IP address / Connection URL');\n    this.portInput = page.getByRole('spinbutton', { name: /port/i });\n    this.usernameInput = page.getByPlaceholder('Enter Username');\n    this.passwordInput = page.getByPlaceholder('Enter Password');\n\n    // Additional settings\n    this.timeoutInput = page.getByRole('spinbutton', { name: /timeout/i });\n    this.selectLogicalDatabaseCheckbox = page.getByTestId('showDb');\n    this.databaseIndexInput = page.getByRole('spinbutton', { name: /database index/i });\n    this.forceStandaloneCheckbox = page.getByRole('checkbox', { name: /force standalone/i });\n\n    // Tabs\n    this.generalTab = page.getByRole('tab', { name: 'General' });\n    this.securityTab = page.getByRole('tab', { name: 'Security' });\n    this.decompressionTab = page.getByRole('tab', { name: 'Decompression & Formatters' });\n  }\n\n  async isVisible(): Promise<boolean> {\n    return this.dialog.isVisible();\n  }\n\n  async getDatabaseAlias(): Promise<string> {\n    return this.databaseAliasInput.inputValue();\n  }\n\n  async getHost(): Promise<string> {\n    return this.hostInput.inputValue();\n  }\n\n  async getPort(): Promise<string> {\n    return this.portInput.inputValue();\n  }\n\n  async getUsername(): Promise<string> {\n    return this.usernameInput.inputValue();\n  }\n\n  async getTimeout(): Promise<string> {\n    return this.timeoutInput.inputValue();\n  }\n\n  async setDatabaseAlias(alias: string): Promise<void> {\n    await this.databaseAliasInput.clear();\n    await this.databaseAliasInput.fill(alias);\n  }\n\n  async submit(): Promise<void> {\n    await this.cloneDatabaseButton.click();\n  }\n\n  async cancel(): Promise<void> {\n    await this.cancelButton.click();\n  }\n\n  async goBack(): Promise<void> {\n    await this.backButton.click();\n  }\n\n  async close(): Promise<void> {\n    await this.closeButton.click();\n  }\n\n  async testConnection(): Promise<void> {\n    await this.testConnectionButton.click();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/components/DatabaseList.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Component Page Object for the Database List\n * Handles interactions with the list of databases\n */\nexport class DatabaseList {\n  readonly page: Page;\n  readonly list: Locator;\n  readonly searchInput: Locator;\n  readonly columnsButton: Locator;\n  readonly selectAllCheckbox: Locator;\n\n  // Bulk selection elements\n  readonly selectionCounter: Locator;\n  readonly exportButton: Locator;\n  readonly exportConfirmButton: Locator;\n  readonly bulkDeleteButton: Locator;\n  readonly cancelSelectingButton: Locator;\n\n  // Pagination elements\n  readonly paginationNav: Locator;\n  readonly paginationFirstPageButton: Locator;\n  readonly paginationLastPageButton: Locator;\n  readonly paginationPreviousButton: Locator;\n  readonly paginationNextButton: Locator;\n  readonly paginationPageInfo: Locator;\n  readonly paginationRowCount: Locator;\n  readonly paginationItemsPerPage: Locator;\n  readonly paginationPageSelect: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.list = page.getByTestId('databases-list');\n    this.searchInput = page.getByTestId('search-database-list');\n    this.columnsButton = page.getByTestId('btn-columns-config');\n    this.selectAllCheckbox = page.locator('table thead').getByRole('checkbox');\n\n    // Bulk selection elements\n    this.selectionCounter = page.getByText(/You selected: \\d+ items?/);\n    this.exportButton = page.getByRole('button', { name: 'Export' });\n    this.exportConfirmButton = page.getByRole('button', { name: 'Export' }).last();\n    this.bulkDeleteButton = page.getByRole('button', { name: 'Delete' });\n    this.cancelSelectingButton = page.getByRole('button', { name: 'Cancel selecting' });\n\n    // Pagination elements\n    this.paginationNav = page.getByRole('navigation', { name: 'Pagination' });\n    this.paginationFirstPageButton = this.paginationNav.getByRole('button', { name: 'first page' });\n    this.paginationLastPageButton = this.paginationNav.getByRole('button', { name: 'last page' });\n    this.paginationPreviousButton = this.paginationNav.getByRole('button', { name: 'previous page' });\n    this.paginationNextButton = this.paginationNav.getByRole('button', { name: 'next page' });\n    this.paginationPageInfo = this.paginationNav.locator('p').filter({ hasText: /\\d+ of \\d+/ });\n    this.paginationRowCount = this.paginationNav.locator('p').filter({ hasText: /Showing \\d+ out of \\d+ rows/ });\n    this.paginationItemsPerPage = this.paginationNav.getByRole('combobox', { name: 'Items per page:' });\n    this.paginationPageSelect = this.paginationNav.getByRole('combobox', { name: 'Page', exact: true });\n  }\n\n  /**\n   * Escape special regex characters in a string\n   */\n  private escapeRegex(str: string): string {\n    return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n  }\n\n  /**\n   * Get a database row by name\n   * Uses a strict text match in the database alias column (2nd column)\n   * Handles optional [dbX] suffix for logical databases\n   */\n  getRow(name: string): Locator {\n    const escapedName = this.escapeRegex(name);\n    return this.page\n      .locator('table tbody tr')\n      .filter({\n        has: this.page\n          .locator('td:nth-child(2)')\n          .filter({ hasText: new RegExp(`^${escapedName}(\\\\s*\\\\[db\\\\d+\\\\])?\\\\s*$`) }),\n      })\n      .first();\n  }\n\n  /**\n   * Get row checkbox by database name\n   */\n  getRowCheckbox(name: string): Locator {\n    return this.getRow(name).getByRole('checkbox');\n  }\n\n  /**\n   * Check if a database exists in the list\n   */\n  async exists(name: string): Promise<boolean> {\n    const row = this.getRow(name);\n    return await row.isVisible().catch(() => false);\n  }\n\n  /**\n   * Click on a database to connect\n   */\n  async connect(name: string): Promise<void> {\n    const row = this.getRow(name);\n    await row.click();\n  }\n\n  /**\n   * Open the context menu for a database\n   */\n  async openContextMenu(name: string): Promise<void> {\n    const row = this.getRow(name);\n    await row.click({ button: 'right' });\n  }\n\n  /**\n   * Delete a database using the row controls dropdown\n   */\n  async delete(name: string): Promise<void> {\n    // Clear search first to ensure the database row is properly visible\n    await this.clearSearch();\n\n    const row = this.getRow(name);\n    await row.hover();\n    await row.getByTestId(/controls-button/).click();\n    await this.page.getByRole('button', { name: 'Remove field' }).click();\n\n    // Wait for confirmation dialog and click Remove\n    // Use exact: true to avoid matching \"Remove field\" button which is also visible\n    const removeButton = this.page.getByRole('button', { name: 'Remove', exact: true });\n    await removeButton.waitFor({ state: 'visible' });\n    await removeButton.click();\n\n    // Wait for the row to be removed from the DOM\n    await row.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Edit a database using the row controls dropdown\n   */\n  async edit(name: string): Promise<void> {\n    const row = this.getRow(name);\n    await row.hover();\n    await row.getByTestId(/controls-button/).click();\n    await this.page.getByRole('button', { name: /edit instance/i }).click();\n  }\n\n  /**\n   * Open the clone database dialog for a database\n   */\n  async openCloneDialog(name: string): Promise<void> {\n    await this.edit(name);\n    await this.page.getByRole('dialog', { name: /edit database/i }).waitFor({ state: 'visible' });\n    await this.page.getByTestId('clone-db-btn').click();\n    await this.page.getByRole('dialog', { name: /clone database/i }).waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Get the visible row count in the current page\n   */\n  async getVisibleRowCount(): Promise<number> {\n    const rows = this.page.locator('table tbody tr');\n    return await rows.count();\n  }\n\n  /**\n   * Get the total count from the pagination info (e.g., \"Showing X out of Y rows\")\n   * Returns the total number of databases, not just visible rows\n   */\n  async getTotalCount(): Promise<number> {\n    const text = await this.paginationRowCount.textContent();\n    const match = text?.match(/out of (\\d+) rows/);\n    return match ? parseInt(match[1], 10) : await this.getVisibleRowCount();\n  }\n\n  // ==================== SEARCH ====================\n\n  /**\n   * Search for databases\n   */\n  async search(query: string): Promise<void> {\n    await this.searchInput.fill(query);\n  }\n\n  /**\n   * Clear the search\n   */\n  async clearSearch(): Promise<void> {\n    await this.searchInput.clear();\n  }\n\n  /**\n   * Get search input value\n   */\n  async getSearchValue(): Promise<string> {\n    return (await this.searchInput.inputValue()) || '';\n  }\n\n  // ==================== COLUMN CONFIGURATION ====================\n\n  /**\n   * Open column configuration dropdown\n   */\n  async openColumnConfig(): Promise<void> {\n    await this.columnsButton.click();\n  }\n\n  /**\n   * Toggle column visibility\n   */\n  async toggleColumn(columnName: string): Promise<void> {\n    await this.openColumnConfig();\n    const checkbox = this.page.getByRole('checkbox', { name: new RegExp(columnName, 'i') });\n    await checkbox.click();\n    await this.page.keyboard.press('Escape');\n  }\n\n  /**\n   * Check if column header is visible\n   */\n  async isColumnVisible(columnName: string): Promise<boolean> {\n    const header = this.page.getByRole('columnheader', { name: new RegExp(columnName, 'i') });\n    return await header.isVisible().catch(() => false);\n  }\n\n  // ==================== SELECTION ====================\n\n  /**\n   * Select a database row by checking its checkbox\n   */\n  async selectRow(name: string): Promise<void> {\n    await this.getRowCheckbox(name).check();\n  }\n\n  /**\n   * Unselect a database row\n   */\n  async unselectRow(name: string): Promise<void> {\n    await this.getRowCheckbox(name).uncheck();\n  }\n\n  /**\n   * Select all databases\n   */\n  async selectAll(): Promise<void> {\n    await this.selectAllCheckbox.check();\n  }\n\n  /**\n   * Unselect all databases\n   */\n  async unselectAll(): Promise<void> {\n    if (await this.cancelSelectingButton.isVisible()) {\n      await this.cancelSelectingButton.click();\n    } else {\n      await this.selectAllCheckbox.uncheck();\n    }\n  }\n\n  /**\n   * Check if row is selected\n   */\n  async isRowSelected(name: string): Promise<boolean> {\n    return await this.getRowCheckbox(name).isChecked();\n  }\n\n  /**\n   * Get selected count from counter text\n   */\n  async getSelectedCount(): Promise<number> {\n    if (!(await this.selectionCounter.isVisible())) {\n      return 0;\n    }\n    const text = (await this.selectionCounter.textContent()) || '';\n    const match = text.match(/(\\d+)/);\n    return match ? parseInt(match[1], 10) : 0;\n  }\n\n  // ==================== BULK ACTIONS ====================\n\n  /**\n   * Delete selected databases\n   */\n  async deleteSelected(): Promise<void> {\n    await this.bulkDeleteButton.click();\n    await this.page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click();\n  }\n\n  /**\n   * Open the export popover for selected databases\n   */\n  async exportSelected(): Promise<void> {\n    await this.exportButton.click();\n  }\n\n  /**\n   * Export selected databases and return the download artifact\n   */\n  async exportSelectedAndDownload(): Promise<import('@playwright/test').Download> {\n    await this.exportSelected();\n    const [download] = await Promise.all([this.page.waitForEvent('download'), this.exportConfirmButton.click()]);\n    return download;\n  }\n\n  /**\n   * Cancel current selection\n   */\n  async cancelSelection(): Promise<void> {\n    await this.cancelSelectingButton.click();\n  }\n\n  // ==================== SORTING ====================\n\n  /**\n   * Sort by column\n   */\n  async sortByColumn(columnName: string): Promise<void> {\n    const header = this.page.getByRole('columnheader', { name: new RegExp(columnName, 'i') });\n    await header.getByRole('button').click();\n  }\n\n  /**\n   * Get database names in order\n   */\n  async getDatabaseNames(): Promise<string[]> {\n    const cells = this.page.locator('table tbody tr td:nth-child(2)');\n    const names = await cells.allTextContents();\n    return names.map((n) => n.trim()).filter((n) => n.length > 0);\n  }\n\n  // ==================== TAGS ====================\n\n  /**\n   * Open tags manager for a database\n   */\n  async openTagsManager(name: string): Promise<void> {\n    const row = this.getRow(name);\n    await row.getByTestId(/manage.*tags/i).click();\n  }\n\n  // ==================== ASSERTIONS ====================\n\n  /**\n   * Assert database is visible\n   * Waits for the database to appear in the list (useful after API creation)\n   */\n  async expectDatabaseVisible(name: string, options: { timeout?: number; searchFirst?: boolean } = {}): Promise<void> {\n    const { timeout = 15000, searchFirst = false } = options;\n\n    if (searchFirst) {\n      await expect(async () => {\n        await this.clearSearch();\n        await this.page.waitForTimeout(100);\n        await this.search(name);\n        await expect(this.getRow(name)).toBeVisible({ timeout: 2000 });\n      }).toPass({ timeout, intervals: [500, 1000, 2000] });\n      return;\n    }\n\n    await expect(this.getRow(name)).toBeVisible({ timeout });\n  }\n\n  /**\n   * Assert database is not visible\n   */\n  async expectDatabaseNotVisible(name: string): Promise<void> {\n    await expect(this.getRow(name)).not.toBeVisible();\n  }\n\n  /**\n   * Assert selection counter shows specific count\n   */\n  async expectSelectedCount(count: number): Promise<void> {\n    if (count === 0) {\n      await expect(this.selectionCounter).not.toBeVisible();\n    } else {\n      await expect(this.selectionCounter).toContainText(count.toString());\n    }\n  }\n\n  // ==================== PAGINATION ====================\n\n  /**\n   * Check if pagination is visible\n   */\n  async isPaginationVisible(): Promise<boolean> {\n    return await this.paginationNav.isVisible().catch(() => false);\n  }\n\n  /**\n   * Go to first page\n   */\n  async goToFirstPage(): Promise<void> {\n    await this.paginationFirstPageButton.click();\n  }\n\n  /**\n   * Go to last page\n   */\n  async goToLastPage(): Promise<void> {\n    await this.paginationLastPageButton.click();\n  }\n\n  /**\n   * Go to next page\n   */\n  async goToNextPage(): Promise<void> {\n    await this.paginationNextButton.click();\n  }\n\n  /**\n   * Go to previous page\n   */\n  async goToPreviousPage(): Promise<void> {\n    await this.paginationPreviousButton.click();\n  }\n\n  /**\n   * Check if next page button is enabled\n   */\n  async isNextPageEnabled(): Promise<boolean> {\n    return await this.paginationNextButton.isEnabled().catch(() => false);\n  }\n\n  /**\n   * Check if previous page button is enabled\n   */\n  async isPreviousPageEnabled(): Promise<boolean> {\n    return await this.paginationPreviousButton.isEnabled().catch(() => false);\n  }\n\n  /**\n   * Check if first page button is enabled\n   */\n  async isFirstPageEnabled(): Promise<boolean> {\n    return await this.paginationFirstPageButton.isEnabled().catch(() => false);\n  }\n\n  /**\n   * Check if last page button is enabled\n   */\n  async isLastPageEnabled(): Promise<boolean> {\n    return await this.paginationLastPageButton.isEnabled().catch(() => false);\n  }\n\n  /**\n   * Get the row count text (e.g., \"Showing 10 out of 20 rows\")\n   */\n  async getRowCountText(): Promise<string> {\n    return (await this.paginationRowCount.textContent()) || '';\n  }\n\n  /**\n   * Get current items per page value\n   */\n  async getItemsPerPage(): Promise<string> {\n    return (await this.paginationItemsPerPage.textContent()) || '';\n  }\n\n  /**\n   * Set items per page\n   */\n  async setItemsPerPage(value: '10' | '25' | '50' | '100'): Promise<void> {\n    await this.paginationItemsPerPage.click();\n    await this.page.getByRole('option', { name: value, exact: true }).click();\n  }\n\n  /**\n   * Get current page number from page select\n   */\n  async getCurrentPage(): Promise<string> {\n    return (await this.paginationPageSelect.textContent()) || '';\n  }\n\n  /**\n   * Select a specific page from dropdown\n   */\n  async selectPage(pageNumber: string): Promise<void> {\n    await this.paginationPageSelect.click();\n    await this.page.getByRole('option', { name: pageNumber, exact: true }).click();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/components/ImportDatabaseDialog.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Component Page Object for the Import Database Dialog\n * Handles importing databases from JSON file\n */\nexport class ImportDatabaseDialog {\n  readonly page: Page;\n  readonly dialog: Locator;\n  readonly filePicker: Locator;\n  readonly submitButton: Locator;\n  readonly cancelButton: Locator;\n  readonly closeButton: Locator;\n  readonly backButton: Locator;\n  readonly okButton: Locator;\n  readonly successAccordion: Locator;\n  readonly failedAccordion: Locator;\n  readonly retryButton: Locator;\n  readonly errorMessage: Locator;\n  readonly dialogCloseButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.dialog = page.getByRole('dialog', { name: /import from file/i });\n    this.filePicker = page.getByTestId('import-file-modal-filepicker');\n    this.submitButton = page.getByTestId('btn-submit');\n    this.cancelButton = page.getByRole('button', { name: 'Cancel' });\n    this.closeButton = page.getByTestId('btn-close');\n    this.backButton = page.getByRole('button', { name: 'back' });\n    this.okButton = page.getByRole('button', { name: 'OK' });\n    this.retryButton = page.getByRole('button', { name: 'Retry' });\n    this.successAccordion = page.getByTestId(/ri-accordion-header-success-results/);\n    this.failedAccordion = page.getByTestId(/ri-accordion-header-fail-results/);\n    this.errorMessage = this.dialog.getByText(/failed to add database connections/i);\n    this.dialogCloseButton = this.dialog.getByRole('button', { name: 'close' });\n  }\n\n  async isVisible(): Promise<boolean> {\n    return await this.dialog.isVisible();\n  }\n\n  async uploadFile(filePath: string): Promise<void> {\n    const [fileChooser] = await Promise.all([this.page.waitForEvent('filechooser'), this.filePicker.click()]);\n    await fileChooser.setFiles(filePath);\n  }\n\n  async submit(): Promise<void> {\n    await this.submitButton.click();\n  }\n\n  async cancel(): Promise<void> {\n    await this.cancelButton.click();\n  }\n\n  async close(): Promise<void> {\n    if (await this.okButton.isVisible()) {\n      await this.okButton.click();\n    } else if (await this.closeButton.isVisible()) {\n      await this.closeButton.click();\n    } else {\n      await this.dialogCloseButton.click();\n    }\n  }\n\n  async goBack(): Promise<void> {\n    await this.backButton.click();\n  }\n\n  async getSuccessCount(): Promise<number> {\n    if (!(await this.successAccordion.isVisible())) {\n      return 0;\n    }\n    const text = (await this.successAccordion.textContent()) || '';\n    const match = text.match(/(\\d+)/);\n    return match ? parseInt(match[1], 10) : 0;\n  }\n\n  async getFailedCount(): Promise<number> {\n    if (!(await this.failedAccordion.isVisible())) {\n      return 0;\n    }\n    const text = (await this.failedAccordion.textContent()) || '';\n    const match = text.match(/(\\d+)/);\n    return match ? parseInt(match[1], 10) : 0;\n  }\n\n  async expandSuccessResults(): Promise<void> {\n    await this.successAccordion.click();\n  }\n\n  async expandFailedResults(): Promise<void> {\n    await this.failedAccordion.click();\n  }\n\n  async importFile(filePath: string): Promise<{ success: number; failed: number }> {\n    await this.uploadFile(filePath);\n    await this.submit();\n    await expect(this.okButton).toBeVisible({ timeout: 30000 });\n    const success = await this.getSuccessCount();\n    const failed = await this.getFailedCount();\n    await this.close();\n    return { success, failed };\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/components/TagsDialog.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Component Page Object for the Tags Dialog\n * Handles adding, editing, and removing tags on databases\n */\nexport class TagsDialog {\n  readonly page: Page;\n  readonly dialog: Locator;\n  readonly title: Locator;\n  readonly addTagButton: Locator;\n  readonly saveButton: Locator;\n  readonly cancelButton: Locator;\n  readonly closeButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.dialog = page.getByRole('dialog', { name: /manage tags/i });\n    this.title = this.dialog.locator('[class*=\"title\"], h1, h2').first();\n    this.addTagButton = page.getByTestId('add-tag-button');\n    this.saveButton = this.dialog.getByRole('button', { name: 'Save tags' });\n    this.cancelButton = page.getByTestId('close-button');\n    this.closeButton = this.dialog.getByRole('button', { name: 'close' });\n  }\n\n  async waitForVisible(): Promise<void> {\n    await this.dialog.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  async isVisible(): Promise<boolean> {\n    return await this.dialog.isVisible().catch(() => false);\n  }\n\n  getTagRows(): Locator {\n    return this.dialog.locator('[data-testid^=\"tag-row\"]');\n  }\n\n  getKeyInput(index: number): Locator {\n    return this.dialog.getByPlaceholder('Select a key or type your own').nth(index);\n  }\n\n  getValueInput(index: number): Locator {\n    return this.dialog.getByPlaceholder('Select a value or type your own').nth(index);\n  }\n\n  getDeleteButton(index: number): Locator {\n    return this.dialog.getByRole('img', { name: 'Delete' }).nth(index);\n  }\n\n  async addTag(key: string, value: string): Promise<void> {\n    await this.addTagButton.click();\n\n    const keyInputs = this.dialog.getByPlaceholder('Select a key or type your own');\n    const count = await keyInputs.count();\n    const newIndex = count - 1;\n\n    const keyInput = this.getKeyInput(newIndex);\n    await keyInput.fill(key);\n    await keyInput.press('Tab');\n\n    const valueInput = this.getValueInput(newIndex);\n    await valueInput.fill(value);\n  }\n\n  async deleteTag(index: number): Promise<void> {\n    await this.getDeleteButton(index).click();\n  }\n\n  async save(): Promise<void> {\n    await this.saveButton.click();\n    await this.dialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async cancel(): Promise<void> {\n    await this.cancelButton.click();\n    await this.dialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async close(): Promise<void> {\n    await this.closeButton.click();\n    await this.dialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  async getTagCount(): Promise<number> {\n    return await this.dialog.getByPlaceholder('Select a key or type your own').count();\n  }\n\n  async isSaveEnabled(): Promise<boolean> {\n    return await this.saveButton.isEnabled();\n  }\n\n  async expectVisible(): Promise<void> {\n    await expect(this.dialog).toBeVisible();\n  }\n\n  async expectHidden(): Promise<void> {\n    await expect(this.dialog).not.toBeVisible();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/components/index.ts",
    "content": "export { AddDatabaseDialog } from './AddDatabaseDialog';\nexport { CloneDatabaseDialog } from './CloneDatabaseDialog';\nexport { DatabaseList } from './DatabaseList';\nexport { ImportDatabaseDialog } from './ImportDatabaseDialog';\nexport { TagsDialog } from './TagsDialog';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/databases/index.ts",
    "content": "export { DatabasesPage } from './DatabasesPage';\nexport { AddDatabaseDialog, CloneDatabaseDialog, DatabaseList, ImportDatabaseDialog, TagsDialog } from './components';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/eula/EulaPage.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\nimport { BasePage } from '../BasePage';\n\n/**\n * Page object for EULA & Privacy Settings popup\n * This popup appears on first launch or after agreements reset\n * Extends BasePage since this is a standalone page\n */\nexport class EulaPage extends BasePage {\n  // Dialog elements\n  readonly dialog: Locator;\n  readonly dialogTitle: Locator;\n\n  // Switches\n  readonly useRecommendedSettingsSwitch: Locator;\n  readonly usageDataSwitch: Locator;\n  readonly encryptionSwitch: Locator;\n  readonly notificationsSwitch: Locator;\n  readonly eulaSwitch: Locator;\n\n  // Submit button\n  readonly submitButton: Locator;\n\n  // Links\n  readonly privacyPolicyLink: Locator;\n  readonly subscriptionAgreementLink: Locator;\n  readonly serverSideLicenseLink: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Dialog\n    this.dialog = page.getByTestId('consents-settings-popup');\n    this.dialogTitle = this.dialog.locator('text=EULA and Privacy settings');\n\n    // Switches - using data-testid pattern from the UI\n    this.useRecommendedSettingsSwitch = page.getByTestId('switch-option-recommended');\n    this.usageDataSwitch = page.getByTestId('switch-option-analytics');\n    this.encryptionSwitch = page.getByTestId('switch-option-encryption');\n    this.notificationsSwitch = page.getByTestId('switch-option-notifications');\n    this.eulaSwitch = page.getByTestId('switch-option-eula');\n\n    // Submit button\n    this.submitButton = page.getByTestId('btn-submit');\n\n    // Links\n    this.privacyPolicyLink = page.getByRole('link', { name: 'Privacy Policy' });\n    this.subscriptionAgreementLink = page.getByRole('link', {\n      name: 'Redis Enterprise Software Subscription Agreement',\n    });\n    this.serverSideLicenseLink = page.getByRole('link', { name: 'Server Side Public License' });\n  }\n\n  async goto(): Promise<void> {\n    await this.gotoHome();\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.dialog.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Check if EULA popup is visible\n   */\n  async isVisible(): Promise<boolean> {\n    try {\n      await expect(this.dialog).toBeVisible({ timeout: 3000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Wait for EULA popup to appear\n   */\n  async waitForPopup(timeout = 10000): Promise<void> {\n    await expect(this.dialog).toBeVisible({ timeout });\n  }\n\n  /**\n   * Accept EULA with default settings (only EULA checked)\n   */\n  async acceptEula(): Promise<void> {\n    await this.eulaSwitch.click();\n    await this.submitButton.click();\n    await expect(this.dialog).not.toBeVisible();\n  }\n\n  /**\n   * Accept EULA with recommended settings\n   */\n  async acceptWithRecommendedSettings(): Promise<void> {\n    await this.useRecommendedSettingsSwitch.click();\n    await this.eulaSwitch.click();\n    await this.submitButton.click();\n    await expect(this.dialog).not.toBeVisible();\n  }\n\n  /**\n   * Accept EULA with custom settings\n   */\n  async acceptWithCustomSettings(options: {\n    analytics?: boolean;\n    encryption?: boolean;\n    notifications?: boolean;\n  }): Promise<void> {\n    if (options.analytics) {\n      await this.usageDataSwitch.click();\n    }\n    if (options.encryption === false) {\n      // Encryption is on by default, click to disable\n      await this.encryptionSwitch.click();\n    }\n    if (options.notifications) {\n      await this.notificationsSwitch.click();\n    }\n\n    // Always need to accept EULA\n    await this.eulaSwitch.click();\n    await this.submitButton.click();\n    await expect(this.dialog).not.toBeVisible();\n  }\n\n  /**\n   * Check if submit button is enabled\n   */\n  async isSubmitEnabled(): Promise<boolean> {\n    return this.submitButton.isEnabled();\n  }\n\n  /**\n   * Check if a switch is checked\n   */\n  async isSwitchChecked(switchLocator: Locator): Promise<boolean> {\n    const ariaChecked = await switchLocator.getAttribute('aria-checked');\n    return ariaChecked === 'true';\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/eula/index.ts",
    "content": "export { EulaPage } from './EulaPage';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/index.ts",
    "content": "export { BasePage } from './BasePage';\nexport { InstancePage } from './InstancePage';\nexport { InstanceHeader, NavigationTabs, BottomPanel } from './components';\nexport { BrowserPage, AddKeyDialog, KeyList } from './browser';\nexport { CliPanel } from './cli';\nexport { CommandHelperPanel } from './command-helper';\nexport {\n  DatabasesPage,\n  AddDatabaseDialog,\n  CloneDatabaseDialog,\n  ImportDatabaseDialog,\n  TagsDialog,\n  DatabaseList,\n} from './databases';\nexport { WorkbenchPage, Editor, ResultsPanel } from './workbench';\nexport { AnalyticsPage } from './analytics';\nexport { SettingsPage } from './settings';\nexport { PubSubPage } from './pubsub';\nexport { SidebarPanel } from './navigation';\nexport { EulaPage } from './eula';\nexport { VectorSearchPage } from './vector-search';\nexport { InsightsPanel } from './insights';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/insights/InsightsPanel.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\n/**\n * Page Object for the Insights Panel (side panel with Tutorials and Tips tabs)\n */\nexport class InsightsPanel {\n  readonly page: Page;\n\n  // Panel container\n  readonly panel: Locator;\n  readonly trigger: Locator;\n\n  // Header controls\n  readonly closeButton: Locator;\n  readonly fullScreenButton: Locator;\n\n  // Tabs\n  readonly tabsContainer: Locator;\n  readonly tutorialsTab: Locator;\n  readonly tipsTab: Locator;\n\n  // Tutorials tab content\n  readonly myTutorialsAccordion: Locator;\n  readonly redisTutorialsAccordion: Locator;\n  readonly enablementArea: Locator;\n\n  // Tutorial page content\n  readonly tutorialPageContent: Locator;\n  readonly paginationMenuButton: Locator;\n  readonly paginationMenu: Locator;\n  readonly nextPageButton: Locator;\n  readonly prevPageButton: Locator;\n\n  // Tips tab content\n  readonly noRecommendationsScreen: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Panel container\n    this.panel = page.getByTestId('side-panels-insights');\n    this.trigger = page.getByTestId('insights-trigger');\n\n    // Header controls\n    this.closeButton = page.getByTestId('close-insights-btn');\n    this.fullScreenButton = page.getByTestId('fullScreen-insights-btn');\n\n    // Tabs - scoped to panel to avoid matching other tabs on the page\n    this.tabsContainer = page.getByTestId('insights-tabs');\n    this.tutorialsTab = this.tabsContainer.getByRole('tab', { name: 'Tutorials' });\n    this.tipsTab = this.tabsContainer.getByRole('tab', { name: /Tips/ });\n\n    // Tutorials tab content - accordion headers (use data-testid)\n    // Custom tutorials: id=\"custom-tutorials\", label=\"MY TUTORIALS\"\n    this.myTutorialsAccordion = page.getByTestId('ri-accordion-header-custom-tutorials');\n    // Main tutorials: id=\"tutorials\", label=\"Tutorials\"\n    this.redisTutorialsAccordion = page.getByTestId('ri-accordion-header-tutorials');\n    this.enablementArea = page.getByTestId('enablementArea');\n\n    // Tutorial page content (when viewing a tutorial)\n    this.tutorialPageContent = page.getByTestId('enablement-area__page');\n    this.paginationMenuButton = page.getByTestId('enablement-area__toggle-pagination-menu-btn');\n    this.paginationMenu = page.getByTestId('enablement-area__pagination-menu');\n    this.nextPageButton = page.getByTestId('enablement-area__next-page-btn');\n    this.prevPageButton = page.getByTestId('enablement-area__prev-page-btn');\n\n    // Tips tab content\n    this.noRecommendationsScreen = page.getByTestId('no-recommendations-screen');\n  }\n\n  /**\n   * Open the Insights panel by clicking the trigger\n   */\n  async open(): Promise<void> {\n    await this.trigger.click();\n    await this.panel.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Close the Insights panel\n   */\n  async close(): Promise<void> {\n    await this.closeButton.click();\n    await this.panel.waitFor({ state: 'hidden' });\n  }\n\n  /**\n   * Check if the panel is open\n   */\n  async isOpen(): Promise<boolean> {\n    return this.panel.isVisible();\n  }\n\n  /**\n   * Switch to the Tutorials tab\n   */\n  async switchToTutorialsTab(): Promise<void> {\n    await this.tutorialsTab.click();\n    await expect(this.tutorialsTab).toHaveAttribute('data-state', 'active');\n  }\n\n  /**\n   * Switch to the Tips tab\n   */\n  async switchToTipsTab(): Promise<void> {\n    await this.tipsTab.click();\n    await expect(this.tipsTab).toHaveAttribute('data-state', 'active');\n  }\n\n  /**\n   * Get the currently active tab name\n   */\n  async getActiveTabName(): Promise<string> {\n    const activeTab = this.tabsContainer.locator('[data-state=\"active\"]');\n    return (await activeTab.textContent()) || '';\n  }\n\n  /**\n   * Get the collapse button inside an accordion header\n   * The aria-expanded attribute is on the button, not the header div\n   */\n  private getAccordionButton(folderId: string): Locator {\n    const header = this.page.getByTestId(`ri-accordion-header-${folderId}`);\n    return header.locator('button[aria-expanded]');\n  }\n\n  /**\n   * Expand a tutorial folder accordion by id\n   * @param folderId - The accordion id (e.g., 'custom-tutorials', 'tutorials')\n   */\n  async expandTutorialFolder(folderId: string): Promise<void> {\n    const button = this.getAccordionButton(folderId);\n    const isExpanded = await button.getAttribute('aria-expanded');\n    if (isExpanded !== 'true') {\n      await button.click();\n      await expect(button).toHaveAttribute('aria-expanded', 'true');\n    }\n  }\n\n  /**\n   * Collapse a tutorial folder accordion by id\n   * @param folderId - The accordion id (e.g., 'custom-tutorials', 'tutorials')\n   */\n  async collapseTutorialFolder(folderId: string): Promise<void> {\n    const button = this.getAccordionButton(folderId);\n    const isExpanded = await button.getAttribute('aria-expanded');\n    if (isExpanded === 'true') {\n      await button.click();\n      await expect(button).toHaveAttribute('aria-expanded', 'false');\n    }\n  }\n\n  /**\n   * Check if a tutorial folder is expanded by id\n   * @param folderId - The accordion id (e.g., 'custom-tutorials', 'tutorials')\n   */\n  async isTutorialFolderExpanded(folderId: string): Promise<boolean> {\n    const button = this.getAccordionButton(folderId);\n    const isExpanded = await button.getAttribute('aria-expanded');\n    return isExpanded === 'true';\n  }\n\n  /**\n   * Check if My tutorials section is visible\n   */\n  async isMyTutorialsVisible(): Promise<boolean> {\n    return this.myTutorialsAccordion.isVisible();\n  }\n\n  /**\n   * Click on a tutorial link by its data-testid\n   * Tutorial links have data-testid=\"internal-link-{id}\"\n   * @param tutorialId - The tutorial link id (e.g., 'introduction', 'sq-intro')\n   */\n  async openTutorial(tutorialId: string): Promise<void> {\n    const tutorialLink = this.page.getByTestId(`internal-link-${tutorialId}`);\n    await tutorialLink.click();\n    await this.tutorialPageContent.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Navigate to next page in tutorial pagination\n   * Waits for the pagination text to change after clicking\n   */\n  async goToNextPage(): Promise<void> {\n    const currentPagination = await this.getPaginationInfo();\n    await this.nextPageButton.click();\n    // Wait for pagination text to change\n    await expect(this.paginationMenuButton).not.toHaveText(currentPagination);\n  }\n\n  /**\n   * Navigate to previous page in tutorial pagination\n   * Waits for the pagination text to change after clicking\n   */\n  async goToPreviousPage(): Promise<void> {\n    const currentPagination = await this.getPaginationInfo();\n    await this.prevPageButton.click();\n    // Wait for pagination text to change\n    await expect(this.paginationMenuButton).not.toHaveText(currentPagination);\n  }\n\n  /**\n   * Get current page info from pagination (e.g., \"1 of 3\")\n   */\n  async getPaginationInfo(): Promise<string> {\n    return (await this.paginationMenuButton.textContent()) || '';\n  }\n\n  /**\n   * Check if next page button is visible (indicates more pages available)\n   */\n  async hasNextPage(): Promise<boolean> {\n    return this.nextPageButton.isVisible();\n  }\n\n  /**\n   * Check if previous page button is visible\n   */\n  async hasPreviousPage(): Promise<boolean> {\n    return this.prevPageButton.isVisible();\n  }\n\n  /**\n   * Run a tutorial command by clicking the run button with specified label\n   * @param label - The label of the run button (from data-testid=\"run-btn-{label}\")\n   */\n  async runCommand(label: string): Promise<void> {\n    const runButton = this.page.getByTestId(`run-btn-${label}`);\n    await runButton.click();\n  }\n\n  /**\n   * Get the first available run button on the tutorial page\n   */\n  getFirstRunButton(): Locator {\n    return this.tutorialPageContent.locator('[data-testid^=\"run-btn-\"]').first();\n  }\n\n  /**\n   * Check if tutorial page content is visible\n   */\n  async isTutorialPageVisible(): Promise<boolean> {\n    return this.tutorialPageContent.isVisible();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/insights/index.ts",
    "content": "export { InsightsPanel } from './InsightsPanel';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/navigation/SidebarPanel.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { BasePage } from '../BasePage';\nimport { HelpMenu } from './components/HelpMenu';\nimport { NotificationCenter } from './components/NotificationCenter';\n\n/**\n * Page Object for Navigation elements (sidebar, help menu, notifications, panels)\n * Extends BasePage since this handles global navigation elements\n */\nexport class SidebarPanel extends BasePage {\n  // Main navigation\n  readonly mainNavigation: Locator;\n  readonly cloudLink: Locator;\n  readonly settingsButton: Locator;\n  readonly githubLink: Locator;\n\n  // Help menu component\n  readonly helpMenu: HelpMenu;\n\n  // Notification center component\n  readonly notificationCenter: NotificationCenter;\n\n  // Copilot panel\n  readonly copilotTrigger: Locator;\n  readonly copilotPanel: Locator;\n  readonly copilotTitle: Locator;\n  readonly copilotCloseButton: Locator;\n  readonly copilotFullScreenButton: Locator;\n  readonly copilotGoogleSignIn: Locator;\n  readonly copilotGithubSignIn: Locator;\n  readonly copilotSsoSignIn: Locator;\n  readonly copilotTermsCheckbox: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Main navigation (redisLogo inherited from BasePage)\n    this.mainNavigation = page.getByRole('navigation', { name: 'Main navigation' });\n    this.cloudLink = page.getByTestId('create-cloud-db-link');\n    this.settingsButton = page\n      .getByTestId('settings-page-btn')\n      .or(page.locator('[data-testid=\"Settings page button\"]'));\n    this.githubLink = page.getByTestId('github-repo-btn');\n\n    // Help menu component\n    this.helpMenu = new HelpMenu(page);\n\n    // Notification center component\n    this.notificationCenter = new NotificationCenter(page);\n\n    // Copilot panel\n    this.copilotTrigger = page.getByTestId('copilot-trigger');\n    this.copilotPanel = page.locator('[class*=\"copilot\"]').filter({ hasText: 'Redis Copilot' });\n    this.copilotTitle = page.getByText('Redis Copilot', { exact: true });\n    this.copilotCloseButton = page.getByTestId('close-copilot-btn');\n    this.copilotFullScreenButton = page.getByRole('button', { name: 'Open full screen' });\n    this.copilotGoogleSignIn = page.getByRole('button', { name: /Google Signin/i });\n    this.copilotGithubSignIn = page.getByRole('button', { name: /Github Github/i });\n    this.copilotSsoSignIn = page.getByRole('button', { name: /Sso SSO/i });\n    this.copilotTermsCheckbox = page.getByRole('checkbox', { name: /By signing up/i });\n  }\n\n  /**\n   * Navigate to home page\n   */\n  async goto(): Promise<void> {\n    await this.gotoHome();\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.mainNavigation.waitFor({ state: 'visible' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/navigation/components/HelpMenu.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Component for Help Menu interactions\n * Handles the Help Center dropdown and its menu items\n */\nexport class HelpMenu {\n  readonly page: Page;\n\n  // Help menu trigger and dialog\n  readonly helpMenuButton: Locator;\n  readonly helpMenuDialog: Locator;\n\n  // Menu items\n  readonly provideFeedbackLink: Locator;\n  readonly keyboardShortcutsButton: Locator;\n  readonly releaseNotesLink: Locator;\n  readonly resetOnboardingButton: Locator;\n\n  // Keyboard shortcuts dialog\n  readonly shortcutsDialog: Locator;\n  readonly shortcutsTitle: Locator;\n  readonly shortcutsCloseButton: Locator;\n  readonly shortcutsDesktopSection: Locator;\n  readonly shortcutsCliSection: Locator;\n  readonly shortcutsWorkbenchSection: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Help menu trigger and dialog\n    this.helpMenuButton = page.getByTestId('help-menu-button');\n    this.helpMenuDialog = page.getByRole('dialog').filter({ hasText: 'Help Center' });\n\n    // Menu items\n    this.provideFeedbackLink = page.getByRole('link', { name: /Provide Feedback/i });\n    this.keyboardShortcutsButton = page.getByText('Keyboard Shortcuts');\n    this.releaseNotesLink = page.getByRole('link', { name: 'Release Notes' });\n    this.resetOnboardingButton = page.getByText('Reset Onboarding');\n\n    // Keyboard shortcuts dialog\n    this.shortcutsDialog = page.getByRole('dialog', { name: 'Shortcuts' });\n    this.shortcutsTitle = this.shortcutsDialog.getByText('Shortcuts', { exact: true });\n    this.shortcutsCloseButton = this.shortcutsDialog.getByRole('button', { name: 'close drawer' });\n    this.shortcutsDesktopSection = this.shortcutsDialog.getByText('Desktop application');\n    this.shortcutsCliSection = this.shortcutsDialog.getByText('CLI', { exact: true });\n    this.shortcutsWorkbenchSection = this.shortcutsDialog.getByText('Workbench', { exact: true });\n  }\n\n  /**\n   * Open help menu\n   */\n  async open(): Promise<void> {\n    await this.helpMenuButton.click();\n    await this.helpMenuDialog.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Close help menu by pressing Escape\n   */\n  async close(): Promise<void> {\n    await this.page.keyboard.press('Escape');\n    await this.helpMenuDialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Check if help menu is open\n   */\n  async isOpen(): Promise<boolean> {\n    return this.helpMenuDialog.isVisible();\n  }\n\n  /**\n   * Open keyboard shortcuts dialog from help menu\n   */\n  async openKeyboardShortcuts(): Promise<void> {\n    if (!(await this.isOpen())) {\n      await this.open();\n    }\n    await this.keyboardShortcutsButton.click();\n    await this.shortcutsDialog.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Close keyboard shortcuts dialog\n   */\n  async closeKeyboardShortcuts(): Promise<void> {\n    await this.shortcutsCloseButton.click();\n    await this.shortcutsDialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Check if keyboard shortcuts dialog is open\n   */\n  async isKeyboardShortcutsOpen(): Promise<boolean> {\n    return this.shortcutsDialog.isVisible();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/navigation/components/NotificationCenter.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * NotificationCenter component for Notification Center interactions\n *\n * Handles locators and methods for the Notification Center dialog\n * accessed from the sidebar navigation.\n */\nexport class NotificationCenter {\n  readonly page: Page;\n\n  // Notification Center button and dialog\n  readonly notificationMenuButton: Locator;\n  readonly notificationCenterDialog: Locator;\n  readonly notificationCenterTitle: Locator;\n\n  // Notification list and items\n  readonly notificationsList: Locator;\n  readonly notificationItems: Locator;\n\n  // Notification elements\n  readonly notificationTitles: Locator;\n  readonly notificationBodies: Locator;\n  readonly notificationDates: Locator;\n  readonly notificationCategories: Locator;\n  readonly notificationLinks: Locator;\n\n  // Badge\n  readonly unreadBadge: Locator;\n\n  // Empty state\n  readonly noNotificationsText: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    // Notification Center button and dialog\n    this.notificationMenuButton = page.getByTestId('notification-menu-button');\n    this.notificationCenterDialog = page.getByTestId('notification-center');\n    this.notificationCenterTitle = this.notificationCenterDialog.getByText('Notification Center');\n\n    // Notification items - use data-testid pattern for notification items\n    this.notificationsList = this.notificationCenterDialog;\n    this.notificationItems = this.notificationCenterDialog.locator('[data-testid^=\"notification-item\"]');\n\n    // Notification elements within items\n    this.notificationTitles = this.notificationItems.locator('> div:first-child');\n    this.notificationBodies = this.notificationItems.locator('> div:nth-child(2)');\n    this.notificationDates = this.notificationItems.locator('p').first();\n    this.notificationCategories = this.notificationItems.locator('p').last();\n    this.notificationLinks = this.notificationItems.locator('a');\n\n    // Badge for unread count\n    this.unreadBadge = page.getByTestId('total-unread-badge');\n\n    // Empty state\n    this.noNotificationsText = this.notificationCenterDialog.getByText('No notifications');\n  }\n\n  /**\n   * Open notification center by clicking the notification button\n   */\n  async open(): Promise<void> {\n    await this.notificationMenuButton.click();\n    await this.notificationCenterDialog.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Close notification center by clicking outside the dialog\n   */\n  async close(): Promise<void> {\n    // Click outside the dialog to close it\n    await this.page.locator('body').click({ position: { x: 10, y: 10 } });\n    await this.notificationCenterDialog.waitFor({ state: 'hidden', timeout: 5000 });\n  }\n\n  /**\n   * Check if notification center is open\n   */\n  async isOpen(): Promise<boolean> {\n    return this.notificationCenterDialog.isVisible();\n  }\n\n  /**\n   * Get notification title by index\n   */\n  getNotificationTitle(index: number): Locator {\n    return this.notificationItems.nth(index).locator('> div:first-child');\n  }\n\n  /**\n   * Get notification body by index\n   */\n  getNotificationBody(index: number): Locator {\n    return this.notificationItems.nth(index).locator('> div:nth-child(2)');\n  }\n\n  /**\n   * Get notification date by index\n   */\n  getNotificationDate(index: number): Locator {\n    return this.notificationItems.nth(index).locator('p').first();\n  }\n\n  /**\n   * Get notification category by index\n   */\n  getNotificationCategory(index: number): Locator {\n    return this.notificationItems.nth(index).locator('p').last();\n  }\n\n  /**\n   * Get notification links by index\n   */\n  getNotificationLinks(index: number): Locator {\n    return this.notificationItems.nth(index).locator('a');\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/navigation/index.ts",
    "content": "export { SidebarPanel } from './SidebarPanel';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/pubsub/PubSubPage.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\n\n/**\n * Pub/Sub Page Object Model\n * Handles subscribe and publish functionality\n *\n * Extends InstancePage to get access to:\n * - instanceHeader (database name, stats, breadcrumb)\n * - navigationTabs (Browse, Workbench, Analyze, Pub/Sub)\n * - bottomPanel (CLI, Command Helper, Profiler)\n */\nexport class PubSubPage extends InstancePage {\n  // Subscribe section\n  readonly patternInput: Locator;\n  readonly subscribeButton: Locator;\n  readonly unsubscribeButton: Locator;\n  readonly notSubscribedMessage: Locator;\n  readonly messagesContainer: Locator;\n  readonly clearMessagesButton: Locator;\n\n  // Publish section\n  readonly channelNameInput: Locator;\n  readonly messageInput: Locator;\n  readonly publishButton: Locator;\n\n  // Messages table/list\n  readonly messagesList: Locator;\n  readonly messagesTable: Locator;\n  readonly messageRows: Locator;\n\n  // Status section\n  readonly statusSection: Locator;\n  readonly messagesCount: Locator;\n  readonly subscribedBadge: Locator;\n  readonly unsubscribedBadge: Locator;\n\n  // Table columns (headers)\n  readonly timestampHeader: Locator;\n  readonly channelHeader: Locator;\n  readonly messageHeader: Locator;\n\n  // Warning message\n  readonly productionWarning: Locator;\n\n  // Cluster mode banner\n  readonly clusterSpublishBanner: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Subscribe section\n    this.patternInput = page.getByPlaceholder('Enter Pattern');\n    this.subscribeButton = page.getByRole('button', { name: 'Subscribe' });\n    this.unsubscribeButton = page.getByRole('button', { name: 'Unsubscribe' });\n    this.notSubscribedMessage = page.getByText('You are not subscribed');\n    this.messagesContainer = page.getByTestId('pubsub-messages').or(page.locator('[data-testid*=\"pubsub\"]'));\n    this.clearMessagesButton = page.getByTestId('clear-pubsub-btn');\n\n    // Publish section\n    this.channelNameInput = page.getByPlaceholder('Enter Channel Name');\n    this.messageInput = page.getByPlaceholder('Enter Message');\n    this.publishButton = page.getByRole('button', { name: 'Publish' });\n\n    // Messages table/list\n    this.messagesList = page.getByTestId('messages-list');\n    this.messagesTable = page.getByRole('table').filter({ hasText: /Channel|Message/ });\n    this.messageRows = this.messagesTable.locator('tbody tr');\n\n    // Status section\n    this.statusSection = page.getByTestId('pub-sub-status');\n    this.messagesCount = page.getByTestId('pub-sub-messages-count');\n    this.subscribedBadge = page.getByText('Subscribed', { exact: true });\n    this.unsubscribedBadge = page.getByText('Unsubscribed', { exact: true });\n\n    // Table headers\n    this.timestampHeader = this.messagesTable.getByRole('columnheader', { name: 'Timestamp' });\n    this.channelHeader = this.messagesTable.getByRole('columnheader', { name: 'Channel' });\n    this.messageHeader = this.messagesTable.getByRole('columnheader', { name: 'Message' });\n\n    // Warning message\n    this.productionWarning = page.getByText('Running in production may decrease performance');\n\n    // Cluster mode banner\n    this.clusterSpublishBanner = page.getByTestId('empty-messages-list-cluster');\n  }\n\n  /**\n   * Navigate to Pub/Sub page via UI\n   */\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoPubSub();\n    await this.waitForLoad();\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.subscribeButton.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Subscribe to a pattern\n   */\n  async subscribe(pattern: string = '*'): Promise<void> {\n    await this.patternInput.clear();\n    await this.patternInput.fill(pattern);\n    await this.subscribeButton.click();\n    await this.unsubscribeButton.waitFor({ state: 'visible', timeout: 10000 });\n  }\n\n  /**\n   * Unsubscribe from current subscription\n   */\n  async unsubscribe(): Promise<void> {\n    await this.unsubscribeButton.click();\n    await this.subscribeButton.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Publish a message to a channel\n   */\n  async publish(channel: string, message: string): Promise<void> {\n    await this.channelNameInput.clear();\n    await this.channelNameInput.fill(channel);\n    await this.messageInput.clear();\n    await this.messageInput.fill(message);\n    await this.publishButton.click();\n  }\n\n  /**\n   * Check if subscribed (Unsubscribe button visible)\n   */\n  async isSubscribed(): Promise<boolean> {\n    try {\n      await this.unsubscribeButton.waitFor({ state: 'visible', timeout: 2000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Check if not subscribed message is visible\n   */\n  async isNotSubscribedMessageVisible(): Promise<boolean> {\n    try {\n      await this.notSubscribedMessage.waitFor({ state: 'visible', timeout: 2000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Get messages count\n   */\n  async getMessagesCount(): Promise<number> {\n    try {\n      await this.messagesTable.waitFor({ state: 'visible', timeout: 2000 });\n      return await this.messageRows.count();\n    } catch {\n      return 0;\n    }\n  }\n\n  /**\n   * Wait for message to appear\n   */\n  async waitForMessage(messageText: string, timeout: number = 10000): Promise<void> {\n    await this.page.getByText(messageText).waitFor({ state: 'visible', timeout });\n  }\n\n  /**\n   * Get the displayed messages count from the status bar\n   */\n  async getDisplayedMessagesCount(): Promise<number> {\n    const text = await this.messagesCount.innerText();\n    return parseInt(text, 10);\n  }\n\n  /**\n   * Check if message table is visible\n   */\n  async isMessageTableVisible(): Promise<boolean> {\n    try {\n      await this.messagesList.waitFor({ state: 'visible', timeout: 2000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Click on a table column header to sort\n   */\n  async sortByColumn(columnName: 'Timestamp' | 'Channel' | 'Message'): Promise<void> {\n    const header = this.messagesTable.getByRole('columnheader', { name: columnName });\n    await header.click();\n  }\n\n  /**\n   * Get cell text by row and column index\n   */\n  async getCellText(rowIndex: number, columnIndex: number): Promise<string> {\n    const row = this.messageRows.nth(rowIndex);\n    const cell = row.locator('td').nth(columnIndex);\n    return cell.innerText();\n  }\n\n  /**\n   * Check if status badge shows \"Subscribed\"\n   */\n  async isStatusSubscribed(): Promise<boolean> {\n    try {\n      await this.subscribedBadge.waitFor({ state: 'visible', timeout: 2000 });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/pubsub/index.ts",
    "content": "export { PubSubPage } from './PubSubPage';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/settings/SettingsPage.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { BasePage } from '../BasePage';\n\n/**\n * Page Object for Settings page\n * Extends BasePage (not InstancePage) since this is a standalone page\n */\nexport class SettingsPage extends BasePage {\n  // Page title\n  readonly pageTitle: Locator;\n\n  // Accordion section headers\n  readonly generalSectionHeader: Locator;\n  readonly privacySectionHeader: Locator;\n  readonly workbenchSectionHeader: Locator;\n  readonly redisCloudSectionHeader: Locator;\n  readonly advancedSectionHeader: Locator;\n\n  // General settings\n  readonly themeDropdown: Locator;\n  readonly notificationSwitch: Locator;\n  readonly dateFormatRadioPreselected: Locator;\n  readonly dateFormatRadioCustom: Locator;\n  readonly dateFormatDropdown: Locator;\n  readonly customDateFormatInput: Locator;\n  readonly customDateFormatSaveButton: Locator;\n  readonly timezoneDropdown: Locator;\n  readonly datePreview: Locator;\n\n  // Privacy settings\n  readonly usageDataSwitch: Locator;\n  readonly privacyPolicyLink: Locator;\n\n  // Workbench settings\n  readonly editorCleanupSwitch: Locator;\n  readonly pipelineCommandsText: Locator;\n  readonly pipelineCommandsValue: Locator;\n  readonly pipelineCommandsInput: Locator;\n  readonly pipelineApplyButton: Locator;\n\n  // Advanced settings\n  readonly advancedWarning: Locator;\n  readonly keysToScanText: Locator;\n  readonly keysToScanValue: Locator;\n  readonly keysToScanInput: Locator;\n  readonly keysToScanApplyButton: Locator;\n\n  // Redis Cloud settings\n  readonly apiUserKeysText: Locator;\n  readonly removeApiKeysButton: Locator;\n  readonly autodiscoverButton: Locator;\n  readonly createCloudDbButton: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Page title\n    this.pageTitle = page.locator('[data-testid=\"settings-page-title\"]').or(page.getByText('Settings').first());\n\n    // The header toggles via its nested collapse trigger button.\n    this.generalSectionHeader = page.locator('[data-test-subj=\"accordion-appearance\"] button[aria-expanded]');\n    this.privacySectionHeader = page.locator('[data-test-subj=\"accordion-privacy-settings\"] button[aria-expanded]');\n    this.workbenchSectionHeader = page.locator('[data-test-subj=\"accordion-workbench-settings\"] button[aria-expanded]');\n    this.redisCloudSectionHeader = page.locator('[data-test-subj=\"accordion-cloud-settings\"] button[aria-expanded]');\n    this.advancedSectionHeader = page.locator('[data-test-subj=\"accordion-advanced-settings\"] button[aria-expanded]');\n\n    // General settings\n    this.themeDropdown = page.getByRole('combobox', { name: /color theme/i });\n    this.notificationSwitch = page\n      .locator('div')\n      .filter({ hasText: /^Show notification$/ })\n      .locator('..')\n      .getByRole('switch');\n    this.dateFormatRadioPreselected = page.getByRole('radio', { name: 'Pre-selected formats' });\n    this.dateFormatRadioCustom = page.getByRole('radio', { name: 'Custom' });\n    this.dateFormatDropdown = page\n      .locator('[data-testid=\"select-datetime-format\"]')\n      .or(page.getByRole('combobox').filter({ hasText: /HH:mm/i }));\n    this.customDateFormatInput = page.getByTestId('custom-datetime-input');\n    this.customDateFormatSaveButton = page.getByTestId('datetime-custom-btn');\n    this.timezoneDropdown = page\n      .locator('[data-testid=\"select-timezone\"]')\n      .or(page.getByRole('combobox').filter({ hasText: /Match System/i }));\n    this.datePreview = page.getByTestId('data-preview');\n\n    // Privacy settings\n    this.usageDataSwitch = page\n      .locator('[data-testid=\"switch-option-analytics\"]')\n      .or(page.getByRole('switch').filter({ hasText: /Usage Data/i }));\n    this.privacyPolicyLink = page.getByRole('link', { name: 'Privacy Policy' });\n\n    // Workbench settings\n    const workbenchSection = page.getByTestId('accordion-workbench-settings');\n\n    this.editorCleanupSwitch = page\n      .locator('[data-testid=\"switch-workbench-cleanup\"]')\n      .or(page.getByRole('switch').filter({ hasText: /Clear the Editor/i }));\n    this.pipelineCommandsText = workbenchSection.getByText(/Commands in pipeline/i);\n    this.pipelineCommandsValue = workbenchSection.getByTestId('pipeline-bunch-value');\n    this.pipelineCommandsInput = workbenchSection.getByTestId('pipeline-bunch-input');\n    this.pipelineApplyButton = this.pipelineCommandsInput.locator('xpath=ancestor::form').getByTestId('apply-btn');\n\n    // Advanced settings\n    this.advancedWarning = page.getByRole('alert').filter({ hasText: /Advanced settings/i });\n    this.keysToScanText = page.getByRole('heading', { name: 'Keys to Scan in List view' });\n    this.keysToScanValue = page.getByTestId(/keys-to-scan-value/);\n    this.keysToScanInput = page.getByTestId('keys-to-scan-input');\n    this.keysToScanApplyButton = page.getByTestId('apply-btn');\n\n    // Redis Cloud settings\n    this.apiUserKeysText = page.getByText('API user keys', { exact: true });\n    this.removeApiKeysButton = page.getByRole('button', { name: 'Remove all API keys' });\n    this.autodiscoverButton = page.getByRole('button', { name: 'Autodiscover' });\n    this.createCloudDbButton = page.getByRole('button', { name: 'Create Redis Cloud database' });\n  }\n\n  /**\n   * Navigate to Settings page\n   */\n  async goto(): Promise<void> {\n    await this.page.getByTestId('settings-page-btn').click();\n    await this.waitForLoad();\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.pageTitle.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Expand General settings section\n   */\n  async expandGeneral(): Promise<void> {\n    await this.generalSectionHeader.click();\n    await this.themeDropdown.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Expand Privacy settings section\n   */\n  async expandPrivacy(): Promise<void> {\n    await this.privacySectionHeader.click();\n    await this.usageDataSwitch.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Expand Workbench settings section\n   */\n  async expandWorkbench(): Promise<void> {\n    await this.workbenchSectionHeader.click();\n    await this.editorCleanupSwitch.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Expand Advanced settings section\n   */\n  async expandAdvanced(): Promise<void> {\n    await this.advancedSectionHeader.click();\n    await this.advancedWarning.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Check if General section is expanded\n   */\n  async isGeneralExpanded(): Promise<boolean> {\n    const expanded = await this.generalSectionHeader.getAttribute('aria-expanded');\n    return expanded === 'true';\n  }\n\n  /**\n   * Check if Privacy section is expanded\n   */\n  async isPrivacyExpanded(): Promise<boolean> {\n    const expanded = await this.privacySectionHeader.getAttribute('aria-expanded');\n    return expanded === 'true';\n  }\n\n  /**\n   * Check if Workbench section is expanded\n   */\n  async isWorkbenchExpanded(): Promise<boolean> {\n    const expanded = await this.workbenchSectionHeader.getAttribute('aria-expanded');\n    return expanded === 'true';\n  }\n\n  /**\n   * Check if Advanced section is expanded\n   */\n  async isAdvancedExpanded(): Promise<boolean> {\n    const expanded = await this.advancedSectionHeader.getAttribute('aria-expanded');\n    return expanded === 'true';\n  }\n\n  /**\n   * Expand Redis Cloud settings section\n   */\n  async expandRedisCloud(): Promise<void> {\n    await this.redisCloudSectionHeader.click();\n    await this.apiUserKeysText.waitFor({ state: 'visible', timeout: 5000 });\n  }\n\n  /**\n   * Check if Redis Cloud section is expanded\n   */\n  async isRedisCloudExpanded(): Promise<boolean> {\n    const expanded = await this.redisCloudSectionHeader.getAttribute('aria-expanded');\n    return expanded === 'true';\n  }\n\n  /**\n   * Set custom date format\n   */\n  async setCustomDateFormat(format: string): Promise<void> {\n    await this.dateFormatRadioCustom.click();\n    await this.customDateFormatInput.waitFor({ state: 'visible' });\n    await this.customDateFormatInput.clear();\n    await this.customDateFormatInput.fill(format);\n    await this.customDateFormatSaveButton.click();\n  }\n\n  /**\n   * Get date preview value\n   */\n  async getDatePreviewValue(): Promise<string> {\n    return await this.datePreview.inputValue();\n  }\n\n  /**\n   * Get current theme value\n   */\n  async getCurrentTheme(): Promise<string> {\n    return (await this.themeDropdown.textContent()) || '';\n  }\n\n  /**\n   * Change theme\n   */\n  async changeTheme(theme: 'Light Theme' | 'Dark Theme' | 'System Theme'): Promise<void> {\n    await this.themeDropdown.click();\n    await this.page.getByRole('option', { name: theme }).click();\n  }\n\n  /**\n   * Toggle notification switch\n   */\n  async toggleNotifications(): Promise<void> {\n    await this.notificationSwitch.click();\n  }\n\n  /**\n   * Check if notifications are enabled\n   */\n  async areNotificationsEnabled(): Promise<boolean> {\n    const checked = await this.notificationSwitch.getAttribute('aria-checked');\n    return checked === 'true';\n  }\n\n  /**\n   * Toggle editor cleanup switch\n   */\n  async toggleEditorCleanup(): Promise<void> {\n    await this.editorCleanupSwitch.click();\n  }\n\n  /**\n   * Check if editor cleanup is enabled (switch is on)\n   */\n  async isEditorCleanupEnabled(): Promise<boolean> {\n    const checked = await this.editorCleanupSwitch.getAttribute('aria-checked');\n    return checked === 'true';\n  }\n\n  /**\n   * Get current pipeline commands value (displayed number)\n   */\n  async getPipelineCommandsValue(): Promise<string> {\n    return (await this.pipelineCommandsValue.textContent()) ?? '';\n  }\n\n  /**\n   * Set pipeline commands value and apply (enters edit mode, fills input, clicks Apply)\n   */\n  async setPipelineCommandsAndApply(value: number): Promise<void> {\n    await this.pipelineCommandsValue.click();\n    await this.pipelineCommandsInput.waitFor({ state: 'visible' });\n    await this.pipelineCommandsInput.clear();\n    await this.pipelineCommandsInput.fill(String(value));\n    await this.pipelineApplyButton.click();\n  }\n\n  /**\n   * Get current keys-to-scan value\n   */\n  async getKeysToScan(): Promise<string> {\n    return (await this.keysToScanValue.textContent()) || '';\n  }\n\n  /**\n   * Set keys-to-scan value and apply\n   */\n  async setKeysToScan(value: string): Promise<void> {\n    await this.keysToScanValue.click();\n    await this.keysToScanInput.waitFor({ state: 'visible' });\n    await this.keysToScanInput.clear();\n    await this.keysToScanInput.fill(value);\n    await this.keysToScanApplyButton.click();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/settings/index.ts",
    "content": "export { SettingsPage } from './SettingsPage';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/VectorSearchPage.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\nimport {\n  RqeNotAvailable,\n  WelcomeScreen,\n  IndexList,\n  CreateIndexForm,\n  CreateIndexOnboarding,\n  QueryEditor,\n  QueryLibrary,\n  QueryResults,\n  IndexInfoPanel,\n  DeleteIndexModal,\n  DeleteQueryModal,\n  SaveQueryModal,\n  PickSampleDataModal,\n} from './components';\n\n/**\n * Vector Search Page Object\n * Main page for Vector Search feature, composes all sub-components.\n *\n * Extends InstancePage which provides:\n * - instanceHeader: Database name, stats, breadcrumb\n * - navigationTabs: Browse, Search, Workbench, Analyze, Pub/Sub tabs\n * - bottomPanel: CLI, Command Helper, Profiler buttons\n */\nexport class VectorSearchPage extends InstancePage {\n  readonly rqeNotAvailableWrapper: Locator;\n  readonly loadingWrapper: Locator;\n  readonly welcomeWrapper: Locator;\n  readonly listWrapper: Locator;\n  readonly queryPageWrapper: Locator;\n  readonly createIndexWrapper: Locator;\n  readonly indexCreatedToast: Locator;\n  readonly indexDeletedToast: Locator;\n  readonly sampleDataToast: Locator;\n\n  readonly rqeNotAvailable: RqeNotAvailable;\n  readonly welcomeScreen: WelcomeScreen;\n  readonly indexList: IndexList;\n  readonly createIndexForm: CreateIndexForm;\n  readonly createIndexOnboarding: CreateIndexOnboarding;\n  readonly queryEditor: QueryEditor;\n  readonly queryLibrary: QueryLibrary;\n  readonly queryResults: QueryResults;\n  readonly indexInfoPanel: IndexInfoPanel;\n  readonly deleteIndexModal: DeleteIndexModal;\n  readonly deleteQueryModal: DeleteQueryModal;\n  readonly saveQueryModal: SaveQueryModal;\n  readonly pickSampleDataModal: PickSampleDataModal;\n\n  constructor(page: Page) {\n    super(page);\n\n    this.rqeNotAvailableWrapper = page.getByTestId('vector-search-page--rqe-not-available');\n    this.loadingWrapper = page.getByTestId('vector-search-page--loading');\n    this.welcomeWrapper = page.getByTestId('vector-search-page--welcome');\n    this.listWrapper = page.getByTestId('vector-search-page--list');\n    this.queryPageWrapper = page.getByTestId('vector-search-query-page');\n    this.createIndexWrapper = page.getByTestId('vector-search--create-index--page');\n    this.indexCreatedToast = page.getByRole('alert').getByText('Index created successfully.');\n    this.indexDeletedToast = page.getByRole('alert').getByText('Index has been deleted');\n    this.sampleDataToast = page.getByRole('alert').getByText('Your sample data is now searchable.');\n\n    this.rqeNotAvailable = new RqeNotAvailable(page);\n    this.welcomeScreen = new WelcomeScreen(page);\n    this.indexList = new IndexList(page);\n    this.createIndexForm = new CreateIndexForm(page);\n    this.createIndexOnboarding = new CreateIndexOnboarding(page);\n    this.queryEditor = new QueryEditor(page);\n    this.queryLibrary = new QueryLibrary(page);\n    this.queryResults = new QueryResults(page);\n    this.indexInfoPanel = new IndexInfoPanel(page);\n    this.deleteIndexModal = new DeleteIndexModal(page);\n    this.deleteQueryModal = new DeleteQueryModal(page);\n    this.saveQueryModal = new SaveQueryModal(page);\n    this.pickSampleDataModal = new PickSampleDataModal(page);\n  }\n\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoSearch();\n    await this.waitForLoad();\n  }\n\n  async openSampleDataModal(): Promise<void> {\n    await expect(this.listWrapper).toBeVisible();\n    await this.indexList.openCreateIndex('sample-data');\n    await expect(this.pickSampleDataModal.heading).toBeVisible();\n  }\n\n  async navigateToCreateIndex(): Promise<void> {\n    await expect(this.listWrapper).toBeVisible();\n    await this.indexList.openCreateIndex('existing-data');\n    await expect(this.createIndexForm.container).toBeVisible();\n  }\n\n  async selectQueryLibraryTab(): Promise<void> {\n    await this.queryEditor.libraryTab.click();\n    await this.queryLibrary.container.waitFor({ state: 'visible' });\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.page.waitForLoadState('load');\n    await this.loadingWrapper.waitFor({ state: 'hidden' }).catch(() => {});\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/CreateIndexForm.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { FieldTypeModal } from './FieldTypeModal';\n\n/**\n * Create Index Form component\n * Form for creating a new search index (sample data or existing data)\n */\nexport class CreateIndexForm {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly content: Locator;\n  readonly header: Locator;\n  readonly footer: Locator;\n  readonly createIndexButton: Locator;\n  readonly cancelButton: Locator;\n\n  readonly browserPanel: Locator;\n  readonly selectKeyOnboarding: Locator;\n  readonly selectKeyOnboardingDismiss: Locator;\n  readonly selectKeyOnboardingClose: Locator;\n\n  readonly prefixInput: Locator;\n  readonly addFieldButton: Locator;\n  readonly fieldTypeModal: FieldTypeModal;\n  readonly toolbar: Locator;\n  readonly tableViewButton: Locator;\n  readonly commandViewButton: Locator;\n  readonly commandView: Locator;\n  readonly viewToggle: Locator;\n  readonly emptyState: Locator;\n\n  readonly indexNameDisplay: Locator;\n  readonly indexNameEditButton: Locator;\n  readonly indexNameInput: Locator;\n  readonly indexNameConfirmButton: Locator;\n  readonly indexNameCancelButton: Locator;\n  readonly indexNameErrorIcon: Locator;\n  readonly indexNameErrorTooltip: Locator;\n  readonly createIndexSubmitTooltip: Locator;\n  readonly typeTabs: Locator;\n  readonly indexDetailsTable: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('vector-search--create-index--page');\n    this.content = page.getByTestId('vector-search--create-index--content');\n    this.header = page.getByTestId('vector-search--create-index--header');\n    this.footer = page.getByTestId('vector-search--create-index--footer');\n    this.createIndexButton = page.getByRole('button', { name: 'Create index' });\n    this.cancelButton = this.footer.getByRole('button', { name: 'Cancel' });\n\n    this.browserPanel = page.getByTestId('vector-search--create-index--browser-panel');\n    this.selectKeyOnboarding = page.getByTestId('select-key-onboarding-content');\n    this.selectKeyOnboardingDismiss = page.getByTestId('select-key-onboarding-dismiss');\n    this.selectKeyOnboardingClose = page.getByTestId('select-key-onboarding-close');\n\n    this.prefixInput = page.getByTestId('vector-search--create-index--prefix-input');\n    this.addFieldButton = page.getByRole('button', { name: '+ Add field' });\n    this.fieldTypeModal = new FieldTypeModal(page);\n    this.toolbar = page.getByTestId('vector-search--create-index--toolbar');\n    this.tableViewButton = page.getByRole('button', { name: 'Table view' });\n    this.commandViewButton = page.getByRole('button', { name: 'Command view' });\n    this.commandView = page.getByTestId('vector-search--create-index--command-view');\n    this.viewToggle = page.getByTestId('vector-search--create-index--view-toggle');\n    this.emptyState = page.getByTestId('vector-search--create-index--empty-state');\n\n    this.indexNameDisplay = page.getByTestId('index-name-display');\n    this.indexNameEditButton = page.getByTestId('index-name-edit-btn');\n    this.indexNameInput = page.getByTestId('index-name-edit-input');\n    this.indexNameConfirmButton = page.getByTestId('index-name-confirm-btn');\n    this.indexNameCancelButton = page.getByTestId('index-name-cancel-btn');\n    this.indexNameErrorIcon = page.getByTestId('index-name-error-icon');\n    this.indexNameErrorTooltip = page.getByTestId('index-name-error-tooltip');\n    this.createIndexSubmitTooltip = page.getByTestId('vector-search--create-index--submit-tooltip');\n\n    this.typeTabs = page.getByTestId('vs-keys-type-tabs');\n    this.indexDetailsTable = page.getByTestId('index-details-table');\n  }\n\n  /**\n   * Switch the browser panel key type tab (HASH or JSON).\n   */\n  async switchKeyTypeTab(tab: 'HASH' | 'JSON'): Promise<void> {\n    await this.typeTabs.getByRole('tab', { name: tab }).click();\n  }\n\n  /**\n   * Select a key from the browser panel in the \"existing data\" create-index flow.\n   *\n   * The key tree uses `:` as the default delimiter. A key like `prefix:key1`\n   * renders as a collapsed folder `prefix` with a leaf `key1` inside.\n   * Folder nodes have a predictable `data-testid`, but leaf nodes include\n   * internal bookkeeping in their testid, so we locate them via\n   * `role=\"treeitem\"` filtered by the displayed short name.\n   *\n   * The tree is virtualized (react-vtree) and only renders items within\n   * the scroll viewport. When parallel tests accumulate many keys the\n   * target folder may be off-screen; {@link scrollTreeToNode} handles\n   * scrolling to reveal it.\n   */\n  async selectKey(keyName: string, delimiter = ':'): Promise<void> {\n    await this.browserPanel.waitFor({ state: 'visible' });\n    await this.browserPanel.getByRole('treeitem').first().waitFor({ state: 'visible' });\n\n    const delimiterIndex = keyName.lastIndexOf(delimiter);\n\n    if (delimiterIndex !== -1) {\n      const folder = keyName.substring(0, delimiterIndex);\n      const collapsed = this.page.getByTestId(`node-item_${folder}`);\n      const expanded = this.page.getByTestId(`node-item_${folder}--expanded`);\n      const folderNode = expanded.or(collapsed);\n\n      await this.scrollTreeToNode(folderNode);\n\n      if (await collapsed.isVisible()) {\n        await collapsed.click();\n        await expanded.waitFor({ state: 'visible' });\n      }\n    }\n\n    const shortName = delimiterIndex !== -1 ? keyName.substring(delimiterIndex + delimiter.length) : keyName;\n\n    const leafNode = this.browserPanel.getByRole('treeitem').filter({ hasText: shortName });\n\n    await this.scrollTreeToNode(leafNode);\n    await leafNode.click();\n  }\n\n  /**\n   * Scroll the virtualized key tree until {@link node} is visible.\n   *\n   * The tree only renders items inside its scroll viewport, so off-screen\n   * nodes don't exist in the DOM. We position the mouse over the tree and\n   * send wheel events to scroll incrementally until the node appears.\n   *\n   * After each wheel event we use {@link Locator.waitFor} with a short\n   * timeout instead of the synchronous {@link Locator.isVisible} check,\n   * giving React's virtualized renderer time to process the scroll and\n   * mount new items before we decide whether to keep scrolling.\n   */\n  private async scrollTreeToNode(node: Locator): Promise<void> {\n    if (await node.isVisible()) {\n      return;\n    }\n\n    const firstItem = this.browserPanel.getByRole('treeitem').first();\n    const box = await firstItem.boundingBox();\n    if (!box) {\n      return;\n    }\n\n    await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);\n\n    const MAX_SCROLL_ATTEMPTS = 50;\n    const SETTLE_TIMEOUT_MS = 200;\n    for (let i = 0; i < MAX_SCROLL_ATTEMPTS; i++) {\n      await this.page.mouse.wheel(0, 200);\n\n      try {\n        await node.waitFor({ state: 'visible', timeout: SETTLE_TIMEOUT_MS });\n        return;\n      } catch {\n        // Node not rendered yet after this scroll step, continue\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/CreateIndexOnboarding.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Create Index Onboarding component\n * Guided popovers that walk the user through the create index form steps:\n * DefineIndex → IndexPrefix → FieldName → SampleValue → IndexingType → CommandView\n */\nexport class CreateIndexOnboarding {\n  readonly page: Page;\n\n  readonly popover: Locator;\n  readonly skipButton: Locator;\n\n  static readonly STEPS = [\n    'defineIndex',\n    'indexPrefix',\n    'fieldName',\n    'sampleValue',\n    'indexingType',\n    'commandView',\n  ] as const;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.popover = page.getByTestId(/create-index-onboarding-popover/);\n    this.skipButton = page.getByRole('button', { name: 'Skip tour' });\n  }\n\n  stepPopover(step: string): Locator {\n    return this.page.getByTestId(`create-index-onboarding-popover-${step}`);\n  }\n\n  stepAction(step: string): Locator {\n    return this.page.getByTestId(`create-index-onboarding-action-${step}`);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/DeleteConfirmationModal.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\nexport class DeleteIndexModal {\n  readonly page: Page;\n\n  readonly dialog: Locator;\n  readonly message: Locator;\n  readonly confirmButton: Locator;\n  readonly cancelButton: Locator;\n  readonly closeButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.dialog = page.getByRole('dialog', { name: 'Delete index' });\n    this.message = this.dialog.getByTestId('delete-index-modal-message');\n    this.confirmButton = this.dialog.getByRole('button', { name: 'Delete index' });\n    this.cancelButton = this.dialog.getByRole('button', { name: 'Keep index' });\n    this.closeButton = this.dialog.getByTestId('delete-index-modal-close');\n  }\n}\n\nexport class DeleteQueryModal {\n  readonly page: Page;\n\n  readonly dialog: Locator;\n  readonly message: Locator;\n  readonly confirmButton: Locator;\n  readonly cancelButton: Locator;\n  readonly closeButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.dialog = page.getByRole('dialog', { name: 'Delete query' });\n    this.message = this.dialog.getByTestId('query-library-delete-modal-message');\n    this.confirmButton = this.dialog.getByRole('button', { name: 'Delete query' });\n    this.cancelButton = this.dialog.getByRole('button', { name: 'Keep query' });\n    this.closeButton = this.dialog.getByTestId('query-library-delete-modal-close');\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/FieldTypeModal.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Field Type Modal (Add / Edit field)\n *\n * Shown when the user clicks \"+ Add field\" or edits an existing field\n * in the create-index schema table.\n */\nexport class FieldTypeModal {\n  readonly page: Page;\n\n  readonly dialog: Locator;\n  readonly form: Locator;\n  readonly fieldNameInput: Locator;\n  readonly fieldTypeSelect: Locator;\n  readonly saveButton: Locator;\n  readonly cancelButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.dialog = page.getByRole('dialog', { name: /Add field|Edit field/ });\n    this.form = page.getByTestId('field-type-modal-form');\n    this.fieldNameInput = page.getByTestId('field-type-modal-field-name');\n    this.fieldTypeSelect = page.getByTestId('field-type-modal-field-type');\n    this.saveButton = this.dialog.getByRole('button', { name: /^(Save|Add)$/ });\n    this.cancelButton = this.dialog.getByRole('button', { name: 'Cancel' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/IndexInfoPanel.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Index Info Panel component\n * Side panel showing index details\n */\nexport class IndexInfoPanel {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly title: Locator;\n  readonly closeButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('view-index-panel');\n    this.title = page.getByText('View index');\n    this.closeButton = page.getByRole('button', { name: 'Close panel' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/IndexList.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Index List component\n * Table of existing search indexes on the list page\n */\nexport class IndexList {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly table: Locator;\n  readonly createIndexButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('vector-search-page--list');\n    this.table = page.getByTestId('vector-search--list--table');\n    this.createIndexButton = page.getByRole('button', { name: '+ Create search index' });\n  }\n\n  getIndexName(indexName: string): Locator {\n    return this.page.getByTestId(`index-name-${indexName}`);\n  }\n\n  getCreateIndexMenuItem(option: 'sample-data' | 'existing-data'): Locator {\n    const text = option === 'sample-data' ? 'Use sample data' : 'Use existing data';\n    return this.page.getByRole('menuitem', { name: text });\n  }\n\n  /**\n   * Open the \"Create index\" menu and click the given option\n   */\n  async openCreateIndex(option: 'sample-data' | 'existing-data'): Promise<void> {\n    await this.createIndexButton.click();\n    await this.getCreateIndexMenuItem(option).click();\n  }\n\n  /**\n   * Get the query button for a specific index row\n   */\n  getQueryButton(indexId: string): Locator {\n    return this.page.getByTestId(`index-query-btn-${indexId}`);\n  }\n\n  /**\n   * Click the query button for a specific index to navigate to the query page\n   */\n  async openQuery(indexName: string): Promise<void> {\n    await this.getQueryButton(indexName).click();\n  }\n\n  /**\n   * Get the actions menu trigger for a specific index row\n   */\n  getActionsMenuTrigger(indexId: string): Locator {\n    return this.page.getByTestId(`index-actions-menu-trigger-${indexId}`);\n  }\n\n  /**\n   * Get a menu action item (e.g. \"Browse\", \"View\", \"Delete\") from the actions menu\n   */\n  getActionMenuItem(name: string): Locator {\n    return this.page.getByRole('menuitem', { name });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/PickSampleDataModal.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\nexport class PickSampleDataModal {\n  readonly page: Page;\n\n  readonly heading: Locator;\n  readonly subtitle: Locator;\n  readonly radioGroup: Locator;\n  readonly closeButton: Locator;\n  readonly cancelButton: Locator;\n  readonly seeIndexDefinitionButton: Locator;\n  readonly startQueryingButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.heading = page.getByTestId('pick-sample-data-modal--heading');\n    this.subtitle = page.getByText('Select a sample dataset.');\n    this.radioGroup = page.getByTestId('pick-sample-data-modal--radio-group');\n    this.closeButton = page.getByTestId('pick-sample-data-modal--close');\n    this.cancelButton = page.getByRole('button', { name: 'Cancel' });\n    this.seeIndexDefinitionButton = page.getByRole('button', { name: 'See index definition' });\n    this.startQueryingButton = page.getByRole('button', { name: 'Start querying' });\n  }\n\n  getSampleDataOption(value: string): Locator {\n    return this.page.getByTestId(`pick-sample-data-modal--option-${value}`);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/QueryEditor.ts",
    "content": "import { Page, Locator, expect } from '@playwright/test';\n\nexport class QueryEditor {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly actionsBar: Locator;\n  readonly runButton: Locator;\n  readonly explainButton: Locator;\n  readonly profileButton: Locator;\n  readonly saveButton: Locator;\n\n  readonly editorLibraryToggle: Locator;\n  readonly editorTab: Locator;\n  readonly libraryTab: Locator;\n\n  readonly textbox: Locator;\n\n  readonly explainTooltip: Locator;\n  readonly profileTooltip: Locator;\n\n  readonly queryOnboarding: Locator;\n  readonly queryOnboardingDismiss: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('vector-search-query-editor');\n    this.actionsBar = page.getByTestId('vector-search-actions');\n    this.runButton = this.actionsBar.getByRole('button', { name: 'submit' });\n    this.explainButton = this.actionsBar.getByRole('button', { name: 'explain' });\n    this.profileButton = this.actionsBar.getByRole('button', { name: 'profile' });\n    this.saveButton = this.actionsBar.getByRole('button', { name: 'save' });\n\n    this.editorLibraryToggle = page.getByTestId('editor-library-toggle');\n    this.editorTab = this.editorLibraryToggle.getByRole('button', { name: 'Query editor' });\n    this.libraryTab = this.editorLibraryToggle.getByRole('button', { name: 'Query library' });\n\n    this.textbox = this.container.getByRole('textbox');\n\n    this.explainTooltip = page.getByTestId('explain-tooltip');\n    this.profileTooltip = page.getByTestId('profile-tooltip');\n\n    this.queryOnboarding = page.getByText('Index created successfully.');\n    this.queryOnboardingDismiss = page.getByRole('button', { name: 'Got it' });\n  }\n\n  async typeQuery(query: string): Promise<void> {\n    // Triple-click selects the entire line without triggering Monaco autocomplete\n    await this.container.click({ clickCount: 3 });\n    await this.page.keyboard.type(query);\n  }\n\n  async clearQuery(): Promise<void> {\n    await this.container.click({ clickCount: 3 });\n    await this.page.keyboard.press('Backspace');\n    await expect(this.textbox).toHaveValue('');\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/QueryLibrary.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Query Library component\n * Panel for managing saved queries\n */\nexport class QueryLibrary {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly searchInput: Locator;\n  readonly emptyMessage: Locator;\n  readonly errorMessage: Locator;\n  readonly deleteSuccessToast: Locator;\n  readonly allItems: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('query-library-view');\n    this.searchInput = page.getByPlaceholder('Search query');\n    this.emptyMessage = this.container.getByText('No saved queries yet');\n    this.errorMessage = this.container.getByText('Failed to load');\n    this.deleteSuccessToast = page.getByText('Query has been deleted.');\n    this.allItems = this.container.locator('[data-testid^=\"query-library-item-\"][data-testid$=\"-header\"]');\n  }\n\n  getItem(id: string): Locator {\n    return this.page.getByTestId(`query-library-item-${id}`);\n  }\n\n  getItemHeader(id: string): Locator {\n    return this.page.getByTestId(`query-library-item-${id}-header`);\n  }\n\n  getItemBody(id: string): Locator {\n    return this.page.getByTestId(`query-library-item-${id}-body`);\n  }\n\n  getItemByName(name: string): Locator {\n    return this.allItems.filter({ hasText: name });\n  }\n\n  getItemRunButton(id: string): Locator {\n    return this.getItem(id).getByRole('button', { name: 'Run' });\n  }\n\n  getItemLoadButton(id: string): Locator {\n    return this.getItem(id).getByRole('button', { name: 'Load' });\n  }\n\n  getItemDeleteButton(id: string): Locator {\n    return this.getItem(id).getByRole('button', { name: 'Delete query' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/QueryResults.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Query Results component\n * Displays results from search queries.\n *\n * The results panel contains:\n * - A \"Clear Results\" button to remove all results\n * - Individual query cards, each with actions (re-run, delete, expand, fullscreen)\n */\nexport class QueryResults {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly noResults: Locator;\n  readonly clearResultsButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('query-results');\n    this.noResults = page.getByText('Your query results will appear here once you run a query.');\n    this.clearResultsButton = page.getByRole('button', { name: 'Clear Results' });\n  }\n\n  /**\n   * Get a query card by its container testid\n   */\n  getCard(id: string): Locator {\n    return this.page.getByTestId(`query-card-container-${id}`);\n  }\n\n  /**\n   * Get the first query card header (clickable to expand/collapse)\n   */\n  get firstCardHeader(): Locator {\n    return this.container.getByTestId('query-card-open').first();\n  }\n\n  /**\n   * Get the re-run button on the first result card.\n   * exact: true prevents matching the outer div[role=\"button\"] card header\n   * whose accessible name contains \"Re-run command\" as a substring.\n   */\n  get firstCardReRunButton(): Locator {\n    return this.container.getByRole('button', { name: 'Re-run command', exact: true }).first();\n  }\n\n  /**\n   * Get the delete button on the first result card.\n   * exact: true prevents matching the outer div[role=\"button\"] card header\n   * whose accessible name contains \"Delete command\" as a substring.\n   */\n  get firstCardDeleteButton(): Locator {\n    return this.container.getByRole('button', { name: 'Delete command', exact: true }).first();\n  }\n\n  /**\n   * Get the fullscreen toggle on the first result card.\n   * exact: true prevents matching the outer div[role=\"button\"] card header.\n   */\n  get firstCardFullScreenButton(): Locator {\n    return this.container.getByRole('button', { name: 'Open full screen', exact: true }).first();\n  }\n\n  /**\n   * Get the expand/collapse toggle on the first result card.\n   * exact: true prevents matching the outer div[role=\"button\"] card header.\n   */\n  get firstCardToggleCollapseButton(): Locator {\n    return this.container.getByRole('button', { name: 'toggle collapse', exact: true }).first();\n  }\n\n  /**\n   * Get the command text of the first result card (always visible in header)\n   */\n  get firstCardCommand(): Locator {\n    return this.container.getByTestId('query-card-command').first();\n  }\n\n  /**\n   * Get the result body of the first card (only visible when expanded).\n   * Matches either the plugin result or the CLI result wrapper.\n   */\n  get firstCardResult(): Locator {\n    return this.container\n      .locator('[data-testid=\"query-plugin-result\"], [data-testid=\"query-cli-result-wrapper\"]')\n      .first();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/RqeNotAvailable.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * RQE Not Available component\n * Shown when Redis instance doesn't have the Search module\n */\nexport class RqeNotAvailable {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly title: Locator;\n  readonly description: Locator;\n  readonly featureList: Locator;\n  readonly getStartedButton: Locator;\n  readonly learnMoreLink: Locator;\n  readonly illustration: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('rqe-not-available');\n    this.title = page.getByText('Redis Query Engine is not available for this database');\n    this.description = page.getByTestId('rqe-description');\n    this.featureList = page.getByTestId('rqe-feature-list');\n    this.getStartedButton = page.getByRole('button', { name: 'Get started for free' });\n    this.learnMoreLink = page.getByRole('link', { name: 'Learn more' });\n    this.illustration = page.getByTestId('rqe-illustration');\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/SaveQueryModal.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\nexport class SaveQueryModal {\n  readonly page: Page;\n\n  readonly body: Locator;\n  readonly nameInput: Locator;\n  readonly saveButton: Locator;\n  readonly cancelButton: Locator;\n  readonly closeButton: Locator;\n\n  readonly successToast: Locator;\n  readonly successToastGoToLibrary: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.body = page.getByRole('dialog', { name: 'Save query' });\n    this.nameInput = page.getByPlaceholder('Enter command name');\n    this.saveButton = this.body.getByRole('button', { name: 'Save query' });\n    this.cancelButton = this.body.getByRole('button', { name: 'Cancel' });\n    this.closeButton = page.getByTestId('save-query-modal-close');\n\n    this.successToast = page.getByText('Query saved to your library.');\n    this.successToastGoToLibrary = page.getByRole('button', { name: 'Go to Query Library' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/WelcomeScreen.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\nexport class WelcomeScreen {\n  readonly page: Page;\n\n  readonly container: Locator;\n  readonly title: Locator;\n  readonly subtitle: Locator;\n  readonly features: Locator;\n  readonly trySampleDataButton: Locator;\n  readonly useMyDatabaseButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.container = page.getByTestId('welcome-screen');\n    this.title = page.getByRole('heading', { name: 'Search your data at in-memory speed' });\n    this.subtitle = page.getByText('Discover how Redis enables full-text and vector search');\n    this.features = page.getByTestId('welcome-screen--features');\n    this.trySampleDataButton = page.getByRole('button', { name: 'Try with sample data' });\n    this.useMyDatabaseButton = page.getByRole('button', { name: 'Use data from my database' });\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/components/index.ts",
    "content": "export { RqeNotAvailable } from './RqeNotAvailable';\nexport { WelcomeScreen } from './WelcomeScreen';\nexport { IndexList } from './IndexList';\nexport { CreateIndexForm } from './CreateIndexForm';\nexport { CreateIndexOnboarding } from './CreateIndexOnboarding';\nexport { FieldTypeModal } from './FieldTypeModal';\nexport { QueryEditor } from './QueryEditor';\nexport { QueryLibrary } from './QueryLibrary';\nexport { QueryResults } from './QueryResults';\nexport { IndexInfoPanel } from './IndexInfoPanel';\nexport { DeleteIndexModal, DeleteQueryModal } from './DeleteConfirmationModal';\nexport { SaveQueryModal } from './SaveQueryModal';\nexport { PickSampleDataModal } from './PickSampleDataModal';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/vector-search/index.ts",
    "content": "export { VectorSearchPage } from './VectorSearchPage';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/workbench/WorkbenchPage.ts",
    "content": "import { Page, Locator } from '@playwright/test';\nimport { InstancePage } from '../InstancePage';\nimport { Editor } from './components/Editor';\nimport { ResultsPanel } from './components/ResultsPanel';\n\n/**\n * Workbench Page Object\n * Handles the Workbench tab for executing Redis commands\n *\n * Extends InstancePage to get access to:\n * - instanceHeader (database name, stats, breadcrumb)\n * - navigationTabs (Browse, Workbench, Analyze, Pub/Sub)\n * - bottomPanel (CLI, Command Helper, Profiler)\n */\nexport class WorkbenchPage extends InstancePage {\n  // Components\n  readonly editor: Editor;\n  readonly resultsPanel: ResultsPanel;\n\n  // Main elements\n  readonly submitButton: Locator;\n  readonly clearResultsButton: Locator;\n  readonly rawModeButton: Locator;\n  readonly groupResultsButton: Locator;\n\n  // Tutorial links\n  readonly introToSearchLink: Locator;\n  readonly basicUseCasesLink: Locator;\n  readonly introToVectorSearchLink: Locator;\n\n  // No results state\n  readonly noResultsContainer: Locator;\n  readonly noResultsTitle: Locator;\n  readonly exploreButton: Locator;\n\n  constructor(page: Page) {\n    super(page);\n\n    // Initialize components\n    this.editor = new Editor(page);\n    this.resultsPanel = new ResultsPanel(page);\n\n    // Main elements\n    this.submitButton = page.getByTestId('btn-submit');\n    this.clearResultsButton = page.locator('button:has-text(\"Clear Results\")');\n    this.rawModeButton = page.getByTestId('btn-change-mode');\n    this.groupResultsButton = page.getByTestId('btn-change-group-mode');\n\n    // Tutorial links\n    this.introToSearchLink = page.getByTestId('query-tutorials-link_sq-intro');\n    this.basicUseCasesLink = page.getByTestId('query-tutorials-link_redis_use_cases_basic');\n    this.introToVectorSearchLink = page.getByTestId('query-tutorials-link_vss-intro');\n\n    // No results state\n    this.noResultsContainer = page.getByTestId('wb_no-results');\n    this.noResultsTitle = page.getByTestId('wb_no-results__title');\n    this.exploreButton = page.getByTestId('no-results-explore-btn');\n  }\n\n  /**\n   * Navigate to Workbench page for a specific database\n   */\n  async goto(databaseId: string): Promise<void> {\n    await this.gotoDatabase(databaseId);\n    await this.navigationTabs.gotoWorkbench();\n    await this.waitForLoad();\n  }\n\n  async waitForLoad(): Promise<void> {\n    await this.editor.waitForEditor();\n  }\n\n  /**\n   * Execute a Redis command and wait for result\n   */\n  async executeCommand(command: string): Promise<void> {\n    const previousCount = await this.resultsPanel.getResultCount();\n    await this.editor.setCommand(command);\n    await this.submitButton.click();\n    await this.resultsPanel.waitForNewResult(previousCount);\n  }\n\n  /**\n   * Execute a command and return the result text\n   */\n  async executeAndGetResult(command: string): Promise<string> {\n    await this.executeCommand(command);\n    return this.resultsPanel.getLastResultText();\n  }\n\n  /**\n   * Check if no results state is displayed\n   */\n  async hasNoResults(): Promise<boolean> {\n    return this.noResultsContainer.isVisible();\n  }\n\n  /**\n   * Clear all results\n   */\n  async clearResults(): Promise<void> {\n    if (await this.clearResultsButton.isVisible()) {\n      await this.clearResultsButton.click();\n      await this.noResultsContainer.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});\n    }\n  }\n\n  /**\n   * Toggle raw mode\n   */\n  async toggleRawMode(): Promise<void> {\n    await this.rawModeButton.click();\n  }\n\n  /**\n   * Toggle group results mode\n   */\n  async toggleGroupResults(): Promise<void> {\n    await this.groupResultsButton.click();\n  }\n\n  /**\n   * Get the number of results displayed\n   */\n  async getResultCount(): Promise<number> {\n    return this.resultsPanel.getResultCount();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/workbench/components/Editor.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Editor component for Workbench\n * Handles the Monaco editor for entering Redis commands\n */\nexport class Editor {\n  readonly page: Page;\n  readonly container: Locator;\n  readonly textbox: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.container = page.getByTestId('main-input-container-area');\n    this.textbox = page.getByRole('textbox', { name: /Editor content/i });\n  }\n\n  /**\n   * Wait for editor to be ready\n   */\n  async waitForEditor(): Promise<void> {\n    await this.container.waitFor({ state: 'visible' });\n  }\n\n  /**\n   * Set command in the editor\n   */\n  async setCommand(command: string): Promise<void> {\n    await this.container.click();\n    await this.textbox.fill(command);\n  }\n\n  /**\n   * Get current command from editor\n   */\n  async getCommand(): Promise<string> {\n    return this.textbox.inputValue();\n  }\n\n  /**\n   * Clear the editor\n   */\n  async clear(): Promise<void> {\n    await this.container.click();\n    await this.textbox.fill('');\n  }\n\n  /**\n   * Append command to existing content\n   */\n  async appendCommand(command: string): Promise<void> {\n    await this.container.click();\n    const currentValue = await this.getCommand();\n    const newValue = currentValue ? `${currentValue}\\n${command}` : command;\n    await this.textbox.fill(newValue);\n  }\n\n  /**\n   * Check if editor is visible\n   */\n  async isVisible(): Promise<boolean> {\n    return this.container.isVisible();\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/workbench/components/ResultsPanel.ts",
    "content": "import { Page, Locator } from '@playwright/test';\n\n/**\n * Results Panel component for Workbench\n * Handles the results display area\n */\nexport class ResultsPanel {\n  readonly page: Page;\n  readonly container: Locator;\n  readonly resultCards: Locator;\n  readonly lastResult: Locator;\n  readonly resultText: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.container = page.locator('[data-testid^=\"query-card-container-\"]').first();\n    this.resultCards = page.locator('[data-testid^=\"query-card-container-\"]');\n    this.lastResult = page.getByTestId('query-cli-result').first();\n    this.resultText = page.getByTestId('query-cli-card-result').first();\n  }\n\n  /**\n   * Wait for a result to appear\n   */\n  async waitForResult(timeout = 10000): Promise<void> {\n    await this.container.waitFor({ state: 'visible', timeout });\n  }\n\n  /**\n   * Wait for a new result to appear (useful when results already exist)\n   */\n  async waitForNewResult(previousCount: number, timeout = 10000): Promise<void> {\n    await this.page.waitForFunction(\n      (expectedCount) => {\n        const cards = document.querySelectorAll('[data-testid^=\"query-card-container-\"]');\n        return cards.length > expectedCount;\n      },\n      previousCount,\n      { timeout },\n    );\n  }\n\n  /**\n   * Get the text of the most recent result (first in DOM order)\n   */\n  async getLastResultText(): Promise<string> {\n    return this.getResultTextByIndex(0);\n  }\n\n  /**\n   * Get the number of result cards\n   */\n  async getResultCount(): Promise<number> {\n    return this.resultCards.count();\n  }\n\n  /**\n   * Get result text by index (0 = most recent)\n   */\n  async getResultTextByIndex(index: number): Promise<string> {\n    const resultCard = this.resultCards.nth(index);\n    const resultText = resultCard.locator('[data-testid=\"query-cli-card-result\"]');\n    await resultText.waitFor({ state: 'visible', timeout: 5000 });\n    return resultText.innerText();\n  }\n\n  /**\n   * Get command text from result card by index\n   */\n  async getCommandTextByIndex(index: number): Promise<string> {\n    const resultCard = this.resultCards.nth(index);\n    const commandText = resultCard.locator('[data-testid=\"query-card-command\"]');\n    return commandText.innerText();\n  }\n\n  /**\n   * Delete result by index\n   */\n  async deleteResultByIndex(index: number): Promise<void> {\n    const resultCard = this.resultCards.nth(index);\n    const deleteButton = resultCard.locator('[data-testid=\"delete-command\"]');\n    await deleteButton.click();\n  }\n\n  /**\n   * Re-run command by index\n   */\n  async rerunCommandByIndex(index: number): Promise<void> {\n    const resultCard = this.resultCards.nth(index);\n    const rerunButton = resultCard.locator('[data-testid=\"re-run-command\"]');\n    await rerunButton.click();\n  }\n\n  /**\n   * Copy command by index\n   */\n  async copyCommandByIndex(index: number): Promise<void> {\n    const resultCard = this.resultCards.nth(index);\n    const copyButton = resultCard.locator('[data-testid=\"copy-command-btn\"]');\n    await copyButton.click();\n  }\n\n  /**\n   * Check if results are visible\n   */\n  async hasResults(): Promise<boolean> {\n    const count = await this.getResultCount();\n    return count > 0;\n  }\n\n  /**\n   * Get execution time of last result\n   */\n  async getLastExecutionTime(): Promise<string> {\n    const timeValue = this.page.getByTestId('command-execution-time-value').first();\n    return timeValue.innerText();\n  }\n\n  /**\n   * Get datetime of result by index (0 = most recent)\n   */\n  async getDateTimeByIndex(index: number): Promise<string> {\n    const resultCard = this.resultCards.nth(index);\n    const dateTime = resultCard.locator('[data-testid=\"command-execution-date-time\"]');\n    await dateTime.waitFor({ state: 'visible', timeout: 5000 });\n    return dateTime.innerText();\n  }\n\n  /**\n   * Get datetime of the most recent result\n   */\n  async getLastDateTime(): Promise<string> {\n    return this.getDateTimeByIndex(0);\n  }\n}\n"
  },
  {
    "path": "tests/e2e-playwright/pages/workbench/components/index.ts",
    "content": "export { Editor } from './Editor';\nexport { ResultsPanel } from './ResultsPanel';\n"
  },
  {
    "path": "tests/e2e-playwright/pages/workbench/index.ts",
    "content": "export { WorkbenchPage } from './WorkbenchPage';\nexport { Editor, ResultsPanel } from './components';\n"
  },
  {
    "path": "tests/e2e-playwright/playwright.config.ts",
    "content": "import { defineConfig, devices, PlaywrightTestConfig } from '@playwright/test';\nimport { appConfig } from './config';\n\n/**\n * Custom test options for our projects\n */\ninterface CustomTestOptions {\n  electronExecutablePath: string | undefined;\n  apiUrl: string;\n}\n\nconst config: PlaywrightTestConfig<CustomTestOptions> = {\n  forbidOnly: !!process.env.CI,\n  // Retry failed tests to handle transient failures\n  retries: process.env.CI ? 2 : 1,\n  reporter: [['html'], ['list'], ['json', { outputFile: 'test-results/results.json' }]],\n\n  use: {\n    trace: 'retain-on-failure',\n    screenshot: 'only-on-failure',\n    video: 'retain-on-failure',\n    viewport: { width: 1920, height: 1080 },\n  },\n\n  // Projects allow different test configurations (parallelism, setup, etc.)\n  // Run specific project: npx playwright test --project=chromium\n  // Run all projects: npx playwright test\n  //\n  // Setup projects run before their dependent test projects.\n  // Teardown projects run after all tests complete.\n  projects: [\n    // ============================================\n    // Setup Projects (run first)\n    // ============================================\n    {\n      name: 'browser-setup',\n      testMatch: /setup\\/browser\\.setup\\.ts/,\n      teardown: 'browser-teardown',\n    },\n    {\n      name: 'electron-setup',\n      testMatch: /setup\\/electron\\.setup\\.ts/,\n      teardown: 'electron-teardown',\n    },\n\n    // ============================================\n    // Teardown Projects (run last)\n    // ============================================\n    {\n      name: 'browser-teardown',\n      testMatch: /setup\\/browser\\.teardown\\.ts/,\n    },\n    {\n      name: 'electron-teardown',\n      testMatch: /setup\\/electron\\.teardown\\.ts/,\n    },\n\n    // ============================================\n    // Browser Projects (Chromium)\n    // ============================================\n    {\n      name: 'chromium',\n      testDir: './tests/main',\n      grepInvert: /@serial/,\n      dependencies: ['browser-setup'],\n      use: {\n        ...devices['Desktop Chrome'],\n        baseURL: appConfig.clientUrl,\n        apiUrl: appConfig.apiUrl,\n      },\n      workers: 4,\n      timeout: 60000,\n    },\n    {\n      name: 'chromium-serial',\n      testDir: './tests/main',\n      grep: /@serial/,\n      dependencies: ['browser-setup'],\n      use: {\n        ...devices['Desktop Chrome'],\n        baseURL: appConfig.clientUrl,\n        apiUrl: appConfig.apiUrl,\n      },\n      fullyParallel: false,\n      workers: 1,\n      timeout: 60000,\n    },\n\n    // ============================================\n    // Electron Projects\n    // ============================================\n    {\n      name: 'electron',\n      testDir: './tests/main',\n      dependencies: ['electron-setup'],\n      use: {\n        electronExecutablePath: appConfig.electronExecutablePath,\n        apiUrl: appConfig.electronApiUrl,\n      },\n      // Electron tests run with single worker (single app instance)\n      fullyParallel: false,\n      workers: 1,\n      timeout: 60000,\n    },\n    // Example: auto-update tests for Electron\n    // {\n    //   name: 'electron-auto-update',\n    //   testDir: './tests/auto-update',\n    //   dependencies: ['electron-setup'],\n    //   use: {\n    //     electronExecutablePath,\n    //     apiUrl: appConfig.electronApiUrl,\n    //   },\n    //   fullyParallel: false,\n    //   workers: 1,\n    //   timeout: 180000,\n    // },\n  ],\n\n  expect: {\n    timeout: 10000,\n  },\n};\n\nexport default defineConfig(config);\n"
  },
  {
    "path": "tests/e2e-playwright/setup/browser.setup.ts",
    "content": "import { test as setup, request } from '@playwright/test';\nimport { appConfig } from '../config';\nimport { ApiHelper } from '../helpers/api';\n\n/**\n * Browser setup project\n * - Verifies the application is running\n * - Cleans up any leftover test data\n */\nsetup('browser setup', async () => {\n  console.log('\\n🚀 Running browser global setup...');\n\n  // Verify the application is running\n  console.log(`   Checking app at ${appConfig.clientUrl}...`);\n  const context = await request.newContext({\n    baseURL: appConfig.clientUrl,\n  });\n\n  try {\n    const response = await context.get('/', { timeout: 10000 });\n    if (!response.ok()) {\n      throw new Error(`Application returned status ${response.status()}`);\n    }\n    console.log('   ✅ Application is running');\n  } catch (error) {\n    console.error('   ❌ Application is not running!');\n    console.error(`   Make sure RedisInsight is running at ${appConfig.clientUrl}`);\n    throw new Error(`Application health check failed: ${error}`);\n  } finally {\n    await context.dispose();\n  }\n\n  // Use ApiHelper for API health check and cleanup\n  const apiHelper = new ApiHelper({ apiUrl: appConfig.apiUrl });\n\n  try {\n    // Verify API is running by fetching databases\n    console.log(`   Checking API at ${appConfig.apiUrl}...`);\n    await apiHelper.getDatabases();\n    console.log('   ✅ API is running');\n\n    // Clean up test databases from previous runs\n    console.log('   Cleaning up test databases from previous runs...');\n    const deletedCount = await apiHelper.deleteTestDatabases();\n\n    if (deletedCount > 0) {\n      console.log(`   ✅ Cleaned up ${deletedCount} test database(s)`);\n    } else {\n      console.log('   ✅ No test databases to clean up');\n    }\n  } catch (error) {\n    console.error('   ❌ API is not running or cleanup failed!');\n    console.error(`   Make sure RedisInsight API is running at ${appConfig.apiUrl}`);\n    throw new Error(`API health check failed: ${error}`);\n  } finally {\n    await apiHelper.dispose();\n  }\n\n  console.log('✅ Browser global setup complete\\n');\n});\n"
  },
  {
    "path": "tests/e2e-playwright/setup/browser.teardown.ts",
    "content": "import { test as teardown } from '@playwright/test';\nimport { appConfig } from 'e2eSrc/config';\nimport { ApiHelper } from '../helpers';\n\n/**\n * Browser teardown project\n * - Cleans up test data created during tests\n */\nteardown('browser teardown', async () => {\n  console.log('\\n🧹 Running browser global teardown...');\n\n  const apiHelper = new ApiHelper({ apiUrl: appConfig.apiUrl });\n\n  try {\n    const deletedCount = await apiHelper.deleteTestDatabases();\n\n    if (deletedCount > 0) {\n      console.log(`   ✅ Cleaned up ${deletedCount} test database(s)`);\n    } else {\n      console.log('   ✅ No test databases to clean up');\n    }\n  } catch (error) {\n    console.warn('   ⚠️ Could not clean up test databases:', error);\n  } finally {\n    await apiHelper.dispose();\n  }\n\n  console.log('✅ Browser global teardown complete\\n');\n});\n"
  },
  {
    "path": "tests/e2e-playwright/setup/electron.setup.ts",
    "content": "import { test as setup } from '@playwright/test';\n\n/**\n * Electron setup project\n *\n * Note: We skip health checks because the Electron app is launched\n * by Playwright fixtures AFTER setup runs.\n * The API won't be available until the Electron app starts.\n */\nsetup('electron setup', async () => {\n  console.log('\\n🚀 Running Electron global setup...');\n  console.log('   ℹ️  Skipping health checks (app launches via fixtures)');\n  console.log('✅ Electron global setup complete\\n');\n});\n"
  },
  {
    "path": "tests/e2e-playwright/setup/electron.teardown.ts",
    "content": "import { test as teardown } from '@playwright/test';\n\n/**\n * Electron teardown project\n *\n * Note: We skip API cleanup because the Electron app\n * (with its internal API) has already been closed by fixtures.\n * Each test is responsible for cleaning up after itself.\n */\nteardown('electron teardown', async () => {\n  console.log('\\n🧹 Running Electron global teardown...');\n  console.log('   ℹ️  Skipping API cleanup (app already closed)');\n  console.log('✅ Electron global teardown complete\\n');\n});\n"
  },
  {
    "path": "tests/e2e-playwright/test-data/browser/index.ts",
    "content": "import { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\nimport {\n  StringKeyData,\n  HashKeyData,\n  ListKeyData,\n  SetKeyData,\n  ZSetKeyData,\n  StreamKeyData,\n  JsonKeyData,\n} from 'e2eSrc/types';\n\n/**\n * Test key name prefix - used by cleanup to identify test keys\n * IMPORTANT: All test key names MUST start with this prefix\n */\nexport const TEST_KEY_PREFIX = 'test-';\n\n/**\n * String key data factory\n */\nexport const StringKeyFactory = Factory.define<StringKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}string-${faker.string.alphanumeric(8)}`,\n  value: faker.lorem.sentence(),\n}));\n\n/**\n * Hash key data factory\n */\nexport const HashKeyFactory = Factory.define<HashKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}hash-${faker.string.alphanumeric(8)}`,\n  fields: [\n    { field: faker.word.noun(), value: faker.lorem.word() },\n    { field: faker.word.noun(), value: faker.lorem.word() },\n  ],\n}));\n\n/**\n * List key data factory\n */\nexport const ListKeyFactory = Factory.define<ListKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}list-${faker.string.alphanumeric(8)}`,\n  elements: [faker.lorem.word(), faker.lorem.word(), faker.lorem.word()],\n}));\n\n/**\n * Set key data factory\n */\nexport const SetKeyFactory = Factory.define<SetKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}set-${faker.string.alphanumeric(8)}`,\n  members: [faker.lorem.word(), faker.lorem.word(), faker.lorem.word()],\n}));\n\n/**\n * Sorted Set (ZSet) key data factory\n */\nexport const ZSetKeyFactory = Factory.define<ZSetKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}zset-${faker.string.alphanumeric(8)}`,\n  members: [\n    { member: faker.lorem.word(), score: '1' },\n    { member: faker.lorem.word(), score: '2' },\n    { member: faker.lorem.word(), score: '3' },\n  ],\n}));\n\n/**\n * Stream key data factory\n */\nexport const StreamKeyFactory = Factory.define<StreamKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}stream-${faker.string.alphanumeric(8)}`,\n  entryId: '*',\n  fields: [{ field: faker.word.noun(), value: faker.lorem.word() }],\n}));\n\n/**\n * JSON key data factory\n */\nexport const JsonKeyFactory = Factory.define<JsonKeyData>(() => ({\n  keyName: `${TEST_KEY_PREFIX}json-${faker.string.alphanumeric(8)}`,\n  value: JSON.stringify({\n    name: faker.person.fullName(),\n    email: faker.internet.email(),\n    age: faker.number.int({ min: 18, max: 80 }),\n  }),\n}));\n\n/**\n * Key factories by type\n */\nexport const keyFactories = {\n  String: StringKeyFactory,\n  Hash: HashKeyFactory,\n  List: ListKeyFactory,\n  Set: SetKeyFactory,\n  'Sorted Set': ZSetKeyFactory,\n  Stream: StreamKeyFactory,\n  JSON: JsonKeyFactory,\n};\n"
  },
  {
    "path": "tests/e2e-playwright/test-data/databases/import-fixtures/generate.ts",
    "content": "import * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { redisConfig } from 'e2eSrc/config';\n\nconst { host, port } = redisConfig.standalone;\n\ninterface ImportFixture {\n  host: string;\n  port: number;\n  name: string;\n  connectionType?: string;\n  tags?: Array<{ key: string; value: string }>;\n}\n\nfunction writeTempFixture(filename: string, data: unknown): string {\n  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ri-import-'));\n  const filePath = path.join(dir, filename);\n  fs.writeFileSync(filePath, JSON.stringify(data, null, 2));\n  return filePath;\n}\n\nexport function generateValidSingle(name = 'test-import-single'): string {\n  const data: ImportFixture[] = [{ host, port, name, connectionType: 'STANDALONE' }];\n  return writeTempFixture('valid-single.json', data);\n}\n\nexport function generateValidMultiple(names = ['test-import-multi-1', 'test-import-multi-2']): string {\n  const data: ImportFixture[] = names.map((n) => ({\n    host,\n    port,\n    name: n,\n    connectionType: 'STANDALONE',\n  }));\n  return writeTempFixture('valid-multiple.json', data);\n}\n\nexport function generatePartialValid(validName = 'test-import-partial-ok'): string {\n  const data = [\n    { host, port, name: validName, connectionType: 'STANDALONE' },\n    { host: '', port: 1, name: 'test-import-partial-fail', connectionType: 'STANDALONE' },\n  ];\n  return writeTempFixture('partial-valid.json', data);\n}\n\nexport function generateWithTags(name = 'test-import-tagged'): string {\n  const data: ImportFixture[] = [\n    {\n      host,\n      port,\n      name,\n      connectionType: 'STANDALONE',\n      tags: [\n        { key: 'env', value: 'test' },\n        { key: 'team', value: 'qa' },\n      ],\n    },\n  ];\n  return writeTempFixture('with-tags.json', data);\n}\n"
  },
  {
    "path": "tests/e2e-playwright/test-data/databases/import-fixtures/invalid-format.txt",
    "content": "this is not valid JSON\nit should cause an import error\n"
  },
  {
    "path": "tests/e2e-playwright/test-data/databases/index.ts",
    "content": "import { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\nimport { redisConfig } from 'e2eSrc/config';\nimport { AddDatabaseConfig, ConnectionType } from 'e2eSrc/types';\n\n/**\n * Test database name prefix - used by cleanup to identify test databases\n * IMPORTANT: All test database names MUST start with this prefix\n */\nexport const TEST_DB_PREFIX = 'test-';\n\n/**\n * Standalone database configuration factory\n */\nexport const StandaloneConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.standalone.host,\n  port: redisConfig.standalone.port,\n  name: `${TEST_DB_PREFIX}standalone-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Standalone V5 database configuration factory (no Search module)\n */\nexport const StandaloneV5ConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.standaloneV5.host,\n  port: redisConfig.standaloneV5.port,\n  name: `${TEST_DB_PREFIX}standalone-v5-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Standalone V7 database configuration factory\n */\nexport const StandaloneV7ConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.standaloneV7.host,\n  port: redisConfig.standaloneV7.port,\n  name: `${TEST_DB_PREFIX}standalone-v7-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Standalone V8 database configuration factory\n */\nexport const StandaloneV8ConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.standaloneV8.host,\n  port: redisConfig.standaloneV8.port,\n  name: `${TEST_DB_PREFIX}standalone-v8-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Cluster database configuration factory\n */\nexport const ClusterConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.cluster.host,\n  port: redisConfig.cluster.port,\n  name: `${TEST_DB_PREFIX}cluster-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Sentinel database configuration factory\n */\nexport const SentinelConfigFactory = Factory.define<AddDatabaseConfig & { masterName: string }>(() => ({\n  host: redisConfig.sentinel.host,\n  port: redisConfig.sentinel.port,\n  password: redisConfig.sentinel.password,\n  name: `${TEST_DB_PREFIX}sentinel-${faker.string.alphanumeric(8)}`,\n  masterName: redisConfig.sentinel.masterName,\n}));\n\n/**\n * Standalone Empty database configuration factory\n * Dedicated server (port 8105) for tests that need an isolated, empty Redis instance\n */\nexport const StandaloneEmptyConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.standaloneEmpty.host,\n  port: redisConfig.standaloneEmpty.port,\n  name: `${TEST_DB_PREFIX}standalone-empty-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Standalone Big database configuration factory\n * Uses a pre-seeded large database (port 8103) for tests requiring many keys\n */\nexport const StandaloneBigConfigFactory = Factory.define<AddDatabaseConfig>(() => ({\n  host: redisConfig.standaloneBig.host,\n  port: redisConfig.standaloneBig.port,\n  name: `${TEST_DB_PREFIX}standalone-big-${faker.string.alphanumeric(8)}`,\n}));\n\n/**\n * Database factories by connection type\n */\nexport const databaseFactories = {\n  [ConnectionType.Standalone]: StandaloneConfigFactory,\n  [ConnectionType.Cluster]: ClusterConfigFactory,\n  [ConnectionType.Sentinel]: SentinelConfigFactory,\n  [ConnectionType.StandaloneBig]: StandaloneBigConfigFactory,\n};\n"
  },
  {
    "path": "tests/e2e-playwright/test-data/vector-search/index.ts",
    "content": "import { Factory } from 'fishery';\nimport { faker } from '@faker-js/faker';\nimport { IndexSchemaField, IndexConfig, IndexHashKeyData, IndexJsonKeyData } from 'e2eSrc/types';\n\n/**\n * Test prefix for vector search resources\n */\nexport const TEST_VS_PREFIX = 'test-vs-';\n\n/**\n * Index schema field factory\n *\n * Generates a single field definition for an FT index schema.\n * Default type is 'text'; override for numeric, tag, vector, etc.\n */\nexport const IndexSchemaFieldFactory = Factory.define<IndexSchemaField>(() => ({\n  name: faker.string.alpha(10),\n  type: 'text',\n}));\n\n/**\n * Index configuration factory\n *\n * Generates a full FT.CREATE-compatible config with a unique name,\n * prefix, and a default 2-field text schema.\n */\nexport const IndexConfigFactory = Factory.define<IndexConfig>(() => {\n  const id = faker.string.alphanumeric(8);\n  return {\n    indexName: `${TEST_VS_PREFIX}idx-${id}`,\n    prefix: `${TEST_VS_PREFIX}${id}:`,\n    schema: [\n      { name: 'name', type: 'text' },\n      { name: 'description', type: 'text' },\n    ],\n    keyType: 'hash',\n  };\n});\n\n/**\n * Hash key factory for vector search tests\n *\n * Generates a hash key with fields commonly used in search index testing:\n * name, description, price, category.\n *\n * Pass `keyName` override to set a specific prefix:\n *   IndexHashKeyFactory.build({ keyName: `${prefix}key1` })\n */\nexport const IndexHashKeyFactory = Factory.define<IndexHashKeyData>(() => ({\n  keyName: `${TEST_VS_PREFIX}${faker.string.alphanumeric(8)}:key-${faker.string.alphanumeric(4)}`,\n  fields: [\n    { field: 'name', value: faker.commerce.productName() },\n    { field: 'description', value: faker.commerce.productDescription() },\n    { field: 'price', value: `${faker.number.int({ min: 10, max: 1000 })}` },\n    { field: 'category', value: faker.commerce.department().toLowerCase() },\n  ],\n}));\n\n/**\n * JSON key factory for vector search tests\n *\n * Generates a JSON key with fields commonly used in search index testing:\n * name, description, price, category.\n *\n * Pass `keyName` override to set a specific prefix:\n *   IndexJsonKeyFactory.build({ keyName: `${prefix}key1` })\n */\nexport const IndexJsonKeyFactory = Factory.define<IndexJsonKeyData>(() => ({\n  keyName: `${TEST_VS_PREFIX}${faker.string.alphanumeric(8)}:key-${faker.string.alphanumeric(4)}`,\n  value: {\n    name: faker.commerce.productName(),\n    description: faker.commerce.productDescription(),\n    price: faker.number.int({ min: 10, max: 1000 }),\n    category: faker.commerce.department().toLowerCase(),\n  },\n}));\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/analytics/database-analysis/database-analysis.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { databaseFactories } from 'e2eSrc/test-data/databases';\nimport { TEST_KEY_PREFIX } from 'e2eSrc/test-data/browser';\nimport { ConnectionType, DatabaseInstance } from 'e2eSrc/types';\n\n/**\n * Analytics > Database Analysis Tests\n *\n * Tests for database analysis feature including:\n * - Generating analysis reports\n * - Viewing data summary (memory/keys charts, TTL distribution)\n * - Top namespaces and top keys tables\n * - Report history\n * - Recommendations (Tips tab)\n * - Navigation between analytics sub-pages\n */\ntest.describe('Analytics > Database Analysis', () => {\n  let database: DatabaseInstance;\n  // Use unique suffix per run to avoid key conflicts from previous runs\n  const uniqueSuffix = Date.now().toString(36);\n  const keyPrefix = `${TEST_KEY_PREFIX}analysis-${uniqueSuffix}`;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    // Create a test database with unique name\n    const config = databaseFactories[ConnectionType.Standalone].build({ name: `test-db-analysis-${uniqueSuffix}` });\n    database = await apiHelper.createDatabase(config);\n\n    // Seed keys with different types and namespace patterns for meaningful analysis\n    // \"user:\" namespace - String keys\n    await apiHelper.createStringKey(database.id, `${keyPrefix}:user:1`, 'John Doe');\n    await apiHelper.createStringKey(database.id, `${keyPrefix}:user:2`, 'Jane Smith');\n    await apiHelper.createStringKey(database.id, `${keyPrefix}:user:3`, 'Bob Wilson');\n\n    // \"session:\" namespace - Hash keys\n    await apiHelper.createHashKey(database.id, `${keyPrefix}:session:1`, [\n      { field: 'token', value: 'abc123' },\n      { field: 'userId', value: '1' },\n    ]);\n    await apiHelper.createHashKey(database.id, `${keyPrefix}:session:2`, [\n      { field: 'token', value: 'def456' },\n      { field: 'userId', value: '2' },\n    ]);\n\n    // \"cache:\" namespace - various types\n    await apiHelper.createStringKey(database.id, `${keyPrefix}:cache:homepage`, 'cached-html-content');\n    await apiHelper.createListKey(database.id, `${keyPrefix}:cache:queue`, ['item1', 'item2', 'item3']);\n\n    // \"settings:\" namespace - Set key\n    await apiHelper.createSetKey(database.id, `${keyPrefix}:settings:tags`, ['redis', 'database', 'cache']);\n\n    // Additional keys for variety\n    await apiHelper.createZSetKey(database.id, `${keyPrefix}:leaderboard`, [\n      { member: 'player1', score: '100' },\n      { member: 'player2', score: '200' },\n      { member: 'player3', score: '300' },\n    ]);\n\n    await apiHelper.createStreamKey(database.id, `${keyPrefix}:events`, [\n      { field: 'type', value: 'click' },\n      { field: 'page', value: 'home' },\n    ]);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (database?.id) {\n      await apiHelper.deleteKeysByPattern(database.id, `${keyPrefix}*`);\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.describe('View Database Analysis', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n    });\n\n    test('should display database analysis page', async ({ analyticsPage }) => {\n      await expect(analyticsPage.databaseAnalysisTab).toBeVisible();\n      await expect(analyticsPage.databaseAnalysisTab).toHaveAttribute('aria-selected', 'true');\n    });\n\n    test('should show new report button', async ({ analyticsPage }) => {\n      await expect(analyticsPage.newReportButton).toBeVisible();\n    });\n\n    test('should generate analysis report', async ({ analyticsPage }) => {\n      await analyticsPage.clickNewReport();\n      await analyticsPage.waitForReportGenerated();\n\n      // Should show scanned keys info\n      await expect(analyticsPage.scannedKeysText).toBeVisible();\n    });\n\n    test('should show data summary tab after analysis', async ({ analyticsPage }) => {\n      await analyticsPage.ensureReportGenerated();\n\n      await expect(analyticsPage.dataSummaryTab).toBeVisible();\n      await expect(analyticsPage.dataSummaryTab).toHaveAttribute('aria-selected', 'true');\n    });\n\n    test('should show tips tab', async ({ analyticsPage }) => {\n      await analyticsPage.ensureReportGenerated();\n\n      await expect(analyticsPage.tipsTab).toBeVisible();\n    });\n  });\n\n  test.describe('Data Summary Charts', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await analyticsPage.ensureReportGenerated();\n    });\n\n    test('should display memory chart', async ({ analyticsPage }) => {\n      await expect(analyticsPage.memoryChartTitle).toBeVisible();\n      await expect(analyticsPage.totalMemoryValue).toBeVisible();\n    });\n\n    test('should display keys chart', async ({ analyticsPage }) => {\n      await expect(analyticsPage.keysChartTitle).toBeVisible();\n      await expect(analyticsPage.totalKeysValue).toBeVisible();\n    });\n\n    test('should display summary per data section', async ({ analyticsPage }) => {\n      await expect(analyticsPage.summaryPerData).toBeVisible();\n    });\n  });\n\n  test.describe('Top Namespaces Table', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await analyticsPage.ensureReportGenerated();\n    });\n\n    test('should show top namespaces section', async ({ analyticsPage }) => {\n      await expect(analyticsPage.topNamespacesContainer).toBeVisible();\n    });\n\n    test('should show namespaces table by memory by default', async ({ analyticsPage }) => {\n      // The memory table should be visible by default\n      await expect(analyticsPage.nspTableMemory).toBeVisible();\n    });\n\n    test('should switch namespaces view to by Number of Keys', async ({ analyticsPage }) => {\n      await analyticsPage.switchTopNamespacesView('keys');\n\n      // Keys table should now be visible\n      await expect(analyticsPage.nspTableKeys).toBeVisible();\n    });\n\n    test('should switch namespaces view back to by Memory', async ({ analyticsPage }) => {\n      // Switch to keys first\n      await analyticsPage.switchTopNamespacesView('keys');\n      await expect(analyticsPage.nspTableKeys).toBeVisible();\n\n      // Switch back to memory\n      await analyticsPage.switchTopNamespacesView('memory');\n      await expect(analyticsPage.nspTableMemory).toBeVisible();\n    });\n  });\n\n  test.describe('Top Keys Table', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await analyticsPage.ensureReportGenerated();\n    });\n\n    test('should show top keys section', async ({ analyticsPage }) => {\n      await expect(analyticsPage.topKeysTitle).toBeVisible();\n    });\n\n    test('should show top keys table by memory by default', async ({ analyticsPage }) => {\n      await expect(analyticsPage.topKeysTableMemory).toBeVisible();\n    });\n\n    test('should switch top keys view to by Length', async ({ analyticsPage }) => {\n      await analyticsPage.switchTopKeysView('length');\n\n      await expect(analyticsPage.topKeysTableLength).toBeVisible();\n    });\n\n    test('should switch top keys view back to by Memory', async ({ analyticsPage }) => {\n      // Switch to length first\n      await analyticsPage.switchTopKeysView('length');\n      await expect(analyticsPage.topKeysTableLength).toBeVisible();\n\n      // Switch back to memory\n      await analyticsPage.switchTopKeysView('memory');\n      await expect(analyticsPage.topKeysTableMemory).toBeVisible();\n    });\n  });\n\n  test.describe('TTL Distribution', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await analyticsPage.ensureReportGenerated();\n    });\n\n    test('should show TTL distribution chart', async ({ analyticsPage }) => {\n      await expect(analyticsPage.ttlDistributionChart).toBeVisible();\n    });\n\n    test('should toggle show no expiry in TTL chart', async ({ analyticsPage }) => {\n      await expect(analyticsPage.showNoExpirySwitch).toBeVisible();\n\n      // Toggle should be clickable without error\n      await analyticsPage.toggleShowNoExpiry();\n    });\n  });\n\n  test.describe('Report History', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await analyticsPage.ensureReportGenerated();\n    });\n\n    test('should show report history dropdown', async ({ analyticsPage }) => {\n      await expect(analyticsPage.reportHistorySelect).toBeVisible();\n    });\n\n    test('should have at least one report in history', async ({ analyticsPage }) => {\n      const reportCount = await analyticsPage.getReportHistoryCount();\n      expect(reportCount).toBeGreaterThan(0);\n    });\n  });\n\n  test.describe('Navigation', () => {\n    test('should navigate from Slow Log to Database Analysis', async ({ analyticsPage }) => {\n      // Start at Slow Log\n      await analyticsPage.gotoSlowLog(database.id);\n      await expect(analyticsPage.slowLogTab).toHaveAttribute('aria-selected', 'true');\n\n      // Click Database Analysis tab\n      await analyticsPage.clickDatabaseAnalysisTab();\n\n      // Should now be on Database Analysis\n      await expect(analyticsPage.databaseAnalysisTab).toHaveAttribute('aria-selected', 'true');\n    });\n\n    test('should navigate from Database Analysis to Slow Log', async ({ analyticsPage }) => {\n      // Start at Database Analysis\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await expect(analyticsPage.databaseAnalysisTab).toHaveAttribute('aria-selected', 'true');\n\n      // Click Slow Log tab\n      await analyticsPage.clickSlowLogTab();\n\n      // Should now be on Slow Log\n      await expect(analyticsPage.slowLogTab).toHaveAttribute('aria-selected', 'true');\n    });\n  });\n\n  test.describe('Recommendations (Tips Tab)', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(database.id);\n      await analyticsPage.ensureReportGenerated();\n    });\n\n    test('should switch to tips tab', async ({ analyticsPage }) => {\n      await analyticsPage.clickTipsTab();\n\n      await expect(analyticsPage.tipsTab).toHaveAttribute('aria-selected', 'true');\n    });\n\n    test('should display tips count in tab label', async ({ analyticsPage }) => {\n      const tipsCount = await analyticsPage.getTipsCount();\n\n      // Tips count should be a number (could be 0 or more)\n      expect(tipsCount).toBeGreaterThanOrEqual(0);\n    });\n\n    test('should show recommendations or empty message', async ({ analyticsPage }) => {\n      await analyticsPage.clickTipsTab();\n\n      const recommendationCount = await analyticsPage.getRecommendationCount();\n\n      if (recommendationCount === 0) {\n        // If no recommendations, empty message should be visible\n        await expect(analyticsPage.emptyRecommendationsMessage).toBeVisible();\n      } else {\n        // If recommendations exist, first accordion should be visible\n        await expect(analyticsPage.recommendationAccordions.first()).toBeVisible();\n      }\n    });\n\n    test('should expand and collapse recommendation', async ({ analyticsPage }) => {\n      await analyticsPage.clickTipsTab();\n\n      const count = await analyticsPage.getRecommendationCount();\n      if (count === 0) {\n        // Skip if no recommendations available\n        return;\n      }\n\n      // First recommendation should be expanded by default\n      const isExpanded = await analyticsPage.isRecommendationExpanded(0);\n      expect(isExpanded).toBe(true);\n\n      // Collapse the recommendation\n      await analyticsPage.toggleRecommendation(0);\n      const isCollapsed = await analyticsPage.isRecommendationExpanded(0);\n      expect(isCollapsed).toBe(false);\n\n      // Expand again\n      await analyticsPage.toggleRecommendation(0);\n      const isExpandedAgain = await analyticsPage.isRecommendationExpanded(0);\n      expect(isExpandedAgain).toBe(true);\n    });\n\n    test('should show voting section for recommendations', async ({ analyticsPage }) => {\n      await analyticsPage.clickTipsTab();\n\n      const count = await analyticsPage.getRecommendationCount();\n      if (count === 0) {\n        return;\n      }\n\n      const hasVoting = await analyticsPage.isVotingSectionVisible();\n      expect(hasVoting).toBe(true);\n    });\n\n    test('should show tutorial button for applicable recommendations', async ({ analyticsPage }) => {\n      await analyticsPage.clickTipsTab();\n\n      const count = await analyticsPage.getRecommendationCount();\n      if (count === 0) {\n        return;\n      }\n\n      // Not all recommendations have tutorials, so we just verify the check works\n      const hasTutorial = await analyticsPage.hasTutorialButton();\n      expect(typeof hasTutorial).toBe('boolean');\n    });\n\n    test('should show badges legend when recommendations exist', async ({ analyticsPage }) => {\n      await analyticsPage.clickTipsTab();\n\n      const count = await analyticsPage.getRecommendationCount();\n      if (count === 0) {\n        return;\n      }\n\n      const hasBadges = await analyticsPage.isBadgesLegendVisible();\n      expect(hasBadges).toBe(true);\n    });\n  });\n});\n\n/**\n * Analytics > Database Analysis - Large Dataset Tests\n *\n * Uses the pre-seeded big database (port 8103) for tests that require\n * a large number of keys (extrapolation, scanned vs estimated, sorting, etc.)\n */\ntest.describe('Analytics > Database Analysis (Large Dataset)', () => {\n  let bigDatabase: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const config = databaseFactories[ConnectionType.StandaloneBig].build();\n    bigDatabase = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (bigDatabase?.id) {\n      await apiHelper.deleteDatabase(bigDatabase.id);\n    }\n  });\n\n  test.describe('Extrapolation', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(bigDatabase.id);\n      await analyticsPage.ensureReportGenerated(120000);\n    });\n\n    test('should show extrapolate results switch on large dataset', async ({ analyticsPage }) => {\n      await expect(analyticsPage.extrapolateSwitch).toBeVisible();\n    });\n\n    test('should toggle extrapolation switch', async ({ analyticsPage }) => {\n      await expect(analyticsPage.extrapolateSwitch).toBeVisible();\n      await analyticsPage.extrapolateSwitch.click();\n    });\n\n    test('should distinguish between scanned and estimated data', async ({ analyticsPage }) => {\n      // The progress element shows \"Scanned X%\" with processed/total key counts\n      await expect(analyticsPage.analysisProgress).toBeVisible();\n      const text = await analyticsPage.analysisProgress.textContent();\n\n      // On a large dataset, the scan should be partial (< 100%)\n      // The format is \"Scanned X% (processed / total keys)\"\n      expect(text).toMatch(/Scanned/);\n      expect(text).toMatch(/\\d+%/);\n    });\n  });\n\n  test.describe('Responsiveness', () => {\n    test('should complete analysis on large dataset within timeout', async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(bigDatabase.id);\n\n      // Generate a new report and verify it completes (allow up to 120s for large dataset)\n      await analyticsPage.clickNewReport();\n      await analyticsPage.waitForReportGenerated(120000);\n\n      // Verify main UI sections are rendered\n      await expect(analyticsPage.analysisProgress).toBeVisible();\n      await expect(analyticsPage.databaseAnalysisPage).toBeVisible();\n    });\n  });\n\n  test.describe('Namespace Sorting', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(bigDatabase.id);\n      await analyticsPage.ensureReportGenerated(120000);\n    });\n\n    test('should sort namespaces by key pattern', async ({ analyticsPage }) => {\n      // The namespace table header has sortable button elements with aria-description\n      const keyPatternHeader = analyticsPage.nspTableMemory\n        .getByRole('columnheader')\n        .filter({ hasText: 'Key Pattern' })\n        .getByRole('button');\n\n      await expect(keyPatternHeader).toBeVisible();\n\n      // Default sort is by Memory desc, so Key Pattern shows \"activate to sort ascending\"\n      await expect(keyPatternHeader).toHaveAttribute('aria-description', /activate to sort ascending/);\n\n      // Click to sort ascending — description now offers \"sort descending\"\n      await keyPatternHeader.click();\n      await expect(keyPatternHeader).toHaveAttribute('aria-description', /activate to sort descending/);\n\n      // Click again to sort descending — description now offers \"unsort\"\n      await keyPatternHeader.click();\n      await expect(keyPatternHeader).toHaveAttribute('aria-description', /activate to unsort/);\n    });\n  });\n\n  test.describe('Namespace Navigation', () => {\n    test.beforeEach(async ({ analyticsPage }) => {\n      await analyticsPage.gotoDatabaseAnalysis(bigDatabase.id);\n      await analyticsPage.ensureReportGenerated(120000);\n    });\n\n    test('should filter namespace to Browser view', async ({ browserPage, analyticsPage, page }) => {\n      // Find the first namespace cell in the Key Pattern column (first data row)\n      const firstRow = analyticsPage.nspTableMemory.getByRole('row').nth(1);\n\n      // The Key Pattern cell contains a button (TableTextBtn) with the namespace name\n      const namespaceButton = firstRow.getByRole('cell').first().getByRole('button');\n      await expect(namespaceButton).toBeVisible();\n\n      // Get the namespace pattern text before clicking\n      const namespaceText = await namespaceButton.textContent();\n      expect(namespaceText).toBeTruthy();\n\n      // Click on the namespace to navigate to Browser\n      await namespaceButton.click();\n\n      // Wait for navigation to Browser page\n      await page.waitForURL(/\\/browser/);\n\n      // Verify the search input is populated with the namespace pattern\n      await expect(browserPage.keyList.searchInput).toHaveValue(namespaceText!);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/browser/add-key/add-key.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport {\n  StringKeyFactory,\n  HashKeyFactory,\n  ListKeyFactory,\n  SetKeyFactory,\n  ZSetKeyFactory,\n  StreamKeyFactory,\n  JsonKeyFactory,\n  TEST_KEY_PREFIX,\n} from 'e2eSrc/test-data/browser';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\n/**\n * Browser > Add Key Tests\n *\n * Tests for adding different key types via the Add Key dialog\n */\ntest.describe('Browser > Add Key', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    // Create a test database for all tests in this file\n    const config = StandaloneConfigFactory.build({ name: 'test-add-key-db' });\n    database = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    // Clean up the test database\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.beforeEach(async ({ browserPage }) => {\n    await browserPage.goto(database.id);\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    // Clean up test keys created during the test\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_KEY_PREFIX}*`);\n  });\n\n  test(`should add a String key`, async ({ browserPage }) => {\n    const keyData = StringKeyFactory.build();\n\n    // Open Add Key dialog\n    await browserPage.openAddKeyDialog();\n\n    // Select String type\n    await browserPage.addKeyDialog.selectKeyType('String');\n\n    // Fill key name and value\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillStringValue(keyData.value);\n\n    // Add the key\n    await browserPage.addKeyDialog.clickAddKey();\n\n    // Verify key appears in the list\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should add a Hash key`, async ({ browserPage }) => {\n    const keyData = HashKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('Hash');\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillHashField(keyData.fields[0].field, keyData.fields[0].value);\n    await browserPage.addKeyDialog.clickAddKey();\n\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should add a List key`, async ({ browserPage }) => {\n    const keyData = ListKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('List');\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillListElement(keyData.elements[0]);\n    await browserPage.addKeyDialog.clickAddKey();\n\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should add a Set key`, async ({ browserPage }) => {\n    const keyData = SetKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('Set');\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillSetMember(keyData.members[0]);\n    await browserPage.addKeyDialog.clickAddKey();\n\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should add a Sorted Set key`, async ({ browserPage }) => {\n    const keyData = ZSetKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('Sorted Set');\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillZSetMember(keyData.members[0].member, keyData.members[0].score);\n    await browserPage.addKeyDialog.clickAddKey();\n\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should add a Stream key`, async ({ browserPage }) => {\n    const keyData = StreamKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('Stream');\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillStreamField(keyData.fields[0].field, keyData.fields[0].value);\n    await browserPage.addKeyDialog.clickAddKey();\n\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should add a JSON key`, async ({ browserPage }) => {\n    const keyData = JsonKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('JSON');\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillJsonValue(keyData.value as string);\n    await browserPage.addKeyDialog.clickAddKey();\n\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n  });\n\n  test(`should show Add Key button disabled when key name is empty`, async ({ browserPage }) => {\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.selectKeyType('String');\n    await browserPage.addKeyDialog.fillStringValue('some value');\n\n    // Key name is empty, button should be disabled\n    await browserPage.addKeyDialog.expectAddKeyDisabled();\n  });\n\n  test(`should cancel adding a key`, async ({ browserPage }) => {\n    const keyData = StringKeyFactory.build();\n\n    await browserPage.openAddKeyDialog();\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.clickCancel();\n\n    // Dialog should be closed\n    const isVisible = await browserPage.addKeyDialog.isVisible();\n    expect(isVisible).toBe(false);\n  });\n\n  test(`should add a key with TTL`, async ({ browserPage }) => {\n    const keyData = StringKeyFactory.build();\n    const ttlSeconds = '60';\n\n    // Open Add Key dialog\n    await browserPage.openAddKeyDialog();\n\n    // Select String type\n    await browserPage.addKeyDialog.selectKeyType('String');\n\n    // Fill key name, value, and TTL\n    await browserPage.addKeyDialog.fillKeyName(keyData.keyName);\n    await browserPage.addKeyDialog.fillStringValue(keyData.value);\n    await browserPage.addKeyDialog.fillTtl(ttlSeconds);\n\n    // Add the key\n    await browserPage.addKeyDialog.clickAddKey();\n\n    // Verify key appears in the list\n    await browserPage.keyList.searchKeys(keyData.keyName);\n    await browserPage.expectKeyInList(keyData.keyName);\n\n    // Click on the key to view details and verify TTL is set\n    await browserPage.keyList.clickKey(keyData.keyName);\n    await expect(browserPage.keyDetails.ttlValue).toBeVisible();\n    // TTL should be less than or equal to 60 seconds (it may have decreased)\n    const ttlText = await browserPage.keyDetails.ttlValue.textContent();\n    expect(ttlText).not.toBe('No limit');\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/browser/key-filtering/key-filtering.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { TEST_KEY_PREFIX } from 'e2eSrc/test-data/browser';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\n/**\n * Key Filtering Patterns Tests\n *\n * Tests the ability to filter keys using various patterns including\n * wildcards, character classes, and escaping special characters.\n */\ntest.describe('Browser > Key Filtering Patterns', () => {\n  let database: DatabaseInstance;\n  // Use a unique suffix per test run to avoid conflicts\n  const uniqueSuffix = `kfp-${Date.now().toString(36)}`;\n\n  // Define test keys with specific naming for pattern testing\n  const testKeys = {\n    prefix1: `${TEST_KEY_PREFIX}filter-a-${uniqueSuffix}`,\n    prefix2: `${TEST_KEY_PREFIX}filter-b-${uniqueSuffix}`,\n    prefix3: `${TEST_KEY_PREFIX}filter-c-${uniqueSuffix}`,\n    numbered1: `${TEST_KEY_PREFIX}item1-${uniqueSuffix}`,\n    numbered2: `${TEST_KEY_PREFIX}item2-${uniqueSuffix}`,\n    numbered3: `${TEST_KEY_PREFIX}item3-${uniqueSuffix}`,\n  };\n\n  test.beforeAll(async ({ apiHelper }) => {\n    // Create a test database with unique name for this test run\n    const dbName = `test-key-filtering-${Date.now().toString(36)}`;\n    const config = StandaloneConfigFactory.build({ name: dbName });\n    database = await apiHelper.createDatabase(config);\n\n    // Create test keys via API\n    for (const [, keyName] of Object.entries(testKeys)) {\n      await apiHelper.createStringKey(database.id, keyName, 'test-value');\n    }\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    // Clean up the test database\n    if (database?.id) {\n      await apiHelper.deleteKeysByPattern(database.id, `${TEST_KEY_PREFIX}*`);\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.beforeEach(async ({ browserPage }) => {\n    await browserPage.goto(database.id);\n  });\n\n  test.describe('Wildcard Patterns', () => {\n    test(`should filter keys with asterisk (*) wildcard`, async ({ browserPage }) => {\n      // Search for keys matching the pattern with asterisk and unique suffix\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}filter-*-${uniqueSuffix}`);\n\n      // Verify that filter keys matching the pattern are shown\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix1)).toBeVisible();\n\n      // Verify numbered keys are not shown (different pattern)\n      await expect(browserPage.keyList.getKeyRow(testKeys.numbered1)).not.toBeVisible();\n    });\n\n    test(`should filter keys with question mark (?) single character wildcard`, async ({ browserPage }) => {\n      // Search for keys matching the pattern with ? wildcard (matches single char)\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}item?-${uniqueSuffix}`);\n\n      // Verify that keys with single character match are shown\n      await expect(browserPage.keyList.getKeyRow(testKeys.numbered1)).toBeVisible();\n      await expect(browserPage.keyList.getKeyRow(testKeys.numbered2)).toBeVisible();\n      await expect(browserPage.keyList.getKeyRow(testKeys.numbered3)).toBeVisible();\n    });\n\n    test(`should filter keys with [xy] character class`, async ({ browserPage }) => {\n      // Search for keys with character class [ab] (matches a or b)\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}filter-[ab]-${uniqueSuffix}`);\n\n      // Verify that keys matching a or b are shown\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix1)).toBeVisible();\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix2)).toBeVisible();\n\n      // Verify key with c is not shown\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix3)).not.toBeVisible();\n    });\n\n    test(`should filter keys with [a-z] character range`, async ({ browserPage }) => {\n      // Search for keys with character range [a-c]\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}filter-[a-c]-${uniqueSuffix}`);\n\n      // Verify that all filter keys are shown (a, b, c are all in range)\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix1)).toBeVisible();\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix2)).toBeVisible();\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix3)).toBeVisible();\n    });\n\n    test(`should filter keys with [^x] negated character class`, async ({ browserPage }) => {\n      // Search for keys with negated character class [^a] (matches anything except 'a')\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}filter-[^a]-${uniqueSuffix}`);\n\n      // Verify that keys NOT matching 'a' are shown (b and c)\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix2)).toBeVisible();\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix3)).toBeVisible();\n\n      // Verify key with 'a' is NOT shown\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix1)).not.toBeVisible();\n    });\n  });\n\n  test.describe('Special Characters', () => {\n    test(`should escape special characters in filter pattern`, async ({ browserPage, cliPanel }) => {\n      // Create a key with special characters (asterisk in the name)\n      const specialKeyName = `${TEST_KEY_PREFIX}special*key-${uniqueSuffix}`;\n      await cliPanel.open();\n      await cliPanel.executeCommand(`SET \"${specialKeyName}\" \"test-value\"`);\n      await cliPanel.hide();\n\n      // Refresh the key list\n      await browserPage.keyList.refresh();\n\n      // Search for the key with escaped asterisk (using backslash)\n      // In Redis KEYS pattern, \\* matches a literal asterisk\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}special\\\\*key-${uniqueSuffix}`);\n\n      // Verify the key with literal asterisk is found\n      await expect(browserPage.keyList.getKeyRow(specialKeyName)).toBeVisible();\n\n      // Clean up the special key\n      await cliPanel.open();\n      await cliPanel.executeCommand(`DEL \"${specialKeyName}\"`);\n    });\n  });\n\n  test.describe('Filter Controls', () => {\n    test(`should clear filter and search again`, async ({ browserPage }) => {\n      // First apply a filter for filter-* keys with unique suffix\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}filter-*-${uniqueSuffix}`);\n\n      // Verify filter is applied - filter keys should be visible\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix1)).toBeVisible();\n\n      // Clear the search\n      await browserPage.keyList.clearSearch();\n\n      // Now search for item keys only\n      await browserPage.keyList.searchKeys(`${TEST_KEY_PREFIX}item*-${uniqueSuffix}`);\n\n      // Verify numbered keys are visible after new search\n      await expect(browserPage.keyList.getKeyRow(testKeys.numbered1)).toBeVisible();\n\n      // Verify filter keys are NOT visible (different pattern)\n      await expect(browserPage.keyList.getKeyRow(testKeys.prefix1)).not.toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/cli/cli-panel.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\nimport { faker } from '@faker-js/faker';\n\ntest.describe('CLI > CLI Panel', () => {\n  let database: DatabaseInstance;\n  const uniqueSuffix = `cli-${Date.now().toString(36)}`;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const config = StandaloneConfigFactory.build({\n      name: `test-cli-panel-${uniqueSuffix}`,\n    });\n    database = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.beforeEach(async ({ browserPage }) => {\n    await browserPage.goto(database.id);\n  });\n\n  test.describe('Panel Lifecycle', () => {\n    test('should open CLI panel', async ({ cliPanel }) => {\n      await cliPanel.open();\n\n      await expect(cliPanel.container).toBeVisible();\n\n      // Using toBeAttached instead of toBeVisible because the element is empty with dimensions 0x0,\n      // and is not detected as visible by the browser.\n      await expect(cliPanel.commandInput).toBeAttached();\n      await expect(cliPanel.hideButton).toBeVisible();\n      await expect(cliPanel.closeButton).toBeVisible();\n    });\n\n    test('should close CLI panel', async ({ cliPanel }) => {\n      await cliPanel.open();\n      await expect(cliPanel.container).toBeVisible();\n\n      await cliPanel.close();\n\n      await expect(cliPanel.hideButton).not.toBeVisible();\n      await expect(cliPanel.closeButton).not.toBeVisible();\n    });\n\n    test('should hide CLI panel and restore it', async ({ cliPanel }) => {\n      await cliPanel.open();\n      await expect(cliPanel.hideButton).toBeVisible();\n\n      await cliPanel.hide();\n      await expect(cliPanel.expandButton).toBeVisible();\n      await expect(cliPanel.hideButton).not.toBeVisible();\n\n      await cliPanel.open();\n      await expect(cliPanel.container).toBeVisible();\n      await expect(cliPanel.hideButton).toBeVisible();\n      await expect(cliPanel.commandInput).toBeAttached();\n    });\n  });\n\n  test.describe('Command Execution', () => {\n    test('should execute command and display output', async ({ cliPanel }) => {\n      await cliPanel.open();\n      await cliPanel.waitForOutput('Connected');\n\n      await cliPanel.executeCommand('PING');\n\n      await expect(cliPanel.successOutput.last()).toContainText('PONG');\n    });\n\n    test('should view command output with correct values', async ({ cliPanel }) => {\n      const keyName = `test-cli-get-${uniqueSuffix}`;\n      const value = faker.lorem.word();\n\n      await cliPanel.open();\n      await cliPanel.waitForOutput('Connected');\n\n      await cliPanel.executeCommandAndWait(`SET ${keyName} ${value}`);\n      await expect(cliPanel.successOutput.last()).toContainText('OK');\n\n      await cliPanel.executeCommand(`GET ${keyName}`);\n      await expect(cliPanel.successOutput.last()).toContainText(value);\n\n      await cliPanel.executeCommand(`DEL ${keyName}`);\n    });\n\n    test('should handle command errors', async ({ cliPanel }) => {\n      await cliPanel.open();\n      await cliPanel.waitForOutput('Connected');\n\n      await cliPanel.executeCommand('INVALID_COMMAND_THAT_DOES_NOT_EXIST');\n\n      await expect(cliPanel.errorOutput.last()).toBeVisible();\n      const errorText = await cliPanel.getLastErrorResponse();\n      expect(errorText.length).toBeGreaterThan(0);\n    });\n\n    test('should execute multiple commands in sequence', async ({ cliPanel }) => {\n      const key1 = `test-cli-seq1-${uniqueSuffix}`;\n      const key2 = `test-cli-seq2-${uniqueSuffix}`;\n      const val1 = faker.lorem.word();\n      const val2 = faker.lorem.word();\n\n      await cliPanel.open();\n      await cliPanel.waitForOutput('Connected');\n\n      await cliPanel.executeCommandAndWait(`SET ${key1} ${val1}`);\n      await expect(cliPanel.successOutput.last()).toContainText('OK');\n\n      await cliPanel.executeCommandAndWait(`SET ${key2} ${val2}`);\n      await expect(cliPanel.successOutput.last()).toContainText('OK');\n\n      await cliPanel.executeCommand(`GET ${key1}`);\n      await expect(cliPanel.successOutput.last()).toContainText(val1);\n\n      const commandCount = await cliPanel.commandWrapper.count();\n      expect(commandCount).toBeGreaterThanOrEqual(3);\n\n      await cliPanel.executeCommand(`DEL ${key1} ${key2}`);\n    });\n  });\n\n  test.describe('Command History', () => {\n    test('should navigate command history with up/down arrows', async ({ cliPanel }) => {\n      const cmd1 = 'PING';\n      const cmd2 = 'DBSIZE';\n      const cmd3 = 'INFO server';\n\n      await cliPanel.open();\n      await cliPanel.waitForOutput('Connected');\n\n      await cliPanel.executeCommandAndWait(cmd1);\n      await cliPanel.executeCommandAndWait(cmd2);\n      await cliPanel.executeCommand(cmd3);\n      await cliPanel.waitForOutput('redis_version');\n\n      await cliPanel.pressArrowUp();\n      await expect(cliPanel.commandInput).toContainText(cmd3);\n\n      await cliPanel.pressArrowUp();\n      await expect(cliPanel.commandInput).toContainText(cmd2);\n\n      await cliPanel.pressArrowUp();\n      await expect(cliPanel.commandInput).toContainText(cmd1);\n\n      await cliPanel.pressArrowDown();\n      await expect(cliPanel.commandInput).toContainText(cmd2);\n    });\n  });\n\n  test.describe('Tab Completion', () => {\n    test('should complete partial command with Tab', async ({ cliPanel }) => {\n      await cliPanel.open();\n      await cliPanel.waitForOutput('Connected');\n\n      await cliPanel.typeCommand('PI');\n      await cliPanel.pressTab();\n\n      const inputText = await cliPanel.getInputText();\n      expect(inputText.toUpperCase()).toContain('PING');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/command-helper/command-helper-panel.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\ntest.describe('Command Helper > Command Helper Panel', () => {\n  let database: DatabaseInstance;\n  const uniqueSuffix = `cmd-helper-${Date.now().toString(36)}`;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const config = StandaloneConfigFactory.build({\n      name: `test-command-helper-${uniqueSuffix}`,\n    });\n    database = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.beforeEach(async ({ browserPage }) => {\n    await browserPage.goto(database.id);\n  });\n\n  test.describe('Panel Lifecycle', () => {\n    test('should open Command Helper panel', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n\n      await expect(commandHelperPanel.container).toBeVisible();\n      await expect(commandHelperPanel.searchInput).toBeVisible();\n      await expect(commandHelperPanel.hideButton).toBeVisible();\n      await expect(commandHelperPanel.closeButton).toBeVisible();\n      await expect(commandHelperPanel.defaultText).toBeVisible();\n    });\n\n    test('should hide and restore Command Helper panel', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n      await expect(commandHelperPanel.hideButton).toBeVisible();\n\n      await commandHelperPanel.hide();\n      await expect(commandHelperPanel.expandButton).toBeVisible();\n      await expect(commandHelperPanel.hideButton).not.toBeVisible();\n\n      await commandHelperPanel.open();\n      await expect(commandHelperPanel.container).toBeVisible();\n      await expect(commandHelperPanel.hideButton).toBeVisible();\n    });\n\n    test('should close Command Helper panel', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n      await expect(commandHelperPanel.container).toBeVisible();\n\n      await commandHelperPanel.close();\n\n      await expect(commandHelperPanel.hideButton).not.toBeVisible();\n      await expect(commandHelperPanel.closeButton).not.toBeVisible();\n    });\n  });\n\n  test.describe('Search', () => {\n    test('should search for a command', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n\n      await commandHelperPanel.search('GET');\n\n      // Wait for search results to appear\n      await expect(commandHelperPanel.searchResultTitles.first()).toBeVisible();\n\n      // Verify search results contain GET command\n      const resultCount = await commandHelperPanel.getSearchResultCount();\n      expect(resultCount).toBeGreaterThan(0);\n    });\n\n    test('should filter commands by category', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n\n      // First search to get some results\n      await commandHelperPanel.search('');\n\n      // Filter by a category using internal group type value (lowercase)\n      // See GROUP_TYPES_DISPLAY in redisinsight/ui/src/constants/keys.ts for mapping\n      await commandHelperPanel.filterByCategory('string');\n\n      // Verify filter is applied by checking results\n      await expect(commandHelperPanel.searchResultTitles.first()).toBeVisible();\n    });\n  });\n\n  test.describe('Command Details', () => {\n    test('should view command details', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n\n      // Search for a specific command\n      await commandHelperPanel.search('SET');\n      await expect(commandHelperPanel.searchResultTitles.first()).toBeVisible();\n\n      // Click on SET command to view details\n      await commandHelperPanel.selectCommand('SET');\n\n      // Verify command details are displayed\n      await expect(commandHelperPanel.commandTitle).toBeVisible();\n      await expect(commandHelperPanel.commandSummary).toBeVisible();\n      await expect(commandHelperPanel.readMoreLink).toBeVisible();\n      await expect(commandHelperPanel.backToListButton).toBeVisible();\n\n      // Verify the title contains SET\n      const title = await commandHelperPanel.getCommandTitle();\n      expect(title.toUpperCase()).toContain('SET');\n    });\n\n    test('should navigate back to search results from command details', async ({ commandHelperPanel }) => {\n      await commandHelperPanel.open();\n\n      // Search and select a command\n      await commandHelperPanel.search('PING');\n      await expect(commandHelperPanel.searchResultTitles.first()).toBeVisible();\n      await commandHelperPanel.selectCommand('PING');\n\n      // Verify we're in command details view\n      await expect(commandHelperPanel.commandTitle).toBeVisible();\n\n      // Go back to list\n      await commandHelperPanel.backToList();\n\n      // Verify we're back to search results\n      await expect(commandHelperPanel.searchResultTitles.first()).toBeVisible();\n      await expect(commandHelperPanel.commandTitle).not.toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/add/add-database.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory, ClusterConfigFactory } from 'e2eSrc/test-data/databases';\nimport { redisConfig } from 'e2eSrc/config/databases';\n\n/**\n * Add Database Tests (TEST_PLAN.md: 1.1 Add Database)\n *\n * Tests for adding databases via the Connection Settings form.\n * Tests use the existing Redis instances configured in docker-compose.\n */\ntest.describe('Add Database', () => {\n  // Track databases created in tests for cleanup\n  const createdDatabaseNames: string[] = [];\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  test.afterEach(async ({ databasesPage }) => {\n    // Clean up databases created during tests (via UI)\n    for (const name of createdDatabaseNames) {\n      try {\n        if (await databasesPage.databaseList.exists(name)) {\n          await databasesPage.databaseList.delete(name);\n        }\n      } catch {\n        // Ignore cleanup errors\n      }\n    }\n    createdDatabaseNames.length = 0;\n  });\n\n  test('should add standalone database', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.submit();\n\n    // Wait for success and verify database in list\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should add database with no auth', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build({\n      username: undefined,\n      password: undefined,\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n\n    // Fill only required fields without auth\n    await addDatabaseDialog.databaseAliasInput.fill(config.name);\n    await addDatabaseDialog.hostInput.fill(config.host);\n    await addDatabaseDialog.portInput.fill(config.port.toString());\n    await addDatabaseDialog.usernameInput.clear();\n\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should add database with username only', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build({\n      username: 'default',\n      password: undefined,\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.databaseAliasInput.fill(config.name);\n    await addDatabaseDialog.hostInput.fill(config.host);\n    await addDatabaseDialog.portInput.fill(config.port.toString());\n    await addDatabaseDialog.usernameInput.fill(config.username!);\n\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should add database with username and password', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build({\n      username: 'default',\n      password: 'testpassword',\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should add cluster database', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = ClusterConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.submit();\n\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should validate required fields', async ({ databasesPage }) => {\n    const { addDatabaseDialog } = databasesPage;\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n\n    // Clear required fields\n    await addDatabaseDialog.databaseAliasInput.clear();\n    await addDatabaseDialog.hostInput.clear();\n    await addDatabaseDialog.portInput.clear();\n\n    // The submit button should be disabled when required fields are empty\n    await expect(addDatabaseDialog.addRedisDatabaseButton).toBeDisabled();\n\n    // Dialog should still be visible\n    await expect(addDatabaseDialog.dialog).toBeVisible();\n  });\n\n  test('should test connection before saving', async ({ databasesPage }) => {\n    const { addDatabaseDialog } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    // Test connection\n    await addDatabaseDialog.testConnection();\n\n    // Should show success message\n    await expect(databasesPage.page.getByText(/connection is successful/i)).toBeVisible({ timeout: 10000 });\n  });\n\n  test('should cancel add database', async ({ databasesPage }) => {\n    const { addDatabaseDialog } = databasesPage;\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n\n    // Fill some data\n    await addDatabaseDialog.databaseAliasInput.fill('test-cancel-db');\n\n    // Cancel\n    await addDatabaseDialog.cancel();\n\n    // Dialog should close\n    await expect(addDatabaseDialog.dialog).not.toBeVisible();\n  });\n\n  test('should add database via Connection URL', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    const connectionUrl = `redis://${config.host}:${config.port}`;\n    createdDatabaseNames.push(`${config.host}:${config.port}`);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.connectionUrlInput.fill(connectionUrl);\n    await addDatabaseDialog.addDatabaseButton.click();\n\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(`${config.host}:${config.port}`, { searchFirst: true });\n  });\n\n  test('should open Connection settings from URL form', async ({ databasesPage }) => {\n    const { addDatabaseDialog } = databasesPage;\n\n    await databasesPage.openAddDatabaseDialog();\n\n    // Verify URL form is shown\n    await expect(addDatabaseDialog.connectionUrlInput).toBeVisible();\n\n    // Click connection settings\n    await addDatabaseDialog.openConnectionSettings();\n\n    // Verify connection settings form is shown\n    await expect(addDatabaseDialog.databaseAliasInput).toBeVisible();\n    await expect(addDatabaseDialog.hostInput).toBeVisible();\n    await expect(addDatabaseDialog.portInput).toBeVisible();\n  });\n\n  test('should configure timeout setting', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n    const customTimeout = 60;\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.setTimeout(customTimeout);\n\n    // Verify timeout value\n    await expect(addDatabaseDialog.timeoutInput).toHaveValue(customTimeout.toString());\n\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should select logical database', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n    const dbIndex = 1;\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.setLogicalDatabase(dbIndex);\n\n    // Verify database index is set\n    await expect(addDatabaseDialog.databaseIndexInput).toHaveValue(dbIndex.toString());\n\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should display logical database index in database list', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n    const dbIndex = 2;\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.setLogicalDatabase(dbIndex);\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n\n    // Search for the database\n    await databaseList.search(config.name);\n\n    // Verify database index is shown in the list (format: \"name [db2]\")\n    const row = databaseList.getRow(config.name);\n    await expect(row).toContainText(`[db${dbIndex}]`);\n  });\n\n  test('should display logical database index in database header', async ({ databasesPage, browserPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n    const dbIndex = 3;\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.setLogicalDatabase(dbIndex);\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n\n    // Connect to the database\n    await databaseList.search(config.name);\n    await databaseList.connect(config.name);\n\n    // Verify database index is shown in the header (format: \"db3\")\n    await expect(browserPage.instanceHeader.logicalDatabaseButton).toContainText(`db${dbIndex}`);\n\n    // Navigate back to databases list\n    await browserPage.gotoHome();\n  });\n\n  test('should display logical database index in edit form', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n    const dbIndex = 4;\n\n    // Create database with logical database\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.setLogicalDatabase(dbIndex);\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n\n    // Open edit dialog\n    await databaseList.search(config.name);\n    await databaseList.edit(config.name);\n\n    // Verify edit dialog opens with the correct title showing db index\n    // The edit dialog title includes the database name with db index annotation\n    const editDialog = databasesPage.page.getByRole('dialog', { name: /edit database/i });\n    await expect(editDialog).toBeVisible();\n\n    // Verify the logical database index is displayed in the edit dialog\n    // The db index is shown as \"Database Index: X\" in the connection info section\n    await expect(editDialog).toContainText(`Database Index:${dbIndex}`);\n\n    // Close the dialog\n    await databasesPage.page.getByRole('button', { name: 'Cancel' }).click();\n  });\n\n  test('should enable force standalone connection', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.setForceStandalone(true);\n\n    // Verify checkbox is checked\n    await expect(addDatabaseDialog.forceStandaloneCheckbox).toBeChecked();\n\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should enable automatic data decompression', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n    await addDatabaseDialog.enableDecompression();\n\n    // Verify checkbox is checked\n    await expect(addDatabaseDialog.enableDecompressionCheckbox).toBeChecked();\n\n    // Go back to General tab and submit\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should configure key name format', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build();\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    // Go to Decompression & Formatters tab and change key name format\n    await addDatabaseDialog.decompressionTab.click();\n    await addDatabaseDialog.keyNameFormatDropdown.click();\n    // Select HEX option (available options: Unicode, HEX)\n    await databasesPage.page.getByRole('option', { name: 'HEX' }).click();\n\n    // Verify the value changed\n    await expect(addDatabaseDialog.keyNameFormatDropdown).toContainText('HEX');\n\n    // Go back to General tab and submit\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should add database with TLS/SSL', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build({\n      host: redisConfig.tlsRedis.host,\n      port: redisConfig.tlsRedis.port,\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    // Configure TLS with CA and client certificates\n    await addDatabaseDialog.configureTls({\n      enabled: true,\n      verifyServerCert: false,\n      caCert: redisConfig.tlsCaCert,\n      clientCert: redisConfig.tlsClientCert,\n    });\n\n    // Go back to General tab and submit\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/clone/clone-database.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory, ClusterConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\nimport { faker } from '@faker-js/faker';\n\n/**\n * Clone Database Tests (TEST_PLAN.md: 1.3 Clone Database)\n *\n * Tests for cloning database connections via the Edit -> Clone flow.\n */\ntest.describe('Clone Database', () => {\n  let standaloneDb: DatabaseInstance;\n  let clusterDb: DatabaseInstance | undefined;\n\n  const clonedNames: string[] = [];\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const standaloneConfig = StandaloneConfigFactory.build();\n    standaloneDb = await apiHelper.createDatabase(standaloneConfig);\n\n    try {\n      const clusterConfig = ClusterConfigFactory.build();\n      clusterDb = await apiHelper.createDatabase(clusterConfig);\n    } catch {\n      // Cluster may not be available in all environments\n    }\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (standaloneDb?.id) {\n      await apiHelper.deleteDatabase(standaloneDb.id);\n    }\n    if (clusterDb?.id) {\n      await apiHelper.deleteDatabase(clusterDb.id);\n    }\n  });\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    if (clonedNames.length === 0) return;\n\n    const knownIds = new Set([standaloneDb?.id, clusterDb?.id].filter(Boolean));\n\n    try {\n      const allDbs = await apiHelper.getDatabases();\n      for (const db of allDbs) {\n        if (clonedNames.includes(db.name) && !knownIds.has(db.id)) {\n          await apiHelper.deleteDatabase(db.id);\n        }\n      }\n    } catch {\n      // Ignore cleanup errors\n    }\n\n    clonedNames.length = 0;\n  });\n\n  test('should clone standalone database with pre-populated form', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n\n    const alias = await cloneDatabaseDialog.getDatabaseAlias();\n    const host = await cloneDatabaseDialog.getHost();\n    const port = await cloneDatabaseDialog.getPort();\n\n    expect(alias).toBe(standaloneDb.name);\n    expect(host).toBe(standaloneDb.host);\n    expect(port).toBe(standaloneDb.port.toString());\n\n    await cloneDatabaseDialog.cancel();\n  });\n\n  test('should clone database with same name', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n    clonedNames.push(standaloneDb.name);\n\n    await cloneDatabaseDialog.submit();\n    await cloneDatabaseDialog.dialog.waitFor({ state: 'hidden' });\n\n    await databaseList.expectDatabaseVisible(standaloneDb.name, { searchFirst: true });\n  });\n\n  test('should clone database with new name', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n    const newName = `test-cloned-${faker.string.alphanumeric(8)}`;\n    clonedNames.push(newName);\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n    await cloneDatabaseDialog.setDatabaseAlias(newName);\n    await cloneDatabaseDialog.submit();\n    await cloneDatabaseDialog.dialog.waitFor({ state: 'hidden' });\n\n    await databaseList.expectDatabaseVisible(newName, { searchFirst: true });\n  });\n\n  test('should cancel clone operation', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n    const newName = `test-cancel-clone-${faker.string.alphanumeric(8)}`;\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n    await cloneDatabaseDialog.setDatabaseAlias(newName);\n    await cloneDatabaseDialog.cancel();\n\n    await expect(cloneDatabaseDialog.dialog).not.toBeVisible();\n    await databaseList.expectDatabaseNotVisible(newName);\n  });\n\n  test('should go back to edit dialog from clone dialog', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n    await expect(cloneDatabaseDialog.dialog).toBeVisible();\n\n    await cloneDatabaseDialog.goBack();\n\n    const editDialog = databasesPage.page.getByRole('dialog', { name: /edit database/i });\n    await expect(editDialog).toBeVisible();\n\n    await databasesPage.page.getByRole('button', { name: 'Cancel' }).click();\n  });\n\n  test('should clone OSS Cluster database', async ({ databasesPage }) => {\n    test.skip(!clusterDb, 'OSS Cluster not available in this environment');\n\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n    const clonedName = `test-clone-cluster-${faker.string.alphanumeric(8)}`;\n    clonedNames.push(clonedName);\n\n    await databaseList.openCloneDialog(clusterDb!.name);\n\n    const host = await cloneDatabaseDialog.getHost();\n    const port = await cloneDatabaseDialog.getPort();\n    expect(host).toBe(clusterDb!.host);\n    expect(port).toBe(clusterDb!.port.toString());\n\n    await cloneDatabaseDialog.setDatabaseAlias(clonedName);\n    await cloneDatabaseDialog.submit();\n    await cloneDatabaseDialog.dialog.waitFor({ state: 'hidden' });\n\n    await databaseList.expectDatabaseVisible(clonedName, { searchFirst: true });\n  });\n\n  test('should verify cloned database appears in list after creation', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n    const clonedName = `test-verify-clone-${faker.string.alphanumeric(8)}`;\n    clonedNames.push(clonedName);\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n    await cloneDatabaseDialog.setDatabaseAlias(clonedName);\n    await cloneDatabaseDialog.submit();\n    await cloneDatabaseDialog.dialog.waitFor({ state: 'hidden' });\n\n    await databaseList.clearSearch();\n    await databaseList.search(clonedName);\n    const row = databaseList.getRow(clonedName);\n    await expect(row).toBeVisible();\n  });\n\n  test('should verify \"New Connection\" badge on cloned database', async ({ databasesPage }) => {\n    const { databaseList, cloneDatabaseDialog } = databasesPage;\n    const clonedName = `test-badge-clone-${faker.string.alphanumeric(8)}`;\n    clonedNames.push(clonedName);\n\n    await databaseList.openCloneDialog(standaloneDb.name);\n    await cloneDatabaseDialog.setDatabaseAlias(clonedName);\n    await cloneDatabaseDialog.submit();\n    await cloneDatabaseDialog.dialog.waitFor({ state: 'hidden' });\n\n    await databaseList.expectDatabaseVisible(clonedName, { searchFirst: true });\n\n    const row = databaseList.getRow(clonedName);\n    const newIndicator = row.getByTestId(/database-status-new-/);\n    await expect(newIndicator).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/decompression/decompression.spec.ts",
    "content": "import { gzipSync } from 'zlib';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\n/**\n * Decompression Tests (TEST_PLAN.md: 1.8 Decompression)\n *\n * Tests for configuring automatic data decompression on database connections.\n */\ntest.describe('Decompression', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const config = StandaloneConfigFactory.build();\n    database = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  test('should confirm setting a decompression type works', async ({ databasesPage }) => {\n    const { databaseList, addDatabaseDialog } = databasesPage;\n    const compressorFormat = 'GZIP';\n\n    // Open edit dialog for the database\n    await databaseList.search(database.name);\n    await databaseList.edit(database.name);\n    const editDialog = databasesPage.page.getByRole('dialog', { name: /edit database/i });\n    await expect(editDialog).toBeVisible();\n\n    // Enable decompression with GZIP format\n    await addDatabaseDialog.enableDecompression(compressorFormat);\n    await expect(addDatabaseDialog.enableDecompressionCheckbox).toBeChecked();\n\n    // Save the changes\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await editDialog.waitFor({ state: 'hidden' });\n\n    // Reopen edit dialog and verify the setting persisted\n    await databaseList.search(database.name);\n    await databaseList.edit(database.name);\n    await expect(editDialog).toBeVisible();\n    await addDatabaseDialog.decompressionTab.click();\n    await expect(addDatabaseDialog.enableDecompressionCheckbox).toBeChecked();\n    await expect(addDatabaseDialog.compressorDropdown).toContainText(compressorFormat);\n\n    // Clean up: disable decompression and save\n    await addDatabaseDialog.enableDecompressionCheckbox.click();\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await editDialog.waitFor({ state: 'hidden' });\n  });\n\n  test('should decompress GZIP-compressed key values in Browser', async ({ apiHelper, browserPage }) => {\n    const originalText = 'Hello, this is decompressed data!';\n    const keyName = `decompression-test:${Date.now()}`;\n    const compressedBuffer = gzipSync(Buffer.from(originalText));\n\n    // Seed a GZIP-compressed key into the database\n    await apiHelper.createStringKey(database.id, keyName, {\n      type: 'Buffer',\n      data: Array.from(compressedBuffer),\n    });\n\n    // Enable GZIP decompression on the database via API\n    await apiHelper.updateDatabase(database.id, { compressor: 'GZIP' });\n\n    try {\n      // Navigate to Browser and verify decompressed value\n      await browserPage.goto(database.id);\n      await browserPage.keyList.searchKeys(keyName);\n      await browserPage.keyList.clickKey(keyName);\n      await browserPage.keyDetails.waitForKeyDetails();\n\n      const displayedValue = await browserPage.keyDetails.getStringValue();\n      expect(displayedValue).toContain(originalText);\n    } finally {\n      // Clean up: remove compressor and delete test key\n      await apiHelper.updateDatabase(database.id, { compressor: 'NONE' });\n      await apiHelper.deleteKeysByPattern(database.id, keyName);\n    }\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/encryption/encryption.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\n\n/**\n * Certificate and Encryption Handling Tests (TEST_PLAN.md: 1.7 Certificate and Encryption Handling)\n *\n * Tests for credential encryption settings managed through the EULA popup.\n * These tests reset the EULA agreements to trigger the consent dialog,\n * so they re-accept EULA after each test to restore normal state.\n */\ntest.describe('Certificate and Encryption Handling', () => {\n  test.describe.configure({ mode: 'serial' });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.acceptEula();\n  });\n\n  test('should store credentials encrypted when encryption enabled', async ({ apiHelper, eulaPage }) => {\n    // Reset agreements to trigger EULA popup\n    await apiHelper.resetAgreements();\n\n    await eulaPage.goto();\n    await eulaPage.waitForPopup();\n\n    // Encryption is enabled by default in the EULA dialog\n    const isEncryptionChecked = await eulaPage.isSwitchChecked(eulaPage.encryptionSwitch);\n    expect(isEncryptionChecked).toBe(true);\n\n    // Accept with encryption enabled (default)\n    await eulaPage.acceptEula();\n\n    // Verify via API that encryption is enabled\n    const settings = await apiHelper.getSettings();\n    expect(settings.agreements?.encryption).toBe(true);\n  });\n\n  test('should display warning when encryption disabled and credentials stored as plaintext', async ({\n    apiHelper,\n    eulaPage,\n  }) => {\n    // Reset agreements to trigger EULA popup again\n    await apiHelper.resetAgreements();\n\n    await eulaPage.goto();\n    await eulaPage.waitForPopup();\n\n    // Disable encryption\n    await eulaPage.encryptionSwitch.click();\n    const isEncryptionChecked = await eulaPage.isSwitchChecked(eulaPage.encryptionSwitch);\n    expect(isEncryptionChecked).toBe(false);\n\n    // Verify the plain-text storage warning is visible in the dialog\n    await expect(eulaPage.dialog.getByText(/stored locally in plain text/)).toBeVisible();\n\n    // Accept EULA and submit\n    await eulaPage.eulaSwitch.click();\n    await eulaPage.submitButton.click();\n    await expect(eulaPage.dialog).not.toBeVisible();\n\n    // Verify via API that encryption is disabled\n    const settings = await apiHelper.getSettings();\n    expect(settings.agreements?.encryption).toBe(false);\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/import-export/import-export.spec.ts",
    "content": "import * as path from 'path';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport {\n  generateValidSingle,\n  generateValidMultiple,\n  generatePartialValid,\n  generateWithTags,\n} from 'e2eSrc/test-data/databases/import-fixtures/generate';\n\nconst fixturesDir = path.resolve(__dirname, '../../../../test-data/databases/import-fixtures');\n\n/**\n * Import/Export Tests (TEST_PLAN.md: 1.5 Import/Export)\n *\n * Tests for importing and exporting database connections.\n */\ntest.describe('Import / Export Databases', () => {\n  const importedDbNames: string[] = [];\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    if (importedDbNames.length === 0) return;\n    try {\n      const allDbs = await apiHelper.getDatabases();\n      for (const db of allDbs) {\n        if (importedDbNames.includes(db.name)) {\n          await apiHelper.deleteDatabase(db.id);\n        }\n      }\n    } catch {\n      // Ignore cleanup errors\n    }\n    importedDbNames.length = 0;\n  });\n\n  // ==================== IMPORT ====================\n\n  test('should open import dialog', async ({ databasesPage }) => {\n    await databasesPage.openImportDialog();\n    await expect(databasesPage.importDatabaseDialog.dialog).toBeVisible();\n    await databasesPage.importDatabaseDialog.cancel();\n  });\n\n  test('should import single database', async ({ databasesPage }) => {\n    const filePath = generateValidSingle();\n    importedDbNames.push('test-import-single');\n\n    await databasesPage.openImportDialog();\n    await databasesPage.importDatabaseDialog.uploadFile(filePath);\n    await databasesPage.importDatabaseDialog.submit();\n\n    await expect(databasesPage.importDatabaseDialog.okButton).toBeVisible({ timeout: 30000 });\n    const successCount = await databasesPage.importDatabaseDialog.getSuccessCount();\n    expect(successCount).toBeGreaterThanOrEqual(1);\n\n    await databasesPage.importDatabaseDialog.close();\n  });\n\n  test('should import multiple databases', async ({ databasesPage }) => {\n    const filePath = generateValidMultiple();\n    importedDbNames.push('test-import-multi-1', 'test-import-multi-2');\n\n    const result = await databasesPage.importDatabasesFromFile(filePath);\n    expect(result.success).toBe(2);\n  });\n\n  test('should show success count after import', async ({ databasesPage }) => {\n    const filePath = generateValidSingle();\n    importedDbNames.push('test-import-single');\n\n    await databasesPage.openImportDialog();\n    await databasesPage.importDatabaseDialog.uploadFile(filePath);\n    await databasesPage.importDatabaseDialog.submit();\n\n    await expect(databasesPage.importDatabaseDialog.okButton).toBeVisible({ timeout: 30000 });\n\n    await databasesPage.importDatabaseDialog.expandSuccessResults();\n    const successCount = await databasesPage.importDatabaseDialog.getSuccessCount();\n    expect(successCount).toBeGreaterThanOrEqual(1);\n\n    await databasesPage.importDatabaseDialog.close();\n  });\n\n  test('should cancel import dialog', async ({ databasesPage }) => {\n    await databasesPage.openImportDialog();\n    await expect(databasesPage.importDatabaseDialog.dialog).toBeVisible();\n\n    await databasesPage.importDatabaseDialog.cancel();\n    await expect(databasesPage.importDatabaseDialog.dialog).not.toBeVisible();\n  });\n\n  test('should import with errors (partial success)', async ({ databasesPage }) => {\n    const filePath = generatePartialValid();\n    importedDbNames.push('test-import-partial-ok');\n\n    await databasesPage.openImportDialog();\n    await databasesPage.importDatabaseDialog.uploadFile(filePath);\n    await databasesPage.importDatabaseDialog.submit();\n\n    await expect(databasesPage.importDatabaseDialog.okButton).toBeVisible({ timeout: 30000 });\n\n    const successCount = await databasesPage.importDatabaseDialog.getSuccessCount();\n    const failedCount = await databasesPage.importDatabaseDialog.getFailedCount();\n\n    expect(successCount).toBeGreaterThanOrEqual(1);\n    expect(failedCount).toBeGreaterThanOrEqual(1);\n\n    await databasesPage.importDatabaseDialog.close();\n  });\n\n  test('should import invalid file format', async ({ databasesPage }) => {\n    const filePath = path.join(fixturesDir, 'invalid-format.txt');\n\n    await databasesPage.openImportDialog();\n    await databasesPage.importDatabaseDialog.uploadFile(filePath);\n    await databasesPage.importDatabaseDialog.submit();\n\n    // Invalid format shows a parse error with a Retry button (not OK)\n    await expect(databasesPage.importDatabaseDialog.errorMessage).toBeVisible({ timeout: 30000 });\n    await expect(databasesPage.importDatabaseDialog.retryButton).toBeVisible();\n\n    await databasesPage.importDatabaseDialog.close();\n  });\n\n  test('should confirm database tags are imported correctly', async ({ databasesPage }) => {\n    const filePath = generateWithTags();\n    importedDbNames.push('test-import-tagged');\n\n    const result = await databasesPage.importDatabasesFromFile(filePath);\n    expect(result.success).toBeGreaterThanOrEqual(1);\n\n    // Verify the imported database exists and has tags\n    const { databaseList, tagsDialog } = databasesPage;\n    await databaseList.expectDatabaseVisible('test-import-tagged', { searchFirst: true });\n\n    await databaseList.openTagsManager('test-import-tagged');\n    await tagsDialog.waitForVisible();\n\n    const tagCount = await tagsDialog.getTagCount();\n    expect(tagCount).toBe(2);\n\n    await tagsDialog.close();\n  });\n\n  // ==================== EXPORT ====================\n\n  test('should export databases', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build({ name: 'test-export-db' });\n    importedDbNames.push(config.name);\n\n    // Add via UI so the list updates immediately\n    await databasesPage.addDatabase(config);\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n\n    await databaseList.selectRow(config.name);\n\n    const download = await databaseList.exportSelectedAndDownload();\n    const suggestedName = download.suggestedFilename();\n    expect(suggestedName).toMatch(/\\.json$/i);\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/list/database-list.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory, ClusterConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\nimport { faker } from '@faker-js/faker';\n\n/**\n * Database List Tests (TEST_PLAN.md: 1.2 Database List)\n *\n * Tests for the database list page: search, columns, selection, bulk actions,\n * row actions, and status indicators.\n */\ntest.describe('Database List', () => {\n  const databases: DatabaseInstance[] = [];\n  let standaloneDb1: DatabaseInstance;\n  let standaloneDb2: DatabaseInstance;\n  let clusterDb: DatabaseInstance | undefined;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const config1 = StandaloneConfigFactory.build({ name: `test-alpha-${faker.string.alphanumeric(6)}` });\n    standaloneDb1 = await apiHelper.createDatabase(config1);\n    databases.push(standaloneDb1);\n\n    const config2 = StandaloneConfigFactory.build({ name: `test-beta-${faker.string.alphanumeric(6)}` });\n    standaloneDb2 = await apiHelper.createDatabase(config2);\n    databases.push(standaloneDb2);\n\n    try {\n      const clusterConfig = ClusterConfigFactory.build({ name: `test-gamma-cluster-${faker.string.alphanumeric(6)}` });\n      clusterDb = await apiHelper.createDatabase(clusterConfig);\n      databases.push(clusterDb);\n    } catch {\n      // Cluster may not be available in all environments\n    }\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    for (const db of databases) {\n      if (db?.id) {\n        try {\n          await apiHelper.deleteDatabase(db.id);\n        } catch {\n          // Ignore cleanup errors\n        }\n      }\n    }\n  });\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  // ==================== SEARCH ====================\n\n  test.describe('Search', () => {\n    test('should filter databases by search query', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.search(standaloneDb1.name);\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n      await databaseList.expectDatabaseNotVisible(standaloneDb2.name);\n    });\n\n    test('should filter with partial match', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      // Search with partial name (the \"test-alpha\" prefix)\n      const partial = standaloneDb1.name.substring(0, 10);\n      await databaseList.search(partial);\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n    });\n\n    test('should perform case-insensitive search', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.search(standaloneDb1.name.toUpperCase());\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n    });\n\n    test('should filter by host', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.search(standaloneDb1.host);\n\n      // Both standalone databases share the same host, so both should be visible\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n      await databaseList.expectDatabaseVisible(standaloneDb2.name);\n    });\n\n    test('should clear search', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.search(standaloneDb1.name);\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n\n      await databaseList.clearSearch();\n      const searchValue = await databaseList.getSearchValue();\n      expect(searchValue).toBe('');\n\n      // All databases should be visible again\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n      await databaseList.expectDatabaseVisible(standaloneDb2.name);\n    });\n\n    test('should show no results message', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n      const nonExistentName = `non-existent-${faker.string.alphanumeric(16)}`;\n\n      await databaseList.search(nonExistentName);\n\n      const noResults = databasesPage.page.getByText(/no results/i);\n      await expect(noResults).toBeVisible();\n    });\n\n    test('should search by database name', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.search(standaloneDb2.name);\n      await databaseList.expectDatabaseVisible(standaloneDb2.name);\n      await databaseList.expectDatabaseNotVisible(standaloneDb1.name);\n    });\n\n    test('should search by host', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.search(standaloneDb1.host);\n      await databaseList.expectDatabaseVisible(standaloneDb1.name);\n    });\n\n    test('should search by port', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      const db = clusterDb ?? standaloneDb1;\n      await databaseList.search(db.port.toString());\n      await databaseList.expectDatabaseVisible(db.name);\n    });\n  });\n\n  // ==================== COLUMNS ====================\n\n  test.describe('Columns', () => {\n    test('should show columns button', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n      await expect(databaseList.columnsButton).toBeVisible();\n    });\n\n    test('should hide and show columns', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      // Verify Host column is visible by default\n      const isHostVisible = await databaseList.isColumnVisible('Host');\n      expect(isHostVisible).toBe(true);\n\n      // Toggle Host column off\n      await databaseList.toggleColumn('Host');\n      const isHostHidden = await databaseList.isColumnVisible('Host');\n      expect(isHostHidden).toBe(false);\n\n      // Toggle Host column back on\n      await databaseList.toggleColumn('Host');\n      const isHostRestored = await databaseList.isColumnVisible('Host');\n      expect(isHostRestored).toBe(true);\n    });\n  });\n\n  // ==================== SELECTION ====================\n\n  test.describe('Selection', () => {\n    test('should select single database', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.selectRow(standaloneDb1.name);\n      const isSelected = await databaseList.isRowSelected(standaloneDb1.name);\n      expect(isSelected).toBe(true);\n\n      await databaseList.expectSelectedCount(1);\n      await databaseList.cancelSelection();\n    });\n\n    test('should select multiple databases', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.selectRow(standaloneDb1.name);\n      await databaseList.selectRow(standaloneDb2.name);\n\n      await databaseList.expectSelectedCount(2);\n      await databaseList.cancelSelection();\n    });\n\n    test('should select all databases', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.selectAll();\n\n      const totalCount = await databaseList.getVisibleRowCount();\n      const selectedCount = await databaseList.getSelectedCount();\n      expect(selectedCount).toBe(totalCount);\n\n      await databaseList.cancelSelection();\n    });\n\n    test('should delete multiple databases', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      // Create two temporary databases for deletion\n      const tmpConfig1 = StandaloneConfigFactory.build({ name: `test-del-a-${faker.string.alphanumeric(6)}` });\n      const tmpConfig2 = StandaloneConfigFactory.build({ name: `test-del-b-${faker.string.alphanumeric(6)}` });\n      // Add databases via UI so they appear immediately in the list\n      await databasesPage.addDatabase(tmpConfig1);\n      await databaseList.expectDatabaseVisible(tmpConfig1.name, { searchFirst: true });\n\n      await databasesPage.addDatabase(tmpConfig2);\n      await databaseList.expectDatabaseVisible(tmpConfig2.name, { searchFirst: true });\n      await databaseList.clearSearch();\n\n      await databaseList.search(tmpConfig1.name.substring(0, 9));\n      await databaseList.selectRow(tmpConfig1.name);\n      await databaseList.selectRow(tmpConfig2.name);\n      await databaseList.deleteSelected();\n\n      await databaseList.clearSearch();\n      await databaseList.expectDatabaseNotVisible(tmpConfig1.name);\n      await databaseList.expectDatabaseNotVisible(tmpConfig2.name);\n    });\n  });\n\n  // ==================== ROW ACTIONS ====================\n\n  test.describe('Row Actions', () => {\n    test('should edit database connection', async ({ databasesPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.edit(standaloneDb1.name);\n\n      const editDialog = databasesPage.page.getByRole('dialog', { name: /edit database/i });\n      await expect(editDialog).toBeVisible();\n\n      await databasesPage.page.getByRole('button', { name: 'Cancel' }).click();\n      await expect(editDialog).not.toBeVisible();\n    });\n\n    test('should clone database connection', async ({ databasesPage }) => {\n      const { databaseList, cloneDatabaseDialog } = databasesPage;\n\n      await databaseList.openCloneDialog(standaloneDb1.name);\n      await expect(cloneDatabaseDialog.dialog).toBeVisible();\n\n      await cloneDatabaseDialog.cancel();\n    });\n\n    test('should connect to database', async ({ databasesPage, browserPage }) => {\n      const { databaseList } = databasesPage;\n\n      await databaseList.connect(standaloneDb1.name);\n\n      // Verify we navigated to the browser page\n      await expect(browserPage.page.getByTestId('browser-page')).toBeVisible({ timeout: 15000 });\n\n      // Navigate back\n      await browserPage.gotoHome();\n    });\n  });\n\n  // ==================== REDIS STACK ====================\n\n  test('should verify Redis Stack icon displayed for databases with modules', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    const row = databaseList.getRow(standaloneDb1.name);\n    await expect(row).toBeVisible();\n\n    const moduleIcons = row.getByTestId(/_module$/);\n    await expect(moduleIcons.first()).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/pagination/pagination.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\nimport { faker } from '@faker-js/faker';\n\nconst DB_COUNT = 20;\nconst PAGINATION_PREFIX = `test-pag-${faker.string.alphanumeric(4)}-`;\n\n/**\n * Pagination Tests (TEST_PLAN.md: 1.4 Pagination)\n *\n * Tests for database list pagination when there are more than 15 databases.\n * Creates 20 databases in beforeAll to ensure pagination is triggered.\n */\ntest.describe('Database List Pagination', () => {\n  const createdDbs: DatabaseInstance[] = [];\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const promises = Array.from({ length: DB_COUNT }, (_, i) => {\n      const config = StandaloneConfigFactory.build({\n        name: `${PAGINATION_PREFIX}${String(i).padStart(2, '0')}`,\n      });\n      return apiHelper.createDatabase(config);\n    });\n\n    const results = await Promise.all(promises);\n    createdDbs.push(...results);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    const promises = createdDbs.map((db) => {\n      if (db?.id) {\n        return apiHelper.deleteDatabase(db.id).catch(() => {});\n      }\n      return Promise.resolve();\n    });\n    await Promise.all(promises);\n  });\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n    // Filter to only show test databases to ensure consistent pagination state\n    await databasesPage.databaseList.search(PAGINATION_PREFIX);\n  });\n\n  test('should show pagination when more than 15 databases', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n    const isPaginationVisible = await databaseList.isPaginationVisible();\n    expect(isPaginationVisible).toBe(true);\n  });\n\n  test('should navigate to next page', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    const firstPageNames = await databaseList.getDatabaseNames();\n    await databaseList.goToNextPage();\n    const secondPageNames = await databaseList.getDatabaseNames();\n\n    expect(secondPageNames.length).toBeGreaterThan(0);\n    // Pages should show different databases\n    expect(firstPageNames[0]).not.toBe(secondPageNames[0]);\n  });\n\n  test('should navigate to previous page', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    const firstPageNames = await databaseList.getDatabaseNames();\n    await databaseList.goToNextPage();\n    await databaseList.goToPreviousPage();\n    const backToFirstPageNames = await databaseList.getDatabaseNames();\n\n    expect(firstPageNames[0]).toBe(backToFirstPageNames[0]);\n  });\n\n  test('should navigate to first and last page', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    // Go to last page\n    await databaseList.goToLastPage();\n    const isNextEnabled = await databaseList.isNextPageEnabled();\n    expect(isNextEnabled).toBe(false);\n\n    // Go back to first page\n    await databaseList.goToFirstPage();\n    const isPrevEnabled = await databaseList.isPreviousPageEnabled();\n    expect(isPrevEnabled).toBe(false);\n  });\n\n  test('should change items per page', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    // Change to 25 items per page - all 20 should fit on one page\n    await databaseList.setItemsPerPage('25');\n\n    const visibleCount = await databaseList.getVisibleRowCount();\n    expect(visibleCount).toBe(DB_COUNT);\n\n    // Pagination nav buttons should be disabled since everything fits on one page\n    const isNextEnabled = await databaseList.isNextPageEnabled();\n    expect(isNextEnabled).toBe(false);\n  });\n\n  test('should select page from dropdown', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    // With default 10 items per page and 20 databases, there should be 2 pages\n    await databaseList.setItemsPerPage('10');\n    await databaseList.selectPage('2');\n\n    const currentPage = await databaseList.getCurrentPage();\n    expect(currentPage).toContain('2');\n  });\n\n  test('should show correct row count', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    const rowCountText = await databaseList.getRowCountText();\n    expect(rowCountText).toContain(`${DB_COUNT}`);\n    expect(rowCountText).toMatch(/Showing \\d+ out of \\d+ rows/);\n  });\n\n  test('should disable pagination buttons on first page', async ({ databasesPage }) => {\n    const { databaseList } = databasesPage;\n\n    // We start on the first page (beforeEach navigates to databases page)\n    const isPrevEnabled = await databaseList.isPreviousPageEnabled();\n    const isFirstEnabled = await databaseList.isFirstPageEnabled();\n    expect(isPrevEnabled).toBe(false);\n    expect(isFirstEnabled).toBe(false);\n\n    // Next and last should be enabled\n    const isNextEnabled = await databaseList.isNextPageEnabled();\n    const isLastEnabled = await databaseList.isLastPageEnabled();\n    expect(isNextEnabled).toBe(true);\n    expect(isLastEnabled).toBe(true);\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/security/connection-security.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { redisConfig } from 'e2eSrc/config/databases';\n\n/**\n * Connection Security Tests (TEST_PLAN.md: 1.1.1 Connection Security)\n *\n * Tests for adding databases with SSH tunneling, SNI, and TLS certificates.\n * These tests require the corresponding infrastructure in docker-compose.\n */\ntest.describe('Connection Security', () => {\n  const createdDatabaseNames: string[] = [];\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  test.afterEach(async ({ databasesPage }) => {\n    for (const name of createdDatabaseNames) {\n      try {\n        if (await databasesPage.databaseList.exists(name)) {\n          await databasesPage.databaseList.delete(name);\n        }\n      } catch {\n        // Ignore cleanup errors\n      }\n    }\n    createdDatabaseNames.length = 0;\n  });\n\n  test('should add database using SSH tunneling', async ({ databasesPage }) => {\n    const sshTunnel = redisConfig.sshTunnel;\n    const sshRedis = redisConfig.sshRedis;\n\n    // Skip if any required SSH config value is missing\n    test.skip(\n      !sshTunnel.host || !sshTunnel.port || !sshTunnel.username || !sshRedis.host || !sshRedis.port,\n      'SSH infrastructure not configured',\n    );\n\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const config = StandaloneConfigFactory.build({\n      host: sshRedis.host!,\n      port: sshRedis.port!,\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    await addDatabaseDialog.configureSsh({\n      host: sshTunnel.host!,\n      port: sshTunnel.port!,\n      username: sshTunnel.username!,\n      password: sshTunnel.password,\n    });\n\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should connect using SNI configuration', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const { caCert, clientCert } = redisConfig.createUniqueTlsCerts();\n    const config = StandaloneConfigFactory.build({\n      host: redisConfig.tlsRedis.host,\n      port: redisConfig.tlsRedis.port,\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    // Enable TLS first (SNI requires TLS)\n    await addDatabaseDialog.configureTls({\n      enabled: true,\n      verifyServerCert: false,\n      caCert,\n      clientCert,\n    });\n\n    // Enable SNI with the TLS host as servername\n    await addDatabaseDialog.enableSni(redisConfig.tlsRedis.host);\n\n    await expect(addDatabaseDialog.sniCheckbox).toBeChecked();\n    await expect(addDatabaseDialog.sniServernameInput).toHaveValue(redisConfig.tlsRedis.host);\n\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n\n  test('should connect with TLS using CA, client, and private key certificates', async ({ databasesPage }) => {\n    const { addDatabaseDialog, databaseList } = databasesPage;\n    const { caCert, clientCert } = redisConfig.createUniqueTlsCerts();\n    const config = StandaloneConfigFactory.build({\n      host: redisConfig.tlsRedis.host,\n      port: redisConfig.tlsRedis.port,\n    });\n    createdDatabaseNames.push(config.name);\n\n    await databasesPage.openAddDatabaseDialog();\n    await addDatabaseDialog.openConnectionSettings();\n    await addDatabaseDialog.fillForm(config);\n\n    await addDatabaseDialog.configureTls({\n      enabled: true,\n      verifyServerCert: false,\n      caCert,\n      clientCert,\n    });\n\n    // Verify all TLS fields are populated\n    await expect(addDatabaseDialog.useTlsCheckbox).toBeChecked();\n    await expect(addDatabaseDialog.requiresClientAuthCheckbox).toBeChecked();\n\n    await addDatabaseDialog.generalTab.click();\n    await addDatabaseDialog.submit();\n    await addDatabaseDialog.waitForHidden();\n    await databaseList.expectDatabaseVisible(config.name, { searchFirst: true });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/databases/tags/tags.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\nimport { faker } from '@faker-js/faker';\n\n/**\n * Database Tags Tests (TEST_PLAN.md: 1.6 Database Tags)\n *\n * Tests for managing database tags - key-value pairs that help categorize databases.\n * Tags are managed via the \"Manage Instance Tags\" dialog accessible from the database list.\n */\ntest.describe('Database Tags', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    // Create a test database for all tests in this file\n    const config = StandaloneConfigFactory.build({ name: 'test-tags-db' });\n    database = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    // Clean up the test database\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.beforeEach(async ({ databasesPage }) => {\n    await databasesPage.goto();\n  });\n\n  test('should open tags dialog for a database', async ({ databasesPage }) => {\n    const { tagsDialog, databaseList } = databasesPage;\n\n    // Open tags manager for the test database\n    await databaseList.openTagsManager(database.name);\n\n    // Verify dialog is visible\n    await tagsDialog.expectVisible();\n\n    // Close the dialog\n    await tagsDialog.close();\n    await tagsDialog.expectHidden();\n  });\n\n  test('should add descriptive tags to a database', async ({ databasesPage }) => {\n    const { tagsDialog, databaseList } = databasesPage;\n    const tagKey = `env-${faker.string.alphanumeric(4)}`;\n    const tagValue = 'production';\n\n    // Open tags manager\n    await databaseList.openTagsManager(database.name);\n\n    // Add a tag\n    await tagsDialog.addTag(tagKey, tagValue);\n\n    // Verify Save button is enabled\n    await expect(tagsDialog.saveButton).toBeEnabled();\n\n    // Save the tags\n    await tagsDialog.save();\n\n    // Dialog should close after save\n    await expect(tagsDialog.dialog).not.toBeVisible();\n\n    // Re-open dialog to verify tag was saved\n    await databaseList.openTagsManager(database.name);\n\n    // Verify tag count is at least 1\n    const tagCount = await tagsDialog.getTagCount();\n    expect(tagCount).toBeGreaterThanOrEqual(1);\n\n    // Clean up - delete the tag we added\n    await tagsDialog.deleteTag(0);\n    await tagsDialog.save();\n  });\n\n  test('should remove tags from a database', async ({ databasesPage }) => {\n    const { tagsDialog, databaseList } = databasesPage;\n    const tagKey = `temp-${faker.string.alphanumeric(4)}`;\n    const tagValue = 'to-delete';\n\n    // First, add a tag to delete\n    await databaseList.openTagsManager(database.name);\n    await tagsDialog.addTag(tagKey, tagValue);\n    await tagsDialog.save();\n    await expect(tagsDialog.dialog).not.toBeVisible();\n\n    // Re-open and verify the tag exists\n    await databaseList.openTagsManager(database.name);\n    const initialCount = await tagsDialog.getTagCount();\n    expect(initialCount).toBeGreaterThanOrEqual(1);\n\n    // Delete the first tag\n    await tagsDialog.deleteTag(0);\n\n    // Save the changes\n    await tagsDialog.save();\n    await expect(tagsDialog.dialog).not.toBeVisible();\n\n    // Re-open and verify tag count decreased\n    await databaseList.openTagsManager(database.name);\n    const finalCount = await tagsDialog.getTagCount();\n    expect(finalCount).toBeLessThan(initialCount);\n\n    await tagsDialog.close();\n  });\n\n  test('should cancel adding a tag without saving', async ({ databasesPage }) => {\n    const { tagsDialog, databaseList } = databasesPage;\n    const tagKey = `cancel-${faker.string.alphanumeric(4)}`;\n    const tagValue = 'should-not-save';\n\n    // Open tags manager and get initial count\n    await databaseList.openTagsManager(database.name);\n    const initialCount = await tagsDialog.getTagCount();\n\n    // Add a tag but don't save\n    await tagsDialog.addTag(tagKey, tagValue);\n\n    // Cancel instead of saving\n    await tagsDialog.cancel();\n    await expect(tagsDialog.dialog).not.toBeVisible();\n\n    // Re-open and verify tag was not saved\n    await databaseList.openTagsManager(database.name);\n    const finalCount = await tagsDialog.getTagCount();\n    expect(finalCount).toBe(initialCount);\n\n    await tagsDialog.close();\n  });\n\n  test('should persist tags after saving and reopening', async ({ databasesPage }) => {\n    const { tagsDialog, databaseList } = databasesPage;\n    const tagKey = `persist-${faker.string.alphanumeric(4)}`;\n    const tagValue = `value-${faker.string.alphanumeric(4)}`;\n\n    // Add and save a tag\n    await databaseList.openTagsManager(database.name);\n    await tagsDialog.addTag(tagKey, tagValue);\n    await tagsDialog.save();\n    await expect(tagsDialog.dialog).not.toBeVisible();\n\n    // Navigate away and back (refresh the page)\n    await databasesPage.goto();\n\n    // Re-open tags dialog and verify tag persisted\n    await databaseList.openTagsManager(database.name);\n    const tagCount = await tagsDialog.getTagCount();\n    expect(tagCount).toBeGreaterThanOrEqual(1);\n\n    // Clean up - delete the tag\n    await tagsDialog.deleteTag(0);\n    await tagsDialog.save();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/insights/insights-panel.spec.ts",
    "content": "import { test, expect } from '../../../fixtures/base';\nimport { standaloneConfig } from '../../../config/databases/standalone';\nimport { DatabaseInstance } from '../../../types';\n\ntest.describe('Insights > Insights Panel', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase({\n      name: 'test-insights-panel-db',\n      host: standaloneConfig.host,\n      port: standaloneConfig.port,\n    });\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test.describe('Panel Lifecycle', () => {\n    test.beforeEach(async ({ browserPage }) => {\n      await browserPage.goto(database.id);\n    });\n\n    test('should open Insights panel', async ({ insightsPanel }) => {\n      await insightsPanel.open();\n      await expect(insightsPanel.panel).toBeVisible();\n    });\n\n    test('should close Insights panel', async ({ insightsPanel }) => {\n      await insightsPanel.open();\n      await expect(insightsPanel.panel).toBeVisible();\n\n      await insightsPanel.close();\n      await expect(insightsPanel.panel).not.toBeVisible();\n    });\n  });\n\n  test.describe('Tab Navigation', () => {\n    test.beforeEach(async ({ browserPage, insightsPanel }) => {\n      await browserPage.goto(database.id);\n      await insightsPanel.open();\n    });\n\n    test('should switch to Tutorials tab', async ({ insightsPanel }) => {\n      // First switch to Tips to ensure we can switch back\n      await insightsPanel.switchToTipsTab();\n      await expect(insightsPanel.tipsTab).toHaveAttribute('data-state', 'active');\n\n      // Now switch to Tutorials\n      await insightsPanel.switchToTutorialsTab();\n      await expect(insightsPanel.tutorialsTab).toHaveAttribute('data-state', 'active');\n    });\n\n    test('should switch to Tips tab', async ({ insightsPanel }) => {\n      // Ensure we're on Tutorials first\n      await insightsPanel.switchToTutorialsTab();\n      await expect(insightsPanel.tutorialsTab).toHaveAttribute('data-state', 'active');\n\n      // Switch to Tips\n      await insightsPanel.switchToTipsTab();\n      await expect(insightsPanel.tipsTab).toHaveAttribute('data-state', 'active');\n    });\n  });\n\n  test.describe('Tutorials Tab Content', () => {\n    test.beforeEach(async ({ browserPage, insightsPanel }) => {\n      await browserPage.goto(database.id);\n      await insightsPanel.open();\n      await insightsPanel.switchToTutorialsTab();\n    });\n\n    test('should expand and collapse tutorial folders', async ({ insightsPanel }) => {\n      // Check Tutorials accordion is visible (main tutorials section)\n      await expect(insightsPanel.redisTutorialsAccordion).toBeVisible();\n\n      // Expand Tutorials if collapsed\n      await insightsPanel.expandTutorialFolder('tutorials');\n      const isExpanded = await insightsPanel.isTutorialFolderExpanded('tutorials');\n      expect(isExpanded).toBe(true);\n\n      // Collapse Tutorials\n      await insightsPanel.collapseTutorialFolder('tutorials');\n      const isCollapsed = !(await insightsPanel.isTutorialFolderExpanded('tutorials'));\n      expect(isCollapsed).toBe(true);\n    });\n\n    test('should view Tutorials section', async ({ insightsPanel }) => {\n      // Verify main Tutorials section is visible\n      await expect(insightsPanel.redisTutorialsAccordion).toBeVisible();\n\n      // The accordion should have proper text content (label is \"Redis tutorials\")\n      await expect(insightsPanel.redisTutorialsAccordion).toContainText('Redis tutorials');\n    });\n\n    test('should run through a tutorial with pagination', async ({ insightsPanel }) => {\n      // Expand the Redis tutorials section first\n      await insightsPanel.expandTutorialFolder('tutorials');\n\n      // Expand the Data Structures nested accordion\n      await insightsPanel.expandTutorialFolder('ds');\n\n      // Open a tutorial (Working with Hashes) - this tutorial has multiple pages\n      await insightsPanel.openTutorial('ds-hashes');\n\n      // Verify tutorial page is displayed\n      await expect(insightsPanel.tutorialPageContent).toBeVisible();\n\n      // Verify pagination is visible (tutorials with multiple pages)\n      await expect(insightsPanel.paginationMenuButton).toBeVisible();\n\n      // Get initial pagination info (format: \"X of Y\")\n      const initialPagination = await insightsPanel.getPaginationInfo();\n      // Match pattern like \"1 of 5\" or \"2 of 5\"\n      expect(initialPagination).toMatch(/\\d+ of \\d+/);\n\n      // Extract current page number and total pages\n      const initialPageMatch = initialPagination.match(/(\\d+) of (\\d+)/);\n      expect(initialPageMatch).not.toBeNull();\n      const initialPage = parseInt(initialPageMatch![1], 10);\n      const totalPages = parseInt(initialPageMatch![2], 10);\n\n      // This tutorial should have multiple pages - fail if it doesn't\n      expect(totalPages).toBeGreaterThan(1);\n\n      // Assert next page button is visible (required for pagination test)\n      await expect(insightsPanel.nextPageButton).toBeVisible();\n\n      // Navigate to next page\n      await insightsPanel.goToNextPage();\n\n      // Verify we moved to the next page\n      const nextPagination = await insightsPanel.getPaginationInfo();\n      const nextPageMatch = nextPagination.match(/(\\d+) of (\\d+)/);\n      expect(nextPageMatch).not.toBeNull();\n      const nextPage = parseInt(nextPageMatch![1], 10);\n      expect(nextPage).toBe(initialPage + 1);\n\n      // Navigate back\n      await insightsPanel.goToPreviousPage();\n\n      // Verify we're back to the initial page\n      const backPagination = await insightsPanel.getPaginationInfo();\n      const backPageMatch = backPagination.match(/(\\d+) of (\\d+)/);\n      expect(backPageMatch).not.toBeNull();\n      const backPage = parseInt(backPageMatch![1], 10);\n      expect(backPage).toBe(initialPage);\n    });\n\n    test('should run a tutorial command', async ({ insightsPanel }) => {\n      // Expand the Redis tutorials section first\n      await insightsPanel.expandTutorialFolder('tutorials');\n\n      // Expand the Data Structures nested accordion\n      await insightsPanel.expandTutorialFolder('ds');\n\n      // Open a tutorial (Working with Hashes) - this tutorial has run buttons\n      await insightsPanel.openTutorial('ds-hashes');\n\n      // Verify tutorial page is displayed\n      await expect(insightsPanel.tutorialPageContent).toBeVisible();\n\n      // Get the first run button - assert it exists (required for this test)\n      const runButton = insightsPanel.getFirstRunButton();\n      await expect(runButton).toBeVisible();\n\n      // Click the run button to execute the command\n      await runButton.click();\n\n      // The button should still be present after execution\n      await expect(runButton).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/navigation/help-menu/help-menu.spec.ts",
    "content": "import { test, expect } from '../../../../fixtures/base';\n\n/**\n * Help Menu tests (TEST_PLAN.md: 0.2 Help Menu)\n *\n * Tests for the Help Center menu accessed from the sidebar navigation.\n * The Help Menu provides access to:\n * - Provide Feedback link (GitHub issues)\n * - Keyboard Shortcuts option (detailed tests in 12.8 Keyboard Shortcuts)\n * - Release Notes link\n * - Reset Onboarding option\n */\ntest.describe('Help Menu', () => {\n  test.beforeEach(async ({ sidebarPanel }) => {\n    await sidebarPanel.goto();\n  });\n\n  test('should open Help Center and display all menu options', async ({ sidebarPanel }) => {\n    const { helpMenu } = sidebarPanel;\n    await helpMenu.open();\n\n    // Verify Help Center dialog is open with all expected options\n    await expect(helpMenu.helpMenuDialog).toBeVisible();\n    await expect(helpMenu.provideFeedbackLink).toBeVisible();\n    await expect(helpMenu.keyboardShortcutsButton).toBeVisible();\n    await expect(helpMenu.releaseNotesLink).toBeVisible();\n    await expect(helpMenu.resetOnboardingButton).toBeVisible();\n  });\n\n  test('should have Release Notes link pointing to GitHub releases', async ({ sidebarPanel }) => {\n    const { helpMenu } = sidebarPanel;\n    await helpMenu.open();\n\n    // Verify link has correct href and opens in new tab\n    await expect(helpMenu.releaseNotesLink).toHaveAttribute(\n      'href',\n      'https://github.com/RedisInsight/RedisInsight/releases',\n    );\n    await expect(helpMenu.releaseNotesLink).toHaveAttribute('target', '_blank');\n  });\n\n  test('should have Provide Feedback link pointing to GitHub issues', async ({ sidebarPanel }) => {\n    const { helpMenu } = sidebarPanel;\n    await helpMenu.open();\n\n    // Verify link has correct href and opens in new tab\n    await expect(helpMenu.provideFeedbackLink).toHaveAttribute(\n      'href',\n      'https://github.com/RedisInsight/RedisInsight/issues',\n    );\n    await expect(helpMenu.provideFeedbackLink).toHaveAttribute('target', '_blank');\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/navigation/main-navigation/main-navigation.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\n\n/**\n * Navigation & Global UI > Main Navigation\n *\n * Tests for the 4 navigation paths in the sidebar:\n * - Settings button navigates to Settings page\n * - Redis logo navigates to home\n * - GitHub repo link\n * - Redis Cloud link\n */\ntest.describe('Navigation & Global UI > Main Navigation', () => {\n  test.beforeEach(async ({ sidebarPanel }) => {\n    await sidebarPanel.goto();\n    await sidebarPanel.waitForLoad();\n  });\n\n  test('should navigate to Settings page', async ({ sidebarPanel, settingsPage }) => {\n    await expect(sidebarPanel.settingsButton).toBeVisible();\n\n    await sidebarPanel.settingsButton.click();\n    await settingsPage.waitForLoad();\n    await expect(settingsPage.pageTitle).toBeVisible();\n  });\n\n  test('should navigate to home via Redis logo', async ({ sidebarPanel, settingsPage }) => {\n    await expect(sidebarPanel.mainNavigation).toBeVisible();\n    await expect(sidebarPanel.redisLogo).toBeVisible();\n\n    // Navigate away from home to Settings\n    await settingsPage.goto();\n    await expect(settingsPage.pageTitle).toBeVisible();\n\n    // Click Redis logo to go back home\n    await sidebarPanel.redisLogo.click();\n    await expect(sidebarPanel.homeTabs).toBeVisible();\n  });\n\n  test('should show GitHub repo link that opens externally', async ({ sidebarPanel }) => {\n    await expect(sidebarPanel.githubLink).toBeVisible();\n    await expect(sidebarPanel.githubLink).toHaveAttribute('href', /github/i);\n    await expect(sidebarPanel.githubLink).toHaveAttribute('target', '_blank');\n  });\n\n  test('should show Redis Cloud link that opens externally', async ({ sidebarPanel }) => {\n    await expect(sidebarPanel.cloudLink).toBeVisible();\n    await expect(sidebarPanel.cloudLink).toHaveAttribute('href', /redis\\.io\\/try-free/i);\n    await expect(sidebarPanel.cloudLink).toHaveAttribute('target', '_blank');\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/navigation/notification-center/notification-center.spec.ts",
    "content": "import { test, expect } from '../../../../fixtures/base';\n\n/**\n * Notification Center tests (TEST_PLAN.md: 0.3 Notification Center)\n *\n * Tests for the Notification Center accessed from the sidebar navigation.\n * The Notification Center displays:\n * - Unread badge count\n * - Notification list with title, body, date, and category\n * - Links within notification bodies\n */\ntest.describe('Notification Center', () => {\n  test.beforeEach(async ({ sidebarPanel }) => {\n    await sidebarPanel.goto();\n  });\n\n  test('should open Notification Center and display notifications', async ({ sidebarPanel }) => {\n    const { notificationCenter } = sidebarPanel;\n    await notificationCenter.open();\n\n    // Verify Notification Center dialog is open with title\n    await expect(notificationCenter.notificationCenterDialog).toBeVisible();\n    await expect(notificationCenter.notificationCenterTitle).toBeVisible();\n    await expect(notificationCenter.notificationCenterTitle).toHaveText('Notification Center');\n\n    // Check if notifications list is displayed\n    const hasNotifications = await notificationCenter.notificationsList.isVisible();\n\n    if (hasNotifications) {\n      // Verify notification items are displayed\n      const itemCount = await notificationCenter.notificationItems.count();\n      expect(itemCount).toBeGreaterThan(0);\n\n      // Verify first notification has all content elements\n      await expect(notificationCenter.getNotificationTitle(0)).toBeVisible();\n      await expect(notificationCenter.getNotificationBody(0)).toBeVisible();\n      await expect(notificationCenter.getNotificationDate(0)).toBeVisible();\n    } else {\n      // If no notifications, should show empty state\n      await expect(notificationCenter.noNotificationsText).toBeVisible();\n    }\n  });\n\n  test('should close Notification Center', async ({ sidebarPanel }) => {\n    const { notificationCenter } = sidebarPanel;\n    await notificationCenter.open();\n\n    // Verify dialog is open\n    await expect(notificationCenter.notificationCenterDialog).toBeVisible();\n\n    // Close and verify\n    await notificationCenter.close();\n    await expect(notificationCenter.notificationCenterDialog).not.toBeVisible();\n  });\n\n  test('should display notification links that are clickable', async ({ sidebarPanel }) => {\n    const { notificationCenter } = sidebarPanel;\n    await notificationCenter.open();\n\n    // Check if there are notification items\n    const itemCount = await notificationCenter.notificationItems.count();\n    if (itemCount === 0) {\n      test.skip();\n      return;\n    }\n\n    // Find links in first notification\n    const links = notificationCenter.getNotificationLinks(0);\n    const linkCount = await links.count();\n\n    if (linkCount > 0) {\n      // Verify first link has href attribute\n      const firstLink = links.first();\n      await expect(firstLink).toBeVisible();\n      const href = await firstLink.getAttribute('href');\n      expect(href).toBeTruthy();\n      expect(href).toMatch(/^https?:\\/\\//);\n    }\n  });\n\n  test('should show unread badge when there are unread notifications', async ({ sidebarPanel }) => {\n    const { notificationCenter } = sidebarPanel;\n\n    // Check if unread badge is visible before opening\n    const isVisible = await notificationCenter.unreadBadge.isVisible();\n\n    if (isVisible) {\n      // Badge should show a number\n      const badgeText = await notificationCenter.unreadBadge.textContent();\n      expect(badgeText).toBeTruthy();\n      // Badge should be a number or \"9+\"\n      expect(badgeText).toMatch(/^(\\d+|\\d\\+)$/);\n    }\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/settings/advanced-settings/advanced-settings.spec.ts",
    "content": "import { test, expect } from '../../../../fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\n/**\n * Advanced Settings tests (TEST_PLAN.md: 7.5 Advanced Settings)\n *\n * Tests for the Advanced section on the Settings page.\n * Verifies the warning callout, \"Keys to Scan\" configuration,\n * and that changing the scan count takes effect in the Browser.\n */\ntest.describe('Advanced Settings', () => {\n  test.beforeEach(async ({ settingsPage }) => {\n    await settingsPage.goto();\n    await settingsPage.expandAdvanced();\n  });\n\n  test('should show advanced settings warning', async ({ settingsPage }) => {\n    await expect(settingsPage.advancedWarning).toBeVisible();\n  });\n\n  test('should show keys to scan setting', async ({ settingsPage }) => {\n    await expect(settingsPage.keysToScanText).toBeVisible();\n    await expect(settingsPage.keysToScanInput).toBeVisible();\n  });\n\n  test('should change keys to scan and verify in Browser', async ({ settingsPage, apiHelper, browserPage }) => {\n    let database: DatabaseInstance | undefined;\n    let originalValue: string | undefined;\n\n    try {\n      const config = StandaloneConfigFactory.build();\n      database = await apiHelper.createDatabase(config);\n\n      // Seed keys so we can observe scan behavior\n      for (let i = 0; i < 15; i++) {\n        await apiHelper.createStringKey(database.id, `adv-scan-test:key${i}`, `value${i}`);\n      }\n\n      originalValue = await settingsPage.getKeysToScan();\n\n      // Change scan count to a small number\n      await settingsPage.setKeysToScan('5');\n\n      // Navigate to Browser and verify keys load\n      await browserPage.goto(database.id);\n      await browserPage.keyList.searchKeys('adv-scan-test:*');\n      const scannedText = await browserPage.keyList.getScannedCountText();\n      expect(scannedText).toBeTruthy();\n    } finally {\n      if (originalValue) {\n        await settingsPage.goto();\n        await settingsPage.expandAdvanced();\n        await settingsPage.setKeysToScan(originalValue);\n      }\n      if (database?.id) {\n        await apiHelper.deleteKeysByPattern(database.id, 'adv-scan-test:*');\n        await apiHelper.deleteDatabase(database.id);\n      }\n    }\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/settings/general-settings/general-settings.spec.ts",
    "content": "import { test, expect } from '../../../../fixtures/base';\n\n/**\n * General Settings tests (TEST_PLAN.md: 7.1 General Settings)\n *\n * Tests for the General section on the Settings page.\n * Verifies theme dropdown, notification switch, date/time format options,\n * custom date format input, and timezone dropdown are displayed and functional.\n */\ntest.describe('General Settings', () => {\n  test.beforeEach(async ({ settingsPage }) => {\n    await settingsPage.goto();\n  });\n\n  test('should view settings page', async ({ settingsPage }) => {\n    await expect(settingsPage.pageTitle).toBeVisible();\n  });\n\n  test('should show theme dropdown', async ({ settingsPage }) => {\n    await settingsPage.expandGeneral();\n    await expect(settingsPage.themeDropdown).toBeVisible();\n  });\n\n  test('should toggle show notifications', async ({ settingsPage }) => {\n    await settingsPage.expandGeneral();\n    await expect(settingsPage.notificationSwitch).toBeVisible();\n\n    const initialState = await settingsPage.areNotificationsEnabled();\n\n    await settingsPage.toggleNotifications();\n    const toggledState = await settingsPage.areNotificationsEnabled();\n    expect(toggledState).toBe(!initialState);\n\n    // Restore original state\n    await settingsPage.toggleNotifications();\n    const restoredState = await settingsPage.areNotificationsEnabled();\n    expect(restoredState).toBe(initialState);\n  });\n\n  test('should show date/time format options', async ({ settingsPage }) => {\n    await settingsPage.expandGeneral();\n    await expect(settingsPage.dateFormatRadioPreselected).toBeVisible();\n    await expect(settingsPage.dateFormatRadioCustom).toBeVisible();\n  });\n\n  test('should change date/time format (custom)', async ({ settingsPage }) => {\n    await settingsPage.expandGeneral();\n    await settingsPage.dateFormatRadioCustom.click();\n    await expect(settingsPage.customDateFormatInput).toBeVisible();\n  });\n\n  test('should show time zone dropdown', async ({ settingsPage }) => {\n    await settingsPage.expandGeneral();\n    await expect(settingsPage.timezoneDropdown).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/settings/redis-cloud-settings/redis-cloud-settings.spec.ts",
    "content": "import { test, expect } from '../../../../fixtures/base';\n\n/**\n * Redis Cloud Settings tests (TEST_PLAN.md: 7.4 Redis Cloud Settings)\n *\n * Tests for the Redis Cloud section on the Settings page.\n * Verifies API user keys text and cloud account buttons are displayed.\n *\n * Note: The Redis Cloud section is behind the `cloudSso` feature flag,\n * which is only enabled for the ELECTRON build type.\n */\ntest.describe('Redis Cloud Settings', () => {\n  test.beforeEach(async ({ settingsPage }, testInfo) => {\n    test.skip(testInfo.project.name !== 'electron', 'Redis Cloud settings only available in Electron');\n\n    await settingsPage.goto();\n    await settingsPage.expandRedisCloud();\n  });\n\n  test('should view Redis Cloud settings', async ({ settingsPage }) => {\n    await expect(settingsPage.apiUserKeysText).toBeVisible();\n  });\n\n  test('should configure cloud account', async ({ settingsPage }) => {\n    await expect(settingsPage.autodiscoverButton).toBeVisible();\n    await expect(settingsPage.createCloudDbButton).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/settings/workbench-settings/workbench-settings.spec.ts",
    "content": "import { test, expect } from '../../../../fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\n/**\n * Workbench Settings tests (TEST_PLAN.md: 7.3 Workbench Settings)\n *\n * Verifies Workbench section controls are visible, that changing\n * editor cleanup and pipeline commands settings persists after navigation,\n * and that those settings take effect in the Workbench (editor cleanup clears\n * or keeps the editor after running a command).\n *\n * Note: \"Configure command timeout\" is N/A -- it's a per-database setting, not on the Settings page.\n */\ntest.describe('Workbench Settings', () => {\n  test.beforeEach(async ({ settingsPage }) => {\n    await settingsPage.goto();\n  });\n\n  test('should show editor cleanup switch', async ({ settingsPage }) => {\n    await settingsPage.expandWorkbench();\n    await expect(settingsPage.editorCleanupSwitch).toBeVisible();\n  });\n\n  test('should show pipeline commands setting', async ({ settingsPage }) => {\n    await settingsPage.expandWorkbench();\n    await expect(settingsPage.pipelineCommandsText).toBeVisible();\n  });\n\n  test('should save editor cleanup when toggled', async ({ settingsPage, databasesPage }) => {\n    await settingsPage.expandWorkbench();\n    await expect(settingsPage.editorCleanupSwitch).toBeVisible();\n\n    const initialState = await settingsPage.isEditorCleanupEnabled();\n    await settingsPage.toggleEditorCleanup();\n    const toggledState = await settingsPage.isEditorCleanupEnabled();\n    expect(toggledState).toBe(!initialState);\n\n    await databasesPage.goto();\n    await settingsPage.goto();\n    await settingsPage.expandWorkbench();\n    const persistedState = await settingsPage.isEditorCleanupEnabled();\n    expect(persistedState).toBe(toggledState);\n\n    await settingsPage.toggleEditorCleanup();\n    const restoredState = await settingsPage.isEditorCleanupEnabled();\n    expect(restoredState).toBe(initialState);\n  });\n\n  test('should save pipeline commands when changed', async ({ settingsPage, databasesPage }) => {\n    await settingsPage.expandWorkbench();\n    await expect(settingsPage.pipelineCommandsValue).toBeVisible();\n\n    const initialValue = await settingsPage.getPipelineCommandsValue();\n    await settingsPage.setPipelineCommandsAndApply(2);\n    await expect(settingsPage.pipelineCommandsValue).toHaveText('2');\n\n    await databasesPage.goto();\n    await settingsPage.goto();\n    await settingsPage.expandWorkbench();\n    const persistedValue = await settingsPage.getPipelineCommandsValue();\n    expect(persistedValue.trim()).toBe('2');\n\n    const restoreValue = initialValue.trim() ? parseInt(initialValue, 10) : 5;\n    await settingsPage.setPipelineCommandsAndApply(restoreValue);\n    await expect(settingsPage.pipelineCommandsValue).toHaveText(String(restoreValue));\n  });\n});\n\ntest.describe('Workbench Settings take effect', () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    const config = StandaloneConfigFactory.build({\n      name: `test-wb-settings-${Date.now().toString(36)}`,\n    });\n    database = await apiHelper.createDatabase(config);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    if (database?.id) {\n      await apiHelper.deleteDatabase(database.id);\n    }\n  });\n\n  test('editor cleanup when enabled clears editor after running command', async ({ settingsPage, workbenchPage }) => {\n    await settingsPage.goto();\n    await settingsPage.expandWorkbench();\n    if (!(await settingsPage.isEditorCleanupEnabled())) {\n      await settingsPage.toggleEditorCleanup();\n    }\n\n    await workbenchPage.goto(database.id);\n    await workbenchPage.executeCommand('PING');\n\n    const editorContent = await workbenchPage.editor.getCommand();\n    expect(editorContent.trim()).toBe('');\n  });\n\n  test('editor cleanup when disabled keeps command in editor after run', async ({ settingsPage, workbenchPage }) => {\n    await settingsPage.goto();\n    await settingsPage.expandWorkbench();\n    if (await settingsPage.isEditorCleanupEnabled()) {\n      await settingsPage.toggleEditorCleanup();\n    }\n\n    await workbenchPage.goto(database.id);\n    await workbenchPage.executeCommand('PING');\n\n    const editorContent = await workbenchPage.editor.getCommand();\n    expect(editorContent.trim()).toContain('PING');\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/browser-integration/browser-integration.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory, StandaloneV7ConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory, IndexSchemaFieldFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_KEY_PATTERN = `test-vs-*-${uniqueId}*`;\nconst TEST_INDEX_PREFIX = `test-vs-browser-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-browser-${uniqueId}-idx`;\nconst TEST_INDEX_NAME_2 = `test-vs-browser-${uniqueId}-idx2`;\n\n/**\n * Vector Search > Browser Page Integration\n *\n * Tests for viewing index data from the Browser page,\n * navigating to create index, and browsing index data.\n *\n * Note: Some tests depend on RI-7944 (Make keys searchable from Browser page).\n */\ntest.describe('Vector Search > Browser Page Integration', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n  });\n\n  // Skip create-index onboarding for all tests\n  test.beforeEach(async ({ browserPage, page }) => {\n    await browserPage.goto(database.id);\n    await page.evaluate(() => localStorage.setItem('vectorSearchCreateIndexOnboarding', 'true'));\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n    await apiHelper.deleteKeysByPattern(database.id, TEST_KEY_PATTERN);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test('should show \"View index\" button for key indexed by a single index', async ({ browserPage, apiHelper }) => {\n    // Seed one index and a key matching its prefix\n    const singlePrefix = `test-vs-single-${uniqueId}:`;\n    const singleIndexName = `test-vs-single-${uniqueId}-idx`;\n    const singleKey = IndexHashKeyFactory.build({ keyName: `${singlePrefix}key1` });\n\n    const singleIndex = IndexConfigFactory.build({\n      indexName: singleIndexName,\n      prefix: singlePrefix,\n      schema: [IndexSchemaFieldFactory.build({ name: 'name', type: 'text' })],\n    });\n    await apiHelper.createIndex(database.id, singleIndex.indexName, singleIndex.prefix, singleIndex.schema);\n    await apiHelper.createHashKey(database.id, singleKey.keyName, singleKey.fields);\n\n    // Select the key and click \"View index\" to navigate to the Search tab\n    await browserPage.keyList.searchKeys(`${singlePrefix}key1`);\n    await browserPage.keyList.selectKeyInTree(`${singlePrefix}key1`);\n    await expect(browserPage.keyDetailsPanel).toBeVisible();\n\n    await expect(browserPage.viewIndexButton).toBeVisible();\n    await browserPage.viewIndexButton.click();\n\n    await expect(browserPage.navigationTabs.searchTab).toHaveAttribute('aria-selected', 'true');\n  });\n\n  test('should show \"View index\" dropdown for key indexed by multiple indexes', async ({ browserPage, apiHelper }) => {\n    // Create two indexes sharing the same prefix so one key is indexed by both\n    const indexConfig = IndexConfigFactory.build({ indexName: TEST_INDEX_NAME, prefix: TEST_INDEX_PREFIX });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n    const indexConfig2 = IndexConfigFactory.build({\n      indexName: TEST_INDEX_NAME_2,\n      prefix: TEST_INDEX_PREFIX,\n      schema: [IndexSchemaFieldFactory.build({ name: 'name', type: 'text' })],\n    });\n    await apiHelper.createIndex(database.id, indexConfig2.indexName, indexConfig2.prefix, indexConfig2.schema);\n\n    const hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key1` });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n\n    // Select the key → dropdown trigger shows with badge count \"2\"\n    await browserPage.keyList.searchKeys(`${TEST_INDEX_PREFIX}key1`);\n    await browserPage.keyList.selectKeyInTree(`${TEST_INDEX_PREFIX}key1`);\n    await expect(browserPage.keyDetailsPanel).toBeVisible();\n\n    await expect(browserPage.viewIndexMenuTrigger).toBeVisible();\n    await expect(browserPage.viewIndexCountBadge).toHaveText('2');\n\n    // Open the View index dropdown and pick one index to navigate\n    await browserPage.viewIndexMenuTrigger.click();\n\n    await expect(browserPage.getViewIndexMenuItem(TEST_INDEX_NAME)).toBeVisible();\n    await expect(browserPage.getViewIndexMenuItem(TEST_INDEX_NAME_2)).toBeVisible();\n\n    await browserPage.getViewIndexMenuItem(TEST_INDEX_NAME).click();\n    await expect(browserPage.navigationTabs.searchTab).toHaveAttribute('aria-selected', 'true');\n  });\n\n  test('should show \"Make searchable\" button for non-indexed key and create index', async ({\n    browserPage,\n    vectorSearchPage,\n    apiHelper,\n  }) => {\n    // Create a key with no matching index\n    const nonIndexedPrefix = `test-vs-nonindexed-${uniqueId}:`;\n    const nonIndexedKeyName = `${nonIndexedPrefix}key1`;\n    const hashKey = IndexHashKeyFactory.build({\n      keyName: nonIndexedKeyName,\n      fields: [{ field: 'title', value: faker.commerce.productName() }],\n    });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n\n    // Select key → \"Make searchable\" button should appear (key has no index)\n    await browserPage.keyList.searchKeys(nonIndexedKeyName);\n    await browserPage.keyList.selectKeyInTree(nonIndexedKeyName);\n    await expect(browserPage.keyDetailsPanel).toBeVisible();\n\n    await expect(browserPage.makeSearchableButton).toBeVisible();\n    await browserPage.makeSearchableButton.click();\n\n    // Walk through Make searchable modal → create index form → create index\n    await expect(browserPage.makeSearchableModal.heading).toBeVisible();\n    await browserPage.makeSearchableModal.continueButton.click();\n\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n\n  test('should show \"Index\" button on folder node and create index', async ({\n    browserPage,\n    vectorSearchPage,\n    apiHelper,\n  }) => {\n    // Create a key with no matching index\n    const indexablePrefix = `test-vs-indexable-${uniqueId}:`;\n    const indexableKeyName = `${indexablePrefix}key1`;\n    const hashKey = IndexHashKeyFactory.build({ keyName: indexableKeyName });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n\n    await browserPage.keyList.searchKeys(indexableKeyName);\n\n    // Hover folder node to reveal Index button, open modal, then create index\n    const folderName = indexablePrefix.slice(0, -1);\n    await browserPage.keyList.hoverFolderNode(folderName);\n\n    const indexButton = browserPage.getIndexFolderButton(folderName);\n    await expect(indexButton).toBeVisible();\n    await indexButton.click();\n\n    await expect(browserPage.makeSearchableModal.heading).toBeVisible();\n    await browserPage.makeSearchableModal.continueButton.click();\n\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n});\n\ntest.describe('Vector Search > Browser Page Integration > RQE Not Available', { tag: '@serial' }, () => {\n  let databaseNoModules: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    databaseNoModules = await apiHelper.createDatabase(StandaloneV7ConfigFactory.build());\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(databaseNoModules.id);\n  });\n\n  test('should show RQE not available when navigating to Search tab on Redis without search module', async ({\n    vectorSearchPage,\n  }) => {\n    await vectorSearchPage.goto(databaseNoModules.id);\n\n    await expect(vectorSearchPage.rqeNotAvailableWrapper).toBeVisible();\n    await expect(vectorSearchPage.rqeNotAvailable.container).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/create-index/existing-data.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport {\n  IndexConfigFactory,\n  IndexHashKeyFactory,\n  IndexJsonKeyFactory,\n  IndexSchemaFieldFactory,\n} from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `a-vs-existing-${uniqueId}:`;\nconst hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key1` });\nconst jsonKey = IndexJsonKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}json1` });\nconst TEST_HASH_KEY = hashKey.keyName;\nconst TEST_JSON_KEY = jsonKey.keyName;\nconst seedIndex = IndexConfigFactory.build();\n\n/**\n * Vector Search > Create Index - Existing Data\n *\n * Tests for creating an index from existing database keys,\n * including schema inference, field editing, and view toggling.\n * Each test completes the full flow: make changes → verify command view → create index.\n */\ntest.describe('Vector Search > Create Index - Existing Data', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n    await apiHelper.createJsonKey(database.id, jsonKey.keyName, JSON.stringify(jsonKey.value));\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId) || name === seedIndex.indexName);\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ vectorSearchPage, apiHelper, page }) => {\n    // Clean indexes and seed one, wait for index to be created\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId) || name === seedIndex.indexName);\n    await apiHelper.createIndex(database.id, seedIndex.indexName, seedIndex.prefix, seedIndex.schema);\n\n    await expect\n      .poll(() => apiHelper.getIndexes(database.id).then((indexes) => indexes.includes(seedIndex.indexName)))\n      .toBe(true);\n\n    await vectorSearchPage.goto(database.id);\n\n    // Skip onboarding\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchSelectKeyOnboarding', 'true');\n      localStorage.setItem('vectorSearchQueryOnboarding', 'true');\n      localStorage.setItem('vectorSearchCreateIndexOnboarding', 'true');\n    });\n\n    // Navigate to list page and open \"Use existing data\" form\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n    await vectorSearchPage.indexList.openCreateIndex('existing-data');\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n  });\n\n  test('should create index with default settings and navigate to query page', async ({ vectorSearchPage }) => {\n    // Select key → schema is auto-inferred\n    await vectorSearchPage.createIndexForm.selectKey(TEST_HASH_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Switch to command view\n    await vectorSearchPage.createIndexForm.commandViewButton.click();\n    await expect(vectorSearchPage.createIndexForm.commandView).toBeVisible();\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n\n  test('should edit key prefix and create index', async ({ vectorSearchPage }) => {\n    // Select key → schema is auto-inferred\n    await vectorSearchPage.createIndexForm.selectKey(TEST_HASH_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Override auto-detected prefix\n    const prefixInput = vectorSearchPage.createIndexForm.prefixInput;\n    await expect(prefixInput).toBeVisible();\n    await prefixInput.clear();\n    await prefixInput.fill('custom-prefix:');\n    await expect(prefixInput).toHaveValue('custom-prefix:');\n\n    // Switch to command view and verify prefix\n    await vectorSearchPage.createIndexForm.commandViewButton.click();\n    await expect(vectorSearchPage.createIndexForm.commandView).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.commandView).toContainText('custom-prefix:');\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n\n  test('should add field and create index', async ({ vectorSearchPage }) => {\n    // Select key → schema is auto-inferred\n    await vectorSearchPage.createIndexForm.selectKey(TEST_HASH_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Open add-field modal and add a new TEXT field\n    await vectorSearchPage.createIndexForm.addFieldButton.click();\n\n    const { fieldTypeModal } = vectorSearchPage.createIndexForm;\n    await expect(fieldTypeModal.fieldNameInput).toBeVisible();\n\n    const newFieldName = `extra-${faker.string.alphanumeric(4)}`;\n    await fieldTypeModal.fieldNameInput.fill(newFieldName);\n    await fieldTypeModal.saveButton.click();\n    await expect(fieldTypeModal.fieldNameInput).not.toBeVisible();\n\n    // Verify new field appears in command view, then create index\n    await vectorSearchPage.createIndexForm.commandViewButton.click();\n    await expect(vectorSearchPage.createIndexForm.commandView).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.commandView).toContainText(newFieldName);\n    await expect(vectorSearchPage.createIndexForm.commandView).toContainText('TEXT');\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n\n  test('should deselect a field row and exclude it from the index', async ({ vectorSearchPage }) => {\n    // Select key → schema is auto-inferred\n    await vectorSearchPage.createIndexForm.selectKey(TEST_HASH_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Deselect the first field row\n    const table = vectorSearchPage.createIndexForm.indexDetailsTable;\n    const rows = table.getByRole('row');\n    const checkboxes = table.getByRole('checkbox');\n\n    const initialCount = await checkboxes.count();\n    expect(initialCount).toBeGreaterThan(1);\n\n    const firstDataRow = rows.nth(1);\n    const deselectedFieldName = await firstDataRow.locator('td').nth(1).innerText();\n    const firstRowCheckbox = firstDataRow.getByRole('checkbox');\n    await firstRowCheckbox.uncheck();\n    await expect(firstRowCheckbox).not.toBeChecked();\n\n    // Verify deselected field is excluded from the command\n    await vectorSearchPage.createIndexForm.commandViewButton.click();\n    await expect(vectorSearchPage.createIndexForm.commandView).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.commandView).not.toContainText(deselectedFieldName);\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n\n  test('should change index name and create index', async ({ vectorSearchPage }) => {\n    // Select key → schema is auto-inferred\n    await vectorSearchPage.createIndexForm.selectKey(TEST_HASH_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Enter rename mode and set custom index name\n    await expect(vectorSearchPage.createIndexForm.indexNameDisplay).toBeVisible();\n    await vectorSearchPage.createIndexForm.indexNameEditButton.click();\n\n    const customName = `custom-idx-${faker.string.alphanumeric(6)}`;\n    await expect(vectorSearchPage.createIndexForm.indexNameInput).toBeVisible();\n    await vectorSearchPage.createIndexForm.indexNameInput.clear();\n    await vectorSearchPage.createIndexForm.indexNameInput.fill(customName);\n\n    await vectorSearchPage.createIndexForm.indexNameConfirmButton.click();\n    await expect(vectorSearchPage.createIndexForm.indexNameDisplay).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.indexNameDisplay).toContainText(customName);\n\n    // Verify custom name in command view\n    await vectorSearchPage.createIndexForm.commandViewButton.click();\n    await expect(vectorSearchPage.createIndexForm.commandView).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.commandView).toContainText(customName);\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n\n  test('should show duplicate index name validation and disable create button', async ({\n    vectorSearchPage,\n    apiHelper,\n  }) => {\n    // Create an index via API to establish a duplicate target\n    const indexConfig = IndexConfigFactory.build({\n      indexName: `dup-idx-${faker.string.alphanumeric(6)}`,\n      prefix: `${TEST_INDEX_PREFIX}dup:`,\n      schema: [IndexSchemaFieldFactory.build({ name: 'name', type: 'text' })],\n    });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n\n    // Re-navigate so the frontend fetches the updated index list\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    await vectorSearchPage.indexList.openCreateIndex('existing-data');\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n\n    // Select key\n    await vectorSearchPage.createIndexForm.selectKey(TEST_HASH_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Enter duplicate name and verify confirm is disabled\n    await vectorSearchPage.createIndexForm.indexNameEditButton.click();\n    await expect(vectorSearchPage.createIndexForm.indexNameInput).toBeVisible();\n\n    await vectorSearchPage.createIndexForm.indexNameInput.clear();\n    await vectorSearchPage.createIndexForm.indexNameInput.fill(indexConfig.indexName);\n\n    await expect(vectorSearchPage.createIndexForm.indexNameConfirmButton).toBeDisabled();\n\n    // Cancel to close the rename input\n    await vectorSearchPage.createIndexForm.indexNameCancelButton.click();\n    await expect(vectorSearchPage.createIndexForm.indexNameDisplay).toBeVisible();\n  });\n\n  test('should create index from JSON key and navigate to query page', async ({ vectorSearchPage }) => {\n    // Switch to JSON tab so the browser panel shows JSON keys\n    await vectorSearchPage.createIndexForm.switchKeyTypeTab('JSON');\n    await vectorSearchPage.createIndexForm.selectKey(TEST_JSON_KEY);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Verify the generated command uses \"ON JSON\" data type\n    await vectorSearchPage.createIndexForm.commandViewButton.click();\n    await expect(vectorSearchPage.createIndexForm.commandView).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.commandView).toContainText('ON JSON');\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/create-index/onboarding.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { CreateIndexOnboarding } from 'e2eSrc/pages/vector-search/components/CreateIndexOnboarding';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `a-vs-onboard-${uniqueId}`;\nconst TEST_KEY_NAME = `${TEST_INDEX_PREFIX}:key1`;\nconst seedIndex = IndexConfigFactory.build();\n\n/**\n * Vector Search > Select Key Onboarding\n *\n * The \"Select a key to get started\" popover appears when the browser panel\n * opens for the first time. It is dismissed via \"Got it\" or automatically\n * when a key is selected.\n */\ntest.describe('Vector Search > Select Key Onboarding', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}:*`);\n    const hashKey = IndexHashKeyFactory.build({ keyName: TEST_KEY_NAME });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId) || name === seedIndex.indexName);\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}:*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ apiHelper, vectorSearchPage, page }) => {\n    await apiHelper.createIndex(database.id, seedIndex.indexName, seedIndex.prefix, seedIndex.schema);\n\n    // Navigate to the app so localStorage operations target the correct origin\n    await vectorSearchPage.goto(database.id);\n\n    // Reset onboarding state so the popover appears\n    await page.evaluate(() => {\n      localStorage.removeItem('vectorSearchSelectKeyOnboarding');\n      localStorage.removeItem('vectorSearchCreateIndexOnboarding');\n    });\n  });\n\n  test('should show select key onboarding and dismiss on \"Got it\"', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.navigateToCreateIndex();\n\n    await expect(vectorSearchPage.createIndexForm.selectKeyOnboarding).toBeVisible();\n    await vectorSearchPage.createIndexForm.selectKeyOnboardingDismiss.click();\n\n    await expect(vectorSearchPage.createIndexForm.selectKeyOnboarding).not.toBeVisible();\n  });\n\n  test('should not show select key onboarding on subsequent visit', async ({ vectorSearchPage, page }) => {\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchSelectKeyOnboarding', 'true');\n    });\n\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.navigateToCreateIndex();\n\n    await expect(vectorSearchPage.createIndexForm.browserPanel).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.selectKeyOnboarding).not.toBeVisible();\n  });\n});\n\n/**\n * Vector Search > Create Index - Onboarding\n *\n * Tests for the create index onboarding flow.\n * The onboarding starts after the user selects a key from the browser panel,\n * which triggers field inference and shows guided popovers through the form steps:\n * DefineIndex → IndexPrefix → FieldName → SampleValue → IndexingType → CommandView\n */\ntest.describe('Vector Search > Create Index - Onboarding', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}:*`);\n    const hashKey = IndexHashKeyFactory.build({ keyName: TEST_KEY_NAME });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId) || name === seedIndex.indexName);\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}:*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ page, apiHelper, vectorSearchPage }) => {\n    // Seed index\n    await apiHelper.createIndex(database.id, seedIndex.indexName, seedIndex.prefix, seedIndex.schema);\n\n    // Navigate to the app so localStorage operations target the correct origin\n    await vectorSearchPage.goto(database.id);\n\n    // Reset create-index onboarding, skip select-key onboarding\n    await page.evaluate(() => {\n      localStorage.removeItem('vectorSearchCreateIndexOnboarding');\n      localStorage.setItem('vectorSearchSelectKeyOnboarding', 'true');\n    });\n  });\n\n  test('should complete onboarding flow through all steps', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.navigateToCreateIndex();\n    await vectorSearchPage.createIndexForm.selectKey(TEST_KEY_NAME);\n\n    const { createIndexOnboarding } = vectorSearchPage;\n    await expect(createIndexOnboarding.stepPopover('defineIndex')).toBeVisible();\n\n    // Walk through all onboarding steps (DefineIndex → IndexPrefix → FieldName → SampleValue → IndexingType → CommandView)\n    for (const step of CreateIndexOnboarding.STEPS) {\n      await expect(createIndexOnboarding.stepAction(step)).toBeVisible();\n      await createIndexOnboarding.stepAction(step).click();\n    }\n\n    await expect(createIndexOnboarding.popover).not.toBeVisible();\n  });\n\n  test('should skip onboarding', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.navigateToCreateIndex();\n    await vectorSearchPage.createIndexForm.selectKey(TEST_KEY_NAME);\n\n    const { createIndexOnboarding } = vectorSearchPage;\n    await expect(createIndexOnboarding.stepPopover('defineIndex')).toBeVisible();\n    await createIndexOnboarding.skipButton.click();\n\n    await expect(createIndexOnboarding.popover).not.toBeVisible();\n  });\n\n  test('should not show onboarding after completion', async ({ vectorSearchPage, page }) => {\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchCreateIndexOnboarding', 'true');\n    });\n\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.navigateToCreateIndex();\n    await vectorSearchPage.createIndexForm.selectKey(TEST_KEY_NAME);\n\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n    await expect(vectorSearchPage.createIndexOnboarding.popover).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/create-index/sample-data.spec.ts",
    "content": "import { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst seedIndex = IndexConfigFactory.build();\n\nconst SAMPLE_DATASETS = [\n  { id: 'e-commerce-discovery', label: 'E-Commerce Discovery (Bikes)', expectedQueryCount: 4 },\n  { id: 'content-recommendations', label: 'Content Recommendations (Movies)', expectedQueryCount: 5 },\n] as const;\n\n/**\n * Vector Search > Create Index - Sample Data\n *\n * Tests for creating an index from sample data,\n * including \"Start querying\" and \"See index definition\" flows.\n *\n * Each test cleans up indexes and creates a seed so the list page appears.\n */\ntest.describe('Vector Search > Create Index - Sample Data', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ apiHelper }) => {\n    // Clean known indexes and seed one so the list page appears\n    const KNOWN_INDEXES = [seedIndex.indexName, 'idx:bikes_vss', 'idx:movies_vss'];\n    await apiHelper.deleteAllIndexes(database.id, (name) => KNOWN_INDEXES.includes(name));\n    await apiHelper.createIndex(database.id, seedIndex.indexName, seedIndex.prefix, seedIndex.schema);\n  });\n\n  test('should close the sample data modal and return to list page', async ({ vectorSearchPage }) => {\n    // Navigate to list page and open sample data modal\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.openSampleDataModal();\n\n    // Close modal and verify list page is visible\n    await vectorSearchPage.pickSampleDataModal.closeButton.click();\n    await expect(vectorSearchPage.pickSampleDataModal.heading).not.toBeVisible();\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n  });\n\n  test('should cancel index creation from \"See index definition\" and return to list page', async ({\n    vectorSearchPage,\n  }) => {\n    // Navigate to list page and open sample data modal\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.openSampleDataModal();\n\n    // Pick sample dataset and see index definition\n    await vectorSearchPage.pickSampleDataModal.getSampleDataOption('e-commerce-discovery').click();\n    await vectorSearchPage.pickSampleDataModal.seeIndexDefinitionButton.click();\n\n    await expect(vectorSearchPage.createIndexWrapper).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n\n    // Cancel index creation and verify list page is visible\n    await vectorSearchPage.createIndexForm.cancelButton.click();\n    await expect(vectorSearchPage.createIndexWrapper).not.toBeVisible();\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n  });\n\n  for (const { id: dataset, label, expectedQueryCount } of SAMPLE_DATASETS) {\n    test.describe(label, () => {\n      test('should create index via \"Start querying\" and verify query library is seeded', async ({\n        vectorSearchPage,\n      }) => {\n        // Navigate to list page and open sample data modal\n        await vectorSearchPage.goto(database.id);\n        await vectorSearchPage.openSampleDataModal();\n\n        // Pick sample dataset and start querying\n        await vectorSearchPage.pickSampleDataModal.getSampleDataOption(dataset).click();\n        await vectorSearchPage.pickSampleDataModal.startQueryingButton.click();\n        await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n\n        // Verify toast and query library is seeded\n        await expect(vectorSearchPage.sampleDataToast).toBeVisible();\n        await expect(vectorSearchPage.queryLibrary.container).toBeVisible();\n        await expect(vectorSearchPage.queryLibrary.allItems).toHaveCount(expectedQueryCount);\n      });\n\n      test('should create index via \"See index definition\" and verify toast', async ({ vectorSearchPage }) => {\n        // Navigate to list page and open sample data modal\n        await vectorSearchPage.goto(database.id);\n        await vectorSearchPage.openSampleDataModal();\n\n        // Pick sample dataset and see index definition\n        await vectorSearchPage.pickSampleDataModal.getSampleDataOption(dataset).click();\n        await vectorSearchPage.pickSampleDataModal.seeIndexDefinitionButton.click();\n\n        // Verify create index form is visible\n        await expect(vectorSearchPage.createIndexWrapper).toBeVisible();\n        await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n        await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n        // Create index and navigate to query page, verify toast\n        await vectorSearchPage.createIndexForm.createIndexButton.click();\n        await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n\n        // Verify toast and query library is seeded\n        await expect(vectorSearchPage.sampleDataToast).toBeVisible();\n        await expect(vectorSearchPage.queryLibrary.container).toBeVisible();\n        await expect(vectorSearchPage.queryLibrary.allItems).toHaveCount(expectedQueryCount);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/list-indexes/create-index.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory, StandaloneEmptyConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `a-vs-create-${uniqueId}:`;\nconst SAMPLE_INDEXES = ['idx:bikes_vss', 'idx:movies_vss'];\nconst seedIndex = IndexConfigFactory.build();\n\n/**\n * Vector Search > Create Index from List Page\n *\n * Tests for creating indexes via the \"+ Create search index\" menu\n * on the list page, including sample data flow, existing data flow,\n * and disabled state when no hash/JSON keys exist.\n */\ntest.describe('Vector Search > Create Index from List Page', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    for (let i = 1; i <= 3; i++) {\n      const hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key${i}` });\n      await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n    }\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(\n      database.id,\n      (name) => name.includes(uniqueId) || name === seedIndex.indexName || SAMPLE_INDEXES.includes(name),\n    );\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ vectorSearchPage, apiHelper, page }) => {\n    // Clean indexes and seed one (so the list page appears)\n    await apiHelper.deleteAllIndexes(\n      database.id,\n      (name) => name.includes(uniqueId) || name === seedIndex.indexName || SAMPLE_INDEXES.includes(name),\n    );\n    await apiHelper.createIndex(database.id, seedIndex.indexName, seedIndex.prefix, seedIndex.schema);\n\n    await expect\n      .poll(() => apiHelper.getIndexes(database.id).then((indexes) => indexes.includes(seedIndex.indexName)))\n      .toBe(true);\n\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    // Skip onboarding (must be after navigation so localStorage targets the app origin)\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchSelectKeyOnboarding', 'true');\n      localStorage.setItem('vectorSearchQueryOnboarding', 'true');\n      localStorage.setItem('vectorSearchCreateIndexOnboarding', 'true');\n    });\n  });\n\n  test('should open sample data modal and complete \"Start querying\" flow', async ({ vectorSearchPage }) => {\n    // Open create index menu → sample data → modal\n    await vectorSearchPage.indexList.createIndexButton.click();\n\n    const sampleDataItem = vectorSearchPage.indexList.getCreateIndexMenuItem('sample-data');\n    await expect(sampleDataItem).toBeVisible();\n    await sampleDataItem.click();\n\n    await expect(vectorSearchPage.pickSampleDataModal.heading).toBeVisible();\n\n    // Pick sample dataset and start querying → navigate to query page\n    await vectorSearchPage.pickSampleDataModal.getSampleDataOption('e-commerce-discovery').click();\n    await vectorSearchPage.pickSampleDataModal.startQueryingButton.click();\n\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.sampleDataToast).toBeVisible();\n    await expect(vectorSearchPage.queryEditor.container).toBeVisible();\n    await expect(vectorSearchPage.queryLibrary.allItems).toHaveCount(4);\n  });\n\n  test('should open sample data modal and navigate to \"See index definition\"', async ({ vectorSearchPage }) => {\n    // Open create index menu → sample data → modal\n    await vectorSearchPage.indexList.openCreateIndex('sample-data');\n    await expect(vectorSearchPage.pickSampleDataModal.heading).toBeVisible();\n\n    // Pick sample dataset and see index definition\n    await vectorSearchPage.pickSampleDataModal.getSampleDataOption('content-recommendations').click();\n    await vectorSearchPage.pickSampleDataModal.seeIndexDefinitionButton.click();\n\n    // Verify create index form is visible\n    await expect(vectorSearchPage.createIndexWrapper).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.container).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n\n    await expect(vectorSearchPage.sampleDataToast).toBeVisible();\n  });\n\n  test('should create index from existing data via list page menu', async ({ vectorSearchPage }) => {\n    // Open create index menu → Use existing data\n    await vectorSearchPage.indexList.createIndexButton.click();\n\n    const existingDataItem = vectorSearchPage.indexList.getCreateIndexMenuItem('existing-data');\n    await expect(existingDataItem).toBeEnabled();\n    await existingDataItem.click();\n\n    await expect(vectorSearchPage.createIndexWrapper).toBeVisible();\n    await expect(vectorSearchPage.createIndexForm.browserPanel).toBeVisible();\n\n    // Select key in browser panel and create index\n    await vectorSearchPage.createIndexForm.selectKey(`${TEST_INDEX_PREFIX}key1`);\n    await expect(vectorSearchPage.createIndexForm.content).toBeVisible();\n\n    // Create index and navigate to query page, verify toast\n    await vectorSearchPage.createIndexForm.createIndexButton.click();\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.indexCreatedToast).toBeVisible();\n  });\n});\n\ntest.describe('Vector Search > Create Index from List Page - No Hash/JSON Keys', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n  const emptyIndex = IndexConfigFactory.build();\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneEmptyConfigFactory.build());\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteIndex(database.id, emptyIndex.indexName);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test('should disable \"Use existing data\" when no hash or JSON keys exist', async ({\n    vectorSearchPage,\n    apiHelper,\n  }) => {\n    // FLUSHDB leaves no hash/JSON keys → \"Use existing data\" should be disabled\n    await apiHelper.sendCommand(database.id, 'FLUSHDB');\n    await apiHelper.createIndex(database.id, emptyIndex.indexName, emptyIndex.prefix, emptyIndex.schema);\n\n    await expect\n      .poll(() => apiHelper.getIndexes(database.id).then((indexes) => indexes.includes(emptyIndex.indexName)))\n      .toBe(true);\n\n    // Navigate to list page\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    // Open create index menu → \"Use existing data\" should be disabled\n    await vectorSearchPage.indexList.createIndexButton.click();\n\n    const existingDataItem = vectorSearchPage.indexList.getCreateIndexMenuItem('existing-data');\n    await expect(existingDataItem).toBeVisible();\n    await expect(existingDataItem).toHaveAttribute('data-disabled', '');\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/list-indexes/list-indexes.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `test-vs-list-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-list-${uniqueId}-idx`;\n\n/**\n * Vector Search > List Indexes\n *\n * Tests for viewing, querying, browsing, inspecting, and deleting\n * indexes from the index list page.\n */\ntest.describe('Vector Search > List Indexes', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    for (let i = 1; i <= 3; i++) {\n      const hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key${i}` });\n      await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n    }\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ vectorSearchPage, apiHelper }) => {\n    // Seed index and poll until available\n    const indexConfig = IndexConfigFactory.build({\n      indexName: TEST_INDEX_NAME,\n      prefix: TEST_INDEX_PREFIX,\n    });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n\n    await expect\n      .poll(() => apiHelper.getIndexes(database.id).then((indexes) => indexes.includes(TEST_INDEX_NAME)))\n      .toBe(true);\n\n    // Navigate to list page\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n  });\n\n  test('should display indexes table with index name and create index button', async ({ vectorSearchPage }) => {\n    await expect(vectorSearchPage.indexList.table).toBeVisible();\n    await expect(vectorSearchPage.indexList.createIndexButton).toBeVisible();\n\n    const indexName = vectorSearchPage.indexList.getIndexName(TEST_INDEX_NAME);\n    await expect(indexName).toBeVisible();\n    await expect(indexName).toHaveText(TEST_INDEX_NAME);\n  });\n\n  test('should navigate to query page when Query button is clicked', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.indexList.openQuery(TEST_INDEX_NAME);\n\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.queryEditor.container).toBeVisible();\n  });\n\n  test('should navigate to browser page when Browse dataset action is clicked', async ({\n    vectorSearchPage,\n    browserPage,\n  }) => {\n    // Open actions menu and select Browse dataset\n    const actionsMenuTrigger = vectorSearchPage.indexList.getActionsMenuTrigger(TEST_INDEX_NAME);\n    await actionsMenuTrigger.click();\n\n    const browseItem = vectorSearchPage.indexList.getActionMenuItem('Browse dataset');\n    await expect(browseItem).toBeVisible();\n    await browseItem.click();\n\n    // Verify browser tab is active and shows index keys\n    await expect(vectorSearchPage.navigationTabs.browseTab).toHaveAttribute('aria-selected', 'true');\n    await expect(browserPage.keyList.indexSelector).toContainText(TEST_INDEX_NAME);\n    await expect(browserPage.keyList.resultsCount).toContainText('Results: 3');\n  });\n\n  test('should open index details side panel via View index action', async ({ vectorSearchPage }) => {\n    // Open actions menu and select View index\n    const actionsMenuTrigger = vectorSearchPage.indexList.getActionsMenuTrigger(TEST_INDEX_NAME);\n    await actionsMenuTrigger.click();\n\n    const viewItem = vectorSearchPage.indexList.getActionMenuItem('View index');\n    await expect(viewItem).toBeVisible();\n    await viewItem.click();\n\n    await expect(vectorSearchPage.indexInfoPanel.container).toBeVisible();\n    await expect(vectorSearchPage.indexInfoPanel.title).toBeVisible();\n\n    // Close panel and verify it hides\n    await vectorSearchPage.indexInfoPanel.closeButton.click();\n    await expect(vectorSearchPage.indexInfoPanel.container).not.toBeVisible();\n  });\n\n  test('should delete index with confirmation', async ({ vectorSearchPage }) => {\n    // Open actions menu and select Delete\n    const actionsMenuTrigger = vectorSearchPage.indexList.getActionsMenuTrigger(TEST_INDEX_NAME);\n    await actionsMenuTrigger.click();\n\n    const deleteItem = vectorSearchPage.indexList.getActionMenuItem('Delete');\n    await expect(deleteItem).toBeVisible();\n    await deleteItem.click();\n\n    // Confirm deletion and verify toast\n    await expect(vectorSearchPage.deleteIndexModal.dialog).toBeVisible();\n    await expect(vectorSearchPage.deleteIndexModal.confirmButton).toBeVisible();\n    await vectorSearchPage.deleteIndexModal.confirmButton.click();\n\n    await expect(vectorSearchPage.deleteIndexModal.dialog).not.toBeVisible();\n    await expect(vectorSearchPage.indexDeletedToast).toBeVisible();\n\n    await expect(vectorSearchPage.indexList.getIndexName(TEST_INDEX_NAME)).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/navigation/navigation.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport {\n  StandaloneConfigFactory,\n  StandaloneEmptyConfigFactory,\n  StandaloneV7ConfigFactory,\n} from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `test-vs-nav-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-nav-${uniqueId}-idx`;\n\n/**\n * Vector Search > Navigation and RQE Availability\n *\n * Tests for navigating to Vector Search and verifying the\n * correct screen is shown based on Redis capabilities.\n *\n * Each test creates its own database to avoid interfering\n * with parallel tests' FT indexes.\n */\ntest.describe('Vector Search > Navigation and RQE Availability', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test('should show welcome screen when no indexes exist', async ({ vectorSearchPage, apiHelper }) => {\n    // Guarantee a completely empty Redis: no keys, no indexes\n    database = await apiHelper.createDatabase(StandaloneEmptyConfigFactory.build());\n    await apiHelper.sendCommand(database.id, 'FLUSHDB');\n    await apiHelper.deleteAllIndexes(database.id);\n\n    // Navigate to search page and verify welcome screen is visible\n    await vectorSearchPage.goto(database.id);\n\n    await expect(vectorSearchPage.navigationTabs.searchTab).toBeVisible();\n    await expect(vectorSearchPage.navigationTabs.searchTab).toHaveAttribute('aria-selected', 'true');\n    await expect(vectorSearchPage.welcomeWrapper).toBeVisible();\n  });\n\n  test('should show list screen when indexes exist', async ({ vectorSearchPage, apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    // Seed index\n    const indexConfig = IndexConfigFactory.build({ indexName: TEST_INDEX_NAME, prefix: TEST_INDEX_PREFIX });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n\n    // Navigate to list page and verify welcome screen is not visible\n    await vectorSearchPage.goto(database.id);\n\n    await expect(vectorSearchPage.navigationTabs.searchTab).toBeVisible();\n    await expect(vectorSearchPage.navigationTabs.searchTab).toHaveAttribute('aria-selected', 'true');\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n  });\n});\n\ntest.describe('Vector Search > RQE Not Available', { tag: '@serial' }, () => {\n  let databaseNoModules: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    databaseNoModules = await apiHelper.createDatabase(StandaloneV7ConfigFactory.build());\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteDatabase(databaseNoModules.id);\n  });\n\n  test('should show RQE not available screen for Redis without search module', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.goto(databaseNoModules.id);\n\n    await expect(vectorSearchPage.rqeNotAvailableWrapper).toBeVisible();\n    await expect(vectorSearchPage.rqeNotAvailable.container).toBeVisible();\n    await expect(vectorSearchPage.rqeNotAvailable.title).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/query/query-editor.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `test-vs-query-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-query-${uniqueId}-idx`;\n\n/**\n * Vector Search > Query Page\n *\n * Tests for running queries, viewing results,\n * and result card actions (re-run, expand/collapse, delete, clear all).\n */\ntest.describe('Vector Search > Query Page', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n  let testQuery: string;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    for (let i = 1; i <= 5; i++) {\n      const hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key${i}` });\n      await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n    }\n\n    testQuery = `FT.SEARCH ${TEST_INDEX_NAME} *`;\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ vectorSearchPage, apiHelper, page }) => {\n    // Clear server-side command history to ensure clean results\n    await apiHelper.deleteCommandExecutions(database.id);\n\n    // Seed index (guard against parallel suites deleting it — FT indexes are server-global)\n    const indexConfig = IndexConfigFactory.build({ indexName: TEST_INDEX_NAME, prefix: TEST_INDEX_PREFIX });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    // Skip onboarding\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchQueryOnboarding', 'true');\n    });\n\n    // Navigate to query page and type the test query\n    await vectorSearchPage.indexList.openQuery(TEST_INDEX_NAME);\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n\n    await vectorSearchPage.queryEditor.typeQuery(testQuery);\n  });\n\n  test('should run query and view results', async ({ vectorSearchPage }) => {\n    // Empty results shown before running the query\n    await expect(vectorSearchPage.queryEditor.container).toBeVisible();\n    await expect(vectorSearchPage.queryResults.noResults).toBeVisible();\n\n    await vectorSearchPage.queryEditor.runButton.click();\n\n    // Result card appears, empty state disappears\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n    await expect(vectorSearchPage.queryResults.noResults).not.toBeVisible();\n  });\n\n  test('should expand and collapse query result card', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.queryEditor.runButton.click();\n\n    const toggleButton = vectorSearchPage.queryResults.firstCardToggleCollapseButton;\n    const resultBody = vectorSearchPage.queryResults.firstCardResult;\n    await expect(resultBody).toBeVisible();\n\n    // Collapse the card\n    await toggleButton.click();\n    await expect(resultBody).not.toBeVisible();\n\n    // Expand it back\n    await toggleButton.click();\n    await expect(resultBody).toBeVisible();\n  });\n\n  test('should re-run query from result card', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.queryEditor.runButton.click();\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n\n    // Re-run the same query from the result card action\n    const reRunButton = vectorSearchPage.queryResults.firstCardReRunButton;\n    await expect(reRunButton).toBeVisible();\n    await reRunButton.click();\n\n    // Verify a new result card appeared with the same query\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n    await expect(vectorSearchPage.queryResults.firstCardCommand).toContainText(testQuery);\n  });\n\n  test('should delete individual result card', async ({ vectorSearchPage }) => {\n    // Run query twice to generate two result cards\n    await vectorSearchPage.queryEditor.runButton.click();\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n    await vectorSearchPage.queryEditor.runButton.click();\n\n    const cards = vectorSearchPage.queryResults.container.getByTestId('query-card-open');\n    await expect(cards).toHaveCount(2);\n\n    // Delete the first card and verify count decreases\n    await vectorSearchPage.queryResults.firstCardDeleteButton.click();\n    await expect(cards).toHaveCount(1);\n  });\n\n  test('should clear all results', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.queryEditor.runButton.click();\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n\n    await expect(vectorSearchPage.queryResults.clearResultsButton).toBeVisible();\n    await vectorSearchPage.queryResults.clearResultsButton.click();\n\n    const cards = vectorSearchPage.queryResults.container.getByTestId('query-card-open');\n    await expect(cards).toHaveCount(0);\n    await expect(vectorSearchPage.queryResults.noResults).toBeVisible();\n  });\n\n  test('should disable explain and profile buttons when editor is empty', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.queryEditor.clearQuery();\n\n    await expect(vectorSearchPage.queryEditor.explainButton).toBeDisabled();\n    await expect(vectorSearchPage.queryEditor.profileButton).toBeDisabled();\n\n    // Verify tooltip explains why the button is disabled\n    await vectorSearchPage.queryEditor.explainButton.hover();\n    await expect(vectorSearchPage.queryEditor.explainTooltip).toContainText('Disabled: no query identified.');\n  });\n\n  test('should disable explain and profile buttons for non-FT query', async ({ vectorSearchPage }) => {\n    // Non-FT commands (e.g. GET) can't be explained or profiled\n    await vectorSearchPage.queryEditor.typeQuery('GET somekey');\n\n    await expect(vectorSearchPage.queryEditor.explainButton).toBeDisabled();\n    await expect(vectorSearchPage.queryEditor.profileButton).toBeDisabled();\n\n    await vectorSearchPage.queryEditor.profileButton.hover();\n    await expect(vectorSearchPage.queryEditor.profileTooltip).toContainText('Disabled: no query identified.');\n  });\n\n  test('should disable save button when editor is empty', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.queryEditor.clearQuery();\n\n    await expect(vectorSearchPage.queryEditor.saveButton).toBeDisabled();\n  });\n\n  test('should execute explain query action', async ({ vectorSearchPage }) => {\n    await expect(vectorSearchPage.queryEditor.container).toBeVisible();\n\n    await vectorSearchPage.queryEditor.explainButton.click();\n\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n  });\n\n  test('should execute profile query action', async ({ vectorSearchPage }) => {\n    await expect(vectorSearchPage.queryEditor.container).toBeVisible();\n\n    await vectorSearchPage.queryEditor.profileButton.click();\n\n    await expect(vectorSearchPage.queryResults.firstCardHeader).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/query/query-library.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `test-vs-lib-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-lib-${uniqueId}-idx`;\nconst TEST_QUERY = '* => [KNN 10 @vec $BLOB]';\n\ntest.describe('Vector Search > Query Library', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    const hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key1` });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n    await apiHelper.deleteAllSavedQueries(database.id, TEST_INDEX_NAME);\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ vectorSearchPage, apiHelper, page }) => {\n    // Seed index and clear saved queries\n    const indexConfig = IndexConfigFactory.build({ indexName: TEST_INDEX_NAME, prefix: TEST_INDEX_PREFIX });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n    await apiHelper.deleteAllSavedQueries(database.id, TEST_INDEX_NAME);\n\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    // Skip onboarding\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchQueryOnboarding', 'true');\n    });\n\n    await vectorSearchPage.indexList.openQuery(TEST_INDEX_NAME);\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n  });\n\n  test('should search and filter saved queries in the library', async ({ vectorSearchPage, apiHelper }) => {\n    // Seed 3 queries via API so the library has data to filter\n    const targetName = `unique-target-${faker.string.alphanumeric(6)}`;\n    await apiHelper.createSavedQuery(database.id, TEST_INDEX_NAME, targetName, TEST_QUERY);\n    await apiHelper.createSavedQuery(\n      database.id,\n      TEST_INDEX_NAME,\n      `other-${faker.string.alphanumeric(6)}`,\n      '@name:hello',\n    );\n    await apiHelper.createSavedQuery(\n      database.id,\n      TEST_INDEX_NAME,\n      `another-${faker.string.alphanumeric(6)}`,\n      '@description:world',\n    );\n\n    await vectorSearchPage.selectQueryLibraryTab();\n\n    await expect(vectorSearchPage.queryLibrary.allItems).toHaveCount(3);\n\n    // Filter by name → only the matching query remains\n    await vectorSearchPage.queryLibrary.searchInput.fill(targetName);\n\n    await expect(vectorSearchPage.queryLibrary.allItems).toHaveCount(1);\n    await expect(vectorSearchPage.queryLibrary.getItemByName(targetName)).toBeVisible();\n  });\n\n  test('should expand and collapse a query library item', async ({ vectorSearchPage, apiHelper }) => {\n    // Seed query via API so the library has data to expand/collapse\n    const queryName = `test-expand-${faker.string.alphanumeric(6)}`;\n    const savedQuery = await apiHelper.createSavedQuery(database.id, TEST_INDEX_NAME, queryName, TEST_QUERY);\n\n    // Select query library tab and verify query is visible\n    await vectorSearchPage.selectQueryLibraryTab();\n\n    const header = vectorSearchPage.queryLibrary.getItemHeader(savedQuery.id);\n    const body = vectorSearchPage.queryLibrary.getItemBody(savedQuery.id);\n\n    await expect(header).toBeVisible();\n    await expect(body).not.toBeVisible();\n\n    // Expand: click header to show query body\n    await header.click();\n    await expect(body).toBeVisible();\n    await expect(body).toContainText(TEST_QUERY);\n\n    // Collapse: click header again to hide body\n    await header.click();\n    await expect(body).not.toBeVisible();\n  });\n\n  test('should run query from library', async ({ vectorSearchPage, apiHelper }) => {\n    // Seed query via API so the library has data to run\n    const queryName = `test-run-${faker.string.alphanumeric(6)}`;\n    const savedQuery = await apiHelper.createSavedQuery(database.id, TEST_INDEX_NAME, queryName, TEST_QUERY);\n\n    // Select query library tab and verify query is visible\n    await vectorSearchPage.selectQueryLibraryTab();\n\n    // Click run button and verify result card is visible\n    const runButton = vectorSearchPage.queryLibrary.getItemRunButton(savedQuery.id);\n    await expect(runButton).toBeVisible();\n    await runButton.click();\n\n    await expect(vectorSearchPage.queryResults.firstCardCommand).toBeVisible();\n    await expect(vectorSearchPage.queryResults.firstCardCommand).toContainText(TEST_QUERY);\n  });\n\n  test('should load query into editor from library', async ({ vectorSearchPage, apiHelper }) => {\n    // Seed query via API so the library has data to load\n    const queryName = `test-load-${faker.string.alphanumeric(6)}`;\n    const savedQuery = await apiHelper.createSavedQuery(database.id, TEST_INDEX_NAME, queryName, TEST_QUERY);\n\n    // Select query library tab and verify query is visible\n    await vectorSearchPage.selectQueryLibraryTab();\n\n    // Click load button and verify editor is visible and query is loaded\n    const loadButton = vectorSearchPage.queryLibrary.getItemLoadButton(savedQuery.id);\n    await expect(loadButton).toBeVisible();\n    await loadButton.click();\n\n    await expect(vectorSearchPage.queryEditor.container).toBeVisible();\n    await expect(vectorSearchPage.queryEditor.textbox).toHaveValue(TEST_QUERY);\n  });\n\n  test('should delete query from library and show notification', async ({ vectorSearchPage, apiHelper }) => {\n    // Seed query via API so the library has data to delete\n    const queryName = `test-delete-${faker.string.alphanumeric(6)}`;\n    const savedQuery = await apiHelper.createSavedQuery(database.id, TEST_INDEX_NAME, queryName, TEST_QUERY);\n\n    // Select query library tab and verify query is visible\n    await vectorSearchPage.selectQueryLibraryTab();\n\n    const deleteButton = vectorSearchPage.queryLibrary.getItemDeleteButton(savedQuery.id);\n    await expect(deleteButton).toBeVisible();\n    await deleteButton.click();\n\n    // Confirm deletion in modal and verify success toast\n    await expect(vectorSearchPage.deleteQueryModal.confirmButton).toBeVisible();\n    await vectorSearchPage.deleteQueryModal.confirmButton.click();\n\n    await expect(vectorSearchPage.queryLibrary.deleteSuccessToast).toBeVisible();\n    await expect(vectorSearchPage.queryLibrary.getItem(savedQuery.id)).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/query/query-onboarding.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `test-vs-qonboard-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-qonboard-${uniqueId}-idx`;\n\n/**\n * Vector Search > Query Page Onboarding\n *\n * The \"Index created successfully\" popover appears the first time the\n * query page is visited. It introduces the Query editor and Query library\n * tabs. Dismissed via the \"Got it\" button, it does not appear on subsequent visits.\n */\ntest.describe('Vector Search > Query Page Onboarding', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    const hashKey = IndexHashKeyFactory.build({\n      keyName: `${TEST_INDEX_PREFIX}key1`,\n      fields: [{ field: 'name', value: faker.commerce.productName() }],\n    });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ apiHelper, vectorSearchPage, page }) => {\n    const indexConfig = IndexConfigFactory.build({ indexName: TEST_INDEX_NAME, prefix: TEST_INDEX_PREFIX });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n\n    // Navigate to the app so localStorage operations target the correct origin\n    await vectorSearchPage.goto(database.id);\n\n    // Reset query onboarding state so the popover appears\n    await page.evaluate(() => {\n      localStorage.removeItem('vectorSearchQueryOnboarding');\n    });\n  });\n\n  test('should show query onboarding and dismiss on \"Got it\"', async ({ vectorSearchPage }) => {\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    await vectorSearchPage.indexList.openQuery(TEST_INDEX_NAME);\n\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.queryEditor.queryOnboarding).toBeVisible();\n\n    await vectorSearchPage.queryEditor.queryOnboardingDismiss.click();\n\n    await expect(vectorSearchPage.queryEditor.queryOnboarding).not.toBeVisible();\n  });\n\n  test('should not show query onboarding on subsequent visit', async ({ vectorSearchPage, page }) => {\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchQueryOnboarding', 'true');\n    });\n\n    await vectorSearchPage.goto(database.id);\n    await vectorSearchPage.indexList.openQuery(TEST_INDEX_NAME);\n\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n    await expect(vectorSearchPage.queryEditor.queryOnboarding).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tests/main/vector-search/query/save-query.spec.ts",
    "content": "import { faker } from '@faker-js/faker';\nimport { test, expect } from 'e2eSrc/fixtures/base';\nimport { StandaloneConfigFactory } from 'e2eSrc/test-data/databases';\nimport { IndexConfigFactory, IndexHashKeyFactory } from 'e2eSrc/test-data/vector-search';\nimport { DatabaseInstance } from 'e2eSrc/types';\n\nconst uniqueId = faker.string.alphanumeric(6);\nconst TEST_INDEX_PREFIX = `test-vs-save-${uniqueId}:`;\nconst TEST_INDEX_NAME = `test-vs-save-${uniqueId}-idx`;\n\n/**\n * Vector Search > Save Query\n *\n * Tests for saving queries from the query editor\n * and cancelling the save modal.\n */\ntest.describe('Vector Search > Save Query', { tag: '@serial' }, () => {\n  let database: DatabaseInstance;\n\n  test.beforeAll(async ({ apiHelper }) => {\n    database = await apiHelper.createDatabase(StandaloneConfigFactory.build());\n\n    const hashKey = IndexHashKeyFactory.build({ keyName: `${TEST_INDEX_PREFIX}key1` });\n    await apiHelper.createHashKey(database.id, hashKey.keyName, hashKey.fields);\n  });\n\n  test.afterEach(async ({ apiHelper }) => {\n    await apiHelper.deleteAllIndexes(database.id, (name) => name.includes(uniqueId));\n  });\n\n  test.afterAll(async ({ apiHelper }) => {\n    await apiHelper.deleteKeysByPattern(database.id, `${TEST_INDEX_PREFIX}*`);\n    await apiHelper.deleteDatabase(database.id);\n  });\n\n  test.beforeEach(async ({ vectorSearchPage, apiHelper, page }) => {\n    // Seed index\n    const indexConfig = IndexConfigFactory.build({ indexName: TEST_INDEX_NAME, prefix: TEST_INDEX_PREFIX });\n    await apiHelper.createIndex(database.id, indexConfig.indexName, indexConfig.prefix, indexConfig.schema);\n\n    // Navigate to query page\n    await vectorSearchPage.goto(database.id);\n    await expect(vectorSearchPage.listWrapper).toBeVisible();\n\n    // Skip onboarding\n    await page.evaluate(() => {\n      localStorage.setItem('vectorSearchQueryOnboarding', 'true');\n    });\n\n    await vectorSearchPage.indexList.openQuery(TEST_INDEX_NAME);\n    await expect(vectorSearchPage.queryPageWrapper).toBeVisible();\n  });\n\n  test('should save query and verify it appears in query library', async ({ vectorSearchPage }) => {\n    const queryName = `test-saved-${faker.string.alphanumeric(6)}`;\n\n    await vectorSearchPage.queryEditor.typeQuery('* => [KNN 10 @vec $BLOB]');\n    await expect(vectorSearchPage.queryEditor.saveButton).toBeEnabled();\n    await vectorSearchPage.queryEditor.saveButton.click();\n\n    await expect(vectorSearchPage.saveQueryModal.body).toBeVisible();\n    // Save the query via modal\n    await vectorSearchPage.saveQueryModal.nameInput.fill(queryName);\n    await vectorSearchPage.saveQueryModal.saveButton.click();\n\n    await expect(vectorSearchPage.saveQueryModal.successToast).toBeVisible();\n    await expect(vectorSearchPage.saveQueryModal.body).not.toBeVisible();\n\n    // Verify saved query appears in library\n    await vectorSearchPage.selectQueryLibraryTab();\n    await expect(vectorSearchPage.queryLibrary.getItemByName(queryName)).toBeVisible();\n  });\n\n  test('should navigate to query library via success toast action', async ({ vectorSearchPage }) => {\n    const queryName = `test-saved-${faker.string.alphanumeric(6)}`;\n\n    await vectorSearchPage.queryEditor.typeQuery('* => [KNN 3 @vec $BLOB]');\n    await expect(vectorSearchPage.queryEditor.saveButton).toBeEnabled();\n    await vectorSearchPage.queryEditor.saveButton.click();\n\n    await vectorSearchPage.saveQueryModal.nameInput.fill(queryName);\n    await vectorSearchPage.saveQueryModal.saveButton.click();\n\n    await expect(vectorSearchPage.saveQueryModal.successToast).toBeVisible();\n\n    // Navigate to library via toast action\n    await vectorSearchPage.saveQueryModal.successToastGoToLibrary.click();\n\n    await expect(vectorSearchPage.queryLibrary.container).toBeVisible();\n    await expect(vectorSearchPage.queryLibrary.getItemByName(queryName)).toBeVisible();\n  });\n\n  test('should cancel save query modal', async ({ vectorSearchPage }) => {\n    const queryName = `test-cancelled-${faker.string.alphanumeric(6)}`;\n\n    await vectorSearchPage.queryEditor.typeQuery('* => [KNN 5 @vec $BLOB]');\n    await expect(vectorSearchPage.queryEditor.saveButton).toBeEnabled();\n    await vectorSearchPage.queryEditor.saveButton.click();\n\n    await expect(vectorSearchPage.saveQueryModal.body).toBeVisible();\n\n    // Cancel without saving\n    await vectorSearchPage.saveQueryModal.nameInput.fill(queryName);\n    await vectorSearchPage.saveQueryModal.cancelButton.click();\n\n    await expect(vectorSearchPage.saveQueryModal.body).not.toBeVisible();\n\n    // Verify query was not saved to library\n    await vectorSearchPage.selectQueryLibraryTab();\n    await expect(vectorSearchPage.queryLibrary.getItemByName(queryName)).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/e2e-playwright/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"ES2022\", \"DOM\"],\n    \"moduleResolution\": \"node\",\n    \"baseUrl\": \".\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\", \"@playwright/test\"],\n    \"paths\": {\n      \"e2eSrc/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tests/e2e-playwright/types/database.ts",
    "content": "/**\n * Database connection types\n */\nexport enum ConnectionType {\n  Standalone = 'STANDALONE',\n  StandaloneBig = 'STANDALONE_BIG',\n  Cluster = 'CLUSTER',\n  Sentinel = 'SENTINEL',\n}\n\n/**\n * Base Redis connection configuration for tests\n */\nexport interface RedisConnectionConfig {\n  host: string;\n  port: number;\n  username?: string;\n  password?: string;\n  db?: number;\n}\n\n/**\n * Configuration for adding a database via UI\n */\nexport interface AddDatabaseConfig extends RedisConnectionConfig {\n  name: string;\n}\n\n/**\n * SSH tunnel configuration\n */\nexport interface SshTunnelConfig {\n  host?: string;\n  port?: number;\n  username?: string;\n  password?: string;\n  privateKey?: string;\n  passphrase?: string;\n}\n\n/**\n * TLS certificate configuration\n */\nexport interface TlsCertConfig {\n  name: string;\n  certificate: string;\n}\n\n/**\n * TLS client certificate configuration (includes private key)\n */\nexport interface TlsClientCertConfig extends TlsCertConfig {\n  key: string;\n}\n\n/**\n * TLS configuration for database connection\n */\nexport interface TlsConfig {\n  enabled: boolean;\n  verifyServerCert?: boolean;\n  useSni?: boolean;\n  sniHost?: string;\n  caCert?: TlsCertConfig;\n  clientCert?: TlsClientCertConfig;\n}\n\n/**\n * Sentinel-specific configuration\n */\nexport interface SentinelConfig extends RedisConnectionConfig {\n  masterName: string;\n}\n\n/**\n * Database instance as returned from the API\n * Only includes fields we need for test assertions/operations\n */\nexport interface DatabaseInstance {\n  id: string;\n  host: string;\n  port: number;\n  name: string;\n  connectionType?: ConnectionType;\n  username?: string | null;\n  password?: string | null;\n  db?: number;\n  tls?: boolean;\n  ssh?: boolean;\n}\n"
  },
  {
    "path": "tests/e2e-playwright/types/index.ts",
    "content": "export * from './database';\nexport * from './key';\nexport * from './vector-search';\n"
  },
  {
    "path": "tests/e2e-playwright/types/key.ts",
    "content": "/**\n * Redis key types\n */\nexport type KeyType = 'Hash' | 'List' | 'Set' | 'Sorted Set' | 'String' | 'JSON' | 'Stream';\n\n/**\n * Key data for creating keys\n */\nexport interface BaseKeyData {\n  keyName: string;\n  ttl?: string;\n}\n\nexport interface StringKeyData extends BaseKeyData {\n  value: string;\n}\n\nexport interface HashKeyData extends BaseKeyData {\n  fields: Array<{ field: string; value: string }>;\n}\n\nexport interface ListKeyData extends BaseKeyData {\n  elements: string[];\n}\n\nexport interface SetKeyData extends BaseKeyData {\n  members: string[];\n}\n\nexport interface ZSetKeyData extends BaseKeyData {\n  members: Array<{ member: string; score: string }>;\n}\n\nexport interface StreamKeyData extends BaseKeyData {\n  entryId?: string;\n  fields: Array<{ field: string; value: string }>;\n}\n\nexport interface JsonKeyData extends BaseKeyData {\n  value: string;\n}\n\nexport type KeyData =\n  | StringKeyData\n  | HashKeyData\n  | ListKeyData\n  | SetKeyData\n  | ZSetKeyData\n  | StreamKeyData\n  | JsonKeyData;\n"
  },
  {
    "path": "tests/e2e-playwright/types/vector-search.ts",
    "content": "/**\n * RediSearch index field schema\n */\nexport interface IndexSchemaField {\n  name: string;\n  type: string;\n}\n\n/**\n * Configuration for creating a RediSearch index via API\n */\nexport interface IndexConfig {\n  indexName: string;\n  prefix: string;\n  schema: IndexSchemaField[];\n  keyType?: 'hash' | 'json';\n}\n\n/**\n * Hash key data shaped for vector search index creation tests\n */\nexport interface IndexHashKeyData {\n  keyName: string;\n  fields: Array<{ field: string; value: string }>;\n}\n\n/**\n * JSON key data shaped for vector search index creation tests\n */\nexport interface IndexJsonKeyData {\n  keyName: string;\n  value: Record<string, unknown>;\n}\n"
  },
  {
    "path": "tests/playwright/.gitignore",
    "content": "\n# Playwright\nnode_modules/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/allure-results/\n/.nyc_output/\n/coverage/\n"
  },
  {
    "path": "tests/playwright/.nycrc.json",
    "content": "{\n  \"all\": true,\n  \"cwd\": \"../../\",\n  \"include\": [\"redisinsight/ui/src/**/*.{ts,tsx,js,jsx}\"],\n  \"exclude\": [\n    \"redisinsight/ui/src/**/*.{test,spec}.{ts,tsx,js,jsx}\",\n    \"redisinsight/ui/src/**/__tests__/**\",\n    \"redisinsight/ui/src/**/__mocks__/**\",\n    \"redisinsight/ui/src/**/*.d.ts\",\n    \"redisinsight/ui/src/**/node_modules/**\"\n  ],\n  \"reporter\": [\"text\", \"html\", \"lcov\"],\n  \"report-dir\": \"tests/playwright/coverage\",\n  \"temp-dir\": \"tests/playwright/.nyc_output\",\n  \"cache\": false,\n  \"check-coverage\": false,\n  \"skip-full\": false,\n  \"skip-empty\": false\n}\n"
  },
  {
    "path": "tests/playwright/README.md",
    "content": "# RedisInsight Playwright Tests\n\nThis project contains end-to-end tests for RedisInsight using [Playwright](https://playwright.dev/). It supports running tests against three different RedisInsight builds:\n\n- **Docker Build**\n- **Electron Build**\n- **Local Web Build** (built directly from the source code)\n\n---\n\n## Installation\n\n> _Note: All commands below should be run from the `tests/playwright` directory._\n\nBefore running any tests, make sure you have the dependencies installed:\n\n1. Install Node dependencies:\n\n   ```shell\n   yarn install\n   ```\n\n2. Install Playwright browsers:\n\n   ```shell\n   yarn playwright install\n   ```\n\n3. Install Playwright OS dependencies (Linux only):\n\n   ```shell\n   sudo yarn playwright install-deps\n   ```\n\n## Prerequisites\n\n- Docker installed and running.\n- Redis test environment and RedisInsight configurations from the `tests/e2e` project.\n\n## Environment-Specific Setup and Test Execution\n\nFor more details, refer to the [Playwright documentation](https://playwright.dev/docs/running-tests).\n\n### Start Redis Test Environment (Required for All Builds)\n\nNavigate to the `tests/e2e` directory and run:\n\n```shell\ndocker compose -p test-docker -f rte.docker-compose.yml up --force-recreate --detach\n```\n\n### Docker Build\n\n- Build the Docker image locally or trigger a [GitHub Action](https://github.com/RedisInsight/RedisInsight/actions/workflows/manual-build.yml) to build and download the artifact (`docker-linux-alpine.amd64.tar`).\n- Load the image:\n  ```shell\n  docker load -i docker-linux-alpine.amd64.tar\n  ```\n- Ensure the following environment variables are set in `tests/e2e/.env`:\n  - `RI_ENCRYPTION_KEY`\n  - `RI_SERVER_TLS_CERT`\n  - `RI_SERVER_TLS_KEY`\n- Navigate to the `tests/e2e` directory and start the container:\n  ```shell\n  docker compose -p e2e-ri-docker -f docker.web.docker-compose.yml up --detach --force-recreate\n  ```\n- Validate app is running at: `https://localhost:5540`.\n\n#### Run Playwright Tests\n\n_Note: Make sure to run the commands bellow from the `e2e/playwright` directory._\n\nRun all tests:\n\n```shell\nyarn test:chromium:docker\n```\n\nRun in debug mode:\n\n```shell\nyarn test:chromium:docker:debug\n```\n\nRun a specific spec file:\n\n```shell\nyarn test:chromium:docker basic-navigation\n```\n\n---\n\n### Electron Build\n\n- Build the project from the root directory:\n  ```shell\n  yarn package:prod\n  ```\n- Update `ELECTRON_EXECUTABLE_PATH` in `tests/playwright/env/.desktop.env` to point to the generated executable file (MacOS by default).\n\n#### Run Playwright Tests\n\n_Note: Make sure to run the commands bellow from the `e2e/playwright` directory._\n\n```shell\nyarn test:electron\n```\n\n---\n\n### Local Web Build\n\n- Make sure you don't have anything (docker container, local server, etc.) running on port 5540.\n- Start the UI and API servers:\n  ```shell\n  yarn dev:ui\n  yarn dev:api\n  ```\n- Access the app at: `http://localhost:8080`.\n\n#### Run Playwright Tests\n\n_Note: Make sure to run the command bellow from the `e2e/playwright` directory._\n\n```shell\nyarn test:chromium:local-web\n```\n\n## Folder structure\n\n- `/env` - contains env configs for the 3 types of builds.\n- `/tests` - Contains the actual tests.\n- `/helpers/api` - ported some api helpers from the tests/e2e project. They are used for setting up data.\n- `/pageObjects` - ported page element locators and logic from the tests/e2e project.\n\n## Extra Tooling\n\n### Auto-Generate Tests\n\nUse Playwright's Codegen to auto-generate tests:\n\n```shell\nyarn playwright codegen\n```\n\n### Interactive UI Mode\n\nStart Playwright's interactive UI mode:\n\n```shell\nyarn playwright test --ui\n```\n\n## Reports\n\n### Allure Reports\n\n- Ensure `JAVA_HOME` is set and JDK version 8 to 11 is installed.\n- Generate a report with history:\n  ```shell\n  yarn test:allureHistoryReport\n  ```\n- For more details, refer to the [Allure documentation](https://allurereport.org/docs/playwright-reference/).\n\n### Execution Time Comparison\n\n| Test Name                         | Framework  | Browser  | Duration |\n| --------------------------------- | ---------- | -------- | -------- |\n| Verify that user can add Hash Key | TestCafe   | Chromium | 27s      |\n| Verify that user can add Hash Key | Playwright | Chromium | 10s      |\n| Verify that user can add Hash Key | TestCafe   | Electron | 30s      |\n| Verify that user can add Hash Key | Playwright | Electron | 18s      |\n\n## Code Coverage\n\n### Overview\n\nThe Playwright tests can collect code coverage for the React frontend application. This helps track which parts of the UI code are being exercised by the end-to-end tests.\n\n### Quick Start\n\n# Start the UI with instrumentation for collecting code coverage\n\nEnsure UI app is running with `COLLECT_COVERAGE=true` env variable, or simply run the following helper from the root folder\n\n```shell\nyarn dev:ui:coverage\n```\n\n# Run tests with coverage and generate both text and HTML reports\n\n```shell\ncd tests/playwright\nyarn test:coverage\n```\n\n### Coverage Reports Location\n\nAfter running coverage tests, reports are generated in:\n\n- **HTML Report**: `tests/playwright/coverage/index.html` - Interactive, browsable coverage report\n- **LCOV Report**: `tests/playwright/coverage/lcov.info` - For CI/CD integration\n"
  },
  {
    "path": "tests/playwright/env/.desktop.env",
    "content": "COMMON_URL=\nAPI_URL=http://localhost:5530/api\nELECTRON_EXECUTABLE_PATH=../../release/mac-arm64/Redis Insight.app/Contents/MacOS/Redis Insight\n\nRI_APP_FOLDER_NAME=.redis-insight-stage\n\nOSS_STANDALONE_HOST=localhost\nOSS_STANDALONE_PORT=8100\n\nOSS_STANDALONE_V5_HOST=localhost\nOSS_STANDALONE_V5_PORT=8101\n\nOSS_STANDALONE_V7_HOST=localhost\nOSS_STANDALONE_V7_PORT=8108\n\nOSS_STANDALONE_V8_HOST=localhost\nOSS_STANDALONE_V8_PORT=8109\n\nOSS_STANDALONE_REDISEARCH_HOST=localhost\nOSS_STANDALONE_REDISEARCH_PORT=8102\n\nOSS_STANDALONE_BIG_HOST=localhost\nOSS_STANDALONE_BIG_PORT=8103\n\nOSS_STANDALONE_TLS_HOST=localhost\nOSS_STANDALONE_TLS_PORT=8104\n\nOSS_STANDALONE_EMPTY_HOST=localhost\nOSS_STANDALONE_EMPTY_PORT=8105\n\nOSS_STANDALONE_REDISGEARS_HOST=localhost\nOSS_STANDALONE_REDISGEARS_PORT=8106\n\nOSS_STANDALONE_NOPERM_HOST=localhost\nOSS_STANDALONE_NOPERM_PORT=8100\n\nOSS_CLUSTER_REDISGEARS_2_HOST=localhost\nOSS_CLUSTER_REDISGEARS_2_PORT=8107\n\nOSS_CLUSTER_HOST=localhost\nOSS_CLUSTER_PORT=8200\n\nOSS_SENTINEL_HOST=localhost\nOSS_SENTINEL_PORT=28100\nOSS_SENTINEL_PASSWORD=password\n\nRE_CLUSTER_HOST=localhost\nRE_CLUSTER_PORT=19443\n\nRI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/e2e/notifications.json\nRI_NOTIFICATION_SYNC_INTERVAL=30000\n\nRI_FEATURES_CONFIG_URL=http://localhost:5551/remote/features-config.json\nRI_FEATURES_CONFIG_SYNC_INTERVAL=50000\n\nREMOTE_FOLDER_PATH=/home/runner/work/RedisInsight/RedisInsight/tests/e2e/remote\n"
  },
  {
    "path": "tests/playwright/env/.docker.env",
    "content": "COMMON_URL=https://localhost:5540\nAPI_URL=https://localhost:5540/api\n\nRI_APP_FOLDER_NAME=.redis-insight\n\nOSS_STANDALONE_HOST=host.docker.internal\nOSS_STANDALONE_PORT=8100\n\nOSS_STANDALONE_V5_HOST=host.docker.internal\nOSS_STANDALONE_V5_PORT=8101\n\nOSS_STANDALONE_V7_HOST=host.docker.internal\nOSS_STANDALONE_V7_PORT=8108\n\nOSS_STANDALONE_V8_HOST=host.docker.internal\nOSS_STANDALONE_V8_PORT=8109\n\nOSS_STANDALONE_REDISEARCH_HOST=host.docker.internal\nOSS_STANDALONE_REDISEARCH_PORT=8102\n\nOSS_STANDALONE_BIG_HOST=host.docker.internal\nOSS_STANDALONE_BIG_PORT=8103\n\nOSS_STANDALONE_TLS_HOST=host.docker.internal\nOSS_STANDALONE_TLS_PORT=8104\n\nOSS_STANDALONE_EMPTY_HOST=host.docker.internal\nOSS_STANDALONE_EMPTY_PORT=8105\n\nOSS_STANDALONE_REDISGEARS_HOST=host.docker.internal\nOSS_STANDALONE_REDISGEARS_PORT=8106\n\nOSS_STANDALONE_NOPERM_HOST=host.docker.internal\nOSS_STANDALONE_NOPERM_PORT=8100\n\nOSS_CLUSTER_REDISGEARS_2_HOST=host.docker.internal\nOSS_CLUSTER_REDISGEARS_2_PORT=8107\n\nOSS_CLUSTER_HOST=host.docker.internal\nOSS_CLUSTER_PORT=8200\n\nOSS_SENTINEL_HOST=host.docker.internal\nOSS_SENTINEL_PORT=28100\nOSS_SENTINEL_PASSWORD=password\n\nRE_CLUSTER_HOST=host.docker.internal\nRE_CLUSTER_PORT=19443\n"
  },
  {
    "path": "tests/playwright/env/.local-web.env",
    "content": "COMMON_URL=http://localhost:8080\nAPI_URL=http://localhost:5540/api\n\nRI_APP_FOLDER_NAME=.redis-insight\n\nOSS_STANDALONE_HOST=localhost\nOSS_STANDALONE_PORT=8100\n\nOSS_STANDALONE_V5_HOST=localhost\nOSS_STANDALONE_V5_PORT=8101\n\nOSS_STANDALONE_V7_HOST=localhost\nOSS_STANDALONE_V7_PORT=8108\n\nOSS_STANDALONE_V8_HOST=localhost\nOSS_STANDALONE_V8_PORT=8109\n\nOSS_STANDALONE_REDISEARCH_HOST=localhost\nOSS_STANDALONE_REDISEARCH_PORT=8102\n\nOSS_STANDALONE_BIG_HOST=localhost\nOSS_STANDALONE_BIG_PORT=8103\n\nOSS_STANDALONE_TLS_HOST=localhost\nOSS_STANDALONE_TLS_PORT=8104\n\nOSS_STANDALONE_EMPTY_HOST=localhost\nOSS_STANDALONE_EMPTY_PORT=8105\n\nOSS_STANDALONE_REDISGEARS_HOST=localhost\nOSS_STANDALONE_REDISGEARS_PORT=8106\n\nOSS_STANDALONE_NOPERM_HOST=localhost\nOSS_STANDALONE_NOPERM_PORT=8100\n\nOSS_CLUSTER_REDISGEARS_2_HOST=localhost\nOSS_CLUSTER_REDISGEARS_2_PORT=8107\n\nOSS_CLUSTER_HOST=localhost\nOSS_CLUSTER_PORT=8200\n\nOSS_SENTINEL_HOST=localhost\nOSS_SENTINEL_PORT=28100\nOSS_SENTINEL_PASSWORD=password\n\nRE_CLUSTER_HOST=localhost\nRE_CLUSTER_PORT=19443\n"
  },
  {
    "path": "tests/playwright/factories/redisearch-index.factory.ts",
    "content": "import { Factory } from 'fishery'\nimport { faker } from '@faker-js/faker'\nimport {\n    CreateRedisearchIndexParameters,\n    RedisearchIndexField,\n} from '../types/indexes'\n\nexport const redisearchIndexFieldFactory = Factory.define<RedisearchIndexField>(\n    ({ params }) => ({\n        name: params.name ?? faker.word.noun(),\n        type:\n            params.type ??\n            faker.helpers.arrayElement([\n                'TEXT',\n                'TAG',\n                'NUMERIC',\n                'GEO',\n                'GEOSHAPE',\n                'VECTOR',\n            ]),\n    }),\n)\n\nexport const redisearchIndexFactory =\n    Factory.define<CreateRedisearchIndexParameters>(({ params }) => ({\n        indexName: params.indexName ?? faker.word.noun(),\n        keyType: params.keyType ?? 'HASH',\n        prefixes: params.prefixes ?? [\n            `product:${faker.string.alphanumeric({ length: 5 })}:`,\n        ],\n        fields: params.fields ?? redisearchIndexFieldFactory.buildList(3),\n    }))\n"
  },
  {
    "path": "tests/playwright/fixtures/test.ts",
    "content": "/* eslint-disable no-empty-pattern */\nimport { test as base, expect } from '@playwright/test'\nimport {\n    BrowserContext,\n    ElectronApplication,\n    Page,\n    _electron as electron,\n} from 'playwright'\nimport log from 'node-color-log'\nimport { AxiosInstance } from 'axios'\nimport * as crypto from 'crypto'\nimport fs from 'fs'\nimport path from 'path'\n\nimport { apiUrl, isElectron, electronExecutablePath } from '../helpers/conf'\nimport { generateApiClient } from '../helpers/api/http-client'\nimport { APIKeyRequests } from '../helpers/api/api-keys'\nimport { DatabaseAPIRequests } from '../helpers/api/api-databases'\nimport { APIIndexRequests } from '../helpers/api/api-indexes'\nimport { UserAgreementDialog } from '../pageObjects'\n\n// Coverage type declaration\ndeclare global {\n    interface Window {\n        // eslint-disable-next-line no-underscore-dangle\n        __coverage__: any\n    }\n}\n\nexport function generateUUID(): string {\n    return crypto.randomBytes(16).toString('hex')\n}\n\ntype CommonFixtures = {\n    forEachTest: void\n    api: {\n        apiClient: AxiosInstance\n        keyService: APIKeyRequests\n        databaseService: DatabaseAPIRequests\n        indexService: APIIndexRequests\n    }\n}\n\nconst commonTest = base.extend<CommonFixtures>({\n    // Simple context setup for coverage\n    context: async ({ context }, use) => {\n        if (process.env.COLLECT_COVERAGE === 'true') {\n            const outputDir = path.join(process.cwd(), '.nyc_output')\n            await fs.promises.mkdir(outputDir, { recursive: true })\n\n            // Expose coverage collection function\n            await context.exposeFunction(\n                'collectIstanbulCoverage',\n                (coverageJSON: string) => {\n                    if (coverageJSON) {\n                        fs.writeFileSync(\n                            path.join(\n                                outputDir,\n                                `playwright_coverage_${generateUUID()}.json`,\n                            ),\n                            coverageJSON,\n                        )\n                    }\n                },\n            )\n        }\n\n        await use(context)\n    },\n\n    api: async ({ page }, use) => {\n        const windowId = await page.evaluate(() => window.windowId)\n\n        const apiClient = generateApiClient(apiUrl, windowId)\n        const databaseService = new DatabaseAPIRequests(apiClient)\n        const keyService = new APIKeyRequests(apiClient, databaseService)\n        const indexService = new APIIndexRequests(apiClient, databaseService)\n\n        await use({ apiClient, keyService, databaseService, indexService })\n    },\n    forEachTest: [\n        async ({ page }, use) => {\n            // before each test:\n            if (!isElectron) {\n                await page.goto('/')\n            } else {\n                await page.locator('[data-testid=\"home-tab-databases\"]').click()\n            }\n\n            const userAgreementDialog = new UserAgreementDialog(page)\n            await userAgreementDialog.acceptLicenseTerms()\n\n            const skipTourElement = page.locator('button', {\n                hasText: 'Skip tour',\n            })\n            if (await skipTourElement.isVisible()) {\n                skipTourElement.click()\n            }\n\n            await use()\n\n            // Collect coverage after each test\n            if (process.env.COLLECT_COVERAGE === 'true') {\n                await page\n                    .evaluate(() => {\n                        if (\n                            typeof window !== 'undefined' &&\n                            // eslint-disable-next-line no-underscore-dangle\n                            window.__coverage__\n                        ) {\n                            ;(window as any).collectIstanbulCoverage(\n                                // eslint-disable-next-line no-underscore-dangle\n                                JSON.stringify(window.__coverage__),\n                            )\n                        }\n                    })\n                    .catch(() => {\n                        // Ignore errors - page might be closed\n                    })\n            }\n        },\n        { auto: true },\n    ],\n})\n\nconst electronTest = commonTest.extend<{\n    electronApp: ElectronApplication | null\n    page: Page\n    context: BrowserContext\n}>({\n    electronApp: async ({}, use) => {\n        const electronApp = await electron.launch({\n            executablePath: electronExecutablePath,\n            args: ['index.html'],\n            timeout: 60000,\n        })\n        electronApp.on('console', (msg) => {\n            log.info(`Electron Log: ${msg.type()} - ${msg.text()}`)\n        })\n\n        // Wait for window startup\n        await new Promise((resolve) => setTimeout(resolve, 2000))\n\n        await use(electronApp)\n\n        log.info('Closing Electron app...')\n        await electronApp.close()\n    },\n    page: async ({ electronApp }, use) => {\n        if (!electronApp) {\n            throw new Error('Electron app is not initialized')\n        }\n\n        const electronPage = await electronApp.firstWindow()\n\n        await use(electronPage)\n    },\n    context: async ({ electronApp }, use) => {\n        if (!electronApp) {\n            throw new Error('Electron app is not initialized')\n        }\n\n        const electronContext = electronApp.context()\n\n        await use(electronContext)\n    },\n})\n\nconst test = isElectron ? electronTest : commonTest\n\nexport { test, expect, isElectron }\n"
  },
  {
    "path": "tests/playwright/helpers/api/api-databases.ts",
    "content": "import { faker } from '@faker-js/faker'\nimport { AxiosInstance } from 'axios'\nimport { AddNewDatabaseParameters, DatabaseInstance } from '../../types'\nimport { ResourcePath } from '../constants'\n\nexport class DatabaseAPIRequests {\n    constructor(private apiClient: AxiosInstance) {}\n\n    async addNewStandaloneDatabaseApi(\n        databaseParameters: AddNewDatabaseParameters,\n        isCloud = false,\n    ): Promise<void> {\n        const uniqueId = faker.string.alphanumeric({ length: 10 })\n        const uniqueIdNumber = faker.number.int({ min: 1, max: 1000 })\n        const requestBody: any = {\n            name: databaseParameters.databaseName,\n            host: databaseParameters.host,\n            port: Number(databaseParameters.port),\n        }\n\n        if (databaseParameters.databaseUsername) {\n            requestBody.username = databaseParameters.databaseUsername\n        }\n\n        if (databaseParameters.databasePassword) {\n            requestBody.password = databaseParameters.databasePassword\n        }\n\n        if (databaseParameters.caCert) {\n            requestBody.tls = true\n            requestBody.verifyServerCert = false\n            requestBody.caCert = {\n                name: `ca-${uniqueId}`,\n                certificate: databaseParameters.caCert.certificate,\n            }\n            requestBody.clientCert = {\n                name: `client-${uniqueId}`,\n                certificate: databaseParameters.clientCert!.certificate,\n                key: databaseParameters.clientCert!.key,\n            }\n        }\n\n        if (isCloud) {\n            requestBody.cloudDetails = {\n                cloudId: uniqueIdNumber,\n                subscriptionType: 'fixed',\n                planMemoryLimit: 30,\n                memoryLimitMeasurementUnit: 'mb',\n                free: true,\n            }\n        }\n\n        const response = await this.apiClient.post(\n            ResourcePath.Databases,\n            requestBody,\n        )\n        if (response.status !== 201)\n            throw new Error(\n                `Database creation failed for ${databaseParameters.databaseName}`,\n            )\n    }\n\n    async getAllDatabases(): Promise<DatabaseInstance[]> {\n        const response = await this.apiClient.get(ResourcePath.Databases)\n        if (response.status !== 200)\n            throw new Error('Failed to retrieve databases')\n        return response.data\n    }\n\n    async getDatabaseIdByName(databaseName?: string): Promise<string> {\n        if (!databaseName) throw new Error('Error: Missing databaseName')\n\n        const allDatabases = await this.getAllDatabases()\n        const foundDb = allDatabases.find((item) => item.name === databaseName)\n\n        if (!foundDb) throw new Error(`Database ${databaseName} not found`)\n\n        return foundDb.id\n    }\n\n    async deleteStandaloneDatabaseApi(\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        if (!databaseId) throw new Error('Error: Missing databaseId')\n\n        const requestBody = { ids: [databaseId] }\n        const response = await this.apiClient.delete(ResourcePath.Databases, {\n            data: requestBody,\n        })\n        if (response.status !== 200)\n            throw new Error(\n                `Failed to delete database ${databaseParameters.databaseName}`,\n            )\n    }\n}\n"
  },
  {
    "path": "tests/playwright/helpers/api/api-indexes.ts",
    "content": "import { AxiosInstance } from 'axios'\nimport { DatabaseAPIRequests } from './api-databases'\nimport {\n    AddNewDatabaseParameters,\n    CreateRedisearchIndexParameters,\n} from '../../types'\nimport { stringToBuffer } from '../utils'\n\nexport class APIIndexRequests {\n    constructor(\n        private apiClient: AxiosInstance,\n        private databaseAPIRequests: DatabaseAPIRequests,\n    ) {}\n\n    /**\n     * Gets all RediSearch indexes using the API (FT._LIST equivalent)\n     * @param databaseParameters Database connection parameters\n     * @returns Array of index names (as strings)\n     */\n    async getAllRedisearchIndexesApi(\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<string[]> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const response = await this.apiClient.get(\n            `/databases/${databaseId}/redisearch`,\n        )\n\n        if (response.status !== 200) {\n            throw new Error('Failed to get RediSearch indexes')\n        }\n\n        // The response should have a property 'indexes' which is an array\n        return (\n            response.data.indexes?.map((idx: any) =>\n                typeof idx === 'string'\n                    ? idx\n                    : Buffer.from(idx.data).toString('utf8'),\n            ) || []\n        )\n    }\n\n    /**\n     * Creates a new RediSearch index using the FT.CREATE command\n     * @param indexParameters Parameters for creating the index\n     * @param databaseParameters Database connection parameters\n     */\n    async createRedisearchIndexApi(\n        indexParameters: CreateRedisearchIndexParameters,\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n\n        const requestBody = {\n            index: stringToBuffer(indexParameters.indexName),\n            type: indexParameters.keyType.toLowerCase(),\n            prefixes: indexParameters.prefixes?.map((prefix) =>\n                stringToBuffer(prefix),\n            ),\n            fields: indexParameters.fields.map((field) => ({\n                name: stringToBuffer(field.name),\n                type: field.type.toLowerCase(),\n            })),\n        }\n\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/redisearch?encoding=buffer`,\n            requestBody,\n        )\n\n        if (response.status !== 201) {\n            throw new Error(\n                `Failed to create RediSearch index: ${indexParameters.indexName}`,\n            )\n        }\n    }\n\n    /**\n     * Deletes a RediSearch index using the FT.DROPINDEX command\n     * @param indexName Name of the index to delete\n     * @param databaseParameters Database connection parameters\n     * @param deleteDocuments Whether to delete associated documents (DD flag)\n     */\n    async deleteRedisearchIndexApi(\n        indexName: string,\n        databaseParameters: AddNewDatabaseParameters,\n        deleteDocuments: boolean = false,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n\n        const requestBody = {\n            index: stringToBuffer(indexName),\n            deleteDocuments,\n        }\n\n        try {\n            const response = await this.apiClient.delete(\n                `/databases/${databaseId}/redisearch?encoding=buffer`,\n                {\n                    data: requestBody,\n                },\n            )\n\n            if (response.status !== 204) {\n                throw new Error(\n                    `Failed to delete RediSearch index: ${indexName}`,\n                )\n            }\n        } catch (error: any) {\n            // Ignore 404 errors as the index might not exist (e.g., during cleanup)\n            if (error.response?.status === 404) {\n                // Index doesn't exist, which is fine for cleanup\n                return\n            }\n            throw error\n        }\n    }\n\n    /**\n     * Deletes all RediSearch indexes using the API\n     * @param databaseParameters Database connection parameters\n     * @param deleteDocuments Whether to delete associated documents (DD flag)\n     */\n    async deleteAllRedisearchIndexesApi(\n        databaseParameters: AddNewDatabaseParameters,\n        deleteDocuments: boolean = false,\n    ): Promise<void> {\n        const indexes =\n            await this.getAllRedisearchIndexesApi(databaseParameters)\n\n        for (const indexName of indexes) {\n            try {\n                await this.deleteRedisearchIndexApi(\n                    indexName,\n                    databaseParameters,\n                    deleteDocuments,\n                )\n            } catch (e) {\n                // Ignore errors for individual indexes, in case of cleanup\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "tests/playwright/helpers/api/api-keys.ts",
    "content": "/* eslint-disable max-len */\nimport { AxiosInstance } from 'axios'\nimport { DatabaseAPIRequests } from './api-databases'\nimport {\n    AddNewDatabaseParameters,\n    HashKeyParameters,\n    SetKeyParameters,\n    StreamKeyParameters,\n} from '../../types'\nimport { stringToBuffer } from '../utils'\n\nconst bufferPathMask = '/databases/databaseId/keys?encoding=buffer'\nexport class APIKeyRequests {\n    constructor(\n        private apiClient: AxiosInstance,\n        private databaseAPIRequests: DatabaseAPIRequests,\n    ) {}\n\n    async addStringKeyApi(\n        keyParameters: { keyName: string; value: string; expire?: number },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            value: stringToBuffer(keyParameters.value),\n            expire: keyParameters?.expire,\n        }\n\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/string?encoding=buffer`,\n            requestBody,\n        )\n\n        if (response.status !== 201) {\n            throw new Error('The creation of new String key request failed')\n        }\n    }\n\n    async addHashKeyApi(\n        keyParameters: HashKeyParameters & { expire?: number },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            fields: keyParameters.fields.map((fields) => ({\n                ...fields,\n                field: stringToBuffer(fields.field),\n                value: stringToBuffer(fields.value),\n            })),\n            expire: keyParameters?.expire,\n        }\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/hash?encoding=buffer`,\n            requestBody,\n        )\n        if (response.status !== 201)\n            throw new Error('The creation of new Hash key request failed')\n    }\n\n    async addListKeyApi(\n        keyParameters: { keyName: string; elements: string[]; expire?: number },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            elements: keyParameters.elements.map((element) =>\n                stringToBuffer(element),\n            ),\n            expire: keyParameters?.expire,\n        }\n\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/list?encoding=buffer`,\n            requestBody,\n        )\n\n        if (response.status !== 201) {\n            throw new Error('The creation of new List key request failed')\n        }\n    }\n\n    async addStreamKeyApi(\n        keyParameters: StreamKeyParameters & { expire?: number },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            entries: keyParameters.entries.map((member) => ({\n                ...member,\n                fields: member.fields.map(({ name, value }) => ({\n                    name: stringToBuffer(name),\n                    value: stringToBuffer(value),\n                })),\n            })),\n            expire: keyParameters?.expire,\n        }\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/streams?encoding=buffer`,\n            requestBody,\n        )\n        if (response.status !== 201)\n            throw new Error('The creation of new Stream key request failed')\n    }\n\n    async addSetKeyApi(\n        keyParameters: SetKeyParameters & { expire?: number },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            members: keyParameters.members.map((member) =>\n                stringToBuffer(member),\n            ),\n            expire: keyParameters?.expire,\n        }\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/set?encoding=buffer`,\n            requestBody,\n        )\n        if (response.status !== 201)\n            throw new Error('The creation of new Set key request failed')\n    }\n\n    async addZSetKeyApi(\n        keyParameters: {\n            keyName: string\n            members: Array<{ name: string; score: number }>\n            expire?: number\n        },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            members: keyParameters.members.map((member) => ({\n                name: stringToBuffer(member.name),\n                score: member.score,\n            })),\n            expire: keyParameters?.expire,\n        }\n\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/zSet?encoding=buffer`,\n            requestBody,\n        )\n\n        if (response.status !== 201) {\n            throw new Error('The creation of new ZSet key request failed')\n        }\n    }\n\n    async addJsonKeyApi(\n        keyParameters: { keyName: string; value: any; expire?: number },\n        databaseParameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        const databaseId = await this.databaseAPIRequests.getDatabaseIdByName(\n            databaseParameters.databaseName,\n        )\n        const requestBody: any = {\n            keyName: stringToBuffer(keyParameters.keyName),\n            data: JSON.stringify(keyParameters.value),\n        }\n\n        if (keyParameters.expire) {\n            requestBody.expire = keyParameters.expire\n        }\n\n        const response = await this.apiClient.post(\n            `/databases/${databaseId}/rejson-rl?encoding=buffer`,\n            requestBody,\n        )\n\n        if (response.status !== 201) {\n            throw new Error('The creation of new JSON key request failed')\n        }\n    }\n\n    async searchKeyByNameApi(\n        keyName: string,\n        databaseName: string,\n    ): Promise<string[]> {\n        const requestBody = {\n            cursor: '0',\n            match: keyName,\n        }\n        const databaseId =\n            await this.databaseAPIRequests.getDatabaseIdByName(databaseName)\n        const response = await this.apiClient.post(\n            bufferPathMask.replace('databaseId', databaseId),\n            requestBody,\n        )\n        if (response.status !== 200)\n            throw new Error('Getting key request failed')\n        return response.data[0].keys\n    }\n\n    async deleteKeyByNameApi(\n        keyName: string,\n        databaseName: string,\n    ): Promise<void> {\n        const databaseId =\n            await this.databaseAPIRequests.getDatabaseIdByName(databaseName)\n        const doesKeyExist = await this.searchKeyByNameApi(\n            keyName,\n            databaseName,\n        )\n        if (doesKeyExist.length > 0) {\n            const requestBody = { keyNames: [stringToBuffer(keyName)] }\n            const response = await this.apiClient.delete(\n                bufferPathMask.replace('databaseId', databaseId),\n                {\n                    data: requestBody,\n                },\n            )\n            if (response.status !== 200)\n                throw new Error('The deletion of the key request failed')\n        }\n    }\n}\n"
  },
  {
    "path": "tests/playwright/helpers/api/http-client.ts",
    "content": "import axios, { AxiosInstance } from 'axios'\nimport https from 'https'\n\nexport function generateApiClient(\n    apiUrl: string,\n    windowId?: string,\n): AxiosInstance {\n    const apiClient = axios.create({\n        baseURL: apiUrl,\n        headers: {\n            'X-Window-Id': windowId,\n        },\n        httpsAgent: new https.Agent({\n            rejectUnauthorized: false, // Allows self-signed/invalid SSL certs\n        }),\n    })\n\n    // Enable logging if DEBUG is set\n    if (process.env.DEBUG) {\n        this.apiClient.interceptors.request.use((request) => {\n            console.log('Starting Request', request)\n            return request\n        })\n        this.apiClient.interceptors.response.use(\n            (response) => {\n                console.log('Response:', response)\n                return response\n            },\n            (error) => {\n                console.error('Error Response:', error.response)\n                return Promise.reject(error)\n            },\n        )\n    }\n\n    return apiClient\n}\n"
  },
  {
    "path": "tests/playwright/helpers/conf.ts",
    "content": "import { faker } from '@faker-js/faker'\nimport * as os from 'os'\nimport * as fs from 'fs'\nimport { join as joinPath } from 'path'\nimport * as path from 'path'\nimport { AddNewDatabaseParameters } from '../types/databases'\n\n// Urls for using in the tests\nexport const commonUrl = process.env.COMMON_URL || 'https://localhost:5540'\nexport const apiUrl = process.env.API_URL || 'https://localhost:5540/api'\nexport const electronExecutablePath = process.env.ELECTRON_EXECUTABLE_PATH\nexport const isElectron = electronExecutablePath !== undefined\nexport const googleUser = process.env.GOOGLE_USER || ''\nexport const googleUserPassword = process.env.GOOGLE_USER_PASSWORD || ''\nexport const samlUser = process.env.E2E_SSO_EMAIL || ''\nexport const samlUserPassword = process.env.E2E_SSO_PASSWORD || ''\n\nexport const workingDirectory =\n    process.env.RI_APP_FOLDER_ABSOLUTE_PATH ||\n    joinPath(os.homedir(), process.env.RI_APP_FOLDER_NAME || '.redis-insight')\nexport const fileDownloadPath = joinPath(os.homedir(), 'Downloads')\nconst uniqueId = faker.string.alphanumeric({ length: 10 })\n\nexport const ossStandaloneConfig: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_HOST!,\n    port: process.env.OSS_STANDALONE_PORT!,\n    databaseName: `${process.env.OSS_STANDALONE_DATABASE_NAME || 'test_standalone'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_PASSWORD,\n}\n\nexport const ossStandaloneConfigEmpty: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_EMPTY_HOST,\n    port: process.env.OSS_STANDALONE_EMPTY_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_EMPTY_DATABASE_NAME || 'test_standalone_empty'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_EMPTY_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_EMPTY_PASSWORD,\n}\n\nexport const ossStandaloneV5Config: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_V5_HOST,\n    port: process.env.OSS_STANDALONE_V5_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_V5_DATABASE_NAME || 'test_standalone-v5'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_V5_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_V5_PASSWORD,\n}\n\nexport const ossStandaloneV7Config: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_V7_HOST,\n    port: process.env.OSS_STANDALONE_V7_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_V7_DATABASE_NAME || 'test_standalone-v7'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_V7_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_V7_PASSWORD,\n}\n\n// TODO: Rename this, please\nexport const ossStandaloneV6Config: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_V8_HOST,\n    port: process.env.OSS_STANDALONE_V8_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_V8_DATABASE_NAME || 'test_standalone-v6'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_V8_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_V8_PASSWORD,\n}\n\nexport const ossStandaloneRedisearch: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_REDISEARCH_HOST,\n    port: process.env.OSS_STANDALONE_REDISEARCH_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_REDISEARCH_DATABASE_NAME || 'test_standalone-redisearch'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_REDISEARCH_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_REDISEARCH_PASSWORD,\n}\n\nexport const ossClusterConfig: AddNewDatabaseParameters = {\n    ossClusterHost: process.env.OSS_CLUSTER_HOST,\n    ossClusterPort: process.env.OSS_CLUSTER_PORT,\n    ossClusterDatabaseName: `${process.env.OSS_CLUSTER_DATABASE_NAME || 'test_cluster'}-${uniqueId}`,\n}\n\nexport const ossSentinelConfig: AddNewDatabaseParameters = {\n    sentinelHost: process.env.OSS_SENTINEL_HOST,\n    sentinelPort: process.env.OSS_SENTINEL_PORT,\n    sentinelPassword: process.env.OSS_SENTINEL_PASSWORD,\n    masters: [\n        {\n            alias: `primary-group-1}-${uniqueId}`,\n            db: '0',\n            name: 'primary-group-1',\n            password: 'defaultpass',\n        },\n        {\n            alias: `primary-group-2}-${uniqueId}`,\n            db: '0',\n            name: 'primary-group-2',\n            password: 'defaultpass',\n        },\n    ],\n    name: ['primary-group-1', 'primary-group-2'],\n}\n\nexport const redisEnterpriseClusterConfig: AddNewDatabaseParameters = {\n    host: process.env.RE_CLUSTER_HOST,\n    port: process.env.RE_CLUSTER_PORT,\n    databaseName: process.env.RE_CLUSTER_DATABASE_NAME || 'test-re-standalone',\n    databaseUsername: process.env.RE_CLUSTER_ADMIN_USER || 'demo@redislabs.com',\n    databasePassword: process.env.RE_CLUSTER_ADMIN_PASSWORD || '123456',\n}\n\nexport const invalidOssStandaloneConfig: AddNewDatabaseParameters = {\n    host: 'oss-standalone-invalid',\n    port: '1010',\n    databaseName: `${process.env.OSS_STANDALONE_INVALID_DATABASE_NAME || 'test_standalone-invalid'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_INVALID_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_INVALID_PASSWORD,\n}\n\nexport const ossStandaloneBigConfig: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_BIG_HOST,\n    port: process.env.OSS_STANDALONE_BIG_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_BIG_DATABASE_NAME || 'test_standalone_big'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_BIG_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_BIG_PASSWORD,\n}\n\nexport const cloudDatabaseConfig: AddNewDatabaseParameters = {\n    host: process.env.E2E_CLOUD_DATABASE_HOST || '',\n    port: process.env.E2E_CLOUD_DATABASE_PORT || '',\n    databaseName: `${process.env.E2E_CLOUD_DATABASE_NAME || 'cloud-database'}-${uniqueId}`,\n    databaseUsername: process.env.E2E_CLOUD_DATABASE_USERNAME,\n    databasePassword: process.env.E2E_CLOUD_DATABASE_PASSWORD,\n    accessKey: process.env.E2E_CLOUD_API_ACCESS_KEY || '',\n    secretKey: process.env.E2E_CLOUD_API_SECRET_KEY || '',\n}\n\nexport const ossStandaloneNoPermissionsConfig: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_NOPERM_HOST,\n    port: process.env.OSS_STANDALONE_NOPERM_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_NOPERM_DATABASE_NAME || 'oss-standalone-no-permissions'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_NOPERM_USERNAME || 'noperm',\n    databasePassword: process.env.OSS_STANDALONE_NOPERM_PASSWORD,\n}\n\nexport const ossStandaloneForSSHConfig: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_SSH_HOST || '172.33.100.111',\n    port: process.env.OSS_STANDALONE_SSH_PORT || '6379',\n    databaseName: `${process.env.OSS_STANDALONE_SSH_DATABASE_NAME || 'oss-standalone-for-ssh'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_SSH_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_SSH_PASSWORD,\n}\n\nexport const ossClusterForSSHConfig: AddNewDatabaseParameters = {\n    host: process.env.OSS_CLUSTER_SSH_HOST || '172.31.100.211',\n    port: process.env.OSS_CLUSTER_SSH_PORT || '6379',\n    databaseName: `${process.env.OSS_CLUSTER_SSH_DATABASE_NAME || 'oss-cluster-for-ssh'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_CLUSTER_SSH_USERNAME,\n    databasePassword: process.env.OSS_CLUSTER_SSH_PASSWORD,\n}\n\nexport const ossStandaloneTlsConfig: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_TLS_HOST,\n    port: process.env.OSS_STANDALONE_TLS_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_TLS_DATABASE_NAME || 'test_standalone_tls'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_TLS_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_TLS_PASSWORD,\n    caCert: {\n        name: `ca}-${uniqueId}`,\n        certificate:\n            process.env.E2E_CA_CRT ||\n            fs.readFileSync(\n                path.resolve(\n                    __dirname,\n                    '../../e2e/rte/oss-standalone-tls/certs/redisCA.crt',\n                ),\n                'utf-8',\n            ),\n    },\n    clientCert: {\n        name: `client}-${uniqueId}`,\n        certificate:\n            process.env.E2E_CLIENT_CRT ||\n            fs.readFileSync(\n                path.resolve(\n                    __dirname,\n                    '../../e2e/rte/oss-standalone-tls/certs/redis.crt',\n                ),\n                'utf-8',\n            ),\n        key:\n            process.env.E2E_CLIENT_KEY ||\n            fs.readFileSync(\n                path.resolve(\n                    __dirname,\n                    '../../e2e/rte/oss-standalone-tls/certs/redis.key',\n                ),\n                'utf-8',\n            ),\n    },\n}\n\nexport const ossStandaloneRedisGears: AddNewDatabaseParameters = {\n    host: process.env.OSS_STANDALONE_REDISGEARS_HOST,\n    port: process.env.OSS_STANDALONE_REDISGEARS_PORT,\n    databaseName: `${process.env.OSS_STANDALONE_REDISGEARS_DATABASE_NAME || 'test_standalone_redisgears'}-${uniqueId}`,\n    databaseUsername: process.env.OSS_STANDALONE_REDISGEARS_USERNAME,\n    databasePassword: process.env.OSS_STANDALONE_REDISGEARS_PASSWORD,\n}\n\nexport const ossClusterRedisGears: AddNewDatabaseParameters = {\n    ossClusterHost: process.env.OSS_CLUSTER_REDISGEARS_2_HOST,\n    ossClusterPort: process.env.OSS_CLUSTER_REDISGEARS_2_PORT,\n    ossClusterDatabaseName: `${process.env.OSS_CLUSTER_REDISGEARS_2_NAME || 'test_cluster-gears-2.0'}-${uniqueId}`,\n}\n"
  },
  {
    "path": "tests/playwright/helpers/constants.ts",
    "content": "export enum KeyTypesTexts {\n    Hash = 'Hash',\n    List = 'List',\n    Set = 'Set',\n    ZSet = 'Sorted Set',\n    String = 'String',\n    ReJSON = 'JSON',\n    Stream = 'Stream',\n    Graph = 'Graph',\n    TimeSeries = 'Time Series',\n}\nexport const keyLength = 50\n\nexport const COMMANDS_TO_CREATE_KEY = Object.freeze({\n    [KeyTypesTexts.Hash]: (\n        key: string,\n        value: string | number = 'value',\n        field: string | number = 'field',\n    ) => `HSET ${key} '${field}' '${value}'`,\n    [KeyTypesTexts.List]: (key: string, element: string | number = 'element') =>\n        `LPUSH ${key} '${element}'`,\n    [KeyTypesTexts.Set]: (key: string, member = 'member') =>\n        `SADD ${key} '${member}'`,\n    [KeyTypesTexts.ZSet]: (key: string, member = 'member', score = 1) =>\n        `ZADD ${key} ${score} '${member}'`,\n    [KeyTypesTexts.String]: (key: string, value = 'val') =>\n        `SET ${key} '${value}'`,\n    [KeyTypesTexts.ReJSON]: (key: string, json = '\"val\"') =>\n        `JSON.SET ${key} . '${json}'`,\n    [KeyTypesTexts.Stream]: (\n        key: string,\n        value: string | number = 'value',\n        field: string | number = 'field',\n    ) => `XADD ${key} * '${field}' '${value}'`,\n    [KeyTypesTexts.Graph]: (key: string) => `GRAPH.QUERY ${key} \"CREATE ()\"`,\n    [KeyTypesTexts.TimeSeries]: (key: string) => `TS.CREATE ${key}`,\n})\n\nexport enum RTE {\n    none = 'none',\n    standalone = 'standalone',\n    sentinel = 'sentinel',\n    ossCluster = 'oss-cluster',\n    reCluster = 're-cluster',\n    reCloud = 're-cloud',\n}\n\nexport enum ENV {\n    web = 'web',\n    desktop = 'desktop',\n}\n\nexport enum RecommendationIds {\n    redisVersion = 'redisVersion',\n    searchVisualization = 'searchVisualization',\n    setPassword = 'setPassword',\n    optimizeTimeSeries = 'RTS',\n    luaScript = 'luaScript',\n    useSmallerKeys = 'useSmallerKeys',\n    avoidLogicalDatabases = 'avoidLogicalDatabases',\n    searchJson = 'searchJSON',\n    rdi = 'tryRDI',\n}\n\nexport enum LibrariesSections {\n    Functions = 'Functions',\n    KeyspaceTriggers = 'Keyspace',\n    ClusterFunctions = 'Cluster',\n    StreamFunctions = 'Stream',\n}\n\nexport enum FunctionsSections {\n    General = 'General',\n    Flag = 'Flag',\n}\n\nexport enum MonacoEditorInputs {\n    // add library fields\n    Code = 'code-value',\n    Configuration = 'configuration-value',\n    // added library fields\n    Library = 'library-code',\n    LibraryConfiguration = 'library-configuration',\n}\n\nexport enum ResourcePath {\n    Databases = '/databases',\n    RedisSentinel = '/redis-sentinel',\n    ClusterDetails = '/cluster-details',\n    SyncFeatures = '/features/sync',\n    Rdi = '/rdi',\n}\n\nexport enum ExploreTabs {\n    Tutorials = 'Tutorials',\n    Tips = 'Tips',\n}\n\nexport enum Compatibility {\n    SearchAndQuery = 'search',\n    Json = 'json',\n    TimeSeries = 'time-series',\n}\n\nexport enum ChatBotTabs {\n    General = 'General',\n    Database = 'Database',\n}\n\nexport enum RedisOverviewPage {\n    DataBase = 'Redis Databases',\n    Rdi = 'My RDI instances',\n}\n\nexport enum TextConnectionSection {\n    Success = 'success',\n    Failed = 'failed',\n}\n\nexport enum RdiTemplatePipelineType {\n    Ingest = 'ingest',\n    WriteBehind = 'write-behind',\n}\n\nexport enum RdiTemplateDatabaseType {\n    SqlServer = 'sql',\n    Oracle = 'oracle',\n    MySql = 'mysql',\n}\n\nexport enum RdiPopoverOptions {\n    Server = 'server',\n    File = 'file',\n    Pipeline = 'empty',\n}\n\nexport enum TlsCertificates {\n    CA = 'ca',\n    Client = 'client',\n}\n\nexport enum AddElementInList {\n    Head,\n    Tail,\n}\n"
  },
  {
    "path": "tests/playwright/helpers/utils.ts",
    "content": "import { expect, Page } from '@playwright/test'\n\nimport { DatabaseAPIRequests } from './api/api-databases'\nimport { ossStandaloneConfig } from './conf'\nimport { AddNewDatabaseParameters } from '../types/databases'\n\nexport async function addStandaloneInstanceAndNavigateToIt(\n    page: Page,\n    databaseService: DatabaseAPIRequests,\n    config: AddNewDatabaseParameters = ossStandaloneConfig,\n): Promise<() => Promise<void>> {\n    // Add a new standalone database\n    databaseService.addNewStandaloneDatabaseApi(config)\n\n    page.reload()\n\n    return async function cleanup() {\n        try {\n            await databaseService.deleteStandaloneDatabaseApi(config)\n        } catch (error) {\n            console.warn('Error during cleanup:', error)\n        }\n    }\n}\n\nexport async function navigateToStandaloneInstance(\n    page: Page,\n    config: AddNewDatabaseParameters = ossStandaloneConfig,\n): Promise<void> {\n    // Click on the added database\n    const dbItems = page.locator('[data-testid^=\"instance-name\"]')\n    const db = dbItems.filter({\n        hasText: config.databaseName?.trim(),\n    })\n    await expect(db).toBeVisible({ timeout: 5000 })\n    await db.first().click()\n}\n\nexport function stringToBuffer(str: string): Buffer {\n    return Buffer.from(str, 'utf-8')\n}\n"
  },
  {
    "path": "tests/playwright/package.json",
    "content": "{\n  \"name\": \"playwright\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@faker-js/faker\": \"^8.4.1\",\n    \"@playwright/test\": \"^1.52.0\",\n    \"@types/node\": \"^22.15.29\",\n    \"allure-commandline\": \"^2.33.0\",\n    \"allure-js-commons\": \"^3.2.0\",\n    \"allure-playwright\": \"^3.2.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"fishery\": \"^2.3.1\",\n    \"nyc\": \"^17.1.0\"\n  },\n  \"scripts\": {\n    \"removeReportDirs\": \"rm -rf allure-results playwright-report test-results\",\n    \"allTests\": \"playwright test\",\n    \"generateReports\": \"allure generate --clean\",\n    \"test:chromium:docker\": \"cross-env envPath=env/.docker.env yarn playwright test --project=Chromium\",\n    \"test:chromium:docker:debug\": \"yarn test:chromium:docker --debug\",\n    \"test:chromium:local-web\": \"cross-env envPath=env/.local-web.env yarn playwright test --project=Chromium\",\n    \"test:chromium:local-web:debug\": \"yarn test:chromium:local-web --debug\",\n    \"test:electron\": \"cross-env envPath=env/.desktop.env yarn playwright test --project=Chromium\",\n    \"test:electron:debug\": \"yarn test:electron --debug\",\n    \"test:coverage\": \"cross-env COLLECT_COVERAGE=true yarn playwright test; yarn coverage\",\n    \"coverage\": \"npx nyc report --reporter=html --reporter=lcov --reporter=text\",\n    \"coverage:clean\": \"rm -rf .nyc_output coverage\",\n    \"clean:results\": \"rm -rf allure-results\",\n    \"prep:history\": \"if [ -d allure-report/history ]; then cp -R allure-report/history allure-results; fi\",\n    \"test:allureHistoryReport\": \"yarn run prep:history && yarn allTests && yarn allure generate --clean -o allure-report allure-results\",\n    \"test:electron:allureHistoryReport\": \"yarn run prep:history && yarn test:electron && yarn allure generate --clean -o allure-report allure-results\",\n    \"generateAndShowReports\": \"allure serve allure-results\",\n    \"test:autogen\": \"playwright codegen\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^1.13.5\",\n    \"dotenv\": \"^16.4.7\",\n    \"dotenv-cli\": \"^8.0.0\",\n    \"fs-extra\": \"^11.3.0\",\n    \"module-alias\": \"^2.2.3\",\n    \"node-color-log\": \"^12.0.1\",\n    \"sqlite3\": \"^5.1.7\"\n  },\n  \"_moduleAliases\": {\n    \"@redislabsdev/redis-ui-icons\": \"../../node_modules/@redis-ui/icons\",\n    \"@redislabsdev/redis-ui-styles\": \"../../node_modules/@redis-ui/styles\",\n    \"@redislabsdev/redis-ui-components\": \"../../node_modules/@redis-ui/components\",\n    \"@redislabsdev/redis-ui-table\": \"../../node_modules/@redis-ui/table\"\n  }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/auto-discover-redis-enterprise-databases.ts",
    "content": "import { Page, Locator } from '@playwright/test'\nimport { BasePage } from './base-page'\n\nexport class AutoDiscoverREDatabases extends BasePage {\n    // BUTTONS\n    readonly addSelectedDatabases: Locator\n\n    readonly databaseCheckbox: Locator\n\n    readonly search: Locator\n\n    readonly viewDatabasesButton: Locator\n\n    // TEXT INPUTS\n    readonly title: Locator\n\n    readonly databaseName: Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.page = page\n        this.addSelectedDatabases = page.getByTestId('btn-add-databases')\n        this.databaseCheckbox = page.locator(\n            '[data-test-subj^=\"checkboxSelectRow\"]',\n        )\n        this.search = page.getByTestId('search')\n        this.viewDatabasesButton = page.getByTestId('btn-view-databases')\n        this.title = page.getByTestId('title')\n        this.databaseName = page.locator('[data-testid^=\"db_name_\"]')\n    }\n\n    // Get databases name\n    async getDatabaseName(): Promise<string | null> {\n        return this.databaseName.textContent()\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/base-overview-page.ts",
    "content": "/* eslint-disable no-await-in-loop */\n/* eslint-disable no-restricted-syntax */\nimport { expect, Locator, Page } from '@playwright/test'\nimport { Toast } from './components/common/toast'\nimport { BasePage } from './base-page'\nimport { RedisOverviewPage } from '../helpers/constants'\nimport { DatabasesForImport } from '../types'\n\nexport class BaseOverviewPage extends BasePage {\n    // Component instance used in methods\n    toast: Toast\n\n    // BUTTONS & ACTION SELECTORS\n    readonly deleteRowButton: Locator\n\n    readonly confirmDeleteButton: Locator\n\n    readonly confirmDeleteAllDbButton: Locator\n\n    // TABLE / LIST SELECTORS\n    readonly instanceRow: Locator\n\n    readonly selectAllCheckbox: Locator\n\n    readonly deleteButtonInPopover: Locator\n\n    dbNameList: Locator\n\n    readonly tableRowContent: Locator\n\n    readonly editDatabaseButton: Locator\n\n    // NAVIGATION SELECTORS\n    readonly databasePageLink: Locator\n\n    readonly rdiPageLink: Locator\n\n    // Additional – used for deletion by name\n    readonly deleteDatabaseButton: Locator\n\n    // MODULE\n    readonly moduleTooltip: Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.toast = new Toast(page)\n\n        // BUTTONS & ACTION SELECTORS\n        this.deleteRowButton = page.locator('[data-testid^=\"delete-instance-\"]')\n        this.confirmDeleteButton = page.locator(\n            '[data-testid^=\"delete-instance-\"]',\n            { hasText: 'Remove' },\n        )\n        this.confirmDeleteAllDbButton = page.getByTestId('delete-selected-dbs')\n\n        // TABLE / LIST SELECTORS\n        this.instanceRow = page.locator('[class*=euiTableRow-isSelectable]')\n        this.selectAllCheckbox = page.locator(\n            '[data-test-subj=\"checkboxSelectAll\"]',\n        )\n        this.deleteButtonInPopover = page.locator('#deletePopover button')\n        this.dbNameList = page.locator('[data-testid^=\"instance-name\"]')\n        this.tableRowContent = page.locator(\n            '[data-test-subj=\"database-alias-column\"]',\n        )\n        this.editDatabaseButton = page.locator('[data-testid^=\"edit-instance\"]')\n\n        // NAVIGATION SELECTORS\n        this.databasePageLink = page.getByTestId('home-tab-databases')\n        this.rdiPageLink = page.getByTestId('home-tab-rdi-instances')\n\n        // Additional – we alias deleteDatabaseButton to the same as deleteRowButton\n        this.deleteDatabaseButton = page.locator(\n            '[data-testid^=\"delete-instance-\"]',\n        )\n\n        // MODULE\n        this.moduleTooltip = page.locator('.euiToolTipPopover')\n    }\n\n    async reloadPage(): Promise<void> {\n        await this.page.reload()\n    }\n\n    async setActivePage(type: RedisOverviewPage): Promise<void> {\n        if (type === RedisOverviewPage.Rdi) {\n            await this.rdiPageLink.click()\n        } else {\n            await this.databasePageLink.click()\n        }\n    }\n\n    async deleteAllInstance(): Promise<void> {\n        const count = await this.instanceRow.count()\n        if (count > 1) {\n            await this.selectAllCheckbox.click()\n            await this.deleteButtonInPopover.click()\n            await this.confirmDeleteAllDbButton.click()\n        } else if (count === 1) {\n            await this.deleteDatabaseButton.click()\n            await this.confirmDeleteButton.click()\n        }\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n    }\n\n    async deleteDatabaseByName(dbName: string): Promise<void> {\n        const count = await this.tableRowContent.count()\n        for (let i = 0; i < count; i += 1) {\n            const text = (await this.tableRowContent.nth(i).textContent()) || ''\n            if (text.includes(dbName)) {\n                // Assumes that the delete button for the row is located at index i-1.\n                await this.deleteRowButton.nth(i - 1).click()\n                await this.confirmDeleteButton.click()\n                break\n            }\n        }\n    }\n\n    async clickOnDBByName(dbName: string): Promise<void> {\n        const db = this.dbNameList.filter({ hasText: dbName.trim() })\n        await expect(db).toBeVisible({ timeout: 10000 })\n        await db.first().click()\n    }\n\n    async clickOnEditDBByName(databaseName: string): Promise<void> {\n        const count = await this.dbNameList.count()\n        for (let i = 0; i < count; i += 1) {\n            const text = (await this.dbNameList.nth(i).textContent()) || ''\n            if (text.includes(databaseName)) {\n                await this.editDatabaseButton.nth(i).click()\n                break\n            }\n        }\n    }\n\n    async checkModulesInTooltip(moduleNameList: string[]): Promise<void> {\n        for (const item of moduleNameList) {\n            await expect(\n                this.moduleTooltip.locator('span', { hasText: `${item} v.` }),\n            ).toBeVisible()\n        }\n    }\n\n    async checkModulesOnPage(moduleList: Locator[]): Promise<void> {\n        for (const item of moduleList) {\n            await expect(item).toBeVisible()\n        }\n    }\n\n    async getAllDatabases(): Promise<string[]> {\n        const databases: string[] = []\n        await expect(this.dbNameList).toBeVisible()\n        const n = await this.dbNameList.count()\n        for (let k = 0; k < n; k += 1) {\n            const name = await this.dbNameList.nth(k).textContent()\n            databases.push(name || '')\n        }\n        return databases\n    }\n\n    async compareInstances(\n        actualList: string[],\n        sortedList: string[],\n    ): Promise<void> {\n        for (let k = 0; k < actualList.length; k += 1) {\n            await expect(actualList[k].trim()).toEqual(sortedList[k].trim())\n        }\n    }\n\n    getDatabaseNamesFromListByResult(\n        listOfDb: DatabasesForImport,\n        result: string,\n    ): string[] {\n        return listOfDb\n            .filter((element) => element.result === result)\n            .map((item) => item.name!)\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/base-page.ts",
    "content": "import { Locator, Page, expect } from '@playwright/test'\n\nexport class BasePage {\n    page: Page\n\n    constructor(page: Page) {\n        this.page = page\n    }\n\n    async reload(): Promise<void> {\n        await this.page.reload()\n    }\n\n    async navigateTo(url: string): Promise<void> {\n        await this.page.goto(url)\n    }\n\n    async navigateToHomeUrl(): Promise<void> {\n        await this.page.goto('/')\n    }\n\n    async click(locator: Locator): Promise<void> {\n        await locator.click()\n    }\n\n    async fill(selector: string, value: string): Promise<void> {\n        await this.page.fill(selector, value)\n    }\n\n    async getText(locator: Locator): Promise<string | null> {\n        return locator.textContent()\n    }\n\n    async isVisible(selctor: string): Promise<boolean> {\n        return this.page.locator(selctor).isVisible()\n    }\n\n    async getByTestId(testId: string): Promise<Locator> {\n        return this.page.getByTestId(testId)\n    }\n\n    async waitForLocatorVisible(locator: Locator, timeout = 6000) {\n        await expect(locator).toBeVisible({ timeout })\n    }\n\n    async waitForLocatorNotVisible(locator: Locator, timeout = 6000) {\n        await expect(locator).not.toBeVisible({ timeout })\n    }\n\n    async goBackHistor(): Promise<void> {\n        await this.page.goBack()\n    }\n\n    async elementExistsSelector(selector: string): Promise<boolean> {\n        const count = await this.page.locator(selector).count()\n        return count > 0\n    }\n\n    async elementExistsLocator(locator: Locator): Promise<boolean> {\n        const count = await locator.count()\n        return count > 0\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/browser-page.ts",
    "content": "/* eslint-disable no-restricted-syntax */\n/* eslint-disable no-await-in-loop */\n/* eslint-disable @typescript-eslint/lines-between-class-members */\nimport { expect, Locator, Page } from '@playwright/test'\nimport { Toast } from './components/common/toast'\n\nimport { BasePage } from './base-page'\nimport { AddElementInList } from '../helpers/constants'\n\nexport class BrowserPage extends BasePage {\n    private toast: Toast\n\n    // CSS Selectors\n    public readonly browserPage: Locator\n    public readonly browserTab: Locator\n    public readonly cssSelectorGrid: Locator\n    public readonly cssSelectorRows: Locator\n    public readonly cssSelectorKey: Locator\n    public readonly cssFilteringLabel: Locator\n    public readonly cssJsonValue: Locator\n    public readonly cssRowInVirtualizedTable: Locator\n    public readonly cssVirtualTableRow: Locator\n    public readonly cssKeyBadge: Locator\n    public readonly cssKeyTtl: Locator\n    public readonly cssKeySize: Locator\n    public readonly cssRemoveSuggestionItem: Locator\n\n    // BUTTONS\n    public readonly applyButton: Locator\n    public readonly cancelButton: Locator\n    public readonly deleteKeyButton: Locator\n    public readonly submitDeleteKeyButton: Locator\n    public readonly confirmDeleteKeyButton: Locator\n    public readonly editKeyTTLButton: Locator\n    public readonly refreshKeysButton: Locator\n    public readonly refreshKeyButton: Locator\n    public readonly editKeyNameButton: Locator\n    public readonly editKeyValueButton: Locator\n    public readonly closeKeyButton: Locator\n    public readonly plusAddKeyButton: Locator\n    public readonly addKeyValueItemsButton: Locator\n    public readonly saveHashFieldButton: Locator\n    public readonly saveMemberButton: Locator\n    public readonly searchButtonInKeyDetails: Locator\n    public readonly addKeyButton: Locator\n    public readonly keyTypeDropDown: Locator\n    public readonly confirmRemoveHashFieldButton: Locator\n    public readonly removeSetMemberButton: Locator\n    public readonly removeHashFieldButton: Locator\n    public readonly removeZsetMemberButton: Locator\n    public readonly confirmRemoveSetMemberButton: Locator\n    public readonly confirmRemoveZSetMemberButton: Locator\n    public readonly saveElementButton: Locator\n    public readonly removeElementFromListIconButton: Locator\n    public readonly removeElementFromListButton: Locator\n    public readonly confirmRemoveListElementButton: Locator\n    public readonly removeElementFromListSelect: Locator\n    public readonly addJsonObjectButton: Locator\n    public readonly addJsonFieldButton: Locator\n    public readonly expandJsonObject: Locator\n    public readonly scoreButton: Locator\n    public readonly sortingButton: Locator\n    public readonly editJsonObjectButton: Locator\n    public readonly applyEditButton: Locator\n    public readonly cancelEditButton: Locator\n    public readonly scanMoreButton: Locator\n    public readonly resizeBtnKeyList: Locator\n    public readonly treeViewButton: Locator\n    public readonly browserViewButton: Locator\n    public readonly searchButton: Locator\n    public readonly clearFilterButton: Locator\n    public readonly fullScreenModeButton: Locator\n    public readonly closeRightPanel: Locator\n    public readonly addNewStreamEntry: Locator\n    public readonly removeEntryButton: Locator\n    public readonly confirmRemoveEntryButton: Locator\n    public readonly clearStreamEntryInputs: Locator\n    public readonly saveGroupsButton: Locator\n    public readonly acknowledgeButton: Locator\n    public readonly confirmAcknowledgeButton: Locator\n    public readonly claimPendingMessageButton: Locator\n    public readonly submitButton: Locator\n    public readonly consumerDestinationSelect: Locator\n    public readonly removeConsumerButton: Locator\n    public readonly removeConsumerGroupButton: Locator\n    public readonly optionalParametersSwitcher: Locator\n    public readonly forceClaimCheckbox: Locator\n    public readonly editStreamLastIdButton: Locator\n    public readonly saveButton: Locator\n    public readonly bulkActionsButton: Locator\n    public readonly editHashButton: Locator\n    public readonly editHashFieldTtlButton: Locator\n    public readonly editZsetButton: Locator\n    public readonly editListButton: Locator\n    public readonly cancelStreamGroupBtn: Locator\n    public readonly patternModeBtn: Locator\n    public readonly redisearchModeBtn: Locator\n    public readonly showFilterHistoryBtn: Locator\n    public readonly clearFilterHistoryBtn: Locator\n    public readonly loadSampleDataBtn: Locator\n    public readonly executeBulkKeyLoadBtn: Locator\n    public readonly backToBrowserBtn: Locator\n    public readonly loadAllBtn: Locator\n    public readonly downloadAllValueBtn: Locator\n    public readonly openTutorialsBtn: Locator\n    public readonly keyItem: Locator\n    public readonly columnsBtn: Locator\n\n    // CONTAINERS\n    public readonly streamGroupsContainer: Locator\n    public readonly streamConsumersContainer: Locator\n    public readonly breadcrumbsContainer: Locator\n    public readonly virtualTableContainer: Locator\n    public readonly streamEntriesContainer: Locator\n    public readonly streamMessagesContainer: Locator\n    public readonly loader: Locator\n    public readonly newIndexPanel: Locator\n\n    // LINKS\n    public readonly internalLinkToWorkbench: Locator\n    public readonly userSurveyLink: Locator\n    public readonly redisearchFreeLink: Locator\n    public readonly guideLinksBtn: Locator\n\n    // OPTION ELEMENTS\n    public readonly stringOption: Locator\n    public readonly jsonOption: Locator\n    public readonly setOption: Locator\n    public readonly zsetOption: Locator\n    public readonly listOption: Locator\n    public readonly hashOption: Locator\n    public readonly streamOption: Locator\n    public readonly removeFromHeadSelection: Locator\n    public readonly filterOptionType: Locator\n    public readonly filterByKeyTypeDropDown: Locator\n    public readonly filterAllKeyType: Locator\n    public readonly consumerOption: Locator\n    public readonly claimTimeOptionSelect: Locator\n    public readonly relativeTimeOption: Locator\n    public readonly timestampOption: Locator\n    public readonly formatSwitcher: Locator\n    public readonly formatSwitcherIcon: Locator\n    public readonly refreshIndexButton: Locator\n    public readonly selectIndexDdn: Locator\n    public readonly createIndexBtn: Locator\n    public readonly cancelIndexCreationBtn: Locator\n    public readonly confirmIndexCreationBtn: Locator\n    public readonly resizeTrigger: Locator\n    public readonly filterHistoryOption: Locator\n    public readonly filterHistoryItemText: Locator\n\n    // TABS\n    public readonly streamTabGroups: Locator\n    public readonly streamTabConsumers: Locator\n    public readonly streamTabs: Locator\n\n    // TEXT INPUTS\n    public readonly addKeyNameInput: Locator\n    public readonly keyNameInput: Locator\n    public readonly keyTTLInput: Locator\n    public readonly editKeyTTLInput: Locator\n    public readonly ttlText: Locator\n    public readonly hashFieldValueInput: Locator\n    public readonly hashFieldNameInput: Locator\n    public readonly hashFieldValueEditor: Locator\n    public readonly hashTtlFieldInput: Locator\n    public readonly listKeyElementEditorInput: Locator\n    public readonly stringKeyValueInput: Locator\n    public readonly jsonKeyValueInput: Locator\n    public readonly jsonUploadInput: Locator\n    public readonly setMemberInput: Locator\n    public readonly zsetMemberScoreInput: Locator\n    public readonly filterByPatterSearchInput: Locator\n    public readonly hashFieldInput: Locator\n    public readonly hashValueInput: Locator\n    public readonly searchInput: Locator\n    public readonly jsonKeyInput: Locator\n    public readonly jsonValueInput: Locator\n    public readonly countInput: Locator\n    public readonly streamEntryId: Locator\n    public readonly streamField: Locator\n    public readonly streamValue: Locator\n    public readonly addAdditionalElement: Locator\n    public readonly streamFieldsValues: Locator\n    public readonly streamEntryIDDateValue: Locator\n    public readonly groupNameInput: Locator\n    public readonly consumerIdInput: Locator\n    public readonly streamMinIdleTimeInput: Locator\n    public readonly claimIdleTimeInput: Locator\n    public readonly claimRetryCountInput: Locator\n    public readonly lastIdInput: Locator\n    public readonly inlineItemEditor: Locator\n    public readonly indexNameInput: Locator\n    public readonly prefixFieldInput: Locator\n    public readonly indexIdentifierInput: Locator\n\n    // TEXT ELEMENTS\n    public readonly keySizeDetails: Locator\n    public readonly keyLengthDetails: Locator\n    public readonly keyNameInTheList: Locator\n    public readonly hashFieldsList: Locator\n    public readonly hashValuesList: Locator\n    public readonly hashField: Locator\n    public readonly hashFieldValue: Locator\n    public readonly setMembersList: Locator\n    public readonly zsetMembersList: Locator\n    public readonly zsetScoresList: Locator\n    public readonly listElementsList: Locator\n    public readonly jsonKeyValue: Locator\n    public readonly jsonError: Locator\n    public readonly tooltip: Locator\n    public readonly dialog: Locator\n    public readonly noResultsFound: Locator\n    public readonly noResultsFoundOnly: Locator\n    public readonly searchAdvices: Locator\n    public readonly keysNumberOfResults: Locator\n    public readonly scannedValue: Locator\n    public readonly totalKeysNumber: Locator\n    public readonly keyDetailsBadge: Locator\n    public readonly modulesTypeDetails: Locator\n    public readonly filteringLabel: Locator\n    public readonly keysSummary: Locator\n    public readonly multiSearchArea: Locator\n    public readonly keyDetailsHeader: Locator\n    public readonly keyListTable: Locator\n    public readonly keyListMessage: Locator\n    public readonly keyDetailsTable: Locator\n    public readonly keyNameFormDetails: Locator\n    public readonly keyDetailsTTL: Locator\n    public readonly progressLine: Locator\n    public readonly progressKeyList: Locator\n    public readonly jsonScalarValue: Locator\n    public readonly noKeysToDisplayText: Locator\n    public readonly streamEntryDate: Locator\n    public readonly streamEntryIdValue: Locator\n    public readonly streamFields: Locator\n    public readonly streamVirtualContainer: Locator\n    public readonly streamEntryFields: Locator\n    public readonly confirmationMessagePopover: Locator\n    public readonly streamGroupId: Locator\n    public readonly streamGroupName: Locator\n    public readonly streamMessage: Locator\n    public readonly streamConsumerName: Locator\n    public readonly consumerGroup: Locator\n    public readonly entryIdInfoIcon: Locator\n    public readonly entryIdError: Locator\n    public readonly pendingCount: Locator\n    public readonly streamRangeBar: Locator\n    public readonly rangeLeftTimestamp: Locator\n    public readonly rangeRightTimestamp: Locator\n    public readonly jsonValue: Locator\n    public readonly stringValueAsJson: Locator\n\n    // POPUPS\n    public readonly changeValueWarning: Locator\n\n    // TABLE\n    public readonly keyListItem: Locator\n\n    // DIALOG\n    public readonly noReadySearchDialogTitle: Locator\n\n    // CHECKBOXES\n    public readonly showTtlCheckbox: Locator\n    public readonly showTtlColumnCheckbox: Locator\n    public readonly showSizeColumnCheckbox: Locator\n\n    // UTILITY FUNCTIONS\n    public readonly getHashTtlFieldInput: (fieldName: string) => Locator\n    public readonly getListElementInput: (count: number) => Locator\n    public readonly getKeySize: (keyName: string) => Locator\n    public readonly getKeyTTl: (keyName: string) => Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.page = page\n        this.toast = new Toast(page)\n\n        // CSS Selectors\n        this.browserTab = page.getByRole('tab', { name: 'Browse' })\n        this.browserPage = page.getByTestId('browser-page')\n        this.cssSelectorGrid = page.locator('[aria-label=\"grid\"]')\n        this.cssSelectorRows = page.locator('[aria-label=\"row\"]')\n        this.cssSelectorKey = page.locator('[data-testid^=\"key-\"]')\n        this.cssFilteringLabel = page.getByTestId('multi-search')\n        this.cssJsonValue = page.getByTestId('value-as-json')\n        this.cssRowInVirtualizedTable = page.locator('[role=\"gridcell\"]')\n        this.cssVirtualTableRow = page.locator('[aria-label=\"row\"]')\n        this.cssKeyBadge = page.locator('[data-testid^=\"badge-\"]')\n        this.cssKeyTtl = page.locator('[data-testid^=\"ttl-\"]')\n        this.cssKeySize = page.locator('[data-testid^=\"size-\"]')\n        this.cssRemoveSuggestionItem = page.locator(\n            '[data-testid^=\"remove-suggestion-item-\"]',\n        )\n\n        // BUTTONS\n        this.applyButton = page.getByTestId('apply-btn')\n        this.cancelButton = page.getByTestId('cancel-btn')\n        this.deleteKeyButton = page.getByTestId('delete-key-btn')\n        this.submitDeleteKeyButton = page.getByTestId('submit-delete-key')\n        this.confirmDeleteKeyButton = page.getByTestId('delete-key-confirm-btn')\n        this.editKeyTTLButton = page.getByTestId('edit-ttl-btn')\n        this.refreshKeysButton = page.getByTestId('keys-refresh-btn')\n        this.refreshKeyButton = page.getByTestId('key-refresh-btn')\n        this.editKeyNameButton = page.getByTestId('edit-key-btn')\n        this.editKeyValueButton = page.getByTestId('edit-key-value-btn')\n        this.closeKeyButton = page.getByTestId('close-key-btn')\n        this.plusAddKeyButton = page.getByTestId('btn-add-key')\n        this.addKeyValueItemsButton = page.getByTestId(\n            'add-key-value-items-btn',\n        )\n        this.saveHashFieldButton = page.getByTestId('save-fields-btn')\n        this.saveMemberButton = page.getByTestId('save-members-btn')\n        this.searchButtonInKeyDetails = page.getByTestId('search-button')\n        this.addKeyButton = page.locator('button', {\n            hasText: /^Add Key$/,\n        })\n        this.keyTypeDropDown = page.locator(\n            'fieldset button.euiSuperSelectControl',\n        )\n        this.confirmRemoveHashFieldButton = page.locator(\n            '[data-testid^=\"remove-hash-button-\"] span',\n        )\n        this.removeSetMemberButton = page.getByTestId('set-remove-btn')\n        this.removeHashFieldButton = page.getByTestId('remove-hash-button')\n        this.removeZsetMemberButton = page.getByTestId('zset-remove-button')\n        this.confirmRemoveSetMemberButton = page.locator(\n            '[data-testid^=\"set-remove-btn-\"] span',\n        )\n        this.confirmRemoveZSetMemberButton = page.locator(\n            '[data-testid^=\"zset-remove-button-\"] span',\n        )\n        this.saveElementButton = page.getByTestId('save-elements-btn')\n        this.removeElementFromListIconButton = page.getByTestId(\n            'remove-key-value-items-btn',\n        )\n        this.removeElementFromListButton = page.getByTestId(\n            'remove-elements-btn',\n        )\n        this.confirmRemoveListElementButton = page.getByTestId('remove-submit')\n        this.removeElementFromListSelect =\n            page.getByTestId('destination-select')\n        this.addJsonObjectButton = page.getByTestId('add-object-btn')\n        this.addJsonFieldButton = page.getByTestId('add-field-btn')\n        this.expandJsonObject = page.getByTestId('expand-object')\n        this.scoreButton = page.getByTestId('score-button')\n        this.sortingButton = page.getByTestId('header-sorting-button')\n        this.editJsonObjectButton = page.getByTestId('edit-json-field')\n        this.applyEditButton = page.getByTestId('apply-edit-btn')\n        this.cancelEditButton = page.getByTestId('cancel-edit-btn')\n        this.scanMoreButton = page.getByTestId('scan-more')\n        this.resizeBtnKeyList = page.locator(\n            '[data-test-subj=\"resize-btn-keyList-keyDetails\"]',\n        )\n        this.treeViewButton = page.getByTestId('view-type-list-btn')\n        this.browserViewButton = page.getByTestId('view-type-browser-btn')\n        this.searchButton = page.getByTestId('search-btn')\n        this.clearFilterButton = page.getByTestId('reset-filter-btn')\n        this.fullScreenModeButton = page.getByTestId('toggle-full-screen')\n        this.closeRightPanel = page.getByTestId('close-right-panel-btn')\n        this.addNewStreamEntry = page.getByTestId('add-key-value-items-btn')\n        this.removeEntryButton = page.locator(\n            '[data-testid^=\"remove-entry-button-\"]',\n        )\n        this.confirmRemoveEntryButton = page\n            .locator('[data-testid^=\"remove-entry-button-\"]')\n            .filter({ hasText: 'Remove' })\n        this.clearStreamEntryInputs = page.getByTestId('remove-item')\n        this.saveGroupsButton = page.getByTestId('save-groups-btn')\n        this.acknowledgeButton = page.getByTestId('acknowledge-btn')\n        this.confirmAcknowledgeButton = page.getByTestId('acknowledge-submit')\n        this.claimPendingMessageButton = page.getByTestId(\n            'claim-pending-message',\n        )\n        this.submitButton = page.getByTestId('btn-submit')\n        this.consumerDestinationSelect = page.getByTestId('destination-select')\n        this.removeConsumerButton = page.locator(\n            '[data-testid^=\"remove-consumer-button\"]',\n        )\n        this.removeConsumerGroupButton = page.locator(\n            '[data-testid^=\"remove-groups-button\"]',\n        )\n        this.optionalParametersSwitcher = page.getByTestId(\n            'optional-parameters-switcher',\n        )\n        this.forceClaimCheckbox = page\n            .getByTestId('force-claim-checkbox')\n            .locator('..')\n        this.editStreamLastIdButton = page.getByTestId('stream-group_edit-btn')\n        this.saveButton = page.getByTestId('save-btn')\n        this.bulkActionsButton = page.getByTestId('btn-bulk-actions')\n        this.editHashButton = page.locator('[data-testid^=\"hash_edit-btn-\"]')\n        this.editHashFieldTtlButton = page.locator(\n            '[data-testid^=\"hash-ttl_edit-btn-\"]',\n        )\n        this.editZsetButton = page.locator('[data-testid^=\"zset_edit-btn-\"]')\n        this.editListButton = page.locator('[data-testid^=\"list_edit-btn-\"]')\n        this.cancelStreamGroupBtn = page.getByTestId('cancel-stream-groups-btn')\n        this.patternModeBtn = page.getByTestId('search-mode-pattern-btn')\n        this.redisearchModeBtn = page.getByTestId('search-mode-redisearch-btn')\n        this.showFilterHistoryBtn = page.getByTestId('show-suggestions-btn')\n        this.clearFilterHistoryBtn = page.getByTestId('clear-history-btn')\n        this.loadSampleDataBtn = page.getByTestId('load-sample-data-btn')\n        this.executeBulkKeyLoadBtn = page.getByTestId(\n            'load-sample-data-btn-confirm',\n        )\n        this.backToBrowserBtn = page.getByTestId('back-right-panel-btn')\n        this.loadAllBtn = page.getByTestId('load-all-value-btn')\n        this.downloadAllValueBtn = page.getByTestId('download-all-value-btn')\n        this.openTutorialsBtn = page.getByTestId('explore-msg-btn')\n        this.keyItem = page.locator(\n            '[data-testid*=\"node-item\"][data-testid*=\"keys:\"]',\n        )\n        this.columnsBtn = page.getByTestId('btn-columns-actions')\n\n        // CONTAINERS\n        this.streamGroupsContainer = page.getByTestId('stream-groups-container')\n        this.streamConsumersContainer = page.getByTestId(\n            'stream-consumers-container',\n        )\n        this.breadcrumbsContainer = page.getByTestId('breadcrumbs-container')\n        this.virtualTableContainer = page.getByTestId('virtual-table-container')\n        this.streamEntriesContainer = page.getByTestId(\n            'stream-entries-container',\n        )\n        this.streamMessagesContainer = page.getByTestId(\n            'stream-messages-container',\n        )\n        this.loader = page.getByTestId('type-loading')\n        this.newIndexPanel = page.getByTestId('create-index-panel')\n\n        // LINKS\n        this.internalLinkToWorkbench = page.getByTestId(\n            'internal-workbench-link',\n        )\n        this.userSurveyLink = page.getByTestId('user-survey-link')\n        this.redisearchFreeLink = page.getByTestId('get-started-link')\n        this.guideLinksBtn = page.locator('[data-testid^=\"guide-button-\"]')\n\n        // OPTION ELEMENTS\n        this.stringOption = page.locator('#string')\n        this.jsonOption = page.locator('#ReJSON-RL')\n        this.setOption = page.locator('#set')\n        this.zsetOption = page.locator('#zset')\n        this.listOption = page.locator('#list')\n        this.hashOption = page.locator('#hash')\n        this.streamOption = page.locator('#stream')\n        this.removeFromHeadSelection = page.locator('#HEAD')\n        this.filterOptionType = page.locator(\n            '[data-test-subj^=\"filter-option-type-\"]',\n        )\n        this.filterByKeyTypeDropDown = page.getByTestId(\n            'select-filter-key-type',\n        )\n        this.filterAllKeyType = page.locator('#all')\n        this.consumerOption = page.getByTestId('consumer-option')\n        this.claimTimeOptionSelect = page.getByTestId('time-option-select')\n        this.relativeTimeOption = page.locator('#idle')\n        this.timestampOption = page.locator('#time')\n        this.formatSwitcher = page.getByTestId('select-format-key-value')\n        this.formatSwitcherIcon = page.locator(\n            '[data-testid^=\"key-value-formatter-option-selected\"]',\n        )\n        this.refreshIndexButton = page.getByTestId('refresh-indexes-btn')\n        this.selectIndexDdn = page.locator(\n            '[data-testid=\"select-index-placeholder\"],[data-testid=\"select-search-mode\"]',\n        )\n        this.createIndexBtn = page.getByTestId('create-index-btn')\n        this.cancelIndexCreationBtn = page.getByTestId(\n            'create-index-cancel-btn',\n        )\n        this.confirmIndexCreationBtn = page.getByTestId('create-index-btn')\n        this.resizeTrigger = page.locator('[data-testid^=\"resize-trigger-\"]')\n        this.filterHistoryOption = page.getByTestId('suggestion-item-')\n        this.filterHistoryItemText = page.getByTestId('suggestion-item-text')\n\n        // TABS\n        this.streamTabGroups = page.getByTestId('stream-tab-Groups')\n        this.streamTabConsumers = page.getByTestId('stream-tab-Consumers')\n        this.streamTabs = page.locator('[data-test-subj=\"stream-tabs\"]')\n\n        // TEXT INPUTS\n        this.addKeyNameInput = page.getByTestId('key')\n        this.keyNameInput = page.getByTestId('edit-key-input')\n        this.keyTTLInput = page.getByTestId('ttl')\n        this.editKeyTTLInput = page.getByTestId('edit-ttl-input')\n        this.ttlText = page.getByTestId('key-ttl-text').locator('span')\n        this.hashFieldValueInput = page.getByTestId('field-value')\n        this.hashFieldNameInput = page.getByTestId('field-name')\n        this.hashFieldValueEditor = page.getByTestId('hash_value-editor')\n        this.hashTtlFieldInput = page.getByTestId('hash-ttl')\n        this.listKeyElementEditorInput = page.locator(\n            '[data-testid^=\"list_value-editor-\"]',\n        )\n        this.stringKeyValueInput = page.getByTestId('string-value')\n        this.jsonKeyValueInput = page.locator('div[data-mode-id=json] textarea')\n        this.jsonUploadInput = page.getByTestId('upload-input-file')\n        this.setMemberInput = page.getByTestId('member-name')\n        this.zsetMemberScoreInput = page.getByTestId('member-score')\n        this.filterByPatterSearchInput = page.getByTestId('search-key')\n        this.hashFieldInput = page.getByTestId('hash-field')\n        this.hashValueInput = page.getByTestId('hash-value')\n        this.searchInput = page.getByTestId('search')\n        this.jsonKeyInput = page.getByTestId('json-key')\n        this.jsonValueInput = page.getByTestId('json-value')\n        this.countInput = page.getByTestId('count-input')\n        this.streamEntryId = page.getByTestId('entryId')\n        this.streamField = page.getByTestId('field-name')\n        this.streamValue = page.getByTestId('field-value')\n        this.addAdditionalElement = page.getByTestId('add-item')\n        this.streamFieldsValues = page.getByTestId('stream-entry-field-')\n        this.streamEntryIDDateValue = page.locator(\n            '[data-testid^=\"stream-entry-\"][data-testid$=\"date\"]',\n        )\n        this.groupNameInput = page.getByTestId('group-name-field')\n        this.consumerIdInput = page.getByTestId('id-field')\n        this.streamMinIdleTimeInput = page.getByTestId('min-idle-time')\n        this.claimIdleTimeInput = page.getByTestId('time-count')\n        this.claimRetryCountInput = page.getByTestId('retry-count')\n        this.lastIdInput = page.getByTestId('last-id-field')\n        this.inlineItemEditor = page.getByTestId('inline-item-editor')\n        this.indexNameInput = page.getByTestId('index-name')\n        this.prefixFieldInput = page.locator('[data-test-subj=\"comboBoxInput\"]')\n        this.indexIdentifierInput = page.getByTestId('identifier-')\n\n        // TEXT ELEMENTS\n        this.keySizeDetails = page.getByTestId('key-size-text')\n        this.keyLengthDetails = page.getByTestId('key-length-text')\n        this.keyNameInTheList = this.cssSelectorKey\n        this.hashFieldsList = page.getByTestId('hash-field-').locator('span')\n        this.hashValuesList = page\n            .getByTestId('hash_content-value-')\n            .locator('span')\n        this.hashField = page.getByTestId('hash-field-').first()\n        this.hashFieldValue = page.getByTestId('hash_content-value-')\n        this.setMembersList = page.locator('[data-testid^=\"set-member-value-\"]')\n        this.zsetMembersList = page.locator(\n            '[data-testid^=\"zset-member-value-\"]',\n        )\n        this.zsetScoresList = page.locator(\n            '[data-testid^=\"zset_content-value-\"]',\n        )\n        this.listElementsList = page.locator(\n            '[data-testid^=\"list_content-value-\"]',\n        )\n        this.jsonKeyValue = page.getByTestId('json-data')\n        this.jsonError = page.getByTestId('edit-json-error')\n        this.tooltip = page.locator('[role=\"tooltip\"]')\n        this.dialog = page.locator('[role=\"dialog\"]')\n        this.noResultsFound = page.locator('[data-test-subj=\"no-result-found\"]')\n        this.noResultsFoundOnly = page.getByTestId('no-result-found-only')\n        this.searchAdvices = page.locator('[data-test-subj=\"search-advices\"]')\n        this.keysNumberOfResults = page.getByTestId('keys-number-of-results')\n        this.scannedValue = page.getByTestId('keys-number-of-scanned')\n        this.totalKeysNumber = page.getByTestId('keys-total')\n        this.keyDetailsBadge = page.locator(\n            '.key-details-header .euiBadge__text',\n        )\n        this.modulesTypeDetails = page.getByTestId('modules-type-details')\n        this.filteringLabel = page.getByTestId('badge-')\n        this.keysSummary = page.getByTestId('keys-summary')\n        this.multiSearchArea = page.getByTestId('multi-search')\n        this.keyDetailsHeader = page.getByTestId('key-details-header')\n        this.keyListTable = page.getByTestId('keyList-table')\n        this.keyListMessage = page.getByTestId('no-result-found-msg')\n        this.keyDetailsTable = page.getByTestId('key-details')\n        this.keyNameFormDetails = page.getByTestId('key-name-text')\n        this.keyDetailsTTL = page.getByTestId('key-ttl-text')\n        this.progressLine = page.locator('div.euiProgress')\n        this.progressKeyList = page.getByTestId('progress-key-list')\n        this.jsonScalarValue = page.getByTestId('json-scalar-value')\n        this.noKeysToDisplayText = page.getByTestId('no-result-found-msg')\n        this.streamEntryDate = page.locator(\n            '[data-testid*=\"-date\"][data-testid*=\"stream-entry\"]',\n        )\n        this.streamEntryIdValue = page.locator(\n            '.streamItemId[data-testid*=\"stream-entry\"]',\n        )\n        this.streamFields = page.locator(\n            '[data-test-subj=\"stream-entries-container\"] .truncateText',\n        )\n        this.streamVirtualContainer = page\n            .locator('[data-testid=\"virtual-grid-container\"] div div')\n            .first()\n        this.streamEntryFields = page.getByTestId('stream-entry-field')\n        this.confirmationMessagePopover = page.locator(\n            'div.euiPopover__panel .euiText',\n        )\n        this.streamGroupId = page\n            .locator('.streamItemId[data-testid^=\"stream-group-id\"]')\n            .first()\n        this.streamGroupName = page.getByTestId('stream-group-name')\n        this.streamMessage = page.locator(\n            '[data-testid*=\"-date\"][data-testid^=\"stream-message\"]',\n        )\n        this.streamConsumerName = page.getByTestId('stream-consumer-')\n        this.consumerGroup = page.getByTestId('stream-group-')\n        this.entryIdInfoIcon = page.getByTestId('entry-id-info-icon')\n        this.entryIdError = page.getByTestId('id-error')\n        this.pendingCount = page.getByTestId('pending-count')\n        this.streamRangeBar = page.getByTestId('mock-fill-range')\n        this.rangeLeftTimestamp = page.getByTestId('range-left-timestamp')\n        this.rangeRightTimestamp = page.getByTestId('range-right-timestamp')\n        this.jsonValue = page.getByTestId('value-as-json')\n        this.stringValueAsJson = page.getByTestId('value-as-json')\n\n        // POPUPS\n        this.changeValueWarning = page.getByTestId('approve-popover')\n\n        // TABLE\n        this.keyListItem = page.locator('[role=\"rowgroup\"] [role=\"row\"]')\n\n        // DIALOG\n        this.noReadySearchDialogTitle = page.getByTestId('welcome-page-title')\n\n        // CHECKBOXES\n        this.showTtlCheckbox = page.getByTestId('test-check-ttl').locator('..')\n        this.showTtlColumnCheckbox = page.getByTestId('show-ttl').locator('..')\n        this.showSizeColumnCheckbox = page\n            .getByTestId('show-key-size')\n            .locator('..')\n\n        // UTILITY FUNCTIONS\n        this.getHashTtlFieldInput = (fieldName: string): Locator =>\n            page.getByTestId(`hash-ttl_content-value-${fieldName}`)\n        this.getListElementInput = (count: number): Locator =>\n            page.locator(`[data-testid*=\"element-${count}\"]`)\n        this.getKeySize = (keyName: string): Locator =>\n            page.getByTestId(`size-${keyName}`)\n        this.getKeyTTl = (keyName: string): Locator =>\n            page.getByTestId(`ttl-${keyName}`)\n    }\n\n    async navigateToBrowserPage(): Promise<void> {\n        await this.browserTab.getByRole('paragraph').click()\n        await this.waitForLocatorVisible(this.browserPage)\n    }\n\n    async commonAddNewKey(keyName: string, TTL?: string): Promise<void> {\n        await this.waitForLocatorNotVisible(this.progressLine)\n        await this.waitForLocatorNotVisible(this.loader)\n        await this.plusAddKeyButton.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        if (TTL !== undefined) {\n            await this.keyTTLInput.click()\n            await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        }\n        await this.keyTypeDropDown.click()\n    }\n\n    async addStringKey(\n        keyName: string,\n        value = ' ',\n        TTL?: string,\n    ): Promise<void> {\n        await this.plusAddKeyButton.click()\n        await this.keyTypeDropDown.click()\n        await this.stringOption.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        if (TTL !== undefined) {\n            await this.keyTTLInput.click()\n            await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        }\n        await this.stringKeyValueInput.click()\n        await this.stringKeyValueInput.fill(value, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.addKeyButton.click()\n    }\n\n    async addJsonKey(\n        keyName: string,\n        value: string,\n        TTL?: string,\n    ): Promise<void> {\n        await this.plusAddKeyButton.click()\n        await this.keyTypeDropDown.click()\n        await this.jsonOption.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n        })\n        await this.jsonKeyValueInput.click()\n        await this.jsonKeyValueInput.fill(value, {\n            timeout: 0,\n        })\n        if (TTL !== undefined) {\n            await this.keyTTLInput.click()\n            await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        }\n        await this.addKeyButton.click()\n    }\n\n    async addSetKey(keyName: string, TTL = ' ', members = ' '): Promise<void> {\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n        await this.waitForLocatorNotVisible(this.progressLine)\n        await this.waitForLocatorNotVisible(this.loader)\n        await this.plusAddKeyButton.click()\n        await this.keyTypeDropDown.click()\n        await this.setOption.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.keyTTLInput.click()\n        await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        await this.setMemberInput.fill(members, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.addKeyButton.click()\n        await this.toast.closeToast()\n    }\n\n    async addZSetKey(\n        keyName: string,\n        scores = ' ',\n        TTL = ' ',\n        members = ' ',\n    ): Promise<void> {\n        await this.waitForLocatorNotVisible(this.progressLine)\n        await this.waitForLocatorNotVisible(this.loader)\n        await this.plusAddKeyButton.click()\n        await this.keyTypeDropDown.click()\n        await this.zsetOption.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.keyTTLInput.click()\n        await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        await this.setMemberInput.fill(members, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.zsetMemberScoreInput.fill(scores, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.addKeyButton.click()\n    }\n\n    async addListKey(\n        keyName: string,\n        TTL = ' ',\n        element: string[] = [' '],\n        position: AddElementInList = AddElementInList.Tail,\n    ): Promise<void> {\n        await this.waitForLocatorNotVisible(this.progressLine)\n        await this.waitForLocatorNotVisible(this.loader)\n        await this.plusAddKeyButton.click()\n        await this.keyTypeDropDown.click()\n        await this.listOption.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.keyTTLInput.click()\n        await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        if (position === AddElementInList.Head) {\n            await this.removeElementFromListSelect.click()\n            await this.removeFromHeadSelection.click()\n            await expect(this.removeFromHeadSelection).not.toBeVisible()\n        }\n        for (let i = 0; i < element.length; i += 1) {\n            await this.getListElementInput(i).click()\n            await this.getListElementInput(i).fill(element[i], {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n            if (element.length > 1 && i < element.length - 1) {\n                await this.addAdditionalElement.click()\n            }\n        }\n        await this.addKeyButton.click()\n    }\n\n    async addHashKey(\n        keyName: string,\n        TTL = ' ',\n        field = ' ',\n        value = ' ',\n        fieldTtl = '',\n    ): Promise<void> {\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n        await this.waitForLocatorNotVisible(this.progressLine)\n        await this.waitForLocatorNotVisible(this.loader)\n        await this.plusAddKeyButton.click()\n        await this.keyTypeDropDown.click()\n        await this.hashOption.click()\n        await this.addKeyNameInput.click()\n        await this.addKeyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.keyTTLInput.click()\n        await this.keyTTLInput.fill(TTL, { timeout: 0, noWaitAfter: false })\n        await this.hashFieldNameInput.fill(field, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.hashFieldValueInput.fill(value, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        if (fieldTtl !== '') {\n            await this.hashTtlFieldInput.fill(fieldTtl, {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n        }\n        await this.addKeyButton.click()\n        await this.toast.closeToast()\n    }\n\n    async addStreamKey(\n        keyName: string,\n        field: string,\n        value: string,\n        TTL?: string,\n    ): Promise<void> {\n        await this.commonAddNewKey(keyName, TTL)\n        await this.streamOption.click()\n        await expect(this.streamEntryId).toHaveValue('*', { timeout: 5000 })\n        await this.streamField.fill(field, { timeout: 0, noWaitAfter: false })\n        await this.streamValue.fill(value, { timeout: 0, noWaitAfter: false })\n        await expect(this.addKeyButton).not.toBeDisabled()\n        await this.addKeyButton.click()\n        await this.toast.closeToast()\n    }\n\n    async addEntryToStream(\n        field: string,\n        value: string,\n        entryId?: string,\n    ): Promise<void> {\n        await this.addNewStreamEntry.click()\n        await this.streamField.fill(field, { timeout: 0, noWaitAfter: false })\n        await this.streamValue.fill(value, { timeout: 0, noWaitAfter: false })\n        if (entryId !== undefined) {\n            await this.streamEntryId.fill(entryId, {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n        }\n        await this.saveElementButton.click()\n        await expect(this.streamEntriesContainer).toContainText(field)\n        await expect(this.streamEntriesContainer).toContainText(value)\n    }\n\n    async fulfillSeveralStreamFields(\n        fields: string[],\n        values: string[],\n        entryId?: string,\n    ): Promise<void> {\n        for (let i = 0; i < fields.length; i += 1) {\n            await this.streamField\n                .nth(-1)\n                .fill(fields[i], { timeout: 0, noWaitAfter: false })\n            await this.streamValue\n                .nth(-1)\n                .fill(values[i], { timeout: 0, noWaitAfter: false })\n            if (i < fields.length - 1) {\n                await this.addAdditionalElement.click()\n            }\n        }\n        if (entryId !== undefined) {\n            await this.streamEntryId.fill(entryId, {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n        }\n    }\n\n    async selectFilterGroupType(groupName: string): Promise<void> {\n        await this.filterByKeyTypeDropDown.click()\n        await this.filterOptionType.locator(groupName).click()\n    }\n\n    async setAllKeyType(): Promise<void> {\n        await this.filterByKeyTypeDropDown.click()\n        await this.filterAllKeyType.click()\n    }\n\n    async searchByKeyName(keyName: string): Promise<void> {\n        await this.filterByPatterSearchInput.click()\n        await this.filterByPatterSearchInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.page.keyboard.press('Enter')\n    }\n\n    getKeySelectorByName(keyName: string): Locator {\n        return this.page.locator(`[data-testid=\"key-${keyName}\"]`)\n    }\n\n    async isKeyIsDisplayedInTheList(keyName: string): Promise<boolean> {\n        const keyNameInTheList = this.getKeySelectorByName(keyName)\n        await this.waitForLocatorNotVisible(this.loader)\n        return keyNameInTheList.isVisible()\n    }\n\n    async deleteKey(): Promise<void> {\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n        await this.keyNameInTheList.click()\n        await this.deleteKeyButton.click()\n        await this.confirmDeleteKeyButton.click()\n    }\n\n    async deleteKeyByName(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName)\n        await this.keyNameInTheList.hover()\n        await this.keyNameInTheList.click()\n        await this.deleteKeyButton.click()\n        await this.confirmDeleteKeyButton.click()\n    }\n\n    async deleteKeysByNames(keyNames: string[]): Promise<void> {\n        for (const name of keyNames) {\n            await this.deleteKeyByName(name)\n        }\n    }\n\n    async deleteKeyByNameFromList(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName)\n        await this.keyNameInTheList.hover()\n        await this.page\n            .locator(`[data-testid=\"delete-key-btn-${keyName}\"]`)\n            .click()\n        await this.submitDeleteKeyButton.click()\n    }\n\n    async editKeyName(keyName: string): Promise<void> {\n        await this.editKeyNameButton.click()\n        await this.keyNameInput.fill(keyName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async editStringKeyValue(value: string): Promise<void> {\n        await this.stringKeyValueInput.click()\n        await this.stringKeyValueInput.fill(value, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async getStringKeyValue(): Promise<string | null> {\n        return this.stringKeyValueInput.textContent()\n    }\n\n    async getZsetKeyScore(): Promise<string | null> {\n        return this.zsetScoresList.textContent()\n    }\n\n    async addFieldToHash(\n        keyFieldValue: string,\n        keyValue: string,\n        fieldTtl = '',\n    ): Promise<void> {\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n        await this.addKeyValueItemsButton.click()\n        await this.hashFieldInput.fill(keyFieldValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.hashValueInput.fill(keyValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        if (fieldTtl !== '') {\n            await this.hashTtlFieldInput.fill(fieldTtl, {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n        }\n        await this.saveHashFieldButton.click()\n    }\n\n    async editHashKeyValue(value: string): Promise<void> {\n        await this.hashFieldValue.hover()\n        await this.editHashButton.click()\n        await this.hashFieldValueEditor.fill(value, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async editHashFieldTtlValue(\n        fieldName: string,\n        fieldTtl: string,\n    ): Promise<void> {\n        await this.getHashTtlFieldInput(fieldName).hover()\n        await this.editHashFieldTtlButton.click()\n        await this.inlineItemEditor.fill(fieldTtl, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async getHashKeyValue(): Promise<string | null> {\n        return this.hashFieldValue.textContent()\n    }\n\n    async editListKeyValue(value: string): Promise<void> {\n        await this.listElementsList.hover()\n        await this.editListButton.click()\n\n        // Wait for any list editor to appear - this is a legacy method\n        const editorInput = this.listKeyElementEditorInput.first()\n        await expect(editorInput).toBeVisible()\n        await editorInput.fill(value, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async getListKeyValue(): Promise<string | null> {\n        return this.listElementsList.textContent()\n    }\n\n    async getJsonKeyValue(): Promise<string | null> {\n        return this.jsonKeyValue.textContent()\n    }\n\n    async searchByTheValueInKeyDetails(value: string): Promise<void> {\n        await this.searchButtonInKeyDetails.click()\n        await this.searchInput.fill(value, { timeout: 0, noWaitAfter: false })\n        await this.page.keyboard.press('Enter')\n    }\n\n    async secondarySearchByTheValueInKeyDetails(value: string): Promise<void> {\n        await this.searchInput.fill(value, { timeout: 0, noWaitAfter: false })\n        await this.page.keyboard.press('Enter')\n    }\n\n    async searchByTheValueInSetKey(value: string): Promise<void> {\n        await this.searchInput.click()\n        await this.searchInput.fill(value, { timeout: 0, noWaitAfter: false })\n        await this.page.keyboard.press('Enter')\n    }\n\n    async addMemberToSet(keyMember: string): Promise<void> {\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n        await this.addKeyValueItemsButton.click()\n        await this.setMemberInput.fill(keyMember, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.saveMemberButton.click()\n    }\n\n    async addMemberToZSet(keyMember: string, score: string): Promise<void> {\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n        await this.addKeyValueItemsButton.click()\n        await this.setMemberInput.fill(keyMember, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.zsetMemberScoreInput.fill(score, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.saveMemberButton.click()\n    }\n\n    async openKeyDetails(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName)\n        await this.keyNameInTheList.click()\n    }\n\n    async openKeyDetailsByKeyName(keyName: string): Promise<void> {\n        const keyNameInTheList = this.page.locator(\n            `[data-testid=\"key-${keyName}\"]`,\n        )\n        await keyNameInTheList.click()\n    }\n\n    async addElementToList(\n        element: string[],\n        position: AddElementInList = AddElementInList.Tail,\n    ): Promise<void> {\n        if (await this.toast.toastCloseButton.isVisible()) {\n            await this.toast.toastCloseButton.click()\n        }\n        await this.addKeyValueItemsButton.click()\n        if (position === AddElementInList.Head) {\n            await this.removeElementFromListSelect.click()\n            await this.removeFromHeadSelection.click()\n            await expect(this.removeFromHeadSelection).not.toBeVisible()\n        }\n        for (let i = 0; i < element.length; i += 1) {\n            await this.getListElementInput(i).click()\n            await this.getListElementInput(i).fill(element[i], {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n            if (element.length > 1 && i < element.length - 1) {\n                await this.addAdditionalElement.click()\n            }\n        }\n        await this.addKeyButton.click()\n    }\n\n    async removeListElementFromHeadOld(): Promise<void> {\n        await this.removeElementFromListIconButton.click()\n        await expect(\n            await this.countInput.getAttribute('disabled'),\n        ).toBeTruthy()\n        await this.removeElementFromListSelect.click()\n        await this.removeFromHeadSelection.click()\n        await this.removeElementFromListButton.click()\n        await this.confirmRemoveListElementButton.click()\n    }\n\n    async removeListElementFromTail(count: string): Promise<void> {\n        await this.removeElementFromListIconButton.click()\n        await this.countInput.fill(count, { timeout: 0, noWaitAfter: false })\n        await this.removeElementFromListButton.click()\n        await this.confirmRemoveListElementButton.click()\n    }\n\n    async removeListElementFromHead(count: string): Promise<void> {\n        await this.removeElementFromListIconButton.click()\n        await this.countInput.fill(count, { timeout: 0, noWaitAfter: false })\n        await this.removeElementFromListSelect.click()\n        await this.removeFromHeadSelection.click()\n        await this.removeElementFromListButton.click()\n        await this.confirmRemoveListElementButton.click()\n    }\n\n    async addJsonKeyOnTheSameLevel(\n        jsonKey: string,\n        jsonKeyValue: string,\n    ): Promise<void> {\n        await this.addJsonObjectButton.click()\n        await this.jsonKeyInput.fill(jsonKey, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.jsonValueInput.fill(jsonKeyValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async addJsonKeyInsideStructure(\n        jsonKey: string,\n        jsonKeyValue: string,\n    ): Promise<void> {\n        await this.expandJsonObject.click()\n        await this.addJsonFieldButton.click()\n        await this.jsonKeyInput.fill(jsonKey, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.jsonValueInput.fill(jsonKeyValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async addJsonValueInsideStructure(jsonKeyValue: string): Promise<void> {\n        await this.expandJsonObject.click()\n        await this.addJsonFieldButton.click()\n        await this.jsonValueInput.fill(jsonKeyValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n    }\n\n    async addJsonStructure(jsonStructure: string): Promise<void> {\n        if (await this.expandJsonObject.isVisible()) {\n            await this.expandJsonObject.click()\n        }\n        await this.editJsonObjectButton.click()\n        await this.jsonValueInput.fill(jsonStructure, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyEditButton.click()\n    }\n\n    async deleteStreamEntry(): Promise<void> {\n        await this.removeEntryButton.click()\n        await this.confirmRemoveEntryButton.click()\n    }\n\n    async getKeyLength(): Promise<string> {\n        const rawValue = await this.keyLengthDetails.textContent()\n        const parts = (rawValue ?? '').split(' ')\n        return parts[parts.length - 1]\n    }\n\n    async createConsumerGroup(groupName: string, id?: string): Promise<void> {\n        await this.addKeyValueItemsButton.click()\n        await this.groupNameInput.fill(groupName, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        if (id !== undefined) {\n            await this.consumerIdInput.fill(id, {\n                timeout: 0,\n                noWaitAfter: false,\n            })\n        }\n        await this.saveGroupsButton.click()\n    }\n\n    async openStreamPendingsView(keyName: string): Promise<void> {\n        await this.openKeyDetails(keyName)\n        await this.streamTabGroups.click()\n        await this.consumerGroup.click()\n        await this.streamConsumerName.click()\n    }\n\n    async selectFormatter(formatter: string): Promise<void> {\n        const option = this.page.locator(\n            `[data-test-subj=\"format-option-${formatter}\"]`,\n        )\n        await this.formatSwitcher.click()\n        await option.click()\n    }\n\n    async verifyScannningMore(): Promise<void> {\n        for (let i = 10; i < 100; i += 10) {\n            const rememberedScanResults = Number(\n                (await this.keysNumberOfResults.textContent())?.replace(\n                    /\\s/g,\n                    '',\n                ),\n            )\n            await expect(this.progressKeyList).not.toBeVisible({\n                timeout: 30000,\n            })\n            const scannedValueText = await this.scannedValue.textContent()\n            const regExp = new RegExp(`${i} ...`)\n            await expect(scannedValueText).toMatch(regExp)\n            await this.scanMoreButton.click()\n            const scannedResults = Number(\n                (await this.keysNumberOfResults.textContent())?.replace(\n                    /\\s/g,\n                    '',\n                ),\n            )\n            await expect(scannedResults).toBeGreaterThan(rememberedScanResults)\n        }\n    }\n\n    async selectIndexByName(index: string): Promise<void> {\n        const option = this.page.locator(\n            `[data-test-subj=\"mode-option-type-${index}\"]`,\n        )\n        await this.selectIndexDdn.click()\n        await option.click()\n    }\n\n    async verifyNoKeysInDatabase(): Promise<void> {\n        await expect(this.keyListMessage).toBeVisible()\n        await expect(this.keysSummary).not.toBeVisible()\n    }\n\n    async clearFilter(): Promise<void> {\n        await this.clearFilterButton.click()\n    }\n\n    async clickGuideLinksByName(guide: string): Promise<void> {\n        const linkGuide = this.page.locator(guide)\n        await linkGuide.click()\n    }\n\n    async isKeyDetailsOpen(keyName: string): Promise<boolean> {\n        try {\n            // Check if the key details header is visible (only present when key is selected)\n            const headerIsVisible = await this.page\n                .getByTestId('key-details-header')\n                .isVisible()\n\n            if (!headerIsVisible) {\n                return false\n            }\n\n            // Check if the key name in the header matches the expected key\n            const keyNameIsVisible = await this.keyNameFormDetails\n                .filter({ hasText: keyName })\n                .isVisible()\n\n            if (!keyNameIsVisible) {\n                return false\n            }\n\n            // Check if any key details content is visible\n            const detailsContainers = [\n                'string-details',\n                'hash-details',\n                'set-details',\n                'list-details',\n                'zset-details',\n                'json-details',\n                'stream-details',\n            ]\n\n            for (const containerId of detailsContainers) {\n                const container = this.page.getByTestId(containerId)\n                if (await container.isVisible()) {\n                    return true\n                }\n            }\n\n            return false\n        } catch (error) {\n            return false\n        }\n    }\n\n    async isKeyDetailsClosed(): Promise<boolean> {\n        try {\n            // Wait for either the header to disappear OR the close button to disappear\n            // This ensures we wait for the UI transition to complete\n            await expect\n                .poll(async () => {\n                    const headerIsVisible = await this.page\n                        .getByTestId('key-details-header')\n                        .isVisible()\n                    const closeRightPanelBtn = await this.page\n                        .getByTestId('close-right-panel-btn')\n                        .isVisible()\n\n                    // Return true if details are closed (header gone OR close button gone)\n                    return !headerIsVisible || !closeRightPanelBtn\n                })\n                .toBe(true)\n\n            return true\n        } catch (error) {\n            return false\n        }\n    }\n\n    async closeKeyDetails(): Promise<void> {\n        await this.closeKeyButton.click()\n    }\n\n    async hashFieldExists(\n        fieldName: string,\n        fieldValue: string,\n    ): Promise<boolean> {\n        try {\n            const fieldLocator = this.page.locator(\n                `[data-testid=\"hash-field-${fieldName}\"]`,\n            )\n            const valueLocator = this.page.locator(\n                `[data-testid=\"hash_content-value-${fieldName}\"]`,\n            )\n\n            const fieldExists = await fieldLocator.isVisible()\n            const valueExists = await valueLocator.isVisible()\n\n            if (!fieldExists || !valueExists) {\n                return false\n            }\n\n            const actualValue = await valueLocator.textContent()\n            return actualValue?.includes(fieldValue) || false\n        } catch {\n            return false\n        }\n    }\n\n    async getAllListElements(): Promise<string[]> {\n        // Wait for list details to be visible first\n        await expect(this.page.getByTestId('list-details')).toBeVisible()\n\n        // Get all list elements' text content\n        const elements = await this.listElementsList.all()\n        const values: string[] = []\n\n        for (let i = 0; i < elements.length; i += 1) {\n            const text = await elements[i].textContent()\n            if (text && text.trim()) {\n                values.push(text.trim())\n            }\n        }\n\n        return values\n    }\n\n    async getAllSetMembers(): Promise<string[]> {\n        // Wait for set details to be visible and loaded\n        await this.waitForSetDetailsToBeVisible()\n\n        // Wait for at least one element to be visible (or confirm none exist)\n        try {\n            await expect(this.setMembersList.first()).toBeVisible()\n        } catch {\n            // No members exist - return empty array\n            return []\n        }\n\n        // Get all set members' text content\n        const elements = await this.setMembersList.all()\n        const textContents = await Promise.all(\n            elements.map(async (element) => {\n                const text = await element.textContent()\n                return text?.trim() || ''\n            }),\n        )\n\n        return textContents.filter((text) => text.length > 0)\n    }\n\n    async getAllZsetMembers(): Promise<Array<{ name: string; score: string }>> {\n        // Wait for zset details to be visible and loaded\n        await this.waitForZsetDetailsToBeVisible()\n\n        // Wait for at least one element to be visible (or confirm none exist)\n        try {\n            await expect(this.zsetMembersList.first()).toBeVisible()\n        } catch {\n            // No members exist - return empty array\n            return []\n        }\n\n        // Get all zset members' names and scores\n        const memberElements = await this.zsetMembersList.all()\n        const scoreElements = await this.zsetScoresList.all()\n        const members: Array<{ name: string; score: string }> = []\n\n        for (let i = 0; i < memberElements.length; i += 1) {\n            const memberText = await memberElements[i].textContent()\n            const scoreText = await scoreElements[i].textContent()\n\n            if (\n                memberText &&\n                memberText.trim() &&\n                scoreText &&\n                scoreText.trim()\n            ) {\n                members.push({\n                    name: memberText.trim(),\n                    score: scoreText.trim(),\n                })\n            }\n        }\n\n        return members\n    }\n\n    async addMemberToZsetKey(member: string, score: number): Promise<void> {\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n        await this.addKeyValueItemsButton.click()\n        await this.setMemberInput.fill(member)\n        await this.zsetMemberScoreInput.fill(score.toString())\n        await this.saveMemberButton.click()\n    }\n\n    async editZsetMemberScore(member: string, newScore: number): Promise<void> {\n        // First ensure we're on the right page and elements are loaded\n        await this.waitForZsetDetailsToBeVisible()\n\n        // Find the member element first and ensure it exists\n        const memberElement = this.page.locator(\n            `[data-testid=\"zset-member-value-${member}\"]`,\n        )\n        await expect(memberElement).toBeVisible()\n\n        // We need to hover over the score element, not the member element\n        // Wait for score elements to be ready\n        await expect(\n            this.page.locator('[data-testid^=\"zset_content-value-\"]').first(),\n        ).toBeVisible()\n\n        // Get all zset content value elements and try each one until we find the right row\n        const allScoreElements = await this.page\n            .locator('[data-testid^=\"zset_content-value-\"]')\n            .all()\n\n        let editButton\n        let foundVisible = false\n\n        for (let i = 0; i < allScoreElements.length && !foundVisible; i += 1) {\n            const scoreElement = allScoreElements[i]\n            // Hover over this score element\n            await scoreElement.hover()\n\n            // Check if an edit button becomes visible\n            editButton = this.page\n                .locator('[data-testid^=\"zset_edit-btn-\"]')\n                .first()\n            foundVisible = await editButton.isVisible()\n        }\n\n        // Click the edit button if we found one\n        if (editButton && foundVisible) {\n            await editButton.click()\n        } else {\n            throw new Error(`Could not find edit button for member: ${member}`)\n        }\n\n        // Use the correct editor element from the unit tests\n        const editorLocator = this.page.locator(\n            '[data-testid=\"inline-item-editor\"]',\n        )\n        await expect(editorLocator).toBeVisible()\n        await editorLocator.clear()\n        await editorLocator.fill(newScore.toString())\n        await this.applyButton.click()\n    }\n\n    async cancelZsetMemberScoreEdit(\n        member: string,\n        newScore: number,\n    ): Promise<void> {\n        // We need to hover over the score element to make the edit button appear\n        // Wait for score elements to be ready\n        await expect(\n            this.page.locator('[data-testid^=\"zset_content-value-\"]').first(),\n        ).toBeVisible()\n\n        // Get all zset content value elements and try each one until we find the right row\n        const allScoreElements = await this.page\n            .locator('[data-testid^=\"zset_content-value-\"]')\n            .all()\n\n        let editButton\n        let foundVisible = false\n\n        for (let i = 0; i < allScoreElements.length && !foundVisible; i += 1) {\n            const scoreElement = allScoreElements[i]\n            // Hover over this score element\n            await scoreElement.hover()\n\n            // Check if an edit button becomes visible\n            editButton = this.page\n                .locator('[data-testid^=\"zset_edit-btn-\"]')\n                .first()\n            foundVisible = await editButton.isVisible()\n        }\n\n        // Click the edit button if we found one\n        if (editButton && foundVisible) {\n            await editButton.click()\n        } else {\n            throw new Error(`Could not find edit button for member: ${member}`)\n        }\n\n        // Use the correct editor element from the unit tests\n        const editorLocator = this.page.locator(\n            '[data-testid=\"inline-item-editor\"]',\n        )\n        await expect(editorLocator).toBeVisible()\n        await editorLocator.clear()\n        await editorLocator.fill(newScore.toString())\n\n        // Cancel using Escape key\n        await this.page.keyboard.press('Escape')\n        await expect(editorLocator).not.toBeVisible()\n    }\n\n    async removeMemberFromZset(member: string): Promise<void> {\n        const memberElement = this.page.locator(\n            `[data-testid=\"zset-member-value-${member}\"]`,\n        )\n        await memberElement.hover()\n        await this.page\n            .locator(`[data-testid=\"zset-remove-button-${member}-icon\"]`)\n            .click()\n        await this.page\n            .locator(`[data-testid^=\"zset-remove-button-${member}\"]`)\n            .getByText('Remove')\n            .click()\n    }\n\n    async removeMultipleMembersFromZset(memberNames: string[]): Promise<void> {\n        for (let i = 0; i < memberNames.length; i += 1) {\n            await this.removeMemberFromZset(memberNames[i])\n        }\n    }\n\n    async removeAllZsetMembers(\n        members: Array<{ name: string; score: number }>,\n    ): Promise<void> {\n        for (let i = 0; i < members.length; i += 1) {\n            await this.removeMemberFromZset(members[i].name)\n        }\n    }\n\n    async waitForZsetLengthToUpdate(expectedLength: number): Promise<void> {\n        await expect\n            .poll(async () => {\n                const keyLength = await this.getKeyLength()\n                return parseInt(keyLength, 10)\n            })\n            .toBe(expectedLength)\n    }\n\n    async verifyZsetContainsMembers(\n        expectedMembers: Array<{ name: string; score: number }>,\n    ): Promise<void> {\n        const displayedMembers = await this.getAllZsetMembers()\n\n        expect(displayedMembers).toHaveLength(expectedMembers.length)\n        expectedMembers.forEach((expectedMember) => {\n            const foundMember = displayedMembers.find(\n                (member) => member.name === expectedMember.name,\n            )\n            expect(foundMember).toBeDefined()\n            expect(foundMember?.score).toBe(expectedMember.score.toString())\n        })\n    }\n\n    async verifyZsetDoesNotContainMembers(\n        unwantedMembers: string[],\n    ): Promise<void> {\n        const displayedMembers = await this.getAllZsetMembers()\n        unwantedMembers.forEach((unwantedMember) => {\n            const foundMember = displayedMembers.find(\n                (member) => member.name === unwantedMember,\n            )\n            expect(foundMember).toBeUndefined()\n        })\n    }\n\n    async verifyZsetMemberExists(member: string): Promise<void> {\n        const memberElement = this.page.locator(\n            `[data-testid=\"zset-member-value-${member}\"]`,\n        )\n        await expect(memberElement).toBeVisible()\n    }\n\n    async verifyZsetMemberNotExists(member: string): Promise<void> {\n        const memberElement = this.page.locator(\n            `[data-testid=\"zset-member-value-${member}\"]`,\n        )\n        await expect(memberElement).not.toBeVisible()\n    }\n\n    async verifyZsetMemberScore(\n        member: string,\n        expectedScore: number,\n    ): Promise<void> {\n        // Since we can't reliably match member to score element by DOM traversal,\n        // let's verify that ANY score element contains our expected score\n        // This is sufficient for our test since we're editing a specific score\n\n        const allScoreElements = await this.page\n            .locator('[data-testid^=\"zset_content-value-\"]')\n            .all()\n\n        let found = false\n        for (const scoreElement of allScoreElements) {\n            const scoreText = await scoreElement.textContent()\n            if (scoreText && scoreText.includes(expectedScore.toString())) {\n                found = true\n                break\n            }\n        }\n\n        if (!found) {\n            throw new Error(\n                `Expected score ${expectedScore} not found in any zset score elements`,\n            )\n        }\n    }\n\n    async waitForZsetScoreToUpdate(expectedScore: number): Promise<void> {\n        await expect\n            .poll(async () => {\n                const allScoreElements = await this.page\n                    .locator('[data-testid^=\"zset_content-value-\"]')\n                    .all()\n\n                const textContents = await Promise.all(\n                    allScoreElements.map((element) => element.textContent()),\n                )\n\n                return textContents.some(\n                    (text) => text && text.includes(expectedScore.toString()),\n                )\n            })\n            .toBe(true)\n    }\n\n    async searchInZsetMembers(searchTerm: string): Promise<void> {\n        // Wait for zset details to be visible first\n        await this.waitForZsetDetailsToBeVisible()\n\n        // Try clicking the search button first to make search input visible\n        await this.searchButtonInKeyDetails.click()\n\n        const searchInput = this.page.getByTestId('search')\n\n        // Wait for search input to be ready\n        await expect(searchInput).toBeVisible()\n        await expect(searchInput).toBeEnabled()\n\n        // Clear any existing search and enter new term\n        await searchInput.clear()\n        await searchInput.fill(searchTerm)\n        await this.page.keyboard.press('Enter')\n\n        // Wait for search to complete by checking if search input has the value\n        await expect\n            .poll(async () => {\n                const inputValue = await searchInput.inputValue()\n                return inputValue\n            })\n            .toBe(searchTerm)\n    }\n\n    async clearZsetSearch(): Promise<void> {\n        // Wait for search input to be ready\n        const searchInput = this.page.getByTestId('search')\n        await expect(searchInput).toBeVisible()\n        await expect(searchInput).toBeEnabled()\n        await searchInput.clear()\n        await this.page.keyboard.press('Enter')\n    }\n\n    async waitForZsetDetailsToBeVisible(): Promise<void> {\n        await expect(this.page.getByTestId('zset-details')).toBeVisible()\n    }\n\n    async waitForZsetMembersToLoad(expectedCount?: number): Promise<void> {\n        await this.waitForZsetDetailsToBeVisible()\n\n        // Wait for loading to complete\n        await expect(\n            this.page.getByTestId('progress-key-zset'),\n        ).not.toBeVisible()\n\n        // If we expect a specific count, wait for that many elements\n        if (expectedCount !== undefined && expectedCount > 0) {\n            await expect\n                .poll(async () => {\n                    const elements = await this.page\n                        .locator(\"[data-testid^='zset-member-value-']\")\n                        .all()\n                    return elements.length\n                })\n                .toBe(expectedCount)\n        } else if (expectedCount === undefined) {\n            // Just wait for at least one element or verify none exist\n            try {\n                await expect(this.zsetMembersList.first()).toBeVisible()\n            } catch {\n                // No elements expected or found - this is fine\n            }\n        }\n    }\n\n    async getAllStreamEntries(): Promise<string[]> {\n        // Get all stream field elements that contain the actual data\n        const fieldElements = await this.page\n            .locator('[data-testid^=\"stream-entry-field-\"]')\n            .all()\n\n        const fieldValues: string[] = []\n\n        for (let i = 0; i < fieldElements.length; i += 1) {\n            const text = await fieldElements[i].textContent()\n            if (text && text.trim()) {\n                fieldValues.push(text.trim())\n            }\n        }\n\n        return fieldValues\n    }\n\n    // Helper methods for key reading operations\n    async openKeyDetailsAndVerify(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName)\n        await this.openKeyDetailsByKeyName(keyName)\n\n        // Wait for key details to be properly loaded\n        await expect.poll(async () => this.isKeyDetailsOpen(keyName)).toBe(true)\n    }\n\n    async closeKeyDetailsAndVerify(): Promise<void> {\n        await this.closeKeyDetails()\n        const isDetailsClosed = await this.isKeyDetailsClosed()\n        expect(isDetailsClosed).toBe(true)\n    }\n\n    async verifyKeySize(): Promise<void> {\n        const keySizeText = await this.keySizeDetails.textContent()\n        expect(keySizeText).toBeTruthy()\n    }\n\n    async verifyKeyLength(expectedLength: string): Promise<void> {\n        const displayedLength = await this.getKeyLength()\n        expect(displayedLength).toBe(expectedLength)\n    }\n\n    async verifyKeyTTL(expectedTTL?: number): Promise<void> {\n        const displayedTTL = await this.keyDetailsTTL.textContent()\n        expect(displayedTTL).toContain('TTL:')\n\n        if (expectedTTL !== undefined) {\n            const ttlMatch = displayedTTL?.match(/TTL:\\s*(\\d+|No limit)/)\n            expect(ttlMatch).toBeTruthy()\n\n            if (ttlMatch && ttlMatch[1] !== 'No limit') {\n                const actualTTL = parseInt(ttlMatch[1], 10)\n                // TTL should be close to what we set (allowing for some time passage during test execution)\n                expect(actualTTL).toBeGreaterThan(expectedTTL - 60)\n                expect(actualTTL).toBeLessThanOrEqual(expectedTTL)\n            }\n        }\n    }\n\n    // Helper methods for verifying key content\n    async verifyStringKeyContent(expectedValue: string): Promise<void> {\n        const displayedValue = await this.getStringKeyValue()\n        expect(displayedValue).toContain(expectedValue)\n    }\n\n    async verifyHashKeyContent(\n        fieldName: string,\n        fieldValue: string,\n    ): Promise<void> {\n        const hashField = await this.hashFieldExists(fieldName, fieldValue)\n        expect(hashField).toBe(true)\n    }\n\n    async verifyListKeyContent(expectedElements: string[]): Promise<void> {\n        const displayedElements = await this.getAllListElements()\n        expect(displayedElements).toHaveLength(expectedElements.length)\n\n        expectedElements.forEach((expectedElement) => {\n            expect(displayedElements).toContain(expectedElement)\n        })\n    }\n\n    async verifySetKeyContent(expectedMembers: string[]): Promise<void> {\n        const displayedMembers = await this.getAllSetMembers()\n        expect(displayedMembers).toHaveLength(expectedMembers.length)\n\n        expectedMembers.forEach((expectedMember) => {\n            expect(displayedMembers).toContain(expectedMember)\n        })\n    }\n\n    async verifyZsetKeyContent(\n        expectedMembers: Array<{ name: string; score: number }>,\n    ): Promise<void> {\n        const displayedMembers = await this.getAllZsetMembers()\n        expect(displayedMembers).toHaveLength(expectedMembers.length)\n\n        expectedMembers.forEach((expectedMember) => {\n            const foundMember = displayedMembers.find(\n                (member) => member.name === expectedMember.name,\n            )\n            expect(foundMember).toBeDefined()\n            expect(foundMember?.score).toBe(expectedMember.score.toString())\n        })\n    }\n\n    async verifyJsonKeyContent(expectedValue: any): Promise<void> {\n        const displayedValue = await this.getJsonKeyValue()\n\n        // Check for scalar properties that should be visible\n        if (typeof expectedValue === 'object' && expectedValue !== null) {\n            if (expectedValue.name)\n                expect(displayedValue).toContain(expectedValue.name)\n            if (expectedValue.age)\n                expect(displayedValue).toContain(expectedValue.age.toString())\n            if (typeof expectedValue.active === 'boolean')\n                expect(displayedValue).toContain(\n                    expectedValue.active.toString(),\n                )\n\n            // Verify JSON structure keys are present\n            Object.keys(expectedValue).forEach((key) => {\n                expect(displayedValue).toContain(key)\n            })\n        }\n    }\n\n    async verifyStreamKeyContent(\n        expectedEntries: Array<{\n            fields: Array<{ name: string; value: string }>\n        }>,\n    ): Promise<void> {\n        const displayedFieldValues = await this.getAllStreamEntries()\n        expect(displayedFieldValues.length).toBeGreaterThan(0)\n\n        // Combine all field values to check for expected content\n        const allFieldsText = displayedFieldValues.join(' ')\n\n        // Check that all expected field values are present\n        expectedEntries.forEach((entry) => {\n            entry.fields.forEach((field) => {\n                expect(allFieldsText).toContain(field.value)\n            })\n        })\n    }\n\n    // Comprehensive helper method for complete key verification\n    async verifyKeyDetails(\n        keyName: string,\n        expectedLength: string,\n        expectedTTL?: number,\n    ): Promise<void> {\n        await this.openKeyDetailsAndVerify(keyName)\n        await this.verifyKeyLength(expectedLength)\n        await this.verifyKeySize()\n        await this.verifyKeyTTL(expectedTTL)\n        await this.closeKeyDetailsAndVerify()\n    }\n\n    async verifyKeyExists(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName)\n        const keyExists = await this.isKeyIsDisplayedInTheList(keyName)\n        expect(keyExists).toBe(true)\n    }\n\n    async verifyKeyDoesNotExist(keyName: string): Promise<void> {\n        await this.searchByKeyName(keyName)\n        const keyStillExists = await this.isKeyIsDisplayedInTheList(keyName)\n        expect(keyStillExists).toBe(false)\n    }\n\n    async deleteKeyFromDetailsView(keyName: string): Promise<void> {\n        await this.openKeyDetailsByKeyName(keyName)\n        await this.deleteKeyButton.click()\n        await this.confirmDeleteKeyButton.click()\n    }\n\n    async deleteKeyFromListView(keyName: string): Promise<void> {\n        await this.deleteKeyByNameFromList(keyName)\n    }\n\n    async startKeyDeletion(keyName: string): Promise<void> {\n        await this.openKeyDetailsByKeyName(keyName)\n        await this.deleteKeyButton.click()\n    }\n\n    async cancelKeyDeletion(): Promise<void> {\n        // Click outside the confirmation popover to cancel deletion\n        await this.keyDetailsHeader.click()\n    }\n\n    async editKeyTTLValue(newTTL: number): Promise<void> {\n        await this.editKeyTTLButton.click()\n        await this.editKeyTTLInput.clear()\n        await this.editKeyTTLInput.fill(newTTL.toString())\n        await this.applyButton.click()\n    }\n\n    async removeKeyTTL(): Promise<void> {\n        await this.editKeyTTLButton.click()\n        await this.editKeyTTLInput.clear()\n        await this.editKeyTTLInput.fill('') // Explicitly set to empty string\n        // Don't fill anything - empty field means persistent (-1)\n        await this.applyButton.click()\n\n        // Wait for the TTL to become persistent using the existing helper\n        await expect\n            .poll(async () => {\n                try {\n                    await this.verifyTTLIsPersistent()\n                    return true\n                } catch {\n                    return false\n                }\n            })\n            .toBe(true)\n    }\n\n    async waitForTTLToUpdate(expectedMinValue: number): Promise<void> {\n        await expect\n            .poll(async () => {\n                const currentTTL = await this.keyDetailsTTL.textContent()\n                const ttlMatch = currentTTL?.match(/TTL:\\s*(\\d+)/)\n                return ttlMatch ? parseInt(ttlMatch[1], 10) : 0\n            })\n            .toBeGreaterThan(expectedMinValue)\n    }\n\n    async verifyTTLIsWithinRange(\n        expectedTTL: number,\n        marginSeconds = 60,\n    ): Promise<void> {\n        const displayedTTL = await this.keyDetailsTTL.textContent()\n        const ttlMatch = displayedTTL?.match(/TTL:\\s*(\\d+)/)\n        expect(ttlMatch).toBeTruthy()\n\n        const actualTTL = parseInt(ttlMatch![1], 10)\n        expect(actualTTL).toBeGreaterThan(expectedTTL - marginSeconds)\n        expect(actualTTL).toBeLessThanOrEqual(expectedTTL)\n    }\n\n    async verifyTTLIsPersistent(): Promise<void> {\n        const displayedTTL = await this.keyDetailsTTL.textContent()\n        expect(displayedTTL).toContain('No limit')\n    }\n\n    async verifyTTLIsNotPersistent(): Promise<void> {\n        const displayedTTL = await this.keyDetailsTTL.textContent()\n        expect(displayedTTL).toContain('TTL:')\n        expect(displayedTTL).not.toContain('No limit')\n    }\n\n    async cancelStringKeyValueEdit(newValue: string): Promise<void> {\n        await this.editKeyValueButton.click()\n        await this.stringKeyValueInput.clear()\n        await this.stringKeyValueInput.fill(newValue)\n        await this.keyDetailsHeader.click()\n    }\n\n    async waitForKeyLengthToUpdate(expectedLength: string): Promise<void> {\n        await expect\n            .poll(async () => {\n                const keyLength = await this.getKeyLength()\n                return keyLength\n            })\n            .toBe(expectedLength)\n    }\n\n    async waitForStringValueToUpdate(expectedValue: string): Promise<void> {\n        await expect\n            .poll(async () => {\n                const currentValue = await this.getStringKeyValue()\n                return currentValue\n            })\n            .toContain(expectedValue)\n    }\n    async editListElementValue(newValue: string): Promise<void> {\n        await this.listElementsList.first().hover()\n        await this.editListButton.first().click()\n\n        // Wait for any list editor to appear - don't assume specific index\n        const editorInput = this.listKeyElementEditorInput.first()\n        await expect(editorInput).toBeVisible()\n        await editorInput.fill(newValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n        await this.applyButton.click()\n\n        // Wait for the editor to close and changes to be applied\n        await expect(editorInput).not.toBeVisible()\n\n        // Wait for the new value to appear in the first list element\n        await expect(this.listElementsList.first()).toContainText(newValue)\n    }\n\n    async cancelListElementEdit(newValue: string): Promise<void> {\n        await this.listElementsList.first().hover()\n        await this.editListButton.first().click()\n\n        // Wait for any list editor to appear - don't assume specific index\n        const editorInput = this.listKeyElementEditorInput.first()\n        await expect(editorInput).toBeVisible()\n        await editorInput.fill(newValue, {\n            timeout: 0,\n            noWaitAfter: false,\n        })\n\n        // Cancel using Escape key\n        await this.page.keyboard.press('Escape')\n\n        // Wait for the editor to close\n        await expect(editorInput).not.toBeVisible()\n    }\n\n    async addElementsToList(\n        elements: string[],\n        position: AddElementInList = AddElementInList.Tail,\n    ): Promise<void> {\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n        await this.addKeyValueItemsButton.click()\n\n        if (position === AddElementInList.Head) {\n            await this.removeElementFromListSelect.click()\n            await this.removeFromHeadSelection.click()\n            await expect(this.removeFromHeadSelection).not.toBeVisible()\n        }\n\n        for (let i = 0; i < elements.length; i += 1) {\n            await this.getListElementInput(i).click()\n            await this.getListElementInput(i).fill(elements[i])\n            if (elements.length > 1 && i < elements.length - 1) {\n                await this.addAdditionalElement.click()\n            }\n        }\n        await this.saveElementButton.click()\n    }\n\n    async removeListElementsFromTail(count: number): Promise<void> {\n        await this.removeElementFromListIconButton.click()\n        await this.countInput.fill(count.toString())\n        await this.removeElementFromListButton.click()\n        await this.confirmRemoveListElementButton.click()\n    }\n\n    async removeListElementsFromHead(count: number): Promise<void> {\n        await this.removeElementFromListIconButton.click()\n        await this.countInput.fill(count.toString())\n        await this.removeElementFromListSelect.click()\n        await this.removeFromHeadSelection.click()\n        await this.removeElementFromListButton.click()\n        await this.confirmRemoveListElementButton.click()\n    }\n\n    async verifyListContainsElements(\n        expectedElements: string[],\n    ): Promise<void> {\n        const displayedElements = await this.getAllListElements()\n        expectedElements.forEach((expectedElement) => {\n            expect(displayedElements).toContain(expectedElement)\n        })\n    }\n\n    async verifyListDoesNotContainElements(\n        unwantedElements: string[],\n    ): Promise<void> {\n        const displayedElements = await this.getAllListElements()\n        unwantedElements.forEach((unwantedElement) => {\n            expect(displayedElements).not.toContain(unwantedElement)\n        })\n    }\n\n    async waitForListLengthToUpdate(expectedLength: number): Promise<void> {\n        await expect\n            .poll(async () => {\n                const keyLength = await this.getKeyLength()\n                return parseInt(keyLength, 10)\n            })\n            .toBe(expectedLength)\n    }\n\n    async addMemberToSetKey(member: string): Promise<void> {\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n        await this.addKeyValueItemsButton.click()\n        await this.setMemberInput.fill(member)\n        await this.saveMemberButton.click()\n    }\n\n    async removeMemberFromSet(member: string): Promise<void> {\n        const memberElement = this.page.locator(\n            `[data-testid=\"set-member-value-${member}\"]`,\n        )\n        await memberElement.hover()\n        await this.page\n            .locator(`[data-testid=\"set-remove-btn-${member}-icon\"]`)\n            .click()\n        await this.page.locator(`[data-testid^=\"set-remove-btn-${member}\"]`)\n    }\n\n    async waitForHashDetailsToBeVisible(): Promise<void> {\n        await expect(this.page.getByTestId('hash-details')).toBeVisible()\n    }\n\n    async verifyHashFieldValueContains(\n        fieldName: string,\n        expectedValue: string,\n    ): Promise<void> {\n        const fieldValueElement = this.page.locator(\n            `[data-testid=\"hash_content-value-${fieldName}\"]`,\n        )\n        await expect(fieldValueElement).toContainText(expectedValue)\n    }\n\n    async verifyHashFieldValueNotContains(\n        fieldName: string,\n        unwantedValue: string,\n    ): Promise<void> {\n        const fieldValueElement = this.page.locator(\n            `[data-testid=\"hash_content-value-${fieldName}\"]`,\n        )\n        await expect(fieldValueElement).not.toContainText(unwantedValue)\n    }\n\n    async waitForHashFieldToBeVisible(fieldName: string): Promise<void> {\n        await expect(\n            this.page.locator(`[data-testid=\"hash-field-${fieldName}\"]`),\n        ).toBeVisible()\n        await expect(\n            this.page.locator(\n                `[data-testid=\"hash_content-value-${fieldName}\"]`,\n            ),\n        ).toBeVisible()\n    }\n\n    async getHashFieldValueElement(fieldName: string) {\n        return this.page.locator(\n            `[data-testid=\"hash_content-value-${fieldName}\"]`,\n        )\n    }\n\n    async editHashFieldValue(\n        fieldName: string,\n        newValue: string,\n    ): Promise<void> {\n        const fieldValueElement = await this.getHashFieldValueElement(fieldName)\n        await fieldValueElement.hover()\n        await this.page\n            .locator(`[data-testid^=\"hash_edit-btn-${fieldName}\"]`)\n            .click()\n\n        const editorLocator = this.page.locator('textarea').first()\n        await expect(editorLocator).toBeVisible()\n        await editorLocator.clear()\n        await editorLocator.fill(newValue)\n        await this.applyButton.click()\n    }\n\n    async cancelHashFieldEdit(\n        fieldName: string,\n        newValue: string,\n    ): Promise<void> {\n        const fieldValueElement = await this.getHashFieldValueElement(fieldName)\n        await fieldValueElement.hover()\n        await this.page\n            .locator(`[data-testid^=\"hash_edit-btn-${fieldName}\"]`)\n            .click()\n\n        const editorLocator = this.page.locator('textarea').first()\n        await expect(editorLocator).toBeVisible()\n        await editorLocator.clear()\n        await editorLocator.fill(newValue)\n\n        // Cancel using Escape key\n        await this.page.keyboard.press('Escape')\n        await expect(editorLocator).not.toBeVisible()\n    }\n\n    async removeHashField(fieldName: string): Promise<void> {\n        const fieldValueElement = await this.getHashFieldValueElement(fieldName)\n        await fieldValueElement.hover()\n        await this.page\n            .locator(`[data-testid=\"remove-hash-button-${fieldName}-icon\"]`)\n            .click()\n        await this.page\n            .locator(`[data-testid^=\"remove-hash-button-${fieldName}\"]`)\n            .getByText('Remove')\n            .click()\n    }\n\n    async waitForSetLengthToUpdate(expectedLength: number): Promise<void> {\n        await expect\n            .poll(async () => {\n                const keyLength = await this.getKeyLength()\n                return parseInt(keyLength, 10)\n            })\n            .toBe(expectedLength)\n    }\n\n    async verifySetContainsMembers(expectedMembers: string[]): Promise<void> {\n        const displayedMembers = await this.getAllSetMembers()\n\n        expect(displayedMembers).toHaveLength(expectedMembers.length)\n        expectedMembers.forEach((expectedMember) => {\n            expect(displayedMembers).toContain(expectedMember)\n        })\n    }\n\n    async verifySetDoesNotContainMembers(\n        unwantedMembers: string[],\n    ): Promise<void> {\n        const displayedMembers = await this.getAllSetMembers()\n        unwantedMembers.forEach((unwantedMember) => {\n            expect(displayedMembers).not.toContain(unwantedMember)\n        })\n    }\n\n    async verifySetMemberExists(member: string): Promise<void> {\n        const memberElement = this.page.locator(\n            `[data-testid=\"set-member-value-${member}\"]`,\n        )\n        await expect(memberElement).toBeVisible()\n    }\n\n    async verifySetMemberNotExists(member: string): Promise<void> {\n        const memberElement = this.page.locator(\n            `[data-testid=\"set-member-value-${member}\"]`,\n        )\n        await expect(memberElement).not.toBeVisible()\n    }\n\n    async searchInSetMembers(searchTerm: string): Promise<void> {\n        // Wait for set details to be visible first\n        await this.waitForSetDetailsToBeVisible()\n\n        // For set keys, the search input is always visible in the table header\n        const searchInput = this.page.getByTestId('search')\n        await expect(searchInput).toBeVisible()\n        await searchInput.fill(searchTerm)\n        await this.page.keyboard.press('Enter')\n\n        // Wait for search to take effect by checking if any elements are present\n        await expect\n            .poll(async () => {\n                const elements = await this.page\n                    .locator('[data-testid^=\"set-member-value-\"]')\n                    .count()\n                return elements >= 0 // Always true, just wait for elements to be ready\n            })\n            .toBeTruthy()\n    }\n\n    async clearSetSearch(): Promise<void> {\n        // For set keys, the search input is always visible\n        const searchInput = this.page.getByTestId('search')\n        await expect(searchInput).toBeVisible()\n        await searchInput.clear()\n        await this.page.keyboard.press('Enter')\n    }\n\n    async waitForSetDetailsToBeVisible(): Promise<void> {\n        await expect(this.page.getByTestId('set-details')).toBeVisible()\n    }\n\n    async verifySetSearchResults(\n        searchTerm: string,\n        allMembers: string[],\n    ): Promise<void> {\n        // Wait for any potential loading to complete\n        await this.waitForSetDetailsToBeVisible()\n\n        // Wait for search filtering to take effect by ensuring elements are ready\n        await expect\n            .poll(async () => {\n                const elements = await this.page\n                    .locator('[data-testid^=\"set-member-value-\"]:visible')\n                    .count()\n                return elements >= 0 // Always true, just wait for elements to be ready\n            })\n            .toBeTruthy()\n\n        // Get all currently visible set member elements\n        const visibleElements = await this.page\n            .locator('[data-testid^=\"set-member-value-\"]:visible')\n            .all()\n\n        // Extract the text content from visible elements\n        const textContents = await Promise.all(\n            visibleElements.map(async (element) => {\n                const textContent = await element.textContent()\n                return textContent?.trim() || ''\n            }),\n        )\n        const visibleMemberTexts = textContents.filter(\n            (text) => text.length > 0,\n        )\n\n        // Check which members should be matching\n        const expectedVisibleMembers = allMembers.filter((member) =>\n            member.includes(searchTerm),\n        )\n        const expectedHiddenMembers = allMembers.filter(\n            (member) => !member.includes(searchTerm),\n        )\n\n        // Verify that all expected visible members are found\n        expectedVisibleMembers.forEach((expectedMember) => {\n            const isFound = visibleMemberTexts.some((visibleText) =>\n                visibleText.includes(expectedMember),\n            )\n            expect(isFound).toBe(true)\n        })\n\n        // Verify that no hidden members are visible\n        expectedHiddenMembers.forEach((hiddenMember) => {\n            const isFound = visibleMemberTexts.some((visibleText) =>\n                visibleText.includes(hiddenMember),\n            )\n            expect(isFound).toBe(false)\n        })\n    }\n\n    async waitForSetMembersToLoad(expectedCount?: number): Promise<void> {\n        await this.waitForSetDetailsToBeVisible()\n\n        // Wait for loading to complete\n        await expect(\n            this.page.getByTestId('progress-key-set'),\n        ).not.toBeVisible()\n\n        // If we expect a specific count, wait for that many elements\n        if (expectedCount !== undefined && expectedCount > 0) {\n            await expect\n                .poll(async () => {\n                    const elements = await this.page\n                        .locator(\"[data-testid^='set-member-value-']\")\n                        .all()\n                    return elements.length\n                })\n                .toBe(expectedCount)\n        } else if (expectedCount === undefined) {\n            // Just wait for at least one element or verify none exist\n            try {\n                await expect(this.setMembersList.first()).toBeVisible()\n            } catch {\n                // No elements expected or found - this is fine\n            }\n        }\n    }\n\n    async verifyHashFieldValue(\n        fieldName: string,\n        expectedValue: string,\n    ): Promise<void> {\n        const fieldValueElement = await this.getHashFieldValueElement(fieldName)\n        await expect(fieldValueElement).toContainText(expectedValue)\n    }\n\n    async verifyHashFieldNotVisible(fieldName: string): Promise<void> {\n        await expect(\n            this.page.locator(`[data-testid=\"hash-field-${fieldName}\"]`),\n        ).not.toBeVisible()\n    }\n\n    async editJsonProperty(\n        propertyKey: string,\n        newValue: string | number | boolean,\n    ): Promise<void> {\n        // TODO: Ideally this should find by property key, but the current DOM structure\n        // makes it complex to navigate from key to value reliably. For now, we use the\n        // working approach of finding by current value.\n        const currentValue = await this.getJsonPropertyValue(propertyKey)\n        if (!currentValue) {\n            throw new Error(`Property \"${propertyKey}\" not found`)\n        }\n\n        // Find and click the value element\n        const valueElement = this.page\n            .getByTestId('json-scalar-value')\n            .filter({ hasText: currentValue })\n            .first()\n\n        await valueElement.click()\n        await expect(this.inlineItemEditor).toBeVisible()\n\n        // Format and apply the new value\n        const formattedValue =\n            typeof newValue === 'string' ? `\"${newValue}\"` : newValue.toString()\n\n        await this.inlineItemEditor.clear()\n        await this.inlineItemEditor.fill(formattedValue)\n        await this.applyButton.click()\n        await expect(this.inlineItemEditor).not.toBeVisible()\n\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n    }\n\n    // Convenience methods that use the generic editJsonProperty method\n    async editJsonString(propertyKey: string, newValue: string): Promise<void> {\n        await this.editJsonProperty(propertyKey, newValue)\n    }\n\n    async editJsonNumber(propertyKey: string, newValue: number): Promise<void> {\n        await this.editJsonProperty(propertyKey, newValue)\n    }\n\n    async editJsonBoolean(\n        propertyKey: string,\n        newValue: boolean,\n    ): Promise<void> {\n        await this.editJsonProperty(propertyKey, newValue)\n    }\n\n    async addJsonProperty(\n        key: string,\n        value: string | number | boolean,\n    ): Promise<void> {\n        // For JSON objects, add a new property at the same level\n        await this.addJsonObjectButton.click()\n\n        // Wait for the form to appear\n        await expect(this.jsonKeyInput).toBeVisible()\n        await expect(this.jsonValueInput).toBeVisible()\n\n        // Format the key and value properly for JSON\n        const formattedKey = `\"${key}\"`\n        let formattedValue: string\n        if (typeof value === 'string') {\n            formattedValue = `\"${value}\"`\n        } else {\n            formattedValue = value.toString()\n        }\n\n        // Fill the key and value\n        await this.jsonKeyInput.clear()\n        await this.jsonKeyInput.fill(formattedKey)\n        await this.jsonValueInput.clear()\n        await this.jsonValueInput.fill(formattedValue)\n\n        // Apply the changes\n        await this.applyButton.click()\n\n        // Wait for the form to disappear\n        await expect(this.jsonKeyInput).not.toBeVisible()\n\n        // Close any success toast if it appears\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n    }\n\n    async editEntireJsonStructure(newJsonStructure: string): Promise<void> {\n        // Switch to Monaco editor\n        await this.page\n            .getByRole('button', { name: 'Change editor type' })\n            .click()\n\n        // Wait for Monaco editor\n        const monacoContainer = this.page.getByTestId('monaco-editor-json-data')\n        await expect(monacoContainer).toBeVisible()\n\n        // Clear and set new JSON content\n        const textarea = monacoContainer.locator('textarea').first()\n        await textarea.focus()\n        await this.page.keyboard.press('Control+A')\n        await this.page.keyboard.press('Delete')\n        await textarea.type(newJsonStructure)\n\n        // Wait for button to be enabled and click it\n        const updateButton = this.page.getByTestId('json-data-update-btn')\n        await expect(updateButton).toBeEnabled()\n        await updateButton.click()\n\n        // Close editor and return to tree view\n        const cancelButton = this.page.getByTestId('json-data-cancel-btn')\n        if (await cancelButton.isVisible()) {\n            await cancelButton.click()\n        }\n\n        if (await this.toast.isCloseButtonVisible()) {\n            await this.toast.closeToast()\n        }\n    }\n\n    async verifyJsonPropertyExists(key: string, value: string): Promise<void> {\n        // Expand all objects and get the actual value\n        const actualValue = await this.getJsonPropertyValue(key)\n        expect(actualValue).toBe(value)\n    }\n\n    async verifyJsonPropertyNotExists(key: string): Promise<void> {\n        const actualValue = await this.getJsonPropertyValue(key)\n        expect(actualValue).toBeNull()\n    }\n\n    async waitForJsonDetailsToBeVisible(): Promise<void> {\n        await expect(this.page.getByTestId('json-details')).toBeVisible()\n    }\n\n    async waitForJsonPropertyUpdate(\n        key: string,\n        expectedValue: string,\n    ): Promise<void> {\n        await expect\n            .poll(async () => {\n                try {\n                    const actualValue = await this.getJsonPropertyValue(key)\n                    return actualValue === expectedValue\n                } catch (error) {\n                    return false\n                }\n            })\n            .toBe(true)\n    }\n\n    async expandAllJsonObjects(): Promise<void> {\n        // Keep expanding until no more expand buttons exist\n        while (true) {\n            const expandButtons = this.page.getByTestId('expand-object')\n            const count = await expandButtons.count()\n\n            if (count === 0) {\n                break // No more expand buttons to click\n            }\n\n            // Click ALL visible expand buttons in this iteration\n            const buttons = await expandButtons.all()\n            for (const button of buttons) {\n                if (await button.isVisible()) {\n                    await button.click()\n                }\n            }\n\n            // Wait for DOM to be ready before checking for new buttons\n            await this.page.waitForLoadState('domcontentloaded')\n        }\n    }\n\n    async getJsonPropertyValue(propertyName: string): Promise<string | null> {\n        // Expand all objects to make sure we can see the property\n        await this.expandAllJsonObjects()\n\n        // Get the JSON content and look for the property with a simple approach\n        const jsonContent = await this.jsonKeyValue.textContent()\n        if (!jsonContent) return null\n\n        // Use a more precise regex pattern for different value types\n        // Try patterns for strings, numbers, and booleans\n        const patterns = [\n            new RegExp(`${propertyName}:\"([^\"]*)\"`, 'g'), // String values: name:\"value\"\n            new RegExp(`${propertyName}:(\\\\d+(?:\\\\.\\\\d+)?)`, 'g'), // Number values: age:25\n            new RegExp(`${propertyName}:(true|false)`, 'g'), // Boolean values: active:true\n        ]\n\n        for (const pattern of patterns) {\n            pattern.lastIndex = 0 // Reset regex state\n            const match = pattern.exec(jsonContent)\n            if (match && match[1]) {\n                return match[1]\n            }\n        }\n\n        return null\n    }\n\n    async verifyJsonStructureValid(): Promise<void> {\n        // Check that no JSON error is displayed\n        await expect(this.jsonError).not.toBeVisible()\n\n        // Check that the JSON data container is visible\n        await expect(this.jsonKeyValue).toBeVisible()\n    }\n\n    async cancelJsonScalarValueEdit(propertyKey: string): Promise<void> {\n        // Store original value, start editing, then cancel\n        const originalValue = await this.getJsonPropertyValue(propertyKey)\n        if (!originalValue) {\n            throw new Error(`Property \"${propertyKey}\" not found`)\n        }\n\n        await this.expandAllJsonObjects()\n\n        // Find the element containing this value\n        const targetElement = this.page\n            .getByTestId('json-scalar-value')\n            .filter({ hasText: originalValue })\n            .first()\n\n        // Start edit, make change, then cancel\n        await targetElement.click()\n        await expect(this.inlineItemEditor).toBeVisible()\n        await this.inlineItemEditor.fill('\"canceled_value\"')\n        await this.page.keyboard.press('Escape')\n        await expect(this.inlineItemEditor).not.toBeVisible()\n\n        // Verify no change occurred\n        const finalValue = await this.getJsonPropertyValue(propertyKey)\n        expect(finalValue).toBe(originalValue)\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/components/common/toast.ts",
    "content": "import { Locator, Page } from '@playwright/test'\nimport { BasePage } from '../../base-page'\nimport { ToastSelectors } from '../../../selectors'\n\nexport class Toast extends BasePage {\n    // Deprecated - use new toast selectors below\n    // TODO: Remove deprecated selectors and usages after migrating all toasts\n    public readonly toastHeader: Locator\n\n    public readonly toastBody: Locator\n\n    public readonly toastSuccess: Locator\n\n    public readonly toastError: Locator\n\n    public readonly toastCloseButton: Locator\n\n    public readonly toastSubmitBtn: Locator\n\n    public readonly toastCancelBtn: Locator\n\n    // New toast selectors\n    public readonly toastContainer: Locator\n\n    public readonly toastMessage: Locator\n\n    public readonly toastDescription: Locator\n\n    public readonly toastActionButton: Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.toastHeader = page.locator(ToastSelectors.toastHeader)\n        this.toastBody = page.locator(ToastSelectors.toastBody)\n        this.toastSuccess = page.locator(ToastSelectors.toastSuccess)\n        this.toastError = page.locator(ToastSelectors.toastError)\n        this.toastCloseButton = page.locator(ToastSelectors.toastCloseButton)\n        this.toastSubmitBtn = page.getByTestId(ToastSelectors.toastSubmitBtn)\n        this.toastCancelBtn = page.getByTestId(ToastSelectors.toastCancelBtn)\n\n        // New toast selectors\n        this.toastContainer = page.locator(ToastSelectors.toastContainer)\n        this.toastMessage = page.locator(ToastSelectors.toastMessage)\n        this.toastDescription = page.locator(ToastSelectors.toastDescription)\n        this.toastActionButton = page.locator(ToastSelectors.toastActionButton)\n    }\n\n    async isCloseButtonVisible(): Promise<boolean> {\n        return this.isVisible(ToastSelectors.toastCloseButton)\n    }\n\n    async closeToast(): Promise<void> {\n        await this.toastActionButton.click()\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/components/redis-cloud-sign-in-panel.ts",
    "content": "import { Locator, Page } from '@playwright/test'\nimport { BasePage } from '../base-page'\n\nexport class RedisCloudSigninPanel extends BasePage {\n    readonly ssoOauthButton: Locator\n\n    readonly ssoEmailInput: Locator\n\n    readonly submitBtn: Locator\n\n    readonly oauthAgreement: Locator\n\n    readonly googleOauth: Locator\n\n    readonly githubOauth: Locator\n\n    readonly ssoOauth: Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.ssoOauthButton = page.getByTestId('sso-oauth')\n        this.ssoEmailInput = page.getByTestId('sso-email')\n        this.submitBtn = page.getByTestId('btn-submit')\n        this.oauthAgreement = page.locator('[for=\"ouath-agreement\"]')\n        this.googleOauth = page.getByTestId('google-oauth')\n        this.githubOauth = page.getByTestId('github-oauth')\n        this.ssoOauth = page.getByTestId('sso-oauth')\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/dialogs/add-rdi-instance-dialog.ts",
    "content": "import { Page, Locator } from '@playwright/test'\nimport { BasePage } from '../base-page'\n\nexport class AddRdiInstanceDialog extends BasePage {\n    // INPUTS\n    readonly rdiAliasInput: Locator\n\n    readonly urlInput: Locator\n\n    readonly usernameInput: Locator\n\n    readonly passwordInput: Locator\n\n    // BUTTONS\n    readonly addInstanceButton: Locator\n\n    readonly cancelInstanceBtn: Locator\n\n    readonly connectToRdiForm: Locator\n\n    // ICONS\n    readonly urlInputInfoIcon: Locator\n\n    readonly usernameInputInfoIcon: Locator\n\n    readonly passwordInputInfoIcon: Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.page = page\n        this.rdiAliasInput = page.getByTestId('connection-form-name-input')\n        this.urlInput = page.getByTestId('connection-form-url-input')\n        this.usernameInput = page.getByTestId('connection-form-username-input')\n        this.passwordInput = page.getByTestId('connection-form-password-input')\n\n        this.addInstanceButton = page.getByTestId('connection-form-add-button')\n        this.cancelInstanceBtn = page.getByTestId(\n            'connection-form-cancel-button',\n        )\n        this.connectToRdiForm = page.getByTestId('connection-form')\n\n        // Assuming that the two-level parent traversal is needed.\n        // Using an XPath locator to navigate two ancestors then find an SVG element.\n        this.urlInputInfoIcon = page\n            .getByTestId('connection-form-url-input')\n            .locator('xpath=ancestor::div[2]//svg')\n        this.usernameInputInfoIcon = page\n            .getByTestId('connection-form-username-input')\n            .locator('xpath=ancestor::div[2]//svg')\n        this.passwordInputInfoIcon = page\n            .getByTestId('connection-form-password-input')\n            .locator('xpath=ancestor::div[2]//svg')\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/dialogs/add-redis-database-dialog.ts",
    "content": "import { expect, Locator, Page } from '@playwright/test'\nimport { TlsCertificates } from '../../helpers/constants'\nimport { RedisCloudSigninPanel } from '../components/redis-cloud-sign-in-panel'\nimport {\n    SentinelParameters,\n    AddNewDatabaseParameters,\n    SSHParameters,\n} from '../../types'\nimport { BasePage } from '../base-page'\n\nexport class AddRedisDatabaseDialog extends BasePage {\n    readonly redisCloudSigninPanel: RedisCloudSigninPanel\n\n    // BUTTONS\n    readonly addDatabaseButton: Locator\n\n    readonly addRedisDatabaseButton: Locator\n\n    readonly customSettingsButton: Locator\n\n    readonly addAutoDiscoverDatabase: Locator\n\n    readonly addCloudDatabaseButton: Locator\n\n    readonly redisSoftwareButton: Locator\n\n    readonly redisSentinelButton: Locator\n\n    // TEXT INPUTS\n    readonly hostInput: Locator\n\n    readonly portInput: Locator\n\n    readonly databaseAliasInput: Locator\n\n    readonly passwordInput: Locator\n\n    readonly usernameInput: Locator\n\n    readonly accessKeyInput: Locator\n\n    readonly secretKeyInput: Locator\n\n    readonly databaseIndexInput: Locator\n\n    // TABS\n    readonly generalTab: Locator\n\n    readonly securityTab: Locator\n\n    readonly decompressionTab: Locator\n\n    // DROPDOWNS\n    readonly caCertField: Locator\n\n    readonly clientCertField: Locator\n\n    readonly selectCompressor: Locator\n\n    // CHECKBOXES\n    readonly databaseIndexCheckbox: Locator\n\n    readonly useSSHCheckbox: Locator\n\n    // RADIO BUTTONS\n    readonly sshPasswordRadioBtn: Locator\n\n    readonly sshPrivateKeyRadioBtn: Locator\n\n    // LABELS\n    readonly dataCompressorLabel: Locator\n\n    // SSH TEXT INPUTS\n    readonly sshHostInput: Locator\n\n    readonly sshPortInput: Locator\n\n    readonly sshUsernameInput: Locator\n\n    readonly sshPasswordInput: Locator\n\n    readonly sshPrivateKeyInput: Locator\n\n    readonly sshPassphraseInput: Locator\n\n    // OTHER\n    readonly timeoutInput: Locator\n\n    // For certificate removal\n    aiChatMessage: Locator\n\n    aiCloseMessage: Locator\n\n    trashIconMsk(certificate: TlsCertificates): string {\n        return `[data-testid^=\"delete-${certificate}-cert\"]`\n    }\n\n    getDeleteCertificate(certificate: TlsCertificates): Locator {\n        return this.page.locator(this.trashIconMsk(certificate))\n    }\n\n    constructor(page: Page) {\n        super(page)\n        this.page = page\n        this.redisCloudSigninPanel = new RedisCloudSigninPanel(page)\n\n        // BUTTONS\n        this.addDatabaseButton = page.locator(\n            '[data-testid^=\"add-redis-database\"]',\n        )\n        this.addRedisDatabaseButton = page.getByTestId('btn-submit')\n        this.customSettingsButton = page.getByTestId('btn-connection-settings')\n        this.addAutoDiscoverDatabase = page.getByTestId(\n            'add-database_tab_software',\n        )\n        this.addCloudDatabaseButton = page.getByTestId('create-free-db-btn')\n        this.redisSoftwareButton = page.getByTestId('option-btn-software')\n        this.redisSentinelButton = page.getByTestId('option-btn-sentinel')\n\n        // TEXT INPUTS\n        this.hostInput = page.getByTestId('host')\n        this.portInput = page.getByTestId('port')\n        this.databaseAliasInput = page.getByTestId('name')\n        this.passwordInput = page.getByTestId('password')\n        this.usernameInput = page.getByTestId('username')\n        this.accessKeyInput = page.getByTestId('access-key')\n        this.secretKeyInput = page.getByTestId('secret-key')\n        this.databaseIndexInput = page.getByTestId('db')\n\n        // TABS\n        this.generalTab = page.getByTestId('manual-form-tab-general')\n        this.securityTab = page.getByTestId('manual-form-tab-security')\n        this.decompressionTab = page.getByTestId(\n            'manual-form-tab-decompression',\n        )\n\n        // DROPDOWNS\n        this.caCertField = page.getByTestId('select-ca-cert')\n        this.clientCertField = page.getByTestId('select-cert')\n        this.selectCompressor = page.getByTestId('select-compressor')\n\n        // CHECKBOXES\n        this.databaseIndexCheckbox = page.locator(\n            '[data-testid=\"showDb\"] ~ div',\n        )\n        this.useSSHCheckbox = page.locator('[data-testid=\"use-ssh\"] ~ div')\n\n        // RADIO BUTTONS\n        this.sshPasswordRadioBtn = page.locator('#password ~ div')\n        this.sshPrivateKeyRadioBtn = page.locator('#privateKey ~ div')\n\n        // LABELS\n        this.dataCompressorLabel = page.getByTestId(\n            '[data-testid=\"showCompressor\"] ~ label',\n        )\n        this.aiChatMessage = page.getByTestId('ai-chat-message-btn')\n        this.aiCloseMessage = page.locator(\n            '[aria-label=\"Closes this modal window\"]',\n        )\n\n        // SSH TEXT INPUTS\n        this.sshHostInput = page.getByTestId('sshHost')\n        this.sshPortInput = page.getByTestId('sshPort')\n        this.sshUsernameInput = page.getByTestId('sshUsername')\n        this.sshPasswordInput = page.getByTestId('sshPassword')\n        this.sshPrivateKeyInput = page.getByTestId('sshPrivateKey')\n        this.sshPassphraseInput = page.getByTestId('sshPassphrase')\n\n        // OTHER\n        this.timeoutInput = page.getByTestId('timeout')\n    }\n\n    async addRedisDataBase(\n        parameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        await expect(this.addDatabaseButton).toBeVisible({ timeout: 10000 })\n        await this.addDatabaseButton.click()\n        await this.customSettingsButton.click()\n        await this.hostInput.fill(parameters.host)\n        await this.portInput.fill(parameters.port)\n        await this.databaseAliasInput.fill(parameters.databaseName || '')\n        if (parameters.databaseUsername) {\n            await this.usernameInput.fill(parameters.databaseUsername)\n        }\n        if (parameters.databasePassword) {\n            await this.passwordInput.fill(parameters.databasePassword)\n        }\n    }\n\n    async addLogicalRedisDatabase(\n        parameters: AddNewDatabaseParameters,\n        index: string,\n    ): Promise<void> {\n        await this.addDatabaseButton.click()\n        await this.customSettingsButton.click()\n        await this.hostInput.fill(parameters.host)\n        await this.portInput.fill(parameters.port)\n        await this.databaseAliasInput.fill(parameters.databaseName || '')\n        if (parameters.databaseUsername) {\n            await this.usernameInput.fill(parameters.databaseUsername)\n        }\n        if (parameters.databasePassword) {\n            await this.passwordInput.fill(parameters.databasePassword)\n        }\n        await this.databaseIndexCheckbox.click()\n        await this.databaseIndexInput.fill(index)\n        await this.addRedisDatabaseButton.click()\n    }\n\n    async addStandaloneSSHDatabase(\n        databaseParameters: AddNewDatabaseParameters,\n        sshParameters: SSHParameters,\n    ): Promise<void> {\n        await this.addDatabaseButton.click()\n        await this.customSettingsButton.click()\n        await this.hostInput.fill(databaseParameters.host)\n        await this.portInput.fill(databaseParameters.port)\n        await this.databaseAliasInput.fill(\n            databaseParameters.databaseName || '',\n        )\n        if (databaseParameters.databaseUsername) {\n            await this.usernameInput.fill(databaseParameters.databaseUsername)\n        }\n        if (databaseParameters.databasePassword) {\n            await this.passwordInput.fill(databaseParameters.databasePassword)\n        }\n        // Navigate to security tab and select SSH Tunnel checkbox\n        await this.securityTab.click()\n        await this.useSSHCheckbox.click()\n        // Fill SSH fields\n        await this.sshHostInput.fill(sshParameters.sshHost)\n        await this.sshPortInput.fill(sshParameters.sshPort)\n        await this.sshUsernameInput.fill(sshParameters.sshUsername)\n        if (sshParameters.sshPassword) {\n            await this.sshPasswordInput.fill(sshParameters.sshPassword)\n        }\n        if (sshParameters.sshPrivateKey) {\n            await this.sshPrivateKeyRadioBtn.click()\n            await this.sshPrivateKeyInput.fill(sshParameters.sshPrivateKey)\n        }\n        if (sshParameters.sshPassphrase) {\n            await this.sshPrivateKeyRadioBtn.click()\n            await this.sshPassphraseInput.fill(sshParameters.sshPassphrase)\n        }\n        await this.addRedisDatabaseButton.click()\n    }\n\n    async discoverSentinelDatabases(\n        parameters: SentinelParameters,\n    ): Promise<void> {\n        await this.addDatabaseButton.click()\n        await this.redisSentinelButton.click()\n        if (parameters.sentinelHost) {\n            await this.hostInput.fill(parameters.sentinelHost)\n        }\n        if (parameters.sentinelPort) {\n            await this.portInput.fill(parameters.sentinelPort)\n        }\n        if (parameters.sentinelPassword) {\n            await this.passwordInput.fill(parameters.sentinelPassword)\n        }\n    }\n\n    async addAutodiscoverRedisSoftwareDatabase(\n        parameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        await this.addDatabaseButton.click()\n        await this.redisSoftwareButton.click()\n        await this.hostInput.fill(parameters.host)\n        await this.portInput.fill(parameters.port)\n        await this.usernameInput.fill(parameters.databaseUsername || '')\n        await this.passwordInput.fill(parameters.databasePassword || '')\n    }\n\n    async addAutodiscoverRedisCloudDatabase(\n        cloudAPIAccessKey: string,\n        cloudAPISecretKey: string,\n    ): Promise<void> {\n        await this.addDatabaseButton.click()\n        await this.addCloudDatabaseButton.click()\n        await this.accessKeyInput.fill(cloudAPIAccessKey)\n        await this.secretKeyInput.fill(cloudAPISecretKey)\n    }\n\n    async addOssClusterDatabase(\n        parameters: AddNewDatabaseParameters,\n    ): Promise<void> {\n        await this.addDatabaseButton.click()\n        await this.customSettingsButton.click()\n        if (parameters.ossClusterHost) {\n            await this.hostInput.fill(parameters.ossClusterHost)\n        }\n        if (parameters.ossClusterPort) {\n            await this.portInput.fill(parameters.ossClusterPort)\n        }\n        if (parameters.ossClusterDatabaseName) {\n            await this.databaseAliasInput.fill(\n                parameters.ossClusterDatabaseName,\n            )\n        }\n    }\n\n    async setCompressorValue(compressor: string): Promise<void> {\n        if (!(await this.selectCompressor.isVisible())) {\n            await this.dataCompressorLabel.click()\n        }\n        await this.selectCompressor.click()\n        await this.page.locator(`[id=\"${compressor}\"]`).click()\n    }\n\n    async removeCertificateButton(\n        certificate: TlsCertificates,\n        name: string,\n    ): Promise<void> {\n        await this.securityTab.click()\n        const row = this.page\n            .locator('button')\n            .locator('div')\n            .filter({ hasText: name })\n        const removeButtonFooter = this.page.locator(\n            '[class^=\"_popoverFooter\"]',\n        )\n        if (certificate === TlsCertificates.CA) {\n            await this.caCertField.click()\n        } else {\n            await this.clientCertField.click()\n        }\n        await row.locator(this.trashIconMsk(certificate)).click()\n        await removeButtonFooter.locator(this.trashIconMsk(certificate)).click()\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/dialogs/user-agreement-dialog.ts",
    "content": "import { expect, Locator, Page } from '@playwright/test'\n\nimport { BasePage } from '../base-page'\nimport { UserAgreementSelectors } from '../../selectors'\n\nexport class UserAgreementDialog extends BasePage {\n    readonly userAgreementsPopup: Locator\n\n    readonly submitButton: Locator\n\n    readonly switchOptionEula: Locator\n\n    readonly switchOptionEncryption: Locator\n\n    readonly pluginSectionWithText: Locator\n\n    readonly recommendedSwitcher: Locator\n\n    constructor(page: Page) {\n        super(page)\n        this.userAgreementsPopup = page.getByTestId(\n            UserAgreementSelectors.userAgreementsPopup,\n        )\n        this.submitButton = page.getByTestId(\n            UserAgreementSelectors.submitButton,\n        )\n        this.switchOptionEula = page.getByTestId(\n            UserAgreementSelectors.switchOptionEula,\n        )\n        this.switchOptionEncryption = page.getByTestId(\n            UserAgreementSelectors.switchOptionEncryption,\n        )\n        this.pluginSectionWithText = page.getByTestId(\n            UserAgreementSelectors.pluginSectionWithText,\n        )\n        this.recommendedSwitcher = page.getByTestId(\n            UserAgreementSelectors.recommendedSwitcher,\n        )\n    }\n\n    async acceptLicenseTerms(): Promise<void> {\n        try {\n            await this.switchOptionEula.waitFor({ timeout: 3000 }) // because the state isn't clear\n        } catch (error) {\n            // Ignore error if the dialog is not visible\n        }\n\n        if (await this.switchOptionEula.isVisible()) {\n            await this.recommendedSwitcher.click()\n            await this.switchOptionEula.click()\n            await this.submitButton.click()\n            await expect(this.userAgreementsPopup).not.toBeVisible({\n                timeout: 2000,\n            })\n        }\n    }\n\n    async getRecommendedSwitcherValue(): Promise<string | null> {\n        return this.recommendedSwitcher.getAttribute('aria-checked')\n    }\n\n    async isUserAgreementDialogVisible(): Promise<boolean> {\n        return this.userAgreementsPopup.isVisible()\n    }\n}\n"
  },
  {
    "path": "tests/playwright/pageObjects/index.ts",
    "content": "export * from './components/common/toast'\nexport * from './components/redis-cloud-sign-in-panel'\nexport * from './dialogs/add-rdi-instance-dialog'\nexport * from './dialogs/add-redis-database-dialog'\nexport * from './dialogs/user-agreement-dialog'\nexport * from './base-overview-page'\nexport * from './browser-page'\nexport * from './rdi-instances-list-page'\nexport * from './auto-discover-redis-enterprise-databases'\nexport * from './base-page'\n"
  },
  {
    "path": "tests/playwright/pageObjects/rdi-instances-list-page.ts",
    "content": "/* eslint-disable no-await-in-loop */\nimport { Page, Locator, expect } from '@playwright/test'\nimport { BaseOverviewPage } from './base-overview-page'\nimport { AddRdiInstanceDialog } from './dialogs/add-rdi-instance-dialog'\nimport { RdiInstance } from '../types'\n\nexport class RdiInstancesListPage extends BaseOverviewPage {\n    readonly AddRdiInstanceDialog: AddRdiInstanceDialog\n\n    readonly addRdiInstanceButton: Locator\n\n    readonly addRdiFromEmptyListBtn: Locator\n\n    readonly quickstartBtn: Locator\n\n    readonly rdiInstanceRow: Locator\n\n    readonly emptyRdiList: Locator\n\n    readonly rdiNameList: Locator\n\n    readonly searchInput: Locator\n\n    readonly sortBy: Locator\n\n    readonly cssRdiAlias: string\n\n    readonly cssUrl: string\n\n    readonly cssRdiVersion: string\n\n    readonly cssLastConnection: string\n\n    // Assuming these selectors exist—update their locators as needed.\n    readonly deleteRowButton: Locator\n\n    readonly confirmDeleteButton: Locator\n\n    readonly editRowButton: Locator\n\n    readonly Toast: { toastCloseButton: Locator }\n\n    constructor(page: Page) {\n        super(page)\n        this.page = page\n\n        this.AddRdiInstanceDialog = new AddRdiInstanceDialog(page)\n\n        // Use getByTestId for selectors with data-testid\n        this.addRdiInstanceButton = page.getByTestId('rdi-instance')\n        this.addRdiFromEmptyListBtn = page.getByTestId(\n            'empty-rdi-instance-button',\n        )\n        this.quickstartBtn = page.getByTestId('empty-rdi-quickstart-button')\n\n        this.rdiInstanceRow = page.locator('[class*=euiTableRow-isSelectable]')\n        this.emptyRdiList = page.getByTestId('empty-rdi-instance-list')\n        this.rdiNameList = page.locator('[class*=column_name] div')\n\n        this.searchInput = page.getByTestId('search-rdi-instance-list')\n\n        // Selector using data-test-subj remains as locator\n        this.sortBy = page.locator(\n            '[data-test-subj=tableHeaderSortButton] span',\n        )\n\n        // CSS selectors (kept as string constants)\n        this.cssRdiAlias = '[data-test-subj=rdi-alias-column]'\n        this.cssUrl = '[data-testid=url]'\n        this.cssRdiVersion = '[data-test-subj=rdi-instance-version-column]'\n        this.cssLastConnection =\n            '[data-test-subj=rdi-instance-last-connection-column]'\n\n        // These selectors are assumed. Adjust the test IDs as per your application.\n        this.deleteRowButton = page.getByTestId('delete-row-button')\n        this.confirmDeleteButton = page.getByTestId('confirm-delete-button')\n        this.editRowButton = page.getByTestId('edit-row-button')\n        this.Toast = {\n            toastCloseButton: page.getByTestId('toast-close-button'),\n        }\n    }\n\n    /**\n     * Add Rdi instance.\n     * @param instanceValue Rdi instance data\n     */\n    async addRdi(instanceValue: RdiInstance): Promise<void> {\n        await this.addRdiInstanceButton.click()\n        await this.AddRdiInstanceDialog.rdiAliasInput.fill(instanceValue.alias)\n        await this.AddRdiInstanceDialog.urlInput.fill(instanceValue.url)\n        if (instanceValue.username) {\n            await this.AddRdiInstanceDialog.usernameInput.fill(\n                instanceValue.username,\n            )\n        }\n        if (instanceValue.password) {\n            await this.AddRdiInstanceDialog.passwordInput.fill(\n                instanceValue.password,\n            )\n        }\n        await this.AddRdiInstanceDialog.addInstanceButton.click()\n        // Wait for the dialog to close after adding the Rdi instance\n        await this.AddRdiInstanceDialog.connectToRdiForm.waitFor({\n            state: 'hidden',\n        })\n    }\n\n    /**\n     * Get Rdi instance values by index.\n     * @param index Index of Rdi instance.\n     */\n    async getRdiInstanceValuesByIndex(index: number): Promise<RdiInstance> {\n        const alias = await this.rdiInstanceRow\n            .nth(index)\n            .locator(this.cssRdiAlias)\n            .innerText()\n        const currentLastConnection = await this.rdiInstanceRow\n            .nth(0)\n            .locator(this.cssLastConnection)\n            .innerText()\n        const currentVersion = await this.rdiInstanceRow\n            .nth(0)\n            .locator(this.cssRdiVersion)\n            .innerText()\n        const currentUrl = await this.rdiInstanceRow\n            .nth(0)\n            .locator(this.cssUrl)\n            .innerText()\n\n        const rdiInstance: RdiInstance = {\n            alias,\n            url: currentUrl,\n            version: currentVersion,\n            lastConnection: currentLastConnection,\n        }\n\n        return rdiInstance\n    }\n\n    /**\n     * Delete Rdi by name.\n     * @param dbName The name of the Rdi to be deleted.\n     */\n    async deleteRdiByName(dbName: string): Promise<void> {\n        const dbNames = this.rdiInstanceRow\n        const count = await dbNames.count()\n        for (let i = 0; i < count; i += 1) {\n            const text = await dbNames.nth(i).innerText()\n            if (text.includes(dbName)) {\n                await this.deleteRowButton.nth(i).click()\n                await this.confirmDeleteButton.click()\n                break\n            }\n        }\n    }\n\n    /**\n     * Edit Rdi by name.\n     * @param dbName The name of the Rdi to be edited.\n     */\n    async clickEditRdiByName(dbName: string): Promise<void> {\n        const rdiNames = this.rdiInstanceRow\n        const count = await rdiNames.count()\n        for (let i = 0; i < count; i += 1) {\n            const text = await rdiNames.nth(i).innerText()\n            if (text.includes(dbName)) {\n                await this.editRowButton.nth(i).click()\n                break\n            }\n        }\n    }\n\n    /**\n     * Click Rdi by name.\n     * @param rdiName The name of the Rdi.\n     */\n    async clickRdiByName(rdiName: string): Promise<void> {\n        if (await this.Toast.toastCloseButton.isVisible()) {\n            await this.Toast.toastCloseButton.click()\n        }\n        // Use getByText with exact match for the Rdi name\n        const rdi = this.rdiNameList.getByText(rdiName.trim(), { exact: true })\n        await expect(rdi).toBeVisible({ timeout: 3000 })\n        await rdi.click()\n    }\n\n    /**\n     * Sort Rdi list by column.\n     * @param columnName The name of the column.\n     */\n    async sortByColumn(columnName: string): Promise<void> {\n        await this.sortBy.filter({ hasText: columnName }).click()\n    }\n\n    /**\n     * Get all Rdi aliases.\n     */\n    async getAllRdiNames(): Promise<string[]> {\n        const rdis: string[] = []\n        const count = await this.rdiInstanceRow.count()\n        for (let i = 0; i < count; i += 1) {\n            const name = await this.rdiInstanceRow\n                .nth(i)\n                .locator(this.cssRdiAlias)\n                .innerText()\n            rdis.push(name)\n        }\n        return rdis\n    }\n}\n"
  },
  {
    "path": "tests/playwright/playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test'\nimport { Status } from 'allure-js-commons'\nimport dotenv from 'dotenv'\nimport * as os from 'os'\nimport './setup/module-mocks'\n\ndotenv.config({\n    path: process.env.envPath ?? 'env/.local-web.env',\n    override: true,\n})\n\nexport type TestOptions = {\n    apiUrl: string\n}\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig<TestOptions>({\n    testDir: './tests',\n    /* Maximum time one test can run for. */\n    timeout: 600 * 1000,\n    expect: {\n        /**\n         * Maximum time expect() should wait for the condition to be met.\n         * For example in `await expect(locator).toHaveText();`\n         */\n        timeout: 5000,\n    },\n    /* Run tests in files in parallel */\n    fullyParallel: true,\n    /* Fail the build on CI if you accidentally left test.only in the source code. */\n    forbidOnly: !!process.env.CI,\n    /* Retry on CI only */\n    retries: process.env.CI ? 2 : 0,\n    /* Opt out of parallel tests on CI. */\n    // workers: process.env.CI ? 1 : undefined,\n    workers: 1,\n    /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n    reporter: [\n        ['line'],\n        ['html'],\n        [\n            'allure-playwright',\n            {\n                resultsDir: 'allure-results',\n                detail: true,\n                suiteTitle: true,\n                links: {\n                    issue: {\n                        nameTemplate: 'Issue #%s',\n                        urlTemplate: 'https://issues.example.com/%s',\n                    },\n                    tms: {\n                        nameTemplate: 'TMS #%s',\n                        urlTemplate: 'https://tms.example.com/%s',\n                    },\n                    jira: {\n                        urlTemplate: (v: any) =>\n                            `https://jira.example.com/browse/${v}`,\n                    },\n                },\n                categories: [\n                    {\n                        name: 'foo',\n                        messageRegex: 'bar',\n                        traceRegex: 'baz',\n                        matchedStatuses: [Status.FAILED, Status.BROKEN],\n                    },\n                ],\n                environmentInfo: {\n                    os_platform: os.platform(),\n                    os_release: os.release(),\n                    os_version: os.version(),\n                    node_version: process.version,\n                },\n            },\n        ],\n    ],\n\n    /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n    use: {\n        /* Base URL to use in actions like `await page.goto('/')`. */\n        // baseURL: 'http://127.0.0.1:3000',\n\n        /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n        trace: 'on-first-retry',\n        testIdAttribute: 'data-testid',\n        headless: true,\n        deviceScaleFactor: undefined,\n        viewport: { width: 1920, height: 1080 },\n        video: {\n            mode: 'on',\n            size: { width: 1920, height: 1080 },\n        },\n    },\n\n    /* Configure projects for major browsers */\n    projects: [\n        {\n            name: 'Chromium',\n            testMatch: ['**.spec.ts'],\n            use: {\n                ...devices['Desktop Chrome'],\n                baseURL: process.env.COMMON_URL,\n                // headless: false,\n                launchOptions: {\n                    args: [\n                        '--no-sandbox',\n                        '--start-maximized',\n                        '--disable-dev-shm-usage',\n                        '--ignore-certificate-errors',\n                        '--disable-search-engine-choice-screen',\n                        // '--disable-blink-features=AutomationControlled',\n                        // '--disable-component-extensions-with-background-pages',\n                    ],\n                },\n            },\n        },\n    ],\n\n    /* Run your local dev server before starting the tests */\n    // webServer: {\n    //   command: 'npm run start',\n    //   url: 'http://127.0.0.1:3000',\n    //   reuseExistingServer: !process.env.CI,\n    // },\n})\n"
  },
  {
    "path": "tests/playwright/selectors/index.ts",
    "content": "export * from './toast-selectors'\nexport * from './user-agreement-selectors'\n"
  },
  {
    "path": "tests/playwright/selectors/toast-selectors.ts",
    "content": "export const ToastSelectors = {\n    // Deprecated - use new toast selectors below\n    // TODO: Remove deprecated selectors and usages after migrating all toasts\n    toastHeader: '[data-test-subj=euiToastHeader]',\n    toastBody: '[class*=euiToastBody]',\n    toastSuccess: '[class*=euiToast--success]',\n    toastError: '[class*=euiToast--danger]',\n    toastCloseButton: '[data-test-subj=toastCloseButton]',\n    toastSubmitBtn: 'submit-tooltip-btn',\n    toastCancelBtn: 'toast-cancel-btn',\n\n    // New selectors - use these for new toasts\n    toastContainer: '[data-testid=\"redisui-toast\"]',\n    toastMessage: '[data-testid=\"redisui-toast-message\"]',\n    toastDescription: '[data-testid=\"redisui-toast-description\"]',\n    toastActionButton: '[data-testid=\"redisui-toast-action-button\"]',\n}\n"
  },
  {
    "path": "tests/playwright/selectors/user-agreement-selectors.ts",
    "content": "export const UserAgreementSelectors = {\n    userAgreementsPopup: 'consents-settings-popup',\n    submitButton: 'btn-submit',\n    switchOptionEula: 'switch-option-eula',\n    switchOptionEncryption: 'switch-option-encryption',\n    pluginSectionWithText: 'plugin-section',\n    recommendedSwitcher: 'switch-option-recommended',\n}\n"
  },
  {
    "path": "tests/playwright/setup/module-mocks.ts",
    "content": "// Module mocks for Node.js environment to handle UI imports\n\nrequire('module-alias/register')\n\nconst Module = require('module')\nconst originalRequire = Module.prototype.require\n\nModule.prototype.require = function (id: string) {\n    // Mock SVG imports with Vite's ?react syntax\n    if (id.endsWith('.svg?react')) {\n        return function SvgMock() {\n            return null\n        }\n    }\n\n    return originalRequire.apply(this, arguments)\n}\n\nexport {}\n"
  },
  {
    "path": "tests/playwright/tests/basic-navigation.spec.ts",
    "content": "import { test, expect } from '../fixtures/test'\n\ntest.describe('Basic Navigation and Element Visibility', () => {\n    test('should navigate to the homepage and verify title', async ({\n        page,\n    }) => {\n        const title = await page.title()\n\n        expect(title).toBe('Redis databases')\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-delete.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../pageObjects/browser-page'\nimport { test } from '../../fixtures/test'\nimport { ossStandaloneConfig } from '../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../helpers/utils'\n\ntest.describe('Browser - Delete Key', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it still exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test.describe('when clicking on the delete button in the details view', () => {\n        test('should delete string key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a string key\n            const keyValue = faker.lorem.words(3)\n            await keyService.addStringKeyApi(\n                { keyName, value: keyValue },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n\n        test('should delete hash key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a hash key\n            const fieldName = faker.string.alphanumeric(8)\n            const fieldValue = faker.lorem.words(2)\n            await keyService.addHashKeyApi(\n                {\n                    keyName,\n                    fields: [{ field: fieldName, value: fieldValue }],\n                },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n\n        test('should delete list key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a list key\n            const listElements = [\n                faker.lorem.word(),\n                faker.lorem.word(),\n                faker.lorem.word(),\n            ]\n            await keyService.addListKeyApi(\n                { keyName, elements: listElements },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n\n        test('should delete set key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a set key\n            const setMembers = [\n                faker.lorem.word(),\n                faker.lorem.word(),\n                faker.lorem.word(),\n            ]\n            await keyService.addSetKeyApi(\n                { keyName, members: setMembers },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n\n        test('should delete sorted set key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a zset key\n            const zsetMembers = [\n                { name: faker.lorem.word(), score: 1.5 },\n                { name: faker.lorem.word(), score: 2.0 },\n                { name: faker.lorem.word(), score: 10 },\n            ]\n            await keyService.addZSetKeyApi(\n                { keyName, members: zsetMembers },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n\n        test('should delete json key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a JSON key\n            const jsonValue = {\n                name: faker.person.fullName(),\n                age: faker.number.int({ min: 18, max: 80 }),\n                active: true,\n            }\n            await keyService.addJsonKeyApi(\n                { keyName, value: jsonValue },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n\n        test('should delete stream key successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a stream key\n            const streamEntries = [\n                {\n                    id: '*',\n                    fields: [\n                        { name: 'temperature', value: '25.5' },\n                        { name: 'location', value: 'sensor-001' },\n                    ],\n                },\n            ]\n            await keyService.addStreamKeyApi(\n                { keyName, entries: streamEntries },\n                ossStandaloneConfig,\n            )\n\n            // Verify key exists, delete it, and verify deletion\n            await browserPage.verifyKeyExists(keyName)\n            await browserPage.deleteKeyFromDetailsView(keyName)\n            await browserPage.verifyKeyDoesNotExist(keyName)\n        })\n    })\n\n    test('should delete key from list view using delete button', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a string key for this test\n        const keyValue = faker.lorem.words(2)\n        await keyService.addStringKeyApi(\n            { keyName, value: keyValue },\n            ossStandaloneConfig,\n        )\n\n        // Verify key exists, delete from list view, and verify deletion\n        await browserPage.verifyKeyExists(keyName)\n        await browserPage.deleteKeyFromListView(keyName)\n        await browserPage.verifyKeyDoesNotExist(keyName)\n    })\n\n    test('should cancel key deletion when outside of the deletion confirmation popover', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a string key\n        const keyValue = faker.lorem.words(3)\n        await keyService.addStringKeyApi(\n            { keyName, value: keyValue },\n            ossStandaloneConfig,\n        )\n\n        // Verify key exists, start deletion but cancel, and verify key still exists\n        await browserPage.verifyKeyExists(keyName)\n        await browserPage.startKeyDeletion(keyName)\n        await browserPage.cancelKeyDeletion()\n        await browserPage.verifyKeyExists(keyName)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit/edit-hash-key.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../../pageObjects/browser-page'\nimport { test, expect } from '../../../fixtures/test'\nimport { ossStandaloneConfig } from '../../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../../helpers/utils'\n\ntest.describe('Browser - Edit Key Operations - Hash Key Editing', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should edit hash field value successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a hash key with a field\n        const fieldName = faker.string.alphanumeric(8)\n        const originalValue = faker.lorem.words(3)\n        const newValue = faker.lorem.words(4)\n\n        await keyService.addHashKeyApi(\n            {\n                keyName,\n                fields: [{ field: fieldName, value: originalValue }],\n            },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and wait for hash to load\n        await browserPage.searchByKeyName(keyName)\n        await browserPage.openKeyDetailsByKeyName(keyName)\n\n        // Wait for field to be visible and verify original value\n        await browserPage.waitForHashFieldToBeVisible(fieldName)\n        await browserPage.verifyHashFieldValue(fieldName, originalValue)\n\n        // Edit the hash field value\n        await browserPage.editHashFieldValue(fieldName, newValue)\n\n        // Verify the value was updated\n        await browserPage.verifyHashFieldValue(fieldName, newValue)\n    })\n\n    test('should cancel hash field value edit operation', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a hash key with a field\n        const fieldName = faker.string.alphanumeric(8)\n        const originalValue = faker.lorem.words(3)\n        const attemptedNewValue = faker.lorem.words(4)\n\n        await keyService.addHashKeyApi(\n            {\n                keyName,\n                fields: [{ field: fieldName, value: originalValue }],\n            },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and wait for hash to load\n        await browserPage.searchByKeyName(keyName)\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForHashDetailsToBeVisible()\n        await browserPage.verifyHashFieldValue(fieldName, originalValue)\n\n        // Start editing but cancel\n        await browserPage.cancelHashFieldEdit(fieldName, attemptedNewValue)\n\n        // Verify the original value is still present and attempted value is not\n        await browserPage.verifyHashFieldValueContains(fieldName, originalValue)\n        await browserPage.verifyHashFieldValueNotContains(\n            fieldName,\n            attemptedNewValue,\n        )\n    })\n\n    test('should add new field to hash key successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a hash key with one field\n        const existingFieldName = faker.string.alphanumeric(8)\n        const existingFieldValue = faker.lorem.words(2)\n        const newFieldName = faker.string.alphanumeric(8)\n        const newFieldValue = faker.lorem.words(3)\n\n        await keyService.addHashKeyApi(\n            {\n                keyName,\n                fields: [\n                    { field: existingFieldName, value: existingFieldValue },\n                ],\n            },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.searchByKeyName(keyName)\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForHashDetailsToBeVisible()\n        await browserPage.waitForKeyLengthToUpdate('1')\n\n        // Add a new field\n        await browserPage.addFieldToHash(newFieldName, newFieldValue)\n\n        // Verify new field appears and length updates\n        await browserPage.waitForHashFieldToBeVisible(newFieldName)\n        await browserPage.verifyHashFieldValue(newFieldName, newFieldValue)\n        await browserPage.waitForKeyLengthToUpdate('2')\n\n        // Verify existing field still exists\n        await browserPage.verifyHashFieldValue(\n            existingFieldName,\n            existingFieldValue,\n        )\n    })\n\n    test('should remove hash field successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a hash key with multiple fields\n        const field1Name = faker.string.alphanumeric(8)\n        const field1Value = faker.lorem.words(2)\n        const field2Name = faker.string.alphanumeric(8)\n        const field2Value = faker.lorem.words(2)\n\n        await keyService.addHashKeyApi(\n            {\n                keyName,\n                fields: [\n                    { field: field1Name, value: field1Value },\n                    { field: field2Name, value: field2Value },\n                ],\n            },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.searchByKeyName(keyName)\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForHashDetailsToBeVisible()\n        await browserPage.waitForKeyLengthToUpdate('2')\n\n        // Remove the first field\n        await browserPage.removeHashField(field1Name)\n\n        // Verify field was removed and length updated\n        await browserPage.waitForKeyLengthToUpdate('1')\n        await browserPage.verifyHashFieldNotVisible(field1Name)\n\n        // Verify other field still exists and key is still open\n        await browserPage.verifyHashFieldValue(field2Name, field2Value)\n        const keyStillExists = await browserPage.isKeyDetailsOpen(keyName)\n        expect(keyStillExists).toBe(true)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit/edit-json-key.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../../pageObjects/browser-page'\nimport { test } from '../../../fixtures/test'\nimport { ossStandaloneConfig } from '../../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../../helpers/utils'\n\ntest.describe('Browser - Edit JSON Key', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should edit JSON scalar values (string, number, boolean)', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const initialValue = {\n            name: faker.person.firstName(),\n            age: faker.number.int({ min: 7, max: 19 }),\n            active: true,\n            score: 87.5,\n            count: 10,\n        }\n        const newName = faker.person.firstName()\n        const newAge = faker.number.int({ min: 20, max: 90 })\n\n        await keyService.addJsonKeyApi(\n            { keyName, value: initialValue },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForJsonDetailsToBeVisible()\n\n        // Edit string value\n        await browserPage.editJsonString('name', newName)\n        await browserPage.waitForJsonPropertyUpdate('name', newName)\n\n        // Edit number values\n        await browserPage.editJsonNumber('age', newAge)\n        await browserPage.waitForJsonPropertyUpdate('age', newAge.toString())\n\n        // Edit boolean value\n        await browserPage.editJsonBoolean('active', false)\n        await browserPage.waitForJsonPropertyUpdate('active', 'false')\n\n        // Assert - verify all changes are applied and structure is valid\n        await browserPage.verifyJsonStructureValid()\n    })\n\n    test('should cancel JSON scalar value edit', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const initialValue = {\n            name: faker.person.firstName(),\n            score: faker.number.int({ min: 1, max: 100 }),\n        }\n\n        await keyService.addJsonKeyApi(\n            { keyName, value: initialValue },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForJsonDetailsToBeVisible()\n\n        // Cancel the scalar value edit for the 'name' property\n        await browserPage.cancelJsonScalarValueEdit('name')\n\n        // Assert - original value should remain unchanged\n        await browserPage.verifyJsonPropertyExists('name', initialValue.name)\n    })\n\n    test('should add new property to JSON object', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const initialValue = {\n            name: faker.person.firstName(),\n            age: faker.number.int({ min: 18, max: 80 }),\n        }\n        const newProperty = 'email'\n        const newValue = faker.internet.email()\n\n        await keyService.addJsonKeyApi(\n            { keyName, value: initialValue },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForJsonDetailsToBeVisible()\n\n        // Add a new property using clean API\n        await browserPage.addJsonProperty(newProperty, newValue)\n\n        // Assert\n        await browserPage.waitForJsonPropertyUpdate(newProperty, newValue)\n\n        // Verify original properties are still present\n        await browserPage.verifyJsonPropertyExists('name', initialValue.name)\n        await browserPage.verifyJsonPropertyExists(\n            'age',\n            initialValue.age.toString(),\n        )\n\n        // Verify key length increased\n        const expectedLength = Object.keys(initialValue).length + 1\n        await browserPage.verifyKeyLength(expectedLength.toString())\n    })\n\n    test('should edit entire JSON structure', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const initialValue = {\n            name: faker.person.firstName(),\n            age: faker.number.int({ min: 18, max: 80 }),\n        }\n        const newStructure = {\n            fullName: faker.person.fullName(),\n            email: faker.internet.email(),\n            isActive: true,\n            metadata: {\n                createdAt: new Date().toISOString(),\n                version: 1,\n            },\n        }\n\n        await keyService.addJsonKeyApi(\n            { keyName, value: initialValue },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForJsonDetailsToBeVisible()\n\n        // Edit the entire JSON structure\n        await browserPage.editEntireJsonStructure(JSON.stringify(newStructure))\n\n        // Assert\n        await browserPage.waitForJsonPropertyUpdate(\n            'fullName',\n            newStructure.fullName,\n        )\n        await browserPage.verifyJsonPropertyExists('email', newStructure.email)\n        await browserPage.verifyJsonPropertyExists('isActive', 'true')\n\n        // Verify metadata object and its nested properties exist\n        // The metadata object should contain the nested properties\n        await browserPage.verifyJsonPropertyExists(\n            'createdAt',\n            newStructure.metadata.createdAt,\n        )\n        await browserPage.verifyJsonPropertyExists('version', '1')\n\n        // Verify old properties are no longer present\n        await browserPage.verifyJsonPropertyNotExists('name')\n        await browserPage.verifyJsonPropertyNotExists('age')\n\n        await browserPage.verifyJsonStructureValid()\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit/edit-list-key.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../../pageObjects/browser-page'\nimport { test, expect } from '../../../fixtures/test'\nimport { ossStandaloneConfig } from '../../../helpers/conf'\nimport { AddElementInList } from '../../../helpers/constants'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../../helpers/utils'\n\ntest.describe('Browser - Edit Key Operations - List Key Editing', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should edit list element value successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key with multiple elements\n        const listElements = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const newElementValue = faker.lorem.word()\n\n        await keyService.addListKeyApi(\n            { keyName, elements: listElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial content\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(listElements)\n        await browserPage.verifyKeyLength(listElements.length.toString())\n\n        // Edit the first element value\n        await browserPage.editListElementValue(newElementValue)\n\n        // Verify the element was updated\n        await browserPage.verifyListContainsElements([newElementValue])\n        await browserPage.verifyListDoesNotContainElements([listElements[0]])\n        await browserPage.verifyKeyLength(listElements.length.toString()) // Length should remain the same\n    })\n\n    test('should cancel list element edit successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key\n        const listElements = [faker.lorem.word(), faker.lorem.word()]\n        const attemptedValue = faker.lorem.word()\n\n        await keyService.addListKeyApi(\n            { keyName, elements: listElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and start edit but cancel\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(listElements)\n\n        // Start edit but cancel\n        await browserPage.cancelListElementEdit(attemptedValue)\n\n        // Verify original content is preserved\n        await browserPage.verifyListContainsElements(listElements)\n        await browserPage.verifyListDoesNotContainElements([attemptedValue])\n    })\n\n    test('should add elements to list tail successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key with initial elements\n        const initialElements = [faker.lorem.word(), faker.lorem.word()]\n        const newElements = [faker.lorem.word(), faker.lorem.word()]\n\n        await keyService.addListKeyApi(\n            { keyName, elements: initialElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and add elements to tail\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(initialElements)\n\n        await browserPage.addElementsToList(newElements, AddElementInList.Tail)\n\n        // Verify all elements are present and length is updated\n        const expectedLength = initialElements.length + newElements.length\n        await browserPage.waitForListLengthToUpdate(expectedLength)\n        await browserPage.verifyListContainsElements([\n            ...initialElements,\n            ...newElements,\n        ])\n    })\n\n    test('should add elements to list head successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key with initial elements\n        const initialElements = [faker.lorem.word(), faker.lorem.word()]\n        const newElements = [faker.lorem.word()]\n\n        await keyService.addListKeyApi(\n            { keyName, elements: initialElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and add elements to head\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(initialElements)\n\n        await browserPage.addElementsToList(newElements, AddElementInList.Head)\n\n        // Verify all elements are present and length is updated\n        const expectedLength = initialElements.length + newElements.length\n        await browserPage.waitForListLengthToUpdate(expectedLength)\n        await browserPage.verifyListContainsElements([\n            ...newElements,\n            ...initialElements,\n        ])\n    })\n\n    test('should remove elements from list tail successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key with multiple elements\n        const listElements = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const removeCount = 2\n\n        await keyService.addListKeyApi(\n            { keyName, elements: listElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and remove elements from tail\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(listElements)\n\n        await browserPage.removeListElementsFromTail(removeCount)\n\n        // Verify length is updated (Redis lists remove from the right/tail)\n        const expectedLength = listElements.length - removeCount\n        await browserPage.waitForListLengthToUpdate(expectedLength)\n        await browserPage.verifyKeyLength(expectedLength.toString())\n\n        // Verify the correct elements were removed (last 2 elements should be gone)\n        const remainingElements = listElements.slice(0, -removeCount) // Keep all but last 2\n        const removedElements = listElements.slice(-removeCount) // Last 2 elements\n        await browserPage.verifyListContainsElements(remainingElements)\n        await browserPage.verifyListDoesNotContainElements(removedElements)\n    })\n\n    test('should remove elements from list head successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key with multiple elements\n        const listElements = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const removeCount = 1\n\n        await keyService.addListKeyApi(\n            { keyName, elements: listElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and remove elements from head\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(listElements)\n\n        await browserPage.removeListElementsFromHead(removeCount)\n\n        // Verify length is updated (Redis lists remove from the left/head)\n        const expectedLength = listElements.length - removeCount\n        await browserPage.waitForListLengthToUpdate(expectedLength)\n        await browserPage.verifyKeyLength(expectedLength.toString())\n\n        // Verify the correct elements were removed (first element should be gone)\n        const remainingElements = listElements.slice(removeCount) // Skip first element\n        const removedElements = listElements.slice(0, removeCount) // First element\n        await browserPage.verifyListContainsElements(remainingElements)\n        await browserPage.verifyListDoesNotContainElements(removedElements)\n    })\n\n    test('should handle removing all elements from list', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a list key with a few elements\n        const listElements = [faker.lorem.word(), faker.lorem.word()]\n\n        await keyService.addListKeyApi(\n            { keyName, elements: listElements },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and remove all elements\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListContainsElements(listElements)\n\n        await browserPage.removeListElementsFromTail(listElements.length)\n\n        // Verify list is empty (key should be removed when list becomes empty)\n        await expect\n            .poll(async () => {\n                try {\n                    return await browserPage.isKeyDetailsOpen(keyName)\n                } catch {\n                    return false\n                }\n            })\n            .toBe(false)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit/edit-set-key.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../../pageObjects/browser-page'\nimport { test, expect } from '../../../fixtures/test'\nimport { ossStandaloneConfig } from '../../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../../helpers/utils'\n\ntest.describe('Browser - Edit Key Operations - Set Key Editing', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should add new member to set key successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a set key with initial members\n        const initialMembers = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const newMember = faker.lorem.word()\n\n        await keyService.addSetKeyApi(\n            { keyName, members: initialMembers },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.waitForSetMembersToLoad(initialMembers.length)\n        await browserPage.verifySetContainsMembers(initialMembers)\n        await browserPage.verifyKeyLength(initialMembers.length.toString())\n\n        // Add a new member\n        await browserPage.addMemberToSetKey(newMember)\n\n        // Verify new member appears and length updates\n        await browserPage.waitForSetLengthToUpdate(initialMembers.length + 1)\n        await browserPage.verifySetContainsMembers([\n            ...initialMembers,\n            newMember,\n        ])\n    })\n\n    test('should handle adding duplicate member to set (no length change)', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a set key with initial members\n        const initialMembers = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const duplicateMember = initialMembers[0] // Use existing member\n\n        await keyService.addSetKeyApi(\n            { keyName, members: initialMembers },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.waitForSetMembersToLoad(initialMembers.length)\n        await browserPage.verifySetContainsMembers(initialMembers)\n        await browserPage.verifyKeyLength(initialMembers.length.toString())\n\n        // Try to add duplicate member\n        await browserPage.addMemberToSetKey(duplicateMember)\n\n        // Wait for the operation to complete and verify length remains the same\n        await browserPage.waitForSetMembersToLoad(initialMembers.length)\n        await browserPage.verifyKeyLength(initialMembers.length.toString())\n        await browserPage.verifySetContainsMembers(initialMembers)\n    })\n\n    test('should remove member from set key successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a set key with multiple members\n        const setMembers = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const memberToRemove = setMembers[1]\n\n        await keyService.addSetKeyApi(\n            { keyName, members: setMembers },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state (remove member test)\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.waitForSetMembersToLoad(setMembers.length)\n        await browserPage.verifySetContainsMembers(setMembers)\n        await browserPage.verifyKeyLength(setMembers.length.toString())\n\n        // Remove a member\n        await browserPage.removeMemberFromSet(memberToRemove)\n\n        // Verify member was removed and length updated\n        await browserPage.waitForSetLengthToUpdate(setMembers.length - 1)\n        await browserPage.verifySetDoesNotContainMembers([memberToRemove])\n\n        // Verify other members still exist\n        const remainingMembers = setMembers.filter(\n            (member) => member !== memberToRemove,\n        )\n        await browserPage.verifySetContainsMembers(remainingMembers)\n    })\n\n    test('should search for specific member in set', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a set key with various members\n        const setMembers = ['apple', 'banana', 'orange', 'grape', 'strawberry']\n        const searchTerm = 'apple'\n\n        await keyService.addSetKeyApi(\n            { keyName, members: setMembers },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.waitForSetMembersToLoad(setMembers.length)\n        await browserPage.verifySetContainsMembers(setMembers)\n\n        // Perform search\n        await browserPage.searchByTheValueInSetKey(searchTerm)\n\n        // Verify search input has the search term\n        const searchInput = browserPage.page.getByTestId('search')\n        await expect(searchInput).toHaveValue(searchTerm)\n\n        // Verify search results: searched item visible, others hidden\n        await browserPage.verifySetSearchResults(searchTerm, setMembers)\n\n        // Clear search and verify all members are visible again\n        await browserPage.clearSetSearch()\n        await expect(searchInput).toHaveValue('')\n        await browserPage.verifySetContainsMembers(setMembers)\n    })\n\n    test('should perform mixed operations on set key', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a set key with initial members\n        const initialMembers = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const newMember = faker.lorem.word()\n        const memberToRemove = initialMembers[1]\n\n        await keyService.addSetKeyApi(\n            { keyName, members: initialMembers },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.waitForSetMembersToLoad(initialMembers.length)\n        await browserPage.verifySetContainsMembers(initialMembers)\n        await browserPage.verifyKeyLength(initialMembers.length.toString())\n\n        // Add a new member\n        await browserPage.addMemberToSetKey(newMember)\n        await browserPage.waitForSetLengthToUpdate(initialMembers.length + 1)\n\n        // Remove an existing member\n        await browserPage.removeMemberFromSet(memberToRemove)\n        await browserPage.waitForSetLengthToUpdate(\n            initialMembers.length, // Back to original length\n        )\n\n        // Verify final state: original members (minus removed) + new member\n        const finalExpectedMembers = initialMembers\n            .filter((member) => member !== memberToRemove)\n            .concat(newMember)\n        await browserPage.verifySetContainsMembers(finalExpectedMembers)\n        await browserPage.verifySetDoesNotContainMembers([memberToRemove])\n    })\n\n    test('should handle removing all members from set (key should be deleted)', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a set key with a few members\n        const setMembers = [faker.lorem.word(), faker.lorem.word()]\n\n        await keyService.addSetKeyApi(\n            { keyName, members: setMembers },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify initial state\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.waitForSetMembersToLoad(setMembers.length)\n        await browserPage.verifySetContainsMembers(setMembers)\n\n        // Remove all members\n        await setMembers.reduce(async (promise, member) => {\n            await promise\n            await browserPage.removeMemberFromSet(member)\n        }, Promise.resolve())\n\n        // Verify set is empty (key should be removed when set becomes empty)\n        await expect\n            .poll(async () => {\n                try {\n                    return await browserPage.isKeyDetailsOpen(keyName)\n                } catch {\n                    return false\n                }\n            })\n            .toBe(false)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit/edit-string-key.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../../pageObjects/browser-page'\nimport { test, expect } from '../../../fixtures/test'\nimport { ossStandaloneConfig } from '../../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../../helpers/utils'\n\ntest.describe('Browser - Edit Key Operations - String Key Editing', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should edit string key value successfully', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a string key\n        const originalValue = faker.lorem.words(3)\n        const newValue = faker.lorem.words(4)\n\n        await keyService.addStringKeyApi(\n            { keyName, value: originalValue },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify original value\n        await browserPage.searchByKeyName(keyName)\n        await browserPage.openKeyDetailsByKeyName(keyName)\n\n        const displayedOriginalValue = await browserPage.getStringKeyValue()\n        expect(displayedOriginalValue).toContain(originalValue)\n\n        // Edit the key value\n        await browserPage.editStringKeyValue(newValue)\n\n        // Wait for value and length to update\n        await browserPage.waitForStringValueToUpdate(newValue)\n        await browserPage.waitForKeyLengthToUpdate(newValue.length.toString())\n    })\n\n    test('should cancel string key value edit operation', async ({\n        api: { keyService },\n    }) => {\n        // Arrange: Create a string key\n        const originalValue = faker.lorem.words(3)\n        const attemptedNewValue = faker.lorem.words(4)\n\n        await keyService.addStringKeyApi(\n            { keyName, value: originalValue },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify original value\n        await browserPage.searchByKeyName(keyName)\n        await browserPage.openKeyDetailsByKeyName(keyName)\n\n        const displayedOriginalValue = await browserPage.getStringKeyValue()\n        expect(displayedOriginalValue).toContain(originalValue)\n\n        // Start editing but cancel\n        await browserPage.cancelStringKeyValueEdit(attemptedNewValue)\n\n        // Verify the original value is still displayed\n        const displayedValueAfterCancel = await browserPage.getStringKeyValue()\n        expect(displayedValueAfterCancel).toContain(originalValue)\n        expect(displayedValueAfterCancel).not.toContain(attemptedNewValue)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit/edit-zset-key.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../../pageObjects/browser-page'\nimport { test, expect } from '../../../fixtures/test'\nimport { ossStandaloneConfig } from '../../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../../helpers/utils'\n\ntest.describe('Browser - Edit ZSet Key', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should edit zset member score', async ({ api: { keyService } }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.5 },\n            { name: faker.lorem.word(), score: 2.0 },\n            { name: faker.lorem.word(), score: 3.5 },\n        ]\n        const memberToEdit = zsetMembers[1]\n        const newScore = 5.0\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n        await browserPage.editZsetMemberScore(memberToEdit.name, newScore)\n\n        // Assert\n        await browserPage.verifyZsetMemberScore(memberToEdit.name, newScore)\n        await browserPage.waitForZsetLengthToUpdate(zsetMembers.length) // Length should remain the same\n    })\n\n    test('should cancel zset member score edit', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.5 },\n            { name: faker.lorem.word(), score: 2.0 },\n        ]\n        const memberToEdit = zsetMembers[0]\n        const originalScore = memberToEdit.score\n        const newScore = 10.0\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n        await browserPage.cancelZsetMemberScoreEdit(memberToEdit.name, newScore)\n\n        // Assert - score should remain unchanged\n        await browserPage.verifyZsetMemberScore(\n            memberToEdit.name,\n            originalScore,\n        )\n    })\n\n    test('should add new member to zset', async ({ api: { keyService } }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.0 },\n            { name: faker.lorem.word(), score: 2.0 },\n        ]\n        const newMember = faker.lorem.word()\n        const newScore = 3.5\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n        await browserPage.addMemberToZsetKey(newMember, newScore)\n\n        // Assert\n        const expectedLength = zsetMembers.length + 1\n        await browserPage.waitForZsetLengthToUpdate(expectedLength)\n        await browserPage.verifyZsetMemberExists(newMember)\n        await browserPage.verifyZsetMemberScore(newMember, newScore)\n\n        // Verify all original members are still present\n        const allExpectedMembers = [\n            ...zsetMembers,\n            { name: newMember, score: newScore },\n        ]\n        await browserPage.verifyZsetContainsMembers(allExpectedMembers)\n    })\n\n    test('should handle adding duplicate member to zset (update score)', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.0 },\n            { name: faker.lorem.word(), score: 2.0 },\n        ]\n        const duplicateMember = zsetMembers[0].name\n        const newScore = 5.0\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n        await browserPage.addMemberToZsetKey(duplicateMember, newScore)\n\n        // Assert - length should remain the same (member was updated, not added)\n        await browserPage.waitForZsetScoreToUpdate(newScore)\n        await browserPage.waitForZsetLengthToUpdate(zsetMembers.length)\n        await browserPage.verifyZsetMemberScore(duplicateMember, newScore)\n    })\n\n    test('should remove single member from zset', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.0 },\n            { name: faker.lorem.word(), score: 2.0 },\n            { name: faker.lorem.word(), score: 3.0 },\n        ]\n        const memberToRemove = zsetMembers[1].name\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n        await browserPage.removeMemberFromZset(memberToRemove)\n\n        // Assert\n        const expectedLength = zsetMembers.length - 1\n        await browserPage.waitForZsetLengthToUpdate(expectedLength)\n        await browserPage.verifyZsetMemberNotExists(memberToRemove)\n        await browserPage.verifyZsetDoesNotContainMembers([memberToRemove])\n\n        // Verify remaining members are still present\n        const remainingMembers = zsetMembers.filter(\n            (member) => member.name !== memberToRemove,\n        )\n        await browserPage.verifyZsetContainsMembers(remainingMembers)\n    })\n\n    test('should remove multiple members from zset', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.0 },\n            { name: faker.lorem.word(), score: 2.0 },\n            { name: faker.lorem.word(), score: 3.0 },\n            { name: faker.lorem.word(), score: 4.0 },\n        ]\n        const membersToRemove = [zsetMembers[0].name, zsetMembers[2].name]\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n\n        // Remove members one by one\n        await browserPage.removeMultipleMembersFromZset(membersToRemove)\n\n        // Assert\n        const expectedLength = zsetMembers.length - membersToRemove.length\n        await browserPage.waitForZsetLengthToUpdate(expectedLength)\n\n        // Verify removed members are gone\n        await browserPage.verifyZsetDoesNotContainMembers(membersToRemove)\n\n        // Verify remaining members are still present\n        const remainingMembers = zsetMembers.filter(\n            (member) => !membersToRemove.includes(member.name),\n        )\n        await browserPage.verifyZsetContainsMembers(remainingMembers)\n    })\n\n    test('should remove all members from zset (delete key when empty)', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.0 },\n            { name: faker.lorem.word(), score: 2.0 },\n        ]\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n\n        // Remove all members\n        await browserPage.removeAllZsetMembers(zsetMembers)\n\n        // Assert - key should be deleted when all members are removed\n        const isDetailsClosed = await browserPage.isKeyDetailsClosed()\n        expect(isDetailsClosed).toBe(true)\n        await browserPage.verifyKeyDoesNotExist(keyName)\n    })\n\n    test('should search within zset members', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: 'apple', score: 1.0 },\n            { name: 'banana', score: 2.0 },\n            { name: 'carrot', score: 3.0 },\n            { name: 'date', score: 4.0 },\n        ]\n        const searchTerm = 'apple' // Exact match search\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n\n        // First verify all members are visible before search\n        await browserPage.verifyZsetContainsMembers(zsetMembers)\n\n        // Search for exact member name\n        await browserPage.searchInZsetMembers(searchTerm)\n\n        // For exact match search, only the searched member should be visible\n        const expectedVisibleMembers = zsetMembers.filter(\n            (member) => member.name === searchTerm,\n        )\n        await browserPage.verifyZsetContainsMembers(expectedVisibleMembers)\n\n        // Clear search and verify all members are visible again\n        await browserPage.clearZsetSearch()\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n        await browserPage.verifyZsetContainsMembers(zsetMembers)\n    })\n\n    test('should handle mixed operations on zset', async ({\n        api: { keyService },\n    }) => {\n        // Arrange\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.0 },\n            { name: faker.lorem.word(), score: 2.0 },\n            { name: faker.lorem.word(), score: 3.0 },\n        ]\n        const newMember = faker.lorem.word()\n        const newScore = 4.5\n        const memberToRemove = zsetMembers[1].name\n        const memberToEdit = zsetMembers[0].name\n        const editedScore = 10.0\n\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers },\n            ossStandaloneConfig,\n        )\n\n        // Act\n        await browserPage.openKeyDetailsByKeyName(keyName)\n        await browserPage.waitForZsetMembersToLoad(zsetMembers.length)\n\n        // Add a new member\n        await browserPage.addMemberToZsetKey(newMember, newScore)\n        await browserPage.waitForZsetLengthToUpdate(zsetMembers.length + 1)\n\n        // Edit existing member's score\n        await browserPage.editZsetMemberScore(memberToEdit, editedScore)\n\n        // Remove a member\n        await browserPage.removeMemberFromZset(memberToRemove)\n        await browserPage.waitForZsetLengthToUpdate(zsetMembers.length) // back to original count\n\n        // Assert final state\n        await browserPage.verifyZsetMemberExists(newMember)\n        await browserPage.verifyZsetMemberScore(newMember, newScore)\n        await browserPage.verifyZsetMemberScore(memberToEdit, editedScore)\n        await browserPage.verifyZsetMemberNotExists(memberToRemove)\n\n        // Verify final member composition\n        const expectedFinalMembers = [\n            { name: memberToEdit, score: editedScore },\n            { name: zsetMembers[2].name, score: zsetMembers[2].score },\n            { name: newMember, score: newScore },\n        ]\n        await browserPage.verifyZsetContainsMembers(expectedFinalMembers)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-edit.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../pageObjects/browser-page'\nimport { test, expect } from '../../fixtures/test'\nimport { ossStandaloneConfig } from '../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../helpers/utils'\n\ntest.describe('Browser - Edit Key Operations', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test.describe('Key Name Editing', () => {\n        test('should edit string key name successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a string key\n            const keyValue = faker.lorem.words(3)\n            const newKeyName = `${keyName}_renamed`\n\n            await keyService.addStringKeyApi(\n                { keyName, value: keyValue },\n                ossStandaloneConfig,\n            )\n\n            // Open key details\n            await browserPage.searchByKeyName(keyName)\n            await browserPage.openKeyDetailsByKeyName(keyName)\n\n            // Edit key name\n            await browserPage.editKeyNameButton.click()\n            await browserPage.keyNameInput.clear()\n            await browserPage.keyNameInput.fill(newKeyName)\n            await browserPage.applyButton.click()\n\n            // Verify key name was updated in the details header\n            await expect\n                .poll(async () => {\n                    const keyNameText =\n                        await browserPage.keyNameFormDetails.textContent()\n                    return keyNameText\n                })\n                .toContain(newKeyName)\n\n            // Wait for the key list to update and verify the new key exists\n            await expect\n                .poll(async () => {\n                    await browserPage.searchByKeyName(newKeyName)\n                    return browserPage.isKeyIsDisplayedInTheList(newKeyName)\n                })\n                .toBe(true)\n\n            // Verify the old key name doesn't exist in list\n            await expect\n                .poll(async () => {\n                    await browserPage.searchByKeyName(keyName)\n                    return browserPage.isKeyIsDisplayedInTheList(keyName)\n                })\n                .toBe(false)\n\n            // Update keyName for cleanup\n            keyName = newKeyName\n        })\n\n        test('should cancel key name edit operation', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a string key\n            const keyValue = faker.lorem.words(3)\n            const originalKeyName = keyName\n            const attemptedNewName = `${keyName}_attempted_rename`\n\n            await keyService.addStringKeyApi(\n                { keyName, value: keyValue },\n                ossStandaloneConfig,\n            )\n\n            // Open key details\n            await browserPage.searchByKeyName(keyName)\n            await browserPage.openKeyDetailsByKeyName(keyName)\n\n            // Verify original key name is displayed\n            const displayedOriginalName =\n                await browserPage.keyNameFormDetails.textContent()\n            expect(displayedOriginalName).toContain(originalKeyName)\n\n            // Start editing but cancel\n            await browserPage.editKeyNameButton.click()\n            await browserPage.keyNameInput.clear()\n            await browserPage.keyNameInput.fill(attemptedNewName)\n\n            // Cancel the edit by clicking outside the edit area\n            await browserPage.keyDetailsHeader.click()\n\n            // Verify the original key name is still displayed (edit was cancelled)\n            const displayedNameAfterCancel =\n                await browserPage.keyNameFormDetails.textContent()\n            expect(displayedNameAfterCancel).toContain(originalKeyName)\n            expect(displayedNameAfterCancel).not.toContain(attemptedNewName)\n\n            // Verify the original key still exists in the list\n            await browserPage.searchByKeyName(originalKeyName)\n            const originalKeyExists =\n                await browserPage.isKeyIsDisplayedInTheList(originalKeyName)\n            expect(originalKeyExists).toBe(true)\n\n            // Verify the attempted new name doesn't exist\n            await browserPage.searchByKeyName(attemptedNewName)\n            const attemptedKeyExists =\n                await browserPage.isKeyIsDisplayedInTheList(attemptedNewName)\n            expect(attemptedKeyExists).toBe(false)\n        })\n    })\n\n    test.describe('TTL Editing', () => {\n        test('should edit string key TTL successfully', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a string key with TTL\n            const keyValue = faker.lorem.words(3)\n            const initialTTL = 3600 // 1 hour\n            const newTTL = 7200 // 2 hours\n\n            await keyService.addStringKeyApi(\n                { keyName, value: keyValue, expire: initialTTL },\n                ossStandaloneConfig,\n            )\n\n            // Open key details and verify initial TTL\n            await browserPage.openKeyDetailsAndVerify(keyName)\n            await browserPage.verifyTTLIsNotPersistent()\n\n            // Edit the TTL and verify update\n            await browserPage.editKeyTTLValue(newTTL)\n            await browserPage.waitForTTLToUpdate(initialTTL)\n            await browserPage.verifyTTLIsWithinRange(newTTL)\n        })\n\n        test('should remove TTL from string key (set to persistent)', async ({\n            api: { keyService },\n        }) => {\n            // Arrange: Create a string key with TTL\n            const keyValue = faker.lorem.words(3)\n            const initialTTL = 3600 // 1 hour\n\n            await keyService.addStringKeyApi(\n                { keyName, value: keyValue, expire: initialTTL },\n                ossStandaloneConfig,\n            )\n\n            // Open key details and verify initial TTL\n            await browserPage.openKeyDetailsAndVerify(keyName)\n            await browserPage.verifyTTLIsNotPersistent()\n\n            // Remove TTL and verify it becomes persistent\n            await browserPage.removeKeyTTL()\n            await browserPage.verifyTTLIsPersistent()\n        })\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/browser/keys-read.spec.ts",
    "content": "import { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../../pageObjects/browser-page'\nimport { test } from '../../fixtures/test'\nimport { ossStandaloneConfig } from '../../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../../helpers/utils'\n\ntest.describe('Browser - Read Key Details', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key if it exists\n        try {\n            await keyService.deleteKeyByNameApi(\n                keyName,\n                ossStandaloneConfig.databaseName,\n            )\n        } catch (error) {\n            // Key might already be deleted in test, ignore error\n        }\n\n        await cleanupInstance()\n    })\n\n    test('should open key details when clicking on string key', async ({\n        api: { keyService },\n    }) => {\n        // Arrange test data\n        const keyValue = faker.lorem.words(3)\n        const keyTTL = 3600 // 1 hour\n\n        // Create a string key with TTL using API\n        await keyService.addStringKeyApi(\n            { keyName, value: keyValue, expire: keyTTL },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyStringKeyContent(keyValue)\n        await browserPage.verifyKeyLength(`${keyValue.length}`)\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n\n    test('should open key details when clicking on hash key', async ({\n        api: { keyService },\n    }) => {\n        const fieldName = faker.string.alphanumeric(8)\n        const fieldValue = faker.lorem.words(2)\n        const keyTTL = 7200 // 2 hours\n\n        // Create a hash key with TTL using API\n        await keyService.addHashKeyApi(\n            {\n                keyName,\n                fields: [{ field: fieldName, value: fieldValue }],\n                expire: keyTTL,\n            },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyHashKeyContent(fieldName, fieldValue)\n        await browserPage.verifyKeyLength('1') // We created 1 field\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n\n    test('should open key details when clicking on list key', async ({\n        api: { keyService },\n    }) => {\n        const listElements = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const keyTTL = 3600 // 1 hour\n\n        // Create a list key with multiple elements using API\n        await keyService.addListKeyApi(\n            { keyName, elements: listElements, expire: keyTTL },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyListKeyContent(listElements)\n        await browserPage.verifyKeyLength(listElements.length.toString())\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n\n    test('should open key details when clicking on set key', async ({\n        api: { keyService },\n    }) => {\n        const setMembers = [\n            faker.lorem.word(),\n            faker.lorem.word(),\n            faker.lorem.word(),\n        ]\n        const keyTTL = 3600 // 1 hour\n\n        // Create a set key with multiple members and TTL using API\n        await keyService.addSetKeyApi(\n            { keyName, members: setMembers, expire: keyTTL },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifySetKeyContent(setMembers)\n        await browserPage.verifyKeyLength(setMembers.length.toString())\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n\n    test('should open key details when clicking on zset key', async ({\n        api: { keyService },\n    }) => {\n        const zsetMembers = [\n            { name: faker.lorem.word(), score: 1.5 },\n            { name: faker.lorem.word(), score: 2.0 },\n            { name: faker.lorem.word(), score: 10 },\n        ]\n        const keyTTL = 3600 // 1 hour\n\n        // Create a zset key with multiple members and TTL using API\n        await keyService.addZSetKeyApi(\n            { keyName, members: zsetMembers, expire: keyTTL },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyZsetKeyContent(zsetMembers)\n        await browserPage.verifyKeyLength(zsetMembers.length.toString())\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n\n    test('should open key details when clicking on json key', async ({\n        api: { keyService },\n    }) => {\n        const jsonValue = {\n            name: faker.person.fullName(),\n            age: faker.number.int({ min: 18, max: 80 }),\n            active: true,\n            hobbies: [faker.lorem.word(), faker.lorem.word()],\n            address: {\n                street: faker.location.streetAddress(),\n                city: faker.location.city(),\n            },\n        }\n        const keyTTL = 1800 // 30 minutes\n\n        // Create a JSON key with TTL using API\n        await keyService.addJsonKeyApi(\n            { keyName, value: jsonValue, expire: keyTTL },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyJsonKeyContent(jsonValue)\n        await browserPage.verifyKeyLength(\n            Object.keys(jsonValue).length.toString(),\n        )\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n\n    test('should open key details when clicking on stream key', async ({\n        api: { keyService },\n    }) => {\n        const streamEntries = [\n            {\n                id: '*',\n                fields: [\n                    { name: 'temperature', value: '25.5' },\n                    { name: 'humidity', value: '60' },\n                    { name: 'location', value: 'sensor-001' },\n                ],\n            },\n            {\n                id: '*',\n                fields: [\n                    { name: 'temperature', value: '26.2' },\n                    { name: 'humidity', value: '58' },\n                    { name: 'location', value: 'sensor-002' },\n                ],\n            },\n        ]\n        const keyTTL = 7200 // 2 hours\n\n        // Create a stream key with multiple entries and TTL using API\n        await keyService.addStreamKeyApi(\n            { keyName, entries: streamEntries, expire: keyTTL },\n            ossStandaloneConfig,\n        )\n\n        // Open key details and verify all aspects\n        await browserPage.openKeyDetailsAndVerify(keyName)\n        await browserPage.verifyStreamKeyContent(streamEntries)\n        await browserPage.verifyKeyLength(streamEntries.length.toString())\n        await browserPage.verifyKeySize()\n        await browserPage.verifyKeyTTL(keyTTL)\n        await browserPage.closeKeyDetailsAndVerify()\n    })\n})\n"
  },
  {
    "path": "tests/playwright/tests/keys.spec.ts",
    "content": "/* eslint-disable no-empty-pattern */\nimport { faker } from '@faker-js/faker'\n\nimport { BrowserPage } from '../pageObjects/browser-page'\nimport { test, expect } from '../fixtures/test'\nimport { ossStandaloneConfig } from '../helpers/conf'\nimport {\n    addStandaloneInstanceAndNavigateToIt,\n    navigateToStandaloneInstance,\n} from '../helpers/utils'\n\ntest.describe('Adding Database Keys', () => {\n    let browserPage: BrowserPage\n    let keyName: string\n    let cleanupInstance: () => Promise<void>\n\n    test.beforeEach(async ({ page, api: { databaseService } }) => {\n        browserPage = new BrowserPage(page)\n        keyName = faker.string.alphanumeric(10)\n        cleanupInstance = await addStandaloneInstanceAndNavigateToIt(\n            page,\n            databaseService,\n        )\n\n        await navigateToStandaloneInstance(page)\n    })\n\n    test.afterEach(async ({ api: { keyService } }) => {\n        // Clean up: delete the key\n        await keyService.deleteKeyByNameApi(\n            keyName,\n            ossStandaloneConfig.databaseName,\n        )\n\n        await cleanupInstance()\n    })\n\n    test('Verify that user can add Hash Key', async ({}) => {\n        await browserPage.addHashKey(keyName)\n\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName)\n        const isKeyIsDisplayedInTheList =\n            await browserPage.isKeyIsDisplayedInTheList(keyName)\n        await expect(isKeyIsDisplayedInTheList).toBe(true)\n    })\n\n    test('Verify that user can add Set Key', async ({}) => {\n        await browserPage.addSetKey(keyName)\n\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName)\n        const isKeyIsDisplayedInTheList =\n            await browserPage.isKeyIsDisplayedInTheList(keyName)\n        await expect(isKeyIsDisplayedInTheList).toBe(true)\n    })\n\n    test('Verify that user can add List Key', async ({}) => {\n        await browserPage.addListKey(keyName)\n\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName)\n        const isKeyIsDisplayedInTheList =\n            await browserPage.isKeyIsDisplayedInTheList(keyName)\n        await expect(isKeyIsDisplayedInTheList).toBe(true)\n    })\n\n    test('Verify that user can add String Key', async ({}) => {\n        await browserPage.addStringKey(keyName)\n\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName)\n        const isKeyIsDisplayedInTheList =\n            await browserPage.isKeyIsDisplayedInTheList(keyName)\n        await expect(isKeyIsDisplayedInTheList).toBe(true)\n    })\n\n    test('Verify that user can add ZSet Key', async ({}) => {\n        const scores = '111'\n        await browserPage.addZSetKey(keyName, scores)\n\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName)\n        const isKeyIsDisplayedInTheList =\n            await browserPage.isKeyIsDisplayedInTheList(keyName)\n        await expect(isKeyIsDisplayedInTheList).toBe(true)\n    })\n\n    test('Verify that user can add Stream key', async ({}) => {\n        const keyField = faker.string.alphanumeric(20)\n        const keyValue = faker.string.alphanumeric(20)\n\n        await browserPage.addStreamKey(keyName, keyField, keyValue)\n\n        // Check that new key is displayed in the list\n        await browserPage.searchByKeyName(keyName)\n        const isKeyIsDisplayedInTheList =\n            await browserPage.isKeyIsDisplayedInTheList(keyName)\n        await expect(isKeyIsDisplayedInTheList).toBe(true)\n    })\n})\n"
  },
  {
    "path": "tests/playwright/types/connections.ts",
    "content": "export type SentinelParameters = {\n    sentinelHost: string\n    sentinelPort: string\n    masters?: {\n        alias?: string\n        db?: string\n        name?: string\n        password?: string\n    }[]\n    sentinelPassword?: string\n    name?: string[]\n}\n\nexport type SSHParameters = {\n    sshHost: string\n    sshPort: string\n    sshUsername: string\n    sshPassword?: string\n    sshPrivateKey?: string\n    sshPassphrase?: string\n}\n"
  },
  {
    "path": "tests/playwright/types/databases.ts",
    "content": "export type DatabasesForImport = {\n    host?: string\n    port?: number | string\n    name?: string\n    result?: string\n    username?: string\n    auth?: string\n    cluster?: boolean | string\n    indName?: string\n    db?: number\n    ssh_port?: number\n    timeout_connect?: number\n    timeout_execute?: number\n    other_field?: string\n    ssl?: boolean\n    ssl_ca_cert_path?: string\n    ssl_local_cert_path?: string\n    ssl_private_key_path?: string\n}[]\n\nexport type AddNewDatabaseParameters = {\n    host?: string\n    port?: string\n    databaseName?: string\n    databaseUsername?: string\n    databasePassword?: string\n\n    // For OSS Cluster parameters, you might use these fields:\n    ossClusterHost?: string\n    ossClusterPort?: string\n    ossClusterDatabaseName?: string\n    caCert?: {\n        name?: string\n        certificate?: string\n    }\n    clientCert?: {\n        name?: string\n        certificate?: string\n        key?: string\n    }\n    accessKey?: string\n    secretKey?: string\n\n    // For OSS Sentinel parameters, you might use these fields:\n    name?: string[]\n    sentinelHost?: string\n    sentinelPort?: string\n    sentinelPassword?: string\n    masters?: {\n        alias: string\n        db: string\n        name: string\n        password: string\n    }[]\n}\n\nexport type DatabaseInstance = {\n    host: string\n    port: number\n    provider?: string\n    id: string\n    connectionType?: string\n    lastConnection?: Date\n    password?: string\n    username?: string\n    name?: string\n    db?: number\n    tls?: boolean\n    ssh?: boolean\n    sshOptions?: {\n        host: string\n        port: number\n        username?: string\n        password?: string | true\n        privateKey?: string\n        passphrase?: string | true\n    }\n    tlsClientAuthRequired?: boolean\n    verifyServerCert?: boolean\n    caCert?: object\n    clientCert?: object\n    authUsername?: string\n    authPass?: string\n    isDeleting?: boolean\n    sentinelMaster?: object\n    modules: object[]\n    version: string\n    isRediStack?: boolean\n    visible?: boolean\n    loading?: boolean\n    isFreeDb?: boolean\n    tags?: {\n        id: string\n        key: string\n        value: string\n        createdAt: string\n        updatedAt: string\n    }[]\n}\n"
  },
  {
    "path": "tests/playwright/types/index.ts",
    "content": "export * from './databases'\nexport * from './connections'\nexport * from './keys'\nexport * from './rdi'\nexport * from './indexes'\n\ndeclare global {\n    interface Window {\n        windowId?: string\n    }\n}\n"
  },
  {
    "path": "tests/playwright/types/indexes.ts",
    "content": "export interface RedisearchIndexField {\n    name: string\n    type: 'TEXT' | 'TAG' | 'NUMERIC' | 'GEO' | 'GEOSHAPE' | 'VECTOR'\n}\n\nexport interface CreateRedisearchIndexParameters {\n    indexName: string\n    keyType: 'HASH' | 'JSON'\n    prefixes?: string[]\n    fields: RedisearchIndexField[]\n}\n"
  },
  {
    "path": "tests/playwright/types/keys.ts",
    "content": "/**\n * Add new keys parameters\n * @param keyName The name of the key\n * @param TTL The ttl of the key\n * @param value The value of the key\n * @param members The members of the key\n * @param scores The scores of the key member\n * @param field The field of the key\n */\nexport type AddNewKeyParameters = {\n    keyName: string\n    value?: string\n    TTL?: string\n    members?: string\n    scores?: string\n    field?: string\n    fields?: [\n        {\n            field?: string\n            valuse?: string\n        },\n    ]\n}\n\n/**\n * Hash key parameters\n * @param keyName The name of the key\n * @param fields The Array with fields\n * @param field The field of the field\n * @param value The value of the field\n\n */\nexport type HashKeyParameters = {\n    keyName: string\n    fields: {\n        field: string\n        value: string\n    }[]\n}\n\n/**\n * Stream key parameters\n * @param keyName The name of the key\n * @param entries The Array with entries\n * @param id The id of entry\n * @param fields The Array with fields\n */\nexport type StreamKeyParameters = {\n    keyName: string\n    entries: {\n        id: string\n        fields: {\n            name: string\n            value: string\n        }[]\n    }[]\n}\n\n/**\n * Set key parameters\n * @param keyName The name of the key\n * @param members The Array with members\n */\nexport type SetKeyParameters = {\n    keyName: string\n    members: string[]\n}\n\n/**\n * Sorted Set key parameters\n * @param keyName The name of the key\n * @param members The Array with members\n * @param name The name of the member\n * @param id The id of the member\n */\nexport type SortedSetKeyParameters = {\n    keyName: string\n    members: {\n        name: string\n        score: number\n    }[]\n}\n\n/**\n * List key parameters\n * @param keyName The name of the key\n * @param element The element in list\n */\nexport type ListKeyParameters = {\n    keyName: string\n    element: string\n}\n\n/**\n * String key parameters\n * @param keyName The name of the key\n * @param value The value in the string\n */\nexport type StringKeyParameters = {\n    keyName: string\n    value: string\n}\n\n/**\n * The key arguments for multiple keys/fields adding\n * @param keysCount The number of keys to add\n * @param fieldsCount The number of fields in key to add\n * @param elementsCount The number of elements in key to add\n * @param membersCount The number of members in key to add\n * @param keyName The full key name\n * @param keyNameStartWith The name of key should start with\n * @param fieldStartWitht The name of field should start with\n * @param fieldValueStartWith The name of field value should start with\n * @param elementStartWith The name of element should start with\n * @param memberStartWith The name of member should start with\n */\n\nexport type AddKeyArguments = {\n    keysCount?: number\n    fieldsCount?: number\n    elementsCount?: number\n    membersCount?: number\n    keyName?: string\n    keyNameStartWith?: string\n    fieldStartWith?: string\n    fieldValueStartWith?: string\n    elementStartWith?: string\n    memberStartWith?: string\n}\n\n/**\n * Keys Data parameters\n * @param textType The type of the key\n * @param keyName The name of the key\n */\nexport type KeyData = {\n    textType: string\n    keyName: string\n}[]\n"
  },
  {
    "path": "tests/playwright/types/rdi.ts",
    "content": "export type RdiInstance = {\n    alias: string\n    url: string\n    version?: string\n    lastConnection?: string\n    username?: string\n    password?: string\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"dom\", \"esnext\", \"WebWorker\"],\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react\",\n    \"strict\": true,\n    \"pretty\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \"./\",\n    /* Additional Checks */\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    /* Module Resolution Options */\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"resolveJsonModule\": true,\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"types\": [\"node\", \"vite/client\"],\n    \"paths\": {\n      \"uiSrc/*\": [\"redisinsight/ui/src/*\"],\n      \"apiSrc/*\": [\"redisinsight/api/src/*\"],\n      \"src/*\": [\"redisinsight/api/src/*\"],\n      \"desktopSrc/*\": [\"redisinsight/desktop/src/*\"],\n      \"tests/*\": [\"redisinsight/ui/__tests__/*\"]\n    }\n  },\n  \"include\": [\n    \"redisinsight/ui/src/**/*\",\n    \"redisinsight/ui/index.tsx\",\n    \"redisinsight/ui/indexElectron.tsx\",\n    \"redisinsight/desktop/**/*\",\n    \"redisinsight/ui/vite-env.d.ts\",\n    \"jest.config.cjs\",\n    \"tests/playwright/**/*\",\n    \"stories/**/*\"\n  ],\n  \"exclude\": [\n    \"redisinsight/desktop/dll/*\",\n    \"./redisinsight/api\",\n    \"**/main.prod.js\",\n    \"**/renderer.prod.js\",\n    \"./release\",\n    \"./node_modules\",\n    \"**/node_modules\",\n    \"./dist\",\n    \"**/dist\"\n  ]\n}\n"
  }
]